diff --git a/.github/workflows/compile-rtk-firmware.yml b/.github/workflows/compile-rtk-firmware.yml new file mode 100644 index 000000000..91b0a89e3 --- /dev/null +++ b/.github/workflows/compile-rtk-firmware.yml @@ -0,0 +1,187 @@ +name: Build RTK Firmware +on: + workflow_dispatch: + branches: + +env: + FILENAME_PREFIX: RTK_Surveyor_Firmware + FIRMWARE_VERSION_MAJOR: 4 + FIRMWARE_VERSION_MINOR: 2 + POINTPERFECT_TOKEN: ${{ secrets.POINTPERFECT_TOKEN }} + +jobs: + build: + + name: Build + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@master + + - name: Get current date + id: date + run: echo "date=$(date +'%b_%d_%Y')" >> $GITHUB_OUTPUT + + - name: Get current date + id: dateNoScores + run: echo "dateNoScores=$(date +'%b %d %Y')" >> $GITHUB_OUTPUT + + - name: Extract branch name + run: echo "BRANCH=${{github.ref_name}}" >> $GITHUB_ENV + + #File_Name_v3_1.bin + #File_Name_RC-Jan_26_2023.bin + - name: Create file ending and compiler flags based on branch + run: | + if [[ $BRANCH == 'main' ]]; then + echo "FILE_ENDING_UNDERSCORE=_v${{ env.FIRMWARE_VERSION_MAJOR }}_${{ env.FIRMWARE_VERSION_MINOR }}" >> "$GITHUB_ENV" + echo "FILE_ENDING_NOUNDERSCORE=_v${{ env.FIRMWARE_VERSION_MAJOR }}.${{ env.FIRMWARE_VERSION_MINOR }}" >> "$GITHUB_ENV" + echo "JSON_ENDING=" >> "$GITHUB_ENV" + echo "JSON_FILE_NAME=RTK-Firmware.json" >> "$GITHUB_ENV" + echo "ENABLE_DEVELOPER=false" >> "$GITHUB_ENV" + echo "DEBUG_LEVEL=none" >> "$GITHUB_ENV" + else + echo "FILE_ENDING_UNDERSCORE=_RC-${{ steps.date.outputs.date }}" >> "$GITHUB_ENV" + echo "FILE_ENDING_NOUNDERSCORE=_RC-${{ steps.dateNoScores.outputs.dateNoScores }}" >> "$GITHUB_ENV" + echo "JSON_ENDING=-${{ steps.dateNoScores.outputs.dateNoScores }}" >> "$GITHUB_ENV" + echo "JSON_FILE_NAME=RTK-RC-Firmware.json" >> "$GITHUB_ENV" + echo "ENABLE_DEVELOPER=true" >> "$GITHUB_ENV" + echo "DEBUG_LEVEL=debug" >> "$GITHUB_ENV" + fi + + - name: Setup Arduino CLI + uses: arduino/setup-arduino-cli@v1 + + - name: Start config file + run: arduino-cli config init --additional-urls "https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json" + + - name: Update index + run: arduino-cli core update-index + + #We limit the ESP32 core to v2.0.2 + - name: Install platform + run: arduino-cli core install esp32:esp32@2.0.2 + + - name: Get Known Libraries + run: arduino-cli lib install + ArduinoJson@6.19.4 + ESP32Time@2.0.0 + ESP32_BleSerial@1.0.5 + "ESP32-OTA-Pull"@1.0.0 + Ethernet@2.0.2 + JC_Button@2.1.2 + PubSubClient@2.8.0 + "SdFat"@2.1.1 + "SparkFun LIS2DH12 Arduino Library"@1.0.3 + "SparkFun MAX1704x Fuel Gauge Arduino Library"@1.0.4 + "SparkFun u-blox GNSS v3"@3.0.14 + SparkFun_WebServer_ESP32_W5500@1.5.5 + SSLClientESP32@2.0.0 + + - name: Enable external libs + run: arduino-cli config set library.enable_unsafe_install true + + - name: Get Libraries + run: arduino-cli lib install --git-url + https://github.com/sparkfun/SparkFun_Qwiic_OLED_Arduino_Library.git + https://github.com/me-no-dev/ESPAsyncWebServer.git + https://github.com/me-no-dev/AsyncTCP.git + + #Incorporate ESP-Now patch into core: https://github.com/espressif/arduino-esp32/pull/7044/files + #- name: Patch ESP32 Core + # run: | + # cd Firmware/RTK_Surveyor/Patch/ + # cp WiFiGeneric.cpp /home/runner/.arduino15/packages/esp32/hardware/esp32/2.0.2/libraries/WiFi/src/WiFiGeneric.cpp + + #Patch Server.h to avoid https://github.com/arduino-libraries/Ethernet/issues/88#issuecomment-455498941 + #Note: this patch can be removed if/when we upgrade to ESP32 core >= v2.0.6 + - name: Patch ESP32 Server.h for Ethernet + run: | + cd Firmware/RTK_Surveyor/Patch/ + cp Server.h /home/runner/.arduino15/packages/esp32/hardware/esp32/2.0.2/cores/esp32/Server.h + + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + # Configure Python - now we have Python installed, we need to provide everything needed by esptool otherwise the compile fails + - name: Configure Python + run: | + pip3 install pyserial + + - name: Update index_html + run: | + cd Firmware/Tools + python index_html_zipper.py ../RTK_Surveyor/AP-Config/index.html ../RTK_Surveyor/form.h + + - name: Update main_js + run: | + cd Firmware/Tools + python main_js_zipper.py ../RTK_Surveyor/AP-Config/src/main.js ../RTK_Surveyor/form.h + + - name: Commit and push form.h + uses: actions-js/push@master + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + directory: ./Firmware/RTK_Surveyor + branch: ${{ env.BRANCH }} + message: 'Update form.h via Python' + + - name: Copy custom app3M_fat9M_16MB.csv + run: + cp Firmware/app3M_fat9M_16MB.csv /home/runner/.arduino15/packages/esp32/hardware/esp32/2.0.2/tools/partitions/app3M_fat9M_16MB.csv + + - name: Compile Sketch + run: arduino-cli compile --fqbn "esp32:esp32:esp32":DebugLevel=${{ env.DEBUG_LEVEL }} ./Firmware/RTK_Surveyor/RTK_Surveyor.ino + --build-property build.partitions=app3M_fat9M_16MB + --build-property upload.maximum_size=3145728 + --build-property "compiler.cpp.extra_flags=\"-DPOINTPERFECT_TOKEN=$POINTPERFECT_TOKEN\" \"-DFIRMWARE_VERSION_MAJOR=$FIRMWARE_VERSION_MAJOR\" \"-DFIRMWARE_VERSION_MINOR=$FIRMWARE_VERSION_MINOR\" \"-DENABLE_DEVELOPER=${{ env.ENABLE_DEVELOPER }}\"" + --export-binaries + + - name: Rename binary + run: | + cd Firmware/RTK_Surveyor/build/esp32.esp32.esp32/ + mv RTK_Surveyor.ino.bin ${{ env.FILENAME_PREFIX }}${{ env.FILE_ENDING_UNDERSCORE }}.bin + + - name: Upload binary to action + uses: actions/upload-artifact@v4 + with: + name: ${{ env.FILENAME_PREFIX }}${{ env.FILE_ENDING_UNDERSCORE }} + path: ./Firmware/RTK_Surveyor/build/esp32.esp32.esp32/${{ env.FILENAME_PREFIX }}${{ env.FILE_ENDING_UNDERSCORE }}.bin + + + - name: Push binary to Binaries Repo + # uses: dmnemec/copy_file_to_another_repo_action #Workaround for Issue: https://github.com/orgs/community/discussions/55820#discussioncomment-5946136 + uses: Jason2866/copy_file_to_another_repo_action@http408_fix + env: + API_TOKEN_GITHUB: ${{ secrets.API_GITHUB_RTK_FILE_TOKEN }} + with: + source_file: ./Firmware/RTK_Surveyor/build/esp32.esp32.esp32/${{ env.FILENAME_PREFIX }}${{ env.FILE_ENDING_UNDERSCORE }}.bin + destination_repo: 'sparkfun/SparkFun_RTK_Firmware_Binaries' + destination_folder: '' + user_email: 'nathan@sparkfun.com' + user_name: 'nseidle' + commit_message: 'Github Action - Updating Binary ${{ steps.dateNoScores.outputs.dateNoScores }}' + + - name: Update JSON File + uses: "DamianReeves/write-file-action@master" + with: + path: ${{ env.JSON_FILE_NAME }} + write-mode: overwrite + contents: | + {"Configurations": [{"Version":"${{ env.FIRMWARE_VERSION_MAJOR }}.${{ env.FIRMWARE_VERSION_MINOR }}${{ env.JSON_ENDING }}", "URL":"https://raw.githubusercontent.com/sparkfun/SparkFun_RTK_Firmware_Binaries/main/${{ env.FILENAME_PREFIX }}${{ env.FILE_ENDING_UNDERSCORE }}.bin"}]} + + - name: Push JSON to Binaries Repo + # uses: dmnemec/copy_file_to_another_repo_action #Workaround for Issue: https://github.com/orgs/community/discussions/55820#discussioncomment-5946136 + uses: Jason2866/copy_file_to_another_repo_action@http408_fix + env: + API_TOKEN_GITHUB: ${{ secrets.API_GITHUB_RTK_FILE_TOKEN }} + with: + source_file: ${{ env.JSON_FILE_NAME }} + destination_repo: 'sparkfun/SparkFun_RTK_Firmware_Binaries' + destination_folder: '' + user_email: 'nathan@sparkfun.com' + user_name: 'nseidle' + commit_message: 'Github Action - Updating JSON ${{ steps.dateNoScores.outputs.dateNoScores }}' \ No newline at end of file diff --git a/.github/workflows/mkdocs.yml b/.github/workflows/mkdocs.yml new file mode 100644 index 000000000..2eb741aed --- /dev/null +++ b/.github/workflows/mkdocs.yml @@ -0,0 +1,16 @@ +name: Run mkdocs +on: + push: + branches: + - main +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: 3.12.0 + - run: pip install mkdocs-material mkdocs-monorepo-plugin setuptools + - run: pip install mkdocs-with-pdf + - run: mkdocs gh-deploy --force diff --git a/.github/workflows/non-release-build.yml b/.github/workflows/non-release-build.yml new file mode 100644 index 000000000..6a90ab236 --- /dev/null +++ b/.github/workflows/non-release-build.yml @@ -0,0 +1,150 @@ +name: RTK Firmware Non-Release Build +on: + workflow_dispatch: + branches: + +env: + FILENAME_PREFIX: RTK_Surveyor_Firmware + FIRMWARE_VERSION_MAJOR: 99 + FIRMWARE_VERSION_MINOR: 99 + POINTPERFECT_TOKEN: ${{ secrets.POINTPERFECT_TOKEN }} + +jobs: + build: + + name: Build + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@master + + - name: Get current date + id: date + run: echo "date=$(date +'%b_%d_%Y')" >> $GITHUB_OUTPUT + + - name: Get current date + id: dateNoScores + run: echo "dateNoScores=$(date +'%b %d %Y')" >> $GITHUB_OUTPUT + + - name: Extract branch name + run: echo "BRANCH=${{github.ref_name}}" >> $GITHUB_ENV + + #File_Name_v3_1.bin + #File_Name_RC-Jan_26_2023.bin + - name: Create file ending and compiler flags based on branch + run: | + echo "FILE_ENDING_UNDERSCORE=_RC-${{ steps.date.outputs.date }}" >> "$GITHUB_ENV" + echo "FILE_ENDING_NOUNDERSCORE=_RC-${{ steps.dateNoScores.outputs.dateNoScores }}" >> "$GITHUB_ENV" + echo "JSON_ENDING=-${{ steps.dateNoScores.outputs.dateNoScores }}" >> "$GITHUB_ENV" + echo "JSON_FILE_NAME=RTK-RC-Firmware.json" >> "$GITHUB_ENV" + echo "ENABLE_DEVELOPER=true" >> "$GITHUB_ENV" + echo "DEBUG_LEVEL=debug" >> "$GITHUB_ENV" + + - name: Setup Arduino CLI + uses: arduino/setup-arduino-cli@v1 + + - name: Start config file + run: arduino-cli config init --additional-urls "https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json" + + - name: Update index + run: arduino-cli core update-index + + #We limit the ESP32 core to v2.0.2 + - name: Install platform + run: arduino-cli core install esp32:esp32@2.0.2 + + - name: Get Known Libraries + run: arduino-cli lib install + ArduinoJson@6.19.4 + ESP32Time@2.0.0 + ESP32_BleSerial@1.0.5 + "ESP32-OTA-Pull"@1.0.0 + Ethernet@2.0.2 + JC_Button@2.1.2 + PubSubClient@2.8.0 + "SdFat"@2.1.1 + "SparkFun LIS2DH12 Arduino Library"@1.0.3 + "SparkFun MAX1704x Fuel Gauge Arduino Library"@1.0.4 + "SparkFun u-blox GNSS v3"@3.0.14 + SparkFun_WebServer_ESP32_W5500@1.5.5 + SSLClientESP32@2.0.0 + + - name: Enable external libs + run: arduino-cli config set library.enable_unsafe_install true + + - name: Get Libraries + run: arduino-cli lib install --git-url + https://github.com/sparkfun/SparkFun_Qwiic_OLED_Arduino_Library.git + https://github.com/me-no-dev/ESPAsyncWebServer.git + https://github.com/me-no-dev/AsyncTCP.git + + #Incorporate ESP-Now patch into core: https://github.com/espressif/arduino-esp32/pull/7044/files + #- name: Patch ESP32 Core + # run: | + # cd Firmware/RTK_Surveyor/Patch/ + # cp WiFiGeneric.cpp /home/runner/.arduino15/packages/esp32/hardware/esp32/2.0.2/libraries/WiFi/src/WiFiGeneric.cpp + + #Patch Server.h to avoid https://github.com/arduino-libraries/Ethernet/issues/88#issuecomment-455498941 + #Note: this patch can be removed if/when we upgrade to ESP32 core >= v2.0.6 + - name: Patch ESP32 Server.h for Ethernet + run: | + cd Firmware/RTK_Surveyor/Patch/ + cp Server.h /home/runner/.arduino15/packages/esp32/hardware/esp32/2.0.2/cores/esp32/Server.h + + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + # Configure Python - now we have Python installed, we need to provide everything needed by esptool otherwise the compile fails + - name: Configure Python + run: | + pip3 install pyserial + + - name: Update index_html + run: | + cd Firmware/Tools + python index_html_zipper.py ../RTK_Surveyor/AP-Config/index.html ../RTK_Surveyor/form.h + + - name: Update main_js + run: | + cd Firmware/Tools + python main_js_zipper.py ../RTK_Surveyor/AP-Config/src/main.js ../RTK_Surveyor/form.h + + - name: Commit and push form.h + uses: actions-js/push@master + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + directory: ./Firmware/RTK_Surveyor + branch: ${{ env.BRANCH }} + message: 'Update form.h via Python' + + - name: Copy custom app3M_fat9M_16MB.csv + run: + cp Firmware/app3M_fat9M_16MB.csv /home/runner/.arduino15/packages/esp32/hardware/esp32/2.0.2/tools/partitions/app3M_fat9M_16MB.csv + + - name: Compile Sketch + run: arduino-cli compile --fqbn "esp32:esp32:esp32":DebugLevel=${{ env.DEBUG_LEVEL }} ./Firmware/RTK_Surveyor/RTK_Surveyor.ino + --build-property build.partitions=app3M_fat9M_16MB + --build-property upload.maximum_size=3145728 + --build-property "compiler.cpp.extra_flags=\"-DPOINTPERFECT_TOKEN=$POINTPERFECT_TOKEN\" \"-DFIRMWARE_VERSION_MAJOR=$FIRMWARE_VERSION_MAJOR\" \"-DFIRMWARE_VERSION_MINOR=$FIRMWARE_VERSION_MINOR\" \"-DENABLE_DEVELOPER=${{ env.ENABLE_DEVELOPER }}\"" + --export-binaries + + - name: Create artifact name + run: | + echo "ARTIFACT=${{ env.FILENAME_PREFIX }}${{ env.FILE_ENDING_UNDERSCORE }}" >> $GITHUB_ENV + + - name: Create artifact directory + run: | + cd Firmware/RTK_Surveyor/build/esp32.esp32.esp32/ + mkdir ${{ env.ARTIFACT }} + mv RTK_Surveyor.ino.bin ${{ env.ARTIFACT }} + mv RTK_Surveyor.ino.elf ${{ env.ARTIFACT }} + + - name: Upload artifact directory to action - avoid double-zip + uses: actions/upload-artifact@v3 + with: + name: ${{ env.ARTIFACT }} + path: Firmware/RTK_Surveyor/build/esp32.esp32.esp32/${{ env.ARTIFACT }} + retention-days: 7 diff --git a/.gitignore b/.gitignore index e110aaf11..5fce5b65b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,38 +1,44 @@ -# Windows image file caches -Thumbs.db -ehthumbs.db - -#Eagle Backup files -*.s#? -*.b#? -*.l#? -*.lck - -# Folder config file -Desktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Windows Installer files -*.cab -*.msi -*.msm -*.msp - -# ========================= -# Operating System Files -# ========================= - -# OSX -# ========================= - +partitions.csv + +tokens.h +.vscode/* + +# Windows image file caches +Thumbs.db +ehthumbs.db + +#Eagle Backup files +*.s#? +*.b#? +*.l#? +*.lck + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# ========================= +# Operating System Files +# ========================= + +# OSX +# ========================= + .DS_Store .AppleDouble .LSOverride # Icon must ends with two \r. -Icon +Icon + # Thumbnails ._* @@ -40,3 +46,14 @@ Icon # Files that might appear on external disk .Spotlight-V100 .Trashes + +# ========================= +# Linux Files +# ========================= + +Compare +NMEA_Client +Read_Map_File +RTK_Reset +Split_Messages +X.509_crt_bundle_bin_to_c diff --git a/Binaries/For_SD_Loading/RTK_Surveyor_Firmware_v11.bin b/Binaries/For_SD_Loading/RTK_Surveyor_Firmware_v11.bin deleted file mode 100644 index 6fc4409dd..000000000 Binary files a/Binaries/For_SD_Loading/RTK_Surveyor_Firmware_v11.bin and /dev/null differ diff --git a/Binaries/For_SD_Loading/RTK_Surveyor_Firmware_v12.bin b/Binaries/For_SD_Loading/RTK_Surveyor_Firmware_v12.bin deleted file mode 100644 index 39463a78a..000000000 Binary files a/Binaries/For_SD_Loading/RTK_Surveyor_Firmware_v12.bin and /dev/null differ diff --git a/Binaries/For_SD_Loading/RTK_Surveyor_Firmware_v13.bin b/Binaries/For_SD_Loading/RTK_Surveyor_Firmware_v13.bin deleted file mode 100644 index 9832a8005..000000000 Binary files a/Binaries/For_SD_Loading/RTK_Surveyor_Firmware_v13.bin and /dev/null differ diff --git a/Binaries/For_SD_Loading/RTK_Surveyor_Firmware_v14.bin b/Binaries/For_SD_Loading/RTK_Surveyor_Firmware_v14.bin deleted file mode 100644 index 4eca72e63..000000000 Binary files a/Binaries/For_SD_Loading/RTK_Surveyor_Firmware_v14.bin and /dev/null differ diff --git a/Binaries/RTK_Surveyor_Firmware_v10_combined.bin b/Binaries/RTK_Surveyor_Firmware_v10_combined.bin deleted file mode 100644 index 7e0df75b5..000000000 Binary files a/Binaries/RTK_Surveyor_Firmware_v10_combined.bin and /dev/null differ diff --git a/Binaries/RTK_Surveyor_Firmware_v11_combined.bin b/Binaries/RTK_Surveyor_Firmware_v11_combined.bin deleted file mode 100644 index 4d74acb5f..000000000 Binary files a/Binaries/RTK_Surveyor_Firmware_v11_combined.bin and /dev/null differ diff --git a/Binaries/RTK_Surveyor_Firmware_v12_combined.bin b/Binaries/RTK_Surveyor_Firmware_v12_combined.bin deleted file mode 100644 index 432f4c6c8..000000000 Binary files a/Binaries/RTK_Surveyor_Firmware_v12_combined.bin and /dev/null differ diff --git a/Binaries/RTK_Surveyor_Firmware_v13_combined.bin b/Binaries/RTK_Surveyor_Firmware_v13_combined.bin deleted file mode 100644 index b486a707f..000000000 Binary files a/Binaries/RTK_Surveyor_Firmware_v13_combined.bin and /dev/null differ diff --git a/Binaries/RTK_Surveyor_Firmware_v14_combined.bin b/Binaries/RTK_Surveyor_Firmware_v14_combined.bin deleted file mode 100644 index ee7bedeb6..000000000 Binary files a/Binaries/RTK_Surveyor_Firmware_v14_combined.bin and /dev/null differ diff --git a/Binaries/readme.md b/Binaries/readme.md deleted file mode 100644 index 360cd8155..000000000 --- a/Binaries/readme.md +++ /dev/null @@ -1,5 +0,0 @@ -This folder contains the various firmware versions for RTK Surveyor. Each binary is created by exporting a sketch binary from Arduino then combining (using the ESP32 tool) with boot_app0.bin, bootloader_qio_80m.bin, and RTK_Surveyor.ino.partitions.bin. You can update the firmware on a device by loading a binary onto the SD card and inserting it into the RTK Surveyor (see more information [here](https://learn.sparkfun.com/tutorials/sparkfun-rtk-surveyor-hookup-guide/firmware-updates-and-customization)) or by using the following CLI: - -esptool.exe --chip esp32 --port COM4 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size detect 0x00 RTK_Surveyor_Firmware_vxx_combined.bin - -Where *COM4* is replaced with the COM port that RTK Surveyor enumerated at and *RTK_Surveyor_Firmware_vxx_combined.bin* is the firmware you would like to load. \ No newline at end of file diff --git a/Firmware/RTKFirmware.csv b/Firmware/RTKFirmware.csv new file mode 100644 index 000000000..7b89daee9 --- /dev/null +++ b/Firmware/RTKFirmware.csv @@ -0,0 +1,6 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xe000, 0x2000, +app0, app, ota_0, 0x10000, 0x640000, +app1, app, ota_1, 0x650000,0x640000, +spiffs, data, spiffs, 0xc90000,0x370000, diff --git a/Firmware/RTK_Surveyor/AP-Config/favicon.ico b/Firmware/RTK_Surveyor/AP-Config/favicon.ico new file mode 100644 index 000000000..5c70911fb Binary files /dev/null and b/Firmware/RTK_Surveyor/AP-Config/favicon.ico differ diff --git a/Firmware/RTK_Surveyor/AP-Config/index.html b/Firmware/RTK_Surveyor/AP-Config/index.html new file mode 100644 index 000000000..65e7c43fe --- /dev/null +++ b/Firmware/RTK_Surveyor/AP-Config/index.html @@ -0,0 +1,2002 @@ + + + + + + + SparkFun RTK Setup + + + + + + + + + + + + + +
+
+ Battery level +67 +
+
+ + + + + + + +
+ +
SparkFun RTK WiFi Setup
+ +
+ +
+ +
+ +
+ RTK Firmware: v0.0
+ ZED-F9P Firmware: v0.0
+ Device Bluetooth ID: 0000
+ LLh: + 40.09029479, + -105.18505761, + 1560.089 (APC) +
+ ECEF: + -1280206.568, + -4716804.403, + 4086665.484 +
+
+ +
+ +
+
+ + +
+ +
+
+
+ +
+ Profile: + + + +
+ +
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+ +
+ +
+ +

+
+

+
+ +
+
+ + + + +
+

+
+ +
+
+ + +
+ +
+
+
+ +
+ Measurement Rate: +
+
+ +
+
+ +

+
+ +

or

+ +
+ +
+
+ +

+
+
+
+ +
+ + + + + +

+
+
+ +
+ +
+ +

+
+
+ +
+ +
+ +

+
+
+ +
+ + + + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+

+ +
+ + + + + +
+ +
+
+ +
+ +

+
+
+ +
+ +
+ +

+
+
+ +
+ +
+ +

+
+
+ +
+ +
+ +

+
+
+ +
+ +
+ +

+
+
+ +
+ +
+ +

+
+
+ +
+ + + + + +
+ + +
+ +
+ + + + +
+ +
+
+ +
+ +
+
+ +
+ + +
+
+
+
+
+
+ + + +
+ +
+
+
+ +
+ + + + + +
+ +
+
+ +
+ +

+
+
+ +
+ +
+ +

+
+
+
+ +
+ + + + + +
+ +
+
+ +
+
+ +
+ +
+
+ + + + +
+
+ +
+
+ +
+
+ +

+
+
+ +
+
+ +
+
+ +

+
+
+ +
+
+ +
+
+ +

+
+
+ +
+
+ +
+
+ +

+
+
+ +
+ +
+ +
+
+ Nickname: X/Y/Z
+ +
+
+ +
+
+ + + + + + +
+
+
+ +
+ +
+
+ +
+
+
+ + + + +
+
+ +
+ +
+ +

+
+
+ +
+ +
+ +

+
+
+ +
+
+ + +
+
+ +
+
+ +
+ +
+
+

+
+ +
+ + + + + + + +
+
+ + + + + + + +
+ + +
+ + +
+ +

+
+
+ +
+ +
+ +

+
+
+ +
+ +
+ +

+
+

+ +
+ +
+ +

+
+
+ +
+ +
+ +
+
+ Nickname: + Lat/Long/Alt
+ +
+
+ +
+
+ + + + + + +
+
+ +
+
+ +
+ + + + + +
+ +
+
+ +
+ +

+
+
+ +
+ +
+ +

+
+
+ +
+ +
+ +

+
+
+ +
+ +
+ +

+
+
+ +
+ +
+ +

+
+
+ +
+ +
+ +

+
+
+ +
+ +
+ +
+ +

+
+
+ +
+ +
+ +

+
+
+ +
+ +
+ +

+
+
+ +
+ +
+ +

+
+
+ +
+ +
+ +

+
+
+ +
+ +
+ +

+
+
+ + +
+ +
+ + + + +
+ +
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+ + +
+ +
+
+
+
+ + + + + +
+
+ + + + + +
+ +
+ +
+ +

+
+
+ +
+ +
+ +

+
+
+ +
+ +
+ +

+
+
+ +
+ + + + + +
+ +
+ + + + + +
+ +
+
+ + +
+ +
+
+
+ +
+ Device ID:

N/A


+ Days until keys expire:

No Keys

+
+ +
+ + + + + +
+ +
+ + + + + +
+
+ +
+
+ + + + + +
+ +
+ +
+ +

+
+
+
+
+
+ + +
+ +
+
+
+ +
+ + + + + +
+
+ +
+ + + + + +
+ +
+ + + + + +
+ +
+
+ + + + + +
+ +
+
+ +
+ +

+
+
+ +
+ +
+ +

+
+
+ +
+ + + + + +
+
+ +
+ + + + + +
+
+ +
+ + + + + +
+
+
+ + +
+ +
+
+
+ +
+ Networks: + + + +
+ +
+ +
+ +

+
+
+ +
+ +
+ +

+
+
+ +
+ +
+ +

+
+
+ +
+ +
+ +

+
+
+ +
+ +
+ +

+
+
+ +
+ +
+ +

+
+
+ +
+ +
+ +

+
+
+ +
+ +
+ +

+
+
+ +
+ + + + + +
+ +
+
+ + +
+ +
+
+
+ +
+ + + + + +
+
+

+
+ +
+
+ +
+ +

+
+
+ +
+ +
+ +

+
+
+
+ +
+ + + + + +
+
+

+
+ +
+
+ +
+ +

+
+
+
+ +
+ + + + + +
+
+

+
+ +
+
+ +
+ +

+
+
+
+
+
+ + +
+ +
+
+
+ +
+ + + + + +
+ +
+
+ Radio MAC:

AA:BB:CC:DD:EE:FF

+
+
+ Paired Radios: +
+

None

+
+
+ +
+
+ + + + +
+
+ +
+
+ + + + + +
+
+ +

+
+
+ +
+
+ + + + + +
+
+ +
+
+
+ + +
+ +
+
+
+ +
+ + v0.0 +
+ +
+
+ + + + + +

+ +
+ + + + + +
+
+
+ + +
+ +
+ + + +
+

+
+
+ +
+ + + + + +
+ +
+ + + + + +
+ +
+ +
+ + +

+
+ +
+ + + +

+
+ +
+
+ +
+
+ +
+
+ + + + +
+
+ +
+ + + + + +
+ +
+
+ + +

+
+
+
+ +
+ + + + + +
+ +
+ + + + + +
+
+ +

+
+ + +
+ + + + + +
+
+
+ + +
+ +
+
+
+ +
+ + + + + +
+ +
+ +
+
+ +
+ +

+
+
+
+ +
+ +

+
+
+
+ +
+ +

+
+
+
+ +
+ +

+
+
+
+ +
+ +
+ +
+ +

+
+
+
+
+ + +
+ +
+
+
+ +
+ +
+ +

+
+
+
+ +
+ +

+
+
+
+ +
+ +

+
+
+
+ +
+ +

+
+
+
+ +
+ +

+
+
+ +
+
+ + +
+ +
+
+
+ +
+ SD Size:

0 MB

/ Free:

0 MB

+
+ + + + + + + + + + + + + + + + + + + + + + +
NameSize
SFE_Express_Settings_0.txt5 MB +
SFE_Express_221031_020106.ubx221 KB +
SFE_Express_221031_020209.ubx408 KB +
+ + + + + + + +
+ +
+ +
+ +
+

+ +
+
+ + + +
+
+
+ +

+

+
+
+ +

+

 

+
+
+
+ +
+ + + + diff --git a/Firmware/RTK_Surveyor/AP-Config/src/Battery0.png b/Firmware/RTK_Surveyor/AP-Config/src/Battery0.png new file mode 100644 index 000000000..b7750797e Binary files /dev/null and b/Firmware/RTK_Surveyor/AP-Config/src/Battery0.png differ diff --git a/Firmware/RTK_Surveyor/AP-Config/src/Battery0_Charging.png b/Firmware/RTK_Surveyor/AP-Config/src/Battery0_Charging.png new file mode 100644 index 000000000..026f8ed97 Binary files /dev/null and b/Firmware/RTK_Surveyor/AP-Config/src/Battery0_Charging.png differ diff --git a/Firmware/RTK_Surveyor/AP-Config/src/Battery1.png b/Firmware/RTK_Surveyor/AP-Config/src/Battery1.png new file mode 100644 index 000000000..ec7dcb659 Binary files /dev/null and b/Firmware/RTK_Surveyor/AP-Config/src/Battery1.png differ diff --git a/Firmware/RTK_Surveyor/AP-Config/src/Battery1_Charging.png b/Firmware/RTK_Surveyor/AP-Config/src/Battery1_Charging.png new file mode 100644 index 000000000..c11a5d972 Binary files /dev/null and b/Firmware/RTK_Surveyor/AP-Config/src/Battery1_Charging.png differ diff --git a/Firmware/RTK_Surveyor/AP-Config/src/Battery2.png b/Firmware/RTK_Surveyor/AP-Config/src/Battery2.png new file mode 100644 index 000000000..1aa43191c Binary files /dev/null and b/Firmware/RTK_Surveyor/AP-Config/src/Battery2.png differ diff --git a/Firmware/RTK_Surveyor/AP-Config/src/Battery2_Charging.png b/Firmware/RTK_Surveyor/AP-Config/src/Battery2_Charging.png new file mode 100644 index 000000000..82fb4d403 Binary files /dev/null and b/Firmware/RTK_Surveyor/AP-Config/src/Battery2_Charging.png differ diff --git a/Firmware/RTK_Surveyor/AP-Config/src/Battery3.png b/Firmware/RTK_Surveyor/AP-Config/src/Battery3.png new file mode 100644 index 000000000..9fcb5f2c3 Binary files /dev/null and b/Firmware/RTK_Surveyor/AP-Config/src/Battery3.png differ diff --git a/Firmware/RTK_Surveyor/AP-Config/src/Battery3_Charging.png b/Firmware/RTK_Surveyor/AP-Config/src/Battery3_Charging.png new file mode 100644 index 000000000..ecb5a292f Binary files /dev/null and b/Firmware/RTK_Surveyor/AP-Config/src/Battery3_Charging.png differ diff --git a/Firmware/RTK_Surveyor/AP-Config/src/BatteryBlank.png b/Firmware/RTK_Surveyor/AP-Config/src/BatteryBlank.png new file mode 100644 index 000000000..f95db25f3 Binary files /dev/null and b/Firmware/RTK_Surveyor/AP-Config/src/BatteryBlank.png differ diff --git a/Firmware/RTK_Surveyor/AP-Config/src/BatteryBlank.png.gz b/Firmware/RTK_Surveyor/AP-Config/src/BatteryBlank.png.gz new file mode 100644 index 000000000..b555e6c1d Binary files /dev/null and b/Firmware/RTK_Surveyor/AP-Config/src/BatteryBlank.png.gz differ diff --git a/Firmware/RTK_Surveyor/AP-Config/src/bootstrap.bundle.min.js b/Firmware/RTK_Surveyor/AP-Config/src/bootstrap.bundle.min.js new file mode 100644 index 000000000..68acb7a31 --- /dev/null +++ b/Firmware/RTK_Surveyor/AP-Config/src/bootstrap.bundle.min.js @@ -0,0 +1,7 @@ +/*! + * Bootstrap v5.0.2 (https://getbootstrap.com/) + * Copyright 2011-2021 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e()}(this,(function(){"use strict";const t={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter(t=>t.matches(e)),parents(t,e){const i=[];let n=t.parentNode;for(;n&&n.nodeType===Node.ELEMENT_NODE&&3!==n.nodeType;)n.matches(e)&&i.push(n),n=n.parentNode;return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]}},e=t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t},i=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i="#"+i.split("#")[1]),e=i&&"#"!==i?i.trim():null}return e},n=t=>{const e=i(t);return e&&document.querySelector(e)?e:null},s=t=>{const e=i(t);return e?document.querySelector(e):null},o=t=>{t.dispatchEvent(new Event("transitionend"))},r=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),a=e=>r(e)?e.jquery?e[0]:e:"string"==typeof e&&e.length>0?t.findOne(e):null,l=(t,e,i)=>{Object.keys(i).forEach(n=>{const s=i[n],o=e[n],a=o&&r(o)?"element":null==(l=o)?""+l:{}.toString.call(l).match(/\s([a-z]+)/i)[1].toLowerCase();var l;if(!new RegExp(s).test(a))throw new TypeError(`${t.toUpperCase()}: Option "${n}" provided type "${a}" but expected type "${s}".`)})},c=t=>!(!r(t)||0===t.getClientRects().length)&&"visible"===getComputedStyle(t).getPropertyValue("visibility"),h=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),d=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?d(t.parentNode):null},u=()=>{},f=t=>t.offsetHeight,p=()=>{const{jQuery:t}=window;return t&&!document.body.hasAttribute("data-bs-no-jquery")?t:null},m=[],g=()=>"rtl"===document.documentElement.dir,_=t=>{var e;e=()=>{const e=p();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(m.length||document.addEventListener("DOMContentLoaded",()=>{m.forEach(t=>t())}),m.push(e)):e()},b=t=>{"function"==typeof t&&t()},v=(t,e,i=!0)=>{if(!i)return void b(t);const n=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let s=!1;const r=({target:i})=>{i===e&&(s=!0,e.removeEventListener("transitionend",r),b(t))};e.addEventListener("transitionend",r),setTimeout(()=>{s||o(e)},n)},y=(t,e,i,n)=>{let s=t.indexOf(e);if(-1===s)return t[!i&&n?t.length-1:0];const o=t.length;return s+=i?1:-1,n&&(s=(s+o)%o),t[Math.max(0,Math.min(s,o-1))]},w=/[^.]*(?=\..*)\.|.*/,E=/\..*/,A=/::\d+$/,T={};let O=1;const C={mouseenter:"mouseover",mouseleave:"mouseout"},k=/^(mouseenter|mouseleave)/i,L=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function x(t,e){return e&&`${e}::${O++}`||t.uidEvent||O++}function D(t){const e=x(t);return t.uidEvent=e,T[e]=T[e]||{},T[e]}function S(t,e,i=null){const n=Object.keys(t);for(let s=0,o=n.length;sfunction(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};n?n=t(n):i=t(i)}const[o,r,a]=I(e,i,n),l=D(t),c=l[a]||(l[a]={}),h=S(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=x(r,e.replace(w,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(let a=o.length;a--;)if(o[a]===r)return s.delegateTarget=r,n.oneOff&&P.off(t,s.type,e,i),i.apply(r,[s]);return null}}(t,i,n):function(t,e){return function i(n){return n.delegateTarget=t,i.oneOff&&P.off(t,n.type,e),e.apply(t,[n])}}(t,i);u.delegationSelector=o?i:null,u.originalHandler=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function j(t,e,i,n,s){const o=S(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function M(t){return t=t.replace(E,""),C[t]||t}const P={on(t,e,i,n){N(t,e,i,n,!1)},one(t,e,i,n){N(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=I(e,i,n),a=r!==e,l=D(t),c=e.startsWith(".");if(void 0!==o){if(!l||!l[r])return;return void j(t,l,r,o,s?i:null)}c&&Object.keys(l).forEach(i=>{!function(t,e,i,n){const s=e[i]||{};Object.keys(s).forEach(o=>{if(o.includes(n)){const n=s[o];j(t,e,i,n.originalHandler,n.delegationSelector)}})}(t,l,i,e.slice(1))});const h=l[r]||{};Object.keys(h).forEach(i=>{const n=i.replace(A,"");if(!a||e.includes(n)){const e=h[i];j(t,l,r,e.originalHandler,e.delegationSelector)}})},trigger(t,e,i){if("string"!=typeof e||!t)return null;const n=p(),s=M(e),o=e!==s,r=L.has(s);let a,l=!0,c=!0,h=!1,d=null;return o&&n&&(a=n.Event(e,i),n(t).trigger(a),l=!a.isPropagationStopped(),c=!a.isImmediatePropagationStopped(),h=a.isDefaultPrevented()),r?(d=document.createEvent("HTMLEvents"),d.initEvent(s,l,!0)):d=new CustomEvent(e,{bubbles:l,cancelable:!0}),void 0!==i&&Object.keys(i).forEach(t=>{Object.defineProperty(d,t,{get:()=>i[t]})}),h&&d.preventDefault(),c&&t.dispatchEvent(d),d.defaultPrevented&&void 0!==a&&a.preventDefault(),d}},H=new Map;var R={set(t,e,i){H.has(t)||H.set(t,new Map);const n=H.get(t);n.has(e)||0===n.size?n.set(e,i):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(n.keys())[0]}.`)},get:(t,e)=>H.has(t)&&H.get(t).get(e)||null,remove(t,e){if(!H.has(t))return;const i=H.get(t);i.delete(e),0===i.size&&H.delete(t)}};class B{constructor(t){(t=a(t))&&(this._element=t,R.set(this._element,this.constructor.DATA_KEY,this))}dispose(){R.remove(this._element,this.constructor.DATA_KEY),P.off(this._element,this.constructor.EVENT_KEY),Object.getOwnPropertyNames(this).forEach(t=>{this[t]=null})}_queueCallback(t,e,i=!0){v(t,e,i)}static getInstance(t){return R.get(t,this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.0.2"}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}static get DATA_KEY(){return"bs."+this.NAME}static get EVENT_KEY(){return"."+this.DATA_KEY}}class W extends B{static get NAME(){return"alert"}close(t){const e=t?this._getRootElement(t):this._element,i=this._triggerCloseEvent(e);null===i||i.defaultPrevented||this._removeElement(e)}_getRootElement(t){return s(t)||t.closest(".alert")}_triggerCloseEvent(t){return P.trigger(t,"close.bs.alert")}_removeElement(t){t.classList.remove("show");const e=t.classList.contains("fade");this._queueCallback(()=>this._destroyElement(t),t,e)}_destroyElement(t){t.remove(),P.trigger(t,"closed.bs.alert")}static jQueryInterface(t){return this.each((function(){const e=W.getOrCreateInstance(this);"close"===t&&e[t](this)}))}static handleDismiss(t){return function(e){e&&e.preventDefault(),t.close(this)}}}P.on(document,"click.bs.alert.data-api",'[data-bs-dismiss="alert"]',W.handleDismiss(new W)),_(W);class q extends B{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=q.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}function z(t){return"true"===t||"false"!==t&&(t===Number(t).toString()?Number(t):""===t||"null"===t?null:t)}function $(t){return t.replace(/[A-Z]/g,t=>"-"+t.toLowerCase())}P.on(document,"click.bs.button.data-api",'[data-bs-toggle="button"]',t=>{t.preventDefault();const e=t.target.closest('[data-bs-toggle="button"]');q.getOrCreateInstance(e).toggle()}),_(q);const U={setDataAttribute(t,e,i){t.setAttribute("data-bs-"+$(e),i)},removeDataAttribute(t,e){t.removeAttribute("data-bs-"+$(e))},getDataAttributes(t){if(!t)return{};const e={};return Object.keys(t.dataset).filter(t=>t.startsWith("bs")).forEach(i=>{let n=i.replace(/^bs/,"");n=n.charAt(0).toLowerCase()+n.slice(1,n.length),e[n]=z(t.dataset[i])}),e},getDataAttribute:(t,e)=>z(t.getAttribute("data-bs-"+$(e))),offset(t){const e=t.getBoundingClientRect();return{top:e.top+document.body.scrollTop,left:e.left+document.body.scrollLeft}},position:t=>({top:t.offsetTop,left:t.offsetLeft})},F={interval:5e3,keyboard:!0,slide:!1,pause:"hover",wrap:!0,touch:!0},V={interval:"(number|boolean)",keyboard:"boolean",slide:"(boolean|string)",pause:"(string|boolean)",wrap:"boolean",touch:"boolean"},K="next",X="prev",Y="left",Q="right",G={ArrowLeft:Q,ArrowRight:Y};class Z extends B{constructor(e,i){super(e),this._items=null,this._interval=null,this._activeElement=null,this._isPaused=!1,this._isSliding=!1,this.touchTimeout=null,this.touchStartX=0,this.touchDeltaX=0,this._config=this._getConfig(i),this._indicatorsElement=t.findOne(".carousel-indicators",this._element),this._touchSupported="ontouchstart"in document.documentElement||navigator.maxTouchPoints>0,this._pointerEvent=Boolean(window.PointerEvent),this._addEventListeners()}static get Default(){return F}static get NAME(){return"carousel"}next(){this._slide(K)}nextWhenVisible(){!document.hidden&&c(this._element)&&this.next()}prev(){this._slide(X)}pause(e){e||(this._isPaused=!0),t.findOne(".carousel-item-next, .carousel-item-prev",this._element)&&(o(this._element),this.cycle(!0)),clearInterval(this._interval),this._interval=null}cycle(t){t||(this._isPaused=!1),this._interval&&(clearInterval(this._interval),this._interval=null),this._config&&this._config.interval&&!this._isPaused&&(this._updateInterval(),this._interval=setInterval((document.visibilityState?this.nextWhenVisible:this.next).bind(this),this._config.interval))}to(e){this._activeElement=t.findOne(".active.carousel-item",this._element);const i=this._getItemIndex(this._activeElement);if(e>this._items.length-1||e<0)return;if(this._isSliding)return void P.one(this._element,"slid.bs.carousel",()=>this.to(e));if(i===e)return this.pause(),void this.cycle();const n=e>i?K:X;this._slide(n,this._items[e])}_getConfig(t){return t={...F,...U.getDataAttributes(this._element),..."object"==typeof t?t:{}},l("carousel",t,V),t}_handleSwipe(){const t=Math.abs(this.touchDeltaX);if(t<=40)return;const e=t/this.touchDeltaX;this.touchDeltaX=0,e&&this._slide(e>0?Q:Y)}_addEventListeners(){this._config.keyboard&&P.on(this._element,"keydown.bs.carousel",t=>this._keydown(t)),"hover"===this._config.pause&&(P.on(this._element,"mouseenter.bs.carousel",t=>this.pause(t)),P.on(this._element,"mouseleave.bs.carousel",t=>this.cycle(t))),this._config.touch&&this._touchSupported&&this._addTouchEventListeners()}_addTouchEventListeners(){const e=t=>{!this._pointerEvent||"pen"!==t.pointerType&&"touch"!==t.pointerType?this._pointerEvent||(this.touchStartX=t.touches[0].clientX):this.touchStartX=t.clientX},i=t=>{this.touchDeltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this.touchStartX},n=t=>{!this._pointerEvent||"pen"!==t.pointerType&&"touch"!==t.pointerType||(this.touchDeltaX=t.clientX-this.touchStartX),this._handleSwipe(),"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout(t=>this.cycle(t),500+this._config.interval))};t.find(".carousel-item img",this._element).forEach(t=>{P.on(t,"dragstart.bs.carousel",t=>t.preventDefault())}),this._pointerEvent?(P.on(this._element,"pointerdown.bs.carousel",t=>e(t)),P.on(this._element,"pointerup.bs.carousel",t=>n(t)),this._element.classList.add("pointer-event")):(P.on(this._element,"touchstart.bs.carousel",t=>e(t)),P.on(this._element,"touchmove.bs.carousel",t=>i(t)),P.on(this._element,"touchend.bs.carousel",t=>n(t)))}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=G[t.key];e&&(t.preventDefault(),this._slide(e))}_getItemIndex(e){return this._items=e&&e.parentNode?t.find(".carousel-item",e.parentNode):[],this._items.indexOf(e)}_getItemByOrder(t,e){const i=t===K;return y(this._items,e,i,this._config.wrap)}_triggerSlideEvent(e,i){const n=this._getItemIndex(e),s=this._getItemIndex(t.findOne(".active.carousel-item",this._element));return P.trigger(this._element,"slide.bs.carousel",{relatedTarget:e,direction:i,from:s,to:n})}_setActiveIndicatorElement(e){if(this._indicatorsElement){const i=t.findOne(".active",this._indicatorsElement);i.classList.remove("active"),i.removeAttribute("aria-current");const n=t.find("[data-bs-target]",this._indicatorsElement);for(let t=0;t{P.trigger(this._element,"slid.bs.carousel",{relatedTarget:r,direction:u,from:o,to:a})};if(this._element.classList.contains("slide")){r.classList.add(d),f(r),s.classList.add(h),r.classList.add(h);const t=()=>{r.classList.remove(h,d),r.classList.add("active"),s.classList.remove("active",d,h),this._isSliding=!1,setTimeout(p,0)};this._queueCallback(t,s,!0)}else s.classList.remove("active"),r.classList.add("active"),this._isSliding=!1,p();l&&this.cycle()}_directionToOrder(t){return[Q,Y].includes(t)?g()?t===Y?X:K:t===Y?K:X:t}_orderToDirection(t){return[K,X].includes(t)?g()?t===X?Y:Q:t===X?Q:Y:t}static carouselInterface(t,e){const i=Z.getOrCreateInstance(t,e);let{_config:n}=i;"object"==typeof e&&(n={...n,...e});const s="string"==typeof e?e:n.slide;if("number"==typeof e)i.to(e);else if("string"==typeof s){if(void 0===i[s])throw new TypeError(`No method named "${s}"`);i[s]()}else n.interval&&n.ride&&(i.pause(),i.cycle())}static jQueryInterface(t){return this.each((function(){Z.carouselInterface(this,t)}))}static dataApiClickHandler(t){const e=s(this);if(!e||!e.classList.contains("carousel"))return;const i={...U.getDataAttributes(e),...U.getDataAttributes(this)},n=this.getAttribute("data-bs-slide-to");n&&(i.interval=!1),Z.carouselInterface(e,i),n&&Z.getInstance(e).to(n),t.preventDefault()}}P.on(document,"click.bs.carousel.data-api","[data-bs-slide], [data-bs-slide-to]",Z.dataApiClickHandler),P.on(window,"load.bs.carousel.data-api",()=>{const e=t.find('[data-bs-ride="carousel"]');for(let t=0,i=e.length;tt===this._element);null!==o&&r.length&&(this._selector=o,this._triggerArray.push(i))}this._parent=this._config.parent?this._getParent():null,this._config.parent||this._addAriaAndCollapsedClass(this._element,this._triggerArray),this._config.toggle&&this.toggle()}static get Default(){return J}static get NAME(){return"collapse"}toggle(){this._element.classList.contains("show")?this.hide():this.show()}show(){if(this._isTransitioning||this._element.classList.contains("show"))return;let e,i;this._parent&&(e=t.find(".show, .collapsing",this._parent).filter(t=>"string"==typeof this._config.parent?t.getAttribute("data-bs-parent")===this._config.parent:t.classList.contains("collapse")),0===e.length&&(e=null));const n=t.findOne(this._selector);if(e){const t=e.find(t=>n!==t);if(i=t?et.getInstance(t):null,i&&i._isTransitioning)return}if(P.trigger(this._element,"show.bs.collapse").defaultPrevented)return;e&&e.forEach(t=>{n!==t&&et.collapseInterface(t,"hide"),i||R.set(t,"bs.collapse",null)});const s=this._getDimension();this._element.classList.remove("collapse"),this._element.classList.add("collapsing"),this._element.style[s]=0,this._triggerArray.length&&this._triggerArray.forEach(t=>{t.classList.remove("collapsed"),t.setAttribute("aria-expanded",!0)}),this.setTransitioning(!0);const o="scroll"+(s[0].toUpperCase()+s.slice(1));this._queueCallback(()=>{this._element.classList.remove("collapsing"),this._element.classList.add("collapse","show"),this._element.style[s]="",this.setTransitioning(!1),P.trigger(this._element,"shown.bs.collapse")},this._element,!0),this._element.style[s]=this._element[o]+"px"}hide(){if(this._isTransitioning||!this._element.classList.contains("show"))return;if(P.trigger(this._element,"hide.bs.collapse").defaultPrevented)return;const t=this._getDimension();this._element.style[t]=this._element.getBoundingClientRect()[t]+"px",f(this._element),this._element.classList.add("collapsing"),this._element.classList.remove("collapse","show");const e=this._triggerArray.length;if(e>0)for(let t=0;t{this.setTransitioning(!1),this._element.classList.remove("collapsing"),this._element.classList.add("collapse"),P.trigger(this._element,"hidden.bs.collapse")},this._element,!0)}setTransitioning(t){this._isTransitioning=t}_getConfig(t){return(t={...J,...t}).toggle=Boolean(t.toggle),l("collapse",t,tt),t}_getDimension(){return this._element.classList.contains("width")?"width":"height"}_getParent(){let{parent:e}=this._config;e=a(e);const i=`[data-bs-toggle="collapse"][data-bs-parent="${e}"]`;return t.find(i,e).forEach(t=>{const e=s(t);this._addAriaAndCollapsedClass(e,[t])}),e}_addAriaAndCollapsedClass(t,e){if(!t||!e.length)return;const i=t.classList.contains("show");e.forEach(t=>{i?t.classList.remove("collapsed"):t.classList.add("collapsed"),t.setAttribute("aria-expanded",i)})}static collapseInterface(t,e){let i=et.getInstance(t);const n={...J,...U.getDataAttributes(t),..."object"==typeof e&&e?e:{}};if(!i&&n.toggle&&"string"==typeof e&&/show|hide/.test(e)&&(n.toggle=!1),i||(i=new et(t,n)),"string"==typeof e){if(void 0===i[e])throw new TypeError(`No method named "${e}"`);i[e]()}}static jQueryInterface(t){return this.each((function(){et.collapseInterface(this,t)}))}}P.on(document,"click.bs.collapse.data-api",'[data-bs-toggle="collapse"]',(function(e){("A"===e.target.tagName||e.delegateTarget&&"A"===e.delegateTarget.tagName)&&e.preventDefault();const i=U.getDataAttributes(this),s=n(this);t.find(s).forEach(t=>{const e=et.getInstance(t);let n;e?(null===e._parent&&"string"==typeof i.parent&&(e._config.parent=i.parent,e._parent=e._getParent()),n="toggle"):n=i,et.collapseInterface(t,n)})})),_(et);var it="top",nt="bottom",st="right",ot="left",rt=[it,nt,st,ot],at=rt.reduce((function(t,e){return t.concat([e+"-start",e+"-end"])}),[]),lt=[].concat(rt,["auto"]).reduce((function(t,e){return t.concat([e,e+"-start",e+"-end"])}),[]),ct=["beforeRead","read","afterRead","beforeMain","main","afterMain","beforeWrite","write","afterWrite"];function ht(t){return t?(t.nodeName||"").toLowerCase():null}function dt(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function ut(t){return t instanceof dt(t).Element||t instanceof Element}function ft(t){return t instanceof dt(t).HTMLElement||t instanceof HTMLElement}function pt(t){return"undefined"!=typeof ShadowRoot&&(t instanceof dt(t).ShadowRoot||t instanceof ShadowRoot)}var mt={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];ft(s)&&ht(s)&&(Object.assign(s.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});ft(n)&&ht(n)&&(Object.assign(n.style,o),Object.keys(s).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function gt(t){return t.split("-")[0]}function _t(t){var e=t.getBoundingClientRect();return{width:e.width,height:e.height,top:e.top,right:e.right,bottom:e.bottom,left:e.left,x:e.left,y:e.top}}function bt(t){var e=_t(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function vt(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&pt(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function yt(t){return dt(t).getComputedStyle(t)}function wt(t){return["table","td","th"].indexOf(ht(t))>=0}function Et(t){return((ut(t)?t.ownerDocument:t.document)||window.document).documentElement}function At(t){return"html"===ht(t)?t:t.assignedSlot||t.parentNode||(pt(t)?t.host:null)||Et(t)}function Tt(t){return ft(t)&&"fixed"!==yt(t).position?t.offsetParent:null}function Ot(t){for(var e=dt(t),i=Tt(t);i&&wt(i)&&"static"===yt(i).position;)i=Tt(i);return i&&("html"===ht(i)||"body"===ht(i)&&"static"===yt(i).position)?e:i||function(t){var e=-1!==navigator.userAgent.toLowerCase().indexOf("firefox");if(-1!==navigator.userAgent.indexOf("Trident")&&ft(t)&&"fixed"===yt(t).position)return null;for(var i=At(t);ft(i)&&["html","body"].indexOf(ht(i))<0;){var n=yt(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function Ct(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}var kt=Math.max,Lt=Math.min,xt=Math.round;function Dt(t,e,i){return kt(t,Lt(e,i))}function St(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function It(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}var Nt={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,n=t.name,s=t.options,o=i.elements.arrow,r=i.modifiersData.popperOffsets,a=gt(i.placement),l=Ct(a),c=[ot,st].indexOf(a)>=0?"height":"width";if(o&&r){var h=function(t,e){return St("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:It(t,rt))}(s.padding,i),d=bt(o),u="y"===l?it:ot,f="y"===l?nt:st,p=i.rects.reference[c]+i.rects.reference[l]-r[l]-i.rects.popper[c],m=r[l]-i.rects.reference[l],g=Ot(o),_=g?"y"===l?g.clientHeight||0:g.clientWidth||0:0,b=p/2-m/2,v=h[u],y=_-d[c]-h[f],w=_/2-d[c]/2+b,E=Dt(v,w,y),A=l;i.modifiersData[n]=((e={})[A]=E,e.centerOffset=E-w,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&vt(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]},jt={top:"auto",right:"auto",bottom:"auto",left:"auto"};function Mt(t){var e,i=t.popper,n=t.popperRect,s=t.placement,o=t.offsets,r=t.position,a=t.gpuAcceleration,l=t.adaptive,c=t.roundOffsets,h=!0===c?function(t){var e=t.x,i=t.y,n=window.devicePixelRatio||1;return{x:xt(xt(e*n)/n)||0,y:xt(xt(i*n)/n)||0}}(o):"function"==typeof c?c(o):o,d=h.x,u=void 0===d?0:d,f=h.y,p=void 0===f?0:f,m=o.hasOwnProperty("x"),g=o.hasOwnProperty("y"),_=ot,b=it,v=window;if(l){var y=Ot(i),w="clientHeight",E="clientWidth";y===dt(i)&&"static"!==yt(y=Et(i)).position&&(w="scrollHeight",E="scrollWidth"),y=y,s===it&&(b=nt,p-=y[w]-n.height,p*=a?1:-1),s===ot&&(_=st,u-=y[E]-n.width,u*=a?1:-1)}var A,T=Object.assign({position:r},l&&jt);return a?Object.assign({},T,((A={})[b]=g?"0":"",A[_]=m?"0":"",A.transform=(v.devicePixelRatio||1)<2?"translate("+u+"px, "+p+"px)":"translate3d("+u+"px, "+p+"px, 0)",A)):Object.assign({},T,((e={})[b]=g?p+"px":"",e[_]=m?u+"px":"",e.transform="",e))}var Pt={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:gt(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,Mt(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,Mt(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}},Ht={passive:!0},Rt={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=dt(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener("scroll",i.update,Ht)})),a&&l.addEventListener("resize",i.update,Ht),function(){o&&c.forEach((function(t){t.removeEventListener("scroll",i.update,Ht)})),a&&l.removeEventListener("resize",i.update,Ht)}},data:{}},Bt={left:"right",right:"left",bottom:"top",top:"bottom"};function Wt(t){return t.replace(/left|right|bottom|top/g,(function(t){return Bt[t]}))}var qt={start:"end",end:"start"};function zt(t){return t.replace(/start|end/g,(function(t){return qt[t]}))}function $t(t){var e=dt(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function Ut(t){return _t(Et(t)).left+$t(t).scrollLeft}function Ft(t){var e=yt(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function Vt(t,e){var i;void 0===e&&(e=[]);var n=function t(e){return["html","body","#document"].indexOf(ht(e))>=0?e.ownerDocument.body:ft(e)&&Ft(e)?e:t(At(e))}(t),s=n===(null==(i=t.ownerDocument)?void 0:i.body),o=dt(n),r=s?[o].concat(o.visualViewport||[],Ft(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(Vt(At(r)))}function Kt(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function Xt(t,e){return"viewport"===e?Kt(function(t){var e=dt(t),i=Et(t),n=e.visualViewport,s=i.clientWidth,o=i.clientHeight,r=0,a=0;return n&&(s=n.width,o=n.height,/^((?!chrome|android).)*safari/i.test(navigator.userAgent)||(r=n.offsetLeft,a=n.offsetTop)),{width:s,height:o,x:r+Ut(t),y:a}}(t)):ft(e)?function(t){var e=_t(t);return e.top=e.top+t.clientTop,e.left=e.left+t.clientLeft,e.bottom=e.top+t.clientHeight,e.right=e.left+t.clientWidth,e.width=t.clientWidth,e.height=t.clientHeight,e.x=e.left,e.y=e.top,e}(e):Kt(function(t){var e,i=Et(t),n=$t(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=kt(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=kt(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+Ut(t),l=-n.scrollTop;return"rtl"===yt(s||i).direction&&(a+=kt(i.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(Et(t)))}function Yt(t){return t.split("-")[1]}function Qt(t){var e,i=t.reference,n=t.element,s=t.placement,o=s?gt(s):null,r=s?Yt(s):null,a=i.x+i.width/2-n.width/2,l=i.y+i.height/2-n.height/2;switch(o){case it:e={x:a,y:i.y-n.height};break;case nt:e={x:a,y:i.y+i.height};break;case st:e={x:i.x+i.width,y:l};break;case ot:e={x:i.x-n.width,y:l};break;default:e={x:i.x,y:i.y}}var c=o?Ct(o):null;if(null!=c){var h="y"===c?"height":"width";switch(r){case"start":e[c]=e[c]-(i[h]/2-n[h]/2);break;case"end":e[c]=e[c]+(i[h]/2-n[h]/2)}}return e}function Gt(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=void 0===n?t.placement:n,o=i.boundary,r=void 0===o?"clippingParents":o,a=i.rootBoundary,l=void 0===a?"viewport":a,c=i.elementContext,h=void 0===c?"popper":c,d=i.altBoundary,u=void 0!==d&&d,f=i.padding,p=void 0===f?0:f,m=St("number"!=typeof p?p:It(p,rt)),g="popper"===h?"reference":"popper",_=t.elements.reference,b=t.rects.popper,v=t.elements[u?g:h],y=function(t,e,i){var n="clippingParents"===e?function(t){var e=Vt(At(t)),i=["absolute","fixed"].indexOf(yt(t).position)>=0&&ft(t)?Ot(t):t;return ut(i)?e.filter((function(t){return ut(t)&&vt(t,i)&&"body"!==ht(t)})):[]}(t):[].concat(e),s=[].concat(n,[i]),o=s[0],r=s.reduce((function(e,i){var n=Xt(t,i);return e.top=kt(n.top,e.top),e.right=Lt(n.right,e.right),e.bottom=Lt(n.bottom,e.bottom),e.left=kt(n.left,e.left),e}),Xt(t,o));return r.width=r.right-r.left,r.height=r.bottom-r.top,r.x=r.left,r.y=r.top,r}(ut(v)?v:v.contextElement||Et(t.elements.popper),r,l),w=_t(_),E=Qt({reference:w,element:b,strategy:"absolute",placement:s}),A=Kt(Object.assign({},b,E)),T="popper"===h?A:w,O={top:y.top-T.top+m.top,bottom:T.bottom-y.bottom+m.bottom,left:y.left-T.left+m.left,right:T.right-y.right+m.right},C=t.modifiersData.offset;if("popper"===h&&C){var k=C[s];Object.keys(O).forEach((function(t){var e=[st,nt].indexOf(t)>=0?1:-1,i=[it,nt].indexOf(t)>=0?"y":"x";O[t]+=k[i]*e}))}return O}function Zt(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,l=i.allowedAutoPlacements,c=void 0===l?lt:l,h=Yt(n),d=h?a?at:at.filter((function(t){return Yt(t)===h})):rt,u=d.filter((function(t){return c.indexOf(t)>=0}));0===u.length&&(u=d);var f=u.reduce((function(e,i){return e[i]=Gt(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[gt(i)],e}),{});return Object.keys(f).sort((function(t,e){return f[t]-f[e]}))}var Jt={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name;if(!e.modifiersData[n]._skip){for(var s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0===r||r,l=i.fallbackPlacements,c=i.padding,h=i.boundary,d=i.rootBoundary,u=i.altBoundary,f=i.flipVariations,p=void 0===f||f,m=i.allowedAutoPlacements,g=e.options.placement,_=gt(g),b=l||(_!==g&&p?function(t){if("auto"===gt(t))return[];var e=Wt(t);return[zt(t),e,zt(e)]}(g):[Wt(g)]),v=[g].concat(b).reduce((function(t,i){return t.concat("auto"===gt(i)?Zt(e,{placement:i,boundary:h,rootBoundary:d,padding:c,flipVariations:p,allowedAutoPlacements:m}):i)}),[]),y=e.rects.reference,w=e.rects.popper,E=new Map,A=!0,T=v[0],O=0;O=0,D=x?"width":"height",S=Gt(e,{placement:C,boundary:h,rootBoundary:d,altBoundary:u,padding:c}),I=x?L?st:ot:L?nt:it;y[D]>w[D]&&(I=Wt(I));var N=Wt(I),j=[];if(o&&j.push(S[k]<=0),a&&j.push(S[I]<=0,S[N]<=0),j.every((function(t){return t}))){T=C,A=!1;break}E.set(C,j)}if(A)for(var M=function(t){var e=v.find((function(e){var i=E.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return T=e,"break"},P=p?3:1;P>0&&"break"!==M(P);P--);e.placement!==T&&(e.modifiersData[n]._skip=!0,e.placement=T,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function te(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function ee(t){return[it,st,nt,ot].some((function(e){return t[e]>=0}))}var ie={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,s=e.rects.popper,o=e.modifiersData.preventOverflow,r=Gt(e,{elementContext:"reference"}),a=Gt(e,{altBoundary:!0}),l=te(r,n),c=te(a,s,o),h=ee(l),d=ee(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:h,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":h,"data-popper-escaped":d})}},ne={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.offset,o=void 0===s?[0,0]:s,r=lt.reduce((function(t,i){return t[i]=function(t,e,i){var n=gt(t),s=[ot,it].indexOf(n)>=0?-1:1,o="function"==typeof i?i(Object.assign({},e,{placement:t})):i,r=o[0],a=o[1];return r=r||0,a=(a||0)*s,[ot,st].indexOf(n)>=0?{x:a,y:r}:{x:r,y:a}}(i,e.rects,o),t}),{}),a=r[e.placement],l=a.x,c=a.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=l,e.modifiersData.popperOffsets.y+=c),e.modifiersData[n]=r}},se={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=Qt({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}},oe={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0!==r&&r,l=i.boundary,c=i.rootBoundary,h=i.altBoundary,d=i.padding,u=i.tether,f=void 0===u||u,p=i.tetherOffset,m=void 0===p?0:p,g=Gt(e,{boundary:l,rootBoundary:c,padding:d,altBoundary:h}),_=gt(e.placement),b=Yt(e.placement),v=!b,y=Ct(_),w="x"===y?"y":"x",E=e.modifiersData.popperOffsets,A=e.rects.reference,T=e.rects.popper,O="function"==typeof m?m(Object.assign({},e.rects,{placement:e.placement})):m,C={x:0,y:0};if(E){if(o||a){var k="y"===y?it:ot,L="y"===y?nt:st,x="y"===y?"height":"width",D=E[y],S=E[y]+g[k],I=E[y]-g[L],N=f?-T[x]/2:0,j="start"===b?A[x]:T[x],M="start"===b?-T[x]:-A[x],P=e.elements.arrow,H=f&&P?bt(P):{width:0,height:0},R=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},B=R[k],W=R[L],q=Dt(0,A[x],H[x]),z=v?A[x]/2-N-q-B-O:j-q-B-O,$=v?-A[x]/2+N+q+W+O:M+q+W+O,U=e.elements.arrow&&Ot(e.elements.arrow),F=U?"y"===y?U.clientTop||0:U.clientLeft||0:0,V=e.modifiersData.offset?e.modifiersData.offset[e.placement][y]:0,K=E[y]+z-V-F,X=E[y]+$-V;if(o){var Y=Dt(f?Lt(S,K):S,D,f?kt(I,X):I);E[y]=Y,C[y]=Y-D}if(a){var Q="x"===y?it:ot,G="x"===y?nt:st,Z=E[w],J=Z+g[Q],tt=Z-g[G],et=Dt(f?Lt(J,K):J,Z,f?kt(tt,X):tt);E[w]=et,C[w]=et-Z}}e.modifiersData[n]=C}},requiresIfExists:["offset"]};function re(t,e,i){void 0===i&&(i=!1);var n,s,o=Et(e),r=_t(t),a=ft(e),l={scrollLeft:0,scrollTop:0},c={x:0,y:0};return(a||!a&&!i)&&(("body"!==ht(e)||Ft(o))&&(l=(n=e)!==dt(n)&&ft(n)?{scrollLeft:(s=n).scrollLeft,scrollTop:s.scrollTop}:$t(n)),ft(e)?((c=_t(e)).x+=e.clientLeft,c.y+=e.clientTop):o&&(c.x=Ut(o))),{x:r.left+l.scrollLeft-c.x,y:r.top+l.scrollTop-c.y,width:r.width,height:r.height}}var ae={placement:"bottom",modifiers:[],strategy:"absolute"};function le(){for(var t=arguments.length,e=new Array(t),i=0;i"applyStyles"===t.name&&!1===t.enabled);this._popper=ue(e,this._menu,i),n&&U.setDataAttribute(this._menu,"popper","static")}"ontouchstart"in document.documentElement&&!t.closest(".navbar-nav")&&[].concat(...document.body.children).forEach(t=>P.on(t,"mouseover",u)),this._element.focus(),this._element.setAttribute("aria-expanded",!0),this._menu.classList.toggle("show"),this._element.classList.toggle("show"),P.trigger(this._element,"shown.bs.dropdown",e)}}hide(){if(h(this._element)||!this._menu.classList.contains("show"))return;const t={relatedTarget:this._element};this._completeHide(t)}dispose(){this._popper&&this._popper.destroy(),super.dispose()}update(){this._inNavbar=this._detectNavbar(),this._popper&&this._popper.update()}_addEventListeners(){P.on(this._element,"click.bs.dropdown",t=>{t.preventDefault(),this.toggle()})}_completeHide(t){P.trigger(this._element,"hide.bs.dropdown",t).defaultPrevented||("ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach(t=>P.off(t,"mouseover",u)),this._popper&&this._popper.destroy(),this._menu.classList.remove("show"),this._element.classList.remove("show"),this._element.setAttribute("aria-expanded","false"),U.removeDataAttribute(this._menu,"popper"),P.trigger(this._element,"hidden.bs.dropdown",t))}_getConfig(t){if(t={...this.constructor.Default,...U.getDataAttributes(this._element),...t},l("dropdown",t,this.constructor.DefaultType),"object"==typeof t.reference&&!r(t.reference)&&"function"!=typeof t.reference.getBoundingClientRect)throw new TypeError("dropdown".toUpperCase()+': Option "reference" provided type "object" without a required "getBoundingClientRect" method.');return t}_getMenuElement(){return t.next(this._element,".dropdown-menu")[0]}_getPlacement(){const t=this._element.parentNode;if(t.classList.contains("dropend"))return ve;if(t.classList.contains("dropstart"))return ye;const e="end"===getComputedStyle(this._menu).getPropertyValue("--bs-position").trim();return t.classList.contains("dropup")?e?ge:me:e?be:_e}_detectNavbar(){return null!==this._element.closest(".navbar")}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map(t=>Number.parseInt(t,10)):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return"static"===this._config.display&&(t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,..."function"==typeof this._config.popperConfig?this._config.popperConfig(t):this._config.popperConfig}}_selectMenuItem({key:e,target:i}){const n=t.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter(c);n.length&&y(n,i,"ArrowDown"===e,!n.includes(i)).focus()}static dropdownInterface(t,e){const i=Ae.getOrCreateInstance(t,e);if("string"==typeof e){if(void 0===i[e])throw new TypeError(`No method named "${e}"`);i[e]()}}static jQueryInterface(t){return this.each((function(){Ae.dropdownInterface(this,t)}))}static clearMenus(e){if(e&&(2===e.button||"keyup"===e.type&&"Tab"!==e.key))return;const i=t.find('[data-bs-toggle="dropdown"]');for(let t=0,n=i.length;tthis.matches('[data-bs-toggle="dropdown"]')?this:t.prev(this,'[data-bs-toggle="dropdown"]')[0];return"Escape"===e.key?(n().focus(),void Ae.clearMenus()):"ArrowUp"===e.key||"ArrowDown"===e.key?(i||n().click(),void Ae.getInstance(n())._selectMenuItem(e)):void(i&&"Space"!==e.key||Ae.clearMenus())}}P.on(document,"keydown.bs.dropdown.data-api",'[data-bs-toggle="dropdown"]',Ae.dataApiKeydownHandler),P.on(document,"keydown.bs.dropdown.data-api",".dropdown-menu",Ae.dataApiKeydownHandler),P.on(document,"click.bs.dropdown.data-api",Ae.clearMenus),P.on(document,"keyup.bs.dropdown.data-api",Ae.clearMenus),P.on(document,"click.bs.dropdown.data-api",'[data-bs-toggle="dropdown"]',(function(t){t.preventDefault(),Ae.dropdownInterface(this)})),_(Ae);class Te{constructor(){this._element=document.body}getWidth(){const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)}hide(){const t=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,"paddingRight",e=>e+t),this._setElementAttributes(".fixed-top, .fixed-bottom, .is-fixed, .sticky-top","paddingRight",e=>e+t),this._setElementAttributes(".sticky-top","marginRight",e=>e-t)}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const n=this.getWidth();this._applyManipulationCallback(t,t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+n)return;this._saveInitialAttribute(t,e);const s=window.getComputedStyle(t)[e];t.style[e]=i(Number.parseFloat(s))+"px"})}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,"paddingRight"),this._resetElementAttributes(".fixed-top, .fixed-bottom, .is-fixed, .sticky-top","paddingRight"),this._resetElementAttributes(".sticky-top","marginRight")}_saveInitialAttribute(t,e){const i=t.style[e];i&&U.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,t=>{const i=U.getDataAttribute(t,e);void 0===i?t.style.removeProperty(e):(U.removeDataAttribute(t,e),t.style[e]=i)})}_applyManipulationCallback(e,i){r(e)?i(e):t.find(e,this._element).forEach(i)}isOverflowing(){return this.getWidth()>0}}const Oe={isVisible:!0,isAnimated:!1,rootElement:"body",clickCallback:null},Ce={isVisible:"boolean",isAnimated:"boolean",rootElement:"(element|string)",clickCallback:"(function|null)"};class ke{constructor(t){this._config=this._getConfig(t),this._isAppended=!1,this._element=null}show(t){this._config.isVisible?(this._append(),this._config.isAnimated&&f(this._getElement()),this._getElement().classList.add("show"),this._emulateAnimation(()=>{b(t)})):b(t)}hide(t){this._config.isVisible?(this._getElement().classList.remove("show"),this._emulateAnimation(()=>{this.dispose(),b(t)})):b(t)}_getElement(){if(!this._element){const t=document.createElement("div");t.className="modal-backdrop",this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_getConfig(t){return(t={...Oe,..."object"==typeof t?t:{}}).rootElement=a(t.rootElement),l("backdrop",t,Ce),t}_append(){this._isAppended||(this._config.rootElement.appendChild(this._getElement()),P.on(this._getElement(),"mousedown.bs.backdrop",()=>{b(this._config.clickCallback)}),this._isAppended=!0)}dispose(){this._isAppended&&(P.off(this._element,"mousedown.bs.backdrop"),this._element.remove(),this._isAppended=!1)}_emulateAnimation(t){v(t,this._getElement(),this._config.isAnimated)}}const Le={backdrop:!0,keyboard:!0,focus:!0},xe={backdrop:"(boolean|string)",keyboard:"boolean",focus:"boolean"};class De extends B{constructor(e,i){super(e),this._config=this._getConfig(i),this._dialog=t.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._isShown=!1,this._ignoreBackdropClick=!1,this._isTransitioning=!1,this._scrollBar=new Te}static get Default(){return Le}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||P.trigger(this._element,"show.bs.modal",{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isAnimated()&&(this._isTransitioning=!0),this._scrollBar.hide(),document.body.classList.add("modal-open"),this._adjustDialog(),this._setEscapeEvent(),this._setResizeEvent(),P.on(this._element,"click.dismiss.bs.modal",'[data-bs-dismiss="modal"]',t=>this.hide(t)),P.on(this._dialog,"mousedown.dismiss.bs.modal",()=>{P.one(this._element,"mouseup.dismiss.bs.modal",t=>{t.target===this._element&&(this._ignoreBackdropClick=!0)})}),this._showBackdrop(()=>this._showElement(t)))}hide(t){if(t&&["A","AREA"].includes(t.target.tagName)&&t.preventDefault(),!this._isShown||this._isTransitioning)return;if(P.trigger(this._element,"hide.bs.modal").defaultPrevented)return;this._isShown=!1;const e=this._isAnimated();e&&(this._isTransitioning=!0),this._setEscapeEvent(),this._setResizeEvent(),P.off(document,"focusin.bs.modal"),this._element.classList.remove("show"),P.off(this._element,"click.dismiss.bs.modal"),P.off(this._dialog,"mousedown.dismiss.bs.modal"),this._queueCallback(()=>this._hideModal(),this._element,e)}dispose(){[window,this._dialog].forEach(t=>P.off(t,".bs.modal")),this._backdrop.dispose(),super.dispose(),P.off(document,"focusin.bs.modal")}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new ke({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_getConfig(t){return t={...Le,...U.getDataAttributes(this._element),..."object"==typeof t?t:{}},l("modal",t,xe),t}_showElement(e){const i=this._isAnimated(),n=t.findOne(".modal-body",this._dialog);this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE||document.body.appendChild(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0,n&&(n.scrollTop=0),i&&f(this._element),this._element.classList.add("show"),this._config.focus&&this._enforceFocus(),this._queueCallback(()=>{this._config.focus&&this._element.focus(),this._isTransitioning=!1,P.trigger(this._element,"shown.bs.modal",{relatedTarget:e})},this._dialog,i)}_enforceFocus(){P.off(document,"focusin.bs.modal"),P.on(document,"focusin.bs.modal",t=>{document===t.target||this._element===t.target||this._element.contains(t.target)||this._element.focus()})}_setEscapeEvent(){this._isShown?P.on(this._element,"keydown.dismiss.bs.modal",t=>{this._config.keyboard&&"Escape"===t.key?(t.preventDefault(),this.hide()):this._config.keyboard||"Escape"!==t.key||this._triggerBackdropTransition()}):P.off(this._element,"keydown.dismiss.bs.modal")}_setResizeEvent(){this._isShown?P.on(window,"resize.bs.modal",()=>this._adjustDialog()):P.off(window,"resize.bs.modal")}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide(()=>{document.body.classList.remove("modal-open"),this._resetAdjustments(),this._scrollBar.reset(),P.trigger(this._element,"hidden.bs.modal")})}_showBackdrop(t){P.on(this._element,"click.dismiss.bs.modal",t=>{this._ignoreBackdropClick?this._ignoreBackdropClick=!1:t.target===t.currentTarget&&(!0===this._config.backdrop?this.hide():"static"===this._config.backdrop&&this._triggerBackdropTransition())}),this._backdrop.show(t)}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(P.trigger(this._element,"hidePrevented.bs.modal").defaultPrevented)return;const{classList:t,scrollHeight:e,style:i}=this._element,n=e>document.documentElement.clientHeight;!n&&"hidden"===i.overflowY||t.contains("modal-static")||(n||(i.overflowY="hidden"),t.add("modal-static"),this._queueCallback(()=>{t.remove("modal-static"),n||this._queueCallback(()=>{i.overflowY=""},this._dialog)},this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;(!i&&t&&!g()||i&&!t&&g())&&(this._element.style.paddingLeft=e+"px"),(i&&!t&&!g()||!i&&t&&g())&&(this._element.style.paddingRight=e+"px")}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=De.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}P.on(document,"click.bs.modal.data-api",'[data-bs-toggle="modal"]',(function(t){const e=s(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),P.one(e,"show.bs.modal",t=>{t.defaultPrevented||P.one(e,"hidden.bs.modal",()=>{c(this)&&this.focus()})}),De.getOrCreateInstance(e).toggle(this)})),_(De);const Se={backdrop:!0,keyboard:!0,scroll:!1},Ie={backdrop:"boolean",keyboard:"boolean",scroll:"boolean"};class Ne extends B{constructor(t,e){super(t),this._config=this._getConfig(e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._addEventListeners()}static get NAME(){return"offcanvas"}static get Default(){return Se}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||P.trigger(this._element,"show.bs.offcanvas",{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._element.style.visibility="visible",this._backdrop.show(),this._config.scroll||((new Te).hide(),this._enforceFocusOnElement(this._element)),this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add("show"),this._queueCallback(()=>{P.trigger(this._element,"shown.bs.offcanvas",{relatedTarget:t})},this._element,!0))}hide(){this._isShown&&(P.trigger(this._element,"hide.bs.offcanvas").defaultPrevented||(P.off(document,"focusin.bs.offcanvas"),this._element.blur(),this._isShown=!1,this._element.classList.remove("show"),this._backdrop.hide(),this._queueCallback(()=>{this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._element.style.visibility="hidden",this._config.scroll||(new Te).reset(),P.trigger(this._element,"hidden.bs.offcanvas")},this._element,!0)))}dispose(){this._backdrop.dispose(),super.dispose(),P.off(document,"focusin.bs.offcanvas")}_getConfig(t){return t={...Se,...U.getDataAttributes(this._element),..."object"==typeof t?t:{}},l("offcanvas",t,Ie),t}_initializeBackDrop(){return new ke({isVisible:this._config.backdrop,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:()=>this.hide()})}_enforceFocusOnElement(t){P.off(document,"focusin.bs.offcanvas"),P.on(document,"focusin.bs.offcanvas",e=>{document===e.target||t===e.target||t.contains(e.target)||t.focus()}),t.focus()}_addEventListeners(){P.on(this._element,"click.dismiss.bs.offcanvas",'[data-bs-dismiss="offcanvas"]',()=>this.hide()),P.on(this._element,"keydown.dismiss.bs.offcanvas",t=>{this._config.keyboard&&"Escape"===t.key&&this.hide()})}static jQueryInterface(t){return this.each((function(){const e=Ne.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}P.on(document,"click.bs.offcanvas.data-api",'[data-bs-toggle="offcanvas"]',(function(e){const i=s(this);if(["A","AREA"].includes(this.tagName)&&e.preventDefault(),h(this))return;P.one(i,"hidden.bs.offcanvas",()=>{c(this)&&this.focus()});const n=t.findOne(".offcanvas.show");n&&n!==i&&Ne.getInstance(n).hide(),Ne.getOrCreateInstance(i).toggle(this)})),P.on(window,"load.bs.offcanvas.data-api",()=>t.find(".offcanvas.show").forEach(t=>Ne.getOrCreateInstance(t).show())),_(Ne);const je=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Me=/^(?:(?:https?|mailto|ftp|tel|file):|[^#&/:?]*(?:[#/?]|$))/i,Pe=/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z]+=*$/i,He=(t,e)=>{const i=t.nodeName.toLowerCase();if(e.includes(i))return!je.has(i)||Boolean(Me.test(t.nodeValue)||Pe.test(t.nodeValue));const n=e.filter(t=>t instanceof RegExp);for(let t=0,e=n.length;t{He(t,a)||i.removeAttribute(t.nodeName)})}return n.body.innerHTML}const Be=new RegExp("(^|\\s)bs-tooltip\\S+","g"),We=new Set(["sanitize","allowList","sanitizeFn"]),qe={animation:"boolean",template:"string",title:"(string|element|function)",trigger:"string",delay:"(number|object)",html:"boolean",selector:"(string|boolean)",placement:"(string|function)",offset:"(array|string|function)",container:"(string|element|boolean)",fallbackPlacements:"array",boundary:"(string|element)",customClass:"(string|function)",sanitize:"boolean",sanitizeFn:"(null|function)",allowList:"object",popperConfig:"(null|object|function)"},ze={AUTO:"auto",TOP:"top",RIGHT:g()?"left":"right",BOTTOM:"bottom",LEFT:g()?"right":"left"},$e={animation:!0,template:'',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:[0,0],container:!1,fallbackPlacements:["top","right","bottom","left"],boundary:"clippingParents",customClass:"",sanitize:!0,sanitizeFn:null,allowList:{"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},popperConfig:null},Ue={HIDE:"hide.bs.tooltip",HIDDEN:"hidden.bs.tooltip",SHOW:"show.bs.tooltip",SHOWN:"shown.bs.tooltip",INSERTED:"inserted.bs.tooltip",CLICK:"click.bs.tooltip",FOCUSIN:"focusin.bs.tooltip",FOCUSOUT:"focusout.bs.tooltip",MOUSEENTER:"mouseenter.bs.tooltip",MOUSELEAVE:"mouseleave.bs.tooltip"};class Fe extends B{constructor(t,e){if(void 0===fe)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t),this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this._config=this._getConfig(e),this.tip=null,this._setListeners()}static get Default(){return $e}static get NAME(){return"tooltip"}static get Event(){return Ue}static get DefaultType(){return qe}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(t){if(this._isEnabled)if(t){const e=this._initializeOnDelegatedTarget(t);e._activeTrigger.click=!e._activeTrigger.click,e._isWithActiveTrigger()?e._enter(null,e):e._leave(null,e)}else{if(this.getTipElement().classList.contains("show"))return void this._leave(null,this);this._enter(null,this)}}dispose(){clearTimeout(this._timeout),P.off(this._element.closest(".modal"),"hide.bs.modal",this._hideModalHandler),this.tip&&this.tip.remove(),this._popper&&this._popper.destroy(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this.isWithContent()||!this._isEnabled)return;const t=P.trigger(this._element,this.constructor.Event.SHOW),i=d(this._element),n=null===i?this._element.ownerDocument.documentElement.contains(this._element):i.contains(this._element);if(t.defaultPrevented||!n)return;const s=this.getTipElement(),o=e(this.constructor.NAME);s.setAttribute("id",o),this._element.setAttribute("aria-describedby",o),this.setContent(),this._config.animation&&s.classList.add("fade");const r="function"==typeof this._config.placement?this._config.placement.call(this,s,this._element):this._config.placement,a=this._getAttachment(r);this._addAttachmentClass(a);const{container:l}=this._config;R.set(s,this.constructor.DATA_KEY,this),this._element.ownerDocument.documentElement.contains(this.tip)||(l.appendChild(s),P.trigger(this._element,this.constructor.Event.INSERTED)),this._popper?this._popper.update():this._popper=ue(this._element,s,this._getPopperConfig(a)),s.classList.add("show");const c="function"==typeof this._config.customClass?this._config.customClass():this._config.customClass;c&&s.classList.add(...c.split(" ")),"ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach(t=>{P.on(t,"mouseover",u)});const h=this.tip.classList.contains("fade");this._queueCallback(()=>{const t=this._hoverState;this._hoverState=null,P.trigger(this._element,this.constructor.Event.SHOWN),"out"===t&&this._leave(null,this)},this.tip,h)}hide(){if(!this._popper)return;const t=this.getTipElement();if(P.trigger(this._element,this.constructor.Event.HIDE).defaultPrevented)return;t.classList.remove("show"),"ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach(t=>P.off(t,"mouseover",u)),this._activeTrigger.click=!1,this._activeTrigger.focus=!1,this._activeTrigger.hover=!1;const e=this.tip.classList.contains("fade");this._queueCallback(()=>{this._isWithActiveTrigger()||("show"!==this._hoverState&&t.remove(),this._cleanTipClass(),this._element.removeAttribute("aria-describedby"),P.trigger(this._element,this.constructor.Event.HIDDEN),this._popper&&(this._popper.destroy(),this._popper=null))},this.tip,e),this._hoverState=""}update(){null!==this._popper&&this._popper.update()}isWithContent(){return Boolean(this.getTitle())}getTipElement(){if(this.tip)return this.tip;const t=document.createElement("div");return t.innerHTML=this._config.template,this.tip=t.children[0],this.tip}setContent(){const e=this.getTipElement();this.setElementContent(t.findOne(".tooltip-inner",e),this.getTitle()),e.classList.remove("fade","show")}setElementContent(t,e){if(null!==t)return r(e)?(e=a(e),void(this._config.html?e.parentNode!==t&&(t.innerHTML="",t.appendChild(e)):t.textContent=e.textContent)):void(this._config.html?(this._config.sanitize&&(e=Re(e,this._config.allowList,this._config.sanitizeFn)),t.innerHTML=e):t.textContent=e)}getTitle(){let t=this._element.getAttribute("data-bs-original-title");return t||(t="function"==typeof this._config.title?this._config.title.call(this._element):this._config.title),t}updateAttachment(t){return"right"===t?"end":"left"===t?"start":t}_initializeOnDelegatedTarget(t,e){const i=this.constructor.DATA_KEY;return(e=e||R.get(t.delegateTarget,i))||(e=new this.constructor(t.delegateTarget,this._getDelegateConfig()),R.set(t.delegateTarget,i,e)),e}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map(t=>Number.parseInt(t,10)):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"onChange",enabled:!0,phase:"afterWrite",fn:t=>this._handlePopperPlacementChange(t)}],onFirstUpdate:t=>{t.options.placement!==t.placement&&this._handlePopperPlacementChange(t)}};return{...e,..."function"==typeof this._config.popperConfig?this._config.popperConfig(e):this._config.popperConfig}}_addAttachmentClass(t){this.getTipElement().classList.add("bs-tooltip-"+this.updateAttachment(t))}_getAttachment(t){return ze[t.toUpperCase()]}_setListeners(){this._config.trigger.split(" ").forEach(t=>{if("click"===t)P.on(this._element,this.constructor.Event.CLICK,this._config.selector,t=>this.toggle(t));else if("manual"!==t){const e="hover"===t?this.constructor.Event.MOUSEENTER:this.constructor.Event.FOCUSIN,i="hover"===t?this.constructor.Event.MOUSELEAVE:this.constructor.Event.FOCUSOUT;P.on(this._element,e,this._config.selector,t=>this._enter(t)),P.on(this._element,i,this._config.selector,t=>this._leave(t))}}),this._hideModalHandler=()=>{this._element&&this.hide()},P.on(this._element.closest(".modal"),"hide.bs.modal",this._hideModalHandler),this._config.selector?this._config={...this._config,trigger:"manual",selector:""}:this._fixTitle()}_fixTitle(){const t=this._element.getAttribute("title"),e=typeof this._element.getAttribute("data-bs-original-title");(t||"string"!==e)&&(this._element.setAttribute("data-bs-original-title",t||""),!t||this._element.getAttribute("aria-label")||this._element.textContent||this._element.setAttribute("aria-label",t),this._element.setAttribute("title",""))}_enter(t,e){e=this._initializeOnDelegatedTarget(t,e),t&&(e._activeTrigger["focusin"===t.type?"focus":"hover"]=!0),e.getTipElement().classList.contains("show")||"show"===e._hoverState?e._hoverState="show":(clearTimeout(e._timeout),e._hoverState="show",e._config.delay&&e._config.delay.show?e._timeout=setTimeout(()=>{"show"===e._hoverState&&e.show()},e._config.delay.show):e.show())}_leave(t,e){e=this._initializeOnDelegatedTarget(t,e),t&&(e._activeTrigger["focusout"===t.type?"focus":"hover"]=e._element.contains(t.relatedTarget)),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState="out",e._config.delay&&e._config.delay.hide?e._timeout=setTimeout(()=>{"out"===e._hoverState&&e.hide()},e._config.delay.hide):e.hide())}_isWithActiveTrigger(){for(const t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1}_getConfig(t){const e=U.getDataAttributes(this._element);return Object.keys(e).forEach(t=>{We.has(t)&&delete e[t]}),(t={...this.constructor.Default,...e,..."object"==typeof t&&t?t:{}}).container=!1===t.container?document.body:a(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),l("tooltip",t,this.constructor.DefaultType),t.sanitize&&(t.template=Re(t.template,t.allowList,t.sanitizeFn)),t}_getDelegateConfig(){const t={};if(this._config)for(const e in this._config)this.constructor.Default[e]!==this._config[e]&&(t[e]=this._config[e]);return t}_cleanTipClass(){const t=this.getTipElement(),e=t.getAttribute("class").match(Be);null!==e&&e.length>0&&e.map(t=>t.trim()).forEach(e=>t.classList.remove(e))}_handlePopperPlacementChange(t){const{state:e}=t;e&&(this.tip=e.elements.popper,this._cleanTipClass(),this._addAttachmentClass(this._getAttachment(e.placement)))}static jQueryInterface(t){return this.each((function(){const e=Fe.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}_(Fe);const Ve=new RegExp("(^|\\s)bs-popover\\S+","g"),Ke={...Fe.Default,placement:"right",offset:[0,8],trigger:"click",content:"",template:''},Xe={...Fe.DefaultType,content:"(string|element|function)"},Ye={HIDE:"hide.bs.popover",HIDDEN:"hidden.bs.popover",SHOW:"show.bs.popover",SHOWN:"shown.bs.popover",INSERTED:"inserted.bs.popover",CLICK:"click.bs.popover",FOCUSIN:"focusin.bs.popover",FOCUSOUT:"focusout.bs.popover",MOUSEENTER:"mouseenter.bs.popover",MOUSELEAVE:"mouseleave.bs.popover"};class Qe extends Fe{static get Default(){return Ke}static get NAME(){return"popover"}static get Event(){return Ye}static get DefaultType(){return Xe}isWithContent(){return this.getTitle()||this._getContent()}getTipElement(){return this.tip||(this.tip=super.getTipElement(),this.getTitle()||t.findOne(".popover-header",this.tip).remove(),this._getContent()||t.findOne(".popover-body",this.tip).remove()),this.tip}setContent(){const e=this.getTipElement();this.setElementContent(t.findOne(".popover-header",e),this.getTitle());let i=this._getContent();"function"==typeof i&&(i=i.call(this._element)),this.setElementContent(t.findOne(".popover-body",e),i),e.classList.remove("fade","show")}_addAttachmentClass(t){this.getTipElement().classList.add("bs-popover-"+this.updateAttachment(t))}_getContent(){return this._element.getAttribute("data-bs-content")||this._config.content}_cleanTipClass(){const t=this.getTipElement(),e=t.getAttribute("class").match(Ve);null!==e&&e.length>0&&e.map(t=>t.trim()).forEach(e=>t.classList.remove(e))}static jQueryInterface(t){return this.each((function(){const e=Qe.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}_(Qe);const Ge={offset:10,method:"auto",target:""},Ze={offset:"number",method:"string",target:"(string|element)"};class Je extends B{constructor(t,e){super(t),this._scrollElement="BODY"===this._element.tagName?window:this._element,this._config=this._getConfig(e),this._selector=`${this._config.target} .nav-link, ${this._config.target} .list-group-item, ${this._config.target} .dropdown-item`,this._offsets=[],this._targets=[],this._activeTarget=null,this._scrollHeight=0,P.on(this._scrollElement,"scroll.bs.scrollspy",()=>this._process()),this.refresh(),this._process()}static get Default(){return Ge}static get NAME(){return"scrollspy"}refresh(){const e=this._scrollElement===this._scrollElement.window?"offset":"position",i="auto"===this._config.method?e:this._config.method,s="position"===i?this._getScrollTop():0;this._offsets=[],this._targets=[],this._scrollHeight=this._getScrollHeight(),t.find(this._selector).map(e=>{const o=n(e),r=o?t.findOne(o):null;if(r){const t=r.getBoundingClientRect();if(t.width||t.height)return[U[i](r).top+s,o]}return null}).filter(t=>t).sort((t,e)=>t[0]-e[0]).forEach(t=>{this._offsets.push(t[0]),this._targets.push(t[1])})}dispose(){P.off(this._scrollElement,".bs.scrollspy"),super.dispose()}_getConfig(t){if("string"!=typeof(t={...Ge,...U.getDataAttributes(this._element),..."object"==typeof t&&t?t:{}}).target&&r(t.target)){let{id:i}=t.target;i||(i=e("scrollspy"),t.target.id=i),t.target="#"+i}return l("scrollspy",t,Ze),t}_getScrollTop(){return this._scrollElement===window?this._scrollElement.pageYOffset:this._scrollElement.scrollTop}_getScrollHeight(){return this._scrollElement.scrollHeight||Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)}_getOffsetHeight(){return this._scrollElement===window?window.innerHeight:this._scrollElement.getBoundingClientRect().height}_process(){const t=this._getScrollTop()+this._config.offset,e=this._getScrollHeight(),i=this._config.offset+e-this._getOffsetHeight();if(this._scrollHeight!==e&&this.refresh(),t>=i){const t=this._targets[this._targets.length-1];this._activeTarget!==t&&this._activate(t)}else{if(this._activeTarget&&t0)return this._activeTarget=null,void this._clear();for(let e=this._offsets.length;e--;)this._activeTarget!==this._targets[e]&&t>=this._offsets[e]&&(void 0===this._offsets[e+1]||t`${t}[data-bs-target="${e}"],${t}[href="${e}"]`),n=t.findOne(i.join(","));n.classList.contains("dropdown-item")?(t.findOne(".dropdown-toggle",n.closest(".dropdown")).classList.add("active"),n.classList.add("active")):(n.classList.add("active"),t.parents(n,".nav, .list-group").forEach(e=>{t.prev(e,".nav-link, .list-group-item").forEach(t=>t.classList.add("active")),t.prev(e,".nav-item").forEach(e=>{t.children(e,".nav-link").forEach(t=>t.classList.add("active"))})})),P.trigger(this._scrollElement,"activate.bs.scrollspy",{relatedTarget:e})}_clear(){t.find(this._selector).filter(t=>t.classList.contains("active")).forEach(t=>t.classList.remove("active"))}static jQueryInterface(t){return this.each((function(){const e=Je.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}P.on(window,"load.bs.scrollspy.data-api",()=>{t.find('[data-bs-spy="scroll"]').forEach(t=>new Je(t))}),_(Je);class ti extends B{static get NAME(){return"tab"}show(){if(this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE&&this._element.classList.contains("active"))return;let e;const i=s(this._element),n=this._element.closest(".nav, .list-group");if(n){const i="UL"===n.nodeName||"OL"===n.nodeName?":scope > li > .active":".active";e=t.find(i,n),e=e[e.length-1]}const o=e?P.trigger(e,"hide.bs.tab",{relatedTarget:this._element}):null;if(P.trigger(this._element,"show.bs.tab",{relatedTarget:e}).defaultPrevented||null!==o&&o.defaultPrevented)return;this._activate(this._element,n);const r=()=>{P.trigger(e,"hidden.bs.tab",{relatedTarget:this._element}),P.trigger(this._element,"shown.bs.tab",{relatedTarget:e})};i?this._activate(i,i.parentNode,r):r()}_activate(e,i,n){const s=(!i||"UL"!==i.nodeName&&"OL"!==i.nodeName?t.children(i,".active"):t.find(":scope > li > .active",i))[0],o=n&&s&&s.classList.contains("fade"),r=()=>this._transitionComplete(e,s,n);s&&o?(s.classList.remove("show"),this._queueCallback(r,e,!0)):r()}_transitionComplete(e,i,n){if(i){i.classList.remove("active");const e=t.findOne(":scope > .dropdown-menu .active",i.parentNode);e&&e.classList.remove("active"),"tab"===i.getAttribute("role")&&i.setAttribute("aria-selected",!1)}e.classList.add("active"),"tab"===e.getAttribute("role")&&e.setAttribute("aria-selected",!0),f(e),e.classList.contains("fade")&&e.classList.add("show");let s=e.parentNode;if(s&&"LI"===s.nodeName&&(s=s.parentNode),s&&s.classList.contains("dropdown-menu")){const i=e.closest(".dropdown");i&&t.find(".dropdown-toggle",i).forEach(t=>t.classList.add("active")),e.setAttribute("aria-expanded",!0)}n&&n()}static jQueryInterface(t){return this.each((function(){const e=ti.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}P.on(document,"click.bs.tab.data-api",'[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),h(this)||ti.getOrCreateInstance(this).show()})),_(ti);const ei={animation:"boolean",autohide:"boolean",delay:"number"},ii={animation:!0,autohide:!0,delay:5e3};class ni extends B{constructor(t,e){super(t),this._config=this._getConfig(e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get DefaultType(){return ei}static get Default(){return ii}static get NAME(){return"toast"}show(){P.trigger(this._element,"show.bs.toast").defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove("hide"),f(this._element),this._element.classList.add("showing"),this._queueCallback(()=>{this._element.classList.remove("showing"),this._element.classList.add("show"),P.trigger(this._element,"shown.bs.toast"),this._maybeScheduleHide()},this._element,this._config.animation))}hide(){this._element.classList.contains("show")&&(P.trigger(this._element,"hide.bs.toast").defaultPrevented||(this._element.classList.remove("show"),this._queueCallback(()=>{this._element.classList.add("hide"),P.trigger(this._element,"hidden.bs.toast")},this._element,this._config.animation)))}dispose(){this._clearTimeout(),this._element.classList.contains("show")&&this._element.classList.remove("show"),super.dispose()}_getConfig(t){return t={...ii,...U.getDataAttributes(this._element),..."object"==typeof t&&t?t:{}},l("toast",t,this.constructor.DefaultType),t}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout(()=>{this.hide()},this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){P.on(this._element,"click.dismiss.bs.toast",'[data-bs-dismiss="toast"]',()=>this.hide()),P.on(this._element,"mouseover.bs.toast",t=>this._onInteraction(t,!0)),P.on(this._element,"mouseout.bs.toast",t=>this._onInteraction(t,!1)),P.on(this._element,"focusin.bs.toast",t=>this._onInteraction(t,!0)),P.on(this._element,"focusout.bs.toast",t=>this._onInteraction(t,!1))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=ni.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}return _(ni),{Alert:W,Button:q,Carousel:Z,Collapse:et,Dropdown:Ae,Modal:De,Offcanvas:Ne,Popover:Qe,ScrollSpy:Je,Tab:ti,Toast:ni,Tooltip:Fe}})); +//# sourceMappingURL=bootstrap.bundle.min.js.map \ No newline at end of file diff --git a/Firmware/RTK_Surveyor/AP-Config/src/bootstrap.min.css b/Firmware/RTK_Surveyor/AP-Config/src/bootstrap.min.css new file mode 100644 index 000000000..07f9a3ef2 --- /dev/null +++ b/Firmware/RTK_Surveyor/AP-Config/src/bootstrap.min.css @@ -0,0 +1,7 @@ +@charset "UTF-8";/*! + * Bootstrap v5.0.2 (https://getbootstrap.com/) + * Copyright 2011-2021 The Bootstrap Authors + * Copyright 2011-2021 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0))}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-font-sans-serif);font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}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}b,strong{font-weight:bolder}.small,small{font-size:.775em; color:#888888;}.mark,mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.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:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.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:.875em;color:#6c757d}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{width:100%;padding-right:var(--bs-gutter-x,.75rem);padding-left:var(--bs-gutter-x,.75rem);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(var(--bs-gutter-y) * -1);margin-right:calc(var(--bs-gutter-x) * -.5);margin-left:calc(var(--bs-gutter-x) * -.5)}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.6666666667%}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.6666666667%}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.6666666667%}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.6666666667%}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.6666666667%}}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-bg:transparent;--bs-table-accent-bg:transparent;--bs-table-striped-color:#212529;--bs-table-striped-bg:rgba(0, 0, 0, 0.05);--bs-table-active-color:#212529;--bs-table-active-bg:rgba(0, 0, 0, 0.1);--bs-table-hover-color:#212529;--bs-table-hover-bg:rgba(0, 0, 0, 0.075);width:100%;margin-bottom:1rem;color:#212529;vertical-align:top;border-color:#dee2e6}.table>:not(caption)>*>*{padding:.5rem .5rem;background-color:var(--bs-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--bs-table-accent-bg)}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table>:not(:last-child)>:last-child>*{border-bottom-color:currentColor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-striped>tbody>tr:nth-of-type(odd){--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-active{--bs-table-accent-bg:var(--bs-table-active-bg);color:var(--bs-table-active-color)}.table-hover>tbody>tr:hover{--bs-table-accent-bg:var(--bs-table-hover-bg);color:var(--bs-table-hover-color)}.table-primary{--bs-table-bg:#cfe2ff;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000;color:#000;border-color:#bacbe6}.table-secondary{--bs-table-bg:#e2e3e5;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000;color:#000;border-color:#cbccce}.table-success{--bs-table-bg:#d1e7dd;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000;color:#000;border-color:#bcd0c7}.table-info{--bs-table-bg:#cff4fc;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000;color:#000;border-color:#badce3}.table-warning{--bs-table-bg:#fff3cd;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000;color:#000;border-color:#e6dbb9}.table-danger{--bs-table-bg:#f8d7da;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000;color:#000;border-color:#dfc2c4}.table-light{--bs-table-bg:#f8f9fa;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000;color:#000;border-color:#dfe0e1}.table-dark{--bs-table-bg:#212529;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:#fff;border-color:#373b3e}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.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}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:#6c757d}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:#212529;background-color:#fff;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{height:1.5em}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#dde0e3}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + (.5rem + 2px));padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + (1rem + 2px));padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + (.75rem + 2px))}textarea.form-control-sm{min-height:calc(1.5em + (.5rem + 2px))}textarea.form-control-lg{min-height:calc(1.5em + (1rem + 2px))}.form-control-color{max-width:3rem;height:auto;padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{height:1.5em;border-radius:.25rem}.form-control-color::-webkit-color-swatch{height:1.5em;border-radius:.25rem}.form-select{display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;-moz-padding-start:calc(0.75rem - 3px);font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:#e9ecef}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #212529}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-input{width:1em;height:1em;margin-top:.25em;vertical-align:top;background-color:#fff;background-repeat:no-repeat;background-position:center;background-size:contain;border:1px solid rgba(0,0,0,.25);-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-print-color-adjust:exact;color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-check-input:checked{background-color:#0d6efd;border-color:#0d6efd}.form-check-input:checked[type=checkbox]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{width:2em;margin-left:-2.5em;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}.form-range{width:100%;height:1.5rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#0d6efd;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b6d4fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#0d6efd;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#b6d4fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.form-range:disabled::-moz-range-thumb{background-color:#adb5bd}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-select{height:calc(3.5rem + 2px);line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;height:100%;padding:1rem .75rem;pointer-events:none;border:1px solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control{padding:1rem .75rem}.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-select:focus{z-index:3}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:3}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#198754}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(25,135,84,.9);border-radius:.25rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#198754;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:#198754}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:#198754}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:#198754}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#198754}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group .form-control.is-valid,.input-group .form-select.is-valid,.was-validated .input-group .form-control:valid,.was-validated .input-group .form-select:valid{z-index:1}.input-group .form-control.is-valid:focus,.input-group .form-select.is-valid:focus,.was-validated .input-group .form-control:valid:focus,.was-validated .input-group .form-select:valid:focus{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:#dc3545}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:#dc3545}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:#dc3545}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group .form-control.is-invalid,.input-group .form-select.is-invalid,.was-validated .input-group .form-control:invalid,.was-validated .input-group .form-select:invalid{z-index:2}.input-group .form-control.is-invalid:focus,.input-group .form-select.is-invalid:focus,.was-validated .input-group .form-control:invalid:focus,.was-validated .input-group .form-select:invalid:focus{z-index:3}.btn{display:inline-block;font-weight:400;line-height:1.5;color:#212529;text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;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}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529}.btn-check:focus+.btn,.btn:focus{outline:0;box-shadow:0 0 8 .25rem rgba(62,160,170,.25)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{pointer-events:none;opacity:.65}.btn-primary{color:#fff;background-color:#3EA0B1;border-color:#93D5E2}.btn-primary:hover{color:#fff;background-color:#338695;border-color:#3EA0B1}.btn-check:focus+.btn-primary,.btn-primary:focus{color:#fff;background-color:#3EA0B1;border-color:#93D5E2;box-shadow:0 0 0 .25rem rgba(62,160,170,.25)}.btn-check:active+.btn-primary,.btn-check:checked+.btn-primary,.btn-primary.active,.btn-primary:active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#3EA0B1;border-color:#93D5E2}.btn-check:active+.btn-primary:focus,.btn-check:checked+.btn-primary:focus,.btn-primary.active:focus,.btn-primary:active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:inset 0 0 0 .25rem rgba(62,160,170,.25)}.btn-primary.disabled,.btn-primary:disabled{color:#87b9c1;background-color:#e1edef;border-color:#e1edef}.btn-secondary{color:#4D4D4D;background-color:#FFAB58;border-color:#FFD0A3}.btn-secondary:hover{color:#4D4D4D;background-color:#EB9947;border-color:#FFAB58}.btn-check:focus+.btn-secondary,.btn-secondary:focus{color:#4D4D4D;background-color:#EB9947;border-color:#FFAB58;box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-check:active+.btn-secondary,.btn-check:checked+.btn-secondary,.btn-secondary.active,.btn-secondary:active,.show>.btn-secondary.dropdown-toggle{color:#4D4D4D;background-color:#EB9947;border-color:#FFAB58}.btn-check:active+.btn-secondary:focus,.btn-check:checked+.btn-secondary:focus,.btn-secondary.active:focus,.btn-secondary:active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-success{color:#fff;background-color:#198754;border-color:#198754}.btn-success:hover{color:#fff;background-color:#157347;border-color:#146c43}.btn-check:focus+.btn-success,.btn-success:focus{color:#fff;background-color:#157347;border-color:#146c43;box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-check:active+.btn-success,.btn-check:checked+.btn-success,.btn-success.active,.btn-success:active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#146c43;border-color:#13653f}.btn-check:active+.btn-success:focus,.btn-check:checked+.btn-success:focus,.btn-success.active:focus,.btn-success:active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#198754;border-color:#198754}.btn-info{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-info:hover{color:#000;background-color:#31d2f2;border-color:#25cff2}.btn-check:focus+.btn-info,.btn-info:focus{color:#000;background-color:#31d2f2;border-color:#25cff2;box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-check:active+.btn-info,.btn-check:checked+.btn-info,.btn-info.active,.btn-info:active,.show>.btn-info.dropdown-toggle{color:#000;background-color:#3dd5f3;border-color:#25cff2}.btn-check:active+.btn-info:focus,.btn-check:checked+.btn-info:focus,.btn-info.active:focus,.btn-info:active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-info.disabled,.btn-info:disabled{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-warning{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#000;background-color:#ffca2c;border-color:#ffc720}.btn-check:focus+.btn-warning,.btn-warning:focus{color:#000;background-color:#ffca2c;border-color:#ffc720;box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-check:active+.btn-warning,.btn-check:checked+.btn-warning,.btn-warning.active,.btn-warning:active,.show>.btn-warning.dropdown-toggle{color:#000;background-color:#ffcd39;border-color:#ffc720}.btn-check:active+.btn-warning:focus,.btn-check:checked+.btn-warning:focus,.btn-warning.active:focus,.btn-warning:active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#bb2d3b;border-color:#b02a37}.btn-check:focus+.btn-danger,.btn-danger:focus{color:#fff;background-color:#bb2d3b;border-color:#b02a37;box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-check:active+.btn-danger,.btn-check:checked+.btn-danger,.btn-danger.active,.btn-danger:active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#b02a37;border-color:#a52834}.btn-check:active+.btn-danger:focus,.btn-check:checked+.btn-danger:focus,.btn-danger.active:focus,.btn-danger:active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-light{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:focus+.btn-light,.btn-light:focus{color:#000;background-color:#f9fafb;border-color:#f9fafb;box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-check:active+.btn-light,.btn-check:checked+.btn-light,.btn-light.active,.btn-light:active,.show>.btn-light.dropdown-toggle{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:active+.btn-light:focus,.btn-check:checked+.btn-light:focus,.btn-light.active:focus,.btn-light:active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-light.disabled,.btn-light:disabled{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-dark{color:#fff;background-color:#212529;border-color:#212529}.btn-dark:hover{color:#fff;background-color:#1c1f23;border-color:#1a1e21}.btn-check:focus+.btn-dark,.btn-dark:focus{color:#fff;background-color:#1c1f23;border-color:#1a1e21;box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-check:active+.btn-dark,.btn-check:checked+.btn-dark,.btn-dark.active,.btn-dark:active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1a1e21;border-color:#191c1f}.btn-check:active+.btn-dark:focus,.btn-check:checked+.btn-dark:focus,.btn-dark.active:focus,.btn-dark:active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#212529;border-color:#212529}.btn-outline-primary{color:#338695;border-color:#338695}.btn-outline-primary:hover{color:#338695;background-color:#fff;border-color:#338695}.btn-check:focus+.btn-outline-primary,.btn-outline-primary:focus{box-shadow:inset 0 0 0 -3px rgba(62,160,177,.5)}.btn-check:active+.btn-outline-primary,.btn-check:checked+.btn-outline-primary,.btn-outline-primary.active,.btn-outline-primary.dropdown-toggle.show,.btn-outline-primary:active{color:#fff;background-color:#338695;border-color:#93D5E2; box-shadow:0 0 -8 .25rem rgba(0,0,0,.5)}.btn-check:active+.btn-outline-primary:focus,.btn-check:checked+.btn-outline-primary:focus,.btn-outline-primary.active:focus,.btn-outline-primary.dropdown-toggle.show:focus,.btn-outline-primary:active:focus{box-shadow:0 0 8 .25rem rgba(62,160,177,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#0d6efd;background-color:transparent}.btn-outline-secondary{color:#4D4D4D;border-color:#EB9947}.btn-outline-secondary:hover{color:#4D4D4D;background-color:#fff;border-color:#EB9947}.btn-check:focus+.btn-outline-secondary,.btn-outline-secondary:focus{box-shadow:inset 0 0 0 3px rgba(255,208,163,.5)}.btn-check:active+.btn-outline-secondary,.btn-check:checked+.btn-outline-secondary,.btn-outline-secondary.active,.btn-outline-secondary.dropdown-toggle.show,.btn-outline-secondary:active{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:active+.btn-outline-secondary:focus,.btn-check:checked+.btn-outline-secondary:focus,.btn-outline-secondary.active:focus,.btn-outline-secondary.dropdown-toggle.show:focus,.btn-outline-secondary:active:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-success{color:#198754;border-color:#198754}.btn-outline-success:hover{color:#fff;background-color:#198754;border-color:#198754}.btn-check:focus+.btn-outline-success,.btn-outline-success:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-check:active+.btn-outline-success,.btn-check:checked+.btn-outline-success,.btn-outline-success.active,.btn-outline-success.dropdown-toggle.show,.btn-outline-success:active{color:#fff;background-color:#198754;border-color:#198754}.btn-check:active+.btn-outline-success:focus,.btn-check:checked+.btn-outline-success:focus,.btn-outline-success.active:focus,.btn-outline-success.dropdown-toggle.show:focus,.btn-outline-success:active:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#198754;background-color:transparent}.btn-outline-info{color:#0dcaf0;border-color:#0dcaf0}.btn-outline-info:hover{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:focus+.btn-outline-info,.btn-outline-info:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-check:active+.btn-outline-info,.btn-check:checked+.btn-outline-info,.btn-outline-info.active,.btn-outline-info.dropdown-toggle.show,.btn-outline-info:active{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:active+.btn-outline-info:focus,.btn-check:checked+.btn-outline-info:focus,.btn-outline-info.active:focus,.btn-outline-info.dropdown-toggle.show:focus,.btn-outline-info:active:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#0dcaf0;background-color:transparent}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:focus+.btn-outline-warning,.btn-outline-warning:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-check:active+.btn-outline-warning,.btn-check:checked+.btn-outline-warning,.btn-outline-warning.active,.btn-outline-warning.dropdown-toggle.show,.btn-outline-warning:active{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:active+.btn-outline-warning:focus,.btn-check:checked+.btn-outline-warning:focus,.btn-outline-warning.active:focus,.btn-outline-warning.dropdown-toggle.show:focus,.btn-outline-warning:active:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:focus+.btn-outline-danger,.btn-outline-danger:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-check:active+.btn-outline-danger,.btn-check:checked+.btn-outline-danger,.btn-outline-danger.active,.btn-outline-danger.dropdown-toggle.show,.btn-outline-danger:active{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:active+.btn-outline-danger:focus,.btn-check:checked+.btn-outline-danger:focus,.btn-outline-danger.active:focus,.btn-outline-danger.dropdown-toggle.show:focus,.btn-outline-danger:active:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:focus+.btn-outline-light,.btn-outline-light:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-check:active+.btn-outline-light,.btn-check:checked+.btn-outline-light,.btn-outline-light.active,.btn-outline-light.dropdown-toggle.show,.btn-outline-light:active{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:active+.btn-outline-light:focus,.btn-check:checked+.btn-outline-light:focus,.btn-outline-light.active:focus,.btn-outline-light.dropdown-toggle.show:focus,.btn-outline-light:active:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-dark{color:#212529;border-color:#212529}.btn-outline-dark:hover{color:#fff;background-color:#212529;border-color:#212529}.btn-check:focus+.btn-outline-dark,.btn-outline-dark:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-check:active+.btn-outline-dark,.btn-check:checked+.btn-outline-dark,.btn-outline-dark.active,.btn-outline-dark.dropdown-toggle.show,.btn-outline-dark:active{color:#fff;background-color:#212529;border-color:#212529}.btn-check:active+.btn-outline-dark:focus,.btn-check:checked+.btn-outline-dark:focus,.btn-outline-dark.active:focus,.btn-outline-dark.dropdown-toggle.show:focus,.btn-outline-dark:active:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#212529;background-color:transparent}.btn-link{font-weight:400;color:#0d6efd;text-decoration:underline}.btn-link:hover{color:#0a58ca}.btn-link.disabled,.btn-link:disabled{color:#6c757d}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.dropdown,.dropend,.dropstart,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;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;z-index:1000;display:none;min-width:10rem;padding:.5rem 0;margin: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}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:.125rem}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;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}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid rgba(0,0,0,.15)}.dropdown-item{display:block;width:100%;padding:.25rem 1rem;clear:both;font-weight:400;color:#212529;text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#1e2125;background-color:#e9ecef}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#0d6efd}.dropdown-item.disabled,.dropdown-item:disabled{color:#adb5bd;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1rem;color:#212529}.dropdown-menu-dark{color:#dee2e6;background-color:#343a40;border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item{color:#dee2e6}.dropdown-menu-dark .dropdown-item:focus,.dropdown-menu-dark .dropdown-item:hover{color:#fff;background-color:rgba(255,255,255,.15)}.dropdown-menu-dark .dropdown-item.active,.dropdown-menu-dark .dropdown-item:active{color:#fff;background-color:#0d6efd}.dropdown-menu-dark .dropdown-item.disabled,.dropdown-menu-dark .dropdown-item:disabled{color:#adb5bd}.dropdown-menu-dark .dropdown-divider{border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item-text{color:#dee2e6}.dropdown-menu-dark .dropdown-header{color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.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:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right: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{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.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~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem;color:#0d6efd;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:#0a58ca}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-link{margin-bottom:-1px;background:0 0;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;isolation:isolate}.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{background:0 0;border:0;border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#0d6efd}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding-top:.5rem;padding-bottom:.5rem}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;text-decoration:none;white-space:nowrap}.navbar-nav{display:flex;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}.navbar-text{padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{flex-basis:100%;flex-grow:1;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;transition:box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 .25rem}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.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,.55)}.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 .nav-link.active,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.55);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.55)}.navbar-light .navbar-text a,.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,.55)}.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 .nav-link.active,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.55);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.55)}.navbar-dark .navbar-text a,.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:flex;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;margin-bottom:10px;}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:1rem 1rem}.card-title{margin-bottom:.5rem}.card-subtitle{margin-top:-.25rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1rem}.card-header{padding:.5rem 1rem;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-footer{padding:.5rem 1rem;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:-.5rem;margin-bottom:-.5rem;margin-left:-.5rem;border-bottom:0}.card-header-pills{margin-right:-.5rem;margin-left:-.5rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-group>.card{margin-bottom:.75rem}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:1rem 1.25rem;font-size:1rem;color:#212529;text-align:left;background-color:#fff;border:0;border-radius:0;overflow-anchor:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,border-radius .15s ease}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:#0c63e4;background-color:#e7f1ff;box-shadow:inset 0 -1px 0 rgba(0,0,0,.125)}.accordion-button:not(.collapsed)::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230c63e4'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");transform:rotate(-180deg)}.accordion-button::after{flex-shrink:0;width:1.25rem;height:1.25rem;margin-left:auto;content:"";background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-size:1.25rem;transition:transform .2s ease-in-out}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.accordion-header{margin-bottom:0}.accordion-item{background-color:#fff;border:1px solid rgba(0,0,0,.125)}.accordion-item:first-of-type{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.accordion-item:first-of-type .accordion-button{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-item:last-of-type .accordion-button.collapsed{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.accordion-item:last-of-type .accordion-collapse{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-body{padding:1rem 1.25rem}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}.accordion-flush .accordion-item .accordion-button{border-radius:0}.breadcrumb{display:flex;flex-wrap:wrap;padding:0 0;margin-bottom:1rem;list-style:none}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:.5rem;color:#6c757d;content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:#6c757d}.pagination{display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;color:#0d6efd;text-decoration:none;background-color:#fff;border:1px solid #dee2e6;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:#0a58ca;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;color:#0a58ca;background-color:#e9ecef;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.page-item:not(:first-child) .page-link{margin-left:-1px}.page-item.active .page-link{z-index:3;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;background-color:#fff;border-color:#dee2e6}.page-link{padding:.375rem .75rem}.page-item:first-child .page-link{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}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem}.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}.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:.35em .65em;font-size:.75em;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{position:relative;padding:1rem 1rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{color:#084298;background-color:#cfe2ff;border-color:#b6d4fe}.alert-primary .alert-link{color:#06357a}.alert-secondary{color:#41464b;background-color:#e2e3e5;border-color:#d3d6d8}.alert-secondary .alert-link{color:#34383c}.alert-success{color:#0f5132;background-color:#d1e7dd;border-color:#badbcc}.alert-success .alert-link{color:#0c4128}.alert-info{color:#055160;background-color:#cff4fc;border-color:#b6effb}.alert-info .alert-link{color:#04414d}.alert-warning{color:#664d03;background-color:#fff3cd;border-color:#ffecb5}.alert-warning .alert-link{color:#523e02}.alert-danger{color:#842029;background-color:#f8d7da;border-color:#f5c2c7}.alert-danger .alert-link{color:#6a1a21}.alert-light{color:#636464;background-color:#fefefe;border-color:#fdfdfe}.alert-light .alert-link{color:#4f5050}.alert-dark{color:#141619;background-color:#d3d3d4;border-color:#bcbebf}.alert-dark .alert-link{color:#101214}@-webkit-keyframes progress-bar-stripes{0%{background-position-x:1rem}}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress{display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#0d6efd;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.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:1s linear infinite progress-bar-stripes;animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.list-group{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>li::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;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:.5rem 1rem;color:#212529;text-decoration:none;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#084298;background-color:#cfe2ff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#084298;background-color:#bacbe6}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#084298;border-color:#084298}.list-group-item-secondary{color:#41464b;background-color:#e2e3e5}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#41464b;background-color:#cbccce}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#41464b;border-color:#41464b}.list-group-item-success{color:#0f5132;background-color:#d1e7dd}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#0f5132;background-color:#bcd0c7}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#0f5132;border-color:#0f5132}.list-group-item-info{color:#055160;background-color:#cff4fc}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#055160;background-color:#badce3}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#055160;border-color:#055160}.list-group-item-warning{color:#664d03;background-color:#fff3cd}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#664d03;background-color:#e6dbb9}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#664d03;border-color:#664d03}.list-group-item-danger{color:#842029;background-color:#f8d7da}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#842029;background-color:#dfc2c4}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#842029;border-color:#842029}.list-group-item-light{color:#636464;background-color:#fefefe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#636464;background-color:#e5e5e5}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#636464;border-color:#636464}.list-group-item-dark{color:#141619;background-color:#d3d3d4}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#141619;background-color:#bebebf}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#141619;border-color:#141619}.btn-close{box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:#000;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;border:0;border-radius:.25rem;opacity:.5}.btn-close:hover{color:#000;text-decoration:none;opacity:.75}.btn-close:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25);opacity:1}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:.25}.btn-close-white{filter:invert(1) grayscale(100%) brightness(200%)}.toast{width:350px;max-width:100%;font-size:.875rem;pointer-events:auto;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .5rem 1rem rgba(0,0,0,.15);border-radius:.25rem}.toast:not(.showing):not(.show){opacity:0}.toast.hide{display:none}.toast-container{width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:.75rem}.toast-header{display:flex;align-items:center;padding:.5rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05);border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.toast-header .btn-close{margin-right:-.375rem;margin-left:.75rem}.toast-body{padding:.75rem;word-wrap:break-word}.modal{position:fixed;top:0;left:0;z-index:1060;display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - 1rem)}.modal-content{position:relative;display:flex;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;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:flex;flex-shrink:0;align-items:center;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .btn-close{padding:.5rem .5rem;margin:-.5rem -.5rem -.5rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;flex:1 1 auto;padding:1rem}.modal-footer{display:flex;flex-wrap:wrap;flex-shrink:0;align-items:center;justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{height:calc(100% - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}.modal-fullscreen .modal-footer{border-radius:0}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}.modal-fullscreen-sm-down .modal-footer{border-radius:0}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}.modal-fullscreen-md-down .modal-footer{border-radius:0}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}.modal-fullscreen-lg-down .modal-footer{border-radius:0}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}.modal-fullscreen-xl-down .modal-footer{border-radius:0}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}.modal-fullscreen-xxl-down .modal-footer{border-radius:0}}.tooltip{position:absolute;z-index:1080;display:block;margin:0;font-family:var(--bs-font-sans-serif);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 .tooltip-arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[data-popper-placement^=right],.bs-tooltip-end{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[data-popper-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[data-popper-placement^=left],.bs-tooltip-start{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;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:1070;display:block;max-width:276px;font-family:var(--bs-font-sans-serif);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 .popover-arrow{position:absolute;display:block;width:1rem;height:.5rem}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[data-popper-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 #f0f0f0}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem 1rem;margin-bottom:0;font-size:1rem;background-color:#f0f0f0;border-bottom:1px solid rgba(0,0,0,.2);border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:1rem 1rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.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:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%;list-style:none}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}@-webkit-keyframes spinner-border{to{transform:rotate(360deg)}}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:.75s linear infinite spinner-border;animation:.75s linear infinite spinner-border}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:.75s linear infinite spinner-grow;animation:.75s linear infinite spinner-grow}.spinner-grow-sm{width:1rem;height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{-webkit-animation-duration:1.5s;animation-duration:1.5s}}.offcanvas{position:fixed;bottom:0;z-index:1050;display:flex;flex-direction:column;max-width:100%;visibility:hidden;background-color:#fff;background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas-header{display:flex;align-items:center;justify-content:space-between;padding:1rem 1rem}.offcanvas-header .btn-close{padding:.5rem .5rem;margin-top:-.5rem;margin-right:-.5rem;margin-bottom:-.5rem}.offcanvas-title{margin-bottom:0;line-height:1.5}.offcanvas-body{flex-grow:1;padding:1rem 1rem;overflow-y:auto}.offcanvas-start{top:0;left:0;width:400px;border-right:1px solid rgba(0,0,0,.2);transform:translateX(-100%)}.offcanvas-end{top:0;right:0;width:400px;border-left:1px solid rgba(0,0,0,.2);transform:translateX(100%)}.offcanvas-top{top:0;right:0;left:0;height:30vh;max-height:100%;border-bottom:1px solid rgba(0,0,0,.2);transform:translateY(-100%)}.offcanvas-bottom{right:0;left:0;height:30vh;max-height:100%;border-top:1px solid rgba(0,0,0,.2);transform:translateY(100%)}.offcanvas.show{transform:none}.clearfix::after{display:block;clear:both;content:""}.link-primary{color:#0d6efd}.link-primary:focus,.link-primary:hover{color:#0a58ca}.link-secondary{color:#6c757d}.link-secondary:focus,.link-secondary:hover{color:#565e64}.link-success{color:#198754}.link-success:focus,.link-success:hover{color:#146c43}.link-info{color:#0dcaf0}.link-info:focus,.link-info:hover{color:#3dd5f3}.link-warning{color:#ffc107}.link-warning:focus,.link-warning:hover{color:#ffcd39}.link-danger{color:#dc3545}.link-danger:focus,.link-danger:hover{color:#b02a37}.link-light{color:#f8f9fa}.link-light:focus,.link-light:hover{color:#f9fafb}.link-dark{color:#212529}.link-dark:focus,.link-dark:hover{color:#1a1e21}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:calc(3 / 4 * 100%)}.ratio-16x9{--bs-aspect-ratio:calc(9 / 16 * 100%)}.ratio-21x9{--bs-aspect-ratio:calc(9 / 21 * 100%)}.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}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.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}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow: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}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:1px solid #dee2e6!important}.border-0{border:0!important}.border-top{border-top:1px solid #dee2e6!important}.border-top-0{border-top:0!important}.border-end{border-right:1px solid #dee2e6!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:1px solid #dee2e6!important}.border-start-0{border-left:0!important}.border-primary{border-color:#0d6efd!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#198754!important}.border-info{border-color:#0dcaf0!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#212529!important}.border-white{border-color:#fff!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-light{font-weight:300!important}.fw-lighter{font-weight:lighter!important}.fw-normal{font-weight:400!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{color:#0d6efd!important}.text-secondary{color:#EB9947!important}.text-success{color:#198754!important}.text-info{color:#0dcaf0!important}.text-warning{color:#ffc107!important}.text-danger{color:#dc3545!important}.text-light{color:#f8f9fa!important}.text-dark{color:#212529!important}.text-white{color:#fff!important}.text-body{color:#212529!important}.text-muted{color:#6c757d!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:rgba(255,255,255,.5)!important}.text-reset{color:inherit!important}.bg-primary{background-color:#0d6efd!important}.bg-secondary{background-color:#6c757d!important}.bg-success{background-color:#198754!important}.bg-info{background-color:#0dcaf0!important}.bg-warning{background-color:#ffc107!important}.bg-danger{background-color:#dc3545!important}.bg-light{background-color:#f8f9fa!important}.bg-dark{background-color:#212529!important}.bg-body{background-color:#fff!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:.25rem!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:.2rem!important}.rounded-2{border-radius:.25rem!important}.rounded-3{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-end{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-start{border-bottom-left-radius:.25rem!important;border-top-left-radius:.25rem!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!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:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!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:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!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:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!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:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!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:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/Firmware/RTK_Surveyor/AP-Config/src/bootstrap.min.js b/Firmware/RTK_Surveyor/AP-Config/src/bootstrap.min.js new file mode 100644 index 000000000..c4c0d1f95 --- /dev/null +++ b/Firmware/RTK_Surveyor/AP-Config/src/bootstrap.min.js @@ -0,0 +1,7 @@ +/*! + * Bootstrap v4.3.1 (https://getbootstrap.com/) + * Copyright 2011-2019 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("jquery"),require("popper.js")):"function"==typeof define&&define.amd?define(["exports","jquery","popper.js"],e):e((t=t||self).bootstrap={},t.jQuery,t.Popper)}(this,function(t,g,u){"use strict";function i(t,e){for(var n=0;nthis._items.length-1||t<0))if(this._isSliding)g(this._element).one(Q.SLID,function(){return e.to(t)});else{if(n===t)return this.pause(),void this.cycle();var i=ndocument.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},t._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},t._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=t.left+t.right
',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent",sanitize:!0,sanitizeFn:null,whiteList:Ee},je="show",He="out",Re={HIDE:"hide"+De,HIDDEN:"hidden"+De,SHOW:"show"+De,SHOWN:"shown"+De,INSERTED:"inserted"+De,CLICK:"click"+De,FOCUSIN:"focusin"+De,FOCUSOUT:"focusout"+De,MOUSEENTER:"mouseenter"+De,MOUSELEAVE:"mouseleave"+De},xe="fade",Fe="show",Ue=".tooltip-inner",We=".arrow",qe="hover",Me="focus",Ke="click",Qe="manual",Be=function(){function i(t,e){if("undefined"==typeof u)throw new TypeError("Bootstrap's tooltips require Popper.js (https://popper.js.org/)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var t=i.prototype;return t.enable=function(){this._isEnabled=!0},t.disable=function(){this._isEnabled=!1},t.toggleEnabled=function(){this._isEnabled=!this._isEnabled},t.toggle=function(t){if(this._isEnabled)if(t){var e=this.constructor.DATA_KEY,n=g(t.currentTarget).data(e);n||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(e,n)),n._activeTrigger.click=!n._activeTrigger.click,n._isWithActiveTrigger()?n._enter(null,n):n._leave(null,n)}else{if(g(this.getTipElement()).hasClass(Fe))return void this._leave(null,this);this._enter(null,this)}},t.dispose=function(){clearTimeout(this._timeout),g.removeData(this.element,this.constructor.DATA_KEY),g(this.element).off(this.constructor.EVENT_KEY),g(this.element).closest(".modal").off("hide.bs.modal"),this.tip&&g(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,(this._activeTrigger=null)!==this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},t.show=function(){var e=this;if("none"===g(this.element).css("display"))throw new Error("Please use show on visible elements");var t=g.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){g(this.element).trigger(t);var n=_.findShadowRoot(this.element),i=g.contains(null!==n?n:this.element.ownerDocument.documentElement,this.element);if(t.isDefaultPrevented()||!i)return;var o=this.getTipElement(),r=_.getUID(this.constructor.NAME);o.setAttribute("id",r),this.element.setAttribute("aria-describedby",r),this.setContent(),this.config.animation&&g(o).addClass(xe);var s="function"==typeof this.config.placement?this.config.placement.call(this,o,this.element):this.config.placement,a=this._getAttachment(s);this.addAttachmentClass(a);var l=this._getContainer();g(o).data(this.constructor.DATA_KEY,this),g.contains(this.element.ownerDocument.documentElement,this.tip)||g(o).appendTo(l),g(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new u(this.element,o,{placement:a,modifiers:{offset:this._getOffset(),flip:{behavior:this.config.fallbackPlacement},arrow:{element:We},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){return e._handlePopperPlacementChange(t)}}),g(o).addClass(Fe),"ontouchstart"in document.documentElement&&g(document.body).children().on("mouseover",null,g.noop);var c=function(){e.config.animation&&e._fixTransition();var t=e._hoverState;e._hoverState=null,g(e.element).trigger(e.constructor.Event.SHOWN),t===He&&e._leave(null,e)};if(g(this.tip).hasClass(xe)){var h=_.getTransitionDurationFromElement(this.tip);g(this.tip).one(_.TRANSITION_END,c).emulateTransitionEnd(h)}else c()}},t.hide=function(t){var e=this,n=this.getTipElement(),i=g.Event(this.constructor.Event.HIDE),o=function(){e._hoverState!==je&&n.parentNode&&n.parentNode.removeChild(n),e._cleanTipClass(),e.element.removeAttribute("aria-describedby"),g(e.element).trigger(e.constructor.Event.HIDDEN),null!==e._popper&&e._popper.destroy(),t&&t()};if(g(this.element).trigger(i),!i.isDefaultPrevented()){if(g(n).removeClass(Fe),"ontouchstart"in document.documentElement&&g(document.body).children().off("mouseover",null,g.noop),this._activeTrigger[Ke]=!1,this._activeTrigger[Me]=!1,this._activeTrigger[qe]=!1,g(this.tip).hasClass(xe)){var r=_.getTransitionDurationFromElement(n);g(n).one(_.TRANSITION_END,o).emulateTransitionEnd(r)}else o();this._hoverState=""}},t.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},t.isWithContent=function(){return Boolean(this.getTitle())},t.addAttachmentClass=function(t){g(this.getTipElement()).addClass(Ae+"-"+t)},t.getTipElement=function(){return this.tip=this.tip||g(this.config.template)[0],this.tip},t.setContent=function(){var t=this.getTipElement();this.setElementContent(g(t.querySelectorAll(Ue)),this.getTitle()),g(t).removeClass(xe+" "+Fe)},t.setElementContent=function(t,e){"object"!=typeof e||!e.nodeType&&!e.jquery?this.config.html?(this.config.sanitize&&(e=Se(e,this.config.whiteList,this.config.sanitizeFn)),t.html(e)):t.text(e):this.config.html?g(e).parent().is(t)||t.empty().append(e):t.text(g(e).text())},t.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},t._getOffset=function(){var e=this,t={};return"function"==typeof this.config.offset?t.fn=function(t){return t.offsets=l({},t.offsets,e.config.offset(t.offsets,e.element)||{}),t}:t.offset=this.config.offset,t},t._getContainer=function(){return!1===this.config.container?document.body:_.isElement(this.config.container)?g(this.config.container):g(document).find(this.config.container)},t._getAttachment=function(t){return Pe[t.toUpperCase()]},t._setListeners=function(){var i=this;this.config.trigger.split(" ").forEach(function(t){if("click"===t)g(i.element).on(i.constructor.Event.CLICK,i.config.selector,function(t){return i.toggle(t)});else if(t!==Qe){var e=t===qe?i.constructor.Event.MOUSEENTER:i.constructor.Event.FOCUSIN,n=t===qe?i.constructor.Event.MOUSELEAVE:i.constructor.Event.FOCUSOUT;g(i.element).on(e,i.config.selector,function(t){return i._enter(t)}).on(n,i.config.selector,function(t){return i._leave(t)})}}),g(this.element).closest(".modal").on("hide.bs.modal",function(){i.element&&i.hide()}),this.config.selector?this.config=l({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},t._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},t._enter=function(t,e){var n=this.constructor.DATA_KEY;(e=e||g(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusin"===t.type?Me:qe]=!0),g(e.getTipElement()).hasClass(Fe)||e._hoverState===je?e._hoverState=je:(clearTimeout(e._timeout),e._hoverState=je,e.config.delay&&e.config.delay.show?e._timeout=setTimeout(function(){e._hoverState===je&&e.show()},e.config.delay.show):e.show())},t._leave=function(t,e){var n=this.constructor.DATA_KEY;(e=e||g(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusout"===t.type?Me:qe]=!1),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=He,e.config.delay&&e.config.delay.hide?e._timeout=setTimeout(function(){e._hoverState===He&&e.hide()},e.config.delay.hide):e.hide())},t._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},t._getConfig=function(t){var e=g(this.element).data();return Object.keys(e).forEach(function(t){-1!==Oe.indexOf(t)&&delete e[t]}),"number"==typeof(t=l({},this.constructor.Default,e,"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),_.typeCheckConfig(be,t,this.constructor.DefaultType),t.sanitize&&(t.template=Se(t.template,t.whiteList,t.sanitizeFn)),t},t._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},t._cleanTipClass=function(){var t=g(this.getTipElement()),e=t.attr("class").match(Ne);null!==e&&e.length&&t.removeClass(e.join(""))},t._handlePopperPlacementChange=function(t){var e=t.instance;this.tip=e.popper,this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},t._fixTransition=function(){var t=this.getTipElement(),e=this.config.animation;null===t.getAttribute("x-placement")&&(g(t).removeClass(xe),this.config.animation=!1,this.hide(),this.show(),this.config.animation=e)},i._jQueryInterface=function(n){return this.each(function(){var t=g(this).data(Ie),e="object"==typeof n&&n;if((t||!/dispose|hide/.test(n))&&(t||(t=new i(this,e),g(this).data(Ie,t)),"string"==typeof n)){if("undefined"==typeof t[n])throw new TypeError('No method named "'+n+'"');t[n]()}})},s(i,null,[{key:"VERSION",get:function(){return"4.3.1"}},{key:"Default",get:function(){return Le}},{key:"NAME",get:function(){return be}},{key:"DATA_KEY",get:function(){return Ie}},{key:"Event",get:function(){return Re}},{key:"EVENT_KEY",get:function(){return De}},{key:"DefaultType",get:function(){return ke}}]),i}();g.fn[be]=Be._jQueryInterface,g.fn[be].Constructor=Be,g.fn[be].noConflict=function(){return g.fn[be]=we,Be._jQueryInterface};var Ve="popover",Ye="bs.popover",ze="."+Ye,Xe=g.fn[Ve],$e="bs-popover",Ge=new RegExp("(^|\\s)"+$e+"\\S+","g"),Je=l({},Be.Default,{placement:"right",trigger:"click",content:"",template:''}),Ze=l({},Be.DefaultType,{content:"(string|element|function)"}),tn="fade",en="show",nn=".popover-header",on=".popover-body",rn={HIDE:"hide"+ze,HIDDEN:"hidden"+ze,SHOW:"show"+ze,SHOWN:"shown"+ze,INSERTED:"inserted"+ze,CLICK:"click"+ze,FOCUSIN:"focusin"+ze,FOCUSOUT:"focusout"+ze,MOUSEENTER:"mouseenter"+ze,MOUSELEAVE:"mouseleave"+ze},sn=function(t){var e,n;function i(){return t.apply(this,arguments)||this}n=t,(e=i).prototype=Object.create(n.prototype),(e.prototype.constructor=e).__proto__=n;var o=i.prototype;return o.isWithContent=function(){return this.getTitle()||this._getContent()},o.addAttachmentClass=function(t){g(this.getTipElement()).addClass($e+"-"+t)},o.getTipElement=function(){return this.tip=this.tip||g(this.config.template)[0],this.tip},o.setContent=function(){var t=g(this.getTipElement());this.setElementContent(t.find(nn),this.getTitle());var e=this._getContent();"function"==typeof e&&(e=e.call(this.element)),this.setElementContent(t.find(on),e),t.removeClass(tn+" "+en)},o._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},o._cleanTipClass=function(){var t=g(this.getTipElement()),e=t.attr("class").match(Ge);null!==e&&0=this._offsets[o]&&("undefined"==typeof this._offsets[o+1]||t + + +Generated by IcoMoon + + + + + + + + + + + \ No newline at end of file diff --git a/Firmware/RTK_Surveyor/AP-Config/src/fonts/icomoon.ttf b/Firmware/RTK_Surveyor/AP-Config/src/fonts/icomoon.ttf new file mode 100644 index 000000000..85ae09356 Binary files /dev/null and b/Firmware/RTK_Surveyor/AP-Config/src/fonts/icomoon.ttf differ diff --git a/Firmware/RTK_Surveyor/AP-Config/src/fonts/icomoon.woff b/Firmware/RTK_Surveyor/AP-Config/src/fonts/icomoon.woff new file mode 100644 index 000000000..69c2a5d66 Binary files /dev/null and b/Firmware/RTK_Surveyor/AP-Config/src/fonts/icomoon.woff differ diff --git a/Firmware/RTK_Surveyor/AP-Config/src/jquery-3.6.0.min.js b/Firmware/RTK_Surveyor/AP-Config/src/jquery-3.6.0.min.js new file mode 100644 index 000000000..c4c6022f2 --- /dev/null +++ b/Firmware/RTK_Surveyor/AP-Config/src/jquery-3.6.0.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0= 121) { + select = ge("dynamicModel"); + let newOption = new Option('Mower', '11'); + select.add(newOption, undefined); + newOption = new Option('E-Scooter', '12'); + select.add(newOption, undefined); + } + } + //Strings generated by RTK unit + else if (id.includes("sdFreeSpace") + || id.includes("sdSize") + || id.includes("hardwareID") + || id.includes("zedFirmwareVersion") + || id.includes("daysRemaining") + || id.includes("profile0Name") + || id.includes("profile1Name") + || id.includes("profile2Name") + || id.includes("profile3Name") + || id.includes("profile4Name") + || id.includes("profile5Name") + || id.includes("profile6Name") + || id.includes("profile7Name") + || id.includes("radioMAC") + || id.includes("deviceBTID") + || id.includes("logFileName") + || id.includes("batteryPercent") + ) { + ge(id).innerHTML = val; + } + else if (id.includes("rtkFirmwareVersion")) { + ge("rtkFirmwareVersion").innerHTML = val; + ge("rtkFirmwareVersionUpgrade").innerHTML = val; + } + else if (id.includes("confirmReset")) { + resetComplete(); + } + else if (id.includes("confirmDataReceipt")) { + confirmDataReceipt(); + } + + else if (id.includes("profileNumber")) { + currentProfileNumber = val; + $("input[name=profileRadio][value=" + currentProfileNumber + "]").prop('checked', true); + } + else if (id.includes("firmwareUploadComplete")) { + firmwareUploadComplete(); + } + else if (id.includes("firmwareUploadStatus")) { + firmwareUploadStatus(val); + } + else if (id.includes("geodeticLat")) { + geodeticLat = val; + ge(id).innerHTML = val; + } + else if (id.includes("geodeticLon")) { + geodeticLon = val; + ge(id).innerHTML = val; + } + else if (id.includes("geodeticAlt")) { + geodeticAlt = val; + ge(id).innerHTML = val; + } + else if (id.includes("ecefX")) { + ecefX = val; + ge(id).innerHTML = val; + } + else if (id.includes("ecefY")) { + ecefY = val; + ge(id).innerHTML = val; + } + else if (id.includes("ecefZ")) { + ecefZ = val; + ge(id).innerHTML = val; + } + else if (id.includes("espnowPeerCount")) { + if (val > 0) + ge("peerMACs").innerHTML = ""; + } + else if (id.includes("peerMAC")) { + ge("peerMACs").innerHTML += val + "
"; + } + else if (id.includes("stationECEF")) { + recordsECEF.push(val); + } + else if (id.includes("stationGeodetic")) { + recordsGeodetic.push(val); + } + else if (id.includes("fmName")) { + lastFileName = val; + } + else if (id.includes("fmSize")) { + fileTableText += ""; + fileTableText += "" + lastFileName + ""; + fileTableText += "" + val + ""; + fileTableText += ""; + fileTableText += ""; + } + else if (id.includes("fmNext")) { + sendFile(); + } + else if (id.includes("UBX_")) { + var messageName = id; + var messageRate = val; + var messageNameLabel = ""; + + var messageData = messageName.split('_'); + if (messageData.length >= 3) { + var messageType = messageData[1]; //UBX_RTCM_1074 = RTCM + if (lastMessageType != messageType) { + lastMessageType = messageType; + messageText += "
"; + } + + messageNameLabel = messageData[1] + "_" + messageData[2]; //RTCM_1074 + if (messageData.length == 4) { + messageNameLabel = messageData[1] + "_" + messageData[2] + "_" + messageData[3]; //RTCM_4072_1 + } + + //Remove Base if seen + messageNameLabel = messageNameLabel.split('Base').join(''); //UBX_RTCM_1074Base + } + + messageText += "
"; + messageText += ""; + messageText += "
"; + messageText += "

"; + messageText += "
"; + } + else if (id.includes("checkingNewFirmware")) { + checkingNewFirmware(); + } + else if (id.includes("newFirmwareVersion")) { + newFirmwareVersion(val); + } + else if (id.includes("gettingNewFirmware")) { + gettingNewFirmware(); + } + else if (id.includes("otaFirmwareStatus")) { + otaFirmwareStatus(val); + } + else if (id.includes("batteryIconFileName")) { + ge("batteryIconFileName").src = val; + } + else if (id.includes("coordinateInputType")) { + coordinateInputType = val; + } + else if (id.includes("fixedLat")) { + fixedLat = val; + } + else if (id.includes("fixedLong")) { + fixedLong = val; + } + + //Check boxes / radio buttons + else if (val == "true") { + try { + ge(id).checked = true; + } catch (error) { + console.log("Issue with ID: " + id) + } + } + else if (val == "false") { + try { + ge(id).checked = false; + } catch (error) { + console.log("Issue with ID: " + id) + } + } + + //All regular input boxes and values + else { + try { + ge(id).value = val; + } catch (error) { + console.log("Issue with ID: " + id) + } + } + } + //console.log("Settings loaded"); + + ge("profileChangeMessage").innerHTML = ''; + ge("resetProfileMsg").innerHTML = ''; + + //Don't update if all we received was coordinate info + if (fullPageUpdate) { + fullPageUpdate = false; + + //Force element updates + ge("measurementRateHz").dispatchEvent(new CustomEvent('change')); + ge("baseTypeSurveyIn").dispatchEvent(new CustomEvent('change')); + ge("baseTypeFixed").dispatchEvent(new CustomEvent('change')); + ge("fixedBaseCoordinateTypeECEF").dispatchEvent(new CustomEvent('change')); + ge("fixedBaseCoordinateTypeGeo").dispatchEvent(new CustomEvent('change')); + ge("enableLogging").dispatchEvent(new CustomEvent('change')); + ge("enableNtripClient").dispatchEvent(new CustomEvent('change')); + ge("enableNtripServer").dispatchEvent(new CustomEvent('change')); + ge("dataPortChannel").dispatchEvent(new CustomEvent('change')); + ge("enableExternalPulse").dispatchEvent(new CustomEvent('change')); + ge("enablePointPerfectCorrections").dispatchEvent(new CustomEvent('change')); + ge("radioType").dispatchEvent(new CustomEvent('change')); + ge("antennaReferencePoint").dispatchEvent(new CustomEvent('change')); + ge("autoIMUmountAlignment").dispatchEvent(new CustomEvent('change')); + ge("enableARPLogging").dispatchEvent(new CustomEvent('change')); + + updateECEFList(); + updateGeodeticList(); + tcpClientBoxes(); + tcpServerBoxes(); + udpBoxes(); + dhcpEthernet(); + updateLatLong(); + } + +} + +function hide(id) { + ge(id).style.display = "none"; +} + +function show(id) { + ge(id).style.display = "block"; +} + +//Create CSV of all setting data +function sendData() { + var settingCSV = ""; + + //Input boxes + var clsElements = document.querySelectorAll(".form-control, .form-dropdown"); + for (let x = 0; x < clsElements.length; x++) { + settingCSV += clsElements[x].id + "," + clsElements[x].value + ","; + } + + //Check boxes, radio buttons + //Remove file manager files + clsElements = document.querySelectorAll(".form-check-input:not(.fileManagerCheck), .form-radio"); + + for (let x = 0; x < clsElements.length; x++) { + settingCSV += clsElements[x].id + "," + clsElements[x].checked + ","; + } + + for (let x = 0; x < recordsECEF.length; x++) { + settingCSV += "stationECEF" + x + ',' + recordsECEF[x] + ","; + } + + for (let x = 0; x < recordsGeodetic.length; x++) { + settingCSV += "stationGeodetic" + x + ',' + recordsGeodetic[x] + ","; + } + + console.log("Sending: " + settingCSV); + websocket.send(settingCSV); + + sendDataTimeout = setTimeout(sendData, 2000); +} + +function showError(id, errorText) { + ge(id + 'Error').innerHTML = '
Error: ' + errorText; +} + +function clearError(id) { + ge(id + 'Error').innerHTML = ''; +} + +function showSuccess(id, msg) { + ge(id + 'Success').innerHTML = '
Success: ' + msg; +} +function clearSuccess(id) { + ge(id + 'Success').innerHTML = ''; +} + +function showMsg(id, msg, error = false) { + if (error == true) { + try { + ge(id).classList.remove('inlineSuccess'); + ge(id).classList.add('inlineError'); + } + catch { } + } + else { + try { + ge(id).classList.remove('inlineError'); + ge(id).classList.add('inlineSuccess'); + } + catch { } + } + ge(id).innerHTML = '
' + msg; +} +function showMsgError(id, msg) { + showMsg(id, "Error: " + msg, true); +} +function clearMsg(id, msg) { + ge(id).innerHTML = ''; +} + +var errorCount = 0; + +function checkMessageValue(id) { + checkElementValue(id, 0, 255, "Must be between 0 and 255", "collapseGNSSConfigMsg"); +} + +function collapseSection(section, caret) { + ge(section).classList.remove('show'); + ge(caret).classList.remove('icon-caret-down'); + ge(caret).classList.remove('icon-caret-up'); + ge(caret).classList.add('icon-caret-down'); +} + +function validateFields() { + //Collapse all sections + collapseSection("collapseProfileConfig", "profileCaret"); + collapseSection("collapseGNSSConfig", "gnssCaret"); + collapseSection("collapseGNSSConfigMsg", "gnssMsgCaret"); + collapseSection("collapseBaseConfig", "baseCaret"); + collapseSection("collapseSensorConfig", "sensorCaret"); + collapseSection("collapsePPConfig", "pointPerfectCaret"); + collapseSection("collapsePortsConfig", "portsCaret"); + collapseSection("collapseWiFiConfig", "wifiCaret"); + collapseSection("collapseTCPUDPConfig", "tcpUdpCaret"); + collapseSection("collapseRadioConfig", "radioCaret"); + collapseSection("collapseSystemConfig", "systemCaret"); + collapseSection("collapseEthernetConfig", "ethernetCaret"); + collapseSection("collapseNTPConfig", "ntpCaret"); + + errorCount = 0; + + //Profile Config + checkElementString("profileName", 1, 49, "Must be 1 to 49 characters", "collapseProfileConfig"); + + //GNSS Config + checkElementValue("measurementRateHz", 0.00012, 10, "Must be between 0.00012 and 10Hz", "collapseGNSSConfig"); + checkConstellations(); + + checkElementValue("minElev", 0, 90, "Must be between 0 and 90", "collapseGNSSConfig"); + checkElementValue("minCNO", 0, 90, "Must be between 0 and 90", "collapseGNSSConfig"); + + if (ge("enableNtripClient").checked) { + checkElementString("ntripClient_CasterHost", 1, 45, "Must be 1 to 45 characters", "collapseGNSSConfig"); + checkElementValue("ntripClient_CasterPort", 1, 99999, "Must be 1 to 99999", "collapseGNSSConfig"); + checkElementString("ntripClient_MountPoint", 1, 30, "Must be 1 to 30 characters", "collapseGNSSConfig"); + checkElementCasterUser("ntripClient_CasterHost", "ntripClient_CasterUser", "rtk2go.com", "@", "Must be an email address", "collapseGNSSConfig"); + } + // Don't overwrite with the defaults here. User may want to disable NTRIP but not lose the existing settings. + // else { + // clearElement("ntripClient_CasterHost", "rtk2go.com"); + // clearElement("ntripClient_CasterPort", 2101); + // clearElement("ntripClient_MountPoint", "bldr_SparkFun1"); + // clearElement("ntripClient_MountPointPW"); + // clearElement("ntripClient_CasterUser", "test@test.com"); + // clearElement("ntripClient_CasterUserPW", ""); + // ge("ntripClient_TransmitGGA").checked = true; + // } + + //Check all UBX message boxes + var ubxMessages = document.querySelectorAll('input[id^=UBX_]'); //match all ids starting with UBX_ + for (let x = 0; x < ubxMessages.length; x++) { + var messageName = ubxMessages[x].id; + checkMessageValue(messageName); + } + + //Base Config + if (platformPrefix != "Express Plus") { + if (ge("baseTypeSurveyIn").checked) { + checkElementValue("observationSeconds", 60, 600, "Must be between 60 to 600", "collapseBaseConfig"); + checkElementValue("observationPositionAccuracy", 1, 5.1, "Must be between 1.0 to 5.0", "collapseBaseConfig"); + + clearElement("fixedEcefX", -1280206.568); + clearElement("fixedEcefY", -4716804.403); + clearElement("fixedEcefZ", 4086665.484); + clearElement("fixedLatText", 40.09029479); + clearElement("fixedLongText", -105.18505761); + clearElement("fixedAltitude", 1560.089); + clearElement("antennaHeight", 0); + clearElement("antennaReferencePoint", 0); + } + else { + clearElement("observationSeconds", 60); + clearElement("observationPositionAccuracy", 5.0); + + if (ge("fixedBaseCoordinateTypeECEF").checked) { + clearElement("fixedLatText", 40.09029479); + clearElement("fixedLongText", -105.18505761); + clearElement("fixedAltitude", 1560.089); + clearElement("antennaHeight", 0); + clearElement("antennaReferencePoint", 0); + + checkElementValue("fixedEcefX", -7000000, 7000000, "Must be -7000000 to 7000000", "collapseBaseConfig"); + checkElementValue("fixedEcefY", -7000000, 7000000, "Must be -7000000 to 7000000", "collapseBaseConfig"); + checkElementValue("fixedEcefZ", -7000000, 7000000, "Must be -7000000 to 7000000", "collapseBaseConfig"); + } + else { + clearElement("fixedEcefX", -1280206.568); + clearElement("fixedEcefY", -4716804.403); + clearElement("fixedEcefZ", 4086665.484); + + checkLatLong(); //Verify Lat/Long input type + checkElementValue("fixedAltitude", -11034, 8849, "Must be -11034 to 8849", "collapseBaseConfig"); + + checkElementValue("antennaHeight", -15000, 15000, "Must be -15000 to 15000", "collapseBaseConfig"); + checkElementValue("antennaReferencePoint", -200.0, 200.0, "Must be -200.0 to 200.0", "collapseBaseConfig"); + } + } + + if (ge("enableNtripServer").checked == true) { + checkElementString("ntripServer_CasterHost_0", 1, 49, "Must be 1 to 49 characters", "collapseBaseConfig"); + checkElementValue("ntripServer_CasterPort_0", 1, 99999, "Must be 1 to 99999", "collapseBaseConfig"); + checkElementString("ntripServer_CasterUser_0", 0, 49, "Must be 0 to 49 characters", "collapseBaseConfig"); + checkElementString("ntripServer_CasterUserPW_0", 0, 49, "Must be 0 to 49 characters", "collapseBaseConfig"); + checkElementString("ntripServer_MountPoint_0", 1, 49, "Must be 1 to 49 characters", "collapseBaseConfig"); + checkElementString("ntripServer_MountPointPW_0", 0, 49, "Must be 0 to 49 characters", "collapseBaseConfig"); + checkElementString("ntripServer_CasterHost_1", 0, 49, "Must be 0 to 49 characters", "collapseBaseConfig"); + checkElementValue("ntripServer_CasterPort_1", 0, 99999, "Must be 0 to 99999", "collapseBaseConfig"); + checkElementString("ntripServer_CasterUser_1", 0, 49, "Must be 0 to 49 characters", "collapseBaseConfig"); + checkElementString("ntripServer_CasterUserPW_1", 0, 49, "Must be 0 to 49 characters", "collapseBaseConfig"); + checkElementString("ntripServer_MountPoint_1", 0, 49, "Must be 0 to 49 characters", "collapseBaseConfig"); + checkElementString("ntripServer_MountPointPW_1", 0, 49, "Must be 0 to 49 characters", "collapseBaseConfig"); + } + // Don't overwrite with the defaults here. User may want to disable NTRIP but not lose the existing settings. + // else { + // clearElement("ntripServer_CasterHost_0", "rtk2go.com"); + // clearElement("ntripServer_CasterPort_0", 2101); + // clearElement("ntripServer_CasterUser_0", "test@test.com"); + // clearElement("ntripServer_CasterUserPW_0", ""); + // clearElement("ntripServer_MountPoint_0", "bldr_dwntwn2"); + // clearElement("ntripServer_MountPointPW_0", "WR5wRo4H"); + // clearElement("ntripServer_CasterHost_1", ""); + // clearElement("ntripServer_CasterPort_1", 0); + // clearElement("ntripServer_CasterUser_1", ""); + // clearElement("ntripServer_CasterUserPW_1", ""); + // clearElement("ntripServer_MountPoint_1", ""); + // clearElement("ntripServer_MountPointPW_1", ""); + // } + } + + //PointPerfect Config + if (platformPrefix == "Facet L-Band" || platformPrefix == "Facet L-Band Direct") { + if (ge("enablePointPerfectCorrections").checked == true) { + value = ge("pointPerfectDeviceProfileToken").value; + if (value.length > 0) + checkElementString("pointPerfectDeviceProfileToken", 36, 36, "Must be 36 characters", "collapsePPConfig"); + } + else { + clearElement("pointPerfectDeviceProfileToken", ""); + ge("autoKeyRenewal").checked = true; + } + } + + //Sensor Config + if (platformPrefix == "Express Plus") { + if (ge("autoIMUmountAlignment").checked == false) { + checkElementValue("imuYaw", 0, 360, "Must be between 0.0 to 360.0", "collapseSensorConfig"); + checkElementValue("imuPitch", -90, 90, "Must be between -90.0 to 90.0", "collapseSensorConfig"); + checkElementValue("imuRoll", -180, 180, "Must be between -180.0 to 180.0", "collapseSensorConfig"); + } + } + + //WiFi Config + checkElementString("wifiNetwork0SSID", 0, 50, "Must be 0 to 50 characters", "collapseWiFiConfig"); + checkElementString("wifiNetwork0Password", 0, 50, "Must be 0 to 50 characters", "collapseWiFiConfig"); + checkElementString("wifiNetwork1SSID", 0, 50, "Must be 0 to 50 characters", "collapseWiFiConfig"); + checkElementString("wifiNetwork1Password", 0, 50, "Must be 0 to 50 characters", "collapseWiFiConfig"); + checkElementString("wifiNetwork2SSID", 0, 50, "Must be 0 to 50 characters", "collapseWiFiConfig"); + checkElementString("wifiNetwork2Password", 0, 50, "Must be 0 to 50 characters", "collapseWiFiConfig"); + checkElementString("wifiNetwork3SSID", 0, 50, "Must be 0 to 50 characters", "collapseWiFiConfig"); + checkElementString("wifiNetwork3Password", 0, 50, "Must be 0 to 50 characters", "collapseWiFiConfig"); + + //TCP/UDP Config + if (ge("enablePvtClient").checked) { + checkElementValue("pvtClientPort", 1, 65535, "Must be 1 to 65535", "collapseTCPUDPConfig"); + checkElementString("pvtClientHost", 1, 49, "Must be 1 to 49 characters", "collapseTCPUDPConfig"); + } + if (ge("enablePvtServer").checked) { + checkElementValue("pvtServerPort", 1, 65535, "Must be 1 to 65535", "collapseTCPUDPConfig"); + } + if (ge("enablePvtUdpServer").checked) { + checkElementValue("pvtUdpServerPort", 1, 65535, "Must be 1 to 65535", "collapseTCPUDPConfig"); + } + checkCheckboxMutex("enablePvtClient", "enablePvtServer", "TCP Client and Server can not be enabled at the same time", "collapseTCPUDPConfig"); + + //System Config + if (ge("enableLogging").checked) { + checkElementValue("maxLogTime_minutes", 1, 1051200, "Must be 1 to 1,051,200", "collapseSystemConfig"); + checkElementValue("maxLogLength_minutes", 1, 1051200, "Must be 1 to 1,051,200", "collapseSystemConfig"); + } + else { + clearElement("maxLogTime_minutes", 60 * 24); + clearElement("maxLogLength_minutes", 60 * 24); + } + + if (ge("enableARPLogging").checked) { + checkElementValue("ARPLoggingInterval", 1, 600, "Must be 1 to 600", "collapseSystemConfig"); + } + else { + clearElement("ARPLoggingInterval", 10); + } + + //Ethernet + if (platformPrefix == "Reference Station") { + if (ge("ethernetDHCP").checked == false) { + checkElementIPAddress("ethernetIP", "Must be nnn.nnn.nnn.nnn", "collapseEthernetConfig"); + checkElementIPAddress("ethernetDNS", "Must be nnn.nnn.nnn.nnn", "collapseEthernetConfig"); + checkElementIPAddress("ethernetGateway", "Must be nnn.nnn.nnn.nnn", "collapseEthernetConfig"); + checkElementIPAddress("ethernetSubnet", "Must be nnn.nnn.nnn.nnn", "collapseEthernetConfig"); + } + checkElementValue("ethernetNtpPort", 0, 65535, "Must be 0 to 65535", "collapseEthernetConfig"); + } + + //NTP + if (platformPrefix == "Reference Station") { + checkElementValue("ntpPollExponent", 3, 17, "Must be 3 to 17", "collapseNTPConfig"); + checkElementValue("ntpPrecision", -30, 0, "Must be -30 to 0", "collapseNTPConfig"); + checkElementValue("ntpRootDelay", 0, 10000000, "Must be 0 to 10,000,000", "collapseNTPConfig"); + checkElementValue("ntpRootDispersion", 0, 10000000, "Must be 0 to 10,000,000", "collapseNTPConfig"); + checkElementString("ntpReferenceId", 1, 4, "Must be 1 to 4 chars", "collapseNTPConfig"); + } + + //Port Config + if (platformPrefix != "Surveyor") { + if (ge("enableExternalPulse").checked) { + checkElementValue("externalPulseTimeBetweenPulse_us", 1, 60000000, "Must be 1 to 60,000,000", "collapsePortsConfig"); + checkElementValue("externalPulseLength_us", 1, 60000000, "Must be 1 to 60,000,000", "collapsePortsConfig"); + } + else { + clearElement("externalPulseTimeBetweenPulse_us", 100000); + clearElement("externalPulseLength_us", 1000000); + ge("externalPulsePolarity").value = 0; + } + } +} + +var currentProfileNumber = 0; + +function changeProfile() { + validateFields(); + + if (errorCount == 1) { + showError('saveBtn', "Please clear " + errorCount + " error"); + clearSuccess('saveBtn'); + $("input[name=profileRadio][value=" + currentProfileNumber + "]").prop('checked', true); + } + else if (errorCount > 1) { + showError('saveBtn', "Please clear " + errorCount + " errors"); + clearSuccess('saveBtn'); + $("input[name=profileRadio][value=" + currentProfileNumber + "]").prop('checked', true); + } + else { + ge("profileChangeMessage").innerHTML = 'Loading. Please wait...'; + + currentProfileNumber = document.querySelector('input[name=profileRadio]:checked').value; + + sendData(); + clearError('saveBtn'); + showSuccess('saveBtn', "Saving..."); + + websocket.send("setProfile," + currentProfileNumber + ","); + + ge("collapseProfileConfig").classList.add('show'); + collapseSection("collapseGNSSConfig", "gnssCaret"); + collapseSection("collapseGNSSConfigMsg", "gnssMsgCaret"); + collapseSection("collapseBaseConfig", "baseCaret"); + collapseSection("collapseSensorConfig", "sensorCaret"); + collapseSection("collapsePPConfig", "pointPerfectCaret"); + collapseSection("collapsePortsConfig", "portsCaret"); + collapseSection("collapseWiFiConfig", "wifiCaret"); + collapseSection("collapseTCPUDPConfig", "tcpUdpCaret"); + collapseSection("collapseRadioConfig", "radioCaret"); + collapseSection("collapseSystemConfig", "systemCaret"); + collapseSection("collapseEthernetConfig", "ethernetCaret"); + collapseSection("collapseNTPConfig", "ntpCaret"); + } +} + +function saveConfig() { + validateFields(); + + if (errorCount == 1) { + showError('saveBtn', "Please clear " + errorCount + " error"); + clearSuccess('saveBtn'); + } + else if (errorCount > 1) { + showError('saveBtn', "Please clear " + errorCount + " errors"); + clearSuccess('saveBtn'); + } + else { + sendData(); + clearError('saveBtn'); + showSuccess('saveBtn', "Saving..."); + } + +} + +function checkConstellations() { + if (ge("ubxConstellationsGPS").checked == false + && ge("ubxConstellationsGalileo").checked == false + && ge("ubxConstellationsBeiDou").checked == false + && ge("ubxConstellationsGLONASS").checked == false + ) { + ge("collapseGNSSConfig").classList.add('show'); + showError('ubxConstellations', "Please choose one constellation"); + errorCount++; + } + else + clearError("ubxConstellations"); +} + +function checkBitMapValue(id, min, max, bitMap, errorText, collapseID) { + value = ge(id).value; + mask = ge(bitMap).value; + if ((value < min) || (value > max) || ((mask & (1 << value)) == 0)) { + ge(id + 'Error').innerHTML = 'Error: ' + errorText; + ge(collapseID).classList.add('show'); + errorCount++; + } + else { + clearError(id); + } +} + +//Check if Lat/Long input types are decipherable +function checkLatLong() { + var id = "fixedLatText"; + var collapseID = "collapseBaseConfig"; + ge("detectedFormatText").value = ""; + + var inputTypeLat = identifyInputType(ge(id).value) + if (inputTypeLat == CoordinateTypes.COORDINATE_INPUT_TYPE_INVALID_UNKNOWN) { + var errorText = "Coordinate format unknown"; + ge(id + 'Error').innerHTML = 'Error: ' + errorText; + ge(collapseID).classList.add('show'); + errorCount++; + } + else if (convertedCoordinate < -180 || convertedCoordinate > 180) { + var errorText = "Must be -180 to 180"; + ge(id + 'Error').innerHTML = 'Error: ' + errorText; + ge(collapseID).classList.add('show'); + errorCount++; + } + else + clearError(id); + + id = "fixedLongText"; + var inputTypeLong = identifyInputType(ge(id).value) + if (inputTypeLong == CoordinateTypes.COORDINATE_INPUT_TYPE_INVALID_UNKNOWN) { + var errorText = "Coordinate format unknown"; + ge(id + 'Error').innerHTML = 'Error: ' + errorText; + ge(collapseID).classList.add('show'); + errorCount++; + } + else if (convertedCoordinate < -180 || convertedCoordinate > 180) { + var errorText = "Must be -180 to 180"; + ge(id + 'Error').innerHTML = 'Error: ' + errorText; + ge(collapseID).classList.add('show'); + errorCount++; + } + else + clearError(id); + + if (inputTypeLong != inputTypeLat) { + var errorText = "Formats must match"; + ge(id + 'Error').innerHTML = 'Error: ' + errorText; + ge(collapseID).classList.add('show'); + errorCount++; + ge("detectedFormatText").innerHTML = printableInputType(CoordinateTypes.COORDINATE_INPUT_TYPE_INVALID_UNKNOWN); + } + else + ge("detectedFormatText").innerHTML = printableInputType(inputTypeLat); +} + +//Based on the coordinateInputType, format the lat/long text boxes +function updateLatLong() { + ge("fixedLatText").value = convertInput(fixedLat, coordinateInputType); + ge("fixedLongText").value = convertInput(fixedLong, coordinateInputType); + checkLatLong(); //Updates the detected format +} + +function checkElementValue(id, min, max, errorText, collapseID) { + value = ge(id).value; + if (value < min || value > max || value == "") { + ge(id + 'Error').innerHTML = 'Error: ' + errorText; + ge(collapseID).classList.add('show'); + if (collapseID == "collapseGNSSConfigMsg") ge("collapseGNSSConfig").classList.add('show'); + errorCount++; + } + else + clearError(id); +} + +function checkElementString(id, min, max, errorText, collapseID) { + value = ge(id).value; + if (value.length < min || value.length > max) { + ge(id + 'Error').innerHTML = 'Error: ' + errorText; + ge(collapseID).classList.add('show'); + errorCount++; + } + else + clearError(id); +} + +function checkElementIPAddress(id, errorText, collapseID) { + value = ge(id).value; + var data = value.split('.'); + if ((data.length != 4) + || ((data[0] == "") || (isNaN(Number(data[0]))) || (data[0] < 0) || (data[0] > 255)) + || ((data[1] == "") || (isNaN(Number(data[1]))) || (data[1] < 0) || (data[1] > 255)) + || ((data[2] == "") || (isNaN(Number(data[2]))) || (data[2] < 0) || (data[2] > 255)) + || ((data[3] == "") || (isNaN(Number(data[3]))) || (data[3] < 0) || (data[3] > 255))) { + ge(id + 'Error').innerHTML = 'Error: ' + errorText; + ge(collapseID).classList.add('show'); + errorCount++; + } + else + clearError(id); +} + +function checkElementCasterUser(host, user, url, needs, errorText, collapseID) { + if (ge(host).value.toLowerCase().includes(url)) { + value = ge(user).value; + if ((value.length < 1) || (value.length > 49) || (value.includes(needs) == false)) { + ge(user + 'Error').innerHTML = 'Error: ' + errorText; + ge(collapseID).classList.add('show'); + errorCount++; + } + else + clearError(user); + } + else + clearError(user); +} + +function checkCheckboxMutex(id1, id2, errorText, collapseID) { + if ((ge(id1).checked) && (ge(id2).checked)) { + ge(id1 + 'Error').innerHTML = 'Error: ' + errorText; + ge(id2 + 'Error').innerHTML = 'Error: ' + errorText; + ge(collapseID).classList.add('show'); + errorCount++; + } + else { + clearError(id1); + clearError(id2); + } +} + +function clearElement(id, value) { + ge(id).value = value; + clearError(id); +} + +function resetToFactoryDefaults() { + ge("factoryDefaultsMsg").innerHTML = "Defaults Applied. Please wait for device reset..."; + websocket.send("factoryDefaultReset,1,"); +} + +function zeroElement(id) { + ge(id).value = 0; +} + +function zeroMessages() { + + var ubxMessages = document.querySelectorAll('input[id^=UBX_]'); //match all ids starting with UBX_ + for (let x = 0; x < ubxMessages.length; x++) { + var messageName = ubxMessages[x].id; + zeroElement(messageName); + } +} +function resetToNmeaDefaults() { + zeroMessages(); + ge("UBX_NMEA_GGA").value = 1; + ge("UBX_NMEA_GSA").value = 1; + ge("UBX_NMEA_GST").value = 1; + ge("UBX_NMEA_GSV").value = 4; + ge("UBX_NMEA_RMC").value = 1; +} +function resetToLoggingDefaults() { + zeroMessages(); + ge("UBX_NMEA_GGA").value = 1; + ge("UBX_NMEA_GSA").value = 1; + ge("UBX_NMEA_GST").value = 1; + ge("UBX_NMEA_GSV").value = 4; + ge("UBX_NMEA_RMC").value = 1; + ge("UBX_RXM_RAWX").value = 1; + ge("UBX_RXM_SFRBX").value = 1; +} + +function resetToRTCMDefaults() { + ge("UBX_RTCM_1005Base").value = 1; + ge("UBX_RTCM_1074Base").value = 1; + ge("UBX_RTCM_1077Base").value = 0; + ge("UBX_RTCM_1084Base").value = 1; + ge("UBX_RTCM_1087Base").value = 0; + + ge("UBX_RTCM_1094Base").value = 1; + ge("UBX_RTCM_1097Base").value = 0; + ge("UBX_RTCM_1124Base").value = 1; + ge("UBX_RTCM_1127Base").value = 0; + ge("UBX_RTCM_1230Base").value = 10; + + ge("UBX_RTCM_4072_0Base").value = 0; + ge("UBX_RTCM_4072_1Base").value = 0; +} + +function resetToLowBandwidthRTCM() { + ge("UBX_RTCM_1005Base").value = 10; + ge("UBX_RTCM_1074Base").value = 2; + ge("UBX_RTCM_1077Base").value = 0; + ge("UBX_RTCM_1084Base").value = 2; + ge("UBX_RTCM_1087Base").value = 0; + + ge("UBX_RTCM_1094Base").value = 2; + ge("UBX_RTCM_1097Base").value = 0; + ge("UBX_RTCM_1124Base").value = 2; + ge("UBX_RTCM_1127Base").value = 0; + ge("UBX_RTCM_1230Base").value = 10; + + ge("UBX_RTCM_4072_0Base").value = 0; + ge("UBX_RTCM_4072_1Base").value = 0; +} + +function useECEFCoordinates() { + ge("fixedEcefX").value = ecefX; + ge("fixedEcefY").value = ecefY; + ge("fixedEcefZ").value = ecefZ; +} +function useGeodeticCoordinates() { + ge("fixedLatText").value = geodeticLat; + ge("fixedLongText").value = geodeticLon; + ge("fixedHAE_APC").value = geodeticAlt; + + $("input[name=markRadio][value=1]").prop('checked', true); + $("input[name=markRadio][value=2]").prop('checked', false); + + adjustHAE(); +} + +function startNewLog() { + websocket.send("startNewLog,1,"); +} + +function exitConfig() { + hide("mainPage"); + show("resetInProcess"); + + websocket.send("exitAndReset,1,"); + resetTimeout = setTimeout(exitConfig, 2000); +} + +function resetComplete() { + clearTimeout(resetTimeout); + hide("mainPage"); + hide("resetInProcess"); + show("resetComplete"); +} + +//Called when the ESP32 has confirmed receipt of data over websocket from AP config page +function confirmDataReceipt() { + //Determine which function sent the original data + if (sendDataTimeout != null) { + clearTimeout(sendDataTimeout); + showSuccess('saveBtn', "All Saved!"); + } + else { + console.log("Unknown owner of confirmDataReceipt"); + } +} + +function firmwareUploadWait() { + var file = ge("submitFirmwareFile").files[0]; + var formdata = new FormData(); + formdata.append("submitFirmwareFile", file); + var ajax = new XMLHttpRequest(); + ajax.open("POST", "/upload"); + ajax.send(formdata); + + ge("firmwareUploadMsg").innerHTML = "
Uploading, please wait..."; +} + +function firmwareUploadStatus(val) { + ge("firmwareUploadMsg").innerHTML = val; +} + +function firmwareUploadComplete() { + show("firmwareUploadComplete"); + hide("mainPage"); +} + +function forgetPairedRadios() { + ge("btnForgetRadiosMsg").innerHTML = "All radios forgotten."; + ge("peerMACs").innerHTML = "None"; + websocket.send("forgetEspNowPeers,1,"); +} + +function btnResetProfile() { + ge("resetProfileMsg").innerHTML = "Resetting profile."; + websocket.send("resetProfile,1,"); +} + +document.addEventListener("DOMContentLoaded", (event) => { + + var radios = document.querySelectorAll('input[name=profileRadio]'); + for (var i = 0, max = radios.length; i < max; i++) { + radios[i].onclick = function () { + changeProfile(); + } + } + + ge("measurementRateHz").addEventListener("change", function () { + ge("measurementRateSec").value = 1.0 / ge("measurementRateHz").value; + }); + + ge("measurementRateSec").addEventListener("change", function () { + ge("measurementRateHz").value = 1.0 / ge("measurementRateSec").value; + }); + + ge("baseTypeSurveyIn").addEventListener("change", function () { + if (ge("baseTypeSurveyIn").checked) { + show("surveyInConfig"); + hide("fixedConfig"); + } + }); + + ge("baseTypeFixed").addEventListener("change", function () { + if (ge("baseTypeFixed").checked) { + show("fixedConfig"); + hide("surveyInConfig"); + } + }); + + ge("fixedBaseCoordinateTypeECEF").addEventListener("change", function () { + if (ge("fixedBaseCoordinateTypeECEF").checked) { + show("ecefConfig"); + hide("geodeticConfig"); + } + }); + + ge("fixedBaseCoordinateTypeGeo").addEventListener("change", function () { + if (ge("fixedBaseCoordinateTypeGeo").checked) { + hide("ecefConfig"); + show("geodeticConfig"); + + if (platformPrefix == "Facet") { + ge("antennaReferencePoint").value = 61.4; + } + else if (platformPrefix == "Facet L-Band" || platformPrefix == "Facet L-Band Direct") { + ge("antennaReferencePoint").value = 69.0; + } + else { + ge("antennaReferencePoint").value = 0.0; + } + } + }); + + ge("enableNtripServer").addEventListener("change", function () { + if (ge("enableNtripServer").checked) { + show("ntripServerConfig"); + } + else { + hide("ntripServerConfig"); + } + }); + + ge("enableNtripClient").addEventListener("change", function () { + if (ge("enableNtripClient").checked) { + show("ntripClientConfig"); + } + else { + hide("ntripClientConfig"); + } + }); + + ge("enableFactoryDefaults").addEventListener("change", function () { + if (ge("enableFactoryDefaults").checked) { + ge("factoryDefaults").disabled = false; + } + else { + ge("factoryDefaults").disabled = true; + } + }); + + ge("dataPortChannel").addEventListener("change", function () { + if (ge("dataPortChannel").value == 0) { + show("dataPortBaudDropdown"); + hide("externalPulseConfig"); + } + else if (ge("dataPortChannel").value == 1) { + hide("dataPortBaudDropdown"); + show("externalPulseConfig"); + } + else { + hide("dataPortBaudDropdown"); + hide("externalPulseConfig"); + } + }); + + ge("dynamicModel").addEventListener("change", function () { + if (ge("dynamicModel").value != 4 && ge("enableSensorFusion").checked) { + ge("dynamicModelSensorFusionError").innerHTML = "
Warning: Dynamic Model not set to Automotive. Sensor Fusion is best used with the Automotive Dynamic Model."; + } + else { + ge("dynamicModelSensorFusionError").innerHTML = ""; + } + }); + + ge("enableSensorFusion").addEventListener("change", function () { + if (ge("dynamicModel").value != 4 && ge("enableSensorFusion").checked) { + ge("dynamicModelSensorFusionError").innerHTML = "
Warning: Dynamic Model not set to Automotive. Sensor Fusion is best used with the Automotive Dynamic Model."; + } + else { + ge("dynamicModelSensorFusionError").innerHTML = ""; + } + }); + + ge("enablePointPerfectCorrections").addEventListener("change", function () { + if (ge("enablePointPerfectCorrections").checked) { + show("ppSettingsConfig"); + } + else { + hide("ppSettingsConfig"); + } + }); + + ge("enableExternalPulse").addEventListener("change", function () { + if (ge("enableExternalPulse").checked) { + show("externalPulseConfigDetails"); + } + else { + hide("externalPulseConfigDetails"); + } + }); + + ge("radioType").addEventListener("change", function () { + if (ge("radioType").value == 0) { + hide("radioDetails"); + } + else if (ge("radioType").value == 1) { + show("radioDetails"); + } + }); + + ge("enableForgetRadios").addEventListener("change", function () { + if (ge("enableForgetRadios").checked) { + ge("btnForgetRadios").disabled = false; + } + else { + ge("btnForgetRadios").disabled = true; + } + }); + + ge("enableLogging").addEventListener("change", function () { + if (ge("enableLogging").checked) { + show("enableLoggingDetails"); + } + else { + hide("enableLoggingDetails"); + } + }); + + ge("enableARPLogging").addEventListener("change", function () { + if (ge("enableARPLogging").checked) { + show("enableARPLoggingDetails"); + } + else { + hide("enableARPLoggingDetails"); + } + }); + + ge("fixedAltitude").addEventListener("change", function () { + adjustHAE(); + }); + + ge("antennaHeight").addEventListener("change", function () { + adjustHAE(); + }); + + ge("antennaReferencePoint").addEventListener("change", function () { + adjustHAE(); + }); + + ge("fixedHAE_APC").addEventListener("change", function () { + adjustHAE(); + }); + + ge("autoIMUmountAlignment").addEventListener("change", function () { + if (ge("autoIMUmountAlignment").checked) { + ge("imuYaw").disabled = true; + ge("imuPitch").disabled = true; + ge("imuRoll").disabled = true; + } + else { + ge("imuYaw").disabled = false; + ge("imuPitch").disabled = false; + ge("imuRoll").disabled = false; + } + }); + +}) + +function addECEF() { + errorCount = 0; + + nicknameECEF.value = removeBadChars(nicknameECEF.value); + + checkElementString("nicknameECEF", 1, 49, "Must be 1 to 49 characters", "collapseBaseConfig"); + checkElementValue("fixedEcefX", -7000000, 7000000, "Must be -7000000 to 7000000", "collapseBaseConfig"); + checkElementValue("fixedEcefY", -7000000, 7000000, "Must be -7000000 to 7000000", "collapseBaseConfig"); + checkElementValue("fixedEcefZ", -7000000, 7000000, "Must be -7000000 to 7000000", "collapseBaseConfig"); + + if (errorCount == 0) { + //Check name against the list + var index = 0; + for (; index < recordsECEF.length; ++index) { + var parts = recordsECEF[index].split(' '); + if (ge("nicknameECEF").value == parts[0]) { + recordsECEF[index] = nicknameECEF.value + ' ' + fixedEcefX.value + ' ' + fixedEcefY.value + ' ' + fixedEcefZ.value; + break; + } + } + if (index == recordsECEF.length) + recordsECEF.push(nicknameECEF.value + ' ' + fixedEcefX.value + ' ' + fixedEcefY.value + ' ' + fixedEcefZ.value); + } + + updateECEFList(); +} + +function deleteECEF() { + + var val = ge("StationCoordinatesECEF").value; + if (val > "") { + var parts = recordsECEF[val].split(' '); + var nickName = parts[0]; + + if (confirm("Delete location " + nickName + "?") == true) { + recordsECEF.splice(val, 1); + } + } + updateECEFList(); +} + +function loadECEF() { + var val = ge("StationCoordinatesECEF").value; + if (val > "") { + var parts = recordsECEF[val].split(' '); + ge("fixedEcefX").value = parts[1]; + ge("fixedEcefY").value = parts[2]; + ge("fixedEcefZ").value = parts[3]; + ge("nicknameECEF").value = parts[0]; + clearError("fixedEcefX"); + clearError("fixedEcefY"); + clearError("fixedEcefZ"); + clearError("nicknameECEF"); + } +} + +//Based on recordsECEF array, update and monospace HTML list +function updateECEFList() { + ge("StationCoordinatesECEF").length = 0; + + if (recordsECEF.length == 0) { + hide("StationCoordinatesECEF"); + nicknameECEFText.innerHTML = "No coordinates stored"; + } + else { + show("StationCoordinatesECEF"); + nicknameECEFText.innerHTML = "Nickname: X/Y/Z"; + if (recordsECEF.length < 5) + ge("StationCoordinatesECEF").size = recordsECEF.length; + } + + for (let index = 0; index < recordsECEF.length; ++index) { + var option = document.createElement('option'); + option.text = recordsECEF[index]; + option.value = index; + ge("StationCoordinatesECEF").add(option); + } + + $("#StationCoordinatesECEF option").each(function () { + var parts = $(this).text().split(' '); + var nickname = parts[0].substring(0, 15); + $(this).text(nickname + ': ' + parts[1] + ' ' + parts[2] + ' ' + parts[3]).text; + }); +} + +function addGeodetic() { + errorCount = 0; + + nicknameGeodetic.value = removeBadChars(nicknameGeodetic.value); + + checkElementString("nicknameGeodetic", 1, 49, "Must be 1 to 49 characters", "collapseBaseConfig"); + checkLatLong(); + checkElementValue("fixedAltitude", -11034, 8849, "Must be -11034 to 8849", "collapseBaseConfig"); + checkElementValue("antennaHeight", -15000, 15000, "Must be -15000 to 15000", "collapseBaseConfig"); + checkElementValue("antennaReferencePoint", -200.0, 200.0, "Must be -200.0 to 200.0", "collapseBaseConfig"); + + if (errorCount == 0) { + //Check name against the list + var index = 0; + for (; index < recordsGeodetic.length; ++index) { + var parts = recordsGeodetic[index].split(' '); + if (ge("nicknameGeodetic").value == parts[0]) { + recordsGeodetic[index] = nicknameGeodetic.value + ' ' + fixedLatText.value + ' ' + fixedLongText.value + ' ' + fixedAltitude.value + ' ' + antennaHeight.value + ' ' + antennaReferencePoint.value; + break; + } + } + if (index == recordsGeodetic.length) + recordsGeodetic.push(nicknameGeodetic.value + ' ' + fixedLatText.value + ' ' + fixedLongText.value + ' ' + fixedAltitude.value + ' ' + antennaHeight.value + ' ' + antennaReferencePoint.value); + } + + updateGeodeticList(); +} + +function deleteGeodetic() { + var val = ge("StationCoordinatesGeodetic").value; + if (val > "") { + var parts = recordsGeodetic[val].split(' '); + var nickName = parts[0]; + + if (confirm("Delete location " + nickName + "?") == true) { + recordsGeodetic.splice(val, 1); + } + } + updateGeodeticList(); +} + +function adjustHAE() { + + var haeMethod = document.querySelector('input[name=markRadio]:checked').value; + var hae; + if (haeMethod == 1) { + ge("fixedHAE_APC").disabled = false; + ge("fixedAltitude").disabled = true; + hae = Number(ge("fixedHAE_APC").value) - (Number(ge("antennaHeight").value) / 1000 + Number(ge("antennaReferencePoint").value) / 1000); + ge("fixedAltitude").value = hae.toFixed(3); + } + else { + ge("fixedHAE_APC").disabled = true; + ge("fixedAltitude").disabled = false; + hae = Number(ge("fixedAltitude").value) + (Number(ge("antennaHeight").value) / 1000 + Number(ge("antennaReferencePoint").value) / 1000); + ge("fixedHAE_APC").value = hae.toFixed(3); + } +} + +function loadGeodetic() { + var val = ge("StationCoordinatesGeodetic").value; + if (val > "") { + var parts = recordsGeodetic[val].split(' '); + var numParts = parts.length; + if (numParts >= 6) { + var latParts = (numParts - 4) / 2; + ge("nicknameGeodetic").value = parts[0]; + ge("fixedLatText").value = parts[1]; + if (latParts > 1) { + for (let moreParts = 1; moreParts < latParts; moreParts++) { + ge("fixedLatText").value += ' ' + parts[moreParts + 1]; + } + } + ge("fixedLongText").value = parts[1 + latParts]; + if (latParts > 1) { + for (let moreParts = 1; moreParts < latParts; moreParts++) { + ge("fixedLongText").value += ' ' + parts[moreParts + 1 + latParts]; + } + } + ge("fixedAltitude").value = parts[numParts - 3]; + ge("antennaHeight").value = parts[numParts - 2]; + ge("antennaReferencePoint").value = parts[numParts - 1]; + + $("input[name=markRadio][value=1]").prop('checked', false); + $("input[name=markRadio][value=2]").prop('checked', true); + + adjustHAE(); + + clearError("nicknameGeodetic"); + clearError("fixedLatText"); + clearError("fixedLongText"); + clearError("fixedAltitude"); + clearError("antennaHeight"); + clearError("antennaReferencePoint"); + } + else { + console.log("stationGeodetic split error"); + } + } +} + +//Based on recordsGeodetic array, update and monospace HTML list +function updateGeodeticList() { + ge("StationCoordinatesGeodetic").length = 0; + + if (recordsGeodetic.length == 0) { + hide("StationCoordinatesGeodetic"); + nicknameGeodeticText.innerHTML = "No coordinates stored"; + } + else { + show("StationCoordinatesGeodetic"); + nicknameGeodeticText.innerHTML = "Nickname: Lat/Long/Alt"; + if (recordsGeodetic.length < 5) + ge("StationCoordinatesGeodetic").size = recordsGeodetic.length; + } + + for (let index = 0; index < recordsGeodetic.length; ++index) { + var option = document.createElement('option'); + option.text = recordsGeodetic[index]; + option.value = index; + ge("StationCoordinatesGeodetic").add(option); + } + + $("#StationCoordinatesGeodetic option").each(function () { + var parts = $(this).text().split(' '); + var nickname = parts[0].substring(0, 15); + + if (parts.length >= 7) { + $(this).text(nickname + ': ' + parts[1] + ' ' + parts[2] + ' ' + parts[3] + + ' ' + parts[4] + ' ' + parts[5] + ' ' + parts[6] + + ' ' + parts[7]).text; + } + else { + $(this).text(nickname + ': ' + parts[1] + ' ' + parts[2] + ' ' + parts[3]).text; + } + + }); +} + +function removeBadChars(val) { + val = val.split(' ').join(''); + val = val.split(',').join(''); + val = val.split('\\').join(''); + return (val); +} + +function getFileList() { + if (showingFileList == false) { + showingFileList = true; + + //If the tab was just opened, create table from scratch + ge("fileManagerTable").innerHTML = "
NameSize
"; + fileTableText = ""; + + xmlhttp = new XMLHttpRequest(); + xmlhttp.open("GET", "/listfiles", false); + xmlhttp.send(); + + parseIncoming(xmlhttp.responseText); //Process CSV data into HTML + + ge("fileManagerTable").innerHTML += fileTableText; + } + else { + showingFileList = false; + } +} + +function getMessageList() { + if (obtainedMessageList == false) { + obtainedMessageList = true; + + ge("messageList").innerHTML = ""; + messageText = ""; + + xmlhttp = new XMLHttpRequest(); + xmlhttp.open("GET", "/listMessages", false); + xmlhttp.send(); + + parseIncoming(xmlhttp.responseText); //Process CSV data into HTML + + ge("messageList").innerHTML += messageText; + } +} + +function getMessageListBase() { + if (obtainedMessageListBase == false) { + obtainedMessageListBase = true; + + ge("messageListBase").innerHTML = ""; + messageText = ""; + + xmlhttp = new XMLHttpRequest(); + xmlhttp.open("GET", "/listMessagesBase", false); + xmlhttp.send(); + + parseIncoming(xmlhttp.responseText); //Process CSV data into HTML + + ge("messageListBase").innerHTML += messageText; + } +} + +function fileManagerDownload() { + selectedFiles = document.querySelectorAll('input[name=fileID]:checked'); + numberOfFilesSelected = document.querySelectorAll('input[name=fileID]:checked').length; + fileNumber = 0; + sendFile(); //Start first send +} + +function sendFile() { + if (fileNumber == numberOfFilesSelected) return; + var urltocall = "/file?name=" + selectedFiles[fileNumber].id + "&action=download"; + console.log(urltocall); + window.location.href = urltocall; + + fileNumber++; +} + +function fileManagerToggle() { + var checkboxes = document.querySelectorAll('input[name=fileID]'); + for (var i = 0, n = checkboxes.length; i < n; i++) { + checkboxes[i].checked = ge("fileSelectAll").checked; + } +} + +function fileManagerDelete() { + selectedFiles = document.querySelectorAll('input[name=fileID]:checked'); + + if (confirm("Delete " + selectedFiles.length + " files?") == false) { + return; + } + + for (let x = 0; x < selectedFiles.length; x++) { + var urltocall = "/file?name=" + selectedFiles[x].id + "&action=delete"; + xmlhttp = new XMLHttpRequest(); + + xmlhttp.open("GET", urltocall, false); + xmlhttp.send(); + } + + //Refresh file list + showingFileList = false; + getFileList(); +} + +function uploadFile() { + var file = ge("file1").files[0]; + var formdata = new FormData(); + formdata.append("file1", file); + var ajax = new XMLHttpRequest(); + ajax.upload.addEventListener("progress", progressHandler, false); + ajax.addEventListener("load", completeHandler, false); + ajax.addEventListener("error", errorHandler, false); + ajax.addEventListener("abort", abortHandler, false); + ajax.open("POST", "/"); + ajax.send(formdata); +} +function progressHandler(event) { + var percent = (event.loaded / event.total) * 100; + ge("progressBar").value = Math.round(percent); + ge("uploadStatus").innerHTML = Math.round(percent) + "% uploaded..."; + if (percent >= 100) { + ge("uploadStatus").innerHTML = "Please wait, writing file to filesystem"; + } +} +function completeHandler(event) { + ge("uploadStatus").innerHTML = "Upload Complete"; + ge("progressBar").value = 0; + + //Refresh file list + showingFileList = false; + getFileList(); + + document.getElementById("uploadStatus").innerHTML = "Upload Complete"; +} +function errorHandler(event) { + ge("uploadStatus").innerHTML = "Upload Failed"; +} +function abortHandler(event) { + ge("uploadStatus").innerHTML = "Upload Aborted"; +} + +function tcpClientBoxes() { + if (ge("enablePvtClient").checked) { + show("tcpClientSettingsConfig"); + } + else { + hide("tcpClientSettingsConfig"); + //ge("pvtClientPort").value = 2947; + } +} + +function tcpServerBoxes() { + if (ge("enablePvtServer").checked) { + show("tcpServerSettingsConfig"); + } + else { + hide("tcpServerSettingsConfig"); + //ge("pvtServerPort").value = 2947; + } +} + +function udpBoxes() { + if (ge("enablePvtUdpServer").checked) { + show("udpSettingsConfig"); + } + else { + hide("udpSettingsConfig"); + //ge("pvtUdpServerPort").value = 10110; + } +} + +function dhcpEthernet() { + if (ge("ethernetDHCP").checked) { + hide("fixedIPSettingsConfigEthernet"); + } + else { + show("fixedIPSettingsConfigEthernet"); + } +} + +function networkCount() { + var count = 0; + + var wifiNetworks = document.querySelectorAll('input[id^=wifiNetwork]' && 'input[id$=SSID]'); + for (let x = 0; x < wifiNetworks.length; x++) { + if (wifiNetworks[x].value.length > 0) + count++; + } + + return (count); +} + +function checkNewFirmware() { + if (networkCount() == 0) { + showMsgError('firmwareCheckNewMsg', "WiFi list is empty"); + return; + } + + ge("btnCheckNewFirmware").disabled = true; + showMsg('firmwareCheckNewMsg', "Connecting to WiFi", false); + + var settingCSV = ""; + + //Send current WiFi SSID and PWs + var clsElements = document.querySelectorAll('input[id^=wifiNetwork]'); + for (let x = 0; x < clsElements.length; x++) { + settingCSV += clsElements[x].id + "," + clsElements[x].value + ","; + } + + if (ge("enableRCFirmware").checked == true) + settingCSV += "enableRCFirmware,true,"; + else + settingCSV += "enableRCFirmware,false,"; + + settingCSV += "checkNewFirmware,1,"; + + console.log("firmware sending: " + settingCSV); + websocket.send(settingCSV); + + checkNewFirmwareTimeout = setTimeout(checkNewFirmware, 2000); +} + +function checkingNewFirmware() { + clearTimeout(checkNewFirmwareTimeout); + console.log("Clearing timeout for checkNewFirmwareTimeout"); + + showMsg('firmwareCheckNewMsg', "Checking firmware version"); +} + +function newFirmwareVersion(firmwareVersion) { + clearMsg('firmwareCheckNewMsg'); + if (firmwareVersion == "ERROR") { + showMsgError('firmwareCheckNewMsg', "WiFi or Server not available"); + hide("divGetNewFirmware"); + ge("btnCheckNewFirmware").disabled = false; + return; + } + else if (firmwareVersion == "CURRENT") { + showMsg('firmwareCheckNewMsg', "Firmware is up to date"); + hide("divGetNewFirmware"); + ge("btnCheckNewFirmware").disabled = false; + return; + } + + ge("btnGetNewFirmware").innerHTML = "Update to v" + firmwareVersion; + ge("btnGetNewFirmware").disabled = false; + ge("firmwareUpdateProgressBar").value = 0; + clearMsg('firmwareUpdateProgressMsg'); + show("divGetNewFirmware"); +} + +function getNewFirmware() { + + if (networkCount() == 0) { + showMsgError('firmwareCheckNewMsg', "WiFi list is empty"); + hide("divGetNewFirmware"); + ge("btnCheckNewFirmware").disabled = false; + return; + } + + ge("btnGetNewFirmware").disabled = true; + clearMsg('firmwareCheckNewMsg'); + showMsg('firmwareUpdateProgressMsg', "Getting new firmware"); + + var settingCSV = ""; + + //Send current WiFi SSID and PWs + var clsElements = document.querySelectorAll('input[id^=wifiNetwork]'); + for (let x = 0; x < clsElements.length; x++) { + settingCSV += clsElements[x].id + "," + clsElements[x].value + ","; + } + settingCSV += "getNewFirmware,1,"; + + console.log("firmware sending: " + settingCSV); + websocket.send(settingCSV); + + getNewFirmwareTimeout = setTimeout(getNewFirmware, 2000); +} + +function gettingNewFirmware(val) { + if (val == "1") { + clearTimeout(getNewFirmwareTimeout); + } + else if (val == "ERROR") { + hide("divGetNewFirmware"); + ge("btnCheckNewFirmware").disabled = false; + showMsg('firmwareCheckNewMsg', "Error getting new firmware", true); + } +} + +function otaFirmwareStatus(percentComplete) { + clearTimeout(getNewFirmwareTimeout); + + showMsg('firmwareUpdateProgressMsg', percentComplete + "% Complete"); + ge("firmwareUpdateProgressBar").value = percentComplete; + + if (percentComplete == 100) { + resetComplete(); + } +} + +//Given a user's string, try to identify the type and return the coordinate in DD.ddddddddd format +function identifyInputType(userEntry) { + var coordinateInputType = CoordinateTypes.COORDINATE_INPUT_TYPE_INVALID_UNKNOWN; + var dashCount = 0; + var spaceCount = 0; + var decimalCount = 0; + var lengthOfLeadingNumber = 0; + convertedCoordinate = 0.0; //Clear what is given to us + + //Scan entry for invalid chars + //A valid entry has only numbers, -, ' ', and . + for (var x = 0; x < userEntry.length; x++) { + + if (isdigit(userEntry[x])) { + if (decimalCount == 0) lengthOfLeadingNumber++ + } + else if (userEntry[x] == '-') dashCount++; //All good + else if (userEntry[x] == ' ') spaceCount++; //All good + else if (userEntry[x] == '.') decimalCount++; //All good + else return (CoordinateTypes.COORDINATE_INPUT_TYPE_INVALID_UNKNOWN); //String contains invalid character + } + + // Seven possible entry types + // DD.dddddd + // DDMM.mmmmmmm + // DD MM.mmmmmmm + // DD-MM.mmmmmmm + // DDMMSS.ssssss + // DD MM SS.ssssss + // DD-MM-SS.ssssss + + if (decimalCount > 1) return (CoordinateTypes.COORDINATE_INPUT_TYPE_INVALID_UNKNOWN); //Just no. 40.09033470 is valid. + if (spaceCount > 2) return (CoordinateTypes.COORDINATE_INPUT_TYPE_INVALID_UNKNOWN); //Only 0, 1, or 2 allowed. 40 05 25.2049 is valid. + if (dashCount > 3) return (CoordinateTypes.COORDINATE_INPUT_TYPE_INVALID_UNKNOWN); //Only 0, 1, 2, or 3 allowed. -105-11-05.1629 is valid. + if (lengthOfLeadingNumber > 7) return (CoordinateTypes.COORDINATE_INPUT_TYPE_INVALID_UNKNOWN); //Only 7 or fewer. -1051105.188992 (DDDMMSS or DDMMSS) is valid + + //console.log("userEntry: " + userEntry); + //console.log("decimalCount: " + decimalCount); + //console.log("spaceCount: " + spaceCount); + //console.log("dashCount: " + dashCount); + //console.log("lengthOfLeadingNumber: " + lengthOfLeadingNumber); + + var negativeSign = false; + if (userEntry[0] == '-') { + userEntry = setCharAt(userEntry, 0, ''); //Remove leading minus + negativeSign = true; + dashCount--; //Use dashCount as the internal dashes only, not the leading negative sign + } + + if (spaceCount == 0 && dashCount == 0 && (lengthOfLeadingNumber == 7 || lengthOfLeadingNumber == 6)) //DDMMSS.ssssss + { + coordinateInputType = CoordinateTypes.COORDINATE_INPUT_TYPE_DDMMSS; + + var intPortion = Math.trunc(Number(userEntry)); //Get DDDMMSS + var decimal = Math.trunc(intPortion / 10000); //Get DDD + intPortion -= (decimal * 10000); + var minutes = Math.trunc(intPortion / 100); //Get MM + + //Find '.' + if (userEntry.indexOf('.') == -1) + coordinateInputType = CoordinateTypes.COORDINATE_INPUT_TYPE_DDMMSS_NO_DECIMAL; + + var seconds = userEntry; //Get DDDMMSS.ssssss + seconds -= (decimal * 10000); //Remove DDD + seconds -= (minutes * 100); //Remove MM + convertedCoordinate = decimal + (minutes / 60.0) + (seconds / 3600.0); + if (negativeSign) convertedCoordinate *= -1; + } + else if (spaceCount == 0 && dashCount == 0 && (lengthOfLeadingNumber == 5 || lengthOfLeadingNumber == 4)) //DDMM.mmmmmmm + { + coordinateInputType = CoordinateTypes.COORDINATE_INPUT_TYPE_DDMM; + + var intPortion = Math.trunc(userEntry); //Get DDDMM + var decimal = intPortion / 100; //Get DDD + intPortion -= (decimal * 100); + var minutes = userEntry; //Get DDDMM.mmmmmmm + minutes -= (decimal * 100); //Remove DDD + convertedCoordinate = decimal + (minutes / 60.0); + if (negativeSign) convertedCoordinate *= -1.0; + } + + else if (dashCount == 1) //DD-MM.mmmmmmm + { + coordinateInputType = CoordinateTypes.COORDINATE_INPUT_TYPE_DD_MM_DASH; + + var data = userEntry.split('-'); + var decimal = Number(data[0]); //Get DD + var minutes = Number(data[1]); //Get MM.mmmmmmm + convertedCoordinate = decimal + (minutes / 60.0); + if (negativeSign) convertedCoordinate *= -1.0; + } + else if (dashCount == 2) //DD-MM-SS.ssss + { + coordinateInputType = CoordinateTypes.COORDINATE_INPUT_TYPE_DD_MM_SS_DASH; + + var data = userEntry.split('-'); + var decimal = Number(data[0]); //Get DD + var minutes = Number(data[1]); //Get MM + + //Find '.' + if (userEntry.indexOf('.') == -1) + coordinateInputType = CoordinateTypes.COORDINATE_INPUT_TYPE_DD_MM_SS_DASH_NO_DECIMAL; + + var seconds = Number(data[2]); //Get SS.ssssss + convertedCoordinate = decimal + (minutes / 60.0) + (seconds / 3600.0); + if (negativeSign) convertedCoordinate *= -1.0; + } + else if (spaceCount == 0) //DD.ddddddddd + { + coordinateInputType = CoordinateTypes.COORDINATE_INPUT_TYPE_DD; + convertedCoordinate = userEntry; + if (negativeSign) convertedCoordinate *= -1.0; + } + else if (spaceCount == 1) //DD MM.mmmmmmm + { + coordinateInputType = CoordinateTypes.COORDINATE_INPUT_TYPE_DD_MM; + + var data = userEntry.split(' '); + var decimal = Number(data[0]); //Get DD + var minutes = Number(data[1]); //Get MM.mmmmmmm + convertedCoordinate = decimal + (minutes / 60.0); + if (negativeSign) convertedCoordinate *= -1.0; + } + else if (spaceCount == 2) //DD MM SS.ssssss + { + coordinateInputType = CoordinateTypes.COORDINATE_INPUT_TYPE_DD_MM_SS; + + var data = userEntry.split(' '); + var decimal = Number(data[0]); //Get DD + var minutes = Number(data[1]); //Get MM + + //Find '.' + if (userEntry.indexOf('.') == -1) + coordinateInputType = CoordinateTypes.COORDINATE_INPUT_TYPE_DD_MM_SS_NO_DECIMAL; + + var seconds = Number(data[2]); //Get SS.ssssss + convertedCoordinate = decimal + (minutes / 60.0) + (seconds / 3600.0); + if (negativeSign) convertedCoordinate *= -1.0; + } + + //console.log("convertedCoordinate: " + Number(convertedCoordinate).toFixed(9)); + //console.log("Detected type: " + printableInputType(coordinateInputType)); + return (coordinateInputType); +} + +//Given a coordinate and input type, output a string +//So DD.ddddddddd can become 'DD MM SS.ssssss', etc +function convertInput(coordinate, coordinateInputType) { + var coordinateString = ""; + + if (coordinateInputType == CoordinateTypes.COORDINATE_INPUT_TYPE_DD) { + coordinate = Number(coordinate).toFixed(9); + return (coordinate); + } + else if (coordinateInputType == CoordinateTypes.COORDINATE_INPUT_TYPE_DD_MM + || coordinateInputType == CoordinateTypes.COORDINATE_INPUT_TYPE_DDMM + || coordinateInputType == CoordinateTypes.COORDINATE_INPUT_TYPE_DD_MM_DASH + || coordinateInputType == CoordinateTypes.COORDINATE_INPUT_TYPE_DD_MM_SYMBOL + ) { + var longitudeDegrees = Math.trunc(coordinate); + coordinate -= longitudeDegrees; + coordinate *= 60; + if (coordinate < 1) + coordinate *= -1; + + coordinate = coordinate.toFixed(7); + + if (coordinateInputType == CoordinateTypes.COORDINATE_INPUT_TYPE_DDMM) + coordinateString = longitudeDegrees + "" + coordinate; + else if (coordinateInputType == CoordinateTypes.COORDINATE_INPUT_TYPE_DD_MM_DASH) + coordinateString = longitudeDegrees + "-" + coordinate; + else if (coordinateInputType == CoordinateTypes.COORDINATE_INPUT_TYPE_DD_MM_SYMBOL) + coordinateString = longitudeDegrees + "°" + coordinate + "'"; + else + coordinateString = longitudeDegrees + " " + coordinate; + } + else if (coordinateInputType == CoordinateTypes.COORDINATE_INPUT_TYPE_DD_MM_SS + || coordinateInputType == CoordinateTypes.COORDINATE_INPUT_TYPE_DDMMSS + || coordinateInputType == CoordinateTypes.COORDINATE_INPUT_TYPE_DD_MM_SS_DASH + || coordinateInputType == CoordinateTypes.COORDINATE_INPUT_TYPE_DD_MM_SS_SYMBOL + || coordinateInputType == CoordinateTypes.COORDINATE_INPUT_TYPE_DDMMSS_NO_DECIMAL + || coordinateInputType == CoordinateTypes.COORDINATE_INPUT_TYPE_DD_MM_SS_NO_DECIMAL + || coordinateInputType == CoordinateTypes.COORDINATE_INPUT_TYPE_DD_MM_SS_DASH_NO_DECIMAL + ) { + var longitudeDegrees = Math.trunc(coordinate); + coordinate -= longitudeDegrees; + coordinate *= 60; + if (coordinate < 1) + coordinate *= -1; + + var longitudeMinutes = Math.trunc(coordinate); + coordinate -= longitudeMinutes; + coordinate *= 60; + + coordinate = coordinate.toFixed(6); + + if (coordinateInputType == CoordinateTypes.COORDINATE_INPUT_TYPE_DDMMSS) + coordinateString = longitudeDegrees + "" + longitudeMinutes + "" + coordinate; + else if (coordinateInputType == CoordinateTypes.COORDINATE_INPUT_TYPE_DD_MM_SS_DASH) + coordinateString = longitudeDegrees + "-" + longitudeMinutes + "-" + coordinate; + else if (coordinateInputType == CoordinateTypes.COORDINATE_INPUT_TYPE_DD_MM_SS_SYMBOL) + coordinateString = longitudeDegrees + "°" + longitudeMinutes + "'" + coordinate + "\""; + else if (coordinateInputType == CoordinateTypes.COORDINATE_INPUT_TYPE_DD_MM_SS) + coordinateString = longitudeDegrees + " " + longitudeMinutes + " " + coordinate; + else if (coordinateInputType == CoordinateTypes.COORDINATE_INPUT_TYPE_DDMMSS_NO_DECIMAL) + coordinateString = longitudeDegrees + "" + longitudeMinutes + "" + Math.round(coordinate); + else if (coordinateInputType == CoordinateTypes.COORDINATE_INPUT_TYPE_DD_MM_SS_NO_DECIMAL) + coordinateString = longitudeDegrees + " " + longitudeMinutes + " " + Math.round(coordinate); + else if (coordinateInputType == CoordinateTypes.COORDINATE_INPUT_TYPE_DD_MM_SS_DASH_NO_DECIMAL) + coordinateString = longitudeDegrees + "-" + longitudeMinutes + "-" + Math.round(coordinate); + } + + return (coordinateString); +} + +function isdigit(c) { return /\d/.test(c); } + +function setCharAt(str, index, chr) { + if (index > str.length - 1) return str; + return str.substring(0, index) + chr + str.substring(index + 1); +} + +//Given an input type, return a printable string +function printableInputType(coordinateInputType) { + switch (coordinateInputType) { + default: + return ("Unknown"); + break; + case (CoordinateTypes.COORDINATE_INPUT_TYPE_DD): + return ("DD.ddddddddd"); + break; + case (CoordinateTypes.COORDINATE_INPUT_TYPE_DDMM): + return ("DDMM.mmmmmmm"); + break; + case (CoordinateTypes.COORDINATE_INPUT_TYPE_DD_MM): + return ("DD MM.mmmmmmm"); + break; + case (CoordinateTypes.COORDINATE_INPUT_TYPE_DD_MM_DASH): + return ("DD-MM.mmmmmmm"); + break; + case (CoordinateTypes.COORDINATE_INPUT_TYPE_DDMMSS): + return ("DDMMSS.ssssss"); + break; + case (CoordinateTypes.COORDINATE_INPUT_TYPE_DD_MM_SS): + return ("DD MM SS.ssssss"); + break; + case (CoordinateTypes.COORDINATE_INPUT_TYPE_DD_MM_SS_DASH): + return ("DD-MM-SS.ssssss"); + break; + case (CoordinateTypes.COORDINATE_INPUT_TYPE_DDMMSS_NO_DECIMAL): + return ("DDMMSS"); + break; + case (CoordinateTypes.COORDINATE_INPUT_TYPE_DD_MM_SS_NO_DECIMAL): + return ("DD MM SS"); + break; + case (CoordinateTypes.COORDINATE_INPUT_TYPE_DD_MM_SS_DASH_NO_DECIMAL): + return ("DD-MM-SS"); + break; + } + return ("Unknown"); +} diff --git a/Firmware/RTK_Surveyor/AP-Config/src/rtk-setup-wifi.png b/Firmware/RTK_Surveyor/AP-Config/src/rtk-setup-wifi.png new file mode 100644 index 000000000..e6e4ec8a7 Binary files /dev/null and b/Firmware/RTK_Surveyor/AP-Config/src/rtk-setup-wifi.png differ diff --git a/Firmware/RTK_Surveyor/AP-Config/src/rtk-setup.png b/Firmware/RTK_Surveyor/AP-Config/src/rtk-setup.png new file mode 100644 index 000000000..cd2b1d8f0 Binary files /dev/null and b/Firmware/RTK_Surveyor/AP-Config/src/rtk-setup.png differ diff --git a/Firmware/RTK_Surveyor/AP-Config/src/style.css b/Firmware/RTK_Surveyor/AP-Config/src/style.css new file mode 100644 index 000000000..e2d9198b2 --- /dev/null +++ b/Firmware/RTK_Surveyor/AP-Config/src/style.css @@ -0,0 +1,63 @@ +@font-face { + font-family: 'icomoon'; + src: url('fonts/icomoon.eot?81wxq3'); + src: url('fonts/icomoon.eot?81wxq3#iefix') format('embedded-opentype'), + url('fonts/icomoon.ttf?81wxq3') format('truetype'), + url('fonts/icomoon.woff?81wxq3') format('woff'), + url('fonts/icomoon.svg?81wxq3#icomoon') format('svg'); + font-weight: normal; + font-style: normal; + font-display: block; +} + +[class^="icon-"], [class*=" icon-"] { + /* use !important to prevent issues with browser extensions that change fonts */ + font-family: 'icomoon' !important; + speak: never; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +input[type="file"] { + display: none; +} + +.icon-close:before { + content: "\f00d"; +} +.icon-remove:before { + content: "\f00d"; +} +.icon-times:before { + content: "\f00d"; +} +.icon-info-circle:before { + content: "\f05a"; +} +.icon-floppy-o:before { + content: "\f0c7"; +} +.icon-save:before { + content: "\f0c7"; +} +.icon-caret-down:before { + content: "\f0d7"; +} +.icon-caret-up:before { + content: "\f0d8"; +} + +.left { + width: 85%; +} + +.right { + width: 7%; +} \ No newline at end of file diff --git a/Firmware/RTK_Surveyor/Base.ino b/Firmware/RTK_Surveyor/Base.ino index 165520fe1..71ef8625a 100644 --- a/Firmware/RTK_Surveyor/Base.ino +++ b/Firmware/RTK_Surveyor/Base.ino @@ -1,230 +1,417 @@ - -//Configure specific aspects of the receiver for base mode +// Configure specific aspects of the receiver for base mode bool configureUbloxModuleBase() { - bool response = true; - int maxWait = 2000; + if (online.gnss == false) + return (false); + + // If our settings haven't changed, and this is first config since power on, trust ZED's settings + if (settings.updateZEDSettings == false && firstPowerOn == true) + { + firstPowerOn = false; // Next time user switches modes, new settings will be applied + log_d("Skipping ZED Base configuration"); + return (true); + } - if (productVariant == RTK_SURVEYOR) - { - digitalWrite(pin_positionAccuracyLED_1cm, LOW); - digitalWrite(pin_positionAccuracyLED_10cm, LOW); - digitalWrite(pin_positionAccuracyLED_100cm, LOW); - } + firstPowerOn = false; // If we switch between rover/base in the future, force config of module. - i2cGNSS.checkUblox(); //Regularly poll to get latest data and any RTCM + theGNSS.checkUblox(); // Regularly poll to get latest data and any RTCM + theGNSS.checkCallbacks(); // Process any callbacks: ie, storePVTdata - if (i2cGNSS.getSurveyInActive() == true) - { - response = i2cGNSS.disableSurveyMode(maxWait); //Disable survey - if (response == false) - Serial.println(F("Disable Survey failed")); - } - - //In base mode we force 1Hz - if (i2cGNSS.getNavigationFrequency(maxWait) != 1) - response &= i2cGNSS.setNavigationFrequency(1, maxWait); - if (response == false) - { - Serial.println(F("configureUbloxModuleBase: Set rate failed")); - return (false); - } - - // Set dynamic model - if (i2cGNSS.getDynamicModel(maxWait) != DYN_MODEL_STATIONARY) - { - response &= i2cGNSS.setDynamicModel(DYN_MODEL_STATIONARY, maxWait); - if (response == false) + theGNSS.setNMEAGPGGAcallbackPtr( + nullptr); // Disable GPGGA call back that may have been set during Rover NTRIP Client mode + + bool success = false; + int tryNo = -1; + + // Try up to MAX_SET_MESSAGES_RETRIES times to configure the GNSS + // This corrects occasional failures seen on the Reference Station where the GNSS is connected via SPI + // instead of I2C and UART1. I believe the SETVAL ACK is occasionally missed due to the level of messages being + // processed. + while ((++tryNo < MAX_SET_MESSAGES_RETRIES) && !success) { - Serial.println(F("setDynamicModel failed")); - return (false); + bool response = true; + + // In Base mode we force 1Hz + response &= theGNSS.newCfgValset(); + response &= theGNSS.addCfgValset(UBLOX_CFG_RATE_MEAS, 1000); + response &= theGNSS.addCfgValset(UBLOX_CFG_RATE_NAV, 1); + + // Since we are at 1Hz, allow GSV NMEA to be reported at whatever the user has chosen + uint32_t spiOffset = + 0; // Set to 3 if using SPI to convert UART1 keys to SPI. This is brittle and non-perfect, but works. + if (USE_SPI_GNSS) + spiOffset = 3; + response &= theGNSS.addCfgValset(ubxMessages[8].msgConfigKey + spiOffset, + settings.ubxMessageRates[8]); // Update rate on module + + if (USE_I2C_GNSS) + response &= + theGNSS.addCfgValset(UBLOX_CFG_MSGOUT_NMEA_ID_GGA_I2C, + 0); // Disable NMEA message that may have been set during Rover NTRIP Client mode + + // Survey mode is only available on ZED-F9P modules + if (commandSupported(UBLOX_CFG_TMODE_MODE) == true) + response &= theGNSS.addCfgValset(UBLOX_CFG_TMODE_MODE, 0); // Disable survey-in mode + + // Note that using UBX-CFG-TMODE3 to set the receiver mode to Survey In or to Fixed Mode, will set + // automatically the dynamic platform model (CFG-NAVSPG-DYNMODEL) to Stationary. + // response &= theGNSS.addCfgValset(UBLOX_CFG_NAVSPG_DYNMODEL, (dynModel)settings.dynamicModel); //Not needed + + // RTCM is only available on ZED-F9P modules + // + // For most RTK products, the GNSS is interfaced via both I2C and UART1. Configuration and PVT/HPPOS messages + // are configured over I2C. Any messages that need to be logged are output on UART1, and received by this code + // using serialGNSS. In base mode the RTK device should output RTCM over all ports: (Primary) UART2 in case the + //Surveyor is connected via radio to rover (Optional) I2C in case user wants base to connect to WiFi and NTRIP + //Caster (Seconday) USB in case the Surveyor is used as an NTRIP caster connected to SBC or other (Tertiary) + //UART1 in case Surveyor is sending RTCM to phone that is then NTRIP Caster + // + // But, on the Reference Station, the GNSS is interfaced via SPI. It has no access to I2C and UART1. + // We use the GNSS library's built-in logging buffer to mimic UART1. The code in Tasks.ino reads + // data from the logging buffer as if it had come from UART1. + // So for that product - in Base mode - we can only output RTCM on SPI, USB and UART2. + // If we want to log the RTCM messages, we need to add them to the logging buffer inside the GNSS library. + // If we want to pass them along to (e.g.) radio, we do that using processRTCM (defined below). + + // Find first RTCM record in ubxMessage array + int firstRTCMRecord = getMessageNumberByName("UBX_RTCM_1005"); + + // ubxMessageRatesBase is an array of ~12 uint8_ts + // ubxMessage is an array of ~80 messages + // We use firstRTCMRecord as an offset for the keys, but use x as the rate + + if (USE_I2C_GNSS) + { + for (int x = 0; x < MAX_UBX_MSG_RTCM; x++) + { + response &= theGNSS.addCfgValset(ubxMessages[firstRTCMRecord + x].msgConfigKey - 1, + settings.ubxMessageRatesBase[x]); // UBLOX_CFG UART1 - 1 = I2C + response &= theGNSS.addCfgValset(ubxMessages[firstRTCMRecord + x].msgConfigKey, + settings.ubxMessageRatesBase[x]); // UBLOX_CFG UART1 + + // Disable messages on SPI + response &= theGNSS.addCfgValset(ubxMessages[firstRTCMRecord + x].msgConfigKey + 3, + 0); // UBLOX_CFG UART1 + 3 = SPI + } + } + else // SPI GNSS + { + for (int x = 0; x < MAX_UBX_MSG_RTCM; x++) + { + response &= theGNSS.addCfgValset(ubxMessages[firstRTCMRecord + x].msgConfigKey + 3, + settings.ubxMessageRatesBase[x]); // UBLOX_CFG UART1 + 3 = SPI + + // Disable messages on I2C and UART1 + response &= theGNSS.addCfgValset(ubxMessages[firstRTCMRecord + x].msgConfigKey - 1, + 0); // UBLOX_CFG UART1 - 1 = I2C + response &= theGNSS.addCfgValset(ubxMessages[firstRTCMRecord + x].msgConfigKey, 0); // UBLOX_CFG UART1 + } + + // Enable logging of these messages so the RTCM will be stored automatically in the logging buffer. + // This mimics the data arriving via UART1. + uint32_t logRTCMMessages = theGNSS.getRTCMLoggingMask(); + logRTCMMessages |= + (SFE_UBLOX_FILTER_RTCM_TYPE1005 | SFE_UBLOX_FILTER_RTCM_TYPE1074 | SFE_UBLOX_FILTER_RTCM_TYPE1084 | + SFE_UBLOX_FILTER_RTCM_TYPE1094 | SFE_UBLOX_FILTER_RTCM_TYPE1124 | SFE_UBLOX_FILTER_RTCM_TYPE1230); + theGNSS.setRTCMLoggingMask(logRTCMMessages); + } + + // Update message rates for UART2 and USB + for (int x = 0; x < MAX_UBX_MSG_RTCM; x++) + { + response &= theGNSS.addCfgValset(ubxMessages[firstRTCMRecord + x].msgConfigKey + 1, + settings.ubxMessageRatesBase[x]); // UBLOX_CFG UART1 + 1 = UART2 + response &= theGNSS.addCfgValset(ubxMessages[firstRTCMRecord + x].msgConfigKey + 2, + settings.ubxMessageRatesBase[x]); // UBLOX_CFG UART1 + 2 = USB + } + + response &= theGNSS.addCfgValset(UBLOX_CFG_NAVSPG_INFIL_MINELEV, settings.minElev); // Set minimum elevation + + response &= theGNSS.sendCfgValset(); // Closing value + + if (response) + success = true; } - } - - //In base mode the Surveyor should output RTCM over UART2 and I2C ports: - //(Primary) UART2 in case the Surveyor is connected via radio to rover - //(Optional) I2C in case user wants base to connect to WiFi and NTRIP Serve to Caster - //(Seconday) USB in case the Surveyor is used as an NTRIP caster - //(Tertiary) UART1 in case Surveyor is sending RTCM to phone that is then NTRIP caster - response &= enableRTCMSentences(COM_PORT_UART2); - response &= enableRTCMSentences(COM_PORT_UART1); - response &= enableRTCMSentences(COM_PORT_USB); - response &= enableRTCMSentences(COM_PORT_I2C); //Enable for plain radio so we can count RTCM packets for display (State: Base-Temp - Transmitting) - - if (response == false) - { - Serial.println(F("RTCM settings failed to enable")); - return (false); - } - - return (response); + + if (!success) + systemPrintln("Base config fail"); + + return (success); } -//Start survey -//The ZED-F9P is slightly different than the NEO-M8P. See the Integration manual 3.5.8 for more info. -bool beginSurveyIn() +// Start survey +// The ZED-F9P is slightly different than the NEO-M8P. See the Integration manual 3.5.8 for more info. +bool surveyInStart() { - bool needSurveyReset = false; - if (i2cGNSS.getSurveyInActive(100) == true) needSurveyReset = true; - if (i2cGNSS.getSurveyInValid(100) == true) needSurveyReset = true; + theGNSS.setVal8(UBLOX_CFG_TMODE_MODE, 0); // Disable survey-in mode + delay(100); - if (needSurveyReset == true) - { - Serial.println("Resetting survey"); + bool needSurveyReset = false; + if (theGNSS.getSurveyInActive(100) == true) + needSurveyReset = true; + if (theGNSS.getSurveyInValid(100) == true) + needSurveyReset = true; - if (resetSurvey() == false) + if (needSurveyReset == true) { - Serial.println(F("Survey reset failed")); - if (resetSurvey() == false) - { - Serial.println(F("Survey reset failed - 2nd attempt")); - } + systemPrintln("Resetting survey"); + + if (surveyInReset() == false) + { + systemPrintln("Survey reset failed - attempt 1/3"); + if (surveyInReset() == false) + { + systemPrintln("Survey reset failed - attempt 2/3"); + if (surveyInReset() == false) + { + systemPrintln("Survey reset failed - attempt 3/3"); + } + } + } + } + + bool response = true; + response &= theGNSS.setVal8(UBLOX_CFG_TMODE_MODE, 1); // Survey-in enable + response &= theGNSS.setVal32(UBLOX_CFG_TMODE_SVIN_ACC_LIMIT, settings.observationPositionAccuracy * 10000); + response &= theGNSS.setVal32(UBLOX_CFG_TMODE_SVIN_MIN_DUR, settings.observationSeconds); + + if (response == false) + { + systemPrintln("Survey start failed"); + return (false); + } + + systemPrintf("Survey started. This will run until %d seconds have passed and less than %0.03f meter accuracy is " + "achieved.\r\n", + settings.observationSeconds, settings.observationPositionAccuracy); + + // Wait until active becomes true + long maxTime = 5000; + long startTime = millis(); + while (theGNSS.getSurveyInActive(100) == false) + { + delay(100); + if (millis() - startTime > maxTime) + return (false); // Reset of survey failed } - } - - bool response = i2cGNSS.enableSurveyMode(settings.observationSeconds, settings.observationPositionAccuracy, 5000); //Enable Survey in, with user parameters. Wait up to 5s. - if (response == false) - { - Serial.println(F("Survey start failed")); - return (false); - } - - Serial.printf("Survey started. This will run until %d seconds have passed and less than %0.03f meter accuracy is achieved.\n\r", - settings.observationSeconds, - settings.observationPositionAccuracy - ); - - //Wait until active becomes true - long maxTime = 5000; - long startTime = millis(); - while(i2cGNSS.getSurveyInActive(100) == false) - { - delay(100); - if(millis() - startTime > maxTime) return(false); //Reset of survey failed - } - return (true); + return (true); } -bool resetSurvey() +// Slightly modified method for restarting survey-in from: +// https://portal.u-blox.com/s/question/0D52p00009IsVoMCAV/restarting-surveyin-on-an-f9p +bool surveyInReset() { - int maxWait = 2000; - - //Slightly modified method for restarting survey-in from: https://portal.u-blox.com/s/question/0D52p00009IsVoMCAV/restarting-surveyin-on-an-f9p - bool response = i2cGNSS.disableSurveyMode(maxWait); //Disable survey - delay(1000); - response &= i2cGNSS.enableSurveyMode(1000, 400.000, maxWait); //Enable Survey in with bogus values - delay(1000); - response &= i2cGNSS.disableSurveyMode(maxWait); //Disable survey - - if(response == false) - return(response); - - //Wait until active and valid becomes false - long maxTime = 5000; - long startTime = millis(); - while(i2cGNSS.getSurveyInActive(100) == true || i2cGNSS.getSurveyInValid(100) == true) - { - delay(100); - if(millis() - startTime > maxTime) return(false); //Reset of survey failed - } + bool response = true; + + // Disable survey-in mode + response &= theGNSS.setVal8(UBLOX_CFG_TMODE_MODE, 0); + delay(1000); + + // Enable Survey in with bogus values + response &= theGNSS.newCfgValset(); + response &= theGNSS.addCfgValset(UBLOX_CFG_TMODE_MODE, 1); // Survey-in enable + response &= theGNSS.addCfgValset(UBLOX_CFG_TMODE_SVIN_ACC_LIMIT, 40 * 10000); // 40.0m + response &= theGNSS.addCfgValset(UBLOX_CFG_TMODE_SVIN_MIN_DUR, 1000); // 1000s + response &= theGNSS.sendCfgValset(); + delay(1000); - return(true); + // Disable survey-in mode + response &= theGNSS.setVal8(UBLOX_CFG_TMODE_MODE, 0); + + if (response == false) + return (response); + + // Wait until active and valid becomes false + long maxTime = 5000; + long startTime = millis(); + while (theGNSS.getSurveyInActive(100) == true || theGNSS.getSurveyInValid(100) == true) + { + delay(100); + if (millis() - startTime > maxTime) + return (false); // Reset of survey failed + } + + return (true); } -//Start the base using fixed coordinates +// Start the base using fixed coordinates bool startFixedBase() { - bool response = false; - int maxWait = 2000; - - if (settings.fixedBaseCoordinateType == COORD_TYPE_ECEF) - { - //Break ECEF into main and high precision parts - //The type casting should not effect rounding of original double cast coordinate - long majorEcefX = floor((settings.fixedEcefX * 100.0) + 0.5); - long minorEcefX = floor((((settings.fixedEcefX * 100.0) - majorEcefX) * 100.0) + 0.5); - long majorEcefY = floor((settings.fixedEcefY * 100) + 0.5); - long minorEcefY = floor((((settings.fixedEcefY * 100.0) - majorEcefY) * 100.0) + 0.5); - long majorEcefZ = floor((settings.fixedEcefZ * 100) + 0.5); - long minorEcefZ = floor((((settings.fixedEcefZ * 100.0) - majorEcefZ) * 100.0) + 0.5); - - // Serial.printf("fixedEcefY (should be -4716808.5807): %0.04f\n\r", settings.fixedEcefY); - // Serial.printf("major (should be -471680858): %ld\n\r", majorEcefY); - // Serial.printf("minor (should be -7): %ld\n\r", minorEcefY); - - //Units are cm with a high precision extension so -1234.5678 should be called: (-123456, -78) - //-1280208.308,-4716803.847,4086665.811 is SparkFun HQ so... - response = i2cGNSS.setStaticPosition(majorEcefX, minorEcefX, - majorEcefY, minorEcefY, - majorEcefZ, minorEcefZ, - false, - maxWait - ); //With high precision 0.1mm parts - } - else if (settings.fixedBaseCoordinateType == COORD_TYPE_GEOGRAPHIC) - { - //Break coordinates into main and high precision parts - //The type casting should not effect rounding of original double cast coordinate - int64_t majorLat = settings.fixedLat * 10000000; - int64_t minorLat = ((settings.fixedLat * 10000000) - majorLat) * 100; - int64_t majorLong = settings.fixedLong * 10000000; - int64_t minorLong = ((settings.fixedLong * 10000000) - majorLong) * 100; - int32_t majorAlt = settings.fixedAltitude * 100; - int32_t minorAlt = ((settings.fixedAltitude * 100) - majorAlt) * 100; - - // Serial.printf("fixedLong (should be -105.184774720): %0.09f\n\r", settings.fixedLong); - // Serial.printf("major (should be -1051847747): %lld\n\r", majorLat); - // Serial.printf("minor (should be -20): %lld\n\r", minorLat); - // - // Serial.printf("fixedLat (should be 40.090335429): %0.09f\n\r", settings.fixedLat); - // Serial.printf("major (should be 400903354): %lld\n\r", majorLong); - // Serial.printf("minor (should be 29): %lld\n\r", minorLong); - // - // Serial.printf("fixedAlt (should be 1560.2284): %0.04f\n\r", settings.fixedAltitude); - // Serial.printf("major (should be 156022): %ld\n\r", majorAlt); - // Serial.printf("minor (should be 84): %ld\n\r", minorAlt); - - response = i2cGNSS.setStaticPosition( - majorLat, minorLat, - majorLong, minorLong, - majorAlt, minorAlt, - true, //Use lat/long as input - maxWait); - } - - return (response); + bool response = true; + int retries = 0; + uint16_t maxWait = 1100; + + do { + if (settings.fixedBaseCoordinateType == COORD_TYPE_ECEF) + { + // Break ECEF into main and high precision parts + // The type casting should not effect rounding of original double cast coordinate + long majorEcefX = floor((settings.fixedEcefX * 100.0) + 0.5); + long minorEcefX = floor((((settings.fixedEcefX * 100.0) - majorEcefX) * 100.0) + 0.5); + long majorEcefY = floor((settings.fixedEcefY * 100) + 0.5); + long minorEcefY = floor((((settings.fixedEcefY * 100.0) - majorEcefY) * 100.0) + 0.5); + long majorEcefZ = floor((settings.fixedEcefZ * 100) + 0.5); + long minorEcefZ = floor((((settings.fixedEcefZ * 100.0) - majorEcefZ) * 100.0) + 0.5); + + // systemPrintf("fixedEcefY (should be -4716808.5807): %0.04f\r\n", settings.fixedEcefY); + // systemPrintf("major (should be -471680858): %ld\r\n", majorEcefY); + // systemPrintf("minor (should be -7): %ld\r\n", minorEcefY); + + // Units are cm with a high precision extension so -1234.5678 should be called: (-123456, -78) + //-1280208.308,-4716803.847,4086665.811 is SparkFun HQ so... + + response &= theGNSS.newCfgValset(); + response &= theGNSS.addCfgValset(UBLOX_CFG_TMODE_MODE, 2); // Fixed + response &= theGNSS.addCfgValset(UBLOX_CFG_TMODE_POS_TYPE, 0); // Position in ECEF + response &= theGNSS.addCfgValset(UBLOX_CFG_TMODE_ECEF_X, majorEcefX); + response &= theGNSS.addCfgValset(UBLOX_CFG_TMODE_ECEF_X_HP, minorEcefX); + response &= theGNSS.addCfgValset(UBLOX_CFG_TMODE_ECEF_Y, majorEcefY); + response &= theGNSS.addCfgValset(UBLOX_CFG_TMODE_ECEF_Y_HP, minorEcefY); + response &= theGNSS.addCfgValset(UBLOX_CFG_TMODE_ECEF_Z, majorEcefZ); + response &= theGNSS.addCfgValset(UBLOX_CFG_TMODE_ECEF_Z_HP, minorEcefZ); + response &= theGNSS.sendCfgValset(); + } + else if (settings.fixedBaseCoordinateType == COORD_TYPE_GEODETIC) + { + // Add height of instrument (HI) to fixed altitude + // https://www.e-education.psu.edu/geog862/node/1853 + // For example, if HAE is at 100.0m, + 2m stick + 73mm ARP = 102.073 + float totalFixedAltitude = + settings.fixedAltitude + (settings.antennaHeight / 1000.0) + (settings.antennaReferencePoint / 1000.0); + + // Break coordinates into main and high precision parts + // The type casting should not effect rounding of original double cast coordinate + int64_t majorLat = settings.fixedLat * 10000000; + int64_t minorLat = ((settings.fixedLat * 10000000) - majorLat) * 100; + int64_t majorLong = settings.fixedLong * 10000000; + int64_t minorLong = ((settings.fixedLong * 10000000) - majorLong) * 100; + int32_t majorAlt = totalFixedAltitude * 100; + int32_t minorAlt = ((totalFixedAltitude * 100) - majorAlt) * 100; + + // systemPrintf("fixedLong (should be -105.184774720): %0.09f\r\n", settings.fixedLong); + // systemPrintf("major (should be -1051847747): %lld\r\n", majorLat); + // systemPrintf("minor (should be -20): %lld\r\n", minorLat); + // + // systemPrintf("fixedLat (should be 40.090335429): %0.09f\r\n", settings.fixedLat); + // systemPrintf("major (should be 400903354): %lld\r\n", majorLong); + // systemPrintf("minor (should be 29): %lld\r\n", minorLong); + // + // systemPrintf("fixedAlt (should be 1560.2284): %0.04f\r\n", settings.fixedAltitude); + // systemPrintf("major (should be 156022): %ld\r\n", majorAlt); + // systemPrintf("minor (should be 84): %ld\r\n", minorAlt); + + response &= theGNSS.newCfgValset(); + response &= theGNSS.addCfgValset(UBLOX_CFG_TMODE_MODE, 2); // Fixed + response &= theGNSS.addCfgValset(UBLOX_CFG_TMODE_POS_TYPE, 1); // Position in LLH + response &= theGNSS.addCfgValset(UBLOX_CFG_TMODE_LAT, majorLat); + response &= theGNSS.addCfgValset(UBLOX_CFG_TMODE_LAT_HP, minorLat); + response &= theGNSS.addCfgValset(UBLOX_CFG_TMODE_LON, majorLong); + response &= theGNSS.addCfgValset(UBLOX_CFG_TMODE_LON_HP, minorLong); + response &= theGNSS.addCfgValset(UBLOX_CFG_TMODE_HEIGHT, majorAlt); + response &= theGNSS.addCfgValset(UBLOX_CFG_TMODE_HEIGHT_HP, minorAlt); + response &= theGNSS.sendCfgValset(maxWait); + } + + if (!response) { + systemPrint("startFixedBase failed! "); + if (retries <= 2) { + systemPrintln("Retrying..."); + delay(1000); + maxWait = 2200; + } + else { + systemPrintln("Giving up and going into Rover mode!"); + } + } + } while ((!response) && (++retries <= 3)); + + return (response); +} + +// This function gets called from the SparkFun u-blox Arduino Library. +// As each RTCM byte comes in you can specify what to do with it +// Useful for passing the RTCM correction data to a radio, Ntrip broadcaster, etc. +void DevUBLOXGNSS::processRTCM(uint8_t incoming) +{ + // We need to prevent ntripServerProcessRTCM from writing data via Ethernet (SPI W5500) + // during an SPI checkUbloxSpi... + // We can pass incoming to ntripServerProcessRTCM if the GNSS is I2C or the variant does not have Ethernet. + // For the Ref Stn, processRTCMBuffer is called manually from inside ntripServerUpdate + if ((USE_SPI_GNSS) && (HAS_ETHERNET)) + return; + + // Check for too many digits + if (settings.enableResetDisplay == true) + { + if (rtcmPacketsSent > 99) + rtcmPacketsSent = 1; // Trim to two digits to avoid overlap + } + else + { + if (rtcmPacketsSent > 999) + rtcmPacketsSent = 1; // Trim to three digits to avoid log icon and increasing bar + } + + // Determine if we should check this byte with the RTCM checker or simply pass it along + bool passAlongIncomingByte = true; + + if (settings.enableRtcmMessageChecking == true) + passAlongIncomingByte &= checkRtcmMessage(incoming); + + // Give this byte to the various possible transmission methods + if (passAlongIncomingByte) + { + rtcmLastReceived = millis(); + rtcmBytesSent++; + + for (int serverIndex = 0; serverIndex < NTRIP_SERVER_MAX; serverIndex++) + ntripServerProcessRTCM(serverIndex, incoming); + + espnowProcessRTCM(incoming); + } } -//This function gets called from the SparkFun u-blox Arduino Library. -//As each RTCM byte comes in you can specify what to do with it -//Useful for passing the RTCM correction data to a radio, Ntrip broadcaster, etc. -void SFE_UBLOX_GNSS::processRTCM(uint8_t incoming) +// For Ref Stn (USE_SPI_GNSS and HAS_ETHERNET), call ntripServerProcessRTCM manually if there is RTCM data in the buffer +void processRTCMBuffer() { - //Count outgoing packets for display - //Assume 1Hz RTCM transmissions - if (millis() - lastRTCMPacketSent > 500) - { - lastRTCMPacketSent = millis(); - rtcmPacketsSent++; - } - - //Check for too many digits - if (logIncreasing == true) - { - if (rtcmPacketsSent > 999) rtcmPacketsSent = 1; //Trim to three digits to avoid log icon - } - else - { - if (rtcmPacketsSent > 9999) rtcmPacketsSent = 1; - } - -#ifdef COMPILE_WIFI - if (caster.connected() == true) - { - caster.write(incoming); //Send this byte to socket - casterBytesSent++; - lastServerSent_ms = millis(); - } -#endif + if ((USE_I2C_GNSS) || (!HAS_ETHERNET)) + return; + + // Check if there is any data waiting in the RTCM buffer + uint16_t rtcmBytesAvail = theGNSS.rtcmBufferAvailable(); + if (rtcmBytesAvail > 0) + { + // Check for too many digits + if (settings.enableResetDisplay == true) + { + if (rtcmPacketsSent > 99) + rtcmPacketsSent = 1; // Trim to two digits to avoid overlap + } + else + { + if (rtcmPacketsSent > 999) + rtcmPacketsSent = 1; // Trim to three digits to avoid log icon and increasing bar + } + + while (rtcmBytesAvail > 0) + { + uint8_t incoming; + + if (theGNSS.extractRTCMBufferData(&incoming, 1) != 1) + return; + + rtcmBytesAvail--; + + // Data in the u-blox library RTCM buffer is pre-checked. We don't need to check it again here. + + rtcmLastReceived = millis(); + rtcmBytesSent++; + + for (int serverIndex = 0; serverIndex < NTRIP_SERVER_MAX; serverIndex++) + ntripServerProcessRTCM(serverIndex, incoming); + + espnowProcessRTCM(incoming); + } + } } diff --git a/Firmware/RTK_Surveyor/Begin.ino b/Firmware/RTK_Surveyor/Begin.ino index 5aac7590e..9f4c77b0c 100644 --- a/Firmware/RTK_Surveyor/Begin.ino +++ b/Firmware/RTK_Surveyor/Begin.ino @@ -1,386 +1,1434 @@ -//Initial startup functions for GNSS, SD, display, radio, etc +/*------------------------------------------------------------------------------ +Begin.ino -//Based on hardware features, determine if this is RTK Surveyor or RTK Express hardware -//Must be called after Wire.begin so that we can do I2C tests + This module implements the initial startup functions for GNSS, SD, display, + radio, etc. +------------------------------------------------------------------------------*/ + +//---------------------------------------- +// Constants +//---------------------------------------- + +#define MAX_ADC_VOLTAGE 3300 // Millivolts + +// Testing shows the combined ADC+resistors is under a 1% window +#define TOLERANCE 5.20 // Percent: 94.8% - 105.2% + +//---------------------------------------- +// Hardware initialization functions +//---------------------------------------- +// Determine if the measured value matches the product ID value +// idWithAdc applies resistor tolerance using worst-case tolerances: +// Upper threshold: R1 down by TOLERANCE, R2 up by TOLERANCE +// Lower threshold: R1 up by TOLERANCE, R2 down by TOLERANCE +bool idWithAdc(uint16_t mvMeasured, float r1, float r2) +{ + float lowerThreshold; + float upperThreshold; + + // ADC input + // r1 KOhms | r2 KOhms + // MAX_ADC_VOLTAGE -----/\/\/\/\-----+-----/\/\/\/\----- Ground + + // Return true if the mvMeasured value is within the tolerance range + // of the mvProduct value + upperThreshold = ceil(MAX_ADC_VOLTAGE * (r2 * (1.0 + (TOLERANCE / 100.0))) / + ((r1 * (1.0 - (TOLERANCE / 100.0))) + (r2 * (1.0 + (TOLERANCE / 100.0))))); + lowerThreshold = floor(MAX_ADC_VOLTAGE * (r2 * (1.0 - (TOLERANCE / 100.0))) / + ((r1 * (1.0 + (TOLERANCE / 100.0))) + (r2 * (1.0 - (TOLERANCE / 100.0))))); + + // systemPrintf("r1: %0.2f r2: %0.2f lowerThreshold: %0.0f mvMeasured: %d upperThreshold: %0.0f\r\n", r1, r2, + // lowerThreshold, mvMeasured, upperThreshold); + + return (upperThreshold > mvMeasured) && (mvMeasured > lowerThreshold); +} + +// Use a pair of resistors on pin 35 to ID the board type +// If the ID resistors are not available then use a variety of other methods +// (I2C, GPIO test, etc) to ID the board. +// Assume no hardware interfaces have been started so we need to start/stop any hardware +// used in tests accordingly. +void identifyBoard() +{ + // Use ADC to check the resistor divider + int pin_deviceID = 35; + uint16_t idValue = analogReadMilliVolts(pin_deviceID); + log_d("Board ADC ID (mV): %d", idValue); + + // Order the following ID checks, by millivolt values high to low + + // Facet L-Band Direct: 4.7/1 --> 534mV < 579mV < 626mV + if (idWithAdc(idValue, 4.7, 1)) + productVariant = RTK_FACET_LBAND_DIRECT; + + // Express: 10/3.3 --> 761mV < 819mV < 879mV + else if (idWithAdc(idValue, 10, 3.3)) + productVariant = RTK_EXPRESS; + + // Reference Station: 20/10 --> 1031mV < 1100mV < 1171mV + else if (idWithAdc(idValue, 20, 10)) + { + productVariant = REFERENCE_STATION; + // We can't auto-detect the ZED version if the firmware is in configViaEthernet mode, + // so fake it here - otherwise messageSupported always returns false + zedFirmwareVersionInt = 112; + } + // Facet: 10/10 --> 1571mV < 1650mV < 1729mV + else if (idWithAdc(idValue, 10, 10)) + productVariant = RTK_FACET; + + // Facet L-Band: 10/20 --> 2129mV < 2200mV < 2269mV + else if (idWithAdc(idValue, 10, 20)) + productVariant = RTK_FACET_LBAND; + + // Express+: 3.3/10 --> 2421mV < 2481mV < 2539mV + else if (idWithAdc(idValue, 3.3, 10)) + productVariant = RTK_EXPRESS_PLUS; + + // ID resistors do not exist for the following: + // Surveyor + // Unknown + else + { + log_d("Out of band or nonexistent resistor IDs"); + productVariant = RTK_UNKNOWN; // Need to wait until the GNSS and Accel have been initialized + } +} + +// Setup any essential power pins +// E.g. turn on power for the display before beginDisplay +void initializePowerPins() +{ + if (productVariant == REFERENCE_STATION) + { + // v10 + // Pin Allocations: + // D0 : Boot + Boot Button + // D1 : Serial TX (CH340 RX) + // D2 : SDIO DAT0 - via 74HC4066 switch + // D3 : Serial RX (CH340 TX) + // D4 : SDIO DAT1 + // D5 : GNSS Chip Select + // D12 : SDIO DAT2 - via 74HC4066 switch + // D13 : SDIO DAT3 + // D14 : SDIO CLK + // D15 : SDIO CMD - via 74HC4066 switch + // D16 : Serial1 RXD : Note: connected to the I/O connector only - not to the ZED-F9P + // D17 : Serial1 TXD : Note: connected to the I/O connector only - not to the ZED-F9P + // D18 : SPI SCK + // D19 : SPI POCI + // D21 : I2C SDA + // D22 : I2C SCL + // D23 : SPI PICO + // D25 : GNSS Time Pulse + // D26 : STAT LED + // D27 : Ethernet Chip Select + // D32 : PWREN + // D33 : Ethernet Interrupt + // A34 : GNSS TX RDY + // A35 : Board Detect (1.1V) + // A36 : microSD card detect + // A39 : Unused analog pin - used to generate random values for SSL + + pin_baseStatusLED = 26; + pin_peripheralPowerControl = 32; + pin_Ethernet_CS = 27; + pin_GNSS_CS = 5; + pin_GNSS_TimePulse = 25; + pin_adc39 = 39; + pin_zed_tx_ready = 34; + pin_microSD_CardDetect = 36; + pin_Ethernet_Interrupt = 33; + pin_setupButton = 0; + + pin_radio_rx = 17; // Radio RX In = ESP TX Out + pin_radio_tx = 16; // Radio TX Out = ESP RX In + + pinMode(pin_Ethernet_CS, OUTPUT); + digitalWrite(pin_Ethernet_CS, HIGH); + pinMode(pin_GNSS_CS, OUTPUT); + digitalWrite(pin_GNSS_CS, HIGH); + + pinMode(pin_peripheralPowerControl, OUTPUT); + digitalWrite(pin_peripheralPowerControl, HIGH); // Turn on SD, W5500, etc + delay(100); + } +} + +// Based on hardware features, determine if this is RTK Surveyor or RTK Express hardware +// Must be called after beginI2C (Wire.begin) so that we can do I2C tests +// Must be called after beginGNSS so the GNSS type is known void beginBoard() { - //Use ADC to check 50% resistor divider - int pin_adc_rtk_facet = 35; - if (analogReadMilliVolts(pin_adc_rtk_facet) > (3300 / 2 * 0.9) && analogReadMilliVolts(pin_adc_rtk_facet) < (3300 / 2 * 1.1)) - { - productVariant = RTK_FACET; - } - else if (isConnected(0x19) == true) //Check for accelerometer - { - productVariant = RTK_EXPRESS; - } - else - { - productVariant = RTK_SURVEYOR; - } - - //Setup hardware pins - if (productVariant == RTK_SURVEYOR) - { - pin_batteryLevelLED_Red = 32; - pin_batteryLevelLED_Green = 33; - pin_positionAccuracyLED_1cm = 2; - pin_positionAccuracyLED_10cm = 15; - pin_positionAccuracyLED_100cm = 13; - pin_baseStatusLED = 4; - pin_bluetoothStatusLED = 12; - pin_baseSwitch = 5; - pin_microSD_CS = 25; - pin_zed_tx_ready = 26; - pin_zed_reset = 27; - pin_batteryLevel_alert = 36; - - strcpy(platformFilePrefix, "SFE_Surveyor"); - strcpy(platformPrefix, "Surveyor"); - } - else if (productVariant == RTK_EXPRESS) - { - pin_muxA = 2; - pin_muxB = 4; - pin_powerSenseAndControl = 13; - pin_setupButton = 14; - pin_microSD_CS = 25; - pin_dac26 = 26; - pin_powerFastOff = 27; - pin_adc39 = 39; - - pinMode(pin_powerSenseAndControl, INPUT_PULLUP); - pinMode(pin_powerFastOff, INPUT); - - if (esp_reset_reason() == ESP_RST_POWERON) - { - powerOnCheck(); //Only do check if we POR start - } - - pinMode(pin_setupButton, INPUT_PULLUP); - - setMuxport(settings.dataPortChannel); //Set mux to user's choice: NMEA, I2C, PPS, or DAC - - strcpy(platformFilePrefix, "SFE_Express"); - strcpy(platformPrefix, "Express"); - } - else if (productVariant == RTK_FACET) - { - //v11 - pin_muxA = 2; - pin_muxB = 0; - pin_powerSenseAndControl = 13; - pin_peripheralPowerControl = 14; - pin_microSD_CS = 25; - pin_dac26 = 26; - pin_powerFastOff = 27; - pin_adc39 = 39; - - pinMode(pin_powerSenseAndControl, INPUT_PULLUP); - pinMode(pin_powerFastOff, INPUT); - - if (esp_reset_reason() == ESP_RST_POWERON) - { - powerOnCheck(); //Only do check if we POR start - } - - pinMode(pin_peripheralPowerControl, OUTPUT); - digitalWrite(pin_peripheralPowerControl, HIGH); //Turn on SD, ZED, etc - - setMuxport(settings.dataPortChannel); //Set mux to user's choice: NMEA, I2C, PPS, or DAC - - delay(1000); - - strcpy(platformFilePrefix, "SFE_Facet"); - strcpy(platformPrefix, "Facet"); - } - - Serial.printf("SparkFun RTK %s v%d.%d-%s\r\n", platformPrefix, FIRMWARE_VERSION_MAJOR, FIRMWARE_VERSION_MINOR, __DATE__); - - //For all boards, check reset reason. If reset was due to wdt or panic, append last log - if (esp_reset_reason() == ESP_RST_POWERON) - { - reuseLastLog = false; //Start new log - } - else - { - reuseLastLog = true; //Attempt to reuse previous log - - Serial.print("Reset reason: "); - switch (esp_reset_reason()) - { - case ESP_RST_UNKNOWN: Serial.println(F("ESP_RST_UNKNOWN")); break; - case ESP_RST_POWERON : Serial.println(F("ESP_RST_POWERON")); break; - case ESP_RST_SW : Serial.println(F("ESP_RST_SW")); break; - case ESP_RST_PANIC : Serial.println(F("ESP_RST_PANIC")); break; - case ESP_RST_INT_WDT : Serial.println(F("ESP_RST_INT_WDT")); break; - case ESP_RST_TASK_WDT : Serial.println(F("ESP_RST_TASK_WDT")); break; - case ESP_RST_WDT : Serial.println(F("ESP_RST_WDT")); break; - case ESP_RST_DEEPSLEEP : Serial.println(F("ESP_RST_DEEPSLEEP")); break; - case ESP_RST_BROWNOUT : Serial.println(F("ESP_RST_BROWNOUT")); break; - case ESP_RST_SDIO : Serial.println(F("ESP_RST_SDIO")); break; - default : Serial.println(F("Unknown")); - } - } + if (productVariant == RTK_UNKNOWN) + { + if (isConnected(0x19) == true) // Check for accelerometer + { + if (zedModuleType == PLATFORM_F9P) + productVariant = RTK_EXPRESS; + else if (zedModuleType == PLATFORM_F9R) + productVariant = RTK_EXPRESS_PLUS; + } + else + { + // Detect RTK Expresses (v1.3 and below) that do not have an accel or device ID resistors + + // On a Surveyor, pin 34 is not connected. On Express, 34 is connected to ZED_TX_READY + const int pin_ZedTxReady = 34; + uint16_t pinValue = analogReadMilliVolts(pin_ZedTxReady); + log_d("Alternate ID pinValue (mV): %d\r\n", pinValue); // Surveyor = 142 to 152, //Express = 3129 + if (pinValue > 3000) + { + if (zedModuleType == PLATFORM_F9P) + productVariant = RTK_EXPRESS; + else if (zedModuleType == PLATFORM_F9R) + productVariant = RTK_EXPRESS_PLUS; + } + else + productVariant = RTK_SURVEYOR; + } + } + + // Setup hardware pins + if (productVariant == RTK_SURVEYOR) + { + pin_batteryLevelLED_Red = 32; + pin_batteryLevelLED_Green = 33; + pin_positionAccuracyLED_1cm = 2; + pin_positionAccuracyLED_10cm = 15; + pin_positionAccuracyLED_100cm = 13; + pin_baseStatusLED = 4; + pin_bluetoothStatusLED = 12; + pin_setupButton = 5; + pin_microSD_CS = 25; + pin_zed_tx_ready = 26; + pin_zed_reset = 27; + pin_batteryLevel_alert = 36; + + // Bug in ZED-F9P v1.13 firmware causes RTK LED to not light when RTK Floating with SBAS on. + // The following changes the POR default but will be overwritten by settings in NVM or settings file + settings.ubxConstellations[1].enabled = false; + } + else if (productVariant == RTK_EXPRESS || productVariant == RTK_EXPRESS_PLUS) + { + pin_muxA = 2; + pin_muxB = 4; + pin_powerSenseAndControl = 13; + pin_setupButton = 14; + pin_microSD_CS = 25; + pin_dac26 = 26; + pin_powerFastOff = 27; + pin_adc39 = 39; + + pinMode(pin_powerSenseAndControl, INPUT_PULLUP); + pinMode(pin_powerFastOff, INPUT); + + if (esp_reset_reason() == ESP_RST_POWERON) + { + powerOnCheck(); // Only do check if we POR start + } + + pinMode(pin_setupButton, INPUT_PULLUP); + + setMuxport(settings.dataPortChannel); // Set mux to user's choice: NMEA, I2C, PPS, or DAC + } + else if (productVariant == RTK_FACET || productVariant == RTK_FACET_LBAND || + productVariant == RTK_FACET_LBAND_DIRECT) + { + // v11 + pin_muxA = 2; + pin_muxB = 0; + pin_powerSenseAndControl = 13; + pin_peripheralPowerControl = 14; + pin_microSD_CS = 25; + pin_dac26 = 26; + pin_powerFastOff = 27; + pin_adc39 = 39; + + pin_radio_rx = 33; + pin_radio_tx = 32; + pin_radio_rst = 15; + pin_radio_pwr = 4; + pin_radio_cts = 5; + // pin_radio_rts = 255; //Not implemented + + pinMode(pin_powerSenseAndControl, INPUT_PULLUP); + pinMode(pin_powerFastOff, INPUT); + + if (esp_reset_reason() == ESP_RST_POWERON) + { + powerOnCheck(); // Only do check if we POR start + } + + pinMode(pin_peripheralPowerControl, OUTPUT); + digitalWrite(pin_peripheralPowerControl, HIGH); // Turn on SD, ZED, etc + + setMuxport(settings.dataPortChannel); // Set mux to user's choice: NMEA, I2C, PPS, or DAC + + // CTS is active low. ESP32 pin 5 has pullup at POR. We must drive it low. + pinMode(pin_radio_cts, OUTPUT); + digitalWrite(pin_radio_cts, LOW); + + if (productVariant == RTK_FACET_LBAND_DIRECT) + { + // Override the default setting if a user has not explicitly configured the setting + if (settings.useI2cForLbandCorrectionsConfigured == false) + settings.useI2cForLbandCorrections = false; + } + } + else if (productVariant == REFERENCE_STATION) + { + // No powerOnCheck + + settings.enablePrintBatteryMessages = false; // No pesky battery messages + } + + displaySfeFlame(); + + char versionString[21]; + getFirmwareVersion(versionString, sizeof(versionString), true); + systemPrintf("SparkFun RTK %s %s\r\n", platformPrefix, versionString); + + // Get unit MAC address + esp_read_mac(wifiMACAddress, ESP_MAC_WIFI_STA); + memcpy(btMACAddress, wifiMACAddress, sizeof(wifiMACAddress)); + btMACAddress[5] += + 2; // Convert MAC address to Bluetooth MAC (add 2): + // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/system.html#mac-address + memcpy(ethernetMACAddress, wifiMACAddress, sizeof(wifiMACAddress)); + ethernetMACAddress[5] += 3; // Convert MAC address to Ethernet MAC (add 3) + + // For all boards, check reset reason. If reset was due to wdt or panic, append last log + loadSettingsPartial(); // Loads settings from LFS + if ((esp_reset_reason() == ESP_RST_POWERON) || (esp_reset_reason() == ESP_RST_SW)) + { + reuseLastLog = false; // Start new log + + if (settings.enableResetDisplay == true) + { + settings.resetCount = 0; + recordSystemSettingsToFileLFS(settingsFileName); // Avoid overwriting LittleFS settings onto SD + } + settings.resetCount = 0; + } + else + { + reuseLastLog = true; // Attempt to reuse previous log + + if (settings.enableResetDisplay == true) + { + settings.resetCount++; + systemPrintf("resetCount: %d\r\n", settings.resetCount); + recordSystemSettingsToFileLFS(settingsFileName); // Avoid overwriting LittleFS settings onto SD + } + + systemPrint("Reset reason: "); + switch (esp_reset_reason()) + { + case ESP_RST_UNKNOWN: + systemPrintln("ESP_RST_UNKNOWN"); + break; + case ESP_RST_POWERON: + systemPrintln("ESP_RST_POWERON"); + break; + case ESP_RST_SW: + systemPrintln("ESP_RST_SW"); + break; + case ESP_RST_PANIC: + systemPrintln("ESP_RST_PANIC"); + break; + case ESP_RST_INT_WDT: + systemPrintln("ESP_RST_INT_WDT"); + break; + case ESP_RST_TASK_WDT: + systemPrintln("ESP_RST_TASK_WDT"); + break; + case ESP_RST_WDT: + systemPrintln("ESP_RST_WDT"); + break; + case ESP_RST_DEEPSLEEP: + systemPrintln("ESP_RST_DEEPSLEEP"); + break; + case ESP_RST_BROWNOUT: + systemPrintln("ESP_RST_BROWNOUT"); + break; + case ESP_RST_SDIO: + systemPrintln("ESP_RST_SDIO"); + break; + default: + systemPrintln("Unknown"); + } + } } void beginSD() { - pinMode(pin_microSD_CS, OUTPUT); - digitalWrite(pin_microSD_CS, HIGH); //Be sure SD is deselected - - if (settings.enableSD == true) - { - //Max power up time is 250ms: https://www.kingston.com/datasheets/SDCIT-specsheet-64gb_en.pdf - //Max current is 200mA average across 1s, peak 300mA - delay(10); - - if (sd.begin(SdSpiConfig(pin_microSD_CS, DEDICATED_SPI, SD_SCK_MHZ(settings.spiFrequency), &spi)) == false) - { - int tries = 0; - int maxTries = 2; - for ( ; tries < maxTries ; tries++) - { - Serial.printf("SD init failed. Trying again %d out of %d\n\r", tries + 1, maxTries); - - delay(250); //Give SD more time to power up, then try again - if (sd.begin(SdSpiConfig(pin_microSD_CS, DEDICATED_SPI, SD_SCK_MHZ(settings.spiFrequency), &spi)) == true) break; - } - - if (tries == maxTries) - { - Serial.println(F("SD init failed. Is card present? Formatted?")); - digitalWrite(pin_microSD_CS, HIGH); //Be sure SD is deselected - online.microSD = false; + if(sdCardForcedOffline == true) return; - } - } - //Change to root directory. All new file creation will be in root. - if (sd.chdir() == false) + bool gotSemaphore; + + online.microSD = false; + gotSemaphore = false; + + while (settings.enableSD == true) { - Serial.println(F("SD change directory failed")); - online.microSD = false; - return; + // Setup SD card access semaphore + if (sdCardSemaphore == nullptr) + sdCardSemaphore = xSemaphoreCreateMutex(); + else if (xSemaphoreTake(sdCardSemaphore, fatSemaphore_shortWait_ms) != pdPASS) + { + // This is OK since a retry will occur next loop + log_d("sdCardSemaphore failed to yield, Begin.ino line %d", __LINE__); + break; + } + gotSemaphore = true; + markSemaphore(FUNCTION_BEGINSD); + + if (USE_SPI_MICROSD) + { + log_d("Initializing microSD - using SPI, SdFat and SdFile"); + + pinMode(pin_microSD_CS, OUTPUT); + digitalWrite(pin_microSD_CS, HIGH); // Be sure SD is deselected + resetSPI(); // Re-initialize the SPI/SD interface + + // Do a quick test to see if a card is present + int tries = 0; + int maxTries = 5; + while (tries < maxTries) + { + if (sdPresent() == true) + break; + // log_d("SD present failed. Trying again %d out of %d", tries + 1, maxTries); + + // Max power up time is 250ms: https://www.kingston.com/datasheets/SDCIT-specsheet-64gb_en.pdf + // Max current is 200mA average across 1s, peak 300mA + delay(10); + tries++; + } + if (tries == maxTries) + break; // Give up loop + + // If an SD card is present, allow SdFat to take over + log_d("SD card detected - using SPI and SdFat"); + + // Allocate the data structure that manages the microSD card + if (!sd) + { + sd = new SdFat(); + if (!sd) + { + log_d("Failed to allocate the SdFat structure!"); + break; + } + } + + if (settings.spiFrequency > 16) + { + systemPrintln("Error: SPI Frequency out of range. Default to 16MHz"); + settings.spiFrequency = 16; + } + + resetSPI(); // Re-initialize the SPI/SD interface + + if (sd->begin(SdSpiConfig(pin_microSD_CS, SHARED_SPI, SD_SCK_MHZ(settings.spiFrequency))) == false) + { + tries = 0; + maxTries = 1; + for (; tries < maxTries; tries++) + { + log_d("SD init failed - using SPI and SdFat. Trying again %d out of %d", tries + 1, maxTries); + + delay(250); // Give SD more time to power up, then try again + if (sd->begin(SdSpiConfig(pin_microSD_CS, SHARED_SPI, SD_SCK_MHZ(settings.spiFrequency))) == true) + break; + } + + if (tries == maxTries) + { + systemPrintln("SD init failed - using SPI and SdFat. Is card formatted?"); + digitalWrite(pin_microSD_CS, HIGH); // Be sure SD is deselected + + sdCardForcedOffline = true; //Prevent future scans for SD cards + + // Check reset count and prevent rolling reboot + if (settings.resetCount < 5) + { + if (settings.forceResetOnSDFail == true) + ESP.restart(); + } + break; + } + } + + // Change to root directory. All new file creation will be in root. + if (sd->chdir() == false) + { + systemPrintln("SD change directory failed"); + break; + } + } +#ifdef COMPILE_SD_MMC + else + { + // Check to see if a card is present + if (sdPresent() == false) + break; // Give up on loop + + systemPrintln("Initializing microSD - using SDIO, SD_MMC and File"); + + // SDIO MMC + if (SD_MMC.begin() == false) + { + int tries = 0; + int maxTries = 1; + for (; tries < maxTries; tries++) + { + log_d("SD init failed - using SD_MMC. Trying again %d out of %d", tries + 1, maxTries); + + delay(250); // Give SD more time to power up, then try again + if (SD_MMC.begin() == true) + break; + } + + if (tries == maxTries) + { + systemPrintln("SD init failed - using SD_MMC. Is card formatted?"); + + // Check reset count and prevent rolling reboot + if (settings.resetCount < 5) + { + if (settings.forceResetOnSDFail == true) + ESP.restart(); + } + break; + } + } + } +#else // COMPILE_SD_MMC + else + { + log_d("SD_MMC not compiled"); + break; // No SD available. + } +#endif // COMPILE_SD_MMC + + if (createTestFile() == false) + { + systemPrintln("Failed to create test file. Format SD card with 'SD Card Formatter'."); + displaySDFail(5000); + break; + } + + // Load firmware file from the microSD card if it is present + scanForFirmware(); + + // Mark card not yet usable for logging + sdCardSize = 0; + outOfSDSpace = true; + + systemPrintln("microSD: Online"); + online.microSD = true; + break; } - //Setup FAT file access semaphore - if (xFATSemaphore == NULL) + // Free the semaphore + if (sdCardSemaphore && gotSemaphore) + xSemaphoreGive(sdCardSemaphore); // Make the file system available for use +} + +void endSD(bool alreadyHaveSemaphore, bool releaseSemaphore) +{ + // Disable logging + endLogging(alreadyHaveSemaphore, false); + + // Done with the SD card + if (online.microSD) { - xFATSemaphore = xSemaphoreCreateMutex(); - if (xFATSemaphore != NULL) - xSemaphoreGive(xFATSemaphore); //Make the file system available for use + if (USE_SPI_MICROSD) + sd->end(); +#ifdef COMPILE_SD_MMC + else + SD_MMC.end(); +#endif // COMPILE_SD_MMC + + online.microSD = false; + systemPrintln("microSD: Offline"); } - if (createTestFile() == false) + // Free the caches for the microSD card + if (USE_SPI_MICROSD) { - Serial.println(F("Failed to create test file. Format SD card with 'SD Card Formatter'.")); - displaySDFail(5000); - online.microSD = false; - return; + if (sd) + { + delete sd; + sd = nullptr; + } } - online.microSD = true; - } - else - { - online.microSD = false; - } + // Release the semaphore + if (releaseSemaphore) + xSemaphoreGive(sdCardSemaphore); +} + +// Attempt to de-init the SD card - SPI only +// https://github.com/greiman/SdFat/issues/351 +void resetSPI() +{ + if (USE_SPI_MICROSD) + { + pinMode(pin_microSD_CS, OUTPUT); + digitalWrite(pin_microSD_CS, HIGH); // De-select SD card + + // Flush SPI interface + SPI.begin(); + SPI.beginTransaction(SPISettings(400000, MSBFIRST, SPI_MODE0)); + for (int x = 0; x < 10; x++) + SPI.transfer(0XFF); + SPI.endTransaction(); + SPI.end(); + + digitalWrite(pin_microSD_CS, LOW); // Select SD card + + // Flush SD interface + SPI.begin(); + SPI.beginTransaction(SPISettings(400000, MSBFIRST, SPI_MODE0)); + for (int x = 0; x < 10; x++) + SPI.transfer(0XFF); + SPI.endTransaction(); + SPI.end(); + + digitalWrite(pin_microSD_CS, HIGH); // Deselet SD card + } } -//We do not start the UART2 for GNSS->BT reception here because the interrupts would be pinned to core 1 -//competing with I2C interrupts -//See issue: https://github.com/espressif/arduino-esp32/issues/3386 -//We instead start a task that runs on core 0, that then begins serial +// We want the UART2 interrupts to be pinned to core 0 to avoid competing with I2C interrupts +// We do not start the UART2 for GNSS->BT reception here because the interrupts would be pinned to core 1 +// We instead start a task that runs on core 0, that then begins serial +// See issue: https://github.com/espressif/arduino-esp32/issues/3386 void beginUART2() { - if (startUART2TaskHandle == NULL) xTaskCreatePinnedToCore( - startUART2Task, - "UARTStart", //Just for humans - 2000, //Stack Size - NULL, //Task input parameter - 0, // Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest. - &startUART2TaskHandle, //Task handle - 0); //Core where task should run, 0=core, 1=Arduino - - while (uart2Started == false) //Wait for task to run once - delay(1); + size_t length; + + // Determine the length of data to be retained in the ring buffer + // after discarding the oldest data + length = settings.gnssHandlerBufferSize; + rbOffsetEntries = (length >> 1) / AVERAGE_SENTENCE_LENGTH_IN_BYTES; + length = settings.gnssHandlerBufferSize + (rbOffsetEntries * sizeof(RING_BUFFER_OFFSET)); + ringBuffer = nullptr; + rbOffsetArray = (RING_BUFFER_OFFSET *)malloc(length); + if (!rbOffsetArray) + { + rbOffsetEntries = 0; + systemPrintln("ERROR: Failed to allocate the ring buffer!"); + } + else + { + ringBuffer = (uint8_t *)&rbOffsetArray[rbOffsetEntries]; + rbOffsetArray[0] = 0; + if (pinUART2TaskHandle == nullptr) + xTaskCreatePinnedToCore( + pinUART2Task, + "UARTStart", // Just for humans + 2000, // Stack Size + nullptr, // Task input parameter + 0, // Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest + &pinUART2TaskHandle, // Task handle + settings.gnssUartInterruptsCore); // Core where task should run, 0=core, 1=Arduino + + while (uart2pinned == false) // Wait for task to run once + delay(1); + } } -//ESP32 requires the creation of an EEPROM space -void beginEEPROM() +// Assign UART2 interrupts to the core that started the task. See: +// https://github.com/espressif/arduino-esp32/issues/3386 +void pinUART2Task(void *pvParameters) { - if (EEPROM.begin(EEPROM_SIZE) == false) - Serial.println(F("beginEEPROM: Failed to initialize EEPROM")); - else - online.eeprom = true; + // Note: ESP32 2.0.6 does some strange auto-bauding thing here which takes 20s to complete if there is no data for + // it to auto-baud. + // That's fine for most RTK products, but causes the Ref Stn to stall for 20s. However, it doesn't stall with + // ESP32 2.0.2... Uncomment these lines to prevent the stall if/when we upgrade to ESP32 ~2.0.6. + // #if defined(REF_STN_GNSS_DEBUG) + // if (ENABLE_DEVELOPER && productVariant == REFERENCE_STATION) + // #else // REF_STN_GNSS_DEBUG + // if (USE_I2C_GNSS) + // #endif // REF_STN_GNSS_DEBUG + { + serialGNSS.setRxBufferSize( + settings.uartReceiveBufferSize); // TODO: work out if we can reduce or skip this when using SPI GNSS + serialGNSS.setTimeout(settings.serialTimeoutGNSS); // Requires serial traffic on the UART pins for detection + serialGNSS.begin(settings.dataPortBaud); // UART2 on pins 16/17 for SPP. The ZED-F9P will be configured to + // output NMEA over its UART1 at the same rate. + + // Reduce threshold value above which RX FIFO full interrupt is generated + // Allows more time between when the UART interrupt occurs and when the FIFO buffer overruns + // serialGNSS.setRxFIFOFull(50); //Available in >v2.0.5 + uart_set_rx_full_threshold(2, settings.serialGNSSRxFullThreshold); // uart_num, threshold + } + + uart2pinned = true; + + vTaskDelete(nullptr); // Delete task once it has run once +} + +void beginFS() +{ + if (online.fs == false) + { + if (LittleFS.begin(true) == false) // Format LittleFS if begin fails + { + systemPrintln("Error: LittleFS not online"); + } + else + { + systemPrintln("LittleFS Started"); + online.fs = true; + } + } } -void beginDisplay() +// Check if configureViaEthernet.txt exists +// Used to indicate if SparkFun_WebServer_ESP32_W5500 needs _exclusive_ access to SPI and interrupts +bool checkConfigureViaEthernet() { - //0x3D is default on Qwiic board - if (isConnected(0x3D) == true || isConnected(0x3C) == true) - { - online.display = true; + if (online.fs == false) + return false; + + if (LittleFS.exists("/configureViaEthernet.txt")) + { + log_d("LittleFS configureViaEthernet.txt exists"); + LittleFS.remove("/configureViaEthernet.txt"); + return true; + } + + return false; +} + +// Force configure-via-ethernet mode by creating configureViaEthernet.txt in LittleFS +// Used to indicate if SparkFun_WebServer_ESP32_W5500 needs _exclusive_ access to SPI and interrupts +bool forceConfigureViaEthernet() +{ + if (online.fs == false) + return false; + + if (LittleFS.exists("/configureViaEthernet.txt")) + { + log_d("LittleFS configureViaEthernet.txt already exists"); + return true; + } - oled.setI2CTransactionSize(64); //Increase to page size of 64. Slight speed improvement over 32 bytes. + File cveFile = LittleFS.open("/configureViaEthernet.txt", FILE_WRITE); + cveFile.close(); - displaySplash(); - } + if (LittleFS.exists("/configureViaEthernet.txt")) + return true; + + log_d("Unable to create configureViaEthernet.txt on LittleFS"); + return false; } -//Connect to and configure ZED-F9P +// Connect to ZED module and identify particulars void beginGNSS() { - if (i2cGNSS.begin() == false) - { - //Try again with power on delay - delay(1000); //Wait for ZED-F9P to power up before it can respond to ACK - if (i2cGNSS.begin() == false) - { - Serial.println(F("u-blox GNSS not detected at default I2C address. Hard stop.")); - displayGNSSFail(0); - blinkError(ERROR_NO_I2C); - } - } - - //Increase transactions to reduce transfer time - i2cGNSS.i2cTransactionSize = 128; - - //Check the firmware version of the ZED-F9P. Based on Example21_ModuleInfo. - if (i2cGNSS.getModuleInfo(1100) == true) // Try to get the module info - { - strcpy(zedFirmwareVersion, i2cGNSS.minfo.extension[1]); - - //i2cGNSS.minfo.extension[1] looks like 'FWVER=HPG 1.12' - //Replace = with - to avoid NVM parsing issues - char *ptr = strchr(zedFirmwareVersion, '='); - if (ptr != NULL) - zedFirmwareVersion[ptr - zedFirmwareVersion] = ':'; - - Serial.print(F("ZED-F9P firmware: ")); - Serial.println(zedFirmwareVersion); - - // if (strcmp(i2cGNSS.minfo.extension[1], latestZEDFirmware) != 0) - // { - // Serial.print(F("The ZED-F9P appears to have outdated firmware. Found: ")); - // Serial.println(i2cGNSS.minfo.extension[1]); - // Serial.print(F("The Surveyor works best with ")); - // Serial.println(latestZEDFirmware); - // Serial.print(F("Please upgrade using u-center.")); - // Serial.println(); - // } - // else - // { - // Serial.println(F("ZED-F9P firmware is current")); - // } - } - - bool response = configureUbloxModule(); - if (response == false) - { - //Try once more - Serial.println(F("Failed to configure module. Trying again.")); - delay(1000); - response = configureUbloxModule(); + // Skip if going into configure-via-ethernet mode + if (configureViaEthernet) + { + log_d("configureViaEthernet: skipping beginGNSS"); + return; + } + + // If we're using SPI, then increase the logging buffer + if (USE_SPI_GNSS) + { + SPI.begin(); // Begin SPI here - beginSD has not yet been called + + // setFileBufferSize must be called _before_ .begin + // Use gnssHandlerBufferSize for now. TODO: work out if the SPI GNSS needs its own buffer size setting + // Also used by Tasks.ino + theGNSS.setFileBufferSize(settings.gnssHandlerBufferSize); + theGNSS.setRTCMBufferSize(settings.gnssHandlerBufferSize); + } + + if (USE_I2C_GNSS) + { + if (theGNSS.begin() == false) + { + log_d("GNSS Failed to begin. Trying again."); + // Try again with power on delay + delay(1000); // Wait for ZED-F9P to power up before it can respond to ACK + if (theGNSS.begin() == false) + { + log_d("GNSS offline"); + displayGNSSFail(1000); + return; + } + } + } + else + { + if (theGNSS.begin(SPI, pin_GNSS_CS) == false) + { + log_d("GNSS Failed to begin. Trying again."); + + // Try again with power on delay + delay(1000); // Wait for ZED-F9P to power up before it can respond to ACK + if (theGNSS.begin(SPI, pin_GNSS_CS) == false) + { + log_d("GNSS offline"); + displayGNSSFail(1000); + return; + } + } + + if (theGNSS.getFileBufferSize() != settings.gnssHandlerBufferSize) // Need to call getFileBufferSize after begin + { + log_d("GNSS offline - no RAM for file buffer"); + displayGNSSFail(1000); + return; + } + if (theGNSS.getRTCMBufferSize() != settings.gnssHandlerBufferSize) // Need to call getRTCMBufferSize after begin + { + log_d("GNSS offline - no RAM for RTCM buffer"); + displayGNSSFail(1000); + return; + } + } + + // Increase transactions to reduce transfer time + if (USE_I2C_GNSS) + theGNSS.i2cTransactionSize = 128; + + // Auto-send Valset messages before the buffer is completely full + theGNSS.autoSendCfgValsetAtSpaceRemaining(16); + + // Check the firmware version of the ZED-F9P. Based on Example21_ModuleInfo. + if (theGNSS.getModuleInfo(1100) == true) // Try to get the module info + { + // Clear the module type. Default to PLATFORM_F9P below - if needed + zedModuleType = 0; + + // Determine if we have a ZED-F9P (Express/Facet) or an ZED-F9R (Express Plus/Facet Plus) + if (strstr(theGNSS.getModuleName(), "ZED-F9P") != nullptr) + zedModuleType = PLATFORM_F9P; + else if (strstr(theGNSS.getModuleName(), "ZED-F9R") != nullptr) + zedModuleType = PLATFORM_F9R; + + // Reconstruct the firmware version + snprintf(zedFirmwareVersion, sizeof(zedFirmwareVersion), "%s %d.%02d", theGNSS.getFirmwareType(), + theGNSS.getFirmwareVersionHigh(), theGNSS.getFirmwareVersionLow()); + + // Construct the firmware version as uint8_t. Note: will fail above 2.55! + zedFirmwareVersionInt = (theGNSS.getFirmwareVersionHigh() * 100) + theGNSS.getFirmwareVersionLow(); + + // Check if this is known firmware + //"1.51" - ZED-F9P released November, 2024 + //"1.50" - ZED-F9P released July, 2024 + //"1.32" - ZED-F9P released May, 2022 + //"1.30" - ZED-F9P (HPG) released Dec, 2021. Also ZED-F9R (HPS) released Sept, 2022 + //"1.21" - F9R HPS v1.21 + //"1.20" - Mostly for F9R HPS 1.20, but also F9P HPG v1.20 + + const uint8_t knownFirmwareVersions[] = {151, 150, 132, 130, 121, 120, 113, 112, 100}; + bool knownFirmware = false; + for (uint8_t i = 0; i < (sizeof(knownFirmwareVersions) / sizeof(uint8_t)); i++) + { + if (zedFirmwareVersionInt == knownFirmwareVersions[i]) + knownFirmware = true; + } + + if (!knownFirmware) + { + systemPrintf("Unknown firmware version: %s\r\n", zedFirmwareVersion); + // Let's be clever and allow ZED-F9P firmware versions higher than knownFirmwareVersions[0] + if ((zedModuleType == PLATFORM_F9P) && (zedFirmwareVersionInt > knownFirmwareVersions[0])) + { + zedFirmwareVersionInt = knownFirmwareVersions[0]; + systemPrintf("Assuming firmware compatibility with %d.%02d\r\n", zedFirmwareVersionInt / 100, zedFirmwareVersionInt % 100); + } + else + { + zedFirmwareVersionInt = 99; // 0.99 invalid firmware version + } + } + + if (zedModuleType == 0) + { + systemPrintf("Unknown ZED module: %s. Assuming compatibility with ZED-F9P\r\n", theGNSS.getModuleName()); + zedModuleType = PLATFORM_F9P; + } + + printZEDInfo(); // Print module type and firmware version + } + + UBX_SEC_UNIQID_data_t chipID; + if (theGNSS.getUniqueChipId(&chipID)) + { + snprintf(zedUniqueId, sizeof(zedUniqueId), "%s", theGNSS.getUniqueChipIdStr(&chipID)); + } + + online.gnss = true; +} + +// Configuration can take >1s so configure during splash +void configureGNSS() +{ + // Skip if going into configure-via-ethernet mode + if (configureViaEthernet) + { + log_d("configureViaEthernet: skipping configureGNSS"); + return; + } + + if (online.gnss == false) + return; + + // Check if the ubxMessageRates or ubxMessageRatesBase need to be defaulted + checkMessageRates(); + + theGNSS.setAutoPVTcallbackPtr(&storePVTdata); // Enable automatic NAV PVT messages with callback to storePVTdata + theGNSS.setAutoHPPOSLLHcallbackPtr( + &storeHPdata); // Enable automatic NAV HPPOSLLH messages with callback to storeHPdata + theGNSS.setRTCM1005InputcallbackPtr( + &storeRTCM1005data); // Configure a callback for RTCM 1005 - parsed from pushRawData + theGNSS.setRTCM1006InputcallbackPtr( + &storeRTCM1006data); // Configure a callback for RTCM 1006 - parsed from pushRawData + + if (HAS_GNSS_TP_INT) + theGNSS.setAutoTIMTPcallbackPtr( + &storeTIMTPdata); // Enable automatic TIM TP messages with callback to storeTIMTPdata + + if (HAS_ANTENNA_SHORT_OPEN) + { + theGNSS.newCfgValset(); + + theGNSS.addCfgValset(UBLOX_CFG_HW_ANT_CFG_SHORTDET, 1); // Enable antenna short detection + theGNSS.addCfgValset(UBLOX_CFG_HW_ANT_CFG_OPENDET, 1); // Enable antenna open detection + + if (theGNSS.sendCfgValset()) + { + theGNSS.setAutoMONHWcallbackPtr( + &storeMONHWdata); // Enable automatic MON HW messages with callback to storeMONHWdata + } + else + { + systemPrintln("Failed to configure GNSS antenna detection"); + } + } + + // Configuring the ZED can take more than 2000ms. We save configuration to + // ZED so there is no need to update settings unless user has modified + // the settings file or internal settings. + if (settings.updateZEDSettings == false) + { + log_d("Skipping ZED configuration"); + return; + } + + bool response = configureUbloxModule(); if (response == false) { - Serial.println(F("Failed to configure module. Hard stop.")); - displayGNSSFail(0); - blinkError(ERROR_GPS_CONFIG_FAIL); + // Try once more + systemPrintln("Failed to configure GNSS module. Trying again."); + delay(1000); + response = configureUbloxModule(); + + if (response == false) + { + systemPrintln("Failed to configure GNSS module."); + displayGNSSFail(1000); + online.gnss = false; + return; + } } - } - Serial.println(F("GNSS configuration complete")); + systemPrintln("GNSS configuration complete"); +} + +// Begin interrupts +void beginInterrupts() +{ + // Skip if going into configure-via-ethernet mode + if (configureViaEthernet) + { + log_d("configureViaEthernet: skipping beginInterrupts"); + return; + } - online.gnss = true; + if (HAS_GNSS_TP_INT) // If the GNSS Time Pulse is connected, use it as an interrupt to set the clock accurately + { + pinMode(pin_GNSS_TimePulse, INPUT); + attachInterrupt(pin_GNSS_TimePulse, tpISR, RISING); + } + +#ifdef COMPILE_ETHERNET + if (HAS_ETHERNET) + { + pinMode(pin_Ethernet_Interrupt, INPUT_PULLUP); // Prepare the interrupt pin + attachInterrupt(pin_Ethernet_Interrupt, ethernetISR, FALLING); // Attach the interrupt + } +#endif // COMPILE_ETHERNET } -//Set LEDs for output and configure PWM +// Set LEDs for output and configure PWM void beginLEDs() { - if (productVariant == RTK_SURVEYOR) - { - pinMode(pin_positionAccuracyLED_1cm, OUTPUT); - pinMode(pin_positionAccuracyLED_10cm, OUTPUT); - pinMode(pin_positionAccuracyLED_100cm, OUTPUT); - pinMode(pin_baseStatusLED, OUTPUT); - pinMode(pin_bluetoothStatusLED, OUTPUT); - pinMode(pin_baseSwitch, INPUT_PULLUP); //HIGH = rover, LOW = base - - digitalWrite(pin_positionAccuracyLED_1cm, LOW); - digitalWrite(pin_positionAccuracyLED_10cm, LOW); - digitalWrite(pin_positionAccuracyLED_100cm, LOW); - digitalWrite(pin_baseStatusLED, LOW); - digitalWrite(pin_bluetoothStatusLED, LOW); - - ledcSetup(ledRedChannel, freq, resolution); - ledcSetup(ledGreenChannel, freq, resolution); - - ledcAttachPin(pin_batteryLevelLED_Red, ledRedChannel); - ledcAttachPin(pin_batteryLevelLED_Green, ledGreenChannel); - - ledcWrite(ledRedChannel, 0); - ledcWrite(ledGreenChannel, 0); - } + if (productVariant == RTK_SURVEYOR) + { + pinMode(pin_positionAccuracyLED_1cm, OUTPUT); + pinMode(pin_positionAccuracyLED_10cm, OUTPUT); + pinMode(pin_positionAccuracyLED_100cm, OUTPUT); + pinMode(pin_baseStatusLED, OUTPUT); + pinMode(pin_bluetoothStatusLED, OUTPUT); + pinMode(pin_setupButton, INPUT_PULLUP); // HIGH = rover, LOW = base + + digitalWrite(pin_positionAccuracyLED_1cm, LOW); + digitalWrite(pin_positionAccuracyLED_10cm, LOW); + digitalWrite(pin_positionAccuracyLED_100cm, LOW); + digitalWrite(pin_baseStatusLED, LOW); + digitalWrite(pin_bluetoothStatusLED, LOW); + + ledcSetup(ledRedChannel, pwmFreq, pwmResolution); + ledcSetup(ledGreenChannel, pwmFreq, pwmResolution); + ledcSetup(ledBTChannel, pwmFreq, pwmResolution); + + ledcAttachPin(pin_batteryLevelLED_Red, ledRedChannel); + ledcAttachPin(pin_batteryLevelLED_Green, ledGreenChannel); + ledcAttachPin(pin_bluetoothStatusLED, ledBTChannel); + + ledcWrite(ledRedChannel, 0); + ledcWrite(ledGreenChannel, 0); + ledcWrite(ledBTChannel, 0); + } + else if (productVariant == REFERENCE_STATION) + { + pinMode(pin_baseStatusLED, OUTPUT); + digitalWrite(pin_baseStatusLED, LOW); + } } -//Configure the on board MAX17048 fuel gauge +// Configure the on board MAX17048 fuel gauge void beginFuelGauge() { - // Set up the MAX17048 LiPo fuel gauge - if (lipo.begin() == false) - { - Serial.println(F("MAX17048 not detected. Continuing.")); - return; - } + if (HAS_NO_BATTERY) + return; // Reference station does not have a battery + + // Set up the MAX17048 LiPo fuel gauge + if (lipo.begin() == false) + { + systemPrintln("Fuel gauge not detected."); + return; + } + + online.battery = true; + + // Always use hibernate mode + if (lipo.getHIBRTActThr() < 0xFF) + lipo.setHIBRTActThr((uint8_t)0xFF); + if (lipo.getHIBRTHibThr() < 0xFF) + lipo.setHIBRTHibThr((uint8_t)0xFF); + + systemPrintln("Fuel gauge configuration complete"); - //Always use hibernate mode - if (lipo.getHIBRTActThr() < 0xFF) lipo.setHIBRTActThr((uint8_t)0xFF); - if (lipo.getHIBRTHibThr() < 0xFF) lipo.setHIBRTHibThr((uint8_t)0xFF); + checkBatteryLevels(); // Force check so you see battery level immediately at power on - Serial.println(F("MAX17048 configuration complete")); + // Check to see if we are dangerously low + if (battLevel < 5 && battChangeRate < 0.5) // 5% and not charging + { + systemPrintln("Battery too low. Please charge. Shutting down..."); + + if (online.display == true) + displayMessage("Charge Battery", 0); + + delay(2000); - online.battery = true; + powerDown(false); // Don't display 'Shutting Down' + } } -//Begin accelerometer if available +// Begin accelerometer if available void beginAccelerometer() { - if (accel.begin() == false) - { - online.accelerometer = false; - return; - } + if (accel.begin() == false) + { + online.accelerometer = false; - //The larger the avgAmount the faster we should read the sensor - //accel.setDataRate(LIS2DH12_ODR_100Hz); //6 measurements a second - accel.setDataRate(LIS2DH12_ODR_400Hz); //25 measurements a second + return; + } + + // The larger the avgAmount the faster we should read the sensor + // accel.setDataRate(LIS2DH12_ODR_100Hz); //6 measurements a second + accel.setDataRate(LIS2DH12_ODR_400Hz); // 25 measurements a second - Serial.println(F("Accelerometer configuration complete")); + systemPrintln("Accelerometer configuration complete"); - online.accelerometer = true; + online.accelerometer = true; } -//Depending on platform and previous power down state, set system state +// Depending on platform and previous power down state, set system state void beginSystemState() { - if (productVariant == RTK_SURVEYOR) - { - //Assume Rover. checkButtons() will correct as needed. - systemState = STATE_ROVER_NOT_STARTED; - buttonPreviousState = BUTTON_BASE; - } - if (productVariant == RTK_EXPRESS || productVariant == RTK_EXPRESS) - { - systemState = settings.lastState; //Return to system state previous to power down. - - if (systemState == STATE_ROVER_NOT_STARTED) - buttonPreviousState = BUTTON_ROVER; - else if (systemState == STATE_BASE_NOT_STARTED) - buttonPreviousState = BUTTON_BASE; + if (systemState > STATE_NOT_SET) + { + systemPrintln("Unknown state - factory reset"); + factoryReset(false); // We do not have the SD semaphore + } + + if (productVariant == RTK_SURVEYOR) + { + if (settings.lastState == STATE_NOT_SET) // Default + { + systemState = STATE_ROVER_NOT_STARTED; + settings.lastState = systemState; + } + + // If the rocker switch was moved while off, force module settings + // When switch is set to '1' = BASE, pin will be shorted to ground + if (settings.lastState == STATE_ROVER_NOT_STARTED && digitalRead(pin_setupButton) == LOW) + settings.updateZEDSettings = true; + else if (settings.lastState == STATE_BASE_NOT_STARTED && digitalRead(pin_setupButton) == HIGH) + settings.updateZEDSettings = true; + + systemState = STATE_ROVER_NOT_STARTED; // Assume Rover. ButtonCheckTask() will correct as needed. + + setupBtn = new Button(pin_setupButton); // Create the button in memory + // Allocation failure handled in ButtonCheckTask + } + else if (productVariant == RTK_EXPRESS || productVariant == RTK_EXPRESS_PLUS) + { + if (settings.lastState == STATE_NOT_SET) // Default + { + systemState = STATE_ROVER_NOT_STARTED; + settings.lastState = systemState; + } + + if (online.lband == false) + systemState = + settings + .lastState; // Return to either Rover or Base Not Started. The last state previous to power down. + else + systemState = STATE_KEYS_STARTED; // Begin process for getting new keys + + setupBtn = new Button(pin_setupButton); // Create the button in memory + powerBtn = new Button(pin_powerSenseAndControl); // Create the button in memory + // Allocation failures handled in ButtonCheckTask + } + else if (productVariant == RTK_FACET || productVariant == RTK_FACET_LBAND || + productVariant == RTK_FACET_LBAND_DIRECT) + { + if (settings.lastState == STATE_NOT_SET) // Default + { + systemState = STATE_ROVER_NOT_STARTED; + settings.lastState = systemState; + } + + if (online.lband == false) + systemState = + settings + .lastState; // Return to either Rover or Base Not Started. The last state previous to power down. + else + systemState = STATE_KEYS_STARTED; // Begin process for getting new keys + + firstRoverStart = true; // Allow user to enter test screen during first rover start + if (systemState == STATE_BASE_NOT_STARTED) + firstRoverStart = false; + + powerBtn = new Button(pin_powerSenseAndControl); // Create the button in memory + // Allocation failure handled in ButtonCheckTask + } + else if (productVariant == REFERENCE_STATION) + { + if (settings.lastState == STATE_NOT_SET) // Default + { + systemState = STATE_BASE_NOT_STARTED; + settings.lastState = systemState; + } + + systemState = + settings + .lastState; // Return to either NTP, Base or Rover Not Started. The last state previous to power down. + + setupBtn = new Button(pin_setupButton); // Create the button in memory + // Allocation failure handled in ButtonCheckTask + } + + // Starts task for monitoring button presses + if (ButtonCheckTaskHandle == nullptr) + xTaskCreate(ButtonCheckTask, + "BtnCheck", // Just for humans + buttonTaskStackSize, // Stack Size + nullptr, // Task input parameter + ButtonCheckTaskPriority, + &ButtonCheckTaskHandle); // Task handle +} + +// Setup the timepulse output on the PPS pin for external triggering +// Setup TM2 time stamp input as need +bool beginExternalTriggers() +{ + // Skip if going into configure-via-ethernet mode + if (configureViaEthernet) + { + log_d("configureViaEthernet: skipping beginExternalTriggers"); + return (false); + } + + if (online.gnss == false) + return (false); + + // If our settings haven't changed, trust ZED's settings + if (settings.updateZEDSettings == false) + { + log_d("Skipping ZED Trigger configuration"); + return (true); + } + + if (settings.dataPortChannel != MUX_PPS_EVENTTRIGGER) + return (true); // No need to configure PPS if port is not selected + + bool response = true; + + response &= theGNSS.newCfgValset(); + response &= theGNSS.addCfgValset(UBLOX_CFG_TP_PULSE_DEF, 0); // Time pulse definition is a period (in us) + response &= theGNSS.addCfgValset(UBLOX_CFG_TP_PULSE_LENGTH_DEF, 1); // Define timepulse by length (not ratio) + response &= + theGNSS.addCfgValset(UBLOX_CFG_TP_USE_LOCKED_TP1, + 1); // Use CFG-TP-PERIOD_LOCK_TP1 and CFG-TP-LEN_LOCK_TP1 as soon as GNSS time is valid + response &= theGNSS.addCfgValset(UBLOX_CFG_TP_TP1_ENA, settings.enableExternalPulse); // Enable/disable timepulse + response &= + theGNSS.addCfgValset(UBLOX_CFG_TP_POL_TP1, settings.externalPulsePolarity); // 0 = falling, 1 = rising edge + + // While the module is _locking_ to GNSS time, turn off pulse + response &= theGNSS.addCfgValset(UBLOX_CFG_TP_PERIOD_TP1, 1000000); // Set the period between pulses in us + response &= theGNSS.addCfgValset(UBLOX_CFG_TP_LEN_TP1, 0); // Set the pulse length in us + + // When the module is _locked_ to GNSS time, make it generate 1Hz (Default is 100ms high, 900ms low) + response &= theGNSS.addCfgValset(UBLOX_CFG_TP_PERIOD_LOCK_TP1, + settings.externalPulseTimeBetweenPulse_us); // Set the period between pulses is us + response &= + theGNSS.addCfgValset(UBLOX_CFG_TP_LEN_LOCK_TP1, settings.externalPulseLength_us); // Set the pulse length in us + response &= theGNSS.sendCfgValset(); + + if (response == false) + systemPrintln("beginExternalTriggers config failed"); + + if (settings.enableExternalHardwareEventLogging == true) + { + theGNSS.setAutoTIMTM2callbackPtr( + &eventTriggerReceived); // Enable automatic TIM TM2 messages with callback to eventTriggerReceived + } else - buttonPreviousState = BUTTON_ROVER; - } + theGNSS.setAutoTIMTM2callbackPtr(nullptr); + + return (response); +} + +void beginIdleTasks() +{ + if (settings.enablePrintIdleTime == true) + { + char taskName[32]; + + for (int index = 0; index < MAX_CPU_CORES; index++) + { + snprintf(taskName, sizeof(taskName), "IdleTask%d", index); + if (idleTaskHandle[index] == nullptr) + xTaskCreatePinnedToCore( + idleTask, + taskName, // Just for humans + 2000, // Stack Size + nullptr, // Task input parameter + 0, // Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest + &idleTaskHandle[index], // Task handle + index); // Core where task should run, 0=core, 1=Arduino + } + } +} + +void beginI2C() +{ + if (pinI2CTaskHandle == nullptr) + xTaskCreatePinnedToCore( + pinI2CTask, + "I2CStart", // Just for humans + 2000, // Stack Size + nullptr, // Task input parameter + 0, // Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest + &pinI2CTaskHandle, // Task handle + settings.i2cInterruptsCore); // Core where task should run, 0=core, 1=Arduino + + while (i2cPinned == false) // Wait for task to run once + delay(1); +} + +// Assign I2C interrupts to the core that started the task. See: https://github.com/espressif/arduino-esp32/issues/3386 +void pinI2CTask(void *pvParameters) +{ + bool i2cBusAvailable; + uint32_t timer; + + Wire.begin(); // Start I2C on core the core that was chosen when the task was started + // Wire.setClock(400000); + + // Display the device addresses + i2cBusAvailable = false; + for (uint8_t addr = 0; addr < 127; addr++) + { + // begin/end wire transmission to see if the bus is responding correctly + // All good: 0ms, response 2 + // SDA/SCL shorted: 1000ms timeout, response 5 + // SCL/VCC shorted: 14ms, response 5 + // SCL/GND shorted: 1000ms, response 5 + // SDA/VCC shorted: 1000ms, response 5 + // SDA/GND shorted: 14ms, response 5 + timer = millis(); + Wire.beginTransmission(addr); + if (Wire.endTransmission() == 0) + { + i2cBusAvailable = true; + switch (addr) + { + default: { + systemPrintf("0x%02x\r\n", addr); + break; + } + + case 0x19: { + systemPrintf("0x%02x - LIS2DH12 Accelerometer\r\n", addr); + break; + } + + case 0x36: { + systemPrintf("0x%02x - MAX17048 Fuel Gauge\r\n", addr); + break; + } + + case 0x3d: { + systemPrintf("0x%02x - SSD1306 (64x48) OLED Driver\r\n", addr); + break; + } + + case 0x42: { + systemPrintf("0x%02x - u-blox ZED-F9P GNSS Receiver\r\n", addr); + break; + } + + case 0x43: { + systemPrintf("0x%02x - u-blox NEO-D9S-00B Correction Data Receiver\r\n", addr); + break; + } + + case 0x60: { + systemPrintf("0x%02x - Crypto Coprocessor\r\n", addr); + break; + } + } + } + else if ((millis() - timer) > 3) + { + systemPrintln("Error: I2C Bus Not Responding"); + i2cBusAvailable = false; + break; + } + } + + // Update the I2C status + online.i2c = i2cBusAvailable; + i2cPinned = true; + vTaskDelete(nullptr); // Delete task once it has run once +} + +// Depending on radio selection, begin hardware +void radioStart() +{ + if (settings.radioType == RADIO_EXTERNAL) + { + espnowStop(); + + // Nothing to start. UART2 of ZED is connected to external Radio port and is configured at + // configureUbloxModule() + } + else if (settings.radioType == RADIO_ESPNOW) + espnowStart(); +} + +// Start task to determine SD card size +void beginSDSizeCheckTask() +{ + if (sdSizeCheckTaskHandle == nullptr) + { + xTaskCreate(sdSizeCheckTask, // Function to call + "SDSizeCheck", // Just for humans + sdSizeCheckStackSize, // Stack Size + nullptr, // Task input parameter + sdSizeCheckTaskPriority, // Priority + &sdSizeCheckTaskHandle); // Task handle + + log_d("sdSizeCheck Task started"); + } +} + +void deleteSDSizeCheckTask() +{ + // Delete task once it's complete + if (sdSizeCheckTaskHandle != nullptr) + { + vTaskDelete(sdSizeCheckTaskHandle); + sdSizeCheckTaskHandle = nullptr; + sdSizeCheckTaskComplete = false; + log_d("sdSizeCheck Task deleted"); + } +} + +// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +// Time Pulse ISR +// Triggered by the rising edge of the time pulse signal, indicates the top-of-second. +// Set the ESP32 RTC to UTC + +void tpISR() +{ + unsigned long millisNow = millis(); + if (!inMainMenu) // Skip this if the menu is open + { + if (online.rtc) // Only sync if the RTC has been set via PVT first + { + if (timTpUpdated) // Only sync if timTpUpdated is true + { + if (millisNow - lastRTCSync > + syncRTCInterval) // Only sync if it is more than syncRTCInterval since the last sync + { + if (millisNow < (timTpArrivalMillis + 999)) // Only sync if the GNSS time is not stale + { + if (fullyResolved) // Only sync if GNSS time is fully resolved + { + if (tAcc < 5000) // Only sync if the tAcc is better than 5000ns + { + // To perform the time zone adjustment correctly, it's easiest if we convert the GNSS + // time and date into Unix epoch first and then apply the timeZone offset + uint32_t epochSecs = timTpEpoch; + uint32_t epochMicros = timTpMicros; + epochSecs += settings.timeZoneSeconds; + epochSecs += settings.timeZoneMinutes * 60; + epochSecs += settings.timeZoneHours * 60 * 60; + + // Set the internal system time + rtc.setTime(epochSecs, epochMicros); + + lastRTCSync = millis(); + rtcSyncd = true; + + gnssSyncTv.tv_sec = epochSecs; // Store the timeval of the sync + gnssSyncTv.tv_usec = epochMicros; + + if (syncRTCInterval < 59000) // From now on, sync every minute + syncRTCInterval = 59000; + } + } + } + } + } + } + } } diff --git a/Firmware/RTK_Surveyor/Bluetooth.ino b/Firmware/RTK_Surveyor/Bluetooth.ino new file mode 100644 index 000000000..7648605d0 --- /dev/null +++ b/Firmware/RTK_Surveyor/Bluetooth.ino @@ -0,0 +1,354 @@ +/*=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + Bluetooth State Values: + BT_OFF = 0, + BT_NOTCONNECTED, + BT_CONNECTED, + + Bluetooth States: + + BT_OFF (Using WiFi) + | ^ + Use Bluetooth | | Use WiFi + bluetoothStart | | bluetoothStop + v | + BT_NOTCONNECTED + | ^ + Client connected | | Client disconnected + v | + BT_CONNECTED + + =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=*/ + +//---------------------------------------- +// Constants +//---------------------------------------- + +//---------------------------------------- +// Locals - compiled out +//---------------------------------------- + +static volatile BTState bluetoothState = BT_OFF; + +#ifdef COMPILE_BT +BTSerialInterface *bluetoothSerial; + +//---------------------------------------- +// Bluetooth Routines - compiled out +//---------------------------------------- + +// Call back for when BT connection event happens (connected/disconnect) +// Used for updating the bluetoothState state machine +void bluetoothCallback(esp_spp_cb_event_t event, esp_spp_cb_param_t *param) +{ + if (event == ESP_SPP_SRV_OPEN_EVT) + { + systemPrintln("BT client Connected"); + bluetoothState = BT_CONNECTED; + if (productVariant == RTK_SURVEYOR) + digitalWrite(pin_bluetoothStatusLED, HIGH); + } + + if (event == ESP_SPP_CLOSE_EVT) + { + systemPrintln("BT client disconnected"); + + btPrintEcho = false; + btPrintEchoExit = true; // Force exit all config menus + printEndpoint = PRINT_ENDPOINT_SERIAL; + + bluetoothState = BT_NOTCONNECTED; + if (productVariant == RTK_SURVEYOR) + digitalWrite(pin_bluetoothStatusLED, LOW); + } +} + +#endif // COMPILE_BT + +//---------------------------------------- +// Global Bluetooth Routines +//---------------------------------------- + +// Return the Bluetooth state +byte bluetoothGetState() +{ +#ifdef COMPILE_BT + return bluetoothState; +#else // COMPILE_BT + return BT_OFF; +#endif // COMPILE_BT +} + +// Read data from the Bluetooth device +int bluetoothRead(uint8_t *buffer, int length) +{ +#ifdef COMPILE_BT + return bluetoothSerial->readBytes(buffer, length); +#else // COMPILE_BT + return 0; +#endif // COMPILE_BT +} + +// Read data from the Bluetooth device +uint8_t bluetoothRead() +{ +#ifdef COMPILE_BT + return bluetoothSerial->read(); +#else // COMPILE_BT + return 0; +#endif // COMPILE_BT +} + +// Determine if data is available +bool bluetoothRxDataAvailable() +{ +#ifdef COMPILE_BT + return bluetoothSerial->available(); +#else // COMPILE_BT + return false; +#endif // COMPILE_BT +} + +// Write data to the Bluetooth device +int bluetoothWrite(const uint8_t *buffer, int length) +{ +#ifdef COMPILE_BT + // BLE write does not handle 0 length requests correctly + if (length > 0) + return bluetoothSerial->write(buffer, length); + else + return 0; +#else // COMPILE_BT + return 0; +#endif // COMPILE_BT +} + +// Write data to the Bluetooth device +int bluetoothWrite(uint8_t value) +{ +#ifdef COMPILE_BT + return bluetoothSerial->write(value); +#else // COMPILE_BT + return 0; +#endif // COMPILE_BT +} + +// Flush Bluetooth device +void bluetoothFlush() +{ +#ifdef COMPILE_BT + bluetoothSerial->flush(); +#else // COMPILE_BT + return; +#endif // COMPILE_BT +} + +// Get MAC, start radio +// Tack device's MAC address to end of friendly broadcast name +// This allows multiple units to be on at same time +void bluetoothStart() +{ +#ifdef COMPILE_BT + if (bluetoothState == BT_OFF) + { + char stateName[11] = {0}; + if (systemState >= STATE_ROVER_NOT_STARTED && systemState <= STATE_ROVER_RTK_FIX) + strncpy(stateName, "Rover-", sizeof(stateName) - 1); + else if (systemState >= STATE_BASE_NOT_STARTED && systemState <= STATE_BASE_FIXED_TRANSMITTING) + strncpy(stateName, "Base-", sizeof(stateName) - 1); + else + { + strncpy(stateName, "Rover-", sizeof(stateName) - 1); + log_d("State out of range for Bluetooth Broadcast: %d", systemState); + } + + char productName[50] = {0}; + strncpy(productName, platformPrefix, sizeof(productName)); + + // BLE is limited to ~28 characters in the device name. Shorten platformPrefix if needed. + if (settings.bluetoothRadioType == BLUETOOTH_RADIO_BLE) + { + if (strcmp(productName, "Facet L-Band Direct") == 0) + { + strncpy(productName, "Facet L-Band", sizeof(productName)); + } + } + + snprintf(deviceName, sizeof(deviceName), "%s %s%02X%02X", productName, stateName, btMACAddress[4], + btMACAddress[5]); + + if (strlen(deviceName) > 28) + { + if (ENABLE_DEVELOPER) + systemPrintf("Warning! The Bluetooth device name '%s' is %d characters long. It may not work in BLE mode.\r\n", deviceName, + strlen(deviceName)); + } + + // Select Bluetooth setup + if (settings.bluetoothRadioType == BLUETOOTH_RADIO_OFF) + return; + else if (settings.bluetoothRadioType == BLUETOOTH_RADIO_SPP) + bluetoothSerial = new BTClassicSerial(); + else if (settings.bluetoothRadioType == BLUETOOTH_RADIO_BLE) + bluetoothSerial = new BTLESerial(); + + // Not yet implemented + // if (pinBluetoothTaskHandle == nullptr) + // xTaskCreatePinnedToCore( + // pinBluetoothTask, + // "BluetoothStart", // Just for humans + // 2000, // Stack Size + // nullptr, // Task input parameter + // 0, // Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the + // lowest &pinBluetoothTaskHandle, // Task handle settings.bluetoothInterruptsCore); // + // Core where task should run, 0=core, 1=Arduino + + // while (bluetoothPinned == false) // Wait for task to run once + // delay(1); + + if (bluetoothSerial->begin(deviceName, false, settings.sppRxQueueSize, settings.sppTxQueueSize) == + false) // localName, isMaster, rxBufferSize, txBufferSize + { + systemPrintln("An error occurred initializing Bluetooth"); + + if (productVariant == RTK_SURVEYOR) + digitalWrite(pin_bluetoothStatusLED, LOW); + return; + } + + // Set PIN to 1234 so we can connect to older BT devices, but not require a PIN for modern device pairing + // See issue: https://github.com/sparkfun/SparkFun_RTK_Firmware/issues/5 + // https://github.com/espressif/esp-idf/issues/1541 + //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + esp_bt_sp_param_t param_type = ESP_BT_SP_IOCAP_MODE; + + esp_bt_io_cap_t iocap = ESP_BT_IO_CAP_NONE; // Requires pin 1234 on old BT dongle, No prompt on new BT dongle + // esp_bt_io_cap_t iocap = ESP_BT_IO_CAP_OUT; //Works but prompts for either pin (old) or 'Does this 6 pin + // appear on the device?' (new) + + esp_bt_gap_set_security_param(param_type, &iocap, sizeof(uint8_t)); + + esp_bt_pin_type_t pin_type = ESP_BT_PIN_TYPE_FIXED; + esp_bt_pin_code_t pin_code; + pin_code[0] = '1'; + pin_code[1] = '2'; + pin_code[2] = '3'; + pin_code[3] = '4'; + esp_bt_gap_set_pin(pin_type, 4, pin_code); + //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + + bluetoothSerial->register_callback(bluetoothCallback); // Controls BT Status LED on Surveyor + bluetoothSerial->setTimeout(250); + + if (settings.bluetoothRadioType == BLUETOOTH_RADIO_SPP) + systemPrint("Bluetooth SPP broadcasting as: "); + else if (settings.bluetoothRadioType == BLUETOOTH_RADIO_BLE) + systemPrint("Bluetooth Low-Energy broadcasting as: "); + + systemPrintln(deviceName); + + // Start task for controlling Bluetooth pair LED + if (productVariant == RTK_SURVEYOR) + { + ledcWrite(ledBTChannel, 255); // Turn on BT LED + btLEDTask.detach(); // Slow down the BT LED blinker task + btLEDTask.attach(btLEDTaskPace2Hz, updateBTled); // Rate in seconds, callback + } + + bluetoothState = BT_NOTCONNECTED; + reportHeapNow(false); + } +#endif // COMPILE_BT +} + +// Assign Bluetooth interrupts to the core that started the task. See: +// https://github.com/espressif/arduino-esp32/issues/3386 +void pinBluetoothTask(void *pvParameters) +{ +#ifdef COMPILE_BT + if (bluetoothSerial->begin(deviceName, false, settings.sppRxQueueSize, settings.sppTxQueueSize) == + false) // localName, isMaster, rxBufferSize, + { + systemPrintln("An error occurred initializing Bluetooth"); + + if (productVariant == RTK_SURVEYOR) + digitalWrite(pin_bluetoothStatusLED, LOW); + } + + bluetoothPinned = true; + + vTaskDelete(nullptr); // Delete task once it has run once +#endif // COMPILE_BT +} + +// This function stops BT so that it can be restarted later +// It also releases as much system resources as possible so that WiFi/caster is more stable +void bluetoothStop() +{ +#ifdef COMPILE_BT + if (bluetoothState == BT_NOTCONNECTED || bluetoothState == BT_CONNECTED) + { + bluetoothSerial->register_callback(nullptr); + bluetoothSerial->flush(); // Complete any transfers + bluetoothSerial->disconnect(); // Drop any clients + bluetoothSerial->end(); // bluetoothSerial->end() will release significant RAM (~100k!) but a + // bluetoothSerial->start will crash. + + log_d("Bluetooth turned off"); + + bluetoothState = BT_OFF; + reportHeapNow(false); + } +#endif // COMPILE_BT + bluetoothIncomingRTCM = false; +} + +// Test the bidirectional communication through UART2 +void bluetoothTest(bool runTest) +{ + // Verify the ESP UART2 can communicate TX/RX to ZED UART1 + const char *bluetoothStatusText; + + if (online.gnss == true) + { + if (runTest && (zedUartPassed == false) && (USE_I2C_GNSS)) + { + tasksStopUART2(); // Stop absoring ZED serial via task + + theGNSS.setVal32(UBLOX_CFG_UART1_BAUDRATE, + (115200 * 2)); // Defaults to 230400 to maximize message output support + serialGNSS.begin((115200 * 2)); // UART2 on pins 16/17 for SPP. The ZED-F9P will be configured to output + // NMEA over its UART1 at the same rate. + + SFE_UBLOX_GNSS_SERIAL myGNSS; + if (myGNSS.begin(serialGNSS) == true) // begin() attempts 3 connections + { + zedUartPassed = true; + bluetoothStatusText = (settings.bluetoothRadioType == BLUETOOTH_RADIO_OFF) ? "Off" : "Online"; + } + else + bluetoothStatusText = "Offline"; + + theGNSS.setVal32(UBLOX_CFG_UART1_BAUDRATE, + settings.dataPortBaud); // Defaults to 230400 to maximize message output support + serialGNSS.begin(settings.dataPortBaud); // UART2 on pins 16/17 for SPP. The ZED-F9P will be configured to + // output NMEA over its UART1 at the same rate. + + tasksStartUART2(); // Return to normal operation + } + else + bluetoothStatusText = (settings.bluetoothRadioType == BLUETOOTH_RADIO_OFF) ? "Off" : "Online"; + } + else + bluetoothStatusText = "GNSS Offline"; + + // Display Bluetooth MAC address and test results + char macAddress[5]; + snprintf(macAddress, sizeof(macAddress), "%02X%02X", btMACAddress[4], btMACAddress[5]); + systemPrint("Bluetooth "); + if (settings.bluetoothRadioType == BLUETOOTH_RADIO_BLE) + systemPrint("Low Energy "); + systemPrint("("); + systemPrint(macAddress); + systemPrint("): "); + systemPrintln(bluetoothStatusText); +} diff --git a/Firmware/RTK_Surveyor/Buttons.ino b/Firmware/RTK_Surveyor/Buttons.ino index e3c49fb16..3ce9ae014 100644 --- a/Firmware/RTK_Surveyor/Buttons.ino +++ b/Firmware/RTK_Surveyor/Buttons.ino @@ -1,250 +1,66 @@ -void checkButtons() +// User has pressed the power button to turn on the system +// Was it an accidental bump or do they really want to turn on? +// Let's make sure they continue to press for 500ms +void powerOnCheck() { - if (productVariant == RTK_SURVEYOR) - { - //Check rover switch and configure module accordingly - //When switch is set to '1' = BASE, pin will be shorted to ground - if (digitalRead(pin_baseSwitch) == LOW) //Switch is set to base mode - { - if (buttonPreviousState == BUTTON_ROVER) - { - buttonPreviousState = BUTTON_BASE; - changeState(STATE_BASE_NOT_STARTED); - } - } - else if (digitalRead(pin_baseSwitch) == HIGH) //Switch is set to Rover - { - if (buttonPreviousState == BUTTON_BASE) - { - buttonPreviousState = BUTTON_ROVER; - changeState(STATE_ROVER_NOT_STARTED); - } - } - } - else if (productVariant == RTK_EXPRESS) - { - //Check to see if user is pressing both buttons simultaneously - show test screen - if (digitalRead(pin_powerSenseAndControl) == LOW && digitalRead(pin_setupButton) == LOW) - { - delay(debounceDelay); //Debounce - if (digitalRead(pin_powerSenseAndControl) == LOW && digitalRead(pin_setupButton) == LOW) - { - displayTest(); - setupButtonState = BUTTON_RELEASED; - } - } - - //Check power button - //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - if (digitalRead(pin_powerSenseAndControl) == LOW && powerPressedStartTime == 0) + powerPressedStartTime = millis(); + if (pin_powerSenseAndControl >= 0) + if (digitalRead(pin_powerSenseAndControl) == LOW) + delay(500); + + if (FIRMWARE_VERSION_MAJOR == 99) { - //Debounce check - delay(debounceDelay); - if (digitalRead(pin_powerSenseAndControl) == LOW) - { - powerPressedStartTime = millis(); - } + // Do not check button if this is a locally compiled developer firmware } - else if (digitalRead(pin_powerSenseAndControl) == LOW && powerPressedStartTime > 0) + else { - //Debounce check - delay(debounceDelay); - if (digitalRead(pin_powerSenseAndControl) == LOW) - { - if ((millis() - powerPressedStartTime) > 2000) - { - powerDown(true); - } - } + if (pin_powerSenseAndControl >= 0) + if (digitalRead(pin_powerSenseAndControl) != LOW) + powerDown(false); // Power button tap. Returning to off state. } - else if (digitalRead(pin_powerSenseAndControl) == HIGH && powerPressedStartTime > 0) - { - //Debounce check - delay(debounceDelay); - if (digitalRead(pin_powerSenseAndControl) == HIGH) - { - Serial.print("Power button released after ms: "); - Serial.println(millis() - powerPressedStartTime); - powerPressedStartTime = 0; //Reset var to return to normal 'on' state - setupByPowerButton = true; //Notify base/rover setup - } - } - //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + powerPressedStartTime = 0; // Reset var to return to normal 'on' state +} - //Check setup button and configure module accordingly - //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - if (digitalRead(pin_setupButton) == LOW || setupByPowerButton == true) - { - delay(debounceDelay); //Debounce - if (digitalRead(pin_setupButton) == LOW || setupByPowerButton == true) - { - //User is pressing button - if (setupButtonState == BUTTON_RELEASED) - { - setupButtonState = BUTTON_PRESSED; +// If we have a power button tap, or if the display is not yet started (no I2C!) +// then don't display a shutdown screen +void powerDown(bool displayInfo) +{ + // Disable SD card use + endSD(false, false); - setupByPowerButton = false; + // Prevent other tasks from logging, even if access to the microSD card was denied + online.logging = false; - //Toggle between Rover and Base system states - if (buttonPreviousState == BUTTON_ROVER) - { - buttonPreviousState = BUTTON_BASE; - changeState(STATE_BASE_NOT_STARTED); - } - else if (buttonPreviousState == BUTTON_BASE) - { - buttonPreviousState = BUTTON_ROVER; - changeState(STATE_ROVER_NOT_STARTED); - } - } //End button state check - } //End debounce button check - } //End first button check - else if (digitalRead(pin_setupButton) == HIGH && setupButtonState == BUTTON_PRESSED) - { - //Return to unpressed state - setupButtonState = BUTTON_RELEASED; - } - //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - }//end productVariant = Express + // If we are in configureViaEthernet mode, we need to shut down the async web server + // otherwise it causes a core panic and badness at the restart + if (configureViaEthernet) + ethernetWebServerStopESP32W5500(); - else if (productVariant == RTK_FACET) - { - //Check power button - //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - if (digitalRead(pin_powerSenseAndControl) == LOW && powerPressedStartTime == 0) - { - //Debounce check - delay(debounceDelay); - if (digitalRead(pin_powerSenseAndControl) == LOW) - { - powerPressedStartTime = millis(); - } - } - else if (digitalRead(pin_powerSenseAndControl) == LOW && powerPressedStartTime > 0) + if (displayInfo == true) { - //Debounce check - delay(debounceDelay); - if (digitalRead(pin_powerSenseAndControl) == LOW) - { - if ((millis() - powerPressedStartTime) > 2000) - { - powerDown(true); - } - } + displayShutdown(); + delay(2000); } - else if (digitalRead(pin_powerSenseAndControl) == HIGH && powerPressedStartTime > 0) - { - //Debounce check - delay(debounceDelay); - if (digitalRead(pin_powerSenseAndControl) == HIGH) - { - Serial.print("Power button released after ms: "); - Serial.println(millis() - powerPressedStartTime); - powerPressedStartTime = 0; //Reset var to return to normal 'on' state - } - } - //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - //Check setup button and configure module accordingly - //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - if (digitalRead(pin_powerSenseAndControl) == LOW) - { - delay(debounceDelay); //Debounce - if (digitalRead(pin_powerSenseAndControl) == LOW) - { - //User is pressing button - if (setupButtonState == BUTTON_RELEASED) - { - setupButtonState = BUTTON_PRESSED; + beginLEDs(); // Turn LEDs off - //Toggle between Rover and Base system states - if (buttonPreviousState == BUTTON_ROVER) - { - buttonPreviousState = BUTTON_BASE; - changeState(STATE_BASE_NOT_STARTED); - } - else if (buttonPreviousState == BUTTON_BASE) - { - buttonPreviousState = BUTTON_ROVER; - changeState(STATE_ROVER_NOT_STARTED); - } - } //End button state check - } //End debounce button check - } //End first button check - else if (digitalRead(pin_powerSenseAndControl) == HIGH && setupButtonState == BUTTON_PRESSED) + if (pin_powerSenseAndControl >= 0) { - //Return to unpressed state - setupButtonState = BUTTON_RELEASED; + pinMode(pin_powerSenseAndControl, OUTPUT); + digitalWrite(pin_powerSenseAndControl, LOW); } - //Check to see if user is pressing both buttons simultaneously - show test screen - // if (digitalRead(pin_powerSenseAndControl) == LOW && digitalRead(pin_setupButton) == LOW) - // { - // delay(debounceDelay); //Debounce - // if (digitalRead(pin_powerSenseAndControl) == LOW && digitalRead(pin_setupButton) == LOW) - // { - // displayTest(); - // setupButtonState = BUTTON_RELEASED; - // buttonPreviousState = BUTTON_ROVER; - // changeState(STATE_ROVER_NOT_STARTED); - // } - // } - //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - }//end productVariant = Facet -} - -//User has pressed the power button to turn on the system -//Was it an accidental bump or do they really want to turn on? -//Let's make sure they continue to press for two seconds -void powerOnCheck() -{ - powerPressedStartTime = millis(); - while (digitalRead(pin_powerSenseAndControl) == LOW) - { - delay(100); //Wait for user to stop pressing button. - - if (millis() - powerPressedStartTime > 500) - break; - } - - if (millis() - powerPressedStartTime < 500) - powerDown(false); //Power button tap. Returning to off state. - - powerPressedStartTime = 0; //Reset var to return to normal 'on' state -} - -//If we have a power button tap, or if the display is not yet started (no I2C!) -//then don't display a shutdown screen -void powerDown(bool displayInfo) -{ - if (online.logging == true) - { - //Attempt to write to file system. This avoids collisions with file writing from other functions like recordSystemSettingsToFile() - //Wait up to 1000ms - if (xSemaphoreTake(xFATSemaphore, 1000 / portTICK_PERIOD_MS) == pdPASS) + if (pin_powerFastOff >= 0) { - //Close down file system - ubxFile.sync(); - ubxFile.close(); - //xSemaphoreGive(xFATSemaphore); //Do not release semaphore - } //End xFATSemaphore - - online.logging = false; - } - - if (displayInfo == true) - { - displayShutdown(); - delay(2000); - } - - pinMode(pin_powerSenseAndControl, OUTPUT); - digitalWrite(pin_powerSenseAndControl, LOW); + pinMode(pin_powerFastOff, OUTPUT); + digitalWrite(pin_powerFastOff, LOW); + } - pinMode(pin_powerFastOff, OUTPUT); - digitalWrite(pin_powerFastOff, LOW); + if ((productVariant == RTK_FACET) || (productVariant == RTK_FACET_LBAND) || + (productVariant == RTK_FACET_LBAND_DIRECT) || (productVariant == REFERENCE_STATION)) + digitalWrite(pin_peripheralPowerControl, LOW); - while (1) - delay(1); + while (1) + delay(1); } diff --git a/Firmware/RTK_Surveyor/Developer.ino b/Firmware/RTK_Surveyor/Developer.ino new file mode 100644 index 000000000..91c8387d4 --- /dev/null +++ b/Firmware/RTK_Surveyor/Developer.ino @@ -0,0 +1,145 @@ +/* +pvtServer.ino + + The code in this module is only compiled when features are disabled in developer + mode (ENABLE_DEVELOPER defined). +*/ + +#ifndef COMPILE_ETHERNET + +//---------------------------------------- +// Ethernet +//---------------------------------------- + +void menuEthernet() {systemPrintln("Ethernet not compiled");} +void ethernetBegin() {} +IPAddress ethernetGetIpAddress() {return IPAddress((uint32_t)0);} +void ethernetUpdate() {} +void ethernetVerifyTables() {} + +void ethernetPvtClientSendData(uint8_t *data, uint16_t length) {} +void ethernetPvtClientUpdate() {} + +void ethernetWebServerStartESP32W5500() {} +void ethernetWebServerStopESP32W5500() {} + +//---------------------------------------- +// NTP: Network Time Protocol +//---------------------------------------- + +void menuNTP() {systemPrint("NTP not compiled");} +void ntpServerBegin() {} +void ntpServerUpdate() {} +void ntpValidateTables() {} +void ntpServerStop() {} + +#endif // COMPILE_ETHERNET + +#if !COMPILE_NETWORK + +//---------------------------------------- +// Network layer +//---------------------------------------- + +void menuNetwork() {systemPrint("Network not compiled");} +void networkUpdate() {} +void networkVerifyTables() {} +void networkStop(uint8_t networkType) {} + +//---------------------------------------- +// NTRIP client +//---------------------------------------- + +void ntripClientPrintStatus() {systemPrint("NTRIP Client not compiled");} +void ntripClientStop(bool clientAllocated) {online.ntripClient = false;} +void ntripClientUpdate() {} +void ntripClientValidateTables() {} + +//---------------------------------------- +// NTRIP server +//---------------------------------------- + +bool ntripServerIsCasting(int serverIndex) {return false;} +void ntripServerPrintStatus(int serverIndex) {systemPrintf("**NTRIP Server %d not compiled**\r\n", serverIndex);} +void ntripServerProcessRTCM(int serverIndex, uint8_t incoming) {} +void ntripServerStop(int serverIndex, bool clientAllocated) {online.ntripServer[serverIndex] = false;} +void ntripServerUpdate() {} +void ntripServerValidateTables() {} + +//---------------------------------------- +// OTA client +//---------------------------------------- + +void otaVerifyTables() {} + +//---------------------------------------- +// PVT client +//---------------------------------------- + +int32_t pvtClientSendData(uint16_t dataHead) {return 0;} +void pvtClientUpdate() {} +void pvtClientValidateTables() {} +void pvtClientZeroTail() {} +void discardPvtClientBytes(RING_BUFFER_OFFSET previousTail, RING_BUFFER_OFFSET newTail) {} + +//---------------------------------------- +// PVT UDP server +//---------------------------------------- + +int32_t pvtUdpServerSendData(uint16_t dataHead) {return 0;} +void pvtUdpServerStop() {} +void pvtUdpServerUpdate() {} +void pvtUdpServerZeroTail() {} +void discardPvtUdpServerBytes(RING_BUFFER_OFFSET previousTail, RING_BUFFER_OFFSET newTail) {} + +#endif // COMPILE_NETWORK + +//---------------------------------------- +// Web Server +//---------------------------------------- + +#ifndef COMPILE_AP + +bool startWebServer(bool startWiFi = true, int httpPort = 80) +{ + systemPrintln("AP not compiled"); + return false; +} +void stopWebServer() {} +bool parseIncomingSettings() {return false;} + +#endif // COMPILE_AP +#ifndef COMPILE_WIFI + +//---------------------------------------- +// PVT server +//---------------------------------------- + +int32_t pvtServerSendData(uint16_t dataHead) {return 0;} +void pvtServerStop() {} +void pvtServerUpdate() {} +void pvtServerZeroTail() {} +void pvtServerValidateTables() {} +void discardPvtServerBytes(RING_BUFFER_OFFSET previousTail, RING_BUFFER_OFFSET newTail) {} + +//---------------------------------------- +// WiFi +//---------------------------------------- + +void menuWiFi() {systemPrintln("WiFi not compiled");}; +bool wifiConnect(unsigned long timeout) {return false;} +IPAddress wifiGetGatewayIpAddress() {return IPAddress((uint32_t)0);} +IPAddress wifiGetIpAddress() {return IPAddress((uint32_t)0);} +int wifiGetRssi() {return -999;} +bool wifiInConfigMode() {return false;} +bool wifiIsConnected() {return false;} +bool wifiIsNeeded() {return false;} +int wifiNetworkCount() {return 0;} +void wifiPrintNetworkInfo() {} +void wifiStart() {} +void wifiStop() {} +void wifiUpdate() {} +void wifiShutdown() {} +#define WIFI_STOP() {} + +#endif // COMPILE_WIFI diff --git a/Firmware/RTK_Surveyor/Display.ino b/Firmware/RTK_Surveyor/Display.ino index 3826de29f..ded614fb7 100644 --- a/Firmware/RTK_Surveyor/Display.ino +++ b/Firmware/RTK_Surveyor/Display.ino @@ -1,1423 +1,3414 @@ -//Given the system state, display the appropriate information +//---------------------------------------- +// Constants +//---------------------------------------- + +// A bitfield is used to flag which icon needs to be illuminated +// systemState will dictate most of the icons needed + +// The radio area (top left corner of display) has three spots for icons +// Left/Center/Right +// Left Radio spot +#define ICON_WIFI_SYMBOL_0_LEFT (1 << 0) // 0, 0 +#define ICON_WIFI_SYMBOL_1_LEFT (1 << 1) // 0, 0 +#define ICON_WIFI_SYMBOL_2_LEFT (1 << 2) // 0, 0 +#define ICON_WIFI_SYMBOL_3_LEFT (1 << 3) // 0, 0 +#define ICON_BT_SYMBOL_LEFT (1 << 4) // 0, 0 +#define ICON_MAC_ADDRESS (1 << 5) // 0, 3 +#define ICON_ESPNOW_SYMBOL_0_LEFT (1 << 6) // 0, 0 +#define ICON_ESPNOW_SYMBOL_1_LEFT (1 << 7) // 0, 0 +#define ICON_ESPNOW_SYMBOL_2_LEFT (1 << 8) // 0, 0 +#define ICON_ESPNOW_SYMBOL_3_LEFT (1 << 9) // 0, 0 +#define ICON_DOWN_ARROW_LEFT (1 << 10) // 0, 0 +#define ICON_UP_ARROW_LEFT (1 << 11) // 0, 0 +#define ICON_BLANK_LEFT (1 << 12) // 0, 0 + +// Center Radio spot +#define ICON_MAC_ADDRESS_2DIGIT (1 << 13) // 13, 3 +#define ICON_BT_SYMBOL_CENTER (1 << 14) // 10, 0 +#define ICON_DOWN_ARROW_CENTER (1 << 15) // 0, 0 +#define ICON_UP_ARROW_CENTER (1 << 16) // 0, 0 + +// Right Radio Spot +#define ICON_WIFI_SYMBOL_0_RIGHT (1 << 17) // center, 0 +#define ICON_WIFI_SYMBOL_1_RIGHT (1 << 18) // center, 0 +#define ICON_WIFI_SYMBOL_2_RIGHT (1 << 19) // center, 0 +#define ICON_WIFI_SYMBOL_3_RIGHT (1 << 20) // center, 0 +#define ICON_BASE_TEMPORARY (1 << 21) // center, 0 +#define ICON_BASE_FIXED (1 << 22) // center, 0 +#define ICON_ROVER_FUSION (1 << 23) // center, 2 +#define ICON_ROVER_FUSION_EMPTY (1 << 24) // center, 2 +#define ICON_DYNAMIC_MODEL (1 << 25) // 27, 0 +#define ICON_DOWN_ARROW_RIGHT (1 << 26) // center, 0 +#define ICON_UP_ARROW_RIGHT (1 << 27) // center, 0 +#define ICON_BLANK_RIGHT (1 << 28) // center, 0 + +// Left + Center Radio spot +#define ICON_IP_ADDRESS (1 << 29) + +// Right top +#define ICON_BATTERY (1 << 0) // 45, 0 + +// Left center +#define ICON_CROSS_HAIR (1 << 1) // 0, 18 +#define ICON_CROSS_HAIR_DUAL (1 << 2) // 0, 18 + +// Right center +#define ICON_HORIZONTAL_ACCURACY (1 << 3) // 16, 20 + +// Left bottom +#define ICON_SIV_ANTENNA (1 << 4) // 2, 35 +#define ICON_SIV_ANTENNA_LBAND (1 << 5) // 2, 35 + +// Right bottom +#define ICON_LOGGING (1 << 6) // right, bottom + +// Left center +#define ICON_CLOCK (1 << 7) +#define ICON_CLOCK_ACCURACY (1 << 8) + +// Right top +#define ICON_ETHERNET (1 << 9) + +// Right bottom +#define ICON_LOGGING_NTP (1 << 10) + +// Left bottom +#define ICON_ANTENNA_SHORT (1 << 11) +#define ICON_ANTENNA_OPEN (1 << 12) + +//---------------------------------------- +// Locals +//---------------------------------------- + +static QwiicMicroOLED oled; +static uint32_t blinking_icons; +static uint32_t icons; +static uint32_t iconsRadio; + +unsigned long ssidDisplayTimer = 0; +bool ssidDisplayFirstHalf = false; + +// Fonts +#include +#include +#include + +// Icons +#include "icons.h" + +//---------------------------------------- +// Routines +//---------------------------------------- + +void beginDisplay() +{ + blinking_icons = 0; + + // At this point we have not identified the RTK platform + // If it's surveyor, there won't be a display and we have a 100ms delay + // If it's other platforms, we will try 3 times + int maxTries = 3; + for (int x = 0; x < maxTries; x++) + { + if (oled.begin() == true) + { + online.display = true; + + systemPrintln("Display started"); + + oled.erase(); + return; + } + + delay(50); // Give display time to startup before attempting again + } + + systemPrintln("Display not detected"); +} + +// Display the SparkFun logo +void displaySfeFlame() +{ + if (online.display == true) + { + oled.erase(); + displayBitmap(0, 0, logoSparkFun_Width, logoSparkFun_Height, logoSparkFun); + oled.display(); + splashStart = millis(); + } +} + +// Avoid code repetition +void displayBatteryVsEthernet() +{ + if (HAS_BATTERY) + icons |= ICON_BATTERY; // Top right + else // if (HAS_ETHERNET) + { + if (online.ethernetStatus == ETH_NOT_STARTED) + blinking_icons &= ~ICON_ETHERNET; // If Ethernet has not stated because not needed, don't display the icon + else if (online.ethernetStatus == ETH_CONNECTED) + blinking_icons |= ICON_ETHERNET; // Don't blink if link is up + else + blinking_icons ^= ICON_ETHERNET; + icons |= (blinking_icons & ICON_ETHERNET); // Top Right + } +} +void displaySivVsOpenShort() +{ + if (!HAS_ANTENNA_SHORT_OPEN) + icons |= paintSIV(); + else + { + if (aStatus == SFE_UBLOX_ANTENNA_STATUS_SHORT) + { + blinking_icons ^= ICON_ANTENNA_SHORT; + icons |= (blinking_icons & ICON_ANTENNA_SHORT); + } + else if (aStatus == SFE_UBLOX_ANTENNA_STATUS_OPEN) + { + blinking_icons ^= ICON_ANTENNA_OPEN; + icons |= (blinking_icons & ICON_ANTENNA_OPEN); + } + else + { + blinking_icons &= ~ICON_ANTENNA_SHORT; + blinking_icons &= ~ICON_ANTENNA_OPEN; + icons |= paintSIV(); + } + } +} + +// Given the system state, display the appropriate information void updateDisplay() { - //Update the display if connected - if (online.display == true) - { - if (millis() - lastDisplayUpdate > 500) //Update display at 2Hz - { - lastDisplayUpdate = millis(); - - oled.clear(PAGE); // Clear the display's internal buffer - - switch (systemState) - { - case (STATE_ROVER_NOT_STARTED): - //Do nothing. Static display shown during state change. - break; - case (STATE_ROVER_NO_FIX): - paintRoverNoFix(); - break; - case (STATE_ROVER_FIX): - paintRoverFix(); - break; - case (STATE_ROVER_RTK_FLOAT): - paintRoverRTKFloat(); - break; - case (STATE_ROVER_RTK_FIX): - paintRoverRTKFix(); - break; - case (STATE_BASE_NOT_STARTED): - //Do nothing. Static display shown during state change. - break; - case (STATE_BASE_TEMP_SETTLE): - paintBaseTempSettle(); - break; - case (STATE_BASE_TEMP_SURVEY_STARTED): - paintBaseTempSurveyStarted(); - break; - case (STATE_BASE_TEMP_TRANSMITTING): - paintBaseTempTransmitting(); - break; - case (STATE_BASE_TEMP_WIFI_STARTED): - paintBaseTempWiFiStarted(); - break; - case (STATE_BASE_TEMP_WIFI_CONNECTED): - paintBaseTempWiFiConnected(); - break; - case (STATE_BASE_TEMP_CASTER_STARTED): - paintBaseTempCasterStarted(); - break; - case (STATE_BASE_TEMP_CASTER_CONNECTED): - paintBaseTempCasterConnected(); - break; - case (STATE_BASE_FIXED_NOT_STARTED): - paintBaseFixedNotStarted(); - break; - case (STATE_BASE_FIXED_TRANSMITTING): - paintBaseFixedTransmitting(); - break; - case (STATE_BASE_FIXED_WIFI_STARTED): - paintBaseFixedWiFiStarted(); - break; - case (STATE_BASE_FIXED_WIFI_CONNECTED): - paintBaseFixedWiFiConnected(); - break; - case (STATE_BASE_FIXED_CASTER_STARTED): - paintBaseFixedCasterStarted(); - break; - case (STATE_BASE_FIXED_CASTER_CONNECTED): - paintBaseFixedCasterConnected(); - break; - default: - displayError((char*)"Display"); - break; - } - - oled.display(); //Push internal buffer to display - } - } + // Update the display if connected + if (online.display == true) + { + if (millis() - lastDisplayUpdate > 500 || forceDisplayUpdate == true) // Update display at 2Hz + { + lastDisplayUpdate = millis(); + forceDisplayUpdate = false; + + oled.reset(false); // Incase of previous corruption, force re-alignment of CGRAM. Do not init buffers as it + // takes time and causes screen to blink. + + oled.erase(); + + icons = 0; + iconsRadio = 0; + switch (systemState) + { + + /* + 111111111122222222223333333333444444444455555555556666 + 0123456789012345678901234567890123456789012345678901234567890123 + .---------------------------------------------------------------- + 0| ******* ** ** ***************** + 1| * * ** ** * * + 2| * ***** * ** ****** * *** *** *** * + 3|* * * * ** * * * *** *** *** *** + 4| * *** * ** * * **** * * * *** *** *** * + 5| * * ** ** ** * * **** * * * *** *** *** * + 6| * ****** * * * * * *** *** *** * + 7| *** **** * * * * * *** *** *** * + 8| * ** * * * * * *** *** *** *** + 9| * * * * * *** *** *** * + 10| * * * * + 11| ****** ***************** + 12| + 13| + 14| + 15| + 16| + 17| + 18| * + 19| * + 20| ******* + 21| * * * *** *** *** + 22| * * * * * * * * * + 23| * * * * * * * * * + 24| * * * ** * * * * * * + 25|******* ******* ** * * * + 26| * * * * * * * * * + 27| * * * * * * * * * + 28| * * * * * * * * * + 29| * * * ** * * ** * * * * + 30| ******* ** *** ** *** *** + 31| * + 32| * + 33| + 34| + 35| + 36| ** ******* + 37| * * *** *** * ** + 38| * * * * * * * * ** + 39| * * * * * * * * * + 40| * * ** * * * * * ***** * + 41| * * ** * * * * + 42| * * * * * * * ***** * + 43| ** * * * * * * * + 44| **** * * * * * * ***** * + 45| ** **** ** * * * * * * + 46| ** ** *** *** * * + 47| ****** ********* + */ + + case (STATE_ROVER_NOT_STARTED): + icons = ICON_CROSS_HAIR // Center left + | ICON_HORIZONTAL_ACCURACY // Center right + | ICON_LOGGING; // Bottom right + displaySivVsOpenShort(); // Bottom left + displayBatteryVsEthernet(); // Top right + iconsRadio = setRadioIcons(); // Top left + break; + case (STATE_ROVER_NO_FIX): + icons = ICON_CROSS_HAIR // Center left + | ICON_HORIZONTAL_ACCURACY // Center right + | ICON_LOGGING; // Bottom right + displaySivVsOpenShort(); // Bottom left + displayBatteryVsEthernet(); // Top right + iconsRadio = setRadioIcons(); // Top left + break; + case (STATE_ROVER_FIX): + icons = ICON_CROSS_HAIR // Center left + | ICON_HORIZONTAL_ACCURACY // Center right + | ICON_LOGGING; // Bottom right + displaySivVsOpenShort(); // Bottom left + displayBatteryVsEthernet(); // Top right + iconsRadio = setRadioIcons(); // Top left + break; + case (STATE_ROVER_RTK_FLOAT): + blinking_icons ^= ICON_CROSS_HAIR_DUAL; + icons = (blinking_icons & ICON_CROSS_HAIR_DUAL) // Center left + | ICON_HORIZONTAL_ACCURACY // Center right + | ICON_LOGGING; // Bottom right + displaySivVsOpenShort(); // Bottom left + displayBatteryVsEthernet(); // Top right + iconsRadio = setRadioIcons(); // Top left + break; + case (STATE_ROVER_RTK_FIX): + icons = ICON_CROSS_HAIR_DUAL // Center left + | ICON_HORIZONTAL_ACCURACY // Center right + | ICON_LOGGING; // Bottom right + displaySivVsOpenShort(); // Bottom left + displayBatteryVsEthernet(); // Top right + iconsRadio = setRadioIcons(); // Top left + break; + + case (STATE_BASE_NOT_STARTED): + // Do nothing. Static display shown during state change. + break; + + // Start of base / survey in / NTRIP mode + // Screen is displayed while we are waiting for horz accuracy to drop to appropriate level + // Blink crosshair icon until we have we have horz accuracy < user defined level + case (STATE_BASE_TEMP_SETTLE): + blinking_icons ^= ICON_CROSS_HAIR; + icons = (blinking_icons & ICON_CROSS_HAIR) // Center left + | ICON_HORIZONTAL_ACCURACY // Center right + | ICON_LOGGING; // Bottom right + displaySivVsOpenShort(); // Bottom left + displayBatteryVsEthernet(); // Top right + iconsRadio = setRadioIcons(); // Top left + break; + case (STATE_BASE_TEMP_SURVEY_STARTED): + icons = ICON_LOGGING; // Bottom right + displayBatteryVsEthernet(); // Top right + iconsRadio = setRadioIcons(); // Top left + paintBaseTempSurveyStarted(); + break; + case (STATE_BASE_TEMP_TRANSMITTING): + icons = ICON_LOGGING; // Bottom right + displayBatteryVsEthernet(); // Top right + iconsRadio = setRadioIcons(); // Top left + paintRTCM(); + break; + case (STATE_BASE_FIXED_NOT_STARTED): + icons = 0; // Top right + displayBatteryVsEthernet(); // Top right + iconsRadio = setRadioIcons(); // Top left + break; + case (STATE_BASE_FIXED_TRANSMITTING): + icons = ICON_LOGGING; // Bottom right + displayBatteryVsEthernet(); // Top right + iconsRadio = setRadioIcons(); // Top left + paintRTCM(); + break; + + case (STATE_NTPSERVER_NOT_STARTED): + case (STATE_NTPSERVER_NO_SYNC): + blinking_icons ^= ICON_CLOCK; + icons = (blinking_icons & ICON_CLOCK) // Center left + | ICON_CLOCK_ACCURACY; // Center right + displaySivVsOpenShort(); // Bottom left + if (online.ethernetStatus == ETH_CONNECTED) + blinking_icons |= ICON_ETHERNET; // Don't blink if link is up + else + blinking_icons ^= ICON_ETHERNET; + icons |= (blinking_icons & ICON_ETHERNET); // Top Right + iconsRadio = ICON_IP_ADDRESS; // Top left + break; + + case (STATE_NTPSERVER_SYNC): + icons = ICON_CLOCK // Center left + | ICON_CLOCK_ACCURACY // Center right + | ICON_LOGGING_NTP; // Bottom right + displaySivVsOpenShort(); // Bottom left + if (online.ethernetStatus == ETH_CONNECTED) + blinking_icons |= ICON_ETHERNET; // Don't blink if link is up + else + blinking_icons ^= ICON_ETHERNET; + icons |= (blinking_icons & ICON_ETHERNET); // Top Right + iconsRadio = ICON_IP_ADDRESS; // Top left + break; + + case (STATE_CONFIG_VIA_ETH_NOT_STARTED): + break; + case (STATE_CONFIG_VIA_ETH_STARTED): + break; + case (STATE_CONFIG_VIA_ETH): + displayConfigViaEthernet(); + break; + case (STATE_CONFIG_VIA_ETH_RESTART_BASE): + break; + + case (STATE_BUBBLE_LEVEL): + paintBubbleLevel(); + break; + case (STATE_PROFILE): + paintProfile(displayProfile); + break; + case (STATE_MARK_EVENT): + // Do nothing. Static display shown during state change. + break; + case (STATE_DISPLAY_SETUP): + paintDisplaySetup(); + break; + case (STATE_WIFI_CONFIG_NOT_STARTED): + displayWiFiConfigNotStarted(); // Display 'WiFi Config' + break; + case (STATE_WIFI_CONFIG): + iconsRadio = setWiFiIcon(); // Blink WiFi in center + displayWiFiConfig(); // Display SSID and IP + break; + case (STATE_TEST): + paintSystemTest(); + break; + case (STATE_TESTING): + paintSystemTest(); + break; + + case (STATE_KEYS_STARTED): + paintRTCWait(); + break; + case (STATE_KEYS_NEEDED): + // Do nothing. Quick, fall through state. + break; + case (STATE_KEYS_WIFI_STARTED): + iconsRadio = setWiFiIcon(); // Blink WiFi in center + paintGettingKeys(); + break; + case (STATE_KEYS_WIFI_CONNECTED): + iconsRadio = setWiFiIcon(); // Blink WiFi in center + paintGettingKeys(); + break; + case (STATE_KEYS_WIFI_TIMEOUT): + // Do nothing. Quick, fall through state. + break; + case (STATE_KEYS_EXPIRED): + // Do nothing. Quick, fall through state. + break; + case (STATE_KEYS_DAYS_REMAINING): + // Do nothing. Quick, fall through state. + break; + case (STATE_KEYS_LBAND_CONFIGURE): + paintLBandConfigure(); + break; + case (STATE_KEYS_LBAND_ENCRYPTED): + // Do nothing. Quick, fall through state. + break; + case (STATE_KEYS_PROVISION_WIFI_STARTED): + iconsRadio = setWiFiIcon(); // Blink WiFi in center + paintGettingKeys(); + break; + case (STATE_KEYS_PROVISION_WIFI_CONNECTED): + iconsRadio = setWiFiIcon(); // Blink WiFi in center + paintGettingKeys(); + break; + + case (STATE_ESPNOW_PAIRING_NOT_STARTED): + paintEspNowPairing(); + break; + case (STATE_ESPNOW_PAIRING): + paintEspNowPairing(); + break; + + case (STATE_SHUTDOWN): + displayShutdown(); + break; + default: + systemPrintf("Unknown display: %d\r\n", systemState); + displayError("Display"); + break; + } + + // Top left corner - Radio icon indicators take three spots (left/center/right) + // Allowed icon combinations: + // Bluetooth + Rover/Base + // WiFi + Bluetooth + Rover/Base + // ESP-Now + Bluetooth + Rover/Base + // ESP-Now + Bluetooth + WiFi + // See setRadioIcons() for the icon selection logic + + // Left spot + if (iconsRadio & ICON_MAC_ADDRESS) + { + char macAddress[5]; + const uint8_t *rtkMacAddress = getMacAddress(); + + // Print four characters of MAC + snprintf(macAddress, sizeof(macAddress), "%02X%02X", rtkMacAddress[4], rtkMacAddress[5]); + oled.setFont(QW_FONT_5X7); // Set font to smallest + oled.setCursor(0, 3); + oled.print(macAddress); + } + else if (iconsRadio & ICON_BT_SYMBOL_LEFT) + displayBitmap(1, 0, BT_Symbol_Width, BT_Symbol_Height, BT_Symbol); + else if (iconsRadio & ICON_WIFI_SYMBOL_0_LEFT) + displayBitmap(0, 0, WiFi_Symbol_Width, WiFi_Symbol_Height, WiFi_Symbol_0); + else if (iconsRadio & ICON_WIFI_SYMBOL_1_LEFT) + displayBitmap(0, 0, WiFi_Symbol_Width, WiFi_Symbol_Height, WiFi_Symbol_1); + else if (iconsRadio & ICON_WIFI_SYMBOL_2_LEFT) + displayBitmap(0, 0, WiFi_Symbol_Width, WiFi_Symbol_Height, WiFi_Symbol_2); + else if (iconsRadio & ICON_WIFI_SYMBOL_3_LEFT) + displayBitmap(0, 0, WiFi_Symbol_Width, WiFi_Symbol_Height, WiFi_Symbol_3); + else if (iconsRadio & ICON_ESPNOW_SYMBOL_0_LEFT) + displayBitmap(0, 0, ESPNOW_Symbol_Width, ESPNOW_Symbol_Height, ESPNOW_Symbol_0); + else if (iconsRadio & ICON_ESPNOW_SYMBOL_1_LEFT) + displayBitmap(0, 0, ESPNOW_Symbol_Width, ESPNOW_Symbol_Height, ESPNOW_Symbol_1); + else if (iconsRadio & ICON_ESPNOW_SYMBOL_2_LEFT) + displayBitmap(0, 0, ESPNOW_Symbol_Width, ESPNOW_Symbol_Height, ESPNOW_Symbol_2); + else if (iconsRadio & ICON_ESPNOW_SYMBOL_3_LEFT) + displayBitmap(0, 0, ESPNOW_Symbol_Width, ESPNOW_Symbol_Height, ESPNOW_Symbol_3); + else if (iconsRadio & ICON_DOWN_ARROW_LEFT) + displayBitmap(1, 0, DownloadArrow_Width, DownloadArrow_Height, DownloadArrow); + else if (iconsRadio & ICON_UP_ARROW_LEFT) + displayBitmap(1, 0, UploadArrow_Width, UploadArrow_Height, UploadArrow); + else if (iconsRadio & ICON_BLANK_LEFT) + { + ; + } + + // Center radio spots + if (iconsRadio & ICON_BT_SYMBOL_CENTER) + { + // Moved to center to give space for ESP NOW icon on far left + displayBitmap(16, 0, BT_Symbol_Width, BT_Symbol_Height, BT_Symbol); + } + else if (iconsRadio & ICON_MAC_ADDRESS_2DIGIT) + { + char macAddress[5]; + const uint8_t *rtkMacAddress = getMacAddress(); + + // Print only last two digits of MAC + snprintf(macAddress, sizeof(macAddress), "%02X", rtkMacAddress[5]); + oled.setFont(QW_FONT_5X7); // Set font to smallest + oled.setCursor(14, 3); + oled.print(macAddress); + } + else if (iconsRadio & ICON_DOWN_ARROW_CENTER) + displayBitmap(16, 0, DownloadArrow_Width, DownloadArrow_Height, DownloadArrow); + else if (iconsRadio & ICON_UP_ARROW_CENTER) + displayBitmap(16, 0, UploadArrow_Width, UploadArrow_Height, UploadArrow); + + // Radio third spot + if (iconsRadio & ICON_WIFI_SYMBOL_0_RIGHT) + displayBitmap(28, 0, WiFi_Symbol_Width, WiFi_Symbol_Height, WiFi_Symbol_0); + else if (iconsRadio & ICON_WIFI_SYMBOL_1_RIGHT) + displayBitmap(28, 0, WiFi_Symbol_Width, WiFi_Symbol_Height, WiFi_Symbol_1); + else if (iconsRadio & ICON_WIFI_SYMBOL_2_RIGHT) + displayBitmap(28, 0, WiFi_Symbol_Width, WiFi_Symbol_Height, WiFi_Symbol_2); + else if (iconsRadio & ICON_WIFI_SYMBOL_3_RIGHT) + displayBitmap(28, 0, WiFi_Symbol_Width, WiFi_Symbol_Height, WiFi_Symbol_3); + else if ((iconsRadio & ICON_DYNAMIC_MODEL) && (online.gnss == true)) + paintDynamicModel(); + else if (iconsRadio & ICON_BASE_TEMPORARY) + displayBitmap(28, 0, BaseTemporary_Width, BaseTemporary_Height, BaseTemporary); + else if (iconsRadio & ICON_BASE_FIXED) + displayBitmap(28, 0, BaseFixed_Width, BaseFixed_Height, BaseFixed); // true - blend with other pixels + else if (iconsRadio & ICON_DOWN_ARROW_RIGHT) + displayBitmap(31, 0, DownloadArrow_Width, DownloadArrow_Height, DownloadArrow); + else if (iconsRadio & ICON_UP_ARROW_RIGHT) + displayBitmap(31, 0, UploadArrow_Width, UploadArrow_Height, UploadArrow); + else if (iconsRadio & ICON_BLANK_RIGHT) + { + ; + } + + // Left + center spot + if (iconsRadio & ICON_IP_ADDRESS) + paintIPAddress(); + + // Top right corner + if (icons & ICON_BATTERY) + paintBatteryLevel(); + else if (icons & ICON_ETHERNET) + displayBitmap(45, 0, Ethernet_Icon_Width, Ethernet_Icon_Height, Ethernet_Icon); + + // Center left + if (icons & ICON_CROSS_HAIR) + displayBitmap(0, 18, CrossHair_Width, CrossHair_Height, CrossHair); + else if (icons & ICON_CROSS_HAIR_DUAL) + displayBitmap(0, 18, CrossHairDual_Width, CrossHairDual_Height, CrossHairDual); + else if (icons & ICON_CLOCK) + paintClock(); + + // Center right + if (icons & ICON_HORIZONTAL_ACCURACY) + paintHorizontalAccuracy(); + else if (icons & ICON_CLOCK_ACCURACY) + paintClockAccuracy(); + + // Bottom left corner + if (icons & ICON_SIV_ANTENNA) + displayBitmap(2, 35, SIV_Antenna_Width, SIV_Antenna_Height, SIV_Antenna); + else if (icons & ICON_SIV_ANTENNA_LBAND) + displayBitmap(2, 35, SIV_Antenna_LBand_Width, SIV_Antenna_LBand_Height, SIV_Antenna_LBand); + else if (icons & ICON_ANTENNA_SHORT) + displayBitmap(2, 35, Antenna_Short_Width, Antenna_Short_Height, Antenna_Short); + else if (icons & ICON_ANTENNA_OPEN) + displayBitmap(2, 35, Antenna_Open_Width, Antenna_Open_Height, Antenna_Open); + + // Bottom right corner + if (icons & ICON_LOGGING) + paintLogging(); + else if (icons & ICON_LOGGING_NTP) + paintLoggingNTP(true); // NTP, no pulse + + oled.display(); // Push internal buffer to display + } + } // End display online } void displaySplash() { - if (online.display == true) - { - //Init and display splash - oled.begin(); // Initialize the OLED - oled.clear(PAGE); // Clear the display's internal memory + if (online.display == true) + { + // Display SparkFun Logo for at least 1/10 of a second + unsigned long minSplashFor = 100; + if (productVariant == REFERENCE_STATION) // Reference station starts up very quickly. Keep splash on for longer + minSplashFor = 1000; + while ((millis() - splashStart) < minSplashFor) + delay(10); - oled.setCursor(10, 2); //x, y - oled.setFontType(0); //Set font to smallest - oled.print(F("SparkFun")); + oled.erase(); - oled.setCursor(21, 13); - oled.setFontType(1); - oled.print(F("RTK")); + int yPos = 0; + int fontHeight = 8; - int textX; - int textY; - int textKerning; + printTextCenter("SparkFun", yPos, QW_FONT_5X7, 1, false); // text, y, font type, kerning, inverted - if (productVariant == RTK_SURVEYOR) - { - textX = 2; - textY = 25; - textKerning = 8; - oled.setFontType(1); - printTextwithKerning((char*)"Surveyor", textX, textY, textKerning); + yPos = yPos + fontHeight + 2; + printTextCenter("RTK", yPos, QW_FONT_8X16, 1, false); + + yPos = yPos + fontHeight + 5; + printTextCenter(productDisplayNames[productVariant], yPos, QW_FONT_8X16, 1, false); + + yPos = yPos + fontHeight + 7; + char unitFirmware[50]; + getFirmwareVersion(unitFirmware, sizeof(unitFirmware), false); + printTextCenter(unitFirmware, yPos, QW_FONT_5X7, 1, false); + + oled.display(); + + // Start the timer for the splash screen display + splashStart = millis(); } - else if (productVariant == RTK_EXPRESS) +} + +void displayShutdown() +{ + displayMessage("Shutting Down...", 0); +} + +// Displays a small error message then hard freeze +// Text wraps and is small but legible +void displayError(const char *errorMessage) +{ + if (online.display == true) { - textX = 3; - textY = 25; - textKerning = 9; - oled.setFontType(1); - printTextwithKerning((char*)"Express", textX, textY, textKerning); + oled.erase(); // Clear the display's internal buffer + + oled.setCursor(0, 0); // x, y + oled.setFont(QW_FONT_5X7); // Set font to smallest + oled.print("Error:"); + + oled.setCursor(2, 10); + // oled.setFont(QW_FONT_8X16); + oled.print(errorMessage); + + oled.display(); // Push internal buffer to display + + while (1) + delay(10); // Hard freeze } - else if (productVariant == RTK_FACET) +} + +/* + 111111111122222222223333333333444444444455555555556666 + 0123456789012345678901234567890123456789012345678901234567890123 + .---------------------------------------------------------------- + 0| ***************** + 1| * * + 2| * *** *** *** * + 3| * *** *** *** *** + 4| * *** *** *** * + 5| * *** *** *** * + 6| * *** *** *** * + 7| * *** *** *** * + 8| * *** *** *** *** + 9| * *** *** *** * + 10| * * + 11| ***************** +*/ + +// Print the classic battery icon with levels +void paintBatteryLevel() +{ + if (online.display == true) { - textX = 11; - textY = 25; - textKerning = 9; - oled.setFontType(1); - printTextwithKerning((char*)"Facet", textX, textY, textKerning); + // Current battery charge level + if (battLevel < 25) + displayBitmap(45, 0, Battery_0_Width, Battery_0_Height, Battery_0); + else if (battLevel < 50) + displayBitmap(45, 0, Battery_1_Width, Battery_1_Height, Battery_1); + else if (battLevel < 75) + displayBitmap(45, 0, Battery_2_Width, Battery_2_Height, Battery_2); + else // batt level > 75 + displayBitmap(45, 0, Battery_3_Width, Battery_3_Height, Battery_3); } - - oled.setCursor(20, 41); - oled.setFontType(0); //Set font to smallest - oled.printf("v%d.%d", FIRMWARE_VERSION_MAJOR, FIRMWARE_VERSION_MINOR); - oled.display(); - } } -void displayShutdown() +/* + 111111111122222222223333333333444444444455555555556666 + 0123456789012345678901234567890123456789012345678901234567890123 + .---------------------------------------------------------------- + 0| + 1| + 2| + 3| *** *** *** *** + 4|* * * * * * * * + 5|* * * * * * * * + 6| *** *** *** *** + 7|* * * * * * * * + 8|* * * * * * * * + 9| *** *** *** *** + 10| + 11| + + or + + 111111111122222222223333333333444444444455555555556666 + 0123456789012345678901234567890123456789012345678901234567890123 + .---------------------------------------------------------------- + 0| * + 1| ** + 2| *** + 3| * * ** + 4| ** * ** + 5| ***** + 6| *** + 7| *** + 8| ***** + 9| ** * ** + 10| * * ** + 11| *** + 12| ** + 13| * + + or + + 111111111122222222223333333333444444444455555555556666 + 0123456789012345678901234567890123456789012345678901234567890123 + .---------------------------------------------------------------- + 0| ******* ** + 1| * * ** + 2| * ***** * ** + 3|* * * * ** + 4| * *** * ** + 5| * * ** ** ** + 6| * ****** + 7| *** **** + 8| * ** +*/ + +// Set bits to turn on various icons in the Radio area +// ie: Bluetooth, WiFi, ESP Now, Mode indicators, as well as sub states of each (MAC, Blinking, Arrows, etc), depending +// on connection state This function has all the logic to determine how a shared icon spot should act. ie: if we need an +// up arrow, blink the ESP Now icon, etc. This function merely sets the bits to what should be displayed. The main +// updateDisplay() function pushes bits to screen. +uint32_t setRadioIcons() { - if (online.display == true) - { - //Show shutdown text - oled.clear(PAGE); // Clear the display's internal memory + uint32_t icons = 0; - oled.setCursor(21, 13); - oled.setFontType(1); + if (online.display == true) + { + // There are three spots for icons in the Wireless area, left/center/right + // There are three radios that could be active: Bluetooth (always indicated), WiFi (if enabled), ESP-Now (if + // enabled) Because of lack of space we will indicate the Base/Rover if only two radios or less are active + + // Count the number of radios in use + uint8_t numberOfRadios = 1; // Bluetooth always indicated. TODO don't count if BT radio type is OFF. + if (wifiState > WIFI_STATE_OFF) + numberOfRadios++; + if (espnowState > ESPNOW_OFF) + numberOfRadios++; + + // Bluetooth only + if (numberOfRadios == 1) + { + icons |= setBluetoothIcon_OneRadio(); - int textX = 2; - int textY = 10; - int textKerning = 8; + icons |= setModeIcon(); // Turn on Rover/Base type icons + } - printTextwithKerning((char*)"Shutting", textX, textY, textKerning); + else if (numberOfRadios == 2) + { + icons |= setBluetoothIcon_TwoRadios(); - textX = 4; - textY = 25; - textKerning = 9; - oled.setFontType(1); + // Do we have WiFi or ESP + if (wifiState > WIFI_STATE_OFF) + icons |= setWiFiIcon_TwoRadios(); + else if (espnowState > ESPNOW_OFF) + icons |= setESPNowIcon_TwoRadios(); - printTextwithKerning((char*)"Down...", textX, textY, textKerning); + icons |= setModeIcon(); // Turn on Rover/Base type icons + } + + else if (numberOfRadios == 3) + { + // Bluetooth is center + icons |= setBluetoothIcon_TwoRadios(); - oled.display(); - } + // ESP Now is left + icons |= setESPNowIcon_TwoRadios(); + + // WiFi is right + icons |= setWiFiIcon_ThreeRadios(); + + // No Rover/Base icons + } + } + + return icons; } -//Displays a small error message then hard freeze -//Text wraps and is small but legible -void displayError(char * errorMessage) +// Bluetooth is in left position +// Set Bluetooth icons (MAC, Connected, arrows) in left position +uint32_t setBluetoothIcon_OneRadio() { - if (online.display == true) - { - oled.clear(PAGE); // Clear the display's internal buffer + uint32_t icons = 0; - oled.setCursor(0, 0); //x, y - oled.setFontType(0); //Set font to smallest - oled.print(F("Error:")); + if (bluetoothGetState() != BT_CONNECTED) + icons |= ICON_MAC_ADDRESS; + else if (bluetoothGetState() == BT_CONNECTED) + { + // Limit how often we update this spot + if (millis() - firstRadioSpotTimer > 2000) + { + firstRadioSpotTimer = millis(); - oled.setCursor(2, 10); - //oled.setFontType(1); - oled.print(errorMessage); + if (bluetoothIncomingRTCM == true || bluetoothOutgoingRTCM == true) + firstRadioSpotBlink ^= 1; // Share the spot + else + firstRadioSpotBlink = false; + } - oled.display(); //Push internal buffer to display + if (firstRadioSpotBlink == false) + icons |= ICON_BT_SYMBOL_LEFT; + else + { + // Share the spot. Determine if we need to indicate Up, or Down + if (bluetoothIncomingRTCM == true) + { + icons |= ICON_DOWN_ARROW_LEFT; + bluetoothIncomingRTCM = false; // Reset, set during UART RX task. + } + else if (bluetoothOutgoingRTCM == true) + { + icons |= ICON_UP_ARROW_LEFT; + bluetoothOutgoingRTCM = false; // Reset, set during UART BT send bytes task. + } + else + icons |= ICON_BT_SYMBOL_LEFT; + } + } - while (1) delay(10); //Hard freeze - } + return icons; } -//Print the classic battery icon with levels -void paintBatteryLevel() +// Bluetooth is in center position +// Set Bluetooth icons (MAC, Connected, arrows) in left position +uint32_t setBluetoothIcon_TwoRadios() +{ + uint32_t icons = 0; + + if (bluetoothGetState() != BT_CONNECTED) + icons |= ICON_MAC_ADDRESS_2DIGIT; + else if (bluetoothGetState() == BT_CONNECTED) + { + // Limit how often we update this spot + if (millis() - secondRadioSpotTimer > 2000) + { + secondRadioSpotTimer = millis(); + + if (bluetoothIncomingRTCM == true || bluetoothOutgoingRTCM == true) + secondRadioSpotBlink ^= 1; // Share the spot + else + secondRadioSpotBlink = false; + } + + if (secondRadioSpotBlink == false) + icons |= ICON_BT_SYMBOL_CENTER; + else + { + // Share the spot. Determine if we need to indicate Up, or Down + if (bluetoothIncomingRTCM == true) + { + icons |= ICON_DOWN_ARROW_CENTER; + bluetoothIncomingRTCM = false; // Reset, set during UART RX task. + } + else if (bluetoothOutgoingRTCM == true) + { + icons |= ICON_UP_ARROW_CENTER; + bluetoothOutgoingRTCM = false; // Reset, set during UART BT send bytes task. + } + else + icons |= ICON_BT_SYMBOL_CENTER; + } + } + + return icons; +} + +// Bluetooth is in center position +// Set ESP Now icon (Solid, arrows, blinking) in left position +uint32_t setESPNowIcon_TwoRadios() { - if (online.display == true) - { - //Current battery charge level - if (battLevel < 25) - oled.drawIcon(45, 0, Battery_0_Width, Battery_0_Height, Battery_0, sizeof(Battery_0), true); - else if (battLevel < 50) - oled.drawIcon(45, 0, Battery_1_Width, Battery_1_Height, Battery_1, sizeof(Battery_1), true); - else if (battLevel < 75) - oled.drawIcon(45, 0, Battery_2_Width, Battery_2_Height, Battery_2, sizeof(Battery_2), true); - else //batt level > 75 - oled.drawIcon(45, 0, Battery_3_Width, Battery_3_Height, Battery_3, sizeof(Battery_3), true); - } -} - -//Display Bluetooth icon, Bluetooth MAC, or WiFi depending on connection state -void paintWirelessIcon() -{ - if (online.display == true) - { - //Bluetooth icon if paired, or Bluetooth MAC address if not paired - if (radioState == BT_CONNECTED) - { - oled.drawIcon(4, 0, BT_Symbol_Width, BT_Symbol_Height, BT_Symbol, sizeof(BT_Symbol), true); - } - else if (radioState == WIFI_ON_NOCONNECTION) - { - //Blink WiFi icon - if (millis() - lastWifiIconUpdate > 500) - { - lastWifiIconUpdate = millis(); - if (wifiIconDisplayed == false) + uint32_t icons = 0; + + if (espnowState == ESPNOW_PAIRED) + { + // Limit how often we update this spot + if (millis() - firstRadioSpotTimer > 2000) { - wifiIconDisplayed = true; + firstRadioSpotTimer = millis(); - //Draw the icon - oled.drawIcon(6, 1, WiFi_Symbol_Width, WiFi_Symbol_Height, WiFi_Symbol, sizeof(WiFi_Symbol), true); + if (espnowIncomingRTCM == true || espnowOutgoingRTCM == true) + firstRadioSpotBlink ^= 1; // Share the spot + else + firstRadioSpotBlink = false; + } + + if (firstRadioSpotBlink == false) + { + if (espnowIncomingRTCM == true) + { + // Based on RSSI, select icon + if (espnowRSSI >= -40) + icons |= ICON_ESPNOW_SYMBOL_3_LEFT; + else if (espnowRSSI >= -60) + icons |= ICON_ESPNOW_SYMBOL_2_LEFT; + else if (espnowRSSI >= -80) + icons |= ICON_ESPNOW_SYMBOL_1_LEFT; + else if (espnowRSSI > -255) + icons |= ICON_ESPNOW_SYMBOL_0_LEFT; + } + else // ESP radio is active, but not receiving RTCM + { + icons |= ICON_ESPNOW_SYMBOL_3_LEFT; // Full symbol + } } else - wifiIconDisplayed = false; - } + { + // Share the spot. Determine if we need to indicate Up, or Down + if (espnowIncomingRTCM == true) + { + icons |= ICON_DOWN_ARROW_LEFT; + espnowIncomingRTCM = false; // Reset, set during ESP Now data received call back + } + else if (espnowOutgoingRTCM == true) + { + icons |= ICON_UP_ARROW_LEFT; + espnowOutgoingRTCM = false; // Reset, set during espnowProcessRTCM() + } + else + { + icons |= ICON_ESPNOW_SYMBOL_3_LEFT; // Full symbol + + // TODO catch RSSI here + } + } } - else if (radioState == WIFI_CONNECTED) + + else // We are not paired, blink icon { - //Solid WiFi icon - oled.drawIcon(6, 1, WiFi_Symbol_Width, WiFi_Symbol_Height, WiFi_Symbol, sizeof(WiFi_Symbol), true); + // Limit how often we update this spot + if (millis() - firstRadioSpotTimer > 2000) + { + firstRadioSpotTimer = millis(); + firstRadioSpotBlink ^= 1; // Share the spot + } + + if (firstRadioSpotBlink == false) + icons |= ICON_ESPNOW_SYMBOL_3_LEFT; // Full symbol + else + icons |= ICON_BLANK_LEFT; } - else + + return icons; +} + +// Bluetooth is in center position +// Set WiFi icon (Solid, arrows, blinking) in left position +uint32_t setWiFiIcon_TwoRadios() +{ + uint32_t icons = 0; + + if (wifiState == WIFI_STATE_CONNECTED) { - char macAddress[5]; - sprintf(macAddress, "%02X%02X", unitMACAddress[4], unitMACAddress[5]); - oled.setFontType(0); //Set font to smallest - oled.setCursor(0, 4); - oled.print(macAddress); + // Limit how often we update this spot + if (millis() - firstRadioSpotTimer > 2000) + { + firstRadioSpotTimer = millis(); + + if (netIncomingRTCM == true || netOutgoingRTCM == true) + firstRadioSpotBlink ^= 1; // Share the spot + else + firstRadioSpotBlink = false; + } + + if (firstRadioSpotBlink == false) + { +#ifdef COMPILE_WIFI + int wifiRSSI = WiFi.RSSI(); +#else // COMPILE_WIFI + int wifiRSSI = -40; // Dummy +#endif // COMPILE_WIFI + // Based on RSSI, select icon + if (wifiRSSI >= -40) + icons |= ICON_WIFI_SYMBOL_3_LEFT; + else if (wifiRSSI >= -60) + icons |= ICON_WIFI_SYMBOL_2_LEFT; + else if (wifiRSSI >= -80) + icons |= ICON_WIFI_SYMBOL_1_LEFT; + else + icons |= ICON_WIFI_SYMBOL_0_LEFT; + } + else + { + // Share the spot. Determine if we need to indicate Up, or Down + if (netIncomingRTCM == true) + { + icons |= ICON_DOWN_ARROW_LEFT; + netIncomingRTCM = false; // Reset, set during NTRIP Client + } + else if (netOutgoingRTCM == true) + { + icons |= ICON_UP_ARROW_LEFT; + netOutgoingRTCM = false; // Reset, set during NTRIP Server + } + else + { +#ifdef COMPILE_WIFI + int wifiRSSI = WiFi.RSSI(); +#else // COMPILE_WIFI + int wifiRSSI = -40; // Dummy +#endif // COMPILE_WIFI + // Based on RSSI, select icon + if (wifiRSSI >= -40) + icons |= ICON_WIFI_SYMBOL_3_LEFT; + else if (wifiRSSI >= -60) + icons |= ICON_WIFI_SYMBOL_2_LEFT; + else if (wifiRSSI >= -80) + icons |= ICON_WIFI_SYMBOL_1_LEFT; + else + icons |= ICON_WIFI_SYMBOL_0_LEFT; + } + } + } + + else // We are not paired, blink icon + { + // Limit how often we update this spot + if (millis() - firstRadioSpotTimer > 2000) + { + firstRadioSpotTimer = millis(); + firstRadioSpotBlink ^= 1; // Share the spot + } + + if (firstRadioSpotBlink == false) + icons |= ICON_WIFI_SYMBOL_3_LEFT; // Full symbol + else + icons |= ICON_BLANK_LEFT; } - } + + return (icons); } -//Display cross hairs and horizontal accuracy -//Display double circle if we have RTK (blink = float, solid = fix) -void paintHorizontalAccuracy() +// Bluetooth is in center position +// Set WiFi icon (Solid, arrows, blinking) in right position +uint32_t setWiFiIcon_ThreeRadios() { - if (online.display == true) - { - //Blink crosshair icon until we achieve <5m horz accuracy (user definable) - if (systemState == STATE_BASE_TEMP_SETTLE) + uint32_t icons = 0; + + if (wifiState == WIFI_STATE_CONNECTED) { - if (millis() - lastCrosshairIconUpdate > 500) - { - lastCrosshairIconUpdate = millis(); - if (crosshairIconDisplayed == false) + // Limit how often we update this spot + if (millis() - thirdRadioSpotTimer > 2000) { - crosshairIconDisplayed = true; + thirdRadioSpotTimer = millis(); - //Draw the icon - oled.drawIcon(0, 18, CrossHair_Width, CrossHair_Height, CrossHair, sizeof(CrossHair), true); + if (netIncomingRTCM == true || netOutgoingRTCM == true) + thirdRadioSpotBlink ^= 1; // Share the spot + else + thirdRadioSpotBlink = false; + } + + if (thirdRadioSpotBlink == false) + { +#ifdef COMPILE_WIFI + int wifiRSSI = WiFi.RSSI(); +#else // COMPILE_WIFI + int wifiRSSI = -40; // Dummy +#endif // COMPILE_WIFI + // Based on RSSI, select icon + if (wifiRSSI >= -40) + icons |= ICON_WIFI_SYMBOL_3_RIGHT; + else if (wifiRSSI >= -60) + icons |= ICON_WIFI_SYMBOL_2_RIGHT; + else if (wifiRSSI >= -80) + icons |= ICON_WIFI_SYMBOL_1_RIGHT; + else + icons |= ICON_WIFI_SYMBOL_0_RIGHT; } else - crosshairIconDisplayed = false; - } + { + // Share the spot. Determine if we need to indicate Up, or Down + if (netIncomingRTCM == true) + { + icons |= ICON_DOWN_ARROW_RIGHT; + netIncomingRTCM = false; // Reset, set during NTRIP Client + } + else if (netOutgoingRTCM == true) + { + icons |= ICON_UP_ARROW_RIGHT; + netOutgoingRTCM = false; // Reset, set during NTRIP Server + } + else + { +#ifdef COMPILE_WIFI + int wifiRSSI = WiFi.RSSI(); +#else // COMPILE_WIFI + int wifiRSSI = -40; // Dummy +#endif // COMPILE_WIFI + // Based on RSSI, select icon + if (wifiRSSI >= -40) + icons |= ICON_WIFI_SYMBOL_3_RIGHT; + else if (wifiRSSI >= -60) + icons |= ICON_WIFI_SYMBOL_2_RIGHT; + else if (wifiRSSI >= -80) + icons |= ICON_WIFI_SYMBOL_1_RIGHT; + else + icons |= ICON_WIFI_SYMBOL_0_RIGHT; + } + } } - else if (systemState == STATE_ROVER_RTK_FLOAT) + + else // We are not paired, blink icon { - if (millis() - lastCrosshairIconUpdate > 500) - { - lastCrosshairIconUpdate = millis(); - if (crosshairIconDisplayed == false) + // Limit how often we update this spot + if (millis() - thirdRadioSpotTimer > 2000) { - crosshairIconDisplayed = true; + thirdRadioSpotTimer = millis(); + thirdRadioSpotBlink ^= 1; // Share the spot + } + + if (thirdRadioSpotBlink == false) + icons |= ICON_WIFI_SYMBOL_3_RIGHT; // Full symbol + else + icons |= ICON_BLANK_RIGHT; + } + + return (icons); +} + +// Bluetooth and ESP Now icons off. WiFi in middle. +// Blink while no clients are connected +uint32_t setWiFiIcon() +{ + uint32_t icons = 0; - //Draw dual crosshair - oled.drawIcon(0, 18, CrossHairDual_Width, CrossHairDual_Height, CrossHairDual, sizeof(CrossHairDual), true); + if (online.display == true) + { + if (wifiState == WIFI_STATE_CONNECTED) + { + icons |= ICON_WIFI_SYMBOL_3_RIGHT; } else - crosshairIconDisplayed = false; - } + { + // Limit how often we update this spot + if (millis() - thirdRadioSpotTimer > 1000) + { + thirdRadioSpotTimer = millis(); + thirdRadioSpotBlink ^= 1; // Blink this icon + } + + if (thirdRadioSpotBlink == false) + icons |= ICON_BLANK_RIGHT; + else + icons |= ICON_WIFI_SYMBOL_3_RIGHT; + } } - else if (systemState == STATE_ROVER_RTK_FIX) + + return (icons); +} + +// Based on system state, turn on the various Rover, Base, Fixed Base icons +uint32_t setModeIcon() +{ + uint32_t icons = 0; + + switch (systemState) { - //Draw dual crosshair - oled.drawIcon(0, 18, CrossHairDual_Width, CrossHairDual_Height, CrossHairDual, sizeof(CrossHairDual), true); + case (STATE_ROVER_NOT_STARTED): + break; + case (STATE_ROVER_NO_FIX): + icons |= ICON_DYNAMIC_MODEL; + break; + case (STATE_ROVER_FIX): + icons |= ICON_DYNAMIC_MODEL; + break; + case (STATE_ROVER_RTK_FLOAT): + icons |= ICON_DYNAMIC_MODEL; + break; + case (STATE_ROVER_RTK_FIX): + icons |= ICON_DYNAMIC_MODEL; + break; + + case (STATE_BASE_NOT_STARTED): + // Do nothing. Static display shown during state change. + break; + case (STATE_BASE_TEMP_SETTLE): + icons |= blinkBaseIcon(ICON_BASE_TEMPORARY); + break; + case (STATE_BASE_TEMP_SURVEY_STARTED): + icons |= blinkBaseIcon(ICON_BASE_TEMPORARY); + break; + case (STATE_BASE_TEMP_TRANSMITTING): + icons |= ICON_BASE_TEMPORARY; + break; + case (STATE_BASE_FIXED_NOT_STARTED): + // Do nothing. Static display shown during state change. + break; + case (STATE_BASE_FIXED_TRANSMITTING): + icons |= ICON_BASE_FIXED; + break; + + case (STATE_NTPSERVER_NOT_STARTED): + case (STATE_NTPSERVER_NO_SYNC): + case (STATE_NTPSERVER_SYNC): + break; + + default: + break; } - else + return (icons); +} + +uint32_t blinkBaseIcon(uint32_t iconType) +{ + uint32_t icons = 0; + + // Limit how often we update this spot + if (millis() - thirdRadioSpotTimer > 1000) { - //Draw crosshair - oled.drawIcon(0, 18, CrossHair_Width, CrossHair_Height, CrossHair, sizeof(CrossHair), true); + thirdRadioSpotTimer = millis(); + thirdRadioSpotBlink ^= 1; // Share the spot } - oled.setFontType(1); //Set font to type 1: 8x16 - oled.setCursor(16, 20); //x, y + if (thirdRadioSpotBlink == false) + icons |= iconType; + else + icons |= ICON_BLANK_RIGHT; + + return icons; +} + +/* + 111111111122222222223333333333444444444455555555556666 + 0123456789012345678901234567890123456789012345678901234567890123 + .---------------------------------------------------------------- + 17| + 18| + 19| + 20| + 21| *** *** *** + 22| * * * * * * + 23| * * * * * * + 24| ** * * * * * * + 25| ** * * * + 26| * * * * * * + 27| * * * * * * + 28| * * * * * * + 29| ** * * ** * * * * + 30| ** *** ** *** *** + 31| + 32| +*/ + +// Display horizontal accuracy +void paintHorizontalAccuracy() +{ + oled.setFont(QW_FONT_8X16); // Set font to type 1: 8x16 + oled.setCursor(16, 20); // x, y oled.print(":"); - float hpa = i2cGNSS.getHorizontalAccuracy() / 10000.0; - if (hpa > 30.0) + + if (online.gnss == false) { - oled.print(F(">30m")); + oled.print("N/A"); } - else if (hpa > 9.9) + else if (horizontalAccuracy > 30.0) { - oled.print(hpa, 1); //Print down to decimeter + oled.print(">30m"); } - else if (hpa > 1.0) + else if (horizontalAccuracy > 9.9) { - oled.print(hpa, 2); //Print down to centimeter + oled.print(horizontalAccuracy, 1); // Print down to decimeter + } + else if (horizontalAccuracy > 1.0) + { + oled.print(horizontalAccuracy, 2); // Print down to centimeter } else { - oled.print("."); //Remove leading zero - oled.printf("%03d", (int)(hpa * 1000)); //Print down to millimeter + oled.print("."); // Remove leading zero + oled.printf("%03d", (int)(horizontalAccuracy * 1000)); // Print down to millimeter } - } } -//Draw either a rover or base icon depending on screen -//Draw a different base if we have fixed coordinate base type -void paintBaseState() +// Display clock with moving hands +void paintClock() +{ + // Animate icon to show system running + static uint8_t clockIconDisplayed = 3; + clockIconDisplayed++; // Goto next icon + clockIconDisplayed %= 4; // Wrap + + if (clockIconDisplayed == 0) + displayBitmap(0, 18, Clock_Icon_Width, Clock_Icon_Height, Clock_Icon_1); + else if (clockIconDisplayed == 1) + displayBitmap(0, 18, Clock_Icon_Width, Clock_Icon_Height, Clock_Icon_2); + else if (clockIconDisplayed == 2) + displayBitmap(0, 18, Clock_Icon_Width, Clock_Icon_Height, Clock_Icon_3); + else + displayBitmap(0, 18, Clock_Icon_Width, Clock_Icon_Height, Clock_Icon_4); +} + +// Display clock accuracy tAcc +void paintClockAccuracy() { - if (online.display == true) - { - if (systemState == STATE_ROVER_NO_FIX || - systemState == STATE_ROVER_FIX || - systemState == STATE_ROVER_RTK_FLOAT || - systemState == STATE_ROVER_RTK_FIX) + oled.setFont(QW_FONT_8X16); // Set font to type 1: 8x16 + oled.setCursor(16, 20); // x, y + oled.print(":"); + + if (online.gnss == false) + { + oled.print(" N/A"); + } + else if (tAcc < 10) // 9 or less : show as 9ns + { + oled.print(tAcc); + displayBitmap(36, 20, Millis_Icon_Width, Millis_Icon_Height, Nanos_Icon); + } + else if (tAcc < 100) // 99 or less : show as 99ns + { + oled.print(tAcc); + displayBitmap(44, 20, Millis_Icon_Width, Millis_Icon_Height, Nanos_Icon); + } + else if (tAcc < 10000) // 9999 or less : show as 9.9μs + { + oled.print(tAcc / 1000); + oled.print("."); + oled.print((tAcc / 100) % 10); + displayBitmap(52, 20, Millis_Icon_Width, Millis_Icon_Height, Micros_Icon); + } + else if (tAcc < 100000) // 99999 or less : show as 99μs { - oled.drawIcon(27, 3, Rover_Width, Rover_Height, Rover, sizeof(Rover), true); + oled.print(tAcc / 1000); + displayBitmap(44, 20, Millis_Icon_Width, Millis_Icon_Height, Micros_Icon); } - else if (systemState == STATE_BASE_TEMP_SETTLE || - systemState == STATE_BASE_TEMP_SURVEY_STARTED //Turn on base icon solid (blink crosshair in paintHorzAcc) - ) + else if (tAcc < 10000000) // 9999999 or less : show as 9.9ms { - //Blink base icon until survey is complete - if (millis() - lastBaseIconUpdate > 500) - { - lastBaseIconUpdate = millis(); - if (baseIconDisplayed == false) + oled.print(tAcc / 1000000); + oled.print("."); + oled.print((tAcc / 100000) % 10); + displayBitmap(52, 20, Millis_Icon_Width, Millis_Icon_Height, Millis_Icon); + } + else // if (tAcc >= 100000) + { + oled.print(">10"); + displayBitmap(52, 20, Millis_Icon_Width, Millis_Icon_Height, Millis_Icon); + } +} + +/* + 111111111122222222223333333333444444444455555555556666 + 0123456789012345678901234567890123456789012345678901234567890123 + .---------------------------------------------------------------- + 0| ** + 1| ** + 2| ****** + 3| * * + 4| * * **** * * + 5| * * **** * * + 6| * * * * + 7| * * * * + 8| * * * * + 9| * * * * + 10| * * + 11| ****** + 12| +*/ + +// Draw the rover icon depending on screen +void paintDynamicModel() +{ + // Display icon associated with current Dynamic Model + switch (settings.dynamicModel) + { + case (DYN_MODEL_PORTABLE): + displayBitmap(28, 0, DynamicModel_Width, DynamicModel_Height, DynamicModel_1_Portable); + break; + case (DYN_MODEL_STATIONARY): + displayBitmap(28, 0, DynamicModel_Width, DynamicModel_Height, DynamicModel_2_Stationary); + break; + case (DYN_MODEL_PEDESTRIAN): + displayBitmap(28, 0, DynamicModel_Width, DynamicModel_Height, DynamicModel_3_Pedestrian); + break; + case (DYN_MODEL_AUTOMOTIVE): + // Normal rover for ZED-F9P, fusion rover for ZED-F9R + if (zedModuleType == PLATFORM_F9P) + { + displayBitmap(28, 0, DynamicModel_Width, DynamicModel_Height, DynamicModel_4_Automotive); + } + else if (zedModuleType == PLATFORM_F9R) { - baseIconDisplayed = true; + // Blink fusion rover until we have calibration + if (theGNSS.packetUBXESFSTATUS->data.fusionMode == 0) // Initializing + { + // Blink Fusion Rover icon until sensor calibration is complete + if (millis() - lastBaseIconUpdate > 500) + { + lastBaseIconUpdate = millis(); + if (baseIconDisplayed == false) + { + baseIconDisplayed = true; + + // Draw the icon + displayBitmap(28, 2, Rover_Fusion_Width, Rover_Fusion_Height, Rover_Fusion); + } + else + baseIconDisplayed = false; + } + } + else if (theGNSS.packetUBXESFSTATUS->data.fusionMode == 1) // Calibrated + { + // Solid fusion rover + displayBitmap(28, 2, Rover_Fusion_Width, Rover_Fusion_Height, Rover_Fusion); + } + else if (theGNSS.packetUBXESFSTATUS->data.fusionMode == 2 || + theGNSS.packetUBXESFSTATUS->data.fusionMode == 3) // Suspended or disabled + { + // Empty rover + displayBitmap(28, 2, Rover_Fusion_Empty_Width, Rover_Fusion_Empty_Height, Rover_Fusion_Empty); + } + } + break; + case (DYN_MODEL_SEA): + displayBitmap(28, 0, DynamicModel_Width, DynamicModel_Height, DynamicModel_5_Sea); + break; + case (DYN_MODEL_AIRBORNE1g): + displayBitmap(28, 0, DynamicModel_Width, DynamicModel_Height, DynamicModel_6_Airborne1g); + break; + case (DYN_MODEL_AIRBORNE2g): + displayBitmap(28, 0, DynamicModel_Width, DynamicModel_Height, DynamicModel_7_Airborne2g); + break; + case (DYN_MODEL_AIRBORNE4g): + displayBitmap(28, 0, DynamicModel_Width, DynamicModel_Height, DynamicModel_8_Airborne4g); + break; + case (DYN_MODEL_WRIST): + displayBitmap(28, 0, DynamicModel_Width, DynamicModel_Height, DynamicModel_9_Wrist); + break; + case (DYN_MODEL_BIKE): + displayBitmap(28, 0, DynamicModel_Width, DynamicModel_Height, DynamicModel_10_Bike); + break; + case (DYN_MODEL_MOWER): + displayBitmap(28, 0, DynamicModel_Width, DynamicModel_Height, DynamicModel_11_Mower); + break; + case (DYN_MODEL_ESCOOTER): + displayBitmap(28, 0, DynamicModel_Width, DynamicModel_Height, DynamicModel_12_EScooter); + break; + } +} + +/* + 111111111122222222223333333333444444444455555555556666 + 0123456789012345678901234567890123456789012345678901234567890123 + .---------------------------------------------------------------- + 35| + 36| ** + 37| * * *** *** + 38| * * * * * * * + 39| * * * * * * * + 40| * * ** * * * * + 41| * * ** * * + 42| * * * * * * + 43| ** * * * * * + 44| **** * * * * * + 45| ** **** ** * * * * + 46| ** ** *** *** + 47| ****** +*/ + +// Select satellite icon and draw sats in view +// Blink icon if no fix +uint32_t paintSIV() +{ + uint32_t blinking; + uint32_t icons; + + oled.setFont(QW_FONT_8X16); // Set font to type 1: 8x16 + oled.setCursor(16, 36); // x, y + oled.print(":"); + + if (online.gnss) + { + if (fixType == 0) // 0 = No Fix + oled.print("0"); + else + oled.print(numSV); + + paintResets(); + + // Determine which icon to display + icons = 0; + if (lbandCorrectionsReceived) + blinking = ICON_SIV_ANTENNA_LBAND; + else + blinking = ICON_SIV_ANTENNA; - //Draw the icon - oled.drawIcon(27, 0, BaseTemporary_Width, BaseTemporary_Height, BaseTemporary, sizeof(BaseTemporary), true); //true - blend with other pixels + // Determine if there is a fix + if (fixType == 3 || fixType == 4 || fixType == 5) // 3D, 3D+DR, or Time + { + // Fix, turn on icon + icons = blinking; } else - baseIconDisplayed = false; - } + { + // Blink satellite dish icon if we don't have a fix + blinking_icons ^= blinking; + if (blinking_icons & blinking) + icons = blinking; + } + } // End gnss online + else + { + oled.print("X"); + + icons = ICON_SIV_ANTENNA; + } + return icons; +} + +/* + 111111111122222222223333333333444444444455555555556666 + 0123456789012345678901234567890123456789012345678901234567890123 + .---------------------------------------------------------------- + 35| + 36| ******* + 37| * ** + 38| * ** + 39| * * + 40| * ***** * + 41| * * + 42| * ***** * + 43| * * + 44| * ***** * + 45| * * + 46| * * + 47| ********* +*/ + +// Draw log icon +// Turn off icon if log file fails to get bigger +void paintLogging() +{ + // Animate icon to show system running + loggingIconDisplayed++; // Goto next icon + loggingIconDisplayed %= 4; // Wrap +#ifdef COMPILE_ETHERNET + if ((online.logging == true) && (logIncreasing || ntpLogIncreasing)) +#else // COMPILE_ETHERNET + if ((online.logging == true) && (logIncreasing)) +#endif // COMPILE_ETHERNET + { + if (loggingType == LOGGING_STANDARD) + { + if (loggingIconDisplayed == 0) + displayBitmap(64 - Logging_0_Width, 48 - Logging_0_Height, Logging_0_Width, Logging_0_Height, + Logging_0); + else if (loggingIconDisplayed == 1) + displayBitmap(64 - Logging_1_Width, 48 - Logging_1_Height, Logging_1_Width, Logging_1_Height, + Logging_1); + else if (loggingIconDisplayed == 2) + displayBitmap(64 - Logging_2_Width, 48 - Logging_2_Height, Logging_2_Width, Logging_2_Height, + Logging_2); + else if (loggingIconDisplayed == 3) + displayBitmap(64 - Logging_3_Width, 48 - Logging_3_Height, Logging_3_Width, Logging_3_Height, + Logging_3); + } + else if (loggingType == LOGGING_PPP) + { + if (loggingIconDisplayed == 0) + displayBitmap(64 - Logging_0_Width, 48 - Logging_0_Height, Logging_0_Width, Logging_0_Height, + Logging_0); + else if (loggingIconDisplayed == 1) + displayBitmap(64 - Logging_1_Width, 48 - Logging_1_Height, Logging_1_Width, Logging_1_Height, + Logging_PPP_1); + else if (loggingIconDisplayed == 2) + displayBitmap(64 - Logging_2_Width, 48 - Logging_2_Height, Logging_2_Width, Logging_2_Height, + Logging_PPP_2); + else if (loggingIconDisplayed == 3) + displayBitmap(64 - Logging_3_Width, 48 - Logging_3_Height, Logging_3_Width, Logging_3_Height, + Logging_PPP_3); + } + else if (loggingType == LOGGING_CUSTOM) + { + if (loggingIconDisplayed == 0) + displayBitmap(64 - Logging_0_Width, 48 - Logging_0_Height, Logging_0_Width, Logging_0_Height, + Logging_0); + else if (loggingIconDisplayed == 1) + displayBitmap(64 - Logging_1_Width, 48 - Logging_1_Height, Logging_1_Width, Logging_1_Height, + Logging_Custom_1); + else if (loggingIconDisplayed == 2) + displayBitmap(64 - Logging_2_Width, 48 - Logging_2_Height, Logging_2_Width, Logging_2_Height, + Logging_Custom_2); + else if (loggingIconDisplayed == 3) + displayBitmap(64 - Logging_3_Width, 48 - Logging_3_Height, Logging_3_Width, Logging_3_Height, + Logging_Custom_3); + } } - else if (systemState == STATE_BASE_TEMP_TRANSMITTING || - systemState == STATE_BASE_TEMP_WIFI_STARTED || - systemState == STATE_BASE_TEMP_WIFI_CONNECTED || - systemState == STATE_BASE_TEMP_CASTER_STARTED || - systemState == STATE_BASE_TEMP_CASTER_CONNECTED) + else { - //Draw the icon - oled.drawIcon(27, 0, BaseTemporary_Width, BaseTemporary_Height, BaseTemporary, sizeof(BaseTemporary), true); //true - blend with other pixels + const int pulseX = 64 - 4; + const int pulseY = oled.getHeight(); + int height; + + // Paint pulse to show system activity + height = loggingIconDisplayed << 2; + if (height) + { + oled.line(pulseX, pulseY, pulseX, pulseY - height); + oled.line(pulseX - 1, pulseY, pulseX - 1, pulseY - height); + } } - else if (systemState == STATE_BASE_FIXED_TRANSMITTING || - systemState == STATE_BASE_FIXED_WIFI_STARTED || - systemState == STATE_BASE_FIXED_WIFI_CONNECTED || - systemState == STATE_BASE_FIXED_CASTER_STARTED || - systemState == STATE_BASE_FIXED_CASTER_CONNECTED) +} + +void paintLoggingNTP(bool noPulse) +{ + // Animate icon to show system running + loggingIconDisplayed++; // Goto next icon + loggingIconDisplayed %= 4; // Wrap +#ifdef COMPILE_ETHERNET // Some redundancy here. paintLoggingNTP should only be called if Ethernet is present + if ((online.logging == true) && (logIncreasing || ntpLogIncreasing)) +#else // COMPILE_ETHERNET + if ((online.logging == true) && (logIncreasing)) +#endif // COMPILE_ETHERNET { - //Draw the icon - oled.drawIcon(27, 0, BaseFixed_Width, BaseFixed_Height, BaseFixed, sizeof(BaseFixed), true); //true - blend with other pixels + if (loggingIconDisplayed == 0) + displayBitmap(64 - Logging_0_Width, 48 - Logging_0_Height, Logging_0_Width, Logging_0_Height, Logging_0); + else if (loggingIconDisplayed == 1) + displayBitmap(64 - Logging_1_Width, 48 - Logging_1_Height, Logging_1_Width, Logging_1_Height, + Logging_NTP_1); + else if (loggingIconDisplayed == 2) + displayBitmap(64 - Logging_2_Width, 48 - Logging_2_Height, Logging_2_Width, Logging_2_Height, + Logging_NTP_2); + else if (loggingIconDisplayed == 3) + displayBitmap(64 - Logging_3_Width, 48 - Logging_3_Height, Logging_3_Width, Logging_3_Height, + Logging_NTP_3); + } + else if (!noPulse) + { + const int pulseX = 64 - 4; + const int pulseY = oled.getHeight(); + int height; + + // Paint pulse to show system activity + height = loggingIconDisplayed << 2; + if (height) + { + oled.line(pulseX, pulseY, pulseX, pulseY - height); + oled.line(pulseX - 1, pulseY, pulseX - 1, pulseY - height); + } } - } } -//Draw satellite icon and sats in view -//Blink icon if no fix -void paintSIV() +// Survey in is running. Show 3D Mean and elapsed time. +void paintBaseTempSurveyStarted() { - if (online.display == true) - { - //Blink satellite dish icon if we don't have a fix - if (i2cGNSS.getFixType() == 3 || i2cGNSS.getFixType() == 5) //3D or Time + oled.setFont(QW_FONT_5X7); + oled.setCursor(0, 23); // x, y + oled.print("Mean:"); + + oled.setCursor(29, 20); // x, y + oled.setFont(QW_FONT_8X16); + if (svinMeanAccuracy < 10.0) // Error check + oled.print(svinMeanAccuracy, 2); + else + oled.print(">10"); + + if (!HAS_ANTENNA_SHORT_OPEN) { - //Fix, turn on icon - oled.drawIcon(2, 35, Antenna_Width, Antenna_Height, Antenna, sizeof(Antenna), true); + oled.setCursor(0, 39); // x, y + oled.setFont(QW_FONT_5X7); + oled.print("Time:"); } else { - if (millis() - lastSatelliteDishIconUpdate > 500) - { - //Serial.println("SIV Blink"); - lastSatelliteDishIconUpdate = millis(); - if (satelliteDishIconDisplayed == false) + static uint32_t blinkers = 0; + if (aStatus == SFE_UBLOX_ANTENNA_STATUS_SHORT) { - satelliteDishIconDisplayed = true; - - //Draw the icon - oled.drawIcon(2, 35, Antenna_Width, Antenna_Height, Antenna, sizeof(Antenna), true); + blinkers ^= ICON_ANTENNA_SHORT; + if (blinkers & ICON_ANTENNA_SHORT) + displayBitmap(2, 35, Antenna_Short_Width, Antenna_Short_Height, Antenna_Short); + } + else if (aStatus == SFE_UBLOX_ANTENNA_STATUS_OPEN) + { + blinkers ^= ICON_ANTENNA_OPEN; + if (blinkers & ICON_ANTENNA_OPEN) + displayBitmap(2, 35, Antenna_Open_Width, Antenna_Open_Height, Antenna_Open); } else - satelliteDishIconDisplayed = false; - } + { + blinkers &= ~ICON_ANTENNA_SHORT; + blinkers &= ~ICON_ANTENNA_OPEN; + oled.setCursor(0, 39); // x, y + oled.setFont(QW_FONT_5X7); + oled.print("Time:"); + } } - oled.setFontType(1); //Set font to type 1: 8x16 - oled.setCursor(16, 36); //x, y - oled.print(":"); + oled.setCursor(30, 36); // x, y + oled.setFont(QW_FONT_8X16); + if (svinObservationTime < 1000) // Error check + oled.print(svinObservationTime); + else + oled.print("0"); +} + +// Given text, a position, and kerning, print text to display +// This is helpful for squishing or stretching a string to appropriately fill the display +void printTextwithKerning(const char *newText, uint8_t xPos, uint8_t yPos, uint8_t kerning) +{ + for (int x = 0; x < strlen(newText); x++) + { + oled.setCursor(xPos, yPos); + oled.print(newText[x]); + xPos += kerning; + } +} + +// Show transmission of RTCM correction data packets to NTRIP caster +void paintRTCM() +{ + int yPos = 17; + + // Determine if the NTRIP Server is casting + bool casting = false; + for (int serverIndex = 0; serverIndex < NTRIP_SERVER_MAX; serverIndex++) + casting |= online.ntripServer[serverIndex]; + + if (casting) + printTextCenter("Casting", yPos, QW_FONT_8X16, 1, false); // text, y, font type, kerning, inverted + else + printTextCenter("Xmitting", yPos, QW_FONT_8X16, 1, false); // text, y, font type, kerning, inverted - if (i2cGNSS.getFixType() == 0) //0 = No Fix + if (!HAS_ANTENNA_SHORT_OPEN) { - oled.print("0"); + oled.setCursor(0, 39); // x, y + oled.setFont(QW_FONT_5X7); + oled.print("RTCM:"); } else { - oled.print(i2cGNSS.getSIV()); + static uint32_t blinkers = 0; + if (aStatus == SFE_UBLOX_ANTENNA_STATUS_SHORT) + { + blinkers ^= ICON_ANTENNA_SHORT; + if (blinkers & ICON_ANTENNA_SHORT) + displayBitmap(2, 35, Antenna_Short_Width, Antenna_Short_Height, Antenna_Short); + } + else if (aStatus == SFE_UBLOX_ANTENNA_STATUS_OPEN) + { + blinkers ^= ICON_ANTENNA_OPEN; + if (blinkers & ICON_ANTENNA_OPEN) + displayBitmap(2, 35, Antenna_Open_Width, Antenna_Open_Height, Antenna_Open); + } + else + { + blinkers &= ~ICON_ANTENNA_SHORT; + blinkers &= ~ICON_ANTENNA_OPEN; + oled.setCursor(0, 39); // x, y + oled.setFont(QW_FONT_5X7); + oled.print("RTCM:"); + } } - } + + if (rtcmPacketsSent < 100) + oled.setCursor(30, 36); // x, y - Give space for two digits + else + oled.setCursor(28, 36); // x, y - Push towards colon to make room for log icon + + oled.setFont(QW_FONT_8X16); // Set font to type 1: 8x16 + oled.print(rtcmPacketsSent); // rtcmPacketsSent is controlled in processRTCM() + + paintResets(); } -//Draw log icon -//Turn off icon if log file fails to get bigger -void paintLogging() +// Show connecting to NTRIP caster service +void paintConnectingToNtripCaster() +{ + int yPos = 18; + printTextCenter("Caster", yPos, QW_FONT_8X16, 1, false); // text, y, font type, kerning, inverted + + int textX = 3; + int textY = 33; + int textKerning = 6; + oled.setFont(QW_FONT_8X16); + + printTextwithKerning("Connecting", textX, textY, textKerning); +} + +// Scroll through IP address. Wipe with spaces both ends. +void paintIPAddress() { - if (online.display == true) - { - if (logIncreasing == true) + char ipAddress[32]; + snprintf(ipAddress, sizeof(ipAddress), " %d.%d.%d.%d ", +#ifdef COMPILE_ETHERNET + Ethernet.localIP()[0], Ethernet.localIP()[1], Ethernet.localIP()[2], Ethernet.localIP()[3]); +#else // COMPILE_ETHERNET + 0, 0, 0, 0); +#endif // COMPILE_ETHERNET + + static uint8_t ipAddressPosition = 0; + + // Check if IP address is all single digits and can be printed without scrolling + if (strlen(ipAddress) <= 21) + ipAddressPosition = 7; + + // Print seven characters of IP address + char printThis[9]; + snprintf(printThis, sizeof(printThis), "%c%c%c%c%c%c%c", ipAddress[ipAddressPosition + 0], + ipAddress[ipAddressPosition + 1], ipAddress[ipAddressPosition + 2], ipAddress[ipAddressPosition + 3], + ipAddress[ipAddressPosition + 4], ipAddress[ipAddressPosition + 5], ipAddress[ipAddressPosition + 6]); + + oled.setFont(QW_FONT_5X7); // Set font to smallest + oled.setCursor(0, 3); + oled.print(printThis); + + ipAddressPosition++; // Increment the print position + if (ipAddress[ipAddressPosition + 7] == 0) // Wrap + ipAddressPosition = 0; +} + +void displayBaseStart(uint16_t displayTime) +{ + if (online.display == true) { - //Animate icon to show system running - if (millis() - lastLoggingIconUpdate > 500) - { - lastLoggingIconUpdate = millis(); + oled.erase(); - if (loggingIconDisplayed == 0) - oled.drawIcon(64 - Logging_0_Width, 48 - Logging_0_Height, Logging_0_Width, Logging_0_Height, Logging_0, sizeof(Logging_0), true); //Draw the icon - else if (loggingIconDisplayed == 1) - oled.drawIcon(64 - Logging_1_Width, 48 - Logging_1_Height, Logging_1_Width, Logging_1_Height, Logging_1, sizeof(Logging_1), true); //Draw the icon - else if (loggingIconDisplayed == 2) - oled.drawIcon(64 - Logging_2_Width, 48 - Logging_2_Height, Logging_2_Width, Logging_2_Height, Logging_2, sizeof(Logging_2), true); //Draw the icon - else if (loggingIconDisplayed == 3) - oled.drawIcon(64 - Logging_3_Width, 48 - Logging_3_Height, Logging_3_Width, Logging_3_Height, Logging_3, sizeof(Logging_3), true); //Draw the icon + uint8_t fontHeight = 15; // Assume fontsize 1 + uint8_t yPos = oled.getHeight() / 2 - fontHeight; + + printTextCenter("Base", yPos, QW_FONT_8X16, 1, false); // text, y, font type, kerning, inverted + oled.display(); + + oled.display(); + + delay(displayTime); + } +} + +void displayBaseSuccess(uint16_t displayTime) +{ + if (online.display == true) + { + oled.erase(); + + uint8_t fontHeight = 15; // Assume fontsize 1 + uint8_t yPos = oled.getHeight() / 2 - fontHeight; + + printTextCenter("Base", yPos, QW_FONT_8X16, 1, false); // text, y, font type, kerning, inverted + printTextCenter("Started", yPos + fontHeight, QW_FONT_8X16, 1, false); // text, y, font type, kerning, inverted + + oled.display(); + + delay(displayTime); + } +} + +void displayBaseFail(uint16_t displayTime) +{ + if (online.display == true) + { + oled.erase(); + + uint8_t fontHeight = 15; // Assume fontsize 1 + uint8_t yPos = oled.getHeight() / 2 - fontHeight; + + printTextCenter("Base", yPos, QW_FONT_8X16, 1, false); // text, y, font type, kerning, inverted + printTextCenter("Failed", yPos + fontHeight, QW_FONT_8X16, 1, false); // text, y, font type, kerning, inverted + + oled.display(); + + delay(displayTime); + } +} + +void displayGNSSFail(uint16_t displayTime) +{ + displayMessage("GNSS Failed", displayTime); +} + +void displayNoWiFi(uint16_t displayTime) +{ + displayMessage("No WiFi", displayTime); +} + +void displayNoSSIDs(uint16_t displayTime) +{ + displayMessage("No SSIDs", displayTime); +} + +void displayAccountExpired(uint16_t displayTime) +{ + displayMessage("Account Expired", displayTime); +} + +void displayNotListed(uint16_t displayTime) +{ + displayMessage("Not Listed", displayTime); +} + +void displayRoverStart(uint16_t displayTime) +{ + if (online.display == true) + { + oled.erase(); + + uint8_t fontHeight = 15; + uint8_t yPos = oled.getHeight() / 2 - fontHeight; + + printTextCenter("Rover", yPos, QW_FONT_8X16, 1, false); // text, y, font type, kerning, inverted + // printTextCenter("Started", yPos + fontHeight, QW_FONT_8X16, 1, false); //text, y, font type, kerning, + // inverted + + oled.display(); + + delay(displayTime); + } +} + +void displayNoRingBuffer(uint16_t displayTime) +{ + if (online.display == true) + { + oled.erase(); + + uint8_t fontHeight = 8; + uint8_t yPos = oled.getHeight() / 3 - fontHeight; + + printTextCenter("Fix GNSS", yPos, QW_FONT_5X7, 1, false); // text, y, font type, kerning, inverted + yPos += fontHeight; + printTextCenter("Handler", yPos, QW_FONT_5X7, 1, false); // text, y, font type, kerning, inverted + yPos += fontHeight; + printTextCenter("Buffer Sz", yPos, QW_FONT_5X7, 1, false); // text, y, font type, kerning, inverted - loggingIconDisplayed++; //Goto next icon - loggingIconDisplayed %= 4; //Wrap - } + oled.display(); + + delay(displayTime); } - } } -//Base screen. Display BLE, rover, battery, HorzAcc and SIV -//Blink SIV until fix -void paintRoverNoFix() +void displayRoverSuccess(uint16_t displayTime) { - if (online.display == true) - { - paintBatteryLevel(); //Top right corner - - paintWirelessIcon(); //Top left corner + if (online.display == true) + { + oled.erase(); - paintBaseState(); //Top center + uint8_t fontHeight = 15; + uint8_t yPos = oled.getHeight() / 2 - fontHeight; - paintHorizontalAccuracy(); + printTextCenter("Rover", yPos, QW_FONT_8X16, 1, false); // text, y, font type, kerning, inverted + printTextCenter("Started", yPos + fontHeight, QW_FONT_8X16, 1, false); // text, y, font type, kerning, inverted - paintSIV(); + oled.display(); - paintLogging(); - } + delay(displayTime); + } } -//Currently identical to RoverNoFix because paintSIV and paintHorizontalAccuracy takes into account system states -void paintRoverFix() +void displayRoverFail(uint16_t displayTime) { - if (online.display == true) - { - paintBatteryLevel(); //Top right corner - - paintWirelessIcon(); //Top left corner + if (online.display == true) + { + oled.erase(); - paintBaseState(); //Top center + uint8_t fontHeight = 15; + uint8_t yPos = oled.getHeight() / 2 - fontHeight; - paintHorizontalAccuracy(); + printTextCenter("Rover", yPos, QW_FONT_8X16, 1, false); // text, y, font type, kerning, inverted + printTextCenter("Failed", yPos + fontHeight, QW_FONT_8X16, 1, false); // text, y, font type, kerning, inverted - paintSIV(); + oled.display(); - paintLogging(); - } + delay(displayTime); + } } -//Currently identical to RoverNoFix because paintSIV and paintHorizontalAccuracy takes into account system states -void paintRoverRTKFloat() +void displayAccelFail(uint16_t displayTime) { - if (online.display == true) - { - paintBatteryLevel(); //Top right corner + if (online.display == true) + { + oled.erase(); - paintWirelessIcon(); //Top left corner + uint8_t fontHeight = 15; + uint8_t yPos = oled.getHeight() / 2 - fontHeight; - paintBaseState(); //Top center + printTextCenter("Accel", yPos, QW_FONT_8X16, 1, false); // text, y, font type, kerning, inverted + printTextCenter("Failed", yPos + fontHeight, QW_FONT_8X16, 1, false); // text, y, font type, kerning, inverted - paintHorizontalAccuracy(); + oled.display(); - paintSIV(); + delay(displayTime); + } +} - paintLogging(); - } +// When user enters serial config menu the display will freeze so show splash while config happens +void displaySerialConfig() +{ + displayMessage("Serial Config", 0); } -void paintRoverRTKFix() +// Display during blocking stop during to prevent screen freeze +void displayWiFiConnect() { - if (online.display == true) - { - paintBatteryLevel(); //Top right corner + displayMessage("WiFi Connect", 0); +} - paintWirelessIcon(); //Top left corner +// When user enters WiFi Config mode from setup, show splash while config happens +void displayWiFiConfigNotStarted() +{ + displayMessage("WiFi Config", 0); +} - paintBaseState(); //Top center +void displayWiFiConfig() +{ + int yPos = WiFi_Symbol_Height + 2; + int fontHeight = 8; - paintHorizontalAccuracy(); + const int displayMaxCharacters = + 10; // Characters before pixels start getting cut off. 11 characters can cut off a few pixels. - paintSIV(); + printTextCenter("SSID:", yPos, QW_FONT_5X7, 1, false); // text, y, font type, kerning, inverted - paintLogging(); - } -} + yPos = yPos + fontHeight + 1; -//Start of base / survey in / NTRIP mode -//Screen is displayed while we are waiting for horz accuracy to drop to appropriate level -//Blink crosshair icon until we have we have horz accuracy < user defined level -void paintBaseTempSettle() -{ - if (online.display == true) - { - paintBatteryLevel(); //Top right corner + // Toggle display back and forth for long SSIDs and IPs + // Run the timer no matter what, but load firstHalf/lastHalf with the same thing if strlen < maxWidth + if (millis() - ssidDisplayTimer > 2000) + { + ssidDisplayTimer = millis(); - paintWirelessIcon(); //Top left corner + if (ssidDisplayFirstHalf == false) + ssidDisplayFirstHalf = true; + else + ssidDisplayFirstHalf = false; + } - paintBaseState(); //Top center + // Convert current SSID to string + char mySSID[50] = {'\0'}; - paintHorizontalAccuracy(); //2nd line +#ifdef COMPILE_WIFI + if (settings.wifiConfigOverAP == true) + snprintf(mySSID, sizeof(mySSID), "%s", "RTK Config"); + else + { + if (WiFi.getMode() == WIFI_STA) + snprintf(mySSID, sizeof(mySSID), "%s", WiFi.SSID().c_str()); - paintSIV(); + // If we failed to connect to a friendly WiFi, and then fell back to AP mode, still display RTK Config + else if (WiFi.getMode() == WIFI_AP) + snprintf(mySSID, sizeof(mySSID), "%s", "RTK Config"); - paintLogging(); - } -} + else + snprintf(mySSID, sizeof(mySSID), "%s", "Error"); + } +#else // COMPILE_WIFI + snprintf(mySSID, sizeof(mySSID), "%s", "!Compiled"); +#endif // COMPILE_WIFI -//Survey in is running. Show 3D Mean and elapsed time. -void paintBaseTempSurveyStarted() -{ - if (online.display == true) - { - paintBatteryLevel(); //Top right corner + char mySSIDFront[displayMaxCharacters + 1]; // 1 for null terminator + char mySSIDBack[displayMaxCharacters + 1]; // 1 for null terminator - paintWirelessIcon(); //Top left corner + // Trim SSID to a max length + strncpy(mySSIDFront, mySSID, displayMaxCharacters); - paintBaseState(); //Top center + if (strlen(mySSID) > displayMaxCharacters) + strncpy(mySSIDBack, mySSID + (strlen(mySSID) - displayMaxCharacters), displayMaxCharacters); + else + strncpy(mySSIDBack, mySSID, displayMaxCharacters); - oled.setFontType(0); - oled.setCursor(0, 23); //x, y - oled.print("Mean:"); + mySSIDFront[displayMaxCharacters] = '\0'; + mySSIDBack[displayMaxCharacters] = '\0'; - oled.setCursor(29, 20); //x, y - oled.setFontType(1); - if (svinMeanAccuracy < 10.0) //Error check - oled.print(svinMeanAccuracy, 2); + if (ssidDisplayFirstHalf == true) + printTextCenter(mySSIDFront, yPos, QW_FONT_5X7, 1, false); else - oled.print(">10"); + printTextCenter(mySSIDBack, yPos, QW_FONT_5X7, 1, false); - oled.setCursor(0, 39); //x, y - oled.setFontType(0); - oled.print("Time:"); + yPos = yPos + fontHeight + 3; + printTextCenter("IP:", yPos, QW_FONT_5X7, 1, false); - oled.setCursor(30, 36); //x, y - oled.setFontType(1); - if (svinObservationTime < 1000) //Error check - oled.print(svinObservationTime); - else - oled.print("0"); + yPos = yPos + fontHeight + 1; - paintLogging(); - } -} +#ifdef COMPILE_AP + IPAddress myIpAddress; + if (WiFi.getMode() == WIFI_AP) + myIpAddress = WiFi.softAPIP(); + else + myIpAddress = WiFi.localIP(); -//Show transmission of RTCM packets -void paintBaseTempTransmitting() -{ - if (online.display == true) - { - paintBatteryLevel(); //Top right corner + // Convert to string + char myIP[20] = {'\0'}; + snprintf(myIP, sizeof(myIP), "%d.%d.%d.%d", myIpAddress[0], myIpAddress[1], myIpAddress[2], myIpAddress[3]); - paintWirelessIcon(); //Top left corner + char myIPFront[displayMaxCharacters + 1]; // 1 for null terminator + char myIPBack[displayMaxCharacters + 1]; // 1 for null terminator - paintBaseState(); //Top center + strncpy(myIPFront, myIP, displayMaxCharacters); - int textX = 1; - int textY = 17; - int textKerning = 8; - oled.setFontType(1); - printTextwithKerning((char*)"Xmitting", textX, textY, textKerning); + if (strlen(myIP) > displayMaxCharacters) + strncpy(myIPBack, myIP + (strlen(myIP) - displayMaxCharacters), displayMaxCharacters); + else + strncpy(myIPBack, myIP, displayMaxCharacters); - oled.setCursor(0, 39); //x, y - oled.setFontType(0); - oled.print("RTCM:"); + myIPFront[displayMaxCharacters] = '\0'; + myIPBack[displayMaxCharacters] = '\0'; - if (rtcmPacketsSent < 100) - oled.setCursor(30, 36); //x, y - Give space for two digits + if (ssidDisplayFirstHalf == true) + printTextCenter(myIPFront, yPos, QW_FONT_5X7, 1, false); else - oled.setCursor(28, 36); //x, y - Push towards colon to make room for log icon + printTextCenter(myIPBack, yPos, QW_FONT_5X7, 1, false); - oled.setFontType(1); //Set font to type 1: 8x16 - oled.print(rtcmPacketsSent); //rtcmPacketsSent is controlled in processRTCM() +#else // COMPILE_AP + printTextCenter("!Compiled", yPos, QW_FONT_5X7, 1, false); +#endif // COMPILE_AP +} - paintLogging(); - } +// When user does a factory reset, let us know +void displaySytemReset() +{ + displayMessage("Factory Reset", 0); } -//Show transmission of RTCM packets -//Blink WiFi icon -void paintBaseTempWiFiStarted() +void displaySurveyStart(uint16_t displayTime) { - if (online.display == true) - { - paintBatteryLevel(); //Top right corner + if (online.display == true) + { + oled.erase(); - paintWirelessIcon(); //Top left corner + uint8_t fontHeight = 15; + uint8_t yPos = oled.getHeight() / 2 - fontHeight; - paintBaseState(); //Top center + printTextCenter("Survey", yPos, QW_FONT_8X16, 1, false); // text, y, font type, kerning, inverted + // printTextCenter("Started", yPos + fontHeight, QW_FONT_8X16, 1, false); //text, y, font type, kerning, + // inverted - int textX = 1; - int textY = 17; - int textKerning = 8; - oled.setFontType(1); - printTextwithKerning((char*)"Xmitting", textX, textY, textKerning); + oled.display(); - oled.setCursor(0, 39); //x, y - oled.setFontType(0); - oled.print("RTCM:"); + delay(displayTime); + } +} - if (rtcmPacketsSent < 100) - oled.setCursor(30, 36); //x, y - Give space for two digits - else - oled.setCursor(28, 36); //x, y - Push towards colon to make room for log icon +void displaySurveyStarted(uint16_t displayTime) +{ + if (online.display == true) + { + oled.erase(); + + uint8_t fontHeight = 15; + uint8_t yPos = oled.getHeight() / 2 - fontHeight; - oled.setFontType(1); //Set font to type 1: 8x16 - oled.print(rtcmPacketsSent); //rtcmPacketsSent is controlled in processRTCM() + printTextCenter("Survey", yPos, QW_FONT_8X16, 1, false); // text, y, font type, kerning, inverted + printTextCenter("Started", yPos + fontHeight, QW_FONT_8X16, 1, false); // text, y, font type, kerning, inverted - paintLogging(); - } + oled.display(); + + delay(displayTime); + } } -//Show transmission of RTCM packets -//Solid WiFi icon -//This is identical to paintBaseTempWiFiStarted -void paintBaseTempWiFiConnected() +// If the SD card is detected but is not formatted correctly, display warning +void displaySDFail(uint16_t displayTime) { - if (online.display == true) - { - paintBatteryLevel(); //Top right corner - - paintWirelessIcon(); //Top left corner + if (online.display == true) + { + oled.erase(); - paintBaseState(); //Top center + uint8_t fontHeight = 15; + uint8_t yPos = oled.getHeight() / 2 - fontHeight; - int textX = 1; - int textY = 17; - int textKerning = 8; - oled.setFontType(1); - printTextwithKerning((char*)"Xmitting", textX, textY, textKerning); + printTextCenter("Format", yPos, QW_FONT_8X16, 1, false); // text, y, font type, kerning, inverted + printTextCenter("SD Card", yPos + fontHeight, QW_FONT_8X16, 1, false); // text, y, font type, kerning, inverted - oled.setCursor(0, 39); //x, y - oled.setFontType(0); - oled.print("RTCM:"); + oled.display(); - if (rtcmPacketsSent < 100) - oled.setCursor(30, 36); //x, y - Give space for two digits - else - oled.setCursor(28, 36); //x, y - Push towards colon to make room for log icon + delay(displayTime); + } +} - oled.setFontType(1); //Set font to type 1: 8x16 - oled.print(rtcmPacketsSent); //rtcmPacketsSent is controlled in processRTCM() +// Draw a frame at outside edge +void drawFrame() +{ + // Init and draw box at edge to see screen alignment + int xMax = 63; + int yMax = 47; + oled.line(0, 0, xMax, 0); // Top + oled.line(0, 0, 0, yMax); // Left + oled.line(0, yMax, xMax, yMax); // Bottom + oled.line(xMax, 0, xMax, yMax); // Right +} - paintLogging(); - } +void displayForcedFirmwareUpdate() +{ + displayMessage("Forced Update", 0); } -//Show connecting to caster service -//Solid WiFi icon -void paintBaseTempCasterStarted() +void displayFirmwareUpdateProgress(int percentComplete) { - if (online.display == true) - { - paintBatteryLevel(); //Top right corner + // Update the display if connected + if (online.display == true) + { + oled.erase(); // Clear the display's internal buffer + + int yPos = 3; + int fontHeight = 8; - paintWirelessIcon(); //Top left corner + printTextCenter("Firmware", yPos, QW_FONT_5X7, 1, false); // text, y, font type, kerning, inverted - paintBaseState(); //Top center + yPos = yPos + fontHeight + 1; + printTextCenter("Update", yPos, QW_FONT_5X7, 1, false); // text, y, font type, kerning, inverted - int textX = 11; - int textY = 17; - int textKerning = 8; + yPos = yPos + fontHeight + 3; + char temp[50]; + snprintf(temp, sizeof(temp), "%d%%", percentComplete); + printTextCenter(temp, yPos, QW_FONT_8X16, 1, false); // text, y, font type, kerning, inverted - printTextwithKerning((char*)"Caster", textX, textY, textKerning); + oled.display(); // Push internal buffer to display + } +} - textX = 3; - textY = 33; - textKerning = 6; - oled.setFontType(1); +void displayEventMarked(uint16_t displayTime) +{ + displayMessage("Event Marked", displayTime); +} - printTextwithKerning((char*)"Connecting", textX, textY, textKerning); - } +void displayNoLogging(uint16_t displayTime) +{ + displayMessage("No Logging", displayTime); } -//Show transmission of RTCM packets to caster service -//Solid WiFi icon -void paintBaseTempCasterConnected() +void displayMarked(uint16_t displayTime) { - if (online.display == true) - { - paintBatteryLevel(); //Top right corner + displayMessage("Marked", displayTime); +} - paintWirelessIcon(); //Top left corner +void displayMarkFailure(uint16_t displayTime) +{ + displayMessage("Mark Failure", displayTime); +} - paintBaseState(); //Top center +void displayNotMarked(uint16_t displayTime) +{ + displayMessage("Not Marked", displayTime); +} - int textX = 4; - int textY = 17; - int textKerning = 8; - oled.setFontType(1); - printTextwithKerning((char*)"Casting", textX, textY, textKerning); +// Show 'Loading Home2' profile identified +// Profiles may not be sequential (user might have empty profile #2, but filled #3) so we load the profile unit, not the +// number +void paintProfile(uint8_t profileUnit) +{ + char profileMessage[20]; //'Loading HomeStar' max of ~18 chars - oled.setCursor(0, 39); //x, y - oled.setFontType(0); - oled.print("RTCM:"); + char profileName[8 + 1]; + if (getProfileNameFromUnit(profileUnit, profileName, sizeof(profileName)) == + true) // Load the profile name, limited to 8 chars + { + settings.updateZEDSettings = true; // When this profile is loaded next, force system to update ZED settings. + recordSystemSettings(); // Before switching, we need to record the current settings to LittleFS and SD - if (rtcmPacketsSent < 100) - oled.setCursor(30, 36); //x, y - Give space for two digits - else - oled.setCursor(28, 36); //x, y - Push towards colon to make room for log icon + // Lookup profileNumber based on unit + uint8_t profileNumber = getProfileNumberFromUnit(profileUnit); + recordProfileNumber(profileNumber); // Update internal settings with user's choice, mark unit for config update - oled.setFontType(1); //Set font to type 1: 8x16 - oled.print(rtcmPacketsSent); //rtcmPacketsSent is controlled in processRTCM() + log_d("Going to profile number %d from unit %d, name '%s'", profileNumber, profileUnit, profileName); - paintLogging(); - } + snprintf(profileMessage, sizeof(profileMessage), "Loading %s", profileName); + displayMessage(profileMessage, 2000); + ESP.restart(); // Profiles require full restart to take effect + } } -//Show transmission of RTCM packets -void paintBaseFixedNotStarted() +// Display unit self-tests until user presses a button to exit +// Allows operator to check: +// Display alignment +// Internal connections to: SD, Accel, Fuel guage, GNSS +// External connections: Loop back test on DATA +void paintSystemTest() { - if (online.display == true) - { - paintBatteryLevel(); //Top right corner + static uint8_t systemTestDisplayNumber = 0; // Tracks which test screen we're looking at. + static unsigned long systemTestDisplayTime = millis(); // Timestamp for swapping the graphic during testing + + if (online.display == true) + { + // Main info display + if (systemTestDisplayNumber == 0) + { + int xOffset = 2; + int yOffset = 2; + + int charHeight = 7; + + drawFrame(); // Outside edge + + // Test SD, accel, batt, GNSS, mux + oled.setFont(QW_FONT_5X7); // Set font to smallest + + oled.setCursor(xOffset, yOffset + (0 * charHeight)); // x, y + oled.print("ZV:"); + oled.print(zedFirmwareVersionInt); + + // ZED-F9P goes to 150 + if (zedModuleType == PLATFORM_F9P) + { + if (zedFirmwareVersionInt < 150) + oled.print("-FAI"); + else + oled.print("-OK"); + } + + // ZED-F9R goes to 130 + else if (zedModuleType == PLATFORM_F9R) + { + if (zedFirmwareVersionInt < 130) + oled.print("-FAI"); + else + oled.print("-OK"); + } + + oled.setCursor(xOffset, yOffset + (1 * charHeight)); // x, y + oled.print("SD:"); + if (online.microSD == false) + beginSD(); // Test if SD is present + if (online.microSD == true) + oled.print("OK"); + else + oled.print("FAIL"); + + if (productVariant != REFERENCE_STATION) + { + oled.setCursor(xOffset, yOffset + (2 * charHeight)); // x, y + oled.print("Batt:"); + if (online.battery == true) + oled.print("OK"); + else + oled.print("FAIL"); + } + + //Check for satellites in view + oled.setCursor(xOffset, yOffset + (3 * charHeight)); // x, y + oled.print("SIV:"); + if (online.gnss == true) + { + theGNSS.checkUblox(); // Regularly poll to get latest data and any RTCM + theGNSS.checkCallbacks(); // Process any callbacks: ie, eventTriggerReceived + + int satsInView = numSV; + if (satsInView > 5) + { + oled.print("OK"); + oled.print("/"); + oled.print(satsInView); + } + else + oled.print("FAIL"); + } + else + oled.print("FAIL"); + + if (productVariant == RTK_EXPRESS || productVariant == RTK_EXPRESS_PLUS || productVariant == RTK_FACET || + productVariant == RTK_FACET_LBAND || productVariant == RTK_FACET_LBAND_DIRECT) + { + oled.setCursor(xOffset, yOffset + (4 * charHeight)); // x, y + oled.print("Mux:"); + + // Set mux to channel 3 and toggle pin and verify with loop back jumper wire inserted by test technician + + setMuxport(MUX_ADC_DAC); // Set mux to DAC so we can toggle back/forth + pinMode(pin_dac26, OUTPUT); + pinMode(pin_adc39, INPUT_PULLUP); + + digitalWrite(pin_dac26, HIGH); + if (digitalRead(pin_adc39) == HIGH) + { + digitalWrite(pin_dac26, LOW); + if (digitalRead(pin_adc39) == LOW) + oled.print("OK"); + else + oled.print("FAIL"); + } + else + oled.print("FAIL"); + } + + // Get the last two digits of MAC + char macAddress[5]; + const uint8_t *rtkMacAddress = getMacAddress(); + snprintf(macAddress, sizeof(macAddress), "%02X%02X", rtkMacAddress[4], rtkMacAddress[5]); + + // Display MAC address + oled.setCursor(xOffset, yOffset + (5 * charHeight)); // x, y + oled.print(macAddress); + oled.print(":"); + + // Verify the ESP UART2 can communicate TX/RX to ZED UART1 + if ((USE_I2C_GNSS) && (zedUartPassed == false)) + { + systemPrintln("GNSS test"); + + setMuxport(MUX_UBLOX_NMEA); // Set mux to UART so we can debug over data port + delay(20); + + // Clear out buffer before starting + while (serialGNSS.available()) + serialGNSS.read(); + serialGNSS.flush(); + + SFE_UBLOX_GNSS_SERIAL myGNSS; + + // begin() attempts 3 connections + if (myGNSS.begin(serialGNSS) == true) + { + + zedUartPassed = true; + oled.print("OK"); + } + else + oled.print("FAIL"); + } + else + oled.print("OK"); + } // End display 0 + + // Display LBand Info + if (systemTestDisplayNumber == 1) + { + int xOffset = 2; + int yOffset = 2; + + int charHeight = 7; + + drawFrame(); // Outside edge + + // Test L-Band + oled.setFont(QW_FONT_5X7); // Set font to smallest - paintWirelessIcon(); //Top left corner + oled.setCursor(xOffset, yOffset + (0 * charHeight)); // x, y + oled.print("LBand:"); + if (online.lband == true) + oled.print("OK"); + else + oled.print("FAIL"); + } // End display 1 - paintBaseState(); //Top center - } + if (productVariant == RTK_FACET_LBAND || productVariant == RTK_FACET_LBAND_DIRECT) + { + // Toggle between two displays + if (millis() - systemTestDisplayTime > 3000) + { + systemTestDisplayTime = millis(); + systemTestDisplayNumber++; + systemTestDisplayNumber %= 2; + } + } + } } -//Show transmission of RTCM packets -void paintBaseFixedTransmitting() -{ - if (online.display == true) - { - paintBatteryLevel(); //Top right corner +// Globals but only used for Bubble Level +double averagedRoll = 0.0; +double averagedPitch = 0.0; - paintWirelessIcon(); //Top left corner +// A bubble level +void paintBubbleLevel() +{ + if (online.accelerometer == true) + { + forceDisplayUpdate = true; // Update the display as quickly as possible - paintBaseState(); //Top center + getAngles(); - int textX = 1; - int textY = 17; - int textKerning = 8; - oled.setFontType(1); - printTextwithKerning((char*)"Xmitting", textX, textY, textKerning); + // Draw dot in middle + oled.pixel(oled.getWidth() / 2, oled.getHeight() / 2); + oled.pixel(oled.getWidth() / 2 + 1, oled.getHeight() / 2); + oled.pixel(oled.getWidth() / 2, oled.getHeight() / 2 + 1); + oled.pixel(oled.getWidth() / 2 + 1, oled.getHeight() / 2 + 1); - oled.setCursor(0, 39); //x, y - oled.setFontType(0); - oled.print("RTCM:"); + // Draw circle relative to dot + const int radiusLarge = 10; + const int radiusSmall = 4; - if (rtcmPacketsSent < 100) - oled.setCursor(30, 36); //x, y - Give space for two digits + oled.circle(oled.getWidth() / 2 - averagedPitch, oled.getHeight() / 2 + averagedRoll, radiusLarge); + oled.circle(oled.getWidth() / 2 - averagedPitch, oled.getHeight() / 2 + averagedRoll, radiusSmall); + } else - oled.setCursor(28, 36); //x, y - Push towards colon to make room for log icon + { + displayAccelFail(0); + } +} + +void getAngles() +{ + if (online.accelerometer == true) + { + averagedRoll = 0.0; + averagedPitch = 0.0; + const int avgAmount = 16; + + // Take an average readings + for (int reading = 0; reading < avgAmount; reading++) + { + while (accel.available() == false) + delay(1); + + float accelX = 0; + float accelY = 0; + float accelZ = 0; + + // Express Accel orientation is different from Facet + if (productVariant == RTK_EXPRESS || productVariant == RTK_EXPRESS_PLUS) + { + accelX = accel.getX(); + accelZ = accel.getY(); + accelY = accel.getZ(); + accelZ *= -1.0; + accelX *= -1.0; + } + else if (productVariant == RTK_FACET || productVariant == RTK_FACET_LBAND || + productVariant == RTK_FACET_LBAND_DIRECT) + { + accelZ = accel.getX(); + accelX = accel.getY(); + accelY = accel.getZ(); + accelZ *= -1.0; + accelY *= -1.0; + accelX *= -1.0; + } + + double roll = atan2(accelY, accelZ) * 57.3; + double pitch = atan2((-accelX), sqrt(accelY * accelY + accelZ * accelZ)) * 57.3; + + averagedRoll += roll; + averagedPitch += pitch; + } - oled.setFontType(1); //Set font to type 1: 8x16 - oled.print(rtcmPacketsSent); //rtcmPacketsSent is controlled in processRTCM() + averagedRoll /= (float)avgAmount; + averagedPitch /= (float)avgAmount; - paintLogging(); - } + // Avoid -0 since we're not printing the decimal portion + if (averagedRoll < 0.5 && averagedRoll > -0.5) + averagedRoll = 0; + if (averagedPitch < 0.5 && averagedPitch > -0.5) + averagedPitch = 0; + } } -//Show transmission of RTCM packets -//Blink WiFi icon -void paintBaseFixedWiFiStarted() +// Display the setup profiles +void paintDisplaySetupProfile(const char *firstState) { - if (online.display == true) - { - paintBatteryLevel(); //Top right corner + int index; + int itemsDisplayed; + char profileName[8 + 1]; - paintWirelessIcon(); //Top left corner + // Display the first state if this is the first profile + itemsDisplayed = 0; + if (displayProfile == 0) + { + printTextCenter(firstState, 12 * itemsDisplayed, QW_FONT_8X16, 1, false); + itemsDisplayed++; + } - paintBaseState(); //Top center + // Display Bubble if this is the second profile + if (displayProfile <= 1) + { + printTextCenter("Bubble", 12 * itemsDisplayed, QW_FONT_8X16, 1, false); + itemsDisplayed++; + } - int textX = 1; - int textY = 17; - int textKerning = 8; - oled.setFontType(1); - printTextwithKerning((char*)"Xmitting", textX, textY, textKerning); + // Display Config if this is the third profile + if (displayProfile <= 2) + { + printTextCenter("Config", 12 * itemsDisplayed, QW_FONT_8X16, 1, false); + itemsDisplayed++; + } - oled.setCursor(0, 39); //x, y - oled.setFontType(0); - oled.print("RTCM:"); + // displayProfile itemsDisplayed index + // 0 3 0 + // 1 2 0 + // 2 1 0 + // 3 0 0 + // 4 0 1 + // 5 0 2 + // n >= 3 0 n - 3 + + // Display the profile names + for (index = (displayProfile >= 3) ? displayProfile - 3 : 0; itemsDisplayed < 4; itemsDisplayed++) + { + // Lookup next available profile, limit to 8 characters + getProfileNameFromUnit(index, profileName, sizeof(profileName)); - if (rtcmPacketsSent < 100) - oled.setCursor(30, 36); //x, y - Give space for two digits - else - oled.setCursor(28, 36); //x, y - Push towards colon to make room for log icon + profileName[6] = 0; // Shorten profileName to 6 characters - oled.setFontType(1); //Set font to type 1: 8x16 - oled.print(rtcmPacketsSent); //rtcmPacketsSent is controlled in processRTCM() + char miniProfileName[16] = {0}; + snprintf(miniProfileName, sizeof(miniProfileName), "%d_%s", index, profileName); // Prefix with index # - paintLogging(); - } + printTextCenter(miniProfileName, 12 * itemsDisplayed, QW_FONT_8X16, 1, itemsDisplayed == 3); + index++; + } } -//Show transmission of RTCM packets -//Solid WiFi icon -//This is identical to paintBaseTempWiFiStarted -void paintBaseFixedWiFiConnected() +// Show different menu 'buttons' to allow user to pause on one to select it +void paintDisplaySetup() { - if (online.display == true) - { - paintBatteryLevel(); //Top right corner + if (zedModuleType == PLATFORM_F9P) + { + if (setupState == STATE_MARK_EVENT) + { + if (productVariant == REFERENCE_STATION) + { + // setupState defaults to STATE_MARK_EVENT, which is not a valid state for the Ref Stn. + // It will be corrected by ButtonCheckTask. Until then, display but don't highlight an option. + printTextCenter("Base", 12 * 0, QW_FONT_8X16, 1, false); // string, y, font type, kerning, inverted + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("NTP", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("Cfg Eth", 12 * 3, QW_FONT_8X16, 1, false); + } + else if (online.accelerometer) + { + printTextCenter("Mark", 12 * 0, QW_FONT_8X16, 1, true); // string, y, font type, kerning, inverted + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("Base", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("Bubble", 12 * 3, QW_FONT_8X16, 1, false); + } + else + { + printTextCenter("Mark", 12 * 0, QW_FONT_8X16, 1, true); // string, y, font type, kerning, inverted + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("Base", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("Config", 12 * 3, QW_FONT_8X16, 1, false); + } + } + else if (setupState == STATE_ROVER_NOT_STARTED) + { + if (productVariant == REFERENCE_STATION) + { + printTextCenter("Base", 12 * 0, QW_FONT_8X16, 1, false); // string, y, font type, kerning, inverted + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, true); + printTextCenter("NTP", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("Cfg Eth", 12 * 3, QW_FONT_8X16, 1, false); + } + else if (online.accelerometer) + { + printTextCenter("Mark", 12 * 0, QW_FONT_8X16, 1, false); + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, true); + printTextCenter("Base", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("Bubble", 12 * 3, QW_FONT_8X16, 1, false); + } + else + { + printTextCenter("Mark", 12 * 0, QW_FONT_8X16, 1, false); + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, true); + printTextCenter("Base", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("Config", 12 * 3, QW_FONT_8X16, 1, false); + } + } + else if (setupState == STATE_BASE_NOT_STARTED) + { + if (productVariant == REFERENCE_STATION) + { + printTextCenter("Base", 12 * 0, QW_FONT_8X16, 1, true); // string, y, font type, kerning, inverted + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("NTP", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("Cfg Eth", 12 * 3, QW_FONT_8X16, 1, false); + } + else if (online.accelerometer) + { + printTextCenter("Mark", 12 * 0, QW_FONT_8X16, 1, false); // string, y, font type, kerning, inverted + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("Base", 12 * 2, QW_FONT_8X16, 1, true); + printTextCenter("Bubble", 12 * 3, QW_FONT_8X16, 1, false); + } + else + { + printTextCenter("Mark", 12 * 0, QW_FONT_8X16, 1, false); // string, y, font type, kerning, inverted + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("Base", 12 * 2, QW_FONT_8X16, 1, true); + printTextCenter("Config", 12 * 3, QW_FONT_8X16, 1, false); + } + } + else if (setupState == STATE_NTPSERVER_NOT_STARTED) + { + { + printTextCenter("Base", 12 * 0, QW_FONT_8X16, 1, false); // string, y, font type, kerning, inverted + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("NTP", 12 * 2, QW_FONT_8X16, 1, true); + printTextCenter("Cfg Eth", 12 * 3, QW_FONT_8X16, 1, false); + } + } + else if (setupState == STATE_BUBBLE_LEVEL) + { + if (online.accelerometer) + { + printTextCenter("Mark", 12 * 0, QW_FONT_8X16, 1, false); // string, y, font type, kerning, inverted + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("Base", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("Bubble", 12 * 3, QW_FONT_8X16, 1, true); + } + else + { + // We should never get here, but just in case + printTextCenter("Mark", 12 * 0, QW_FONT_8X16, 1, false); // string, y, font type, kerning, inverted + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("Base", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("Config", 12 * 3, QW_FONT_8X16, 1, true); + } + } + else if (setupState == STATE_CONFIG_VIA_ETH_NOT_STARTED) + { + printTextCenter("Base", 12 * 0, QW_FONT_8X16, 1, false); // string, y, font type, kerning, inverted + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("NTP", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("Cfg Eth", 12 * 3, QW_FONT_8X16, 1, true); + } + else if (setupState == STATE_WIFI_CONFIG_NOT_STARTED) + { + if (productVariant == REFERENCE_STATION) + { + printTextCenter("Rover", 12 * 0, QW_FONT_8X16, 1, false); // string, y, font type, kerning, inverted + printTextCenter("NTP", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("Cfg Eth", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("CfgWiFi", 12 * 3, QW_FONT_8X16, 1, true); + } + else if (online.accelerometer) + { + printTextCenter("Rover", 12 * 0, QW_FONT_8X16, 1, false); + printTextCenter("Base", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("Bubble", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("Config", 12 * 3, QW_FONT_8X16, 1, true); + } + else + { + printTextCenter("Mark", 12 * 0, QW_FONT_8X16, 1, false); + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("Base", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("Config", 12 * 3, QW_FONT_8X16, 1, true); + } + } + + // If we are on an L-Band unit, display GetKeys option + else if (setupState == STATE_KEYS_NEEDED) + { + if (online.accelerometer) + { + printTextCenter("Base", 12 * 0, QW_FONT_8X16, 1, false); + printTextCenter("Bubble", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("Config", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("GetKeys", 12 * 3, QW_FONT_8X16, 1, true); + } + else + { + printTextCenter("Rover", 12 * 0, QW_FONT_8X16, 1, false); + printTextCenter("Base", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("Config", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("GetKeys", 12 * 3, QW_FONT_8X16, 1, true); + } + } + + else if (setupState == STATE_ESPNOW_PAIRING_NOT_STARTED) + { + if (productVariant == REFERENCE_STATION) + { + printTextCenter("NTP", 12 * 0, QW_FONT_8X16, 1, false); // string, y, font type, kerning, inverted + printTextCenter("Cfg Eth", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("CfgWiFi", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("E-Pair", 12 * 3, QW_FONT_8X16, 1, true); + } + else if (productVariant == RTK_FACET_LBAND || productVariant == RTK_FACET_LBAND_DIRECT) + { + // If we are on an L-Band unit, scroll GetKeys option + if (online.accelerometer) + { + printTextCenter("Bubble", 12 * 0, QW_FONT_8X16, 1, false); + printTextCenter("Config", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("GetKeys", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("E-Pair", 12 * 3, QW_FONT_8X16, 1, true); + } + else + { + printTextCenter("Base", 12 * 0, QW_FONT_8X16, 1, false); + printTextCenter("Config", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("GetKeys", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("E-Pair", 12 * 3, QW_FONT_8X16, 1, true); + } + } + else if (online.accelerometer) + { + printTextCenter("Base", 12 * 0, QW_FONT_8X16, 1, false); + printTextCenter("Bubble", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("Config", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("E-Pair", 12 * 3, QW_FONT_8X16, 1, true); + } + else + { + printTextCenter("Rover", 12 * 0, QW_FONT_8X16, 1, false); + printTextCenter("Base", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("Config", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("E-Pair", 12 * 3, QW_FONT_8X16, 1, true); + } + } + + else if (setupState == STATE_PROFILE) + paintDisplaySetupProfile("Base"); + } // end type F9P + else if (zedModuleType == PLATFORM_F9R) + { + if (setupState == STATE_MARK_EVENT) + { + if (online.accelerometer) + { + printTextCenter("Mark", 12 * 0, QW_FONT_8X16, 1, true); // string, y, font type, kerning, inverted + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("Bubble", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("Config", 12 * 3, QW_FONT_8X16, 1, false); + } + else + { + printTextCenter("Mark", 12 * 0, QW_FONT_8X16, 1, true); // string, y, font type, kerning, inverted + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("Config", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("E-Pair", 12 * 3, QW_FONT_8X16, 1, false); + } + } + else if (setupState == STATE_ROVER_NOT_STARTED) + { + if (online.accelerometer) + { + printTextCenter("Mark", 12 * 0, QW_FONT_8X16, 1, false); + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, true); + printTextCenter("Bubble", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("Config", 12 * 3, QW_FONT_8X16, 1, false); + } + else + { + printTextCenter("Mark", 12 * 0, QW_FONT_8X16, 1, false); + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, true); + printTextCenter("Config", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("E-Pair", 12 * 3, QW_FONT_8X16, 1, false); + } + } + else if (setupState == STATE_BUBBLE_LEVEL) + { + if (online.accelerometer) + { + printTextCenter("Mark", 12 * 0, QW_FONT_8X16, 1, false); + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("Bubble", 12 * 2, QW_FONT_8X16, 1, true); + printTextCenter("Config", 12 * 3, QW_FONT_8X16, 1, false); + } + else + { + // We should never get here, but just in case + printTextCenter("Mark", 12 * 0, QW_FONT_8X16, 1, false); + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("Config", 12 * 2, QW_FONT_8X16, 1, true); + printTextCenter("E-Pair", 12 * 3, QW_FONT_8X16, 1, false); + } + } + else if (setupState == STATE_WIFI_CONFIG_NOT_STARTED) + { + if (online.accelerometer) + { + printTextCenter("Mark", 12 * 0, QW_FONT_8X16, 1, false); + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("Bubble", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("Config", 12 * 3, QW_FONT_8X16, 1, true); + } + else + { + printTextCenter("Mark", 12 * 0, QW_FONT_8X16, 1, false); + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("Config", 12 * 2, QW_FONT_8X16, 1, true); + printTextCenter("E-Pair", 12 * 3, QW_FONT_8X16, 1, false); + } + } + else if (setupState == STATE_ESPNOW_PAIRING_NOT_STARTED) + { + if (online.accelerometer) + { + printTextCenter("Rover", 12 * 0, QW_FONT_8X16, 1, false); + printTextCenter("Bubble", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("Config", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("E-Pair", 12 * 3, QW_FONT_8X16, 1, true); + } + else + { + printTextCenter("Mark", 12 * 0, QW_FONT_8X16, 1, false); + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("Config", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("E-Pair", 12 * 3, QW_FONT_8X16, 1, true); + } + } + else if (setupState == STATE_PROFILE) + paintDisplaySetupProfile("Rover"); + } // end type F9R +} - paintWirelessIcon(); //Top left corner +// Given text, and location, print text center of the screen +void printTextCenter(const char *text, uint8_t yPos, QwiicFont &fontType, uint8_t kerning, + bool highlight) // text, y, font type, kearning, inverted +{ + oled.setFont(fontType); + oled.setDrawMode(grROPXOR); - paintBaseState(); //Top center + uint8_t fontWidth = fontType.width; + if (fontWidth == 8) + fontWidth = 7; // 8x16, but widest character is only 7 pixels. - int textX = 1; - int textY = 17; - int textKerning = 8; - oled.setFontType(1); - printTextwithKerning((char*)"Xmitting", textX, textY, textKerning); + uint8_t xStart = (oled.getWidth() / 2) - ((strlen(text) * (fontWidth + kerning)) / 2) + 1; - oled.setCursor(0, 39); //x, y - oled.setFontType(0); - oled.print("RTCM:"); + uint8_t xPos = xStart; + for (int x = 0; x < strlen(text); x++) + { + oled.setCursor(xPos, yPos); + oled.print(text[x]); + xPos += fontWidth + kerning; + } - if (rtcmPacketsSent < 100) - oled.setCursor(30, 36); //x, y - Give space for two digits - else - oled.setCursor(28, 36); //x, y - Push towards colon to make room for log icon + if (highlight) // Draw a box, inverted over text + { + uint8_t textPixelWidth = strlen(text) * (fontWidth + kerning); - oled.setFontType(1); //Set font to type 1: 8x16 - oled.print(rtcmPacketsSent); //rtcmPacketsSent is controlled in processRTCM() + // Error check + int xBoxStart = xStart - 5; + if (xBoxStart < 0) + xBoxStart = 0; + int xBoxEnd = textPixelWidth + 9; + if (xBoxEnd > oled.getWidth() - 1) + xBoxEnd = oled.getWidth() - 1; - paintLogging(); - } + oled.rectangleFill(xBoxStart, yPos, xBoxEnd, 12, 1); // x, y, width, height, color + } } -//Show connecting to caster service -//Solid WiFi icon -void paintBaseFixedCasterStarted() +// Given a message (one or two words) display centered +void displayMessage(const char *message, uint16_t displayTime) { - if (online.display == true) - { - paintBatteryLevel(); //Top right corner + if (online.display == true) + { + char temp[21]; + uint8_t fontHeight = 15; // Assume fontsize 1 + + // Count words based on spaces + uint8_t wordCount = 0; + strncpy(temp, message, sizeof(temp) - 1); // strtok modifies the message so make copy + char *token = strtok(temp, " "); + while (token != nullptr) + { + wordCount++; + token = strtok(nullptr, " "); + } - paintWirelessIcon(); //Top left corner + uint8_t yPos = (oled.getHeight() / 2) - (fontHeight / 2); + if (wordCount == 2) + yPos -= (fontHeight / 2); - paintBaseState(); //Top center + oled.erase(); - int textX = 11; - int textY = 18; - int textKerning = 8; + // drawFrame(); - printTextwithKerning((char*)"Caster", textX, textY, textKerning); + strncpy(temp, message, sizeof(temp) - 1); + token = strtok(temp, " "); + while (token != nullptr) + { + printTextCenter(token, yPos, QW_FONT_8X16, 1, false); // text, y, font type, kerning, inverted + token = strtok(nullptr, " "); + yPos += fontHeight; + } - textX = 3; - textY = 33; - textKerning = 6; - oled.setFontType(1); + oled.display(); - printTextwithKerning((char*)"Connecting", textX, textY, textKerning); - } + delay(displayTime); + } } -//Show transmission of RTCM packets to caster service -//Solid WiFi icon -void paintBaseFixedCasterConnected() +void paintResets() { - if (online.display == true) - { - paintBatteryLevel(); //Top right corner + if (settings.enableResetDisplay == true) + { + oled.setFont(QW_FONT_5X7); // Small font + oled.setCursor(16 + (8 * 3) + 7, 38); // x, y - paintWirelessIcon(); //Top left corner + if (settings.enablePrintBufferOverrun == false) + oled.print(settings.resetCount); + else + oled.print(settings.resetCount + bufferOverruns); + } +} - paintBaseState(); //Top center +// Wrapper to avoid needing to pass width/height data twice +void displayBitmap(uint8_t x, uint8_t y, uint8_t imageWidth, uint8_t imageHeight, const uint8_t *imageData) +{ + oled.bitmap(x, y, x + imageWidth, y + imageHeight, (uint8_t *)imageData, imageWidth, imageHeight); +} - int textX = 4; - int textY = 17; - int textKerning = 8; - oled.setFontType(1); - printTextwithKerning((char*)"Casting", textX, textY, textKerning); +void displayKeysUpdated() +{ + displayMessage("Keys Updated", 2000); +} - oled.setCursor(0, 39); //x, y - oled.setFontType(0); - oled.print("RTCM:"); +void paintKeyDaysRemaining(int daysRemaining, uint16_t displayTime) +{ + // 28 days + // until PP + // keys expire - if (rtcmPacketsSent < 100) - oled.setCursor(30, 36); //x, y - Give space for two digits - else - oled.setCursor(28, 36); //x, y - Push towards colon to make room for log icon + if (online.display == true) + { + oled.erase(); - oled.setFontType(1); //Set font to type 1: 8x16 - oled.print(rtcmPacketsSent); //rtcmPacketsSent is controlled in processRTCM() + if (daysRemaining < 0) + daysRemaining = 0; - paintLogging(); - } -} + int rightSideStart = 24; // Force the small text to rightside of screen -void displayBaseStart(uint16_t displayTime) -{ - if (online.display == true) - { - oled.clear(PAGE); + oled.setFont(QW_FONT_LARGENUM); - oled.setCursor(21, 13); - oled.setFontType(1); + String days = String(daysRemaining); + int dayTextWidth = oled.getStringWidth(days); - int textX = 18; - int textY = 10; - int textKerning = 8; + int largeTextX = (rightSideStart / 2) - (dayTextWidth / 2); // Center point for x coord - printTextwithKerning((char*)"Base", textX, textY, textKerning); + oled.setCursor(largeTextX, 0); + oled.print(daysRemaining); - oled.display(); + oled.setFont(QW_FONT_5X7); - delay(displayTime); - } -} + int x = ((oled.getWidth() - rightSideStart) / 2) + rightSideStart; // Center point for x coord + int y = 0; + int fontHeight = 10; + int textX; -void displayBaseSuccess(uint16_t displayTime) -{ - if (online.display == true) - { - oled.clear(PAGE); + textX = x - (oled.getStringWidth("days") / 2); // Starting point of text + oled.setCursor(textX, y); + oled.print("Days"); - oled.setCursor(21, 13); - oled.setFontType(1); + y += fontHeight; + textX = x - (oled.getStringWidth("Until") / 2); + oled.setCursor(textX, y); + oled.print("Until"); - int textX = 18; - int textY = 10; - int textKerning = 8; + y += fontHeight; + textX = x - (oled.getStringWidth("PP") / 2); + oled.setCursor(textX, y); + oled.print("PP"); - printTextwithKerning((char*)"Base", textX, textY, textKerning); + y += fontHeight; + textX = x - (oled.getStringWidth("Keys") / 2); + oled.setCursor(textX, y); + oled.print("Keys"); - textX = 5; - textY = 25; - textKerning = 8; - oled.setFontType(1); + y += fontHeight; + textX = x - (oled.getStringWidth("Expire") / 2); + oled.setCursor(textX, y); + oled.print("Expire"); - printTextwithKerning((char*)"Started", textX, textY, textKerning); - oled.display(); + oled.display(); - delay(displayTime); - } + delay(displayTime); + } } -void displayBaseFail(uint16_t displayTime) +void paintKeyWiFiFail(uint16_t displayTime) { - if (online.display == true) - { - oled.clear(PAGE); + // PP + // Update + // Failed + // No WiFi + + if (online.display == true) + { + oled.erase(); + + oled.setFont(QW_FONT_8X16); - oled.setCursor(21, 13); - oled.setFontType(1); + int y = 0; + int fontHeight = 13; - int textX = 18; - int textY = 10; - int textKerning = 8; + printTextCenter("PP", y, QW_FONT_8X16, 1, false); // text, y, font type, kerning, inverted - printTextwithKerning((char*)"Base", textX, textY, textKerning); + y += fontHeight; + printTextCenter("Update", y, QW_FONT_8X16, 1, false); // text, y, font type, kerning, inverted - textX = 10; - textY = 25; - textKerning = 8; - oled.setFontType(1); + y += fontHeight; + printTextCenter("Failed", y, QW_FONT_8X16, 1, false); // text, y, font type, kerning, inverted - printTextwithKerning((char*)"Failed", textX, textY, textKerning); - oled.display(); + y += fontHeight + 1; + printTextCenter("No WiFi", y, QW_FONT_5X7, 1, false); // text, y, font type, kerning, inverted - delay(displayTime); - } + oled.display(); + + delay(displayTime); + } } -void displayGNSSFail(uint16_t displayTime) +void paintNtripWiFiFail(uint16_t displayTime, bool Client) { - if (online.display == true) - { - oled.clear(PAGE); + // NTRIP + // Client or Server + // Failed + // No WiFi - oled.setCursor(21, 13); - oled.setFontType(1); + if (online.display == true) + { + oled.erase(); - int textX = 18; - int textY = 10; - int textKerning = 8; + int y = 0; + int fontHeight = 13; - printTextwithKerning((char*)"GNSS", textX, textY, textKerning); + const char *string = Client ? "Client" : "Server"; - textX = 10; - textY = 25; - textKerning = 8; - oled.setFontType(1); + printTextCenter("NTRIP", y, QW_FONT_8X16, 1, false); // text, y, font type, kerning, inverted - printTextwithKerning((char*)"Failed", textX, textY, textKerning); - oled.display(); + y += fontHeight; + printTextCenter(string, y, QW_FONT_8X16, 1, false); // text, y, font type, kerning, inverted - delay(displayTime); - } -} + y += fontHeight; + printTextCenter("Failed", y, QW_FONT_8X16, 1, false); // text, y, font type, kerning, inverted -void displayRoverStart(uint16_t displayTime) -{ - if (online.display == true) - { - oled.clear(PAGE); + y += fontHeight + 1; + printTextCenter("No WiFi", y, QW_FONT_5X7, 1, false); // text, y, font type, kerning, inverted - oled.setCursor(21, 13); - oled.setFontType(1); + oled.display(); - int textX = 14; - int textY = 10; - int textKerning = 8; + delay(displayTime); + } +} - printTextwithKerning((char*)"Rover", textX, textY, textKerning); +void paintKeysExpired() +{ + displayMessage("Keys Expired", 4000); +} - oled.display(); +void paintLBandConfigure() +{ + displayMessage("L-Band Config", 0); +} - delay(displayTime); - } +void paintGettingKeys() +{ + displayMessage("Getting Keys", 0); } -void displayRoverSuccess(uint16_t displayTime) +void paintGettingEthernetIP() { - if (online.display == true) - { - oled.clear(PAGE); + displayMessage("Getting IP", 0); +} - oled.setCursor(21, 13); - oled.setFontType(1); +// If an L-Band is indoors without reception, we have a ~2s wait for the RTC to come online +// Display something while we wait +void paintRTCWait() +{ + displayMessage("RTC Wait", 0); +} - int textX = 14; - int textY = 10; - int textKerning = 8; +void paintKeyProvisionFail(uint16_t displayTime) +{ + // Whitelist Error - printTextwithKerning((char*)"Rover", textX, textY, textKerning); + // ZTP + // Failed + // ID: + // 10chars - textX = 5; - textY = 25; - textKerning = 8; - oled.setFontType(1); + if (online.display == true) + { + oled.erase(); - printTextwithKerning((char*)"Started", textX, textY, textKerning); - oled.display(); + oled.setFont(QW_FONT_5X7); - delay(displayTime); - } -} + int y = 0; + int fontHeight = 8; -void displayRoverFail(uint16_t displayTime) -{ - if (online.display == true) - { - oled.clear(PAGE); + printTextCenter("ZTP", y, QW_FONT_5X7, 1, false); // text, y, font type, kerning, inverted + + y += fontHeight; + printTextCenter("Failed", y, QW_FONT_5X7, 1, false); // text, y, font type, kerning, inverted - oled.setCursor(21, 13); - oled.setFontType(1); + y += fontHeight; + printTextCenter("ID:", y, QW_FONT_5X7, 1, false); // text, y, font type, kerning, inverted - int textX = 14; - int textY = 10; - int textKerning = 8; + // The MAC address is 12 characters long so we have to split it onto two lines + char hardwareID[13]; + const uint8_t *rtkMacAddress = getMacAddress(); - printTextwithKerning((char*)"Rover", textX, textY, textKerning); + snprintf(hardwareID, sizeof(hardwareID), "%02X%02X%02X", rtkMacAddress[0], rtkMacAddress[1], rtkMacAddress[2]); + y += fontHeight; + printTextCenter(hardwareID, y, QW_FONT_5X7, 1, false); // text, y, font type, kerning, inverted - textX = 10; - textY = 25; - textKerning = 8; - oled.setFontType(1); + snprintf(hardwareID, sizeof(hardwareID), "%02X%02X%02X", rtkMacAddress[3], rtkMacAddress[4], rtkMacAddress[5]); + y += fontHeight; + printTextCenter(hardwareID, y, QW_FONT_5X7, 1, false); // text, y, font type, kerning, inverted - printTextwithKerning((char*)"Failed", textX, textY, textKerning); - oled.display(); + oled.display(); - delay(displayTime); - } + delay(displayTime); + } } -//When user enter serial config menu the display will freeze so show splash while config happens -void displaySerialConfig() +// Show screen while ESP-Now is pairing +void paintEspNowPairing() { - if (online.display == true) - { - oled.clear(PAGE); + displayMessage("ESP-Now Pairing", 0); +} +void paintEspNowPaired() +{ + displayMessage("ESP-Now Paired", 2000); +} - oled.setCursor(21, 13); - oled.setFontType(1); +void displayNtpStart(uint16_t displayTime) +{ + if (online.display == true) + { + oled.erase(); - int textX = 10; - int textY = 10; - int textKerning = 8; + uint8_t fontHeight = 15; + uint8_t yPos = oled.getHeight() / 2 - fontHeight; - printTextwithKerning((char*)"Serial", textX, textY, textKerning); + printTextCenter("NTP", yPos, QW_FONT_8X16, 1, false); // text, y, font type, kerning, inverted - textX = 10; - textY = 25; - textKerning = 8; - oled.setFontType(1); + oled.display(); - printTextwithKerning((char*)"Config", textX, textY, textKerning); - oled.display(); - } + delay(displayTime); + } } -void displaySurveyStart(uint16_t displayTime) +void displayNtpStarted(uint16_t displayTime) { - if (online.display == true) - { - oled.clear(PAGE); - - oled.setCursor(21, 13); - oled.setFontType(1); + if (online.display == true) + { + oled.erase(); - int textX = 10; - int textY = 10; - int textKerning = 8; + uint8_t fontHeight = 15; + uint8_t yPos = oled.getHeight() / 2 - fontHeight; - printTextwithKerning((char*)"Survey", textX, textY, textKerning); + printTextCenter("NTP", yPos, QW_FONT_8X16, 1, false); // text, y, font type, kerning, inverted + printTextCenter("Started", yPos + fontHeight, QW_FONT_8X16, 1, false); // text, y, font type, kerning, inverted - oled.display(); + oled.display(); - delay(displayTime); - } + delay(displayTime); + } } -void displaySurveyStarted(uint16_t displayTime) +void displayNtpNotReady(uint16_t displayTime) { - if (online.display == true) - { - oled.clear(PAGE); - - oled.setCursor(21, 13); - oled.setFontType(1); - - int textX = 10; - int textY = 10; - int textKerning = 8; + if (online.display == true) + { + oled.erase(); - printTextwithKerning((char*)"Survey", textX, textY, textKerning); + uint8_t fontHeight = 8; + uint8_t yPos = oled.getHeight() / 2 - fontHeight; - textX = 6; - textY = 25; - textKerning = 8; - oled.setFontType(1); + printTextCenter("Ethernet", yPos, QW_FONT_5X7, 1, false); // text, y, font type, kerning, inverted + printTextCenter("Not Ready", yPos + fontHeight, QW_FONT_5X7, 1, false); // text, y, font type, kerning, inverted - printTextwithKerning((char*)"Started", textX, textY, textKerning); - oled.display(); + oled.display(); - delay(displayTime); - } + delay(displayTime); + } } -//If the SD card is detected but is not formatted correctly, display warning -void displaySDFail(uint16_t displayTime) +void displayNTPFail(uint16_t displayTime) { - if (online.display == true) - { - oled.clear(PAGE); - - oled.setCursor(21, 13); - oled.setFontType(1); - - int textX = 11; - int textY = 10; - int textKerning = 8; + if (online.display == true) + { + oled.erase(); - printTextwithKerning((char*)"Format", textX, textY, textKerning); + uint8_t fontHeight = 8; + uint8_t yPos = oled.getHeight() / 2 - fontHeight; - textX = 7; - textY = 25; - textKerning = 8; - oled.setFontType(1); + printTextCenter("NTP", yPos, QW_FONT_5X7, 1, false); // text, y, font type, kerning, inverted + printTextCenter("Failed", yPos + fontHeight, QW_FONT_5X7, 1, false); // text, y, font type, kerning, inverted - printTextwithKerning((char*)"SD Card", textX, textY, textKerning); - oled.display(); + oled.display(); - delay(displayTime); - } + delay(displayTime); + } } -//Draw a frame at outside edge -void drawFrame() +void displayConfigViaEthNotStarted(uint16_t displayTime) { - //Init and draw box at edge to see screen alignment - int xMax = 63; - int yMax = 47; - oled.line(0, 0, xMax, 0); //Top - oled.line(0, 0, 0, yMax); //Left - oled.line(0, yMax, xMax, yMax); //Bottom - oled.line(xMax, 0, xMax, yMax); //Right + if (online.display == true) + { + oled.erase(); + + uint8_t fontHeight = 8; + uint8_t yPos = fontHeight; + + printTextCenter("Configure", yPos, QW_FONT_5X7, 1, false); // text, y, font type, kerning, inverted + yPos += fontHeight; + printTextCenter("Via", yPos, QW_FONT_5X7, 1, false); // text, y, font type, kerning, inverted + yPos += fontHeight; + printTextCenter("Ethernet", yPos, QW_FONT_5X7, 1, false); // text, y, font type, kerning, inverted + yPos += fontHeight; + printTextCenter("Restart", yPos, QW_FONT_5X7, 1, true); // text, y, font type, kerning, inverted + + oled.display(); + + delay(displayTime); + } } -//Display unit self-tests until user presses a button to exit -//Allows operator to check: -// Display alignment -// Internal connections to: SD, Accel, Fuel guage, GNSS -// External connections: Loop back test on DATA -void displayTest() +void displayConfigViaEthStarted(uint16_t displayTime) { - if (online.display == true) - { - int xOffset = 2; - int yOffset = 2; - - int charHeight = 7; - - inTestMode = true; //Reroutes bluetooth bytes - - char macAddress[5]; - sprintf(macAddress, "%02X%02X", unitMACAddress[4], unitMACAddress[5]); - - //Enable RTCM 1230. This is the GLONASS bias sentence and is transmitted - //even if there is no GPS fix. We use it to test serial output. - i2cGNSS.enableRTCMmessage(UBX_RTCM_1230, COM_PORT_UART2, 1); //Enable message every second - - oled.clear(PAGE); // Clear the display's internal memory - - drawFrame(); //Outside edge - - oled.setFontType(0); //Set font to smallest - oled.setCursor(xOffset, yOffset); //x, y - oled.print(F("Test Menu")); - - oled.display(); - - //Wait for user to stop pressing buttons - if (productVariant == RTK_EXPRESS) - { - while (digitalRead(pin_setupButton) == LOW || digitalRead(pin_powerSenseAndControl) == LOW) - delay(10); - } - else if (productVariant == RTK_FACET) - { - while (digitalRead(pin_powerSenseAndControl) == LOW) - delay(10); - } - - //For Surveyor, we need to monitor the rocker switch - ButtonState previousRockerSwitch = BUTTON_ROVER; - if (productVariant == RTK_SURVEYOR) - { - if (digitalRead(pin_baseSwitch) == LOW) //Switch is set to Base - previousRockerSwitch = BUTTON_BASE; - } - - //Update display until user presses the setup button - while (1) - { - //Check for user interaction - if (productVariant == RTK_EXPRESS) - { - if (digitalRead(pin_setupButton) == LOW) break; - } - else if (productVariant == RTK_FACET) - { - while (digitalRead(pin_powerSenseAndControl) == LOW) - delay(10); - } - else if (productVariant == RTK_SURVEYOR) - { - //Check if rocker switch moved - if (digitalRead(pin_baseSwitch) == HIGH && //Switch is set to Rover - previousRockerSwitch == BUTTON_BASE) break; - if (digitalRead(pin_baseSwitch) == LOW && //Switch is set to Base - previousRockerSwitch == BUTTON_ROVER) break; - } - - oled.clear(PAGE); // Clear the display's internal memory - - drawFrame(); //Outside edge - - //Test SD, accel, batt, GNSS, mux - oled.setFontType(0); //Set font to smallest - oled.setCursor(xOffset, yOffset); //x, y - oled.print(F("SD:")); - - if (online.microSD == false) - beginSD(); //Test if SD is present - if (online.microSD == true) - oled.print(F("OK")); - else - oled.print(F("FAIL")); - - oled.setCursor(xOffset, yOffset + (1 * charHeight) ); //x, y - oled.print(F("Accel:")); - if (online.accelerometer == true) - oled.print(F("OK")); - else - oled.print(F("FAIL")); - - oled.setCursor(xOffset, yOffset + (2 * charHeight) ); //x, y - oled.print(F("Batt:")); - if (online.battery == true) - oled.print(F("OK")); - else - oled.print(F("FAIL")); - - i2cGNSS.checkUblox(); - oled.setCursor(xOffset, yOffset + (3 * charHeight) ); //x, y - oled.print(F("GNSS:")); - int satsInView = i2cGNSS.getSIV(); - if (online.gnss == true && satsInView > 8) - { - oled.print(F("OK")); - oled.print(F("/")); - oled.print(satsInView); - } - else - oled.print(F("FAIL")); - - oled.setCursor(xOffset, yOffset + (4 * charHeight) ); //x, y - oled.print(F("Mux:")); - - //Set mux to channel 3 and toggle pin and verify with loop back jumper wire inserted by test technician - - setMuxport(MUX_ADC_DAC); //Set mux to DAC so we can toggle back/forth - pinMode(pin_dac26, OUTPUT); - pinMode(pin_adc39, INPUT_PULLUP); - - digitalWrite(pin_dac26, HIGH); - if (digitalRead(pin_adc39) == HIGH) - { - digitalWrite(pin_dac26, LOW); - if (digitalRead(pin_adc39) == LOW) - oled.print(F("OK")); - else - oled.print(F("FAIL")); - } - else - oled.print(F("FAIL")); + if (online.display == true) + { + oled.erase(); + + uint8_t fontHeight = 8; + uint8_t yPos = fontHeight; - //Display MAC address - oled.setCursor(xOffset, yOffset + (5 * charHeight) ); //x, y - oled.print(macAddress); - oled.print(":"); - if (incomingBTTest == 0) - oled.print(F("FAIL")); - else - { - oled.write(incomingBTTest); - oled.print(F("-OK")); - } + printTextCenter("Configure", yPos, QW_FONT_5X7, 1, false); // text, y, font type, kerning, inverted + yPos += fontHeight; + printTextCenter("Via", yPos, QW_FONT_5X7, 1, false); // text, y, font type, kerning, inverted + yPos += fontHeight; + printTextCenter("Ethernet", yPos, QW_FONT_5X7, 1, false); // text, y, font type, kerning, inverted + yPos += fontHeight; + printTextCenter("Started", yPos, QW_FONT_5X7, 1, false); // text, y, font type, kerning, inverted - //Display incoming BT characters + oled.display(); - oled.display(); - delay(250); + delay(displayTime); } +} - // Serial.println(F("Any character received over Blueooth connection will be displayed here")); +void displayConfigViaEthernet() +{ +#ifdef COMPILE_ETHERNET - inTestMode = false; //Reroutes bluetooth bytes + if (online.display == true) + { + oled.erase(); - setMuxport(settings.dataPortChannel); //Return mux to original channel + uint8_t xPos = (oled.getWidth() / 2) - (Ethernet_Icon_Width / 2); + uint8_t yPos = Ethernet_Icon_Height / 2; - //Disable RTCM sentences - i2cGNSS.enableRTCMmessage(UBX_RTCM_1230, COM_PORT_UART2, 0); + static bool blink = 0; + blink ^= 1; - oled.clear(PAGE); // Clear the display's internal memory + if (ETH.linkUp() || blink) + displayBitmap(xPos, yPos, Ethernet_Icon_Width, Ethernet_Icon_Height, Ethernet_Icon); - drawFrame(); //Outside edge + yPos += Ethernet_Icon_Height * 1.5; - oled.setFontType(0); //Set font to smallest - oled.setCursor(xOffset, yOffset); //x, y - oled.print(F("Stop Test")); + printTextCenter("IP:", yPos, QW_FONT_5X7, 1, false); // text, y, font type, kerning, inverted + yPos += 8; - oled.display(); + char ipAddress[40]; + IPAddress localIP = ETH.localIP(); + snprintf(ipAddress, sizeof(ipAddress), " %d.%d.%d.%d ", localIP[0], localIP[1], localIP[2], + localIP[3]); - //Wait for user to stop pressing buttons - if (productVariant == RTK_EXPRESS) - { - while (digitalRead(pin_setupButton) == LOW) - delay(10); - } - else if (productVariant == RTK_FACET) - { - while (digitalRead(pin_powerSenseAndControl) == LOW) - delay(10); - } + static uint8_t ipAddressPosition = 0; - delay(500); - } -} + // Print ten characters of IP address + char printThis[12]; -void displayForcedFirmwareUpdate() -{ - if (online.display == true) - { - oled.clear(PAGE); + // Check if the IP address is <= 10 chars and will fit without scrolling + if (strlen(ipAddress) <= 28) + ipAddressPosition = 9; + else if (strlen(ipAddress) <= 30) + ipAddressPosition = 10; - oled.setCursor(21, 13); - oled.setFontType(1); + snprintf(printThis, sizeof(printThis), "%c%c%c%c%c%c%c%c%c%c", ipAddress[ipAddressPosition + 0], + ipAddress[ipAddressPosition + 1], ipAddress[ipAddressPosition + 2], ipAddress[ipAddressPosition + 3], + ipAddress[ipAddressPosition + 4], ipAddress[ipAddressPosition + 5], ipAddress[ipAddressPosition + 6], + ipAddress[ipAddressPosition + 7], ipAddress[ipAddressPosition + 8], ipAddress[ipAddressPosition + 9]); - int textX = 11; - int textY = 10; - int textKerning = 8; + oled.setCursor(0, yPos); + oled.print(printThis); - printTextwithKerning((char*)"Forced", textX, textY, textKerning); + ipAddressPosition++; // Increment the print position + if (ipAddress[ipAddressPosition + 10] == 0) // Wrap + ipAddressPosition = 0; - textX = 11; - textY = 25; - textKerning = 8; - oled.setFontType(1); + oled.display(); + } + +#else // COMPILE_ETHERNET + uint8_t fontHeight = 15; + uint8_t yPos = oled.getHeight() / 2 - fontHeight; + printTextCenter("!Compiled", yPos, QW_FONT_5X7, 1, false); +#endif // COMPILE_ETHERNET +} - printTextwithKerning((char*)"Update", textX, textY, textKerning); - oled.display(); - } +const uint8_t *getMacAddress() +{ + static const uint8_t zero[6] = {0, 0, 0, 0, 0, 0}; + +#ifdef COMPILE_BT + if (bluetoothState != BT_OFF) + return btMACAddress; +#endif // COMPILE_BT +#ifdef COMPILE_WIFI + if (wifiState != WIFI_STATE_OFF) + return wifiMACAddress; +#endif // COMPILE_WIFI +#ifdef COMPILE_ETHERNET + if ((online.ethernetStatus >= ETH_STARTED_CHECK_CABLE) && (online.ethernetStatus <= ETH_CONNECTED)) + return ethernetMACAddress; +#endif // COMPILE_ETHERNET + return zero; } diff --git a/Firmware/RTK_Surveyor/ESPNOW.ino b/Firmware/RTK_Surveyor/ESPNOW.ino new file mode 100644 index 000000000..820c61cff --- /dev/null +++ b/Firmware/RTK_Surveyor/ESPNOW.ino @@ -0,0 +1,483 @@ +/* + Use ESP NOW protocol to transmit RTCM between RTK Products via 2.4GHz + + How pairing works: + 1. Device enters pairing mode + 2. Device adds the broadcast MAC (all 0xFFs) as peer + 3. Device waits for incoming pairing packet from remote + 4. If valid pairing packet received, add peer, immediately transmit a pairing packet to that peer and exit. + + ESP NOW is bare metal, there is no guaranteed packet delivery. For RTCM byte transmissions using ESP NOW: + We don't care about dropped packets or packets out of order. The ZED will check the integrity of the RTCM packet. + We don't care if the ESP NOW packet is corrupt or not. RTCM has its own CRC. RTK needs valid RTCM once every + few seconds so a single dropped frame is not critical. +*/ + +// Create a struct for ESP NOW pairing +typedef struct PairMessage +{ + uint8_t macAddress[6]; + bool encrypt; + uint8_t channel; + uint8_t crc; // Simple check - add MAC together and limit to 8 bit +} PairMessage; + +// Callback when data is sent +#ifdef COMPILE_ESPNOW +void espnowOnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) +{ + // systemPrint("Last Packet Send Status: "); + // if (status == ESP_NOW_SEND_SUCCESS) + // systemPrintln("Delivery Success"); + // else + // systemPrintln("Delivery Fail"); +} +#endif // COMPILE_ESPNOW + +// Callback when data is received +void espnowOnDataReceived(const uint8_t *mac, const uint8_t *incomingData, int len) +{ +#ifdef COMPILE_ESPNOW + if (espnowState == ESPNOW_PAIRING) + { + if (len == sizeof(PairMessage)) // First error check + { + PairMessage pairMessage; + memcpy(&pairMessage, incomingData, sizeof(pairMessage)); + + // Check CRC + uint8_t tempCRC = 0; + for (int x = 0; x < 6; x++) + tempCRC += pairMessage.macAddress[x]; + + if (tempCRC == pairMessage.crc) // 2nd error check + { + memcpy(&receivedMAC, pairMessage.macAddress, 6); + espnowSetState(ESPNOW_MAC_RECEIVED); + } + // else Pair CRC failed + } + } + else + { + espnowRSSI = packetRSSI; // Record this packets RSSI as an ESP NOW packet + + // Pass RTCM bytes (presumably) from ESP NOW out ESP32-UART2 to ZED-UART1 / SPI + if (USE_I2C_GNSS) + serialGNSS.write(incomingData, len); + else + theGNSS.pushRawData((uint8_t *)incomingData, len); + if (!inMainMenu) + log_d("ESPNOW received %d RTCM bytes, pushed to ZED, RSSI: %d", len, espnowRSSI); + + espnowIncomingRTCM = true; + lastEspnowRssiUpdate = millis(); + } +#endif // COMPILE_ESPNOW +} + +// Callback for all RX Packets +// Get RSSI of all incoming management packets: https://esp32.com/viewtopic.php?t=13889 +#ifdef COMPILE_ESPNOW +void promiscuous_rx_cb(void *buf, wifi_promiscuous_pkt_type_t type) +{ + // All espnow traffic uses action frames which are a subtype of the mgmnt frames so filter out everything else. + if (type != WIFI_PKT_MGMT) + return; + + const wifi_promiscuous_pkt_t *ppkt = (wifi_promiscuous_pkt_t *)buf; + packetRSSI = ppkt->rx_ctrl.rssi; +} +#endif // COMPILE_ESPNOW + +// If WiFi is already enabled, simply add the LR protocol +// If the radio is off entirely, start the radio, turn on only the LR protocol +void espnowStart() +{ +#ifdef COMPILE_ESPNOW + + esp_err_t response; + + if (wifiState == WIFI_STATE_OFF && espnowState == ESPNOW_OFF) + { + if (WiFi.getMode() != WIFI_STA) + WiFi.mode(WIFI_STA); + + // Radio is off, turn it on + // esp_wifi_set_protocol requires WiFi to be started + response = esp_wifi_set_protocol(WIFI_IF_STA, WIFI_PROTOCOL_LR); // Stops WiFi Station. + if (response != ESP_OK) + systemPrintf("espnowStart: Error setting ESP-Now lone protocol: %s\r\n", esp_err_to_name(response)); + else + log_d("WiFi off, ESP-Now added to protocols"); + } + // If WiFi is on but ESP NOW is off, then enable LR protocol + else if (wifiState > WIFI_STATE_OFF && espnowState == ESPNOW_OFF) + { + if (WiFi.getMode() != WIFI_STA) + WiFi.mode(WIFI_STA); + + // Enable WiFi + ESP-Now + // Enable long range, PHY rate of ESP32 will be 512Kbps or 256Kbps + // esp_wifi_set_protocol requires WiFi to be started + response = esp_wifi_set_protocol(WIFI_IF_STA, + WIFI_PROTOCOL_11B | WIFI_PROTOCOL_11G | WIFI_PROTOCOL_11N | WIFI_PROTOCOL_LR); + if (response != ESP_OK) + systemPrintf("espnowStart: Error setting ESP-Now + WiFi protocols: %s\r\n", esp_err_to_name(response)); + else + log_d("WiFi on, ESP-Now added to protocols"); + } + + // If ESP-Now is already active, do nothing + else + { + log_d("ESP-Now already on"); + } + + // Init ESP-NOW + if (esp_now_init() != ESP_OK) + { + systemPrintln("espnowStart: Error starting ESP-Now"); + return; + } + + // Use promiscuous callback to capture RSSI of packet + response = esp_wifi_set_promiscuous(true); + if (response != ESP_OK) + systemPrintf("espnowStart: Error setting promiscuous mode: %s\r\n", esp_err_to_name(response)); + + esp_wifi_set_promiscuous_rx_cb(&promiscuous_rx_cb); + + // Register callbacks + // esp_now_register_send_cb(espnowOnDataSent); + esp_now_register_recv_cb(espnowOnDataReceived); + + if (settings.espnowPeerCount == 0) + { + espnowSetState(ESPNOW_ON); + } + else + { + // If we already have peers, move to paired state + espnowSetState(ESPNOW_PAIRED); + + log_d("Adding %d espnow peers", settings.espnowPeerCount); + for (int x = 0; x < settings.espnowPeerCount; x++) + { + if (esp_now_is_peer_exist(settings.espnowPeers[x]) == true) + log_d("Peer already exists"); + else + { + esp_err_t result = espnowAddPeer(settings.espnowPeers[x]); + if (result != ESP_OK) + log_d("Failed to add peer #%d", x); + } + } + } + + if (settings.espnowBroadcast == true) + { + // Add broadcast peer if override is turned on + uint8_t broadcastMac[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + + if (esp_now_is_peer_exist(broadcastMac) == true) + log_d("Broadcast peer already exists"); + else + { + esp_err_t result = espnowAddPeer(broadcastMac, false); // Encryption not support for broadcast MAC + if (result != ESP_OK) + log_d("Failed to add broadcast peer"); + } + } + + systemPrintln("ESP-Now Started"); +#endif // COMPILE_ESPNOW +} + +// If WiFi is already enabled, simply remove the LR protocol +// If WiFi is off, stop the radio entirely +void espnowStop() +{ +#ifdef COMPILE_ESPNOW + if (espnowState == ESPNOW_OFF) + return; + + // Turn off promiscuous WiFi mode + esp_err_t response = esp_wifi_set_promiscuous(false); + if (response != ESP_OK) + systemPrintf("espnowStop: Failed to set promiscuous mode: %s\r\n", esp_err_to_name(response)); + + esp_wifi_set_promiscuous_rx_cb(nullptr); + + // Deregister callbacks + // esp_now_unregister_send_cb(); + response = esp_now_unregister_recv_cb(); + if (response != ESP_OK) + systemPrintf("espnowStop: Failed to unregister receive callback: %s\r\n", esp_err_to_name(response)); + + // Forget all ESP-Now Peers + for (int x = 0; x < settings.espnowPeerCount; x++) + espnowRemovePeer(settings.espnowPeers[x]); + + if (WiFi.getMode() != WIFI_STA) + WiFi.mode(WIFI_STA); + + // Leave WiFi with default settings (no WIFI_PROTOCOL_LR for ESP NOW) + // esp_wifi_set_protocol requires WiFi to be started + response = esp_wifi_set_protocol(WIFI_IF_STA, WIFI_PROTOCOL_11B | WIFI_PROTOCOL_11G | WIFI_PROTOCOL_11N); + if (response != ESP_OK) + systemPrintf("espnowStop: Error setting WiFi protocols: %s\r\n", esp_err_to_name(response)); + else + log_d("WiFi on, ESP-Now added to protocols"); + + // Deinit ESP-NOW + if (esp_now_deinit() != ESP_OK) + { + systemPrintln("Error deinitializing ESP-NOW"); + return; + } + + espnowSetState(ESPNOW_OFF); + + if (wifiState == WIFI_STATE_OFF) + { + // ESP Now was the only thing using the radio so turn WiFi radio off entirely + WiFi.mode(WIFI_OFF); + + log_d("WiFi Radio off entirely"); + } + // If WiFi is on, then restart WiFi + else if (wifiState > WIFI_STATE_OFF) + { + log_d("ESP-Now starting WiFi"); + wifiStart(); // Force WiFi to restart + } + +#endif // COMPILE_ESPNOW +} + +// Start ESP-Now if needed, put ESP-Now into broadcast state +void espnowBeginPairing() +{ + espnowStart(); + + // To begin pairing, we must add the broadcast MAC to the peer list + uint8_t broadcastMac[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + espnowAddPeer(broadcastMac, false); // Encryption is not supported for multicast addresses + + espnowSetState(ESPNOW_PAIRING); +} + +// Regularly call during pairing to see if we've received a Pairing message +bool espnowIsPaired() +{ +#ifdef COMPILE_ESPNOW + if (espnowState == ESPNOW_MAC_RECEIVED) + { + + if (settings.espnowBroadcast == false) + { + // Remove broadcast peer + uint8_t broadcastMac[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + espnowRemovePeer(broadcastMac); + } + + if (esp_now_is_peer_exist(receivedMAC) == true) + log_d("Peer already exists"); + else + { + // Add new peer to system + espnowAddPeer(receivedMAC); + + // Record this MAC to peer list + memcpy(settings.espnowPeers[settings.espnowPeerCount], receivedMAC, 6); + settings.espnowPeerCount++; + settings.espnowPeerCount %= ESPNOW_MAX_PEERS; + } + + // Send message directly to the received MAC (not unicast), then exit + espnowSendPairMessage(receivedMAC); + + // Enable radio. User may have arrived here from the setup menu rather than serial menu. + settings.radioType = RADIO_ESPNOW; + + recordSystemSettings(); // Record radioType and espnowPeerCount to NVM + + espnowSetState(ESPNOW_PAIRED); + return (true); + } +#endif // COMPILE_ESPNOW + return (false); +} + +// Create special pair packet to a given MAC +esp_err_t espnowSendPairMessage(uint8_t *sendToMac) +{ +#ifdef COMPILE_ESPNOW + // Assemble message to send + PairMessage pairMessage; + + // Get unit MAC address + memcpy(pairMessage.macAddress, wifiMACAddress, 6); + pairMessage.encrypt = false; + pairMessage.channel = 0; + + pairMessage.crc = 0; // Calculate CRC + for (int x = 0; x < 6; x++) + pairMessage.crc += wifiMACAddress[x]; + + return (esp_now_send(sendToMac, (uint8_t *)&pairMessage, sizeof(pairMessage))); // Send packet to given MAC +#else // COMPILE_ESPNOW + return (ESP_OK); +#endif // COMPILE_ESPNOW +} + +// Add a given MAC address to the peer list +esp_err_t espnowAddPeer(uint8_t *peerMac) +{ + return (espnowAddPeer(peerMac, true)); // Encrypt by default +} + +esp_err_t espnowAddPeer(uint8_t *peerMac, bool encrypt) +{ +#ifdef COMPILE_ESPNOW + esp_now_peer_info_t peerInfo; + + memcpy(peerInfo.peer_addr, peerMac, 6); + peerInfo.channel = 0; + peerInfo.ifidx = WIFI_IF_STA; + // memcpy(peerInfo.lmk, "RTKProductsLMK56", 16); + // peerInfo.encrypt = encrypt; + peerInfo.encrypt = false; + + esp_err_t result = esp_now_add_peer(&peerInfo); + if (result != ESP_OK) + log_d("Failed to add peer: 0x%02X%02X%02X%02X%02X%02X", peerMac[0], peerMac[1], peerMac[2], peerMac[3], + peerMac[4], peerMac[5]); + return (result); +#else // COMPILE_ESPNOW + return (ESP_OK); +#endif // COMPILE_ESPNOW +} + +// Remove a given MAC address from the peer list +esp_err_t espnowRemovePeer(uint8_t *peerMac) +{ +#ifdef COMPILE_ESPNOW + esp_err_t response = esp_now_del_peer(peerMac); + if (response != ESP_OK) + log_d("Failed to remove peer: %s", esp_err_to_name(response)); + + return (response); +#else // COMPILE_ESPNOW + return (ESP_OK); +#endif // COMPILE_ESPNOW +} + +// Update the state of the ESP Now state machine +void espnowSetState(ESPNOWState newState) +{ + if (espnowState == newState) + systemPrint("*"); + espnowState = newState; + + systemPrint("espnowState: "); + switch (newState) + { + case ESPNOW_OFF: + systemPrintln("ESPNOW_OFF"); + break; + case ESPNOW_ON: + systemPrintln("ESPNOW_ON"); + break; + case ESPNOW_PAIRING: + systemPrintln("ESPNOW_PAIRING"); + break; + case ESPNOW_MAC_RECEIVED: + systemPrintln("ESPNOW_MAC_RECEIVED"); + break; + case ESPNOW_PAIRED: + systemPrintln("ESPNOW_PAIRED"); + break; + default: + systemPrintf("Unknown ESPNOW state: %d\r\n", newState); + break; + } +} + +void espnowProcessRTCM(byte incoming) +{ +#ifdef COMPILE_ESPNOW + if (espnowState == ESPNOW_PAIRED) + { + // Move this byte into ESP NOW to send buffer + espnowOutgoing[espnowOutgoingSpot++] = incoming; + espnowLastAdd = millis(); + + if (espnowOutgoingSpot == sizeof(espnowOutgoing)) + { + espnowOutgoingSpot = 0; // Wrap + + if (settings.espnowBroadcast == false) + esp_now_send(0, (uint8_t *)&espnowOutgoing, sizeof(espnowOutgoing)); // Send packet to all peers + else + { + uint8_t broadcastMac[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + esp_now_send(broadcastMac, (uint8_t *)&espnowOutgoing, + sizeof(espnowOutgoing)); // Send packet via broadcast + } + + delay(10); // We need a small delay between sending multiple packets + + espnowBytesSent += sizeof(espnowOutgoing); + + espnowOutgoingRTCM = true; + } + } +#endif // COMPILE_ESPNOW +} + +// A blocking function that is used to pair two devices +// either through the serial menu or AP config +void espnowStaticPairing() +{ + systemPrintln("Begin ESP NOW Pairing"); + + // Start ESP-Now if needed, put ESP-Now into broadcast state + espnowBeginPairing(); + + // Begin sending our MAC every 250ms until a remote device sends us there info + randomSeed(millis()); + + systemPrintln("Begin pairing. Place other unit in pairing mode. Press any key to exit."); + clearBuffer(); + + uint8_t broadcastMac[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + + bool exitPair = false; + while (exitPair == false) + { + if (systemAvailable()) + { + systemPrintln("User pressed button. Pairing canceled."); + break; + } + + int timeout = 1000 + random(0, 100); // Delay 1000 to 1100ms + for (int x = 0; x < timeout; x++) + { + delay(1); + + if (espnowIsPaired() == true) // Check if we've received a pairing message + { + systemPrintln("Pairing compete"); + exitPair = true; + break; + } + } + + espnowSendPairMessage(broadcastMac); // Send unit's MAC address over broadcast, no ack, no encryption + + systemPrintln("Scanning for other radio..."); + } +} diff --git a/Firmware/RTK_Surveyor/Esp32Timer.h b/Firmware/RTK_Surveyor/Esp32Timer.h new file mode 100644 index 000000000..d2563ff8d --- /dev/null +++ b/Firmware/RTK_Surveyor/Esp32Timer.h @@ -0,0 +1,254 @@ +// Esp32Timer.h + +#ifndef __ESP32_TIMER_H__ +#define __ESP32_TIMER_H__ + +extern void systemPrintf(const char * format, ...); + +#define TIMG0 0x3ff5f000 +#define TIMG1 0x3ff60000 + +#define TIMG_T0CONFIG_REG 0x0000 // RW +#define TIMG_T0LO_REG 0x0004 // RO +#define TIMG_T0HI_REG 0x0008 // RO +#define TIMG_T0UPDATE_REG 0x000c // WO +#define TIMG_T0ALARMLO_REG 0x0010 // RW +#define TIMG_T0ALARMHI_REG 0x0014 // RW +#define TIMG_T0LOADLO_REG 0x0018 // RW +#define TIMG_T0LOADHI_REG 0x001c // RW +#define TIMG_T0LOAD_REG 0x0020 // WO + +#define TIMG_T1CONFIG_REG 0x0024 // RW +#define TIMG_T1LO_REG 0x0028 // RO +#define TIMG_T1HI_REG 0x002c // RO +#define TIMG_T1UPDATE_REG 0x0030 // WO +#define TIMG_T1ALARMLO_REG 0x0034 // RW +#define TIMG_T1ALARMHI_REG 0x0038 // RW +#define TIMG_T1LOADLO_REG 0x003c // RW +#define TIMG_T1LOADHI_REG 0x0040 // RW +#define TIMG_T1LOAD_REG 0x0044 // WO + +#define TIMG_T_WDTCONFIG0_REG 0x0048 // RW +#define TIMG_T_WDTCONFIG1_REG 0x004c // RW, Clock prescale * 12.5ns +#define TIMG_T_WDTCONFIG2_REG 0x0050 // RW +#define TIMG_T_WDTCONFIG3_REG 0x0054 // RW +#define TIMG_T_WDTCONFIG4_REG 0x0058 // RW +#define TIMG_T_WDTCONFIG5_REG 0x005c // RW +#define TIMG_T_WDTFEED_REG 0x0060 // WO +#define TIMG_T_WDTWPROTECT_REG 0x0064 // RW + +#define TIMG_RTCCALICFG_REG 0x0068 // varies +#define TIMG_RTCCALICFG1_REG 0x006c // RO + +#define TIMG_T_INT_ENA_REG 0x0098 // RW +#define TIMG_T_INT_RAW_REG 0x009c // RO +#define TIMG_T_INT_ST_REG 0x00a0 // RO +#define TIMG_T_INT_CLR_REG 0x00a4 // WO + +// TIMG_TxCONFIG_REG +#define TIMG_Tx_EN 0x80000000 // Enable the timer +#define TIMG_Tx_INCREASE 0x40000000 // Timer value increases every clock tick +#define TIMG_Tx_AUTORELOAD 0x20000000 // Reload timer upon alarm +#define TIMG_Tx_DIVIDER 0x1ffff000 // Clock prescale value +#define TIMG_Tx_EDGE_INT_EN 0x00000800 // Alarm generates edge interrupt +#define TIMG_Tx_LEVEL_INT_EN 0x00000400 // Alarm generates level interrupt +#define TIMG_Tx_ALARM_EN 0x00000200 // Alarm enable + +// TIMG_T_WDTCONFIG0_REG +#define TIMG_T_WDT_EN 0x80000000 // Enable MWDT + +#define TIMG_T_WDT_STG0 0x60000000 // Stage 0 configuration +#define TIMG_T_WDT_STG0_RST_SYSTEM 0x60000000 +#define TIMG_T_WDT_STG0_RST_CPU 0x40000000 +#define TIMG_T_WDT_STG0_INTERRUPT 0x20000000 +#define TIMG_T_WDT_STG0_OFF 0x00000000 + +#define TIMG_T_WDT_STG1 0x18000000 // Stage 1 configuration +#define TIMG_T_WDT_STG1_RST_SYSTEM 0x18000000 +#define TIMG_T_WDT_STG1_RST_CPU 0x10000000 +#define TIMG_T_WDT_STG1_INTERRUPT 0x08000000 +#define TIMG_T_WDT_STG1_OFF 0x00000000 + +#define TIMG_T_WDT_STG2 0x06000000 // Stage 2 configuration +#define TIMG_T_WDT_STG2_RST_SYSTEM 0x06000000 +#define TIMG_T_WDT_STG2_RST_CPU 0x04000000 +#define TIMG_T_WDT_STG2_INTERRUPT 0x02000000 +#define TIMG_T_WDT_STG2_OFF 0x00000000 + +#define TIMG_T_WDT_STG3 0x01800000 // Stage 3 configuration +#define TIMG_T_WDT_STG3_RST_SYSTEM 0x01800000 +#define TIMG_T_WDT_STG3_RST_CPU 0x01000000 +#define TIMG_T_WDT_STG3_INTERRUPT 0x00800000 +#define TIMG_T_WDT_STG3_OFF 0x00000000 + +#define TIMG_T_WDT_EDGE_INT_EN 0x00400000 // Enable edge interrupts +#define TIMG_T_WDT_LEVEL_INT_EN 0x00200000 // Enable level interrupts + +#define TIMG_T_WDT_CPU_RESET_LENGTH 0x001c0000 // CPU reset pulse width +#define TIMG_T_WDT_CPU_RESET_3200ns 0x001c0000 +#define TIMG_T_WDT_CPU_RESET_1600ns 0x00180000 +#define TIMG_T_WDT_CPU_RESET_800ns 0x00140000 +#define TIMG_T_WDT_CPU_RESET_500ns 0x00100000 +#define TIMG_T_WDT_CPU_RESET_400ns 0x000c0000 +#define TIMG_T_WDT_CPU_RESET_300ns 0x00080000 +#define TIMG_T_WDT_CPU_RESET_200ns 0x00040000 +#define TIMG_T_WDT_CPU_RESET_100ns 0x00000000 + +#define TIMG_T_WDT_SYS_RESET_LENGTH 0x00038000 // System reset pulse width +#define TIMG_T_WDT_SYS_RESET_3200ns 0x00038000 +#define TIMG_T_WDT_SYS_RESET_1600ns 0x00030000 +#define TIMG_T_WDT_SYS_RESET_800ns 0x00028000 +#define TIMG_T_WDT_SYS_RESET_500ns 0x00020000 +#define TIMG_T_WDT_SYS_RESET_400ns 0x00018000 +#define TIMG_T_WDT_SYS_RESET_300ns 0x00010000 +#define TIMG_T_WDT_SYS_RESET_200ns 0x00008000 +#define TIMG_T_WDT_SYS_RESET_100ns 0x00000000 + +#define TIMG_T_WDT_FLASHBOOT_MOD_EN 0x00004000 + +double printClockPeriod(uint32_t config) +{ + uint32_t clocks; + double clockPeriod; + double multiplier; + const char * units; + + clocks = config >> 16; + clockPeriod = 0.0000000125 * clocks; + if (clockPeriod >= 1.) + { + units = "Sec"; + multiplier = 1; + } + else if (clockPeriod >= 0.001) + { + units = "mSec"; + multiplier = 1000.; + } + else if (clockPeriod >= 0.000001) + { + units = "uSec"; + multiplier = 1000000; + } + else + { + units = "nSec"; + multiplier = 1000000000.; + } + systemPrintf(" Clock period: %7.3f %s (%d - 12.5 nSec clocks)", clockPeriod * multiplier, units, clocks); + return clockPeriod; +} + +void printWdtTimeout(double clockPeriod, uint32_t clocks) +{ + double multiplier; + double timeout; + const char * units; + + timeout = clockPeriod * (double)clocks; + if (timeout >= 1.) + { + units = "Sec"; + multiplier = 1; + } + else if (timeout >= 0.001) + { + units = "mSec"; + multiplier = 1000.; + } + else if (timeout >= 0.000000) + { + units = "uSec"; + multiplier = 1000000.; + } + else + { + units = "nSec"; + multiplier = 1000000000.; + } + systemPrintf(", timeout: %5.1f %s (%d clocks)\r\n", timeout * multiplier, units, clocks); +} + +void printWdt(intptr_t baseAddress) +{ + double clockPeriod; + const char * const config[] = + { + "Off", + "Interrupt", + "Reset CPU", + "Reset System" + }; + uint32_t protect; + const int pulseWidth[] = {100, 200, 300, 400, 500, 800, 1600, 3200}; + uint32_t value[6]; + + systemPrintf("0x%08x: Watch Dog Timer\r\n", baseAddress); + value[0] = *(uint32_t *)(baseAddress + TIMG_T_WDTCONFIG0_REG); + systemPrintf(" 0x%08x: TIMG_T_WDTCONFIG0_REG\r\n", value[0]); + value[1] = *(uint32_t *)(baseAddress + TIMG_T_WDTCONFIG1_REG); + systemPrintf(" 0x%08x: TIMG_T_WDTCONFIG1_REG\r\n", value[1]); + value[2] = *(uint32_t *)(baseAddress + TIMG_T_WDTCONFIG2_REG); + systemPrintf(" 0x%08x: TIMG_T_WDTCONFIG2_REG\r\n", value[2]); + value[3] = *(uint32_t *)(baseAddress + TIMG_T_WDTCONFIG3_REG); + systemPrintf(" 0x%08x: TIMG_T_WDTCONFIG3_REG\r\n", value[3]); + value[4] = *(uint32_t *)(baseAddress + TIMG_T_WDTCONFIG4_REG); + systemPrintf(" 0x%08x: TIMG_T_WDTCONFIG4_REG\r\n", value[4]); + value[5] = *(uint32_t *)(baseAddress + TIMG_T_WDTCONFIG5_REG); + systemPrintf(" 0x%08x: TIMG_T_WDTCONFIG5_REG\r\n", value[5]); + protect = *(uint32_t *)(baseAddress + TIMG_T_WDTWPROTECT_REG); + systemPrintf(" 0x%08x: TIMG_T_WDTWPROTECT_REG\r\n", protect); + + if (value[0] & TIMG_T_WDT_EN) + { + // TIMG_T_WDTCONFIG0_REG + systemPrintf(" Watch dog enabled\r\n"); + clockPeriod = printClockPeriod(value[1]); + systemPrintf(" Stage %d: %s", 0, config[(value[0] >> 29) & 3]); + if (value[0] & TIMG_T_WDT_STG0_RST_SYSTEM) + printWdtTimeout(clockPeriod, value[2]); + systemPrintf("\r\n"); + systemPrintf(" Stage %d: %s", 1, config[(value[0] >> 27) & 3]); + if (value[0] & TIMG_T_WDT_STG1_RST_SYSTEM) + printWdtTimeout(clockPeriod, value[3]); + systemPrintf("\r\n"); + systemPrintf(" Stage %d: %s", 2, config[(value[0] >> 25) & 3]); + if (value[0] & TIMG_T_WDT_STG2_RST_SYSTEM) + printWdtTimeout(clockPeriod, value[4]); + systemPrintf("\r\n"); + systemPrintf(" Stage %d: %s", 3, config[(value[0] >> 23) & 3]); + if (value[0] & TIMG_T_WDT_STG3_RST_SYSTEM) + printWdtTimeout(clockPeriod, value[5]); + systemPrintf("\r\n"); + if ((value[0] & TIMG_T_WDT_STG0_INTERRUPT) + || (value[0] & TIMG_T_WDT_STG1_INTERRUPT) + || (value[0] & TIMG_T_WDT_STG2_INTERRUPT) + || (value[0] & TIMG_T_WDT_STG3_INTERRUPT)) + { + if (value[0] & TIMG_T_WDT_EDGE_INT_EN) + systemPrintf(" Generate edge interrupt\r\n"); + if (value[0] & TIMG_T_WDT_LEVEL_INT_EN) + systemPrintf(" Generate level interrupt\r\n"); + } + if (((value[0] & TIMG_T_WDT_STG0_RST_SYSTEM) == TIMG_T_WDT_STG0_RST_CPU) + || ((value[0] & TIMG_T_WDT_STG1_RST_SYSTEM) == TIMG_T_WDT_STG1_RST_CPU) + || ((value[0] & TIMG_T_WDT_STG2_RST_SYSTEM) == TIMG_T_WDT_STG2_RST_CPU) + || ((value[0] & TIMG_T_WDT_STG3_RST_SYSTEM) == TIMG_T_WDT_STG3_RST_CPU)) + { + systemPrintf(" CPU reset pulse: %d nSec\r\n", pulseWidth[(value[0] >> 18) & 7]); + } + if (((value[0] & TIMG_T_WDT_STG0_RST_SYSTEM) == TIMG_T_WDT_STG0_RST_SYSTEM) + || ((value[0] & TIMG_T_WDT_STG1_RST_SYSTEM) == TIMG_T_WDT_STG1_RST_SYSTEM) + || ((value[0] & TIMG_T_WDT_STG2_RST_SYSTEM) == TIMG_T_WDT_STG2_RST_SYSTEM) + || ((value[0] & TIMG_T_WDT_STG3_RST_SYSTEM) == TIMG_T_WDT_STG3_RST_SYSTEM)) + { + systemPrintf(" System reset pulse: %d nSec\r\n", pulseWidth[(value[0] >> 15) & 7]); + } + if (value[0] & TIMG_T_WDT_FLASHBOOT_MOD_EN) + systemPrintf(" Flash boot protection enabled\r\n"); + } + else + systemPrintf(" Watch dog disabled\r\n"); +} + +#endif // __ESP32_TIMER_H__ diff --git a/Firmware/RTK_Surveyor/Ethernet.ino b/Firmware/RTK_Surveyor/Ethernet.ino new file mode 100644 index 000000000..9273715b7 --- /dev/null +++ b/Firmware/RTK_Surveyor/Ethernet.ino @@ -0,0 +1,385 @@ +#ifdef COMPILE_ETHERNET + +// Get the Ethernet parameters +void menuEthernet() +{ + if (!HAS_ETHERNET) + { + clearBuffer(); // Empty buffer of any newline chars + return; + } + + bool restartEthernet = false; + + while (1) + { + systemPrintln(); + systemPrintln("Menu: Ethernet"); + systemPrintln(); + + systemPrint("1) Ethernet Config: "); + if (settings.ethernetDHCP) + systemPrintln("DHCP"); + else + systemPrintln("Fixed IP"); + + if (!settings.ethernetDHCP) + { + systemPrint("2) Fixed IP Address: "); + systemPrintln(settings.ethernetIP.toString().c_str()); + systemPrint("3) DNS: "); + systemPrintln(settings.ethernetDNS.toString().c_str()); + systemPrint("4) Gateway: "); + systemPrintln(settings.ethernetGateway.toString().c_str()); + systemPrint("5) Subnet Mask: "); + systemPrintln(settings.ethernetSubnet.toString().c_str()); + } + + systemPrintln("x) Exit"); + + byte incoming = getCharacterNumber(); + + if (incoming == 1) + { + settings.ethernetDHCP ^= 1; + restartEthernet = true; + } + else if ((!settings.ethernetDHCP) && (incoming == 2)) + { + systemPrint("Enter new IP Address: "); + char tempStr[20]; + if (getIPAddress(tempStr, sizeof(tempStr)) == INPUT_RESPONSE_VALID) + { + String tempString = String(tempStr); + settings.ethernetIP.fromString(tempString); + restartEthernet = true; + } + else + systemPrint("Error: invalid IP Address"); + } + else if ((!settings.ethernetDHCP) && (incoming == 3)) + { + systemPrint("Enter new DNS: "); + char tempStr[20]; + if (getIPAddress(tempStr, sizeof(tempStr)) == INPUT_RESPONSE_VALID) + { + String tempString = String(tempStr); + settings.ethernetDNS.fromString(tempString); + restartEthernet = true; + } + else + systemPrint("Error: invalid DNS"); + } + else if ((!settings.ethernetDHCP) && (incoming == 4)) + { + systemPrint("Enter new Gateway: "); + char tempStr[20]; + if (getIPAddress(tempStr, sizeof(tempStr)) == INPUT_RESPONSE_VALID) + { + String tempString = String(tempStr); + settings.ethernetGateway.fromString(tempString); + restartEthernet = true; + } + else + systemPrint("Error: invalid Gateway"); + } + else if ((!settings.ethernetDHCP) && (incoming == 5)) + { + systemPrint("Enter new Subnet Mask: "); + char tempStr[20]; + if (getIPAddress(tempStr, sizeof(tempStr)) == INPUT_RESPONSE_VALID) + { + String tempString = String(tempStr); + settings.ethernetSubnet.fromString(tempString); + restartEthernet = true; + } + else + systemPrint("Error: invalid Subnet Mask"); + } + else if (incoming == 'x') + break; + else if (incoming == INPUT_RESPONSE_GETCHARACTERNUMBER_EMPTY) + break; + else if (incoming == INPUT_RESPONSE_GETCHARACTERNUMBER_TIMEOUT) + break; + else + printUnknown(incoming); + } + + clearBuffer(); // Empty buffer of any newline chars + + if (restartEthernet) // Restart Ethernet to use the new ethernet settings + { + ethernetRestart(); + } +} + +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +// Ethernet routines +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +// Regularly called to update the Ethernet status +void ethernetBegin() +{ + if (HAS_ETHERNET == false) + return; + + // Skip if going into configure-via-ethernet mode + if (configureViaEthernet) + { + log_d("configureViaEthernet: skipping ethernetBegin"); + return; + } + + if (!ethernetIsNeeded()) + return; + + if (PERIODIC_DISPLAY(PD_ETHERNET_STATE)) + { + PERIODIC_CLEAR(PD_ETHERNET_STATE); + ethernetDisplayState(); + systemPrintln(); + } + switch (online.ethernetStatus) + { + case (ETH_NOT_STARTED): + Ethernet.init(pin_Ethernet_CS); + + // First we start Ethernet without DHCP to detect if a cable is connected + // DHCP causes system freeze for ~62 seconds so we avoid it until a cable is connected + Ethernet.begin(ethernetMACAddress, settings.ethernetIP, settings.ethernetDNS, settings.ethernetGateway, + settings.ethernetSubnet); + + if (Ethernet.hardwareStatus() == EthernetNoHardware) // Check that a W5n00 has been detected + { + log_d("Ethernet hardware not found"); + online.ethernetStatus = ETH_CAN_NOT_BEGIN; + return; + } + + online.ethernetStatus = ETH_STARTED_CHECK_CABLE; + lastEthernetCheck = millis(); // Wait a full second before checking the cable + + break; + + case (ETH_STARTED_CHECK_CABLE): + if (millis() - lastEthernetCheck > 1000) // Check for cable every second + { + lastEthernetCheck = millis(); + + if (Ethernet.linkStatus() == LinkON) + { + log_d("Ethernet cable detected"); + + if (settings.ethernetDHCP) + { + paintGettingEthernetIP(); + online.ethernetStatus = ETH_STARTED_START_DHCP; + } + else + { + systemPrintln("Ethernet started with static IP"); + online.ethernetStatus = ETH_CONNECTED; + } + } + else + { + // log_d("No cable detected"); + } + } + break; + + case (ETH_STARTED_START_DHCP): + if (millis() - lastEthernetCheck > 1000) // Try DHCP every second + { + lastEthernetCheck = millis(); + + if (Ethernet.begin(ethernetMACAddress, 20000)) // Restart Ethernet with DHCP. Use 20s timeout + { + log_d("Ethernet started with DHCP"); + online.ethernetStatus = ETH_CONNECTED; + } + } + break; + + case (ETH_CONNECTED): + if (Ethernet.linkStatus() == LinkOFF) + { + log_d("Ethernet cable disconnected!"); + online.ethernetStatus = ETH_STARTED_CHECK_CABLE; + } + break; + + case (ETH_CAN_NOT_BEGIN): + break; + + default: + log_d("Unknown status"); + break; + } +} + +// Display the Ethernet state +void ethernetDisplayState() +{ + if (online.ethernetStatus >= ethernetStateEntries) + systemPrint("UNKNOWN"); + else + systemPrint(ethernetStates[online.ethernetStatus]); +} + +// Return the IP address for the Ethernet controller +IPAddress ethernetGetIpAddress() +{ + return Ethernet.localIP(); +} + +// Determine if Ethernet is needed. Saves RAM... +bool ethernetIsNeeded() +{ + // Does NTP need Ethernet? + if (systemState >= STATE_NTPSERVER_NOT_STARTED && systemState <= STATE_NTPSERVER_SYNC) + return true; + + // Does Base mode NTRIP Server need Ethernet? + if (settings.enableNtripServer == true && + (systemState >= STATE_BASE_NOT_STARTED && systemState <= STATE_BASE_FIXED_TRANSMITTING) + ) + return true; + + // Does Rover mode NTRIP Client need Ethernet? + if (settings.enableNtripClient == true && + (systemState >= STATE_ROVER_NOT_STARTED && systemState <= STATE_ROVER_RTK_FIX) + ) + return true; + + // Does PVT client or server need Ethernet? + if (settings.enablePvtClient || settings.enablePvtServer + || settings.enablePvtUdpServer || settings.enableAutoFirmwareUpdate) + return true; + + return false; +} + +// Ethernet (W5500) ISR +// Triggered by the falling edge of the W5500 interrupt signal - indicates the arrival of a packet +// Record the time the packet arrived +void ethernetISR() +{ + // Don't check or clear the interrupt here - + // it may clash with a GNSS SPI transaction and cause a wdt timeout. + // Do it in updateEthernet + gettimeofday((timeval *)ðernetNtpTv, nullptr); // Record the time of the NTP interrupt +} + +// Restart the Ethernet controller +void ethernetRestart() +{ + // Reset online.ethernetStatus so ethernetBegin will call Ethernet.begin to use the new settings + online.ethernetStatus = ETH_NOT_STARTED; + + // NTP Server + ntpServerStop(); + + // NTRIP? +} + +// Update the Ethernet state +void ethernetUpdate() +{ + // Skip if in configure-via-ethernet mode + if (configureViaEthernet) + { + // log_d("configureViaEthernet: skipping updateEthernet"); + return; + } + + if (!HAS_ETHERNET) + return; + + if (online.ethernetStatus == ETH_CAN_NOT_BEGIN) + return; + + ethernetBegin(); // This updates the link status + + // Maintain the ethernet connection + if ((online.ethernetStatus >= ETH_STARTED_CHECK_CABLE) && (online.ethernetStatus <= ETH_CONNECTED)) + switch (Ethernet.maintain()) + { + case 1: + // renewed fail + if (settings.enablePrintEthernetDiag && (!inMainMenu)) + systemPrintln("Ethernet: Error: renewed fail"); + ethernetRestart(); // Restart Ethernet + break; + + case 2: + // renewed success + if (settings.enablePrintEthernetDiag && (!inMainMenu)) + { + systemPrint("Ethernet: Renewed success. IP address: "); + systemPrintln(Ethernet.localIP()); + } + break; + + case 3: + // rebind fail + if (settings.enablePrintEthernetDiag && (!inMainMenu)) + systemPrintln("Ethernet: Error: rebind fail"); + ethernetRestart(); // Restart Ethernet + break; + + case 4: + // rebind success + if (settings.enablePrintEthernetDiag && (!inMainMenu)) + { + systemPrint("Ethernet: Rebind success. IP address: "); + systemPrintln(Ethernet.localIP()); + } + break; + + default: + // nothing happened + break; + } +} + +// Verify the Ethernet tables +void ethernetVerifyTables() +{ + // Verify the table lengths + if (ethernetStateEntries != ETH_MAX_STATE) + reportFatalError("Please fix ethernetStates table to match ethernetStatus_e"); +} + +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +// Web server routines +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +// Start Ethernet WebServer ESP32 W5500 - needs exclusive access to WiFi, SPI and Interrupts +void ethernetWebServerStartESP32W5500() +{ + // Configure the W5500 + // To be called before ETH.begin() + ESP32_W5500_onEvent(); + + // start the ethernet connection and the server: + // Use DHCP dynamic IP + // bool begin(int POCI_GPIO, int PICO_GPIO, int SCLK_GPIO, int CS_GPIO, int INT_GPIO, int SPI_CLOCK_MHZ, + // int SPI_HOST, uint8_t *W5500_Mac = W5500_Default_Mac, bool installIsrService = true); + ETH.begin(pin_POCI, pin_PICO, pin_SCK, pin_Ethernet_CS, pin_Ethernet_Interrupt, 25, SPI3_HOST, ethernetMACAddress); + + if (!settings.ethernetDHCP) + ETH.config(settings.ethernetIP, settings.ethernetGateway, settings.ethernetSubnet, settings.ethernetDNS); + + if (ETH.linkUp()) + ESP32_W5500_waitForConnect(); +} + +// Stop the Ethernet web server +void ethernetWebServerStopESP32W5500() +{ + ETH.end(); // This is _really_ important. It undoes the low-level changes to SPI and interrupts +} + +#endif // COMPILE_ETHERNET diff --git a/Firmware/RTK_Surveyor/FileSdFatMMC.h b/Firmware/RTK_Surveyor/FileSdFatMMC.h new file mode 100644 index 000000000..a1dbc2a36 --- /dev/null +++ b/Firmware/RTK_Surveyor/FileSdFatMMC.h @@ -0,0 +1,239 @@ +// Define a hybrid class which can support both SdFat SdFile and SD_MMC File + +#ifdef COMPILE_SD_MMC + +// #include "FS.h" +#include "SD_MMC.h" //Also includes FS.h + +class FileSdFatMMC : public SdFile, public File + +#else // COMPILE_SD_MMC + +class FileSdFatMMC : public SdFile + +#endif // COMPILE_SD_MMC + +{ + public: + FileSdFatMMC() + { + if (USE_SPI_MICROSD) + _sdFile = new SdFile; +#ifdef COMPILE_SD_MMC + else + _file = new File; +#endif // COMPILE_SD_MMC + }; + + ~FileSdFatMMC() + { + if (USE_SPI_MICROSD) + { + ; + // if (_sdFile) //operator bool + // delete _sdFile; + } +#ifdef COMPILE_SD_MMC + else + { + ; + // if (_file) //operator bool + // delete _file; + } +#endif // COMPILE_SD_MMC + }; + + operator bool() + { + if (USE_SPI_MICROSD) + return _sdFile; +#ifdef COMPILE_SD_MMC + else + return _file; +#endif // COMPILE_SD_MMC + return false; // Keep the compiler happy + }; + + size_t println(const char printMe[]) + { + if (USE_SPI_MICROSD) + return _sdFile->println(printMe); +#ifdef COMPILE_SD_MMC + else + return _file->println(printMe); +#endif // COMPILE_SD_MMC + return 0; // Keep the compiler happy + }; + + bool open(const char *filepath, oflag_t mode) + { + if (USE_SPI_MICROSD) + { + if (_sdFile->open(filepath, mode) == true) + return true; + return false; + } +#ifdef COMPILE_SD_MMC + else + { + if (mode & O_APPEND) + *_file = SD_MMC.open(filepath, FILE_APPEND); + else if (mode & O_WRITE) + *_file = SD_MMC.open(filepath, FILE_WRITE); + else // if (mode & O_READ) + *_file = SD_MMC.open(filepath, FILE_READ); + if (_file) // operator bool + return true; + return false; + } +#endif // COMPILE_SD_MMC + return false; // Keep the compiler happy + }; + + uint32_t size() + { + if (USE_SPI_MICROSD) + return _sdFile->size(); +#ifdef COMPILE_SD_MMC + else + return _file->size(); +#endif // COMPILE_SD_MMC + return 0; // Keep the compiler happy + }; + + uint32_t position() + { + if (USE_SPI_MICROSD) + return _sdFile->position(); +#ifdef COMPILE_SD_MMC + else + return _file->position(); +#endif // COMPILE_SD_MMC + return 0; // Keep the compiler happy + }; + + int available() + { + if (USE_SPI_MICROSD) + return _sdFile->available(); +#ifdef COMPILE_SD_MMC + else + return _file->available(); +#endif // COMPILE_SD_MMC + return 0; // Keep the compiler happy + }; + + int read(uint8_t *buf, uint16_t nbyte) + { + if (USE_SPI_MICROSD) + return _sdFile->read(buf, nbyte); +#ifdef COMPILE_SD_MMC + else + return _file->read(buf, nbyte); +#endif // COMPILE_SD_MMC + return 0; // Keep the compiler happy + }; + + size_t write(const uint8_t *buf, size_t size) + { + if (USE_SPI_MICROSD) + return _sdFile->write(buf, size); +#ifdef COMPILE_SD_MMC + else + return _file->write(buf, size); +#endif // COMPILE_SD_MMC + return 0; // Keep the compiler happy + }; + + void close() + { + if (USE_SPI_MICROSD) + _sdFile->close(); +#ifdef COMPILE_SD_MMC + else + _file->close(); +#endif // COMPILE_SD_MMC + }; + + void flush() + { + if (USE_SPI_MICROSD) + _sdFile->sync(); +#ifdef COMPILE_SD_MMC + else + _file->flush(); +#endif // COMPILE_SD_MMC + }; + + void updateFileAccessTimestamp() + { + if (USE_SPI_MICROSD) + { + if (online.rtc == true) + { + // ESP32Time returns month:0-11 + _sdFile->timestamp(T_ACCESS, rtc.getYear(), rtc.getMonth() + 1, rtc.getDay(), rtc.getHour(true), + rtc.getMinute(), rtc.getSecond()); + _sdFile->timestamp(T_WRITE, rtc.getYear(), rtc.getMonth() + 1, rtc.getDay(), rtc.getHour(true), + rtc.getMinute(), rtc.getSecond()); + } + } + }; + + void updateFileCreateTimestamp() + { + if (USE_SPI_MICROSD) + { + if (online.rtc == true) + { + _sdFile->timestamp(T_CREATE, rtc.getYear(), rtc.getMonth() + 1, rtc.getDay(), rtc.getHour(true), + rtc.getMinute(), rtc.getSecond()); // ESP32Time returns month:0-11 + } + } + }; + + void sync() + { + if (USE_SPI_MICROSD) + _sdFile->sync(); + }; + + int fileSize() + { + if (USE_SPI_MICROSD) + return _sdFile->fileSize(); +#ifdef COMPILE_SD_MMC + else + return _file->size(); +#endif // COMPILE_SD_MMC + return 0; // Keep the compiler happy + }; + + protected: + SdFile *_sdFile; +#ifdef COMPILE_SD_MMC + File *_file; +#endif // COMPILE_SD_MMC +}; + +// Update the file access and write time with date and time obtained from GNSS +// These are SdFile-specific. SD_MMC does this automatically +void updateDataFileAccess(SdFile *dataFile) +{ + if (online.rtc == true) + { + // ESP32Time returns month:0-11 + dataFile->timestamp(T_ACCESS, rtc.getYear(), rtc.getMonth() + 1, rtc.getDay(), rtc.getHour(true), + rtc.getMinute(), rtc.getSecond()); + dataFile->timestamp(T_WRITE, rtc.getYear(), rtc.getMonth() + 1, rtc.getDay(), rtc.getHour(true), + rtc.getMinute(), rtc.getSecond()); + } +} + +// Update the file create time with date and time obtained from GNSS +void updateDataFileCreate(SdFile *dataFile) +{ + if (online.rtc == true) + dataFile->timestamp(T_CREATE, rtc.getYear(), rtc.getMonth() + 1, rtc.getDay(), rtc.getHour(true), + rtc.getMinute(), rtc.getSecond()); // ESP32Time returns month:0-11 +} diff --git a/Firmware/RTK_Surveyor/Form.ino b/Firmware/RTK_Surveyor/Form.ino new file mode 100644 index 000000000..8c7647b72 --- /dev/null +++ b/Firmware/RTK_Surveyor/Form.ino @@ -0,0 +1,2095 @@ +/*------------------------------------------------------------------------------ +Form.ino + + Start and stop the web-server, provide the form and handle browser input. +------------------------------------------------------------------------------*/ + +#ifdef COMPILE_AP + +// Once connected to the access point for WiFi Config, the ESP32 sends current setting values in one long string to +// websocket After user clicks 'save', data is validated via main.js and a long string of values is returned. + +bool websocketConnected = false; + +class CaptiveRequestHandler : public AsyncWebHandler +{ + public: + // https://en.wikipedia.org/wiki/Captive_portal + String urls[5] = {"/hotspot-detect.html", "/library/test/success.html", "/generate_204", "/ncsi.txt", + "/check_network_status.txt"}; + CaptiveRequestHandler() + { + } + virtual ~CaptiveRequestHandler() + { + } + + bool canHandle(AsyncWebServerRequest *request) + { + for (int i = 0; i < 5; i++) + { + if (request->url().equals(urls[i])) + return true; + } + return false; + } + + // Provide a custom small site for redirecting the user to the config site + // HTTP redirect does not work and the relative links on the default config site do not work, because the phone is + // requesting a different server + void handleRequest(AsyncWebServerRequest *request) + { + String logmessage = "Captive Portal Client:" + request->client()->remoteIP().toString() + " " + request->url(); + systemPrintln(logmessage); + AsyncResponseStream *response = request->beginResponseStream("text/html"); + response->print("RTK Config"); + response->print("
"); + response->printf("
SparkFun "
+                         "RTK WiFi Setup
", + WiFi.softAPIP().toString().c_str()); + response->printf("

Configure your RTK receiver here

", + WiFi.softAPIP().toString().c_str()); + response->print("
"); + request->send(response); + } +}; + +// Start webserver in AP mode +bool startWebServer(bool startWiFi = true, int httpPort = 80) +{ + do + { + ntripClientStop(true); // Do not allocate new wifiClient + for (int serverIndex = 0; serverIndex < NTRIP_SERVER_MAX; serverIndex++) + ntripServerStop(serverIndex, true); // Do not allocate new wifiClient + + if (startWiFi) + if (wifiStartAP() == false) // Exits calling wifiConnect() + break; + + if (settings.mdnsEnable == true) + { + if (MDNS.begin("rtk") == false) // This should make the module findable from 'rtk.local' in browser + systemPrintln("Error setting up MDNS responder!"); + else + MDNS.addService("http", "tcp", 80); // Add service to MDNS-SD + } + + incomingSettings = (char *)malloc(AP_CONFIG_SETTING_SIZE); + if (!incomingSettings) + { + systemPrintln("ERROR: Failed to allocate incomingSettings"); + break; + } + memset(incomingSettings, 0, AP_CONFIG_SETTING_SIZE); + + // Pre-load settings CSV + settingsCSV = (char *)malloc(AP_CONFIG_SETTING_SIZE); + if (!settingsCSV) + { + systemPrintln("ERROR: Failed to allocate settingsCSV"); + break; + } + createSettingsString(settingsCSV); + + webserver = new AsyncWebServer(httpPort); + if (!webserver) + { + systemPrintln("ERROR: Failed to allocate webserver"); + break; + } + websocket = new AsyncWebSocket("/ws"); + if (!websocket) + { + systemPrintln("ERROR: Failed to allocate websocket"); + break; + } + + if (settings.enableCaptivePortal == true) + webserver->addHandler(new CaptiveRequestHandler()).setFilter(ON_AP_FILTER); // only when requested from AP + + websocket->onEvent(onWsEvent); + webserver->addHandler(websocket); + + // * index.html (not gz'd) + // * favicon.ico + + // * /src/bootstrap.bundle.min.js - Needed for popper + // * /src/bootstrap.min.css + // * /src/bootstrap.min.js + // * /src/jquery-3.6.0.min.js + // * /src/main.js (not gz'd) + // * /src/rtk-setup.png + // * /src/style.css + + // * /src/fonts/icomoon.eot + // * /src/fonts/icomoon.svg + // * /src/fonts/icomoon.ttf + // * /src/fonts/icomoon.woof + + // * /listfiles responds with a CSV of files and sizes in root + // * /listMessages responds with a CSV of messages supported by this platform + // * /listMessagesBase responds with a CSV of RTCM Base messages supported by this platform + // * /file allows the download or deletion of a file + + webserver->onNotFound(notFound); + + webserver->onFileUpload( + handleUpload); // Run handleUpload function when any file is uploaded. Must be before server.on() calls. + + webserver->on("/", HTTP_GET, [](AsyncWebServerRequest *request) { + AsyncWebServerResponse *response = + request->beginResponse_P(200, "text/html", index_html, sizeof(index_html)); + response->addHeader("Content-Encoding", "gzip"); + request->send(response); + }); + + webserver->on("/favicon.ico", HTTP_GET, [](AsyncWebServerRequest *request) { + AsyncWebServerResponse *response = + request->beginResponse_P(200, "text/plain", favicon_ico, sizeof(favicon_ico)); + response->addHeader("Content-Encoding", "gzip"); + request->send(response); + }); + + webserver->on("/src/bootstrap.bundle.min.js", HTTP_GET, [](AsyncWebServerRequest *request) { + AsyncWebServerResponse *response = request->beginResponse_P(200, "text/javascript", bootstrap_bundle_min_js, + sizeof(bootstrap_bundle_min_js)); + response->addHeader("Content-Encoding", "gzip"); + request->send(response); + }); + + webserver->on("/src/bootstrap.min.css", HTTP_GET, [](AsyncWebServerRequest *request) { + AsyncWebServerResponse *response = + request->beginResponse_P(200, "text/css", bootstrap_min_css, sizeof(bootstrap_min_css)); + response->addHeader("Content-Encoding", "gzip"); + request->send(response); + }); + + webserver->on("/src/bootstrap.min.js", HTTP_GET, [](AsyncWebServerRequest *request) { + AsyncWebServerResponse *response = + request->beginResponse_P(200, "text/javascript", bootstrap_min_js, sizeof(bootstrap_min_js)); + response->addHeader("Content-Encoding", "gzip"); + request->send(response); + }); + + webserver->on("/src/jquery-3.6.0.min.js", HTTP_GET, [](AsyncWebServerRequest *request) { + AsyncWebServerResponse *response = + request->beginResponse_P(200, "text/javascript", jquery_js, sizeof(jquery_js)); + response->addHeader("Content-Encoding", "gzip"); + request->send(response); + }); + + webserver->on("/src/main.js", HTTP_GET, [](AsyncWebServerRequest *request) { + AsyncWebServerResponse *response = + request->beginResponse_P(200, "text/javascript", main_js, sizeof(main_js)); + response->addHeader("Content-Encoding", "gzip"); + request->send(response); + }); + + webserver->on("/src/rtk-setup.png", HTTP_GET, [](AsyncWebServerRequest *request) { + AsyncWebServerResponse *response; + if (productVariant == REFERENCE_STATION) + response = request->beginResponse_P(200, "image/png", rtkSetup_png, sizeof(rtkSetup_png)); + else + response = request->beginResponse_P(200, "image/png", rtkSetupWiFi_png, sizeof(rtkSetupWiFi_png)); + response->addHeader("Content-Encoding", "gzip"); + request->send(response); + }); + + // Battery icons + webserver->on("/src/BatteryBlank.png", HTTP_GET, [](AsyncWebServerRequest *request) { + AsyncWebServerResponse *response = + request->beginResponse_P(200, "image/png", batteryBlank_png, sizeof(batteryBlank_png)); + response->addHeader("Content-Encoding", "gzip"); + request->send(response); + }); + webserver->on("/src/Battery0.png", HTTP_GET, [](AsyncWebServerRequest *request) { + AsyncWebServerResponse *response = + request->beginResponse_P(200, "image/png", battery0_png, sizeof(battery0_png)); + response->addHeader("Content-Encoding", "gzip"); + request->send(response); + }); + webserver->on("/src/Battery1.png", HTTP_GET, [](AsyncWebServerRequest *request) { + AsyncWebServerResponse *response = + request->beginResponse_P(200, "image/png", battery1_png, sizeof(battery1_png)); + response->addHeader("Content-Encoding", "gzip"); + request->send(response); + }); + webserver->on("/src/Battery2.png", HTTP_GET, [](AsyncWebServerRequest *request) { + AsyncWebServerResponse *response = + request->beginResponse_P(200, "image/png", battery2_png, sizeof(battery2_png)); + response->addHeader("Content-Encoding", "gzip"); + request->send(response); + }); + webserver->on("/src/Battery3.png", HTTP_GET, [](AsyncWebServerRequest *request) { + AsyncWebServerResponse *response = + request->beginResponse_P(200, "image/png", battery3_png, sizeof(battery3_png)); + response->addHeader("Content-Encoding", "gzip"); + request->send(response); + }); + + webserver->on("/src/Battery0_Charging.png", HTTP_GET, [](AsyncWebServerRequest *request) { + AsyncWebServerResponse *response = + request->beginResponse_P(200, "image/png", battery0_Charging_png, sizeof(battery0_Charging_png)); + response->addHeader("Content-Encoding", "gzip"); + request->send(response); + }); + webserver->on("/src/Battery1_Charging.png", HTTP_GET, [](AsyncWebServerRequest *request) { + AsyncWebServerResponse *response = + request->beginResponse_P(200, "image/png", battery1_Charging_png, sizeof(battery1_Charging_png)); + response->addHeader("Content-Encoding", "gzip"); + request->send(response); + }); + webserver->on("/src/Battery2_Charging.png", HTTP_GET, [](AsyncWebServerRequest *request) { + AsyncWebServerResponse *response = + request->beginResponse_P(200, "image/png", battery2_Charging_png, sizeof(battery2_Charging_png)); + response->addHeader("Content-Encoding", "gzip"); + request->send(response); + }); + webserver->on("/src/Battery3_Charging.png", HTTP_GET, [](AsyncWebServerRequest *request) { + AsyncWebServerResponse *response = + request->beginResponse_P(200, "image/png", battery3_Charging_png, sizeof(battery3_Charging_png)); + response->addHeader("Content-Encoding", "gzip"); + request->send(response); + }); + + webserver->on("/src/style.css", HTTP_GET, [](AsyncWebServerRequest *request) { + AsyncWebServerResponse *response = request->beginResponse_P(200, "text/css", style_css, sizeof(style_css)); + response->addHeader("Content-Encoding", "gzip"); + request->send(response); + }); + + webserver->on("/src/fonts/icomoon.eot", HTTP_GET, [](AsyncWebServerRequest *request) { + AsyncWebServerResponse *response = + request->beginResponse_P(200, "text/plain", icomoon_eot, sizeof(icomoon_eot)); + response->addHeader("Content-Encoding", "gzip"); + request->send(response); + }); + + webserver->on("/src/fonts/icomoon.svg", HTTP_GET, [](AsyncWebServerRequest *request) { + AsyncWebServerResponse *response = + request->beginResponse_P(200, "text/plain", icomoon_svg, sizeof(icomoon_svg)); + response->addHeader("Content-Encoding", "gzip"); + request->send(response); + }); + + webserver->on("/src/fonts/icomoon.ttf", HTTP_GET, [](AsyncWebServerRequest *request) { + AsyncWebServerResponse *response = + request->beginResponse_P(200, "text/plain", icomoon_ttf, sizeof(icomoon_ttf)); + response->addHeader("Content-Encoding", "gzip"); + request->send(response); + }); + + webserver->on("/src/fonts/icomoon.woof", HTTP_GET, [](AsyncWebServerRequest *request) { + AsyncWebServerResponse *response = + request->beginResponse_P(200, "text/plain", icomoon_woof, sizeof(icomoon_woof)); + response->addHeader("Content-Encoding", "gzip"); + request->send(response); + }); + + // Handler for the /upload form POST + webserver->on( + "/upload", HTTP_POST, [](AsyncWebServerRequest *request) { request->send(200); }, handleFirmwareFileUpload); + + // Handler for file manager + webserver->on("/listfiles", HTTP_GET, [](AsyncWebServerRequest *request) { + String logmessage = "Client:" + request->client()->remoteIP().toString() + " " + request->url(); + systemPrintln(logmessage); + String files; + getFileList(files); + request->send(200, "text/plain", files); + }); + + // Handler for supported messages list + webserver->on("/listMessages", HTTP_GET, [](AsyncWebServerRequest *request) { + String logmessage = "Client:" + request->client()->remoteIP().toString() + " " + request->url(); + systemPrintln(logmessage); + String messages; + createMessageList(messages); + request->send(200, "text/plain", messages); + }); + + // Handler for supported RTCM/Base messages list + webserver->on("/listMessagesBase", HTTP_GET, [](AsyncWebServerRequest *request) { + String logmessage = "Client:" + request->client()->remoteIP().toString() + " " + request->url(); + systemPrintln(logmessage); + String messageList; + createMessageListBase(messageList); + request->send(200, "text/plain", messageList); + }); + + // Handler for file manager + webserver->on("/file", HTTP_GET, [](AsyncWebServerRequest *request) { handleFileManager(request); }); + + webserver->begin(); + + if (settings.debugWiFiConfig == true) + systemPrintln("Web Server Started"); + reportHeapNow(false); + return true; + } while (0); + + // Release the resources + stopWebServer(); + return false; +} + +void stopWebServer() +{ + if (webserver != nullptr) + { + webserver->end(); + free(webserver); + webserver = nullptr; + } + + if (websocket != nullptr) + { + delete websocket; + websocket = nullptr; + } + + if (settingsCSV != nullptr) + { + free(settingsCSV); + settingsCSV = nullptr; + } + + if (incomingSettings != nullptr) + { + free(incomingSettings); + incomingSettings = nullptr; + } + + if (settings.debugWiFiConfig == true) + systemPrintln("Web Server Stopped"); + reportHeapNow(false); +} + +void notFound(AsyncWebServerRequest *request) +{ + String logmessage = "Client:" + request->client()->remoteIP().toString() + " " + request->url(); + systemPrintln(logmessage); + request->send(404, "text/plain", "Not found"); +} + +// Handler for firmware file downloads +static void handleFileManager(AsyncWebServerRequest *request) +{ + // This section does not tolerate semaphore transactions + String logmessage = "Client:" + request->client()->remoteIP().toString() + " " + request->url(); + + if (request->hasParam("name") && request->hasParam("action")) + { + const char *fileName = request->getParam("name")->value().c_str(); + const char *fileAction = request->getParam("action")->value().c_str(); + + logmessage = "Client:" + request->client()->remoteIP().toString() + " " + request->url() + + "?name=" + String(fileName) + "&action=" + String(fileAction); + + char slashFileName[60]; + snprintf(slashFileName, sizeof(slashFileName), "/%s", request->getParam("name")->value().c_str()); + + bool fileExists; + if (USE_SPI_MICROSD) + { + fileExists = sd->exists(slashFileName); + } +#ifdef COMPILE_SD_MMC + else + { + fileExists = SD_MMC.exists(slashFileName); + } +#endif // COMPILE_SD_MMC + + if (fileExists == false) + { + systemPrintln(logmessage + " ERROR: file does not exist"); + request->send(400, "text/plain", "ERROR: file does not exist"); + } + else + { + systemPrintln(logmessage + " file exists"); + + if (strcmp(fileAction, "download") == 0) + { + logmessage += " downloaded"; + + if (managerFileOpen == false) + { + // Allocate the managerTempFile + if (!managerTempFile) + { + managerTempFile = new FileSdFatMMC; + if (!managerTempFile) + { + systemPrintln("Failed to allocate managerTempFile!"); + return; + } + } + + if (managerTempFile->open(slashFileName, O_READ) == true) + managerFileOpen = true; + else + systemPrintln("Error: File Manager failed to open file"); + } + else + { + // File is already in use. Wait your turn. + request->send(202, "text/plain", "ERROR: File already downloading"); + } + + int dataAvailable; + dataAvailable = managerTempFile->size() - managerTempFile->position(); + + AsyncWebServerResponse *response = request->beginResponse( + "text/plain", dataAvailable, [](uint8_t *buffer, size_t maxLen, size_t index) -> size_t { + uint32_t bytes = 0; + uint32_t availableBytes; + availableBytes = managerTempFile->available(); + + if (availableBytes > maxLen) + { + bytes = managerTempFile->read(buffer, maxLen); + } + else + { + bytes = managerTempFile->read(buffer, availableBytes); + managerTempFile->close(); + + managerFileOpen = false; + + websocket->textAll("fmNext,1,"); // Tell browser to send next file if needed + } + + return bytes; + }); + + response->addHeader("Cache-Control", "no-cache"); + response->addHeader("Content-Disposition", "attachment; filename=" + String(fileName)); + response->addHeader("Access-Control-Allow-Origin", "*"); + request->send(response); + } + else if (strcmp(fileAction, "delete") == 0) + { + logmessage += " deleted"; + if (USE_SPI_MICROSD) + sd->remove(slashFileName); +#ifdef COMPILE_SD_MMC + else + SD_MMC.remove(slashFileName); +#endif // COMPILE_SD_MMC + request->send(200, "text/plain", "Deleted File: " + String(fileName)); + } + else + { + logmessage += " ERROR: invalid action param supplied"; + request->send(400, "text/plain", "ERROR: invalid action param supplied"); + } + systemPrintln(logmessage); + } + } + else + { + request->send(400, "text/plain", "ERROR: name and action params required"); + } +} + +// Handler for firmware file upload +static void handleFirmwareFileUpload(AsyncWebServerRequest *request, String fileName, size_t index, uint8_t *data, + size_t len, bool final) +{ + if (!index) + { + // Check file name against valid firmware names + const char *BIN_EXT = "bin"; + const char *BIN_HEADER = "RTK_Surveyor_Firmware"; + + int fnameLen = fileName.length(); + char fname[fnameLen + 2] = {'/'}; // Filename must start with / or VERY bad things happen on SD_MMC + fileName.toCharArray(&fname[1], fnameLen + 1); + fname[fnameLen + 1] = '\0'; // Terminate array + + // Check 'bin' extension + if (strcmp(BIN_EXT, &fname[strlen(fname) - strlen(BIN_EXT)]) == 0) + { + // Check for 'RTK_Surveyor_Firmware' start of file name + if (strncmp(fname, BIN_HEADER, strlen(BIN_HEADER)) == 0) + { + // Begin update process + if (!Update.begin(UPDATE_SIZE_UNKNOWN)) + { + Update.printError(Serial); + return request->send(400, "text/plain", "OTA could not begin"); + } + } + else + { + systemPrintf("Unknown: %s\r\n", fname); + return request->send(400, "text/html", "Error: Unknown file type"); + } + } + else + { + systemPrintf("Unknown: %s\r\n", fname); + return request->send(400, "text/html", "Error: Unknown file type"); + } + } + + // Write chunked data to the free sketch space + if (len) + { + if (Update.write(data, len) != len) + return request->send(400, "text/plain", "OTA could not begin"); + else + { + binBytesSent += len; + + // Send an update to browser every 100k + if (binBytesSent - binBytesLastUpdate > 100000) + { + binBytesLastUpdate = binBytesSent; + + char bytesSentMsg[100]; + snprintf(bytesSentMsg, sizeof(bytesSentMsg), "%'d bytes sent", binBytesSent); + + systemPrintf("bytesSentMsg: %s\r\n", bytesSentMsg); + + char statusMsg[200] = {'\0'}; + stringRecord(statusMsg, "firmwareUploadStatus", + bytesSentMsg); // Convert to "firmwareUploadMsg,11214 bytes sent," + + systemPrintf("msg: %s\r\n", statusMsg); + websocket->textAll(statusMsg); + } + } + } + + if (final) + { + if (!Update.end(true)) + { + Update.printError(Serial); + return request->send(400, "text/plain", "Could not end OTA"); + } + else + { + websocket->textAll("firmwareUploadComplete,1,"); + systemPrintln("Firmware update complete. Restarting"); + delay(500); + ESP.restart(); + } + } +} + +// Events triggered by web sockets +void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, + size_t len) +{ + if (type == WS_EVT_CONNECT) + { + if (settings.debugWiFiConfig == true) + systemPrintln("Websocket client connected"); + client->text(settingsCSV); + lastDynamicDataUpdate = millis(); + websocketConnected = true; + } + else if (type == WS_EVT_DISCONNECT) + { + if (settings.debugWiFiConfig == true) + systemPrintln("Websocket client disconnected"); + + // User has either refreshed the page or disconnected. Recompile the current settings. + createSettingsString(settingsCSV); + websocketConnected = false; + } + else if (type == WS_EVT_DATA) + { + if (currentlyParsingData == false) + { + for (int i = 0; i < len; i++) + { + incomingSettings[incomingSettingsSpot++] = data[i]; + incomingSettingsSpot %= AP_CONFIG_SETTING_SIZE; + } + timeSinceLastIncomingSetting = millis(); + } + } + else + { + if (settings.debugWiFiConfig == true) + systemPrintf("onWsEvent: unrecognised AwsEventType %d\r\n", type); + } +} +// Create a csv string with current settings +void createSettingsString(char *newSettings) +{ + char tagText[32]; + char nameText[64]; + + newSettings[0] = '\0'; // Erase current settings string + + // System Info + char apPlatformPrefix[80]; + strncpy(apPlatformPrefix, platformPrefixTable[productVariant], sizeof(apPlatformPrefix)); + stringRecord(newSettings, "platformPrefix", apPlatformPrefix); + + char apRtkFirmwareVersion[86]; + getFirmwareVersion(apRtkFirmwareVersion, sizeof(apRtkFirmwareVersion), true); + stringRecord(newSettings, "rtkFirmwareVersion", apRtkFirmwareVersion); + + if (!configureViaEthernet) // ZED type is unknown if we are in configure-via-ethernet mode + { + char apZedPlatform[50]; + if (zedModuleType == PLATFORM_F9P) + strcpy(apZedPlatform, "ZED-F9P"); + else if (zedModuleType == PLATFORM_F9R) + strcpy(apZedPlatform, "ZED-F9R"); + + char apZedFirmwareVersion[80]; + snprintf(apZedFirmwareVersion, sizeof(apZedFirmwareVersion), "%s Firmware: %s ID: %s", apZedPlatform, + zedFirmwareVersion, zedUniqueId); + stringRecord(newSettings, "zedFirmwareVersion", apZedFirmwareVersion); + stringRecord(newSettings, "zedFirmwareVersionInt", zedFirmwareVersionInt); + } + else + { + char apZedFirmwareVersion[80]; + snprintf(apZedFirmwareVersion, sizeof(apZedFirmwareVersion), "ZED-F9: Unknown"); + stringRecord(newSettings, "zedFirmwareVersion", apZedFirmwareVersion); + } + + char apDeviceBTID[30]; + snprintf(apDeviceBTID, sizeof(apDeviceBTID), "Device Bluetooth ID: %02X%02X", btMACAddress[4], btMACAddress[5]); + stringRecord(newSettings, "deviceBTID", apDeviceBTID); + + // GNSS Config + stringRecord(newSettings, "measurementRateHz", 1000.0 / settings.measurementRate, 2); // 2 = decimals to print + stringRecord(newSettings, "dynamicModel", settings.dynamicModel); + stringRecord(newSettings, "ubxConstellationsGPS", settings.ubxConstellations[0].enabled); // GPS + stringRecord(newSettings, "ubxConstellationsSBAS", settings.ubxConstellations[1].enabled); // SBAS + stringRecord(newSettings, "ubxConstellationsGalileo", settings.ubxConstellations[2].enabled); // Galileo + stringRecord(newSettings, "ubxConstellationsBeiDou", settings.ubxConstellations[3].enabled); // BeiDou + stringRecord(newSettings, "ubxConstellationsGLONASS", settings.ubxConstellations[5].enabled); // GLONASS + + // Base Config + stringRecord(newSettings, "baseTypeSurveyIn", !settings.fixedBase); + stringRecord(newSettings, "baseTypeFixed", settings.fixedBase); + stringRecord(newSettings, "observationSeconds", settings.observationSeconds); + stringRecord(newSettings, "observationPositionAccuracy", settings.observationPositionAccuracy, 2); + + if (settings.fixedBaseCoordinateType == COORD_TYPE_ECEF) + { + stringRecord(newSettings, "fixedBaseCoordinateTypeECEF", true); + stringRecord(newSettings, "fixedBaseCoordinateTypeGeo", false); + } + else + { + stringRecord(newSettings, "fixedBaseCoordinateTypeECEF", false); + stringRecord(newSettings, "fixedBaseCoordinateTypeGeo", true); + } + + stringRecord(newSettings, "fixedEcefX", settings.fixedEcefX, 3); + stringRecord(newSettings, "fixedEcefY", settings.fixedEcefY, 3); + stringRecord(newSettings, "fixedEcefZ", settings.fixedEcefZ, 3); + stringRecord(newSettings, "fixedLat", settings.fixedLat, haeNumberOfDecimals); + stringRecord(newSettings, "fixedLong", settings.fixedLong, haeNumberOfDecimals); + stringRecord(newSettings, "fixedAltitude", settings.fixedAltitude, 4); + + stringRecord(newSettings, "enableNtripServer", settings.enableNtripServer); + for (int serverIndex = 0; serverIndex < NTRIP_SERVER_MAX; serverIndex++) + { + char name[50]; + sprintf(name, "ntripServer_%s_%d", "CasterHost", serverIndex); + stringRecord(newSettings, name, &settings.ntripServer_CasterHost[serverIndex][0]); + sprintf(name, "ntripServer_%s_%d", "CasterPort", serverIndex); + stringRecord(newSettings, name, settings.ntripServer_CasterPort[serverIndex]); + sprintf(name, "ntripServer_%s_%d", "CasterUser", serverIndex); + stringRecord(newSettings, name, &settings.ntripServer_CasterUser[serverIndex][0]); + sprintf(name, "ntripServer_%s_%d", "CasterUserPW", serverIndex); + stringRecord(newSettings, name, &settings.ntripServer_CasterUserPW[serverIndex][0]); + sprintf(name, "ntripServer_%s_%d", "MountPoint", serverIndex); + stringRecord(newSettings, name, &settings.ntripServer_MountPoint[serverIndex][0]); + sprintf(name, "ntripServer_%s_%d", "MountPointPW", serverIndex); + stringRecord(newSettings, name, &settings.ntripServer_MountPointPW[serverIndex][0]); + } + + stringRecord(newSettings, "enableNtripClient", settings.enableNtripClient); + stringRecord(newSettings, "ntripClient_CasterHost", settings.ntripClient_CasterHost); + stringRecord(newSettings, "ntripClient_CasterPort", settings.ntripClient_CasterPort); + stringRecord(newSettings, "ntripClient_CasterUser", settings.ntripClient_CasterUser); + stringRecord(newSettings, "ntripClient_CasterUserPW", settings.ntripClient_CasterUserPW); + stringRecord(newSettings, "ntripClient_MountPoint", settings.ntripClient_MountPoint); + stringRecord(newSettings, "ntripClient_MountPointPW", settings.ntripClient_MountPointPW); + stringRecord(newSettings, "ntripClient_TransmitGGA", settings.ntripClient_TransmitGGA); + + // Sensor Fusion Config + stringRecord(newSettings, "enableSensorFusion", settings.enableSensorFusion); + stringRecord(newSettings, "autoIMUmountAlignment", settings.autoIMUmountAlignment); + + // System Config + stringRecord(newSettings, "enableUART2UBXIn", settings.enableUART2UBXIn); + stringRecord(newSettings, "enableLogging", settings.enableLogging); + stringRecord(newSettings, "enableARPLogging", settings.enableARPLogging); + stringRecord(newSettings, "ARPLoggingInterval", settings.ARPLoggingInterval_s); + stringRecord(newSettings, "maxLogTime_minutes", settings.maxLogTime_minutes); + stringRecord(newSettings, "maxLogLength_minutes", settings.maxLogLength_minutes); + + char sdCardSizeChar[20]; + String cardSize; + stringHumanReadableSize(cardSize, sdCardSize); + cardSize.toCharArray(sdCardSizeChar, sizeof(sdCardSizeChar)); + char sdFreeSpaceChar[20]; + String freeSpace; + stringHumanReadableSize(freeSpace, sdFreeSpace); + freeSpace.toCharArray(sdFreeSpaceChar, sizeof(sdFreeSpaceChar)); + + stringRecord(newSettings, "sdFreeSpace", sdFreeSpaceChar); + stringRecord(newSettings, "sdSize", sdCardSizeChar); + + stringRecord(newSettings, "enableResetDisplay", settings.enableResetDisplay); + + // Ethernet + stringRecord(newSettings, "ethernetDHCP", settings.ethernetDHCP); + char ipAddressChar[20]; + snprintf(ipAddressChar, sizeof(ipAddressChar), "%s", settings.ethernetIP.toString().c_str()); + stringRecord(newSettings, "ethernetIP", ipAddressChar); + snprintf(ipAddressChar, sizeof(ipAddressChar), "%s", settings.ethernetDNS.toString().c_str()); + stringRecord(newSettings, "ethernetDNS", ipAddressChar); + snprintf(ipAddressChar, sizeof(ipAddressChar), "%s", settings.ethernetGateway.toString().c_str()); + stringRecord(newSettings, "ethernetGateway", ipAddressChar); + snprintf(ipAddressChar, sizeof(ipAddressChar), "%s", settings.ethernetSubnet.toString().c_str()); + stringRecord(newSettings, "ethernetSubnet", ipAddressChar); + stringRecord(newSettings, "httpPort", settings.httpPort); + stringRecord(newSettings, "ethernetNtpPort", settings.ethernetNtpPort); + stringRecord(newSettings, "pvtClientPort", settings.pvtClientPort); + stringRecord(newSettings, "pvtClientHost", settings.pvtClientHost); + + // Network layer + stringRecord(newSettings, "defaultNetworkType", settings.defaultNetworkType); + stringRecord(newSettings, "enableNetworkFailover", settings.enableNetworkFailover); + + // NTP + stringRecord(newSettings, "ntpPollExponent", settings.ntpPollExponent); + stringRecord(newSettings, "ntpPrecision", settings.ntpPrecision); + stringRecord(newSettings, "ntpRootDelay", settings.ntpRootDelay); + stringRecord(newSettings, "ntpRootDispersion", settings.ntpRootDispersion); + stringRecord(newSettings, "ntpPollExponent", settings.ntpPollExponent); + char ntpRefId[5]; + snprintf(ntpRefId, sizeof(ntpRefId), "%s", settings.ntpReferenceId); + stringRecord(newSettings, "ntpReferenceId", ntpRefId); + + // Automatic firmware update settings + stringRecord(newSettings, "enableAutoFirmwareUpdate", settings.enableAutoFirmwareUpdate); + stringRecord(newSettings, "autoFirmwareCheckMinutes", settings.autoFirmwareCheckMinutes); + + // Turn on SD display block last + stringRecord(newSettings, "sdMounted", online.microSD); + + // Port Config + stringRecord(newSettings, "dataPortBaud", settings.dataPortBaud); + stringRecord(newSettings, "radioPortBaud", settings.radioPortBaud); + stringRecord(newSettings, "dataPortChannel", settings.dataPortChannel); + + // L-Band + char hardwareID[13]; + snprintf(hardwareID, sizeof(hardwareID), "%02X%02X%02X%02X%02X%02X", lbandMACAddress[0], lbandMACAddress[1], + lbandMACAddress[2], lbandMACAddress[3], lbandMACAddress[4], lbandMACAddress[5]); + stringRecord(newSettings, "hardwareID", hardwareID); + + char apDaysRemaining[20]; + if (strlen(settings.pointPerfectCurrentKey) > 0) + { +#ifdef COMPILE_L_BAND + int daysRemaining = daysFromEpoch(settings.pointPerfectNextKeyStart + settings.pointPerfectNextKeyDuration + 1); + snprintf(apDaysRemaining, sizeof(apDaysRemaining), "%d", daysRemaining); +#endif // COMPILE_L_BAND + } + else + snprintf(apDaysRemaining, sizeof(apDaysRemaining), "No Keys"); + + stringRecord(newSettings, "daysRemaining", apDaysRemaining); + + stringRecord(newSettings, "pointPerfectDeviceProfileToken", settings.pointPerfectDeviceProfileToken); + stringRecord(newSettings, "enablePointPerfectCorrections", settings.enablePointPerfectCorrections); + stringRecord(newSettings, "autoKeyRenewal", settings.autoKeyRenewal); + stringRecord(newSettings, "geographicRegion", settings.geographicRegion); + + // External PPS/Triggers + stringRecord(newSettings, "enableExternalPulse", settings.enableExternalPulse); + stringRecord(newSettings, "externalPulseTimeBetweenPulse_us", settings.externalPulseTimeBetweenPulse_us); + stringRecord(newSettings, "externalPulseLength_us", settings.externalPulseLength_us); + stringRecord(newSettings, "externalPulsePolarity", settings.externalPulsePolarity); + stringRecord(newSettings, "enableExternalHardwareEventLogging", settings.enableExternalHardwareEventLogging); + + // Profiles + stringRecord( + newSettings, "profileName", + profileNames[profileNumber]); // Must come before profile number so AP config page JS has name before number + stringRecord(newSettings, "profileNumber", profileNumber); + for (int index = 0; index < MAX_PROFILE_COUNT; index++) + { + snprintf(tagText, sizeof(tagText), "profile%dName", index); + snprintf(nameText, sizeof(nameText), "%d: %s", index + 1, profileNames[index]); + stringRecord(newSettings, tagText, nameText); + } + // stringRecord(newSettings, "activeProfiles", activeProfiles); + + // System state at power on. Convert various system states to either Rover or Base or NTP. + int lastState; // 0 = Rover, 1 = Base, 2 = NTP + if (productVariant == REFERENCE_STATION) + { + lastState = 1; // Default Base + if (settings.lastState >= STATE_ROVER_NOT_STARTED && settings.lastState <= STATE_ROVER_RTK_FIX) + lastState = 0; + if (settings.lastState >= STATE_NTPSERVER_NOT_STARTED && settings.lastState <= STATE_NTPSERVER_SYNC) + lastState = 2; + } + else + { + lastState = 0; // Default Rover + if (settings.lastState >= STATE_BASE_NOT_STARTED && settings.lastState <= STATE_BASE_FIXED_TRANSMITTING) + lastState = 1; + } + stringRecord(newSettings, "baseRoverSetup", lastState); + + // Bluetooth radio type + stringRecord(newSettings, "bluetoothRadioType", settings.bluetoothRadioType); + + // Current coordinates come from HPPOSLLH call back + stringRecord(newSettings, "geodeticLat", latitude, haeNumberOfDecimals); + stringRecord(newSettings, "geodeticLon", longitude, haeNumberOfDecimals); + stringRecord(newSettings, "geodeticAlt", altitude, 3); + + double ecefX = 0; + double ecefY = 0; + double ecefZ = 0; + + geodeticToEcef(latitude, longitude, altitude, &ecefX, &ecefY, &ecefZ); + + stringRecord(newSettings, "ecefX", ecefX, 3); + stringRecord(newSettings, "ecefY", ecefY, 3); + stringRecord(newSettings, "ecefZ", ecefZ, 3); + + // Antenna height and ARP + stringRecord(newSettings, "antennaHeight", settings.antennaHeight); + stringRecord(newSettings, "antennaReferencePoint", settings.antennaReferencePoint, 1); + + // Radio / ESP-Now settings + char radioMAC[18]; // Send radio MAC + snprintf(radioMAC, sizeof(radioMAC), "%02X:%02X:%02X:%02X:%02X:%02X", wifiMACAddress[0], wifiMACAddress[1], + wifiMACAddress[2], wifiMACAddress[3], wifiMACAddress[4], wifiMACAddress[5]); + stringRecord(newSettings, "radioMAC", radioMAC); + stringRecord(newSettings, "radioType", settings.radioType); + stringRecord(newSettings, "espnowPeerCount", settings.espnowPeerCount); + for (int index = 0; index < settings.espnowPeerCount; index++) + { + snprintf(tagText, sizeof(tagText), "peerMAC%d", index); + snprintf(nameText, sizeof(nameText), "%02X:%02X:%02X:%02X:%02X:%02X", settings.espnowPeers[index][0], + settings.espnowPeers[index][1], settings.espnowPeers[index][2], settings.espnowPeers[index][3], + settings.espnowPeers[index][4], settings.espnowPeers[index][5]); + stringRecord(newSettings, tagText, nameText); + } + stringRecord(newSettings, "espnowBroadcast", settings.espnowBroadcast); + + stringRecord(newSettings, "logFileName", logFileName); + + if (HAS_NO_BATTERY) // Ref Stn does not have a battery + { + stringRecord(newSettings, "batteryIconFileName", (char *)"src/BatteryBlank.png"); + stringRecord(newSettings, "batteryPercent", (char *)" "); + } + else + { + // Determine battery icon + int iconLevel = 0; + if (battLevel < 25) + iconLevel = 0; + else if (battLevel < 50) + iconLevel = 1; + else if (battLevel < 75) + iconLevel = 2; + else // batt level > 75 + iconLevel = 3; + + char batteryIconFileName[sizeof("src/Battery2_Charging.png__")]; // sizeof() includes 1 for \0 termination + + if (externalPowerConnected) + snprintf(batteryIconFileName, sizeof(batteryIconFileName), "src/Battery%d_Charging.png", iconLevel); + else + snprintf(batteryIconFileName, sizeof(batteryIconFileName), "src/Battery%d.png", iconLevel); + + stringRecord(newSettings, "batteryIconFileName", batteryIconFileName); + + // Determine battery percent + char batteryPercent[sizeof("+100%__")]; + int tempLevel = battLevel; + if (tempLevel > 100) + tempLevel = 100; + + if (externalPowerConnected) + snprintf(batteryPercent, sizeof(batteryPercent), "+%d%%", tempLevel); + else + snprintf(batteryPercent, sizeof(batteryPercent), "%d%%", tempLevel); + stringRecord(newSettings, "batteryPercent", batteryPercent); + } + + stringRecord(newSettings, "minElev", settings.minElev); + stringRecord(newSettings, "imuYaw", settings.imuYaw); + stringRecord(newSettings, "imuPitch", settings.imuPitch); + stringRecord(newSettings, "imuRoll", settings.imuRoll); + stringRecord(newSettings, "sfDisableWheelDirection", settings.sfDisableWheelDirection); + stringRecord(newSettings, "sfCombineWheelTicks", settings.sfCombineWheelTicks); + stringRecord(newSettings, "rateNavPrio", settings.rateNavPrio); + stringRecord(newSettings, "sfUseSpeed", settings.sfUseSpeed); + stringRecord(newSettings, "coordinateInputType", settings.coordinateInputType); + // stringRecord(newSettings, "lbandFixTimeout_seconds", settings.lbandFixTimeout_seconds); + + if (zedModuleType == PLATFORM_F9R) + stringRecord(newSettings, "minCNO", settings.minCNO_F9R); + else + stringRecord(newSettings, "minCNO", settings.minCNO_F9P); + + stringRecord(newSettings, "mdnsEnable", settings.mdnsEnable); + + // Add ECEF and Geodetic station data to the end of settings + for (int index = 0; index < COMMON_COORDINATES_MAX_STATIONS; index++) // Arbitrary 50 station limit + { + // stationInfo example: LocationA,-1280206.568,-4716804.403,4086665.484 + char stationInfo[100]; + + // Try SD, then LFS + if (getFileLineSD(stationCoordinateECEFFileName, index, stationInfo, sizeof(stationInfo)) == + true) // fileName, lineNumber, array, arraySize + { + trim(stationInfo); // Remove trailing whitespace + + if (settings.debugWiFiConfig == true) + systemPrintf("ECEF SD station %d - found: %s\r\n", index, stationInfo); + + replaceCharacter(stationInfo, ',', ' '); // Change all , to ' ' for easier parsing on the JS side + snprintf(tagText, sizeof(tagText), "stationECEF%d", index); + stringRecord(newSettings, tagText, stationInfo); + } + else if (getFileLineLFS(stationCoordinateECEFFileName, index, stationInfo, sizeof(stationInfo)) == + true) // fileName, lineNumber, array, arraySize + { + trim(stationInfo); // Remove trailing whitespace + + if (settings.debugWiFiConfig == true) + systemPrintf("ECEF LFS station %d - found: %s\r\n", index, stationInfo); + + replaceCharacter(stationInfo, ',', ' '); // Change all , to ' ' for easier parsing on the JS side + snprintf(tagText, sizeof(tagText), "stationECEF%d", index); + stringRecord(newSettings, tagText, stationInfo); + } + else + { + // We could not find this line + break; + } + } + + for (int index = 0; index < COMMON_COORDINATES_MAX_STATIONS; index++) // Arbitrary 50 station limit + { + // stationInfo example: LocationA,40.09029479,-105.18505761,1560.089 + char stationInfo[100]; + + // Try SD, then LFS + if (getFileLineSD(stationCoordinateGeodeticFileName, index, stationInfo, sizeof(stationInfo)) == + true) // fileName, lineNumber, array, arraySize + { + trim(stationInfo); // Remove trailing whitespace + + if (settings.debugWiFiConfig == true) + systemPrintf("Geo SD station %d - found: %s\r\n", index, stationInfo); + + replaceCharacter(stationInfo, ',', ' '); // Change all , to ' ' for easier parsing on the JS side + snprintf(tagText, sizeof(tagText), "stationGeodetic%d", index); + stringRecord(newSettings, tagText, stationInfo); + } + else if (getFileLineLFS(stationCoordinateGeodeticFileName, index, stationInfo, sizeof(stationInfo)) == + true) // fileName, lineNumber, array, arraySize + { + trim(stationInfo); // Remove trailing whitespace + + if (settings.debugWiFiConfig == true) + systemPrintf("Geo LFS station %d - found: %s\r\n", index, stationInfo); + + replaceCharacter(stationInfo, ',', ' '); // Change all , to ' ' for easier parsing on the JS side + snprintf(tagText, sizeof(tagText), "stationGeodetic%d", index); + stringRecord(newSettings, tagText, stationInfo); + } + else + { + // We could not find this line + break; + } + } + + // Add WiFi credential table + for (int x = 0; x < MAX_WIFI_NETWORKS; x++) + { + snprintf(tagText, sizeof(tagText), "wifiNetwork%dSSID", x); + stringRecord(newSettings, tagText, settings.wifiNetworks[x].ssid); + + snprintf(tagText, sizeof(tagText), "wifiNetwork%dPassword", x); + stringRecord(newSettings, tagText, settings.wifiNetworks[x].password); + } + + // Drop downs on the AP config page expect a value, whereas bools get stringRecord as true/false + if (settings.wifiConfigOverAP == true) + stringRecord(newSettings, "wifiConfigOverAP", 1); // 1 = AP mode, 0 = WiFi + else + stringRecord(newSettings, "wifiConfigOverAP", 0); // 1 = AP mode, 0 = WiFi + + stringRecord(newSettings, "enablePvtServer", settings.enablePvtServer); + stringRecord(newSettings, "enablePvtClient", settings.enablePvtClient); + stringRecord(newSettings, "pvtServerPort", settings.pvtServerPort); + stringRecord(newSettings, "enablePvtUdpServer", settings.enablePvtUdpServer); + stringRecord(newSettings, "pvtUdpServerPort", settings.pvtUdpServerPort); + stringRecord(newSettings, "enableRCFirmware", enableRCFirmware); + + // New settings not yet integrated + //... + + strcat(newSettings, "\0"); + systemPrintf("newSettings len: %d\r\n", strlen(newSettings)); + systemPrintf("newSettings: %s\r\n", newSettings); +} + +// Create a csv string with the dynamic data to update (current coordinates, battery level, etc) +void createDynamicDataString(char *settingsCSV) +{ + settingsCSV[0] = '\0'; // Erase current settings string + + // Current coordinates come from HPPOSLLH call back + stringRecord(settingsCSV, "geodeticLat", latitude, haeNumberOfDecimals); + stringRecord(settingsCSV, "geodeticLon", longitude, haeNumberOfDecimals); + stringRecord(settingsCSV, "geodeticAlt", altitude, 3); + + double ecefX = 0; + double ecefY = 0; + double ecefZ = 0; + + geodeticToEcef(latitude, longitude, altitude, &ecefX, &ecefY, &ecefZ); + + stringRecord(settingsCSV, "ecefX", ecefX, 3); + stringRecord(settingsCSV, "ecefY", ecefY, 3); + stringRecord(settingsCSV, "ecefZ", ecefZ, 3); + + if (HAS_NO_BATTERY) // Ref Stn does not have a battery + { + stringRecord(settingsCSV, "batteryIconFileName", (char *)"src/BatteryBlank.png"); + stringRecord(settingsCSV, "batteryPercent", (char *)" "); + } + else + { + // Determine battery icon + int iconLevel = 0; + if (battLevel < 25) + iconLevel = 0; + else if (battLevel < 50) + iconLevel = 1; + else if (battLevel < 75) + iconLevel = 2; + else // batt level > 75 + iconLevel = 3; + + char batteryIconFileName[sizeof("src/Battery2_Charging.png__")]; // sizeof() includes 1 for \0 termination + + if (externalPowerConnected) + snprintf(batteryIconFileName, sizeof(batteryIconFileName), "src/Battery%d_Charging.png", iconLevel); + else + snprintf(batteryIconFileName, sizeof(batteryIconFileName), "src/Battery%d.png", iconLevel); + + stringRecord(settingsCSV, "batteryIconFileName", batteryIconFileName); + + // Determine battery percent + char batteryPercent[sizeof("+100%__")]; + if (externalPowerConnected) + snprintf(batteryPercent, sizeof(batteryPercent), "+%d%%", battLevel); + else + snprintf(batteryPercent, sizeof(batteryPercent), "%d%%", battLevel); + stringRecord(settingsCSV, "batteryPercent", batteryPercent); + } + + strcat(settingsCSV, "\0"); +} + +// Given a settingName, and string value, update a given setting +void updateSettingWithValue(const char *settingName, const char *settingValueStr) +{ + char *ptr; + double settingValue = strtod(settingValueStr, &ptr); + + bool settingValueBool = false; + if (strcmp(settingValueStr, "true") == 0) + settingValueBool = true; + + if (strcmp(settingName, "maxLogTime_minutes") == 0) + settings.maxLogTime_minutes = settingValue; + else if (strcmp(settingName, "maxLogLength_minutes") == 0) + settings.maxLogLength_minutes = settingValue; + else if (strcmp(settingName, "measurementRateHz") == 0) + { + settings.measurementRate = (int)(1000.0 / settingValue); + + // This is one of the first settings to be received. If seen, remove the station files. + removeFile(stationCoordinateECEFFileName); + removeFile(stationCoordinateGeodeticFileName); + if (settings.debugWiFiConfig == true) + systemPrintln("Station coordinate files removed"); + } + else if (strcmp(settingName, "dynamicModel") == 0) + settings.dynamicModel = settingValue; + else if (strcmp(settingName, "baseTypeFixed") == 0) + settings.fixedBase = settingValueBool; + else if (strcmp(settingName, "observationSeconds") == 0) + settings.observationSeconds = settingValue; + else if (strcmp(settingName, "observationPositionAccuracy") == 0) + settings.observationPositionAccuracy = settingValue; + else if (strcmp(settingName, "fixedBaseCoordinateTypeECEF") == 0) + settings.fixedBaseCoordinateType = + !settingValueBool; // When ECEF is true, fixedBaseCoordinateType = 0 (COORD_TYPE_ECEF) + else if (strcmp(settingName, "fixedEcefX") == 0) + settings.fixedEcefX = settingValue; + else if (strcmp(settingName, "fixedEcefY") == 0) + settings.fixedEcefY = settingValue; + else if (strcmp(settingName, "fixedEcefZ") == 0) + settings.fixedEcefZ = settingValue; + else if (strcmp(settingName, "fixedLatText") == 0) + { + double newCoordinate = 0.0; + CoordinateInputType newCoordinateInputType = + coordinateIdentifyInputType((char *)settingValueStr, &newCoordinate); + if (newCoordinateInputType == COORDINATE_INPUT_TYPE_INVALID_UNKNOWN) + settings.fixedLat = 0.0; + else + { + settings.fixedLat = newCoordinate; + settings.coordinateInputType = newCoordinateInputType; + } + } + else if (strcmp(settingName, "fixedLongText") == 0) + { + // Lat defines the settings.coordinateInputType. Don't update it here + double newCoordinate = 0.0; + if (coordinateIdentifyInputType((char *)settingValueStr, &newCoordinate) == + COORDINATE_INPUT_TYPE_INVALID_UNKNOWN) + settings.fixedLong = 0.0; + else + settings.fixedLong = newCoordinate; + } + else if (strcmp(settingName, "fixedAltitude") == 0) + settings.fixedAltitude = settingValue; + else if (strcmp(settingName, "dataPortBaud") == 0) + settings.dataPortBaud = settingValue; + else if (strcmp(settingName, "radioPortBaud") == 0) + settings.radioPortBaud = settingValue; + else if (strcmp(settingName, "enableUART2UBXIn") == 0) + settings.enableUART2UBXIn = settingValueBool; + else if (strcmp(settingName, "enableLogging") == 0) + settings.enableLogging = settingValueBool; + else if (strcmp(settingName, "enableARPLogging") == 0) + settings.enableARPLogging = settingValueBool; + else if (strcmp(settingName, "ARPLoggingInterval") == 0) + settings.ARPLoggingInterval_s = settingValue; + else if (strcmp(settingName, "dataPortChannel") == 0) + settings.dataPortChannel = (muxConnectionType_e)settingValue; + else if (strcmp(settingName, "autoIMUmountAlignment") == 0) + settings.autoIMUmountAlignment = settingValueBool; + else if (strcmp(settingName, "enableSensorFusion") == 0) + settings.enableSensorFusion = settingValueBool; + else if (strcmp(settingName, "enableResetDisplay") == 0) + settings.enableResetDisplay = settingValueBool; + + else if (strcmp(settingName, "enableExternalPulse") == 0) + settings.enableExternalPulse = settingValueBool; + else if (strcmp(settingName, "externalPulseTimeBetweenPulse_us") == 0) + settings.externalPulseTimeBetweenPulse_us = settingValue; + else if (strcmp(settingName, "externalPulseLength_us") == 0) + settings.externalPulseLength_us = settingValue; + else if (strcmp(settingName, "externalPulsePolarity") == 0) + settings.externalPulsePolarity = (pulseEdgeType_e)settingValue; + else if (strcmp(settingName, "enableExternalHardwareEventLogging") == 0) + settings.enableExternalHardwareEventLogging = settingValueBool; + else if (strcmp(settingName, "profileName") == 0) + { + strcpy(settings.profileName, settingValueStr); + setProfileName(profileNumber); // Copy the current settings.profileName into the array of profile names at + // location profileNumber + } + else if (strcmp(settingName, "enableNtripServer") == 0) + settings.enableNtripServer = settingValueBool; + + else if (strcmp(settingName, "enableNtripClient") == 0) + settings.enableNtripClient = settingValueBool; + else if (strcmp(settingName, "ntripClient_CasterHost") == 0) + strcpy(settings.ntripClient_CasterHost, settingValueStr); + else if (strcmp(settingName, "ntripClient_CasterPort") == 0) + settings.ntripClient_CasterPort = settingValue; + else if (strcmp(settingName, "ntripClient_CasterUser") == 0) + strcpy(settings.ntripClient_CasterUser, settingValueStr); + else if (strcmp(settingName, "ntripClient_CasterUserPW") == 0) + strcpy(settings.ntripClient_CasterUserPW, settingValueStr); + else if (strcmp(settingName, "ntripClient_MountPoint") == 0) + strcpy(settings.ntripClient_MountPoint, settingValueStr); + else if (strcmp(settingName, "ntripClient_MountPointPW") == 0) + strcpy(settings.ntripClient_MountPointPW, settingValueStr); + else if (strcmp(settingName, "ntripClient_TransmitGGA") == 0) + settings.ntripClient_TransmitGGA = settingValueBool; + + else if (strcmp(settingName, "serialTimeoutGNSS") == 0) + settings.serialTimeoutGNSS = settingValue; + else if (strcmp(settingName, "pointPerfectDeviceProfileToken") == 0) + strcpy(settings.pointPerfectDeviceProfileToken, settingValueStr); + else if (strcmp(settingName, "enablePointPerfectCorrections") == 0) + settings.enablePointPerfectCorrections = settingValueBool; + else if (strcmp(settingName, "autoKeyRenewal") == 0) + settings.autoKeyRenewal = settingValueBool; + else if (strcmp(settingName, "antennaHeight") == 0) + settings.antennaHeight = settingValue; + else if (strcmp(settingName, "antennaReferencePoint") == 0) + settings.antennaReferencePoint = settingValue; + else if (strcmp(settingName, "bluetoothRadioType") == 0) + settings.bluetoothRadioType = (BluetoothRadioType_e)settingValue; // 0 = SPP, 1 = BLE, 2 = Off + else if (strcmp(settingName, "espnowBroadcast") == 0) + settings.espnowBroadcast = settingValueBool; + else if (strcmp(settingName, "radioType") == 0) + settings.radioType = (RadioType_e)settingValue; // 0 = Radio off, 1 = ESP-Now + else if (strcmp(settingName, "baseRoverSetup") == 0) + { + // 0 = Rover, 1 = Base, 2 = NTP + settings.lastState = STATE_ROVER_NOT_STARTED; // Default + if (settingValue == 1) + settings.lastState = STATE_BASE_NOT_STARTED; + if (settingValue == 2) + settings.lastState = STATE_NTPSERVER_NOT_STARTED; + } + else if (strstr(settingName, "stationECEF") != nullptr) + { + replaceCharacter((char *)settingValueStr, ' ', ','); // Replace all ' ' with ',' before recording to file + recordLineToSD(stationCoordinateECEFFileName, settingValueStr); + recordLineToLFS(stationCoordinateECEFFileName, settingValueStr); + if (settings.debugWiFiConfig == true) + systemPrintf("%s recorded\r\n", settingValueStr); + } + else if (strstr(settingName, "stationGeodetic") != nullptr) + { + replaceCharacter((char *)settingValueStr, ' ', ','); // Replace all ' ' with ',' before recording to file + recordLineToSD(stationCoordinateGeodeticFileName, settingValueStr); + recordLineToLFS(stationCoordinateGeodeticFileName, settingValueStr); + if (settings.debugWiFiConfig == true) + systemPrintf("%s recorded\r\n", settingValueStr); + } + else if (strcmp(settingName, "pvtServerPort") == 0) + settings.pvtServerPort = settingValue; + else if (strcmp(settingName, "pvtUdpServerPort") == 0) + settings.pvtUdpServerPort = settingValue; + else if (strcmp(settingName, "wifiConfigOverAP") == 0) + { + if (settingValue == 1) // Drop downs come back as a value + settings.wifiConfigOverAP = true; + else + settings.wifiConfigOverAP = false; + } + + else if (strcmp(settingName, "enablePvtClient") == 0) + settings.enablePvtClient = settingValueBool; + else if (strcmp(settingName, "enablePvtServer") == 0) + settings.enablePvtServer = settingValueBool; + else if (strcmp(settingName, "enablePvtUdpServer") == 0) + settings.enablePvtUdpServer = settingValueBool; + else if (strcmp(settingName, "enableRCFirmware") == 0) + enableRCFirmware = settingValueBool; + else if (strcmp(settingName, "minElev") == 0) + settings.minElev = settingValue; + else if (strcmp(settingName, "imuYaw") == 0) + settings.imuYaw = settingValue * 100; // Comes in as 0 to 360.0 but stored as 0 to 36,000 + else if (strcmp(settingName, "imuPitch") == 0) + settings.imuPitch = settingValue * 100; // Comes in as -90 to 90.0 but stored as -9000 to 9000 + else if (strcmp(settingName, "imuRoll") == 0) + settings.imuRoll = settingValue * 100; // Comes in as -180 to 180.0 but stored as -18000 to 18000 + else if (strcmp(settingName, "sfDisableWheelDirection") == 0) + settings.sfDisableWheelDirection = settingValueBool; + else if (strcmp(settingName, "sfCombineWheelTicks") == 0) + settings.sfCombineWheelTicks = settingValueBool; + else if (strcmp(settingName, "rateNavPrio") == 0) + settings.rateNavPrio = settingValue; + else if (strcmp(settingName, "minCNO") == 0) + { + if (zedModuleType == PLATFORM_F9R) + settings.minCNO_F9R = settingValue; + else + settings.minCNO_F9P = settingValue; + } + + else if (strcmp(settingName, "ethernetDHCP") == 0) + settings.ethernetDHCP = settingValueBool; + else if (strcmp(settingName, "ethernetIP") == 0) + { + String tempString = String(settingValueStr); + settings.ethernetIP.fromString(tempString); + } + else if (strcmp(settingName, "ethernetDNS") == 0) + { + String tempString = String(settingValueStr); + settings.ethernetDNS.fromString(tempString); + } + else if (strcmp(settingName, "ethernetGateway") == 0) + { + String tempString = String(settingValueStr); + settings.ethernetGateway.fromString(tempString); + } + else if (strcmp(settingName, "ethernetSubnet") == 0) + { + String tempString = String(settingValueStr); + settings.ethernetSubnet.fromString(tempString); + } + else if (strcmp(settingName, "httpPort") == 0) + settings.httpPort = settingValue; + else if (strcmp(settingName, "ethernetNtpPort") == 0) + settings.ethernetNtpPort = settingValue; + else if (strcmp(settingName, "pvtClientPort") == 0) + settings.pvtClientPort = settingValue; + else if (strcmp(settingName, "pvtClientHost") == 0) + strcpy(settings.pvtClientHost, settingValueStr); + + // Network layer + else if (strcmp(settingName, "defaultNetworkType") == 0) + settings.defaultNetworkType = settingValue; + else if (strcmp(settingName, "enableNetworkFailover") == 0) + settings.enableNetworkFailover = settingValue; + + // NTP + else if (strcmp(settingName, "ntpPollExponent") == 0) + settings.ntpPollExponent = settingValue; + else if (strcmp(settingName, "ntpPrecision") == 0) + settings.ntpPrecision = settingValue; + else if (strcmp(settingName, "ntpRootDelay") == 0) + settings.ntpRootDelay = settingValue; + else if (strcmp(settingName, "ntpRootDispersion") == 0) + settings.ntpRootDispersion = settingValue; + else if (strcmp(settingName, "ntpReferenceId") == 0) + { + strcpy(settings.ntpReferenceId, settingValueStr); + for (int i = strlen(settingValueStr); i < 5; i++) + settings.ntpReferenceId[i] = 0; + } + else if (strcmp(settingName, "mdnsEnable") == 0) + settings.mdnsEnable = settingValueBool; + + // Automatic firmware update settings + else if (strcmp(settingName, "enableAutoFirmwareUpdate") == 0) + settings.enableAutoFirmwareUpdate = settingValueBool; + else if (strcmp(settingName, "autoFirmwareCheckMinutes") == 0) + settings.autoFirmwareCheckMinutes = settingValueBool; + + else if (strcmp(settingName, "geographicRegion") == 0) + settings.geographicRegion = settingValue; + + // Unused variables - read to avoid errors + else if (strcmp(settingName, "measurementRateSec") == 0) + { + } + else if (strcmp(settingName, "baseTypeSurveyIn") == 0) + { + } + else if (strcmp(settingName, "fixedBaseCoordinateTypeGeo") == 0) + { + } + else if (strcmp(settingName, "saveToArduino") == 0) + { + } + else if (strcmp(settingName, "enableFactoryDefaults") == 0) + { + } + else if (strcmp(settingName, "enableFirmwareUpdate") == 0) + { + } + else if (strcmp(settingName, "enableForgetRadios") == 0) + { + } + else if (strcmp(settingName, "nicknameECEF") == 0) + { + } + else if (strcmp(settingName, "nicknameGeodetic") == 0) + { + } + else if (strcmp(settingName, "fileSelectAll") == 0) + { + } + else if (strcmp(settingName, "fixedHAE_APC") == 0) + { + } + else if (strcmp(settingName, "measurementRateSecBase") == 0) + { + } + + // Special actions + else if (strcmp(settingName, "firmwareFileName") == 0) + { + mountSDThenUpdate(settingValueStr); + + // If update is successful, it will force system reset and not get here. + + if (productVariant == REFERENCE_STATION) + requestChangeState(STATE_BASE_NOT_STARTED); // If update failed, return to Base mode. + else + requestChangeState(STATE_ROVER_NOT_STARTED); // If update failed, return to Rover mode. + } + else if (strcmp(settingName, "factoryDefaultReset") == 0) + factoryReset(false); // We do not have the sdSemaphore + else if (strcmp(settingName, "exitAndReset") == 0) + { + // Confirm receipt + if (settings.debugWiFiConfig == true) + systemPrintln("Sending reset confirmation"); + + websocket->textAll("confirmReset,1,"); + delay(500); // Allow for delivery + + if (configureViaEthernet) + systemPrintln("Reset after Configure-Via-Ethernet"); + else + systemPrintln("Reset after AP Config"); + + if (configureViaEthernet) + { + ethernetWebServerStopESP32W5500(); + + // We need to exit configure-via-ethernet mode. + // But if the settings have not been saved then lastState will still be STATE_CONFIG_VIA_ETH_STARTED. + // If that is true, then force exit to Base mode. I think it is the best we can do. + //(If the settings have been saved, then the code will restart in NTP, Base or Rover mode as desired.) + if (settings.lastState == STATE_CONFIG_VIA_ETH_STARTED) + { + systemPrintln("Settings were not saved. Resetting into Base mode."); + settings.lastState = STATE_BASE_NOT_STARTED; + recordSystemSettings(); + } + } + + ESP.restart(); + } + else if (strcmp(settingName, "setProfile") == 0) + { + // Change to new profile + if (settings.debugWiFiConfig == true) + systemPrintf("Changing to profile number %d\r\n", settingValue); + changeProfileNumber(settingValue); + + // Load new profile into system + loadSettings(); + + // Send new settings to browser. Re-use settingsCSV to avoid stack. + memset(settingsCSV, 0, AP_CONFIG_SETTING_SIZE); // Clear any garbage from settings array + + createSettingsString(settingsCSV); + + if (settings.debugWiFiConfig == true) + { + systemPrintf("Sending profile %d\r\n", settingValue); + systemPrintf("Profile contents: %s\r\n", settingsCSV); + } + websocket->textAll(settingsCSV); + } + else if (strcmp(settingName, "resetProfile") == 0) + { + settingsToDefaults(); // Overwrite our current settings with defaults + + recordSystemSettings(); // Overwrite profile file and NVM with these settings + + // Get bitmask of active profiles + activeProfiles = loadProfileNames(); + + // Send new settings to browser. Re-use settingsCSV to avoid stack. + memset(settingsCSV, 0, AP_CONFIG_SETTING_SIZE); // Clear any garbage from settings array + + createSettingsString(settingsCSV); + + if (settings.debugWiFiConfig == true) + { + systemPrintf("Sending reset profile %d\r\n", settingValue); + systemPrintf("Profile contents: %s\r\n", settingsCSV); + } + websocket->textAll(settingsCSV); + } + else if (strcmp(settingName, "forgetEspNowPeers") == 0) + { + // Forget all ESP-Now Peers + for (int x = 0; x < settings.espnowPeerCount; x++) + espnowRemovePeer(settings.espnowPeers[x]); + settings.espnowPeerCount = 0; + } + else if (strcmp(settingName, "startNewLog") == 0) + { + if (settings.enableLogging == true && online.logging == true) + { + endLogging(false, true); //(gotSemaphore, releaseSemaphore) Close file. Reset parser stats. + beginLogging(); // Create new file based on current RTC. + setLoggingType(); // Determine if we are standard, PPP, or custom. Changes logging icon accordingly. + + char newFileNameCSV[sizeof("logFileName,") + sizeof(logFileName) + 1]; + snprintf(newFileNameCSV, sizeof(newFileNameCSV), "logFileName,%s,", logFileName); + + websocket->textAll(newFileNameCSV); // Tell the config page the name of the file we just created + } + } + else if (strcmp(settingName, "checkNewFirmware") == 0) + { + if (settings.debugWiFiConfig == true) + systemPrintln("Checking for new OTA Pull firmware"); + + websocket->textAll("checkingNewFirmware,1,"); // Tell the config page we received their request + + char reportedVersion[20]; + char newVersionCSV[100]; + + // Get firmware version from server + if (otaCheckVersion(reportedVersion, sizeof(reportedVersion))) + { + // We got a version number, now determine if it's newer or not + char currentVersion[21]; + getFirmwareVersion(currentVersion, sizeof(currentVersion), enableRCFirmware); + if (isReportedVersionNewer(reportedVersion, currentVersion) == true) + { + if (settings.debugWiFiConfig == true) + systemPrintln("New version detected"); + snprintf(newVersionCSV, sizeof(newVersionCSV), "newFirmwareVersion,%s,", reportedVersion); + } + else + { + if (settings.debugWiFiConfig == true) + systemPrintln("No new firmware available"); + snprintf(newVersionCSV, sizeof(newVersionCSV), "newFirmwareVersion,CURRENT,"); + } + } + else + { + // Failed to get version number + if (settings.debugWiFiConfig == true) + systemPrintln("Sending error to AP config page"); + snprintf(newVersionCSV, sizeof(newVersionCSV), "newFirmwareVersion,ERROR,"); + } + + websocket->textAll(newVersionCSV); + } + else if (strcmp(settingName, "getNewFirmware") == 0) + { + if (settings.debugWiFiConfig == true) + systemPrintln("Getting new OTA Pull firmware"); + + websocket->textAll("gettingNewFirmware,1,"); // Tell the config page we received their request + + apConfigFirmwareUpdateInProcess = true; + otaUpdate(); + + // We get here if WiFi failed to connect + websocket->textAll("gettingNewFirmware,ERROR,"); + } + + // Check for bulk settings (constellations and message rates) + // Must be last on else list + else + { + bool knownSetting = false; + + // Scan for WiFi credentials + if (knownSetting == false) + { + for (int x = 0; x < MAX_WIFI_NETWORKS; x++) + { + char tempString[100]; // wifiNetwork0Password=parachutes + snprintf(tempString, sizeof(tempString), "wifiNetwork%dSSID", x); + if (strcmp(settingName, tempString) == 0) + { + strcpy(settings.wifiNetworks[x].ssid, settingValueStr); + knownSetting = true; + break; + } + else + { + snprintf(tempString, sizeof(tempString), "wifiNetwork%dPassword", x); + if (strcmp(settingName, tempString) == 0) + { + strcpy(settings.wifiNetworks[x].password, settingValueStr); + knownSetting = true; + break; + } + } + } + } + + // Scan for constellation settings + if (knownSetting == false) + { + for (int x = 0; x < MAX_CONSTELLATIONS; x++) + { + char tempString[50]; // ubxConstellationsSBAS + snprintf(tempString, sizeof(tempString), "ubxConstellations%s", settings.ubxConstellations[x].textName); + + if (strcmp(settingName, tempString) == 0) + { + settings.ubxConstellations[x].enabled = settingValueBool; + knownSetting = true; + break; + } + } + } + + // Scan for message settings + if (knownSetting == false) + { + char tempString[50]; + + for (int x = 0; x < MAX_UBX_MSG; x++) + { + snprintf(tempString, sizeof(tempString), "%s", ubxMessages[x].msgTextName); // UBX_RTCM_1074 + if (strcmp(settingName, tempString) == 0) + { + settings.ubxMessageRates[x] = settingValue; + knownSetting = true; + break; + } + } + } + + // Scan for Base RTCM message settings + if (knownSetting == false) + { + int firstRTCMRecord = getMessageNumberByName("UBX_RTCM_1005"); + + char tempString[50]; + for (int x = 0; x < MAX_UBX_MSG_RTCM; x++) + { + snprintf(tempString, sizeof(tempString), "%sBase", + ubxMessages[firstRTCMRecord + x].msgTextName); // UBX_RTCM_1074Base + if (strcmp(settingName, tempString) == 0) + { + settings.ubxMessageRatesBase[x] = settingValue; + knownSetting = true; + break; + } + } + } + + // Scan for ntripServerCasterHost + if (knownSetting == false) + { + for (int serverIndex = 0; serverIndex < NTRIP_SERVER_MAX; serverIndex++) + { + char tempString[50]; + snprintf(tempString, sizeof(tempString), "ntripServer_CasterHost_%d", serverIndex); + if (strcmp(settingName, tempString) == 0) + { + strcpy(&settings.ntripServer_CasterHost[serverIndex][0], settingValueStr); + knownSetting = true; + break; + } + } + } + + // Scan for ntripServerCasterPort + if (knownSetting == false) + { + for (int serverIndex = 0; serverIndex < NTRIP_SERVER_MAX; serverIndex++) + { + char tempString[50]; + snprintf(tempString, sizeof(tempString), "ntripServer_CasterPort_%d", serverIndex); + if (strcmp(settingName, tempString) == 0) + { + settings.ntripServer_CasterPort[serverIndex] = settingValue; + knownSetting = true; + break; + } + } + } + + // Scan for ntripServerCasterUser + if (knownSetting == false) + { + for (int serverIndex = 0; serverIndex < NTRIP_SERVER_MAX; serverIndex++) + { + char tempString[50]; + snprintf(tempString, sizeof(tempString), "ntripServer_CasterUser_%d", serverIndex); + if (strcmp(settingName, tempString) == 0) + { + strcpy(&settings.ntripServer_CasterUser[serverIndex][0], settingValueStr); + knownSetting = true; + break; + } + } + } + + // Scan for ntripServerCasterUserPW + if (knownSetting == false) + { + for (int serverIndex = 0; serverIndex < NTRIP_SERVER_MAX; serverIndex++) + { + char tempString[50]; + snprintf(tempString, sizeof(tempString), "ntripServer_CasterUserPW_%d", serverIndex); + if (strcmp(settingName, tempString) == 0) + { + strcpy(&settings.ntripServer_CasterUserPW[serverIndex][0], settingValueStr); + knownSetting = true; + break; + } + } + } + + // Scan for ntripServerMountPoint + if (knownSetting == false) + { + for (int serverIndex = 0; serverIndex < NTRIP_SERVER_MAX; serverIndex++) + { + char tempString[50]; + snprintf(tempString, sizeof(tempString), "ntripServer_MountPoint_%d", serverIndex); + if (strcmp(settingName, tempString) == 0) + { + strcpy(&settings.ntripServer_MountPoint[serverIndex][0], settingValueStr); + knownSetting = true; + break; + } + } + } + + // Scan for ntripServerMountPointPW + if (knownSetting == false) + { + for (int serverIndex = 0; serverIndex < NTRIP_SERVER_MAX; serverIndex++) + { + char tempString[50]; + snprintf(tempString, sizeof(tempString), "ntripServer_MountPointPW_%d", serverIndex); + if (strcmp(settingName, tempString) == 0) + { + strcpy(&settings.ntripServer_MountPointPW[serverIndex][0], settingValueStr); + knownSetting = true; + break; + } + } + } + + // Last catch + if (knownSetting == false) + { + systemPrintf("Unknown '%s': %0.3lf\r\n", settingName, settingValue); + } + } // End last strcpy catch +} + +// Add record with int +void stringRecord(char *settingsCSV, const char *id, int settingValue) +{ + char record[100]; + snprintf(record, sizeof(record), "%s,%d,", id, settingValue); + strcat(settingsCSV, record); +} + +// Add record with uint32_t +void stringRecord(char *settingsCSV, const char *id, uint32_t settingValue) +{ + char record[100]; + snprintf(record, sizeof(record), "%s,%d,", id, settingValue); + strcat(settingsCSV, record); +} + +// Add record with double +void stringRecord(char *settingsCSV, const char *id, double settingValue, int decimalPlaces) +{ + char format[10]; + snprintf(format, sizeof(format), "%%0.%dlf", decimalPlaces); // Create '%0.09lf' + + char formattedValue[20]; + snprintf(formattedValue, sizeof(formattedValue), format, settingValue); + + char record[100]; + snprintf(record, sizeof(record), "%s,%s,", id, formattedValue); + strcat(settingsCSV, record); +} + +// Add record with bool +void stringRecord(char *settingsCSV, const char *id, bool settingValue) +{ + char temp[10]; + if (settingValue == true) + strcpy(temp, "true"); + else + strcpy(temp, "false"); + + char record[100]; + snprintf(record, sizeof(record), "%s,%s,", id, temp); + strcat(settingsCSV, record); +} + +// Add record with string +void stringRecord(char *settingsCSV, const char *id, char *settingValue) +{ + char record[100]; + snprintf(record, sizeof(record), "%s,%s,", id, settingValue); + strcat(settingsCSV, record); +} + +// Add record with uint64_t +void stringRecord(char *settingsCSV, const char *id, uint64_t settingValue) +{ + char record[100]; + snprintf(record, sizeof(record), "%s,%lld,", id, settingValue); + strcat(settingsCSV, record); +} + +// Break CSV into setting constituents +// Can't use strtok because we may have two commas next to each other, ie +// measurementRateHz,4.00,measurementRateSec,,dynamicModel,0, +bool parseIncomingSettings() +{ + char settingName[100] = {'\0'}; + char valueStr[150] = {'\0'}; // stationGeodetic1,ANameThatIsTooLongToBeDisplayed 40.09029479 -105.18505761 1560.089 + + char *commaPtr = incomingSettings; + char *headPtr = incomingSettings; + + int counter = 0; + int maxAttempts = 500; + while (*headPtr) // Check if we've reached the end of the string + { + // Spin to first comma + commaPtr = strstr(headPtr, ","); + if (commaPtr != nullptr) + { + *commaPtr = '\0'; + strcpy(settingName, headPtr); + headPtr = commaPtr + 1; + } + + commaPtr = strstr(headPtr, ","); + if (commaPtr != nullptr) + { + *commaPtr = '\0'; + strcpy(valueStr, headPtr); + headPtr = commaPtr + 1; + } + + // log_d("settingName: %s value: %s", settingName, valueStr); + + updateSettingWithValue(settingName, valueStr); + + // Avoid infinite loop if response is malformed + counter++; + if (counter == maxAttempts) + { + systemPrintln("Error: Incoming settings malformed."); + break; + } + } + + if (counter < maxAttempts) + { + // Confirm receipt + if (settings.debugWiFiConfig == true) + systemPrintln("Sending receipt confirmation of settings"); + websocket->textAll("confirmDataReceipt,1,"); + } + + return (true); +} + +// When called, responds with the root folder list of files on SD card +// Name and size are formatted in CSV, formatted to html by JS +void getFileList(String &returnText) +{ + returnText = ""; + + // Update the SD Size and Free Space + String cardSize; + stringHumanReadableSize(cardSize, sdCardSize); + returnText += "sdSize," + cardSize + ","; + String freeSpace; + stringHumanReadableSize(freeSpace, sdFreeSpace); + returnText += "sdFreeSpace," + freeSpace + ","; + + char fileName[50]; // Handle long file names + + // Attempt to gain access to the SD card + if (xSemaphoreTake(sdCardSemaphore, fatSemaphore_longWait_ms) == pdPASS) + { + markSemaphore(FUNCTION_FILEMANAGER_UPLOAD1); + + if (USE_SPI_MICROSD) + { + SdFile root; + root.open("/"); // Open root + SdFile file; + uint16_t fileCount = 0; + + while (file.openNext(&root, O_READ)) + { + if (file.isFile()) + { + fileCount++; + + file.getName(fileName, sizeof(fileName)); + + String fileSize; + stringHumanReadableSize(fileSize, file.fileSize()); + returnText += "fmName," + String(fileName) + ",fmSize," + fileSize + ","; + } + } + + root.close(); + file.close(); + } +#ifdef COMPILE_SD_MMC + else + { + File root = SD_MMC.open("/"); // Open root + + if (root && root.isDirectory()) + { + uint16_t fileCount = 0; + + File file = root.openNextFile(); + while (file) + { + if (!file.isDirectory()) + { + fileCount++; + + String fileSize; + stringHumanReadableSize(fileSize, file.size()); + returnText += "fmName," + String(file.name()) + ",fmSize," + fileSize + ","; + } + + file = root.openNextFile(); + } + } + + root.close(); + } +#endif // COMPILE_SD_MMC + + xSemaphoreGive(sdCardSemaphore); + } + else + { + char semaphoreHolder[50]; + getSemaphoreFunction(semaphoreHolder); + + // This is an error because the current settings no longer match the settings + // on the microSD card, and will not be restored to the expected settings! + systemPrintf("sdCardSemaphore failed to yield, held by %s, Form.ino line %d\r\n", semaphoreHolder, __LINE__); + } + + if (settings.debugWiFiConfig == true) + systemPrintf("returnText (%d bytes): %s\r\n", returnText.length(), returnText.c_str()); +} + +// When called, responds with the messages supported on this platform +// Message name and current rate are formatted in CSV, formatted to html by JS +void createMessageList(String &returnText) +{ + returnText = ""; + + for (int messageNumber = 0; messageNumber < MAX_UBX_MSG; messageNumber++) + { + if (messageSupported(messageNumber) == true) + returnText += String(ubxMessages[messageNumber].msgTextName) + "," + + String(settings.ubxMessageRates[messageNumber]) + ","; // UBX_RTCM_1074,4, + } + + if (settings.debugWiFiConfig == true) + systemPrintf("returnText (%d bytes): %s\r\n", returnText.length(), returnText.c_str()); +} + +// When called, responds with the RTCM/Base messages supported on this platform +// Message name and current rate are formatted in CSV, formatted to html by JS +void createMessageListBase(String &returnText) +{ + returnText = ""; + + int firstRTCMRecord = getMessageNumberByName("UBX_RTCM_1005"); + + for (int messageNumber = 0; messageNumber < MAX_UBX_MSG_RTCM; messageNumber++) + { + if (messageSupported(firstRTCMRecord + messageNumber) == true) + returnText += String(ubxMessages[messageNumber + firstRTCMRecord].msgTextName) + "Base," + + String(settings.ubxMessageRatesBase[messageNumber]) + ","; // UBX_RTCM_1074Base,4, + } + + if (settings.debugWiFiConfig == true) + systemPrintf("returnText (%d bytes): %s\r\n", returnText.length(), returnText.c_str()); +} + +// Handles uploading of user files to SD +void handleUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) +{ + String logmessage = ""; + + if (!index) + { + logmessage = "Upload Start: " + String(filename); + + int fileNameLen = filename.length(); + char tempFileName[fileNameLen + 2] = {'/'}; // Filename must start with / or VERY bad things happen on SD_MMC + filename.toCharArray(&tempFileName[1], fileNameLen + 1); + tempFileName[fileNameLen + 1] = '\0'; // Terminate array + + // Allocate the managerTempFile + if (!managerTempFile) + { + managerTempFile = new FileSdFatMMC; + if (!managerTempFile) + { + systemPrintln("Failed to allocate managerTempFile!"); + return; + } + } + // Attempt to gain access to the SD card + if (xSemaphoreTake(sdCardSemaphore, fatSemaphore_longWait_ms) == pdPASS) + { + markSemaphore(FUNCTION_FILEMANAGER_UPLOAD1); + + managerTempFile->open(tempFileName, O_CREAT | O_APPEND | O_WRITE); + + xSemaphoreGive(sdCardSemaphore); + } + + systemPrintln(logmessage); + } + + if (len) + { + // Attempt to gain access to the SD card + if (xSemaphoreTake(sdCardSemaphore, fatSemaphore_longWait_ms) == pdPASS) + { + markSemaphore(FUNCTION_FILEMANAGER_UPLOAD2); + + managerTempFile->write(data, len); // stream the incoming chunk to the opened file + + xSemaphoreGive(sdCardSemaphore); + } + } + + if (final) + { + logmessage = "Upload Complete: " + String(filename) + ",size: " + String(index + len); + + // Attempt to gain access to the SD card + if (xSemaphoreTake(sdCardSemaphore, fatSemaphore_longWait_ms) == pdPASS) + { + markSemaphore(FUNCTION_FILEMANAGER_UPLOAD3); + + managerTempFile->updateFileCreateTimestamp(); // Update the file create time & date + + managerTempFile->close(); + + xSemaphoreGive(sdCardSemaphore); + } + + systemPrintln(logmessage); + request->redirect("/"); + } +} + +#endif // COMPILE_AP diff --git a/Firmware/RTK_Surveyor/GpsMessageParser.h b/Firmware/RTK_Surveyor/GpsMessageParser.h new file mode 100644 index 000000000..50a32c8ef --- /dev/null +++ b/Firmware/RTK_Surveyor/GpsMessageParser.h @@ -0,0 +1,146 @@ +/*------------------------------------------------------------------------------ +GpsMessageParser.h + + Constant and routine declarations for the GPS message parser. +------------------------------------------------------------------------------*/ + +#ifndef __GPS_MESSAGE_PARSER_H__ +#define __GPS_MESSAGE_PARSER_H__ + +#include + +#include "crc24q.h" // 24-bit CRC-24Q cyclic redundancy checksum for RTCM parsing + +//---------------------------------------- +// Constants +//---------------------------------------- + +#define PARSE_BUFFER_LENGTH 3000 // Some USB RAWX messages can be > 2k + +enum +{ + SENTENCE_TYPE_NONE = 0, + + // Add new sentence types below in alphabetical order + SENTENCE_TYPE_NMEA, + SENTENCE_TYPE_RTCM, + SENTENCE_TYPE_UBX, +}; + +//---------------------------------------- +// Types +//---------------------------------------- + +typedef struct _PARSE_STATE *P_PARSE_STATE; + +// Parse routine +typedef uint8_t (*PARSE_ROUTINE)(P_PARSE_STATE parse, // Parser state + uint8_t data); // Incoming data byte + +// End of message callback routine +typedef void (*PARSE_EOM_CALLBACK)(P_PARSE_STATE parse, // Parser state + uint8_t type); // Message type + +typedef struct _PARSE_STATE +{ + PARSE_ROUTINE state; // Parser state routine + PARSE_EOM_CALLBACK eomCallback; // End of message callback routine + const char *parserName; // Name of parser + uint32_t crc; // RTCM computed CRC + uint32_t rtcmCrc; // Computed CRC value for the RTCM message + uint32_t invalidRtcmCrcs; // Number of bad RTCM CRCs detected + uint16_t bytesRemaining; // Bytes remaining in RTCM CRC calculation + uint16_t length; // Message length including line termination + uint16_t maxLength; // Maximum message length including line termination + uint16_t message; // RTCM message number + uint16_t nmeaLength; // Length of the NMEA message without line termination + uint8_t buffer[PARSE_BUFFER_LENGTH]; // Buffer containing the message + uint8_t nmeaMessageName[16]; // Message name + uint8_t nmeaMessageNameLength; // Length of the message name + uint8_t ck_a; // U-blox checksum byte 1 + uint8_t ck_b; // U-blox checksum byte 2 + bool computeCrc; // Compute the CRC when true +} PARSE_STATE; + +//---------------------------------------- +// Macros +//---------------------------------------- + +#ifdef PARSE_NMEA_MESSAGES +#define NMEA_PREAMBLE nmeaPreamble, +#else +#define NMEA_PREAMBLE +#endif // PARSE_NMEA_MESSAGES + +#ifdef PARSE_RTCM_MESSAGES +#define RTCM_PREAMBLE rtcmPreamble, +#else +#define RTCM_PREAMBLE +#endif // PARSE_RTCM_MESSAGES + +#ifdef PARSE_UBLOX_MESSAGES +#define UBLOX_PREAMBLE ubloxPreamble, +#else +#define UBLOX_PREAMBLE +#endif // PARSE_UBLOX_MESSAGES + +#define GPS_PARSE_TABLE \ +PARSE_ROUTINE const gpsParseTable[] = \ +{ \ + NMEA_PREAMBLE \ + RTCM_PREAMBLE \ + UBLOX_PREAMBLE \ +}; \ + \ +const int gpsParseTableEntries = sizeof(gpsParseTable) / sizeof(gpsParseTable[0]); + +//---------------------------------------- +// External values +//---------------------------------------- + +extern PARSE_ROUTINE const gpsParseTable[]; +extern const int gpsParseTableEntries; + +//---------------------------------------- +// External routines +//---------------------------------------- + +// Main parser routine +uint8_t gpsMessageParserFirstByte(PARSE_STATE *parse, uint8_t data); + +// NMEA parse routines +uint8_t nmeaPreamble(PARSE_STATE *parse, uint8_t data); +uint8_t nmeaFindFirstComma(PARSE_STATE *parse, uint8_t data); +uint8_t nmeaFindAsterisk(PARSE_STATE *parse, uint8_t data); +uint8_t nmeaChecksumByte1(PARSE_STATE *parse, uint8_t data); +uint8_t nmeaChecksumByte2(PARSE_STATE *parse, uint8_t data); +uint8_t nmeaLineTermination(PARSE_STATE *parse, uint8_t data); + +// RTCM parse routines +uint8_t rtcmPreamble(PARSE_STATE *parse, uint8_t data); +uint8_t rtcmReadLength1(PARSE_STATE *parse, uint8_t data); +uint8_t rtcmReadLength2(PARSE_STATE *parse, uint8_t data); +uint8_t rtcmReadMessage1(PARSE_STATE *parse, uint8_t data); +uint8_t rtcmReadMessage2(PARSE_STATE *parse, uint8_t data); +uint8_t rtcmReadData(PARSE_STATE *parse, uint8_t data); +uint8_t rtcmReadCrc(PARSE_STATE *parse, uint8_t data); + +// u-blox parse routines +uint8_t ubloxPreamble(PARSE_STATE *parse, uint8_t data); +uint8_t ubloxSync2(PARSE_STATE *parse, uint8_t data); +uint8_t ubloxClass(PARSE_STATE *parse, uint8_t data); +uint8_t ubloxId(PARSE_STATE *parse, uint8_t data); +uint8_t ubloxLength1(PARSE_STATE *parse, uint8_t data); +uint8_t ubloxLength2(PARSE_STATE *parse, uint8_t data); +uint8_t ubloxPayload(PARSE_STATE *parse, uint8_t data); +uint8_t ubloxCkA(PARSE_STATE *parse, uint8_t data); +uint8_t ubloxCkB(PARSE_STATE *parse, uint8_t data); + +// External print routines +void printNmeaChecksumError(PARSE_STATE *parse); +void printRtcmChecksumError(PARSE_STATE *parse); +void printRtcmMaxLength(PARSE_STATE *parse); +void printUbloxChecksumError(PARSE_STATE *parse); +void printUbloxInvalidData(PARSE_STATE *parse); + +#endif // __GPS_MESSAGE_PARSER_H__ diff --git a/Firmware/RTK_Surveyor/GpsMessageParser.ino b/Firmware/RTK_Surveyor/GpsMessageParser.ino new file mode 100644 index 000000000..74d5d643c --- /dev/null +++ b/Firmware/RTK_Surveyor/GpsMessageParser.ino @@ -0,0 +1,27 @@ +/*------------------------------------------------------------------------------ +GpsMessageParser.ino + + Parse messages from GPS radios +------------------------------------------------------------------------------*/ + +// Wait for the first byte in the GPS message +uint8_t gpsMessageParserFirstByte(PARSE_STATE *parse, uint8_t data) +{ + int index; + PARSE_ROUTINE parseRoutine; + uint8_t sentenceType; + + // Walk through the parse table + for (index = 0; index < gpsParseTableEntries; index++) + { + parseRoutine = gpsParseTable[index]; + sentenceType = parseRoutine(parse, data); + if (sentenceType) + return sentenceType; + } + + // preamble byte not found + parse->length = 0; + parse->state = gpsMessageParserFirstByte; + return SENTENCE_TYPE_NONE; +} diff --git a/Firmware/RTK_Surveyor/NTP.ino b/Firmware/RTK_Surveyor/NTP.ino new file mode 100644 index 000000000..d07ce6169 --- /dev/null +++ b/Firmware/RTK_Surveyor/NTP.ino @@ -0,0 +1,1022 @@ +/*------------------------------------------------------------------------------ +NTP.ino + + This module implements the network time protocol (NTP). + + NTP Testing using Ethernet: + + Raspberry Pi Setup: + * Install Raspberry Pi OS + * Edit /etc/systemd/timesyncd.conf + * Remove '#" from in front of NTP= line + * Set NTP= line to: + NTP="your NTP server address" "addresses from FallbackNTPK= line" + * without the double quotes + * Force a time update using: + sudo systemctl restart systemd-timesyncd.service + + NTP Testing on Raspberry Pi: + * Log into the Raspberry Pi system + * Start the terminal program + * Display the time server using: + timedatectl timesync-status + * Verify that the Server specifies your NTP server IP address + * Force a time update using: + sudo systemctl restart systemd-timesyncd.service + + Test Setup: + + RTK Reference Station Raspberry Pi + ^ NTP Server + | Ethernet cable ^ + v | + Ethernet Switch <-----------------' + ^ + | Ethernet cable + v + Internet Firewall + ^ + | Ethernet cable + v + Modem + ^ + | + v + Internet + ^ + | + v + NTP Server + +------------------------------------------------------------------------------*/ + +#ifdef COMPILE_ETHERNET + +//---------------------------------------- +// Constants +//---------------------------------------- + +enum NTP_STATE +{ + NTP_STATE_OFF, + NTP_STATE_NETWORK_STARTING, + NTP_STATE_NETWORK_CONNECTED, + NTP_STATE_SERVER_RUNNING, + // Insert new states here + NTP_STATE_MAX +}; + +const char * const ntpServerStateName[] = +{ + "NTP_STATE_OFF", + "NTP_STATE_NETWORK_STARTING", + "NTP_STATE_NETWORK_CONNECTED", + "NTP_STATE_SERVER_RUNNING" +}; +const int ntpServerStateNameEntries = sizeof(ntpServerStateName) / sizeof(ntpServerStateName[0]); + +const RtkMode_t ntpServerMode = RTK_MODE_NTP; + +//---------------------------------------- +// Locals +//---------------------------------------- + +static derivedEthernetUDP *ntpServer; // This will be instantiated when we know the NTP port +static uint8_t ntpServerState; +static volatile uint8_t ntpSockIndex; // The W5500 socket index for NTP - so we can enable and read the correct interrupt +static uint32_t lastLoggedNTPRequest; + +//---------------------------------------- +// Menu to get the NTP settings +//---------------------------------------- + +void menuNTP() +{ + if (!HAS_ETHERNET) + { + clearBuffer(); // Empty buffer of any newline chars + return; + } + + while (1) + { + systemPrintln(); + systemPrintln("Menu: NTP"); + systemPrintln(); + + systemPrint("1) Poll Exponent: 2^"); + systemPrintln(settings.ntpPollExponent); + + systemPrint("2) Precision: 2^"); + systemPrintln(settings.ntpPrecision); + + systemPrint("3) Root Delay (us): "); + systemPrintln(settings.ntpRootDelay); + + systemPrint("4) Root Dispersion (us): "); + systemPrintln(settings.ntpRootDispersion); + + systemPrint("5) Reference ID: "); + systemPrintln(settings.ntpReferenceId); + + systemPrintln("x) Exit"); + + byte incoming = getCharacterNumber(); + + if (incoming == 1) + { + systemPrint("Enter new poll exponent (2^, Min 3, Max 17): "); + long newVal = getNumber(); + if ((newVal >= 3) && (newVal <= 17)) + settings.ntpPollExponent = newVal; + else + systemPrintln("Error: poll exponent out of range"); + } + else if (incoming == 2) + { + systemPrint("Enter new precision (2^, Min -30, Max 0): "); + long newVal = getNumber(); + if ((newVal >= -30) && (newVal <= 0)) + settings.ntpPrecision = newVal; + else + systemPrintln("Error: precision out of range"); + } + else if (incoming == 3) + { + systemPrint("Enter new root delay (us): "); + long newVal = getNumber(); + if ((newVal >= 0) && (newVal <= 1000000)) + settings.ntpRootDelay = newVal; + else + systemPrintln("Error: root delay out of range"); + } + else if (incoming == 4) + { + systemPrint("Enter new root dispersion (us): "); + long newVal = getNumber(); + if ((newVal >= 0) && (newVal <= 1000000)) + settings.ntpRootDispersion = newVal; + else + systemPrintln("Error: root dispersion out of range"); + } + else if (incoming == 5) + { + systemPrint("Enter new Reference ID (4 Chars Max): "); + char newId[5]; + if (getString(newId, 5) == INPUT_RESPONSE_VALID) + { + int i = 0; + for (; i < strlen(newId); i++) + settings.ntpReferenceId[i] = newId[i]; + for (; i < 5; i++) + settings.ntpReferenceId[i] = 0; + } + else + systemPrintln("Error: invalid Reference ID"); + } + else if (incoming == 'x') + break; + else if (incoming == INPUT_RESPONSE_GETCHARACTERNUMBER_EMPTY) + break; + else if (incoming == INPUT_RESPONSE_GETCHARACTERNUMBER_TIMEOUT) + break; + else + printUnknown(incoming); + } + + clearBuffer(); // Empty buffer of any newline chars +} + +// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +// NTP Packet storage and utilities + +struct NTPpacket +{ + static const uint8_t NTPpacketSize = 48; + + uint8_t packet[NTPpacketSize]; // Copy of the NTP packet + void setPacket(uint8_t *ptr) + { + memcpy(packet, ptr, NTPpacketSize); + } + void getPacket(uint8_t *ptr) + { + memcpy(ptr, packet, NTPpacketSize); + } + + const uint32_t NTPtoUnixOffset = 2208988800; // NTP starts at Jan 1st 1900. Unix starts at Jan 1st 1970. + + uint8_t LiVnMode; // Leap Indicator, Version Number, Mode + + // Leap Indicator is 2 bits : + // 00 : No warning + // 01 : Last minute of the day has 61s + // 10 : Last minute of the day has 59 s + // 11 : Alarm condition (clock not synchronized) + const uint8_t defaultLeapInd = 0; + uint8_t LI() + { + return LiVnMode >> 6; + } + void LI(uint8_t val) + { + LiVnMode = (LiVnMode & 0x3F) | ((val & 0x03) << 6); + } + + // Version Number is 3 bits. NTP version is currently four (4) + const uint8_t defaultVersion = 4; + uint8_t VN() + { + return (LiVnMode >> 3) & 0x07; + } + void VN(uint8_t val) + { + LiVnMode = (LiVnMode & 0xC7) | ((val & 0x07) << 3); + } + + // Mode is 3 bits: + // 0 : Reserved + // 1 : Symmetric active + // 2 : Symmetric passive + // 3 : Client + // 4 : Server + // 5 : Broadcast + // 6 : NTP control message + // 7 : Reserved for private use + const uint8_t defaultMode = 4; + uint8_t mode() + { + return (LiVnMode & 0x07); + } + void mode(uint8_t val) + { + LiVnMode = (LiVnMode & 0xF8) | (val & 0x07); + } + + // Stratum is 8 bits: + // 0 : Unspecified + // 1 : Reference clock (e.g., radio clock) + // 2-15 : Secondary server (via NTP) + // 16-255 : Unreachable + // + // We'll use 1 = Reference Clock + const uint8_t defaultStratum = 1; + uint8_t stratum; + + // Poll exponent + // This is an eight-bit unsigned integer indicating the maximum interval between successive messages, + // in seconds to the nearest power of two. + // In the reference implementation, the values can range from 3 (8 s) through 17 (36 h). + // + // RFC 5905 suggests 6-10. We'll use 6. 2^6 = 64 seconds + const uint8_t defaultPollExponent = 6; + uint8_t pollExponent; + + // Precision + // This is an eight-bit signed integer indicating the precision of the system clock, + // in seconds to the nearest power of two. For instance, a value of -18 corresponds to a precision of about 4us. + // + // tAcc is usually around 1us. So we'll use -20 (0xEC). 2^-20 = 0.95us + const int8_t defaultPrecision = -20; // 0xEC + int8_t precision; + + // Root delay + // This is a 32-bit, unsigned, fixed-point number indicating the total round-trip delay to the reference clock, + // in seconds with fraction point between bits 15 and 16. In contrast to the calculated peer round-trip delay, + // which can take both positive and negative values, this value is always positive. + // + // We are the reference clock, so we'll use zero (0x00000000). + const uint32_t defaultRootDelay = 0x00000000; + uint32_t rootDelay; + + // Root dispersion + // This is a 32-bit, unsigned, fixed-point number indicating the maximum error relative to the reference clock, + // in seconds with fraction point between bits 15 and 16. + // + // Tricky... Could depend on interrupt service time? Maybe go with ~1ms? + const uint32_t defaultRootDispersion = 0x00000042; // 1007us + uint32_t rootDispersion; + + // Reference identifier + // This is a 32-bit code identifying the particular reference clock. The interpretation depends on the value in + // the stratum field. For stratum 0 (unsynchronized), this is a four-character ASCII (American Standard Code for + // Information Interchange) string called the kiss code, which is used for debugging and monitoring purposes. + // GPS : Global Positioning System + const uint8_t referenceIdLen = 4; + const char defaultReferenceId[4] = {'G', 'P', 'S', 0}; + char referenceId[4]; + + // Reference timestamp + // This is the local time at which the system clock was last set or corrected, in 64-bit NTP timestamp format. + uint32_t referenceTimestampSeconds; + uint32_t referenceTimestampFraction; + + // Originate timestamp + // This is the local time at which the request departed the client for the server, in 64-bit NTP timestamp format. + uint32_t originateTimestampSeconds; + uint32_t originateTimestampFraction; + + // Receive timestamp + // This is the local time at which the request arrived at the server, in 64-bit NTP timestamp format. + uint32_t receiveTimestampSeconds; + uint32_t receiveTimestampFraction; + + // Transmit timestamp + // This is the local time at which the reply departed the server for the client, in 64-bit NTP timestamp format. + uint32_t transmitTimestampSeconds; + uint32_t transmitTimestampFraction; + + typedef union { + int8_t signed8; + uint8_t unsigned8; + } unsignedSigned8; + + uint32_t extractUnsigned32(uint8_t *ptr) + { + uint32_t val = 0; + val |= *ptr++ << 24; // NTP data is Big-Endian + val |= *ptr++ << 16; + val |= *ptr++ << 8; + val |= *ptr++; + return val; + } + + void insertUnsigned32(uint8_t *ptr, uint32_t val) + { + *ptr++ = val >> 24; // NTP data is Big-Endian + *ptr++ = (val >> 16) & 0xFF; + *ptr++ = (val >> 8) & 0xFF; + *ptr++ = val & 0xFF; + } + + // Extract the data from an NTP packet into the correct fields + void extract() + { + uint8_t *ptr = packet; + + LiVnMode = *ptr++; + stratum = *ptr++; + pollExponent = *ptr++; + + unsignedSigned8 converter8; + converter8.unsigned8 = *ptr++; // Convert to int8_t without ambiguity + precision = converter8.signed8; + + rootDelay = extractUnsigned32(ptr); + ptr += 4; + rootDispersion = extractUnsigned32(ptr); + ptr += 4; + + for (uint8_t i = 0; i < referenceIdLen; i++) + referenceId[i] = *ptr++; + + referenceTimestampSeconds = extractUnsigned32(ptr); + ptr += 4; + referenceTimestampFraction = + extractUnsigned32(ptr); // Note: the fraction is in increments of (1 / 2^32) secs, not microseconds + ptr += 4; + originateTimestampSeconds = extractUnsigned32(ptr); + ptr += 4; + originateTimestampFraction = extractUnsigned32(ptr); + ptr += 4; + receiveTimestampSeconds = extractUnsigned32(ptr); + ptr += 4; + receiveTimestampFraction = extractUnsigned32(ptr); + ptr += 4; + transmitTimestampSeconds = extractUnsigned32(ptr); + ptr += 4; + transmitTimestampFraction = extractUnsigned32(ptr); + ptr += 4; + } + + // Insert the data from the fields into an NTP packet + void insert() + { + uint8_t *ptr = packet; + + *ptr++ = LiVnMode; + *ptr++ = stratum; + *ptr++ = pollExponent; + + unsignedSigned8 converter8; + converter8.signed8 = precision; + *ptr++ = converter8.unsigned8; // Convert to uint8_t without ambiguity + + insertUnsigned32(ptr, rootDelay); + ptr += 4; + insertUnsigned32(ptr, rootDispersion); + ptr += 4; + + for (uint8_t i = 0; i < 4; i++) + *ptr++ = referenceId[i]; + + insertUnsigned32(ptr, referenceTimestampSeconds); + ptr += 4; + insertUnsigned32( + ptr, + referenceTimestampFraction); // Note: the fraction is in increments of (1 / 2^32) secs, not microseconds + ptr += 4; + insertUnsigned32(ptr, originateTimestampSeconds); + ptr += 4; + insertUnsigned32(ptr, originateTimestampFraction); + ptr += 4; + insertUnsigned32(ptr, receiveTimestampSeconds); + ptr += 4; + insertUnsigned32(ptr, receiveTimestampFraction); + ptr += 4; + insertUnsigned32(ptr, transmitTimestampSeconds); + ptr += 4; + insertUnsigned32(ptr, transmitTimestampFraction); + ptr += 4; + } + + uint32_t convertMicrosToSecsAndFraction(uint32_t val) // 16-bit fraction used by root delay and dispersion + { + double secs = val; + secs /= 1000000.0; // Convert micros to seconds + secs = floor(secs); // Convert to integer, round down + + double microsecs = val; + microsecs -= secs * 1000000.0; // Subtract the seconds + microsecs /= 1000000.0; // Convert micros to seconds + microsecs *= pow(2.0, 16.0); // Convert to 16-bit fraction + + uint32_t result = ((uint32_t)secs) << 16; + result |= ((uint32_t)microsecs) & 0xFFFF; + return (result); + } + + uint32_t convertMicrosToFraction(uint32_t val) // 32-bit fraction used by the timestamps + { + val %= 1000000; // Just in case + double v = val; // Convert micros to double + v /= 1000000.0; // Convert micros to seconds + v *= pow(2.0, 32.0); // Convert to fraction + return (uint32_t)v; + } + + uint32_t convertFractionToMicros(uint32_t val) // 32-bit fraction used by the timestamps + { + double v = val; // Convert fraction to double + v /= pow(2.0, 32.0); // Convert fraction to seconds + v *= 1000000.0; // Convert to micros + uint32_t ret = (uint32_t)v; + ret %= 1000000; // Just in case + return ret; + } + + uint32_t convertNTPsecondsToUnix(uint32_t val) + { + return (val - NTPtoUnixOffset); + } + + uint32_t convertUnixSecondsToNTP(uint32_t val) + { + return (val + NTPtoUnixOffset); + } +}; + +// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +// NTP process one request +// recTv contains the timeval the NTP packet was received - from the W5500 interrupt +// syncTv contains the timeval when the RTC was last sync'd +// ntpDiag will contain useful diagnostics +bool ntpProcessOneRequest(bool process, const timeval *recTv, const timeval *syncTv, char *ntpDiag = nullptr, + size_t ntpDiagSize = 0); // Header +bool ntpProcessOneRequest(bool process, const timeval *recTv, const timeval *syncTv, char *ntpDiag, size_t ntpDiagSize) +{ + bool processed = false; + + if (ntpDiag != nullptr) + *ntpDiag = 0; // Clear any existing diagnostics + + int packetDataSize = ntpServer->parsePacket(); + + IPAddress remoteIP = ntpServer->remoteIP(); + uint16_t remotePort = ntpServer->remotePort(); + + if (ntpDiag != nullptr) // Add the packet size and remote IP/Port to the diagnostics + { + snprintf(ntpDiag, ntpDiagSize, "NTP request from: Remote IP: %d.%d.%d.%d Remote Port: %d\r\n", remoteIP[0], + remoteIP[1], remoteIP[2], remoteIP[3], remotePort); + } + + if (packetDataSize && (packetDataSize >= NTPpacket::NTPpacketSize)) + { + // Read the NTP packet + NTPpacket packet; + + ntpServer->read((char *)&packet.packet, NTPpacket::NTPpacketSize); // Copy the NTP data into our packet + + // If process is false, return now + if (!process) + { + char tmpbuf[128]; + snprintf(tmpbuf, sizeof(tmpbuf), + "NTP request ignored. Time has not been synchronized - or not in NTP mode.\r\n"); + strlcat(ntpDiag, tmpbuf, ntpDiagSize); + return false; + } + + packet.extract(); // Extract the raw data into fields + + packet.LI(packet.defaultLeapInd); // Clear the leap second adjustment. TODO: set this correctly using + // getLeapSecondEvent from the GNSS + packet.VN(packet.defaultVersion); // Set the version number + packet.mode(packet.defaultMode); // Set the mode + packet.stratum = packet.defaultStratum; // Set the stratum + packet.pollExponent = settings.ntpPollExponent; // Set the poll interval + packet.precision = settings.ntpPrecision; // Set the precision + packet.rootDelay = packet.convertMicrosToSecsAndFraction(settings.ntpRootDelay); // Set the Root Delay + packet.rootDispersion = + packet.convertMicrosToSecsAndFraction(settings.ntpRootDispersion); // Set the Root Dispersion + for (uint8_t i = 0; i < packet.referenceIdLen; i++) + packet.referenceId[i] = settings.ntpReferenceId[i]; // Set the reference Id + + // REF: http://support.ntp.org/bin/view/Support/DraftRfc2030 + // '.. the client sets the Transmit Timestamp field in the request + // to the time of day according to the client clock in NTP timestamp format.' + // '.. The server copies this field to the originate timestamp in the reply and + // sets the Receive Timestamp and Transmit Timestamp fields to the time of day + // according to the server clock in NTP timestamp format.' + + // Important note: the NTP Era started January 1st 1900. + // tv will contain the time based on the Unix epoch (January 1st 1970) + // We need to adjust... + + // First, add the client transmit timestamp to our diagnostics + if (ntpDiag != nullptr) + { + char tmpbuf[128]; + snprintf(tmpbuf, sizeof(tmpbuf), "Originate Timestamp (Client Transmit): %u.%06u\r\n", + packet.transmitTimestampSeconds, packet.convertFractionToMicros(packet.transmitTimestampFraction)); + strlcat(ntpDiag, tmpbuf, ntpDiagSize); + } + + // Copy the client transmit timestamp into the originate timestamp + packet.originateTimestampSeconds = packet.transmitTimestampSeconds; + packet.originateTimestampFraction = packet.transmitTimestampFraction; + + // Set the receive timestamp to the time we received the packet (logged by the W5500 interrupt) + uint32_t recUnixSeconds = recTv->tv_sec; + recUnixSeconds -= settings.timeZoneSeconds; // Subtract the time zone offset to convert recTv to Unix time + recUnixSeconds -= settings.timeZoneMinutes * 60; + recUnixSeconds -= settings.timeZoneHours * 60 * 60; + packet.receiveTimestampSeconds = packet.convertUnixSecondsToNTP(recUnixSeconds); // Unix -> NTP + packet.receiveTimestampFraction = packet.convertMicrosToFraction(recTv->tv_usec); // Micros to 1/2^32 + + // Add the receive timestamp to the diagnostics + if (ntpDiag != nullptr) + { + char tmpbuf[128]; + snprintf(tmpbuf, sizeof(tmpbuf), "Received Timestamp: %u.%06u\r\n", + packet.receiveTimestampSeconds, packet.convertFractionToMicros(packet.receiveTimestampFraction)); + strlcat(ntpDiag, tmpbuf, ntpDiagSize); + } + + // Add when our clock was last sync'd + uint32_t syncUnixSeconds = syncTv->tv_sec; + syncUnixSeconds -= settings.timeZoneSeconds; // Subtract the time zone offset to convert recTv to Unix time + syncUnixSeconds -= settings.timeZoneMinutes * 60; + syncUnixSeconds -= settings.timeZoneHours * 60 * 60; + packet.referenceTimestampSeconds = packet.convertUnixSecondsToNTP(syncUnixSeconds); // Unix -> NTP + packet.referenceTimestampFraction = packet.convertMicrosToFraction(syncTv->tv_usec); // Micros to 1/2^32 + + // Add that to the diagnostics + if (ntpDiag != nullptr) + { + char tmpbuf[128]; + snprintf(tmpbuf, sizeof(tmpbuf), "Reference Timestamp (Last Sync): %u.%06u\r\n", + packet.referenceTimestampSeconds, + packet.convertFractionToMicros(packet.referenceTimestampFraction)); + strlcat(ntpDiag, tmpbuf, ntpDiagSize); + } + + // Add the transmit time - i.e. now! + timeval txTime; + gettimeofday(&txTime, nullptr); + uint32_t nowUnixSeconds = txTime.tv_sec; + nowUnixSeconds -= settings.timeZoneSeconds; // Subtract the time zone offset to convert recTv to Unix time + nowUnixSeconds -= settings.timeZoneMinutes * 60; + nowUnixSeconds -= settings.timeZoneHours * 60 * 60; + packet.transmitTimestampSeconds = packet.convertUnixSecondsToNTP(nowUnixSeconds); // Unix -> NTP + packet.transmitTimestampFraction = packet.convertMicrosToFraction(txTime.tv_usec); // Micros to 1/2^32 + + packet.insert(); // Copy the data fields back into the buffer + + // Now transmit the response to the client. + ntpServer->beginPacket(remoteIP, remotePort); + ntpServer->write(packet.packet, NTPpacket::NTPpacketSize); + int result = ntpServer->endPacket(); + processed = true; + + // Add our server transmit time to the diagnostics + if (ntpDiag != nullptr) + { + char tmpbuf[128]; + snprintf(tmpbuf, sizeof(tmpbuf), "Transmit Timestamp: %u.%06u\r\n", + packet.transmitTimestampSeconds, packet.convertFractionToMicros(packet.transmitTimestampFraction)); + strlcat(ntpDiag, tmpbuf, ntpDiagSize); + } + + /* + // Add the socketSendUDP result to the diagnostics + if (ntpDiag != nullptr) + { + char tmpbuf[128]; + snprintf(tmpbuf, sizeof(tmpbuf), "socketSendUDP result: %d\r\n", result); + strlcat(ntpDiag, tmpbuf, ntpDiagSize); + } + */ + + /* + // Add the packet to the diagnostics + if (ntpDiag != nullptr) + { + char tmpbuf[128]; + snprintf(tmpbuf, sizeof(tmpbuf), "Packet: "); + strlcat(ntpDiag, tmpbuf, ntpDiagSize); + for (int i = 0; i < NTPpacket::NTPpacketSize; i++) + { + snprintf(tmpbuf, sizeof(tmpbuf), "%02X ", packet.packet[i]); + strlcat(ntpDiag, tmpbuf, ntpDiagSize); + } + snprintf(tmpbuf, sizeof(tmpbuf), "\r\n"); + strlcat(ntpDiag, tmpbuf, ntpDiagSize); + } + */ + } + return processed; +} + +// Configure specific aspects of the receiver for NTP mode +bool configureUbloxModuleNTP() +{ + if (!HAS_GNSS_TP_INT) + return (false); + + if (online.gnss == false) + return (false); + + // If our settings haven't changed, and this is first config since power on, trust ZED's settings + // Unless this is a Ref Syn - where the GNSS has no battery-backed RAM + if (productVariant != REFERENCE_STATION && settings.updateZEDSettings == false && firstPowerOn == true) + { + firstPowerOn = false; // Next time user switches modes, new settings will be applied + log_d("Skipping ZED NTP configuration"); + return (true); + } + + firstPowerOn = false; // If we switch between rover/base in the future, force config of module. + + theGNSS.checkUblox(); // Regularly poll to get latest data and any RTCM + theGNSS.checkCallbacks(); // Process any callbacks: ie, storePVTdata + + theGNSS.setNMEAGPGGAcallbackPtr( + nullptr); // Disable GPGGA call back that may have been set during Rover NTRIP Client mode + + int tryNo = -1; + bool success = false; + + // Try up to MAX_SET_MESSAGES_RETRIES times to configure the GNSS + // This corrects occasional failures seen on the Reference Station where the GNSS is connected via SPI + // instead of I2C and UART1. I believe the SETVAL ACK is occasionally missed due to the level of messages being + // processed. + while ((++tryNo < MAX_SET_MESSAGES_RETRIES) && !success) + { + bool response = true; + + // In NTP mode we force 1Hz + response &= theGNSS.newCfgValset(); + response &= theGNSS.addCfgValset(UBLOX_CFG_RATE_MEAS, 1000); + response &= theGNSS.addCfgValset(UBLOX_CFG_RATE_NAV, 1); + + // Survey mode is only available on ZED-F9P modules + if (zedModuleType == PLATFORM_F9P) + response &= theGNSS.addCfgValset(UBLOX_CFG_TMODE_MODE, 0); // Disable survey-in mode + + // Set dynamic model to stationary + response &= theGNSS.addCfgValset(UBLOX_CFG_NAVSPG_DYNMODEL, DYN_MODEL_STATIONARY); // Set dynamic model + + // Set time pulse to 1Hz (100:900) + response &= theGNSS.addCfgValset(UBLOX_CFG_TP_PULSE_DEF, 0); // Time pulse definition is a period (in us) + response &= theGNSS.addCfgValset(UBLOX_CFG_TP_PULSE_LENGTH_DEF, 1); // Define timepulse by length (not ratio) + response &= + theGNSS.addCfgValset(UBLOX_CFG_TP_USE_LOCKED_TP1, + 1); // Use CFG-TP-PERIOD_LOCK_TP1 and CFG-TP-LEN_LOCK_TP1 as soon as GNSS time is valid + response &= theGNSS.addCfgValset(UBLOX_CFG_TP_TP1_ENA, 1); // Enable timepulse + response &= theGNSS.addCfgValset(UBLOX_CFG_TP_POL_TP1, 1); // 1 = rising edge + + // While the module is _locking_ to GNSS time, turn off pulse + response &= theGNSS.addCfgValset(UBLOX_CFG_TP_PERIOD_TP1, 1000000); // Set the period between pulses in us + response &= theGNSS.addCfgValset(UBLOX_CFG_TP_LEN_TP1, 0); // Set the pulse length in us + + // When the module is _locked_ to GNSS time, make it generate 1Hz (100ms high, 900ms low) + response &= theGNSS.addCfgValset(UBLOX_CFG_TP_PERIOD_LOCK_TP1, 1000000); // Set the period between pulses is us + response &= theGNSS.addCfgValset(UBLOX_CFG_TP_LEN_LOCK_TP1, 100000); // Set the pulse length in us + + // Ensure pulse is aligned to top-of-second. This is the default. Set it here just to make sure. + response &= theGNSS.addCfgValset(UBLOX_CFG_TP_ALIGN_TO_TOW_TP1, 1); + + // Set the time grid to UTC. This is the default. Set it here just to make sure. + response &= theGNSS.addCfgValset(UBLOX_CFG_TP_TIMEGRID_TP1, 0); // 0=UTC; 1=GPS + + // Sync to GNSS. This is the default. Set it here just to make sure. + response &= theGNSS.addCfgValset(UBLOX_CFG_TP_SYNC_GNSS_TP1, 1); + + response &= theGNSS.addCfgValset(UBLOX_CFG_NAVSPG_INFIL_MINELEV, settings.minElev); // Set minimum elevation + + // Ensure PVT, HPPOSLLH and TP messages are being output at 1Hz on the correct port + if (USE_I2C_GNSS) + { + response &= theGNSS.addCfgValset(UBLOX_CFG_MSGOUT_UBX_NAV_PVT_I2C, 1); + response &= theGNSS.addCfgValset(UBLOX_CFG_MSGOUT_UBX_NAV_HPPOSLLH_I2C, 1); + response &= theGNSS.addCfgValset(UBLOX_CFG_MSGOUT_UBX_TIM_TP_I2C, 1); + if (zedModuleType == PLATFORM_F9R) + { + response &= theGNSS.addCfgValset(UBLOX_CFG_MSGOUT_UBX_ESF_STATUS_I2C, 1); + } + } + else + { + response &= theGNSS.addCfgValset(UBLOX_CFG_MSGOUT_UBX_NAV_PVT_SPI, 1); + response &= theGNSS.addCfgValset(UBLOX_CFG_MSGOUT_UBX_NAV_HPPOSLLH_SPI, 1); + response &= theGNSS.addCfgValset(UBLOX_CFG_MSGOUT_UBX_TIM_TP_SPI, 1); + if (zedModuleType == PLATFORM_F9R) + { + response &= theGNSS.addCfgValset(UBLOX_CFG_MSGOUT_UBX_ESF_STATUS_SPI, 1); + } + } + + response &= theGNSS.sendCfgValset(); // Closing value + + if (response) + success = true; + } + + if (!success) + systemPrintln("NTP config fail"); + + return (success); +} + +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +// NTP Server routines +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +// Update the state of the NTP server state machine +void ntpServerSetState(uint8_t newState) +{ + if ((settings.debugNtp || PERIODIC_DISPLAY(PD_NTP_SERVER_STATE)) && (!inMainMenu)) + { + if (ntpServerState == newState) + systemPrint("*"); + else + systemPrintf("%s --> ", ntpServerStateName[ntpServerState]); + } + ntpServerState = newState; + if (settings.debugNtp || PERIODIC_DISPLAY(PD_NTP_SERVER_STATE)) + { + PERIODIC_CLEAR(PD_NTP_SERVER_STATE); + if (newState >= NTP_STATE_MAX) + { + systemPrintf("Unknown NTP Server state: %d\r\n", newState); + reportFatalError("Unknown NTP Server state"); + } + else if (!inMainMenu) + systemPrintln(ntpServerStateName[ntpServerState]); + } +} + +// Stop the NTP server +void ntpServerStop() +{ + // Mark the NTP server as off + online.NTPServer = false; + + // Release the NTP server memory + if (ntpServer) + { + w5500DisableSocketInterrupt(ntpSockIndex); // Disable the receive interrupt + ntpServer->stop(); + delete ntpServer; + ntpServer = nullptr; + if (!inMainMenu) + reportHeapNow(settings.debugNtp); + } + + // Release the network resources + if (networkGetUserNetwork(NETWORK_USER_NTP_SERVER)) + networkUserClose(NETWORK_USER_NTP_SERVER); + + // Stop the NTP server + ntpServerSetState(NTP_STATE_OFF); +} + +// Update the NTP server state +void ntpServerUpdate() +{ + char ntpDiag[512]; // Char array to hold diagnostic messages + + if (!HAS_ETHERNET) + return; + + // Shutdown the NTP server when the mode or setting changes + if (NEQ_RTK_MODE(ntpServerMode)) + { + if (ntpServerState > NTP_STATE_OFF) + ntpServerStop(); + return; + } + + // Process the NTP state + DMW_st(ntpServerSetState, ntpServerState); + switch (ntpServerState) + { + default: + break; + + case NTP_STATE_OFF: + // Determine if the NTP server is enabled + if (EQ_RTK_MODE(ntpServerMode)) + { + // Start the network + if (networkUserOpen(NETWORK_USER_NTP_SERVER, NETWORK_TYPE_ETHERNET)) + ntpServerSetState(NTP_STATE_NETWORK_STARTING); + } + break; + + // Wait for the network conection + case NTP_STATE_NETWORK_STARTING: + // Determine if the network has failed + if (networkIsShuttingDown(NETWORK_USER_NTP_SERVER)) + // Stop the NTP server, restart it if possible + ntpServerStop(); + + // Determine if the network is connected + else if (networkUserConnected(NETWORK_USER_NTP_SERVER)) + ntpServerSetState(NTP_STATE_NETWORK_CONNECTED); + break; + + case NTP_STATE_NETWORK_CONNECTED: + // Determine if the network has failed + if (networkIsShuttingDown(NETWORK_USER_NTP_SERVER)) + // Stop the NTP server, restart it if possible + ntpServerStop(); + + // Attempt to start the NTP server + else + { + ntpServer = new derivedEthernetUDP; + if (!ntpServer) + // Insufficient memory to start the NTP server + ntpServerStop(); + else + { + // Start the NTP server + ntpServer->begin(settings.ethernetNtpPort); + ntpSockIndex = ntpServer->getSockIndex(); // Get the socket index + w5500ClearSocketInterrupts(); // Clear all interrupts + w5500EnableSocketInterrupt(ntpSockIndex); // Enable the RECV interrupt for the desired socket index + online.NTPServer = true; + if (!inMainMenu) + reportHeapNow(settings.debugNtp); + ntpServerSetState(NTP_STATE_SERVER_RUNNING); + } + } + break; + + case NTP_STATE_SERVER_RUNNING: + // Determine if the network has failed + if (networkIsShuttingDown(NETWORK_USER_NTP_SERVER)) + // Stop the NTP server, restart it if possible + ntpServerStop(); + + else + { + if (w5500CheckSocketInterrupt(ntpSockIndex)) + w5500ClearSocketInterrupt(ntpSockIndex); // Clear the socket interrupt here + + // Check for new NTP requests - if the time has been sync'd + bool processed = ntpProcessOneRequest(systemState == STATE_NTPSERVER_SYNC, (const timeval *)ðernetNtpTv, + (const timeval *)&gnssSyncTv, ntpDiag, sizeof(ntpDiag)); + if (processed) + { + // Print the diagnostics - if enabled + if ((settings.debugNtp || PERIODIC_DISPLAY(PD_NTP_SERVER_DATA)) && (!inMainMenu)) + { + PERIODIC_CLEAR(PD_NTP_SERVER_DATA); + systemPrint(ntpDiag); + } + + // Log the NTP request to file - if enabled + if (settings.enableNTPFile) + { + // Gain access to the SPI controller for the microSD card + if (xSemaphoreTake(sdCardSemaphore, fatSemaphore_longWait_ms) == pdPASS) + { + markSemaphore(FUNCTION_NTPEVENT); + + // Get the marks file name + char fileName[32]; + bool fileOpen = false; + bool sdCardWasOnline; + int year; + int month; + int day; + + // Get the date + year = rtc.getYear(); + month = rtc.getMonth() + 1; + day = rtc.getDay(); + + // Build the file name + snprintf(fileName, sizeof(fileName), "/NTP_Requests_%04d_%02d_%02d.txt", year, month, day); + + // Try to gain access the SD card + sdCardWasOnline = online.microSD; + if (online.microSD != true) + beginSD(); + + if (online.microSD == true) + { + // Check if the NTP file already exists + bool ntpFileExists = false; + if (USE_SPI_MICROSD) + { + ntpFileExists = sd->exists(fileName); + } + #ifdef COMPILE_SD_MMC + else + { + ntpFileExists = SD_MMC.exists(fileName); + } + #endif // COMPILE_SD_MMC + + // Open the NTP file + FileSdFatMMC ntpFile; + + if (ntpFileExists) + { + if (ntpFile && ntpFile.open(fileName, O_APPEND | O_WRITE)) + { + fileOpen = true; + ntpFile.updateFileCreateTimestamp(); + } + } + else + { + if (ntpFile && ntpFile.open(fileName, O_CREAT | O_WRITE)) + { + fileOpen = true; + ntpFile.updateFileAccessTimestamp(); + + // If you want to add a file header, do it here + } + } + + if (fileOpen) + { + // Write the NTP request to the file + ntpFile.write((const uint8_t *)ntpDiag, strlen(ntpDiag)); + + // Update the file to create time & date + ntpFile.updateFileCreateTimestamp(); + + // Close the mark file + ntpFile.close(); + } + + // Dismount the SD card + if (!sdCardWasOnline) + endSD(true, false); + } + + // Done with the SPI controller + xSemaphoreGive(sdCardSemaphore); + + lastLoggedNTPRequest = millis(); + ntpLogIncreasing = true; + } // End sdCardSemaphore + } + } + + if (millis() > (lastLoggedNTPRequest + 5000)) + ntpLogIncreasing = false; + } + break; + } + + // Periodically display the NTP server state + if (PERIODIC_DISPLAY(PD_NTP_SERVER_STATE)) + ntpServerSetState(ntpServerState); +} + +// Verify the NTP tables +void ntpValidateTables() +{ + if (ntpServerStateNameEntries != NTP_STATE_MAX) + reportFatalError("Fix ntpServerStateNameEntries to match NTP_STATE"); +} + +#endif // COMPILE_ETHERNET diff --git a/Firmware/RTK_Surveyor/NVM.ino b/Firmware/RTK_Surveyor/NVM.ino index c38a639de..8701ead8d 100644 --- a/Firmware/RTK_Surveyor/NVM.ino +++ b/Firmware/RTK_Surveyor/NVM.ino @@ -1,488 +1,1833 @@ +/* + For any new setting added to the settings struct, we must add it to setting file + recording and logging, and to the WiFi AP load/read in the following places: + + recordSystemSettingsToFile(); + parseLine(); + createSettingsString(); + updateSettingWithValue(); + + form.h also needs to be updated to include a space for user input. This is best + edited in the index.html and main.js files. +*/ + +// We use the LittleFS library to store user profiles in SPIFFs +// Move selected user profile from SPIFFs into settings struct (RAM) +// We originally used EEPROM but it was limited to 4096 bytes. Each settings struct is ~4000 bytes +// so multiple user profiles wouldn't fit. Prefences was limited to a single putBytes of ~3000 bytes. +// So we moved again to SPIFFs. It's being replaced by LittleFS so here we are. void loadSettings() { - //First load any settings from NVM - //After, we'll load settings from config file if available - //We'll then re-record settings so that the settings from the file over-rides internal NVM settings - - //Check to see if EEPROM is blank - uint32_t testRead = 0; - if (EEPROM.get(0, testRead) == 0xFFFFFFFF) - { - Serial.println(F("EEPROM is blank. Default settings applied")); - recordSystemSettings(); //Record default settings to EEPROM and config file. At power on, settings are in default state - } - - //Check that the current settings struct size matches what is stored in EEPROM - //Misalignment happens when we add a new feature or setting - int tempSize = 0; - EEPROM.get(0, tempSize); //Load the sizeOfSettings - if (tempSize != sizeof(settings)) - { - Serial.println(F("Settings wrong size. Default settings applied")); - recordSystemSettings(); //Record default settings to EEPROM and config file. At power on, settings are in default state - } - - //Check that the rtkIdentifier is correct - //(It is possible for two different versions of the code to have the same sizeOfSettings - which causes problems!) - int tempIdentifier = 0; - EEPROM.get(sizeof(int), tempIdentifier); //Load the identifier from the EEPROM location after sizeOfSettings (int) - if (tempIdentifier != RTK_IDENTIFIER) - { - Serial.println(F("Settings are not valid for this variant of RTK Surveyor. Default settings applied")); - recordSystemSettings(); //Record default settings to EEPROM and config file. At power on, settings are in default state - } - - //Read current settings - EEPROM.get(0, settings); - - loadSystemSettingsFromFile(); //Load any settings from config file. This will over-write any pre-existing EEPROM settings. - //Record these new settings to EEPROM and config file to be sure they are the same - //(do this even if loadSystemSettingsFromFile returned false) - recordSystemSettings(); + // If we have a profile in both LFS and SD, the SD settings will overwrite LFS + loadSystemSettingsFromFileLFS(settingsFileName, &settings); + + // Temp store any variables from LFS that should override SD + int resetCount = settings.resetCount; + + loadSystemSettingsFromFileSD(settingsFileName, &settings); + settings.resetCount = resetCount; + + // Change empty profile name to 'Profile1' etc + if (strlen(settings.profileName) == 0) + snprintf(settings.profileName, sizeof(settings.profileName), "Profile%d", profileNumber + 1); + + // Record these settings to LittleFS and SD file to be sure they are the same + recordSystemSettings(); + + // Get bitmask of active profiles + activeProfiles = loadProfileNames(); + + systemPrintf("Profile '%s' loaded\r\n", profileNames[profileNumber]); +} + +// Set the settingsFileName and coordinate file names used many places +void setSettingsFileName() +{ + snprintf(settingsFileName, sizeof(settingsFileName), "/%s_Settings_%d.txt", platformFilePrefix, profileNumber); + snprintf(stationCoordinateECEFFileName, sizeof(stationCoordinateECEFFileName), "/StationCoordinates-ECEF_%d.csv", + profileNumber); + snprintf(stationCoordinateGeodeticFileName, sizeof(stationCoordinateGeodeticFileName), + "/StationCoordinates-Geodetic_%d.csv", profileNumber); +} + +// Load only LFS settings without recording +// Used at very first boot to test for resetCounter +void loadSettingsPartial() +{ + // First, look up the last used profile number + loadProfileNumber(); + + // Set the settingsFileName used many places + setSettingsFileName(); + + loadSystemSettingsFromFileLFS(settingsFileName, &settings); } -//Record the current settings struct to EEPROM and then to config file void recordSystemSettings() { - settings.sizeOfSettings = sizeof(settings); - if (settings.sizeOfSettings > EEPROM_SIZE) - { - displayError((char*)"EEPROM"); + settings.sizeOfSettings = sizeof(settings); // Update to current setting size + + recordSystemSettingsToFileSD(settingsFileName); // Record to SD if available + recordSystemSettingsToFileLFS(settingsFileName); // Record to LFS if available +} + +// Export the current settings to a config file on SD +// We share the recording with LittleFS so this is all the semphore and SD specific handling +void recordSystemSettingsToFileSD(char *fileName) +{ + bool gotSemaphore = false; + bool wasSdCardOnline; - while (1) //Hard freeze + // Try to gain access the SD card + wasSdCardOnline = online.microSD; + if (online.microSD != true) + beginSD(); + + while (online.microSD == true) { - Serial.printf("Size of settings is %d bytes\n\r", sizeof(settings)); - Serial.println(F("Increase the EEPROM footprint!")); - delay(1000); + // Attempt to write to file system. This avoids collisions with file writing from other functions like + // updateLogs() + if (xSemaphoreTake(sdCardSemaphore, fatSemaphore_longWait_ms) == pdPASS) + { + markSemaphore(FUNCTION_RECORDSETTINGS); + + gotSemaphore = true; + + if (USE_SPI_MICROSD) + { + if (sd->exists(fileName)) + { + log_d("Removing from SD: %s", fileName); + sd->remove(fileName); + } + + SdFile settingsFile; // FAT32 + if (settingsFile.open(fileName, O_CREAT | O_APPEND | O_WRITE) == false) + { + systemPrintln("Failed to create settings file"); + break; + } + + updateDataFileCreate(&settingsFile); // Update the file to create time & date + + recordSystemSettingsToFile((File *)&settingsFile); // Record all the settings via strings to file + + updateDataFileAccess(&settingsFile); // Update the file access time & date + + settingsFile.close(); + } +#ifdef COMPILE_SD_MMC + else + { + if (SD_MMC.exists(fileName)) + { + log_d("Removing from SD: %s", fileName); + SD_MMC.remove(fileName); + } + + File settingsFile = SD_MMC.open(fileName, FILE_WRITE); + + if (!settingsFile) + { + systemPrintln("Failed to create settings file"); + break; + } + + recordSystemSettingsToFile(&settingsFile); // Record all the settings via strings to file + + settingsFile.close(); + } +#endif // COMPILE_SD_MMC + + log_d("Settings recorded to SD: %s", fileName); + } + else + { + char semaphoreHolder[50]; + getSemaphoreFunction(semaphoreHolder); + + // This is an error because the current settings no longer match the settings + // on the microSD card, and will not be restored to the expected settings! + systemPrintf("sdCardSemaphore failed to yield, held by %s, NVM.ino line %d\r\n", semaphoreHolder, __LINE__); + } + break; } - } - EEPROM.put(0, settings); - EEPROM.commit(); - delay(1); //Give CPU time to pet WDT - recordSystemSettingsToFile(); + // Release access the SD card + if (online.microSD && (!wasSdCardOnline)) + endSD(gotSemaphore, true); + else if (gotSemaphore) + xSemaphoreGive(sdCardSemaphore); } -//Export the current settings to a config file -void recordSystemSettingsToFile() +// Export the current settings to a config file on SD +// We share the recording with LittleFS so this is all the semphore and SD specific handling +void recordSystemSettingsToFileLFS(char *fileName) { - if (online.microSD == true) - { - //Attempt to write to file system. This avoids collisions with file writing from other functions like updateLogs() - if (xSemaphoreTake(xFATSemaphore, fatSemaphore_longWait_ms) == pdPASS) - { - //Assemble settings file name - char settingsFileName[40]; //SFE_Surveyor_Settings.txt - strcpy(settingsFileName, platformFilePrefix); - strcat(settingsFileName, "_Settings.txt"); - - if (sd.exists(settingsFileName)) - sd.remove(settingsFileName); - - SdFile settingsFile; //FAT32 - if (settingsFile.open(settingsFileName, O_CREAT | O_APPEND | O_WRITE) == false) - { - Serial.println(F("Failed to create settings file")); - return; - } - if (online.gnss) - updateDataFileCreate(&settingsFile); // Update the file to create time & date - - settingsFile.println("sizeOfSettings=" + (String)settings.sizeOfSettings); - settingsFile.println("rtkIdentifier=" + (String)settings.rtkIdentifier); - - char firmwareVersion[30]; //v1.3 December 31 2021 - sprintf(firmwareVersion, "v%d.%d-%s", FIRMWARE_VERSION_MAJOR, FIRMWARE_VERSION_MINOR, __DATE__); - settingsFile.println("rtkFirmwareVersion=" + (String)firmwareVersion); - - settingsFile.println("zedFirmwareVersion=" + (String)zedFirmwareVersion); - settingsFile.println("printDebugMessages=" + (String)settings.printDebugMessages); - settingsFile.println("enableSD=" + (String)settings.enableSD); - settingsFile.println("enableDisplay=" + (String)settings.enableDisplay); - settingsFile.println("maxLogTime_minutes=" + (String)settings.maxLogTime_minutes); - settingsFile.println("observationSeconds=" + (String)settings.observationSeconds); - settingsFile.println("observationPositionAccuracy=" + (String)settings.observationPositionAccuracy); - settingsFile.println("fixedBase=" + (String)settings.fixedBase); - settingsFile.println("fixedBaseCoordinateType=" + (String)settings.fixedBaseCoordinateType); - settingsFile.println("fixedEcefX=" + (String)settings.fixedEcefX); - settingsFile.println("fixedEcefY=" + (String)settings.fixedEcefY); - settingsFile.println("fixedEcefZ=" + (String)settings.fixedEcefZ); - - //Print Lat/Long doubles with 9 decimals - char longPrint[20]; //-105.123456789 - sprintf(longPrint, "%0.9f", settings.fixedLat); - settingsFile.println("fixedLat=" + (String)longPrint); - sprintf(longPrint, "%0.9f", settings.fixedLong); - settingsFile.println("fixedLong=" + (String)longPrint); - sprintf(longPrint, "%0.4f", settings.fixedAltitude); - settingsFile.println("fixedAltitude=" + (String)longPrint); - - settingsFile.println("dataPortBaud=" + (String)settings.dataPortBaud); - settingsFile.println("radioPortBaud=" + (String)settings.radioPortBaud); - settingsFile.println("enableNtripServer=" + (String)settings.enableNtripServer); - settingsFile.println("casterHost=" + (String)settings.casterHost); - settingsFile.println("casterPort=" + (String)settings.casterPort); - settingsFile.println("mountPoint=" + (String)settings.mountPoint); - settingsFile.println("mountPointPW=" + (String)settings.mountPointPW); - settingsFile.println("wifiSSID=" + (String)settings.wifiSSID); - settingsFile.println("wifiPW=" + (String)settings.wifiPW); - settingsFile.println("surveyInStartingAccuracy=" + (String)settings.surveyInStartingAccuracy); - settingsFile.println("measurementRate=" + (String)settings.measurementRate); - settingsFile.println("navigationRate=" + (String)settings.navigationRate); - settingsFile.println("enableI2Cdebug=" + (String)settings.enableI2Cdebug); - settingsFile.println("enableHeapReport=" + (String)settings.enableHeapReport); - settingsFile.println("enableTaskReports=" + (String)settings.enableTaskReports); - settingsFile.println("dataPortChannel=" + (String)settings.dataPortChannel); - settingsFile.println("spiFrequency=" + (String)settings.spiFrequency); - settingsFile.println("sppRxQueueSize=" + (String)settings.sppRxQueueSize); - settingsFile.println("sppTxQueueSize=" + (String)settings.sppTxQueueSize); - settingsFile.println("dynamicModel=" + (String)settings.dynamicModel); - settingsFile.println("lastState=" + (String)settings.lastState); - settingsFile.println("throttleDuringSPPCongestion=" + (String)settings.throttleDuringSPPCongestion); - - //Record constellation settings - for (int x = 0 ; x < MAX_CONSTELLATIONS ; x++) - { - char tempString[50]; //constellation.BeiDou=1 - sprintf(tempString, "constellation.%s=%d", ubxConstellations[x].textName, ubxConstellations[x].enabled); - settingsFile.println(tempString); - } - - //Record message settings - for (int x = 0 ; x < MAX_UBX_MSG ; x++) - { - char tempString[50]; //message.nmea_dtm.msgRate=5 - sprintf(tempString, "message.%s.msgRate=%d", ubxMessages[x].msgTextName, ubxMessages[x].msgRate); - settingsFile.println(tempString); - } - - if (online.gnss) - updateDataFileAccess(&settingsFile); // Update the file access time & date - - settingsFile.close(); - - xSemaphoreGive(xFATSemaphore); - } - } + if (online.fs == true) + { + if (LittleFS.exists(fileName)) + { + LittleFS.remove(fileName); + log_d("Removing LittleFS: %s", fileName); + } + + File settingsFile = LittleFS.open(fileName, FILE_WRITE); + if (!settingsFile) + { + log_d("Failed to write to settings file %s", fileName); + } + else + { + recordSystemSettingsToFile(&settingsFile); // Record all the settings via strings to file + settingsFile.close(); + log_d("Settings recorded to LittleFS: %s", fileName); + } + } } -//If a config file exists on the SD card, load them and overwrite the local settings -//Heavily based on ReadCsvFile from SdFat library -//Returns true if some settings were loaded from a file -//Returns false if a file was not opened/loaded -bool loadSystemSettingsFromFile() +// Write the settings struct to a clear text file +void recordSystemSettingsToFile(File *settingsFile) { - if (online.microSD == true) - { - //Attempt to access file system. This avoids collisions with file writing from other functions like recordSystemSettingsToFile() and F9PSerialReadTask() - if (xSemaphoreTake(xFATSemaphore, fatSemaphore_longWait_ms) == pdPASS) - { - //Assemble settings file name - char settingsFileName[40]; //SFE_Surveyor_Settings.txt - strcpy(settingsFileName, platformFilePrefix); - strcat(settingsFileName, "_Settings.txt"); - - if (sd.exists(settingsFileName)) - { - SdFile settingsFile; //FAT32 - if (settingsFile.open(settingsFileName, O_READ) == false) - { - Serial.println(F("Failed to open settings file")); - xSemaphoreGive(xFATSemaphore); - return (false); - } - - char line[60]; - int lineNumber = 0; - - while (settingsFile.available()) { - - //Get the next line from the file - //int n = getLine(&settingsFile, line, sizeof(line)); //Use with SD library - int n = settingsFile.fgets(line, sizeof(line)); //Use with SdFat library - if (n <= 0) { - Serial.printf("Failed to read line %d from settings file\r\n", lineNumber); - } - else if (line[n - 1] != '\n' && n == (sizeof(line) - 1)) { - Serial.printf("Settings line %d too long\r\n", lineNumber); + settingsFile->printf("%s=%d\r\n", "sizeOfSettings", settings.sizeOfSettings); + settingsFile->printf("%s=%d\r\n", "rtkIdentifier", settings.rtkIdentifier); + + char firmwareVersion[30]; // v1.3 December 31 2021 + getFirmwareVersion(firmwareVersion, sizeof(firmwareVersion), true); + settingsFile->printf("%s=%s\r\n", "rtkFirmwareVersion", firmwareVersion); + + settingsFile->printf("%s=%s\r\n", "zedFirmwareVersion", zedFirmwareVersion); + + settingsFile->printf("%s=%s\r\n", "zedUniqueId", zedUniqueId); + + if (productVariant == RTK_FACET_LBAND || productVariant == RTK_FACET_LBAND_DIRECT) + settingsFile->printf("%s=%s\r\n", "neoFirmwareVersion", neoFirmwareVersion); + + settingsFile->printf("%s=%d\r\n", "printDebugMessages", settings.printDebugMessages); + settingsFile->printf("%s=%d\r\n", "enableSD", settings.enableSD); + settingsFile->printf("%s=%d\r\n", "enableDisplay", settings.enableDisplay); + settingsFile->printf("%s=%d\r\n", "maxLogTime_minutes", settings.maxLogTime_minutes); + settingsFile->printf("%s=%d\r\n", "maxLogLength_minutes", settings.maxLogLength_minutes); + settingsFile->printf("%s=%d\r\n", "observationSeconds", settings.observationSeconds); + settingsFile->printf("%s=%0.2f\r\n", "observationPositionAccuracy", settings.observationPositionAccuracy); + settingsFile->printf("%s=%d\r\n", "fixedBase", settings.fixedBase); + settingsFile->printf("%s=%d\r\n", "fixedBaseCoordinateType", settings.fixedBaseCoordinateType); + settingsFile->printf("%s=%0.3f\r\n", "fixedEcefX", settings.fixedEcefX); //-1280206.568 + settingsFile->printf("%s=%0.3f\r\n", "fixedEcefY", settings.fixedEcefY); + settingsFile->printf("%s=%0.3f\r\n", "fixedEcefZ", settings.fixedEcefZ); + settingsFile->printf("%s=%0.9f\r\n", "fixedLat", settings.fixedLat); // 40.09029479 + settingsFile->printf("%s=%0.9f\r\n", "fixedLong", settings.fixedLong); + settingsFile->printf("%s=%0.4f\r\n", "fixedAltitude", settings.fixedAltitude); + settingsFile->printf("%s=%d\r\n", "dataPortBaud", settings.dataPortBaud); + settingsFile->printf("%s=%d\r\n", "radioPortBaud", settings.radioPortBaud); + settingsFile->printf("%s=%0.1f\r\n", "surveyInStartingAccuracy", settings.surveyInStartingAccuracy); + settingsFile->printf("%s=%d\r\n", "measurementRate", settings.measurementRate); + settingsFile->printf("%s=%d\r\n", "navigationRate", settings.navigationRate); + settingsFile->printf("%s=%d\r\n", "enableI2Cdebug", settings.enableI2Cdebug); + settingsFile->printf("%s=%d\r\n", "enableHeapReport", settings.enableHeapReport); + settingsFile->printf("%s=%d\r\n", "enableTaskReports", settings.enableTaskReports); + settingsFile->printf("%s=%d\r\n", "dataPortChannel", (uint8_t)settings.dataPortChannel); + settingsFile->printf("%s=%d\r\n", "spiFrequency", settings.spiFrequency); + settingsFile->printf("%s=%d\r\n", "sppRxQueueSize", settings.sppRxQueueSize); + settingsFile->printf("%s=%d\r\n", "sppTxQueueSize", settings.sppTxQueueSize); + settingsFile->printf("%s=%d\r\n", "dynamicModel", settings.dynamicModel); + settingsFile->printf("%s=%d\r\n", "lastState", settings.lastState); + settingsFile->printf("%s=%d\r\n", "enableSensorFusion", settings.enableSensorFusion); + settingsFile->printf("%s=%d\r\n", "autoIMUmountAlignment", settings.autoIMUmountAlignment); + settingsFile->printf("%s=%d\r\n", "enableResetDisplay", settings.enableResetDisplay); + settingsFile->printf("%s=%d\r\n", "enableExternalPulse", settings.enableExternalPulse); + settingsFile->printf("%s=%llu\r\n", "externalPulseTimeBetweenPulse_us", settings.externalPulseTimeBetweenPulse_us); + settingsFile->printf("%s=%llu\r\n", "externalPulseLength_us", settings.externalPulseLength_us); + settingsFile->printf("%s=%d\r\n", "externalPulsePolarity", settings.externalPulsePolarity); + settingsFile->printf("%s=%d\r\n", "enableExternalHardwareEventLogging", + settings.enableExternalHardwareEventLogging); + settingsFile->printf("%s=%s\r\n", "profileName", settings.profileName); + settingsFile->printf("%s=%d\r\n", "enableNtripServer", settings.enableNtripServer); + settingsFile->printf("%s=%d\r\n", "ntripServer_StartAtSurveyIn", settings.ntripServer_StartAtSurveyIn); + for (int serverIndex = 0; serverIndex < NTRIP_SERVER_MAX; serverIndex++) + { + settingsFile->printf("%s_%d=%s\r\n", "ntripServer_CasterHost", serverIndex, &settings.ntripServer_CasterHost[serverIndex][0]); + settingsFile->printf("%s_%d=%d\r\n", "ntripServer_CasterPort", serverIndex, settings.ntripServer_CasterPort[serverIndex]); + settingsFile->printf("%s_%d=%s\r\n", "ntripServer_CasterUser", serverIndex, &settings.ntripServer_CasterUser[serverIndex][0]); + settingsFile->printf("%s_%d=%s\r\n", "ntripServer_CasterUserPW", serverIndex, &settings.ntripServer_CasterUserPW[serverIndex][0]); + settingsFile->printf("%s_%d=%s\r\n", "ntripServer_MountPoint", serverIndex, &settings.ntripServer_MountPoint[serverIndex][0]); + settingsFile->printf("%s_%d=%s\r\n", "ntripServer_MountPointPW", serverIndex, &settings.ntripServer_MountPointPW[serverIndex][0]); + } + settingsFile->printf("%s=%d\r\n", "enableNtripClient", settings.enableNtripClient); + settingsFile->printf("%s=%s\r\n", "ntripClient_CasterHost", settings.ntripClient_CasterHost); + settingsFile->printf("%s=%d\r\n", "ntripClient_CasterPort", settings.ntripClient_CasterPort); + settingsFile->printf("%s=%s\r\n", "ntripClient_CasterUser", settings.ntripClient_CasterUser); + settingsFile->printf("%s=%s\r\n", "ntripClient_CasterUserPW", settings.ntripClient_CasterUserPW); + settingsFile->printf("%s=%s\r\n", "ntripClient_MountPoint", settings.ntripClient_MountPoint); + settingsFile->printf("%s=%s\r\n", "ntripClient_MountPointPW", settings.ntripClient_MountPointPW); + settingsFile->printf("%s=%d\r\n", "ntripClient_TransmitGGA", settings.ntripClient_TransmitGGA); + settingsFile->printf("%s=%d\r\n", "serialTimeoutGNSS", settings.serialTimeoutGNSS); + + // Point Perfect + settingsFile->printf("%s=%s\r\n", "pointPerfectDeviceProfileToken", settings.pointPerfectDeviceProfileToken); + settingsFile->printf("%s=%d\r\n", "enablePointPerfectCorrections", settings.enablePointPerfectCorrections); + settingsFile->printf("%s=%d\r\n", "autoKeyRenewal", settings.autoKeyRenewal); + settingsFile->printf("%s=%s\r\n", "pointPerfectClientID", settings.pointPerfectClientID); + settingsFile->printf("%s=%s\r\n", "pointPerfectBrokerHost", settings.pointPerfectBrokerHost); + settingsFile->printf("%s=%s\r\n", "pointPerfectLBandTopic", settings.pointPerfectLBandTopic); + settingsFile->printf("%s=%s\r\n", "pointPerfectCurrentKey", settings.pointPerfectCurrentKey); + settingsFile->printf("%s=%llu\r\n", "pointPerfectCurrentKeyDuration", settings.pointPerfectCurrentKeyDuration); + settingsFile->printf("%s=%llu\r\n", "pointPerfectCurrentKeyStart", settings.pointPerfectCurrentKeyStart); + settingsFile->printf("%s=%s\r\n", "pointPerfectNextKey", settings.pointPerfectNextKey); + settingsFile->printf("%s=%llu\r\n", "pointPerfectNextKeyDuration", settings.pointPerfectNextKeyDuration); + settingsFile->printf("%s=%llu\r\n", "pointPerfectNextKeyStart", settings.pointPerfectNextKeyStart); + settingsFile->printf("%s=%llu\r\n", "lastKeyAttempt", settings.lastKeyAttempt); + settingsFile->printf("%s=%d\r\n", "debugPpCertificate", settings.debugPpCertificate); + + settingsFile->printf("%s=%d\r\n", "updateZEDSettings", settings.updateZEDSettings); + settingsFile->printf("%s=%d\r\n", "enableLogging", settings.enableLogging); + settingsFile->printf("%s=%d\r\n", "enableARPLogging", settings.enableARPLogging); + settingsFile->printf("%s=%d\r\n", "ARPLoggingInterval_s", settings.ARPLoggingInterval_s); + settingsFile->printf("%s=%d\r\n", "timeZoneHours", settings.timeZoneHours); + settingsFile->printf("%s=%d\r\n", "timeZoneMinutes", settings.timeZoneMinutes); + settingsFile->printf("%s=%d\r\n", "timeZoneSeconds", settings.timeZoneSeconds); + settingsFile->printf("%s=%d\r\n", "enablePrintState", settings.enablePrintState); + settingsFile->printf("%s=%d\r\n", "debugWifiState", settings.debugWifiState); + settingsFile->printf("%s=%d\r\n", "debugNtripClientState", settings.debugNtripClientState); + settingsFile->printf("%s=%d\r\n", "debugNtripServerState", settings.debugNtripServerState); + settingsFile->printf("%s=%d\r\n", "enablePrintPosition", settings.enablePrintPosition); + settingsFile->printf("%s=%d\r\n", "enablePrintIdleTime", settings.enablePrintIdleTime); + settingsFile->printf("%s=%d\r\n", "enableMarksFile", settings.enableMarksFile); + settingsFile->printf("%s=%d\r\n", "enableUART2UBXIn", settings.enableUART2UBXIn); + settingsFile->printf("%s=%d\r\n", "enablePrintBatteryMessages", settings.enablePrintBatteryMessages); + settingsFile->printf("%s=%d\r\n", "enablePrintRoverAccuracy", settings.enablePrintRoverAccuracy); + settingsFile->printf("%s=%d\r\n", "enablePrintBadMessages", settings.enablePrintBadMessages); + settingsFile->printf("%s=%d\r\n", "enablePrintLogFileMessages", settings.enablePrintLogFileMessages); + settingsFile->printf("%s=%d\r\n", "enablePrintLogFileStatus", settings.enablePrintLogFileStatus); + settingsFile->printf("%s=%d\r\n", "enablePrintRingBufferOffsets", settings.enablePrintRingBufferOffsets); + settingsFile->printf("%s=%d\r\n", "debugNtripServerRtcm", settings.debugNtripServerRtcm); + settingsFile->printf("%s=%d\r\n", "debugNtripClientRtcm", settings.debugNtripClientRtcm); + settingsFile->printf("%s=%d\r\n", "enablePrintStates", settings.enablePrintStates); + settingsFile->printf("%s=%d\r\n", "enablePrintDuplicateStates", settings.enablePrintDuplicateStates); + settingsFile->printf("%s=%d\r\n", "enablePrintRtcSync", settings.enablePrintRtcSync); + settingsFile->printf("%s=%d\r\n", "debugNtp", settings.debugNtp); + settingsFile->printf("%s=%d\r\n", "enablePrintEthernetDiag", settings.enablePrintEthernetDiag); + settingsFile->printf("%s=%d\r\n", "radioType", settings.radioType); + + // Network layer + settingsFile->printf("%s=%d\r\n", "defaultNetworkType", settings.defaultNetworkType); + settingsFile->printf("%s=%d\r\n", "debugNetworkLayer", settings.debugNetworkLayer); + settingsFile->printf("%s=%d\r\n", "enableNetworkFailover", settings.enableNetworkFailover); + settingsFile->printf("%s=%d\r\n", "printNetworkStatus", settings.printNetworkStatus); + + // Record peer MAC addresses + for (int x = 0; x < settings.espnowPeerCount; x++) + { + char tempString[50]; // espnowPeers.1=B4,C1,33,42,DE,01, + snprintf(tempString, sizeof(tempString), "espnowPeers.%d=%02X,%02X,%02X,%02X,%02X,%02X,", x, + settings.espnowPeers[x][0], settings.espnowPeers[x][1], settings.espnowPeers[x][2], + settings.espnowPeers[x][3], settings.espnowPeers[x][4], settings.espnowPeers[x][5]); + settingsFile->println(tempString); + } + settingsFile->printf("%s=%d\r\n", "espnowPeerCount", settings.espnowPeerCount); + settingsFile->printf("%s=%d\r\n", "enableRtcmMessageChecking", settings.enableRtcmMessageChecking); + settingsFile->printf("%s=%d\r\n", "bluetoothRadioType", settings.bluetoothRadioType); + settingsFile->printf("%s=%d\r\n", "enablePvtClient", settings.enablePvtClient); + settingsFile->printf("%s=%d\r\n", "enablePvtServer", settings.enablePvtServer); + settingsFile->printf("%s=%d\r\n", "enablePvtUdpServer", settings.enablePvtUdpServer); + settingsFile->printf("%s=%d\r\n", "debugPvtClient", settings.debugPvtClient); + settingsFile->printf("%s=%d\r\n", "debugPvtServer", settings.debugPvtServer); + settingsFile->printf("%s=%d\r\n", "debugPvtUdpServer", settings.debugPvtUdpServer); + settingsFile->printf("%s=%d\r\n", "espnowBroadcast", settings.espnowBroadcast); + settingsFile->printf("%s=%d\r\n", "antennaHeight", settings.antennaHeight); + settingsFile->printf("%s=%0.2f\r\n", "antennaReferencePoint", settings.antennaReferencePoint); + settingsFile->printf("%s=%d\r\n", "echoUserInput", settings.echoUserInput); + settingsFile->printf("%s=%d\r\n", "uartReceiveBufferSize", settings.uartReceiveBufferSize); + settingsFile->printf("%s=%d\r\n", "gnssHandlerBufferSize", settings.gnssHandlerBufferSize); + settingsFile->printf("%s=%d\r\n", "enablePrintBufferOverrun", settings.enablePrintBufferOverrun); + settingsFile->printf("%s=%d\r\n", "enablePrintSDBuffers", settings.enablePrintSDBuffers); + settingsFile->printf("%s=%d\r\n", "periodicDisplay", settings.periodicDisplay); + settingsFile->printf("%s=%d\r\n", "periodicDisplayInterval", settings.periodicDisplayInterval); + settingsFile->printf("%s=%d\r\n", "rebootSeconds", settings.rebootSeconds); + settingsFile->printf("%s=%d\r\n", "forceResetOnSDFail", settings.forceResetOnSDFail); + + // Record WiFi credential table + for (int x = 0; x < MAX_WIFI_NETWORKS; x++) + { + char tempString[100]; // wifiNetwork0Password=parachutes + + snprintf(tempString, sizeof(tempString), "wifiNetwork%dSSID=%s", x, settings.wifiNetworks[x].ssid); + settingsFile->println(tempString); + snprintf(tempString, sizeof(tempString), "wifiNetwork%dPassword=%s", x, settings.wifiNetworks[x].password); + settingsFile->println(tempString); + } + + settingsFile->printf("%s=%d\r\n", "wifiConfigOverAP", settings.wifiConfigOverAP); + settingsFile->printf("%s=%d\r\n", "pvtServerPort", settings.pvtServerPort); + settingsFile->printf("%s=%d\r\n", "pvtUdpServerPort", settings.pvtUdpServerPort); + settingsFile->printf("%s=%d\r\n", "minElev", settings.minElev); + + settingsFile->printf("%s=%d\r\n", "imuYaw", settings.imuYaw); + settingsFile->printf("%s=%d\r\n", "imuPitch", settings.imuPitch); + settingsFile->printf("%s=%d\r\n", "imuRoll", settings.imuRoll); + settingsFile->printf("%s=%d\r\n", "sfDisableWheelDirection", settings.sfDisableWheelDirection); + settingsFile->printf("%s=%d\r\n", "sfCombineWheelTicks", settings.sfCombineWheelTicks); + settingsFile->printf("%s=%d\r\n", "rateNavPrio", settings.rateNavPrio); + settingsFile->printf("%s=%d\r\n", "sfUseSpeed", settings.sfUseSpeed); + settingsFile->printf("%s=%d\r\n", "coordinateInputType", settings.coordinateInputType); + settingsFile->printf("%s=%d\r\n", "lbandFixTimeout_seconds", settings.lbandFixTimeout_seconds); + settingsFile->printf("%s=%d\r\n", "minCNO_F9R", settings.minCNO_F9R); + settingsFile->printf("%s=%d\r\n", "minCNO_F9P", settings.minCNO_F9P); + settingsFile->printf("%s=%d\r\n", "shutdownNoChargeTimeout_s", settings.shutdownNoChargeTimeout_s); + settingsFile->printf("%s=%d\r\n", "disableSetupButton", settings.disableSetupButton); + settingsFile->printf("%s=%d\r\n", "useI2cForLbandCorrections", settings.useI2cForLbandCorrections); + settingsFile->printf("%s=%d\r\n", "useI2cForLbandCorrectionsConfigured", + settings.useI2cForLbandCorrectionsConfigured); + + // Record constellation settings + for (int x = 0; x < MAX_CONSTELLATIONS; x++) + { + char tempString[50]; // constellation.BeiDou=1 + snprintf(tempString, sizeof(tempString), "constellation.%s=%d", settings.ubxConstellations[x].textName, + settings.ubxConstellations[x].enabled); + settingsFile->println(tempString); + } + + // Record message settings + for (int x = 0; x < MAX_UBX_MSG; x++) + { + char tempString[50]; // message.nmea_dtm.msgRate=5 + snprintf(tempString, sizeof(tempString), "message.%s.msgRate=%d", ubxMessages[x].msgTextName, + settings.ubxMessageRates[x]); + settingsFile->println(tempString); + } + + // Record Base RTCM message settings + int firstRTCMRecord = getMessageNumberByName("UBX_RTCM_1005"); + for (int x = 0; x < MAX_UBX_MSG_RTCM; x++) + { + char tempString[50]; // messageBase.UBX_RTCM_1094.msgRate=5 + snprintf(tempString, sizeof(tempString), "messageBase.%s.msgRate=%d", + ubxMessages[firstRTCMRecord + x].msgTextName, settings.ubxMessageRatesBase[x]); + settingsFile->println(tempString); + } + + // Ethernet + { + settingsFile->printf("%s=%s\r\n", "ethernetIP", settings.ethernetIP.toString().c_str()); + settingsFile->printf("%s=%s\r\n", "ethernetDNS", settings.ethernetDNS.toString().c_str()); + settingsFile->printf("%s=%s\r\n", "ethernetGateway", settings.ethernetGateway.toString().c_str()); + settingsFile->printf("%s=%s\r\n", "ethernetSubnet", settings.ethernetSubnet.toString().c_str()); + settingsFile->printf("%s=%d\r\n", "httpPort", settings.httpPort); + settingsFile->printf("%s=%d\r\n", "ethernetNtpPort", settings.ethernetNtpPort); + settingsFile->printf("%s=%d\r\n", "ethernetDHCP", settings.ethernetDHCP); + settingsFile->printf("%s=%d\r\n", "enableNTPFile", settings.enableNTPFile); + settingsFile->printf("%s=%d\r\n", "pvtClientPort", settings.pvtClientPort); + settingsFile->printf("%s=%s\r\n", "pvtClientHost", settings.pvtClientHost); + } + + // NTP + { + settingsFile->printf("%s=%d\r\n", "ntpPollExponent", settings.ntpPollExponent); + settingsFile->printf("%s=%d\r\n", "ntpPrecision", settings.ntpPrecision); + settingsFile->printf("%s=%d\r\n", "ntpRootDelay", settings.ntpRootDelay); + settingsFile->printf("%s=%d\r\n", "ntpRootDispersion", settings.ntpRootDispersion); + settingsFile->printf("%s=%s\r\n", "ntpReferenceId", settings.ntpReferenceId); + } + + settingsFile->printf("%s=%d\r\n", "mdnsEnable", settings.mdnsEnable); + settingsFile->printf("%s=%d\r\n", "serialGNSSRxFullThreshold", settings.serialGNSSRxFullThreshold); + settingsFile->printf("%s=%d\r\n", "btReadTaskPriority", settings.btReadTaskPriority); + settingsFile->printf("%s=%d\r\n", "gnssReadTaskPriority", settings.gnssReadTaskPriority); + settingsFile->printf("%s=%d\r\n", "handleGnssDataTaskPriority", settings.handleGnssDataTaskPriority); + settingsFile->printf("%s=%d\r\n", "btReadTaskCore", settings.btReadTaskCore); + settingsFile->printf("%s=%d\r\n", "gnssReadTaskCore", settings.gnssReadTaskCore); + settingsFile->printf("%s=%d\r\n", "handleGnssDataTaskCore", settings.handleGnssDataTaskCore); + settingsFile->printf("%s=%d\r\n", "gnssUartInterruptsCore", settings.gnssUartInterruptsCore); + settingsFile->printf("%s=%d\r\n", "bluetoothInterruptsCore", settings.bluetoothInterruptsCore); + settingsFile->printf("%s=%d\r\n", "i2cInterruptsCore", settings.i2cInterruptsCore); + settingsFile->printf("%s=%d\r\n", "rtcmTimeoutBeforeUsingLBand_s", settings.rtcmTimeoutBeforeUsingLBand_s); + + // Automatic Firmware Update + settingsFile->printf("%s=%d\r\n", "autoFirmwareCheckMinutes", settings.autoFirmwareCheckMinutes); + settingsFile->printf("%s=%d\r\n", "debugFirmwareUpdate", settings.debugFirmwareUpdate); + settingsFile->printf("%s=%d\r\n", "enableAutoFirmwareUpdate", settings.enableAutoFirmwareUpdate); + + settingsFile->printf("%s=%d\r\n", "debugLBand", settings.debugLBand); + settingsFile->printf("%s=%d\r\n", "enableCaptivePortal", settings.enableCaptivePortal); + settingsFile->printf("%s=%d\r\n", "enableZedUsb", settings.enableZedUsb); + settingsFile->printf("%s=%d\r\n", "debugWiFiConfig", settings.debugWiFiConfig); + + settingsFile->printf("%s=%d\r\n", "geographicRegion", settings.geographicRegion); + + // Add new settings above <------------------------------------------------------------> +} + +// Given a fileName, parse the file and load the given settings struct +// Returns true if some settings were loaded from a file +// Returns false if a file was not opened/loaded +bool loadSystemSettingsFromFileSD(char *fileName, Settings *settings) +{ + bool gotSemaphore = false; + bool status = false; + bool wasSdCardOnline; + + // Try to gain access the SD card + wasSdCardOnline = online.microSD; + if (online.microSD != true) + beginSD(); + + while (online.microSD == true) + { + // Attempt to access file system. This avoids collisions with file writing from other functions like + // recordSystemSettingsToFile() and F9PSerialReadTask() + if (xSemaphoreTake(sdCardSemaphore, fatSemaphore_longWait_ms) == pdPASS) + { + markSemaphore(FUNCTION_LOADSETTINGS); + + gotSemaphore = true; + + if (USE_SPI_MICROSD) + { + if (!sd->exists(fileName)) + { + log_d("File %s not found", fileName); + break; + } + + SdFile settingsFile; // FAT32 + if (settingsFile.open(fileName, O_READ) == false) + { + systemPrintln("Failed to open settings file"); + break; + } + + char line[100]; + int lineNumber = 0; + + while (settingsFile.available()) + { + // Get the next line from the file + int n = settingsFile.fgets(line, sizeof(line)); + if (n <= 0) + { + systemPrintf("Failed to read line %d from settings file\r\n", lineNumber); + } + else if (line[n - 1] != '\n' && n == (sizeof(line) - 1)) + { + systemPrintf("Settings line %d too long\r\n", lineNumber); + if (lineNumber == 0) + { + // If we can't read the first line of the settings file, give up + systemPrintln("Giving up on settings file"); + break; + } + } + else if (parseLine(line, settings) == false) + { + systemPrintf("Failed to parse line %d: %s\r\n", lineNumber, line); + if (lineNumber == 0) + { + // If we can't read the first line of the settings file, give up + systemPrintln("Giving up on settings file"); + break; + } + } + + lineNumber++; + } + + // systemPrintln("Config file read complete"); + settingsFile.close(); + status = true; + break; + } +#ifdef COMPILE_SD_MMC + else + { + if (!SD_MMC.exists(fileName)) + { + log_d("File %s not found", fileName); + break; + } + + File settingsFile = SD_MMC.open(fileName, FILE_READ); + + if (!settingsFile) + { + systemPrintln("Failed to open settings file"); + break; + } + + char line[100]; + int lineNumber = 0; + + while (settingsFile.available()) + { + // Get the next line from the file + int n = getLine(&settingsFile, line, sizeof(line)); + if (n <= 0) + { + systemPrintf("Failed to read line %d from settings file\r\n", lineNumber); + } + else if (line[n - 1] != '\n' && n == (sizeof(line) - 1)) + { + systemPrintf("Settings line %d too long\r\n", lineNumber); + if (lineNumber == 0) + { + // If we can't read the first line of the settings file, give up + systemPrintln("Giving up on settings file"); + break; + } + } + else if (parseLine(line, settings) == false) + { + systemPrintf("Failed to parse line %d: %s\r\n", lineNumber, line); + if (lineNumber == 0) + { + // If we can't read the first line of the settings file, give up + systemPrintln("Giving up on settings file"); + break; + } + } + + lineNumber++; + } + + // systemPrintln("Config file read complete"); + settingsFile.close(); + status = true; + break; + } +#endif // COMPILE_SD_MMC + } // End Semaphore check + else + { + // This is an error because if the settings exist on the microSD card that + // those settings are not overriding the current settings as documented! + systemPrintf("sdCardSemaphore failed to yield, NVM.ino line %d\r\n", __LINE__); + } + break; + } // End SD online + + if (online.microSD != true) + log_d("Config file read failed: SD offline"); + + // Release access the SD card + if (online.microSD && (!wasSdCardOnline)) + endSD(gotSemaphore, true); + else if (gotSemaphore) + xSemaphoreGive(sdCardSemaphore); + + return status; +} + +// Given a fileName, parse the file and load the given settings struct +// Returns true if some settings were loaded from a file +// Returns false if a file was not opened/loaded +bool loadSystemSettingsFromFileLFS(char *fileName, Settings *settings) +{ + // log_d("reading setting fileName: %s", fileName); + + File settingsFile = LittleFS.open(fileName, FILE_READ); + if (!settingsFile) + { + // log_d("settingsFile not found in LittleFS\r\n"); + return (false); + } + + char line[100]; + int lineNumber = 0; + + while (settingsFile.available()) + { + // Get the next line from the file + int n; + n = getLine(&settingsFile, line, sizeof(line)); + + if (n <= 0) + { + systemPrintf("Failed to read line %d from settings file\r\n", lineNumber); + } + else if (line[n - 1] != '\n' && n == (sizeof(line) - 1)) + { + systemPrintf("Settings line %d too long\r\n", lineNumber); if (lineNumber == 0) { - //If we can't read the first line of the settings file, give up - Serial.println(F("Giving up on settings file")); - xSemaphoreGive(xFATSemaphore); - return (false); + // If we can't read the first line of the settings file, give up + systemPrintln("Giving up on settings file"); + return (false); } - } - else if (parseLine(line) == false) { - Serial.printf("Failed to parse line %d: %s\r\n", lineNumber, line); + } + else if (parseLine(line, settings) == false) + { + systemPrintf("Failed to parse line %d: %s\r\n", lineNumber, line); if (lineNumber == 0) { - //If we can't read the first line of the settings file, give up - Serial.println(F("Giving up on settings file")); - xSemaphoreGive(xFATSemaphore); - return (false); + // If we can't read the first line of the settings file, give up + systemPrintln("Giving up on settings file"); + return (false); } - } + } - lineNumber++; + lineNumber++; + if (lineNumber > 400) // Arbitrary limit. Catch corrupt files. + { + log_d("Giving up reading file: %s", fileName); + break; } + } - //Serial.println(F("Config file read complete")); - settingsFile.close(); - xSemaphoreGive(xFATSemaphore); - return (true); - } - else - { - Serial.println(F("No config file found. Using settings from EEPROM.")); - //The defaults of the struct will be recorded to a file later on. - xSemaphoreGive(xFATSemaphore); - return (false); - } + settingsFile.close(); + return (true); +} + +// Convert a given line from file into a settingName and value +// Sets the setting if the name is known +bool parseLine(char *str, Settings *settings) +{ + char *ptr; - } //End Semaphore check - } //End SD online + // Set strtok start of line. + str = strtok(str, "="); + if (!str) + { + log_d("Fail"); + return false; + } + + // Store this setting name + char settingName[100]; + snprintf(settingName, sizeof(settingName), "%s", str); + + double d = 0.0; + char settingString[100] = ""; + + // Move pointer to end of line + str = strtok(nullptr, "\n"); + if (!str) + { + // This line does not contain a \n or the settingString is zero length + // so there is nothing to parse + // https://github.com/sparkfun/SparkFun_RTK_Firmware/issues/77 + } + else + { + // if (strcmp(settingName, "ntripServer_CasterHost") == 0) //Debug + // if (strcmp(settingName, "profileName") == 0) //Debug + // systemPrintf("Found problem spot raw: %s\r\n", str); + + // Assume the value is a string such as 8d8a48b. The leading number causes skipSpace to fail. + // If settingString has a mix of letters and numbers, just convert to string + snprintf(settingString, sizeof(settingString), "%s", str); + + // Check if string is mixed: 8a011EF, 192.168.1.1, -102.4, t6-h4$, etc. + bool hasSymbol = false; + int decimalCount = 0; + for (int x = 0; x < strlen(settingString); x++) + { + if (settingString[x] == '.') + decimalCount++; + else if (x == 0 && settingString[x] == '-') + { + ; // Do nothing + } + else if (isAlpha(settingString[x])) + hasSymbol = true; + else if (isDigit(settingString[x]) == false) + hasSymbol = true; + } + + // See issue: https://github.com/sparkfun/SparkFun_RTK_Firmware/issues/274 + if (hasSymbol || decimalCount > 1) + { + // It's a mix. Skip strtod. + + // if (strcmp(settingName, "ntripServer_CasterHost") == 0) //Debug + // systemPrintf("Skipping strtod - settingString: %s\r\n", settingString); + } + else + { + // Attempt to convert string to double + d = strtod(str, &ptr); + + if (d == 0.0) // strtod failed, may be string or may be 0 but let it pass + { + snprintf(settingString, sizeof(settingString), "%s", str); + } + else + { + if (str == ptr || *skipSpace(ptr)) + return false; // Check str pointer + } + } + } + + // log_d("settingName: %s - value: %s - d: %0.9f", settingName, settingString, d); + + // Get setting name + if (strcmp(settingName, "sizeOfSettings") == 0) + { + // We may want to cause a factory reset from the settings file rather than the menu + // If user sets sizeOfSettings to -1 in config file, RTK Surveyor will factory reset + if (d == -1) + { + // Erase file system, erase settings file, reset u-blox module, display message on OLED + factoryReset(true); // We already have the SD semaphore + } + + // Check to see if this setting file is compatible with this version of RTK Surveyor + if (d != sizeof(Settings)) + log_d("Settings size is %d but current firmware expects %d. Attempting to use settings from file.", (int)d, + sizeof(Settings)); + } + + else if (strcmp(settingName, "rtkIdentifier") == 0) + { + } // Do nothing. Just read it to avoid 'Unknown setting' error + else if (strcmp(settingName, "rtkFirmwareVersion") == 0) + { + } // Do nothing. Just read it to avoid 'Unknown setting' error + else if (strcmp(settingName, "zedFirmwareVersion") == 0) + { + } // Do nothing. Just read it to avoid 'Unknown setting' error + else if (strcmp(settingName, "zedUniqueId") == 0) + { + } // Do nothing. Just read it to avoid 'Unknown setting' error + else if (strcmp(settingName, "neoFirmwareVersion") == 0) + { + } // Do nothing. Just read it to avoid 'Unknown setting' error - Serial.println(F("Config file read failed: SD offline")); - return (false); //SD offline + else if (strcmp(settingName, "printDebugMessages") == 0) + settings->printDebugMessages = d; + else if (strcmp(settingName, "enableSD") == 0) + settings->enableSD = d; + else if (strcmp(settingName, "enableDisplay") == 0) + settings->enableDisplay = d; + else if (strcmp(settingName, "maxLogTime_minutes") == 0) + settings->maxLogTime_minutes = d; + else if (strcmp(settingName, "maxLogLength_minutes") == 0) + settings->maxLogLength_minutes = d; + else if (strcmp(settingName, "observationSeconds") == 0) + { + if (settings->observationSeconds != + d) // If a setting for the ZED has changed, apply, and trigger module config update + { + settings->observationSeconds = d; + settings->updateZEDSettings = true; + } + } + else if (strcmp(settingName, "observationPositionAccuracy") == 0) + { + if (settings->observationPositionAccuracy != d) + { + settings->observationPositionAccuracy = d; + settings->updateZEDSettings = true; + } + } + else if (strcmp(settingName, "fixedBase") == 0) + { + if (settings->fixedBase != d) + { + settings->fixedBase = d; + settings->updateZEDSettings = true; + } + } + else if (strcmp(settingName, "fixedBaseCoordinateType") == 0) + { + if (settings->fixedBaseCoordinateType != d) + { + settings->fixedBaseCoordinateType = d; + settings->updateZEDSettings = true; + } + } + else if (strcmp(settingName, "fixedEcefX") == 0) + { + if (settings->fixedEcefX != d) + { + settings->fixedEcefX = d; + settings->updateZEDSettings = true; + } + } + else if (strcmp(settingName, "fixedEcefY") == 0) + { + if (settings->fixedEcefY != d) + { + settings->fixedEcefY = d; + settings->updateZEDSettings = true; + } + } + else if (strcmp(settingName, "fixedEcefZ") == 0) + { + if (settings->fixedEcefZ != d) + { + settings->fixedEcefZ = d; + settings->updateZEDSettings = true; + } + } + else if (strcmp(settingName, "fixedLat") == 0) + { + if (settings->fixedLat != d) + { + settings->fixedLat = d; + settings->updateZEDSettings = true; + } + } + else if (strcmp(settingName, "fixedLong") == 0) + { + if (settings->fixedLong != d) + { + settings->fixedLong = d; + settings->updateZEDSettings = true; + } + } + else if (strcmp(settingName, "fixedAltitude") == 0) + { + if (settings->fixedAltitude != d) + { + settings->fixedAltitude = d; + settings->updateZEDSettings = true; + } + } + else if (strcmp(settingName, "dataPortBaud") == 0) + { + if (settings->dataPortBaud != d) + { + settings->dataPortBaud = d; + settings->updateZEDSettings = true; + } + } + else if (strcmp(settingName, "radioPortBaud") == 0) + { + if (settings->radioPortBaud != d) + { + settings->radioPortBaud = d; + settings->updateZEDSettings = true; + } + } + else if (strcmp(settingName, "surveyInStartingAccuracy") == 0) + settings->surveyInStartingAccuracy = d; + else if (strcmp(settingName, "measurementRate") == 0) + { + if (settings->measurementRate != d) + { + settings->measurementRate = d; + settings->updateZEDSettings = true; + } + } + else if (strcmp(settingName, "navigationRate") == 0) + { + if (settings->navigationRate != d) + { + settings->navigationRate = d; + settings->updateZEDSettings = true; + } + } + else if (strcmp(settingName, "enableI2Cdebug") == 0) + settings->enableI2Cdebug = d; + else if (strcmp(settingName, "enableHeapReport") == 0) + settings->enableHeapReport = d; + else if (strcmp(settingName, "enableTaskReports") == 0) + settings->enableTaskReports = d; + else if (strcmp(settingName, "dataPortChannel") == 0) + settings->dataPortChannel = (muxConnectionType_e)d; + else if (strcmp(settingName, "spiFrequency") == 0) + settings->spiFrequency = d; + else if (strcmp(settingName, "enableLogging") == 0) + settings->enableLogging = d; + else if (strcmp(settingName, "enableARPLogging") == 0) + settings->enableARPLogging = d; + else if (strcmp(settingName, "ARPLoggingInterval_s") == 0) + settings->ARPLoggingInterval_s = d; + else if (strcmp(settingName, "enableMarksFile") == 0) + settings->enableMarksFile = d; + else if (strcmp(settingName, "enableNTPFile") == 0) + settings->enableNTPFile = d; + else if (strcmp(settingName, "enableUART2UBXIn") == 0) + settings->enableUART2UBXIn = d; + else if (strcmp(settingName, "sppRxQueueSize") == 0) + settings->sppRxQueueSize = d; + else if (strcmp(settingName, "sppTxQueueSize") == 0) + settings->sppTxQueueSize = d; + else if (strcmp(settingName, "dynamicModel") == 0) + { + if (settings->dynamicModel != d) + { + settings->dynamicModel = d; + settings->updateZEDSettings = true; + } + } + else if (strcmp(settingName, "lastState") == 0) + { + if (settings->lastState != (SystemState)d) + { + settings->lastState = (SystemState)d; + settings->updateZEDSettings = true; + } + } + else if (strcmp(settingName, "enableSensorFusion") == 0) + { + if (settings->enableSensorFusion != d) + { + settings->enableSensorFusion = d; + settings->updateZEDSettings = true; + } + } + else if (strcmp(settingName, "autoIMUmountAlignment") == 0) + { + if (settings->autoIMUmountAlignment != d) + { + settings->autoIMUmountAlignment = d; + settings->updateZEDSettings = true; + } + } + else if (strcmp(settingName, "enableResetDisplay") == 0) + settings->enableResetDisplay = d; + else if (strcmp(settingName, "enableExternalPulse") == 0) + { + if (settings->enableExternalPulse != d) + { + settings->enableExternalPulse = d; + settings->updateZEDSettings = true; + } + } + else if (strcmp(settingName, "externalPulseTimeBetweenPulse_us") == 0) + { + if (settings->externalPulseTimeBetweenPulse_us != d) + { + settings->externalPulseTimeBetweenPulse_us = d; + settings->updateZEDSettings = true; + } + } + else if (strcmp(settingName, "externalPulseLength_us") == 0) + { + if (settings->externalPulseLength_us != d) + { + settings->externalPulseLength_us = d; + settings->updateZEDSettings = true; + } + } + else if (strcmp(settingName, "externalPulsePolarity") == 0) + { + if (settings->externalPulsePolarity != (pulseEdgeType_e)d) + { + settings->externalPulsePolarity = (pulseEdgeType_e)d; + settings->updateZEDSettings = true; + } + } + else if (strcmp(settingName, "enableExternalHardwareEventLogging") == 0) + { + if (settings->enableExternalHardwareEventLogging != d) + { + settings->enableExternalHardwareEventLogging = d; + settings->updateZEDSettings = true; + } + } + else if (strcmp(settingName, "profileName") == 0) + strcpy(settings->profileName, settingString); + else if (strcmp(settingName, "enableNtripServer") == 0) + settings->enableNtripServer = d; + else if (strcmp(settingName, "ntripServer_StartAtSurveyIn") == 0) + settings->ntripServer_StartAtSurveyIn = d; + else if (strcmp(settingName, "enableNtripClient") == 0) + settings->enableNtripClient = d; + else if (strcmp(settingName, "ntripClient_CasterHost") == 0) + strcpy(settings->ntripClient_CasterHost, settingString); + else if (strcmp(settingName, "ntripClient_CasterPort") == 0) + settings->ntripClient_CasterPort = d; + else if (strcmp(settingName, "ntripClient_CasterUser") == 0) + strcpy(settings->ntripClient_CasterUser, settingString); + else if (strcmp(settingName, "ntripClient_CasterUserPW") == 0) + strcpy(settings->ntripClient_CasterUserPW, settingString); + else if (strcmp(settingName, "ntripClient_MountPoint") == 0) + strcpy(settings->ntripClient_MountPoint, settingString); + else if (strcmp(settingName, "ntripClient_MountPointPW") == 0) + strcpy(settings->ntripClient_MountPointPW, settingString); + else if (strcmp(settingName, "ntripClient_TransmitGGA") == 0) + settings->ntripClient_TransmitGGA = d; + else if (strcmp(settingName, "serialTimeoutGNSS") == 0) + settings->serialTimeoutGNSS = d; + + // Point Perfect + else if (strcmp(settingName, "pointPerfectDeviceProfileToken") == 0) + strcpy(settings->pointPerfectDeviceProfileToken, settingString); + else if (strcmp(settingName, "enablePointPerfectCorrections") == 0) + settings->enablePointPerfectCorrections = d; + else if (strcmp(settingName, "autoKeyRenewal") == 0) + settings->autoKeyRenewal = d; + else if (strcmp(settingName, "pointPerfectClientID") == 0) + strcpy(settings->pointPerfectClientID, settingString); + else if (strcmp(settingName, "pointPerfectBrokerHost") == 0) + strcpy(settings->pointPerfectBrokerHost, settingString); + else if (strcmp(settingName, "pointPerfectLBandTopic") == 0) + strcpy(settings->pointPerfectLBandTopic, settingString); + + else if (strcmp(settingName, "pointPerfectCurrentKey") == 0) + strcpy(settings->pointPerfectCurrentKey, settingString); + else if (strcmp(settingName, "pointPerfectCurrentKeyDuration") == 0) + settings->pointPerfectCurrentKeyDuration = d; + else if (strcmp(settingName, "pointPerfectCurrentKeyStart") == 0) + settings->pointPerfectCurrentKeyStart = d; + + else if (strcmp(settingName, "pointPerfectNextKey") == 0) + strcpy(settings->pointPerfectNextKey, settingString); + else if (strcmp(settingName, "pointPerfectNextKeyDuration") == 0) + settings->pointPerfectNextKeyDuration = d; + else if (strcmp(settingName, "pointPerfectNextKeyStart") == 0) + settings->pointPerfectNextKeyStart = d; + + else if (strcmp(settingName, "lastKeyAttempt") == 0) + settings->lastKeyAttempt = d; + else if (strcmp(settingName, "debugPpCertificate") == 0) + settings->debugPpCertificate = d; + + else if (strcmp(settingName, "updateZEDSettings") == 0) + { + if (settings->updateZEDSettings != d) + settings->updateZEDSettings = true; // If there is a discrepancy, push ZED reconfig + } + else if (strcmp(settingName, "timeZoneHours") == 0) + settings->timeZoneHours = d; + else if (strcmp(settingName, "timeZoneMinutes") == 0) + settings->timeZoneMinutes = d; + else if (strcmp(settingName, "timeZoneSeconds") == 0) + settings->timeZoneSeconds = d; + else if (strcmp(settingName, "enablePrintState") == 0) + settings->enablePrintState = d; + else if (strcmp(settingName, "debugWifiState") == 0) + settings->debugWifiState = d; + else if (strcmp(settingName, "debugNtripClientState") == 0) + settings->debugNtripClientState = d; + else if (strcmp(settingName, "debugNtripServerState") == 0) + settings->debugNtripServerState = d; + else if (strcmp(settingName, "enablePrintPosition") == 0) + settings->enablePrintPosition = d; + else if (strcmp(settingName, "enablePrintIdleTime") == 0) + settings->enablePrintIdleTime = d; + else if (strcmp(settingName, "enablePrintBatteryMessages") == 0) + settings->enablePrintBatteryMessages = d; + else if (strcmp(settingName, "enablePrintRoverAccuracy") == 0) + settings->enablePrintRoverAccuracy = d; + else if (strcmp(settingName, "enablePrintBadMessages") == 0) + settings->enablePrintBadMessages = d; + else if (strcmp(settingName, "enablePrintLogFileMessages") == 0) + settings->enablePrintLogFileMessages = d; + else if (strcmp(settingName, "enablePrintLogFileStatus") == 0) + settings->enablePrintLogFileStatus = d; + else if (strcmp(settingName, "enablePrintRingBufferOffsets") == 0) + settings->enablePrintRingBufferOffsets = d; + else if (strcmp(settingName, "debugNtripServerRtcm") == 0) + settings->debugNtripServerRtcm = d; + else if (strcmp(settingName, "debugNtripClientRtcm") == 0) + settings->debugNtripClientRtcm = d; + else if (strcmp(settingName, "enablePrintStates") == 0) + settings->enablePrintStates = d; + else if (strcmp(settingName, "enablePrintDuplicateStates") == 0) + settings->enablePrintDuplicateStates = d; + else if (strcmp(settingName, "enablePrintRtcSync") == 0) + settings->enablePrintRtcSync = d; + else if (strcmp(settingName, "debugNtp") == 0) + settings->debugNtp = d; + else if (strcmp(settingName, "enablePrintEthernetDiag") == 0) + settings->enablePrintEthernetDiag = d; + else if (strcmp(settingName, "radioType") == 0) + settings->radioType = (RadioType_e)d; + else if (strcmp(settingName, "espnowPeerCount") == 0) + settings->espnowPeerCount = d; + else if (strcmp(settingName, "enableRtcmMessageChecking") == 0) + settings->enableRtcmMessageChecking = d; + else if (strcmp(settingName, "radioType") == 0) + settings->radioType = (RadioType_e)d; + else if (strcmp(settingName, "bluetoothRadioType") == 0) + settings->bluetoothRadioType = (BluetoothRadioType_e)d; + else if (strcmp(settingName, "enablePvtClient") == 0) + settings->enablePvtClient = d; + else if (strcmp(settingName, "enablePvtServer") == 0) + settings->enablePvtServer = d; + else if (strcmp(settingName, "enablePvtUdpServer") == 0) + settings->enablePvtUdpServer = d; + else if (strcmp(settingName, "debugPvtClient") == 0) + settings->debugPvtClient = d; + else if (strcmp(settingName, "debugPvtServer") == 0) + settings->debugPvtServer = d; + else if (strcmp(settingName, "debugPvtUdpServer") == 0) + settings->debugPvtUdpServer = d; + else if (strcmp(settingName, "espnowBroadcast") == 0) + settings->espnowBroadcast = d; + else if (strcmp(settingName, "antennaHeight") == 0) + settings->antennaHeight = d; + else if (strcmp(settingName, "antennaReferencePoint") == 0) + settings->antennaReferencePoint = d; + else if (strcmp(settingName, "echoUserInput") == 0) + settings->echoUserInput = d; + else if (strcmp(settingName, "uartReceiveBufferSize") == 0) + settings->uartReceiveBufferSize = d; + else if (strcmp(settingName, "gnssHandlerBufferSize") == 0) + settings->gnssHandlerBufferSize = d; + else if (strcmp(settingName, "enablePrintBufferOverrun") == 0) + settings->enablePrintBufferOverrun = d; + else if (strcmp(settingName, "enablePrintSDBuffers") == 0) + settings->enablePrintSDBuffers = d; + else if (strcmp(settingName, "periodicDisplay") == 0) + settings->periodicDisplay = d; + else if (strcmp(settingName, "periodicDisplayInterval") == 0) + settings->periodicDisplayInterval = d; + else if (strcmp(settingName, "rebootSeconds") == 0) + settings->rebootSeconds = d; + else if (strcmp(settingName, "forceResetOnSDFail") == 0) + settings->forceResetOnSDFail = d; + else if (strcmp(settingName, "wifiConfigOverAP") == 0) + settings->wifiConfigOverAP = d; + else if (strcmp(settingName, "pvtServerPort") == 0) + settings->pvtServerPort = d; + else if (strcmp(settingName, "pvtUdpServerPort") == 0) + settings->pvtUdpServerPort = d; + else if (strcmp(settingName, "minElev") == 0) + { + if (settings->minElev != d) + { + settings->minElev = d; + settings->updateZEDSettings = true; + } + } + else if (strcmp(settingName, "imuYaw") == 0) + { + if (settings->imuYaw != d) + { + settings->imuYaw = d; + settings->updateZEDSettings = true; + } + } + else if (strcmp(settingName, "imuPitch") == 0) + { + if (settings->imuPitch != d) + { + settings->imuPitch = d; + settings->updateZEDSettings = true; + } + } + else if (strcmp(settingName, "imuRoll") == 0) + { + if (settings->imuRoll != d) + { + settings->imuRoll = d; + settings->updateZEDSettings = true; + } + } + else if (strcmp(settingName, "sfDisableWheelDirection") == 0) + { + if (settings->sfDisableWheelDirection != d) + { + settings->sfDisableWheelDirection = d; + settings->updateZEDSettings = true; + } + } + else if (strcmp(settingName, "sfCombineWheelTicks") == 0) + { + if (settings->sfCombineWheelTicks != d) + { + settings->sfCombineWheelTicks = d; + settings->updateZEDSettings = true; + } + } + else if (strcmp(settingName, "rateNavPrio") == 0) + { + if (settings->rateNavPrio != d) + { + settings->rateNavPrio = d; + settings->updateZEDSettings = true; + } + } + else if (strcmp(settingName, "sfUseSpeed") == 0) + { + if (settings->sfUseSpeed != d) + { + settings->sfUseSpeed = d; + settings->updateZEDSettings = true; + } + } + // Ethernet + else if (strcmp(settingName, "ethernetIP") == 0) + { + String addr = String(settingString); + settings->ethernetIP.fromString(addr); + } + else if (strcmp(settingName, "ethernetDNS") == 0) + { + String addr = String(settingString); + settings->ethernetDNS.fromString(addr); + } + else if (strcmp(settingName, "ethernetGateway") == 0) + { + String addr = String(settingString); + settings->ethernetGateway.fromString(addr); + } + else if (strcmp(settingName, "ethernetSubnet") == 0) + { + String addr = String(settingString); + settings->ethernetSubnet.fromString(addr); + } + else if (strcmp(settingName, "httpPort") == 0) + settings->httpPort = d; + else if (strcmp(settingName, "ethernetNtpPort") == 0) + settings->ethernetNtpPort = d; + else if (strcmp(settingName, "ethernetDHCP") == 0) + settings->ethernetDHCP = d; + else if (strcmp(settingName, "pvtClientPort") == 0) + settings->pvtClientPort = d; + else if (strcmp(settingName, "pvtClientHost") == 0) + strcpy(settings->pvtClientHost, settingString); + // NTP + else if (strcmp(settingName, "ntpPollExponent") == 0) + settings->ntpPollExponent = d; + else if (strcmp(settingName, "ntpPrecision") == 0) + settings->ntpPrecision = d; + else if (strcmp(settingName, "ntpRootDelay") == 0) + settings->ntpRootDelay = d; + else if (strcmp(settingName, "ntpRootDispersion") == 0) + settings->ntpRootDispersion = d; + else if (strcmp(settingName, "ntpReferenceId") == 0) + { + strcpy(settings->ntpReferenceId, settingString); + for (int i = strlen(settingString); i < 5; i++) + settings->ntpReferenceId[i] = 0; + } + else if (strcmp(settingName, "coordinateInputType") == 0) + settings->coordinateInputType = (CoordinateInputType)d; + else if (strcmp(settingName, "lbandFixTimeout_seconds") == 0) + settings->lbandFixTimeout_seconds = d; + else if (strcmp(settingName, "minCNO_F9R") == 0) + { + if (settings->minCNO_F9R != d) + { + settings->minCNO_F9R = d; + settings->updateZEDSettings = true; + } + } + else if (strcmp(settingName, "minCNO_F9P") == 0) + { + if (settings->minCNO_F9P != d) + { + settings->minCNO_F9P = d; + settings->updateZEDSettings = true; + } + } + else if (strcmp(settingName, "mdnsEnable") == 0) + settings->mdnsEnable = d; + else if (strcmp(settingName, "serialGNSSRxFullThreshold") == 0) + settings->serialGNSSRxFullThreshold = d; + else if (strcmp(settingName, "btReadTaskPriority") == 0) + settings->btReadTaskPriority = d; + else if (strcmp(settingName, "gnssReadTaskPriority") == 0) + settings->gnssReadTaskPriority = d; + else if (strcmp(settingName, "handleGnssDataTaskPriority") == 0) + settings->handleGnssDataTaskPriority = d; + else if (strcmp(settingName, "btReadTaskCore") == 0) + settings->btReadTaskCore = d; + else if (strcmp(settingName, "gnssReadTaskCore") == 0) + settings->gnssReadTaskCore = d; + else if (strcmp(settingName, "handleGnssDataTaskCore") == 0) + settings->handleGnssDataTaskCore = d; + else if (strcmp(settingName, "gnssUartInterruptsCore") == 0) + settings->gnssUartInterruptsCore = d; + else if (strcmp(settingName, "bluetoothInterruptsCore") == 0) + settings->bluetoothInterruptsCore = d; + else if (strcmp(settingName, "i2cInterruptsCore") == 0) + settings->i2cInterruptsCore = d; + else if (strcmp(settingName, "shutdownNoChargeTimeout_s") == 0) + settings->shutdownNoChargeTimeout_s = d; + else if (strcmp(settingName, "disableSetupButton") == 0) + settings->disableSetupButton = d; + else if (strcmp(settingName, "useI2cForLbandCorrections") == 0) + settings->useI2cForLbandCorrections = d; + else if (strcmp(settingName, "useI2cForLbandCorrectionsConfigured") == 0) + settings->useI2cForLbandCorrectionsConfigured = d; + + // Network layer + else if (strcmp(settingName, "defaultNetworkType") == 0) + settings->defaultNetworkType = d; + else if (strcmp(settingName, "debugNetworkLayer") == 0) + settings->debugNetworkLayer = d; + else if (strcmp(settingName, "enableNetworkFailover") == 0) + settings->enableNetworkFailover = d; + else if (strcmp(settingName, "printNetworkStatus") == 0) + settings->printNetworkStatus = d; + else if (strcmp(settingName, "rtcmTimeoutBeforeUsingLBand_s") == 0) + settings->rtcmTimeoutBeforeUsingLBand_s = d; + + // Automatic Firmware Update + else if (strcmp(settingName, "autoFirmwareCheckMinutes") == 0) + settings->autoFirmwareCheckMinutes = d; + else if (strcmp(settingName, "debugFirmwareUpdate") == 0) + settings->debugFirmwareUpdate = d; + else if (strcmp(settingName, "enableAutoFirmwareUpdate") == 0) + settings->enableAutoFirmwareUpdate = d; + + else if (strcmp(settingName, "debugLBand") == 0) + settings->debugLBand = d; + else if (strcmp(settingName, "enableCaptivePortal") == 0) + settings->enableCaptivePortal = d; + else if (strcmp(settingName, "enableZedUsb") == 0) + settings->enableZedUsb = d; + else if (strcmp(settingName, "debugWiFiConfig") == 0) + settings->debugWiFiConfig = d; + + else if (strcmp(settingName, "geographicRegion") == 0) + settings->geographicRegion = d; + + // Add new settings above + //<------------------------------------------------------------> + + // Check for bulk settings (WiFi credentials, constellations, message rates, ESPNOW Peers) + // Must be last on else list + else + { + bool knownSetting = false; + + // Scan for WiFi settings + if (knownSetting == false) + { + for (int x = 0; x < MAX_WIFI_NETWORKS; x++) + { + char tempString[100]; // wifiNetwork0Password=parachutes + snprintf(tempString, sizeof(tempString), "wifiNetwork%dSSID", x); + if (strcmp(settingName, tempString) == 0) + { + strcpy(settings->wifiNetworks[x].ssid, settingString); + knownSetting = true; + break; + } + else + { + snprintf(tempString, sizeof(tempString), "wifiNetwork%dPassword", x); + if (strcmp(settingName, tempString) == 0) + { + strcpy(settings->wifiNetworks[x].password, settingString); + knownSetting = true; + break; + } + } + } + } + + // Scan for constellation settings + if (knownSetting == false) + { + for (int x = 0; x < MAX_CONSTELLATIONS; x++) + { + char tempString[50]; // constellation.GPS=1 + snprintf(tempString, sizeof(tempString), "constellation.%s", settings->ubxConstellations[x].textName); + + if (strcmp(settingName, tempString) == 0) + { + if (settings->ubxConstellations[x].enabled != d) + { + settings->ubxConstellations[x].enabled = d; + settings->updateZEDSettings = true; + } + + knownSetting = true; + break; + } + } + } + + // Scan for message settings + if (knownSetting == false) + { + for (int x = 0; x < MAX_UBX_MSG; x++) + { + char tempString[50]; // message.nmea_dtm.msgRate=5 + snprintf(tempString, sizeof(tempString), "message.%s.msgRate", ubxMessages[x].msgTextName); + + if (strcmp(settingName, tempString) == 0) + { + if (settings->ubxMessageRates[x] != d) + { + settings->ubxMessageRates[x] = d; + settings->updateZEDSettings = true; + } + + knownSetting = true; + break; + } + } + } + + // Scan for Base RTCM message settings + if (knownSetting == false) + { + int firstRTCMRecord = getMessageNumberByName("UBX_RTCM_1005"); + + for (int x = 0; x < MAX_UBX_MSG_RTCM; x++) + { + char tempString[50]; // messageBase.UBX_RTCM_1094.msgRate=5 + + snprintf(tempString, sizeof(tempString), "messageBase.%s.msgRate", + ubxMessages[firstRTCMRecord + x].msgTextName); + + if (strcmp(settingName, tempString) == 0) + { + if (settings->ubxMessageRatesBase[x] != d) + { + settings->ubxMessageRatesBase[x] = d; + settings->updateZEDSettings = true; + } + + knownSetting = true; + break; + } + } + } + + // Scan for ESPNOW peers + if (knownSetting == false) + { + for (int x = 0; x < ESPNOW_MAX_PEERS; x++) + { + char tempString[50]; // espnowPeers.1=B4,C1,33,42,DE,01, + snprintf(tempString, sizeof(tempString), "espnowPeers.%d", x); + + if (strcmp(settingName, tempString) == 0) + { + uint8_t macAddress[6]; + uint8_t macByte = 0; + + char *token = strtok(settingString, ","); // Break string up on , + while (token != nullptr && macByte < sizeof(macAddress)) + { + settings->espnowPeers[x][macByte++] = (uint8_t)strtol(token, nullptr, 16); + token = strtok(nullptr, ","); + } + + knownSetting = true; + break; + } + } + } + + // Scan for ntripServer_CasterHost + if (knownSetting == false) + { + for (int serverIndex = 0; serverIndex < NTRIP_SERVER_MAX; serverIndex++) + { + char tempString[50]; + snprintf(tempString, sizeof(tempString), "ntripServer_CasterHost_%d", serverIndex); + if (strcmp(settingName, tempString) == 0) + { + strcpy(&settings->ntripServer_CasterHost[serverIndex][0], settingString); + knownSetting = true; + break; + } + } + } + + // Scan for ntripServer_CasterPort + if (knownSetting == false) + { + for (int serverIndex = 0; serverIndex < NTRIP_SERVER_MAX; serverIndex++) + { + char tempString[50]; + snprintf(tempString, sizeof(tempString), "ntripServer_CasterPort_%d", serverIndex); + if (strcmp(settingName, tempString) == 0) + { + settings->ntripServer_CasterPort[serverIndex] = d; + knownSetting = true; + break; + } + } + } + + // Scan for ntripServer_CasterUser + if (knownSetting == false) + { + for (int serverIndex = 0; serverIndex < NTRIP_SERVER_MAX; serverIndex++) + { + char tempString[50]; + snprintf(tempString, sizeof(tempString), "ntripServer_CasterUser_%d", serverIndex); + if (strcmp(settingName, tempString) == 0) + { + strcpy(&settings->ntripServer_CasterUser[serverIndex][0], settingString); + knownSetting = true; + break; + } + } + } + + // Scan for ntripServer_CasterUserPW + if (knownSetting == false) + { + for (int serverIndex = 0; serverIndex < NTRIP_SERVER_MAX; serverIndex++) + { + char tempString[50]; + snprintf(tempString, sizeof(tempString), "ntripServer_CasterUserPW_%d", serverIndex); + if (strcmp(settingName, tempString) == 0) + { + strcpy(&settings->ntripServer_CasterUserPW[serverIndex][0], settingString); + knownSetting = true; + break; + } + } + } + + // Scan for ntripServer_MountPoint + if (knownSetting == false) + { + for (int serverIndex = 0; serverIndex < NTRIP_SERVER_MAX; serverIndex++) + { + char tempString[50]; + snprintf(tempString, sizeof(tempString), "ntripServer_MountPoint_%d", serverIndex); + if (strcmp(settingName, tempString) == 0) + { + strcpy(&settings->ntripServer_MountPoint[serverIndex][0], settingString); + knownSetting = true; + break; + } + } + } + + // Scan for ntripServer_MountPointPW + if (knownSetting == false) + { + for (int serverIndex = 0; serverIndex < NTRIP_SERVER_MAX; serverIndex++) + { + char tempString[50]; + snprintf(tempString, sizeof(tempString), "ntripServer_MountPointPW_%d", serverIndex); + if (strcmp(settingName, tempString) == 0) + { + strcpy(&settings->ntripServer_MountPointPW[serverIndex][0], settingString); + knownSetting = true; + break; + } + } + } + + // Last catch + if (knownSetting == false) + { + log_d("Unknown setting %s", settingName); + } + } + + return (true); +} + +// The SD library doesn't have a fgets function like SD fat so recreate it here +// Read the current line in the file until we hit a EOL char \r or \n +int getLine(File *openFile, char *lineChars, int lineSize) +{ + int count = 0; + while (openFile->available()) + { + byte incoming = openFile->read(); + if (incoming == '\r' || incoming == '\n') + { + // Sometimes a line has multiple terminators + while (openFile->peek() == '\r' || openFile->peek() == '\n') + openFile->read(); // Dump it to prevent next line read corruption + break; + } + + lineChars[count++] = incoming; + + if (count == lineSize - 1) + break; // Stop before overun of buffer + } + lineChars[count] = '\0'; // Terminate string + return (count); } // Check for extra characters in field or find minus sign. -char* skipSpace(char* str) { - while (isspace(*str)) str++; - return str; +char *skipSpace(char *str) +{ + while (isspace(*str)) + str++; + return str; +} + +// Load the special profileNumber file in LittleFS and return one byte value +void loadProfileNumber() +{ + if (profileNumber < MAX_PROFILE_COUNT) + return; // Only load it once + + File fileProfileNumber = LittleFS.open("/profileNumber.txt", FILE_READ); + if (!fileProfileNumber) + { + log_d("profileNumber.txt not found"); + settings.updateZEDSettings = true; // Force module update + recordProfileNumber(0); // Record profile + } + else + { + profileNumber = fileProfileNumber.read(); + fileProfileNumber.close(); + } + + // We have arbitrary limit of user profiles + if (profileNumber >= MAX_PROFILE_COUNT) + { + log_d("ProfileNumber invalid. Going to zero."); + settings.updateZEDSettings = true; // Force module update + recordProfileNumber(0); // Record profile + } + + log_d("Using profile #%d", profileNumber); } -//Convert a given line from file into a settingName and value -//Sets the setting if the name is known -bool parseLine(char* str) { - char* ptr; - - //Debug - //Serial.printf("Line contents: %s", str); - //Serial.flush(); - - // Set strtok start of line. - str = strtok(str, "="); - if (!str) return false; - - //Store this setting name - char settingName[40]; - sprintf(settingName, "%s", str); - - //Move pointer to end of line - str = strtok(nullptr, "\n"); - if (!str) return false; - - //Serial.printf("s = %s\r\n", str); - //Serial.flush(); - - //Attempt to convert string to double. - double d = strtod(str, &ptr); - - //Serial.printf("d = %lf\r\n", d); - //Serial.flush(); - - char settingValue[50]; - if (d == 0.0) //strtod failed, may be string or may be 0 but let it pass - { - sprintf(settingValue, "%s", str); - } - else - { - if (str == ptr || *skipSpace(ptr)) return false; //Check str pointer - - //See issue https://github.com/sparkfun/SparkFun_RTK_Firmware/issues/47 - sprintf(settingValue, "%1.0lf", d); //Catch when the input is pure numbers (strtod was successful), store as settingValue - } - - // Get setting name - if (strcmp(settingName, "sizeOfSettings") == 0) - { - //We may want to cause a factory reset from the settings file rather than the menu - //If user sets sizeOfSettings to -1 in config file, RTK Surveyor will factory reset - if (d == -1) - { - eepromErase(); - - //Assemble settings file name - char settingsFileName[40]; //SFE_Surveyor_Settings.txt - strcpy(settingsFileName, platformFilePrefix); - strcat(settingsFileName, "_Settings.txt"); - sd.remove(settingsFileName); - - Serial.printf("RTK %s has been factory reset via settings file. Freezing. Please restart and open terminal at 115200bps.\n\r", platformPrefix); - - while (1) - delay(1); //Prevent CPU freakout - } - - //Check to see if this setting file is compatible with this version of RTK Surveyor - if (d != sizeof(settings)) - Serial.printf("Warning: Settings size is %d but current firmware expects %d. Attempting to use settings from file.\r\n", (int)d, sizeof(settings)); - - } - else if (strcmp(settingName, "rtkIdentifier") == 0) - settings.rtkIdentifier = d; - else if (strcmp(settingName, "rtkFirmwareVersion") == 0) - {} //Do nothing. Just read it to avoid 'Unknown setting' error - else if (strcmp(settingName, "zedFirmwareVersion") == 0) - {} //Do nothing. Just read it to avoid 'Unknown setting' error - else if (strcmp(settingName, "printDebugMessages") == 0) - settings.printDebugMessages = d; - else if (strcmp(settingName, "enableSD") == 0) - settings.enableSD = d; - else if (strcmp(settingName, "enableDisplay") == 0) - settings.enableDisplay = d; - else if (strcmp(settingName, "maxLogTime_minutes") == 0) - settings.maxLogTime_minutes = d; - else if (strcmp(settingName, "observationSeconds") == 0) - settings.observationSeconds = d; - else if (strcmp(settingName, "observationPositionAccuracy") == 0) - settings.observationPositionAccuracy = d; - else if (strcmp(settingName, "fixedBase") == 0) - settings.fixedBase = d; - else if (strcmp(settingName, "fixedBaseCoordinateType") == 0) - settings.fixedBaseCoordinateType = d; - else if (strcmp(settingName, "fixedEcefX") == 0) - settings.fixedEcefX = d; - else if (strcmp(settingName, "fixedEcefY") == 0) - settings.fixedEcefY = d; - else if (strcmp(settingName, "fixedEcefZ") == 0) - settings.fixedEcefZ = d; - else if (strcmp(settingName, "fixedLat") == 0) - settings.fixedLat = d; - else if (strcmp(settingName, "fixedLong") == 0) - settings.fixedLong = d; - else if (strcmp(settingName, "fixedAltitude") == 0) - settings.fixedAltitude = d; - else if (strcmp(settingName, "dataPortBaud") == 0) - settings.dataPortBaud = d; - else if (strcmp(settingName, "radioPortBaud") == 0) - settings.radioPortBaud = d; - else if (strcmp(settingName, "enableNtripServer") == 0) - settings.enableNtripServer = d; - else if (strcmp(settingName, "casterHost") == 0) - strcpy(settings.casterHost, settingValue); - else if (strcmp(settingName, "casterPort") == 0) - settings.casterPort = d; - else if (strcmp(settingName, "mountPoint") == 0) - strcpy(settings.mountPoint, settingValue); - else if (strcmp(settingName, "mountPointPW") == 0) - strcpy(settings.mountPointPW, settingValue); - else if (strcmp(settingName, "wifiSSID") == 0) - strcpy(settings.wifiSSID, settingValue); - else if (strcmp(settingName, "wifiPW") == 0) - strcpy(settings.wifiPW, settingValue); - else if (strcmp(settingName, "surveyInStartingAccuracy") == 0) - settings.surveyInStartingAccuracy = d; - else if (strcmp(settingName, "measurementRate") == 0) - settings.measurementRate = d; - else if (strcmp(settingName, "navigationRate") == 0) - settings.navigationRate = d; - else if (strcmp(settingName, "enableI2Cdebug") == 0) - settings.enableI2Cdebug = d; - else if (strcmp(settingName, "enableHeapReport") == 0) - settings.enableHeapReport = d; - else if (strcmp(settingName, "enableTaskReports") == 0) - settings.enableTaskReports = d; - else if (strcmp(settingName, "dataPortChannel") == 0) - settings.dataPortChannel = (muxConnectionType_e)d; - else if (strcmp(settingName, "spiFrequency") == 0) - settings.spiFrequency = d; - else if (strcmp(settingName, "enableLogging") == 0) - settings.enableLogging = d; - else if (strcmp(settingName, "sppRxQueueSize") == 0) - settings.sppRxQueueSize = d; - else if (strcmp(settingName, "sppTxQueueSize") == 0) - settings.sppTxQueueSize = d; - else if (strcmp(settingName, "dynamicModel") == 0) - settings.dynamicModel = d; - else if (strcmp(settingName, "lastState") == 0) - settings.lastState = (SystemState)d; - else if (strcmp(settingName, "throttleDuringSPPCongestion") == 0) - settings.throttleDuringSPPCongestion = d; - - //Check for bulk settings (constellations and message rates) - //Must be last on else list - else - { - bool knownSetting = false; - - //Scan for constellation settings - if (knownSetting == false) - { - for (int x = 0 ; x < MAX_CONSTELLATIONS ; x++) - { - char tempString[50]; //constellation.GPS=1 - sprintf(tempString, "constellation.%s", ubxConstellations[x].textName); - - if (strcmp(settingName, tempString) == 0) - { - ubxConstellations[x].enabled = d; - knownSetting = true; - break; - } - } - } - - //Scan for message settings - if (knownSetting == false) - { - for (int x = 0 ; x < MAX_UBX_MSG ; x++) - { - char tempString[50]; //message.nmea_dtm.msgRate=5 - sprintf(tempString, "message.%s.msgRate", ubxMessages[x].msgTextName); - - if (strcmp(settingName, tempString) == 0) - { - ubxMessages[x].msgRate = d; - knownSetting = true; - break; - } - } - } - - //Last catch - if (knownSetting == false) - { - Serial.printf("Unknown setting %s on line: %s\r\n", settingName, str); - } - } - - return (true); +// Record the given profile number as well as a config bool +void recordProfileNumber(uint8_t newProfileNumber) +{ + profileNumber = newProfileNumber; + File fileProfileNumber = LittleFS.open("/profileNumber.txt", FILE_WRITE); + if (!fileProfileNumber) + { + log_d("profileNumber.txt failed to open"); + return; + } + fileProfileNumber.write(newProfileNumber); + fileProfileNumber.close(); } -//ESP32 doesn't have erase command so we do it here -void eepromErase() +// Populate profileNames[][] based on names found in LittleFS and SD +// If both SD and LittleFS contain a profile, SD wins. +uint8_t loadProfileNames() { - for (int i = 0 ; i < EEPROM_SIZE ; i++) { - EEPROM.write(i, 0xFF); //Reset to all 1s - } - EEPROM.commit(); + int profiles = 0; + + for (int x = 0; x < MAX_PROFILE_COUNT; x++) + profileNames[x][0] = '\0'; // Ensure every profile name is terminated + + // Check LittleFS and SD for profile names + for (int x = 0; x < MAX_PROFILE_COUNT; x++) + { + char fileName[60]; + snprintf(fileName, sizeof(fileName), "/%s_Settings_%d.txt", platformFilePrefix, x); + + if (getProfileName(fileName, profileNames[x], sizeof(profileNames[x])) == true) + // Mark this profile as active + profiles |= 1 << x; + } + + return (profiles); } -//The SD library doesn't have a fgets function like SD fat so recreate it here -//Read the current line in the file until we hit a EOL char \r or \n -int getLine(File * openFile, char * lineChars, int lineSize) +// Given a profile number, copy the current settings.profileName into the array of profile names +void setProfileName(uint8_t ProfileNumber) { - int count = 0; - while (openFile->available()) - { - byte incoming = openFile->read(); - if (incoming == '\r' || incoming == '\n') + // Update the name in the array of profile names + strncpy(profileNames[profileNumber], settings.profileName, sizeof(profileNames[0]) - 1); + + // Mark this profile as active + activeProfiles |= 1 << ProfileNumber; +} + +// Open the clear text file, scan for 'profileName' and return the string +// Returns true if successfully found tag in file, length may be zero +// Looks at LittleFS first, then SD +bool getProfileName(char *fileName, char *profileName, uint8_t profileNameLength) +{ + // Create a temporary settings struc on the heap (not the stack because it is ~4500 bytes) + Settings *tempSettings = new Settings; + + // If we have a profile in both LFS and SD, SD wins + bool responseLFS = loadSystemSettingsFromFileLFS(fileName, tempSettings); + bool responseSD = loadSystemSettingsFromFileSD(fileName, tempSettings); + + // Zero terminate the profile name + *profileName = 0; + if (responseLFS == true || responseSD == true) + snprintf(profileName, profileNameLength, "%s", tempSettings->profileName); // snprintf handles null terminator + + delete tempSettings; + + return (responseLFS | responseSD); +} + +// Loads a given profile name. +// Profiles may not be sequential (user might have empty profile #2, but filled #3) so we load the profile unit, not the +// number Return true if successful +bool getProfileNameFromUnit(uint8_t profileUnit, char *profileName, uint8_t profileNameLength) +{ + uint8_t located = 0; + + // Step through possible profiles looking for the specified unit + for (int x = 0; x < MAX_PROFILE_COUNT; x++) { - //Sometimes a line has multiple terminators - while (openFile->peek() == '\r' || openFile->peek() == '\n') - openFile->read(); //Dump it to prevent next line read corruption - break; + if (activeProfiles & (1 << x)) + { + if (located == profileUnit) + { + snprintf(profileName, profileNameLength, "%s", profileNames[x]); // snprintf handles null terminator + return (true); + } + + located++; // Valid settingFileName but not the unit we are looking for + } + } + log_d("Profile unit %d not found", profileUnit); + + return (false); +} + +// Return profile number based on units +// Profiles may not be sequential (user might have empty profile #2, but filled #3) so we look up the profile unit and +// return the count +uint8_t getProfileNumberFromUnit(uint8_t profileUnit) +{ + uint8_t located = 0; + + // Step through possible profiles looking for the 1st, 2nd, 3rd, or 4th unit + for (int x = 0; x < MAX_PROFILE_COUNT; x++) + { + if (activeProfiles & (1 << x)) + { + if (located == profileUnit) + return (x); + + located++; // Valid settingFileName but not the unit we are looking for + } + } + log_d("Profile unit %d not found", profileUnit); + + return (0); +} + +// Record large character blob to file +void recordFile(const char *fileID, char *fileContents, uint32_t fileSize) +{ + char fileName[80]; + snprintf(fileName, sizeof(fileName), "/%s_%s_%d.txt", platformFilePrefix, fileID, profileNumber); + + if (LittleFS.exists(fileName)) + { + LittleFS.remove(fileName); + log_d("Removing LittleFS: %s", fileName); } - lineChars[count++] = incoming; + File fileToWrite = LittleFS.open(fileName, FILE_WRITE); + if (!fileToWrite) + { + log_d("Failed to write to file %s", fileName); + } + else + { + fileToWrite.write((uint8_t *)fileContents, fileSize); // Store cert into file + fileToWrite.close(); + log_d("File recorded to LittleFS: %s", fileName); + } +} + +// Read file into given char array +void loadFile(const char *fileID, char *fileContents) +{ + char fileName[80]; + snprintf(fileName, sizeof(fileName), "/%s_%s_%d.txt", platformFilePrefix, fileID, profileNumber); - if (count == lineSize - 1) - break; //Stop before overun of buffer - } - lineChars[count] = '\0'; //Terminate string - return (count); + File fileToRead = LittleFS.open(fileName, FILE_READ); + if (fileToRead) + { + fileToRead.read((uint8_t *)fileContents, fileToRead.size()); // Read contents into pointer + fileToRead.close(); + log_d("File loaded from LittleFS: %s", fileName); + } + else + { + log_d("Failed to read from LittleFS: %s", fileName); + } } diff --git a/Firmware/RTK_Surveyor/Network.ino b/Firmware/RTK_Surveyor/Network.ino new file mode 100644 index 000000000..5211b6919 --- /dev/null +++ b/Firmware/RTK_Surveyor/Network.ino @@ -0,0 +1,1316 @@ +/*------------------------------------------------------------------------------ +Network.ino + + This module implements the network layer. An overview of the network stack + is shown below: + + Application Layer: + + NTRIP Server NTRIP Client TCP client + ^ ^ ^ + | | | + | | | + '-------+------+-------+-----' + ^ ^ + | | + | V + | Service Layer: DHCP, DNS, SSL, HTTP, ... + | ^ + | | + V V + Network (Client) Layer + ^ + | + .------------+------------. + | | + V V + Ethernet WiFi + + Network States: + + .-------------------->NETWORK_STATE_OFF + | | + | | + | | restart + | | or + | | networkUserOpen() + | networkStop() | networkStart() + | | + | | + | | + | V + +<-------------------NETWORK_STATE_DELAY--------------------------------. + ^ | | + | | Delay complete | + | | | + | V V + +<----------------NETWORK_STATE_CONNECTING----------------------------->+ + ^ | Network Failed | + | | Media connected | + | networkUserClose() | Retry | + | && V | + | activeUsers == 0 +<----------------. | + | | | networkUserClose() | + | V | && | + +<------------------NETWORK_STATE_IN_USE--------' activeUsers != 0 | + ^ | | + | | Network failed | + | | or | + | | networkStop() | + | V | + | +<----------------------------------------' + | | + | V + | +<----------------. + | | | networkUserClose() + | V | && + '-------------------NETWORK_WAIT_NO_USERS-------' activeUsers != 0 + + Network testing on an RTK Reference Station using NTRIP client: + + 1. Network retries using Ethernet, no WiFi setup: + * Remove Ethernet cable, expecting retry Ethernet after delay + * Progressive delay maxes out at 8 minutes + * After cable is plugged in NTRIP client restarts + + 2. Network retries using WiFi, use an invalid SSID, default network is WiFi, + failover disabled: + * WiFi fails to connect, expecting retry WiFi after delay + * Progressive delay maxes out at 8 minutes + * After a valid SSID is set, the NTRIP client restarts + + Network testing on Reference Station using NTRIP server: + + 1. Network retries using Ethernet, no WiFi setup: + * Remove Ethernet cable, expecting retry Ethernet after delay + * Progressive delay maxes out at 8 minutes + * After cable is plugged in NTRIP server restarts + + 2. Network retries using WiFi, use an invalid SSID, default network is WiFi, + failover disabled: + * WiFi fails to connect, expecting retry WiFi after delay + * Progressive delay maxes out at 8 minutes + * After cable is plugged in NTRIP server restarts + + Network failover testing on Reference Station, WiFi setup, failover enabled: + + 1. Using NTRIP client: + * Remove Ethernet cable, expecting failover to WiFi with no delay and + NTRIP client restarts + * Disable WiFi at access point, expecting failover to Ethernet with no + delay, NTRIP client restarts + + 2. Using NTRIP server: + * Remove Ethernet cable, expecting failover to WiFi with no delay and + NTRIP server restarts + * Disable WiFi at access point, expecting failover to Ethernet with no + delay, NTRIP server restarts + + Test Setup: + + RTK Reference Station + ^ ^ + WiFi | | Ethernet cable + v v + WiFi Access Point <-----------> Ethernet Switch + Ethernet ^ + Cable | Ethernet cable + v + Internet Firewall + ^ + | Ethernet cable + v + Modem + ^ + | + v + Internet + ^ + | + v + NTRIP Caster + + Possible NTRIP Casters + + * https://emlid.com/ntrip-caster/ + * http://rtk2go.com/ + * private SNIP NTRIP caster +------------------------------------------------------------------------------*/ + +#if COMPILE_NETWORK + +//---------------------------------------- +// Constants +//---------------------------------------- + +#define NETWORK_CONNECTION_TIMEOUT (15 * 1000) // Timeout for network media connection +#define NETWORK_DELAY_BEFORE_RETRY (75 * 100) // Delay between network connection retries +#define NETWORK_IP_ADDRESS_DISPLAY (12 * 1000) // Delay in milliseconds between display of IP address +#define NETWORK_MAX_IDLE_TIME 500 // Maximum network idle time before shutdown +#define NETWORK_MAX_RETRIES 7 // 7.5, 15, 30, 60, 2m, 4m, 8m + +// Specify which network to use next when a network failure occurs +const uint8_t networkFailover[] = +{ + NETWORK_TYPE_ETHERNET, // WiFi --> Ethernet + NETWORK_TYPE_WIFI, // Ethernet --> WiFi +}; +const int networkFailoverEntries = sizeof(networkFailover) / sizeof(networkFailover[0]); + +// List of network names +const char * const networkName[] = +{ + "WiFi", // NETWORK_TYPE_WIFI + "Ethernet", // NETWORK_TYPE_ETHERNET + "Hardware Default", // NETWORK_TYPE_DEFAULT + "Active", // NETWORK_TYPE_ACTIVE +}; +const int networkNameEntries = sizeof(networkName) / sizeof(networkName[0]); + +// List of state names +const char * const networkState[] = +{ + "NETWORK_STATE_OFF", + "NETWORK_STATE_DELAY", + "NETWORK_STATE_CONNECTING", + "NETWORK_STATE_IN_USE", + "NETWORK_STATE_WAIT_NO_USERS", +}; +const int networkStateEntries = sizeof(networkState) / sizeof(networkState[0]); + +// List of network users +const char * const networkUser[] = +{ + "NTP Server", + "NTRIP Client", + "OTA Firmware Update", + "PVT Client", + "PVT Server", + "PVT UDP Server", + "NTRIP Server 0", + "NTRIP Server 1", +}; +const int networkUserEntries = sizeof(networkUser) / sizeof(networkUser[0]); + +//---------------------------------------- +// Locals +//---------------------------------------- + +static NETWORK_DATA networkData = {NETWORK_TYPE_ACTIVE, NETWORK_TYPE_ACTIVE}; +static uint32_t networkLastIpAddressDisplayMillis[NETWORK_TYPE_MAX]; + +//---------------------------------------- +// Menu to get the common network settings +//---------------------------------------- +void menuNetwork() +{ + while (1) + { + systemPrintln(); + systemPrintln("Menu: Network"); + systemPrintln(); + + //------------------------------ + // Display the PVT client menu items + //------------------------------ + + // Display the menu + systemPrintf("1) PVT Client: %s\r\n", settings.enablePvtClient ? "Enabled" : "Disabled"); + if (settings.enablePvtClient) + { + systemPrintf("2) PVT Client Host: %s\r\n", settings.pvtClientHost); + systemPrintf("3) PVT Client Port: %ld\r\n", settings.pvtClientPort); + } + + //------------------------------ + // Display the PVT server menu items + //------------------------------ + + systemPrintf("4) PVT Server: %s\r\n", settings.enablePvtServer ? "Enabled" : "Disabled"); + + if (settings.enablePvtServer) + systemPrintf("5) PVT Server Port: %ld\r\n", settings.pvtServerPort); + + systemPrintf("6) PVT UDP Server: %s\r\n", settings.enablePvtUdpServer ? "Enabled" : "Disabled"); + + if (settings.enablePvtUdpServer) + systemPrintf("7) PVT UDP Server Port: %ld\r\n", settings.pvtUdpServerPort); + + if (HAS_ETHERNET) + { + //------------------------------ + // Display the network layer menu items + //------------------------------ + + systemPrint("d) Default network: "); + networkPrintName(settings.defaultNetworkType); + systemPrintln(); + + systemPrint("f) Ethernet / WiFi Failover: "); + systemPrintf("%s\r\n", settings.enableNetworkFailover ? "Enabled" : "Disabled"); + } + + //------------------------------ + // Finish the menu and get the input + //------------------------------ + + systemPrintln("x) Exit"); + byte incoming = getCharacterNumber(); + + //------------------------------ + // Get the PVT client parameters + //------------------------------ + + // Toggle PVT client enable + if (incoming == 1) + settings.enablePvtClient ^= 1; + + // Get the PVT client host + else if ((incoming == 2) && settings.enablePvtClient) + { + char hostname[sizeof(settings.pvtClientHost)]; + + systemPrint("Enter PVT client host name / address: "); + + // Get the host name or IP address + memset(hostname, 0, sizeof(hostname)); + getString(hostname, sizeof(hostname) - 1); + strcpy(settings.pvtClientHost, hostname); + + // Remove any http:// or https:// prefix from host name + // strtok modifies string to be parsed so we create a copy + strncpy(hostname, settings.pvtClientHost, sizeof(hostname) - 1); + char *token = strtok(hostname, "//"); + if (token != nullptr) + { + token = strtok(nullptr, "//"); // Advance to data after // + if (token != nullptr) + strcpy(settings.pvtClientHost, token); + } + } + + // Get the PVT client port number + else if ((incoming == 3) && settings.enablePvtClient) + { + systemPrint("Enter the PVT client port number to use (0 to 65535): "); + int portNumber = getNumber(); // Returns EXIT, TIMEOUT, or long + if ((portNumber != INPUT_RESPONSE_GETNUMBER_EXIT) && + (portNumber != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + { + if ((portNumber < 0) || (portNumber > 65535)) + systemPrintln("Error: Port number out of range"); + else + { + settings.pvtClientPort = portNumber; // Recorded to NVM and file at main menu exit + } + } + } + + //------------------------------ + // Get the PVT server parameters + //------------------------------ + + else if (incoming == 4) + // Toggle WiFi NEMA server + settings.enablePvtServer ^= 1; + + else if (incoming == 5) + { + systemPrint("Enter the TCP port to use (0 to 65535): "); + int portNumber = getNumber(); // Returns EXIT, TIMEOUT, or long + if ((portNumber != INPUT_RESPONSE_GETNUMBER_EXIT) && (portNumber != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + { + if (portNumber < 0 || portNumber > 65535) + systemPrintln("Error: TCP Port out of range"); + else + settings.pvtServerPort = portNumber; // Recorded to NVM and file at main menu exit + } + } + + //------------------------------ + // Get the PVT UDP server parameters + //------------------------------ + + else if (incoming == 6) + // Toggle WiFi UDP NEMA server + settings.enablePvtUdpServer ^= 1; + + else if (incoming == 7 && settings.enablePvtUdpServer) + { + systemPrint("Enter the UDP port to use (0 to 65535): "); + int portNumber = getNumber(); // Returns EXIT, TIMEOUT, or long + if ((portNumber != INPUT_RESPONSE_GETNUMBER_EXIT) && (portNumber != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + { + if (portNumber < 0 || portNumber > 65535) + systemPrintln("Error: UDP Port out of range"); + else + settings.pvtUdpServerPort = portNumber; // Recorded to NVM and file at main menu exit + } + } + + //------------------------------ + // Get the network layer parameters + //------------------------------ + + else if ((incoming == 'd') && HAS_ETHERNET) + { + // Toggle the network type + settings.defaultNetworkType += 1; + if (settings.defaultNetworkType > NETWORK_TYPE_USE_DEFAULT) + settings.defaultNetworkType = 0; + } + else if ((incoming == 'f') && HAS_ETHERNET) + { + // Toggle failover support + settings.enableNetworkFailover ^= 1; + } + + //------------------------------ + // Handle exit and invalid input + //------------------------------ + + else if (incoming == 'x') + break; + else if (incoming == INPUT_RESPONSE_GETCHARACTERNUMBER_EMPTY) + break; + else if (incoming == INPUT_RESPONSE_GETCHARACTERNUMBER_TIMEOUT) + break; + else + printUnknown(incoming); + } +} + +//---------------------------------------- +// Allocate a network client +//---------------------------------------- +NetworkClient * networkClient(uint8_t user, bool useSSL) +{ + NetworkClient * client; + int type; + + type = networkGetType(user); +#if defined(COMPILE_ETHERNET) + if (type == NETWORK_TYPE_ETHERNET) + { + if (useSSL) + client = new NetworkEthernetSslClient(); + else + client = new NetworkEthernetClient; + } + else +#endif // COMPILE_ETHERNET + { +#if defined(COMPILE_WIFI) + if (useSSL) + client = new NetworkWiFiSslClient(); + else + client = new NetworkWiFiClient(); +#else // COMPILE_WIFI + client = nullptr; +#endif // COMPILE_WIFI + } + return client; +} + +//---------------------------------------- +// Display the IP address +//---------------------------------------- +void networkDisplayIpAddress(uint8_t networkType) +{ + char ipAddress[32]; + NETWORK_DATA * network; + + network = &networkData; +// network = networkGet(networkType, false); + if (network && (networkType == network->type) && (network->state >= NETWORK_STATE_IN_USE)) + { + if (settings.debugNetworkLayer || settings.printNetworkStatus) + { + strcpy(ipAddress, networkGetIpAddress(networkType).toString().c_str()); + if (network->type == NETWORK_TYPE_WIFI) + systemPrintf("%s IP address: %s, RSSI: %d\r\n", networkName[network->type], ipAddress, wifiGetRssi()); + else + systemPrintf("%s IP address: %s\r\n", networkName[network->type], ipAddress); + + // The address was just displayed + networkLastIpAddressDisplayMillis[networkType] = millis(); + } + } +} + +//---------------------------------------- +// Get the network type +//---------------------------------------- +NETWORK_DATA * networkGet(uint8_t networkType, bool updateRequestedNetwork) +{ + NETWORK_DATA * network; + uint8_t selectedNetworkType; + + do + { + network = &networkData; + + // Translate the default network type + selectedNetworkType = networkTranslateNetworkType(networkType, false); + if (settings.debugNetworkLayer && (networkType != selectedNetworkType)) + systemPrintf("networkGet, networkType: %s --> %s\r\n", + networkName[networkType], networkName[selectedNetworkType]); + networkType = selectedNetworkType; + + // Select the network + if (updateRequestedNetwork && ((network->state < NETWORK_STATE_CONNECTING) + || (network->state == NETWORK_STATE_WAIT_NO_USERS))) + { + selectedNetworkType = network->requestedNetwork; + if ((selectedNetworkType == NETWORK_TYPE_ACTIVE) + && (networkType <= NETWORK_TYPE_USE_DEFAULT)) + selectedNetworkType = networkType; + else if ((selectedNetworkType == NETWORK_TYPE_USE_DEFAULT) + && (networkType < NETWORK_TYPE_MAX)) + selectedNetworkType = networkType; + if (settings.debugNetworkLayer && (network->requestedNetwork != selectedNetworkType)) + systemPrintf("networkUserOpen, network->requestedNetwork: %s --> %s\r\n", + networkName[network->requestedNetwork], + networkName[selectedNetworkType]); + network->requestedNetwork = selectedNetworkType; + + // Update the network type before connecting to the network + if (network->state < NETWORK_STATE_CONNECTING) + { + if (settings.debugNetworkLayer && (network->type != selectedNetworkType)) + systemPrintf("networkUserOpen, network->type: %s --> %s\r\n", + networkName[network->type], networkName[selectedNetworkType]); + network->type = selectedNetworkType; + } + } + + // Determine if the network was found + if ((network->state == NETWORK_STATE_OFF) + || (networkType == network->type) + || (networkType == NETWORK_TYPE_ACTIVE)) + break; + + // Network not available if another device is using it + network = nullptr; + } while (0); + + // Return the network + return network; +} + +//---------------------------------------- +// Get the IP address +//---------------------------------------- +IPAddress networkGetIpAddress(uint8_t networkType) +{ + if (networkType == NETWORK_TYPE_ETHERNET) + return ethernetGetIpAddress(); + if (networkType == NETWORK_TYPE_WIFI) + return wifiGetIpAddress(); + return IPAddress((uint32_t)0); +} + +//---------------------------------------- +// Get the network type +//---------------------------------------- +uint8_t networkGetType(uint8_t user) +{ + NETWORK_DATA * network; + + network = networkGetUserNetwork(user); + if (network) + return network->type; + return NETWORK_TYPE_WIFI; +} + +//---------------------------------------- +// Get the network with this active user +//---------------------------------------- +NETWORK_DATA * networkGetUserNetwork(NETWORK_USER user) +{ + NETWORK_DATA * network; + int networkType; + NETWORK_USER userMask; + + // Locate the network for this user + userMask = 1 << user; + for (networkType = 0; networkType < NETWORK_TYPE_MAX; networkType++) + { + network = networkGet(networkType, false); + if (network && ((network->activeUsers & userMask) + || (network->userOpens & userMask))) + return network; + } + + return nullptr; //User is not active on any network +} + +//---------------------------------------- +// Perform the common network initialization +//---------------------------------------- +void networkInitialize(NETWORK_DATA * network) +{ + uint8_t requestedNetwork; + NETWORK_USER userOpens; + + // Save the values + requestedNetwork = network->requestedNetwork; + if (settings.debugNetworkLayer && (requestedNetwork != network->type)) + systemPrintf("networkInitialize, network->type: %s --> %s\r\n", + networkName[network->type], networkName[requestedNetwork]); + userOpens = network->userOpens; + + // Initialize the network + memset(network, 0, sizeof(*network)); + + // Complete the initialization + network->requestedNetwork = requestedNetwork; + network->type = requestedNetwork; + network->userOpens = userOpens; + network->timeout = 2; + network->timerStart = millis(); +} + +//---------------------------------------- +// Determine if the network is connected to the media +//---------------------------------------- +bool networkIsConnected(NETWORK_DATA * network) +{ + // Determine the network is connected + if (network && (network->state == NETWORK_STATE_IN_USE)) + return networkIsMediaConnected(network); + return false; +} + +//---------------------------------------- +// Determine if the network is connected to the media +//---------------------------------------- +bool networkIsTypeConnected(uint8_t networkType) +{ + // Determine the network is connected + return networkIsConnected(networkGet(networkType, false)); +} + +//---------------------------------------- +// Determine if the network is connected to the media +//---------------------------------------- +bool networkIsMediaConnected(NETWORK_DATA * network) +{ + bool isConnected; + + // Determine if the network is connected to the media + switch (network->type) + { + default: + isConnected = false; + break; + + case NETWORK_TYPE_ETHERNET: + isConnected = (online.ethernetStatus == ETH_CONNECTED); + break; + + case NETWORK_TYPE_WIFI: + isConnected = wifiIsConnected(); + break; + } + + // Verify that the network has an IP address + if (isConnected && (networkGetIpAddress(network->type) != 0)) + { + networkPeriodicallyDisplayIpAddress(); + return true; + } + + // The network is not ready for use + return false; +} + +//---------------------------------------- +// Determine if the network is off +//---------------------------------------- +bool networkIsOff(uint8_t networkType) +{ + NETWORK_DATA * network; + + network = networkGet(networkType, false); + return network && (network->state == NETWORK_STATE_OFF); +} + +//---------------------------------------- +// Determine if the network is shutting down +//---------------------------------------- +bool networkIsShuttingDown(uint8_t user) +{ + NETWORK_DATA * network; + + network = networkGetUserNetwork(user); + return network && (network->state == NETWORK_STATE_WAIT_NO_USERS); +} + +//---------------------------------------- +// Periodically display the IP address +//---------------------------------------- +void networkPeriodicallyDisplayIpAddress() +{ + if (PERIODIC_DISPLAY(PD_ETHERNET_IP_ADDRESS)) + { + PERIODIC_CLEAR(PD_ETHERNET_IP_ADDRESS); + networkDisplayIpAddress(NETWORK_TYPE_ETHERNET); + } + if (PERIODIC_DISPLAY(PD_WIFI_IP_ADDRESS)) + { + PERIODIC_CLEAR(PD_WIFI_IP_ADDRESS); + networkDisplayIpAddress(NETWORK_TYPE_WIFI); + } +} + +//---------------------------------------- +// Print the name associated with a network type +//---------------------------------------- +void networkPrintName(uint8_t networkType) +{ + if (networkType > NETWORK_TYPE_USE_DEFAULT) + systemPrint("Unknown"); + else if (HAS_ETHERNET) + systemPrint(networkName[networkType]); + else + systemPrint(networkName[NETWORK_TYPE_WIFI]); +} + +//---------------------------------------- +// Attempt to restart the network +//---------------------------------------- +void networkRestart(uint8_t user) +{ + // Determine if restart is possible + networkRestartNetwork(networkGetUserNetwork(user)); +} + +void networkRestartNetwork(NETWORK_DATA * network) +{ + // Determine if restart is possible + if (network && (!network->shutdown)) + + // The network was not stopped, allow it to be restarted + network->restart = true; +} + +//---------------------------------------- +// Retry the network connection +//---------------------------------------- +void networkRetry(NETWORK_DATA * network, uint8_t previousNetworkType) +{ + uint8_t networkType; + int seconds; + uint8_t users; + + // Determine the delay multiplier + network->connectionAttempt += 1; + if (network->connectionAttempt > NETWORK_MAX_RETRIES) + // Use the maximum delay and continue retrying the network connection + network->connectionAttempt = NETWORK_MAX_RETRIES; + + // Compute the delay between retries + network->timeout = NETWORK_DELAY_BEFORE_RETRY << (network->connectionAttempt - 1); + + // Determine if failover is possible + if (HAS_ETHERNET && (wifiNetworkCount() > 0) && settings.enableNetworkFailover + && (network->requestedNetwork >= NETWORK_TYPE_MAX)) + { + // Get the next failover network + networkType = networkFailover[previousNetworkType]; + if (settings.debugNetworkLayer || settings.printNetworkStatus) + { + systemPrint("Network failover: "); + systemPrint(networkName[previousNetworkType]); + systemPrint("-->"); + systemPrintln(networkName[networkType]); + } + + // Initialize the network + network->requestedNetwork = networkType; + } + + // Display the delay + if ((settings.debugNetworkLayer || settings.printNetworkStatus) && network->timeout) + { + seconds = network->timeout / 1000; + if (seconds < 120) + systemPrintf("Network delaying %d seconds before connection\r\n", seconds); + else + systemPrintf("Network delaying %d minutes before connection\r\n", seconds / 60); + } + + // Start the delay between network connection retries + network->timerStart = millis(); + networkSetState(network, NETWORK_STATE_DELAY); +} + +//---------------------------------------- +// Set the next state for the network state machine +//---------------------------------------- +void networkSetState(NETWORK_DATA * network, byte newState) +{ + // Display the state transition + if (settings.debugNetworkLayer) + { + // Display the network state + systemPrint("Network State: "); + if (newState != network->state) + systemPrintf("%s --> ", networkState[network->state]); + else + systemPrint("*"); + + // Display the new network state + if (newState >= networkStateEntries) + { + systemPrintf("Unknown network layer state (%d)\r\n", newState); + reportFatalError("Unknown network layer state"); + } + else + systemPrintf("%s\r\n", networkState[newState]); + } + + // Validate the network state + if (newState >= NETWORK_STATE_MAX) + reportFatalError("Invalid network state"); + + // Set the new state + network->state = newState; +} + +//---------------------------------------- +// Shutdown access to the network hardware +//---------------------------------------- +void networkShutdownHardware(NETWORK_DATA * network) +{ + // Stop WiFi if necessary + if (network->type == NETWORK_TYPE_WIFI) + { + if (settings.debugNetworkLayer) + systemPrintln("Network stopping WiFi"); + wifiShutdown(); + } +} + +//---------------------------------------- +// Start the network +//---------------------------------------- +void networkStart(uint8_t networkType) +{ + NETWORK_DATA * network; + + // Validate the network type + if (networkType >= NETWORK_TYPE_LAST) + reportFatalError("Attempting to start an invalid network type!"); + + // Start the network layer + if (settings.debugNetworkLayer) + systemPrintf("Network request to start %s\r\n", networkName[networkType]); + + // Get the network data + network = networkGet(networkType, false); + if (!network) + reportFatalError("Network failed to get the network structure"); + else + { + // Verify that the network is stopped + if (network->state != NETWORK_STATE_OFF) + systemPrintf("Network already started!\r\n"); + else + { + // Start the network layer + if (settings.debugNetworkLayer) + systemPrintf("Network layer starting %s\r\n", networkName[network->type]); + + // Initialize the network + networkInitialize(network); + + // Delay before starting the network + networkSetState(network, NETWORK_STATE_DELAY); + } + } +} + +//---------------------------------------- +// Stop the network +//---------------------------------------- +void networkStop(uint8_t networkType) +{ + NETWORK_DATA * network; + bool restart; + int serverIndex; + bool shutdown; + int user; + + do + { + // Validate the network type + if (networkType >= NETWORK_TYPE_MAX) + reportFatalError("Attempt to shutdown invalid network type!"); + + // Shutdown all networks + if (networkType >= NETWORK_TYPE_MAX) + { + for (networkType = 0; networkType < NETWORK_TYPE_MAX; networkType++) + networkStop(networkType); + break; + } + + // Determine if the network is running + network = networkGet(networkType, false); + if ((!network) || (networkType != network->type)) + // The network is already stopped + break; + + // Save the shutdown status + shutdown = network->shutdown; + + // Stop the clients of this network + for (user = 0; user < (sizeof(network->activeUsers) * 8); user++) + { + // Determine if the network client is active + if (network->activeUsers & (1 << user)) + { + // When user calls networkUserClose don't recursively + // call networkStop + network->shutdown = false; + + // Stop the network client + switch(user) + { + default: + if ((user >= NETWORK_USER_NTRIP_SERVER) + && (user < (NETWORK_USER_NTRIP_SERVER + NTRIP_SERVER_MAX))) + { + serverIndex = user - NETWORK_USER_NTRIP_SERVER; + if (settings.debugNetworkLayer) + systemPrintln("Network layer stopping NTRIP server"); + ntripServerStop(serverIndex, true); // Note: was ntripServerRestart(serverIndex); + } + break; + + case NETWORK_USER_NTP_SERVER: + if (settings.debugNetworkLayer) + systemPrintln("Network layer stopping NTP server"); + ntpServerStop(); + break; + + case NETWORK_USER_NTRIP_CLIENT: + if (settings.debugNetworkLayer) + systemPrintln("Network layer stopping NTRIP client"); + ntripClientStop(true); // Note: was ntripClientRestart(); + break; + + case NETWORK_USER_OTA_FIRMWARE_UPDATE: + if (settings.debugNetworkLayer) + systemPrintln("Network layer stopping OTA firmware update"); + otaStop(); + break; + + case NETWORK_USER_PVT_CLIENT: + if (settings.debugNetworkLayer) + systemPrintln("Network layer stopping PVT client"); + pvtClientStop(); + break; + + case NETWORK_USER_PVT_SERVER: + if (settings.debugNetworkLayer) + systemPrintln("Network layer stopping PVT server"); + pvtServerStop(); + break; + + case NETWORK_USER_PVT_UDP_SERVER: + if (settings.debugNetworkLayer) + systemPrintln("Network layer stopping PVT UDP server"); + pvtUdpServerStop(); + break; + } + } + } + + // Restore the shutdown status + network->shutdown = shutdown; + + // Determine if the network can be stopped now + if ((network->state < NETWORK_STATE_IN_USE) || (!network->activeUsers)) + { + // Remember the current network info + restart = network->restart; + + // Stop the network layer + networkShutdownHardware(network); + if (settings.debugNetworkLayer) + systemPrintln("Network layer stopping"); + + // Initialize the network layer + // requestedNetwork is set below upon entry to NETWORK_STATE_CONNECTING and + // indicates the network desired upon restart + // userOpens may be non-zero and indicates users waiting for network restart + // activeUsers is already zero + // Don't initialize connectionAttempt + // networkRetry or networkStart initializes: + // connectionAttempt + // timeout + // timerStart + network->restart = false; + network->shutdown = false; + networkSetState(network, NETWORK_STATE_OFF); + + // Restart the network if requested + if (restart) + { + if (settings.debugNetworkLayer) + systemPrintln("Network layer restarting"); + networkRetry(network, network->type); + } + + // Update the network type + network->type = network->requestedNetwork; + break; + } + + // Start shutting down the network and wait for users to detect the shutdown + if (network->state != NETWORK_STATE_WAIT_NO_USERS) + { + network->shutdown = true; + if (settings.debugNetworkLayer) + systemPrintln("Network layer waiting for users to stop!"); + networkSetState(network, NETWORK_STATE_WAIT_NO_USERS); + } + } while (0); +} + +//---------------------------------------- +// Translate the network type +//---------------------------------------- +uint8_t networkTranslateNetworkType(uint8_t networkType, bool translateActive) +{ + uint8_t newNetworkType; +//systemPrintf("networkTranslateNetworkType(%s, %s) called\r\n", networkName[networkType], translateActive ? "true" : "false"); + + // Get the default network type + newNetworkType = networkType; + if ((newNetworkType == NETWORK_TYPE_USE_DEFAULT) + || (translateActive && (newNetworkType == NETWORK_TYPE_ACTIVE))) + newNetworkType = settings.defaultNetworkType; + + // Translate the default network type + if (newNetworkType == NETWORK_TYPE_USE_DEFAULT) + { + if (HAS_ETHERNET) + newNetworkType = NETWORK_TYPE_ETHERNET; + else + newNetworkType = NETWORK_TYPE_WIFI; + } + return newNetworkType; +} + +//---------------------------------------- +// Update the network device state +//---------------------------------------- +void networkTypeUpdate(uint8_t networkType) +{ + char errorMsg[64]; + NETWORK_DATA * network; + + // Update the physical network connections + switch (networkType) + { + case NETWORK_TYPE_WIFI: + wifiUpdate(); + break; + + case NETWORK_TYPE_ETHERNET: + ethernetUpdate(); + break; + } + + // Locate an active network + network = &networkData; + if ((network->type != networkType) && (network->state >= NETWORK_STATE_CONNECTING)) + return; + + // Process the network state + DMW_if + networkSetState(network, network->state); + switch (network->state) + { + default: + sprintf(errorMsg, "Invalid network state (%d) during update!", network->state); + reportFatalError(errorMsg); + break; + + // Leave the network off + case NETWORK_STATE_OFF: + break; + + // Pause before making the network connection + case NETWORK_STATE_DELAY: + // Determine if the network is shutting down + if (network->shutdown) + { + NETWORK_STOP(network->type); + } + + // Delay before starting the network + else if ((millis() - network->timerStart) >= network->timeout) + { + // Start the network + network->type = networkTranslateNetworkType(network->requestedNetwork, true); + if (settings.debugNetworkLayer && (network->type != network->requestedNetwork)) + systemPrintf("networkTypeUpdate, network->type: %s --> %s\r\n", + networkName[network->requestedNetwork], networkName[network->type]); + if (settings.debugNetworkLayer) + systemPrintf("networkTypeUpdate, network->requestedNetwork: %s --> %s\r\n", + networkName[network->requestedNetwork], + networkName[NETWORK_TYPE_ACTIVE]); + network->requestedNetwork = NETWORK_TYPE_ACTIVE; + if (settings.debugNetworkLayer) + systemPrintf("Network starting %s\r\n", networkName[network->type]); + if (network->type == NETWORK_TYPE_WIFI) + wifiStart(); + network->timerStart = millis(); + network->timeout = NETWORK_CONNECTION_TIMEOUT; + networkSetState(network, NETWORK_STATE_CONNECTING); + } + break; + + // Wait for the network connection + case NETWORK_STATE_CONNECTING: + // Determine if the network is shutting down + if (network->shutdown) + { + NETWORK_STOP(network->type); + } + + // Determine if the connection failed + else if ((millis() - network->timerStart) >= network->timeout) + { + // Retry the network connection + if (settings.debugNetworkLayer) + systemPrintf("Network: %s connection timed out\r\n", networkName[network->type]); + networkRestartNetwork(network); + NETWORK_STOP(network->type); + } + + // Determine if the RTK host is connected to the network + else if (networkIsMediaConnected(network)) + { + if (settings.debugNetworkLayer) + systemPrintf("Network connected to %s\r\n", networkName[network->type]); + network->timerStart = millis(); + network->timeout = NETWORK_MAX_IDLE_TIME; + network->activeUsers = network->userOpens; + networkSetState(network, NETWORK_STATE_IN_USE); + networkDisplayIpAddress(network->type); + } + break; + + // There is at least one active user of the network connection + case NETWORK_STATE_IN_USE: + // Determine if the network is shutting down + if (network->shutdown) + { + NETWORK_STOP(network->type); + } + + // Verify that the RTK device is still connected to the network + else if (!networkIsMediaConnected(network)) + { + // The network failed + if (settings.debugNetworkLayer) + systemPrintf("Network: %s connection failed!\r\n", networkName[network->type]); + networkRestartNetwork(network); + NETWORK_STOP(network->type); + } + + // Check for the idle timeout + else if ((millis() - network->timerStart) >= network->timeout) + { + // Determine if the network is in use + network->timerStart = millis(); + if (network->activeUsers) + { + // Network in use, reduce future connection delays + network->connectionAttempt = 0; + + // Set the next time that network idle should be checked + network->timeout = NETWORK_MAX_IDLE_TIME; + } + + // Without users there is no need for the network. + else + { + if (settings.debugNetworkLayer) + systemPrintf("Network shutting down %s, no users\r\n", networkName[network->type]); + NETWORK_STOP(network->type); + } + } + break; + + case NETWORK_STATE_WAIT_NO_USERS: + // Stop the network when all the users are removed + if (!network->activeUsers) + NETWORK_STOP(network->type); + break; + } + + // Periodically display the state + if (PERIODIC_DISPLAY(PD_NETWORK_STATE)) + networkSetState(network, network->state); +} + +//---------------------------------------- +// Maintain the network connections +//---------------------------------------- +void networkUpdate() +{ + uint8_t networkType; + + // Update the network layer + for (networkType = 0; networkType < NETWORK_TYPE_MAX; networkType++) + networkTypeUpdate(networkType); + if (PERIODIC_DISPLAY(PD_NETWORK_STATE)) + PERIODIC_CLEAR(PD_NETWORK_STATE); + + // Update the network services + ntpServerUpdate(); // Process any received NTP requests + ntripClientUpdate(); // Check the NTRIP client connection and move data NTRIP --> ZED + ntripServerUpdate(); // Check the NTRIP server connection and move data ZED --> NTRIP + otaClientUpdate(); // Perform automatic over-the-air firmware updates + pvtClientUpdate(); // Turn on the PVT client as needed + pvtServerUpdate(); // Turn on the PVT server as needed + pvtUdpServerUpdate(); // Turn on the PVT UDP server as needed + + // Display the IP addresses + networkPeriodicallyDisplayIpAddress(); +} + +//---------------------------------------- +// Stop a user of the network +//---------------------------------------- +void networkUserClose(uint8_t user) +{ + char errorText[64]; + NETWORK_DATA * network; + NETWORK_USER userMask; + + // Verify the user number + if (user >= NETWORK_USER_MAX) + { + sprintf(errorText, "Invalid network user (%d)", user); + reportFatalError(errorText); + } + else + { + // Verify that this user is running + userMask = 1 << user; + network = networkGetUserNetwork(user); + if (network && (network->userOpens & userMask)) + { + // Done with this network user + network->activeUsers &= ~userMask; + network->userOpens &= ~userMask; + if (settings.debugNetworkLayer) + { + systemPrintf("Network stopping user %s", networkUser[user]); + if (network->state != NETWORK_STATE_OFF) + systemPrintf(" on %s", networkName[network->type]); + systemPrintln(); + } + + // Shutdown the network if requested + if (network->shutdown && (!network->activeUsers)) + NETWORK_STOP(network->type); + } + + // The network user is not running + else + { + sprintf(errorText, "Network user %s is already idle", networkUser[user]); + reportFatalError(errorText); + } + } +} + +//---------------------------------------- +// Determine if the network user is connected to the media +//---------------------------------------- +bool networkUserConnected(NETWORK_USER user) +{ + NETWORK_DATA * network; + + network = networkGetUserNetwork(user); + if (network && (network->state != NETWORK_STATE_WAIT_NO_USERS)) + return networkIsConnected(network); + return false; +} + +//---------------------------------------- +// Start a user of the network +//---------------------------------------- +bool networkUserOpen(uint8_t user, uint8_t networkType) +{ + char errorText[64]; + NETWORK_DATA * network; + NETWORK_USER userMask; + + do + { + // Verify the user number + if (user >= NETWORK_USER_MAX) + { + sprintf(errorText, "Invalid network user (%d)", user); + reportFatalError(errorText); + break; + } + + // Determine if the network is available + network = networkGet(networkType, true); + if (network && (network->state != NETWORK_STATE_WAIT_NO_USERS) && (!network->shutdown)) + { + userMask = 1 << user; + if ((network->activeUsers >> user) & 1) + { + reportFatalError("Network user already started!"); + break; + } + + // Start the user + if (settings.debugNetworkLayer) + systemPrintf("Network starting user %s on %s\r\n", networkUser[user], networkName[network->type]); + switch (network->state) + { + case NETWORK_STATE_OFF: + networkStart(network->type); + break; + + case NETWORK_STATE_IN_USE: + network->activeUsers |= userMask; + break; + } + network->userOpens |= userMask; + return true; + } + } while (0); + + // The network user was not started + return false; +} + +// Verify the network layer tables +void networkVerifyTables() +{ + // Verify the table lengths + if (networkFailoverEntries != NETWORK_TYPE_MAX) + reportFatalError("Fix networkFailover table to match NetworkTypes"); + if (networkNameEntries != NETWORK_TYPE_LAST) + reportFatalError("Fix networkName table to match NetworkTypes"); + if (networkStateEntries != NETWORK_STATE_MAX) + reportFatalError("Fix networkState table to match NetworkStates"); + if (networkUserEntries != NETWORK_USER_MAX) + reportFatalError("Fix networkUser table to match NetworkUsers"); +} + +#endif // COMPILE_NETWORK diff --git a/Firmware/RTK_Surveyor/NetworkClient.h b/Firmware/RTK_Surveyor/NetworkClient.h new file mode 100644 index 000000000..0ceba6fa4 --- /dev/null +++ b/Firmware/RTK_Surveyor/NetworkClient.h @@ -0,0 +1,345 @@ +#ifndef __NETWORK_CLIENT_H__ +#define __NETWORK_CLIENT_H__ + +extern uint8_t networkGetType(uint8_t user); + +class NetworkClient : public Client +{ + protected: + + Client * _client; // Ethernet or WiFi client + bool _friendClass; + uint8_t _networkType; + + public: + + //------------------------------ + // Create the network client + //------------------------------ + NetworkClient(Client * client, uint8_t networkType) + { + _friendClass = true; + _networkType = networkType; + _client = client; + } + + NetworkClient(uint8_t user) + { + _friendClass = false; + _networkType = networkGetType(user); +#if defined(COMPILE_ETHERNET) + if (_networkType == NETWORK_TYPE_ETHERNET) + _client = new EthernetClient; + else +#endif // COMPILE_ETHERNET +#if defined(COMPILE_WIFI) + _client = new WiFiClient; +#else // COMPILE_WIFI + _client = nullptr; +#endif // COMPILE_WIFI + }; + + //------------------------------ + // Delete the network client + //------------------------------ + ~NetworkClient() + { + if (_client) + { + _client->stop(); + if (!_friendClass) + delete _client; + _client = nullptr; + } + }; + + //------------------------------ + // Determine if receive data is available + //------------------------------ + + int available() + { + if (_client) + return _client->available(); + return 0; + } + + //------------------------------ + // Determine if the network client was allocated + //------------------------------ + + operator bool() + { + return _client; + } + + //------------------------------ + // Connect to the server + //------------------------------ + + int connect(IPAddress ip, uint16_t port) + { + if (_client) + return _client->connect(ip, port); + return 0; + } + + int connect(const char *host, uint16_t port) + { + if (_client) + return _client->connect(host, port); + return 0; + } + + //------------------------------ + // Determine if the client is connected to the server + //------------------------------ + + uint8_t connected() + { + if (_client) + return _client->connected(); + return 0; + } + + //------------------------------ + // Finish transmitting all the data to the server + //------------------------------ + + void flush() + { + if (_client) + _client->flush(); + } + + //------------------------------ + // Look at the next received byte in the data stream + //------------------------------ + + int peek() + { + if (_client) + return _client->peek(); + return -1; + } + + //------------------------------ + // Display the network client status + //------------------------------ + size_t print(const char *printMe) + { + if (_client) + return _client->print(printMe); + return 0; + }; + + //------------------------------ + // Receive a data byte from the server + //------------------------------ + + int read() + { + if (_client) + return _client->read(); + return 0; + } + + //------------------------------ + // Receive a buffer of data from the server + //------------------------------ + + int read(uint8_t *buf, size_t size) + { + if (_client) + return _client->read(buf, size); + return 0; + } + + //------------------------------ + // Get the remote IP address + //------------------------------ + + IPAddress remoteIP() + { +#if defined(COMPILE_ETHERNET) + if (_networkType == NETWORK_TYPE_ETHERNET) + return ((EthernetClient *)_client)->remoteIP(); +#endif // COMPILE_ETHERNET +#if defined(COMPILE_WIFI) + if (_networkType == NETWORK_TYPE_WIFI) + return ((WiFiClient *)_client)->remoteIP(); +#endif // COMPILE_WIFI + return IPAddress((uint32_t)0); + } + + //------------------------------ + // Get the remote port number + //------------------------------ + + uint16_t remotePort() + { +#if defined(COMPILE_ETHERNET) + if (_networkType == NETWORK_TYPE_ETHERNET) + return ((EthernetClient *)_client)->remotePort(); +#endif // COMPILE_ETHERNET +#if defined(COMPILE_WIFI) + if (_networkType == NETWORK_TYPE_WIFI) + return ((WiFiClient *)_client)->remotePort(); +#endif // COMPILE_WIFI + return 0; + } + + //------------------------------ + // Stop the network client + //------------------------------ + + void stop() + { + if (_client) + _client->stop(); + } + + //------------------------------ + // Send a data byte to the server + //------------------------------ + + size_t write(uint8_t b) + { + if (_client) + return _client->write(b); + return 0; + } + + //------------------------------ + // Send a buffer of data to the server + //------------------------------ + + size_t write(const uint8_t *buf, size_t size) + { + if (_client) + return _client->write(buf, size); + return 0; + } + + protected: + + //------------------------------ + // Return the IP address + //------------------------------ + + uint8_t* rawIPAddress(IPAddress& addr) + { + return Client::rawIPAddress(addr); + } + + //------------------------------ + // Declare the friend classes + //------------------------------ + + friend class NetworkEthernetClient; + friend class NetworkEthernetSslClient; + friend class NetworkWiFiClient; + friend class NetworkWiFiSslClient; +}; + +#ifdef COMPILE_ETHERNET +class NetworkEthernetClient : public NetworkClient +{ + private: + + EthernetClient _ethernetClient; + + public: + + NetworkEthernetClient() : + NetworkClient(&_ethernetClient, NETWORK_TYPE_ETHERNET) + { + } + + NetworkEthernetClient(EthernetClient& client) : + _ethernetClient{client}, + NetworkClient(&_ethernetClient, NETWORK_TYPE_ETHERNET) + { + } + + ~NetworkEthernetClient() + { + this->~NetworkClient(); + } +}; + +class NetworkEthernetSslClient : public NetworkClient +{ + protected: + + EthernetClient _ethernetClient; + SSLClientESP32 _sslClient; + + public: + + NetworkEthernetSslClient() : + _sslClient(), + NetworkClient(&_sslClient, NETWORK_TYPE_ETHERNET) + { + _sslClient.setClient(&_ethernetClient); + _sslClient.setCACertBundle(x509CertificateBundle); + } + + ~NetworkEthernetSslClient() + { + this->~NetworkClient(); + } +}; +#endif // COMPILE_ETHERNET + +#ifdef COMPILE_WIFI +class NetworkWiFiClient : public NetworkClient +{ + protected: + + WiFiClient _client; + + public: + + NetworkWiFiClient() : + NetworkClient(&_client, NETWORK_TYPE_WIFI) + { + } + + NetworkWiFiClient(WiFiClient& client) : + _client{client}, + NetworkClient(&_client, NETWORK_TYPE_WIFI) + { + } + + ~NetworkWiFiClient() + { + this->~NetworkClient(); + } +}; + +class NetworkWiFiSslClient : public NetworkClient +{ + protected: + + WiFiClient _wifiClient; + SSLClientESP32 _sslClient; + + public: + + NetworkWiFiSslClient() : + _sslClient(), + NetworkClient(&_sslClient, NETWORK_TYPE_WIFI) + { + _sslClient.setClient(&_wifiClient); + _sslClient.setCACertBundle(x509CertificateBundle); + } + + ~NetworkWiFiSslClient() + { + this->~NetworkClient(); + } +}; +#endif // COMPILE_WIFI + +#endif // __NETWORK_CLIENT_H__ diff --git a/Firmware/RTK_Surveyor/NetworkUDP.h b/Firmware/RTK_Surveyor/NetworkUDP.h new file mode 100644 index 000000000..3548d0c75 --- /dev/null +++ b/Firmware/RTK_Surveyor/NetworkUDP.h @@ -0,0 +1,306 @@ +#ifndef __NETWORK_UDP_H__ +#define __NETWORK_UDP_H__ + +extern uint8_t networkGetType(uint8_t user); + +class NetworkUDP : public UDP +{ + protected: + + UDP * _udp; // Ethernet or WiFi udp + bool _friendClass; + uint8_t _networkType; + + public: + + //------------------------------ + // Create the network client + //------------------------------ + NetworkUDP(UDP * udp, uint8_t networkType) + { + _friendClass = true; + _networkType = networkType; + _udp = udp; + } + + NetworkUDP(uint8_t user) + { + _friendClass = false; + _networkType = networkGetType(user); +#if defined(COMPILE_ETHERNET) + if (_networkType == NETWORK_TYPE_ETHERNET) + _udp = new EthernetUDP; + else +#endif // COMPILE_ETHERNET +#if defined(COMPILE_WIFI) + _udp = new WiFiUDP; +#else // COMPILE_WIFI + _udp = nullptr; +#endif // COMPILE_WIFI + }; + + //------------------------------ + // Delete the network client + //------------------------------ + ~NetworkUDP() + { + if (_udp) + { + _udp->stop(); + if (!_friendClass) + delete _udp; + _udp = nullptr; + } + }; + + + //------------------------------ + // Determine if the network client was allocated + //------------------------------ + + operator bool() + { + return _udp; + } + + //------------------------------ + // Start to the server + //------------------------------ + + uint8_t begin(uint16_t port) + { + if (_udp) + return _udp->begin(port); + return 0; + } + + //------------------------------ + // Stop the network client + //------------------------------ + + void stop() + { + if (_udp) + _udp->stop(); + } + + //------------------------------ + // Determine if receive data is available + //------------------------------ + + int available() + { + if (_udp) + return _udp->available(); + return 0; + } + + //------------------------------ + // Read available data + //------------------------------ + + int read() + { + if (_udp) + return _udp->read(); + return 0; + } + + //------------------------------ + // Read available data + //------------------------------ + + int read(unsigned char* buf, size_t length) + { + if (_udp) + return _udp->read(buf, length); + return 0; + } + + //------------------------------ + // Read available data + //------------------------------ + + int read(char* buf, size_t length) + { + if (_udp) + return _udp->read(buf, length); + return 0; + } + + //------------------------------ + // Look at the next received byte in the data stream + //------------------------------ + + int peek() + { + if (_udp) + return _udp->peek(); + return 0; + } + + //------------------------------ + // Finish transmitting all the data + //------------------------------ + + void flush() + { + if (_udp) + _udp->flush(); + } + + //------------------------------ + // Send a data byte to the server + //------------------------------ + + size_t write(uint8_t b) + { + if (_udp) + return _udp->write(b); + return 0; + } + + //------------------------------ + // Send a buffer of data to the server + //------------------------------ + + size_t write(const uint8_t *buf, size_t size) + { + if (_udp) + return _udp->write(buf, size); + return 0; + } + + //------------------------------ + // Begin a UDP packet + //------------------------------ + + int beginPacket(IPAddress ip, uint16_t port) + { + if (_udp) + return _udp->beginPacket(ip, port); + return 0; + } + + //------------------------------ + // Begin a UDP packet + //------------------------------ + + int beginPacket(const char* host, uint16_t port) + { + if (_udp) + return _udp->beginPacket(host, port); + return 0; + } + + //------------------------------ + // Parse UDP packet + //------------------------------ + + int parsePacket() + { + if (_udp) + return _udp->parsePacket(); + return 0; + } + + //------------------------------ + // End the current UDP packet + //------------------------------ + + int endPacket() + { + if (_udp) + return _udp->endPacket(); + return 0; + } + + //------------------------------ + // Get the remote IP address + //------------------------------ + + IPAddress remoteIP() + { +#if defined(COMPILE_ETHERNET) + if (_networkType == NETWORK_TYPE_ETHERNET) + return ((EthernetUDP *)_udp)->remoteIP(); +#endif // COMPILE_ETHERNET +#if defined(COMPILE_WIFI) + if (_networkType == NETWORK_TYPE_WIFI) + return ((WiFiUDP *)_udp)->remoteIP(); +#endif // COMPILE_WIFI + return IPAddress((uint32_t)0); + } + + //------------------------------ + // Get the remote port number + //------------------------------ + + uint16_t remotePort() + { +#if defined(COMPILE_ETHERNET) + if (_networkType == NETWORK_TYPE_ETHERNET) + return ((EthernetUDP *)_udp)->remotePort(); +#endif // COMPILE_ETHERNET +#if defined(COMPILE_WIFI) + if (_networkType == NETWORK_TYPE_WIFI) + return ((WiFiUDP *)_udp)->remotePort(); +#endif // COMPILE_WIFI + return 0; + } + + protected: + + //------------------------------ + // Declare the friend classes + //------------------------------ + + friend class NetworkEthernetUdp; + friend class NetworkWiFiUdp; +}; + +#ifdef COMPILE_ETHERNET +class NetworkEthernetUdp : public NetworkUDP +{ + private: + + EthernetUDP _udp; + + public: + + NetworkEthernetUdp(EthernetUDP& udp) : + _udp{udp}, + NetworkUDP(&_udp, NETWORK_TYPE_ETHERNET) + { + } + + ~NetworkEthernetUdp() + { + this->~NetworkUDP(); + } +}; +#endif // COMPILE_ETHERNET + +#ifdef COMPILE_WIFI +class NetworkWiFiUdp : public NetworkUDP +{ + private: + + WiFiUDP _udp; + + public: + + NetworkWiFiUdp(WiFiUDP& udp) : + _udp{udp}, + NetworkUDP(&_udp, NETWORK_TYPE_WIFI) + { + } + + ~NetworkWiFiUdp() + { + this->~NetworkUDP(); + } +}; +#endif // COMPILE_WIFI + +#endif // __NETWORK_CLIENT_H__ diff --git a/Firmware/RTK_Surveyor/NtripClient.ino b/Firmware/RTK_Surveyor/NtripClient.ino new file mode 100644 index 000000000..7b01b4a6c --- /dev/null +++ b/Firmware/RTK_Surveyor/NtripClient.ino @@ -0,0 +1,893 @@ +/*------------------------------------------------------------------------------ +NtripClient.ino + + The NTRIP client sits on top of the network layer and receives correction data + from an NTRIP caster that is provided to the ZED (GNSS radio). + + Satellite ... Satellite + | | | + | | | + | V | + | RTK | + '------> Base <------' + Station + | + | NTRIP Server sends correction data + V + NTRIP Caster + | + | NTRIP Client receives correction data + V + Bluetooth RTK Network: NMEA Client + .---------------- Rover --------------------------. + | | | + | NMEA | Network: NEMA Server | NMEA + | position | NEMA position data | position + | data V | data + | Computer or | + '------------> Cell Phone <-----------------------' + for display + + NTRIP Client Testing: + + Using Ethernet on RTK Reference Station: + + 1. Network failure - Disconnect Ethernet cable at RTK Reference Station, + expecting retry NTRIP client connection after network restarts + + Using WiFi on RTK Express or RTK Reference Station: + + 1. Internet link failure - Disconnect Ethernet cable between Ethernet + switch and the firewall to simulate an internet failure, expecting + retry NTRIP client connection after delay + + 2. Internet outage - Disconnect Ethernet cable between Ethernet + switch and the firewall to simulate an internet failure, expecting + retries to exceed the connection limit causing the NTRIP client to + shutdown after about 2 hours. Restarting the NTRIP client may be + done by rebooting the RTK or by using the configuration menus to + turn off and on the NTRIP client. + + Test Setup: + + RTK Express RTK Reference Station + ^ ^ ^ + WiFi | WiFi | | Ethernet cable + v v v + WiFi Access Point <-----------> Ethernet Switch + Ethernet ^ + Cable | Ethernet cable + v + Internet Firewall + ^ + | Ethernet cable + v + Modem + ^ + | + v + Internet + ^ + | + v + NTRIP Caster + + Possible NTRIP Casters + + * https://emlid.com/ntrip-caster/ + * http://rtk2go.com/ + * private SNIP NTRIP caster +------------------------------------------------------------------------------*/ + +/*=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + NTRIP Client States: + NTRIP_CLIENT_OFF: Network off or using NTRIP server + NTRIP_CLIENT_ON: WIFI_STATE_START state + NTRIP_CLIENT_NETWORK_STARTED: Connecting to the network + NTRIP_CLIENT_NETWORK_CONNECTED: Connected to the network + NTRIP_CLIENT_CONNECTING: Attempting a connection to the NTRIP caster + NTRIP_CLIENT_WAIT_RESPONSE: Wait for a response from the NTRIP caster + NTRIP_CLIENT_CONNECTED: Connected to the NTRIP caster + + NTRIP_CLIENT_OFF + | ^ + ntripClientStart | | ntripClientShutdown() + v | + NTRIP_CLIENT_ON <--------------. + | | + | | ntripClientRestart() + v Fail | + NTRIP_CLIENT_NETWORK_STARTED ------->+ + | ^ + | | + v | + NTRIP_CLIENT_NETWORK_CONNECTED | + | | + | | + v Fail | + NTRIP_CLIENT_CONNECTING ---------->+ + | ^ + | | + v Fail | + NTRIP_CLIENT_WAIT_RESPONSE -------->+ + | ^ + | | + v Fail | + NTRIP_CLIENT_CONNECTED -----------' + + =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=*/ + +#if COMPILE_NETWORK + +//---------------------------------------- +// Constants +//---------------------------------------- + +// Size of the credentials buffer in bytes +static const int CREDENTIALS_BUFFER_SIZE = 512; + +// Give up connecting after this number of attempts +// Connection attempts are throttled to increase the time between attempts +// 30 attempts with 15 second increases will take almost two hours +static const int MAX_NTRIP_CLIENT_CONNECTION_ATTEMPTS = 30; + +// NTRIP caster response timeout +static const uint32_t NTRIP_CLIENT_RESPONSE_TIMEOUT = 10 * 1000; // Milliseconds + +// NTRIP client receive data timeout +static const uint32_t NTRIP_CLIENT_RECEIVE_DATA_TIMEOUT = 30 * 1000; // Milliseconds + +// Most incoming data is around 500 bytes but may be larger +static const int RTCM_DATA_SIZE = 512 * 4; + +// NTRIP client server request buffer size +static const int SERVER_BUFFER_SIZE = CREDENTIALS_BUFFER_SIZE + 3; + +static const int NTRIPCLIENT_MS_BETWEEN_GGA = 5000; // 5s between transmission of GGA messages, if enabled + +// NTRIP client connection delay before resetting the connect accempt counter +static const int NTRIP_CLIENT_CONNECTION_TIME = 5 * 60 * 1000; + +// Define the NTRIP client states +enum NTRIPClientState +{ + NTRIP_CLIENT_OFF = 0, // Using Bluetooth or NTRIP server + NTRIP_CLIENT_ON, // WIFI_STATE_START state + NTRIP_CLIENT_NETWORK_STARTED, // Connecting to WiFi access point or Ethernet + NTRIP_CLIENT_NETWORK_CONNECTED, // Connected to an access point or Ethernet + NTRIP_CLIENT_CONNECTING, // Attempting a connection to the NTRIP caster + NTRIP_CLIENT_WAIT_RESPONSE, // Wait for a response from the NTRIP caster + NTRIP_CLIENT_CONNECTED, // Connected to the NTRIP caster + // Insert new states here + NTRIP_CLIENT_STATE_MAX // Last entry in the state list +}; + +const char * const ntripClientStateName[] = +{ + "NTRIP_CLIENT_OFF", + "NTRIP_CLIENT_ON", + "NTRIP_CLIENT_NETWORK_STARTED", + "NTRIP_CLIENT_NETWORK_CONNECTED", + "NTRIP_CLIENT_CONNECTING", + "NTRIP_CLIENT_WAIT_RESPONSE", + "NTRIP_CLIENT_CONNECTED" +}; + +const int ntripClientStateNameEntries = sizeof(ntripClientStateName) / sizeof(ntripClientStateName[0]); + +const RtkMode_t ntripClientMode = RTK_MODE_ROVER + | RTK_MODE_BASE_SURVEY_IN; + +//---------------------------------------- +// Locals +//---------------------------------------- + +// The network connection to the NTRIP caster to obtain RTCM data. +static NetworkClient *ntripClient; +static volatile uint8_t ntripClientState = NTRIP_CLIENT_OFF; + +// Throttle the time between connection attempts +// ms - Max of 4,294,967,295 or 4.3M seconds or 71,000 minutes or 1193 hours or 49 days between attempts +static int ntripClientConnectionAttempts; // Count the number of connection attempts between restarts +static uint32_t ntripClientConnectionAttemptTimeout; +static int ntripClientConnectionAttemptsTotal; // Count the number of connection attempts absolutely + +// NTRIP client timer usage: +// * Reconnection delay +// * Measure the connection response time +// * Receive NTRIP data timeout +static uint32_t ntripClientTimer; +static uint32_t ntripClientStartTime; // For calculating uptime + +// Throttle GGA transmission to Caster to 1 report every 5 seconds +unsigned long lastGGAPush; + +//---------------------------------------- +// NTRIP Client Routines +//---------------------------------------- + +bool ntripClientConnect() +{ + if (!ntripClient) + return false; + + // Remove any http:// or https:// prefix from host name + char hostname[51]; + strncpy(hostname, settings.ntripClient_CasterHost, + sizeof(hostname) - 1); // strtok modifies string to be parsed so we create a copy + char *token = strtok(hostname, "//"); + if (token != nullptr) + { + token = strtok(nullptr, "//"); // Advance to data after // + if (token != nullptr) + strcpy(settings.ntripClient_CasterHost, token); + } + + if (settings.debugNtripClientState) + systemPrintf("NTRIP Client connecting to %s:%d\r\n", + settings.ntripClient_CasterHost, + settings.ntripClient_CasterPort); + + int connectResponse = ntripClient->connect(settings.ntripClient_CasterHost, settings.ntripClient_CasterPort); + + if (connectResponse < 1) + { + if (settings.debugNtripClientState) + systemPrintf("NTRIP Client connection to NTRIP caster %s:%d failed\r\n", + settings.ntripClient_CasterHost, + settings.ntripClient_CasterPort); + return false; + } + + // Set up the server request (GET) + char serverRequest[SERVER_BUFFER_SIZE]; + int length; + snprintf(serverRequest, SERVER_BUFFER_SIZE, "GET /%s HTTP/1.0\r\nUser-Agent: NTRIP SparkFun_RTK_%s_", + settings.ntripClient_MountPoint, platformPrefix); + length = strlen(serverRequest); + getFirmwareVersion(&serverRequest[length], SERVER_BUFFER_SIZE - 2 - length, false); + length = strlen(serverRequest); + serverRequest[length++] = '\r'; + serverRequest[length++] = '\n'; + serverRequest[length++] = 0; + + // Set up the credentials + char credentials[CREDENTIALS_BUFFER_SIZE]; + if (strlen(settings.ntripClient_CasterUser) == 0) + { + strncpy(credentials, "Accept: */*\r\nConnection: close\r\n", sizeof(credentials) - 1); + } + else + { + // Pass base64 encoded user:pw + char userCredentials[sizeof(settings.ntripClient_CasterUser) + sizeof(settings.ntripClient_CasterUserPW) + + 1]; // The ':' takes up a spot + snprintf(userCredentials, sizeof(userCredentials), "%s:%s", settings.ntripClient_CasterUser, + settings.ntripClient_CasterUserPW); + + if (settings.debugNtripClientState) + { + systemPrint("NTRIP Client sending credentials: "); + systemPrintln(userCredentials); + } + + // Encode with ESP32 built-in library + base64 b; + String strEncodedCredentials = b.encode(userCredentials); + char encodedCredentials[strEncodedCredentials.length() + 1]; + strEncodedCredentials.toCharArray(encodedCredentials, + sizeof(encodedCredentials)); // Convert String to char array + + snprintf(credentials, sizeof(credentials), "Authorization: Basic %s\r\n", encodedCredentials); + } + + // Add the encoded credentials to the server request + strncat(serverRequest, credentials, SERVER_BUFFER_SIZE - 1); + strncat(serverRequest, "\r\n", SERVER_BUFFER_SIZE - 1); + + if (settings.debugNtripClientState) + { + systemPrint("NTRIP Client serverRequest size: "); + systemPrint(strlen(serverRequest)); + systemPrint(" of "); + systemPrint(sizeof(serverRequest)); + systemPrintln(" bytes available"); + systemPrintln("NTRIP Client sending server request: "); + systemPrintln(serverRequest); + } + + // Send the server request + ntripClient->write((const uint8_t *)serverRequest, strlen(serverRequest)); + ntripClientTimer = millis(); + return true; +} + +// Determine if another connection is possible or if the limit has been reached +bool ntripClientConnectLimitReached() +{ + int seconds; + + // Retry the connection a few times + bool limitReached = (ntripClientConnectionAttempts >= MAX_NTRIP_CLIENT_CONNECTION_ATTEMPTS); + + // Attempt to restart the network if possible + if (settings.enableNtripClient && (!limitReached)) + networkRestart(NETWORK_USER_NTRIP_CLIENT); + + // Restart the NTRIP client + ntripClientStop(limitReached || (!settings.enableNtripClient)); + + ntripClientConnectionAttempts++; + ntripClientConnectionAttemptsTotal++; + if (settings.debugNtripClientState) + ntripClientPrintStatus(); + + if (limitReached == false) + { + if (ntripClientConnectionAttempts == 1) + ntripClientConnectionAttemptTimeout = 15 * 1000L; // Wait 15s + else if (ntripClientConnectionAttempts == 2) + ntripClientConnectionAttemptTimeout = 30 * 1000L; // Wait 30s + else if (ntripClientConnectionAttempts == 3) + ntripClientConnectionAttemptTimeout = 1 * 60 * 1000L; // Wait 1 minute + else if (ntripClientConnectionAttempts == 4) + ntripClientConnectionAttemptTimeout = 2 * 60 * 1000L; // Wait 2 minutes + else + ntripClientConnectionAttemptTimeout = + (ntripClientConnectionAttempts - 4) * 5 * 60 * 1000L; // Wait 5, 10, 15, etc minutes between attempts + + // Display the delay before starting the NTRIP client + if (settings.debugNtripClientState && ntripClientConnectionAttemptTimeout) + { + seconds = ntripClientConnectionAttemptTimeout / 1000; + if (seconds < 120) + systemPrintf("NTRIP Client trying again in %d seconds.\r\n", seconds); + else + systemPrintf("NTRIP Client trying again in %d minutes.\r\n", seconds / 60); + } + } + else + // No more connection attempts, switching to Bluetooth + systemPrintln("NTRIP Client connection attempts exceeded!"); + return limitReached; +} + +// Print the NTRIP client state summary +void ntripClientPrintStateSummary() +{ + switch (ntripClientState) + { + default: + systemPrintf("Unknown: %d", ntripClientState); + break; + case NTRIP_CLIENT_OFF: + systemPrint("Disconnected"); + break; + case NTRIP_CLIENT_ON: + case NTRIP_CLIENT_NETWORK_STARTED: + case NTRIP_CLIENT_NETWORK_CONNECTED: + case NTRIP_CLIENT_CONNECTING: + case NTRIP_CLIENT_WAIT_RESPONSE: + systemPrint("Connecting"); + break; + case NTRIP_CLIENT_CONNECTED: + systemPrint("Connected"); + break; + } +} + +// Print the NTRIP Client status +void ntripClientPrintStatus() +{ + uint32_t days; + byte hours; + uint64_t milliseconds; + byte minutes; + byte seconds; + + // Display NTRIP Client status and uptime + if (settings.enableNtripClient && + ((systemState >= STATE_ROVER_NOT_STARTED) && (systemState <= STATE_ROVER_RTK_FIX))) + { + systemPrint("NTRIP Client "); + ntripClientPrintStateSummary(); + systemPrintf(" - %s/%s:%d", settings.ntripClient_CasterHost, + settings.ntripClient_MountPoint, settings.ntripClient_CasterPort); + + if (ntripClientState == NTRIP_CLIENT_CONNECTED) + // Use ntripClientTimer since it gets reset after each successful data + // receiption from the NTRIP caster + milliseconds = ntripClientTimer - ntripClientStartTime; + else + { + milliseconds = ntripClientStartTime; + systemPrint(" Last"); + } + + // Display the uptime + days = milliseconds / MILLISECONDS_IN_A_DAY; + milliseconds %= MILLISECONDS_IN_A_DAY; + + hours = milliseconds / MILLISECONDS_IN_AN_HOUR; + milliseconds %= MILLISECONDS_IN_AN_HOUR; + + minutes = milliseconds / MILLISECONDS_IN_A_MINUTE; + milliseconds %= MILLISECONDS_IN_A_MINUTE; + + seconds = milliseconds / MILLISECONDS_IN_A_SECOND; + milliseconds %= MILLISECONDS_IN_A_SECOND; + + systemPrint(" Uptime: "); + systemPrintf("%d %02d:%02d:%02d.%03lld (Reconnects: %d)\r\n", + days, hours, minutes, seconds, milliseconds, ntripClientConnectionAttemptsTotal); + } +} + +// Determine if NTRIP client data is available +int ntripClientReceiveDataAvailable() +{ + return ntripClient->available(); +} + +// Read the response from the NTRIP client +void ntripClientResponse(char *response, size_t maxLength) +{ + char *responseEnd; + + // Make sure that we can zero terminate the response + responseEnd = &response[maxLength - 1]; + + // Read bytes from the caster and store them + while ((response < responseEnd) && (ntripClientReceiveDataAvailable() > 0)) + { + *response++ = ntripClient->read(); + } + + // Zero terminate the response + *response = '\0'; +} + +// Restart the NTRIP client +void ntripClientRestart() +{ + // Save the previous uptime value + if (ntripClientState == NTRIP_CLIENT_CONNECTED) + ntripClientStartTime = ntripClientTimer - ntripClientStartTime; + ntripClientConnectLimitReached(); +} + +// Update the state of the NTRIP client state machine +// PERIODIC_DISPLAY(PD_NTRIP_CLIENT_STATE) is handled by ntripClientUpdate +void ntripClientSetState(uint8_t newState) +{ + if (settings.debugNtripClientState) + { + if (ntripClientState == newState) + systemPrint("NTRIP client: *"); + else + systemPrintf("NTRIP client: %s --> ", ntripClientStateName[ntripClientState]); + } + ntripClientState = newState; + if (settings.debugNtripClientState) + { + if (ntripClientState >= NTRIP_CLIENT_STATE_MAX) + { + systemPrintf("Unknown client state %d\r\n", ntripClientState); + reportFatalError("Unknown NTRIP Client state"); + } + else + systemPrintln(ntripClientStateName[ntripClientState]); + } +} + +// Shutdown the NTRIP client +void ntripClientShutdown() +{ + ntripClientStop(true); +} + +// Start the NTRIP client +void ntripClientStart() +{ + // Display the heap state + reportHeapNow(settings.debugNtripClientState); + + // Start the NTRIP client + systemPrintln("NTRIP Client start"); + ntripClientStop(false); +} + +// Shutdown or restart the NTRIP client +void ntripClientStop(bool shutdown) +{ + if (ntripClient) + { + // Break the NTRIP client connection if necessary + if (ntripClient->connected()) + ntripClient->stop(); + + // Free the NTRIP client resources + delete ntripClient; + ntripClient = nullptr; + reportHeapNow(settings.debugNtripClientState); + } + + // Increase timeouts if we started the network + if (ntripClientState > NTRIP_CLIENT_ON) + { + // Mark the Client stop so that we don't immediately attempt re-connect to Caster + ntripClientTimer = millis(); + + // Done with the network + if (networkGetUserNetwork(NETWORK_USER_NTRIP_CLIENT)) + networkUserClose(NETWORK_USER_NTRIP_CLIENT); + } + + // Return the Main Talker ID to "GN". + if (online.gnss) + { + theGNSS.setVal8(UBLOX_CFG_NMEA_MAINTALKERID, 3); // Return talker ID to GNGGA after NTRIP Client set to GPGGA + theGNSS.setNMEAGPGGAcallbackPtr(nullptr); // Remove callback + } + + // Determine the next NTRIP client state + online.ntripClient = false; + netIncomingRTCM = false; + if (shutdown) + { + ntripClientSetState(NTRIP_CLIENT_OFF); + settings.enableNtripClient = false; + ntripClientConnectionAttempts = 0; + ntripClientConnectionAttemptTimeout = 0; + } + else + ntripClientSetState(NTRIP_CLIENT_ON); +} + +// Check for the arrival of any correction data. Push it to the GNSS. +// Stop task if the connection has dropped or if we receive no data for maxTimeBeforeHangup_ms +void ntripClientUpdate() +{ + // Shutdown the NTRIP client when the mode or setting changes + DMW_st(ntripClientSetState, ntripClientState); + if (NEQ_RTK_MODE(ntripClientMode) || (!settings.enableNtripClient)) + { + if (ntripClientState > NTRIP_CLIENT_OFF) + { + ntripClientStop(true); + ntripClientConnectionAttempts = 0; + ntripClientConnectionAttemptTimeout = 0; + ntripClientSetState(NTRIP_CLIENT_OFF); + } + } + + // Enable the network and the NTRIP client if requested + switch (ntripClientState) + { + case NTRIP_CLIENT_OFF: + if (EQ_RTK_MODE(ntripClientMode) && settings.enableNtripClient) + ntripClientStart(); + break; + + // Start the network + case NTRIP_CLIENT_ON: + if (networkUserOpen(NETWORK_USER_NTRIP_CLIENT, NETWORK_TYPE_ACTIVE)) + ntripClientSetState(NTRIP_CLIENT_NETWORK_STARTED); + break; + + // Wait for a network media connection + case NTRIP_CLIENT_NETWORK_STARTED: + // Determine if the network has failed + if (networkIsShuttingDown(NETWORK_USER_NTRIP_CLIENT)) + // Failed to connect to to the network, attempt to restart the network + ntripClientStop(true); // Note: was ntripClientRestart(); + + // Determine if the network is connected to the media + else if (networkUserConnected(NETWORK_USER_NTRIP_CLIENT)) + { + // Allocate the ntripClient structure + ntripClient = new NetworkClient(NETWORK_USER_NTRIP_CLIENT); + if (!ntripClient) + { + // Failed to allocate the ntripClient structure + systemPrintln("ERROR: Failed to allocate the ntripClient structure!"); + ntripClientShutdown(); + } + else + { + reportHeapNow(settings.debugNtripClientState); + + // The network is available for the NTRIP client + ntripClientSetState(NTRIP_CLIENT_NETWORK_CONNECTED); + } + } + break; + + case NTRIP_CLIENT_NETWORK_CONNECTED: + // Determine if the network has failed + if (networkIsShuttingDown(NETWORK_USER_NTRIP_CLIENT)) + // Failed to connect to to the network, attempt to restart the network + ntripClientStop(true); // Note: was ntripClientRestart(); + + // If GGA transmission is enabled, wait for GNSS lock before connecting to NTRIP Caster + // If GGA transmission is not enabled, start connecting to NTRIP Caster + else if ((!settings.ntripClient_TransmitGGA) || (fixType >= 3) && (fixType <= 5)) + { + // Delay before opening the NTRIP client connection + if ((millis() - ntripClientTimer) >= ntripClientConnectionAttemptTimeout) + { + // Open connection to NTRIP caster service + if (!ntripClientConnect()) + { + // Assume service not available + if (ntripClientConnectLimitReached()) // Updates ntripClientConnectionAttemptTimeout + systemPrintln("NTRIP caster failed to connect. Do you have your caster address and port correct?"); + } + else + { + // Socket opened to NTRIP system + if (settings.debugNtripClientState) + systemPrintf("NTRIP Client waiting for response from %s:%d\r\n", + settings.ntripClient_CasterHost, + settings.ntripClient_CasterPort); + ntripClientSetState(NTRIP_CLIENT_WAIT_RESPONSE); + } + } + } + break; + + case NTRIP_CLIENT_WAIT_RESPONSE: + // Determine if the network has failed + if (networkIsShuttingDown(NETWORK_USER_NTRIP_CLIENT)) + // Failed to connect to to the network, attempt to restart the network + ntripClientStop(true); // Note: was ntripClientRestart(); + + // Check for no response from the caster service + else if (ntripClientReceiveDataAvailable() < + strlen("ICY 200 OK")) // Wait until at least a few bytes have arrived + { + // Check for response timeout + if (millis() - ntripClientTimer > NTRIP_CLIENT_RESPONSE_TIMEOUT) + { + // NTRIP web service did not respond + if (ntripClientConnectLimitReached()) // Updates ntripClientConnectionAttemptTimeout + systemPrintln("NTRIP Caster failed to respond. Do you have your caster address and port correct?"); + } + } + else + { + // Caster web service responded + char response[512]; + ntripClientResponse(&response[0], sizeof(response)); + + if (settings.debugNtripClientState) + systemPrintf("Caster Response: %s\r\n", response); + else + log_d("Caster Response: %s", response); + + // Look for various responses + if (strstr(response, "200") != nullptr) //'200' found + { + // We got a response, now check it for possible errors + if (strcasestr(response, "banned") != nullptr) + { + systemPrintf("NTRIP Client connected to caster but caster responded with banned error: %s\r\n", + response); + + ntripClientConnectLimitReached(); //Re-attempted after a period of time. Shuts down NTRIP Client if limit reached. + } + else if (strcasestr(response, "sandbox") != nullptr) + { + systemPrintf("NTRIP Client connected to caster but caster responded with sandbox error: %s\r\n", + response); + + ntripClientConnectLimitReached(); //Re-attempted after a period of time. Shuts down NTRIP Client if limit reached. + } + else if (strcasestr(response, "SOURCETABLE") != nullptr) + { + systemPrintf("Caster may not have mountpoint %s. Caster responded with problem: %s\r\n", + settings.ntripClient_MountPoint, response); + + // Stop NTRIP client operations + ntripClientShutdown(); + } + else + { + // We successfully connected + // Timeout receiving NTRIP data, retry the NTRIP client connection + if (online.rtc && online.gnss) + { + int hours; + int minutes; + int seconds; + + seconds = rtc.getLocalEpoch() % SECONDS_IN_A_DAY; + hours = seconds / SECONDS_IN_AN_HOUR; + seconds -= hours * SECONDS_IN_AN_HOUR; + minutes = seconds / SECONDS_IN_A_MINUTE; + seconds -= minutes * SECONDS_IN_A_MINUTE; + systemPrintf("NTRIP Client connected to %s:%d at %d:%02d:%02d\r\n", + settings.ntripClient_CasterHost, settings.ntripClient_CasterPort, hours, minutes, + seconds); + } + else + systemPrintf("NTRIP Client connected to %s:%d\r\n", settings.ntripClient_CasterHost, + settings.ntripClient_CasterPort); + + // Connection is now open, start the NTRIP receive data timer + ntripClientTimer = millis(); + + if (settings.ntripClient_TransmitGGA == true) + { + // Set the Main Talker ID to "GP". The NMEA GGA messages will be GPGGA instead of GNGGA + theGNSS.setVal8(UBLOX_CFG_NMEA_MAINTALKERID, 1); + theGNSS.setNMEAGPGGAcallbackPtr(&pushGPGGA); // Set up the callback for GPGGA + + float measurementFrequency = (1000.0 / settings.measurementRate) / settings.navigationRate; + if (measurementFrequency < 0.2) + measurementFrequency = 0.2; // 0.2Hz * 5 = 1 measurement every 5 seconds + log_d("Adjusting GGA setting to %f", measurementFrequency); + theGNSS.setVal8( + UBLOX_CFG_MSGOUT_NMEA_ID_GGA_I2C, + measurementFrequency); // Enable GGA over I2C. Tell the module to output GGA every second + + lastGGAPush = + millis() - NTRIPCLIENT_MS_BETWEEN_GGA; // Force immediate transmission of GGA message + } + + // We don't use a task because we use I2C hardware (and don't have a semphore). + online.ntripClient = true; + ntripClientStartTime = millis(); + ntripClientSetState(NTRIP_CLIENT_CONNECTED); + } + } + else if (strstr(response, "401") != nullptr) + { + // Look for '401 Unauthorized' + systemPrintf( + "NTRIP Caster responded with unauthorized error: %s. Are you sure your caster credentials are correct?\r\n", + response); + + // Stop NTRIP client operations + ntripClientShutdown(); + } + // Other errors returned by the caster + else + { + systemPrintf("NTRIP Client connected but caster responded with problem: %s\r\n", response); + + // Stop NTRIP client operations + ntripClientShutdown(); + } + } + break; + + case NTRIP_CLIENT_CONNECTED: + // Determine if the network has failed + if (networkIsShuttingDown(NETWORK_USER_NTRIP_CLIENT)) + // Failed to connect to to the network, attempt to restart the network + ntripClientStop(true); // Note: was ntripClientRestart(); + + // Check for a broken connection + else if (!ntripClient->connected()) + { + // Broken connection, retry the NTRIP client connection + systemPrintln("NTRIP Client connection to caster was broken"); + ntripClientRestart(); + } + else + { + // Handle other types of NTRIP connection failures to prevent + // hammering the NTRIP caster with rapid connection attempts. + // A fast reconnect is reasonable after a long NTRIP caster + // connection. However increasing backoff delays should be + // added when the NTRIP caster fails after a short connection + // interval. + if (((millis() - ntripClientStartTime) > NTRIP_CLIENT_CONNECTION_TIME) + && (ntripClientConnectionAttempts || ntripClientConnectionAttemptTimeout)) + { + // After a long connection period, reset the attempt counter + ntripClientConnectionAttempts = 0; + ntripClientConnectionAttemptTimeout = 0; + if (settings.debugNtripClientState) + systemPrintln("NTRIP Client resetting connection attempt counter and timeout"); + } + + // Check for timeout receiving NTRIP data + if (ntripClientReceiveDataAvailable() == 0) + { + // Don't fail during retransmission attempts + if ((millis() - ntripClientTimer) > NTRIP_CLIENT_RECEIVE_DATA_TIMEOUT) + { + // Timeout receiving NTRIP data, retry the NTRIP client connection + if (online.rtc && online.gnss) + { + int hours; + int minutes; + int seconds; + + seconds = rtc.getLocalEpoch() % SECONDS_IN_A_DAY; + hours = seconds / SECONDS_IN_AN_HOUR; + seconds -= hours * SECONDS_IN_AN_HOUR; + minutes = seconds / SECONDS_IN_A_MINUTE; + seconds -= minutes * SECONDS_IN_A_MINUTE; + systemPrintf("NTRIP Client timeout receiving data at %d:%02d:%02d\r\n", + hours, minutes, seconds); + } + else + systemPrintln("NTRIP Client timeout receiving data"); + ntripClientRestart(); + } + } + else + { + // Receive data from the NTRIP Caster + uint8_t rtcmData[RTCM_DATA_SIZE]; + size_t rtcmCount = 0; + + // Collect any available RTCM data + if (ntripClientReceiveDataAvailable() > 0) + { + rtcmCount = ntripClient->read(rtcmData, sizeof(rtcmData)); + if (rtcmCount) + { + // Restart the NTRIP receive data timer + ntripClientTimer = millis(); + + // Record the arrival of RTCM from the WiFi connection. This resets the RTCM timeout used on the L-Band. + rtcmLastPacketReceived = millis(); + + // Push RTCM to GNSS module over I2C / SPI + theGNSS.pushRawData(rtcmData, rtcmCount); + netIncomingRTCM = true; + + if ((settings.debugNtripClientRtcm || PERIODIC_DISPLAY(PD_NTRIP_CLIENT_DATA)) + && (!inMainMenu)) + { + PERIODIC_CLEAR(PD_NTRIP_CLIENT_DATA); + systemPrintf("NTRIP Client received %d RTCM bytes, pushed to ZED\r\n", rtcmCount); + } + } + } + } + } + break; + } + + // Periodically display the NTRIP client state + if (PERIODIC_DISPLAY(PD_NTRIP_CLIENT_STATE)) + { + systemPrintf("NTRIP client state: %s\r\n", ntripClientStateName[ntripClientState]); + PERIODIC_CLEAR(PD_NTRIP_CLIENT_STATE); + } +} + +// Verify the NTRIP client tables +void ntripClientValidateTables() +{ + if (ntripClientStateNameEntries != NTRIP_CLIENT_STATE_MAX) + reportFatalError("Fix ntripClientStateNameEntries to match NTRIPClientState"); +} + +void pushGPGGA(NMEA_GGA_data_t *nmeaData) +{ + // Provide the caster with our current position as needed + if (ntripClient->connected() && settings.ntripClient_TransmitGGA == true) + { + if (millis() - lastGGAPush > NTRIPCLIENT_MS_BETWEEN_GGA) + { + lastGGAPush = millis(); + + if (settings.debugNtripClientRtcm || PERIODIC_DISPLAY(PD_NTRIP_CLIENT_GGA)) + { + PERIODIC_CLEAR(PD_NTRIP_CLIENT_GGA); + systemPrintf("NTRIP Client pushing GGA to server: %s", (const char *)nmeaData->nmea); + } + + // Push our current GGA sentence to caster + ntripClient->print((const char *)nmeaData->nmea); + } + } +} + +#endif // COMPILE_NETWORK diff --git a/Firmware/RTK_Surveyor/NtripServer.ino b/Firmware/RTK_Surveyor/NtripServer.ino new file mode 100644 index 000000000..32a133d2a --- /dev/null +++ b/Firmware/RTK_Surveyor/NtripServer.ino @@ -0,0 +1,856 @@ +/*------------------------------------------------------------------------------ +NtripServer.ino + + The NTRIP server sits on top of the network layer and sends correction data + from the ZED (GNSS radio) to an NTRIP caster. + + Satellite ... Satellite + | | | + | | | + | V | + | RTK | + '------> Base <------' + Station + | + | NTRIP Server sends correction data + V + NTRIP Caster + | + | NTRIP Client receives correction data + V + Bluetooth RTK Network: NMEA Client + .---------------- Rover --------------------------. + | | | + | NMEA | Network: NEMA Server | NMEA + | position | NEMA position data | position + | data V | data + | Computer or | + '------------> Cell Phone <-----------------------' + for display + + NTRIP Server Testing: + + Using Ethernet on RTK Reference Station: + + 1. Network failure - Disconnect Ethernet cable at RTK Reference Station, + expecting retry NTRIP server connection after network restarts + + Using WiFi on RTK Express or RTK Reference Station: + + 1. Internet link failure - Disconnect Ethernet cable between Ethernet + switch and the firewall to simulate an internet failure, expecting + retry NTRIP server connection after delay + + 2. Internet outage - Disconnect Ethernet cable between Ethernet + switch and the firewall to simulate an internet failure, expecting + retries to exceed the connection limit causing the NTRIP server to + shutdown after about 25 hours and 18 minutes. Restarting the NTRIP + server may be done by rebooting the RTK or by using the configuration + menus to turn off and on the NTRIP server. + + Test Setup: + + RTK Reference Station + ^ ^ + WiFi | | Ethernet cable + v v + WiFi Access Point <-----------> Ethernet Switch + Ethernet ^ + Cable | Ethernet cable + v + Internet Firewall + ^ + | Ethernet cable + v + Modem + ^ + | + v + Internet + ^ + | + v + NTRIP Caster + + Possible NTRIP Casters + + * https://emlid.com/ntrip-caster/ + * http://rtk2go.com/ + * private SNIP NTRIP caster +------------------------------------------------------------------------------*/ + +/*=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + NTRIP Server States: + NTRIP_SERVER_OFF: Network off or using NTRIP Client + NTRIP_SERVER_ON: WIFI_STATE_START state + NTRIP_SERVER_NETWORK_STARTED: Connecting to the network + NTRIP_SERVER_NETWORK_CONNECTED: Connected to the network + NTRIP_SERVER_WAIT_GNSS_DATA: Waiting for correction data from GNSS + NTRIP_SERVER_CONNECTING: Attempting a connection to the NTRIP caster + NTRIP_SERVER_AUTHORIZATION: Validate the credentials + NTRIP_SERVER_CASTING: Sending correction data to the NTRIP caster + + NTRIP_SERVER_OFF + | ^ + ntripServerStart | | ntripServerShutdown() + v | + .---------> NTRIP_SERVER_ON <-------------------. + | | | + | | | ntripServerRestart() + | v Fail | + | NTRIP_SERVER_NETWORK_STARTED ------------->+ + | | ^ + | | | + | v Fail | + | NTRIP_SERVER_NETWORK_CONNECTED ----------->+ + | | ^ + | | Network | + | v Fail | + | NTRIP_SERVER_WAIT_GNSS_DATA -------------->+ + | | ^ + | | Discard Data Network | + | v Fail | + | NTRIP_SERVER_CONNECTING ---------------->+ + | | ^ + | | Discard Data Network | + | v Fail | + | NTRIP_SERVER_AUTHORIZATION -------------->+ + | | ^ + | | Discard Data Network | + | v Fail | + | NTRIP_SERVER_CASTING -----------------' + | | + | | Data timeout + | | + | | Close Server connection + '------------------' + + =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=*/ + +#if COMPILE_NETWORK + +//---------------------------------------- +// Constants +//---------------------------------------- + +// Give up connecting after this number of attempts +// Connection attempts are throttled by increasing the time between attempts by +// 5 minutes. The NTRIP server stops retrying after 25 hours and 18 minutes +static const int MAX_NTRIP_SERVER_CONNECTION_ATTEMPTS = 28; + +// NTRIP server connection delay before resetting the connect accempt counter +static const int NTRIP_SERVER_CONNECTION_TIME = 5 * 60 * 1000; + +// Define the NTRIP server states +enum NTRIPServerState +{ + NTRIP_SERVER_OFF = 0, // Using Bluetooth or NTRIP client + NTRIP_SERVER_ON, // WIFI_STATE_START state + NTRIP_SERVER_NETWORK_STARTED, // Connecting to WiFi access point + NTRIP_SERVER_NETWORK_CONNECTED, // WiFi connected to an access point + NTRIP_SERVER_WAIT_GNSS_DATA, // Waiting for correction data from GNSS + NTRIP_SERVER_CONNECTING, // Attempting a connection to the NTRIP caster + NTRIP_SERVER_AUTHORIZATION, // Validate the credentials + NTRIP_SERVER_CASTING, // Sending correction data to the NTRIP caster + // Insert new states here + NTRIP_SERVER_STATE_MAX // Last entry in the state list +}; + +const char * const ntripServerStateName[] = +{ + "NTRIP_SERVER_OFF", + "NTRIP_SERVER_ON", + "NTRIP_SERVER_NETWORK_STARTED", + "NTRIP_SERVER_NETWORK_CONNECTED", + "NTRIP_SERVER_WAIT_GNSS_DATA", + "NTRIP_SERVER_CONNECTING", + "NTRIP_SERVER_AUTHORIZATION", + "NTRIP_SERVER_CASTING" +}; + +const int ntripServerStateNameEntries = sizeof(ntripServerStateName) / sizeof(ntripServerStateName[0]); + +const RtkMode_t ntripServerMode = RTK_MODE_BASE_FIXED; + +//---------------------------------------- +// Locals +//---------------------------------------- + +// NTRIP Servers +static NTRIP_SERVER_DATA ntripServerArray[NTRIP_SERVER_MAX]; + +//---------------------------------------- +// NTRIP Server Routines +//---------------------------------------- + +// Initiate a connection to the NTRIP caster +bool ntripServerConnectCaster(int serverIndex) +{ + NTRIP_SERVER_DATA * ntripServer = &ntripServerArray[serverIndex]; + const int SERVER_BUFFER_SIZE = 512; + char serverBuffer[SERVER_BUFFER_SIZE]; + + // Remove any http:// or https:// prefix from host name + char hostname[51]; + strncpy(hostname, settings.ntripServer_CasterHost[serverIndex], + sizeof(hostname) - 1); // strtok modifies string to be parsed so we create a copy + char *token = strtok(hostname, "//"); + if (token != nullptr) + { + token = strtok(nullptr, "//"); // Advance to data after // + if (token != nullptr) + strcpy(settings.ntripServer_CasterHost[serverIndex], token); + } + + if (settings.debugNtripServerState) + systemPrintf("NTRIP Server %d connecting to %s:%d\r\n", serverIndex, + settings.ntripServer_CasterHost[serverIndex], + settings.ntripServer_CasterPort[serverIndex]); + + // Attempt a connection to the NTRIP caster + if (!ntripServer->networkClient->connect(settings.ntripServer_CasterHost[serverIndex], + settings.ntripServer_CasterPort[serverIndex])) + { + if (settings.debugNtripServerState) + systemPrintf("NTRIP Server %d connection to NTRIP caster %s:%d failed\r\n", + serverIndex, + settings.ntripServer_CasterHost[serverIndex], + settings.ntripServer_CasterPort[serverIndex]); + return false; + } + + if (settings.debugNtripServerState) + systemPrintf("NTRIP Server %d sending authorization credentials\r\n", serverIndex); + + // Build the authorization credentials message + // * Mount point + // * Password + // * Agent + snprintf(serverBuffer, SERVER_BUFFER_SIZE, "SOURCE %s /%s\r\nSource-Agent: NTRIP SparkFun_RTK_%s/\r\n\r\n", + settings.ntripServer_MountPointPW[serverIndex], + settings.ntripServer_MountPoint[serverIndex], platformPrefix); + int length = strlen(serverBuffer); + getFirmwareVersion(&serverBuffer[length], sizeof(serverBuffer) - length, false); + + // Send the authorization credentials to the NTRIP caster + ntripServer->networkClient->write((const uint8_t *)serverBuffer, strlen(serverBuffer)); + return true; +} + +// Determine if the connection limit has been reached +bool ntripServerConnectLimitReached(int serverIndex) +{ + NTRIP_SERVER_DATA * ntripServer = &ntripServerArray[serverIndex]; + int seconds; + + // Retry the connection a few times + bool limitReached = (ntripServer->connectionAttempts >= MAX_NTRIP_SERVER_CONNECTION_ATTEMPTS); + + // Attempt to restart the network if possible + if (settings.enableNtripServer && (!limitReached)) + networkRestart(NETWORK_USER_NTRIP_SERVER + serverIndex); + + ntripServerStop(serverIndex, limitReached || (!settings.enableNtripServer)); + + ntripServer->connectionAttempts++; + ntripServer->connectionAttemptsTotal++; + if (settings.debugNtripServerState) + ntripServerPrintStatus(serverIndex); + + if (limitReached == false) + { + if (ntripServer->connectionAttempts == 1) + ntripServer->connectionAttemptTimeout = 15 * 1000L; // Wait 15s + else if (ntripServer->connectionAttempts == 2) + ntripServer->connectionAttemptTimeout = 30 * 1000L; // Wait 30s + else if (ntripServer->connectionAttempts == 3) + ntripServer->connectionAttemptTimeout = 1 * 60 * 1000L; // Wait 1 minute + else if (ntripServer->connectionAttempts == 4) + ntripServer->connectionAttemptTimeout = 2 * 60 * 1000L; // Wait 2 minutes + else + ntripServer->connectionAttemptTimeout = + (ntripServer->connectionAttempts - 4) * 5 * 60 * 1000L; // Wait 5, 10, 15, etc minutes between attempts + + // Display the delay before starting the NTRIP server + if (settings.debugNtripServerState && ntripServer->connectionAttemptTimeout) + { + seconds = ntripServer->connectionAttemptTimeout / 1000; + if (seconds < 120) + systemPrintf("NTRIP Server %d trying again in %d seconds.\r\n", serverIndex, seconds); + else + systemPrintf("NTRIP Server %d trying again in %d minutes.\r\n", serverIndex, seconds / 60); + } + } + else + // No more connection attempts + systemPrintf("NTRIP Server %d connection attempts exceeded!\r\n", serverIndex); + return limitReached; +} + +// Print the NTRIP server state summary +void ntripServerPrintStateSummary(int serverIndex) +{ + NTRIP_SERVER_DATA * ntripServer = &ntripServerArray[serverIndex]; + + switch (ntripServer->state) + { + default: + systemPrintf("Unknown: %d", ntripServer->state); + break; + case NTRIP_SERVER_OFF: + systemPrint("Disconnected"); + break; + case NTRIP_SERVER_ON: + case NTRIP_SERVER_NETWORK_STARTED: + case NTRIP_SERVER_NETWORK_CONNECTED: + case NTRIP_SERVER_WAIT_GNSS_DATA: + case NTRIP_SERVER_CONNECTING: + case NTRIP_SERVER_AUTHORIZATION: + systemPrint("Connecting"); + break; + case NTRIP_SERVER_CASTING: + systemPrint("Connected"); + break; + } +} + +// Print the NTRIP server status +void ntripServerPrintStatus (int serverIndex) +{ + NTRIP_SERVER_DATA * ntripServer = &ntripServerArray[serverIndex]; + uint64_t milliseconds; + uint32_t days; + byte hours; + byte minutes; + byte seconds; + + if (settings.enableNtripServer == true && + (systemState >= STATE_BASE_NOT_STARTED && systemState <= STATE_BASE_FIXED_TRANSMITTING)) + { + systemPrintf("NTRIP Server %d ", serverIndex); + ntripServerPrintStateSummary(serverIndex); + systemPrintf(" - %s/%s:%d", settings.ntripServer_CasterHost[serverIndex], + settings.ntripServer_MountPoint[serverIndex], + settings.ntripServer_CasterPort[serverIndex]); + + if (ntripServer->state == NTRIP_SERVER_CASTING) + // Use ntripServer->timer since it gets reset after each successful data + // receiption from the NTRIP caster + milliseconds = ntripServer->timer - ntripServer->startTime; + else + { + milliseconds = ntripServer->startTime; + systemPrint(" Last"); + } + + // Display the uptime + days = milliseconds / MILLISECONDS_IN_A_DAY; + milliseconds %= MILLISECONDS_IN_A_DAY; + + hours = milliseconds / MILLISECONDS_IN_AN_HOUR; + milliseconds %= MILLISECONDS_IN_AN_HOUR; + + minutes = milliseconds / MILLISECONDS_IN_A_MINUTE; + milliseconds %= MILLISECONDS_IN_A_MINUTE; + + seconds = milliseconds / MILLISECONDS_IN_A_SECOND; + milliseconds %= MILLISECONDS_IN_A_SECOND; + + systemPrint(" Uptime: "); + systemPrintf("%d %02d:%02d:%02d.%03lld (Reconnects: %d)\r\n", + days, hours, minutes, seconds, milliseconds, ntripServer->connectionAttemptsTotal); + } +} + +// This function gets called as each RTCM byte comes in +void ntripServerProcessRTCM(int serverIndex, uint8_t incoming) +{ + NTRIP_SERVER_DATA * ntripServer = &ntripServerArray[serverIndex]; + + if (ntripServer->state == NTRIP_SERVER_CASTING) + { + // Generate and print timestamp if needed + uint32_t currentMilliseconds; + if (online.rtc) + { + // Timestamp the RTCM messages + currentMilliseconds = millis(); + if (((settings.debugNtripServerRtcm && ((currentMilliseconds - ntripServer->previousMilliseconds) > 5)) + || PERIODIC_DISPLAY(PD_NTRIP_SERVER_DATA)) && (!settings.enableRtcmMessageChecking) + && (!inMainMenu) && ntripServer->bytesSent) + { + PERIODIC_CLEAR(PD_NTRIP_SERVER_DATA); + printTimeStamp(); + // 1 2 3 + // 123456789012345678901234567890 + // YYYY-mm-dd HH:MM:SS.xxxrn0 + struct tm timeinfo = rtc.getTimeStruct(); + char timestamp[30]; + strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", &timeinfo); + systemPrintf(" Tx%d RTCM: %s.%03ld, %d bytes sent\r\n", serverIndex, timestamp, rtc.getMillis(), ntripServer->zedBytesSent); + ntripServer->zedBytesSent = 0; + } + ntripServer->previousMilliseconds = currentMilliseconds; + } + + // If we have not gotten new RTCM bytes for a period of time, assume end of frame + if (((millis() - ntripServer->timer) > 100) && (ntripServer->bytesSent > 0)) + { + if ((!inMainMenu) && settings.debugNtripServerState) + systemPrintf("NTRIP Server %d transmitted %d RTCM bytes to Caster\r\n", serverIndex, ntripServer->bytesSent); + + ntripServer->bytesSent = 0; + } + + if (ntripServer->networkClient->connected()) + { + ntripServer->networkClient->write(incoming); // Send this byte to socket + ntripServer->bytesSent++; + ntripServer->zedBytesSent++; + ntripServer->timer = millis(); + netOutgoingRTCM = true; + } + } + + // Indicate that the GNSS is providing correction data + else if (ntripServer->state == NTRIP_SERVER_WAIT_GNSS_DATA) + { + ntripServerSetState(ntripServer, NTRIP_SERVER_CONNECTING); + rtcmParsingState = RTCM_TRANSPORT_STATE_WAIT_FOR_PREAMBLE_D3; + } +} + +// Read the authorization response from the NTRIP caster +void ntripServerResponse(int serverIndex, char *response, size_t maxLength) +{ + NTRIP_SERVER_DATA * ntripServer = &ntripServerArray[serverIndex]; + char *responseEnd; + + // Make sure that we can zero terminate the response + responseEnd = &response[maxLength - 1]; + + // Read bytes from the caster and store them + while ((response < responseEnd) && ntripServer->networkClient->available()) + *response++ = ntripServer->networkClient->read(); + + // Zero terminate the response + *response = '\0'; +} + +// Restart the NTRIP server +void ntripServerRestart(int serverIndex) +{ + NTRIP_SERVER_DATA * ntripServer = &ntripServerArray[serverIndex]; + + // Save the previous uptime value + if (ntripServer->state == NTRIP_SERVER_CASTING) + ntripServer->startTime = ntripServer->timer - ntripServer->startTime; + ntripServerConnectLimitReached(serverIndex); +} + +// Update the state of the NTRIP server state machine +void ntripServerSetState(NTRIP_SERVER_DATA * ntripServer, uint8_t newState) +{ + int serverIndex = -999; + for (int index = 0; index < NTRIP_SERVER_MAX; index++) + { + if (ntripServer == &ntripServerArray[index]) + { + serverIndex = index; + break; + } + } + + // PERIODIC_DISPLAY(PD_NTRIP_SERVER_STATE) is handled by ntripServerUpdate + if (settings.debugNtripServerState) + { + if (ntripServer->state == newState) + systemPrintf("NTRIP server %d: *", serverIndex); // If the state is not changing - print * + else + systemPrintf("NTRIP server %d: %s --> ", serverIndex, ntripServerStateName[ntripServer->state]); + } + ntripServer->state = newState; + if (settings.debugNtripServerState) + { + if (ntripServer->state >= NTRIP_SERVER_STATE_MAX) + { + systemPrintf("Unknown server state %d\r\n", ntripServer->state); + reportFatalError("Unknown NTRIP Server state"); + } + else + systemPrintln(ntripServerStateName[ntripServer->state]); + } +} + +// Shutdown the NTRIP server +void ntripServerShutdown(int serverIndex) +{ + ntripServerStop(serverIndex, true); +} + +// Start the NTRIP server +void ntripServerStart(int serverIndex) +{ + // Display the heap state + reportHeapNow(settings.debugNtripServerState); + + // Start the NTRIP server + systemPrintf("NTRIP Server %d start\r\n", serverIndex); + ntripServerStop(serverIndex, false); +} + +// Shutdown or restart the NTRIP server +void ntripServerStop(int serverIndex, bool shutdown) +{ + bool enabled; + NTRIP_SERVER_DATA * ntripServer = &ntripServerArray[serverIndex]; + + if (ntripServer->networkClient) + { + // Break the NTRIP server connection if necessary + if (ntripServer->networkClient->connected()) + ntripServer->networkClient->stop(); + + // Free the NTRIP server resources + delete ntripServer->networkClient; + ntripServer->networkClient = nullptr; + reportHeapNow(settings.debugNtripServerState); + } + + // Increase timeouts if we started the network + if (ntripServer->state > NTRIP_SERVER_ON) + { + // Mark the Server stop so that we don't immediately attempt re-connect to Caster + ntripServer->timer = millis(); + + // Done with the network + if (networkGetUserNetwork(NETWORK_USER_NTRIP_SERVER + serverIndex)) + networkUserClose(NETWORK_USER_NTRIP_SERVER + serverIndex); + } + + // Determine the next NTRIP server state + online.ntripServer[serverIndex] = false; + if (shutdown + || (!settings.ntripServer_CasterHost[serverIndex][0]) + || (!settings.ntripServer_CasterPort[serverIndex]) + || (!settings.ntripServer_MountPoint[serverIndex][0])) + { + if (shutdown) + { + if (settings.debugNtripServerState) + systemPrintf("ntripServerStop server %d shutdown requested\r\n", serverIndex); + } + else + { + if (settings.debugNtripServerState && (!settings.ntripServer_CasterHost[serverIndex][0])) + systemPrintf("ntripServerStop server %d caster host not configured!\r\n", serverIndex); + if (settings.debugNtripServerState && (!settings.ntripServer_CasterPort[serverIndex])) + systemPrintf("ntripServerStop server %d caster port not configured!\r\n", serverIndex); + if (settings.debugNtripServerState && (!settings.ntripServer_MountPoint[serverIndex][0])) + systemPrintf("ntripServerStop server %d mount point not configured!\r\n", serverIndex); + } + ntripServerSetState(ntripServer, NTRIP_SERVER_OFF); + ntripServer->connectionAttempts = 0; + ntripServer->connectionAttemptTimeout = 0; + + // Determine if any of the NTRIP servers are enabled + enabled = false; + for (int index = 0; index < NTRIP_SERVER_MAX; index++) + if (online.ntripServer[index]) + { + enabled = true; + break; + } + //settings.enableNtripServer = enabled; + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Why? Setting settings.enableNtripServer to false means + // the server connections cannot be (re)started without setting settings.enableNtripServer back + // to true via the menu / web config... Was the intent to close the network connection when all + // servers have disconnected? + } + else + { + systemPrintf("ntripServerStop server %d start requested\r\n", serverIndex); + ntripServerSetState(ntripServer, NTRIP_SERVER_ON); + } +} + +// Update the NTRIP server state machine +void ntripServerUpdate(int serverIndex) +{ + // Get the NTRIP data structure + NTRIP_SERVER_DATA * ntripServer = &ntripServerArray[serverIndex]; + + // For Ref Stn, process any RTCM data waiting in the u-blox library RTCM Buffer + // This causes the state change from NTRIP_SERVER_WAIT_GNSS_DATA to NTRIP_SERVER_CONNECTING + processRTCMBuffer(); + + // Shutdown the NTRIP server when the mode or setting changes + DMW_ds(ntripServerSetState, ntripServer); // DMW: set the server state to the same state - causes a print + if (NEQ_RTK_MODE(ntripServerMode) || (!settings.enableNtripServer)) + { + if (ntripServer->state > NTRIP_SERVER_OFF) + { + ntripServerStop(serverIndex, true); // This was false. Needs checking. TODO + ntripServer->connectionAttempts = 0; // Duplicate? ntripServerStop does this... TODO + ntripServer->connectionAttemptTimeout = 0; // Duplicate? ntripServerStop does this... TODO + ntripServerSetState(ntripServer, NTRIP_SERVER_OFF); // Duplicate? ntripServerStop does this... TODO + } + } + + // Enable the network and the NTRIP server if requested + switch (ntripServer->state) + { + case NTRIP_SERVER_OFF: + if (EQ_RTK_MODE(ntripServerMode) && settings.enableNtripServer + && settings.ntripServer_CasterHost[serverIndex][0] + && settings.ntripServer_CasterPort[serverIndex] + && settings.ntripServer_MountPoint[serverIndex][0]) + { + ntripServerStart(serverIndex); + } + break; + + // Start the network + case NTRIP_SERVER_ON: + if (networkUserOpen(NETWORK_USER_NTRIP_SERVER + serverIndex, NETWORK_TYPE_ACTIVE)) + ntripServerSetState(ntripServer, NTRIP_SERVER_NETWORK_STARTED); + break; + + // Wait for a network media connection + case NTRIP_SERVER_NETWORK_STARTED: + // Determine if the network has failed + if (networkIsShuttingDown(NETWORK_USER_NTRIP_SERVER + serverIndex)) + // Failed to connect to to the network, attempt to restart the network + ntripServerStop(serverIndex, true); // Note: was ntripServerRestart(serverIndex); + + // Determine if the network is connected to the media + else if (networkUserConnected(NETWORK_USER_NTRIP_SERVER + serverIndex)) + { + // Allocate the ntripServer structure + ntripServer->networkClient = new NetworkClient(NETWORK_USER_NTRIP_SERVER + serverIndex); + if (!ntripServer->networkClient) + { + // Failed to allocate the ntripServer structure + systemPrintf("ERROR: Failed to allocate the ntripServer %d structure!\r\n", serverIndex); + ntripServerShutdown(serverIndex); + } + else + { + reportHeapNow(settings.debugNtripServerState); + + // The network is available for the NTRIP server + ntripServerSetState(ntripServer, NTRIP_SERVER_NETWORK_CONNECTED); + } + } + break; + + // Network available + case NTRIP_SERVER_NETWORK_CONNECTED: + // Determine if the network has failed + if (networkIsShuttingDown(NETWORK_USER_NTRIP_SERVER + serverIndex)) + // Failed to connect to to the network, attempt to restart the network + ntripServerStop(serverIndex, true); // Note: was ntripServerRestart(serverIndex); + + else if (settings.enableNtripServer + && (millis() - ntripServer->lastConnectionAttempt > ntripServer->connectionAttemptTimeout)) + { + // No RTCM correction data sent yet + rtcmPacketsSent = 0; + + // Open socket to NTRIP caster + ntripServerSetState(ntripServer, NTRIP_SERVER_WAIT_GNSS_DATA); + } + break; + + // Wait for GNSS correction data + case NTRIP_SERVER_WAIT_GNSS_DATA: + // Determine if the network has failed + if (networkIsShuttingDown(NETWORK_USER_NTRIP_SERVER + serverIndex)) + // Failed to connect to to the network, attempt to restart the network + ntripServerStop(serverIndex, true); // Note: was ntripServerRestart(serverIndex); + + // State change handled in ntripServerProcessRTCM + break; + + // Initiate the connection to the NTRIP caster + case NTRIP_SERVER_CONNECTING: + // Determine if the network has failed + if (networkIsShuttingDown(NETWORK_USER_NTRIP_SERVER + serverIndex)) + // Failed to connect to to the network, attempt to restart the network + ntripServerStop(serverIndex, true); // Note: was ntripServerRestart(serverIndex); + + // Delay before opening the NTRIP server connection + else if ((millis() - ntripServer->timer) >= ntripServer->connectionAttemptTimeout) + { + // Attempt a connection to the NTRIP caster + if (!ntripServerConnectCaster(serverIndex)) + { + // Assume service not available + if (ntripServerConnectLimitReached(serverIndex)) // Update ntripServer->connectionAttemptTimeout + systemPrintf("NTRIP Server %d failed to connect! Do you have your caster address and port correct?\r\n", serverIndex); + } + else + { + // Connection open to NTRIP caster, wait for the authorization response + ntripServer->timer = millis(); + ntripServerSetState(ntripServer, NTRIP_SERVER_AUTHORIZATION); + } + } + break; + + // Wait for authorization response + case NTRIP_SERVER_AUTHORIZATION: + // Determine if the network has failed + if (networkIsShuttingDown(NETWORK_USER_NTRIP_SERVER + serverIndex)) + // Failed to connect to to the network, attempt to restart the network + ntripServerStop(serverIndex, true); // Note: was ntripServerRestart(serverIndex); + + // Check if caster service responded + else if (ntripServer->networkClient->available() < strlen("ICY 200 OK")) // Wait until at least a few bytes have arrived + { + // Check for response timeout + if (millis() - ntripServer->timer > 10000) + { + if (ntripServerConnectLimitReached(serverIndex)) + systemPrintf("Caster %d failed to respond. Do you have your caster address and port correct?\r\n", serverIndex); + } + } + else + { + // NTRIP caster's authorization response received + char response[512]; + ntripServerResponse(serverIndex, response, sizeof(response)); + + if (settings.debugNtripServerState) + systemPrintf("Server %d Response: %s\r\n", serverIndex, response); + else + log_d("Server %d Response: %s", serverIndex, response); + + // Look for various responses + if (strstr(response, "200") != nullptr) //'200' found + { + // We got a response, now check it for possible errors + if (strcasestr(response, "banned") != nullptr) + { + systemPrintf("NTRIP Server %d connected to caster but caster responded with banned error: %s\r\n", + serverIndex, response); + + // Stop NTRIP Server operations + ntripServerShutdown(serverIndex); + } + else if (strcasestr(response, "sandbox") != nullptr) + { + systemPrintf("NTRIP Server %d connected to caster but caster responded with sandbox error: %s\r\n", + serverIndex, response); + + // Stop NTRIP Server operations + ntripServerShutdown(serverIndex); + } + + systemPrintf("NTRIP Server %d connected to %s:%d %s\r\n", serverIndex, + settings.ntripServer_CasterHost[serverIndex], + settings.ntripServer_CasterPort[serverIndex], + settings.ntripServer_MountPoint[serverIndex]); + + // Connection is now open, start the RTCM correction data timer + ntripServer->timer = millis(); + + // We don't use a task because we use I2C hardware (and don't have a semphore). + online.ntripServer[serverIndex] = true; + ntripServer->startTime = millis(); + ntripServerSetState(ntripServer, NTRIP_SERVER_CASTING); + } + + // Look for '401 Unauthorized' + else if (strstr(response, "401") != nullptr) + { + systemPrintf( + "NTRIP Caster %d responded with unauthorized error: %s. Are you sure your caster credentials are correct?\r\n", + serverIndex, response); + + // Give up - Shutdown NTRIP server, no further retries + ntripServerShutdown(serverIndex); + } + + // Other errors returned by the caster + else + { + systemPrintf("NTRIP Server %d connected but caster responded with problem: %s\r\n", serverIndex, response); + + // Check for connection limit + if (ntripServerConnectLimitReached(serverIndex)) + systemPrintf("NTRIP Server %d retry limit reached; do you have your caster address and port correct?\r\n", serverIndex); + } + } + break; + + // NTRIP server authorized to send RTCM correction data to NTRIP caster + case NTRIP_SERVER_CASTING: + // Determine if the network has failed + if (networkIsShuttingDown(NETWORK_USER_NTRIP_SERVER + serverIndex)) + // Failed to connect to to the network, attempt to restart the network + ntripServerStop(serverIndex, true); // Note: was ntripServerRestart(serverIndex); + + // Check for a broken connection + else if (!ntripServer->networkClient->connected()) + { + // Broken connection, retry the NTRIP connection + systemPrintf("Connection to NTRIP Caster %d was lost\r\n", serverIndex); + ntripServerRestart(serverIndex); + } + else if ((millis() - ntripServer->timer) > (15 * 1000)) + { + // GNSS stopped sending RTCM correction data + systemPrintf("NTRIP Server %d breaking connection to caster due to lack of RTCM data!\r\n", serverIndex); + ntripServerRestart(serverIndex); + } + else + { + // Handle other types of NTRIP connection failures to prevent + // hammering the NTRIP caster with rapid connection attempts. + // A fast reconnect is reasonable after a long NTRIP caster + // connection. However increasing backoff delays should be + // added when the NTRIP caster fails after a short connection + // interval. + if (((millis() - ntripServer->startTime) > NTRIP_SERVER_CONNECTION_TIME) + && (ntripServer->connectionAttempts || ntripServer->connectionAttemptTimeout)) + { + // After a long connection period, reset the attempt counter + ntripServer->connectionAttempts = 0; + ntripServer->connectionAttemptTimeout = 0; + if (settings.debugNtripServerState) + systemPrintf("NTRIP Server %d resetting connection attempt counter and timeout\r\n", serverIndex); + } + + // All is well + cyclePositionLEDs(); + } + break; + } + + // Periodically display the state + if (PERIODIC_DISPLAY(PD_NTRIP_SERVER_STATE)) + { + systemPrintf("NTRIP Server %d state: %s\r\n", serverIndex, ntripServerStateName[ntripServer->state]); + if (serverIndex == (NTRIP_SERVER_MAX - 1)) + PERIODIC_CLEAR(PD_NTRIP_SERVER_STATE); // Clear the periodic display only on the last server + } +} + +// Update the NTRIP server state machine +void ntripServerUpdate() +{ + for (int serverIndex = 0; serverIndex < NTRIP_SERVER_MAX; serverIndex++) + ntripServerUpdate(serverIndex); +} + +// Verify the NTRIP server tables +void ntripServerValidateTables() +{ + if (ntripServerStateNameEntries != NTRIP_SERVER_STATE_MAX) + reportFatalError("Fix ntripServerStateNameEntries to match NTRIPServerState"); + if (NETWORK_USER_MAX > (sizeof(NETWORK_USER) * 8)) + reportFatalError("Increase the NETWORK_USER type"); +} + +#endif // COMPILE_NETWORK diff --git a/Firmware/RTK_Surveyor/OtaClient.ino b/Firmware/RTK_Surveyor/OtaClient.ino new file mode 100644 index 000000000..eaaf1b02f --- /dev/null +++ b/Firmware/RTK_Surveyor/OtaClient.ino @@ -0,0 +1,868 @@ +/*------------------------------------------------------------------------------ +OtaClient.ino + + The Over-The-Air (OTA) client sits on top of the network layer and requests + a JSON file from the GitHub server that describes the version and URL of + the released firmware. If the released firmware is more recent then the OTA + client downloads and flashes the released firmware. + + RTK + Device + ^ + | OTA client + V + GitHub + +------------------------------------------------------------------------------*/ + +#if COMPILE_NETWORK + +//---------------------------------------- +// Constants +//---------------------------------------- + +#define HTTPS_TRANSPORT (OTA_USE_SSL ? "https://" : "http://") + +#define OTA_JSON_FILE_URL \ + "/sparkfun/SparkFun_RTK_Firmware_Binaries/main/RTK-Firmware.json" +#define OTA_NO_PROGRESS_TIMEOUT (3 * 60 * 1000) // 3 minutes +#define OTA_SERVER "raw.githubusercontent.com" +#define OTA_SERVER_PORT 443 +#define OTA_USE_SSL 1 + +enum OtaState +{ + OTA_STATE_OFF = 0, + OTA_STATE_START_NETWORK, + OTA_STATE_WAIT_FOR_NETWORK, + OTA_STATE_JSON_FILE_REQUEST, + OTA_STATE_JSON_FILE_PARSE_HTTP_STATUS, + OTA_STATE_JSON_FILE_PARSE_LENGTH, + OTA_STATE_JSON_FILE_SKIP_HEADERS, + OTA_STATE_JSON_FILE_READ_DATA, + OTA_STATE_PARSE_JSON_DATA, + OTA_STATE_BIN_FILE_REQUEST, + OTA_STATE_BIN_FILE_PARSE_HTTP_STATUS, + OTA_STATE_BIN_FILE_PARSE_LENGTH, + OTA_STATE_BIN_FILE_SKIP_HEADERS, + OTA_STATE_BIN_FILE_READ_DATA, + // Insert new states before this line + OTA_STATE_MAX +}; + +const char * const otaStateNames[] = +{ + "OTA_STATE_OFF", + "OTA_STATE_START_NETWORK", + "OTA_STATE_WAIT_FOR_NETWORK", + "OTA_STATE_JSON_FILE_REQUEST", + "OTA_STATE_JSON_FILE_PARSE_HTTP_STATUS", + "OTA_STATE_JSON_FILE_PARSE_LENGTH", + "OTA_STATE_JSON_FILE_SKIP_HEADERS", + "OTA_STATE_JSON_FILE_READ_DATA", + "OTA_STATE_PARSE_JSON_DATA", + "OTA_STATE_BIN_FILE_REQUEST", + "OTA_STATE_BIN_FILE_PARSE_HTTP_STATUS", + "OTA_STATE_BIN_FILE_PARSE_LENGTH", + "OTA_STATE_BIN_FILE_SKIP_HEADERS", + "OTA_STATE_BIN_FILE_READ_DATA" +}; +const int otaStateEntries = sizeof(otaStateNames) / sizeof(otaStateNames[0]); + +const RtkMode_t otaClientMode = RTK_MODE_BASE_FIXED + | RTK_MODE_BASE_SURVEY_IN + | RTK_MODE_BUBBLE_LEVEL + | RTK_MODE_NTP + | RTK_MODE_ROVER; + +//---------------------------------------- +// Locals +//---------------------------------------- + +static byte otaBluetoothState = BT_OFF; +static char otaBuffer[1379]; +static int otaBufferData; +static NetworkClient * otaClient; +static int otaFileBytes; +static int otaFileSize; +static String otaJsonFileData; +static String otaReleasedFirmwareURL; +static uint8_t otaState; +static uint32_t otaTimer; + +//---------------------------------------- +// Over-The-Air (OTA) firmware update support routines +//---------------------------------------- + +// Get the OTA state name +const char * otaGetStateName(uint8_t state, char * string) +{ + if (state < OTA_STATE_MAX) + return otaStateNames[state]; + sprintf(string, "Unknown state (%d)", state); + return string; +} + +// Get the file length from the HTTP header +int otaParseFileLength() +{ + int fileLength; + + // Parse the file length from the HTTP header + otaBufferData = 0; + if (sscanf(otaBuffer, "Content-Length: %d", &fileLength) == 1) + return fileLength; + return -1; +} + +// Get the server file status from the HTTP header +int otaParseJsonStatus() +{ + int status; + + // Parse the status from the HTTP header + if (sscanf(otaBuffer, "HTTP/1.1 %d", &status) == 1) + return status; + return -1; +} + +// Read data from the JSON or firmware file +void otaReadFileData(int bufferLength) +{ + int bytesToRead; + + // Determine how much data is available + otaBufferData = 0; + bytesToRead = otaClient->available(); + if (bytesToRead) + { + // Determine the number of bytes to read + if (bytesToRead > bufferLength) + bytesToRead = bufferLength; + + // Read in the file data + otaBufferData = otaClient->read((uint8_t *)otaBuffer, bytesToRead); + } +} + +// Read a line from the HTTP header +int otaReadHeaderLine() +{ + int bytesToRead; + String otaReleasedFirmwareVersion; + int status; + + // Determine if the network is shutting down + if (networkIsShuttingDown(NETWORK_USER_OTA_FIRMWARE_UPDATE)) + { + systemPrintln("OTA: Network is shutting down!"); + otaStop(); + return -1; + } + + // Determine if the network is connected to the media + if (!networkUserConnected(NETWORK_USER_OTA_FIRMWARE_UPDATE)) + { + systemPrintln("OTA: Network has failed!"); + otaStop(); + return -1; + } + + // Verify the connection to the HTTP server + if (!otaClient->connected()) + { + systemPrintln("OTA: HTTP connection broken!"); + otaStop(); + return -1; + } + + // Read in the released firmware data + while (otaClient->available()) + { + otaBuffer[otaBufferData] = otaClient->read(); + + // Drop the carriage return + if (otaBuffer[otaBufferData] != '\r') + { + // Build the header line + if (otaBuffer[otaBufferData] != '\n') + otaBufferData += 1; + else + { + // Zero-terminate the header line + otaBuffer[otaBufferData] = 0; + return 0; + } + } + } + return 1; +} + +// Set the next OTA state +void otaSetState(uint8_t newState) +{ + char string1[40]; + char string2[40]; + const char * arrow; + const char * asterisk; + const char * initialState; + const char * endingState; + bool pd; + + // Display the state transition + pd = PERIODIC_DISPLAY(PD_OTA_CLIENT_STATE); + if ((settings.debugFirmwareUpdate) || pd) + { + arrow = ""; + asterisk = ""; + initialState = ""; + if (newState == otaState) + asterisk = "*"; + else + { + initialState = otaGetStateName(otaState, string1); + arrow = " --> "; + } + } + + // Set the new state + otaState = newState; + if ((settings.debugFirmwareUpdate) || pd) + { + // Display the new firmware update state + PERIODIC_CLEAR(PD_OTA_CLIENT_STATE); + endingState = otaGetStateName(newState, string2); + if (!online.rtc) + systemPrintf("%s%s%s%s\r\n", asterisk, initialState, arrow, endingState); + else + { + // Timestamp the state change + // 1 2 + // 12345678901234567890123456 + // YYYY-mm-dd HH:MM:SS.xxxrn0 + struct tm timeinfo = rtc.getTimeStruct(); + char s[30]; + strftime(s, sizeof(s), "%Y-%m-%d %H:%M:%S", &timeinfo); + systemPrintf("%s%s%s%s, %s.%03ld\r\n", asterisk, initialState, arrow, endingState, s, rtc.getMillis()); + } + } + + // Display the starting percentage + if (otaState == OTA_STATE_BIN_FILE_READ_DATA) + otaDisplayPercentage(otaFileBytes, otaFileSize, pd); + + // Validate the firmware update state + if (newState >= OTA_STATE_MAX) + reportFatalError("Invalid OTA state"); +} + +// Stop the OTA firmware update +void otaStop() +{ + if (settings.debugFirmwareUpdate) + systemPrintln("otaStop called"); + + if (otaState != OTA_STATE_OFF) + { + // Stop WiFi + systemPrintln("OTA stopping WiFi"); + online.otaFirmwareUpdate = false; + + // Stop writing to flash + if (Update.isRunning()) + Update.abort(); + + // Close the SSL connection + if (otaClient) + { + delete otaClient; + otaClient = nullptr; + } + + // Close the network connection + if (networkGetUserNetwork(NETWORK_USER_OTA_FIRMWARE_UPDATE)) + networkUserClose(NETWORK_USER_OTA_FIRMWARE_UPDATE); + + // Stop the firmware update + otaBufferData = 0; + otaJsonFileData = String(""); + otaSetState(OTA_STATE_OFF); + otaTimer = millis(); + + // Restart bluetooth if necessary + if (otaBluetoothState == BT_CONNECTED) + { + otaBluetoothState = BT_OFF; + if (settings.debugFirmwareUpdate) + systemPrintln("Firmware update restarting Bluetooth"); + bluetoothStart(); // Restart BT according to settings + } + } +}; + +int otaWriteDataToFlash(int bytesToWrite) +{ + int bytesWritten; + + bytesWritten = 0; + if (bytesToWrite) + { + // Write the data to flash + bytesWritten = Update.write((uint8_t *)otaBuffer, bytesToWrite); + if (bytesWritten) + { + otaFileBytes += bytesWritten; + if (bytesWritten != bytesToWrite) + { + // Only a portion of the data was written, move the rest of + // the data to the beginning of the buffer + memcpy(otaBuffer, &otaBuffer[bytesWritten], bytesToWrite - bytesWritten); + if (settings.debugFirmwareUpdate) + systemPrintf("OTA: Wrote only %d of %d bytes to flash\r\n", bytesWritten, otaBufferData); + } + + // Display the percentage written + otaPullCallback(otaFileBytes, otaFileSize); + } + } + + // Return the number of bytes to write + return bytesToWrite - bytesWritten; +} + +//---------------------------------------- +// Over-The-Air (OTA) firmware update state machine +//---------------------------------------- + +// Perform the over-the-air (OTA) firmware updates +void otaClientUpdate() +{ + int bytesWritten; + int32_t checkIntervalMillis; + NETWORK_DATA * network; + String otaReleasedFirmwareVersion; + int status; + + // Perform the firmware update + if (!inMainMenu) + { + // Shutdown the OTA client when the mode or setting changes + DMW_st(otaSetState, otaState); + if (NEQ_RTK_MODE(otaClientMode) || (!settings.enableAutoFirmwareUpdate)) + { + if (otaState > OTA_STATE_OFF) + { + otaStop(); + + // Due to the interruption, enable a fast retry + otaTimer = millis() - checkIntervalMillis + OTA_NO_PROGRESS_TIMEOUT; + } + } + + // Walk the state machine to do the firmware update + switch (otaState) + { + // Handle invalid OTA states + default: { + systemPrintf("ERROR: Unknown OTA state (%d)\r\n", otaState); + otaStop(); + break; + } + + // Over-the-air firmware updates are not active + case OTA_STATE_OFF: { + // Determine if the user enabled automatic firmware updates + if (EQ_RTK_MODE(otaClientMode) && settings.enableAutoFirmwareUpdate) + { + // Wait until it is time to check for a firmware update + checkIntervalMillis = settings.autoFirmwareCheckMinutes * 60 * 1000; + if ((int32_t)(millis() - otaTimer) >= checkIntervalMillis) + { + otaTimer = millis(); + online.otaFirmwareUpdate = true; + otaSetState(OTA_STATE_START_NETWORK); + } + } + break; + } + + // Start the network + case OTA_STATE_START_NETWORK: { + if (settings.debugFirmwareUpdate) + systemPrintln("OTA starting network"); + if (!networkUserOpen(NETWORK_USER_OTA_FIRMWARE_UPDATE, NETWORK_TYPE_ACTIVE)) + { + systemPrintln("OTA: Firmware update failed, unable to start network"); + otaStop(); + } + else + otaSetState(OTA_STATE_WAIT_FOR_NETWORK); + break; + } + + // Wait for connection to the network + case OTA_STATE_WAIT_FOR_NETWORK: { + // Determine if the network has failed + if (networkIsShuttingDown(NETWORK_USER_OTA_FIRMWARE_UPDATE)) + { + systemPrintln("OTA: Network is shutting down!"); + otaStop(); + } + + // Determine if the network is connected to the media + else if (networkUserConnected(NETWORK_USER_OTA_FIRMWARE_UPDATE)) + { + if (settings.debugFirmwareUpdate) + systemPrintln("OTA connected to network"); + + // Allocate the OTA firmware update client + otaClient = networkClient(NETWORK_USER_OTA_FIRMWARE_UPDATE, OTA_USE_SSL); + if (!otaClient) + { + systemPrintln("ERROR: Failed to allocate OTA client!"); + otaStop(); + } + else + { + // Stop Bluetooth + otaBluetoothState = bluetoothGetState(); + if (otaBluetoothState != BT_OFF) + { + if (settings.debugFirmwareUpdate) + systemPrintln("OTA stopping Bluetooth"); + bluetoothStop(); + } + + // Connect to GitHub + otaSetState(OTA_STATE_JSON_FILE_REQUEST); + } + } + break; + } + + // Issue the HTTP request to get the JSON file + case OTA_STATE_JSON_FILE_REQUEST: { + // Determine if the network is shutting down + if (networkIsShuttingDown(NETWORK_USER_OTA_FIRMWARE_UPDATE)) + { + systemPrintln("OTA: Network is shutting down!"); + otaStop(); + } + + // Determine if the network is connected to the media + else if (!networkUserConnected(NETWORK_USER_OTA_FIRMWARE_UPDATE)) + { + systemPrintln("OTA: Network has failed!"); + otaStop(); + } + + // Attempt to connect to the server using HTTPS + else if (!otaClient->connect(OTA_SERVER, OTA_SERVER_PORT)) + { + // if you didn't get a connection to the server: + systemPrintln("OTA: Connection failed"); + otaStop(); + } + else + { + systemPrintln("OTA: Requesting JSON file"); + + // Make the HTTP request: + otaClient->print("GET "); + otaClient->print(OTA_FIRMWARE_JSON_URL); + otaClient->println(" HTTP/1.1"); + otaClient->println("User-Agent: RTK OTA Client"); + otaClient->print("Host: "); + otaClient->println(OTA_SERVER); + otaClient->println("Connection: close"); + otaClient->println(); + otaBufferData = 0; + otaSetState(OTA_STATE_JSON_FILE_PARSE_HTTP_STATUS); + } + break; + } + + // Locate the HTTP status header + case OTA_STATE_JSON_FILE_PARSE_HTTP_STATUS: { + status = otaReadHeaderLine(); + if (status) + break; + + // Verify that the server found the file + status = otaParseJsonStatus(); + if (settings.debugFirmwareUpdate) + systemPrintf("OTA: Server file status: %d\r\n", status); + if (status != 200) + { + if (settings.debugFirmwareUpdate) + systemPrintln("OTA: Server failed to locate the JSON file"); + otaStop(); + } + else + { + otaBufferData = 0; + otaSetState(OTA_STATE_JSON_FILE_PARSE_LENGTH); + } + break; + } + + // Locate the file length header + case OTA_STATE_JSON_FILE_PARSE_LENGTH: { + status = otaReadHeaderLine(); + if (status) + break; + + // Verify the header line length + if (!otaBufferData) + { + if (settings.debugFirmwareUpdate) + systemPrintln("OTA: JSON file length not found"); + otaStop(); + break; + } + + // Get the file length + otaFileSize = otaParseFileLength(); + if (otaFileSize >= 0) + { + if (settings.debugFirmwareUpdate) + systemPrintf("OTA: JSON file length %d bytes\r\n", otaFileSize); + otaBufferData = 0; + otaFileBytes = 0; + otaSetState(OTA_STATE_JSON_FILE_SKIP_HEADERS); + } + break; + } + + // Skip over the rest of the HTTP headers + case OTA_STATE_JSON_FILE_SKIP_HEADERS: { + status = otaReadHeaderLine(); + if (status) + break; + + // Determine if this is the separater between the HTTP headers + // and the file data + if (!otaBufferData) + otaSetState(OTA_STATE_JSON_FILE_READ_DATA); + otaBufferData = 0; + break; + } + + // Receive the JSON data from the HTTP server + case OTA_STATE_JSON_FILE_READ_DATA: { + // Determine if the network is shutting down + if (networkIsShuttingDown(NETWORK_USER_OTA_FIRMWARE_UPDATE)) + { + systemPrintf("OTA: Network is shutting down after %d bytes!\r\n", otaFileBytes); + otaStop(); + } + + // Determine if the network is connected to the media + else if (!networkUserConnected(NETWORK_USER_OTA_FIRMWARE_UPDATE)) + { + systemPrintf("OTA: Network has failed after %d bytes!\r\n", otaFileBytes); + otaStop(); + } + + // Verify the connection to the HTTP server + else if (!otaClient->connected()) + { + systemPrintf("OTA: HTTP connection broken after %d bytes!\r\n", otaFileBytes); + otaStop(); + } + else + { + // Read data from the JSON file + otaReadFileData(sizeof(otaBuffer) - 1); + if (otaBufferData) + { + // Zero terminate the file data in the buffer + otaBuffer[otaBufferData] = 0; + + // Append the JSON file data to the string + otaJsonFileData += String(&otaBuffer[0]); + otaBufferData = 0; + } + + // Done if not at the end-of-file + if (otaJsonFileData.length() != otaFileSize) + break; + + // Reached end-of-file + // Parse the JSON file + if (settings.debugFirmwareUpdate) + systemPrintf("OTA: JSON data: %s\r\n", otaJsonFileData.c_str()); + otaSetState(OTA_STATE_PARSE_JSON_DATA); + } + break; + } + + // Parse the JSON data and determine if new firmware is available + case OTA_STATE_PARSE_JSON_DATA: { + // Locate the fields in the JSON file + DynamicJsonDocument doc(1000); + if (deserializeJson(doc, otaJsonFileData.c_str()) != DeserializationError::Ok) + { + systemPrintln("OTA: Failed to parse the JSON file data"); + otaStop(); + } + else + { + char versionString[9]; + + // Get the current version + getFirmwareVersion(versionString, sizeof(versionString), false); + + // Step through the configurations looking for a match + for (auto config : doc["Configurations"].as()) + { + // Get the latest released version + otaReleasedFirmwareVersion = config["Version"].isNull() ? "" : (const char *)config["Version"]; + otaReleasedFirmwareURL = config["URL"].isNull() ? "" : (const char *)config["URL"]; + if ((tolower(versionString[0]) != 'd') + && (FIRMWARE_VERSION_MAJOR != 99) + && (String(&versionString[1]) >= otaReleasedFirmwareVersion)) + { + if (settings.debugFirmwareUpdate) + systemPrintf("OTA: Current firmware %s is beyond released %s, no change necessary.\r\n", + versionString, otaReleasedFirmwareVersion.c_str()); + otaStop(); + } + else + { + if (settings.debugFirmwareUpdate) + systemPrintf("OTA: Firmware URL %s\r\n", otaReleasedFirmwareURL.c_str()); + if ((strncasecmp(HTTPS_TRANSPORT, + otaReleasedFirmwareURL.c_str(), + strlen(HTTPS_TRANSPORT)) != 0) + || (strncasecmp(OTA_SERVER, + &otaReleasedFirmwareURL.c_str()[strlen(HTTPS_TRANSPORT)], + strlen(OTA_SERVER)) != 0)) + { + if (settings.debugFirmwareUpdate) + systemPrintln("OTA: Invalid firmware URL"); + otaStop(); + } + else + { + // Break the connection with the HTTP server + otaClient->stop(); + if (settings.debugFirmwareUpdate) + systemPrintf("OTA: Upgrading firmware from %s to %s\r\n", + versionString, otaReleasedFirmwareVersion.c_str()); + otaReleasedFirmwareURL.remove(0, strlen(HTTPS_TRANSPORT) + strlen(OTA_SERVER)); + otaSetState(OTA_STATE_BIN_FILE_REQUEST); + } + } + break; + } + } + break; + } + + // Issue the HTTP request to get the released firmware file + case OTA_STATE_BIN_FILE_REQUEST: { + // Determine if the network is shutting down + if (networkIsShuttingDown(NETWORK_USER_OTA_FIRMWARE_UPDATE)) + { + systemPrintln("OTA: Network is shutting down!"); + otaStop(); + } + + // Determine if the network is connected to the media + else if (!networkUserConnected(NETWORK_USER_OTA_FIRMWARE_UPDATE)) + { + systemPrintln("OTA: Network has failed!"); + otaStop(); + } + + // Attempt to connect to the server using HTTPS + else if (!otaClient->connect(OTA_SERVER, OTA_SERVER_PORT)) + { + // if you didn't get a connection to the server: + systemPrintln("OTA: Connection failed"); + otaStop(); + } + else + { + systemPrintln("OTA: Requesting BIN file"); + + // Make the HTTP request: + otaClient->print("GET "); + otaClient->print(otaReleasedFirmwareURL.c_str()); + otaClient->println(" HTTP/1.1"); + otaClient->println("User-Agent: RTK OTA Client"); + otaClient->print("Host: "); + otaClient->println(OTA_SERVER); + otaClient->println("Connection: close"); + otaClient->println(); + otaBufferData = 0; + otaSetState(OTA_STATE_BIN_FILE_PARSE_HTTP_STATUS); + } + break; + } + + // Locate the HTTP status header + case OTA_STATE_BIN_FILE_PARSE_HTTP_STATUS: { + status = otaReadHeaderLine(); + if (status) + break; + + // Verify that the server found the file + status = otaParseJsonStatus(); + if (settings.debugFirmwareUpdate) + systemPrintf("OTA: Server file status: %d\r\n", status); + if (status != 200) + { + if (settings.debugFirmwareUpdate) + systemPrintln("OTA: Server failed to locate the JSON file"); + otaStop(); + } + else + { + otaBufferData = 0; + otaSetState(OTA_STATE_BIN_FILE_PARSE_LENGTH); + } + break; + } + + // Locate the file length header + case OTA_STATE_BIN_FILE_PARSE_LENGTH: { + status = otaReadHeaderLine(); + if (status) + break; + + // Verify the header line length + if (!otaBufferData) + { + if (settings.debugFirmwareUpdate) + systemPrintln("OTA: BIN file length not found"); + otaStop(); + break; + } + + // Get the file length + otaFileSize = otaParseFileLength(); + if (otaFileSize >= 0) + { + if (settings.debugFirmwareUpdate) + systemPrintf("OTA: BIN file length %d bytes\r\n", otaFileSize); + otaBufferData = 0; + otaFileBytes = 0; + otaSetState(OTA_STATE_BIN_FILE_SKIP_HEADERS); + } + break; + } + + // Skip over the rest of the HTTP headers + case OTA_STATE_BIN_FILE_SKIP_HEADERS: { + status = otaReadHeaderLine(); + if (status) + break; + + // Determine if this is the separater between the HTTP headers + // and the file data + if (!otaBufferData) + { + // Start the firmware update process + if (!Update.begin(UPDATE_SIZE_UNKNOWN)) + otaStop(); + else + { + otaTimer = millis(); + otaSetState(OTA_STATE_BIN_FILE_READ_DATA); + } + } + otaBufferData = 0; + break; + } + + // Receive the bin file from the HTTP server + case OTA_STATE_BIN_FILE_READ_DATA: { + do + { + bytesWritten = 0; + + // Determine if the network is shutting down + if (networkIsShuttingDown(NETWORK_USER_OTA_FIRMWARE_UPDATE)) + { + systemPrintln("OTA: Network is shutting down!"); + otaStop(); + } + + // Determine if the network is connected to the media + else if (!networkUserConnected(NETWORK_USER_OTA_FIRMWARE_UPDATE)) + { + systemPrintln("OTA: Network has failed!"); + otaStop(); + } + + // Verify the connection to the HTTP server + else if (!otaClient->connected()) + { + systemPrintf("OTA: HTTP connection broken after %d bytes!\r\n", otaFileBytes); + otaStop(); + } + + // Determine if progress is being made + else if ((millis() - otaTimer) >= OTA_NO_PROGRESS_TIMEOUT) + { + systemPrintln("OTA: No progress being made, link broken!"); + otaStop(); + checkIntervalMillis = settings.autoFirmwareCheckMinutes * 60 * 1000; + + // Delay for OTA_NO_PROGRESS_TIMEOUT + otaTimer = millis() - checkIntervalMillis + OTA_NO_PROGRESS_TIMEOUT; + } + + // Read data and write it to the flash + else + { + // Read data from the binary file + if (!otaBufferData) + otaReadFileData(sizeof(otaBuffer)); + + // Write the data to the flash + if (otaBufferData) + { + bytesWritten = otaBufferData; + otaBufferData = otaWriteDataToFlash(otaBufferData); + bytesWritten -= otaBufferData; + otaTimer = millis(); + + // Check for end-of-file + if (otaFileBytes == otaFileSize) + { + // The end-of-file was reached + if (settings.debugFirmwareUpdate) + systemPrintf("OTA: Downloaded %d bytes\r\n", otaFileBytes); + Update.end(true); + + // Reset the system + systemPrintln("OTA: Starting the new firmware"); + delay(1000); + ESP.restart(); + break; + } + } + } + } while (bytesWritten); + break; + } + } + + // Periodically display the PVT client state + if (PERIODIC_DISPLAY(PD_OTA_CLIENT_STATE)) + otaSetState(otaState); + } +} + +// Verify the firmware update tables +void otaVerifyTables() +{ + // Verify the table lengths + if (otaStateEntries != OTA_STATE_MAX) + reportFatalError("Fix otaStateNames table to match OtaState"); +} + +#endif // COMPILE_NETWORK diff --git a/Firmware/RTK_Surveyor/Parse_NMEA.ino b/Firmware/RTK_Surveyor/Parse_NMEA.ino new file mode 100644 index 000000000..a05dd0c4d --- /dev/null +++ b/Firmware/RTK_Surveyor/Parse_NMEA.ino @@ -0,0 +1,116 @@ +/*------------------------------------------------------------------------------ +Parse_NMEA.ino + + NMEA message parsing support routines +------------------------------------------------------------------------------*/ + +// +// NMEA Message +// +// +----------+---------+--------+---------+----------+----------+ +// | Preamble | Name | Comma | Data | Asterisk | Checksum | +// | 8 bits | n bytes | 8 bits | n bytes | 8 bits | 2 bytes | +// | $ | | , | | | | +// +----------+---------+--------+---------+----------+----------+ +// | | +// |<-------- Checksum -------->| +// + +// Check for the preamble +uint8_t nmeaPreamble(PARSE_STATE *parse, uint8_t data) +{ + if (data == '$') + { + parse->crc = 0; + parse->computeCrc = false; + parse->nmeaMessageNameLength = 0; + parse->state = nmeaFindFirstComma; + return SENTENCE_TYPE_NMEA; + } + return SENTENCE_TYPE_NONE; +} + +// Read the message name +uint8_t nmeaFindFirstComma(PARSE_STATE *parse, uint8_t data) +{ + parse->crc ^= data; + if ((data != ',') || (parse->nmeaMessageNameLength == 0)) + { + if ((data < 'A') || (data > 'Z')) + { + parse->length = 0; + parse->buffer[parse->length++] = data; + return gpsMessageParserFirstByte(parse, data); + } + + // Save the message name + parse->nmeaMessageName[parse->nmeaMessageNameLength++] = data; + } + else + { + // Zero terminate the message name + parse->nmeaMessageName[parse->nmeaMessageNameLength++] = 0; + parse->state = nmeaFindAsterisk; + } + return SENTENCE_TYPE_NMEA; +} + +// Read the message data +uint8_t nmeaFindAsterisk(PARSE_STATE *parse, uint8_t data) +{ + if (data != '*') + parse->crc ^= data; + else + parse->state = nmeaChecksumByte1; + return SENTENCE_TYPE_NMEA; +} + +// Read the first checksum byte +uint8_t nmeaChecksumByte1(PARSE_STATE *parse, uint8_t data) +{ + parse->state = nmeaChecksumByte2; + return SENTENCE_TYPE_NMEA; +} + +// Read the second checksum byte +uint8_t nmeaChecksumByte2(PARSE_STATE *parse, uint8_t data) +{ + parse->nmeaLength = parse->length; + parse->state = nmeaLineTermination; + return SENTENCE_TYPE_NMEA; +} + +// Read the line termination +uint8_t nmeaLineTermination(PARSE_STATE *parse, uint8_t data) +{ + int checksum; + + // Process the line termination + if ((data != '\r') && (data != '\n')) + { + // Don't include this character in the buffer + parse->length--; + + // Convert the checksum characters into binary + checksum = AsciiToNibble(parse->buffer[parse->nmeaLength - 2]) << 4; + checksum |= AsciiToNibble(parse->buffer[parse->nmeaLength - 1]); + + // Validate the checksum + if (checksum == parse->crc) + parse->crc = 0; + if (settings.enablePrintBadMessages && parse->crc && (!inMainMenu)) + printNmeaChecksumError(parse); + + // Process this message if CRC is valid + if (parse->crc == 0) + parse->eomCallback(parse, SENTENCE_TYPE_NMEA); + else + failedParserMessages_NMEA++; + + // Add this character to the beginning of the buffer + parse->length = 0; + parse->buffer[parse->length++] = data; + return gpsMessageParserFirstByte(parse, data); + } + return SENTENCE_TYPE_NMEA; +} diff --git a/Firmware/RTK_Surveyor/Parse_RTCM.ino b/Firmware/RTK_Surveyor/Parse_RTCM.ino new file mode 100644 index 000000000..e3402c1dd --- /dev/null +++ b/Firmware/RTK_Surveyor/Parse_RTCM.ino @@ -0,0 +1,134 @@ +/*------------------------------------------------------------------------------ +Parse_RTCM.ino + + RTCM message parsing support routines +------------------------------------------------------------------------------*/ + +// +// RTCM Standard 10403.2 - Chapter 4, Transport Layer +// +// |<------------- 3 bytes ------------>|<----- length ----->|<- 3 bytes ->| +// | | | | +// +----------+--------+----------------+---------+----------+-------------+ +// | Preamble | Fill | Message Length | Message | Fill | CRC-24Q | +// | 8 bits | 6 bits | 10 bits | n-bits | 0-7 bits | 24 bits | +// | 0xd3 | 000000 | (in bytes) | | zeros | | +// +----------+--------+----------------+---------+----------+-------------+ +// | | +// |<------------------------ CRC -------------------------->| +// + +// Check for the preamble +uint8_t rtcmPreamble(PARSE_STATE *parse, uint8_t data) +{ + if (data == 0xd3) + { + // Start the CRC with this byte + parse->crc = 0; + parse->crc = COMPUTE_CRC24Q(parse, data); + parse->computeCrc = true; + + // Get the message length + parse->state = rtcmReadLength1; + return SENTENCE_TYPE_RTCM; + } + return SENTENCE_TYPE_NONE; +} + +// Read the upper two bits of the length +uint8_t rtcmReadLength1(PARSE_STATE *parse, uint8_t data) +{ + // Verify the length byte - check the 6 MS bits are all zero + if (data & (~3)) + { + // Invalid length, place this byte at the beginning of the buffer + parse->length = 0; + parse->buffer[parse->length++] = data; + parse->computeCrc = false; + + // Start searching for a preamble byte + return gpsMessageParserFirstByte(parse, data); + } + + // Save the upper 2 bits of the length + parse->bytesRemaining = data << 8; + parse->state = rtcmReadLength2; + return SENTENCE_TYPE_RTCM; +} + +// Read the lower 8 bits of the length +uint8_t rtcmReadLength2(PARSE_STATE *parse, uint8_t data) +{ + parse->bytesRemaining |= data; + parse->state = rtcmReadMessage1; + return SENTENCE_TYPE_RTCM; +} + +// Read the upper 8 bits of the message number +uint8_t rtcmReadMessage1(PARSE_STATE *parse, uint8_t data) +{ + parse->message = data << 4; + parse->bytesRemaining -= 1; + parse->state = rtcmReadMessage2; + return SENTENCE_TYPE_RTCM; +} + +// Read the lower 4 bits of the message number +uint8_t rtcmReadMessage2(PARSE_STATE *parse, uint8_t data) +{ + parse->message |= data >> 4; + parse->bytesRemaining -= 1; + parse->state = rtcmReadData; + return SENTENCE_TYPE_RTCM; +} + +// Read the rest of the message +uint8_t rtcmReadData(PARSE_STATE *parse, uint8_t data) +{ + // Account for this data byte + parse->bytesRemaining -= 1; + + // Wait until all the data is received + if (parse->bytesRemaining <= 0) + { + parse->rtcmCrc = parse->crc & 0x00ffffff; + parse->bytesRemaining = 3; + parse->state = rtcmReadCrc; + } + return SENTENCE_TYPE_RTCM; +} + +// Read the CRC +uint8_t rtcmReadCrc(PARSE_STATE *parse, uint8_t data) +{ + // Account for this data byte + parse->bytesRemaining -= 1; + + // Wait until all the data is received + if (parse->bytesRemaining > 0) + return SENTENCE_TYPE_RTCM; + + // Update the maximum message length + if (parse->length > parse->maxLength) + { + parse->maxLength = parse->length; + printRtcmMaxLength(parse); + } + + // Display the RTCM messages with bad CRC + parse->crc &= 0x00ffffff; + if (settings.enablePrintBadMessages && parse->crc && (!inMainMenu)) + printRtcmChecksumError(parse); + + // Process the message if CRC is valid + if (parse->crc == 0) + parse->eomCallback(parse, SENTENCE_TYPE_RTCM); + else + failedParserMessages_RTCM++; + + // Search for another preamble byte + parse->length = 0; + parse->computeCrc = false; + parse->state = gpsMessageParserFirstByte; + return SENTENCE_TYPE_NONE; +} diff --git a/Firmware/RTK_Surveyor/Parse_UBLOX.ino b/Firmware/RTK_Surveyor/Parse_UBLOX.ino new file mode 100644 index 000000000..3d613acd8 --- /dev/null +++ b/Firmware/RTK_Surveyor/Parse_UBLOX.ino @@ -0,0 +1,155 @@ +/*------------------------------------------------------------------------------ +Parse_UBLOX.ino + + u-blox message parsing support routines +------------------------------------------------------------------------------*/ + +// +// U-BLOX Message +// +// |<-- Preamble --->| +// | | +// +--------+--------+---------+--------+---------+---------+--------+--------+ +// | SYNC | SYNC | Class | ID | Length | Payload | CK_A | CK_B | +// | 8 bits | 8 bits | 8 bits | 8 bits | 2 bytes | n bytes | 8 bits | 8 bits | +// | 0xb5 | 0x62 | | | | | | | +// +--------+--------+---------+--------+---------+---------+--------+--------+ +// | | +// |<------------- Checksum ------------->| +// +// 8-Bit Fletcher Algorithm, which is used in the TCP standard (RFC 1145) +// http://www.ietf.org/rfc/rfc1145.txt +// Checksum calculation +// Initialization: CK_A = CK_B = 0 +// CK_A += data +// CK_B += CK_A +// + +// Check for the preamble +uint8_t ubloxPreamble(PARSE_STATE *parse, uint8_t data) +{ + if (data == 0xb5) + { + parse->state = ubloxSync2; + return SENTENCE_TYPE_UBX; + } + return SENTENCE_TYPE_NONE; +} + +// Read the second sync byte +uint8_t ubloxSync2(PARSE_STATE *parse, uint8_t data) +{ + // Verify the sync 2 byte + if (data != 0x62) + { + // Display the invalid data + if (settings.enablePrintBadMessages && (!inMainMenu)) + printUbloxInvalidData(parse); + + // Invalid sync 2 byte, place this byte at the beginning of the buffer + parse->length = 0; + parse->buffer[parse->length++] = data; + + // Start searching for a preamble byte + return gpsMessageParserFirstByte(parse, data); + } + + parse->state = ubloxClass; + return SENTENCE_TYPE_UBX; +} + +// Read the class byte +uint8_t ubloxClass(PARSE_STATE *parse, uint8_t data) +{ + // Start the checksum calculation + parse->ck_a = data; + parse->ck_b = data; + + // Save the class as the upper 8-bits of the message + parse->message = ((uint16_t)data) << 8; + parse->state = ubloxId; + return SENTENCE_TYPE_UBX; +} + +// Read the ID byte +uint8_t ubloxId(PARSE_STATE *parse, uint8_t data) +{ + // Calculate the checksum + parse->ck_a += data; + parse->ck_b += parse->ck_a; + + // Save the ID as the lower 8-bits of the message + parse->message |= data; + parse->state = ubloxLength1; + return SENTENCE_TYPE_UBX; +} + +// Read the first length byte +uint8_t ubloxLength1(PARSE_STATE *parse, uint8_t data) +{ + // Calculate the checksum + parse->ck_a += data; + parse->ck_b += parse->ck_a; + + // Save the first length byte + parse->bytesRemaining = data; + parse->state = ubloxLength2; + return SENTENCE_TYPE_UBX; +} + +// Read the second length byte +uint8_t ubloxLength2(PARSE_STATE *parse, uint8_t data) +{ + // Calculate the checksum + parse->ck_a += data; + parse->ck_b += parse->ck_a; + + // Save the second length byte + parse->bytesRemaining |= ((uint16_t)data) << 8; + parse->state = ubloxPayload; + return SENTENCE_TYPE_UBX; +} + +// Read the payload +uint8_t ubloxPayload(PARSE_STATE *parse, uint8_t data) +{ + // Compute the checksum over the payload + if (parse->bytesRemaining--) + { + // Calculate the checksum + parse->ck_a += data; + parse->ck_b += parse->ck_a; + return SENTENCE_TYPE_UBX; + } + return ubloxCkA(parse, data); +} + +// Read the CK_A byte +uint8_t ubloxCkA(PARSE_STATE *parse, uint8_t data) +{ + parse->state = ubloxCkB; + return SENTENCE_TYPE_UBX; +} + +// Read the CK_B byte +uint8_t ubloxCkB(PARSE_STATE *parse, uint8_t data) +{ + bool badChecksum; + + // Validate the checksum + badChecksum = + ((parse->buffer[parse->length - 2] != parse->ck_a) || (parse->buffer[parse->length - 1] != parse->ck_b)); + if (settings.enablePrintBadMessages && badChecksum && (!inMainMenu)) + printUbloxChecksumError(parse); + + // Process this message if checksum is valid + if (badChecksum == false) + parse->eomCallback(parse, SENTENCE_TYPE_UBX); + else + failedParserMessages_UBX++; + + // Search for the next preamble byte + parse->length = 0; + parse->state = gpsMessageParserFirstByte; + return SENTENCE_TYPE_NONE; +} diff --git a/Firmware/RTK_Surveyor/Patch/Server.h b/Firmware/RTK_Surveyor/Patch/Server.h new file mode 100644 index 000000000..fc482ecc3 --- /dev/null +++ b/Firmware/RTK_Surveyor/Patch/Server.h @@ -0,0 +1,32 @@ +/* + Server.h - Base class that provides Server + Copyright (c) 2011 Adrian McEwen. All right reserved. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef server_h +#define server_h + +#include "Print.h" + +class Server: public Print +{ +public: + //virtual void begin(uint16_t port=0) =0; + void begin() {}; +}; + +#endif diff --git a/Firmware/RTK_Surveyor/Patch/WiFiGeneric.cpp b/Firmware/RTK_Surveyor/Patch/WiFiGeneric.cpp new file mode 100644 index 000000000..9104eab68 --- /dev/null +++ b/Firmware/RTK_Surveyor/Patch/WiFiGeneric.cpp @@ -0,0 +1,1484 @@ +/* + ESP8266WiFiGeneric.cpp - WiFi library for esp8266 + + Copyright (c) 2014 Ivan Grokhotkov. All rights reserved. + This file is part of the esp8266 core for Arduino environment. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + + Reworked on 28 Dec 2015 by Markus Sattler + + */ + +#include "WiFi.h" +#include "WiFiGeneric.h" + +extern "C" { +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include "lwip/ip_addr.h" +#include "lwip/opt.h" +#include "lwip/err.h" +#include "lwip/dns.h" +#include "dhcpserver/dhcpserver_options.h" + +} //extern "C" + +#include "esp32-hal.h" +#include +#include "sdkconfig.h" + +#define _byte_swap32(num) (((num>>24)&0xff) | ((num<<8)&0xff0000) | ((num>>8)&0xff00) | ((num<<24)&0xff000000)) +ESP_EVENT_DEFINE_BASE(ARDUINO_EVENTS); +/* + * Private (exposable) methods + * */ +static esp_netif_t* esp_netifs[ESP_IF_MAX] = {nullptr, nullptr, nullptr}; +esp_interface_t get_esp_netif_interface(esp_netif_t* esp_netif){ + for(int i=0; i(local_ip); + info.gw.addr = static_cast(gateway); + info.netmask.addr = static_cast(subnet); + + log_v("Configuring %s static IP: " IPSTR ", MASK: " IPSTR ", GW: " IPSTR, + interface == ESP_IF_WIFI_STA ? "Station" : + interface == ESP_IF_WIFI_AP ? "SoftAP" : "Ethernet", + IP2STR(&info.ip), IP2STR(&info.netmask), IP2STR(&info.gw)); + + esp_err_t err = ESP_OK; + if(interface != ESP_IF_WIFI_AP){ + err = esp_netif_dhcpc_get_status(esp_netif, &status); + if(err){ + log_e("DHCPC Get Status Failed! 0x%04x", err); + return err; + } + err = esp_netif_dhcpc_stop(esp_netif); + if(err && err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED){ + log_e("DHCPC Stop Failed! 0x%04x", err); + return err; + } + err = esp_netif_set_ip_info(esp_netif, &info); + if(err){ + log_e("Netif Set IP Failed! 0x%04x", err); + return err; + } + if(info.ip.addr == 0){ + err = esp_netif_dhcpc_start(esp_netif); + if(err){ + log_e("DHCPC Start Failed! 0x%04x", err); + return err; + } + } + } else { + err = esp_netif_dhcps_get_status(esp_netif, &status); + if(err){ + log_e("DHCPS Get Status Failed! 0x%04x", err); + return err; + } + err = esp_netif_dhcps_stop(esp_netif); + if(err && err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED){ + log_e("DHCPS Stop Failed! 0x%04x", err); + return err; + } + err = esp_netif_set_ip_info(esp_netif, &info); + if(err){ + log_e("Netif Set IP Failed! 0x%04x", err); + return err; + } + + dhcps_lease_t lease; + lease.enable = true; + uint8_t CIDR = WiFiGenericClass::calculateSubnetCIDR(subnet); + log_v("SoftAP: %s | Gateway: %s | DHCP Start: %s | Netmask: %s", local_ip.toString().c_str(), gateway.toString().c_str(), dhcp_lease_start.toString().c_str(), subnet.toString().c_str()); + // netmask must have room for at least 12 IP addresses (AP + GW + 10 DHCP Leasing addresses) + // netmask also must be limited to the last 8 bits of IPv4, otherwise this function won't work + // IDF NETIF checks netmask for the 3rd byte: https://github.com/espressif/esp-idf/blob/master/components/esp_netif/lwip/esp_netif_lwip.c#L1857-L1862 + if (CIDR > 28 || CIDR < 24) { + log_e("Bad netmask. It must be from /24 to /28 (255.255.255. 0<->240)"); + return ESP_FAIL; // ESP_FAIL if initializing failed + } + // The code below is ready for any netmask, not limited to 255.255.255.0 + uint32_t netmask = _byte_swap32(info.netmask.addr); + uint32_t ap_ipaddr = _byte_swap32(info.ip.addr); + uint32_t dhcp_ipaddr = _byte_swap32(static_cast(dhcp_lease_start)); + dhcp_ipaddr = dhcp_ipaddr == 0 ? ap_ipaddr + 1 : dhcp_ipaddr; + uint32_t leaseStartMax = ~netmask - 10; + // there will be 10 addresses for DHCP to lease + lease.start_ip.addr = dhcp_ipaddr; + lease.end_ip.addr = lease.start_ip.addr + 10; + // Check if local_ip is in the same subnet as the dhcp leasing range initial address + if ((ap_ipaddr & netmask) != (dhcp_ipaddr & netmask)) { + log_e("The AP IP address (%s) and the DHCP start address (%s) must be in the same subnet", + local_ip.toString().c_str(), IPAddress(_byte_swap32(dhcp_ipaddr)).toString().c_str()); + return ESP_FAIL; // ESP_FAIL if initializing failed + } + // prevents DHCP lease range to overflow subnet range + if ((dhcp_ipaddr & ~netmask) >= leaseStartMax) { + // make first DHCP lease addr stay in the begining of the netmask range + lease.start_ip.addr = (dhcp_ipaddr & netmask) + 1; + lease.end_ip.addr = lease.start_ip.addr + 10; + log_w("DHCP Lease out of range - Changing DHCP leasing start to %s", IPAddress(_byte_swap32(lease.start_ip.addr)).toString().c_str()); + } + // Check if local_ip is within DHCP range + if (ap_ipaddr >= lease.start_ip.addr && ap_ipaddr <= lease.end_ip.addr) { + log_e("The AP IP address (%s) can't be within the DHCP range (%s -- %s)", + local_ip.toString().c_str(), IPAddress(_byte_swap32(lease.start_ip.addr)).toString().c_str(), IPAddress(_byte_swap32(lease.end_ip.addr)).toString().c_str()); + return ESP_FAIL; // ESP_FAIL if initializing failed + } + // Check if gateway is within DHCP range + uint32_t gw_ipaddr = _byte_swap32(info.gw.addr); + bool gw_in_same_subnet = (gw_ipaddr & netmask) == (ap_ipaddr & netmask); + if (gw_in_same_subnet && gw_ipaddr >= lease.start_ip.addr && gw_ipaddr <= lease.end_ip.addr) { + log_e("The GatewayP address (%s) can't be within the DHCP range (%s -- %s)", + gateway.toString().c_str(), IPAddress(_byte_swap32(lease.start_ip.addr)).toString().c_str(), IPAddress(_byte_swap32(lease.end_ip.addr)).toString().c_str()); + return ESP_FAIL; // ESP_FAIL if initializing failed + } + // all done, just revert back byte order of DHCP lease range + lease.start_ip.addr = _byte_swap32(lease.start_ip.addr); + lease.end_ip.addr = _byte_swap32(lease.end_ip.addr); + log_v("DHCP Server Range: %s to %s", IPAddress(lease.start_ip.addr).toString().c_str(), IPAddress(lease.end_ip.addr).toString().c_str()); + err = tcpip_adapter_dhcps_option( + (tcpip_adapter_dhcp_option_mode_t)TCPIP_ADAPTER_OP_SET, + (tcpip_adapter_dhcp_option_id_t)ESP_NETIF_SUBNET_MASK, + (void*)&info.netmask.addr, sizeof(info.netmask.addr) + ); + if(err){ + log_e("DHCPS Set Netmask Failed! 0x%04x", err); + return err; + } + err = tcpip_adapter_dhcps_option( + (tcpip_adapter_dhcp_option_mode_t)TCPIP_ADAPTER_OP_SET, + (tcpip_adapter_dhcp_option_id_t)REQUESTED_IP_ADDRESS, + (void*)&lease, sizeof(dhcps_lease_t) + ); + if(err){ + log_e("DHCPS Set Lease Failed! 0x%04x", err); + return err; + } + err = esp_netif_dhcps_start(esp_netif); + if(err){ + log_e("DHCPS Start Failed! 0x%04x", err); + return err; + } + } + return err; +} + +esp_err_t set_esp_interface_dns(esp_interface_t interface, IPAddress main_dns=IPAddress(), IPAddress backup_dns=IPAddress(), IPAddress fallback_dns=IPAddress()){ + esp_netif_t *esp_netif = esp_netifs[interface]; + esp_netif_dns_info_t dns; + dns.ip.type = ESP_IPADDR_TYPE_V4; + dns.ip.u_addr.ip4.addr = static_cast(main_dns); + if(dns.ip.u_addr.ip4.addr && esp_netif_set_dns_info(esp_netif, ESP_NETIF_DNS_MAIN, &dns) != ESP_OK){ + log_e("Set Main DNS Failed!"); + return ESP_FAIL; + } + if(interface != ESP_IF_WIFI_AP){ + dns.ip.u_addr.ip4.addr = static_cast(backup_dns); + if(dns.ip.u_addr.ip4.addr && esp_netif_set_dns_info(esp_netif, ESP_NETIF_DNS_BACKUP, &dns) != ESP_OK){ + log_e("Set Backup DNS Failed!"); + return ESP_FAIL; + } + dns.ip.u_addr.ip4.addr = static_cast(fallback_dns); + if(dns.ip.u_addr.ip4.addr && esp_netif_set_dns_info(esp_netif, ESP_NETIF_DNS_FALLBACK, &dns) != ESP_OK){ + log_e("Set Fallback DNS Failed!"); + return ESP_FAIL; + } + } + return ESP_OK; +} + +#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_VERBOSE +static const char * auth_mode_str(int authmode) +{ + switch (authmode) { + case WIFI_AUTH_OPEN: + return ("OPEN"); + break; + case WIFI_AUTH_WEP: + return ("WEP"); + break; + case WIFI_AUTH_WPA_PSK: + return ("WPA_PSK"); + break; + case WIFI_AUTH_WPA2_PSK: + return ("WPA2_PSK"); + break; + case WIFI_AUTH_WPA_WPA2_PSK: + return ("WPA_WPA2_PSK"); + break; + case WIFI_AUTH_WPA2_ENTERPRISE: + return ("WPA2_ENTERPRISE"); + break; + case WIFI_AUTH_WPA3_PSK: + return ("WPA3_PSK"); + break; + case WIFI_AUTH_WPA2_WPA3_PSK: + return ("WPA2_WPA3_PSK"); + break; + case WIFI_AUTH_WAPI_PSK: + return ("WPAPI_PSK"); + break; + default: + break; + } + return ("UNKNOWN"); +} +#endif + +static char default_hostname[32] = {0,}; +static const char * get_esp_netif_hostname(){ + if(default_hostname[0] == 0){ + uint8_t eth_mac[6]; + esp_wifi_get_mac((wifi_interface_t)WIFI_IF_STA, eth_mac); + snprintf(default_hostname, 32, "%s%02X%02X%02X", CONFIG_IDF_TARGET "-", eth_mac[3], eth_mac[4], eth_mac[5]); + } + return (const char *)default_hostname; +} +static void set_esp_netif_hostname(const char * name){ + if(name){ + snprintf(default_hostname, 32, "%s", name); + } +} + +static xQueueHandle _arduino_event_queue; +static TaskHandle_t _arduino_event_task_handle = nullptr; +static EventGroupHandle_t _arduino_event_group = nullptr; + +static void _arduino_event_task(void * arg){ + arduino_event_t *data = nullptr; + for (;;) { + if(xQueueReceive(_arduino_event_queue, &data, portMAX_DELAY) == pdTRUE){ + WiFiGenericClass::_eventCallback(data); + free(data); + data = nullptr; + } + } + vTaskDelete(nullptr); + _arduino_event_task_handle = nullptr; +} + +esp_err_t postArduinoEvent(arduino_event_t *data) +{ + if(data == nullptr){ + return ESP_FAIL; + } + arduino_event_t * event = (arduino_event_t*)malloc(sizeof(arduino_event_t)); + if(event == nullptr){ + log_e("Arduino Event Malloc Failed!"); + return ESP_FAIL; + } + memcpy(event, data, sizeof(arduino_event_t)); + if (xQueueSend(_arduino_event_queue, &event, portMAX_DELAY) != pdPASS) { + log_e("Arduino Event Send Failed!"); + return ESP_FAIL; + } + return ESP_OK; +} + +static void _arduino_event_cb(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) { + arduino_event_t arduino_event; + arduino_event.event_id = ARDUINO_EVENT_MAX; + + /* + * STA + * */ + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) { + log_v("STA Started"); + arduino_event.event_id = ARDUINO_EVENT_WIFI_STA_START; + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_STOP) { + log_v("STA Stopped"); + arduino_event.event_id = ARDUINO_EVENT_WIFI_STA_STOP; + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_AUTHMODE_CHANGE) { + #if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_VERBOSE + wifi_event_sta_authmode_change_t * event = (wifi_event_sta_authmode_change_t*)event_data; + log_v("STA Auth Mode Changed: From: %s, To: %s", auth_mode_str(event->old_mode), auth_mode_str(event->new_mode)); + #endif + arduino_event.event_id = ARDUINO_EVENT_WIFI_STA_AUTHMODE_CHANGE; + memcpy(&arduino_event.event_info.wifi_sta_authmode_change, event_data, sizeof(wifi_event_sta_authmode_change_t)); + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_CONNECTED) { + #if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_VERBOSE + wifi_event_sta_connected_t * event = (wifi_event_sta_connected_t*)event_data; + log_v("STA Connected: SSID: %s, BSSID: " MACSTR ", Channel: %u, Auth: %s", event->ssid, MAC2STR(event->bssid), event->channel, auth_mode_str(event->authmode)); + #endif + arduino_event.event_id = ARDUINO_EVENT_WIFI_STA_CONNECTED; + memcpy(&arduino_event.event_info.wifi_sta_connected, event_data, sizeof(wifi_event_sta_connected_t)); + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { + #if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_VERBOSE + wifi_event_sta_disconnected_t * event = (wifi_event_sta_disconnected_t*)event_data; + log_v("STA Disconnected: SSID: %s, BSSID: " MACSTR ", Reason: %u", event->ssid, MAC2STR(event->bssid), event->reason); + #endif + arduino_event.event_id = ARDUINO_EVENT_WIFI_STA_DISCONNECTED; + memcpy(&arduino_event.event_info.wifi_sta_disconnected, event_data, sizeof(wifi_event_sta_disconnected_t)); + } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { + #if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_VERBOSE + ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data; + log_v("STA Got %sIP:" IPSTR, event->ip_changed?"New ":"Same ", IP2STR(&event->ip_info.ip)); + #endif + arduino_event.event_id = ARDUINO_EVENT_WIFI_STA_GOT_IP; + memcpy(&arduino_event.event_info.got_ip, event_data, sizeof(ip_event_got_ip_t)); + } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_LOST_IP) { + log_v("STA IP Lost"); + arduino_event.event_id = ARDUINO_EVENT_WIFI_STA_LOST_IP; + + /* + * SCAN + * */ + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_SCAN_DONE) { + #if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_VERBOSE + wifi_event_sta_scan_done_t * event = (wifi_event_sta_scan_done_t*)event_data; + log_v("SCAN Done: ID: %u, Status: %u, Results: %u", event->scan_id, event->status, event->number); + #endif + arduino_event.event_id = ARDUINO_EVENT_WIFI_SCAN_DONE; + memcpy(&arduino_event.event_info.wifi_scan_done, event_data, sizeof(wifi_event_sta_scan_done_t)); + + /* + * AP + * */ + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_START) { + log_v("AP Started"); + arduino_event.event_id = ARDUINO_EVENT_WIFI_AP_START; + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STOP) { + log_v("AP Stopped"); + arduino_event.event_id = ARDUINO_EVENT_WIFI_AP_STOP; + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_PROBEREQRECVED) { + #if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_VERBOSE + wifi_event_ap_probe_req_rx_t * event = (wifi_event_ap_probe_req_rx_t*)event_data; + log_v("AP Probe Request: RSSI: %d, MAC: " MACSTR, event->rssi, MAC2STR(event->mac)); + #endif + arduino_event.event_id = ARDUINO_EVENT_WIFI_AP_PROBEREQRECVED; + memcpy(&arduino_event.event_info.wifi_ap_probereqrecved, event_data, sizeof(wifi_event_ap_probe_req_rx_t)); + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STACONNECTED) { + #if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_VERBOSE + wifi_event_ap_staconnected_t* event = (wifi_event_ap_staconnected_t*) event_data; + log_v("AP Station Connected: MAC: " MACSTR ", AID: %d", MAC2STR(event->mac), event->aid); + #endif + arduino_event.event_id = ARDUINO_EVENT_WIFI_AP_STACONNECTED; + memcpy(&arduino_event.event_info.wifi_ap_staconnected, event_data, sizeof(wifi_event_ap_staconnected_t)); + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STADISCONNECTED) { + #if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_VERBOSE + wifi_event_ap_stadisconnected_t* event = (wifi_event_ap_stadisconnected_t*) event_data; + log_v("AP Station Disconnected: MAC: " MACSTR ", AID: %d", MAC2STR(event->mac), event->aid); + #endif + arduino_event.event_id = ARDUINO_EVENT_WIFI_AP_STADISCONNECTED; + memcpy(&arduino_event.event_info.wifi_ap_stadisconnected, event_data, sizeof(wifi_event_ap_stadisconnected_t)); + } else if (event_base == IP_EVENT && event_id == IP_EVENT_AP_STAIPASSIGNED) { + #if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_VERBOSE + ip_event_ap_staipassigned_t * event = (ip_event_ap_staipassigned_t*)event_data; + log_v("AP Station IP Assigned:" IPSTR, IP2STR(&event->ip)); + #endif + arduino_event.event_id = ARDUINO_EVENT_WIFI_AP_STAIPASSIGNED; + memcpy(&arduino_event.event_info.wifi_ap_staipassigned, event_data, sizeof(ip_event_ap_staipassigned_t)); + + /* + * ETH + * */ + } else if (event_base == ETH_EVENT && event_id == ETHERNET_EVENT_CONNECTED) { + log_v("Ethernet Link Up"); + arduino_event.event_id = ARDUINO_EVENT_ETH_CONNECTED; + memcpy(&arduino_event.event_info.eth_connected, event_data, sizeof(esp_eth_handle_t)); + } else if (event_base == ETH_EVENT && event_id == ETHERNET_EVENT_DISCONNECTED) { + log_v("Ethernet Link Down"); + arduino_event.event_id = ARDUINO_EVENT_ETH_DISCONNECTED; + } else if (event_base == ETH_EVENT && event_id == ETHERNET_EVENT_START) { + log_v("Ethernet Started"); + arduino_event.event_id = ARDUINO_EVENT_ETH_START; + } else if (event_base == ETH_EVENT && event_id == ETHERNET_EVENT_STOP) { + log_v("Ethernet Stopped"); + arduino_event.event_id = ARDUINO_EVENT_ETH_STOP; + } else if (event_base == IP_EVENT && event_id == IP_EVENT_ETH_GOT_IP) { + #if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_VERBOSE + ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data; + log_v("Ethernet got %sip:" IPSTR, event->ip_changed?"new":"", IP2STR(&event->ip_info.ip)); + #endif + arduino_event.event_id = ARDUINO_EVENT_ETH_GOT_IP; + memcpy(&arduino_event.event_info.got_ip, event_data, sizeof(ip_event_got_ip_t)); + + /* + * IPv6 + * */ + } else if (event_base == IP_EVENT && event_id == IP_EVENT_GOT_IP6) { + ip_event_got_ip6_t * event = (ip_event_got_ip6_t*)event_data; + esp_interface_t iface = get_esp_netif_interface(event->esp_netif); + log_v("IF[%d] Got IPv6: IP Index: %d, Zone: %d, " IPV6STR, iface, event->ip_index, event->ip6_info.ip.zone, IPV62STR(event->ip6_info.ip)); + memcpy(&arduino_event.event_info.got_ip6, event_data, sizeof(ip_event_got_ip6_t)); + if(iface == ESP_IF_WIFI_STA){ + arduino_event.event_id = ARDUINO_EVENT_WIFI_STA_GOT_IP6; + } else if(iface == ESP_IF_WIFI_AP){ + arduino_event.event_id = ARDUINO_EVENT_WIFI_AP_GOT_IP6; + } else if(iface == ESP_IF_ETH){ + arduino_event.event_id = ARDUINO_EVENT_ETH_GOT_IP6; + } + + /* + * WPS + * */ + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_WPS_ER_SUCCESS) { + arduino_event.event_id = ARDUINO_EVENT_WPS_ER_SUCCESS; + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_WPS_ER_FAILED) { + arduino_event.event_id = ARDUINO_EVENT_WPS_ER_FAILED; + memcpy(&arduino_event.event_info.wps_fail_reason, event_data, sizeof(wifi_event_sta_wps_fail_reason_t)); + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_WPS_ER_TIMEOUT) { + arduino_event.event_id = ARDUINO_EVENT_WPS_ER_TIMEOUT; + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_WPS_ER_PIN) { + arduino_event.event_id = ARDUINO_EVENT_WPS_ER_PIN; + memcpy(&arduino_event.event_info.wps_er_pin, event_data, sizeof(wifi_event_sta_wps_er_pin_t)); + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_WPS_ER_PBC_OVERLAP) { + arduino_event.event_id = ARDUINO_EVENT_WPS_ER_PBC_OVERLAP; + + /* + * FTM + * */ + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_FTM_REPORT) { + arduino_event.event_id = ARDUINO_EVENT_WIFI_FTM_REPORT; + memcpy(&arduino_event.event_info.wifi_ftm_report, event_data, sizeof(wifi_event_ftm_report_t)); + + + /* + * SMART CONFIG + * */ + } else if (event_base == SC_EVENT && event_id == SC_EVENT_SCAN_DONE) { + log_v("SC Scan Done"); + arduino_event.event_id = ARDUINO_EVENT_SC_SCAN_DONE; + } else if (event_base == SC_EVENT && event_id == SC_EVENT_FOUND_CHANNEL) { + log_v("SC Found Channel"); + arduino_event.event_id = ARDUINO_EVENT_SC_FOUND_CHANNEL; + } else if (event_base == SC_EVENT && event_id == SC_EVENT_GOT_SSID_PSWD) { + #if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_ERROR + smartconfig_event_got_ssid_pswd_t *event = (smartconfig_event_got_ssid_pswd_t *)event_data; + log_v("SC: SSID: %s, Password: %s", (const char *)event->ssid, (const char *)event->password); + #endif + arduino_event.event_id = ARDUINO_EVENT_SC_GOT_SSID_PSWD; + memcpy(&arduino_event.event_info.sc_got_ssid_pswd, event_data, sizeof(smartconfig_event_got_ssid_pswd_t)); + + } else if (event_base == SC_EVENT && event_id == SC_EVENT_SEND_ACK_DONE) { + log_v("SC Send Ack Done"); + arduino_event.event_id = ARDUINO_EVENT_SC_SEND_ACK_DONE; + + /* + * Provisioning + * */ + } else if (event_base == WIFI_PROV_EVENT && event_id == WIFI_PROV_INIT) { + log_v("Provisioning Initialized!"); + arduino_event.event_id = ARDUINO_EVENT_PROV_INIT; + } else if (event_base == WIFI_PROV_EVENT && event_id == WIFI_PROV_DEINIT) { + log_v("Provisioning Uninitialized!"); + arduino_event.event_id = ARDUINO_EVENT_PROV_DEINIT; + } else if (event_base == WIFI_PROV_EVENT && event_id == WIFI_PROV_START) { + log_v("Provisioning Start!"); + arduino_event.event_id = ARDUINO_EVENT_PROV_START; + } else if (event_base == WIFI_PROV_EVENT && event_id == WIFI_PROV_END) { + log_v("Provisioning End!"); + wifi_prov_mgr_deinit(); + arduino_event.event_id = ARDUINO_EVENT_PROV_END; + } else if (event_base == WIFI_PROV_EVENT && event_id == WIFI_PROV_CRED_RECV) { + #if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_VERBOSE + wifi_sta_config_t *event = (wifi_sta_config_t *)event_data; + log_v("Provisioned Credentials: SSID: %s, Password: %s", (const char *) event->ssid, (const char *) event->password); + #endif + arduino_event.event_id = ARDUINO_EVENT_PROV_CRED_RECV; + memcpy(&arduino_event.event_info.prov_cred_recv, event_data, sizeof(wifi_sta_config_t)); + } else if (event_base == WIFI_PROV_EVENT && event_id == WIFI_PROV_CRED_FAIL) { + #if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_ERROR + wifi_prov_sta_fail_reason_t *reason = (wifi_prov_sta_fail_reason_t *)event_data; + log_e("Provisioning Failed: Reason : %s", (*reason == WIFI_PROV_STA_AUTH_ERROR)?"Authentication Failed":"AP Not Found"); + #endif + arduino_event.event_id = ARDUINO_EVENT_PROV_CRED_FAIL; + memcpy(&arduino_event.event_info.prov_fail_reason, event_data, sizeof(wifi_prov_sta_fail_reason_t)); + } else if (event_base == WIFI_PROV_EVENT && event_id == WIFI_PROV_CRED_SUCCESS) { + log_v("Provisioning Success!"); + arduino_event.event_id = ARDUINO_EVENT_PROV_CRED_SUCCESS; + } + + if(arduino_event.event_id < ARDUINO_EVENT_MAX){ + postArduinoEvent(&arduino_event); + } +} + +static bool _start_network_event_task(){ + if(!_arduino_event_group){ + _arduino_event_group = xEventGroupCreate(); + if(!_arduino_event_group){ + log_e("Network Event Group Create Failed!"); + return false; + } + xEventGroupSetBits(_arduino_event_group, WIFI_DNS_IDLE_BIT); + } + if(!_arduino_event_queue){ + _arduino_event_queue = xQueueCreate(32, sizeof(arduino_event_t*)); + if(!_arduino_event_queue){ + log_e("Network Event Queue Create Failed!"); + return false; + } + } + + esp_err_t err = esp_event_loop_create_default(); + if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { + log_e("esp_event_loop_create_default failed!"); + return err; + } + + if(!_arduino_event_task_handle){ + xTaskCreateUniversal(_arduino_event_task, "arduino_events", 4096, nullptr, ESP_TASKD_EVENT_PRIO - 1, &_arduino_event_task_handle, ARDUINO_EVENT_RUNNING_CORE); + if(!_arduino_event_task_handle){ + log_e("Network Event Task Start Failed!"); + return false; + } + } + + if(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &_arduino_event_cb, nullptr, nullptr)){ + log_e("event_handler_instance_register for WIFI_EVENT Failed!"); + return false; + } + + if(esp_event_handler_instance_register(IP_EVENT, ESP_EVENT_ANY_ID, &_arduino_event_cb, nullptr, nullptr)){ + log_e("event_handler_instance_register for IP_EVENT Failed!"); + return false; + } + + if(esp_event_handler_instance_register(SC_EVENT, ESP_EVENT_ANY_ID, &_arduino_event_cb, nullptr, nullptr)){ + log_e("event_handler_instance_register for SC_EVENT Failed!"); + return false; + } + + if(esp_event_handler_instance_register(ETH_EVENT, ESP_EVENT_ANY_ID, &_arduino_event_cb, nullptr, nullptr)){ + log_e("event_handler_instance_register for ETH_EVENT Failed!"); + return false; + } + + if(esp_event_handler_instance_register(WIFI_PROV_EVENT, ESP_EVENT_ANY_ID, &_arduino_event_cb, nullptr, nullptr)){ + log_e("event_handler_instance_register for WIFI_PROV_EVENT Failed!"); + return false; + } + + return true; +} + +bool tcpipInit(){ + static bool initialized = false; + if(!initialized){ + initialized = true; +#if CONFIG_IDF_TARGET_ESP32 + uint8_t mac[8]; + if(esp_efuse_mac_get_default(mac) == ESP_OK){ + esp_base_mac_addr_set(mac); + } +#endif + initialized = esp_netif_init() == ESP_OK; + if(initialized){ + initialized = _start_network_event_task(); + } else { + log_e("esp_netif_init failed!"); + } + } + return initialized; +} + +/* + * WiFi INIT + * */ + +static bool lowLevelInitDone = false; +bool WiFiGenericClass::_wifiUseStaticBuffers = false; + +bool WiFiGenericClass::useStaticBuffers(){ + return _wifiUseStaticBuffers; +} + +void WiFiGenericClass::useStaticBuffers(bool bufferMode){ + if (lowLevelInitDone) { + log_w("WiFi already started. Call WiFi.mode(WIFI_MODE_NULL) before setting Static Buffer Mode."); + } + _wifiUseStaticBuffers = bufferMode; +} + +// Temporary fix to ensure that CDC+JTAG stay on on ESP32-C3 +#if CONFIG_IDF_TARGET_ESP32C3 +extern "C" void phy_bbpll_en_usb(bool en); +#endif + +bool wifiLowLevelInit(bool persistent){ + if(!lowLevelInitDone){ + lowLevelInitDone = true; + if(!tcpipInit()){ + lowLevelInitDone = false; + return lowLevelInitDone; + } + if(esp_netifs[ESP_IF_WIFI_AP] == nullptr){ + esp_netifs[ESP_IF_WIFI_AP] = esp_netif_create_default_wifi_ap(); + } + if(esp_netifs[ESP_IF_WIFI_STA] == nullptr){ + esp_netifs[ESP_IF_WIFI_STA] = esp_netif_create_default_wifi_sta(); + } + + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + + if(!WiFiGenericClass::useStaticBuffers()) { + cfg.static_tx_buf_num = 0; + cfg.dynamic_tx_buf_num = 32; + cfg.tx_buf_type = 1; + cfg.cache_tx_buf_num = 4; // can't be zero! + cfg.static_rx_buf_num = 4; + cfg.dynamic_rx_buf_num = 32; + } + + esp_err_t err = esp_wifi_init(&cfg); + if(err){ + log_e("esp_wifi_init %d", err); + lowLevelInitDone = false; + return lowLevelInitDone; + } +// Temporary fix to ensure that CDC+JTAG stay on on ESP32-C3 +#if CONFIG_IDF_TARGET_ESP32C3 + phy_bbpll_en_usb(true); +#endif + if(!persistent){ + lowLevelInitDone = esp_wifi_set_storage(WIFI_STORAGE_RAM) == ESP_OK; + } + if(lowLevelInitDone){ + arduino_event_t arduino_event; + arduino_event.event_id = ARDUINO_EVENT_WIFI_READY; + postArduinoEvent(&arduino_event); + } + } + return lowLevelInitDone; +} + +static bool wifiLowLevelDeinit(){ + if(lowLevelInitDone){ + lowLevelInitDone = !(esp_wifi_deinit() == ESP_OK); + } + return !lowLevelInitDone; +} + +static bool _esp_wifi_started = false; + +static bool espWiFiStart(){ + if(_esp_wifi_started){ + return true; + } + _esp_wifi_started = true; + esp_err_t err = esp_wifi_start(); + if (err != ESP_OK) { + _esp_wifi_started = false; + log_e("esp_wifi_start %d", err); + return _esp_wifi_started; + } + return _esp_wifi_started; +} + +static bool espWiFiStop(){ + esp_err_t err; + if(!_esp_wifi_started){ + return true; + } + _esp_wifi_started = false; + err = esp_wifi_stop(); + if(err){ + log_e("Could not stop WiFi! %d", err); + _esp_wifi_started = true; + return false; + } + return wifiLowLevelDeinit(); +} + +// ----------------------------------------------------------------------------------------------------------------------- +// ------------------------------------------------- Generic WiFi function ----------------------------------------------- +// ----------------------------------------------------------------------------------------------------------------------- + +typedef struct WiFiEventCbList { + static wifi_event_id_t current_id; + wifi_event_id_t id; + WiFiEventCb cb; + WiFiEventFuncCb fcb; + WiFiEventSysCb scb; + arduino_event_id_t event; + + WiFiEventCbList() : id(current_id++), cb(nullptr), fcb(nullptr), scb(nullptr), event(ARDUINO_EVENT_WIFI_READY) {} +} WiFiEventCbList_t; +wifi_event_id_t WiFiEventCbList::current_id = 1; + + +// arduino dont like std::vectors move static here +static std::vector cbEventList; + +bool WiFiGenericClass::_persistent = true; +bool WiFiGenericClass::_long_range = false; +wifi_mode_t WiFiGenericClass::_forceSleepLastMode = WIFI_MODE_NULL; +#if CONFIG_IDF_TARGET_ESP32S2 +wifi_ps_type_t WiFiGenericClass::_sleepEnabled = WIFI_PS_NONE; +#else +wifi_ps_type_t WiFiGenericClass::_sleepEnabled = WIFI_PS_MIN_MODEM; +#endif + +WiFiGenericClass::WiFiGenericClass() +{ +} + +const char * WiFiGenericClass::getHostname() +{ + return get_esp_netif_hostname(); +} + +bool WiFiGenericClass::setHostname(const char * hostname) +{ + set_esp_netif_hostname(hostname); + return true; +} + +int WiFiGenericClass::setStatusBits(int bits){ + if(!_arduino_event_group){ + return 0; + } + return xEventGroupSetBits(_arduino_event_group, bits); +} + +int WiFiGenericClass::clearStatusBits(int bits){ + if(!_arduino_event_group){ + return 0; + } + return xEventGroupClearBits(_arduino_event_group, bits); +} + +int WiFiGenericClass::getStatusBits(){ + if(!_arduino_event_group){ + return 0; + } + return xEventGroupGetBits(_arduino_event_group); +} + +int WiFiGenericClass::waitStatusBits(int bits, uint32_t timeout_ms){ + if(!_arduino_event_group){ + return 0; + } + return xEventGroupWaitBits( + _arduino_event_group, // The event group being tested. + bits, // The bits within the event group to wait for. + pdFALSE, // BIT_0 and BIT_4 should be cleared before returning. + pdTRUE, // Don't wait for both bits, either bit will do. + timeout_ms / portTICK_PERIOD_MS ) & bits; // Wait a maximum of 100ms for either bit to be set. +} + +/** + * set callback function + * @param cbEvent WiFiEventCb + * @param event optional filter (WIFI_EVENT_MAX is all events) + */ +wifi_event_id_t WiFiGenericClass::onEvent(WiFiEventCb cbEvent, arduino_event_id_t event) +{ + if(!cbEvent) { + return 0; + } + WiFiEventCbList_t newEventHandler; + newEventHandler.cb = cbEvent; + newEventHandler.fcb = nullptr; + newEventHandler.scb = nullptr; + newEventHandler.event = event; + cbEventList.push_back(newEventHandler); + return newEventHandler.id; +} + +wifi_event_id_t WiFiGenericClass::onEvent(WiFiEventFuncCb cbEvent, arduino_event_id_t event) +{ + if(!cbEvent) { + return 0; + } + WiFiEventCbList_t newEventHandler; + newEventHandler.cb = nullptr; + newEventHandler.fcb = cbEvent; + newEventHandler.scb = nullptr; + newEventHandler.event = event; + cbEventList.push_back(newEventHandler); + return newEventHandler.id; +} + +wifi_event_id_t WiFiGenericClass::onEvent(WiFiEventSysCb cbEvent, arduino_event_id_t event) +{ + if(!cbEvent) { + return 0; + } + WiFiEventCbList_t newEventHandler; + newEventHandler.cb = nullptr; + newEventHandler.fcb = nullptr; + newEventHandler.scb = cbEvent; + newEventHandler.event = event; + cbEventList.push_back(newEventHandler); + return newEventHandler.id; +} + +/** + * removes a callback form event handler + * @param cbEvent WiFiEventCb + * @param event optional filter (WIFI_EVENT_MAX is all events) + */ +void WiFiGenericClass::removeEvent(WiFiEventCb cbEvent, arduino_event_id_t event) +{ + if(!cbEvent) { + return; + } + + for(uint32_t i = 0; i < cbEventList.size(); i++) { + WiFiEventCbList_t entry = cbEventList[i]; + if(entry.cb == cbEvent && entry.event == event) { + cbEventList.erase(cbEventList.begin() + i); + } + } +} + +void WiFiGenericClass::removeEvent(WiFiEventSysCb cbEvent, arduino_event_id_t event) +{ + if(!cbEvent) { + return; + } + + for(uint32_t i = 0; i < cbEventList.size(); i++) { + WiFiEventCbList_t entry = cbEventList[i]; + if(entry.scb == cbEvent && entry.event == event) { + cbEventList.erase(cbEventList.begin() + i); + } + } +} + +void WiFiGenericClass::removeEvent(wifi_event_id_t id) +{ + for(uint32_t i = 0; i < cbEventList.size(); i++) { + WiFiEventCbList_t entry = cbEventList[i]; + if(entry.id == id) { + cbEventList.erase(cbEventList.begin() + i); + } + } +} + +/** + * callback for WiFi events + * @param arg + */ +#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_DEBUG +const char * arduino_event_names[] = { + "WIFI_READY", + "SCAN_DONE", + "STA_START", "STA_STOP", "STA_CONNECTED", "STA_DISCONNECTED", "STA_AUTHMODE_CHANGE", "STA_GOT_IP", "STA_GOT_IP6", "STA_LOST_IP", + "AP_START", "AP_STOP", "AP_STACONNECTED", "AP_STADISCONNECTED", "AP_STAIPASSIGNED", "AP_PROBEREQRECVED", "AP_GOT_IP6", + "FTM_REPORT", + "ETH_START", "ETH_STOP", "ETH_CONNECTED", "ETH_DISCONNECTED", "ETH_GOT_IP", "ETH_GOT_IP6", + "WPS_ER_SUCCESS", "WPS_ER_FAILED", "WPS_ER_TIMEOUT", "WPS_ER_PIN", "WPS_ER_PBC_OVERLAP", + "SC_SCAN_DONE", "SC_FOUND_CHANNEL", "SC_GOT_SSID_PSWD", "SC_SEND_ACK_DONE", + "PROV_INIT", "PROV_DEINIT", "PROV_START", "PROV_END", "PROV_CRED_RECV", "PROV_CRED_FAIL", "PROV_CRED_SUCCESS" +}; +#endif +#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_WARN +const char * system_event_reasons[] = { "UNSPECIFIED", "AUTH_EXPIRE", "AUTH_LEAVE", "ASSOC_EXPIRE", "ASSOC_TOOMANY", "NOT_AUTHED", "NOT_ASSOCED", "ASSOC_LEAVE", "ASSOC_NOT_AUTHED", "DISASSOC_PWRCAP_BAD", "DISASSOC_SUPCHAN_BAD", "UNSPECIFIED", "IE_INVALID", "MIC_FAILURE", "4WAY_HANDSHAKE_TIMEOUT", "GROUP_KEY_UPDATE_TIMEOUT", "IE_IN_4WAY_DIFFERS", "GROUP_CIPHER_INVALID", "PAIRWISE_CIPHER_INVALID", "AKMP_INVALID", "UNSUPP_RSN_IE_VERSION", "INVALID_RSN_IE_CAP", "802_1X_AUTH_FAILED", "CIPHER_SUITE_REJECTED", "BEACON_TIMEOUT", "NO_AP_FOUND", "AUTH_FAIL", "ASSOC_FAIL", "HANDSHAKE_TIMEOUT", "CONNECTION_FAIL" }; +#define reason2str(r) ((r>176)?system_event_reasons[r-176]:system_event_reasons[r-1]) +#endif +esp_err_t WiFiGenericClass::_eventCallback(arduino_event_t *event) +{ + static bool first_connect = true; + + if(event->event_id < ARDUINO_EVENT_MAX) { + log_d("Arduino Event: %d - %s", event->event_id, arduino_event_names[event->event_id]); + } + if(event->event_id == ARDUINO_EVENT_WIFI_SCAN_DONE) { + WiFiScanClass::_scanDone(); + + } else if(event->event_id == ARDUINO_EVENT_WIFI_STA_START) { + WiFiSTAClass::_setStatus(WL_DISCONNECTED); + setStatusBits(STA_STARTED_BIT); + if(esp_wifi_set_ps(_sleepEnabled) != ESP_OK){ + log_e("esp_wifi_set_ps failed"); + } + } else if(event->event_id == ARDUINO_EVENT_WIFI_STA_STOP) { + WiFiSTAClass::_setStatus(WL_NO_SHIELD); + clearStatusBits(STA_STARTED_BIT | STA_CONNECTED_BIT | STA_HAS_IP_BIT | STA_HAS_IP6_BIT); + } else if(event->event_id == ARDUINO_EVENT_WIFI_STA_CONNECTED) { + WiFiSTAClass::_setStatus(WL_IDLE_STATUS); + setStatusBits(STA_CONNECTED_BIT); + + //esp_netif_create_ip6_linklocal(esp_netifs[ESP_IF_WIFI_STA]); + } else if(event->event_id == ARDUINO_EVENT_WIFI_STA_DISCONNECTED) { + uint8_t reason = event->event_info.wifi_sta_disconnected.reason; + log_w("Reason: %u - %s", reason, reason2str(reason)); + if(reason == WIFI_REASON_NO_AP_FOUND) { + WiFiSTAClass::_setStatus(WL_NO_SSID_AVAIL); + } else if((reason == WIFI_REASON_AUTH_FAIL) && !first_connect){ + WiFiSTAClass::_setStatus(WL_CONNECT_FAILED); + } else if(reason == WIFI_REASON_BEACON_TIMEOUT || reason == WIFI_REASON_HANDSHAKE_TIMEOUT) { + WiFiSTAClass::_setStatus(WL_CONNECTION_LOST); + } else if(reason == WIFI_REASON_AUTH_EXPIRE) { + + } else { + WiFiSTAClass::_setStatus(WL_DISCONNECTED); + } + clearStatusBits(STA_CONNECTED_BIT | STA_HAS_IP_BIT | STA_HAS_IP6_BIT); + if(first_connect && ((reason == WIFI_REASON_AUTH_EXPIRE) || + (reason >= WIFI_REASON_BEACON_TIMEOUT))) + { + log_d("WiFi Reconnect Running"); + WiFi.disconnect(); + WiFi.begin(); + first_connect = false; + } + else if(WiFi.getAutoReconnect()){ + if((reason == WIFI_REASON_AUTH_EXPIRE) || + (reason >= WIFI_REASON_BEACON_TIMEOUT && reason != WIFI_REASON_AUTH_FAIL)) + { + log_d("WiFi AutoReconnect Running"); + WiFi.disconnect(); + WiFi.begin(); + } + } + else if (reason == WIFI_REASON_ASSOC_FAIL){ + WiFiSTAClass::_setStatus(WL_CONNECT_FAILED); + } + } else if(event->event_id == ARDUINO_EVENT_WIFI_STA_GOT_IP) { +#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_DEBUG + uint8_t * ip = (uint8_t *)&(event->event_info.got_ip.ip_info.ip.addr); + uint8_t * mask = (uint8_t *)&(event->event_info.got_ip.ip_info.netmask.addr); + uint8_t * gw = (uint8_t *)&(event->event_info.got_ip.ip_info.gw.addr); + log_d("STA IP: %u.%u.%u.%u, MASK: %u.%u.%u.%u, GW: %u.%u.%u.%u", + ip[0], ip[1], ip[2], ip[3], + mask[0], mask[1], mask[2], mask[3], + gw[0], gw[1], gw[2], gw[3]); +#endif + WiFiSTAClass::_setStatus(WL_CONNECTED); + setStatusBits(STA_HAS_IP_BIT | STA_CONNECTED_BIT); + } else if(event->event_id == ARDUINO_EVENT_WIFI_STA_LOST_IP) { + WiFiSTAClass::_setStatus(WL_IDLE_STATUS); + clearStatusBits(STA_HAS_IP_BIT); + + } else if(event->event_id == ARDUINO_EVENT_WIFI_AP_START) { + setStatusBits(AP_STARTED_BIT); + } else if(event->event_id == ARDUINO_EVENT_WIFI_AP_STOP) { + clearStatusBits(AP_STARTED_BIT | AP_HAS_CLIENT_BIT); + } else if(event->event_id == ARDUINO_EVENT_WIFI_AP_STACONNECTED) { + setStatusBits(AP_HAS_CLIENT_BIT); + } else if(event->event_id == ARDUINO_EVENT_WIFI_AP_STADISCONNECTED) { + wifi_sta_list_t clients; + if(esp_wifi_ap_get_sta_list(&clients) != ESP_OK || !clients.num){ + clearStatusBits(AP_HAS_CLIENT_BIT); + } + + } else if(event->event_id == ARDUINO_EVENT_ETH_START) { + setStatusBits(ETH_STARTED_BIT); + } else if(event->event_id == ARDUINO_EVENT_ETH_STOP) { + clearStatusBits(ETH_STARTED_BIT | ETH_CONNECTED_BIT | ETH_HAS_IP_BIT | ETH_HAS_IP6_BIT); + } else if(event->event_id == ARDUINO_EVENT_ETH_CONNECTED) { + setStatusBits(ETH_CONNECTED_BIT); + } else if(event->event_id == ARDUINO_EVENT_ETH_DISCONNECTED) { + clearStatusBits(ETH_CONNECTED_BIT | ETH_HAS_IP_BIT | ETH_HAS_IP6_BIT); + } else if(event->event_id == ARDUINO_EVENT_ETH_GOT_IP) { +#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_DEBUG + uint8_t * ip = (uint8_t *)&(event->event_info.got_ip.ip_info.ip.addr); + uint8_t * mask = (uint8_t *)&(event->event_info.got_ip.ip_info.netmask.addr); + uint8_t * gw = (uint8_t *)&(event->event_info.got_ip.ip_info.gw.addr); + log_d("ETH IP: %u.%u.%u.%u, MASK: %u.%u.%u.%u, GW: %u.%u.%u.%u", + ip[0], ip[1], ip[2], ip[3], + mask[0], mask[1], mask[2], mask[3], + gw[0], gw[1], gw[2], gw[3]); +#endif + setStatusBits(ETH_CONNECTED_BIT | ETH_HAS_IP_BIT); + + } else if(event->event_id == ARDUINO_EVENT_WIFI_STA_GOT_IP6) { + setStatusBits(STA_CONNECTED_BIT | STA_HAS_IP6_BIT); + } else if(event->event_id == ARDUINO_EVENT_WIFI_AP_GOT_IP6) { + setStatusBits(AP_HAS_IP6_BIT); + } else if(event->event_id == ARDUINO_EVENT_ETH_GOT_IP6) { + setStatusBits(ETH_CONNECTED_BIT | ETH_HAS_IP6_BIT); + } else if(event->event_id == ARDUINO_EVENT_SC_GOT_SSID_PSWD) { + WiFi.begin( + (const char *)event->event_info.sc_got_ssid_pswd.ssid, + (const char *)event->event_info.sc_got_ssid_pswd.password, + 0, + ((event->event_info.sc_got_ssid_pswd.bssid_set == true)?event->event_info.sc_got_ssid_pswd.bssid:nullptr) + ); + } else if(event->event_id == ARDUINO_EVENT_SC_SEND_ACK_DONE) { + esp_smartconfig_stop(); + WiFiSTAClass::_smartConfigDone = true; + } + + for(uint32_t i = 0; i < cbEventList.size(); i++) { + WiFiEventCbList_t entry = cbEventList[i]; + if(entry.cb || entry.fcb || entry.scb) { + if(entry.event == (arduino_event_id_t) event->event_id || entry.event == ARDUINO_EVENT_MAX) { + if(entry.cb) { + entry.cb((arduino_event_id_t) event->event_id); + } else if(entry.fcb) { + entry.fcb((arduino_event_id_t) event->event_id, (arduino_event_info_t) event->event_info); + } else { + entry.scb(event); + } + } + } + } + return ESP_OK; +} + +/** + * Return the current channel associated with the network + * @return channel (1-13) + */ +int32_t WiFiGenericClass::channel(void) +{ + uint8_t primaryChan = 0; + wifi_second_chan_t secondChan = WIFI_SECOND_CHAN_NONE; + if(!lowLevelInitDone){ + return primaryChan; + } + esp_wifi_get_channel(&primaryChan, &secondChan); + return primaryChan; +} + + +/** + * store WiFi config in SDK flash area + * @param persistent + */ +void WiFiGenericClass::persistent(bool persistent) +{ + _persistent = persistent; +} + + +/** + * enable WiFi long range mode + * @param enable + */ +void WiFiGenericClass::enableLongRange(bool enable) +{ + _long_range = enable; +} + + +/** + * set new mode + * @param m WiFiMode_t + */ +bool WiFiGenericClass::mode(wifi_mode_t m) +{ + wifi_mode_t cm = getMode(); + if(cm == m) { + return true; + } + if(!cm && m){ + if(!wifiLowLevelInit(_persistent)){ + return false; + } + } else if(cm && !m){ + return espWiFiStop(); + } + + esp_err_t err; + if(m & WIFI_MODE_STA){ + err = set_esp_interface_hostname(ESP_IF_WIFI_STA, get_esp_netif_hostname()); + if(err){ + log_e("Could not set hostname! %d", err); + return false; + } + } + err = esp_wifi_set_mode(m); + if(err){ + log_e("Could not set mode! %d", err); + return false; + } + if(_long_range){ + if(m & WIFI_MODE_STA){ + err = esp_wifi_set_protocol(WIFI_IF_STA, WIFI_PROTOCOL_LR); + if(err != ESP_OK){ + log_e("Could not enable long range on STA! %d", err); + return false; + } + } + if(m & WIFI_MODE_AP){ + err = esp_wifi_set_protocol(WIFI_IF_AP, WIFI_PROTOCOL_LR); + if(err != ESP_OK){ + log_e("Could not enable long range on AP! %d", err); + return false; + } + } + } + if(!espWiFiStart()){ + return false; + } + + #ifdef BOARD_HAS_DUAL_ANTENNA + if(!setDualAntennaConfig(ANT1, ANT2, WIFI_RX_ANT_AUTO, WIFI_TX_ANT_AUTO)){ + log_e("Dual Antenna Config failed!"); + return false; + } + #endif + + return true; +} + +/** + * get WiFi mode + * @return WiFiMode + */ +wifi_mode_t WiFiGenericClass::getMode() +{ + if(!lowLevelInitDone || !_esp_wifi_started){ + return WIFI_MODE_NULL; + } + wifi_mode_t mode; + if(esp_wifi_get_mode(&mode) != ESP_OK){ + log_w("WiFi not started"); + return WIFI_MODE_NULL; + } + return mode; +} + +/** + * control STA mode + * @param enable bool + * @return ok + */ +bool WiFiGenericClass::enableSTA(bool enable) +{ + + wifi_mode_t currentMode = getMode(); + bool isEnabled = ((currentMode & WIFI_MODE_STA) != 0); + + if(isEnabled != enable) { + if(enable) { + return mode((wifi_mode_t)(currentMode | WIFI_MODE_STA)); + } + return mode((wifi_mode_t)(currentMode & (~WIFI_MODE_STA))); + } + return true; +} + +/** + * control AP mode + * @param enable bool + * @return ok + */ +bool WiFiGenericClass::enableAP(bool enable) +{ + + wifi_mode_t currentMode = getMode(); + bool isEnabled = ((currentMode & WIFI_MODE_AP) != 0); + + if(isEnabled != enable) { + if(enable) { + return mode((wifi_mode_t)(currentMode | WIFI_MODE_AP)); + } + return mode((wifi_mode_t)(currentMode & (~WIFI_MODE_AP))); + } + return true; +} + +/** + * control modem sleep when only in STA mode + * @param enable bool + * @return ok + */ +bool WiFiGenericClass::setSleep(bool enabled){ + return setSleep(enabled?WIFI_PS_MIN_MODEM:WIFI_PS_NONE); +} + +/** + * control modem sleep when only in STA mode + * @param mode wifi_ps_type_t + * @return ok + */ +bool WiFiGenericClass::setSleep(wifi_ps_type_t sleepType) +{ + if(sleepType != _sleepEnabled){ + _sleepEnabled = sleepType; + if((getMode() & WIFI_MODE_STA) != 0){ + if(esp_wifi_set_ps(_sleepEnabled) != ESP_OK){ + log_e("esp_wifi_set_ps failed!"); + return false; + } + } + return true; + } + return false; +} + +/** + * get modem sleep enabled + * @return true if modem sleep is enabled + */ +wifi_ps_type_t WiFiGenericClass::getSleep() +{ + return _sleepEnabled; +} + +/** + * control wifi tx power + * @param power enum maximum wifi tx power + * @return ok + */ +bool WiFiGenericClass::setTxPower(wifi_power_t power){ + if((getStatusBits() & (STA_STARTED_BIT | AP_STARTED_BIT)) == 0){ + log_w("Neither AP or STA has been started"); + return false; + } + return esp_wifi_set_max_tx_power(power) == ESP_OK; +} + +wifi_power_t WiFiGenericClass::getTxPower(){ + int8_t power; + if((getStatusBits() & (STA_STARTED_BIT | AP_STARTED_BIT)) == 0){ + log_w("Neither AP or STA has been started"); + return WIFI_POWER_19_5dBm; + } + if(esp_wifi_get_max_tx_power(&power)){ + return WIFI_POWER_19_5dBm; + } + return (wifi_power_t)power; +} + +/** + * Initiate FTM Session. + * @param frm_count Number of FTM frames requested in terms of 4 or 8 bursts (allowed values - 0(No pref), 16, 24, 32, 64) + * @param burst_period Requested time period between consecutive FTM bursts in 100's of milliseconds (allowed values - 0(No pref), 2 - 255) + * @param channel Primary channel of the FTM Responder + * @param mac MAC address of the FTM Responder + * @return true on success + */ +bool WiFiGenericClass::initiateFTM(uint8_t frm_count, uint16_t burst_period, uint8_t channel, const uint8_t * mac) { + wifi_ftm_initiator_cfg_t ftmi_cfg = { + .resp_mac = {0,0,0,0,0,0}, + .channel = channel, + .frm_count = frm_count, + .burst_period = burst_period, + }; + if(mac != nullptr){ + memcpy(ftmi_cfg.resp_mac, mac, 6); + } + // Request FTM session with the Responder + if (ESP_OK != esp_wifi_ftm_initiate_session(&ftmi_cfg)) { + log_e("Failed to initiate FTM session"); + return false; + } + return true; +} + +/** + * Configure Dual antenna. + * @param gpio_ant1 Configure the GPIO number for the antenna 1 connected to the RF switch (default GPIO2 on ESP32-WROOM-DA) + * @param gpio_ant2 Configure the GPIO number for the antenna 2 connected to the RF switch (default GPIO25 on ESP32-WROOM-DA) + * @param rx_mode Set the RX antenna mode. See wifi_rx_ant_t for the options. + * @param tx_mode Set the TX antenna mode. See wifi_tx_ant_t for the options. + * @return true on success + */ +bool WiFiGenericClass::setDualAntennaConfig(uint8_t gpio_ant1, uint8_t gpio_ant2, wifi_rx_ant_t rx_mode, wifi_tx_ant_t tx_mode) { + + wifi_ant_gpio_config_t wifi_ant_io; + + if (ESP_OK != esp_wifi_get_ant_gpio(&wifi_ant_io)) { + log_e("Failed to get antenna configuration"); + return false; + } + + wifi_ant_io.gpio_cfg[0].gpio_num = gpio_ant1; + wifi_ant_io.gpio_cfg[0].gpio_select = 1; + wifi_ant_io.gpio_cfg[1].gpio_num = gpio_ant2; + wifi_ant_io.gpio_cfg[1].gpio_select = 1; + + if (ESP_OK != esp_wifi_set_ant_gpio(&wifi_ant_io)) { + log_e("Failed to set antenna GPIO configuration"); + return false; + } + + // Set antenna default configuration + wifi_ant_config_t ant_config = { + .rx_ant_mode = WIFI_ANT_MODE_AUTO, + .rx_ant_default = WIFI_ANT_MAX, // Ignored in AUTO mode + .tx_ant_mode = WIFI_ANT_MODE_AUTO, + .enabled_ant0 = 1, + .enabled_ant1 = 2, + }; + + switch (rx_mode) + { + case WIFI_RX_ANT0: + ant_config.rx_ant_mode = WIFI_ANT_MODE_ANT0; + break; + case WIFI_RX_ANT1: + ant_config.rx_ant_mode = WIFI_ANT_MODE_ANT1; + break; + case WIFI_RX_ANT_AUTO: + log_i("TX Antenna will be automatically selected"); + ant_config.rx_ant_default = WIFI_ANT_ANT0; + ant_config.rx_ant_mode = WIFI_ANT_MODE_AUTO; + // Force TX for AUTO if RX is AUTO + ant_config.tx_ant_mode = WIFI_ANT_MODE_AUTO; + goto set_ant; + break; + default: + log_e("Invalid default antenna! Falling back to AUTO"); + ant_config.rx_ant_mode = WIFI_ANT_MODE_AUTO; + break; + } + + switch (tx_mode) + { + case WIFI_TX_ANT0: + ant_config.tx_ant_mode = WIFI_ANT_MODE_ANT0; + break; + case WIFI_TX_ANT1: + ant_config.tx_ant_mode = WIFI_ANT_MODE_ANT1; + break; + case WIFI_TX_ANT_AUTO: + log_i("RX Antenna will be automatically selected"); + ant_config.rx_ant_default = WIFI_ANT_ANT0; + ant_config.tx_ant_mode = WIFI_ANT_MODE_AUTO; + // Force RX for AUTO if RX is AUTO + ant_config.rx_ant_mode = WIFI_ANT_MODE_AUTO; + break; + default: + log_e("Invalid default antenna! Falling back to AUTO"); + ant_config.rx_ant_default = WIFI_ANT_ANT0; + ant_config.tx_ant_mode = WIFI_ANT_MODE_AUTO; + break; + } + +set_ant: + if (ESP_OK != esp_wifi_set_ant(&ant_config)) { + log_e("Failed to set antenna configuration"); + return false; + } + + return true; +} + +// ----------------------------------------------------------------------------------------------------------------------- +// ------------------------------------------------ Generic Network function --------------------------------------------- +// ----------------------------------------------------------------------------------------------------------------------- + +/** + * DNS callback + * @param name + * @param ipaddr + * @param callback_arg + */ +static void wifi_dns_found_callback(const char *name, const ip_addr_t *ipaddr, void *callback_arg) +{ + if(ipaddr) { + (*reinterpret_cast(callback_arg)) = ipaddr->u_addr.ip4.addr; + } + xEventGroupSetBits(_arduino_event_group, WIFI_DNS_DONE_BIT); +} + +/** + * Resolve the given hostname to an IP address. + * @param aHostname Name to be resolved + * @param aResult IPAddress structure to store the returned IP address + * @return 1 if aIPAddrString was successfully converted to an IP address, + * else error code + */ +int WiFiGenericClass::hostByName(const char* aHostname, IPAddress& aResult) +{ + ip_addr_t addr; + aResult = static_cast(0); + waitStatusBits(WIFI_DNS_IDLE_BIT, 16000); + clearStatusBits(WIFI_DNS_IDLE_BIT | WIFI_DNS_DONE_BIT); + err_t err = dns_gethostbyname(aHostname, &addr, &wifi_dns_found_callback, &aResult); + if(err == ERR_OK && addr.u_addr.ip4.addr) { + aResult = addr.u_addr.ip4.addr; + } else if(err == ERR_INPROGRESS) { + waitStatusBits(WIFI_DNS_DONE_BIT, 15000); //real internal timeout in lwip library is 14[s] + clearStatusBits(WIFI_DNS_DONE_BIT); + } + setStatusBits(WIFI_DNS_IDLE_BIT); + if((uint32_t)aResult == 0){ + log_e("DNS Failed for %s", aHostname); + } + return (uint32_t)aResult != 0; +} + +IPAddress WiFiGenericClass::calculateNetworkID(IPAddress ip, IPAddress subnet) { + IPAddress networkID; + + for (size_t i = 0; i < 4; i++) + networkID[i] = subnet[i] & ip[i]; + + return networkID; +} + +IPAddress WiFiGenericClass::calculateBroadcast(IPAddress ip, IPAddress subnet) { + IPAddress broadcastIp; + + for (int i = 0; i < 4; i++) + broadcastIp[i] = ~subnet[i] | ip[i]; + + return broadcastIp; +} + +uint8_t WiFiGenericClass::calculateSubnetCIDR(IPAddress subnetMask) { + uint8_t CIDR = 0; + + for (uint8_t i = 0; i < 4; i++) { + if (subnetMask[i] == 0x80) // 128 + CIDR += 1; + else if (subnetMask[i] == 0xC0) // 192 + CIDR += 2; + else if (subnetMask[i] == 0xE0) // 224 + CIDR += 3; + else if (subnetMask[i] == 0xF0) // 242 + CIDR += 4; + else if (subnetMask[i] == 0xF8) // 248 + CIDR += 5; + else if (subnetMask[i] == 0xFC) // 252 + CIDR += 6; + else if (subnetMask[i] == 0xFE) // 254 + CIDR += 7; + else if (subnetMask[i] == 0xFF) // 255 + CIDR += 8; + } + + return CIDR; +} diff --git a/Firmware/RTK_Surveyor/PvtClient.ino b/Firmware/RTK_Surveyor/PvtClient.ino new file mode 100644 index 000000000..287a94060 --- /dev/null +++ b/Firmware/RTK_Surveyor/PvtClient.ino @@ -0,0 +1,534 @@ +/* +PvtClient.ino + + The (position, velocity and time) client sits on top of the network layer and + sends position data to one + or more computers or cell phones for display. + + Satellite ... Satellite + | | | + | | | + | V | + | RTK | + '------> Base <------' + Station + | + | NTRIP Server sends correction data + V + NTRIP Caster + | + | NTRIP Client receives correction data + | + V + Bluetooth RTK Network: PVT Client + .---------------- Rover ----------------------------------. + | | | + | | Network: PVT Server | + | PVT data | Position, velocity & time data | PVT data + | V | + | Computer or | + '------------> Cell Phone <-------------------------------' + for display + + PVT Client Testing: + + Using Ethernet on RTK Reference Station and specifying a PVT Client host: + + 1. Network failure - Disconnect Ethernet cable at RTK Reference Station, + expecting retry PVT client connection after network restarts + + 2. Internet link failure - Disconnect Ethernet cable between Ethernet + switch and the firewall to simulate an internet failure, expecting + retry PVT client connection after delay + + 3. Internet outage - Disconnect Ethernet cable between Ethernet + switch and the firewall to simulate an internet failure, expecting + PVT client retry interval to increase from 5 seconds to 5 minutes + and 20 seconds, and the PVT client to reconnect following the outage. + + Using WiFi on RTK Express or RTK Reference Station, no PVT Client host + specified, running Vespucci on the cell phone: + + Vespucci Setup: + * Click on the gear icon + * Scroll down and click on Advanced preferences + * Click on Location settings + * Click on GPS/GNSS source + * Set to NMEA from TCP server + * Click on NMEA network source + * Set IP address to 127.0.0.1:1958 + * Uncheck Leave GPS/GNSS turned off + * Check Fallback to network location + * Click on Stale location after + * Set the value 5 seconds + * Exit the menus + + 1. Verify connection to the Vespucci application on the cell phone + (gateway IP address). + + 2. Vespucci not running: Stop the Vespucci application, expecting PVT + client retry interval to increase from 5 seconds to 5 minutes and + 20 seconds. The PVT client connects once the Vespucci application + is restarted on the phone. + + Test Setups: + + RTK Express RTK Reference Station + ^ ^ ^ + WiFi | WiFi | | Ethernet cable + v v v + WiFi Access Point <-----------> Ethernet Switch + Ethernet ^ + Cable | Ethernet cable + v + Internet Firewall + ^ + | Ethernet cable + v + Modem + ^ + | + v + Internet + ^ + | + v + NMEA Server + NTRIP Caster + + + RTK Express RTK Reference Station + ^ ^ + WiFi | WiFi | + \ / + \ / + v v + Cell Phone (NMEA Server) + ^ + | + v + NTRIP Caster + + Possible NTRIP Casters + + * https://emlid.com/ntrip-caster/ + * http://rtk2go.com/ + * private SNIP NTRIP caster +*/ + +#if COMPILE_NETWORK + +//---------------------------------------- +// Constants +//---------------------------------------- + +#define PVT_MAX_CONNECTIONS 6 +#define PVT_DELAY_BETWEEN_CONNECTIONS (5 * 1000) + +// Define the PVT client states +enum PvtClientStates +{ + PVT_CLIENT_STATE_OFF = 0, + PVT_CLIENT_STATE_NETWORK_STARTED, + PVT_CLIENT_STATE_CLIENT_STARTING, + PVT_CLIENT_STATE_CONNECTED, + // Insert new states here + PVT_CLIENT_STATE_MAX // Last entry in the state list +}; + +const char * const pvtClientStateName[] = +{ + "PVT_CLIENT_STATE_OFF", + "PVT_CLIENT_STATE_NETWORK_STARTED", + "PVT_CLIENT_STATE_CLIENT_STARTING", + "PVT_CLIENT_STATE_CONNECTED" +}; + +const int pvtClientStateNameEntries = sizeof(pvtClientStateName) / sizeof(pvtClientStateName[0]); + +const RtkMode_t pvtClientMode = RTK_MODE_BASE_FIXED + | RTK_MODE_BASE_SURVEY_IN + | RTK_MODE_ROVER; + +//---------------------------------------- +// Locals +//---------------------------------------- + +static NetworkClient * pvtClient; +static IPAddress pvtClientIpAddress; +static uint8_t pvtClientState; +static volatile RING_BUFFER_OFFSET pvtClientTail; +static volatile bool pvtClientWriteError; + +//---------------------------------------- +// PVT Client handleGnssDataTask Support Routines +//---------------------------------------- + +// Send PVT data to the NMEA server +int32_t pvtClientSendData(uint16_t dataHead) +{ + bool connected; + int32_t bytesToSend; + int32_t bytesSent; + + // Determine if a client is connected + bytesToSend = 0; + connected = settings.enablePvtClient && online.pvtClient; + + // Determine if the client is connected + if ((!connected) || (!online.pvtClient)) + pvtClientTail = dataHead; + else + { + // Determine the amount of data in the buffer + bytesToSend = dataHead - pvtClientTail; + if (bytesToSend < 0) + bytesToSend += settings.gnssHandlerBufferSize; + if (bytesToSend > 0) + { + // Reduce bytes to send if we have more to send then the end of the buffer + // We'll wrap next loop + if ((pvtClientTail + bytesToSend) > settings.gnssHandlerBufferSize) + bytesToSend = settings.gnssHandlerBufferSize - pvtClientTail; + + // Send the data to the NMEA server + bytesSent = pvtClient->write(&ringBuffer[pvtClientTail], bytesToSend); + if (bytesSent >= 0) + { + if ((settings.debugPvtClient || PERIODIC_DISPLAY(PD_PVT_CLIENT_DATA)) && (!inMainMenu)) + { + PERIODIC_CLEAR(PD_PVT_CLIENT_DATA); + systemPrintf("PVT client sent %d bytes, %d remaining\r\n", bytesSent, bytesToSend - bytesSent); + } + + // Assume all data was sent, wrap the buffer pointer + pvtClientTail += bytesSent; + if (pvtClientTail >= settings.gnssHandlerBufferSize) + pvtClientTail -= settings.gnssHandlerBufferSize; + + // Update space available for use in UART task + bytesToSend = dataHead - pvtClientTail; + if (bytesToSend < 0) + bytesToSend += settings.gnssHandlerBufferSize; + } + + // Failed to write the data + else + { + // Done with this client connection + if (!inMainMenu) + systemPrintf("PVT client breaking connection with %s:%d\r\n", + pvtClientIpAddress.toString().c_str(), settings.pvtClientPort); + + pvtClientWriteError = true; + bytesToSend = 0; + } + } + } + + // Return the amount of space that WiFi is using in the buffer + return bytesToSend; +} + +// Update the state of the PVT client state machine +void pvtClientSetState(uint8_t newState) +{ + if ((settings.debugPvtClient || PERIODIC_DISPLAY(PD_PVT_CLIENT_STATE)) && (!inMainMenu)) + { + if (pvtClientState == newState) + systemPrint("*"); + else + systemPrintf("%s --> ", pvtClientStateName[pvtClientState]); + } + pvtClientState = newState; + if ((settings.debugPvtClient || PERIODIC_DISPLAY(PD_PVT_CLIENT_STATE)) && (!inMainMenu)) + { + PERIODIC_CLEAR(PD_PVT_CLIENT_STATE); + if (newState >= PVT_CLIENT_STATE_MAX) + { + systemPrintf("Unknown PVT Client state: %d\r\n", pvtClientState); + reportFatalError("Unknown PVT Client state"); + } + else + systemPrintln(pvtClientStateName[pvtClientState]); + } +} + +// Remove previous messages from the ring buffer +void discardPvtClientBytes(RING_BUFFER_OFFSET previousTail, RING_BUFFER_OFFSET newTail) +{ + if (previousTail < newTail) + { + // No buffer wrap occurred + if ((pvtClientTail >= previousTail) && (pvtClientTail < newTail)) + pvtClientTail = newTail; + } + else + { + // Buffer wrap occurred + if ((pvtClientTail >= previousTail) || (pvtClientTail < newTail)) + pvtClientTail = newTail; + } +} + +//---------------------------------------- +// PVT Client Routines +//---------------------------------------- + +// Start the PVT client +bool pvtClientStart() +{ + NetworkClient * client; + + // Allocate the PVT client + client = new NetworkClient(NETWORK_USER_PVT_CLIENT); + if (client) + { + // Get the host name + char hostname[sizeof(settings.pvtClientHost)]; + strcpy(hostname, settings.pvtClientHost); + if (!strlen(hostname)) + { + // No host was specified, assume we are using WiFi and using a phone + // running the application and a WiFi hot spot. The IP address of + // the phone is provided to the RTK during the DHCP handshake as the + // gateway IP address. + + // Attempt the PVT client connection + pvtClientIpAddress = wifiGetGatewayIpAddress(); + sprintf(hostname, "%s", pvtClientIpAddress.toString().c_str()); + } + + // Display the PVT client connection attempt + if (settings.debugPvtClient) + systemPrintf("PVT client connecting to %s:%d\r\n", hostname, settings.pvtClientPort); + + // Attempt the PVT client connection + if (client->connect(hostname, settings.pvtClientPort)) + { + // Get the client IP address + pvtClientIpAddress = client->remoteIP(); + + // Display the PVT client connection + systemPrintf("PVT client connected to %s:%d\r\n", + pvtClientIpAddress.toString().c_str(), settings.pvtClientPort); + + // The PVT client is connected + pvtClient = client; + pvtClientWriteError = false; + online.pvtClient = true; + return true; + } + else + { + // Release any allocated resources + client->stop(); + delete client; + } + } + return false; +} + +// Stop the PVT client +void pvtClientStop() +{ + NetworkClient * client; + IPAddress ipAddress; + + client = pvtClient; + if (client) + { + // Delay to allow the UART task to finish with the pvtClient + online.pvtClient = false; + delay(5); + + // Done with the PVT client connection + ipAddress = pvtClientIpAddress; + pvtClientIpAddress = IPAddress((uint32_t)0); + pvtClient->stop(); + delete pvtClient; + pvtClient = nullptr; + + // Notify the user of the PVT client shutdown + if (!inMainMenu) + systemPrintf("PVT client disconnected from %s:%d\r\n", + ipAddress.toString().c_str(), settings.pvtClientPort); + } + + // Done with the network + if (pvtClientState != PVT_CLIENT_STATE_OFF) + networkUserClose(NETWORK_USER_PVT_CLIENT); + + // Initialize the PVT client + pvtClientWriteError = false; + if (settings.debugPvtClient) + systemPrintln("PVT client offline"); + pvtClientSetState(PVT_CLIENT_STATE_OFF); +} + +// Update the PVT client state +void pvtClientUpdate() +{ + static uint8_t connectionAttempt; + static uint32_t connectionDelay; + uint32_t days; + byte hours; + uint64_t milliseconds; + byte minutes; + byte seconds; + static uint32_t timer; + + // Shutdown the PVT client when the mode or setting changes + DMW_st(pvtClientSetState, pvtClientState); + if (NEQ_RTK_MODE(pvtClientMode) || (!settings.enablePvtClient)) + { + if (pvtClientState > PVT_CLIENT_STATE_OFF) + pvtClientStop(); + } + + /* + PVT Client state machine + + .---------------->PVT_CLIENT_STATE_OFF + | | + | pvtClientStop | settings.enablePvtClient + | | + | V + +<----------PVT_CLIENT_STATE_NETWORK_STARTED + ^ | + | | networkUserConnected + | | + | V + +<----------PVT_CLIENT_STATE_CLIENT_STARTING + ^ | + | | pvtClientStart = true + | | + | V + '--------------PVT_CLIENT_STATE_CONNECTED + */ + + switch (pvtClientState) + { + default: + systemPrintf("PVT client state: %d\r\n", pvtClientState); + reportFatalError("Invalid PVT client state"); + break; + + // Wait until the PVT client is enabled + case PVT_CLIENT_STATE_OFF: + // Determine if the PVT client should be running + if (EQ_RTK_MODE(pvtClientMode) && settings.enablePvtClient) + { + if (networkUserOpen(NETWORK_USER_PVT_CLIENT, NETWORK_TYPE_ACTIVE)) + { + timer = 0; + pvtClientSetState(PVT_CLIENT_STATE_NETWORK_STARTED); + } + } + break; + + // Wait until the network is connected + case PVT_CLIENT_STATE_NETWORK_STARTED: + // Determine if the network has failed + if (networkIsShuttingDown(NETWORK_USER_PVT_CLIENT)) + // Failed to connect to to the network, attempt to restart the network + pvtClientStop(); + + // Determine if WiFi is required + else if ((!strlen(settings.pvtClientHost)) && (networkGetType(NETWORK_TYPE_ACTIVE) != NETWORK_TYPE_WIFI)) + { + // Wrong network type, WiFi is required but another network is being used + if ((millis() - timer) >= (15 * 1000)) + { + timer = millis(); + systemPrintln("PVT Client must connect via WiFi when no host is specified"); + } + } + + // Wait for the network to connect to the media + else if (networkUserConnected(NETWORK_USER_PVT_CLIENT)) + { + // The network type and host provide a valid configuration + timer = millis(); + pvtClientSetState(PVT_CLIENT_STATE_CLIENT_STARTING); + } + break; + + // Attempt the connection ot the PVT server + case PVT_CLIENT_STATE_CLIENT_STARTING: + // Determine if the network has failed + if (networkIsShuttingDown(NETWORK_USER_PVT_CLIENT)) + // Failed to connect to to the network, attempt to restart the network + pvtClientStop(); + + // Delay before connecting to the network + else if ((millis() - timer) >= connectionDelay) + { + timer = millis(); + + // Start the PVT client + if (!pvtClientStart()) + { + // Connection failure + if (settings.debugPvtClient) + systemPrintln("PVT Client connection failed"); + connectionDelay = PVT_DELAY_BETWEEN_CONNECTIONS << connectionAttempt; + if (connectionAttempt < PVT_MAX_CONNECTIONS) + connectionAttempt += 1; + + // Display the uptime + milliseconds = connectionDelay; + days = milliseconds / MILLISECONDS_IN_A_DAY; + milliseconds %= MILLISECONDS_IN_A_DAY; + hours = milliseconds / MILLISECONDS_IN_AN_HOUR; + milliseconds %= MILLISECONDS_IN_AN_HOUR; + minutes = milliseconds / MILLISECONDS_IN_A_MINUTE; + milliseconds %= MILLISECONDS_IN_A_MINUTE; + seconds = milliseconds / MILLISECONDS_IN_A_SECOND; + milliseconds %= MILLISECONDS_IN_A_SECOND; + if (settings.debugPvtClient) + systemPrintf("PVT Client delaying %d %02d:%02d:%02d.%03lld\r\n", + days, hours, minutes, seconds, milliseconds); + } + else + { + // Successful connection + connectionAttempt = 0; + pvtClientSetState(PVT_CLIENT_STATE_CONNECTED); + } + } + break; + + // Wait for the PVT client to shutdown or a PVT client link failure + case PVT_CLIENT_STATE_CONNECTED: + // Determine if the network has failed + if (networkIsShuttingDown(NETWORK_USER_PVT_CLIENT)) + // Failed to connect to to the network, attempt to restart the network + pvtClientStop(); + + // Determine if the PVT client link is broken + else if ((!*pvtClient) || (!pvtClient->connected()) || pvtClientWriteError) + // Stop the PVT client + pvtClientStop(); + break; + } + + // Periodically display the PVT client state + if (PERIODIC_DISPLAY(PD_PVT_CLIENT_STATE)) + pvtClientSetState(pvtClientState); +} + +// Verify the PVT client tables +void pvtClientValidateTables() +{ + if (pvtClientStateNameEntries != PVT_CLIENT_STATE_MAX) + reportFatalError("Fix pvtClientStateNameEntries to match PvtClientStates"); +} + +// Zero the PVT client tail +void pvtClientZeroTail() +{ + pvtClientTail = 0; +} + +#endif // COMPILE_NETWORK diff --git a/Firmware/RTK_Surveyor/PvtServer.ino b/Firmware/RTK_Surveyor/PvtServer.ino new file mode 100644 index 000000000..ed91cd710 --- /dev/null +++ b/Firmware/RTK_Surveyor/PvtServer.ino @@ -0,0 +1,540 @@ +/* +pvtServer.ino + + The PVT (position, velocity and time) server sits on top of the network layer + and sends position data to one or more computers or cell phones for display. + + Satellite ... Satellite + | | | + | | | + | V | + | RTK | + '------> Base <------' + Station + | + | NTRIP Server sends correction data + V + NTRIP Caster + | + | NTRIP Client receives correction data + | + V + Bluetooth RTK Network: PVT Client + .---------------- Rover ----------------------------------. + | | | + | | Network: PVT Server | + | PVT data | Position, velocity & time data | PVT data + | V | + | Computer or | + '------------> Cell Phone <-------------------------------' + for display + +*/ + +#ifdef COMPILE_WIFI + +//---------------------------------------- +// Constants +//---------------------------------------- + +#define PVT_SERVER_MAX_CLIENTS 4 +#define PVT_SERVER_CLIENT_DATA_TIMEOUT (15 * 1000) + +// Define the PVT server states +enum PvtServerStates +{ + PVT_SERVER_STATE_OFF = 0, + PVT_SERVER_STATE_NETWORK_STARTED, + PVT_SERVER_STATE_RUNNING, + // Insert new states here + PVT_SERVER_STATE_MAX // Last entry in the state list +}; + +const char * const pvtServerStateName[] = +{ + "PVT_SERVER_STATE_OFF", + "PVT_SERVER_STATE_NETWORK_STARTED", + "PVT_SERVER_STATE_RUNNING", +}; + +const int pvtServerStateNameEntries = sizeof(pvtServerStateName) / sizeof(pvtServerStateName[0]); + +const RtkMode_t pvtServerMode = RTK_MODE_BASE_FIXED + | RTK_MODE_BASE_SURVEY_IN + | RTK_MODE_ROVER; + +//---------------------------------------- +// Locals +//---------------------------------------- + +// PVT server +static WiFiServer * pvtServer = nullptr; +static uint8_t pvtServerState; +static uint32_t pvtServerTimer; + +// PVT server clients +static volatile uint8_t pvtServerClientConnected; +static volatile uint8_t pvtServerClientDataSent; +static volatile uint8_t pvtServerClientWriteError; +static NetworkClient * pvtServerClient[PVT_SERVER_MAX_CLIENTS]; +static IPAddress pvtServerClientIpAddress[PVT_SERVER_MAX_CLIENTS]; +static volatile RING_BUFFER_OFFSET pvtServerClientTails[PVT_SERVER_MAX_CLIENTS]; + +//---------------------------------------- +// PVT Server handleGnssDataTask Support Routines +//---------------------------------------- + +// Send data to the PVT clients +int32_t pvtServerClientSendData(int index, uint8_t *data, uint16_t length) +{ + + length = pvtServerClient[index]->write(data, length); + if (length >= 0) + { + // Update the data sent flag when data successfully sent + if (length > 0) + pvtServerClientDataSent |= 1 << index; + if ((settings.debugPvtServer || PERIODIC_DISPLAY(PD_PVT_SERVER_CLIENT_DATA)) && (!inMainMenu)) + { + PERIODIC_CLEAR(PD_PVT_SERVER_CLIENT_DATA); + systemPrintf("PVT server wrote %d bytes to %d.%d.%d.%d\r\n", + length, + pvtServerClientIpAddress[index][0], + pvtServerClientIpAddress[index][1], + pvtServerClientIpAddress[index][2], + pvtServerClientIpAddress[index][3]); + } + } + + // Failed to write the data + else + { + // Done with this client connection + if ((settings.debugPvtServer || PERIODIC_DISPLAY(PD_PVT_SERVER_CLIENT_DATA)) && (!inMainMenu)) + { + PERIODIC_CLEAR(PD_PVT_SERVER_CLIENT_DATA); + systemPrintf("PVT server breaking connection %d with client %d.%d.%d.%d\r\n", + index, + pvtServerClientIpAddress[index][0], + pvtServerClientIpAddress[index][1], + pvtServerClientIpAddress[index][2], + pvtServerClientIpAddress[index][3]); + } + + pvtServerClient[index]->stop(); + pvtServerClientConnected &= ~(1 << index); + pvtServerClientWriteError |= 1 << index; + length = 0; + } + return length; +} + +// Send PVT data to the clients +int32_t pvtServerSendData(uint16_t dataHead) +{ + int32_t usedSpace = 0; + + int32_t bytesToSend; + int index; + uint16_t tail; + + // Update each of the clients + for (index = 0; index < PVT_SERVER_MAX_CLIENTS; index++) + { + tail = pvtServerClientTails[index]; + + // Determine if the client is connected + if (!(pvtServerClientConnected & (1 << index))) + tail = dataHead; + else + { + // Determine the amount of PVT data in the buffer + bytesToSend = dataHead - tail; + if (bytesToSend < 0) + bytesToSend += settings.gnssHandlerBufferSize; + if (bytesToSend > 0) + { + // Reduce bytes to send if we have more to send then the end of the buffer + // We'll wrap next loop + if ((tail + bytesToSend) > settings.gnssHandlerBufferSize) + bytesToSend = settings.gnssHandlerBufferSize - tail; + + // Send the data to the PVT server clients + bytesToSend = pvtServerClientSendData(index, &ringBuffer[tail], bytesToSend); + + // Assume all data was sent, wrap the buffer pointer + tail += bytesToSend; + if (tail >= settings.gnssHandlerBufferSize) + tail -= settings.gnssHandlerBufferSize; + + // Update space available for use in UART task + bytesToSend = dataHead - tail; + if (bytesToSend < 0) + bytesToSend += settings.gnssHandlerBufferSize; + if (usedSpace < bytesToSend) + usedSpace = bytesToSend; + } + } + pvtServerClientTails[index] = tail; + } + + // Return the amount of space that PVT server client is using in the buffer + return usedSpace; +} + +// Remove previous messages from the ring buffer +void discardPvtServerBytes(RING_BUFFER_OFFSET previousTail, RING_BUFFER_OFFSET newTail) +{ + int index; + uint16_t tail; + + // Update each of the clients + for (index = 0; index < PVT_SERVER_MAX_CLIENTS; index++) + { + tail = pvtServerClientTails[index]; + if (previousTail < newTail) + { + // No buffer wrap occurred + if ((tail >= previousTail) && (tail < newTail)) + pvtServerClientTails[index] = newTail; + } + else + { + // Buffer wrap occurred + if ((tail >= previousTail) || (tail < newTail)) + pvtServerClientTails[index] = newTail; + } + } +} + +//---------------------------------------- +// PVT Server Routines +//---------------------------------------- + +// Update the state of the PVT server state machine +void pvtServerSetState(uint8_t newState) +{ + if ((settings.debugPvtServer || PERIODIC_DISPLAY(PD_PVT_SERVER_STATE)) && (!inMainMenu)) + { + if (pvtServerState == newState) + systemPrint("*"); + else + systemPrintf("%s --> ", pvtServerStateName[pvtServerState]); + } + pvtServerState = newState; + if ((settings.debugPvtServer || PERIODIC_DISPLAY(PD_PVT_SERVER_STATE)) && (!inMainMenu)) + { + PERIODIC_CLEAR(PD_PVT_SERVER_STATE); + if (newState >= PVT_SERVER_STATE_MAX) + { + systemPrintf("Unknown PVT Server state: %d\r\n", pvtServerState); + reportFatalError("Unknown PVT Server state"); + } + else + systemPrintln(pvtServerStateName[pvtServerState]); + } +} + +// Start the PVT server +bool pvtServerStart() +{ + IPAddress localIp; + + if (settings.debugPvtServer && (!inMainMenu)) + systemPrintln("PVT server starting the server"); + + // Start the PVT server + pvtServer = new WiFiServer(settings.pvtServerPort); + if (!pvtServer) + return false; + + pvtServer->begin(); + online.pvtServer = true; + localIp = wifiGetIpAddress(); + systemPrintf("PVT server online, IP address %d.%d.%d.%d:%d\r\n", + localIp[0], localIp[1], localIp[2], localIp[3], + settings.pvtServerPort); + return true; +} + +// Stop the PVT server +void pvtServerStop() +{ + int index; + + // Notify the rest of the system that the PVT server is shutting down + if (online.pvtServer) + { + // Notify the UART2 tasks of the PVT server shutdown + online.pvtServer = false; + delay(5); + } + + // Determine if PVT server clients are active + if (pvtServerClientConnected) + { + // Shutdown the PVT server client links + for (index = 0; index < PVT_SERVER_MAX_CLIENTS; index++) + pvtServerStopClient(index); + } + + // Shutdown the PVT server + if (pvtServer != nullptr) + { + // Stop the PVT server + if (settings.debugPvtServer && (!inMainMenu)) + systemPrintln("PVT server stopping"); + pvtServer->stop(); + delete pvtServer; + pvtServer = nullptr; + } + + // Stop using the network + if (pvtServerState != PVT_SERVER_STATE_OFF) + { + networkUserClose(NETWORK_USER_PVT_SERVER); + + // The PVT server is now off + pvtServerSetState(PVT_SERVER_STATE_OFF); + pvtServerTimer = millis(); + } +} + +// Stop the PVT server client +void pvtServerStopClient(int index) +{ + bool connected; + bool dataSent; + + // Done with this client connection + if ((settings.debugPvtServer || PERIODIC_DISPLAY(PD_PVT_SERVER_DATA)) && (!inMainMenu)) + { + PERIODIC_CLEAR(PD_PVT_SERVER_DATA); + + // Determine the shutdown reason + connected = pvtServerClient[index]->connected() + && (!(pvtServerClientWriteError & (1 << index))); + dataSent = ((millis() - pvtServerTimer) < PVT_SERVER_CLIENT_DATA_TIMEOUT) + || (pvtServerClientDataSent & (1 << index)); + if (!dataSent) + systemPrintf("PVT Server: No data sent over %d seconds\r\n", + PVT_SERVER_CLIENT_DATA_TIMEOUT / 1000); + if (!connected) + systemPrintf("PVT Server: Link to client broken\r\n"); + systemPrintf("PVT server client %d disconnected from %d.%d.%d.%d\r\n", + index, + pvtServerClientIpAddress[index][0], + pvtServerClientIpAddress[index][1], + pvtServerClientIpAddress[index][2], + pvtServerClientIpAddress[index][3]); + } + + // Shutdown the PVT server client link + pvtServerClient[index]->stop(); + pvtServerClientConnected &= ~(1 << index); + pvtServerClientWriteError &= ~(1 << index); +} + +// Update the PVT server state +void pvtServerUpdate() +{ + bool connected; + bool dataSent; + int index; + IPAddress ipAddress; + + // Shutdown the PVT server when the mode or setting changes + DMW_st(pvtServerSetState, pvtServerState); + if (NEQ_RTK_MODE(pvtServerMode) || (!settings.enablePvtServer)) + { + if (pvtServerState > PVT_SERVER_STATE_OFF) + pvtServerStop(); + } + + /* + PVT Server state machine + + .---------------->PVT_SERVER_STATE_OFF + | | + | pvtServerStop | settings.enablePvtServer + | | + | V + +<----------PVT_SERVER_STATE_NETWORK_STARTED + ^ | + | | networkUserConnected + | | + | V + +<--------------PVT_SERVER_STATE_RUNNING + ^ | + | | network failure + | | + | V + '-----------PVT_SERVER_STATE_WAIT_NO_CLIENTS + */ + + switch (pvtServerState) + { + default: + break; + + // Wait until the PVT server is enabled + case PVT_SERVER_STATE_OFF: + // Determine if the PVT server should be running + if (EQ_RTK_MODE(pvtServerMode) && settings.enablePvtServer && (!wifiIsConnected())) + { + if (networkUserOpen(NETWORK_USER_PVT_SERVER, NETWORK_TYPE_ACTIVE)) + { + if (settings.debugPvtServer && (!inMainMenu)) + systemPrintln("PVT server starting the network"); + pvtServerSetState(PVT_SERVER_STATE_NETWORK_STARTED); + } + } + break; + + // Wait until the network is connected + case PVT_SERVER_STATE_NETWORK_STARTED: + // Determine if the network has failed + if (networkIsShuttingDown(NETWORK_USER_PVT_SERVER)) + // Failed to connect to to the network, attempt to restart the network + pvtServerStop(); + + // Wait for the network to connect to the media + else if (networkUserConnected(NETWORK_USER_PVT_SERVER)) + { + // Delay before starting the PVT server + if ((millis() - pvtServerTimer) >= (1 * 1000)) + { + // The network type and host provide a valid configuration + pvtServerTimer = millis(); + + // Start the PVT server + if (pvtServerStart()) + pvtServerSetState(PVT_SERVER_STATE_RUNNING); + } + } + break; + + // Handle client connections and link failures + case PVT_SERVER_STATE_RUNNING: + // Determine if the network has failed + if ((!settings.enablePvtServer) || networkIsShuttingDown(NETWORK_USER_PVT_SERVER)) + { + if ((settings.debugPvtServer || PERIODIC_DISPLAY(PD_PVT_SERVER_DATA)) && (!inMainMenu)) + { + PERIODIC_CLEAR(PD_PVT_SERVER_DATA); + systemPrintln("PVT server initiating shutdown"); + } + + // Network connection failed, attempt to restart the network + pvtServerStop(); + break; + } + + // Walk the list of PVT server clients + for (index = 0; index < PVT_SERVER_MAX_CLIENTS; index++) + { + // Determine if the client data structure is in use + if (pvtServerClientConnected & (1 << index)) + { + // Data structure in use + // Check for a working PVT server client connection + connected = pvtServerClient[index]->connected(); + dataSent = ((millis() - pvtServerTimer) < PVT_SERVER_CLIENT_DATA_TIMEOUT) + || (pvtServerClientDataSent & (1 << index)); + if (connected && dataSent) + { + // Display this client connection + if (PERIODIC_DISPLAY(PD_PVT_SERVER_DATA) && (!inMainMenu)) + { + PERIODIC_CLEAR(PD_PVT_SERVER_DATA); + systemPrintf("PVT server client %d connected to %d.%d.%d.%d\r\n", + index, + pvtServerClientIpAddress[index][0], + pvtServerClientIpAddress[index][1], + pvtServerClientIpAddress[index][2], + pvtServerClientIpAddress[index][3]); + } + } + + // Shutdown the PVT server client link + else + pvtServerStopClient(index); + } + } + + // Walk the list of PVT server clients + for (index = 0; index < PVT_SERVER_MAX_CLIENTS; index++) + { + // Determine if the client data structure is in use + if (!(pvtServerClientConnected & (1 << index))) + { + WiFiClient client; + + // Data structure not in use + // Check for another PVT server client + client = pvtServer->available(); + + // Done if no PVT server client found + if (!client) + break; + + // Start processing the new PVT server client connection + pvtServerClient[index] = new NetworkWiFiClient(client); + pvtServerClientIpAddress[index] = pvtServerClient[index]->remoteIP(); + pvtServerClientConnected |= 1 << index; + pvtServerClientDataSent |= 1 << index; + if ((settings.debugPvtServer || PERIODIC_DISPLAY(PD_PVT_SERVER_DATA)) && (!inMainMenu)) + { + PERIODIC_CLEAR(PD_PVT_SERVER_DATA); + systemPrintf("PVT server client %d connected to %d.%d.%d.%d\r\n", + index, + pvtServerClientIpAddress[index][0], + pvtServerClientIpAddress[index][1], + pvtServerClientIpAddress[index][2], + pvtServerClientIpAddress[index][3]); + } + } + } + + // Check for data moving across the connections + if ((millis() - pvtServerTimer) >= PVT_SERVER_CLIENT_DATA_TIMEOUT) + { + // Restart the data verification + pvtServerTimer = millis(); + pvtServerClientDataSent = 0; + } + break; + } + + // Periodically display the PVT state + if (PERIODIC_DISPLAY(PD_PVT_SERVER_STATE) && (!inMainMenu)) + pvtServerSetState(pvtServerState); +} + +// Verify the PVT server tables +void pvtServerValidateTables() +{ + char line[128]; + + // Verify PVT_SERVER_MAX_CLIENTS + if ((sizeof(pvtServerClientConnected) * 8) < PVT_SERVER_MAX_CLIENTS) + { + snprintf(line, sizeof(line), + "Please set PVT_SERVER_MAX_CLIENTS <= %d or increase size of pvtServerClientConnected", + sizeof(pvtServerClientConnected) * 8); + reportFatalError(line); + } + if (pvtServerStateNameEntries != PVT_SERVER_STATE_MAX) + reportFatalError("Fix pvtServerStateNameEntries to match PvtServerStates"); +} + +// Zero the PVT server client tails +void pvtServerZeroTail() +{ + int index; + + for (index = 0; index < PVT_SERVER_MAX_CLIENTS; index++) + pvtServerClientTails[index] = 0; +} + +#endif // COMPILE_WIFI diff --git a/Firmware/RTK_Surveyor/PvtUdpServer.ino b/Firmware/RTK_Surveyor/PvtUdpServer.ino new file mode 100644 index 000000000..36637b0e4 --- /dev/null +++ b/Firmware/RTK_Surveyor/PvtUdpServer.ino @@ -0,0 +1,383 @@ +/* +pvtUdpServer.ino + + The PVT (position, velocity and time) server sits on top of the network layer + and sends position data to one or more computers or cell phones for display. + + Satellite ... Satellite + | | | + | | | + | V | + | RTK | + '------> Base <------' + Station + | + | NTRIP Server sends correction data + V + NTRIP Caster + | + | NTRIP Client receives correction data + | + V + Bluetooth RTK Network: PVT Client + .---------------- Rover ----------------------------------. + | | | + | | Network: PVT Server | + | PVT data | Position, velocity & time data | PVT data + | V | + | Computer or | + '------------> Cell Phone <-------------------------------' + for display + + PVT UDP Server Testing: + + RTK Express(+) with WiFi enabled, PvtUdpServer enabled, PvtUdpPort setup, + Smartphone with QField: + + Network Setup: + Connect the Smartphone and the RTK Express to the same Network (e.g. the Smartphones HotSpot). + + QField Setup: + * Open a project + * Open the left menu + * Click on the gear icon + * Click on Settings + * Click on Positioning + * Add a new Positioning device + * Set Connection type to UDP (NMEA) + * Set the Adress to + * Set the Port to the value of the specified PvtUdpPort (default 10110) + * Optional: give it a name (e.g. RTK Express UDP) + * Click on the Checkmark + * Make sure the new device is set as the Postioning device in use + * Exit the menus + + 1. Long press on the location icon in the lower right corner + + 2. Enable Show Position Information + + 3. Verify that the displayed coordinates, fix tpe etc. are valid +*/ + +#if COMPILE_NETWORK + +//---------------------------------------- +// Constants +//---------------------------------------- + +// Define the PVT server states +enum PvtUdpServerStates +{ + PVT_UDP_SERVER_STATE_OFF = 0, + PVT_UDP_SERVER_STATE_NETWORK_STARTED, + PVT_UDP_SERVER_STATE_RUNNING, + // Insert new states here + PVT_UDP_SERVER_STATE_MAX // Last entry in the state list +}; + +const char * const pvtUdpServerStateName[] = +{ + "PVT_UDP_SERVER_STATE_OFF", + "PVT_UDP_SERVER_STATE_NETWORK_STARTED", + "PVT_UDP_SERVER_STATE_RUNNING", +}; + +const int pvtUdpServerStateNameEntries = sizeof(pvtUdpServerStateName) / sizeof(pvtUdpServerStateName[0]); + +const RtkMode_t pvtUdpServerMode = RTK_MODE_BASE_FIXED + | RTK_MODE_BASE_SURVEY_IN + | RTK_MODE_ROVER; + +//---------------------------------------- +// Locals +//---------------------------------------- + +// PVT UDP server +static NetworkUDP *pvtUdpServer = nullptr; +static uint8_t pvtUdpServerState; +static uint32_t pvtUdpServerTimer; +static volatile RING_BUFFER_OFFSET pvtUdpServerTail; +//---------------------------------------- +// PVT UDP Server handleGnssDataTask Support Routines +//---------------------------------------- + +// Send data as broadcast +int32_t pvtUdpServerSendDataBroadcast(uint8_t *data, uint16_t length) +{ + if (!length) + return 0; + + // Send the data as broadcast + if (settings.enablePvtUdpServer && online.pvtUdpServer && wifiIsConnected()) + { + pvtUdpServer->beginPacket(WiFi.broadcastIP(), settings.pvtUdpServerPort); + pvtUdpServer->write(data, length); + if(pvtUdpServer->endPacket()){ + if ((settings.debugPvtUdpServer || PERIODIC_DISPLAY(PD_PVT_UDP_SERVER_BROADCAST_DATA)) && (!inMainMenu)) + { + systemPrintf("PVT UDP Server wrote %d bytes as broadcast on port %d\r\n", length, settings.pvtUdpServerPort); + PERIODIC_CLEAR(PD_PVT_UDP_SERVER_BROADCAST_DATA); + } + } + // Failed to write the data + else if ((settings.debugPvtUdpServer || PERIODIC_DISPLAY(PD_PVT_UDP_SERVER_BROADCAST_DATA)) && (!inMainMenu)) + { + PERIODIC_CLEAR(PD_PVT_UDP_SERVER_BROADCAST_DATA); + systemPrintf("PVT UDP Server failed to write %d bytes as broadcast\r\n", length); + length = 0; + } + } + return length; +} + +// Send PVT data as broadcast +int32_t pvtUdpServerSendData(uint16_t dataHead) +{ + int32_t usedSpace = 0; + + int32_t bytesToSend; + + uint16_t tail; + + tail = pvtUdpServerTail; + + // Determine the amount of PVT data in the buffer + bytesToSend = dataHead - tail; + if (bytesToSend < 0) + bytesToSend += settings.gnssHandlerBufferSize; + if (bytesToSend > 0) + { + // Reduce bytes to send if we have more to send then the end of the buffer + // We'll wrap next loop + if ((tail + bytesToSend) > settings.gnssHandlerBufferSize) + bytesToSend = settings.gnssHandlerBufferSize - tail; + + // Send the data + bytesToSend = pvtUdpServerSendDataBroadcast(&ringBuffer[tail], bytesToSend); + + // Assume all data was sent, wrap the buffer pointer + tail += bytesToSend; + if (tail >= settings.gnssHandlerBufferSize) + tail -= settings.gnssHandlerBufferSize; + + // Update space available for use in UART task + bytesToSend = dataHead - tail; + if (bytesToSend < 0) + bytesToSend += settings.gnssHandlerBufferSize; + if (usedSpace < bytesToSend) + usedSpace = bytesToSend; + } + + pvtUdpServerTail = tail; + + // Return the amount of space that PVT server client is using in the buffer + return usedSpace; +} + +// Remove previous messages from the ring buffer +void discardPvtUdpServerBytes(RING_BUFFER_OFFSET previousTail, RING_BUFFER_OFFSET newTail) +{ + int index; + uint16_t tail; + + tail = pvtUdpServerTail; + if (previousTail < newTail) + { + // No buffer wrap occurred + if ((tail >= previousTail) && (tail < newTail)) + pvtUdpServerTail = newTail; + } + else + { + // Buffer wrap occurred + if ((tail >= previousTail) || (tail < newTail)) + pvtUdpServerTail = newTail; + } +} + +//---------------------------------------- +// PVT Server Routines +//---------------------------------------- + +// Update the state of the PVT server state machine +void pvtUdpServerSetState(uint8_t newState) +{ + if ((settings.debugPvtUdpServer || PERIODIC_DISPLAY(PD_PVT_UDP_SERVER_STATE)) && (!inMainMenu)) + { + if (pvtUdpServerState == newState) + systemPrint("*"); + else + systemPrintf("%s --> ", pvtUdpServerStateName[pvtUdpServerState]); + } + pvtUdpServerState = newState; + if ((settings.debugPvtUdpServer || PERIODIC_DISPLAY(PD_PVT_UDP_SERVER_STATE)) && (!inMainMenu)) + { + PERIODIC_CLEAR(PD_PVT_UDP_SERVER_STATE); + if (newState >= PVT_UDP_SERVER_STATE_MAX) + { + systemPrintf("Unknown PVT UDP Server state: %d\r\n", pvtUdpServerState); + reportFatalError("Unknown PVT UDP Server state"); + } + else + systemPrintln(pvtUdpServerStateName[pvtUdpServerState]); + } +} + +// Start the PVT server +bool pvtUdpServerStart() +{ + IPAddress localIp; + + if (settings.debugPvtUdpServer && (!inMainMenu)) + systemPrintln("PVT UDP server starting"); + + // Start the PVT server + pvtUdpServer = new NetworkUDP(NETWORK_USER_PVT_UDP_SERVER); + if (!pvtUdpServer) + return false; + + pvtUdpServer->begin(settings.pvtUdpServerPort); + online.pvtUdpServer = true; + systemPrintf("PVT UDP server online, broadcasting to port %d\r\n", settings.pvtUdpServerPort); + return true; +} + +// Stop the PVT UDP server +void pvtUdpServerStop() +{ + int index; + + // Notify the rest of the system that the PVT server is shutting down + if (online.pvtUdpServer) + { + // Notify the UART2 tasks of the PVT server shutdown + online.pvtUdpServer = false; + delay(5); + } + + // Shutdown the PVT server + if (pvtUdpServer != nullptr) + { + // Stop the PVT server + if (settings.debugPvtUdpServer && (!inMainMenu)) + systemPrintln("PVT UDP server stopping"); + pvtUdpServer->stop(); + delete pvtUdpServer; + pvtUdpServer = nullptr; + } + + // Stop using the network + if (pvtUdpServerState != PVT_UDP_SERVER_STATE_OFF) + { + networkUserClose(NETWORK_USER_PVT_UDP_SERVER); + + // The PVT server is now off + pvtUdpServerSetState(PVT_UDP_SERVER_STATE_OFF); + pvtUdpServerTimer = millis(); + } +} + +// Update the PVT server state +void pvtUdpServerUpdate() +{ + bool connected; + bool dataSent; + int index; + IPAddress ipAddress; + + // Shutdown the PVT UDP server when the mode or setting changes + DMW_st(pvtUdpServerSetState, pvtUdpServerState); + if (NEQ_RTK_MODE(pvtUdpServerMode) || (!settings.enablePvtUdpServer)) + { + if (pvtUdpServerState > PVT_UDP_SERVER_STATE_OFF) + pvtUdpServerStop(); + } + + /* + PVT UDP Server state machine + + .--------------->PVT_UDP_SERVER_STATE_OFF + | | + | pvtUdpServerStop | settings.enablePvtUdpServer + | | + | V + +<---------PVT_UDP_SERVER_STATE_NETWORK_STARTED + ^ | + | | networkUserConnected + | | + | V + '--------------PVT_UDP_SERVER_STATE_RUNNING + */ + + switch (pvtUdpServerState) + { + default: + break; + + // Wait until the PVT server is enabled + case PVT_UDP_SERVER_STATE_OFF: + // Determine if the PVT server should be running + if (EQ_RTK_MODE(pvtUdpServerMode) && settings.enablePvtUdpServer && (!wifiIsConnected())) + { + if (networkUserOpen(NETWORK_USER_PVT_UDP_SERVER, NETWORK_TYPE_ACTIVE)) + { + if (settings.debugPvtUdpServer && (!inMainMenu)) + systemPrintln("PVT UDP server starting the network"); + pvtUdpServerSetState(PVT_UDP_SERVER_STATE_NETWORK_STARTED); + } + } + break; + + // Wait until the network is connected + case PVT_UDP_SERVER_STATE_NETWORK_STARTED: + // Determine if the network has failed + if (networkIsShuttingDown(NETWORK_USER_PVT_UDP_SERVER)) + // Failed to connect to to the network, attempt to restart the network + pvtUdpServerStop(); + + // Wait for the network to connect to the media + else if (networkUserConnected(NETWORK_USER_PVT_UDP_SERVER)) + { + // Delay before starting the PVT server + if ((millis() - pvtUdpServerTimer) >= (1 * 1000)) + { + // The network type and host provide a valid configuration + pvtUdpServerTimer = millis(); + + // Start the PVT UDP server + if (pvtUdpServerStart()) + pvtUdpServerSetState(PVT_UDP_SERVER_STATE_RUNNING); + } + } + break; + + // Handle client connections and link failures + case PVT_UDP_SERVER_STATE_RUNNING: + // Determine if the network has failed + if ((!settings.enablePvtUdpServer) || networkIsShuttingDown(NETWORK_USER_PVT_UDP_SERVER)) + { + if ((settings.debugPvtUdpServer || PERIODIC_DISPLAY(PD_PVT_UDP_SERVER_DATA)) && (!inMainMenu)) + { + PERIODIC_CLEAR(PD_PVT_UDP_SERVER_DATA); + systemPrintln("PVT server initiating shutdown"); + } + + // Network connection failed, attempt to restart the network + pvtUdpServerStop(); + break; + } + break; + } + + // Periodically display the PVT state + if (PERIODIC_DISPLAY(PD_PVT_UDP_SERVER_STATE) && (!inMainMenu)) + pvtUdpServerSetState(pvtUdpServerState); +} + +// Zero the PVT server client tails +void pvtUdpServerZeroTail() +{ + pvtUdpServerTail = 0; +} + +#endif // COMPILE_NETWORK diff --git a/Firmware/RTK_Surveyor/RTK_Surveyor.ino b/Firmware/RTK_Surveyor/RTK_Surveyor.ino index a4e92779d..ff33c7ffe 100644 --- a/Firmware/RTK_Surveyor/RTK_Surveyor.ino +++ b/Firmware/RTK_Surveyor/RTK_Surveyor.ino @@ -3,470 +3,1464 @@ SparkFun Electronics Nathan Seidle - This firmware runs the core of the SparkFun RTK Surveyor product. It runs on an ESP32 + This firmware runs the core of the SparkFun RTK products. It runs on an ESP32 and communicates with the ZED-F9P. - Select the ESP32 Dev Module from the boards list. This maps the same pins to the ESP32-WROOM module. - Select 'Minimal SPIFFS (1.9MB App)' from the partition list. This will enable SD firmware updates. + Compiled with Arduino v1.8.15 with ESP32 core v2.0.2. + + For compilation instructions see https://docs.sparkfun.com/SparkFun_RTK_Firmware/firmware_update/#compiling-source Special thanks to Avinab Malla for guidance on getting xTasks implemented. The RTK Surveyor implements classic Bluetooth SPP to transfer data from the ZED-F9P to the phone and receive any RTCM from the phone and feed it back - to the ZED-F9P to achieve RTK: F9PSerialWriteTask(), F9PSerialReadTask(). - - A settings file is accessed on microSD if available otherwise settings are pulled from - ESP32's emulated EEPROM. - - As of v1.2, the heap is approximately 94072 during Rover Fix, 142260 during WiFi Casting. This is - important to maintain as unit will begin to have stability issues at ~30k. - - The main loop handles lower priority updates such as: - Fuel gauge checking and power LED color update - Setup switch monitoring (module configure between Rover and Base) - Text menu interactions - - Main Menu (Display MAC address / broadcast name): - (Done) GNSS - Configure measurement rate, SBAS - (Done) Log - Control messages logged to SD - (Done) Broadcast - Control messages sent over BT SPP - (Done) Base - Enter fixed coordinates, survey-in settings, WiFi/Caster settings, - (Done) Ports - Configure Radio and Data port baud rates - (Done) Test menu - (Done) Firmware upgrade menu - Enable various debug outputs sent over BT + to the ZED-F9P to achieve RTK: btReadTask(), gnssReadTask(). + Settings are loaded from microSD if available otherwise settings are pulled from ESP32's file system LittleFS. */ -const int FIRMWARE_VERSION_MAJOR = 1; -const int FIRMWARE_VERSION_MINOR = 4; - -//Define the RTK board identifier: +#define COMPILE_ETHERNET // Comment out to remove Ethernet (W5500) support +#define COMPILE_WIFI // Comment out to remove WiFi functionality + +#ifdef COMPILE_WIFI +#define COMPILE_AP // Requires WiFi. Comment out to remove Access Point functionality +#define COMPILE_ESPNOW // Requires WiFi. Comment out to remove ESP-Now functionality. +#endif // COMPILE_WIFI + +#define COMPILE_BT // Comment out to remove Bluetooth functionality +#define COMPILE_L_BAND // Comment out to remove L-Band functionality +#define COMPILE_SD_MMC // Comment out to remove REFERENCE_STATION microSD SD_MMC support +// #define REF_STN_GNSS_DEBUG //Uncomment this line to output GNSS library debug messages on serialGNSS. Ref Stn only. +// Needs ENABLE_DEVELOPER + +#if defined(COMPILE_WIFI) || defined(COMPILE_ETHERNET) +#define COMPILE_NETWORK true +#else // COMPILE_WIFI || COMPILE_ETHERNET +#define COMPILE_NETWORK false +#endif // COMPILE_WIFI || COMPILE_ETHERNET + +// Always define ENABLE_DEVELOPER to enable its use in conditional statements +#ifndef ENABLE_DEVELOPER +#define ENABLE_DEVELOPER \ + true // This enable specials developer modes (don't check power button at startup). Passed in from compiler flags. +#endif // ENABLE_DEVELOPER + +// This is passed in from compiler extra flags +#ifndef POINTPERFECT_TOKEN +#define FIRMWARE_VERSION_MAJOR 99 +#define FIRMWARE_VERSION_MINOR 99 +#endif // POINTPERFECT_TOKEN + +// Define the RTK board identifier: // This is an int which is unique to this variant of the RTK Surveyor hardware which allows us -// to make sure that the settings in EEPROM are correct for this version of the RTK +// to make sure that the settings stored in flash (LittleFS) are correct for this version of the RTK // (sizeOfSettings is not necessarily unique and we want to avoid problems when swapping from one variant to another) // It is the sum of: // the major firmware version * 0x10 // the minor firmware version #define RTK_IDENTIFIER (FIRMWARE_VERSION_MAJOR * 0x10 + FIRMWARE_VERSION_MINOR) +#define NTRIP_SERVER_MAX 2 + +#ifdef COMPILE_ETHERNET +#include // http://librarymanager/All#Arduino_Ethernet +#include "SparkFun_WebServer_ESP32_W5500.h" //http://librarymanager/All#SparkFun_WebServer_ESP32_W5500 v1.5.5 +#endif // COMPILE_ETHERNET + +#ifdef COMPILE_WIFI +#include "ESP32OTAPull.h" //http://librarymanager/All#ESP-OTA-Pull Used for getting +#include "esp_wifi.h" //Needed for esp_wifi_set_protocol() +#include //Built-in. +#include //Built-in. +#include //Built-in. Needed for ThingStream API for ZTP +#include //http://librarymanager/All#PubSubClient_MQTT_Lightweight by Nick O'Leary v2.8.0 Used for MQTT obtaining of keys +#include //Built-in. +#include //Built-in. +#include //Built-in. +#endif // COMPILE_WIFI + +#if COMPILE_NETWORK +#include // http://librarymanager/All#SSLClientESP32 +#include "X509_Certificate_Bundle.h" // Root certificates +#endif // COMPILE_NETWORK + #include "settings.h" -//Hardware connections +#define MAX_CPU_CORES 2 +#define IDLE_COUNT_PER_SECOND 515400 //Found by empirical sketch +#define IDLE_TIME_DISPLAY_SECONDS 5 +#define MAX_IDLE_TIME_COUNT (IDLE_TIME_DISPLAY_SECONDS * IDLE_COUNT_PER_SECOND) +#define MILLISECONDS_IN_A_SECOND 1000 +#define MILLISECONDS_IN_A_MINUTE (60 * MILLISECONDS_IN_A_SECOND) +#define MILLISECONDS_IN_AN_HOUR (60 * MILLISECONDS_IN_A_MINUTE) +#define MILLISECONDS_IN_A_DAY (24 * MILLISECONDS_IN_AN_HOUR) + +#define SECONDS_IN_A_MINUTE 60 +#define SECONDS_IN_AN_HOUR (60 * SECONDS_IN_A_MINUTE) +#define SECONDS_IN_A_DAY (24 * SECONDS_IN_AN_HOUR) + +// Hardware connections //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= -//These pins are set in beginBoard() -int pin_batteryLevelLED_Red; -int pin_batteryLevelLED_Green; -int pin_positionAccuracyLED_1cm; -int pin_positionAccuracyLED_10cm; -int pin_positionAccuracyLED_100cm; -int pin_baseStatusLED; -int pin_bluetoothStatusLED; -int pin_baseSwitch; -int pin_microSD_CS; -int pin_zed_tx_ready; -int pin_zed_reset; -int pin_batteryLevel_alert; - -int pin_muxA; -int pin_muxB; -int pin_powerSenseAndControl; -int pin_setupButton; -int pin_powerFastOff; -int pin_dac26; -int pin_adc39; -int pin_peripheralPowerControl; +// These pins are set in beginBoard() +int pin_batteryLevelLED_Red = -1; +int pin_batteryLevelLED_Green = -1; +int pin_positionAccuracyLED_1cm = -1; +int pin_positionAccuracyLED_10cm = -1; +int pin_positionAccuracyLED_100cm = -1; +int pin_baseStatusLED = -1; +int pin_bluetoothStatusLED = -1; +int pin_microSD_CS = -1; +int pin_zed_tx_ready = -1; +int pin_zed_reset = -1; +int pin_batteryLevel_alert = -1; + +int pin_muxA = -1; +int pin_muxB = -1; +int pin_powerSenseAndControl = -1; +int pin_setupButton = -1; +int pin_powerFastOff = -1; +int pin_dac26 = -1; +int pin_adc39 = -1; +int pin_peripheralPowerControl = -1; + +int pin_radio_rx = -1; +int pin_radio_tx = -1; +int pin_radio_rst = -1; +int pin_radio_pwr = -1; +int pin_radio_cts = -1; +int pin_radio_rts = -1; + +int pin_Ethernet_CS = -1; +int pin_Ethernet_Interrupt = -1; +int pin_GNSS_CS = -1; +int pin_GNSS_TimePulse = -1; +int pin_microSD_CardDetect = -1; + +int pin_PICO = 23; +int pin_POCI = 19; +int pin_SCK = 18; //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= -//I2C for GNSS, battery gauge, display, accelerometer +// I2C for GNSS, battery gauge, display, accelerometer //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- #include //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- -//EEPROM for storing settings +// LittleFS for storing settings for different user profiles //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- -#include -#define EEPROM_SIZE 4096 //ESP32 emulates EEPROM in non-volatile storage (external flash IC). Max is 508k. +#include + +#define MAX_PROFILE_COUNT 8 +uint8_t activeProfiles = 0; // Bit vector indicating which profiles are active +uint8_t displayProfile; // Range: 0 - (MAX_PROFILE_COUNT - 1) +uint8_t profileNumber = MAX_PROFILE_COUNT; // profileNumber gets set once at boot to save loading time +char profileNames[MAX_PROFILE_COUNT][50]; // Populated based on names found in LittleFS and SD +char settingsFileName[60]; // Contains the %s_Settings_%d.txt with current profile number set + +char stationCoordinateECEFFileName[60]; // Contains the /StationCoordinates-ECEF_%d.csv with current profile number set +char stationCoordinateGeodeticFileName[60]; // Contains the /StationCoordinates-Geodetic_%d.csv with current profile + // number set +const int COMMON_COORDINATES_MAX_STATIONS = 50; // Record upto 50 ECEF and Geodetic commonly used stations //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- -//Handy library for setting ESP32 system time to GNSS time +// Handy library for setting ESP32 system time to GNSS time //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- -#include //http://librarymanager/All#ESP32Time +#include //http://librarymanager/All#ESP32Time by FBiego v2.0.0 ESP32Time rtc; +unsigned long syncRTCInterval = 1000; // To begin, sync RTC every second. Interval can be increased once sync'd. //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- -//microSD Interface +// microSD Interface //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- #include -#include "SdFat.h" -SdFat sd; -SPIClass spi = SPIClass(VSPI); //We need to pass the class into SD.begin so we can set the SPI freq in beginSD() +#include "SdFat.h" //http://librarymanager/All#sdfat_exfat by Bill Greiman. Currently uses v2.1.1 +SdFat *sd; + +#include "FileSdFatMMC.h" //Hybrid SdFat and SD_MMC file access -char platformFilePrefix[40] = "SFE_Surveyor"; //Sets the prefix for logs and settings files +#define platformFilePrefix platformFilePrefixTable[productVariant] // Sets the prefix for logs and settings files -SdFile ubxFile; //File that all gnss ubx messages setences are written to -unsigned long lastUBXLogSyncTime = 0; //Used to record to SD every half second -int startLogTime_minutes = 0; //Mark when we start logging so we can stop logging after maxLogTime_minutes +FileSdFatMMC *ubxFile; // File that all GNSS ubx messages sentences are written to +unsigned long lastUBXLogSyncTime = 0; // Used to record to SD every half second +int startLogTime_minutes = 0; // Mark when we start any logging so we can stop logging after maxLogTime_minutes +int startCurrentLogTime_minutes = + 0; // Mark when we start this specific log file so we can close it after x minutes and start a new one -//System crashes if two tasks access a file at the same time -//So we use a semaphore to see if file system is available -SemaphoreHandle_t xFATSemaphore; +// System crashes if two tasks access a file at the same time +// So we use a semaphore to see if file system is available +SemaphoreHandle_t sdCardSemaphore; +TickType_t loggingSemaphoreWait_ms = 10 / portTICK_PERIOD_MS; const TickType_t fatSemaphore_shortWait_ms = 10 / portTICK_PERIOD_MS; const TickType_t fatSemaphore_longWait_ms = 200 / portTICK_PERIOD_MS; + +// Display used/free space in menu and config page +uint64_t sdCardSize = 0; +uint64_t sdFreeSpace = 0; +bool outOfSDSpace = false; +const uint32_t sdMinAvailableSpace = 10000000; // Minimum available bytes before SD is marked as out of space + +// Controls Logging Icon type +typedef enum LoggingType +{ + LOGGING_UNKNOWN = 0, + LOGGING_STANDARD, + LOGGING_PPP, + LOGGING_CUSTOM +} LoggingType; +LoggingType loggingType = LOGGING_UNKNOWN; + +FileSdFatMMC *managerTempFile; // File used for uploading or downloading in file manager section of AP config +bool managerFileOpen = false; + +TaskHandle_t sdSizeCheckTaskHandle = nullptr; // Store handles so that we can kill the task once size is found +const uint8_t sdSizeCheckTaskPriority = 0; // 3 being the highest, and 0 being the lowest +const int sdSizeCheckStackSize = 3000; +bool sdSizeCheckTaskComplete = false; + +char logFileName[sizeof("SFE_Reference_Station_230101_120101.ubx_plusExtraSpace")] = {0}; //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- -//Connection settings to NTRIP Caster -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +// Over-the-Air (OTA) update support +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- -#define COMPILE_WIFI 1 //Comment out to remove all WiFi functionality +#include //http://librarymanager/All#Arduino_JSON_messagepack v6.19.4 + +#include "esp_ota_ops.h" //Needed for partition counting and updateFromSD + +#define NETWORK_STOP(type) \ + { \ + if (settings.debugNetworkLayer) \ + systemPrintf("networkStop called by %s %d\r\n", __FILE__, __LINE__); \ + networkStop(type); \ + } #ifdef COMPILE_WIFI -#include -#include "esp_wifi.h" //Needed for init/deinit of resources to free up RAM -WiFiClient caster; -#endif -const char * ntrip_server_name = "SparkFun_RTK_Surveyor"; +#define WIFI_STOP() \ +{ \ + if (settings.debugWifiState) \ + systemPrintf("wifiStop called by %s %d\r\n", __FILE__, __LINE__); \ + wifiStop(); \ +} -unsigned long lastServerSent_ms = 0; //Time of last data pushed to caster -unsigned long lastServerReport_ms = 0; //Time of last report of caster bytes sent -int maxTimeBeforeHangup_ms = 10000; //If we fail to get a complete RTCM frame after 10s, then disconnect from caster +#endif // COMPILE_WIFI -uint32_t casterBytesSent = 0; //Just a running total -uint32_t casterResponseWaitStartTime = 0; //Used to detect if caster service times out -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +#define OTA_FIRMWARE_JSON_URL \ + "https://raw.githubusercontent.com/sparkfun/SparkFun_RTK_Firmware_Binaries/main/RTK-Firmware.json" +#define OTA_RC_FIRMWARE_JSON_URL \ + "https://raw.githubusercontent.com/sparkfun/SparkFun_RTK_Firmware_Binaries/main/RTK-RC-Firmware.json" +bool apConfigFirmwareUpdateInProcess = false; // Goes true once WiFi is connected and OTA pull begins +unsigned int binBytesSent = 0; // Tracks firmware bytes sent over WiFi OTA update via AP config. -//GNSS configuration +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +// Connection settings to NTRIP Caster //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= -#include //http://librarymanager/All#SparkFun_u-blox_GNSS +#include "base64.h" //Built-in. Needed for NTRIP Client credential encoding. -//Note: There are two prevalent versions of the ZED-F9P: v1.12 (part# -01B) and v1.13 (-02B). -//v1.13 causes the RTK LED to not function if SBAS is enabled. To avoid this, we -//disable SBAS by default. +bool enableRCFirmware = false; // Goes true from AP config page +bool currentlyParsingData = false; // Goes true when we hit 750ms timeout with new data -char zedFirmwareVersion[20]; //The string looks like 'FWVER=HPG 1.12'. Output to debug menu and settings file. +// Give up connecting after this number of attempts +// Connection attempts are throttled to increase the time between attempts +int wifiMaxConnectionAttempts = 500; +int wifiOriginalMaxConnectionAttempts = wifiMaxConnectionAttempts; // Modified during L-Band WiFi connect attempt +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= -// Extend the class for getModuleInfo. Used to diplay ZED-F9P firmware version in debug menu. -class SFE_UBLOX_GNSS_ADD : public SFE_UBLOX_GNSS +// GNSS configuration +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +#include //http://librarymanager/All#SparkFun_u-blox_GNSS_v3 v3.0.5 + +char zedFirmwareVersion[20]; // The string looks like 'HPG 1.12'. Output to system status menu and settings file. +char neoFirmwareVersion[20]; // Output to system status menu. +uint8_t zedFirmwareVersionInt = 0; // Controls which features (constellations) can be configured (v1.12 doesn't support + // SBAS). Note: will fail above 2.55! +uint8_t zedModuleType = PLATFORM_F9P; // Controls which messages are supported and configured +char zedUniqueId[11] = {'0', '0', '0', '0', '0', '0', + '0', '0', '0', '0', 0}; // Output to system status menu and log file. + +// Use Michael's lock/unlock methods to prevent the UART2 task from calling checkUblox during a sendCommand and +// waitForResponse. Also prevents pushRawData from being called too. +class SFE_UBLOX_GNSS_SUPER_DERIVED : public SFE_UBLOX_GNSS_SUPER { public: - boolean getModuleInfo(uint16_t maxWait = 1100); //Queries module, texts + // SemaphoreHandle_t gnssSemaphore = nullptr; - struct minfoStructure // Structure to hold the module info (uses 341 bytes of RAM) + // Revert to a simple bool lock. The Mutex was causing occasional panics caused by + // vTaskPriorityDisinheritAfterTimeout in lock() (I think possibly / probably caused by the GNSS not being pinned to + // one core? + bool iAmLocked = false; + + bool createLock(void) + { + // if (gnssSemaphore == nullptr) + // gnssSemaphore = xSemaphoreCreateMutex(); + // return gnssSemaphore; + + return true; + } + bool lock(void) + { + // return (xSemaphoreTake(gnssSemaphore, 2100) == pdPASS); + + if (!iAmLocked) + { + iAmLocked = true; + return true; + } + + unsigned long startTime = millis(); + while (((millis() - startTime) < 2100) && (iAmLocked)) + delay(1); // Yield + + if (!iAmLocked) + { + iAmLocked = true; + return true; + } + + return false; + } + void unlock(void) { - char swVersion[30]; - char hwVersion[10]; - uint8_t extensionNo = 0; - char extension[10][30]; - } minfo; + // xSemaphoreGive(gnssSemaphore); + + iAmLocked = false; + } + void deleteLock(void) + { + // vSemaphoreDelete(gnssSemaphore); + // gnssSemaphore = nullptr; + } }; -SFE_UBLOX_GNSS_ADD i2cGNSS; +SFE_UBLOX_GNSS_SUPER_DERIVED theGNSS; + +#ifdef COMPILE_L_BAND +static SFE_UBLOX_GNSS_SUPER i2cLBand; // NEO-D9S + +void checkRXMCOR(UBX_RXM_COR_data_t *ubxDataStruct); +#endif + +volatile struct timeval + gnssSyncTv; // This holds the time the RTC was sync'd to GNSS time via Time Pulse interrupt - used by NTP +struct timeval previousGnssSyncTv; // This holds the time of the previous RTC sync + +// These globals are updated regularly via the storePVTdata callback +unsigned long pvtArrivalMillis = 0; +bool pvtUpdated = false; +double latitude; +double longitude; +float altitude; +float horizontalAccuracy; +bool validDate; +bool validTime; +bool confirmedDate; +bool confirmedTime; +bool fullyResolved; +uint32_t tAcc; +uint8_t gnssDay; +uint8_t gnssMonth; +uint16_t gnssYear; +uint8_t gnssHour; +uint8_t gnssMinute; +uint8_t gnssSecond; +int32_t gnssNano; +uint16_t mseconds; +uint8_t numSV; +uint8_t fixType; +uint8_t carrSoln; + +unsigned long timTpArrivalMillis = 0; +bool timTpUpdated = false; +uint32_t timTpEpoch; +uint32_t timTpMicros; + +uint8_t aStatus = SFE_UBLOX_ANTENNA_STATUS_DONTKNOW; + +unsigned long lastARPLog = 0; // Time of the last ARP log event +bool newARPAvailable = false; +int64_t ARPECEFX = 0; // ARP ECEF is 38-bit signed +int64_t ARPECEFY = 0; +int64_t ARPECEFZ = 0; +uint16_t ARPECEFH = 0; + +const byte haeNumberOfDecimals = 8; // Used for printing and transmitting lat/lon +bool lBandCommunicationEnabled = false; +bool lBandForceGetKeys = false; //Used to allow key update from display +unsigned long rtcmLastPacketReceived = 0; //Monitors the last time we received RTCM. Proctors PMP vs RTCM prioritization. +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +// GPS parse table +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +// Define the parsers that get included +#define PARSE_NMEA_MESSAGES +#define PARSE_RTCM_MESSAGES +#define PARSE_UBLOX_MESSAGES + +// Build the GPS_PARSE_TABLE macro +#include "GpsMessageParser.h" // Include the parser + +// Create the GPS message parse table instance +GPS_PARSE_TABLE; -//Used for config ZED for things not supported in library: getPortSettings, getSerialRate, getNMEASettings, getRTCMSettings -//This array holds the payload data bytes. Global so that we can use between config functions. -#define MAX_PAYLOAD_SIZE 384 // Override MAX_PAYLOAD_SIZE for getModuleInfo which can return up to 348 bytes -uint8_t settingPayload[MAX_PAYLOAD_SIZE]; //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= -//Battery fuel gauge and PWM LEDs +// Battery fuel gauge and PWM LEDs //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= -#include // Click here to get the library: http://librarymanager/All#SparkFun_MAX1704x_Fuel_Gauge_Arduino_Library +#include //Click here to get the library: http://librarymanager/All#SparkFun_MAX1704x_Fuel_Gauge_Arduino_Library SFE_MAX1704X lipo(MAX1704X_MAX17048); -// setting PWM properties -const int freq = 5000; +// RTK Surveyor LED PWM properties +const int pwmFreq = 5000; const int ledRedChannel = 0; const int ledGreenChannel = 1; -const int resolution = 8; +const int ledBTChannel = 2; +const int pwmResolution = 8; -int battLevel = 0; //SOC measured from fuel gauge, in %. Used in multiple places (display, serial debug, log) +int pwmFadeAmount = 10; +int btFadeLevel = 0; + +int battLevel = 0; // SOC measured from fuel gauge, in %. Used in multiple places (display, serial debug, log) float battVoltage = 0.0; float battChangeRate = 0.0; //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= -//Hardware serial and BT buffers +// Hardware serial and BT buffers //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= -//We use a local copy of the BluetoothSerial library so that we can increase the RX buffer. See issue: https://github.com/sparkfun/SparkFun_RTK_Surveyor/issues/18 +#ifdef COMPILE_BT +// See bluetoothSelect.h for implemenation +#include "bluetoothSelect.h" +#endif // COMPILE_BT -#define COMPILE_BT 1 //Comment out to disable all Bluetooth +#define platformPrefix platformPrefixTable[productVariant] // Sets the prefix for broadcast names -#ifdef COMPILE_BT -#include "src/BluetoothSerial/BluetoothSerial.h" -BluetoothSerial SerialBT; -#include "esp_bt.h" //Core access is needed for BT stop. See customBTstop() for more info. -#include "esp_gap_bt_api.h" //Needed for setting of pin. See issue: https://github.com/sparkfun/SparkFun_RTK_Surveyor/issues/5 -#endif +#include //Required for uart_set_rx_full_threshold() on cores > 2) +#define AVERAGE_SENTENCE_LENGTH_IN_BYTES 32 +RING_BUFFER_OFFSET * rbOffsetArray; +uint16_t rbOffsetEntries; -#define SERIAL_SIZE_RX 4096 //Reduced from 16384 to make room for WiFi/NTRIP server capabilities -uint8_t rBuffer[SERIAL_SIZE_RX]; //Buffer for reading from F9P to SPP -uint8_t wBuffer[SERIAL_SIZE_RX]; //Buffer for writing from incoming SPP to F9P -TaskHandle_t F9PSerialReadTaskHandle = NULL; //Store handles so that we can kill them if user goes into WiFi NTRIP Server mode -TaskHandle_t F9PSerialWriteTaskHandle = NULL; //Store handles so that we can kill them if user goes into WiFi NTRIP Server mode +uint8_t *ringBuffer; // Buffer for reading from F9P. At 230400bps, 23040 bytes/s. If SD blocks for 250ms, we need 23040 + // * 0.25 = 5760 bytes worst case. +TaskHandle_t gnssReadTaskHandle = + nullptr; // Store handles so that we can kill them if user goes into WiFi NTRIP Server mode +const int gnssReadTaskStackSize = 2500; -TaskHandle_t startUART2TaskHandle = NULL; //Dummy task to start UART2 on core 0. -bool uart2Started = false; +TaskHandle_t handleGnssDataTaskHandle = nullptr; +const int handleGnssDataTaskStackSize = 3000; -//Reduced stack size from 10,000 to 2,000 to make room for WiFi/NTRIP server capabilities -const int readTaskStackSize = 2000; -const int writeTaskStackSize = 2000; +TaskHandle_t pinUART2TaskHandle = nullptr; // Dummy task to start hardware on an assigned core +volatile bool uart2pinned = false; // This variable is touched by core 0 but checked by core 1. Must be volatile. -char incomingBTTest = 0; //Stores incoming text over BT when in test mode -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +TaskHandle_t pinI2CTaskHandle = nullptr; // Dummy task to start hardware on an assigned core +volatile bool i2cPinned = false; // This variable is touched by core 0 but checked by core 1. Must be volatile. + +TaskHandle_t pinBluetoothTaskHandle = nullptr; // Dummy task to start hardware on an assigned core +volatile bool bluetoothPinned = false; // This variable is touched by core 0 but checked by core 1. Must be volatile. + +volatile static int combinedSpaceRemaining = 0; // Overrun indicator +volatile static long fileSize = 0; // Updated with each write +int bufferOverruns = 0; // Running count of possible data losses since power-on + +bool zedUartPassed = false; // Goes true during testing if ESP can communicate with ZED over UART +const uint8_t btEscapeCharacter = '+'; +const uint8_t btMaxEscapeCharacters = 3; // Number of characters needed to enter command mode over B -//External Display //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= -#include //Click here to get the library: http://librarymanager/All#SparkFun_Micro_OLED -#include "icons.h" -#define PIN_RESET 9 -#define DC_JUMPER 1 -MicroOLED oled(PIN_RESET, DC_JUMPER); +// External Display +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +#include //http://librarymanager/All#SparkFun_Qwiic_Graphic_OLED //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= -//Firmware binaries loaded from SD +// Firmware binaries loaded from SD //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= #include int binCount = 0; -char binFileNames[10][50]; -const char* forceFirmwareFileName = "RTK_Surveyor_Firmware_Force.bin"; //File that will be loaded at startup regardless of user input +const int maxBinFiles = 10; +char binFileNames[maxBinFiles][50]; +const char *forceFirmwareFileName = + "RTK_Surveyor_Firmware_Force.bin"; // File that will be loaded at startup regardless of user input +int binBytesLastUpdate = 0; // Allows websocket notification to be sent every 100k bytes //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= -//Low frequency tasks +// Low frequency tasks //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= #include Ticker btLEDTask; -float btLEDTaskPace = 0.5; //Seconds - -//Ticker battCheckTask; -//float battCheckTaskPace = 2.0; //Seconds +float btLEDTaskPace2Hz = 0.5; +float btLEDTaskPace33Hz = 0.03; //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= -//Accelerometer for bubble leveling +// Accelerometer for bubble leveling //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= #include "SparkFun_LIS2DH12.h" //Click here to get the library: http://librarymanager/All#SparkFun_LIS2DH12 SPARKFUN_LIS2DH12 accel; //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= -//Global variables +// Buttons - Interrupt driven and debounce //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- -uint8_t unitMACAddress[6]; //Use MAC address in BT broadcast and display -char deviceName[20]; //The serial string that is broadcast. Ex: 'Surveyor Base-BC61' -const byte menuTimeout = 15; //Menus will exit/timeout after this number of seconds -bool inTestMode = false; //Used to re-route BT traffic while in test sub menu -int systemTime_minutes = 0; //Used to test if logging is less than max minutes -uint32_t powerPressedStartTime = 0; //Times how long user has been holding power button, used for power down -uint8_t debounceDelay = 20; //ms to delay between button reads +#include //http://librarymanager/All#JC_Button v2.1.2 +Button *setupBtn = nullptr; // We can't instantiate the buttons here because we don't yet know what pin numbers to use +Button *powerBtn = nullptr; + +TaskHandle_t ButtonCheckTaskHandle = nullptr; +const uint8_t ButtonCheckTaskPriority = 1; // 3 being the highest, and 0 being the lowest +const int buttonTaskStackSize = 2000; + +const int shutDownButtonTime = 2000; // ms press and hold before shutdown +unsigned long lastRockerSwitchChange = 0; // If quick toggle is detected (less than 500ms), enter WiFi AP Config mode +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + +// Webserver for serving config page from ESP32 as Acess Point +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +#ifdef COMPILE_WIFI +#ifdef COMPILE_AP + +#include "ESPAsyncWebServer.h" //Get from: https://github.com/me-no-dev/ESPAsyncWebServer v1.2.3 +#include "form.h" + +AsyncWebServer *webserver = nullptr; +AsyncWebSocket *websocket = nullptr; + +char *settingsCSV = nullptr; // Push large array onto heap + +#endif // COMPILE_AP +#endif // COMPILE_WIFI + +// Because the incoming string is longer than max len, there are multiple callbacks so we +// use a global to combine the incoming +#define AP_CONFIG_SETTING_SIZE 5000 +char *incomingSettings = nullptr; +int incomingSettingsSpot = 0; +unsigned long timeSinceLastIncomingSetting = 0; +unsigned long lastDynamicDataUpdate = 0; +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +// PointPerfect Corrections +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +#if __has_include("tokens.h") +#include "tokens.h" +#endif // __has_include("tokens.h") + +float lBandEBNO = 0.0; // Used on system status menu +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +// ESP NOW for multipoint wireless broadcasting over 2.4GHz +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +#ifdef COMPILE_ESPNOW + +#include + +uint8_t espnowOutgoing[250]; // ESP NOW has max of 250 characters +unsigned long espnowLastAdd; // Tracks how long since last byte was added to the outgoing buffer +uint8_t espnowOutgoingSpot = 0; // ESP Now has max of 250 characters +uint16_t espnowBytesSent = 0; // May be more than 255 +uint8_t receivedMAC[6]; // Holds the broadcast MAC during pairing + +int packetRSSI = 0; +unsigned long lastEspnowRssiUpdate = 0; + +#endif // COMPILE_ESPNOW + +int espnowRSSI = 0; +const uint8_t ESPNOW_MAX_PEERS = 5; // Maximum of 5 rovers + +// Ethernet +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +#ifdef COMPILE_ETHERNET +IPAddress ethernetIPAddress; +IPAddress ethernetDNS; +IPAddress ethernetGateway; +IPAddress ethernetSubnetMask; + +class derivedEthernetUDP : public EthernetUDP +{ + public: + uint8_t getSockIndex() + { + return sockindex; // sockindex is protected in EthernetUDP. A derived class can access it. + } +}; +volatile struct timeval ethernetNtpTv; // This will hold the time the Ethernet NTP packet arrived +bool ntpLogIncreasing; + +#endif // COMPILE_ETHERNET + +unsigned long lastEthernetCheck = 0; // Prevents cable checking from continually happening +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +// Global variables +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +#define lbandMACAddress btMACAddress +uint8_t wifiMACAddress[6]; // Display this address in the system menu +uint8_t btMACAddress[6]; // Display this address when Bluetooth is enabled, otherwise display wifiMACAddress +uint8_t ethernetMACAddress[6]; // Display this address when Ethernet is enabled, otherwise display wifiMACAddress +char deviceName[70]; // The serial string that is broadcast. Ex: 'Surveyor Base-BC61' +const uint16_t menuTimeout = 60 * 10; // Menus will exit/timeout after this number of seconds +int systemTime_minutes = 0; // Used to test if logging is less than max minutes +uint32_t powerPressedStartTime = 0; // Times how long user has been holding power button, used for power down +bool inMainMenu = false; // Set true when in the serial config menu system. +bool btPrintEcho = false; // Set true when in the serial config menu system via Bluetooth. +bool btPrintEchoExit = false; // When true, exit all config menus. uint32_t lastBattUpdate = 0; uint32_t lastDisplayUpdate = 0; +bool forceDisplayUpdate = false; // Goes true when setup is pressed, causes display to refresh real time uint32_t lastSystemStateUpdate = 0; +bool forceSystemStateUpdate = false; // Set true to avoid update wait uint32_t lastAccuracyLEDUpdate = 0; -uint32_t lastBaseLEDupdate = 0; //Controls the blinking of the Base LED - -uint32_t lastFileReport = 0; //When logging, print file record stats every few seconds -long lastStackReport = 0; //Controls the report rate of stack highwater mark within a task -uint32_t lastHeapReport = 0; //Report heap every 1s if option enabled -uint32_t lastTaskHeapReport = 0; //Report task heap every 1s if option enabled -uint32_t lastCasterLEDupdate = 0; //Controls the cycling of position LEDs during casting - -uint32_t lastSatelliteDishIconUpdate = 0; -bool satelliteDishIconDisplayed = false; //Toggles as lastSatelliteDishIconUpdate goes above 1000ms -uint32_t lastCrosshairIconUpdate = 0; -bool crosshairIconDisplayed = false; //Toggles as lastCrosshairIconUpdate goes above 1000ms +uint32_t lastBaseLEDupdate = 0; // Controls the blinking of the Base LED + +uint32_t lastFileReport = 0; // When logging, print file record stats every few seconds +long lastStackReport = 0; // Controls the report rate of stack highwater mark within a task +uint32_t lastHeapReport = 0; // Report heap every 1s if option enabled +uint32_t lastTaskHeapReport = 0; // Report task heap every 1s if option enabled +uint32_t lastCasterLEDupdate = 0; // Controls the cycling of position LEDs during casting +uint32_t lastRTCAttempt = 0; // Wait 1000ms between checking GNSS for current date/time +uint32_t lastRTCSync = 0; // Time in millis when the RTC was last sync'd +bool rtcSyncd = false; // Set to true when the RTC has been sync'd via TP pulse +uint32_t lastPrintPosition = 0; // For periodic display of the position +uint32_t lastPrintState = 0; // For periodic display of the RTK state (solution) + uint32_t lastBaseIconUpdate = 0; -bool baseIconDisplayed = false; //Toggles as lastBaseIconUpdate goes above 1000ms -uint32_t lastWifiIconUpdate = 0; -bool wifiIconDisplayed = false; //Toggles as lastWifiIconUpdate goes above 1000ms -uint32_t lastLoggingIconUpdate = 0; -int loggingIconDisplayed = 0; //Increases every 500ms while logging +bool baseIconDisplayed = false; // Toggles as lastBaseIconUpdate goes above 1000ms +uint8_t loggingIconDisplayed = 0; // Increases every 500ms while logging +uint8_t espnowIconDisplayed = 0; // Increases every 500ms while transmitting uint64_t lastLogSize = 0; -bool logIncreasing = false; //Goes true when log file is greater than lastLogSize -bool reuseLastLog = false; //Goes true if we have a reset due to software (rather than POR) +bool logIncreasing = false; // Goes true when log file is greater than lastLogSize or logPosition changes +bool reuseLastLog = false; // Goes true if we have a reset due to software (rather than POR) -uint32_t lastRTCMPacketSent = 0; //Used to count RTCM packets sent during base mode -uint32_t rtcmPacketsSent = 0; //Used to count RTCM packets sent via processRTCM() +uint16_t rtcmPacketsSent = 0; // Used to count RTCM packets sent via processRTCM() +uint32_t rtcmBytesSent = 0; +uint32_t rtcmLastReceived = 0; -uint32_t maxSurveyInWait_s = 60L * 15L; //Re-start survey-in after X seconds +uint32_t maxSurveyInWait_s = 60L * 15L; // Re-start survey-in after X seconds -uint32_t totalWriteTime = 0; //Used to calculate overall write speed using SdFat library +uint16_t svinObservationTime = 0; // Use globals so we don't have to request these values multiple times (slow response) +float svinMeanAccuracy = 0; -bool setupByPowerButton = false; //We can change setup via tapping power button +uint32_t lastSetupMenuChange = 0; // Auto-selects the setup menu option after 1500ms +uint32_t lastTestMenuChange = 0; // Avoids exiting the test menu for at least 1 second + +bool firstRoverStart = false; // Used to detect if user is toggling power button at POR to enter test menu + +bool newEventToRecord = false; // Goes true when INT pin goes high +uint32_t triggerCount = 0; // Global copy - TM2 event counter +uint32_t triggerTowMsR = 0; // Global copy - Time Of Week of rising edge (ms) +uint32_t triggerTowSubMsR = 0; // Global copy - Millisecond fraction of Time Of Week of rising edge in nanoseconds +uint32_t triggerAccEst = 0; // Global copy - Accuracy estimate in nanoseconds + +bool firstPowerOn = true; // After boot, apply new settings to ZED if user switches between base or rover +unsigned long splashStart = 0; // Controls how long the splash is displayed for. Currently min of 2s. +bool restartBase = false; // If user modifies any NTRIP Server settings, we need to restart the base +bool restartRover = false; // If user modifies any NTRIP Client settings, we need to restart the rover + +unsigned long startTime = 0; // Used for checking longest running functions +bool lbandCorrectionsReceived = false; // Used to display L-Band SIV icon when corrections are successfully decrypted +unsigned long lastLBandDecryption = 0; // Timestamp of last successfully decrypted PMP message +volatile bool mqttMessageReceived = false; // Goes true when the subscribed MQTT channel reports back +uint8_t leapSeconds = 0; // Gets set if GNSS is online +unsigned long rtcWaitTime = 0; // At poweron, we give the RTC a few seconds to update during PointPerfect Key checking + +TaskHandle_t idleTaskHandle[MAX_CPU_CORES]; +uint32_t max_idle_count = MAX_IDLE_TIME_COUNT; + +bool firstRadioSpotBlink = false; // Controls when the shared icon space is toggled +unsigned long firstRadioSpotTimer = 0; +bool secondRadioSpotBlink = false; // Controls when the shared icon space is toggled +unsigned long secondRadioSpotTimer = 0; +bool thirdRadioSpotBlink = false; // Controls when the shared icon space is toggled +unsigned long thirdRadioSpotTimer = 0; + +bool bluetoothIncomingRTCM = false; +bool bluetoothOutgoingRTCM = false; +bool netIncomingRTCM = false; +bool netOutgoingRTCM = false; +bool espnowIncomingRTCM = false; +bool espnowOutgoingRTCM = false; + +static RtcmTransportState rtcmParsingState = RTCM_TRANSPORT_STATE_WAIT_FOR_PREAMBLE_D3; +uint16_t failedParserMessages_UBX = 0; +uint16_t failedParserMessages_RTCM = 0; +uint16_t failedParserMessages_NMEA = 0; + +unsigned long btLastByteReceived = 0; // Track when last BT transmission was received. +const long btMinEscapeTime = 2000; // Bluetooth serial traffic must stop this amount before an escape char is recognized +uint8_t btEscapeCharsReceived = 0; // Used to enter command mode + +bool externalPowerConnected = false; // Goes true when a high voltage is seen on power control pin + +// configureViaEthernet: +// Set to true if configureViaEthernet.txt exists in LittleFS. +// Causes setup and loop to skip any code which would cause SPI or interrupts to be initialized. +// This is to allow SparkFun_WebServer_ESP32_W5500 to have _exclusive_ access to WiFi, SPI and Interrupts. +bool configureViaEthernet = false; + +unsigned long lbandTimeFloatStarted = 0; // Monitors the ZED during L-Band reception if a fix takes too long +int lbandRestarts = 0; +unsigned long lbandTimeToFix = 0; +unsigned long lbandLastReport = 0; + +volatile PeriodicDisplay_t periodicDisplay; + +unsigned long shutdownNoChargeTimer = 0; + +RtkMode_t rtkMode; // Mode of operation + +bool sdCardForcedOffline = false; //Goes true if a isPresent() test passes, but then sdFat fails to mount SD card. +//See issue: https://github.com/sparkfun/SparkFun_RTK_Firmware/issues/758 + +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + +#define DEAD_MAN_WALKING_ENABLED 0 + +#if DEAD_MAN_WALKING_ENABLED + +// Developer subsitutions enabled by changing DEAD_MAN_WALKING_ENABLED +// from 0 to 1 +volatile bool deadManWalking; +#define DMW_if if (deadManWalking) +#define DMW_c(string) DMW_if systemPrintf("%s called\r\n", string); +#define DMW_ds(routine, dataStructure) DMW_if routine(dataStructure, dataStructure->state); +#define DMW_m(string) DMW_if systemPrintln(string); +#define DMW_r(string) DMW_if systemPrintf("%s returning\r\n",string); +#define DMW_rs(string, status) DMW_if systemPrintf("%s returning %d\r\n",string, (int32_t)status); +#define DMW_st(routine, state) DMW_if routine(state); + +#define START_DEAD_MAN_WALKING \ +{ \ + deadManWalking = true; \ + \ + /* Output as much as possible to identify the location of the failure */ \ + settings.printDebugMessages = true; \ + settings.enableI2Cdebug = true; \ + settings.enableHeapReport = true; \ + settings.enableTaskReports = true; \ + settings.enablePrintState = true; \ + settings.enablePrintPosition = true; \ + settings.enablePrintIdleTime = true; \ + settings.enablePrintBatteryMessages = true; \ + settings.enablePrintRoverAccuracy = true; \ + settings.enablePrintBadMessages = true; \ + settings.enablePrintLogFileMessages = true; \ + settings.enablePrintLogFileStatus = true; \ + settings.enablePrintRingBufferOffsets = true; \ + settings.enablePrintStates = true; \ + settings.enablePrintDuplicateStates = true; \ + settings.enablePrintRtcSync = true; \ + settings.enablePrintBufferOverrun = true; \ + settings.enablePrintSDBuffers = true; \ + settings.periodicDisplay = (PeriodicDisplay_t)-1; \ + settings.enablePrintEthernetDiag = true; \ + settings.debugWifiState = true; \ + settings.debugNetworkLayer = true; \ + settings.printNetworkStatus = true; \ + settings.debugNtripClientRtcm = true; \ + settings.debugNtripClientState = true; \ + settings.debugNtripServerRtcm = true; \ + settings.debugNtripServerState = true; \ + settings.debugPvtClient = true; \ + settings.debugPvtServer = true; \ + settings.debugPvtUdpServer = true; \ +} + +#else // 0 + +// Production substitutions +#define deadManWalking 0 +#define DMW_if if (0) +#define DMW_c(string) +#define DMW_ds(routine, dataStructure) +#define DMW_m(string) +#define DMW_r(string) +#define DMW_rs(string, status) +#define DMW_st(routine, state) + +#endif // 0 + +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +/* + +---------------------------------------+ +----------+ + | ESP32 | | GNSS | Antenna + +-------------+ | | | | | + | Phone | | .-----------. .--------. |27 42| | | + | RTCM |--->|-->| |--------->| |-->|----->|TXD, MISO | | + | | | | Bluetooth | | UART 2 | | | UART1 | | + | NMEA + RTCM |<---|<--| |<-------+-| |<--|<-----|RXD, MOSI |<----' + +-------------+ | '-----------' | '--------' |28 43| | + | | | | | + .---------+ | | | | | + / uSD Card | | | | | | + / | | .----. V | | | + | Log File |<---|<--| |<--------------+ | | |47 + | | | | | | | | D_SEL |<---- N/C (1) + | Profile # |<-->|<->| SD |<--> Profile | | | 0 = SPI | + | | | | | | | | 1 = I2C | + | Settings |<-->|<->| |<--> Settings | | | UART1 | + | | | '----' | | | | + +------------+ | | | | | + | .--------. | | | | + | | |<----------' | | | + | | USB | | | | + USB UART <--->|<->| Serial |<-- Debug Output | | | + (Config ESP32) | | | | | | + | | |<-- Serial Config | | UART 2 |<--> Radio + | '--------' | | | Connector + | | | | (Correction + | .------. | | | Data) + Browser <--->|<->| |<---> WiFi Config | | | + | | | | | | + +--------------+ | | | | | USB |<--> USB UART + | |<--|<--| WiFi |<---- NMEA + RTCM <-. | | | (Config UBLOX) + | NTRIP Caster | | | | | | | | + | |-->|-->| |-----------. | |6 46| | + +--------------+ | | | | | .----|<-----|TXREADY | + | '------' | | v | | | + | | .-----. | | | + | '----->| | |33 44| | + | | |<->|<---->|SDA, CS_N | + | Commands -------->| I2C | | | I2C | + | | |-->|----->|SCL, CLK | + | Status <--------| | |36 45| | + | '-----' | +----------+ + | | + +---------------------------------------+ + 26| |24 A B + | | 0 0 = X0, Y0 + V V 0 1 = X1, Y1 + +-------+ 1 0 = X2, Y2 + | B A | 1 1 = X3, Y3 + | | + | X0|<--- GNSS UART1 TXD + | | + | X1|<--- GNSS PPS STAT + 3 <---|X | + | X2|<--- SCL + | | + | X3|<--- DAC2 + Data Port | | + | Y0|----> ZED UART1 RXD + | | + | Y1|<--> ZED EXT INT + 2 <-->|Y | + | Y2|---> SDA + | | + | Y3|---> ADC39 + | | + | MUX | + +-------+ +*/ +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +// Initialize any globals that can't easily be given default values + +void initializeGlobals() +{ + gnssSyncTv.tv_sec = 0; + gnssSyncTv.tv_usec = 0; + previousGnssSyncTv.tv_sec = 0; + previousGnssSyncTv.tv_usec = 0; +} -uint16_t svinObservationTime = 0; //Use globals so we don't have to request these values multiple times (slow response) -float svinMeanAccuracy = 0; //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- void setup() { - Serial.begin(115200); //UART0 for programming and debugging + initializeGlobals(); // Initialize any global variables that can't be given default values - Wire.begin(); //Start I2C on core 1 - Wire.setClock(400000); + Serial.begin(115200); // UART0 for programming and debugging - beginBoard(); //Determine what hardware platform we are running on + DMW_c("verifyTables"); + verifyTables (); // Verify the consistency of the internal tables - beginDisplay(); //Check if an external Qwiic OLED is attached + DMW_c("identifyBoard"); + identifyBoard(); // Determine what hardware platform we are running on - beginLEDs(); //LED and PWM setup + DMW_c("initializePowerPins"); + initializePowerPins(); // Initialize any essential power pins - e.g. enable power for the Display - //Start EEPROM and SD for settings, and display for output - //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- - beginEEPROM(); + DMW_c("beginMux"); + beginMux(); // Must come before I2C activity to avoid external devices from corrupting the bus. See issue #474: + // https://github.com/sparkfun/SparkFun_RTK_Firmware/issues/474 - //eepromErase(); + DMW_c("beginI2C"); + beginI2C(); - beginSD(); //Test if SD is present - if (online.microSD == true) - { - Serial.println(F("microSD online")); - scanForFirmware(); //See if SD card contains new firmware that should be loaded at startup - } + DMW_c("beginDisplay"); + beginDisplay(); // Start display to be able to display any errors - loadSettings(); //Attempt to load settings after SD is started so we can read the settings file if available - //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + DMW_c("findSpiffsPartition"); + if (!findSpiffsPartition()) + { + printPartitionTable(); // Print the partition tables + reportFatalError("spiffs partition not found!"); + } + + DMW_c("beginFS"); + beginFS(); // Start LittleFS file system for settings + + DMW_c("checkConfigureViaEthernet"); + configureViaEthernet = + checkConfigureViaEthernet(); // Check if going into dedicated configureViaEthernet (STATE_CONFIG_VIA_ETH) mode + + DMW_c("beginGNSS"); + beginGNSS(); // Connect to GNSS to get module type + + DMW_c("beginBoard"); + beginBoard(); // Now finish setting up the board and check the on button + + DMW_c("displaySplash"); + displaySplash(); // Display the RTK product name and firmware version + + DMW_c("beginLEDs"); + beginLEDs(); // LED and PWM setup + + DMW_c("beginSD"); + beginSD(); // Test if SD is present + + DMW_c("loadSettings"); + loadSettings(); // Attempt to load settings after SD is started so we can read the settings file if available + + DMW_c("beginIdleTasks"); + beginIdleTasks(); // Enable processor load calculations + + DMW_c("beginUART2"); + beginUART2(); // Start UART2 on core 0, used to receive serial from ZED and pass out over SPP + + DMW_c("beginFuelGauge"); + beginFuelGauge(); // Configure battery fuel guage monitor - beginUART2(); //Start UART2 on core 0, used to receive serial from ZED and pass out over SPP + DMW_c("configureGNSS"); + configureGNSS(); // Configure ZED module - beginFuelGauge(); //Configure battery fuel guage monitor - checkBatteryLevels(); //Force display so you see battery level immediately at power on + DMW_c("ethernetBegin"); + ethernetBegin(); // Start-up the Ethernet connection - beginGNSS(); //Connect and configure ZED-F9P + DMW_c("beginAccelerometer"); + beginAccelerometer(); - beginAccelerometer(); + DMW_c("beginLBand"); + beginLBand(); // Begin L-Band - beginSystemState(); //Determine initial system state + DMW_c("beginExternalTriggers"); + beginExternalTriggers(); // Configure the time pulse output and TM2 input - Serial.flush(); //Complete any previous prints + DMW_c("beginInterrupts"); + beginInterrupts(); // Begin the TP and W5500 interrupts - danceLEDs(); //Turn on LEDs like a car dashboard + DMW_c("beginSystemState"); + beginSystemState(); // Determine initial system state. Start task for button monitoring. + + DMW_c("updateRTC"); + updateRTC(); // The GNSS likely has time/date. Update ESP32 RTC to match. Needed for PointPerfect key expiration. + + Serial.flush(); // Complete any previous prints + + log_d("Boot time: %d", millis()); + + DMW_c("danceLEDs"); + danceLEDs(); // Turn on LEDs like a car dashboard } void loop() { - i2cGNSS.checkUblox(); //Regularly poll to get latest data and any RTCM + static uint32_t lastPeriodicDisplay; + + // Determine which items are periodically displayed + if ((millis() - lastPeriodicDisplay) >= settings.periodicDisplayInterval) + { + lastPeriodicDisplay = millis(); + periodicDisplay = settings.periodicDisplay; + + // Reboot the system after a specified timeout + if (((lastPeriodicDisplay / 1000) > settings.rebootSeconds) && (!inMainMenu)) + ESP.restart(); + } + if (deadManWalking) + periodicDisplay = (PeriodicDisplay_t)-1; + + if (online.gnss == true) + { + DMW_c("theGNSS.checkUblox"); + theGNSS.checkUblox(); // Regularly poll to get latest data and any RTCM + DMW_c("theGNSS.checkCallbacks"); + theGNSS.checkCallbacks(); // Process any callbacks: ie, eventTriggerReceived + } + + DMW_c("updateSystemState"); + updateSystemState(); - checkButtons(); //Change system state as needed + DMW_c("updateBattery"); + updateBattery(); - updateSystemState(); + DMW_c("updateDisplay"); + updateDisplay(); - updateBattLEDs(); + DMW_c("updateRTC"); + updateRTC(); // Set system time to GNSS once we have fix - updateDisplay(); + DMW_c("updateSD"); + updateSD(); // Check if SD needs to be started or is at max capacity - updateRTC(); //Set system time to GNSS once we have fix + DMW_c("updateLogs"); + updateLogs(); // Record any new data. Create or close files as needed. - updateLogs(); //Record any new data. Create or close files as needed. + DMW_c("reportHeap"); + reportHeap(); // If debug enabled, report free heap - reportHeap(); //If debug enabled, report free heap + DMW_c("updateSerial"); + updateSerial(); // Menu system via ESP32 USB connection - //Menu system via ESP32 USB connection - if (Serial.available()) menuMain(); //Present user menu + DMW_c("networkUpdate"); + networkUpdate(); // Maintain the network connections - //Convert current system time to minutes. This is used in F9PSerialReadTask()/updateLogs() to see if we are within max log window. - systemTime_minutes = millis() / 1000L / 60; + DMW_c("updateLBand"); + updateLBand(); // Check if we've recently received PointPerfect corrections or not - delay(10); //A small delay prevents panic if no other I2C or functions are called + DMW_c("updateRadio"); + updateRadio(); // Check if we need to finish sending any RTCM over link radio + + DMW_c("printPosition"); + printPosition(); // Periodically print GNSS coordinates if enabled + + DMW_c("printRTKState"); + printRTKState(); // Periodically print RTK state (solution) if enabled + + // A small delay prevents panic if no other I2C or functions are called + delay(10); } -//Create or close files as needed (startup or as user changes settings) -//Push new data to log as needed -void updateLogs() +// Monitor if SD card is online or not +// Attempt to remount SD card if card is offline but present +// Capture card size when mounted +void updateSD() { - if (online.logging == false && settings.enableLogging == true) - { - beginLogging(); - } - else if (online.logging == true && settings.enableLogging == false) - { - //Close down file - if (xSemaphoreTake(xFATSemaphore, fatSemaphore_longWait_ms) == pdPASS) + if (online.microSD == false) { - ubxFile.sync(); - ubxFile.close(); - online.logging = false; - //xSemaphoreGive(xFATSemaphore); //Do not release semaphore + // Are we offline because we are out of space? + if (outOfSDSpace == true) + { + if (sdPresent() == false) // Poll card to see if user has removed card + outOfSDSpace = false; + } + else if (sdPresent() == true) // Poll card to see if a card is inserted + { + beginSD(); // Attempt to start SD + if(online.microSD == true) + systemPrintln("SD inserted"); + } } - } - //Report file sizes to show recording is working - if (online.logging == true) - { - if (millis() - lastFileReport > 5000) + if (online.logging == true && sdCardSize > 0 && + sdFreeSpace < sdMinAvailableSpace) // Stop logging if we are below the min { - long fileSize = 0; + log_d("Logging stopped. SD full."); + outOfSDSpace = true; + endSD(false, true); //(alreadyHaveSemaphore, releaseSemaphore) Close down file. + return; + } - //Attempt to access file system. This avoids collisions with file writing from other functions like recordSystemSettingsToFile() and F9PSerialReadTask() - if (xSemaphoreTake(xFATSemaphore, fatSemaphore_shortWait_ms) == pdPASS) - { - fileSize = ubxFile.fileSize(); + if (online.microSD && sdCardSize == 0) + beginSDSizeCheckTask(); // Start task to determine SD card size - xSemaphoreGive(xFATSemaphore); - } + if (sdSizeCheckTaskComplete == true) + deleteSDSizeCheckTask(); - if (fileSize > 0) - { - lastFileReport = millis(); - Serial.printf("UBX file size: %ld", fileSize); + // Check if SD card is still present + if (productVariant == REFERENCE_STATION) + { + if (sdPresent() == false) + endSD(false, true); //(alreadyHaveSemaphore, releaseSemaphore) Close down SD. + } +} - if ((systemTime_minutes - startLogTime_minutes) < settings.maxLogTime_minutes) +// Create or close files as needed (startup or as user changes settings) +// Push new data to log as needed +void updateLogs() +{ + // Convert current system time to minutes. This is used in F9PSerialReadTask()/updateLogs() to see if we are within + // max log window. + systemTime_minutes = millis() / 1000L / 60; + + // If we are in AP config, don't touch the SD card + if (systemState == STATE_WIFI_CONFIG_NOT_STARTED || systemState == STATE_WIFI_CONFIG) + return; + + if (online.microSD == false) + return; // We can't log if there is no SD + + if (outOfSDSpace == true) + return; // We can't log if we are out of SD space + + if (online.logging == false && settings.enableLogging == true) + { + beginLogging(); + + setLoggingType(); // Determine if we are standard, PPP, or custom. Changes logging icon accordingly. + } + else if (online.logging == true && settings.enableLogging == false) + { + // Close down file + endSD(false, true); + } + else if (online.logging == true && settings.enableLogging == true && + (systemTime_minutes - startCurrentLogTime_minutes) >= settings.maxLogLength_minutes) + { + if (settings.runLogTest == false) + endSD(false, true); // Close down file. A new one will be created at the next calling of updateLogs(). + else if (settings.runLogTest == true) + updateLogTest(); + } + + if (online.logging == true) + { + // Record any pending trigger events + if (newEventToRecord == true) { - //Calculate generation and write speeds every 5 seconds - uint32_t delta = fileSize - lastLogSize; - Serial.printf(" - Generation rate: %0.1fkB/s", delta / 5.0 / 1000.0); - Serial.printf(" - Write speed: %0.1fkB/s", delta / (totalWriteTime / 1000000.0) / 1000.0); + systemPrintln("Recording event"); + + // Record trigger count with Time Of Week of rising edge (ms), Millisecond fraction of Time Of Week of + // rising edge (ns), and accuracy estimate (ns) + char eventData[82]; // Max NMEA sentence length is 82 + snprintf(eventData, sizeof(eventData), "%d,%d,%d,%d", triggerCount, triggerTowMsR, triggerTowSubMsR, + triggerAccEst); + + char nmeaMessage[82]; // Max NMEA sentence length is 82 + createNMEASentence(CUSTOM_NMEA_TYPE_EVENT, nmeaMessage, sizeof(nmeaMessage), + eventData); // textID, buffer, sizeOfBuffer, text + + if (xSemaphoreTake(sdCardSemaphore, fatSemaphore_shortWait_ms) == pdPASS) + { + markSemaphore(FUNCTION_EVENT); + + ubxFile->println(nmeaMessage); + + xSemaphoreGive(sdCardSemaphore); + newEventToRecord = false; + } + else + { + char semaphoreHolder[50]; + getSemaphoreFunction(semaphoreHolder); + + // While a retry does occur during the next loop, it is possible to loose + // trigger events if they occur too rapidly or if the log file is closed + // before the trigger event is written! + log_w("sdCardSemaphore failed to yield, held by %s, RTK_Surveyor.ino line %d", semaphoreHolder, + __LINE__); + } } - else + + // Record the Antenna Reference Position - if available + if (newARPAvailable == true && settings.enableARPLogging && + ((millis() - lastARPLog) > (settings.ARPLoggingInterval_s * 1000))) { - Serial.printf(" reached max log time %d", settings.maxLogTime_minutes); + systemPrintln("Recording Antenna Reference Position"); + + lastARPLog = millis(); + newARPAvailable = false; + + double x = ARPECEFX; + x /= 10000.0; // Convert to m + double y = ARPECEFY; + y /= 10000.0; // Convert to m + double z = ARPECEFZ; + z /= 10000.0; // Convert to m + double h = ARPECEFH; + h /= 10000.0; // Convert to m + char ARPData[82]; // Max NMEA sentence length is 82 + snprintf(ARPData, sizeof(ARPData), "%.4f,%.4f,%.4f,%.4f", x, y, z, h); + + char nmeaMessage[82]; // Max NMEA sentence length is 82 + createNMEASentence(CUSTOM_NMEA_TYPE_ARP_ECEF_XYZH, nmeaMessage, sizeof(nmeaMessage), + ARPData); // textID, buffer, sizeOfBuffer, text + + if (xSemaphoreTake(sdCardSemaphore, fatSemaphore_shortWait_ms) == pdPASS) + { + markSemaphore(FUNCTION_EVENT); + + ubxFile->println(nmeaMessage); + + xSemaphoreGive(sdCardSemaphore); + newEventToRecord = false; + } + else + { + char semaphoreHolder[50]; + getSemaphoreFunction(semaphoreHolder); + log_w("sdCardSemaphore failed to yield, held by %s, RTK_Surveyor.ino line %d", semaphoreHolder, + __LINE__); + } } - Serial.println(); - - totalWriteTime = 0; //Reset write time every 5s - - if (fileSize > lastLogSize) + // Report file sizes to show recording is working + if ((millis() - lastFileReport) > 5000) { - lastLogSize = fileSize; - logIncreasing = true; + if (fileSize > 0) + { + lastFileReport = millis(); + if (settings.enablePrintLogFileStatus) + { + systemPrintf("Log file size: %ld", fileSize); + + if ((systemTime_minutes - startLogTime_minutes) < settings.maxLogTime_minutes) + { + // Calculate generation and write speeds every 5 seconds + uint32_t fileSizeDelta = fileSize - lastLogSize; + systemPrintf(" - Generation rate: %0.1fkB/s", fileSizeDelta / 5.0 / 1000.0); + } + else + { + systemPrintf(" reached max log time %d", settings.maxLogTime_minutes); + } + + systemPrintln(); + } + + if (fileSize > lastLogSize) + { + lastLogSize = fileSize; + logIncreasing = true; + } + else + { + log_d("No increase in file size"); + logIncreasing = false; + + endSD(false, true); // alreadyHaveSemaphore, releaseSemaphore + } + } } - else - logIncreasing = false; - } } - } } -//Once we have a fix, sync system clock to GNSS -//All SD writes will use the system date/time +// Once we have a fix, sync system clock to GNSS +// All SD writes will use the system date/time void updateRTC() { - if (online.rtc == false) - { - if (online.gnss == true) + if (online.rtc == false) // Only do this if the rtc has not been sync'd previously + { + if (online.gnss == true) // Only do this if the GNSS is online + { + if (millis() - lastRTCAttempt > syncRTCInterval) // Only attempt this once per second + { + lastRTCAttempt = millis(); + + // theGNSS.checkUblox and theGNSS.checkCallbacks are called in the loop but updateRTC + // can also be called duing begin. To be safe, check for fresh PVT data here. + theGNSS.checkUblox(); // Poll to get latest data + theGNSS.checkCallbacks(); // Process any callbacks: ie, storePVTdata + + bool timeValid = false; + if (validTime == true && + validDate == true) // Will pass if ZED's RTC is reporting (regardless of GNSS fix) + timeValid = true; + if (confirmedTime == true && confirmedDate == true) // Requires GNSS fix + timeValid = true; + if (timeValid && + (millis() - pvtArrivalMillis > 999)) // If the GNSS time is over a second old, don't use it + timeValid = false; + + if (timeValid == true) + { + // To perform the time zone adjustment correctly, it's easiest if we convert the GNSS time and date + // into Unix epoch first and then apply the timeZone offset + uint32_t epochSecs; + uint32_t epochMicros; + convertGnssTimeToEpoch(&epochSecs, &epochMicros); + epochSecs += settings.timeZoneSeconds; + epochSecs += settings.timeZoneMinutes * 60; + epochSecs += settings.timeZoneHours * 60 * 60; + + // Set the internal system time + rtc.setTime(epochSecs, epochMicros); + + online.rtc = true; + lastRTCSync = millis(); + + systemPrint("System time set to: "); + systemPrintln(rtc.getDateTime(true)); + + recordSystemSettingsToFileSD( + settingsFileName); // This will re-record the setting file with current date/time. + } + else + { + systemPrintln("No GNSS date/time available for system RTC."); + } // End timeValid + } // End lastRTCAttempt + } // End online.gnss + } // End online.rtc + + // Print TP time sync information here. Trying to do it in the ISR would be a bad idea.... + if (settings.enablePrintRtcSync == true) + { + if ((previousGnssSyncTv.tv_sec != gnssSyncTv.tv_sec) || (previousGnssSyncTv.tv_usec != gnssSyncTv.tv_usec)) + { + time_t nowtime; + struct tm *nowtm; + char tmbuf[64], buf[64]; + + nowtime = gnssSyncTv.tv_sec; + nowtm = localtime(&nowtime); + strftime(tmbuf, sizeof tmbuf, "%Y-%m-%d %H:%M:%S", nowtm); + systemPrintf("RTC resync took place at: %s.%03d\r\n", tmbuf, gnssSyncTv.tv_usec / 1000); + + previousGnssSyncTv.tv_sec = gnssSyncTv.tv_sec; + previousGnssSyncTv.tv_usec = gnssSyncTv.tv_usec; + } + } +} + +// Called from main loop +// Control incoming/outgoing RTCM data from: +// External radio - this is normally a serial telemetry radio hung off the RADIO port +// Internal ESP NOW radio - Use the ESP32 to directly transmit/receive RTCM over 2.4GHz (no WiFi needed) +void updateRadio() +{ + // If we have not gotten new RTCM bytes for a period of time, assume end of frame + if (millis() - rtcmLastReceived > 50 && rtcmBytesSent > 0) { - if (i2cGNSS.getConfirmedDate() == true && i2cGNSS.getConfirmedTime() == true) - { - //Set the internal system time - //This is normally set with WiFi NTP but we will rarely have WiFi - rtc.setTime(i2cGNSS.getSecond(), i2cGNSS.getMinute(), i2cGNSS.getHour(), i2cGNSS.getDay(), i2cGNSS.getMonth(), i2cGNSS.getYear()); // 17th Jan 2021 15:24:30 + rtcmBytesSent = 0; + rtcmPacketsSent++; // If not checking RTCM CRC, count based on timeout + } - online.rtc = true; +#ifdef COMPILE_ESPNOW + if (settings.radioType == RADIO_ESPNOW) + { + if (espnowState == ESPNOW_PAIRED) + { + // If it's been longer than a few ms since we last added a byte to the buffer + // then we've reached the end of the RTCM stream. Send partial buffer. + if (espnowOutgoingSpot > 0 && (millis() - espnowLastAdd) > 50) + { + if (settings.espnowBroadcast == false) + esp_now_send(0, (uint8_t *)&espnowOutgoing, espnowOutgoingSpot); // Send partial packet to all peers + else + { + uint8_t broadcastMac[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + esp_now_send(broadcastMac, (uint8_t *)&espnowOutgoing, + espnowOutgoingSpot); // Send packet via broadcast + } + + if (!inMainMenu) + log_d("ESPNOW transmitted %d RTCM bytes", espnowBytesSent + espnowOutgoingSpot); + espnowBytesSent = 0; + espnowOutgoingSpot = 0; // Reset + } + + // If we don't receive an ESP NOW packet after some time, set RSSI to very negative + // This removes the ESPNOW icon from the display when the link goes down + if (millis() - lastEspnowRssiUpdate > 5000 && espnowRSSI > -255) + espnowRSSI = -255; + } + } +#endif // COMPILE_ESPNOW +} + +// Record who is holding the semaphore +volatile SemaphoreFunction semaphoreFunction = FUNCTION_NOT_SET; - Serial.print(F("System time set to: ")); - Serial.println(rtc.getTime("%B %d %Y %H:%M:%S")); //From ESP32Time library example +void markSemaphore(SemaphoreFunction functionNumber) +{ + semaphoreFunction = functionNumber; +} - recordSystemSettingsToFile(); //This will re-record the setting file with current date/time. - } +// Resolves the holder to a printable string +void getSemaphoreFunction(char *functionName) +{ + switch (semaphoreFunction) + { + default: + strcpy(functionName, "Unknown"); + break; + + case FUNCTION_SYNC: + strcpy(functionName, "Sync"); + break; + case FUNCTION_WRITESD: + strcpy(functionName, "Write"); + break; + case FUNCTION_FILESIZE: + strcpy(functionName, "FileSize"); + break; + case FUNCTION_EVENT: + strcpy(functionName, "Event"); + break; + case FUNCTION_BEGINSD: + strcpy(functionName, "BeginSD"); + break; + case FUNCTION_RECORDSETTINGS: + strcpy(functionName, "Record Settings"); + break; + case FUNCTION_LOADSETTINGS: + strcpy(functionName, "Load Settings"); + break; + case FUNCTION_MARKEVENT: + strcpy(functionName, "Mark Event"); + break; + case FUNCTION_GETLINE: + strcpy(functionName, "Get line"); + break; + case FUNCTION_REMOVEFILE: + strcpy(functionName, "Remove file"); + break; + case FUNCTION_RECORDLINE: + strcpy(functionName, "Record Line"); + break; + case FUNCTION_CREATEFILE: + strcpy(functionName, "Create File"); + break; + case FUNCTION_ENDLOGGING: + strcpy(functionName, "End Logging"); + break; + case FUNCTION_FINDLOG: + strcpy(functionName, "Find Log"); + break; + case FUNCTION_LOGTEST: + strcpy(functionName, "Log Test"); + break; + case FUNCTION_FILELIST: + strcpy(functionName, "File List"); + break; + case FUNCTION_FILEMANAGER_OPEN1: + strcpy(functionName, "FileManager Open1"); + break; + case FUNCTION_FILEMANAGER_OPEN2: + strcpy(functionName, "FileManager Open2"); + break; + case FUNCTION_FILEMANAGER_OPEN3: + strcpy(functionName, "FileManager Open3"); + break; + case FUNCTION_FILEMANAGER_UPLOAD1: + strcpy(functionName, "FileManager Upload1"); + break; + case FUNCTION_FILEMANAGER_UPLOAD2: + strcpy(functionName, "FileManager Upload2"); + break; + case FUNCTION_FILEMANAGER_UPLOAD3: + strcpy(functionName, "FileManager Upload3"); + break; + case FUNCTION_SDSIZECHECK: + strcpy(functionName, "SD Size Check"); + break; + case FUNCTION_LOG_CLOSURE: + strcpy(functionName, "Log Closure"); + break; + case FUNCTION_NTPEVENT: + strcpy(functionName, "NTP Event"); + break; } - } } diff --git a/Firmware/RTK_Surveyor/Rover.ino b/Firmware/RTK_Surveyor/Rover.ino index 1599daea0..29b6053f9 100644 --- a/Firmware/RTK_Surveyor/Rover.ino +++ b/Firmware/RTK_Surveyor/Rover.ino @@ -1,271 +1,321 @@ - -//Configure specific aspects of the receiver for rover mode +// Configure specific aspects of the receiver for rover mode bool configureUbloxModuleRover() { - bool response = true; - int maxWait = 2000; - - response = i2cGNSS.disableSurveyMode(maxWait); //Disable survey - if (response == false) - Serial.println(F("Disable Survey failed")); - - // Set dynamic model - if (i2cGNSS.getDynamicModel(maxWait) != settings.dynamicModel) - { - response = i2cGNSS.setDynamicModel((dynModel)settings.dynamicModel, maxWait); - if (response == false) - Serial.println(F("setDynamicModel failed")); - } - - //Disable RTCM sentences on I2C, USB, and UART2 - response = true; //Reset - response &= disableRTCMSentences(COM_PORT_I2C); - response &= disableRTCMSentences(COM_PORT_UART2); - response &= disableRTCMSentences(COM_PORT_USB); - - //Re-enable any RTCM msgs on UART1 the user has set within settings - response &= configureGNSSMessageRates(COM_PORT_UART1, ubxMessages); //Make sure the appropriate messages are enabled - - if (response == false) - Serial.println(F("Disable RTCM failed")); - - response = setNMEASettings(); //Enable high precision NMEA and extended sentences - if (response == false) - Serial.println(F("setNMEASettings failed")); - - response = true; //Reset - - //The last thing we do is set output rate. - response = true; //Reset - if (i2cGNSS.getMeasurementRate() != settings.measurementRate) - { - response &= i2cGNSS.setMeasurementRate(settings.measurementRate); - } - if (i2cGNSS.getNavigationRate() != settings.navigationRate) - { - response &= i2cGNSS.setNavigationRate(settings.navigationRate); - } - if (response == false) - Serial.println(F("Set Nav Rate failed")); - - return (response); -} + if (online.gnss == false) + { + log_d("GNSS not online"); + return (false); + } -//The u-blox library doesn't directly support NMEA configuration so let's do it manually -bool setNMEASettings() -{ - uint8_t customPayload[MAX_PAYLOAD_SIZE]; // This array holds the payload data bytes - ubxPacket customCfg = {0, 0, 0, 0, 0, customPayload, 0, 0, SFE_UBLOX_PACKET_VALIDITY_NOT_DEFINED, SFE_UBLOX_PACKET_VALIDITY_NOT_DEFINED}; - - customCfg.cls = UBX_CLASS_CFG; // This is the message Class - customCfg.id = UBX_CFG_NMEA; // This is the message ID - customCfg.len = 0; // Setting the len (length) to zero let's us poll the current settings - customCfg.startingSpot = 0; // Always set the startingSpot to zero (unless you really know what you are doing) - - uint16_t maxWait = 1250; // Wait for up to 250ms (Serial may need a lot longer e.g. 1100) - - // Read the current setting. The results will be loaded into customCfg. - if (i2cGNSS.sendCommand(&customCfg, maxWait) != SFE_UBLOX_STATUS_DATA_RECEIVED) // We are expecting data and an ACK - { - Serial.println(F("NMEA setting failed")); - return (false); - } - - customPayload[3] |= (1 << 3); //Set the highPrec flag - - customPayload[8] = 1; //Enable extended satellite numbering - - // Now we write the custom packet back again to change the setting - if (i2cGNSS.sendCommand(&customCfg, maxWait) != SFE_UBLOX_STATUS_DATA_SENT) // This time we are only expecting an ACK - { - Serial.println(F("NMEA setting failed")); - return (false); - } - return (true); -} + // If our settings haven't changed, and this is first config since power on, trust ZED's settings + if (settings.updateZEDSettings == false && firstPowerOn == true) + { + firstPowerOn = false; // Next time user switches modes, new settings will be applied + log_d("Skipping ZED Rover configuration"); + return (true); + } -//Returns true if constellation is enabled -bool getConstellation(uint8_t constellation) -{ - uint8_t customPayload[MAX_PAYLOAD_SIZE]; // This array holds the payload data bytes - ubxPacket customCfg = {0, 0, 0, 0, 0, customPayload, 0, 0, SFE_UBLOX_PACKET_VALIDITY_NOT_DEFINED, SFE_UBLOX_PACKET_VALIDITY_NOT_DEFINED}; + firstPowerOn = false; // If we switch between rover/base in the future, force config of module. - customCfg.cls = UBX_CLASS_CFG; // This is the message Class - customCfg.id = UBX_CFG_GNSS; // This is the message ID - customCfg.len = 0; // Setting the len (length) to zero lets us poll the current settings - customCfg.startingSpot = 0; // Always set the startingSpot to zero (unless you really know what you are doing) + theGNSS.checkUblox(); // Regularly poll to get latest data and any RTCM + theGNSS.checkCallbacks(); // Process any callbacks: ie, storePVTdata - uint16_t maxWait = 1250; // Wait for up to 250ms (Serial may need a lot longer e.g. 1100) + bool success = false; + int tryNo = -1; - // Read the current setting. The results will be loaded into customCfg. - if (i2cGNSS.sendCommand(&customCfg, maxWait) != SFE_UBLOX_STATUS_DATA_RECEIVED) // We are expecting data and an ACK - { - Serial.println(F("Get Constellation failed")); - return (false); - } + // Try up to MAX_SET_MESSAGES_RETRIES times to configure the GNSS + // This corrects occasional failures seen on the Reference Station where the GNSS is connected via SPI + // instead of I2C and UART1. I believe the SETVAL ACK is occasionally missed due to the level of messages being + // processed. + while ((++tryNo < MAX_SET_MESSAGES_RETRIES) && !success) + { + bool response = true; - if (customPayload[8 + 8 * constellation] & (1 << 0)) return true; //Check if bit 0 is set - return false; -} + // Set output rate + response &= theGNSS.newCfgValset(); + response &= theGNSS.addCfgValset(UBLOX_CFG_RATE_MEAS, settings.measurementRate); + response &= theGNSS.addCfgValset(UBLOX_CFG_RATE_NAV, settings.navigationRate); -//The u-blox library doesn't directly support constellation control so let's do it manually -//Also allows the enable/disable of any constellation (BeiDou, Galileo, etc) -bool setConstellation(uint8_t constellation, bool enable) -{ - uint8_t customPayload[MAX_PAYLOAD_SIZE]; // This array holds the payload data bytes - ubxPacket customCfg = {0, 0, 0, 0, 0, customPayload, 0, 0, SFE_UBLOX_PACKET_VALIDITY_NOT_DEFINED, SFE_UBLOX_PACKET_VALIDITY_NOT_DEFINED}; - - customCfg.cls = UBX_CLASS_CFG; // This is the message Class - customCfg.id = UBX_CFG_GNSS; // This is the message ID - customCfg.len = 0; // Setting the len (length) to zero lets us poll the current settings - customCfg.startingSpot = 0; // Always set the startingSpot to zero (unless you really know what you are doing) - - uint16_t maxWait = 1250; // Wait for up to 250ms (Serial may need a lot longer e.g. 1100) - - // Read the current setting. The results will be loaded into customCfg. - if (i2cGNSS.sendCommand(&customCfg, maxWait) != SFE_UBLOX_STATUS_DATA_RECEIVED) // We are expecting data and an ACK - { - Serial.println(F("Set Constellation failed")); - return (false); - } - - if (enable) - { - if (constellation == SFE_UBLOX_GNSS_ID_GPS || constellation == SFE_UBLOX_GNSS_ID_QZSS) - { - //QZSS must follow GPS - customPayload[locateGNSSID(customPayload, SFE_UBLOX_GNSS_ID_GPS) + 4] |= (1 << 0); //Set the enable bit - customPayload[locateGNSSID(customPayload, SFE_UBLOX_GNSS_ID_QZSS) + 4] |= (1 << 0); //Set the enable bit - } - else - { - customPayload[locateGNSSID(customPayload, constellation) + 4] |= (1 << 0); //Set the enable bit - } + // Survey mode is only available on ZED-F9P modules + if (commandSupported(UBLOX_CFG_TMODE_MODE) == true) + response &= theGNSS.addCfgValset(UBLOX_CFG_TMODE_MODE, 0); // Disable survey-in mode - //Set sigCfgMask as well - if (constellation == SFE_UBLOX_GNSS_ID_GPS || constellation == SFE_UBLOX_GNSS_ID_QZSS) - { - customPayload[locateGNSSID(customPayload, SFE_UBLOX_GNSS_ID_GPS) + 6] |= 0x11; //Enable GPS L1C/A, and L2C + response &= + theGNSS.addCfgValset(UBLOX_CFG_NAVSPG_DYNMODEL, (dynModel)settings.dynamicModel); // Set dynamic model - //QZSS must follow GPS - customPayload[locateGNSSID(customPayload, SFE_UBLOX_GNSS_ID_QZSS) + 6] = 0x11; //Enable QZSS L1C/A, and L2C - Follow u-center - //customPayload[locateGNSSID(customPayload, SFE_UBLOX_GNSS_ID_QZSS) + 6] = 0x15; //Enable QZSS L1C/A, L1S, and L2C - } - else if (constellation == SFE_UBLOX_GNSS_ID_SBAS) - { - customPayload[locateGNSSID(customPayload, constellation) + 6] |= 0x01; //Enable SBAS L1C/A - } - else if (constellation == SFE_UBLOX_GNSS_ID_GALILEO) - { - customPayload[locateGNSSID(customPayload, constellation) + 6] |= 0x21; //Enable Galileo E1/E5b - } - else if (constellation == SFE_UBLOX_GNSS_ID_BEIDOU) - { - customPayload[locateGNSSID(customPayload, constellation) + 6] |= 0x11; //Enable BeiDou B1I/B2I - } - else if (constellation == SFE_UBLOX_GNSS_ID_GLONASS) - { - customPayload[locateGNSSID(customPayload, constellation) + 6] |= 0x11; //Enable GLONASS L1 and L2 - } - } - else //Disable - { - //QZSS must follow GPS - if (constellation == SFE_UBLOX_GNSS_ID_GPS || constellation == SFE_UBLOX_GNSS_ID_QZSS) - { - customPayload[locateGNSSID(customPayload, SFE_UBLOX_GNSS_ID_GPS) + 4] &= ~(1 << 0); //Clear the enable bit + // RTCM is only available on ZED-F9P modules + // + // For most RTK products, the GNSS is interfaced via both I2C and UART1. Configuration and PVT/HPPOS messages + // are configured over I2C. Any messages that need to be logged are output on UART1, and received by this code + // using serialGNSS. So in Rover mode, we want to disable any RTCM messages on I2C (and USB and UART2). + // + // But, on the Reference Station, the GNSS is interfaced via SPI. It has no access to I2C and UART1. So for that + // product - in Rover mode - we want to leave any RTCM messages enabled on SPI so they can be logged if desired. + + // Find first RTCM record in ubxMessage array + int firstRTCMRecord = getMessageNumberByName("UBX_RTCM_1005"); + + if (zedModuleType == PLATFORM_F9P) + { + if (USE_I2C_GNSS) + { + // Set RTCM messages to user's settings + for (int x = 0; x < MAX_UBX_MSG_RTCM; x++) + response &= theGNSS.addCfgValset( + ubxMessages[firstRTCMRecord + x].msgConfigKey - 1, + settings.ubxMessageRates[firstRTCMRecord + x]); // UBLOX_CFG UART1 - 1 = I2C + } + else + { + for (int x = 0; x < MAX_UBX_MSG_RTCM; x++) + response &= theGNSS.addCfgValset( + ubxMessages[firstRTCMRecord + x].msgConfigKey + 3, + settings.ubxMessageRates[firstRTCMRecord + x]); // UBLOX_CFG UART1 + 3 = SPI + } + + // Set RTCM messages to user's settings + for (int x = 0; x < MAX_UBX_MSG_RTCM; x++) + { + response &= + theGNSS.addCfgValset(ubxMessages[firstRTCMRecord + x].msgConfigKey + 1, + settings.ubxMessageRates[firstRTCMRecord + x]); // UBLOX_CFG UART1 + 1 = UART2 + response &= + theGNSS.addCfgValset(ubxMessages[firstRTCMRecord + x].msgConfigKey + 2, + settings.ubxMessageRates[firstRTCMRecord + x]); // UBLOX_CFG UART1 + 2 = USB + } + } + + response &= theGNSS.addCfgValset(UBLOX_CFG_NMEA_MAINTALKERID, + 3); // Return talker ID to GNGGA after NTRIP Client set to GPGGA + + response &= theGNSS.addCfgValset(UBLOX_CFG_NMEA_HIGHPREC, 1); // Enable high precision NMEA + response &= theGNSS.addCfgValset(UBLOX_CFG_NMEA_SVNUMBERING, 1); // Enable extended satellite numbering + + response &= theGNSS.addCfgValset(UBLOX_CFG_NAVSPG_INFIL_MINELEV, settings.minElev); // Set minimum elevation + + response &= theGNSS.sendCfgValset(); // Closing - customPayload[locateGNSSID(customPayload, SFE_UBLOX_GNSS_ID_QZSS) + 4] &= ~(1 << 0); //Clear the enable bit + if (response) + success = true; } - else + + if (!success) + log_d("Rover config failed 1"); + + if (zedModuleType == PLATFORM_F9R) { - customPayload[locateGNSSID(customPayload, constellation) + 4] &= ~(1 << 0); //Clear the enable bit + bool response = true; + + response &= theGNSS.newCfgValset(); + + response &= + theGNSS.addCfgValset(UBLOX_CFG_SFCORE_USE_SF, settings.enableSensorFusion); // Enable/disable sensor fusion + response &= + theGNSS.addCfgValset(UBLOX_CFG_SFIMU_AUTO_MNTALG_ENA, + settings.autoIMUmountAlignment); // Enable/disable Automatic IMU-mount Alignment + + if (zedFirmwareVersionInt >= 121) + { + response &= theGNSS.addCfgValset(UBLOX_CFG_SFIMU_IMU_MNTALG_YAW, settings.imuYaw); + response &= theGNSS.addCfgValset(UBLOX_CFG_SFIMU_IMU_MNTALG_PITCH, settings.imuPitch); + response &= theGNSS.addCfgValset(UBLOX_CFG_SFIMU_IMU_MNTALG_ROLL, settings.imuRoll); + response &= theGNSS.addCfgValset(UBLOX_CFG_SFODO_DIS_AUTODIRPINPOL, settings.sfDisableWheelDirection); + response &= theGNSS.addCfgValset(UBLOX_CFG_SFODO_COMBINE_TICKS, settings.sfCombineWheelTicks); + response &= theGNSS.addCfgValset(UBLOX_CFG_RATE_NAV_PRIO, settings.rateNavPrio); + response &= theGNSS.addCfgValset(UBLOX_CFG_SFODO_USE_SPEED, settings.sfUseSpeed); + } + + response &= theGNSS.sendCfgValset(); // Closing - 28 keys + + if (response == false) + { + log_d("Rover config failed 2"); + success = false; + } } - } + if (!success) + systemPrintln("Rover config fail"); - // Now we write the custom packet back again to change the setting - if (i2cGNSS.sendCommand(&customCfg, maxWait) != SFE_UBLOX_STATUS_DATA_SENT) // This time we are only expecting an ACK - { - Serial.println(F("Constellation setting failed")); - return (false); - } + return (success); +} - return (true); +// Turn on the three accuracy LEDs depending on our current HPA (horizontal positional accuracy) +void updateAccuracyLEDs() +{ + // Update the horizontal accuracy LEDs only every second or so + if (millis() - lastAccuracyLEDUpdate > 2000) + { + lastAccuracyLEDUpdate = millis(); + + if (online.gnss == true) + { + if (horizontalAccuracy > 0) + { + if (settings.enablePrintRoverAccuracy) + { + systemPrint("Rover Accuracy (m): "); + systemPrint(horizontalAccuracy, 4); // Print the accuracy with 4 decimal places + systemPrint(", SIV: "); + systemPrint(numSV); + systemPrintln(); + } + + if (productVariant == RTK_SURVEYOR) + { + if (horizontalAccuracy <= 0.02) + { + digitalWrite(pin_positionAccuracyLED_1cm, HIGH); + digitalWrite(pin_positionAccuracyLED_10cm, HIGH); + digitalWrite(pin_positionAccuracyLED_100cm, HIGH); + } + else if (horizontalAccuracy <= 0.100) + { + digitalWrite(pin_positionAccuracyLED_1cm, LOW); + digitalWrite(pin_positionAccuracyLED_10cm, HIGH); + digitalWrite(pin_positionAccuracyLED_100cm, HIGH); + } + else if (horizontalAccuracy <= 1.0000) + { + digitalWrite(pin_positionAccuracyLED_1cm, LOW); + digitalWrite(pin_positionAccuracyLED_10cm, LOW); + digitalWrite(pin_positionAccuracyLED_100cm, HIGH); + } + else if (horizontalAccuracy > 1.0) + { + digitalWrite(pin_positionAccuracyLED_1cm, LOW); + digitalWrite(pin_positionAccuracyLED_10cm, LOW); + digitalWrite(pin_positionAccuracyLED_100cm, LOW); + } + } + } + else if (settings.enablePrintRoverAccuracy) + { + systemPrint("Rover Accuracy: "); + systemPrint(horizontalAccuracy); + systemPrint(" "); + systemPrint("No lock. SIV: "); + systemPrint(numSV); + systemPrintln(); + } + } // End GNSS online checking + } // Check every 2000ms } -//Given a payload, return the location of a given constellation -//This is needed because IMES is not currently returned in the query packet -//so QZSS and GLONAS are offset by -8 bytes. -uint8_t locateGNSSID(uint8_t *customPayload, uint8_t constellation) +// These are the callbacks that get regularly called, globals are updated +void storePVTdata(UBX_NAV_PVT_data_t *ubxDataStruct) { - for (int x = 0 ; x < 7 ; x++) //Assume max of 7 constellations - { - if (customPayload[4 + 8 * x] == constellation) //Test gnssid - return (4 + x * 8); - } - - Serial.print(F("locateGNSSID failed: ")); - Serial.println(constellation); - return (0); + altitude = ubxDataStruct->height / 1000.0; + + gnssDay = ubxDataStruct->day; + gnssMonth = ubxDataStruct->month; + gnssYear = ubxDataStruct->year; + + gnssHour = ubxDataStruct->hour; + gnssMinute = ubxDataStruct->min; + gnssSecond = ubxDataStruct->sec; + gnssNano = ubxDataStruct->nano; + mseconds = ceil((ubxDataStruct->iTOW % 1000) / 10.0); // Limit to first two digits + + numSV = ubxDataStruct->numSV; + fixType = ubxDataStruct->fixType; + carrSoln = ubxDataStruct->flags.bits.carrSoln; + + validDate = ubxDataStruct->valid.bits.validDate; + validTime = ubxDataStruct->valid.bits.validTime; + fullyResolved = ubxDataStruct->valid.bits.fullyResolved; + tAcc = ubxDataStruct->tAcc; + confirmedDate = ubxDataStruct->flags2.bits.confirmedDate; + confirmedTime = ubxDataStruct->flags2.bits.confirmedTime; + + pvtArrivalMillis = millis(); + pvtUpdated = true; } -//Turn on the three accuracy LEDs depending on our current HPA (horizontal positional accuracy) -void updateAccuracyLEDs() +void storeHPdata(UBX_NAV_HPPOSLLH_data_t *ubxDataStruct) { - //Update the horizontal accuracy LEDs only every second or so - if (millis() - lastAccuracyLEDUpdate > 2000) - { - lastAccuracyLEDUpdate = millis(); + horizontalAccuracy = ((float)ubxDataStruct->hAcc) / 10000.0; // Convert hAcc from mm*0.1 to m - uint32_t accuracy = i2cGNSS.getHorizontalAccuracy(250); + latitude = ((double)ubxDataStruct->lat) / 10000000.0; + latitude += ((double)ubxDataStruct->latHp) / 1000000000.0; + longitude = ((double)ubxDataStruct->lon) / 10000000.0; + longitude += ((double)ubxDataStruct->lonHp) / 1000000000.0; +} - if (accuracy > 0) - { - // Convert the horizontal accuracy (mm * 10^-1) to a float - float f_accuracy = accuracy; - f_accuracy = f_accuracy / 10000.0; // Convert from mm * 10^-1 to m +void storeTIMTPdata(UBX_TIM_TP_data_t *ubxDataStruct) +{ + uint32_t tow = ubxDataStruct->week - SFE_UBLOX_JAN_1ST_2020_WEEK; // Calculate the number of weeks since Jan 1st + // 2020 + tow *= SFE_UBLOX_SECS_PER_WEEK; // Convert weeks to seconds + tow += SFE_UBLOX_EPOCH_WEEK_2086; // Add the TOW for Jan 1st 2020 + tow += ubxDataStruct->towMS / 1000; // Add the TOW for the next TP - Serial.print(F("Rover Accuracy (m): ")); - Serial.print(f_accuracy, 4); // Print the accuracy with 4 decimal places - Serial.println(); + uint32_t us = ubxDataStruct->towMS % 1000; // Extract the milliseconds + us *= 1000; // Convert to microseconds - if (productVariant == RTK_SURVEYOR) - { - if (f_accuracy <= 0.02) - { - digitalWrite(pin_positionAccuracyLED_1cm, HIGH); - digitalWrite(pin_positionAccuracyLED_10cm, HIGH); - digitalWrite(pin_positionAccuracyLED_100cm, HIGH); - } - else if (f_accuracy <= 0.100) - { - digitalWrite(pin_positionAccuracyLED_1cm, LOW); - digitalWrite(pin_positionAccuracyLED_10cm, HIGH); - digitalWrite(pin_positionAccuracyLED_100cm, HIGH); - } - else if (f_accuracy <= 1.0000) - { - digitalWrite(pin_positionAccuracyLED_1cm, LOW); - digitalWrite(pin_positionAccuracyLED_10cm, LOW); - digitalWrite(pin_positionAccuracyLED_100cm, HIGH); - } - else if (f_accuracy > 1.0) - { - digitalWrite(pin_positionAccuracyLED_1cm, LOW); - digitalWrite(pin_positionAccuracyLED_10cm, LOW); - digitalWrite(pin_positionAccuracyLED_100cm, LOW); - } - } + double subMS = ubxDataStruct->towSubMS; // Get towSubMS (ms * 2^-32) + subMS *= pow(2.0, -32.0); // Convert to milliseconds + subMS *= 1000; // Convert to microseconds + + us += (uint32_t)subMS; // Add subMS + + timTpEpoch = tow; + timTpMicros = us; + timTpArrivalMillis = millis(); + timTpUpdated = true; +} + +void storeMONHWdata(UBX_MON_HW_data_t *ubxDataStruct) +{ + aStatus = ubxDataStruct->aStatus; +} + +void storeRTCM1005data(RTCM_1005_data_t *rtcmData1005) +{ + ARPECEFX = rtcmData1005->AntennaReferencePointECEFX; + ARPECEFY = rtcmData1005->AntennaReferencePointECEFY; + ARPECEFZ = rtcmData1005->AntennaReferencePointECEFZ; + ARPECEFH = 0; + newARPAvailable = true; +} + +void storeRTCM1006data(RTCM_1006_data_t *rtcmData1006) +{ + ARPECEFX = rtcmData1006->AntennaReferencePointECEFX; + ARPECEFY = rtcmData1006->AntennaReferencePointECEFY; + ARPECEFZ = rtcmData1006->AntennaReferencePointECEFZ; + ARPECEFH = rtcmData1006->AntennaHeight; + newARPAvailable = true; +} + +// Helper method to convert GNSS time and date into Unix Epoch +void convertGnssTimeToEpoch(uint32_t *epochSecs, uint32_t *epochMicros) +{ + uint32_t t = SFE_UBLOX_DAYS_FROM_1970_TO_2020; // Jan 1st 2020 as days from Jan 1st 1970 + t += (uint32_t)SFE_UBLOX_DAYS_SINCE_2020[gnssYear - 2020]; // Add on the number of days since 2020 + t += (uint32_t) + SFE_UBLOX_DAYS_SINCE_MONTH[gnssYear % 4 == 0 ? 0 : 1][gnssMonth - 1]; // Add on the number of days since Jan 1st + t += (uint32_t)gnssDay - 1; // Add on the number of days since the 1st of the month + t *= 24; // Convert to hours + t += (uint32_t)gnssHour; // Add on the hour + t *= 60; // Convert to minutes + t += (uint32_t)gnssMinute; // Add on the minute + t *= 60; // Convert to seconds + t += (uint32_t)gnssSecond; // Add on the second + + int32_t us = gnssNano / 1000; // Convert nanos to micros + uint32_t micro; + // Adjust t if nano is negative + if (us < 0) + { + micro = (uint32_t)(us + 1000000); // Make nano +ve + t--; // Decrement t by 1 second } else { - Serial.print(F("Rover Accuracy: ")); - Serial.print(accuracy); - Serial.print(" "); - Serial.print(F("No lock. SIV: ")); - Serial.print(i2cGNSS.getSIV()); - Serial.println(); + micro = us; } - } + + *epochSecs = t; + *epochMicros = micro; } diff --git a/Firmware/RTK_Surveyor/SD.ino b/Firmware/RTK_Surveyor/SD.ino new file mode 100644 index 000000000..efb7bda48 --- /dev/null +++ b/Firmware/RTK_Surveyor/SD.ino @@ -0,0 +1,169 @@ +/* + These are low level functions to aid in detecting whether a card is present or not. + Because of ESP32 v2 core, SdFat can only operate using Shared SPI. This makes the sd->begin test take over 1s + which causes the RTK product to boot slowly. To circumvent this, we will ping the SD card directly to see if it + responds. Failures take 2ms, successes take 1ms. + + From Prototype puzzle: + https://github.com/sparkfunX/ThePrototype/blob/master/Firmware/TestSketches/sdLocker/sdLocker.ino License: Public + domain. This code is based on Karl Lunt's work: https://www.seanet.com/~karllunt/sdlocker2.html +*/ + +// Define commands for the SD card +#define SD_GO_IDLE (0x40 + 0) // CMD0 - go to idle state +#define SD_INIT (0x40 + 1) // CMD1 - start initialization +#define SD_SEND_IF_COND (0x40 + 8) // CMD8 - send interface (conditional), works for SDHC only +#define SD_SEND_STATUS (0x40 + 13) // CMD13 - send card status +#define SD_SET_BLK_LEN (0x40 + 16) // CMD16 - set length of block in bytes +#define SD_LOCK_UNLOCK (0x40 + 42) // CMD42 - lock/unlock card +#define CMD55 (0x40 + 55) // multi-byte preface command +#define SD_READ_OCR (0x40 + 58) // read OCR +#define SD_ADV_INIT (0xc0 + 41) // ACMD41, for SDHC cards - advanced start initialization + +// Define options for accessing the SD card's PWD (CMD42) +#define MASK_ERASE 0x08 // erase the entire card +#define MASK_LOCK_UNLOCK 0x04 // lock or unlock the card with password +#define MASK_CLR_PWD 0x02 // clear password +#define MASK_SET_PWD 0x01 // set password + +// Define bit masks for fields in the lock/unlock command (CMD42) data structure +#define SET_PWD_MASK (1 << 0) +#define CLR_PWD_MASK (1 << 1) +#define LOCK_UNLOCK_MASK (1 << 2) +#define ERASE_MASK (1 << 3) + +// Begin initialization by sending CMD0 and waiting until SD card +// responds with In Idle Mode (0x01). If the response is not 0x01 +// within a reasonable amount of time, there is no SD card on the bus. +// Returns false if not card is detected +// Returns true if a card responds +// This test takes approximately 13ms to complete +bool sdPresent(void) +{ + if (productVariant == REFERENCE_STATION) + { + if (pin_microSD_CardDetect > 0) + { + pinMode(pin_microSD_CardDetect, INPUT); // Internal pullups not supported on input only pins + if (digitalRead(pin_microSD_CardDetect) == LOW) + return (true); // Card low - SD in place + return (false); // Card detect high - No SD + } + } + else if (USE_SPI_MICROSD) + { + byte response = 0; + + SPI.begin(); + SPI.setClockDivider(SPI_CLOCK_DIV2); + SPI.setDataMode(SPI_MODE0); + SPI.setBitOrder(MSBFIRST); + pinMode(pin_microSD_CS, OUTPUT); + + // Sending clocks while card power stabilizes... + deselectCard(); // always make sure + for (byte i = 0; i < 30; i++) // send several clocks while card power stabilizes + xchg(0xff); + + // Sending CMD0 - GO IDLE... + for (byte i = 0; i < 0x10; i++) // Attempt to go idle + { + response = sdSendCommand(SD_GO_IDLE, 0); // send CMD0 - go to idle state + if (response == 1) + break; + } + if (response != 1) + return (false); // Card failed to respond to idle + } + + return (true); +} + +/* + sdSendCommand send raw command to SD card, return response + + This routine accepts a single SD command and a 4-byte argument. It sends + the command plus argument, adding the appropriate CRC. It then returns + the one-byte response from the SD card. + + For advanced commands (those with a command byte having bit 7 set), this + routine automatically sends the required preface command (CMD55) before + sending the requested command. + + Upon exit, this routine returns the response byte from the SD card. + Possible responses are: + 0xff No response from card; card might actually be missing + 0x01 SD card returned 0x01, which is OK for most commands + 0x?? other responses are command-specific +*/ +byte sdSendCommand(byte command, unsigned long arg) +{ + byte response; + + if (command & 0x80) // special case, ACMD(n) is sent as CMD55 and CMDn + { + command &= 0x7f; // strip high bit for later + response = sdSendCommand(CMD55, 0); // send first part (recursion) + if (response > 1) + return (response); + } + + deselectCard(); + xchg(0xFF); + selectCard(); // enable CS + xchg(0xFF); + + xchg(command | 0x40); // command always has bit 6 set! + xchg((byte)(arg >> 24)); // send data, starting with top byte + xchg((byte)(arg >> 16)); + xchg((byte)(arg >> 8)); + xchg((byte)(arg & 0xFF)); + + byte crc = 0x01; // good for most cases + if (command == SD_GO_IDLE) + crc = 0x95; // this will be good enough for most commands + if (command == SD_SEND_IF_COND) + crc = 0x87; // special case, have to use different CRC + xchg(crc); // send final byte + + for (int i = 0; i < 30; i++) // loop until timeout or response + { + response = xchg(0xFF); + if ((response & 0x80) == 0) + break; // high bit cleared means we got a response + } + + /* + We have issued the command but the SD card is still selected. We + only deselectCard the card if the command we just sent is NOT a command + that requires additional data exchange, such as reading or writing + a block. + */ + if ((command != SD_READ_OCR) && (command != SD_SEND_STATUS) && (command != SD_SEND_IF_COND) && + (command != SD_LOCK_UNLOCK)) + { + deselectCard(); // all done + xchg(0xFF); // close with eight more clocks + } + + return (response); // let the caller sort it out +} + +// Select (enable) the SD card +void selectCard(void) +{ + digitalWrite(pin_microSD_CS, LOW); +} + +// Deselect (disable) the SD card +void deselectCard(void) +{ + digitalWrite(pin_microSD_CS, HIGH); +} + +// Exchange a byte of data with the SD card via host's SPI bus +byte xchg(byte val) +{ + byte receivedVal = SPI.transfer(val); + return receivedVal; +} diff --git a/Firmware/RTK_Surveyor/States.ino b/Firmware/RTK_Surveyor/States.ino index b95e55c2b..30657156d 100644 --- a/Firmware/RTK_Surveyor/States.ino +++ b/Firmware/RTK_Surveyor/States.ino @@ -1,610 +1,1365 @@ /* This is the main state machine for the device. It's big but controls each step of the system. - See system state chart document for a visual representation of how states can change to/from. + See system state diagram for a visual representation of how states can change to/from. + Statemachine diagram: + https://lucid.app/lucidchart/53519501-9fa5-4352-aa40-673f88ca0c9b/edit?invitationId=inv_ebd4b988-513d-4169-93fd-c291851108f8 */ -//Given the current state, see if conditions have moved us to a new state -//A user pressing the setup button (change between rover/base) is handled by checkpin_setupButton() +static uint32_t lastStateTime = 0; + +// Given the current state, see if conditions have moved us to a new state +// A user pressing the setup button (change between rover/base) is handled by checkpin_setupButton() void updateSystemState() { - if (millis() - lastSystemStateUpdate > 500) - { - lastSystemStateUpdate = millis(); - - //Move between states as needed - switch (systemState) + if (millis() - lastSystemStateUpdate > 500 || forceSystemStateUpdate == true) { - case (STATE_ROVER_NOT_STARTED): + lastSystemStateUpdate = millis(); + forceSystemStateUpdate = false; + + // Check to see if any external sources need to change state + if (newSystemStateRequested == true) { - //Configure for rover mode - displayRoverStart(0); + newSystemStateRequested = false; + if (systemState != requestedSystemState) + { + changeState(requestedSystemState); + lastStateTime = millis(); + } + } - //If we are survey'd in, but switch is rover then disable survey - if (configureUbloxModuleRover() == false) - { - Serial.println(F("Rover config failed")); - displayRoverFail(1000); - return; - } + if (settings.enablePrintStates && ((millis() - lastStateTime) > 15000)) + { + changeState(systemState); + lastStateTime = millis(); + } - stopWiFi(); //Turn off WiFi and release all resources - startBluetooth(); //Turn on Bluetooth with 'Rover' name + // Move between states as needed + DMW_st(changeState, systemState); + switch (systemState) + { + /* + .-----------------------------------. + | STATE_ROVER_NOT_STARTED | + | Text: 'Rover' and 'Rover Started' | + '-----------------------------------' + | + | + | + V + .-----------------------------------. + | STATE_ROVER_NO_FIX | + | SIV Icon Blink | + | "HPA: >30m" | + | "SIV: 0" | + '-----------------------------------' + | + | GPS Lock + | 3D, 3D+DR + V + .-----------------------------------. + | STATE_ROVER_FIX | Carrier + | SIV Icon Solid | Solution = 2 + .-------->| "HPA: .513" |---------. + | | "SIV: 30" | | + | '-----------------------------------' | + | | | + | | Carrier Solution = 1 | + | V | + | .-----------------------------------. | + | | STATE_ROVER_RTK_FLOAT | | + | No RTK | Double Crosshair Blinking | | + +<--------| "*HPA: .080" | | + ^ | "SIV: 30" | | + | '-----------------------------------' | + | ^ | | + | | | Carrier | + | | | Solution = 2 | + | | V | + | Carrier | +<-------------------' + | Solution = 1 | | + | | V + | .-----------------------------------. + | | STATE_ROVER_RTK_FIX | + | No RTK | Double Crosshair Solid | + '---------| "*HPA: .014" | + | "SIV: 30" | + '-----------------------------------' + + */ + case (STATE_ROVER_NOT_STARTED): { + RTK_MODE(RTK_MODE_ROVER); + if (online.gnss == false) + { + firstRoverStart = false; // If GNSS is offline, we still need to allow button use + return; + } - if (productVariant == RTK_SURVEYOR) - digitalWrite(pin_baseStatusLED, LOW); + if (productVariant == RTK_SURVEYOR) + { + digitalWrite(pin_baseStatusLED, LOW); + digitalWrite(pin_positionAccuracyLED_1cm, LOW); + digitalWrite(pin_positionAccuracyLED_10cm, LOW); + digitalWrite(pin_positionAccuracyLED_100cm, LOW); + ledcWrite(ledBTChannel, 0); // Turn off BT LED + } - settings.lastState = STATE_ROVER_NOT_STARTED; - recordSystemSettings(); + if (productVariant == REFERENCE_STATION) + { + digitalWrite(pin_baseStatusLED, LOW); + } - displayRoverSuccess(500); + // Configure for rover mode + displayRoverStart(0); - changeState(STATE_ROVER_NO_FIX); + // If we are survey'd in, but switch is rover then disable survey + if (configureUbloxModuleRover() == false) + { + systemPrintln("Rover config failed"); + displayRoverFail(1000); + return; + } + + setMuxport(settings.dataPortChannel); // Return mux to original channel + + NETWORK_STOP(NETWORK_TYPE_WIFI); + WIFI_STOP(); // Stop WiFi, ntripClient will start as needed. + bluetoothStart(); // Turn on Bluetooth with 'Rover' name + radioStart(); // Start internal radio if enabled, otherwise disable + + if (!tasksStartUART2()) // Start monitoring the UART1 from ZED for NMEA and UBX data (enables logging) + displayRoverFail(1000); + else + { + settings.updateZEDSettings = false; // On the next boot, no need to update the ZED on this profile + settings.lastState = STATE_ROVER_NOT_STARTED; + recordSystemSettings(); // Record this state for next POR + + displayRoverSuccess(500); + + changeState(STATE_ROVER_NO_FIX); + + firstRoverStart = false; // Do not allow entry into test menu again + } } break; - case (STATE_ROVER_NO_FIX): - { - if (i2cGNSS.getFixType() == 3) //3D - changeState(STATE_ROVER_FIX); + case (STATE_ROVER_NO_FIX): { + if (fixType == 3 || fixType == 4) // 3D, 3D+DR + changeState(STATE_ROVER_FIX); } break; - case (STATE_ROVER_FIX): - { - updateAccuracyLEDs(); + case (STATE_ROVER_FIX): { + updateAccuracyLEDs(); - byte rtkType = i2cGNSS.getCarrierSolutionType(); - if (rtkType == 1) //RTK Float - changeState(STATE_ROVER_RTK_FLOAT); - else if (rtkType == 2) //RTK Fix - changeState(STATE_ROVER_RTK_FIX); + if (carrSoln == 1) // RTK Float + { + lbandTimeFloatStarted = + millis(); // Restart timer for L-Band. Don't immediately reset ZED to achieve fix. + changeState(STATE_ROVER_RTK_FLOAT); + } + else if (carrSoln == 2) // RTK Fix + changeState(STATE_ROVER_RTK_FIX); } break; - case (STATE_ROVER_RTK_FLOAT): - { - updateAccuracyLEDs(); + case (STATE_ROVER_RTK_FLOAT): { + updateAccuracyLEDs(); - byte rtkType = i2cGNSS.getCarrierSolutionType(); - if (rtkType == 0) //No RTK - changeState(STATE_ROVER_FIX); - if (rtkType == 2) //RTK Fix - changeState(STATE_ROVER_RTK_FIX); + if (carrSoln == 0) // No RTK + changeState(STATE_ROVER_FIX); + if (carrSoln == 2) // RTK Fix + changeState(STATE_ROVER_RTK_FIX); } break; - case (STATE_ROVER_RTK_FIX): - { - updateAccuracyLEDs(); + case (STATE_ROVER_RTK_FIX): { + updateAccuracyLEDs(); - byte rtkType = i2cGNSS.getCarrierSolutionType(); - if (rtkType == 0) //No RTK - changeState(STATE_ROVER_FIX); - if (rtkType == 1) //RTK Float - changeState(STATE_ROVER_RTK_FLOAT); + if (carrSoln == 0) // No RTK + changeState(STATE_ROVER_FIX); + if (carrSoln == 1) // RTK Float + { + lbandTimeFloatStarted = + millis(); // Restart timer for L-Band. Don't immediately reset ZED to achieve fix. + changeState(STATE_ROVER_RTK_FLOAT); + } } break; - case (STATE_BASE_NOT_STARTED): - { - //Turn off base LED until we successfully enter temp/fix state - if (productVariant == RTK_SURVEYOR) - { - digitalWrite(pin_baseStatusLED, LOW); - digitalWrite(pin_positionAccuracyLED_1cm, LOW); - digitalWrite(pin_positionAccuracyLED_10cm, LOW); - digitalWrite(pin_positionAccuracyLED_100cm, LOW); - } - - displayBaseStart(0); //Show 'Base' - - //Restart Bluetooth with 'Base' name - //We start BT regardless of Ntrip Server in case user wants to transmit survey-in stats over BT - stopWiFi(); - startBluetooth(); - - if (configureUbloxModuleBase() == true) - { - settings.lastState = STATE_BASE_NOT_STARTED; //Record this state for next POR - recordSystemSettings(); + /* + .-----------------------------------. + startBase() | STATE_BASE_NOT_STARTED | + .------------| Text: 'Base' | + | = false '-----------------------------------' + | | + | Stop WiFi, | startBase() = true + | Stop | Stop WiFi + | Bluetooth | Start Bluetooth + | V + | .-----------------------------------. + | | STATE_BASE_TEMP_SETTLE | + | | Temp Base Icon. Blinking HPA. | + | | "HPA: 7.15" | + | | "SIV: 5" | + | '-----------------------------------' + V | + STATE_BASE_FIXED_NOT_STARTED | horizontalAccuracy > 0.0 + (next diagram) | && horizontalAccuracy + | < settings.surveyInStartingAccuracy + | && surveyInStart() == true + V + .-----------------------------------. + | STATE_BASE_TEMP_SURVEY_STARTED | svinObservationTime > + | Temp Base Icon blinking | maxSurveyInWait_s + | "Mean: 0.089" |--------------. + | "Time: 36" | | + '-----------------------------------' | + | | + | getSurveyInValid() | + | = true V + | STATE_ROVER_NOT_STARTED + V (Previous diagram) + .-----------------------------------. + | STATE_BASE_TEMP_TRANSMITTING | + | Temp Base Icon solid | + | "Xmitting" | + | "RTCM: 2145" | + '-----------------------------------' + + */ + + case (STATE_BASE_NOT_STARTED): { + RTK_MODE(RTK_MODE_BASE_SURVEY_IN); + firstRoverStart = false; // If base is starting, no test menu, normal button use. + + if (online.gnss == false) + return; + + // Turn off base LED until we successfully enter temp/fix state + if (productVariant == RTK_SURVEYOR) + { + digitalWrite(pin_baseStatusLED, LOW); + digitalWrite(pin_positionAccuracyLED_1cm, LOW); + digitalWrite(pin_positionAccuracyLED_10cm, LOW); + digitalWrite(pin_positionAccuracyLED_100cm, LOW); + ledcWrite(ledBTChannel, 0); // Turn off BT LED + } - displayBaseSuccess(500); //Show 'Base Started' + if (productVariant == REFERENCE_STATION) + digitalWrite(pin_baseStatusLED, LOW); - if (settings.fixedBase == false) + displayBaseStart(0); // Show 'Base' + + // Allow WiFi to continue running if NTRIP Client is needed for assisted survey in + if (wifiIsNeeded() == false) { - changeState(STATE_BASE_TEMP_SETTLE); + NETWORK_STOP(NETWORK_TYPE_WIFI); + WIFI_STOP(); } - else if (settings.fixedBase == true) + + bluetoothStop(); + bluetoothStart(); // Restart Bluetooth with 'Base' identifier + + // Start monitoring the UART1 from ZED for NMEA and UBX data (enables logging) + if (tasksStartUART2() && configureUbloxModuleBase()) { - changeState(STATE_BASE_FIXED_NOT_STARTED); + settings.updateZEDSettings = false; // On the next boot, no need to update the ZED on this profile + settings.lastState = STATE_BASE_NOT_STARTED; // Record this state for next POR + recordSystemSettings(); // Record this state for next POR + + displayBaseSuccess(500); // Show 'Base Started' + + if (settings.fixedBase == false) + changeState(STATE_BASE_TEMP_SETTLE); + else if (settings.fixedBase == true) + changeState(STATE_BASE_FIXED_NOT_STARTED); + } + else + { + displayBaseFail(1000); } - } - else - { - displayBaseFail(1000); - } } break; - //Wait for horz acc of 5m or less before starting survey in - case (STATE_BASE_TEMP_SETTLE): - { - //Blink base LED slowly while we wait for first fix - if (millis() - lastBaseLEDupdate > 1000) - { - lastBaseLEDupdate = millis(); + // Wait for horz acc of 5m or less before starting survey in + case (STATE_BASE_TEMP_SETTLE): { + // Blink base LED slowly while we wait for first fix + if (millis() - lastBaseLEDupdate > 1000) + { + lastBaseLEDupdate = millis(); - if (productVariant == RTK_SURVEYOR) - digitalWrite(pin_baseStatusLED, !digitalRead(pin_baseStatusLED)); - } + if ((productVariant == RTK_SURVEYOR) || (productVariant == REFERENCE_STATION)) + digitalWrite(pin_baseStatusLED, !digitalRead(pin_baseStatusLED)); + } - //Check for <1m horz accuracy before starting surveyIn - uint32_t accuracy = i2cGNSS.getHorizontalAccuracy(); + // Check for <1m horz accuracy before starting surveyIn + systemPrintf("Waiting for Horz Accuracy < %0.2f meters: %0.2f, SIV: %d\r\n", + settings.surveyInStartingAccuracy, horizontalAccuracy, numSV); - float f_accuracy = accuracy; - f_accuracy = f_accuracy / 10000.0; // Convert the horizontal accuracy (mm * 10^-1) to a float + if (horizontalAccuracy > 0.0 && horizontalAccuracy < settings.surveyInStartingAccuracy) + { + displaySurveyStart(0); // Show 'Survey' - Serial.printf("Waiting for Horz Accuracy < %0.2f meters: %0.2f\n\r", settings.surveyInStartingAccuracy, f_accuracy); + if (surveyInStart() == true) // Begin survey + { + displaySurveyStarted(500); // Show 'Survey Started' - if (f_accuracy > 0.0 && f_accuracy < settings.surveyInStartingAccuracy) - { - displaySurveyStart(0); //Show 'Survey' + changeState(STATE_BASE_TEMP_SURVEY_STARTED); + } + } + } + break; - if (beginSurveyIn() == true) //Begin survey + // Check survey status until it completes or 15 minutes elapses and we go back to rover + case (STATE_BASE_TEMP_SURVEY_STARTED): { + // Blink base LED quickly during survey in + if (millis() - lastBaseLEDupdate > 500) { - displaySurveyStarted(500); //Show 'Survey Started' + lastBaseLEDupdate = millis(); - changeState(STATE_BASE_TEMP_SURVEY_STARTED); + if ((productVariant == RTK_SURVEYOR) || (productVariant == REFERENCE_STATION)) + digitalWrite(pin_baseStatusLED, !digitalRead(pin_baseStatusLED)); + } + + // Get the data once to avoid duplicate slow responses + svinObservationTime = theGNSS.getSurveyInObservationTime(50); + svinMeanAccuracy = theGNSS.getSurveyInMeanAccuracy(50); + + if (theGNSS.getSurveyInValid(50) == true) // Survey in complete + { + systemPrintf("Observation Time: %d\r\n", svinObservationTime); + systemPrintln("Base survey complete! RTCM now broadcasting."); + + if ((productVariant == RTK_SURVEYOR) || (productVariant == REFERENCE_STATION)) + digitalWrite(pin_baseStatusLED, HIGH); // Indicate survey complete + + // Start the NTRIP server if requested + RTK_MODE(RTK_MODE_BASE_FIXED); + + radioStart(); // Start internal radio if enabled, otherwise disable + + rtcmPacketsSent = 0; // Reset any previous number + changeState(STATE_BASE_TEMP_TRANSMITTING); + } + else + { + systemPrint("Time elapsed: "); + systemPrint(svinObservationTime); + systemPrint(" Accuracy: "); + systemPrint(svinMeanAccuracy, 3); + systemPrint(" SIV: "); + systemPrint(numSV); + systemPrintln(); + + if (svinObservationTime > maxSurveyInWait_s) + { + systemPrintf("Survey-In took more than %d minutes. Returning to rover mode.\r\n", + maxSurveyInWait_s / 60); + + if (surveyInReset() == false) + { + systemPrintln("Survey reset failed - attempt 1/3"); + if (surveyInReset() == false) + { + systemPrintln("Survey reset failed - attempt 2/3"); + if (surveyInReset() == false) + { + systemPrintln("Survey reset failed - attempt 3/3"); + } + } + } + + changeState(STATE_ROVER_NOT_STARTED); + } } - } } break; - //Check survey status until it completes or 15 minutes elapses and we go back to rover - case (STATE_BASE_TEMP_SURVEY_STARTED): - { - //Blink base LED quickly during survey in - if (millis() - lastBaseLEDupdate > 500) - { - lastBaseLEDupdate = millis(); + // Leave base temp transmitting over external radio, or WiFi/NTRIP, or ESP NOW + case (STATE_BASE_TEMP_TRANSMITTING): { + } + break; - if (productVariant == RTK_SURVEYOR) - digitalWrite(pin_baseStatusLED, !digitalRead(pin_baseStatusLED)); - } + /* + .-----------------------------------. + startBase() | STATE_BASE_FIXED_NOT_STARTED | + = false | Text: "Base Started" | + .-------------| | + | '-----------------------------------' + V | + STATE_ROVER_NOT_STARTED | startBase() = true + (Rover diagram) V + .-----------------------------------. + | STATE_BASE_FIXED_TRANSMITTING | + | Castle Base Icon solid | + | "Xmitting" | + | "RTCM: 0" | + '-----------------------------------' + + */ + + // User has set switch to base with fixed option enabled. Let's configure and try to get there. + // If fixed base fails, we'll handle it here + case (STATE_BASE_FIXED_NOT_STARTED): { + RTK_MODE(RTK_MODE_BASE_FIXED); + bool response = startFixedBase(); + if (response == true) + { + if ((productVariant == RTK_SURVEYOR) || (productVariant == REFERENCE_STATION)) + digitalWrite(pin_baseStatusLED, HIGH); // Turn on base LED + + radioStart(); // Start internal radio if enabled, otherwise disable + + changeState(STATE_BASE_FIXED_TRANSMITTING); + } + else + { + systemPrintln("Fixed base start failed"); + displayBaseFail(1000); + + changeState(STATE_ROVER_NOT_STARTED); // Return to rover mode to avoid being in fixed base mode + } + } + break; + + // Leave base fixed transmitting if user has enabled WiFi/NTRIP + case (STATE_BASE_FIXED_TRANSMITTING): { + } + break; + + case (STATE_BUBBLE_LEVEL): { + // Do nothing - display only + } + break; - //Get the data once to avoid duplicate slow responses - svinObservationTime = i2cGNSS.getSurveyInObservationTime(100); - svinMeanAccuracy = i2cGNSS.getSurveyInMeanAccuracy(100); + case (STATE_PROFILE): { + // Do nothing - display only + } + break; - if (i2cGNSS.getSurveyInValid(100) == true) //Survey in complete - { - Serial.printf("obs time: %d\n\r", svinObservationTime); - Serial.println(F("Base survey complete! RTCM now broadcasting.")); + case (STATE_MARK_EVENT): { + bool logged = false; + bool marked = false; + // Gain access to the SPI controller for the microSD card + if (xSemaphoreTake(sdCardSemaphore, fatSemaphore_longWait_ms) == pdPASS) + { + markSemaphore(FUNCTION_MARKEVENT); + + // Record this user event to the log + if (online.logging == true) + { + char nmeaMessage[82]; // Max NMEA sentence length is 82 + createNMEASentence(CUSTOM_NMEA_TYPE_WAYPOINT, nmeaMessage, sizeof(nmeaMessage), + (char *)"CustomEvent"); // textID, buffer, sizeOfBuffer, text + ubxFile->println(nmeaMessage); + logged = true; + } + + // Record this point to the marks file + if (settings.enableMarksFile) + { + // Get the marks file name + char fileName[32]; + bool fileOpen = false; + char markBuffer[100]; + bool sdCardWasOnline; + int year; + int month; + int day; + + // Get the date + year = rtc.getYear(); + month = rtc.getMonth() + 1; + day = rtc.getDay(); + + // Build the file name + snprintf(fileName, sizeof(fileName), "/Marks_%04d_%02d_%02d.csv", year, month, day); + + // Try to gain access the SD card + sdCardWasOnline = online.microSD; + if (online.microSD != true) + beginSD(); + + if (online.microSD == true) + { + // Check if the marks file already exists + bool marksFileExists = false; + if (USE_SPI_MICROSD) + { + marksFileExists = sd->exists(fileName); + } +#ifdef COMPILE_SD_MMC + else + { + marksFileExists = SD_MMC.exists(fileName); + } +#endif // COMPILE_SD_MMC + + // Open the marks file + FileSdFatMMC marksFile; + + if (marksFileExists) + { + if (marksFile && marksFile.open(fileName, O_APPEND | O_WRITE)) + { + fileOpen = true; + marksFile.updateFileCreateTimestamp(); + } + } + else + { + if (marksFile && marksFile.open(fileName, O_CREAT | O_WRITE)) + { + fileOpen = true; + marksFile.updateFileAccessTimestamp(); + + // Add the column headers + // YYYYMMDDHHMMSS, Lat: xxxx, Long: xxxx, Alt: xxxx, SIV: xx, HPA: xxxx, Batt: xxx + // 1 2 3 4 5 6 7 8 9 + // 1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901 + strcpy(markBuffer, "Date, Time, Latitude, Longitude, Altitude Meters, SIV, HPA Meters, " + "Battery Level, Voltage\n"); + marksFile.write((const uint8_t *)markBuffer, strlen(markBuffer)); + } + } + + if (fileOpen) + { + // Create the mark text + // 1 2 3 4 5 6 7 8 + // 12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 + // YYYY-MM-DD, HH:MM:SS, ---Latitude---, --Longitude---, --Alt--,SIV, --HPA---,Level,Volts\n + if (horizontalAccuracy >= 100.) + snprintf( + markBuffer, sizeof(markBuffer), + "%04d-%02d-%02d, %02d:%02d:%02d, %14.9f, %14.9f, %7.1f, %2d, %8.0f, %3d%%, %4.2f\n", + year, month, day, rtc.getHour(true), rtc.getMinute(), rtc.getSecond(), latitude, + longitude, altitude, numSV, horizontalAccuracy, battLevel, battVoltage); + else if (horizontalAccuracy >= 10.) + snprintf( + markBuffer, sizeof(markBuffer), + "%04d-%02d-%02d, %02d:%02d:%02d, %14.9f, %14.9f, %7.1f, %2d, %8.1f, %3d%%, %4.2f\n", + year, month, day, rtc.getHour(true), rtc.getMinute(), rtc.getSecond(), latitude, + longitude, altitude, numSV, horizontalAccuracy, battLevel, battVoltage); + else if (horizontalAccuracy >= 1.) + snprintf( + markBuffer, sizeof(markBuffer), + "%04d-%02d-%02d, %02d:%02d:%02d, %14.9f, %14.9f, %7.1f, %2d, %8.2f, %3d%%, %4.2f\n", + year, month, day, rtc.getHour(true), rtc.getMinute(), rtc.getSecond(), latitude, + longitude, altitude, numSV, horizontalAccuracy, battLevel, battVoltage); + else + snprintf( + markBuffer, sizeof(markBuffer), + "%04d-%02d-%02d, %02d:%02d:%02d, %14.9f, %14.9f, %7.1f, %2d, %8.3f, %3d%%, %4.2f\n", + year, month, day, rtc.getHour(true), rtc.getMinute(), rtc.getSecond(), latitude, + longitude, altitude, numSV, horizontalAccuracy, battLevel, battVoltage); + + // Write the mark to the file + marksFile.write((const uint8_t *)markBuffer, strlen(markBuffer)); + + // Update the file to create time & date + marksFile.updateFileCreateTimestamp(); + + // Close the mark file + marksFile.close(); + + marked = true; + } + + // Dismount the SD card + if (!sdCardWasOnline) + endSD(true, false); + } + } + + // Done with the SPI controller + xSemaphoreGive(sdCardSemaphore); + + // Record this event to the log + if ((online.logging == true) && (settings.enableMarksFile)) + { + if (logged && marked) + displayEventMarked(500); // Show 'Event Marked' + else if (marked) + displayNoLogging(500); // Show 'No Logging' + else if (logged) + displayNotMarked(500); // Show 'Not Marked' + else + displayMarkFailure(500); // Show 'Mark Failure' + } + else if (settings.enableMarksFile) + { + if (marked) + displayMarked(500); // Show 'Marked' + else + displayNotMarked(500); // Show 'Not Marked' + } + else if (logged) + displayEventMarked(500); // Show 'Event Marked' + else + displayNoLogging(500); // Show 'No Logging' + + // Return to the previous state + changeState(lastSystemState); + } // End sdCardSemaphore + else + { + // Enable retry by not changing states + log_d("sdCardSemaphore failed to yield in STATE_MARK_EVENT"); + } + } + break; + + case (STATE_DISPLAY_SETUP): { + if (millis() - lastSetupMenuChange > 1500) + { + forceSystemStateUpdate = true; // Immediately go to this new state + changeState(setupState); // Change to last setup state + if (setupState == STATE_BUBBLE_LEVEL) + RTK_MODE(RTK_MODE_BUBBLE_LEVEL); + } + } + break; + + case (STATE_WIFI_CONFIG_NOT_STARTED): { if (productVariant == RTK_SURVEYOR) - digitalWrite(pin_baseStatusLED, HIGH); //Indicate survey complete + { + // Start BT LED Fade to indicate start of WiFi + btLEDTask.detach(); // Increase BT LED blinker task rate + btLEDTask.attach(btLEDTaskPace33Hz, updateBTled); // Rate in seconds, callback + + digitalWrite(pin_baseStatusLED, LOW); + digitalWrite(pin_positionAccuracyLED_1cm, LOW); + digitalWrite(pin_positionAccuracyLED_10cm, LOW); + digitalWrite(pin_positionAccuracyLED_100cm, LOW); + } - rtcmPacketsSent = 0; //Reset any previous number - changeState(STATE_BASE_TEMP_TRANSMITTING); - } - else - { - Serial.print(F("Time elapsed: ")); - Serial.print(svinObservationTime); - Serial.print(F(" Accuracy: ")); - Serial.print(svinMeanAccuracy); - Serial.print(F(" SIV: ")); - Serial.print(i2cGNSS.getSIV()); - Serial.println(); + if (productVariant == REFERENCE_STATION) + digitalWrite(pin_baseStatusLED, LOW); - if (svinObservationTime > maxSurveyInWait_s) + displayWiFiConfigNotStarted(); // Display immediately during SD cluster pause + + bluetoothStop(); + espnowStop(); + + tasksStopUART2(); // Delete F9 serial tasks if running + if (!startWebServer()) // Start in AP mode and show config html page + changeState(STATE_ROVER_NOT_STARTED); + else { - Serial.printf("Survey-In took more than %d minutes. Returning to rover mode.\n\r", maxSurveyInWait_s / 60); + RTK_MODE(RTK_MODE_WIFI_CONFIG); + changeState(STATE_WIFI_CONFIG); + } + } + break; - resetSurvey(); + case (STATE_WIFI_CONFIG): { + if (incomingSettingsSpot > 0) + { + // Allow for 750ms before we parse buffer for all data to arrive + if (millis() - timeSinceLastIncomingSetting > 750) + { + currentlyParsingData = + true; // Disallow new data to flow from websocket while we are parsing the current data + + systemPrint("Parsing: "); + for (int x = 0; x < incomingSettingsSpot; x++) + systemWrite(incomingSettings[x]); + systemPrintln(); + + parseIncomingSettings(); + settings.updateZEDSettings = + true; // When this profile is loaded next, force system to update ZED settings. + recordSystemSettings(); // Record these settings to unit + + // Clear buffer + incomingSettingsSpot = 0; + memset(incomingSettings, 0, AP_CONFIG_SETTING_SIZE); + + currentlyParsingData = false; // Allow new data from websocket + } + } - changeState(STATE_ROVER_NOT_STARTED); +#ifdef COMPILE_WIFI +#ifdef COMPILE_AP + // Dynamically update the coordinates on the AP page + if (websocketConnected == true) + { + if (millis() - lastDynamicDataUpdate > 1000) + { + lastDynamicDataUpdate = millis(); + createDynamicDataString(settingsCSV); + + // log_d("Sending coordinates: %s", settingsCSV); + websocket->textAll(settingsCSV); + } } - } +#endif // COMPILE_AP +#endif // COMPILE_WIFI } break; - //Leave base temp transmitting if user has enabled WiFi/NTRIP - case (STATE_BASE_TEMP_TRANSMITTING): - { - if (settings.enableNtripServer == true) - { - //Turn off Bluetooth and turn on WiFi - endBluetooth(); - startWiFi(); + // Setup device for testing + case (STATE_TEST): { + // Debounce entry into test menu + if (millis() - lastTestMenuChange > 500) + { + tasksStopUART2(); // Stop absoring ZED serial via task + zedUartPassed = false; + + // Enable RTCM 1230. This is the GLONASS bias sentence and is transmitted + // even if there is no GPS fix. We use it to test serial output. + theGNSS.newCfgValset(); // Create a new Configuration Item VALSET message + theGNSS.addCfgValset(UBLOX_CFG_MSGOUT_RTCM_3X_TYPE1230_UART2, 1); // Enable message 1230 every second + theGNSS.sendCfgValset(); // Send the VALSET - changeState(STATE_BASE_TEMP_WIFI_STARTED); - } + RTK_MODE(RTK_MODE_TESTING); + changeState(STATE_TESTING); + } } break; - //Check to see if we have connected over WiFi - case (STATE_BASE_TEMP_WIFI_STARTED): - { -#ifdef COMPILE_WIFI - byte wifiStatus = WiFi.status(); - if (wifiStatus == WL_CONNECTED) - { - radioState = WIFI_CONNECTED; - - changeState(STATE_BASE_TEMP_WIFI_CONNECTED); - } - else - { - Serial.print(F("WiFi Status: ")); - switch (wifiStatus) { - case WL_NO_SSID_AVAIL: - Serial.printf("SSID '%s' not detected\n\r", settings.wifiSSID); - break; - case WL_NO_SHIELD: Serial.println(F("WL_NO_SHIELD")); break; - case WL_IDLE_STATUS: Serial.println(F("WL_IDLE_STATUS")); break; - case WL_SCAN_COMPLETED: Serial.println(F("WL_SCAN_COMPLETED")); break; - case WL_CONNECTED: Serial.println(F("WL_CONNECTED")); break; - case WL_CONNECT_FAILED: Serial.println(F("WL_CONNECT_FAILED")); break; - case WL_CONNECTION_LOST: Serial.println(F("WL_CONNECTION_LOST")); break; - case WL_DISCONNECTED: Serial.println(F("WL_DISCONNECTED")); break; - } - delay(1000); - } -#endif - } - break; - - case (STATE_BASE_TEMP_WIFI_CONNECTED): - { - if (productVariant == RTK_SURVEYOR) - { - digitalWrite(pin_positionAccuracyLED_1cm, LOW); - digitalWrite(pin_positionAccuracyLED_10cm, LOW); - digitalWrite(pin_positionAccuracyLED_100cm, LOW); - } - - if (settings.enableNtripServer == true) - { - //Open connection to caster service -#ifdef COMPILE_WIFI - if (caster.connect(settings.casterHost, settings.casterPort) == true) //Attempt connection + // Display testing screen - do nothing + case (STATE_TESTING): { + // Exit via button press task + } + break; + +#ifdef COMPILE_L_BAND + case (STATE_KEYS_STARTED): { + if (rtcWaitTime == 0) + rtcWaitTime = millis(); + + // We want an immediate change from this state + forceSystemStateUpdate = true; // Immediately go to this new state + + // If user has turned off PointPerfect, skip everything + if (settings.enablePointPerfectCorrections == false) { - changeState(STATE_BASE_TEMP_CASTER_STARTED); + changeState(settings.lastState); // Go to either rover or base + } - Serial.printf("Connected to %s:%d\n\r", settings.casterHost, settings.casterPort); + // If there is no WiFi setup, and no keys, skip everything + else if (wifiNetworkCount() == 0 && strlen(settings.pointPerfectCurrentKey) == 0) + { + displayNoSSIDs(2000); + changeState(settings.lastState); // Go to either rover or base + } - const int SERVER_BUFFER_SIZE = 512; - char serverBuffer[SERVER_BUFFER_SIZE]; + // If we don't have keys, begin zero touch provisioning + else if (strlen(settings.pointPerfectCurrentKey) == 0 || strlen(settings.pointPerfectNextKey) == 0) + { + log_d("L_Band Keys starting WiFi"); + + // Temporarily limit WiFi connection attempts + wifiOriginalMaxConnectionAttempts = wifiMaxConnectionAttempts; + wifiMaxConnectionAttempts = 0; // Override setting during key retrieval. Give up after single failure. - snprintf(serverBuffer, SERVER_BUFFER_SIZE, "SOURCE %s /%s\r\nSource-Agent: NTRIP %s/%s\r\n\r\n", - settings.mountPointPW, settings.mountPoint, ntrip_server_name, "App Version 1.0"); + wifiStart(); + changeState(STATE_KEYS_PROVISION_WIFI_STARTED); + } - //Serial.printf("Sending credentials:\n%s\n\r", serverBuffer); - caster.write(serverBuffer, strlen(serverBuffer)); + // Determine if we have valid date/time RTC from last boot + else if (online.rtc == false) + { + if (millis() - rtcWaitTime > 2000) + { + // If RTC is not available, we will assume we need keys + changeState(STATE_KEYS_NEEDED); + } + } - casterResponseWaitStartTime = millis(); + else + { + // Determine days until next key expires + int daysRemaining = + daysFromEpoch(settings.pointPerfectNextKeyStart + settings.pointPerfectNextKeyDuration + 1); + log_d("Days until keys expire: %d", daysRemaining); + + if (checkCertificates() && (daysRemaining > 28 && daysRemaining <= 56)) + changeState(STATE_KEYS_DAYS_REMAINING); + else + changeState(STATE_KEYS_NEEDED); } -#endif - } } break; - //Wait for response for caster service and make sure it's valid - case (STATE_BASE_TEMP_CASTER_STARTED): - { -#ifdef COMPILE_WIFI - //Check if caster service responded - if (caster.available() == 0) - { - if (millis() - casterResponseWaitStartTime > 5000) + case (STATE_KEYS_NEEDED): { + forceSystemStateUpdate = true; // immediately go to this new state + + if (online.rtc == false) { - Serial.println(F("Caster failed to respond. Do you have your caster address and port correct?")); - caster.stop(); + log_d("Keys Needed. RTC offline. Starting WiFi"); + + // Temporarily limit WiFi connection attempts + wifiOriginalMaxConnectionAttempts = wifiMaxConnectionAttempts; + wifiMaxConnectionAttempts = 0; // Override setting during key retrieval. Give up after single failure. - changeState(STATE_BASE_TEMP_WIFI_CONNECTED); //Return to previous state + wifiStart(); + changeState(STATE_KEYS_WIFI_STARTED); // If we can't check the RTC, continue } - } - else - { - //Check reply - bool connectionSuccess = false; - char response[512]; - int responseSpot = 0; - while (caster.available()) + + // When did we last try to get keys? Attempt every 24 hours + else if (rtc.getEpoch() - settings.lastKeyAttempt > (60 * 60 * 24)) { - response[responseSpot++] = caster.read(); - if (strstr(response, "200") != NULL) //Look for 'ICY 200 OK' - connectionSuccess = true; - if (responseSpot == 512 - 1) break; + settings.lastKeyAttempt = rtc.getEpoch(); // Mark it + recordSystemSettings(); // Record these settings to unit + + log_d("Keys Needed. Starting WiFi"); + + // Temporarily limit WiFi connection attempts + wifiOriginalMaxConnectionAttempts = wifiMaxConnectionAttempts; + wifiMaxConnectionAttempts = 0; // Override setting during key retrieval. Give up after single failure. + + wifiStart(); // Starts WiFi state machine + changeState(STATE_KEYS_WIFI_STARTED); } - response[responseSpot] = '\0'; - //Serial.printf("Caster responded with: %s\n\r", response); - if (connectionSuccess == false) + // Added to display WiFi error if user selects GetKeys from the display + // Normally, this would be caught during STATE_KEYS_STARTED + else if (wifiNetworkCount() == 0) { - Serial.printf("Caster responded with bad news: %s. Are you sure your caster credentials are correct?\n\r", response); - changeState(STATE_BASE_TEMP_WIFI_CONNECTED); //Return to previous state + displayNoSSIDs(1000); + changeState( + STATE_KEYS_DAYS_REMAINING); // We have valid keys, we've already tried today. No need to try again. } - else + + // Added to allow user to select GetKeys from the display + // This forces a key update + else if (lBandForceGetKeys == true) { - //We're connected! - //Serial.println(F("Connected to caster")); + lBandForceGetKeys = false; - //Reset flags - lastServerReport_ms = millis(); - lastServerSent_ms = millis(); - casterBytesSent = 0; + log_d("Force key update. Starting WiFi"); - rtcmPacketsSent = 0; //Reset any previous number + // Temporarily limit WiFi connection attempts + wifiOriginalMaxConnectionAttempts = wifiMaxConnectionAttempts; + wifiMaxConnectionAttempts = 0; // Override setting during key retrieval. Give up after single failure. - changeState(STATE_BASE_TEMP_CASTER_CONNECTED); + wifiStart(); // Starts WiFi state machine + + changeState(STATE_KEYS_WIFI_STARTED); + } + + else + { + log_d("Already tried to obtain keys for today"); + changeState( + STATE_KEYS_DAYS_REMAINING); // We have valid keys, we've already tried today. No need to try again. } - } -#endif } break; - //Monitor connected state - case (STATE_BASE_TEMP_CASTER_CONNECTED): - { - cyclePositionLEDs(); + case (STATE_KEYS_WIFI_STARTED): { + if (wifiIsConnected()) + changeState(STATE_KEYS_WIFI_CONNECTED); + else + { + wifiShutdown(); // Turn off WiFi -#ifdef COMPILE_WIFI - if (caster.connected() == false) - { - Serial.println(F("Caster no longer connected. Reconnecting...")); - changeState(STATE_BASE_TEMP_WIFI_CONNECTED); //Return to 2 earlier states to try to reconnect - } -#endif + wifiMaxConnectionAttempts = + wifiOriginalMaxConnectionAttempts; // Override setting to 2 attemps during keys + changeState(STATE_KEYS_WIFI_TIMEOUT); + } } break; - //User has set switch to base with fixed option enabled. Let's configure and try to get there. - //If fixed base fails, we'll handle it here - case (STATE_BASE_FIXED_NOT_STARTED): - { - bool response = startFixedBase(); - if (response == true) - { - if (productVariant == RTK_SURVEYOR) - digitalWrite(pin_baseStatusLED, HIGH); //Turn on base LED + case (STATE_KEYS_WIFI_CONNECTED): { - changeState(STATE_BASE_FIXED_TRANSMITTING); - } - else - { - Serial.println(F("Fixed base start failed")); - displayBaseFail(1000); + // Check that the certs are valid + if (checkCertificates() == true) + { + // Update the keys + if (pointperfectUpdateKeys() == true) // Connect to ThingStream MQTT and get PointPerfect key UBX packet + displayKeysUpdated(); + } + else + { + // Erase keys + erasePointperfectCredentials(); - changeState(STATE_ROVER_NOT_STARTED); //Return to rover mode to avoid being in fixed base mode - } + // Provision device + if (pointperfectProvisionDevice() == true) // Connect to ThingStream API and get keys + displayKeysUpdated(); + } + + wifiShutdown(); // Turn off WiFi + forceSystemStateUpdate = true; // Imediately go to this new state + changeState(STATE_KEYS_DAYS_REMAINING); } break; - //Leave base fixed transmitting if user has enabled WiFi/NTRIP - case (STATE_BASE_FIXED_TRANSMITTING): - { - if (settings.enableNtripServer == true) - { - //Turn off Bluetooth and turn on WiFi - endBluetooth(); - startWiFi(); + case (STATE_KEYS_DAYS_REMAINING): { + if (online.rtc == true) + { + if (settings.pointPerfectNextKeyStart > 0) + { + int daysRemaining = + daysFromEpoch(settings.pointPerfectNextKeyStart + settings.pointPerfectNextKeyDuration + 1); + systemPrintf("Days until PointPerfect keys expire: %d\r\n", daysRemaining); + if (daysRemaining >= 0) + { + paintKeyDaysRemaining(daysRemaining, 2000); + } + else + { + paintKeysExpired(); + } + } + } + paintLBandConfigure(); + + forceSystemStateUpdate = true; // Imediately go to this new state + changeState(STATE_KEYS_LBAND_CONFIGURE); + } + break; + + case (STATE_KEYS_LBAND_CONFIGURE): { + // Be sure we ignore any external RTCM sources + theGNSS.setUART2Input(COM_TYPE_UBX); // Set the UART2 to input UBX (no RTCM) - rtcmPacketsSent = 0; //Reset any previous number + pointperfectApplyKeys(); // Send current keys, if available, to ZED-F9P - changeState(STATE_BASE_FIXED_WIFI_STARTED); - } + forceSystemStateUpdate = true; // Imediately go to this new state + changeState(settings.lastState); // Go to either rover or base } break; - //Check to see if we have connected over WiFi - case (STATE_BASE_FIXED_WIFI_STARTED): - { -#ifdef COMPILE_WIFI - byte wifiStatus = WiFi.status(); - if (wifiStatus == WL_CONNECTED) - { - radioState = WIFI_CONNECTED; - - changeState(STATE_BASE_FIXED_WIFI_CONNECTED); - } - else - { - Serial.print(F("WiFi Status: ")); - switch (wifiStatus) { - case WL_NO_SSID_AVAIL: - Serial.printf("SSID '%s' not detected\n\r", settings.wifiSSID); - break; - case WL_NO_SHIELD: Serial.println(F("WL_NO_SHIELD")); break; - case WL_IDLE_STATUS: Serial.println(F("WL_IDLE_STATUS")); break; - case WL_SCAN_COMPLETED: Serial.println(F("WL_SCAN_COMPLETED")); break; - case WL_CONNECTED: Serial.println(F("WL_CONNECTED")); break; - case WL_CONNECT_FAILED: Serial.println(F("WL_CONNECT_FAILED")); break; - case WL_CONNECTION_LOST: Serial.println(F("WL_CONNECTION_LOST")); break; - case WL_DISCONNECTED: Serial.println(F("WL_DISCONNECTED")); break; - } - delay(1000); - } -#endif - } - break; - - case (STATE_BASE_FIXED_WIFI_CONNECTED): - { - if (productVariant == RTK_SURVEYOR) - { - digitalWrite(pin_positionAccuracyLED_1cm, LOW); - digitalWrite(pin_positionAccuracyLED_10cm, LOW); - digitalWrite(pin_positionAccuracyLED_100cm, LOW); - } - if (settings.enableNtripServer == true) - { -#ifdef COMPILE_WIFI - //Open connection to caster service - if (caster.connect(settings.casterHost, settings.casterPort) == true) //Attempt connection + case (STATE_KEYS_WIFI_TIMEOUT): { + paintKeyWiFiFail(2000); + + forceSystemStateUpdate = true; // Imediately go to this new state + + if (online.rtc == true) + { + int daysRemaining = + daysFromEpoch(settings.pointPerfectNextKeyStart + settings.pointPerfectNextKeyDuration + 1); + + if (daysRemaining >= 0) + { + changeState(STATE_KEYS_DAYS_REMAINING); + } + else + { + paintKeysExpired(); + changeState(STATE_KEYS_LBAND_ENCRYPTED); + } + } + else { - changeState(STATE_BASE_FIXED_CASTER_STARTED); + // No WiFi. No RTC. We don't know if the keys we have are expired. Attempt to use them. + changeState(STATE_KEYS_LBAND_CONFIGURE); + } + } + break; - Serial.printf("Connected to %s:%d\n\r", settings.casterHost, settings.casterPort); + case (STATE_KEYS_LBAND_ENCRYPTED): { + // Since L-Band is not available, be sure RTCM can be provided over UART2 + theGNSS.setUART2Input(COM_TYPE_RTCM3); // Set the UART2 to input RTCM - const int SERVER_BUFFER_SIZE = 512; - char serverBuffer[SERVER_BUFFER_SIZE]; + forceSystemStateUpdate = true; // Imediately go to this new state + changeState(settings.lastState); // Go to either rover or base + } + break; - snprintf(serverBuffer, SERVER_BUFFER_SIZE, "SOURCE %s /%s\r\nSource-Agent: NTRIP %s/%s\r\n\r\n", - settings.mountPointPW, settings.mountPoint, ntrip_server_name, "App Version 1.0"); + case (STATE_KEYS_PROVISION_WIFI_STARTED): { + if (wifiIsConnected()) + changeState(STATE_KEYS_PROVISION_WIFI_CONNECTED); + else + { + wifiShutdown(); // Turn off WiFi + changeState(STATE_KEYS_WIFI_TIMEOUT); + } + } + break; - //Serial.printf("Sending credentials:\n%s\n\r", serverBuffer); - caster.write(serverBuffer, strlen(serverBuffer)); + case (STATE_KEYS_PROVISION_WIFI_CONNECTED): { + forceSystemStateUpdate = true; // Imediately go to this new state - casterResponseWaitStartTime = millis(); + if (pointperfectProvisionDevice() == true) + { + displayKeysUpdated(); + changeState(STATE_KEYS_DAYS_REMAINING); } -#endif - } + else + { + paintKeyProvisionFail(10000); // Device not whitelisted. Show device ID. + changeState(STATE_KEYS_LBAND_ENCRYPTED); + } + wifiShutdown(); // Turn off WiFi } break; +#endif // COMPILE_L_BAND - //Wait for response for caster service and make sure it's valid - case (STATE_BASE_FIXED_CASTER_STARTED): - { -#ifdef COMPILE_WIFI - //Check if caster service responded - if (caster.available() < 10) - { - if (millis() - casterResponseWaitStartTime > 5000) + case (STATE_ESPNOW_PAIRING_NOT_STARTED): { +#ifdef COMPILE_ESPNOW + paintEspNowPairing(); + + // Start ESP-Now if needed, put ESP-Now into broadcast state + espnowBeginPairing(); + + changeState(STATE_ESPNOW_PAIRING); +#else // COMPILE_ESPNOW + changeState(STATE_ROVER_NOT_STARTED); +#endif // COMPILE_ESPNOW + } + break; + + case (STATE_ESPNOW_PAIRING): { + if (espnowIsPaired() == true) { - Serial.println(F("Caster failed to respond. Do you have your caster address and port correct?")); - caster.stop(); - delay(10); //Yield to RTOS + paintEspNowPaired(); - changeState(STATE_BASE_FIXED_WIFI_CONNECTED); //Return to previous state + // Return to the previous state + changeState(lastSystemState); } - } - else - { - //Check reply - bool connectionSuccess = false; - char response[512]; - int responseSpot = 0; - while (caster.available()) + else { - response[responseSpot++] = caster.read(); - if (strstr(response, "200") != NULL) //Look for 'ICY 200 OK' - connectionSuccess = true; - if (responseSpot == 512 - 1) break; + uint8_t broadcastMac[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + espnowSendPairMessage(broadcastMac); // Send unit's MAC address over broadcast, no ack, no encryption } - response[responseSpot] = '\0'; - //Serial.printf("Caster responded with: %s\n\r", response); + } + break; + +#ifdef COMPILE_ETHERNET + case (STATE_NTPSERVER_NOT_STARTED): { + RTK_MODE(RTK_MODE_NTP); + firstRoverStart = false; // If NTP is starting, no test menu, normal button use. - if (connectionSuccess == false) + if (online.gnss == false) + return; + + displayNtpStart(500); // Show 'NTP' + + // Start monitoring the UART1 from ZED for NMEA and UBX data (enables logging) + if (tasksStartUART2() && configureUbloxModuleNTP()) { - Serial.printf("Caster responded with bad news: %s. Are you sure your caster credentials are correct?", response); - changeState(STATE_BASE_FIXED_WIFI_CONNECTED); //Return to previous state + settings.updateZEDSettings = false; // On the next boot, no need to update the ZED on this profile + settings.lastState = STATE_NTPSERVER_NOT_STARTED; // Record this state for next POR + recordSystemSettings(); + + if (online.NTPServer) + { + if (settings.debugNtp) + systemPrintln("NTP Server started"); + displayNtpStarted(500); // Show 'NTP Started' + changeState(STATE_NTPSERVER_NO_SYNC); + } + else + { + if (settings.debugNtp) + systemPrintln("NTP Server waiting for Ethernet"); + displayNtpNotReady(1000); // Show 'Ethernet Not Ready' + changeState(STATE_NTPSERVER_NO_SYNC); + } } else { - //We're connected! - //Serial.println(F("Connected to caster")); + if (settings.debugNtp) + systemPrintln("NTP Server ZED configuration failed"); + displayNTPFail(1000); // Show 'NTP Failed' + // Do we stay in STATE_NTPSERVER_NOT_STARTED? Or should we reset? + } + } + break; + + case (STATE_NTPSERVER_NO_SYNC): { + if (rtcSyncd) + { + if (settings.debugNtp) + systemPrintln("NTP Server RTC synchronized"); + changeState(STATE_NTPSERVER_SYNC); + } + } + break; + + case (STATE_NTPSERVER_SYNC): { + // Do nothing - display only + } + break; + + case (STATE_CONFIG_VIA_ETH_NOT_STARTED): { + displayConfigViaEthNotStarted(1500); + + settings.updateZEDSettings = false; // On the next boot, no need to update the ZED on this profile + settings.lastState = STATE_CONFIG_VIA_ETH_STARTED; // Record the _next_ state for POR + recordSystemSettings(); + + forceConfigureViaEthernet(); // Create a file in LittleFS to force code into configure-via-ethernet mode - //Reset flags - lastServerReport_ms = millis(); - lastServerSent_ms = millis(); - casterBytesSent = 0; + ESP.restart(); // Restart to go into the dedicated configure-via-ethernet mode + } + break; - rtcmPacketsSent = 0; //Reset any previous number + case (STATE_CONFIG_VIA_ETH_STARTED): { + RTK_MODE(RTK_MODE_ETHERNET_CONFIG); + // The code should only be able to enter this state if configureViaEthernet is true. + // If configureViaEthernet is not true, we need to restart again. + //(If we continue, startEthernerWebServerESP32W5500 will fail as it won't have exclusive access to SPI and + // ints). + if (!configureViaEthernet) + { + displayConfigViaEthNotStarted(1500); + settings.lastState = STATE_CONFIG_VIA_ETH_STARTED; // Re-record this state for POR + recordSystemSettings(); - changeState(STATE_BASE_FIXED_CASTER_CONNECTED); + forceConfigureViaEthernet(); // Create a file in LittleFS to force code into configure-via-ethernet mode + + ESP.restart(); // Restart to go into the dedicated configure-via-ethernet mode } - } -#endif + + displayConfigViaEthStarted(1500); + + bluetoothStop(); // Should be redundant - but just in case + espnowStop(); // Should be redundant - but just in case + tasksStopUART2(); // Delete F9 serial tasks if running + + ethernetWebServerStartESP32W5500(); // Start Ethernet in dedicated configure-via-ethernet mode + + if (!startWebServer(false, settings.httpPort)) // Start the async web server + changeState(STATE_ROVER_NOT_STARTED); + else + changeState(STATE_CONFIG_VIA_ETH); } break; - //Monitor connected state - case (STATE_BASE_FIXED_CASTER_CONNECTED): - { - cyclePositionLEDs(); + case (STATE_CONFIG_VIA_ETH): { + // Display will show the IP address (displayConfigViaEthernet) + + if (incomingSettingsSpot > 0) + { + // Allow for 750ms before we parse buffer for all data to arrive + if (millis() - timeSinceLastIncomingSetting > 750) + { + currentlyParsingData = + true; // Disallow new data to flow from websocket while we are parsing the current data + + systemPrint("Parsing: "); + for (int x = 0; x < incomingSettingsSpot; x++) + systemWrite(incomingSettings[x]); + systemPrintln(); + + parseIncomingSettings(); + settings.updateZEDSettings = + true; // When this profile is loaded next, force system to update ZED settings. + recordSystemSettings(); // Record these settings to unit + + // Clear buffer + incomingSettingsSpot = 0; + memset(incomingSettings, 0, AP_CONFIG_SETTING_SIZE); + + currentlyParsingData = false; // Allow new data from websocket + } + } #ifdef COMPILE_WIFI - if (caster.connected() == false) - { - changeState(STATE_BASE_FIXED_WIFI_CONNECTED); - } -#endif +#ifdef COMPILE_AP + // Dynamically update the coordinates on the AP page + if (websocketConnected == true) + { + if (millis() - lastDynamicDataUpdate > 1000) + { + lastDynamicDataUpdate = millis(); + createDynamicDataString(settingsCSV); + + // log_d("Sending coordinates: %s", settingsCSV); + websocket->textAll(settingsCSV); + } + } +#endif // COMPILE_AP +#endif // COMPILE_WIFI + } + break; + + case (STATE_CONFIG_VIA_ETH_RESTART_BASE): { + displayConfigViaEthNotStarted(1000); + + ethernetWebServerStopESP32W5500(); + + settings.updateZEDSettings = false; // On the next boot, no need to update the ZED on this profile + settings.lastState = STATE_BASE_NOT_STARTED; // Record the _next_ state for POR + recordSystemSettings(); + + ESP.restart(); } break; +#endif // COMPILE_ETHERNET + case (STATE_SHUTDOWN): { + forceDisplayUpdate = true; + powerDown(true); + } + break; + + default: { + systemPrintf("Unknown state: %d\r\n", systemState); + } + break; + } } - } } -//Change states and print the new state -void changeState(SystemState newState) +// System state changes may only occur within main state machine +// To allow state changes from external sources (ie, Button Tasks) requests can be made +// Requests are handled at the start of updateSystemState() +void requestChangeState(SystemState requestedState) { - systemState = newState; + newSystemStateRequested = true; + requestedSystemState = requestedState; + log_d("Requested System State: %d", requestedSystemState); +} - //Debug print - switch (systemState) - { +// Print the current state +const char *getState(SystemState state, char *buffer) +{ + switch (state) + { case (STATE_ROVER_NOT_STARTED): - Serial.println(F("State: Rover - Not Started")); - break; + return "STATE_ROVER_NOT_STARTED"; case (STATE_ROVER_NO_FIX): - Serial.println(F("State: Rover - No Fix")); - break; + return "STATE_ROVER_NO_FIX"; case (STATE_ROVER_FIX): - Serial.println(F("State: Rover - Fix")); - break; + return "STATE_ROVER_FIX"; case (STATE_ROVER_RTK_FLOAT): - Serial.println(F("State: Rover - RTK Float")); - break; + return "STATE_ROVER_RTK_FLOAT"; case (STATE_ROVER_RTK_FIX): - Serial.println(F("State: Rover - RTK Fix")); - break; + return "STATE_ROVER_RTK_FIX"; case (STATE_BASE_NOT_STARTED): - Serial.println(F("State: Base - Not Started")); - break; + return "STATE_BASE_NOT_STARTED"; case (STATE_BASE_TEMP_SETTLE): - Serial.println(F("State: Base-Temp - Settle")); - break; + return "STATE_BASE_TEMP_SETTLE"; case (STATE_BASE_TEMP_SURVEY_STARTED): - Serial.println(F("State: Base-Temp - Survey Started")); - break; + return "STATE_BASE_TEMP_SURVEY_STARTED"; case (STATE_BASE_TEMP_TRANSMITTING): - Serial.println(F("State: Base-Temp - Transmitting")); - break; - case (STATE_BASE_TEMP_WIFI_STARTED): - Serial.println(F("State: Base-Temp - WiFi Started")); - break; - case (STATE_BASE_TEMP_WIFI_CONNECTED): - Serial.println(F("State: Base-Temp - WiFi Connected")); - break; - case (STATE_BASE_TEMP_CASTER_STARTED): - Serial.println(F("State: Base-Temp - Caster Started")); - break; - case (STATE_BASE_TEMP_CASTER_CONNECTED): - Serial.println(F("State: Base-Temp - Caster Connected")); - break; + return "STATE_BASE_TEMP_TRANSMITTING"; case (STATE_BASE_FIXED_NOT_STARTED): - Serial.println(F("State: Base-Fixed - Not Started")); - break; + return "STATE_BASE_FIXED_NOT_STARTED"; case (STATE_BASE_FIXED_TRANSMITTING): - Serial.println(F("State: Base-Fixed - Transmitting")); - break; - case (STATE_BASE_FIXED_WIFI_STARTED): - Serial.println(F("State: Base-Fixed - WiFi Started")); - break; - case (STATE_BASE_FIXED_WIFI_CONNECTED): - Serial.println(F("State: Base-Fixed - WiFi Connected")); - break; - case (STATE_BASE_FIXED_CASTER_STARTED): - Serial.println(F("State: Base-Fixed - Caster Started")); - break; - case (STATE_BASE_FIXED_CASTER_CONNECTED): - Serial.println(F("State: Base-Fixed - Caster Connected")); - break; - default: - Serial.printf("State Unknown: %d\n\r", systemState); - break; - } + return "STATE_BASE_FIXED_TRANSMITTING"; + case (STATE_BUBBLE_LEVEL): + return "STATE_BUBBLE_LEVEL"; + case (STATE_MARK_EVENT): + return "STATE_MARK_EVENT"; + case (STATE_DISPLAY_SETUP): + return "STATE_DISPLAY_SETUP"; + case (STATE_WIFI_CONFIG_NOT_STARTED): + return "STATE_WIFI_CONFIG_NOT_STARTED"; + case (STATE_WIFI_CONFIG): + return "STATE_WIFI_CONFIG"; + case (STATE_TEST): + return "STATE_TEST"; + case (STATE_TESTING): + return "STATE_TESTING"; + case (STATE_PROFILE): + return "STATE_PROFILE"; +#ifdef COMPILE_L_BAND + case (STATE_KEYS_STARTED): + return "STATE_KEYS_STARTED"; + case (STATE_KEYS_NEEDED): + return "STATE_KEYS_NEEDED"; + case (STATE_KEYS_WIFI_STARTED): + return "STATE_KEYS_WIFI_STARTED"; + case (STATE_KEYS_WIFI_CONNECTED): + return "STATE_KEYS_WIFI_CONNECTED"; + case (STATE_KEYS_WIFI_TIMEOUT): + return "STATE_KEYS_WIFI_TIMEOUT"; + case (STATE_KEYS_EXPIRED): + return "STATE_KEYS_EXPIRED"; + case (STATE_KEYS_DAYS_REMAINING): + return "STATE_KEYS_DAYS_REMAINING"; + case (STATE_KEYS_LBAND_CONFIGURE): + return "STATE_KEYS_LBAND_CONFIGURE"; + case (STATE_KEYS_LBAND_ENCRYPTED): + return "STATE_KEYS_LBAND_ENCRYPTED"; + case (STATE_KEYS_PROVISION_WIFI_STARTED): + return "STATE_KEYS_PROVISION_WIFI_STARTED"; + case (STATE_KEYS_PROVISION_WIFI_CONNECTED): + return "STATE_KEYS_PROVISION_WIFI_CONNECTED"; +#endif // COMPILE_L_BAND + + case (STATE_ESPNOW_PAIRING_NOT_STARTED): + return "STATE_ESPNOW_PAIRING_NOT_STARTED"; + case (STATE_ESPNOW_PAIRING): + return "STATE_ESPNOW_PAIRING"; + + case (STATE_NTPSERVER_NOT_STARTED): + return "STATE_NTPSERVER_NOT_STARTED"; + case (STATE_NTPSERVER_NO_SYNC): + return "STATE_NTPSERVER_NO_SYNC"; + case (STATE_NTPSERVER_SYNC): + return "STATE_NTPSERVER_SYNC"; + + case (STATE_CONFIG_VIA_ETH_NOT_STARTED): + return "STATE_CONFIG_VIA_ETH_NOT_STARTED"; + case (STATE_CONFIG_VIA_ETH_STARTED): + return "STATE_CONFIG_VIA_ETH_STARTED"; + case (STATE_CONFIG_VIA_ETH): + return "STATE_CONFIG_VIA_ETH"; + case (STATE_CONFIG_VIA_ETH_RESTART_BASE): + return "STATE_CONFIG_VIA_ETH_RESTART_BASE"; + + case (STATE_SHUTDOWN): + return "STATE_SHUTDOWN"; + case (STATE_NOT_SET): + return "STATE_NOT_SET"; + } + + // Handle the unknown case + sprintf(buffer, "Unknown: %d", state); + return buffer; +} + +// Change states and print the new state +void changeState(SystemState newState) +{ + char string1[30]; + char string2[30]; + const char *arrow; + const char *asterisk; + const char *initialState; + const char *endingState; + + // Log the heap size at the state change + reportHeapNow(false); + + // Debug print of new state, add leading asterisk for repeated states + if ((!settings.enablePrintDuplicateStates) && (newState == systemState)) + return; + + if (settings.enablePrintStates) + { + arrow = ""; + asterisk = ""; + initialState = ""; + if (newState == systemState) + asterisk = "*"; + else + { + initialState = getState(systemState, string1); + arrow = " --> "; + } + } + + // Set the new state + systemState = newState; + if (settings.enablePrintStates) + { + endingState = getState(newState, string2); + + if (!online.rtc) + systemPrintf("%s%s%s%s\r\n", asterisk, initialState, arrow, endingState); + else + { + // Timestamp the state change + // 1 2 + // 12345678901234567890123456 + // YYYY-mm-dd HH:MM:SS.xxxrn0 + struct tm timeinfo = rtc.getTimeStruct(); + char s[30]; + strftime(s, sizeof(s), "%Y-%m-%d %H:%M:%S", &timeinfo); + systemPrintf("%s%s%s%s, %s.%03ld\r\n", asterisk, initialState, arrow, endingState, s, rtc.getMillis()); + } + } } diff --git a/Firmware/RTK_Surveyor/System.ino b/Firmware/RTK_Surveyor/System.ino index 2d253fb0b..dd0031a40 100644 --- a/Firmware/RTK_Surveyor/System.ino +++ b/Firmware/RTK_Surveyor/System.ino @@ -1,793 +1,1208 @@ -//Get MAC, start radio -//Tack device's MAC address to end of friendly broadcast name -//This allows multiple units to be on at same time -bool startBluetooth() +// Setup the u-blox module for any setup (base or rover) +// In general we check if the setting is incorrect before writing it. Otherwise, the set commands have, on rare +// occasion, become corrupt. The worst is when the I2C port gets turned off or the I2C address gets borked. +bool configureUbloxModule() { -#ifdef COMPILE_BT + if (online.gnss == false) + return (false); - //Get unit MAC address - esp_read_mac(unitMACAddress, ESP_MAC_WIFI_STA); - unitMACAddress[5] += 2; //Convert MAC address to Bluetooth MAC (add 2): https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/system.html#mac-address + bool response = true; - char stateName[10]; - if (buttonPreviousState == BUTTON_ROVER) - strcpy(stateName, "Rover"); - else - strcpy(stateName, "Base"); + // Turn on/off debug messages + if (settings.enableI2Cdebug) + { +#if defined(REF_STN_GNSS_DEBUG) + if (ENABLE_DEVELOPER && productVariant == REFERENCE_STATION) + theGNSS.enableDebugging(serialGNSS); // Output all debug messages over serialGNSS + else +#endif // REF_STN_GNSS_DEBUG + theGNSS.enableDebugging(Serial, true); // Enable only the critical debug messages over Serial + } + else + theGNSS.disableDebugging(); - sprintf(deviceName, "%s %s-%02X%02X", platformPrefix, stateName, unitMACAddress[4], unitMACAddress[5]); //Base mode + // Wait for initial report from module + int maxWait = 2000; + startTime = millis(); + while (pvtUpdated == false) + { + theGNSS.checkUblox(); // Regularly poll to get latest data and any RTCM + theGNSS.checkCallbacks(); // Process any callbacks: ie, eventTriggerReceived + delay(10); + if ((millis() - startTime) > maxWait) + { + log_d("PVT Update failed"); + break; + } + } - if (SerialBT.begin(deviceName, false, settings.sppRxQueueSize, settings.sppTxQueueSize) == false) //localName, isMaster, rxBufferSize, txBufferSize - { - Serial.println(F("An error occurred initializing Bluetooth")); - radioState = RADIO_OFF; + // The first thing we do is go to 1Hz to lighten any I2C traffic from a previous configuration + response &= theGNSS.newCfgValset(); + response &= theGNSS.addCfgValset(UBLOX_CFG_RATE_MEAS, 1000); + response &= theGNSS.addCfgValset(UBLOX_CFG_RATE_NAV, 1); - if (productVariant == RTK_SURVEYOR) - digitalWrite(pin_bluetoothStatusLED, LOW); - return (false); - } - - //Set PIN to 1234 so we can connect to older BT devices, but not require a PIN for modern device pairing - //See issue: https://github.com/sparkfun/SparkFun_RTK_Surveyor/issues/5 - //https://github.com/espressif/esp-idf/issues/1541 - //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- - esp_bt_sp_param_t param_type = ESP_BT_SP_IOCAP_MODE; - - esp_bt_io_cap_t iocap = ESP_BT_IO_CAP_NONE; //Requires pin 1234 on old BT dongle, No prompt on new BT dongle - //esp_bt_io_cap_t iocap = ESP_BT_IO_CAP_OUT; //Works but prompts for either pin (old) or 'Does this 6 pin appear on the device?' (new) - - esp_bt_gap_set_security_param(param_type, &iocap, sizeof(uint8_t)); - - esp_bt_pin_type_t pin_type = ESP_BT_PIN_TYPE_FIXED; - esp_bt_pin_code_t pin_code; - pin_code[0] = '1'; - pin_code[1] = '2'; - pin_code[2] = '3'; - pin_code[3] = '4'; - esp_bt_gap_set_pin(pin_type, 4, pin_code); - //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- - - SerialBT.register_callback(btCallback); //Controls BT Status LED on Surveyor - SerialBT.setTimeout(250); - - Serial.print(F("Bluetooth broadcasting as: ")); - Serial.println(deviceName); - - radioState = BT_ON_NOCONNECTION; - - if (productVariant == RTK_SURVEYOR) - digitalWrite(pin_bluetoothStatusLED, HIGH); - - //Start the tasks for handling incoming and outgoing BT bytes to/from ZED-F9P - if (F9PSerialReadTaskHandle == NULL) - xTaskCreate( - F9PSerialReadTask, - "F9Read", //Just for humans - readTaskStackSize, //Stack Size - NULL, //Task input parameter - 1, //Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest. - &F9PSerialReadTaskHandle); //Task handle - - if (F9PSerialWriteTaskHandle == NULL) - xTaskCreate( - F9PSerialWriteTask, - "F9Write", //Just for humans - writeTaskStackSize, //Stack Size - NULL, //Task input parameter - 0, //Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest. - &F9PSerialWriteTaskHandle); //Task handle - - //Start task for controlling Bluetooth pair LED - if (productVariant == RTK_SURVEYOR) - btLEDTask.attach(btLEDTaskPace, updateBTled); //Rate in seconds, callback -#endif - - return (true); + if (commandSupported(UBLOX_CFG_TMODE_MODE) == true) + response &= theGNSS.addCfgValset(UBLOX_CFG_TMODE_MODE, 0); // Disable survey-in mode + + // UART1 will primarily be used to pass NMEA and UBX from ZED to ESP32 (eventually to cell phone) + // but the phone can also provide RTCM data and a user may want to configure the ZED over Bluetooth. + // So let's be sure to enable UBX+NMEA+RTCM on the input + if (USE_I2C_GNSS) + { + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1OUTPROT_UBX, 1); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1OUTPROT_NMEA, 1); + if (commandSupported(UBLOX_CFG_UART1OUTPROT_RTCM3X) == true) + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1OUTPROT_RTCM3X, 1); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1INPROT_UBX, 1); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1INPROT_NMEA, 1); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1INPROT_RTCM3X, 1); + if (commandSupported(UBLOX_CFG_UART1INPROT_SPARTN) == true) + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1INPROT_SPARTN, 0); + + response &= theGNSS.addCfgValset( + UBLOX_CFG_UART1_BAUDRATE, settings.dataPortBaud); // Defaults to 230400 to maximize message output support + response &= theGNSS.addCfgValset( + UBLOX_CFG_UART2_BAUDRATE, + settings.radioPortBaud); // Defaults to 57600 to match SiK telemetry radio firmware default + + // Disable SPI port - This is just to remove some overhead by ZED + response &= theGNSS.addCfgValset(UBLOX_CFG_SPIOUTPROT_UBX, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_SPIOUTPROT_NMEA, 0); + if (commandSupported(UBLOX_CFG_SPIOUTPROT_RTCM3X) == true) + response &= theGNSS.addCfgValset(UBLOX_CFG_SPIOUTPROT_RTCM3X, 0); + + response &= theGNSS.addCfgValset(UBLOX_CFG_SPIINPROT_UBX, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_SPIINPROT_NMEA, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_SPIINPROT_RTCM3X, 0); + if (commandSupported(UBLOX_CFG_SPIINPROT_SPARTN) == true) + response &= theGNSS.addCfgValset(UBLOX_CFG_SPIINPROT_SPARTN, 0); + } + else // SPI GNSS + { + response &= theGNSS.addCfgValset(UBLOX_CFG_SPIOUTPROT_UBX, 1); + response &= theGNSS.addCfgValset(UBLOX_CFG_SPIOUTPROT_NMEA, 1); + if (commandSupported(UBLOX_CFG_SPIOUTPROT_RTCM3X) == true) + response &= theGNSS.addCfgValset(UBLOX_CFG_SPIOUTPROT_RTCM3X, 1); + response &= theGNSS.addCfgValset(UBLOX_CFG_SPIINPROT_UBX, 1); + response &= theGNSS.addCfgValset(UBLOX_CFG_SPIINPROT_NMEA, 1); + response &= theGNSS.addCfgValset(UBLOX_CFG_SPIINPROT_RTCM3X, 1); + if (commandSupported(UBLOX_CFG_SPIINPROT_SPARTN) == true) + response &= theGNSS.addCfgValset(UBLOX_CFG_SPIINPROT_SPARTN, 0); + + // Disable I2C and UART1 ports - This is just to remove some overhead by ZED + response &= theGNSS.addCfgValset(UBLOX_CFG_I2COUTPROT_UBX, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_I2COUTPROT_NMEA, 0); + if (commandSupported(UBLOX_CFG_I2COUTPROT_RTCM3X) == true) + response &= theGNSS.addCfgValset(UBLOX_CFG_I2COUTPROT_RTCM3X, 0); + + response &= theGNSS.addCfgValset(UBLOX_CFG_I2CINPROT_UBX, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_I2CINPROT_NMEA, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_I2CINPROT_RTCM3X, 0); + if (commandSupported(UBLOX_CFG_I2CINPROT_SPARTN) == true) + response &= theGNSS.addCfgValset(UBLOX_CFG_I2CINPROT_SPARTN, 0); + + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1OUTPROT_UBX, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1OUTPROT_NMEA, 0); + if (commandSupported(UBLOX_CFG_UART1OUTPROT_RTCM3X) == true) + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1OUTPROT_RTCM3X, 0); + + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1INPROT_UBX, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1INPROT_NMEA, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1INPROT_RTCM3X, 0); + if (commandSupported(UBLOX_CFG_UART1INPROT_SPARTN) == true) + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1INPROT_SPARTN, 0); + } + + // Set the UART2 to only do RTCM (in case this device goes into base mode) + response &= theGNSS.addCfgValset(UBLOX_CFG_UART2OUTPROT_UBX, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART2OUTPROT_NMEA, 0); + if (commandSupported(UBLOX_CFG_UART2OUTPROT_RTCM3X) == true) + response &= theGNSS.addCfgValset(UBLOX_CFG_UART2OUTPROT_RTCM3X, 1); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART2INPROT_UBX, settings.enableUART2UBXIn); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART2INPROT_NMEA, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART2INPROT_RTCM3X, 1); + if (commandSupported(UBLOX_CFG_UART2INPROT_SPARTN) == true) + response &= theGNSS.addCfgValset(UBLOX_CFG_UART2INPROT_SPARTN, 0); + + // We don't want NMEA over I2C, but we will want to deliver RTCM, and UBX+RTCM is not an option + if (USE_I2C_GNSS) + { + response &= theGNSS.addCfgValset(UBLOX_CFG_I2COUTPROT_UBX, 1); + response &= theGNSS.addCfgValset(UBLOX_CFG_I2COUTPROT_NMEA, 1); + if (commandSupported(UBLOX_CFG_I2COUTPROT_RTCM3X) == true) + response &= theGNSS.addCfgValset(UBLOX_CFG_I2COUTPROT_RTCM3X, 1); + + response &= theGNSS.addCfgValset(UBLOX_CFG_I2CINPROT_UBX, 1); + response &= theGNSS.addCfgValset(UBLOX_CFG_I2CINPROT_NMEA, 1); + response &= theGNSS.addCfgValset(UBLOX_CFG_I2CINPROT_RTCM3X, 1); + + if (commandSupported(UBLOX_CFG_I2CINPROT_SPARTN) == true) + { + // We push NEO-D9S correction data over the I2C interface via the PMP message. This uses the UBX protocol. + // SPARTN is not needed on I2C + response &= theGNSS.addCfgValset(UBLOX_CFG_I2CINPROT_SPARTN, 0); + } + } + + if (settings.enableZedUsb == true) + { + // The USB port on the ZED may be used for RTCM to/from the computer (as an NTRIP caster or client) + // So let's be sure all protocols are on for the USB port + response &= theGNSS.addCfgValset(UBLOX_CFG_USBOUTPROT_UBX, 1); + response &= theGNSS.addCfgValset(UBLOX_CFG_USBOUTPROT_NMEA, 1); + if (commandSupported(UBLOX_CFG_USBOUTPROT_RTCM3X) == true) + response &= theGNSS.addCfgValset(UBLOX_CFG_USBOUTPROT_RTCM3X, 1); + response &= theGNSS.addCfgValset(UBLOX_CFG_USBINPROT_UBX, 1); + response &= theGNSS.addCfgValset(UBLOX_CFG_USBINPROT_NMEA, 1); + response &= theGNSS.addCfgValset(UBLOX_CFG_USBINPROT_RTCM3X, 1); + if (commandSupported(UBLOX_CFG_USBINPROT_SPARTN) == true) + { + // See issue: https://github.com/sparkfun/SparkFun_RTK_Firmware/issues/713 + response &= theGNSS.addCfgValset(UBLOX_CFG_USBINPROT_SPARTN, 1); + } + } + else + { + //Disable all protocols over USB + response &= theGNSS.addCfgValset(UBLOX_CFG_USBOUTPROT_UBX, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_USBOUTPROT_NMEA, 0); + if (commandSupported(UBLOX_CFG_USBOUTPROT_RTCM3X) == true) + response &= theGNSS.addCfgValset(UBLOX_CFG_USBOUTPROT_RTCM3X, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_USBINPROT_UBX, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_USBINPROT_NMEA, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_USBINPROT_RTCM3X, 0); + if (commandSupported(UBLOX_CFG_USBINPROT_SPARTN) == true) + { + // See issue: https://github.com/sparkfun/SparkFun_RTK_Firmware/issues/713 + response &= theGNSS.addCfgValset(UBLOX_CFG_USBINPROT_SPARTN, 0); + } + } + + if (commandSupported(UBLOX_CFG_NAVSPG_INFIL_MINCNO) == true) + { + if (zedModuleType == PLATFORM_F9R) + response &= theGNSS.addCfgValset( + UBLOX_CFG_NAVSPG_INFIL_MINCNO, + settings.minCNO_F9R); // Set minimum satellite signal level for navigation - default 20 + else + response &= theGNSS.addCfgValset( + UBLOX_CFG_NAVSPG_INFIL_MINCNO, + settings.minCNO_F9P); // Set minimum satellite signal level for navigation - default 6 + } + + if (commandSupported(UBLOX_CFG_NAV2_OUT_ENABLED) == true) + { + // Count NAV2 messages and enable NAV2 as needed. + if (getNAV2MessageCount() > 0) + { + response &= theGNSS.addCfgValset( + UBLOX_CFG_NAV2_OUT_ENABLED, + 1); // Enable NAV2 messages. This has the side effect of causing RTCM to generate twice as fast. + } + else + response &= theGNSS.addCfgValset(UBLOX_CFG_NAV2_OUT_ENABLED, 0); // Disable NAV2 messages + } + + response &= theGNSS.sendCfgValset(); + + if (response == false) + systemPrintln("Module failed config block 0"); + response = true; // Reset + + // Enable the constellations the user has set + response &= setConstellations(true); // 19 messages. Send newCfg or sendCfg with value set + if (response == false) + systemPrintln("Module failed config block 1"); + response = true; // Reset + + // Make sure the appropriate messages are enabled + response &= setMessages(MAX_SET_MESSAGES_RETRIES); // Does a complete open/closed val set + if (response == false) + systemPrintln("Module failed config block 2"); + response = true; // Reset + + // Disable NMEA messages on all but UART1 + response &= theGNSS.newCfgValset(); + + response &= theGNSS.addCfgValset(UBLOX_CFG_MSGOUT_NMEA_ID_GGA_I2C, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_MSGOUT_NMEA_ID_GSA_I2C, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_MSGOUT_NMEA_ID_GSV_I2C, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_MSGOUT_NMEA_ID_RMC_I2C, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_MSGOUT_NMEA_ID_GST_I2C, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_MSGOUT_NMEA_ID_GLL_I2C, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_MSGOUT_NMEA_ID_VTG_I2C, 0); + + response &= theGNSS.addCfgValset(UBLOX_CFG_MSGOUT_NMEA_ID_GGA_UART2, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_MSGOUT_NMEA_ID_GSA_UART2, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_MSGOUT_NMEA_ID_GSV_UART2, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_MSGOUT_NMEA_ID_RMC_UART2, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_MSGOUT_NMEA_ID_GST_UART2, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_MSGOUT_NMEA_ID_GLL_UART2, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_MSGOUT_NMEA_ID_VTG_UART2, 0); + + if (USE_I2C_GNSS) // Don't disable NMEA on SPI if the GNSS is SPI! + { + response &= theGNSS.addCfgValset(UBLOX_CFG_MSGOUT_NMEA_ID_GGA_SPI, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_MSGOUT_NMEA_ID_GSA_SPI, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_MSGOUT_NMEA_ID_GSV_SPI, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_MSGOUT_NMEA_ID_RMC_SPI, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_MSGOUT_NMEA_ID_GST_SPI, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_MSGOUT_NMEA_ID_GLL_SPI, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_MSGOUT_NMEA_ID_VTG_SPI, 0); + } + + if (USE_SPI_GNSS) // If the GNSS is SPI, _do_ disable NMEA on UART1 + { + response &= theGNSS.addCfgValset(UBLOX_CFG_MSGOUT_NMEA_ID_GGA_UART1, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_MSGOUT_NMEA_ID_GSA_UART1, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_MSGOUT_NMEA_ID_GSV_UART1, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_MSGOUT_NMEA_ID_RMC_UART1, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_MSGOUT_NMEA_ID_GST_UART1, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_MSGOUT_NMEA_ID_GLL_UART1, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_MSGOUT_NMEA_ID_VTG_UART1, 0); + } + + response &= theGNSS.sendCfgValset(); + + if (response == false) + systemPrintln("Module failed config block 3"); + + if (zedModuleType == PLATFORM_F9R) + { + response &= theGNSS.setAutoESFSTATUS( + true, false); // Tell the GPS to "send" each ESF Status, but do not update stale data when accessed + } + + return (response); } -//This function stops BT so that it can be restarted later -//It also releases as much system resources as possible so that WiFi/caster is more stable -void endBluetooth() +// Turn on indicator LEDs to verify LED function and indicate setup sucess +void danceLEDs() { - //Delete tasks if running - if (F9PSerialReadTaskHandle != NULL) - { - vTaskDelete(F9PSerialReadTaskHandle); - F9PSerialReadTaskHandle = NULL; - } - if (F9PSerialWriteTaskHandle != NULL) - { - vTaskDelete(F9PSerialWriteTaskHandle); - F9PSerialWriteTaskHandle = NULL; - } - -#ifdef COMPILE_BT - SerialBT.flush(); //Complete any transfers - SerialBT.disconnect(); //Drop any clients - SerialBT.end(); //SerialBT.end() will release significant RAM (~100k!) but a SerialBT.start will crash. -#endif - - //The following code releases the BT hardware so that it can be restarted with a SerialBT.begin - customBTstop(); - Serial.println(F("Bluetooth turned off")); - - radioState = RADIO_OFF; -} + if (productVariant == RTK_SURVEYOR) + { + for (int x = 0; x < 2; x++) + { + digitalWrite(pin_positionAccuracyLED_1cm, HIGH); + digitalWrite(pin_positionAccuracyLED_10cm, HIGH); + digitalWrite(pin_positionAccuracyLED_100cm, HIGH); + digitalWrite(pin_baseStatusLED, HIGH); + digitalWrite(pin_bluetoothStatusLED, HIGH); + delay(100); + digitalWrite(pin_positionAccuracyLED_1cm, LOW); + digitalWrite(pin_positionAccuracyLED_10cm, LOW); + digitalWrite(pin_positionAccuracyLED_100cm, LOW); + digitalWrite(pin_baseStatusLED, LOW); + digitalWrite(pin_bluetoothStatusLED, LOW); + delay(100); + } -//Starting and restarting BT is a problem. See issue: https://github.com/espressif/arduino-esp32/issues/2718 -//To work around the bug without modifying the core we create our own btStop() function with -//the patch from github -bool customBTstop() { -#ifdef COMPILE_BT - if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_IDLE) { - return true; - } - if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_ENABLED) { - if (esp_bt_controller_disable()) { - log_e("BT Disable failed"); - return false; - } - while (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_ENABLED); - } - if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_INITED) - { - log_i("inited"); - if (esp_bt_controller_deinit()) - { - log_e("BT deint failed"); - return false; - } - while (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_INITED) - ; - return true; - } - log_e("BT Stop failed"); -#endif - return false; + digitalWrite(pin_positionAccuracyLED_1cm, HIGH); + digitalWrite(pin_positionAccuracyLED_10cm, HIGH); + digitalWrite(pin_positionAccuracyLED_100cm, HIGH); + digitalWrite(pin_baseStatusLED, HIGH); + digitalWrite(pin_bluetoothStatusLED, HIGH); + + delay(250); + digitalWrite(pin_positionAccuracyLED_1cm, LOW); + delay(250); + digitalWrite(pin_positionAccuracyLED_10cm, LOW); + delay(250); + digitalWrite(pin_positionAccuracyLED_100cm, LOW); + + delay(250); + digitalWrite(pin_baseStatusLED, LOW); + delay(250); + digitalWrite(pin_bluetoothStatusLED, LOW); + } + else + { + // Units can boot under 1s. Keep splash screen up for at least 2s. + while ((millis() - splashStart) < 2000) + delay(1); + } } -//Start WiFi assuming it was previously fully released -//See WiFiBluetoothSwitch sketch for more info -void startWiFi() +// Update Battery level LEDs every 5s +void updateBattery() { -#ifdef COMPILE_WIFI - wifi_init_config_t wifi_init_config = WIFI_INIT_CONFIG_DEFAULT(); - esp_wifi_init(&wifi_init_config); //Restart WiFi resources - - Serial.printf("Connecting to local WiFi: %s\n\r", settings.wifiSSID); - WiFi.begin(settings.wifiSSID, settings.wifiPW); -#endif + if (millis() - lastBattUpdate > 5000) + { + lastBattUpdate = millis(); - radioState = WIFI_ON_NOCONNECTION; + checkBatteryLevels(); + } } -//Stop WiFi and release all resources -//See WiFiBluetoothSwitch sketch for more info -void stopWiFi() +// When called, checks level of battery and updates the LED brightnesses +// And outputs a serial message to USB +void checkBatteryLevels() { -#ifdef COMPILE_WIFI - caster.stop(); - WiFi.mode(WIFI_OFF); - esp_wifi_deinit(); //Free all resources -#endif + if (online.battery == true) + { + battLevel = lipo.getSOC(); + battVoltage = lipo.getVoltage(); + battChangeRate = lipo.getChangeRate(); + } + else + { + // False numbers but above system cut-off level + battLevel = 10; + battVoltage = 3.7; + battChangeRate = 0; + } - Serial.println("WiFi Stopped"); + if (battChangeRate >= -0.01) + externalPowerConnected = true; + else + externalPowerConnected = false; - radioState = RADIO_OFF; + if (settings.enablePrintBatteryMessages) + { + char tempStr[25]; + if (externalPowerConnected) + snprintf(tempStr, sizeof(tempStr), "C"); + else + snprintf(tempStr, sizeof(tempStr), "Disc"); + + systemPrintf("Batt (%d%%): Voltage: %0.02fV", battLevel, battVoltage); + + systemPrintf(" %sharging: %0.02f%%/hr ", tempStr, battChangeRate); + + if (battLevel < 10) + snprintf(tempStr, sizeof(tempStr), "Red"); + else if (battLevel < 50) + snprintf(tempStr, sizeof(tempStr), "Yellow"); + else if (battLevel <= 110) + snprintf(tempStr, sizeof(tempStr), "Green"); + else + snprintf(tempStr, sizeof(tempStr), "No batt"); + + systemPrintf("%s\r\n", tempStr); + } + + // Check if we need to shutdown due to no charging + if (settings.shutdownNoChargeTimeout_s > 0) + { + if (externalPowerConnected == false) + { + int secondsSinceLastCharger = (millis() - shutdownNoChargeTimer) / 1000; + if (secondsSinceLastCharger > settings.shutdownNoChargeTimeout_s) + powerDown(true); + } + else + { + shutdownNoChargeTimer = millis(); // Reset timer because power is attached + } + } + + if (productVariant == RTK_SURVEYOR) + { + if (battLevel < 10) + { + ledcWrite(ledRedChannel, 255); + ledcWrite(ledGreenChannel, 0); + } + else if (battLevel < 50) + { + ledcWrite(ledRedChannel, 128); + ledcWrite(ledGreenChannel, 128); + } + else if (battLevel <= 110) + { + ledcWrite(ledRedChannel, 0); + ledcWrite(ledGreenChannel, 255); + } + else + { + ledcWrite(ledRedChannel, 10); + ledcWrite(ledGreenChannel, 0); + } + } } -//Setup the u-blox module for any setup (base or rover) -//In general we check if the setting is incorrect before writing it. Otherwise, the set commands have, on rare occasion, become -//corrupt. The worst is when the I2C port gets turned off or the I2C address gets borked. -bool configureUbloxModule() +// Ping an I2C device and see if it responds +bool isConnected(uint8_t deviceAddress) { - boolean response = true; - int maxWait = 2000; - - i2cGNSS.checkUblox(); //Regularly poll to get latest data and any RTCM - - //The first thing we do is go to 1Hz to lighten any I2C traffic from a previous configuration - if (i2cGNSS.getNavigationFrequency(maxWait) != 1) - response &= i2cGNSS.setNavigationFrequency(1, maxWait); - if (response == false) - Serial.println(F("Set rate failed")); - - response = i2cGNSS.disableSurveyMode(maxWait); //Disable survey - if (response == false) - Serial.println(F("Disable Survey failed")); - -#define OUTPUT_SETTING 14 -#define INPUT_SETTING 12 - - //UART1 will primarily be used to pass NMEA and UBX from ZED to ESP32 (eventually to cell phone) - //but the phone can also provide RTCM data and a user may want to configure the ZED over Bluetooth. - //So let's be sure to enable UBX+NMEA+RTCM on the input - getPortSettings(COM_PORT_UART1); //Load the settingPayload with this port's settings - if (settingPayload[OUTPUT_SETTING] != (COM_TYPE_NMEA | COM_TYPE_UBX | COM_TYPE_RTCM3) || settingPayload[INPUT_SETTING] != (COM_TYPE_NMEA | COM_TYPE_UBX | COM_TYPE_RTCM3)) - { - response &= i2cGNSS.setPortOutput(COM_PORT_UART1, COM_TYPE_NMEA | COM_TYPE_UBX | COM_TYPE_RTCM3); //Set the UART1 to output UBX+NMEA+RTCM - response &= i2cGNSS.setPortInput(COM_PORT_UART1, COM_TYPE_NMEA | COM_TYPE_UBX | COM_TYPE_RTCM3); //Set the UART1 to input UBX+NMEA+RTCM - } - - //Disable SPI port - This is just to remove some overhead by ZED - getPortSettings(COM_PORT_SPI); //Load the settingPayload with this port's settings - if (settingPayload[OUTPUT_SETTING] != 0 || settingPayload[INPUT_SETTING] != 0) - { - response &= i2cGNSS.setPortOutput(COM_PORT_SPI, 0); //Disable all protocols - response &= i2cGNSS.setPortInput(COM_PORT_SPI, 0); //Disable all protocols - } - - getPortSettings(COM_PORT_UART2); //Load the settingPayload with this port's settings - if (settingPayload[OUTPUT_SETTING] != COM_TYPE_RTCM3 || settingPayload[INPUT_SETTING] != COM_TYPE_RTCM3) - { - response &= i2cGNSS.setPortOutput(COM_PORT_UART2, COM_TYPE_RTCM3); //Set the UART2 to output RTCM (in case this device goes into base mode) - response &= i2cGNSS.setPortInput(COM_PORT_UART2, COM_TYPE_RTCM3); //Set the UART2 to input RTCM - } - - //Turn on RTCM over I2C port so that we can harvest RTCM over I2C and send out over WiFi - //This is easier than parsing over UART because the library handles the frame detection - getPortSettings(COM_PORT_I2C); //Load the settingPayload with this port's settings - if (settingPayload[OUTPUT_SETTING] != (COM_TYPE_UBX | COM_TYPE_RTCM3) || settingPayload[INPUT_SETTING] != COM_TYPE_UBX) - { - response &= i2cGNSS.setPortOutput(COM_PORT_I2C, COM_TYPE_UBX | COM_TYPE_RTCM3); //Set the I2C port to output UBX (config), NMEA (logging), and RTCM3 (casting) - response &= i2cGNSS.setPortInput(COM_PORT_I2C, COM_TYPE_UBX); //Set the I2C port to input UBX only - } - - //The USB port on the ZED may be used for RTCM to/from the computer (as an NTRIP caster or client) - //So let's be sure all protocols are on for the USB port - getPortSettings(COM_PORT_USB); //Load the settingPayload with this port's settings - if (settingPayload[OUTPUT_SETTING] != (COM_TYPE_UBX | COM_TYPE_NMEA | COM_TYPE_RTCM3) || settingPayload[INPUT_SETTING] != (COM_TYPE_UBX | COM_TYPE_NMEA | COM_TYPE_RTCM3)) - { - response &= i2cGNSS.setPortOutput(COM_PORT_USB, (COM_TYPE_UBX | COM_TYPE_NMEA | COM_TYPE_RTCM3)); //Set the USB port to everything - response &= i2cGNSS.setPortInput(COM_PORT_USB, (COM_TYPE_UBX | COM_TYPE_NMEA | COM_TYPE_RTCM3)); //Set the USB port to everything - } - - response &= configureConstellations(); //Enable the constellations the user has set - - response &= configureGNSSMessageRates(COM_PORT_UART1, ubxMessages); //Make sure the appropriate messages are enabled - - response &= i2cGNSS.setAutoPVT(true, false); //Tell the GPS to "send" each solution, but do not update stale data when accessed - response &= i2cGNSS.setAutoHPPOSLLH(true, false); //Tell the GPS to "send" each high res solution, but do not update stale data when accessed - - if (getSerialRate(COM_PORT_UART1) != settings.dataPortBaud) - { - Serial.println(F("Updating UART1 rate")); - i2cGNSS.setSerialRate(settings.dataPortBaud, COM_PORT_UART1); //Set UART1 to 115200 - } - if (getSerialRate(COM_PORT_UART2) != settings.radioPortBaud) - { - Serial.println(F("Updating UART2 rate")); - i2cGNSS.setSerialRate(settings.radioPortBaud, COM_PORT_UART2); //Set UART2 to 57600 to match SiK telemetry radio firmware default - } - - if (response == false) - { - Serial.println(F("Module failed initial config.")); - } - - response &= i2cGNSS.saveConfiguration(); //Save the current settings to flash and BBR - if (response == false) - Serial.println(F("Module failed to save.")); - - //Turn on/off debug messages - if (settings.enableI2Cdebug) - i2cGNSS.enableDebugging(Serial, true); //Enable only the critical debug messages over Serial - else - i2cGNSS.disableDebugging(); - - return (response); + Wire.beginTransmission(deviceAddress); + if (Wire.endTransmission() == 0) + return true; + return false; } +// Create a test file in file structure to make sure we can +bool createTestFile() +{ + FileSdFatMMC testFile; + + // TODO: double-check that SdFat tollerates preceding slashes + char testFileName[40] = "/testfile.txt"; + + // Attempt to write to the file system + if ((!testFile) || (testFile.open(testFileName, O_CREAT | O_APPEND | O_WRITE) != true)) + { + systemPrintln("createTestFile: failed to create (open) test file"); + return (false); + } + + testFile.println("Testing..."); + // File successfully created + testFile.close(); -//Disable all the NMEA sentences on a given com port -bool disableNMEASentences(uint8_t portType) + if (USE_SPI_MICROSD) + { + if (sd->exists(testFileName)) + sd->remove(testFileName); + return (!sd->exists(testFileName)); + } +#ifdef COMPILE_SD_MMC + else + { + if (SD_MMC.exists(testFileName)) + SD_MMC.remove(testFileName); + return (!SD_MMC.exists(testFileName)); + } +#endif // COMPILE_SD_MMC + + return (false); +} + +// If debug option is on, print available heap +void reportHeapNow(bool alwaysPrint) { - bool response = true; - if (getNMEASettings(UBX_NMEA_GGA, portType) != 0) - response &= i2cGNSS.disableNMEAMessage(UBX_NMEA_GGA, portType); - if (getNMEASettings(UBX_NMEA_GSA, portType) != 0) - response &= i2cGNSS.disableNMEAMessage(UBX_NMEA_GSA, portType); - if (getNMEASettings(UBX_NMEA_GSV, portType) != 0) - response &= i2cGNSS.disableNMEAMessage(UBX_NMEA_GSV, portType); - if (getNMEASettings(UBX_NMEA_RMC, portType) != 0) - response &= i2cGNSS.disableNMEAMessage(UBX_NMEA_RMC, portType); - if (getNMEASettings(UBX_NMEA_GST, portType) != 0) - response &= i2cGNSS.disableNMEAMessage(UBX_NMEA_GST, portType); - if (getNMEASettings(UBX_NMEA_GLL, portType) != 0) - response &= i2cGNSS.disableNMEAMessage(UBX_NMEA_GLL, portType); - if (getNMEASettings(UBX_NMEA_VTG, portType) != 0) - response &= i2cGNSS.disableNMEAMessage(UBX_NMEA_VTG, portType); - - return (response); + if (alwaysPrint || (settings.enableHeapReport == true)) + { + lastHeapReport = millis(); + systemPrintf("FreeHeap: %d / HeapLowestPoint: %d / LargestBlock: %d\r\n", ESP.getFreeHeap(), + xPortGetMinimumEverFreeHeapSize(), heap_caps_get_largest_free_block(MALLOC_CAP_8BIT)); + } } -//Enable RTCM sentences for a given com port -//This function is needed when switching from rover to base -//We over-ride user settings in base mode -bool enableRTCMSentences(uint8_t portType) +// If debug option is on, print available heap +void reportHeap() { - bool response = true; - int maxWait = 1000; //When I2C traffic is large during logging, give extra time - - if (getRTCMSettings(UBX_RTCM_1005, portType) != 1) - response &= i2cGNSS.enableRTCMmessage(UBX_RTCM_1005, portType, 1, maxWait); //Enable message 1005 to output through UART2, message every second - if (getRTCMSettings(UBX_RTCM_1074, portType) != 1) - response &= i2cGNSS.enableRTCMmessage(UBX_RTCM_1074, portType, 1, maxWait); - if (getRTCMSettings(UBX_RTCM_1084, portType) != 1) - response &= i2cGNSS.enableRTCMmessage(UBX_RTCM_1084, portType, 1, maxWait); - if (getRTCMSettings(UBX_RTCM_1094, portType) != 1) - response &= i2cGNSS.enableRTCMmessage(UBX_RTCM_1094, portType, 1, maxWait); - if (getRTCMSettings(UBX_RTCM_1124, portType) != 1) - response &= i2cGNSS.enableRTCMmessage(UBX_RTCM_1124, portType, 1, maxWait); - if (getRTCMSettings(UBX_RTCM_1230, portType) != 10) - response &= i2cGNSS.enableRTCMmessage(UBX_RTCM_1230, portType, 10, maxWait); //Enable message every 10 seconds - - return (response); + if (settings.enableHeapReport == true) + { + if (millis() - lastHeapReport > 1000) + { + reportHeapNow(false); + } + } } -//Disable RTCM sentences for a given com port -//This function is needed when switching from base to rover -//It's used for turning off RTCM on USB, UART2, and I2C ports. -//UART1 should be re-enabled with user settings using the configureGNSSMessageRates() function -bool disableRTCMSentences(uint8_t portType) +// Based on current LED state, blink upwards fashion +// Used to indicate casting +void cyclePositionLEDs() { - bool response = true; - int maxWait = 1000; //When I2C traffic is large during logging, give extra time - - if (getRTCMSettings(UBX_RTCM_1005, portType) != 0) - response &= i2cGNSS.disableRTCMmessage(UBX_RTCM_1005, portType, maxWait); - if (getRTCMSettings(UBX_RTCM_1074, portType) != 0) - response &= i2cGNSS.disableRTCMmessage(UBX_RTCM_1074, portType, maxWait); - if (getRTCMSettings(UBX_RTCM_1084, portType) != 0) - response &= i2cGNSS.disableRTCMmessage(UBX_RTCM_1084, portType, maxWait); - if (getRTCMSettings(UBX_RTCM_1094, portType) != 0) - response &= i2cGNSS.disableRTCMmessage(UBX_RTCM_1094, portType, maxWait); - if (getRTCMSettings(UBX_RTCM_1124, portType) != 0) - response &= i2cGNSS.disableRTCMmessage(UBX_RTCM_1124, portType, maxWait); - if (getRTCMSettings(UBX_RTCM_1230, portType) != 0) - response &= i2cGNSS.disableRTCMmessage(UBX_RTCM_1230, portType, maxWait); - return (response); + if (productVariant == RTK_SURVEYOR) + { + // Cycle position LEDs to indicate casting + if (millis() - lastCasterLEDupdate > 500) + { + lastCasterLEDupdate = millis(); + if (digitalRead(pin_positionAccuracyLED_100cm) == HIGH) + { + digitalWrite(pin_positionAccuracyLED_1cm, LOW); + digitalWrite(pin_positionAccuracyLED_10cm, HIGH); + digitalWrite(pin_positionAccuracyLED_100cm, LOW); + } + else if (digitalRead(pin_positionAccuracyLED_10cm) == HIGH) + { + digitalWrite(pin_positionAccuracyLED_1cm, HIGH); + digitalWrite(pin_positionAccuracyLED_10cm, LOW); + digitalWrite(pin_positionAccuracyLED_100cm, LOW); + } + else // Catch all + { + digitalWrite(pin_positionAccuracyLED_1cm, LOW); + digitalWrite(pin_positionAccuracyLED_10cm, LOW); + digitalWrite(pin_positionAccuracyLED_100cm, HIGH); + } + } + } +} + +// Determine MUX pins for this platform and set MUX to ADC/DAC to avoid I2C bus failure +void beginMux() +{ + if (productVariant == RTK_EXPRESS || productVariant == RTK_EXPRESS_PLUS) + { + pin_muxA = 2; + pin_muxB = 4; + } + else if (productVariant == RTK_FACET || productVariant == RTK_FACET_LBAND) + { + pin_muxA = 2; + pin_muxB = 0; + } + + setMuxport(MUX_ADC_DAC); // Set mux to user's choice: NMEA, I2C, PPS, or DAC } -//Given a portID, load the settings associated -bool getPortSettings(uint8_t portID) +// Set the port of the 1:4 dual channel analog mux +// This allows NMEA, I2C, PPS/Event, and ADC/DAC to be routed through data port via software select +void setMuxport(int channelNumber) { - ubxPacket customCfg = {0, 0, 0, 0, 0, settingPayload, 0, 0, SFE_UBLOX_PACKET_VALIDITY_NOT_DEFINED, SFE_UBLOX_PACKET_VALIDITY_NOT_DEFINED}; + if (pin_muxA >= 0 && pin_muxB >= 0) + { + pinMode(pin_muxA, OUTPUT); + pinMode(pin_muxB, OUTPUT); + + if (channelNumber > 3) + return; // Error check + + switch (channelNumber) + { + case 0: + digitalWrite(pin_muxA, LOW); + digitalWrite(pin_muxB, LOW); + break; + case 1: + digitalWrite(pin_muxA, HIGH); + digitalWrite(pin_muxB, LOW); + break; + case 2: + digitalWrite(pin_muxA, LOW); + digitalWrite(pin_muxB, HIGH); + break; + case 3: + digitalWrite(pin_muxA, HIGH); + digitalWrite(pin_muxB, HIGH); + break; + } + } +} - customCfg.cls = UBX_CLASS_CFG; // This is the message Class - customCfg.id = UBX_CFG_PRT; // This is the message ID - customCfg.len = 1; - customCfg.startingSpot = 0; // Always set the startingSpot to zero (unless you really know what you are doing) +// Create $GNTXT, type message complete with CRC +// https://www.nmea.org/Assets/20160520%20txt%20amendment.pdf +// Used for recording system events (boot reason, event triggers, etc) inside the log +void createNMEASentence(customNmeaType_e textID, char *nmeaMessage, size_t sizeOfNmeaMessage, char *textMessage) +{ + // Currently we don't have messages longer than 82 char max so we hardcode the sentence numbers + const uint8_t totalNumberOfSentences = 1; + const uint8_t sentenceNumber = 1; - uint16_t maxWait = 250; // Wait for up to 250ms (Serial may need a lot longer e.g. 1100) + char nmeaTxt[200]; // Max NMEA sentence length is 82 + snprintf(nmeaTxt, sizeof(nmeaTxt), "$GNTXT,%02d,%02d,%02d,%s*", totalNumberOfSentences, sentenceNumber, textID, + textMessage); - settingPayload[0] = portID; //Request the caller's portID from GPS module + // From: http://engineeringnotes.blogspot.com/2015/02/generate-crc-for-nmea-strings-arduino.html + byte CRC = 0; // XOR chars between '$' and '*' + for (byte x = 1; x < strlen(nmeaTxt) - 1; x++) + CRC = CRC ^ nmeaTxt[x]; - // Read the current setting. The results will be loaded into customCfg. - if (i2cGNSS.sendCommand(&customCfg, maxWait) != SFE_UBLOX_STATUS_DATA_RECEIVED) // We are expecting data and an ACK - { - Serial.println(F("getPortSettings failed")); - return (false); - } + snprintf(nmeaMessage, sizeOfNmeaMessage, "%s%02X", nmeaTxt, CRC); +} - return (true); +// Reset settings struct to default initializers +void settingsToDefaults() +{ + static const Settings defaultSettings; + memcpy(&settings, &defaultSettings, sizeof(defaultSettings)); } -//Given a portID and a NMEA message type, load the settings associated -uint8_t getNMEASettings(uint8_t msgID, uint8_t portID) +// Given a spot in the ubxMsg array, return true if this message is supported on this platform and firmware version +bool messageSupported(int messageNumber) { - ubxPacket customCfg = {0, 0, 0, 0, 0, settingPayload, 0, 0, SFE_UBLOX_PACKET_VALIDITY_NOT_DEFINED, SFE_UBLOX_PACKET_VALIDITY_NOT_DEFINED}; + bool messageSupported = false; - customCfg.cls = UBX_CLASS_CFG; // This is the message Class - customCfg.id = UBX_CFG_MSG; // This is the message ID - customCfg.len = 2; - customCfg.startingSpot = 0; // Always set the startingSpot to zero (unless you really know what you are doing) + if ((zedModuleType == PLATFORM_F9P) && + (zedFirmwareVersionInt >= ubxMessages[messageNumber].f9pFirmwareVersionSupported)) + messageSupported = true; + else if ((zedModuleType == PLATFORM_F9R) && + (zedFirmwareVersionInt >= ubxMessages[messageNumber].f9rFirmwareVersionSupported)) + messageSupported = true; - uint16_t maxWait = 250; // Wait for up to 250ms (Serial may need a lot longer e.g. 1100) + return (messageSupported); +} +// Given a command key, return true if that key is supported on this platform and firmware version +bool commandSupported(const uint32_t key) +{ + bool commandSupported = false; - settingPayload[0] = UBX_CLASS_NMEA; - settingPayload[1] = msgID; + // Locate this key in the known key array + int commandNumber = 0; + for (; commandNumber < MAX_UBX_CMD; commandNumber++) + { + if (ubxCommands[commandNumber].cmdKey == key) + break; + } + if (commandNumber == MAX_UBX_CMD) + { + systemPrintf("commandSupported: Unknown command key 0x%02X\r\n", key); + commandSupported = false; + } + else + { + if ((zedModuleType == PLATFORM_F9P) && + (zedFirmwareVersionInt >= ubxCommands[commandNumber].f9pFirmwareVersionSupported)) + commandSupported = true; + else if ((zedModuleType == PLATFORM_F9R) && + (zedFirmwareVersionInt >= ubxCommands[commandNumber].f9rFirmwareVersionSupported)) + commandSupported = true; + } + return (commandSupported); +} - // Read the current setting. The results will be loaded into customCfg. - if (i2cGNSS.sendCommand(&customCfg, maxWait) != SFE_UBLOX_STATUS_DATA_RECEIVED) // We are expecting data and an ACK - { - Serial.println(F("getNMEASettings failed")); - return (false); - } +// Enable all the valid messages for this platform +// There are many messages so split into batches. VALSET is limited to 64 max per batch +// Uses dummy newCfg and sendCfg values to be sure we open/close a complete set +bool setMessages(int maxRetries) +{ + uint32_t spiOffset = + 0; // Set to 3 if using SPI to convert UART1 keys to SPI. This is brittle and non-perfect, but works. + if (USE_SPI_GNSS) + spiOffset = 3; + + bool success = false; + int tryNo = -1; + + // Try up to maxRetries times to configure the messages + // This corrects occasional failures seen on the Reference Station where the GNSS is connected via SPI + // instead of I2C and UART1. I believe the SETVAL ACK is occasionally missed due to the level of messages being + // processed. + while ((++tryNo < maxRetries) && !success) + { + bool response = true; + int messageNumber = 0; + + while (messageNumber < MAX_UBX_MSG) + { + response &= theGNSS.newCfgValset(); + + do + { + if (messageSupported(messageNumber) == true) + { + uint8_t rate = settings.ubxMessageRates[messageNumber]; + + // If the GNSS is SPI, we need to make sure that NAV_PVT, NAV_HPPOSLLH and ESF_STATUS remained + // enabled (but not enabled for logging) + if (USE_SPI_GNSS) + { + if (ubxMessages[messageNumber].msgClass == UBX_CLASS_NAV) + if ((ubxMessages[messageNumber].msgID == UBX_NAV_PVT) || + (ubxMessages[messageNumber].msgID == UBX_NAV_HPPOSLLH)) + if (rate == 0) + rate = 1; + if (ubxMessages[messageNumber].msgClass == UBX_CLASS_ESF) + if (ubxMessages[messageNumber].msgID == UBX_ESF_STATUS) + if (zedModuleType == PLATFORM_F9R) + if (rate == 0) + rate = 1; + if (ubxMessages[messageNumber].msgClass == UBX_CLASS_TIM) + { + if (ubxMessages[messageNumber].msgID == UBX_TIM_TM2) + if (rate == 0) + rate = 1; + if (ubxMessages[messageNumber].msgID == UBX_TIM_TP) + if (HAS_GNSS_TP_INT) + if (rate == 0) + rate = 1; + } + if (ubxMessages[messageNumber].msgClass == UBX_CLASS_RXM) + if (ubxMessages[messageNumber].msgID == UBX_RXM_COR) + if (rate == 0) + rate = 1; + if (ubxMessages[messageNumber].msgClass == UBX_CLASS_NMEA) + if (ubxMessages[messageNumber].msgID == UBX_NMEA_GGA) + if (rate == 0) + rate = 1; + if (ubxMessages[messageNumber].msgClass == UBX_CLASS_MON) + if (ubxMessages[messageNumber].msgID == UBX_MON_HW) + if (rate == 0) + rate = 1; + } + + response &= theGNSS.addCfgValset(ubxMessages[messageNumber].msgConfigKey + spiOffset, rate); + } + messageNumber++; + } while (((messageNumber % 43) < 42) && + (messageNumber < MAX_UBX_MSG)); // Limit 1st batch to 42. Batches after that will be (up to) 43 in + // size. It's a HHGTTG thing. + + if (theGNSS.sendCfgValset() == false) + { + log_d("sendCfg failed at messageNumber %d %s. Try %d of %d.", messageNumber - 1, + (messageNumber - 1) < MAX_UBX_MSG ? ubxMessages[messageNumber - 1].msgTextName : "", tryNo + 1, + maxRetries); + response &= false; // If any one of the Valset fails, report failure overall + } + } + + // For SPI GNSS products, we need to add each message to the GNSS Library logging buffer + // to mimic UART1 + if (USE_SPI_GNSS) + { + uint32_t logRTCMMessages = 0; + uint32_t logNMEAMessages = 0; + + for (messageNumber = 0; messageNumber < MAX_UBX_MSG; messageNumber++) + { + if (ubxMessages[messageNumber].msgClass == UBX_RTCM_MSB) // RTCM messages + { + if (messageSupported(messageNumber) == true) + logRTCMMessages |= ubxMessages[messageNumber].filterMask; + } + else if (ubxMessages[messageNumber].msgClass == UBX_CLASS_NMEA) // NMEA messages + { + if (messageSupported(messageNumber) == true) + logNMEAMessages |= ubxMessages[messageNumber].filterMask; + } + else // UBX messages + { + if (messageSupported(messageNumber) == true) + theGNSS.enableUBXlogging(ubxMessages[messageNumber].msgClass, ubxMessages[messageNumber].msgID, + settings.ubxMessageRates[messageNumber] > 0); + } + } + + theGNSS.setRTCMLoggingMask(logRTCMMessages); + theGNSS.setNMEALoggingMask(logNMEAMessages); + } + + if (response) + success = true; + } - return (settingPayload[2 + portID]); //Return just the byte associated with this portID + return (success); } -//Given a portID and a RTCM message type, load the settings associated -uint8_t getRTCMSettings(uint8_t msgID, uint8_t portID) +// Enable all the valid messages for this platform over the USB port +// Add 2 to every UART1 key. This is brittle and non-perfect, but works. +bool setMessagesUSB(int maxRetries) { - ubxPacket customCfg = {0, 0, 0, 0, 0, settingPayload, 0, 0, SFE_UBLOX_PACKET_VALIDITY_NOT_DEFINED, SFE_UBLOX_PACKET_VALIDITY_NOT_DEFINED}; + bool success = false; + int tryNo = -1; + + // Try up to maxRetries times to configure the messages + // This corrects occasional failures seen on the Reference Station where the GNSS is connected via SPI + // instead of I2C and UART1. I believe the SETVAL ACK is occasionally missed due to the level of messages being + // processed. + while ((++tryNo < maxRetries) && !success) + { + bool response = true; + int messageNumber = 0; + + while (messageNumber < MAX_UBX_MSG) + { + response &= theGNSS.newCfgValset(); + + do + { + if (messageSupported(messageNumber) == true) + response &= theGNSS.addCfgValset(ubxMessages[messageNumber].msgConfigKey + 2, + settings.ubxMessageRates[messageNumber]); + messageNumber++; + } while (((messageNumber % 43) < 42) && + (messageNumber < MAX_UBX_MSG)); // Limit 1st batch to 42. Batches after that will be (up to) 43 in + // size. It's a HHGTTG thing. + + response &= theGNSS.sendCfgValset(); + } + + if (response) + success = true; + } - customCfg.cls = UBX_CLASS_CFG; // This is the message Class - customCfg.id = UBX_CFG_MSG; // This is the message ID - customCfg.len = 2; - customCfg.startingSpot = 0; // Always set the startingSpot to zero (unless you really know what you are doing) + return (success); +} - uint16_t maxWait = 1250; // Wait for up to 1250ms (Serial may need a lot longer e.g. 1100) +// Enable all the valid constellations and bands for this platform +// Band support varies between platforms and firmware versions +// We open/close a complete set if sendCompleteBatch = true +// 19 messages +bool setConstellations(bool sendCompleteBatch) +{ + bool response = true; - settingPayload[0] = UBX_RTCM_MSB; - settingPayload[1] = msgID; + if (sendCompleteBatch) + response &= theGNSS.newCfgValset(); - // Read the current setting. The results will be loaded into customCfg. - if (i2cGNSS.sendCommand(&customCfg, maxWait) != SFE_UBLOX_STATUS_DATA_RECEIVED) // We are expecting data and an ACK - { - Serial.println(F("getRTCMSettings failed")); - return (false); - } + bool enableMe = settings.ubxConstellations[0].enabled; + response &= theGNSS.addCfgValset(settings.ubxConstellations[0].configKey, enableMe); // GPS - return (settingPayload[2 + portID]); //Return just the byte associated with this portID -} + response &= theGNSS.addCfgValset(UBLOX_CFG_SIGNAL_GPS_L1CA_ENA, settings.ubxConstellations[0].enabled); + response &= theGNSS.addCfgValset(UBLOX_CFG_SIGNAL_GPS_L2C_ENA, settings.ubxConstellations[0].enabled); -//Given a portID and a NMEA message type, load the settings associated -uint32_t getSerialRate(uint8_t portID) -{ - ubxPacket customCfg = {0, 0, 0, 0, 0, settingPayload, 0, 0, SFE_UBLOX_PACKET_VALIDITY_NOT_DEFINED, SFE_UBLOX_PACKET_VALIDITY_NOT_DEFINED}; + // v1.12 ZED-F9P firmware does not allow for SBAS control + // Also, if we can't identify the version (99), skip SBAS enable + if ((zedModuleType == PLATFORM_F9P) && ((zedFirmwareVersionInt == 112) || (zedFirmwareVersionInt == 99))) + { + // Skip + } + else + { + response &= theGNSS.addCfgValset(settings.ubxConstellations[1].configKey, + settings.ubxConstellations[1].enabled); // SBAS + response &= theGNSS.addCfgValset(UBLOX_CFG_SIGNAL_SBAS_L1CA_ENA, settings.ubxConstellations[1].enabled); + } - customCfg.cls = UBX_CLASS_CFG; // This is the message Class - customCfg.id = UBX_CFG_PRT; // This is the message ID - customCfg.len = 1; - customCfg.startingSpot = 0; // Always set the startingSpot to zero (unless you really know what you are doing) + response &= + theGNSS.addCfgValset(settings.ubxConstellations[2].configKey, settings.ubxConstellations[2].enabled); // GAL + response &= theGNSS.addCfgValset(UBLOX_CFG_SIGNAL_GAL_E1_ENA, settings.ubxConstellations[2].enabled); + response &= theGNSS.addCfgValset(UBLOX_CFG_SIGNAL_GAL_E5B_ENA, settings.ubxConstellations[2].enabled); - uint16_t maxWait = 250; // Wait for up to 250ms (Serial may need a lot longer e.g. 1100) + response &= + theGNSS.addCfgValset(settings.ubxConstellations[3].configKey, settings.ubxConstellations[3].enabled); // BDS + response &= theGNSS.addCfgValset(UBLOX_CFG_SIGNAL_BDS_B1_ENA, settings.ubxConstellations[3].enabled); + response &= theGNSS.addCfgValset(UBLOX_CFG_SIGNAL_BDS_B2_ENA, settings.ubxConstellations[3].enabled); - settingPayload[0] = portID; + response &= + theGNSS.addCfgValset(settings.ubxConstellations[4].configKey, settings.ubxConstellations[4].enabled); // QZSS + response &= theGNSS.addCfgValset(UBLOX_CFG_SIGNAL_QZSS_L1CA_ENA, settings.ubxConstellations[4].enabled); - // Read the current setting. The results will be loaded into customCfg. - if (i2cGNSS.sendCommand(&customCfg, maxWait) != SFE_UBLOX_STATUS_DATA_RECEIVED) // We are expecting data and an ACK - { - Serial.println(F("getSerialRate failed")); - return (false); - } + // UBLOX_CFG_SIGNAL_QZSS_L1S_ENA not supported on F9R in v1.21 and below + if (zedModuleType == PLATFORM_F9P) + response &= theGNSS.addCfgValset(UBLOX_CFG_SIGNAL_QZSS_L1S_ENA, settings.ubxConstellations[4].enabled); + else if ((zedModuleType == PLATFORM_F9R) && (zedFirmwareVersionInt > 121)) + response &= theGNSS.addCfgValset(UBLOX_CFG_SIGNAL_QZSS_L1S_ENA, settings.ubxConstellations[4].enabled); + + response &= theGNSS.addCfgValset(UBLOX_CFG_SIGNAL_QZSS_L2C_ENA, settings.ubxConstellations[4].enabled); + + response &= + theGNSS.addCfgValset(settings.ubxConstellations[5].configKey, settings.ubxConstellations[5].enabled); // GLO + response &= theGNSS.addCfgValset(UBLOX_CFG_SIGNAL_GLO_L1_ENA, settings.ubxConstellations[5].enabled); + response &= theGNSS.addCfgValset(UBLOX_CFG_SIGNAL_GLO_L2_ENA, settings.ubxConstellations[5].enabled); - return (((uint32_t)settingPayload[10] << 16) | ((uint32_t)settingPayload[9] << 8) | settingPayload[8]); + if (sendCompleteBatch) + response &= theGNSS.sendCfgValset(); + + return (response); } -//Freeze displaying a given error code -void blinkError(t_errorNumber errorNumber) +// Periodically print position if enabled +void printPosition() { - while (1) - { - for (int x = 0 ; x < errorNumber ; x++) + // Periodically print the position + if (settings.enablePrintPosition && ((millis() - lastPrintPosition) > 15000)) { - if (productVariant == RTK_SURVEYOR) - { - digitalWrite(pin_positionAccuracyLED_1cm, HIGH); - digitalWrite(pin_positionAccuracyLED_10cm, HIGH); - digitalWrite(pin_positionAccuracyLED_100cm, HIGH); - digitalWrite(pin_baseStatusLED, HIGH); - digitalWrite(pin_bluetoothStatusLED, HIGH); - delay(200); - digitalWrite(pin_positionAccuracyLED_1cm, LOW); - digitalWrite(pin_positionAccuracyLED_10cm, LOW); - digitalWrite(pin_positionAccuracyLED_100cm, LOW); - digitalWrite(pin_baseStatusLED, LOW); - digitalWrite(pin_bluetoothStatusLED, LOW); - delay(200); - } + printCurrentConditions(); + lastPrintPosition = millis(); } - - delay(2000); - } } -//Turn on indicator LEDs to verify LED function and indicate setup sucess -void danceLEDs() +// Periodically print RTK state if enabled +void printRTKState() { - if (productVariant == RTK_SURVEYOR) - { - for (int x = 0 ; x < 2 ; x++) - { - digitalWrite(pin_positionAccuracyLED_1cm, HIGH); - digitalWrite(pin_positionAccuracyLED_10cm, HIGH); - digitalWrite(pin_positionAccuracyLED_100cm, HIGH); - digitalWrite(pin_baseStatusLED, HIGH); - digitalWrite(pin_bluetoothStatusLED, HIGH); - delay(100); - digitalWrite(pin_positionAccuracyLED_1cm, LOW); - digitalWrite(pin_positionAccuracyLED_10cm, LOW); - digitalWrite(pin_positionAccuracyLED_100cm, LOW); - digitalWrite(pin_baseStatusLED, LOW); - digitalWrite(pin_bluetoothStatusLED, LOW); - delay(100); - } - - digitalWrite(pin_positionAccuracyLED_1cm, HIGH); - digitalWrite(pin_positionAccuracyLED_10cm, HIGH); - digitalWrite(pin_positionAccuracyLED_100cm, HIGH); - digitalWrite(pin_baseStatusLED, HIGH); - digitalWrite(pin_bluetoothStatusLED, HIGH); - - delay(250); - digitalWrite(pin_positionAccuracyLED_1cm, LOW); - delay(250); - digitalWrite(pin_positionAccuracyLED_10cm, LOW); - delay(250); - digitalWrite(pin_positionAccuracyLED_100cm, LOW); - - delay(250); - digitalWrite(pin_baseStatusLED, LOW); - delay(250); - digitalWrite(pin_bluetoothStatusLED, LOW); - } + // Periodically print the RTK state + if (settings.enablePrintState && ((millis() - lastPrintState) > 15000)) + { + printCurrentRTKState(); + lastPrintState = millis(); + } } -//Call back for when BT connection event happens (connected/disconnect) -//Used for updating the radioState state machine -#ifdef COMPILE_BT -void btCallback(esp_spp_cb_event_t event, esp_spp_cb_param_t *param) { - if (event == ESP_SPP_SRV_OPEN_EVT) { - Serial.println(F("Client Connected")); - radioState = BT_CONNECTED; - if (productVariant == RTK_SURVEYOR) - digitalWrite(pin_bluetoothStatusLED, HIGH); - } +// Given a user's string, try to identify the type and return the coordinate in DD.ddddddddd format +CoordinateInputType coordinateIdentifyInputType(const char *userEntryOriginal, double *coordinate) +{ + char userEntry[50]; + strncpy(userEntry, userEntryOriginal, + sizeof(userEntry) - 1); // strtok modifies the message so make copy into userEntry - if (event == ESP_SPP_CLOSE_EVT ) { - Serial.println(F("Client disconnected")); - radioState = BT_ON_NOCONNECTION; - if (productVariant == RTK_SURVEYOR) - digitalWrite(pin_bluetoothStatusLED, LOW); - } -} -#endif + *coordinate = 0.0; // Clear what is given to us -//Update Battery level LEDs every 5s -void updateBattLEDs() -{ - if (millis() - lastBattUpdate > 5000) - { - lastBattUpdate = millis(); + CoordinateInputType coordinateInputType = COORDINATE_INPUT_TYPE_INVALID_UNKNOWN; - checkBatteryLevels(); - } -} + int dashCount = 0; + int spaceCount = 0; + int decimalCount = 0; + int lengthOfLeadingNumber = 0; -//When called, checks level of battery and updates the LED brightnesses -//And outputs a serial message to USB -void checkBatteryLevels() -{ - battLevel = lipo.getSOC(); - battVoltage = lipo.getVoltage(); - battChangeRate = lipo.getChangeRate(); + // Scan entry for invalid chars + // A valid entry has only numbers, -, ' ', and . + for (int x = 0; x < strlen(userEntry); x++) + { + if (isdigit(userEntry[x])) // All good + { + if (decimalCount == 0) + lengthOfLeadingNumber++; + } + else if (userEntry[x] == '-') + dashCount++; // All good + else if (userEntry[x] == ' ') + spaceCount++; // All good + else if (userEntry[x] == '.') + decimalCount++; // All good + else + return (COORDINATE_INPUT_TYPE_INVALID_UNKNOWN); // String contains invalid character + } - Serial.printf("Batt (%d%%): Voltage: %0.02fV", battLevel, battVoltage); + // Seven possible entry types + // DD.ddddddddd + // DDMM.mmmmmmm + // DD MM.mmmmmmm + // DD-MM.mmmmmmm + // DDMMSS.ssssss + // DD MM SS.ssssss + // DD-MM-SS.ssssss + // DDMMSS + // DD MM SS + // DD-MM-SS + + if (decimalCount > 1) + return (COORDINATE_INPUT_TYPE_INVALID_UNKNOWN); // 40.09.033 is not valid. + if (spaceCount > 2) + return (COORDINATE_INPUT_TYPE_INVALID_UNKNOWN); // Only 0, 1, or 2 allowed. 40 05 25.2049 is valid. + if (dashCount > 3) + return (COORDINATE_INPUT_TYPE_INVALID_UNKNOWN); // Only 0, 1, 2, or 3 allowed. -105-11-05.1629 is valid. + if (lengthOfLeadingNumber > 7) + return (COORDINATE_INPUT_TYPE_INVALID_UNKNOWN); // Only 7 or fewer. -1051105.188992 (DDDMMSS or DDMMSS) is valid + + bool negativeSign = false; + if (userEntry[0] == '-') + { + userEntry[0] = ' '; + negativeSign = true; + dashCount--; // Use dashCount as the internal dashes only, not the leading negative sign + } + + if (spaceCount == 0 && dashCount == 0 && + (lengthOfLeadingNumber == 7 || lengthOfLeadingNumber == 6)) // DDMMSS.ssssss or DDMMSS + { + coordinateInputType = COORDINATE_INPUT_TYPE_DDMMSS; - char tempStr[25]; - if (battChangeRate > 0) - sprintf(tempStr, "C"); - else - sprintf(tempStr, "Disc"); - Serial.printf(" %sharging: %0.02f%%/hr ", tempStr, battChangeRate); + long intPortion = atoi(userEntry); // Get DDDMMSS + long decimal = intPortion / 10000L; // Get DDD + intPortion -= (decimal * 10000L); + long minutes = intPortion / 100L; // Get MM - if (battLevel < 10) - sprintf(tempStr, "Red"); - else if (battLevel < 50) - sprintf(tempStr, "Yellow"); - else if (battLevel >= 50) - sprintf(tempStr, "Green"); - else - sprintf(tempStr, "No batt"); + // Find '.' + char *decimalPtr = strchr(userEntry, '.'); + if (decimalPtr == nullptr) + coordinateInputType = COORDINATE_INPUT_TYPE_DDMMSS_NO_DECIMAL; - Serial.printf("%s\n\r", tempStr); + double seconds = atof(userEntry); // Get DDDMMSS.ssssss + seconds -= (decimal * 10000); // Remove DDD + seconds -= (minutes * 100); // Remove MM + *coordinate = decimal + (minutes / (double)60) + (seconds / (double)3600); - if (productVariant == RTK_SURVEYOR) - { - if (battLevel < 10) + if (negativeSign) + *coordinate *= -1; + } + else if (spaceCount == 0 && dashCount == 0 && + (lengthOfLeadingNumber == 5 || lengthOfLeadingNumber == 4)) // DDMM.mmmmmmm { - ledcWrite(ledRedChannel, 255); - ledcWrite(ledGreenChannel, 0); + coordinateInputType = COORDINATE_INPUT_TYPE_DDMM; + + long intPortion = atoi(userEntry); // Get DDDMM + long decimal = intPortion / 100L; // Get DDD + intPortion -= (decimal * 100L); + double minutes = atof(userEntry); // Get DDDMM.mmmmmmm + minutes -= (decimal * 100L); // Remove DDD + *coordinate = decimal + (minutes / (double)60); + if (negativeSign) + *coordinate *= -1; } - else if (battLevel < 50) + else if (dashCount == 1) // DD-MM.mmmmmmm { - ledcWrite(ledRedChannel, 128); - ledcWrite(ledGreenChannel, 128); + coordinateInputType = COORDINATE_INPUT_TYPE_DD_MM_DASH; + + char *token = strtok(userEntry, "-"); // Modifies the given array + // We trust that token points at something because the dashCount is > 0 + int decimal = atoi(token); // Get DD + token = strtok(nullptr, "-"); + double minutes = atof(token); // Get MM.mmmmmmm + *coordinate = decimal + (minutes / 60.0); + if (negativeSign) + *coordinate *= -1; } - else if (battLevel >= 50) + else if (dashCount == 2) // DD-MM-SS.ssss or DD-MM-SS { - ledcWrite(ledRedChannel, 0); - ledcWrite(ledGreenChannel, 255); + coordinateInputType = COORDINATE_INPUT_TYPE_DD_MM_SS_DASH; + + char *token = strtok(userEntry, "-"); // Modifies the given array + // We trust that token points at something because the spaceCount is > 0 + int decimal = atoi(token); // Get DD + token = strtok(nullptr, "-"); + int minutes = atoi(token); // Get MM + token = strtok(nullptr, "-"); + + // Find '.' + char *decimalPtr = strchr(token, '.'); // Use token, not userEntry, as the dashes are now NULL + if (decimalPtr == nullptr) + coordinateInputType = COORDINATE_INPUT_TYPE_DD_MM_SS_DASH_NO_DECIMAL; + + double seconds = atof(token); // Get SS.ssssss + *coordinate = decimal + (minutes / (double)60) + (seconds / (double)3600); + if (negativeSign) + *coordinate *= -1; } - else + else if (spaceCount == 0) // DD.dddddd { - ledcWrite(ledRedChannel, 10); - ledcWrite(ledGreenChannel, 0); + coordinateInputType = COORDINATE_INPUT_TYPE_DD; + sscanf(userEntry, "%lf", coordinate); // Load float from userEntry into coordinate + if (negativeSign) + *coordinate *= -1; } - } -} + else if (spaceCount == 1) // DD MM.mmmmmmm + { + coordinateInputType = COORDINATE_INPUT_TYPE_DD_MM; + + char *token = strtok(userEntry, " "); // Modifies the given array + // We trust that token points at something because the spaceCount is > 0 + int decimal = atoi(token); // Get DD + token = strtok(nullptr, " "); + double minutes = atof(token); // Get MM.mmmmmmm + *coordinate = decimal + (minutes / 60.0); + if (negativeSign) + *coordinate *= -1; + } + else if (spaceCount == 2) // DD MM SS.ssssss or DD MM SS + { + coordinateInputType = COORDINATE_INPUT_TYPE_DD_MM_SS; -//Ping an I2C device and see if it responds -bool isConnected(uint8_t deviceAddress) -{ - Wire.beginTransmission(deviceAddress); - if (Wire.endTransmission() == 0) - return true; - return false; -} + char *token = strtok(userEntry, " "); // Modifies the given array + // We trust that token points at something because the spaceCount is > 0 + int decimal = atoi(token); // Get DD + token = strtok(nullptr, " "); + int minutes = atoi(token); // Get MM + token = strtok(nullptr, " "); -//Given text, a position, and kerning, print text to display -//This is helpful for squishing or stretching a string to appropriately fill the display -void printTextwithKerning(char *newText, uint8_t xPos, uint8_t yPos, uint8_t kerning) -{ - for (int x = 0 ; x < strlen(newText) ; x++) - { - oled.setCursor(xPos, yPos); - oled.print(newText[x]); - xPos += kerning; - } -} -//Create a test file in file structure to make sure we can -bool createTestFile() -{ - SdFile testFile; - char testFileName[40] = "testfile.txt"; + // Find '.' + char *decimalPtr = strchr(token, '.'); + if (decimalPtr == nullptr) + coordinateInputType = COORDINATE_INPUT_TYPE_DD_MM_SS_NO_DECIMAL; - //Attempt to write to file system. This avoids collisions with file writing from other functions like recordSystemSettingsToFile() and F9PSerialReadTask() - if (xSemaphoreTake(xFATSemaphore, fatSemaphore_shortWait_ms) == pdPASS) - { - if (testFile.open(testFileName, O_CREAT | O_APPEND | O_WRITE) == true) - { - testFile.close(); + double seconds = atof(token); // Get SS.ssssss - if (sd.exists(testFileName)) - sd.remove(testFileName); - xSemaphoreGive(xFATSemaphore); - return (true); + *coordinate = decimal + (minutes / (double)60) + (seconds / (double)3600); + if (negativeSign) + *coordinate *= -1; } - xSemaphoreGive(xFATSemaphore); - } - return (false); + return (coordinateInputType); } -//If debug option is on, print available heap -void reportHeap() +// Given a coordinate and input type, output a string +// So DD.ddddddddd can become 'DD MM SS.ssssss', etc +void coordinateConvertInput(double coordinate, CoordinateInputType coordinateInputType, char *coordinateString, + int sizeOfCoordinateString) { - if (settings.enableHeapReport == true) - { - if (millis() - lastHeapReport > 1000) + if (coordinateInputType == COORDINATE_INPUT_TYPE_DD) { - lastHeapReport = millis(); - Serial.printf("FreeHeap: %d / HeapLowestPoint: %d / LargestBlock: %d\n\r", ESP.getFreeHeap(), xPortGetMinimumEverFreeHeapSize(), heap_caps_get_largest_free_block(MALLOC_CAP_8BIT)); + snprintf(coordinateString, sizeOfCoordinateString, "%0.9f", coordinate); } - } -} - -//Based on current LED state, blink upwards fashion -//Used to indicate casting -void cyclePositionLEDs() -{ - if (productVariant == RTK_SURVEYOR) - { - //Cycle position LEDs to indicate casting - if (millis() - lastCasterLEDupdate > 500) - { - lastCasterLEDupdate = millis(); - if (digitalRead(pin_positionAccuracyLED_100cm) == HIGH) - { - digitalWrite(pin_positionAccuracyLED_1cm, LOW); - digitalWrite(pin_positionAccuracyLED_10cm, HIGH); - digitalWrite(pin_positionAccuracyLED_100cm, LOW); - } - else if (digitalRead(pin_positionAccuracyLED_10cm) == HIGH) - { - digitalWrite(pin_positionAccuracyLED_1cm, HIGH); - digitalWrite(pin_positionAccuracyLED_10cm, LOW); - digitalWrite(pin_positionAccuracyLED_100cm, LOW); - } - else //Catch all - { - digitalWrite(pin_positionAccuracyLED_1cm, LOW); - digitalWrite(pin_positionAccuracyLED_10cm, LOW); - digitalWrite(pin_positionAccuracyLED_100cm, HIGH); - } + else if (coordinateInputType == COORDINATE_INPUT_TYPE_DD_MM || coordinateInputType == COORDINATE_INPUT_TYPE_DDMM || + coordinateInputType == COORDINATE_INPUT_TYPE_DD_MM_DASH || + coordinateInputType == COORDINATE_INPUT_TYPE_DD_MM_SYMBOL) + { + int longitudeDegrees = (int)coordinate; + coordinate -= longitudeDegrees; + coordinate *= 60; + if (coordinate < 1) + coordinate *= -1; + + if (coordinateInputType == COORDINATE_INPUT_TYPE_DDMM) + snprintf(coordinateString, sizeOfCoordinateString, "%02d%010.7f", longitudeDegrees, coordinate); + else if (coordinateInputType == COORDINATE_INPUT_TYPE_DD_MM_DASH) + snprintf(coordinateString, sizeOfCoordinateString, "%02d-%010.7f", longitudeDegrees, coordinate); + else if (coordinateInputType == COORDINATE_INPUT_TYPE_DD_MM_SYMBOL) + snprintf(coordinateString, sizeOfCoordinateString, "%02d°%010.7f'", longitudeDegrees, coordinate); + else if (coordinateInputType == COORDINATE_INPUT_TYPE_DD_MM) + snprintf(coordinateString, sizeOfCoordinateString, "%02d %010.7f", longitudeDegrees, coordinate); + } + else if (coordinateInputType == COORDINATE_INPUT_TYPE_DD_MM_SS || + coordinateInputType == COORDINATE_INPUT_TYPE_DDMMSS || + coordinateInputType == COORDINATE_INPUT_TYPE_DD_MM_SS_DASH || + coordinateInputType == COORDINATE_INPUT_TYPE_DD_MM_SS_SYMBOL || + coordinateInputType == COORDINATE_INPUT_TYPE_DDMMSS_NO_DECIMAL || + coordinateInputType == COORDINATE_INPUT_TYPE_DD_MM_SS_NO_DECIMAL || + coordinateInputType == COORDINATE_INPUT_TYPE_DD_MM_SS_DASH_NO_DECIMAL) + { + int longitudeDegrees = (int)coordinate; + coordinate -= longitudeDegrees; + coordinate *= 60; + if (coordinate < 1) + coordinate *= -1; + + int longitudeMinutes = (int)coordinate; + coordinate -= longitudeMinutes; + coordinate *= 60; + if (coordinateInputType == COORDINATE_INPUT_TYPE_DDMMSS) + snprintf(coordinateString, sizeOfCoordinateString, "%02d%02d%09.6f", longitudeDegrees, longitudeMinutes, + coordinate); + else if (coordinateInputType == COORDINATE_INPUT_TYPE_DD_MM_SS_DASH) + snprintf(coordinateString, sizeOfCoordinateString, "%02d-%02d-%09.6f", longitudeDegrees, longitudeMinutes, + coordinate); + else if (coordinateInputType == COORDINATE_INPUT_TYPE_DD_MM_SS_SYMBOL) + snprintf(coordinateString, sizeOfCoordinateString, "%02d°%02d'%09.6f\"", longitudeDegrees, longitudeMinutes, + coordinate); + else if (coordinateInputType == COORDINATE_INPUT_TYPE_DD_MM_SS) + snprintf(coordinateString, sizeOfCoordinateString, "%02d %02d %09.6f", longitudeDegrees, longitudeMinutes, + coordinate); + else if (coordinateInputType == COORDINATE_INPUT_TYPE_DDMMSS_NO_DECIMAL) + snprintf(coordinateString, sizeOfCoordinateString, "%02d%02d%02d", longitudeDegrees, longitudeMinutes, + (int)round(coordinate)); + else if (coordinateInputType == COORDINATE_INPUT_TYPE_DD_MM_SS_NO_DECIMAL) + snprintf(coordinateString, sizeOfCoordinateString, "%02d %02d %02d", longitudeDegrees, longitudeMinutes, + (int)round(coordinate)); + else if (coordinateInputType == COORDINATE_INPUT_TYPE_DD_MM_SS_DASH_NO_DECIMAL) + snprintf(coordinateString, sizeOfCoordinateString, "%02d-%02d-%02d", longitudeDegrees, longitudeMinutes, + (int)round(coordinate)); + } + else + { + log_d("Unknown coordinate input type"); } - } } - -//Set the port of the 1:4 dual channel analog mux -//This allows NMEA, I2C, PPS/Event, and ADC/DAC to be routed through data port via software select -void setMuxport(int channelNumber) +// Given an input type, return a printable string +const char *coordinatePrintableInputType(CoordinateInputType coordinateInputType) { - if (productVariant == RTK_EXPRESS) - { - pinMode(pin_muxA, OUTPUT); - pinMode(pin_muxB, OUTPUT); - - if (channelNumber > 3) return; //Error check - - switch (channelNumber) + switch (coordinateInputType) { - case 0: - digitalWrite(pin_muxA, LOW); - digitalWrite(pin_muxB, LOW); + default: + return ("Unknown"); + break; + case (COORDINATE_INPUT_TYPE_DD): + return ("DD.ddddddddd"); + break; + case (COORDINATE_INPUT_TYPE_DDMM): + return ("DDMM.mmmmmmm"); break; - case 1: - digitalWrite(pin_muxA, HIGH); - digitalWrite(pin_muxB, LOW); + case (COORDINATE_INPUT_TYPE_DD_MM): + return ("DD MM.mmmmmmm"); break; - case 2: - digitalWrite(pin_muxA, LOW); - digitalWrite(pin_muxB, HIGH); + case (COORDINATE_INPUT_TYPE_DD_MM_DASH): + return ("DD-MM.mmmmmmm"); break; - case 3: - digitalWrite(pin_muxA, HIGH); - digitalWrite(pin_muxB, HIGH); + case (COORDINATE_INPUT_TYPE_DD_MM_SYMBOL): + return ("DD°MM.mmmmmmm'"); + break; + case (COORDINATE_INPUT_TYPE_DDMMSS): + return ("DDMMSS.ssssss"); + break; + case (COORDINATE_INPUT_TYPE_DD_MM_SS): + return ("DD MM SS.ssssss"); + break; + case (COORDINATE_INPUT_TYPE_DD_MM_SS_DASH): + return ("DD-MM-SS.ssssss"); + break; + case (COORDINATE_INPUT_TYPE_DD_MM_SS_SYMBOL): + return ("DD°MM'SS.ssssss\""); + break; + case (COORDINATE_INPUT_TYPE_DDMMSS_NO_DECIMAL): + return ("DDMMSS"); + break; + case (COORDINATE_INPUT_TYPE_DD_MM_SS_NO_DECIMAL): + return ("DD MM SS"); + break; + case (COORDINATE_INPUT_TYPE_DD_MM_SS_DASH_NO_DECIMAL): + return ("DD-MM-SS"); break; } - } + return ("Unknown"); } -boolean SFE_UBLOX_GNSS_ADD::getModuleInfo(uint16_t maxWait) +// Print the error message every 15 seconds +void reportFatalError(const char *errorMsg) { - i2cGNSS.minfo.hwVersion[0] = 0; - i2cGNSS.minfo.swVersion[0] = 0; - for (int i = 0; i < 10; i++) - i2cGNSS.minfo.extension[i][0] = 0; - i2cGNSS.minfo.extensionNo = 0; - - // Let's create our custom packet - uint8_t customPayload[MAX_PAYLOAD_SIZE]; // This array holds the payload data bytes - - // The next line creates and initialises the packet information which wraps around the payload - ubxPacket customCfg = {0, 0, 0, 0, 0, customPayload, 0, 0, SFE_UBLOX_PACKET_VALIDITY_NOT_DEFINED, SFE_UBLOX_PACKET_VALIDITY_NOT_DEFINED}; - - customCfg.cls = UBX_CLASS_MON; // This is the message Class - customCfg.id = UBX_MON_VER; // This is the message ID - customCfg.len = 0; // Setting the len (length) to zero let's us poll the current settings - customCfg.startingSpot = 0; // Always set the startingSpot to zero (unless you really know what you are doing) - - // Now let's send the command. The module info is returned in customPayload - - if (sendCommand(&customCfg, maxWait) != SFE_UBLOX_STATUS_DATA_RECEIVED) - return (false); //If command send fails then bail - - // Now let's extract the module info from customPayload - - uint16_t position = 0; - for (int i = 0; i < 30; i++) - { - minfo.swVersion[i] = customPayload[position]; - position++; - } - for (int i = 0; i < 10; i++) - { - minfo.hwVersion[i] = customPayload[position]; - position++; - } - - while (customCfg.len >= position + 30) - { - for (int i = 0; i < 30; i++) - { - minfo.extension[minfo.extensionNo][i] = customPayload[position]; - position++; - } - minfo.extensionNo++; - if (minfo.extensionNo > 9) - break; - } - - return (true); //Success! -} - -//Create $GNTXT, type message complete with CRC -//https://www.nmea.org/Assets/20160520%20txt%20amendment.pdf -//Used for reporting a system reboot inside the log -void createNMEASentence(uint8_t sentenceNumber, uint8_t textID, char *nmeaMessage, char *textMessage) -{ - char nmeaTxt[82]; //Max NMEA sentence length is 82 - sprintf(nmeaTxt, "$GNTXT,01,%02d,%02d,%s*", sentenceNumber, textID, textMessage); - - //From: http://engineeringnotes.blogspot.com/2015/02/generate-crc-for-nmea-strings-arduino.html - byte CRC = 0; // XOR chars between '$' and '*' - for (byte x = 1 ; x < strlen(nmeaTxt) - 1; x++) - CRC = CRC ^ nmeaTxt[x]; - - sprintf(nmeaMessage, "%s%02X", nmeaTxt, CRC); + while (1) + { + systemPrint("HALTED: "); + systemPrint(errorMsg); + systemPrintln(); + sleep(15); + } } diff --git a/Firmware/RTK_Surveyor/Tasks.ino b/Firmware/RTK_Surveyor/Tasks.ino index fc55a0247..ae80edfad 100644 --- a/Firmware/RTK_Surveyor/Tasks.ino +++ b/Firmware/RTK_Surveyor/Tasks.ino @@ -1,148 +1,1767 @@ -//High frequency tasks made by createTask() -//And any low frequency tasks that are called by Ticker - -//If the phone has any new data (NTRIP RTCM, etc), read it in over Bluetooth and pass along to ZED -//Task for writing to the GNSS receiver -void F9PSerialWriteTask(void *e) -{ - while (true) - { -#ifdef COMPILE_BT - //Receive RTCM corrections or UBX config messages over bluetooth and pass along to ZED - while (SerialBT.available()) - { - taskYIELD(); - if (inTestMode == false) - { - //Pass bytes to GNSS receiver - auto s = SerialBT.readBytes(wBuffer, SERIAL_SIZE_RX); - serialGNSS.write(wBuffer, s); +/*------------------------------------------------------------------------------ +Tasks.ino + + This module implements the high frequency tasks made by xTaskCreate() and any + low frequency tasks that are called by Ticker. + + GNSS + | + v + .--------+--------. + | | + v v + SPI or I2C + | | + | | + '------->+<-------' + | + | gnssReadTask + | gpsMessageParserFirstByte + | ... + | processUart1Message + | + v + Ring Buffer + | + | handleGnssDataTask + | + v + .---------------+-------+-------+---------------+ + | | | | + | | | | + v v v v + Bluetooth PVT Client PVT Server SD Card + +------------------------------------------------------------------------------*/ + +//---------------------------------------- +// Macros +//---------------------------------------- + +#define WRAP_OFFSET(offset, increment, arraySize) \ + { \ + offset += increment; \ + if (offset >= arraySize) \ + offset -= arraySize; \ + } + +//---------------------------------------- +// Constants +//---------------------------------------- + +enum RingBufferConsumers +{ + RBC_BLUETOOTH = 0, + RBC_PVT_CLIENT, + RBC_PVT_SERVER, + RBC_SD_CARD, + RBC_PVT_UDP_SERVER, + // Insert new consumers here + RBC_MAX +}; + +const char *const ringBufferConsumer[] = { + "Bluetooth", "PVT Client", "PVT Server", "SD Card", "PVT UDP Server", +}; + +const int ringBufferConsumerEntries = sizeof(ringBufferConsumer) / sizeof(ringBufferConsumer[0]); + +//---------------------------------------- +// Locals +//---------------------------------------- + +volatile static RING_BUFFER_OFFSET dataHead; // Head advances as data comes in from GNSS's UART +volatile int32_t availableHandlerSpace; // settings.gnssHandlerBufferSize - usedSpace +volatile const char *slowConsumer; + +// Buffer the incoming Bluetooth stream so that it can be passed in bulk over I2C +uint8_t bluetoothOutgoingToZed[100]; +uint16_t bluetoothOutgoingToZedHead; +unsigned long lastZedI2CSend; // Timestamp of the last time we sent RTCM ZED over I2C + +// Ring buffer tails +static RING_BUFFER_OFFSET btRingBufferTail; // BT Tail advances as it is sent over BT +static RING_BUFFER_OFFSET sdRingBufferTail; // SD Tail advances as it is recorded to SD + +// Ring buffer offsets +static uint16_t rbOffsetHead; + +//---------------------------------------- +// Task routines +//---------------------------------------- + +// If the phone has any new data (NTRIP RTCM, etc), read it in over Bluetooth and pass along to ZED +// Scan for escape characters to enter config menu +void btReadTask(void *e) +{ + int rxBytes; + + while (true) + { + // Display an alive message + if (PERIODIC_DISPLAY(PD_TASK_BLUETOOTH_READ)) + { + PERIODIC_CLEAR(PD_TASK_BLUETOOTH_READ); + systemPrintln("btReadTask running"); + } + + // Receive RTCM corrections or UBX config messages over bluetooth and pass along to ZED + rxBytes = 0; + if (bluetoothGetState() == BT_CONNECTED) + { + while (btPrintEcho == false && bluetoothRxDataAvailable()) + { + // Check stream for command characters + byte incoming = bluetoothRead(); + rxBytes += 1; + + if (incoming == btEscapeCharacter) + { + // Ignore escape characters received within 2 seconds of serial traffic + // Allow escape characters received within first 2 seconds of power on + if (millis() - btLastByteReceived > btMinEscapeTime || millis() < btMinEscapeTime) + { + btEscapeCharsReceived++; + if (btEscapeCharsReceived == btMaxEscapeCharacters) + { + printEndpoint = PRINT_ENDPOINT_ALL; + systemPrintln("Echoing all serial to BT device"); + btPrintEcho = true; + + btEscapeCharsReceived = 0; + btLastByteReceived = millis(); + } + } + else + { + // Ignore this escape character, passing along to output + if (USE_I2C_GNSS) + { + // serialGNSS.write(incoming); + addToZedI2CBuffer(btEscapeCharacter); + } + else + theGNSS.pushRawData(&incoming, 1); + } + } + else // This is just a character in the stream, ignore + { + // Pass any escape characters that turned out to not be a complete escape sequence + while (btEscapeCharsReceived-- > 0) + { + if (USE_I2C_GNSS) + { + // serialGNSS.write(btEscapeCharacter); + addToZedI2CBuffer(btEscapeCharacter); + } + else + { + uint8_t escChar = btEscapeCharacter; + theGNSS.pushRawData(&escChar, 1); + } + } + + // Pass byte to GNSS receiver or to system + // TODO - control if this RTCM source should be listened to or not + if (USE_I2C_GNSS) + { + // UART RX can be corrupted by UART TX + // See issue: https://github.com/sparkfun/SparkFun_RTK_Firmware/issues/469 + // serialGNSS.write(incoming); + addToZedI2CBuffer(incoming); + } + else + theGNSS.pushRawData(&incoming, 1); + + btLastByteReceived = millis(); + btEscapeCharsReceived = 0; // Update timeout check for escape char and partial frame + + bluetoothIncomingRTCM = true; + + // Record the arrival of RTCM from the Bluetooth connection (a phone or tablet is providing the RTCM + // via NTRIP). This resets the RTCM timeout used on the L-Band. + rtcmLastPacketReceived = millis(); + + } // End just a character in the stream + + } // End btPrintEcho == false && bluetoothRxDataAvailable() + + if (PERIODIC_DISPLAY(PD_BLUETOOTH_DATA_RX)) + { + PERIODIC_CLEAR(PD_BLUETOOTH_DATA_RX); + systemPrintf("Bluetooth received %d bytes\r\n", rxBytes); + } + } // End bluetoothGetState() == BT_CONNECTED + + if (bluetoothOutgoingToZedHead > 0 && ((millis() - lastZedI2CSend) > 100)) + { + sendZedI2CBuffer(); // Send any outstanding RTCM + } if (settings.enableTaskReports == true) - Serial.printf("SerialWriteTask High watermark: %d\n\r", uxTaskGetStackHighWaterMark(NULL)); - } - else - { - char incoming = SerialBT.read(); - Serial.printf("I heard: %c\n", incoming); - incomingBTTest = incoming; //Displayed during system test - } + systemPrintf("SerialWriteTask High watermark: %d\r\n", uxTaskGetStackHighWaterMark(nullptr)); + + feedWdt(); + taskYIELD(); + } // End while(true) +} + +// Add byte to buffer that will be sent to ZED +// We cannot write single characters to the ZED over I2C (as this will change the address pointer) +void addToZedI2CBuffer(uint8_t incoming) +{ + bluetoothOutgoingToZed[bluetoothOutgoingToZedHead] = incoming; + + bluetoothOutgoingToZedHead++; + if (bluetoothOutgoingToZedHead == sizeof(bluetoothOutgoingToZed)) + { + sendZedI2CBuffer(); + } +} + +// Push the buffered data in bulk to the GNSS over I2C +bool sendZedI2CBuffer() +{ + bool response = theGNSS.pushRawData(bluetoothOutgoingToZed, bluetoothOutgoingToZedHead); + + if (response == true) + { + if (PERIODIC_DISPLAY(PD_ZED_DATA_TX)) + { + PERIODIC_CLEAR(PD_ZED_DATA_TX); + systemPrintf("ZED TX: Sending %d bytes from I2C\r\n", bluetoothOutgoingToZedHead); + } + // log_d("Pushed %d bytes RTCM to ZED", bluetoothOutgoingToZedHead); } -#endif - taskYIELD(); - } + // No matter the response, wrap the head and reset the timer + bluetoothOutgoingToZedHead = 0; + lastZedI2CSend = millis(); + return (response); +} + +// Normally a delay(1) will feed the WDT but if we don't want to wait that long, this feeds the WDT without delay +void feedWdt() +{ + vTaskDelay(1); } -//If the ZED has any new NMEA data, pass it out over Bluetooth -//Task for reading data from the GNSS receiver. -void F9PSerialReadTask(void *e) +//---------------------------------------------------------------------- +// The ESP32<->ZED-F9P serial connection is default 230,400bps to facilitate +// 10Hz fix rate with PPP Logging Defaults (NMEAx5 + RXMx2) messages enabled. +// ESP32 UART2 is begun with settings.uartReceiveBufferSize size buffer. The circular buffer +// is 1024*6. At approximately 46.1K characters/second, a 6144 * 2 +// byte buffer should hold 267ms worth of serial data. Assuming SD writes are +// 250ms worst case, we should record incoming all data. Bluetooth congestion +// or conflicts with the SD card semaphore should clear within this time. +// +// Ring buffer empty when all the tails == dataHead +// +// +---------+ +// | | +// | | +// | | +// | | +// +---------+ <-- dataHead, btRingBufferTail, sdRingBufferTail, etc. +// +// Ring buffer contains data when any tail != dataHead +// +// +---------+ +// | | +// | | +// | yyyyyyy | <-- dataHead +// | xxxxxxx | <-- btRingBufferTail (1 byte in buffer) +// +---------+ <-- sdRingBufferTail (2 bytes in buffer) +// +// +---------+ +// | yyyyyyy | <-- btRingBufferTail (1 byte in buffer) +// | xxxxxxx | <-- sdRingBufferTail (2 bytes in buffer) +// | | +// | | +// +---------+ <-- dataHead +// +// Maximum ring buffer fill is settings.gnssHandlerBufferSize - 1 +//---------------------------------------------------------------------- + +// Read bytes from ZED-F9P UART1 into ESP32 circular buffer +// If data is coming in at 230,400bps = 23,040 bytes/s = one byte every 0.043ms +// If SD blocks for 150ms (not extraordinary) that is 3,488 bytes that must be buffered +// The ESP32 Arduino FIFO is ~120 bytes by default but overridden to 50 bytes (see pinUART2Task() and +// uart_set_rx_full_threshold()). We use this task to harvest from FIFO into circular buffer during SD write blocking +// time. +void gnssReadTask(void *e) { - while (true) - { - while (serialGNSS.available()) + static PARSE_STATE parse = {gpsMessageParserFirstByte, processUart1Message, "Log"}; + + uint8_t incomingData = 0; + + while (true) { - auto s = serialGNSS.readBytes(rBuffer, SERIAL_SIZE_RX); + // Display an alive message + if (PERIODIC_DISPLAY(PD_TASK_GNSS_READ)) + { + PERIODIC_CLEAR(PD_TASK_GNSS_READ); + systemPrintln("gnssReadTask running"); + } + + if (settings.enableTaskReports == true) + systemPrintf("SerialReadTask High watermark: %d\r\n", uxTaskGetStackHighWaterMark(nullptr)); - //If we are actively survey-in then do not pass NMEA data from ZED to phone - if (systemState == STATE_BASE_TEMP_SETTLE || systemState == STATE_BASE_TEMP_SURVEY_STARTED) - { - //Do nothing + // Determine if serial data is available + if (USE_I2C_GNSS) + { + while (serialGNSS.available()) + { + // Read the data from UART1 + uint8_t incomingData[500]; + int bytesIncoming = serialGNSS.read(incomingData, sizeof(incomingData)); + + for (int x = 0; x < bytesIncoming; x++) + { + // Save the data byte + parse.buffer[parse.length++] = incomingData[x]; + parse.length %= PARSE_BUFFER_LENGTH; + + // Compute the CRC value for the message + if (parse.computeCrc) + parse.crc = COMPUTE_CRC24Q(&parse, incomingData[x]); + + // Update the parser state based on the incoming byte + parse.state(&parse, incomingData[x]); + } + } + } + else // SPI GNSS + { + theGNSS.checkUblox(); // Check for new data + while (theGNSS.fileBufferAvailable() > 0) + { + // Read the data from the logging buffer + theGNSS.extractFileBufferData(&incomingData, + 1); // TODO: make this more efficient by reading multiple bytes? + + // Save the data byte + parse.buffer[parse.length++] = incomingData; + parse.length %= PARSE_BUFFER_LENGTH; + + // Compute the CRC value for the message + if (parse.computeCrc) + parse.crc = COMPUTE_CRC24Q(&parse, incomingData); + + // Update the parser state based on the incoming byte + parse.state(&parse, incomingData); + } + } + + feedWdt(); taskYIELD(); - } -#ifdef COMPILE_BT - else if (SerialBT.connected()) - { - if (SerialBT.isCongested() == false) + } +} + +// Process a complete message incoming from parser +// If we get a complete NMEA/UBX/RTCM message, pass on to SD/BT/PVT interfaces +void processUart1Message(PARSE_STATE *parse, uint8_t type) +{ + int32_t bytesToCopy; + const char *consumer; + RING_BUFFER_OFFSET remainingBytes; + int32_t space; + int32_t use; + + // Display the message + if ((settings.enablePrintLogFileMessages || PERIODIC_DISPLAY(PD_ZED_DATA_RX)) && (!parse->crc) && (!inMainMenu)) + { + PERIODIC_CLEAR(PD_ZED_DATA_RX); + if (settings.enablePrintLogFileMessages) { - SerialBT.write(rBuffer, s); //Push new data to BT SPP + printTimeStamp(); + systemPrint(" "); } - else if (settings.throttleDuringSPPCongestion == false) + else + systemPrint("ZED RX: "); + switch (type) + { + case SENTENCE_TYPE_NMEA: + systemPrintf("%s NMEA %s, %2d bytes\r\n", parse->parserName, parse->nmeaMessageName, parse->length); + break; + + case SENTENCE_TYPE_RTCM: + systemPrintf("%s RTCM %d, %2d bytes\r\n", parse->parserName, parse->message, parse->length); + break; + + case SENTENCE_TYPE_UBX: + systemPrintf("%s UBX %d.%d, %2d bytes\r\n", parse->parserName, parse->message >> 8, parse->message & 0xff, + parse->length); + break; + } + } + + // Determine if this message will fit into the ring buffer + bytesToCopy = parse->length; + space = availableHandlerSpace; + use = settings.gnssHandlerBufferSize - space; + consumer = (char *)slowConsumer; + if ((bytesToCopy > space) && (!inMainMenu)) + { + int32_t bufferedData; + int32_t bytesToDiscard; + int32_t discardedBytes; + int32_t listEnd; + int32_t messageLength; + int32_t offsetBytes; + int32_t previousTail; + int32_t rbOffsetTail; + + // Determine the tail of the ring buffer + previousTail = dataHead + space + 1; + if (previousTail >= settings.gnssHandlerBufferSize) + previousTail -= settings.gnssHandlerBufferSize; + + /* The rbOffsetArray holds the offsets into the ring buffer of the + * start of each of the parsed messages. A head (rbOffsetHead) and + * tail (rbOffsetTail) offsets are used for this array to insert and + * remove entries. Typically this task only manipulates the head as + * new messages are placed into the ring buffer. The handleGnssDataTask + * normally manipulates the tail as data is removed from the buffer. + * However this task will manipulate the tail under two conditions: + * + * 1. The ring buffer gets full and data must be discarded + * + * 2. The rbOffsetArray is too small to hold all of the message + * offsets for the data in the ring buffer. The array is full + * when (Head + 1) == Tail + * + * Notes: + * The rbOffsetArray is allocated along with the ring buffer in + * Begin.ino + * + * The first entry rbOffsetArray[0] is initialized to zero (0) + * in Begin.ino + * + * The array always has one entry in it containing the head offset + * which contains a valid offset into the ringBuffer, handled below + * + * The empty condition is Tail == Head + * + * The amount of data described by the rbOffsetArray is + * rbOffsetArray[Head] - rbOffsetArray[Tail] + * + * rbOffsetArray ringBuffer + * .-----------------. .-----------------. + * | | | | + * +-----------------+ | | + * Tail --> | Msg 1 Offset |---------->+-----------------+ <-- Tail n + * +-----------------+ | Msg 1 | + * | Msg 2 Offset |--------. | | + * +-----------------+ | | | + * | Msg 3 Offset |------. '->+-----------------+ + * +-----------------+ | | Msg 2 | + * Head --> | Head Offset |--. | | | + * +-----------------+ | | | | + * | | | | | | + * +-----------------+ | | | | + * | | | '--->+-----------------+ + * +-----------------+ | | Msg 3 | + * | | | | | + * +-----------------+ '------->+-----------------+ <-- dataHead + * | | | | + */ + + // Determine the index for the end of the circular ring buffer + // offset list + listEnd = rbOffsetHead; + WRAP_OFFSET(listEnd, 1, rbOffsetEntries); + + // Update the tail, walk newest message to oldest message + rbOffsetTail = rbOffsetHead; + bufferedData = 0; + messageLength = 0; + while ((rbOffsetTail != listEnd) && (bufferedData < use)) + { + // Determine the amount of data in the ring buffer up until + // either the tail or the end of the rbOffsetArray + // + // | | + // | | Valid, still in ring buffer + // | Newest | + // +-----------+ <-- rbOffsetHead + // | | + // | | free space + // | | + // rbOffsetTail --> +-----------+ <-- bufferedData + // | ring | + // | buffer | <-- used + // | data | + // +-----------+ Valid, still in ring buffer + // | | + // + messageLength = rbOffsetArray[rbOffsetTail]; + WRAP_OFFSET(rbOffsetTail, rbOffsetEntries - 1, rbOffsetEntries); + messageLength -= rbOffsetArray[rbOffsetTail]; + if (messageLength < 0) + messageLength += settings.gnssHandlerBufferSize; + bufferedData += messageLength; + } + + // Account for any data in the ring buffer not described by the array + // + // | | + // +-----------+ + // | Oldest | + // | | + // | ring | + // | buffer | <-- used + // | data | + // +-----------+ Valid, still in ring buffer + // | | + // rbOffsetTail --> +-----------+ <-- bufferedData + // | | + // | Newest | + // +-----------+ <-- rbOffsetHead + // | | + // + discardedBytes = 0; + if (bufferedData < use) + discardedBytes = use - bufferedData; + + // Writing to the SD card, the network or Bluetooth, a partial + // message may be written leaving the tail pointer mid-message + // + // | | + // rbOffsetTail --> +-----------+ + // | Oldest | + // | | + // | ring | + // | buffer | <-- used + // | data | Valid, still in ring buffer + // +-----------+ <-- + // | | + // +-----------+ + // | | + // | Newest | + // +-----------+ <-- rbOffsetHead + // | | + // + else if (bufferedData > use) { - SerialBT.write(rBuffer, s); //Push new data to SPP regardless of congestion + // Remove the remaining portion of the oldest entry in the array + discardedBytes = messageLength + use - bufferedData; + WRAP_OFFSET(rbOffsetTail, 1, rbOffsetEntries); } + + // rbOffsetTail now points to the beginning of a message in the + // ring buffer + // Determine the amount of data to discard + bytesToDiscard = discardedBytes; + if (bytesToDiscard < bytesToCopy) + bytesToDiscard = bytesToCopy; + if (bytesToDiscard < AMOUNT_OF_RING_BUFFER_DATA_TO_DISCARD) + bytesToDiscard = AMOUNT_OF_RING_BUFFER_DATA_TO_DISCARD; + + // Walk the ring buffer messages from oldest to newest + while ((discardedBytes < bytesToDiscard) && (rbOffsetTail != rbOffsetHead)) + { + // Determine the length of the oldest message + WRAP_OFFSET(rbOffsetTail, 1, rbOffsetEntries); + discardedBytes = rbOffsetArray[rbOffsetTail] - previousTail; + if (discardedBytes < 0) + discardedBytes += settings.gnssHandlerBufferSize; + } + + // Discard the oldest data from the ring buffer + if (consumer) + systemPrintf("Ring buffer full: discarding %d bytes, %s is slow\r\n", discardedBytes, consumer); else + systemPrintf("Ring buffer full: discarding %d bytes\r\n", discardedBytes); + updateRingBufferTails(previousTail, rbOffsetArray[rbOffsetTail]); + availableHandlerSpace += discardedBytes; + } + + // Add another message to the ring buffer + // Account for this message + availableHandlerSpace -= bytesToCopy; + + // Fill the buffer to the end and then start at the beginning + if ((dataHead + bytesToCopy) > settings.gnssHandlerBufferSize) + bytesToCopy = settings.gnssHandlerBufferSize - dataHead; + + // Display the dataHead offset + if (settings.enablePrintRingBufferOffsets && (!inMainMenu)) + systemPrintf("DH: %4d --> ", dataHead); + + // Copy the data into the ring buffer + memcpy(&ringBuffer[dataHead], parse->buffer, bytesToCopy); + dataHead += bytesToCopy; + if (dataHead >= settings.gnssHandlerBufferSize) + dataHead -= settings.gnssHandlerBufferSize; + + // Determine the remaining bytes + remainingBytes = parse->length - bytesToCopy; + if (remainingBytes) + { + // Copy the remaining bytes into the beginning of the ring buffer + memcpy(ringBuffer, &parse->buffer[bytesToCopy], remainingBytes); + dataHead += remainingBytes; + if (dataHead >= settings.gnssHandlerBufferSize) + dataHead -= settings.gnssHandlerBufferSize; + } + + // Add the head offset to the offset array + WRAP_OFFSET(rbOffsetHead, 1, rbOffsetEntries); + rbOffsetArray[rbOffsetHead] = dataHead; + + // Display the dataHead offset + if (settings.enablePrintRingBufferOffsets && (!inMainMenu)) + systemPrintf("%4d\r\n", dataHead); +} + +// Remove previous messages from the ring buffer +void updateRingBufferTails(RING_BUFFER_OFFSET previousTail, RING_BUFFER_OFFSET newTail) +{ + // Trim any long or medium tails + discardRingBufferBytes(&btRingBufferTail, previousTail, newTail); + discardRingBufferBytes(&sdRingBufferTail, previousTail, newTail); + discardPvtClientBytes(previousTail, newTail); + discardPvtServerBytes(previousTail, newTail); + discardPvtUdpServerBytes(previousTail, newTail); +} + +// Remove previous messages from the ring buffer +void discardRingBufferBytes(RING_BUFFER_OFFSET *tail, RING_BUFFER_OFFSET previousTail, RING_BUFFER_OFFSET newTail) +{ + // The longest tail is being trimmed. Medium length tails may contain + // some data within the region begin trimmed. The shortest tails will + // be trimmed. + // + // Devices that get their tails trimmed, may output a partial message + // prior to the buffer trimming. After the trimming, the tail of the + // ring buffer points to the beginning of a new message. + // + // previousTail newTail + // | | + // Before trimming v Discarded v After trimming + // ----+----------------- ... -----+-- .. ---+-----------+------ + // | Partial message | | | + // ----+----------------- ... -----+-- .. ---+-----------+------ + // ^ ^ ^ + // | | | + // long tail ----' '--- medium tail '-- short tail + // + // Determine if the trimmed data wraps the end of the buffer + if (previousTail < newTail) + { + // No buffer wrap occurred + // Only discard the data from long and medium tails + if ((*tail >= previousTail) && (*tail < newTail)) + *tail = newTail; + } + else + { + // Buffer wrap occurred + if ((*tail >= previousTail) || (*tail < newTail)) + *tail = newTail; + } +} + +// If new data is in the ringBuffer, dole it out to appropriate interface +// Send data out Bluetooth, record to SD, or send to network clients +// Each device (Bluetooth, SD and network client) gets its own tail. If the +// device is running too slowly then data for that device is dropped. +// The usedSpace variable tracks the total space in use in the buffer. +void handleGnssDataTask(void *e) +{ + int32_t bytesToSend; + bool connected; + uint32_t deltaMillis; + int32_t freeSpace; + uint16_t listEnd; + static uint32_t maxMillis[RBC_MAX]; + uint32_t startMillis; + int32_t usedSpace; + + // Initialize the tails + btRingBufferTail = 0; + pvtClientZeroTail(); + pvtServerZeroTail(); + pvtUdpServerZeroTail(); + sdRingBufferTail = 0; + + while (true) + { + // Display an alive message + if (PERIODIC_DISPLAY(PD_TASK_HANDLE_GNSS_DATA)) + { + PERIODIC_CLEAR(PD_TASK_HANDLE_GNSS_DATA); + systemPrintln("handleGnssDataTask running"); + } + + usedSpace = 0; + + //---------------------------------------------------------------------- + // Send data over Bluetooth + //---------------------------------------------------------------------- + + startMillis = millis(); + + // Determine BT connection state + bool connected = (bluetoothGetState() == BT_CONNECTED) && (systemState != STATE_BASE_TEMP_SETTLE) && + (systemState != STATE_BASE_TEMP_SURVEY_STARTED); + if (!connected) + // Discard the data + btRingBufferTail = dataHead; + else + { + // Determine the amount of Bluetooth data in the buffer + bytesToSend = dataHead - btRingBufferTail; + if (bytesToSend < 0) + bytesToSend += settings.gnssHandlerBufferSize; + if (bytesToSend > 0) + { + // Reduce bytes to send if we have more to send then the end of + // the buffer, we'll wrap next loop + if ((btRingBufferTail + bytesToSend) > settings.gnssHandlerBufferSize) + bytesToSend = settings.gnssHandlerBufferSize - btRingBufferTail; + + // If we are in the config menu, suppress data flowing from ZED to cell phone + if (btPrintEcho == false) + // Push new data to BT SPP + bytesToSend = bluetoothWrite(&ringBuffer[btRingBufferTail], bytesToSend); + + // Account for the data that was sent + if (bytesToSend > 0) + { + // If we are in base mode, assume part of the outgoing data is RTCM + if (systemState >= STATE_BASE_NOT_STARTED && systemState <= STATE_BASE_FIXED_TRANSMITTING) + bluetoothOutgoingRTCM = true; + + // Account for the sent or dropped data + btRingBufferTail += bytesToSend; + if (btRingBufferTail >= settings.gnssHandlerBufferSize) + btRingBufferTail -= settings.gnssHandlerBufferSize; + + // Remember the maximum transfer time + deltaMillis = millis() - startMillis; + if (maxMillis[RBC_BLUETOOTH] < deltaMillis) + maxMillis[RBC_BLUETOOTH] = deltaMillis; + + // Display the data movement + if (PERIODIC_DISPLAY(PD_BLUETOOTH_DATA_TX)) + { + PERIODIC_CLEAR(PD_BLUETOOTH_DATA_TX); + systemPrintf("Bluetooth: %d bytes written\r\n", bytesToSend); + } + } + else + log_w("BT failed to send"); + + // Determine the amount of data that remains in the buffer + bytesToSend = dataHead - btRingBufferTail; + if (bytesToSend < 0) + bytesToSend += settings.gnssHandlerBufferSize; + if (usedSpace < bytesToSend) + { + usedSpace = bytesToSend; + slowConsumer = "Bluetooth"; + } + } + } + + //---------------------------------------------------------------------- + // Send data to the network clients + //---------------------------------------------------------------------- + + startMillis = millis(); + + // Update space available for use in UART task + bytesToSend = pvtClientSendData(dataHead); + if (usedSpace < bytesToSend) { - //Don't push data to BT SPP if there is congestion to prevent heap hits. - log_d("Dropped SPP Bytes: %d", s); + usedSpace = bytesToSend; + slowConsumer = "PVT client"; } - } -#endif - if (settings.enableTaskReports == true) - Serial.printf("SerialReadTask High watermark: %d\n\r", uxTaskGetStackHighWaterMark(NULL)); + // Remember the maximum transfer time + deltaMillis = millis() - startMillis; + if (maxMillis[RBC_PVT_CLIENT] < deltaMillis) + maxMillis[RBC_PVT_CLIENT] = deltaMillis; + + startMillis = millis(); - //If user wants to log, record to SD - if (online.logging == true) - { - //Check if we are inside the max time window for logging - if ((systemTime_minutes - startLogTime_minutes) < settings.maxLogTime_minutes) + // Update space available for use in UART task + bytesToSend = pvtServerSendData(dataHead); + if (usedSpace < bytesToSend) { - //Attempt to write to file system. This avoids collisions with file writing from other functions like recordSystemSettingsToFile() - if (xSemaphoreTake(xFATSemaphore, fatSemaphore_shortWait_ms) == pdPASS) - { - ubxFile.write(rBuffer, s); + usedSpace = bytesToSend; + slowConsumer = "PVT server"; + } + + // Remember the maximum transfer time + deltaMillis = millis() - startMillis; + if (maxMillis[RBC_PVT_SERVER] < deltaMillis) + maxMillis[RBC_PVT_SERVER] = deltaMillis; + + startMillis = millis(); + + // Update space available for use in UART task + bytesToSend = pvtUdpServerSendData(dataHead); + if (usedSpace < bytesToSend) + { + usedSpace = bytesToSend; + slowConsumer = "PVT UDP server"; + } - //Force file sync every 5000ms - if (millis() - lastUBXLogSyncTime > 5000) + // Remember the maximum transfer time + deltaMillis = millis() - startMillis; + if (maxMillis[RBC_PVT_UDP_SERVER] < deltaMillis) + maxMillis[RBC_PVT_UDP_SERVER] = deltaMillis; + + //---------------------------------------------------------------------- + // Log data to the SD card + //---------------------------------------------------------------------- + + // Determine if the SD card is enabled for logging + connected = online.logging && ((systemTime_minutes - startLogTime_minutes) < settings.maxLogTime_minutes); + + // If user wants to log, record to SD + if (!connected) + // Discard the data + sdRingBufferTail = dataHead; + else + { + // Determine the amount of microSD card logging data in the buffer + bytesToSend = dataHead - sdRingBufferTail; + if (bytesToSend < 0) + bytesToSend += settings.gnssHandlerBufferSize; + if (bytesToSend > 0) { - if (productVariant == RTK_SURVEYOR) - digitalWrite(pin_baseStatusLED, !digitalRead(pin_baseStatusLED)); //Blink LED to indicate logging activity + // Attempt to gain access to the SD card, avoids collisions with file + // writing from other functions like recordSystemSettingsToFile() + if (xSemaphoreTake(sdCardSemaphore, loggingSemaphoreWait_ms) == pdPASS) + { + markSemaphore(FUNCTION_WRITESD); + + // Reduce bytes to record if we have more then the end of the buffer + if ((sdRingBufferTail + bytesToSend) > settings.gnssHandlerBufferSize) + bytesToSend = settings.gnssHandlerBufferSize - sdRingBufferTail; + + if (settings.enablePrintSDBuffers && (!inMainMenu)) + { + int bufferAvailable; + if (USE_I2C_GNSS) + bufferAvailable = serialGNSS.available(); + else + { + theGNSS.checkUblox(); + bufferAvailable = theGNSS.fileBufferAvailable(); + } + int availableUARTSpace; + if (USE_I2C_GNSS) + availableUARTSpace = settings.uartReceiveBufferSize - bufferAvailable; + else + // Use gnssHandlerBufferSize for now. TODO: work out if the SPI GNSS needs its own buffer + // size setting + availableUARTSpace = settings.gnssHandlerBufferSize - bufferAvailable; + systemPrintf("SD Incoming Serial: %04d\tToRead: %04d\tMovedToBuffer: %04d\tavailableUARTSpace: " + "%04d\tavailableHandlerSpace: %04d\tToRecord: %04d\tRecorded: %04d\tBO: %d\r\n", + bufferAvailable, 0, 0, availableUARTSpace, availableHandlerSpace, bytesToSend, 0, + bufferOverruns); + } + + // Write the data to the file + long startTime = millis(); + startMillis = millis(); + + bytesToSend = ubxFile->write(&ringBuffer[sdRingBufferTail], bytesToSend); + if (PERIODIC_DISPLAY(PD_SD_LOG_WRITE) && (bytesToSend > 0)) + { + PERIODIC_CLEAR(PD_SD_LOG_WRITE); + systemPrintf("SD %d bytes written to log file\r\n", bytesToSend); + } + + static unsigned long lastFlush = 0; + if (USE_MMC_MICROSD) + { + if (millis() > (lastFlush + 250)) // Flush every 250ms, not every write + { + ubxFile->flush(); + lastFlush += 250; + } + } + fileSize = ubxFile->fileSize(); // Update file size - long startWriteTime = micros(); - taskYIELD(); - ubxFile.sync(); - taskYIELD(); - long stopWriteTime = micros(); - totalWriteTime += stopWriteTime - startWriteTime; //Used to calculate overall write speed + sdFreeSpace -= bytesToSend; // Update remaining space on SD - if (productVariant == RTK_SURVEYOR) - digitalWrite(pin_baseStatusLED, !digitalRead(pin_baseStatusLED)); //Return LED to previous state + // Force file sync every 60s + if (millis() - lastUBXLogSyncTime > 60000) + { + if (productVariant == RTK_SURVEYOR) + digitalWrite(pin_baseStatusLED, + !digitalRead(pin_baseStatusLED)); // Blink LED to indicate logging activity - updateDataFileAccess(&ubxFile); // Update the file access time & date + ubxFile->sync(); + ubxFile->updateFileAccessTimestamp(); // Update the file access time & date - lastUBXLogSyncTime = millis(); + if (productVariant == RTK_SURVEYOR) + digitalWrite(pin_baseStatusLED, + !digitalRead(pin_baseStatusLED)); // Return LED to previous state + + lastUBXLogSyncTime = millis(); + } + + // Remember the maximum transfer time + deltaMillis = millis() - startMillis; + if (maxMillis[RBC_SD_CARD] < deltaMillis) + maxMillis[RBC_SD_CARD] = deltaMillis; + long endTime = millis(); + + if (settings.enablePrintBufferOverrun) + { + if (endTime - startTime > 150) + systemPrintf("Long Write! Time: %ld ms / Location: %ld / Recorded %d bytes / " + "spaceRemaining %d bytes\r\n", + endTime - startTime, fileSize, bytesToSend, combinedSpaceRemaining); + } + + xSemaphoreGive(sdCardSemaphore); + + // Account for the sent data or dropped + if (bytesToSend > 0) + { + sdRingBufferTail += bytesToSend; + if (sdRingBufferTail >= settings.gnssHandlerBufferSize) + sdRingBufferTail -= settings.gnssHandlerBufferSize; + } + } // End sdCardSemaphore + else + { + char semaphoreHolder[50]; + getSemaphoreFunction(semaphoreHolder); + log_w("sdCardSemaphore failed to yield for SD write, held by %s, Tasks.ino line %d", + semaphoreHolder, __LINE__); + + delay(1); // Needed to prevent WDT resets during long Record Settings locks + taskYIELD(); + } + + // Update space available for use in UART task + bytesToSend = dataHead - sdRingBufferTail; + if (bytesToSend < 0) + bytesToSend += settings.gnssHandlerBufferSize; + if (usedSpace < bytesToSend) + { + usedSpace = bytesToSend; + slowConsumer = "SD card"; + } + } // bytesToSend + } // End connected + + //---------------------------------------------------------------------- + // Update the available space in the ring buffer + //---------------------------------------------------------------------- + + freeSpace = settings.gnssHandlerBufferSize - usedSpace; + + // Don't fill the last byte to prevent buffer overflow + if (freeSpace) + freeSpace -= 1; + availableHandlerSpace = freeSpace; + + //---------------------------------------------------------------------- + // Display the millisecond values for the different ring buffer consumers + //---------------------------------------------------------------------- + + if (PERIODIC_DISPLAY(PD_RING_BUFFER_MILLIS)) + { + int milliseconds; + int seconds; + + PERIODIC_CLEAR(PD_RING_BUFFER_MILLIS); + for (int index = 0; index < RBC_MAX; index++) + { + milliseconds = maxMillis[index]; + if (milliseconds > 1) + { + seconds = milliseconds / MILLISECONDS_IN_A_SECOND; + milliseconds %= MILLISECONDS_IN_A_SECOND; + systemPrintf("%s: %d:%03d Sec\r\n", ringBufferConsumer[index], seconds, milliseconds); + } + } + } + + //---------------------------------------------------------------------- + // Let other tasks run, prevent watch dog timer (WDT) resets + //---------------------------------------------------------------------- + + delay(1); + taskYIELD(); + } +} + +// Control BT status LED according to bluetoothGetState() +void updateBTled() +{ + if (productVariant == RTK_SURVEYOR) + { + // Blink on/off while we wait for BT connection + if (bluetoothGetState() == BT_NOTCONNECTED) + { + if (btFadeLevel == 0) + btFadeLevel = 255; + else + btFadeLevel = 0; + ledcWrite(ledBTChannel, btFadeLevel); + } + + // Solid LED if BT Connected + else if (bluetoothGetState() == BT_CONNECTED) + ledcWrite(ledBTChannel, 255); + + // Pulse LED while no BT and we wait for WiFi connection + else if (wifiState == WIFI_STATE_CONNECTING || wifiState == WIFI_STATE_CONNECTED) + { + // Fade in/out the BT LED during WiFi AP mode + btFadeLevel += pwmFadeAmount; + if (btFadeLevel <= 0 || btFadeLevel >= 255) + pwmFadeAmount *= -1; + + if (btFadeLevel > 255) + btFadeLevel = 255; + if (btFadeLevel < 0) + btFadeLevel = 0; + + ledcWrite(ledBTChannel, btFadeLevel); + } + else + ledcWrite(ledBTChannel, 0); + } +} + +// For RTK Express and RTK Facet, monitor momentary buttons +void ButtonCheckTask(void *e) +{ + uint8_t index; + + if (setupBtn != nullptr) + setupBtn->begin(); + if (powerBtn != nullptr) + powerBtn->begin(); + + while (true) + { + // Display an alive message + if (PERIODIC_DISPLAY(PD_TASK_BUTTON_CHECK)) + { + PERIODIC_CLEAR(PD_TASK_BUTTON_CHECK); + systemPrintln("ButtonCheckTask running"); + } + + /* RTK Surveyor + + .----------------------------. + | | + V | + .------------------. | + | Power On | | + '------------------' | + | | + | Setup button = 0 | + V | + .------------------. | + .------>| Rover Mode | | + | '------------------' | + | | | + | | Setup button = 1 | + | V | + | .------------------. | + '-------| Base Mode | | + Setup button = 0 '------------------' | + after long time | | | + | | Setup button = 0 | + Setup button = 0 | | after short time | + after short time | | (< 500 mSec) | + (< 500 mSec) | | | + STATE_ROVER_NOT_STARTED | | | + V V | + .------------------. .------------------. | + | Test Mode | | WiFi Config Mode |----------' + '------------------' '------------------' + + */ + + if (productVariant == RTK_SURVEYOR) + { + if (setupBtn && + (settings.disableSetupButton == false)) // Allow check of the setup button if not overridden by settings + { + setupBtn->read(); + + // When switch is set to '1' = BASE, pin will be shorted to ground + if (setupBtn->isPressed()) // Switch is set to base mode + { + if (buttonPreviousState == BUTTON_ROVER) + { + lastRockerSwitchChange = millis(); // Record for WiFi AP access + buttonPreviousState = BUTTON_BASE; + requestChangeState(STATE_BASE_NOT_STARTED); + } + } + else if (setupBtn->wasReleased()) // Switch is set to Rover + { + if (buttonPreviousState == BUTTON_BASE) + { + buttonPreviousState = BUTTON_ROVER; + + // If quick toggle is detected (less than 500ms), enter WiFi AP Config mode + if (millis() - lastRockerSwitchChange < 500) + { + if (systemState == STATE_ROVER_NOT_STARTED && + online.display == true) // Catch during Power On + requestChangeState(STATE_TEST); // If RTK Surveyor, with display attached, during Rover + // not started, then enter test mode + else + requestChangeState(STATE_WIFI_CONFIG_NOT_STARTED); + } + else + { + requestChangeState(STATE_ROVER_NOT_STARTED); + } + } + } + } + } + else if (productVariant == RTK_EXPRESS || + productVariant == RTK_EXPRESS_PLUS) // Express: Check both of the momentary switches + { + if (setupBtn != nullptr) + setupBtn->read(); + if (powerBtn != nullptr) + powerBtn->read(); + + if (systemState == STATE_SHUTDOWN) + { + // Ignore button presses while shutting down + } + else if (powerBtn != nullptr && powerBtn->pressedFor(shutDownButtonTime)) + { + forceSystemStateUpdate = true; + requestChangeState(STATE_SHUTDOWN); + + if (inMainMenu) + powerDown(true); // State machine is not updated while in menu system so go straight to power down + // as needed + } + else if ((setupBtn != nullptr && setupBtn->pressedFor(500)) && + (powerBtn != nullptr && powerBtn->pressedFor(500))) + { + forceSystemStateUpdate = true; + requestChangeState(STATE_TEST); + lastTestMenuChange = millis(); // Avoid exiting test menu for 1s + } + else if (setupBtn != nullptr && setupBtn->wasReleased()) + { + if (settings.disableSetupButton == + false) // Allow check of the setup button if not overridden by settings + { + switch (systemState) + { + // If we are in any running state, change to STATE_DISPLAY_SETUP + case STATE_ROVER_NOT_STARTED: + case STATE_ROVER_NO_FIX: + case STATE_ROVER_FIX: + case STATE_ROVER_RTK_FLOAT: + case STATE_ROVER_RTK_FIX: + case STATE_BASE_NOT_STARTED: + case STATE_BASE_TEMP_SETTLE: + case STATE_BASE_TEMP_SURVEY_STARTED: + case STATE_BASE_TEMP_TRANSMITTING: + case STATE_BASE_FIXED_NOT_STARTED: + case STATE_BASE_FIXED_TRANSMITTING: + case STATE_BUBBLE_LEVEL: + case STATE_WIFI_CONFIG_NOT_STARTED: + case STATE_WIFI_CONFIG: + case STATE_ESPNOW_PAIRING_NOT_STARTED: + case STATE_ESPNOW_PAIRING: + lastSystemState = + systemState; // Remember this state to return after we mark an event or ESP-Now pair + requestChangeState(STATE_DISPLAY_SETUP); + setupState = STATE_MARK_EVENT; + lastSetupMenuChange = millis(); + break; + + case STATE_MARK_EVENT: + // If the user presses the setup button during a mark event, do nothing + // Allow system to return to lastSystemState + break; + + case STATE_PROFILE: + // If the user presses the setup button during a profile change, do nothing + // Allow system to return to lastSystemState + break; + + case STATE_TEST: + // Do nothing. User is releasing the setup button. + break; + + case STATE_TESTING: + // If we are in testing, return to Rover Not Started + requestChangeState(STATE_ROVER_NOT_STARTED); + break; + + case STATE_DISPLAY_SETUP: + // If we are displaying the setup menu, cycle through possible system states + // Exit display setup and enter new system state after ~1500ms in updateSystemState() + lastSetupMenuChange = millis(); + + forceDisplayUpdate = true; // User is interacting so repaint display quickly + + switch (setupState) + { + case STATE_MARK_EVENT: + setupState = STATE_ROVER_NOT_STARTED; + break; + case STATE_ROVER_NOT_STARTED: + // If F9R, skip base state + if (zedModuleType == PLATFORM_F9R) + { + // If accel offline, skip bubble + if (online.accelerometer == true) + setupState = STATE_BUBBLE_LEVEL; + else + setupState = STATE_WIFI_CONFIG_NOT_STARTED; + } + else + setupState = STATE_BASE_NOT_STARTED; + break; + case STATE_BASE_NOT_STARTED: + // If accel offline, skip bubble + if (online.accelerometer == true) + setupState = STATE_BUBBLE_LEVEL; + else + setupState = STATE_WIFI_CONFIG_NOT_STARTED; + break; + case STATE_BUBBLE_LEVEL: + setupState = STATE_WIFI_CONFIG_NOT_STARTED; + break; + case STATE_WIFI_CONFIG_NOT_STARTED: + setupState = STATE_ESPNOW_PAIRING_NOT_STARTED; + break; + case STATE_ESPNOW_PAIRING_NOT_STARTED: + // If only one active profile do not show any profiles + index = getProfileNumberFromUnit(0); + displayProfile = getProfileNumberFromUnit(1); + setupState = (index >= displayProfile) ? STATE_MARK_EVENT : STATE_PROFILE; + displayProfile = 0; + break; + case STATE_PROFILE: + // Done when no more active profiles + displayProfile++; + if (!getProfileNumberFromUnit(displayProfile)) + setupState = STATE_MARK_EVENT; + break; + default: + systemPrintf("ButtonCheckTask unknown setup state: %d\r\n", setupState); + setupState = STATE_MARK_EVENT; + break; + } + break; + + default: + systemPrintf("ButtonCheckTask unknown system state: %d\r\n", systemState); + requestChangeState(STATE_ROVER_NOT_STARTED); + break; + } + } // End disabdisableSetupButton check + } + } // End Platform = RTK Express + else if (productVariant == RTK_FACET || productVariant == RTK_FACET_LBAND || + productVariant == RTK_FACET_LBAND_DIRECT) // Check one momentary button + { + if (powerBtn != nullptr) + powerBtn->read(); + + if (systemState == STATE_SHUTDOWN) + { + // Ignore button presses while shutting down + } + else if (powerBtn != nullptr && powerBtn->pressedFor(shutDownButtonTime)) + { + forceSystemStateUpdate = true; + requestChangeState(STATE_SHUTDOWN); + + if (inMainMenu) + powerDown(true); // State machine is not updated while in menu system so go straight to power down + // as needed + } + else if (powerBtn != nullptr && + (systemState == STATE_ROVER_NOT_STARTED || systemState == STATE_KEYS_STARTED) && + firstRoverStart == true && powerBtn->pressedFor(500)) + { + forceSystemStateUpdate = true; + requestChangeState(STATE_TEST); + lastTestMenuChange = millis(); // Avoid exiting test menu for 1s + } + else if (powerBtn != nullptr && powerBtn->wasReleased() && firstRoverStart == false) + { + if (settings.disableSetupButton == + false) // Allow check of the setup button if not overridden by settings + { + switch (systemState) + { + // If we are in any running state, change to STATE_DISPLAY_SETUP + case STATE_ROVER_NOT_STARTED: + case STATE_ROVER_NO_FIX: + case STATE_ROVER_FIX: + case STATE_ROVER_RTK_FLOAT: + case STATE_ROVER_RTK_FIX: + case STATE_BASE_NOT_STARTED: + case STATE_BASE_TEMP_SETTLE: + case STATE_BASE_TEMP_SURVEY_STARTED: + case STATE_BASE_TEMP_TRANSMITTING: + case STATE_BASE_FIXED_NOT_STARTED: + case STATE_BASE_FIXED_TRANSMITTING: + case STATE_BUBBLE_LEVEL: + case STATE_WIFI_CONFIG_NOT_STARTED: + case STATE_WIFI_CONFIG: + case STATE_ESPNOW_PAIRING_NOT_STARTED: + case STATE_ESPNOW_PAIRING: + lastSystemState = + systemState; // Remember this state to return after we mark an event or ESP-Now pair + requestChangeState(STATE_DISPLAY_SETUP); + setupState = STATE_MARK_EVENT; + lastSetupMenuChange = millis(); + break; + + case STATE_MARK_EVENT: + // If the user presses the setup button during a mark event, do nothing + // Allow system to return to lastSystemState + break; + + case STATE_PROFILE: + // If the user presses the setup button during a profile change, do nothing + // Allow system to return to lastSystemState + break; + + case STATE_TEST: + // Do nothing. User is releasing the setup button. + break; + + case STATE_TESTING: + // If we are in testing, return to Rover Not Started + requestChangeState(STATE_ROVER_NOT_STARTED); + break; + + case STATE_DISPLAY_SETUP: + // If we are displaying the setup menu, cycle through possible system states + // Exit display setup and enter new system state after ~1500ms in updateSystemState() + lastSetupMenuChange = millis(); + + forceDisplayUpdate = true; // User is interacting so repaint display quickly + + switch (setupState) + { + case STATE_MARK_EVENT: + setupState = STATE_ROVER_NOT_STARTED; + break; + case STATE_ROVER_NOT_STARTED: + // If F9R, skip base state + if (zedModuleType == PLATFORM_F9R) + { + // If accel offline, skip bubble + if (online.accelerometer == true) + setupState = STATE_BUBBLE_LEVEL; + else + setupState = STATE_WIFI_CONFIG_NOT_STARTED; + } + else + setupState = STATE_BASE_NOT_STARTED; + break; + case STATE_BASE_NOT_STARTED: + // If accel offline, skip bubble + if (online.accelerometer == true) + setupState = STATE_BUBBLE_LEVEL; + else + setupState = STATE_WIFI_CONFIG_NOT_STARTED; + break; + case STATE_BUBBLE_LEVEL: + setupState = STATE_WIFI_CONFIG_NOT_STARTED; + break; + case STATE_WIFI_CONFIG_NOT_STARTED: + if (productVariant == RTK_FACET_LBAND || productVariant == RTK_FACET_LBAND_DIRECT) + { + lBandForceGetKeys = true; + setupState = STATE_KEYS_NEEDED; + } + else + setupState = STATE_ESPNOW_PAIRING_NOT_STARTED; + break; + + case STATE_KEYS_NEEDED: + lBandForceGetKeys = false; // User has scrolled past the GetKeys option + setupState = STATE_ESPNOW_PAIRING_NOT_STARTED; + break; + + case STATE_ESPNOW_PAIRING_NOT_STARTED: + // If only one active profile do not show any profiles + index = getProfileNumberFromUnit(0); + displayProfile = getProfileNumberFromUnit(1); + setupState = (index >= displayProfile) ? STATE_MARK_EVENT : STATE_PROFILE; + displayProfile = 0; + break; + case STATE_PROFILE: + // Done when no more active profiles + displayProfile++; + if (!getProfileNumberFromUnit(displayProfile)) + setupState = STATE_MARK_EVENT; + break; + default: + systemPrintf("ButtonCheckTask unknown setup state: %d\r\n", setupState); + setupState = STATE_MARK_EVENT; + break; + } + break; + + default: + systemPrintf("ButtonCheckTask unknown system state: %d\r\n", systemState); + requestChangeState(STATE_ROVER_NOT_STARTED); + break; + } + } // End disableSetupButton check + } + } // End Platform = RTK Facet + else if (productVariant == REFERENCE_STATION) // Check one momentary button + { + if (setupBtn != nullptr) + setupBtn->read(); + + if (systemState == STATE_SHUTDOWN) + { + // Ignore button presses while shutting down + } + else if (setupBtn != nullptr && setupBtn->pressedFor(shutDownButtonTime)) + { + forceSystemStateUpdate = true; + requestChangeState(STATE_SHUTDOWN); + + if (inMainMenu) + powerDown(true); // State machine is not updated while in menu system so go straight to power down + // as needed + } + else if (setupBtn != nullptr && systemState == STATE_BASE_NOT_STARTED && firstRoverStart == true && + setupBtn->pressedFor(500)) + { + forceSystemStateUpdate = true; + requestChangeState(STATE_TEST); + lastTestMenuChange = millis(); // Avoid exiting test menu for 1s } + else if (setupBtn != nullptr && setupBtn->wasReleased() && firstRoverStart == false) + { + if (settings.disableSetupButton == + false) // Allow check of the setup button if not overridden by settings + { + + switch (systemState) + { + // If we are in any running state, change to STATE_DISPLAY_SETUP + case STATE_BASE_NOT_STARTED: + case STATE_BASE_TEMP_SETTLE: + case STATE_BASE_TEMP_SURVEY_STARTED: + case STATE_BASE_TEMP_TRANSMITTING: + case STATE_BASE_FIXED_NOT_STARTED: + case STATE_BASE_FIXED_TRANSMITTING: + case STATE_ROVER_NOT_STARTED: + case STATE_ROVER_NO_FIX: + case STATE_ROVER_FIX: + case STATE_ROVER_RTK_FLOAT: + case STATE_ROVER_RTK_FIX: + case STATE_NTPSERVER_NOT_STARTED: + case STATE_NTPSERVER_NO_SYNC: + case STATE_NTPSERVER_SYNC: + case STATE_WIFI_CONFIG_NOT_STARTED: + case STATE_WIFI_CONFIG: + case STATE_CONFIG_VIA_ETH_NOT_STARTED: + case STATE_ESPNOW_PAIRING_NOT_STARTED: + case STATE_ESPNOW_PAIRING: + lastSystemState = systemState; // Remember this state to return after ESP-Now pair + requestChangeState(STATE_DISPLAY_SETUP); + setupState = STATE_BASE_NOT_STARTED; + lastSetupMenuChange = millis(); + break; + + case STATE_CONFIG_VIA_ETH_STARTED: + case STATE_CONFIG_VIA_ETH: + // If the user presses the button during configure-via-ethernet, then do a complete restart into + // Base mode + requestChangeState(STATE_CONFIG_VIA_ETH_RESTART_BASE); + break; + + case STATE_PROFILE: + // If the user presses the setup button during a profile change, do nothing + // Allow system to return to lastSystemState + break; + + case STATE_TEST: + // Do nothing. User is releasing the setup button. + break; - xSemaphoreGive(xFATSemaphore); - } //End xFATSemaphore - else - { - log_d("F9SerialRead: Semaphore failed to yield"); - } - } //End maxLogTime - } //End logging + case STATE_TESTING: + // If we are in testing, return to Base Not Started + requestChangeState(STATE_BASE_NOT_STARTED); + break; - taskYIELD(); + case STATE_DISPLAY_SETUP: + // If we are displaying the setup menu, cycle through possible system states + // Exit display setup and enter new system state after ~1500ms in updateSystemState() + lastSetupMenuChange = millis(); - } //End Serial.available() + forceDisplayUpdate = true; // User is interacting so repaint display quickly - taskYIELD(); - } + switch (setupState) + { + case STATE_BASE_NOT_STARTED: + setupState = STATE_ROVER_NOT_STARTED; + break; + case STATE_ROVER_NOT_STARTED: + setupState = STATE_NTPSERVER_NOT_STARTED; + break; + case STATE_NTPSERVER_NOT_STARTED: + setupState = STATE_CONFIG_VIA_ETH_NOT_STARTED; + break; + case STATE_CONFIG_VIA_ETH_NOT_STARTED: + setupState = STATE_WIFI_CONFIG_NOT_STARTED; + break; + case STATE_WIFI_CONFIG_NOT_STARTED: + setupState = STATE_ESPNOW_PAIRING_NOT_STARTED; + break; + case STATE_ESPNOW_PAIRING_NOT_STARTED: + // If only one active profile do not show any profiles + index = getProfileNumberFromUnit(0); + displayProfile = getProfileNumberFromUnit(1); + setupState = (index >= displayProfile) ? STATE_BASE_NOT_STARTED : STATE_PROFILE; + displayProfile = 0; + break; + case STATE_PROFILE: + // Done when no more active profiles + displayProfile++; + if (!getProfileNumberFromUnit(displayProfile)) + setupState = STATE_BASE_NOT_STARTED; + break; + case STATE_MARK_EVENT: // Skip the warning message if setupState is still in the default Mark + // Event state + setupState = STATE_BASE_NOT_STARTED; + break; + default: + systemPrintf("ButtonCheckTask unknown setup state: %d\r\n", setupState); + setupState = STATE_BASE_NOT_STARTED; + break; + } + break; + + default: + systemPrintf("ButtonCheckTask unknown system state: %d\r\n", systemState); + requestChangeState(STATE_BASE_NOT_STARTED); + break; + } + } // End disableSetupButton check + } + } // End Platform = REFERENCE_STATION + + delay(1); // Poor man's way of feeding WDT. Required to prevent Priority 1 tasks from causing WDT reset + taskYIELD(); + } } -//Assign UART2 interrupts to the current core. See: https://github.com/espressif/arduino-esp32/issues/3386 -void startUART2Task( void *pvParameters ) +void idleTask(void *e) { - serialGNSS.begin(settings.dataPortBaud); //UART2 on pins 16/17 for SPP. The ZED-F9P will be configured to output NMEA over its UART1 at the same rate. - serialGNSS.setRxBufferSize(SERIAL_SIZE_RX); - serialGNSS.setTimeout(50); + int cpu = xPortGetCoreID(); + uint32_t idleCount = 0; + uint32_t lastDisplayIdleTime = 0; + uint32_t lastStackPrintTime = 0; - uart2Started = true; + while (1) + { + // Increment a count during the idle time + idleCount++; + + // Determine if it is time to print the CPU idle times + if ((millis() - lastDisplayIdleTime) >= (IDLE_TIME_DISPLAY_SECONDS * 1000) && !inMainMenu) + { + lastDisplayIdleTime = millis(); + + // Get the idle time + if (idleCount > max_idle_count) + max_idle_count = idleCount; + + // Display the idle times + if (settings.enablePrintIdleTime) + { + systemPrintf("CPU %d idle time: %d%% (%d/%d)\r\n", cpu, idleCount * 100 / max_idle_count, idleCount, + max_idle_count); - vTaskDelete( NULL ); //Delete task once it has run once + // Print the task count + if (cpu) + systemPrintf("%d Tasks\r\n", uxTaskGetNumberOfTasks()); + } + + // Restart the idle count for the next display time + idleCount = 0; + } + + // Display the high water mark if requested + if ((settings.enableTaskReports == true) && + ((millis() - lastStackPrintTime) >= (IDLE_TIME_DISPLAY_SECONDS * 1000))) + { + lastStackPrintTime = millis(); + systemPrintf("idleTask %d High watermark: %d\r\n", xPortGetCoreID(), uxTaskGetStackHighWaterMark(nullptr)); + } + + // The idle task should NOT delay or yield + } } -//Control BT status LED according to bluetoothState -void updateBTled() +// Serial Read/Write tasks for the F9P must be started after BT is up and running otherwise SerialBT->available will +// cause reboot +bool tasksStartUART2() { - if (productVariant == RTK_SURVEYOR) - { - if (radioState == BT_ON_NOCONNECTION) - digitalWrite(pin_bluetoothStatusLED, !digitalRead(pin_bluetoothStatusLED)); - else if (radioState == BT_CONNECTED) - digitalWrite(pin_bluetoothStatusLED, HIGH); - else - digitalWrite(pin_bluetoothStatusLED, LOW); - } + // Verify that the ring buffer was successfully allocated + if (!ringBuffer) + { + systemPrintln("ERROR: Ring buffer allocation failure!"); + systemPrintln("Decrease GNSS handler (ring) buffer size"); + displayNoRingBuffer(5000); + return false; + } + + availableHandlerSpace = settings.gnssHandlerBufferSize; + + // Reads data from ZED and stores data into circular buffer + if (gnssReadTaskHandle == nullptr) + xTaskCreatePinnedToCore(gnssReadTask, // Function to call + "gnssRead", // Just for humans + gnssReadTaskStackSize, // Stack Size + nullptr, // Task input parameter + settings.gnssReadTaskPriority, // Priority + &gnssReadTaskHandle, // Task handle + settings.gnssReadTaskCore); // Core where task should run, 0=core, 1=Arduino + + // Reads data from circular buffer and sends data to SD, SPP, or network clients + if (handleGnssDataTaskHandle == nullptr) + xTaskCreatePinnedToCore(handleGnssDataTask, // Function to call + "handleGNSSData", // Just for humans + handleGnssDataTaskStackSize, // Stack Size + nullptr, // Task input parameter + settings.handleGnssDataTaskPriority, // Priority + &handleGnssDataTaskHandle, // Task handle + settings.handleGnssDataTaskCore); // Core where task should run, 0=core, 1=Arduino + + // Reads data from BT and sends to ZED + if (btReadTaskHandle == nullptr) + xTaskCreatePinnedToCore(btReadTask, // Function to call + "btRead", // Just for humans + btReadTaskStackSize, // Stack Size + nullptr, // Task input parameter + settings.btReadTaskPriority, // Priority + &btReadTaskHandle, // Task handle + settings.btReadTaskCore); // Core where task should run, 0=core, 1=Arduino + return true; +} + +// Stop tasks - useful when running firmware update or WiFi AP is running +void tasksStopUART2() +{ + // Delete tasks if running + if (gnssReadTaskHandle != nullptr) + { + vTaskDelete(gnssReadTaskHandle); + gnssReadTaskHandle = nullptr; + } + if (handleGnssDataTaskHandle != nullptr) + { + vTaskDelete(handleGnssDataTaskHandle); + handleGnssDataTaskHandle = nullptr; + } + if (btReadTaskHandle != nullptr) + { + vTaskDelete(btReadTaskHandle); + btReadTaskHandle = nullptr; + } + + // Give the other CPU time to finish + // Eliminates CPU bus hang condition + delay(100); +} + +// Checking the number of available clusters on the SD card can take multiple seconds +// Rather than blocking the system, we run a background task +// Once the size check is complete, the task is removed +void sdSizeCheckTask(void *e) +{ + while (true) + { + // Display an alive message + if (PERIODIC_DISPLAY(PD_TASK_SD_SIZE_CHECK)) + { + PERIODIC_CLEAR(PD_TASK_SD_SIZE_CHECK); + systemPrintln("sdSizeCheckTask running"); + } + + if (online.microSD && sdCardSize == 0) + { + // Attempt to gain access to the SD card + if (xSemaphoreTake(sdCardSemaphore, fatSemaphore_longWait_ms) == pdPASS) + { + markSemaphore(FUNCTION_SDSIZECHECK); + + if (USE_SPI_MICROSD) + { + csd_t csd; + sd->card()->readCSD(&csd); // Card Specific Data + sdCardSize = (uint64_t)512 * sd->card()->sectorCount(); + + sd->volumeBegin(); + + // Find available cluster/space + sdFreeSpace = sd->vol()->freeClusterCount(); // This takes a few seconds to complete + sdFreeSpace *= sd->vol()->sectorsPerCluster(); + sdFreeSpace *= 512L; // Bytes per sector + } +#ifdef COMPILE_SD_MMC + else + { + sdCardSize = SD_MMC.cardSize(); + sdFreeSpace = SD_MMC.totalBytes() - SD_MMC.usedBytes(); + } +#endif // COMPILE_SD_MMC + + xSemaphoreGive(sdCardSemaphore); + + // uint64_t sdUsedSpace = sdCardSize - sdFreeSpace; //Don't think of it as used, think of it as unusable + + String cardSize; + stringHumanReadableSize(cardSize, sdCardSize); + String freeSpace; + stringHumanReadableSize(freeSpace, sdFreeSpace); + systemPrintf("SD card size: %s / Free space: %s\r\n", cardSize, freeSpace); + + outOfSDSpace = false; + + sdSizeCheckTaskComplete = true; + } + else + { + char semaphoreHolder[50]; + getSemaphoreFunction(semaphoreHolder); + log_d("sdCardSemaphore failed to yield, held by %s, Tasks.ino line %d\r\n", semaphoreHolder, __LINE__); + } + } + + delay(1); + taskYIELD(); // Let other tasks run + } +} + +// Validate the task table lengths +void tasksValidateTables() +{ + if (ringBufferConsumerEntries != RBC_MAX) + reportFatalError("Fix ringBufferConsumer table to match RingBufferConsumers"); } diff --git a/Firmware/RTK_Surveyor/W5500.ino b/Firmware/RTK_Surveyor/W5500.ino new file mode 100644 index 000000000..092cdc327 --- /dev/null +++ b/Firmware/RTK_Surveyor/W5500.ino @@ -0,0 +1,160 @@ +#ifdef COMPILE_ETHERNET + +// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +// Extra code for the W5500 (vs. w5100.h) + +const uint16_t w5500RTR = 0x0019; // Retry Count Register - Common Register Block +const uint16_t w5500SIR = 0x0017; // Socket Interrupt Register - Common Register Block +const uint16_t w5500SIMR = 0x0018; // Socket Interrupt Mask Register - Common Register Block +const uint16_t w5500SnIR = 0x0002; // Socket n Interrupt Register - Socket Register Block +const uint16_t w5500SnIMR = 0x002C; // Socket n Interrupt Mask Register - Socket Register Block + +const uint8_t w5500CommonRegister = 0x00 << 3; // Block Select +const uint8_t w5500Socket0Register = 0x01 << 3; // Block Select +const uint8_t w5500Socket1Register = 0x05 << 3; // Block Select +const uint8_t w5500Socket2Register = 0x09 << 3; // Block Select +const uint8_t w5500Socket3Register = 0x0D << 3; // Block Select +const uint8_t w5500Socket4Register = 0x11 << 3; // Block Select +const uint8_t w5500Socket5Register = 0x15 << 3; // Block Select +const uint8_t w5500Socket6Register = 0x19 << 3; // Block Select +const uint8_t w5500Socket7Register = 0x1D << 3; // Block Select +const uint8_t w5500RegisterWrite = 0x01 << 2; // Read/Write bit +const uint8_t w5500VDM = 0x00 << 0; // Variable Data Length Mode +const uint8_t w5500FDM1 = 0x01 << 0; // Fixed Data Length Mode 1 Byte +const uint8_t w5500FDM2 = 0x02 << 0; // Fixed Data Length Mode 2 Byte +const uint8_t w5500FDM4 = 0x03 << 0; // Fixed Data Length Mode 4 Byte + +const uint8_t w5500SocketRegisters[] = {w5500Socket0Register, w5500Socket1Register, w5500Socket2Register, + w5500Socket3Register, w5500Socket4Register, w5500Socket5Register, + w5500Socket6Register, w5500Socket7Register}; + +const uint8_t w5500SIR_ClearAll = 0xFF; +const uint8_t w5500SIMR_EnableAll = 0xFF; + +const uint8_t w5500SnIR_ClearAll = 0xFF; + +const uint8_t w5500SnIMR_CON = 0x01 << 0; +const uint8_t w5500SnIMR_DISCON = 0x01 << 1; +const uint8_t w5500SnIMR_RECV = 0x01 << 2; +const uint8_t w5500SnIMR_TIMEOUT = 0x01 << 3; +const uint8_t w5500SnIMR_SENDOK = 0x01 << 4; + +void w5500write(SPIClass &spiPort, const int cs, uint16_t address, uint8_t control, uint8_t *data, uint8_t len) +{ + // Apply settings + spiPort.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0)); + + // Signal communication start + digitalWrite(cs, LOW); + + spiPort.transfer(address >> 8); // Address Phase + spiPort.transfer(address & 0xFF); + + spiPort.transfer(control | w5500RegisterWrite | w5500VDM); // Control Phase + + for (uint8_t i = 0; i < len; i++) + { + spiPort.transfer(*data++); // Data Phase + } + + // End communication + digitalWrite(cs, HIGH); + spiPort.endTransaction(); +} + +void w5500read(SPIClass &spiPort, const int cs, uint16_t address, uint8_t control, uint8_t *data, uint8_t len) +{ + // Apply settings + spiPort.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0)); + + // Signal communication start + digitalWrite(cs, LOW); + + spiPort.transfer(address >> 8); // Address Phase + spiPort.transfer(address & 0xFF); + + spiPort.transfer(control | w5500VDM); // Control Phase + + for (uint8_t i = 0; i < len; i++) + { + *data++ = spiPort.transfer(0x00); // Data Phase + } + + // End communication + digitalWrite(cs, HIGH); + spiPort.endTransaction(); +} + +void w5500ClearSocketInterrupts() +{ + // Clear all W5500 socket interrupts. + for (uint8_t i = 0; i < (sizeof(w5500SocketRegisters) / sizeof(uint8_t)); i++) + { + w5500write(SPI, pin_Ethernet_CS, w5500SnIR, w5500SocketRegisters[i], (uint8_t *)&w5500SnIR_ClearAll, 1); + } + w5500write(SPI, pin_Ethernet_CS, w5500SIR, w5500CommonRegister, (uint8_t *)&w5500SIR_ClearAll, 1); +} + +void w5500ClearSocketInterrupt(uint8_t sockIndex) +{ + // Clear the interrupt for sockIndex only + + // Sn_IR indicates the status of Socket Interrupt such as establishment, termination, + // receiving data, timeout). When an interrupt occurs and the corresponding bit of + // Sn_IMR is ‘1’, the corresponding bit of Sn_IR becomes ‘1’. + // In order to clear the Sn_IR bit, the host should write the bit to ‘1’. + w5500write(SPI, pin_Ethernet_CS, w5500SnIR, w5500SocketRegisters[sockIndex], (uint8_t *)&w5500SnIR_ClearAll, 1); + + // SIR indicates the interrupt status of Socket. Each bit of SIR be still ‘1’ until Sn_IR is + // cleared by the host. If Sn_IR is not equal to ‘0x00’, the n-th bit of SIR is ‘1’ and INTn + // PIN is asserted until SIR is ‘0x00’. + uint8_t SIR = 1 << sockIndex; + w5500write(SPI, pin_Ethernet_CS, w5500SIR, w5500CommonRegister, &SIR, 1); +} + +bool w5500CheckSocketInterrupt(uint8_t sockIndex) +{ + // Check the interrupt for sockIndex only + uint8_t S_INT = 1 << sockIndex; + uint8_t SIR; + w5500read(SPI, pin_Ethernet_CS, w5500SIR, w5500CommonRegister, &SIR, 1); + return ((S_INT & SIR) > 0); +} + +void w5500EnableSocketInterrupts() +{ + // Enable the RECV interrupt on all eight sockets + for (uint8_t i = 0; i < (sizeof(w5500SocketRegisters) / sizeof(uint8_t)); i++) + { + w5500write(SPI, pin_Ethernet_CS, w5500SnIMR, w5500SocketRegisters[i], (uint8_t *)&w5500SnIMR_RECV, 1); + } + + w5500write(SPI, pin_Ethernet_CS, w5500SIMR, w5500CommonRegister, (uint8_t *)&w5500SIMR_EnableAll, + 1); // Enable the socket interrupt on all eight sockets +} + +void w5500EnableSocketInterrupt(uint8_t sockIndex) +{ + w5500write(SPI, pin_Ethernet_CS, w5500SnIMR, w5500SocketRegisters[sockIndex], (uint8_t *)&w5500SnIMR_RECV, + 1); // Enable the RECV interrupt for sockIndex only + + // Read-Modify-Write + uint8_t SIMR; + w5500read(SPI, pin_Ethernet_CS, w5500SIMR, w5500CommonRegister, &SIMR, 1); + SIMR |= 1 << sockIndex; + w5500write(SPI, pin_Ethernet_CS, w5500SIMR, w5500CommonRegister, &SIMR, 1); // Enable the socket interrupt +} + +void w5500DisableSocketInterrupt(uint8_t sockIndex) +{ + w5500write(SPI, pin_Ethernet_CS, w5500SnIMR, w5500SocketRegisters[sockIndex], (uint8_t *)&w5500SnIMR_RECV, + 1); // Enable the RECV interrupt for sockIndex only + + // Read-Modify-Write + uint8_t SIMR; + w5500read(SPI, pin_Ethernet_CS, w5500SIMR, w5500CommonRegister, &SIMR, 1); + SIMR &= ~(1 << sockIndex); + w5500write(SPI, pin_Ethernet_CS, w5500SIMR, w5500CommonRegister, &SIMR, 1); // Disable the socket interrupt +} + +#endif // COMPILE_ETHERNET diff --git a/Firmware/RTK_Surveyor/WiFi.ino b/Firmware/RTK_Surveyor/WiFi.ino new file mode 100644 index 000000000..e9101f451 --- /dev/null +++ b/Firmware/RTK_Surveyor/WiFi.ino @@ -0,0 +1,681 @@ +/*=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + WiFi Status Values: + WL_CONNECTED: assigned when connected to a WiFi network + WL_CONNECTION_LOST: assigned when the connection is lost + WL_CONNECT_FAILED: assigned when the connection fails for all the attempts + WL_DISCONNECTED: assigned when disconnected from a network + WL_IDLE_STATUS: it is a temporary status assigned when WiFi.begin() is called and + remains active until the number of attempts expires (resulting in + WL_CONNECT_FAILED) or a connection is established (resulting in + WL_CONNECTED) + WL_NO_SHIELD: assigned when no WiFi shield is present + WL_NO_SSID_AVAIL: assigned when no SSID are available + WL_SCAN_COMPLETED: assigned when the scan networks is completed + + WiFi Station States: + + WIFI_STATE_OFF<-------------------. + | | + wifiStart() | | + | | WL_CONNECT_FAILED (Bad password) + | | WL_NO_SSID_AVAIL (Out of range) + v Fail | + WIFI_STATE_CONNECTING------------->+ + | ^ ^ + wifiConnect() | | | wifiShutdown() + | | WL_CONNECTION_LOST | + | | WL_DISCONNECTED | + v | | + WIFI_STATE_CONNECTED -------------' + =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=*/ + +//---------------------------------------- +// Globals +//---------------------------------------- + +int wifiConnectionAttempts; // Count the number of connection attempts between restarts + +#ifdef COMPILE_WIFI + +//---------------------------------------- +// Constants +//---------------------------------------- + +//---------------------------------------- +// Locals +//---------------------------------------- + +static uint32_t wifiLastConnectionAttempt; + +// Throttle the time between connection attempts +// ms - Max of 4,294,967,295 or 4.3M seconds or 71,000 minutes or 1193 hours or 49 days between attempts +static uint32_t wifiConnectionAttemptsTotal; // Count the number of connection attempts absolutely +static uint32_t wifiConnectionAttemptTimeout; + +// WiFi Timer usage: +// * Measure interval to display IP address +static unsigned long wifiDisplayTimer; + +// Last time the WiFi state was displayed +static uint32_t lastWifiState; + +// DNS server for Captive Portal +static DNSServer dnsServer; + +//---------------------------------------- +// WiFi Routines +//---------------------------------------- + +// Set WiFi credentials +// Enable TCP connections +void menuWiFi() +{ + bool restartWiFi = false; // Restart WiFi if user changes anything + + while (1) + { + systemPrintln(); + systemPrintln("Menu: WiFi Networks"); + + for (int x = 0; x < MAX_WIFI_NETWORKS; x++) + { + systemPrintf("%d) SSID %d: %s\r\n", (x * 2) + 1, x + 1, settings.wifiNetworks[x].ssid); + systemPrintf("%d) Password %d: %s\r\n", (x * 2) + 2, x + 1, settings.wifiNetworks[x].password); + } + + systemPrint("a) Configure device via WiFi Access Point or connect to WiFi: "); + systemPrintf("%s\r\n", settings.wifiConfigOverAP ? "AP" : "WiFi"); + + systemPrint("c) Captive Portal: "); + systemPrintf("%s\r\n", settings.enableCaptivePortal ? "Enabled" : "Disabled"); + + systemPrint("m) MDNS: "); + systemPrintf("%s\r\n", settings.mdnsEnable ? "Enabled" : "Disabled"); + + systemPrintln("x) Exit"); + + byte incoming = getCharacterNumber(); + + if (incoming >= 1 && incoming <= MAX_WIFI_NETWORKS * 2) + { + int arraySlot = ((incoming - 1) / 2); // Adjust incoming to array starting at 0 + + if (incoming % 2 == 1) + { + systemPrintf("Enter SSID network %d: ", arraySlot + 1); + getString(settings.wifiNetworks[arraySlot].ssid, sizeof(settings.wifiNetworks[arraySlot].ssid)); + restartWiFi = true; // If we are modifying the SSID table, force restart of WiFi + } + else + { + systemPrintf("Enter Password for %s: ", settings.wifiNetworks[arraySlot].ssid); + getString(settings.wifiNetworks[arraySlot].password, sizeof(settings.wifiNetworks[arraySlot].password)); + restartWiFi = true; // If we are modifying the SSID table, force restart of WiFi + } + } + else if (incoming == 'a') + { + settings.wifiConfigOverAP ^= 1; + restartWiFi = true; + } + else if (incoming == 'c') + { + settings.enableCaptivePortal ^= 1; + } + else if (incoming == 'm') + { + settings.mdnsEnable ^= 1; + } + else if (incoming == 'x') + break; + else if (incoming == INPUT_RESPONSE_GETCHARACTERNUMBER_EMPTY) + break; + else if (incoming == INPUT_RESPONSE_GETCHARACTERNUMBER_TIMEOUT) + break; + else + printUnknown(incoming); + } + + // Erase passwords from empty SSID entries + for (int x = 0; x < MAX_WIFI_NETWORKS; x++) + { + if (strlen(settings.wifiNetworks[x].ssid) == 0) + strcpy(settings.wifiNetworks[x].password, ""); + } + + // Restart WiFi if anything changes + if (restartWiFi == true) + { + // Restart the AP webserver if we are in that state + if (systemState == STATE_WIFI_CONFIG) + requestChangeState(STATE_WIFI_CONFIG_NOT_STARTED); + else + { + // Restart WiFi if we are not in AP config mode + if (wifiIsConnected()) + { + log_d("Menu caused restarting of WiFi"); + WIFI_STOP(); + wifiStart(); + wifiConnectionAttempts = 0; // Reset the timeout + } + } + } + + clearBuffer(); // Empty buffer of any newline chars +} + +// Display the WiFi IP address +void wifiDisplayIpAddress() +{ + systemPrint("WiFi IP address: "); + systemPrint(WiFi.localIP()); + systemPrintf(" RSSI: %d\r\n", WiFi.RSSI()); + + wifiDisplayTimer = millis(); +} + +// Get the WiFi adapter status +byte wifiGetStatus() +{ + return WiFi.status(); +} + +// Update the state of the WiFi state machine +void wifiSetState(byte newState) +{ + if ((settings.debugWifiState || PERIODIC_DISPLAY(PD_WIFI_STATE)) && (wifiState == newState)) + systemPrint("*"); + wifiState = newState; + + if (settings.debugWifiState || PERIODIC_DISPLAY(PD_WIFI_STATE)) + { + PERIODIC_CLEAR(PD_WIFI_STATE); + switch (newState) + { + default: + systemPrintf("Unknown WiFi state: %d\r\n", newState); + break; + case WIFI_STATE_OFF: + systemPrintln("WIFI_STATE_OFF"); + break; + case WIFI_STATE_START: + systemPrintln("WIFI_STATE_START"); + break; + case WIFI_STATE_CONNECTING: + systemPrintln("WIFI_STATE_CONNECTING"); + break; + case WIFI_STATE_CONNECTED: + systemPrintln("WIFI_STATE_CONNECTED"); + break; + } + } +} + +//---------------------------------------- +// WiFi Config Support Routines - compiled out +//---------------------------------------- + +// Start the access point for user to connect to and configure device +// We can also start as a WiFi station and attempt to connect to local WiFi for config +bool wifiStartAP() +{ + return(wifiStartAP(false)); //Don't force AP mode +} + +bool wifiStartAP(bool forceAP) +{ + if (settings.wifiConfigOverAP == true || forceAP) + { + // Stop any current WiFi activity + WIFI_STOP(); + + // Start in AP mode + WiFi.mode(WIFI_AP); + + // Before starting AP mode, be sure we have default WiFi protocols enabled. + // esp_wifi_set_protocol requires WiFi to be started + esp_err_t response = + esp_wifi_set_protocol(WIFI_IF_AP, WIFI_PROTOCOL_11B | WIFI_PROTOCOL_11G | WIFI_PROTOCOL_11N); + if (response != ESP_OK) + systemPrintf("wifiStartAP: Error setting WiFi protocols: %s\r\n", esp_err_to_name(response)); + else + log_d("WiFi protocols set"); + + IPAddress local_IP(192, 168, 4, 1); + IPAddress gateway(192, 168, 4, 1); + IPAddress subnet(255, 255, 255, 0); + + WiFi.softAPConfig(local_IP, gateway, subnet); + if (WiFi.softAP("RTK Config") == false) // Must be short enough to fit OLED Width + { + systemPrintln("WiFi AP failed to start"); + return (false); + } + systemPrint("WiFi AP Started with IP: "); + systemPrintln(WiFi.softAPIP()); + + // Start DNS Server + if (dnsServer.start(53, "*", WiFi.softAPIP()) == false) + { + systemPrintln("WiFi DNS Server failed to start"); + return (false); + } + else + { + log_d("DNS Server started"); + } + } + else + { + // Start webserver on local WiFi instead of AP + + // Attempt to connect to local WiFi with increasing timeouts + int timeout = 0; + int x = 0; + const int maxTries = 2; + for (; x < maxTries; x++) + { + timeout += 5000; + if (wifiConnect(timeout) == true) // Attempt to connect to any SSID on settings list + { + wifiPrintNetworkInfo(); + break; + } + } + if (x == maxTries) + { + displayNoWiFi(2000); + return(wifiStartAP(true)); // Because there is no local WiFi available, force AP mode so user can still get access/configure it + } + } + + return (true); +} + +//---------------------------------------- +// WiFi Routines +//---------------------------------------- + +// Advance the WiFi state from off to connected +// Throttle connection attempts as needed +void wifiUpdate() +{ + // Skip if in configure-via-ethernet mode + if (configureViaEthernet) + { + // log_d("configureViaEthernet: skipping wifiUpdate"); + return; + } + + // Periodically display the WiFi state + if (settings.debugWifiState && ((millis() - lastWifiState) > 15000)) + { + wifiSetState(wifiState); + lastWifiState = millis(); + } + + DMW_st(wifiSetState, wifiState); + switch (wifiState) + { + default: + systemPrintf("Unknown wifiState: %d\r\n", wifiState); + break; + + case WIFI_STATE_OFF: + // Any service that needs WiFi will call wifiStart() + break; + + case WIFI_STATE_CONNECTING: + // Pause until connection timeout has passed + if (millis() - wifiLastConnectionAttempt > wifiConnectionAttemptTimeout) + { + wifiLastConnectionAttempt = millis(); + + if (wifiConnect(10000) == true) // Attempt to connect to any SSID on settings list + { + if (espnowState > ESPNOW_OFF) + espnowStart(); + + wifiSetState(WIFI_STATE_CONNECTED); + } + else + { + // We failed to connect + if (wifiConnectLimitReached() == false) // Increases wifiConnectionAttemptTimeout + { + if (wifiConnectionAttemptTimeout / 1000 < 120) + systemPrintf("Next WiFi attempt in %d seconds.\r\n", wifiConnectionAttemptTimeout / 1000); + else + systemPrintf("Next WiFi attempt in %d minutes.\r\n", wifiConnectionAttemptTimeout / 1000 / 60); + } + else + { + systemPrintln("WiFi connection failed. Giving up."); + displayNoWiFi(2000); + WIFI_STOP(); // Move back to WIFI_STATE_OFF + } + } + } + + break; + + case WIFI_STATE_CONNECTED: + // Verify link is still up + if (wifiIsConnected() == false) + { + systemPrintln("WiFi link lost"); + wifiConnectionAttempts = 0; // Reset the timeout + wifiSetState(WIFI_STATE_CONNECTING); + } + + // If WiFi is connected, and no services require WiFi, shut it off + else if (wifiIsNeeded() == false) + WIFI_STOP(); + + break; + } + + // Process DNS when we are in AP mode for captive portal + if (WiFi.getMode() == WIFI_AP && settings.enableCaptivePortal) + { + dnsServer.processNextRequest(); + } +} + +// Starts the WiFi connection state machine (moves from WIFI_STATE_OFF to WIFI_STATE_CONNECTING) +// Sets the appropriate protocols (WiFi + ESP-Now) +// If radio is off entirely, start WiFi +// If ESP-Now is active, only add the LR protocol +void wifiStart() +{ + if (wifiNetworkCount() == 0) + { + systemPrintln("Error: Please enter at least one SSID before using WiFi"); + displayNoSSIDs(2000); + WIFI_STOP(); + return; + } + + if (wifiIsConnected() == true) + return; // We don't need to do anything + + if (wifiState > WIFI_STATE_OFF) + return; // We're in the midst of connecting + + log_d("Starting WiFi"); + + wifiSetState(WIFI_STATE_CONNECTING); // This starts the state machine running + + // Display the heap state + reportHeapNow(settings.debugWifiState); +} + +// Stop WiFi and release all resources +void wifiStop() +{ + // Stop the web server + stopWebServer(); + + // Stop the multicast domain name server + if (settings.mdnsEnable == true) + MDNS.end(); + + // Stop the DNS server if we were using the captive portal + if (WiFi.getMode() == WIFI_AP && settings.enableCaptivePortal) + dnsServer.stop(); + + // Stop the other network clients and then WiFi + NETWORK_STOP(NETWORK_TYPE_WIFI); +} + +// Stop WiFi and release all resources +// If ESP NOW is active, leave WiFi on enough for ESP NOW +void wifiShutdown() +{ + wifiSetState(WIFI_STATE_OFF); + + wifiConnectionAttempts = 0; // Reset the timeout + + // If ESP-Now is active, change protocol to only Long Range and re-start WiFi + if (espnowState > ESPNOW_OFF) + { + if (WiFi.getMode() != WIFI_STA) + WiFi.mode(WIFI_STA); + + // Enable long range, PHY rate of ESP32 will be 512Kbps or 256Kbps + // esp_wifi_set_protocol requires WiFi to be started + esp_err_t response = esp_wifi_set_protocol(WIFI_IF_STA, WIFI_PROTOCOL_LR); + if (response != ESP_OK) + systemPrintf("wifiShutdown: Error setting ESP-Now lone protocol: %s\r\n", esp_err_to_name(response)); + else + log_d("WiFi disabled, ESP-Now left in place"); + } + else + { + WiFi.mode(WIFI_OFF); + log_d("WiFi Stopped"); + } + + // Display the heap state + reportHeapNow(settings.debugWifiState); +} + +bool wifiIsConnected() +{ + return (wifiGetStatus() == WL_CONNECTED); +} + +// Attempts a connection to all provided SSIDs +// Returns true if successful +// Gives up if no SSID detected or connection times out +bool wifiConnect(unsigned long timeout) +{ + if (wifiIsConnected()) + return (true); // Nothing to do + + displayWiFiConnect(); + + // Before we can issue esp_wifi_() commands WiFi must be started + if (WiFi.getMode() != WIFI_STA) + WiFi.mode(WIFI_STA); + + // Verify that the necessary protocols are set + uint8_t protocols = 0; + esp_err_t response = esp_wifi_get_protocol(WIFI_IF_STA, &protocols); + if (response != ESP_OK) + systemPrintf("wifiConnect: Failed to get protocols: %s\r\n", esp_err_to_name(response)); + + // If ESP-NOW is running, blend in ESP-NOW protocol. + if (espnowState > ESPNOW_OFF) + { + if (protocols != (WIFI_PROTOCOL_11B | WIFI_PROTOCOL_11G | WIFI_PROTOCOL_11N | WIFI_PROTOCOL_LR)) + { + esp_err_t response = + esp_wifi_set_protocol(WIFI_IF_STA, WIFI_PROTOCOL_11B | WIFI_PROTOCOL_11G | WIFI_PROTOCOL_11N | + WIFI_PROTOCOL_LR); // Enable WiFi + ESP-Now. + if (response != ESP_OK) + systemPrintf("wifiConnect: Error setting WiFi + ESP-NOW protocols: %s\r\n", esp_err_to_name(response)); + } + } + else + { + // Make sure default WiFi protocols are in place + if (protocols != (WIFI_PROTOCOL_11B | WIFI_PROTOCOL_11G | WIFI_PROTOCOL_11N)) + { + esp_err_t response = esp_wifi_set_protocol(WIFI_IF_STA, WIFI_PROTOCOL_11B | WIFI_PROTOCOL_11G | + WIFI_PROTOCOL_11N); // Enable WiFi. + if (response != ESP_OK) + systemPrintf("wifiConnect: Error setting WiFi protocols: %s\r\n", esp_err_to_name(response)); + } + } + + WiFiMulti wifiMulti; + + // Load SSIDs + for (int x = 0; x < MAX_WIFI_NETWORKS; x++) + { + if (strlen(settings.wifiNetworks[x].ssid) > 0) + wifiMulti.addAP(settings.wifiNetworks[x].ssid, settings.wifiNetworks[x].password); + } + + systemPrint("Connecting WiFi... "); + + int wifiResponse = wifiMulti.run(timeout); + if (wifiResponse == WL_CONNECTED) + { + if (settings.enablePvtClient == true || settings.enablePvtServer == true || settings.enablePvtUdpServer == true) + { + if (settings.mdnsEnable == true) + { + if (MDNS.begin("rtk") == false) // This should make the module findable from 'rtk.local' in browser + systemPrintln("Error setting up MDNS responder!"); + else + MDNS.addService("http", "tcp", settings.httpPort); // Add service to MDNS + } + } + + systemPrintln(); + return true; + } + else if (wifiResponse == WL_DISCONNECTED) + systemPrint("No friendly WiFi networks detected.\r\n"); + else + systemPrintf("WiFi failed to connect: error #%d.\r\n", wifiResponse); + + return false; +} + +// Based on the current settings and system states, determine if we need WiFi on or not +// This function does not start WiFi. Any service that needs it should call wifiStart(). +// This function is used to turn WiFi off if nothing needs it. +bool wifiIsNeeded() +{ + if (settings.enablePvtClient == true) + return true; + if (settings.enablePvtServer == true) + return true; + if (settings.enablePvtUdpServer == true) + return true; + if (settings.enableAutoFirmwareUpdate) + return true; + + // Handle WiFi within systemStates + if (systemState <= STATE_ROVER_RTK_FIX && settings.enableNtripClient == true) + return true; + + if (systemState >= STATE_BASE_NOT_STARTED && systemState <= STATE_BASE_FIXED_TRANSMITTING && + settings.enableNtripServer == true) + return true; + + // If the user has enabled NTRIP Client for an Assisted Survey-In, and Survey-In is running, keep WiFi on. + if (systemState >= STATE_BASE_NOT_STARTED && systemState <= STATE_BASE_TEMP_SURVEY_STARTED && + settings.enableNtripClient == true && settings.fixedBase == false) + return true; + + // If WiFi is on while we are in the following states, allow WiFi to continue to operate + if (systemState >= STATE_BUBBLE_LEVEL && systemState <= STATE_PROFILE) + { + // Keep WiFi on if user presses setup button, enters bubble level, is in AP config mode, etc + return true; + } + + if (systemState == STATE_KEYS_WIFI_STARTED || systemState == STATE_KEYS_WIFI_CONNECTED) + return true; + if (systemState == STATE_KEYS_PROVISION_WIFI_STARTED || systemState == STATE_KEYS_PROVISION_WIFI_CONNECTED) + return true; + + return false; +} + +// Counts the number of entered SSIDs +int wifiNetworkCount() +{ + // Count SSIDs + int networkCount = 0; + for (int x = 0; x < MAX_WIFI_NETWORKS; x++) + { + if (strlen(settings.wifiNetworks[x].ssid) > 0) + networkCount++; + } + return networkCount; +} + +// Determine if another connection is possible or if the limit has been reached +bool wifiConnectLimitReached() +{ + // Retry the connection a few times + bool limitReached = false; + if (wifiConnectionAttempts++ >= wifiMaxConnectionAttempts) + limitReached = true; + + wifiConnectionAttemptsTotal++; + + if (limitReached == false) + { + wifiConnectionAttemptTimeout = + wifiConnectionAttempts * 15 * 1000L; // Wait 15, 30, 45, etc seconds between attempts + + reportHeapNow(settings.debugWifiState); + } + else + { + // No more connection attempts + systemPrintln("WiFi connection attempts exceeded!"); + } + return limitReached; +} + +void wifiPrintNetworkInfo() +{ + systemPrintln("\nNetwork Configuration:"); + systemPrintln("----------------------"); + systemPrint(" SSID: "); + systemPrintln(WiFi.SSID()); + systemPrint(" WiFi Status: "); + systemPrintln(WiFi.status()); + systemPrint("WiFi Strength: "); + systemPrint(WiFi.RSSI()); + systemPrintln(" dBm"); + systemPrint(" MAC: "); + systemPrintln(WiFi.macAddress()); + systemPrint(" IP: "); + systemPrintln(WiFi.localIP()); + systemPrint(" Subnet: "); + systemPrintln(WiFi.subnetMask()); + systemPrint(" Gateway: "); + systemPrintln(WiFi.gatewayIP()); + systemPrint(" DNS 1: "); + systemPrintln(WiFi.dnsIP(0)); + systemPrint(" DNS 2: "); + systemPrintln(WiFi.dnsIP(1)); + systemPrint(" DNS 3: "); + systemPrintln(WiFi.dnsIP(2)); + systemPrintln(); +} + +// Returns true if unit is in config mode +// Used to disallow services (NTRIP, TCP, etc) from updating +bool wifiInConfigMode() +{ + if (systemState >= STATE_WIFI_CONFIG_NOT_STARTED && systemState <= STATE_WIFI_CONFIG) + return true; + return false; +} + +IPAddress wifiGetGatewayIpAddress() +{ + return WiFi.gatewayIP(); +} + +IPAddress wifiGetIpAddress() +{ + return WiFi.localIP(); +} + +int wifiGetRssi() +{ + return WiFi.RSSI(); +} + +#endif // COMPILE_WIFI diff --git a/Firmware/RTK_Surveyor/X509_Certificate_Bundle.h b/Firmware/RTK_Surveyor/X509_Certificate_Bundle.h new file mode 100644 index 000000000..d64efab17 --- /dev/null +++ b/Firmware/RTK_Surveyor/X509_Certificate_Bundle.h @@ -0,0 +1,1056 @@ +const uint8_t x509CertificateBundle[] = +{ + 0x00, 0x29, 0x00, 0x3b, 0x01, 0x26, 0x30, 0x39, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, // 16 + 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, 0x0f, 0x30, 0x0d, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x06, // 32 + 0x41, 0x6d, 0x61, 0x7a, 0x6f, 0x6e, 0x31, 0x19, 0x30, 0x17, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, // 48 + 0x10, 0x41, 0x6d, 0x61, 0x7a, 0x6f, 0x6e, 0x20, 0x52, 0x6f, 0x6f, 0x74, 0x20, 0x43, 0x41, 0x20, // 64 + 0x31, 0x30, 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, // 80 + 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0f, 0x00, 0x30, 0x82, 0x01, 0x0a, 0x02, 0x82, 0x01, // 96 + 0x01, 0x00, 0xb2, 0x78, 0x80, 0x71, 0xca, 0x78, 0xd5, 0xe3, 0x71, 0xaf, 0x47, 0x80, 0x50, 0x74, // 112 + 0x7d, 0x6e, 0xd8, 0xd7, 0x88, 0x76, 0xf4, 0x99, 0x68, 0xf7, 0x58, 0x21, 0x60, 0xf9, 0x74, 0x84, // 128 + 0x01, 0x2f, 0xac, 0x02, 0x2d, 0x86, 0xd3, 0xa0, 0x43, 0x7a, 0x4e, 0xb2, 0xa4, 0xd0, 0x36, 0xba, // 144 + 0x01, 0xbe, 0x8d, 0xdb, 0x48, 0xc8, 0x07, 0x17, 0x36, 0x4c, 0xf4, 0xee, 0x88, 0x23, 0xc7, 0x3e, // 160 + 0xeb, 0x37, 0xf5, 0xb5, 0x19, 0xf8, 0x49, 0x68, 0xb0, 0xde, 0xd7, 0xb9, 0x76, 0x38, 0x1d, 0x61, // 176 + 0x9e, 0xa4, 0xfe, 0x82, 0x36, 0xa5, 0xe5, 0x4a, 0x56, 0xe4, 0x45, 0xe1, 0xf9, 0xfd, 0xb4, 0x16, // 192 + 0xfa, 0x74, 0xda, 0x9c, 0x9b, 0x35, 0x39, 0x2f, 0xfa, 0xb0, 0x20, 0x50, 0x06, 0x6c, 0x7a, 0xd0, // 208 + 0x80, 0xb2, 0xa6, 0xf9, 0xaf, 0xec, 0x47, 0x19, 0x8f, 0x50, 0x38, 0x07, 0xdc, 0xa2, 0x87, 0x39, // 224 + 0x58, 0xf8, 0xba, 0xd5, 0xa9, 0xf9, 0x48, 0x67, 0x30, 0x96, 0xee, 0x94, 0x78, 0x5e, 0x6f, 0x89, // 240 + 0xa3, 0x51, 0xc0, 0x30, 0x86, 0x66, 0xa1, 0x45, 0x66, 0xba, 0x54, 0xeb, 0xa3, 0xc3, 0x91, 0xf9, // 256 + 0x48, 0xdc, 0xff, 0xd1, 0xe8, 0x30, 0x2d, 0x7d, 0x2d, 0x74, 0x70, 0x35, 0xd7, 0x88, 0x24, 0xf7, // 272 + 0x9e, 0xc4, 0x59, 0x6e, 0xbb, 0x73, 0x87, 0x17, 0xf2, 0x32, 0x46, 0x28, 0xb8, 0x43, 0xfa, 0xb7, // 288 + 0x1d, 0xaa, 0xca, 0xb4, 0xf2, 0x9f, 0x24, 0x0e, 0x2d, 0x4b, 0xf7, 0x71, 0x5c, 0x5e, 0x69, 0xff, // 304 + 0xea, 0x95, 0x02, 0xcb, 0x38, 0x8a, 0xae, 0x50, 0x38, 0x6f, 0xdb, 0xfb, 0x2d, 0x62, 0x1b, 0xc5, // 320 + 0xc7, 0x1e, 0x54, 0xe1, 0x77, 0xe0, 0x67, 0xc8, 0x0f, 0x9c, 0x87, 0x23, 0xd6, 0x3f, 0x40, 0x20, // 336 + 0x7f, 0x20, 0x80, 0xc4, 0x80, 0x4c, 0x3e, 0x3b, 0x24, 0x26, 0x8e, 0x04, 0xae, 0x6c, 0x9a, 0xc8, // 352 + 0xaa, 0x0d, 0x02, 0x03, 0x01, 0x00, 0x01, 0x00, 0x3b, 0x02, 0x26, 0x30, 0x39, 0x31, 0x0b, 0x30, // 368 + 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, 0x0f, 0x30, 0x0d, 0x06, 0x03, // 384 + 0x55, 0x04, 0x0a, 0x13, 0x06, 0x41, 0x6d, 0x61, 0x7a, 0x6f, 0x6e, 0x31, 0x19, 0x30, 0x17, 0x06, // 400 + 0x03, 0x55, 0x04, 0x03, 0x13, 0x10, 0x41, 0x6d, 0x61, 0x7a, 0x6f, 0x6e, 0x20, 0x52, 0x6f, 0x6f, // 416 + 0x74, 0x20, 0x43, 0x41, 0x20, 0x32, 0x30, 0x82, 0x02, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, // 432 + 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x02, 0x0f, 0x00, 0x30, 0x82, // 448 + 0x02, 0x0a, 0x02, 0x82, 0x02, 0x01, 0x00, 0xad, 0x96, 0x9f, 0x2d, 0x9c, 0x4a, 0x4c, 0x4a, 0x81, // 464 + 0x79, 0x51, 0x99, 0xec, 0x8a, 0xcb, 0x6b, 0x60, 0x51, 0x13, 0xbc, 0x4d, 0x6d, 0x06, 0xfc, 0xb0, // 480 + 0x08, 0x8d, 0xdd, 0x19, 0x10, 0x6a, 0xc7, 0x26, 0x0c, 0x35, 0xd8, 0xc0, 0x6f, 0x20, 0x84, 0xe9, // 496 + 0x94, 0xb1, 0x9b, 0x85, 0x03, 0xc3, 0x5b, 0xdb, 0x4a, 0xe8, 0xc8, 0xf8, 0x90, 0x76, 0xd9, 0x5b, // 512 + 0x4f, 0xe3, 0x4c, 0xe8, 0x06, 0x36, 0x4d, 0xcc, 0x9a, 0xac, 0x3d, 0x0c, 0x90, 0x2b, 0x92, 0xd4, // 528 + 0x06, 0x19, 0x60, 0xac, 0x37, 0x44, 0x79, 0x85, 0x81, 0x82, 0xad, 0x5a, 0x37, 0xe0, 0x0d, 0xcc, // 544 + 0x9d, 0xa6, 0x4c, 0x52, 0x76, 0xea, 0x43, 0x9d, 0xb7, 0x04, 0xd1, 0x50, 0xf6, 0x55, 0xe0, 0xd5, // 560 + 0xd2, 0xa6, 0x49, 0x85, 0xe9, 0x37, 0xe9, 0xca, 0x7e, 0xae, 0x5c, 0x95, 0x4d, 0x48, 0x9a, 0x3f, // 576 + 0xae, 0x20, 0x5a, 0x6d, 0x88, 0x95, 0xd9, 0x34, 0xb8, 0x52, 0x1a, 0x43, 0x90, 0xb0, 0xbf, 0x6c, // 592 + 0x05, 0xb9, 0xb6, 0x78, 0xb7, 0xea, 0xd0, 0xe4, 0x3a, 0x3c, 0x12, 0x53, 0x62, 0xff, 0x4a, 0xf2, // 608 + 0x7b, 0xbe, 0x35, 0x05, 0xa9, 0x12, 0x34, 0xe3, 0xf3, 0x64, 0x74, 0x62, 0x2c, 0x3d, 0x00, 0x49, // 624 + 0x5a, 0x28, 0xfe, 0x32, 0x44, 0xbb, 0x87, 0xdd, 0x65, 0x27, 0x02, 0x71, 0x3b, 0xda, 0x4a, 0xf7, // 640 + 0x1f, 0xda, 0xcd, 0xf7, 0x21, 0x55, 0x90, 0x4f, 0x0f, 0xec, 0xae, 0x82, 0xe1, 0x9f, 0x6b, 0xd9, // 656 + 0x45, 0xd3, 0xbb, 0xf0, 0x5f, 0x87, 0xed, 0x3c, 0x2c, 0x39, 0x86, 0xda, 0x3f, 0xde, 0xec, 0x72, // 672 + 0x55, 0xeb, 0x79, 0xa3, 0xad, 0xdb, 0xdd, 0x7c, 0xb0, 0xba, 0x1c, 0xce, 0xfc, 0xde, 0x4f, 0x35, // 688 + 0x76, 0xcf, 0x0f, 0xf8, 0x78, 0x1f, 0x6a, 0x36, 0x51, 0x46, 0x27, 0x61, 0x5b, 0xe9, 0x9e, 0xcf, // 704 + 0xf0, 0xa2, 0x55, 0x7d, 0x7c, 0x25, 0x8a, 0x6f, 0x2f, 0xb4, 0xc5, 0xcf, 0x84, 0x2e, 0x2b, 0xfd, // 720 + 0x0d, 0x51, 0x10, 0x6c, 0xfb, 0x5f, 0x1b, 0xbc, 0x1b, 0x7e, 0xc5, 0xae, 0x3b, 0x98, 0x01, 0x31, // 736 + 0x92, 0xff, 0x0b, 0x57, 0xf4, 0x9a, 0xb2, 0xb9, 0x57, 0xe9, 0xab, 0xef, 0x0d, 0x76, 0xd1, 0xf0, // 752 + 0xee, 0xf4, 0xce, 0x86, 0xa7, 0xe0, 0x6e, 0xe9, 0xb4, 0x69, 0xa1, 0xdf, 0x69, 0xf6, 0x33, 0xc6, // 768 + 0x69, 0x2e, 0x97, 0x13, 0x9e, 0xa5, 0x87, 0xb0, 0x57, 0x10, 0x81, 0x37, 0xc9, 0x53, 0xb3, 0xbb, // 784 + 0x7f, 0xf6, 0x92, 0xd1, 0x9c, 0xd0, 0x18, 0xf4, 0x92, 0x6e, 0xda, 0x83, 0x4f, 0xa6, 0x63, 0x99, // 800 + 0x4c, 0xa5, 0xfb, 0x5e, 0xef, 0x21, 0x64, 0x7a, 0x20, 0x5f, 0x6c, 0x64, 0x85, 0x15, 0xcb, 0x37, // 816 + 0xe9, 0x62, 0x0c, 0x0b, 0x2a, 0x16, 0xdc, 0x01, 0x2e, 0x32, 0xda, 0x3e, 0x4b, 0xf5, 0x9e, 0x3a, // 832 + 0xf6, 0x17, 0x40, 0x94, 0xef, 0x9e, 0x91, 0x08, 0x86, 0xfa, 0xbe, 0x63, 0xa8, 0x5a, 0x33, 0xec, // 848 + 0xcb, 0x74, 0x43, 0x95, 0xf9, 0x6c, 0x69, 0x52, 0x36, 0xc7, 0x29, 0x6f, 0xfc, 0x55, 0x03, 0x5c, // 864 + 0x1f, 0xfb, 0x9f, 0xbd, 0x47, 0xeb, 0xe7, 0x49, 0x47, 0x95, 0x0b, 0x4e, 0x89, 0x22, 0x09, 0x49, // 880 + 0xe0, 0xf5, 0x61, 0x1e, 0xf1, 0xbf, 0x2e, 0x8a, 0x72, 0x6e, 0x80, 0x59, 0xff, 0x57, 0x3a, 0xf9, // 896 + 0x75, 0x32, 0xa3, 0x4e, 0x5f, 0xec, 0xed, 0x28, 0x62, 0xd9, 0x4d, 0x73, 0xf2, 0xcc, 0x81, 0x17, // 912 + 0x60, 0xed, 0xcd, 0xeb, 0xdc, 0xdb, 0xa7, 0xca, 0xc5, 0x7e, 0x02, 0xbd, 0xf2, 0x54, 0x08, 0x54, // 928 + 0xfd, 0xb4, 0x2d, 0x09, 0x2c, 0x17, 0x54, 0x4a, 0x98, 0xd1, 0x54, 0xe1, 0x51, 0x67, 0x08, 0xd2, // 944 + 0xed, 0x6e, 0x7e, 0x6f, 0x3f, 0xd2, 0x2d, 0x81, 0x59, 0x29, 0x66, 0xcb, 0x90, 0x39, 0x95, 0x11, // 960 + 0x1e, 0x74, 0x27, 0xfe, 0xdd, 0xeb, 0xaf, 0x02, 0x03, 0x01, 0x00, 0x01, 0x00, 0x3b, 0x00, 0x5b, // 976 + 0x30, 0x39, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, // 992 + 0x0f, 0x30, 0x0d, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x06, 0x41, 0x6d, 0x61, 0x7a, 0x6f, 0x6e, // 1008 + 0x31, 0x19, 0x30, 0x17, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x10, 0x41, 0x6d, 0x61, 0x7a, 0x6f, // 1024 + 0x6e, 0x20, 0x52, 0x6f, 0x6f, 0x74, 0x20, 0x43, 0x41, 0x20, 0x33, 0x30, 0x59, 0x30, 0x13, 0x06, // 1040 + 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, // 1056 + 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0x29, 0x97, 0xa7, 0xc6, 0x41, 0x7f, 0xc0, 0x0d, 0x9b, 0xe8, // 1072 + 0x01, 0x1b, 0x56, 0xc6, 0xf2, 0x52, 0xa5, 0xba, 0x2d, 0xb2, 0x12, 0xe8, 0xd2, 0x2e, 0xd7, 0xfa, // 1088 + 0xc9, 0xc5, 0xd8, 0xaa, 0x6d, 0x1f, 0x73, 0x81, 0x3b, 0x3b, 0x98, 0x6b, 0x39, 0x7c, 0x33, 0xa5, // 1104 + 0xc5, 0x4e, 0x86, 0x8e, 0x80, 0x17, 0x68, 0x62, 0x45, 0x57, 0x7d, 0x44, 0x58, 0x1d, 0xb3, 0x37, // 1120 + 0xe5, 0x67, 0x08, 0xeb, 0x66, 0xde, 0x00, 0x3b, 0x00, 0x78, 0x30, 0x39, 0x31, 0x0b, 0x30, 0x09, // 1136 + 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, 0x0f, 0x30, 0x0d, 0x06, 0x03, 0x55, // 1152 + 0x04, 0x0a, 0x13, 0x06, 0x41, 0x6d, 0x61, 0x7a, 0x6f, 0x6e, 0x31, 0x19, 0x30, 0x17, 0x06, 0x03, // 1168 + 0x55, 0x04, 0x03, 0x13, 0x10, 0x41, 0x6d, 0x61, 0x7a, 0x6f, 0x6e, 0x20, 0x52, 0x6f, 0x6f, 0x74, // 1184 + 0x20, 0x43, 0x41, 0x20, 0x34, 0x30, 0x76, 0x30, 0x10, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, // 1200 + 0x02, 0x01, 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x22, 0x03, 0x62, 0x00, 0x04, 0xd2, 0xab, 0x8a, // 1216 + 0x37, 0x4f, 0xa3, 0x53, 0x0d, 0xfe, 0xc1, 0x8a, 0x7b, 0x4b, 0xa8, 0x7b, 0x46, 0x4b, 0x63, 0xb0, // 1232 + 0x62, 0xf6, 0x2d, 0x1b, 0xdb, 0x08, 0x71, 0x21, 0xd2, 0x00, 0xe8, 0x63, 0xbd, 0x9a, 0x27, 0xfb, // 1248 + 0xf0, 0x39, 0x6e, 0x5d, 0xea, 0x3d, 0xa5, 0xc9, 0x81, 0xaa, 0xa3, 0x5b, 0x20, 0x98, 0x45, 0x5d, // 1264 + 0x16, 0xdb, 0xfd, 0xe8, 0x10, 0x6d, 0xe3, 0x9c, 0xe0, 0xe3, 0xbd, 0x5f, 0x84, 0x62, 0xf3, 0x70, // 1280 + 0x64, 0x33, 0xa0, 0xcb, 0x24, 0x2f, 0x70, 0xba, 0x88, 0xa1, 0x2a, 0xa0, 0x75, 0xf8, 0x81, 0xae, // 1296 + 0x62, 0x06, 0xc4, 0x81, 0xdb, 0x39, 0x6e, 0x29, 0xb0, 0x1e, 0xfa, 0x2e, 0x5c, 0x00, 0x41, 0x01, // 1312 + 0x26, 0x30, 0x3f, 0x31, 0x24, 0x30, 0x22, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x1b, 0x44, 0x69, // 1328 + 0x67, 0x69, 0x74, 0x61, 0x6c, 0x20, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x20, // 1344 + 0x54, 0x72, 0x75, 0x73, 0x74, 0x20, 0x43, 0x6f, 0x2e, 0x31, 0x17, 0x30, 0x15, 0x06, 0x03, 0x55, // 1360 + 0x04, 0x03, 0x13, 0x0e, 0x44, 0x53, 0x54, 0x20, 0x52, 0x6f, 0x6f, 0x74, 0x20, 0x43, 0x41, 0x20, // 1376 + 0x58, 0x33, 0x30, 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, // 1392 + 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0f, 0x00, 0x30, 0x82, 0x01, 0x0a, 0x02, 0x82, // 1408 + 0x01, 0x01, 0x00, 0xdf, 0xaf, 0xe9, 0x97, 0x50, 0x08, 0x83, 0x57, 0xb4, 0xcc, 0x62, 0x65, 0xf6, // 1424 + 0x90, 0x82, 0xec, 0xc7, 0xd3, 0x2c, 0x6b, 0x30, 0xca, 0x5b, 0xec, 0xd9, 0xc3, 0x7d, 0xc7, 0x40, // 1440 + 0xc1, 0x18, 0x14, 0x8b, 0xe0, 0xe8, 0x33, 0x76, 0x49, 0x2a, 0xe3, 0x3f, 0x21, 0x49, 0x93, 0xac, // 1456 + 0x4e, 0x0e, 0xaf, 0x3e, 0x48, 0xcb, 0x65, 0xee, 0xfc, 0xd3, 0x21, 0x0f, 0x65, 0xd2, 0x2a, 0xd9, // 1472 + 0x32, 0x8f, 0x8c, 0xe5, 0xf7, 0x77, 0xb0, 0x12, 0x7b, 0xb5, 0x95, 0xc0, 0x89, 0xa3, 0xa9, 0xba, // 1488 + 0xed, 0x73, 0x2e, 0x7a, 0x0c, 0x06, 0x32, 0x83, 0xa2, 0x7e, 0x8a, 0x14, 0x30, 0xcd, 0x11, 0xa0, // 1504 + 0xe1, 0x2a, 0x38, 0xb9, 0x79, 0x0a, 0x31, 0xfd, 0x50, 0xbd, 0x80, 0x65, 0xdf, 0xb7, 0x51, 0x63, // 1520 + 0x83, 0xc8, 0xe2, 0x88, 0x61, 0xea, 0x4b, 0x61, 0x81, 0xec, 0x52, 0x6b, 0xb9, 0xa2, 0xe2, 0x4b, // 1536 + 0x1a, 0x28, 0x9f, 0x48, 0xa3, 0x9e, 0x0c, 0xda, 0x09, 0x8e, 0x3e, 0x17, 0x2e, 0x1e, 0xdd, 0x20, // 1552 + 0xdf, 0x5b, 0xc6, 0x2a, 0x8a, 0xab, 0x2e, 0xbd, 0x70, 0xad, 0xc5, 0x0b, 0x1a, 0x25, 0x90, 0x74, // 1568 + 0x72, 0xc5, 0x7b, 0x6a, 0xab, 0x34, 0xd6, 0x30, 0x89, 0xff, 0xe5, 0x68, 0x13, 0x7b, 0x54, 0x0b, // 1584 + 0xc8, 0xd6, 0xae, 0xec, 0x5a, 0x9c, 0x92, 0x1e, 0x3d, 0x64, 0xb3, 0x8c, 0xc6, 0xdf, 0xbf, 0xc9, // 1600 + 0x41, 0x70, 0xec, 0x16, 0x72, 0xd5, 0x26, 0xec, 0x38, 0x55, 0x39, 0x43, 0xd0, 0xfc, 0xfd, 0x18, // 1616 + 0x5c, 0x40, 0xf1, 0x97, 0xeb, 0xd5, 0x9a, 0x9b, 0x8d, 0x1d, 0xba, 0xda, 0x25, 0xb9, 0xc6, 0xd8, // 1632 + 0xdf, 0xc1, 0x15, 0x02, 0x3a, 0xab, 0xda, 0x6e, 0xf1, 0x3e, 0x2e, 0xf5, 0x5c, 0x08, 0x9c, 0x3c, // 1648 + 0xd6, 0x83, 0x69, 0xe4, 0x10, 0x9b, 0x19, 0x2a, 0xb6, 0x29, 0x57, 0xe3, 0xe5, 0x3d, 0x9b, 0x9f, // 1664 + 0xf0, 0x02, 0x5d, 0x02, 0x03, 0x01, 0x00, 0x01, 0x00, 0x48, 0x00, 0x78, 0x30, 0x46, 0x31, 0x0b, // 1680 + 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x42, 0x45, 0x31, 0x19, 0x30, 0x17, 0x06, // 1696 + 0x03, 0x55, 0x04, 0x0a, 0x13, 0x10, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x53, 0x69, 0x67, 0x6e, // 1712 + 0x20, 0x6e, 0x76, 0x2d, 0x73, 0x61, 0x31, 0x1c, 0x30, 0x1a, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, // 1728 + 0x13, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x53, 0x69, 0x67, 0x6e, 0x20, 0x52, 0x6f, 0x6f, 0x74, // 1744 + 0x20, 0x45, 0x34, 0x36, 0x30, 0x76, 0x30, 0x10, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, // 1760 + 0x01, 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x22, 0x03, 0x62, 0x00, 0x04, 0x9c, 0x0e, 0xb1, 0xcf, // 1776 + 0xb7, 0xe8, 0x9e, 0x52, 0x77, 0x75, 0x34, 0xfa, 0xa5, 0x46, 0xa7, 0xad, 0x32, 0x19, 0x32, 0xb4, // 1792 + 0x07, 0xa9, 0x27, 0xca, 0x94, 0xbb, 0x0c, 0xd2, 0x0a, 0x10, 0xc7, 0xda, 0x89, 0xb0, 0x97, 0x0c, // 1808 + 0x70, 0x13, 0x09, 0x01, 0x8e, 0xd8, 0xea, 0x47, 0xea, 0xbe, 0xb2, 0x80, 0x2b, 0xcd, 0xfc, 0x28, // 1824 + 0x0d, 0xdb, 0xac, 0xbc, 0xa4, 0x86, 0x37, 0xed, 0x70, 0x08, 0x00, 0x75, 0xea, 0x93, 0x0b, 0x7b, // 1840 + 0x2e, 0x52, 0x9c, 0x23, 0x68, 0x23, 0x06, 0x43, 0xec, 0x92, 0x2f, 0x53, 0x84, 0xdb, 0xfb, 0x47, // 1856 + 0x14, 0x07, 0xe8, 0x5f, 0x94, 0x67, 0x5d, 0xc9, 0x7a, 0x81, 0x3c, 0x20, 0x00, 0x48, 0x02, 0x26, // 1872 + 0x30, 0x46, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x42, 0x45, 0x31, // 1888 + 0x19, 0x30, 0x17, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x10, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, // 1904 + 0x53, 0x69, 0x67, 0x6e, 0x20, 0x6e, 0x76, 0x2d, 0x73, 0x61, 0x31, 0x1c, 0x30, 0x1a, 0x06, 0x03, // 1920 + 0x55, 0x04, 0x03, 0x13, 0x13, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x53, 0x69, 0x67, 0x6e, 0x20, // 1936 + 0x52, 0x6f, 0x6f, 0x74, 0x20, 0x52, 0x34, 0x36, 0x30, 0x82, 0x02, 0x22, 0x30, 0x0d, 0x06, 0x09, // 1952 + 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x02, 0x0f, 0x00, // 1968 + 0x30, 0x82, 0x02, 0x0a, 0x02, 0x82, 0x02, 0x01, 0x00, 0xac, 0xac, 0x74, 0x32, 0xe8, 0xb3, 0x65, // 1984 + 0xe5, 0xba, 0xed, 0x43, 0x26, 0x1d, 0xa6, 0x89, 0x0d, 0x45, 0xba, 0x29, 0x88, 0xb2, 0xa4, 0x1d, // 2000 + 0x63, 0xdd, 0xd3, 0xc1, 0x2c, 0x09, 0x57, 0x89, 0x39, 0xa1, 0x55, 0xe9, 0x67, 0x34, 0x77, 0x0c, // 2016 + 0x6e, 0xe4, 0x55, 0x1d, 0x52, 0x25, 0xd2, 0x13, 0x6b, 0x5e, 0xe1, 0x1d, 0xa9, 0xb7, 0x7d, 0x89, // 2032 + 0x32, 0x5f, 0x0d, 0x9e, 0x9f, 0x2c, 0x7a, 0x63, 0x60, 0x40, 0x1f, 0xa6, 0xb0, 0xb6, 0x78, 0x8f, // 2048 + 0x99, 0x54, 0x96, 0x08, 0x58, 0xae, 0xe4, 0x06, 0xbc, 0x62, 0x05, 0x02, 0x16, 0xbf, 0xaf, 0xa8, // 2064 + 0x23, 0x03, 0xb6, 0x94, 0x0f, 0xbc, 0x6e, 0x6c, 0xc2, 0xcb, 0xd5, 0xa6, 0xbb, 0x0c, 0xe9, 0xf6, // 2080 + 0xc1, 0x02, 0xfb, 0x21, 0xde, 0x66, 0xdd, 0x17, 0xab, 0x74, 0x42, 0xef, 0xf0, 0x74, 0x2f, 0x25, // 2096 + 0xf4, 0xea, 0x6b, 0x55, 0x5b, 0x90, 0xdb, 0x9d, 0xdf, 0x5e, 0x87, 0x0a, 0x40, 0xfb, 0xad, 0x19, // 2112 + 0x6b, 0xfb, 0xf7, 0xca, 0x60, 0x88, 0xde, 0xda, 0xc1, 0x8f, 0xd6, 0xae, 0xd5, 0x7f, 0xd4, 0x3c, // 2128 + 0x83, 0xee, 0xd7, 0x16, 0x4c, 0x83, 0x45, 0x33, 0x6b, 0x27, 0xd0, 0x86, 0xd0, 0x1c, 0x2d, 0x6b, // 2144 + 0xf3, 0xab, 0x7d, 0xf1, 0x85, 0xa9, 0xf5, 0x28, 0xd2, 0xad, 0xef, 0xf3, 0x84, 0x4b, 0x1c, 0x87, // 2160 + 0xfc, 0x13, 0xa3, 0x3a, 0x72, 0xa2, 0x5a, 0x11, 0x2b, 0xd6, 0x27, 0x71, 0x27, 0xed, 0x81, 0x2d, // 2176 + 0x6d, 0x66, 0x81, 0x92, 0x87, 0xb4, 0x1b, 0x58, 0x7a, 0xcc, 0x3f, 0x0a, 0xfa, 0x46, 0x4f, 0x4d, // 2192 + 0x78, 0x5c, 0xf8, 0x2b, 0x48, 0xe3, 0x04, 0x84, 0xcb, 0x5d, 0xf6, 0xb4, 0x6a, 0xb3, 0x65, 0xfc, // 2208 + 0x42, 0x9e, 0x51, 0x26, 0x23, 0x20, 0xcb, 0x3d, 0x14, 0xf9, 0x81, 0xed, 0x65, 0x16, 0x00, 0x4f, // 2224 + 0x1a, 0x64, 0x97, 0x66, 0x08, 0xcf, 0x8c, 0x7b, 0xe3, 0x2b, 0xc0, 0x9d, 0xf9, 0x14, 0xf2, 0x1b, // 2240 + 0xf1, 0x56, 0x6a, 0x16, 0xbf, 0x2c, 0x85, 0x85, 0xcd, 0x78, 0x38, 0x9a, 0xeb, 0x42, 0x6a, 0x02, // 2256 + 0x34, 0x18, 0x83, 0x17, 0x4e, 0x94, 0x56, 0xf8, 0xb6, 0x82, 0xb5, 0xf3, 0x96, 0xdd, 0x3d, 0xf3, // 2272 + 0xbe, 0x7f, 0x20, 0x77, 0x3e, 0x7b, 0x19, 0x23, 0x6b, 0x2c, 0xd4, 0x72, 0x73, 0x43, 0x57, 0x7d, // 2288 + 0xe0, 0xf8, 0xd7, 0x69, 0x4f, 0x17, 0x36, 0x04, 0xf9, 0xc0, 0x90, 0x60, 0x37, 0x45, 0xde, 0xe6, // 2304 + 0x0c, 0xd8, 0x74, 0x8d, 0xae, 0x9c, 0xa2, 0x6d, 0x74, 0x5d, 0x42, 0xbe, 0x06, 0xf5, 0xd9, 0x64, // 2320 + 0x6e, 0x02, 0x10, 0xac, 0x89, 0xb0, 0x4c, 0x3b, 0x07, 0x4d, 0x40, 0x7e, 0x24, 0xc5, 0x8a, 0x98, // 2336 + 0x82, 0x79, 0x8e, 0xa4, 0xa7, 0x82, 0x20, 0x8d, 0x23, 0xfa, 0x27, 0x71, 0xc9, 0xdf, 0xc6, 0x41, // 2352 + 0x74, 0xa0, 0x4d, 0xf6, 0x91, 0x16, 0xdc, 0x46, 0x8c, 0x5f, 0x29, 0x63, 0x31, 0x59, 0x71, 0x0c, // 2368 + 0xd8, 0x6f, 0xc2, 0xb6, 0x32, 0x7d, 0xfb, 0xe6, 0x5d, 0x53, 0xa6, 0x7e, 0x15, 0xfc, 0xbb, 0x75, // 2384 + 0x7c, 0x5d, 0xec, 0xf8, 0xf6, 0x17, 0x1c, 0xec, 0xc7, 0x6b, 0x19, 0xcb, 0xf3, 0x7b, 0xf0, 0x2b, // 2400 + 0x07, 0xa5, 0xd9, 0x6c, 0x79, 0x54, 0x76, 0x6c, 0x9d, 0x1c, 0xa6, 0x6e, 0x0e, 0xe9, 0x79, 0x0c, // 2416 + 0xa8, 0x23, 0x6a, 0xa3, 0xdf, 0x1b, 0x30, 0x31, 0x9f, 0xb1, 0x54, 0x7b, 0xfe, 0x6a, 0xcb, 0x66, // 2432 + 0xaa, 0xdc, 0x65, 0xd0, 0xa2, 0x9e, 0x4a, 0x9a, 0x07, 0x21, 0x6b, 0x81, 0x8f, 0xdb, 0xc4, 0x59, // 2448 + 0xfa, 0xde, 0x22, 0xc0, 0x04, 0x9c, 0xe3, 0xaa, 0x5b, 0x36, 0x93, 0xe8, 0x3d, 0xbd, 0x7a, 0xa1, // 2464 + 0x9d, 0x0b, 0x76, 0xb1, 0x0b, 0xc7, 0x9d, 0xfd, 0xcf, 0x98, 0xa8, 0x06, 0xc2, 0xf8, 0x2a, 0xa3, // 2480 + 0xa1, 0x83, 0xa0, 0xb7, 0x25, 0x72, 0xa5, 0x02, 0xe3, 0x02, 0x03, 0x01, 0x00, 0x01, 0x00, 0x49, // 2496 + 0x02, 0x26, 0x30, 0x47, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, // 2512 + 0x53, 0x31, 0x22, 0x30, 0x20, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x19, 0x47, 0x6f, 0x6f, 0x67, // 2528 + 0x6c, 0x65, 0x20, 0x54, 0x72, 0x75, 0x73, 0x74, 0x20, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, // 2544 + 0x73, 0x20, 0x4c, 0x4c, 0x43, 0x31, 0x14, 0x30, 0x12, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x0b, // 2560 + 0x47, 0x54, 0x53, 0x20, 0x52, 0x6f, 0x6f, 0x74, 0x20, 0x52, 0x31, 0x30, 0x82, 0x02, 0x22, 0x30, // 2576 + 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, // 2592 + 0x02, 0x0f, 0x00, 0x30, 0x82, 0x02, 0x0a, 0x02, 0x82, 0x02, 0x01, 0x00, 0xb6, 0x11, 0x02, 0x8b, // 2608 + 0x1e, 0xe3, 0xa1, 0x77, 0x9b, 0x3b, 0xdc, 0xbf, 0x94, 0x3e, 0xb7, 0x95, 0xa7, 0x40, 0x3c, 0xa1, // 2624 + 0xfd, 0x82, 0xf9, 0x7d, 0x32, 0x06, 0x82, 0x71, 0xf6, 0xf6, 0x8c, 0x7f, 0xfb, 0xe8, 0xdb, 0xbc, // 2640 + 0x6a, 0x2e, 0x97, 0x97, 0xa3, 0x8c, 0x4b, 0xf9, 0x2b, 0xf6, 0xb1, 0xf9, 0xce, 0x84, 0x1d, 0xb1, // 2656 + 0xf9, 0xc5, 0x97, 0xde, 0xef, 0xb9, 0xf2, 0xa3, 0xe9, 0xbc, 0x12, 0x89, 0x5e, 0xa7, 0xaa, 0x52, // 2672 + 0xab, 0xf8, 0x23, 0x27, 0xcb, 0xa4, 0xb1, 0x9c, 0x63, 0xdb, 0xd7, 0x99, 0x7e, 0xf0, 0x0a, 0x5e, // 2688 + 0xeb, 0x68, 0xa6, 0xf4, 0xc6, 0x5a, 0x47, 0x0d, 0x4d, 0x10, 0x33, 0xe3, 0x4e, 0xb1, 0x13, 0xa3, // 2704 + 0xc8, 0x18, 0x6c, 0x4b, 0xec, 0xfc, 0x09, 0x90, 0xdf, 0x9d, 0x64, 0x29, 0x25, 0x23, 0x07, 0xa1, // 2720 + 0xb4, 0xd2, 0x3d, 0x2e, 0x60, 0xe0, 0xcf, 0xd2, 0x09, 0x87, 0xbb, 0xcd, 0x48, 0xf0, 0x4d, 0xc2, // 2736 + 0xc2, 0x7a, 0x88, 0x8a, 0xbb, 0xba, 0xcf, 0x59, 0x19, 0xd6, 0xaf, 0x8f, 0xb0, 0x07, 0xb0, 0x9e, // 2752 + 0x31, 0xf1, 0x82, 0xc1, 0xc0, 0xdf, 0x2e, 0xa6, 0x6d, 0x6c, 0x19, 0x0e, 0xb5, 0xd8, 0x7e, 0x26, // 2768 + 0x1a, 0x45, 0x03, 0x3d, 0xb0, 0x79, 0xa4, 0x94, 0x28, 0xad, 0x0f, 0x7f, 0x26, 0xe5, 0xa8, 0x08, // 2784 + 0xfe, 0x96, 0xe8, 0x3c, 0x68, 0x94, 0x53, 0xee, 0x83, 0x3a, 0x88, 0x2b, 0x15, 0x96, 0x09, 0xb2, // 2800 + 0xe0, 0x7a, 0x8c, 0x2e, 0x75, 0xd6, 0x9c, 0xeb, 0xa7, 0x56, 0x64, 0x8f, 0x96, 0x4f, 0x68, 0xae, // 2816 + 0x3d, 0x97, 0xc2, 0x84, 0x8f, 0xc0, 0xbc, 0x40, 0xc0, 0x0b, 0x5c, 0xbd, 0xf6, 0x87, 0xb3, 0x35, // 2832 + 0x6c, 0xac, 0x18, 0x50, 0x7f, 0x84, 0xe0, 0x4c, 0xcd, 0x92, 0xd3, 0x20, 0xe9, 0x33, 0xbc, 0x52, // 2848 + 0x99, 0xaf, 0x32, 0xb5, 0x29, 0xb3, 0x25, 0x2a, 0xb4, 0x48, 0xf9, 0x72, 0xe1, 0xca, 0x64, 0xf7, // 2864 + 0xe6, 0x82, 0x10, 0x8d, 0xe8, 0x9d, 0xc2, 0x8a, 0x88, 0xfa, 0x38, 0x66, 0x8a, 0xfc, 0x63, 0xf9, // 2880 + 0x01, 0xf9, 0x78, 0xfd, 0x7b, 0x5c, 0x77, 0xfa, 0x76, 0x87, 0xfa, 0xec, 0xdf, 0xb1, 0x0e, 0x79, // 2896 + 0x95, 0x57, 0xb4, 0xbd, 0x26, 0xef, 0xd6, 0x01, 0xd1, 0xeb, 0x16, 0x0a, 0xbb, 0x8e, 0x0b, 0xb5, // 2912 + 0xc5, 0xc5, 0x8a, 0x55, 0xab, 0xd3, 0xac, 0xea, 0x91, 0x4b, 0x29, 0xcc, 0x19, 0xa4, 0x32, 0x25, // 2928 + 0x4e, 0x2a, 0xf1, 0x65, 0x44, 0xd0, 0x02, 0xce, 0xaa, 0xce, 0x49, 0xb4, 0xea, 0x9f, 0x7c, 0x83, // 2944 + 0xb0, 0x40, 0x7b, 0xe7, 0x43, 0xab, 0xa7, 0x6c, 0xa3, 0x8f, 0x7d, 0x89, 0x81, 0xfa, 0x4c, 0xa5, // 2960 + 0xff, 0xd5, 0x8e, 0xc3, 0xce, 0x4b, 0xe0, 0xb5, 0xd8, 0xb3, 0x8e, 0x45, 0xcf, 0x76, 0xc0, 0xed, // 2976 + 0x40, 0x2b, 0xfd, 0x53, 0x0f, 0xb0, 0xa7, 0xd5, 0x3b, 0x0d, 0xb1, 0x8a, 0xa2, 0x03, 0xde, 0x31, // 2992 + 0xad, 0xcc, 0x77, 0xea, 0x6f, 0x7b, 0x3e, 0xd6, 0xdf, 0x91, 0x22, 0x12, 0xe6, 0xbe, 0xfa, 0xd8, // 3008 + 0x32, 0xfc, 0x10, 0x63, 0x14, 0x51, 0x72, 0xde, 0x5d, 0xd6, 0x16, 0x93, 0xbd, 0x29, 0x68, 0x33, // 3024 + 0xef, 0x3a, 0x66, 0xec, 0x07, 0x8a, 0x26, 0xdf, 0x13, 0xd7, 0x57, 0x65, 0x78, 0x27, 0xde, 0x5e, // 3040 + 0x49, 0x14, 0x00, 0xa2, 0x00, 0x7f, 0x9a, 0xa8, 0x21, 0xb6, 0xa9, 0xb1, 0x95, 0xb0, 0xa5, 0xb9, // 3056 + 0x0d, 0x16, 0x11, 0xda, 0xc7, 0x6c, 0x48, 0x3c, 0x40, 0xe0, 0x7e, 0x0d, 0x5a, 0xcd, 0x56, 0x3c, // 3072 + 0xd1, 0x97, 0x05, 0xb9, 0xcb, 0x4b, 0xed, 0x39, 0x4b, 0x9c, 0xc4, 0x3f, 0xd2, 0x55, 0x13, 0x6e, // 3088 + 0x24, 0xb0, 0xd6, 0x71, 0xfa, 0xf4, 0xc1, 0xba, 0xcc, 0xed, 0x1b, 0xf5, 0xfe, 0x81, 0x41, 0xd8, // 3104 + 0x00, 0x98, 0x3d, 0x3a, 0xc8, 0xae, 0x7a, 0x98, 0x37, 0x18, 0x05, 0x95, 0x02, 0x03, 0x01, 0x00, // 3120 + 0x01, 0x00, 0x49, 0x02, 0x26, 0x30, 0x47, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, // 3136 + 0x13, 0x02, 0x55, 0x53, 0x31, 0x22, 0x30, 0x20, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x19, 0x47, // 3152 + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x20, 0x54, 0x72, 0x75, 0x73, 0x74, 0x20, 0x53, 0x65, 0x72, 0x76, // 3168 + 0x69, 0x63, 0x65, 0x73, 0x20, 0x4c, 0x4c, 0x43, 0x31, 0x14, 0x30, 0x12, 0x06, 0x03, 0x55, 0x04, // 3184 + 0x03, 0x13, 0x0b, 0x47, 0x54, 0x53, 0x20, 0x52, 0x6f, 0x6f, 0x74, 0x20, 0x52, 0x32, 0x30, 0x82, // 3200 + 0x02, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, // 3216 + 0x00, 0x03, 0x82, 0x02, 0x0f, 0x00, 0x30, 0x82, 0x02, 0x0a, 0x02, 0x82, 0x02, 0x01, 0x00, 0xce, // 3232 + 0xde, 0xfd, 0xa6, 0xfb, 0xec, 0xec, 0x14, 0x34, 0x3c, 0x07, 0x06, 0x5a, 0x6c, 0x59, 0xf7, 0x19, // 3248 + 0x35, 0xdd, 0xf7, 0xc1, 0x9d, 0x55, 0xaa, 0xd3, 0xcd, 0x3b, 0xa4, 0x93, 0x72, 0xef, 0x0a, 0xfa, // 3264 + 0x6d, 0x9d, 0xf6, 0xf0, 0x85, 0x80, 0x5b, 0xa1, 0x48, 0x52, 0x9f, 0x39, 0xc5, 0xb7, 0xee, 0x28, // 3280 + 0xac, 0xef, 0xcb, 0x76, 0x68, 0x14, 0xb9, 0xdf, 0xad, 0x01, 0x6c, 0x99, 0x1f, 0xc4, 0x22, 0x1d, // 3296 + 0x9f, 0xfe, 0x72, 0x77, 0xe0, 0x2c, 0x5b, 0xaf, 0xe4, 0x04, 0xbf, 0x4f, 0x72, 0xa0, 0x1a, 0x34, // 3312 + 0x98, 0xe8, 0x39, 0x68, 0xec, 0x95, 0x25, 0x7b, 0x76, 0xa1, 0xe6, 0x69, 0xb9, 0x85, 0x19, 0xbd, // 3328 + 0x89, 0x8c, 0xfe, 0xad, 0xed, 0x36, 0xea, 0x73, 0xbc, 0xff, 0x83, 0xe2, 0xcb, 0x7d, 0xc1, 0xd2, // 3344 + 0xce, 0x4a, 0xb3, 0x8d, 0x05, 0x9e, 0x8b, 0x49, 0x93, 0xdf, 0xc1, 0x5b, 0xd0, 0x6e, 0x5e, 0xf0, // 3360 + 0x2e, 0x30, 0x2e, 0x82, 0xfc, 0xfa, 0xbc, 0xb4, 0x17, 0x0a, 0x48, 0xe5, 0x88, 0x9b, 0xc5, 0x9b, // 3376 + 0x6b, 0xde, 0xb0, 0xca, 0xb4, 0x03, 0xf0, 0xda, 0xf4, 0x90, 0xb8, 0x65, 0x64, 0xf7, 0x5c, 0x4c, // 3392 + 0xad, 0xe8, 0x7e, 0x66, 0x5e, 0x99, 0xd7, 0xb8, 0xc2, 0x3e, 0xc8, 0xd0, 0x13, 0x9d, 0xad, 0xee, // 3408 + 0xe4, 0x45, 0x7b, 0x89, 0x55, 0xf7, 0x8a, 0x1f, 0x62, 0x52, 0x84, 0x12, 0xb3, 0xc2, 0x40, 0x97, // 3424 + 0xe3, 0x8a, 0x1f, 0x47, 0x91, 0xa6, 0x74, 0x5a, 0xd2, 0xf8, 0xb1, 0x63, 0x28, 0x10, 0xb8, 0xb3, // 3440 + 0x09, 0xb8, 0x56, 0x77, 0x40, 0xa2, 0x26, 0x98, 0x79, 0xc6, 0xfe, 0xdf, 0x25, 0xee, 0x3e, 0xe5, // 3456 + 0xa0, 0x7f, 0xd4, 0x61, 0x0f, 0x51, 0x4b, 0x3c, 0x3f, 0x8c, 0xda, 0xe1, 0x70, 0x74, 0xd8, 0xc2, // 3472 + 0x68, 0xa1, 0xf9, 0xc1, 0x0c, 0xe9, 0xa1, 0xe2, 0x7f, 0xbb, 0x55, 0x3c, 0x76, 0x06, 0xee, 0x6a, // 3488 + 0x4e, 0xcc, 0x92, 0x88, 0x30, 0x4d, 0x9a, 0xbd, 0x4f, 0x0b, 0x48, 0x9a, 0x84, 0xb5, 0x98, 0xa3, // 3504 + 0xd5, 0xfb, 0x73, 0xc1, 0x57, 0x61, 0xdd, 0x28, 0x56, 0x75, 0x13, 0xae, 0x87, 0x8e, 0xe7, 0x0c, // 3520 + 0x51, 0x09, 0x10, 0x75, 0x88, 0x4c, 0xbc, 0x8d, 0xf9, 0x7b, 0x3c, 0xd4, 0x22, 0x48, 0x1f, 0x2a, // 3536 + 0xdc, 0xeb, 0x6b, 0xbb, 0x44, 0xb1, 0xcb, 0x33, 0x71, 0x32, 0x46, 0xaf, 0xad, 0x4a, 0xf1, 0x8c, // 3552 + 0xe8, 0x74, 0x3a, 0xac, 0xe7, 0x1a, 0x22, 0x73, 0x80, 0xd2, 0x30, 0xf7, 0x25, 0x42, 0xc7, 0x22, // 3568 + 0x3b, 0x3b, 0x12, 0xad, 0x96, 0x2e, 0xc6, 0xc3, 0x76, 0x07, 0xaa, 0x20, 0xb7, 0x35, 0x49, 0x57, // 3584 + 0xe9, 0x92, 0x49, 0xe8, 0x76, 0x16, 0x72, 0x31, 0x67, 0x2b, 0x96, 0x7e, 0x8a, 0xa3, 0xc7, 0x94, // 3600 + 0x56, 0x22, 0xbf, 0x6a, 0x4b, 0x7e, 0x01, 0x21, 0xb2, 0x23, 0x32, 0xdf, 0xe4, 0x9a, 0x44, 0x6d, // 3616 + 0x59, 0x5b, 0x5d, 0xf5, 0x00, 0xa0, 0x1c, 0x9b, 0xc6, 0x78, 0x97, 0x8d, 0x90, 0xff, 0x9b, 0xc8, // 3632 + 0xaa, 0xb4, 0xaf, 0x11, 0x51, 0x39, 0x5e, 0xd9, 0xfb, 0x67, 0xad, 0xd5, 0x5b, 0x11, 0x9d, 0x32, // 3648 + 0x9a, 0x1b, 0xbd, 0xd5, 0xba, 0x5b, 0xa5, 0xc9, 0xcb, 0x25, 0x69, 0x53, 0x55, 0x27, 0x5c, 0xe0, // 3664 + 0xca, 0x36, 0xcb, 0x88, 0x61, 0xfb, 0x1e, 0xb7, 0xd0, 0xcb, 0xee, 0x16, 0xfb, 0xd3, 0xa6, 0x4c, // 3680 + 0xde, 0x92, 0xa5, 0xd4, 0xe2, 0xdf, 0xf5, 0x06, 0x54, 0xde, 0x2e, 0x9d, 0x4b, 0xb4, 0x93, 0x30, // 3696 + 0xaa, 0x81, 0xce, 0xdd, 0x1a, 0xdc, 0x51, 0x73, 0x0d, 0x4f, 0x70, 0xe9, 0xe5, 0xb6, 0x16, 0x21, // 3712 + 0x19, 0x79, 0xb2, 0xe6, 0x89, 0x0b, 0x75, 0x64, 0xca, 0xd5, 0xab, 0xbc, 0x09, 0xc1, 0x18, 0xa1, // 3728 + 0xff, 0xd4, 0x54, 0xa1, 0x85, 0x3c, 0xfd, 0x14, 0x24, 0x03, 0xb2, 0x87, 0xd3, 0xa4, 0xb7, 0x02, // 3744 + 0x03, 0x01, 0x00, 0x01, 0x00, 0x49, 0x00, 0x78, 0x30, 0x47, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, // 3760 + 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, 0x22, 0x30, 0x20, 0x06, 0x03, 0x55, 0x04, 0x0a, // 3776 + 0x13, 0x19, 0x47, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x20, 0x54, 0x72, 0x75, 0x73, 0x74, 0x20, 0x53, // 3792 + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x20, 0x4c, 0x4c, 0x43, 0x31, 0x14, 0x30, 0x12, 0x06, // 3808 + 0x03, 0x55, 0x04, 0x03, 0x13, 0x0b, 0x47, 0x54, 0x53, 0x20, 0x52, 0x6f, 0x6f, 0x74, 0x20, 0x52, // 3824 + 0x33, 0x30, 0x76, 0x30, 0x10, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x05, // 3840 + 0x2b, 0x81, 0x04, 0x00, 0x22, 0x03, 0x62, 0x00, 0x04, 0x1f, 0x4f, 0x33, 0x87, 0x33, 0x29, 0x8a, // 3856 + 0xa1, 0x84, 0xde, 0xcb, 0xc7, 0x21, 0x58, 0x41, 0x89, 0xea, 0x56, 0x9d, 0x2b, 0x4b, 0x85, 0xc6, // 3872 + 0x1d, 0x4c, 0x27, 0xbc, 0x7f, 0x26, 0x51, 0x72, 0x6f, 0xe2, 0x9f, 0xd6, 0xa3, 0xca, 0xcc, 0x45, // 3888 + 0x14, 0x46, 0x8b, 0xad, 0xef, 0x7e, 0x86, 0x8c, 0xec, 0xb1, 0x7e, 0x2f, 0xff, 0xa9, 0x71, 0x9d, // 3904 + 0x18, 0x84, 0x45, 0x04, 0x41, 0x55, 0x6e, 0x2b, 0xea, 0x26, 0x7f, 0xbb, 0x90, 0x01, 0xe3, 0x4b, // 3920 + 0x19, 0xba, 0xe4, 0x54, 0x96, 0x45, 0x09, 0xb1, 0xd5, 0x6c, 0x91, 0x44, 0xad, 0x84, 0x13, 0x8e, // 3936 + 0x9a, 0x8c, 0x0d, 0x80, 0x0c, 0x32, 0xf6, 0xe0, 0x27, 0x00, 0x49, 0x00, 0x78, 0x30, 0x47, 0x31, // 3952 + 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, 0x22, 0x30, 0x20, // 3968 + 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x19, 0x47, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x20, 0x54, 0x72, // 3984 + 0x75, 0x73, 0x74, 0x20, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x20, 0x4c, 0x4c, 0x43, // 4000 + 0x31, 0x14, 0x30, 0x12, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x0b, 0x47, 0x54, 0x53, 0x20, 0x52, // 4016 + 0x6f, 0x6f, 0x74, 0x20, 0x52, 0x34, 0x30, 0x76, 0x30, 0x10, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, // 4032 + 0x3d, 0x02, 0x01, 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x22, 0x03, 0x62, 0x00, 0x04, 0xf3, 0x74, // 4048 + 0x73, 0xa7, 0x68, 0x8b, 0x60, 0xae, 0x43, 0xb8, 0x35, 0xc5, 0x81, 0x30, 0x7b, 0x4b, 0x49, 0x9d, // 4064 + 0xfb, 0xc1, 0x61, 0xce, 0xe6, 0xde, 0x46, 0xbd, 0x6b, 0xd5, 0x61, 0x18, 0x35, 0xae, 0x40, 0xdd, // 4080 + 0x73, 0xf7, 0x89, 0x91, 0x30, 0x5a, 0xeb, 0x3c, 0xee, 0x85, 0x7c, 0xa2, 0x40, 0x76, 0x3b, 0xa9, // 4096 + 0xc6, 0xb8, 0x47, 0xd8, 0x2a, 0xe7, 0x92, 0x91, 0x6a, 0x73, 0xe9, 0xb1, 0x72, 0x39, 0x9f, 0x29, // 4112 + 0x9f, 0xa2, 0x98, 0xd3, 0x5f, 0x5e, 0x58, 0x86, 0x65, 0x0f, 0xa1, 0x84, 0x65, 0x06, 0xd1, 0xdc, // 4128 + 0x8b, 0xc9, 0xc7, 0x73, 0xc8, 0x8c, 0x6a, 0x2f, 0xe5, 0xc4, 0xab, 0xd1, 0x1d, 0x8a, 0x00, 0x4c, // 4144 + 0x02, 0x26, 0x30, 0x4a, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, // 4160 + 0x53, 0x31, 0x12, 0x30, 0x10, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x09, 0x49, 0x64, 0x65, 0x6e, // 4176 + 0x54, 0x72, 0x75, 0x73, 0x74, 0x31, 0x27, 0x30, 0x25, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x1e, // 4192 + 0x49, 0x64, 0x65, 0x6e, 0x54, 0x72, 0x75, 0x73, 0x74, 0x20, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x72, // 4208 + 0x63, 0x69, 0x61, 0x6c, 0x20, 0x52, 0x6f, 0x6f, 0x74, 0x20, 0x43, 0x41, 0x20, 0x31, 0x30, 0x82, // 4224 + 0x02, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, // 4240 + 0x00, 0x03, 0x82, 0x02, 0x0f, 0x00, 0x30, 0x82, 0x02, 0x0a, 0x02, 0x82, 0x02, 0x01, 0x00, 0xa7, // 4256 + 0x50, 0x19, 0xde, 0x3f, 0x99, 0x3d, 0xd4, 0x33, 0x46, 0xf1, 0x6f, 0x51, 0x61, 0x82, 0xb2, 0xa9, // 4272 + 0x4f, 0x8f, 0x67, 0x89, 0x5d, 0x84, 0xd9, 0x53, 0xdd, 0x0c, 0x28, 0xd9, 0xd7, 0xf0, 0xff, 0xae, // 4288 + 0x95, 0x43, 0x72, 0x99, 0xf9, 0xb5, 0x5d, 0x7c, 0x8a, 0xc1, 0x42, 0xe1, 0x31, 0x50, 0x74, 0xd1, // 4304 + 0x81, 0x0d, 0x7c, 0xcd, 0x9b, 0x21, 0xab, 0x43, 0xe2, 0xac, 0xad, 0x5e, 0x86, 0x6e, 0xf3, 0x09, // 4320 + 0x8a, 0x1f, 0x5a, 0x32, 0xbd, 0xa2, 0xeb, 0x94, 0xf9, 0xe8, 0x5c, 0x0a, 0xec, 0xff, 0x98, 0xd2, // 4336 + 0xaf, 0x71, 0xb3, 0xb4, 0x53, 0x9f, 0x4e, 0x87, 0xef, 0x92, 0xbc, 0xbd, 0xec, 0x4f, 0x32, 0x30, // 4352 + 0x88, 0x4b, 0x17, 0x5e, 0x57, 0xc4, 0x53, 0xc2, 0xf6, 0x02, 0x97, 0x8d, 0xd9, 0x62, 0x2b, 0xbf, // 4368 + 0x24, 0x1f, 0x62, 0x8d, 0xdf, 0xc3, 0xb8, 0x29, 0x4b, 0x49, 0x78, 0x3c, 0x93, 0x60, 0x88, 0x22, // 4384 + 0xfc, 0x99, 0xda, 0x36, 0xc8, 0xc2, 0xa2, 0xd4, 0x2c, 0x54, 0x00, 0x67, 0x35, 0x6e, 0x73, 0xbf, // 4400 + 0x02, 0x58, 0xf0, 0xa4, 0xdd, 0xe5, 0xb0, 0xa2, 0x26, 0x7a, 0xca, 0xe0, 0x36, 0xa5, 0x19, 0x16, // 4416 + 0xf5, 0xfd, 0xb7, 0xef, 0xae, 0x3f, 0x40, 0xf5, 0x6d, 0x5a, 0x04, 0xfd, 0xce, 0x34, 0xca, 0x24, // 4432 + 0xdc, 0x74, 0x23, 0x1b, 0x5d, 0x33, 0x13, 0x12, 0x5d, 0xc4, 0x01, 0x25, 0xf6, 0x30, 0xdd, 0x02, // 4448 + 0x5d, 0x9f, 0xe0, 0xd5, 0x47, 0xbd, 0xb4, 0xeb, 0x1b, 0xa1, 0xbb, 0x49, 0x49, 0xd8, 0x9f, 0x5b, // 4464 + 0x02, 0xf3, 0x8a, 0xe4, 0x24, 0x90, 0xe4, 0x62, 0x4f, 0x4f, 0xc1, 0xaf, 0x8b, 0x0e, 0x74, 0x17, // 4480 + 0xa8, 0xd1, 0x72, 0x88, 0x6a, 0x7a, 0x01, 0x49, 0xcc, 0xb4, 0x46, 0x79, 0xc6, 0x17, 0xb1, 0xda, // 4496 + 0x98, 0x1e, 0x07, 0x59, 0xfa, 0x75, 0x21, 0x85, 0x65, 0xdd, 0x90, 0x56, 0xce, 0xfb, 0xab, 0xa5, // 4512 + 0x60, 0x9d, 0xc4, 0x9d, 0xf9, 0x52, 0xb0, 0x8b, 0xbd, 0x87, 0xf9, 0x8f, 0x2b, 0x23, 0x0a, 0x23, // 4528 + 0x76, 0x3b, 0xf7, 0x33, 0xe1, 0xc9, 0x00, 0xf3, 0x69, 0xf9, 0x4b, 0xa2, 0xe0, 0x4e, 0xbc, 0x7e, // 4544 + 0x93, 0x39, 0x84, 0x07, 0xf7, 0x44, 0x70, 0x7e, 0xfe, 0x07, 0x5a, 0xe5, 0xb1, 0xac, 0xd1, 0x18, // 4560 + 0xcc, 0xf2, 0x35, 0xe5, 0x49, 0x49, 0x08, 0xca, 0x56, 0xc9, 0x3d, 0xfb, 0x0f, 0x18, 0x7d, 0x8b, // 4576 + 0x3b, 0xc1, 0x13, 0xc2, 0x4d, 0x8f, 0xc9, 0x4f, 0x0e, 0x37, 0xe9, 0x1f, 0xa1, 0x0e, 0x6a, 0xdf, // 4592 + 0x62, 0x2e, 0xcb, 0x35, 0x06, 0x51, 0x79, 0x2c, 0xc8, 0x25, 0x38, 0xf4, 0xfa, 0x4b, 0xa7, 0x89, // 4608 + 0x5c, 0x9c, 0xd2, 0xe3, 0x0d, 0x39, 0x86, 0x4a, 0x74, 0x7c, 0xd5, 0x59, 0x87, 0xc2, 0x3f, 0x4e, // 4624 + 0x0c, 0x5c, 0x52, 0xf4, 0x3d, 0xf7, 0x52, 0x82, 0xf1, 0xea, 0xa3, 0xac, 0xfd, 0x49, 0x34, 0x1a, // 4640 + 0x28, 0xf3, 0x41, 0x88, 0x3a, 0x13, 0xee, 0xe8, 0xde, 0xff, 0x99, 0x1d, 0x5f, 0xba, 0xcb, 0xe8, // 4656 + 0x1e, 0xf2, 0xb9, 0x50, 0x60, 0xc0, 0x31, 0xd3, 0x73, 0xe5, 0xef, 0xbe, 0xa0, 0xed, 0x33, 0x0b, // 4672 + 0x74, 0xbe, 0x20, 0x20, 0xc4, 0x67, 0x6c, 0xf0, 0x08, 0x03, 0x7a, 0x55, 0x80, 0x7f, 0x46, 0x4e, // 4688 + 0x96, 0xa7, 0xf4, 0x1e, 0x3e, 0xe1, 0xf6, 0xd8, 0x09, 0xe1, 0x33, 0x64, 0x2b, 0x63, 0xd7, 0x32, // 4704 + 0x5e, 0x9f, 0xf9, 0xc0, 0x7b, 0x0f, 0x78, 0x6f, 0x97, 0xbc, 0x93, 0x9a, 0xf9, 0x9c, 0x12, 0x90, // 4720 + 0x78, 0x7a, 0x80, 0x87, 0x15, 0xd7, 0x72, 0x74, 0x9c, 0x55, 0x74, 0x78, 0xb1, 0xba, 0xe1, 0x6e, // 4736 + 0x70, 0x04, 0xba, 0x4f, 0xa0, 0xba, 0x68, 0xc3, 0x7b, 0xff, 0x31, 0xf0, 0x73, 0x3d, 0x3d, 0x94, // 4752 + 0x2a, 0xb1, 0x0b, 0x41, 0x0e, 0xa0, 0xfe, 0x4d, 0x88, 0x65, 0x6b, 0x79, 0x33, 0xb4, 0xd7, 0x02, // 4768 + 0x03, 0x01, 0x00, 0x01, 0x00, 0x4e, 0x01, 0x26, 0x30, 0x4c, 0x31, 0x20, 0x30, 0x1e, 0x06, 0x03, // 4784 + 0x55, 0x04, 0x0b, 0x13, 0x17, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x53, 0x69, 0x67, 0x6e, 0x20, // 4800 + 0x52, 0x6f, 0x6f, 0x74, 0x20, 0x43, 0x41, 0x20, 0x2d, 0x20, 0x52, 0x33, 0x31, 0x13, 0x30, 0x11, // 4816 + 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x0a, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x53, 0x69, 0x67, // 4832 + 0x6e, 0x31, 0x13, 0x30, 0x11, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x0a, 0x47, 0x6c, 0x6f, 0x62, // 4848 + 0x61, 0x6c, 0x53, 0x69, 0x67, 0x6e, 0x30, 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, // 4864 + 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0f, 0x00, 0x30, 0x82, // 4880 + 0x01, 0x0a, 0x02, 0x82, 0x01, 0x01, 0x00, 0xcc, 0x25, 0x76, 0x90, 0x79, 0x06, 0x78, 0x22, 0x16, // 4896 + 0xf5, 0xc0, 0x83, 0xb6, 0x84, 0xca, 0x28, 0x9e, 0xfd, 0x05, 0x76, 0x11, 0xc5, 0xad, 0x88, 0x72, // 4912 + 0xfc, 0x46, 0x02, 0x43, 0xc7, 0xb2, 0x8a, 0x9d, 0x04, 0x5f, 0x24, 0xcb, 0x2e, 0x4b, 0xe1, 0x60, // 4928 + 0x82, 0x46, 0xe1, 0x52, 0xab, 0x0c, 0x81, 0x47, 0x70, 0x6c, 0xdd, 0x64, 0xd1, 0xeb, 0xf5, 0x2c, // 4944 + 0xa3, 0x0f, 0x82, 0x3d, 0x0c, 0x2b, 0xae, 0x97, 0xd7, 0xb6, 0x14, 0x86, 0x10, 0x79, 0xbb, 0x3b, // 4960 + 0x13, 0x80, 0x77, 0x8c, 0x08, 0xe1, 0x49, 0xd2, 0x6a, 0x62, 0x2f, 0x1f, 0x5e, 0xfa, 0x96, 0x68, // 4976 + 0xdf, 0x89, 0x27, 0x95, 0x38, 0x9f, 0x06, 0xd7, 0x3e, 0xc9, 0xcb, 0x26, 0x59, 0x0d, 0x73, 0xde, // 4992 + 0xb0, 0xc8, 0xe9, 0x26, 0x0e, 0x83, 0x15, 0xc6, 0xef, 0x5b, 0x8b, 0xd2, 0x04, 0x60, 0xca, 0x49, // 5008 + 0xa6, 0x28, 0xf6, 0x69, 0x3b, 0xf6, 0xcb, 0xc8, 0x28, 0x91, 0xe5, 0x9d, 0x8a, 0x61, 0x57, 0x37, // 5024 + 0xac, 0x74, 0x14, 0xdc, 0x74, 0xe0, 0x3a, 0xee, 0x72, 0x2f, 0x2e, 0x9c, 0xfb, 0xd0, 0xbb, 0xbf, // 5040 + 0xf5, 0x3d, 0x00, 0xe1, 0x06, 0x33, 0xe8, 0x82, 0x2b, 0xae, 0x53, 0xa6, 0x3a, 0x16, 0x73, 0x8c, // 5056 + 0xdd, 0x41, 0x0e, 0x20, 0x3a, 0xc0, 0xb4, 0xa7, 0xa1, 0xe9, 0xb2, 0x4f, 0x90, 0x2e, 0x32, 0x60, // 5072 + 0xe9, 0x57, 0xcb, 0xb9, 0x04, 0x92, 0x68, 0x68, 0xe5, 0x38, 0x26, 0x60, 0x75, 0xb2, 0x9f, 0x77, // 5088 + 0xff, 0x91, 0x14, 0xef, 0xae, 0x20, 0x49, 0xfc, 0xad, 0x40, 0x15, 0x48, 0xd1, 0x02, 0x31, 0x61, // 5104 + 0x19, 0x5e, 0xb8, 0x97, 0xef, 0xad, 0x77, 0xb7, 0x64, 0x9a, 0x7a, 0xbf, 0x5f, 0xc1, 0x13, 0xef, // 5120 + 0x9b, 0x62, 0xfb, 0x0d, 0x6c, 0xe0, 0x54, 0x69, 0x16, 0xa9, 0x03, 0xda, 0x6e, 0xe9, 0x83, 0x93, // 5136 + 0x71, 0x76, 0xc6, 0x69, 0x85, 0x82, 0x17, 0x02, 0x03, 0x01, 0x00, 0x01, 0x00, 0x4e, 0x02, 0x26, // 5152 + 0x30, 0x4c, 0x31, 0x20, 0x30, 0x1e, 0x06, 0x03, 0x55, 0x04, 0x0b, 0x13, 0x17, 0x47, 0x6c, 0x6f, // 5168 + 0x62, 0x61, 0x6c, 0x53, 0x69, 0x67, 0x6e, 0x20, 0x52, 0x6f, 0x6f, 0x74, 0x20, 0x43, 0x41, 0x20, // 5184 + 0x2d, 0x20, 0x52, 0x36, 0x31, 0x13, 0x30, 0x11, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x0a, 0x47, // 5200 + 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x53, 0x69, 0x67, 0x6e, 0x31, 0x13, 0x30, 0x11, 0x06, 0x03, 0x55, // 5216 + 0x04, 0x03, 0x13, 0x0a, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x53, 0x69, 0x67, 0x6e, 0x30, 0x82, // 5232 + 0x02, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, // 5248 + 0x00, 0x03, 0x82, 0x02, 0x0f, 0x00, 0x30, 0x82, 0x02, 0x0a, 0x02, 0x82, 0x02, 0x01, 0x00, 0x95, // 5264 + 0x07, 0xe8, 0x73, 0xca, 0x66, 0xf9, 0xec, 0x14, 0xca, 0x7b, 0x3c, 0xf7, 0x0d, 0x08, 0xf1, 0xb4, // 5280 + 0x45, 0x0b, 0x2c, 0x82, 0xb4, 0x48, 0xc6, 0xeb, 0x5b, 0x3c, 0xae, 0x83, 0xb8, 0x41, 0x92, 0x33, // 5296 + 0x14, 0xa4, 0x6f, 0x7f, 0xe9, 0x2a, 0xcc, 0xc6, 0xb0, 0x88, 0x6b, 0xc5, 0xb6, 0x89, 0xd1, 0xc6, // 5312 + 0xb2, 0xff, 0x14, 0xce, 0x51, 0x14, 0x21, 0xec, 0x4a, 0xdd, 0x1b, 0x5a, 0xc6, 0xd6, 0x87, 0xee, // 5328 + 0x4d, 0x3a, 0x15, 0x06, 0xed, 0x64, 0x66, 0x0b, 0x92, 0x80, 0xca, 0x44, 0xde, 0x73, 0x94, 0x4e, // 5344 + 0xf3, 0xa7, 0x89, 0x7f, 0x4f, 0x78, 0x63, 0x08, 0xc8, 0x12, 0x50, 0x6d, 0x42, 0x66, 0x2f, 0x4d, // 5360 + 0xb9, 0x79, 0x28, 0x4d, 0x52, 0x1a, 0x8a, 0x1a, 0x80, 0xb7, 0x19, 0x81, 0x0e, 0x7e, 0xc4, 0x8a, // 5376 + 0xbc, 0x64, 0x4c, 0x21, 0x1c, 0x43, 0x68, 0xd7, 0x3d, 0x3c, 0x8a, 0xc5, 0xb2, 0x66, 0xd5, 0x90, // 5392 + 0x9a, 0xb7, 0x31, 0x06, 0xc5, 0xbe, 0xe2, 0x6d, 0x32, 0x06, 0xa6, 0x1e, 0xf9, 0xb9, 0xeb, 0xaa, // 5408 + 0xa3, 0xb8, 0xbf, 0xbe, 0x82, 0x63, 0x50, 0xd0, 0xf0, 0x18, 0x89, 0xdf, 0xe4, 0x0f, 0x79, 0xf5, // 5424 + 0xea, 0xa2, 0x1f, 0x2a, 0xd2, 0x70, 0x2e, 0x7b, 0xe7, 0xbc, 0x93, 0xbb, 0x6d, 0x53, 0xe2, 0x48, // 5440 + 0x7c, 0x8c, 0x10, 0x07, 0x38, 0xff, 0x66, 0xb2, 0x77, 0x61, 0x7e, 0xe0, 0xea, 0x8c, 0x3c, 0xaa, // 5456 + 0xb4, 0xa4, 0xf6, 0xf3, 0x95, 0x4a, 0x12, 0x07, 0x6d, 0xfd, 0x8c, 0xb2, 0x89, 0xcf, 0xd0, 0xa0, // 5472 + 0x61, 0x77, 0xc8, 0x58, 0x74, 0xb0, 0xd4, 0x23, 0x3a, 0xf7, 0x5d, 0x3a, 0xca, 0xa2, 0xdb, 0x9d, // 5488 + 0x09, 0xde, 0x5d, 0x44, 0x2d, 0x90, 0xf1, 0x81, 0xcd, 0x57, 0x92, 0xfa, 0x7e, 0xbc, 0x50, 0x04, // 5504 + 0x63, 0x34, 0xdf, 0x6b, 0x93, 0x18, 0xbe, 0x6b, 0x36, 0xb2, 0x39, 0xe4, 0xac, 0x24, 0x36, 0xb7, // 5520 + 0xf0, 0xef, 0xb6, 0x1c, 0x13, 0x57, 0x93, 0xb6, 0xde, 0xb2, 0xf8, 0xe2, 0x85, 0xb7, 0x73, 0xa2, // 5536 + 0xb8, 0x35, 0xaa, 0x45, 0xf2, 0xe0, 0x9d, 0x36, 0xa1, 0x6f, 0x54, 0x8a, 0xf1, 0x72, 0x56, 0x6e, // 5552 + 0x2e, 0x88, 0xc5, 0x51, 0x42, 0x44, 0x15, 0x94, 0xee, 0xa3, 0xc5, 0x38, 0x96, 0x9b, 0x4e, 0x4e, // 5568 + 0x5a, 0x0b, 0x47, 0xf3, 0x06, 0x36, 0x49, 0x77, 0x30, 0xbc, 0x71, 0x37, 0xe5, 0xa6, 0xec, 0x21, // 5584 + 0x08, 0x75, 0xfc, 0xe6, 0x61, 0x16, 0x3f, 0x77, 0xd5, 0xd9, 0x91, 0x97, 0x84, 0x0a, 0x6c, 0xd4, // 5600 + 0x02, 0x4d, 0x74, 0xc0, 0x14, 0xed, 0xfd, 0x39, 0xfb, 0x83, 0xf2, 0x5e, 0x14, 0xa1, 0x04, 0xb0, // 5616 + 0x0b, 0xe9, 0xfe, 0xee, 0x8f, 0xe1, 0x6e, 0x0b, 0xb2, 0x08, 0xb3, 0x61, 0x66, 0x09, 0x6a, 0xb1, // 5632 + 0x06, 0x3a, 0x65, 0x96, 0x59, 0xc0, 0xf0, 0x35, 0xfd, 0xc9, 0xda, 0x28, 0x8d, 0x1a, 0x11, 0x87, // 5648 + 0x70, 0x81, 0x0a, 0xa8, 0x9a, 0x75, 0x1d, 0x9e, 0x3a, 0x86, 0x05, 0x00, 0x9e, 0xdb, 0x80, 0xd6, // 5664 + 0x25, 0xf9, 0xdc, 0x05, 0x9e, 0x27, 0x59, 0x4c, 0x76, 0x39, 0x5b, 0xea, 0xf9, 0xa5, 0xa1, 0xd8, // 5680 + 0x83, 0x0f, 0xd1, 0xff, 0xdf, 0x30, 0x11, 0xf9, 0x85, 0xcf, 0x33, 0x48, 0xf5, 0xca, 0x6d, 0x64, // 5696 + 0x14, 0x2c, 0x7a, 0x58, 0x4f, 0xd3, 0x4b, 0x08, 0x49, 0xc5, 0x95, 0x64, 0x1a, 0x63, 0x0e, 0x79, // 5712 + 0x3d, 0xf5, 0xb3, 0x8c, 0xca, 0x58, 0xad, 0x9c, 0x42, 0x45, 0x79, 0x6e, 0x0e, 0x87, 0x19, 0x5c, // 5728 + 0x54, 0xb1, 0x65, 0xb6, 0xbf, 0x8c, 0x9b, 0xdc, 0x13, 0xe9, 0x0d, 0x6f, 0xb8, 0x2e, 0xdc, 0x67, // 5744 + 0x6e, 0xc9, 0x8b, 0x11, 0xb5, 0x84, 0x14, 0x8a, 0x00, 0x19, 0x70, 0x83, 0x79, 0x91, 0x97, 0x91, // 5760 + 0xd4, 0x1a, 0x27, 0xbf, 0x37, 0x1e, 0x32, 0x07, 0xd8, 0x14, 0x63, 0x3c, 0x28, 0x4c, 0xaf, 0x02, // 5776 + 0x03, 0x01, 0x00, 0x01, 0x00, 0x4f, 0x02, 0x26, 0x30, 0x4d, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, // 5792 + 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, 0x12, 0x30, 0x10, 0x06, 0x03, 0x55, 0x04, 0x0a, // 5808 + 0x13, 0x09, 0x49, 0x64, 0x65, 0x6e, 0x54, 0x72, 0x75, 0x73, 0x74, 0x31, 0x2a, 0x30, 0x28, 0x06, // 5824 + 0x03, 0x55, 0x04, 0x03, 0x13, 0x21, 0x49, 0x64, 0x65, 0x6e, 0x54, 0x72, 0x75, 0x73, 0x74, 0x20, // 5840 + 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x20, 0x53, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x20, 0x52, 0x6f, // 5856 + 0x6f, 0x74, 0x20, 0x43, 0x41, 0x20, 0x31, 0x30, 0x82, 0x02, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, // 5872 + 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x02, 0x0f, 0x00, 0x30, // 5888 + 0x82, 0x02, 0x0a, 0x02, 0x82, 0x02, 0x01, 0x00, 0xb6, 0x22, 0x94, 0xfc, 0xa4, 0x48, 0xaf, 0xe8, // 5904 + 0x47, 0x6b, 0x0a, 0xfb, 0x27, 0x76, 0xe4, 0xf2, 0x3f, 0x8a, 0x3b, 0x7a, 0x4a, 0x2c, 0x31, 0x2a, // 5920 + 0x8c, 0x8d, 0xb0, 0xa9, 0xc3, 0x31, 0x6b, 0xa8, 0x77, 0x76, 0x84, 0x26, 0xb6, 0xac, 0x81, 0x42, // 5936 + 0x0d, 0x08, 0xeb, 0x55, 0x58, 0xbb, 0x7a, 0xf8, 0xbc, 0x65, 0x7d, 0xf2, 0xa0, 0x6d, 0x8b, 0xa8, // 5952 + 0x47, 0xe9, 0x62, 0x76, 0x1e, 0x11, 0xee, 0x08, 0x14, 0xd1, 0xb2, 0x44, 0x16, 0xf4, 0xea, 0xd0, // 5968 + 0xfa, 0x1e, 0x2f, 0x5e, 0xdb, 0xcb, 0x73, 0x41, 0xae, 0xbc, 0x00, 0xb0, 0x4a, 0x2b, 0x40, 0xb2, // 5984 + 0xac, 0xe1, 0x3b, 0x4b, 0xc2, 0x2d, 0x9d, 0xe4, 0xa1, 0x9b, 0xec, 0x1a, 0x3a, 0x1e, 0xf0, 0x08, // 6000 + 0xb3, 0xd0, 0xe4, 0x24, 0x35, 0x07, 0x9f, 0x9c, 0xb4, 0xc9, 0x52, 0x6d, 0xdb, 0x07, 0xca, 0x8f, // 6016 + 0xb5, 0x5b, 0xf0, 0x83, 0xf3, 0x4f, 0xc7, 0x2d, 0xa5, 0xc8, 0xad, 0xcb, 0x95, 0x20, 0xa4, 0x31, // 6032 + 0x28, 0x57, 0x58, 0x5a, 0xe4, 0x8d, 0x1b, 0x9a, 0xab, 0x9e, 0x0d, 0x0c, 0xf2, 0x0a, 0x33, 0x39, // 6048 + 0x22, 0x39, 0x0a, 0x97, 0x2e, 0xf3, 0x53, 0x77, 0xb9, 0x44, 0x45, 0xfd, 0x84, 0xcb, 0x36, 0x20, // 6064 + 0x81, 0x59, 0x2d, 0x9a, 0x6f, 0x6d, 0x48, 0x48, 0x61, 0xca, 0x4c, 0xdf, 0x53, 0xd1, 0xaf, 0x52, // 6080 + 0xbc, 0x44, 0x9f, 0xab, 0x2f, 0x6b, 0x83, 0x72, 0xef, 0x75, 0x80, 0xda, 0x06, 0x33, 0x1b, 0x5d, // 6096 + 0xc8, 0xda, 0x63, 0xc6, 0x4d, 0xcd, 0xac, 0x66, 0x31, 0xcd, 0xd1, 0xde, 0x3e, 0x87, 0x10, 0x36, // 6112 + 0xe1, 0xb9, 0xa4, 0x7a, 0xef, 0x60, 0x50, 0xb2, 0xcb, 0xca, 0xa6, 0x56, 0xe0, 0x37, 0xaf, 0xab, // 6128 + 0x34, 0x13, 0x39, 0x25, 0xe8, 0x39, 0x66, 0xe4, 0x98, 0x7a, 0xaa, 0x12, 0x98, 0x9c, 0x59, 0x66, // 6144 + 0x86, 0x3e, 0xad, 0xf1, 0xb0, 0xca, 0x3e, 0x06, 0x0f, 0x7b, 0xf0, 0x11, 0x4b, 0x37, 0xa0, 0x44, // 6160 + 0x6d, 0x7b, 0xcb, 0xa8, 0x8c, 0x71, 0xf4, 0xd5, 0xb5, 0x91, 0x36, 0xcc, 0xf0, 0x15, 0xc6, 0x2b, // 6176 + 0xde, 0x51, 0x17, 0xb1, 0x97, 0x4c, 0x50, 0x3d, 0xb1, 0x95, 0x59, 0x7c, 0x05, 0x7d, 0x2d, 0x21, // 6192 + 0xd5, 0x00, 0xbf, 0x01, 0x67, 0xa2, 0x5e, 0x7b, 0xa6, 0x5c, 0xf2, 0xf7, 0x22, 0xf1, 0x90, 0x0d, // 6208 + 0x93, 0xdb, 0xaa, 0x44, 0x51, 0x66, 0xcc, 0x7d, 0x76, 0x03, 0xeb, 0x6a, 0xa8, 0x2a, 0x38, 0x19, // 6224 + 0x97, 0x76, 0x0d, 0x6b, 0x8a, 0x61, 0xf9, 0xbc, 0xf6, 0xee, 0x76, 0xfd, 0x70, 0x2b, 0xdd, 0x29, // 6240 + 0x3c, 0xf8, 0x0a, 0x1e, 0x5b, 0x42, 0x1c, 0x8b, 0x56, 0x2f, 0x55, 0x1b, 0x1c, 0xa1, 0x2e, 0xb5, // 6256 + 0xc7, 0x16, 0xe6, 0xf8, 0xaa, 0x3c, 0x92, 0x8e, 0x69, 0xb6, 0x01, 0xc1, 0xb5, 0x86, 0x9d, 0x89, // 6272 + 0x0f, 0x0b, 0x38, 0x94, 0x54, 0xe8, 0xea, 0xdc, 0x9e, 0x3d, 0x25, 0xbc, 0x53, 0x26, 0xed, 0xd5, // 6288 + 0xab, 0x39, 0xaa, 0xc5, 0x40, 0x4c, 0x54, 0xab, 0xb2, 0xb4, 0xd9, 0xd9, 0xf8, 0xd7, 0x72, 0xdb, // 6304 + 0x1c, 0xbc, 0x6d, 0xbd, 0x65, 0x5f, 0xef, 0x88, 0x35, 0x2a, 0x66, 0x2f, 0xee, 0xf6, 0xb3, 0x65, // 6320 + 0xf0, 0x33, 0x8d, 0x7c, 0x98, 0x41, 0x69, 0x46, 0x0f, 0x43, 0x1c, 0x69, 0xfa, 0x9b, 0xb5, 0xd0, // 6336 + 0x61, 0x6a, 0xcd, 0xca, 0x4b, 0xd9, 0x4c, 0x90, 0x46, 0xab, 0x15, 0x59, 0xa1, 0x47, 0x54, 0x29, // 6352 + 0x2e, 0x83, 0x28, 0x5f, 0x1c, 0xc2, 0xa2, 0xab, 0x72, 0x17, 0x00, 0x06, 0x8e, 0x45, 0xec, 0x8b, // 6368 + 0xe2, 0x33, 0x3d, 0x7f, 0xda, 0x19, 0x44, 0xe4, 0x62, 0x72, 0xc3, 0xdf, 0x22, 0xc6, 0xf2, 0x56, // 6384 + 0xd4, 0xdd, 0x5f, 0x95, 0x72, 0xed, 0x6d, 0x5f, 0xf7, 0x48, 0x03, 0x5b, 0xfd, 0xc5, 0x2a, 0xa0, // 6400 + 0xf6, 0x73, 0x23, 0x84, 0x10, 0x1b, 0x01, 0xe7, 0x02, 0x03, 0x01, 0x00, 0x01, 0x00, 0x4f, 0x02, // 6416 + 0x26, 0x30, 0x4d, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, // 6432 + 0x31, 0x17, 0x30, 0x15, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x0e, 0x44, 0x69, 0x67, 0x69, 0x43, // 6448 + 0x65, 0x72, 0x74, 0x2c, 0x20, 0x49, 0x6e, 0x63, 0x2e, 0x31, 0x25, 0x30, 0x23, 0x06, 0x03, 0x55, // 6464 + 0x04, 0x03, 0x13, 0x1c, 0x44, 0x69, 0x67, 0x69, 0x43, 0x65, 0x72, 0x74, 0x20, 0x54, 0x4c, 0x53, // 6480 + 0x20, 0x52, 0x53, 0x41, 0x34, 0x30, 0x39, 0x36, 0x20, 0x52, 0x6f, 0x6f, 0x74, 0x20, 0x47, 0x35, // 6496 + 0x30, 0x82, 0x02, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, // 6512 + 0x01, 0x05, 0x00, 0x03, 0x82, 0x02, 0x0f, 0x00, 0x30, 0x82, 0x02, 0x0a, 0x02, 0x82, 0x02, 0x01, // 6528 + 0x00, 0xb3, 0xd0, 0xf4, 0xc9, 0x79, 0x11, 0x9d, 0xfd, 0xfc, 0x66, 0x81, 0xe7, 0xcc, 0xd5, 0xe4, // 6544 + 0xbc, 0xec, 0x81, 0x3e, 0x6a, 0x35, 0x8e, 0x2e, 0xb7, 0xe7, 0xde, 0xaf, 0xf9, 0x07, 0x4d, 0xcf, // 6560 + 0x30, 0x9d, 0xea, 0x09, 0x0b, 0x99, 0xbd, 0x6c, 0x57, 0xda, 0x18, 0x4a, 0xb8, 0x78, 0xac, 0x3a, // 6576 + 0x39, 0xa8, 0xa6, 0x48, 0xac, 0x2e, 0x72, 0xe5, 0xbd, 0xeb, 0xf1, 0x1a, 0xcd, 0xe7, 0xa4, 0x03, // 6592 + 0xa9, 0x3f, 0x11, 0xb4, 0xd8, 0x2f, 0x89, 0x16, 0xfb, 0x94, 0x01, 0x3d, 0xbb, 0x2f, 0xf8, 0x13, // 6608 + 0x05, 0xa1, 0x78, 0x1c, 0x8e, 0x28, 0xe0, 0x45, 0xe0, 0x83, 0xf4, 0x59, 0x1b, 0x95, 0xb3, 0xae, // 6624 + 0x7e, 0x03, 0x45, 0xe5, 0xbe, 0xc2, 0x42, 0xfe, 0xee, 0xf2, 0x3c, 0xb6, 0x85, 0x13, 0x98, 0x32, // 6640 + 0x9d, 0x16, 0xa8, 0x29, 0xc2, 0x0b, 0x1c, 0x38, 0xdc, 0x9f, 0x31, 0x77, 0x5c, 0xbf, 0x27, 0xa3, // 6656 + 0xfc, 0x27, 0xac, 0xb7, 0x2b, 0xbd, 0x74, 0x9b, 0x17, 0x2d, 0xf2, 0x81, 0xda, 0x5d, 0xb0, 0xe1, // 6672 + 0x23, 0x17, 0x3e, 0x88, 0x4a, 0x12, 0x23, 0xd0, 0xea, 0xcf, 0x9d, 0xde, 0x03, 0x17, 0xb1, 0x42, // 6688 + 0x4a, 0xa0, 0x16, 0x4c, 0xa4, 0x6d, 0x93, 0xe9, 0x3f, 0x3a, 0xee, 0x3a, 0x7c, 0x9d, 0x58, 0x9d, // 6704 + 0xf4, 0x4e, 0x8f, 0xfc, 0x3b, 0x23, 0xc8, 0x6d, 0xb8, 0xe2, 0x05, 0xda, 0xcc, 0xeb, 0xec, 0xc3, // 6720 + 0x31, 0xf4, 0xd7, 0xa7, 0x29, 0x54, 0x80, 0xcf, 0x44, 0x5b, 0x4c, 0x6f, 0x30, 0x9e, 0xf3, 0xcc, // 6736 + 0xdd, 0x1f, 0x94, 0x43, 0x9d, 0x4d, 0x7f, 0x70, 0x70, 0x0d, 0xd4, 0x3a, 0xd1, 0x37, 0xf0, 0x6c, // 6752 + 0x9d, 0x9b, 0xc0, 0x14, 0x93, 0x58, 0xef, 0xcd, 0x41, 0x38, 0x75, 0xbc, 0x13, 0x03, 0x95, 0x7c, // 6768 + 0x7f, 0xe3, 0x5c, 0xe9, 0xd5, 0x0d, 0xd5, 0xe2, 0x7c, 0x10, 0x62, 0xaa, 0x6b, 0xf0, 0x3d, 0x76, // 6784 + 0xf3, 0x3f, 0xa3, 0xe8, 0xb0, 0xc1, 0xfd, 0xef, 0xaa, 0x57, 0x4d, 0xac, 0x86, 0xa7, 0x18, 0xb4, // 6800 + 0x29, 0xc1, 0x2c, 0x0e, 0xbf, 0x64, 0xbe, 0x29, 0x8c, 0xd8, 0x02, 0x2d, 0xcd, 0x5c, 0x2f, 0xf2, // 6816 + 0x7f, 0xef, 0x15, 0xf4, 0x0c, 0x15, 0xac, 0x0a, 0xb0, 0xf1, 0xd3, 0x0d, 0x4f, 0x6a, 0x4d, 0x77, // 6832 + 0x97, 0x01, 0xa0, 0xf1, 0x66, 0xb7, 0xb7, 0xce, 0xef, 0xce, 0xec, 0xec, 0xa5, 0x75, 0xca, 0xac, // 6848 + 0xe3, 0xe1, 0x63, 0xf7, 0xb8, 0xa1, 0x04, 0xc8, 0xbc, 0x7b, 0x3f, 0x5d, 0x2d, 0x16, 0x22, 0x56, // 6864 + 0xed, 0x48, 0x49, 0xfe, 0xa7, 0x2f, 0x79, 0x30, 0x25, 0x9b, 0xba, 0x6b, 0x2d, 0x3f, 0x9d, 0x3b, // 6880 + 0xc4, 0x17, 0xe7, 0x1d, 0x2e, 0xfb, 0xf2, 0xcf, 0xa6, 0xfc, 0xe3, 0x14, 0x2c, 0x96, 0x98, 0x21, // 6896 + 0x8c, 0xb4, 0x91, 0xe9, 0x19, 0x60, 0x83, 0xf2, 0x30, 0x2b, 0x06, 0x73, 0x50, 0xd5, 0x98, 0x3b, // 6912 + 0x06, 0xe9, 0xc7, 0x8a, 0x0c, 0x60, 0x8c, 0x28, 0xf8, 0x52, 0x9b, 0x6e, 0xe1, 0xf6, 0x4d, 0xbb, // 6928 + 0x06, 0x24, 0x9b, 0xd7, 0x2b, 0x26, 0x3f, 0xfd, 0x2a, 0x2f, 0x71, 0xf5, 0xd6, 0x24, 0xbe, 0x7f, // 6944 + 0x31, 0x9e, 0x0f, 0x6d, 0xe8, 0x8f, 0x4f, 0x4d, 0xa3, 0x3f, 0xff, 0x35, 0xea, 0xdf, 0x49, 0x5e, // 6960 + 0x41, 0x8f, 0x86, 0xf9, 0xf1, 0x77, 0x79, 0x4b, 0x1b, 0xb4, 0xa3, 0x5e, 0x2f, 0xfb, 0x46, 0x02, // 6976 + 0xd0, 0x66, 0x13, 0x5e, 0x5e, 0x85, 0x4f, 0xce, 0xd8, 0x70, 0x88, 0x7b, 0xce, 0x01, 0xb5, 0x96, // 6992 + 0x97, 0xd7, 0xcd, 0x7d, 0xfd, 0x82, 0xf8, 0xc2, 0x24, 0xc1, 0xca, 0x01, 0x39, 0x4f, 0x8d, 0xa2, // 7008 + 0xc1, 0x14, 0x40, 0x1f, 0x9c, 0x66, 0xd5, 0x0c, 0x09, 0x46, 0xd6, 0xf2, 0xd0, 0xd1, 0x48, 0x76, // 7024 + 0x56, 0x3a, 0x43, 0xcb, 0xb6, 0x0a, 0x11, 0x39, 0xba, 0x8c, 0x13, 0x6c, 0x06, 0xb5, 0x9e, 0xcf, // 7040 + 0xeb, 0x02, 0x03, 0x01, 0x00, 0x01, 0x00, 0x50, 0x00, 0x78, 0x30, 0x4e, 0x31, 0x0b, 0x30, 0x09, // 7056 + 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, 0x17, 0x30, 0x15, 0x06, 0x03, 0x55, // 7072 + 0x04, 0x0a, 0x13, 0x0e, 0x44, 0x69, 0x67, 0x69, 0x43, 0x65, 0x72, 0x74, 0x2c, 0x20, 0x49, 0x6e, // 7088 + 0x63, 0x2e, 0x31, 0x26, 0x30, 0x24, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x1d, 0x44, 0x69, 0x67, // 7104 + 0x69, 0x43, 0x65, 0x72, 0x74, 0x20, 0x54, 0x4c, 0x53, 0x20, 0x45, 0x43, 0x43, 0x20, 0x50, 0x33, // 7120 + 0x38, 0x34, 0x20, 0x52, 0x6f, 0x6f, 0x74, 0x20, 0x47, 0x35, 0x30, 0x76, 0x30, 0x10, 0x06, 0x07, // 7136 + 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x22, 0x03, 0x62, // 7152 + 0x00, 0x04, 0xc1, 0x44, 0xa1, 0xcf, 0x11, 0x97, 0x50, 0x9a, 0xde, 0x23, 0x82, 0x35, 0x07, 0xcd, // 7168 + 0xd0, 0xcb, 0x18, 0x9d, 0xd2, 0xf1, 0x7f, 0x77, 0x35, 0x4f, 0x3b, 0xdd, 0x94, 0x72, 0x52, 0xed, // 7184 + 0xc2, 0x3b, 0xf8, 0xec, 0xfa, 0x7b, 0x6b, 0x58, 0x20, 0xec, 0x99, 0xae, 0xc9, 0xfc, 0x68, 0xb3, // 7200 + 0x75, 0xb9, 0xdb, 0x09, 0xec, 0xc8, 0x13, 0xf5, 0x4e, 0xc6, 0x0a, 0x1d, 0x66, 0x30, 0x4c, 0xbb, // 7216 + 0x1f, 0x47, 0x0a, 0x3c, 0x61, 0x10, 0x42, 0x29, 0x7c, 0xa5, 0x08, 0x0e, 0xe0, 0x22, 0xe9, 0xd3, // 7232 + 0x35, 0x68, 0xce, 0x9b, 0x63, 0x9f, 0x84, 0xb5, 0x99, 0x4d, 0x58, 0xa0, 0x8e, 0xf5, 0x54, 0xe7, // 7248 + 0x95, 0xc9, 0x00, 0x51, 0x02, 0x26, 0x30, 0x4f, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, // 7264 + 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, 0x29, 0x30, 0x27, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x20, // 7280 + 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x20, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, // 7296 + 0x79, 0x20, 0x52, 0x65, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x20, 0x47, 0x72, 0x6f, 0x75, 0x70, // 7312 + 0x31, 0x15, 0x30, 0x13, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x0c, 0x49, 0x53, 0x52, 0x47, 0x20, // 7328 + 0x52, 0x6f, 0x6f, 0x74, 0x20, 0x58, 0x31, 0x30, 0x82, 0x02, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, // 7344 + 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x02, 0x0f, 0x00, 0x30, // 7360 + 0x82, 0x02, 0x0a, 0x02, 0x82, 0x02, 0x01, 0x00, 0xad, 0xe8, 0x24, 0x73, 0xf4, 0x14, 0x37, 0xf3, // 7376 + 0x9b, 0x9e, 0x2b, 0x57, 0x28, 0x1c, 0x87, 0xbe, 0xdc, 0xb7, 0xdf, 0x38, 0x90, 0x8c, 0x6e, 0x3c, // 7392 + 0xe6, 0x57, 0xa0, 0x78, 0xf7, 0x75, 0xc2, 0xa2, 0xfe, 0xf5, 0x6a, 0x6e, 0xf6, 0x00, 0x4f, 0x28, // 7408 + 0xdb, 0xde, 0x68, 0x86, 0x6c, 0x44, 0x93, 0xb6, 0xb1, 0x63, 0xfd, 0x14, 0x12, 0x6b, 0xbf, 0x1f, // 7424 + 0xd2, 0xea, 0x31, 0x9b, 0x21, 0x7e, 0xd1, 0x33, 0x3c, 0xba, 0x48, 0xf5, 0xdd, 0x79, 0xdf, 0xb3, // 7440 + 0xb8, 0xff, 0x12, 0xf1, 0x21, 0x9a, 0x4b, 0xc1, 0x8a, 0x86, 0x71, 0x69, 0x4a, 0x66, 0x66, 0x6c, // 7456 + 0x8f, 0x7e, 0x3c, 0x70, 0xbf, 0xad, 0x29, 0x22, 0x06, 0xf3, 0xe4, 0xc0, 0xe6, 0x80, 0xae, 0xe2, // 7472 + 0x4b, 0x8f, 0xb7, 0x99, 0x7e, 0x94, 0x03, 0x9f, 0xd3, 0x47, 0x97, 0x7c, 0x99, 0x48, 0x23, 0x53, // 7488 + 0xe8, 0x38, 0xae, 0x4f, 0x0a, 0x6f, 0x83, 0x2e, 0xd1, 0x49, 0x57, 0x8c, 0x80, 0x74, 0xb6, 0xda, // 7504 + 0x2f, 0xd0, 0x38, 0x8d, 0x7b, 0x03, 0x70, 0x21, 0x1b, 0x75, 0xf2, 0x30, 0x3c, 0xfa, 0x8f, 0xae, // 7520 + 0xdd, 0xda, 0x63, 0xab, 0xeb, 0x16, 0x4f, 0xc2, 0x8e, 0x11, 0x4b, 0x7e, 0xcf, 0x0b, 0xe8, 0xff, // 7536 + 0xb5, 0x77, 0x2e, 0xf4, 0xb2, 0x7b, 0x4a, 0xe0, 0x4c, 0x12, 0x25, 0x0c, 0x70, 0x8d, 0x03, 0x29, // 7552 + 0xa0, 0xe1, 0x53, 0x24, 0xec, 0x13, 0xd9, 0xee, 0x19, 0xbf, 0x10, 0xb3, 0x4a, 0x8c, 0x3f, 0x89, // 7568 + 0xa3, 0x61, 0x51, 0xde, 0xac, 0x87, 0x07, 0x94, 0xf4, 0x63, 0x71, 0xec, 0x2e, 0xe2, 0x6f, 0x5b, // 7584 + 0x98, 0x81, 0xe1, 0x89, 0x5c, 0x34, 0x79, 0x6c, 0x76, 0xef, 0x3b, 0x90, 0x62, 0x79, 0xe6, 0xdb, // 7600 + 0xa4, 0x9a, 0x2f, 0x26, 0xc5, 0xd0, 0x10, 0xe1, 0x0e, 0xde, 0xd9, 0x10, 0x8e, 0x16, 0xfb, 0xb7, // 7616 + 0xf7, 0xa8, 0xf7, 0xc7, 0xe5, 0x02, 0x07, 0x98, 0x8f, 0x36, 0x08, 0x95, 0xe7, 0xe2, 0x37, 0x96, // 7632 + 0x0d, 0x36, 0x75, 0x9e, 0xfb, 0x0e, 0x72, 0xb1, 0x1d, 0x9b, 0xbc, 0x03, 0xf9, 0x49, 0x05, 0xd8, // 7648 + 0x81, 0xdd, 0x05, 0xb4, 0x2a, 0xd6, 0x41, 0xe9, 0xac, 0x01, 0x76, 0x95, 0x0a, 0x0f, 0xd8, 0xdf, // 7664 + 0xd5, 0xbd, 0x12, 0x1f, 0x35, 0x2f, 0x28, 0x17, 0x6c, 0xd2, 0x98, 0xc1, 0xa8, 0x09, 0x64, 0x77, // 7680 + 0x6e, 0x47, 0x37, 0xba, 0xce, 0xac, 0x59, 0x5e, 0x68, 0x9d, 0x7f, 0x72, 0xd6, 0x89, 0xc5, 0x06, // 7696 + 0x41, 0x29, 0x3e, 0x59, 0x3e, 0xdd, 0x26, 0xf5, 0x24, 0xc9, 0x11, 0xa7, 0x5a, 0xa3, 0x4c, 0x40, // 7712 + 0x1f, 0x46, 0xa1, 0x99, 0xb5, 0xa7, 0x3a, 0x51, 0x6e, 0x86, 0x3b, 0x9e, 0x7d, 0x72, 0xa7, 0x12, // 7728 + 0x05, 0x78, 0x59, 0xed, 0x3e, 0x51, 0x78, 0x15, 0x0b, 0x03, 0x8f, 0x8d, 0xd0, 0x2f, 0x05, 0xb2, // 7744 + 0x3e, 0x7b, 0x4a, 0x1c, 0x4b, 0x73, 0x05, 0x12, 0xfc, 0xc6, 0xea, 0xe0, 0x50, 0x13, 0x7c, 0x43, // 7760 + 0x93, 0x74, 0xb3, 0xca, 0x74, 0xe7, 0x8e, 0x1f, 0x01, 0x08, 0xd0, 0x30, 0xd4, 0x5b, 0x71, 0x36, // 7776 + 0xb4, 0x07, 0xba, 0xc1, 0x30, 0x30, 0x5c, 0x48, 0xb7, 0x82, 0x3b, 0x98, 0xa6, 0x7d, 0x60, 0x8a, // 7792 + 0xa2, 0xa3, 0x29, 0x82, 0xcc, 0xba, 0xbd, 0x83, 0x04, 0x1b, 0xa2, 0x83, 0x03, 0x41, 0xa1, 0xd6, // 7808 + 0x05, 0xf1, 0x1b, 0xc2, 0xb6, 0xf0, 0xa8, 0x7c, 0x86, 0x3b, 0x46, 0xa8, 0x48, 0x2a, 0x88, 0xdc, // 7824 + 0x76, 0x9a, 0x76, 0xbf, 0x1f, 0x6a, 0xa5, 0x3d, 0x19, 0x8f, 0xeb, 0x38, 0xf3, 0x64, 0xde, 0xc8, // 7840 + 0x2b, 0x0d, 0x0a, 0x28, 0xff, 0xf7, 0xdb, 0xe2, 0x15, 0x42, 0xd4, 0x22, 0xd0, 0x27, 0x5d, 0xe1, // 7856 + 0x79, 0xfe, 0x18, 0xe7, 0x70, 0x88, 0xad, 0x4e, 0xe6, 0xd9, 0x8b, 0x3a, 0xc6, 0xdd, 0x27, 0x51, // 7872 + 0x6e, 0xff, 0xbc, 0x64, 0xf5, 0x33, 0x43, 0x4f, 0x02, 0x03, 0x01, 0x00, 0x01, 0x00, 0x51, 0x00, // 7888 + 0x78, 0x30, 0x4f, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, // 7904 + 0x31, 0x29, 0x30, 0x27, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x20, 0x49, 0x6e, 0x74, 0x65, 0x72, // 7920 + 0x6e, 0x65, 0x74, 0x20, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x20, 0x52, 0x65, 0x73, // 7936 + 0x65, 0x61, 0x72, 0x63, 0x68, 0x20, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x31, 0x15, 0x30, 0x13, 0x06, // 7952 + 0x03, 0x55, 0x04, 0x03, 0x13, 0x0c, 0x49, 0x53, 0x52, 0x47, 0x20, 0x52, 0x6f, 0x6f, 0x74, 0x20, // 7968 + 0x58, 0x32, 0x30, 0x76, 0x30, 0x10, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, // 7984 + 0x05, 0x2b, 0x81, 0x04, 0x00, 0x22, 0x03, 0x62, 0x00, 0x04, 0xcd, 0x9b, 0xd5, 0x9f, 0x80, 0x83, // 8000 + 0x0a, 0xec, 0x09, 0x4a, 0xf3, 0x16, 0x4a, 0x3e, 0x5c, 0xcf, 0x77, 0xac, 0xde, 0x67, 0x05, 0x0d, // 8016 + 0x1d, 0x07, 0xb6, 0xdc, 0x16, 0xfb, 0x5a, 0x8b, 0x14, 0xdb, 0xe2, 0x71, 0x60, 0xc4, 0xba, 0x45, // 8032 + 0x95, 0x11, 0x89, 0x8e, 0xea, 0x06, 0xdf, 0xf7, 0x2a, 0x16, 0x1c, 0xa4, 0xb9, 0xc5, 0xc5, 0x32, // 8048 + 0xe0, 0x03, 0xe0, 0x1e, 0x82, 0x18, 0x38, 0x8b, 0xd7, 0x45, 0xd8, 0x0a, 0x6a, 0x6e, 0xe6, 0x00, // 8064 + 0x77, 0xfb, 0x02, 0x51, 0x7d, 0x22, 0xd8, 0x0a, 0x6e, 0x9a, 0x5b, 0x77, 0xdf, 0xf0, 0xfa, 0x41, // 8080 + 0xec, 0x39, 0xdc, 0x75, 0xca, 0x68, 0x07, 0x0c, 0x1f, 0xea, 0x00, 0x52, 0x00, 0x5b, 0x30, 0x50, // 8096 + 0x31, 0x24, 0x30, 0x22, 0x06, 0x03, 0x55, 0x04, 0x0b, 0x13, 0x1b, 0x47, 0x6c, 0x6f, 0x62, 0x61, // 8112 + 0x6c, 0x53, 0x69, 0x67, 0x6e, 0x20, 0x45, 0x43, 0x43, 0x20, 0x52, 0x6f, 0x6f, 0x74, 0x20, 0x43, // 8128 + 0x41, 0x20, 0x2d, 0x20, 0x52, 0x34, 0x31, 0x13, 0x30, 0x11, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, // 8144 + 0x0a, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x53, 0x69, 0x67, 0x6e, 0x31, 0x13, 0x30, 0x11, 0x06, // 8160 + 0x03, 0x55, 0x04, 0x03, 0x13, 0x0a, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x53, 0x69, 0x67, 0x6e, // 8176 + 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, // 8192 + 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0xb8, 0xc6, 0x79, 0xd3, 0x8f, // 8208 + 0x6c, 0x25, 0x0e, 0x9f, 0x2e, 0x39, 0x19, 0x1c, 0x03, 0xa4, 0xae, 0x9a, 0xe5, 0x39, 0x07, 0x09, // 8224 + 0x16, 0xca, 0x63, 0xb1, 0xb9, 0x86, 0xf8, 0x8a, 0x57, 0xc1, 0x57, 0xce, 0x42, 0xfa, 0x73, 0xa1, // 8240 + 0xf7, 0x65, 0x42, 0xff, 0x1e, 0xc1, 0x00, 0xb2, 0x6e, 0x73, 0x0e, 0xff, 0xc7, 0x21, 0xe5, 0x18, // 8256 + 0xa4, 0xaa, 0xd9, 0x71, 0x3f, 0xa8, 0xd4, 0xb9, 0xce, 0x8c, 0x1d, 0x00, 0x52, 0x00, 0x78, 0x30, // 8272 + 0x50, 0x31, 0x24, 0x30, 0x22, 0x06, 0x03, 0x55, 0x04, 0x0b, 0x13, 0x1b, 0x47, 0x6c, 0x6f, 0x62, // 8288 + 0x61, 0x6c, 0x53, 0x69, 0x67, 0x6e, 0x20, 0x45, 0x43, 0x43, 0x20, 0x52, 0x6f, 0x6f, 0x74, 0x20, // 8304 + 0x43, 0x41, 0x20, 0x2d, 0x20, 0x52, 0x35, 0x31, 0x13, 0x30, 0x11, 0x06, 0x03, 0x55, 0x04, 0x0a, // 8320 + 0x13, 0x0a, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x53, 0x69, 0x67, 0x6e, 0x31, 0x13, 0x30, 0x11, // 8336 + 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x0a, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x53, 0x69, 0x67, // 8352 + 0x6e, 0x30, 0x76, 0x30, 0x10, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x05, // 8368 + 0x2b, 0x81, 0x04, 0x00, 0x22, 0x03, 0x62, 0x00, 0x04, 0x47, 0x45, 0x0e, 0x96, 0xfb, 0x7d, 0x5d, // 8384 + 0xbf, 0xe9, 0x39, 0xd1, 0x21, 0xf8, 0x9f, 0x0b, 0xb6, 0xd5, 0x7b, 0x1e, 0x92, 0x3a, 0x48, 0x59, // 8400 + 0x1c, 0xf0, 0x62, 0x31, 0x2d, 0xc0, 0x7a, 0x28, 0xfe, 0x1a, 0xa7, 0x5c, 0xb3, 0xb6, 0xcc, 0x97, // 8416 + 0xe7, 0x45, 0xd4, 0x58, 0xfa, 0xd1, 0x77, 0x6d, 0x43, 0xa2, 0xc0, 0x87, 0x65, 0x34, 0x0a, 0x1f, // 8432 + 0x7a, 0xdd, 0xeb, 0x3c, 0x33, 0xa1, 0xc5, 0x9d, 0x4d, 0xa4, 0x6f, 0x41, 0x95, 0x38, 0x7f, 0xc9, // 8448 + 0x1e, 0x84, 0xeb, 0xd1, 0x9e, 0x49, 0x92, 0x87, 0x94, 0x87, 0x0c, 0x3a, 0x85, 0x4a, 0x66, 0x9f, // 8464 + 0x9d, 0x59, 0x93, 0x4d, 0x97, 0x61, 0x06, 0x86, 0x4a, 0x00, 0x59, 0x01, 0x26, 0x30, 0x57, 0x31, // 8480 + 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x42, 0x45, 0x31, 0x19, 0x30, 0x17, // 8496 + 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x10, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x53, 0x69, 0x67, // 8512 + 0x6e, 0x20, 0x6e, 0x76, 0x2d, 0x73, 0x61, 0x31, 0x10, 0x30, 0x0e, 0x06, 0x03, 0x55, 0x04, 0x0b, // 8528 + 0x13, 0x07, 0x52, 0x6f, 0x6f, 0x74, 0x20, 0x43, 0x41, 0x31, 0x1b, 0x30, 0x19, 0x06, 0x03, 0x55, // 8544 + 0x04, 0x03, 0x13, 0x12, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x53, 0x69, 0x67, 0x6e, 0x20, 0x52, // 8560 + 0x6f, 0x6f, 0x74, 0x20, 0x43, 0x41, 0x30, 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, // 8576 + 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0f, 0x00, 0x30, 0x82, // 8592 + 0x01, 0x0a, 0x02, 0x82, 0x01, 0x01, 0x00, 0xda, 0x0e, 0xe6, 0x99, 0x8d, 0xce, 0xa3, 0xe3, 0x4f, // 8608 + 0x8a, 0x7e, 0xfb, 0xf1, 0x8b, 0x83, 0x25, 0x6b, 0xea, 0x48, 0x1f, 0xf1, 0x2a, 0xb0, 0xb9, 0x95, // 8624 + 0x11, 0x04, 0xbd, 0xf0, 0x63, 0xd1, 0xe2, 0x67, 0x66, 0xcf, 0x1c, 0xdd, 0xcf, 0x1b, 0x48, 0x2b, // 8640 + 0xee, 0x8d, 0x89, 0x8e, 0x9a, 0xaf, 0x29, 0x80, 0x65, 0xab, 0xe9, 0xc7, 0x2d, 0x12, 0xcb, 0xab, // 8656 + 0x1c, 0x4c, 0x70, 0x07, 0xa1, 0x3d, 0x0a, 0x30, 0xcd, 0x15, 0x8d, 0x4f, 0xf8, 0xdd, 0xd4, 0x8c, // 8672 + 0x50, 0x15, 0x1c, 0xef, 0x50, 0xee, 0xc4, 0x2e, 0xf7, 0xfc, 0xe9, 0x52, 0xf2, 0x91, 0x7d, 0xe0, // 8688 + 0x6d, 0xd5, 0x35, 0x30, 0x8e, 0x5e, 0x43, 0x73, 0xf2, 0x41, 0xe9, 0xd5, 0x6a, 0xe3, 0xb2, 0x89, // 8704 + 0x3a, 0x56, 0x39, 0x38, 0x6f, 0x06, 0x3c, 0x88, 0x69, 0x5b, 0x2a, 0x4d, 0xc5, 0xa7, 0x54, 0xb8, // 8720 + 0x6c, 0x89, 0xcc, 0x9b, 0xf9, 0x3c, 0xca, 0xe5, 0xfd, 0x89, 0xf5, 0x12, 0x3c, 0x92, 0x78, 0x96, // 8736 + 0xd6, 0xdc, 0x74, 0x6e, 0x93, 0x44, 0x61, 0xd1, 0x8d, 0xc7, 0x46, 0xb2, 0x75, 0x0e, 0x86, 0xe8, // 8752 + 0x19, 0x8a, 0xd5, 0x6d, 0x6c, 0xd5, 0x78, 0x16, 0x95, 0xa2, 0xe9, 0xc8, 0x0a, 0x38, 0xeb, 0xf2, // 8768 + 0x24, 0x13, 0x4f, 0x73, 0x54, 0x93, 0x13, 0x85, 0x3a, 0x1b, 0xbc, 0x1e, 0x34, 0xb5, 0x8b, 0x05, // 8784 + 0x8c, 0xb9, 0x77, 0x8b, 0xb1, 0xdb, 0x1f, 0x20, 0x91, 0xab, 0x09, 0x53, 0x6e, 0x90, 0xce, 0x7b, // 8800 + 0x37, 0x74, 0xb9, 0x70, 0x47, 0x91, 0x22, 0x51, 0x63, 0x16, 0x79, 0xae, 0xb1, 0xae, 0x41, 0x26, // 8816 + 0x08, 0xc8, 0x19, 0x2b, 0xd1, 0x46, 0xaa, 0x48, 0xd6, 0x64, 0x2a, 0xd7, 0x83, 0x34, 0xff, 0x2c, // 8832 + 0x2a, 0xc1, 0x6c, 0x19, 0x43, 0x4a, 0x07, 0x85, 0xe7, 0xd3, 0x7c, 0xf6, 0x21, 0x68, 0xef, 0xea, // 8848 + 0xf2, 0x52, 0x9f, 0x7f, 0x93, 0x90, 0xcf, 0x02, 0x03, 0x01, 0x00, 0x01, 0x00, 0x5c, 0x01, 0x26, // 8864 + 0x30, 0x5a, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x49, 0x45, 0x31, // 8880 + 0x12, 0x30, 0x10, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x09, 0x42, 0x61, 0x6c, 0x74, 0x69, 0x6d, // 8896 + 0x6f, 0x72, 0x65, 0x31, 0x13, 0x30, 0x11, 0x06, 0x03, 0x55, 0x04, 0x0b, 0x13, 0x0a, 0x43, 0x79, // 8912 + 0x62, 0x65, 0x72, 0x54, 0x72, 0x75, 0x73, 0x74, 0x31, 0x22, 0x30, 0x20, 0x06, 0x03, 0x55, 0x04, // 8928 + 0x03, 0x13, 0x19, 0x42, 0x61, 0x6c, 0x74, 0x69, 0x6d, 0x6f, 0x72, 0x65, 0x20, 0x43, 0x79, 0x62, // 8944 + 0x65, 0x72, 0x54, 0x72, 0x75, 0x73, 0x74, 0x20, 0x52, 0x6f, 0x6f, 0x74, 0x30, 0x82, 0x01, 0x22, // 8960 + 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, // 8976 + 0x82, 0x01, 0x0f, 0x00, 0x30, 0x82, 0x01, 0x0a, 0x02, 0x82, 0x01, 0x01, 0x00, 0xa3, 0x04, 0xbb, // 8992 + 0x22, 0xab, 0x98, 0x3d, 0x57, 0xe8, 0x26, 0x72, 0x9a, 0xb5, 0x79, 0xd4, 0x29, 0xe2, 0xe1, 0xe8, // 9008 + 0x95, 0x80, 0xb1, 0xb0, 0xe3, 0x5b, 0x8e, 0x2b, 0x29, 0x9a, 0x64, 0xdf, 0xa1, 0x5d, 0xed, 0xb0, // 9024 + 0x09, 0x05, 0x6d, 0xdb, 0x28, 0x2e, 0xce, 0x62, 0xa2, 0x62, 0xfe, 0xb4, 0x88, 0xda, 0x12, 0xeb, // 9040 + 0x38, 0xeb, 0x21, 0x9d, 0xc0, 0x41, 0x2b, 0x01, 0x52, 0x7b, 0x88, 0x77, 0xd3, 0x1c, 0x8f, 0xc7, // 9056 + 0xba, 0xb9, 0x88, 0xb5, 0x6a, 0x09, 0xe7, 0x73, 0xe8, 0x11, 0x40, 0xa7, 0xd1, 0xcc, 0xca, 0x62, // 9072 + 0x8d, 0x2d, 0xe5, 0x8f, 0x0b, 0xa6, 0x50, 0xd2, 0xa8, 0x50, 0xc3, 0x28, 0xea, 0xf5, 0xab, 0x25, // 9088 + 0x87, 0x8a, 0x9a, 0x96, 0x1c, 0xa9, 0x67, 0xb8, 0x3f, 0x0c, 0xd5, 0xf7, 0xf9, 0x52, 0x13, 0x2f, // 9104 + 0xc2, 0x1b, 0xd5, 0x70, 0x70, 0xf0, 0x8f, 0xc0, 0x12, 0xca, 0x06, 0xcb, 0x9a, 0xe1, 0xd9, 0xca, // 9120 + 0x33, 0x7a, 0x77, 0xd6, 0xf8, 0xec, 0xb9, 0xf1, 0x68, 0x44, 0x42, 0x48, 0x13, 0xd2, 0xc0, 0xc2, // 9136 + 0xa4, 0xae, 0x5e, 0x60, 0xfe, 0xb6, 0xa6, 0x05, 0xfc, 0xb4, 0xdd, 0x07, 0x59, 0x02, 0xd4, 0x59, // 9152 + 0x18, 0x98, 0x63, 0xf5, 0xa5, 0x63, 0xe0, 0x90, 0x0c, 0x7d, 0x5d, 0xb2, 0x06, 0x7a, 0xf3, 0x85, // 9168 + 0xea, 0xeb, 0xd4, 0x03, 0xae, 0x5e, 0x84, 0x3e, 0x5f, 0xff, 0x15, 0xed, 0x69, 0xbc, 0xf9, 0x39, // 9184 + 0x36, 0x72, 0x75, 0xcf, 0x77, 0x52, 0x4d, 0xf3, 0xc9, 0x90, 0x2c, 0xb9, 0x3d, 0xe5, 0xc9, 0x23, // 9200 + 0x53, 0x3f, 0x1f, 0x24, 0x98, 0x21, 0x5c, 0x07, 0x99, 0x29, 0xbd, 0xc6, 0x3a, 0xec, 0xe7, 0x6e, // 9216 + 0x86, 0x3a, 0x6b, 0x97, 0x74, 0x63, 0x33, 0xbd, 0x68, 0x18, 0x31, 0xf0, 0x78, 0x8d, 0x76, 0xbf, // 9232 + 0xfc, 0x9e, 0x8e, 0x5d, 0x2a, 0x86, 0xa7, 0x4d, 0x90, 0xdc, 0x27, 0x1a, 0x39, 0x02, 0x03, 0x01, // 9248 + 0x00, 0x01, 0x00, 0x63, 0x01, 0x26, 0x30, 0x61, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, // 9264 + 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, 0x15, 0x30, 0x13, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x0c, // 9280 + 0x44, 0x69, 0x67, 0x69, 0x43, 0x65, 0x72, 0x74, 0x20, 0x49, 0x6e, 0x63, 0x31, 0x19, 0x30, 0x17, // 9296 + 0x06, 0x03, 0x55, 0x04, 0x0b, 0x13, 0x10, 0x77, 0x77, 0x77, 0x2e, 0x64, 0x69, 0x67, 0x69, 0x63, // 9312 + 0x65, 0x72, 0x74, 0x2e, 0x63, 0x6f, 0x6d, 0x31, 0x20, 0x30, 0x1e, 0x06, 0x03, 0x55, 0x04, 0x03, // 9328 + 0x13, 0x17, 0x44, 0x69, 0x67, 0x69, 0x43, 0x65, 0x72, 0x74, 0x20, 0x47, 0x6c, 0x6f, 0x62, 0x61, // 9344 + 0x6c, 0x20, 0x52, 0x6f, 0x6f, 0x74, 0x20, 0x43, 0x41, 0x30, 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, // 9360 + 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0f, // 9376 + 0x00, 0x30, 0x82, 0x01, 0x0a, 0x02, 0x82, 0x01, 0x01, 0x00, 0xe2, 0x3b, 0xe1, 0x11, 0x72, 0xde, // 9392 + 0xa8, 0xa4, 0xd3, 0xa3, 0x57, 0xaa, 0x50, 0xa2, 0x8f, 0x0b, 0x77, 0x90, 0xc9, 0xa2, 0xa5, 0xee, // 9408 + 0x12, 0xce, 0x96, 0x5b, 0x01, 0x09, 0x20, 0xcc, 0x01, 0x93, 0xa7, 0x4e, 0x30, 0xb7, 0x53, 0xf7, // 9424 + 0x43, 0xc4, 0x69, 0x00, 0x57, 0x9d, 0xe2, 0x8d, 0x22, 0xdd, 0x87, 0x06, 0x40, 0x00, 0x81, 0x09, // 9440 + 0xce, 0xce, 0x1b, 0x83, 0xbf, 0xdf, 0xcd, 0x3b, 0x71, 0x46, 0xe2, 0xd6, 0x66, 0xc7, 0x05, 0xb3, // 9456 + 0x76, 0x27, 0x16, 0x8f, 0x7b, 0x9e, 0x1e, 0x95, 0x7d, 0xee, 0xb7, 0x48, 0xa3, 0x08, 0xda, 0xd6, // 9472 + 0xaf, 0x7a, 0x0c, 0x39, 0x06, 0x65, 0x7f, 0x4a, 0x5d, 0x1f, 0xbc, 0x17, 0xf8, 0xab, 0xbe, 0xee, // 9488 + 0x28, 0xd7, 0x74, 0x7f, 0x7a, 0x78, 0x99, 0x59, 0x85, 0x68, 0x6e, 0x5c, 0x23, 0x32, 0x4b, 0xbf, // 9504 + 0x4e, 0xc0, 0xe8, 0x5a, 0x6d, 0xe3, 0x70, 0xbf, 0x77, 0x10, 0xbf, 0xfc, 0x01, 0xf6, 0x85, 0xd9, // 9520 + 0xa8, 0x44, 0x10, 0x58, 0x32, 0xa9, 0x75, 0x18, 0xd5, 0xd1, 0xa2, 0xbe, 0x47, 0xe2, 0x27, 0x6a, // 9536 + 0xf4, 0x9a, 0x33, 0xf8, 0x49, 0x08, 0x60, 0x8b, 0xd4, 0x5f, 0xb4, 0x3a, 0x84, 0xbf, 0xa1, 0xaa, // 9552 + 0x4a, 0x4c, 0x7d, 0x3e, 0xcf, 0x4f, 0x5f, 0x6c, 0x76, 0x5e, 0xa0, 0x4b, 0x37, 0x91, 0x9e, 0xdc, // 9568 + 0x22, 0xe6, 0x6d, 0xce, 0x14, 0x1a, 0x8e, 0x6a, 0xcb, 0xfe, 0xcd, 0xb3, 0x14, 0x64, 0x17, 0xc7, // 9584 + 0x5b, 0x29, 0x9e, 0x32, 0xbf, 0xf2, 0xee, 0xfa, 0xd3, 0x0b, 0x42, 0xd4, 0xab, 0xb7, 0x41, 0x32, // 9600 + 0xda, 0x0c, 0xd4, 0xef, 0xf8, 0x81, 0xd5, 0xbb, 0x8d, 0x58, 0x3f, 0xb5, 0x1b, 0xe8, 0x49, 0x28, // 9616 + 0xa2, 0x70, 0xda, 0x31, 0x04, 0xdd, 0xf7, 0xb2, 0x16, 0xf2, 0x4c, 0x0a, 0x4e, 0x07, 0xa8, 0xed, // 9632 + 0x4a, 0x3d, 0x5e, 0xb5, 0x7f, 0xa3, 0x90, 0xc3, 0xaf, 0x27, 0x02, 0x03, 0x01, 0x00, 0x01, 0x00, // 9648 + 0x63, 0x01, 0x26, 0x30, 0x61, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, // 9664 + 0x55, 0x53, 0x31, 0x15, 0x30, 0x13, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x0c, 0x44, 0x69, 0x67, // 9680 + 0x69, 0x43, 0x65, 0x72, 0x74, 0x20, 0x49, 0x6e, 0x63, 0x31, 0x19, 0x30, 0x17, 0x06, 0x03, 0x55, // 9696 + 0x04, 0x0b, 0x13, 0x10, 0x77, 0x77, 0x77, 0x2e, 0x64, 0x69, 0x67, 0x69, 0x63, 0x65, 0x72, 0x74, // 9712 + 0x2e, 0x63, 0x6f, 0x6d, 0x31, 0x20, 0x30, 0x1e, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x17, 0x44, // 9728 + 0x69, 0x67, 0x69, 0x43, 0x65, 0x72, 0x74, 0x20, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x20, 0x52, // 9744 + 0x6f, 0x6f, 0x74, 0x20, 0x47, 0x32, 0x30, 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, // 9760 + 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0f, 0x00, 0x30, 0x82, // 9776 + 0x01, 0x0a, 0x02, 0x82, 0x01, 0x01, 0x00, 0xbb, 0x37, 0xcd, 0x34, 0xdc, 0x7b, 0x6b, 0xc9, 0xb2, // 9792 + 0x68, 0x90, 0xad, 0x4a, 0x75, 0xff, 0x46, 0xba, 0x21, 0x0a, 0x08, 0x8d, 0xf5, 0x19, 0x54, 0xc9, // 9808 + 0xfb, 0x88, 0xdb, 0xf3, 0xae, 0xf2, 0x3a, 0x89, 0x91, 0x3c, 0x7a, 0xe6, 0xab, 0x06, 0x1a, 0x6b, // 9824 + 0xcf, 0xac, 0x2d, 0xe8, 0x5e, 0x09, 0x24, 0x44, 0xba, 0x62, 0x9a, 0x7e, 0xd6, 0xa3, 0xa8, 0x7e, // 9840 + 0xe0, 0x54, 0x75, 0x20, 0x05, 0xac, 0x50, 0xb7, 0x9c, 0x63, 0x1a, 0x6c, 0x30, 0xdc, 0xda, 0x1f, // 9856 + 0x19, 0xb1, 0xd7, 0x1e, 0xde, 0xfd, 0xd7, 0xe0, 0xcb, 0x94, 0x83, 0x37, 0xae, 0xec, 0x1f, 0x43, // 9872 + 0x4e, 0xdd, 0x7b, 0x2c, 0xd2, 0xbd, 0x2e, 0xa5, 0x2f, 0xe4, 0xa9, 0xb8, 0xad, 0x3a, 0xd4, 0x99, // 9888 + 0xa4, 0xb6, 0x25, 0xe9, 0x9b, 0x6b, 0x00, 0x60, 0x92, 0x60, 0xff, 0x4f, 0x21, 0x49, 0x18, 0xf7, // 9904 + 0x67, 0x90, 0xab, 0x61, 0x06, 0x9c, 0x8f, 0xf2, 0xba, 0xe9, 0xb4, 0xe9, 0x92, 0x32, 0x6b, 0xb5, // 9920 + 0xf3, 0x57, 0xe8, 0x5d, 0x1b, 0xcd, 0x8c, 0x1d, 0xab, 0x95, 0x04, 0x95, 0x49, 0xf3, 0x35, 0x2d, // 9936 + 0x96, 0xe3, 0x49, 0x6d, 0xdd, 0x77, 0xe3, 0xfb, 0x49, 0x4b, 0xb4, 0xac, 0x55, 0x07, 0xa9, 0x8f, // 9952 + 0x95, 0xb3, 0xb4, 0x23, 0xbb, 0x4c, 0x6d, 0x45, 0xf0, 0xf6, 0xa9, 0xb2, 0x95, 0x30, 0xb4, 0xfd, // 9968 + 0x4c, 0x55, 0x8c, 0x27, 0x4a, 0x57, 0x14, 0x7c, 0x82, 0x9d, 0xcd, 0x73, 0x92, 0xd3, 0x16, 0x4a, // 9984 + 0x06, 0x0c, 0x8c, 0x50, 0xd1, 0x8f, 0x1e, 0x09, 0xbe, 0x17, 0xa1, 0xe6, 0x21, 0xca, 0xfd, 0x83, // 10000 + 0xe5, 0x10, 0xbc, 0x83, 0xa5, 0x0a, 0xc4, 0x67, 0x28, 0xf6, 0x73, 0x14, 0x14, 0x3d, 0x46, 0x76, // 10016 + 0xc3, 0x87, 0x14, 0x89, 0x21, 0x34, 0x4d, 0xaf, 0x0f, 0x45, 0x0c, 0xa6, 0x49, 0xa1, 0xba, 0xbb, // 10032 + 0x9c, 0xc5, 0xb1, 0x33, 0x83, 0x29, 0x85, 0x02, 0x03, 0x01, 0x00, 0x01, 0x00, 0x63, 0x00, 0x78, // 10048 + 0x30, 0x61, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, // 10064 + 0x15, 0x30, 0x13, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x0c, 0x44, 0x69, 0x67, 0x69, 0x43, 0x65, // 10080 + 0x72, 0x74, 0x20, 0x49, 0x6e, 0x63, 0x31, 0x19, 0x30, 0x17, 0x06, 0x03, 0x55, 0x04, 0x0b, 0x13, // 10096 + 0x10, 0x77, 0x77, 0x77, 0x2e, 0x64, 0x69, 0x67, 0x69, 0x63, 0x65, 0x72, 0x74, 0x2e, 0x63, 0x6f, // 10112 + 0x6d, 0x31, 0x20, 0x30, 0x1e, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x17, 0x44, 0x69, 0x67, 0x69, // 10128 + 0x43, 0x65, 0x72, 0x74, 0x20, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x20, 0x52, 0x6f, 0x6f, 0x74, // 10144 + 0x20, 0x47, 0x33, 0x30, 0x76, 0x30, 0x10, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, // 10160 + 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x22, 0x03, 0x62, 0x00, 0x04, 0xdd, 0xa7, 0xd9, 0xbb, 0x8a, // 10176 + 0xb8, 0x0b, 0xfb, 0x0b, 0x7f, 0x21, 0xd2, 0xf0, 0xbe, 0xbe, 0x73, 0xf3, 0x33, 0x5d, 0x1a, 0xbc, // 10192 + 0x34, 0xea, 0xde, 0xc6, 0x9b, 0xbc, 0xd0, 0x95, 0xf6, 0xf0, 0xcc, 0xd0, 0x0b, 0xba, 0x61, 0x5b, // 10208 + 0x51, 0x46, 0x7e, 0x9e, 0x2d, 0x9f, 0xee, 0x8e, 0x63, 0x0c, 0x17, 0xec, 0x07, 0x70, 0xf5, 0xcf, // 10224 + 0x84, 0x2e, 0x40, 0x83, 0x9c, 0xe8, 0x3f, 0x41, 0x6d, 0x3b, 0xad, 0xd3, 0xa4, 0x14, 0x59, 0x36, // 10240 + 0x78, 0x9d, 0x03, 0x43, 0xee, 0x10, 0x13, 0x6c, 0x72, 0xde, 0xae, 0x88, 0xa7, 0xa1, 0x6b, 0xb5, // 10256 + 0x43, 0xce, 0x67, 0xdc, 0x23, 0xff, 0x03, 0x1c, 0xa3, 0xe2, 0x3e, 0x00, 0x64, 0x02, 0x26, 0x30, // 10272 + 0x62, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, 0x15, // 10288 + 0x30, 0x13, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x0c, 0x44, 0x69, 0x67, 0x69, 0x43, 0x65, 0x72, // 10304 + 0x74, 0x20, 0x49, 0x6e, 0x63, 0x31, 0x19, 0x30, 0x17, 0x06, 0x03, 0x55, 0x04, 0x0b, 0x13, 0x10, // 10320 + 0x77, 0x77, 0x77, 0x2e, 0x64, 0x69, 0x67, 0x69, 0x63, 0x65, 0x72, 0x74, 0x2e, 0x63, 0x6f, 0x6d, // 10336 + 0x31, 0x21, 0x30, 0x1f, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x18, 0x44, 0x69, 0x67, 0x69, 0x43, // 10352 + 0x65, 0x72, 0x74, 0x20, 0x54, 0x72, 0x75, 0x73, 0x74, 0x65, 0x64, 0x20, 0x52, 0x6f, 0x6f, 0x74, // 10368 + 0x20, 0x47, 0x34, 0x30, 0x82, 0x02, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, // 10384 + 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x02, 0x0f, 0x00, 0x30, 0x82, 0x02, 0x0a, 0x02, // 10400 + 0x82, 0x02, 0x01, 0x00, 0xbf, 0xe6, 0x90, 0x73, 0x68, 0xde, 0xbb, 0xe4, 0x5d, 0x4a, 0x3c, 0x30, // 10416 + 0x22, 0x30, 0x69, 0x33, 0xec, 0xc2, 0xa7, 0x25, 0x2e, 0xc9, 0x21, 0x3d, 0xf2, 0x8a, 0xd8, 0x59, // 10432 + 0xc2, 0xe1, 0x29, 0xa7, 0x3d, 0x58, 0xab, 0x76, 0x9a, 0xcd, 0xae, 0x7b, 0x1b, 0x84, 0x0d, 0xc4, // 10448 + 0x30, 0x1f, 0xf3, 0x1b, 0xa4, 0x38, 0x16, 0xeb, 0x56, 0xc6, 0x97, 0x6d, 0x1d, 0xab, 0xb2, 0x79, // 10464 + 0xf2, 0xca, 0x11, 0xd2, 0xe4, 0x5f, 0xd6, 0x05, 0x3c, 0x52, 0x0f, 0x52, 0x1f, 0xc6, 0x9e, 0x15, // 10480 + 0xa5, 0x7e, 0xbe, 0x9f, 0xa9, 0x57, 0x16, 0x59, 0x55, 0x72, 0xaf, 0x68, 0x93, 0x70, 0xc2, 0xb2, // 10496 + 0xba, 0x75, 0x99, 0x6a, 0x73, 0x32, 0x94, 0xd1, 0x10, 0x44, 0x10, 0x2e, 0xdf, 0x82, 0xf3, 0x07, // 10512 + 0x84, 0xe6, 0x74, 0x3b, 0x6d, 0x71, 0xe2, 0x2d, 0x0c, 0x1b, 0xee, 0x20, 0xd5, 0xc9, 0x20, 0x1d, // 10528 + 0x63, 0x29, 0x2d, 0xce, 0xec, 0x5e, 0x4e, 0xc8, 0x93, 0xf8, 0x21, 0x61, 0x9b, 0x34, 0xeb, 0x05, // 10544 + 0xc6, 0x5e, 0xec, 0x5b, 0x1a, 0xbc, 0xeb, 0xc9, 0xcf, 0xcd, 0xac, 0x34, 0x40, 0x5f, 0xb1, 0x7a, // 10560 + 0x66, 0xee, 0x77, 0xc8, 0x48, 0xa8, 0x66, 0x57, 0x57, 0x9f, 0x54, 0x58, 0x8e, 0x0c, 0x2b, 0xb7, // 10576 + 0x4f, 0xa7, 0x30, 0xd9, 0x56, 0xee, 0xca, 0x7b, 0x5d, 0xe3, 0xad, 0xc9, 0x4f, 0x5e, 0xe5, 0x35, // 10592 + 0xe7, 0x31, 0xcb, 0xda, 0x93, 0x5e, 0xdc, 0x8e, 0x8f, 0x80, 0xda, 0xb6, 0x91, 0x98, 0x40, 0x90, // 10608 + 0x79, 0xc3, 0x78, 0xc7, 0xb6, 0xb1, 0xc4, 0xb5, 0x6a, 0x18, 0x38, 0x03, 0x10, 0x8d, 0xd8, 0xd4, // 10624 + 0x37, 0xa4, 0x2e, 0x05, 0x7d, 0x88, 0xf5, 0x82, 0x3e, 0x10, 0x91, 0x70, 0xab, 0x55, 0x82, 0x41, // 10640 + 0x32, 0xd7, 0xdb, 0x04, 0x73, 0x2a, 0x6e, 0x91, 0x01, 0x7c, 0x21, 0x4c, 0xd4, 0xbc, 0xae, 0x1b, // 10656 + 0x03, 0x75, 0x5d, 0x78, 0x66, 0xd9, 0x3a, 0x31, 0x44, 0x9a, 0x33, 0x40, 0xbf, 0x08, 0xd7, 0x5a, // 10672 + 0x49, 0xa4, 0xc2, 0xe6, 0xa9, 0xa0, 0x67, 0xdd, 0xa4, 0x27, 0xbc, 0xa1, 0x4f, 0x39, 0xb5, 0x11, // 10688 + 0x58, 0x17, 0xf7, 0x24, 0x5c, 0x46, 0x8f, 0x64, 0xf7, 0xc1, 0x69, 0x88, 0x76, 0x98, 0x76, 0x3d, // 10704 + 0x59, 0x5d, 0x42, 0x76, 0x87, 0x89, 0x97, 0x69, 0x7a, 0x48, 0xf0, 0xe0, 0xa2, 0x12, 0x1b, 0x66, // 10720 + 0x9a, 0x74, 0xca, 0xde, 0x4b, 0x1e, 0xe7, 0x0e, 0x63, 0xae, 0xe6, 0xd4, 0xef, 0x92, 0x92, 0x3a, // 10736 + 0x9e, 0x3d, 0xdc, 0x00, 0xe4, 0x45, 0x25, 0x89, 0xb6, 0x9a, 0x44, 0x19, 0x2b, 0x7e, 0xc0, 0x94, // 10752 + 0xb4, 0xd2, 0x61, 0x6d, 0xeb, 0x33, 0xd9, 0xc5, 0xdf, 0x4b, 0x04, 0x00, 0xcc, 0x7d, 0x1c, 0x95, // 10768 + 0xc3, 0x8f, 0xf7, 0x21, 0xb2, 0xb2, 0x11, 0xb7, 0xbb, 0x7f, 0xf2, 0xd5, 0x8c, 0x70, 0x2c, 0x41, // 10784 + 0x60, 0xaa, 0xb1, 0x63, 0x18, 0x44, 0x95, 0x1a, 0x76, 0x62, 0x7e, 0xf6, 0x80, 0xb0, 0xfb, 0xe8, // 10800 + 0x64, 0xa6, 0x33, 0xd1, 0x89, 0x07, 0xe1, 0xbd, 0xb7, 0xe6, 0x43, 0xa4, 0x18, 0xb8, 0xa6, 0x77, // 10816 + 0x01, 0xe1, 0x0f, 0x94, 0x0c, 0x21, 0x1d, 0xb2, 0x54, 0x29, 0x25, 0x89, 0x6c, 0xe5, 0x0e, 0x52, // 10832 + 0x51, 0x47, 0x74, 0xbe, 0x26, 0xac, 0xb6, 0x41, 0x75, 0xde, 0x7a, 0xac, 0x5f, 0x8d, 0x3f, 0xc9, // 10848 + 0xbc, 0xd3, 0x41, 0x11, 0x12, 0x5b, 0xe5, 0x10, 0x50, 0xeb, 0x31, 0xc5, 0xca, 0x72, 0x16, 0x22, // 10864 + 0x09, 0xdf, 0x7c, 0x4c, 0x75, 0x3f, 0x63, 0xec, 0x21, 0x5f, 0xc4, 0x20, 0x51, 0x6b, 0x6f, 0xb1, // 10880 + 0xab, 0x86, 0x8b, 0x4f, 0xc2, 0xd6, 0x45, 0x5f, 0x9d, 0x20, 0xfc, 0xa1, 0x1e, 0xc5, 0xc0, 0x8f, // 10896 + 0xa2, 0xb1, 0x7e, 0x0a, 0x26, 0x99, 0xf5, 0xe4, 0x69, 0x2f, 0x98, 0x1d, 0x2d, 0xf5, 0xd9, 0xa9, // 10912 + 0xb2, 0x1d, 0xe5, 0x1b, 0x02, 0x03, 0x01, 0x00, 0x01, 0x00, 0x65, 0x01, 0x24, 0x30, 0x63, 0x31, // 10928 + 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, 0x21, 0x30, 0x1f, // 10944 + 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x18, 0x54, 0x68, 0x65, 0x20, 0x47, 0x6f, 0x20, 0x44, 0x61, // 10960 + 0x64, 0x64, 0x79, 0x20, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x2c, 0x20, 0x49, 0x6e, 0x63, 0x2e, 0x31, // 10976 + 0x31, 0x30, 0x2f, 0x06, 0x03, 0x55, 0x04, 0x0b, 0x13, 0x28, 0x47, 0x6f, 0x20, 0x44, 0x61, 0x64, // 10992 + 0x64, 0x79, 0x20, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x20, 0x32, 0x20, 0x43, 0x65, 0x72, 0x74, 0x69, // 11008 + 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, // 11024 + 0x74, 0x79, 0x30, 0x82, 0x01, 0x20, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, // 11040 + 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0d, 0x00, 0x30, 0x82, 0x01, 0x08, 0x02, 0x82, // 11056 + 0x01, 0x01, 0x00, 0xde, 0x9d, 0xd7, 0xea, 0x57, 0x18, 0x49, 0xa1, 0x5b, 0xeb, 0xd7, 0x5f, 0x48, // 11072 + 0x86, 0xea, 0xbe, 0xdd, 0xff, 0xe4, 0xef, 0x67, 0x1c, 0xf4, 0x65, 0x68, 0xb3, 0x57, 0x71, 0xa0, // 11088 + 0x5e, 0x77, 0xbb, 0xed, 0x9b, 0x49, 0xe9, 0x70, 0x80, 0x3d, 0x56, 0x18, 0x63, 0x08, 0x6f, 0xda, // 11104 + 0xf2, 0xcc, 0xd0, 0x3f, 0x7f, 0x02, 0x54, 0x22, 0x54, 0x10, 0xd8, 0xb2, 0x81, 0xd4, 0xc0, 0x75, // 11120 + 0x3d, 0x4b, 0x7f, 0xc7, 0x77, 0xc3, 0x3e, 0x78, 0xab, 0x1a, 0x03, 0xb5, 0x20, 0x6b, 0x2f, 0x6a, // 11136 + 0x2b, 0xb1, 0xc5, 0x88, 0x7e, 0xc4, 0xbb, 0x1e, 0xb0, 0xc1, 0xd8, 0x45, 0x27, 0x6f, 0xaa, 0x37, // 11152 + 0x58, 0xf7, 0x87, 0x26, 0xd7, 0xd8, 0x2d, 0xf6, 0xa9, 0x17, 0xb7, 0x1f, 0x72, 0x36, 0x4e, 0xa6, // 11168 + 0x17, 0x3f, 0x65, 0x98, 0x92, 0xdb, 0x2a, 0x6e, 0x5d, 0xa2, 0xfe, 0x88, 0xe0, 0x0b, 0xde, 0x7f, // 11184 + 0xe5, 0x8d, 0x15, 0xe1, 0xeb, 0xcb, 0x3a, 0xd5, 0xe2, 0x12, 0xa2, 0x13, 0x2d, 0xd8, 0x8e, 0xaf, // 11200 + 0x5f, 0x12, 0x3d, 0xa0, 0x08, 0x05, 0x08, 0xb6, 0x5c, 0xa5, 0x65, 0x38, 0x04, 0x45, 0x99, 0x1e, // 11216 + 0xa3, 0x60, 0x60, 0x74, 0xc5, 0x41, 0xa5, 0x72, 0x62, 0x1b, 0x62, 0xc5, 0x1f, 0x6f, 0x5f, 0x1a, // 11232 + 0x42, 0xbe, 0x02, 0x51, 0x65, 0xa8, 0xae, 0x23, 0x18, 0x6a, 0xfc, 0x78, 0x03, 0xa9, 0x4d, 0x7f, // 11248 + 0x80, 0xc3, 0xfa, 0xab, 0x5a, 0xfc, 0xa1, 0x40, 0xa4, 0xca, 0x19, 0x16, 0xfe, 0xb2, 0xc8, 0xef, // 11264 + 0x5e, 0x73, 0x0d, 0xee, 0x77, 0xbd, 0x9a, 0xf6, 0x79, 0x98, 0xbc, 0xb1, 0x07, 0x67, 0xa2, 0x15, // 11280 + 0x0d, 0xdd, 0xa0, 0x58, 0xc6, 0x44, 0x7b, 0x0a, 0x3e, 0x62, 0x28, 0x5f, 0xba, 0x41, 0x07, 0x53, // 11296 + 0x58, 0xcf, 0x11, 0x7e, 0x38, 0x74, 0xc5, 0xf8, 0xff, 0xb5, 0x69, 0x90, 0x8f, 0x84, 0x74, 0xea, // 11312 + 0x97, 0x1b, 0xaf, 0x02, 0x01, 0x03, 0x00, 0x67, 0x01, 0x26, 0x30, 0x65, 0x31, 0x0b, 0x30, 0x09, // 11328 + 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, 0x15, 0x30, 0x13, 0x06, 0x03, 0x55, // 11344 + 0x04, 0x0a, 0x13, 0x0c, 0x44, 0x69, 0x67, 0x69, 0x43, 0x65, 0x72, 0x74, 0x20, 0x49, 0x6e, 0x63, // 11360 + 0x31, 0x19, 0x30, 0x17, 0x06, 0x03, 0x55, 0x04, 0x0b, 0x13, 0x10, 0x77, 0x77, 0x77, 0x2e, 0x64, // 11376 + 0x69, 0x67, 0x69, 0x63, 0x65, 0x72, 0x74, 0x2e, 0x63, 0x6f, 0x6d, 0x31, 0x24, 0x30, 0x22, 0x06, // 11392 + 0x03, 0x55, 0x04, 0x03, 0x13, 0x1b, 0x44, 0x69, 0x67, 0x69, 0x43, 0x65, 0x72, 0x74, 0x20, 0x41, // 11408 + 0x73, 0x73, 0x75, 0x72, 0x65, 0x64, 0x20, 0x49, 0x44, 0x20, 0x52, 0x6f, 0x6f, 0x74, 0x20, 0x43, // 11424 + 0x41, 0x30, 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, // 11440 + 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0f, 0x00, 0x30, 0x82, 0x01, 0x0a, 0x02, 0x82, 0x01, // 11456 + 0x01, 0x00, 0xad, 0x0e, 0x15, 0xce, 0xe4, 0x43, 0x80, 0x5c, 0xb1, 0x87, 0xf3, 0xb7, 0x60, 0xf9, // 11472 + 0x71, 0x12, 0xa5, 0xae, 0xdc, 0x26, 0x94, 0x88, 0xaa, 0xf4, 0xce, 0xf5, 0x20, 0x39, 0x28, 0x58, // 11488 + 0x60, 0x0c, 0xf8, 0x80, 0xda, 0xa9, 0x15, 0x95, 0x32, 0x61, 0x3c, 0xb5, 0xb1, 0x28, 0x84, 0x8a, // 11504 + 0x8a, 0xdc, 0x9f, 0x0a, 0x0c, 0x83, 0x17, 0x7a, 0x8f, 0x90, 0xac, 0x8a, 0xe7, 0x79, 0x53, 0x5c, // 11520 + 0x31, 0x84, 0x2a, 0xf6, 0x0f, 0x98, 0x32, 0x36, 0x76, 0xcc, 0xde, 0xdd, 0x3c, 0xa8, 0xa2, 0xef, // 11536 + 0x6a, 0xfb, 0x21, 0xf2, 0x52, 0x61, 0xdf, 0x9f, 0x20, 0xd7, 0x1f, 0xe2, 0xb1, 0xd9, 0xfe, 0x18, // 11552 + 0x64, 0xd2, 0x12, 0x5b, 0x5f, 0xf9, 0x58, 0x18, 0x35, 0xbc, 0x47, 0xcd, 0xa1, 0x36, 0xf9, 0x6b, // 11568 + 0x7f, 0xd4, 0xb0, 0x38, 0x3e, 0xc1, 0x1b, 0xc3, 0x8c, 0x33, 0xd9, 0xd8, 0x2f, 0x18, 0xfe, 0x28, // 11584 + 0x0f, 0xb3, 0xa7, 0x83, 0xd6, 0xc3, 0x6e, 0x44, 0xc0, 0x61, 0x35, 0x96, 0x16, 0xfe, 0x59, 0x9c, // 11600 + 0x8b, 0x76, 0x6d, 0xd7, 0xf1, 0xa2, 0x4b, 0x0d, 0x2b, 0xff, 0x0b, 0x72, 0xda, 0x9e, 0x60, 0xd0, // 11616 + 0x8e, 0x90, 0x35, 0xc6, 0x78, 0x55, 0x87, 0x20, 0xa1, 0xcf, 0xe5, 0x6d, 0x0a, 0xc8, 0x49, 0x7c, // 11632 + 0x31, 0x98, 0x33, 0x6c, 0x22, 0xe9, 0x87, 0xd0, 0x32, 0x5a, 0xa2, 0xba, 0x13, 0x82, 0x11, 0xed, // 11648 + 0x39, 0x17, 0x9d, 0x99, 0x3a, 0x72, 0xa1, 0xe6, 0xfa, 0xa4, 0xd9, 0xd5, 0x17, 0x31, 0x75, 0xae, // 11664 + 0x85, 0x7d, 0x22, 0xae, 0x3f, 0x01, 0x46, 0x86, 0xf6, 0x28, 0x79, 0xc8, 0xb1, 0xda, 0xe4, 0x57, // 11680 + 0x17, 0xc4, 0x7e, 0x1c, 0x0e, 0xb0, 0xb4, 0x92, 0xa6, 0x56, 0xb3, 0xbd, 0xb2, 0x97, 0xed, 0xaa, // 11696 + 0xa7, 0xf0, 0xb7, 0xc5, 0xa8, 0x3f, 0x95, 0x16, 0xd0, 0xff, 0xa1, 0x96, 0xeb, 0x08, 0x5f, 0x18, // 11712 + 0x77, 0x4f, 0x02, 0x03, 0x01, 0x00, 0x01, 0x00, 0x67, 0x01, 0x26, 0x30, 0x65, 0x31, 0x0b, 0x30, // 11728 + 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, 0x15, 0x30, 0x13, 0x06, 0x03, // 11744 + 0x55, 0x04, 0x0a, 0x13, 0x0c, 0x44, 0x69, 0x67, 0x69, 0x43, 0x65, 0x72, 0x74, 0x20, 0x49, 0x6e, // 11760 + 0x63, 0x31, 0x19, 0x30, 0x17, 0x06, 0x03, 0x55, 0x04, 0x0b, 0x13, 0x10, 0x77, 0x77, 0x77, 0x2e, // 11776 + 0x64, 0x69, 0x67, 0x69, 0x63, 0x65, 0x72, 0x74, 0x2e, 0x63, 0x6f, 0x6d, 0x31, 0x24, 0x30, 0x22, // 11792 + 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x1b, 0x44, 0x69, 0x67, 0x69, 0x43, 0x65, 0x72, 0x74, 0x20, // 11808 + 0x41, 0x73, 0x73, 0x75, 0x72, 0x65, 0x64, 0x20, 0x49, 0x44, 0x20, 0x52, 0x6f, 0x6f, 0x74, 0x20, // 11824 + 0x47, 0x32, 0x30, 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, // 11840 + 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0f, 0x00, 0x30, 0x82, 0x01, 0x0a, 0x02, 0x82, // 11856 + 0x01, 0x01, 0x00, 0xd9, 0xe7, 0x28, 0x2f, 0x52, 0x3f, 0x36, 0x72, 0x49, 0x88, 0x93, 0x34, 0xf3, // 11872 + 0xf8, 0x6a, 0x1e, 0x31, 0x54, 0x80, 0x9f, 0xad, 0x54, 0x41, 0xb5, 0x47, 0xdf, 0x96, 0xa8, 0xd4, // 11888 + 0xaf, 0x80, 0x2d, 0xb9, 0x0a, 0xcf, 0x75, 0xfd, 0x89, 0xa5, 0x7d, 0x24, 0xfa, 0xe3, 0x22, 0x0c, // 11904 + 0x2b, 0xbc, 0x95, 0x17, 0x0b, 0x33, 0xbf, 0x19, 0x4d, 0x41, 0x06, 0x90, 0x00, 0xbd, 0x0c, 0x4d, // 11920 + 0x10, 0xfe, 0x07, 0xb5, 0xe7, 0x1c, 0x6e, 0x22, 0x55, 0x31, 0x65, 0x97, 0xbd, 0xd3, 0x17, 0xd2, // 11936 + 0x1e, 0x62, 0xf3, 0xdb, 0xea, 0x6c, 0x50, 0x8c, 0x3f, 0x84, 0x0c, 0x96, 0xcf, 0xb7, 0xcb, 0x03, // 11952 + 0xe0, 0xca, 0x6d, 0xa1, 0x14, 0x4c, 0x1b, 0x89, 0xdd, 0xed, 0x00, 0xb0, 0x52, 0x7c, 0xaf, 0x91, // 11968 + 0x6c, 0xb1, 0x38, 0x13, 0xd1, 0xe9, 0x12, 0x08, 0xc0, 0x00, 0xb0, 0x1c, 0x2b, 0x11, 0xda, 0x77, // 11984 + 0x70, 0x36, 0x9b, 0xae, 0xce, 0x79, 0x87, 0xdc, 0x82, 0x70, 0xe6, 0x09, 0x74, 0x70, 0x55, 0x69, // 12000 + 0xaf, 0xa3, 0x68, 0x9f, 0xbf, 0xdd, 0xb6, 0x79, 0xb3, 0xf2, 0x9d, 0x70, 0x29, 0x55, 0xf4, 0xab, // 12016 + 0xff, 0x95, 0x61, 0xf3, 0xc9, 0x40, 0x6f, 0x1d, 0xd1, 0xbe, 0x93, 0xbb, 0xd3, 0x88, 0x2a, 0xbb, // 12032 + 0x9d, 0xbf, 0x72, 0x5a, 0x56, 0x71, 0x3b, 0x3f, 0xd4, 0xf3, 0xd1, 0x0a, 0xfe, 0x28, 0xef, 0xa3, // 12048 + 0xee, 0xd9, 0x99, 0xaf, 0x03, 0xd3, 0x8f, 0x60, 0xb7, 0xf2, 0x92, 0xa1, 0xb1, 0xbd, 0x89, 0x89, // 12064 + 0x1f, 0x30, 0xcd, 0xc3, 0xa6, 0x2e, 0x62, 0x33, 0xae, 0x16, 0x02, 0x77, 0x44, 0x5a, 0xe7, 0x81, // 12080 + 0x0a, 0x3c, 0xa7, 0x44, 0x2e, 0x79, 0xb8, 0x3f, 0x04, 0xbc, 0x5c, 0xa0, 0x87, 0xe1, 0x1b, 0xaf, // 12096 + 0x51, 0x8e, 0xcd, 0xec, 0x2c, 0xfa, 0xf8, 0xfe, 0x6d, 0xf0, 0x3a, 0x7c, 0xaa, 0x8b, 0xe4, 0x67, // 12112 + 0x95, 0x31, 0x8d, 0x02, 0x03, 0x01, 0x00, 0x01, 0x00, 0x67, 0x00, 0x78, 0x30, 0x65, 0x31, 0x0b, // 12128 + 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, 0x15, 0x30, 0x13, 0x06, // 12144 + 0x03, 0x55, 0x04, 0x0a, 0x13, 0x0c, 0x44, 0x69, 0x67, 0x69, 0x43, 0x65, 0x72, 0x74, 0x20, 0x49, // 12160 + 0x6e, 0x63, 0x31, 0x19, 0x30, 0x17, 0x06, 0x03, 0x55, 0x04, 0x0b, 0x13, 0x10, 0x77, 0x77, 0x77, // 12176 + 0x2e, 0x64, 0x69, 0x67, 0x69, 0x63, 0x65, 0x72, 0x74, 0x2e, 0x63, 0x6f, 0x6d, 0x31, 0x24, 0x30, // 12192 + 0x22, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x1b, 0x44, 0x69, 0x67, 0x69, 0x43, 0x65, 0x72, 0x74, // 12208 + 0x20, 0x41, 0x73, 0x73, 0x75, 0x72, 0x65, 0x64, 0x20, 0x49, 0x44, 0x20, 0x52, 0x6f, 0x6f, 0x74, // 12224 + 0x20, 0x47, 0x33, 0x30, 0x76, 0x30, 0x10, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, // 12240 + 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x22, 0x03, 0x62, 0x00, 0x04, 0x19, 0xe7, 0xbc, 0xac, 0x44, // 12256 + 0x65, 0xed, 0xcd, 0xb8, 0x3f, 0x58, 0xfb, 0x8d, 0xb1, 0x57, 0xa9, 0x44, 0x2d, 0x05, 0x15, 0xf2, // 12272 + 0xef, 0x0b, 0xff, 0x10, 0x74, 0x9f, 0xb5, 0x62, 0x52, 0x5f, 0x66, 0x7e, 0x1f, 0xe5, 0xdc, 0x1b, // 12288 + 0x45, 0x79, 0x0b, 0xcc, 0xc6, 0x53, 0x0a, 0x9d, 0x8d, 0x5d, 0x02, 0xd9, 0xa9, 0x59, 0xde, 0x02, // 12304 + 0x5a, 0xf6, 0x95, 0x2a, 0x0e, 0x8d, 0x38, 0x4a, 0x8a, 0x49, 0xc6, 0xbc, 0xc6, 0x03, 0x38, 0x07, // 12320 + 0x5f, 0x55, 0xda, 0x7e, 0x09, 0x6e, 0xe2, 0x7f, 0x5e, 0xd0, 0x45, 0x20, 0x0f, 0x59, 0x76, 0x10, // 12336 + 0xd6, 0xa0, 0x24, 0xf0, 0x2d, 0xde, 0x36, 0xf2, 0x6c, 0x29, 0x39, 0x00, 0x6a, 0x01, 0x24, 0x30, // 12352 + 0x68, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, 0x25, // 12368 + 0x30, 0x23, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x1c, 0x53, 0x74, 0x61, 0x72, 0x66, 0x69, 0x65, // 12384 + 0x6c, 0x64, 0x20, 0x54, 0x65, 0x63, 0x68, 0x6e, 0x6f, 0x6c, 0x6f, 0x67, 0x69, 0x65, 0x73, 0x2c, // 12400 + 0x20, 0x49, 0x6e, 0x63, 0x2e, 0x31, 0x32, 0x30, 0x30, 0x06, 0x03, 0x55, 0x04, 0x0b, 0x13, 0x29, // 12416 + 0x53, 0x74, 0x61, 0x72, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x20, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x20, // 12432 + 0x32, 0x20, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, // 12448 + 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x30, 0x82, 0x01, 0x20, 0x30, 0x0d, 0x06, // 12464 + 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0d, // 12480 + 0x00, 0x30, 0x82, 0x01, 0x08, 0x02, 0x82, 0x01, 0x01, 0x00, 0xb7, 0x32, 0xc8, 0xfe, 0xe9, 0x71, // 12496 + 0xa6, 0x04, 0x85, 0xad, 0x0c, 0x11, 0x64, 0xdf, 0xce, 0x4d, 0xef, 0xc8, 0x03, 0x18, 0x87, 0x3f, // 12512 + 0xa1, 0xab, 0xfb, 0x3c, 0xa6, 0x9f, 0xf0, 0xc3, 0xa1, 0xda, 0xd4, 0xd8, 0x6e, 0x2b, 0x53, 0x90, // 12528 + 0xfb, 0x24, 0xa4, 0x3e, 0x84, 0xf0, 0x9e, 0xe8, 0x5f, 0xec, 0xe5, 0x27, 0x44, 0xf5, 0x28, 0xa6, // 12544 + 0x3f, 0x7b, 0xde, 0xe0, 0x2a, 0xf0, 0xc8, 0xaf, 0x53, 0x2f, 0x9e, 0xca, 0x05, 0x01, 0x93, 0x1e, // 12560 + 0x8f, 0x66, 0x1c, 0x39, 0xa7, 0x4d, 0xfa, 0x5a, 0xb6, 0x73, 0x04, 0x25, 0x66, 0xeb, 0x77, 0x7f, // 12576 + 0xe7, 0x59, 0xc6, 0x4a, 0x99, 0x25, 0x14, 0x54, 0xeb, 0x26, 0xc7, 0xf3, 0x7f, 0x19, 0xd5, 0x30, // 12592 + 0x70, 0x8f, 0xaf, 0xb0, 0x46, 0x2a, 0xff, 0xad, 0xeb, 0x29, 0xed, 0xd7, 0x9f, 0xaa, 0x04, 0x87, // 12608 + 0xa3, 0xd4, 0xf9, 0x89, 0xa5, 0x34, 0x5f, 0xdb, 0x43, 0x91, 0x82, 0x36, 0xd9, 0x66, 0x3c, 0xb1, // 12624 + 0xb8, 0xb9, 0x82, 0xfd, 0x9c, 0x3a, 0x3e, 0x10, 0xc8, 0x3b, 0xef, 0x06, 0x65, 0x66, 0x7a, 0x9b, // 12640 + 0x19, 0x18, 0x3d, 0xff, 0x71, 0x51, 0x3c, 0x30, 0x2e, 0x5f, 0xbe, 0x3d, 0x77, 0x73, 0xb2, 0x5d, // 12656 + 0x06, 0x6c, 0xc3, 0x23, 0x56, 0x9a, 0x2b, 0x85, 0x26, 0x92, 0x1c, 0xa7, 0x02, 0xb3, 0xe4, 0x3f, // 12672 + 0x0d, 0xaf, 0x08, 0x79, 0x82, 0xb8, 0x36, 0x3d, 0xea, 0x9c, 0xd3, 0x35, 0xb3, 0xbc, 0x69, 0xca, // 12688 + 0xf5, 0xcc, 0x9d, 0xe8, 0xfd, 0x64, 0x8d, 0x17, 0x80, 0x33, 0x6e, 0x5e, 0x4a, 0x5d, 0x99, 0xc9, // 12704 + 0x1e, 0x87, 0xb4, 0x9d, 0x1a, 0xc0, 0xd5, 0x6e, 0x13, 0x35, 0x23, 0x5e, 0xdf, 0x9b, 0x5f, 0x3d, // 12720 + 0xef, 0xd6, 0xf7, 0x76, 0xc2, 0xea, 0x3e, 0xbb, 0x78, 0x0d, 0x1c, 0x42, 0x67, 0x6b, 0x04, 0xd8, // 12736 + 0xf8, 0xd6, 0xda, 0x6f, 0x8b, 0xf2, 0x44, 0xa0, 0x01, 0xab, 0x02, 0x01, 0x03, 0x00, 0x6e, 0x01, // 12752 + 0x26, 0x30, 0x6c, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, // 12768 + 0x31, 0x15, 0x30, 0x13, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x0c, 0x44, 0x69, 0x67, 0x69, 0x43, // 12784 + 0x65, 0x72, 0x74, 0x20, 0x49, 0x6e, 0x63, 0x31, 0x19, 0x30, 0x17, 0x06, 0x03, 0x55, 0x04, 0x0b, // 12800 + 0x13, 0x10, 0x77, 0x77, 0x77, 0x2e, 0x64, 0x69, 0x67, 0x69, 0x63, 0x65, 0x72, 0x74, 0x2e, 0x63, // 12816 + 0x6f, 0x6d, 0x31, 0x2b, 0x30, 0x29, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x22, 0x44, 0x69, 0x67, // 12832 + 0x69, 0x43, 0x65, 0x72, 0x74, 0x20, 0x48, 0x69, 0x67, 0x68, 0x20, 0x41, 0x73, 0x73, 0x75, 0x72, // 12848 + 0x61, 0x6e, 0x63, 0x65, 0x20, 0x45, 0x56, 0x20, 0x52, 0x6f, 0x6f, 0x74, 0x20, 0x43, 0x41, 0x30, // 12864 + 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, // 12880 + 0x05, 0x00, 0x03, 0x82, 0x01, 0x0f, 0x00, 0x30, 0x82, 0x01, 0x0a, 0x02, 0x82, 0x01, 0x01, 0x00, // 12896 + 0xc6, 0xcc, 0xe5, 0x73, 0xe6, 0xfb, 0xd4, 0xbb, 0xe5, 0x2d, 0x2d, 0x32, 0xa6, 0xdf, 0xe5, 0x81, // 12912 + 0x3f, 0xc9, 0xcd, 0x25, 0x49, 0xb6, 0x71, 0x2a, 0xc3, 0xd5, 0x94, 0x34, 0x67, 0xa2, 0x0a, 0x1c, // 12928 + 0xb0, 0x5f, 0x69, 0xa6, 0x40, 0xb1, 0xc4, 0xb7, 0xb2, 0x8f, 0xd0, 0x98, 0xa4, 0xa9, 0x41, 0x59, // 12944 + 0x3a, 0xd3, 0xdc, 0x94, 0xd6, 0x3c, 0xdb, 0x74, 0x38, 0xa4, 0x4a, 0xcc, 0x4d, 0x25, 0x82, 0xf7, // 12960 + 0x4a, 0xa5, 0x53, 0x12, 0x38, 0xee, 0xf3, 0x49, 0x6d, 0x71, 0x91, 0x7e, 0x63, 0xb6, 0xab, 0xa6, // 12976 + 0x5f, 0xc3, 0xa4, 0x84, 0xf8, 0x4f, 0x62, 0x51, 0xbe, 0xf8, 0xc5, 0xec, 0xdb, 0x38, 0x92, 0xe3, // 12992 + 0x06, 0xe5, 0x08, 0x91, 0x0c, 0xc4, 0x28, 0x41, 0x55, 0xfb, 0xcb, 0x5a, 0x89, 0x15, 0x7e, 0x71, // 13008 + 0xe8, 0x35, 0xbf, 0x4d, 0x72, 0x09, 0x3d, 0xbe, 0x3a, 0x38, 0x50, 0x5b, 0x77, 0x31, 0x1b, 0x8d, // 13024 + 0xb3, 0xc7, 0x24, 0x45, 0x9a, 0xa7, 0xac, 0x6d, 0x00, 0x14, 0x5a, 0x04, 0xb7, 0xba, 0x13, 0xeb, // 13040 + 0x51, 0x0a, 0x98, 0x41, 0x41, 0x22, 0x4e, 0x65, 0x61, 0x87, 0x81, 0x41, 0x50, 0xa6, 0x79, 0x5c, // 13056 + 0x89, 0xde, 0x19, 0x4a, 0x57, 0xd5, 0x2e, 0xe6, 0x5d, 0x1c, 0x53, 0x2c, 0x7e, 0x98, 0xcd, 0x1a, // 13072 + 0x06, 0x16, 0xa4, 0x68, 0x73, 0xd0, 0x34, 0x04, 0x13, 0x5c, 0xa1, 0x71, 0xd3, 0x5a, 0x7c, 0x55, // 13088 + 0xdb, 0x5e, 0x64, 0xe1, 0x37, 0x87, 0x30, 0x56, 0x04, 0xe5, 0x11, 0xb4, 0x29, 0x80, 0x12, 0xf1, // 13104 + 0x79, 0x39, 0x88, 0xa2, 0x02, 0x11, 0x7c, 0x27, 0x66, 0xb7, 0x88, 0xb7, 0x78, 0xf2, 0xca, 0x0a, // 13120 + 0xa8, 0x38, 0xab, 0x0a, 0x64, 0xc2, 0xbf, 0x66, 0x5d, 0x95, 0x84, 0xc1, 0xa1, 0x25, 0x1e, 0x87, // 13136 + 0x5d, 0x1a, 0x50, 0x0b, 0x20, 0x12, 0xcc, 0x41, 0xbb, 0x6e, 0x0b, 0x51, 0x38, 0xb8, 0x4b, 0xcb, // 13152 + 0x02, 0x03, 0x01, 0x00, 0x01, 0x00, 0x84, 0x01, 0x26, 0x30, 0x81, 0x81, 0x31, 0x0b, 0x30, 0x09, // 13168 + 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x47, 0x42, 0x31, 0x1b, 0x30, 0x19, 0x06, 0x03, 0x55, // 13184 + 0x04, 0x08, 0x13, 0x12, 0x47, 0x72, 0x65, 0x61, 0x74, 0x65, 0x72, 0x20, 0x4d, 0x61, 0x6e, 0x63, // 13200 + 0x68, 0x65, 0x73, 0x74, 0x65, 0x72, 0x31, 0x10, 0x30, 0x0e, 0x06, 0x03, 0x55, 0x04, 0x07, 0x13, // 13216 + 0x07, 0x53, 0x61, 0x6c, 0x66, 0x6f, 0x72, 0x64, 0x31, 0x1a, 0x30, 0x18, 0x06, 0x03, 0x55, 0x04, // 13232 + 0x0a, 0x13, 0x11, 0x43, 0x4f, 0x4d, 0x4f, 0x44, 0x4f, 0x20, 0x43, 0x41, 0x20, 0x4c, 0x69, 0x6d, // 13248 + 0x69, 0x74, 0x65, 0x64, 0x31, 0x27, 0x30, 0x25, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x1e, 0x43, // 13264 + 0x4f, 0x4d, 0x4f, 0x44, 0x4f, 0x20, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, // 13280 + 0x69, 0x6f, 0x6e, 0x20, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x30, 0x82, 0x01, // 13296 + 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, // 13312 + 0x03, 0x82, 0x01, 0x0f, 0x00, 0x30, 0x82, 0x01, 0x0a, 0x02, 0x82, 0x01, 0x01, 0x00, 0xd0, 0x40, // 13328 + 0x8b, 0x8b, 0x72, 0xe3, 0x91, 0x1b, 0xf7, 0x51, 0xc1, 0x1b, 0x54, 0x04, 0x98, 0xd3, 0xa9, 0xbf, // 13344 + 0xc1, 0xe6, 0x8a, 0x5d, 0x3b, 0x87, 0xfb, 0xbb, 0x88, 0xce, 0x0d, 0xe3, 0x2f, 0x3f, 0x06, 0x96, // 13360 + 0xf0, 0xa2, 0x29, 0x50, 0x99, 0xae, 0xdb, 0x3b, 0xa1, 0x57, 0xb0, 0x74, 0x51, 0x71, 0xcd, 0xed, // 13376 + 0x42, 0x91, 0x4d, 0x41, 0xfe, 0xa9, 0xc8, 0xd8, 0x6a, 0x86, 0x77, 0x44, 0xbb, 0x59, 0x66, 0x97, // 13392 + 0x50, 0x5e, 0xb4, 0xd4, 0x2c, 0x70, 0x44, 0xcf, 0xda, 0x37, 0x95, 0x42, 0x69, 0x3c, 0x30, 0xc4, // 13408 + 0x71, 0xb3, 0x52, 0xf0, 0x21, 0x4d, 0xa1, 0xd8, 0xba, 0x39, 0x7c, 0x1c, 0x9e, 0xa3, 0x24, 0x9d, // 13424 + 0xf2, 0x83, 0x16, 0x98, 0xaa, 0x16, 0x7c, 0x43, 0x9b, 0x15, 0x5b, 0xb7, 0xae, 0x34, 0x91, 0xfe, // 13440 + 0xd4, 0x62, 0x26, 0x18, 0x46, 0x9a, 0x3f, 0xeb, 0xc1, 0xf9, 0xf1, 0x90, 0x57, 0xeb, 0xac, 0x7a, // 13456 + 0x0d, 0x8b, 0xdb, 0x72, 0x30, 0x6a, 0x66, 0xd5, 0xe0, 0x46, 0xa3, 0x70, 0xdc, 0x68, 0xd9, 0xff, // 13472 + 0x04, 0x48, 0x89, 0x77, 0xde, 0xb5, 0xe9, 0xfb, 0x67, 0x6d, 0x41, 0xe9, 0xbc, 0x39, 0xbd, 0x32, // 13488 + 0xd9, 0x62, 0x02, 0xf1, 0xb1, 0xa8, 0x3d, 0x6e, 0x37, 0x9c, 0xe2, 0x2f, 0xe2, 0xd3, 0xa2, 0x26, // 13504 + 0x8b, 0xc6, 0xb8, 0x55, 0x43, 0x88, 0xe1, 0x23, 0x3e, 0xa5, 0xd2, 0x24, 0x39, 0x6a, 0x47, 0xab, // 13520 + 0x00, 0xd4, 0xa1, 0xb3, 0xa9, 0x25, 0xfe, 0x0d, 0x3f, 0xa7, 0x1d, 0xba, 0xd3, 0x51, 0xc1, 0x0b, // 13536 + 0xa4, 0xda, 0xac, 0x38, 0xef, 0x55, 0x50, 0x24, 0x05, 0x65, 0x46, 0x93, 0x34, 0x4f, 0x2d, 0x8d, // 13552 + 0xad, 0xc6, 0xd4, 0x21, 0x19, 0xd2, 0x8e, 0xca, 0x05, 0x61, 0x71, 0x07, 0x73, 0x47, 0xe5, 0x8a, // 13568 + 0x19, 0x12, 0xbd, 0x04, 0x4d, 0xce, 0x4e, 0x9c, 0xa5, 0x48, 0xac, 0xbb, 0x26, 0xf7, 0x02, 0x03, // 13584 + 0x01, 0x00, 0x01, 0x00, 0x86, 0x01, 0x26, 0x30, 0x81, 0x83, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, // 13600 + 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, 0x10, 0x30, 0x0e, 0x06, 0x03, 0x55, 0x04, 0x08, // 13616 + 0x13, 0x07, 0x41, 0x72, 0x69, 0x7a, 0x6f, 0x6e, 0x61, 0x31, 0x13, 0x30, 0x11, 0x06, 0x03, 0x55, // 13632 + 0x04, 0x07, 0x13, 0x0a, 0x53, 0x63, 0x6f, 0x74, 0x74, 0x73, 0x64, 0x61, 0x6c, 0x65, 0x31, 0x1a, // 13648 + 0x30, 0x18, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x11, 0x47, 0x6f, 0x44, 0x61, 0x64, 0x64, 0x79, // 13664 + 0x2e, 0x63, 0x6f, 0x6d, 0x2c, 0x20, 0x49, 0x6e, 0x63, 0x2e, 0x31, 0x31, 0x30, 0x2f, 0x06, 0x03, // 13680 + 0x55, 0x04, 0x03, 0x13, 0x28, 0x47, 0x6f, 0x20, 0x44, 0x61, 0x64, 0x64, 0x79, 0x20, 0x52, 0x6f, // 13696 + 0x6f, 0x74, 0x20, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x20, 0x41, // 13712 + 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x20, 0x2d, 0x20, 0x47, 0x32, 0x30, 0x82, 0x01, // 13728 + 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, // 13744 + 0x03, 0x82, 0x01, 0x0f, 0x00, 0x30, 0x82, 0x01, 0x0a, 0x02, 0x82, 0x01, 0x01, 0x00, 0xbf, 0x71, // 13760 + 0x62, 0x08, 0xf1, 0xfa, 0x59, 0x34, 0xf7, 0x1b, 0xc9, 0x18, 0xa3, 0xf7, 0x80, 0x49, 0x58, 0xe9, // 13776 + 0x22, 0x83, 0x13, 0xa6, 0xc5, 0x20, 0x43, 0x01, 0x3b, 0x84, 0xf1, 0xe6, 0x85, 0x49, 0x9f, 0x27, // 13792 + 0xea, 0xf6, 0x84, 0x1b, 0x4e, 0xa0, 0xb4, 0xdb, 0x70, 0x98, 0xc7, 0x32, 0x01, 0xb1, 0x05, 0x3e, // 13808 + 0x07, 0x4e, 0xee, 0xf4, 0xfa, 0x4f, 0x2f, 0x59, 0x30, 0x22, 0xe7, 0xab, 0x19, 0x56, 0x6b, 0xe2, // 13824 + 0x80, 0x07, 0xfc, 0xf3, 0x16, 0x75, 0x80, 0x39, 0x51, 0x7b, 0xe5, 0xf9, 0x35, 0xb6, 0x74, 0x4e, // 13840 + 0xa9, 0x8d, 0x82, 0x13, 0xe4, 0xb6, 0x3f, 0xa9, 0x03, 0x83, 0xfa, 0xa2, 0xbe, 0x8a, 0x15, 0x6a, // 13856 + 0x7f, 0xde, 0x0b, 0xc3, 0xb6, 0x19, 0x14, 0x05, 0xca, 0xea, 0xc3, 0xa8, 0x04, 0x94, 0x3b, 0x46, // 13872 + 0x7c, 0x32, 0x0d, 0xf3, 0x00, 0x66, 0x22, 0xc8, 0x8d, 0x69, 0x6d, 0x36, 0x8c, 0x11, 0x18, 0xb7, // 13888 + 0xd3, 0xb2, 0x1c, 0x60, 0xb4, 0x38, 0xfa, 0x02, 0x8c, 0xce, 0xd3, 0xdd, 0x46, 0x07, 0xde, 0x0a, // 13904 + 0x3e, 0xeb, 0x5d, 0x7c, 0xc8, 0x7c, 0xfb, 0xb0, 0x2b, 0x53, 0xa4, 0x92, 0x62, 0x69, 0x51, 0x25, // 13920 + 0x05, 0x61, 0x1a, 0x44, 0x81, 0x8c, 0x2c, 0xa9, 0x43, 0x96, 0x23, 0xdf, 0xac, 0x3a, 0x81, 0x9a, // 13936 + 0x0e, 0x29, 0xc5, 0x1c, 0xa9, 0xe9, 0x5d, 0x1e, 0xb6, 0x9e, 0x9e, 0x30, 0x0a, 0x39, 0xce, 0xf1, // 13952 + 0x88, 0x80, 0xfb, 0x4b, 0x5d, 0xcc, 0x32, 0xec, 0x85, 0x62, 0x43, 0x25, 0x34, 0x02, 0x56, 0x27, // 13968 + 0x01, 0x91, 0xb4, 0x3b, 0x70, 0x2a, 0x3f, 0x6e, 0xb1, 0xe8, 0x9c, 0x88, 0x01, 0x7d, 0x9f, 0xd4, // 13984 + 0xf9, 0xdb, 0x53, 0x6d, 0x60, 0x9d, 0xbf, 0x2c, 0xe7, 0x58, 0xab, 0xb8, 0x5f, 0x46, 0xfc, 0xce, // 14000 + 0xc4, 0x1b, 0x03, 0x3c, 0x09, 0xeb, 0x49, 0x31, 0x5c, 0x69, 0x46, 0xb3, 0xe0, 0x47, 0x02, 0x03, // 14016 + 0x01, 0x00, 0x01, 0x00, 0x88, 0x00, 0x78, 0x30, 0x81, 0x85, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, // 14032 + 0x55, 0x04, 0x06, 0x13, 0x02, 0x47, 0x42, 0x31, 0x1b, 0x30, 0x19, 0x06, 0x03, 0x55, 0x04, 0x08, // 14048 + 0x13, 0x12, 0x47, 0x72, 0x65, 0x61, 0x74, 0x65, 0x72, 0x20, 0x4d, 0x61, 0x6e, 0x63, 0x68, 0x65, // 14064 + 0x73, 0x74, 0x65, 0x72, 0x31, 0x10, 0x30, 0x0e, 0x06, 0x03, 0x55, 0x04, 0x07, 0x13, 0x07, 0x53, // 14080 + 0x61, 0x6c, 0x66, 0x6f, 0x72, 0x64, 0x31, 0x1a, 0x30, 0x18, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, // 14096 + 0x11, 0x43, 0x4f, 0x4d, 0x4f, 0x44, 0x4f, 0x20, 0x43, 0x41, 0x20, 0x4c, 0x69, 0x6d, 0x69, 0x74, // 14112 + 0x65, 0x64, 0x31, 0x2b, 0x30, 0x29, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x22, 0x43, 0x4f, 0x4d, // 14128 + 0x4f, 0x44, 0x4f, 0x20, 0x45, 0x43, 0x43, 0x20, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, // 14144 + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x30, // 14160 + 0x76, 0x30, 0x10, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x05, 0x2b, 0x81, // 14176 + 0x04, 0x00, 0x22, 0x03, 0x62, 0x00, 0x04, 0x03, 0x47, 0x7b, 0x2f, 0x75, 0xc9, 0x82, 0x15, 0x85, // 14192 + 0xfb, 0x75, 0xe4, 0x91, 0x16, 0xd4, 0xab, 0x62, 0x99, 0xf5, 0x3e, 0x52, 0x0b, 0x06, 0xce, 0x41, // 14208 + 0x00, 0x7f, 0x97, 0xe1, 0x0a, 0x24, 0x3c, 0x1d, 0x01, 0x04, 0xee, 0x3d, 0xd2, 0x8d, 0x09, 0x97, // 14224 + 0x0c, 0xe0, 0x75, 0xe4, 0xfa, 0xfb, 0x77, 0x8a, 0x2a, 0xf5, 0x03, 0x60, 0x4b, 0x36, 0x8b, 0x16, // 14240 + 0x23, 0x16, 0xad, 0x09, 0x71, 0xf4, 0x4a, 0xf4, 0x28, 0x50, 0xb4, 0xfe, 0x88, 0x1c, 0x6e, 0x3f, // 14256 + 0x6c, 0x2f, 0x2f, 0x09, 0x59, 0x5b, 0xa5, 0x5b, 0x0b, 0x33, 0x99, 0xe2, 0xc3, 0x3d, 0x89, 0xf9, // 14272 + 0x6a, 0x2c, 0xef, 0xb2, 0xd3, 0x06, 0xe9, 0x00, 0x88, 0x02, 0x26, 0x30, 0x81, 0x85, 0x31, 0x0b, // 14288 + 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x47, 0x42, 0x31, 0x1b, 0x30, 0x19, 0x06, // 14304 + 0x03, 0x55, 0x04, 0x08, 0x13, 0x12, 0x47, 0x72, 0x65, 0x61, 0x74, 0x65, 0x72, 0x20, 0x4d, 0x61, // 14320 + 0x6e, 0x63, 0x68, 0x65, 0x73, 0x74, 0x65, 0x72, 0x31, 0x10, 0x30, 0x0e, 0x06, 0x03, 0x55, 0x04, // 14336 + 0x07, 0x13, 0x07, 0x53, 0x61, 0x6c, 0x66, 0x6f, 0x72, 0x64, 0x31, 0x1a, 0x30, 0x18, 0x06, 0x03, // 14352 + 0x55, 0x04, 0x0a, 0x13, 0x11, 0x43, 0x4f, 0x4d, 0x4f, 0x44, 0x4f, 0x20, 0x43, 0x41, 0x20, 0x4c, // 14368 + 0x69, 0x6d, 0x69, 0x74, 0x65, 0x64, 0x31, 0x2b, 0x30, 0x29, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, // 14384 + 0x22, 0x43, 0x4f, 0x4d, 0x4f, 0x44, 0x4f, 0x20, 0x52, 0x53, 0x41, 0x20, 0x43, 0x65, 0x72, 0x74, // 14400 + 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, // 14416 + 0x69, 0x74, 0x79, 0x30, 0x82, 0x02, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, // 14432 + 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x02, 0x0f, 0x00, 0x30, 0x82, 0x02, 0x0a, 0x02, // 14448 + 0x82, 0x02, 0x01, 0x00, 0x91, 0xe8, 0x54, 0x92, 0xd2, 0x0a, 0x56, 0xb1, 0xac, 0x0d, 0x24, 0xdd, // 14464 + 0xc5, 0xcf, 0x44, 0x67, 0x74, 0x99, 0x2b, 0x37, 0xa3, 0x7d, 0x23, 0x70, 0x00, 0x71, 0xbc, 0x53, // 14480 + 0xdf, 0xc4, 0xfa, 0x2a, 0x12, 0x8f, 0x4b, 0x7f, 0x10, 0x56, 0xbd, 0x9f, 0x70, 0x72, 0xb7, 0x61, // 14496 + 0x7f, 0xc9, 0x4b, 0x0f, 0x17, 0xa7, 0x3d, 0xe3, 0xb0, 0x04, 0x61, 0xee, 0xff, 0x11, 0x97, 0xc7, // 14512 + 0xf4, 0x86, 0x3e, 0x0a, 0xfa, 0x3e, 0x5c, 0xf9, 0x93, 0xe6, 0x34, 0x7a, 0xd9, 0x14, 0x6b, 0xe7, // 14528 + 0x9c, 0xb3, 0x85, 0xa0, 0x82, 0x7a, 0x76, 0xaf, 0x71, 0x90, 0xd7, 0xec, 0xfd, 0x0d, 0xfa, 0x9c, // 14544 + 0x6c, 0xfa, 0xdf, 0xb0, 0x82, 0xf4, 0x14, 0x7e, 0xf9, 0xbe, 0xc4, 0xa6, 0x2f, 0x4f, 0x7f, 0x99, // 14560 + 0x7f, 0xb5, 0xfc, 0x67, 0x43, 0x72, 0xbd, 0x0c, 0x00, 0xd6, 0x89, 0xeb, 0x6b, 0x2c, 0xd3, 0xed, // 14576 + 0x8f, 0x98, 0x1c, 0x14, 0xab, 0x7e, 0xe5, 0xe3, 0x6e, 0xfc, 0xd8, 0xa8, 0xe4, 0x92, 0x24, 0xda, // 14592 + 0x43, 0x6b, 0x62, 0xb8, 0x55, 0xfd, 0xea, 0xc1, 0xbc, 0x6c, 0xb6, 0x8b, 0xf3, 0x0e, 0x8d, 0x9a, // 14608 + 0xe4, 0x9b, 0x6c, 0x69, 0x99, 0xf8, 0x78, 0x48, 0x30, 0x45, 0xd5, 0xad, 0xe1, 0x0d, 0x3c, 0x45, // 14624 + 0x60, 0xfc, 0x32, 0x96, 0x51, 0x27, 0xbc, 0x67, 0xc3, 0xca, 0x2e, 0xb6, 0x6b, 0xea, 0x46, 0xc7, // 14640 + 0xc7, 0x20, 0xa0, 0xb1, 0x1f, 0x65, 0xde, 0x48, 0x08, 0xba, 0xa4, 0x4e, 0xa9, 0xf2, 0x83, 0x46, // 14656 + 0x37, 0x84, 0xeb, 0xe8, 0xcc, 0x81, 0x48, 0x43, 0x67, 0x4e, 0x72, 0x2a, 0x9b, 0x5c, 0xbd, 0x4c, // 14672 + 0x1b, 0x28, 0x8a, 0x5c, 0x22, 0x7b, 0xb4, 0xab, 0x98, 0xd9, 0xee, 0xe0, 0x51, 0x83, 0xc3, 0x09, // 14688 + 0x46, 0x4e, 0x6d, 0x3e, 0x99, 0xfa, 0x95, 0x17, 0xda, 0x7c, 0x33, 0x57, 0x41, 0x3c, 0x8d, 0x51, // 14704 + 0xed, 0x0b, 0xb6, 0x5c, 0xaf, 0x2c, 0x63, 0x1a, 0xdf, 0x57, 0xc8, 0x3f, 0xbc, 0xe9, 0x5d, 0xc4, // 14720 + 0x9b, 0xaf, 0x45, 0x99, 0xe2, 0xa3, 0x5a, 0x24, 0xb4, 0xba, 0xa9, 0x56, 0x3d, 0xcf, 0x6f, 0xaa, // 14736 + 0xff, 0x49, 0x58, 0xbe, 0xf0, 0xa8, 0xff, 0xf4, 0xb8, 0xad, 0xe9, 0x37, 0xfb, 0xba, 0xb8, 0xf4, // 14752 + 0x0b, 0x3a, 0xf9, 0xe8, 0x43, 0x42, 0x1e, 0x89, 0xd8, 0x84, 0xcb, 0x13, 0xf1, 0xd9, 0xbb, 0xe1, // 14768 + 0x89, 0x60, 0xb8, 0x8c, 0x28, 0x56, 0xac, 0x14, 0x1d, 0x9c, 0x0a, 0xe7, 0x71, 0xeb, 0xcf, 0x0e, // 14784 + 0xdd, 0x3d, 0xa9, 0x96, 0xa1, 0x48, 0xbd, 0x3c, 0xf7, 0xaf, 0xb5, 0x0d, 0x22, 0x4c, 0xc0, 0x11, // 14800 + 0x81, 0xec, 0x56, 0x3b, 0xf6, 0xd3, 0xa2, 0xe2, 0x5b, 0xb7, 0xb2, 0x04, 0x22, 0x52, 0x95, 0x80, // 14816 + 0x93, 0x69, 0xe8, 0x8e, 0x4c, 0x65, 0xf1, 0x91, 0x03, 0x2d, 0x70, 0x74, 0x02, 0xea, 0x8b, 0x67, // 14832 + 0x15, 0x29, 0x69, 0x52, 0x02, 0xbb, 0xd7, 0xdf, 0x50, 0x6a, 0x55, 0x46, 0xbf, 0xa0, 0xa3, 0x28, // 14848 + 0x61, 0x7f, 0x70, 0xd0, 0xc3, 0xa2, 0xaa, 0x2c, 0x21, 0xaa, 0x47, 0xce, 0x28, 0x9c, 0x06, 0x45, // 14864 + 0x76, 0xbf, 0x82, 0x18, 0x27, 0xb4, 0xd5, 0xae, 0xb4, 0xcb, 0x50, 0xe6, 0x6b, 0xf4, 0x4c, 0x86, // 14880 + 0x71, 0x30, 0xe9, 0xa6, 0xdf, 0x16, 0x86, 0xe0, 0xd8, 0xff, 0x40, 0xdd, 0xfb, 0xd0, 0x42, 0x88, // 14896 + 0x7f, 0xa3, 0x33, 0x3a, 0x2e, 0x5c, 0x1e, 0x41, 0x11, 0x81, 0x63, 0xce, 0x18, 0x71, 0x6b, 0x2b, // 14912 + 0xec, 0xa6, 0x8a, 0xb7, 0x31, 0x5c, 0x3a, 0x6a, 0x47, 0xe0, 0xc3, 0x79, 0x59, 0xd6, 0x20, 0x1a, // 14928 + 0xaf, 0xf2, 0x6a, 0x98, 0xaa, 0x72, 0xbc, 0x57, 0x4a, 0xd2, 0x4b, 0x9d, 0xbb, 0x10, 0xfc, 0xb0, // 14944 + 0x4c, 0x41, 0xe5, 0xed, 0x1d, 0x3d, 0x5e, 0x28, 0x9d, 0x9c, 0xcc, 0xbf, 0xb3, 0x51, 0xda, 0xa7, // 14960 + 0x47, 0xe5, 0x84, 0x53, 0x02, 0x03, 0x01, 0x00, 0x01, 0x00, 0x8b, 0x00, 0x78, 0x30, 0x81, 0x88, // 14976 + 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, 0x13, 0x30, // 14992 + 0x11, 0x06, 0x03, 0x55, 0x04, 0x08, 0x13, 0x0a, 0x4e, 0x65, 0x77, 0x20, 0x4a, 0x65, 0x72, 0x73, // 15008 + 0x65, 0x79, 0x31, 0x14, 0x30, 0x12, 0x06, 0x03, 0x55, 0x04, 0x07, 0x13, 0x0b, 0x4a, 0x65, 0x72, // 15024 + 0x73, 0x65, 0x79, 0x20, 0x43, 0x69, 0x74, 0x79, 0x31, 0x1e, 0x30, 0x1c, 0x06, 0x03, 0x55, 0x04, // 15040 + 0x0a, 0x13, 0x15, 0x54, 0x68, 0x65, 0x20, 0x55, 0x53, 0x45, 0x52, 0x54, 0x52, 0x55, 0x53, 0x54, // 15056 + 0x20, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x31, 0x2e, 0x30, 0x2c, 0x06, 0x03, 0x55, 0x04, // 15072 + 0x03, 0x13, 0x25, 0x55, 0x53, 0x45, 0x52, 0x54, 0x72, 0x75, 0x73, 0x74, 0x20, 0x45, 0x43, 0x43, // 15088 + 0x20, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x41, // 15104 + 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x30, 0x76, 0x30, 0x10, 0x06, 0x07, 0x2a, 0x86, // 15120 + 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x22, 0x03, 0x62, 0x00, 0x04, // 15136 + 0x1a, 0xac, 0x54, 0x5a, 0xa9, 0xf9, 0x68, 0x23, 0xe7, 0x7a, 0xd5, 0x24, 0x6f, 0x53, 0xc6, 0x5a, // 15152 + 0xd8, 0x4b, 0xab, 0xc6, 0xd5, 0xb6, 0xd1, 0xe6, 0x73, 0x71, 0xae, 0xdd, 0x9c, 0xd6, 0x0c, 0x61, // 15168 + 0xfd, 0xdb, 0xa0, 0x89, 0x03, 0xb8, 0x05, 0x14, 0xec, 0x57, 0xce, 0xee, 0x5d, 0x3f, 0xe2, 0x21, // 15184 + 0xb3, 0xce, 0xf7, 0xd4, 0x8a, 0x79, 0xe0, 0xa3, 0x83, 0x7e, 0x2d, 0x97, 0xd0, 0x61, 0xc4, 0xf1, // 15200 + 0x99, 0xdc, 0x25, 0x91, 0x63, 0xab, 0x7f, 0x30, 0xa3, 0xb4, 0x70, 0xe2, 0xc7, 0xa1, 0x33, 0x9c, // 15216 + 0xf3, 0xbf, 0x2e, 0x5c, 0x53, 0xb1, 0x5f, 0xb3, 0x7d, 0x32, 0x7f, 0x8a, 0x34, 0xe3, 0x79, 0x79, // 15232 + 0x00, 0x8b, 0x02, 0x26, 0x30, 0x81, 0x88, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, // 15248 + 0x13, 0x02, 0x55, 0x53, 0x31, 0x13, 0x30, 0x11, 0x06, 0x03, 0x55, 0x04, 0x08, 0x13, 0x0a, 0x4e, // 15264 + 0x65, 0x77, 0x20, 0x4a, 0x65, 0x72, 0x73, 0x65, 0x79, 0x31, 0x14, 0x30, 0x12, 0x06, 0x03, 0x55, // 15280 + 0x04, 0x07, 0x13, 0x0b, 0x4a, 0x65, 0x72, 0x73, 0x65, 0x79, 0x20, 0x43, 0x69, 0x74, 0x79, 0x31, // 15296 + 0x1e, 0x30, 0x1c, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x15, 0x54, 0x68, 0x65, 0x20, 0x55, 0x53, // 15312 + 0x45, 0x52, 0x54, 0x52, 0x55, 0x53, 0x54, 0x20, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x31, // 15328 + 0x2e, 0x30, 0x2c, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x25, 0x55, 0x53, 0x45, 0x52, 0x54, 0x72, // 15344 + 0x75, 0x73, 0x74, 0x20, 0x52, 0x53, 0x41, 0x20, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, // 15360 + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x30, // 15376 + 0x82, 0x02, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, // 15392 + 0x05, 0x00, 0x03, 0x82, 0x02, 0x0f, 0x00, 0x30, 0x82, 0x02, 0x0a, 0x02, 0x82, 0x02, 0x01, 0x00, // 15408 + 0x80, 0x12, 0x65, 0x17, 0x36, 0x0e, 0xc3, 0xdb, 0x08, 0xb3, 0xd0, 0xac, 0x57, 0x0d, 0x76, 0xed, // 15424 + 0xcd, 0x27, 0xd3, 0x4c, 0xad, 0x50, 0x83, 0x61, 0xe2, 0xaa, 0x20, 0x4d, 0x09, 0x2d, 0x64, 0x09, // 15440 + 0xdc, 0xce, 0x89, 0x9f, 0xcc, 0x3d, 0xa9, 0xec, 0xf6, 0xcf, 0xc1, 0xdc, 0xf1, 0xd3, 0xb1, 0xd6, // 15456 + 0x7b, 0x37, 0x28, 0x11, 0x2b, 0x47, 0xda, 0x39, 0xc6, 0xbc, 0x3a, 0x19, 0xb4, 0x5f, 0xa6, 0xbd, // 15472 + 0x7d, 0x9d, 0xa3, 0x63, 0x42, 0xb6, 0x76, 0xf2, 0xa9, 0x3b, 0x2b, 0x91, 0xf8, 0xe2, 0x6f, 0xd0, // 15488 + 0xec, 0x16, 0x20, 0x90, 0x09, 0x3e, 0xe2, 0xe8, 0x74, 0xc9, 0x18, 0xb4, 0x91, 0xd4, 0x62, 0x64, // 15504 + 0xdb, 0x7f, 0xa3, 0x06, 0xf1, 0x88, 0x18, 0x6a, 0x90, 0x22, 0x3c, 0xbc, 0xfe, 0x13, 0xf0, 0x87, // 15520 + 0x14, 0x7b, 0xf6, 0xe4, 0x1f, 0x8e, 0xd4, 0xe4, 0x51, 0xc6, 0x11, 0x67, 0x46, 0x08, 0x51, 0xcb, // 15536 + 0x86, 0x14, 0x54, 0x3f, 0xbc, 0x33, 0xfe, 0x7e, 0x6c, 0x9c, 0xff, 0x16, 0x9d, 0x18, 0xbd, 0x51, // 15552 + 0x8e, 0x35, 0xa6, 0xa7, 0x66, 0xc8, 0x72, 0x67, 0xdb, 0x21, 0x66, 0xb1, 0xd4, 0x9b, 0x78, 0x03, // 15568 + 0xc0, 0x50, 0x3a, 0xe8, 0xcc, 0xf0, 0xdc, 0xbc, 0x9e, 0x4c, 0xfe, 0xaf, 0x05, 0x96, 0x35, 0x1f, // 15584 + 0x57, 0x5a, 0xb7, 0xff, 0xce, 0xf9, 0x3d, 0xb7, 0x2c, 0xb6, 0xf6, 0x54, 0xdd, 0xc8, 0xe7, 0x12, // 15600 + 0x3a, 0x4d, 0xae, 0x4c, 0x8a, 0xb7, 0x5c, 0x9a, 0xb4, 0xb7, 0x20, 0x3d, 0xca, 0x7f, 0x22, 0x34, // 15616 + 0xae, 0x7e, 0x3b, 0x68, 0x66, 0x01, 0x44, 0xe7, 0x01, 0x4e, 0x46, 0x53, 0x9b, 0x33, 0x60, 0xf7, // 15632 + 0x94, 0xbe, 0x53, 0x37, 0x90, 0x73, 0x43, 0xf3, 0x32, 0xc3, 0x53, 0xef, 0xdb, 0xaa, 0xfe, 0x74, // 15648 + 0x4e, 0x69, 0xc7, 0x6b, 0x8c, 0x60, 0x93, 0xde, 0xc4, 0xc7, 0x0c, 0xdf, 0xe1, 0x32, 0xae, 0xcc, // 15664 + 0x93, 0x3b, 0x51, 0x78, 0x95, 0x67, 0x8b, 0xee, 0x3d, 0x56, 0xfe, 0x0c, 0xd0, 0x69, 0x0f, 0x1b, // 15680 + 0x0f, 0xf3, 0x25, 0x26, 0x6b, 0x33, 0x6d, 0xf7, 0x6e, 0x47, 0xfa, 0x73, 0x43, 0xe5, 0x7e, 0x0e, // 15696 + 0xa5, 0x66, 0xb1, 0x29, 0x7c, 0x32, 0x84, 0x63, 0x55, 0x89, 0xc4, 0x0d, 0xc1, 0x93, 0x54, 0x30, // 15712 + 0x19, 0x13, 0xac, 0xd3, 0x7d, 0x37, 0xa7, 0xeb, 0x5d, 0x3a, 0x6c, 0x35, 0x5c, 0xdb, 0x41, 0xd7, // 15728 + 0x12, 0xda, 0xa9, 0x49, 0x0b, 0xdf, 0xd8, 0x80, 0x8a, 0x09, 0x93, 0x62, 0x8e, 0xb5, 0x66, 0xcf, // 15744 + 0x25, 0x88, 0xcd, 0x84, 0xb8, 0xb1, 0x3f, 0xa4, 0x39, 0x0f, 0xd9, 0x02, 0x9e, 0xeb, 0x12, 0x4c, // 15760 + 0x95, 0x7c, 0xf3, 0x6b, 0x05, 0xa9, 0x5e, 0x16, 0x83, 0xcc, 0xb8, 0x67, 0xe2, 0xe8, 0x13, 0x9d, // 15776 + 0xcc, 0x5b, 0x82, 0xd3, 0x4c, 0xb3, 0xed, 0x5b, 0xff, 0xde, 0xe5, 0x73, 0xac, 0x23, 0x3b, 0x2d, // 15792 + 0x00, 0xbf, 0x35, 0x55, 0x74, 0x09, 0x49, 0xd8, 0x49, 0x58, 0x1a, 0x7f, 0x92, 0x36, 0xe6, 0x51, // 15808 + 0x92, 0x0e, 0xf3, 0x26, 0x7d, 0x1c, 0x4d, 0x17, 0xbc, 0xc9, 0xec, 0x43, 0x26, 0xd0, 0xbf, 0x41, // 15824 + 0x5f, 0x40, 0xa9, 0x44, 0x44, 0xf4, 0x99, 0xe7, 0x57, 0x87, 0x9e, 0x50, 0x1f, 0x57, 0x54, 0xa8, // 15840 + 0x3e, 0xfd, 0x74, 0x63, 0x2f, 0xb1, 0x50, 0x65, 0x09, 0xe6, 0x58, 0x42, 0x2e, 0x43, 0x1a, 0x4c, // 15856 + 0xb4, 0xf0, 0x25, 0x47, 0x59, 0xfa, 0x04, 0x1e, 0x93, 0xd4, 0x26, 0x46, 0x4a, 0x50, 0x81, 0xb2, // 15872 + 0xde, 0xbe, 0x78, 0xb7, 0xfc, 0x67, 0x15, 0xe1, 0xc9, 0x57, 0x84, 0x1e, 0x0f, 0x63, 0xd6, 0xe9, // 15888 + 0x62, 0xba, 0xd6, 0x5f, 0x55, 0x2e, 0xea, 0x5c, 0xc6, 0x28, 0x08, 0x04, 0x25, 0x39, 0xb8, 0x0e, // 15904 + 0x2b, 0xa9, 0xf2, 0x4c, 0x97, 0x1c, 0x07, 0x3f, 0x0d, 0x52, 0xf5, 0xed, 0xef, 0x2f, 0x82, 0x0f, // 15920 + 0x02, 0x03, 0x01, 0x00, 0x01, 0x00, 0x92, 0x01, 0x26, 0x30, 0x81, 0x8f, 0x31, 0x0b, 0x30, 0x09, // 15936 + 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, 0x10, 0x30, 0x0e, 0x06, 0x03, 0x55, // 15952 + 0x04, 0x08, 0x13, 0x07, 0x41, 0x72, 0x69, 0x7a, 0x6f, 0x6e, 0x61, 0x31, 0x13, 0x30, 0x11, 0x06, // 15968 + 0x03, 0x55, 0x04, 0x07, 0x13, 0x0a, 0x53, 0x63, 0x6f, 0x74, 0x74, 0x73, 0x64, 0x61, 0x6c, 0x65, // 15984 + 0x31, 0x25, 0x30, 0x23, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x1c, 0x53, 0x74, 0x61, 0x72, 0x66, // 16000 + 0x69, 0x65, 0x6c, 0x64, 0x20, 0x54, 0x65, 0x63, 0x68, 0x6e, 0x6f, 0x6c, 0x6f, 0x67, 0x69, 0x65, // 16016 + 0x73, 0x2c, 0x20, 0x49, 0x6e, 0x63, 0x2e, 0x31, 0x32, 0x30, 0x30, 0x06, 0x03, 0x55, 0x04, 0x03, // 16032 + 0x13, 0x29, 0x53, 0x74, 0x61, 0x72, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x20, 0x52, 0x6f, 0x6f, 0x74, // 16048 + 0x20, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x20, 0x41, 0x75, 0x74, // 16064 + 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x20, 0x2d, 0x20, 0x47, 0x32, 0x30, 0x82, 0x01, 0x22, 0x30, // 16080 + 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, // 16096 + 0x01, 0x0f, 0x00, 0x30, 0x82, 0x01, 0x0a, 0x02, 0x82, 0x01, 0x01, 0x00, 0xbd, 0xed, 0xc1, 0x03, // 16112 + 0xfc, 0xf6, 0x8f, 0xfc, 0x02, 0xb1, 0x6f, 0x5b, 0x9f, 0x48, 0xd9, 0x9d, 0x79, 0xe2, 0xa2, 0xb7, // 16128 + 0x03, 0x61, 0x56, 0x18, 0xc3, 0x47, 0xb6, 0xd7, 0xca, 0x3d, 0x35, 0x2e, 0x89, 0x43, 0xf7, 0xa1, // 16144 + 0x69, 0x9b, 0xde, 0x8a, 0x1a, 0xfd, 0x13, 0x20, 0x9c, 0xb4, 0x49, 0x77, 0x32, 0x29, 0x56, 0xfd, // 16160 + 0xb9, 0xec, 0x8c, 0xdd, 0x22, 0xfa, 0x72, 0xdc, 0x27, 0x61, 0x97, 0xee, 0xf6, 0x5a, 0x84, 0xec, // 16176 + 0x6e, 0x19, 0xb9, 0x89, 0x2c, 0xdc, 0x84, 0x5b, 0xd5, 0x74, 0xfb, 0x6b, 0x5f, 0xc5, 0x89, 0xa5, // 16192 + 0x10, 0x52, 0x89, 0x46, 0x55, 0xf4, 0xb8, 0x75, 0x1c, 0xe6, 0x7f, 0xe4, 0x54, 0xae, 0x4b, 0xf8, // 16208 + 0x55, 0x72, 0x57, 0x02, 0x19, 0xf8, 0x17, 0x71, 0x59, 0xeb, 0x1e, 0x28, 0x07, 0x74, 0xc5, 0x9d, // 16224 + 0x48, 0xbe, 0x6c, 0xb4, 0xf4, 0xa4, 0xb0, 0xf3, 0x64, 0x37, 0x79, 0x92, 0xc0, 0xec, 0x46, 0x5e, // 16240 + 0x7f, 0xe1, 0x6d, 0x53, 0x4c, 0x62, 0xaf, 0xcd, 0x1f, 0x0b, 0x63, 0xbb, 0x3a, 0x9d, 0xfb, 0xfc, // 16256 + 0x79, 0x00, 0x98, 0x61, 0x74, 0xcf, 0x26, 0x82, 0x40, 0x63, 0xf3, 0xb2, 0x72, 0x6a, 0x19, 0x0d, // 16272 + 0x99, 0xca, 0xd4, 0x0e, 0x75, 0xcc, 0x37, 0xfb, 0x8b, 0x89, 0xc1, 0x59, 0xf1, 0x62, 0x7f, 0x5f, // 16288 + 0xb3, 0x5f, 0x65, 0x30, 0xf8, 0xa7, 0xb7, 0x4d, 0x76, 0x5a, 0x1e, 0x76, 0x5e, 0x34, 0xc0, 0xe8, // 16304 + 0x96, 0x56, 0x99, 0x8a, 0xb3, 0xf0, 0x7f, 0xa4, 0xcd, 0xbd, 0xdc, 0x32, 0x31, 0x7c, 0x91, 0xcf, // 16320 + 0xe0, 0x5f, 0x11, 0xf8, 0x6b, 0xaa, 0x49, 0x5c, 0xd1, 0x99, 0x94, 0xd1, 0xa2, 0xe3, 0x63, 0x5b, // 16336 + 0x09, 0x76, 0xb5, 0x56, 0x62, 0xe1, 0x4b, 0x74, 0x1d, 0x96, 0xd4, 0x26, 0xd4, 0x08, 0x04, 0x59, // 16352 + 0xd0, 0x98, 0x0e, 0x0e, 0xe6, 0xde, 0xfc, 0xc3, 0xec, 0x1f, 0x90, 0xf1, 0x02, 0x03, 0x01, 0x00, // 16368 + 0x01, 0x00, 0x9b, 0x01, 0x26, 0x30, 0x81, 0x98, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, // 16384 + 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, 0x10, 0x30, 0x0e, 0x06, 0x03, 0x55, 0x04, 0x08, 0x13, 0x07, // 16400 + 0x41, 0x72, 0x69, 0x7a, 0x6f, 0x6e, 0x61, 0x31, 0x13, 0x30, 0x11, 0x06, 0x03, 0x55, 0x04, 0x07, // 16416 + 0x13, 0x0a, 0x53, 0x63, 0x6f, 0x74, 0x74, 0x73, 0x64, 0x61, 0x6c, 0x65, 0x31, 0x25, 0x30, 0x23, // 16432 + 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x1c, 0x53, 0x74, 0x61, 0x72, 0x66, 0x69, 0x65, 0x6c, 0x64, // 16448 + 0x20, 0x54, 0x65, 0x63, 0x68, 0x6e, 0x6f, 0x6c, 0x6f, 0x67, 0x69, 0x65, 0x73, 0x2c, 0x20, 0x49, // 16464 + 0x6e, 0x63, 0x2e, 0x31, 0x3b, 0x30, 0x39, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x32, 0x53, 0x74, // 16480 + 0x61, 0x72, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x20, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, // 16496 + 0x20, 0x52, 0x6f, 0x6f, 0x74, 0x20, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, // 16512 + 0x65, 0x20, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x20, 0x2d, 0x20, 0x47, 0x32, // 16528 + 0x30, 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, // 16544 + 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0f, 0x00, 0x30, 0x82, 0x01, 0x0a, 0x02, 0x82, 0x01, 0x01, // 16560 + 0x00, 0xd5, 0x0c, 0x3a, 0xc4, 0x2a, 0xf9, 0x4e, 0xe2, 0xf5, 0xbe, 0x19, 0x97, 0x5f, 0x8e, 0x88, // 16576 + 0x53, 0xb1, 0x1f, 0x3f, 0xcb, 0xcf, 0x9f, 0x20, 0x13, 0x6d, 0x29, 0x3a, 0xc8, 0x0f, 0x7d, 0x3c, // 16592 + 0xf7, 0x6b, 0x76, 0x38, 0x63, 0xd9, 0x36, 0x60, 0xa8, 0x9b, 0x5e, 0x5c, 0x00, 0x80, 0xb2, 0x2f, // 16608 + 0x59, 0x7f, 0xf6, 0x87, 0xf9, 0x25, 0x43, 0x86, 0xe7, 0x69, 0x1b, 0x52, 0x9a, 0x90, 0xe1, 0x71, // 16624 + 0xe3, 0xd8, 0x2d, 0x0d, 0x4e, 0x6f, 0xf6, 0xc8, 0x49, 0xd9, 0xb6, 0xf3, 0x1a, 0x56, 0xae, 0x2b, // 16640 + 0xb6, 0x74, 0x14, 0xeb, 0xcf, 0xfb, 0x26, 0xe3, 0x1a, 0xba, 0x1d, 0x96, 0x2e, 0x6a, 0x3b, 0x58, // 16656 + 0x94, 0x89, 0x47, 0x56, 0xff, 0x25, 0xa0, 0x93, 0x70, 0x53, 0x83, 0xda, 0x84, 0x74, 0x14, 0xc3, // 16672 + 0x67, 0x9e, 0x04, 0x68, 0x3a, 0xdf, 0x8e, 0x40, 0x5a, 0x1d, 0x4a, 0x4e, 0xcf, 0x43, 0x91, 0x3b, // 16688 + 0xe7, 0x56, 0xd6, 0x00, 0x70, 0xcb, 0x52, 0xee, 0x7b, 0x7d, 0xae, 0x3a, 0xe7, 0xbc, 0x31, 0xf9, // 16704 + 0x45, 0xf6, 0xc2, 0x60, 0xcf, 0x13, 0x59, 0x02, 0x2b, 0x80, 0xcc, 0x34, 0x47, 0xdf, 0xb9, 0xde, // 16720 + 0x90, 0x65, 0x6d, 0x02, 0xcf, 0x2c, 0x91, 0xa6, 0xa6, 0xe7, 0xde, 0x85, 0x18, 0x49, 0x7c, 0x66, // 16736 + 0x4e, 0xa3, 0x3a, 0x6d, 0xa9, 0xb5, 0xee, 0x34, 0x2e, 0xba, 0x0d, 0x03, 0xb8, 0x33, 0xdf, 0x47, // 16752 + 0xeb, 0xb1, 0x6b, 0x8d, 0x25, 0xd9, 0x9b, 0xce, 0x81, 0xd1, 0x45, 0x46, 0x32, 0x96, 0x70, 0x87, // 16768 + 0xde, 0x02, 0x0e, 0x49, 0x43, 0x85, 0xb6, 0x6c, 0x73, 0xbb, 0x64, 0xea, 0x61, 0x41, 0xac, 0xc9, // 16784 + 0xd4, 0x54, 0xdf, 0x87, 0x2f, 0xc7, 0x22, 0xb2, 0x26, 0xcc, 0x9f, 0x59, 0x54, 0x68, 0x9f, 0xfc, // 16800 + 0xbe, 0x2a, 0x2f, 0xc4, 0x55, 0x1c, 0x75, 0x40, 0x60, 0x17, 0x85, 0x02, 0x55, 0x39, 0x8b, 0x7f, // 16816 + 0x05, 0x02, 0x03, 0x01, 0x00, 0x01, // 16822 +}; + diff --git a/Firmware/RTK_Surveyor/ZED.ino b/Firmware/RTK_Surveyor/ZED.ino new file mode 100644 index 000000000..e2e2ae25a --- /dev/null +++ b/Firmware/RTK_Surveyor/ZED.ino @@ -0,0 +1,109 @@ +// Enable data output from the NEO +bool zedEnableLBandCommunication() +{ + bool response = true; + +#ifdef COMPILE_L_BAND + + response &= theGNSS.setRXMCORcallbackPtr(&checkRXMCOR); // Enable callback to check if the PMP data is being decrypted successfully + + if (productVariant == RTK_FACET_LBAND_DIRECT) + { + // Setup for ZED to NEO serial communication + response &= theGNSS.setVal32(UBLOX_CFG_UART2INPROT_UBX, true); // Configure ZED for UBX input on UART2 + + // Disable PMP callback over I2C. Enable UARTs. + response &= i2cLBand.setRXMPMPmessageCallbackPtr(nullptr); // Disable PMP callback to push raw PMP over I2C + + response &= i2cLBand.newCfgValset(); + response &= i2cLBand.addCfgValset(UBLOX_CFG_MSGOUT_UBX_RXM_PMP_I2C, 0); // Disable UBX-RXM-PMP on NEO's I2C port + + response &= i2cLBand.addCfgValset(UBLOX_CFG_UART2OUTPROT_UBX, 1); // Enable UBX output on NEO's UART2 + response &= i2cLBand.addCfgValset(UBLOX_CFG_MSGOUT_UBX_RXM_PMP_UART2, 1); // Output UBX-RXM-PMP on NEO's UART2 + response &= i2cLBand.addCfgValset(UBLOX_CFG_UART2_BAUDRATE, settings.radioPortBaud); // Match baudrate with ZED + } + else if (productVariant == RTK_FACET_LBAND) + { + // Older versions of the Facet L-Band had solder jumpers that could be closed to directly connect the NEO + // to the ZED. If the user has explicitly disabled I2C corrections, enable a UART connection. + if (settings.useI2cForLbandCorrections == true) + { + response &= theGNSS.setVal32(UBLOX_CFG_UART2INPROT_UBX, settings.enableUART2UBXIn); + + i2cLBand.setRXMPMPmessageCallbackPtr(&pushRXMPMP); // Enable PMP callback to push raw PMP over I2C + + response &= i2cLBand.newCfgValset(); + response &= i2cLBand.addCfgValset(UBLOX_CFG_MSGOUT_UBX_RXM_PMP_I2C, 1); // Enable UBX-RXM-PMP on NEO's I2C port + + response &= i2cLBand.addCfgValset(UBLOX_CFG_UART2OUTPROT_UBX, 0); // Disable UBX output on NEO's UART2 + response &= i2cLBand.addCfgValset(UBLOX_CFG_MSGOUT_UBX_RXM_PMP_UART2, 0); // Disable UBX-RXM-PMP on NEO's UART2 + } + else // Setup ZED to NEO serial communication + { + response &= theGNSS.setVal32(UBLOX_CFG_UART2INPROT_UBX, true); // Configure ZED for UBX input on UART2 + + i2cLBand.setRXMPMPmessageCallbackPtr(nullptr); // Disable PMP callback to push raw PMP over I2C + + response &= i2cLBand.newCfgValset(); + response &= i2cLBand.addCfgValset(UBLOX_CFG_MSGOUT_UBX_RXM_PMP_I2C, 0); // Disable UBX-RXM-PMP on NEO's I2C port + + response &= i2cLBand.addCfgValset(UBLOX_CFG_UART2OUTPROT_UBX, 1); // Enable UBX output on UART2 + response &= i2cLBand.addCfgValset(UBLOX_CFG_MSGOUT_UBX_RXM_PMP_UART2, 1); // Output UBX-RXM-PMP on UART2 + response &= + i2cLBand.addCfgValset(UBLOX_CFG_UART2_BAUDRATE, settings.radioPortBaud); // Match baudrate with ZED + } + } + else + { + systemPrintln("zedEnableLBandCorrections: Unknown platform"); + return (false); + } + + response &= i2cLBand.sendCfgValset(); + +#endif + + return (response); +} + +// Disable data output from the NEO +bool zedDisableLBandCommunication() +{ + bool response = true; + +#ifdef COMPILE_L_BAND + response &= i2cLBand.setRXMPMPmessageCallbackPtr(nullptr); // Disable PMP callback no matter the platform + response &= theGNSS.setRXMCORcallbackPtr(nullptr); // Disable callback to check if the PMP data is being decrypted successfully + + if (productVariant == RTK_FACET_LBAND_DIRECT) + { + response &= i2cLBand.newCfgValset(); + response &= i2cLBand.addCfgValset(UBLOX_CFG_UART2OUTPROT_UBX, 0); // Disable UBX output from NEO's UART2 + } + else if (productVariant == RTK_FACET_LBAND) + { + // Older versions of the Facet L-Band had solder jumpers that could be closed to directly connect the NEO + // to the ZED. Check if the user has explicitly set I2C corrections. + if (settings.useI2cForLbandCorrections == true) + { + response &= i2cLBand.newCfgValset(); + response &= i2cLBand.addCfgValset(UBLOX_CFG_MSGOUT_UBX_RXM_PMP_I2C, 0); // Disable UBX-RXM-PMP from NEO's I2C port + } + else // Setup ZED to NEO serial communication + { + response &= i2cLBand.newCfgValset(); + response &= i2cLBand.addCfgValset(UBLOX_CFG_UART2OUTPROT_UBX, 0); // Disable UBX output from NEO's UART2 + } + } + else + { + systemPrintln("zedEnableLBandCorrections: Unknown platform"); + return (false); + } + + response &= i2cLBand.sendCfgValset(); + +#endif + + return (response); +} \ No newline at end of file diff --git a/Firmware/RTK_Surveyor/bluetoothSelect.h b/Firmware/RTK_Surveyor/bluetoothSelect.h new file mode 100644 index 000000000..11d1279a0 --- /dev/null +++ b/Firmware/RTK_Surveyor/bluetoothSelect.h @@ -0,0 +1,169 @@ +#ifdef COMPILE_BT + +//We use a local copy of the BluetoothSerial library so that we can increase the RX buffer. See issues: +//https://github.com/sparkfun/SparkFun_RTK_Firmware/issues/23 +//https://github.com/sparkfun/SparkFun_RTK_Firmware/issues/469 +#include "src/BluetoothSerial/BluetoothSerial.h" + +#include //Click here to get the library: http://librarymanager/All#ESP32_BleSerial v1.0.4 by Avinab Malla + +class BTSerialInterface +{ + public: + virtual bool begin(String deviceName, bool isMaster, uint16_t rxQueueSize, uint16_t txQueueSize) = 0; + virtual void disconnect() = 0; + virtual void end() = 0; + virtual esp_err_t register_callback(esp_spp_cb_t *callback) = 0; + virtual void setTimeout(unsigned long timeout) = 0; + + virtual int available() = 0; + virtual size_t readBytes(uint8_t *buffer, size_t bufferSize) = 0; + virtual int read() = 0; + + // virtual bool isCongested() = 0; + virtual size_t write(const uint8_t *buffer, size_t size) = 0; + virtual size_t write(uint8_t value) = 0; + virtual void flush() = 0; +}; + +class BTClassicSerial : public virtual BTSerialInterface, public BluetoothSerial +{ + // Everything is already implemented in BluetoothSerial since the code was + // originally written using that class + public: + bool begin(String deviceName, bool isMaster, uint16_t rxQueueSize, uint16_t txQueueSize) + { + return BluetoothSerial::begin(deviceName, isMaster, rxQueueSize, txQueueSize); + } + + void disconnect() + { + BluetoothSerial::disconnect(); + } + + void end() + { + BluetoothSerial::end(); + } + + esp_err_t register_callback(esp_spp_cb_t *callback) + { + return BluetoothSerial::register_callback(callback); + } + + void setTimeout(unsigned long timeout) + { + BluetoothSerial::setTimeout(timeout); + } + + int available() + { + return BluetoothSerial::available(); + } + + size_t readBytes(uint8_t *buffer, size_t bufferSize) + { + return BluetoothSerial::readBytes(buffer, bufferSize); + } + + int read() + { + return BluetoothSerial::read(); + } + + size_t write(const uint8_t *buffer, size_t size) + { + return BluetoothSerial::write(buffer, size); + } + + size_t write(uint8_t value) + { + return BluetoothSerial::write(value); + } + + void flush() + { + BluetoothSerial::flush(); + } +}; + +class BTLESerial : public virtual BTSerialInterface, public BleSerial +{ + public: + // Missing from BleSerial + bool begin(String deviceName, bool isMaster, uint16_t rxQueueSize, uint16_t txQueueSize) + { + BleSerial::begin(deviceName.c_str()); + return true; + } + + void disconnect() + { + Server->disconnect(Server->getConnId()); + } + + void end() + { + BleSerial::end(); + } + + esp_err_t register_callback(esp_spp_cb_t *callback) + { + connectionCallback = callback; + return ESP_OK; + } + + void setTimeout(unsigned long timeout) + { + BleSerial::setTimeout(timeout); + } + + int available() + { + return BleSerial::available(); + } + + size_t readBytes(uint8_t *buffer, size_t bufferSize) + { + return BleSerial::readBytes(buffer, bufferSize); + } + + int read() + { + return BleSerial::read(); + } + + size_t write(const uint8_t *buffer, size_t size) + { + return BleSerial::write(buffer, size); + } + + size_t write(uint8_t value) + { + return BleSerial::write(value); + } + + void flush() + { + BleSerial::flush(); + } + + // override BLEServerCallbacks + void onConnect(BLEServer *pServer) + { + // bleConnected = true; Removed until PR is accepted + connectionCallback(ESP_SPP_SRV_OPEN_EVT, nullptr); + } + + void onDisconnect(BLEServer *pServer) + { + // bleConnected = false; Removed until PR is accepted + connectionCallback(ESP_SPP_CLOSE_EVT, nullptr); + Server->startAdvertising(); + } + + private: + esp_spp_cb_t *connectionCallback; +}; + +#endif // COMPILE_BT diff --git a/Firmware/RTK_Surveyor/crc24q.h b/Firmware/RTK_Surveyor/crc24q.h new file mode 100644 index 000000000..793cf8c1d --- /dev/null +++ b/Firmware/RTK_Surveyor/crc24q.h @@ -0,0 +1,99 @@ +/* + This is an implementation of the CRC-24Q cyclic redundancy checksum + used by Qualcomm, RTCM104V3, and PGP 6.5.1. According to the RTCM104V3 + standard, it uses the error polynomial + + x^24+ x^23+ x^18+ x^17+ x^14+ x^11+ x^10+ x^7+ x^6+ x^5+ x^4+ x^3+ x+1 + + This corresponds to a mask of 0x1864CFB. For a primer on CRC theory, + including detailed discussion of how and why the error polynomial is + expressed by this mask, see . + + 1) It detects all single bit errors per 24-bit code word. + 2) It detects all double bit error combinations in a code word. + 3) It detects any odd number of errors. + 4) It detects any burst error for which the length of the burst is less than + or equal to 24 bits. + 5) It detects most large error bursts with length greater than 24 bits; + the odds of a false positive are at most 2^-23. + + This hash should not be considered cryptographically secure, but it + is extremely good at detecting noise errors. + + Note that this version has a seed of 0 wired in. The RTCM104V3 standard + requires this. + + This file is Copyright 2008 by the GPSD project + SPDX-License-Identifier: BSD-2-clause +*/ + +//This file is originally from: https://gitlab.com/gpsd/gpsd/-/blob/master/gpsd/crc24q.c + +static const int unsigned crc24q[256] = { + 0x00000000u, 0x01864CFBu, 0x028AD50Du, 0x030C99F6u, + 0x0493E6E1u, 0x0515AA1Au, 0x061933ECu, 0x079F7F17u, + 0x08A18139u, 0x0927CDC2u, 0x0A2B5434u, 0x0BAD18CFu, + 0x0C3267D8u, 0x0DB42B23u, 0x0EB8B2D5u, 0x0F3EFE2Eu, + 0x10C54E89u, 0x11430272u, 0x124F9B84u, 0x13C9D77Fu, + 0x1456A868u, 0x15D0E493u, 0x16DC7D65u, 0x175A319Eu, + 0x1864CFB0u, 0x19E2834Bu, 0x1AEE1ABDu, 0x1B685646u, + 0x1CF72951u, 0x1D7165AAu, 0x1E7DFC5Cu, 0x1FFBB0A7u, + 0x200CD1E9u, 0x218A9D12u, 0x228604E4u, 0x2300481Fu, + 0x249F3708u, 0x25197BF3u, 0x2615E205u, 0x2793AEFEu, + 0x28AD50D0u, 0x292B1C2Bu, 0x2A2785DDu, 0x2BA1C926u, + 0x2C3EB631u, 0x2DB8FACAu, 0x2EB4633Cu, 0x2F322FC7u, + 0x30C99F60u, 0x314FD39Bu, 0x32434A6Du, 0x33C50696u, + 0x345A7981u, 0x35DC357Au, 0x36D0AC8Cu, 0x3756E077u, + 0x38681E59u, 0x39EE52A2u, 0x3AE2CB54u, 0x3B6487AFu, + 0x3CFBF8B8u, 0x3D7DB443u, 0x3E712DB5u, 0x3FF7614Eu, + 0x4019A3D2u, 0x419FEF29u, 0x429376DFu, 0x43153A24u, + 0x448A4533u, 0x450C09C8u, 0x4600903Eu, 0x4786DCC5u, + 0x48B822EBu, 0x493E6E10u, 0x4A32F7E6u, 0x4BB4BB1Du, + 0x4C2BC40Au, 0x4DAD88F1u, 0x4EA11107u, 0x4F275DFCu, + 0x50DCED5Bu, 0x515AA1A0u, 0x52563856u, 0x53D074ADu, + 0x544F0BBAu, 0x55C94741u, 0x56C5DEB7u, 0x5743924Cu, + 0x587D6C62u, 0x59FB2099u, 0x5AF7B96Fu, 0x5B71F594u, + 0x5CEE8A83u, 0x5D68C678u, 0x5E645F8Eu, 0x5FE21375u, + 0x6015723Bu, 0x61933EC0u, 0x629FA736u, 0x6319EBCDu, + 0x648694DAu, 0x6500D821u, 0x660C41D7u, 0x678A0D2Cu, + 0x68B4F302u, 0x6932BFF9u, 0x6A3E260Fu, 0x6BB86AF4u, + 0x6C2715E3u, 0x6DA15918u, 0x6EADC0EEu, 0x6F2B8C15u, + 0x70D03CB2u, 0x71567049u, 0x725AE9BFu, 0x73DCA544u, + 0x7443DA53u, 0x75C596A8u, 0x76C90F5Eu, 0x774F43A5u, + 0x7871BD8Bu, 0x79F7F170u, 0x7AFB6886u, 0x7B7D247Du, + 0x7CE25B6Au, 0x7D641791u, 0x7E688E67u, 0x7FEEC29Cu, + 0x803347A4u, 0x81B50B5Fu, 0x82B992A9u, 0x833FDE52u, + 0x84A0A145u, 0x8526EDBEu, 0x862A7448u, 0x87AC38B3u, + 0x8892C69Du, 0x89148A66u, 0x8A181390u, 0x8B9E5F6Bu, + 0x8C01207Cu, 0x8D876C87u, 0x8E8BF571u, 0x8F0DB98Au, + 0x90F6092Du, 0x917045D6u, 0x927CDC20u, 0x93FA90DBu, + 0x9465EFCCu, 0x95E3A337u, 0x96EF3AC1u, 0x9769763Au, + 0x98578814u, 0x99D1C4EFu, 0x9ADD5D19u, 0x9B5B11E2u, + 0x9CC46EF5u, 0x9D42220Eu, 0x9E4EBBF8u, 0x9FC8F703u, + 0xA03F964Du, 0xA1B9DAB6u, 0xA2B54340u, 0xA3330FBBu, + 0xA4AC70ACu, 0xA52A3C57u, 0xA626A5A1u, 0xA7A0E95Au, + 0xA89E1774u, 0xA9185B8Fu, 0xAA14C279u, 0xAB928E82u, + 0xAC0DF195u, 0xAD8BBD6Eu, 0xAE872498u, 0xAF016863u, + 0xB0FAD8C4u, 0xB17C943Fu, 0xB2700DC9u, 0xB3F64132u, + 0xB4693E25u, 0xB5EF72DEu, 0xB6E3EB28u, 0xB765A7D3u, + 0xB85B59FDu, 0xB9DD1506u, 0xBAD18CF0u, 0xBB57C00Bu, + 0xBCC8BF1Cu, 0xBD4EF3E7u, 0xBE426A11u, 0xBFC426EAu, + 0xC02AE476u, 0xC1ACA88Du, 0xC2A0317Bu, 0xC3267D80u, + 0xC4B90297u, 0xC53F4E6Cu, 0xC633D79Au, 0xC7B59B61u, + 0xC88B654Fu, 0xC90D29B4u, 0xCA01B042u, 0xCB87FCB9u, + 0xCC1883AEu, 0xCD9ECF55u, 0xCE9256A3u, 0xCF141A58u, + 0xD0EFAAFFu, 0xD169E604u, 0xD2657FF2u, 0xD3E33309u, + 0xD47C4C1Eu, 0xD5FA00E5u, 0xD6F69913u, 0xD770D5E8u, + 0xD84E2BC6u, 0xD9C8673Du, 0xDAC4FECBu, 0xDB42B230u, + 0xDCDDCD27u, 0xDD5B81DCu, 0xDE57182Au, 0xDFD154D1u, + 0xE026359Fu, 0xE1A07964u, 0xE2ACE092u, 0xE32AAC69u, + 0xE4B5D37Eu, 0xE5339F85u, 0xE63F0673u, 0xE7B94A88u, + 0xE887B4A6u, 0xE901F85Du, 0xEA0D61ABu, 0xEB8B2D50u, + 0xEC145247u, 0xED921EBCu, 0xEE9E874Au, 0xEF18CBB1u, + 0xF0E37B16u, 0xF16537EDu, 0xF269AE1Bu, 0xF3EFE2E0u, + 0xF4709DF7u, 0xF5F6D10Cu, 0xF6FA48FAu, 0xF77C0401u, + 0xF842FA2Fu, 0xF9C4B6D4u, 0xFAC82F22u, 0xFB4E63D9u, + 0xFCD11CCEu, 0xFD575035u, 0xFE5BC9C3u, 0xFFDD8538u, +}; + +#define COMPUTE_CRC24Q(parse, data) (((parse)->crc << 8) ^ crc24q[data ^ (((parse)->crc >> 16) & 0xff)]) diff --git a/Firmware/RTK_Surveyor/form.h b/Firmware/RTK_Surveyor/form.h new file mode 100644 index 000000000..b49a81996 --- /dev/null +++ b/Firmware/RTK_Surveyor/form.h @@ -0,0 +1,8850 @@ +//This contains files neccessary to load the config page: +//Files that do not change (boostrap.min.css, favicon) are gzip and stored in const array +// * index.html +// * favicon.ico + +// * /src/bootstrap.bundle.min.js - Needed for popper +// * /src/bootstrap.min.css +// * /src/bootstrap.min.js +// * /src/jquery-3.6.0.min.js +// * /src/main.js +// * /src/rtk-setup.png +// * /src/style.css + +// * /src/fonts/icomoon.eot +// * /src/fonts/icomoon.svg +// * /src/fonts/icomoon.ttf +// * /src/fonts/icomoon.woof + +//To create uint8_t array from png/css/js etc. (will work on _any_ file, not just png's): +// cd Firmware\Tools +// python png_zipper.py ..\RTK_Surveyor\AP-Config\src\rtk-setup-wifi.png +// The hex is saved in ..\RTK_Surveyor\AP-Config\src\rtk-setup-wifi.png.gzip_hex + +//To convert AP-Config\src\main.js to main_js[], run the Python main_js_zipper.py script in the Tools folder: +// cd Firmware\Tools +// python main_js_zipper.py + +static const uint8_t main_js[] PROGMEM = { + 0x1F, 0x8B, 0x08, 0x08, 0x3B, 0xEE, 0x9C, 0x67, 0x02, 0xFF, 0x6D, 0x61, 0x69, 0x6E, 0x2E, 0x6A, + 0x73, 0x2E, 0x67, 0x7A, 0x69, 0x70, 0x00, 0xED, 0x7D, 0xEB, 0x7A, 0xDB, 0x38, 0x92, 0xE8, 0xFF, + 0x3C, 0x05, 0x5A, 0x67, 0x4E, 0x4B, 0x1A, 0xCB, 0xB2, 0x24, 0x5F, 0x12, 0xC7, 0xB1, 0x77, 0x7D, + 0x4B, 0xE2, 0x33, 0xB1, 0xE3, 0xCF, 0x4A, 0x3A, 0x9D, 0x64, 0x72, 0xBC, 0xB4, 0x08, 0xCB, 0x9C, + 0x48, 0xA4, 0x96, 0xA4, 0x62, 0x7B, 0x66, 0xF3, 0x4E, 0xFB, 0x0C, 0xFB, 0x64, 0xA7, 0x0A, 0x17, + 0x12, 0x00, 0xC1, 0x8B, 0x2E, 0x76, 0x32, 0x73, 0xDA, 0xDF, 0x4C, 0xDA, 0x26, 0x80, 0xAA, 0x42, + 0xA1, 0x50, 0x28, 0x14, 0x0A, 0x85, 0x6F, 0x4E, 0x48, 0x86, 0x4E, 0x4C, 0x6F, 0x9D, 0x7B, 0xB2, + 0x4B, 0xFE, 0xE3, 0x36, 0x7A, 0xBE, 0xB6, 0xF6, 0xA7, 0x7F, 0xDC, 0x7A, 0xBE, 0x1B, 0xDC, 0xB6, + 0x47, 0xC1, 0xC0, 0x89, 0xBD, 0xC0, 0x6F, 0xDF, 0x04, 0x51, 0xEC, 0x3B, 0x63, 0xFA, 0x7D, 0xED, + 0x36, 0xFA, 0x8F, 0x9D, 0x27, 0xDF, 0xA0, 0xD1, 0x2D, 0xBD, 0x8A, 0x82, 0xC1, 0x57, 0x1A, 0xEF, + 0x3C, 0x79, 0x22, 0xAA, 0x3B, 0xAE, 0x7B, 0xFC, 0x8D, 0xFA, 0xF1, 0x1B, 0x2F, 0x8A, 0xA9, 0x4F, + 0xC3, 0x46, 0x7D, 0x14, 0x38, 0x6E, 0xBD, 0x45, 0x02, 0xFF, 0x0D, 0xFC, 0xD2, 0x84, 0x9A, 0xD7, + 0x53, 0x7F, 0x80, 0x10, 0xC5, 0xA7, 0x06, 0xC5, 0xFA, 0x4D, 0xF2, 0x8F, 0x27, 0x04, 0x7E, 0x3C, + 0xDF, 0x8B, 0x3F, 0xD0, 0xAB, 0x3E, 0x03, 0xDB, 0x80, 0xEA, 0xDF, 0x95, 0x06, 0x46, 0xA1, 0x68, + 0x92, 0x50, 0x01, 0xC4, 0xFB, 0xF4, 0x96, 0xA4, 0x35, 0x44, 0xA7, 0x00, 0x8A, 0x56, 0xAF, 0x1D, + 0xF8, 0x63, 0x1A, 0x45, 0xCE, 0x90, 0x42, 0x8B, 0x04, 0x78, 0x63, 0x1C, 0x0D, 0x25, 0x48, 0xFC, + 0x99, 0x38, 0x61, 0x44, 0x4F, 0xFC, 0x41, 0x30, 0xF6, 0xFC, 0x21, 0x16, 0xB6, 0x5D, 0x27, 0x76, + 0x04, 0xAC, 0xEF, 0x3A, 0x61, 0x43, 0xDA, 0xA0, 0xB2, 0x6D, 0x48, 0xE3, 0x69, 0xE8, 0x13, 0x37, + 0x18, 0x4C, 0xC7, 0xD0, 0xB1, 0xF6, 0x90, 0xC6, 0xC7, 0x23, 0x8A, 0xBF, 0x1E, 0xDC, 0x9F, 0x40, + 0x6F, 0x79, 0x9F, 0x90, 0x7D, 0xD7, 0xDE, 0x1D, 0x75, 0xDF, 0x38, 0x48, 0x77, 0x67, 0x47, 0xF9, + 0x12, 0xF8, 0xC3, 0xF4, 0xD3, 0x64, 0xE4, 0xC4, 0xD7, 0x41, 0x38, 0x3E, 0x0F, 0x29, 0x94, 0xC2, + 0xF7, 0x5A, 0x7F, 0x1A, 0x7E, 0xA3, 0xF7, 0x41, 0x58, 0xE3, 0x15, 0x86, 0x34, 0x70, 0x69, 0xEC, + 0x0D, 0x38, 0xA0, 0x8D, 0x4E, 0xBB, 0xD3, 0x35, 0x0A, 0x80, 0xC0, 0x5D, 0xB2, 0xDA, 0xED, 0x6C, + 0xB6, 0xBB, 0xDB, 0x7A, 0xD1, 0xFE, 0x08, 0xDB, 0x74, 0x37, 0x3B, 0x9D, 0xB6, 0x68, 0x44, 0x07, + 0xF4, 0xFA, 0x77, 0x56, 0xBD, 0xF7, 0xAC, 0xD3, 0xEB, 0x6C, 0xB5, 0x37, 0xB7, 0x9E, 0xA5, 0x25, + 0x1F, 0xB1, 0x64, 0xE3, 0x69, 0x77, 0xEB, 0x59, 0x67, 0xA3, 0xBD, 0xD1, 0x59, 0x4F, 0x4B, 0x3E, + 0x31, 0xDC, 0xCF, 0xB6, 0xB6, 0xB6, 0x36, 0xDB, 0x1B, 0xCF, 0x36, 0x78, 0xC1, 0xC8, 0x89, 0xE2, + 0x97, 0xDE, 0x88, 0x9E, 0x81, 0xD8, 0x20, 0xE5, 0x35, 0xD9, 0x4B, 0xF8, 0x34, 0x1D, 0x5F, 0xD1, + 0x30, 0xED, 0xA6, 0xCF, 0xFE, 0x7E, 0x7B, 0x8D, 0xD5, 0xA3, 0x3E, 0x1D, 0xD1, 0x41, 0x4C, 0xDD, + 0xB4, 0x38, 0x12, 0x5F, 0x58, 0xB1, 0x02, 0x2A, 0xBA, 0x09, 0x40, 0xF0, 0x86, 0xF8, 0x19, 0x25, + 0x0E, 0xC7, 0xD3, 0x19, 0x45, 0x94, 0x17, 0x06, 0x57, 0xB1, 0xE3, 0xF9, 0xD4, 0x3D, 0xE5, 0x83, + 0x5D, 0xA9, 0xC2, 0x81, 0x13, 0x51, 0xBD, 0x92, 0x40, 0x21, 0xEA, 0x5C, 0xBC, 0x3B, 0x3C, 0xCD, + 0x02, 0xC2, 0x1E, 0xBD, 0x73, 0xAE, 0xE0, 0x1F, 0x7A, 0x17, 0x2B, 0xE4, 0x09, 0x31, 0x33, 0xBE, + 0x22, 0x5B, 0x04, 0xB8, 0x77, 0xF7, 0x13, 0x9A, 0x5F, 0x22, 0x88, 0xC1, 0x52, 0x56, 0x1C, 0xD2, + 0x41, 0x10, 0xBA, 0xD1, 0xF1, 0xE1, 0xF1, 0x4B, 0xF8, 0xFC, 0xF9, 0xCB, 0x8E, 0xFA, 0xF5, 0x95, + 0x18, 0x53, 0xA5, 0xE4, 0x7A, 0x3A, 0x1A, 0x9D, 0x03, 0xAC, 0xF7, 0x13, 0x10, 0x5D, 0xA5, 0x5B, + 0xA2, 0x59, 0x44, 0xE3, 0x77, 0xDE, 0x98, 0x06, 0xD3, 0x58, 0x32, 0xD9, 0x77, 0x8F, 0x40, 0xC6, + 0xB5, 0x8F, 0x83, 0x1B, 0x3A, 0xF8, 0x7A, 0x46, 0x6F, 0x5F, 0x7A, 0xE1, 0xF8, 0xD6, 0x09, 0xA9, + 0x56, 0x08, 0xB2, 0x6D, 0x2B, 0x7A, 0x32, 0x08, 0x7C, 0x60, 0xD1, 0x61, 0x00, 0x74, 0x79, 0x3E, + 0xA0, 0xC6, 0xCE, 0xE0, 0xB0, 0xF1, 0x19, 0x72, 0xF8, 0xF6, 0xED, 0xC5, 0xD1, 0xC9, 0xD9, 0xFE, + 0xBB, 0xE3, 0xCB, 0x93, 0xB3, 0xF3, 0xF7, 0xEF, 0x2E, 0xDF, 0x7D, 0x3C, 0x3F, 0xBE, 0x3C, 0x3A, + 0x7A, 0x4E, 0x3A, 0x2D, 0xB2, 0xB6, 0x76, 0x44, 0xAF, 0x9D, 0x29, 0x08, 0xE6, 0xD1, 0x51, 0xDB, + 0x95, 0x3F, 0x85, 0xED, 0x4E, 0x4F, 0x9F, 0x93, 0x2E, 0x6B, 0x09, 0xBF, 0xB6, 0xC7, 0xF8, 0x53, + 0x58, 0xFF, 0x12, 0x1B, 0xF4, 0x78, 0x03, 0x52, 0xB5, 0xC5, 0xE5, 0xD1, 0x7E, 0xFF, 0xF5, 0x73, + 0xB2, 0xCE, 0x9B, 0xAD, 0x56, 0x6E, 0xD6, 0xFF, 0x78, 0x7A, 0xF0, 0xF6, 0xCD, 0x73, 0xB2, 0xC1, + 0x1B, 0xFE, 0xCF, 0x7F, 0xCB, 0x96, 0xE3, 0x71, 0xBD, 0xA4, 0x57, 0xFD, 0xFE, 0x73, 0xB2, 0x99, + 0x90, 0x49, 0xFA, 0xFD, 0x76, 0xC4, 0x7E, 0xCA, 0x71, 0x42, 0xC3, 0xAD, 0xF9, 0x1A, 0x8A, 0x6E, + 0x3E, 0x4D, 0xBA, 0xB9, 0x3A, 0x53, 0x6B, 0xD9, 0xDB, 0x67, 0x69, 0x6F, 0xEB, 0x09, 0x80, 0x5A, + 0x69, 0x7F, 0x2F, 0xCF, 0xDE, 0x5E, 0x1E, 0x1D, 0x1F, 0x9E, 0x9C, 0xEE, 0x03, 0x8C, 0x6D, 0x39, + 0xA4, 0xFD, 0x3E, 0x59, 0x25, 0x67, 0x01, 0x71, 0xE9, 0xC0, 0x1B, 0x3B, 0xA3, 0x2A, 0x74, 0xA8, + 0x70, 0xBA, 0x1D, 0x95, 0x15, 0x33, 0x83, 0x42, 0x86, 0xE8, 0xF0, 0xBA, 0x2A, 0x73, 0xAA, 0xC2, + 0x3B, 0x39, 0xFB, 0x6D, 0xFF, 0xCD, 0xC9, 0xD1, 0xE5, 0xFB, 0xB3, 0xBF, 0x9C, 0xBD, 0xFD, 0x70, + 0x06, 0x60, 0x7A, 0x2D, 0xB9, 0x10, 0xC0, 0x74, 0xF9, 0x46, 0x43, 0x50, 0x6F, 0xE9, 0x8C, 0x41, + 0xD5, 0xD7, 0x16, 0xCA, 0x6F, 0x90, 0x7C, 0x3D, 0xF1, 0x27, 0xD3, 0x58, 0xE8, 0x0C, 0x63, 0x76, + 0xB5, 0xF3, 0xBA, 0xA1, 0xAE, 0xB8, 0x99, 0x35, 0x4D, 0x2E, 0x5A, 0x6B, 0x6B, 0x38, 0x65, 0x83, + 0x11, 0x85, 0xF5, 0x7E, 0xD8, 0xA8, 0x79, 0xA2, 0x8A, 0x54, 0x60, 0xCF, 0x49, 0x8D, 0xAC, 0x10, + 0xAC, 0x0F, 0xD0, 0xB0, 0x3E, 0x92, 0x85, 0xCB, 0x21, 0xD0, 0x81, 0x2B, 0x63, 0x34, 0x19, 0x79, + 0x71, 0xA3, 0xDE, 0xAA, 0x8B, 0xE5, 0x11, 0x16, 0x2C, 0xD2, 0x18, 0xC1, 0x8A, 0x7C, 0xC7, 0x54, + 0x38, 0xFC, 0xE7, 0x05, 0xAB, 0xDE, 0x1E, 0x51, 0x7F, 0x18, 0xDF, 0x00, 0xCF, 0xBA, 0xF8, 0x71, + 0x65, 0x97, 0xF4, 0xD4, 0x25, 0x17, 0xA1, 0x7A, 0xA8, 0xF5, 0xB1, 0xEE, 0xE7, 0xBB, 0x2F, 0x3B, + 0x5A, 0xC9, 0x37, 0x67, 0x94, 0x14, 0x01, 0x35, 0x5D, 0xA5, 0xD8, 0x24, 0xDF, 0xE5, 0x04, 0x03, + 0xAC, 0x15, 0x52, 0x6B, 0x61, 0x4B, 0xFE, 0x01, 0x7E, 0x91, 0x3D, 0xE0, 0xCD, 0xFA, 0x13, 0x18, + 0x38, 0x80, 0x0B, 0xDD, 0x1D, 0x3B, 0xBE, 0x1B, 0x25, 0x45, 0xDE, 0x35, 0x69, 0x78, 0x6E, 0x1B, + 0x18, 0x31, 0x9A, 0xBA, 0x34, 0x6A, 0xD4, 0x22, 0xF7, 0x34, 0x98, 0xFA, 0x30, 0x46, 0xB5, 0xA6, + 0x4A, 0x32, 0x07, 0xF3, 0x0E, 0x57, 0xFC, 0xC0, 0x5F, 0x0B, 0xAE, 0xAF, 0x49, 0xFF, 0x88, 0x80, + 0x2E, 0x74, 0xB4, 0x1A, 0x08, 0x8D, 0x91, 0x0F, 0xCA, 0x9C, 0xE9, 0xE0, 0x9A, 0x09, 0x03, 0x7F, + 0x6E, 0x3C, 0x97, 0x36, 0x6A, 0xB8, 0x96, 0x9C, 0x3A, 0x3E, 0x70, 0x3D, 0xAC, 0x35, 0x77, 0xB4, + 0x4A, 0xDF, 0xB5, 0xBF, 0x28, 0xC0, 0xD1, 0x20, 0xC7, 0xE1, 0xD4, 0x0E, 0x18, 0x97, 0xB0, 0x8A, + 0x80, 0xD3, 0xDF, 0x12, 0xF0, 0x38, 0x22, 0x00, 0x5D, 0x37, 0x44, 0x32, 0x78, 0x32, 0x76, 0x0A, + 0x50, 0xA5, 0x23, 0x49, 0x2C, 0xA2, 0xD8, 0x8B, 0x47, 0x6C, 0x5D, 0xBB, 0x78, 0xF7, 0x17, 0x36, + 0x2C, 0x46, 0x5B, 0x18, 0x33, 0xD2, 0x07, 0x2B, 0x6A, 0x52, 0xD3, 0x01, 0x64, 0x96, 0x33, 0xEC, + 0xB0, 0x32, 0x9C, 0x92, 0xD3, 0x26, 0x29, 0xAA, 0xCD, 0x94, 0xCF, 0x9E, 0x2B, 0x58, 0x6C, 0x0F, + 0x03, 0xFF, 0xDA, 0x1B, 0x9A, 0xDC, 0x49, 0xC7, 0x06, 0x56, 0xC8, 0x28, 0x08, 0xCB, 0x6A, 0x4D, + 0x26, 0x65, 0x35, 0x68, 0x7C, 0x43, 0x43, 0x9F, 0xC6, 0x65, 0xF5, 0xFC, 0xB8, 0x00, 0xD4, 0xDA, + 0x1A, 0xAF, 0xE4, 0x8C, 0x46, 0xC1, 0xED, 0x07, 0xEF, 0xA5, 0xF7, 0x16, 0x94, 0xC8, 0xB1, 0x84, + 0x3C, 0xF2, 0x80, 0xD5, 0xD0, 0x0C, 0xAA, 0xBD, 0x0C, 0xD0, 0x14, 0x00, 0xAB, 0x94, 0x12, 0x7A, + 0x37, 0x71, 0xFC, 0x08, 0x54, 0xC1, 0x6C, 0xD0, 0xFA, 0x14, 0xD8, 0x17, 0x16, 0x40, 0xCB, 0xA1, + 0x1F, 0x27, 0xEB, 0x79, 0x10, 0xC6, 0x87, 0x37, 0x8E, 0xEF, 0xD3, 0xD1, 0x51, 0x18, 0x4C, 0x60, + 0x93, 0xE0, 0x57, 0x94, 0x6B, 0xCB, 0x38, 0x1E, 0xDF, 0x4D, 0xC0, 0x6E, 0x89, 0x6A, 0xE4, 0xBF, + 0xFE, 0x8B, 0x58, 0x8A, 0x5F, 0x3A, 0x03, 0x1A, 0xFF, 0x31, 0xC6, 0x0F, 0x33, 0xC6, 0x0B, 0x8D, + 0x18, 0x39, 0x1F, 0x4D, 0xA3, 0x02, 0xAD, 0x57, 0x3C, 0x32, 0x7C, 0xF4, 0xFE, 0x18, 0x99, 0xAA, + 0xB3, 0x0F, 0xF6, 0xA3, 0xB5, 0xF1, 0xF4, 0x4E, 0x4C, 0xBB, 0x5E, 0xAD, 0x09, 0x0B, 0x19, 0x6C, + 0xC6, 0x5F, 0xBF, 0x3B, 0x7D, 0x83, 0x7A, 0xF7, 0xC3, 0x0D, 0xA5, 0xA3, 0xB5, 0x23, 0x2F, 0x24, + 0xC7, 0xB0, 0xD0, 0xBB, 0x34, 0xAC, 0xCD, 0x3B, 0x1F, 0xD9, 0x84, 0x23, 0x6F, 0x56, 0x0F, 0x60, + 0xF1, 0x2C, 0x9C, 0x94, 0xA2, 0x0E, 0x01, 0xA4, 0xB0, 0x91, 0x7B, 0xE0, 0x29, 0xCA, 0x21, 0xFD, + 0x31, 0x45, 0x2B, 0x0E, 0xE2, 0x05, 0xBD, 0xA6, 0x21, 0xF5, 0x07, 0x94, 0xF4, 0x63, 0xE6, 0xEB, + 0xF9, 0xE1, 0x1A, 0x94, 0xE3, 0x2A, 0x1F, 0x1E, 0x5E, 0xEF, 0x9F, 0x7C, 0x78, 0xAC, 0x26, 0x97, + 0x62, 0x79, 0xFE, 0x1D, 0xDD, 0x1E, 0x7C, 0x87, 0xFD, 0x1B, 0x0D, 0x11, 0xD0, 0x09, 0xD2, 0x9C, + 0xB5, 0x42, 0x4F, 0xA7, 0xB0, 0xE9, 0x06, 0x4B, 0x96, 0x12, 0xE7, 0x2A, 0xF8, 0x46, 0x49, 0xB6, + 0x21, 0x89, 0x69, 0x14, 0x9B, 0xAD, 0x02, 0xD7, 0xBB, 0xBE, 0x87, 0xCD, 0x7F, 0x1C, 0x83, 0xC5, + 0x1F, 0x11, 0x77, 0x4A, 0x49, 0x1C, 0x90, 0x6B, 0xD1, 0x92, 0x8C, 0xBC, 0xB1, 0xC7, 0xC5, 0x22, + 0xB2, 0xDA, 0xB4, 0x7B, 0xBB, 0xB0, 0x95, 0xE9, 0x5A, 0x25, 0x86, 0x39, 0x6D, 0x40, 0xE1, 0xA0, + 0x3A, 0x72, 0xEF, 0x7D, 0x67, 0xEC, 0x0D, 0x00, 0x1B, 0x1D, 0xD9, 0xC6, 0x09, 0x77, 0x0B, 0x3E, + 0xBD, 0x7D, 0x3B, 0x61, 0x1B, 0x15, 0xEE, 0xC9, 0xE3, 0x7F, 0x34, 0xEA, 0xA7, 0xC1, 0x2D, 0x0D, + 0xEB, 0x2D, 0x52, 0xEF, 0x76, 0xEB, 0x36, 0x29, 0x60, 0x78, 0xD0, 0xF5, 0xD8, 0x48, 0x20, 0xB4, + 0xC8, 0xD4, 0x77, 0x41, 0xC2, 0x7D, 0xEA, 0x5A, 0x5A, 0xE4, 0x21, 0x3A, 0x5E, 0xED, 0xC3, 0x66, + 0x2B, 0x16, 0xC8, 0x7A, 0x8B, 0x23, 0xB3, 0x8D, 0x32, 0xEC, 0x3A, 0xE2, 0x90, 0xB1, 0x7A, 0x88, + 0x1E, 0x52, 0x07, 0x1D, 0x5D, 0x57, 0xF7, 0x04, 0x8D, 0xE1, 0xA9, 0xEF, 0xC5, 0x25, 0xD2, 0x10, + 0xB9, 0x2F, 0x43, 0x4A, 0xFB, 0x13, 0x50, 0xAA, 0xB5, 0xA6, 0x86, 0x0B, 0xB4, 0xAF, 0x51, 0xB3, + 0xEF, 0xFD, 0xBD, 0xAC, 0xD2, 0x8D, 0x13, 0xBA, 0x38, 0xCC, 0x27, 0x47, 0x25, 0x15, 0xB3, 0xC2, + 0x54, 0xD2, 0xC0, 0x75, 0xEE, 0xA3, 0x0B, 0x3A, 0x76, 0x3C, 0x1F, 0xFA, 0x5A, 0x52, 0x77, 0x12, + 0x06, 0xB8, 0x3B, 0xE9, 0xA0, 0xA3, 0xB0, 0x5A, 0xD5, 0x6E, 0xF5, 0xAA, 0xBD, 0xEA, 0x55, 0xD7, + 0xAB, 0x57, 0xDD, 0xA8, 0x5E, 0x75, 0xB3, 0x7A, 0xD5, 0xAD, 0xEA, 0x55, 0x9F, 0x56, 0xA8, 0x1A, + 0x3A, 0xAE, 0x17, 0x9C, 0xEE, 0x1F, 0x96, 0x0D, 0x15, 0xFD, 0xE6, 0x0D, 0xE8, 0xC1, 0xBB, 0x52, + 0x21, 0x80, 0xDD, 0xB5, 0xF4, 0xE7, 0x96, 0xD4, 0xBC, 0x72, 0x62, 0x98, 0x46, 0xF7, 0xE7, 0x34, + 0x1C, 0x30, 0x0D, 0x9B, 0x54, 0x36, 0x15, 0x05, 0xE8, 0x06, 0xCF, 0xD5, 0xED, 0x13, 0x6D, 0xE7, + 0x58, 0xA6, 0x1D, 0xC3, 0xF8, 0x6B, 0x46, 0x2E, 0x2D, 0x38, 0xAC, 0xF5, 0xF2, 0xB1, 0xE6, 0xB7, + 0x7A, 0x3F, 0x19, 0x02, 0x57, 0x69, 0x6D, 0x01, 0x92, 0x07, 0xB8, 0x58, 0x85, 0xE3, 0x0B, 0x74, + 0xC0, 0x66, 0x89, 0x65, 0x7E, 0xD9, 0xC3, 0x60, 0x3C, 0x01, 0x8D, 0x48, 0x1B, 0xCD, 0x99, 0xC1, + 0xA2, 0x07, 0xF7, 0x82, 0x0E, 0xA8, 0x37, 0xB1, 0x00, 0xCF, 0xD6, 0xD1, 0x31, 0x94, 0xA0, 0x10, + 0xA2, 0xC7, 0x9D, 0xF7, 0x16, 0xE8, 0xD3, 0x10, 0xEC, 0x88, 0xF8, 0x5C, 0xAD, 0x65, 0xE3, 0xEC, + 0x9F, 0xD0, 0xC3, 0x34, 0x99, 0xC6, 0x9F, 0xF1, 0x3C, 0x69, 0x57, 0x00, 0xBD, 0x40, 0x59, 0xFD, + 0xF2, 0x19, 0x2A, 0x4F, 0xE9, 0x2E, 0x7A, 0x05, 0xAC, 0xD0, 0x56, 0x48, 0xED, 0x0B, 0xF0, 0x1E, + 0xDA, 0x4C, 0x1A, 0x75, 0xE6, 0x9B, 0xA6, 0x78, 0xBA, 0x84, 0x9E, 0x80, 0x19, 0x58, 0x25, 0x17, + 0xB7, 0xF7, 0x13, 0x3C, 0x9D, 0x92, 0xCC, 0xCE, 0x76, 0xC8, 0x5E, 0xAF, 0x31, 0x37, 0x26, 0xB4, + 0xAF, 0x70, 0x1B, 0x54, 0x8C, 0x87, 0xD7, 0x6A, 0x70, 0x67, 0x55, 0x55, 0x44, 0xCA, 0x31, 0x90, + 0x6D, 0x02, 0xA8, 0x67, 0x44, 0x36, 0x49, 0x5F, 0x68, 0x0E, 0x2A, 0x27, 0x4D, 0x05, 0xB8, 0xD9, + 0x6A, 0xFB, 0x60, 0xB8, 0xF7, 0x47, 0x05, 0xFD, 0xE6, 0xE7, 0x5C, 0x4B, 0xC7, 0xCD, 0x0E, 0xCB, + 0xB2, 0x58, 0xE5, 0x19, 0xDA, 0x83, 0xE0, 0xFB, 0x68, 0xC7, 0xF7, 0xF1, 0xA1, 0xF0, 0x7D, 0xB2, + 0xE3, 0xFB, 0xF4, 0x20, 0xF8, 0xA2, 0x89, 0x1F, 0xDC, 0x9E, 0x53, 0x0A, 0x9B, 0x87, 0xA9, 0xCD, + 0xC6, 0x4D, 0x6C, 0x4E, 0xD2, 0x69, 0x5A, 0x37, 0xBD, 0x13, 0x68, 0x0B, 0xEB, 0x5D, 0x64, 0xEE, + 0x78, 0x6B, 0xD5, 0x89, 0x10, 0x20, 0xEC, 0xAB, 0x88, 0x15, 0xFE, 0x0A, 0xEB, 0x26, 0xAA, 0xA6, + 0x17, 0x57, 0xE1, 0xDE, 0x0C, 0xA8, 0x22, 0x6E, 0x57, 0xE3, 0x79, 0x9E, 0x6D, 0x1D, 0x48, 0x0E, + 0xFB, 0xDA, 0x93, 0x69, 0x74, 0x33, 0xA3, 0x3E, 0x10, 0xB0, 0xE5, 0xA9, 0x60, 0x2E, 0x7C, 0x59, + 0x61, 0x1E, 0x1C, 0xD7, 0x63, 0x6E, 0x09, 0x98, 0xA0, 0x8D, 0x73, 0xDF, 0x99, 0x64, 0xE0, 0x7A, + 0xCC, 0xAD, 0xD6, 0xAC, 0x8A, 0x54, 0x8F, 0x58, 0x81, 0xE5, 0xB5, 0x17, 0x71, 0x48, 0x9C, 0x91, + 0x37, 0xF4, 0x77, 0xEB, 0x23, 0x7A, 0x1D, 0xD7, 0xF7, 0x4C, 0x77, 0xB1, 0xA5, 0x85, 0xBB, 0x87, + 0x2B, 0x8B, 0x46, 0x1F, 0x0E, 0xDB, 0x1A, 0x16, 0x54, 0x6D, 0x2D, 0xC7, 0xBA, 0x72, 0xA3, 0x17, + 0x6C, 0xB1, 0x23, 0xF1, 0xFD, 0x84, 0xEE, 0xF2, 0x35, 0xEB, 0x2A, 0xB8, 0xAB, 0x83, 0xBD, 0xB4, + 0x5B, 0xB7, 0x51, 0x53, 0x27, 0x6C, 0x59, 0xAC, 0x23, 0xB0, 0x93, 0xA3, 0x3A, 0x19, 0x40, 0x85, + 0x08, 0xFE, 0x84, 0x4D, 0xFB, 0x2A, 0x6B, 0xBD, 0xCA, 0xE1, 0x29, 0xFE, 0xFC, 0x43, 0xFC, 0x5C, + 0xDF, 0xAB, 0x48, 0xD2, 0x5A, 0x3C, 0x93, 0x94, 0xC2, 0x28, 0x43, 0xD3, 0xEC, 0x88, 0xE0, 0x71, + 0x31, 0xD2, 0x3D, 0xCB, 0x72, 0xF8, 0xFE, 0xE0, 0xF7, 0xCB, 0x2C, 0x24, 0xE5, 0xA8, 0x5C, 0x88, + 0x8C, 0xE7, 0xEE, 0xE4, 0xD5, 0xB8, 0xE0, 0xBE, 0xFF, 0x8C, 0xF2, 0x31, 0x80, 0xBC, 0x71, 0xAE, + 0xE8, 0x48, 0x1E, 0x9E, 0xE7, 0xD4, 0x3B, 0x12, 0x27, 0x58, 0x69, 0x2B, 0x79, 0x92, 0x75, 0x69, + 0x6E, 0xFD, 0xB0, 0x27, 0x4A, 0x2B, 0x79, 0x90, 0x05, 0x9B, 0xDF, 0x75, 0xDB, 0xD6, 0x57, 0x3D, + 0xFC, 0xE7, 0xC7, 0x75, 0x4A, 0xE3, 0xCF, 0xDD, 0x2F, 0xE8, 0x22, 0x40, 0x5E, 0x60, 0x38, 0xC1, + 0x65, 0xB7, 0xF3, 0x74, 0x03, 0x6A, 0xE0, 0xEF, 0x19, 0x40, 0x88, 0xD7, 0x8C, 0x17, 0xF8, 0x65, + 0x57, 0x85, 0x6D, 0x43, 0x2F, 0x27, 0xE1, 0xA9, 0x8D, 0x04, 0xFC, 0x6B, 0xC7, 0xDA, 0x44, 0x0D, + 0x57, 0x60, 0x92, 0x72, 0x13, 0x9A, 0xE2, 0x64, 0x98, 0x89, 0x46, 0x4B, 0x95, 0xF1, 0x7A, 0x87, + 0x51, 0xB0, 0x2F, 0xD9, 0x21, 0xA2, 0xF2, 0xB9, 0xC7, 0xF8, 0x90, 0xF0, 0xC0, 0xDA, 0x79, 0x0B, + 0xD3, 0x77, 0x77, 0xC9, 0x46, 0x5E, 0xAF, 0xE7, 0x25, 0xC4, 0xFA, 0x79, 0x3D, 0xA5, 0x6F, 0xA3, + 0xF3, 0xB4, 0x77, 0xD9, 0xAD, 0xC2, 0x0A, 0x68, 0x40, 0xC7, 0xE8, 0x7E, 0x61, 0xD1, 0x1B, 0xD0, + 0x85, 0x88, 0x52, 0x7F, 0x06, 0x86, 0x25, 0x9F, 0xA4, 0x2C, 0x22, 0x9C, 0x7A, 0xB3, 0xFD, 0xB7, + 0xC0, 0xF3, 0x1B, 0xF5, 0x7A, 0x33, 0x23, 0x3A, 0x58, 0xFE, 0xA4, 0x80, 0xA8, 0xCC, 0xA8, 0xBA, + 0xDE, 0x37, 0x4D, 0xA9, 0x0C, 0xC3, 0x60, 0x3A, 0x21, 0x61, 0x70, 0xCB, 0x75, 0xD2, 0x38, 0x1A, + 0x2A, 0x8C, 0x48, 0xB4, 0x92, 0x29, 0x09, 0x19, 0xB0, 0x23, 0xD6, 0x0F, 0x80, 0xC8, 0xD5, 0x9A, + 0xD9, 0x5E, 0xA2, 0x1C, 0x04, 0xA3, 0xD5, 0x68, 0xBC, 0xBA, 0x41, 0xF0, 0x97, 0x2D, 0xF6, 0x2F, + 0xA3, 0x82, 0x35, 0xAF, 0xEF, 0x19, 0x4D, 0x39, 0x73, 0xA0, 0xFD, 0xF3, 0x17, 0x6B, 0xAC, 0x46, + 0x29, 0x19, 0x4A, 0xEF, 0x34, 0x54, 0x1B, 0x75, 0x5D, 0x11, 0xF3, 0x88, 0x23, 0x43, 0xBD, 0x06, + 0x7E, 0x1C, 0x06, 0xA3, 0x7A, 0x09, 0x0E, 0xA9, 0xB9, 0x33, 0x5D, 0xE4, 0x9B, 0x17, 0xB5, 0x8C, + 0x69, 0xAB, 0x4A, 0xEC, 0x9B, 0x90, 0x1C, 0xB0, 0xC7, 0x61, 0x18, 0xA4, 0x74, 0x7A, 0xFE, 0xC8, + 0xF3, 0x29, 0xFF, 0x06, 0x0A, 0x7F, 0x52, 0x0A, 0x77, 0x0D, 0x18, 0xB2, 0xC7, 0xFF, 0x9D, 0x41, + 0xE9, 0xB3, 0x85, 0xC6, 0xF3, 0x87, 0x4A, 0x78, 0x8F, 0x65, 0xBF, 0x97, 0xAD, 0x34, 0xCB, 0x62, + 0xE0, 0xA7, 0xCD, 0x72, 0xB7, 0xEE, 0xD9, 0x3A, 0x33, 0xEF, 0x8B, 0x98, 0x5F, 0xB3, 0xB0, 0x1F, + 0xD9, 0x3A, 0xB3, 0x74, 0x23, 0x88, 0x1D, 0xD9, 0x2C, 0x6F, 0x7F, 0x97, 0xA9, 0x32, 0x63, 0x27, + 0x84, 0x37, 0xE5, 0x04, 0xE4, 0x33, 0xF5, 0xBF, 0xD8, 0xEC, 0x53, 0x6B, 0xC5, 0x76, 0x14, 0x0E, + 0x66, 0xF7, 0x51, 0x64, 0x62, 0x4E, 0x6C, 0xDE, 0x04, 0x5B, 0x60, 0xCA, 0x6C, 0xE6, 0x9E, 0x88, + 0x81, 0xB4, 0x19, 0x7C, 0x49, 0x74, 0xE4, 0x1C, 0x10, 0x03, 0xF4, 0x39, 0xDA, 0x41, 0xF2, 0xF0, + 0x4A, 0x03, 0xA6, 0xE2, 0x9D, 0x65, 0xC6, 0x14, 0x01, 0x13, 0x8D, 0x46, 0x64, 0x8D, 0x30, 0x17, + 0x1A, 0xB9, 0x9A, 0xC6, 0xB1, 0xEA, 0x00, 0xAF, 0x16, 0x7B, 0x11, 0x87, 0xF7, 0x96, 0x65, 0x4A, + 0xEC, 0x92, 0x84, 0xF3, 0x22, 0x09, 0x63, 0xD0, 0x34, 0x38, 0x19, 0x38, 0xF1, 0xE0, 0x86, 0x34, + 0x28, 0xCE, 0x73, 0xDB, 0x5A, 0xA7, 0xC5, 0xBB, 0x9C, 0x44, 0xD1, 0x94, 0x92, 0x5B, 0x0F, 0xD6, + 0xC6, 0x93, 0x23, 0x19, 0xFA, 0xD2, 0xAC, 0x7E, 0xDC, 0x50, 0x1C, 0x9A, 0x52, 0xB1, 0x17, 0x22, + 0xB6, 0xF0, 0xF1, 0xBA, 0xA1, 0x0C, 0xD9, 0xFE, 0x68, 0x04, 0xFB, 0x99, 0xE1, 0x74, 0x84, 0x01, + 0x44, 0x4C, 0xC7, 0xF3, 0xE1, 0xC3, 0x13, 0x47, 0xA6, 0x93, 0x8D, 0x91, 0x9B, 0xA1, 0x83, 0xAC, + 0xB9, 0xCD, 0xDE, 0x7C, 0xF8, 0x31, 0xFA, 0x6E, 0x0B, 0xCD, 0xEA, 0xCB, 0x73, 0x1A, 0xF4, 0x14, + 0x61, 0x34, 0x92, 0x30, 0x6F, 0xD9, 0xEE, 0x94, 0xBB, 0xCA, 0xF0, 0xD8, 0x77, 0x48, 0x85, 0xED, + 0x67, 0xEC, 0x84, 0xEB, 0xF5, 0x9D, 0xA4, 0x3A, 0xF3, 0x34, 0x0A, 0xF7, 0xDA, 0x29, 0x2C, 0xFA, + 0xD9, 0x9A, 0x82, 0x80, 0xA3, 0xC0, 0xAF, 0xC7, 0x64, 0xCA, 0xC3, 0x6E, 0x40, 0x60, 0x1C, 0x60, + 0xF7, 0x2D, 0xC5, 0x1D, 0x24, 0xF5, 0xBE, 0xC1, 0xE0, 0xDF, 0x3A, 0x91, 0xA2, 0x0B, 0x60, 0x04, + 0xAE, 0x83, 0x27, 0xD2, 0x7E, 0xD3, 0x83, 0x76, 0x54, 0x36, 0xE5, 0x46, 0xA7, 0xA6, 0xE3, 0xFA, + 0x32, 0x08, 0x07, 0x14, 0x86, 0x8C, 0x85, 0x4F, 0x0B, 0x02, 0xD2, 0xA1, 0x64, 0xA7, 0xDC, 0xD4, + 0x89, 0xA6, 0x21, 0x2B, 0xC7, 0xA5, 0xF6, 0xF5, 0xDF, 0xA1, 0x17, 0xAE, 0x17, 0x4D, 0x70, 0x68, + 0x58, 0x20, 0x3A, 0x9E, 0xD2, 0x90, 0xC3, 0x69, 0x14, 0x07, 0x63, 0xFE, 0x37, 0xEC, 0xC0, 0x90, + 0x3D, 0xF5, 0xA6, 0xA2, 0x7F, 0x87, 0x22, 0x02, 0x01, 0x35, 0x18, 0x0F, 0x15, 0x3A, 0xF1, 0x17, + 0x06, 0xF4, 0x12, 0xB5, 0xCD, 0xDC, 0x50, 0x98, 0xAE, 0x3A, 0x60, 0xA7, 0xAD, 0x6A, 0x9C, 0x1F, + 0xF7, 0x15, 0x2C, 0x17, 0xE6, 0x2B, 0x1A, 0xCC, 0x0D, 0x92, 0xFA, 0xB8, 0xB7, 0x7C, 0x13, 0x0C, + 0x87, 0xEC, 0x94, 0x67, 0x21, 0x28, 0x67, 0x71, 0xE8, 0x4D, 0xE4, 0x91, 0xEC, 0xE2, 0x90, 0xE4, + 0x71, 0xEC, 0x9C, 0x90, 0x8C, 0xD8, 0xA5, 0x05, 0x29, 0x3A, 0xBE, 0x83, 0x75, 0xD9, 0x77, 0x46, + 0xE7, 0x53, 0xA6, 0x62, 0x17, 0x82, 0x75, 0x0E, 0x3B, 0x81, 0xF8, 0x9C, 0x86, 0xD7, 0x74, 0x10, + 0x1F, 0x06, 0x21, 0x06, 0x54, 0xE0, 0xE9, 0xEC, 0xDC, 0x50, 0xD9, 0x12, 0xC7, 0x57, 0xF8, 0x39, + 0x21, 0x38, 0x7E, 0x4C, 0x7D, 0xDF, 0x49, 0x02, 0x09, 0x18, 0x85, 0xF3, 0x43, 0x9B, 0xC6, 0xC1, + 0xC9, 0xE9, 0xFB, 0x31, 0xFA, 0x00, 0xF7, 0xD1, 0xC1, 0x33, 0x5E, 0x5C, 0x22, 0xF6, 0x2F, 0xCE, + 0xE7, 0x11, 0xD2, 0x04, 0x12, 0xD7, 0x3D, 0x38, 0xFB, 0x30, 0xFA, 0x5F, 0x35, 0x0F, 0x79, 0x89, + 0x74, 0xA3, 0x99, 0xA5, 0xF1, 0x40, 0x48, 0xF4, 0x01, 0x2E, 0x48, 0x46, 0x09, 0x97, 0xD0, 0x4C, + 0xC9, 0xD4, 0x9D, 0x64, 0xBE, 0xB9, 0x37, 0x83, 0x89, 0x8C, 0x33, 0xC8, 0x62, 0x07, 0x33, 0x09, + 0xCD, 0x1A, 0x59, 0x00, 0x6B, 0xA3, 0x7A, 0x3B, 0x85, 0x85, 0x2C, 0xC0, 0x62, 0x23, 0x74, 0xAF, + 0x58, 0xD8, 0xA2, 0xF8, 0x1E, 0xD6, 0x14, 0x64, 0xC5, 0x88, 0xDD, 0xFB, 0xA9, 0xF9, 0x81, 0x4F, + 0x6B, 0xFA, 0xBD, 0x16, 0x16, 0x5F, 0x51, 0xDE, 0xF2, 0x6A, 0x14, 0x0C, 0xBE, 0xF2, 0xA6, 0x60, + 0x3E, 0x85, 0x14, 0xD5, 0xF9, 0x61, 0xFF, 0x37, 0x12, 0xF0, 0xA5, 0x42, 0x84, 0x16, 0xB0, 0xD0, + 0x5D, 0x05, 0xB6, 0xB8, 0x6D, 0x90, 0xDC, 0xE3, 0xE1, 0x57, 0x10, 0x58, 0x55, 0x6C, 0xAC, 0xB8, + 0x6E, 0xD6, 0xD6, 0x4E, 0xD2, 0x45, 0x3D, 0xA9, 0x3B, 0x18, 0x45, 0xE2, 0x62, 0x0D, 0x5E, 0x2D, + 0x48, 0xA2, 0x4B, 0xFF, 0x73, 0x0A, 0x06, 0x30, 0xBF, 0x41, 0x12, 0x84, 0x60, 0x18, 0x34, 0x6A, + 0x6D, 0x75, 0x6F, 0xD7, 0x22, 0xFC, 0x4F, 0xD7, 0x88, 0x45, 0xB4, 0x85, 0x2C, 0x2B, 0x18, 0x84, + 0xEF, 0x01, 0xBE, 0xAF, 0xAC, 0xA8, 0x8B, 0x98, 0x42, 0x30, 0xEC, 0xB6, 0x94, 0x06, 0x9F, 0xEF, + 0xBE, 0xB4, 0x45, 0xFC, 0x31, 0x3B, 0xCC, 0xD2, 0x4B, 0xB8, 0x55, 0xC1, 0x0A, 0x93, 0x21, 0xCB, + 0x58, 0x9F, 0x2D, 0x8B, 0xF1, 0x99, 0x78, 0x17, 0x70, 0xD1, 0x26, 0x63, 0xEE, 0xFE, 0x63, 0x7F, + 0xF0, 0xF2, 0x59, 0x99, 0x92, 0xFA, 0x13, 0x9F, 0xFB, 0x41, 0xDC, 0x68, 0x9B, 0x4E, 0xC5, 0xA6, + 0xE4, 0x17, 0x23, 0x25, 0xB1, 0x36, 0x1E, 0x97, 0x5B, 0xD2, 0xC8, 0xCC, 0xF2, 0xCB, 0x46, 0x87, + 0xEA, 0x38, 0xAF, 0x44, 0x87, 0xE6, 0x87, 0x07, 0x24, 0x18, 0x81, 0x5C, 0x6F, 0xD5, 0xE1, 0x5F, + 0x05, 0x14, 0xD0, 0x31, 0x1B, 0xFE, 0xC4, 0xB1, 0x3E, 0x13, 0x0D, 0x89, 0xBF, 0xDE, 0x46, 0x87, + 0x2C, 0xB4, 0xD3, 0x62, 0x98, 0x89, 0x3E, 0x2C, 0xF0, 0x43, 0x6E, 0x68, 0xA6, 0xA8, 0x32, 0x57, + 0xE1, 0x70, 0x16, 0x36, 0xB4, 0xF2, 0x27, 0xD2, 0xB7, 0xAB, 0x5C, 0x05, 0x82, 0xCE, 0xA5, 0x97, + 0x85, 0x1A, 0xB2, 0xB0, 0x45, 0x7A, 0x9D, 0x4E, 0xA7, 0x99, 0x55, 0x19, 0xCC, 0x3B, 0x01, 0xAA, + 0xA2, 0x45, 0x98, 0x6D, 0x8C, 0xBE, 0x08, 0x4D, 0x85, 0x60, 0xBF, 0xB8, 0x07, 0xC3, 0x30, 0x39, + 0xF1, 0xFC, 0x84, 0x15, 0x3C, 0x27, 0xD8, 0xED, 0xA4, 0xB5, 0x8E, 0x62, 0x30, 0xA2, 0x4E, 0x28, + 0x71, 0x54, 0x03, 0x5C, 0xCF, 0x12, 0xD9, 0x9F, 0x0E, 0x06, 0x60, 0x22, 0x33, 0x32, 0x95, 0x9B, + 0x10, 0x09, 0x1C, 0x51, 0x6E, 0x23, 0x51, 0x14, 0x71, 0x22, 0xA1, 0x2D, 0x02, 0xD7, 0xA9, 0x4B, + 0x81, 0x57, 0x85, 0x6B, 0xA1, 0x10, 0x6C, 0x72, 0x49, 0x9D, 0xE0, 0xA4, 0x34, 0x92, 0x93, 0xDB, + 0x92, 0xD7, 0x62, 0xFB, 0x81, 0xDB, 0x37, 0x76, 0x06, 0xAE, 0x08, 0x58, 0x76, 0x5B, 0x23, 0xF7, + 0x6C, 0xE8, 0x49, 0xC2, 0x85, 0xAA, 0x1D, 0x32, 0x5D, 0xD2, 0x10, 0x4E, 0xA5, 0x84, 0xB2, 0x9D, + 0xE2, 0x56, 0x18, 0xFB, 0xA4, 0xF9, 0xA1, 0xAC, 0x9E, 0x0C, 0xBE, 0x3B, 0xFA, 0x87, 0xB6, 0x97, + 0x31, 0x36, 0x5F, 0x33, 0x53, 0x98, 0x41, 0x57, 0x46, 0x9F, 0xA5, 0x4B, 0xC5, 0x14, 0x5A, 0x8E, + 0x30, 0xD9, 0x90, 0xDB, 0x46, 0x5A, 0x8C, 0x51, 0x2A, 0xED, 0x8A, 0x18, 0xA9, 0xE3, 0x57, 0x13, + 0x22, 0x2D, 0xEE, 0xD0, 0x24, 0xD1, 0x0A, 0xA6, 0xD0, 0x28, 0xE3, 0x6D, 0xAC, 0xB8, 0x16, 0x49, + 0x61, 0x17, 0x2F, 0x11, 0x2E, 0x3B, 0x2A, 0xE5, 0x37, 0x25, 0x15, 0x78, 0xA8, 0x31, 0xC5, 0x0E, + 0xF0, 0x37, 0x5C, 0x6B, 0x14, 0x49, 0x64, 0x65, 0x42, 0xBD, 0xCA, 0xB2, 0x16, 0x5E, 0xBD, 0xEB, + 0x6D, 0x6E, 0x02, 0xB1, 0x2C, 0x8A, 0xF0, 0x8A, 0xC2, 0xFF, 0xE2, 0x5B, 0x4A, 0x7D, 0xD2, 0x61, + 0xBB, 0x68, 0x28, 0xAB, 0x41, 0xE1, 0x20, 0x18, 0x8D, 0x9C, 0x49, 0x44, 0x5F, 0x9D, 0xF5, 0xFB, + 0x3C, 0xE6, 0x92, 0x6D, 0x1C, 0x8D, 0xF9, 0x29, 0x2A, 0xF5, 0xB9, 0x61, 0x0A, 0xFA, 0x62, 0xC0, + 0xA3, 0xE4, 0x06, 0x4E, 0x48, 0x55, 0x4D, 0x20, 0x0A, 0x6C, 0xE3, 0x8D, 0xFC, 0x93, 0xA3, 0x06, + 0x35, 0x79, 0x4B, 0x9B, 0x5C, 0x80, 0xD6, 0x5B, 0x65, 0xA5, 0xAB, 0xB8, 0x9A, 0xCF, 0xD8, 0x64, + 0x3A, 0x29, 0x6A, 0xC0, 0xE5, 0x28, 0x8B, 0x40, 0xED, 0x2B, 0x2C, 0xE4, 0x1E, 0x5A, 0x60, 0x2F, + 0x3D, 0x3A, 0x72, 0xA3, 0x46, 0x7A, 0x9F, 0xEA, 0x50, 0x30, 0x41, 0x58, 0x40, 0x83, 0x34, 0x80, + 0xD2, 0x64, 0x4F, 0xC2, 0x54, 0xB1, 0x15, 0x17, 0xB1, 0xAC, 0xC0, 0x6D, 0xB9, 0x9F, 0x47, 0xEC, + 0xD2, 0x4E, 0xC9, 0x6D, 0x9D, 0x0E, 0x09, 0x36, 0x1D, 0xFA, 0x51, 0x34, 0x6B, 0x3B, 0x1C, 0x4A, + 0xD1, 0x14, 0x7E, 0xAD, 0xD6, 0xFA, 0x20, 0x8D, 0x07, 0x86, 0xA6, 0x2C, 0x3A, 0xB8, 0x52, 0xBB, + 0xBE, 0x1A, 0x23, 0x0C, 0x2D, 0x45, 0xCC, 0x70, 0xA5, 0xB6, 0xE7, 0xE7, 0x0A, 0x8B, 0xD4, 0xBD, + 0x50, 0xB5, 0xD6, 0xB0, 0xA7, 0x8B, 0x54, 0x00, 0xF8, 0x67, 0xA5, 0x96, 0x18, 0xEF, 0x9B, 0x36, + 0xBC, 0xF5, 0xAE, 0xBD, 0x6A, 0xED, 0xDE, 0x1D, 0x9E, 0xBF, 0x3F, 0x52, 0x68, 0x06, 0xCB, 0xFF, + 0xBD, 0x3B, 0xA9, 0xD6, 0x96, 0x45, 0x45, 0xA5, 0x4D, 0x99, 0x15, 0x56, 0x91, 0xC3, 0xF7, 0x51, + 0x4C, 0xC7, 0x0A, 0x87, 0xF9, 0xDF, 0x95, 0xDA, 0x1E, 0xEB, 0x91, 0xD7, 0xD0, 0x3A, 0x89, 0xC5, + 0xAE, 0xD4, 0xFE, 0xEC, 0x9D, 0xD2, 0x5D, 0x0C, 0xCF, 0x96, 0xAD, 0xF8, 0x2A, 0x60, 0xAA, 0x2D, + 0x3E, 0x63, 0x84, 0xF8, 0x13, 0xDE, 0x32, 0xA3, 0xAA, 0x78, 0xD0, 0x6C, 0x1A, 0x83, 0x86, 0xAE, + 0xED, 0x16, 0xDE, 0xFB, 0xDD, 0xD8, 0x56, 0xB4, 0x56, 0x17, 0x63, 0x97, 0x37, 0xB6, 0xA1, 0xA5, + 0x13, 0x3A, 0x03, 0xD8, 0x73, 0x47, 0xAA, 0xDA, 0xD2, 0x67, 0x58, 0x33, 0x41, 0x8D, 0x73, 0x20, + 0x0F, 0x2F, 0x57, 0x91, 0x16, 0x47, 0x53, 0x0B, 0x6F, 0x68, 0x76, 0x3A, 0xDD, 0x5E, 0x8B, 0x5D, + 0x31, 0xCD, 0x2A, 0x4E, 0x5E, 0xCA, 0xD4, 0x67, 0xB7, 0xC3, 0x1A, 0xD8, 0x26, 0xAB, 0xE4, 0x25, + 0xA2, 0x3C, 0xC4, 0x7B, 0xD3, 0x14, 0xAA, 0x30, 0x6D, 0xD1, 0x90, 0x04, 0xDA, 0xC8, 0xF1, 0x7C, + 0xF8, 0xF0, 0xAD, 0xC6, 0x14, 0xF7, 0x76, 0x27, 0x57, 0x6F, 0x6F, 0x77, 0x2A, 0xA0, 0xCD, 0x80, + 0x3E, 0x3C, 0x7B, 0xBB, 0x10, 0xE4, 0xC4, 0x38, 0xC9, 0x73, 0xEE, 0x08, 0x8B, 0x5E, 0x35, 0x57, + 0x6C, 0x63, 0xED, 0xA7, 0x8D, 0x2E, 0x0F, 0x1D, 0x60, 0x4D, 0xF8, 0x3A, 0x88, 0x62, 0x31, 0xEC, + 0x9B, 0x99, 0x61, 0xDF, 0xCC, 0x19, 0xF6, 0x6C, 0xB7, 0x73, 0xBA, 0x9E, 0xC5, 0x87, 0xDA, 0x82, + 0xE3, 0xDB, 0xC6, 0x1F, 0x13, 0x25, 0xFB, 0x38, 0x1B, 0x2A, 0x5B, 0xDF, 0xD8, 0xD5, 0x52, 0xEE, + 0x34, 0x61, 0xB8, 0xD6, 0x3B, 0x26, 0xA2, 0xF5, 0xCE, 0xDC, 0x7D, 0xE3, 0x1D, 0x79, 0x1F, 0xD1, + 0xB0, 0x80, 0xA1, 0x96, 0x12, 0x6C, 0xC1, 0x74, 0x4E, 0xFC, 0xB5, 0x37, 0x0C, 0xDA, 0x83, 0x60, + 0x8C, 0x7F, 0xFD, 0x7B, 0x4D, 0x21, 0xCD, 0xF1, 0x09, 0x46, 0x78, 0x8F, 0x08, 0x2C, 0x95, 0xEC, + 0x5E, 0x5E, 0x21, 0x61, 0xD2, 0x9F, 0x4D, 0xB8, 0x3F, 0x19, 0x16, 0xE3, 0xF0, 0x36, 0xF4, 0x62, + 0xE1, 0x15, 0x07, 0x05, 0x43, 0x5C, 0x7E, 0xF5, 0x3F, 0x22, 0xA0, 0x6C, 0x68, 0x9B, 0x20, 0x05, + 0xB0, 0xC7, 0xBD, 0x27, 0xB7, 0x0E, 0xE8, 0x0A, 0xE0, 0x82, 0xEB, 0x45, 0x28, 0x4B, 0xE4, 0xEC, + 0xDD, 0xC5, 0xC9, 0x39, 0xEE, 0x8B, 0x09, 0x6C, 0x5A, 0xC9, 0x28, 0x80, 0x65, 0x16, 0x9B, 0xD3, + 0x3B, 0x58, 0xB6, 0xD1, 0xD3, 0x20, 0x2F, 0x33, 0xB4, 0x25, 0x46, 0xC5, 0xF0, 0x84, 0xBF, 0xF8, + 0x46, 0x19, 0x37, 0x11, 0x9C, 0x43, 0x45, 0x7C, 0x51, 0x7A, 0x2F, 0xBA, 0x51, 0x11, 0x80, 0x90, + 0x9C, 0x5E, 0xB7, 0xD3, 0xAD, 0xDA, 0x50, 0x13, 0x83, 0xDA, 0xD5, 0xC8, 0x0D, 0x2F, 0xFB, 0x13, + 0x27, 0xFC, 0xFA, 0x72, 0xEA, 0x77, 0x6B, 0xB3, 0x03, 0x39, 0xFF, 0x30, 0x23, 0xC9, 0x72, 0xC4, + 0xF1, 0x96, 0xC8, 0xBF, 0xE3, 0x3F, 0x73, 0x74, 0x1B, 0x61, 0x00, 0x62, 0x80, 0x62, 0x34, 0x1C, + 0x1A, 0xF3, 0xEB, 0x5D, 0xE8, 0xF8, 0xD1, 0xD8, 0x8B, 0x5F, 0xBD, 0xDA, 0xAF, 0xD9, 0x4F, 0xC3, + 0xA0, 0xA1, 0xE1, 0x16, 0x41, 0x5B, 0xEA, 0xFD, 0xC1, 0xEF, 0xF2, 0x90, 0xDB, 0x70, 0x08, 0x4D, + 0xAF, 0xEE, 0x84, 0xC9, 0x5B, 0xEC, 0xFB, 0xA8, 0xF3, 0xE0, 0x63, 0xCF, 0xFD, 0xBF, 0xBB, 0x18, + 0x48, 0xF1, 0x85, 0xC7, 0x54, 0x8C, 0xD9, 0x6E, 0x00, 0x31, 0x78, 0x6E, 0x44, 0x60, 0x3B, 0x1E, + 0x32, 0x51, 0x62, 0xB2, 0x89, 0xD5, 0x72, 0xF7, 0xFB, 0x0A, 0xDA, 0xBC, 0xBD, 0x7E, 0x36, 0xC0, + 0x49, 0x69, 0xC4, 0x7D, 0x1F, 0xC6, 0xDC, 0xD5, 0x6C, 0x77, 0xA5, 0x69, 0xD3, 0x70, 0x17, 0xB1, + 0x20, 0x13, 0x65, 0xE5, 0xB2, 0x5C, 0xF1, 0xFA, 0xA5, 0xE8, 0x16, 0xA6, 0x54, 0xD2, 0x96, 0xD3, + 0x0F, 0x8B, 0x8E, 0xCE, 0x51, 0x9B, 0xC1, 0x15, 0x0C, 0xFA, 0x37, 0xB6, 0x68, 0x81, 0x29, 0x10, + 0xF8, 0x2E, 0x2A, 0x82, 0xAD, 0x0E, 0xFE, 0xDF, 0xB6, 0x7E, 0x6C, 0x75, 0x70, 0x2E, 0x43, 0x99, + 0xAA, 0x2E, 0x0E, 0x72, 0x2F, 0x96, 0x15, 0x23, 0x3C, 0x0F, 0x22, 0x0F, 0xFF, 0xBB, 0x3F, 0x18, + 0x4C, 0x41, 0x3D, 0xDE, 0x73, 0x05, 0xBA, 0xD9, 0xEE, 0x5A, 0x10, 0x77, 0xDB, 0x0C, 0xF3, 0x66, + 0xBB, 0x00, 0xB3, 0x8E, 0x5A, 0x13, 0x76, 0x76, 0x7C, 0x72, 0xCC, 0x62, 0x7E, 0x5B, 0x5A, 0x82, + 0x1C, 0x93, 0x60, 0x7B, 0xAB, 0x8F, 0xD8, 0x4A, 0x49, 0x9E, 0x53, 0xAD, 0xD5, 0x27, 0x68, 0xA5, + 0x24, 0xD6, 0x29, 0x6F, 0xF4, 0xC6, 0x89, 0xD1, 0x65, 0xC2, 0x9A, 0xB5, 0x3B, 0xDB, 0x9D, 0xDE, + 0xF6, 0xC6, 0xD3, 0xED, 0x0A, 0xCD, 0x02, 0x7F, 0x28, 0xDA, 0xF1, 0x54, 0x41, 0xCF, 0x36, 0x3B, + 0x9B, 0x4F, 0xB7, 0xBA, 0xE5, 0x2D, 0xF7, 0x47, 0xB1, 0x17, 0x4F, 0x5D, 0x66, 0x8D, 0x6D, 0x6E, + 0x01, 0xCE, 0x67, 0xC5, 0xE8, 0xC4, 0x61, 0xC1, 0x6B, 0xEA, 0x0D, 0x6F, 0x10, 0x5D, 0xA7, 0x4A, + 0x6D, 0xE3, 0x68, 0x41, 0x6F, 0xF5, 0xBD, 0xE8, 0x74, 0x57, 0x07, 0x97, 0x23, 0xAA, 0x85, 0x24, + 0x14, 0x4B, 0x1B, 0x48, 0x53, 0xD3, 0x92, 0x84, 0xA0, 0xFC, 0x0C, 0x2F, 0x67, 0x7E, 0x2D, 0x30, + 0xAC, 0x8B, 0x0D, 0xED, 0xDC, 0xC3, 0x3B, 0xFB, 0x10, 0xCF, 0x36, 0xCC, 0xD9, 0xA6, 0x59, 0x8D, + 0xA0, 0xCF, 0xCD, 0xA7, 0x1D, 0xF6, 0xD3, 0x22, 0xC9, 0x2F, 0x89, 0x2E, 0x90, 0x65, 0xA8, 0x08, + 0xC4, 0xAF, 0xD5, 0xD4, 0x50, 0x19, 0xE2, 0x8F, 0x3F, 0x0A, 0xF1, 0xA7, 0x87, 0x42, 0x6C, 0xB9, + 0x41, 0x5C, 0x49, 0x54, 0x2B, 0xA8, 0xC8, 0xF9, 0xD5, 0xE4, 0x4C, 0xAA, 0xD2, 0xCE, 0xC7, 0xF4, + 0xB4, 0x0C, 0x16, 0xD0, 0xDF, 0x68, 0x88, 0xD7, 0x5F, 0xE1, 0xDB, 0x1A, 0x8B, 0x0C, 0x4A, 0x43, + 0x04, 0x2B, 0x0F, 0x82, 0x32, 0x43, 0x56, 0xBB, 0xDD, 0xCE, 0xFA, 0x46, 0x8B, 0x3C, 0x7B, 0xA6, + 0xED, 0x4A, 0xF9, 0x67, 0x1C, 0x00, 0x2C, 0xA8, 0xB8, 0xF8, 0xE4, 0xA0, 0x34, 0xE7, 0xD6, 0x2A, + 0xA6, 0x6F, 0xEB, 0xB4, 0x88, 0xF8, 0x8F, 0x82, 0x73, 0x53, 0x0C, 0x3A, 0xFB, 0x65, 0x21, 0x59, + 0xCB, 0x9B, 0x9D, 0xAB, 0xBD, 0x0E, 0x28, 0x04, 0x76, 0x0E, 0xD0, 0xD6, 0x70, 0xB3, 0x0F, 0x88, + 0x9B, 0xFD, 0x32, 0xAB, 0xB8, 0x29, 0xB1, 0x3D, 0x96, 0x4D, 0x64, 0x72, 0xAE, 0x9F, 0xD8, 0x8C, + 0x59, 0xDF, 0x77, 0xE1, 0xA6, 0x8B, 0x03, 0x50, 0xEC, 0xFC, 0xCB, 0xCE, 0xCC, 0x9E, 0x84, 0x99, + 0xCC, 0x95, 0x2C, 0x56, 0xDC, 0x1C, 0x48, 0xAC, 0xD5, 0x36, 0x96, 0xD5, 0x10, 0xE6, 0xF7, 0x13, + 0xED, 0x72, 0x86, 0xB1, 0x63, 0xF4, 0xB3, 0xB3, 0x84, 0x7E, 0x16, 0xA3, 0x3D, 0xFF, 0xF0, 0xA8, + 0x88, 0xD3, 0x0D, 0xD0, 0x83, 0x8D, 0x6B, 0x31, 0xDA, 0x47, 0xEE, 0xAF, 0x22, 0xC7, 0xDD, 0x07, + 0x42, 0x5B, 0x2C, 0xC7, 0x02, 0xAB, 0x29, 0xC7, 0x9D, 0x87, 0x93, 0xE3, 0xEE, 0x8F, 0x92, 0xE3, + 0xEE, 0x8F, 0x91, 0xE3, 0x1F, 0x83, 0x76, 0x89, 0xFD, 0x55, 0xB3, 0x1F, 0x3C, 0xB6, 0xFB, 0x27, + 0xEB, 0x02, 0x2A, 0x76, 0x67, 0xD8, 0x97, 0x07, 0x8B, 0x23, 0x68, 0x06, 0x30, 0x89, 0xBE, 0x57, + 0xDC, 0x41, 0x33, 0x34, 0x4F, 0x94, 0xB7, 0xDD, 0x35, 0x33, 0x23, 0x24, 0xA1, 0x9F, 0x6A, 0x33, + 0x35, 0x37, 0xB4, 0x2A, 0xF7, 0x4E, 0xB9, 0xB7, 0x7E, 0x7C, 0xEB, 0xF7, 0xE6, 0x04, 0x24, 0xE9, + 0xF8, 0x70, 0xB1, 0x79, 0x7B, 0x11, 0x6C, 0xBC, 0x9E, 0xA3, 0x3B, 0x89, 0xD6, 0xAB, 0xCD, 0x3B, + 0x28, 0x5D, 0x63, 0x7B, 0x32, 0xCB, 0x88, 0xCC, 0x8B, 0x58, 0x51, 0x26, 0x73, 0x0F, 0xC2, 0x22, + 0x8D, 0x73, 0x70, 0x7F, 0xD7, 0x5D, 0x4B, 0x6A, 0x98, 0x61, 0x89, 0x8B, 0x69, 0x59, 0xA9, 0xA0, + 0x74, 0x6B, 0x2F, 0x3F, 0xCE, 0xB1, 0xC4, 0xF2, 0x93, 0x31, 0xDB, 0x2C, 0x26, 0x5A, 0x01, 0x72, + 0xC4, 0xF2, 0x5C, 0x88, 0x43, 0xA0, 0x77, 0xC1, 0x57, 0x8A, 0x6E, 0x2E, 0x56, 0x79, 0xC7, 0x76, + 0xCD, 0x78, 0x4A, 0x93, 0x6B, 0x7E, 0xB6, 0xFB, 0xC6, 0xD6, 0xC3, 0xA9, 0x62, 0x6C, 0x2D, 0xB2, + 0xBE, 0xC5, 0xFF, 0x9F, 0x68, 0xD3, 0xF5, 0xAD, 0xBC, 0xB3, 0xAA, 0xF3, 0x22, 0x35, 0x5A, 0xEA, + 0xE8, 0x28, 0xA5, 0xA4, 0x56, 0xCB, 0xC6, 0x51, 0xB0, 0xD0, 0xCB, 0xBF, 0xD0, 0xFB, 0x0B, 0xEA, + 0xD3, 0x5B, 0x67, 0x54, 0xCB, 0xBF, 0xA5, 0x60, 0xC8, 0x0A, 0x3F, 0xCE, 0xAD, 0x20, 0x25, 0xA5, + 0x8E, 0xC8, 0xBC, 0xE0, 0x4F, 0x65, 0xC8, 0xB5, 0x10, 0x98, 0x02, 0x73, 0xC5, 0x1B, 0x4F, 0x3F, + 0x3A, 0xB7, 0x7C, 0x05, 0x5B, 0xDF, 0xCA, 0x39, 0xA2, 0x63, 0x87, 0x2B, 0x5B, 0xC6, 0x26, 0xA5, + 0x5F, 0x90, 0xC1, 0xCA, 0x8E, 0xE8, 0xDC, 0x8B, 0x07, 0x37, 0xB8, 0x23, 0xDA, 0xCE, 0x3B, 0x35, + 0x83, 0x12, 0x8E, 0x6D, 0x7B, 0x61, 0x64, 0x17, 0xD0, 0x92, 0xED, 0xFC, 0x9E, 0xE1, 0xBE, 0xEF, + 0x99, 0x15, 0x1B, 0x7C, 0xE6, 0xE8, 0xD8, 0x2F, 0x55, 0xF0, 0x19, 0x43, 0x8A, 0xA7, 0xDD, 0x65, + 0x67, 0xB1, 0x78, 0x06, 0x7E, 0x06, 0x18, 0x83, 0xF0, 0x6B, 0xA7, 0xDF, 0x3F, 0x39, 0xE2, 0xBC, + 0xDE, 0xEC, 0x98, 0xD6, 0xC2, 0x66, 0xDE, 0xE9, 0x95, 0x72, 0xA4, 0x6E, 0x39, 0x90, 0xB4, 0xA1, + 0x39, 0x77, 0xA2, 0x08, 0x7E, 0x71, 0x1F, 0x1E, 0x55, 0xF7, 0x71, 0x7A, 0xD4, 0x7D, 0xBC, 0x1E, + 0xF5, 0x1E, 0xA7, 0x47, 0xBD, 0xC7, 0xEB, 0xD1, 0xFA, 0xE3, 0xF4, 0x68, 0x7D, 0x49, 0x3D, 0x12, + 0x53, 0xEB, 0xDD, 0xE1, 0xF9, 0xDA, 0xFB, 0xA3, 0x73, 0x53, 0x5D, 0x2A, 0x2B, 0xDF, 0xB7, 0xB8, + 0xEA, 0x51, 0xB9, 0xD0, 0x0A, 0x13, 0xD9, 0x22, 0x3D, 0xB0, 0xDE, 0xDA, 0xDC, 0x5C, 0xCF, 0x9C, + 0x91, 0xB3, 0x8F, 0x2A, 0x81, 0x5A, 0x70, 0x4A, 0xC9, 0x91, 0x75, 0x82, 0x44, 0x39, 0x85, 0xAF, + 0xBE, 0xB5, 0xB6, 0x61, 0xFA, 0x6E, 0xEF, 0xBC, 0xE9, 0xE2, 0x29, 0xEF, 0x3C, 0x6F, 0xB1, 0x8C, + 0xCE, 0xE7, 0x90, 0xF4, 0xDE, 0x9D, 0xCC, 0x4E, 0x55, 0xD2, 0x68, 0x79, 0x84, 0xF1, 0x38, 0x11, + 0x91, 0x44, 0xE2, 0x74, 0x1A, 0xD3, 0xBB, 0xAC, 0xD0, 0x60, 0xBC, 0x8E, 0xC1, 0x4A, 0xF8, 0x04, + 0x30, 0x09, 0xAF, 0xC0, 0xE2, 0x37, 0x78, 0x01, 0x19, 0x38, 0x3E, 0xDB, 0x45, 0x01, 0x29, 0xBC, + 0x8D, 0x4B, 0x9C, 0x98, 0x6D, 0xA7, 0x22, 0x3C, 0xA9, 0x8C, 0x3D, 0x16, 0x66, 0x93, 0x4B, 0x98, + 0x5C, 0xFF, 0x59, 0x70, 0x51, 0xBE, 0x40, 0xA7, 0x77, 0x2F, 0xAA, 0xF1, 0x6E, 0xEC, 0xDC, 0x41, + 0x13, 0x0C, 0x38, 0xBE, 0x1C, 0x7B, 0x3E, 0xF4, 0x32, 0xE2, 0xDC, 0xEB, 0x76, 0x36, 0xBB, 0xBD, + 0x4E, 0x26, 0x3A, 0xA2, 0xDB, 0x82, 0xEF, 0xAD, 0x9E, 0xEE, 0xE5, 0xD4, 0x02, 0xA0, 0x8A, 0xE3, + 0x3E, 0x38, 0xBA, 0x37, 0xCC, 0xD8, 0x5B, 0x2E, 0x42, 0x6B, 0xDC, 0xAB, 0x6E, 0xA9, 0x59, 0xFB, + 0xBA, 0xD5, 0x21, 0x7F, 0x26, 0x3D, 0xF5, 0xB8, 0xCF, 0xD6, 0x28, 0x43, 0xB1, 0xDE, 0xEC, 0xBB, + 0x2D, 0x10, 0x47, 0xBB, 0x09, 0x53, 0x6D, 0x34, 0xD2, 0x26, 0x27, 0x7E, 0x8C, 0x47, 0x61, 0x23, + 0x21, 0xCB, 0x59, 0xC6, 0x6C, 0x2D, 0x85, 0x25, 0x76, 0x84, 0x1D, 0xF3, 0xF0, 0x5B, 0x86, 0xA9, + 0x15, 0x19, 0x9C, 0xC5, 0xC9, 0x4D, 0x13, 0xD6, 0x08, 0x48, 0x47, 0xAF, 0x0F, 0xCF, 0x67, 0x34, + 0x36, 0x4F, 0xCE, 0xF7, 0x79, 0xF0, 0x4B, 0x0A, 0xE5, 0xE4, 0x5C, 0x0D, 0x92, 0xF1, 0x7D, 0xBF, + 0xAD, 0xFC, 0x5F, 0x65, 0xCF, 0x71, 0x61, 0x86, 0xD3, 0x12, 0x2C, 0x47, 0x67, 0xFD, 0xC7, 0x40, + 0xF3, 0x8A, 0x3F, 0xA2, 0xF3, 0x18, 0xA8, 0xFA, 0xD3, 0x2B, 0xF8, 0x77, 0x61, 0x4C, 0xDF, 0x8B, + 0x64, 0x59, 0xE2, 0x3A, 0x8B, 0x27, 0x42, 0x29, 0x77, 0xB2, 0x4A, 0xB9, 0x63, 0x55, 0xCA, 0x76, + 0x8C, 0x89, 0x34, 0x9E, 0xBD, 0x3B, 0x9F, 0x5F, 0x10, 0xAD, 0xDE, 0x56, 0xA0, 0x70, 0x34, 0x82, + 0x1D, 0x53, 0xE0, 0x73, 0xAD, 0xBE, 0x0E, 0x93, 0xE0, 0xA9, 0xBA, 0x77, 0x64, 0xAA, 0xE8, 0xA9, + 0x4A, 0x63, 0x1A, 0x79, 0x59, 0x16, 0xE9, 0x36, 0x01, 0xDA, 0x06, 0x1E, 0xCB, 0xBA, 0x00, 0x5B, + 0x09, 0x8C, 0x38, 0xD3, 0x4E, 0x70, 0xD6, 0x19, 0x0F, 0x3A, 0x73, 0xC2, 0xBE, 0x08, 0x02, 0xD8, + 0x75, 0x8E, 0x98, 0xD8, 0xE0, 0x1E, 0xA5, 0x93, 0x39, 0x95, 0xE4, 0x7B, 0x93, 0x4E, 0x0B, 0xBF, + 0x1A, 0xC7, 0x53, 0xB3, 0xE2, 0xF1, 0xA2, 0x89, 0x48, 0x1F, 0xF1, 0x10, 0xC8, 0x52, 0x1F, 0xE9, + 0x24, 0x19, 0xC2, 0x13, 0x57, 0x58, 0x3F, 0x19, 0xE3, 0x87, 0xD9, 0x3E, 0x51, 0x21, 0x02, 0xC5, + 0xC1, 0x12, 0xC6, 0x15, 0x62, 0x77, 0xAC, 0x6F, 0x17, 0xE8, 0x3A, 0xDD, 0xBC, 0x5D, 0x5A, 0x3D, + 0x74, 0x87, 0xAA, 0x2D, 0x71, 0x0D, 0x3A, 0xE0, 0x1B, 0x48, 0xF6, 0xF7, 0xE5, 0x34, 0x4A, 0x14, + 0xBD, 0xC9, 0x52, 0xA1, 0xED, 0x6D, 0x2C, 0x55, 0x23, 0xAC, 0xCB, 0xF7, 0xB3, 0x1A, 0x05, 0x62, + 0x41, 0x5B, 0x2A, 0xDE, 0x19, 0x9C, 0x26, 0x55, 0xB8, 0xC1, 0x08, 0x2A, 0x8C, 0x17, 0xC9, 0xEF, + 0x51, 0xC7, 0xD6, 0x78, 0x68, 0x32, 0x01, 0x66, 0xBD, 0x13, 0x7A, 0xF1, 0x7D, 0x2D, 0xCD, 0x3E, + 0xD0, 0xC9, 0x6E, 0xD3, 0xE5, 0xDB, 0x31, 0xF6, 0x9C, 0x9B, 0xC6, 0x6D, 0x10, 0xBC, 0xD0, 0x2A, + 0xAA, 0x28, 0xF7, 0x2D, 0xF5, 0xAB, 0x0B, 0x4A, 0xD8, 0xAE, 0x1A, 0xA1, 0xBD, 0x4B, 0xB4, 0xE4, + 0xCE, 0xE9, 0xFD, 0xAE, 0x7A, 0xE4, 0x7C, 0xA3, 0x07, 0xB1, 0x5F, 0x87, 0x21, 0x38, 0x07, 0x06, + 0x00, 0x6F, 0x19, 0x1F, 0xD8, 0x3D, 0x17, 0x05, 0x02, 0x3E, 0xE7, 0xC1, 0xFE, 0xAC, 0x99, 0xD6, + 0x8C, 0xBC, 0x25, 0x95, 0x40, 0x52, 0x2A, 0x3C, 0x68, 0x8E, 0x50, 0xC5, 0x04, 0x31, 0xBA, 0xBB, + 0xB7, 0xB4, 0xDE, 0x46, 0x3F, 0x65, 0x77, 0xFF, 0xA1, 0xDD, 0x8D, 0xAE, 0x92, 0x30, 0x02, 0xDF, + 0xE8, 0x03, 0x05, 0xD8, 0x26, 0xA2, 0xD7, 0xB7, 0x8E, 0x17, 0xB7, 0xDB, 0xED, 0xBA, 0x12, 0xAD, + 0x90, 0x23, 0x83, 0xF6, 0x08, 0x4C, 0x19, 0x7E, 0x99, 0xED, 0xE8, 0x73, 0x49, 0x7C, 0xE2, 0x94, + 0x7D, 0xA2, 0xE6, 0x9A, 0xE3, 0x97, 0x85, 0x4D, 0x83, 0x58, 0x1F, 0x1C, 0xA5, 0x58, 0xBD, 0xE3, + 0xA7, 0x0E, 0x5E, 0xDF, 0xF9, 0x86, 0xDD, 0x69, 0xB7, 0xB5, 0x80, 0x0B, 0xE3, 0x46, 0x64, 0x2D, + 0xCD, 0x8C, 0xD1, 0x2A, 0x62, 0x7C, 0x4B, 0x03, 0x82, 0x2C, 0xCD, 0xB9, 0x6A, 0x60, 0x5E, 0x2B, + 0x52, 0xEF, 0x37, 0x2D, 0x72, 0xAD, 0x67, 0xF1, 0xAB, 0x3D, 0x8B, 0x5C, 0xEF, 0x59, 0xF4, 0x8A, + 0xCF, 0xE2, 0xD7, 0x7C, 0x16, 0xBB, 0xEA, 0xB3, 0xC8, 0x75, 0x9F, 0x45, 0xAF, 0xFC, 0x2C, 0x76, + 0xED, 0x67, 0xD1, 0xAB, 0x3F, 0xCB, 0xB8, 0xFE, 0x33, 0xDF, 0x15, 0xA0, 0x64, 0xF5, 0x4A, 0xEF, + 0x51, 0xC2, 0xE4, 0xE4, 0x75, 0xFF, 0x09, 0xD6, 0xA6, 0x9F, 0x6B, 0xE9, 0xB0, 0x6A, 0xF6, 0x87, + 0x52, 0x96, 0x96, 0xD4, 0x12, 0xD6, 0x5B, 0x4B, 0xCA, 0xE5, 0x64, 0xD4, 0x88, 0xD3, 0xAB, 0x3B, + 0xBD, 0xC6, 0xAB, 0xF3, 0xBE, 0x65, 0x8F, 0x9D, 0xD0, 0xF2, 0xEB, 0xAF, 0xC4, 0xDE, 0x0C, 0x64, + 0x62, 0xC4, 0x52, 0xD5, 0xCC, 0xDC, 0xF4, 0x80, 0x7A, 0x47, 0xC1, 0x74, 0x2E, 0xA4, 0x6F, 0xDE, + 0x9E, 0xED, 0xF7, 0x73, 0xE9, 0x6D, 0x1A, 0x0B, 0xAA, 0xED, 0xF2, 0x4B, 0x99, 0xEA, 0x57, 0x04, + 0x25, 0x83, 0x5E, 0x15, 0x99, 0x9B, 0x00, 0x03, 0x1D, 0x60, 0x2F, 0xC8, 0x6E, 0xFA, 0x27, 0x75, + 0x54, 0x59, 0x49, 0x85, 0x69, 0x65, 0xC5, 0x14, 0x10, 0x9B, 0x28, 0x64, 0xFB, 0x9B, 0xB9, 0xCB, + 0x8B, 0xBD, 0x3E, 0xF0, 0xE2, 0x53, 0x67, 0x92, 0xDE, 0x15, 0x1E, 0x7B, 0x3E, 0xFC, 0xE3, 0xDC, + 0xB5, 0xC8, 0x15, 0x2B, 0x51, 0xEE, 0xF9, 0xB7, 0x12, 0x8D, 0x70, 0x72, 0xA4, 0x4C, 0x66, 0x79, + 0x1C, 0x9B, 0x64, 0xD4, 0xE2, 0xD4, 0x8D, 0x9D, 0xE8, 0x2B, 0x2F, 0xE0, 0x80, 0xB4, 0x42, 0x94, + 0x1F, 0x7E, 0x0C, 0x4B, 0x5E, 0x20, 0xCA, 0x26, 0x1E, 0x2A, 0x8B, 0x0F, 0x7B, 0x88, 0x9E, 0x7F, + 0x68, 0x30, 0x20, 0xBF, 0x92, 0x46, 0x97, 0xBC, 0x78, 0xC1, 0x71, 0x35, 0x9B, 0x38, 0x4A, 0x9D, + 0xA6, 0x31, 0x3A, 0x05, 0xF9, 0x01, 0xEC, 0x49, 0x07, 0x94, 0xB6, 0x4A, 0xAF, 0xCA, 0xC6, 0xB3, + 0x68, 0x10, 0x32, 0x1E, 0x2F, 0x99, 0xC5, 0x40, 0xD1, 0x8B, 0xF2, 0xB2, 0x0B, 0xF4, 0xDF, 0x12, + 0x95, 0x1A, 0xE1, 0xEB, 0x82, 0xEC, 0xBD, 0xC9, 0x09, 0xA8, 0x64, 0xDC, 0xFC, 0x19, 0x83, 0x95, + 0x44, 0xB7, 0x2A, 0x69, 0x55, 0xD8, 0xA3, 0x8A, 0x7A, 0x1C, 0xF9, 0x4E, 0x9A, 0x47, 0x25, 0xE9, + 0x1A, 0x56, 0xB2, 0xAC, 0xFC, 0x69, 0xC6, 0x30, 0x97, 0xC6, 0xFC, 0x19, 0x5E, 0xD8, 0xA1, 0x0A, + 0x38, 0xE9, 0x1E, 0x25, 0x49, 0xD7, 0xF2, 0x4D, 0x66, 0x61, 0xC3, 0x60, 0x77, 0x9E, 0xC1, 0xCF, + 0x73, 0xC1, 0x7A, 0xF2, 0xAE, 0xEF, 0x93, 0x3C, 0x81, 0x0D, 0x55, 0x18, 0x9A, 0xC9, 0x78, 0xEB, + 0xCD, 0xAA, 0x3E, 0x72, 0x69, 0xBC, 0xAD, 0x69, 0xDE, 0xBC, 0x49, 0x46, 0x14, 0x89, 0x54, 0x9E, + 0xD8, 0xBC, 0x66, 0xBD, 0x20, 0x53, 0xFF, 0xAB, 0x8F, 0x89, 0x60, 0x76, 0x7E, 0x36, 0x59, 0x41, + 0x86, 0xD8, 0x9E, 0x06, 0x7D, 0xC1, 0x4E, 0x77, 0x51, 0xF8, 0x6D, 0xA5, 0x7B, 0x78, 0xE2, 0x5B, + 0xC8, 0x82, 0x34, 0x42, 0xF8, 0x99, 0x3C, 0x21, 0xFE, 0xE9, 0x3A, 0x9F, 0x3B, 0x4D, 0xB8, 0xA8, + 0x28, 0xF2, 0x2C, 0x2F, 0x37, 0xEC, 0x58, 0x64, 0x8F, 0xA7, 0x7A, 0x9C, 0x5D, 0xF8, 0x58, 0xBB, + 0x3F, 0xA4, 0xEF, 0x0F, 0xE9, 0xB3, 0x4B, 0x5F, 0x46, 0x56, 0x7E, 0xD9, 0xD5, 0x14, 0x5E, 0x61, + 0xFF, 0xB9, 0xEE, 0x8C, 0xC8, 0x18, 0xF9, 0xC0, 0x2E, 0x1D, 0xFE, 0x24, 0xDD, 0x2F, 0x54, 0xF2, + 0x2A, 0x1D, 0x93, 0x10, 0x36, 0x63, 0xB8, 0xF2, 0xA4, 0x13, 0x6A, 0xBE, 0x99, 0x92, 0xCB, 0xF6, + 0x79, 0xC9, 0xD0, 0x06, 0x41, 0xE4, 0x23, 0xC3, 0x85, 0xCC, 0x25, 0xF8, 0xF6, 0xD9, 0x0D, 0xB5, + 0x65, 0xAC, 0x6D, 0xC9, 0x99, 0x88, 0xE5, 0x60, 0x05, 0xAD, 0x8D, 0x70, 0x44, 0x63, 0x1C, 0x2D, + 0x7E, 0xBD, 0x34, 0x59, 0x5E, 0x8D, 0x5C, 0x6B, 0x69, 0x1E, 0x13, 0x7D, 0x65, 0x4D, 0x57, 0x44, + 0x31, 0x41, 0x18, 0xAA, 0x86, 0xAC, 0xD3, 0xB2, 0x11, 0x91, 0x66, 0x21, 0x31, 0xB4, 0x5A, 0x21, + 0x30, 0xA8, 0x54, 0x04, 0x2D, 0x7B, 0xD9, 0x85, 0xA7, 0xDA, 0x8C, 0x44, 0xB0, 0x2D, 0xE7, 0xAF, + 0xE8, 0x7E, 0xD6, 0xEA, 0xCB, 0xA4, 0x88, 0x49, 0xCD, 0xBE, 0x39, 0xCD, 0xBD, 0x24, 0xAE, 0x8E, + 0x1B, 0x74, 0xA8, 0x43, 0x14, 0x73, 0x2E, 0xFD, 0x13, 0x8F, 0x4B, 0x6A, 0x3F, 0xC6, 0x78, 0xE3, + 0x5A, 0x2F, 0x35, 0x88, 0x76, 0x73, 0x73, 0xDE, 0xCC, 0x6D, 0xEF, 0xCF, 0xA5, 0x78, 0xF2, 0x46, + 0x47, 0x9C, 0x4E, 0x2C, 0x77, 0x78, 0x64, 0xD8, 0xA3, 0x3E, 0x4A, 0x69, 0x30, 0x24, 0xB3, 0xBD, + 0xFF, 0xF1, 0xCF, 0xA2, 0xB3, 0xF3, 0x58, 0x97, 0x1E, 0x40, 0x6A, 0x99, 0xCA, 0x66, 0xE3, 0x99, + 0xF2, 0x10, 0x3A, 0x67, 0x92, 0x48, 0xDA, 0xDF, 0x96, 0x64, 0xB3, 0x5D, 0x8C, 0xFA, 0xF6, 0xF9, + 0x2F, 0xF8, 0x7A, 0xC1, 0x13, 0xE5, 0xDD, 0x34, 0x5E, 0xFC, 0xB9, 0xF3, 0x45, 0xCA, 0x3D, 0x7E, + 0xF3, 0xA2, 0x33, 0xE7, 0xAC, 0xC1, 0x1D, 0x8D, 0xB2, 0xBC, 0xD9, 0xE4, 0x65, 0xB2, 0xFA, 0x0B, + 0xD8, 0xE2, 0x68, 0x1F, 0xF6, 0x30, 0x4D, 0x53, 0xD3, 0x06, 0xBC, 0x5B, 0x02, 0xBC, 0xAB, 0x03, + 0xEF, 0x9A, 0xC0, 0xBB, 0x45, 0xC0, 0x7B, 0x25, 0xC0, 0x7B, 0x3A, 0xF0, 0x9E, 0x09, 0xBC, 0x57, + 0x04, 0x7C, 0xBD, 0x04, 0xF8, 0xBA, 0x0E, 0x7C, 0xDD, 0x04, 0xBE, 0x9E, 0x00, 0xFF, 0x17, 0x10, + 0x59, 0x25, 0x95, 0xC7, 0x4D, 0x10, 0x81, 0xA8, 0x4E, 0xE1, 0x57, 0xF8, 0x37, 0x1C, 0xB5, 0x88, + 0x4F, 0xA9, 0x1B, 0x95, 0xC8, 0xB1, 0x70, 0xC9, 0x60, 0x5B, 0x21, 0xC5, 0xED, 0x38, 0x78, 0x83, + 0xAF, 0x6B, 0x02, 0x64, 0xDA, 0x68, 0xA6, 0x69, 0xD4, 0x01, 0x64, 0x53, 0x37, 0x63, 0x92, 0x19, + 0x80, 0x38, 0x33, 0x61, 0xD3, 0xE9, 0x66, 0x3D, 0x55, 0x1E, 0x5D, 0x65, 0xC7, 0x9E, 0x2A, 0x8F, + 0x8D, 0x6D, 0xF5, 0x73, 0x82, 0x90, 0x91, 0xDF, 0x4C, 0x23, 0x2E, 0x2C, 0xD9, 0xED, 0x11, 0xF1, + 0x9C, 0x63, 0x36, 0xFB, 0xB8, 0xE5, 0xDB, 0x48, 0xFA, 0x79, 0xA2, 0xE5, 0x1C, 0x90, 0x8D, 0x23, + 0xE3, 0x52, 0x95, 0x01, 0x17, 0x15, 0xB3, 0x5E, 0x35, 0x2D, 0xC6, 0xCB, 0x73, 0xBB, 0x2D, 0xD8, + 0xC8, 0xF4, 0x2A, 0x0C, 0x30, 0xDF, 0xDD, 0x74, 0x95, 0x53, 0xE0, 0x5F, 0x7F, 0x25, 0xFC, 0x63, + 0x2F, 0xFD, 0x98, 0x99, 0x0D, 0xDD, 0xF9, 0xA7, 0x03, 0x00, 0xFE, 0x69, 0x3D, 0x2B, 0x5D, 0xBB, + 0x13, 0x14, 0x79, 0x61, 0xF3, 0x45, 0x6B, 0x27, 0xB9, 0xB8, 0x2E, 0xF0, 0x3D, 0xA2, 0x9E, 0x8E, + 0x4F, 0xC9, 0x09, 0x2F, 0xE7, 0x40, 0xD1, 0x0C, 0x66, 0x79, 0xD6, 0xDF, 0x05, 0x2F, 0x1D, 0x3C, + 0xFE, 0xBA, 0x3F, 0x12, 0x97, 0x9C, 0x74, 0x03, 0x52, 0x2F, 0xCB, 0x66, 0x63, 0xAF, 0xC9, 0x22, + 0xB2, 0x3F, 0x81, 0x15, 0x86, 0xBA, 0xDA, 0x79, 0x1C, 0xCB, 0x4B, 0xC2, 0x5F, 0xF1, 0xE4, 0xD8, + 0xD0, 0x63, 0x6B, 0x4D, 0xF5, 0x69, 0x60, 0x62, 0x6F, 0x50, 0xB6, 0xBA, 0x2D, 0xD3, 0xED, 0xF7, + 0x77, 0x1A, 0x06, 0x29, 0x17, 0x72, 0xBA, 0xDF, 0xC9, 0xB6, 0x91, 0x39, 0x4D, 0x58, 0xE7, 0xFE, + 0xD5, 0x12, 0xB3, 0xA8, 0x4C, 0xB1, 0xA5, 0x64, 0xF9, 0x9E, 0x19, 0xF2, 0xB3, 0x31, 0x75, 0x32, + 0xE3, 0xAD, 0xF3, 0x29, 0x35, 0xFC, 0xB1, 0x2F, 0x67, 0xA7, 0xC7, 0xFB, 0x97, 0x3C, 0x23, 0x8E, + 0xE4, 0x72, 0xD7, 0x56, 0xA3, 0x5F, 0x5E, 0xE3, 0x5D, 0x69, 0x8D, 0xDF, 0x94, 0x1A, 0x1B, 0x96, + 0x1A, 0x17, 0xA7, 0x87, 0x3A, 0x8C, 0x6C, 0x07, 0x45, 0x94, 0xDE, 0xBF, 0x50, 0x1F, 0xD5, 0x1A, + 0x17, 0xBF, 0x9F, 0x5E, 0x5E, 0xEC, 0x7F, 0xF8, 0xBD, 0xB8, 0x46, 0xFF, 0xE5, 0xC5, 0xC1, 0xEF, + 0x26, 0xA3, 0x32, 0x9C, 0xC2, 0xE7, 0x9A, 0xAC, 0x53, 0x5F, 0x79, 0xCB, 0xA9, 0xB3, 0x89, 0x9B, + 0xD5, 0x02, 0x6C, 0xEA, 0x93, 0x4F, 0xE5, 0xD5, 0x9E, 0x1A, 0xD5, 0x3A, 0xD6, 0x6A, 0xCF, 0xAA, + 0x41, 0x7B, 0x66, 0x81, 0x66, 0xAB, 0xB7, 0x5D, 0x0D, 0xDC, 0x76, 0x25, 0xE2, 0xBA, 0xBD, 0x4A, + 0xD0, 0xBA, 0xBD, 0x4A, 0xD0, 0x7A, 0xEB, 0x1D, 0x13, 0x9A, 0xB5, 0x13, 0xEC, 0xAD, 0xAF, 0x4E, + 0x05, 0x88, 0xFC, 0x51, 0xB0, 0x6C, 0x45, 0xCB, 0xF0, 0x83, 0x91, 0x85, 0x97, 0xDE, 0x6E, 0x3D, + 0x37, 0xBE, 0xC1, 0xB6, 0xD5, 0x45, 0xA0, 0x53, 0x4D, 0x06, 0x7A, 0x4B, 0x95, 0x81, 0xDE, 0x72, + 0x65, 0xA0, 0xB7, 0x54, 0x19, 0xE8, 0xFD, 0xB3, 0xC9, 0x00, 0x98, 0x78, 0x98, 0xF3, 0x27, 0x75, + 0x92, 0x45, 0x59, 0xEF, 0x11, 0x4F, 0x9A, 0x92, 0x82, 0x60, 0x8F, 0xC8, 0xEE, 0x64, 0xEB, 0x7C, + 0x34, 0xEA, 0x7C, 0xB4, 0xD4, 0xF9, 0x64, 0xD4, 0xF9, 0xA4, 0xA9, 0x6E, 0xA0, 0x46, 0x26, 0x12, + 0x2F, 0xA4, 0x28, 0xEB, 0xCF, 0x52, 0x9E, 0x11, 0x2E, 0xF7, 0x57, 0x29, 0xEF, 0xFE, 0x1A, 0x95, + 0x5F, 0xEF, 0x1F, 0x5F, 0xEE, 0x9F, 0x1F, 0x5A, 0xEA, 0xEE, 0x8F, 0x62, 0x31, 0x18, 0x7A, 0x3C, + 0xD4, 0xD8, 0x09, 0xBF, 0x6A, 0xC1, 0x50, 0xDD, 0x92, 0x88, 0xA7, 0x92, 0xE6, 0x3D, 0x5B, 0x73, + 0xBE, 0xEF, 0x10, 0xF8, 0x1D, 0xF7, 0x6F, 0xD3, 0x28, 0x06, 0x4A, 0x1B, 0x66, 0x7E, 0x73, 0xB4, + 0x3F, 0xCE, 0xE8, 0x2D, 0xAC, 0x7C, 0x09, 0xCB, 0x32, 0xC1, 0x43, 0x69, 0x1D, 0x8B, 0x6D, 0x45, + 0xEF, 0xBC, 0xD8, 0x08, 0x77, 0x60, 0x2F, 0x34, 0xD4, 0xC6, 0x8E, 0xE7, 0x9F, 0xB3, 0x08, 0xAC, + 0x9D, 0x24, 0x87, 0xB4, 0x78, 0xA5, 0xE7, 0xC4, 0x3F, 0x0F, 0x03, 0x3C, 0x9D, 0x4F, 0xE2, 0x8D, + 0x4C, 0x9C, 0x08, 0x75, 0xDF, 0x77, 0x55, 0x83, 0x2E, 0x79, 0x4D, 0xDC, 0x9A, 0xC5, 0x3D, 0x25, + 0xC3, 0x9A, 0xC7, 0xDD, 0x78, 0x86, 0x5C, 0xE6, 0x8E, 0x46, 0x33, 0x57, 0x42, 0x50, 0x61, 0x0B, + 0x74, 0xF6, 0x8E, 0xF0, 0xAF, 0xD9, 0x8E, 0x18, 0x9D, 0x4C, 0xDF, 0xE1, 0x96, 0x4F, 0x49, 0x80, + 0xD1, 0x87, 0xCF, 0x0B, 0xDD, 0x50, 0xEE, 0xBD, 0x3D, 0xEE, 0x9F, 0xAF, 0xF7, 0xC8, 0x0D, 0x7B, + 0x6D, 0x88, 0xBD, 0x63, 0x0E, 0x85, 0x21, 0x7F, 0xC6, 0x1C, 0xDF, 0x9A, 0x60, 0x7E, 0x18, 0x4C, + 0x2B, 0x90, 0x32, 0x87, 0x5C, 0x87, 0xC1, 0x98, 0xEC, 0x9F, 0xF3, 0x06, 0x43, 0x32, 0x01, 0xAA, + 0xD4, 0x44, 0xD5, 0xD9, 0xD7, 0xD0, 0x93, 0x2C, 0x91, 0x47, 0x40, 0x49, 0x38, 0xF6, 0x7C, 0xB0, + 0xB0, 0x6F, 0x3C, 0x30, 0x40, 0xD5, 0x87, 0x2B, 0xB8, 0xB3, 0x38, 0x08, 0x3D, 0x30, 0x7E, 0x9C, + 0x11, 0x7F, 0xD8, 0x42, 0x6E, 0xBF, 0xCC, 0xD4, 0xF9, 0xBF, 0xEC, 0x12, 0x7F, 0x3A, 0x1A, 0x35, + 0xCD, 0x0D, 0x8A, 0x99, 0x4D, 0x5F, 0x67, 0x64, 0x51, 0x50, 0x06, 0xBE, 0x76, 0xD5, 0x87, 0x3F, + 0xDD, 0x5F, 0x8A, 0xAF, 0x54, 0xA8, 0xEF, 0x01, 0xBC, 0xE7, 0xC7, 0x4C, 0x04, 0xFE, 0x0F, 0xFC, + 0x01, 0x66, 0xD9, 0x5E, 0x8B, 0xB7, 0x6D, 0x88, 0xF4, 0xA7, 0xC9, 0x3F, 0xC0, 0x66, 0x43, 0x3B, + 0x59, 0x66, 0x19, 0x7B, 0xF9, 0x4D, 0xEB, 0x68, 0x7A, 0x35, 0xF6, 0x62, 0xF9, 0xCE, 0x1D, 0x3E, + 0x3F, 0x07, 0x93, 0x8C, 0xBD, 0x4C, 0xF1, 0xB9, 0xF3, 0x25, 0xF5, 0x96, 0xA1, 0xBB, 0x59, 0x78, + 0xCC, 0xF0, 0xE5, 0x15, 0x74, 0xEF, 0xAB, 0xC1, 0x2A, 0xB2, 0xB8, 0xED, 0x4C, 0x26, 0x7C, 0x32, + 0x65, 0xC1, 0xB6, 0x18, 0xDA, 0x66, 0x0A, 0xD3, 0xF9, 0x9B, 0x73, 0x27, 0xE0, 0xFD, 0x7E, 0xFA, + 0xE6, 0x75, 0x8C, 0xA1, 0xDA, 0xB0, 0xBB, 0x48, 0x9F, 0x62, 0xC1, 0x0A, 0xED, 0x00, 0x00, 0x36, + 0x6A, 0xE7, 0x6F, 0xC1, 0x6C, 0x04, 0x2E, 0xAE, 0x4D, 0x59, 0x8F, 0x6A, 0x6A, 0x0D, 0x36, 0x95, + 0x24, 0x05, 0xEA, 0xCB, 0x5A, 0x3A, 0x17, 0x2C, 0xDB, 0x33, 0xCC, 0x11, 0xCF, 0x0B, 0x3D, 0x74, + 0xCA, 0x4F, 0xB4, 0x58, 0x49, 0xE3, 0x55, 0x95, 0xDC, 0xD7, 0xDE, 0x35, 0x0D, 0x5C, 0x8C, 0x90, + 0x3D, 0x47, 0x96, 0x0F, 0x34, 0x33, 0x71, 0xF9, 0x34, 0xCB, 0x7B, 0xF7, 0x3E, 0x6F, 0xF2, 0x6A, + 0x08, 0x82, 0x70, 0x48, 0xE3, 0x73, 0xC7, 0x0B, 0xA9, 0xCB, 0x94, 0xA9, 0xBE, 0x64, 0x5C, 0xC5, + 0xFE, 0x4B, 0x56, 0x83, 0x97, 0x59, 0x58, 0xC4, 0x1E, 0x69, 0x63, 0x85, 0x0C, 0x56, 0x10, 0xC7, + 0xD4, 0x6F, 0x2B, 0xC1, 0x05, 0x79, 0x6F, 0x77, 0x9F, 0xF1, 0x77, 0x69, 0xAC, 0x9B, 0x59, 0x86, + 0xF1, 0x38, 0x9A, 0x9C, 0xF1, 0x67, 0xC3, 0x23, 0x8B, 0xBA, 0x05, 0xC2, 0x2E, 0x94, 0x87, 0xCE, + 0x34, 0xA2, 0x8B, 0x5F, 0x40, 0xAB, 0xB1, 0x76, 0x6C, 0xB3, 0x29, 0xE2, 0x54, 0xF3, 0x36, 0xD5, + 0x2A, 0x9C, 0x94, 0x84, 0x64, 0xA3, 0xEB, 0xB8, 0x2E, 0x7B, 0x5A, 0x08, 0x3D, 0x1B, 0xD4, 0xC7, + 0x3C, 0xBC, 0x47, 0x6F, 0x4F, 0x41, 0xF7, 0xC6, 0xF8, 0x8D, 0x3F, 0xE3, 0xD6, 0x22, 0x0D, 0x8A, + 0x55, 0x9A, 0x64, 0x77, 0x4F, 0xDD, 0x3A, 0x0B, 0x86, 0x55, 0xD8, 0x35, 0x67, 0xE3, 0x69, 0xEB, + 0xEA, 0xD3, 0x36, 0xEC, 0x60, 0x1C, 0x4D, 0x13, 0x76, 0x46, 0x00, 0xBF, 0x70, 0xC8, 0xC9, 0xC6, + 0xD8, 0x43, 0x6F, 0xBF, 0x73, 0x07, 0xBF, 0xE8, 0x3B, 0x64, 0x5E, 0xED, 0xB3, 0xF7, 0xA5, 0x1D, + 0xF8, 0x83, 0x91, 0x37, 0xC0, 0x78, 0x9D, 0x84, 0xB7, 0x8D, 0xEC, 0x0D, 0x03, 0x2D, 0xD2, 0x3C, + 0xE7, 0x72, 0x79, 0xDE, 0xC3, 0x6D, 0x59, 0x3E, 0x71, 0x78, 0x38, 0xE1, 0xAD, 0x28, 0x2D, 0x80, + 0xFA, 0x74, 0xA0, 0x1A, 0x77, 0xED, 0x0E, 0x59, 0xCB, 0xC5, 0xA7, 0xF8, 0x6F, 0xBE, 0xAB, 0xB3, + 0xDD, 0x0A, 0x72, 0x19, 0xC4, 0xA5, 0x58, 0x8B, 0x68, 0x53, 0xBA, 0x60, 0x21, 0xCE, 0x92, 0xA9, + 0x75, 0x66, 0xD2, 0x66, 0xCF, 0xFA, 0xCA, 0xF5, 0x47, 0x24, 0xEA, 0xD9, 0x2F, 0x77, 0x70, 0xFD, + 0xC1, 0x4C, 0xBC, 0xDC, 0xEC, 0x02, 0xB6, 0x9E, 0xC8, 0x87, 0xF2, 0x16, 0xEE, 0x86, 0x04, 0x54, + 0xD8, 0x87, 0x1C, 0xFA, 0xD2, 0x0E, 0xE4, 0x77, 0xD2, 0xD2, 0x87, 0xE2, 0x14, 0x9F, 0x73, 0xF7, + 0x68, 0xAE, 0xCC, 0xA1, 0xBC, 0x7F, 0x68, 0xF0, 0x17, 0x75, 0x6F, 0x98, 0xD8, 0xFE, 0xF3, 0x77, + 0x8F, 0xBF, 0x18, 0xB8, 0xEC, 0xDE, 0xBD, 0x52, 0x83, 0x3B, 0xCD, 0xCE, 0x71, 0xE2, 0xF3, 0x3B, + 0xC7, 0x3B, 0x9F, 0xED, 0x5C, 0x26, 0xB1, 0x4B, 0x5E, 0x5A, 0x9A, 0x5A, 0xD3, 0xFE, 0x08, 0x68, + 0xEE, 0xE3, 0x76, 0x72, 0x1E, 0x6F, 0x75, 0xDB, 0x1B, 0xA5, 0x89, 0x33, 0x1F, 0x30, 0x69, 0xCE, + 0x4C, 0xB4, 0x6E, 0xB7, 0x3B, 0x73, 0x24, 0xF9, 0xAC, 0x02, 0xBA, 0x93, 0x0F, 0xD9, 0x22, 0x58, + 0xB6, 0x7C, 0x8E, 0x73, 0xCB, 0x53, 0x41, 0x72, 0x48, 0xFB, 0x1C, 0x51, 0xD2, 0x22, 0xCD, 0x78, + 0x5F, 0x8C, 0x8B, 0x61, 0x85, 0xF6, 0x79, 0xBD, 0x4D, 0xF2, 0x3A, 0x2C, 0xA3, 0xB7, 0x45, 0x49, + 0x22, 0x8C, 0xDE, 0xF2, 0xAA, 0xF3, 0xF7, 0xB6, 0xB8, 0xBD, 0xA5, 0xB7, 0xC6, 0x71, 0xC7, 0xE2, + 0x3D, 0xCE, 0x02, 0xCC, 0xE9, 0xB5, 0xE5, 0x40, 0x85, 0x3F, 0xFA, 0xC8, 0xD3, 0x1B, 0x64, 0x9E, + 0x2A, 0x2E, 0x64, 0x41, 0x29, 0x30, 0x6B, 0x5E, 0x24, 0x95, 0x1D, 0xD9, 0x87, 0x44, 0xE7, 0x66, + 0x44, 0x16, 0x54, 0x12, 0x30, 0xD3, 0xB1, 0x8F, 0xBD, 0x6C, 0x71, 0xE0, 0x4C, 0xDD, 0x23, 0xE3, + 0xC9, 0x43, 0x43, 0xB5, 0xAA, 0x57, 0x10, 0x4B, 0xE5, 0xA4, 0x02, 0x45, 0x5D, 0xBB, 0x0A, 0xAF, + 0x42, 0x91, 0x58, 0xC9, 0x66, 0xA2, 0x68, 0x5E, 0x5C, 0x33, 0xF4, 0x5E, 0x1F, 0xD6, 0x7B, 0xB0, + 0xB8, 0xBD, 0xC1, 0x29, 0x2C, 0x38, 0x8B, 0x8D, 0xA9, 0x0E, 0x87, 0xB3, 0x0F, 0x83, 0x44, 0xE4, + 0x9D, 0x04, 0x2E, 0xFD, 0xFC, 0x36, 0xD5, 0xCB, 0x29, 0xBB, 0xE7, 0x5C, 0x28, 0xFA, 0x2A, 0x40, + 0xB5, 0xD5, 0x31, 0xBF, 0xE7, 0x92, 0xDD, 0xB7, 0x7E, 0x70, 0x42, 0x9F, 0xBD, 0x15, 0x78, 0xC4, + 0x5B, 0x12, 0xD6, 0x94, 0x65, 0x05, 0x81, 0x6D, 0x0D, 0xC6, 0x68, 0xEE, 0x4F, 0xE3, 0x60, 0x1C, + 0xC4, 0xDE, 0x37, 0xDA, 0x26, 0x22, 0xD5, 0x17, 0x87, 0x49, 0xBC, 0x88, 0x5C, 0xC1, 0x3E, 0x1B, + 0x9D, 0x8A, 0x6E, 0x9A, 0xC8, 0x31, 0xAD, 0xAF, 0xC3, 0x6C, 0xD7, 0x66, 0x98, 0x75, 0x33, 0xF5, + 0xA3, 0x56, 0x41, 0x23, 0x19, 0x2C, 0xFC, 0x63, 0xC4, 0x7E, 0xFA, 0x11, 0xCB, 0xCF, 0x00, 0xB8, + 0xE0, 0x5A, 0x52, 0x9A, 0x5A, 0xD0, 0xAE, 0x4D, 0x27, 0x13, 0xF9, 0xFC, 0xFA, 0x5C, 0xEA, 0xA8, + 0xBC, 0xB9, 0x85, 0x07, 0xE6, 0xDD, 0xFE, 0x05, 0x7B, 0x5E, 0x31, 0x55, 0x40, 0xAE, 0x06, 0x3E, + 0xA2, 0xB1, 0xE3, 0x8D, 0xA2, 0x19, 0x7B, 0x3E, 0x0B, 0x20, 0x8D, 0x07, 0xEA, 0xDB, 0xD4, 0x73, + 0xF7, 0x5C, 0x05, 0x92, 0xBF, 0x60, 0x0A, 0x2F, 0x35, 0xD6, 0x2D, 0xEF, 0x64, 0x21, 0xE8, 0xAE, + 0x9D, 0x9B, 0xC5, 0xA0, 0x6D, 0x26, 0x94, 0xE2, 0x50, 0x5B, 0x82, 0xFD, 0xA4, 0x43, 0x2B, 0xD0, + 0x47, 0x86, 0x2F, 0x6F, 0x31, 0xE3, 0xA9, 0x10, 0x58, 0xA9, 0xF1, 0x64, 0xA6, 0x8F, 0x5A, 0x90, + 0x07, 0x85, 0x99, 0x8F, 0x14, 0xB9, 0x57, 0x2B, 0xCF, 0x29, 0xF1, 0x95, 0x40, 0x58, 0xFA, 0xAA, + 0xE5, 0x67, 0x5A, 0xB0, 0xBB, 0x65, 0xB9, 0x9E, 0xCC, 0x1E, 0xA7, 0xF5, 0x17, 0xE9, 0x74, 0x05, + 0x28, 0x59, 0x27, 0x43, 0xF2, 0x88, 0xC2, 0x1C, 0x9D, 0xD6, 0x0E, 0xEB, 0x32, 0xE0, 0xF5, 0x07, + 0x13, 0x1E, 0x0A, 0x7C, 0x66, 0x5F, 0xBC, 0x64, 0x34, 0xC6, 0xC9, 0xE9, 0xB2, 0x3B, 0x91, 0x93, + 0x75, 0x75, 0x6E, 0xF9, 0x2B, 0x49, 0xE3, 0x6A, 0xD3, 0x3A, 0x22, 0x5D, 0x6B, 0x81, 0x7E, 0x50, + 0x6A, 0xF2, 0x7C, 0xAB, 0xD5, 0xEA, 0xB2, 0x74, 0xA9, 0xA5, 0x6A, 0xA7, 0x40, 0x85, 0xD9, 0x28, + 0x33, 0xD4, 0x60, 0x31, 0x69, 0xF9, 0x95, 0xB3, 0xB4, 0x59, 0xF5, 0x2B, 0x1B, 0xAC, 0xEF, 0x4D, + 0xE5, 0x58, 0x03, 0x47, 0xE6, 0xF0, 0xF8, 0x65, 0xC2, 0x7C, 0xEB, 0xA3, 0x9A, 0xBE, 0x37, 0xF8, + 0x8A, 0xE7, 0x02, 0xEC, 0x2D, 0x74, 0xE9, 0xA8, 0xE1, 0xEF, 0xE0, 0x1E, 0x38, 0xEE, 0x21, 0xA6, + 0x2A, 0x6A, 0x64, 0xEB, 0xD8, 0x9E, 0x9D, 0x4C, 0xB2, 0x20, 0x29, 0xB5, 0x97, 0xF0, 0xB8, 0xC2, + 0xA3, 0xBE, 0xE4, 0xF3, 0xA8, 0xAF, 0xF7, 0x3C, 0xDA, 0x8B, 0x3D, 0x39, 0x89, 0x11, 0x34, 0xF3, + 0x46, 0x5E, 0x29, 0xC6, 0x91, 0x23, 0xCE, 0xD0, 0xF1, 0xFC, 0x48, 0xDC, 0x77, 0x82, 0x79, 0xAD, + 0x05, 0x43, 0x7A, 0xBE, 0x4B, 0xEF, 0xF4, 0x84, 0x43, 0xEC, 0xF8, 0x68, 0x47, 0x94, 0x24, 0xCF, + 0xDB, 0x33, 0x71, 0x91, 0x87, 0x47, 0x2B, 0x2B, 0xAC, 0x34, 0x9B, 0x87, 0x3B, 0x24, 0x13, 0x27, + 0x8C, 0x23, 0x26, 0x75, 0x49, 0xB3, 0xCF, 0xAC, 0xF2, 0x17, 0x79, 0x55, 0x82, 0x98, 0x61, 0xD7, + 0x52, 0x87, 0x68, 0xB2, 0xA6, 0x98, 0x57, 0x0C, 0x24, 0xDE, 0x87, 0xB0, 0x78, 0x2A, 0xB3, 0x68, + 0xF0, 0x5C, 0x38, 0x3B, 0x0D, 0x56, 0x48, 0x9D, 0xC5, 0x24, 0xA7, 0xD2, 0x96, 0x57, 0xF0, 0x31, + 0xAF, 0xE0, 0x93, 0x2D, 0x75, 0x38, 0xFE, 0x5C, 0x85, 0xD4, 0xF9, 0x5A, 0xEC, 0x0B, 0x4D, 0xAF, + 0x11, 0x32, 0x76, 0xEF, 0x5A, 0xB8, 0xAA, 0x27, 0x1C, 0x57, 0xCB, 0x27, 0xD3, 0xE8, 0xA6, 0xF1, + 0xA0, 0x7D, 0xD2, 0x33, 0x95, 0xF1, 0x7B, 0x6F, 0x88, 0x08, 0x97, 0x01, 0x33, 0x20, 0x06, 0xF6, + 0x7A, 0x94, 0x97, 0xEA, 0x41, 0xBF, 0x00, 0x48, 0x04, 0x06, 0x88, 0x7C, 0x77, 0x4A, 0xB0, 0x91, + 0x3A, 0xA2, 0xDA, 0xDD, 0x23, 0xB2, 0x67, 0xDC, 0xFC, 0xCA, 0x13, 0x21, 0xA8, 0x6B, 0x17, 0x20, + 0x6C, 0x80, 0xBC, 0x11, 0xE1, 0xBC, 0x52, 0x56, 0x76, 0x9E, 0x18, 0x57, 0xBC, 0x58, 0xF4, 0x43, + 0xA3, 0x76, 0xC4, 0xA8, 0x27, 0xA3, 0x60, 0xC0, 0x68, 0x64, 0x29, 0x39, 0x92, 0xE6, 0x2B, 0xA4, + 0xF6, 0x6F, 0xB5, 0x66, 0x5E, 0x96, 0x79, 0x75, 0x48, 0x90, 0x92, 0x01, 0xC5, 0x1E, 0x80, 0x4E, + 0xB4, 0x1C, 0x7B, 0x96, 0xB3, 0x11, 0x0F, 0xE4, 0x35, 0x85, 0xFE, 0x03, 0x79, 0x98, 0x1B, 0x94, + 0xC6, 0xB9, 0xD9, 0xFD, 0x92, 0x53, 0xF5, 0x63, 0xA6, 0x6A, 0x2F, 0xAF, 0xEA, 0xA7, 0x4C, 0xD5, + 0x75, 0xA3, 0xAA, 0x7D, 0xF6, 0x2B, 0x03, 0x6A, 0xCB, 0x67, 0xA1, 0x52, 0x5D, 0x52, 0xE3, 0x63, + 0x69, 0x8D, 0x4F, 0x79, 0x35, 0x74, 0xD2, 0xB4, 0xD4, 0x0D, 0xC9, 0x6D, 0x53, 0x85, 0xD1, 0xC4, + 0x09, 0x43, 0xE7, 0xBE, 0x25, 0x04, 0x80, 0x25, 0xDF, 0x1D, 0x07, 0x7E, 0x10, 0x4D, 0x9C, 0x01, + 0x25, 0xCC, 0x1D, 0xC2, 0xD4, 0xB0, 0x71, 0xCF, 0x34, 0x15, 0x14, 0x25, 0x60, 0x21, 0x57, 0x0C, + 0xC4, 0xF5, 0x9A, 0x74, 0xE9, 0x47, 0x61, 0xC8, 0x2A, 0x95, 0xCC, 0xCA, 0xC0, 0xED, 0xF6, 0x3C, + 0xB0, 0x69, 0xF7, 0xD5, 0x2E, 0x63, 0xA0, 0x9F, 0x19, 0xA2, 0xA1, 0x5C, 0x43, 0xC5, 0x18, 0xFD, + 0x20, 0xA4, 0x6E, 0xAD, 0x28, 0x45, 0x0D, 0xDB, 0x74, 0x2C, 0x8C, 0x56, 0x94, 0x3E, 0x27, 0xBF, + 0xAF, 0x7D, 0x5C, 0xFB, 0x54, 0xD3, 0xAF, 0x23, 0x59, 0x7A, 0xFF, 0x82, 0x6C, 0x36, 0x33, 0x56, + 0x58, 0x2E, 0x4F, 0x23, 0xEF, 0xEF, 0x94, 0xD8, 0x54, 0xB3, 0xA6, 0x1D, 0x93, 0x9B, 0x07, 0xE9, + 0xD2, 0x39, 0xEB, 0x5A, 0x89, 0x13, 0x34, 0x98, 0xB0, 0xB1, 0x57, 0x02, 0x3D, 0x06, 0xB0, 0x8C, + 0x80, 0x18, 0x88, 0x0B, 0x07, 0x75, 0x5E, 0x41, 0x9D, 0xA6, 0xFC, 0x4B, 0x3B, 0xE6, 0x17, 0xCB, + 0xB3, 0x2B, 0x5F, 0xA6, 0xA6, 0x9C, 0x43, 0xAC, 0x58, 0x9F, 0x6D, 0xB9, 0x4C, 0xC0, 0x8B, 0x38, + 0xBC, 0xB9, 0xBE, 0x28, 0xFC, 0xA9, 0x51, 0xFB, 0x5F, 0xF6, 0x46, 0x02, 0x1B, 0xB4, 0xA5, 0xCE, + 0xE0, 0xA6, 0x61, 0xDF, 0x27, 0xA8, 0x3A, 0xE9, 0x4F, 0x8D, 0xF8, 0xC6, 0x8B, 0x9A, 0xAC, 0x23, + 0x8D, 0x66, 0xA1, 0x56, 0xF7, 0x75, 0xAD, 0xDE, 0x8E, 0xA6, 0x57, 0x11, 0xB7, 0x4C, 0xD9, 0x0B, + 0x78, 0x5A, 0x42, 0x3D, 0x05, 0x68, 0xD2, 0x14, 0x16, 0x3C, 0x7E, 0x17, 0x49, 0x6A, 0xB2, 0x64, + 0x09, 0x94, 0xFA, 0xCA, 0xF8, 0xB0, 0xFE, 0x85, 0x83, 0x48, 0xF7, 0x4C, 0xDF, 0x75, 0x23, 0x5C, + 0x86, 0xD2, 0x56, 0x33, 0xC4, 0x65, 0xED, 0x32, 0x63, 0x5C, 0xAF, 0x57, 0xC5, 0x20, 0x97, 0x2D, + 0x96, 0x65, 0x94, 0xA7, 0xF7, 0xC6, 0x9F, 0x3C, 0xCE, 0xDB, 0x87, 0x8F, 0xF9, 0xDC, 0xE1, 0x63, + 0x3E, 0x71, 0xF8, 0x83, 0xEC, 0xF3, 0x44, 0x82, 0x66, 0xB7, 0xD1, 0x65, 0xD3, 0x59, 0xED, 0xF4, + 0x44, 0x04, 0x67, 0xB3, 0xD5, 0x0D, 0x74, 0x8A, 0xBD, 0x6E, 0xCC, 0x16, 0xCD, 0x5A, 0x15, 0xD1, + 0xE9, 0xD6, 0x22, 0x11, 0x8C, 0x6E, 0x2B, 0x93, 0xF2, 0x6A, 0x94, 0x69, 0x52, 0x66, 0x2F, 0xD3, + 0x25, 0xE3, 0x01, 0x6C, 0x7F, 0x63, 0xC4, 0xAC, 0xF6, 0x7F, 0x52, 0x47, 0xDB, 0x03, 0xFC, 0xF4, + 0x7C, 0xB2, 0xED, 0x27, 0x24, 0xD1, 0xF9, 0x7B, 0x8A, 0x8C, 0x6A, 0x2D, 0x33, 0x89, 0x4D, 0x01, + 0x9C, 0xD9, 0x2C, 0x4E, 0x44, 0xF1, 0xE7, 0xD9, 0x5E, 0x24, 0x63, 0x5B, 0x7D, 0x8B, 0x51, 0xC4, + 0x59, 0xC5, 0x0D, 0xA8, 0x6E, 0xD6, 0x6E, 0x1C, 0x7A, 0x4A, 0xE3, 0x9B, 0xC0, 0xAD, 0x96, 0xB6, + 0x35, 0xBD, 0x50, 0x91, 0xCD, 0xD9, 0xAA, 0x80, 0x4C, 0xF9, 0xAF, 0xC0, 0x37, 0x8E, 0x47, 0x2C, + 0xBE, 0xCD, 0x7C, 0x67, 0x98, 0xCD, 0x5B, 0x9C, 0xEB, 0xD6, 0x03, 0x9C, 0xF0, 0x4D, 0xE4, 0x0A, + 0xC8, 0xBB, 0x7C, 0xD2, 0x24, 0xAB, 0xA4, 0xA1, 0xD4, 0x31, 0x9D, 0xC5, 0xA2, 0xD2, 0x1A, 0xCB, + 0xDD, 0x0C, 0x23, 0x96, 0xAD, 0x6A, 0x0F, 0x88, 0x92, 0x4D, 0x9A, 0xC5, 0xC4, 0x4B, 0x33, 0x00, + 0x68, 0x6D, 0xC7, 0x01, 0x8B, 0x5F, 0x6C, 0xAC, 0x97, 0x65, 0xED, 0xCD, 0x67, 0x97, 0xCE, 0x80, + 0x12, 0x6E, 0x19, 0xBC, 0xB5, 0xB3, 0xCB, 0xA4, 0xB5, 0x09, 0x3C, 0x78, 0x6C, 0x7E, 0x65, 0xEF, + 0x0B, 0xD9, 0xD9, 0x65, 0x6E, 0xA7, 0x7F, 0x5A, 0xFD, 0x31, 0x1D, 0x9F, 0x8B, 0x76, 0xAC, 0xBD, + 0xB6, 0xAB, 0x90, 0x18, 0x93, 0x4A, 0x7B, 0xBB, 0x64, 0xCB, 0xB6, 0x68, 0x8F, 0x9C, 0x58, 0x42, + 0x49, 0x2B, 0xAF, 0x92, 0x0D, 0xE4, 0x64, 0x2F, 0xEB, 0x6B, 0xCE, 0x5F, 0xA4, 0x2D, 0x5B, 0xEA, + 0x92, 0x7B, 0x60, 0x59, 0x37, 0x80, 0xA4, 0x3A, 0x21, 0x6A, 0x2F, 0x7B, 0x08, 0xAA, 0x6D, 0x96, + 0xC6, 0xB0, 0x43, 0x94, 0xE4, 0x77, 0x77, 0x94, 0x3F, 0x5F, 0x24, 0x1D, 0x53, 0xBE, 0xEA, 0x21, + 0xE9, 0x95, 0xA8, 0x5C, 0xD9, 0xD5, 0xEC, 0xF8, 0x14, 0xC1, 0x0A, 0x31, 0x09, 0xCF, 0x86, 0x41, + 0x7E, 0xCF, 0xE1, 0x44, 0xF6, 0x96, 0x9B, 0x60, 0x05, 0x40, 0x95, 0x64, 0xFF, 0x5C, 0x5C, 0x31, + 0x29, 0x2E, 0x62, 0x4B, 0x7E, 0x27, 0x2A, 0x73, 0xC8, 0xA2, 0xDA, 0x38, 0x22, 0x45, 0x42, 0xD7, + 0x2D, 0x82, 0x66, 0x55, 0x23, 0xB6, 0xC6, 0xBD, 0xFC, 0xC6, 0xB9, 0x91, 0xA9, 0x19, 0x20, 0xDD, + 0x2F, 0x46, 0x5C, 0xF0, 0x3C, 0xB7, 0x0D, 0xE5, 0x75, 0xC1, 0x19, 0xE0, 0xF4, 0x0A, 0x6E, 0x2D, + 0x6A, 0x70, 0xB4, 0xC3, 0xBA, 0xBC, 0x0C, 0x23, 0x96, 0x29, 0xBD, 0x93, 0x5B, 0x57, 0x9F, 0x23, + 0x65, 0xF5, 0x12, 0xA9, 0x29, 0xA9, 0x98, 0x0E, 0x77, 0x7E, 0x45, 0x63, 0x68, 0x4B, 0x2B, 0x9A, + 0xC3, 0x58, 0xF5, 0x21, 0x07, 0xF5, 0xB6, 0x5B, 0xC4, 0x55, 0xBB, 0x64, 0x0C, 0x61, 0x9A, 0x38, + 0x9B, 0xE8, 0xF9, 0x7B, 0x91, 0xE7, 0x2E, 0x69, 0x3C, 0x97, 0xF7, 0x4E, 0xB7, 0xC1, 0x0A, 0x3D, + 0x78, 0x8A, 0x46, 0x2E, 0xF2, 0xE2, 0x19, 0xDB, 0x83, 0xCA, 0x9E, 0x3C, 0x9B, 0x74, 0x98, 0x92, + 0xF3, 0x80, 0x1E, 0xBD, 0x39, 0xD1, 0x27, 0x9E, 0x3D, 0x99, 0x02, 0x77, 0x0D, 0x64, 0xCD, 0xEE, + 0xE0, 0x33, 0x19, 0x53, 0xD1, 0xC9, 0xA7, 0xB0, 0x5D, 0x77, 0xF4, 0x99, 0x3B, 0xE7, 0x19, 0x9D, + 0x7D, 0x55, 0x36, 0xDE, 0xCB, 0x74, 0xF8, 0x19, 0xDB, 0xE7, 0xC5, 0x9C, 0x7E, 0x0A, 0x53, 0x66, + 0x73, 0xFC, 0x25, 0xB3, 0xE5, 0x87, 0x3A, 0xFF, 0x34, 0xF1, 0x50, 0x6D, 0x2B, 0xB4, 0xA2, 0x9E, + 0x9A, 0xAB, 0xE5, 0xD2, 0x5C, 0x84, 0x99, 0xB5, 0x52, 0xAF, 0xB0, 0x61, 0xB6, 0xD8, 0x34, 0x3F, + 0x6C, 0x95, 0x81, 0x78, 0xAA, 0xB9, 0x21, 0x4B, 0x95, 0xE1, 0x83, 0x38, 0x3F, 0x15, 0x19, 0xF8, + 0x9E, 0xB9, 0x2C, 0xAE, 0x79, 0x31, 0x95, 0xBB, 0xAC, 0xDC, 0xDC, 0x86, 0x7F, 0x95, 0x91, 0x6D, + 0xFF, 0x0D, 0x34, 0x7B, 0xA3, 0x5E, 0x4F, 0x2E, 0xF0, 0x1A, 0x55, 0x5A, 0xE5, 0x55, 0xFE, 0xFA, + 0xD7, 0x4C, 0x9D, 0x90, 0xC6, 0xD3, 0xD0, 0x67, 0xA6, 0xBA, 0x41, 0xDD, 0x90, 0xC6, 0x78, 0x6D, + 0x58, 0x53, 0xC5, 0xEC, 0xA6, 0x36, 0xA8, 0x2C, 0x90, 0x1E, 0x59, 0x66, 0x7D, 0xED, 0x2D, 0x53, + 0x47, 0xEC, 0xB1, 0x14, 0xD7, 0xDD, 0xC9, 0x35, 0x73, 0xD5, 0xC5, 0xCE, 0x15, 0xB9, 0x75, 0x22, + 0x82, 0x2B, 0x37, 0xC1, 0xAB, 0xC6, 0xD4, 0x6D, 0x11, 0x3E, 0xA5, 0x09, 0x4B, 0x53, 0xCA, 0xEF, + 0xA2, 0x47, 0x83, 0x10, 0xF3, 0x19, 0x19, 0xBB, 0x9C, 0x11, 0x3D, 0x75, 0x7C, 0x67, 0x48, 0xC3, + 0x77, 0x58, 0x33, 0x13, 0xF2, 0xCB, 0xDA, 0xEF, 0xBD, 0x88, 0x43, 0xE2, 0x60, 0x2C, 0xCD, 0x6E, + 0x7D, 0x44, 0xAF, 0xE3, 0x3A, 0x7C, 0xB8, 0xD9, 0x43, 0x4F, 0xC2, 0x8B, 0x35, 0xF8, 0x05, 0xFF, + 0xE8, 0x83, 0x22, 0x13, 0x7F, 0xB8, 0x7B, 0x2F, 0xD2, 0xA4, 0xE1, 0xBB, 0xDC, 0xDE, 0xB8, 0x0A, + 0xEE, 0xEA, 0xC4, 0x73, 0x77, 0xEB, 0x88, 0x90, 0x6F, 0xEC, 0xF7, 0x47, 0xA3, 0x3A, 0x61, 0x69, + 0xB7, 0xE0, 0x6B, 0x10, 0x8E, 0x57, 0x59, 0xC5, 0x55, 0xDE, 0x54, 0xA1, 0x8B, 0xF9, 0x27, 0xEB, + 0xB0, 0x3C, 0x1E, 0xE2, 0x1D, 0x50, 0x0E, 0x41, 0x92, 0x1C, 0x0C, 0x87, 0x78, 0xD9, 0x13, 0xE8, + 0x59, 0x43, 0xB4, 0x6B, 0x71, 0x98, 0xFC, 0xC3, 0xE8, 0x56, 0xD4, 0x36, 0x7B, 0x80, 0x1A, 0x3F, + 0xCA, 0x6C, 0xB8, 0x35, 0x85, 0x95, 0x77, 0xE3, 0xD1, 0x4D, 0x1C, 0x4F, 0x0A, 0x2F, 0x72, 0x2B, + 0xF5, 0xC4, 0x7D, 0xEE, 0x57, 0xC7, 0xFC, 0x3A, 0x37, 0xAE, 0xC2, 0xEC, 0xB6, 0x79, 0x2D, 0x6B, + 0x9B, 0xC9, 0x16, 0xEC, 0xB6, 0xAE, 0xAA, 0x27, 0x40, 0xCE, 0x23, 0x7A, 0xE2, 0x0F, 0x82, 0x31, + 0xAA, 0x11, 0x59, 0x2D, 0xA4, 0xD1, 0x04, 0xCC, 0x09, 0x46, 0x24, 0x4B, 0x41, 0x25, 0x72, 0x16, + 0x90, 0xC3, 0xFE, 0x6F, 0x3C, 0xCB, 0x00, 0x58, 0x26, 0x01, 0x5B, 0xFC, 0x9F, 0x54, 0x1F, 0x48, + 0x30, 0xBD, 0xB5, 0xEE, 0x97, 0x2C, 0xA6, 0xBA, 0xD4, 0x29, 0x9B, 0xF5, 0xEF, 0xA6, 0x70, 0x8B, + 0x44, 0x4B, 0x19, 0xF9, 0x0E, 0xAE, 0x62, 0xC7, 0x03, 0x31, 0x54, 0xCA, 0xAD, 0x32, 0x6E, 0xAD, + 0x67, 0xCA, 0x39, 0xBF, 0xA7, 0x9A, 0x54, 0x28, 0x88, 0xE6, 0x16, 0xB5, 0x1E, 0x66, 0x84, 0x65, + 0x52, 0xA9, 0x1F, 0x38, 0xC8, 0x79, 0x5C, 0x80, 0xF1, 0x55, 0xBA, 0x5E, 0x3E, 0x56, 0x07, 0x2C, + 0xE5, 0x62, 0xF1, 0x78, 0x61, 0x9D, 0xAA, 0x63, 0xC6, 0xEB, 0x96, 0x8C, 0x9B, 0x48, 0x54, 0xF3, + 0x43, 0xC7, 0x8E, 0xD1, 0xF0, 0x53, 0x8C, 0x5F, 0x86, 0x1B, 0xE5, 0x63, 0xA8, 0x4C, 0xF1, 0xA3, + 0xE0, 0xD6, 0x47, 0x3F, 0x53, 0x9A, 0x64, 0x81, 0xA9, 0x54, 0xEA, 0xE2, 0xBC, 0xAD, 0x7E, 0x67, + 0x1F, 0x21, 0x9E, 0x1C, 0x29, 0x9E, 0x54, 0x8E, 0xD6, 0x67, 0x9E, 0xB3, 0xB7, 0xD7, 0x0C, 0x58, + 0x5F, 0x40, 0x9E, 0x1F, 0xA8, 0x66, 0xCD, 0x9A, 0x4F, 0xC5, 0xC9, 0x57, 0x63, 0x5E, 0xF2, 0x5B, + 0xFB, 0xF8, 0xA0, 0x2F, 0xA6, 0xB0, 0xC1, 0xA4, 0x12, 0x51, 0xCC, 0x4A, 0xF4, 0x1C, 0x38, 0x49, + 0x55, 0x45, 0x7A, 0x55, 0x98, 0xBB, 0x76, 0xEA, 0x9B, 0x62, 0x91, 0x4E, 0x9D, 0xC4, 0xD3, 0x70, + 0x14, 0x07, 0x03, 0xCC, 0xED, 0x07, 0xD2, 0xB6, 0x86, 0x20, 0xFE, 0x8D, 0x91, 0x8F, 0x7E, 0x72, + 0x8D, 0x9B, 0x9F, 0x53, 0xF0, 0x98, 0x94, 0x0F, 0x7D, 0xE7, 0xBF, 0x3A, 0x8C, 0x9A, 0x5D, 0x57, + 0x0C, 0x83, 0x10, 0x63, 0x75, 0x23, 0x98, 0x80, 0x17, 0x3C, 0x05, 0xA5, 0x0A, 0xB5, 0xDB, 0xD2, + 0x1D, 0xDF, 0xBE, 0x09, 0xE9, 0x35, 0xE6, 0xFC, 0x93, 0xD5, 0x84, 0xD0, 0xA5, 0xB8, 0x30, 0xCF, + 0x64, 0xCE, 0xE8, 0xCB, 0x65, 0x4F, 0xF1, 0x2D, 0xCA, 0x45, 0x76, 0xF6, 0xC1, 0xCF, 0x4B, 0xD5, + 0x80, 0xFB, 0x83, 0x14, 0xAA, 0x96, 0xAC, 0xC1, 0xCF, 0xA4, 0x6A, 0x48, 0x2B, 0x62, 0xBA, 0x86, + 0xE4, 0xBD, 0x9A, 0x64, 0x65, 0x4A, 0x56, 0xFC, 0x34, 0x0E, 0xB7, 0x4C, 0xC2, 0xA9, 0x9E, 0x44, + 0x64, 0x59, 0xF2, 0xFD, 0x24, 0xEF, 0xA0, 0x24, 0x33, 0xEE, 0xD2, 0x7E, 0xC7, 0xB7, 0x91, 0xD8, + 0xEA, 0x2E, 0x4E, 0x4C, 0x32, 0x0A, 0x51, 0x95, 0x2C, 0x73, 0xB3, 0xA6, 0xE4, 0x84, 0xB4, 0x01, + 0xB7, 0x66, 0x85, 0xAC, 0x2E, 0x99, 0x77, 0x19, 0x81, 0x64, 0x7D, 0xA9, 0xED, 0x54, 0x56, 0x9D, + 0x85, 0xBA, 0x33, 0x21, 0xA4, 0x5C, 0x63, 0x6A, 0xAF, 0x8B, 0x5E, 0xD0, 0x6B, 0x50, 0x91, 0x37, + 0x3C, 0x43, 0x4F, 0x72, 0x8E, 0x5C, 0x68, 0x59, 0x68, 0x86, 0xB2, 0x91, 0xC2, 0x8C, 0x65, 0x8C, + 0x79, 0xE9, 0x19, 0x12, 0xAF, 0xA4, 0xFF, 0xC1, 0x5F, 0xBB, 0x4B, 0xC9, 0xF8, 0xC3, 0x21, 0xCD, + 0x99, 0xE4, 0x87, 0x13, 0x6A, 0x09, 0x5C, 0x9F, 0x84, 0xC1, 0x10, 0xB3, 0x7D, 0x03, 0x60, 0xF9, + 0xEB, 0x6B, 0xC7, 0x77, 0x47, 0x98, 0x40, 0x59, 0x65, 0x2C, 0x83, 0x92, 0x6D, 0xCE, 0x34, 0x0C, + 0xA6, 0xDC, 0xE5, 0x39, 0x73, 0x66, 0x69, 0xCA, 0xFD, 0x4E, 0x22, 0x73, 0xEF, 0x2C, 0x0D, 0x9D, + 0x2B, 0xFE, 0x42, 0x31, 0xFB, 0x6F, 0x7E, 0x43, 0x23, 0xAF, 0x51, 0x61, 0x46, 0x23, 0x25, 0x0F, + 0x9C, 0xC1, 0x05, 0x99, 0x7F, 0x26, 0x1D, 0xDC, 0x09, 0x0D, 0x07, 0x94, 0x85, 0xB1, 0xF0, 0xB2, + 0xF6, 0x88, 0xE5, 0xAA, 0x21, 0x6B, 0x84, 0xFF, 0x19, 0x07, 0x31, 0x6E, 0xF4, 0xFE, 0x8C, 0x07, + 0x37, 0x4A, 0x1A, 0x1F, 0x01, 0xF6, 0xC0, 0x09, 0x15, 0x1F, 0xEC, 0xA9, 0x13, 0xDF, 0xB4, 0xC3, + 0x60, 0x0A, 0xD4, 0x08, 0xB0, 0x4A, 0x56, 0xD0, 0xA9, 0x92, 0x08, 0xC9, 0x30, 0x4E, 0x2C, 0xED, + 0x70, 0xAA, 0xFD, 0x6F, 0x21, 0x90, 0xD4, 0x4D, 0x33, 0xDD, 0xB2, 0x3D, 0xBF, 0x20, 0x79, 0x0F, + 0xF3, 0xFA, 0x75, 0xCC, 0x73, 0xC6, 0x02, 0x3C, 0x35, 0x25, 0x9B, 0x6E, 0x8B, 0xDC, 0x86, 0x1E, + 0x4B, 0xFB, 0xC3, 0xC4, 0x1B, 0x8C, 0x08, 0x26, 0xD3, 0xEC, 0x11, 0xBD, 0x9A, 0x25, 0xD9, 0xAB, + 0x21, 0x13, 0x3A, 0x23, 0xCB, 0xF0, 0xF2, 0x54, 0x4C, 0x24, 0xC9, 0xC5, 0x54, 0xC6, 0x48, 0xE9, + 0x19, 0x5C, 0xC6, 0x04, 0x67, 0xDF, 0x12, 0x25, 0x8E, 0x99, 0x94, 0xB8, 0xEF, 0xE9, 0xE0, 0xFE, + 0xC4, 0x9D, 0x95, 0x6A, 0x85, 0x1F, 0xAA, 0x9C, 0xCF, 0xC5, 0x8C, 0x97, 0x0E, 0x10, 0xE9, 0xEA, + 0x40, 0xD5, 0x39, 0x30, 0x17, 0xD0, 0x7D, 0x04, 0x20, 0xA0, 0xA6, 0x60, 0xE3, 0x81, 0xC8, 0x6C, + 0x70, 0x80, 0xEB, 0x66, 0xE6, 0xED, 0x3A, 0x71, 0x5B, 0xF4, 0x5B, 0x5C, 0x94, 0x69, 0x81, 0xBB, + 0x3E, 0x13, 0x48, 0xF6, 0x3B, 0x9E, 0xD6, 0x7D, 0x1E, 0xF7, 0xD9, 0x96, 0xB4, 0xE4, 0xA3, 0xCD, + 0x24, 0x42, 0xD2, 0xC1, 0x5E, 0x2E, 0x57, 0x12, 0x62, 0x6E, 0x6F, 0x3C, 0xB5, 0xAD, 0xE4, 0x00, + 0x98, 0x27, 0xA9, 0x28, 0xE9, 0x5C, 0x51, 0xD2, 0x8C, 0xA4, 0x73, 0xBC, 0xD2, 0x3C, 0x9D, 0x2B, + 0x6A, 0xA9, 0x75, 0x8E, 0x57, 0xAC, 0xD8, 0xB9, 0xA9, 0x3B, 0x29, 0xE9, 0xD6, 0x7B, 0x77, 0x52, + 0xDE, 0xB3, 0xA9, 0x3B, 0x99, 0xB9, 0x4F, 0xB9, 0x6D, 0xB4, 0xDE, 0x24, 0xE8, 0x8D, 0x0E, 0x75, + 0x3B, 0x5D, 0x99, 0xD4, 0x55, 0xEF, 0x91, 0x7B, 0x33, 0x98, 0xC8, 0xA7, 0x37, 0xB3, 0xBD, 0x12, + 0x05, 0x47, 0xAF, 0x0F, 0xCF, 0xED, 0xFD, 0x51, 0xD2, 0x2F, 0x9D, 0x9C, 0xEB, 0xF4, 0x49, 0xA8, + 0x85, 0x7D, 0x53, 0xD2, 0x23, 0x95, 0xB7, 0x57, 0xE9, 0x86, 0x82, 0xDB, 0x20, 0xFC, 0xCA, 0x42, + 0xDB, 0x74, 0x7B, 0xD8, 0x88, 0x82, 0xC4, 0x6F, 0xF8, 0x8E, 0xEA, 0x19, 0x6F, 0x50, 0x39, 0x19, + 0xB8, 0xD2, 0xE6, 0x4B, 0x1D, 0x2F, 0xD9, 0x27, 0x65, 0x7F, 0xDA, 0xED, 0xF7, 0x4D, 0xF3, 0xD9, + 0xB0, 0xF8, 0x54, 0x84, 0x79, 0x06, 0x1F, 0x32, 0x59, 0xAD, 0x87, 0x36, 0x9D, 0xF1, 0x6C, 0x41, + 0xA7, 0x69, 0x1C, 0x37, 0x69, 0x59, 0xE8, 0x35, 0x0F, 0x24, 0x2B, 0xB3, 0xA5, 0xF4, 0x3F, 0xA3, + 0xB7, 0x32, 0x8F, 0xA1, 0x36, 0xBE, 0x06, 0x03, 0xCD, 0x93, 0x1D, 0x1C, 0x98, 0xD3, 0x68, 0x28, + 0xDE, 0x88, 0x94, 0x39, 0xFC, 0x0E, 0x05, 0x44, 0x28, 0xC1, 0xA4, 0x90, 0xF8, 0x5A, 0x2D, 0x5B, + 0x05, 0xF0, 0x82, 0x3F, 0x1D, 0x4F, 0xF0, 0xF9, 0xF0, 0x9D, 0x22, 0xEB, 0x58, 0xDC, 0xCD, 0x3D, + 0x34, 0x08, 0xCB, 0x0B, 0x28, 0x11, 0x44, 0xE4, 0xE2, 0x07, 0x41, 0xF1, 0xF1, 0x16, 0x3D, 0xBE, + 0x9D, 0x14, 0x10, 0xA4, 0xA6, 0x66, 0xA4, 0x54, 0xC5, 0xD1, 0x17, 0xF9, 0xF4, 0x70, 0x8B, 0xAE, + 0xB8, 0x17, 0x60, 0xBB, 0x09, 0x66, 0x8A, 0x7C, 0x64, 0x99, 0x35, 0x26, 0x38, 0xAE, 0xEC, 0xB0, + 0xED, 0xFC, 0x43, 0x94, 0x0A, 0xD4, 0x28, 0x12, 0x4B, 0xD4, 0x7C, 0xB2, 0x53, 0x20, 0x26, 0x0A, + 0xE8, 0x3C, 0x29, 0x51, 0x88, 0x5F, 0xD9, 0x55, 0x1B, 0xA4, 0x5B, 0x00, 0xFE, 0x58, 0xB4, 0x5E, + 0x22, 0xA3, 0xE4, 0xA0, 0x50, 0x1B, 0x00, 0x5D, 0x5F, 0x5D, 0x1C, 0x2A, 0x23, 0xA0, 0xBC, 0x35, + 0xCA, 0x8E, 0x87, 0x73, 0x48, 0xC8, 0x34, 0x6D, 0x61, 0x6D, 0x89, 0x46, 0x7B, 0x70, 0xA2, 0xAC, + 0x21, 0x1B, 0xA9, 0x96, 0x1C, 0x10, 0xA3, 0xB6, 0x29, 0xBC, 0x98, 0xE2, 0x50, 0x86, 0x24, 0xAB, + 0xC7, 0xAE, 0x52, 0x34, 0x98, 0x83, 0x80, 0xE5, 0xA7, 0xE0, 0xBB, 0x25, 0x09, 0xAC, 0x69, 0xCD, + 0x9E, 0xA8, 0x95, 0xA7, 0xE1, 0xB9, 0x0A, 0x3E, 0x6B, 0xFE, 0xDA, 0x0C, 0x51, 0xB6, 0x2C, 0xB6, + 0xAC, 0x12, 0x00, 0xB7, 0xCD, 0x3C, 0x2D, 0x0B, 0x6B, 0x0E, 0xCA, 0x66, 0xD6, 0xA7, 0x50, 0x3B, + 0xC4, 0x76, 0x4C, 0xCE, 0x05, 0x59, 0x28, 0x4E, 0x39, 0xED, 0x93, 0x00, 0xE0, 0xD2, 0xE9, 0x23, + 0x08, 0x4D, 0xF2, 0x78, 0x12, 0x58, 0x38, 0x22, 0xF1, 0xBC, 0xAB, 0xAE, 0x6C, 0x13, 0x1C, 0xBF, + 0xF1, 0x1A, 0x8D, 0x6B, 0xFD, 0x6F, 0xAD, 0x7F, 0xB9, 0x38, 0x95, 0xE7, 0x89, 0x8C, 0xF6, 0x2C, + 0xE9, 0xD8, 0xF1, 0xC5, 0xC5, 0xDB, 0x8B, 0xDA, 0x7C, 0x5A, 0x08, 0xD8, 0xC1, 0x17, 0x3E, 0x96, + 0x90, 0xC4, 0xF9, 0x06, 0xD6, 0x1C, 0x77, 0x86, 0xEF, 0x18, 0x6B, 0x95, 0xEB, 0x7D, 0x7B, 0x45, + 0x63, 0x4D, 0xF9, 0xE8, 0x07, 0x94, 0xA5, 0xFA, 0xC9, 0x88, 0x61, 0xD3, 0xD5, 0x9C, 0x96, 0xAC, + 0xC1, 0xD6, 0xC9, 0xC3, 0xF7, 0x17, 0x17, 0xC7, 0x67, 0xEF, 0x6C, 0xDD, 0xCC, 0xED, 0xA1, 0xA4, + 0x03, 0xD5, 0xEC, 0x74, 0x82, 0xBA, 0x0E, 0x8F, 0xFB, 0x1F, 0xBD, 0x6F, 0xAA, 0x0A, 0x37, 0xF1, + 0x98, 0xE6, 0x2F, 0x8B, 0x5B, 0x00, 0x42, 0xBF, 0xD5, 0x58, 0x74, 0xAF, 0xC6, 0x87, 0x9D, 0x42, + 0x40, 0x76, 0x6A, 0xF4, 0xFC, 0xB4, 0x08, 0xFD, 0x3C, 0x6F, 0xAB, 0x62, 0x97, 0x43, 0xBD, 0x91, + 0x22, 0x8D, 0x22, 0x67, 0x95, 0x8D, 0x75, 0x86, 0x1F, 0xDD, 0x9C, 0xD0, 0x8F, 0xB8, 0x96, 0xFE, + 0xF8, 0xF1, 0xB5, 0x2E, 0xD0, 0x95, 0xA6, 0x7B, 0x46, 0xBA, 0xB3, 0x43, 0x01, 0xFD, 0x7F, 0x25, + 0xB2, 0xDE, 0xA2, 0xAB, 0xE5, 0x5A, 0xE9, 0xDE, 0x1F, 0xCB, 0xB8, 0xBE, 0x8C, 0x5B, 0x96, 0x4A, + 0x5D, 0x34, 0x1F, 0x7A, 0xA1, 0xD4, 0xB1, 0x59, 0x97, 0x49, 0x83, 0x20, 0xDB, 0x22, 0x39, 0xE4, + 0x70, 0xD5, 0x29, 0xA5, 0x9C, 0xE0, 0xCB, 0xF0, 0x57, 0x54, 0x98, 0xDD, 0x5A, 0x6E, 0x1A, 0x73, + 0x2B, 0x29, 0x4D, 0xAB, 0x2A, 0x96, 0xD0, 0xB2, 0x6B, 0xCC, 0x03, 0x4D, 0xAD, 0x32, 0x9D, 0xCE, + 0xB4, 0x81, 0x64, 0x83, 0x2E, 0xF5, 0xDA, 0xAB, 0x02, 0xFA, 0xB6, 0x27, 0x88, 0x1D, 0x89, 0x5D, + 0xE4, 0xF0, 0x16, 0xDE, 0x27, 0xE9, 0x15, 0xB1, 0x1A, 0x1A, 0x79, 0x7C, 0xAA, 0x3E, 0x3D, 0x0D, + 0x2C, 0xDC, 0x1D, 0x66, 0xE6, 0xF2, 0xAE, 0xAA, 0xA3, 0x0D, 0x60, 0x8A, 0x6B, 0xDE, 0x44, 0xB3, + 0x9B, 0x71, 0xA9, 0x19, 0x4F, 0x04, 0x68, 0xF7, 0x60, 0x5F, 0x79, 0xDF, 0xA8, 0x4F, 0x1C, 0xF6, + 0x40, 0x5D, 0x1D, 0xE3, 0xC7, 0x42, 0x96, 0x17, 0x3D, 0x0E, 0xEF, 0x71, 0x25, 0x92, 0x2F, 0x32, + 0xF3, 0xC0, 0x89, 0xFB, 0x09, 0x0F, 0xAB, 0x13, 0xFB, 0x27, 0xFD, 0x9D, 0x56, 0xE2, 0xF9, 0xE4, + 0xE8, 0xA8, 0xED, 0xCA, 0x1F, 0xF9, 0x5C, 0x69, 0x32, 0x0C, 0xD9, 0xD7, 0x9D, 0x11, 0xE7, 0xB1, + 0x0F, 0xA8, 0xF4, 0xBD, 0x68, 0xE6, 0x99, 0x54, 0x32, 0xE7, 0x0B, 0xCF, 0xEA, 0xCB, 0x8F, 0xD1, + 0x8D, 0x72, 0xD5, 0x2F, 0x51, 0x8F, 0x18, 0x1B, 0x68, 0xF9, 0x8E, 0xAF, 0xB6, 0x8F, 0x9D, 0x91, + 0xA5, 0x84, 0xAB, 0xAA, 0xB7, 0xD7, 0x6F, 0x28, 0x4B, 0x20, 0x6F, 0x9E, 0xCD, 0xD9, 0x5E, 0x5A, + 0xE6, 0x19, 0x57, 0xF1, 0xEA, 0x18, 0x8A, 0x17, 0xB9, 0xBD, 0x71, 0xD8, 0x72, 0x35, 0x64, 0x8C, + 0x07, 0x1E, 0x4F, 0xA3, 0x44, 0x2B, 0x0F, 0x1C, 0x9F, 0x50, 0x64, 0x08, 0x53, 0x99, 0x9E, 0x0F, + 0x83, 0x0F, 0x7A, 0x0E, 0xAF, 0x03, 0x46, 0xA2, 0xCA, 0x3E, 0xE1, 0xDF, 0x78, 0x2D, 0x7C, 0x61, + 0x21, 0xF0, 0x47, 0xF7, 0xE2, 0x14, 0x2F, 0x6A, 0x91, 0xD5, 0x16, 0x06, 0x06, 0xB5, 0xD8, 0x30, + 0xB5, 0xF5, 0x93, 0x2A, 0xF5, 0xC1, 0x2D, 0xC9, 0x78, 0x53, 0xF5, 0xEA, 0xF7, 0x9E, 0x22, 0xD7, + 0x1B, 0x7A, 0x71, 0x3A, 0x4C, 0xA0, 0x5D, 0x33, 0xCF, 0xFD, 0x61, 0x45, 0x9D, 0x5D, 0x6C, 0x21, + 0xB7, 0xF2, 0x69, 0x65, 0xA5, 0x28, 0x4B, 0x97, 0x8A, 0x06, 0xA1, 0xD4, 0x57, 0xEB, 0xCD, 0x74, + 0xE0, 0x60, 0x0B, 0x8F, 0xDD, 0x1F, 0x8D, 0xC8, 0x30, 0x08, 0xDC, 0x0A, 0xAD, 0x09, 0xB4, 0x4E, + 0xC7, 0x77, 0xE6, 0xE6, 0x6D, 0x44, 0xAE, 0xF4, 0xAB, 0x08, 0x80, 0xF4, 0x27, 0xCC, 0xF9, 0xB8, + 0x32, 0x3B, 0xC5, 0x65, 0x1B, 0x15, 0x10, 0x1F, 0x8C, 0x16, 0x88, 0xB4, 0xA1, 0x67, 0x37, 0x41, + 0xF5, 0x23, 0x23, 0x30, 0xDB, 0x51, 0x78, 0x26, 0x41, 0x14, 0x79, 0x18, 0xB0, 0xC4, 0xA5, 0x01, + 0xE7, 0xA7, 0x94, 0x93, 0x74, 0x2A, 0xA6, 0x1F, 0x4E, 0x4F, 0xDB, 0x63, 0xFE, 0x93, 0x7E, 0x23, + 0xB6, 0x8F, 0xAB, 0xB6, 0x8F, 0xA7, 0xA7, 0xFD, 0x7E, 0x3B, 0x62, 0x3F, 0x5A, 0x73, 0x62, 0xF9, + 0x0C, 0x00, 0x56, 0xD3, 0xCF, 0x4F, 0xAC, 0x72, 0xC2, 0x42, 0xEC, 0x17, 0xE7, 0xDC, 0xFF, 0xC1, + 0x28, 0x2E, 0x3F, 0x68, 0x93, 0x0D, 0x98, 0x65, 0xDB, 0x9D, 0xF5, 0xF5, 0x8D, 0xA7, 0x1D, 0x9C, + 0x5E, 0x8C, 0x7F, 0xED, 0x34, 0x84, 0x2C, 0x9D, 0xE9, 0x7B, 0xA4, 0xB7, 0x0C, 0xC4, 0x6F, 0x71, + 0xDE, 0x75, 0xD8, 0x25, 0x5E, 0x98, 0x61, 0x3D, 0x7C, 0xF7, 0x2E, 0xB8, 0xC5, 0xE7, 0xFE, 0x36, + 0x3A, 0xA4, 0xB3, 0x49, 0x7A, 0x9B, 0xED, 0x5E, 0x67, 0x63, 0xDB, 0x42, 0x4A, 0xAA, 0x8B, 0xF6, + 0xC8, 0xFA, 0x92, 0x29, 0xE9, 0x31, 0x62, 0xD6, 0x53, 0x62, 0x56, 0xBB, 0x9D, 0xCD, 0xD5, 0x6E, + 0x77, 0xB5, 0xB3, 0xD9, 0xEE, 0x6E, 0xF5, 0x6C, 0xE4, 0xD8, 0x35, 0xDA, 0x1E, 0x46, 0x61, 0x2E, + 0x89, 0xB4, 0xA7, 0x48, 0xD3, 0x35, 0xBD, 0xA5, 0x21, 0xA7, 0xA7, 0xDB, 0x45, 0x6A, 0x9E, 0x3D, + 0xDB, 0xDE, 0xEE, 0x91, 0xC6, 0x11, 0x97, 0x2C, 0xAC, 0xC2, 0x7F, 0x6B, 0x26, 0x34, 0x4A, 0x69, + 0xD7, 0x2C, 0xB2, 0x64, 0xA2, 0x72, 0x53, 0x2C, 0x5D, 0x42, 0x76, 0x6C, 0xB5, 0x55, 0x89, 0xE3, + 0x0D, 0xD4, 0x2F, 0xF6, 0x36, 0xA9, 0xA8, 0x08, 0x6B, 0x2F, 0xF9, 0x3B, 0x07, 0x87, 0x1C, 0x4F, + 0x81, 0x40, 0xFE, 0x69, 0xAF, 0x6D, 0x65, 0x37, 0x6F, 0x69, 0x2D, 0x52, 0xAD, 0x79, 0x9F, 0x0E, + 0x1D, 0xCC, 0xFC, 0xD9, 0xF7, 0x86, 0xBE, 0x6E, 0x3C, 0xE9, 0x2A, 0xAC, 0x93, 0xEA, 0xCF, 0x54, + 0x59, 0x27, 0xC5, 0xDC, 0xEC, 0xC4, 0x00, 0xD0, 0x7D, 0x45, 0xB7, 0xB7, 0x50, 0x82, 0xEA, 0xFC, + 0x21, 0xC7, 0x0B, 0x16, 0x25, 0x0A, 0xF4, 0x30, 0x3A, 0xF0, 0x6D, 0xE9, 0x69, 0x94, 0xC6, 0x84, + 0xEB, 0x44, 0xE8, 0x37, 0xD9, 0x92, 0xCE, 0xAF, 0xAE, 0xB2, 0x67, 0xCC, 0x41, 0x39, 0xA6, 0xE2, + 0xEE, 0xF0, 0xF7, 0xCC, 0x3D, 0x9F, 0xA7, 0x98, 0x64, 0x25, 0x94, 0x2F, 0x60, 0x2D, 0xE6, 0x81, + 0x60, 0x17, 0xA9, 0x05, 0x52, 0x89, 0x86, 0x44, 0x80, 0xC7, 0xF4, 0xC6, 0xA9, 0xEB, 0x36, 0x2C, + 0x37, 0xE8, 0x69, 0x56, 0x56, 0x78, 0xF1, 0x25, 0x47, 0xB2, 0xA1, 0xF8, 0x29, 0x26, 0x70, 0xCF, + 0x2D, 0xDD, 0x82, 0x35, 0x6E, 0x6D, 0x2D, 0xAB, 0xF0, 0xD4, 0x07, 0x7B, 0xE6, 0x37, 0x51, 0x38, + 0x5C, 0x25, 0x94, 0x80, 0x5F, 0x16, 0x67, 0x27, 0x45, 0x3C, 0xCC, 0x9C, 0x9D, 0xA4, 0x02, 0x5B, + 0xFD, 0x81, 0xBC, 0xBF, 0x97, 0x0A, 0x39, 0x1B, 0x1E, 0xB0, 0xB9, 0x89, 0x98, 0x36, 0x1A, 0x14, + 0x21, 0xDA, 0x3A, 0x08, 0x05, 0x34, 0xBF, 0xB8, 0xD7, 0x51, 0x61, 0xA4, 0xEB, 0x7D, 0x5A, 0x6D, + 0x75, 0x37, 0xD1, 0xD4, 0xFC, 0xC8, 0x58, 0xBB, 0xEC, 0xC7, 0x1E, 0xE2, 0x04, 0x81, 0x88, 0x59, + 0x60, 0x49, 0x01, 0xA2, 0x04, 0xCD, 0xE9, 0xA9, 0x1A, 0x84, 0xFB, 0xD2, 0x03, 0xCB, 0x04, 0x56, + 0x57, 0xCD, 0xD2, 0x48, 0xED, 0x11, 0x16, 0xFC, 0xFE, 0xF6, 0x9A, 0xBD, 0xCB, 0x8D, 0x83, 0xB1, + 0xDA, 0x35, 0xDD, 0xF9, 0x8B, 0xB2, 0xFE, 0xF2, 0xEC, 0xED, 0xE5, 0xD1, 0xF1, 0xE1, 0xC9, 0xE9, + 0xFE, 0x1B, 0x63, 0x14, 0x22, 0x0A, 0x33, 0xD5, 0xC5, 0x6E, 0x25, 0xF4, 0x18, 0xEC, 0x56, 0xC5, + 0x81, 0xEF, 0x29, 0x79, 0x0B, 0x2B, 0xCB, 0xD2, 0x89, 0xA4, 0x32, 0x5A, 0x6D, 0x22, 0xF9, 0xF8, + 0xE7, 0x84, 0x5F, 0xA2, 0x01, 0xB0, 0x4C, 0xB9, 0x2E, 0x63, 0xB1, 0x28, 0x25, 0xB2, 0x95, 0x14, + 0xC8, 0x1A, 0xD9, 0x82, 0x15, 0x90, 0x5D, 0xFB, 0x94, 0x38, 0xD6, 0xC8, 0xFA, 0x16, 0xE6, 0x3C, + 0x68, 0x1A, 0x37, 0x16, 0x95, 0x19, 0xDC, 0xB4, 0x22, 0xF8, 0x33, 0x32, 0xDE, 0xBA, 0x25, 0x5C, + 0x70, 0xE6, 0x6D, 0x16, 0xCE, 0xBC, 0x8D, 0x64, 0xE6, 0x69, 0xE6, 0xC7, 0xF2, 0x26, 0x5E, 0xC5, + 0x69, 0xA7, 0x2C, 0x2A, 0xEA, 0xF8, 0xE7, 0x4C, 0x36, 0x53, 0xF0, 0x67, 0x9D, 0x5E, 0xB9, 0x93, + 0xCB, 0x2E, 0x85, 0x1A, 0x6B, 0x58, 0x28, 0xA8, 0x68, 0x60, 0x01, 0x6C, 0x17, 0xC1, 0x59, 0x45, + 0x6A, 0x2E, 0xE9, 0x69, 0x77, 0x34, 0x0F, 0x59, 0x22, 0x40, 0x9A, 0xB4, 0x74, 0xF9, 0x78, 0xAF, + 0x3E, 0xD0, 0x80, 0x5F, 0x9E, 0x9E, 0x5E, 0x1E, 0xED, 0xF7, 0x5F, 0x1B, 0xC3, 0x2E, 0x42, 0x9C, + 0x52, 0xA5, 0x23, 0xEE, 0x2A, 0xAC, 0x9A, 0x77, 0x59, 0xD2, 0x31, 0x56, 0x1F, 0xB2, 0xEF, 0x7C, + 0x51, 0xC4, 0x22, 0x67, 0xE8, 0xD4, 0xFA, 0xDD, 0x2F, 0x8A, 0x2A, 0xCC, 0x8C, 0xDE, 0x23, 0x0F, + 0x46, 0xFE, 0x58, 0xF4, 0x92, 0xB1, 0x90, 0xA6, 0xFB, 0xD2, 0xC7, 0x02, 0x94, 0xEF, 0x4F, 0x36, + 0x1C, 0x3F, 0xCD, 0xCA, 0xA4, 0xB0, 0xA7, 0xCA, 0x02, 0xA5, 0x76, 0xA8, 0x97, 0x76, 0x28, 0xBB, + 0x42, 0xFD, 0xB8, 0xE5, 0x23, 0x47, 0xE6, 0x8C, 0x05, 0x84, 0x0B, 0x5D, 0xEA, 0x35, 0x5A, 0xAA, + 0xCC, 0xED, 0x94, 0xB0, 0x21, 0x55, 0xB0, 0x0F, 0xD6, 0x41, 0xA1, 0xE1, 0xC8, 0xC3, 0x69, 0xB8, + 0x8A, 0xB3, 0x89, 0xFC, 0xFF, 0xA6, 0xDC, 0xF4, 0x71, 0xE8, 0x25, 0xE3, 0x40, 0x1E, 0xC6, 0xAA, + 0xE7, 0x13, 0xF8, 0x27, 0x1A, 0x8C, 0x9F, 0x4D, 0xB5, 0xFD, 0xAB, 0x69, 0x35, 0xDB, 0xE6, 0xDE, + 0xD2, 0x8C, 0x6F, 0xED, 0x45, 0xCF, 0x2C, 0xE5, 0xCD, 0x24, 0x3F, 0xCA, 0x76, 0xD3, 0xEE, 0x31, + 0xC0, 0xF7, 0x6D, 0xD9, 0x95, 0x12, 0xF4, 0xEF, 0x71, 0x70, 0x93, 0x10, 0x0C, 0x4A, 0x3C, 0x43, + 0x49, 0x7D, 0xE9, 0x96, 0x31, 0x6A, 0x1A, 0x17, 0x2E, 0x6D, 0x55, 0x76, 0xF4, 0x53, 0x00, 0xC5, + 0x9D, 0x8F, 0x1E, 0xE4, 0xF4, 0x62, 0x62, 0x8B, 0x04, 0xD3, 0x18, 0xFF, 0x70, 0xC4, 0x11, 0x01, + 0x34, 0xEA, 0x07, 0xBA, 0xC3, 0x1F, 0xBD, 0xD7, 0x57, 0xC0, 0xE5, 0x31, 0x25, 0x75, 0x63, 0xAA, + 0xD5, 0x5B, 0x84, 0xC6, 0x03, 0xED, 0x41, 0x5F, 0x64, 0x04, 0xA3, 0x43, 0xA1, 0xAB, 0x65, 0x13, + 0x35, 0xFB, 0xE1, 0x80, 0xF0, 0x98, 0x2A, 0xA7, 0x9A, 0xFC, 0x9A, 0x82, 0x45, 0x52, 0xAB, 0x8B, + 0x6A, 0xD3, 0xAA, 0x0E, 0x52, 0xC9, 0x1C, 0x58, 0x87, 0xCD, 0x3C, 0x1A, 0x56, 0xC9, 0xB0, 0x1F, + 0xAC, 0x2D, 0x48, 0xE6, 0xA5, 0xB2, 0x17, 0x81, 0xFD, 0xD4, 0x82, 0xD0, 0x96, 0x0A, 0x4C, 0x9A, + 0xDC, 0x4B, 0x06, 0xD9, 0xFF, 0x78, 0x7A, 0xF0, 0xF6, 0x0D, 0x03, 0x6A, 0xDE, 0xFF, 0x18, 0x05, + 0xFE, 0x90, 0xE5, 0xB6, 0x38, 0xA2, 0xC3, 0x90, 0x9A, 0x5E, 0x89, 0xCC, 0x48, 0x18, 0x43, 0x0B, + 0x9B, 0x26, 0x13, 0x80, 0xB5, 0x1E, 0x4C, 0xFE, 0xAD, 0xCE, 0x8E, 0x91, 0x3B, 0x2C, 0x29, 0x7D, + 0x41, 0x72, 0x35, 0xA6, 0xDC, 0x4B, 0xDB, 0xE5, 0x2A, 0xFD, 0x23, 0x91, 0xA7, 0xA7, 0xCD, 0x4C, + 0x8E, 0xB2, 0x05, 0x87, 0x37, 0x8F, 0xB4, 0x64, 0x0A, 0x65, 0x58, 0xB8, 0x02, 0xB3, 0x0A, 0x0F, + 0xD6, 0x93, 0xAA, 0x3B, 0xD9, 0x13, 0x93, 0x25, 0x09, 0xCA, 0x5C, 0xC4, 0xAD, 0x3E, 0x06, 0x75, + 0x5C, 0xE6, 0xE6, 0xA2, 0xEF, 0x7F, 0xFE, 0x5B, 0x27, 0x10, 0xBF, 0xD5, 0x6B, 0x3A, 0x99, 0xF3, + 0xC0, 0x25, 0xB6, 0x7E, 0x2F, 0x5D, 0xB7, 0x5C, 0x2A, 0x7E, 0xC5, 0x65, 0xA8, 0x97, 0xA5, 0x82, + 0x53, 0x36, 0x4A, 0xCB, 0x87, 0xAA, 0xA8, 0x99, 0xE5, 0x75, 0x5E, 0xB1, 0x7C, 0x96, 0x4F, 0xF1, + 0x43, 0xC2, 0x36, 0x36, 0xA3, 0xFF, 0x12, 0xDA, 0x57, 0xA3, 0xF9, 0xD4, 0xE6, 0xC7, 0x9E, 0x85, + 0x66, 0x01, 0xA0, 0x88, 0xE6, 0xCA, 0x7A, 0x7F, 0x6B, 0xE9, 0x7A, 0xBF, 0xDF, 0x9F, 0x5B, 0xF3, + 0x67, 0x58, 0xF4, 0x48, 0x2B, 0x82, 0x90, 0xBA, 0xF9, 0x17, 0x05, 0x1B, 0xE1, 0xAB, 0x8F, 0x44, + 0xF9, 0xC2, 0x0B, 0x86, 0x8D, 0xF8, 0x7A, 0x76, 0x25, 0xF9, 0x6B, 0xAD, 0xF6, 0x50, 0x7D, 0x68, + 0xCE, 0xBD, 0x2A, 0xD9, 0x68, 0x27, 0x0F, 0xCD, 0x78, 0x43, 0xBD, 0x2E, 0x5D, 0xDC, 0x95, 0x4B, + 0x8D, 0x56, 0xB5, 0xB0, 0x64, 0x01, 0x5A, 0xB0, 0x27, 0xC5, 0xE3, 0xF0, 0xC8, 0x7D, 0x31, 0x16, + 0x8F, 0x07, 0x98, 0xD1, 0x45, 0x1D, 0xCA, 0x5C, 0xF4, 0xD1, 0xF1, 0x19, 0x71, 0x95, 0x32, 0xD0, + 0x69, 0x00, 0xAB, 0x9B, 0x6C, 0xB3, 0xF6, 0x57, 0x77, 0xAD, 0x1D, 0xE3, 0xA5, 0xE5, 0x41, 0x73, + 0x87, 0xE8, 0x49, 0x15, 0xE4, 0xE1, 0x39, 0xEC, 0x84, 0x5B, 0x3C, 0x5D, 0x17, 0x6C, 0x5C, 0x6F, + 0x42, 0x35, 0x10, 0x93, 0xE7, 0x1A, 0xDB, 0xC3, 0xCD, 0xB2, 0xBC, 0x9A, 0xB4, 0xAA, 0x04, 0xBC, + 0xC0, 0x67, 0x6D, 0x6F, 0x8E, 0xD5, 0xB4, 0x2C, 0x59, 0x22, 0x13, 0xD9, 0x0A, 0xC2, 0xC5, 0x18, + 0x04, 0xAD, 0x9C, 0x03, 0x5F, 0x61, 0xD9, 0x85, 0xD5, 0xED, 0xBB, 0xAF, 0x6D, 0xD8, 0x05, 0x68, + 0x27, 0xF5, 0x16, 0xC8, 0xAD, 0xBB, 0x72, 0xB3, 0xB8, 0x92, 0x1F, 0x41, 0x66, 0x15, 0xB8, 0xC5, + 0x07, 0xB3, 0x48, 0x51, 0x15, 0x16, 0x03, 0xC0, 0x5F, 0x5F, 0x7E, 0x6E, 0x24, 0x49, 0xE6, 0x23, + 0x51, 0x7B, 0xEF, 0x7F, 0xF5, 0x2D, 0x8F, 0xFB, 0x1A, 0xE9, 0xB8, 0x07, 0x78, 0xC5, 0xB7, 0x51, + 0x79, 0xDB, 0x9E, 0x83, 0x4B, 0xF5, 0x4E, 0x2C, 0x15, 0x21, 0xEC, 0xA9, 0x72, 0x51, 0xA6, 0x6E, + 0xD0, 0xA5, 0xA2, 0xBC, 0x2C, 0xC2, 0x49, 0x1E, 0x0E, 0x29, 0x5F, 0x8F, 0x73, 0x31, 0xAF, 0x3E, + 0x10, 0x66, 0x66, 0xBD, 0x14, 0xF0, 0x38, 0xF1, 0x2C, 0x2D, 0xBD, 0xBF, 0x45, 0x78, 0x35, 0x9F, + 0xD6, 0x03, 0x60, 0x2E, 0x67, 0xF6, 0xEA, 0xC3, 0xA0, 0x37, 0x17, 0xD2, 0x42, 0xCE, 0x3F, 0x44, + 0xC7, 0xAB, 0xE0, 0xE6, 0xDC, 0x7F, 0x28, 0xB6, 0x57, 0x22, 0x81, 0x8F, 0x40, 0x01, 0x09, 0xDF, + 0x9F, 0xE4, 0x69, 0xBB, 0xEF, 0x4F, 0xFE, 0x1F, 0xBA, 0xC0, 0x1A, 0x6B, 0xBF, 0x23, 0x00 +}; ///main_js + +//To convert AP-Config\index.html to index_html[], run the Python index_html_zipper.py script in the Tools folder: +// cd Firmware\Tools +// python index_html_zipper.py + +static const uint8_t index_html[] PROGMEM = { + 0x1F, 0x8B, 0x08, 0x08, 0x3A, 0xEE, 0x9C, 0x67, 0x02, 0xFF, 0x69, 0x6E, 0x64, 0x65, 0x78, 0x2E, + 0x68, 0x74, 0x6D, 0x6C, 0x2E, 0x67, 0x7A, 0x69, 0x70, 0x00, 0xED, 0x7D, 0xDB, 0x72, 0xDB, 0xC8, + 0x92, 0xE0, 0xBB, 0xBF, 0xA2, 0x86, 0xB3, 0x33, 0x96, 0xE7, 0x88, 0x14, 0x49, 0x5D, 0x6C, 0xEB, + 0xD8, 0x8C, 0xD0, 0xD5, 0x56, 0x1C, 0xC9, 0xE6, 0x8A, 0xF2, 0x71, 0x77, 0x6F, 0xEC, 0x76, 0x80, + 0x40, 0x91, 0xC4, 0x31, 0x08, 0xA0, 0x81, 0x82, 0x2E, 0x3D, 0x31, 0x13, 0xE7, 0x33, 0x66, 0x3E, + 0x64, 0x7F, 0x60, 0x3F, 0xE5, 0x7C, 0xC9, 0x66, 0x66, 0x15, 0x40, 0x00, 0x04, 0x49, 0x00, 0xBC, + 0xAB, 0xE5, 0x8E, 0x96, 0x44, 0x10, 0x75, 0xCB, 0xCA, 0xCC, 0xCA, 0xCC, 0xCA, 0xCB, 0x87, 0x7F, + 0x3A, 0xFF, 0x7A, 0x76, 0xF7, 0x73, 0xFB, 0x82, 0x0D, 0xC4, 0xD0, 0x6A, 0xBD, 0xFA, 0x80, 0xBF, + 0x98, 0xA5, 0xD9, 0xFD, 0x8F, 0x15, 0x6E, 0x57, 0x5A, 0xAF, 0xE0, 0x09, 0xD7, 0x8C, 0xD6, 0x2B, + 0x06, 0xFF, 0x3E, 0x0C, 0xB9, 0xD0, 0x98, 0x3E, 0xD0, 0x3C, 0x9F, 0x8B, 0x8F, 0x95, 0x40, 0xF4, + 0xAA, 0xEF, 0x2A, 0x6C, 0x2F, 0xFE, 0xE5, 0x40, 0x08, 0xB7, 0xCA, 0x7F, 0x0B, 0xCC, 0xFB, 0x8F, + 0x95, 0x9F, 0xAA, 0xDF, 0x4E, 0xAA, 0x67, 0xCE, 0xD0, 0xD5, 0x84, 0xD9, 0xB5, 0x78, 0x85, 0xE9, + 0x8E, 0x2D, 0xB8, 0x0D, 0x2D, 0xAF, 0x2E, 0x3E, 0x72, 0xA3, 0xCF, 0x77, 0xF5, 0x81, 0xE7, 0x0C, + 0xF9, 0xC7, 0xC6, 0xA8, 0x13, 0x61, 0x0A, 0x8B, 0xB7, 0x3A, 0xAE, 0xE6, 0xFD, 0xB8, 0x0C, 0x6C, + 0x76, 0x7B, 0xF7, 0x17, 0xD6, 0xE1, 0x22, 0x70, 0x3F, 0xEC, 0xC9, 0x6F, 0x62, 0x43, 0xD9, 0x1A, + 0x34, 0xAD, 0xDC, 0x9B, 0xFC, 0xC1, 0x75, 0x3C, 0x51, 0xA1, 0x6F, 0xF0, 0x5F, 0x34, 0xCA, 0x83, + 0x69, 0x88, 0xC1, 0x47, 0x83, 0xDF, 0x9B, 0x3A, 0xAF, 0xD2, 0x87, 0x5D, 0x66, 0xDA, 0xA6, 0x30, + 0x35, 0xAB, 0xEA, 0xEB, 0x9A, 0x05, 0x03, 0xEF, 0xB2, 0xA1, 0xF6, 0x68, 0x0E, 0x83, 0xE1, 0xE8, + 0x41, 0xE0, 0x73, 0x8F, 0x3E, 0x69, 0x30, 0xE7, 0x8F, 0xF5, 0x5D, 0xE6, 0x0F, 0x3C, 0xD3, 0xFE, + 0x51, 0x15, 0x4E, 0xB5, 0x67, 0x8A, 0x8F, 0x4F, 0xDC, 0x1F, 0xCD, 0xD6, 0x82, 0x2F, 0x98, 0xC7, + 0xAD, 0x8F, 0x15, 0x5F, 0x3C, 0x59, 0xDC, 0x1F, 0x70, 0x2E, 0x2A, 0x6C, 0xE0, 0xF1, 0x1E, 0x3C, + 0xF1, 0xF4, 0xBD, 0xAE, 0xE3, 0x08, 0x5F, 0x78, 0x9A, 0x5B, 0x1B, 0x9A, 0x76, 0x4D, 0xF7, 0xFD, + 0x4A, 0xCE, 0x86, 0xF4, 0x34, 0xDE, 0xC0, 0xD7, 0x3D, 0xD3, 0x15, 0x0C, 0xBE, 0x93, 0x2F, 0xFC, + 0xED, 0xB7, 0x80, 0x7B, 0x4F, 0xD5, 0xFD, 0xDA, 0x51, 0xAD, 0x4E, 0x9D, 0xFF, 0x0D, 0x5E, 0xFD, + 0xB0, 0x27, 0x5F, 0x9B, 0xD0, 0x26, 0x39, 0x9B, 0x42, 0x0D, 0xBA, 0x81, 0x6D, 0xC0, 0x84, 0xC6, + 0xDB, 0xC5, 0x1B, 0xB6, 0xA2, 0x2D, 0xF8, 0x1F, 0x3B, 0x86, 0xA3, 0x07, 0x43, 0xD8, 0x85, 0x37, + 0x35, 0xC7, 0xDE, 0x79, 0xAD, 0x5B, 0xA6, 0xFE, 0xE3, 0xF5, 0x2E, 0x7B, 0x5D, 0x13, 0x4E, 0xBF, + 0x6F, 0xF1, 0x6A, 0x57, 0xD8, 0xF0, 0xB1, 0x17, 0xD8, 0xBA, 0x30, 0x1D, 0x9B, 0xED, 0xF0, 0x37, + 0xEC, 0xDF, 0xA3, 0xD6, 0xB2, 0x07, 0x58, 0x7E, 0xE0, 0x79, 0xD0, 0xC5, 0x9D, 0xE6, 0xF5, 0xB9, + 0xA8, 0xE9, 0x03, 0xD3, 0x32, 0xE0, 0xF3, 0xFF, 0xAA, 0xFF, 0xEF, 0x37, 0xAA, 0x9B, 0x33, 0x4B, + 0xF3, 0xFD, 0x9D, 0xD7, 0x26, 0xEC, 0x78, 0x55, 0xD7, 0x3C, 0x2E, 0xAA, 0x86, 0xF3, 0x60, 0xB3, + 0xD8, 0xE7, 0xC0, 0x7D, 0xFD, 0xE6, 0xCF, 0x51, 0xC7, 0xFF, 0xF1, 0x46, 0x4E, 0x37, 0x3D, 0x7B, + 0x04, 0xF6, 0x68, 0xF2, 0x35, 0x5F, 0x00, 0xC2, 0xEA, 0xD5, 0xBE, 0xE7, 0x04, 0x6E, 0x6A, 0x5A, + 0x03, 0x6E, 0xF6, 0x07, 0xE2, 0x98, 0xD5, 0xFF, 0x9C, 0x78, 0xEC, 0xDC, 0x73, 0xAF, 0x67, 0x39, + 0x0F, 0xC7, 0x6C, 0x60, 0x1A, 0x06, 0xB7, 0x93, 0xDF, 0x02, 0x04, 0x6D, 0xDF, 0xC4, 0x85, 0x1E, + 0xAB, 0x0E, 0x58, 0xBD, 0x76, 0xE0, 0x33, 0xAE, 0xF9, 0x3C, 0xF9, 0x66, 0xD7, 0xF1, 0x0C, 0xC0, + 0xBE, 0xAE, 0x23, 0x84, 0x33, 0x3C, 0x66, 0xBE, 0x63, 0x99, 0x06, 0x6B, 0xB8, 0x8F, 0xEC, 0x9F, + 0xF5, 0x3A, 0xFE, 0x17, 0x5B, 0xCA, 0xAB, 0xD1, 0x7C, 0x2D, 0xD3, 0x17, 0x9B, 0x3D, 0x5B, 0xFC, + 0xE7, 0x6A, 0x86, 0x61, 0xDA, 0xFD, 0xAA, 0x27, 0xE7, 0x74, 0x58, 0x77, 0x1F, 0xB3, 0x97, 0x23, + 0xBB, 0x05, 0xA2, 0xF0, 0x99, 0x30, 0x76, 0xB3, 0x9F, 0x0F, 0x52, 0x2B, 0x95, 0xDF, 0x1D, 0x33, + 0xDB, 0xB1, 0x53, 0x93, 0x1C, 0x02, 0xF6, 0x98, 0x76, 0xD5, 0xE2, 0x3D, 0x04, 0xC4, 0x84, 0x31, + 0xBB, 0x01, 0x2C, 0xC1, 0x3E, 0xEE, 0x01, 0xD2, 0xFA, 0xA9, 0x9E, 0x9D, 0x40, 0x00, 0xB1, 0xF2, + 0x04, 0x10, 0xE3, 0xB3, 0x35, 0x6D, 0xFC, 0xFA, 0xC2, 0xF3, 0x1C, 0x2F, 0xD5, 0xD2, 0x30, 0x7D, + 0xD7, 0xD2, 0x9E, 0x8E, 0x99, 0x7C, 0x25, 0x39, 0x2D, 0xDD, 0xB1, 0x1C, 0x98, 0xAF, 0xC7, 0x8D, + 0xE4, 0xF3, 0x1E, 0x30, 0xAF, 0xAA, 0x6F, 0xFE, 0x0E, 0x03, 0xFA, 0x43, 0xCD, 0xB2, 0xB8, 0x37, + 0x6D, 0xD8, 0x4E, 0xA0, 0xEB, 0x08, 0x8F, 0xE2, 0x03, 0xF7, 0x3D, 0x9E, 0xDE, 0xF8, 0x69, 0x43, + 0x47, 0xDF, 0x3F, 0x28, 0x94, 0xEA, 0x3A, 0x96, 0x31, 0x69, 0xFB, 0x1E, 0xAB, 0xD4, 0x3E, 0x35, + 0xAB, 0xC9, 0x1B, 0x81, 0xFF, 0x88, 0x3D, 0x1F, 0xB3, 0xFD, 0xFA, 0xBF, 0x4C, 0xEE, 0x55, 0xF6, + 0xD0, 0xAC, 0x4F, 0xEB, 0xB8, 0x39, 0x05, 0xAD, 0xC2, 0x1E, 0x0E, 0xA6, 0xF6, 0x70, 0x30, 0xB9, + 0x07, 0x4D, 0x08, 0x60, 0xBA, 0xA9, 0xC6, 0xAE, 0x13, 0x52, 0x8B, 0xD6, 0x05, 0x12, 0x08, 0x44, + 0x0A, 0xE0, 0xBF, 0x57, 0x4D, 0xDB, 0xE0, 0x8F, 0xC7, 0xAC, 0x51, 0xAF, 0xA7, 0x48, 0x42, 0x91, + 0x42, 0x63, 0x0C, 0x1A, 0x70, 0x28, 0x55, 0x15, 0x44, 0x8E, 0xEA, 0x19, 0xDF, 0xD2, 0x74, 0x85, + 0xE3, 0x02, 0x19, 0x25, 0x27, 0xAB, 0xD8, 0x9B, 0x64, 0x68, 0x1F, 0xF6, 0xE4, 0xD1, 0xFD, 0xEA, + 0x43, 0xD7, 0x31, 0x9E, 0x14, 0x8F, 0x37, 0xCC, 0x7B, 0xA6, 0x23, 0xDF, 0xFC, 0x58, 0xC1, 0x83, + 0x52, 0x03, 0x04, 0xF1, 0x2A, 0xCC, 0x34, 0x3E, 0x56, 0xD4, 0xF2, 0xAE, 0xE0, 0x71, 0x65, 0xC4, + 0x0D, 0xA9, 0x81, 0x66, 0x99, 0x7D, 0xFB, 0x63, 0x85, 0xE6, 0x5B, 0x09, 0x9B, 0xAB, 0xF7, 0x63, + 0xEF, 0xD2, 0xFB, 0xE6, 0xB0, 0x9F, 0xEE, 0xEE, 0xD2, 0xB4, 0xF8, 0x17, 0x38, 0xAD, 0x2B, 0xA3, + 0xA3, 0xE5, 0x54, 0x7E, 0xDB, 0xFC, 0xF5, 0x6C, 0x40, 0xAB, 0xE9, 0xD7, 0x5C, 0xBB, 0x5F, 0x81, + 0x81, 0xE0, 0xDC, 0x56, 0xDF, 0x31, 0x8B, 0xDF, 0x73, 0xAB, 0xD2, 0x02, 0x06, 0xED, 0x6A, 0x76, + 0xBC, 0xCF, 0x36, 0xF7, 0x74, 0x38, 0x18, 0x2A, 0x89, 0x81, 0x09, 0xB9, 0xE5, 0xCC, 0x08, 0xFD, + 0x60, 0x30, 0x04, 0xC3, 0xC7, 0x4A, 0x48, 0x0E, 0x8A, 0x1A, 0x2A, 0xAD, 0x3F, 0x1D, 0xBD, 0x05, + 0x18, 0x41, 0x9F, 0xB1, 0x55, 0xEE, 0xC1, 0x32, 0x15, 0x84, 0xE4, 0x9F, 0xD3, 0xA0, 0x95, 0xEA, + 0x98, 0x98, 0x4E, 0x7C, 0x57, 0x08, 0x09, 0x47, 0x7B, 0x28, 0xB7, 0x50, 0x02, 0xD9, 0xE3, 0x20, + 0x3A, 0x5D, 0xD9, 0x6D, 0xCF, 0x41, 0xC2, 0x8D, 0xC3, 0xB9, 0xDB, 0xBA, 0xC5, 0xEF, 0x04, 0xC0, + 0xE2, 0xC3, 0x5E, 0xB7, 0xF5, 0xA1, 0xEB, 0xD1, 0xFF, 0x28, 0x01, 0x49, 0x11, 0x86, 0x99, 0x3E, + 0xF0, 0x0B, 0x3C, 0x93, 0x11, 0x5C, 0xAC, 0x6D, 0x21, 0x47, 0x66, 0x0F, 0x9A, 0x29, 0x6A, 0xB5, + 0xDA, 0xAA, 0xA6, 0x8E, 0xE2, 0x9C, 0xC5, 0x05, 0xCF, 0x98, 0x39, 0x3B, 0x87, 0xCE, 0x26, 0x4C, + 0x7D, 0xA0, 0xF9, 0xC0, 0x9B, 0x1F, 0x18, 0xF5, 0xB1, 0x8A, 0xC9, 0xF6, 0x4C, 0x6F, 0xF8, 0x00, + 0x22, 0xC0, 0x37, 0xD7, 0x72, 0x34, 0x23, 0x7B, 0xD6, 0xE9, 0xF9, 0x5E, 0xAA, 0x36, 0x2C, 0x70, + 0x0D, 0x4D, 0x70, 0x60, 0x95, 0xB2, 0x55, 0x8D, 0x25, 0x37, 0x41, 0x2E, 0x24, 0xDC, 0x88, 0x9C, + 0x8B, 0x69, 0xBD, 0xCA, 0x24, 0x29, 0xC4, 0x63, 0x5C, 0x6A, 0xF4, 0x3E, 0x88, 0xA6, 0xC3, 0x6A, + 0xA3, 0x09, 0x32, 0x16, 0x12, 0x52, 0x44, 0x2F, 0x9E, 0xF8, 0x51, 0xF5, 0x51, 0x10, 0x8E, 0xD1, + 0x49, 0x42, 0x4A, 0xFE, 0x6E, 0x5E, 0x9A, 0x52, 0x54, 0x46, 0xF1, 0x2C, 0x36, 0x9B, 0xDC, 0x60, + 0x8E, 0x01, 0xB6, 0x31, 0x19, 0xB0, 0x43, 0x68, 0xD4, 0xD6, 0xFA, 0x3C, 0xE7, 0x82, 0x3C, 0xE7, + 0x61, 0x6C, 0x1F, 0xBB, 0x96, 0xA3, 0xFF, 0xF8, 0x73, 0xBC, 0x83, 0x19, 0x9D, 0xC8, 0xE3, 0x04, + 0xD9, 0x73, 0x8A, 0xD9, 0xE0, 0x3F, 0x5C, 0x7C, 0xB8, 0x73, 0xC7, 0x31, 0x5E, 0x01, 0x10, 0x0B, + 0x1F, 0xFF, 0x95, 0x7B, 0x3E, 0x70, 0xE8, 0xC9, 0x1C, 0xE1, 0xBE, 0x5E, 0xAB, 0x2B, 0x96, 0x80, + 0x88, 0x30, 0x36, 0xC6, 0xA8, 0xD7, 0xDF, 0xB9, 0x91, 0xBB, 0xD7, 0x5F, 0x2E, 0xCE, 0xAB, 0x97, + 0xEF, 0xDB, 0xB1, 0xD9, 0xC5, 0xC6, 0x61, 0x33, 0x06, 0x92, 0xE8, 0x76, 0x7A, 0x77, 0x75, 0x3E, + 0x79, 0x80, 0x73, 0x89, 0x92, 0xA7, 0x56, 0xC0, 0x05, 0x60, 0xE3, 0x80, 0x5D, 0x9D, 0xC3, 0xD1, + 0x0A, 0xFF, 0x72, 0x8E, 0xA1, 0x3B, 0x20, 0x36, 0x99, 0x36, 0xE0, 0xBA, 0x7F, 0x7D, 0xFD, 0x79, + 0xF2, 0x38, 0xD7, 0xD7, 0x83, 0xE3, 0xB1, 0x6E, 0x92, 0x5D, 0xF5, 0xB9, 0x63, 0x70, 0x90, 0x9A, + 0xAF, 0x35, 0x31, 0xB9, 0x9F, 0x03, 0x58, 0xFE, 0xFB, 0x7A, 0xF3, 0xFD, 0xC1, 0xDB, 0xF7, 0x6A, + 0x86, 0xBB, 0x79, 0xBB, 0x9D, 0x06, 0xE7, 0x6A, 0xA3, 0x7E, 0x58, 0x6B, 0xBC, 0x3B, 0xAC, 0x1F, + 0xBE, 0x3D, 0x6A, 0x14, 0xEC, 0xF8, 0xC4, 0x9A, 0x32, 0xDF, 0xC6, 0xE1, 0x11, 0xCC, 0xF8, 0x5D, + 0x38, 0x59, 0xB6, 0x73, 0xD2, 0x3E, 0x7B, 0x33, 0x0E, 0xCF, 0x5C, 0x88, 0x13, 0x83, 0xF5, 0xC5, + 0xD9, 0xC5, 0xE5, 0xE4, 0x41, 0xF1, 0xDB, 0x59, 0xD0, 0xE6, 0x3A, 0xEF, 0xFD, 0x34, 0x0D, 0x20, + 0xCD, 0x77, 0xF5, 0x66, 0xFD, 0xA8, 0x76, 0x78, 0xF4, 0x2E, 0x27, 0x3C, 0xB0, 0xC7, 0x9F, 0xA7, + 0xF4, 0x78, 0xF0, 0xB6, 0x71, 0xF4, 0xAE, 0x7E, 0x50, 0x3B, 0xA8, 0xEF, 0x17, 0xE8, 0xF1, 0x97, + 0x69, 0xB8, 0xF0, 0xEE, 0xE8, 0xE8, 0xE8, 0xB0, 0x76, 0xF0, 0xEE, 0x20, 0x7D, 0x18, 0xCF, 0x02, + 0x6C, 0x9C, 0xA5, 0x65, 0x7E, 0x1E, 0x78, 0x21, 0xE7, 0x18, 0x8A, 0x6A, 0x3D, 0x2D, 0xCC, 0x8C, + 0xB3, 0x3B, 0x3A, 0x47, 0xC6, 0xF8, 0xD1, 0x3F, 0x55, 0xAB, 0xAC, 0x1A, 0xFE, 0x63, 0x70, 0x62, + 0xF7, 0x40, 0x8C, 0x61, 0x67, 0x8E, 0xDD, 0x33, 0xFB, 0xB1, 0x2F, 0xAA, 0xD5, 0xD6, 0x38, 0x1F, + 0x53, 0xC3, 0x1B, 0xA0, 0x96, 0x81, 0x4E, 0xD4, 0xD7, 0xDC, 0x6A, 0x33, 0x83, 0x75, 0x7D, 0x90, + 0x7A, 0x47, 0x24, 0x50, 0x09, 0x9B, 0xC1, 0xFF, 0x55, 0xD7, 0x33, 0x61, 0x72, 0x4F, 0x6C, 0xA4, + 0x2B, 0x4B, 0xD6, 0xEB, 0xCA, 0x29, 0xC8, 0x19, 0x54, 0x98, 0x78, 0x72, 0x61, 0x19, 0xB2, 0x8B, + 0x0A, 0x83, 0x13, 0x4B, 0xAB, 0xCA, 0x16, 0x74, 0x8C, 0x58, 0x9A, 0xEB, 0xF3, 0x4A, 0xE6, 0x1E, + 0xC9, 0x57, 0x49, 0xAB, 0xFE, 0x58, 0xF9, 0xE7, 0xF0, 0xDD, 0x76, 0xB2, 0x77, 0xCD, 0x33, 0xB5, + 0x2A, 0x7F, 0x84, 0x0D, 0x30, 0x38, 0x9E, 0xA7, 0x9A, 0x05, 0xDD, 0xC9, 0xA7, 0x78, 0x74, 0x78, + 0x8E, 0xE5, 0x8F, 0xC6, 0x49, 0xB6, 0x6D, 0x65, 0x8E, 0x9A, 0x04, 0x60, 0xE0, 0x69, 0xA4, 0xF9, + 0x7F, 0x30, 0x13, 0x4B, 0x43, 0xA5, 0x7D, 0x74, 0x18, 0x92, 0x0A, 0x8F, 0xDA, 0x3C, 0xEB, 0x9A, + 0x2C, 0xA5, 0xE5, 0xE3, 0x29, 0x67, 0x66, 0xE1, 0x8C, 0x04, 0x48, 0x26, 0xC6, 0x4C, 0xDA, 0xA6, + 0x70, 0x19, 0x6C, 0xD8, 0x85, 0x7D, 0x52, 0xE4, 0x9A, 0x6F, 0x65, 0x89, 0x6E, 0x34, 0xCF, 0x60, + 0xF8, 0xA3, 0x8A, 0xF2, 0x76, 0x1A, 0x9F, 0xB2, 0x5A, 0xF4, 0x1C, 0x6F, 0xA8, 0x34, 0x77, 0xC0, + 0xD4, 0xE6, 0x04, 0xC8, 0xC5, 0xA0, 0x77, 0x3C, 0xF1, 0x05, 0x49, 0x78, 0xAA, 0x5F, 0x21, 0x14, + 0x42, 0x74, 0xFD, 0x2A, 0x90, 0x9D, 0xCE, 0x87, 0x64, 0x10, 0x93, 0xF2, 0xFB, 0xC4, 0x2E, 0x48, + 0xFB, 0x47, 0x2B, 0x1B, 0x88, 0x16, 0xDC, 0xE2, 0xBA, 0x60, 0x1A, 0x53, 0x1B, 0xC3, 0x00, 0x0D, + 0x40, 0xEB, 0xE6, 0x36, 0x83, 0xFE, 0xE1, 0x88, 0x66, 0x4A, 0x52, 0x05, 0x55, 0xDC, 0x81, 0xE7, + 0x9A, 0x08, 0x5F, 0xAC, 0xB1, 0x13, 0x21, 0xA5, 0xBC, 0xDD, 0xB8, 0xD4, 0xF4, 0x60, 0xC2, 0x01, + 0x1E, 0x00, 0x84, 0x7D, 0xEA, 0x98, 0x1B, 0xA3, 0xF7, 0x43, 0xBC, 0xD0, 0x07, 0x9A, 0xDD, 0xE7, + 0x3E, 0x43, 0x01, 0xCC, 0xD7, 0xEE, 0xE1, 0x95, 0x07, 0x1A, 0x0F, 0xD4, 0xD9, 0x5E, 0x8F, 0xA3, + 0x21, 0x28, 0x9A, 0x0C, 0xC8, 0x60, 0x51, 0x3F, 0xA0, 0x76, 0xD3, 0x7B, 0xAF, 0x3B, 0xD0, 0x26, + 0x89, 0x5C, 0xAF, 0xF1, 0x45, 0x17, 0x26, 0xE3, 0x73, 0xA3, 0x36, 0x05, 0xB6, 0x63, 0xE0, 0x23, + 0x54, 0x33, 0xED, 0x9E, 0x53, 0xD5, 0x4D, 0x4F, 0x87, 0x01, 0x05, 0x7F, 0x14, 0x11, 0x59, 0x0E, + 0x7D, 0xDC, 0xA9, 0x49, 0x4C, 0x2B, 0xC5, 0xBC, 0xB2, 0x51, 0x20, 0xC5, 0xB0, 0x72, 0xA0, 0x07, + 0xCA, 0x51, 0xAD, 0x19, 0x9B, 0x9F, 0xCD, 0x6A, 0x13, 0xDA, 0x6D, 0xC8, 0xE9, 0xA6, 0x82, 0xC2, + 0xB4, 0xDD, 0x40, 0x28, 0xF6, 0xE2, 0x69, 0x86, 0xE9, 0x54, 0x94, 0xAD, 0x55, 0xC1, 0xFF, 0x56, + 0x3E, 0xBB, 0xD7, 0x40, 0xEE, 0xF8, 0x58, 0xA9, 0xCF, 0xEA, 0xCE, 0xD2, 0xBA, 0xDC, 0x8A, 0x53, + 0x79, 0x9D, 0x74, 0xC1, 0x96, 0xDA, 0x78, 0x38, 0xB1, 0xE9, 0x8D, 0xB9, 0x40, 0xF9, 0x2C, 0x20, + 0xD9, 0x28, 0x0C, 0xC9, 0x86, 0x84, 0x64, 0xA3, 0xB9, 0x7F, 0x70, 0x78, 0xF4, 0xF6, 0xDD, 0xFB, + 0xFA, 0xE8, 0xAF, 0x17, 0xA8, 0x2A, 0xA8, 0x36, 0x0B, 0x43, 0xB5, 0x29, 0xA1, 0xFA, 0x02, 0x41, + 0x05, 0xC1, 0xFD, 0xC2, 0x10, 0xDC, 0x7F, 0x81, 0x60, 0x02, 0x82, 0x07, 0x85, 0x21, 0x78, 0xF0, + 0x02, 0xC1, 0x04, 0x04, 0x0F, 0x0B, 0x43, 0xF0, 0xF0, 0x05, 0x82, 0x09, 0x08, 0x1E, 0x15, 0x86, + 0xE0, 0xD1, 0x0B, 0x04, 0x13, 0x10, 0x7C, 0x5B, 0x18, 0x82, 0x6F, 0x17, 0x07, 0xC1, 0x05, 0x82, + 0x50, 0x4E, 0x13, 0xDE, 0x8F, 0xE6, 0x29, 0x8D, 0xF3, 0xA1, 0x1A, 0x1A, 0xBF, 0x6A, 0x51, 0x06, + 0xC9, 0x7D, 0xFA, 0xE3, 0x80, 0x7E, 0xD2, 0x40, 0xD4, 0x47, 0x24, 0xC3, 0x4D, 0x05, 0x0B, 0x76, + 0x7E, 0x3C, 0x1B, 0x04, 0x49, 0x7D, 0x0C, 0x07, 0x7D, 0x47, 0xC3, 0xBD, 0x2D, 0xB2, 0x89, 0x28, + 0xA8, 0x57, 0x12, 0x10, 0x51, 0xCA, 0x6A, 0x42, 0x83, 0x96, 0xBB, 0x32, 0xBD, 0x57, 0x37, 0xDD, + 0x80, 0xAE, 0xF8, 0xA2, 0xBE, 0x63, 0xD7, 0x7E, 0xB8, 0xBF, 0xEE, 0xB4, 0xBD, 0x9D, 0x48, 0x01, + 0xE3, 0x03, 0x9D, 0x91, 0x12, 0x74, 0x03, 0x2A, 0x0B, 0x9A, 0x58, 0x93, 0x83, 0xA9, 0xCB, 0xBE, + 0x29, 0xC3, 0x2D, 0x1E, 0x55, 0xC6, 0xED, 0x25, 0xEA, 0xB6, 0xF7, 0x30, 0x07, 0x75, 0x29, 0xEB, + 0x46, 0xD2, 0x4E, 0x41, 0xD7, 0x38, 0xC2, 0xA6, 0x1B, 0x03, 0x85, 0x3D, 0x95, 0x49, 0x06, 0x90, + 0x4C, 0x4C, 0x9C, 0xAE, 0xBF, 0xD2, 0x95, 0xAD, 0x7D, 0x86, 0xFE, 0x06, 0x63, 0xE3, 0xEC, 0xBC, + 0xA9, 0xA8, 0x9B, 0x0A, 0xF5, 0x20, 0xDB, 0x58, 0xB0, 0x24, 0xCD, 0x3A, 0xA6, 0x5D, 0xCB, 0x49, + 0xA4, 0x55, 0x61, 0x54, 0xA5, 0x7B, 0x9A, 0x2E, 0x1C, 0x58, 0xB9, 0xC1, 0x7B, 0x5A, 0x60, 0x09, + 0x7F, 0x96, 0xDA, 0xBA, 0x14, 0xD5, 0x75, 0x16, 0x47, 0xCA, 0x8F, 0xD5, 0x5E, 0x0C, 0xFA, 0x37, + 0x7E, 0x7F, 0x61, 0x08, 0x9D, 0x65, 0xCF, 0xC9, 0x78, 0x35, 0x65, 0xC2, 0xFB, 0xF4, 0xA5, 0xD3, + 0x59, 0xA5, 0xFD, 0x6E, 0x28, 0x80, 0x6F, 0xC6, 0x8D, 0x78, 0x0B, 0xB6, 0xD8, 0xE1, 0x7A, 0x4A, + 0x99, 0xEB, 0x62, 0x0D, 0xB3, 0xE1, 0x1E, 0x83, 0x54, 0xCA, 0x50, 0xD7, 0xB7, 0x7D, 0x7F, 0xCD, + 0x56, 0xBA, 0xA4, 0x81, 0x6E, 0xEA, 0x5A, 0x26, 0x5B, 0xE7, 0x98, 0x2B, 0xAA, 0x8D, 0xA9, 0x26, + 0x3A, 0xBA, 0xEC, 0xE2, 0x9A, 0x1F, 0x78, 0x44, 0xE3, 0xB7, 0x9A, 0xE0, 0x57, 0x78, 0xD2, 0x4C, + 0xA1, 0xC8, 0x9B, 0xD1, 0xEB, 0x0C, 0xDF, 0x3F, 0xCE, 0x75, 0xDA, 0x4D, 0x67, 0xC1, 0x13, 0xCE, + 0xC6, 0x26, 0xF1, 0xC3, 0x46, 0x13, 0x49, 0x7A, 0x9F, 0x8D, 0x18, 0x7A, 0x1E, 0x7E, 0x11, 0x3B, + 0xFF, 0x53, 0x0B, 0xFC, 0xFC, 0x7B, 0xE2, 0x26, 0x32, 0x7E, 0xD2, 0x5F, 0xD9, 0x33, 0x3B, 0xC6, + 0x7F, 0x9F, 0x7F, 0x3F, 0xCE, 0xF5, 0xDE, 0x02, 0x99, 0x6A, 0x8A, 0xB9, 0xDE, 0x0D, 0x38, 0xB3, + 0x83, 0x61, 0x97, 0x7B, 0xCC, 0xE9, 0x31, 0xF2, 0xC8, 0x00, 0xFC, 0xF5, 0xD1, 0x50, 0x68, 0x39, + 0xBA, 0x44, 0xE6, 0x7F, 0xFC, 0xFD, 0xBF, 0x7A, 0xE6, 0x23, 0xF7, 0xFF, 0xF1, 0xF7, 0xFF, 0x66, + 0x2E, 0xBC, 0xE8, 0x73, 0x40, 0x5B, 0xA3, 0xC6, 0x4E, 0xEC, 0x27, 0x31, 0x30, 0xED, 0x3E, 0xD3, + 0xBA, 0xCE, 0x3D, 0x67, 0x07, 0x9F, 0x7F, 0x07, 0x99, 0xF2, 0x09, 0xB0, 0x06, 0x4D, 0x97, 0xA3, + 0x6B, 0x36, 0x78, 0xB9, 0xCF, 0x7D, 0xEA, 0x09, 0xE8, 0x6D, 0x8F, 0x7A, 0xEE, 0xA3, 0x13, 0x03, + 0xBA, 0xDE, 0xE8, 0x1E, 0x87, 0xA5, 0xE9, 0x26, 0xF7, 0x6B, 0xEC, 0x8B, 0x03, 0x48, 0xC0, 0x70, + 0x46, 0x31, 0x40, 0x33, 0x0F, 0x2F, 0x97, 0x4D, 0x9F, 0xFC, 0xB0, 0x3C, 0xF2, 0xC0, 0x42, 0xD6, + 0xDF, 0x80, 0xC1, 0xC8, 0x92, 0x69, 0xDA, 0xEC, 0x14, 0x6F, 0xF6, 0x87, 0x8E, 0xC1, 0x6B, 0xEC, + 0x5C, 0x9E, 0x04, 0xC7, 0x38, 0x99, 0x1A, 0xBB, 0x36, 0x87, 0x26, 0xBA, 0xCF, 0xD4, 0xEA, 0xF5, + 0x7A, 0xA3, 0xD9, 0xA4, 0x76, 0x75, 0xF8, 0x22, 0xC7, 0xB6, 0x2F, 0xF5, 0xB8, 0xC8, 0x7B, 0x6C, + 0x8C, 0xDE, 0x9B, 0x21, 0x0E, 0xE6, 0x38, 0x63, 0x26, 0x50, 0x86, 0x14, 0x52, 0x0F, 0xCB, 0x10, + 0x46, 0x5C, 0x92, 0x94, 0x28, 0x94, 0x29, 0x4B, 0xC6, 0xAE, 0x09, 0xC6, 0xE9, 0x27, 0xC7, 0x30, + 0x6E, 0x76, 0xD3, 0xB2, 0x02, 0xE6, 0x2C, 0xC9, 0x2F, 0x36, 0x6E, 0xE2, 0x3E, 0x9D, 0x00, 0x84, + 0x17, 0x64, 0xB8, 0x9E, 0x3A, 0x0C, 0xC2, 0x87, 0x2D, 0xC7, 0xFB, 0xB0, 0x07, 0xBF, 0x68, 0xBC, + 0xCD, 0x60, 0x49, 0x1D, 0xAE, 0x57, 0x5A, 0x1D, 0xA2, 0x4F, 0x9F, 0x75, 0xB9, 0x78, 0xE0, 0x40, + 0x22, 0xB1, 0x77, 0xFC, 0x0D, 0xE3, 0x37, 0x53, 0x66, 0x5A, 0x03, 0x4E, 0x00, 0x64, 0x2F, 0xB1, + 0xCC, 0x44, 0x2F, 0x46, 0x60, 0x03, 0x36, 0x30, 0x01, 0x20, 0x76, 0x68, 0x2A, 0xB2, 0xD8, 0x04, + 0xDE, 0xBB, 0xC0, 0xAB, 0xC0, 0x7F, 0x7A, 0x81, 0x25, 0xF9, 0x83, 0xD0, 0x7E, 0x10, 0x8B, 0x4A, + 0xBC, 0xCC, 0xEF, 0xD1, 0x63, 0xAA, 0xC7, 0x1F, 0xA2, 0x19, 0x68, 0xBA, 0xE7, 0xF8, 0xF0, 0x0B, + 0x78, 0x13, 0xBC, 0x0D, 0x2F, 0xDE, 0xF3, 0x27, 0xB6, 0xD3, 0x3C, 0xF8, 0x13, 0x1B, 0x38, 0x81, + 0xE7, 0xBF, 0x59, 0x04, 0x77, 0x8A, 0xD8, 0x51, 0x03, 0xDF, 0x79, 0xD7, 0x78, 0x7F, 0x14, 0x0E, + 0x0F, 0x1C, 0x89, 0x40, 0x9E, 0x1B, 0x9A, 0x31, 0xF7, 0xAD, 0x17, 0xA6, 0xB4, 0x50, 0xA6, 0x44, + 0x14, 0x54, 0x92, 0x2B, 0x41, 0xDB, 0x05, 0xB0, 0xA5, 0x12, 0x5F, 0xCF, 0x54, 0x65, 0xC9, 0x47, + 0xE6, 0xC9, 0xD6, 0x86, 0xA6, 0x7E, 0x03, 0xA8, 0x68, 0x9D, 0x7B, 0x8E, 0x2B, 0x25, 0xCE, 0x5C, + 0x86, 0x8F, 0x78, 0xD3, 0x4A, 0xEB, 0x5C, 0x7E, 0x62, 0xF4, 0x31, 0x87, 0xAD, 0x42, 0xAA, 0x6E, + 0xCA, 0x56, 0x94, 0xE8, 0x6A, 0x6C, 0x5E, 0xC9, 0xCD, 0x32, 0xD4, 0x2C, 0xE5, 0x6E, 0xCD, 0x00, + 0x9E, 0xE3, 0x92, 0x88, 0x31, 0xBA, 0x67, 0x6B, 0x3B, 0x9E, 0xC0, 0xA8, 0x84, 0x0F, 0x7B, 0xF2, + 0xAB, 0x42, 0xED, 0x61, 0xB8, 0x8E, 0x20, 0xF1, 0x07, 0xA8, 0xA8, 0x54, 0x0F, 0xFB, 0x30, 0x03, + 0x6E, 0x80, 0xE0, 0x03, 0x5A, 0x85, 0x5D, 0xAA, 0x87, 0x83, 0x4A, 0xEB, 0x24, 0x10, 0xCE, 0xD0, + 0x11, 0xE6, 0x7D, 0xB9, 0x55, 0x1C, 0xE2, 0x61, 0xA0, 0x95, 0x6A, 0x7A, 0x04, 0x83, 0x9B, 0x5E, + 0xD7, 0xF1, 0x6C, 0xCE, 0x1A, 0xFD, 0x52, 0x5D, 0xBC, 0x8D, 0x75, 0xD1, 0x2C, 0xD7, 0xC5, 0xBB, + 0x58, 0x17, 0x07, 0xE5, 0xBA, 0x78, 0x5F, 0x69, 0x7D, 0xF7, 0x4C, 0x5F, 0x94, 0x6A, 0xDC, 0x00, + 0x3C, 0x3A, 0x35, 0x7F, 0xE4, 0x80, 0x3E, 0xB0, 0x4D, 0x42, 0xF3, 0xD6, 0x8A, 0x7C, 0x0E, 0x4E, + 0x8C, 0xBF, 0x05, 0xBE, 0x08, 0x8F, 0x45, 0xC1, 0x3D, 0x5B, 0xB3, 0x98, 0x66, 0xF5, 0x1D, 0xCF, + 0x14, 0x83, 0x21, 0x1E, 0x30, 0x43, 0x4D, 0xE8, 0x03, 0xFA, 0x1E, 0x34, 0x5D, 0x69, 0x3B, 0xD1, + 0x5C, 0xD7, 0x32, 0x95, 0x54, 0xCF, 0xED, 0x7B, 0xD3, 0x73, 0x6C, 0x1C, 0x57, 0x1D, 0xB5, 0xCA, + 0x4B, 0x81, 0x99, 0x43, 0xD7, 0x83, 0xF3, 0x4C, 0xF6, 0xED, 0x71, 0x9D, 0x03, 0xFA, 0x79, 0xAF, + 0x7D, 0x39, 0x8C, 0x0B, 0xCA, 0xAA, 0xEC, 0x00, 0x0E, 0xE1, 0xF8, 0x79, 0xAD, 0x9C, 0x1E, 0x02, + 0x74, 0x22, 0x70, 0xEE, 0x4D, 0x03, 0x9D, 0x13, 0xE0, 0xDC, 0xF3, 0xE0, 0x54, 0xD6, 0xF5, 0x80, + 0xCE, 0xC9, 0xD0, 0xF9, 0x1B, 0xC3, 0x04, 0x80, 0x4D, 0xD7, 0xD0, 0x03, 0x93, 0x46, 0x8C, 0x0F, + 0x84, 0x53, 0x07, 0x38, 0x05, 0xB6, 0x1F, 0x98, 0x44, 0xBF, 0x0C, 0xE0, 0x23, 0x90, 0x1F, 0xD0, + 0x29, 0x4A, 0x1C, 0x89, 0x1A, 0xF4, 0xE1, 0x6D, 0x7B, 0xD2, 0x92, 0xF0, 0x4C, 0xB6, 0x60, 0xE3, + 0x2C, 0x74, 0x0C, 0x42, 0x17, 0x0B, 0xD0, 0x0A, 0xF0, 0x34, 0xC6, 0x03, 0x1E, 0xCE, 0x79, 0x98, + 0x7B, 0x34, 0x1E, 0xA8, 0x34, 0xD8, 0x3B, 0x68, 0x21, 0x52, 0x80, 0x88, 0x66, 0x29, 0xE7, 0xAD, + 0x3F, 0xC5, 0xD4, 0x8A, 0x90, 0xA7, 0x6C, 0x92, 0x7F, 0x44, 0xEC, 0x3C, 0x8A, 0xB3, 0xD2, 0x0E, + 0xB7, 0x7D, 0xC7, 0xBB, 0x0C, 0xD0, 0xEB, 0xB2, 0xB4, 0x35, 0x36, 0xCB, 0x4B, 0x6F, 0xE9, 0x66, + 0xF6, 0xA1, 0x69, 0x5F, 0x58, 0xFC, 0xBE, 0x92, 0x29, 0x15, 0x1C, 0x8D, 0x59, 0xD5, 0x6F, 0x60, + 0x5B, 0x3B, 0x7F, 0x65, 0xD8, 0x84, 0xD0, 0xE0, 0x78, 0xC5, 0xF6, 0x49, 0x18, 0x1F, 0x63, 0xE2, + 0x18, 0x0F, 0x27, 0x80, 0x78, 0x66, 0x70, 0x8C, 0x19, 0xF1, 0x09, 0x59, 0x35, 0x69, 0x21, 0xF2, + 0x81, 0x04, 0x2C, 0xCB, 0x14, 0x64, 0xBA, 0xEC, 0x72, 0x94, 0x52, 0x0D, 0x89, 0x92, 0xA0, 0x5D, + 0x6F, 0xB0, 0xE9, 0xB2, 0xF8, 0x55, 0x84, 0xDA, 0xA9, 0x22, 0x57, 0x11, 0x53, 0x64, 0x35, 0x25, + 0xA6, 0x29, 0xA4, 0xC8, 0x75, 0x11, 0xA1, 0x5E, 0x5E, 0xC2, 0x25, 0xC4, 0xB2, 0x11, 0xFF, 0xEC, + 0xCB, 0xD7, 0x22, 0x78, 0x7F, 0xB6, 0xF7, 0xA5, 0xBE, 0x2E, 0x7C, 0x1F, 0xE1, 0x33, 0xFA, 0xB8, + 0x69, 0x96, 0x8C, 0x40, 0xC9, 0x44, 0xEE, 0x11, 0x03, 0x3D, 0x32, 0x4E, 0x41, 0x37, 0x42, 0xAA, + 0x08, 0xFD, 0xBD, 0x91, 0xE5, 0x36, 0xEB, 0xA9, 0xC7, 0xB7, 0x2F, 0xF4, 0x30, 0x9B, 0x1E, 0x10, + 0x57, 0xF2, 0x92, 0x03, 0xBC, 0xBB, 0x0E, 0x6A, 0x90, 0xB6, 0x67, 0xDB, 0x47, 0x4C, 0x21, 0xDE, + 0x78, 0x36, 0xE0, 0xFA, 0x8F, 0x53, 0xE7, 0x91, 0xFB, 0x39, 0x69, 0x22, 0xD1, 0x1A, 0x1A, 0x9D, + 0x25, 0x3E, 0xE7, 0xD1, 0x3E, 0x16, 0x29, 0x75, 0xDD, 0xC5, 0x05, 0x15, 0x10, 0x31, 0x74, 0xCD, + 0x25, 0x11, 0x05, 0xE4, 0x09, 0x98, 0xA8, 0x0A, 0xC1, 0x05, 0x99, 0x43, 0xBE, 0x83, 0xA2, 0x8D, + 0xA4, 0x0D, 0x38, 0x08, 0x3C, 0x07, 0x04, 0x18, 0xA0, 0x01, 0xD3, 0xB5, 0xF8, 0x88, 0x76, 0x22, + 0xCB, 0x43, 0xF4, 0x55, 0x72, 0xC1, 0x20, 0x23, 0x39, 0x43, 0x1E, 0x97, 0x72, 0x7C, 0x66, 0x98, + 0xBA, 0x40, 0x71, 0x0A, 0x85, 0x20, 0x9B, 0x03, 0x9D, 0xA1, 0x57, 0x69, 0xE0, 0xA1, 0x44, 0x06, + 0xD3, 0xE0, 0x1E, 0x46, 0xAA, 0x24, 0xBB, 0xA1, 0xF9, 0x90, 0x00, 0x8B, 0xA4, 0x28, 0x05, 0x55, + 0x69, 0x15, 0x91, 0x42, 0x54, 0xF2, 0x65, 0x72, 0x3F, 0x95, 0x56, 0xDC, 0xB4, 0x60, 0x06, 0x8B, + 0x41, 0x89, 0x4E, 0x0C, 0x1C, 0x9F, 0x47, 0x4B, 0x03, 0x91, 0x10, 0x3A, 0x09, 0x05, 0xB9, 0xA1, + 0x5C, 0x6A, 0x97, 0xE3, 0x67, 0x62, 0x03, 0x46, 0xE0, 0xE1, 0xDF, 0x91, 0x5C, 0xA5, 0x6B, 0x96, + 0x1E, 0x44, 0xEB, 0x8B, 0x58, 0xC3, 0xA7, 0x76, 0x67, 0x97, 0x75, 0x4E, 0x4F, 0xE0, 0xE7, 0xA7, + 0xEB, 0xAF, 0x5F, 0x4E, 0x3A, 0xF0, 0xC7, 0x29, 0x37, 0xCF, 0x9D, 0x60, 0x97, 0x98, 0xC4, 0x27, + 0xCD, 0x32, 0x2D, 0xEE, 0x6C, 0xA2, 0x6F, 0x6A, 0xAE, 0xD3, 0x40, 0x47, 0xD4, 0x4F, 0xDC, 0xD2, + 0xCE, 0x26, 0x81, 0xB1, 0xF6, 0x8A, 0xF9, 0x4B, 0xDA, 0x08, 0xBA, 0x8F, 0x49, 0x72, 0x00, 0x10, + 0x56, 0x5A, 0xF0, 0x63, 0xEF, 0x7F, 0xFE, 0xD2, 0xE9, 0xCC, 0x26, 0x0D, 0xC9, 0x70, 0xC6, 0xC7, + 0xA0, 0xE7, 0xE1, 0x05, 0x1C, 0x3D, 0x82, 0x59, 0x47, 0x8E, 0x1D, 0x92, 0xFF, 0x64, 0x8F, 0xBD, + 0x0D, 0x40, 0x42, 0x14, 0x03, 0xBD, 0x18, 0x7E, 0xAE, 0x18, 0x42, 0x72, 0xE0, 0xAD, 0xC0, 0x23, + 0x49, 0x6A, 0x80, 0x4B, 0xF2, 0x8F, 0x55, 0xA3, 0x52, 0x38, 0xFC, 0x36, 0xC0, 0x4A, 0xB2, 0xA8, + 0x4A, 0x4B, 0xFE, 0x5E, 0x31, 0xA4, 0xC2, 0xC1, 0xB7, 0x02, 0xA9, 0x24, 0x53, 0x07, 0xA4, 0x92, + 0x7F, 0xAC, 0x1A, 0xA9, 0xC2, 0xE1, 0x0B, 0xC3, 0xCA, 0xCD, 0xEE, 0x70, 0x96, 0x3C, 0x55, 0x04, + 0xF0, 0xE8, 0xFC, 0x30, 0x2F, 0xC0, 0xB9, 0x8D, 0xA2, 0xC8, 0x17, 0xE1, 0x99, 0xEE, 0x99, 0x65, + 0x62, 0xC4, 0x77, 0xEB, 0x82, 0x1E, 0xB1, 0x2F, 0x77, 0xB7, 0x57, 0x6D, 0x26, 0x1F, 0x2E, 0x17, + 0xEA, 0x19, 0x73, 0x58, 0x91, 0x64, 0xF6, 0x8D, 0xA4, 0x15, 0x10, 0xC9, 0xE4, 0x62, 0x75, 0x1A, + 0x1D, 0x45, 0x16, 0x34, 0x20, 0x63, 0x54, 0x33, 0x08, 0x39, 0x20, 0x9D, 0xC9, 0x0C, 0x2A, 0x38, + 0x92, 0x14, 0x53, 0x34, 0x90, 0x46, 0x7C, 0x12, 0x5D, 0x7C, 0xEE, 0x61, 0xF0, 0x4D, 0x8D, 0xFD, + 0xEC, 0x04, 0x5E, 0x18, 0x89, 0x33, 0x0C, 0x7C, 0x81, 0xEA, 0xCC, 0x83, 0x89, 0x37, 0xDD, 0x32, + 0x6C, 0xD8, 0x43, 0xA7, 0x33, 0xA6, 0x09, 0x86, 0xD7, 0x82, 0xC2, 0x1C, 0xF2, 0xB8, 0x10, 0x73, + 0x6E, 0xFA, 0x08, 0x81, 0x2D, 0x8B, 0xA0, 0xC1, 0xAD, 0xB3, 0x47, 0x9B, 0x16, 0xBA, 0xC9, 0x64, + 0x06, 0x7C, 0xE5, 0xD3, 0x7C, 0x72, 0xEB, 0xBF, 0x69, 0x79, 0x3F, 0x36, 0x8D, 0x5F, 0xCF, 0x60, + 0x6B, 0xB8, 0xF7, 0xD9, 0xF1, 0x73, 0x28, 0xA7, 0x33, 0xDD, 0x31, 0x0F, 0xC7, 0x14, 0x68, 0xD9, + 0xFD, 0xCC, 0x9E, 0x71, 0xFC, 0xE3, 0x7C, 0xB7, 0x5D, 0x93, 0xBC, 0x32, 0x8F, 0x8A, 0x5E, 0x5D, + 0xCD, 0xF0, 0xCC, 0x9C, 0x00, 0xA4, 0xDC, 0xF7, 0x56, 0xD9, 0xED, 0x97, 0x7F, 0x77, 0xB5, 0x6A, + 0xD4, 0x69, 0x27, 0x52, 0x5D, 0xAD, 0x1E, 0x75, 0x70, 0xFC, 0x55, 0xA3, 0xCE, 0x4C, 0xCB, 0xC1, + 0x04, 0x30, 0xCD, 0x81, 0x3C, 0xD8, 0xFE, 0xF9, 0x21, 0x0F, 0x9C, 0x28, 0xDE, 0x3A, 0x91, 0x07, + 0xC7, 0xDF, 0x78, 0xBE, 0x43, 0x40, 0x9A, 0x03, 0x75, 0xB0, 0xFD, 0xF3, 0x44, 0x9D, 0xF6, 0xF7, + 0x75, 0x23, 0x0F, 0x6B, 0x7F, 0xDF, 0x0A, 0xFC, 0x01, 0x48, 0xCD, 0x89, 0x41, 0xED, 0xEF, 0xCF, + 0x09, 0x87, 0x6E, 0x9C, 0xC0, 0x16, 0x6D, 0xC7, 0xB4, 0x45, 0xA5, 0x0C, 0x86, 0x50, 0xF3, 0x1C, + 0x47, 0x13, 0xF4, 0xBF, 0xC1, 0xE8, 0x11, 0x03, 0x42, 0x29, 0xE4, 0x18, 0xB5, 0x7F, 0x9E, 0xA8, + 0x31, 0x27, 0x7B, 0x99, 0x78, 0xA9, 0x94, 0x1F, 0x79, 0x66, 0xBF, 0x55, 0x9E, 0xFD, 0xBC, 0x95, + 0xB8, 0xBD, 0x12, 0xFC, 0x2A, 0xCB, 0x7E, 0xE2, 0x3D, 0x6C, 0x12, 0x8E, 0x45, 0x16, 0x85, 0x46, + 0x5E, 0x7B, 0x4E, 0x6E, 0x13, 0x43, 0x7C, 0xF9, 0x77, 0x98, 0xF9, 0x72, 0x68, 0x8A, 0x4F, 0x9F, + 0x4E, 0x2A, 0xAD, 0xF0, 0x03, 0x83, 0x4F, 0xA8, 0x74, 0x4B, 0xD6, 0x9C, 0x6F, 0xF7, 0xE7, 0xB1, + 0x37, 0x4C, 0x9A, 0xD0, 0xCC, 0xFD, 0x0C, 0x6C, 0xEA, 0x91, 0x1B, 0xAB, 0x8E, 0x66, 0xA2, 0xEB, + 0x1C, 0x9D, 0xC0, 0x83, 0x09, 0xEA, 0x7E, 0x0B, 0x4C, 0xF4, 0x90, 0x81, 0xBF, 0xFA, 0x81, 0xA5, + 0x79, 0xA3, 0x4B, 0x12, 0x95, 0x55, 0x8D, 0x8C, 0x14, 0x74, 0x0B, 0x83, 0x1E, 0xA6, 0x6C, 0x07, + 0xE1, 0x3B, 0x94, 0x01, 0x6F, 0x6F, 0x98, 0xAF, 0xB2, 0x89, 0xE0, 0xF7, 0xB2, 0x4B, 0xF8, 0x65, + 0x33, 0x0D, 0x9D, 0xC6, 0x30, 0x29, 0xAB, 0x66, 0x59, 0x4F, 0x2A, 0x70, 0x4A, 0x5D, 0x16, 0x69, + 0x1E, 0xF7, 0x05, 0xD3, 0xEE, 0x35, 0x93, 0x12, 0xF6, 0xA6, 0x6D, 0x22, 0x19, 0x86, 0x8C, 0x0D, + 0x0F, 0xA9, 0x9A, 0xFC, 0x3D, 0x46, 0x33, 0x5D, 0x3A, 0x1E, 0xEB, 0x05, 0x22, 0xF0, 0xC8, 0x07, + 0x0A, 0x33, 0xB5, 0x3A, 0xF6, 0x74, 0x42, 0x42, 0xA4, 0x02, 0xB0, 0x39, 0x0F, 0x68, 0xEA, 0xF9, + 0x0A, 0x20, 0xBF, 0x00, 0xC0, 0x79, 0x36, 0x17, 0x33, 0xCD, 0x59, 0x0B, 0xA4, 0xC5, 0x32, 0xF4, + 0x08, 0x62, 0x10, 0x4E, 0xF9, 0x8B, 0x23, 0xC2, 0x19, 0x57, 0x5A, 0x68, 0x13, 0x23, 0x93, 0x15, + 0x5E, 0xAC, 0xC7, 0x8D, 0x80, 0xAC, 0xCA, 0x6C, 0x47, 0xB0, 0xF0, 0xCD, 0x5C, 0x74, 0xBA, 0x48, + 0x5A, 0xCD, 0x9A, 0x6C, 0x31, 0x34, 0x9B, 0xDF, 0x29, 0x5D, 0xD1, 0xE3, 0x57, 0x99, 0x10, 0xD0, + 0xF5, 0x1C, 0x23, 0xD0, 0x85, 0xCF, 0x1E, 0x06, 0xA6, 0x3E, 0x60, 0x03, 0x4C, 0x8F, 0x13, 0xCE, + 0x0D, 0xAF, 0x47, 0x6D, 0xA4, 0x92, 0x7B, 0x53, 0x3C, 0xED, 0xB2, 0x27, 0x27, 0x20, 0x32, 0x0B, + 0x42, 0xE8, 0x9A, 0x68, 0x15, 0xD6, 0x0C, 0xBC, 0x04, 0x8E, 0x9A, 0x8C, 0x41, 0xBC, 0xFB, 0xC4, + 0xC8, 0x38, 0x2A, 0x6F, 0x49, 0xD1, 0x63, 0x3C, 0xBA, 0x8F, 0x2D, 0x4E, 0x71, 0x4B, 0xA3, 0xBA, + 0xBC, 0x0E, 0xE0, 0xB9, 0xCE, 0xAC, 0xB1, 0x60, 0xC1, 0x42, 0xF6, 0x48, 0xC5, 0xE5, 0xD0, 0xAF, + 0xFA, 0x54, 0xC6, 0x00, 0x4E, 0xF3, 0x53, 0x93, 0xA1, 0x85, 0x51, 0x2C, 0x6B, 0x9F, 0x0B, 0x15, + 0x16, 0x7C, 0x6D, 0xFA, 0x62, 0xE7, 0xCD, 0x58, 0xD0, 0xEC, 0xD0, 0xA0, 0x5F, 0x2A, 0x5F, 0xF1, + 0xC4, 0x20, 0xC4, 0xE9, 0x96, 0xE7, 0x1C, 0x01, 0x8A, 0x33, 0x83, 0x11, 0x31, 0xD0, 0x73, 0xEA, + 0x28, 0x25, 0x63, 0x15, 0xB1, 0xDF, 0xE9, 0x7B, 0xA8, 0x00, 0x44, 0xB1, 0x77, 0x7E, 0x3C, 0x62, + 0x11, 0x9A, 0xCE, 0x1F, 0xB4, 0x38, 0xDA, 0xEB, 0x59, 0x51, 0xC3, 0x8B, 0xBD, 0x07, 0xF8, 0x72, + 0x73, 0x71, 0x42, 0xEE, 0x02, 0xB7, 0x27, 0xDF, 0x7F, 0xA2, 0xF4, 0x58, 0x78, 0xF2, 0x89, 0x07, + 0x87, 0x0D, 0x1D, 0x1F, 0x49, 0x79, 0x38, 0x74, 0x6C, 0xF2, 0xD0, 0xC0, 0x3C, 0xF8, 0xE8, 0x3C, + 0x01, 0xBB, 0xE8, 0x4B, 0x67, 0x56, 0x09, 0x8F, 0x6E, 0x20, 0x92, 0x7E, 0x0F, 0x7E, 0xE0, 0xE2, + 0xBB, 0xBE, 0xF4, 0x66, 0x85, 0x13, 0xD7, 0x66, 0x6F, 0xEB, 0xB1, 0x44, 0x5B, 0xAA, 0xA1, 0x5F, + 0x63, 0x17, 0x1A, 0xF0, 0x8E, 0xB0, 0x1F, 0x19, 0x1A, 0x22, 0x19, 0xA6, 0xDA, 0x28, 0xC5, 0x5D, + 0xC2, 0x06, 0x34, 0x3D, 0x43, 0x91, 0x3D, 0xDB, 0xA9, 0xBF, 0xA1, 0x89, 0x0F, 0x9C, 0x07, 0x98, + 0x8E, 0xC0, 0x48, 0x12, 0x0A, 0x36, 0x91, 0xBD, 0x51, 0x3A, 0x5B, 0x35, 0xE5, 0x9D, 0x06, 0xFB, + 0x08, 0xB8, 0x3E, 0xFA, 0x32, 0xFA, 0x06, 0xC3, 0xF3, 0x1A, 0xE8, 0x72, 0xB5, 0xCB, 0x0E, 0xD5, + 0x3B, 0xF2, 0x3B, 0x15, 0xF2, 0x72, 0xC8, 0x28, 0x92, 0xEF, 0x4D, 0x8C, 0xED, 0x10, 0xC0, 0x40, + 0xAC, 0xD8, 0x65, 0x9F, 0x3A, 0xF4, 0xE3, 0x0E, 0x7F, 0xFC, 0x55, 0xFA, 0x5C, 0xDC, 0xDE, 0x9C, + 0xA9, 0x30, 0x3A, 0xFF, 0x98, 0xD5, 0x51, 0xB2, 0x6B, 0xD6, 0xB7, 0x32, 0x39, 0xD8, 0xAC, 0x70, + 0xD8, 0xE9, 0xE4, 0x32, 0x25, 0x6F, 0x5D, 0x46, 0x82, 0xC4, 0xC3, 0x8C, 0xFC, 0x88, 0x99, 0x3D, + 0x96, 0xCD, 0x15, 0x10, 0x67, 0x79, 0xE3, 0xF9, 0x02, 0xEC, 0x21, 0xD7, 0xD4, 0xEE, 0xFA, 0x93, + 0x92, 0x05, 0xE4, 0x3B, 0x22, 0x23, 0x76, 0x4A, 0x91, 0xE9, 0x77, 0xCE, 0x97, 0x58, 0xCF, 0xA3, + 0xF4, 0x00, 0x80, 0x14, 0x1D, 0x8A, 0x97, 0x82, 0xA3, 0x2D, 0x57, 0xB7, 0x61, 0x17, 0x6C, 0x07, + 0x71, 0xEF, 0xF1, 0xF0, 0x4D, 0xCE, 0xCC, 0x02, 0x39, 0x63, 0x89, 0x96, 0x03, 0x55, 0x15, 0xB3, + 0xBA, 0x1C, 0xC0, 0x5E, 0x27, 0x3B, 0x4F, 0xC0, 0x56, 0x7D, 0x57, 0x0A, 0xB2, 0xEC, 0x4F, 0xEC, + 0xF6, 0xA7, 0x9B, 0xC7, 0x66, 0x31, 0x08, 0xE7, 0xC0, 0xDC, 0xD8, 0x01, 0x8D, 0x07, 0x6C, 0x65, + 0xC9, 0x81, 0x4D, 0x39, 0x1E, 0x67, 0xCE, 0x3D, 0x95, 0xD2, 0x80, 0x42, 0xF1, 0xD6, 0x99, 0xD2, + 0x40, 0x66, 0x65, 0xF7, 0x97, 0x95, 0x94, 0xF4, 0x34, 0xD6, 0x75, 0x21, 0xB1, 0x21, 0xD6, 0x70, + 0xC2, 0x3E, 0x20, 0x20, 0x33, 0x12, 0x8C, 0x57, 0x26, 0x0B, 0x79, 0x31, 0x60, 0xA7, 0xB2, 0x22, + 0x10, 0x04, 0x36, 0x29, 0x2B, 0xC2, 0xD4, 0xE5, 0x97, 0xCC, 0x59, 0x8A, 0xFD, 0xCB, 0x30, 0xD2, + 0x2B, 0x5B, 0x26, 0x46, 0x9A, 0xE9, 0x72, 0x91, 0xC8, 0xAC, 0x14, 0x02, 0xEA, 0x0E, 0x1E, 0x76, + 0x54, 0x3F, 0x61, 0xBE, 0xA5, 0xF0, 0x79, 0xD2, 0xD4, 0xA5, 0x1A, 0xCE, 0xB4, 0x6B, 0xC4, 0x6D, + 0x8B, 0x63, 0x23, 0xB4, 0xE4, 0x5F, 0xD5, 0x2B, 0x7B, 0xC5, 0x9E, 0xB5, 0x57, 0x32, 0xA4, 0xD7, + 0x05, 0xA9, 0xCB, 0x04, 0xC4, 0x89, 0x92, 0x0F, 0x80, 0x60, 0xA6, 0x31, 0x9C, 0x26, 0xF3, 0x55, + 0xDC, 0x11, 0xE5, 0x94, 0x17, 0xEC, 0x87, 0x4D, 0xD5, 0x6B, 0x04, 0xE5, 0x1C, 0xE8, 0x72, 0xE6, + 0x74, 0x29, 0x43, 0xBB, 0x81, 0x9A, 0xD6, 0x3F, 0xFE, 0xFE, 0x5F, 0x7E, 0x78, 0x24, 0x61, 0xD6, + 0x02, 0xEC, 0x39, 0xEC, 0xB1, 0x46, 0x31, 0xBC, 0xD4, 0x23, 0xF4, 0x84, 0xE2, 0x10, 0x39, 0xAA, + 0xA3, 0xA8, 0x44, 0x13, 0x97, 0x71, 0x4C, 0xDA, 0x0F, 0x14, 0xCF, 0x5C, 0x50, 0x0B, 0x1F, 0x81, + 0x96, 0x05, 0xC6, 0x11, 0x1D, 0xD5, 0xA3, 0x68, 0xE1, 0x07, 0x90, 0xA8, 0x06, 0x32, 0x88, 0x48, + 0x33, 0xC2, 0xC4, 0xAE, 0x72, 0x02, 0x38, 0x5B, 0xB4, 0xB1, 0xF4, 0x60, 0x66, 0xD1, 0x22, 0x70, + 0x34, 0xD0, 0x0F, 0xED, 0x74, 0xD4, 0x72, 0x18, 0xE4, 0x3C, 0xE4, 0x62, 0xE0, 0x18, 0x0C, 0xA4, + 0x47, 0x93, 0x63, 0xE0, 0xD5, 0x7F, 0xEE, 0xD7, 0xF5, 0x61, 0x46, 0xDC, 0x14, 0x8A, 0xA6, 0xA8, + 0x7A, 0xDE, 0x03, 0x77, 0xA9, 0xB1, 0x2B, 0x5B, 0x87, 0xF1, 0xFD, 0x30, 0x80, 0x2A, 0xF4, 0xCB, + 0xFF, 0xDA, 0x45, 0x57, 0x16, 0x39, 0xF0, 0x9D, 0x39, 0xE4, 0x61, 0xD6, 0x85, 0x5B, 0x69, 0x6C, + 0x32, 0x30, 0xF1, 0x86, 0xCD, 0x30, 0x1B, 0x7A, 0xCC, 0xDD, 0xD7, 0x94, 0x7D, 0xF1, 0x28, 0xE8, + 0x89, 0xC6, 0x22, 0x49, 0x19, 0xE3, 0xB1, 0x60, 0x0A, 0x26, 0xC6, 0x89, 0x9D, 0x72, 0x2C, 0x29, + 0x31, 0x7A, 0xC9, 0xF4, 0xC3, 0x49, 0x1B, 0xE4, 0x23, 0x43, 0xE1, 0xD3, 0x55, 0x78, 0x65, 0x18, + 0x25, 0x79, 0x50, 0x21, 0x55, 0x94, 0x8F, 0x16, 0x13, 0xD8, 0x50, 0x39, 0x86, 0x51, 0x3C, 0x40, + 0x5D, 0x06, 0x8E, 0x1D, 0xD6, 0xEA, 0xC3, 0x91, 0xA4, 0x79, 0x44, 0xA2, 0xE6, 0x51, 0xBD, 0xEE, + 0xEF, 0xB2, 0x46, 0x8D, 0x3E, 0xD0, 0x0B, 0x5B, 0xE7, 0x54, 0x13, 0x72, 0x80, 0x75, 0x7A, 0xD4, + 0x38, 0x23, 0x7C, 0x50, 0x99, 0x01, 0xA6, 0x5E, 0x2B, 0x35, 0x9A, 0x59, 0x91, 0x26, 0x88, 0x59, + 0xB3, 0x53, 0x49, 0xC5, 0x30, 0x0F, 0x1D, 0xA3, 0xD8, 0x8E, 0xFF, 0xA6, 0xF4, 0x15, 0x40, 0x22, + 0x08, 0x3C, 0x47, 0x00, 0x70, 0x71, 0x47, 0x88, 0x0C, 0xC0, 0xE4, 0xBE, 0x08, 0x18, 0x6F, 0xBB, + 0xED, 0xD7, 0x4C, 0xB1, 0x15, 0xB5, 0x15, 0xBF, 0x39, 0x51, 0x64, 0x3E, 0xD7, 0x4D, 0x53, 0x06, + 0x3E, 0x85, 0x9C, 0x68, 0x66, 0xB7, 0xC4, 0xA9, 0xF6, 0xCF, 0x59, 0x47, 0x00, 0x8F, 0xC0, 0xB3, + 0x37, 0xE2, 0x5A, 0x33, 0x9B, 0xEE, 0x0C, 0xE7, 0x41, 0x3D, 0x42, 0x3A, 0x85, 0x7F, 0xDD, 0xEA, + 0xFE, 0x32, 0x51, 0x6F, 0x0C, 0xD8, 0x65, 0x70, 0x30, 0xDD, 0xC9, 0x06, 0xA7, 0x21, 0xA0, 0x13, + 0x77, 0x5E, 0x89, 0xE8, 0x12, 0x3B, 0x19, 0x17, 0x87, 0xA2, 0xD4, 0xD0, 0x19, 0x82, 0x51, 0x31, + 0x81, 0x48, 0x0E, 0xD0, 0xA2, 0x5F, 0x89, 0x63, 0x45, 0x56, 0x4B, 0x6A, 0xB1, 0x9D, 0xB3, 0x81, + 0x83, 0x41, 0x30, 0x58, 0x80, 0x82, 0x39, 0xB3, 0x1D, 0x32, 0x3E, 0xA9, 0x12, 0x1A, 0x6F, 0xC2, + 0x3A, 0x0D, 0x6B, 0x11, 0xAF, 0xE2, 0x62, 0x95, 0x88, 0x89, 0x41, 0x91, 0x30, 0x85, 0xD2, 0x05, + 0x08, 0x53, 0x54, 0x51, 0x46, 0x0A, 0x46, 0xDC, 0x44, 0x9B, 0x76, 0xB8, 0xCE, 0x68, 0x1D, 0x2C, + 0x56, 0xA2, 0x03, 0x45, 0x11, 0x69, 0xD4, 0xC6, 0x08, 0xED, 0xA4, 0xF1, 0x4C, 0x4A, 0x17, 0xC3, + 0x21, 0x37, 0x4C, 0x29, 0x46, 0x75, 0x79, 0xDF, 0x0C, 0x23, 0xC0, 0xC9, 0xF1, 0xF6, 0xF6, 0xEE, + 0xEC, 0x66, 0xFC, 0x22, 0xEA, 0x44, 0x89, 0x66, 0x91, 0xEC, 0x63, 0xFA, 0x52, 0xB0, 0x8A, 0xE4, + 0x3C, 0x92, 0x37, 0xDA, 0xED, 0x36, 0xDB, 0x71, 0x65, 0x71, 0x27, 0x9F, 0x83, 0x14, 0x18, 0x78, + 0x4C, 0x04, 0xC2, 0x01, 0x3D, 0xC7, 0x7A, 0x43, 0xE9, 0xEE, 0xF1, 0xAD, 0x94, 0xFC, 0xC1, 0xB4, + 0xBE, 0x86, 0x46, 0x7B, 0xBA, 0x77, 0x23, 0x23, 0x9E, 0x1E, 0x89, 0x97, 0x31, 0xD9, 0x24, 0xAC, + 0x18, 0xF4, 0xDA, 0x1F, 0xC1, 0x0D, 0x13, 0xBA, 0x38, 0x81, 0x65, 0x70, 0x6F, 0x17, 0x94, 0x1B, + 0xCB, 0x01, 0xDC, 0x72, 0xB6, 0x4F, 0x2C, 0x21, 0xD0, 0x2E, 0x40, 0x26, 0x99, 0xE1, 0xB6, 0x3E, + 0x8D, 0x92, 0x69, 0x0A, 0x52, 0xF1, 0x0A, 0xF1, 0x08, 0xE9, 0x4E, 0x56, 0x7B, 0x51, 0x69, 0x65, + 0xD5, 0xD6, 0x27, 0x68, 0xBB, 0x9E, 0xFB, 0x30, 0x1A, 0xD1, 0x7E, 0x89, 0x7B, 0x70, 0x62, 0x06, + 0xD3, 0xE6, 0x48, 0x95, 0x67, 0xD8, 0xE8, 0x0B, 0x3F, 0x24, 0xE8, 0x89, 0x31, 0xF3, 0xB9, 0x4F, + 0xF2, 0xB0, 0x1E, 0xCC, 0x8C, 0x0D, 0x2A, 0x76, 0x13, 0x99, 0x57, 0x24, 0x48, 0x37, 0x4E, 0x14, + 0x02, 0x4C, 0x78, 0xFF, 0xBC, 0x1B, 0x3B, 0xD7, 0xF3, 0xDD, 0x5B, 0x4D, 0x34, 0xB1, 0x05, 0x3E, + 0x81, 0x36, 0x06, 0xD3, 0x5C, 0xB9, 0x4E, 0xF3, 0xE7, 0xA0, 0x8A, 0xCC, 0x6E, 0xE3, 0x23, 0xA1, + 0xC9, 0xED, 0x1A, 0xA3, 0x04, 0x68, 0x5B, 0x2F, 0xF1, 0xD6, 0xFD, 0x97, 0x8B, 0xF3, 0x7C, 0x66, + 0xB3, 0x25, 0x5D, 0x50, 0xC6, 0xB8, 0x77, 0x3B, 0x50, 0x99, 0x3E, 0x54, 0x84, 0xA9, 0x9C, 0x65, + 0x8C, 0x03, 0x63, 0x6E, 0x0E, 0x87, 0xDE, 0x90, 0x5C, 0xF3, 0xA7, 0xBD, 0x9F, 0xF7, 0x7E, 0x41, + 0x28, 0x71, 0x7F, 0x0B, 0xB3, 0xDD, 0xCD, 0xB6, 0xF3, 0xE6, 0x49, 0xE0, 0xB6, 0x28, 0x0A, 0x48, + 0x24, 0x6C, 0xDB, 0xCF, 0x8B, 0xE5, 0x69, 0x36, 0x72, 0x21, 0x8B, 0x50, 0x4D, 0xA2, 0xAA, 0x38, + 0x19, 0xFD, 0x74, 0x9C, 0xFF, 0x42, 0x7E, 0x36, 0xAC, 0xA6, 0xAA, 0x57, 0xEF, 0xF2, 0x2E, 0xA7, + 0x90, 0x74, 0x1B, 0x5B, 0x70, 0xCE, 0xEE, 0xDD, 0x54, 0xBB, 0x79, 0xE4, 0xD7, 0xE7, 0x8D, 0x43, + 0x3F, 0xE7, 0xC3, 0xA1, 0x9F, 0x9F, 0x0D, 0x0E, 0xFD, 0x5C, 0x12, 0x87, 0x7E, 0x7E, 0xC1, 0xA1, + 0x49, 0x38, 0xF4, 0x4B, 0x3E, 0x1C, 0xFA, 0xE5, 0xD9, 0xE0, 0xD0, 0x2F, 0x25, 0x71, 0xE8, 0x97, + 0x17, 0x1C, 0x4A, 0x3B, 0x23, 0x83, 0xFC, 0x84, 0xD2, 0xB9, 0x94, 0xD3, 0xF3, 0x60, 0x51, 0xBE, + 0x82, 0x06, 0x6B, 0x45, 0xA4, 0x59, 0x6E, 0xC3, 0xF1, 0x35, 0x17, 0x42, 0xA4, 0x78, 0xCB, 0x67, + 0x81, 0x4A, 0x72, 0x17, 0xCF, 0x42, 0x27, 0x9A, 0x6F, 0x78, 0xAD, 0x11, 0x93, 0xA2, 0xD7, 0x9E, + 0x5C, 0x96, 0xB2, 0xBE, 0xA1, 0x91, 0xA0, 0x97, 0x90, 0x8E, 0x95, 0x55, 0x83, 0xB2, 0x91, 0x80, + 0x9C, 0xFC, 0x5B, 0x00, 0xFB, 0x02, 0xD3, 0xD7, 0x1D, 0xF7, 0x69, 0xCF, 0x45, 0xC7, 0x59, 0x65, + 0x00, 0x19, 0x93, 0xA8, 0xB7, 0x5B, 0x8E, 0xCE, 0x9F, 0xA0, 0x75, 0xB9, 0xDC, 0xA7, 0x88, 0xFA, + 0x94, 0x26, 0x9B, 0x3B, 0xA2, 0xCD, 0x49, 0x75, 0x51, 0xBF, 0xA8, 0x37, 0x8F, 0xE5, 0x4E, 0x4D, + 0xAD, 0xE4, 0x9B, 0x3D, 0x66, 0x3C, 0x37, 0xA9, 0xCA, 0xF8, 0x79, 0x96, 0x2E, 0x3E, 0x8B, 0x73, + 0x9A, 0xF4, 0x9D, 0x6F, 0xFE, 0x4E, 0x89, 0x36, 0x73, 0xE3, 0x46, 0xE4, 0x9E, 0x12, 0xD6, 0x8B, + 0xDE, 0x3F, 0xAC, 0xE7, 0x73, 0x4E, 0x89, 0x26, 0x9D, 0xCA, 0x51, 0x89, 0x9A, 0xAB, 0x34, 0x4F, + 0x9D, 0xB0, 0x78, 0x31, 0x5B, 0x16, 0xAF, 0x43, 0xCB, 0x12, 0x25, 0x64, 0xF3, 0xA4, 0xC2, 0x9C, + 0x32, 0x64, 0x73, 0x34, 0xE4, 0xE9, 0x8A, 0x86, 0xDC, 0xA7, 0x21, 0xCF, 0x56, 0x34, 0x1A, 0xA6, + 0x5E, 0xC5, 0x83, 0xEB, 0x6E, 0xA0, 0x89, 0x2B, 0xFF, 0xCE, 0x71, 0xAE, 0x1D, 0xBB, 0x7F, 0xE7, + 0x9C, 0xF2, 0x73, 0x89, 0x81, 0xC0, 0x47, 0x26, 0x4E, 0xA4, 0x50, 0x12, 0xE7, 0xD2, 0x93, 0x9E, + 0x9D, 0x6F, 0x74, 0x53, 0x65, 0x90, 0xB1, 0x4B, 0xA2, 0x05, 0x9A, 0x90, 0x34, 0xC3, 0x48, 0x4A, + 0x26, 0x8B, 0xB5, 0x1B, 0xA9, 0xEE, 0xD1, 0x58, 0x74, 0x62, 0x18, 0x05, 0xAD, 0x43, 0x53, 0x5C, + 0xCB, 0xB4, 0xA5, 0xCE, 0x3A, 0xEC, 0x3F, 0xB4, 0x71, 0x2D, 0x6A, 0xDE, 0x06, 0x20, 0xA0, 0xE0, + 0xCB, 0x9C, 0xF9, 0x68, 0x04, 0x9C, 0xFB, 0x39, 0x7D, 0xDA, 0x14, 0x9B, 0x1C, 0x20, 0x00, 0xC8, + 0x12, 0xBB, 0xB2, 0x52, 0x86, 0x66, 0x50, 0xA4, 0xCE, 0x2E, 0x99, 0xE0, 0xA2, 0x52, 0x45, 0x1E, + 0xD7, 0xE1, 0xB4, 0x88, 0x49, 0x12, 0xF4, 0xB6, 0x5C, 0x14, 0x33, 0xC5, 0x33, 0xB6, 0xCC, 0x2D, + 0xE2, 0x26, 0x7B, 0xF1, 0x97, 0x0B, 0x9F, 0xB8, 0x33, 0xF5, 0x6E, 0xA1, 0xB1, 0xFE, 0xBB, 0x85, + 0x4F, 0x94, 0x75, 0x4B, 0xDD, 0xB1, 0x2D, 0xF6, 0x4A, 0xA1, 0xAF, 0x7A, 0x2D, 0x79, 0xEF, 0xB3, + 0xC5, 0xB7, 0x0A, 0x9F, 0xA2, 0x95, 0x8F, 0x6E, 0x16, 0x72, 0x53, 0x5E, 0x1E, 0xCE, 0x96, 0xB8, + 0x59, 0xC8, 0x18, 0x2D, 0xBA, 0x5D, 0xB8, 0xBE, 0x1E, 0xE4, 0x1E, 0x78, 0xE3, 0x2F, 0x21, 0x60, + 0x31, 0xE9, 0x7B, 0x07, 0x7C, 0xF4, 0x72, 0xEB, 0x30, 0x9F, 0x7A, 0x1D, 0xE3, 0x0F, 0xD7, 0x9A, + 0x20, 0xFD, 0xE7, 0x55, 0x11, 0x44, 0xCD, 0x20, 0xAD, 0x49, 0x05, 0x29, 0xA1, 0x7F, 0x53, 0x04, + 0x46, 0x11, 0xFB, 0x4C, 0xD9, 0xEC, 0xB6, 0x65, 0x0C, 0x2F, 0x09, 0x18, 0x14, 0xB7, 0xE0, 0xA9, + 0x96, 0xCF, 0xC7, 0xF0, 0x12, 0xC7, 0x0C, 0xD4, 0x47, 0x96, 0x8A, 0x1A, 0x30, 0xC0, 0xE6, 0xE3, + 0x46, 0x08, 0x85, 0x12, 0xC8, 0xA1, 0x9A, 0x3E, 0x1B, 0x0B, 0xAF, 0x94, 0xC8, 0x05, 0x49, 0x9E, + 0x97, 0x58, 0xCF, 0x40, 0x64, 0xDA, 0x67, 0x8B, 0xD9, 0x7C, 0x27, 0x85, 0x1F, 0x83, 0x2C, 0x2E, + 0x47, 0x62, 0x72, 0xA8, 0xFC, 0x48, 0x92, 0xB4, 0xF4, 0x24, 0x27, 0x3C, 0xDD, 0xD6, 0xB3, 0xAD, + 0xF7, 0xC7, 0x85, 0x1B, 0x67, 0x12, 0xFC, 0x89, 0x25, 0x59, 0x75, 0x65, 0x36, 0x31, 0x8F, 0x67, + 0xF5, 0xF8, 0x7C, 0x72, 0x91, 0xFB, 0x34, 0xBE, 0xD1, 0xBC, 0x1F, 0x7B, 0x30, 0x1A, 0x3A, 0x50, + 0x96, 0x3B, 0xC2, 0x17, 0x23, 0x75, 0xC4, 0x24, 0x8F, 0xCF, 0x1C, 0x9B, 0xB2, 0x13, 0x2A, 0x33, + 0x78, 0x61, 0x59, 0xA6, 0xEB, 0x3B, 0xA6, 0x11, 0x55, 0x01, 0x83, 0x09, 0x87, 0xC5, 0xC2, 0x94, + 0x80, 0x12, 0x49, 0x5E, 0xA8, 0x6F, 0x69, 0x0A, 0x72, 0xF1, 0xF7, 0xF1, 0xF9, 0xD0, 0xB1, 0x03, + 0x2A, 0x15, 0xA2, 0xFC, 0xF2, 0x71, 0x8B, 0x6C, 0xA3, 0x88, 0xE0, 0xB2, 0x74, 0xE1, 0xA5, 0x88, + 0x00, 0x93, 0xDB, 0xE4, 0x3B, 0x93, 0x5D, 0x1F, 0x14, 0x91, 0xDD, 0x8A, 0x5F, 0xC8, 0x45, 0x98, + 0x9C, 0x77, 0x4D, 0xF9, 0x6E, 0x84, 0x72, 0xBE, 0xE6, 0x8E, 0x4F, 0x64, 0xE9, 0xA5, 0x04, 0xE7, + 0xE2, 0x24, 0x79, 0x4B, 0xB8, 0x1F, 0x14, 0xB0, 0x22, 0x4F, 0x2E, 0xE5, 0x8E, 0xC4, 0x91, 0xAC, + 0xE3, 0xDE, 0xA8, 0xC4, 0xED, 0x5F, 0x58, 0x74, 0x08, 0xB8, 0x09, 0x2A, 0x34, 0x45, 0xF8, 0x18, + 0x45, 0x5B, 0x69, 0x96, 0x0E, 0x4D, 0x91, 0xBF, 0x60, 0x1A, 0x33, 0x99, 0x03, 0x9E, 0x33, 0x78, + 0x44, 0x3C, 0xA7, 0xC4, 0x09, 0xB2, 0x0C, 0x2D, 0xE7, 0xAA, 0x27, 0xF3, 0x34, 0x70, 0x63, 0x37, + 0x9A, 0x9A, 0xCC, 0xF4, 0xAF, 0x26, 0x4C, 0x01, 0x46, 0xFF, 0xE6, 0x07, 0x5D, 0xE1, 0x69, 0x94, + 0x45, 0xFF, 0xDF, 0x64, 0x8D, 0x25, 0xE5, 0x31, 0x7B, 0x02, 0xBF, 0x6D, 0x5B, 0x63, 0x8A, 0x67, + 0x61, 0x6C, 0xCB, 0xC9, 0x6D, 0x7B, 0x94, 0xC7, 0x25, 0xE2, 0x46, 0x51, 0xCC, 0x36, 0x74, 0x87, + 0x5F, 0x50, 0x8D, 0x98, 0xD0, 0x6D, 0x76, 0xC4, 0xCF, 0x06, 0xD0, 0xC3, 0x93, 0x2C, 0x26, 0x28, + 0x03, 0x87, 0x54, 0x35, 0x43, 0x9C, 0x16, 0xF9, 0xB6, 0x6A, 0xEA, 0xEE, 0xCA, 0x75, 0x80, 0xDF, + 0x0C, 0x12, 0xA3, 0xEE, 0x52, 0x10, 0x4F, 0x60, 0xCB, 0x17, 0xA8, 0x49, 0x54, 0xA4, 0x26, 0x5E, + 0xD0, 0x30, 0x5A, 0x27, 0x6D, 0x39, 0x05, 0xF5, 0x58, 0x0F, 0xDA, 0x93, 0xAF, 0x0C, 0x58, 0xAA, + 0xC2, 0xC0, 0x00, 0xC3, 0xF9, 0x26, 0x5C, 0xFC, 0x31, 0xAA, 0x8A, 0xB4, 0x85, 0x4A, 0xDF, 0xDC, + 0xA9, 0x2E, 0xB6, 0x9F, 0xC0, 0x9B, 0x13, 0x08, 0x9C, 0xE5, 0xCA, 0x90, 0x94, 0x4D, 0xE8, 0x16, + 0x74, 0x71, 0xD2, 0x3E, 0x4B, 0xD3, 0x39, 0x3C, 0xDA, 0x50, 0x32, 0x87, 0x99, 0x65, 0x50, 0xB9, + 0x66, 0x18, 0x79, 0x09, 0x5C, 0x51, 0x48, 0x5E, 0xF2, 0x7E, 0x21, 0xBE, 0x22, 0xC4, 0xB7, 0x22, + 0xCD, 0x5A, 0x93, 0x5B, 0x2B, 0x77, 0x36, 0x87, 0xA0, 0x3D, 0x9E, 0x7B, 0x51, 0x21, 0x47, 0xBE, + 0xE2, 0xDF, 0x34, 0xCC, 0xCE, 0x30, 0xAF, 0x9C, 0xBD, 0x3C, 0x62, 0x38, 0x37, 0x7D, 0x41, 0xE5, + 0xEF, 0xA2, 0x53, 0x8A, 0xE2, 0x42, 0x94, 0xBC, 0xAC, 0xA0, 0x12, 0xA2, 0xA1, 0x14, 0x9F, 0x13, + 0x22, 0x73, 0x74, 0x58, 0x05, 0x7E, 0x40, 0x39, 0xC7, 0x28, 0xE5, 0x8A, 0x23, 0xA8, 0x26, 0x94, + 0xDD, 0x97, 0xA1, 0xB2, 0x32, 0xBA, 0xD7, 0xF4, 0x87, 0xF2, 0xA4, 0x42, 0xBA, 0xD1, 0xEC, 0x27, + 0x06, 0xB8, 0xC6, 0x29, 0x2F, 0x97, 0x5F, 0x63, 0x27, 0x43, 0x4C, 0xA9, 0x47, 0x24, 0x60, 0x28, + 0xC4, 0x47, 0xEA, 0xE8, 0xF2, 0x1E, 0x26, 0x5E, 0x81, 0x39, 0x7A, 0x14, 0x30, 0x22, 0x4D, 0x8D, + 0x38, 0xC5, 0xED, 0xF5, 0xC9, 0x28, 0x6F, 0x4C, 0x39, 0x5C, 0x8A, 0xAB, 0x5C, 0x12, 0xF5, 0x0B, + 0x99, 0x53, 0x12, 0x4D, 0x9F, 0x9B, 0xB1, 0x4D, 0x2D, 0xEE, 0x36, 0x0C, 0x13, 0x9A, 0x94, 0x96, + 0x55, 0xB1, 0x86, 0xF9, 0x38, 0x42, 0x34, 0x8A, 0x4C, 0xB0, 0xB9, 0x09, 0xAC, 0x01, 0x8F, 0x36, + 0xA5, 0x55, 0x1B, 0x45, 0xB8, 0x44, 0xF8, 0xD1, 0x1D, 0xE0, 0x3B, 0x3A, 0x1D, 0x9E, 0xE3, 0x7C, + 0x02, 0x08, 0xCE, 0x16, 0xA3, 0xC8, 0x78, 0xDF, 0x34, 0xC6, 0xFA, 0x53, 0x25, 0xBD, 0x63, 0x07, + 0xB3, 0x2A, 0x27, 0x15, 0xBE, 0x00, 0xDF, 0x98, 0x5D, 0x4F, 0xC9, 0xB4, 0xC5, 0x39, 0x88, 0x3A, + 0x56, 0xF1, 0x0C, 0xF7, 0x8F, 0xD9, 0x25, 0x80, 0x4B, 0xEC, 0xBC, 0x6D, 0xD4, 0xDE, 0x01, 0xEC, + 0xE5, 0x27, 0x76, 0x5D, 0x3D, 0xC5, 0x49, 0xEC, 0x1C, 0xBD, 0xC7, 0x67, 0x77, 0x5F, 0xDB, 0x8D, + 0xFA, 0xD1, 0xCE, 0x61, 0xB3, 0xF6, 0xFE, 0xCD, 0xB3, 0xF4, 0x08, 0x5B, 0x1F, 0xF3, 0x49, 0x11, + 0x59, 0x19, 0x26, 0x94, 0xEC, 0xE2, 0x59, 0x5A, 0xFE, 0x01, 0x9F, 0x7F, 0x45, 0xD1, 0xBA, 0x8C, + 0x78, 0xA2, 0x24, 0xDD, 0x9D, 0xF5, 0xF3, 0x95, 0xE9, 0x26, 0xBD, 0x50, 0xC6, 0x6E, 0x13, 0xF7, + 0x38, 0x4B, 0x71, 0x0F, 0x62, 0x15, 0xC1, 0x30, 0xCD, 0x29, 0xA4, 0xEE, 0xBB, 0x9B, 0x60, 0x39, + 0x32, 0x8B, 0x19, 0x89, 0x2C, 0xF2, 0x6B, 0x92, 0xBC, 0xD1, 0x11, 0xD4, 0x63, 0xDC, 0x90, 0xF5, + 0x85, 0x77, 0x99, 0x3F, 0x40, 0xFD, 0x18, 0x53, 0x36, 0x22, 0x51, 0xA2, 0x25, 0x9A, 0x42, 0x64, + 0x41, 0xD6, 0xAE, 0xBD, 0x90, 0xF8, 0x02, 0x2D, 0x7F, 0x11, 0xEA, 0x86, 0x39, 0xEF, 0x8A, 0x5F, + 0xDB, 0xA8, 0x2E, 0x56, 0x46, 0xD9, 0xE4, 0x7A, 0xB1, 0x1A, 0xEA, 0x0E, 0xBD, 0x5E, 0x43, 0xFF, + 0x81, 0x4A, 0x99, 0x6B, 0x3B, 0xF2, 0xB6, 0x5F, 0x33, 0x6D, 0x7F, 0x02, 0x15, 0x57, 0x86, 0x83, + 0x53, 0xD6, 0x18, 0xA6, 0xB1, 0x70, 0x69, 0xE1, 0x41, 0x2C, 0xF5, 0x6A, 0x8C, 0x37, 0x57, 0xE2, + 0x42, 0x94, 0x96, 0x91, 0x5C, 0xB4, 0xE3, 0x6E, 0xD8, 0xE8, 0xCE, 0xFD, 0x07, 0xA4, 0xC2, 0xA3, + 0x25, 0xC6, 0x31, 0x44, 0x08, 0x56, 0x2A, 0x96, 0x21, 0x6C, 0xFD, 0x12, 0xCF, 0xB0, 0x74, 0x42, + 0x5A, 0x7C, 0x3C, 0xC3, 0xB5, 0x26, 0xF6, 0xF0, 0xE6, 0x1B, 0x2F, 0xF8, 0x5E, 0xC2, 0x1A, 0xD6, + 0x14, 0xD6, 0x10, 0x52, 0x50, 0xCE, 0xD0, 0x86, 0xDC, 0xDB, 0x13, 0xDF, 0xDC, 0x85, 0x47, 0x42, + 0x8C, 0x4E, 0xA5, 0xEC, 0x68, 0x88, 0xD1, 0xF7, 0x1B, 0x16, 0x11, 0x71, 0x50, 0xAF, 0xD5, 0xDF, + 0xD7, 0x9B, 0xEF, 0x0F, 0xDE, 0xBE, 0x67, 0xD5, 0x46, 0xFD, 0xB0, 0xD6, 0x78, 0x77, 0x58, 0x3F, + 0x7C, 0x7B, 0xD4, 0x60, 0x8D, 0xC3, 0x23, 0xF8, 0xEE, 0xDD, 0xFB, 0xC5, 0x86, 0x43, 0xAC, 0x60, + 0xBC, 0x30, 0x16, 0x62, 0x05, 0x43, 0xE5, 0x08, 0x84, 0x98, 0x38, 0x8B, 0x42, 0xF7, 0xE8, 0xE5, + 0x66, 0xFC, 0x12, 0x05, 0x31, 0x21, 0x0A, 0x62, 0x5C, 0x88, 0x5C, 0x78, 0x24, 0x44, 0x38, 0xC4, + 0x12, 0xA2, 0x21, 0x96, 0x3D, 0xFB, 0xF8, 0x18, 0xCB, 0x89, 0x8A, 0x58, 0xF6, 0x0A, 0x92, 0xA3, + 0x6C, 0x7D, 0x74, 0x44, 0xFC, 0xE8, 0xFA, 0xC3, 0x04, 0x49, 0x2C, 0x25, 0xC5, 0xDA, 0xD2, 0x6B, + 0x0B, 0x77, 0xB8, 0x77, 0x8F, 0x35, 0xFF, 0x12, 0xB5, 0x85, 0xE5, 0xC3, 0x95, 0xD5, 0x16, 0x0E, + 0xE7, 0xB0, 0x9E, 0xDA, 0xC2, 0x3E, 0x8D, 0x8E, 0x82, 0x77, 0xE0, 0x12, 0x72, 0x87, 0xB6, 0xF0, + 0xD7, 0xFE, 0x58, 0x91, 0x61, 0x4A, 0x9E, 0x3A, 0x56, 0x62, 0xF8, 0x2B, 0xA5, 0x54, 0x93, 0x35, + 0x86, 0xA5, 0x34, 0x2F, 0xD0, 0xC1, 0x43, 0xD3, 0x31, 0x41, 0x99, 0x52, 0x88, 0x93, 0xFD, 0x04, + 0x32, 0x6F, 0x99, 0x9D, 0x28, 0x6F, 0xFC, 0x7C, 0xEA, 0x0E, 0xCB, 0x0D, 0x5D, 0x7B, 0xDD, 0x61, + 0x39, 0x8D, 0x58, 0x49, 0xDC, 0x5F, 0xEB, 0xEB, 0xAE, 0x3C, 0xCC, 0x1A, 0x9B, 0x58, 0xA4, 0x2F, + 0x0B, 0x50, 0xC5, 0x8A, 0xA8, 0x65, 0xF4, 0xF0, 0x2C, 0x0A, 0xF5, 0x25, 0xD6, 0x85, 0x85, 0x71, + 0xD7, 0x8B, 0x42, 0x38, 0x83, 0xD5, 0xA3, 0x50, 0xBE, 0x1A, 0xC4, 0x59, 0xA0, 0x9A, 0x03, 0x89, + 0x64, 0x0F, 0xCF, 0x0F, 0x89, 0xB0, 0xC0, 0xE9, 0x7A, 0x91, 0x88, 0x8A, 0xC9, 0x6E, 0x3E, 0x1F, + 0x52, 0x80, 0x9A, 0x03, 0x85, 0x64, 0x0F, 0xCF, 0x13, 0x85, 0xDA, 0xDF, 0x37, 0x00, 0x89, 0xDA, + 0xDF, 0xB7, 0x04, 0x8F, 0x08, 0x5A, 0x73, 0x62, 0x12, 0xF6, 0xF1, 0x9C, 0x70, 0x69, 0x54, 0xF0, + 0x14, 0x60, 0xB3, 0xE4, 0xCA, 0xC4, 0x1B, 0x8D, 0x26, 0x09, 0x40, 0x94, 0x42, 0x92, 0x78, 0x0F, + 0xCF, 0x13, 0x45, 0xE6, 0x66, 0x37, 0xAB, 0xA9, 0x50, 0x3C, 0x07, 0x9E, 0x2D, 0xB1, 0x46, 0x71, + 0x36, 0x34, 0xE7, 0xC4, 0xB4, 0x75, 0xB3, 0xA3, 0xA9, 0x37, 0xE9, 0xCB, 0xD4, 0xE1, 0x1A, 0x6B, + 0xD7, 0xE1, 0x9A, 0xDB, 0xA1, 0xC3, 0x35, 0xE6, 0xD6, 0xE1, 0x1A, 0xCF, 0x54, 0x87, 0x6B, 0xAC, + 0x5D, 0x87, 0x6B, 0x6E, 0x8B, 0x0E, 0xD7, 0x98, 0x5B, 0x87, 0x6B, 0x3C, 0x53, 0x1D, 0xAE, 0xB1, + 0x76, 0xF1, 0xBB, 0xB9, 0x1D, 0x3A, 0x5C, 0x63, 0x6E, 0x1D, 0xAE, 0xF1, 0x6C, 0x75, 0xB8, 0xC6, + 0x26, 0xE8, 0x70, 0xCD, 0x6D, 0xD1, 0xE1, 0x1A, 0x0B, 0xD0, 0xE1, 0x1A, 0xCF, 0x54, 0x87, 0x6B, + 0x2C, 0x5D, 0x87, 0x6B, 0x6E, 0x87, 0x0E, 0xD7, 0x98, 0x5B, 0x87, 0x6B, 0x3C, 0x5B, 0x1D, 0xAE, + 0xB1, 0x0D, 0x3A, 0x5C, 0x73, 0x5B, 0x74, 0xB8, 0xC6, 0x02, 0x74, 0xB8, 0xB5, 0xE2, 0x1A, 0x96, + 0x81, 0xBD, 0x74, 0x3C, 0xD6, 0x0B, 0x44, 0xE0, 0x71, 0x46, 0xF5, 0x65, 0xFD, 0x69, 0xE5, 0xD8, + 0xA2, 0x6B, 0x3E, 0xCD, 0xB2, 0x9C, 0x87, 0xEF, 0xE6, 0xA5, 0xF9, 0x15, 0x56, 0x74, 0x81, 0xF7, + 0x9F, 0x36, 0x17, 0x33, 0xEF, 0x71, 0xA7, 0x5F, 0x6B, 0x37, 0x8A, 0x24, 0xDF, 0xCB, 0x7D, 0xD7, + 0x4D, 0x90, 0x07, 0xDE, 0x8F, 0x93, 0xFD, 0xE2, 0x88, 0x70, 0xAE, 0x95, 0x16, 0x5E, 0x03, 0xE3, + 0x43, 0x8A, 0x14, 0x88, 0xDF, 0x7B, 0xB3, 0x2A, 0x55, 0x24, 0x0D, 0xDF, 0xCC, 0xEF, 0xFD, 0x3B, + 0xCF, 0x75, 0x78, 0x0C, 0x41, 0xB2, 0x26, 0x9B, 0x33, 0xEE, 0x7C, 0x61, 0xBE, 0x1F, 0xEA, 0xAA, + 0xFC, 0xAB, 0xCD, 0x6E, 0xEF, 0xFE, 0x82, 0xB5, 0xB6, 0x8C, 0x40, 0x17, 0x61, 0xC1, 0xFB, 0x81, + 0x86, 0x51, 0x1E, 0x6A, 0x6E, 0x40, 0x72, 0xB6, 0x8D, 0x77, 0xDB, 0xF7, 0xA6, 0x78, 0xDA, 0x65, + 0x4F, 0x4E, 0x40, 0x37, 0xE0, 0x41, 0x08, 0x5D, 0xAC, 0xCC, 0xC5, 0x35, 0x8A, 0x05, 0x89, 0x9A, + 0x8C, 0x41, 0xBC, 0xFB, 0x24, 0xE3, 0xB7, 0x65, 0xF1, 0x53, 0xD3, 0x67, 0xD2, 0x6B, 0x2C, 0xE3, + 0x46, 0xBC, 0x52, 0xC2, 0x15, 0x66, 0x91, 0xBE, 0x23, 0x0B, 0xC8, 0x31, 0x20, 0xBF, 0x9E, 0x58, + 0x69, 0x79, 0xD6, 0x35, 0xFB, 0x94, 0x8E, 0x95, 0x17, 0x53, 0xE4, 0x56, 0xD4, 0xE7, 0xE2, 0x66, + 0x54, 0xD6, 0x1B, 0xD3, 0x53, 0x52, 0x22, 0x80, 0xA4, 0x0B, 0xD3, 0xD0, 0xA0, 0x5F, 0x4E, 0x20, + 0x90, 0xF5, 0x4C, 0xAC, 0x76, 0x3D, 0xDD, 0xB3, 0x22, 0x47, 0xE5, 0xEB, 0x09, 0x55, 0xAE, 0x31, + 0x92, 0x5E, 0x7A, 0x0A, 0xDC, 0xF8, 0x7D, 0x9C, 0xE2, 0xF4, 0x91, 0x0A, 0x15, 0xC2, 0x1E, 0xEF, + 0x7B, 0xFA, 0xC6, 0x51, 0xED, 0xBA, 0x5B, 0xF2, 0xAF, 0xFE, 0x60, 0xCE, 0x53, 0xCD, 0x7A, 0xB4, + 0x97, 0xB3, 0xFC, 0xB7, 0x16, 0xEB, 0xDA, 0x42, 0x0B, 0xC0, 0x98, 0x2A, 0x0F, 0x18, 0xF9, 0xD0, + 0x14, 0xB1, 0x8C, 0x05, 0x14, 0xDD, 0xA9, 0x61, 0xF8, 0x86, 0x21, 0x69, 0x0A, 0x29, 0xB2, 0xF1, + 0xF9, 0x77, 0x22, 0x46, 0x55, 0xFC, 0xDD, 0x67, 0x8D, 0x7A, 0xFD, 0x70, 0x17, 0x7E, 0xBE, 0x3D, + 0xC0, 0x9F, 0xEF, 0xE8, 0xE7, 0x7B, 0xFC, 0xD9, 0x68, 0x1E, 0xC8, 0xE8, 0xAB, 0x7A, 0x2D, 0x6C, + 0xD4, 0x68, 0xEE, 0xD7, 0x55, 0x18, 0x97, 0xF2, 0x60, 0x87, 0x33, 0x81, 0x92, 0x2A, 0xE0, 0xB7, + 0x94, 0xA6, 0xC2, 0x0F, 0xCB, 0x08, 0x3F, 0xC0, 0xF8, 0xB6, 0x41, 0xFE, 0xC0, 0xE8, 0x0F, 0x26, + 0x34, 0xD3, 0x72, 0x3C, 0x95, 0x0C, 0x41, 0xCD, 0x15, 0xE3, 0xC7, 0xF7, 0xE0, 0x5C, 0x91, 0xBB, + 0x10, 0xCE, 0x68, 0x54, 0x45, 0x98, 0xEA, 0x06, 0x37, 0xEB, 0xF1, 0x64, 0x0B, 0xB1, 0xDA, 0xCB, + 0x8C, 0x4A, 0x2B, 0x23, 0xE7, 0x00, 0x7E, 0xE2, 0x99, 0x86, 0xC1, 0x6D, 0x7C, 0x1F, 0xE7, 0x4A, + 0xD9, 0x56, 0xB0, 0xD2, 0x1F, 0x42, 0x00, 0x4B, 0x1A, 0x6E, 0x97, 0x57, 0xCD, 0xD4, 0x5A, 0xE7, + 0x45, 0x30, 0x7C, 0x4A, 0xE9, 0xF3, 0x91, 0xD3, 0x36, 0x25, 0x28, 0x11, 0x8E, 0x7B, 0x7C, 0x28, + 0x7D, 0xB6, 0x67, 0x9F, 0xE5, 0xC9, 0xA6, 0x5D, 0x07, 0xF0, 0x7D, 0x18, 0xB6, 0x2E, 0xE7, 0x77, + 0x99, 0xED, 0x63, 0x19, 0xCB, 0x6B, 0xE2, 0x71, 0x9F, 0x8B, 0x3B, 0x07, 0x11, 0x45, 0x1D, 0x10, + 0x94, 0x93, 0xF5, 0x16, 0x1F, 0xC3, 0xAE, 0xE7, 0x3A, 0x21, 0xC2, 0x86, 0xF9, 0x5C, 0x2C, 0xF3, + 0xA6, 0x8D, 0x59, 0x1F, 0x30, 0xAE, 0x9D, 0x87, 0xD3, 0x90, 0xC4, 0x10, 0x30, 0x11, 0x40, 0xF2, + 0x9D, 0xF7, 0x0E, 0x83, 0x0E, 0x58, 0xD4, 0x03, 0x50, 0x9D, 0xFD, 0xA3, 0x10, 0x68, 0x66, 0xC3, + 0x06, 0xF1, 0x76, 0x98, 0x3C, 0x8E, 0x2A, 0xCB, 0xF6, 0xAC, 0xCC, 0xF1, 0x38, 0x6B, 0x10, 0x92, + 0x8C, 0xAB, 0xE1, 0x3F, 0x90, 0x52, 0x6C, 0x1F, 0xB8, 0x96, 0x24, 0xB4, 0xD8, 0xF3, 0xF4, 0x11, + 0x1E, 0xA7, 0x30, 0x03, 0x34, 0x37, 0xD3, 0x60, 0x7D, 0xCD, 0xCD, 0xF4, 0x76, 0x0B, 0x37, 0x7B, + 0x82, 0x3F, 0x71, 0xFA, 0xF4, 0x95, 0xE5, 0xC7, 0x69, 0x1A, 0xA1, 0x5B, 0x5D, 0x9E, 0x33, 0x37, + 0x13, 0x34, 0xD9, 0xE7, 0x70, 0x27, 0xD1, 0x79, 0xA1, 0x63, 0x36, 0xD1, 0x34, 0x73, 0xC8, 0x54, + 0x54, 0x8C, 0xED, 0xD8, 0x93, 0xB6, 0x3E, 0x01, 0xEA, 0x40, 0x26, 0x02, 0xC0, 0xC3, 0x38, 0xB6, + 0x7E, 0x3C, 0x7C, 0x2B, 0xF3, 0x9C, 0xCE, 0xD9, 0x78, 0x9D, 0x85, 0x1A, 0x33, 0xF9, 0x6F, 0x62, + 0xE9, 0x19, 0x23, 0x4D, 0x66, 0xB9, 0xAD, 0xF5, 0x38, 0xFD, 0xCA, 0x09, 0x5F, 0x06, 0xA8, 0xE6, + 0x55, 0x5A, 0x0A, 0xDA, 0xF2, 0xE3, 0x2A, 0xFC, 0x7D, 0x93, 0xC3, 0xE7, 0x90, 0x8A, 0x40, 0xF0, + 0x78, 0xBB, 0x30, 0x9F, 0x5F, 0x0E, 0x5C, 0xB3, 0xEB, 0xE0, 0x3E, 0x5C, 0xDD, 0x7C, 0x43, 0x9E, + 0x37, 0xD4, 0x40, 0x0B, 0x87, 0xFF, 0x47, 0x75, 0x89, 0x49, 0x58, 0xA0, 0xEC, 0x4E, 0x98, 0xD8, + 0xC2, 0xD6, 0xEE, 0x41, 0x52, 0xA1, 0x7C, 0x4C, 0xEC, 0x2B, 0x86, 0x14, 0x6A, 0xAE, 0x0B, 0x5C, + 0x97, 0x5C, 0xA5, 0xD1, 0x17, 0x38, 0x00, 0xF6, 0xEE, 0x08, 0x0C, 0x90, 0x45, 0x35, 0x07, 0x84, + 0x17, 0xC2, 0x57, 0x10, 0x5A, 0xBE, 0x63, 0x9D, 0x64, 0x83, 0xF7, 0x41, 0x0A, 0xE2, 0x51, 0xE7, + 0x9A, 0x85, 0x3E, 0xC1, 0x54, 0xD1, 0x1B, 0xE5, 0x11, 0xA0, 0x82, 0x6A, 0xAC, 0x07, 0xD5, 0xB5, + 0xEA, 0xA0, 0xA8, 0xE2, 0xB3, 0x0E, 0x99, 0x65, 0xC5, 0x08, 0x8C, 0xB0, 0x82, 0x7D, 0xA3, 0x9C, + 0x20, 0x27, 0x96, 0xD9, 0xB7, 0x11, 0x09, 0x2A, 0xAD, 0x13, 0x78, 0x4C, 0xFB, 0x49, 0x76, 0x15, + 0x16, 0x7D, 0xB3, 0x5C, 0x74, 0x9E, 0x30, 0x99, 0x15, 0xC9, 0xF9, 0xB8, 0x66, 0xCC, 0x6B, 0xA0, + 0x53, 0xDE, 0x15, 0x13, 0x84, 0x5D, 0x61, 0xF6, 0x9E, 0xD8, 0x00, 0x8E, 0x70, 0x99, 0x98, 0x89, + 0xF4, 0x75, 0x46, 0x25, 0xBC, 0x03, 0xCA, 0xC9, 0x02, 0xF8, 0x36, 0x2A, 0x8E, 0xAD, 0xC2, 0xB4, + 0xEF, 0x39, 0xA8, 0xF3, 0x16, 0xFF, 0xC7, 0xDF, 0xFF, 0xDB, 0x67, 0x3D, 0x0F, 0x03, 0xBA, 0x41, + 0x25, 0x88, 0xDE, 0x8A, 0xE1, 0xA0, 0x0C, 0x0E, 0x30, 0xB6, 0x51, 0x6C, 0xCE, 0x6D, 0x43, 0x8D, + 0xDB, 0x4F, 0xCD, 0x61, 0xF0, 0xB3, 0xF6, 0x50, 0xC9, 0x0E, 0xE5, 0x1E, 0x2F, 0xCE, 0xA9, 0x3D, + 0xB0, 0x13, 0x1B, 0x0E, 0xDE, 0xE3, 0xFC, 0xE0, 0x99, 0x8A, 0x01, 0x31, 0xA6, 0x85, 0x61, 0x05, + 0x3D, 0xAA, 0x8F, 0x8E, 0x18, 0x2E, 0xD3, 0xE1, 0x68, 0x38, 0x16, 0xE9, 0x58, 0x4F, 0xDA, 0x43, + 0xAD, 0xA8, 0x6D, 0x68, 0x51, 0xC6, 0x90, 0xD9, 0x9B, 0x37, 0x8B, 0xFC, 0xCA, 0x06, 0xCB, 0x17, + 0xBB, 0xD7, 0x56, 0x7B, 0x39, 0xA3, 0x4B, 0x37, 0xF6, 0x6E, 0x59, 0x8B, 0xEC, 0xA2, 0xE2, 0x7C, + 0x4A, 0x60, 0x6B, 0xDB, 0x14, 0xFA, 0x20, 0x2F, 0xBE, 0xD2, 0xCB, 0x6B, 0xC2, 0x58, 0x17, 0xC7, + 0x7E, 0xC1, 0xD9, 0xD9, 0x38, 0x2B, 0x77, 0x34, 0x2F, 0xD6, 0xD2, 0xDB, 0x5B, 0x88, 0xB7, 0xB7, + 0x20, 0x46, 0xE7, 0x45, 0x5B, 0x7C, 0x77, 0x4D, 0x58, 0x0B, 0x1B, 0x63, 0xBD, 0x20, 0xED, 0x6C, + 0xA4, 0xA5, 0xED, 0xCC, 0x8B, 0xB3, 0xF8, 0xF2, 0xBA, 0x51, 0x76, 0x71, 0xC2, 0xA9, 0xDF, 0x53, + 0x82, 0xFA, 0xF7, 0x01, 0xE7, 0xD6, 0xB9, 0xA9, 0xC2, 0xFD, 0x2A, 0x2D, 0xF5, 0x98, 0xD1, 0x73, + 0x76, 0x67, 0xC2, 0x78, 0xD1, 0xB7, 0xAF, 0xA6, 0x1B, 0xA8, 0x84, 0x7C, 0x6B, 0xB9, 0x82, 0xEC, + 0xC4, 0x89, 0xAF, 0x57, 0x94, 0x45, 0xF9, 0xF4, 0x81, 0x40, 0x26, 0x10, 0x64, 0x46, 0x38, 0x2F, + 0x15, 0x3A, 0x49, 0x1A, 0xDA, 0x80, 0x6B, 0x98, 0x78, 0x28, 0x26, 0xA8, 0x5E, 0x92, 0x9D, 0x62, + 0x5B, 0xC5, 0xD4, 0x45, 0x62, 0xE3, 0x99, 0x33, 0xEC, 0x02, 0x31, 0xD1, 0xA6, 0x22, 0xD2, 0xF9, + 0x95, 0x96, 0x7A, 0x14, 0xC3, 0x44, 0x7F, 0xD9, 0xA8, 0x95, 0x31, 0x8B, 0x57, 0x0B, 0xE5, 0xDB, + 0x98, 0x6A, 0xAA, 0x4B, 0x7C, 0xDB, 0xE3, 0x9A, 0x17, 0xC3, 0x18, 0x3F, 0x7E, 0xF1, 0x08, 0xC8, + 0x34, 0x15, 0x25, 0x10, 0xA5, 0x70, 0xEF, 0xA1, 0xDD, 0x36, 0x22, 0x53, 0x39, 0xA3, 0xA6, 0x74, + 0xEA, 0x69, 0x73, 0xAF, 0x87, 0x89, 0x62, 0xD6, 0x6D, 0xDB, 0x74, 0xDD, 0x25, 0xD9, 0x35, 0xDB, + 0xED, 0x52, 0x36, 0xCD, 0xA8, 0xD9, 0x9C, 0xF6, 0x4C, 0xE9, 0xE7, 0x91, 0x00, 0x72, 0xCA, 0xAA, + 0xE9, 0xC6, 0xDE, 0xD8, 0x28, 0xDB, 0x66, 0x04, 0x82, 0x82, 0x76, 0xCD, 0x12, 0x37, 0xD7, 0xE7, + 0x14, 0x3B, 0xCF, 0xAE, 0xCE, 0x8F, 0x43, 0x01, 0x61, 0x00, 0x1D, 0x3E, 0xC0, 0x62, 0xAF, 0xCE, + 0xA7, 0xE4, 0x54, 0xDA, 0x3B, 0x41, 0x29, 0x61, 0x6A, 0x52, 0xA4, 0x73, 0xCC, 0xBD, 0x0E, 0xD2, + 0x9C, 0x69, 0xB1, 0x1F, 0x1C, 0xFE, 0x84, 0xFD, 0x87, 0xA3, 0x24, 0x1A, 0xC6, 0x80, 0xAF, 0x6F, + 0x39, 0x9A, 0x01, 0x81, 0x05, 0x4C, 0x19, 0xC9, 0x61, 0x7F, 0x81, 0xD6, 0x13, 0x65, 0x92, 0xB5, + 0xE6, 0x71, 0x88, 0xA3, 0xD8, 0x59, 0x94, 0x68, 0xC0, 0x8F, 0x72, 0x3A, 0xC4, 0xBF, 0x9F, 0xCA, + 0xCC, 0x62, 0x8D, 0x57, 0x61, 0x0B, 0x9E, 0x38, 0xEF, 0x15, 0xE6, 0x81, 0x50, 0xD9, 0x80, 0x13, + 0x54, 0x3A, 0x4A, 0xD6, 0xE0, 0x4B, 0x73, 0x70, 0xCC, 0x08, 0xBC, 0xD5, 0x26, 0x31, 0x55, 0x36, + 0xB4, 0xEF, 0x69, 0xEE, 0xC0, 0xD4, 0x6F, 0x79, 0x1F, 0x96, 0x78, 0xEE, 0x39, 0xAE, 0x64, 0x27, + 0xB9, 0x74, 0xB6, 0x74, 0x73, 0xAA, 0x6E, 0xAA, 0x9E, 0x30, 0xF9, 0x68, 0xB6, 0xF7, 0x5E, 0x32, + 0x39, 0xD9, 0x58, 0x97, 0x99, 0xF3, 0x4C, 0x2A, 0x1E, 0x86, 0x9A, 0x75, 0xAE, 0x2A, 0xA7, 0xC9, + 0x14, 0x58, 0xF5, 0x4A, 0xEB, 0x5B, 0x27, 0x5F, 0x5A, 0xAA, 0xF1, 0x3C, 0x64, 0x17, 0xDF, 0x66, + 0xB7, 0x9C, 0x9D, 0xC4, 0x6A, 0xB1, 0x68, 0xDC, 0x91, 0xC0, 0x7C, 0x72, 0x02, 0x8F, 0x8D, 0xA0, + 0x06, 0xF2, 0x50, 0x9F, 0x3C, 0xA8, 0xC8, 0x3F, 0x43, 0x2A, 0xB7, 0x2A, 0xF1, 0xB7, 0x72, 0xFF, + 0x50, 0xA8, 0xDF, 0xF3, 0xF8, 0x6F, 0x01, 0xB7, 0xF5, 0xA7, 0x18, 0x72, 0x7F, 0xEB, 0x6C, 0x12, + 0x5E, 0x4B, 0x91, 0xC2, 0x9B, 0x03, 0xE9, 0x5D, 0xB7, 0xC3, 0x05, 0xE6, 0x6C, 0xF1, 0x27, 0x9E, + 0x68, 0xF3, 0x71, 0xEB, 0x42, 0x77, 0x18, 0x70, 0x92, 0xDC, 0x72, 0x9B, 0x3F, 0x68, 0x96, 0xBA, + 0xBC, 0x80, 0x07, 0x4C, 0x3D, 0xC9, 0x99, 0x0F, 0x71, 0xDE, 0x9B, 0x8B, 0xF8, 0x14, 0x16, 0x64, + 0x4C, 0x99, 0x69, 0x1C, 0x51, 0xE8, 0xFA, 0x1D, 0x39, 0x2A, 0x1D, 0xC5, 0x48, 0xC4, 0x0C, 0xD6, + 0xEA, 0x3C, 0xB0, 0xE6, 0x3B, 0x86, 0x27, 0x31, 0x5E, 0x4C, 0xD0, 0x01, 0xFD, 0xB4, 0x2B, 0xEF, + 0xD6, 0x22, 0xBD, 0x50, 0x13, 0x82, 0x0F, 0x5D, 0x4A, 0x30, 0xAB, 0xFC, 0x0D, 0xF1, 0xCF, 0xD7, + 0x9F, 0x9D, 0x21, 0x7F, 0x4D, 0x7E, 0x86, 0xBB, 0xCC, 0xC1, 0x9B, 0x0D, 0x0D, 0xFB, 0xD9, 0xC5, + 0xEF, 0x3C, 0x5C, 0x1F, 0x0D, 0x54, 0x98, 0x6B, 0xAF, 0xD1, 0x94, 0xB3, 0x02, 0xE7, 0xF0, 0xB8, + 0xBC, 0x29, 0xE5, 0xAE, 0xB6, 0xE7, 0xF4, 0x4C, 0x8B, 0xDF, 0x39, 0x3F, 0xB8, 0xBD, 0x80, 0x78, + 0x94, 0xF1, 0x34, 0xC6, 0x4A, 0xBC, 0x53, 0xE3, 0x30, 0x1A, 0xE8, 0x78, 0x4D, 0x2E, 0xAD, 0x57, + 0x3D, 0xC9, 0x2A, 0x1D, 0xAF, 0xAF, 0xD9, 0xE6, 0xEF, 0x52, 0x14, 0x1F, 0x68, 0x3E, 0x26, 0x76, + 0x72, 0x3C, 0xD7, 0x21, 0x3F, 0x32, 0x60, 0x9A, 0x76, 0xBF, 0x23, 0x40, 0xA7, 0x1C, 0xB2, 0x31, + 0xF9, 0x40, 0xD3, 0x75, 0xB4, 0x0F, 0xEE, 0x4A, 0x4F, 0x55, 0x93, 0x84, 0x04, 0x8F, 0x93, 0xE7, + 0x2B, 0x52, 0x25, 0xF5, 0xAE, 0x07, 0x3E, 0xA0, 0xAF, 0xCA, 0x09, 0x85, 0xF7, 0x71, 0xB4, 0x70, + 0x81, 0x0B, 0x8F, 0xA3, 0x23, 0xA0, 0xF4, 0x13, 0xAB, 0xB2, 0xF0, 0x76, 0xBA, 0xE3, 0x6A, 0xDE, + 0x8F, 0xCB, 0xC0, 0x96, 0x10, 0xDA, 0x0E, 0x57, 0xD7, 0x79, 0x22, 0x42, 0xDE, 0x2E, 0xD8, 0x53, + 0x7F, 0x06, 0x6A, 0xE7, 0xF6, 0xD6, 0x9F, 0xDE, 0xCF, 0xF2, 0x3D, 0xF6, 0x97, 0xEE, 0xD0, 0x84, + 0x81, 0x99, 0xFE, 0x5A, 0x75, 0xFE, 0x45, 0x6B, 0xF9, 0xB8, 0xA0, 0x72, 0x8A, 0x7E, 0xAC, 0xE5, + 0x24, 0x15, 0x7E, 0x04, 0xAC, 0x31, 0xDD, 0x1D, 0x1B, 0x6F, 0x94, 0xD2, 0x3E, 0x75, 0x39, 0xE5, + 0xF4, 0x76, 0x1A, 0x80, 0xFC, 0x79, 0xB1, 0xF7, 0xB3, 0x81, 0x06, 0x07, 0xB0, 0x15, 0x29, 0x0E, + 0x61, 0x7F, 0xB3, 0xD2, 0xBA, 0xC5, 0x8E, 0xA0, 0xA8, 0xAF, 0x53, 0x2D, 0x80, 0xC3, 0x98, 0x0A, + 0xD8, 0xC9, 0xB8, 0x68, 0x7C, 0x40, 0x1E, 0xD8, 0xC7, 0xAC, 0xA0, 0x1A, 0x91, 0xEC, 0x33, 0x39, + 0x65, 0xF9, 0x28, 0x4B, 0x81, 0x28, 0xA6, 0x3B, 0x1C, 0xBC, 0xAB, 0x83, 0xFA, 0x80, 0x3F, 0x4B, + 0x29, 0x10, 0xEF, 0x8F, 0xB0, 0x39, 0xFE, 0x2C, 0xA7, 0x7F, 0xBC, 0x6F, 0x62, 0x7B, 0xFA, 0x55, + 0xAA, 0x83, 0xFD, 0x77, 0x07, 0xD8, 0x01, 0xFD, 0x2A, 0xD5, 0xC1, 0xE1, 0x5B, 0x5A, 0x01, 0xFD, + 0x2A, 0xB7, 0x84, 0xC6, 0xA1, 0x5C, 0x03, 0xFD, 0x2E, 0xD5, 0x45, 0x73, 0xBF, 0x4E, 0xAB, 0x90, + 0xBF, 0x4B, 0x75, 0x71, 0x70, 0x54, 0x97, 0x1B, 0x49, 0xBF, 0xCB, 0x6D, 0x65, 0xB3, 0x21, 0x37, + 0x93, 0x7E, 0x6F, 0x9E, 0x52, 0x78, 0x47, 0x1E, 0xFF, 0x40, 0x4B, 0x24, 0xCE, 0xE0, 0x1D, 0x26, + 0xCA, 0x17, 0xB7, 0x27, 0xE7, 0x57, 0x5F, 0x19, 0x32, 0xAD, 0xD0, 0x63, 0x5F, 0x07, 0x62, 0xEE, + 0x4B, 0x3F, 0x7C, 0x10, 0xB6, 0x75, 0xAC, 0x3F, 0x8A, 0xD5, 0x54, 0x30, 0xF5, 0xE5, 0xC0, 0x41, + 0xD1, 0x05, 0x3D, 0x82, 0x90, 0x8C, 0x62, 0x52, 0x0B, 0x6D, 0x7F, 0xD7, 0xF5, 0x9F, 0x97, 0x8E, + 0x88, 0xE0, 0x5E, 0x10, 0x7B, 0x4B, 0x75, 0x55, 0x69, 0xDD, 0x04, 0x8F, 0x4C, 0x7D, 0x28, 0xCC, + 0xD7, 0xD2, 0x9D, 0x65, 0x4D, 0x76, 0x11, 0xBC, 0x0D, 0x90, 0xF9, 0xCB, 0xCD, 0xC5, 0x49, 0x59, + 0xCB, 0x48, 0xBB, 0xDD, 0xD9, 0xBB, 0xB8, 0xC7, 0x40, 0x8C, 0x3B, 0xC0, 0xD2, 0x3E, 0x66, 0x8F, + 0x2D, 0x43, 0xDC, 0x72, 0x79, 0xC3, 0xE0, 0x51, 0xAD, 0x0C, 0x40, 0x7E, 0xD5, 0x3C, 0x2B, 0xC7, + 0xED, 0x40, 0xBB, 0x3E, 0x3F, 0xDB, 0x3B, 0x3F, 0x39, 0xDB, 0x4C, 0xF2, 0x3C, 0x3F, 0xB9, 0x3B, + 0x49, 0xD2, 0xA2, 0x92, 0x30, 0x54, 0x00, 0x8D, 0x61, 0xF6, 0xC8, 0x13, 0x4F, 0xA4, 0x3C, 0x43, + 0x71, 0x97, 0x64, 0xD5, 0x35, 0xF5, 0x22, 0xD0, 0xAB, 0xA9, 0x59, 0xEC, 0xEE, 0xA7, 0xBD, 0xDB, + 0x9F, 0x90, 0x5E, 0x2D, 0x95, 0xBC, 0x3E, 0x5E, 0xFE, 0xA8, 0xC6, 0x70, 0x83, 0xD4, 0xD6, 0x24, + 0x5A, 0xBB, 0x01, 0x48, 0x47, 0xCC, 0xE5, 0xD8, 0x0F, 0xCC, 0xC0, 0x60, 0x4E, 0x20, 0x50, 0xE0, + 0x46, 0xB5, 0xE7, 0xD0, 0xF6, 0x95, 0xC7, 0x2A, 0x30, 0x11, 0x11, 0x36, 0x46, 0x79, 0x1C, 0xE6, + 0x01, 0xFB, 0x12, 0x75, 0x84, 0xEA, 0x93, 0x4D, 0x75, 0x60, 0x94, 0x9F, 0x6B, 0xE7, 0xFC, 0x64, + 0xAF, 0x73, 0x76, 0x2D, 0xB3, 0xDD, 0xEA, 0x72, 0x2F, 0x6B, 0xF2, 0xE6, 0x71, 0xEF, 0xDC, 0xF4, + 0xE4, 0x3D, 0x78, 0x7C, 0x1A, 0x09, 0x57, 0x5A, 0x01, 0xA2, 0xB7, 0xA6, 0xD3, 0xD5, 0xAE, 0xDA, + 0xC2, 0xC4, 0xBB, 0x4A, 0xB9, 0xEA, 0x99, 0xDE, 0x10, 0xAF, 0x08, 0x26, 0x79, 0xCE, 0x22, 0xA0, + 0xB6, 0xCE, 0x3E, 0x1B, 0x52, 0x36, 0x8A, 0x2C, 0x8B, 0xE0, 0x41, 0x52, 0xC2, 0x3A, 0xC7, 0x6D, + 0x98, 0x53, 0xC0, 0x4A, 0xF4, 0x38, 0x36, 0xD7, 0x17, 0xF1, 0xEA, 0x45, 0xBC, 0xFA, 0xE3, 0x88, + 0x57, 0x57, 0xB6, 0x64, 0xC3, 0x18, 0x4D, 0xB8, 0xAB, 0xC2, 0x2B, 0xD3, 0xC2, 0xD6, 0x38, 0x7F, + 0x9F, 0x28, 0x6B, 0xC5, 0x18, 0x58, 0x8C, 0x7F, 0x49, 0xF8, 0x6D, 0x98, 0xA0, 0x95, 0x8B, 0x89, + 0x61, 0x15, 0x60, 0x0F, 0x8E, 0x81, 0x36, 0x9E, 0x2D, 0x8B, 0x4A, 0x03, 0xBE, 0x58, 0x43, 0xBC, + 0xBC, 0x82, 0xBC, 0x88, 0x4F, 0x34, 0xBA, 0x30, 0x0D, 0x9F, 0x32, 0x7A, 0xBC, 0x7C, 0x8B, 0x7C, + 0xE6, 0x5C, 0x56, 0x6C, 0x96, 0xFF, 0xC4, 0x6D, 0x4E, 0xE8, 0xAB, 0xC9, 0x55, 0xA3, 0x63, 0x08, + 0xEB, 0x28, 0x89, 0x00, 0x8E, 0x67, 0x78, 0x6F, 0xA8, 0x82, 0x88, 0x87, 0x01, 0xE0, 0x6E, 0x8F, + 0x72, 0xEA, 0xEC, 0xCA, 0xF8, 0x5E, 0xD8, 0x6F, 0x8C, 0xC2, 0x8D, 0xA2, 0x5B, 0x6A, 0xEC, 0x96, + 0xFF, 0x16, 0x98, 0x1E, 0xF7, 0x47, 0x85, 0xD2, 0xD3, 0x01, 0xFD, 0xCA, 0x14, 0x2F, 0x3B, 0x68, + 0xD4, 0xEB, 0xBB, 0xF5, 0x7A, 0x3D, 0xF0, 0x95, 0x38, 0xA2, 0xEA, 0x4C, 0xA3, 0x0C, 0xF2, 0x3E, + 0xFA, 0x4A, 0xCE, 0x80, 0x8B, 0x07, 0xCE, 0x6D, 0xF9, 0x9E, 0x5F, 0xDB, 0x6A, 0x4B, 0xFE, 0x04, + 0x52, 0x39, 0xE7, 0x18, 0x1E, 0xED, 0x17, 0xA6, 0x98, 0xB9, 0x6E, 0x08, 0xD2, 0xF2, 0x43, 0x62, + 0x5E, 0x77, 0x00, 0xF9, 0x53, 0x09, 0x78, 0xFA, 0xFC, 0x6B, 0xE0, 0xE7, 0xB3, 0xB9, 0x97, 0xB8, + 0x2B, 0xB8, 0x1B, 0xDF, 0x65, 0xB6, 0xF3, 0xFF, 0xFE, 0xAF, 0x9F, 0xA3, 0xC6, 0xE9, 0x3C, 0x75, + 0xFA, 0xF2, 0x1B, 0xA4, 0x8B, 0x7B, 0xC6, 0xCE, 0x84, 0x65, 0xA1, 0xCA, 0x7D, 0xB3, 0x7A, 0x7B, + 0x6E, 0x65, 0x72, 0x13, 0xEB, 0xBD, 0x26, 0xC6, 0xB0, 0x4C, 0xFC, 0x6B, 0xC7, 0x19, 0x90, 0x44, + 0xBC, 0xAD, 0xC0, 0xAB, 0x11, 0x64, 0xCA, 0x63, 0x53, 0xD4, 0xC7, 0x26, 0xE1, 0x10, 0xD9, 0xD9, + 0x71, 0x76, 0x17, 0x46, 0x9F, 0x17, 0x55, 0x89, 0x66, 0x22, 0x54, 0xDB, 0xB1, 0x34, 0xCF, 0x14, + 0x4F, 0x4B, 0xC6, 0xA7, 0x70, 0x98, 0xE3, 0x02, 0x4C, 0x2A, 0xA1, 0x7D, 0x65, 0x4F, 0x7A, 0x7C, + 0x0B, 0x47, 0x5F, 0x95, 0xD0, 0xC7, 0x26, 0x9B, 0x86, 0x6E, 0x4D, 0x72, 0xBB, 0xC6, 0x2D, 0x28, + 0x58, 0xD2, 0x6D, 0xCC, 0x52, 0x74, 0xA9, 0x59, 0x56, 0xF1, 0xBE, 0x0A, 0x94, 0x86, 0x5B, 0xCA, + 0xB5, 0x31, 0x5A, 0x6C, 0xA4, 0x6C, 0xE2, 0x9B, 0x7D, 0x94, 0x0D, 0x1F, 0x30, 0xC2, 0x17, 0xA4, + 0xFA, 0x01, 0x74, 0xA5, 0xDC, 0xC4, 0x74, 0x32, 0x5F, 0xEC, 0x78, 0x12, 0x56, 0x1C, 0xD6, 0xF7, + 0x46, 0x56, 0xCC, 0x7A, 0x48, 0xBE, 0xD0, 0x53, 0x10, 0xA0, 0x37, 0x62, 0x22, 0x51, 0x0C, 0xC8, + 0xCF, 0x26, 0xCD, 0xD1, 0xAB, 0xCD, 0x10, 0xF0, 0x3F, 0x2B, 0x77, 0x52, 0x32, 0x51, 0x5E, 0x3B, + 0xFD, 0x3E, 0xBA, 0x7B, 0xA6, 0xE5, 0xFD, 0x99, 0x50, 0x91, 0x06, 0x4E, 0xD5, 0x7C, 0xA9, 0x7A, + 0xC1, 0xCC, 0xA9, 0x8C, 0xEB, 0x0D, 0xD9, 0x4B, 0x5C, 0xB1, 0x1A, 0x01, 0xE3, 0xB2, 0x90, 0x1F, + 0x45, 0xD2, 0x1B, 0xC7, 0x19, 0xF9, 0x6F, 0xA4, 0xA8, 0xBF, 0x5F, 0x8F, 0x8C, 0x8A, 0xFA, 0x13, + 0xBB, 0xBD, 0xE9, 0xD4, 0xD0, 0xFD, 0xA1, 0x17, 0x58, 0x4A, 0x71, 0x46, 0x2F, 0x2D, 0x66, 0x99, + 0x3F, 0x40, 0x9B, 0x08, 0xF0, 0x5A, 0x50, 0x78, 0x26, 0xA8, 0xCD, 0x81, 0xA5, 0xB4, 0xE3, 0x0E, + 0x97, 0xAE, 0x12, 0x28, 0xF8, 0x50, 0x21, 0x78, 0x5F, 0xC5, 0x85, 0xA8, 0xF2, 0xF1, 0xBF, 0x5C, + 0x9C, 0x57, 0x2F, 0xDF, 0xB7, 0xD9, 0x95, 0x2D, 0x30, 0xFA, 0x9E, 0xBE, 0xBA, 0xD1, 0xEC, 0x40, + 0x93, 0xFD, 0x0F, 0xB1, 0x5E, 0x75, 0xC2, 0x2E, 0x3A, 0x5B, 0x53, 0x29, 0x90, 0x7A, 0x6C, 0x9D, + 0xBA, 0xC6, 0xC6, 0xF9, 0x25, 0x7F, 0x3B, 0xB9, 0xBD, 0x6B, 0x7E, 0x3B, 0xFD, 0xE9, 0xCA, 0x8E, + 0x28, 0x0D, 0x3E, 0xC1, 0xCE, 0x20, 0x45, 0x7C, 0x9D, 0x1E, 0xFC, 0x44, 0x9B, 0x41, 0x1D, 0xAC, + 0xC2, 0x13, 0x39, 0x3E, 0xD3, 0x15, 0x59, 0x90, 0x62, 0x00, 0x91, 0xF3, 0x77, 0xEC, 0xD8, 0xA2, + 0x59, 0x95, 0xF1, 0x5A, 0xBF, 0x36, 0x56, 0x37, 0x0E, 0xEB, 0x2E, 0xA2, 0xA5, 0x3D, 0x22, 0xB1, + 0x2F, 0x17, 0x5F, 0xAB, 0xE7, 0xEF, 0xCF, 0xB6, 0x20, 0x6A, 0x65, 0x31, 0x8E, 0x2B, 0x94, 0x52, + 0xF0, 0x19, 0xF9, 0xAD, 0xE0, 0x7A, 0x4A, 0xB9, 0xAD, 0xC4, 0x1A, 0x66, 0x83, 0x3D, 0x06, 0xA9, + 0x94, 0xD3, 0xCA, 0x83, 0xD9, 0x33, 0x37, 0xCA, 0x67, 0x65, 0xEA, 0x5A, 0x4A, 0xBA, 0xAC, 0x8C, + 0x6B, 0x9E, 0xB0, 0x95, 0xD3, 0x34, 0x85, 0x2F, 0xA0, 0x45, 0x3B, 0xDE, 0x0F, 0xFF, 0x78, 0x65, + 0x0C, 0x00, 0x48, 0x98, 0xE9, 0x1E, 0xA7, 0x88, 0x47, 0xD8, 0x6B, 0x3A, 0x9F, 0x60, 0x9E, 0xC2, + 0x61, 0x07, 0x72, 0xF7, 0x6C, 0x35, 0xA5, 0x1A, 0x3B, 0x01, 0x59, 0x33, 0xFC, 0x14, 0x49, 0x9E, + 0x70, 0x32, 0xE2, 0xD5, 0x9B, 0xAD, 0x2A, 0x56, 0x72, 0x5F, 0x84, 0xEF, 0x8C, 0x22, 0x16, 0xA2, + 0x97, 0xF1, 0xF2, 0xAC, 0xF6, 0x87, 0x49, 0xE0, 0x81, 0x28, 0xAE, 0x36, 0xB4, 0xDE, 0xE9, 0x60, + 0x0C, 0x51, 0x4C, 0x2B, 0xDF, 0x1F, 0xD3, 0xD1, 0xF0, 0x95, 0x3C, 0xA5, 0x63, 0x4A, 0xEB, 0xF7, + 0x45, 0x9C, 0x18, 0xC7, 0xE7, 0xAE, 0x4E, 0x2C, 0x01, 0x3B, 0x9C, 0x2F, 0xBA, 0x3A, 0xDD, 0xC5, + 0xB6, 0x65, 0x06, 0x88, 0xCF, 0xBF, 0x0D, 0xDD, 0xC0, 0x1F, 0xC6, 0x8C, 0x2D, 0x6C, 0x7F, 0x9F, + 0x0A, 0x98, 0x4D, 0xDC, 0xDC, 0x68, 0x65, 0x85, 0xF7, 0x34, 0x6C, 0xB9, 0xC5, 0xFB, 0xDA, 0xC8, + 0x4B, 0x96, 0xCD, 0xCD, 0xDB, 0x39, 0x39, 0xF7, 0xA2, 0xBB, 0xD6, 0xD8, 0x72, 0x4A, 0x6C, 0x2C, + 0x86, 0x12, 0x37, 0x71, 0x3F, 0x4B, 0x53, 0x62, 0xE3, 0x19, 0x50, 0x62, 0x33, 0x2F, 0x25, 0xEE, + 0x6F, 0xDE, 0xCE, 0x35, 0x4B, 0x51, 0x62, 0x73, 0xCB, 0x29, 0xB1, 0xB9, 0x18, 0x4A, 0xDC, 0xC4, + 0xFD, 0x2C, 0x4D, 0x89, 0xCD, 0x67, 0x40, 0x89, 0xFB, 0x79, 0x29, 0xF1, 0x60, 0xF3, 0x76, 0x6E, + 0xBF, 0x14, 0x25, 0xEE, 0x6F, 0x39, 0x25, 0xEE, 0x2F, 0x86, 0x12, 0x37, 0x71, 0x3F, 0x4B, 0x53, + 0xE2, 0xFE, 0x3A, 0x29, 0x31, 0xB2, 0x6F, 0x90, 0x39, 0xE1, 0x0E, 0x16, 0x3D, 0x7E, 0x63, 0x98, + 0xCB, 0xC0, 0x19, 0xED, 0xF4, 0xA8, 0x27, 0xCC, 0xA0, 0xA3, 0x3C, 0x74, 0xD9, 0x8D, 0x63, 0x14, + 0xF7, 0xA0, 0x4C, 0x75, 0x97, 0x9A, 0x2C, 0x56, 0x52, 0x39, 0x69, 0x2F, 0xC2, 0x8F, 0xB2, 0x51, + 0x69, 0x9D, 0xB4, 0x4B, 0xF9, 0xDD, 0xD5, 0x2B, 0x2D, 0xB4, 0x3C, 0x6C, 0xA4, 0xC3, 0xDD, 0x49, + 0x3B, 0xE6, 0x6E, 0xA7, 0x02, 0x2B, 0xA5, 0x75, 0xC3, 0x73, 0x34, 0x43, 0xD7, 0x7C, 0x11, 0xBA, + 0x21, 0xEB, 0x3A, 0xF7, 0x7D, 0x46, 0x41, 0x7C, 0x0C, 0xC3, 0x87, 0xB9, 0x81, 0x55, 0x4C, 0xAA, + 0x12, 0xCC, 0x35, 0x06, 0x7D, 0x91, 0x79, 0x25, 0xBB, 0xB7, 0x58, 0x78, 0x31, 0x3A, 0x52, 0x5B, + 0xF2, 0x5D, 0xB4, 0xB2, 0xC4, 0x5C, 0xB4, 0xC9, 0xB4, 0x82, 0xA9, 0xDA, 0xB1, 0xB1, 0x4C, 0x16, + 0x82, 0xA9, 0xE3, 0xDA, 0xE8, 0x02, 0xED, 0xC1, 0xE0, 0xB5, 0xE7, 0x9B, 0x36, 0xE8, 0xEE, 0xAC, + 0xBD, 0xF7, 0xED, 0xBC, 0xFD, 0x9C, 0x8C, 0xB0, 0xB0, 0x24, 0x58, 0x51, 0x29, 0x33, 0x6C, 0xA2, + 0x69, 0x36, 0xF8, 0x93, 0x00, 0x4B, 0xD9, 0x62, 0x85, 0xEE, 0x7E, 0x33, 0xDC, 0x8D, 0xB2, 0xC6, + 0xCE, 0x58, 0xD2, 0x02, 0xEC, 0xB1, 0x0B, 0x4F, 0x80, 0x73, 0x2F, 0xCE, 0x2C, 0x93, 0xF2, 0x2F, + 0xC3, 0xE4, 0x99, 0xFC, 0x7B, 0x25, 0x19, 0x6C, 0xA2, 0x81, 0xA7, 0x52, 0x7B, 0x54, 0xAA, 0x01, + 0x76, 0x5B, 0xBE, 0x7F, 0xEA, 0x3C, 0x72, 0x2A, 0x59, 0xB1, 0x2A, 0xF6, 0xD9, 0x93, 0xF5, 0x95, + 0xB8, 0xB1, 0x3B, 0x89, 0xD9, 0x45, 0x6C, 0xCE, 0x0D, 0xFC, 0x81, 0xF4, 0x6E, 0xC6, 0x52, 0x2A, + 0xC4, 0xE2, 0xFA, 0xE6, 0x3D, 0xB7, 0x11, 0x91, 0xC9, 0x9F, 0xB9, 0xB6, 0xAD, 0xA9, 0xC8, 0x73, + 0x09, 0x7A, 0x6E, 0xD6, 0xFE, 0x96, 0x11, 0x65, 0x72, 0xC9, 0x2A, 0x11, 0x46, 0x94, 0x4F, 0x50, + 0x52, 0x2A, 0x07, 0x44, 0xB8, 0x30, 0x2C, 0xF3, 0x5C, 0x29, 0xE3, 0xD6, 0x44, 0x85, 0xB0, 0xE1, + 0xC1, 0xCC, 0x2B, 0xF1, 0x11, 0x45, 0x1E, 0xAF, 0xD1, 0x75, 0xC7, 0xA6, 0xEC, 0xE6, 0x5E, 0x78, + 0x3E, 0x87, 0xDE, 0x02, 0x03, 0x5C, 0x85, 0x2C, 0x6A, 0x46, 0x4E, 0x05, 0x30, 0x57, 0x9D, 0xE6, + 0x2A, 0xAF, 0x5B, 0xC3, 0xCB, 0x13, 0x24, 0x09, 0x72, 0xDC, 0xAF, 0xBD, 0xE4, 0x64, 0x28, 0xAA, + 0x53, 0xA4, 0x50, 0x2D, 0xFF, 0x5D, 0x46, 0x8C, 0x1A, 0x13, 0x7D, 0x6C, 0x7B, 0x71, 0xCE, 0x68, + 0x31, 0x18, 0x3A, 0x55, 0x8A, 0xF6, 0xB0, 0xE1, 0x22, 0x89, 0x29, 0x22, 0x14, 0xC5, 0xE0, 0xF1, + 0x38, 0x08, 0xFC, 0x78, 0xB6, 0xB3, 0xE6, 0xFB, 0x83, 0xB7, 0x2F, 0xA8, 0x5F, 0x1A, 0xF5, 0x69, + 0xA7, 0x8B, 0x63, 0x3B, 0x36, 0x5B, 0x6B, 0xB2, 0x91, 0xB5, 0x89, 0x73, 0x61, 0x25, 0x51, 0xC4, + 0x48, 0xF9, 0xF7, 0x8A, 0xC4, 0x39, 0x35, 0x70, 0x6E, 0x71, 0x4E, 0xBE, 0xBF, 0x29, 0xE2, 0x1C, + 0x55, 0x65, 0x65, 0xA6, 0xDD, 0x75, 0x02, 0x10, 0xE4, 0xE8, 0x34, 0x53, 0xC5, 0x39, 0x31, 0x97, + 0x61, 0x52, 0xB8, 0x93, 0x1E, 0xA9, 0xE1, 0x71, 0x87, 0x35, 0xFC, 0xE4, 0xAB, 0x9B, 0xE9, 0x26, + 0xB0, 0x34, 0xF9, 0x4E, 0x6E, 0xE0, 0x32, 0xE5, 0x3B, 0x39, 0xC2, 0xCA, 0xE5, 0x3B, 0x39, 0xEC, + 0xCB, 0x19, 0xF3, 0xFC, 0xCF, 0x98, 0xD8, 0x4E, 0x17, 0x39, 0x63, 0x46, 0xCD, 0xFE, 0x98, 0x67, + 0xCC, 0x37, 0xC3, 0x0D, 0x8F, 0x19, 0x34, 0xCF, 0xAC, 0xF4, 0x98, 0x19, 0x8D, 0x9D, 0xEF, 0xA4, + 0x09, 0x0C, 0x77, 0x53, 0xCE, 0x98, 0x91, 0xB5, 0x95, 0x8E, 0x11, 0x1F, 0xBA, 0xC5, 0x6A, 0x51, + 0xB2, 0xF0, 0x2A, 0x03, 0x48, 0xFE, 0x91, 0x4E, 0x8F, 0x68, 0x1F, 0x97, 0x76, 0x80, 0x04, 0x86, + 0xBB, 0xF2, 0xA3, 0x23, 0x5A, 0xD5, 0xC6, 0x9D, 0x1E, 0x48, 0xA8, 0xD9, 0xA7, 0x47, 0xA3, 0xDE, + 0x68, 0xD4, 0x5F, 0x8E, 0x8F, 0x12, 0xC7, 0x47, 0x72, 0xB7, 0x8B, 0x9C, 0x20, 0x89, 0x96, 0xCF, + 0x20, 0x2B, 0xA2, 0xCC, 0x43, 0xF7, 0x8C, 0x2E, 0x36, 0x68, 0x41, 0xA5, 0xEE, 0x35, 0xE2, 0x2D, + 0xB3, 0x21, 0x1F, 0x07, 0x56, 0xEA, 0x52, 0x83, 0x12, 0x86, 0x6D, 0xD4, 0x9D, 0xC6, 0xF4, 0xE5, + 0xCC, 0x9B, 0x15, 0x31, 0xFB, 0x9A, 0xB9, 0x60, 0x3A, 0x44, 0x79, 0xC3, 0x4C, 0x13, 0x2D, 0x97, + 0xFB, 0x70, 0x74, 0xA7, 0x1C, 0xFB, 0xB8, 0x90, 0xBC, 0x60, 0x5F, 0x7B, 0xBD, 0xD2, 0x09, 0xD3, + 0x3B, 0xED, 0xEA, 0x17, 0xE7, 0x61, 0x33, 0x33, 0x70, 0x51, 0xA2, 0x29, 0x8C, 0xA2, 0x81, 0x59, + 0xEE, 0x37, 0xC3, 0x34, 0x2D, 0x51, 0x42, 0x2B, 0x79, 0x83, 0x0C, 0x04, 0x58, 0xA5, 0x3F, 0x76, + 0xD1, 0x7E, 0x1C, 0x3D, 0x1A, 0xC2, 0xA9, 0x63, 0xCA, 0x2B, 0x66, 0x02, 0x37, 0x79, 0xEF, 0x53, + 0xE5, 0x7A, 0x55, 0xCF, 0xDE, 0xF7, 0x29, 0x8A, 0xEC, 0xB3, 0xF3, 0xC0, 0xEF, 0x31, 0x51, 0xC6, + 0x29, 0x80, 0x44, 0x38, 0x8E, 0x18, 0xB0, 0x4E, 0xBB, 0x8D, 0x63, 0xD9, 0x8E, 0x18, 0x0D, 0x27, + 0xC8, 0xFE, 0xEC, 0xA3, 0x95, 0x1A, 0xD3, 0x5C, 0xFC, 0x19, 0x1F, 0xC7, 0x9A, 0x9C, 0x5E, 0x5F, + 0xC4, 0xE3, 0x44, 0x69, 0x40, 0xD8, 0x95, 0xAD, 0x4B, 0x0C, 0x43, 0xA0, 0x2A, 0x9B, 0xDF, 0x22, + 0x5B, 0x29, 0x98, 0x01, 0x03, 0x09, 0xAB, 0x9B, 0x93, 0xB3, 0xA8, 0xE4, 0x07, 0xCD, 0x01, 0x1E, + 0x4C, 0xAE, 0xF6, 0x71, 0x72, 0x72, 0x7C, 0x7A, 0x7A, 0x7C, 0x76, 0x76, 0x7C, 0x7E, 0x7E, 0x7C, + 0x71, 0x71, 0x7C, 0x79, 0x59, 0xD6, 0x5D, 0xA5, 0xF4, 0xAC, 0xDB, 0x9A, 0x89, 0x69, 0xE0, 0x68, + 0xF2, 0xFE, 0x71, 0xB1, 0x3C, 0x0B, 0x79, 0xFA, 0x8F, 0x9F, 0xE3, 0x9C, 0x7B, 0x00, 0x0D, 0x7F, + 0x5A, 0xF1, 0x13, 0x9B, 0x6F, 0xAE, 0x65, 0x1D, 0x1B, 0xAA, 0x99, 0x4B, 0xE9, 0x14, 0x18, 0x37, + 0x9C, 0x1A, 0xC3, 0xE3, 0x43, 0xF7, 0xF1, 0xCF, 0x79, 0xC0, 0xA0, 0x8E, 0xE8, 0xE4, 0xD1, 0x8B, + 0x90, 0x81, 0x13, 0x19, 0xF7, 0x41, 0x6E, 0x42, 0x65, 0xD2, 0x19, 0x9E, 0x29, 0x16, 0xE7, 0xBB, + 0x9F, 0x8A, 0xD4, 0x3B, 0x37, 0x1A, 0x06, 0x14, 0x3C, 0x74, 0x28, 0x21, 0xBD, 0x0B, 0xB8, 0x9C, + 0x17, 0xC0, 0x99, 0x80, 0x93, 0x50, 0xA8, 0x90, 0x7D, 0x24, 0xAE, 0xF0, 0x2A, 0x2D, 0x96, 0x7E, + 0xFF, 0xC1, 0x51, 0x7A, 0xA1, 0xBF, 0xCB, 0xF8, 0xA3, 0x29, 0xD0, 0x41, 0x47, 0xC9, 0x4E, 0xD2, + 0xB3, 0x86, 0xEC, 0x8D, 0x74, 0xDB, 0xE6, 0x73, 0x01, 0x9B, 0xA9, 0x00, 0x2D, 0x8B, 0xF1, 0xBC, + 0xC6, 0x55, 0xBD, 0x46, 0xBB, 0xA3, 0xCF, 0x31, 0x27, 0xFA, 0x37, 0xDB, 0x14, 0x2A, 0xB4, 0xC9, + 0x7F, 0xD0, 0x5C, 0xA4, 0xD8, 0xF0, 0xB2, 0x8E, 0xFB, 0xCA, 0xFD, 0x06, 0x60, 0x8C, 0xF5, 0xBE, + 0x86, 0x81, 0x4D, 0xC9, 0xB2, 0xEC, 0x3E, 0xF2, 0x4D, 0x1B, 0xD8, 0x18, 0xB0, 0xE5, 0x07, 0x50, + 0x3E, 0x81, 0xDF, 0xFE, 0x61, 0xE3, 0xE6, 0x8B, 0xE6, 0x05, 0xCA, 0x1D, 0x6C, 0x5F, 0xD0, 0xB0, + 0x72, 0xE9, 0xA0, 0x0C, 0xAA, 0x68, 0x26, 0x0C, 0xFB, 0x95, 0x0F, 0x31, 0x8A, 0x2D, 0xD7, 0xEE, + 0x84, 0xB8, 0x9E, 0x37, 0x51, 0xC6, 0xFC, 0x26, 0x99, 0xC4, 0xAC, 0x73, 0xCD, 0x31, 0xB0, 0xA9, + 0x43, 0x6E, 0xAC, 0x91, 0x14, 0x81, 0xB8, 0x28, 0x21, 0x81, 0xA6, 0xEB, 0x54, 0x37, 0x51, 0xC3, + 0x92, 0xF3, 0x16, 0x0F, 0xC3, 0xE1, 0x5D, 0x79, 0x88, 0xD0, 0x79, 0x27, 0x4B, 0xBF, 0x84, 0x30, + 0x60, 0xC3, 0x00, 0x6F, 0xEF, 0x4D, 0xCF, 0x17, 0x32, 0x31, 0x1D, 0xAD, 0x04, 0xFE, 0xEC, 0x61, + 0x78, 0x3C, 0x45, 0x13, 0x4A, 0x5A, 0x05, 0xF2, 0x24, 0x0A, 0x9E, 0x7D, 0x29, 0xB0, 0x0D, 0x94, + 0x35, 0xDF, 0x71, 0x39, 0xED, 0x9C, 0x48, 0xE0, 0xCF, 0x84, 0x93, 0xA2, 0xE0, 0x99, 0xD0, 0xA3, + 0x2E, 0xA5, 0x20, 0x30, 0x7E, 0x36, 0xB4, 0x46, 0x24, 0x55, 0xFC, 0x68, 0x70, 0xB3, 0xA6, 0x7D, + 0xE3, 0xF7, 0x53, 0xDA, 0x7B, 0x27, 0x20, 0x47, 0xCA, 0x35, 0x5C, 0xAB, 0x6F, 0x0A, 0x47, 0xF3, + 0x5D, 0xDB, 0x79, 0x38, 0x0D, 0x0D, 0x9E, 0x95, 0x56, 0xF4, 0x27, 0x43, 0x67, 0x5D, 0x50, 0xFC, + 0xF9, 0x8A, 0x98, 0x54, 0x6A, 0x22, 0xEB, 0xE7, 0x3E, 0x54, 0xC5, 0x07, 0xC5, 0x93, 0x5D, 0xB2, + 0xFB, 0x22, 0x28, 0x80, 0x55, 0x80, 0x28, 0x29, 0xCF, 0x6A, 0x9F, 0xDB, 0x86, 0x8F, 0xD7, 0x91, + 0xD2, 0x8F, 0xE6, 0xDE, 0xD4, 0x62, 0x76, 0x63, 0xE1, 0xD0, 0x37, 0xA4, 0x74, 0x61, 0x99, 0x4E, + 0xE0, 0x50, 0x76, 0x1F, 0xD9, 0x0E, 0x9C, 0xE6, 0x03, 0xED, 0x9E, 0x33, 0xA5, 0x34, 0x86, 0xF6, + 0xE7, 0x72, 0xA9, 0x38, 0xB6, 0xF9, 0x98, 0x5F, 0xA8, 0x55, 0xAB, 0xF3, 0xE4, 0x0B, 0x3E, 0x7C, + 0x4E, 0x66, 0x2D, 0xB9, 0xA2, 0x52, 0x76, 0xAD, 0x44, 0xD3, 0xEC, 0xCD, 0x49, 0xC0, 0x2B, 0x65, + 0xD9, 0xF2, 0x65, 0xF3, 0x4D, 0x32, 0x6D, 0xCD, 0x58, 0xD1, 0x1C, 0xB6, 0xAD, 0xC0, 0xB5, 0x80, + 0x64, 0xBF, 0xF0, 0x87, 0x4B, 0x95, 0x68, 0xFB, 0xDC, 0xBC, 0x2F, 0x68, 0xDE, 0x9A, 0xCE, 0x5F, + 0xC7, 0x06, 0xA8, 0xB4, 0x14, 0xF0, 0xC3, 0x07, 0x79, 0x6A, 0x09, 0x22, 0x89, 0x93, 0x5E, 0x2F, + 0x7E, 0x84, 0xCD, 0xFE, 0x0A, 0x7C, 0x08, 0x76, 0xED, 0x9B, 0xDB, 0x07, 0xE1, 0x87, 0x4F, 0xD6, + 0x6C, 0xEF, 0xEB, 0xB5, 0xFA, 0x3C, 0x36, 0x8D, 0x19, 0x5A, 0xBF, 0x66, 0x99, 0x7D, 0x1B, 0xC0, + 0x8E, 0x57, 0x64, 0xDE, 0xAC, 0xE3, 0x6C, 0x9A, 0x78, 0x71, 0x86, 0xA0, 0x8B, 0xC3, 0x29, 0x8F, + 0x32, 0x3A, 0x9B, 0x9F, 0x47, 0xB2, 0x86, 0x9E, 0xEA, 0x1F, 0xAF, 0x19, 0x69, 0x4C, 0xB2, 0x6A, + 0xC1, 0x17, 0xD1, 0x86, 0x8C, 0x10, 0x77, 0xC5, 0x89, 0xA5, 0xE4, 0x74, 0xA2, 0x02, 0x59, 0x70, + 0x86, 0x80, 0xDE, 0xC8, 0xCC, 0x1E, 0xC3, 0x2A, 0x6F, 0x51, 0x26, 0x78, 0x90, 0x57, 0x47, 0xA9, + 0x2F, 0x54, 0x3E, 0x29, 0x5F, 0xA9, 0xA9, 0x1B, 0x9B, 0x2B, 0x36, 0x26, 0x93, 0x85, 0x0B, 0x09, + 0x37, 0x1C, 0x85, 0x32, 0x29, 0x7E, 0xCD, 0x94, 0x85, 0x16, 0xAA, 0xC5, 0xDD, 0x9E, 0x8D, 0x48, + 0xF2, 0x84, 0x9C, 0x8A, 0x4E, 0x39, 0x9C, 0xE3, 0x23, 0x2C, 0x58, 0x9D, 0x6A, 0x16, 0x9F, 0xCA, + 0x9A, 0x84, 0x1D, 0x5A, 0x7B, 0x84, 0x63, 0x78, 0xA8, 0x68, 0xA6, 0x2D, 0x35, 0x2A, 0xC0, 0x3E, + 0xCC, 0xBB, 0xD2, 0xE3, 0x9A, 0x08, 0x30, 0x89, 0x18, 0x50, 0x07, 0x1B, 0x6A, 0x4F, 0x4C, 0x19, + 0x77, 0x7D, 0x21, 0xCB, 0xC6, 0x7E, 0x81, 0x8F, 0x1E, 0x47, 0xDB, 0x05, 0xC7, 0x53, 0x8A, 0x50, + 0xB3, 0x67, 0x72, 0xCB, 0x60, 0x18, 0x31, 0xF7, 0x8C, 0xAC, 0x16, 0x93, 0xD9, 0x9B, 0x37, 0x4B, + 0x01, 0x48, 0x72, 0x4B, 0x59, 0x60, 0xC0, 0xBC, 0xFF, 0xC4, 0x45, 0x82, 0xF1, 0xE5, 0xAF, 0xBC, + 0x9D, 0x8B, 0xB3, 0xA6, 0xBB, 0x5F, 0x2C, 0x5F, 0xED, 0x27, 0x7A, 0x47, 0xAE, 0xFA, 0xCD, 0x35, + 0xA8, 0x96, 0x86, 0xC3, 0xEE, 0xF7, 0x6B, 0x8D, 0x83, 0x5A, 0xE3, 0x30, 0x2F, 0x47, 0x95, 0x70, + 0x69, 0x7D, 0x70, 0x3D, 0xA7, 0x4F, 0x46, 0xB5, 0x38, 0xBF, 0x90, 0xDD, 0xB6, 0xD5, 0x57, 0xA7, + 0x9A, 0x17, 0x91, 0x52, 0xA3, 0x59, 0x01, 0x84, 0x7C, 0x84, 0x3F, 0xEA, 0xF5, 0x7C, 0x48, 0xAF, + 0x20, 0xFC, 0x60, 0x1A, 0x62, 0x70, 0xBC, 0x5F, 0xAF, 0x93, 0x19, 0x15, 0x78, 0x90, 0xEA, 0x3C, + 0xB7, 0x7E, 0x99, 0x3D, 0xB7, 0x29, 0x6A, 0xE6, 0xFE, 0xDB, 0x7F, 0xC9, 0xA1, 0x69, 0x2A, 0x38, + 0x2C, 0xC5, 0xE3, 0x28, 0xEF, 0x81, 0x9D, 0xE4, 0xA4, 0x69, 0x3D, 0xBF, 0x40, 0x3C, 0x2E, 0xD6, + 0x14, 0xAC, 0xA8, 0x4B, 0xBC, 0x2E, 0x10, 0x33, 0x7D, 0x24, 0x41, 0x33, 0xE8, 0x0E, 0x4D, 0x11, + 0x62, 0xCE, 0x25, 0x3D, 0x77, 0x6C, 0x59, 0x41, 0x20, 0x0E, 0x5A, 0x14, 0xA0, 0xBE, 0x6B, 0xA6, + 0x40, 0xDB, 0xC0, 0xDE, 0xF4, 0x81, 0xE5, 0xCB, 0xEC, 0xF4, 0xEA, 0xCB, 0xAB, 0xD2, 0xBE, 0x09, + 0x0B, 0x2E, 0xC0, 0xAD, 0x88, 0x01, 0xF8, 0x68, 0x60, 0x9B, 0xE2, 0xB5, 0x3F, 0x62, 0xB2, 0xDD, + 0x27, 0x26, 0xA5, 0x43, 0xB2, 0xF2, 0x82, 0x60, 0x6D, 0x23, 0x19, 0x52, 0x11, 0x4D, 0xCA, 0x81, + 0x47, 0xD6, 0xAC, 0xD0, 0x61, 0x95, 0xB9, 0x03, 0x60, 0x03, 0x78, 0x3D, 0xD7, 0x3E, 0x5B, 0x97, + 0xF3, 0xEA, 0xD4, 0x22, 0xF4, 0x63, 0x14, 0x81, 0x2B, 0x2B, 0x63, 0x70, 0x99, 0x86, 0xDC, 0x13, + 0x39, 0x6C, 0x24, 0xD0, 0x63, 0xF6, 0xFE, 0x5B, 0x47, 0x3A, 0xA3, 0x06, 0xEE, 0x3C, 0x37, 0xD6, + 0xC9, 0x9E, 0x22, 0xB1, 0xFD, 0x0A, 0x76, 0x11, 0x4B, 0x11, 0x75, 0x44, 0x99, 0x02, 0x33, 0xA9, + 0x4E, 0x33, 0xA6, 0xBC, 0xA0, 0xFB, 0x6C, 0xEA, 0xB0, 0xEC, 0x8D, 0xF6, 0xA9, 0xE6, 0xF3, 0x92, + 0xB5, 0xAD, 0x5A, 0x5F, 0xEE, 0xDA, 0x9B, 0x77, 0x13, 0xDE, 0x0E, 0xD4, 0xE5, 0xB3, 0xDC, 0x43, + 0xD3, 0xC6, 0xE2, 0xC9, 0x0E, 0xDD, 0x59, 0x53, 0xB9, 0x07, 0xD4, 0x00, 0xEE, 0x64, 0xEC, 0x75, + 0x74, 0xDF, 0xD2, 0xA6, 0xFB, 0x96, 0xAF, 0x36, 0x7E, 0x79, 0xCB, 0x7D, 0x2E, 0xB6, 0x37, 0xB5, + 0xDC, 0xA2, 0x3D, 0x4B, 0xA3, 0xCC, 0xBB, 0x98, 0x0A, 0x17, 0x40, 0xD9, 0x39, 0x67, 0x67, 0xA0, + 0x72, 0xAF, 0xC2, 0xB1, 0x74, 0x76, 0xD2, 0xDF, 0x85, 0xBB, 0x88, 0x6A, 0x6C, 0x68, 0xEA, 0x9E, + 0x03, 0x8B, 0x24, 0xE3, 0x02, 0x29, 0x5B, 0x82, 0x98, 0xF2, 0x2E, 0x19, 0xF9, 0x86, 0xC0, 0xCF, + 0xB4, 0x3E, 0x1F, 0x25, 0x2D, 0xB4, 0x60, 0x8E, 0x09, 0x93, 0x5E, 0xBE, 0x92, 0xDC, 0x1B, 0x58, + 0xE3, 0x26, 0x0E, 0xF2, 0x59, 0x2E, 0x0D, 0xCB, 0x75, 0xD9, 0x04, 0xF9, 0x0E, 0xE6, 0x81, 0x19, + 0x91, 0x7F, 0x1D, 0x9A, 0x76, 0x20, 0xB8, 0x9F, 0xE1, 0xB4, 0x79, 0x90, 0x72, 0xDA, 0x7C, 0x3B, + 0xE6, 0xB4, 0x79, 0xA3, 0x3D, 0x62, 0x6E, 0xEB, 0xD9, 0x11, 0x9D, 0x58, 0xA1, 0x63, 0x07, 0x46, + 0x7A, 0xB3, 0xAE, 0x88, 0xCE, 0xAF, 0x58, 0xEA, 0x1D, 0x79, 0x16, 0xAC, 0x1C, 0x31, 0x4A, 0x96, + 0x86, 0x41, 0xE5, 0x5F, 0x1F, 0x98, 0xFC, 0x1E, 0x91, 0xCF, 0x92, 0x1B, 0xA3, 0xC2, 0x9C, 0xB9, + 0x86, 0xCE, 0xA2, 0x77, 0xAA, 0x5E, 0x77, 0x30, 0xCA, 0x37, 0x6D, 0x99, 0x20, 0x66, 0xE1, 0x7B, + 0x96, 0x03, 0x3F, 0x40, 0xEA, 0x1B, 0x4A, 0x83, 0xB6, 0x8D, 0x73, 0xDA, 0x45, 0xFD, 0x4D, 0x98, + 0x3A, 0xF3, 0x03, 0xEF, 0x9E, 0xCB, 0xF2, 0xF4, 0x1A, 0x74, 0xE6, 0xA1, 0xFE, 0x17, 0x96, 0x88, + 0xC0, 0x48, 0x52, 0x18, 0x3D, 0xEE, 0x8A, 0x7A, 0x00, 0xB0, 0x56, 0x1B, 0x51, 0x63, 0xD7, 0x38, + 0x02, 0x3C, 0xC4, 0xC6, 0xCD, 0x77, 0xEF, 0x46, 0xDF, 0x3C, 0x1F, 0x67, 0xD5, 0x9C, 0x45, 0x2B, + 0x48, 0x8B, 0xF2, 0x87, 0xC0, 0x16, 0x54, 0x95, 0xC7, 0x71, 0xB4, 0xCD, 0x65, 0x24, 0x19, 0x6F, + 0x37, 0x67, 0xA6, 0x99, 0x55, 0x50, 0xA7, 0x2A, 0xAD, 0xB1, 0x0A, 0xFA, 0xBC, 0x56, 0xB5, 0x4B, + 0x88, 0x42, 0x97, 0x4A, 0x80, 0x40, 0x4B, 0x49, 0x2A, 0x48, 0xD2, 0xA0, 0x46, 0xC6, 0x39, 0xA4, + 0xCF, 0x90, 0xFB, 0xEB, 0x1E, 0xD7, 0x30, 0xC2, 0x2C, 0x8B, 0x14, 0xE9, 0x3B, 0x24, 0x45, 0xE9, + 0x5A, 0x67, 0xD1, 0x59, 0xA1, 0xC2, 0x0A, 0x34, 0x49, 0x9F, 0x92, 0x0E, 0x9F, 0x39, 0xA9, 0x2D, + 0x91, 0xD6, 0x52, 0x48, 0x58, 0x80, 0xDA, 0x92, 0x2D, 0x97, 0x4A, 0x6F, 0x38, 0x28, 0xEC, 0xBC, + 0xD4, 0x79, 0xD5, 0x10, 0x39, 0xFD, 0xC0, 0xCA, 0xD7, 0x84, 0x49, 0x0F, 0x18, 0xA3, 0xF6, 0x84, + 0xED, 0x87, 0xA4, 0x3A, 0x52, 0x43, 0x51, 0x67, 0x39, 0x96, 0x28, 0x93, 0x0B, 0xB9, 0xF0, 0x5F, + 0x6C, 0x69, 0x5F, 0xA0, 0xF9, 0xE4, 0x8B, 0x89, 0xCE, 0xE5, 0xC5, 0xAF, 0x30, 0x52, 0x2D, 0xE8, + 0x3E, 0xE6, 0xC7, 0xB1, 0x3C, 0xAC, 0xFA, 0xF9, 0x79, 0xE9, 0xC1, 0x11, 0xED, 0xA1, 0x95, 0x0D, + 0xC0, 0xB5, 0x20, 0xF3, 0x5D, 0xC2, 0x84, 0x17, 0xEB, 0x1E, 0xED, 0x77, 0x1D, 0xFC, 0x48, 0x37, + 0x22, 0xF0, 0x60, 0xDD, 0xDE, 0x78, 0x67, 0x96, 0x83, 0xCE, 0x71, 0x64, 0x0B, 0x09, 0x3C, 0x2A, + 0x15, 0x8C, 0xCC, 0x96, 0x2E, 0xE0, 0x71, 0x9E, 0xBE, 0xE2, 0xC0, 0x8E, 0xCD, 0x6B, 0x7F, 0xE8, + 0x82, 0x32, 0x8D, 0x14, 0x11, 0x2F, 0xAA, 0xBA, 0xCC, 0xC9, 0x6D, 0x3B, 0x5D, 0x4B, 0x06, 0x1E, + 0xAD, 0xA4, 0x40, 0x4C, 0x4C, 0x07, 0x89, 0xCD, 0x22, 0xAF, 0x63, 0xC8, 0xE2, 0xEF, 0xE7, 0xE2, + 0xE1, 0x82, 0x88, 0x90, 0x27, 0x18, 0x16, 0x68, 0x6B, 0xEC, 0x96, 0x53, 0x11, 0x6B, 0x1D, 0x4B, + 0x60, 0xF9, 0x54, 0x1D, 0x5A, 0x1A, 0xF0, 0xC8, 0x55, 0xBE, 0x51, 0xAF, 0x1F, 0xEE, 0xC1, 0x8F, + 0xA3, 0x48, 0x40, 0xD0, 0x0C, 0x43, 0x56, 0x48, 0xC5, 0x3E, 0x00, 0x99, 0x9F, 0x41, 0x81, 0xC7, + 0xD4, 0x1E, 0x6D, 0x44, 0x75, 0xC7, 0xD1, 0x74, 0xB0, 0x4E, 0x8E, 0x07, 0x48, 0x55, 0x4A, 0x22, + 0x85, 0x6E, 0x72, 0x71, 0x15, 0x35, 0x16, 0x0B, 0x07, 0x63, 0x3B, 0x79, 0xCA, 0x39, 0x2E, 0x81, + 0x71, 0x26, 0x11, 0x16, 0x69, 0x35, 0xD4, 0x13, 0x41, 0x16, 0xCD, 0x8B, 0xBF, 0x49, 0x53, 0x06, + 0xDA, 0xC4, 0x4C, 0xB5, 0x2E, 0x95, 0x57, 0x08, 0xFA, 0x1A, 0x6A, 0xF6, 0x93, 0x2A, 0xA8, 0x9E, + 0x5B, 0xFC, 0x5C, 0x2A, 0xFF, 0xCD, 0xCB, 0x83, 0x73, 0x8B, 0x12, 0xF3, 0x48, 0xA3, 0x19, 0xE8, + 0x97, 0xFB, 0x5A, 0x69, 0xBC, 0xED, 0x86, 0x86, 0xAF, 0x93, 0xF9, 0x3A, 0x8C, 0xE0, 0xB9, 0x9D, + 0x1A, 0x27, 0xC6, 0x0A, 0xE4, 0x24, 0x1D, 0xEF, 0xB2, 0xD2, 0x1A, 0x05, 0x0A, 0xB5, 0x3D, 0x47, + 0x38, 0x40, 0xA6, 0xC5, 0x8D, 0xEF, 0xE3, 0xDD, 0x4E, 0x58, 0xC1, 0x82, 0x8C, 0xF0, 0x9D, 0x76, + 0xBB, 0xB4, 0x09, 0xFE, 0xFA, 0xA2, 0xAC, 0x05, 0x3E, 0x57, 0x28, 0xDB, 0xAA, 0x2D, 0xF0, 0x67, + 0x51, 0x4C, 0x03, 0x86, 0x83, 0x21, 0x37, 0x3A, 0xC3, 0x9E, 0x4D, 0x3D, 0x1E, 0x34, 0xC6, 0x3D, + 0xBC, 0x59, 0xC1, 0x48, 0x5B, 0xDC, 0x64, 0x52, 0x41, 0x76, 0x00, 0x86, 0x6F, 0xE2, 0x91, 0x65, + 0xD7, 0xCE, 0x43, 0xF5, 0xC2, 0xE6, 0x5E, 0xFF, 0x89, 0xED, 0x00, 0x90, 0xDE, 0x50, 0xC8, 0x9A, + 0x08, 0x3C, 0x3B, 0xF6, 0x8E, 0xD3, 0xEB, 0xC5, 0x14, 0x68, 0x8C, 0x45, 0xD3, 0x7C, 0x90, 0x12, + 0x35, 0xCF, 0x7A, 0x22, 0x8B, 0xED, 0xA7, 0xAB, 0x4E, 0xBC, 0x1E, 0x39, 0xA9, 0xE8, 0xF8, 0xDA, + 0x8B, 0x85, 0x3F, 0x0A, 0x16, 0xD0, 0x74, 0xE1, 0x78, 0x4F, 0x0A, 0x86, 0xB1, 0x28, 0x07, 0xF9, + 0x3C, 0x04, 0xAE, 0xBF, 0x0A, 0x93, 0x7F, 0x7A, 0x2E, 0x79, 0x04, 0xC0, 0xC5, 0x22, 0x6F, 0x7A, + 0xD5, 0xF2, 0x78, 0xE4, 0x1E, 0x5E, 0x1D, 0xE1, 0x21, 0x88, 0x25, 0xEE, 0x31, 0x00, 0x88, 0x52, + 0x18, 0x90, 0x6E, 0xE2, 0xE1, 0x85, 0x11, 0x1D, 0xB2, 0x51, 0x00, 0x26, 0x96, 0xDD, 0x33, 0x29, + 0xDB, 0xA4, 0x03, 0xAA, 0x8B, 0xA3, 0xFF, 0x88, 0x5A, 0xD4, 0xE0, 0x24, 0x7E, 0x52, 0x96, 0x20, + 0x1B, 0x2F, 0x53, 0xF0, 0x76, 0x78, 0xA8, 0x99, 0xE4, 0x83, 0x43, 0x96, 0xA4, 0xCC, 0x38, 0x88, + 0xD1, 0x18, 0x4B, 0x8C, 0x79, 0xD8, 0xD8, 0x54, 0x16, 0xB3, 0xFC, 0xE7, 0x27, 0x6B, 0xD3, 0xBD, + 0x34, 0x3E, 0x95, 0x89, 0x65, 0x88, 0x94, 0x67, 0xDA, 0x85, 0x3B, 0x27, 0x85, 0xA4, 0x89, 0x38, + 0x86, 0x5B, 0xB9, 0x51, 0x4E, 0x48, 0x3D, 0x53, 0x3B, 0x1E, 0x51, 0xD6, 0x2C, 0xC5, 0x3B, 0xBC, + 0x71, 0x4F, 0x8E, 0x3C, 0xC7, 0x95, 0xFB, 0xAB, 0xD9, 0x4C, 0x83, 0x0A, 0x8C, 0x49, 0x46, 0x61, + 0xF0, 0x6E, 0xD0, 0xFF, 0x85, 0x7C, 0x93, 0xCE, 0xF1, 0x4F, 0x86, 0x7F, 0x4F, 0x77, 0x66, 0x5D, + 0x80, 0xF6, 0x5A, 0xC4, 0xC1, 0x0F, 0xC1, 0x7E, 0x2E, 0xCD, 0x4E, 0x30, 0x47, 0xF9, 0x87, 0xBC, + 0xCA, 0x65, 0x67, 0x4E, 0x80, 0x74, 0xB9, 0x0A, 0xF6, 0x95, 0x98, 0xC6, 0xEA, 0x79, 0xD7, 0x09, + 0x23, 0xA1, 0x94, 0x49, 0xD1, 0x35, 0x5B, 0x09, 0x55, 0xB6, 0x39, 0xE0, 0x55, 0x46, 0x18, 0x75, + 0x48, 0x8E, 0x7F, 0xB2, 0x09, 0xC8, 0xFB, 0x36, 0xD0, 0x3D, 0xC5, 0x1F, 0x56, 0xE1, 0x0F, 0xC9, + 0x77, 0xF4, 0x27, 0x60, 0x00, 0x3E, 0x15, 0x36, 0x95, 0xBE, 0x7E, 0x84, 0x0F, 0x28, 0xB3, 0x16, + 0x8F, 0x65, 0xD8, 0xDE, 0x5A, 0x8D, 0x17, 0x00, 0x27, 0xCF, 0x26, 0x84, 0x5A, 0x5F, 0xE8, 0x01, + 0xE1, 0x9A, 0x9A, 0x48, 0x18, 0x31, 0xB0, 0xE0, 0x70, 0x84, 0x8B, 0x54, 0xF7, 0x85, 0x02, 0x12, + 0x52, 0x8D, 0x5F, 0x4D, 0xF1, 0xC8, 0xCB, 0xE1, 0xF3, 0x98, 0x02, 0x79, 0x2A, 0x7A, 0x21, 0x82, + 0xC3, 0x26, 0xC5, 0x2F, 0xA4, 0x00, 0xB0, 0xE9, 0x09, 0xC7, 0xD5, 0x6C, 0xCF, 0x3F, 0x9F, 0xB5, + 0x41, 0xF0, 0x0B, 0xC1, 0x1D, 0xA0, 0x1D, 0x16, 0x9F, 0x2D, 0x99, 0x69, 0xC6, 0x47, 0xCF, 0x77, + 0x04, 0x1B, 0x03, 0xDD, 0x0D, 0xA7, 0x89, 0x07, 0xEF, 0x8A, 0x39, 0x6C, 0xDC, 0x2C, 0x18, 0x41, + 0x8B, 0xF8, 0x2C, 0xAA, 0x15, 0xB8, 0x8E, 0x5A, 0xC8, 0x08, 0x55, 0x4E, 0x27, 0x86, 0xAE, 0xD6, + 0x8F, 0xB2, 0x9A, 0xC2, 0x89, 0x0C, 0x00, 0xDF, 0x65, 0xE7, 0x5F, 0x3A, 0xBB, 0xEC, 0x13, 0xE8, + 0x47, 0x0F, 0xC0, 0x89, 0x51, 0x86, 0xEC, 0x04, 0x5D, 0xEC, 0xE9, 0x46, 0xF3, 0x7F, 0x6C, 0xB1, + 0x2F, 0xC9, 0x6C, 0x97, 0x3D, 0x02, 0xC5, 0x55, 0x3B, 0x99, 0x02, 0x2C, 0x04, 0xE4, 0xB2, 0x53, + 0x81, 0x85, 0xE8, 0x76, 0xD5, 0x4E, 0x94, 0xB7, 0x39, 0x1C, 0xB3, 0x03, 0x5E, 0xA6, 0x36, 0x6C, + 0x5D, 0x7E, 0x21, 0x80, 0x6D, 0x88, 0x52, 0xE4, 0x86, 0xA4, 0x4E, 0xD7, 0x2C, 0xB4, 0x23, 0x0B, + 0x5D, 0x1A, 0xC9, 0x9E, 0x73, 0xEE, 0xB0, 0xC3, 0x05, 0x67, 0x0D, 0x8B, 0x21, 0x46, 0x6E, 0xBB, + 0xDD, 0xA8, 0xCD, 0x7A, 0xEC, 0x75, 0x0B, 0xA7, 0x0A, 0xE0, 0x49, 0x33, 0xC8, 0x02, 0xDE, 0xD8, + 0x06, 0x4A, 0xC0, 0x85, 0xBC, 0x20, 0x7F, 0x61, 0xE4, 0xCF, 0x07, 0xB6, 0x14, 0xF6, 0x43, 0xA3, + 0x67, 0x82, 0xFE, 0xEA, 0x34, 0x9E, 0x41, 0x02, 0xEA, 0xAD, 0x6D, 0x20, 0x83, 0x70, 0x41, 0x2F, + 0xA4, 0x50, 0x98, 0x14, 0xF2, 0x83, 0x2E, 0x45, 0x0E, 0xAA, 0xE1, 0x33, 0x21, 0x09, 0x29, 0x94, + 0xCE, 0xA0, 0x88, 0x98, 0xE4, 0xBA, 0x0D, 0x54, 0xE1, 0xCB, 0xE9, 0x0E, 0x61, 0xBA, 0x2F, 0x94, + 0x51, 0x9C, 0x32, 0x14, 0x4A, 0x14, 0x26, 0x0C, 0xD9, 0x6E, 0x03, 0x6F, 0x36, 0xA7, 0xAB, 0x2C, + 0xF3, 0x94, 0xD5, 0x0C, 0x97, 0xFE, 0x45, 0xB8, 0x89, 0xBC, 0xC3, 0xD9, 0x64, 0x14, 0xA1, 0x2B, + 0xC6, 0xA6, 0xCC, 0x4E, 0x37, 0x5C, 0x3C, 0xD5, 0xF0, 0xD8, 0x00, 0x91, 0x9E, 0x89, 0x74, 0xD3, + 0x68, 0xEE, 0x6F, 0xAE, 0x67, 0x4A, 0xF1, 0x52, 0xA2, 0x87, 0x95, 0xC5, 0xB8, 0x78, 0x27, 0x91, + 0x3F, 0xDC, 0xC9, 0x5C, 0x7E, 0xA6, 0xA9, 0x46, 0xCB, 0x2B, 0x1E, 0xBA, 0x18, 0xE3, 0x26, 0x62, + 0xC5, 0xBA, 0xED, 0x9A, 0xB6, 0x70, 0x97, 0x64, 0xD2, 0x84, 0xD5, 0x95, 0xB2, 0x66, 0x8E, 0xDA, + 0xCD, 0x69, 0xC8, 0x1C, 0x81, 0x37, 0x65, 0xC3, 0xC4, 0x35, 0x6F, 0x92, 0xF9, 0x72, 0xB4, 0xE2, + 0xA5, 0x58, 0x2E, 0x8B, 0x32, 0x51, 0x1B, 0xA9, 0xC7, 0xB2, 0x2E, 0x1E, 0x5D, 0x80, 0xAD, 0x3D, + 0x8B, 0x89, 0xE2, 0xAB, 0x2C, 0x7C, 0xF7, 0x78, 0xC5, 0x1E, 0x83, 0x32, 0x04, 0x40, 0xDE, 0xAA, + 0x28, 0x2F, 0xD6, 0xA1, 0xF6, 0x68, 0x0E, 0x83, 0x61, 0xE4, 0x4F, 0xC5, 0xBA, 0x5C, 0x3C, 0x70, + 0x6E, 0x83, 0x14, 0x42, 0xD7, 0x74, 0xE6, 0x3D, 0x8F, 0x22, 0xCA, 0x6A, 0xEC, 0xA2, 0xD6, 0xAF, + 0xA9, 0xF2, 0xAD, 0x92, 0x2F, 0x93, 0xAD, 0x14, 0xAF, 0x64, 0x8E, 0xF0, 0xFA, 0xD8, 0x1F, 0x5D, + 0x31, 0x63, 0x57, 0x18, 0x13, 0xF0, 0x7F, 0x8E, 0xD8, 0x47, 0x76, 0x74, 0xE0, 0xBF, 0xB0, 0xEE, + 0x49, 0xAC, 0x3B, 0x8D, 0x3F, 0xB9, 0x58, 0x77, 0xAA, 0xD1, 0xEA, 0x58, 0xF7, 0xC2, 0x48, 0xC6, + 0xE3, 0xBA, 0x89, 0x19, 0x83, 0x66, 0xD1, 0x4B, 0xF8, 0xDE, 0x06, 0xD0, 0x8A, 0x1B, 0xCE, 0x25, + 0xAC, 0x65, 0xA8, 0xE2, 0x77, 0x75, 0xCB, 0xD1, 0x7F, 0x4C, 0x23, 0x8E, 0x6A, 0xB3, 0x3E, 0x22, + 0x8F, 0x51, 0x2F, 0x44, 0x1F, 0xF8, 0xDD, 0x47, 0xF6, 0x9F, 0x8D, 0xE0, 0x85, 0x44, 0xA6, 0x92, + 0x48, 0x84, 0x2F, 0xB9, 0xE9, 0x23, 0x6C, 0xB1, 0x7D, 0xC4, 0x71, 0xEB, 0x38, 0xE2, 0x9C, 0x5B, + 0x33, 0x4D, 0x3D, 0xF8, 0x1E, 0xA3, 0x17, 0x37, 0x80, 0x3A, 0x84, 0x43, 0x5E, 0x40, 0x58, 0x07, + 0xAD, 0x2A, 0x3C, 0xD3, 0xC5, 0xC4, 0xA8, 0xDA, 0x53, 0x78, 0xA7, 0xEF, 0x45, 0x4E, 0xBD, 0x44, + 0x2D, 0x98, 0x83, 0x30, 0xF0, 0x63, 0xF7, 0x48, 0xF5, 0x17, 0xEC, 0x9F, 0x82, 0xFD, 0x23, 0x84, + 0xC8, 0x8B, 0xFD, 0x51, 0x8B, 0x2D, 0xC5, 0x7E, 0x90, 0x57, 0x65, 0x46, 0xB9, 0x5C, 0x24, 0x10, + 0xBD, 0xBD, 0x41, 0x12, 0x15, 0x47, 0x20, 0x03, 0xDE, 0x5B, 0x20, 0x4C, 0xDF, 0xF3, 0xBC, 0x74, + 0xD0, 0xA8, 0xD7, 0x5F, 0x48, 0x61, 0x26, 0x29, 0x8C, 0xB0, 0xA3, 0x10, 0x3D, 0x44, 0xCD, 0xB6, + 0x90, 0x28, 0x42, 0xBC, 0xB9, 0x32, 0x66, 0x51, 0x44, 0x84, 0x61, 0x57, 0xE7, 0x6B, 0x21, 0x07, + 0x1D, 0xD3, 0x96, 0x90, 0x57, 0xA8, 0xD9, 0x33, 0x15, 0x51, 0xA4, 0xD0, 0x3E, 0x86, 0xF0, 0x9F, + 0xDA, 0x9D, 0x51, 0x18, 0x31, 0xD6, 0xB5, 0x1A, 0x68, 0x9E, 0xA6, 0x83, 0x06, 0xF1, 0xC7, 0x95, + 0x87, 0x66, 0x18, 0x3A, 0x53, 0xD8, 0x90, 0x9B, 0x02, 0x46, 0x6D, 0x96, 0x87, 0xFE, 0x0B, 0x32, + 0xF5, 0x60, 0xB8, 0x2E, 0xBB, 0xD1, 0x6C, 0xD0, 0x3A, 0xBD, 0xC5, 0x1A, 0x7B, 0x1C, 0x5B, 0x8F, + 0x12, 0xC5, 0xE1, 0x28, 0xD7, 0xA6, 0x4F, 0x4E, 0x3A, 0xD2, 0xF3, 0xC3, 0xE2, 0x6A, 0xD0, 0x4A, + 0xB9, 0x44, 0xBB, 0x53, 0xEC, 0x3D, 0x69, 0xD3, 0xD0, 0x04, 0x33, 0xD0, 0x65, 0x7C, 0x0E, 0x99, + 0x86, 0xA0, 0xCC, 0x31, 0x26, 0x18, 0x87, 0x12, 0xBD, 0xE5, 0x37, 0x05, 0x25, 0xC0, 0xAF, 0xAC, + 0x40, 0x31, 0xE0, 0x6C, 0x94, 0x35, 0x28, 0xBE, 0xC4, 0x85, 0xD9, 0x83, 0xF2, 0x66, 0xA5, 0xEB, + 0x9C, 0xB3, 0x8E, 0xF9, 0x3B, 0x8F, 0x0A, 0xDD, 0xF8, 0x06, 0x7E, 0x9C, 0x1C, 0x64, 0x5E, 0x67, + 0x37, 0xA7, 0x48, 0x61, 0x6C, 0x8F, 0x5D, 0x7A, 0x3C, 0xDE, 0x0E, 0x3F, 0x76, 0x5C, 0x60, 0xBB, + 0xD3, 0x79, 0xED, 0xAC, 0x8E, 0x8B, 0xDF, 0x2E, 0x50, 0xBA, 0xCC, 0xF4, 0x0E, 0xDF, 0xE1, 0xC3, + 0x69, 0x87, 0x93, 0xF0, 0x14, 0x8C, 0x5E, 0x5B, 0xBC, 0x27, 0x5E, 0xCF, 0xE0, 0x41, 0x62, 0xD0, + 0xC2, 0xE0, 0xFB, 0x0F, 0x7B, 0xF0, 0xC7, 0xCC, 0x37, 0x11, 0x82, 0x79, 0xDE, 0x34, 0x5A, 0x09, + 0xA6, 0x39, 0xF2, 0xDF, 0x0B, 0xD7, 0xD2, 0xA1, 0x88, 0xA2, 0x13, 0xCB, 0xAA, 0x4C, 0xF4, 0xFD, + 0x2B, 0x58, 0x1F, 0x20, 0x06, 0x20, 0x22, 0x67, 0x0C, 0x4F, 0x87, 0xA9, 0x4E, 0x73, 0xED, 0xDB, + 0x13, 0xDE, 0xE2, 0xC0, 0x68, 0x50, 0x96, 0x82, 0x8B, 0x47, 0x0A, 0x96, 0xF8, 0x35, 0xF4, 0x50, + 0xFB, 0xB5, 0x5E, 0x13, 0x8F, 0x62, 0xFA, 0x3C, 0xC2, 0xF6, 0x87, 0x84, 0x27, 0x79, 0xDE, 0x9C, + 0x00, 0x5C, 0x19, 0xBA, 0x86, 0x90, 0xB8, 0x3A, 0x97, 0xA0, 0x9E, 0x3C, 0xA5, 0x7C, 0xE0, 0x9D, + 0xB0, 0x37, 0x2C, 0xCE, 0x70, 0xF2, 0x14, 0x27, 0x58, 0xDB, 0x3E, 0x34, 0x9B, 0x8D, 0xFA, 0x7E, + 0xE3, 0xD7, 0x7A, 0xB3, 0xDE, 0xA8, 0x1F, 0xC9, 0xFC, 0x11, 0x79, 0x00, 0x0C, 0xCD, 0xD8, 0x5F, + 0x96, 0xB9, 0x19, 0x63, 0xF3, 0xFA, 0xC3, 0xED, 0x47, 0xB3, 0xFE, 0x3E, 0xFF, 0x7E, 0x1C, 0xD4, + 0xDF, 0xAD, 0x6A, 0x3F, 0xD4, 0xBC, 0xB6, 0x62, 0x3F, 0xE0, 0x1B, 0x3C, 0x0C, 0x5A, 0x53, 0x4E, + 0x8F, 0x5C, 0x1B, 0x29, 0xCF, 0xD2, 0x1C, 0x5B, 0x39, 0xFD, 0x5A, 0xAE, 0x32, 0x92, 0xE0, 0x62, + 0x00, 0x38, 0xC7, 0x1A, 0x3C, 0xC4, 0x8E, 0xE5, 0x5F, 0xF9, 0xD2, 0x84, 0xE4, 0xDC, 0xEB, 0x45, + 0xBE, 0x55, 0x66, 0x6D, 0x20, 0x46, 0x61, 0xF2, 0x52, 0x5A, 0x9D, 0xFA, 0x7B, 0x11, 0xEB, 0xCB, + 0xB1, 0xE5, 0x93, 0xFD, 0x11, 0xA6, 0x68, 0xAC, 0x85, 0x53, 0xF4, 0x4E, 0x4C, 0xCF, 0x8B, 0x7F, + 0x37, 0x46, 0xE7, 0x79, 0x23, 0x9E, 0x8F, 0x57, 0xA6, 0xAA, 0x45, 0xE1, 0x6F, 0x46, 0x1E, 0x5E, + 0x99, 0xF9, 0xF5, 0x55, 0x61, 0x45, 0x2E, 0x3B, 0xFB, 0xB3, 0x9B, 0x91, 0xEF, 0xB9, 0x1E, 0x4B, + 0xF7, 0x3C, 0x33, 0x95, 0xF3, 0xF4, 0x6C, 0xCA, 0x4A, 0x1C, 0x94, 0x8B, 0xC3, 0x94, 0xAE, 0x81, + 0x3F, 0x29, 0x19, 0x7D, 0xC9, 0x00, 0x21, 0xDB, 0x48, 0xE9, 0x53, 0xC9, 0xB7, 0x07, 0xDE, 0x64, + 0xF9, 0x3B, 0xDB, 0x5E, 0x91, 0x95, 0x48, 0x3C, 0xA5, 0xFB, 0x62, 0x00, 0xB4, 0x0F, 0x0A, 0x01, + 0xFC, 0xDD, 0x20, 0x73, 0x45, 0xA3, 0x19, 0x3E, 0xAA, 0x4F, 0xCB, 0xC1, 0x31, 0x25, 0x03, 0x91, + 0x76, 0xCF, 0x4F, 0x85, 0x3D, 0xA6, 0xA5, 0xC9, 0xF4, 0x0F, 0x49, 0x52, 0xC2, 0x77, 0xE5, 0x7D, + 0x31, 0x65, 0x13, 0x82, 0x4F, 0x13, 0x71, 0x25, 0x75, 0xFD, 0x3D, 0x66, 0x5C, 0xC0, 0xAE, 0xD0, + 0x90, 0xD0, 0x88, 0x0C, 0x09, 0xD3, 0x09, 0x31, 0x94, 0xEE, 0xE5, 0x6C, 0x4B, 0x15, 0x2C, 0x4F, + 0xF4, 0x10, 0x86, 0x4E, 0x16, 0x09, 0xA8, 0x9C, 0xE4, 0x82, 0x91, 0x63, 0xDB, 0xE4, 0x56, 0x15, + 0xDE, 0x1C, 0xAC, 0x4C, 0x98, 0xB5, 0x39, 0x4E, 0x20, 0x70, 0xBA, 0x99, 0x9B, 0x84, 0x6D, 0x46, + 0x9B, 0x74, 0x81, 0xB5, 0x0D, 0x41, 0xE9, 0x9D, 0xB8, 0x51, 0x14, 0xC4, 0x38, 0xBE, 0x3F, 0x1E, + 0x1F, 0x3A, 0xF7, 0xCA, 0xEE, 0x13, 0x8D, 0x52, 0x62, 0xC3, 0xD4, 0x0A, 0xB2, 0x36, 0x2C, 0x47, + 0xF4, 0xAA, 0xDB, 0xFA, 0x57, 0xBB, 0xEB, 0xBB, 0x7F, 0xCE, 0xBB, 0x1D, 0xA9, 0x47, 0xE9, 0x8F, + 0xBE, 0xEE, 0x99, 0xAE, 0x60, 0xBE, 0xA7, 0x03, 0x1E, 0x78, 0xFA, 0x1E, 0x46, 0x62, 0xD7, 0xFE, + 0x46, 0x33, 0x90, 0x5F, 0xC9, 0x77, 0x63, 0xCD, 0x3E, 0xC4, 0x9F, 0x93, 0x08, 0xE1, 0xD8, 0x54, + 0x04, 0xCA, 0xC1, 0x2C, 0x7F, 0x3E, 0xFB, 0xC8, 0x0C, 0x47, 0x0F, 0xD0, 0xBE, 0x57, 0xFB, 0x2D, + 0xE0, 0xDE, 0x93, 0x54, 0x94, 0x1C, 0x0F, 0x54, 0xA5, 0x9D, 0xD7, 0x35, 0x21, 0x5E, 0xBF, 0x89, + 0x5A, 0x86, 0x6D, 0x6A, 0x20, 0x7E, 0x5C, 0x68, 0xFA, 0x60, 0x47, 0xB0, 0x8F, 0x2D, 0xF6, 0xEF, + 0x89, 0xF9, 0x63, 0xBE, 0xAB, 0xAE, 0xE3, 0x08, 0x5F, 0x78, 0x9A, 0x5B, 0xBB, 0x93, 0x2D, 0x76, + 0xC4, 0xA8, 0x93, 0xFF, 0x78, 0xA3, 0x66, 0x18, 0xCE, 0x0B, 0x36, 0x01, 0x34, 0x70, 0xE0, 0x3D, + 0x1F, 0xF6, 0x06, 0x62, 0x08, 0x0C, 0xF8, 0xFF, 0x03, 0x5E, 0x8B, 0x87, 0x84, 0x17, 0xDA, 0x00 +}; ///index_html + +//Content of bootstrap.bundle.min.jss with gzip compression +static const uint8_t bootstrap_bundle_min_js[] PROGMEM = { + 0x1F, 0x8B, 0x08, 0x08, 0x21, 0x7F, 0x4E, 0x61, 0x04, 0x00, 0x62, 0x6F, 0x6F, 0x74, 0x73, 0x74, + 0x72, 0x61, 0x70, 0x2E, 0x62, 0x75, 0x6E, 0x64, 0x6C, 0x65, 0x2E, 0x6D, 0x69, 0x6E, 0x2E, 0x6A, + 0x73, 0x00, 0xCC, 0x5D, 0xE9, 0x76, 0xDB, 0xC6, 0x15, 0xFE, 0xDF, 0xA7, 0x90, 0xD0, 0x54, 0x05, + 0x22, 0x88, 0x92, 0xBA, 0xFD, 0x00, 0x83, 0xF0, 0x28, 0xB2, 0xD2, 0xB8, 0xB1, 0x2D, 0xD7, 0x56, + 0xEC, 0x24, 0x0C, 0xEB, 0x42, 0xE4, 0x48, 0x84, 0x4D, 0x01, 0x0C, 0x00, 0x4A, 0x56, 0x48, 0xBE, + 0x7B, 0xBF, 0x7B, 0x67, 0xC5, 0x42, 0x4A, 0x4E, 0xDB, 0xD3, 0xE6, 0x38, 0x22, 0x30, 0x98, 0xF5, + 0xCE, 0x9D, 0xBB, 0xCF, 0xCC, 0xE1, 0xE7, 0xBB, 0xBF, 0xD9, 0xD9, 0xF9, 0x7C, 0xE7, 0xAB, 0x3C, + 0xAF, 0xCA, 0xAA, 0x48, 0xE6, 0x3B, 0xB7, 0x7F, 0xEE, 0x1D, 0xF5, 0xFE, 0xB0, 0xE3, 0x4F, 0xAB, + 0x6A, 0x5E, 0x46, 0x87, 0x87, 0xD7, 0xA2, 0xBA, 0xD4, 0x1F, 0x7B, 0xE3, 0xFC, 0xE6, 0x30, 0xE0, + 0x02, 0xA7, 0xF9, 0xFC, 0xBE, 0x48, 0xAF, 0xA7, 0xD5, 0xCE, 0x1F, 0x8E, 0x8E, 0x8F, 0x0F, 0xFE, + 0x70, 0xF4, 0x87, 0xE3, 0x9D, 0x8B, 0xA9, 0x70, 0x2A, 0x3A, 0x59, 0x54, 0xD3, 0xBC, 0x28, 0x9D, + 0x9A, 0xD2, 0x6A, 0xBA, 0xB8, 0xE4, 0x3A, 0xAA, 0xBB, 0xCB, 0xF2, 0xD0, 0x54, 0x7B, 0x78, 0x8D, + 0x3F, 0xD3, 0xF2, 0x70, 0x9C, 0x67, 0x55, 0x91, 0x5E, 0x2E, 0x2A, 0x14, 0x93, 0xAD, 0x3C, 0x4B, + 0xC7, 0x22, 0x2B, 0xC5, 0x64, 0x67, 0x91, 0x4D, 0x44, 0xB1, 0xF3, 0xFC, 0xE9, 0xC5, 0x63, 0xAA, + 0xBB, 0x9C, 0xE5, 0x97, 0x87, 0x37, 0x49, 0x9A, 0x1D, 0x3E, 0x7B, 0x7A, 0x7A, 0xF6, 0xE2, 0xF5, + 0x19, 0x57, 0x76, 0xF8, 0x9B, 0xDD, 0xAB, 0x45, 0x36, 0xAE, 0xD2, 0x3C, 0xF3, 0xAB, 0x50, 0x04, + 0x4B, 0x2F, 0xBF, 0x7C, 0x2F, 0xC6, 0x95, 0x17, 0xC7, 0xD5, 0xFD, 0x5C, 0xE4, 0x57, 0x3B, 0xE2, + 0xE3, 0x3C, 0x2F, 0xAA, 0x72, 0x6F, 0xCF, 0xA3, 0xE6, 0xAE, 0xD2, 0x4C, 0x4C, 0xBC, 0x5D, 0xFD, + 0xF1, 0x26, 0x9F, 0x2C, 0x66, 0x62, 0x20, 0x7F, 0x7A, 0x2A, 0x6B, 0x2C, 0xFC, 0x20, 0xF2, 0x74, + 0xB5, 0xB6, 0x26, 0x59, 0x7A, 0x6F, 0x4F, 0xFE, 0xF6, 0x92, 0x9B, 0xC9, 0x40, 0x3E, 0xFA, 0x22, + 0x88, 0xFC, 0x2A, 0xEE, 0x6A, 0xE0, 0x1A, 0xBD, 0x4E, 0x66, 0x17, 0xD3, 0xB4, 0x1C, 0xD8, 0xC7, + 0xA8, 0x5A, 0xAD, 0x4A, 0x31, 0xBB, 0x0A, 0x7A, 0x66, 0x78, 0xD4, 0xE6, 0xDA, 0xAF, 0xF0, 0x31, + 0xF4, 0xCD, 0x80, 0x30, 0x9A, 0x45, 0x29, 0x76, 0x90, 0x21, 0xC5, 0x88, 0xFA, 0x80, 0x64, 0x59, + 0xED, 0x54, 0xF1, 0x12, 0x6D, 0x4C, 0x22, 0x1A, 0x6E, 0x3C, 0xC9, 0xC7, 0x8B, 0x1B, 0x91, 0x55, + 0x3D, 0xFD, 0x70, 0x36, 0x13, 0xF4, 0x13, 0xC4, 0x5F, 0x0E, 0x47, 0x3D, 0x14, 0x18, 0x27, 0x95, + 0xDF, 0xEB, 0xF5, 0x54, 0x72, 0x6F, 0x5E, 0xE4, 0x55, 0x4E, 0x5D, 0xEB, 0xFD, 0xBC, 0x10, 0xC5, + 0xFD, 0x6B, 0x31, 0x13, 0x63, 0x4C, 0xCC, 0xC9, 0x6C, 0xD6, 0x1B, 0x27, 0xB3, 0x99, 0x2F, 0xC2, + 0x2A, 0x08, 0x42, 0xAA, 0xFF, 0x3C, 0x13, 0x0F, 0x36, 0xF1, 0x40, 0xAD, 0xB6, 0xCA, 0x70, 0x3C, + 0x4D, 0x67, 0x93, 0x42, 0x64, 0x5C, 0x65, 0xB3, 0x73, 0x55, 0x4F, 0x7F, 0x0E, 0x7A, 0x57, 0xE9, + 0xAC, 0x12, 0x05, 0x80, 0xF9, 0x65, 0xD5, 0xBB, 0x49, 0xAA, 0xF1, 0x54, 0x94, 0xBE, 0x40, 0x97, + 0xE6, 0x09, 0x3E, 0x57, 0xA5, 0x9C, 0x63, 0x09, 0x88, 0x34, 0x1E, 0x8E, 0xFA, 0x33, 0x51, 0xED, + 0x64, 0x71, 0xD5, 0x93, 0xDF, 0x5F, 0xE4, 0x13, 0xD1, 0xBF, 0xCA, 0x0B, 0xBF, 0x9F, 0xED, 0xED, + 0x65, 0xBD, 0x0C, 0xAF, 0x17, 0xE8, 0x56, 0x1C, 0xC7, 0xF4, 0xA5, 0x77, 0xF6, 0xEC, 0xEC, 0xF9, + 0xD9, 0x8B, 0x8B, 0x77, 0x2F, 0xCE, 0x9F, 0x9C, 0xED, 0xED, 0xFD, 0x71, 0x37, 0x8E, 0x6D, 0x9E, + 0x7E, 0x90, 0x39, 0x0D, 0xEE, 0xED, 0xA5, 0xBD, 0xF9, 0xA2, 0x9C, 0xFA, 0x59, 0x10, 0x66, 0xC8, + 0xE5, 0x54, 0x5F, 0x88, 0x6A, 0x51, 0x64, 0x3B, 0xE9, 0x3A, 0x9C, 0x17, 0xE2, 0x56, 0x76, 0x88, + 0x7A, 0x91, 0xA2, 0x17, 0x94, 0x92, 0xE6, 0x8B, 0x52, 0x01, 0xE6, 0x75, 0x7A, 0x39, 0x4B, 0xB3, + 0x6B, 0xD9, 0xA3, 0xB4, 0x1F, 0x2C, 0xD3, 0x2B, 0x3F, 0x75, 0x87, 0x25, 0xEB, 0x1A, 0xA6, 0xA3, + 0x7E, 0x1A, 0xA7, 0x1B, 0x4A, 0xAF, 0x55, 0xA6, 0xD1, 0x3A, 0xCC, 0xC4, 0xC7, 0xAA, 0xDE, 0x20, + 0xA5, 0xFC, 0xBA, 0xC6, 0x6C, 0xC9, 0x76, 0x43, 0x6B, 0x4C, 0x3B, 0x66, 0x60, 0x39, 0xC9, 0x97, + 0xD5, 0x7E, 0xFC, 0x3C, 0xA9, 0xA6, 0xBD, 0xAB, 0x59, 0x8E, 0x7A, 0x8F, 0xC5, 0x5F, 0x3E, 0xE7, + 0xD7, 0x22, 0xC9, 0x26, 0xF9, 0x8D, 0x1F, 0x04, 0xEB, 0x3B, 0x4C, 0x9D, 0xF0, 0x0D, 0x92, 0x5C, + 0x0B, 0x5D, 0xEB, 0x57, 0xF7, 0x4F, 0x27, 0x3E, 0xB0, 0x49, 0x03, 0xAC, 0x5A, 0x87, 0x29, 0xD5, + 0xCA, 0x7D, 0x47, 0xFD, 0x94, 0xF5, 0xA4, 0x92, 0xA4, 0x41, 0xF8, 0xDE, 0x24, 0xA9, 0x92, 0x83, + 0xCB, 0xF2, 0xA0, 0x4A, 0x0A, 0x7C, 0xF0, 0x82, 0x3E, 0xBA, 0xBF, 0x2B, 0x56, 0x2B, 0xEF, 0xB7, + 0x1E, 0xA6, 0x4F, 0x0F, 0xB9, 0x55, 0x6C, 0x5A, 0x88, 0x2B, 0x95, 0x39, 0x5D, 0xAD, 0x76, 0xD3, + 0x5E, 0x9A, 0x8D, 0x67, 0x8B, 0x09, 0x46, 0x8C, 0x82, 0x98, 0x46, 0xA4, 0x94, 0xA8, 0xB2, 0x2A, + 0xDF, 0x82, 0xB0, 0xF8, 0x5E, 0xCF, 0xD3, 0x70, 0xD8, 0xC9, 0x16, 0xB3, 0x59, 0x7F, 0x7B, 0x7E, + 0x95, 0xE6, 0xA7, 0x31, 0x1E, 0xF6, 0xF1, 0x65, 0x3E, 0x4B, 0x2B, 0x4E, 0x1C, 0x1E, 0x8F, 0x02, + 0x00, 0x29, 0xDD, 0xDB, 0xC3, 0x1B, 0xF0, 0x28, 0x1D, 0xA4, 0x3D, 0xF4, 0x09, 0x10, 0x89, 0xA8, + 0xDE, 0xB5, 0x6A, 0x43, 0x60, 0xCE, 0x68, 0xCC, 0x0A, 0x63, 0x51, 0xC0, 0xAF, 0x0C, 0x40, 0x40, + 0x49, 0x34, 0xD8, 0xEA, 0xEB, 0x06, 0x93, 0x35, 0x10, 0xB2, 0x9E, 0xB0, 0xDC, 0x5C, 0x7C, 0xB0, + 0xB1, 0xB4, 0x2A, 0x9B, 0x73, 0x59, 0x2C, 0xDC, 0xB4, 0x9C, 0x13, 0x1A, 0x9C, 0xDD, 0x22, 0xB3, + 0x9F, 0x89, 0xBB, 0x1D, 0xF9, 0xE4, 0x55, 0x98, 0xC6, 0x32, 0x25, 0x52, 0x23, 0xB2, 0x89, 0x87, + 0xC9, 0x0C, 0x0B, 0x2A, 0xB2, 0xEB, 0xEF, 0x82, 0x40, 0x29, 0x42, 0x6A, 0x49, 0x59, 0x45, 0xA0, + 0xB8, 0xCD, 0xD3, 0xC9, 0xCE, 0x11, 0x46, 0x5C, 0xF5, 0xDE, 0x73, 0xB3, 0x48, 0xAB, 0xE2, 0x6A, + 0x78, 0x04, 0x78, 0xD8, 0x6F, 0x66, 0x55, 0x05, 0x61, 0x12, 0x8B, 0xF8, 0x4B, 0x39, 0x24, 0x55, + 0x62, 0x20, 0x90, 0x3B, 0x12, 0x91, 0x47, 0x74, 0x2D, 0xBB, 0x36, 0xF4, 0x95, 0x00, 0x22, 0x7A, + 0x33, 0x91, 0x5D, 0x57, 0xD3, 0x2F, 0x8F, 0x06, 0x55, 0x4F, 0x91, 0x21, 0x3D, 0xA0, 0x70, 0x16, + 0x13, 0xF2, 0x87, 0x69, 0x80, 0x51, 0x9D, 0x73, 0xE7, 0x7A, 0x1F, 0xC4, 0x7D, 0xE9, 0xA7, 0x41, + 0x0F, 0x48, 0x7F, 0x96, 0x8C, 0xB1, 0x5C, 0x0D, 0xB0, 0xCA, 0x38, 0x1D, 0x66, 0x23, 0xC0, 0x40, + 0xE0, 0x07, 0xBD, 0xC8, 0xF7, 0xF6, 0x0A, 0x3F, 0x0F, 0x06, 0x9E, 0x90, 0xE8, 0xE9, 0x71, 0x9D, + 0x71, 0xEC, 0xCF, 0x62, 0x4A, 0xF5, 0xF6, 0x67, 0xD1, 0x72, 0xDD, 0xAB, 0xF2, 0xD7, 0xDC, 0x29, + 0x49, 0xBA, 0x66, 0x81, 0x5C, 0x3F, 0xFE, 0xE1, 0x4F, 0xA5, 0x3F, 0x4C, 0x0E, 0x7E, 0x19, 0xED, + 0x07, 0x87, 0x29, 0x4D, 0x3E, 0x32, 0x3E, 0xCB, 0xEF, 0x44, 0x71, 0x9A, 0x94, 0x20, 0xDC, 0xFD, + 0xDB, 0xA4, 0xD8, 0x99, 0x31, 0x12, 0x12, 0x74, 0x5F, 0x89, 0xEB, 0xB3, 0x8F, 0x73, 0xBF, 0x0C, + 0x7A, 0x95, 0x28, 0x2B, 0x3F, 0x09, 0x82, 0x6A, 0x5A, 0xE4, 0x77, 0x3B, 0xF4, 0x8D, 0x80, 0x72, + 0x56, 0x14, 0x98, 0xA6, 0x7F, 0x7E, 0xB6, 0xAC, 0x50, 0xCD, 0x77, 0xF3, 0xB9, 0xAE, 0x66, 0x1D, + 0xED, 0x9C, 0xCF, 0x69, 0x36, 0x76, 0xBC, 0xCF, 0x96, 0xD9, 0xDA, 0xDB, 0x01, 0x5D, 0xBD, 0x4D, + 0x27, 0x60, 0x94, 0x04, 0x20, 0x4A, 0x4C, 0x90, 0x08, 0xBC, 0x27, 0x9E, 0x86, 0xD1, 0x3B, 0x1F, + 0xCA, 0xB5, 0xD7, 0xFB, 0x67, 0xB0, 0xC6, 0x0C, 0x8E, 0xD5, 0x0C, 0x16, 0x40, 0x95, 0xD5, 0xEA, + 0x28, 0x8E, 0xE5, 0x92, 0x39, 0x9D, 0xA5, 0x18, 0xF4, 0x2B, 0x94, 0x2A, 0xFD, 0x40, 0x01, 0x19, + 0xF3, 0xE9, 0xDD, 0xA6, 0x25, 0x56, 0xBF, 0xA0, 0x35, 0x46, 0xB9, 0xF2, 0x9B, 0xF9, 0x02, 0xF5, + 0xBE, 0xAE, 0xEE, 0x67, 0x02, 0x15, 0x50, 0xC9, 0x97, 0x45, 0x8E, 0x1E, 0x56, 0xF7, 0x6F, 0x92, + 0xD9, 0x42, 0xF8, 0xB2, 0x40, 0x8A, 0x75, 0x70, 0xEF, 0x05, 0xE1, 0x94, 0x1A, 0x23, 0x64, 0xB1, + 0x33, 0xBE, 0xDB, 0x41, 0x6B, 0xB1, 0x30, 0x77, 0x41, 0xE4, 0x67, 0x49, 0x59, 0x3E, 0x4B, 0x4B, + 0x3C, 0x41, 0x24, 0x00, 0x23, 0xC7, 0x9A, 0x03, 0x6A, 0x26, 0x68, 0x7E, 0xE2, 0xA1, 0xAF, 0x2E, + 0x6E, 0xE9, 0xF4, 0x81, 0x7D, 0x8C, 0xAA, 0xDE, 0x34, 0x29, 0xED, 0xD2, 0xB7, 0x65, 0x69, 0x1C, + 0x57, 0xC9, 0xAC, 0x14, 0xDE, 0xAE, 0x1A, 0x6D, 0x67, 0xAE, 0x20, 0x9C, 0xF0, 0x82, 0xA0, 0x69, + 0xDA, 0xC4, 0xD4, 0x7A, 0x49, 0x55, 0x01, 0x8F, 0x5E, 0x4F, 0x93, 0x49, 0x7E, 0x57, 0xA3, 0x14, + 0x28, 0xD5, 0x21, 0x12, 0x70, 0x6B, 0xAF, 0xF2, 0x9C, 0x59, 0x43, 0x60, 0xD6, 0x69, 0x2D, 0xD9, + 0xB7, 0x4B, 0x76, 0x27, 0xC5, 0xF7, 0x24, 0x1B, 0x53, 0x51, 0xD9, 0x06, 0x65, 0xD2, 0x8B, 0x5D, + 0x13, 0xCA, 0x0D, 0xB9, 0xAA, 0xC8, 0x65, 0x73, 0x83, 0x89, 0xEF, 0xBC, 0x9A, 0x35, 0xBF, 0x88, + 0x7D, 0x5A, 0x1E, 0xEB, 0xF0, 0x2A, 0x66, 0x1E, 0x9A, 0x5F, 0x5D, 0x95, 0xA2, 0xFA, 0x46, 0x90, + 0x50, 0x17, 0xCE, 0xF9, 0xA3, 0xEC, 0xE5, 0xF2, 0xFD, 0xDF, 0x69, 0x31, 0x46, 0xD5, 0x3A, 0xBE, + 0x4B, 0x33, 0xB4, 0x61, 0xE8, 0xF4, 0xDE, 0x9E, 0x05, 0xCF, 0x65, 0x3E, 0xB9, 0x67, 0xB0, 0xB7, + 0x09, 0x75, 0x96, 0x1F, 0xC8, 0xF5, 0xEC, 0x05, 0x83, 0x4A, 0xB5, 0x7E, 0x03, 0x96, 0x1C, 0x5E, + 0x73, 0x2B, 0x5E, 0x51, 0xCD, 0x08, 0xAB, 0x36, 0x42, 0x7A, 0x92, 0x16, 0xE1, 0x3B, 0x9E, 0x10, + 0x5A, 0x3D, 0xA2, 0x2F, 0xB8, 0x98, 0x01, 0xE1, 0xDC, 0x67, 0xAA, 0xAE, 0x80, 0x2A, 0x49, 0xFF, + 0x8B, 0x93, 0xE7, 0x67, 0x20, 0xA8, 0xA2, 0x77, 0xC5, 0x6C, 0x4C, 0xFD, 0xE2, 0x83, 0x1C, 0xCB, + 0xD3, 0x0C, 0xA2, 0xC3, 0x55, 0x32, 0x16, 0xA1, 0xFA, 0xD2, 0x3B, 0xA5, 0xA2, 0xC5, 0x82, 0xC8, + 0x62, 0x5C, 0x99, 0xD4, 0x2C, 0x47, 0xFA, 0xD5, 0x0C, 0x32, 0x15, 0x37, 0xE9, 0xEB, 0x7A, 0xB2, + 0xB0, 0x55, 0x53, 0x00, 0x5E, 0xE8, 0xCD, 0xF2, 0x64, 0x22, 0x09, 0x95, 0x1D, 0x4D, 0x21, 0x92, + 0xC9, 0xFD, 0xEB, 0x2A, 0xA9, 0xC4, 0xC0, 0xBF, 0x51, 0xEB, 0x69, 0xB5, 0x32, 0x9F, 0x93, 0xC9, + 0x84, 0x69, 0x2C, 0xA1, 0xBB, 0xC8, 0x20, 0xD0, 0x78, 0x4F, 0xCE, 0x9F, 0xA3, 0xD5, 0x8A, 0xD2, + 0x50, 0x1D, 0xF0, 0x31, 0xE4, 0xE1, 0xDE, 0x18, 0xD2, 0x45, 0xD3, 0x45, 0x0C, 0x35, 0x08, 0x6F, + 0x58, 0xF4, 0x20, 0x96, 0x1D, 0x11, 0x55, 0x08, 0x2F, 0x19, 0x4A, 0x6D, 0xEC, 0xA3, 0xA9, 0xAA, + 0x28, 0xC3, 0xAD, 0x22, 0x8C, 0xF1, 0xEE, 0x11, 0xD5, 0xC9, 0xCC, 0x50, 0x23, 0x2F, 0x2F, 0xAA, + 0x4B, 0xE2, 0x1B, 0x12, 0x8E, 0x19, 0xF2, 0xAA, 0x3C, 0x95, 0xCE, 0x73, 0x44, 0x62, 0xD4, 0xD2, + 0x72, 0x83, 0x27, 0x8B, 0x22, 0xA1, 0xDF, 0x48, 0x84, 0x4E, 0xA2, 0x98, 0x25, 0xF7, 0x51, 0xAA, + 0xD1, 0xA5, 0xD7, 0x26, 0x17, 0xB6, 0x89, 0x17, 0x8B, 0x9B, 0x4B, 0x51, 0x10, 0x7A, 0x96, 0xE2, + 0x6B, 0x80, 0xAF, 0xC2, 0x60, 0xC2, 0xB2, 0x23, 0x39, 0x35, 0x6B, 0x23, 0x83, 0x48, 0x3C, 0xF0, + 0x45, 0x2C, 0x34, 0x97, 0x0D, 0xBD, 0x00, 0x7C, 0x02, 0x83, 0x4A, 0x1B, 0x29, 0xC7, 0xE2, 0x8F, + 0x9F, 0xFB, 0x5D, 0x2D, 0xEC, 0x77, 0xD5, 0x0F, 0x20, 0x1E, 0xAD, 0x03, 0xFA, 0xFA, 0x67, 0x8C, + 0x92, 0xF8, 0xC2, 0xEE, 0xB1, 0xEA, 0x67, 0x11, 0xFB, 0x4B, 0x29, 0x6E, 0x60, 0x58, 0x0C, 0x38, + 0x92, 0x34, 0xC0, 0xD0, 0x90, 0xE7, 0x28, 0x14, 0x98, 0xE3, 0x9B, 0xFC, 0x56, 0xD4, 0xE6, 0xB1, + 0xC9, 0x33, 0xC3, 0x22, 0x08, 0x09, 0xB8, 0xC1, 0xBA, 0x2F, 0x68, 0xD2, 0x1F, 0xCC, 0x8C, 0xC5, + 0x78, 0x91, 0xDE, 0x88, 0x7C, 0x51, 0xF9, 0x3C, 0xFF, 0xE5, 0x6A, 0x95, 0xA3, 0x73, 0x90, 0x13, + 0xF0, 0xFF, 0xBD, 0x9A, 0x47, 0xBC, 0xE0, 0x93, 0xEC, 0x6D, 0xD5, 0x03, 0xB8, 0xC5, 0xC7, 0x73, + 0x2C, 0x05, 0x5E, 0x10, 0x07, 0xC7, 0xE8, 0x64, 0xA9, 0x67, 0xAE, 0x1A, 0xEE, 0xA6, 0x90, 0x74, + 0x07, 0x95, 0x42, 0xC1, 0x83, 0xE3, 0xE8, 0x68, 0xA4, 0x86, 0x97, 0xC7, 0x3A, 0x55, 0xC3, 0xB8, + 0xDC, 0x87, 0xA0, 0x72, 0x1C, 0x1D, 0x1C, 0x87, 0x19, 0x8F, 0xD2, 0x2F, 0xF7, 0xF3, 0xE0, 0x77, + 0x79, 0x10, 0x56, 0x43, 0x96, 0xEA, 0x6E, 0x92, 0x8F, 0xFE, 0x51, 0x28, 0x1F, 0xD3, 0xCC, 0x2F, + 0xC3, 0xFC, 0xE0, 0x38, 0x08, 0x20, 0x78, 0xDE, 0xC5, 0x87, 0xC3, 0x7F, 0xF4, 0x46, 0x9F, 0xFB, + 0x83, 0xF8, 0xA7, 0x5E, 0xEF, 0xF3, 0xE0, 0xA7, 0xDE, 0xAA, 0xF7, 0xF9, 0x61, 0x78, 0x16, 0x1F, + 0xE2, 0x15, 0x0F, 0x27, 0xF1, 0x61, 0x14, 0xFD, 0x34, 0xD9, 0xFF, 0xEC, 0x30, 0xBC, 0x88, 0x97, + 0x6B, 0x06, 0xF4, 0x79, 0xAC, 0xE1, 0x7C, 0x1A, 0x2F, 0x6F, 0x72, 0x28, 0x32, 0x82, 0x56, 0x55, + 0xE4, 0xF1, 0x33, 0x00, 0x5B, 0x78, 0x21, 0x3F, 0xCE, 0x44, 0x72, 0x2B, 0x74, 0xF2, 0xA2, 0xF2, + 0xD6, 0xE1, 0x87, 0xF8, 0xF0, 0x1F, 0xBE, 0x2D, 0xB2, 0xB2, 0xD9, 0xC0, 0x7E, 0xC3, 0x67, 0x31, + 0x31, 0xD2, 0xD7, 0xA2, 0xF2, 0x87, 0xDE, 0x18, 0xAB, 0xF8, 0x03, 0x10, 0x63, 0x72, 0x39, 0xD3, + 0x8F, 0x9C, 0x79, 0x31, 0xD7, 0x4F, 0x40, 0xD5, 0x0C, 0xCF, 0xC4, 0x72, 0x20, 0xF1, 0x62, 0x71, + 0x2E, 0xF4, 0x97, 0xBB, 0xA9, 0x10, 0x33, 0xBC, 0x60, 0x65, 0x3E, 0xA7, 0xF7, 0xD7, 0xE3, 0x22, + 0x9F, 0x21, 0xC1, 0xED, 0xA0, 0xE9, 0x95, 0x7A, 0x64, 0x8C, 0xC0, 0x73, 0xC9, 0xC2, 0x16, 0x4B, + 0x8A, 0xE6, 0x8D, 0xA7, 0xD8, 0x83, 0x44, 0x42, 0x4D, 0xCA, 0x27, 0x48, 0xF4, 0x65, 0x29, 0x1F, + 0xB9, 0x47, 0x79, 0x41, 0x5C, 0x98, 0xD7, 0xD6, 0x78, 0x9A, 0x64, 0xD7, 0x54, 0x55, 0x95, 0x2F, + 0xC6, 0x53, 0xAE, 0x49, 0xBF, 0x50, 0x1B, 0xFA, 0x19, 0x95, 0xEA, 0xC7, 0x31, 0x58, 0x02, 0x77, + 0x78, 0x9E, 0xA7, 0x04, 0x16, 0x6A, 0xC6, 0xBE, 0x51, 0x21, 0xFB, 0x86, 0xD6, 0xCC, 0x33, 0x03, + 0xCE, 0xBE, 0x9A, 0x6A, 0xAE, 0x45, 0x09, 0xAC, 0x10, 0xDC, 0xB4, 0x7D, 0x95, 0xFD, 0xB2, 0xEF, + 0xB2, 0x03, 0x57, 0x20, 0x6C, 0x34, 0x90, 0xCB, 0xD9, 0x82, 0xA0, 0x62, 0x32, 0xA1, 0xB8, 0x30, + 0x10, 0xE0, 0x87, 0xC5, 0xE5, 0x4D, 0x5A, 0xE9, 0x12, 0x69, 0xA6, 0x9F, 0x08, 0x84, 0x92, 0x92, + 0xE2, 0x67, 0x91, 0xA9, 0x87, 0x4B, 0x01, 0xEA, 0x27, 0xE4, 0xAB, 0xAC, 0x2D, 0xFD, 0x85, 0xAA, + 0x55, 0x83, 0x69, 0xD3, 0x4C, 0x8F, 0xE9, 0x2E, 0xBA, 0x5C, 0xD9, 0x8E, 0x0A, 0x12, 0xA7, 0xF0, + 0x9B, 0x5C, 0xE6, 0x72, 0x36, 0xE4, 0x3C, 0x8E, 0x82, 0xBE, 0xA6, 0x98, 0x3B, 0x1F, 0xA5, 0xD6, + 0x64, 0x05, 0x6F, 0x08, 0x5F, 0x62, 0x1D, 0x45, 0x9F, 0x2D, 0xCF, 0xF7, 0xF7, 0xD7, 0xFF, 0x24, + 0xF9, 0x65, 0x91, 0xCA, 0xC5, 0xBB, 0x5A, 0x51, 0x92, 0x29, 0xF9, 0xC4, 0xAF, 0x2C, 0x5B, 0xFF, + 0xE8, 0x88, 0xDF, 0xB6, 0x44, 0x2C, 0xC2, 0x8B, 0xA1, 0x18, 0xC5, 0xF4, 0x67, 0xB5, 0x5A, 0xAE, + 0xF9, 0xCD, 0xD6, 0xF0, 0x5A, 0xD1, 0x66, 0xE2, 0x90, 0xBA, 0xAA, 0x2C, 0x76, 0xE4, 0x57, 0x54, + 0xCA, 0x4A, 0x9B, 0x5C, 0xEF, 0x47, 0x10, 0x59, 0x33, 0xBD, 0x6E, 0xCB, 0x2F, 0xF2, 0x7E, 0xB9, + 0xBF, 0x8F, 0x62, 0x7A, 0x45, 0x0F, 0xB3, 0x61, 0x39, 0x1A, 0x11, 0x21, 0xC8, 0x7B, 0xC0, 0xA6, + 0xEB, 0x34, 0x4B, 0x66, 0xDF, 0x40, 0x19, 0x9B, 0x89, 0x42, 0x12, 0xAF, 0xBC, 0x37, 0xC1, 0x5C, + 0x5C, 0x33, 0x8E, 0x29, 0x95, 0x80, 0xBE, 0x18, 0x7E, 0x90, 0xAF, 0x1D, 0xA9, 0xC6, 0xF6, 0xF2, + 0xA9, 0xEC, 0xA5, 0xED, 0x60, 0x5B, 0x3A, 0x07, 0x01, 0xCF, 0x06, 0x69, 0x24, 0xB0, 0xBA, 0xA9, + 0x2B, 0xCF, 0x1D, 0x60, 0x3C, 0x23, 0x19, 0xC1, 0xCF, 0x49, 0x88, 0x43, 0x1F, 0x83, 0x70, 0x98, + 0x85, 0x20, 0x1E, 0x0E, 0x10, 0x5E, 0x28, 0xC2, 0x86, 0x74, 0x56, 0x4C, 0x75, 0xED, 0xBB, 0xA6, + 0xF6, 0xD5, 0xCA, 0xF0, 0x23, 0x1A, 0x5D, 0x8A, 0xAA, 0x00, 0xB3, 0x30, 0x93, 0x70, 0x0B, 0x3F, + 0x48, 0x59, 0x5A, 0x04, 0xBA, 0x87, 0x15, 0x31, 0x45, 0x5D, 0x3F, 0xD2, 0x99, 0xA3, 0x11, 0xCD, + 0x9E, 0x01, 0x37, 0x26, 0x17, 0x4C, 0xD6, 0x57, 0xAB, 0x46, 0xC2, 0x2E, 0x40, 0xA4, 0xE1, 0x23, + 0x38, 0x89, 0xA4, 0x9D, 0x66, 0x92, 0x95, 0x4F, 0x1B, 0xC5, 0x03, 0x43, 0x76, 0x59, 0x3F, 0x90, + 0xE6, 0x1A, 0x01, 0xDA, 0x9F, 0x0D, 0xA0, 0xE6, 0xC1, 0x56, 0x10, 0xA5, 0x31, 0xF1, 0x9C, 0x35, + 0x77, 0x71, 0x98, 0x87, 0x45, 0x98, 0x8C, 0xE2, 0xA7, 0xBE, 0x24, 0xE9, 0xD0, 0x5F, 0x08, 0x9B, + 0x20, 0xA1, 0xCF, 0x86, 0xC9, 0x08, 0xE3, 0xA3, 0x1F, 0x50, 0x4B, 0x12, 0xA3, 0x5F, 0xFB, 0x63, + 0x64, 0xCE, 0x01, 0x5E, 0x1E, 0x2E, 0x41, 0x60, 0xEA, 0x72, 0x70, 0x7F, 0xDA, 0x03, 0x0F, 0x39, + 0xBF, 0xBA, 0x8A, 0xF5, 0xC3, 0xDE, 0x5E, 0xA9, 0xF9, 0xED, 0x04, 0x88, 0x59, 0x30, 0xBF, 0x9A, + 0xCF, 0x20, 0xB3, 0xF8, 0x77, 0xA1, 0x47, 0xD2, 0xEE, 0x22, 0xCE, 0x07, 0x1A, 0x40, 0x7A, 0x76, + 0x55, 0x95, 0x66, 0x5E, 0x40, 0xE4, 0x1D, 0xE4, 0x6A, 0x19, 0x85, 0x7C, 0x61, 0x50, 0x53, 0x73, + 0xCA, 0x62, 0x1D, 0x97, 0xFD, 0x02, 0xCA, 0x14, 0x60, 0x89, 0xF1, 0xE3, 0x39, 0x2E, 0x5C, 0x39, + 0x54, 0x63, 0x32, 0x34, 0x2E, 0x8D, 0xC5, 0xC9, 0xC1, 0x41, 0x3F, 0x20, 0x8C, 0xA5, 0xF1, 0xC6, + 0x71, 0xA1, 0x07, 0x56, 0x36, 0xE0, 0x1E, 0x17, 0x61, 0x66, 0x46, 0xF7, 0x92, 0x44, 0x57, 0x74, + 0xBB, 0xEC, 0x11, 0x86, 0x70, 0xEF, 0xC3, 0xB4, 0x97, 0xCC, 0xE7, 0xB3, 0x7B, 0x0C, 0x76, 0x58, + 0x62, 0x79, 0xBB, 0x88, 0x0C, 0xDB, 0x19, 0x03, 0x39, 0x72, 0x47, 0xDC, 0x1E, 0x6F, 0xEA, 0x67, + 0x26, 0x31, 0x6B, 0x36, 0x8F, 0x1A, 0x5A, 0xCD, 0x67, 0xAA, 0x79, 0x68, 0xFB, 0xB2, 0x71, 0xA4, + 0x41, 0xA1, 0x0C, 0x64, 0x83, 0x41, 0x7F, 0xD1, 0xB5, 0xDC, 0xF4, 0x3C, 0x86, 0x8B, 0xD6, 0x1A, + 0x2D, 0x90, 0xA6, 0x66, 0xB2, 0xC4, 0xA3, 0xA1, 0x21, 0x93, 0x70, 0x3C, 0x9C, 0x8C, 0xE2, 0x45, + 0xD8, 0x21, 0x3B, 0x26, 0xE1, 0x22, 0xCC, 0x03, 0xBB, 0x98, 0xDE, 0xDB, 0xC5, 0x64, 0x27, 0xEF, + 0xB5, 0x2F, 0x20, 0xC3, 0x72, 0x5A, 0x3F, 0x27, 0xA5, 0xBC, 0x53, 0x7C, 0x49, 0xC3, 0x3C, 0x84, + 0x31, 0x17, 0x2C, 0x81, 0x66, 0x3E, 0x08, 0xA9, 0xF3, 0x95, 0xD8, 0xA1, 0xA2, 0xC3, 0xDC, 0xF4, + 0x66, 0xE4, 0x34, 0x86, 0x65, 0x6E, 0x20, 0x06, 0x10, 0x19, 0x24, 0x3B, 0x23, 0x24, 0x0B, 0x4F, + 0x87, 0x15, 0x30, 0xB9, 0x5A, 0xCB, 0x5E, 0xBC, 0x8C, 0x97, 0x12, 0xF0, 0x3C, 0x15, 0x4B, 0xBB, + 0xE8, 0x77, 0x8F, 0x21, 0xDD, 0x60, 0xD8, 0x9D, 0xDF, 0x8E, 0xE8, 0xDB, 0xD5, 0x95, 0x4E, 0x78, + 0x04, 0x7D, 0x90, 0xCB, 0xAB, 0xC4, 0x58, 0x0A, 0x67, 0x79, 0x25, 0x71, 0x41, 0xEB, 0xDB, 0x2E, + 0x33, 0xD1, 0x30, 0xFD, 0xD0, 0xB2, 0xB2, 0x4A, 0x66, 0x2E, 0x49, 0xC6, 0x0C, 0x15, 0xCF, 0x86, + 0xC5, 0x48, 0xD7, 0xED, 0x2C, 0x3A, 0x06, 0xF4, 0x8C, 0xD6, 0x65, 0x58, 0xEA, 0x95, 0xB9, 0x1E, + 0xEF, 0xED, 0xB9, 0xB4, 0x7B, 0x66, 0x6D, 0x0F, 0x29, 0x64, 0x36, 0x63, 0x8C, 0x36, 0xA3, 0x31, + 0xE6, 0x08, 0xC0, 0x98, 0xD9, 0x43, 0xDF, 0x2D, 0x5E, 0xDA, 0xE2, 0xB9, 0x94, 0xCA, 0x73, 0x6B, + 0x91, 0xCA, 0x02, 0x4B, 0x8E, 0xCB, 0x61, 0x3E, 0xEA, 0x9B, 0x89, 0x6F, 0xA2, 0x15, 0x52, 0xDA, + 0x68, 0x08, 0x1C, 0x0D, 0xD6, 0x34, 0x02, 0x94, 0x00, 0x28, 0x66, 0x29, 0x66, 0xED, 0x18, 0x92, + 0xAA, 0x26, 0x1B, 0xD3, 0x18, 0xE3, 0x6E, 0x77, 0x69, 0x5A, 0x1B, 0x91, 0xE9, 0x40, 0x6A, 0x66, + 0xFE, 0x24, 0xF4, 0x94, 0xC5, 0x2D, 0x01, 0x81, 0xED, 0xEA, 0xAE, 0x88, 0xA7, 0xA4, 0x9F, 0x69, + 0xF0, 0x89, 0x56, 0x77, 0xC5, 0xA6, 0xEE, 0x86, 0x15, 0x72, 0x5E, 0x8B, 0x42, 0x0E, 0xF4, 0x61, + 0x54, 0x90, 0xBA, 0xB9, 0xEE, 0xE3, 0xDC, 0x27, 0x2D, 0xE3, 0x39, 0x29, 0x1B, 0x79, 0x4C, 0x96, + 0x88, 0x12, 0x06, 0x2D, 0xC9, 0x9B, 0xB0, 0x2A, 0x98, 0x2A, 0x85, 0x33, 0x92, 0xED, 0xC7, 0xF4, + 0x67, 0x0A, 0x4D, 0x00, 0x26, 0x01, 0xAE, 0x41, 0xD5, 0x86, 0x85, 0x43, 0x92, 0x71, 0x02, 0x0E, + 0x2C, 0x4D, 0x64, 0x4C, 0x76, 0x32, 0xB2, 0x84, 0xE8, 0x8E, 0x25, 0x01, 0x55, 0x91, 0xF4, 0xD2, + 0x92, 0x0C, 0x23, 0x89, 0x1A, 0x43, 0x95, 0xC3, 0x8C, 0x33, 0x41, 0xF3, 0x63, 0xF9, 0xED, 0xE9, + 0xCD, 0x8D, 0x98, 0xA4, 0x20, 0x2D, 0x5D, 0x99, 0xD0, 0x32, 0xE5, 0x79, 0x22, 0xAE, 0x92, 0xC5, + 0x0C, 0xF6, 0x15, 0x41, 0x4D, 0xD1, 0x87, 0x20, 0x2C, 0x06, 0xFE, 0xC4, 0x2A, 0x99, 0xE3, 0x42, + 0xA0, 0x0A, 0x65, 0xAC, 0xFB, 0xE6, 0xE2, 0xF9, 0x33, 0x7E, 0x2C, 0x3D, 0xAC, 0x5B, 0x80, 0x3D, + 0xAD, 0xE4, 0x97, 0x32, 0x9C, 0xD1, 0x22, 0x0A, 0xA2, 0x09, 0x0B, 0xC6, 0xA7, 0x8B, 0xB2, 0xCA, + 0x6F, 0x74, 0xF7, 0x97, 0x97, 0x8B, 0x4B, 0x98, 0x3F, 0xCA, 0x68, 0x16, 0x4A, 0xC1, 0x2F, 0xC1, + 0x5B, 0xB4, 0x7B, 0xB4, 0xB6, 0xE6, 0x3A, 0xB2, 0x62, 0x6E, 0x30, 0xA7, 0x55, 0xD6, 0xD2, 0x26, + 0x3D, 0x1B, 0xDA, 0x18, 0xE4, 0x4F, 0xC2, 0x2A, 0x5C, 0x12, 0x3F, 0x20, 0x2D, 0x26, 0x05, 0x15, + 0xC0, 0xE4, 0x61, 0x60, 0x30, 0x69, 0xF6, 0xE6, 0x72, 0x40, 0x6A, 0x78, 0x04, 0x93, 0xBD, 0xBD, + 0xA6, 0x11, 0x72, 0x42, 0x43, 0x98, 0x34, 0x00, 0xB0, 0xB7, 0x67, 0xBA, 0x94, 0xEC, 0xED, 0x25, + 0xED, 0x8A, 0x26, 0xD0, 0xC6, 0xBF, 0xE1, 0x41, 0x3E, 0x4F, 0xE6, 0x6C, 0x74, 0x7B, 0x15, 0x2F, + 0x21, 0x80, 0x6A, 0x6C, 0xF9, 0x86, 0xA6, 0x9A, 0xED, 0x5E, 0xDF, 0xF4, 0x64, 0xB2, 0xCA, 0x6B, + 0x35, 0xD3, 0x6F, 0x48, 0x6F, 0x25, 0xC9, 0x25, 0xE3, 0xBC, 0x42, 0xD9, 0xC8, 0xB2, 0x1E, 0x49, + 0x9E, 0x03, 0xFC, 0x08, 0x39, 0xE9, 0x11, 0x15, 0xC8, 0xC9, 0x27, 0x24, 0x8D, 0x75, 0xD6, 0x03, + 0x36, 0xC9, 0x45, 0x99, 0xFD, 0x1E, 0xA8, 0x34, 0x9B, 0xC1, 0xA6, 0x77, 0x03, 0xF9, 0x75, 0xA7, + 0x9A, 0x26, 0xC0, 0x9D, 0xCC, 0x9A, 0x77, 0x76, 0x00, 0xA4, 0x1D, 0xA1, 0x0C, 0x1D, 0xF0, 0x9E, + 0xC1, 0x31, 0x64, 0xBE, 0x45, 0x3B, 0x9F, 0x2D, 0x4F, 0x8A, 0x22, 0xB9, 0xEF, 0x5D, 0x15, 0x30, + 0xA1, 0x67, 0x12, 0xE8, 0x01, 0xA9, 0xB6, 0x6B, 0xB2, 0xE1, 0x85, 0xE8, 0xA0, 0x76, 0x93, 0xE8, + 0x01, 0xED, 0xED, 0xE9, 0x7E, 0xD3, 0x0F, 0xF7, 0x9A, 0xF9, 0x8B, 0x24, 0xF1, 0x94, 0x59, 0x12, + 0x33, 0x9D, 0xBF, 0x46, 0x28, 0x77, 0x52, 0x3B, 0xEA, 0xB4, 0x27, 0x29, 0x3E, 0x2D, 0x11, 0x1A, + 0x77, 0xCA, 0xE3, 0x46, 0xF5, 0x3A, 0xBD, 0xC2, 0x32, 0xEC, 0xB3, 0xA5, 0x6E, 0xE7, 0xAB, 0xE5, + 0xD8, 0x9A, 0x4F, 0x88, 0x0B, 0x00, 0x1D, 0x12, 0xFC, 0xA2, 0x37, 0x2C, 0xF9, 0xF4, 0xDE, 0xC9, + 0x11, 0x12, 0xE3, 0x7C, 0x45, 0x80, 0xAB, 0xA7, 0x86, 0xFC, 0xE6, 0x54, 0xD1, 0x7B, 0x72, 0x72, + 0x71, 0xF2, 0xEE, 0xDB, 0xB3, 0x1F, 0xF8, 0x0B, 0x08, 0x11, 0xA1, 0x44, 0x4E, 0x86, 0xCF, 0xE5, + 0x2B, 0xC5, 0xAC, 0x1E, 0x5B, 0x43, 0x10, 0x32, 0x7F, 0x7E, 0x28, 0xFB, 0xD9, 0x1B, 0x58, 0x20, + 0x65, 0x7E, 0x89, 0xC5, 0x04, 0x86, 0xF3, 0xBB, 0x4C, 0x63, 0xF1, 0x8B, 0xE4, 0x46, 0x94, 0x5C, + 0x49, 0x1D, 0xE5, 0x29, 0x05, 0x28, 0xCD, 0xA4, 0x01, 0x78, 0xFD, 0x0E, 0x62, 0xD1, 0x42, 0x9C, + 0x62, 0xC2, 0x2F, 0x93, 0xF1, 0x07, 0x6B, 0x6A, 0x59, 0xDE, 0x2A, 0xC4, 0x5B, 0x97, 0xA4, 0xD3, + 0x8D, 0x77, 0x50, 0xFB, 0x53, 0x35, 0xCB, 0x0E, 0xD7, 0x7C, 0xC5, 0xC0, 0x97, 0xDD, 0x33, 0x23, + 0x70, 0xCB, 0x9C, 0x17, 0xA7, 0xBC, 0xD0, 0x75, 0x59, 0xAA, 0x15, 0x72, 0xA1, 0xA9, 0x80, 0x0B, + 0xD6, 0xEA, 0xA6, 0xF9, 0x17, 0x77, 0xFC, 0x01, 0x99, 0xDB, 0x2E, 0x4F, 0x65, 0x51, 0x74, 0x1B, + 0xD9, 0x79, 0x73, 0xF6, 0xEA, 0xF5, 0xD3, 0xF3, 0x17, 0xBE, 0xAE, 0xD6, 0x63, 0x97, 0xB0, 0xE7, + 0x66, 0x21, 0xFB, 0x1A, 0xBE, 0x5B, 0x6B, 0xB5, 0xB4, 0x54, 0xFF, 0xFE, 0x87, 0x7C, 0xB1, 0x33, + 0x4D, 0x6E, 0x81, 0xE8, 0xF9, 0x4E, 0x7A, 0x33, 0x97, 0xE0, 0x46, 0xEB, 0x62, 0x47, 0x15, 0xBE, + 0x11, 0xF0, 0x0A, 0x4F, 0x76, 0x3C, 0xAA, 0xC0, 0x0B, 0x77, 0x00, 0xCB, 0x1D, 0x01, 0x60, 0xEE, + 0x8C, 0x61, 0x20, 0x22, 0xDB, 0x47, 0xB5, 0xFB, 0xFB, 0x5A, 0x5F, 0x34, 0x14, 0x6C, 0x67, 0x2E, + 0xCB, 0x9E, 0xB7, 0xCF, 0x03, 0xA5, 0x3A, 0xDC, 0xBC, 0x66, 0x12, 0x6D, 0x66, 0x9D, 0x55, 0x57, + 0xB3, 0x5E, 0x4B, 0x8C, 0x7D, 0x0B, 0xC3, 0x38, 0xA9, 0xE1, 0x84, 0xBB, 0x8D, 0x61, 0xD9, 0xC2, + 0xC9, 0x0C, 0x53, 0xEF, 0xA1, 0x04, 0x50, 0xCF, 0x55, 0xEE, 0xAA, 0x81, 0xC4, 0x26, 0x69, 0xB9, + 0xD5, 0x66, 0x4A, 0xE4, 0x88, 0xEA, 0x58, 0x96, 0xC6, 0xF2, 0x5D, 0x31, 0x84, 0x53, 0xAA, 0x47, + 0x51, 0xDB, 0xA0, 0x2F, 0x7D, 0x0A, 0x31, 0xB4, 0x97, 0xB4, 0x45, 0xDE, 0x56, 0x2B, 0x59, 0x50, + 0xA2, 0xBA, 0xAE, 0x1F, 0xEA, 0x43, 0xAB, 0x49, 0x33, 0xF3, 0x92, 0x98, 0x55, 0x3D, 0xEE, 0x6C, + 0x09, 0x26, 0xD0, 0x93, 0xBD, 0x47, 0x99, 0x76, 0xF3, 0xB6, 0xD8, 0xCB, 0x9E, 0x65, 0xA3, 0x1E, + 0x97, 0xED, 0x5D, 0x96, 0xB6, 0xA8, 0xED, 0x80, 0x2A, 0xE6, 0xDA, 0xE6, 0xE5, 0x47, 0x70, 0xDD, + 0x69, 0x7E, 0x07, 0x36, 0xAF, 0xA1, 0xD3, 0x6D, 0xBE, 0xBF, 0x82, 0x56, 0x8E, 0x4C, 0x3C, 0xAE, + 0xC6, 0x2A, 0x21, 0xBE, 0x20, 0xD3, 0x27, 0xE8, 0x79, 0x91, 0xDF, 0xDB, 0xF6, 0xC0, 0x3A, 0x30, + 0xEA, 0x76, 0xFA, 0xD2, 0xB4, 0x1E, 0x84, 0xED, 0x21, 0x4C, 0xEC, 0x18, 0x0C, 0x2E, 0x35, 0xAC, + 0xB5, 0x0E, 0x08, 0xB8, 0x69, 0x41, 0x0B, 0xDA, 0xBA, 0xD3, 0xED, 0x5C, 0xBF, 0xED, 0x75, 0x2C, + 0x3B, 0x49, 0x08, 0xFA, 0xDC, 0x1A, 0x3B, 0x44, 0xA0, 0x1B, 0x0A, 0x90, 0x01, 0x99, 0xBE, 0x0E, + 0x4C, 0xAB, 0x53, 0x96, 0x62, 0x9E, 0xA4, 0xE5, 0x4D, 0x5A, 0x96, 0x4E, 0x9B, 0xAE, 0x36, 0x4A, + 0xBE, 0xAC, 0x36, 0x03, 0x53, 0x33, 0xA9, 0x6A, 0x5C, 0xAF, 0x41, 0xC5, 0x32, 0xE3, 0x3B, 0x0D, + 0xA5, 0x35, 0xCB, 0x0C, 0xB3, 0xC7, 0x96, 0xF6, 0x64, 0x9E, 0x7A, 0xE1, 0xEF, 0x87, 0xDA, 0xEA, + 0x3E, 0x91, 0xAD, 0xC6, 0x0A, 0x8D, 0x47, 0xBF, 0x0F, 0xDF, 0xF6, 0xEA, 0xFD, 0xA1, 0x75, 0xFB, + 0x16, 0xA2, 0xC4, 0x3B, 0xFF, 0x6D, 0xA0, 0x28, 0xF9, 0xCF, 0x8F, 0x59, 0x17, 0xB0, 0xEE, 0x57, + 0x30, 0x2E, 0xAF, 0xAB, 0xFC, 0xFA, 0x7A, 0x46, 0x44, 0xB9, 0x86, 0xF6, 0xBD, 0xB2, 0xE6, 0x55, + 0x49, 0x8A, 0x34, 0x39, 0x60, 0xEB, 0x16, 0x59, 0x65, 0xEA, 0x39, 0x2D, 0xA2, 0x70, 0x55, 0x94, + 0x1B, 0x60, 0xB9, 0x05, 0xA2, 0xFC, 0xDB, 0xF3, 0xF6, 0xF3, 0xB6, 0x79, 0xE3, 0xD6, 0x6A, 0x13, + 0xC7, 0x93, 0x66, 0x55, 0x99, 0x5F, 0x6C, 0x3B, 0x1E, 0x58, 0x84, 0xCC, 0xBA, 0x5A, 0x39, 0xBE, + 0x23, 0x76, 0x66, 0xC6, 0xCA, 0x4E, 0x8D, 0xDC, 0xC6, 0x1F, 0xE8, 0x07, 0x03, 0x93, 0x18, 0x79, + 0xBA, 0x20, 0xAD, 0x76, 0x7E, 0x1E, 0xD0, 0x53, 0x54, 0x39, 0x6A, 0xD3, 0x67, 0xCE, 0x98, 0xAC, + 0xD2, 0x74, 0x38, 0x3C, 0x39, 0xF8, 0x71, 0x74, 0x78, 0x1D, 0x82, 0xCB, 0x78, 0x07, 0xA0, 0x62, + 0x75, 0x3F, 0x62, 0xB0, 0x11, 0x21, 0xE4, 0xE4, 0x74, 0x62, 0x84, 0x1C, 0x76, 0xAC, 0xE6, 0x8F, + 0x30, 0x82, 0x59, 0x58, 0x13, 0xF9, 0xDC, 0xA5, 0xCC, 0xAA, 0xBC, 0xA5, 0x2A, 0x5B, 0xEA, 0x0A, + 0xFA, 0xDD, 0x20, 0x17, 0x81, 0x9E, 0xDC, 0x60, 0x4D, 0xA8, 0xF6, 0xB3, 0xAE, 0xFE, 0x3B, 0x16, + 0xC8, 0x9E, 0xA0, 0x3E, 0x83, 0x2D, 0x5A, 0x3A, 0x6B, 0xE2, 0x90, 0x6E, 0xD4, 0xDB, 0xFF, 0x0C, + 0xF5, 0x11, 0x1B, 0x55, 0xF2, 0x4C, 0xAB, 0xB4, 0xA5, 0x0C, 0x1B, 0x8B, 0x4B, 0xB9, 0xA9, 0x56, + 0x92, 0x96, 0x66, 0xCD, 0xCF, 0xB1, 0x5C, 0x1B, 0x18, 0xE0, 0x51, 0x4D, 0x8E, 0x2B, 0xF9, 0xCA, + 0x25, 0x87, 0x5E, 0xD6, 0xE2, 0x50, 0x6A, 0x5A, 0xE4, 0x25, 0xA4, 0xEF, 0xBA, 0x82, 0x34, 0x13, + 0x75, 0xF5, 0xE8, 0xF0, 0x1F, 0x08, 0x5B, 0x62, 0x15, 0x89, 0x02, 0x47, 0x60, 0xA5, 0x2C, 0x4E, + 0x2A, 0xFF, 0x28, 0xA8, 0xCF, 0xF4, 0x7E, 0xA6, 0x15, 0xB2, 0x30, 0xD3, 0xDE, 0xD8, 0x90, 0x3C, + 0xD5, 0xF1, 0x2F, 0xB6, 0x17, 0xD0, 0xA1, 0x08, 0xBA, 0xA2, 0x3D, 0x34, 0x2D, 0x1F, 0x52, 0xE6, + 0xEB, 0x2D, 0x40, 0x05, 0x19, 0x90, 0x0E, 0x40, 0x97, 0xD1, 0x71, 0x11, 0x96, 0x49, 0x81, 0xD8, + 0xD6, 0x2F, 0x6C, 0xDC, 0x94, 0x4B, 0xE8, 0x29, 0x91, 0xE8, 0xE1, 0xEF, 0x7E, 0xDD, 0x0F, 0x28, + 0x0D, 0xAB, 0x17, 0xF9, 0x3C, 0x9C, 0x89, 0xAB, 0x0A, 0x59, 0xE8, 0xA7, 0x33, 0xCF, 0x33, 0x7C, + 0x80, 0xAC, 0x3E, 0xCF, 0xA5, 0xF3, 0x23, 0x02, 0x18, 0x7D, 0xAE, 0x56, 0x3B, 0x24, 0x75, 0x25, + 0x26, 0x81, 0x4B, 0x60, 0x12, 0xBF, 0x8E, 0x97, 0x6C, 0xA5, 0xBE, 0x4D, 0x66, 0xD1, 0x9F, 0xC5, + 0x1F, 0x43, 0xCC, 0xCB, 0x65, 0x9E, 0x14, 0x93, 0x08, 0x0A, 0x1B, 0x20, 0x36, 0x81, 0xEE, 0x72, + 0x8C, 0x60, 0xA0, 0x45, 0x09, 0x8F, 0xC1, 0x54, 0xDA, 0xE8, 0xEF, 0x20, 0x95, 0xD3, 0x67, 0x36, + 0x92, 0x93, 0x6A, 0x13, 0xBE, 0x71, 0x2A, 0xF1, 0xFC, 0x8C, 0x17, 0xEE, 0xEA, 0x52, 0x9A, 0x3D, + 0x02, 0xCF, 0xD6, 0xE9, 0xA9, 0x34, 0x4F, 0x56, 0x8D, 0xBC, 0x2A, 0x61, 0x25, 0x75, 0x4E, 0xE4, + 0x55, 0x4D, 0xF9, 0x32, 0xC1, 0x54, 0x22, 0x5B, 0xB5, 0x15, 0xC8, 0xC6, 0xED, 0xFB, 0x3A, 0xFC, + 0x36, 0xF6, 0x28, 0xC4, 0xC6, 0x0B, 0xBF, 0x8F, 0x3D, 0x5A, 0x8A, 0x5E, 0xF8, 0x43, 0xEC, 0x61, + 0xC8, 0x48, 0xF9, 0x7B, 0xEC, 0x51, 0x90, 0x1D, 0x9E, 0xFE, 0x1A, 0x93, 0x2A, 0x90, 0xDF, 0xD1, + 0xE8, 0xA3, 0xBF, 0x87, 0xFC, 0xFC, 0x8A, 0x3E, 0x45, 0x3F, 0x68, 0x29, 0xFC, 0x47, 0x45, 0xBB, + 0x9B, 0xF2, 0x38, 0x2F, 0xA9, 0x72, 0x31, 0x17, 0x78, 0x0C, 0x14, 0x05, 0x4E, 0x2B, 0x71, 0x53, + 0xB2, 0xD4, 0xAA, 0x12, 0x34, 0x14, 0xDC, 0x34, 0x22, 0xC6, 0x86, 0xFB, 0xD7, 0x32, 0x97, 0x2F, + 0x69, 0xB0, 0x13, 0xA8, 0xC5, 0x26, 0xE5, 0x35, 0xE0, 0x82, 0x71, 0x9B, 0x24, 0x1E, 0xA6, 0x74, + 0x60, 0xA9, 0xB2, 0x36, 0xF9, 0x35, 0x2D, 0x93, 0xEF, 0xE3, 0x23, 0x27, 0x09, 0x3E, 0xC3, 0x2A, + 0x31, 0x49, 0xEF, 0xD0, 0xFF, 0xAB, 0xF4, 0x3A, 0x36, 0x72, 0xD6, 0x29, 0xBF, 0xFB, 0xA9, 0xE9, + 0x3F, 0x50, 0x72, 0x9C, 0x50, 0xA0, 0xA0, 0xEE, 0x9E, 0x0D, 0x02, 0xF1, 0x60, 0x72, 0x2D, 0xD8, + 0x0F, 0x74, 0x60, 0xB3, 0x35, 0x58, 0x8F, 0xAE, 0x47, 0x76, 0x67, 0x31, 0xA7, 0x68, 0x3E, 0x8C, + 0xC7, 0xCB, 0x33, 0xC7, 0xC9, 0x92, 0x66, 0x3B, 0x9B, 0x7C, 0xD2, 0x90, 0xAA, 0x93, 0xDB, 0xF4, + 0x9A, 0xAA, 0x26, 0x5F, 0xD8, 0x05, 0x15, 0x7A, 0x49, 0x5E, 0x93, 0xF2, 0x4B, 0x3D, 0x04, 0xE5, + 0x44, 0x91, 0x46, 0x3B, 0x6D, 0x49, 0x53, 0x1E, 0xD1, 0x97, 0xE6, 0x9B, 0xED, 0x4A, 0xD3, 0x98, + 0x57, 0xFA, 0x35, 0x41, 0x58, 0x53, 0x67, 0xC3, 0x2B, 0xBE, 0x5E, 0x6F, 0x64, 0xD0, 0x7A, 0xFC, + 0xDE, 0x9A, 0x10, 0xCB, 0x30, 0x68, 0x46, 0x5D, 0xFF, 0xDB, 0x80, 0x53, 0xDF, 0xC2, 0x61, 0xF4, + 0x46, 0x86, 0x74, 0x20, 0x83, 0xF5, 0xE3, 0x4F, 0xD3, 0xC9, 0x44, 0xC0, 0xB4, 0x31, 0xAE, 0xA9, + 0x4C, 0xA4, 0x58, 0xF2, 0xBB, 0xAC, 0x70, 0x4D, 0x78, 0xDA, 0xA8, 0xF7, 0x7B, 0xA4, 0x12, 0x56, + 0xB0, 0x4C, 0x03, 0x9B, 0x75, 0x13, 0x55, 0x8E, 0x82, 0xB0, 0x73, 0x8E, 0x08, 0x15, 0x0F, 0xA8, + 0xDA, 0x70, 0xA7, 0x91, 0x28, 0x17, 0x43, 0xB3, 0x1B, 0x7E, 0xEE, 0x77, 0x4D, 0xE5, 0xF8, 0x7E, + 0x8C, 0x91, 0x90, 0x75, 0x23, 0xC4, 0x43, 0x52, 0x3C, 0x55, 0x08, 0xED, 0xD7, 0xF1, 0x3B, 0xE8, + 0xC2, 0xF7, 0x35, 0x17, 0x66, 0xB9, 0xB2, 0xA3, 0xE3, 0xC7, 0xCD, 0x32, 0xE8, 0xC3, 0x27, 0x37, + 0x11, 0xD4, 0x10, 0x7B, 0x6F, 0xCF, 0x7D, 0xEB, 0xD9, 0x8A, 0x77, 0xEB, 0x8D, 0x1B, 0xFD, 0x79, + 0x31, 0x9F, 0x80, 0xA9, 0x9A, 0x06, 0x5B, 0x4D, 0x80, 0x2E, 0x9A, 0x8F, 0x36, 0xC4, 0xCE, 0x44, + 0xE0, 0xA8, 0xF8, 0x03, 0x2E, 0xD4, 0x98, 0xFC, 0xC8, 0x24, 0x22, 0xC6, 0x14, 0xB3, 0x23, 0x65, + 0xA4, 0xB0, 0xB3, 0x7F, 0xE0, 0x9D, 0x55, 0x4E, 0xF3, 0x5B, 0xA7, 0x0F, 0xED, 0x05, 0x28, 0xBF, + 0xD4, 0xA7, 0xB3, 0x39, 0x93, 0xDA, 0xE4, 0x60, 0x17, 0xF9, 0x53, 0xE4, 0x7A, 0x4A, 0x2E, 0x6C, + 0xBF, 0xA3, 0x7E, 0x19, 0xE3, 0xF1, 0xA5, 0x43, 0xBE, 0x8C, 0x23, 0x1B, 0xB6, 0xC5, 0x2F, 0x8E, + 0x1C, 0x8F, 0x50, 0x83, 0x24, 0xD5, 0xE2, 0x1B, 0x48, 0x6C, 0x6A, 0x1A, 0x0F, 0x3C, 0x20, 0x30, + 0xEB, 0x0C, 0x66, 0xDD, 0x84, 0x46, 0x27, 0xE1, 0xF1, 0x72, 0xDB, 0xEC, 0xF2, 0xD7, 0x75, 0xF1, + 0x37, 0x89, 0xEE, 0xCA, 0x3C, 0xE6, 0x20, 0xA1, 0xB5, 0x21, 0x89, 0x2F, 0xD3, 0xC1, 0xB7, 0xD1, + 0xF7, 0x7D, 0x77, 0x9D, 0x64, 0x2E, 0x09, 0x1E, 0x0A, 0x30, 0x6E, 0x4B, 0xE0, 0x6A, 0x16, 0xF4, + 0x25, 0x02, 0x5E, 0xBF, 0x0E, 0xF1, 0xE7, 0xBB, 0x5E, 0x5B, 0x60, 0xA9, 0xC3, 0x92, 0x72, 0xB5, + 0x74, 0x7B, 0x8A, 0x03, 0x5A, 0x82, 0xB7, 0xCE, 0x7C, 0x4B, 0x10, 0xA0, 0x50, 0xBD, 0xC1, 0xE4, + 0xAE, 0xDF, 0x49, 0x4D, 0xE0, 0xF5, 0x5D, 0x3A, 0x17, 0x46, 0x6A, 0xAE, 0x64, 0x00, 0x67, 0x72, + 0x89, 0xEA, 0x1B, 0x84, 0x39, 0x60, 0xB8, 0x7E, 0x11, 0xFF, 0x49, 0xC3, 0xD9, 0xCA, 0x88, 0x87, + 0xCD, 0xBC, 0xFD, 0x0E, 0xAA, 0x0E, 0x25, 0xC7, 0x85, 0x81, 0x40, 0xD4, 0xDE, 0xDF, 0x23, 0x98, + 0x36, 0xBA, 0xA8, 0xDE, 0xB2, 0x86, 0x7B, 0x9A, 0x05, 0xB3, 0xA3, 0x25, 0xAB, 0x8F, 0xDB, 0xF8, + 0xD0, 0xEB, 0x93, 0x57, 0x69, 0x7D, 0x52, 0x7D, 0x25, 0x4B, 0x54, 0xA8, 0x64, 0x00, 0x4C, 0xA2, + 0x5B, 0xBD, 0x9C, 0x43, 0x2C, 0xB3, 0xAE, 0xCA, 0x6D, 0x5C, 0x41, 0x67, 0xFD, 0xB2, 0x2C, 0x57, + 0xBE, 0xB1, 0x34, 0x7B, 0xD4, 0xBB, 0x4B, 0x6B, 0x9A, 0x13, 0xD4, 0x17, 0x9B, 0x04, 0xDC, 0xDE, + 0x5E, 0x17, 0x7F, 0x52, 0xA9, 0x04, 0x34, 0xE6, 0x38, 0x2D, 0x7E, 0xB1, 0xF1, 0x8B, 0x15, 0xE9, + 0xC8, 0xF5, 0xD0, 0x62, 0x50, 0xA4, 0xA2, 0xCC, 0x45, 0x26, 0xA3, 0xE2, 0x54, 0x3A, 0x85, 0xE8, + 0xED, 0xED, 0xC9, 0x80, 0x82, 0xD6, 0x87, 0x41, 0x57, 0x15, 0x7E, 0x8B, 0xC3, 0x57, 0xF2, 0x4D, + 0x94, 0x30, 0x59, 0xF6, 0xC6, 0x2C, 0x40, 0x7E, 0x1F, 0x44, 0xED, 0x6C, 0xFA, 0x9B, 0x0E, 0xFC, + 0x6D, 0x62, 0x90, 0xAD, 0x68, 0x6F, 0xCF, 0x3C, 0xEA, 0x00, 0xD0, 0xE3, 0xC1, 0x51, 0xD4, 0xD5, + 0xD0, 0x41, 0xB3, 0x1D, 0x84, 0xCE, 0xFC, 0xA7, 0xC6, 0x8F, 0xC1, 0xB6, 0xFB, 0xB8, 0xB1, 0x61, + 0x35, 0xC3, 0xB5, 0x45, 0xF7, 0x20, 0x46, 0xBA, 0x24, 0xA6, 0x2D, 0x52, 0xED, 0xED, 0x31, 0x0B, + 0x52, 0x6F, 0x7E, 0xEB, 0x7B, 0x47, 0x91, 0xD8, 0x89, 0x28, 0x6A, 0x22, 0x61, 0xF8, 0xE7, 0xA3, + 0xA3, 0xFD, 0x4D, 0x34, 0xBF, 0x2F, 0x49, 0x7B, 0x93, 0x69, 0xC3, 0xC0, 0x77, 0xDD, 0x20, 0xEA, + 0x75, 0xE3, 0xA8, 0x5C, 0x15, 0x08, 0xB9, 0x29, 0x92, 0x6B, 0x96, 0xA8, 0x5A, 0x0B, 0xA1, 0xAD, + 0x7B, 0x92, 0x1A, 0xD3, 0x9E, 0x9E, 0x41, 0xE7, 0xFA, 0x54, 0x39, 0x3A, 0x09, 0xC0, 0xC6, 0x75, + 0x69, 0xE3, 0x5E, 0x5A, 0x65, 0x98, 0x50, 0x6C, 0x34, 0x51, 0x60, 0x69, 0xF9, 0xBA, 0xF0, 0x01, + 0x77, 0x1A, 0xFA, 0x5D, 0xD4, 0xD9, 0x31, 0x2B, 0x43, 0x7E, 0x42, 0xBF, 0x4C, 0x40, 0x4F, 0xAB, + 0x4C, 0xBA, 0xBD, 0x0C, 0x44, 0xFE, 0xEE, 0xA1, 0x04, 0x6B, 0x4D, 0x01, 0x95, 0x7E, 0x7B, 0x98, + 0x66, 0x88, 0xC2, 0x5B, 0x51, 0x78, 0x53, 0x02, 0x2D, 0xFD, 0x30, 0x95, 0xA1, 0x12, 0x4A, 0xCD, + 0xA7, 0x9F, 0x6B, 0x32, 0x74, 0x07, 0x4D, 0x1A, 0xFF, 0xD7, 0x21, 0xAB, 0xBD, 0xA3, 0x3E, 0xE3, + 0x65, 0xDB, 0x58, 0x55, 0x23, 0xED, 0x68, 0xB6, 0xCE, 0xCC, 0xAD, 0x73, 0xDD, 0xD5, 0x3D, 0xA4, + 0xD9, 0xCB, 0x06, 0xA9, 0x76, 0xE3, 0x98, 0x17, 0x8A, 0x5A, 0xE8, 0xEA, 0x70, 0xE4, 0x72, 0x4F, + 0x1B, 0xF0, 0x66, 0xDB, 0xFC, 0xEA, 0xFE, 0xBC, 0xC0, 0x5E, 0xA1, 0xFA, 0xF6, 0x0F, 0x32, 0xD5, + 0x7C, 0xAB, 0xF5, 0xF8, 0x7B, 0xDF, 0xA9, 0x82, 0xBD, 0xA4, 0x35, 0xC4, 0x27, 0x75, 0xCD, 0x5A, + 0x4D, 0x49, 0x86, 0x50, 0x56, 0xD3, 0x5A, 0xD0, 0x4B, 0x5B, 0x68, 0xE1, 0xC0, 0xC5, 0xAE, 0xF4, + 0x4F, 0x15, 0x8C, 0x82, 0x7E, 0xDB, 0x38, 0xDB, 0x96, 0x57, 0x1A, 0x68, 0xB2, 0xAC, 0x85, 0x9F, + 0x20, 0x1C, 0x73, 0x92, 0x22, 0x42, 0x8B, 0x55, 0xEA, 0x34, 0x24, 0xF7, 0x51, 0x54, 0x42, 0xF3, + 0x8C, 0x32, 0x72, 0x52, 0x90, 0x71, 0x85, 0xFB, 0xF1, 0x54, 0xAB, 0x47, 0xC6, 0xC4, 0xCC, 0x68, + 0xB2, 0x41, 0xC5, 0xB2, 0xE0, 0x6C, 0x8F, 0x48, 0x8F, 0xA1, 0x5D, 0xA8, 0x9F, 0x76, 0x98, 0x8C, + 0x55, 0x21, 0x98, 0x72, 0x54, 0x52, 0xD3, 0x60, 0x38, 0x5E, 0x14, 0x34, 0xE9, 0x9E, 0x15, 0xA5, + 0x34, 0x7E, 0x0C, 0xEB, 0xDB, 0x3F, 0x46, 0x1B, 0x5B, 0xB6, 0xA1, 0x52, 0x55, 0x7C, 0xD4, 0xAF, + 0xBE, 0x30, 0xA1, 0x52, 0xD5, 0xFE, 0x3E, 0x62, 0x4C, 0x6A, 0x71, 0xA1, 0x4F, 0x69, 0xB7, 0x03, + 0xEC, 0x80, 0x1B, 0x4C, 0x24, 0x0C, 0x6F, 0x98, 0xBC, 0xD0, 0xE1, 0x63, 0xC4, 0xCD, 0xC6, 0xDD, + 0xB3, 0x1F, 0x2C, 0xA9, 0x8A, 0x06, 0xC5, 0xB0, 0x43, 0xE5, 0x8F, 0x00, 0xFD, 0xA6, 0xA1, 0x86, + 0x6C, 0x69, 0xC4, 0x88, 0x2F, 0xB1, 0x34, 0x3F, 0xC0, 0xE4, 0xDB, 0x12, 0xFA, 0x2D, 0x13, 0x6F, + 0x8B, 0xC7, 0xAB, 0xD5, 0xA7, 0xA2, 0x99, 0xDC, 0x36, 0xD3, 0xF4, 0xFF, 0x35, 0xA1, 0x22, 0x36, + 0x80, 0x44, 0xB3, 0x07, 0x06, 0x09, 0xEA, 0x1A, 0xF8, 0xEE, 0x22, 0xD2, 0x1E, 0x0D, 0xDD, 0xF7, + 0x78, 0xDB, 0xC7, 0xD5, 0xAA, 0x93, 0xF1, 0x74, 0xAB, 0x20, 0x31, 0x1C, 0xAE, 0xF5, 0x0F, 0xFF, + 0x46, 0x13, 0x6B, 0x4D, 0xB3, 0xDA, 0x2B, 0xDB, 0x2C, 0x9F, 0x8B, 0x9C, 0xE9, 0x09, 0xAF, 0xEE, + 0x4F, 0xD6, 0x71, 0xC2, 0xBC, 0x0B, 0x55, 0xA0, 0x5E, 0x15, 0x71, 0xAA, 0xFA, 0xD4, 0xA4, 0x5B, + 0x59, 0x88, 0xCF, 0x49, 0x57, 0xB1, 0x02, 0xF1, 0x04, 0xDA, 0x90, 0xD0, 0x52, 0x39, 0xC7, 0x71, + 0x46, 0x34, 0x0E, 0x01, 0x03, 0xE3, 0x81, 0x57, 0x57, 0xA3, 0xA5, 0x31, 0x23, 0x6A, 0xA4, 0x72, + 0xB4, 0xE6, 0x84, 0x72, 0xB7, 0x34, 0x71, 0x99, 0xB9, 0xAD, 0x89, 0x2F, 0x54, 0xAF, 0x72, 0xEA, + 0xE8, 0x45, 0xFE, 0x44, 0x83, 0xC8, 0xCF, 0x18, 0x99, 0x28, 0xD4, 0xAB, 0xC3, 0xF7, 0x63, 0x97, + 0x40, 0x2D, 0x4C, 0xAD, 0x6D, 0x37, 0x0A, 0x36, 0xEB, 0x6E, 0xF6, 0x4B, 0x9B, 0x2C, 0x23, 0x4E, + 0x2A, 0x68, 0x79, 0xD0, 0x9C, 0x72, 0xBB, 0x25, 0xA2, 0x40, 0x0A, 0x9D, 0xD0, 0x6A, 0xF6, 0x28, + 0x84, 0xDA, 0xDD, 0x96, 0xB5, 0xB6, 0x10, 0xC9, 0x22, 0xE8, 0xB4, 0x8F, 0x15, 0x6A, 0x0D, 0xA9, + 0xAD, 0x13, 0x86, 0x78, 0x3F, 0xAC, 0x6C, 0x36, 0x68, 0x77, 0xE1, 0xD0, 0xEE, 0x85, 0xA4, 0xDD, + 0x39, 0xD1, 0xEE, 0x04, 0xB4, 0x1B, 0x03, 0xAA, 0xD7, 0xD7, 0x05, 0x70, 0x6E, 0x82, 0xE0, 0xBD, + 0x2C, 0x1A, 0xD4, 0x68, 0x82, 0x1D, 0x9B, 0xD4, 0xFF, 0xB2, 0x91, 0x3E, 0x05, 0x46, 0xB6, 0x92, + 0xF4, 0x2E, 0x52, 0x39, 0x9E, 0xA2, 0x4D, 0xC6, 0xA7, 0xE1, 0xC4, 0x96, 0x6B, 0x13, 0xBC, 0x72, + 0x33, 0xE1, 0x0F, 0x27, 0x21, 0x9A, 0x6C, 0xA3, 0x80, 0x1B, 0xF4, 0x3E, 0x0F, 0x11, 0xEE, 0xD5, + 0xE5, 0x30, 0xA4, 0x50, 0x3F, 0x8E, 0x05, 0x13, 0x70, 0xC6, 0xEC, 0x6C, 0x69, 0x65, 0x73, 0xE7, + 0x3A, 0xDB, 0xA6, 0x7D, 0x24, 0x8C, 0x0B, 0x56, 0x91, 0x5F, 0xB7, 0xE8, 0x80, 0xD5, 0xD0, 0x87, + 0x7F, 0x0F, 0x7F, 0x18, 0x99, 0x98, 0x26, 0xA4, 0x0F, 0xC8, 0xF7, 0x43, 0x92, 0xC6, 0x0F, 0x83, + 0xEF, 0xA3, 0x6F, 0x23, 0x7E, 0x22, 0xED, 0x1F, 0x1B, 0x67, 0xDA, 0x4B, 0xC6, 0x56, 0xF3, 0x6D, + 0xF8, 0x7D, 0x77, 0x35, 0xDF, 0x0F, 0x7E, 0x88, 0xFE, 0x1E, 0xF1, 0x13, 0x29, 0xCB, 0xA8, 0x46, + 0x19, 0xFC, 0x34, 0xE6, 0x18, 0xAF, 0x58, 0x5D, 0xD8, 0xF9, 0xB1, 0xED, 0x87, 0x51, 0x59, 0x78, + 0xF3, 0x86, 0x22, 0x82, 0x10, 0x05, 0xE2, 0xB4, 0xDF, 0x32, 0x1A, 0x90, 0x84, 0x97, 0xB1, 0xE9, + 0x21, 0x0B, 0xF1, 0x47, 0x98, 0x18, 0xB0, 0x52, 0xC7, 0xF9, 0x36, 0x82, 0x07, 0x7A, 0x8C, 0x6C, + 0xBC, 0xE9, 0x49, 0x9A, 0xCF, 0x9D, 0xEF, 0x41, 0x2A, 0xED, 0x27, 0x7D, 0x9E, 0x29, 0x64, 0x69, + 0x55, 0x21, 0x63, 0x7C, 0xD9, 0x80, 0xC2, 0x41, 0x26, 0x88, 0xD6, 0xEC, 0xDE, 0x02, 0xF7, 0x22, + 0xD7, 0xA1, 0x03, 0x19, 0x24, 0xD4, 0x89, 0xDA, 0xC9, 0xF6, 0xCF, 0xA0, 0x4F, 0x45, 0x7C, 0x85, + 0x0B, 0x99, 0x63, 0x49, 0xCB, 0x7A, 0x05, 0xFA, 0x45, 0x5B, 0x32, 0xCD, 0xC2, 0x4E, 0xD5, 0xB4, + 0xFE, 0x7A, 0xEF, 0xE2, 0x8F, 0xBD, 0x16, 0xEC, 0x65, 0x68, 0x6F, 0xE5, 0x7A, 0x7D, 0x89, 0x47, + 0x9E, 0xCC, 0xD3, 0x53, 0x72, 0xC9, 0xA9, 0x38, 0x36, 0xD7, 0xAB, 0x22, 0xAD, 0x2B, 0x66, 0xDB, + 0xEA, 0xAE, 0xE8, 0x5C, 0xC3, 0x86, 0x3C, 0xB4, 0x42, 0x74, 0x96, 0x1B, 0x4C, 0x42, 0x22, 0xD8, + 0x66, 0x2C, 0x82, 0xC7, 0x44, 0xF2, 0xB6, 0x07, 0x65, 0x9C, 0x7E, 0x46, 0x50, 0x33, 0xA0, 0x64, + 0x03, 0x68, 0xC7, 0xC0, 0x65, 0xC4, 0x1B, 0xF2, 0x32, 0xBE, 0xD5, 0xFD, 0x7D, 0xB4, 0x19, 0xBA, + 0xAD, 0x27, 0x6C, 0xF4, 0x62, 0x9B, 0xCA, 0x1D, 0xB7, 0xA5, 0x37, 0xAC, 0xF5, 0x6C, 0x14, 0xEE, + 0x0C, 0x9B, 0x5D, 0x1D, 0x79, 0xE8, 0x57, 0x07, 0xB0, 0x59, 0x57, 0xD2, 0xB6, 0x76, 0xB9, 0x95, + 0x60, 0x43, 0x23, 0xEE, 0x3E, 0x32, 0xC5, 0xDC, 0x1D, 0x1F, 0x27, 0x21, 0x50, 0x6C, 0x27, 0x62, + 0xF4, 0xFB, 0x9A, 0x48, 0x09, 0x53, 0x85, 0x30, 0x22, 0xE5, 0x17, 0x29, 0x8B, 0x95, 0x9D, 0x60, + 0x82, 0xDC, 0x17, 0x3A, 0x30, 0x52, 0x49, 0x81, 0x74, 0x87, 0xFE, 0xA8, 0xD7, 0xD7, 0xDF, 0xE2, + 0xA5, 0xF4, 0x93, 0x92, 0x83, 0x4A, 0xAA, 0x3C, 0x70, 0x1F, 0x23, 0xCC, 0xB1, 0xD2, 0x1F, 0xAC, + 0xCF, 0x48, 0x7D, 0xB7, 0xCE, 0x26, 0x2D, 0x6F, 0x78, 0xDA, 0x17, 0x24, 0xAA, 0x4F, 0x71, 0x06, + 0x95, 0x17, 0x66, 0xF7, 0x91, 0xA4, 0x85, 0x8F, 0xF4, 0xC2, 0x28, 0x0E, 0xC7, 0x31, 0x6A, 0x1A, + 0x7C, 0xFF, 0x6C, 0xB9, 0x88, 0xC7, 0x70, 0xF3, 0x25, 0x73, 0xF8, 0xCD, 0x47, 0x43, 0xDA, 0x83, + 0x8D, 0x1D, 0xD2, 0x9F, 0x35, 0x22, 0x06, 0xD2, 0xC9, 0xDA, 0x1B, 0x85, 0x5B, 0x0A, 0x36, 0x84, + 0xFE, 0x4D, 0x55, 0xFC, 0xD3, 0x10, 0x2B, 0x33, 0x99, 0x1B, 0x2B, 0xAD, 0x4D, 0xA7, 0xE0, 0xE9, + 0x2C, 0xF5, 0x74, 0x0A, 0x4C, 0xA7, 0xD8, 0xDF, 0xB7, 0x24, 0x95, 0xEC, 0xB4, 0xB4, 0xDB, 0x82, + 0x86, 0x5E, 0xE8, 0xBA, 0x73, 0xD7, 0xF7, 0x4B, 0x64, 0xBA, 0xD6, 0x23, 0x19, 0xE1, 0xB3, 0x1B, + 0xF3, 0x76, 0x62, 0x55, 0xB1, 0x31, 0xE7, 0x97, 0x26, 0xFE, 0xBB, 0x03, 0x92, 0x72, 0x4F, 0x5F, + 0x0A, 0x04, 0x91, 0xDF, 0xE4, 0x64, 0xD7, 0xE5, 0x5B, 0x99, 0x66, 0xE3, 0x90, 0x5E, 0xF2, 0xBB, + 0xAF, 0xB6, 0x3F, 0x77, 0x64, 0x5D, 0xAD, 0x8C, 0xE9, 0xF0, 0x04, 0x5A, 0xC7, 0x49, 0x36, 0x39, + 0x55, 0x80, 0x98, 0x9C, 0x12, 0xC6, 0x34, 0xA4, 0x94, 0x76, 0xAF, 0x1A, 0x56, 0x4A, 0x09, 0x4E, + 0xC9, 0x2D, 0xD5, 0xCB, 0x03, 0xFE, 0xAA, 0xBF, 0x6D, 0xF1, 0x57, 0xA9, 0xAE, 0x38, 0x21, 0x25, + 0x8F, 0x10, 0x72, 0x38, 0xEC, 0x48, 0x82, 0x60, 0x4A, 0x52, 0xBC, 0x52, 0x0C, 0x28, 0x1D, 0x5D, + 0xE1, 0x1F, 0xE6, 0x2E, 0x9D, 0x48, 0xBE, 0x5A, 0x3D, 0xB6, 0x05, 0x4D, 0x7E, 0x19, 0x4F, 0xC2, + 0xB4, 0xEF, 0x4E, 0x0A, 0x26, 0xD4, 0xD0, 0x0D, 0x8F, 0x5B, 0x86, 0x13, 0x4B, 0x8D, 0x86, 0xF8, + 0x5C, 0xE8, 0x66, 0x76, 0xD1, 0xA5, 0xC5, 0x08, 0x3B, 0x67, 0x77, 0x03, 0xA9, 0x96, 0x9F, 0x3D, + 0x56, 0x44, 0xDB, 0xC5, 0xA2, 0xCE, 0xC1, 0x58, 0x18, 0x07, 0x32, 0x9E, 0x53, 0x58, 0x9C, 0x14, + 0xD2, 0x43, 0x65, 0x74, 0x6D, 0xAB, 0xE6, 0x34, 0xD0, 0xB5, 0xBE, 0xCF, 0x16, 0xDB, 0x99, 0xE4, + 0xC8, 0xC9, 0xDE, 0x44, 0x06, 0x52, 0xE5, 0x26, 0xC1, 0x2E, 0xE5, 0xAA, 0x1E, 0x84, 0xA8, 0xD0, + 0x12, 0xD1, 0xC3, 0x69, 0x6B, 0x22, 0x14, 0x78, 0xD7, 0x28, 0xBA, 0x49, 0x64, 0xE6, 0x89, 0x20, + 0x0A, 0x6E, 0xC6, 0xB0, 0x59, 0xE0, 0x27, 0xB3, 0x52, 0xCD, 0x08, 0xC9, 0x3D, 0x43, 0x62, 0x65, + 0x4A, 0xBB, 0x62, 0x93, 0x07, 0xCC, 0x61, 0x3B, 0xC4, 0x6A, 0xC5, 0x81, 0xA9, 0x94, 0xE4, 0x36, + 0x14, 0x32, 0x64, 0xAC, 0x10, 0x64, 0x69, 0xE1, 0x93, 0x14, 0x7D, 0x2B, 0x59, 0x2C, 0xE8, 0xD7, + 0xBA, 0xDB, 0x21, 0x8F, 0x9A, 0xEA, 0x1E, 0xB0, 0x2E, 0x5A, 0xCC, 0x69, 0xE5, 0x2C, 0x69, 0xE3, + 0x2C, 0x44, 0x9D, 0xF8, 0xA8, 0x63, 0x6D, 0x9A, 0xA9, 0x6C, 0x7F, 0xAA, 0xC1, 0x62, 0x5B, 0xDF, + 0x26, 0xD4, 0x64, 0x97, 0x7D, 0x02, 0x27, 0x08, 0x80, 0xAD, 0xE2, 0x3B, 0xCB, 0xDC, 0xB2, 0x5F, + 0x94, 0xAF, 0x36, 0x8B, 0xE4, 0x78, 0x35, 0x9B, 0x49, 0xF5, 0xBE, 0xBA, 0x7D, 0x9F, 0xCC, 0xF1, + 0xF5, 0x93, 0x0B, 0xF6, 0x4B, 0xB3, 0xBF, 0x60, 0x73, 0x34, 0xE0, 0xF2, 0x91, 0x00, 0x45, 0xC3, + 0x8F, 0x04, 0xA9, 0xF0, 0x24, 0x16, 0x6D, 0x86, 0xAB, 0xE7, 0x6D, 0x18, 0x19, 0x89, 0x3F, 0x5B, + 0x31, 0x33, 0xAB, 0xA3, 0xE6, 0xBA, 0xDE, 0x02, 0xA0, 0xB6, 0xB1, 0xCD, 0x5A, 0x32, 0xB6, 0x6D, + 0xEC, 0x7B, 0xF3, 0x8F, 0xDE, 0x9A, 0x50, 0x72, 0x2B, 0xE5, 0xDA, 0xFD, 0x54, 0xD2, 0xB5, 0x6D, + 0x6D, 0x51, 0x6B, 0x8F, 0x5C, 0x5B, 0x6A, 0xD9, 0x3F, 0x66, 0x0D, 0xC8, 0x21, 0x56, 0xF5, 0x21, + 0x6E, 0x0C, 0x27, 0x42, 0x46, 0x1E, 0x7A, 0xD8, 0x50, 0x6E, 0x1F, 0x37, 0xB7, 0x12, 0x0B, 0x1E, + 0x89, 0x32, 0x16, 0x11, 0x8C, 0x75, 0x7B, 0xE3, 0x7A, 0x92, 0x1E, 0xE7, 0xA3, 0xA0, 0x6E, 0x47, + 0x14, 0x2C, 0xE9, 0x2D, 0x37, 0x97, 0xC6, 0x68, 0x48, 0x98, 0xE0, 0x5D, 0xD8, 0x74, 0x4C, 0xCC, + 0x96, 0x09, 0x22, 0xE2, 0xDB, 0x1E, 0x95, 0x5D, 0x90, 0xE2, 0x81, 0x05, 0x49, 0x9B, 0x74, 0x36, + 0xAE, 0xC7, 0xB0, 0x73, 0x4A, 0x18, 0xD3, 0x37, 0x2E, 0xBC, 0x4D, 0x4B, 0xE0, 0xD1, 0xF0, 0xFD, + 0x94, 0x25, 0xB9, 0x65, 0x5D, 0x71, 0x14, 0xCA, 0xC3, 0x0B, 0x6B, 0xDD, 0xEC, 0x2A, 0xE9, 0x59, + 0xDD, 0x42, 0x6D, 0xD5, 0xE9, 0x66, 0xF7, 0xA5, 0x9B, 0xFD, 0x6F, 0x21, 0xFE, 0x54, 0x6B, 0x1D, + 0xAA, 0x68, 0xAD, 0x6C, 0x2A, 0x01, 0x96, 0x37, 0x17, 0x83, 0x20, 0x1F, 0x11, 0x6E, 0xAE, 0x6B, + 0x6B, 0xC0, 0x4A, 0x3A, 0x0F, 0x2F, 0xCF, 0xBB, 0x74, 0x52, 0x4D, 0x21, 0xBC, 0xA8, 0x07, 0x04, + 0xA8, 0x09, 0x0E, 0xF3, 0x5A, 0xBB, 0xB2, 0xDC, 0x92, 0xB4, 0x75, 0xC5, 0xD3, 0xC5, 0xBA, 0xC6, + 0xEA, 0xFB, 0x02, 0xFB, 0x2B, 0x84, 0x0D, 0xA1, 0xF8, 0xE7, 0x63, 0x64, 0x67, 0x25, 0x47, 0x42, + 0x73, 0x16, 0x24, 0x2B, 0x6B, 0x2F, 0x83, 0x12, 0x5F, 0xB0, 0xF3, 0xAB, 0xE6, 0xBD, 0x73, 0xD5, + 0xD5, 0xA0, 0xFF, 0x80, 0xF0, 0x28, 0xC2, 0x61, 0xA5, 0x22, 0x10, 0x37, 0x66, 0xB2, 0x7B, 0x4F, + 0x2A, 0x52, 0x7A, 0xD5, 0x1A, 0x6B, 0x2A, 0xB7, 0xDB, 0xC8, 0x59, 0xBF, 0xCE, 0xD9, 0xD3, 0xC1, + 0x03, 0xFC, 0x2C, 0x6A, 0xA0, 0xDE, 0x27, 0x31, 0x3B, 0x04, 0x99, 0x1A, 0x79, 0xB6, 0x25, 0x39, + 0xB8, 0x47, 0x6B, 0x35, 0x85, 0x1D, 0x23, 0x46, 0x49, 0xCC, 0xDA, 0xA8, 0x93, 0xB3, 0xB6, 0xDE, + 0x65, 0x7F, 0x21, 0xB3, 0xCA, 0x72, 0xCD, 0xA6, 0x3E, 0x3E, 0xB2, 0xC1, 0xC8, 0xDA, 0x5D, 0xC7, + 0x20, 0x1D, 0x12, 0x68, 0x56, 0x44, 0xC4, 0x0F, 0xD5, 0xBE, 0x67, 0x22, 0x2A, 0xBA, 0x0C, 0xEB, + 0xEE, 0x72, 0x83, 0x34, 0xD9, 0x53, 0xE4, 0x46, 0x27, 0x08, 0x80, 0xED, 0x9A, 0x9A, 0x76, 0x18, + 0xF1, 0x78, 0x3B, 0x8C, 0x50, 0x76, 0x18, 0x41, 0x76, 0x98, 0x5F, 0x6B, 0x59, 0x71, 0xE5, 0xB3, + 0xB6, 0x69, 0x65, 0xB3, 0xE9, 0xC0, 0x94, 0x72, 0xB4, 0xFA, 0xAD, 0x4A, 0x5F, 0xE8, 0xBB, 0xC1, + 0xF7, 0xBE, 0x77, 0xC2, 0x87, 0x86, 0x35, 0x3C, 0xA0, 0xAB, 0x55, 0x7B, 0xF3, 0xB7, 0xCA, 0x69, + 0xD3, 0xEB, 0x3E, 0xD3, 0x8E, 0x28, 0x7E, 0xBB, 0x3E, 0x37, 0x1A, 0x65, 0x68, 0xA7, 0xBC, 0x7C, + 0xD2, 0x4E, 0xF6, 0xB2, 0x7B, 0x15, 0xB6, 0xB1, 0x8C, 0xA3, 0x89, 0xFB, 0x38, 0x54, 0x46, 0xED, + 0x24, 0x11, 0x56, 0x29, 0x69, 0x4D, 0x6F, 0xDA, 0xB3, 0xFA, 0x4A, 0x43, 0x5D, 0x88, 0xF5, 0xB7, + 0xD0, 0x54, 0x80, 0xAA, 0x5C, 0x3A, 0x44, 0xE7, 0xDA, 0xE9, 0x90, 0x79, 0x48, 0xF2, 0x71, 0x1A, + 0x6E, 0x10, 0xA6, 0x33, 0x5A, 0x32, 0xBC, 0x9F, 0x40, 0x54, 0xF2, 0xA4, 0xAB, 0xB4, 0xA2, 0x92, + 0x73, 0x88, 0xD1, 0x78, 0xB8, 0xCC, 0x11, 0x2A, 0x0E, 0x17, 0x49, 0x59, 0x99, 0xA8, 0xD6, 0xBC, + 0xD2, 0x91, 0xAE, 0x45, 0x15, 0x0F, 0xD3, 0x0A, 0xF9, 0xF0, 0x19, 0xC9, 0x38, 0x87, 0x0B, 0x16, + 0x75, 0x5A, 0xD5, 0x93, 0x05, 0xEA, 0xF6, 0x3B, 0xF7, 0x6A, 0x57, 0xFA, 0x64, 0xC0, 0xA1, 0xD8, + 0xF7, 0x94, 0x9B, 0x23, 0xC4, 0x23, 0xFB, 0x36, 0x98, 0x1C, 0x0D, 0x47, 0xA0, 0xDC, 0xA8, 0x79, + 0xA4, 0x73, 0x16, 0xD8, 0x93, 0xED, 0x25, 0x38, 0x61, 0x12, 0xDF, 0x1F, 0x5D, 0x79, 0xB8, 0xAD, + 0xFA, 0x31, 0xAA, 0x57, 0xA7, 0x53, 0xBC, 0x12, 0xF2, 0x6C, 0x0A, 0xF9, 0x93, 0x5C, 0x01, 0x32, + 0x32, 0x49, 0x7D, 0x7F, 0x9E, 0xF0, 0x29, 0x17, 0x37, 0xF8, 0xD1, 0xDF, 0x29, 0xC9, 0x7C, 0x7F, + 0x5B, 0xC0, 0xD9, 0x82, 0xB7, 0x3B, 0xFA, 0xD5, 0x39, 0x64, 0xE2, 0xC8, 0x9E, 0x50, 0x31, 0xAD, + 0xDC, 0xC5, 0x34, 0xF0, 0xE5, 0x81, 0x5A, 0x12, 0x6B, 0x3D, 0xAF, 0x11, 0x3D, 0x1E, 0xD5, 0xCF, + 0x6E, 0x98, 0x54, 0x32, 0x12, 0x40, 0xE1, 0x8C, 0xD9, 0x1B, 0x2B, 0x4D, 0x65, 0x6C, 0xC4, 0x1D, + 0x4A, 0x8A, 0xB4, 0xF3, 0x96, 0x93, 0x46, 0x32, 0xF2, 0xC5, 0x6E, 0x6E, 0x08, 0xE4, 0x19, 0x4C, + 0x48, 0x83, 0xE0, 0x2B, 0x8A, 0x27, 0x6A, 0x41, 0x3A, 0x27, 0xD5, 0x09, 0x2D, 0x3F, 0xBE, 0x49, + 0xC5, 0xDD, 0x6A, 0x25, 0xAB, 0x36, 0x27, 0x56, 0xD9, 0xBE, 0x2C, 0xE4, 0x38, 0xDA, 0x27, 0x59, + 0xC9, 0x5E, 0xF6, 0x8C, 0x63, 0xB3, 0xF6, 0x4D, 0xA5, 0xDA, 0x6A, 0xAE, 0x1E, 0xA8, 0x86, 0x36, + 0xC3, 0x76, 0x57, 0x45, 0x5F, 0x5A, 0xD5, 0xCD, 0x9D, 0xEA, 0x9A, 0xA7, 0x74, 0xD6, 0x4E, 0xD9, + 0xA2, 0xB5, 0xD4, 0xD1, 0x9C, 0xF9, 0xDE, 0x6C, 0xCD, 0x7E, 0x08, 0xD6, 0x04, 0xC2, 0x1B, 0x48, + 0x1E, 0x44, 0x42, 0x23, 0x8F, 0x0F, 0x0B, 0xE0, 0x83, 0x91, 0x4A, 0xE0, 0x57, 0xC6, 0x67, 0x90, + 0xB1, 0x11, 0x70, 0x8A, 0x29, 0x8C, 0x34, 0x36, 0x5C, 0x65, 0xCE, 0x81, 0x05, 0x76, 0x16, 0xF8, + 0x80, 0x93, 0xDA, 0xAE, 0x6C, 0xD1, 0x53, 0x92, 0x87, 0x43, 0x49, 0xFC, 0x66, 0x51, 0xB2, 0x5D, + 0x96, 0xDC, 0xE4, 0xB0, 0x92, 0x47, 0x91, 0x64, 0x48, 0x49, 0x14, 0x75, 0xB2, 0xA9, 0x65, 0x6C, + 0xAA, 0xA3, 0xB4, 0x3E, 0xA0, 0x5D, 0x82, 0xD4, 0x4D, 0xE5, 0x8F, 0xAF, 0xDA, 0x05, 0x8F, 0x4D, + 0xAF, 0x71, 0x4C, 0x00, 0x57, 0xC9, 0x66, 0x61, 0xB7, 0x43, 0xD9, 0x96, 0x7E, 0xC0, 0xF4, 0x40, + 0xD5, 0xEE, 0x1E, 0x13, 0x01, 0x1B, 0x94, 0x8D, 0x68, 0x00, 0x36, 0x1D, 0x94, 0x75, 0x3E, 0x4D, + 0x72, 0x1F, 0x67, 0xF6, 0xBC, 0x48, 0x10, 0xB9, 0xA1, 0xFF, 0x71, 0x0C, 0xE5, 0xD5, 0x15, 0x1A, + 0xDC, 0x0C, 0x22, 0x48, 0xE4, 0xCB, 0x79, 0x4E, 0x7A, 0x68, 0xB4, 0x34, 0x9B, 0x0D, 0xB0, 0xDD, + 0x7C, 0x4E, 0x0F, 0xD4, 0xF1, 0x02, 0x99, 0xAE, 0xEF, 0xE5, 0x56, 0x03, 0xEF, 0x88, 0xC2, 0xF4, + 0xE7, 0xFC, 0x7B, 0x03, 0x32, 0x9F, 0x66, 0xF4, 0xB8, 0x0E, 0x13, 0x8A, 0xBA, 0x77, 0xCA, 0xE3, + 0x20, 0x19, 0xEC, 0xFC, 0x45, 0xAF, 0xF0, 0x0D, 0x96, 0x4D, 0x01, 0x7A, 0x39, 0x66, 0xDE, 0xAD, + 0x97, 0x43, 0x1D, 0x3E, 0x16, 0x94, 0x3D, 0xD9, 0x15, 0x0D, 0x2F, 0xF5, 0x1A, 0x84, 0x7A, 0x52, + 0x88, 0xC6, 0xDA, 0xCC, 0xDC, 0xEC, 0xDE, 0x5E, 0xBD, 0xB2, 0xE6, 0x77, 0x5D, 0x97, 0x7C, 0x0B, + 0x42, 0x87, 0xB7, 0x7E, 0x32, 0x76, 0x64, 0xF5, 0x59, 0x0F, 0xCB, 0x4E, 0xDC, 0xC8, 0x6B, 0x47, + 0xD3, 0xE8, 0xAE, 0xD3, 0xBE, 0x62, 0x67, 0xE3, 0x2C, 0x39, 0xB4, 0x1C, 0x54, 0x8B, 0xB0, 0x01, + 0xFC, 0x41, 0xCA, 0x3B, 0x14, 0x52, 0x49, 0x01, 0x85, 0xC5, 0xDE, 0x56, 0xC2, 0xB9, 0x4C, 0xE2, + 0x5C, 0xD6, 0xC6, 0xB9, 0x4C, 0x8D, 0x3B, 0xAF, 0xE3, 0xDC, 0xA6, 0xD1, 0x65, 0x6D, 0x0C, 0x33, + 0x38, 0x44, 0x73, 0xF8, 0xF3, 0x02, 0x1E, 0xBA, 0x32, 0xC2, 0x71, 0x51, 0xF2, 0xC0, 0x32, 0xB5, + 0x2C, 0x47, 0x6B, 0x4B, 0x7F, 0xAF, 0x6B, 0x04, 0x47, 0x9F, 0x38, 0x76, 0xC0, 0x27, 0x8E, 0x59, + 0x3A, 0xF2, 0xAE, 0x52, 0xC0, 0x7C, 0xCC, 0x36, 0x1A, 0xD6, 0x02, 0x80, 0x8D, 0xFC, 0x1B, 0x4A, + 0x5D, 0x00, 0xAF, 0xF2, 0x21, 0x34, 0x9B, 0x6C, 0xC2, 0x42, 0x7D, 0xE0, 0xDF, 0x50, 0xF2, 0x53, + 0xBC, 0xCA, 0x07, 0x77, 0xAB, 0x4D, 0xF8, 0x51, 0x3F, 0xDC, 0xCB, 0xA2, 0xCE, 0xDE, 0xB6, 0x4B, + 0xA7, 0x67, 0xDC, 0x4B, 0x2C, 0x0D, 0xBD, 0xB7, 0xE6, 0x2D, 0x77, 0x20, 0x8B, 0xEB, 0xA7, 0x01, + 0x6A, 0x6C, 0x36, 0xD1, 0xC1, 0xAA, 0xA7, 0x07, 0x69, 0xF0, 0x45, 0x7C, 0xCC, 0xA7, 0x9E, 0xAA, + 0x94, 0x20, 0x34, 0x79, 0x74, 0xF7, 0x0F, 0x32, 0x95, 0x29, 0x8B, 0x75, 0x12, 0xA6, 0xF5, 0xA3, + 0xDD, 0xCE, 0xC3, 0xBD, 0xAC, 0x6D, 0xF7, 0x91, 0xE0, 0x48, 0x35, 0x20, 0x32, 0xA7, 0xF3, 0xB7, + 0xBC, 0x83, 0xDF, 0xD0, 0x30, 0xF7, 0xF0, 0xC4, 0xBD, 0xBD, 0xDA, 0xAB, 0x3C, 0x14, 0xD0, 0x2A, + 0x11, 0xF6, 0x5C, 0xDB, 0xDD, 0x23, 0xFA, 0x02, 0xC9, 0x7A, 0x4E, 0x27, 0xF0, 0x18, 0x8C, 0xEF, + 0x4F, 0x72, 0xE6, 0x8C, 0x14, 0x61, 0x0A, 0xBF, 0x30, 0x48, 0x33, 0x57, 0x93, 0x39, 0xC5, 0xEA, + 0xE7, 0xFB, 0x62, 0x0F, 0x48, 0x6F, 0x9A, 0x97, 0x95, 0x3A, 0xD8, 0x16, 0xD2, 0x8F, 0xCA, 0x78, + 0x6C, 0xFB, 0x7B, 0x6F, 0x91, 0x45, 0x71, 0xDF, 0xAE, 0xD3, 0xF0, 0x6C, 0xFE, 0x3B, 0x27, 0xFF, + 0xD0, 0xAB, 0x88, 0x17, 0x50, 0x60, 0xD0, 0x84, 0xFE, 0x4C, 0xBD, 0x91, 0x09, 0x78, 0x63, 0x21, + 0x20, 0xF8, 0x32, 0x3E, 0xB2, 0x45, 0xCF, 0x9C, 0xA2, 0xBE, 0xCF, 0xDC, 0x75, 0xD0, 0xE0, 0xD2, + 0x91, 0xDD, 0xC7, 0x12, 0x68, 0xCE, 0x6C, 0x53, 0xCC, 0x53, 0x8B, 0x29, 0x9E, 0xB8, 0x4C, 0x71, + 0x5A, 0xDD, 0xF0, 0x56, 0xC6, 0xA9, 0x6C, 0x21, 0xD2, 0x4B, 0x11, 0xC3, 0x99, 0x31, 0xD7, 0xAB, + 0x81, 0xC8, 0x9F, 0xCB, 0x6C, 0x0C, 0x29, 0x96, 0x46, 0xD0, 0x32, 0x77, 0xD5, 0x56, 0x7F, 0x61, + 0xAB, 0x97, 0xFC, 0x9C, 0x8F, 0xE9, 0x4C, 0x3F, 0x32, 0xDF, 0x8D, 0xEF, 0x19, 0x6A, 0x9A, 0xE6, + 0x0E, 0x24, 0xA2, 0x68, 0xE9, 0xB4, 0x21, 0xDF, 0x9C, 0x73, 0x4D, 0x64, 0xAF, 0x91, 0xF8, 0x3D, + 0x51, 0xF8, 0x4D, 0x2D, 0xB0, 0x35, 0xE6, 0x0E, 0x53, 0x4E, 0xB5, 0x4B, 0x2D, 0x85, 0x86, 0x71, + 0x8F, 0x14, 0x5B, 0x7D, 0x3F, 0xE0, 0xCC, 0xF6, 0xB0, 0x41, 0x94, 0xF1, 0xED, 0x90, 0xE9, 0x0B, + 0xA4, 0x2C, 0xDA, 0xBF, 0xC6, 0xEF, 0x0F, 0xD4, 0x46, 0x87, 0xF3, 0x42, 0xFB, 0x6A, 0x33, 0xA5, + 0x83, 0x63, 0x0C, 0xCC, 0xEE, 0x20, 0x5A, 0x94, 0x30, 0x1D, 0x5D, 0x63, 0x38, 0x75, 0xE1, 0xCD, + 0xCC, 0x36, 0x80, 0x01, 0xF6, 0x92, 0x7F, 0x94, 0x87, 0xA9, 0x6C, 0x2A, 0x6C, 0xB3, 0x5F, 0xC0, + 0x73, 0x89, 0x04, 0xB2, 0x2F, 0xD5, 0xC1, 0x19, 0x37, 0xC1, 0x59, 0x3B, 0x1C, 0x45, 0xC3, 0x2D, + 0x8D, 0x4F, 0x18, 0x5E, 0x57, 0x72, 0x78, 0x43, 0x39, 0xFE, 0x90, 0x87, 0xED, 0xE2, 0x20, 0x2F, + 0x9F, 0x2F, 0x8E, 0xFA, 0x6A, 0x09, 0xC9, 0xC1, 0xCB, 0x48, 0x00, 0x6C, 0xB2, 0xF0, 0xF8, 0xD0, + 0x6C, 0x3E, 0xD7, 0x10, 0x15, 0xDF, 0xAC, 0x56, 0x4E, 0x2A, 0x58, 0x43, 0x39, 0x17, 0x1C, 0x8B, + 0x81, 0xF4, 0x39, 0xD6, 0x67, 0x45, 0x7D, 0xCB, 0xF4, 0x6A, 0x5D, 0xAD, 0x78, 0x90, 0x43, 0xCF, + 0x14, 0x47, 0xF3, 0x4E, 0x21, 0xA7, 0x17, 0x19, 0x28, 0xCF, 0x6C, 0x76, 0xCA, 0x47, 0xBC, 0x61, + 0x6E, 0x48, 0x4D, 0x96, 0x6E, 0x16, 0x59, 0xA1, 0xFD, 0xC8, 0xDF, 0x32, 0xE5, 0x83, 0x41, 0x2E, + 0xDD, 0x1B, 0x9D, 0x14, 0xE8, 0x19, 0xE7, 0x43, 0xAF, 0x2D, 0x0E, 0xD7, 0x4E, 0x41, 0xE3, 0x5D, + 0xF6, 0xC2, 0x22, 0xDC, 0xA9, 0xBB, 0x5E, 0xA5, 0xAA, 0xA3, 0xF5, 0x1C, 0xDB, 0xC5, 0x8A, 0x16, + 0xEA, 0xC0, 0xFB, 0x08, 0x3B, 0xCF, 0xBD, 0xC7, 0xC2, 0xDF, 0x87, 0x2A, 0xD6, 0xC7, 0x28, 0x86, + 0xCF, 0xF4, 0x73, 0x9A, 0x85, 0x1F, 0xD5, 0x73, 0x41, 0xCC, 0xC2, 0xB2, 0x9C, 0x27, 0x55, 0xE3, + 0x54, 0xAE, 0x0F, 0x94, 0xF0, 0x4C, 0x86, 0xA4, 0x3A, 0x0B, 0xE9, 0xB5, 0xEE, 0x4D, 0x5B, 0xF6, + 0x00, 0xBF, 0xE6, 0x6D, 0x95, 0x47, 0x8A, 0x89, 0x1C, 0x69, 0xF6, 0x71, 0x24, 0xF9, 0x06, 0xF6, + 0x42, 0xBA, 0x4B, 0xF2, 0x69, 0x55, 0x3F, 0x07, 0xAF, 0xCD, 0xB1, 0xDD, 0xEE, 0x08, 0x3A, 0x13, + 0x15, 0xF9, 0x25, 0xC3, 0xE6, 0x11, 0xBE, 0x30, 0xE2, 0x2D, 0xCB, 0x23, 0x1D, 0x82, 0x2D, 0xEB, + 0x41, 0x9D, 0x72, 0x2D, 0xAA, 0xD6, 0x62, 0x1B, 0xF3, 0x22, 0xAA, 0x87, 0x82, 0xEF, 0xB4, 0xB4, + 0x06, 0xC9, 0x23, 0x6D, 0x48, 0x3F, 0x61, 0x81, 0x24, 0x9C, 0xCD, 0x9F, 0x5E, 0xA5, 0xC0, 0x12, + 0xD2, 0xB4, 0x95, 0x4C, 0x75, 0xCE, 0xC4, 0xA2, 0x44, 0x44, 0xDD, 0x35, 0xB0, 0xB3, 0x47, 0x1B, + 0x6F, 0xB9, 0x1C, 0xC5, 0xD2, 0x61, 0xF2, 0x12, 0x8A, 0x9C, 0x1B, 0xE6, 0xA4, 0x75, 0xDA, 0xF9, + 0x4A, 0x78, 0xBE, 0xB4, 0x4D, 0x2E, 0x52, 0x46, 0x3A, 0x42, 0x6C, 0xF2, 0xE5, 0xCA, 0x3E, 0x4E, + 0xE3, 0x4E, 0xD9, 0xE5, 0x75, 0xA5, 0xC3, 0x60, 0x8C, 0xCA, 0x40, 0xC7, 0xFD, 0xEB, 0xBC, 0xB5, + 0xCD, 0x39, 0x7E, 0x6B, 0x86, 0x08, 0xCC, 0x63, 0x74, 0x76, 0xA9, 0xBB, 0x49, 0xFC, 0xDC, 0x3C, + 0x43, 0x46, 0x89, 0x98, 0xE2, 0xF2, 0xE4, 0x14, 0x74, 0x94, 0xA8, 0x4F, 0x51, 0x6B, 0x13, 0x12, + 0x2B, 0x48, 0xE8, 0x9E, 0xC4, 0xE0, 0xED, 0x39, 0x1D, 0xE7, 0xE6, 0x31, 0x79, 0x9A, 0x0D, 0xD2, + 0x2A, 0xC2, 0xD8, 0xAE, 0xCC, 0x3B, 0x2A, 0x2C, 0xE9, 0x70, 0xDF, 0x54, 0x36, 0xD4, 0x33, 0x42, + 0xEB, 0x70, 0x3C, 0xDA, 0x6F, 0x27, 0xCE, 0x46, 0x07, 0x05, 0xFD, 0x51, 0x5F, 0x14, 0x48, 0x91, + 0x17, 0xC7, 0xF7, 0xCA, 0x0F, 0xED, 0x22, 0x74, 0xAA, 0xEF, 0x39, 0x77, 0xE3, 0x5D, 0x7C, 0x3D, + 0xD0, 0x0D, 0x5F, 0xCB, 0xBD, 0x11, 0x4A, 0xA0, 0xC0, 0x49, 0x3A, 0x91, 0x4A, 0x91, 0x22, 0x07, + 0x12, 0x08, 0x23, 0xE3, 0xF9, 0xE1, 0x1F, 0x0E, 0x6E, 0x0E, 0xFF, 0x10, 0xDE, 0xE2, 0x58, 0xA8, + 0xC5, 0x08, 0xA7, 0x9C, 0xBE, 0x3B, 0x98, 0xA0, 0xB9, 0x83, 0xE9, 0xF0, 0x6A, 0x84, 0xA3, 0x45, + 0xDF, 0xE1, 0x33, 0xBD, 0x1F, 0xFE, 0x61, 0xFF, 0x12, 0x47, 0x8A, 0x62, 0x59, 0xDC, 0x86, 0x77, + 0x21, 0x5C, 0xD3, 0x27, 0xF1, 0xAC, 0xDF, 0x98, 0x7F, 0xDA, 0x21, 0xED, 0xFB, 0x7C, 0x3A, 0xCA, + 0xF0, 0x64, 0x14, 0x9F, 0x01, 0xB8, 0x63, 0x34, 0xA7, 0x31, 0x22, 0x3E, 0x3B, 0xB8, 0xA3, 0xB3, + 0x25, 0x1E, 0xA5, 0x39, 0x18, 0xCC, 0xD3, 0x58, 0x07, 0xB4, 0xB4, 0xB6, 0xB2, 0x81, 0x0A, 0x55, + 0x91, 0xD0, 0x39, 0x60, 0x7C, 0x1C, 0x79, 0x51, 0xAA, 0x02, 0x00, 0x20, 0x50, 0xB4, 0x4F, 0xA6, + 0x02, 0x85, 0x83, 0x40, 0xD4, 0x52, 0x08, 0x1A, 0xE7, 0xB3, 0x67, 0x01, 0x1D, 0xA1, 0x03, 0x99, + 0xA7, 0x95, 0x31, 0x24, 0x51, 0xB8, 0xA5, 0x03, 0xC4, 0x59, 0x50, 0x13, 0x62, 0x6B, 0x4B, 0xC0, + 0x1B, 0x99, 0x4F, 0x4F, 0xAF, 0xCE, 0x3E, 0xC2, 0x26, 0x4A, 0x59, 0x94, 0xC9, 0xEA, 0xFC, 0x16, + 0x56, 0x9C, 0x19, 0xD6, 0x29, 0x8E, 0x70, 0x7D, 0x5F, 0xC5, 0x4C, 0x2A, 0xA4, 0x9D, 0x44, 0xD2, + 0x0B, 0xF5, 0xA2, 0x88, 0x86, 0x7A, 0x63, 0xCA, 0xA1, 0x9E, 0x1D, 0x01, 0xF9, 0x79, 0x55, 0x5B, + 0xC8, 0xA6, 0xC7, 0xE6, 0x91, 0x84, 0x5F, 0x5A, 0xCF, 0x16, 0xB5, 0xC3, 0xDC, 0x88, 0x9C, 0x25, + 0x05, 0x58, 0x18, 0xFE, 0x14, 0x26, 0x24, 0x3D, 0xCF, 0x17, 0x27, 0xE3, 0x31, 0x86, 0x2A, 0xCF, + 0x1A, 0xC6, 0xCA, 0x25, 0x73, 0x6E, 0x32, 0x27, 0x4E, 0x40, 0xA7, 0x98, 0x4B, 0x72, 0xA9, 0x57, + 0xFA, 0x34, 0x66, 0x05, 0x71, 0x3C, 0xE8, 0x9A, 0xD1, 0x8F, 0xDC, 0xA3, 0x7B, 0x74, 0x46, 0x4B, + 0x3F, 0xE2, 0x16, 0xDE, 0xC8, 0x97, 0xE0, 0x8E, 0xB3, 0x57, 0x54, 0xFB, 0x6A, 0x75, 0xAC, 0xA5, + 0xF2, 0x8F, 0x11, 0xB6, 0xBA, 0xE2, 0x9F, 0xF8, 0x3C, 0x0B, 0x0E, 0x33, 0x3A, 0xFE, 0x29, 0xBC, + 0x57, 0x49, 0xA9, 0x49, 0xC2, 0xE9, 0x7E, 0x79, 0xE7, 0x15, 0x20, 0xE3, 0xC1, 0x98, 0xBE, 0xE4, + 0x58, 0x95, 0x53, 0x34, 0xBB, 0xB0, 0x08, 0x33, 0xC1, 0x7E, 0xA5, 0x09, 0x16, 0xE5, 0x14, 0xFD, + 0x98, 0xDB, 0xE4, 0x2B, 0x24, 0x5F, 0x61, 0x61, 0xE5, 0x4D, 0x0D, 0xCA, 0x03, 0xDF, 0x0F, 0xAF, + 0x3B, 0xD2, 0xEF, 0x3D, 0x5A, 0x64, 0x79, 0x85, 0x85, 0x93, 0x56, 0x58, 0x33, 0xD6, 0x8A, 0x33, + 0x93, 0x23, 0xBE, 0xA7, 0x95, 0x98, 0x06, 0x58, 0x39, 0x9E, 0xBB, 0xFC, 0xBC, 0xF0, 0x4C, 0x27, + 0xBC, 0x95, 0xA4, 0xED, 0x9E, 0xBA, 0x55, 0x13, 0x66, 0x94, 0xE4, 0x75, 0x1F, 0x9F, 0x51, 0xB2, + 0x11, 0x18, 0x08, 0x99, 0xEF, 0xB4, 0x83, 0xD7, 0xD4, 0xA6, 0x13, 0x64, 0x6D, 0x01, 0xD6, 0xED, + 0x7D, 0x58, 0xD2, 0xD2, 0x20, 0x23, 0xC9, 0x65, 0x8C, 0xF9, 0x9D, 0x1F, 0xC4, 0xF7, 0xC3, 0xBB, + 0xD1, 0x41, 0xA6, 0x95, 0x9A, 0xF9, 0xE7, 0x71, 0xC2, 0x07, 0x0C, 0x07, 0x9C, 0x93, 0xCD, 0x29, + 0xEF, 0x62, 0x10, 0xA7, 0x05, 0xE5, 0x3C, 0x43, 0x4E, 0xA5, 0x0D, 0x2D, 0x74, 0x46, 0xC9, 0x57, + 0x4E, 0x70, 0x6C, 0x70, 0x9D, 0x6E, 0x5A, 0x75, 0xBC, 0x58, 0x53, 0x2C, 0xEF, 0x7B, 0x7B, 0xD8, + 0x68, 0x32, 0x68, 0x91, 0xD8, 0x8B, 0xD0, 0xF7, 0x4F, 0x98, 0x28, 0x5C, 0x8E, 0x88, 0x3E, 0x1D, + 0x81, 0xBE, 0x7B, 0xE1, 0xC9, 0xF0, 0xDD, 0x28, 0xBE, 0x31, 0x6F, 0x56, 0x6A, 0x89, 0xFD, 0xDB, + 0x2E, 0x14, 0x09, 0xBE, 0xF8, 0xC3, 0x40, 0xCA, 0x26, 0xB3, 0x84, 0x7C, 0x07, 0xFB, 0x0B, 0xF2, + 0x5F, 0x86, 0x3B, 0xDE, 0xFE, 0x9C, 0x1E, 0x02, 0x54, 0x63, 0xBE, 0xFE, 0x71, 0x52, 0xFB, 0x2E, + 0x1F, 0x8E, 0x02, 0x34, 0x03, 0x7A, 0xDE, 0xD9, 0x3F, 0x61, 0xFB, 0xC7, 0xD9, 0xB9, 0x53, 0x42, + 0x76, 0x71, 0x61, 0x13, 0x9C, 0x5E, 0xD2, 0x6B, 0x20, 0x01, 0xF4, 0x52, 0x33, 0xDE, 0x86, 0x0A, + 0xDB, 0x66, 0xC0, 0xDA, 0xEA, 0xF8, 0xB0, 0x7D, 0xC9, 0x25, 0x81, 0x58, 0x3A, 0x69, 0x6B, 0x45, + 0x96, 0x16, 0x8D, 0x41, 0xD9, 0x32, 0x62, 0xD0, 0x76, 0x85, 0x16, 0xF6, 0x23, 0x60, 0x97, 0x63, + 0x49, 0xA7, 0xF5, 0x15, 0x3B, 0xB3, 0x19, 0x70, 0x4E, 0x60, 0x82, 0x25, 0xED, 0x30, 0xC0, 0x6B, + 0x2C, 0x40, 0x97, 0x6D, 0x2B, 0x23, 0x4E, 0x9B, 0x22, 0x5A, 0xDA, 0x12, 0x31, 0x1F, 0xB5, 0x5F, + 0x1A, 0xBD, 0x8D, 0xCA, 0xB5, 0x22, 0xCD, 0x62, 0x9B, 0xC8, 0xB0, 0xB7, 0x67, 0x6C, 0x1A, 0xEA, + 0x43, 0xDC, 0x9A, 0xAD, 0x46, 0x86, 0xF0, 0x79, 0x07, 0x53, 0x1F, 0x87, 0x4B, 0x45, 0xD9, 0xA2, + 0xAD, 0xED, 0x85, 0xDB, 0xCC, 0x52, 0x1A, 0x98, 0x08, 0x0E, 0x77, 0x41, 0x17, 0xE1, 0xD0, 0x31, + 0xFC, 0x17, 0x76, 0x0E, 0x47, 0xF2, 0x04, 0x67, 0x18, 0x32, 0xA1, 0x3D, 0x8A, 0xFA, 0xF7, 0x4F, + 0x1D, 0x84, 0x2C, 0xD4, 0x61, 0x13, 0xB3, 0x9D, 0xDE, 0x3D, 0xEE, 0xEC, 0xB5, 0x35, 0x2A, 0x6D, + 0x03, 0x71, 0x3B, 0x53, 0xB8, 0xF4, 0x5C, 0x86, 0x6B, 0x90, 0xC3, 0xAB, 0x4B, 0x4B, 0xEB, 0x90, + 0x72, 0xF1, 0x16, 0xE8, 0x6F, 0x2A, 0xA0, 0x14, 0xEA, 0xE4, 0xCE, 0x40, 0xD4, 0x7D, 0x65, 0x16, + 0x89, 0x70, 0x37, 0xCA, 0x3E, 0xD6, 0xFE, 0x1A, 0x2C, 0x1F, 0x29, 0x36, 0x68, 0xFB, 0x6F, 0x98, + 0xD9, 0x05, 0x44, 0x2E, 0x1F, 0x75, 0xF8, 0x49, 0x98, 0x5B, 0xCC, 0xC7, 0x2E, 0x05, 0xB0, 0x3E, + 0x7C, 0x92, 0x47, 0x5B, 0x87, 0x89, 0xFD, 0x54, 0xAC, 0x56, 0x05, 0x16, 0xC9, 0xA4, 0x43, 0x0E, + 0x20, 0xF1, 0xD5, 0x7A, 0x37, 0x84, 0xAA, 0x97, 0x75, 0x63, 0x57, 0x2C, 0x0B, 0x6B, 0x5F, 0x6C, + 0x69, 0xE7, 0x34, 0xCB, 0x71, 0xB7, 0xC1, 0xAC, 0xE3, 0x90, 0x59, 0x1D, 0xDE, 0x03, 0x5B, 0xA3, + 0xDC, 0xA1, 0x04, 0xF0, 0xB2, 0xDF, 0x07, 0x47, 0x31, 0xCE, 0x3A, 0xB2, 0xCB, 0x01, 0xD5, 0xB2, + 0xD7, 0xCC, 0x93, 0x9B, 0x1B, 0xD7, 0x5E, 0xDD, 0x4F, 0x69, 0x5F, 0x96, 0x78, 0x44, 0x17, 0xD6, + 0x0E, 0x7E, 0x7C, 0x05, 0x7C, 0x90, 0xC2, 0x0C, 0x4B, 0x3B, 0x46, 0xE8, 0xA1, 0x34, 0x23, 0xF4, + 0x48, 0xF5, 0x8E, 0x25, 0x23, 0x99, 0xE2, 0x0A, 0x3D, 0x6F, 0xAB, 0xEE, 0x63, 0x9C, 0xA8, 0x86, + 0x15, 0xD7, 0xB6, 0x92, 0x85, 0x56, 0xA8, 0xE0, 0xF0, 0xDA, 0x7A, 0x21, 0x9D, 0x62, 0x5F, 0x55, + 0x74, 0x3A, 0xA6, 0xA2, 0xE3, 0x3F, 0xA3, 0x4B, 0xE4, 0x73, 0x42, 0x2F, 0x78, 0xAB, 0x0E, 0xFE, + 0x44, 0x1E, 0x27, 0xB8, 0xCD, 0xFE, 0xB2, 0xA1, 0x59, 0xCE, 0xB8, 0x42, 0x99, 0x0D, 0x4D, 0xFD, + 0xAC, 0x9B, 0xB2, 0xC7, 0x50, 0x71, 0x4D, 0xD6, 0x6C, 0x62, 0x8C, 0x93, 0xF6, 0x9C, 0x1E, 0x5A, + 0x5E, 0xC9, 0xB5, 0xF8, 0x5E, 0x2E, 0xE3, 0xD0, 0x1C, 0xF2, 0xA3, 0xD2, 0x7F, 0x90, 0xE9, 0x8E, + 0xB5, 0xEE, 0x3B, 0xDB, 0x3B, 0xB6, 0x88, 0x9E, 0xD1, 0x7B, 0x20, 0x0F, 0x03, 0xE2, 0xF6, 0x7A, + 0xB6, 0x72, 0x5B, 0xEA, 0x6B, 0xA7, 0x27, 0xF7, 0xCA, 0x80, 0x03, 0x92, 0xA8, 0x24, 0xD4, 0x30, + 0x73, 0x5E, 0xBE, 0x0F, 0x4B, 0xE7, 0xED, 0x07, 0xD5, 0xE5, 0x43, 0x92, 0x48, 0x57, 0xB2, 0xEA, + 0x15, 0x7D, 0xC4, 0x75, 0x14, 0x2B, 0x19, 0x51, 0xA2, 0x9C, 0xE4, 0xE9, 0x7E, 0xB9, 0x9F, 0x39, + 0x83, 0x7F, 0xE3, 0x9A, 0x15, 0xFB, 0x66, 0xE1, 0x09, 0x0E, 0x8E, 0x84, 0x97, 0xAF, 0x2F, 0x0D, + 0x1C, 0x26, 0x7F, 0x65, 0x37, 0xB5, 0xD6, 0xAC, 0x23, 0xF8, 0xF9, 0xAD, 0xB6, 0xA2, 0xB9, 0x96, + 0x12, 0xCA, 0xCF, 0x4A, 0xA5, 0x30, 0x06, 0x39, 0x7B, 0x12, 0x52, 0x74, 0x25, 0xBD, 0xF6, 0x5F, + 0x57, 0xF2, 0x3A, 0xA7, 0xCA, 0x3F, 0xE1, 0xFC, 0x6B, 0x1A, 0x7A, 0xC9, 0xFB, 0xBA, 0x94, 0xBF, + 0xCE, 0x4F, 0x9B, 0x7E, 0xB7, 0x60, 0x20, 0x3B, 0x1B, 0xA5, 0x5C, 0x15, 0x36, 0x9C, 0xD1, 0xE4, + 0x65, 0x14, 0xA5, 0x5C, 0x0E, 0x86, 0xB9, 0xA1, 0x0C, 0x39, 0x9D, 0x05, 0xB2, 0x48, 0x66, 0xE4, + 0x93, 0xA3, 0xB3, 0x04, 0x56, 0x2B, 0xEC, 0xA4, 0xFD, 0x9A, 0x72, 0x0E, 0xB2, 0x08, 0x03, 0x8C, + 0x48, 0xD2, 0x16, 0x3A, 0xB7, 0x25, 0x0C, 0xE5, 0x20, 0x89, 0x12, 0x9D, 0xFC, 0x06, 0x3D, 0xA3, + 0x8F, 0x2E, 0xDA, 0x7C, 0xBB, 0xD5, 0xE4, 0x50, 0x85, 0x72, 0x59, 0x91, 0xE0, 0x2D, 0x0F, 0x75, + 0xBA, 0xE7, 0xA5, 0xC5, 0x29, 0xFB, 0x95, 0x14, 0xF1, 0xD4, 0xFA, 0xA2, 0x8F, 0x48, 0x92, 0xF2, + 0xE1, 0xDA, 0x69, 0xE2, 0xFB, 0x9A, 0x2D, 0xC2, 0xBB, 0x55, 0x43, 0xF0, 0xD8, 0x0B, 0x84, 0xF6, + 0x5D, 0xF4, 0xAE, 0x9B, 0xFD, 0x18, 0xDF, 0x08, 0x61, 0x1A, 0xA3, 0x07, 0x54, 0x53, 0x57, 0xFD, + 0x24, 0x79, 0xA5, 0xA6, 0xA0, 0x02, 0x7C, 0x47, 0x00, 0xC8, 0x91, 0x06, 0x83, 0xBC, 0x04, 0x03, + 0x22, 0xA9, 0xCA, 0x6D, 0xC4, 0x58, 0xDC, 0x3E, 0xE1, 0x0F, 0x76, 0xC7, 0x88, 0x94, 0x80, 0xFB, + 0x15, 0x91, 0x23, 0x05, 0x66, 0x23, 0xE8, 0x05, 0x9F, 0x97, 0xC9, 0x15, 0xE2, 0x49, 0xF4, 0x06, + 0xEB, 0x0E, 0x63, 0x1D, 0x1D, 0x80, 0x0F, 0x6A, 0xEF, 0x1A, 0xC1, 0x13, 0xF3, 0x8A, 0x35, 0x05, + 0x72, 0xA6, 0xDC, 0x02, 0xA5, 0xB6, 0x83, 0xE7, 0xB0, 0xEA, 0x17, 0xFB, 0xDF, 0xF1, 0x98, 0xEE, + 0xB1, 0x1B, 0x6C, 0x4D, 0x8B, 0x49, 0x22, 0x4F, 0x87, 0xAE, 0xF3, 0xCE, 0x59, 0xC3, 0x3B, 0xEC, + 0x03, 0x88, 0xF9, 0xEF, 0xBE, 0x3A, 0xA6, 0x80, 0x2D, 0xED, 0xD2, 0x49, 0x10, 0xCB, 0x1F, 0xFD, + 0x45, 0xF6, 0x46, 0xBB, 0x15, 0x74, 0xA9, 0x3A, 0x7C, 0xA4, 0x0F, 0xC2, 0x96, 0xAC, 0x41, 0x53, + 0x39, 0x03, 0xE2, 0x66, 0xAA, 0x1C, 0x47, 0xDC, 0xAA, 0xEA, 0xA3, 0xAC, 0x86, 0x1E, 0xEF, 0x65, + 0x73, 0x30, 0x25, 0xD1, 0x5D, 0x5E, 0x5D, 0x93, 0xEB, 0x4C, 0xEB, 0x67, 0x95, 0x5C, 0x21, 0x6A, + 0x71, 0x88, 0x8D, 0x8B, 0x43, 0xF0, 0xE2, 0xC0, 0xBC, 0x7D, 0x20, 0x1B, 0x90, 0xA3, 0xAB, 0x84, + 0x75, 0x34, 0x28, 0x07, 0xA5, 0xFB, 0x35, 0x3A, 0xA2, 0x14, 0x95, 0x43, 0xA5, 0x60, 0x65, 0xB9, + 0xB5, 0xA8, 0x31, 0xD4, 0xB1, 0x87, 0x4A, 0xD5, 0xBE, 0xD7, 0x2A, 0xD2, 0x49, 0xB4, 0xCB, 0xF3, + 0x20, 0x73, 0x28, 0xA0, 0x9A, 0xDA, 0x99, 0x4D, 0xC5, 0x14, 0xA9, 0x19, 0x54, 0x77, 0x24, 0x11, + 0x3D, 0x84, 0xA0, 0x00, 0x13, 0xB3, 0xDE, 0x97, 0xC6, 0x87, 0x50, 0xEF, 0x53, 0x97, 0xEA, 0x43, + 0x69, 0x77, 0xFC, 0x20, 0x37, 0x28, 0x95, 0x6B, 0x94, 0x2A, 0x80, 0x52, 0x09, 0x90, 0x89, 0x4E, + 0xC5, 0x27, 0xA8, 0xD6, 0x96, 0xF6, 0x0F, 0x1B, 0x1D, 0x5D, 0xC7, 0x8E, 0xA3, 0xEB, 0xEF, 0x75, + 0x6D, 0x5F, 0xC9, 0x1C, 0x4A, 0xE6, 0x51, 0xE2, 0x4A, 0x5B, 0xDB, 0xC7, 0x1D, 0x97, 0x18, 0x89, + 0x0C, 0x4F, 0x60, 0x5A, 0xF5, 0x83, 0x79, 0x65, 0xFD, 0xE0, 0xE3, 0x7E, 0x2A, 0x91, 0x08, 0x16, + 0x9F, 0x4C, 0x3F, 0x85, 0x33, 0x7C, 0xB9, 0xC7, 0x17, 0xD9, 0x7B, 0xFA, 0x64, 0x1E, 0xFB, 0xE5, + 0x5D, 0x5A, 0xD1, 0xD9, 0xE8, 0x88, 0xCC, 0x4C, 0x68, 0xB3, 0x19, 0xB8, 0x55, 0xBC, 0x94, 0xA3, + 0x43, 0x21, 0x93, 0x73, 0x2D, 0xB7, 0x57, 0xF7, 0x39, 0x53, 0xE6, 0x64, 0x52, 0x35, 0xB7, 0x33, + 0x95, 0x2A, 0x93, 0xD3, 0x29, 0x82, 0x58, 0x2D, 0x4F, 0x6E, 0xF3, 0xA0, 0xA5, 0x56, 0x1E, 0x19, + 0x10, 0x61, 0xF3, 0xC8, 0xE6, 0xD6, 0xCC, 0xE7, 0xC7, 0xB8, 0x03, 0xE0, 0xB4, 0x22, 0x53, 0x81, + 0xBE, 0x3C, 0x4C, 0x0A, 0xF3, 0x63, 0x65, 0x67, 0x54, 0xB6, 0xBB, 0x71, 0xCB, 0x1E, 0xA9, 0x47, + 0x5C, 0xC8, 0x11, 0x2B, 0xD9, 0x20, 0x22, 0x53, 0x5E, 0x4C, 0x7F, 0x0E, 0xFC, 0x74, 0x38, 0x1D, + 0x11, 0x90, 0xF8, 0x27, 0x70, 0xFA, 0xCB, 0x12, 0x85, 0xCD, 0xB9, 0xDF, 0xC8, 0xB9, 0x36, 0x97, + 0x12, 0xDA, 0x69, 0xFE, 0xAB, 0x22, 0xC2, 0x0D, 0xDE, 0x48, 0xFE, 0x59, 0xE5, 0x8B, 0x23, 0xC5, + 0xD0, 0xCE, 0x71, 0x4D, 0x25, 0x1C, 0x38, 0x93, 0x1F, 0x49, 0xED, 0xF0, 0x92, 0xF4, 0x81, 0xA4, + 0xB8, 0xAF, 0x69, 0x87, 0x03, 0xB2, 0x4A, 0xCC, 0xE7, 0xB0, 0x94, 0x29, 0x39, 0xD5, 0x8B, 0xB4, + 0xBA, 0x98, 0x57, 0x5F, 0xA9, 0x12, 0x35, 0x75, 0x71, 0x60, 0xD9, 0x01, 0x66, 0x71, 0x6C, 0xED, + 0xC2, 0xA7, 0xF2, 0x16, 0x9F, 0x70, 0x6A, 0x33, 0x8F, 0x07, 0xCA, 0x20, 0xE6, 0x45, 0xE3, 0x70, + 0x42, 0x1A, 0xEA, 0xCC, 0x56, 0xBA, 0x88, 0xCD, 0x89, 0xE2, 0x38, 0x66, 0x87, 0xCC, 0x33, 0xA9, + 0x36, 0xAD, 0x76, 0x19, 0x69, 0x3A, 0xEC, 0xBC, 0x3B, 0xF3, 0xC1, 0x9C, 0x4C, 0xB3, 0x73, 0x32, + 0xCD, 0x92, 0xC1, 0x46, 0xB5, 0xC6, 0x8E, 0xA2, 0x81, 0x67, 0xD6, 0x85, 0x17, 0xE9, 0x0F, 0xE1, + 0x3B, 0xB5, 0x42, 0x1A, 0xC2, 0xFA, 0x25, 0x92, 0x6B, 0x0A, 0xEC, 0xAD, 0x93, 0x6F, 0xB8, 0x18, + 0x5C, 0x47, 0x53, 0xB2, 0x8D, 0x36, 0x6F, 0xEC, 0x90, 0x32, 0x4A, 0x0B, 0x84, 0x34, 0x57, 0x1D, + 0xFC, 0x41, 0x32, 0x73, 0xEA, 0x29, 0x6E, 0x47, 0x75, 0x94, 0x36, 0xE5, 0x29, 0xB2, 0xC2, 0x4B, + 0xDD, 0x61, 0x44, 0x52, 0x8C, 0xF2, 0x2A, 0x0D, 0xCE, 0xE9, 0x6F, 0x64, 0xDC, 0xC3, 0x0B, 0xB2, + 0x14, 0x0D, 0x84, 0xDE, 0xE5, 0xD2, 0x25, 0x76, 0x2E, 0xA4, 0x37, 0x8A, 0xBD, 0xB8, 0x29, 0x1E, + 0xA4, 0xD0, 0xB4, 0xAB, 0x9C, 0x89, 0x64, 0xE0, 0xC6, 0x9D, 0xA2, 0x54, 0xA9, 0xA3, 0xD3, 0x10, + 0x85, 0xB7, 0xAF, 0x59, 0x08, 0x5F, 0x03, 0x89, 0x39, 0xB4, 0xB5, 0x80, 0x28, 0x47, 0xDB, 0x33, + 0xE1, 0x00, 0xE3, 0x7B, 0x6E, 0xA8, 0xC1, 0x06, 0x3F, 0xA0, 0x16, 0xC9, 0x64, 0xE8, 0x6F, 0x60, + 0x78, 0xD9, 0x33, 0xA4, 0xCB, 0x47, 0x9D, 0x14, 0x18, 0x2E, 0xC8, 0x1F, 0xD5, 0xB3, 0x49, 0x0C, + 0x14, 0xFF, 0xE4, 0x1A, 0xF9, 0x49, 0x25, 0x04, 0xEC, 0x0A, 0xE1, 0xC6, 0x73, 0x7B, 0x9C, 0x47, + 0xA1, 0xD8, 0x62, 0x21, 0x2B, 0x3F, 0x28, 0x64, 0x91, 0x42, 0xF3, 0xC5, 0x42, 0x55, 0x8B, 0x0F, + 0xEC, 0xEC, 0x07, 0x5F, 0x34, 0x59, 0xEE, 0x63, 0x95, 0xB8, 0x26, 0xCF, 0xEE, 0x2D, 0x38, 0x5B, + 0x74, 0xCB, 0xCE, 0x32, 0x7B, 0x37, 0xAB, 0x74, 0xAB, 0xB6, 0x15, 0x44, 0xA8, 0x8E, 0xB0, 0x00, + 0x92, 0x3C, 0xF0, 0x2E, 0x80, 0xA9, 0x0E, 0xF4, 0x7A, 0x69, 0x63, 0x55, 0xEE, 0x42, 0x95, 0x3F, + 0xBA, 0x0C, 0xB5, 0xB1, 0xC1, 0x55, 0xE1, 0xED, 0xB2, 0x2D, 0xD7, 0x64, 0x66, 0xFF, 0xB6, 0xC3, + 0x36, 0x00, 0x53, 0x3C, 0xB0, 0xE8, 0xA2, 0x8E, 0xEF, 0x27, 0xA8, 0xFA, 0x5C, 0xDA, 0x91, 0xEF, + 0xA9, 0xE7, 0x07, 0x17, 0xF4, 0x77, 0xFF, 0x86, 0xFE, 0x6A, 0x81, 0xEF, 0x42, 0x0F, 0xF8, 0x5E, + 0x3D, 0xE0, 0xB3, 0x1B, 0xCB, 0x70, 0xCF, 0xA3, 0x47, 0x49, 0xFA, 0xC1, 0x37, 0xFA, 0x51, 0xE2, + 0xE3, 0x85, 0x82, 0xE1, 0x3D, 0xFF, 0xE2, 0xA3, 0xFC, 0x5D, 0x87, 0xA7, 0x71, 0x55, 0xB7, 0x4F, + 0x28, 0x91, 0x8A, 0x08, 0xAB, 0xDB, 0xC1, 0xBD, 0xBD, 0x53, 0x89, 0x24, 0x1F, 0xE2, 0x53, 0x6C, + 0xC5, 0xA8, 0x45, 0x50, 0x9D, 0x6F, 0x0D, 0x58, 0x1A, 0x96, 0x14, 0xA6, 0xD8, 0xF0, 0xEE, 0xB1, + 0x85, 0x92, 0x56, 0x52, 0xDA, 0xF5, 0x11, 0x24, 0x3C, 0x82, 0x15, 0xB7, 0x7F, 0x4E, 0x1B, 0x1D, + 0xE2, 0x0F, 0xC0, 0xE0, 0xCF, 0x05, 0x29, 0x5F, 0x5A, 0x6C, 0xB6, 0x64, 0xF6, 0xC7, 0x4F, 0x27, + 0xB3, 0x0E, 0x2D, 0xCD, 0x9B, 0xA4, 0xB2, 0xB0, 0x54, 0x8C, 0xE9, 0xE8, 0x15, 0x68, 0xC3, 0x1B, + 0x0A, 0x74, 0x66, 0x4B, 0x04, 0xF3, 0x53, 0xBE, 0x9C, 0x40, 0x4C, 0x4E, 0xA0, 0x32, 0xBD, 0xD4, + 0xB5, 0x96, 0xE1, 0xD8, 0x92, 0xBC, 0xD9, 0x00, 0x9C, 0x6B, 0x06, 0x42, 0xFA, 0x03, 0x2B, 0x16, + 0x13, 0x4C, 0x6E, 0x32, 0x48, 0xAA, 0x28, 0xA9, 0x36, 0xAF, 0x74, 0x25, 0x3C, 0x10, 0xA0, 0x69, + 0x51, 0x17, 0x15, 0xE8, 0xEB, 0x64, 0x5B, 0xF6, 0x71, 0x1D, 0x62, 0x28, 0xD4, 0xA7, 0xB6, 0x17, + 0x76, 0x93, 0x1A, 0x2A, 0x90, 0x30, 0xB8, 0x8A, 0x17, 0x8F, 0xF0, 0x46, 0x12, 0xBF, 0x72, 0xFD, + 0x68, 0x69, 0xA8, 0xA1, 0x04, 0x81, 0xDA, 0x85, 0x11, 0x38, 0x8C, 0x82, 0x50, 0x54, 0xC0, 0xEA, + 0x7A, 0x4D, 0x44, 0x6C, 0xA4, 0x5D, 0x99, 0x5D, 0x47, 0xFF, 0x5E, 0x41, 0x6B, 0x05, 0xBF, 0xD9, + 0x10, 0xBC, 0x74, 0x85, 0x29, 0x3E, 0xB8, 0x1A, 0x0A, 0xA3, 0xC7, 0xFF, 0xCD, 0x98, 0x9A, 0x08, + 0xF8, 0x9F, 0xE6, 0x07, 0xED, 0xB2, 0xBF, 0x1A, 0x8F, 0xA8, 0xDC, 0x3F, 0xDE, 0x72, 0x7A, 0x61, + 0xB7, 0xDE, 0x87, 0x74, 0x6E, 0x83, 0x20, 0x08, 0x41, 0xA8, 0x85, 0x13, 0x78, 0x7C, 0xDA, 0xF6, + 0x26, 0xC9, 0x05, 0xE9, 0x5B, 0xDB, 0xE0, 0x44, 0xF8, 0xA2, 0x36, 0x87, 0xD4, 0x30, 0xC3, 0xA2, + 0xD4, 0xD4, 0x45, 0xBE, 0x49, 0x13, 0xF9, 0x16, 0x0D, 0x16, 0x7B, 0xD5, 0x42, 0xC0, 0x1A, 0x67, + 0x5D, 0xAD, 0xC0, 0x58, 0x37, 0x22, 0xE4, 0x75, 0x6C, 0xAD, 0xA2, 0x16, 0xFB, 0xDF, 0x91, 0x57, + 0xF7, 0x3A, 0x00, 0xC7, 0x9C, 0x41, 0x9F, 0x7A, 0x07, 0x46, 0x72, 0x8D, 0xB0, 0x9E, 0x1A, 0xB7, + 0x03, 0x94, 0xA4, 0x87, 0x2A, 0xC6, 0x47, 0x66, 0x78, 0xFA, 0xF2, 0xEA, 0xBE, 0x84, 0xF1, 0x5B, + 0x47, 0x51, 0x1A, 0xFE, 0x42, 0x2F, 0x60, 0xA7, 0xBF, 0x10, 0xE3, 0x01, 0x2B, 0xBA, 0x06, 0x2B, + 0x7A, 0x4B, 0x4D, 0xD0, 0xA5, 0xC5, 0xF1, 0xF0, 0xDA, 0xF0, 0xA1, 0xCB, 0xCE, 0x20, 0xB6, 0xB4, + 0x1D, 0x3E, 0x6C, 0x1B, 0x67, 0xF6, 0xF8, 0x23, 0x2A, 0xDE, 0x80, 0x97, 0xD3, 0x3A, 0x5E, 0x4E, + 0x0C, 0x5E, 0x8E, 0xC3, 0x3A, 0xD8, 0xA2, 0x79, 0xD8, 0x09, 0xA4, 0xE8, 0x66, 0x8D, 0x7B, 0xC5, + 0x74, 0x60, 0x32, 0x54, 0xA9, 0xA6, 0x77, 0x16, 0x6C, 0xA0, 0x61, 0x1A, 0x3F, 0xD3, 0x77, 0xA4, + 0x80, 0xB6, 0x03, 0x23, 0x2F, 0xE2, 0x5B, 0x62, 0xAB, 0xE7, 0xD0, 0x7C, 0xCF, 0xBF, 0xB8, 0xD5, + 0xDB, 0xA0, 0x70, 0xCD, 0x9D, 0x44, 0xC8, 0x53, 0x7C, 0x3E, 0x1F, 0x85, 0x1F, 0x68, 0x2C, 0xA7, + 0x01, 0xAE, 0x57, 0x54, 0x42, 0x27, 0x9D, 0x74, 0xC1, 0x29, 0x1F, 0x5B, 0x04, 0xF0, 0x03, 0x2D, + 0xE7, 0xF0, 0x49, 0xFC, 0xB1, 0xB5, 0xD7, 0x25, 0x7C, 0x1D, 0xFF, 0xB5, 0x01, 0x8C, 0xD3, 0x2D, + 0xC0, 0x70, 0x30, 0x29, 0x5A, 0x58, 0xD0, 0x60, 0xB0, 0x4F, 0x51, 0xF9, 0xB3, 0x41, 0x49, 0xAE, + 0xEE, 0xE8, 0xD9, 0x80, 0x60, 0x5A, 0xF5, 0xEF, 0x87, 0x4F, 0x46, 0x5F, 0xDE, 0xE1, 0x0F, 0x28, + 0xC7, 0x53, 0x9A, 0xE2, 0xA7, 0x81, 0xA4, 0x1E, 0x2F, 0xE4, 0x4B, 0xF8, 0x1E, 0x32, 0x85, 0xF2, + 0xE8, 0xBF, 0x97, 0x1B, 0xAE, 0x5F, 0x0F, 0x3F, 0x8C, 0xBE, 0x88, 0x49, 0x2D, 0x73, 0x92, 0x9E, + 0x52, 0x52, 0xF8, 0x7A, 0xF8, 0x42, 0x7E, 0x7A, 0xDF, 0x13, 0xB7, 0x70, 0xD0, 0x76, 0x92, 0xB0, + 0x8A, 0x8C, 0xDA, 0xCB, 0x8B, 0xF8, 0x94, 0x40, 0x79, 0xAC, 0x4E, 0x71, 0x3A, 0xE3, 0x4D, 0xA5, + 0xA7, 0xE1, 0xFB, 0x80, 0x36, 0xB8, 0x9E, 0x04, 0x7A, 0x55, 0x3E, 0x8F, 0xDB, 0x8B, 0xFD, 0x56, + 0x6E, 0x00, 0xB0, 0x75, 0x9B, 0x38, 0xB7, 0x33, 0x79, 0x97, 0x8B, 0xDC, 0x5F, 0x6B, 0xE2, 0x54, + 0xD4, 0x46, 0xCA, 0x23, 0x04, 0x6E, 0x3C, 0xD0, 0x2D, 0xFC, 0x93, 0x3B, 0x77, 0x55, 0xDA, 0x05, + 0xD8, 0x88, 0xC7, 0x1D, 0x44, 0xE0, 0xEA, 0xCB, 0x78, 0x3E, 0xF8, 0x63, 0x74, 0xDC, 0x7F, 0xF9, + 0x25, 0x04, 0x3B, 0x95, 0x8A, 0x85, 0xF4, 0xDC, 0x7F, 0x19, 0xF4, 0x5F, 0x1E, 0x1C, 0x04, 0x7D, + 0xC7, 0xA8, 0x8E, 0xF4, 0x0B, 0x40, 0x74, 0x13, 0xD5, 0x21, 0x14, 0x72, 0x72, 0xC7, 0x17, 0x78, + 0xE3, 0x7B, 0x1F, 0xF1, 0x01, 0x5A, 0x45, 0x97, 0xFF, 0x59, 0x72, 0x67, 0x6F, 0xA4, 0x6C, 0xB1, + 0x5C, 0x0D, 0xFC, 0x05, 0x6B, 0xC7, 0xC6, 0x59, 0x89, 0x46, 0xF4, 0x8B, 0x75, 0xBD, 0x53, 0xEC, + 0x21, 0xE9, 0x54, 0xE4, 0x99, 0xA5, 0x8B, 0x8A, 0xD4, 0xC9, 0xE1, 0xF8, 0x7B, 0x60, 0xE2, 0x0F, + 0x53, 0x6B, 0x73, 0x52, 0x42, 0x83, 0xB2, 0x4F, 0xEC, 0x43, 0x13, 0xB3, 0xB6, 0x27, 0xF9, 0x60, + 0x8A, 0xED, 0x53, 0x31, 0x65, 0xBB, 0xC2, 0x8F, 0x29, 0x74, 0x80, 0x42, 0x8E, 0x85, 0x53, 0x38, + 0x3B, 0x5B, 0x08, 0xF5, 0x49, 0x36, 0xE0, 0xFD, 0x0B, 0xE0, 0x11, 0x37, 0x2E, 0x6D, 0xA8, 0x87, + 0xB7, 0x4A, 0x16, 0xB7, 0xE6, 0xD9, 0x15, 0x8A, 0x3F, 0xF0, 0x4E, 0xCC, 0x8D, 0xFC, 0xE1, 0x51, + 0xBE, 0xFB, 0x87, 0x99, 0x08, 0x87, 0xD2, 0x64, 0x1D, 0x94, 0xA1, 0x6C, 0x52, 0x86, 0xBC, 0xE5, + 0x4E, 0x6A, 0x34, 0x17, 0x16, 0x6A, 0xFD, 0xD6, 0x35, 0xAE, 0xC8, 0xD1, 0x74, 0x30, 0x23, 0x89, + 0xCA, 0xE4, 0x2C, 0x5F, 0x79, 0xA7, 0xD4, 0x2C, 0xC6, 0xBC, 0x52, 0x1C, 0x43, 0x38, 0xA6, 0xA7, + 0x84, 0xAE, 0x81, 0xA4, 0xFB, 0xAE, 0x00, 0xD1, 0x19, 0xA4, 0x0C, 0xFA, 0x1D, 0x07, 0xFD, 0x46, + 0x1F, 0x88, 0xB3, 0x5B, 0xE9, 0xF5, 0x54, 0x69, 0x3A, 0xC6, 0xAD, 0xA4, 0x7C, 0x80, 0x67, 0xE5, + 0x38, 0x99, 0x0B, 0x9D, 0x3A, 0x0E, 0xD3, 0xF2, 0x95, 0x2E, 0xF2, 0x0D, 0x5B, 0x81, 0x41, 0x5D, + 0x00, 0xDA, 0x97, 0x36, 0x33, 0xE0, 0x3D, 0x59, 0xFF, 0xA7, 0xFC, 0x51, 0xA6, 0x7F, 0x07, 0xD2, + 0xE6, 0xEC, 0xA1, 0xB9, 0x5A, 0x06, 0x21, 0x9B, 0xF4, 0xD0, 0x26, 0xAD, 0x8A, 0x4C, 0x63, 0x80, + 0x5A, 0x0C, 0x0F, 0xE3, 0x40, 0x33, 0xB4, 0xE3, 0x31, 0x33, 0xDF, 0x14, 0x1F, 0x58, 0x72, 0x94, + 0x0D, 0xD6, 0xC4, 0x82, 0xC1, 0xF0, 0x28, 0xC4, 0xA5, 0xFB, 0x24, 0x1B, 0xCC, 0xAA, 0x07, 0x78, + 0x1C, 0xA6, 0x63, 0x83, 0x16, 0x7A, 0xAD, 0x4C, 0x73, 0xC3, 0x1C, 0x05, 0x1C, 0x76, 0x90, 0xB1, + 0x3C, 0x8C, 0xBB, 0x87, 0x8F, 0xD1, 0x68, 0x47, 0xD4, 0x44, 0x3A, 0x48, 0xDB, 0x0A, 0x46, 0x8D, + 0x47, 0x70, 0x18, 0x54, 0x8A, 0xDE, 0xE5, 0xC4, 0xA9, 0x12, 0xFC, 0x1C, 0x8F, 0x8C, 0x86, 0x45, + 0x92, 0x0B, 0x19, 0x6E, 0xFD, 0x04, 0xBF, 0xB0, 0xC2, 0x86, 0x1C, 0xDA, 0xD5, 0x6A, 0x5F, 0x99, + 0x7C, 0x8A, 0x75, 0x84, 0xA7, 0x42, 0x19, 0x53, 0x53, 0x13, 0x7E, 0x95, 0x07, 0x2A, 0xF6, 0x9C, + 0x6F, 0x0A, 0x1C, 0x3A, 0x14, 0x6D, 0x04, 0xB4, 0x4D, 0x40, 0x35, 0xC6, 0xF8, 0x7B, 0xFF, 0x68, + 0x27, 0xF2, 0x96, 0xEF, 0xB0, 0x29, 0xC5, 0xB3, 0x70, 0x7B, 0x96, 0xFB, 0x7D, 0x18, 0x83, 0x6C, + 0x1E, 0x1B, 0xDE, 0x54, 0x00, 0x79, 0x4A, 0x83, 0x3C, 0xB5, 0x42, 0x1D, 0x38, 0xC4, 0xFB, 0x92, + 0x1E, 0x21, 0x67, 0x52, 0x6D, 0x1D, 0xEB, 0xAE, 0xA1, 0x38, 0xB6, 0x28, 0x88, 0x51, 0x24, 0xEB, + 0x74, 0x64, 0xBB, 0x56, 0xB9, 0xD1, 0x61, 0x9B, 0xEB, 0x61, 0xD5, 0x89, 0xDC, 0xA7, 0x06, 0x12, + 0x3E, 0xBC, 0x02, 0x3E, 0x49, 0x34, 0x06, 0xFB, 0xA3, 0x43, 0xEE, 0xC2, 0x99, 0x2B, 0xF7, 0x8E, + 0x9B, 0x72, 0xEF, 0xB4, 0x21, 0xF7, 0x4E, 0xAC, 0xC4, 0xCC, 0x32, 0x71, 0x85, 0xFD, 0x93, 0x00, + 0xCC, 0x95, 0x6D, 0x70, 0xB1, 0x5A, 0x41, 0xA4, 0x31, 0x9F, 0x94, 0x7F, 0xED, 0xC6, 0x66, 0x98, + 0x0F, 0x8E, 0x20, 0xF4, 0x5D, 0x2B, 0x5A, 0x6A, 0x84, 0xA4, 0x59, 0x5D, 0x48, 0x1A, 0x6B, 0xB1, + 0xA8, 0x21, 0x2E, 0x4D, 0xD7, 0x14, 0x27, 0xD4, 0x0C, 0xA4, 0xB8, 0x8C, 0x7F, 0x68, 0xA4, 0xDC, + 0xC6, 0xBB, 0x97, 0xE1, 0x3D, 0x85, 0x45, 0xBE, 0xE3, 0x98, 0xA1, 0x8F, 0x6C, 0x71, 0xD6, 0xDA, + 0x2B, 0x64, 0xC5, 0xED, 0xF1, 0x0B, 0x27, 0x1D, 0x6C, 0xE5, 0xA2, 0xC9, 0x56, 0xCE, 0xBB, 0x56, + 0xFD, 0xCD, 0xE0, 0xE6, 0xD7, 0x04, 0x43, 0xDE, 0x84, 0xA7, 0x96, 0xF9, 0x93, 0x80, 0x73, 0xC6, + 0xE2, 0x7E, 0x8E, 0x18, 0x12, 0xA5, 0xDB, 0x2B, 0xE3, 0xE9, 0xBD, 0x0A, 0x84, 0x7C, 0xA6, 0xDF, + 0x55, 0x20, 0xE4, 0x47, 0xFD, 0xDE, 0x32, 0xAE, 0x42, 0x62, 0x3D, 0x1B, 0xDE, 0x8F, 0xC2, 0xD7, + 0xFC, 0xB3, 0x7F, 0x0D, 0xD9, 0x30, 0x7C, 0xCA, 0xCF, 0x07, 0xD7, 0xC3, 0x67, 0xA3, 0xF0, 0x05, + 0x2C, 0x81, 0x07, 0x17, 0xC3, 0x8F, 0x30, 0x97, 0xA2, 0xFD, 0xF7, 0x8E, 0x20, 0x7C, 0x39, 0x38, + 0x41, 0x72, 0x44, 0xDF, 0xC2, 0xE7, 0x6E, 0xBA, 0xCC, 0x1F, 0x1D, 0xD0, 0x67, 0x08, 0x5D, 0xCD, + 0xD8, 0x3D, 0x5C, 0x57, 0x48, 0x97, 0xC9, 0x0E, 0x2E, 0x2B, 0x08, 0x5E, 0x91, 0x32, 0xD4, 0x1F, + 0x69, 0x43, 0x3D, 0xC5, 0x2C, 0x34, 0xC1, 0x3F, 0x94, 0xA1, 0xB5, 0xBF, 0x9D, 0xE3, 0x1D, 0x12, + 0x01, 0xBB, 0x1C, 0x07, 0x8F, 0xC8, 0xD3, 0xD3, 0x58, 0xF2, 0x50, 0x40, 0xF0, 0x57, 0xF1, 0x2B, + 0x1A, 0xF7, 0x5B, 0xFC, 0x60, 0xC8, 0x3F, 0x53, 0xBC, 0xE5, 0x51, 0xC8, 0xDD, 0xFF, 0x06, 0x7F, + 0x82, 0xF0, 0x97, 0xF8, 0x96, 0x06, 0x4B, 0x96, 0xE3, 0x17, 0x07, 0x3F, 0x1F, 0x7C, 0x75, 0x70, + 0x1E, 0xBD, 0x97, 0xBF, 0xE1, 0x67, 0xF8, 0x74, 0x20, 0xBF, 0xED, 0xBF, 0xD8, 0xFF, 0x79, 0xFF, + 0xED, 0xFE, 0x79, 0xF4, 0x5C, 0xFE, 0x86, 0xDF, 0xC5, 0x1D, 0x7B, 0x9B, 0x2A, 0xBF, 0x99, 0x18, + 0xE0, 0x86, 0x97, 0xEF, 0x06, 0x7A, 0x82, 0xBE, 0xB3, 0x5E, 0x2B, 0x0A, 0x16, 0xFD, 0xCE, 0x71, + 0x55, 0xC9, 0xE0, 0xD1, 0x37, 0x4D, 0xF0, 0x28, 0xE6, 0x36, 0xE8, 0x4E, 0xAE, 0x51, 0x75, 0x4C, + 0x2B, 0x6A, 0xF8, 0x56, 0xCE, 0xF5, 0x2F, 0x07, 0x6F, 0x0E, 0xBE, 0x0E, 0xBF, 0x97, 0x2F, 0x9F, + 0x1D, 0xBC, 0x61, 0x2D, 0x41, 0xE2, 0xD3, 0x0F, 0x04, 0x82, 0xAB, 0x01, 0x6C, 0x81, 0xAF, 0xC3, + 0x6F, 0x83, 0xE8, 0x75, 0xF8, 0x24, 0xBC, 0x1A, 0x7C, 0x80, 0x3E, 0x11, 0xE2, 0x54, 0xF3, 0xA7, + 0x41, 0x9F, 0x8A, 0xC4, 0x3F, 0x84, 0xA7, 0xFC, 0x73, 0xF0, 0x84, 0x44, 0x7E, 0x85, 0x89, 0x7F, + 0xE7, 0xD5, 0x64, 0x31, 0xF1, 0xAF, 0xFC, 0x6E, 0x31, 0xF1, 0x47, 0x34, 0x77, 0x37, 0x0A, 0xFF, + 0x16, 0xFF, 0x08, 0x5C, 0xFB, 0xFB, 0x88, 0x4E, 0xFD, 0xFA, 0x11, 0x98, 0xF6, 0x57, 0x58, 0x22, + 0x2A, 0xD3, 0xE8, 0xDF, 0xA8, 0xD1, 0xBF, 0x85, 0x3F, 0xCA, 0x46, 0xAB, 0x8A, 0x5A, 0xAD, 0xA0, + 0xBB, 0x52, 0xD1, 0x18, 0x04, 0xE3, 0x54, 0xFE, 0x1E, 0xFC, 0xB8, 0x5E, 0x77, 0x70, 0x8B, 0xD3, + 0xED, 0x02, 0xB8, 0x23, 0x70, 0x17, 0x46, 0xE0, 0x6E, 0x48, 0xDA, 0xBB, 0xC7, 0xCA, 0x13, 0x1E, + 0x12, 0xBD, 0x3C, 0x63, 0xE3, 0x6C, 0xA1, 0x36, 0x11, 0x25, 0xF1, 0x15, 0xBF, 0xCF, 0x62, 0x37, + 0x62, 0xE0, 0xC8, 0x46, 0x09, 0x10, 0x46, 0x8D, 0xED, 0x7A, 0x55, 0x3C, 0x9B, 0x38, 0xF5, 0x6E, + 0x42, 0xC7, 0x64, 0x50, 0x40, 0xAB, 0x6F, 0x0D, 0xC2, 0xF2, 0xEE, 0xCA, 0xAF, 0xC9, 0x33, 0x42, + 0x5F, 0x66, 0x31, 0xC5, 0xCB, 0x06, 0x64, 0x9E, 0x97, 0xFB, 0xC0, 0xAE, 0xE8, 0x67, 0xE0, 0xB6, + 0x45, 0x1E, 0x5A, 0x37, 0xA2, 0xC0, 0x69, 0xBA, 0xEC, 0x99, 0xE7, 0x75, 0xF4, 0x19, 0x95, 0x0C, + 0x42, 0xE9, 0x38, 0xF5, 0xFD, 0x71, 0xFC, 0x8E, 0x9E, 0x02, 0x62, 0xC5, 0xC2, 0xF5, 0x80, 0x8E, + 0x7B, 0xF7, 0x36, 0x05, 0x25, 0x11, 0xCC, 0x49, 0x37, 0x7F, 0xC0, 0x2C, 0xFB, 0x1D, 0xF7, 0x8A, + 0xB7, 0x31, 0x15, 0xD2, 0x38, 0x39, 0x73, 0xDA, 0x3D, 0x18, 0xB3, 0x87, 0x87, 0x0D, 0xB6, 0xE6, + 0x03, 0xCA, 0x23, 0xFD, 0x5E, 0xED, 0x6D, 0x2A, 0xEA, 0x5B, 0xBD, 0xB4, 0x09, 0x58, 0x7A, 0x84, + 0x12, 0xE1, 0xC6, 0xBF, 0x99, 0x6D, 0xD3, 0x66, 0x42, 0x61, 0x1C, 0xEF, 0xE2, 0xAA, 0xCE, 0x04, + 0x62, 0x53, 0x91, 0x35, 0x00, 0x55, 0x31, 0x76, 0x30, 0xB2, 0x3F, 0x54, 0x9F, 0x6F, 0x16, 0x0A, + 0xD6, 0xFA, 0xF9, 0xB0, 0x12, 0xE9, 0x28, 0xC7, 0x8E, 0xA8, 0x2F, 0x60, 0x22, 0x85, 0x9E, 0x4F, + 0xC6, 0x33, 0x5B, 0x00, 0x2F, 0x6A, 0x9E, 0x76, 0x85, 0xD2, 0x68, 0xDA, 0x2A, 0xE6, 0xAE, 0x4F, + 0x5B, 0xCE, 0xDB, 0x54, 0x7D, 0xC3, 0xB6, 0xB7, 0xA0, 0x16, 0x76, 0xC2, 0x7B, 0xD9, 0x2D, 0x9E, + 0xF1, 0xAE, 0x5A, 0x36, 0x77, 0x2A, 0xFE, 0x8D, 0xCE, 0x99, 0x0D, 0xC5, 0xCF, 0x35, 0x04, 0xEA, + 0xE1, 0xD7, 0xC3, 0x11, 0xE4, 0xC1, 0xD2, 0x66, 0x3B, 0x97, 0x7C, 0xBE, 0x2E, 0xD5, 0x26, 0x22, + 0x2A, 0xFB, 0x8D, 0xDB, 0xF7, 0x36, 0x60, 0x79, 0x2E, 0x1B, 0x2F, 0xE9, 0x4E, 0xF6, 0xCE, 0x99, + 0xE0, 0xC3, 0x3C, 0xC5, 0xE4, 0xB9, 0x3B, 0x21, 0x4A, 0xB8, 0x68, 0x07, 0x74, 0x26, 0xBC, 0x37, + 0xB1, 0xB6, 0x1A, 0x23, 0x24, 0x6B, 0x5A, 0x17, 0x39, 0x32, 0x55, 0x65, 0x82, 0x1B, 0x51, 0xCC, + 0x28, 0x19, 0x94, 0x5B, 0xC6, 0xE7, 0xC9, 0xDB, 0x3C, 0x62, 0xB4, 0x36, 0xC6, 0x52, 0x84, 0x9C, + 0xB1, 0x64, 0xE1, 0x26, 0x4A, 0x20, 0x05, 0xEA, 0x51, 0x5B, 0x19, 0x08, 0x03, 0xA3, 0x1B, 0x84, + 0x13, 0x2D, 0xF7, 0xB4, 0x55, 0x99, 0xDC, 0x7E, 0x04, 0x18, 0xF0, 0x52, 0x8B, 0x11, 0x8B, 0x9D, + 0x9E, 0xC9, 0xDD, 0x64, 0x6F, 0xA4, 0xBF, 0xA7, 0xE1, 0x77, 0xA0, 0xE4, 0x46, 0x1A, 0x3B, 0x70, + 0xF4, 0x60, 0xDE, 0xF0, 0xCD, 0x95, 0x06, 0xA4, 0xE3, 0xB6, 0xE5, 0xA3, 0x9D, 0x62, 0xAC, 0x52, + 0x29, 0x3F, 0xBD, 0x06, 0x71, 0xCB, 0x30, 0x6E, 0x73, 0x64, 0x49, 0xB7, 0x7D, 0x5E, 0xB0, 0xB1, + 0x45, 0x09, 0x77, 0x32, 0x24, 0x6C, 0x43, 0xCE, 0x94, 0x62, 0xA9, 0x65, 0x4E, 0xA2, 0x32, 0x4E, + 0x5C, 0x4F, 0x89, 0x8F, 0x7C, 0x56, 0x48, 0x29, 0x3F, 0x86, 0xC6, 0xF5, 0x84, 0x14, 0x4D, 0x3E, + 0x39, 0x7A, 0x46, 0xBD, 0x5A, 0x6A, 0xCA, 0xC9, 0x5D, 0xAE, 0x83, 0x4C, 0x1E, 0x7B, 0xC2, 0x8D, + 0xE2, 0x45, 0x8E, 0xB1, 0x94, 0xBB, 0x17, 0xF1, 0xDE, 0x87, 0x96, 0x40, 0x0D, 0xAF, 0xA9, 0xC3, + 0x19, 0x5B, 0x9E, 0xE8, 0x0D, 0xFD, 0xE4, 0x04, 0x7A, 0xD0, 0x03, 0x1F, 0x57, 0x8F, 0xB4, 0x3A, + 0x76, 0x7A, 0xE1, 0x6C, 0x2E, 0x16, 0x9C, 0x09, 0xE1, 0xD1, 0x82, 0xB4, 0x17, 0xA2, 0x3D, 0xBF, + 0xE6, 0x67, 0xB3, 0xB8, 0x61, 0x59, 0x49, 0xD0, 0xE9, 0x70, 0x73, 0x77, 0x64, 0x82, 0xAF, 0x32, + 0xDC, 0xCC, 0x54, 0x99, 0x04, 0x2C, 0xD4, 0x16, 0x02, 0xA6, 0xA4, 0xDA, 0x6D, 0x5C, 0x39, 0xA9, + 0x41, 0x4E, 0xA1, 0x9F, 0x02, 0xD6, 0x10, 0xBA, 0xB2, 0x52, 0x3A, 0xF2, 0xD1, 0x0F, 0x06, 0x14, + 0x09, 0xA3, 0xC1, 0xB9, 0xA6, 0xFB, 0x22, 0x80, 0xBC, 0x3F, 0xEF, 0x84, 0x49, 0x21, 0xE3, 0xE1, + 0xAC, 0xB7, 0x2E, 0xE9, 0x35, 0x97, 0x79, 0x3C, 0xDE, 0x0E, 0x54, 0xA5, 0x98, 0xD0, 0xA4, 0xB5, + 0x0B, 0x6F, 0xF3, 0x28, 0x29, 0x8C, 0xB5, 0x3A, 0x4A, 0x83, 0xC0, 0x2D, 0xD7, 0x44, 0xE0, 0xF0, + 0x55, 0xC6, 0x9C, 0x92, 0x40, 0xD2, 0x41, 0x6E, 0x4B, 0x59, 0x1D, 0x3C, 0xA3, 0xBE, 0xA1, 0x0B, + 0x54, 0x2F, 0x60, 0x61, 0x2E, 0x94, 0x9E, 0x1A, 0x3A, 0x95, 0x81, 0xC4, 0xCE, 0x18, 0xD7, 0x48, + 0x5E, 0x76, 0x23, 0x5B, 0x25, 0x1E, 0x4E, 0x55, 0xA4, 0xA4, 0x0F, 0xAD, 0x0C, 0x5D, 0x1F, 0x8B, + 0xEF, 0xF8, 0xD5, 0x92, 0x17, 0x89, 0xD3, 0x88, 0x52, 0x50, 0x3C, 0xC6, 0x48, 0x6F, 0xA1, 0xA8, + 0x45, 0x81, 0xD8, 0x1D, 0x20, 0xBC, 0x31, 0x41, 0xC8, 0x3D, 0x6B, 0xCB, 0x84, 0xA4, 0xFB, 0x3A, + 0x91, 0x29, 0xE8, 0x9B, 0xDC, 0xB0, 0x60, 0xB7, 0x2B, 0x26, 0xAD, 0x58, 0x68, 0x13, 0x02, 0x7E, + 0x49, 0x59, 0x41, 0x29, 0xB5, 0x89, 0xF1, 0x18, 0x8F, 0x86, 0x5A, 0xDB, 0x82, 0x36, 0xED, 0xD1, + 0xD3, 0xA2, 0x51, 0xA0, 0x21, 0x44, 0x55, 0x0A, 0x93, 0xDB, 0x61, 0x6E, 0x1A, 0xEF, 0x02, 0xB3, + 0x87, 0x32, 0x03, 0x4F, 0xCD, 0xBE, 0x68, 0x37, 0xA8, 0x6D, 0xEA, 0x99, 0xBC, 0x84, 0x60, 0x97, + 0x74, 0x4A, 0x35, 0x02, 0x4D, 0x17, 0xDA, 0x85, 0x86, 0x19, 0x9D, 0x3E, 0x8A, 0xCE, 0x66, 0xB4, + 0x00, 0xD5, 0xC0, 0x6A, 0x71, 0xF3, 0x05, 0x21, 0x49, 0x01, 0x55, 0x53, 0x92, 0xAD, 0x7E, 0x07, + 0x7E, 0xE4, 0x7C, 0x5D, 0x7D, 0x6E, 0x90, 0xC3, 0xA0, 0xC2, 0x4C, 0xA2, 0xC9, 0xC4, 0x41, 0x93, + 0x35, 0x88, 0x62, 0xA2, 0x0E, 0x55, 0x76, 0xE0, 0x9B, 0x61, 0xE3, 0xEA, 0x1A, 0x3C, 0x48, 0xA2, + 0x06, 0x09, 0x5C, 0x06, 0x6A, 0x06, 0x68, 0x44, 0xAF, 0xB1, 0x45, 0x05, 0x17, 0xCC, 0x36, 0x84, + 0x85, 0x69, 0xCF, 0xE2, 0x11, 0x1F, 0xB3, 0xEE, 0x27, 0xF2, 0xE8, 0x86, 0x76, 0x1D, 0x05, 0x07, + 0xC7, 0x6D, 0xAC, 0x49, 0x25, 0x52, 0xBF, 0xF2, 0xD9, 0x2D, 0x6D, 0x94, 0x85, 0xD6, 0x9C, 0xD5, + 0x8E, 0x13, 0xD2, 0x51, 0x25, 0x68, 0xA5, 0xA4, 0xF3, 0x9E, 0xF9, 0x1F, 0xC8, 0x17, 0x08, 0x82, + 0xBA, 0x4C, 0xD8, 0xC5, 0x64, 0x75, 0x1D, 0xFF, 0x91, 0x3A, 0x74, 0x69, 0xC6, 0xF2, 0xAF, 0x39, + 0xAE, 0x7E, 0x6A, 0x05, 0xAB, 0x09, 0x32, 0xCF, 0xB6, 0xE2, 0x4C, 0xC5, 0x57, 0xC9, 0x32, 0x97, + 0x5E, 0xEB, 0xF2, 0x3D, 0xCB, 0x9E, 0xFD, 0xD4, 0x76, 0xD6, 0x96, 0xDD, 0x1D, 0xD3, 0xC9, 0x98, + 0x79, 0xF6, 0x75, 0x5A, 0x94, 0x95, 0x04, 0x50, 0x2B, 0x41, 0xB1, 0x83, 0xA9, 0x94, 0x11, 0xA7, + 0x22, 0x1E, 0x13, 0x10, 0x27, 0xFC, 0xBB, 0x6C, 0x0A, 0x48, 0xD1, 0xF0, 0x55, 0x05, 0xA1, 0x20, + 0x7C, 0x09, 0x2B, 0x02, 0xD1, 0xB5, 0x70, 0xF1, 0x88, 0x7C, 0x30, 0x44, 0x86, 0x7F, 0xAB, 0x60, + 0x7A, 0x09, 0x5F, 0x80, 0xA7, 0x08, 0x2A, 0x76, 0x25, 0x34, 0xB2, 0x5F, 0x15, 0x42, 0xFC, 0x82, + 0x1A, 0xDE, 0xBD, 0x9B, 0x17, 0x79, 0x95, 0xBF, 0x7B, 0xC7, 0xE1, 0x4A, 0x6A, 0x29, 0xFE, 0x95, + 0xA2, 0xAC, 0x29, 0xBE, 0x31, 0x1A, 0x0B, 0x74, 0xA9, 0x42, 0x01, 0x6D, 0xAF, 0x89, 0xFE, 0x0A, + 0x41, 0x9A, 0x4F, 0x22, 0x97, 0xE6, 0xD6, 0xAF, 0xC8, 0x62, 0x33, 0x15, 0xB5, 0xB4, 0x68, 0x51, + 0x7F, 0x7F, 0x96, 0x56, 0x40, 0x47, 0xC1, 0x11, 0xA3, 0xA9, 0x39, 0xE7, 0x20, 0xD3, 0xBE, 0xFF, + 0xB2, 0x22, 0x0D, 0x95, 0xD5, 0x28, 0xF2, 0xAD, 0x99, 0xAD, 0x69, 0xA8, 0xD9, 0xFA, 0xC2, 0xC8, + 0xE7, 0xAC, 0xC2, 0xA7, 0xF9, 0x87, 0x03, 0xA8, 0x55, 0x28, 0x75, 0x23, 0x42, 0x26, 0x6A, 0x85, + 0xCC, 0x84, 0x3A, 0xA8, 0x28, 0xB2, 0xE1, 0x45, 0x6A, 0xA8, 0x36, 0x78, 0xC7, 0x12, 0x2E, 0xCF, + 0x3C, 0xA2, 0xA4, 0xF6, 0xD2, 0x39, 0x5D, 0x49, 0x20, 0xD8, 0xD9, 0xB7, 0x59, 0x15, 0xDA, 0xE3, + 0x82, 0xA2, 0xDA, 0xD1, 0x41, 0x05, 0xFE, 0x6A, 0x3B, 0x9D, 0x39, 0x3E, 0x28, 0x72, 0x4F, 0x12, + 0xB2, 0x07, 0x09, 0x45, 0xB5, 0x43, 0x85, 0x6E, 0x38, 0x85, 0xFE, 0xAA, 0x92, 0x9C, 0xC5, 0x3D, + 0x63, 0xC8, 0x6E, 0xF6, 0x69, 0xEE, 0xFC, 0xB9, 0x93, 0x69, 0xFC, 0xA3, 0x4A, 0xAB, 0x6C, 0xF6, + 0xD9, 0xAA, 0x1F, 0x2F, 0x49, 0x74, 0x80, 0x55, 0xBD, 0x0A, 0x9D, 0x73, 0x6B, 0xA2, 0x9B, 0x4A, + 0x1D, 0x8E, 0x02, 0xD4, 0xA9, 0xED, 0x3B, 0x8A, 0x80, 0x5A, 0xF5, 0x3D, 0x16, 0xD1, 0xAB, 0x8A, + 0xFD, 0x99, 0x11, 0x70, 0x8D, 0x5C, 0x1E, 0x51, 0x2A, 0xD4, 0xD5, 0xB3, 0x11, 0x10, 0xB0, 0x66, + 0x52, 0x8A, 0x80, 0x9A, 0x0D, 0xFB, 0x5F, 0x94, 0xDB, 0x23, 0xE9, 0xE7, 0x52, 0x4E, 0x7C, 0x25, + 0xAE, 0xCF, 0x3E, 0xCE, 0x7D, 0x8F, 0xEF, 0x44, 0xFD, 0x6E, 0xBE, 0xE2, 0xDF, 0x27, 0x88, 0x04, + 0x5D, 0x49, 0xDB, 0xBE, 0x07, 0xF1, 0x5B, 0xC4, 0x74, 0xAA, 0xBE, 0x47, 0x8E, 0x21, 0x8E, 0x7D, + 0xE3, 0x27, 0x85, 0x18, 0xD7, 0xEA, 0xA3, 0x4D, 0x8A, 0x4C, 0xC6, 0xF0, 0x9D, 0xFA, 0x28, 0x91, + 0x50, 0x16, 0x56, 0x2F, 0xBA, 0xFC, 0x65, 0x2D, 0x8B, 0xAE, 0xC2, 0x2D, 0x11, 0xDE, 0xAA, 0x2C, + 0x84, 0xB7, 0x26, 0x03, 0x63, 0xB3, 0xAE, 0xE4, 0x5E, 0xE6, 0xA8, 0x25, 0x46, 0x6E, 0xFE, 0xF0, + 0x0E, 0x6A, 0xA1, 0x82, 0x13, 0x4C, 0xF4, 0x7F, 0x18, 0x59, 0x5F, 0x69, 0x1B, 0x7F, 0x2D, 0x6E, + 0xEA, 0x83, 0xBB, 0x42, 0x5C, 0x07, 0x0E, 0x24, 0x44, 0xE6, 0xC9, 0x3D, 0xC8, 0x7D, 0x3A, 0xD6, + 0x08, 0x2D, 0x4F, 0x44, 0xE4, 0xD5, 0xCC, 0xCB, 0x89, 0xAE, 0x89, 0x97, 0x9B, 0x64, 0xCE, 0x6C, + 0x83, 0x9E, 0x9F, 0x90, 0xB2, 0xA8, 0x2E, 0xBB, 0x35, 0xE2, 0x42, 0xE0, 0xD9, 0x4E, 0xB4, 0xCF, + 0x22, 0xB7, 0xBD, 0x68, 0x7D, 0x5C, 0xC9, 0x43, 0xA6, 0x02, 0xA7, 0x5B, 0x32, 0x43, 0xA3, 0x57, + 0x1E, 0x07, 0x43, 0xAA, 0xDC, 0x4E, 0xB3, 0xA6, 0xA7, 0xED, 0x9B, 0x78, 0xED, 0x19, 0xE8, 0x27, + 0x62, 0xC3, 0x19, 0xE8, 0xC0, 0x48, 0x7D, 0x06, 0xBA, 0x39, 0x62, 0x54, 0xB9, 0x7B, 0xA8, 0xB9, + 0xED, 0x87, 0x9F, 0x0B, 0x5D, 0x02, 0xE3, 0x58, 0xD8, 0xAF, 0xCF, 0xF1, 0xA6, 0x94, 0x1F, 0xE7, + 0x76, 0xCE, 0x17, 0xC9, 0xED, 0x65, 0x52, 0xA8, 0x5C, 0x92, 0x30, 0xCA, 0x24, 0xFF, 0xD7, 0xDE, + 0xFA, 0x7A, 0x27, 0x3A, 0x3E, 0xD3, 0xF9, 0x7C, 0x36, 0xCB, 0x99, 0xD8, 0x7C, 0xD2, 0xF6, 0x04, + 0x9B, 0x47, 0xB1, 0x47, 0xD4, 0xBD, 0xBC, 0x7D, 0xDA, 0x38, 0x73, 0x75, 0xB5, 0x7A, 0xCC, 0x0D, + 0x23, 0x5B, 0x0F, 0xDF, 0x76, 0x4F, 0xDF, 0x6E, 0x57, 0x6F, 0xC1, 0xF7, 0x88, 0xA3, 0x6B, 0xF5, + 0x99, 0xB3, 0x27, 0xAC, 0x2D, 0x49, 0x04, 0xFF, 0x1A, 0xFC, 0x5F, 0xC1, 0xBA, 0x51, 0x39, 0x64, + 0xCF, 0xC6, 0x65, 0x2A, 0xB5, 0xEF, 0x92, 0xBD, 0x3F, 0x78, 0xCC, 0xB4, 0x01, 0x52, 0x28, 0xDA, + 0x87, 0xE1, 0xF2, 0xA0, 0xEA, 0xF3, 0x1B, 0x7C, 0xD7, 0x6B, 0xDD, 0x64, 0x6E, 0x07, 0x19, 0x1A, + 0x8E, 0x21, 0x8F, 0x9C, 0x90, 0xB7, 0x61, 0xD4, 0x8E, 0x5B, 0xBC, 0x12, 0x9D, 0x87, 0x2D, 0x7A, + 0x5F, 0xC1, 0xBA, 0x4F, 0x62, 0xEF, 0xFC, 0xF7, 0xE5, 0x8E, 0xEE, 0x54, 0xB9, 0xA3, 0xD4, 0xCE, + 0x1D, 0xC9, 0x2C, 0x77, 0xB0, 0xBB, 0xA4, 0x9A, 0x97, 0xD1, 0xE1, 0xA1, 0xDA, 0x42, 0xFE, 0xBE, + 0x84, 0xFC, 0x78, 0x1D, 0xA0, 0x1D, 0x79, 0xB2, 0x7D, 0x6D, 0x90, 0x7D, 0x4F, 0x1E, 0x6C, 0xD1, + 0xBA, 0x75, 0xD0, 0xAC, 0xD5, 0x01, 0x4A, 0x44, 0x85, 0xDF, 0xFD, 0x31, 0xC0, 0xD7, 0x64, 0xD3, + 0xB7, 0xA8, 0x75, 0x98, 0x65, 0x67, 0x46, 0x19, 0xD1, 0xD6, 0xFD, 0xA9, 0xE3, 0x52, 0x58, 0x39, + 0x4A, 0xB5, 0xF8, 0xA0, 0x0E, 0xBB, 0xE7, 0x3C, 0x98, 0xE3, 0xC7, 0xDD, 0x33, 0xD4, 0xD0, 0xBC, + 0xD4, 0xA8, 0xD0, 0x10, 0x9F, 0x2B, 0x66, 0x34, 0xB3, 0xA0, 0x5F, 0x5B, 0xEB, 0x0B, 0xD2, 0x37, + 0xEC, 0x3C, 0xA9, 0xDB, 0x2C, 0x1E, 0x3B, 0x97, 0x6A, 0x9B, 0x74, 0xB0, 0x7E, 0xF4, 0x9D, 0xD1, + 0xE8, 0x8E, 0xBD, 0x49, 0xDF, 0xEB, 0x65, 0x8C, 0x3B, 0x07, 0xF8, 0xA1, 0x73, 0x5A, 0xAC, 0xE6, + 0x8D, 0x53, 0x41, 0xEB, 0x17, 0xA5, 0x8F, 0x71, 0xBC, 0xD0, 0x04, 0xE0, 0xA9, 0x1D, 0x0C, 0xC9, + 0xC7, 0x61, 0xEA, 0x5B, 0x46, 0xE5, 0xED, 0xE6, 0x8B, 0xD6, 0xE5, 0x85, 0x57, 0xA8, 0xA7, 0xF4, + 0xEB, 0xA9, 0x0F, 0x9F, 0xF1, 0x1D, 0x76, 0x2E, 0x50, 0x45, 0x2F, 0x3A, 0x4F, 0xD1, 0xDE, 0x94, + 0xED, 0x31, 0x07, 0x67, 0xBB, 0x8B, 0x6D, 0xCD, 0x67, 0x5F, 0x6F, 0xA2, 0x1B, 0xBB, 0xBF, 0x8A, + 0x70, 0x6C, 0xA7, 0x07, 0xF2, 0x95, 0x24, 0x16, 0x2C, 0x16, 0x8A, 0x0E, 0x60, 0x39, 0x9B, 0x98, + 0x52, 0x5E, 0x52, 0x47, 0x5C, 0x84, 0xD9, 0xDB, 0x73, 0xDF, 0x7A, 0x4A, 0x8F, 0x00, 0x78, 0x99, + 0x95, 0xF4, 0x4C, 0xA1, 0xB5, 0xD6, 0x9C, 0x97, 0x8F, 0x67, 0x03, 0x9D, 0x2D, 0xA8, 0x7A, 0x36, + 0xDC, 0x92, 0xDB, 0xB8, 0x0F, 0xD2, 0x9E, 0x8A, 0x5A, 0x87, 0x2A, 0x9F, 0xFB, 0xDE, 0xBE, 0xB2, + 0xB1, 0x7E, 0x6F, 0x03, 0x9A, 0x68, 0xC0, 0x60, 0xF9, 0xE0, 0x81, 0xE1, 0x4E, 0x13, 0x6D, 0x2A, + 0x09, 0x26, 0xF2, 0x09, 0xEB, 0xE2, 0x57, 0x20, 0xFF, 0xD5, 0x55, 0x07, 0xF6, 0x3F, 0x6A, 0xB6, + 0x2C, 0x16, 0x35, 0x4F, 0x0F, 0x7E, 0x18, 0xBB, 0x6B, 0xD9, 0x3E, 0x69, 0x5D, 0x79, 0x08, 0xA5, + 0xE4, 0x23, 0xAF, 0xBF, 0x53, 0x95, 0x3C, 0x48, 0x62, 0x1E, 0x75, 0x3A, 0xB6, 0x3B, 0x09, 0xCD, + 0x8B, 0xA4, 0xD3, 0x2B, 0x75, 0xBA, 0x35, 0x97, 0x76, 0x04, 0xA0, 0x9E, 0x42, 0x83, 0xC7, 0xDF, + 0x2D, 0x5D, 0xF1, 0x2D, 0xD2, 0x4E, 0x63, 0x61, 0xBB, 0x4E, 0x2B, 0x8A, 0x04, 0x61, 0x9B, 0x13, + 0xB8, 0xE4, 0x7F, 0x17, 0x03, 0x72, 0x68, 0xBE, 0xEB, 0x3C, 0xD8, 0xED, 0x28, 0xD0, 0xED, 0x46, + 0xE8, 0xE4, 0x98, 0xB6, 0x8B, 0x8D, 0x5B, 0x07, 0x7E, 0x1F, 0xED, 0x48, 0x0D, 0x7C, 0xC7, 0x51, + 0xD6, 0x76, 0xA0, 0xCA, 0xDE, 0xA6, 0x98, 0x9E, 0x1D, 0x6A, 0x74, 0x47, 0x77, 0x7A, 0x07, 0xFB, + 0x6A, 0xA6, 0xB8, 0x81, 0x6C, 0x27, 0x51, 0xAC, 0x16, 0x19, 0xBC, 0xCE, 0x3E, 0x78, 0xEA, 0x2C, + 0xE4, 0x1E, 0xAE, 0x92, 0x31, 0xE1, 0x76, 0x4D, 0x69, 0xD0, 0x9A, 0x06, 0xF8, 0x96, 0xF5, 0xE6, + 0x5C, 0x9A, 0x29, 0x3C, 0xA0, 0xD9, 0xE7, 0x53, 0x04, 0xB9, 0x0A, 0xA3, 0x3F, 0x3A, 0xD7, 0x73, + 0xBB, 0x45, 0xDD, 0x63, 0xD6, 0x48, 0xAC, 0xE9, 0x96, 0xD3, 0xA8, 0x72, 0xD2, 0x40, 0xEC, 0xDD, + 0x7B, 0x0F, 0x64, 0xE6, 0xD5, 0x6A, 0xB3, 0xDF, 0x0B, 0x73, 0x4C, 0x3E, 0xE9, 0xCE, 0xC4, 0x5B, + 0x5B, 0xE7, 0xD7, 0x59, 0xDC, 0xE5, 0xC3, 0xED, 0xF4, 0xF9, 0x24, 0x6F, 0x92, 0xD9, 0x02, 0xEB, + 0xE0, 0x80, 0xCE, 0x61, 0xD6, 0xBB, 0x47, 0xBC, 0x80, 0x50, 0xFA, 0xC6, 0x37, 0xE0, 0xDA, 0xDC, + 0x91, 0xC5, 0xDC, 0x83, 0xB4, 0x31, 0xB8, 0x16, 0x11, 0x99, 0x30, 0x07, 0x97, 0x22, 0x7A, 0x27, + 0xD6, 0x35, 0xE2, 0x69, 0x21, 0x2B, 0x03, 0x71, 0x6A, 0xE0, 0x69, 0x73, 0x5A, 0x4F, 0xAE, 0x0F, + 0x56, 0x2D, 0x0D, 0x50, 0xB5, 0x82, 0x53, 0x35, 0xCE, 0x57, 0x97, 0x15, 0xB7, 0x6F, 0x69, 0x19, + 0x98, 0x7D, 0x70, 0xA1, 0xC7, 0xC6, 0x64, 0x22, 0x47, 0xCD, 0x3B, 0x33, 0x2B, 0xBA, 0x12, 0xB3, + 0xF3, 0x50, 0x98, 0x0A, 0xF2, 0xD3, 0x97, 0x14, 0xC3, 0x51, 0x5F, 0x62, 0x68, 0xBE, 0x29, 0xEA, + 0xD8, 0x59, 0x77, 0x5D, 0x50, 0x5C, 0xAC, 0x81, 0x1D, 0xAE, 0x63, 0x70, 0x43, 0xCC, 0x8C, 0x31, + 0xF5, 0xD9, 0xD0, 0x91, 0x9A, 0xFC, 0xA5, 0x53, 0x61, 0xDA, 0xAB, 0x47, 0xA2, 0xD9, 0x82, 0x32, + 0x41, 0x16, 0x73, 0xC1, 0xB8, 0x5E, 0x8F, 0xB4, 0x2B, 0xD7, 0x39, 0x06, 0xAF, 0x56, 0xBB, 0x52, + 0xF1, 0xF8, 0xEA, 0x60, 0xD3, 0xD7, 0x78, 0xB8, 0xF5, 0xF4, 0xDB, 0xE3, 0x35, 0xE2, 0x0B, 0x88, + 0x76, 0x31, 0x91, 0xEA, 0x80, 0x64, 0xAD, 0x05, 0x57, 0x67, 0x1C, 0x6C, 0xFC, 0x42, 0xCE, 0xAB, + 0x8D, 0x1F, 0xD7, 0x6B, 0x79, 0xCB, 0x0D, 0x2F, 0x5D, 0xBA, 0x73, 0xD3, 0x5F, 0xC2, 0x65, 0x40, + 0x7E, 0x04, 0x29, 0x37, 0xC0, 0x53, 0xB2, 0x6C, 0x5C, 0x45, 0xDB, 0x58, 0xBA, 0x3B, 0xF6, 0x35, + 0x45, 0xF9, 0x28, 0xCB, 0x2B, 0x9F, 0xC6, 0x2E, 0x45, 0x4F, 0x7E, 0x8D, 0xCC, 0xAB, 0xE7, 0xB0, + 0x21, 0x7D, 0x2F, 0x10, 0xC5, 0x3B, 0x66, 0x66, 0xC3, 0xC3, 0xBD, 0x9F, 0xC1, 0x3F, 0xE2, 0x19, + 0x63, 0x06, 0xEF, 0xE3, 0x0A, 0x77, 0x33, 0x7B, 0x55, 0x20, 0xCC, 0xE8, 0x52, 0xC2, 0xB3, 0x2A, + 0xA3, 0xEE, 0xC1, 0x86, 0x9B, 0x01, 0x4F, 0xC4, 0xC6, 0xAB, 0x01, 0x89, 0x2A, 0xFC, 0xFF, 0x9D, + 0x0A, 0x8F, 0x0E, 0xEB, 0x21, 0x6D, 0xBB, 0x70, 0x8F, 0xAF, 0x42, 0xA7, 0x89, 0x2B, 0xD5, 0x0D, + 0xC6, 0xA4, 0x63, 0xFC, 0x81, 0xCF, 0x44, 0x07, 0x47, 0xAB, 0xF2, 0x0C, 0x87, 0x0C, 0x62, 0x36, + 0x17, 0x73, 0x75, 0xD6, 0xBB, 0xBC, 0xD9, 0xFD, 0x22, 0xB9, 0xF4, 0x76, 0xE9, 0x1D, 0x9F, 0x9A, + 0x97, 0xEC, 0x6D, 0xBE, 0x3F, 0x4C, 0xF7, 0xA7, 0x79, 0x1D, 0x1C, 0x69, 0x26, 0xF6, 0x3A, 0xB8, + 0xCC, 0xBD, 0x24, 0x24, 0x53, 0x80, 0x37, 0x00, 0xE7, 0x43, 0x70, 0x59, 0x17, 0x45, 0xCF, 0x58, + 0x47, 0xC9, 0x0C, 0x52, 0x1A, 0xCB, 0x46, 0x40, 0xF4, 0x30, 0xCD, 0x16, 0x42, 0x66, 0x7C, 0x8C, + 0x3E, 0x6E, 0x8A, 0xE8, 0xEB, 0x8A, 0x9A, 0xD2, 0xAF, 0xAD, 0x65, 0xDD, 0xBA, 0xCA, 0x89, 0x04, + 0x40, 0x34, 0x3B, 0x79, 0x89, 0x03, 0x08, 0x7D, 0x3E, 0x14, 0xD6, 0xA2, 0x5A, 0xA6, 0x30, 0x95, + 0x0A, 0xD5, 0x93, 0xAD, 0x78, 0xEE, 0xA1, 0x23, 0x14, 0xC1, 0xDC, 0x3D, 0x1A, 0x8A, 0xF0, 0x40, + 0x1E, 0xB0, 0xD5, 0xAD, 0x99, 0x52, 0x77, 0xD4, 0xBA, 0x59, 0x3B, 0x4E, 0x7D, 0x4E, 0x3F, 0x45, + 0x84, 0x6C, 0x98, 0xD1, 0x58, 0xCD, 0xE8, 0x6A, 0xA5, 0x6E, 0x3B, 0x97, 0xCB, 0x7A, 0x25, 0x29, + 0x99, 0xB9, 0xFB, 0x7C, 0x45, 0xE7, 0x26, 0xE9, 0xFD, 0xF9, 0xA2, 0x75, 0x01, 0xBA, 0xED, 0x86, + 0x94, 0xA9, 0xDD, 0x66, 0xE0, 0x08, 0xE6, 0x34, 0x96, 0xC8, 0x63, 0x68, 0x2D, 0x59, 0x43, 0x83, + 0x28, 0x2D, 0xC2, 0x77, 0xDA, 0x2C, 0x2C, 0xD6, 0x97, 0x78, 0xAE, 0x9F, 0x98, 0xDA, 0xB8, 0x43, + 0xF2, 0x5B, 0x79, 0x7F, 0xBB, 0xBE, 0x45, 0x52, 0x6C, 0xB9, 0xC6, 0xBD, 0x3D, 0x8A, 0x81, 0xF7, + 0x7A, 0x8E, 0x05, 0xE3, 0x80, 0xC4, 0x93, 0x16, 0x51, 0x83, 0xF6, 0x18, 0x8C, 0xA5, 0x30, 0x4E, + 0xAA, 0xB6, 0xA5, 0x22, 0x49, 0x17, 0x55, 0xB5, 0x5B, 0xBE, 0xDA, 0x92, 0x5C, 0x82, 0x68, 0x77, + 0x2E, 0x64, 0x57, 0xBA, 0xD7, 0x14, 0x2D, 0xDC, 0x2D, 0xD8, 0xAB, 0x2F, 0xBC, 0x90, 0x9D, 0xB4, + 0xDD, 0x76, 0x6E, 0x31, 0x6A, 0x5D, 0xB1, 0x20, 0x8F, 0xCC, 0xCE, 0xE7, 0x24, 0x74, 0x24, 0xD7, + 0x6C, 0x78, 0x47, 0x9A, 0xD4, 0x1D, 0x1B, 0xED, 0x67, 0x7C, 0x8F, 0x2D, 0x7D, 0x00, 0xDF, 0xC6, + 0xC6, 0x69, 0x51, 0x3E, 0xB0, 0xBA, 0x99, 0x93, 0x44, 0x52, 0x77, 0x92, 0x44, 0x67, 0x7B, 0xFE, + 0xE1, 0x91, 0xF6, 0x7A, 0x37, 0x47, 0x30, 0xF0, 0xD1, 0x2B, 0xA3, 0x8D, 0x33, 0x3D, 0x05, 0x45, + 0x70, 0xA8, 0x16, 0x60, 0x67, 0x40, 0x6E, 0x67, 0xCB, 0x4E, 0x8D, 0x53, 0x13, 0x56, 0x11, 0x2A, + 0x93, 0x18, 0xA8, 0x2B, 0x6B, 0x90, 0x17, 0x7C, 0x0F, 0x7A, 0x4D, 0x76, 0x26, 0xD0, 0x06, 0x65, + 0xA6, 0x83, 0x8F, 0x15, 0x5E, 0xD8, 0xC9, 0x6D, 0xF4, 0xA6, 0x7D, 0xDD, 0x06, 0x72, 0x4D, 0x1A, + 0x0A, 0xBB, 0xBD, 0x70, 0x63, 0x3B, 0x5C, 0x42, 0x54, 0xDE, 0x89, 0xCD, 0x41, 0xF8, 0x49, 0x8D, + 0x34, 0xF1, 0xED, 0xF1, 0xF5, 0x32, 0xAC, 0x36, 0xD4, 0x5A, 0x1B, 0x79, 0xB3, 0xA4, 0x24, 0x31, + 0xBF, 0xAA, 0x24, 0xB7, 0xF9, 0xAB, 0xC0, 0x55, 0x3F, 0x01, 0xA9, 0x8D, 0xF0, 0x1B, 0x79, 0xA2, + 0xBA, 0x7B, 0xE3, 0x44, 0x04, 0xCA, 0x00, 0x7E, 0x21, 0x5C, 0xB3, 0x77, 0xF3, 0x52, 0xC5, 0xB8, + 0xA6, 0x68, 0xAF, 0xAF, 0xD5, 0xE9, 0xDF, 0x8E, 0xE0, 0xB9, 0x41, 0x61, 0xAF, 0x1D, 0xEB, 0xD1, + 0x3A, 0x1B, 0x5C, 0x9D, 0xAB, 0x98, 0x66, 0x30, 0x53, 0x70, 0x8E, 0x83, 0x2A, 0x60, 0xB3, 0x4E, + 0x53, 0x8F, 0x31, 0x0D, 0x6A, 0xB3, 0x9C, 0x12, 0x8E, 0x48, 0x6A, 0xFD, 0x1A, 0x52, 0xAB, 0x73, + 0x13, 0xB7, 0x6E, 0x78, 0x93, 0x8E, 0x1A, 0x7A, 0x2A, 0xA4, 0xF5, 0x95, 0xDC, 0x59, 0x06, 0x21, + 0x5B, 0xEC, 0x57, 0x5B, 0xCB, 0x7B, 0x3D, 0x0E, 0x31, 0x38, 0xA0, 0xED, 0xC8, 0x3B, 0xEA, 0x59, + 0x7A, 0x6C, 0xF0, 0x9A, 0x96, 0x07, 0x9C, 0x82, 0xC7, 0xB2, 0xC2, 0x3C, 0xDE, 0x53, 0x36, 0xEF, + 0x57, 0xB5, 0x52, 0x2B, 0x2F, 0xEF, 0x12, 0x70, 0x8A, 0x13, 0x6C, 0xDA, 0xE3, 0x5E, 0xAA, 0x1A, + 0x13, 0xBA, 0x7C, 0x1C, 0x7A, 0x53, 0x32, 0x33, 0x55, 0x36, 0xC7, 0x2D, 0x8F, 0x3E, 0xDA, 0x74, + 0xAF, 0x5D, 0x4F, 0x7F, 0x8E, 0x95, 0xC9, 0xC0, 0x5B, 0x6F, 0x80, 0x66, 0xD8, 0xBA, 0x02, 0xBF, + 0x3D, 0x3F, 0x2C, 0xA9, 0x3F, 0x4F, 0xB2, 0x74, 0xBE, 0x98, 0x31, 0xA5, 0xB5, 0x17, 0x72, 0xB3, + 0x95, 0x89, 0x64, 0x83, 0xA6, 0x16, 0xB6, 0xB7, 0xD7, 0xC2, 0x87, 0x2F, 0x6B, 0xF8, 0xB3, 0x9F, + 0x69, 0x2A, 0xBD, 0x79, 0xD0, 0xD4, 0x3D, 0x7B, 0x03, 0xA3, 0xAC, 0xB0, 0xEB, 0x18, 0x75, 0xC8, + 0x9A, 0x7D, 0x35, 0x72, 0x3C, 0xC6, 0xA9, 0xEF, 0x2A, 0x66, 0x00, 0x6D, 0x42, 0x51, 0x56, 0x81, + 0xBC, 0x64, 0x2F, 0x58, 0x73, 0x54, 0x83, 0x01, 0x76, 0x21, 0x1E, 0x83, 0x66, 0x0E, 0xB8, 0x3F, + 0xA5, 0x58, 0x0D, 0x6F, 0x6C, 0xD1, 0xFF, 0x12, 0x62, 0x3E, 0xD8, 0xC0, 0x46, 0x9C, 0x0C, 0xD6, + 0x1B, 0xE1, 0x6F, 0x95, 0x08, 0x0B, 0xE1, 0x7E, 0xDA, 0x65, 0x2E, 0x57, 0xC8, 0xB4, 0xDE, 0xD0, + 0xBE, 0xAC, 0xEC, 0x71, 0x18, 0xD5, 0xBA, 0x3E, 0xA9, 0x81, 0x12, 0x4E, 0x8C, 0x94, 0xEA, 0x95, + 0x32, 0xAE, 0x69, 0x1B, 0x04, 0x1D, 0x22, 0xE4, 0x6B, 0x8B, 0x5B, 0xBB, 0x82, 0xD0, 0xC5, 0x16, + 0xB2, 0x82, 0x6E, 0xEE, 0x91, 0xDA, 0x61, 0x4E, 0x21, 0xC3, 0x29, 0xD5, 0x2A, 0xF5, 0x83, 0xA6, + 0x26, 0x6F, 0xAD, 0x94, 0xA8, 0x2E, 0x2D, 0xB5, 0x02, 0x8E, 0xA9, 0xB1, 0xC6, 0x8A, 0xC6, 0xE2, + 0xFA, 0x12, 0x51, 0x27, 0x72, 0x9C, 0xE7, 0x70, 0x5E, 0xA5, 0xE5, 0x9B, 0xB4, 0x4C, 0x2F, 0xE5, + 0x2D, 0xD0, 0x69, 0x79, 0x92, 0xA5, 0x37, 0x24, 0xBE, 0xCB, 0x13, 0x14, 0x73, 0x0D, 0xCD, 0x48, + 0x1D, 0x36, 0xC6, 0x4C, 0x46, 0xF7, 0x50, 0x1E, 0x85, 0x1F, 0x9E, 0xDA, 0x5A, 0x6A, 0x57, 0x46, + 0xDB, 0xCA, 0x6C, 0x62, 0xBD, 0x4E, 0x5F, 0x0D, 0x42, 0x3B, 0x52, 0x9B, 0xF5, 0x7B, 0x86, 0x33, + 0xAD, 0xA8, 0x29, 0xEB, 0x68, 0xFD, 0x50, 0xE3, 0x33, 0xC4, 0xB6, 0xB6, 0x7A, 0x50, 0x0D, 0xC9, + 0x44, 0x97, 0xA0, 0x80, 0x93, 0x7D, 0x94, 0x82, 0x8B, 0xEA, 0xCC, 0x89, 0x5A, 0x60, 0x2F, 0x62, + 0xB3, 0xBA, 0x9E, 0x19, 0xDC, 0xC0, 0x57, 0x58, 0xC4, 0x95, 0xF8, 0x8D, 0x0B, 0x8A, 0xED, 0x78, + 0x11, 0x2B, 0xEE, 0x9B, 0x3E, 0x18, 0xAB, 0x1C, 0xB2, 0xB7, 0xD2, 0xEA, 0xD7, 0xC5, 0x35, 0xCD, + 0xBC, 0x37, 0x84, 0x12, 0x82, 0x6B, 0x25, 0x20, 0xC8, 0x0B, 0x13, 0x2F, 0xD5, 0xB9, 0x25, 0xF8, + 0x95, 0x6C, 0xEE, 0xE1, 0xFE, 0xBA, 0x2D, 0x3E, 0x68, 0x5B, 0xEE, 0x6C, 0x94, 0xBE, 0x59, 0x7F, + 0x43, 0x58, 0xEB, 0x83, 0x53, 0xBD, 0x8A, 0xD1, 0xAB, 0x01, 0xB6, 0x83, 0xBB, 0xCB, 0xD0, 0x1B, + 0x5D, 0xC6, 0x9B, 0xA4, 0xB7, 0x90, 0xC5, 0x95, 0x9A, 0x49, 0x7A, 0x44, 0xEC, 0xC1, 0x6A, 0x93, + 0xCC, 0x0E, 0x08, 0x09, 0x48, 0xF2, 0xF0, 0x36, 0x03, 0xBA, 0x75, 0xE1, 0xDE, 0x55, 0x32, 0x69, + 0xDD, 0x7A, 0x8B, 0xAB, 0x18, 0x3B, 0x2E, 0x4A, 0xDC, 0x7A, 0x3F, 0xE3, 0xB9, 0xA8, 0x5F, 0x96, + 0x67, 0xCD, 0x71, 0x14, 0x13, 0x1D, 0xF4, 0x1C, 0x3C, 0x26, 0xFF, 0xA4, 0xFB, 0xCE, 0x57, 0x37, + 0x3A, 0x5D, 0xC7, 0x02, 0xA1, 0xED, 0x7D, 0x06, 0x6B, 0x96, 0x4D, 0x5C, 0x64, 0xFF, 0xB7, 0x1D, + 0x9F, 0x5B, 0x57, 0x4F, 0x16, 0x3A, 0x25, 0x9F, 0x44, 0xC7, 0x64, 0x2A, 0x21, 0xB0, 0xFD, 0x41, + 0x39, 0x2A, 0xB4, 0x70, 0x6B, 0x7B, 0x23, 0x51, 0xA8, 0xDE, 0x5E, 0x6D, 0xD1, 0x05, 0xEB, 0xAE, + 0xC5, 0x72, 0xD4, 0x76, 0x51, 0xD9, 0xCF, 0xD0, 0xE9, 0xA4, 0x7F, 0xA4, 0x06, 0xDD, 0x0D, 0x5D, + 0x68, 0xCC, 0x8D, 0xC1, 0xC2, 0xCE, 0x25, 0x1A, 0xAC, 0x5B, 0xF8, 0x48, 0x33, 0x05, 0xE5, 0xA8, + 0xB5, 0x96, 0x36, 0xAF, 0xC6, 0x40, 0x93, 0xBB, 0x67, 0x20, 0x54, 0xBA, 0x1B, 0x44, 0xED, 0x20, + 0x6B, 0x5F, 0xE6, 0x49, 0x31, 0xA1, 0x67, 0x56, 0x95, 0x38, 0x08, 0xE5, 0xA3, 0x9B, 0xAB, 0x1D, + 0xEB, 0x61, 0x4A, 0x59, 0xA2, 0x26, 0x0B, 0x9B, 0x77, 0x43, 0xA5, 0x9E, 0x88, 0x4F, 0xB8, 0x12, + 0xFF, 0xA1, 0x9B, 0xEF, 0x27, 0xE0, 0x8F, 0xF9, 0xB5, 0x73, 0x51, 0xB6, 0xD7, 0x93, 0x8B, 0x44, + 0x7E, 0xF0, 0xEA, 0x50, 0xD5, 0xA5, 0xF4, 0x40, 0x54, 0xBD, 0xA9, 0xE4, 0xB3, 0x38, 0x99, 0xF4, + 0x2B, 0x7C, 0x78, 0x82, 0x0F, 0x0E, 0xDC, 0x5F, 0x93, 0x83, 0xD4, 0xD2, 0x45, 0x44, 0x9A, 0x22, + 0x62, 0xEB, 0x2B, 0x55, 0xC1, 0x29, 0xE1, 0x08, 0x3E, 0x3E, 0x74, 0x81, 0xBF, 0x0C, 0xE8, 0xFF, + 0x2A, 0x91, 0xC1, 0x94, 0x17, 0x62, 0x6B, 0x68, 0xC9, 0xB3, 0x2D, 0x71, 0x23, 0x3C, 0x38, 0x13, + 0x34, 0x62, 0x0D, 0x18, 0xB5, 0xDE, 0x76, 0x87, 0x83, 0x20, 0xB7, 0x21, 0xE4, 0xB5, 0xEC, 0xAB, + 0xD5, 0x86, 0xFB, 0x8D, 0x1F, 0x0C, 0xD0, 0x90, 0xBD, 0x09, 0x9B, 0xAE, 0xDD, 0x75, 0xA7, 0x1F, + 0xB2, 0x01, 0x50, 0x7D, 0x7F, 0xB6, 0xC5, 0x48, 0x9F, 0xAC, 0x49, 0x1B, 0x00, 0x79, 0x14, 0x34, + 0x21, 0xA9, 0xC6, 0x17, 0xD6, 0xB4, 0xA7, 0x26, 0xD9, 0x93, 0xB8, 0x40, 0xAE, 0x17, 0xCF, 0x06, + 0xFA, 0xBC, 0x5F, 0x94, 0xD5, 0x13, 0x46, 0x0F, 0x57, 0xB5, 0x91, 0xA6, 0x02, 0xB6, 0x25, 0xB9, + 0xC9, 0xAF, 0xE8, 0xC4, 0x5A, 0x93, 0xBC, 0xD1, 0xD7, 0x4B, 0x5C, 0x00, 0xB1, 0xB0, 0xA5, 0x81, + 0x8A, 0xAB, 0x5D, 0xAA, 0x6F, 0x8A, 0x7E, 0x93, 0x6E, 0x59, 0x29, 0x03, 0x88, 0x62, 0x54, 0xA6, + 0x62, 0x8B, 0xD2, 0x0E, 0x9D, 0x68, 0xD5, 0xAD, 0xE8, 0x15, 0x95, 0x11, 0x9D, 0xD4, 0x65, 0x31, + 0x6F, 0x95, 0x51, 0x0E, 0x68, 0x65, 0x3C, 0x8A, 0x5B, 0xEA, 0x81, 0xBF, 0x11, 0xBD, 0x99, 0xCC, + 0x59, 0xFA, 0x47, 0x73, 0xAF, 0xBF, 0x33, 0x17, 0xB4, 0xC9, 0xD6, 0xAE, 0x16, 0x18, 0x1E, 0xCC, + 0xDA, 0x08, 0xDD, 0x5D, 0x73, 0x02, 0x19, 0xF7, 0xE4, 0xD5, 0xD9, 0x89, 0x37, 0xB2, 0x76, 0x4B, + 0x74, 0xA7, 0x79, 0x79, 0x67, 0x87, 0xA2, 0xBD, 0xFB, 0x18, 0x6C, 0xFD, 0x94, 0x2B, 0xB6, 0x19, + 0x20, 0x5B, 0xEE, 0xD7, 0x6E, 0x2E, 0x7D, 0xE3, 0x7A, 0x6B, 0x63, 0x6C, 0x5F, 0x3C, 0x06, 0x67, + 0x1F, 0x8F, 0x5D, 0x57, 0x57, 0x8E, 0xED, 0x82, 0x09, 0x68, 0x9A, 0x99, 0x2E, 0x3F, 0xDE, 0x21, + 0xCE, 0x35, 0x3D, 0x12, 0x51, 0x55, 0xEE, 0xC7, 0x63, 0x5F, 0xB0, 0xF1, 0xDA, 0x6A, 0x99, 0x4E, + 0x60, 0x7E, 0x4E, 0x39, 0x9B, 0x11, 0x2F, 0xA1, 0x70, 0xF9, 0xE5, 0x50, 0x2A, 0x90, 0x35, 0x42, + 0x3E, 0xEA, 0x8C, 0x2E, 0x70, 0x9A, 0x6E, 0x52, 0x70, 0x2D, 0x7D, 0x35, 0xA3, 0x3F, 0x1E, 0x05, + 0xCD, 0xF5, 0x94, 0x0D, 0x55, 0xDF, 0xD9, 0x30, 0x91, 0x36, 0x81, 0x58, 0x77, 0x70, 0x87, 0x7A, + 0x90, 0xFD, 0x07, 0xE1, 0x3B, 0x82, 0xBE, 0xBE, 0xB1, 0xBA, 0xC6, 0x75, 0x75, 0x6F, 0x03, 0x57, + 0xFA, 0x6F, 0x23, 0xD3, 0xBA, 0x1E, 0x39, 0x60, 0x5A, 0x91, 0xB2, 0xD7, 0x33, 0xF1, 0xF8, 0x38, + 0x81, 0x4D, 0x12, 0x1A, 0xC9, 0x60, 0x9A, 0x1C, 0x84, 0x1F, 0x59, 0x00, 0xAB, 0xAD, 0x5C, 0x47, + 0xBD, 0x6C, 0x77, 0x2F, 0xCC, 0x3A, 0xF8, 0x2C, 0x2B, 0x40, 0xB5, 0x39, 0x0C, 0xFA, 0x9B, 0xDC, + 0xE3, 0x7B, 0x7B, 0x9B, 0xBE, 0xF0, 0xD5, 0xA9, 0xE4, 0xC7, 0x02, 0x61, 0xE2, 0xF7, 0xB3, 0x67, + 0x67, 0xCF, 0xCF, 0x5E, 0x5C, 0xBC, 0x7B, 0x71, 0xFE, 0xE4, 0x6C, 0xB5, 0xAA, 0xD1, 0xF8, 0xA6, + 0xF0, 0x67, 0x2B, 0xEC, 0xB4, 0xBD, 0x68, 0xB7, 0x27, 0xEE, 0xBD, 0x9D, 0xE5, 0x70, 0x20, 0x74, + 0xCA, 0x59, 0xCD, 0x78, 0x11, 0x69, 0xA5, 0x79, 0x4C, 0x64, 0x89, 0x82, 0xE6, 0xEE, 0xD1, 0xF6, + 0xBC, 0x45, 0xCE, 0x17, 0xC8, 0x49, 0x08, 0x35, 0xEB, 0xB5, 0x5B, 0x53, 0xC9, 0x7D, 0xC5, 0xD7, + 0x46, 0xDB, 0x14, 0x54, 0x0C, 0xF5, 0xFE, 0xD3, 0x2E, 0xE4, 0x57, 0x54, 0xA0, 0x86, 0x83, 0x8C, + 0xFE, 0x66, 0x06, 0x32, 0xDE, 0x03, 0xF2, 0xB5, 0xB4, 0x84, 0x3F, 0x70, 0x0D, 0x7D, 0xBB, 0x86, + 0x76, 0x68, 0xDB, 0x46, 0x01, 0xE8, 0x11, 0x91, 0x67, 0xDD, 0x52, 0x84, 0xC0, 0x6A, 0xA8, 0xE1, + 0x15, 0xD9, 0x32, 0x1A, 0x1D, 0x5F, 0x3E, 0xBC, 0xC6, 0x99, 0xB3, 0xB6, 0x73, 0x34, 0xF8, 0xA2, + 0xFE, 0x0E, 0xF4, 0xD3, 0x3C, 0x69, 0xB5, 0x32, 0xFD, 0xDD, 0xFE, 0xC5, 0xFA, 0x4F, 0x74, 0x86, + 0xA0, 0x99, 0x83, 0x1B, 0xE5, 0xF5, 0xDD, 0xE2, 0x05, 0xCB, 0xBA, 0xE4, 0xD6, 0x12, 0x30, 0xAC, + 0x39, 0xBE, 0x8B, 0xA5, 0x37, 0xA7, 0x48, 0x4B, 0xE1, 0x35, 0xE7, 0x4D, 0x25, 0x1D, 0x16, 0x1D, + 0x9C, 0xD5, 0x91, 0x13, 0x83, 0xA8, 0xB3, 0x22, 0xC7, 0x55, 0xA5, 0x2A, 0xD2, 0x63, 0x53, 0xB3, + 0xAA, 0x45, 0x01, 0x3B, 0xEF, 0x34, 0xCE, 0x88, 0x27, 0xE6, 0xD1, 0x03, 0x09, 0xD6, 0x86, 0x19, + 0x6E, 0x83, 0x8B, 0x62, 0x18, 0xEA, 0x16, 0x81, 0x86, 0x38, 0xD4, 0x45, 0xBC, 0x55, 0x3F, 0x5A, + 0x05, 0x6D, 0xBB, 0x2E, 0xB3, 0x5A, 0x6E, 0xA3, 0x1E, 0x32, 0x66, 0xF8, 0x61, 0x7A, 0x20, 0x29, + 0x07, 0x13, 0x84, 0x47, 0x51, 0x1A, 0xD9, 0x91, 0x07, 0xF3, 0x32, 0xFD, 0xD8, 0xBC, 0xCA, 0x9A, + 0x5C, 0x91, 0x46, 0x25, 0x57, 0x70, 0xB7, 0x78, 0xAC, 0x5B, 0x68, 0x4A, 0xC8, 0xD6, 0x5E, 0x79, + 0xC2, 0x80, 0xE4, 0x9D, 0x32, 0x7E, 0x5B, 0xF6, 0x96, 0x36, 0xDB, 0x47, 0x45, 0xC6, 0x69, 0x48, + 0x07, 0xEB, 0x9A, 0xE8, 0x48, 0xEC, 0xED, 0x53, 0xA4, 0x69, 0x42, 0xF5, 0x8D, 0x22, 0xEA, 0x60, + 0x9B, 0x6E, 0x16, 0xB9, 0x42, 0x6F, 0x6F, 0xBC, 0x28, 0x88, 0xE9, 0xE8, 0xCB, 0xE4, 0x7D, 0xBE, + 0xA3, 0xAA, 0x93, 0x59, 0xD7, 0xB4, 0x28, 0x1D, 0x53, 0xB3, 0x29, 0xF3, 0xDE, 0xDE, 0x83, 0xAB, + 0xC2, 0x8A, 0xD0, 0xBA, 0x90, 0x51, 0xCB, 0x5C, 0x36, 0x6B, 0xB8, 0xFE, 0xC3, 0xC1, 0xFF, 0xD2, + 0xA8, 0xB3, 0xDE, 0xD6, 0xE8, 0xF2, 0x21, 0x79, 0xD8, 0x08, 0xBF, 0x8F, 0x11, 0x8C, 0x65, 0xA8, + 0x96, 0xE9, 0x0A, 0x36, 0xCA, 0xD7, 0x4E, 0x57, 0x17, 0x72, 0x7F, 0x3C, 0x62, 0x74, 0xEA, 0xDA, + 0x05, 0x1D, 0x74, 0xF6, 0xE5, 0x76, 0x27, 0x96, 0xBE, 0xE1, 0x76, 0x17, 0xEC, 0x4F, 0xFB, 0x48, + 0xC8, 0x9E, 0x6C, 0xEF, 0x94, 0x58, 0xAD, 0xEC, 0xD0, 0x35, 0xDA, 0xEA, 0xD8, 0x6F, 0xBA, 0xC4, + 0x0D, 0xFF, 0x3B, 0xB9, 0x63, 0xCF, 0xB0, 0x70, 0xE6, 0x89, 0x8D, 0x12, 0x5B, 0x58, 0x9E, 0x5E, + 0x1A, 0xAD, 0x12, 0x68, 0x61, 0x63, 0xA1, 0x5A, 0xCB, 0x5E, 0x9D, 0x6F, 0x35, 0xD8, 0xD8, 0x86, + 0xD8, 0xF0, 0x60, 0x5D, 0xA3, 0x5D, 0x1B, 0x83, 0x0D, 0x5D, 0x80, 0x3F, 0x0E, 0xA6, 0x70, 0x70, + 0x35, 0x96, 0xAF, 0xB5, 0x7A, 0xF3, 0x95, 0x1E, 0x5F, 0x1E, 0xF5, 0xD9, 0xDF, 0x4F, 0x81, 0xF1, + 0x68, 0x18, 0xC7, 0xDA, 0xE3, 0x01, 0x2F, 0x78, 0x96, 0x1A, 0x79, 0x9B, 0x2A, 0x2A, 0x3F, 0xC7, + 0x33, 0xBE, 0xB4, 0x80, 0x1D, 0x39, 0x41, 0xE8, 0x53, 0x31, 0x53, 0x07, 0x55, 0xF8, 0xA8, 0x3A, + 0xC8, 0xE3, 0x61, 0x2A, 0x59, 0x77, 0x10, 0x9F, 0xE5, 0x03, 0x1D, 0xA0, 0x4B, 0xB7, 0x1E, 0xAC, + 0xDF, 0xF3, 0x36, 0x86, 0x3E, 0x85, 0x62, 0x6B, 0xF0, 0x93, 0x95, 0x88, 0x9F, 0x74, 0x44, 0x6D, + 0x99, 0x30, 0xA8, 0xEE, 0xC0, 0xAD, 0xAA, 0x19, 0xB8, 0x55, 0x3D, 0x3E, 0x70, 0xAB, 0xE2, 0xC0, + 0x2D, 0x2A, 0x42, 0xE7, 0x36, 0xF0, 0xC5, 0xD8, 0x5D, 0xBE, 0x6D, 0xB3, 0x6E, 0xB7, 0x3A, 0xB6, + 0x39, 0x47, 0xCB, 0xAB, 0xAD, 0xF5, 0x5A, 0xA9, 0x42, 0x04, 0xFD, 0x2E, 0x2D, 0x5D, 0x7E, 0xDB, + 0xAE, 0xA1, 0x4B, 0x4B, 0x84, 0x90, 0x32, 0x5D, 0x93, 0x68, 0x77, 0xD8, 0x82, 0x74, 0x7E, 0xB5, + 0x48, 0x9B, 0x86, 0x8D, 0xB1, 0xEC, 0x8E, 0xA2, 0xA9, 0x56, 0x78, 0x0A, 0xC2, 0x0D, 0x53, 0x20, + 0x02, 0x19, 0x42, 0x5F, 0xF3, 0xBF, 0x3F, 0x31, 0xBE, 0xCA, 0xD7, 0x5B, 0x2C, 0x9C, 0x72, 0x51, + 0x50, 0xA0, 0x63, 0xF8, 0xD4, 0xCD, 0x66, 0xCD, 0x98, 0x6D, 0xCB, 0xA6, 0x2A, 0xD4, 0x36, 0x6D, + 0xBE, 0xF8, 0x94, 0x9D, 0x6E, 0x0F, 0xED, 0x6B, 0xB3, 0xF6, 0x87, 0x4F, 0xB6, 0x5A, 0x6E, 0xD9, + 0xCC, 0xD6, 0x36, 0x29, 0x42, 0x40, 0x1A, 0x27, 0xD9, 0x6D, 0x52, 0x7A, 0x5B, 0xAD, 0x92, 0xAF, + 0xC5, 0x7F, 0xDA, 0xEA, 0xF8, 0xA0, 0x79, 0xD1, 0xF4, 0xEC, 0xDF, 0x34, 0x31, 0xD6, 0xE9, 0xC2, + 0x2D, 0x69, 0xEA, 0x29, 0x62, 0x87, 0x21, 0xD4, 0xF1, 0x33, 0x24, 0xAB, 0x4E, 0xC6, 0x5C, 0x9F, + 0x28, 0x45, 0x79, 0xD1, 0x8A, 0x2F, 0x6D, 0xB8, 0x81, 0xB6, 0x40, 0xB6, 0x55, 0xAA, 0xF3, 0xAC, + 0x7B, 0x5B, 0xDC, 0x83, 0x62, 0xDE, 0xFF, 0x42, 0xF9, 0xEC, 0xD0, 0x1D, 0xB7, 0xF1, 0xC7, 0x87, + 0xF5, 0xB9, 0xAD, 0xD3, 0xB6, 0xAE, 0xB7, 0x8E, 0xBE, 0x6B, 0x5B, 0x61, 0x03, 0x3F, 0xF6, 0xF6, + 0x1E, 0x36, 0xE4, 0xD9, 0x96, 0x3A, 0xD1, 0x61, 0xB3, 0x76, 0x68, 0x4B, 0xB6, 0xC0, 0x71, 0x39, + 0x5B, 0x14, 0x1D, 0xE6, 0xFF, 0xC7, 0x5A, 0xDE, 0x3A, 0x04, 0xF1, 0x87, 0x15, 0xEC, 0xFF, 0xB1, + 0x3E, 0xB1, 0x79, 0x7D, 0xA8, 0xD6, 0xBB, 0x17, 0x82, 0x5E, 0x07, 0x9F, 0xA2, 0x0D, 0x58, 0xB8, + 0x77, 0x62, 0x82, 0x6B, 0x1F, 0xFC, 0x4F, 0x18, 0xFA, 0x6C, 0x73, 0x5B, 0x4D, 0x6B, 0xAF, 0xFF, + 0x43, 0xA6, 0x35, 0x07, 0xF3, 0xAB, 0xF0, 0x29, 0x51, 0xF2, 0x4F, 0x35, 0x1F, 0x76, 0x2A, 0x17, + 0xB5, 0x00, 0x84, 0xA3, 0x5A, 0xB0, 0xC0, 0x26, 0x83, 0x5A, 0x23, 0x64, 0x80, 0x70, 0xCD, 0x90, + 0x67, 0xE6, 0xA5, 0x9B, 0xE8, 0x55, 0xB0, 0x7C, 0x1C, 0x38, 0xB7, 0xD8, 0x55, 0x1C, 0x28, 0x88, + 0xBA, 0x6D, 0x45, 0x18, 0x0B, 0x4A, 0xE3, 0xAD, 0x23, 0xB2, 0x18, 0xA9, 0x96, 0xF5, 0x87, 0xEA, + 0xF9, 0xD3, 0xF7, 0xE0, 0x59, 0x4D, 0xD2, 0xF6, 0xAB, 0xD3, 0x37, 0x63, 0xBE, 0x92, 0x94, 0xD4, + 0x80, 0x57, 0x10, 0x3E, 0xD2, 0x2E, 0x63, 0x2A, 0xF9, 0x24, 0xDB, 0xCC, 0xDE, 0x5E, 0x6D, 0x6E, + 0x7E, 0x6D, 0x90, 0xBE, 0x96, 0xE6, 0x5E, 0xFC, 0xDB, 0x72, 0xAA, 0x80, 0xD0, 0x49, 0xF0, 0xE7, + 0x4D, 0x48, 0xE5, 0xDB, 0x14, 0x9A, 0x82, 0xF7, 0x8E, 0xD4, 0x2C, 0xCF, 0x11, 0x6A, 0xB8, 0xFB, + 0x9F, 0x28, 0xCD, 0x52, 0xC5, 0xDC, 0x91, 0xED, 0x02, 0xAD, 0x85, 0xE3, 0x56, 0xA1, 0xD6, 0xE4, + 0xAA, 0x09, 0xB6, 0xAE, 0x11, 0x5B, 0x2E, 0x60, 0x1E, 0xF3, 0xA3, 0x64, 0xDB, 0x66, 0x5C, 0x73, + 0x3B, 0x86, 0x59, 0x8A, 0xAE, 0x69, 0x27, 0x4D, 0xDB, 0x2A, 0xBE, 0x6A, 0x61, 0xB4, 0x6E, 0x44, + 0xB7, 0x03, 0x55, 0xB1, 0xD7, 0xE0, 0x7B, 0xD9, 0x2E, 0x9F, 0x80, 0xF7, 0xA2, 0x11, 0x4C, 0x6C, + 0xE4, 0x8D, 0x0D, 0xF3, 0x9B, 0xB6, 0x85, 0xE0, 0x9A, 0xD5, 0x0C, 0x71, 0x77, 0x93, 0x6E, 0xD8, + 0xCA, 0x7E, 0xCB, 0x6E, 0xB5, 0xFB, 0x54, 0xDB, 0xF1, 0xB9, 0x09, 0xB7, 0x02, 0x25, 0x32, 0xB1, + 0xE4, 0xFD, 0xC2, 0x48, 0xDE, 0xEF, 0x85, 0x3E, 0x37, 0x0E, 0xF0, 0x27, 0x4A, 0x74, 0xCD, 0xF7, + 0xCF, 0x7A, 0x98, 0xEA, 0x94, 0x2F, 0x39, 0x9A, 0x62, 0x17, 0x20, 0x7E, 0xF0, 0x72, 0x43, 0xA8, + 0xE8, 0x51, 0x37, 0xB3, 0x6B, 0xCC, 0xCC, 0x18, 0x8F, 0xA0, 0xEF, 0x95, 0xDC, 0xE3, 0x5D, 0xD0, + 0xEB, 0xC7, 0x59, 0x9A, 0x7D, 0x88, 0xB8, 0x04, 0x36, 0x25, 0x3D, 0x17, 0x31, 0x2E, 0xFD, 0x1B, + 0x44, 0xF8, 0xC7, 0x1B, 0xEF, 0x07, 0x2B, 0x9C, 0xBA, 0x32, 0xC3, 0x7D, 0x93, 0x57, 0xD5, 0x1C, + 0x21, 0xF9, 0xB3, 0x15, 0x36, 0xF3, 0x20, 0x3E, 0x6C, 0x35, 0xFC, 0xC7, 0x6F, 0xF7, 0x0E, 0xA3, + 0xC1, 0xE8, 0x73, 0x64, 0x1C, 0xFE, 0xF6, 0x70, 0x30, 0x5A, 0x7D, 0x16, 0x04, 0x87, 0x69, 0xF8, + 0x92, 0xCA, 0x13, 0x04, 0xA8, 0x06, 0x10, 0xD7, 0x6B, 0xF1, 0xD3, 0x21, 0x9E, 0x2E, 0x6F, 0xE6, + 0xAB, 0xEB, 0xF4, 0x6A, 0xF5, 0x7E, 0x2E, 0xAE, 0xF1, 0xE7, 0x7A, 0x35, 0x87, 0x27, 0xBC, 0x4A, + 0xAF, 0xAE, 0x56, 0x77, 0xE2, 0x72, 0x1E, 0xAC, 0x68, 0x9F, 0x62, 0xCE, 0x39, 0x6F, 0x28, 0xC7, + 0xCD, 0xFC, 0x4F, 0x2B, 0x00, 0x9D, 0x3E, 0xDE, 0x04, 0xAB, 0x64, 0x31, 0x49, 0xF5, 0xC7, 0x3F, + 0x22, 0x3D, 0xE1, 0x6F, 0xD8, 0x43, 0x07, 0xFC, 0xE9, 0xD3, 0xB9, 0x38, 0x7F, 0xF9, 0x53, 0x38, + 0xFC, 0x69, 0xB2, 0x7F, 0x98, 0x1C, 0xFC, 0x82, 0x0B, 0x68, 0x3E, 0xFF, 0x0C, 0xFD, 0xF8, 0x46, + 0xC4, 0xAC, 0x1E, 0x38, 0x61, 0x76, 0x15, 0xBB, 0x43, 0x08, 0x25, 0x31, 0xA3, 0xCF, 0xF2, 0x3B, + 0xBD, 0x7B, 0x92, 0x23, 0xEF, 0x6B, 0xFB, 0x8E, 0xD4, 0xC1, 0x8F, 0xEF, 0x05, 0x1F, 0x69, 0x97, + 0x62, 0x85, 0x6A, 0xFF, 0xD3, 0x73, 0xB5, 0x03, 0x40, 0xD6, 0xC5, 0x7B, 0x00, 0xF1, 0xF5, 0x65, + 0x47, 0xAA, 0x45, 0x4C, 0x7D, 0x58, 0x1D, 0x4D, 0x34, 0xFA, 0xA1, 0x26, 0x37, 0xBF, 0x52, 0x27, + 0xBB, 0xD4, 0xB7, 0xD9, 0x60, 0x66, 0xED, 0x36, 0x1B, 0xC1, 0xDB, 0x6C, 0x68, 0xA7, 0x08, 0x16, + 0xB8, 0x6C, 0xC2, 0xF6, 0xEE, 0xA8, 0xAF, 0x1E, 0x8E, 0x9D, 0xA3, 0x31, 0x5F, 0xA9, 0xF8, 0x44, + 0x15, 0x1A, 0xA5, 0xAA, 0x0A, 0x34, 0x81, 0xE3, 0x1B, 0x16, 0x3A, 0x4F, 0xB3, 0xB4, 0xF7, 0x2E, + 0xF8, 0x95, 0xED, 0x3A, 0xCB, 0x22, 0x2A, 0x22, 0xF5, 0xC9, 0xF9, 0xF3, 0x97, 0x14, 0x69, 0x5A, + 0x04, 0x32, 0xE2, 0x94, 0xB6, 0x7C, 0xBC, 0x66, 0x82, 0x47, 0xCE, 0x44, 0xDA, 0xAD, 0x71, 0xC8, + 0x57, 0x85, 0xD2, 0xB9, 0xE8, 0xEE, 0xA9, 0x74, 0x82, 0xAE, 0xB3, 0xAA, 0x6D, 0x81, 0xCE, 0xA4, + 0xD5, 0xF4, 0x67, 0xA2, 0xC3, 0xAF, 0x41, 0xF7, 0x89, 0xEC, 0x9D, 0xCC, 0xC0, 0xE8, 0x3F, 0xF7, + 0x82, 0x3A, 0x34, 0x70, 0x5A, 0xA5, 0x85, 0x46, 0xEA, 0x6E, 0x3A, 0xC2, 0x17, 0xC0, 0x84, 0x76, + 0x25, 0x6D, 0x99, 0xD5, 0xDD, 0xD2, 0xD9, 0xCC, 0x13, 0x00, 0x26, 0x26, 0xFE, 0xA7, 0xAF, 0x37, + 0xC0, 0xA8, 0x68, 0x9D, 0xA2, 0xDE, 0xC3, 0xD4, 0x39, 0x05, 0x9F, 0xCE, 0x7E, 0xB5, 0x1F, 0xC5, + 0x10, 0xBD, 0x1C, 0xF1, 0x09, 0x89, 0x62, 0x98, 0xF1, 0x03, 0x76, 0x7B, 0xBA, 0x6B, 0x79, 0xF9, + 0x0D, 0xCD, 0x41, 0x42, 0x06, 0x9F, 0x96, 0x80, 0x68, 0x51, 0x90, 0x18, 0x92, 0x82, 0xB8, 0x04, + 0x87, 0x0C, 0x21, 0xFE, 0xE6, 0xE2, 0xF9, 0x33, 0xD5, 0xA5, 0xAF, 0xEA, 0xC7, 0xFF, 0xF8, 0xFF, + 0x58, 0xFD, 0xF4, 0x53, 0x19, 0x30, 0x9D, 0xCE, 0x67, 0x55, 0x3A, 0xFF, 0xE9, 0xA7, 0xD7, 0xFB, + 0x58, 0xBE, 0xA4, 0x79, 0xBC, 0x75, 0xE9, 0x41, 0x99, 0x90, 0x68, 0xF4, 0x0B, 0x2D, 0x7A, 0xBE, + 0x46, 0x85, 0x18, 0x3A, 0x9E, 0x75, 0xF2, 0xD7, 0x19, 0x2D, 0xF3, 0x9F, 0xA1, 0x94, 0x27, 0x3A, + 0xCA, 0xC9, 0x51, 0xC1, 0x41, 0x30, 0xE6, 0xA4, 0x5A, 0xD8, 0x03, 0x6B, 0xAA, 0xB4, 0x9A, 0xB5, + 0x4E, 0xB8, 0xA9, 0x1D, 0x56, 0xA3, 0xA4, 0x53, 0x5B, 0x64, 0x22, 0xF8, 0xC8, 0x1B, 0x5F, 0xDE, + 0x20, 0x67, 0x0F, 0xC3, 0x21, 0xF4, 0x70, 0xD5, 0x7D, 0x35, 0xF5, 0xB6, 0x72, 0xF5, 0x29, 0x70, + 0x8E, 0x50, 0x37, 0x1F, 0xDD, 0x16, 0x1F, 0x3C, 0xB6, 0x47, 0xC9, 0x3D, 0xA2, 0x68, 0x77, 0xDC, + 0xB6, 0xD1, 0xBE, 0xFD, 0x27, 0xF2, 0xB8, 0xC2, 0xED, 0xC7, 0xFE, 0x80, 0xEF, 0x20, 0x5A, 0xF9, + 0x94, 0x34, 0x95, 0xCE, 0xCE, 0x69, 0x40, 0x3B, 0x23, 0xB5, 0xB0, 0x57, 0x47, 0xFE, 0xB8, 0xF9, + 0xCD, 0x24, 0xE9, 0x03, 0x46, 0x1E, 0x79, 0x48, 0xD0, 0x3A, 0xFC, 0x05, 0x93, 0x78, 0xF2, 0xDD, + 0xC5, 0xB9, 0x3E, 0x22, 0xEC, 0xE2, 0xFC, 0xA5, 0xBA, 0xC5, 0xF9, 0xD5, 0xD3, 0xBF, 0x7E, 0x73, + 0x11, 0x99, 0x33, 0x99, 0x3C, 0x73, 0xF1, 0xF3, 0x57, 0xE7, 0x17, 0x17, 0xE7, 0xCF, 0xED, 0x59, + 0xAF, 0xCF, 0xCE, 0xBE, 0x96, 0xF9, 0x54, 0x86, 0x88, 0xF3, 0xA3, 0xEE, 0xCF, 0x6A, 0x08, 0xB2, + 0x7B, 0x64, 0x31, 0xE3, 0xF7, 0x5F, 0x20, 0x84, 0x72, 0x87, 0x35, 0xB5, 0xD8, 0x53, 0xB8, 0xE8, + 0xED, 0x90, 0xE6, 0x63, 0x5F, 0xBF, 0xEC, 0xC8, 0x73, 0xC0, 0x27, 0x68, 0xE1, 0xD3, 0x21, 0xBE, + 0x75, 0x66, 0xE0, 0x15, 0xA0, 0x32, 0xA8, 0xBF, 0xBF, 0xB7, 0xD8, 0x35, 0x25, 0x5B, 0xF0, 0x0E, + 0xF3, 0x7D, 0x83, 0x95, 0x1A, 0xD7, 0x8E, 0x24, 0x6E, 0xED, 0x1E, 0x1B, 0xA4, 0xC2, 0xB3, 0x45, + 0x22, 0x05, 0x15, 0x7B, 0xBC, 0xD4, 0xD1, 0xC8, 0xA0, 0x08, 0xE5, 0xEC, 0x42, 0x85, 0xA1, 0x2C, + 0xA3, 0x01, 0x67, 0x20, 0x26, 0x01, 0xB4, 0xE5, 0x78, 0xAA, 0x06, 0x86, 0xD8, 0xD9, 0x07, 0x14, + 0x5D, 0x4C, 0xA0, 0x59, 0x75, 0x26, 0x7F, 0x09, 0xBA, 0x82, 0x46, 0x19, 0x24, 0x6C, 0x55, 0x28, + 0x88, 0x81, 0x4F, 0xA8, 0xC1, 0x84, 0x16, 0x95, 0x32, 0x37, 0x1C, 0xFE, 0x83, 0xF5, 0xD1, 0xE1, + 0x4F, 0x77, 0x07, 0x23, 0x62, 0x74, 0xB8, 0x23, 0x02, 0xA5, 0xA4, 0x70, 0x6F, 0x19, 0x3F, 0x83, + 0x87, 0xCA, 0x88, 0x19, 0xBA, 0x8A, 0x7E, 0x21, 0x13, 0xBA, 0xCC, 0x7F, 0x0A, 0xFC, 0xC5, 0xE8, + 0x67, 0xF2, 0x67, 0x22, 0xE8, 0x17, 0xA0, 0xA6, 0x1F, 0x71, 0x43, 0x7F, 0xA7, 0x05, 0xFF, 0x3D, + 0xE6, 0xBF, 0x7F, 0xE0, 0xBF, 0x7F, 0xE4, 0xBF, 0x7F, 0xE2, 0xBF, 0x7F, 0xE6, 0xBF, 0x7F, 0xA1, + 0xBF, 0x29, 0xFF, 0xB9, 0xB9, 0x8E, 0x86, 0x52, 0x96, 0xA0, 0xBF, 0x80, 0x30, 0x53, 0x9E, 0xCA, + 0x76, 0x43, 0x1F, 0x3A, 0xAF, 0x4F, 0xA1, 0xC7, 0x35, 0x13, 0x5C, 0x52, 0xF6, 0x61, 0xCE, 0x7F, + 0x0A, 0xEE, 0x47, 0xC9, 0x7F, 0xFE, 0x55, 0xDD, 0xB5, 0xF7, 0x36, 0x4E, 0x04, 0xF1, 0xFF, 0xF9, + 0x14, 0x21, 0xA0, 0x2A, 0xD6, 0x6D, 0x4B, 0x2B, 0x84, 0x84, 0x1C, 0x7C, 0xD1, 0xD1, 0xF6, 0xE0, + 0x1E, 0x50, 0xB8, 0xB6, 0xC0, 0x51, 0x55, 0x5C, 0xDA, 0x5B, 0xA8, 0x21, 0x97, 0x94, 0xD8, 0xE5, + 0xD5, 0xF8, 0xBB, 0x33, 0x33, 0xFB, 0x98, 0x7D, 0xD9, 0x71, 0xAE, 0x05, 0x84, 0x78, 0x34, 0xDE, + 0x97, 0xD7, 0xB3, 0xB3, 0xB3, 0xB3, 0xB3, 0xBF, 0x9D, 0x79, 0x03, 0x54, 0xA1, 0x1F, 0xD7, 0xD3, + 0x39, 0xFD, 0xBD, 0xB9, 0x50, 0x7F, 0xA8, 0x28, 0xCC, 0x38, 0xD0, 0x65, 0xF0, 0xD7, 0x0D, 0xFD, + 0x0F, 0x8B, 0x36, 0xB1, 0xB7, 0xAF, 0x46, 0x9C, 0x02, 0x0B, 0x7F, 0xFE, 0xE4, 0xE0, 0x30, 0xB7, + 0x86, 0x0D, 0xC3, 0x9E, 0x02, 0x92, 0x0F, 0x0E, 0xBF, 0xCC, 0x59, 0xDB, 0xE4, 0xAC, 0xE3, 0xCF, + 0x8F, 0xBE, 0xCD, 0xC9, 0xEC, 0x10, 0x25, 0x7F, 0xA9, 0xD2, 0xFD, 0xF2, 0x4F, 0xBE, 0x3C, 0x3E, + 0x7C, 0x71, 0x72, 0x78, 0x90, 0xE3, 0x05, 0x51, 0xB9, 0x84, 0xCD, 0xA4, 0x97, 0xBD, 0xFF, 0xFC, + 0xC9, 0xFE, 0xB3, 0xDC, 0xAA, 0xE0, 0x9C, 0xF1, 0xF8, 0x68, 0xFF, 0xF4, 0xF8, 0x09, 0x34, 0xA9, + 0xB7, 0x78, 0x71, 0xE6, 0xD1, 0xE9, 0x89, 0xCE, 0x5D, 0xDC, 0xD4, 0x5E, 0xF6, 0x17, 0x47, 0xA7, + 0xC7, 0x87, 0x80, 0xE3, 0x38, 0x7C, 0x91, 0x2B, 0x44, 0x91, 0xC4, 0xDD, 0x4C, 0x5C, 0xE4, 0xF9, + 0xE1, 0xA3, 0x6F, 0x0E, 0x75, 0x11, 0x90, 0x44, 0xBF, 0xB9, 0x44, 0xB0, 0xD6, 0xD5, 0xC7, 0x5D, + 0xD6, 0xD5, 0x8D, 0xFD, 0x38, 0xE9, 0xD6, 0x37, 0x70, 0xE3, 0x14, 0xD8, 0x6F, 0xCB, 0xEA, 0x50, + 0xDD, 0x92, 0x67, 0x43, 0x63, 0x5D, 0xBE, 0x91, 0x40, 0x82, 0xC2, 0x3C, 0x93, 0x24, 0x38, 0x46, + 0x37, 0x9F, 0x7C, 0x48, 0x31, 0x05, 0xA9, 0xF8, 0x9B, 0x3C, 0x51, 0xC2, 0xA2, 0xB8, 0x6D, 0xDE, + 0xD6, 0xEF, 0x19, 0xF4, 0xDD, 0x2D, 0x5D, 0xC9, 0xDE, 0xAE, 0xCB, 0xDE, 0xEF, 0xC0, 0x97, 0x1A, + 0x9A, 0xBB, 0x25, 0x68, 0xB7, 0xCD, 0xD5, 0x4F, 0xD7, 0x7B, 0x3E, 0xFB, 0x55, 0x36, 0x92, 0x68, + 0x03, 0x49, 0x31, 0xB1, 0xD0, 0xC6, 0xD3, 0x96, 0xB9, 0xA7, 0x8D, 0xCC, 0x3A, 0x21, 0x55, 0x24, + 0x48, 0x50, 0x15, 0x0C, 0xD4, 0x30, 0xC8, 0xCC, 0x20, 0x89, 0xCF, 0x39, 0x22, 0xC3, 0xF9, 0xD1, + 0xFC, 0x00, 0xD8, 0xED, 0x27, 0xB6, 0x50, 0xA2, 0x4A, 0x29, 0x83, 0x31, 0xDA, 0xB9, 0x54, 0x07, + 0xC9, 0xE9, 0x74, 0x01, 0xC9, 0x25, 0x6D, 0x91, 0x1F, 0xB9, 0x99, 0xB0, 0x50, 0x41, 0x06, 0x71, + 0x3B, 0xAD, 0x8A, 0xC0, 0xA1, 0x39, 0x24, 0x10, 0x6F, 0x9B, 0x04, 0x72, 0xEB, 0x6A, 0x3B, 0x0D, + 0x6F, 0x3F, 0x29, 0xAF, 0x2D, 0x6C, 0x7B, 0xBD, 0x67, 0x25, 0x15, 0x7E, 0x8B, 0xEA, 0xBA, 0xED, + 0xAA, 0x0D, 0x2F, 0x25, 0xBB, 0xEF, 0xD7, 0x9B, 0x6E, 0x36, 0xAF, 0xD1, 0x9D, 0xC9, 0x13, 0xC5, + 0xB0, 0x23, 0x8F, 0x7D, 0x93, 0xF0, 0x41, 0xBE, 0x6E, 0x6C, 0x4E, 0x89, 0x43, 0x3C, 0xA5, 0x08, + 0xD0, 0x7F, 0xFA, 0x16, 0x28, 0xB3, 0xEB, 0xD6, 0x96, 0xF9, 0x65, 0xB4, 0xDB, 0x9E, 0x9E, 0x82, + 0x7C, 0x83, 0x9F, 0xEB, 0xD4, 0x4E, 0xA1, 0x32, 0x60, 0xC6, 0x77, 0x40, 0x37, 0x1C, 0x51, 0xA0, + 0xC5, 0xC0, 0x57, 0xF0, 0xE9, 0x95, 0x1C, 0xDC, 0xC0, 0x7F, 0xD8, 0xD4, 0x00, 0xB6, 0x23, 0xFA, + 0x20, 0x60, 0xA0, 0x5B, 0xA8, 0xF4, 0x2D, 0x68, 0x6A, 0x56, 0x0D, 0x2F, 0x86, 0xD5, 0xA2, 0x91, + 0x59, 0xAD, 0x42, 0x16, 0x0C, 0x5D, 0x5D, 0xA5, 0xED, 0xA0, 0x09, 0x5F, 0x3D, 0x34, 0xB3, 0x76, + 0x50, 0x62, 0xE3, 0x41, 0x6C, 0x84, 0x66, 0x9B, 0xD3, 0x04, 0xA7, 0x6B, 0x55, 0x6E, 0x8E, 0x89, + 0xAE, 0x9E, 0x3E, 0xFC, 0x65, 0x7E, 0x09, 0x9A, 0xCB, 0xCB, 0x96, 0x1C, 0xFA, 0xD4, 0xD4, 0x31, + 0xDD, 0xBB, 0x73, 0xFF, 0xCB, 0xAA, 0x22, 0xC5, 0xA9, 0x18, 0xA1, 0x6C, 0x14, 0x7D, 0x1A, 0x8A, + 0x15, 0x10, 0x99, 0xBE, 0x49, 0x9B, 0xD4, 0x84, 0x45, 0x8F, 0x83, 0x0D, 0x34, 0x00, 0x40, 0x82, + 0x7C, 0x7D, 0xF1, 0x27, 0x57, 0x80, 0x72, 0x7A, 0x10, 0xC2, 0xA3, 0x19, 0xAB, 0xFF, 0x6D, 0x6D, + 0x55, 0xE9, 0x9B, 0x2F, 0x63, 0xB3, 0x7B, 0x5A, 0xEB, 0xC2, 0xC4, 0x28, 0x56, 0x93, 0x64, 0x32, + 0x50, 0x17, 0xD6, 0x7C, 0x65, 0x4B, 0xAB, 0x44, 0x40, 0xE0, 0x74, 0x0D, 0x31, 0x65, 0x19, 0x0E, + 0xDF, 0x09, 0x9B, 0x2F, 0xA2, 0xDB, 0xD2, 0x4C, 0x52, 0xE8, 0x25, 0x27, 0x93, 0x2E, 0x36, 0x9A, + 0xEA, 0xFE, 0xDE, 0xB2, 0xEE, 0x37, 0x0B, 0x1C, 0xE3, 0xBC, 0x40, 0x72, 0x8C, 0xAA, 0x84, 0xFF, + 0xA7, 0x47, 0x27, 0x8F, 0x7E, 0x78, 0x76, 0xF8, 0x52, 0xCD, 0x78, 0xF1, 0xF6, 0x8C, 0x03, 0xF3, + 0x14, 0x81, 0x15, 0x33, 0x0F, 0x6F, 0x09, 0x2D, 0x6E, 0xC8, 0xDD, 0x46, 0xED, 0x08, 0xDC, 0x82, + 0x4D, 0x92, 0x1E, 0xD6, 0xF2, 0xC0, 0x31, 0x60, 0xF0, 0x8A, 0x4A, 0xA4, 0xDD, 0x11, 0x4E, 0xA1, + 0xF1, 0x2A, 0x75, 0x90, 0x65, 0x07, 0xFE, 0x72, 0xED, 0xC0, 0x3B, 0xBA, 0xF0, 0xA4, 0x2D, 0x03, + 0x3A, 0xD8, 0x96, 0x35, 0xBE, 0x8C, 0x79, 0x0F, 0xF6, 0xE7, 0x97, 0xC6, 0x4D, 0xD1, 0x00, 0xB1, + 0xCB, 0xFF, 0xA0, 0x2B, 0xB6, 0xDB, 0x94, 0x23, 0x42, 0xB6, 0x3A, 0x5E, 0x15, 0x66, 0x4C, 0x3B, + 0x90, 0x44, 0xE3, 0xD6, 0x63, 0x2A, 0x23, 0xD9, 0x42, 0xB5, 0x66, 0x1C, 0x26, 0x90, 0xC4, 0xDA, + 0x94, 0x45, 0x48, 0x65, 0x45, 0xEA, 0xDC, 0xD4, 0x43, 0x15, 0x7B, 0x24, 0xBD, 0xAA, 0x35, 0x76, + 0x29, 0x11, 0x57, 0xF6, 0xD0, 0x10, 0xC5, 0xB4, 0xCB, 0x35, 0x81, 0x30, 0x4E, 0x89, 0x2C, 0x12, + 0x78, 0x1B, 0xF6, 0x11, 0xF5, 0xF3, 0x76, 0x60, 0x54, 0xEB, 0x81, 0xE0, 0x3F, 0x3A, 0xE6, 0x9D, + 0xEE, 0xF7, 0xD2, 0x4A, 0xCC, 0x5E, 0x32, 0x93, 0x74, 0xF7, 0xB6, 0x4C, 0x1A, 0x5C, 0xE7, 0x16, + 0xC4, 0xDD, 0x38, 0x89, 0x32, 0xD2, 0x1A, 0xD3, 0x6A, 0xA5, 0x89, 0xF6, 0x6E, 0x11, 0x31, 0x1A, + 0xB0, 0x44, 0xA8, 0x34, 0xA0, 0x02, 0x33, 0x87, 0x51, 0x55, 0x33, 0x73, 0xED, 0x89, 0x67, 0xB4, + 0xAE, 0x6C, 0x2C, 0xC9, 0xD4, 0x5E, 0x2C, 0xD4, 0x59, 0x46, 0x69, 0xA5, 0x25, 0x56, 0xEB, 0x33, + 0x97, 0x7F, 0x65, 0x96, 0xDA, 0x22, 0xB0, 0xC7, 0x4A, 0xED, 0x49, 0xAD, 0x8F, 0x53, 0x4A, 0x56, + 0x4F, 0x3C, 0x35, 0xDD, 0x18, 0x82, 0x99, 0xFF, 0x61, 0x8B, 0x8B, 0x38, 0xB3, 0x60, 0x2E, 0xC0, + 0x04, 0x62, 0x79, 0xAF, 0xEB, 0x9A, 0xE7, 0x71, 0xBF, 0x3B, 0xAA, 0xA6, 0x16, 0x5B, 0x06, 0x7D, + 0x90, 0xA4, 0xB1, 0xC8, 0xF0, 0xC6, 0xA5, 0xB6, 0x0C, 0x0D, 0xCE, 0x5A, 0x6C, 0x72, 0xC3, 0x2B, + 0x3C, 0x1F, 0x67, 0xA5, 0x27, 0xB0, 0x51, 0x08, 0x54, 0x8A, 0xAD, 0xE5, 0x1E, 0xAD, 0xF8, 0xC6, + 0x1A, 0xA4, 0x78, 0x48, 0x0A, 0x21, 0xA3, 0x69, 0xAB, 0x19, 0x58, 0xE8, 0xD9, 0xDB, 0x44, 0xAF, + 0xB0, 0x9B, 0x4D, 0x33, 0x42, 0x86, 0x66, 0x74, 0x25, 0x7D, 0x84, 0x6E, 0x79, 0xA5, 0xF2, 0x06, + 0xE3, 0x5F, 0x01, 0x41, 0xB3, 0xCF, 0x44, 0x3A, 0x27, 0xB4, 0x58, 0x17, 0xB8, 0xC7, 0x21, 0x1A, + 0xED, 0x11, 0xBD, 0xD5, 0x56, 0x22, 0x2C, 0x1B, 0xC8, 0xF7, 0x87, 0x79, 0x7B, 0x21, 0xDD, 0x27, + 0xC8, 0x6D, 0x79, 0x91, 0x9F, 0x64, 0x6C, 0x39, 0xE4, 0xF0, 0x17, 0x0C, 0xEF, 0xD2, 0xD7, 0x9E, + 0xAC, 0x69, 0xC7, 0x4F, 0x66, 0x0B, 0x10, 0x50, 0xCA, 0xED, 0xA6, 0x8C, 0x3A, 0x95, 0x35, 0x4C, + 0xD5, 0xDB, 0x99, 0x8C, 0x51, 0x8B, 0x3F, 0x79, 0x2A, 0x9E, 0x39, 0xC7, 0x5B, 0xC0, 0xEC, 0x2B, + 0xE7, 0x80, 0xB1, 0x24, 0xF3, 0x8B, 0xC3, 0x48, 0x08, 0xE5, 0x59, 0xBB, 0x60, 0x53, 0xA5, 0x49, + 0x9C, 0x44, 0x1A, 0x5A, 0xA8, 0xFB, 0xC6, 0xC5, 0xE0, 0x9B, 0xF4, 0x74, 0x63, 0xED, 0x8B, 0x8F, + 0x58, 0xC9, 0x9A, 0xA6, 0x56, 0xA3, 0xC9, 0x10, 0xC6, 0x43, 0xD9, 0x1B, 0x4D, 0x82, 0x76, 0x0B, + 0x5F, 0x37, 0xDD, 0x7B, 0x49, 0xD7, 0x5B, 0x43, 0xAB, 0x9E, 0xA6, 0x3F, 0x1A, 0x46, 0x46, 0xAE, + 0x56, 0x2F, 0x90, 0x52, 0xA8, 0x8B, 0xEB, 0xA6, 0x54, 0x4B, 0x70, 0x4A, 0x82, 0xC2, 0x51, 0xD9, + 0xCE, 0xC3, 0x86, 0xA2, 0xD2, 0xAC, 0x23, 0x99, 0x0E, 0x69, 0x2D, 0x09, 0x86, 0x11, 0xB4, 0xC6, + 0x64, 0xFB, 0x42, 0xE2, 0x6C, 0xF8, 0x5F, 0xF9, 0x65, 0xE4, 0x6D, 0xBD, 0xE7, 0x98, 0x31, 0xF6, + 0xC2, 0x48, 0xF1, 0xFE, 0x1D, 0x0F, 0x8A, 0xB1, 0x09, 0xD5, 0xE7, 0x8E, 0x38, 0xFF, 0xAD, 0xDC, + 0x31, 0x8A, 0x7B, 0xF3, 0x01, 0x09, 0xE6, 0x68, 0xAF, 0x9E, 0xA6, 0x4A, 0xFE, 0x6A, 0xE7, 0xFD, + 0xDB, 0xE4, 0xBE, 0xAB, 0x51, 0x16, 0xEC, 0x57, 0x4E, 0xBF, 0xE7, 0xFB, 0x57, 0x60, 0x9C, 0x8D, + 0xC3, 0x5A, 0x07, 0xC1, 0x25, 0x7E, 0x9C, 0xE7, 0xB5, 0xBD, 0xC6, 0x47, 0xFB, 0x77, 0x45, 0x73, + 0x4B, 0x0B, 0xD5, 0x0C, 0x82, 0xFA, 0xCE, 0x85, 0x17, 0x16, 0x25, 0x27, 0xBC, 0x43, 0x1C, 0xEB, + 0x07, 0xC5, 0x9C, 0x7D, 0x02, 0x81, 0xD7, 0xAB, 0x6D, 0xE3, 0xC9, 0x12, 0xB1, 0x39, 0xF2, 0x1E, + 0x1D, 0x4F, 0xCA, 0x6E, 0xC7, 0x93, 0xF1, 0x4E, 0x0C, 0xB9, 0x2C, 0xB5, 0xFE, 0x84, 0x7B, 0x0D, + 0x3E, 0xEC, 0xDA, 0x1E, 0x3E, 0xA0, 0x0A, 0x09, 0xD1, 0xA2, 0x20, 0x48, 0x29, 0x69, 0x33, 0xF8, + 0x4B, 0x9E, 0xD5, 0xBE, 0xA7, 0xDC, 0xF3, 0x26, 0x30, 0xE9, 0x79, 0x60, 0x12, 0xA3, 0xB8, 0xF0, + 0xEE, 0xC2, 0xDF, 0x0C, 0xC0, 0xFA, 0xC4, 0xFE, 0xF2, 0xEA, 0x0C, 0xF7, 0x06, 0xFD, 0x74, 0x1C, + 0xB2, 0x02, 0xFB, 0xEB, 0x80, 0x39, 0xA8, 0xB0, 0x97, 0x96, 0xAD, 0xC1, 0x4D, 0x79, 0xD0, 0x1F, + 0xE0, 0xCB, 0xDE, 0x4C, 0xE7, 0x37, 0xD3, 0x19, 0x69, 0x6E, 0x3C, 0x2F, 0xD5, 0x49, 0x88, 0x92, + 0x99, 0x2D, 0xEF, 0x73, 0x0C, 0xC4, 0x2D, 0x25, 0xB4, 0xF9, 0x59, 0x94, 0xBD, 0x9B, 0x23, 0x63, + 0x72, 0x67, 0x73, 0x60, 0xB0, 0x1E, 0x27, 0x68, 0x22, 0xBB, 0x3E, 0x9C, 0xED, 0x69, 0x75, 0x1A, + 0x0F, 0x54, 0x76, 0xD4, 0xE6, 0xED, 0x0C, 0xF1, 0x41, 0x93, 0xB5, 0x18, 0xCB, 0x8A, 0x18, 0x14, + 0xE8, 0x83, 0x83, 0xE8, 0xC5, 0xF7, 0x6B, 0x99, 0x0B, 0x3B, 0xEC, 0xCD, 0x1F, 0x82, 0xC7, 0xB9, + 0x09, 0xF6, 0x98, 0x4B, 0x0F, 0xB9, 0x7B, 0x3A, 0x3A, 0x6C, 0xF4, 0xF4, 0x02, 0xDF, 0x48, 0x5A, + 0x1B, 0x68, 0xF8, 0xB7, 0xE1, 0x8B, 0x6E, 0xDD, 0x40, 0xAB, 0x02, 0x42, 0x9A, 0x79, 0xFE, 0x56, + 0x8A, 0xC4, 0x08, 0x54, 0x08, 0xB3, 0x38, 0x01, 0x4F, 0xCA, 0xC4, 0x1D, 0x84, 0x3E, 0x0D, 0x09, + 0x6C, 0x06, 0x3A, 0xF3, 0x2E, 0xFC, 0xED, 0xEA, 0x07, 0xED, 0x2D, 0x66, 0xD3, 0x0B, 0x09, 0xC4, + 0x0F, 0x4B, 0xB2, 0xA2, 0x04, 0x8D, 0xAC, 0x35, 0x7C, 0x51, 0x23, 0xF0, 0xDE, 0x4E, 0x1B, 0x99, + 0x3D, 0xAE, 0x1A, 0x02, 0x2F, 0x69, 0xA6, 0x24, 0x65, 0xA3, 0x97, 0x85, 0x9B, 0x34, 0x61, 0xA0, + 0x47, 0x68, 0xCF, 0x3E, 0x33, 0x07, 0x3C, 0x0A, 0x97, 0x86, 0xE4, 0x9F, 0xA8, 0xA4, 0x61, 0xAE, + 0xA7, 0xDE, 0x39, 0x5D, 0x6A, 0x97, 0x1B, 0xD8, 0xAA, 0x81, 0x7E, 0xF4, 0x03, 0x9A, 0x94, 0xEE, + 0x66, 0x67, 0xE2, 0x3D, 0x15, 0xAA, 0x50, 0x3E, 0xF2, 0x8C, 0xD2, 0xD2, 0xB1, 0x48, 0xA7, 0x8A, + 0x0B, 0xC9, 0xAE, 0x8D, 0x25, 0x39, 0x36, 0x0E, 0x12, 0x08, 0x93, 0x34, 0xE1, 0x66, 0x0A, 0xA0, + 0xA2, 0x69, 0x9C, 0x26, 0x5A, 0xBA, 0x6F, 0xD0, 0x0E, 0xD5, 0xC4, 0xE9, 0x96, 0x6A, 0x31, 0xCB, + 0x4D, 0x7E, 0xD6, 0xA8, 0x49, 0x7D, 0x7F, 0xD4, 0x87, 0xAE, 0x75, 0x91, 0x5F, 0x26, 0x6F, 0x9D, + 0x7A, 0x20, 0x6B, 0x54, 0xE4, 0x5A, 0x37, 0xD8, 0xBD, 0x09, 0x0C, 0x69, 0x3D, 0xE8, 0x8B, 0x12, + 0xA5, 0x93, 0xBE, 0xF0, 0x2B, 0x45, 0x5E, 0x2B, 0xCD, 0x52, 0x0D, 0x66, 0xB9, 0xC9, 0xCF, 0x9A, + 0xF4, 0x77, 0x50, 0x48, 0x5D, 0x2D, 0x4C, 0x06, 0x60, 0x51, 0x49, 0x58, 0x2D, 0xE8, 0xE0, 0x27, + 0x4E, 0x06, 0x24, 0x4E, 0x8C, 0x4A, 0xA2, 0x65, 0x39, 0xD6, 0x2A, 0xD7, 0xC3, 0x81, 0xCD, 0x7E, + 0xC5, 0x03, 0x11, 0xF9, 0x0B, 0xF1, 0xB7, 0x0A, 0x99, 0x85, 0x8E, 0x72, 0x51, 0xE7, 0xAE, 0xE1, + 0x84, 0x81, 0x02, 0x5C, 0x8A, 0x1E, 0x21, 0x01, 0xD2, 0xBE, 0x96, 0x80, 0x79, 0xAC, 0xBB, 0x25, + 0x6B, 0x1E, 0x2E, 0x54, 0x80, 0x15, 0x4E, 0x98, 0x78, 0xE6, 0xA3, 0x7C, 0x3A, 0x72, 0xF2, 0x60, + 0x7D, 0x50, 0x80, 0x17, 0xA7, 0x51, 0x33, 0xC2, 0x23, 0xFD, 0xAB, 0xB8, 0x45, 0x0E, 0xCF, 0xF5, + 0x13, 0xC5, 0x07, 0x33, 0x0F, 0x4D, 0xB2, 0x3E, 0x89, 0x24, 0xA8, 0xAF, 0x7F, 0x15, 0xFA, 0x2F, + 0x28, 0x0B, 0x1A, 0x7A, 0x95, 0x25, 0xAB, 0x5D, 0x2A, 0xC9, 0x08, 0x15, 0xED, 0xEF, 0xC2, 0xFE, + 0xF2, 0x2A, 0xCF, 0x46, 0xE6, 0x70, 0x72, 0x7D, 0xD8, 0x83, 0xDA, 0xDD, 0xF2, 0xD6, 0xD6, 0xFE, + 0x00, 0x7B, 0x5F, 0xE7, 0x49, 0xD4, 0xEE, 0xCE, 0x37, 0xD8, 0xEE, 0x36, 0xEE, 0xFE, 0x29, 0xF6, + 0x04, 0x4F, 0x5E, 0x9B, 0xDD, 0x65, 0x31, 0x63, 0x96, 0x94, 0xC4, 0x92, 0x6E, 0x5E, 0x5B, 0x67, + 0xCF, 0xE4, 0x39, 0x2C, 0x4E, 0x6E, 0x51, 0x48, 0xC1, 0x1E, 0xC3, 0x9F, 0x30, 0xD9, 0x09, 0x6C, + 0x10, 0x5A, 0xBE, 0x6E, 0xBB, 0x4C, 0x9D, 0xB0, 0x90, 0x86, 0xAB, 0x15, 0x49, 0x6B, 0xDA, 0x9D, + 0xD5, 0xC0, 0xA5, 0x9F, 0xCA, 0x6C, 0xAC, 0x6D, 0x19, 0x38, 0x35, 0x35, 0x8A, 0xED, 0xE1, 0x2E, + 0xFC, 0x36, 0xFB, 0xB7, 0x5A, 0x07, 0x0B, 0x60, 0xD6, 0x96, 0x90, 0x18, 0xDB, 0x4E, 0x24, 0xCC, + 0xD8, 0x6E, 0xD5, 0x5E, 0xF7, 0x55, 0x87, 0xBE, 0x94, 0xB0, 0xA9, 0x64, 0x07, 0x2A, 0x30, 0xB2, + 0x4E, 0x40, 0x7A, 0xAD, 0x9B, 0x77, 0xD9, 0xFA, 0x52, 0xFA, 0x7A, 0xEA, 0xA4, 0xC5, 0x89, 0x30, + 0x9F, 0x65, 0x77, 0x46, 0x57, 0x3F, 0xBE, 0x17, 0x74, 0xF5, 0xDB, 0xE0, 0xA6, 0x35, 0x66, 0xFA, + 0x87, 0xD1, 0x63, 0x8B, 0xAF, 0xFD, 0xA6, 0x0D, 0x7A, 0x07, 0xD4, 0x43, 0x81, 0xEB, 0x40, 0xEF, + 0x9E, 0x49, 0x12, 0x38, 0xD0, 0x7D, 0x23, 0x62, 0x2C, 0x55, 0x2C, 0xE8, 0x8A, 0x71, 0x47, 0x1F, + 0x9F, 0xB3, 0xA2, 0x47, 0x1B, 0x09, 0x05, 0x55, 0xA3, 0xC2, 0xC3, 0x16, 0x88, 0x95, 0x7E, 0x67, + 0x0C, 0xB1, 0x8A, 0xCB, 0x04, 0x10, 0xAB, 0xAB, 0x0F, 0xC3, 0xFC, 0x2B, 0x39, 0x7D, 0xAD, 0x20, + 0x56, 0x57, 0x1F, 0x26, 0x1B, 0x40, 0xB1, 0x16, 0x20, 0xB0, 0x1A, 0xF1, 0x5D, 0xF8, 0x8D, 0x48, + 0x5B, 0xD3, 0xF1, 0x2E, 0x68, 0x60, 0x23, 0x5E, 0x46, 0x18, 0x1C, 0xFD, 0xA6, 0x18, 0x83, 0xC3, + 0x59, 0x2E, 0x06, 0x87, 0x93, 0x43, 0x0C, 0x0E, 0x67, 0xA4, 0x31, 0x38, 0x9C, 0xED, 0x62, 0x70, + 0xDC, 0x8C, 0x18, 0x83, 0xC3, 0x99, 0x49, 0x0C, 0x0E, 0x67, 0x77, 0x61, 0x70, 0xB8, 0x48, 0x1A, + 0x83, 0xC3, 0x45, 0x2C, 0x06, 0xE7, 0x6B, 0xC6, 0xE0, 0x3C, 0x96, 0xB7, 0x5D, 0x28, 0x93, 0x67, + 0x1D, 0x28, 0x13, 0xDD, 0x6A, 0x17, 0xCA, 0xE4, 0xE5, 0x7A, 0x94, 0xC9, 0x77, 0xB2, 0xCD, 0xF6, + 0xED, 0x1B, 0x7A, 0xAD, 0x4A, 0x0E, 0x29, 0xA6, 0x68, 0x6C, 0x06, 0x0F, 0x4C, 0xDF, 0xAB, 0x15, + 0x4B, 0x25, 0xC2, 0x1F, 0x44, 0x82, 0x35, 0x7A, 0x89, 0x6B, 0x7C, 0x0E, 0xF8, 0x58, 0x58, 0x0B, + 0x3B, 0x9F, 0x62, 0x84, 0x7D, 0x8A, 0x9B, 0x60, 0x4E, 0x4F, 0x35, 0x90, 0xFD, 0x43, 0x76, 0x73, + 0xB7, 0xEB, 0x69, 0xC3, 0xB9, 0x8A, 0x68, 0x57, 0x16, 0xF1, 0x17, 0xA4, 0xE2, 0x1D, 0xAB, 0x60, + 0xFE, 0x65, 0xCA, 0x2C, 0x9B, 0x89, 0x0D, 0xFA, 0x83, 0x74, 0xA0, 0xDE, 0x94, 0x7D, 0x0C, 0xF7, + 0x77, 0x33, 0xE6, 0xE8, 0x77, 0xAE, 0x35, 0xE6, 0x84, 0x9C, 0xD7, 0x6B, 0xB7, 0xAA, 0xE5, 0x11, + 0x6D, 0x15, 0xDD, 0x0D, 0xB8, 0x4A, 0xBF, 0xEF, 0xF5, 0xFD, 0x9B, 0xFB, 0x5D, 0xDF, 0xEF, 0xB8, + 0x7A, 0x7E, 0xFD, 0xDF, 0xAF, 0x9E, 0x5F, 0xDB, 0xD5, 0xF3, 0x33, 0x0E, 0xAC, 0xBA, 0xB7, 0x2B, + 0x54, 0x5D, 0x0D, 0x51, 0x36, 0xE1, 0x66, 0xD0, 0x99, 0xC3, 0xF7, 0x5C, 0xCC, 0x68, 0xB0, 0xA6, + 0xB0, 0xEE, 0xAC, 0x29, 0x1E, 0xA3, 0xB0, 0xAD, 0xE0, 0x7C, 0xBA, 0xC9, 0xD5, 0x70, 0x75, 0xC3, + 0xD2, 0x38, 0x52, 0x1D, 0x7E, 0x7A, 0x74, 0xF0, 0x32, 0x02, 0x39, 0x99, 0x5B, 0x4B, 0x13, 0x75, + 0xEB, 0x21, 0xF7, 0x32, 0xFB, 0x41, 0x08, 0x7F, 0x30, 0xA6, 0x9B, 0xE2, 0xD5, 0xFB, 0x81, 0x79, + 0x91, 0x3E, 0xA7, 0x19, 0x60, 0xC8, 0xA6, 0x6D, 0xBC, 0x76, 0x23, 0x06, 0x6D, 0x25, 0x66, 0xC0, + 0x20, 0xDB, 0x78, 0xAD, 0xE7, 0x9A, 0xC2, 0xED, 0xB4, 0x15, 0x0C, 0xA2, 0xF2, 0xBC, 0xD2, 0x3D, + 0x50, 0x74, 0xAD, 0xE0, 0xDE, 0x82, 0x4E, 0xC0, 0xE2, 0x9C, 0xC0, 0xFB, 0x37, 0x4A, 0x76, 0x01, + 0x8F, 0xAE, 0x23, 0x8C, 0x62, 0x57, 0xDB, 0xC5, 0x62, 0xEA, 0x89, 0xA1, 0x7A, 0xC4, 0xC5, 0x4C, + 0xFD, 0xAA, 0xAE, 0xFF, 0x74, 0x3D, 0x06, 0x5D, 0x2F, 0x17, 0x97, 0xB2, 0xAA, 0xAC, 0x3C, 0x05, + 0x5C, 0xF2, 0x52, 0x56, 0xE8, 0x1A, 0xC3, 0xCD, 0x5F, 0x87, 0xA9, 0xFC, 0xAC, 0x63, 0xB5, 0xE3, + 0xD7, 0x36, 0xA6, 0x71, 0x9E, 0x10, 0xA9, 0x01, 0x2F, 0x52, 0xA9, 0x3B, 0x6A, 0x9C, 0x27, 0xE6, + 0x0C, 0x22, 0x1F, 0xDA, 0x60, 0x5E, 0x60, 0x1B, 0x25, 0xAE, 0x8D, 0xBC, 0xD3, 0x28, 0x2E, 0x9D, + 0xC8, 0x3C, 0x91, 0x2A, 0xAA, 0x82, 0x5B, 0x70, 0x30, 0x66, 0xD0, 0xFF, 0x63, 0xE3, 0x7C, 0x6C, + 0x94, 0xE5, 0xBB, 0xE3, 0xBE, 0x03, 0xE5, 0x0D, 0x08, 0xB7, 0xE5, 0xA4, 0x02, 0x4D, 0xF5, 0x25, + 0x33, 0x9F, 0xFD, 0xD4, 0x41, 0x91, 0xB4, 0x38, 0x93, 0x45, 0x31, 0x47, 0x1E, 0x5D, 0x16, 0x8B, + 0x09, 0x2F, 0x04, 0x8B, 0x8C, 0xD0, 0xD2, 0x28, 0x22, 0x96, 0x2C, 0x12, 0x97, 0xE9, 0xA0, 0x74, + 0x23, 0x8D, 0x69, 0x23, 0x3C, 0x37, 0x2E, 0xAB, 0x04, 0xE8, 0x36, 0x67, 0xB5, 0x67, 0xA7, 0x67, + 0xE5, 0x39, 0xB4, 0x02, 0xFB, 0xCB, 0xEB, 0x07, 0x95, 0x58, 0xD8, 0x08, 0xF1, 0x04, 0xC7, 0xCE, + 0xDC, 0x5B, 0x50, 0xD9, 0x4E, 0xB5, 0x58, 0xD6, 0x23, 0x9A, 0xA5, 0xF0, 0x08, 0xA7, 0xD5, 0xDB, + 0x12, 0xFE, 0xE7, 0x6F, 0xF2, 0x3D, 0x0A, 0xED, 0xC0, 0x6D, 0x2F, 0x48, 0xC6, 0x42, 0x2E, 0xA5, + 0x38, 0x63, 0xEF, 0x3C, 0x6B, 0xDC, 0x7B, 0xCE, 0x84, 0xE2, 0x48, 0xB3, 0xAE, 0xCF, 0xB4, 0x11, + 0x10, 0x32, 0x30, 0x5C, 0xB0, 0xF0, 0xB4, 0xE1, 0xFB, 0xB4, 0x95, 0xE1, 0xB3, 0x3B, 0xDD, 0x6F, + 0x76, 0x0C, 0x0E, 0xF4, 0x29, 0x90, 0xB0, 0x64, 0x5F, 0x67, 0x74, 0xE0, 0x7B, 0x5B, 0xBE, 0x46, + 0xAF, 0x3F, 0x26, 0x71, 0x5C, 0xA2, 0x37, 0x9E, 0x02, 0xD6, 0x22, 0xB7, 0xF7, 0x26, 0x77, 0xA7, + 0x7C, 0x5D, 0x94, 0xFC, 0x58, 0x0C, 0xDF, 0x1B, 0x3E, 0x28, 0xCD, 0x10, 0xCC, 0xDC, 0x3A, 0xA2, + 0x06, 0xE1, 0x4B, 0xBB, 0x71, 0x97, 0x2B, 0x79, 0xD6, 0xA5, 0x27, 0x8F, 0x9E, 0x28, 0xA9, 0x39, + 0x04, 0xB1, 0x69, 0xE4, 0xCB, 0x23, 0x25, 0xCB, 0x53, 0xF9, 0x95, 0x79, 0x47, 0x13, 0xF3, 0x6E, + 0xFA, 0xA5, 0xB1, 0x57, 0x9E, 0xD5, 0x8A, 0x62, 0x70, 0xBC, 0x99, 0xFE, 0x31, 0xF2, 0x8C, 0x30, + 0x5E, 0x21, 0x11, 0xA1, 0x80, 0x52, 0x4D, 0xB9, 0x91, 0xF2, 0x7A, 0x74, 0x83, 0xBF, 0xDD, 0x0D, + 0xF6, 0x40, 0x15, 0x93, 0x9F, 0xDB, 0x36, 0x77, 0xF4, 0x6C, 0x69, 0xAC, 0xEC, 0xE3, 0xF9, 0x96, + 0x92, 0x11, 0x0F, 0x3C, 0xE9, 0xA2, 0xA6, 0x81, 0x90, 0xAD, 0x22, 0xA0, 0x2C, 0x12, 0xE5, 0x1F, + 0xC8, 0x6D, 0x4A, 0x8D, 0xBE, 0x97, 0x26, 0x72, 0x2C, 0x60, 0x94, 0x36, 0x13, 0x0A, 0xEC, 0x87, + 0x45, 0xC9, 0x5D, 0xF5, 0x26, 0xDF, 0x99, 0xF7, 0xA4, 0x95, 0xA0, 0xED, 0xBD, 0xF3, 0x71, 0xBC, + 0xC8, 0x28, 0x60, 0x86, 0x93, 0x8E, 0x30, 0x9B, 0x9A, 0xD1, 0xDA, 0x71, 0x0D, 0x28, 0xFD, 0x89, + 0x27, 0x03, 0x60, 0xF2, 0x43, 0x5A, 0x90, 0xF2, 0x70, 0x97, 0xE1, 0x35, 0xE9, 0x85, 0xCD, 0x01, + 0x74, 0x93, 0x9D, 0x76, 0xC4, 0x77, 0x0C, 0x65, 0xC1, 0xCD, 0xF1, 0x07, 0x8C, 0xE5, 0xF6, 0xF6, + 0x38, 0x4B, 0x7D, 0x42, 0xF0, 0xF5, 0x12, 0xFB, 0xF3, 0xD0, 0x69, 0xC3, 0x24, 0x5A, 0xBD, 0x2A, + 0xCC, 0x7B, 0xB0, 0x87, 0x37, 0xC0, 0x3F, 0x89, 0x53, 0xB3, 0x88, 0x36, 0xE1, 0xAB, 0x40, 0xCB, + 0xE2, 0x4C, 0x50, 0x6E, 0x12, 0x9F, 0x2B, 0x85, 0xFF, 0x99, 0x8C, 0x92, 0x70, 0xD7, 0x85, 0x04, + 0x94, 0x00, 0x15, 0x95, 0x86, 0x2F, 0x82, 0xAB, 0xD6, 0x54, 0x94, 0xBB, 0x73, 0x41, 0x79, 0x78, + 0xB1, 0xC8, 0xA4, 0xBC, 0xF2, 0x7D, 0x96, 0x96, 0x3B, 0x3F, 0x2F, 0xCA, 0x39, 0xB5, 0x07, 0x3A, + 0x71, 0xF2, 0xD4, 0xC2, 0x53, 0x52, 0x86, 0xD9, 0xC4, 0xDB, 0x89, 0xD8, 0x4C, 0x13, 0xCB, 0x7F, + 0x9E, 0x08, 0xC2, 0x35, 0xCC, 0xA2, 0x0D, 0x85, 0xFA, 0xF4, 0x21, 0x74, 0xA6, 0x2D, 0x27, 0xCB, + 0x47, 0xF3, 0xD6, 0x5A, 0xC6, 0xAF, 0x43, 0x35, 0x9A, 0x0B, 0x0A, 0xA2, 0x29, 0x5C, 0xAD, 0x6B, + 0xE8, 0x68, 0xED, 0x36, 0xC4, 0x30, 0xFA, 0x56, 0x62, 0xDD, 0x2D, 0x52, 0xD2, 0xFC, 0xD3, 0xE2, + 0xBA, 0xB5, 0x57, 0xC2, 0x6F, 0x8D, 0xEB, 0xF2, 0xFB, 0x18, 0xC9, 0xE5, 0xBD, 0xB3, 0xEF, 0x1B, + 0x1A, 0xF8, 0xC7, 0x01, 0xE2, 0xA5, 0x97, 0x41, 0xC3, 0x4C, 0xDE, 0x72, 0x98, 0x74, 0x18, 0x6A, + 0x18, 0xEA, 0x36, 0xAD, 0x67, 0xB8, 0x2B, 0x7B, 0x6A, 0xF8, 0xB9, 0x5F, 0xDC, 0xFB, 0xD4, 0x4E, + 0xC8, 0x16, 0xBC, 0xFB, 0x86, 0xE8, 0xE9, 0x7F, 0xBD, 0x21, 0x8A, 0x7D, 0x04, 0xB8, 0x54, 0x0E, + 0x7C, 0x04, 0xDC, 0x46, 0xE1, 0x18, 0xA1, 0x4C, 0xA1, 0x57, 0x6C, 0xF0, 0xC2, 0xE0, 0xD1, 0x0D, + 0x7B, 0xF3, 0x54, 0x1D, 0x78, 0xA3, 0x57, 0x80, 0xA7, 0x36, 0x1E, 0x56, 0x5D, 0x3A, 0x7B, 0xA1, + 0xF6, 0xAB, 0x48, 0xD3, 0x8B, 0xA1, 0x73, 0xD5, 0xE3, 0x9E, 0x9D, 0x0A, 0x6F, 0x6D, 0xAD, 0xF7, + 0xAF, 0x68, 0x06, 0xD9, 0xA0, 0x7C, 0x49, 0x10, 0x5B, 0x59, 0x55, 0xC5, 0xD7, 0x35, 0xD2, 0xE7, + 0xF2, 0xA9, 0x39, 0x4B, 0x51, 0x15, 0x19, 0x1D, 0x36, 0x3C, 0x7D, 0x4E, 0xA1, 0x18, 0xED, 0xAD, + 0x6B, 0x38, 0x37, 0x3D, 0x0A, 0x92, 0x26, 0xC3, 0xBC, 0xBA, 0x5C, 0x5C, 0xCB, 0xC1, 0xC3, 0xC1, + 0xAC, 0x84, 0xFF, 0xED, 0xE8, 0xEE, 0xE5, 0x43, 0xF3, 0x6B, 0x2C, 0x4D, 0xB8, 0xCC, 0x52, 0xCC, + 0xD1, 0x32, 0x20, 0xCF, 0x24, 0x2F, 0x75, 0x8D, 0x51, 0xAE, 0xE5, 0x84, 0xE7, 0x9B, 0x64, 0xAC, + 0x00, 0xD2, 0x5B, 0x74, 0x46, 0x69, 0x67, 0x0D, 0x7C, 0xAD, 0xEB, 0xAE, 0x54, 0x63, 0x32, 0xE9, + 0xB4, 0x4B, 0xDB, 0x26, 0x16, 0x5B, 0x5B, 0x8B, 0x76, 0x78, 0x75, 0xBC, 0xE0, 0xF0, 0x3B, 0xE7, + 0x7C, 0xB9, 0xC3, 0x77, 0x50, 0xC5, 0x0E, 0xE6, 0x7A, 0x7E, 0x5E, 0x0F, 0x5F, 0xC5, 0x2D, 0xDF, + 0x05, 0xE7, 0x41, 0x93, 0xA0, 0x93, 0xA5, 0x28, 0x5D, 0x7F, 0x3C, 0xCB, 0x2C, 0x07, 0xB9, 0xE4, + 0x2E, 0x8C, 0xA2, 0x14, 0x96, 0x03, 0xAA, 0x62, 0x44, 0x61, 0x39, 0x81, 0x0D, 0x80, 0x16, 0xEC, + 0x29, 0x00, 0x3C, 0x20, 0x1C, 0x05, 0x49, 0x13, 0x96, 0xB8, 0xF0, 0x0E, 0x3B, 0xF6, 0x36, 0x12, + 0x52, 0x0B, 0x97, 0x20, 0x86, 0x10, 0xB1, 0xB6, 0x8B, 0x02, 0x2F, 0xC9, 0xC0, 0xBF, 0x1D, 0xE8, + 0x6D, 0xA1, 0x28, 0xA9, 0xD7, 0x75, 0xEB, 0x4D, 0x74, 0x9F, 0x62, 0x6B, 0x52, 0xD7, 0x2B, 0xA4, + 0x3A, 0xB4, 0xB2, 0x98, 0x8C, 0xAA, 0x48, 0x3A, 0x76, 0xFA, 0x10, 0x5B, 0x0A, 0x49, 0x9E, 0x9E, + 0x88, 0x1A, 0x51, 0xE3, 0x4C, 0x17, 0x60, 0xB2, 0x12, 0xFE, 0xDF, 0x21, 0x7A, 0x2D, 0x1E, 0x9D, + 0x97, 0x68, 0xFE, 0xF6, 0x30, 0x24, 0xB0, 0xAE, 0xE4, 0x8D, 0x09, 0x39, 0xEF, 0x97, 0x1D, 0x6F, + 0x10, 0x24, 0x83, 0x0A, 0xA4, 0xFE, 0x4F, 0xB1, 0x43, 0x35, 0x50, 0x84, 0xCA, 0x14, 0x84, 0x43, + 0xAD, 0x36, 0x18, 0x6B, 0x1E, 0x23, 0xB5, 0xC8, 0x60, 0xE9, 0x8B, 0x1B, 0x97, 0x2D, 0x8D, 0xCB, + 0xEE, 0xC6, 0x11, 0x89, 0xF1, 0x23, 0x6E, 0x93, 0x65, 0xC7, 0x48, 0x42, 0x2B, 0x2D, 0xD7, 0x62, + 0x66, 0x12, 0xB9, 0x4E, 0x06, 0xC1, 0xCB, 0x2B, 0x60, 0xB8, 0xE7, 0x4F, 0xB0, 0x57, 0x95, 0xC3, + 0x83, 0xA3, 0x0A, 0x1E, 0x1D, 0xBA, 0x89, 0x56, 0xFE, 0x09, 0x22, 0x91, 0xB2, 0x80, 0x93, 0x49, + 0x4D, 0x09, 0x03, 0x97, 0x19, 0xB6, 0x8D, 0xB5, 0xAB, 0xB2, 0xA7, 0x0E, 0x21, 0x64, 0x77, 0xC8, + 0x7F, 0xA0, 0x54, 0x03, 0x4C, 0x3F, 0x1F, 0xDD, 0x79, 0xB1, 0xAE, 0xCB, 0xD6, 0xC5, 0xFA, 0x5F, + 0x5B, 0xAA, 0x13, 0xDE, 0x92, 0x80, 0x91, 0x3A, 0xFD, 0x24, 0x41, 0x3E, 0x68, 0xC7, 0x83, 0x28, + 0xFD, 0xBA, 0x9C, 0xCD, 0x92, 0x19, 0xB8, 0x52, 0x45, 0xCE, 0x42, 0x7B, 0x39, 0x50, 0xAA, 0x5B, + 0x1D, 0x28, 0xAD, 0x56, 0x5D, 0xD4, 0x33, 0x80, 0x1B, 0xF2, 0x1F, 0x54, 0x97, 0x76, 0x72, 0x97, + 0x69, 0xEF, 0x1F, 0x68, 0xED, 0xC2, 0x75, 0xCB, 0x49, 0x22, 0x6C, 0x82, 0x35, 0xD0, 0x36, 0xA2, + 0x2C, 0x7D, 0xBF, 0x10, 0xB6, 0x0E, 0xFC, 0xD6, 0x85, 0x3F, 0x92, 0x1F, 0x1A, 0xF3, 0xEC, 0xBC, + 0xDC, 0xC0, 0x3C, 0xDB, 0x6D, 0x56, 0xB5, 0x70, 0x18, 0xD7, 0x5A, 0x09, 0x20, 0x90, 0x2F, 0xF0, + 0x60, 0x8D, 0x78, 0x6E, 0x4A, 0x34, 0x05, 0xCC, 0x06, 0x67, 0x3E, 0xD3, 0xCE, 0xC1, 0xFC, 0xFC, + 0xBE, 0x77, 0xBB, 0xFD, 0x73, 0x31, 0x59, 0x76, 0x9A, 0x2A, 0xCB, 0xB2, 0xEB, 0xFA, 0xF7, 0xB4, + 0xAA, 0xAD, 0xD6, 0xB5, 0x7E, 0x95, 0xA7, 0xE2, 0x1D, 0xAE, 0x38, 0x1D, 0xB8, 0x51, 0xD7, 0xA5, + 0x4D, 0xB7, 0xF9, 0x5E, 0xA1, 0xCB, 0x12, 0x42, 0xFB, 0xAA, 0xA4, 0x62, 0x9B, 0x07, 0x4A, 0xC0, + 0x09, 0xDB, 0xDB, 0x43, 0x63, 0xF8, 0x5E, 0xD3, 0x44, 0x5F, 0xB7, 0x9A, 0x3D, 0x34, 0x0C, 0xA4, + 0xA9, 0x6D, 0xED, 0xCD, 0xF4, 0xCF, 0x0B, 0x79, 0x7C, 0x79, 0x25, 0x5F, 0xDF, 0xCC, 0x54, 0x50, + 0xEB, 0xC8, 0x6F, 0x62, 0x9A, 0xA8, 0xEC, 0x55, 0x73, 0xBD, 0x9E, 0xAB, 0xFA, 0xD6, 0xC7, 0xE1, + 0xE6, 0xDA, 0x01, 0xEF, 0xE9, 0x1F, 0x73, 0x03, 0x3A, 0x03, 0xF5, 0xF4, 0xE0, 0xF6, 0x72, 0x30, + 0xA9, 0x7A, 0xD8, 0x9B, 0x48, 0xAE, 0x21, 0x36, 0xC9, 0xB2, 0xFD, 0xA9, 0xD7, 0x93, 0x00, 0xBE, + 0x0D, 0xB7, 0xD3, 0x2D, 0x65, 0x59, 0xDE, 0x8B, 0xD9, 0x16, 0xA1, 0x53, 0x44, 0x96, 0x1E, 0xC0, + 0xA9, 0x26, 0xE6, 0xB8, 0x10, 0x7A, 0x6E, 0xE4, 0x28, 0xF0, 0x4B, 0xAB, 0x6C, 0x5B, 0xAD, 0xBA, + 0x24, 0x9B, 0xE6, 0x96, 0x76, 0xDC, 0xA0, 0x87, 0x78, 0xF6, 0x5E, 0x4E, 0x82, 0x3B, 0xC3, 0xD3, + 0xD7, 0xC5, 0xDC, 0x69, 0x51, 0x8B, 0xE9, 0xDF, 0x4B, 0x3C, 0xF1, 0x54, 0xE0, 0x49, 0x58, 0xB5, + 0xA7, 0x95, 0x74, 0xAE, 0x46, 0xE6, 0xFC, 0x0C, 0x2F, 0x1A, 0xE6, 0xED, 0x82, 0x59, 0x8E, 0x2F, + 0x60, 0x89, 0xFA, 0x65, 0x8C, 0x15, 0x2C, 0x28, 0x36, 0xE7, 0x27, 0xAA, 0xDE, 0x29, 0xBA, 0x65, + 0x03, 0xCB, 0xBE, 0x0C, 0x1C, 0x35, 0xC4, 0xDC, 0x65, 0xF7, 0x92, 0x01, 0x86, 0x73, 0x1C, 0x86, + 0x1E, 0x29, 0xDB, 0x62, 0x8E, 0x90, 0xCF, 0xB7, 0x76, 0x51, 0x01, 0x4B, 0xC7, 0xE6, 0x7E, 0x32, + 0x89, 0x57, 0x92, 0x3E, 0x32, 0x29, 0xA7, 0xB7, 0x7F, 0x4C, 0x4B, 0x7A, 0x6E, 0xD4, 0x82, 0xE1, + 0xA3, 0xE1, 0x7B, 0x77, 0xB7, 0xB3, 0x95, 0x9B, 0xBA, 0x57, 0x23, 0x7B, 0xE9, 0x46, 0xCC, 0x20, + 0xF6, 0x6A, 0x63, 0xB7, 0xAB, 0x8D, 0x0D, 0x3A, 0xD2, 0xF8, 0x83, 0xDD, 0xED, 0x71, 0x23, 0x56, + 0x1C, 0xEE, 0xAA, 0xAB, 0xCE, 0xCB, 0xFF, 0xCA, 0xB0, 0xE4, 0xF8, 0xF7, 0xD4, 0x3D, 0xFD, 0x61, + 0x34, 0x87, 0x03, 0x9F, 0xDB, 0x47, 0x33, 0xB9, 0xAC, 0xF3, 0x6F, 0xC5, 0xA7, 0x37, 0x75, 0x0D, + 0x3A, 0xD9, 0xAF, 0x62, 0x7F, 0xBA, 0x24, 0xB4, 0x51, 0xFE, 0xBD, 0xD8, 0x07, 0x03, 0xD2, 0xF4, + 0xBA, 0x92, 0xB9, 0xAC, 0xC5, 0x81, 0xDE, 0x09, 0xE4, 0x8F, 0xA4, 0xA0, 0x0B, 0x0B, 0xF9, 0x81, + 0x14, 0x47, 0xC6, 0x2B, 0x65, 0xFE, 0xA5, 0x14, 0x5F, 0x29, 0x7C, 0x46, 0xFE, 0xB5, 0x14, 0xEA, + 0x74, 0xE1, 0xF8, 0xFA, 0xCF, 0xFC, 0xA9, 0x14, 0x27, 0xD3, 0x8B, 0xBC, 0x2E, 0xC5, 0x09, 0x0E, + 0x4F, 0x3E, 0x87, 0x1F, 0x0A, 0x83, 0x96, 0x3F, 0x96, 0xD8, 0x9F, 0xF1, 0x3B, 0x1F, 0x7C, 0xF0, + 0xDE, 0x00, 0xC6, 0x70, 0x79, 0x29, 0xBF, 0x98, 0x92, 0x2B, 0xAC, 0xD3, 0x17, 0xCF, 0x8B, 0x0B, + 0xE3, 0x15, 0x68, 0xE7, 0xE2, 0x06, 0xB1, 0x8B, 0x3B, 0x6F, 0x80, 0x53, 0x7E, 0xAE, 0xD0, 0xE4, + 0xFC, 0x37, 0x2B, 0x75, 0xB8, 0x40, 0x97, 0x33, 0x01, 0x00 +}; //bootsrap.bundle.min.jss + +//Content of bootstrap.min.css with gzip compression +static const uint8_t bootstrap_min_css[] PROGMEM = { + 0x1F, 0x8B, 0x08, 0x08, 0xDF, 0x38, 0x1D, 0x61, 0x04, 0x00, 0x62, 0x6F, 0x6F, 0x74, 0x73, 0x74, + 0x72, 0x61, 0x70, 0x2E, 0x6D, 0x69, 0x6E, 0x2E, 0x63, 0x73, 0x73, 0x00, 0xEC, 0x5D, 0xD9, 0xAE, + 0xF3, 0x34, 0x10, 0xBE, 0xE7, 0x29, 0x0A, 0x08, 0x41, 0x21, 0x29, 0x4D, 0xD2, 0xFD, 0x08, 0xC4, + 0x2E, 0x90, 0x80, 0x0B, 0x16, 0x09, 0x84, 0xB8, 0x48, 0x13, 0xB7, 0x0D, 0x24, 0x6D, 0x49, 0x5A, + 0x68, 0xA9, 0x0E, 0xE2, 0x21, 0x78, 0x00, 0x9E, 0x85, 0x47, 0xE1, 0x49, 0x18, 0x2F, 0x89, 0xF7, + 0xC4, 0xE9, 0xCF, 0x0E, 0xFF, 0x72, 0x4E, 0x6B, 0x8F, 0xC7, 0x33, 0xE3, 0x99, 0xCF, 0x5B, 0x62, + 0xBF, 0x96, 0xEC, 0xE2, 0xB2, 0x42, 0xA7, 0xC1, 0x33, 0x9F, 0x7E, 0xF2, 0x8E, 0xBF, 0x78, 0xE6, + 0xE1, 0xE5, 0x17, 0x9F, 0x7E, 0x6A, 0xF0, 0xE2, 0xE0, 0x8D, 0xC3, 0xE1, 0x54, 0x9D, 0xCA, 0xF8, + 0x38, 0xF8, 0x76, 0x3A, 0x1A, 0x8F, 0xC2, 0xC1, 0x0B, 0xBB, 0xD3, 0xE9, 0x58, 0xAD, 0x5E, 0x7E, + 0x79, 0x8B, 0x4E, 0xEB, 0x3A, 0x73, 0x94, 0x1C, 0x8A, 0x97, 0x87, 0x98, 0xFE, 0xCD, 0xC3, 0xF1, + 0x5A, 0x66, 0xDB, 0xDD, 0x69, 0x10, 0x8E, 0x83, 0xC0, 0x0F, 0xC7, 0x61, 0x30, 0xF8, 0x64, 0x87, + 0x04, 0x3E, 0xAF, 0x9F, 0x4F, 0xBB, 0x43, 0x59, 0x59, 0x89, 0xBF, 0xCB, 0x4E, 0x27, 0x54, 0x7A, + 0x83, 0xF7, 0xF6, 0xC9, 0x08, 0x13, 0xBD, 0x9F, 0x25, 0x68, 0x5F, 0xA1, 0x74, 0x70, 0xDE, 0xA7, + 0xA8, 0x1C, 0x7C, 0xF0, 0xDE, 0x27, 0x82, 0x0C, 0xD9, 0x69, 0x77, 0x5E, 0x93, 0xDA, 0x4F, 0xDF, + 0xAD, 0xAB, 0x97, 0x1B, 0x81, 0x5E, 0x5E, 0xE7, 0x87, 0xF5, 0xCB, 0x45, 0x9C, 0xED, 0x5F, 0x7E, + 0xFF, 0xBD, 0x37, 0xDF, 0xFE, 0xF0, 0xE3, 0xB7, 0xB1, 0x74, 0x2F, 0xAF, 0x4A, 0x20, 0xB8, 0xF9, + 0xFE, 0xBA, 0xF2, 0xD7, 0xF9, 0x19, 0xAD, 0x9E, 0x1D, 0xA7, 0x33, 0xB4, 0x49, 0x1F, 0x48, 0x4A, + 0xB6, 0x4F, 0xB3, 0xED, 0x61, 0xF5, 0xEC, 0x6C, 0x16, 0x8C, 0x37, 0x21, 0x4D, 0x3B, 0x9E, 0xCB, + 0x63, 0x8E, 0x20, 0x6D, 0x33, 0x09, 0x93, 0x80, 0xA5, 0x65, 0xFB, 0xAF, 0x57, 0xCF, 0xA6, 0xB3, + 0x28, 0x5A, 0x4C, 0x68, 0x4A, 0x89, 0x52, 0x48, 0x48, 0xA2, 0xE9, 0x64, 0x4A, 0x13, 0x0E, 0x65, + 0xBC, 0xDF, 0x42, 0xB1, 0x4D, 0x3A, 0x47, 0x01, 0x23, 0xBA, 0xA2, 0x3C, 0x3F, 0x7C, 0x07, 0x69, + 0x9B, 0x24, 0x18, 0xCF, 0x69, 0xDA, 0xB6, 0x44, 0x68, 0xBF, 0x7A, 0x36, 0x58, 0x2E, 0xE6, 0x53, + 0x46, 0x76, 0x42, 0x71, 0xBE, 0x7A, 0x36, 0x1C, 0x27, 0xCB, 0x25, 0x23, 0x4A, 0xAE, 0xF1, 0x1E, + 0x4B, 0x9A, 0xC4, 0x9B, 0x31, 0x4D, 0xF9, 0x6E, 0x97, 0x9D, 0x10, 0xE6, 0xB4, 0xA9, 0xD9, 0xC4, + 0x57, 0x90, 0x31, 0x99, 0x4F, 0xE7, 0x29, 0x4F, 0xF1, 0xD3, 0xB8, 0x04, 0x41, 0xA3, 0x49, 0x14, + 0x4F, 0x58, 0xC1, 0x63, 0x99, 0x15, 0x71, 0x79, 0x95, 0xF5, 0xAE, 0x50, 0x72, 0xD8, 0x03, 0xAD, + 0xC2, 0xA2, 0x3A, 0x27, 0x09, 0xAA, 0xAA, 0x5A, 0xBA, 0xDA, 0x46, 0x9B, 0x83, 0x22, 0x4B, 0x5C, + 0xEE, 0xB3, 0xFD, 0x56, 0xD6, 0x2B, 0xC5, 0xEA, 0x97, 0xB2, 0x4D, 0x72, 0xDC, 0xD0, 0x40, 0xB6, + 0xD8, 0x2C, 0x37, 0x31, 0x49, 0x62, 0x02, 0x86, 0x41, 0x38, 0x0D, 0x97, 0x34, 0x65, 0x73, 0xD8, + 0x9F, 0xFC, 0x2A, 0xDE, 0x63, 0xA9, 0xCA, 0x6C, 0xB3, 0xAA, 0xAE, 0xD5, 0x09, 0x15, 0xFE, 0x39, + 0xF3, 0xFC, 0xF8, 0x08, 0x2D, 0xE1, 0xD3, 0x04, 0xEF, 0x99, 0x8F, 0xD1, 0xF6, 0x80, 0x06, 0x9F, + 0xBE, 0xF7, 0x8C, 0xF7, 0xD1, 0x61, 0x7D, 0x38, 0x1D, 0xBC, 0x67, 0xDE, 0x45, 0xF9, 0xB7, 0xE8, + 0x94, 0x25, 0xF1, 0xE0, 0x43, 0x74, 0x46, 0xCF, 0x78, 0xAF, 0x97, 0x59, 0x9C, 0x7B, 0xCF, 0x7C, + 0x08, 0x99, 0x83, 0x8F, 0x81, 0xE3, 0x33, 0xDE, 0x33, 0xEF, 0x67, 0x6B, 0x54, 0xC6, 0xA7, 0xEC, + 0xB0, 0x67, 0x29, 0xBC, 0x26, 0xEF, 0x99, 0xD7, 0x31, 0x7F, 0x70, 0xC9, 0xFC, 0x50, 0x0E, 0xDE, + 0x2E, 0x0E, 0x5F, 0x65, 0xCF, 0xF0, 0x5A, 0xF4, 0x84, 0x8F, 0xAF, 0xC5, 0xFA, 0x90, 0x3F, 0x43, + 0xF9, 0x4B, 0xA5, 0x04, 0x45, 0x8A, 0xC3, 0xFE, 0x50, 0x1D, 0xE3, 0x04, 0xAD, 0x3E, 0x7E, 0xE7, + 0x03, 0xF8, 0xEC, 0x7F, 0x84, 0xB6, 0xE7, 0x3C, 0x2E, 0xBD, 0x0F, 0xD0, 0x3E, 0x3F, 0x78, 0x90, + 0x14, 0x27, 0x07, 0xEF, 0xCD, 0xC3, 0xBE, 0x3A, 0xE4, 0x71, 0x25, 0xC9, 0x87, 0xC9, 0x81, 0xFB, + 0x9B, 0x87, 0x73, 0x99, 0xA1, 0x12, 0x54, 0xFA, 0xEE, 0x19, 0xAF, 0x61, 0xD7, 0xB4, 0x71, 0x9A, + 0xA1, 0xFD, 0x69, 0x95, 0x67, 0x7B, 0x14, 0x97, 0xCD, 0xF7, 0x17, 0x82, 0xC5, 0x38, 0x45, 0x5B, + 0x6F, 0x50, 0x6E, 0xD7, 0xF1, 0x0B, 0xE1, 0x74, 0xEA, 0x0D, 0xF8, 0x8F, 0xF1, 0x28, 0x98, 0x0E, + 0x2D, 0x59, 0xC3, 0xE1, 0xE3, 0x8B, 0xDE, 0x6A, 0x15, 0x6F, 0x20, 0xFE, 0xE0, 0xF7, 0x1A, 0x6D, + 0x0E, 0x25, 0xBA, 0xAD, 0x0F, 0x17, 0xBF, 0xCA, 0xBE, 0xC7, 0x6D, 0xBC, 0x3E, 0x94, 0x10, 0x82, + 0x3E, 0xA4, 0x3C, 0xBE, 0x56, 0xA0, 0x34, 0x8B, 0x07, 0x2F, 0x1C, 0x4B, 0xB4, 0x41, 0x25, 0x09, + 0x81, 0x73, 0x82, 0x52, 0x50, 0x19, 0x8B, 0xBF, 0x02, 0x5D, 0x69, 0x0E, 0xDA, 0x27, 0x68, 0x78, + 0xA3, 0x71, 0x57, 0x25, 0xE5, 0x21, 0xCF, 0xFD, 0x35, 0xDA, 0xC5, 0xDF, 0x66, 0x87, 0x72, 0x55, + 0x15, 0x90, 0xBA, 0x7B, 0x7C, 0x5C, 0x1F, 0xD2, 0xEB, 0x0D, 0xFC, 0x72, 0x9B, 0xED, 0x57, 0xE3, + 0x07, 0x62, 0xB9, 0x4D, 0x5C, 0x64, 0xF9, 0x75, 0xF5, 0x6D, 0x5C, 0xBE, 0x60, 0xF2, 0x8B, 0x21, + 0xA1, 0xC2, 0x62, 0xA1, 0x55, 0x50, 0xA2, 0x82, 0x7E, 0xFD, 0x0E, 0x11, 0x17, 0x9B, 0x8C, 0xC7, + 0x0F, 0xD8, 0x24, 0xFE, 0x8E, 0x7E, 0x0F, 0x46, 0xD3, 0x87, 0x04, 0x37, 0x50, 0xE3, 0x6A, 0xEB, + 0x38, 0xF9, 0x7A, 0x5B, 0x1E, 0x00, 0x50, 0x7C, 0x92, 0x41, 0x43, 0x09, 0xCA, 0xAF, 0xBF, 0xCE, + 0x4E, 0x10, 0x81, 0x17, 0xCA, 0xDA, 0x8F, 0xD3, 0xAF, 0xCE, 0x15, 0x30, 0x18, 0x8F, 0x9F, 0xE3, + 0xB9, 0xF1, 0xD1, 0xDF, 0x01, 0x5F, 0xE2, 0xCE, 0xAC, 0xF8, 0x09, 0xC2, 0x1D, 0x1A, 0x06, 0x94, + 0x3D, 0x3D, 0xEE, 0x4A, 0xA6, 0x0A, 0x11, 0x6C, 0x30, 0x66, 0x55, 0x67, 0xFB, 0x1D, 0x08, 0x7E, + 0xD2, 0xAB, 0x4E, 0xCE, 0x25, 0x2E, 0x47, 0x3C, 0xE8, 0x81, 0x1A, 0x18, 0x8C, 0x70, 0x80, 0x66, + 0xCE, 0x4E, 0xD7, 0xD5, 0x28, 0x9C, 0x02, 0x47, 0x30, 0xE7, 0xE9, 0x85, 0x2F, 0xB0, 0x48, 0x5F, + 0x0E, 0x6F, 0xB5, 0x56, 0xC7, 0xCB, 0xE3, 0x68, 0x17, 0x78, 0xA3, 0x5D, 0x08, 0xFF, 0x23, 0xF8, + 0x3F, 0x81, 0xFF, 0x53, 0xF8, 0x3F, 0xF3, 0x20, 0x19, 0x52, 0x21, 0x11, 0xD2, 0x20, 0x69, 0x37, + 0x63, 0x22, 0xF9, 0xA7, 0xC3, 0x11, 0x98, 0xB3, 0x2F, 0x10, 0x36, 0xA7, 0x43, 0xB1, 0x1A, 0x4D, + 0x55, 0x03, 0x4E, 0x35, 0x03, 0x86, 0xB8, 0x2A, 0x60, 0x7B, 0xE3, 0x66, 0x4F, 0xE2, 0x3C, 0x79, + 0x21, 0x18, 0x45, 0x73, 0x5C, 0x7C, 0xF0, 0xD2, 0x00, 0xAC, 0xFC, 0xED, 0x77, 0xC3, 0xC6, 0x2F, + 0x0A, 0xA8, 0xE0, 0xBB, 0x2C, 0x3D, 0xED, 0x56, 0x41, 0x38, 0x1E, 0x1F, 0x2F, 0xC3, 0x9B, 0xC6, + 0x21, 0x24, 0x35, 0x3F, 0x02, 0xEB, 0x10, 0xC4, 0x35, 0xB0, 0x0E, 0x19, 0xEB, 0xD1, 0xB2, 0x9D, + 0xB3, 0xC6, 0x20, 0x64, 0x7C, 0x23, 0xB0, 0x81, 0x81, 0x2F, 0xE3, 0x3A, 0x6B, 0xE7, 0xAA, 0x15, + 0x0F, 0x46, 0xF3, 0x5A, 0xE0, 0x09, 0x58, 0x56, 0x67, 0x1C, 0xCE, 0x6B, 0x81, 0xA3, 0x76, 0xD6, + 0x1A, 0x83, 0xA0, 0x31, 0xC5, 0x14, 0x1A, 0x4C, 0xCA, 0x08, 0x49, 0x0E, 0x69, 0xD5, 0x99, 0x98, + 0x81, 0x53, 0x8F, 0x2D, 0xED, 0x4A, 0x29, 0xE2, 0xF5, 0xBA, 0xFC, 0x22, 0x8D, 0x4F, 0x31, 0xED, + 0x95, 0x32, 0xC8, 0x8F, 0x73, 0xFF, 0x94, 0x9D, 0x72, 0xF4, 0xA5, 0x47, 0x32, 0xE9, 0xE7, 0x9B, + 0xE4, 0xFC, 0x29, 0xF4, 0x08, 0x14, 0x87, 0x56, 0xA4, 0xD3, 0xC5, 0xCE, 0x30, 0x48, 0x81, 0x2D, + 0x4A, 0x1F, 0x3A, 0x09, 0xC0, 0xA1, 0x2B, 0xF0, 0xEB, 0x1D, 0xCA, 0x8F, 0x0F, 0x16, 0xAE, 0x7E, + 0xF5, 0x75, 0x76, 0xF4, 0x71, 0x37, 0xBA, 0x3F, 0xEC, 0xD1, 0x43, 0x6B, 0xEE, 0x63, 0x9C, 0xA6, + 0x25, 0xF4, 0x43, 0x37, 0x5D, 0x3B, 0x06, 0x02, 0xA7, 0x6B, 0x8E, 0x80, 0xB4, 0x2C, 0xE2, 0x5C, + 0x74, 0xDB, 0x3A, 0xE4, 0x1E, 0x0F, 0xB9, 0x77, 0xCE, 0x6F, 0x47, 0xE0, 0x03, 0x18, 0xE6, 0xE7, + 0x68, 0x73, 0xA2, 0x0E, 0x92, 0xE6, 0x1E, 0xC9, 0xEA, 0x34, 0xE2, 0x21, 0x1F, 0x00, 0x21, 0xFC, + 0x3C, 0x03, 0x39, 0xF9, 0x7C, 0xC6, 0x9F, 0x15, 0x89, 0xC6, 0x8F, 0xE9, 0xE9, 0x26, 0x86, 0xD1, + 0x7C, 0x0C, 0x49, 0xE9, 0xCD, 0x14, 0x6D, 0x34, 0x8D, 0xC8, 0x02, 0xE5, 0x60, 0xAC, 0x92, 0x7C, + 0xFD, 0xCD, 0xF9, 0x70, 0x42, 0x0D, 0x0A, 0x0E, 0xC6, 0x03, 0x52, 0xF5, 0xDA, 0x83, 0xE1, 0xCC, + 0x61, 0xBF, 0x95, 0x18, 0x43, 0xC7, 0x03, 0x36, 0x7F, 0x1C, 0x55, 0xA0, 0x71, 0xEE, 0x91, 0x9F, + 0x82, 0x67, 0x8C, 0xE6, 0xF3, 0x29, 0x54, 0x31, 0x60, 0xC8, 0xB6, 0x20, 0x7F, 0x1E, 0x1E, 0x47, + 0xC0, 0xFA, 0x6B, 0x0F, 0xFF, 0xA8, 0x4D, 0x01, 0xE0, 0x02, 0x74, 0x06, 0x2C, 0x4C, 0x36, 0x0B, + 0x14, 0x3D, 0x56, 0x67, 0xA8, 0xFC, 0x7C, 0xBC, 0x1D, 0x0F, 0x55, 0x46, 0x5A, 0xBA, 0x44, 0x39, + 0xB4, 0xCF, 0xB7, 0xE8, 0x41, 0xA8, 0x0B, 0x57, 0x25, 0x19, 0x7D, 0xFC, 0xF0, 0x2D, 0x2A, 0x71, + 0x2F, 0x9C, 0xFB, 0x31, 0x40, 0xE4, 0x7E, 0xB5, 0x8E, 0x2B, 0x84, 0x09, 0x30, 0xBF, 0x1B, 0x33, + 0x82, 0x0F, 0x5E, 0x0D, 0xCA, 0x61, 0xEE, 0x60, 0x74, 0xF8, 0x8A, 0xBF, 0xC5, 0x37, 0x52, 0x3F, + 0x1B, 0x9B, 0xB4, 0x38, 0xDA, 0x63, 0xBC, 0xDA, 0x1D, 0xA0, 0x96, 0x86, 0x3E, 0x9E, 0x2E, 0x92, + 0x18, 0x52, 0x09, 0x4A, 0xEE, 0xA0, 0xDB, 0xF9, 0x72, 0x48, 0x3F, 0x27, 0xD0, 0xAB, 0x56, 0x5F, + 0x0E, 0x3D, 0x7B, 0x16, 0xE7, 0xC4, 0x31, 0x5A, 0xAD, 0x99, 0xBA, 0x61, 0x72, 0x48, 0x91, 0xF7, + 0xF5, 0x3A, 0xF5, 0xA0, 0x5F, 0x83, 0x71, 0x43, 0x71, 0xBC, 0x69, 0x1D, 0x95, 0xDE, 0xEF, 0x4B, + 0xFD, 0x14, 0x58, 0x2A, 0xCD, 0x4A, 0x94, 0x10, 0x9E, 0xF9, 0xA9, 0x7C, 0x38, 0xEF, 0x33, 0xCC, + 0xD5, 0x5F, 0x67, 0x69, 0xB6, 0xC2, 0x3F, 0x7C, 0x2C, 0x4C, 0x99, 0xA5, 0xE8, 0x11, 0x2A, 0xB9, + 0xA5, 0x59, 0x75, 0xCC, 0x61, 0x60, 0x47, 0xDC, 0xE3, 0xA1, 0xC3, 0x49, 0x1F, 0x70, 0xD1, 0x0D, + 0x1E, 0x60, 0xC6, 0xE7, 0xD3, 0x41, 0x6C, 0xA2, 0x05, 0x6E, 0x23, 0xCC, 0x70, 0x80, 0x2B, 0xE3, + 0x8E, 0xD2, 0xA8, 0x2B, 0x2B, 0xFF, 0x1D, 0x74, 0x41, 0xFE, 0xBA, 0x44, 0xF1, 0xD7, 0x2C, 0xA6, + 0x1E, 0x49, 0x31, 0x95, 0x21, 0x2D, 0x55, 0x0F, 0x83, 0x69, 0xA9, 0xEF, 0x60, 0xE0, 0xBD, 0x22, + 0x45, 0x7D, 0xFC, 0xFD, 0x31, 0x7E, 0x95, 0x14, 0x95, 0xF8, 0x3F, 0x82, 0x09, 0x05, 0x07, 0xC4, + 0x78, 0x39, 0x9A, 0x60, 0xF9, 0xB5, 0x0A, 0x78, 0xCF, 0xAC, 0xFA, 0x28, 0xEF, 0xC8, 0xD9, 0x80, + 0x04, 0x0F, 0x7C, 0xCE, 0x15, 0x65, 0x87, 0x2B, 0x18, 0x88, 0x95, 0x8C, 0x95, 0x36, 0x50, 0x43, + 0x74, 0x93, 0x6D, 0xCF, 0xA5, 0x1E, 0x79, 0x59, 0xB1, 0xF5, 0xAA, 0x6F, 0xB7, 0x37, 0xC5, 0x9D, + 0x8B, 0x2C, 0x4D, 0x73, 0xF4, 0x78, 0x8A, 0xD7, 0x39, 0x68, 0x16, 0x1F, 0x29, 0x60, 0x41, 0x93, + 0xAD, 0x68, 0x63, 0x30, 0x91, 0xB0, 0x9C, 0x79, 0x7C, 0xAC, 0xD0, 0xAA, 0xFE, 0xF0, 0xC8, 0x88, + 0x6B, 0xB9, 0x70, 0x4B, 0x32, 0x34, 0xA8, 0x53, 0x38, 0x44, 0xD4, 0xEA, 0xB3, 0x11, 0x39, 0x0D, + 0x06, 0x26, 0x01, 0x06, 0x8D, 0xC7, 0xD3, 0xEE, 0xC6, 0x93, 0x9A, 0xB6, 0x13, 0x92, 0x6A, 0xE4, + 0x2D, 0xE2, 0x53, 0xB2, 0xF3, 0xD9, 0xD8, 0xE4, 0x84, 0x47, 0x5A, 0xDE, 0x29, 0xF5, 0x4E, 0x1B, + 0x18, 0x79, 0x79, 0xA7, 0x1D, 0xFC, 0x43, 0x31, 0x7C, 0x2D, 0x6F, 0x4C, 0x6C, 0xD9, 0x17, 0x68, + 0x22, 0x43, 0x58, 0x18, 0xA3, 0x66, 0x29, 0x4B, 0x62, 0xFD, 0xD9, 0xF8, 0x31, 0x8F, 0xD7, 0x28, + 0xAF, 0x3D, 0x15, 0xCA, 0x11, 0x28, 0x20, 0x0E, 0xFB, 0xB8, 0x3E, 0x9F, 0x4E, 0xA0, 0xAE, 0xDC, + 0x44, 0x63, 0x96, 0xBC, 0xDA, 0x1C, 0x12, 0xF8, 0x8A, 0x43, 0x91, 0x7E, 0xF4, 0xBF, 0xCD, 0xAA, + 0x0C, 0x6C, 0x3A, 0xBC, 0x1D, 0xCE, 0x27, 0xCC, 0xA5, 0x21, 0xF5, 0xB2, 0xFD, 0xF1, 0x7C, 0xF2, + 0x0E, 0xC7, 0x13, 0x76, 0x81, 0xA3, 0x07, 0x70, 0x02, 0x31, 0xE4, 0x61, 0x5D, 0x41, 0xAB, 0xD8, + 0x38, 0x70, 0x6C, 0x34, 0xD0, 0xBC, 0xDD, 0xD4, 0x43, 0xB0, 0x8A, 0x18, 0x67, 0x6A, 0x58, 0x32, + 0xA4, 0xDB, 0x40, 0x00, 0xD0, 0xE8, 0xFF, 0x02, 0x46, 0xAE, 0xE8, 0x15, 0x4A, 0xF7, 0xE5, 0x8D, + 0x75, 0x71, 0xC7, 0x43, 0xB6, 0x3F, 0x01, 0x10, 0xB3, 0x72, 0x4D, 0x0C, 0xB0, 0xC0, 0x61, 0xE9, + 0x2B, 0xB0, 0x0E, 0xF6, 0x96, 0xF4, 0x56, 0x0F, 0xEA, 0x82, 0xC7, 0x2F, 0xF2, 0xAC, 0x3A, 0x7D, + 0xB9, 0x6A, 0x5A, 0x09, 0xFC, 0x0B, 0xE1, 0x29, 0x18, 0xCC, 0x2D, 0x93, 0xAF, 0x51, 0x49, 0xA6, + 0xA2, 0x49, 0x7C, 0x3A, 0x94, 0xB5, 0x69, 0x99, 0x14, 0xA7, 0xEB, 0xB1, 0x91, 0xC2, 0xA3, 0xDF, + 0xA0, 0x6F, 0x44, 0xA7, 0xFA, 0x0B, 0x60, 0x6C, 0x91, 0xC1, 0x37, 0x66, 0x7B, 0xC6, 0x1E, 0x4F, + 0x9D, 0x50, 0x0C, 0x0A, 0xC1, 0x44, 0x84, 0xE6, 0xC8, 0x9C, 0x48, 0x33, 0x34, 0x62, 0x0E, 0x45, + 0xC6, 0x7A, 0x1E, 0xAF, 0x47, 0xCB, 0x64, 0xD5, 0x2A, 0xA9, 0xAA, 0xB5, 0x40, 0xEB, 0xE2, 0xF0, + 0xBD, 0x4F, 0x5A, 0x1D, 0x14, 0xDD, 0xA3, 0x92, 0x87, 0xAA, 0xE4, 0x71, 0x54, 0xE7, 0xA6, 0x9D, + 0x4B, 0x44, 0xDA, 0xB1, 0x8E, 0x47, 0x88, 0x5B, 0x94, 0xA7, 0x20, 0xE2, 0x8D, 0x8F, 0xAF, 0xC6, + 0x75, 0x28, 0x35, 0x10, 0xD9, 0xB0, 0xC4, 0xBE, 0x8A, 0xB6, 0x60, 0xE4, 0x1B, 0x80, 0x64, 0x7C, + 0x22, 0x51, 0xF4, 0x40, 0x4A, 0x91, 0xE1, 0xBD, 0x56, 0x50, 0x0E, 0xC6, 0x8E, 0xA1, 0x9E, 0xC9, + 0xA9, 0x5A, 0x86, 0x7F, 0x4C, 0x10, 0x7D, 0xF8, 0x47, 0x33, 0x5E, 0x7A, 0xF1, 0x96, 0xE4, 0xD0, + 0x5E, 0x34, 0xD2, 0xB9, 0x93, 0xC0, 0x30, 0x0E, 0xE6, 0xB7, 0x05, 0xF2, 0x81, 0x2F, 0xFE, 0x76, + 0xF5, 0x89, 0x09, 0x3C, 0x1B, 0x05, 0x35, 0x10, 0xF1, 0xC8, 0x23, 0x2A, 0xAD, 0x64, 0x3B, 0x98, + 0x62, 0x76, 0x70, 0x02, 0x15, 0xCE, 0x27, 0x64, 0xCF, 0x06, 0x45, 0x76, 0x1D, 0x2C, 0x70, 0x33, + 0x5A, 0x33, 0xAF, 0xA0, 0x2D, 0x2D, 0xCF, 0x5D, 0x81, 0x2B, 0x4E, 0x9D, 0xC4, 0xAF, 0x8E, 0x60, + 0x48, 0xE6, 0xD8, 0xD4, 0xD4, 0xA4, 0xAB, 0x7B, 0x64, 0x1E, 0x09, 0x2C, 0x92, 0xDD, 0x97, 0x35, + 0x7A, 0xF8, 0x87, 0xCD, 0x06, 0x7C, 0x63, 0xE5, 0x87, 0xC7, 0xCB, 0x83, 0x21, 0x0C, 0xB0, 0x38, + 0xA4, 0x42, 0xA1, 0x1A, 0xCA, 0xA2, 0xE9, 0xF8, 0x4D, 0xF1, 0x43, 0x7D, 0x92, 0x97, 0x21, 0x78, + 0x09, 0xA2, 0x7D, 0x87, 0x51, 0xB6, 0xB6, 0xB4, 0xA4, 0xC3, 0x26, 0x83, 0x45, 0x0B, 0x0A, 0x04, + 0x40, 0x48, 0xC5, 0xC7, 0x2D, 0xCF, 0xFD, 0x84, 0x33, 0x23, 0xB4, 0xE7, 0x23, 0xF8, 0x67, 0x6A, + 0xA2, 0x7C, 0xB0, 0x87, 0x33, 0x68, 0x0D, 0x18, 0x69, 0xC6, 0xE1, 0x6C, 0x53, 0xC6, 0x05, 0xBA, + 0x35, 0x61, 0x50, 0x9D, 0x0B, 0xBC, 0x06, 0xD4, 0x10, 0x63, 0x28, 0xF2, 0xB3, 0x13, 0x78, 0xB9, + 0x12, 0xA8, 0xC7, 0xF2, 0xB0, 0x25, 0x23, 0x6F, 0xDB, 0x88, 0xEE, 0x8B, 0x1D, 0x74, 0x86, 0x68, + 0xFF, 0xA5, 0x04, 0x52, 0x4F, 0x67, 0xC5, 0xF1, 0x50, 0x9E, 0x62, 0xE8, 0x6E, 0x46, 0xE0, 0xC5, + 0xB2, 0x9B, 0x87, 0xDA, 0x5C, 0x33, 0x82, 0x1E, 0x78, 0xC4, 0xCA, 0xFB, 0x86, 0xF9, 0xE5, 0xAC, + 0x9E, 0x04, 0x4E, 0xC8, 0xFC, 0x52, 0x2E, 0xAB, 0xCF, 0x53, 0x5B, 0xA2, 0xCE, 0x58, 0x0B, 0x8D, + 0xBB, 0x26, 0xCB, 0x30, 0x0B, 0x9D, 0xD6, 0x91, 0x1E, 0x8D, 0x96, 0xBF, 0x93, 0x00, 0x62, 0x2D, + 0x93, 0x91, 0x22, 0x42, 0x64, 0x10, 0x21, 0x6C, 0x44, 0x88, 0x7E, 0x27, 0x11, 0xC4, 0x5A, 0x26, + 0xB2, 0x00, 0x86, 0x89, 0xED, 0xA4, 0xB6, 0x41, 0x38, 0x9A, 0xFF, 0x4E, 0x02, 0x88, 0xB5, 0x44, + 0xAA, 0x0D, 0xA6, 0x06, 0x11, 0xC2, 0x46, 0x84, 0xE0, 0x77, 0x12, 0x41, 0xAC, 0x25, 0x92, 0x05, + 0x98, 0x75, 0x2E, 0x74, 0xFC, 0x1E, 0x02, 0xCC, 0x4C, 0x8B, 0x21, 0x24, 0x1C, 0xCF, 0x7B, 0xD2, + 0x03, 0xA6, 0xF2, 0xAC, 0x15, 0x2A, 0xC1, 0x99, 0xBC, 0x73, 0x64, 0xD4, 0x34, 0xDC, 0xFB, 0xD0, + 0x92, 0x78, 0x37, 0xC2, 0x85, 0x4E, 0x46, 0xBB, 0xF3, 0x3C, 0x86, 0xD4, 0x64, 0x97, 0xE5, 0xE9, + 0xB0, 0x9E, 0xD1, 0x96, 0x44, 0x57, 0x2A, 0xF7, 0x28, 0xDB, 0xC3, 0x24, 0x31, 0x86, 0xB2, 0x85, + 0x3E, 0x55, 0x50, 0x06, 0x54, 0x67, 0x0C, 0x92, 0x49, 0x5C, 0x81, 0x48, 0x7C, 0xE6, 0x6B, 0x9A, + 0xDE, 0x6B, 0xC8, 0x21, 0x16, 0x78, 0x55, 0x10, 0x49, 0x9D, 0x89, 0x8B, 0x74, 0x30, 0xD0, 0x80, + 0x9F, 0xA5, 0x38, 0xC9, 0xF7, 0x09, 0x77, 0x4B, 0x85, 0x5C, 0x72, 0x75, 0x10, 0x6E, 0xE0, 0xDA, + 0x2C, 0x92, 0x26, 0x50, 0x12, 0x2F, 0xC5, 0x3E, 0xF3, 0xEB, 0x8F, 0x3F, 0xFD, 0xF2, 0xF3, 0x33, + 0x60, 0x8F, 0x62, 0xEB, 0x6F, 0xF2, 0x73, 0x86, 0x65, 0xBB, 0xF8, 0xC2, 0x40, 0x43, 0xE8, 0xBA, + 0x28, 0x15, 0xEC, 0x60, 0x14, 0xEB, 0x7D, 0x9C, 0xE5, 0x4D, 0xBF, 0x41, 0x95, 0xB5, 0xAC, 0x52, + 0x52, 0x1C, 0xC7, 0xCB, 0x7F, 0x03, 0x32, 0x22, 0x1F, 0x3C, 0x9B, 0x22, 0x14, 0xA2, 0x99, 0x3C, + 0x17, 0xAA, 0x79, 0xB4, 0x55, 0x4E, 0x27, 0x3E, 0x46, 0x2F, 0xA8, 0x33, 0x7D, 0x10, 0xD0, 0xB4, + 0x80, 0x21, 0x7B, 0x7B, 0x43, 0x5D, 0x4F, 0x71, 0xBA, 0xEC, 0x88, 0xAD, 0x15, 0x03, 0x87, 0xD2, + 0xE3, 0x1F, 0xA9, 0xB5, 0xC4, 0x84, 0x7C, 0x2B, 0x7E, 0x2B, 0xA4, 0xBC, 0xAA, 0x10, 0xBF, 0x5D, + 0x72, 0xE9, 0xDB, 0x25, 0xBF, 0xE9, 0x23, 0x3B, 0xEA, 0xAE, 0x7C, 0xA2, 0xBE, 0x85, 0x9E, 0x13, + 0x13, 0x7B, 0x74, 0x55, 0x6F, 0xF8, 0x20, 0x84, 0x4F, 0x1B, 0x59, 0xED, 0xFC, 0x8D, 0x21, 0x85, + 0xC5, 0x1C, 0x9A, 0x60, 0x08, 0xFD, 0xE9, 0x7C, 0x86, 0x23, 0x9F, 0x0B, 0x29, 0xEB, 0x22, 0xF8, + 0xC8, 0x74, 0x02, 0x18, 0xF1, 0x68, 0x60, 0x31, 0x9F, 0x2D, 0x74, 0x16, 0xDC, 0x38, 0x56, 0x86, + 0xF3, 0xD0, 0xC2, 0x70, 0xB9, 0x0C, 0x65, 0x86, 0xEE, 0xB6, 0x17, 0xD8, 0x2F, 0x67, 0x0D, 0x7B, + 0x1D, 0xED, 0xEE, 0xE5, 0x2F, 0xB7, 0xAD, 0x18, 0x41, 0xC1, 0xC4, 0x56, 0xDD, 0xE4, 0xF7, 0xAA, + 0x4E, 0xFA, 0x26, 0xD7, 0x1E, 0x51, 0x5B, 0x8E, 0xCA, 0xC3, 0x77, 0x37, 0xC9, 0x3B, 0xD8, 0x08, + 0xFE, 0x41, 0x4C, 0xBC, 0x02, 0x06, 0xD7, 0xB1, 0xB5, 0xC9, 0xD1, 0xE5, 0x01, 0xFF, 0xA0, 0xF3, + 0x43, 0xFC, 0x43, 0x58, 0xD0, 0xA1, 0x5D, 0x8C, 0xEA, 0x72, 0xD7, 0x21, 0x6C, 0x7E, 0xFA, 0x81, + 0xEC, 0x71, 0x66, 0xD2, 0x0B, 0x21, 0x1D, 0x4D, 0x87, 0x82, 0x2F, 0x76, 0x91, 0x12, 0x2D, 0x5E, + 0x7D, 0xF1, 0x46, 0xA4, 0xAA, 0x76, 0x25, 0x5E, 0x6F, 0x1D, 0x8B, 0x33, 0x22, 0xAC, 0xB7, 0x1E, + 0x46, 0x9D, 0x62, 0x00, 0x6B, 0x31, 0x94, 0x3A, 0x48, 0x45, 0x2B, 0xE8, 0x06, 0xC0, 0x48, 0x91, + 0x13, 0x09, 0x57, 0xC1, 0x00, 0x56, 0x66, 0x9E, 0x23, 0x42, 0x63, 0x34, 0xAC, 0x7C, 0x1C, 0x69, + 0x4C, 0x7C, 0xB2, 0x68, 0x83, 0xBF, 0x33, 0xF1, 0xF1, 0x47, 0x81, 0x32, 0xD0, 0xC8, 0xB8, 0x5A, + 0x02, 0x59, 0x68, 0x21, 0x9B, 0x4A, 0x54, 0x91, 0x85, 0x2A, 0x82, 0xB1, 0x59, 0xF3, 0x47, 0x2C, + 0x30, 0xB1, 0x14, 0x08, 0xA7, 0x22, 0xD5, 0xD4, 0x46, 0x25, 0x55, 0x3E, 0xB3, 0x69, 0x32, 0x1B, + 0xCD, 0xEA, 0x3F, 0xF3, 0xE7, 0x5A, 0xF1, 0x27, 0x07, 0x7F, 0x37, 0x5B, 0x14, 0x32, 0x7A, 0x18, + 0xB5, 0x2A, 0x5C, 0xED, 0x5A, 0x15, 0x8E, 0xA6, 0xAD, 0x8A, 0xFE, 0xD6, 0xAD, 0x0A, 0x47, 0x03, + 0x57, 0x85, 0xA3, 0x8D, 0xAB, 0xC2, 0xD5, 0xCC, 0xAD, 0x20, 0x9D, 0x03, 0xCA, 0x98, 0xED, 0x0C, + 0x19, 0x3D, 0xEC, 0x5C, 0xA4, 0xAE, 0x76, 0x2E, 0x52, 0x47, 0x3B, 0x17, 0x69, 0x7F, 0x3B, 0x17, + 0xA9, 0xA3, 0x9D, 0x8B, 0xD4, 0xD1, 0xCE, 0x45, 0xFA, 0x04, 0x76, 0xE6, 0x7D, 0x57, 0x0E, 0xD8, + 0x6E, 0xB6, 0x33, 0x64, 0xF4, 0xB0, 0x73, 0xBE, 0x75, 0xB5, 0x73, 0xBE, 0x75, 0xB4, 0x73, 0xBE, + 0xED, 0x6F, 0xE7, 0x7C, 0xEB, 0x68, 0xE7, 0x7C, 0xEB, 0x68, 0xE7, 0x7C, 0xFB, 0x04, 0x76, 0xC6, + 0x9D, 0x78, 0x63, 0xE8, 0x8B, 0x05, 0x8A, 0x21, 0xA3, 0x87, 0xA1, 0x2F, 0xB9, 0xAB, 0xA1, 0x2F, + 0xB9, 0xA3, 0xA1, 0x2F, 0x79, 0x7F, 0x43, 0x5F, 0x72, 0x47, 0x43, 0x5F, 0x72, 0x47, 0x43, 0x5F, + 0xF2, 0x27, 0x31, 0xF4, 0x44, 0x34, 0xB4, 0xDD, 0xD2, 0xFD, 0x4C, 0xDD, 0xC3, 0xD6, 0xEE, 0xC6, + 0xBE, 0xCB, 0xDA, 0xEE, 0xE6, 0x76, 0xB7, 0x77, 0x0F, 0x83, 0x13, 0xB3, 0xE2, 0x7C, 0xAB, 0xD9, + 0x28, 0x49, 0x60, 0xCC, 0x5F, 0x8C, 0x1A, 0x9D, 0x28, 0x59, 0xD8, 0x51, 0xE9, 0x9C, 0xD1, 0x45, + 0x16, 0x85, 0x59, 0xF6, 0xA4, 0xCB, 0x88, 0x8C, 0x6E, 0x6A, 0xA4, 0x9B, 0x04, 0x6A, 0x75, 0x33, + 0x4B, 0xF3, 0xB1, 0xEC, 0xB9, 0x39, 0x5B, 0xD3, 0x6E, 0x61, 0xA4, 0x9B, 0x69, 0xDA, 0x2D, 0x8D, + 0x74, 0xF3, 0x5A, 0xBB, 0x60, 0x6C, 0x36, 0xA6, 0xA6, 0x5E, 0x60, 0xB6, 0xFA, 0x52, 0xD3, 0x2F, + 0x08, 0x5B, 0x5C, 0x99, 0x2E, 0xF8, 0x42, 0x0B, 0x8A, 0x43, 0x60, 0xAE, 0x1B, 0xA7, 0x08, 0x45, + 0x0A, 0xB1, 0xD1, 0x38, 0x49, 0x24, 0x91, 0x84, 0x53, 0x9E, 0x33, 0x11, 0x73, 0xC4, 0xA6, 0xE2, + 0x24, 0x53, 0x91, 0x44, 0x6C, 0x25, 0x4E, 0x32, 0x93, 0x48, 0xA6, 0x82, 0xF8, 0x73, 0x39, 0xC7, + 0x24, 0xFF, 0x42, 0x22, 0x99, 0x99, 0xE4, 0x5F, 0x4A, 0x24, 0x73, 0x2E, 0x3F, 0x34, 0x8A, 0x6C, + 0x1F, 0x93, 0x02, 0x81, 0x64, 0x43, 0xA5, 0x1D, 0xB6, 0xFE, 0xD8, 0x1B, 0x6D, 0x2F, 0xFE, 0x58, + 0x99, 0x00, 0x8D, 0xEB, 0xAC, 0xAB, 0x92, 0x75, 0xA5, 0x59, 0x01, 0x29, 0x15, 0xA8, 0xA5, 0xE8, + 0x32, 0x45, 0x4D, 0x70, 0x55, 0x08, 0xAE, 0x22, 0x41, 0x48, 0x38, 0x84, 0x1A, 0x07, 0x31, 0xFF, + 0xAA, 0xE4, 0x5F, 0x85, 0xFC, 0x88, 0x94, 0x8F, 0xD4, 0x89, 0x9B, 0x90, 0x7B, 0x55, 0x72, 0xAF, + 0x4D, 0xEE, 0x84, 0x94, 0x9D, 0xE8, 0x93, 0x3E, 0x21, 0xFF, 0xAA, 0xE4, 0x5F, 0x85, 0xFC, 0x29, + 0x29, 0x3F, 0x55, 0xCA, 0x47, 0x42, 0xEE, 0x55, 0xC9, 0xBD, 0x92, 0xDC, 0xCE, 0xA1, 0xBC, 0x0B, + 0xA8, 0x55, 0x85, 0x2B, 0xAE, 0x55, 0x85, 0x33, 0xB4, 0x01, 0xA9, 0x0D, 0xDD, 0x1A, 0x0A, 0x67, + 0x80, 0xAB, 0x0A, 0x67, 0x8C, 0x03, 0x52, 0x1B, 0xCC, 0x35, 0x14, 0xCE, 0x48, 0x57, 0x15, 0xCE, + 0x60, 0x07, 0xA4, 0x36, 0xBC, 0x6B, 0x28, 0xDC, 0x21, 0xAF, 0x2A, 0xDC, 0x51, 0xAF, 0x2A, 0x9C, + 0x80, 0x0F, 0xC8, 0xE4, 0xD8, 0x1E, 0x0B, 0x59, 0x0E, 0xB0, 0x58, 0x15, 0x2E, 0xC8, 0x08, 0x54, + 0x3A, 0x38, 0xF2, 0x4C, 0x17, 0x7C, 0xAC, 0x0A, 0x17, 0x88, 0x04, 0x2A, 0x1D, 0x25, 0x79, 0xA6, + 0x0B, 0x50, 0x56, 0x85, 0x0B, 0x56, 0x02, 0x95, 0x0E, 0x97, 0x3C, 0xD3, 0x09, 0x31, 0x81, 0xAC, + 0x1D, 0x34, 0x81, 0x82, 0xE2, 0x26, 0xFE, 0xA0, 0x43, 0x27, 0x27, 0xB8, 0xEA, 0x04, 0xD7, 0x86, + 0x20, 0x60, 0x1C, 0x5A, 0x60, 0x94, 0xE4, 0x32, 0x3E, 0x2D, 0x60, 0x4A, 0x5A, 0x9B, 0x71, 0xB3, + 0x43, 0x2A, 0xC9, 0x64, 0xCC, 0xEC, 0xC0, 0x4A, 0x7C, 0xA2, 0xE6, 0xA5, 0xC3, 0x2B, 0xA7, 0x61, + 0x9C, 0x6C, 0x20, 0x4B, 0xDC, 0x87, 0xF1, 0xB1, 0x43, 0x2D, 0xC9, 0x64, 0x9C, 0xEC, 0x80, 0x4B, + 0x9C, 0xAC, 0xE6, 0xA5, 0xC3, 0x2E, 0xA7, 0x61, 0x9C, 0x8C, 0xE0, 0xDB, 0x3D, 0xC1, 0x77, 0x41, + 0xDF, 0x22, 0x75, 0x45, 0xDF, 0x22, 0x75, 0x46, 0x5F, 0x20, 0xB5, 0xA1, 0x6F, 0x43, 0xE1, 0x8C, + 0xBE, 0x45, 0xEA, 0x8C, 0xBE, 0x40, 0x6A, 0x43, 0xDF, 0x86, 0xC2, 0x19, 0x7D, 0x8B, 0xD4, 0x19, + 0x7D, 0x81, 0xD4, 0x86, 0xBE, 0x0D, 0x85, 0x3B, 0xFA, 0x16, 0xA9, 0x3B, 0xFA, 0x16, 0xA9, 0x13, + 0xFA, 0x02, 0x99, 0x8A, 0xBE, 0x3C, 0xCB, 0x01, 0x7D, 0x8B, 0xD4, 0x05, 0x7D, 0x81, 0x4A, 0x47, + 0x5F, 0x9E, 0xE9, 0x82, 0xBE, 0x45, 0xEA, 0x82, 0xBE, 0x40, 0xA5, 0xA3, 0x2F, 0xCF, 0x74, 0x41, + 0xDF, 0x22, 0x75, 0x41, 0x5F, 0xA0, 0xD2, 0xD1, 0x97, 0x67, 0x3A, 0xA1, 0x2F, 0x90, 0xB5, 0xA3, + 0x2F, 0x50, 0x50, 0xF4, 0xC5, 0x1F, 0x74, 0xF4, 0xE5, 0x04, 0x57, 0x9D, 0xE0, 0xDA, 0x10, 0x04, + 0x8C, 0x43, 0x0B, 0xFA, 0x92, 0x5C, 0xC6, 0xA7, 0x05, 0x7D, 0x49, 0x6B, 0x33, 0x6E, 0x76, 0xF4, + 0x25, 0x99, 0x8C, 0x99, 0x1D, 0x7D, 0x89, 0x4F, 0xD4, 0xBC, 0x74, 0xF4, 0xE5, 0x34, 0x8C, 0x93, + 0x0D, 0x7D, 0x89, 0xFB, 0x30, 0x3E, 0x76, 0xF4, 0x25, 0x99, 0x8C, 0x93, 0x1D, 0x7D, 0x89, 0x93, + 0xD5, 0xBC, 0x74, 0xF4, 0xE5, 0x34, 0x8C, 0x93, 0x33, 0xFA, 0xCA, 0xCB, 0x7E, 0x2E, 0xE8, 0x9B, + 0x6F, 0x5D, 0xD1, 0x37, 0xDF, 0x3A, 0xA3, 0x2F, 0x90, 0xDA, 0xD0, 0xB7, 0xA1, 0x70, 0x46, 0xDF, + 0x7C, 0xEB, 0x8C, 0xBE, 0x40, 0x6A, 0x43, 0xDF, 0x86, 0xC2, 0x19, 0x7D, 0xF3, 0xAD, 0x33, 0xFA, + 0x02, 0xA9, 0x0D, 0x7D, 0x1B, 0x0A, 0x77, 0xF4, 0xCD, 0xB7, 0xEE, 0xE8, 0x9B, 0x6F, 0x9D, 0xD0, + 0x17, 0xC8, 0x54, 0xF4, 0xE5, 0x59, 0x0E, 0xE8, 0x9B, 0x6F, 0x5D, 0xD0, 0x17, 0xA8, 0x74, 0xF4, + 0xE5, 0x99, 0x2E, 0xE8, 0x9B, 0x6F, 0x5D, 0xD0, 0x17, 0xA8, 0x74, 0xF4, 0xE5, 0x99, 0x2E, 0xE8, + 0x9B, 0x6F, 0x5D, 0xD0, 0x17, 0xA8, 0x74, 0xF4, 0xE5, 0x99, 0x4E, 0xE8, 0x0B, 0x64, 0x1D, 0xE8, + 0x9B, 0xB3, 0x35, 0x03, 0xFC, 0x41, 0x47, 0x5F, 0x4E, 0x70, 0xD5, 0x09, 0xAE, 0x0D, 0x41, 0xC0, + 0x38, 0xB4, 0xA0, 0x2F, 0xC9, 0x65, 0x7C, 0x5A, 0xD0, 0x97, 0xB4, 0x36, 0xE3, 0x66, 0x47, 0x5F, + 0x92, 0xC9, 0x98, 0xD9, 0xD1, 0x97, 0xF8, 0x44, 0xCD, 0x4B, 0x47, 0x5F, 0x4E, 0xC3, 0x38, 0xD9, + 0xD0, 0x97, 0xB8, 0x0F, 0xE3, 0x63, 0x47, 0x5F, 0x92, 0xC9, 0x38, 0xD9, 0xD1, 0x97, 0x38, 0x59, + 0xCD, 0x4B, 0x47, 0x5F, 0x4E, 0xC3, 0x38, 0x39, 0xA3, 0xAF, 0xBC, 0x19, 0xE0, 0xB4, 0x9E, 0x7A, + 0x71, 0x5E, 0x52, 0xBD, 0xB8, 0xAF, 0xAA, 0x5E, 0x3A, 0x17, 0x56, 0x2F, 0xEE, 0x6B, 0xAB, 0x17, + 0xF7, 0xE5, 0xD5, 0x4B, 0xE7, 0x0A, 0xEB, 0xC5, 0x7D, 0x91, 0xF5, 0xE2, 0xBE, 0xCE, 0x7A, 0xE9, + 0x5C, 0x6A, 0xBD, 0xF4, 0x58, 0x6D, 0xBD, 0xF4, 0x58, 0x70, 0xBD, 0xB8, 0xAD, 0xB9, 0x02, 0x99, + 0x0A, 0xBF, 0x3C, 0xCB, 0x01, 0x7E, 0x2F, 0xB9, 0x0B, 0xFC, 0x02, 0x95, 0x0E, 0xBF, 0x3C, 0xD3, + 0x05, 0x7E, 0x2F, 0xB9, 0x0B, 0xFC, 0x02, 0x95, 0x0E, 0xBF, 0x3C, 0xD3, 0x05, 0x7E, 0x2F, 0xB9, + 0x0B, 0xFC, 0x02, 0x95, 0x0E, 0xBF, 0x3C, 0xD3, 0x09, 0x7E, 0x81, 0xAC, 0x1D, 0x7E, 0x81, 0x82, + 0xC2, 0x2F, 0xFE, 0xA0, 0xC3, 0x2F, 0x27, 0xB8, 0xEA, 0x04, 0xD7, 0x86, 0x20, 0x60, 0x1C, 0x5A, + 0xE0, 0x97, 0xE4, 0x32, 0x3E, 0x2D, 0xF0, 0x4B, 0x5A, 0x9B, 0x71, 0xB3, 0xC3, 0x2F, 0xC9, 0x64, + 0xCC, 0xEC, 0xF0, 0x4B, 0x7C, 0xA2, 0xE6, 0xA5, 0xC3, 0x2F, 0xA7, 0x61, 0x9C, 0x6C, 0xF0, 0x4B, + 0xDC, 0x87, 0xF1, 0xB1, 0xC3, 0x2F, 0xC9, 0x64, 0x9C, 0xEC, 0xF0, 0x4B, 0x9C, 0xAC, 0xE6, 0xA5, + 0xC3, 0x2F, 0xA7, 0x61, 0x9C, 0xDC, 0xE1, 0x57, 0xDA, 0x22, 0x74, 0xC4, 0xDF, 0x1E, 0x00, 0xDC, + 0x07, 0x81, 0x1D, 0x20, 0xB8, 0x0F, 0x06, 0xF7, 0x01, 0x61, 0x07, 0x14, 0xEE, 0x03, 0xC3, 0x7D, + 0x70, 0xD8, 0x01, 0x88, 0x7B, 0x21, 0x71, 0x2F, 0x28, 0x76, 0xC5, 0xE2, 0x36, 0x30, 0x76, 0x43, + 0x63, 0x47, 0x38, 0x6E, 0xC7, 0x63, 0x47, 0x40, 0x76, 0x44, 0xE4, 0x76, 0x48, 0x76, 0xC4, 0x64, + 0x47, 0x50, 0x6E, 0x47, 0x65, 0x57, 0x58, 0x76, 0xC0, 0x65, 0x0E, 0xCC, 0x56, 0x64, 0xE6, 0xD0, + 0x6C, 0xC5, 0x66, 0x0E, 0xCE, 0x1D, 0xE8, 0xCC, 0xE1, 0xB9, 0x03, 0x9F, 0x39, 0x40, 0xB7, 0x23, + 0x34, 0x87, 0xE8, 0x76, 0x8C, 0xE6, 0x20, 0xDD, 0x86, 0xD2, 0x1C, 0xA6, 0xDB, 0x70, 0x9A, 0x03, + 0x75, 0x3B, 0x52, 0x73, 0xA8, 0x6E, 0xC7, 0x6A, 0x0E, 0xD6, 0x6D, 0x68, 0xCD, 0xE1, 0xDA, 0x8A, + 0xD7, 0x23, 0xF2, 0x46, 0x28, 0xCD, 0x22, 0x1F, 0xFD, 0xF5, 0x56, 0x3C, 0x17, 0xE2, 0x41, 0xC8, + 0x89, 0x93, 0x04, 0x52, 0x5A, 0x08, 0xE0, 0x9D, 0xEF, 0xEC, 0x88, 0xD4, 0x97, 0x5D, 0x75, 0x02, + 0xCC, 0x82, 0x9C, 0xE4, 0x31, 0xF6, 0x06, 0xEC, 0xDF, 0x68, 0x3C, 0x1D, 0xCA, 0x75, 0xE1, 0x57, + 0xB6, 0x65, 0x4E, 0x7A, 0xBE, 0x89, 0x51, 0x20, 0xF1, 0x21, 0xEF, 0x49, 0xCB, 0x6C, 0xB4, 0x6C, + 0xA3, 0x38, 0x73, 0x90, 0x87, 0xE3, 0x94, 0xE9, 0xA9, 0x7B, 0x89, 0xA9, 0xFA, 0xFA, 0x38, 0x3C, + 0x18, 0xFA, 0x20, 0xBD, 0x9B, 0xCA, 0x1E, 0x77, 0x67, 0x06, 0x7F, 0x95, 0xBC, 0x9D, 0xC0, 0x9E, + 0x3B, 0x1F, 0xBE, 0xFA, 0xE2, 0xAB, 0x2F, 0xF2, 0xA7, 0xE8, 0xC9, 0xFB, 0x1A, 0x23, 0xE3, 0xA3, + 0xF4, 0xF5, 0xB3, 0xA6, 0xBC, 0xAD, 0x86, 0xAC, 0x16, 0x26, 0x59, 0xDD, 0xE7, 0x1E, 0x2F, 0x90, + 0x8E, 0x9F, 0x8F, 0x8D, 0xD3, 0xC3, 0x77, 0xF0, 0x88, 0x3C, 0x3E, 0x21, 0x69, 0x4C, 0xFE, 0x2E, + 0xE1, 0x0F, 0x3C, 0x83, 0x4F, 0x19, 0xE9, 0x4D, 0x3B, 0xAC, 0x05, 0x24, 0xEF, 0xDC, 0x36, 0xAF, + 0x50, 0xC9, 0x2F, 0xEC, 0xD6, 0x34, 0xE4, 0x2D, 0x5C, 0x95, 0x86, 0x0A, 0x52, 0x93, 0x68, 0x6F, + 0x61, 0xBC, 0x2A, 0x7C, 0x01, 0xAD, 0x65, 0xE9, 0xF5, 0xA3, 0x45, 0x1E, 0x47, 0xD4, 0x48, 0xE4, + 0x51, 0x5B, 0xF9, 0xC5, 0x65, 0x48, 0x60, 0xB5, 0xC0, 0x4E, 0x48, 0x9B, 0x41, 0xE9, 0x2B, 0x38, + 0xF4, 0x57, 0x5D, 0x82, 0xD6, 0x8B, 0x52, 0xB9, 0x5C, 0x23, 0x10, 0xB7, 0xE3, 0x60, 0xDC, 0x55, + 0x44, 0x2D, 0x34, 0x1E, 0x40, 0x31, 0xB9, 0x50, 0x0E, 0xAF, 0xA3, 0x19, 0x8B, 0xE9, 0x4D, 0x47, + 0xAB, 0xE3, 0xB1, 0x42, 0x1B, 0xE2, 0xD5, 0x53, 0xB9, 0xC2, 0x2F, 0x2B, 0x1E, 0x36, 0x3E, 0x7E, + 0x6D, 0xF0, 0x85, 0x43, 0x0A, 0xEF, 0xB3, 0x98, 0xDA, 0x8F, 0x7B, 0x88, 0xC4, 0x85, 0x78, 0x0A, + 0x77, 0x21, 0x5B, 0xCC, 0x92, 0xD6, 0xE7, 0xE1, 0xE5, 0x54, 0x03, 0x25, 0xE5, 0x15, 0x98, 0xF3, + 0x39, 0x7F, 0x1E, 0x77, 0x5C, 0x35, 0xF2, 0xD5, 0xA9, 0x36, 0x42, 0xC9, 0x2B, 0x33, 0x66, 0xCB, + 0x75, 0xD5, 0x87, 0x47, 0x29, 0x18, 0xF7, 0x6C, 0xB2, 0x41, 0xE1, 0x66, 0x63, 0x40, 0x27, 0x9A, + 0x3B, 0x4D, 0xE7, 0x9B, 0xB0, 0x05, 0xDC, 0xC6, 0xE3, 0xB1, 0x19, 0x8F, 0x9E, 0x85, 0xA0, 0x5D, + 0xA3, 0x99, 0x9E, 0xA9, 0x97, 0xE4, 0x22, 0xD3, 0x82, 0x9B, 0x34, 0x40, 0x89, 0x96, 0x27, 0x96, + 0xE3, 0x1F, 0x25, 0x74, 0xA9, 0x2B, 0x65, 0x2A, 0xD7, 0x47, 0x63, 0xE9, 0x4A, 0x03, 0x04, 0x45, + 0x68, 0x6A, 0x53, 0x3A, 0x9D, 0xA7, 0x8B, 0x34, 0xBE, 0x4B, 0xE9, 0x64, 0x9D, 0x40, 0x93, 0xDD, + 0xA1, 0x74, 0x1A, 0xA4, 0x61, 0x3A, 0xB9, 0x4F, 0x69, 0x5A, 0x29, 0x53, 0xBA, 0x3E, 0xF8, 0x4B, + 0x55, 0x19, 0x6C, 0x3A, 0x4F, 0x53, 0x6B, 0x3B, 0xCF, 0xD3, 0x75, 0x7A, 0x67, 0x3B, 0x27, 0xE9, + 0x38, 0x99, 0xDF, 0xA1, 0x72, 0x12, 0xA4, 0xB3, 0xE4, 0xDE, 0x76, 0x26, 0x95, 0x32, 0x95, 0xC9, + 0xB1, 0x66, 0xBA, 0x5F, 0x6F, 0x26, 0x9B, 0xC4, 0xEE, 0xD7, 0x68, 0x81, 0x36, 0xF7, 0xE9, 0x1B, + 0xA7, 0x09, 0x8A, 0xEE, 0xD0, 0x77, 0xBD, 0x01, 0xB7, 0x5B, 0xDE, 0xA9, 0x2F, 0xA9, 0x94, 0xE9, + 0x5B, 0x1F, 0xDA, 0x26, 0xAB, 0x4C, 0xDE, 0x2F, 0x8B, 0x12, 0x6B, 0x13, 0x6F, 0x42, 0x34, 0x4F, + 0xA2, 0xBB, 0x54, 0x46, 0xB3, 0x74, 0xBD, 0x5E, 0xDE, 0xA1, 0x32, 0x4A, 0x50, 0xB0, 0x46, 0xF7, + 0xA9, 0x4C, 0x2B, 0x65, 0x2A, 0xB3, 0x23, 0xE9, 0x34, 0x8D, 0x17, 0xE9, 0xDC, 0x18, 0xA9, 0xAC, + 0x76, 0xF8, 0x73, 0x5F, 0x23, 0xA7, 0x9B, 0x24, 0x4C, 0x26, 0xF7, 0x68, 0x3C, 0x4D, 0xE6, 0x49, + 0x7C, 0x9F, 0xC6, 0xB4, 0x52, 0xA6, 0x31, 0x3D, 0x70, 0x4F, 0x56, 0x98, 0x1D, 0xBF, 0xD7, 0xA2, + 0x30, 0x82, 0xE1, 0xD5, 0x9D, 0x0A, 0xA3, 0x31, 0x0A, 0xEE, 0x52, 0x18, 0xCD, 0xD0, 0xFC, 0x5E, + 0x85, 0x71, 0xA5, 0xBC, 0x89, 0xCB, 0xAF, 0x65, 0x7D, 0x5B, 0xC6, 0xCE, 0x34, 0x37, 0x89, 0xC6, + 0xD1, 0xC4, 0xAA, 0x2F, 0x89, 0x08, 0x8B, 0xBE, 0xD1, 0x3C, 0x5A, 0x47, 0x36, 0xA0, 0xE6, 0x25, + 0x75, 0x7D, 0xA3, 0x30, 0x9A, 0x46, 0x7A, 0x14, 0x8B, 0xE5, 0xF8, 0x47, 0x49, 0xDF, 0xBA, 0xD2, + 0x5A, 0x5F, 0x78, 0x39, 0xFF, 0x08, 0x67, 0x07, 0xE2, 0x01, 0x46, 0x7D, 0xFA, 0x8F, 0x7F, 0x21, + 0xAB, 0x3F, 0xCD, 0x51, 0x01, 0x4D, 0x3A, 0x3D, 0x7B, 0x0F, 0x0F, 0xE3, 0x4E, 0x87, 0x73, 0xB2, + 0xE3, 0xEB, 0x4B, 0xFC, 0xCD, 0xC3, 0xF9, 0x74, 0xB4, 0x24, 0x4F, 0xB7, 0xA8, 0xEC, 0xF1, 0xB8, + 0xB0, 0x7F, 0x0D, 0x86, 0x2A, 0xE6, 0xB3, 0xB9, 0xB5, 0x8A, 0x22, 0xFD, 0x5D, 0xAA, 0x58, 0x2E, + 0x03, 0x6B, 0x15, 0xF9, 0xF6, 0x77, 0xA9, 0x22, 0x08, 0x96, 0x4B, 0x6B, 0x1D, 0x97, 0xFC, 0xF7, + 0xA9, 0x23, 0x6A, 0xAB, 0xE3, 0xAE, 0x4A, 0x46, 0xF8, 0xE5, 0x6B, 0x9F, 0x1C, 0xD8, 0x63, 0x7A, + 0xA5, 0x97, 0x2E, 0x33, 0x71, 0x9A, 0x7A, 0xD8, 0xCF, 0x5F, 0x50, 0x14, 0x5E, 0x81, 0x07, 0xB9, + 0x94, 0x53, 0x8B, 0x8C, 0x24, 0xCA, 0x6B, 0xD9, 0xED, 0xA7, 0xF1, 0xE0, 0x69, 0xB9, 0x2A, 0x04, + 0x6E, 0x32, 0x5D, 0x8E, 0x4E, 0x29, 0x04, 0x02, 0xFD, 0x25, 0x72, 0xAD, 0x8E, 0xAA, 0x30, 0xD4, + 0x11, 0x76, 0x56, 0x22, 0x52, 0xC8, 0x6F, 0x8E, 0xE3, 0x0C, 0x66, 0x6E, 0xFC, 0xEA, 0xBB, 0xF8, + 0xEA, 0x39, 0x2E, 0xE5, 0xF0, 0xA2, 0x39, 0x29, 0x8B, 0x5F, 0x43, 0x85, 0x36, 0x94, 0xCF, 0x01, + 0x33, 0x9D, 0x59, 0x53, 0x5B, 0x7D, 0x44, 0x7E, 0xFD, 0x11, 0x47, 0x63, 0x8A, 0xA9, 0x79, 0x76, + 0x5C, 0x71, 0x83, 0x5C, 0x0C, 0x2F, 0xA4, 0x43, 0x27, 0x32, 0x81, 0x4E, 0xD5, 0x72, 0x7A, 0xCA, + 0x03, 0x3E, 0xFE, 0x47, 0x4B, 0x54, 0xBE, 0x9B, 0xDF, 0x66, 0xC7, 0xCB, 0x25, 0xEC, 0x30, 0x3A, + 0x11, 0x17, 0x07, 0x70, 0xA6, 0x69, 0x35, 0x40, 0x71, 0x85, 0x07, 0x72, 0xFE, 0x01, 0x0E, 0x8B, + 0xE2, 0x73, 0x76, 0x2D, 0xAF, 0xEB, 0xE4, 0x52, 0xFA, 0x75, 0x78, 0x93, 0xDB, 0x80, 0xD6, 0xCC, + 0x4F, 0x83, 0x93, 0x9B, 0x88, 0x1E, 0x41, 0x83, 0x8F, 0x6F, 0xF9, 0xB2, 0x09, 0xCC, 0x15, 0x3D, + 0x1C, 0xC5, 0x46, 0xA8, 0x9E, 0x9D, 0x44, 0xBE, 0x7E, 0x51, 0x22, 0x90, 0x79, 0x9F, 0x5F, 0xBF, + 0x54, 0x4F, 0x4D, 0x92, 0xD9, 0xD0, 0x23, 0xB3, 0x6E, 0x4E, 0x2D, 0x27, 0xF5, 0x1F, 0x8B, 0xD9, + 0x7A, 0xBE, 0x41, 0x0F, 0xCD, 0x11, 0x5B, 0xE2, 0xE2, 0x06, 0x5D, 0xD6, 0x60, 0x6E, 0x4D, 0x96, + 0x71, 0x82, 0xC8, 0x0B, 0x82, 0xB1, 0x17, 0x4E, 0x23, 0x0F, 0x92, 0x87, 0xB2, 0x0C, 0xF2, 0x59, + 0x3E, 0x7E, 0x0C, 0xD5, 0x92, 0x03, 0x7D, 0xBE, 0x8D, 0xE1, 0x60, 0xE5, 0x1B, 0xF7, 0x2F, 0x54, + 0xA8, 0xE5, 0x48, 0xFB, 0x83, 0x47, 0x27, 0x68, 0x47, 0x0E, 0x32, 0xBC, 0xC9, 0x47, 0x9D, 0x35, + 0xA7, 0x64, 0xA9, 0xE5, 0xFA, 0x17, 0xA9, 0xAD, 0xEB, 0x49, 0xC9, 0xDC, 0xCA, 0x37, 0xDD, 0x66, + 0x68, 0x09, 0x63, 0xA0, 0x8D, 0x8D, 0xA3, 0xE5, 0x3C, 0x1F, 0x73, 0x20, 0xD6, 0x47, 0x50, 0xF9, + 0x75, 0xB2, 0xCF, 0xD2, 0x6B, 0xC3, 0x31, 0x5C, 0x80, 0x23, 0x9F, 0x56, 0x52, 0x89, 0xFA, 0xF4, + 0x0D, 0x9E, 0xD1, 0x19, 0xA4, 0x4C, 0x6C, 0xE6, 0x2D, 0x3E, 0xFA, 0x16, 0x66, 0xFD, 0x15, 0x8D, + 0xA4, 0xFB, 0x4E, 0x76, 0xAB, 0xBF, 0x72, 0x51, 0x84, 0x25, 0x31, 0xE5, 0x4C, 0x37, 0x31, 0x2E, + 0x6D, 0x01, 0xC9, 0x25, 0xB6, 0x50, 0xFC, 0xB9, 0x01, 0x6D, 0x6E, 0xC8, 0x8E, 0x30, 0xA7, 0xCB, + 0x2B, 0x1D, 0x81, 0x6B, 0xE1, 0xAC, 0x37, 0x19, 0xA0, 0xC3, 0x18, 0x45, 0x72, 0x0D, 0xC6, 0xB3, + 0xA0, 0xFE, 0xF7, 0x32, 0xE2, 0x65, 0x8D, 0x42, 0x7F, 0xA9, 0xB7, 0xFD, 0x23, 0x7D, 0xBD, 0xC5, + 0xA9, 0x74, 0xA3, 0xD2, 0xF6, 0xFC, 0x5D, 0x22, 0xC1, 0x50, 0x6F, 0xBF, 0x78, 0xC0, 0xDD, 0x04, + 0xF6, 0xB5, 0xCB, 0xC9, 0x7D, 0x14, 0xA4, 0x9E, 0xE0, 0xD7, 0x7B, 0xC4, 0x23, 0x6C, 0xCA, 0xD4, + 0x83, 0x1B, 0x3A, 0xB0, 0x11, 0x33, 0xF4, 0xE5, 0x6D, 0x9B, 0xE4, 0x52, 0x32, 0x3E, 0x89, 0xC4, + 0x89, 0x4E, 0x18, 0x94, 0x96, 0xEC, 0x60, 0x5D, 0xF9, 0x7C, 0xA7, 0x47, 0x8D, 0x1E, 0x1E, 0x15, + 0x60, 0x5A, 0x36, 0x07, 0x87, 0x91, 0x01, 0x6A, 0x33, 0x1E, 0xC6, 0x0F, 0xCF, 0x0E, 0x1F, 0xD4, + 0xE5, 0x7C, 0xFC, 0x53, 0x1F, 0xBF, 0x9A, 0x8E, 0x56, 0xD5, 0xAA, 0x6C, 0xED, 0x0E, 0x79, 0x0D, + 0x32, 0x4C, 0x85, 0x0C, 0xA5, 0xAC, 0x20, 0x65, 0xC3, 0xA8, 0xA9, 0x49, 0x04, 0x37, 0xC4, 0xFC, + 0xC3, 0x05, 0xC9, 0xB7, 0x76, 0xF3, 0x07, 0x46, 0xEB, 0x93, 0xCA, 0xF5, 0x63, 0xAE, 0x4C, 0xB6, + 0x8F, 0x4C, 0xF5, 0x75, 0xD8, 0x9E, 0xF3, 0xE7, 0x0A, 0x53, 0x7D, 0x03, 0x8B, 0xBA, 0x81, 0x59, + 0xDB, 0xC0, 0x54, 0xB9, 0x9B, 0xD5, 0x7F, 0x57, 0x11, 0xEA, 0x73, 0x40, 0x25, 0x51, 0x5A, 0x7C, + 0x7E, 0x2E, 0x3A, 0x7D, 0x53, 0xFA, 0xCE, 0xA0, 0x69, 0xCA, 0xDF, 0xD7, 0xEA, 0xB2, 0xFD, 0x28, + 0xCC, 0x08, 0xE7, 0x0A, 0xE1, 0xF6, 0x15, 0x8F, 0xE6, 0x52, 0x21, 0xCD, 0x54, 0xFC, 0x49, 0xA6, + 0x0F, 0x94, 0x03, 0x1D, 0x88, 0x4B, 0x27, 0x58, 0x8A, 0xC3, 0x76, 0x1D, 0x01, 0x6C, 0xA2, 0x34, + 0xCE, 0x70, 0x1F, 0x2F, 0xE2, 0xC1, 0x3D, 0xF0, 0x3D, 0x64, 0x81, 0xAB, 0x0C, 0x83, 0x88, 0x32, + 0x35, 0x44, 0x56, 0xD0, 0x58, 0xAC, 0x41, 0xC6, 0xCC, 0x13, 0xFC, 0x41, 0x74, 0xBC, 0xFC, 0x21, + 0xF7, 0x46, 0x08, 0xA9, 0xB0, 0x41, 0x06, 0xD7, 0xBE, 0x9C, 0xCB, 0xFC, 0x85, 0x67, 0xF0, 0xA1, + 0xFB, 0x2B, 0xF2, 0xFD, 0x65, 0x38, 0x46, 0xFA, 0xA5, 0x4B, 0x91, 0x7B, 0xCF, 0x45, 0x09, 0x7C, + 0x1C, 0xC0, 0xC7, 0x7D, 0xF5, 0xCA, 0xF3, 0xF8, 0x0E, 0x1B, 0xB8, 0xC2, 0xE6, 0xBB, 0xEF, 0xBE, + 0x1B, 0x7D, 0x17, 0x8D, 0x0E, 0xE5, 0xF6, 0xE5, 0x10, 0x16, 0x38, 0x31, 0xF1, 0xF3, 0x83, 0x6F, + 0x33, 0xF4, 0xDD, 0x1B, 0x87, 0xCB, 0x2B, 0xCF, 0x93, 0xE3, 0xA8, 0x67, 0xF0, 0xEF, 0xF9, 0xE7, + 0x22, 0x04, 0xE5, 0x8F, 0xF1, 0x69, 0x37, 0x80, 0x98, 0xCB, 0x5F, 0x79, 0x1E, 0x77, 0xCB, 0xCF, + 0x0F, 0xF0, 0xF1, 0xF0, 0x5F, 0xA3, 0x57, 0x9E, 0x7F, 0x2E, 0x8C, 0xE8, 0x45, 0x2F, 0x75, 0x92, + 0x8F, 0x55, 0x81, 0x5D, 0xD2, 0x57, 0x9E, 0x27, 0xA2, 0x49, 0xC9, 0x5F, 0x81, 0x57, 0xA8, 0xE9, + 0xC4, 0xE2, 0xAF, 0x3C, 0x1F, 0x3E, 0x3F, 0x48, 0x5F, 0x79, 0xFE, 0x83, 0x70, 0x30, 0xCD, 0x67, + 0x03, 0xF8, 0xEB, 0xCF, 0x9E, 0x7F, 0x99, 0x56, 0x8D, 0x25, 0x83, 0x4F, 0xCF, 0x0C, 0x45, 0x85, + 0x4B, 0x04, 0xD3, 0xF6, 0x13, 0x78, 0x1F, 0xFB, 0x24, 0xE6, 0x35, 0x07, 0xC7, 0xD3, 0x2B, 0x7E, + 0x58, 0x3B, 0x24, 0x08, 0x3B, 0xA4, 0x40, 0xC7, 0x5A, 0x63, 0x06, 0x7D, 0x68, 0x10, 0x1E, 0xED, + 0xEB, 0x09, 0x7F, 0xDC, 0x92, 0xC0, 0x13, 0xAD, 0x54, 0xF4, 0x1B, 0x92, 0x51, 0x67, 0xD7, 0xE7, + 0x19, 0x62, 0x2E, 0x9B, 0xDD, 0xFF, 0xFE, 0x33, 0x77, 0xCA, 0xFE, 0x8B, 0xE2, 0x9C, 0x9F, 0xB2, + 0x23, 0xBE, 0x00, 0x42, 0x4C, 0xA6, 0xD7, 0x8B, 0xF0, 0x9B, 0x46, 0x5E, 0x79, 0x26, 0x78, 0xE6, + 0xCB, 0xA1, 0x32, 0x08, 0xA1, 0x6D, 0xA8, 0x79, 0x3C, 0x51, 0x42, 0xAA, 0xA4, 0x41, 0x26, 0xEB, + 0x14, 0x5B, 0x26, 0xE7, 0x47, 0x42, 0x97, 0x78, 0x5F, 0x4A, 0x1F, 0x88, 0x61, 0x00, 0x96, 0xF5, + 0x65, 0x41, 0x29, 0xF1, 0x51, 0xD7, 0xF2, 0xA8, 0x4D, 0xD4, 0xF5, 0x3B, 0x9E, 0xCA, 0xC7, 0x53, + 0x96, 0x61, 0x90, 0xCC, 0x3D, 0xDF, 0xBA, 0x9E, 0xDE, 0x2E, 0xF1, 0x56, 0x7B, 0x79, 0x19, 0x4A, + 0x77, 0x28, 0xF9, 0x5A, 0x41, 0x3F, 0xA1, 0x53, 0x09, 0x34, 0x7E, 0x35, 0xA4, 0x2A, 0x6B, 0xB8, + 0x81, 0xC6, 0x74, 0x20, 0x7C, 0xF6, 0xC9, 0x21, 0xEA, 0xE2, 0xD9, 0xD7, 0xE2, 0xA3, 0x69, 0x3E, + 0x5D, 0xA7, 0xD1, 0xE9, 0x19, 0x0C, 0x37, 0xDD, 0x13, 0xF9, 0x28, 0x2F, 0x67, 0x42, 0x82, 0xFE, + 0xC8, 0x4E, 0x37, 0x52, 0x3A, 0x01, 0x87, 0x05, 0x2B, 0xD8, 0xF9, 0x7C, 0x3A, 0x52, 0xB0, 0x87, + 0x8F, 0xE0, 0x2F, 0x71, 0xFB, 0x27, 0x8A, 0xEB, 0xA6, 0xF0, 0x11, 0x3C, 0x92, 0x75, 0x6D, 0xF5, + 0x85, 0x40, 0xE8, 0x02, 0x7B, 0x3D, 0x0F, 0x7A, 0x92, 0x6E, 0x41, 0xBA, 0xE0, 0x47, 0x12, 0x20, + 0x60, 0xBF, 0x94, 0x4F, 0xC3, 0xA7, 0xE6, 0xB3, 0x15, 0xC2, 0x34, 0x07, 0xA5, 0x04, 0x7E, 0xEE, + 0x51, 0xA7, 0x5F, 0xB1, 0xA7, 0x4C, 0xA0, 0x5F, 0xC0, 0x67, 0x92, 0xAE, 0x49, 0xAC, 0xEE, 0x61, + 0x0B, 0xFF, 0x85, 0xE5, 0xF8, 0xB9, 0xA1, 0x46, 0xFF, 0xC7, 0x01, 0x8C, 0x58, 0x07, 0xF9, 0xAC, + 0xC7, 0x3F, 0xBF, 0xDF, 0x83, 0x57, 0xCF, 0x13, 0xED, 0x7C, 0x54, 0x43, 0xFE, 0xD1, 0x9D, 0x6E, + 0x88, 0xFF, 0x39, 0x74, 0xBA, 0xE0, 0xD6, 0x4F, 0xD8, 0xE3, 0x46, 0xB4, 0xC7, 0x85, 0x4E, 0x7E, + 0x9C, 0x47, 0x83, 0x28, 0x37, 0x75, 0xB9, 0x2D, 0x76, 0xE1, 0xBE, 0xF2, 0xC7, 0x1A, 0xC5, 0x9F, + 0x0C, 0xE0, 0xDF, 0x62, 0xB0, 0x60, 0x46, 0x49, 0xB2, 0x32, 0x81, 0xFB, 0xD2, 0x4A, 0x3C, 0x64, + 0x20, 0xC6, 0xA9, 0xCD, 0xE1, 0x20, 0xBC, 0xD2, 0x98, 0xB0, 0x5E, 0x94, 0x22, 0xF0, 0x5C, 0x80, + 0x3D, 0x58, 0x4A, 0xEE, 0xE7, 0x31, 0x7F, 0xF8, 0xE8, 0xEB, 0x2F, 0x71, 0x84, 0xDD, 0xC2, 0xC1, + 0x8A, 0xBC, 0x8F, 0x35, 0x2D, 0xC8, 0x51, 0x30, 0x20, 0x9F, 0xF9, 0x6D, 0x62, 0xD3, 0x16, 0x2E, + 0x3F, 0x08, 0x59, 0x74, 0x67, 0xCC, 0xD3, 0x1B, 0xAE, 0x26, 0xFE, 0x52, 0xA7, 0xBE, 0x69, 0xB5, + 0x54, 0xDF, 0x65, 0x30, 0x11, 0x50, 0x6E, 0x5E, 0x62, 0x3D, 0x0D, 0xCF, 0x1F, 0xD8, 0xBA, 0x9D, + 0x50, 0xB9, 0x28, 0xC9, 0x27, 0x65, 0xFF, 0xD0, 0x06, 0xB7, 0x3B, 0x79, 0x54, 0x3B, 0x39, 0x86, + 0xBF, 0xE7, 0xC2, 0x85, 0xF0, 0x88, 0x2B, 0x3C, 0xF7, 0x1E, 0x2E, 0xB5, 0xF6, 0x32, 0x76, 0x68, + 0x58, 0x8F, 0x7A, 0x04, 0x2C, 0x03, 0x7B, 0xA8, 0x8C, 0x65, 0xF5, 0xC2, 0x4F, 0xB8, 0x18, 0x68, + 0x33, 0xB6, 0x3E, 0x16, 0xD5, 0xE9, 0xF5, 0xCE, 0xE3, 0x2F, 0x82, 0x9A, 0x48, 0x80, 0x1A, 0xDA, + 0x61, 0x69, 0x76, 0x97, 0xC5, 0x77, 0xEA, 0x97, 0xA4, 0x89, 0x4A, 0xDD, 0x3E, 0x7F, 0xB5, 0x9F, + 0xB9, 0x81, 0x29, 0x40, 0x8A, 0xF9, 0x04, 0x6F, 0xE9, 0xD4, 0x60, 0xB6, 0x88, 0xB3, 0x3E, 0xED, + 0xD9, 0x70, 0xB3, 0xD1, 0x38, 0x5E, 0xC3, 0xC8, 0x09, 0xEE, 0xF6, 0x78, 0x20, 0xDB, 0xBA, 0xF8, + 0x72, 0x2A, 0x36, 0x7E, 0x1A, 0x0F, 0x4D, 0xCB, 0xFC, 0x02, 0x8F, 0x06, 0x37, 0x5E, 0xC2, 0x69, + 0x1E, 0xCF, 0xE0, 0x18, 0x41, 0x72, 0xDC, 0xC1, 0x69, 0x56, 0xE3, 0x06, 0xB9, 0x3C, 0x55, 0x3C, + 0xC2, 0xDB, 0x38, 0x16, 0x5E, 0x8D, 0xDB, 0x96, 0x6E, 0x9F, 0x70, 0x52, 0x27, 0x08, 0xC2, 0x1C, + 0xBE, 0x19, 0x18, 0xE9, 0x79, 0xC2, 0x15, 0x22, 0x79, 0x06, 0x31, 0x4D, 0x8F, 0x74, 0xBF, 0x69, + 0x03, 0x28, 0x3C, 0x50, 0xC5, 0xE3, 0x60, 0xCF, 0x75, 0x3C, 0x25, 0xD5, 0x81, 0xE5, 0xA6, 0x29, + 0xBF, 0x33, 0x7F, 0x61, 0xF2, 0x85, 0x61, 0x05, 0xD5, 0x97, 0x41, 0xC9, 0xAA, 0x9A, 0x95, 0xAC, + 0x5B, 0x89, 0xAF, 0x60, 0xD1, 0xCF, 0xE2, 0x69, 0xFB, 0x6C, 0xBA, 0x65, 0xED, 0xD6, 0xEB, 0xDA, + 0x64, 0x44, 0x24, 0x7C, 0x4C, 0x7B, 0x3E, 0x7F, 0xE0, 0xF6, 0xCA, 0xC3, 0x9F, 0x54, 0x0D, 0xD3, + 0xAA, 0xD3, 0x0B, 0x7B, 0x01, 0x7C, 0x4B, 0x2B, 0xF5, 0xDD, 0xE5, 0x69, 0x61, 0x55, 0xCF, 0x35, + 0xF4, 0xD6, 0x5C, 0xCF, 0xD2, 0xC9, 0x06, 0xB5, 0x72, 0x28, 0xCF, 0xFB, 0x3D, 0x86, 0x06, 0x90, + 0x03, 0x8A, 0x1B, 0x22, 0x9C, 0x06, 0xB8, 0x1E, 0xCB, 0xF2, 0x6A, 0xA6, 0xEE, 0x4A, 0xCA, 0x1D, + 0x08, 0x3A, 0x03, 0xDD, 0xB5, 0xF4, 0x10, 0xA0, 0x9F, 0xED, 0x7E, 0x7D, 0xBF, 0x13, 0x03, 0xF3, + 0x7F, 0x9B, 0x07, 0xBB, 0x80, 0xE8, 0x5D, 0xEE, 0xAB, 0x36, 0x04, 0x4E, 0xE8, 0xEB, 0xB7, 0x0D, + 0x8F, 0xBB, 0x7C, 0xB6, 0x2E, 0xFD, 0xF7, 0x73, 0xD3, 0xB6, 0x01, 0xBF, 0x91, 0xD0, 0x02, 0x07, + 0xBA, 0x60, 0x71, 0xBA, 0x9E, 0xAE, 0x53, 0x0B, 0x0F, 0x6E, 0x11, 0xB7, 0xF2, 0x64, 0x95, 0x09, + 0x7A, 0x68, 0xFD, 0xEA, 0x50, 0x85, 0xE0, 0x55, 0x69, 0x37, 0xC1, 0xD3, 0x33, 0xF9, 0x8A, 0xA9, + 0xB8, 0xC7, 0x12, 0x89, 0x7B, 0x33, 0xEA, 0xC5, 0x3B, 0x53, 0xB5, 0x0E, 0x3A, 0x39, 0xD1, 0x47, + 0x3C, 0xF4, 0x26, 0x4D, 0x76, 0x59, 0x0E, 0x2D, 0x2F, 0x6F, 0x38, 0x04, 0xC2, 0xEE, 0x82, 0xC1, + 0xE0, 0xFA, 0xF2, 0x93, 0xD8, 0x92, 0xCD, 0x75, 0x37, 0xEC, 0xD6, 0x5D, 0xDC, 0x45, 0x8B, 0x6E, + 0xCB, 0x46, 0x3D, 0x10, 0x59, 0x72, 0xD0, 0x35, 0xE5, 0x20, 0xE7, 0x09, 0x46, 0xFB, 0x8A, 0xF6, + 0x6A, 0xBC, 0xB4, 0xB6, 0xC3, 0xCD, 0x60, 0x00, 0xB5, 0x80, 0xDB, 0x23, 0x5D, 0x82, 0x41, 0x54, + 0x06, 0x9D, 0xCF, 0x76, 0x39, 0x97, 0xC5, 0x2B, 0xD6, 0x9A, 0x00, 0x00, 0x5C, 0x87, 0xEF, 0xF6, + 0x43, 0x69, 0xB9, 0x16, 0xDE, 0xBF, 0x35, 0x2E, 0x07, 0x43, 0x72, 0xB7, 0x8A, 0x64, 0x78, 0xE4, + 0x75, 0x0B, 0xF2, 0x47, 0xCA, 0x50, 0xC7, 0x32, 0xDE, 0x23, 0xC4, 0x93, 0x84, 0x27, 0xE7, 0x4C, + 0x83, 0xEB, 0xC9, 0xF9, 0xB8, 0xB4, 0xC5, 0x0F, 0xCA, 0x2A, 0xC1, 0x6C, 0xCA, 0x83, 0x64, 0x55, + 0xE1, 0x1B, 0x30, 0x5F, 0x18, 0x2D, 0xA6, 0x43, 0x1A, 0x45, 0x00, 0x17, 0xE8, 0xF3, 0x17, 0xE8, + 0x7E, 0xB1, 0x90, 0xF4, 0xD9, 0x0B, 0xA3, 0x80, 0x24, 0x39, 0x34, 0x16, 0xAD, 0xEF, 0xAE, 0x26, + 0x63, 0x45, 0x5B, 0x4C, 0xF6, 0x27, 0x2A, 0xA3, 0xB6, 0xFA, 0xEF, 0x5C, 0x35, 0x99, 0x15, 0xFB, + 0xE4, 0x6A, 0x55, 0x1D, 0xAF, 0x5B, 0x2F, 0xB0, 0x21, 0xAB, 0xFD, 0xE4, 0x8A, 0xB0, 0x6A, 0x05, + 0x2B, 0x59, 0x08, 0xE6, 0xDB, 0xC2, 0xBE, 0xAD, 0xC4, 0x59, 0x01, 0x7A, 0x25, 0x8B, 0x9B, 0xD5, + 0x20, 0x01, 0x3B, 0x08, 0x3F, 0x90, 0x4E, 0x3C, 0x78, 0xEE, 0x41, 0xB8, 0xF4, 0x53, 0xAD, 0x49, + 0x0F, 0x5B, 0x99, 0x40, 0xDF, 0x6A, 0xFB, 0x1E, 0x5F, 0xB8, 0x0A, 0xD5, 0x44, 0x12, 0xAB, 0x01, + 0x99, 0xBE, 0xEA, 0x12, 0xD5, 0xD4, 0xA1, 0x4E, 0xDD, 0xCA, 0x8F, 0x3E, 0x0C, 0x2E, 0x5A, 0x54, + 0x34, 0x21, 0xDB, 0xEC, 0xF8, 0x43, 0x1E, 0xEA, 0xE6, 0xF7, 0x02, 0xD7, 0xB5, 0x7C, 0xB7, 0x83, + 0x4A, 0x7D, 0xC0, 0x57, 0x32, 0x56, 0x23, 0xCD, 0x69, 0xDB, 0xA8, 0xEB, 0xB7, 0x21, 0x2B, 0x6B, + 0x9C, 0x6F, 0x5F, 0x25, 0xAB, 0x03, 0x4A, 0xA2, 0xDC, 0x48, 0x6A, 0xAE, 0xD4, 0x42, 0x5A, 0xA6, + 0x6E, 0xD1, 0xFB, 0x1F, 0xA9, 0x11, 0x59, 0x55, 0x05, 0x15, 0x55, 0x4B, 0xD4, 0x45, 0xE5, 0xB9, + 0x92, 0xA8, 0x5A, 0xA6, 0x2E, 0xEA, 0x93, 0x3C, 0x7B, 0xE5, 0x6C, 0x24, 0x45, 0x2C, 0x65, 0x0B, + 0x57, 0xD3, 0x9C, 0x80, 0xE0, 0x68, 0x17, 0x57, 0xF8, 0xD1, 0xED, 0x2C, 0x8D, 0xE9, 0x6B, 0xD5, + 0xA3, 0xB4, 0x3C, 0x1C, 0x61, 0xE8, 0x8F, 0xE7, 0xF0, 0xDB, 0x6D, 0x8E, 0xC8, 0x5B, 0xD3, 0xFC, + 0x8D, 0xF3, 0x17, 0xF6, 0x2F, 0x45, 0x43, 0xAF, 0x9B, 0x8D, 0xF6, 0xDA, 0x3A, 0x49, 0x50, 0x99, + 0xAB, 0xA9, 0x05, 0xDA, 0x9F, 0x87, 0x6C, 0x15, 0x02, 0x77, 0x4A, 0x54, 0xF4, 0xE6, 0x49, 0x50, + 0xF9, 0x55, 0x7D, 0x25, 0x53, 0xD2, 0x4D, 0x91, 0xC7, 0x41, 0xAB, 0xC9, 0xD0, 0x6B, 0x63, 0x60, + 0x30, 0xC3, 0x9F, 0xAC, 0x12, 0x33, 0xE9, 0x26, 0x2B, 0x6B, 0x21, 0x4C, 0x55, 0xB1, 0x34, 0x22, + 0x39, 0x54, 0x77, 0xC0, 0x3B, 0xFE, 0x52, 0xDA, 0x06, 0xA1, 0x14, 0x47, 0x3C, 0x4D, 0x84, 0x0A, + 0x34, 0x52, 0x9E, 0xCA, 0x89, 0x6F, 0xD2, 0x7A, 0x3C, 0x7E, 0x48, 0x97, 0x6B, 0x84, 0x13, 0x6D, + 0x0A, 0xC9, 0x79, 0x8F, 0x8A, 0x0C, 0xD2, 0xFD, 0xB1, 0xE2, 0xA3, 0x3F, 0xEE, 0x2F, 0xCE, 0x04, + 0x4B, 0xF8, 0x3A, 0x79, 0x94, 0x15, 0x36, 0x0F, 0xFA, 0x09, 0xEB, 0x06, 0xC4, 0xA7, 0x0F, 0x62, + 0xE5, 0xFA, 0xA5, 0x67, 0x7A, 0xBC, 0x4A, 0x52, 0x05, 0x86, 0x00, 0xEE, 0xB8, 0xBB, 0x9E, 0xAC, + 0xC7, 0x85, 0x53, 0x2F, 0x88, 0xA6, 0xDE, 0x02, 0x0E, 0x29, 0x59, 0x0E, 0x6D, 0x40, 0xCA, 0x3C, + 0xEF, 0x07, 0xC5, 0x5C, 0x1E, 0xCF, 0x91, 0xF5, 0xF5, 0x46, 0xDF, 0x71, 0x6F, 0x45, 0xE9, 0x60, + 0x65, 0x2E, 0xAE, 0x53, 0xE9, 0xAC, 0xE4, 0x07, 0x12, 0xE4, 0xA7, 0xBE, 0x9A, 0xEA, 0x55, 0x4E, + 0x12, 0x15, 0x65, 0x5B, 0x7B, 0xBC, 0xD4, 0x4C, 0xFC, 0x2E, 0x39, 0xFD, 0xB9, 0x39, 0x76, 0xCD, + 0xE2, 0x1F, 0xBA, 0xFA, 0x0E, 0x53, 0x31, 0xB2, 0xF6, 0xCE, 0x37, 0xF5, 0xF8, 0x9A, 0x3B, 0x95, + 0x90, 0x3D, 0x04, 0x35, 0x8A, 0x06, 0xB3, 0xD1, 0x3C, 0x7A, 0x7F, 0x34, 0xC3, 0xD7, 0x0F, 0x47, + 0x89, 0x3F, 0x9A, 0xC0, 0xA3, 0x12, 0xE3, 0xC9, 0x68, 0x32, 0x83, 0xDF, 0x13, 0xB8, 0x0A, 0x36, + 0xF0, 0x47, 0x8B, 0x1C, 0x7E, 0x0D, 0xF0, 0xD7, 0x08, 0xB2, 0xA3, 0xD1, 0x22, 0x19, 0xCD, 0xFC, + 0xD1, 0x2C, 0x82, 0x24, 0xF8, 0x1D, 0xCE, 0xE1, 0x37, 0x5C, 0x9B, 0x0B, 0xA7, 0xD0, 0x00, 0x8F, + 0x19, 0x66, 0x01, 0xD3, 0x57, 0x28, 0x45, 0x58, 0xC1, 0xDF, 0xEF, 0x7F, 0xAF, 0xE7, 0xAB, 0xEA, + 0x17, 0xF0, 0x98, 0x1D, 0x83, 0x05, 0xB5, 0xA4, 0xED, 0x81, 0x2B, 0x4A, 0x5E, 0x53, 0x47, 0x8C, + 0xD8, 0x98, 0x6A, 0x69, 0x7E, 0x3A, 0xD8, 0x71, 0x70, 0x02, 0x4A, 0x68, 0x74, 0x85, 0xD6, 0xA7, + 0x0C, 0x78, 0x94, 0xD0, 0x45, 0x6B, 0xA5, 0xA6, 0xE6, 0x99, 0x4C, 0xBD, 0x46, 0xAF, 0xCE, 0x33, + 0x4B, 0x7E, 0xEB, 0xE7, 0x80, 0x8D, 0xA1, 0x21, 0xE4, 0xAD, 0x66, 0x6E, 0x6B, 0x04, 0xF9, 0x01, + 0xAC, 0x96, 0xF8, 0xE1, 0x44, 0x2D, 0xE1, 0x63, 0x66, 0x46, 0x10, 0x9B, 0x3F, 0xDB, 0x35, 0x6C, + 0x9E, 0xE4, 0x82, 0xCF, 0x9E, 0x53, 0x09, 0xFE, 0xD8, 0x57, 0xB7, 0x60, 0x6D, 0xB5, 0xF5, 0x2F, + 0xCD, 0x6B, 0x56, 0x5A, 0x66, 0x42, 0x1E, 0x6B, 0xFA, 0x83, 0x37, 0x7D, 0xFF, 0x8E, 0xCF, 0x58, + 0x7A, 0xBF, 0xB3, 0x7E, 0xFF, 0x10, 0xB8, 0x6B, 0x7B, 0x64, 0xD4, 0xA3, 0xBF, 0x68, 0xA0, 0xD5, + 0x4F, 0x01, 0xDB, 0x9F, 0x23, 0xF5, 0xFA, 0x03, 0x1C, 0x75, 0x51, 0x27, 0x7C, 0xA3, 0xA4, 0xBF, + 0x33, 0xBC, 0x29, 0x7B, 0xD5, 0x5D, 0x40, 0xC1, 0x29, 0x1D, 0xD0, 0xC2, 0xC4, 0x96, 0xED, 0x85, + 0xBB, 0xB2, 0x37, 0x6C, 0x9D, 0xBB, 0x57, 0x45, 0x6C, 0xE4, 0x5A, 0x11, 0x35, 0xE8, 0xEF, 0x63, + 0x3B, 0xFD, 0x41, 0x15, 0x47, 0x29, 0xF4, 0x82, 0xB7, 0x36, 0x6D, 0x71, 0xCC, 0xEB, 0xAC, 0xD4, + 0xC1, 0x97, 0x34, 0x80, 0xC6, 0x3D, 0x8E, 0xBC, 0x86, 0x60, 0x19, 0x67, 0x49, 0x34, 0x6E, 0x5D, + 0x89, 0x95, 0xEB, 0xCA, 0x95, 0x5E, 0x74, 0xF1, 0x66, 0x45, 0x23, 0x70, 0x91, 0x97, 0xB6, 0x9F, + 0x83, 0xD4, 0x94, 0xD0, 0x41, 0x96, 0x5A, 0xF6, 0x7E, 0xA5, 0x44, 0x0D, 0x4C, 0x2B, 0x33, 0xBF, + 0xFB, 0x2C, 0x24, 0x4D, 0xA2, 0xE9, 0x64, 0x8A, 0x59, 0xFF, 0x63, 0xE6, 0x21, 0xE1, 0xD8, 0x83, + 0x67, 0x02, 0x66, 0xCB, 0xF6, 0x79, 0x08, 0x53, 0xE8, 0x07, 0xCD, 0x68, 0x9E, 0x92, 0xDB, 0x3E, + 0x1F, 0xB1, 0xB2, 0xE9, 0xA6, 0x74, 0x9C, 0x97, 0x64, 0x7B, 0x87, 0x99, 0x09, 0x21, 0xE2, 0x70, + 0x29, 0x35, 0xDE, 0xDF, 0x61, 0x6E, 0x12, 0x84, 0xF0, 0xEF, 0xF9, 0x01, 0x1B, 0x2D, 0xE0, 0x8F, + 0x74, 0x65, 0x8F, 0x7C, 0xB6, 0x8D, 0x4C, 0xA8, 0xF8, 0xF2, 0xF3, 0x44, 0x09, 0x30, 0x9C, 0x3D, + 0x3F, 0x48, 0xAE, 0xF8, 0x17, 0x7E, 0xBA, 0x08, 0xBA, 0xF5, 0xE7, 0x5F, 0xE6, 0xE3, 0x00, 0xCB, + 0x78, 0x85, 0x0E, 0x07, 0xA6, 0xA3, 0x05, 0x74, 0xF1, 0xB3, 0xDD, 0x68, 0xF2, 0xFE, 0x0C, 0xC6, + 0x05, 0xD3, 0xA6, 0xE7, 0xD6, 0x99, 0x2F, 0x46, 0x21, 0x61, 0x3F, 0x9A, 0xD5, 0xE2, 0x71, 0x81, + 0x58, 0x1D, 0x54, 0xE2, 0x7F, 0xE2, 0x5C, 0x27, 0xDB, 0x77, 0xF7, 0x60, 0xDC, 0xB1, 0x38, 0xA9, + 0xD9, 0xBD, 0xDA, 0x3A, 0x35, 0x1E, 0x8D, 0x3D, 0xE6, 0x3B, 0xB5, 0xC7, 0x37, 0xB9, 0xBA, 0xFC, + 0x7F, 0x93, 0x39, 0x4F, 0xB6, 0xEF, 0x9E, 0xF5, 0xB4, 0x85, 0xA6, 0x81, 0xA1, 0xC3, 0x5C, 0xC4, + 0xAD, 0x8C, 0xE3, 0xDC, 0xC7, 0xA9, 0xC6, 0xFE, 0xE5, 0xFF, 0x9F, 0xFF, 0xFC, 0xD1, 0xF3, 0x9F, + 0xFF, 0x28, 0xA4, 0xFE, 0x5D, 0xE6, 0x53, 0x0E, 0x18, 0x4A, 0x89, 0x7F, 0x77, 0x08, 0x55, 0x06, + 0xE4, 0xDD, 0x40, 0xC4, 0x69, 0x9D, 0xD0, 0x48, 0x63, 0xDD, 0x73, 0x66, 0x55, 0x17, 0xD0, 0xE7, + 0x56, 0x7D, 0xAA, 0x73, 0x9C, 0x5D, 0x65, 0x7B, 0xF7, 0xF9, 0x95, 0xBB, 0x1D, 0xEF, 0x99, 0x61, + 0x59, 0x8B, 0xDE, 0x24, 0xAD, 0x1D, 0xE7, 0x58, 0xD9, 0xFE, 0xC9, 0x66, 0x59, 0xD9, 0xDE, 0x61, + 0x9E, 0x95, 0xED, 0x5D, 0x67, 0x4E, 0x75, 0xD7, 0xEC, 0x5E, 0x42, 0xE9, 0xFD, 0x9A, 0x1D, 0x66, + 0x37, 0xB9, 0x5D, 0xE6, 0x5B, 0xD9, 0xBE, 0xDF, 0xDC, 0xA9, 0xD6, 0xA1, 0x6F, 0xB9, 0x46, 0x13, + 0xF3, 0xAC, 0x0B, 0x6F, 0xB1, 0x1A, 0x9F, 0x57, 0x7F, 0xE2, 0x1D, 0x6D, 0x92, 0x92, 0xC2, 0x79, + 0xBD, 0x65, 0xDC, 0x3C, 0xE7, 0xA4, 0xBE, 0x99, 0x58, 0xC0, 0x91, 0x5C, 0x39, 0x52, 0x1E, 0xD3, + 0x6B, 0x9E, 0xC6, 0x3D, 0x57, 0xA8, 0x64, 0xF2, 0xF3, 0x27, 0xC5, 0xF5, 0x54, 0x35, 0xC1, 0xE9, + 0x00, 0x11, 0xE3, 0x43, 0x62, 0x8E, 0x7B, 0xFD, 0xA6, 0x59, 0xD9, 0x3F, 0xE4, 0x18, 0x1A, 0x68, + 0x6D, 0xFD, 0xD9, 0x33, 0x9C, 0xCA, 0x4E, 0xAA, 0x96, 0x1A, 0x55, 0x78, 0xB9, 0x80, 0xFA, 0x0D, + 0x7F, 0xB3, 0x80, 0xF9, 0x91, 0xF5, 0xE5, 0xC4, 0x85, 0x84, 0x5D, 0xB3, 0xD0, 0x0B, 0x66, 0x63, + 0x2F, 0x98, 0xD3, 0xD7, 0x40, 0x09, 0xDF, 0x51, 0xFD, 0x38, 0x23, 0xE5, 0xD7, 0x7C, 0xDB, 0x64, + 0x28, 0x4F, 0xE1, 0x54, 0xF7, 0x26, 0x65, 0x60, 0x78, 0x69, 0x41, 0x7F, 0x51, 0x01, 0x13, 0x35, + 0x07, 0x62, 0xB7, 0xCD, 0xB2, 0x9F, 0x8D, 0xDE, 0x7E, 0x7D, 0xFC, 0x46, 0xA0, 0xBC, 0xBB, 0xB6, + 0x8C, 0xDE, 0x9A, 0xBE, 0x1D, 0x4A, 0x5C, 0xA8, 0x45, 0x3A, 0x78, 0xC1, 0x4B, 0x2F, 0xCB, 0x29, + 0xE3, 0x25, 0x57, 0xA0, 0x1B, 0x4F, 0x64, 0xEE, 0x89, 0x5F, 0x68, 0xFE, 0xDD, 0x52, 0x6B, 0xFD, + 0x6F, 0xA7, 0xED, 0xA9, 0x58, 0xEC, 0x29, 0x5C, 0x59, 0x2E, 0x9E, 0x5D, 0xF7, 0x80, 0x76, 0xB9, + 0x47, 0x94, 0x81, 0xAC, 0x4B, 0x9D, 0x86, 0x1F, 0xDC, 0x7A, 0x55, 0xCC, 0x51, 0x37, 0xE4, 0xEF, + 0xD6, 0xB7, 0x45, 0x05, 0x6E, 0xCD, 0x76, 0x45, 0x38, 0x95, 0xAE, 0x8E, 0x21, 0x67, 0x25, 0xE6, + 0x38, 0xA8, 0xC6, 0x1A, 0x54, 0xBB, 0xA8, 0xC0, 0xB1, 0x75, 0x18, 0xD7, 0x26, 0x42, 0x64, 0x59, + 0xEA, 0xD4, 0xDA, 0x7E, 0x8B, 0xF9, 0x7A, 0x99, 0x04, 0xA6, 0xA7, 0x85, 0x02, 0x94, 0x36, 0x4F, + 0x0B, 0xC9, 0x89, 0xB4, 0x1E, 0x7E, 0x98, 0x3A, 0xCB, 0x9C, 0xBC, 0x85, 0xFF, 0x1A, 0x58, 0xBD, + 0xF3, 0xCE, 0xEB, 0x6F, 0x4C, 0x17, 0x8C, 0x15, 0x4F, 0x7C, 0x6B, 0xFC, 0x7A, 0x24, 0xB2, 0x6A, + 0xA2, 0xA6, 0x93, 0xE1, 0xDB, 0x6F, 0x2C, 0x97, 0x93, 0x39, 0x63, 0x28, 0xD7, 0xA2, 0x07, 0x8E, + 0x5C, 0x83, 0x27, 0x7F, 0xA5, 0x34, 0x4F, 0x52, 0x61, 0xD7, 0x8B, 0xD5, 0xD0, 0x3C, 0xD1, 0xC2, + 0x0B, 0x26, 0x70, 0xE3, 0x88, 0x3D, 0x7E, 0xB8, 0x78, 0xBA, 0xE3, 0xB5, 0x2B, 0xC0, 0xA3, 0x88, + 0xA7, 0xEA, 0x71, 0xC4, 0xF3, 0x74, 0x77, 0x7B, 0x12, 0xE5, 0x3B, 0xF5, 0xA1, 0xF6, 0xED, 0xD6, + 0x8A, 0xD3, 0xE9, 0xBA, 0xE9, 0x79, 0x6A, 0x50, 0x39, 0xE9, 0xE9, 0x32, 0x4C, 0xD6, 0x9A, 0x8B, + 0x73, 0xD4, 0x43, 0x8A, 0xCB, 0x52, 0xA7, 0xB7, 0x80, 0x12, 0x3F, 0xDD, 0x92, 0xDB, 0x91, 0x27, + 0x92, 0x8A, 0xEA, 0x93, 0xFA, 0x5B, 0xB9, 0xF0, 0x9D, 0x28, 0x7D, 0x6F, 0x48, 0xE2, 0xE2, 0xD2, + 0x01, 0x05, 0xD3, 0x79, 0xA4, 0xB5, 0x6C, 0x30, 0x99, 0x25, 0x93, 0x48, 0x8F, 0x23, 0x91, 0xB9, + 0x27, 0x7E, 0x71, 0xE9, 0x80, 0x5A, 0x6A, 0xEA, 0xE8, 0x80, 0xA0, 0x41, 0xA6, 0xE4, 0x6D, 0x37, + 0x7B, 0xFC, 0x70, 0xB1, 0x74, 0x3F, 0xB3, 0x8B, 0xCD, 0x23, 0x87, 0xA7, 0xA9, 0x71, 0xC3, 0x73, + 0xFA, 0xF5, 0x3F, 0x54, 0x33, 0x55, 0xDD, 0x68, 0x36, 0x8D, 0x36, 0x76, 0x15, 0xB8, 0x31, 0x3B, + 0x15, 0xA9, 0xA9, 0x74, 0x75, 0x0C, 0x39, 0x52, 0xA8, 0x38, 0xA8, 0xE6, 0x12, 0x28, 0x5A, 0xB3, + 0x70, 0x76, 0x3C, 0x4A, 0xC4, 0x54, 0xC7, 0x18, 0xE9, 0xF4, 0x6E, 0x72, 0xB5, 0x03, 0x3F, 0x40, + 0xDE, 0xF8, 0x06, 0x57, 0x12, 0x6F, 0xEA, 0x07, 0xD6, 0xE4, 0x44, 0xCE, 0x82, 0x46, 0x47, 0x3B, + 0xA3, 0x08, 0xAE, 0xE0, 0xD8, 0x84, 0x0A, 0xA3, 0x70, 0x0A, 0x17, 0x49, 0x84, 0x7A, 0x74, 0x34, + 0x9C, 0xBD, 0xE6, 0x13, 0xCD, 0xB9, 0xBB, 0x8E, 0x8E, 0x8E, 0x25, 0x80, 0x6E, 0x3F, 0xF4, 0xC2, + 0xF1, 0xC4, 0x1A, 0x17, 0x5C, 0x20, 0xDD, 0x97, 0x74, 0x69, 0x99, 0xFF, 0xF0, 0x04, 0x39, 0x16, + 0x9A, 0x64, 0xD5, 0x5B, 0x3A, 0xF4, 0x4B, 0xD3, 0xE9, 0x26, 0xEA, 0xB4, 0xA1, 0x2A, 0x73, 0x47, + 0x14, 0x70, 0x12, 0x4D, 0x7E, 0x35, 0xD9, 0xE4, 0xFC, 0x4D, 0xE6, 0x3D, 0x9E, 0xAF, 0x18, 0xBE, + 0xE1, 0xC5, 0xDD, 0x9E, 0xD7, 0x5D, 0x27, 0x3D, 0xA1, 0xC3, 0xB2, 0xEB, 0x3D, 0xDA, 0xB9, 0x6C, + 0x36, 0x49, 0x30, 0xAE, 0x11, 0x56, 0x4E, 0x94, 0xB8, 0xB8, 0x78, 0x3E, 0x14, 0x8B, 0xC3, 0x44, + 0xE7, 0x35, 0x0F, 0xC7, 0xBA, 0xE7, 0x8B, 0xCC, 0x3D, 0xF1, 0x8B, 0x8B, 0xFF, 0xB7, 0xD4, 0xD4, + 0xB1, 0x30, 0x18, 0xCC, 0x61, 0xEC, 0x3B, 0xF1, 0x66, 0x76, 0xF7, 0xE7, 0x52, 0xE9, 0x7E, 0x64, + 0x97, 0x9A, 0xC7, 0x01, 0x4F, 0x53, 0x43, 0x81, 0xE7, 0xF4, 0x8B, 0x06, 0x50, 0x2C, 0x8D, 0x96, + 0x9D, 0x76, 0x35, 0xA8, 0xD0, 0x16, 0x10, 0x3A, 0x95, 0xAE, 0x8E, 0x21, 0x47, 0x8A, 0x0C, 0x07, + 0xD5, 0x5C, 0x82, 0x43, 0x6F, 0x15, 0xCE, 0x8D, 0x87, 0x07, 0x4F, 0x75, 0x8C, 0x90, 0x4E, 0xDF, + 0xA6, 0xB7, 0xC1, 0xB4, 0x76, 0x2D, 0x7C, 0xB9, 0x59, 0x5F, 0xFC, 0x15, 0x99, 0xB8, 0x8C, 0x9B, + 0xD6, 0xEB, 0x30, 0x8D, 0xD6, 0xEA, 0x95, 0x3C, 0xE3, 0x30, 0x8E, 0xE6, 0x7A, 0x7C, 0x08, 0xBC, + 0x3D, 0xE1, 0xB3, 0xCB, 0xA8, 0xA9, 0xA5, 0x9E, 0xAE, 0x65, 0x73, 0x78, 0x96, 0x26, 0xF2, 0x96, + 0x73, 0x6B, 0x74, 0x70, 0x99, 0x74, 0x9F, 0xB2, 0x89, 0xDC, 0x84, 0x06, 0x4F, 0x52, 0x22, 0x83, + 0x67, 0xF4, 0x1B, 0x2F, 0x51, 0x9D, 0x14, 0x45, 0xE3, 0x69, 0xB8, 0x88, 0x26, 0xED, 0xD2, 0xDB, + 0xE3, 0x42, 0x27, 0xD2, 0x35, 0xD1, 0x33, 0xA4, 0xA0, 0xE8, 0xD6, 0xCA, 0x29, 0x26, 0x94, 0xB6, + 0xE0, 0xCC, 0x78, 0x48, 0xF0, 0x44, 0xC7, 0x71, 0x52, 0xA7, 0x33, 0x93, 0xDB, 0x82, 0xDA, 0xA2, + 0xAA, 0xB9, 0x3B, 0x88, 0xF3, 0xE0, 0x89, 0x02, 0x0F, 0xA7, 0xFE, 0x02, 0x8A, 0x6C, 0x6A, 0x3F, + 0x95, 0x13, 0xF5, 0x78, 0xE0, 0xAC, 0x3D, 0xFE, 0xD1, 0xA9, 0xAF, 0xB0, 0xD7, 0xD2, 0xD5, 0x57, + 0x04, 0x1E, 0xAC, 0x49, 0xC2, 0xFF, 0xA8, 0x2D, 0x1E, 0xB8, 0x4C, 0xBA, 0x2B, 0xE9, 0x12, 0xD7, + 0xC1, 0xC0, 0x53, 0xE4, 0x58, 0xE0, 0xE9, 0x3D, 0xFB, 0x08, 0x37, 0x5B, 0x6A, 0x82, 0x5B, 0x23, + 0x41, 0xA3, 0xD1, 0x95, 0xD0, 0xD2, 0xA5, 0x30, 0xE8, 0x54, 0xC8, 0x29, 0x0A, 0x94, 0x36, 0xE0, + 0xDC, 0x78, 0x18, 0xF0, 0x34, 0xD7, 0x7E, 0xA1, 0xC3, 0x83, 0xC9, 0x15, 0x52, 0x2D, 0x81, 0xC4, + 0x37, 0x20, 0x38, 0x0B, 0x9E, 0xC8, 0x59, 0xB8, 0xF4, 0x09, 0x41, 0x12, 0x6C, 0x42, 0x6D, 0xCA, + 0x17, 0x07, 0x28, 0x34, 0x2C, 0xE6, 0x36, 0x9C, 0xBD, 0xE6, 0x93, 0x4B, 0x7F, 0xD0, 0x52, 0x47, + 0xC7, 0x2C, 0x7A, 0xE6, 0xC1, 0x1A, 0xE1, 0xDC, 0xEE, 0xFE, 0x5C, 0x1C, 0xDD, 0x7D, 0x74, 0x59, + 0x99, 0xDB, 0xF0, 0x04, 0xD9, 0xF5, 0x9B, 0xE4, 0x7E, 0x9D, 0x00, 0x55, 0x44, 0x9B, 0xFB, 0x61, + 0xA5, 0xDB, 0x64, 0xEE, 0xEC, 0x02, 0x6A, 0x12, 0x4D, 0x7E, 0x35, 0xD9, 0xE4, 0xF5, 0x4D, 0xE6, + 0x3D, 0x4E, 0x2F, 0x99, 0xBD, 0xE1, 0xC4, 0x1D, 0xBE, 0x49, 0x72, 0x44, 0xFD, 0x4E, 0x67, 0x65, + 0x3B, 0x2A, 0xCD, 0x56, 0x86, 0x6D, 0xA3, 0x81, 0x27, 0x1A, 0xCB, 0x71, 0x7F, 0xE7, 0x84, 0x4E, + 0x17, 0xD2, 0x50, 0x52, 0xDD, 0xDF, 0x4D, 0x95, 0x78, 0xA6, 0xC4, 0xF6, 0xB5, 0x6F, 0x1F, 0xCE, + 0x7C, 0x56, 0x56, 0xBE, 0xED, 0x23, 0x1C, 0xCE, 0xBC, 0x73, 0x7B, 0xC2, 0x45, 0x32, 0xEE, 0xF3, + 0x7A, 0x9E, 0xEA, 0x1B, 0xC4, 0x75, 0x8C, 0x94, 0x4C, 0xBC, 0x3B, 0xF6, 0x85, 0xD8, 0x6E, 0xCD, + 0x40, 0x71, 0x38, 0xBF, 0xD9, 0x2B, 0xE3, 0xA7, 0xA5, 0xF6, 0x30, 0x09, 0xB5, 0xB7, 0xAB, 0x61, + 0x1A, 0x6A, 0x3D, 0x4B, 0x0A, 0x29, 0x77, 0x23, 0xB5, 0xD0, 0x4B, 0xD1, 0x78, 0x73, 0xDB, 0x21, + 0xE4, 0xDE, 0xA0, 0xD7, 0xCE, 0x83, 0x4E, 0xCF, 0xE5, 0xF1, 0x27, 0x1D, 0x03, 0xD4, 0xBA, 0x17, + 0x2C, 0x55, 0x63, 0xDD, 0x0D, 0xE1, 0x8D, 0xC8, 0x97, 0xD2, 0xA5, 0x92, 0xBD, 0x37, 0x3F, 0x68, + 0xD4, 0xE9, 0x5C, 0x3B, 0xA2, 0x8E, 0x6F, 0x20, 0x18, 0x92, 0x3B, 0x23, 0xAF, 0x09, 0xBC, 0x70, + 0x3A, 0x85, 0xB5, 0x8F, 0x05, 0xD8, 0x3B, 0x72, 0x71, 0xB3, 0xEE, 0x8D, 0x0D, 0x57, 0xF9, 0x9A, + 0xF8, 0xD3, 0x73, 0xDD, 0x22, 0x90, 0xD2, 0xBA, 0xC6, 0x60, 0xE7, 0x62, 0xBD, 0x83, 0xCE, 0x8E, + 0xC1, 0xA5, 0xD0, 0xB7, 0xEB, 0x4F, 0x69, 0xFA, 0x58, 0xA1, 0xB5, 0x04, 0x55, 0xC0, 0xA9, 0x33, + 0x0B, 0x70, 0xAB, 0xE3, 0xF9, 0x7D, 0xC8, 0xF6, 0x46, 0x74, 0x86, 0xD6, 0x38, 0x33, 0xEF, 0x95, + 0x70, 0x9B, 0xF6, 0x8A, 0x34, 0xB6, 0x4B, 0xE2, 0xBA, 0x62, 0xCC, 0xCA, 0xF5, 0xDA, 0x17, 0xB1, + 0xF3, 0xEC, 0x88, 0x32, 0xBE, 0xD1, 0xA0, 0x27, 0xBA, 0x18, 0xB9, 0x79, 0x0F, 0xC6, 0x29, 0xB8, + 0x08, 0x5F, 0x07, 0x07, 0x6B, 0x11, 0x8A, 0x87, 0x95, 0x9E, 0xE7, 0x12, 0x54, 0x94, 0xD2, 0x29, + 0xA4, 0x1C, 0xAD, 0x6A, 0xD0, 0xD4, 0x71, 0x73, 0xC4, 0x44, 0xDD, 0xAA, 0x35, 0xA5, 0x70, 0xD7, + 0xBD, 0x85, 0xBE, 0x47, 0x20, 0xA9, 0x6D, 0xAC, 0x33, 0x33, 0x07, 0x91, 0xBE, 0x95, 0x22, 0xD9, + 0xB0, 0x47, 0x08, 0x91, 0x4D, 0x14, 0xD7, 0xD5, 0x67, 0x56, 0xC8, 0x79, 0xDB, 0xA4, 0x93, 0xA1, + 0x2D, 0x78, 0xE8, 0x86, 0x84, 0x96, 0xE2, 0x62, 0xD2, 0x20, 0x82, 0x4E, 0x09, 0x26, 0x97, 0x13, + 0xA7, 0xB1, 0x0F, 0xE6, 0xDA, 0xE9, 0x44, 0x66, 0x61, 0x98, 0xE3, 0xE8, 0x19, 0x0E, 0xB1, 0x22, + 0x6C, 0x47, 0xFC, 0x2E, 0x16, 0x34, 0xA8, 0xE6, 0xB2, 0x79, 0xA2, 0x93, 0x5A, 0xD5, 0xA4, 0xD9, + 0x8E, 0xCA, 0xDA, 0x88, 0x7B, 0x44, 0x86, 0xD2, 0x8C, 0x1A, 0x2B, 0x73, 0x5C, 0xE8, 0x7B, 0x2D, + 0xDC, 0x5E, 0x3D, 0x82, 0xA2, 0xDE, 0x65, 0x71, 0x5D, 0x73, 0x66, 0xE5, 0x7A, 0xEE, 0xAB, 0xD8, + 0x78, 0x76, 0x84, 0x06, 0xDF, 0xA9, 0xD0, 0x13, 0x5D, 0x2C, 0x8B, 0x87, 0x6D, 0xC1, 0x32, 0xF2, + 0x9C, 0xA6, 0x4B, 0x8C, 0x6F, 0xA7, 0x07, 0xB5, 0x09, 0xC5, 0x03, 0x45, 0xCF, 0x73, 0x88, 0x15, + 0x46, 0xE9, 0x14, 0x2E, 0x8E, 0x56, 0x35, 0x68, 0xEA, 0xB8, 0xBB, 0x62, 0xA4, 0x6E, 0xD3, 0x9A, + 0x52, 0xB8, 0xEB, 0xDE, 0x42, 0xDF, 0x23, 0x7A, 0xD4, 0x36, 0xD6, 0x99, 0x99, 0xE3, 0x47, 0xDF, + 0x8C, 0x91, 0x6C, 0xD8, 0x23, 0x84, 0xD8, 0x36, 0x8C, 0xEB, 0x1A, 0x35, 0x2B, 0xD6, 0x63, 0xE3, + 0xA5, 0x93, 0xA5, 0x2D, 0x80, 0x9A, 0xDD, 0x0C, 0x3D, 0xCD, 0xC9, 0xB4, 0xF5, 0xE3, 0xF3, 0x2E, + 0xE1, 0x43, 0xD9, 0x76, 0x7A, 0x93, 0x5D, 0xA2, 0x26, 0x76, 0xF4, 0x2C, 0x87, 0xD0, 0xA1, 0x84, + 0x4E, 0x23, 0x32, 0x47, 0x73, 0x1A, 0x94, 0x74, 0xDB, 0x7E, 0x31, 0x11, 0xB7, 0x28, 0x4C, 0x09, + 0x9C, 0xD5, 0xB6, 0x93, 0xF7, 0x09, 0x1A, 0xB9, 0x65, 0x75, 0x5E, 0xE6, 0x98, 0xD1, 0x76, 0x6B, + 0x24, 0xE3, 0xF5, 0x08, 0x19, 0xBA, 0x4F, 0xE3, 0xBA, 0x9E, 0xCD, 0x4A, 0xF5, 0xD8, 0x99, 0xB1, + 0x73, 0x6C, 0x0F, 0x18, 0xB6, 0xDF, 0xA1, 0x27, 0x39, 0x19, 0x75, 0xB2, 0x80, 0x4E, 0x7C, 0x09, + 0xE7, 0x93, 0x3B, 0x8D, 0xC7, 0x08, 0xDF, 0x4E, 0x27, 0xB2, 0x09, 0x54, 0x47, 0x8B, 0x9E, 0xE3, + 0x10, 0x2C, 0x84, 0xCE, 0xAD, 0x97, 0x71, 0xB3, 0xA4, 0x41, 0x3F, 0xA7, 0xFD, 0x19, 0x03, 0xAD, + 0x5D, 0x57, 0x9A, 0xEF, 0xAA, 0xB1, 0x95, 0xBA, 0x4F, 0x98, 0x28, 0x2D, 0xAA, 0x33, 0xB3, 0xC4, + 0x89, 0xBE, 0x9D, 0xC3, 0xED, 0xD6, 0x23, 0x4C, 0xC8, 0x46, 0x4E, 0xDF, 0x55, 0x70, 0xD7, 0xAD, + 0x9B, 0x4E, 0x86, 0xB6, 0x20, 0xA1, 0xDB, 0x22, 0x5A, 0x8A, 0x8B, 0x41, 0xA3, 0xC8, 0x8B, 0xE6, + 0xDE, 0x24, 0x70, 0x89, 0x0F, 0xCC, 0xB3, 0xD3, 0x71, 0xCC, 0xA2, 0x30, 0x87, 0xD1, 0x33, 0x1C, + 0x62, 0x03, 0x93, 0x39, 0x75, 0x23, 0x8E, 0xF6, 0x33, 0xA8, 0xE6, 0xB2, 0x81, 0xA3, 0x93, 0x5A, + 0xD5, 0xA4, 0xD9, 0x8E, 0xCA, 0xDA, 0x88, 0x7B, 0x44, 0x85, 0xD4, 0x88, 0x1A, 0x23, 0x73, 0x44, + 0x28, 0xFB, 0x3D, 0xEE, 0x97, 0x17, 0xD7, 0x5B, 0xA6, 0xFB, 0xAF, 0x6F, 0xEA, 0xCB, 0x55, 0xD2, + 0xA2, 0xB5, 0xF6, 0xEA, 0x14, 0x70, 0x43, 0x25, 0xAE, 0x9A, 0x73, 0x50, 0xBA, 0x8E, 0x78, 0xBA, + 0x48, 0x62, 0x9E, 0xCB, 0x05, 0x6F, 0x92, 0x2C, 0xCB, 0x76, 0xA4, 0x8C, 0x7C, 0x38, 0x28, 0x2B, + 0xB3, 0x7D, 0x82, 0x73, 0x3C, 0x39, 0xCF, 0xAA, 0xE0, 0x3C, 0xE1, 0xCB, 0x13, 0x1D, 0xB8, 0xB9, + 0x89, 0x53, 0x74, 0xD3, 0x8F, 0xEC, 0xA6, 0x6F, 0x43, 0x61, 0xF3, 0xC4, 0xA5, 0xF3, 0x8B, 0x50, + 0x98, 0x97, 0xFE, 0x26, 0x14, 0x49, 0x26, 0x2F, 0x83, 0x13, 0x07, 0x1B, 0x36, 0x87, 0xFA, 0xC2, + 0xB4, 0x1D, 0xCC, 0x96, 0xC7, 0xC7, 0x4A, 0xCC, 0x96, 0x4E, 0x28, 0x69, 0x28, 0x40, 0xBD, 0xFA, + 0x74, 0xF4, 0xF1, 0x03, 0x6E, 0xA5, 0x4D, 0x0E, 0xBE, 0xB7, 0x83, 0x57, 0xDD, 0xD0, 0x5E, 0x7C, + 0x4D, 0x8C, 0xD2, 0xC0, 0x3B, 0xBA, 0xF4, 0x5D, 0x2E, 0xF7, 0xE3, 0xF9, 0x79, 0x3D, 0xBA, 0x06, + 0x4D, 0xA0, 0x78, 0xE4, 0x13, 0xDC, 0xD6, 0x4B, 0x3F, 0x90, 0x0B, 0x58, 0xE9, 0x47, 0xD3, 0xB9, + 0xC2, 0x8F, 0xDA, 0x46, 0xAF, 0x7E, 0x20, 0xAD, 0x46, 0xB3, 0x5A, 0xC5, 0x1B, 0x78, 0x27, 0xCB, + 0xFC, 0xFA, 0xA0, 0xF4, 0xC6, 0x67, 0x38, 0xD5, 0xAF, 0x25, 0xAC, 0x53, 0xF1, 0x8B, 0x8D, 0x10, + 0x1C, 0xAB, 0x67, 0x9E, 0x11, 0xCE, 0xAE, 0x04, 0x47, 0x02, 0xEF, 0x10, 0x6F, 0xA8, 0xA7, 0x2F, + 0xE0, 0xD3, 0x74, 0xDB, 0xED, 0xDF, 0xCD, 0x0D, 0xE3, 0xEC, 0x3B, 0xAE, 0xDD, 0x56, 0x44, 0xD7, + 0x06, 0x15, 0xC7, 0xD3, 0xB5, 0xD6, 0x49, 0x14, 0x7F, 0xFC, 0x28, 0x9F, 0xE5, 0x69, 0x38, 0x3E, + 0xA6, 0x39, 0x8E, 0x07, 0x0F, 0x3D, 0x44, 0xAF, 0xE0, 0xE7, 0x22, 0x43, 0x9E, 0x78, 0x59, 0xCE, + 0x48, 0xBA, 0x1B, 0x7D, 0x35, 0x56, 0x5E, 0x32, 0xB4, 0xBF, 0x5E, 0x89, 0x45, 0x82, 0x97, 0x31, + 0xAB, 0x13, 0xBD, 0xC6, 0x9F, 0xD4, 0xD2, 0x7D, 0xBD, 0x23, 0xB9, 0x4E, 0x88, 0x9F, 0x20, 0x7E, + 0x69, 0xBF, 0xAB, 0x31, 0x98, 0x9A, 0xCF, 0x9E, 0x51, 0x0C, 0xF1, 0x05, 0x3E, 0x0F, 0xC0, 0x5F, + 0x57, 0xF0, 0x5A, 0x3B, 0xDC, 0x34, 0x51, 0x7E, 0x79, 0x6B, 0x4E, 0xD1, 0xA1, 0x76, 0x93, 0x4F, + 0xC4, 0x31, 0x71, 0xA0, 0x17, 0x03, 0xDF, 0x7C, 0xCC, 0xA4, 0xB1, 0x2A, 0x49, 0x33, 0x12, 0x6A, + 0x15, 0x96, 0xFC, 0x8E, 0x66, 0x63, 0x53, 0xE1, 0xEB, 0xAA, 0x15, 0xEE, 0x90, 0x62, 0x20, 0x32, + 0x73, 0xA6, 0xF7, 0x11, 0xD0, 0x0A, 0x9A, 0xF0, 0xE4, 0x2D, 0x3A, 0x9D, 0xC3, 0xCB, 0xF8, 0xC3, + 0x9B, 0x2A, 0x69, 0xD1, 0x43, 0xAB, 0xE2, 0x5E, 0xC5, 0xAA, 0xC2, 0x4D, 0xB7, 0xAA, 0x70, 0x54, + 0xCF, 0xA0, 0xDF, 0x7C, 0xB6, 0x30, 0xE8, 0x57, 0xA4, 0xEE, 0xFA, 0x15, 0xE9, 0xBD, 0xFA, 0x15, + 0xA9, 0x9B, 0x7E, 0x45, 0x7A, 0xBF, 0x7E, 0xCB, 0x65, 0x68, 0xD0, 0x2F, 0xDF, 0xBA, 0xEB, 0x97, + 0x6F, 0xEF, 0xD5, 0x2F, 0xDF, 0xBA, 0xE9, 0x97, 0x6F, 0xEF, 0xD7, 0x2F, 0x80, 0xC3, 0x37, 0x0C, + 0x0A, 0x5E, 0x72, 0x77, 0x05, 0x2F, 0xF9, 0xBD, 0x0A, 0x5E, 0x72, 0x37, 0x05, 0x2F, 0xF9, 0x13, + 0x28, 0x38, 0x31, 0x2B, 0xD8, 0x4B, 0xC3, 0x27, 0x50, 0xD1, 0x59, 0x47, 0x77, 0x25, 0x59, 0x17, + 0x3D, 0x70, 0x40, 0x5A, 0x22, 0x19, 0xED, 0xF2, 0xB4, 0x83, 0xD1, 0xC6, 0xE6, 0xEB, 0x88, 0x1B, + 0xF6, 0x7F, 0x66, 0x5F, 0x3E, 0xBE, 0xA3, 0x0B, 0xE7, 0x44, 0xCE, 0x7D, 0x39, 0x68, 0xD5, 0xBB, + 0x4F, 0x87, 0x46, 0x71, 0xB1, 0xF4, 0xF8, 0x41, 0xF1, 0x05, 0xBB, 0xBD, 0x71, 0x6E, 0x6D, 0xED, + 0xA6, 0x8E, 0x3F, 0xD5, 0xDC, 0xED, 0xF6, 0xAD, 0x5D, 0x4E, 0x37, 0xB7, 0x5E, 0xC6, 0x62, 0xFA, + 0x46, 0xAD, 0x27, 0xB5, 0xB7, 0x6C, 0x0B, 0x59, 0xC7, 0xBA, 0x04, 0x09, 0xCD, 0x3E, 0x6D, 0x84, + 0x5B, 0x86, 0x07, 0x94, 0xB1, 0x8D, 0x30, 0x21, 0x6F, 0x24, 0x5E, 0xCD, 0x1F, 0xDE, 0x4C, 0x7D, + 0xEA, 0xC2, 0xE3, 0xBA, 0x76, 0xFA, 0x35, 0x82, 0x13, 0x3E, 0x5C, 0x6E, 0x9B, 0x74, 0x94, 0xAE, + 0xAF, 0x13, 0x71, 0x1A, 0x27, 0x6F, 0x12, 0x94, 0xE9, 0xEF, 0x38, 0xED, 0x36, 0xD0, 0x7D, 0x87, + 0x53, 0xA6, 0xD9, 0xB7, 0xF8, 0xA6, 0x2D, 0x3E, 0x25, 0xA3, 0xDC, 0xD9, 0x00, 0x5C, 0x9F, 0xA1, + 0x71, 0x23, 0xD8, 0x07, 0xC7, 0x0D, 0x7B, 0x7A, 0x57, 0x88, 0x72, 0x5B, 0xBC, 0xFD, 0x9C, 0x48, + 0x3A, 0xB2, 0xCF, 0x61, 0xA2, 0xBA, 0x02, 0x53, 0xED, 0xD4, 0x33, 0x56, 0xEC, 0x83, 0xFE, 0x6C, + 0xBF, 0x43, 0x65, 0x76, 0x32, 0x1E, 0xAA, 0xE2, 0x74, 0x77, 0x88, 0xE1, 0x08, 0x94, 0xB1, 0xA2, + 0x05, 0x5B, 0x4E, 0x91, 0x13, 0xE5, 0x75, 0x86, 0x00, 0x61, 0xD1, 0xAC, 0x37, 0x93, 0x28, 0x0C, + 0xEB, 0xA5, 0x2B, 0x39, 0x55, 0x5F, 0x90, 0x32, 0x6A, 0x65, 0xBD, 0xD9, 0x4F, 0xA9, 0x84, 0x2F, + 0x75, 0xC8, 0xE9, 0x7C, 0xBD, 0x43, 0xBA, 0x11, 0x4D, 0xB9, 0x38, 0xAC, 0xFB, 0xC0, 0x18, 0xA5, + 0x2B, 0x27, 0x13, 0x7F, 0xB9, 0xBD, 0x05, 0x8A, 0x1D, 0x8A, 0xC1, 0xB2, 0x8A, 0x3B, 0xE8, 0xCB, + 0x28, 0x4A, 0xDF, 0x3C, 0x36, 0x2C, 0x80, 0x48, 0x8B, 0x34, 0x86, 0x16, 0x56, 0xAC, 0xD0, 0x5C, + 0x65, 0xC3, 0xEB, 0x35, 0xF9, 0x9E, 0xEC, 0x60, 0x8A, 0x6A, 0x7C, 0x89, 0x96, 0x5F, 0x8C, 0xA7, + 0x37, 0x02, 0x3D, 0x70, 0x4E, 0x5E, 0x2A, 0xD4, 0xA2, 0x43, 0xE7, 0x3B, 0x90, 0xE5, 0x95, 0xAB, + 0x71, 0x28, 0xC0, 0x9C, 0xD3, 0x81, 0xB0, 0x7B, 0xB9, 0xB8, 0xD9, 0x6B, 0x65, 0xFF, 0x5D, 0x85, + 0x66, 0xFE, 0xEC, 0x22, 0x84, 0xCB, 0xA2, 0x2B, 0x75, 0x67, 0x97, 0x7A, 0x7F, 0x63, 0xEF, 0x48, + 0x77, 0x9C, 0xA7, 0x81, 0xAF, 0x52, 0x90, 0x10, 0x14, 0x36, 0x25, 0x47, 0xD3, 0xB2, 0x20, 0x10, + 0x02, 0xF1, 0x8F, 0x07, 0x00, 0x21, 0x90, 0xD2, 0x26, 0xED, 0x16, 0x92, 0xEE, 0xAA, 0xE9, 0x07, + 0x85, 0x0A, 0x9E, 0x9D, 0xF1, 0x91, 0x8C, 0x3D, 0x63, 0x27, 0x4E, 0xBB, 0x5C, 0x12, 0xDA, 0xEF, + 0xDB, 0x6D, 0x3D, 0xA7, 0xC7, 0xE3, 0xDB, 0x1E, 0x77, 0xAE, 0x1C, 0x22, 0xD9, 0xE9, 0xF6, 0x6E, + 0x29, 0xBC, 0x8D, 0xBC, 0xB3, 0x54, 0xA5, 0x17, 0x4E, 0x2A, 0x5A, 0x55, 0x61, 0x88, 0xAE, 0xB8, + 0x7C, 0xF8, 0x80, 0x1F, 0xA3, 0xAE, 0x89, 0xF7, 0x3D, 0x8A, 0x85, 0x3D, 0xA0, 0x78, 0xC9, 0xC9, + 0x1D, 0x69, 0xEA, 0x77, 0x07, 0x3F, 0xB5, 0x3E, 0x89, 0x00, 0xF9, 0x7D, 0xEC, 0xDD, 0x2B, 0x27, + 0x23, 0xCF, 0xFA, 0xF7, 0xC3, 0x30, 0xAE, 0xF2, 0xEC, 0x61, 0x4C, 0xED, 0x79, 0x5E, 0xF8, 0xC7, + 0x23, 0x70, 0x25, 0xC3, 0x0B, 0x56, 0x75, 0xC6, 0x00, 0x07, 0x64, 0x65, 0x34, 0x07, 0x1E, 0xC5, + 0x3D, 0xFA, 0xBA, 0xD5, 0x34, 0xB4, 0x33, 0x23, 0x5E, 0x43, 0xA2, 0x8C, 0x3F, 0xBC, 0x29, 0x4E, + 0xD7, 0xA1, 0x07, 0xD1, 0x7E, 0x7C, 0xD3, 0x9E, 0x0F, 0xBB, 0x5F, 0x23, 0x3D, 0xC4, 0x91, 0x38, + 0x6A, 0xAE, 0x67, 0xF1, 0xB0, 0xC2, 0xA6, 0xE9, 0xD7, 0x45, 0x49, 0x21, 0x7F, 0x86, 0x1F, 0xF9, + 0x73, 0x3F, 0x54, 0x61, 0x86, 0xC1, 0x9F, 0xE9, 0x71, 0xB3, 0x66, 0x8F, 0x33, 0x39, 0x7D, 0x33, + 0xF0, 0x09, 0xA7, 0x9B, 0xDF, 0x36, 0x0A, 0xCB, 0xB4, 0x47, 0xB5, 0xF3, 0x93, 0xF5, 0x1E, 0x15, + 0x82, 0x25, 0x07, 0xF4, 0x99, 0xB9, 0x7A, 0x8B, 0xFC, 0xB6, 0xD7, 0x8A, 0x48, 0x56, 0x23, 0xF0, + 0x80, 0x03, 0x7D, 0x56, 0x6B, 0x91, 0x93, 0x67, 0x13, 0x81, 0x07, 0xA6, 0x7A, 0x78, 0xE8, 0x01, + 0xE8, 0x43, 0x3F, 0x57, 0x09, 0x40, 0x7B, 0xF3, 0xE2, 0xC1, 0x9A, 0x3C, 0x9A, 0x55, 0x64, 0xFD, + 0x98, 0xD6, 0x1A, 0xC2, 0xC7, 0x8E, 0x4D, 0x95, 0x0F, 0xDC, 0x0C, 0xBA, 0xBD, 0x96, 0x0F, 0xC2, + 0xEC, 0x94, 0xAD, 0xB9, 0x99, 0x74, 0xA2, 0xBD, 0x39, 0x34, 0x2E, 0xB3, 0xDE, 0x07, 0xCA, 0x74, + 0x89, 0x44, 0x89, 0xB4, 0xC1, 0x97, 0xD5, 0xB6, 0x3C, 0x9C, 0xAA, 0x6D, 0x17, 0x5E, 0xEF, 0x4D, + 0x73, 0xB4, 0x9E, 0xEA, 0xC3, 0x8A, 0x4D, 0xEB, 0xBC, 0x8E, 0x46, 0x18, 0xD2, 0xF0, 0x23, 0x00, + 0xD3, 0x8C, 0xB7, 0x86, 0x5D, 0x2C, 0x42, 0x9B, 0x05, 0x24, 0x18, 0x6E, 0x1F, 0xA0, 0x1E, 0xC8, + 0xE6, 0x21, 0x48, 0x16, 0x6F, 0x27, 0x38, 0xD1, 0x2D, 0x0D, 0x86, 0xD1, 0x2C, 0x84, 0x56, 0xC8, + 0x00, 0x6D, 0x21, 0xBB, 0x81, 0xEA, 0xFE, 0x11, 0xD4, 0x32, 0x00, 0x80, 0xB5, 0x5C, 0xC7, 0xE2, + 0xE7, 0xA1, 0xFE, 0xC0, 0x76, 0xB8, 0x98, 0x8E, 0xC2, 0xE9, 0xA6, 0x8A, 0xE4, 0x27, 0xF7, 0x4E, + 0xC7, 0x07, 0xF4, 0x83, 0xFB, 0xB7, 0x92, 0xDB, 0x5F, 0x1F, 0x20, 0x32, 0x78, 0xFF, 0xB0, 0xCB, + 0x16, 0xDF, 0x3D, 0xEC, 0x41, 0xBA, 0x27, 0x16, 0xDF, 0xBD, 0xDB, 0xCD, 0x88, 0xBD, 0x70, 0xEE, + 0x2D, 0xBB, 0xE7, 0x5A, 0x3A, 0xD6, 0x67, 0x59, 0xED, 0x8A, 0x37, 0xF5, 0x59, 0xF1, 0x38, 0x17, + 0x9B, 0xD6, 0xF6, 0x3E, 0x63, 0xEA, 0xAD, 0x07, 0x92, 0x88, 0x39, 0xC3, 0x0C, 0x58, 0x25, 0x28, + 0x6B, 0x8E, 0x31, 0xCE, 0x16, 0xDB, 0xFD, 0xC3, 0xE1, 0x3E, 0xDD, 0x3E, 0xA6, 0x27, 0x4C, 0xD4, + 0xD1, 0x08, 0x98, 0xE9, 0x83, 0x56, 0x43, 0x08, 0xB5, 0x9F, 0x62, 0x69, 0x4F, 0x9A, 0x67, 0xFD, + 0x5F, 0x3D, 0xE7, 0x3A, 0x80, 0xA2, 0xCA, 0x6F, 0xD4, 0xA7, 0xCA, 0x10, 0x35, 0x6A, 0xF2, 0x80, + 0x89, 0x3F, 0x07, 0x10, 0x01, 0xB2, 0x51, 0x95, 0x13, 0x5D, 0x94, 0xF7, 0xE0, 0xD2, 0xC1, 0x9E, + 0xE5, 0x2C, 0x1F, 0xF3, 0x38, 0x5F, 0x07, 0xDD, 0x77, 0x53, 0x39, 0x9D, 0xF5, 0x7F, 0x01, 0x03, + 0x75, 0x20, 0x8B, 0x7D, 0x46, 0xDB, 0x18, 0xF6, 0xC2, 0xA1, 0xA7, 0x65, 0x88, 0x5E, 0x0E, 0x75, + 0x6D, 0x3A, 0x8F, 0xDB, 0x53, 0x62, 0xE7, 0xC6, 0x27, 0x72, 0xE0, 0xF9, 0x7F, 0x30, 0x60, 0xEA, + 0xCA, 0x71, 0x8F, 0x11, 0x36, 0x01, 0x14, 0xD8, 0x3B, 0x20, 0x47, 0xE3, 0x3F, 0xF4, 0x69, 0x06, + 0x2F, 0x6B, 0x02, 0xC2, 0x03, 0xEE, 0x2A, 0x3E, 0xAA, 0xFB, 0x3B, 0x54, 0x25, 0x61, 0x86, 0x00, + 0xC2, 0x31, 0xDA, 0x14, 0xED, 0x41, 0x58, 0x4F, 0x7E, 0x01, 0x05, 0x7F, 0xF9, 0x38, 0xE1, 0xCC, + 0x5D, 0x4A, 0xA2, 0x21, 0x88, 0x04, 0x17, 0x8A, 0xD5, 0x91, 0x42, 0x29, 0x77, 0xFD, 0xF3, 0x67, + 0xF2, 0xCB, 0x4B, 0x71, 0xAC, 0xAC, 0x85, 0x4F, 0x1B, 0x47, 0x1B, 0xDA, 0x6E, 0x86, 0xA5, 0x4A, + 0x30, 0x7A, 0xBF, 0xE3, 0x91, 0x64, 0x95, 0x37, 0x36, 0x64, 0x90, 0xEB, 0x2C, 0xD1, 0xA6, 0x3A, + 0xFF, 0x52, 0x55, 0xC7, 0xAE, 0xB5, 0x57, 0xAB, 0xA2, 0x8E, 0x87, 0xB8, 0x55, 0x62, 0xA7, 0xCD, + 0x67, 0x0B, 0xC1, 0xA5, 0x38, 0x1C, 0xAB, 0xD3, 0x03, 0x4F, 0x82, 0xA9, 0xE9, 0x9B, 0x43, 0xE9, + 0x02, 0xD4, 0x7B, 0x57, 0x6A, 0xE3, 0xC4, 0x6D, 0x1B, 0x57, 0xEA, 0xA5, 0x76, 0xA6, 0x5E, 0x6A, + 0x5F, 0x97, 0xA8, 0xD7, 0x14, 0x27, 0x5B, 0xA4, 0xCB, 0x6A, 0xB4, 0x81, 0x16, 0xA4, 0xBC, 0x5A, + 0xF6, 0xC9, 0x92, 0xD4, 0x61, 0x22, 0x4C, 0xB7, 0xC6, 0xB4, 0xEE, 0x13, 0x45, 0xA1, 0x6B, 0x9C, + 0xBD, 0x1A, 0xAC, 0xD7, 0x77, 0x8E, 0x17, 0x6F, 0xE9, 0xFC, 0x35, 0x77, 0xF4, 0x62, 0x32, 0x8E, + 0x8D, 0x09, 0x53, 0x8B, 0xC8, 0x77, 0x66, 0xA4, 0x3D, 0x43, 0xB6, 0xB6, 0x1D, 0xAA, 0x5A, 0x32, + 0x99, 0xE6, 0x63, 0x51, 0x77, 0x14, 0xC9, 0xA8, 0xC0, 0xB2, 0x66, 0x59, 0x75, 0x98, 0x17, 0x6B, + 0x47, 0xAF, 0x07, 0x7C, 0x27, 0x76, 0x18, 0x6B, 0x9D, 0x3B, 0x4B, 0xC4, 0x0E, 0xFD, 0x1D, 0xD2, + 0xC5, 0xB8, 0xFB, 0xDB, 0xD1, 0xC0, 0xD9, 0xAF, 0x11, 0xE1, 0xDA, 0xCE, 0x22, 0x1F, 0xE2, 0x10, + 0x04, 0xDD, 0x31, 0xBB, 0x7C, 0x8E, 0xA2, 0xEA, 0xF3, 0x84, 0x4E, 0xF7, 0xC4, 0x40, 0xD8, 0x9E, + 0xE3, 0x86, 0x94, 0x59, 0x74, 0xD8, 0x3E, 0x7B, 0x02, 0xAF, 0xEB, 0x96, 0x72, 0x21, 0x36, 0x67, + 0x94, 0xD1, 0xD5, 0x17, 0xF7, 0x92, 0xD6, 0xE4, 0x07, 0x7B, 0xDC, 0x4F, 0xF2, 0xE8, 0x96, 0x19, + 0xFD, 0x37, 0x6A, 0xB7, 0x27, 0x70, 0x33, 0xE8, 0x7D, 0x2F, 0x5D, 0xD1, 0xFF, 0x5C, 0x9C, 0xDE, + 0x13, 0x5B, 0xDD, 0x1A, 0xA4, 0x93, 0x1F, 0xD6, 0xF9, 0xCF, 0x4F, 0xF3, 0x7E, 0x1B, 0x25, 0xFA, + 0x75, 0xEC, 0xB4, 0x8C, 0x96, 0x51, 0x5D, 0x5E, 0x0A, 0x21, 0xBC, 0xB9, 0x62, 0x93, 0x74, 0x7C, + 0x1E, 0x5D, 0xB7, 0xA1, 0xD4, 0x33, 0xB3, 0x19, 0x20, 0x35, 0x1F, 0x2A, 0x82, 0x83, 0x20, 0xA4, + 0x92, 0x76, 0x07, 0xBB, 0x1C, 0xE4, 0x21, 0x0D, 0xC3, 0x82, 0xCE, 0x6F, 0x75, 0x92, 0x8B, 0x1D, + 0x37, 0x79, 0xBF, 0x23, 0xF5, 0xF3, 0xA1, 0x3D, 0x6C, 0x6A, 0xBF, 0x12, 0xD8, 0x12, 0x58, 0x0D, + 0xE0, 0x5B, 0x87, 0xE6, 0xE5, 0xF9, 0x74, 0x2E, 0xA0, 0xC6, 0x61, 0x03, 0xA1, 0x4A, 0x85, 0x73, + 0x22, 0x7E, 0x69, 0xF7, 0xC2, 0xFE, 0x33, 0x41, 0x84, 0x51, 0x53, 0xDE, 0x53, 0x8A, 0x4D, 0x39, + 0xB1, 0x14, 0x9B, 0xF2, 0xAE, 0x52, 0x6C, 0xCA, 0x57, 0x2D, 0xC5, 0xA6, 0xBC, 0xA5, 0x14, 0x9B, + 0xF2, 0xB5, 0x4A, 0xB1, 0x29, 0x27, 0x97, 0xA2, 0x3E, 0xF9, 0x44, 0x18, 0xD5, 0xFB, 0x7B, 0x4A, + 0xB1, 0xDE, 0x4F, 0x2C, 0xC5, 0x7A, 0x7F, 0x57, 0x29, 0xD6, 0xFB, 0x57, 0x2D, 0xC5, 0x7A, 0x7F, + 0x4B, 0x29, 0xD6, 0xFB, 0xD7, 0x2A, 0xC5, 0x7A, 0x3F, 0xB9, 0x14, 0xBB, 0xF3, 0x5D, 0x84, 0xD3, + 0xA5, 0xBE, 0xA7, 0x18, 0x2F, 0xF5, 0xC4, 0x62, 0xBC, 0xD4, 0x77, 0x15, 0xE3, 0xA5, 0x7E, 0xD5, + 0x62, 0xBC, 0xD4, 0xB7, 0x14, 0xE3, 0xA5, 0x7E, 0xAD, 0x62, 0xBC, 0xD4, 0xD3, 0x8B, 0x71, 0xE9, + 0x2E, 0xC6, 0x3B, 0xCB, 0x71, 0x7A, 0x41, 0xDE, 0x5B, 0x92, 0xAF, 0x5D, 0x94, 0x37, 0x96, 0xE5, + 0x2B, 0x16, 0xE6, 0x68, 0x69, 0x5A, 0x04, 0xF7, 0x74, 0x82, 0xD3, 0xCA, 0xEA, 0x9E, 0x72, 0x7A, + 0xCD, 0x32, 0xBA, 0xA1, 0x7C, 0x5E, 0xA9, 0x6C, 0x06, 0xCB, 0xA5, 0xC7, 0xAD, 0x45, 0x66, 0x66, + 0xF6, 0x84, 0x95, 0xEF, 0x6E, 0x3F, 0xCE, 0x87, 0x08, 0x70, 0x55, 0xCF, 0x87, 0xA1, 0x26, 0x11, + 0x53, 0x18, 0xDB, 0xA6, 0xE7, 0x84, 0x79, 0x1E, 0x44, 0xA9, 0x55, 0x0B, 0xC1, 0xF4, 0xA9, 0xB8, + 0x0E, 0x12, 0x84, 0xCB, 0x8D, 0x9C, 0x43, 0x16, 0xC6, 0x41, 0x2F, 0x96, 0x79, 0x51, 0xE9, 0xDA, + 0xD9, 0x14, 0x73, 0x6A, 0x1F, 0x70, 0x1B, 0x72, 0xE0, 0xAC, 0x0A, 0xE5, 0xE7, 0x9C, 0xA4, 0xFD, + 0xD5, 0x4F, 0x3D, 0x66, 0xE2, 0x9F, 0xF1, 0xD4, 0x63, 0xFF, 0x9A, 0xA0, 0x50, 0xF5, 0x9D, 0xF4, + 0xA3, 0xF8, 0x61, 0xA6, 0xFF, 0x41, 0x6E, 0xDE, 0x49, 0x1F, 0xC7, 0x1E, 0x7B, 0x6C, 0x0E, 0x67, + 0x71, 0x5B, 0xAC, 0x39, 0x88, 0xC7, 0x14, 0x63, 0xCF, 0x5B, 0x8F, 0xCB, 0xD9, 0xFA, 0x29, 0x4D, + 0xE1, 0x4F, 0x92, 0xAB, 0xBF, 0x69, 0x06, 0x7F, 0xE9, 0xDB, 0x85, 0x5E, 0xFB, 0xC0, 0x1C, 0x78, + 0x92, 0xD7, 0x0A, 0x82, 0x59, 0xF1, 0x30, 0x04, 0x44, 0x5F, 0xF6, 0xA2, 0x84, 0xD4, 0x33, 0x71, + 0xC0, 0x84, 0x54, 0x78, 0x5C, 0x84, 0x1D, 0xC0, 0xD2, 0xE2, 0xFD, 0x08, 0x4A, 0xF8, 0x28, 0x33, + 0x5E, 0xB3, 0xF9, 0x81, 0x23, 0x69, 0xA9, 0x00, 0x72, 0xAD, 0x53, 0x00, 0xA2, 0xD4, 0xCD, 0x2B, + 0x6D, 0x1D, 0x24, 0x0D, 0xEB, 0xB8, 0x8F, 0x4F, 0x1A, 0xC6, 0x47, 0xD7, 0x74, 0x13, 0x33, 0xA0, + 0xA2, 0x33, 0x93, 0xDA, 0xB5, 0xD1, 0x9F, 0x3D, 0x5E, 0xC7, 0x39, 0x4A, 0x62, 0x2B, 0xFE, 0xEF, + 0xAC, 0xE8, 0x42, 0xD3, 0x59, 0xF7, 0xEB, 0x9F, 0xAD, 0xEE, 0xDA, 0x48, 0x58, 0xF9, 0xA6, 0x7B, + 0x32, 0x56, 0x79, 0x84, 0x79, 0x6A, 0xBC, 0x1F, 0x83, 0xD7, 0xB9, 0x6D, 0x71, 0x2A, 0x83, 0x96, + 0xFC, 0xD9, 0x02, 0x30, 0x0E, 0xB8, 0xE3, 0x4F, 0x7E, 0x01, 0x77, 0x51, 0xE3, 0xB4, 0xCD, 0xA9, + 0x2A, 0x7E, 0x8A, 0xC4, 0xF7, 0xC0, 0x4B, 0x74, 0xCA, 0xD3, 0x02, 0xEE, 0xD0, 0xA5, 0x9E, 0x4B, + 0x74, 0x64, 0xE9, 0x39, 0x89, 0x61, 0x4F, 0x4B, 0xE5, 0xEB, 0xB3, 0xA7, 0x93, 0x75, 0xEA, 0x83, + 0xDC, 0x2A, 0x88, 0x35, 0xD6, 0xA2, 0x3E, 0xB4, 0xFA, 0xB4, 0x12, 0x6E, 0x93, 0xE3, 0x7A, 0xBE, + 0xBD, 0x91, 0xAA, 0x53, 0x39, 0xA9, 0xB9, 0x23, 0x6F, 0xB0, 0xE9, 0x4C, 0xE4, 0xD9, 0x66, 0x53, + 0xEF, 0x54, 0xEB, 0x05, 0xE3, 0x68, 0x06, 0x59, 0x9F, 0xB3, 0x7D, 0x37, 0x3F, 0xAA, 0x43, 0x8B, + 0xBA, 0x40, 0x25, 0x4C, 0xC5, 0x51, 0x0F, 0x76, 0x2A, 0x21, 0x40, 0x17, 0x3C, 0xA6, 0x10, 0xA0, + 0x8D, 0xF8, 0xAD, 0x4F, 0x22, 0x7E, 0x60, 0xA8, 0xF6, 0xC0, 0x94, 0xFD, 0x40, 0xA1, 0xEE, 0x9E, + 0x9F, 0xA1, 0xCA, 0x99, 0xA6, 0xD7, 0x25, 0x03, 0x62, 0xCB, 0x5F, 0xC9, 0x3E, 0x5D, 0xB7, 0xC6, + 0x9E, 0x74, 0x67, 0x05, 0x34, 0xEA, 0xF9, 0x70, 0xAE, 0x2B, 0xB2, 0x81, 0xBD, 0xC8, 0x11, 0xDE, + 0xBE, 0xD9, 0x48, 0x14, 0x6B, 0xFB, 0xD3, 0xE9, 0x40, 0x5A, 0xB8, 0xAC, 0x32, 0xA6, 0x39, 0x9D, + 0x58, 0xB2, 0xC5, 0x1D, 0x5A, 0xF4, 0x46, 0xAC, 0x0F, 0xF0, 0xA3, 0x75, 0x86, 0x49, 0x67, 0x02, + 0x8D, 0x76, 0x0D, 0x38, 0xE1, 0xCC, 0xEA, 0x96, 0x59, 0x55, 0xE2, 0x8C, 0x14, 0xDC, 0x40, 0x95, + 0xB2, 0x24, 0x3B, 0x7C, 0xD8, 0x5F, 0xDC, 0x33, 0x47, 0x12, 0x34, 0xCB, 0xBF, 0x63, 0xA1, 0x3A, + 0xB3, 0x12, 0xAC, 0x3A, 0x94, 0xD0, 0xB8, 0xDE, 0x4A, 0x0C, 0x14, 0x93, 0x47, 0x6D, 0xD1, 0x4F, + 0x04, 0xA9, 0x6E, 0xD9, 0x41, 0xEE, 0x9B, 0xDB, 0x4D, 0x47, 0xB4, 0xE0, 0xAE, 0x82, 0x89, 0x58, + 0x9A, 0x2A, 0x89, 0x5E, 0xA0, 0xB6, 0xB8, 0xCB, 0xBD, 0x6D, 0x9B, 0xBD, 0x9F, 0x93, 0xA6, 0x3C, + 0x34, 0xFB, 0x48, 0x38, 0x19, 0xB4, 0xC9, 0x8E, 0xEB, 0xD2, 0xC6, 0x7D, 0x1D, 0xF8, 0xDB, 0x3B, + 0x89, 0x6A, 0xE5, 0xAC, 0x2A, 0xF3, 0xC9, 0x58, 0xA9, 0xA2, 0xBC, 0x07, 0x94, 0xAC, 0x38, 0x1A, + 0x09, 0x20, 0xD0, 0xD8, 0x81, 0x46, 0x1A, 0x82, 0xF2, 0xD7, 0xB5, 0x79, 0x4E, 0x05, 0xAF, 0x7F, + 0x75, 0xDB, 0xA6, 0x9A, 0x2D, 0xD5, 0xC2, 0xD1, 0xB6, 0x66, 0x2D, 0xB0, 0xFD, 0xDB, 0x34, 0x06, + 0xBD, 0x63, 0x77, 0x55, 0xCE, 0xEF, 0x4F, 0xB0, 0x4F, 0x27, 0xFA, 0x50, 0x2E, 0x4B, 0xB7, 0x81, + 0xA2, 0x7A, 0xBD, 0x43, 0x9B, 0x02, 0x8E, 0xAD, 0x1A, 0x1A, 0xB3, 0x8D, 0xB1, 0x6F, 0xF0, 0xBB, + 0x68, 0xE8, 0x99, 0xB6, 0xDB, 0x8F, 0xBB, 0x8E, 0x72, 0x9E, 0x99, 0x75, 0xE1, 0x21, 0x18, 0x9F, + 0xFB, 0xD4, 0xCD, 0xA2, 0x45, 0xA3, 0x31, 0x55, 0xF4, 0xB8, 0x87, 0x39, 0x14, 0xE0, 0x47, 0xF5, + 0x46, 0xCF, 0xE1, 0xA1, 0x37, 0x4E, 0x63, 0x1C, 0x64, 0x55, 0x20, 0x08, 0x30, 0xEB, 0xAD, 0xB2, + 0x85, 0x59, 0x27, 0xCB, 0x46, 0xBB, 0x7A, 0x73, 0xFF, 0x3B, 0xCC, 0x80, 0xB6, 0x00, 0x3F, 0x3C, + 0x83, 0xD7, 0xBF, 0x01, 0x84, 0xE3, 0xC8, 0x80, 0xD5, 0x75, 0x04, 0x03, 0x5B, 0x2B, 0x32, 0x84, + 0x50, 0xD5, 0x7C, 0x5A, 0xB8, 0x08, 0xE7, 0xC8, 0xD6, 0x77, 0xE2, 0x29, 0xC6, 0xBD, 0xDC, 0xE2, + 0xB8, 0x7D, 0x7A, 0x3E, 0xFD, 0x4D, 0xE7, 0x17, 0x87, 0x1E, 0xB8, 0x7E, 0xB0, 0x54, 0x04, 0xF0, + 0xD4, 0x98, 0x29, 0xB4, 0x44, 0xF8, 0xC1, 0x00, 0x86, 0x22, 0xDD, 0xA0, 0x0F, 0xFB, 0x52, 0xCE, + 0xFB, 0xD3, 0x8F, 0xDB, 0x55, 0x56, 0x2D, 0x1D, 0x36, 0xAD, 0xD6, 0xBB, 0x04, 0xCC, 0xEA, 0x08, + 0xC0, 0x1E, 0x89, 0x71, 0x41, 0xCC, 0xC6, 0x04, 0xE3, 0x32, 0xF5, 0x19, 0xF2, 0xBF, 0x7C, 0x6A, + 0x9A, 0xAC, 0xE0, 0xDF, 0xBB, 0x33, 0x71, 0xC0, 0xEB, 0xD3, 0x77, 0xDF, 0x49, 0x33, 0x95, 0x47, + 0x63, 0xAE, 0x2A, 0x41, 0xD1, 0xE9, 0x4D, 0x0D, 0xD3, 0x55, 0x71, 0x9C, 0xF3, 0xB9, 0x2C, 0xD5, + 0xCC, 0x32, 0x59, 0xAC, 0x96, 0xAB, 0xD9, 0x52, 0xFC, 0x2E, 0x16, 0xF9, 0x22, 0x9F, 0x49, 0x76, + 0xB3, 0xC5, 0x3A, 0xFE, 0x68, 0x16, 0x7F, 0xFD, 0xD1, 0x2C, 0x89, 0x17, 0xE9, 0x63, 0x56, 0xE7, + 0x02, 0x21, 0x12, 0xBF, 0xD7, 0x14, 0x4D, 0xFC, 0xAF, 0xA3, 0xD5, 0xCC, 0xA4, 0x8F, 0x14, 0x3D, + 0x24, 0x47, 0x16, 0xDB, 0x58, 0x02, 0x7E, 0x63, 0x13, 0x57, 0xE5, 0x9D, 0xE2, 0xC1, 0x7B, 0xE8, + 0x95, 0xCE, 0xC5, 0xB9, 0x7A, 0x2F, 0x4A, 0x3E, 0x8A, 0xCB, 0x6A, 0xEF, 0xB2, 0xB1, 0xB6, 0xA9, + 0x5A, 0x8A, 0x7F, 0x3A, 0x89, 0x01, 0x71, 0xAC, 0xEB, 0x9B, 0xAA, 0x5D, 0x78, 0xEE, 0xC2, 0x1E, + 0x70, 0xE3, 0xAD, 0x62, 0xE3, 0xD2, 0xEC, 0xDF, 0x5D, 0x34, 0xAA, 0x8A, 0xFF, 0x87, 0x8A, 0x26, + 0xF0, 0x80, 0x8A, 0x6A, 0xCB, 0xF8, 0xA1, 0xA0, 0xBE, 0x64, 0x67, 0x8B, 0xF4, 0xB6, 0x23, 0x41, + 0x1E, 0x07, 0x08, 0x69, 0x02, 0xEC, 0xAB, 0x47, 0x29, 0xC7, 0x50, 0x6B, 0x18, 0x3D, 0x06, 0x7D, + 0xDA, 0xE7, 0xA3, 0xD5, 0x66, 0xBD, 0x0B, 0x38, 0x1E, 0xA4, 0xA3, 0x27, 0xAB, 0xF7, 0x58, 0xD3, + 0x3C, 0x7B, 0x58, 0xD8, 0xCD, 0x83, 0xEE, 0x27, 0xE9, 0x9C, 0x0E, 0x31, 0xF4, 0xB5, 0x4A, 0x67, + 0x43, 0x3F, 0xBE, 0x4E, 0x41, 0xF9, 0xE8, 0xCE, 0xEF, 0x79, 0x17, 0x9D, 0x7F, 0x7D, 0xA9, 0xAE, + 0xF7, 0x9D, 0x93, 0x1E, 0x62, 0x3D, 0x63, 0x06, 0xFD, 0x8B, 0xC6, 0xE0, 0xB6, 0x12, 0xB2, 0x99, + 0xB5, 0x35, 0x99, 0x5B, 0xF3, 0x79, 0x46, 0x50, 0x17, 0x88, 0xEA, 0x1D, 0x54, 0xA1, 0x4D, 0x6C, + 0x0C, 0x6E, 0x34, 0x27, 0x7F, 0xBF, 0x51, 0xB0, 0x47, 0xF8, 0x4B, 0xA7, 0x0C, 0x81, 0x3A, 0x69, + 0x65, 0x5E, 0xDB, 0x0E, 0x72, 0xF1, 0xC4, 0x1E, 0xEB, 0x70, 0xA4, 0x5D, 0xFD, 0xA6, 0x7D, 0x1A, + 0x52, 0x46, 0x2F, 0x1B, 0x71, 0x1A, 0x5A, 0x55, 0x9C, 0xF1, 0x30, 0xA4, 0x8A, 0x74, 0x44, 0x14, + 0xC0, 0xCD, 0xB3, 0x98, 0x16, 0x42, 0xE9, 0x5B, 0x00, 0x0B, 0xA0, 0xF5, 0xD7, 0x1E, 0x54, 0x5C, + 0xAC, 0x6F, 0x96, 0xDB, 0xD3, 0x9B, 0x66, 0x13, 0x72, 0x35, 0x46, 0x34, 0x4A, 0xF6, 0x84, 0x4D, + 0x96, 0x04, 0x3F, 0x1D, 0x8B, 0x6C, 0xA5, 0x26, 0x1F, 0xD0, 0x84, 0x2B, 0xDF, 0x3A, 0x1E, 0x27, + 0xEA, 0xAF, 0xBE, 0xC1, 0xE0, 0xB3, 0x38, 0xAB, 0xA1, 0x2B, 0xDD, 0x96, 0xE6, 0x97, 0xE3, 0x75, + 0x5F, 0x8C, 0xC7, 0x13, 0x91, 0x6B, 0x77, 0xB1, 0xF9, 0x61, 0xF6, 0xF6, 0x87, 0x6F, 0xCF, 0x99, + 0x02, 0x7A, 0xA7, 0x82, 0x46, 0x44, 0x7C, 0x29, 0x20, 0xFB, 0x72, 0x49, 0xCC, 0x36, 0x99, 0x95, + 0xA5, 0x98, 0xDB, 0x04, 0xE8, 0x2A, 0xB5, 0x5C, 0xC6, 0x87, 0xFB, 0xD6, 0xD1, 0xF5, 0x90, 0xEB, + 0x42, 0x81, 0x2D, 0xB9, 0xBE, 0x42, 0xF1, 0xCF, 0x8E, 0xCE, 0x83, 0x7B, 0x62, 0xB4, 0x10, 0xEB, + 0x7B, 0x11, 0x46, 0x3B, 0xDD, 0x4F, 0xAC, 0x5B, 0x47, 0xBE, 0xE0, 0x13, 0xEE, 0x2B, 0x26, 0x06, + 0x5B, 0xDA, 0x53, 0x87, 0xB2, 0xBD, 0xB5, 0xEF, 0x16, 0x82, 0x95, 0x57, 0xF3, 0xE9, 0x24, 0x6A, + 0xC5, 0x6F, 0x06, 0x4B, 0x98, 0xE9, 0x9E, 0x26, 0x36, 0x6A, 0x1F, 0x70, 0xB3, 0x44, 0x5B, 0xC4, + 0x4E, 0x44, 0xFE, 0x18, 0x57, 0xC0, 0x90, 0x10, 0x70, 0x83, 0x2B, 0xFC, 0x82, 0x8F, 0x61, 0x7D, + 0x5C, 0x58, 0xCD, 0xD6, 0xC6, 0x61, 0x73, 0x85, 0x41, 0x9B, 0x50, 0x53, 0xA1, 0xB0, 0xC1, 0x87, + 0xB3, 0x83, 0xA1, 0x02, 0xEA, 0x62, 0x98, 0x7F, 0x40, 0x0F, 0xE6, 0xC0, 0x30, 0xDB, 0x0B, 0x71, + 0x88, 0xCF, 0x95, 0x67, 0x95, 0xE5, 0x64, 0x41, 0x67, 0xEE, 0x6E, 0x0E, 0xB7, 0xDB, 0x24, 0x1B, + 0x31, 0x49, 0xE6, 0x90, 0x76, 0xBB, 0x81, 0x32, 0x8F, 0x7D, 0xFC, 0xE2, 0xDA, 0xC6, 0x60, 0x1C, + 0x18, 0x0B, 0x96, 0x30, 0xB8, 0xC3, 0x3A, 0xE9, 0x98, 0xC3, 0x70, 0x69, 0x77, 0x58, 0x27, 0x75, + 0x5B, 0x87, 0x8A, 0xDB, 0x14, 0xE5, 0xDE, 0x13, 0xC3, 0x09, 0xAB, 0x4C, 0x2E, 0xCC, 0xB3, 0xCA, + 0x6D, 0xF3, 0xAC, 0xE1, 0xBB, 0x15, 0x41, 0x68, 0x1D, 0xC7, 0xE4, 0xAA, 0x06, 0xD6, 0x4F, 0x7E, + 0xA5, 0xCB, 0x71, 0xA5, 0x86, 0xDE, 0x30, 0xD8, 0x40, 0x13, 0x2F, 0x18, 0xF2, 0x6D, 0x4C, 0x54, + 0x5D, 0x45, 0x6E, 0x22, 0xA7, 0xAF, 0x36, 0xE7, 0xE3, 0x4C, 0x81, 0x1D, 0x5D, 0x21, 0x5E, 0x72, + 0x2E, 0x6A, 0x90, 0xC7, 0x31, 0xF8, 0x9E, 0x99, 0x63, 0x48, 0x32, 0xFD, 0xA6, 0x89, 0x96, 0x27, + 0xA7, 0x52, 0xF8, 0x4C, 0x8C, 0xDE, 0x1F, 0xD5, 0x40, 0x55, 0xA6, 0xC4, 0xA8, 0x1D, 0x0C, 0xF2, + 0xD8, 0x1C, 0x5A, 0x79, 0xA6, 0x8D, 0x9C, 0x96, 0x93, 0x8E, 0xCE, 0xB1, 0x66, 0x2A, 0xDE, 0x41, + 0xFD, 0xDC, 0x56, 0xA3, 0x9B, 0x20, 0x7D, 0x77, 0x87, 0xB9, 0xD7, 0xF5, 0x22, 0x41, 0xE6, 0xF4, + 0x59, 0xD0, 0xF8, 0xA3, 0x65, 0xFA, 0xF8, 0x91, 0xA3, 0x45, 0xDE, 0xEE, 0xAA, 0x94, 0x35, 0xCA, + 0x9B, 0x55, 0xB9, 0xDC, 0x55, 0x84, 0xD5, 0xCC, 0xC8, 0x78, 0xCF, 0x76, 0x95, 0xE5, 0xEB, 0x42, + 0x23, 0x3A, 0x1E, 0x46, 0x4C, 0x96, 0xAB, 0xE5, 0xC6, 0xD5, 0x63, 0xA6, 0x55, 0x56, 0xB1, 0xD7, + 0x28, 0xB2, 0x72, 0x55, 0x7E, 0x64, 0x33, 0xF3, 0xC8, 0xCD, 0x96, 0xD9, 0x47, 0xD9, 0x56, 0xA3, + 0xD2, 0x67, 0xE2, 0xE2, 0x5D, 0x9E, 0x64, 0xA9, 0x43, 0x6A, 0x99, 0x54, 0xEB, 0xB2, 0xA4, 0x79, + 0x2D, 0xCA, 0xCD, 0x96, 0xB2, 0x72, 0xE6, 0x75, 0xBB, 0x4C, 0xD2, 0x4E, 0x3D, 0xFB, 0x5D, 0xAD, + 0x3C, 0x4F, 0x56, 0xB1, 0xD3, 0xB8, 0xBB, 0xE5, 0x6E, 0xCB, 0x8C, 0x5B, 0xED, 0x76, 0x1B, 0x93, + 0x8F, 0x53, 0xDA, 0x12, 0x8C, 0x57, 0x6A, 0x2C, 0xFA, 0x60, 0xD1, 0x6A, 0xB5, 0x2C, 0xE3, 0xCC, + 0xDD, 0xBF, 0x66, 0x5B, 0x92, 0x43, 0x48, 0xAC, 0xB6, 0x9B, 0x9C, 0xB0, 0x72, 0xC9, 0xCC, 0xD3, + 0xAC, 0x8A, 0x53, 0x8D, 0x48, 0x5E, 0x78, 0x01, 0xFF, 0x89, 0xD3, 0x47, 0x97, 0xC8, 0x8F, 0xCA, + 0x75, 0x59, 0x50, 0x91, 0xF9, 0x36, 0xDD, 0xAE, 0x6D, 0x4E, 0x2E, 0x89, 0xAB, 0x22, 0x29, 0xD2, + 0x44, 0xE3, 0xD9, 0x0F, 0x64, 0xAC, 0xB2, 0x15, 0x38, 0x8F, 0x4B, 0x60, 0x25, 0x7E, 0xA8, 0xC0, + 0x12, 0x7E, 0x2A, 0x8B, 0x91, 0x4B, 0xDE, 0x72, 0x97, 0xC7, 0x39, 0x56, 0x53, 0x23, 0x8C, 0x55, + 0xB2, 0x4C, 0x56, 0x89, 0x2B, 0x7F, 0x65, 0x06, 0x3F, 0x4B, 0x5A, 0x86, 0xDB, 0x4D, 0xB5, 0xD9, + 0x99, 0x7C, 0x5C, 0xD2, 0x92, 0x38, 0x49, 0x93, 0xE5, 0xEF, 0x9F, 0x43, 0x0B, 0xB1, 0xF9, 0xE9, + 0x70, 0x8E, 0x7E, 0xAA, 0x7E, 0xDD, 0x9D, 0x8A, 0xA6, 0x6A, 0x67, 0x2F, 0xA7, 0xE7, 0xFD, 0x09, + 0xFC, 0x2C, 0x12, 0x27, 0x58, 0xDA, 0xF3, 0xE9, 0xF0, 0x52, 0xB5, 0xD7, 0xF8, 0x9D, 0xAB, 0xE3, + 0x1A, 0x56, 0x74, 0x91, 0xAD, 0x18, 0x1C, 0x07, 0xBF, 0x8F, 0x7C, 0xD1, 0x11, 0xD9, 0x53, 0x15, + 0xD5, 0x03, 0x48, 0x1C, 0x1A, 0xE0, 0xCE, 0xEE, 0x42, 0x00, 0x61, 0x74, 0x44, 0x4D, 0x5A, 0x51, + 0x53, 0xCD, 0x90, 0x8B, 0x97, 0xEE, 0x88, 0x1C, 0x54, 0xAD, 0xC9, 0x7D, 0x95, 0x6F, 0xD4, 0x6B, + 0x4E, 0x85, 0xE4, 0x62, 0x00, 0xF4, 0x9C, 0x13, 0x37, 0x0B, 0xAC, 0x1C, 0xF2, 0x99, 0x8A, 0xA3, + 0x9C, 0x4A, 0xBE, 0x42, 0xAF, 0x02, 0xD2, 0x47, 0x7B, 0x61, 0x3D, 0xC8, 0xC4, 0x7B, 0xCB, 0xBC, + 0xAC, 0xF6, 0x0F, 0xCE, 0xD8, 0x63, 0xB3, 0x34, 0x7F, 0xE7, 0xC1, 0xE8, 0xBE, 0xD8, 0xF7, 0x3C, + 0x7E, 0xC7, 0x43, 0xE9, 0x87, 0xAC, 0x6D, 0x1E, 0xF4, 0xFB, 0x9C, 0xAE, 0xBD, 0x62, 0x6F, 0x4B, + 0x72, 0x58, 0x1C, 0x21, 0x43, 0x67, 0xC8, 0xA2, 0x76, 0x78, 0x95, 0x20, 0x0D, 0x92, 0x74, 0x71, + 0xF7, 0x67, 0xD0, 0xE0, 0x1D, 0x8E, 0x50, 0x46, 0x4E, 0x27, 0xFE, 0x64, 0x32, 0xC5, 0x4D, 0x45, + 0xE5, 0xD5, 0x54, 0x17, 0x9D, 0x56, 0x03, 0x4B, 0xD2, 0x38, 0xDA, 0x13, 0xE2, 0xC8, 0xC3, 0x37, + 0x88, 0x3D, 0xB5, 0x05, 0x45, 0x44, 0x47, 0x58, 0x77, 0xA8, 0x4E, 0xA0, 0x1E, 0xAE, 0x1C, 0xC8, + 0xB5, 0x35, 0xA5, 0xDC, 0xF6, 0xF9, 0x8D, 0x9C, 0x6A, 0x41, 0x6E, 0xAA, 0xF3, 0xC7, 0xAD, 0x12, + 0xED, 0xA4, 0xFF, 0xAC, 0x3E, 0xF4, 0x2B, 0x25, 0x5D, 0xA5, 0xD2, 0xD4, 0xED, 0x7B, 0x9A, 0x10, + 0x56, 0x3C, 0x16, 0x6F, 0xCF, 0xE1, 0xD7, 0xEC, 0xED, 0x9E, 0xF3, 0xE1, 0xB8, 0x05, 0x95, 0xAA, + 0xA3, 0x93, 0xBB, 0x0A, 0xE0, 0x56, 0xC8, 0xF4, 0xAB, 0xB1, 0xF9, 0x68, 0x07, 0x77, 0xE0, 0x21, + 0x29, 0x7D, 0x3C, 0xF4, 0xE1, 0x3E, 0x37, 0x94, 0xC5, 0xF4, 0xB2, 0xC5, 0x04, 0xAF, 0x88, 0xC8, + 0x47, 0x68, 0xBC, 0x1A, 0xE0, 0xEA, 0x0E, 0x6E, 0x8D, 0x7A, 0x1B, 0x3B, 0xC6, 0x65, 0x74, 0x11, + 0xC7, 0x1D, 0x06, 0x06, 0x45, 0x05, 0xE7, 0x22, 0x6C, 0x85, 0x1E, 0xD5, 0x63, 0x93, 0x1F, 0xDF, + 0x8C, 0x47, 0x17, 0x91, 0x6F, 0xA1, 0xDC, 0x57, 0x84, 0xFE, 0x35, 0x49, 0x27, 0x39, 0x9F, 0x52, + 0x8D, 0x0A, 0xC0, 0x88, 0x86, 0x4C, 0xF4, 0x84, 0x50, 0x32, 0x4E, 0x53, 0x72, 0x59, 0xCA, 0x0D, + 0x70, 0x3D, 0xE9, 0x8E, 0xA5, 0x13, 0xC2, 0xFA, 0x03, 0x9A, 0xE0, 0x38, 0x47, 0x39, 0x4E, 0xA4, + 0x15, 0x64, 0x71, 0x4E, 0x18, 0x2F, 0x48, 0xB4, 0xB8, 0xC1, 0xD6, 0xFC, 0xE1, 0x37, 0xA8, 0xFF, + 0x45, 0xED, 0xB8, 0x30, 0xE4, 0xC1, 0xFC, 0x2C, 0xC4, 0x8F, 0x54, 0x49, 0x4E, 0xD9, 0xEB, 0x89, + 0x83, 0xC5, 0xA1, 0x6B, 0x4D, 0x5B, 0x79, 0x01, 0x65, 0xA6, 0x4B, 0xE3, 0xA6, 0x0D, 0xA7, 0x0D, + 0x28, 0x5C, 0x2C, 0x25, 0xA9, 0x9E, 0x4A, 0xBD, 0x43, 0x82, 0x56, 0xD7, 0x5E, 0x13, 0x74, 0x0B, + 0x81, 0x54, 0xFF, 0xD9, 0x2D, 0xB7, 0xFC, 0xA8, 0x6D, 0xC2, 0xFD, 0x04, 0x90, 0xFF, 0x66, 0x57, + 0x01, 0x89, 0x7F, 0xAF, 0xB7, 0x80, 0xC0, 0x7B, 0x1C, 0x06, 0xC8, 0xC7, 0x4B, 0xF4, 0x5E, 0x9F, + 0x09, 0x12, 0x32, 0xD1, 0x6D, 0xFC, 0x97, 0xFA, 0x3D, 0x3A, 0x34, 0xE5, 0x04, 0xBF, 0x69, 0xCA, + 0xBF, 0xDB, 0x6F, 0x9A, 0xF2, 0x6F, 0xF6, 0x9B, 0xA6, 0xBC, 0xCB, 0x6F, 0x9A, 0x72, 0xBC, 0x48, + 0xEF, 0xF6, 0x9B, 0x10, 0x21, 0xF7, 0xFB, 0xCD, 0xE3, 0x63, 0x3A, 0xE0, 0x37, 0xF5, 0x7E, 0x82, + 0xDF, 0xD4, 0xFB, 0xBF, 0xDB, 0x6F, 0xEA, 0xFD, 0xDF, 0xEC, 0x37, 0xF5, 0xFE, 0x2E, 0xBF, 0xA9, + 0xF7, 0xE3, 0x45, 0x7A, 0xB7, 0xDF, 0x84, 0x08, 0xB9, 0xDF, 0x6F, 0xBA, 0xC0, 0x05, 0x1E, 0x25, + 0x2E, 0x13, 0x06, 0x34, 0xD1, 0xE5, 0xEF, 0x1E, 0xD3, 0x44, 0x97, 0xBF, 0x79, 0x58, 0x13, 0x5D, + 0xEE, 0x1A, 0xD9, 0x44, 0x97, 0x80, 0xA1, 0xC7, 0xDD, 0x8E, 0x13, 0x22, 0xE4, 0x15, 0x1C, 0x67, + 0x39, 0xEC, 0x38, 0xD3, 0x3C, 0xE7, 0x1F, 0x70, 0x9D, 0xBF, 0xDF, 0x77, 0xEE, 0x75, 0x9E, 0x90, + 0x82, 0xBD, 0xDF, 0x7B, 0x42, 0xA4, 0x4C, 0x75, 0x1F, 0x83, 0x5E, 0x9D, 0x9F, 0xA1, 0xA7, 0x63, + 0x38, 0x06, 0x53, 0x82, 0x1C, 0x2B, 0x82, 0x4D, 0x7F, 0x60, 0x1D, 0x40, 0x37, 0x7A, 0xAF, 0x8D, + 0xCD, 0x0B, 0x27, 0xEE, 0xED, 0x78, 0xE9, 0x27, 0xAE, 0xC7, 0x20, 0x95, 0x7F, 0x9D, 0x66, 0x54, + 0x25, 0x48, 0xD9, 0x54, 0xAB, 0xA9, 0x2A, 0x2D, 0x82, 0x5E, 0x49, 0x50, 0x32, 0x95, 0x0D, 0xED, + 0x44, 0x26, 0x6F, 0xF2, 0x56, 0x95, 0x8F, 0xC3, 0x74, 0x33, 0x8E, 0xD2, 0x49, 0x43, 0x8E, 0xAA, + 0xB5, 0xDD, 0x6C, 0xB7, 0xDB, 0x6A, 0xBA, 0x5A, 0x41, 0xA6, 0xD4, 0x52, 0xD1, 0x94, 0x98, 0xC8, + 0x24, 0x4E, 0xDC, 0x7D, 0xF3, 0xD2, 0x4F, 0x35, 0xA3, 0xA2, 0x0A, 0x32, 0xA2, 0x57, 0xA5, 0xCD, + 0xB6, 0x8C, 0xB7, 0xEB, 0xA9, 0x2A, 0x05, 0x99, 0x50, 0xCB, 0x44, 0x13, 0x62, 0x22, 0x93, 0x37, + 0x65, 0x33, 0xD1, 0x4D, 0x3C, 0xD1, 0x78, 0x82, 0x24, 0xCC, 0x72, 0x3E, 0x65, 0x36, 0x45, 0xB9, + 0xAD, 0xB2, 0x49, 0xCA, 0x84, 0x99, 0x4D, 0x09, 0x44, 0xB3, 0x61, 0x22, 0x13, 0x36, 0x71, 0x57, + 0xD4, 0x4B, 0x3F, 0xD1, 0x78, 0x9A, 0x2A, 0xC8, 0x7E, 0x5E, 0x95, 0xAA, 0x55, 0xB9, 0xD9, 0x3C, + 0x4E, 0x55, 0x29, 0xC8, 0x84, 0x5A, 0x26, 0x9A, 0x10, 0x13, 0x99, 0xBC, 0x69, 0x9B, 0xBC, 0x3E, + 0xF2, 0x89, 0x06, 0x54, 0x44, 0x41, 0xF6, 0xF3, 0x2A, 0x54, 0xEE, 0xB6, 0xE9, 0x76, 0x39, 0x51, + 0xA1, 0x20, 0xF3, 0x69, 0x91, 0x68, 0x3E, 0x4C, 0x64, 0xE2, 0x26, 0xED, 0x58, 0x7B, 0xA8, 0x27, + 0x1A, 0x4F, 0xD2, 0x04, 0xD9, 0xCE, 0xAB, 0x4E, 0x95, 0x8B, 0x9F, 0x69, 0xEA, 0x04, 0x99, 0x4E, + 0x4B, 0x44, 0xD3, 0x61, 0x22, 0x93, 0x36, 0x65, 0xF3, 0xDD, 0x4D, 0x3C, 0xD1, 0x70, 0x82, 0x24, + 0xC8, 0x6E, 0x5E, 0x65, 0x36, 0x15, 0xFC, 0xEC, 0x26, 0x29, 0x13, 0x64, 0x36, 0x2D, 0x10, 0xCD, + 0x86, 0x89, 0xBF, 0xE3, 0xB9, 0x20, 0xF5, 0x3C, 0xFD, 0xE1, 0x37, 0xB1, 0x91, 0xA4, 0xF7, 0xF2, + 0x22, 0x48, 0xEA, 0xAE, 0x23, 0x19, 0x57, 0x91, 0xCC, 0xB7, 0xA3, 0x53, 0x79, 0x20, 0x0C, 0x7E, + 0xE3, 0xCB, 0x03, 0xB1, 0xD9, 0x9E, 0x7F, 0x6C, 0x6E, 0xFC, 0xFE, 0xC5, 0xF7, 0x91, 0x00, 0xCB, + 0xB8, 0x8C, 0x24, 0x2F, 0x1E, 0x89, 0x6B, 0x45, 0xE2, 0x7F, 0x91, 0xCC, 0xE4, 0x1D, 0xE0, 0x24, + 0x59, 0xC0, 0x30, 0x43, 0xDE, 0x39, 0x5A, 0x2D, 0xF2, 0x8F, 0x80, 0xC1, 0x92, 0xA0, 0x24, 0x1A, + 0x45, 0xFE, 0xFE, 0xFA, 0x51, 0x7E, 0xFE, 0xA8, 0x5E, 0x09, 0x0C, 0x20, 0x31, 0x59, 0x45, 0x06, + 0x22, 0xF0, 0x93, 0xA8, 0x75, 0xE4, 0xC7, 0x8C, 0x14, 0xA6, 0x92, 0xFB, 0xD1, 0x4C, 0x60, 0x00, + 0xF1, 0x3A, 0x5E, 0xF7, 0x78, 0xB1, 0x42, 0xE1, 0x37, 0x97, 0x66, 0xEA, 0xF0, 0xC1, 0x87, 0x60, + 0x7A, 0x19, 0xCB, 0x61, 0x86, 0xF7, 0x96, 0x06, 0x83, 0xC5, 0x7F, 0xD2, 0xBD, 0x1A, 0xBF, 0xC8, + 0x8D, 0x92, 0xB6, 0xDD, 0x11, 0xAC, 0xE6, 0xDC, 0x1F, 0x44, 0xDA, 0xB5, 0x49, 0xAC, 0x0F, 0x2F, + 0xDF, 0x76, 0x16, 0xB9, 0xE7, 0x99, 0x18, 0x1C, 0x71, 0x0B, 0x0E, 0xD3, 0x70, 0xF3, 0xCD, 0xB5, + 0xDD, 0xD6, 0x6D, 0xAA, 0xBF, 0x69, 0x01, 0xD0, 0x56, 0x75, 0xB5, 0x3D, 0x6B, 0x40, 0xF3, 0xFC, + 0x1B, 0x4F, 0xA5, 0x09, 0x98, 0xB3, 0xD4, 0xCC, 0x59, 0x24, 0x8F, 0x76, 0x5C, 0xC1, 0x9D, 0x40, + 0x1E, 0x6C, 0x18, 0x82, 0x8D, 0xCE, 0xEF, 0x25, 0xF3, 0xD9, 0xFE, 0x54, 0xFC, 0xDA, 0x6E, 0x8B, + 0xBA, 0x7A, 0x4F, 0x6C, 0x43, 0xCF, 0x67, 0x1B, 0x39, 0x1F, 0x3E, 0x56, 0x6D, 0xFB, 0x5E, 0x2A, + 0x12, 0x20, 0x42, 0xFC, 0x33, 0x4C, 0xB1, 0xF4, 0x56, 0x75, 0x96, 0x8B, 0x28, 0x2A, 0x22, 0x56, + 0x31, 0x6E, 0x5D, 0x3B, 0x5E, 0xD5, 0x23, 0xD9, 0x12, 0x85, 0x1A, 0xF2, 0x3C, 0xDC, 0x47, 0xF9, + 0xFC, 0x8E, 0xB7, 0xD4, 0xE7, 0x76, 0x61, 0xE1, 0x76, 0x71, 0xE0, 0x93, 0xEB, 0x32, 0x9F, 0xF2, + 0x10, 0xB9, 0x0C, 0x9C, 0x04, 0x42, 0xE7, 0xF8, 0x6D, 0x7E, 0xED, 0xCC, 0x1A, 0x6B, 0xCC, 0xC5, + 0xD3, 0xA1, 0xB4, 0x83, 0xE9, 0x6B, 0x40, 0xD4, 0x47, 0x85, 0xD7, 0x46, 0xEB, 0x0A, 0x54, 0x98, + 0x4D, 0xB7, 0x3D, 0xBA, 0xDD, 0x91, 0x25, 0xCA, 0x93, 0x8D, 0x14, 0x62, 0x6B, 0x62, 0x59, 0xB7, + 0xD8, 0xCF, 0xD8, 0x7D, 0x75, 0x77, 0x4C, 0x02, 0x4D, 0xA7, 0xAF, 0xBC, 0x8D, 0x5C, 0x94, 0x26, + 0xDB, 0xF0, 0x0B, 0xC7, 0xF3, 0x89, 0xAF, 0x57, 0xC4, 0xC3, 0xE1, 0x49, 0xE2, 0x7C, 0xFE, 0x17, + 0x45, 0xCD, 0x31, 0x0D, 0x62, 0x1C, 0x2A, 0xA5, 0x21, 0x39, 0xB2, 0x35, 0xBD, 0xAD, 0x6A, 0x9B, + 0xD4, 0xBA, 0x62, 0xA5, 0x41, 0xCE, 0x50, 0x48, 0xBF, 0x2F, 0x9A, 0xE7, 0xD2, 0x7C, 0x6A, 0x6F, + 0x77, 0xB8, 0x54, 0xA5, 0x3E, 0xB1, 0x0A, 0x7C, 0xCD, 0x03, 0xAB, 0x49, 0x0C, 0x23, 0x7B, 0xD3, + 0xDD, 0x8C, 0xAB, 0xEB, 0xBA, 0xF3, 0x92, 0x9F, 0xF1, 0x56, 0xF9, 0x45, 0x1F, 0xF9, 0xA2, 0x31, + 0xC3, 0xF1, 0xAA, 0x85, 0x96, 0x1F, 0x95, 0x87, 0xA2, 0x7E, 0xDE, 0x3B, 0x4E, 0x62, 0xE0, 0x23, + 0x6D, 0xD6, 0x73, 0xAE, 0x2E, 0x37, 0xD4, 0xBC, 0x16, 0x3B, 0x30, 0xDE, 0xCC, 0xE6, 0xEB, 0xBE, + 0x65, 0x9A, 0xE9, 0xBB, 0x2D, 0xA0, 0xCD, 0x27, 0x7D, 0xB2, 0x42, 0x00, 0xE9, 0x15, 0x94, 0x76, + 0x24, 0x5A, 0x9B, 0x79, 0xF0, 0x19, 0xA5, 0x10, 0xF9, 0x52, 0x53, 0xAD, 0xAA, 0xAC, 0xD8, 0x0C, + 0x55, 0x2B, 0x62, 0xE6, 0x49, 0xA3, 0xA8, 0x17, 0x05, 0xBC, 0x04, 0xBA, 0x15, 0x5D, 0xC4, 0xE9, + 0xDC, 0xB6, 0xAB, 0x0E, 0xD8, 0x29, 0x5A, 0x7C, 0xFD, 0x3A, 0xAE, 0x74, 0x3E, 0xD9, 0xE0, 0xCE, + 0x22, 0xD9, 0x44, 0xF9, 0x29, 0xB4, 0xB8, 0xAE, 0x3D, 0xD0, 0x71, 0xE2, 0x79, 0x69, 0xEB, 0xB2, + 0x1E, 0xE5, 0x23, 0x5D, 0x93, 0x38, 0x04, 0x21, 0x52, 0x15, 0xBE, 0x2A, 0x47, 0x9B, 0x04, 0xB1, + 0x5E, 0xEC, 0xCD, 0x10, 0xD1, 0xFB, 0xA6, 0x50, 0x62, 0xE8, 0xE1, 0x61, 0x5D, 0x8A, 0x18, 0x22, + 0x06, 0x34, 0x2F, 0x9E, 0x76, 0x25, 0x65, 0x3D, 0x43, 0x06, 0x39, 0xE1, 0xB5, 0x45, 0x08, 0x10, + 0xA1, 0x60, 0x03, 0xAB, 0xED, 0x32, 0xC6, 0x7C, 0xFC, 0xFC, 0x8B, 0x51, 0x55, 0x7F, 0x7E, 0x72, + 0x4D, 0xE1, 0x63, 0x26, 0x47, 0xBA, 0x34, 0xF6, 0x3F, 0x14, 0xAC, 0x9E, 0xCA, 0xC5, 0xB1, 0x90, + 0x86, 0x3B, 0xDB, 0x75, 0xEB, 0x2E, 0xFE, 0xE4, 0x17, 0x49, 0x1C, 0x17, 0x06, 0x46, 0x5E, 0x93, + 0x1A, 0x6E, 0xA7, 0xB3, 0xE0, 0x66, 0xDA, 0xC0, 0xC4, 0xEC, 0xD1, 0x56, 0x9A, 0xF4, 0x50, 0x46, + 0x1B, 0xFD, 0x71, 0xA4, 0xD2, 0xEC, 0x3F, 0x86, 0xEB, 0x3B, 0xC3, 0x84, 0x91, 0x5B, 0x1E, 0x60, + 0x59, 0xB3, 0x0E, 0x71, 0x87, 0xF6, 0x07, 0x24, 0xD3, 0x84, 0x2A, 0x12, 0xCA, 0xE0, 0x5B, 0x9A, + 0xD3, 0x4B, 0x48, 0x12, 0x54, 0xC7, 0xF2, 0x13, 0xD2, 0xDF, 0xA0, 0x41, 0x79, 0xB1, 0x8C, 0x07, + 0x7B, 0xCB, 0x26, 0x5C, 0x6E, 0x36, 0x70, 0xED, 0x8C, 0x7E, 0xF6, 0xFE, 0xB5, 0xEB, 0x35, 0xD2, + 0xE1, 0x28, 0x48, 0x76, 0x9B, 0x8A, 0x63, 0x9E, 0x5C, 0x6C, 0x43, 0x75, 0x85, 0x08, 0xD3, 0x89, + 0xBE, 0xDC, 0x26, 0xB5, 0xB0, 0x99, 0x2C, 0xEF, 0xB9, 0xAF, 0x99, 0x13, 0xDA, 0x8C, 0x53, 0xB5, + 0x8D, 0xA1, 0x17, 0xCC, 0xC6, 0xFC, 0x3B, 0xF5, 0x98, 0x9F, 0x7A, 0xFF, 0xA0, 0x3F, 0x5D, 0x6A, + 0x83, 0xFA, 0x23, 0xA0, 0x1E, 0xDC, 0xAF, 0x75, 0x11, 0x25, 0xC9, 0x52, 0x50, 0xF5, 0x16, 0x7E, + 0x53, 0xD7, 0x90, 0x6B, 0xA8, 0x95, 0x57, 0xB3, 0x75, 0x41, 0x7C, 0xD1, 0x7F, 0x59, 0xE3, 0x02, + 0x6D, 0xC5, 0x98, 0x73, 0xA0, 0x7D, 0x8C, 0x41, 0xE5, 0x99, 0x81, 0x79, 0xB9, 0x60, 0xBB, 0x13, + 0x8C, 0xEF, 0xEE, 0x91, 0x38, 0x36, 0x7A, 0x16, 0xE7, 0x8E, 0xB6, 0x44, 0xD7, 0x59, 0xE7, 0x8B, + 0x47, 0x79, 0xDC, 0x86, 0xF1, 0x82, 0xB2, 0x8C, 0x60, 0x92, 0xF0, 0x0A, 0x96, 0xD3, 0x9C, 0x5E, + 0xC9, 0x82, 0x9A, 0xDB, 0x64, 0x4B, 0x6A, 0xBA, 0x89, 0x16, 0xD5, 0x54, 0x63, 0x96, 0x75, 0x98, + 0x76, 0xBD, 0x5A, 0x7B, 0x4D, 0xDB, 0x94, 0xAF, 0x65, 0xDA, 0xA6, 0x7C, 0x4D, 0xD3, 0x36, 0xE5, + 0x6D, 0xA6, 0x6D, 0xCA, 0x5B, 0x4C, 0xDB, 0x94, 0x37, 0x9A, 0xF6, 0xF1, 0x31, 0xF1, 0x9A, 0xB6, + 0xDE, 0xBF, 0x96, 0x69, 0xEB, 0xFD, 0x6B, 0x9A, 0xB6, 0xDE, 0xDF, 0x66, 0xDA, 0x7A, 0x7F, 0x8B, + 0x69, 0xEB, 0xFD, 0x8D, 0xA6, 0x4D, 0x92, 0xC7, 0x47, 0xAF, 0x6D, 0x2F, 0xF5, 0x6B, 0xD9, 0xF6, + 0x52, 0xBF, 0xA6, 0x6D, 0x2F, 0xF5, 0x6D, 0xB6, 0xBD, 0xD4, 0xB7, 0xD8, 0xF6, 0x52, 0xDF, 0x6A, + 0xDB, 0x6C, 0xC8, 0xB6, 0xAF, 0x68, 0xDC, 0x57, 0xB6, 0xEE, 0xCD, 0xE6, 0xBD, 0xD1, 0xBE, 0x81, + 0x06, 0x5E, 0x88, 0x17, 0xD3, 0xCF, 0x87, 0x17, 0x7E, 0x9B, 0xD5, 0x98, 0x57, 0x7C, 0x14, 0xDB, + 0x17, 0x25, 0x7A, 0xBB, 0xA9, 0x05, 0xB9, 0x5D, 0xD1, 0x1C, 0xEA, 0x5F, 0x31, 0x5A, 0x87, 0x4C, + 0x6C, 0x61, 0xB2, 0x1A, 0xB5, 0xD5, 0xE9, 0xB0, 0x9B, 0x0B, 0x2C, 0x8C, 0xAB, 0x71, 0x6A, 0x8A, + 0xDA, 0xBA, 0xE4, 0xBC, 0x8C, 0xD9, 0xF0, 0x97, 0x86, 0xBB, 0x33, 0xBF, 0xCB, 0xA7, 0x36, 0x9C, + 0x2B, 0xAD, 0x2A, 0x51, 0x2F, 0xCB, 0x61, 0x82, 0x3D, 0xD1, 0x86, 0x59, 0xD3, 0x19, 0x2C, 0x21, + 0x6F, 0x9E, 0xC1, 0x18, 0x56, 0x2B, 0xA4, 0xD6, 0x4C, 0xE4, 0x72, 0x89, 0x99, 0xC2, 0xB0, 0xF0, + 0xD2, 0x9A, 0x4E, 0x92, 0x9A, 0x2B, 0x42, 0x59, 0x14, 0x7C, 0x89, 0xD2, 0x1D, 0x99, 0x1A, 0xE7, + 0x58, 0xBA, 0x04, 0xE8, 0xE4, 0xEA, 0xB1, 0x07, 0xCC, 0xBA, 0x0F, 0x51, 0x71, 0x3A, 0x01, 0x0A, + 0x2B, 0x29, 0x52, 0x38, 0xD2, 0xD7, 0x41, 0xBA, 0x11, 0xF5, 0x6C, 0xB1, 0x84, 0x2F, 0x3E, 0x86, + 0xFD, 0xCD, 0x23, 0xCE, 0x18, 0x83, 0xA2, 0x79, 0x5F, 0x9A, 0xD5, 0x00, 0x5D, 0xBC, 0x72, 0xEC, + 0xFF, 0xFB, 0x02, 0x9C, 0xA0, 0x17, 0x01, 0x46, 0xF9, 0x4E, 0xEC, 0x55, 0x44, 0x2F, 0xCF, 0x2F, + 0x2F, 0x80, 0x08, 0x9A, 0x6E, 0xE5, 0x9D, 0xA5, 0x1F, 0x3E, 0x85, 0x19, 0xC3, 0xF7, 0x0F, 0x26, + 0x32, 0x24, 0xE0, 0xDC, 0x4A, 0xE8, 0x3C, 0x8B, 0x27, 0x31, 0x23, 0x39, 0x23, 0xBC, 0x99, 0x25, + 0xF5, 0xEC, 0xEB, 0x2E, 0x19, 0x9D, 0xF5, 0x88, 0x2C, 0xAF, 0x91, 0xC9, 0x65, 0x10, 0xD5, 0x32, + 0xE9, 0xCC, 0xEA, 0x2C, 0x1B, 0x13, 0x2A, 0x9C, 0xAB, 0x87, 0x2B, 0x29, 0xE7, 0x58, 0xB6, 0x5D, + 0xAB, 0x63, 0xD9, 0xDB, 0x35, 0x56, 0x62, 0x26, 0xB2, 0x23, 0xD9, 0xA1, 0xDC, 0xA9, 0x65, 0xF5, + 0xD2, 0x04, 0x66, 0x0E, 0x5D, 0xF1, 0xA3, 0xBB, 0x85, 0xA3, 0xC9, 0x6D, 0x25, 0x7C, 0x26, 0x97, + 0x4C, 0xBC, 0x46, 0x77, 0x9A, 0x5E, 0x92, 0xDC, 0x62, 0x7C, 0xE5, 0x52, 0xB6, 0xF5, 0x55, 0xDA, + 0xCD, 0x8E, 0xAD, 0x59, 0x92, 0xDC, 0x39, 0x24, 0x10, 0x0C, 0xE1, 0x6A, 0x13, 0x7C, 0x5B, 0xF1, + 0x08, 0xB2, 0xB5, 0x42, 0xF5, 0x99, 0x5B, 0x41, 0x1D, 0xF6, 0x8E, 0xD1, 0xD6, 0x74, 0x3D, 0xE7, + 0x16, 0x53, 0x83, 0x8F, 0x11, 0x37, 0x97, 0x5D, 0xC3, 0xCD, 0x8E, 0x2E, 0xF8, 0xD1, 0xEC, 0x73, + 0xF6, 0x04, 0x41, 0x3A, 0xD7, 0xAB, 0xB8, 0x3A, 0x88, 0x0F, 0xF3, 0x74, 0x54, 0x83, 0x23, 0x8A, + 0x8A, 0xE7, 0x77, 0x75, 0x67, 0x01, 0x00, 0x85, 0xE5, 0xE9, 0x3D, 0xE3, 0xC3, 0xF1, 0x58, 0x9D, + 0x8C, 0x65, 0x81, 0x54, 0xAE, 0x90, 0xB8, 0x42, 0xBC, 0x0C, 0xDE, 0xF4, 0x76, 0xAF, 0x3C, 0x7A, + 0x76, 0xC6, 0xC0, 0x2C, 0x62, 0x6C, 0xC3, 0xBB, 0x22, 0xDF, 0x92, 0xE7, 0x9A, 0x0D, 0x4D, 0x50, + 0x5F, 0xB1, 0xE0, 0xF3, 0xFF, 0x08, 0xE5, 0xD6, 0x11, 0xCA, 0xDF, 0xB2, 0xE4, 0xDD, 0x97, 0xF8, + 0xAC, 0xFB, 0x30, 0x69, 0x90, 0x93, 0x98, 0xB5, 0x2D, 0x1F, 0xE0, 0xA7, 0x03, 0x84, 0x3E, 0x30, + 0xF0, 0xE8, 0x10, 0xC8, 0x12, 0x7B, 0xCF, 0x80, 0x48, 0x0B, 0x1C, 0x1D, 0x5F, 0x7C, 0x66, 0xAB, + 0xF6, 0x60, 0xD2, 0x02, 0x9C, 0x80, 0x75, 0x6B, 0xAB, 0x96, 0x13, 0xA3, 0x85, 0xB1, 0xC7, 0x78, + 0x8F, 0xD0, 0xCE, 0x1E, 0x5E, 0xE1, 0xEE, 0x26, 0x3F, 0x26, 0x8D, 0x8E, 0x6E, 0x21, 0x3C, 0x83, + 0x1A, 0xCB, 0x3B, 0xF2, 0x3B, 0xF5, 0x95, 0xC5, 0x3B, 0xAA, 0xAE, 0xC4, 0xBA, 0xE2, 0x16, 0xC2, + 0x24, 0x7D, 0x85, 0xFF, 0x07, 0x2B, 0x29, 0xBB, 0x85, 0xC1, 0xB2, 0xAC, 0x8E, 0x25, 0x01, 0xAB, + 0xC6, 0x9B, 0x97, 0xE4, 0x27, 0x86, 0x7A, 0x66, 0x1C, 0x90, 0xFB, 0x94, 0xC1, 0x32, 0xF6, 0x29, + 0xC5, 0x7B, 0x16, 0x6F, 0x09, 0x73, 0xBB, 0x49, 0x99, 0x77, 0x96, 0xB4, 0xE4, 0x11, 0x52, 0xD6, + 0xA0, 0xB6, 0x1B, 0x4B, 0x69, 0x0D, 0x46, 0xBC, 0x49, 0xEF, 0x49, 0x25, 0xAE, 0xBC, 0x6A, 0xB0, + 0xC8, 0x15, 0x4A, 0x8F, 0x81, 0xC3, 0xB4, 0x7B, 0xAA, 0xAF, 0x62, 0x1A, 0x54, 0xBA, 0x54, 0x3E, + 0x9F, 0x99, 0xC4, 0x74, 0xC4, 0x66, 0x5B, 0x8A, 0x8D, 0xDB, 0xEE, 0x2C, 0x60, 0xC5, 0x24, 0xA4, + 0x84, 0x15, 0xA6, 0xA7, 0x90, 0xF5, 0xCE, 0xD3, 0x6D, 0xBA, 0xDF, 0x52, 0xC8, 0xD8, 0x7D, 0xA8, + 0x75, 0x9C, 0x01, 0x6B, 0xFB, 0x50, 0x87, 0x87, 0x38, 0x10, 0x04, 0xC6, 0xDF, 0xE1, 0xF1, 0x27, + 0x5A, 0x8C, 0x7E, 0xC9, 0xBF, 0x4F, 0xBA, 0x8B, 0xC5, 0x4F, 0x70, 0x56, 0x81, 0xFD, 0x60, 0x03, + 0xA6, 0xC6, 0xA1, 0x88, 0x80, 0xC3, 0xE1, 0xBF, 0xA4, 0x0D, 0x03, 0x75, 0xC2, 0x9A, 0x30, 0xA2, + 0x16, 0x9F, 0x0A, 0x7A, 0x5A, 0x31, 0xF4, 0x17, 0x7B, 0x70, 0x7C, 0x6F, 0x23, 0x06, 0x2C, 0x82, + 0xDA, 0x30, 0xD4, 0x9B, 0xE1, 0x69, 0xB5, 0xC1, 0x8E, 0xB7, 0x28, 0xAE, 0x1C, 0x5C, 0xF3, 0x9D, + 0xF0, 0xC0, 0x13, 0x8E, 0x13, 0xDD, 0x8F, 0x26, 0x69, 0x87, 0x0A, 0x3A, 0x48, 0x95, 0xCE, 0xFF, + 0x92, 0xFD, 0x79, 0x3B, 0x53, 0x8E, 0xC0, 0x85, 0x88, 0xC2, 0x63, 0x51, 0xF3, 0xF8, 0x2E, 0xE2, + 0xA5, 0x11, 0xC8, 0x61, 0x5B, 0xD5, 0x7C, 0xCF, 0x1D, 0x61, 0x0B, 0xEB, 0x80, 0x08, 0x34, 0x3E, + 0x6F, 0xB6, 0x4F, 0xDD, 0x01, 0xE5, 0x97, 0xE2, 0x18, 0xFD, 0x8A, 0xA8, 0x6A, 0x0E, 0xE5, 0x3B, + 0xED, 0xE4, 0x3C, 0x56, 0x43, 0x89, 0x3B, 0x17, 0x20, 0xE3, 0xD0, 0xBA, 0x2A, 0x4E, 0x1F, 0x83, + 0xC9, 0x9F, 0x8C, 0xAA, 0x8F, 0xA4, 0x23, 0xE1, 0x6E, 0xD4, 0x34, 0x03, 0x43, 0x32, 0x9B, 0xEA, + 0xD8, 0x07, 0xD0, 0x44, 0x52, 0x7F, 0x4C, 0x54, 0x78, 0xC0, 0x0E, 0xFC, 0x3A, 0x92, 0xCF, 0x00, + 0x1F, 0x6A, 0x58, 0xA7, 0xD4, 0x4A, 0x7F, 0xE2, 0x07, 0x79, 0x02, 0xFF, 0xAF, 0x6E, 0x0B, 0x37, + 0x6C, 0x65, 0x90, 0x9F, 0xB6, 0xB2, 0xE1, 0xD1, 0x11, 0x26, 0x57, 0x0F, 0x24, 0x0D, 0x24, 0xFC, + 0x8C, 0x69, 0xE6, 0x2D, 0x44, 0xDB, 0xC4, 0xBF, 0xEB, 0x54, 0x42, 0x5E, 0x1D, 0xCB, 0x07, 0x2E, + 0x45, 0x1D, 0xE6, 0xB4, 0xD3, 0x55, 0x85, 0x9E, 0x5F, 0x1D, 0x07, 0xCE, 0xBE, 0x51, 0x27, 0x60, + 0x51, 0x04, 0xA3, 0x73, 0xAA, 0x2D, 0x85, 0x70, 0x7D, 0x3C, 0x22, 0x22, 0x25, 0x03, 0xF1, 0xE5, + 0x71, 0x35, 0x9B, 0x1C, 0x8F, 0xFA, 0x18, 0xC5, 0x04, 0xB2, 0x9E, 0xA1, 0x0D, 0x83, 0x54, 0x0D, + 0xFD, 0xC4, 0x9E, 0x9F, 0x32, 0x96, 0xDC, 0x1C, 0xFE, 0x1C, 0x21, 0x99, 0x23, 0x83, 0x3C, 0x6F, + 0x5E, 0x22, 0x67, 0x20, 0xA0, 0x04, 0xCF, 0x45, 0x53, 0x25, 0x43, 0x4A, 0xD3, 0x46, 0xE4, 0x39, + 0xE8, 0xE4, 0x80, 0xB5, 0x5C, 0x76, 0xEB, 0xCC, 0x35, 0x8B, 0x5B, 0xF0, 0xEF, 0xF0, 0x80, 0x67, + 0x21, 0xF2, 0x27, 0x2A, 0x3A, 0x50, 0x31, 0x44, 0x73, 0x71, 0x7A, 0xAE, 0x55, 0xDD, 0x60, 0xC9, + 0xB2, 0x18, 0xBC, 0x43, 0x93, 0xBE, 0x6B, 0xE8, 0x0D, 0x3E, 0xFA, 0xE8, 0x91, 0x27, 0x54, 0xA0, + 0x6E, 0x70, 0x72, 0x7C, 0x0A, 0x29, 0x0E, 0x5D, 0x44, 0x12, 0xA7, 0xE3, 0x71, 0xE7, 0x0D, 0x0F, + 0xA3, 0x39, 0x4A, 0xE2, 0x86, 0x57, 0x85, 0x26, 0xD9, 0x29, 0xD0, 0xCC, 0xFA, 0xA6, 0x8B, 0x1B, + 0xA8, 0x2E, 0x11, 0x78, 0x24, 0x28, 0x4A, 0x0F, 0x90, 0x3D, 0xA5, 0xEA, 0x5E, 0x72, 0xC2, 0x1B, + 0x06, 0xC6, 0xDE, 0x92, 0x9B, 0xA3, 0x9E, 0xE3, 0x79, 0xB2, 0x21, 0x47, 0x21, 0x7E, 0xB0, 0x7C, + 0x71, 0xD7, 0xA3, 0xAA, 0x84, 0xB9, 0x43, 0x22, 0xEB, 0xB5, 0x3A, 0x1C, 0x16, 0xCA, 0xCF, 0xA1, + 0x4F, 0xCC, 0xF4, 0x9E, 0x0A, 0x03, 0x66, 0x16, 0xFE, 0x50, 0x9E, 0xAE, 0x12, 0xBF, 0x3C, 0xD9, + 0xFD, 0x7B, 0xDE, 0x08, 0xA6, 0xB7, 0x6B, 0xA0, 0xA0, 0xE8, 0xED, 0x9A, 0x24, 0x59, 0x64, 0xF9, + 0x72, 0x96, 0xD0, 0x77, 0x7D, 0x62, 0xF9, 0x64, 0xCF, 0xD7, 0x39, 0xFC, 0x5E, 0xCF, 0x3E, 0xAA, + 0xE5, 0x83, 0x3E, 0xB3, 0x9C, 0x60, 0x45, 0xF8, 0xAC, 0x8F, 0xF3, 0xFD, 0x9E, 0x1A, 0x52, 0xF9, + 0x63, 0x41, 0xFC, 0x6E, 0x8C, 0xA7, 0x58, 0xFF, 0x35, 0x46, 0x5A, 0x8A, 0x7C, 0x2B, 0x1B, 0xF1, + 0xEC, 0xD4, 0xAB, 0x19, 0xB1, 0x9C, 0xF7, 0xA1, 0xA3, 0x48, 0xDA, 0x54, 0xBD, 0x94, 0x34, 0xFB, + 0x68, 0xA6, 0xD8, 0xA6, 0xA2, 0x00, 0x42, 0xDE, 0x3E, 0xB2, 0x86, 0x67, 0xE5, 0x61, 0x5B, 0x9C, + 0x9F, 0x4F, 0x2D, 0x6F, 0x30, 0x7D, 0x2F, 0x61, 0x62, 0x50, 0x68, 0xAB, 0xE5, 0xF4, 0x34, 0x93, + 0xD8, 0x38, 0xDA, 0xC3, 0xB2, 0x24, 0x27, 0x6F, 0x30, 0xF2, 0x79, 0x21, 0xA0, 0xD0, 0xE7, 0x2C, + 0x9C, 0xAA, 0xCF, 0xD4, 0xB4, 0x45, 0x6C, 0x2F, 0x00, 0x75, 0x75, 0xFE, 0xDE, 0x77, 0x7D, 0x4D, + 0xA8, 0xF9, 0x71, 0xDC, 0x1D, 0x39, 0xED, 0xCE, 0x26, 0xBE, 0xF4, 0x81, 0x69, 0x33, 0xF8, 0xE8, + 0x51, 0x17, 0x40, 0x96, 0x6E, 0xF0, 0x5D, 0x35, 0x55, 0xA0, 0x86, 0xC8, 0x6C, 0xF4, 0xF8, 0xF8, + 0x08, 0x49, 0xDB, 0x37, 0xA7, 0x16, 0x5A, 0x32, 0x3D, 0xBA, 0xBE, 0x7D, 0xFD, 0xD9, 0x5C, 0xBA, + 0x83, 0x36, 0x60, 0x20, 0x9A, 0xB8, 0xB6, 0x9E, 0x07, 0x69, 0xB8, 0x6F, 0x99, 0x1E, 0x83, 0x36, + 0xC8, 0xFA, 0xAC, 0x4B, 0x71, 0x92, 0x75, 0x03, 0x1F, 0xD7, 0x70, 0x67, 0x5B, 0xBC, 0x08, 0x72, + 0x87, 0x4F, 0xA2, 0xF3, 0xA8, 0x7C, 0xF7, 0x6F, 0x73, 0xF5, 0x1E, 0xA3, 0xAD, 0x29, 0x2D, 0xA7, + 0x81, 0x7D, 0x1A, 0xD2, 0x8C, 0x6E, 0xF9, 0xA0, 0x36, 0x2A, 0xE4, 0x72, 0x58, 0x87, 0xA1, 0x50, + 0xFD, 0x2D, 0xF5, 0xC8, 0x8D, 0xB2, 0x39, 0x95, 0x1A, 0x66, 0x70, 0xE7, 0xEE, 0x14, 0x65, 0xC5, + 0xAD, 0x8B, 0xB8, 0xAE, 0xB0, 0xD1, 0xED, 0x8B, 0x9C, 0xBB, 0x45, 0xCA, 0xCD, 0x60, 0x8A, 0x78, + 0x65, 0xAF, 0xD9, 0x65, 0x2B, 0xF5, 0x98, 0xDD, 0xEF, 0x9F, 0xDF, 0x46, 0xB6, 0xB0, 0x91, 0x27, + 0x75, 0xAE, 0xE4, 0x51, 0x80, 0x48, 0xC4, 0x23, 0xC5, 0xF8, 0xFB, 0x0B, 0xF1, 0x45, 0x57, 0x07, + 0xA8, 0x93, 0xA2, 0x2A, 0x7C, 0x29, 0x72, 0xEB, 0x5A, 0x21, 0xF5, 0x46, 0xE8, 0x97, 0x1D, 0x32, + 0x0F, 0xDA, 0xBB, 0x58, 0xE7, 0x2C, 0x5C, 0x30, 0xC9, 0xF6, 0x27, 0x93, 0xB0, 0xA9, 0x1D, 0xC4, + 0xE1, 0x69, 0xB6, 0x37, 0x24, 0x3F, 0x93, 0xA5, 0x93, 0xB4, 0x6A, 0x86, 0x4A, 0x6E, 0x2F, 0x16, + 0xB3, 0xE2, 0x77, 0xD8, 0xA5, 0x1B, 0x70, 0x33, 0xC8, 0x19, 0x56, 0x3C, 0x32, 0x37, 0x82, 0xE2, + 0x7C, 0x45, 0x5E, 0x0B, 0x93, 0xC3, 0xAB, 0x94, 0x30, 0x75, 0x76, 0xB3, 0x7C, 0x79, 0x01, 0xE2, + 0x34, 0x67, 0x6A, 0x51, 0x0A, 0x85, 0x43, 0x0B, 0x52, 0xE0, 0xDA, 0x59, 0xF5, 0x17, 0x62, 0x70, + 0x63, 0x6B, 0x7B, 0xC5, 0x83, 0x6D, 0x49, 0x96, 0x99, 0xA8, 0x7C, 0xA3, 0x87, 0xCC, 0xB0, 0xB5, + 0xDB, 0x7E, 0xE2, 0x49, 0x87, 0x02, 0x79, 0xDE, 0xED, 0xB6, 0xC5, 0xF1, 0xE7, 0xA2, 0xA5, 0x37, + 0x72, 0xF8, 0xD4, 0x28, 0xCE, 0xE3, 0x90, 0x8B, 0x47, 0xE4, 0xD6, 0x25, 0x5B, 0x3F, 0xB9, 0xA1, + 0x1F, 0xC4, 0x11, 0xBF, 0xE7, 0x3A, 0xDC, 0x6D, 0x6B, 0x2F, 0x98, 0x77, 0xD6, 0x49, 0x21, 0x2C, + 0xF4, 0x8E, 0xE7, 0xC4, 0xDB, 0x3F, 0x86, 0x84, 0x49, 0xD7, 0x71, 0x54, 0xEC, 0x5D, 0x95, 0x12, + 0xFA, 0x98, 0xBA, 0x21, 0x2B, 0xF4, 0xB2, 0x0E, 0x12, 0xC8, 0x65, 0x46, 0x91, 0x69, 0xE9, 0x69, + 0x1F, 0x27, 0x3C, 0x2B, 0xF4, 0x8E, 0xA4, 0x41, 0xAD, 0x67, 0xEF, 0xE6, 0x51, 0x07, 0x5D, 0xC1, + 0x65, 0x90, 0x34, 0xAB, 0x01, 0xF6, 0x2F, 0xB4, 0x0E, 0xAD, 0x06, 0xA1, 0x2C, 0x79, 0x26, 0x0C, + 0x1F, 0x2C, 0xF1, 0x88, 0x02, 0x25, 0xA6, 0x4A, 0x62, 0x82, 0x40, 0x88, 0x2D, 0x48, 0x67, 0xAD, + 0x1F, 0x39, 0x8A, 0xEB, 0x69, 0xE4, 0xB2, 0x61, 0xE8, 0xA2, 0xB2, 0x43, 0x83, 0x6F, 0x79, 0x5E, + 0x35, 0x9B, 0xEB, 0x74, 0xF9, 0xA0, 0xF7, 0x44, 0xE1, 0x54, 0xB6, 0x3A, 0xD6, 0x49, 0x57, 0xD0, + 0xE4, 0xFA, 0x2D, 0xB4, 0x1C, 0x93, 0x56, 0x77, 0xC5, 0x93, 0x15, 0x18, 0x53, 0x8C, 0xC6, 0xB1, + 0x46, 0x98, 0x5E, 0x35, 0xB0, 0xD2, 0x70, 0xB1, 0x00, 0x5F, 0x48, 0x53, 0x54, 0x18, 0x89, 0xCA, + 0x0E, 0xD0, 0x4D, 0xA0, 0xC8, 0x15, 0x53, 0x19, 0xDF, 0x7C, 0x95, 0x57, 0xAB, 0xA5, 0xA2, 0xA4, + 0xF1, 0xA6, 0x92, 0xC7, 0x8F, 0xD6, 0x39, 0x81, 0x21, 0x4F, 0x4C, 0xA3, 0x41, 0x3E, 0x56, 0xDB, + 0x65, 0xA6, 0xA8, 0xEC, 0xF0, 0x4B, 0xE5, 0xB6, 0xD8, 0xC5, 0x06, 0x40, 0xF3, 0xC2, 0x04, 0x9B, + 0x51, 0x56, 0x96, 0xF9, 0x4E, 0x33, 0xA2, 0x01, 0x89, 0xA0, 0xAC, 0x92, 0x78, 0x6D, 0xC3, 0x90, + 0x1D, 0xA6, 0x69, 0x8E, 0x48, 0x55, 0x66, 0x8F, 0x8A, 0x8A, 0xC4, 0xE7, 0x29, 0xB7, 0x30, 0x9D, + 0xCC, 0x2D, 0x10, 0xF2, 0xC3, 0x24, 0x8B, 0xDD, 0x26, 0x4E, 0x8B, 0x4C, 0x2B, 0x81, 0xF1, 0x6A, + 0x30, 0x28, 0xBE, 0x09, 0xD1, 0xCC, 0x8C, 0x14, 0x9B, 0x97, 0xC0, 0xDF, 0x6D, 0x7A, 0xF9, 0xA7, + 0x9F, 0xEC, 0x90, 0xF9, 0x06, 0x40, 0x73, 0xC2, 0x04, 0x62, 0xFE, 0x22, 0xA9, 0xC4, 0xBB, 0x2F, + 0xB2, 0x17, 0x1C, 0xDC, 0xC7, 0xD0, 0x38, 0xDD, 0x4E, 0x9B, 0xED, 0xD1, 0xE6, 0xC4, 0x02, 0x4F, + 0x61, 0x15, 0xED, 0x0B, 0xF4, 0x85, 0x91, 0xA4, 0x9B, 0x5B, 0x9E, 0x2E, 0x93, 0x3E, 0x7B, 0x7F, + 0xE4, 0x18, 0x98, 0xE7, 0x5A, 0xBA, 0x26, 0x8F, 0x92, 0x4B, 0x72, 0x65, 0x72, 0x2C, 0x8C, 0xE5, + 0x25, 0xE3, 0x18, 0x6A, 0x9F, 0x29, 0x9B, 0x7D, 0x38, 0x5B, 0xCE, 0xDE, 0x9F, 0xE9, 0xEA, 0xAC, + 0x39, 0xAE, 0x2E, 0x8F, 0x3E, 0x82, 0x47, 0x20, 0x48, 0x56, 0x94, 0x22, 0x4D, 0x86, 0x29, 0xD2, + 0x04, 0x29, 0xE4, 0x40, 0x22, 0x3A, 0xF3, 0xDB, 0xBE, 0xBC, 0xF1, 0xB4, 0xC6, 0x19, 0x59, 0xAC, + 0x69, 0x75, 0x33, 0x47, 0xC8, 0x47, 0x97, 0x22, 0x14, 0x07, 0xE8, 0x8E, 0xB7, 0x3F, 0xFD, 0x6A, + 0x8B, 0xEF, 0xC6, 0x49, 0x0A, 0xF6, 0x49, 0x9F, 0xAE, 0xBF, 0x03, 0xAE, 0xC5, 0x26, 0x8D, 0xFD, + 0xD7, 0x2E, 0x3B, 0xFE, 0x6D, 0x73, 0xAF, 0x08, 0x5F, 0xB0, 0x6B, 0x94, 0xD1, 0x94, 0x7F, 0x81, + 0x8C, 0xC7, 0xC7, 0xD4, 0x94, 0x51, 0xEF, 0xFF, 0x02, 0x19, 0xEA, 0x52, 0x26, 0x0A, 0xB9, 0xD4, + 0x7F, 0x85, 0x90, 0x25, 0x11, 0xF2, 0x0A, 0x52, 0x16, 0x30, 0x6C, 0x7D, 0x53, 0xD4, 0xF5, 0xAF, + 0x91, 0x1A, 0xB4, 0x3E, 0xD0, 0x84, 0x48, 0x34, 0x33, 0x32, 0x12, 0x8D, 0x8A, 0x18, 0x22, 0xBF, + 0xCE, 0x8D, 0xCF, 0xA0, 0xDC, 0xF9, 0xE9, 0x70, 0x9C, 0xF3, 0x2A, 0xFF, 0xD6, 0xA1, 0x79, 0x79, + 0x3E, 0x9D, 0x8B, 0x3E, 0x66, 0x09, 0xF4, 0xC7, 0x98, 0xA6, 0x2B, 0xBE, 0x9D, 0x88, 0x0B, 0x51, + 0x98, 0xD6, 0xDF, 0xCF, 0xB6, 0x30, 0xE9, 0xEE, 0xAB, 0x01, 0x91, 0xC3, 0x6A, 0x31, 0x58, 0xD7, + 0x3D, 0x7E, 0x3C, 0x47, 0xA0, 0xE3, 0xF5, 0x22, 0x04, 0xF6, 0xEB, 0x50, 0x98, 0x24, 0xAA, 0xD7, + 0xA9, 0x3A, 0x6F, 0x9F, 0xA0, 0x92, 0x8A, 0x96, 0x56, 0x77, 0xFB, 0xA3, 0x4F, 0xC8, 0x79, 0xAB, + 0xAC, 0xD5, 0x5A, 0xEA, 0x33, 0xA4, 0x6F, 0x8E, 0xDB, 0xE2, 0x5C, 0x5D, 0x49, 0x8E, 0xD4, 0xEA, + 0x4C, 0x9F, 0x58, 0xD5, 0x90, 0xAD, 0xF6, 0xD0, 0x3A, 0x72, 0xF0, 0xFB, 0x42, 0x8D, 0xD3, 0xBB, + 0x37, 0x02, 0xAF, 0x9E, 0xB7, 0x03, 0x31, 0x5B, 0x1D, 0x05, 0x28, 0x4D, 0x91, 0x21, 0x89, 0xE3, + 0x35, 0xA0, 0x51, 0xCD, 0xF8, 0xAA, 0x54, 0x8E, 0xAD, 0x32, 0x4F, 0xB1, 0x55, 0x2A, 0xC7, 0x16, + 0xD9, 0xF4, 0x90, 0x68, 0x90, 0x9F, 0xCE, 0x99, 0x01, 0x9D, 0x6E, 0x52, 0xC8, 0x1D, 0x73, 0x3D, + 0x48, 0xC7, 0xDD, 0x73, 0x8E, 0x51, 0x1D, 0x4B, 0x0D, 0x97, 0x05, 0xC9, 0x11, 0xC4, 0x20, 0x50, + 0x63, 0x88, 0x8F, 0x26, 0x42, 0x57, 0x52, 0xF2, 0x4C, 0x09, 0x16, 0xA6, 0xF8, 0xE6, 0x42, 0xD3, + 0xF5, 0x8B, 0x96, 0xBA, 0x13, 0x55, 0xCE, 0x2D, 0x6B, 0xC3, 0x43, 0x74, 0x82, 0x0B, 0x59, 0xDF, + 0x73, 0x47, 0x5C, 0xF5, 0xDD, 0x44, 0x2D, 0x23, 0xB5, 0x12, 0x41, 0x16, 0x26, 0x28, 0x0A, 0x2E, + 0x56, 0x38, 0x57, 0x30, 0x2C, 0x74, 0x82, 0xE7, 0x40, 0xD8, 0x9F, 0x0E, 0x65, 0x0F, 0x17, 0x5F, + 0x6C, 0xF0, 0x19, 0x9A, 0x18, 0x54, 0x48, 0x7E, 0x73, 0x20, 0x44, 0xE6, 0x72, 0x4A, 0x9F, 0xE2, + 0x42, 0xDC, 0x42, 0x7D, 0xE9, 0x31, 0x31, 0xC9, 0x46, 0x15, 0xD3, 0x3D, 0x6B, 0xC2, 0xEB, 0x32, + 0x81, 0x8D, 0xA5, 0xD3, 0x38, 0xF2, 0xF1, 0x19, 0x0D, 0xCA, 0x7C, 0x43, 0x1D, 0x1C, 0xBF, 0x06, + 0x06, 0x9F, 0xE2, 0x94, 0xD0, 0xFB, 0x12, 0xE2, 0x44, 0x9F, 0xE2, 0x4F, 0x73, 0xCA, 0x20, 0x5E, + 0x3B, 0x39, 0xD4, 0x7B, 0x8B, 0x83, 0x12, 0x9C, 0x31, 0xE9, 0x4E, 0x62, 0x95, 0x39, 0x24, 0x67, + 0xF9, 0xEB, 0x9A, 0x43, 0x1D, 0xFF, 0x06, 0x9B, 0x47, 0xF5, 0xDD, 0x89, 0xDA, 0x0D, 0x45, 0xF9, + 0xE0, 0xD4, 0x89, 0xDE, 0x35, 0xB4, 0xAC, 0xE9, 0x75, 0xA3, 0xCB, 0x11, 0x14, 0x19, 0x50, 0xB9, + 0x10, 0x75, 0x67, 0xE9, 0xEB, 0x44, 0x91, 0x84, 0x76, 0xA7, 0x26, 0x33, 0x68, 0x6D, 0xA2, 0x58, + 0x4D, 0x95, 0x69, 0x72, 0xAE, 0xD2, 0x61, 0x4D, 0x8E, 0x42, 0x92, 0x58, 0x81, 0x92, 0xD8, 0x86, + 0xA9, 0xF6, 0x0E, 0xF8, 0x75, 0x7D, 0x88, 0x03, 0x98, 0xF7, 0xD0, 0xDC, 0x49, 0x0C, 0x3C, 0x35, + 0x02, 0x63, 0x2F, 0x1B, 0x41, 0xE0, 0xAE, 0x3A, 0x26, 0x0E, 0xC9, 0x35, 0x28, 0x77, 0x90, 0x01, + 0x5F, 0x05, 0x65, 0x5C, 0xA1, 0xE1, 0x04, 0x9E, 0xBA, 0xF7, 0xA3, 0x80, 0xBC, 0x83, 0xE4, 0x8C, + 0x08, 0x38, 0x2A, 0x20, 0x67, 0xD9, 0x4F, 0xCD, 0xBB, 0xDE, 0xC7, 0x15, 0x00, 0x2A, 0x02, 0x96, + 0x0F, 0xE2, 0xD7, 0x7C, 0x88, 0x34, 0xBA, 0xB8, 0x4F, 0xDA, 0x8C, 0x13, 0xFE, 0xEA, 0x22, 0xFC, + 0x96, 0x13, 0xEA, 0xB5, 0x7C, 0xDF, 0x9B, 0xF0, 0x1C, 0x15, 0xAC, 0xE5, 0x1A, 0x6D, 0xE0, 0xEA, + 0xC5, 0x15, 0x3F, 0x06, 0xF1, 0x53, 0x3E, 0x88, 0x5F, 0x5C, 0x7C, 0x65, 0xFF, 0x66, 0xAE, 0x45, + 0x05, 0x71, 0x96, 0x65, 0x6B, 0x91, 0x39, 0x79, 0xEB, 0x2E, 0xDC, 0xFA, 0x16, 0xC4, 0x1F, 0x1D, + 0xDE, 0xFC, 0xEE, 0x92, 0xA1, 0x7B, 0x70, 0x5C, 0xE2, 0x0A, 0xCB, 0x80, 0x76, 0x79, 0x93, 0xCE, + 0xC5, 0xBD, 0x5F, 0xA1, 0x71, 0x3D, 0x38, 0xC6, 0xD1, 0x71, 0x29, 0x05, 0x09, 0x70, 0x15, 0xC6, + 0x49, 0xA0, 0xD7, 0x55, 0x10, 0x1D, 0x97, 0x57, 0x38, 0xBA, 0x5A, 0x34, 0x41, 0x5C, 0x5C, 0x3B, + 0xE1, 0xB8, 0xFD, 0xBA, 0x08, 0xA2, 0xE3, 0xF2, 0x08, 0x47, 0xD7, 0xCB, 0x18, 0x88, 0x6D, 0xAC, + 0x7D, 0x70, 0x6C, 0xBD, 0xA6, 0x81, 0xC8, 0xB8, 0xB4, 0xE1, 0x64, 0x7D, 0xFA, 0x09, 0x71, 0x8D, + 0xB5, 0x0B, 0x07, 0xAE, 0x1C, 0xCA, 0x32, 0x9D, 0x77, 0x0E, 0xCC, 0xA4, 0xC3, 0xC2, 0x99, 0x04, + 0x47, 0x4A, 0x6D, 0xA4, 0xD4, 0x89, 0x94, 0xD9, 0x48, 0x99, 0x13, 0x69, 0x69, 0x23, 0x2D, 0x9D, + 0x48, 0xB9, 0x8D, 0x94, 0xDB, 0x48, 0xBF, 0x44, 0x69, 0x7E, 0xD5, 0x8A, 0xE4, 0xEF, 0xD8, 0x10, + 0x68, 0x10, 0x35, 0x4D, 0x4C, 0x20, 0xEB, 0x8E, 0x66, 0x4D, 0x69, 0x92, 0x58, 0x13, 0xB1, 0x86, + 0x52, 0x8D, 0x38, 0x15, 0x90, 0x0D, 0x37, 0x1B, 0x45, 0x89, 0x9B, 0x15, 0x94, 0xFA, 0x67, 0xC2, + 0xFA, 0x67, 0x6B, 0x38, 0x25, 0x66, 0x9F, 0x1A, 0x43, 0x7C, 0xF4, 0x60, 0x3D, 0x89, 0xBC, 0xAA, + 0xF9, 0x1C, 0xCD, 0xEC, 0x13, 0x64, 0x56, 0x83, 0x20, 0xB7, 0x04, 0xB4, 0xEE, 0xA9, 0xD6, 0x84, + 0x4A, 0x4A, 0xC4, 0xA5, 0x21, 0x1B, 0x28, 0xF3, 0xAB, 0xA1, 0x3C, 0xC3, 0x4F, 0x7D, 0x86, 0x3D, + 0xF4, 0x3F, 0x53, 0xEE, 0x3F, 0x3F, 0xB1, 0x2C, 0x3F, 0xF5, 0x59, 0xF6, 0xA1, 0xC9, 0xAD, 0x02, + 0x71, 0xEC, 0xE5, 0x6A, 0xC5, 0xF4, 0x62, 0x28, 0x30, 0x3E, 0x75, 0xBC, 0x83, 0xC2, 0xD0, 0xD4, + 0xA6, 0x12, 0xC5, 0x54, 0xA9, 0x2E, 0x9E, 0xD1, 0xA9, 0x82, 0x01, 0x7E, 0x5B, 0x31, 0xDE, 0x08, + 0xF2, 0xC8, 0xF0, 0x51, 0x2A, 0xA8, 0x8F, 0x58, 0x6D, 0xEF, 0xC5, 0xC6, 0x0E, 0x49, 0xEC, 0x46, + 0x49, 0x0C, 0x94, 0xC4, 0x42, 0xC1, 0xA8, 0x65, 0xC0, 0xC7, 0x8A, 0x61, 0xE6, 0x43, 0x4B, 0x2C, + 0x34, 0xCE, 0x4D, 0x4C, 0x7E, 0xAF, 0x76, 0x70, 0x34, 0x86, 0x73, 0x7C, 0x26, 0x58, 0x2A, 0xC1, + 0xC9, 0xAB, 0xCB, 0x3C, 0xE1, 0xE9, 0xB2, 0xC9, 0xBE, 0x10, 0xDD, 0xED, 0x1E, 0x30, 0x62, 0x9A, + 0x9C, 0xC8, 0x64, 0x35, 0x32, 0xA7, 0xB0, 0x54, 0xC1, 0x5C, 0xA0, 0x4C, 0x82, 0x12, 0x07, 0x64, + 0xA9, 0x20, 0x4E, 0xAA, 0x5C, 0xC2, 0x32, 0x02, 0x21, 0x9B, 0x74, 0xBA, 0xDF, 0x74, 0x85, 0x85, + 0x53, 0x20, 0x3F, 0xAD, 0x9A, 0x13, 0xFB, 0x02, 0xCA, 0x0D, 0xD1, 0xA9, 0xFD, 0x42, 0x46, 0xAA, + 0x92, 0x87, 0x08, 0xF5, 0x5E, 0xE2, 0x75, 0x70, 0xA7, 0x71, 0x88, 0x41, 0x21, 0xB7, 0x57, 0xDD, + 0xF4, 0x1A, 0x38, 0x40, 0x2E, 0x6F, 0x07, 0xD4, 0xBF, 0xBA, 0xC9, 0x35, 0xD0, 0x24, 0x37, 0xF6, + 0x48, 0xB5, 0x9D, 0x8D, 0x14, 0x66, 0x63, 0x4E, 0x03, 0x66, 0xE4, 0x14, 0x90, 0xE8, 0xC3, 0xD7, + 0x76, 0xE5, 0x5B, 0xB3, 0x1E, 0x02, 0x5C, 0x16, 0x32, 0x49, 0xBA, 0x44, 0x1F, 0x91, 0x5E, 0xF6, + 0xB2, 0x68, 0x74, 0x1A, 0x23, 0x21, 0x6E, 0x66, 0xA5, 0x0D, 0x19, 0xC0, 0x72, 0x31, 0x2B, 0xC5, + 0x6F, 0x04, 0xEA, 0x5E, 0x56, 0xA2, 0xCF, 0x10, 0xCC, 0xB5, 0x64, 0xEA, 0xB8, 0x63, 0x21, 0x1A, + 0xBA, 0x95, 0xA2, 0x1D, 0x75, 0x2A, 0x0B, 0x0B, 0xAD, 0x69, 0xA6, 0xFA, 0xED, 0x09, 0x45, 0xB3, + 0x53, 0x9D, 0x1C, 0x7E, 0xC7, 0x8E, 0x8E, 0xA1, 0x6A, 0xBB, 0x63, 0x82, 0xC7, 0xE8, 0x88, 0x80, + 0x16, 0x47, 0x7C, 0x91, 0xE6, 0xC6, 0xD6, 0xB6, 0x36, 0x09, 0x54, 0x8A, 0x07, 0x1D, 0x1D, 0x0E, + 0x09, 0xBA, 0x34, 0x37, 0x49, 0x6F, 0x20, 0x4C, 0x72, 0x59, 0x47, 0x0D, 0xB7, 0xE4, 0xB3, 0x60, + 0x57, 0x35, 0x79, 0x8A, 0x12, 0x0E, 0x8F, 0x35, 0x2C, 0xE6, 0xA0, 0x44, 0x83, 0x1C, 0x54, 0xA9, + 0x06, 0xA5, 0x1C, 0x94, 0x69, 0x50, 0xC6, 0x41, 0x4B, 0x0D, 0x5A, 0x72, 0x50, 0xAE, 0x41, 0x39, + 0x07, 0xD5, 0x45, 0x9F, 0x01, 0x6B, 0xBE, 0x22, 0x26, 0x41, 0x5D, 0x84, 0x26, 0x3B, 0x3D, 0xB1, + 0xA3, 0x64, 0xDA, 0xC0, 0xB4, 0x07, 0x72, 0x58, 0xD6, 0xC1, 0x12, 0x06, 0x5A, 0xF6, 0x20, 0x07, + 0x5D, 0xDE, 0x01, 0x33, 0x0A, 0x92, 0x8E, 0xA9, 0xA1, 0x7C, 0xF4, 0x75, 0xE9, 0xB3, 0xC0, 0xA7, + 0x8A, 0xD6, 0x59, 0xD2, 0x98, 0x90, 0x25, 0x36, 0x19, 0xE6, 0x93, 0xD3, 0x6A, 0x20, 0x61, 0x90, + 0x12, 0x06, 0x43, 0xF4, 0x0E, 0xF2, 0xCC, 0x26, 0x4F, 0xFC, 0xD4, 0x09, 0x27, 0x5E, 0x12, 0xE2, + 0x21, 0xE1, 0x89, 0x4B, 0x7A, 0x6E, 0x33, 0xC8, 0xFC, 0xE4, 0x19, 0x23, 0x96, 0x05, 0x62, 0xD3, + 0x8B, 0x14, 0x37, 0x3D, 0x2F, 0xB1, 0x5F, 0xB1, 0xC4, 0xC8, 0xB2, 0x01, 0xBD, 0x58, 0x48, 0xC8, + 0x12, 0x83, 0x4C, 0x17, 0x89, 0x8F, 0x56, 0x83, 0x09, 0x83, 0xD4, 0x62, 0x30, 0x4C, 0xEF, 0x20, + 0xCF, 0x4C, 0xF2, 0x64, 0x88, 0x3A, 0xE1, 0xC4, 0x4B, 0x8B, 0x78, 0x58, 0x78, 0xE2, 0x92, 0x9E, + 0x9B, 0x0C, 0xB2, 0x21, 0xF2, 0x8C, 0x11, 0xEB, 0x12, 0x43, 0x7A, 0xF1, 0xDD, 0x47, 0xCF, 0x4B, + 0xEC, 0x4C, 0x4A, 0xCC, 0x06, 0xFA, 0xCA, 0x05, 0x31, 0x88, 0xE1, 0x39, 0x02, 0x31, 0x2D, 0x83, + 0x33, 0xEB, 0x31, 0x0C, 0x62, 0x1E, 0x06, 0xF7, 0x59, 0x00, 0x51, 0x2A, 0xD6, 0x90, 0xD8, 0x60, + 0x7F, 0x83, 0x81, 0x38, 0xAC, 0x4D, 0xE0, 0x28, 0xAC, 0xDE, 0x73, 0x0C, 0x5E, 0xB9, 0x19, 0x0E, + 0xAB, 0xBF, 0x0C, 0x63, 0xB4, 0x92, 0x02, 0xD2, 0x06, 0xB2, 0x3C, 0x54, 0xE5, 0x36, 0x90, 0xE7, + 0xD1, 0x6A, 0xB5, 0x89, 0x52, 0x8A, 0xE4, 0xC0, 0xC9, 0x08, 0x4E, 0xC2, 0x50, 0x20, 0xDB, 0xA3, + 0xB5, 0x60, 0x03, 0xF9, 0x1E, 0xF1, 0xF5, 0x8D, 0xCA, 0xF8, 0x88, 0x47, 0xB7, 0x90, 0x73, 0x7F, + 0xE7, 0xD0, 0x42, 0xBE, 0x79, 0xF3, 0x4F, 0x50, 0x52, 0x1B, 0xC5, 0x81, 0x91, 0x59, 0x18, 0x09, + 0x47, 0x58, 0xDA, 0x08, 0x0E, 0x1E, 0x90, 0xDF, 0xC1, 0xB6, 0xB8, 0xC5, 0xDC, 0xFA, 0xDA, 0x5B, + 0x31, 0x6D, 0xC4, 0x5D, 0x6E, 0x0B, 0x90, 0xF4, 0x00, 0xCC, 0x21, 0x42, 0x53, 0x84, 0x72, 0x60, + 0xD6, 0x03, 0x13, 0x06, 0x5B, 0x22, 0xCC, 0x41, 0x99, 0xF7, 0xD0, 0x8C, 0xC2, 0x2E, 0xA8, 0xAA, + 0xA3, 0x33, 0xD7, 0x10, 0x5E, 0x60, 0x40, 0x88, 0x59, 0xE1, 0xB5, 0x93, 0x53, 0x6B, 0x28, 0x61, + 0x91, 0x52, 0x16, 0x83, 0x1C, 0x1C, 0x0C, 0x32, 0xC2, 0x20, 0x19, 0xA0, 0x4F, 0x38, 0xF9, 0x92, + 0x92, 0x0F, 0x2A, 0x90, 0xB8, 0x34, 0xC8, 0x09, 0x8B, 0xCC, 0xCF, 0x80, 0x5B, 0xFF, 0x57, 0xB4, + 0x3E, 0x6D, 0xE6, 0xE9, 0xD5, 0x8E, 0x98, 0x10, 0x26, 0x26, 0xA1, 0xDF, 0xF6, 0xBC, 0x11, 0x41, + 0x16, 0xA9, 0xCD, 0x62, 0x84, 0x83, 0x83, 0x41, 0x66, 0x31, 0x48, 0x06, 0xE9, 0x13, 0x4E, 0xBE, + 0xB4, 0xC9, 0x47, 0x14, 0x48, 0x5C, 0x1A, 0xE4, 0x16, 0x8B, 0x6C, 0x88, 0x01, 0xB7, 0xFE, 0x99, + 0x5A, 0xDF, 0x86, 0x7A, 0x4D, 0x8C, 0x28, 0xD4, 0x84, 0x1C, 0x83, 0xDA, 0x88, 0x21, 0x70, 0x2B, + 0x30, 0x14, 0x9A, 0x4B, 0x8A, 0x50, 0xF1, 0x3A, 0x6C, 0xC3, 0x47, 0xAB, 0xAA, 0x40, 0xE2, 0x95, + 0x91, 0xE3, 0xF0, 0xFA, 0xC6, 0x50, 0x46, 0xEB, 0x94, 0x40, 0xE2, 0xB5, 0x86, 0xA2, 0x88, 0x7E, + 0x72, 0xB0, 0x06, 0x88, 0x8E, 0x72, 0xDC, 0xC9, 0x45, 0x4F, 0x39, 0xEA, 0xC8, 0xA2, 0xAB, 0x1C, + 0xF3, 0x56, 0xD1, 0x57, 0x8E, 0x3B, 0xA4, 0xE8, 0x2C, 0xC7, 0xBC, 0x4E, 0xF4, 0x83, 0x03, 0xED, + 0xAA, 0xE8, 0x08, 0xC7, 0x1A, 0x4E, 0xD1, 0x13, 0x8E, 0x34, 0x8D, 0xA2, 0x2B, 0x1C, 0x6E, 0xFD, + 0x44, 0x5F, 0x38, 0xD6, 0xBC, 0x89, 0xCE, 0x70, 0xB0, 0x05, 0x93, 0x61, 0x48, 0x9A, 0xE7, 0xE3, + 0xB3, 0x5C, 0x23, 0xB9, 0x7A, 0xE3, 0xF9, 0x21, 0xCE, 0xDC, 0x22, 0x17, 0xB9, 0xC5, 0x50, 0x26, + 0xEA, 0xD9, 0x05, 0xFD, 0xAE, 0xD2, 0xEC, 0x83, 0x19, 0x68, 0xF4, 0xF3, 0x2F, 0x94, 0x20, 0x75, + 0x10, 0xA4, 0x9A, 0x60, 0xF1, 0xC8, 0xF1, 0x33, 0x07, 0xBE, 0xC6, 0x5E, 0x71, 0xEC, 0x25, 0xC7, + 0x4E, 0xD7, 0x1D, 0xF7, 0x8C, 0xE3, 0xE7, 0x06, 0x7E, 0xC2, 0x8B, 0x0A, 0x30, 0x56, 0x26, 0x06, + 0x03, 0x9F, 0xA3, 0xC3, 0xB9, 0xA8, 0x0F, 0xDB, 0xAB, 0x11, 0xE4, 0x50, 0xA5, 0x50, 0x3C, 0x15, + 0x30, 0xD0, 0xC4, 0x53, 0x29, 0x16, 0xDE, 0x2F, 0x7A, 0x23, 0xCF, 0x0C, 0x90, 0x98, 0xC5, 0xB1, + 0x0B, 0xA7, 0x3A, 0x59, 0x58, 0x3A, 0xCD, 0xC6, 0x44, 0xA1, 0x88, 0xB8, 0x64, 0xEC, 0x36, 0xCF, + 0x75, 0x69, 0xA1, 0xAC, 0x9D, 0x28, 0x44, 0xA0, 0x4A, 0x32, 0xF1, 0xEA, 0x27, 0xF0, 0x06, 0xEB, + 0xFA, 0x06, 0x81, 0xB6, 0x8D, 0x0D, 0x06, 0x7B, 0xDB, 0x18, 0x72, 0x59, 0x8A, 0xE0, 0x50, 0x94, + 0x7A, 0x6F, 0x21, 0xA4, 0x26, 0xF8, 0x5C, 0x5D, 0xBA, 0x15, 0x4E, 0x12, 0x42, 0x92, 0x61, 0x55, + 0xC7, 0xD2, 0xC4, 0x11, 0xCD, 0x18, 0x47, 0xD2, 0x8B, 0x6A, 0x88, 0xE7, 0x58, 0x54, 0x23, 0x71, + 0x01, 0xD4, 0x39, 0x1C, 0x57, 0xB0, 0x80, 0x21, 0xA2, 0x37, 0x47, 0xB0, 0x65, 0x7D, 0x70, 0x50, + 0x6A, 0xC8, 0x30, 0xB9, 0x34, 0xC8, 0xF9, 0x09, 0x56, 0x38, 0xF7, 0x4F, 0x8C, 0x83, 0x09, 0x64, + 0x4C, 0xE0, 0xF4, 0x59, 0x75, 0xDA, 0x0A, 0xAB, 0x93, 0x60, 0x99, 0x3D, 0x80, 0x91, 0xBC, 0x79, + 0x79, 0x71, 0x93, 0xF4, 0x00, 0x6E, 0xC7, 0xE2, 0x45, 0x56, 0x8A, 0xDF, 0x18, 0x0D, 0x42, 0x28, + 0x91, 0xDA, 0x39, 0xE2, 0x21, 0x37, 0x19, 0xDE, 0xF1, 0x99, 0x61, 0xAA, 0x24, 0x8A, 0xA9, 0xE2, + 0x74, 0x5E, 0x5D, 0x01, 0x39, 0x11, 0xD5, 0x0C, 0x05, 0x8A, 0x70, 0xCA, 0xCA, 0x79, 0x53, 0x84, + 0x61, 0xF1, 0xDB, 0x1F, 0x5F, 0x7D, 0xF1, 0xF8, 0xB8, 0x5C, 0x53, 0x3C, 0xF7, 0x6D, 0x0E, 0x86, + 0xC5, 0x6F, 0x68, 0x50, 0x14, 0xF7, 0xDD, 0x0B, 0x8A, 0xE5, 0xBC, 0x4F, 0x41, 0x91, 0x5C, 0xB7, + 0x24, 0x18, 0x0E, 0xBF, 0xFD, 0xC0, 0x50, 0x64, 0xB1, 0xA0, 0x3E, 0x3B, 0x82, 0xA0, 0x1E, 0xD9, + 0x1B, 0xE1, 0xD1, 0xBC, 0x39, 0x57, 0xA5, 0x7D, 0x81, 0x86, 0xB3, 0xA9, 0xE1, 0xD2, 0x9E, 0xD8, + 0xC8, 0xE6, 0x01, 0xC5, 0xF2, 0xB9, 0x5B, 0x2B, 0xC4, 0xA6, 0x0F, 0x1C, 0x3A, 0x69, 0x4E, 0x55, + 0x5B, 0x75, 0x06, 0x39, 0x1C, 0x9F, 0xAA, 0xD3, 0xC1, 0x6A, 0x30, 0x36, 0xFB, 0xDE, 0x29, 0xF8, + 0xAD, 0x42, 0xEE, 0x1F, 0x80, 0x8E, 0xDE, 0xC1, 0x09, 0x78, 0x2E, 0x81, 0x40, 0xBB, 0x09, 0x47, + 0xE7, 0x1E, 0x03, 0xE8, 0xCA, 0x5F, 0x38, 0x2E, 0x77, 0x1D, 0xC0, 0xD5, 0x8E, 0xC3, 0xD1, 0xB9, + 0x0F, 0x01, 0xBA, 0xF6, 0x20, 0x8E, 0xCD, 0x9D, 0x09, 0xB0, 0xB5, 0x2B, 0x71, 0x64, 0xF4, 0x2A, + 0x44, 0x56, 0x3E, 0xC5, 0x71, 0xD1, 0x35, 0x10, 0x57, 0xFA, 0x8E, 0x53, 0xE5, 0x9D, 0x8D, 0xA8, + 0xCA, 0x3B, 0x08, 0xD3, 0xB8, 0x74, 0xCD, 0xF1, 0x0D, 0x20, 0x21, 0xDB, 0x8B, 0x8B, 0xBD, 0x90, + 0xCC, 0x82, 0x79, 0xE0, 0x38, 0xAA, 0x43, 0xB1, 0x3C, 0x0B, 0x9F, 0x7D, 0x15, 0x3D, 0x4C, 0x7D, + 0x75, 0xBD, 0x1A, 0x0B, 0xE9, 0x48, 0xA2, 0x9E, 0x8F, 0xF5, 0x83, 0x09, 0xC4, 0x2B, 0x0B, 0x96, + 0x3C, 0xDC, 0xC2, 0x00, 0xC0, 0xA4, 0xF9, 0xE1, 0x14, 0xE4, 0x91, 0xA7, 0x7A, 0x45, 0xDF, 0x93, + 0xB8, 0x4C, 0x9E, 0x1F, 0x4E, 0x41, 0x64, 0x5E, 0x22, 0x92, 0xE8, 0x6B, 0xBC, 0x1C, 0x4D, 0xE6, + 0x9E, 0xA0, 0x31, 0xED, 0x65, 0x19, 0x42, 0xB3, 0xE3, 0x8A, 0xA5, 0xED, 0xC0, 0x8B, 0x62, 0x82, + 0x19, 0xBB, 0x90, 0x12, 0xC6, 0xCE, 0xC3, 0x2D, 0x0D, 0x96, 0x9B, 0x51, 0xCC, 0xCC, 0x83, 0xB8, + 0x3D, 0x9C, 0xB6, 0x75, 0x75, 0x65, 0x17, 0xD1, 0x5D, 0xB8, 0x2F, 0x87, 0xBA, 0x66, 0x98, 0x6E, + 0xBE, 0xE4, 0x20, 0xA4, 0x9C, 0x67, 0xF8, 0xB4, 0x66, 0xE1, 0x07, 0xC7, 0xB3, 0x27, 0xC6, 0x69, + 0xA1, 0x54, 0xF6, 0xC5, 0xD6, 0x50, 0x09, 0x0A, 0xFB, 0x3A, 0x81, 0x96, 0x3F, 0xA5, 0x17, 0x20, + 0x46, 0x0D, 0x4B, 0xC3, 0x29, 0x3F, 0x09, 0xB4, 0xE9, 0xEF, 0x8B, 0xEE, 0xEA, 0x01, 0x5E, 0x6F, + 0x77, 0x5D, 0x3E, 0x38, 0x1C, 0x39, 0x1E, 0xBF, 0xD0, 0xE0, 0xBF, 0x63, 0xA6, 0xEF, 0x67, 0x34, + 0x01, 0x57, 0x34, 0xDA, 0x66, 0xFC, 0x96, 0x46, 0xDB, 0x0C, 0x5D, 0xD4, 0x28, 0x01, 0x1E, 0x72, + 0x03, 0xA2, 0x6D, 0xA6, 0x5E, 0x82, 0x68, 0x9B, 0xF1, 0x7B, 0x10, 0x6D, 0xC3, 0xAF, 0x42, 0x50, + 0x8C, 0xF1, 0xDB, 0x10, 0x6D, 0x13, 0x7E, 0x21, 0xA2, 0x6D, 0xA6, 0xDC, 0x89, 0x68, 0x1B, 0x7E, + 0x2D, 0x82, 0x62, 0x4C, 0xBB, 0x19, 0xD1, 0x36, 0xC3, 0x97, 0x23, 0x04, 0x05, 0x20, 0x85, 0x1C, + 0xA6, 0x6B, 0x9B, 0xD0, 0xF3, 0x74, 0x6D, 0x33, 0xED, 0x48, 0x5D, 0xDB, 0xDC, 0x7A, 0xAA, 0xAE, + 0x6D, 0xEE, 0x3B, 0x58, 0xD7, 0x36, 0x21, 0x67, 0xEB, 0xDA, 0x26, 0xE8, 0x78, 0x5D, 0x13, 0x7C, + 0xC2, 0xAE, 0x09, 0x3D, 0x64, 0xD7, 0x36, 0x41, 0xE7, 0xEC, 0xDA, 0x26, 0xF8, 0xA8, 0x5D, 0xDB, + 0xDC, 0x72, 0xDA, 0xAE, 0x6D, 0xF0, 0xC0, 0x1D, 0x85, 0xD8, 0x67, 0xEE, 0x38, 0x38, 0x55, 0x60, + 0x0F, 0xD4, 0x7F, 0xF2, 0x0E, 0x80, 0x4B, 0x05, 0xF4, 0xD0, 0x86, 0x9E, 0xBF, 0x6B, 0xEE, 0x3A, + 0x82, 0xD7, 0x36, 0x37, 0x9F, 0xC2, 0x6B, 0x9B, 0xDB, 0x0F, 0xE2, 0xB5, 0xCD, 0xFD, 0x67, 0xF1, + 0xDA, 0xE6, 0xDE, 0xE3, 0x78, 0x6D, 0x73, 0xD7, 0x89, 0xBC, 0xE6, 0xA6, 0x43, 0x79, 0x6D, 0x33, + 0xF9, 0x5C, 0xDE, 0x9F, 0xAD, 0xDD, 0x4D, 0xCA, 0x16, 0x31, 0x10, 0x04, 0xE0, 0xBD, 0xA7, 0x10, + 0xDC, 0x09, 0xFE, 0x0B, 0x82, 0xE0, 0x0D, 0x74, 0x23, 0x78, 0x00, 0xB5, 0xA5, 0x11, 0xAA, 0x51, + 0x8C, 0x42, 0xA3, 0x78, 0x77, 0x13, 0x15, 0x47, 0x3B, 0xAF, 0xD3, 0x95, 0xF4, 0xBB, 0x13, 0xBA, + 0xE6, 0xC3, 0xC9, 0x98, 0x08, 0xF3, 0x94, 0x63, 0xB3, 0xF5, 0x6A, 0x5E, 0xB3, 0xCD, 0x76, 0x9E, + 0x6D, 0x16, 0xF4, 0x6C, 0xBB, 0xA3, 0xD7, 0x6C, 0xAB, 0xA6, 0xD7, 0x6C, 0xAF, 0xA9, 0xD7, 0xAC, + 0x56, 0xD6, 0x6B, 0x56, 0xEA, 0xEB, 0xD9, 0x56, 0x65, 0xAF, 0xD9, 0x52, 0x6B, 0xCF, 0x36, 0x8A, + 0x7B, 0xCD, 0x16, 0xBB, 0x7B, 0xCD, 0x56, 0xEB, 0x7B, 0xCD, 0xB6, 0x1A, 0x7C, 0xB6, 0x54, 0xE2, + 0x6B, 0x96, 0xF6, 0xF8, 0x9A, 0x85, 0x2A, 0x5F, 0x9C, 0x3E, 0xF8, 0x3D, 0xBD, 0x78, 0x6D, 0x28, + 0xF4, 0x85, 0x69, 0xE8, 0xF4, 0x85, 0x69, 0xA8, 0xF5, 0x85, 0x69, 0x68, 0xF6, 0xC5, 0xE9, 0xFF, + 0xCB, 0x7D, 0xED, 0xFF, 0xFD, 0xBE, 0x96, 0x54, 0xFC, 0xDA, 0x79, 0xCB, 0xAF, 0x9D, 0x16, 0xFD, + 0x5A, 0xD2, 0xF5, 0x6B, 0xA7, 0x75, 0xBF, 0x96, 0x36, 0xFE, 0x9A, 0xED, 0x96, 0xFE, 0xFA, 0x95, + 0xE5, 0xDE, 0x5F, 0xB3, 0x6A, 0xF5, 0xAF, 0x59, 0xAD, 0xFD, 0xD7, 0xAF, 0x2F, 0x17, 0x00, 0x9B, + 0xD5, 0x3A, 0x80, 0xCD, 0xD2, 0x86, 0x51, 0x56, 0x03, 0x6C, 0xB6, 0xDB, 0x04, 0x6C, 0x56, 0x2F, + 0x03, 0x36, 0xAB, 0xF6, 0x01, 0x9B, 0xD5, 0x2A, 0x81, 0xCD, 0xEA, 0xAD, 0xC0, 0x66, 0xB5, 0x62, + 0x60, 0xB3, 0x72, 0x37, 0x70, 0x7A, 0x8C, 0xFF, 0xCE, 0xD3, 0x87, 0xF5, 0x3B, 0x94, 0x97, 0x04, + 0x9B, 0xA5, 0x3D, 0xC1, 0x66, 0x44, 0x55, 0xB0, 0x59, 0xDA, 0x16, 0x4C, 0x97, 0xA5, 0xA7, 0xDE, + 0xFD, 0xEF, 0x1C, 0x3A, 0x12, 0xC4, 0x79, 0x33, 0x62, 0xF9, 0x91, 0x32, 0x52, 0xE9, 0xB1, 0x31, + 0x42, 0xC4, 0xD9, 0x30, 0x62, 0xE9, 0xF6, 0x1F, 0x21, 0x66, 0x8F, 0xF7, 0xDC, 0x9B, 0x9E, 0x4B, + 0x8B, 0x84, 0xCD, 0xB8, 0x2E, 0x61, 0x33, 0xAA, 0x4E, 0xD8, 0x8C, 0x69, 0x14, 0x36, 0xE3, 0x4A, + 0x85, 0xCD, 0x98, 0x5E, 0x61, 0x33, 0xB2, 0x5A, 0x78, 0x2C, 0xC7, 0xF1, 0xB7, 0x50, 0x08, 0x30, + 0x05, 0xC3, 0x66, 0x44, 0xC7, 0xB0, 0x59, 0x5E, 0x33, 0x6C, 0xC6, 0x34, 0x0D, 0x9B, 0xE5, 0x65, + 0xC3, 0x66, 0x4C, 0xDF, 0xB0, 0xD9, 0x5C, 0x39, 0x3C, 0x66, 0xE7, 0xAD, 0xC3, 0x9F, 0x77, 0x7D, + 0x04, 0x2E, 0xCD, 0xCF, 0xBA, 0x87, 0x3F, 0xEF, 0xF5, 0x18, 0x5F, 0xBC, 0xFE, 0xB4, 0x81, 0xD8, + 0xE7, 0xFB, 0x25, 0xC4, 0x66, 0x69, 0xB9, 0x89, 0xE8, 0x21, 0xF6, 0x9F, 0x52, 0xAE, 0x22, 0x36, + 0xAB, 0xB6, 0x11, 0x9B, 0xA5, 0xE5, 0x29, 0xA2, 0x90, 0xD8, 0xAC, 0xDA, 0x49, 0x6C, 0xB6, 0x5F, + 0x4B, 0x6C, 0x76, 0x8D, 0x66, 0x62, 0xB3, 0x7A, 0x39, 0xB1, 0x59, 0xB5, 0x9F, 0xD8, 0xEC, 0x1A, + 0x15, 0xC5, 0x66, 0xD5, 0x96, 0xE2, 0xFC, 0x3C, 0xFE, 0x0D, 0xE4, 0x8B, 0x3E, 0x52, 0xE9, 0xA2, + 0xFE, 0x0E, 0xE5, 0x8D, 0xC5, 0x74, 0x5D, 0x7E, 0xA7, 0xF2, 0xDE, 0x62, 0xB3, 0xB4, 0xBA, 0x48, + 0x6C, 0xF0, 0x91, 0x23, 0xB6, 0xF0, 0x88, 0xE5, 0xBB, 0x74, 0xA4, 0x98, 0x9D, 0x38, 0x72, 0xF9, + 0x5E, 0xEB, 0xA9, 0xF1, 0xB7, 0x58, 0x5E, 0x66, 0x6C, 0x46, 0xF6, 0x19, 0x9B, 0x71, 0x95, 0xC6, + 0x66, 0x54, 0xAB, 0xB1, 0x19, 0x59, 0x6C, 0x6C, 0x46, 0x75, 0x1B, 0x8F, 0x7B, 0x3D, 0x4E, 0xEC, + 0x98, 0xA0, 0x1A, 0x8E, 0xCD, 0x98, 0x92, 0x63, 0x33, 0xA2, 0xE7, 0xD8, 0x8C, 0xAA, 0x3A, 0x1E, + 0x77, 0x18, 0xCE, 0xC6, 0xD0, 0xB5, 0x31, 0xB2, 0x21, 0xD6, 0x8C, 0x2B, 0x89, 0x35, 0x63, 0x7A, + 0x62, 0xFF, 0xF9, 0xB2, 0xE2, 0x21, 0x6B, 0x26, 0x84, 0xAC, 0x99, 0xE4, 0xB2, 0x66, 0x72, 0x2E, + 0x6B, 0x46, 0x7D, 0x5B, 0xCC, 0x96, 0x3F, 0x2F, 0x66, 0xC4, 0x17, 0xC6, 0x2C, 0xFD, 0xC8, 0x98, + 0x11, 0xDF, 0x19, 0xB3, 0x85, 0x4F, 0x8D, 0xD9, 0xD2, 0xD7, 0xC6, 0x2C, 0xFD, 0xE0, 0x98, 0x2D, + 0x7E, 0x73, 0xCC, 0x84, 0x90, 0x35, 0x13, 0x46, 0xD6, 0x4C, 0x58, 0x59, 0x33, 0x59, 0x93, 0x35, + 0x93, 0x5D, 0x59, 0x33, 0xA9, 0xC9, 0x9A, 0x09, 0x23, 0x6B, 0x26, 0x8C, 0xAC, 0x99, 0xB0, 0xB2, + 0x66, 0xC2, 0xCA, 0x9A, 0x09, 0x25, 0x6B, 0x26, 0xB4, 0xAC, 0x99, 0xEC, 0xC8, 0x9A, 0xC9, 0x21, + 0x6B, 0x71, 0x12, 0x64, 0x6D, 0x1A, 0x3F, 0xFC, 0x35, 0xFE, 0xCF, 0xF4, 0x90, 0xB5, 0x79, 0x18, + 0x64, 0x2D, 0x8E, 0x49, 0x59, 0x33, 0x29, 0xC9, 0x9A, 0xC9, 0xB6, 0xAC, 0x99, 0xEC, 0xCB, 0x9A, + 0x49, 0x5D, 0xD6, 0x4C, 0xAA, 0xB2, 0x66, 0x52, 0x91, 0x35, 0x93, 0x2D, 0x59, 0x33, 0x59, 0x96, + 0x35, 0x93, 0x75, 0x59, 0x33, 0xD9, 0x93, 0x35, 0x93, 0x3D, 0x59, 0x33, 0xD9, 0x96, 0x35, 0x93, + 0x2D, 0x59, 0x33, 0xD9, 0x93, 0x35, 0x93, 0x9A, 0xAC, 0x99, 0x54, 0x64, 0xCD, 0x64, 0x4B, 0xD6, + 0x4C, 0x56, 0x64, 0xCD, 0x64, 0x43, 0xD6, 0x4C, 0x16, 0x65, 0xCD, 0x64, 0x55, 0xD6, 0x4C, 0x76, + 0x64, 0xCD, 0x64, 0x49, 0xD6, 0x4C, 0x52, 0x59, 0x33, 0x09, 0xB2, 0x16, 0xA7, 0x0F, 0x7E, 0x4F, + 0x2F, 0x5E, 0x1B, 0x64, 0x2D, 0x4C, 0x83, 0xAC, 0x85, 0x69, 0x90, 0xB5, 0x30, 0x0D, 0xB2, 0x16, + 0xA7, 0xFF, 0x97, 0xB5, 0x3E, 0x3C, 0x64, 0x2D, 0x8E, 0xA2, 0xAC, 0x4D, 0xF3, 0x87, 0x7F, 0xE6, + 0x97, 0xC6, 0x41, 0xD6, 0xC2, 0x34, 0xC8, 0xDA, 0x34, 0x0F, 0xB2, 0x16, 0xA6, 0x89, 0xAC, 0xF5, + 0xC4, 0xAE, 0xAC, 0xF5, 0x2B, 0xCB, 0xB2, 0x66, 0x52, 0x95, 0x35, 0x93, 0x9A, 0xAC, 0xF5, 0xEB, + 0xCB, 0xB2, 0x66, 0x52, 0x93, 0x35, 0x93, 0xF4, 0xAD, 0x7B, 0x26, 0x6B, 0x26, 0xBB, 0xB2, 0x66, + 0x52, 0x97, 0x35, 0x93, 0xAA, 0xAC, 0x99, 0xD4, 0x64, 0xCD, 0xA4, 0x2E, 0x6B, 0x26, 0x35, 0x59, + 0x33, 0x29, 0xCB, 0xDA, 0xF4, 0x18, 0xFF, 0x9D, 0xA7, 0x0F, 0xEB, 0x77, 0x28, 0x97, 0x35, 0x93, + 0x54, 0xD6, 0x4C, 0x08, 0x59, 0x33, 0x49, 0x65, 0x2D, 0x5D, 0x96, 0x9E, 0x7A, 0xF7, 0xBF, 0x73, + 0xE8, 0x48, 0x10, 0xE7, 0xCD, 0x88, 0xE5, 0x47, 0xCA, 0x48, 0xA5, 0xC7, 0xC6, 0x08, 0x11, 0x67, + 0xC3, 0x88, 0xA5, 0xDB, 0x7F, 0x84, 0x98, 0x3D, 0xDE, 0x73, 0x6F, 0x7A, 0x2E, 0x95, 0x35, 0x13, + 0x4E, 0xD6, 0x4C, 0x28, 0x59, 0x33, 0x61, 0x64, 0xCD, 0x84, 0x93, 0x35, 0x13, 0x46, 0xD6, 0x4C, + 0x48, 0x59, 0x33, 0x89, 0xB2, 0x36, 0x05, 0x18, 0x59, 0x33, 0x21, 0x64, 0xCD, 0x24, 0x97, 0x35, + 0x13, 0x46, 0xD6, 0x4C, 0x72, 0x59, 0x33, 0x61, 0x64, 0xCD, 0x64, 0x96, 0xB5, 0x63, 0x76, 0x2E, + 0x6B, 0x3F, 0xEF, 0xFA, 0x08, 0x5C, 0x9A, 0x9F, 0xC9, 0xDA, 0xCF, 0x7B, 0x3D, 0xC6, 0x17, 0xAF, + 0x3F, 0x95, 0xB5, 0x3E, 0xDF, 0x97, 0x35, 0x93, 0xF4, 0xC5, 0x3B, 0x21, 0x6B, 0xFD, 0xA7, 0x94, + 0x65, 0xCD, 0xA4, 0x2A, 0x6B, 0x26, 0xE9, 0xFB, 0x7C, 0x42, 0xD6, 0x4C, 0xAA, 0xB2, 0x66, 0xB2, + 0x2F, 0x6B, 0x26, 0xD7, 0x90, 0x35, 0x93, 0xBA, 0xAC, 0x99, 0x54, 0x65, 0xCD, 0xE4, 0x1A, 0xB2, + 0x66, 0x52, 0x95, 0xB5, 0xF9, 0x79, 0xFC, 0x1B, 0xC8, 0x17, 0x7D, 0xA4, 0xD2, 0x45, 0xFD, 0x1D, + 0xCA, 0x65, 0x2D, 0x5D, 0x97, 0xDF, 0xA9, 0x5C, 0xD6, 0x4C, 0x52, 0x59, 0x23, 0x36, 0xF8, 0xC8, + 0x11, 0x5B, 0x78, 0xC4, 0xF2, 0x5D, 0x3A, 0x52, 0xCC, 0x4E, 0x1C, 0xB9, 0x7C, 0xAF, 0xF5, 0xD4, + 0xF8, 0x5B, 0x2C, 0x97, 0x35, 0x13, 0x52, 0xD6, 0x4C, 0x38, 0x59, 0x33, 0xA1, 0x64, 0xCD, 0x84, + 0x94, 0x35, 0x13, 0x4A, 0xD6, 0x4C, 0x26, 0x59, 0x9B, 0x12, 0x94, 0xAC, 0x99, 0x30, 0xB2, 0x66, + 0x42, 0xC8, 0x9A, 0x09, 0x25, 0x6B, 0x26, 0x94, 0xAC, 0x99, 0x90, 0xB2, 0x66, 0xC2, 0xC9, 0x9A, + 0xC9, 0xA6, 0xAC, 0x8D, 0xFF, 0x4F, 0xEC, 0x90, 0x35, 0x28, 0x21, 0x6B, 0xD0, 0x5C, 0xD6, 0x7A, + 0xE6, 0x4C, 0xD6, 0xFA, 0x9C, 0x91, 0x35, 0xE8, 0xAA, 0xAC, 0x41, 0x73, 0x59, 0xEB, 0x99, 0x49, + 0xD6, 0x62, 0x22, 0x97, 0x35, 0x28, 0x2F, 0x6B, 0xD0, 0x15, 0x59, 0xEB, 0xE9, 0x49, 0xD6, 0x62, + 0x62, 0x4D, 0xD6, 0xA0, 0x84, 0xAC, 0x41, 0x19, 0x59, 0x83, 0xB2, 0xB2, 0x06, 0x5D, 0x93, 0x35, + 0xE8, 0xAE, 0xAC, 0x41, 0x6B, 0xB2, 0x06, 0x65, 0x64, 0x0D, 0xCA, 0xC8, 0x1A, 0x94, 0x95, 0x35, + 0x28, 0x2B, 0x6B, 0x50, 0x4A, 0xD6, 0xA0, 0xB4, 0xAC, 0x41, 0x77, 0x64, 0xAD, 0x5F, 0x75, 0xC8, + 0x5A, 0x98, 0x04, 0x59, 0x9B, 0xC6, 0x0F, 0x7F, 0x8D, 0xFF, 0x33, 0x3D, 0x64, 0x6D, 0x1E, 0x06, + 0x59, 0x8B, 0x63, 0x52, 0xD6, 0xA0, 0x25, 0x59, 0x83, 0x6E, 0xCB, 0x1A, 0x74, 0x5F, 0xD6, 0xA0, + 0x75, 0x59, 0x83, 0x56, 0x65, 0x0D, 0x5A, 0x91, 0x35, 0xE8, 0x96, 0xAC, 0x41, 0x97, 0x65, 0x0D, + 0xBA, 0x2E, 0x6B, 0xD0, 0x3D, 0x59, 0x83, 0xEE, 0xC9, 0x1A, 0x74, 0x5B, 0xD6, 0xA0, 0x5B, 0xB2, + 0x06, 0xDD, 0x93, 0x35, 0x68, 0x4D, 0xD6, 0xA0, 0x15, 0x59, 0x83, 0x6E, 0xC9, 0x1A, 0x74, 0x45, + 0xD6, 0xA0, 0x1B, 0xB2, 0x06, 0x5D, 0x94, 0x35, 0xE8, 0xAA, 0xAC, 0x41, 0x77, 0x64, 0x0D, 0xBA, + 0x24, 0x6B, 0xD0, 0x54, 0xD6, 0x7A, 0x24, 0xC8, 0x5A, 0x98, 0x3E, 0xF8, 0x3D, 0xBD, 0x78, 0x6D, + 0x90, 0xB5, 0x30, 0x0D, 0xB2, 0x16, 0xA6, 0x41, 0xD6, 0xC2, 0xF4, 0xF4, 0x6B, 0xF4, 0xFA, 0x7F, + 0x59, 0xEB, 0xC3, 0x49, 0xD6, 0x8E, 0x51, 0x94, 0xB5, 0x69, 0xFE, 0xF0, 0xCF, 0xFC, 0xD2, 0x38, + 0xC8, 0x5A, 0x98, 0x06, 0x59, 0x9B, 0xE6, 0x41, 0xD6, 0xC2, 0x34, 0x91, 0xB5, 0x9E, 0xD8, 0x95, + 0xB5, 0x7E, 0x65, 0x59, 0xD6, 0xA0, 0x55, 0x59, 0x83, 0xD6, 0x64, 0xAD, 0x5F, 0x5F, 0x96, 0x35, + 0x68, 0x4D, 0xD6, 0xA0, 0xE9, 0x5B, 0xF7, 0x44, 0xD6, 0xFA, 0x8F, 0xD8, 0x95, 0x35, 0x68, 0x5D, + 0xD6, 0xA0, 0x55, 0x59, 0x83, 0xD6, 0x64, 0x0D, 0x5A, 0x97, 0x35, 0x68, 0x4D, 0xD6, 0xA0, 0x65, + 0x59, 0x9B, 0x1E, 0xE3, 0xBF, 0xF3, 0xF4, 0x61, 0xFD, 0x0E, 0xE5, 0xB2, 0x06, 0x4D, 0x65, 0x0D, + 0x4A, 0xC8, 0x1A, 0x34, 0x95, 0xB5, 0x74, 0x59, 0x7A, 0xEA, 0xDD, 0xFF, 0xCE, 0xA1, 0x23, 0x41, + 0x9C, 0x37, 0x23, 0x96, 0x1F, 0x29, 0x23, 0x95, 0x1E, 0x1B, 0x23, 0x44, 0x9C, 0x0D, 0x23, 0x96, + 0x6E, 0xFF, 0x11, 0x62, 0xF6, 0x78, 0xCF, 0xBD, 0xE9, 0xB9, 0x54, 0xD6, 0xA0, 0x9C, 0xAC, 0x41, + 0x29, 0x59, 0x83, 0x32, 0xB2, 0x06, 0xE5, 0x64, 0x0D, 0xCA, 0xC8, 0x1A, 0x94, 0x94, 0xB5, 0x1E, + 0x0C, 0xB2, 0x36, 0x05, 0x18, 0x59, 0x83, 0x12, 0xB2, 0x06, 0xCD, 0x65, 0x0D, 0xCA, 0xC8, 0x1A, + 0x34, 0x97, 0x35, 0x28, 0x23, 0x6B, 0x3D, 0x35, 0xC9, 0xDA, 0x31, 0x3B, 0x97, 0xB5, 0x9F, 0x77, + 0x7D, 0x04, 0x2E, 0xCD, 0xCF, 0x64, 0xED, 0xE7, 0xBD, 0x1E, 0xE3, 0x8B, 0xD7, 0x9F, 0xCA, 0x5A, + 0x9F, 0xEF, 0xCB, 0x1A, 0x34, 0x7D, 0xF1, 0x4E, 0xC8, 0x5A, 0xFF, 0x29, 0x65, 0x59, 0x83, 0x56, + 0x65, 0x0D, 0x9A, 0xBE, 0xCF, 0x27, 0x64, 0x0D, 0x5A, 0x95, 0x35, 0xE8, 0xBE, 0xAC, 0x41, 0xAF, + 0x21, 0x6B, 0xD0, 0xBA, 0xAC, 0x41, 0xAB, 0xB2, 0x06, 0xBD, 0x86, 0xAC, 0x41, 0xAB, 0xB2, 0x36, + 0x3F, 0x8F, 0x7F, 0x03, 0xF9, 0xA2, 0x8F, 0x54, 0xBA, 0xA8, 0xBF, 0x43, 0xB9, 0xAC, 0xA5, 0xEB, + 0xF2, 0x3B, 0x95, 0xCB, 0x1A, 0x34, 0x95, 0x35, 0x62, 0x83, 0x8F, 0x1C, 0xB1, 0x85, 0x47, 0x2C, + 0xDF, 0xA5, 0x23, 0xC5, 0xEC, 0xC4, 0x91, 0xCB, 0xF7, 0x5A, 0x4F, 0x8D, 0xBF, 0xC5, 0x72, 0x59, + 0x83, 0x92, 0xB2, 0x06, 0xE5, 0x64, 0x0D, 0x4A, 0xC9, 0x1A, 0x94, 0x94, 0x35, 0x28, 0x25, 0x6B, + 0x3D, 0x16, 0x65, 0x6D, 0x4A, 0x50, 0xB2, 0x06, 0x65, 0x64, 0x0D, 0x4A, 0xC8, 0x1A, 0x94, 0x92, + 0x35, 0x28, 0x25, 0x6B, 0x50, 0x52, 0xD6, 0xA0, 0x9C, 0xAC, 0x41, 0x37, 0x65, 0xED, 0xC1, 0xC3, + 0xFB, 0xF7, 0xFF, 0xA2, 0x35, 0x07, 0x41, 0x6B, 0x8E, 0x9C, 0xD6, 0x7A, 0xE6, 0x8C, 0xD6, 0xFA, + 0x9C, 0xA1, 0x35, 0xC7, 0x2A, 0xAD, 0x39, 0x72, 0x5A, 0xEB, 0x99, 0x89, 0xD6, 0x62, 0x22, 0xA7, + 0x35, 0x07, 0x4F, 0x6B, 0x8E, 0x15, 0x5A, 0xEB, 0xE9, 0x89, 0xD6, 0x62, 0x62, 0x8D, 0xD6, 0x1C, + 0x04, 0xAD, 0x39, 0x18, 0x5A, 0x73, 0xB0, 0xB4, 0xE6, 0x58, 0xA3, 0x35, 0xC7, 0x2E, 0xAD, 0x39, + 0x6A, 0xB4, 0xE6, 0x60, 0x68, 0xCD, 0xC1, 0xD0, 0x9A, 0x83, 0xA5, 0x35, 0x07, 0x4B, 0x6B, 0x0E, + 0x8A, 0xD6, 0x1C, 0x34, 0xAD, 0x39, 0x76, 0x68, 0xAD, 0x5F, 0x75, 0xD0, 0x5A, 0x98, 0x04, 0x5A, + 0x9B, 0xC6, 0x0F, 0x7F, 0x8D, 0xFF, 0x33, 0x3D, 0x68, 0x6D, 0x1E, 0x06, 0x5A, 0x8B, 0x63, 0x92, + 0xD6, 0x1C, 0x25, 0x5A, 0x73, 0x6C, 0xD3, 0x9A, 0x63, 0x9F, 0xD6, 0x1C, 0x75, 0x5A, 0x73, 0x54, + 0x69, 0xCD, 0x51, 0xA1, 0x35, 0xC7, 0x16, 0xAD, 0x39, 0x96, 0x69, 0xCD, 0xB1, 0x4E, 0x6B, 0x8E, + 0x3D, 0x5A, 0x73, 0xEC, 0xD1, 0x9A, 0x63, 0x9B, 0xD6, 0x1C, 0x5B, 0xB4, 0xE6, 0xD8, 0xA3, 0x35, + 0x47, 0x8D, 0xD6, 0x1C, 0x15, 0x5A, 0x73, 0x6C, 0xD1, 0x9A, 0x63, 0x85, 0xD6, 0x1C, 0x1B, 0xB4, + 0xE6, 0x58, 0xA4, 0x35, 0xC7, 0x2A, 0xAD, 0x39, 0x76, 0x68, 0xCD, 0xB1, 0x44, 0x6B, 0x8E, 0x94, + 0xD6, 0x7A, 0x24, 0xD0, 0x5A, 0x98, 0x3E, 0xF8, 0x3D, 0xBD, 0x78, 0x6D, 0xA0, 0xB5, 0x30, 0x0D, + 0xB4, 0x16, 0xA6, 0x81, 0xD6, 0xC2, 0x34, 0xD0, 0x5A, 0x9C, 0xFE, 0x9F, 0xD6, 0xFA, 0xF0, 0xA0, + 0xB5, 0x38, 0x8A, 0xB4, 0x36, 0xCD, 0x1F, 0xFE, 0x99, 0x5F, 0x1A, 0x07, 0x5A, 0x0B, 0xD3, 0x40, + 0x6B, 0xD3, 0x3C, 0xD0, 0x5A, 0x98, 0x26, 0xB4, 0xD6, 0x13, 0xBB, 0xB4, 0xD6, 0xAF, 0x2C, 0xD3, + 0x9A, 0xA3, 0x4A, 0x6B, 0x8E, 0x1A, 0xAD, 0xF5, 0xEB, 0xCB, 0xB4, 0xE6, 0xA8, 0xD1, 0x9A, 0x23, + 0x7D, 0xED, 0x9E, 0xD0, 0x5A, 0xFF, 0x11, 0xBB, 0xB4, 0xE6, 0xA8, 0xD3, 0x9A, 0xA3, 0x4A, 0x6B, + 0x8E, 0x1A, 0xAD, 0x39, 0xEA, 0xB4, 0xE6, 0xA8, 0xD1, 0x9A, 0xA3, 0x4C, 0x6B, 0xD3, 0x63, 0xFC, + 0x77, 0x9E, 0x3E, 0xAC, 0xDF, 0xA1, 0x9C, 0xD6, 0x1C, 0x29, 0xAD, 0x39, 0x08, 0x5A, 0x73, 0xA4, + 0xB4, 0x96, 0x2E, 0x4B, 0x4F, 0xBD, 0xFB, 0xDF, 0x39, 0x74, 0x24, 0x88, 0xF3, 0x66, 0xC4, 0xF2, + 0x23, 0x65, 0xA4, 0xD2, 0x63, 0x63, 0x84, 0x88, 0xB3, 0x61, 0xC4, 0xD2, 0xED, 0x3F, 0x42, 0xCC, + 0x1E, 0xEF, 0xB9, 0x37, 0x3D, 0x97, 0xD2, 0x9A, 0x83, 0xA3, 0x35, 0x07, 0x45, 0x6B, 0x0E, 0x86, + 0xD6, 0x1C, 0x1C, 0xAD, 0x39, 0x18, 0x5A, 0x73, 0x90, 0xB4, 0xD6, 0x83, 0x81, 0xD6, 0xA6, 0x00, + 0x43, 0x6B, 0x0E, 0x82, 0xD6, 0x1C, 0x39, 0xAD, 0x39, 0x18, 0x5A, 0x73, 0xE4, 0xB4, 0xE6, 0x60, + 0x68, 0xAD, 0xA7, 0x26, 0x5A, 0x3B, 0x66, 0xE7, 0xB4, 0xF6, 0xF3, 0xAE, 0x8F, 0xC0, 0xA5, 0xF9, + 0x19, 0xAD, 0xFD, 0xBC, 0xD7, 0x63, 0x7C, 0xF1, 0xFA, 0x53, 0x5A, 0xEB, 0xF3, 0x7D, 0x5A, 0x73, + 0xA4, 0x6F, 0xDE, 0x09, 0x5A, 0xEB, 0x3F, 0xA5, 0x4C, 0x6B, 0x8E, 0x2A, 0xAD, 0x39, 0xD2, 0x17, + 0xFA, 0x04, 0xAD, 0x39, 0xAA, 0xB4, 0xE6, 0xD8, 0xA7, 0x35, 0xC7, 0x35, 0x68, 0xCD, 0x51, 0xA7, + 0x35, 0x47, 0x95, 0xD6, 0x1C, 0xD7, 0xA0, 0x35, 0x47, 0x95, 0xD6, 0xE6, 0xE7, 0xF1, 0x6F, 0x20, + 0x5F, 0xF4, 0x91, 0x4A, 0x17, 0xF5, 0x77, 0x28, 0xA7, 0xB5, 0x74, 0x5D, 0x7E, 0xA7, 0x72, 0x5A, + 0x73, 0xA4, 0xB4, 0x46, 0x6C, 0xF0, 0x91, 0x23, 0xB6, 0xF0, 0x88, 0xE5, 0xBB, 0x74, 0xA4, 0x98, + 0x9D, 0x38, 0x72, 0xF9, 0x5E, 0xEB, 0xA9, 0xF1, 0xB7, 0x58, 0x4E, 0x6B, 0x0E, 0x92, 0xD6, 0x1C, + 0x1C, 0xAD, 0x39, 0x28, 0x5A, 0x73, 0x90, 0xB4, 0xE6, 0xA0, 0x68, 0xAD, 0xC7, 0x22, 0xAD, 0x4D, + 0x09, 0x8A, 0xD6, 0x1C, 0x0C, 0xAD, 0x39, 0x08, 0x5A, 0x73, 0x50, 0xB4, 0xE6, 0xA0, 0x68, 0xCD, + 0x41, 0xD2, 0x9A, 0x83, 0xA3, 0x35, 0xC7, 0x2E, 0xAD, 0x3D, 0xFE, 0x97, 0xD6, 0x38, 0x5B, 0xA3, + 0x70, 0x2D, 0xD5, 0x35, 0x92, 0xD7, 0x36, 0x7C, 0x8D, 0x02, 0x36, 0x42, 0xD8, 0x28, 0x62, 0x5B, + 0x32, 0xB6, 0x45, 0x64, 0x23, 0x94, 0x6D, 0x99, 0xD9, 0x38, 0x67, 0x23, 0xA1, 0x6D, 0x41, 0xDA, + 0x96, 0xA9, 0xAD, 0x60, 0x6D, 0x65, 0x6C, 0x23, 0xB5, 0x8D, 0xE4, 0xB6, 0x05, 0x6F, 0x5B, 0x00, + 0x37, 0x56, 0xDC, 0x56, 0xC8, 0x6D, 0xD3, 0xDC, 0x4E, 0xD0, 0x2D, 0x53, 0xB7, 0x84, 0xDD, 0xCE, + 0xDD, 0x2D, 0x83, 0x37, 0x5E, 0xDE, 0xAA, 0xF4, 0x56, 0xB1, 0xB7, 0x12, 0xBE, 0x5D, 0x45, 0xDF, + 0xAE, 0xC0, 0x6F, 0x45, 0x7F, 0xDB, 0x05, 0xB8, 0x1D, 0x81, 0xDB, 0x22, 0xB8, 0x6D, 0x83, 0xDB, + 0x46, 0xB8, 0x8A, 0xC2, 0xED, 0x32, 0xDC, 0xB6, 0xC3, 0x95, 0x21, 0xAE, 0x28, 0x71, 0xBB, 0x14, + 0xB7, 0x68, 0x71, 0x7B, 0x18, 0xB7, 0xAE, 0x71, 0x1B, 0x1C, 0xB7, 0xE9, 0x71, 0xAB, 0x20, 0xC7, + 0x88, 0x5C, 0x42, 0x72, 0x89, 0xC9, 0x25, 0x28, 0x97, 0xA8, 0x5C, 0xC2, 0x72, 0x89, 0xCB, 0x9D, + 0xC3, 0xDC, 0x99, 0xCC, 0xA5, 0x34, 0x97, 0xD9, 0x5C, 0x82, 0x73, 0xA9, 0xCE, 0x25, 0x3C, 0x47, + 0xF8, 0x5C, 0x01, 0xE8, 0xF2, 0x37, 0xE6, 0x84, 0xD0, 0xA5, 0xEF, 0xD3, 0x73, 0xA2, 0xF3, 0xAA, + 0xD1, 0xE5, 0x2F, 0xE2, 0x09, 0xA4, 0xF3, 0xAA, 0xD2, 0x11, 0xAF, 0xF0, 0x53, 0xA6, 0x2B, 0x38, + 0x9D, 0x5F, 0x03, 0xEA, 0xAE, 0x20, 0x75, 0x65, 0xAA, 0xF3, 0x6B, 0x58, 0x5D, 0x19, 0xEB, 0x52, + 0x96, 0x22, 0xB4, 0x2E, 0xE5, 0x3A, 0xA7, 0xBC, 0x8E, 0x02, 0x3B, 0x46, 0xEC, 0x9C, 0x22, 0x3B, + 0xC6, 0xEC, 0xD2, 0xD5, 0xF9, 0x85, 0x5A, 0x84, 0xDA, 0xE5, 0x87, 0xD0, 0xC8, 0x11, 0xE7, 0xCC, + 0x88, 0xE5, 0x67, 0xC9, 0x48, 0x31, 0x07, 0xC6, 0xC8, 0xE5, 0x67, 0xC2, 0x48, 0x51, 0x1B, 0xFF, + 0x97, 0x6A, 0x31, 0x78, 0x47, 0xEB, 0x1D, 0xCB, 0x77, 0xA4, 0xDF, 0xD1, 0x80, 0x47, 0x0A, 0x1E, + 0x4F, 0x78, 0xB9, 0xE1, 0x91, 0x88, 0xC7, 0x29, 0x1E, 0xC5, 0x78, 0xA4, 0xE3, 0x51, 0x90, 0x47, + 0x4A, 0xDE, 0x29, 0xE5, 0xE5, 0x96, 0x97, 0x62, 0x5E, 0xA6, 0x79, 0x39, 0xE7, 0xA5, 0x9E, 0x57, + 0x02, 0x3D, 0xE2, 0x85, 0x3F, 0x23, 0x7A, 0xB9, 0x07, 0x10, 0xA4, 0x97, 0x6A, 0x41, 0x6E, 0x7A, + 0x04, 0x25, 0x30, 0xA8, 0x97, 0x4A, 0x43, 0xAE, 0x7A, 0x25, 0xD6, 0x4B, 0x89, 0x89, 0x73, 0x3D, + 0xBF, 0x06, 0xEC, 0x5D, 0x41, 0xF6, 0x52, 0xC2, 0xE2, 0x68, 0xEF, 0x0A, 0xB6, 0x97, 0xE3, 0x5E, + 0xBA, 0xF4, 0x23, 0x96, 0x2F, 0xED, 0x48, 0xA5, 0x6B, 0x37, 0x42, 0xC4, 0xEA, 0x8C, 0x58, 0x7A, + 0xF7, 0xBF, 0x84, 0x8C, 0x21, 0x3E, 0x62, 0xCB, 0x8F, 0x20, 0xB3, 0xA9, 0x47, 0x8E, 0xD8, 0xB7, + 0x23, 0x46, 0xED, 0xCD, 0x11, 0x24, 0x76, 0xDF, 0x2F, 0x22, 0xA3, 0xA0, 0x8F, 0x97, 0x3E, 0x9A, + 0xFA, 0x58, 0xEB, 0xE3, 0xB1, 0x8F, 0xD5, 0x3E, 0x82, 0xFB, 0x58, 0xEF, 0x23, 0xC1, 0x8F, 0x13, + 0x3F, 0x96, 0xFC, 0x58, 0xF3, 0xE3, 0xD1, 0x8F, 0x56, 0xBF, 0x7D, 0xF6, 0xFB, 0xF3, 0x2F, 0xEA, + 0xDA, 0x70, 0x86, 0xFE, 0xAA, 0xEC, 0x4E, 0x7B, 0xFF, 0xF5, 0xDD, 0xD3, 0x87, 0xD3, 0x1D, 0xF6, + 0xC0, 0xC3, 0xBF, 0x03, 0xF3, 0xF8, 0xD1, 0x31, 0xEE, 0x2B, 0xF4, 0xE4, 0xC2, 0x0F, 0x78, 0xFC, + 0x4F, 0xE2, 0x67, 0x60, 0xFE, 0xDD, 0x7D, 0xEC, 0xCC, 0xF1, 0xF9, 0xDB, 0x5D, 0xB9, 0xF3, 0xF3, + 0x17, 0x8C, 0x0C, 0xFE, 0x0C, 0xAE, 0xDA, 0xE0, 0xCF, 0x8B, 0x72, 0x1D, 0xFC, 0x15, 0x9B, 0x7D, + 0x70, 0x0E, 0xE5, 0x42, 0xF8, 0x33, 0xC6, 0x1B, 0xE1, 0xCF, 0xF8, 0x8A, 0x12, 0xFE, 0xBA, 0x60, + 0x76, 0xC2, 0x39, 0xB4, 0x26, 0x85, 0x3F, 0x2F, 0x39, 0xB5, 0xC2, 0xEF, 0x37, 0xEE, 0xDD, 0xBE, + 0x75, 0xB3, 0x7D, 0xF8, 0xF2, 0xE9, 0xED, 0xBB, 0x17, 0xAF, 0x3F, 0x7E, 0xEC, 0x3B, 0xE0, 0xD5, + 0xCB, 0xE7, 0xCF, 0xDE, 0x7C, 0xF8, 0xF0, 0xB9, 0x7D, 0xEE, 0x5E, 0x74, 0xB7, 0xFF, 0x89, 0xBB, + 0xFB, 0xB6, 0xB5, 0xBB, 0xF6, 0xFA, 0xE3, 0xCD, 0xDB, 0xF7, 0x7E, 0x00, 0x22, 0x7E, 0x14, 0x4B, + 0x30, 0x61, 0x02, 0x00 +}; //bootstrap.min.css + +//Content of jquery-3.6.0.min.js.gz with gzip compression +static const uint8_t jquery_js[] PROGMEM = { + 0x1F, 0x8B, 0x08, 0x08, 0x82, 0x8A, 0x0C, 0x61, 0x04, 0x00, 0x6A, 0x71, 0x75, 0x65, 0x72, 0x79, + 0x2D, 0x33, 0x2E, 0x36, 0x2E, 0x30, 0x2E, 0x6D, 0x69, 0x6E, 0x2E, 0x6A, 0x73, 0x00, 0xBC, 0x5D, + 0x69, 0x7B, 0xDB, 0xC6, 0x11, 0xFE, 0xDE, 0x5F, 0x21, 0xA2, 0xAE, 0x0A, 0x98, 0x2B, 0x5A, 0x74, + 0x9A, 0xB4, 0x85, 0x04, 0xF3, 0x71, 0x64, 0x27, 0x71, 0x9B, 0xD3, 0x72, 0xDB, 0xA4, 0x14, 0x9D, + 0x07, 0x22, 0x41, 0x09, 0x31, 0x05, 0x30, 0x00, 0xA8, 0xA3, 0x02, 0xFB, 0xDB, 0xFB, 0xCE, 0xEC, + 0x89, 0x83, 0xB2, 0xD3, 0x2B, 0x87, 0xB8, 0xD8, 0xFB, 0x98, 0x9D, 0x9D, 0x6B, 0x67, 0x9F, 0x3C, + 0x1E, 0xEC, 0xFD, 0xF4, 0xDD, 0x26, 0x29, 0xEE, 0xF6, 0xAE, 0x3F, 0x1A, 0x7D, 0x32, 0x3A, 0xDC, + 0xAB, 0xF7, 0xFC, 0x79, 0xB0, 0xF7, 0xCD, 0x3A, 0xC9, 0xFE, 0x74, 0xBA, 0xF7, 0x59, 0xBE, 0xC9, + 0x16, 0x71, 0x95, 0xE6, 0xD9, 0x5E, 0x9C, 0x2D, 0xF6, 0xF2, 0xEA, 0x32, 0x29, 0xF6, 0xE6, 0x79, + 0x56, 0x15, 0xE9, 0xF9, 0xA6, 0xCA, 0x8B, 0x12, 0xD9, 0x7F, 0xFA, 0x99, 0x8A, 0x8F, 0xF2, 0xE2, + 0xE2, 0xC9, 0x2A, 0x9D, 0x27, 0x59, 0x99, 0xEC, 0x3D, 0x7E, 0xF2, 0xAB, 0xC1, 0x72, 0x93, 0xCD, + 0xA9, 0xA0, 0x9F, 0x88, 0x2A, 0xB8, 0xF7, 0x36, 0x88, 0x2E, 0x51, 0x6C, 0x5E, 0x79, 0x47, 0x5E, + 0x7E, 0xFE, 0x53, 0x82, 0x40, 0x14, 0x55, 0x77, 0xEB, 0x24, 0x5F, 0xEE, 0x5D, 0xE5, 0x8B, 0xCD, + 0x2A, 0xD9, 0xDF, 0xDF, 0x91, 0x30, 0x4A, 0x6E, 0xD7, 0x79, 0x51, 0x95, 0x93, 0xE6, 0x67, 0x94, + 0x8C, 0x16, 0xF9, 0x7C, 0x73, 0x95, 0x64, 0xD5, 0xA4, 0x42, 0x33, 0x83, 0xC3, 0x20, 0xB4, 0xAD, + 0x06, 0xF7, 0xE9, 0xD2, 0x1F, 0xD8, 0x2C, 0x41, 0x75, 0x59, 0xE4, 0x37, 0x7B, 0x59, 0x72, 0xB3, + 0xF7, 0xB2, 0x28, 0xF2, 0xC2, 0xF7, 0xD4, 0xB8, 0x8B, 0xE4, 0xE7, 0x4D, 0x5A, 0x24, 0xE5, 0x5E, + 0xBC, 0x77, 0x93, 0x66, 0x8B, 0xFC, 0x06, 0x3F, 0xD5, 0x25, 0xBE, 0x74, 0x49, 0x2F, 0x38, 0x2A, + 0x92, 0x6A, 0x53, 0x64, 0x7B, 0x15, 0xAA, 0xDD, 0x86, 0xFC, 0xD7, 0xF7, 0x30, 0x33, 0xC9, 0x32, + 0xCD, 0x92, 0x85, 0x37, 0x50, 0xDD, 0x55, 0xE5, 0x27, 0xF2, 0x27, 0xAC, 0x2E, 0xD3, 0x52, 0x98, + 0x0E, 0x9D, 0x88, 0xA4, 0x35, 0x0D, 0xD7, 0x71, 0xB1, 0x57, 0x45, 0xD3, 0x99, 0x28, 0xA2, 0x6F, + 0x78, 0xDC, 0xA3, 0x8B, 0xA4, 0xFA, 0xB6, 0xC8, 0xAB, 0x9C, 0xAA, 0xFB, 0x66, 0x29, 0xCA, 0xA8, + 0x1A, 0x95, 0x34, 0xA7, 0xE2, 0x02, 0xA1, 0xE5, 0x2A, 0xAE, 0x26, 0xEE, 0xF8, 0x54, 0xA7, 0x64, + 0xCA, 0x68, 0x1E, 0xAF, 0x56, 0xDC, 0xBD, 0xDE, 0x2C, 0x58, 0xB3, 0x39, 0x32, 0xC5, 0xEB, 0xF5, + 0xEA, 0xCE, 0x47, 0x93, 0xC8, 0x28, 0x36, 0xA8, 0x74, 0xBD, 0x29, 0x2F, 0x45, 0x8A, 0x40, 0x8A, + 0xD1, 0xDC, 0xA2, 0xCD, 0x2C, 0xBA, 0xDF, 0x8A, 0x3C, 0xCA, 0x46, 0x55, 0x7E, 0x8A, 0x7E, 0x66, + 0x17, 0xE2, 0x1A, 0x1F, 0x97, 0x71, 0xF9, 0xCD, 0x4D, 0x86, 0xBE, 0xAD, 0x93, 0xA2, 0xBA, 0x13, + 0x71, 0x74, 0x6D, 0xD3, 0x57, 0x51, 0x2C, 0x1B, 0x97, 0x83, 0x08, 0xC4, 0x1D, 0x55, 0x71, 0x15, + 0x75, 0xFB, 0xE1, 0xE9, 0x28, 0xBB, 0xC0, 0xB4, 0xE8, 0xD9, 0xE6, 0xEA, 0x3C, 0x29, 0xEC, 0x2C, + 0x26, 0xA3, 0x2C, 0x5F, 0x24, 0x6F, 0xF0, 0x81, 0x44, 0x5D, 0xC4, 0x4D, 0x4E, 0xAB, 0xE4, 0x6A, + 0x2B, 0x6E, 0x7B, 0x5A, 0xD8, 0xCB, 0x36, 0xAB, 0xD5, 0x20, 0x42, 0xC1, 0x24, 0x8A, 0x00, 0x1F, + 0x72, 0x25, 0xB6, 0xE2, 0x65, 0x74, 0x62, 0x00, 0x41, 0xCC, 0xA3, 0x7B, 0xAA, 0x2A, 0x1C, 0x1C, + 0x8A, 0xB2, 0x98, 0xD3, 0x4F, 0x86, 0xD9, 0x49, 0x64, 0xE0, 0x2B, 0x02, 0x31, 0x0A, 0x6F, 0x8F, + 0x74, 0xF5, 0x7B, 0xE7, 0x04, 0xC3, 0x22, 0x0B, 0xEE, 0x69, 0xC5, 0x0A, 0x91, 0x62, 0x7E, 0xFC, + 0x2C, 0xCA, 0xEA, 0xFA, 0x65, 0x30, 0x9A, 0x17, 0x49, 0x5C, 0x25, 0x2F, 0x57, 0x09, 0x55, 0xED, + 0x7B, 0xE5, 0xBC, 0x48, 0xD7, 0x04, 0x31, 0x00, 0xBE, 0x7C, 0x54, 0x25, 0xB7, 0x55, 0x44, 0xF0, + 0xBF, 0x04, 0xBC, 0x15, 0x7B, 0x69, 0xB6, 0x37, 0x0F, 0x7C, 0xCC, 0xF6, 0xB4, 0x98, 0xD5, 0x35, + 0x2F, 0xF7, 0xF3, 0x4A, 0x6E, 0x25, 0xF4, 0xB8, 0xF9, 0xED, 0x17, 0x41, 0xB0, 0xBF, 0x9F, 0x8F, + 0x4A, 0x37, 0x0E, 0x6D, 0x07, 0x47, 0x58, 0x8D, 0x24, 0x5E, 0xD0, 0x52, 0x26, 0xD9, 0xE2, 0xE4, + 0x32, 0x5D, 0x2D, 0xFC, 0x3C, 0x18, 0xAD, 0xE3, 0x02, 0x1D, 0xF8, 0x1A, 0x13, 0x37, 0x2A, 0x92, + 0xAB, 0xFC, 0x3A, 0xD1, 0x29, 0x5B, 0x33, 0x8C, 0x9B, 0xD6, 0x3C, 0x61, 0x82, 0x26, 0xC9, 0xD0, + 0xF3, 0xC2, 0xCE, 0xB6, 0x4B, 0xEA, 0xBA, 0x6F, 0xAD, 0x26, 0xD9, 0x34, 0xD7, 0x70, 0x86, 0x01, + 0xE8, 0x62, 0xA1, 0x4E, 0xDF, 0xD2, 0xFC, 0x2C, 0x23, 0x8F, 0x11, 0x89, 0x27, 0x4E, 0x23, 0x17, + 0x07, 0xE8, 0xA6, 0x79, 0x07, 0x9E, 0x8E, 0x96, 0x19, 0x40, 0x2E, 0xAD, 0x38, 0xC5, 0x99, 0xEA, + 0x35, 0xF5, 0x51, 0x6E, 0x8C, 0xC1, 0x80, 0xD6, 0x7F, 0x95, 0x64, 0x17, 0xD5, 0xA5, 0x87, 0xA9, + 0xA3, 0x45, 0x1D, 0xC9, 0x4F, 0x40, 0x29, 0x06, 0xA3, 0xB7, 0xE5, 0xE0, 0x0A, 0xE1, 0xFD, 0xFD, + 0xC1, 0x2D, 0xFF, 0xF8, 0x5E, 0x5C, 0x14, 0xF1, 0x1D, 0xBA, 0x4D, 0x2B, 0x74, 0x88, 0x9F, 0xAA, + 0xAE, 0x15, 0x8C, 0xD9, 0xB1, 0x54, 0xFB, 0xFB, 0x87, 0xC7, 0xF8, 0x53, 0x1D, 0x8C, 0x69, 0x59, + 0xB0, 0x1D, 0xA8, 0x4B, 0xD1, 0xE9, 0x68, 0xAD, 0xF7, 0x5F, 0x74, 0x2F, 0x11, 0x5B, 0xB8, 0x14, + 0xD8, 0x3C, 0xD8, 0xB3, 0x9B, 0x39, 0x10, 0x5E, 0x78, 0x2A, 0x64, 0x17, 0xC2, 0x43, 0x51, 0xE5, + 0xCF, 0xA9, 0x25, 0xBB, 0xE1, 0xCC, 0x10, 0x4B, 0x39, 0x4B, 0x84, 0x00, 0xB0, 0xCF, 0xB0, 0xAC, + 0x36, 0x4F, 0x77, 0x09, 0xDC, 0xCC, 0x61, 0x72, 0x7C, 0x38, 0xA1, 0xD0, 0x34, 0x19, 0xD2, 0x8F, + 0x1A, 0xEF, 0x2C, 0x94, 0x71, 0xB3, 0xAD, 0xA0, 0x0D, 0x7B, 0x5A, 0xC5, 0xF3, 0x77, 0xA6, 0x4A, + 0x33, 0x63, 0xE8, 0xFD, 0x55, 0x52, 0x5C, 0x24, 0x3E, 0x97, 0x74, 0x3A, 0xED, 0x07, 0x22, 0xB1, + 0x38, 0x0C, 0x43, 0x4C, 0xAE, 0xE5, 0x5E, 0x8D, 0x18, 0x45, 0x55, 0x5B, 0x91, 0xC4, 0xF3, 0xCB, + 0xBE, 0x3E, 0x9E, 0x8E, 0x28, 0x85, 0x2B, 0x64, 0x94, 0x71, 0x15, 0xAF, 0x6D, 0xB6, 0xCC, 0x64, + 0xE3, 0x06, 0x4D, 0xCF, 0xFC, 0xD3, 0x11, 0xF2, 0xC9, 0x42, 0xFD, 0x10, 0xC0, 0x23, 0xA6, 0x28, + 0xAA, 0x34, 0x40, 0xBD, 0x8C, 0xE5, 0x6C, 0xCD, 0xBB, 0x2A, 0x2E, 0x25, 0x02, 0x93, 0x55, 0xC7, + 0xC5, 0x05, 0xEF, 0xE7, 0x92, 0x2A, 0x58, 0xA6, 0x45, 0x59, 0xED, 0xAA, 0x20, 0xF9, 0xD9, 0x3F, + 0x44, 0x9E, 0x55, 0xFC, 0x60, 0x96, 0x83, 0x31, 0xF2, 0x24, 0xD7, 0x49, 0xF6, 0xFE, 0x7E, 0x9C, + 0x8E, 0x2E, 0x8A, 0xE4, 0x81, 0x11, 0xFA, 0xD5, 0x70, 0x1C, 0xFC, 0xE6, 0x29, 0x0F, 0x2D, 0x5F, + 0x2C, 0xFE, 0xF3, 0x0A, 0xF7, 0x2A, 0x55, 0x5B, 0xF2, 0x73, 0xCF, 0xBA, 0x53, 0x31, 0xBB, 0x33, + 0x86, 0xC9, 0xD0, 0x67, 0x30, 0x0A, 0x0F, 0x83, 0xA3, 0xFE, 0xE6, 0x0E, 0x8F, 0xA3, 0x6C, 0x7F, + 0x3F, 0x3B, 0xAE, 0x26, 0x53, 0x06, 0xAC, 0x6C, 0x36, 0x0B, 0xA7, 0x33, 0xAA, 0x3E, 0xDB, 0xD9, + 0x59, 0x0B, 0x35, 0x75, 0xDD, 0x05, 0x30, 0x09, 0x98, 0xE1, 0x46, 0x94, 0x38, 0x99, 0xC3, 0x6A, + 0x44, 0x3F, 0xA2, 0x5C, 0xD3, 0xB2, 0xE2, 0x4B, 0x06, 0xB6, 0x02, 0xE0, 0x74, 0x5B, 0xA1, 0x8D, + 0x88, 0x36, 0x9B, 0x0E, 0x9B, 0xF6, 0xE4, 0x70, 0x18, 0xD5, 0x0A, 0xC6, 0xB2, 0x38, 0x67, 0xCC, + 0x22, 0x4F, 0x0F, 0x81, 0x71, 0xEE, 0x01, 0x29, 0xD1, 0x58, 0x6C, 0x4C, 0xB4, 0x19, 0xF6, 0x2A, + 0x1A, 0x8C, 0x8F, 0x96, 0x74, 0xAA, 0x9F, 0xE7, 0xF9, 0x2A, 0x89, 0x1D, 0xB4, 0x15, 0x03, 0x27, + 0xE0, 0x8C, 0x6A, 0x54, 0x56, 0xAA, 0xCA, 0x86, 0xC3, 0x40, 0x74, 0xB0, 0x5F, 0x5C, 0xD7, 0x57, + 0x7E, 0x1C, 0xD4, 0xB5, 0x1F, 0xE3, 0x24, 0x0B, 0xD0, 0x64, 0x14, 0x6D, 0x50, 0x49, 0x2C, 0xB7, + 0x4B, 0x79, 0x70, 0x10, 0x1C, 0x95, 0xC7, 0x9B, 0x23, 0x2A, 0x0D, 0x34, 0x2F, 0x8F, 0x1C, 0x3F, + 0x69, 0x54, 0x1F, 0x30, 0xCA, 0xAF, 0x24, 0x6E, 0x29, 0xA2, 0x64, 0x5A, 0xCD, 0x84, 0xF7, 0xE3, + 0x8F, 0x8C, 0x5D, 0x7E, 0xFC, 0x11, 0x87, 0x59, 0x04, 0xE4, 0x13, 0xE3, 0xA7, 0xA0, 0xDE, 0xED, + 0xEF, 0xD3, 0xCF, 0xE9, 0x28, 0x2D, 0xBF, 0x5D, 0xC5, 0x69, 0x26, 0xA7, 0xD9, 0x2F, 0xA8, 0x0B, + 0x69, 0xC4, 0x48, 0x06, 0x49, 0xFC, 0x4B, 0xC7, 0x42, 0x30, 0xC1, 0xE9, 0x13, 0x53, 0x8D, 0x79, + 0x94, 0x02, 0xEF, 0x35, 0x33, 0x64, 0xC1, 0x64, 0x3A, 0x0B, 0xD3, 0xBA, 0x6E, 0x55, 0x47, 0x09, + 0x59, 0x88, 0x41, 0xA7, 0x98, 0x2A, 0x41, 0xC5, 0x23, 0xBD, 0x1C, 0xFE, 0x0A, 0x73, 0x8D, 0x8A, + 0xC3, 0xEB, 0x3C, 0x5D, 0xEC, 0x1D, 0xAA, 0x5E, 0x71, 0x16, 0xC4, 0x6A, 0x18, 0x8A, 0xED, 0xFA, + 0xF9, 0xF7, 0x20, 0xC1, 0x62, 0x1C, 0xAD, 0xA1, 0x22, 0xA2, 0xBC, 0xA1, 0xBF, 0x1C, 0x7E, 0x15, + 0x57, 0x97, 0xA3, 0x82, 0xA2, 0xAF, 0xFC, 0x20, 0x18, 0x01, 0xA0, 0x57, 0xF1, 0x3C, 0xF1, 0x9F, + 0x9C, 0xBD, 0x78, 0x72, 0x21, 0x3C, 0x2F, 0x10, 0x69, 0xF9, 0x1A, 0x27, 0xD7, 0x1D, 0x1D, 0xB4, + 0x09, 0x91, 0x60, 0x0D, 0x50, 0x6E, 0x93, 0x67, 0x84, 0x6E, 0xB2, 0x3C, 0x5F, 0x3B, 0xF0, 0x88, + 0xCE, 0x37, 0x86, 0xD4, 0xDD, 0x0A, 0x22, 0xD3, 0x67, 0x82, 0x3F, 0xA0, 0x43, 0x6C, 0x2A, 0xD7, + 0x76, 0x4F, 0xE6, 0x9F, 0xD1, 0xBC, 0x9B, 0x13, 0x8C, 0xCE, 0x8A, 0x81, 0x5F, 0x45, 0x05, 0x85, + 0xFB, 0x0E, 0x3C, 0xCC, 0xF2, 0xB5, 0xC2, 0xCD, 0xC2, 0x73, 0xA0, 0xDD, 0x43, 0xC9, 0xCA, 0x05, + 0x7F, 0x7C, 0x2B, 0x12, 0x28, 0x0B, 0x00, 0x2B, 0xAB, 0x80, 0xFA, 0xF9, 0xF2, 0x6A, 0x5D, 0xDD, + 0xED, 0xEA, 0xE7, 0x91, 0x03, 0x1D, 0xAA, 0xC3, 0x63, 0xDD, 0xF3, 0x43, 0x9C, 0x18, 0xAB, 0xFC, + 0x3C, 0x5E, 0xBD, 0xBC, 0x8E, 0x57, 0xA6, 0xA8, 0x26, 0x41, 0x88, 0x16, 0xB9, 0x97, 0xF4, 0x4A, + 0x45, 0xDD, 0xE0, 0xE0, 0x16, 0x29, 0x06, 0x89, 0xDB, 0xFC, 0xB2, 0x31, 0x6C, 0xA6, 0xE8, 0x90, + 0xE8, 0x91, 0x35, 0x0D, 0xF4, 0x1E, 0x2D, 0x63, 0x60, 0xFA, 0x24, 0x3D, 0x2A, 0x8E, 0x31, 0x65, + 0x12, 0x90, 0x07, 0x63, 0x74, 0x5E, 0x13, 0x92, 0x20, 0x51, 0xB0, 0x09, 0xE9, 0x27, 0x08, 0xCE, + 0x41, 0xE3, 0xBC, 0xDB, 0x26, 0x2B, 0xD0, 0xAE, 0x86, 0x90, 0x49, 0xDE, 0x5F, 0x42, 0x03, 0x4E, + 0x42, 0xE7, 0xC6, 0xBB, 0x84, 0xC1, 0xB3, 0xAF, 0x7F, 0x74, 0x40, 0x4F, 0x67, 0x47, 0x6D, 0x0A, + 0x0E, 0xFD, 0x95, 0xD3, 0x47, 0xDD, 0x9E, 0xE8, 0xB3, 0x2D, 0x13, 0x5E, 0xC9, 0x74, 0xA7, 0x4B, + 0x97, 0xE0, 0x64, 0x0C, 0x93, 0x20, 0xDC, 0xA8, 0x45, 0x10, 0x28, 0x20, 0x32, 0x2C, 0x42, 0x66, + 0xDB, 0xB4, 0x73, 0xD8, 0x3A, 0x80, 0xAB, 0xC9, 0xC1, 0x38, 0x4C, 0xF5, 0x3A, 0x27, 0x3C, 0x93, + 0xDC, 0x54, 0xA3, 0x98, 0x9C, 0x37, 0xD9, 0xDD, 0x61, 0xA5, 0x26, 0x8F, 0x26, 0x56, 0xA4, 0xDD, + 0xB9, 0x4C, 0xA6, 0xE9, 0x70, 0x38, 0x23, 0x32, 0xCF, 0x8C, 0x4A, 0xE7, 0x89, 0x52, 0x81, 0xE9, + 0x20, 0x64, 0xDF, 0xEA, 0x95, 0x6D, 0xA0, 0x20, 0x96, 0x20, 0x45, 0xCD, 0xB9, 0xA9, 0x19, 0xB8, + 0x6B, 0x90, 0x1D, 0xA5, 0xC7, 0xF9, 0x51, 0x8A, 0xEA, 0x07, 0x95, 0x8F, 0x16, 0x90, 0x27, 0x00, + 0x38, 0xC7, 0xC0, 0x1D, 0x8C, 0xD9, 0x39, 0xCE, 0x6C, 0xD6, 0xA2, 0x79, 0x56, 0x77, 0x08, 0x58, + 0x54, 0x1F, 0xA3, 0x1D, 0x03, 0x16, 0xBC, 0xAE, 0x76, 0x24, 0xF9, 0x71, 0x71, 0x94, 0xA3, 0x29, + 0x85, 0xDB, 0xD2, 0x88, 0x9A, 0xCC, 0x81, 0x70, 0x50, 0x09, 0x41, 0xBB, 0x6C, 0x11, 0xC4, 0xA8, + 0x01, 0x8A, 0x9C, 0x81, 0xE2, 0xFD, 0x05, 0x54, 0xFF, 0x2E, 0xFC, 0x98, 0x28, 0xA3, 0x4D, 0xBA, + 0x08, 0xC7, 0xA2, 0xDC, 0xAC, 0xD7, 0x74, 0x6C, 0xDC, 0x01, 0xD9, 0xF6, 0xD0, 0x9D, 0xA7, 0x77, + 0x57, 0xE7, 0xF9, 0x8A, 0x11, 0xE4, 0x32, 0x9B, 0xCA, 0x2F, 0xE2, 0x01, 0x8A, 0x18, 0x7B, 0x0F, + 0xD3, 0xDC, 0x89, 0x0A, 0x84, 0xA2, 0x5B, 0xBC, 0x4F, 0xE5, 0x61, 0xB0, 0xF7, 0x35, 0x93, 0x7F, + 0x7B, 0x92, 0x63, 0xD9, 0xFB, 0x4C, 0x53, 0x9B, 0x0C, 0x1E, 0x7B, 0x2F, 0x40, 0xC0, 0xEF, 0xBD, + 0x4E, 0x2E, 0x5E, 0xDE, 0xAE, 0x15, 0xA2, 0x90, 0x28, 0x48, 0x35, 0xEC, 0xF1, 0xF1, 0x05, 0xDA, + 0x7E, 0xCF, 0x0B, 0x5A, 0x87, 0x73, 0x36, 0x35, 0x18, 0xC6, 0x1B, 0x56, 0x43, 0x6F, 0xE6, 0xA1, + 0x3B, 0xE0, 0x8C, 0xBE, 0xCC, 0x6F, 0x92, 0xE2, 0x24, 0x2E, 0x13, 0x1C, 0x8C, 0x01, 0xF3, 0x79, + 0xCE, 0x31, 0x97, 0xE9, 0x73, 0x6E, 0x21, 0xCE, 0x45, 0x8E, 0xC5, 0xB8, 0x14, 0x4B, 0x71, 0x21, + 0x6E, 0xC4, 0x46, 0xAC, 0xC4, 0x1B, 0x71, 0x22, 0x62, 0xF1, 0x52, 0x5C, 0x8B, 0x52, 0xCC, 0xC5, + 0x1D, 0x68, 0x68, 0xAF, 0x4C, 0xFF, 0xF1, 0x8F, 0x55, 0xE2, 0x0D, 0xC7, 0x8F, 0x81, 0x1C, 0xB9, + 0xB3, 0x62, 0x1D, 0x65, 0x96, 0x9D, 0x79, 0x17, 0x1D, 0x32, 0x20, 0x5E, 0x45, 0x1B, 0xB4, 0x27, + 0x6E, 0xE5, 0xCF, 0x73, 0xF9, 0xF3, 0xB5, 0xFC, 0xF9, 0xA9, 0x9F, 0x14, 0x4F, 0x68, 0xFB, 0xF2, + 0xF1, 0x08, 0xDE, 0x59, 0x00, 0xF1, 0xBC, 0xC0, 0x81, 0xD7, 0x66, 0xF6, 0x98, 0x45, 0xFD, 0x99, + 0x78, 0xC5, 0x7C, 0x2D, 0xBE, 0xD4, 0x3C, 0xE3, 0x17, 0x3A, 0xF0, 0x8D, 0x61, 0x52, 0xBF, 0x8D, + 0x76, 0xEC, 0x18, 0xEA, 0xA0, 0x85, 0xAD, 0x0C, 0xB0, 0x95, 0x49, 0x8C, 0x93, 0x80, 0xF2, 0xA0, + 0x2E, 0x04, 0xAA, 0x3F, 0x1A, 0x7B, 0x1F, 0x8C, 0xB7, 0xE2, 0x75, 0xE4, 0xCD, 0x2F, 0x93, 0xF9, + 0xBB, 0x64, 0x51, 0x97, 0xC9, 0x0A, 0x53, 0x8C, 0x40, 0x5C, 0xDE, 0x65, 0xF3, 0x3A, 0x86, 0xE4, + 0x61, 0x89, 0xD1, 0x97, 0x1C, 0xC2, 0x21, 0x73, 0x57, 0xB3, 0x48, 0x22, 0x5F, 0x95, 0x35, 0x58, + 0xF2, 0xA4, 0xA8, 0x17, 0x69, 0x19, 0x9F, 0xAF, 0x50, 0xE0, 0x32, 0x5D, 0x2C, 0x92, 0xAC, 0x4E, + 0x4B, 0x6C, 0x86, 0x7A, 0x85, 0xD3, 0xA4, 0xBE, 0xDA, 0xAC, 0xAA, 0x74, 0xBD, 0x4A, 0xEA, 0x7C, + 0x8D, 0x84, 0x02, 0xE7, 0x51, 0x9E, 0xAD, 0xEE, 0x6A, 0x29, 0x04, 0xA0, 0xB6, 0xE6, 0x48, 0x58, + 0x78, 0xE2, 0xAB, 0xC8, 0x9B, 0x9E, 0x9D, 0xDD, 0x3E, 0x3D, 0x3C, 0x3B, 0xAB, 0xCE, 0xCE, 0x8A, + 0xB3, 0xB3, 0xEC, 0xEC, 0x6C, 0x39, 0xF3, 0xC4, 0xAB, 0xC8, 0xF3, 0x27, 0xE1, 0x19, 0xFE, 0x41, + 0xF2, 0x22, 0x3E, 0x58, 0x3E, 0x3F, 0xF8, 0x6C, 0x76, 0x3F, 0x16, 0x9F, 0x6C, 0xBD, 0xE1, 0x57, + 0x43, 0x6F, 0x52, 0x73, 0xD2, 0x5B, 0x5B, 0xA4, 0x46, 0xBE, 0x9B, 0x03, 0xFC, 0xBC, 0x3D, 0x3B, + 0x3C, 0x40, 0x8D, 0xBF, 0x5F, 0xCE, 0x82, 0xA1, 0x27, 0xFE, 0x16, 0x79, 0xC8, 0xC7, 0x65, 0x1E, + 0xFB, 0xDE, 0xF0, 0xD5, 0xD0, 0x0B, 0x50, 0xAF, 0xFA, 0x9E, 0x3E, 0x7E, 0xFB, 0xA8, 0x1E, 0xFC, + 0x73, 0x36, 0x89, 0x02, 0x19, 0x83, 0xA4, 0xDF, 0xFA, 0xAA, 0xDD, 0x11, 0x55, 0x85, 0x7F, 0x7E, + 0x3B, 0x0B, 0x1E, 0x07, 0xBF, 0xAD, 0xCF, 0xBC, 0x76, 0xC2, 0x99, 0x47, 0x29, 0x67, 0x5E, 0xAD, + 0xEA, 0x0D, 0x6A, 0x55, 0xCB, 0xD9, 0x19, 0x06, 0xF0, 0x59, 0xE4, 0x85, 0x2A, 0x81, 0xCB, 0xF9, + 0xBE, 0xFF, 0xCB, 0xAB, 0x0E, 0xEA, 0x76, 0x8A, 0x1F, 0x4C, 0x51, 0xFD, 0xAC, 0xF6, 0x86, 0x7F, + 0x1B, 0x7A, 0xC8, 0x53, 0x8F, 0x90, 0xEF, 0x8C, 0x9A, 0x16, 0x9F, 0x46, 0x00, 0x5C, 0xB5, 0xC1, + 0x7C, 0xF4, 0x03, 0xA3, 0xF7, 0x2E, 0xB0, 0x97, 0x1E, 0xB9, 0xF1, 0xDE, 0x5B, 0xEE, 0xE3, 0x90, + 0x2B, 0x7E, 0xAB, 0x2A, 0x9D, 0x05, 0xBA, 0x15, 0xD4, 0x28, 0xD3, 0x1F, 0xA9, 0xC2, 0x3F, 0xF6, + 0x14, 0x7E, 0x2C, 0xE4, 0x0F, 0x92, 0xFF, 0xD1, 0x4C, 0xD6, 0xF3, 0xFA, 0x6C, 0xF8, 0xCF, 0x59, + 0xCD, 0x1F, 0x81, 0xC9, 0xFA, 0x97, 0x56, 0xF7, 0xEA, 0x67, 0x88, 0xFC, 0xDE, 0x8D, 0xFC, 0x2C, + 0x10, 0x7F, 0x6D, 0xD6, 0xC7, 0xF3, 0xF7, 0x08, 0xF9, 0x3E, 0x8F, 0xEE, 0x5F, 0xBD, 0x08, 0x1B, + 0x69, 0xBF, 0x56, 0xB3, 0x8B, 0xD4, 0x93, 0x2F, 0x9F, 0x9F, 0x9E, 0x36, 0x53, 0x31, 0x16, 0x9B, + 0xFE, 0xE6, 0xF9, 0xE7, 0xCD, 0x54, 0x99, 0x54, 0x4F, 0x1F, 0xCF, 0x28, 0xF9, 0xF9, 0x9B, 0x37, + 0xAF, 0x9B, 0xE9, 0x98, 0xDD, 0x40, 0x7C, 0x7B, 0xFA, 0xF2, 0x2F, 0x2F, 0xBE, 0x69, 0x27, 0x7C, + 0x86, 0xE6, 0xBE, 0x78, 0xF5, 0x65, 0xAB, 0x33, 0xA1, 0xCF, 0xE0, 0xCD, 0xEC, 0x51, 0xBD, 0x8A, + 0xF1, 0x27, 0xAB, 0x2E, 0xE9, 0xFF, 0x03, 0xFA, 0x08, 0x0E, 0xFC, 0x39, 0x89, 0x09, 0xEA, 0x7C, + 0x79, 0x40, 0xC8, 0x56, 0x41, 0x84, 0x9A, 0x2D, 0xE2, 0x85, 0x6A, 0xB0, 0x2F, 0x58, 0x92, 0xE9, + 0x10, 0x10, 0x1C, 0xF8, 0x80, 0xF8, 0xC7, 0x41, 0x56, 0x5B, 0xA0, 0xE4, 0x04, 0xFD, 0x4D, 0xC9, + 0x43, 0xAC, 0xB8, 0xFA, 0x54, 0xAB, 0xEF, 0xA5, 0x18, 0x09, 0x11, 0xE5, 0xCD, 0x8E, 0x31, 0xB0, + 0xBF, 0xC6, 0x34, 0x3C, 0x52, 0x59, 0xB2, 0x24, 0x59, 0x94, 0x27, 0x79, 0x46, 0x32, 0x93, 0xB0, + 0x67, 0xF1, 0xE4, 0xDA, 0x85, 0xB6, 0x57, 0xC9, 0xCF, 0xF5, 0x05, 0xC6, 0x24, 0x47, 0x64, 0x07, + 0xD8, 0x1C, 0x03, 0x3E, 0x0E, 0xD0, 0xAD, 0x60, 0xC2, 0x5D, 0xB7, 0x1D, 0x43, 0xAE, 0x68, 0xFA, + 0x16, 0x7D, 0x7F, 0xA4, 0xBA, 0xB8, 0x15, 0x3F, 0x44, 0x4F, 0xBE, 0x78, 0xF3, 0xD5, 0x97, 0x8F, + 0x9E, 0xA4, 0xE2, 0xBB, 0xE8, 0x09, 0x75, 0x30, 0xCD, 0xD6, 0x9B, 0x4A, 0x61, 0x9F, 0x9A, 0xFA, + 0x15, 0x03, 0x5F, 0xD4, 0x90, 0xC4, 0x54, 0x79, 0x16, 0x50, 0xBE, 0x3F, 0x21, 0xDF, 0xE5, 0xD9, + 0x82, 0x82, 0x7F, 0x46, 0x70, 0xFA, 0xF6, 0x7E, 0x36, 0x3C, 0xBB, 0x3F, 0x2B, 0x1F, 0x9F, 0x4D, + 0x33, 0x48, 0x4D, 0xAF, 0x93, 0xBD, 0xB3, 0x9B, 0x27, 0xE2, 0xEF, 0xB2, 0xB6, 0x5F, 0xFB, 0x53, + 0x42, 0x04, 0x98, 0x21, 0xFF, 0xEC, 0x06, 0x7F, 0xCF, 0x46, 0x3A, 0x02, 0x75, 0x89, 0x24, 0x89, + 0x9E, 0x4C, 0x31, 0xC2, 0x27, 0xA2, 0x4A, 0x1A, 0xB0, 0xF6, 0x1E, 0x54, 0xE3, 0xBB, 0xB8, 0x86, + 0xC6, 0x72, 0xC1, 0x73, 0x19, 0xF5, 0xD1, 0x59, 0xDE, 0xE1, 0xAD, 0x37, 0x4C, 0x24, 0xB6, 0xF6, + 0xC7, 0xC1, 0xC1, 0x27, 0x1F, 0x7F, 0xFC, 0xD1, 0x27, 0x86, 0x45, 0x04, 0xB3, 0x91, 0x81, 0x6D, + 0x94, 0x67, 0xE4, 0x68, 0x59, 0xE4, 0x57, 0x27, 0x97, 0x71, 0x71, 0x92, 0x2F, 0x40, 0x74, 0x0D, + 0x39, 0x6B, 0x10, 0xF6, 0x26, 0x3E, 0x7B, 0x36, 0x3E, 0xAC, 0x3F, 0xFE, 0xF8, 0xE9, 0x1F, 0x3F, + 0x11, 0xE3, 0xC3, 0xA7, 0x1F, 0xED, 0x67, 0xF5, 0xC7, 0x9F, 0x7C, 0xF4, 0xF4, 0x90, 0xD8, 0xD5, + 0x02, 0xA3, 0xF2, 0xA7, 0x84, 0xF8, 0x6E, 0xC7, 0x4B, 0xC6, 0x7D, 0xF5, 0xDB, 0x83, 0x09, 0xD6, + 0x03, 0x3F, 0x8F, 0x68, 0x93, 0xDB, 0x94, 0x83, 0xB3, 0xCD, 0x67, 0xF8, 0x87, 0x66, 0x04, 0xEC, + 0x42, 0xDA, 0x1E, 0x81, 0xEE, 0xE5, 0xC4, 0x3B, 0x3B, 0xF4, 0x48, 0xB6, 0x87, 0xC0, 0x66, 0xB9, + 0x5C, 0x2E, 0xBC, 0x50, 0x8F, 0xE8, 0x50, 0x80, 0x85, 0x1F, 0x62, 0xC2, 0x68, 0x90, 0x73, 0xD5, + 0xBD, 0xE7, 0x95, 0xAF, 0x4F, 0x1E, 0xA4, 0x1A, 0xA9, 0xA5, 0x3F, 0xFE, 0x04, 0x59, 0xF7, 0xBC, + 0x50, 0x66, 0x07, 0x9B, 0x9E, 0xB8, 0x8C, 0xE8, 0x1B, 0xE2, 0x68, 0xE3, 0x24, 0x3A, 0x4F, 0xFC, + 0xAE, 0x54, 0x64, 0x70, 0xC8, 0xB2, 0x45, 0x7D, 0xC8, 0x90, 0x90, 0x32, 0x4D, 0x56, 0x0B, 0x48, + 0xEA, 0xA8, 0x63, 0x52, 0x7A, 0xF9, 0x75, 0x7C, 0x95, 0xB4, 0x08, 0x01, 0x71, 0xBF, 0x48, 0x8B, + 0xD0, 0xB3, 0x82, 0x3A, 0x4F, 0x64, 0x04, 0xEB, 0x10, 0x71, 0x5D, 0x80, 0xA7, 0xF2, 0x40, 0x29, + 0x54, 0xC5, 0xDD, 0xFD, 0x17, 0x5A, 0xC6, 0x11, 0x7D, 0x23, 0x89, 0xD2, 0xF5, 0x88, 0xF7, 0x28, + 0x95, 0x28, 0x03, 0xD1, 0xFC, 0xAA, 0xA6, 0xCE, 0xB7, 0x91, 0x16, 0x19, 0xF9, 0xE9, 0x16, 0x32, + 0xDF, 0xF9, 0x25, 0xF5, 0xFC, 0x8B, 0xE8, 0x9E, 0xAB, 0x0D, 0x35, 0xED, 0x3A, 0x69, 0x4E, 0xEF, + 0x97, 0xB2, 0x55, 0x7C, 0xA8, 0x56, 0x2B, 0x2C, 0x5F, 0x2F, 0xB1, 0xAE, 0x67, 0x93, 0xB9, 0x8A, + 0x1B, 0xB4, 0x9D, 0xD0, 0x31, 0xAE, 0xE8, 0x5D, 0xFC, 0x04, 0x47, 0x86, 0xD6, 0xA5, 0x73, 0x7C, + 0x6B, 0xE5, 0x8F, 0x65, 0x22, 0x09, 0x6C, 0x51, 0xC8, 0xBA, 0x98, 0xC3, 0x17, 0x25, 0xD3, 0x3D, + 0x73, 0xB1, 0x8C, 0x58, 0xC0, 0x97, 0xDF, 0x64, 0x49, 0xF1, 0x42, 0xD3, 0x36, 0x6B, 0x12, 0x51, + 0x9A, 0xE1, 0x84, 0x7F, 0x04, 0xBD, 0x2A, 0x25, 0xAF, 0xD3, 0x99, 0xE6, 0x03, 0x8C, 0x60, 0x98, + 0x20, 0x78, 0x80, 0xFF, 0xC7, 0xA0, 0x8A, 0xD7, 0xFB, 0xFB, 0x7F, 0x94, 0x3F, 0x63, 0xFE, 0xB4, + 0x04, 0x06, 0x71, 0x2D, 0xC4, 0xDD, 0xBE, 0xC1, 0xAC, 0x88, 0x24, 0x02, 0x93, 0x78, 0x22, 0x5E, + 0x06, 0xAC, 0x2C, 0x90, 0x59, 0x91, 0xB6, 0x89, 0xFE, 0x0E, 0x46, 0x37, 0x99, 0xD3, 0x24, 0x10, + 0x99, 0x92, 0x46, 0x9B, 0xE9, 0x78, 0xC6, 0x79, 0xFE, 0x18, 0x21, 0x0B, 0x87, 0x06, 0x90, 0x07, + 0x24, 0x24, 0xB2, 0x55, 0xB2, 0xDF, 0x4F, 0xEF, 0x5E, 0x2D, 0x40, 0xE0, 0x06, 0x8D, 0xA6, 0xE2, + 0x51, 0xBA, 0x40, 0x89, 0xD4, 0x44, 0x4A, 0x3A, 0x38, 0xC6, 0x16, 0x95, 0xFC, 0x14, 0xF2, 0x2C, + 0x59, 0xB4, 0xB0, 0xEC, 0xA9, 0x6A, 0x7F, 0x9F, 0x16, 0x24, 0xC6, 0xEF, 0xFB, 0xEA, 0xA1, 0x0E, + 0x6D, 0xA6, 0x4F, 0x67, 0x3A, 0x5D, 0x03, 0x51, 0x26, 0xDC, 0x2E, 0x96, 0x9F, 0xDE, 0xBD, 0x89, + 0x2F, 0x08, 0x34, 0x69, 0x64, 0x82, 0x7B, 0xC8, 0x83, 0xFB, 0x68, 0x86, 0x36, 0x16, 0xCD, 0x9C, + 0x27, 0x40, 0xA6, 0x25, 0xF2, 0xD2, 0xAA, 0xEC, 0x48, 0x79, 0x6F, 0x6B, 0x26, 0x27, 0x46, 0x43, + 0x5D, 0x45, 0x7B, 0x8B, 0xD1, 0xCF, 0x25, 0x78, 0x96, 0xC1, 0xD7, 0xD3, 0x8A, 0xF6, 0xDF, 0x8C, + 0x98, 0xF0, 0x6B, 0x2C, 0xDC, 0x35, 0x84, 0xE5, 0x65, 0x45, 0xFD, 0x42, 0x0C, 0x2F, 0x84, 0x15, + 0x30, 0x0F, 0x76, 0xEF, 0x2A, 0xB9, 0x72, 0xF3, 0xA8, 0x22, 0xF8, 0x11, 0xE3, 0x48, 0x2E, 0xE0, + 0x5F, 0x74, 0x65, 0x75, 0xFD, 0x0F, 0x15, 0xA4, 0x9C, 0x3E, 0xF2, 0x24, 0xEA, 0x9B, 0x26, 0x17, + 0x90, 0xEB, 0xC8, 0xCD, 0x91, 0x39, 0x09, 0x68, 0x03, 0xD3, 0x54, 0x30, 0x19, 0x58, 0xE3, 0xE4, + 0x83, 0x96, 0xA9, 0x29, 0x97, 0xF7, 0xD2, 0x85, 0x07, 0x7E, 0xB4, 0x8C, 0x4A, 0x23, 0xE1, 0x28, + 0x12, 0xE0, 0xAB, 0x80, 0xF0, 0x50, 0x3B, 0x23, 0x64, 0x47, 0xA7, 0x18, 0x79, 0x1E, 0x81, 0xC4, + 0xBE, 0xA4, 0x4E, 0xA8, 0xAD, 0xA1, 0x36, 0x4E, 0x0E, 0x51, 0xD2, 0x0A, 0x6C, 0x52, 0xE4, 0x97, + 0x13, 0xEF, 0xD7, 0xDE, 0xB0, 0x0C, 0xBD, 0x90, 0x5B, 0xF6, 0x18, 0x39, 0x0D, 0x6F, 0x13, 0x9F, + 0x92, 0x83, 0xA3, 0x79, 0xB4, 0x1A, 0xFD, 0x94, 0xA7, 0x99, 0xEF, 0x09, 0x1C, 0x55, 0x84, 0x26, + 0x3A, 0x53, 0xBF, 0x1C, 0xB1, 0xB8, 0xFA, 0x94, 0x4F, 0xAB, 0xBC, 0x78, 0x8E, 0x3D, 0x3C, 0xA7, + 0x49, 0xB7, 0x38, 0xE0, 0x6B, 0xBF, 0x22, 0x1D, 0xD9, 0x16, 0x4A, 0x2B, 0xEC, 0xF0, 0xBB, 0x7B, + 0x92, 0x6B, 0x9D, 0xEE, 0xEF, 0x6B, 0x85, 0x41, 0x6B, 0x88, 0xD8, 0xB9, 0x86, 0x57, 0xAB, 0xCC, + 0x50, 0x1F, 0x09, 0xEF, 0xD1, 0xD8, 0x0B, 0xD4, 0x36, 0xB6, 0x7B, 0x9B, 0xB8, 0x8C, 0x7B, 0xCD, + 0xBD, 0xEA, 0x23, 0xC5, 0xA4, 0x26, 0xBE, 0xCB, 0x77, 0x2B, 0x7E, 0x95, 0xD7, 0x3F, 0x78, 0x76, + 0x0E, 0x7C, 0x03, 0x42, 0xFF, 0x4B, 0x9E, 0x17, 0x4C, 0x3D, 0xFA, 0x0F, 0x76, 0x0C, 0x62, 0x84, + 0x51, 0x79, 0x99, 0x2E, 0x2B, 0x3F, 0x80, 0xB2, 0x4A, 0xC1, 0x4A, 0x94, 0x39, 0xD8, 0x84, 0xF0, + 0x8E, 0x65, 0x64, 0xA6, 0xA7, 0xB3, 0x88, 0xC4, 0x4A, 0x36, 0x7D, 0x9E, 0x58, 0x01, 0xE9, 0x49, + 0x4B, 0x4D, 0x63, 0x11, 0x36, 0xE3, 0x5C, 0x8D, 0xD7, 0x07, 0xB4, 0x2F, 0xEC, 0x7C, 0x69, 0xC1, + 0x8C, 0x99, 0xB0, 0xCA, 0x42, 0x0B, 0xE9, 0x6B, 0x76, 0xE8, 0x5C, 0x50, 0x07, 0x78, 0x26, 0x62, + 0x84, 0x9D, 0xDE, 0x2E, 0x93, 0x06, 0x0E, 0xD5, 0x2C, 0x65, 0x8D, 0xA9, 0x2C, 0xA2, 0xAC, 0x09, + 0x15, 0x05, 0xA0, 0xE2, 0x7C, 0x14, 0x63, 0x39, 0xBE, 0x80, 0x30, 0x6D, 0x05, 0xEC, 0x0A, 0x41, + 0x02, 0xD0, 0xAB, 0xAD, 0x6D, 0xDD, 0xA8, 0xAD, 0xC2, 0x1A, 0x52, 0x2D, 0xFB, 0xFB, 0x04, 0xFF, + 0x16, 0x5D, 0xCA, 0xEF, 0xCA, 0xF9, 0x46, 0xBB, 0xF9, 0xA6, 0x98, 0x27, 0xAF, 0x48, 0xD9, 0x77, + 0x50, 0xB9, 0x5F, 0x84, 0x0B, 0x8A, 0x40, 0xAF, 0x10, 0xE3, 0xD9, 0x40, 0x76, 0x27, 0x43, 0xFF, + 0xE8, 0xA0, 0x3A, 0x4D, 0xCF, 0x57, 0xC0, 0xB7, 0x2C, 0xF2, 0x74, 0x98, 0xB6, 0x83, 0xB1, 0x91, + 0x71, 0x4C, 0xC6, 0xE1, 0xC1, 0xD8, 0xF6, 0x12, 0xB4, 0x81, 0x3D, 0xB8, 0x7B, 0xB4, 0x81, 0x44, + 0x5E, 0x3D, 0x78, 0x64, 0x52, 0x87, 0x09, 0xB7, 0x53, 0x6B, 0xCE, 0x5C, 0x5E, 0xA2, 0x4F, 0x9D, + 0x7A, 0xCD, 0x62, 0xEF, 0xAC, 0x4D, 0x75, 0xD3, 0x57, 0xED, 0x2A, 0xC5, 0x90, 0xA4, 0xE6, 0xF8, + 0xCB, 0x69, 0xAF, 0x01, 0x69, 0x17, 0xEF, 0x19, 0x07, 0xF8, 0xDC, 0x2B, 0x56, 0x50, 0x4D, 0x92, + 0x06, 0x80, 0x0C, 0xC6, 0x0D, 0x3A, 0x61, 0xE2, 0xAD, 0xE2, 0xF3, 0x64, 0x25, 0x73, 0xDA, 0xB0, + 0x53, 0xA6, 0x51, 0x81, 0x2D, 0x48, 0x7D, 0x0B, 0x3B, 0x9F, 0x69, 0xF9, 0x42, 0x45, 0xC8, 0x91, + 0xB8, 0x31, 0x40, 0x98, 0x03, 0x92, 0x2F, 0xD3, 0x16, 0xE8, 0x2B, 0x6D, 0x5B, 0x47, 0x37, 0x6D, + 0x5A, 0x6B, 0x9E, 0xAF, 0x13, 0x3F, 0x36, 0xE3, 0x5E, 0x39, 0x94, 0x50, 0x6E, 0x62, 0xF3, 0x68, + 0x98, 0x0B, 0x95, 0xD4, 0x95, 0x3A, 0xC6, 0xA4, 0x69, 0x36, 0xF4, 0x42, 0x1E, 0x40, 0xAA, 0x55, + 0x34, 0x41, 0x3D, 0x05, 0xA8, 0x03, 0xBE, 0xA3, 0x02, 0xB2, 0x2B, 0x1C, 0x05, 0x52, 0x20, 0x00, + 0xA1, 0x2C, 0xFD, 0x50, 0x18, 0x44, 0x08, 0xFE, 0xB5, 0x5D, 0xBA, 0xB3, 0x9B, 0x9E, 0xFB, 0xDE, + 0xA7, 0x84, 0xEF, 0x3D, 0xF1, 0x68, 0x9C, 0x5B, 0x12, 0x48, 0xE0, 0xC8, 0x05, 0x64, 0x46, 0x65, + 0x32, 0x52, 0x52, 0xA7, 0x88, 0x45, 0xE2, 0x25, 0x4D, 0xDF, 0xF7, 0x5F, 0x7D, 0x19, 0x75, 0xE1, + 0x89, 0xA7, 0x28, 0x43, 0x15, 0xE5, 0x1A, 0x18, 0xF0, 0x2F, 0xAF, 0x5F, 0x89, 0x8C, 0xC5, 0x93, + 0x2D, 0x5A, 0x85, 0x4E, 0x0F, 0x2D, 0x94, 0xD1, 0xCD, 0x6B, 0x91, 0xEE, 0x0F, 0xF2, 0xD0, 0x41, + 0x1E, 0x52, 0xBB, 0x18, 0xF8, 0x04, 0xF8, 0x11, 0xC7, 0x41, 0xCC, 0xC7, 0x1B, 0xEE, 0x51, 0x52, + 0xE9, 0xDA, 0xBA, 0xFD, 0x20, 0x84, 0x0B, 0x6A, 0xA8, 0xDB, 0x6A, 0xB8, 0x36, 0x42, 0xBF, 0x41, + 0x74, 0xB2, 0xBF, 0x4F, 0x24, 0x4A, 0xE1, 0x6C, 0xFA, 0xA2, 0xDD, 0x2B, 0xA6, 0x37, 0xFC, 0x93, + 0xA8, 0xE8, 0xF4, 0x17, 0x7A, 0xF2, 0x41, 0xEA, 0x9F, 0x80, 0xCA, 0xE4, 0x9A, 0xB0, 0xCF, 0x4F, + 0x46, 0x98, 0xDF, 0x18, 0x32, 0x93, 0xBF, 0xA6, 0xC9, 0x0D, 0x36, 0x08, 0xD9, 0x05, 0x20, 0x31, + 0xCA, 0x28, 0x75, 0x14, 0x2F, 0x16, 0x2F, 0xAF, 0x51, 0xEE, 0xCB, 0xB4, 0xAC, 0x12, 0xF4, 0x6A, + 0xD2, 0x8D, 0x22, 0x3B, 0x89, 0x55, 0x1E, 0xE3, 0x28, 0xCC, 0x61, 0xA1, 0x31, 0x0E, 0xC2, 0x8C, + 0x30, 0x1B, 0x30, 0x3E, 0xE7, 0x42, 0x85, 0xEE, 0xA7, 0xEF, 0xE5, 0x99, 0xCD, 0x8E, 0xD3, 0x4B, + 0x1D, 0xC4, 0xD1, 0xBC, 0x8F, 0x0E, 0xDF, 0x8B, 0x5D, 0x25, 0x38, 0xE2, 0x1B, 0x9F, 0x1D, 0x9C, + 0xBF, 0x48, 0xAF, 0x3D, 0x54, 0xD9, 0x0B, 0x32, 0x9D, 0xB3, 0x13, 0xBB, 0xB7, 0x1B, 0xE9, 0xAB, + 0xD3, 0x79, 0x4F, 0x1F, 0x1F, 0x7B, 0x5C, 0xA7, 0x02, 0xE9, 0x2D, 0x75, 0x37, 0xD6, 0x87, 0x68, + 0xB9, 0xA3, 0xCF, 0xE0, 0x4C, 0x34, 0x51, 0x14, 0x81, 0xEF, 0x14, 0x83, 0x36, 0x79, 0x61, 0x92, + 0xBD, 0x80, 0x6A, 0xEC, 0x05, 0xE6, 0x5D, 0x75, 0xF7, 0x4E, 0xC0, 0x49, 0x7E, 0x25, 0x27, 0x80, + 0x46, 0x3F, 0xD8, 0x41, 0x0F, 0x7A, 0x8F, 0xDD, 0x71, 0xEC, 0x22, 0x05, 0xA3, 0x3F, 0x4B, 0x40, + 0x3E, 0xD9, 0x91, 0x2E, 0x4B, 0x32, 0xFD, 0xFA, 0xA1, 0x4B, 0x06, 0xBA, 0xF6, 0x54, 0x0C, 0x5A, + 0x15, 0xCA, 0xCD, 0xD1, 0x17, 0xEB, 0x9F, 0xDA, 0x6E, 0xDA, 0xC6, 0x26, 0xFE, 0xF9, 0x68, 0x99, + 0xAE, 0x20, 0xFF, 0x1D, 0xBD, 0x7A, 0xD1, 0xB7, 0x85, 0x0D, 0xFD, 0x52, 0x81, 0x70, 0xB1, 0xBA, + 0xF0, 0x9E, 0x0E, 0xF6, 0x51, 0x7B, 0x12, 0x2F, 0x0A, 0x6A, 0x23, 0x5B, 0x34, 0x5B, 0x10, 0x15, + 0x13, 0xA0, 0x7D, 0x40, 0x55, 0xB5, 0x28, 0xFA, 0xFD, 0xFD, 0x97, 0xE6, 0xC4, 0x6E, 0x13, 0xFB, + 0xB6, 0x4B, 0xD9, 0x64, 0x9A, 0x91, 0x32, 0x76, 0xBB, 0x0D, 0xC2, 0x07, 0x47, 0x95, 0x7D, 0xD0, + 0xA8, 0xE4, 0xF8, 0x7B, 0xBA, 0xD7, 0x1A, 0xA8, 0x3C, 0xB1, 0x3A, 0x71, 0x72, 0x02, 0x4C, 0xCD, + 0xAC, 0x71, 0x82, 0x56, 0x6A, 0xC3, 0x47, 0xE4, 0x7F, 0x73, 0x4A, 0xA4, 0xC2, 0xB7, 0x67, 0x62, + 0xA4, 0x25, 0x0D, 0xD7, 0x08, 0x74, 0x94, 0xEF, 0xE8, 0x20, 0x61, 0x26, 0xD3, 0x31, 0xCD, 0x97, + 0x80, 0x72, 0x3E, 0x82, 0x22, 0xA2, 0x0B, 0x44, 0xA8, 0xD5, 0xE1, 0x5E, 0xA1, 0x44, 0x95, 0x8C, + 0xEB, 0xBF, 0xD7, 0x84, 0xA2, 0x92, 0x79, 0xC5, 0xF4, 0x7C, 0x40, 0xAE, 0x17, 0xF5, 0xEF, 0xDC, + 0x49, 0x9F, 0x54, 0x63, 0xE7, 0x54, 0x75, 0x8B, 0x77, 0xA3, 0xD5, 0x80, 0x42, 0x66, 0xAC, 0x90, + 0xDE, 0x41, 0x5A, 0x89, 0xD6, 0xEB, 0xF6, 0xF2, 0xF3, 0xC2, 0x51, 0x37, 0xF5, 0x57, 0x2E, 0xD7, + 0x80, 0x30, 0x04, 0x0F, 0xFC, 0x5E, 0x53, 0x85, 0x39, 0x6B, 0xB9, 0x82, 0x31, 0x81, 0x82, 0x39, + 0x6E, 0x8C, 0x36, 0x2A, 0x73, 0x54, 0x51, 0x9A, 0x58, 0x30, 0xF0, 0xC2, 0x72, 0xD1, 0x68, 0x37, + 0xC3, 0xF9, 0x8B, 0x81, 0xA9, 0x59, 0xFC, 0xA5, 0x5A, 0x9D, 0x9D, 0x59, 0x58, 0xB9, 0x5C, 0xD2, + 0xC0, 0xAF, 0xE9, 0x8F, 0x64, 0x4A, 0x0D, 0x72, 0xEB, 0x4C, 0x21, 0xF3, 0xA4, 0xD8, 0x66, 0x5D, + 0x35, 0x6E, 0x17, 0x9D, 0x65, 0x38, 0xF3, 0xE8, 0x50, 0x8F, 0xBC, 0xE3, 0x78, 0x0F, 0xC8, 0xED, + 0xB7, 0xDE, 0xF0, 0x74, 0xE8, 0xFD, 0xF6, 0xD9, 0xF1, 0x93, 0xF8, 0xD9, 0xB1, 0x14, 0x25, 0xDA, + 0xE8, 0x03, 0x92, 0xDC, 0xFD, 0x76, 0xEF, 0xAA, 0x04, 0x7B, 0x91, 0xDF, 0xCC, 0xE3, 0x35, 0x7A, + 0x9D, 0x44, 0xBF, 0x45, 0xEE, 0x7C, 0xCD, 0xF4, 0x8E, 0xD6, 0x7C, 0x70, 0xDC, 0x13, 0x19, 0x89, + 0x80, 0x8C, 0x7E, 0xE6, 0x89, 0xBE, 0x33, 0x6A, 0xDA, 0xAC, 0xEE, 0x2D, 0xCA, 0xCE, 0x0C, 0x72, + 0xDF, 0xDF, 0xBF, 0x96, 0xEB, 0xE3, 0x91, 0x8E, 0x61, 0x16, 0x59, 0xF5, 0x02, 0x89, 0xFB, 0xCF, + 0x48, 0x2A, 0xDD, 0x5F, 0xA9, 0xEE, 0x89, 0xAD, 0xAA, 0xAE, 0x55, 0x55, 0xA4, 0xC8, 0x30, 0xF5, + 0xF0, 0x0E, 0xA9, 0x49, 0xB4, 0xBB, 0xBB, 0xAE, 0x74, 0xF1, 0xCF, 0x48, 0x8E, 0xBF, 0xAF, 0x36, + 0xA4, 0x05, 0xC2, 0xEF, 0x30, 0x6D, 0x9A, 0x6E, 0x0F, 0x82, 0x16, 0x9B, 0x9D, 0xD1, 0x59, 0xC9, + 0xF6, 0x05, 0xCD, 0xF3, 0xAF, 0xDA, 0xD1, 0x3A, 0xF2, 0xD3, 0x1C, 0x3F, 0x38, 0x10, 0xCA, 0x23, + 0x43, 0x1F, 0x3C, 0x43, 0xA1, 0xD2, 0x56, 0x75, 0xAA, 0xB5, 0x49, 0x3B, 0x4A, 0xC6, 0xBF, 0xE6, + 0xC9, 0x18, 0x3E, 0xEE, 0x29, 0x3A, 0xFA, 0xF5, 0x68, 0x48, 0x62, 0xE1, 0x1D, 0x45, 0xCF, 0x20, + 0xF5, 0x45, 0x92, 0x59, 0x53, 0x2B, 0x0A, 0x66, 0xEA, 0xA1, 0x05, 0xB3, 0x49, 0x0B, 0x3C, 0x2F, + 0x8B, 0x64, 0x89, 0x99, 0xD8, 0x33, 0xF4, 0xFF, 0x6F, 0x75, 0xA8, 0x09, 0xAF, 0xDD, 0x74, 0x03, + 0xA1, 0x4F, 0x2C, 0x34, 0x4A, 0x83, 0xD6, 0xDD, 0xEB, 0x76, 0x54, 0xB5, 0x16, 0x0E, 0xFB, 0x98, + 0x16, 0x4E, 0xEA, 0xE8, 0xBA, 0xCB, 0xB7, 0x63, 0x9D, 0x5F, 0x78, 0x0F, 0xAD, 0xEB, 0xA2, 0x0F, + 0xD4, 0xED, 0x6A, 0x5A, 0xC5, 0x1A, 0x6A, 0x79, 0x0A, 0x3A, 0xB6, 0x77, 0x25, 0x93, 0x8C, 0x07, + 0xD9, 0xA9, 0xC9, 0x26, 0xA1, 0x1F, 0xA1, 0x9E, 0x0B, 0xD4, 0xD4, 0x41, 0x03, 0x66, 0xC6, 0x06, + 0x87, 0xBB, 0x9B, 0x41, 0xA6, 0x5F, 0xD8, 0x4E, 0x5F, 0x35, 0x8F, 0x45, 0x78, 0xEB, 0x80, 0x80, + 0x18, 0x3D, 0x0E, 0x69, 0xED, 0x03, 0xC2, 0x6A, 0x57, 0x24, 0xCA, 0x48, 0x4A, 0x9D, 0x5F, 0x63, + 0xB8, 0x79, 0x14, 0xEB, 0xA4, 0xBA, 0x8E, 0x47, 0x37, 0xC9, 0xF9, 0xBB, 0xB4, 0xFA, 0xAA, 0x99, + 0x97, 0x12, 0xAE, 0xF2, 0x7F, 0xF4, 0xC4, 0xE6, 0x7D, 0x39, 0xCB, 0x56, 0x24, 0xA1, 0xCC, 0x16, + 0xF4, 0x2D, 0x68, 0x56, 0xA0, 0xA8, 0xCD, 0x18, 0x8F, 0x70, 0xFE, 0x68, 0xAE, 0x2D, 0x01, 0x59, + 0x13, 0x67, 0xBF, 0xA6, 0xE5, 0x80, 0xB6, 0x28, 0x8F, 0xAC, 0x54, 0x23, 0x1B, 0x44, 0x50, 0x5E, + 0x12, 0x54, 0x5F, 0x47, 0xD7, 0x66, 0xC2, 0x1C, 0x4D, 0xC9, 0xB5, 0x92, 0x90, 0xD5, 0x44, 0xE7, + 0x42, 0x3C, 0xD7, 0x97, 0xA7, 0x74, 0xF3, 0x54, 0x7A, 0x3E, 0x62, 0x18, 0xF3, 0x5C, 0x11, 0xE3, + 0xAD, 0xB9, 0xAA, 0x6F, 0xF3, 0x32, 0xA5, 0x6E, 0xC3, 0x96, 0x99, 0x58, 0x6B, 0x27, 0x5B, 0x56, + 0xC1, 0xFA, 0xA8, 0x0C, 0x26, 0x7D, 0x92, 0xF1, 0x3F, 0x36, 0xA4, 0x2F, 0x93, 0xA4, 0xCD, 0x5D, + 0x85, 0x24, 0xA5, 0xA9, 0x9A, 0x82, 0xA3, 0x23, 0x47, 0x13, 0x8F, 0x99, 0x84, 0xD9, 0x12, 0xFE, + 0x8E, 0x07, 0x2E, 0x07, 0x47, 0xB1, 0x99, 0x69, 0x7A, 0x62, 0x83, 0x7E, 0x01, 0x99, 0xE3, 0x8E, + 0xAE, 0x93, 0x30, 0xE8, 0x93, 0xFD, 0x9D, 0xA9, 0x28, 0x4A, 0xF2, 0xFD, 0xCE, 0x39, 0x5B, 0x29, + 0x99, 0x4F, 0x15, 0xB9, 0x9D, 0x0C, 0x28, 0xC5, 0xA1, 0x7A, 0x06, 0x87, 0x47, 0x46, 0x36, 0x26, + 0x7E, 0x82, 0x05, 0x4D, 0xA7, 0x9E, 0xC4, 0xD5, 0xEA, 0xAF, 0x68, 0x17, 0x1C, 0x1E, 0xC9, 0x49, + 0x1A, 0xEC, 0xEC, 0xD3, 0xC1, 0xA0, 0xDA, 0x95, 0x64, 0x88, 0x63, 0x48, 0x64, 0xC7, 0xC4, 0x96, + 0xF6, 0x31, 0xDF, 0x51, 0xE4, 0x57, 0xED, 0xD8, 0x2A, 0x98, 0xEC, 0x6C, 0x0F, 0x83, 0x0D, 0xC7, + 0x01, 0x66, 0x77, 0xC1, 0xD6, 0x8A, 0x2F, 0x12, 0x62, 0x43, 0x49, 0x9D, 0xB3, 0xB3, 0x1B, 0x52, + 0x9A, 0x92, 0x4D, 0x30, 0xBA, 0x13, 0xB4, 0xD8, 0x6C, 0x8C, 0x25, 0xCF, 0x77, 0xFE, 0x1A, 0xC6, + 0x47, 0x64, 0x51, 0x54, 0x71, 0x9E, 0x6A, 0x47, 0x1E, 0xF4, 0x6B, 0x1C, 0x6E, 0x26, 0xDF, 0xFA, + 0x1B, 0x64, 0x3F, 0xA0, 0x1F, 0x74, 0xE6, 0x30, 0xFC, 0xDD, 0x7E, 0x46, 0x65, 0xC7, 0x58, 0x9A, + 0x0F, 0x9F, 0x52, 0x63, 0x85, 0x64, 0x17, 0x8C, 0x69, 0x39, 0xFB, 0x49, 0xA6, 0x3E, 0xC9, 0x8C, + 0x08, 0x9E, 0x6A, 0xC6, 0x0A, 0x90, 0x14, 0xA3, 0xCE, 0x75, 0x5D, 0x34, 0x1A, 0xDD, 0x63, 0xF4, + 0x2A, 0xA5, 0x70, 0xDE, 0xD7, 0x3D, 0x2A, 0x9A, 0xA2, 0x17, 0xAA, 0xA4, 0x11, 0x43, 0x1E, 0x81, + 0x15, 0x39, 0x32, 0xB2, 0x42, 0x17, 0x6E, 0xE2, 0xD1, 0x26, 0x93, 0x52, 0xDC, 0x8C, 0x72, 0x55, + 0xFD, 0xB9, 0x4A, 0x27, 0x97, 0xCA, 0x11, 0x43, 0xE2, 0x89, 0x96, 0x4A, 0xFC, 0x04, 0xA0, 0xCD, + 0x0D, 0x29, 0x39, 0x59, 0xCB, 0x34, 0xC1, 0x29, 0xA1, 0xCC, 0xB6, 0xA6, 0x1E, 0x97, 0x32, 0x88, + 0x7E, 0x1F, 0x02, 0x45, 0x9C, 0x80, 0xBC, 0x4B, 0x34, 0x8E, 0xEB, 0xD7, 0x24, 0x96, 0xD4, 0x79, + 0x92, 0xD2, 0xF2, 0x1F, 0x24, 0x50, 0x91, 0x0E, 0xC6, 0x6C, 0x2F, 0x83, 0xD4, 0x1B, 0x75, 0x30, + 0x2B, 0x68, 0xCE, 0x96, 0x4E, 0xA3, 0xC4, 0x14, 0x97, 0x46, 0xF7, 0xD0, 0xD5, 0x72, 0x04, 0x24, + 0x77, 0xE6, 0xF5, 0xB3, 0x48, 0xB0, 0x62, 0x42, 0x1B, 0x40, 0xDE, 0x83, 0x2A, 0xEB, 0xDA, 0xE2, + 0x13, 0xA5, 0xD5, 0xB2, 0x11, 0x06, 0x57, 0xE8, 0x95, 0xE9, 0x91, 0xFA, 0xAB, 0x94, 0xC3, 0x63, + 0x56, 0xC3, 0x9D, 0xC8, 0x51, 0x03, 0x2C, 0xF4, 0xE1, 0x43, 0xC3, 0xD7, 0xA8, 0xA5, 0x7F, 0xCA, + 0xFA, 0x36, 0x1D, 0xCB, 0x89, 0x78, 0x4E, 0xEE, 0x38, 0x2B, 0xD5, 0xC2, 0x42, 0x90, 0x56, 0x0D, + 0x0F, 0x96, 0x55, 0xA8, 0xA1, 0x21, 0xF3, 0x6E, 0x99, 0x51, 0xCD, 0xA4, 0x80, 0xFB, 0x85, 0x9C, + 0x2A, 0x37, 0xA7, 0x68, 0xE5, 0x0C, 0x26, 0xD2, 0xDA, 0x6D, 0xF0, 0x52, 0xF3, 0x3D, 0x1A, 0x7A, + 0xAC, 0x75, 0xEB, 0xA4, 0x08, 0x1D, 0x51, 0x0D, 0x2D, 0xD5, 0xCB, 0x49, 0x8B, 0xF1, 0x07, 0xC0, + 0xC3, 0x20, 0xAE, 0xCB, 0x10, 0xCB, 0xD5, 0x2C, 0x20, 0xC0, 0x4F, 0xE6, 0xE9, 0x32, 0x85, 0x58, + 0xB7, 0x90, 0x5C, 0x61, 0x48, 0x13, 0xCA, 0xC3, 0x4F, 0x4A, 0x90, 0xDE, 0xAE, 0xFE, 0xDA, 0x4E, + 0xE0, 0x10, 0x64, 0x6A, 0x4B, 0x6D, 0xC4, 0x45, 0xD8, 0x1C, 0xB6, 0x51, 0xA2, 0x73, 0x5B, 0xE9, + 0xF4, 0x0E, 0x4B, 0x73, 0xBB, 0xC7, 0x39, 0xC5, 0xDE, 0x26, 0x2B, 0x92, 0x79, 0x7E, 0x91, 0xA5, + 0xFF, 0x48, 0x16, 0x7B, 0x30, 0xC6, 0xC5, 0xFD, 0xA5, 0x12, 0x25, 0x43, 0x28, 0x8D, 0x54, 0x95, + 0x9B, 0x2C, 0x05, 0xB1, 0x70, 0x9A, 0x17, 0xBD, 0xB2, 0x44, 0xB0, 0x3D, 0x0A, 0x6F, 0xF0, 0xB6, + 0x06, 0x3E, 0x01, 0xCC, 0x25, 0x15, 0xE0, 0xED, 0xC5, 0x86, 0x8C, 0xB5, 0x63, 0x4C, 0x0B, 0xEC, + 0xAC, 0x15, 0x6E, 0x84, 0xD5, 0xF8, 0x39, 0xDD, 0xCF, 0x32, 0x9A, 0x76, 0x90, 0x22, 0x9C, 0xE0, + 0xFF, 0x14, 0x88, 0x95, 0x66, 0x09, 0x21, 0x67, 0x91, 0x2C, 0x21, 0x30, 0x09, 0x05, 0x69, 0x1F, + 0x60, 0xD5, 0xB4, 0xE9, 0x5F, 0xE0, 0xA8, 0x37, 0x48, 0x01, 0xC2, 0x35, 0x65, 0xB4, 0x99, 0xC7, + 0x06, 0x3A, 0x37, 0x11, 0x83, 0x25, 0xE9, 0xE3, 0xA3, 0x92, 0x27, 0xFF, 0x4D, 0x72, 0xDB, 0x3F, + 0x00, 0xCF, 0x53, 0x03, 0xB0, 0x47, 0x2E, 0x23, 0x28, 0xA9, 0xCA, 0x45, 0x0F, 0x80, 0xE4, 0xFE, + 0x28, 0x7F, 0xC6, 0xF4, 0x29, 0x13, 0xBA, 0xC6, 0xA3, 0x7C, 0x49, 0x87, 0xED, 0x4E, 0x32, 0x8D, + 0x5E, 0x9B, 0x91, 0x47, 0x2C, 0x36, 0x46, 0x33, 0x6C, 0x63, 0xC2, 0x24, 0xDE, 0x51, 0x72, 0x84, + 0x88, 0x86, 0x56, 0x24, 0x1B, 0x46, 0x39, 0xBA, 0x67, 0xB4, 0xBA, 0x1F, 0xC9, 0xA6, 0x7F, 0xE7, + 0x6A, 0x6D, 0x65, 0x4F, 0xFF, 0x4A, 0xD0, 0x22, 0xF3, 0x99, 0x79, 0x63, 0x01, 0x84, 0xAC, 0x03, + 0x48, 0xC0, 0x6C, 0x62, 0xE1, 0x9F, 0xB3, 0x5C, 0x58, 0x61, 0x99, 0x32, 0xBA, 0x77, 0x74, 0x65, + 0xE1, 0xC7, 0x87, 0x42, 0x52, 0xDA, 0xDF, 0x96, 0xC9, 0x06, 0xB6, 0xD8, 0xD8, 0x0B, 0x8C, 0x96, + 0xC2, 0xCF, 0x85, 0xDD, 0x1E, 0x64, 0xF1, 0x4D, 0x3C, 0x37, 0xFD, 0x16, 0xC9, 0x8A, 0xAC, 0x52, + 0x10, 0xE7, 0x3D, 0xF3, 0xC2, 0x8E, 0x39, 0x82, 0xBA, 0x44, 0x41, 0x06, 0xC8, 0xDE, 0x5E, 0x4F, + 0x3A, 0xA2, 0x87, 0x3A, 0x9A, 0x6E, 0x03, 0xA4, 0xF9, 0xA6, 0x54, 0xC3, 0x6F, 0x94, 0xFD, 0xE7, + 0xAE, 0x4C, 0x10, 0x18, 0x21, 0xEA, 0x33, 0x16, 0x69, 0x85, 0xF7, 0x6C, 0xDD, 0xD4, 0x27, 0x81, + 0x83, 0xAE, 0x3D, 0xA2, 0x3F, 0x2D, 0xF1, 0x16, 0xD4, 0x80, 0x1F, 0x41, 0x53, 0x4A, 0x7F, 0x81, + 0x3D, 0xA6, 0xBF, 0xE3, 0xBF, 0x1F, 0xE3, 0xAF, 0xD9, 0x53, 0x36, 0x2B, 0x31, 0x91, 0x0C, 0x84, + 0x4F, 0x09, 0x08, 0xB9, 0x20, 0xE9, 0x53, 0x29, 0xC0, 0x5A, 0x47, 0x61, 0x6D, 0x46, 0x7E, 0x17, + 0x6C, 0xA5, 0xE1, 0xD4, 0x83, 0x7D, 0x69, 0xE0, 0x18, 0xD4, 0x0F, 0x9B, 0x23, 0x34, 0xC0, 0x49, + 0xA6, 0xA6, 0x8F, 0x82, 0x89, 0xEA, 0x9D, 0xDE, 0xD1, 0xF8, 0x3C, 0x84, 0x35, 0x2A, 0x75, 0x36, + 0x1A, 0xFA, 0xF4, 0x33, 0xA1, 0x2E, 0x53, 0xF0, 0x13, 0x64, 0x83, 0x34, 0xFC, 0x29, 0x8C, 0xFF, + 0xC8, 0xA2, 0x89, 0x2A, 0x93, 0x65, 0x3D, 0xD8, 0x36, 0xE9, 0xAF, 0x80, 0xCA, 0x7E, 0x2C, 0xCB, + 0xFE, 0x7E, 0x86, 0xEE, 0xFF, 0xA1, 0x93, 0x21, 0xC4, 0x0F, 0xC6, 0xD8, 0x6E, 0x71, 0xAB, 0xAD, + 0xC4, 0xFA, 0xB6, 0xCE, 0x00, 0xCD, 0xA3, 0x0C, 0xCD, 0x8E, 0x86, 0xB5, 0xCF, 0x47, 0x3C, 0x07, + 0x7C, 0x3C, 0xC9, 0x3A, 0x26, 0xB4, 0x13, 0x43, 0x1E, 0xD0, 0x84, 0x72, 0x46, 0xCD, 0x29, 0x0F, + 0x81, 0x8D, 0xBF, 0x97, 0xD9, 0x33, 0x3A, 0xDE, 0xAA, 0x08, 0x32, 0x1F, 0x3A, 0x68, 0xE4, 0x47, + 0xA6, 0x2F, 0x14, 0xFA, 0x60, 0x98, 0x85, 0x56, 0x70, 0x1E, 0xC0, 0xE2, 0x4C, 0x87, 0x91, 0x8F, + 0x1B, 0x8A, 0xF0, 0xC7, 0xCE, 0x21, 0x98, 0x76, 0x6E, 0x2C, 0x73, 0x62, 0xDC, 0xD5, 0xFA, 0x08, + 0x64, 0x2B, 0x00, 0x5A, 0x81, 0x10, 0xD9, 0xCF, 0xBD, 0x5F, 0xDA, 0xDB, 0xAB, 0x06, 0x54, 0xE2, + 0x2C, 0x4B, 0xBA, 0x5A, 0xEB, 0x9E, 0xDE, 0x2B, 0x94, 0x56, 0xAB, 0xB8, 0xBF, 0xBF, 0x43, 0xC3, + 0xA8, 0xE5, 0xC4, 0x2C, 0xDD, 0xEA, 0xE9, 0xD7, 0xD5, 0x34, 0x61, 0xF2, 0xC0, 0xB5, 0xB8, 0xAA, + 0x1A, 0xB6, 0x5E, 0xFE, 0x5B, 0x63, 0x9C, 0x88, 0xAC, 0x3E, 0x87, 0xC9, 0x42, 0x8D, 0x26, 0xF5, + 0x0A, 0xA7, 0x5A, 0xDF, 0xD5, 0x4E, 0x5E, 0x83, 0x3E, 0xBC, 0x36, 0xB7, 0x02, 0x31, 0xE7, 0xA3, + 0xAE, 0xDF, 0x2F, 0x0C, 0x46, 0x81, 0x3E, 0xA5, 0x84, 0x17, 0xF0, 0x5E, 0xDB, 0x62, 0xC3, 0x34, + 0xF7, 0x2E, 0x4B, 0x6F, 0x1F, 0xD0, 0xC8, 0x2A, 0x92, 0x00, 0x03, 0x00, 0x81, 0xD7, 0x32, 0xBE, + 0x27, 0xB6, 0x8E, 0x78, 0x9F, 0x70, 0x50, 0xD0, 0x74, 0x0C, 0x09, 0xAF, 0x7B, 0x32, 0x6A, 0x52, + 0x11, 0xF2, 0x0C, 0x91, 0x43, 0x7D, 0x0E, 0xF8, 0xF3, 0xAD, 0xFA, 0xC4, 0xD5, 0x17, 0xBE, 0xDD, + 0x67, 0x20, 0x2D, 0x0D, 0x42, 0xEF, 0xB1, 0x4D, 0x3C, 0x18, 0x1F, 0x37, 0xD3, 0x1E, 0xD9, 0xB4, + 0x4A, 0x01, 0xD4, 0x41, 0xAA, 0xA1, 0x51, 0x36, 0xF5, 0x4F, 0x95, 0x05, 0x65, 0x7D, 0x42, 0x18, + 0xD6, 0xF0, 0xE1, 0x53, 0x41, 0x78, 0x83, 0x91, 0x47, 0xA3, 0xD2, 0x5A, 0x96, 0x20, 0xA0, 0x97, + 0x18, 0xBF, 0x32, 0xB0, 0xAA, 0xEB, 0x1E, 0x8E, 0xB9, 0x76, 0x88, 0xBB, 0xB0, 0x90, 0xDB, 0x0E, + 0xB6, 0xB9, 0x14, 0x44, 0xAE, 0xE0, 0x9E, 0xAD, 0x9C, 0xAE, 0xBB, 0x88, 0x91, 0x0B, 0xC6, 0x7A, + 0xE9, 0x40, 0x3D, 0xAC, 0xBB, 0x3D, 0xB2, 0x6E, 0x74, 0xE3, 0x0F, 0x7E, 0x47, 0xD6, 0xDE, 0x9E, + 0xB2, 0xDD, 0x64, 0x70, 0xD6, 0xB3, 0x4B, 0x07, 0xDE, 0x85, 0x9C, 0x9F, 0xEB, 0x49, 0x8F, 0x05, + 0xDB, 0xC0, 0xE5, 0x16, 0xB6, 0xBB, 0xAE, 0x09, 0x58, 0xFB, 0xAC, 0xE8, 0x0E, 0xED, 0x5E, 0x4D, + 0x3C, 0xE7, 0xC4, 0xF3, 0x7A, 0x0E, 0x81, 0x79, 0x93, 0x0B, 0x59, 0x46, 0xB7, 0xBB, 0x37, 0x8B, + 0x58, 0xE3, 0x4E, 0x03, 0xDD, 0xDA, 0x14, 0x0B, 0xBA, 0xBB, 0x45, 0x56, 0x3C, 0x7C, 0x42, 0xDF, + 0x69, 0x72, 0x02, 0x74, 0x45, 0x6C, 0x18, 0x8B, 0x18, 0x17, 0x9E, 0x56, 0x2C, 0xA2, 0xBF, 0x9D, + 0xC4, 0xBB, 0xB7, 0xDF, 0x32, 0xA4, 0x91, 0xC7, 0x6D, 0x7A, 0x18, 0xF5, 0x6F, 0xA2, 0x15, 0xA6, + 0x2A, 0x5B, 0xF1, 0xC5, 0xD0, 0x4B, 0xB4, 0x8B, 0x1B, 0x5D, 0x8D, 0xE1, 0x6C, 0xCD, 0xF6, 0x27, + 0x5B, 0xAA, 0x68, 0x7A, 0x35, 0x99, 0x3B, 0x27, 0x7E, 0x38, 0x1F, 0xAD, 0x62, 0x15, 0x9E, 0x89, + 0xAB, 0xFD, 0xFD, 0x35, 0x04, 0x1A, 0x11, 0xAC, 0x83, 0xFC, 0x82, 0x6E, 0x3B, 0x40, 0x89, 0x80, + 0x2E, 0xCE, 0x03, 0x98, 0xA1, 0xD0, 0xB5, 0x31, 0xFC, 0xD0, 0xCD, 0xB1, 0x60, 0x1A, 0x2B, 0x32, + 0xEC, 0xD5, 0x0B, 0x8A, 0xCF, 0x9D, 0x6F, 0x95, 0xE1, 0x12, 0xF1, 0xB8, 0x79, 0x07, 0x14, 0x88, + 0x6E, 0xBD, 0x03, 0x61, 0x49, 0x86, 0x66, 0xF4, 0xF3, 0x74, 0x06, 0xAE, 0xAD, 0x84, 0x10, 0xC5, + 0xDA, 0xF7, 0xD1, 0xBD, 0x32, 0x33, 0x1D, 0xC3, 0x21, 0x12, 0x63, 0xFC, 0x37, 0x5D, 0x51, 0xD5, + 0xD0, 0x68, 0x47, 0x87, 0xD8, 0x9F, 0x1B, 0xB2, 0xD2, 0xF7, 0xD9, 0x6A, 0xAD, 0x39, 0x13, 0xFB, + 0xFB, 0xC3, 0xE1, 0x02, 0xD9, 0xA5, 0x0C, 0x3F, 0x45, 0xC3, 0xD1, 0xF4, 0x1D, 0x16, 0x77, 0x31, + 0x3B, 0x92, 0xD7, 0x7B, 0x0C, 0xE5, 0x02, 0x16, 0x15, 0xD5, 0x35, 0x47, 0x96, 0xFC, 0x97, 0x46, + 0x26, 0xD8, 0x26, 0x62, 0x11, 0x7C, 0xF8, 0x28, 0x7E, 0xE1, 0x7A, 0xAB, 0x61, 0xF2, 0x20, 0x64, + 0xEF, 0xE3, 0x7F, 0xA3, 0xE7, 0x34, 0x35, 0x0B, 0xF4, 0x96, 0x67, 0xAB, 0x79, 0x9B, 0xC9, 0x5F, + 0x1C, 0x44, 0xD7, 0xD4, 0xF4, 0x05, 0xD8, 0xB4, 0xDF, 0x5C, 0x44, 0xD1, 0x21, 0xDD, 0x24, 0x8E, + 0x16, 0x4F, 0x2E, 0xB6, 0xDB, 0x9E, 0x43, 0x57, 0xE4, 0xFA, 0xD8, 0x8D, 0xC1, 0xDF, 0xAC, 0x99, + 0x50, 0xC3, 0x45, 0x5E, 0xB4, 0x7C, 0x4E, 0xE2, 0x4D, 0x49, 0x0B, 0x21, 0xA2, 0x39, 0x32, 0x97, + 0x84, 0x00, 0xBA, 0x56, 0x96, 0x0A, 0xA0, 0xF4, 0x65, 0x05, 0x92, 0xCA, 0xD7, 0x7B, 0x9D, 0x46, + 0x36, 0x89, 0xFD, 0x1C, 0xD2, 0x8C, 0xE3, 0x58, 0x5B, 0x77, 0x02, 0x1B, 0x4D, 0x13, 0x20, 0x16, + 0x60, 0x53, 0xDC, 0xC2, 0x71, 0xDB, 0x6A, 0x5D, 0xEF, 0xF0, 0x93, 0x36, 0xFB, 0xF4, 0x80, 0x39, + 0x47, 0xF2, 0xA0, 0x11, 0xC7, 0xB7, 0x48, 0x2F, 0xE8, 0x12, 0x92, 0x31, 0xE1, 0xE0, 0xAF, 0x6D, + 0xD0, 0x77, 0x9E, 0x52, 0x65, 0x38, 0xDC, 0x29, 0x35, 0xC6, 0xC4, 0xA9, 0x99, 0x09, 0x71, 0xB5, + 0xAD, 0x0A, 0x57, 0x5D, 0x55, 0x8B, 0xD4, 0x52, 0xD1, 0x1F, 0xF0, 0xA6, 0x7E, 0xD2, 0xB6, 0x47, + 0x33, 0x93, 0x51, 0x62, 0x32, 0xDA, 0x23, 0x68, 0x58, 0x9C, 0x46, 0xA5, 0x66, 0xFB, 0x0B, 0x31, + 0xE5, 0x25, 0x4E, 0x9A, 0xA3, 0x89, 0x31, 0x1A, 0x40, 0x0E, 0xE0, 0x62, 0x26, 0x49, 0x94, 0x98, + 0x87, 0x43, 0x3F, 0x29, 0x99, 0xA3, 0xEC, 0xBA, 0x4D, 0x56, 0x30, 0x2D, 0x23, 0x20, 0x95, 0x53, + 0x52, 0x05, 0x1C, 0x89, 0x82, 0x23, 0xF9, 0x73, 0x90, 0x4A, 0x98, 0x26, 0x2D, 0x21, 0x56, 0xC0, + 0x1D, 0xE3, 0x03, 0xD6, 0x45, 0x96, 0x61, 0x4F, 0x34, 0x9F, 0x4E, 0x15, 0x28, 0x36, 0x7D, 0x57, + 0x2D, 0x90, 0xDE, 0xB5, 0x09, 0xE2, 0x6E, 0xC5, 0x74, 0xC4, 0x35, 0xF8, 0x9C, 0xBA, 0x26, 0x06, + 0xC6, 0x9E, 0x6E, 0x15, 0xF7, 0x75, 0x15, 0x67, 0x17, 0x8D, 0x66, 0xEC, 0x88, 0xFF, 0xAA, 0xC8, + 0x3F, 0xA6, 0x0A, 0x76, 0x41, 0x2C, 0x95, 0x27, 0x78, 0x85, 0x2C, 0x15, 0x62, 0x9F, 0x07, 0x29, + 0x33, 0xD1, 0xD5, 0xAF, 0x2D, 0xF2, 0x7B, 0x96, 0x3D, 0x12, 0x7B, 0xCE, 0x35, 0xB5, 0x09, 0x93, + 0xDB, 0xAB, 0x55, 0x48, 0x09, 0xD4, 0x81, 0x76, 0x9A, 0x8C, 0x57, 0x47, 0x00, 0x2A, 0x69, 0x0B, + 0x0A, 0x1C, 0x0F, 0x01, 0x66, 0xD0, 0x19, 0x9F, 0xD2, 0x5B, 0x09, 0x0A, 0xC4, 0xF5, 0xB9, 0x82, + 0xAA, 0xB6, 0x81, 0x5D, 0x60, 0xC4, 0xA0, 0x34, 0x53, 0xB8, 0x1D, 0x80, 0xF6, 0x7B, 0xE8, 0x3E, + 0x50, 0xBC, 0xF9, 0x3C, 0x96, 0x42, 0x59, 0x1B, 0xA6, 0x7D, 0x78, 0xE9, 0x2A, 0xD6, 0x8D, 0x5D, + 0x3E, 0xB7, 0x91, 0x2E, 0xC0, 0xAF, 0xE5, 0x79, 0xB3, 0x3E, 0x47, 0x58, 0x8C, 0xDB, 0xB8, 0x7C, + 0x07, 0x6A, 0x57, 0xFA, 0xC9, 0x28, 0x9E, 0x13, 0xAF, 0x67, 0x6D, 0x7E, 0x06, 0x27, 0xD4, 0xE4, + 0x67, 0x7C, 0x71, 0xAA, 0xB6, 0x61, 0x4C, 0x04, 0x4E, 0xC3, 0x81, 0x2F, 0x4D, 0xDF, 0x68, 0x16, + 0x49, 0x2D, 0x54, 0xD7, 0xFF, 0x44, 0x44, 0x7C, 0xCE, 0xB6, 0x81, 0x20, 0xF8, 0x94, 0x3E, 0x22, + 0x84, 0x2D, 0x1C, 0x8C, 0x78, 0x04, 0xC4, 0x53, 0xF6, 0x1B, 0xD2, 0x00, 0xA5, 0xE0, 0x0A, 0x7F, + 0xB1, 0x35, 0x9E, 0x63, 0x8C, 0x47, 0xBD, 0x48, 0x46, 0xAA, 0x26, 0xC0, 0x94, 0x54, 0x30, 0x39, + 0x49, 0x5A, 0x05, 0xB9, 0x15, 0x3A, 0xD4, 0x37, 0xF8, 0xA6, 0x0D, 0x9E, 0xF3, 0x65, 0x2B, 0xE0, + 0x41, 0x09, 0x65, 0xC6, 0x6F, 0x6B, 0x4D, 0xE8, 0xC6, 0x6E, 0xA3, 0xCA, 0x0F, 0xE2, 0xFC, 0x01, + 0xA0, 0x16, 0x24, 0x8E, 0x3F, 0xE9, 0xBB, 0xCF, 0x2B, 0xFB, 0xD0, 0xD3, 0xDB, 0x81, 0x39, 0x13, + 0x46, 0xDC, 0x3A, 0x2B, 0xA6, 0xC9, 0xBD, 0x47, 0x52, 0xF4, 0x8D, 0xED, 0x4F, 0x72, 0xC7, 0x99, + 0x39, 0xA5, 0xDC, 0x3C, 0x81, 0x7D, 0x99, 0xBF, 0xEB, 0xC9, 0x2C, 0xAD, 0x1D, 0xFF, 0xC3, 0x65, + 0x72, 0x6C, 0x26, 0x35, 0xD0, 0x38, 0x51, 0xF0, 0x26, 0xC1, 0x57, 0x78, 0xBA, 0x9B, 0xF9, 0xC3, + 0xCD, 0x3E, 0x3D, 0xAA, 0xC1, 0xD6, 0x0F, 0xE0, 0x95, 0xEC, 0x82, 0x4F, 0x1D, 0xBD, 0xE8, 0x2A, + 0x11, 0xE9, 0xFE, 0xB6, 0x2E, 0xD3, 0xDA, 0xE6, 0xD6, 0x39, 0x04, 0xCC, 0x19, 0x3B, 0xCC, 0x1E, + 0xD0, 0x33, 0xE3, 0x38, 0x9D, 0xDC, 0x15, 0x7A, 0x4E, 0xE1, 0x21, 0x84, 0xF2, 0x24, 0x3F, 0xEB, + 0x1C, 0x5D, 0xDC, 0x3F, 0xA5, 0x7B, 0x33, 0xD9, 0x10, 0xD7, 0x96, 0x28, 0x27, 0xB1, 0xF3, 0x94, + 0x77, 0xE7, 0x65, 0x48, 0xDC, 0x80, 0xAC, 0x8E, 0x20, 0xEB, 0x79, 0x1A, 0x24, 0x6D, 0x5B, 0x89, + 0x04, 0xE5, 0xC1, 0xDE, 0x3F, 0x54, 0x7C, 0xFC, 0x9E, 0xE2, 0x2B, 0x1E, 0xCA, 0xEE, 0xCB, 0xC5, + 0xBA, 0xAF, 0xD5, 0x71, 0x36, 0x41, 0x8F, 0x8F, 0x40, 0xBC, 0x1C, 0x1C, 0x14, 0x47, 0xBA, 0xB2, + 0xA2, 0x51, 0xD9, 0xC5, 0x07, 0x56, 0x96, 0x1D, 0x0D, 0x87, 0xC5, 0x71, 0xD5, 0x5B, 0x0B, 0x1D, + 0x23, 0x06, 0xCA, 0xC1, 0xDA, 0x44, 0x0E, 0xCC, 0xFF, 0x2C, 0xEE, 0x8B, 0x78, 0x91, 0xE6, 0x10, + 0x1C, 0x49, 0x1C, 0x72, 0x9E, 0xDF, 0x52, 0x78, 0x09, 0xF4, 0x4B, 0xBF, 0x6B, 0xB0, 0x9D, 0x37, + 0x79, 0xB1, 0xA0, 0x70, 0x7A, 0x15, 0x5F, 0x50, 0xE4, 0x36, 0x70, 0x09, 0xA9, 0x68, 0xC1, 0x06, + 0x2A, 0xDA, 0xB4, 0xF3, 0xBE, 0xDC, 0x9C, 0x5F, 0xA5, 0x15, 0xE5, 0x2F, 0x12, 0x10, 0x3D, 0xDD, + 0xFC, 0x97, 0x9C, 0xDF, 0x58, 0x94, 0x5E, 0x91, 0xE1, 0xFA, 0xD6, 0x7C, 0xDE, 0x22, 0xD5, 0x0E, + 0xB0, 0x82, 0x80, 0xB1, 0x71, 0xE3, 0xC5, 0xF3, 0x8E, 0x30, 0x6F, 0x47, 0xD5, 0x70, 0x08, 0xB5, + 0x04, 0x7B, 0x75, 0x90, 0xC2, 0x5F, 0xA3, 0xA0, 0xB0, 0x35, 0xE1, 0xEA, 0x50, 0x29, 0x0C, 0xFD, + 0xB4, 0x61, 0x43, 0xE0, 0x42, 0xAC, 0x14, 0xF2, 0x00, 0xAB, 0xB4, 0x02, 0x9D, 0x2B, 0x96, 0xBC, + 0xA1, 0x2C, 0x8E, 0x22, 0x00, 0x9E, 0x83, 0x35, 0x72, 0x94, 0x1E, 0x0A, 0xFD, 0x4C, 0xDA, 0xAB, + 0xA0, 0xEE, 0xDE, 0xA0, 0x17, 0x1B, 0x66, 0x8C, 0x9A, 0x67, 0x53, 0x5D, 0x2F, 0x03, 0x4D, 0x13, + 0xC9, 0x12, 0xF6, 0xB4, 0x7A, 0x90, 0xE1, 0x23, 0xDA, 0x77, 0xCD, 0x6A, 0xA2, 0x9E, 0x36, 0x7A, + 0x1A, 0xD9, 0xDF, 0xD7, 0xF5, 0x1B, 0xED, 0xA0, 0x64, 0x25, 0xDE, 0xDF, 0x3F, 0x44, 0x32, 0x89, + 0x9E, 0x48, 0x12, 0x3D, 0x31, 0x24, 0x7A, 0xD2, 0x22, 0xD1, 0xED, 0x37, 0x67, 0x10, 0x30, 0xCF, + 0x5C, 0x3D, 0x80, 0x40, 0x02, 0xD9, 0x28, 0xCE, 0xB2, 0x23, 0x7D, 0x39, 0x06, 0xCC, 0x4C, 0x3A, + 0x9D, 0x33, 0x6B, 0xD5, 0xE0, 0x48, 0x22, 0xE7, 0x72, 0x50, 0x4C, 0x82, 0xA7, 0x02, 0x7F, 0xF8, + 0x5E, 0x0C, 0x65, 0x8F, 0xE2, 0x80, 0xE2, 0xBA, 0x03, 0x34, 0x33, 0xE9, 0x78, 0x57, 0x4A, 0x7C, + 0x2B, 0x03, 0x19, 0x1F, 0xA7, 0x9A, 0x12, 0xEF, 0x9B, 0x6A, 0x74, 0xA6, 0x6B, 0xF2, 0xCF, 0x8A, + 0x39, 0x88, 0xC3, 0x55, 0xC6, 0x9E, 0x73, 0x24, 0x4C, 0xD1, 0x77, 0xDB, 0xE2, 0x9B, 0xC4, 0x37, + 0xCE, 0x52, 0x2C, 0xCC, 0x62, 0x09, 0x25, 0x99, 0x7C, 0x28, 0x00, 0x74, 0xD6, 0x3F, 0x8A, 0xBC, + 0x99, 0x5F, 0x19, 0xCF, 0x25, 0x3C, 0xEF, 0x25, 0xD3, 0xB8, 0xC4, 0x86, 0xC3, 0xAA, 0x9B, 0xEB, + 0x21, 0x56, 0x49, 0xEE, 0xE1, 0x9C, 0xE7, 0xB9, 0x92, 0x1F, 0x65, 0xE0, 0x38, 0x02, 0xB1, 0x5D, + 0x38, 0x49, 0xFC, 0x85, 0xB8, 0x24, 0xE1, 0x05, 0xEE, 0xAA, 0xDB, 0xA3, 0xE7, 0x1A, 0x15, 0x5E, + 0x63, 0x35, 0x51, 0xF7, 0x75, 0x84, 0x3C, 0xD7, 0x01, 0xF4, 0x43, 0x88, 0xBB, 0x93, 0x71, 0x77, + 0x14, 0x47, 0xF9, 0x83, 0xB6, 0xED, 0x78, 0xE7, 0x72, 0x18, 0x0D, 0x65, 0x83, 0x3F, 0x18, 0x80, + 0xF1, 0xBA, 0x30, 0xA7, 0x5B, 0x5B, 0xBB, 0x70, 0x92, 0xD4, 0x09, 0x54, 0xD6, 0x25, 0x43, 0xCA, + 0x2E, 0x19, 0x4A, 0x9E, 0x2B, 0xCC, 0xAE, 0x2B, 0x82, 0xDA, 0xFA, 0x50, 0xAB, 0x41, 0x10, 0x28, + 0xAC, 0xFD, 0x1A, 0x5B, 0x5F, 0x66, 0xCC, 0x19, 0x2C, 0xA3, 0x01, 0xC8, 0x10, 0x72, 0x42, 0x75, + 0x39, 0x99, 0x87, 0x98, 0xEC, 0x39, 0xB1, 0xCB, 0xDC, 0x43, 0x6C, 0xD0, 0x8B, 0xC9, 0x1D, 0x81, + 0xEC, 0x64, 0x11, 0x62, 0x1F, 0x5F, 0xB3, 0xCF, 0x15, 0x9C, 0x79, 0x04, 0x39, 0x90, 0xBB, 0x5C, + 0xF8, 0x4B, 0xB1, 0x96, 0x39, 0xAF, 0xC1, 0x6C, 0x47, 0x28, 0xBC, 0x16, 0x1B, 0x7C, 0xF8, 0x29, + 0xAA, 0x96, 0x09, 0x30, 0x3B, 0xEC, 0x5E, 0x06, 0xF2, 0x63, 0x80, 0x69, 0xCE, 0x8B, 0xB2, 0x9E, + 0x6E, 0x10, 0x22, 0xDE, 0x63, 0xA9, 0x42, 0x31, 0xCE, 0x32, 0x22, 0x36, 0xA4, 0x98, 0x04, 0xEC, + 0xA7, 0x0C, 0xE0, 0x2F, 0x26, 0x08, 0xF5, 0xAD, 0x7B, 0xEB, 0x5B, 0xCB, 0xFA, 0x52, 0xB9, 0x8E, + 0x4B, 0x7C, 0xA1, 0xA2, 0xA3, 0x3B, 0x3E, 0x51, 0xC5, 0x9A, 0xD9, 0x2A, 0xBA, 0xC9, 0xF3, 0x9E, + 0xE2, 0xC4, 0x2D, 0xA4, 0xD1, 0xDD, 0xE4, 0x5B, 0xBE, 0x90, 0x16, 0x96, 0x32, 0x96, 0x7C, 0x4E, + 0x48, 0xEE, 0x28, 0x95, 0xDD, 0x93, 0x3B, 0x7F, 0xCD, 0x03, 0xA6, 0xA3, 0x78, 0xB2, 0xD6, 0x8A, + 0x1E, 0xB4, 0xA5, 0x45, 0x6D, 0xE1, 0x1A, 0xD0, 0x30, 0x51, 0x3D, 0xC0, 0x1D, 0x3F, 0x34, 0x1F, + 0xEA, 0x7B, 0x4C, 0xF8, 0x6C, 0x18, 0xFC, 0xBF, 0x6C, 0xE0, 0xE3, 0x54, 0x42, 0x88, 0x05, 0xEB, + 0x1C, 0x87, 0x89, 0xD6, 0x6A, 0x4C, 0x49, 0x90, 0xCC, 0xF4, 0x02, 0x89, 0x4E, 0xF2, 0xBA, 0x76, + 0x92, 0x20, 0xB6, 0xA3, 0x2D, 0x41, 0x6A, 0x6D, 0xDA, 0x16, 0xC0, 0xCD, 0x3B, 0xC8, 0xE6, 0x74, + 0x0B, 0xA8, 0x23, 0x8A, 0x76, 0xD5, 0x9F, 0x09, 0xF3, 0xF0, 0x2D, 0x16, 0x31, 0x09, 0x74, 0xBE, + 0x79, 0x34, 0xED, 0xDF, 0xE1, 0x83, 0x9C, 0x76, 0x56, 0x5D, 0x93, 0xD4, 0xF2, 0x86, 0xB6, 0x15, + 0x39, 0xC5, 0x08, 0x2C, 0xA0, 0x6D, 0x54, 0xF6, 0x70, 0x25, 0x03, 0x76, 0x7F, 0xA5, 0x91, 0xE4, + 0x4F, 0xB7, 0x33, 0x6C, 0xD6, 0x42, 0xBB, 0x19, 0xAA, 0x9A, 0x43, 0x2D, 0xD5, 0x50, 0x03, 0x74, + 0x00, 0x3D, 0xBD, 0x01, 0x78, 0x06, 0x38, 0x6C, 0x66, 0x06, 0xE3, 0x71, 0x01, 0x29, 0x28, 0x77, + 0xB2, 0xAB, 0x69, 0xE6, 0x06, 0x28, 0x56, 0x2B, 0xB2, 0x03, 0x12, 0xF0, 0x28, 0xE7, 0x30, 0x24, + 0x95, 0x71, 0xBD, 0x34, 0x34, 0xDA, 0xCD, 0x54, 0x45, 0xAE, 0x50, 0x84, 0xF1, 0xC0, 0xF8, 0x18, + 0xA2, 0x1C, 0xD9, 0x0D, 0x0E, 0xE2, 0x24, 0xB5, 0x62, 0xFB, 0x12, 0x77, 0x6F, 0x95, 0x0F, 0x42, + 0xFF, 0x5E, 0xAA, 0x4B, 0xBD, 0x3D, 0xA9, 0xC4, 0x28, 0x0F, 0x9E, 0xCA, 0x2A, 0x27, 0xD8, 0x89, + 0xA1, 0xE7, 0x01, 0xFF, 0x77, 0x6E, 0x9B, 0xC1, 0x95, 0xD4, 0x31, 0x90, 0xD4, 0x4B, 0x5B, 0x65, + 0x49, 0x13, 0x26, 0xD0, 0x49, 0x19, 0x1B, 0xE9, 0x78, 0x13, 0xCB, 0x07, 0x39, 0x2E, 0x73, 0xA9, + 0xBB, 0x66, 0x46, 0xC3, 0x48, 0x3D, 0x34, 0x1F, 0x57, 0x89, 0xF5, 0xF7, 0x66, 0x66, 0xAB, 0xB4, + 0x04, 0x4A, 0x43, 0x70, 0x42, 0x92, 0x7A, 0x94, 0x10, 0x97, 0x51, 0x49, 0xE7, 0xCD, 0xBB, 0x84, + 0x94, 0xAF, 0x51, 0x9F, 0xA8, 0xA4, 0x29, 0x3A, 0xBD, 0x55, 0x62, 0x7F, 0xD6, 0xB3, 0x06, 0xE6, + 0x1E, 0xF3, 0x61, 0xB8, 0x32, 0xDA, 0xD4, 0x23, 0x48, 0x21, 0x14, 0xAE, 0x43, 0xE3, 0x46, 0x47, + 0xA6, 0x25, 0x12, 0xC1, 0xBD, 0xF1, 0xB0, 0x42, 0xA8, 0x1A, 0x67, 0xD9, 0x8F, 0x7C, 0x3F, 0x15, + 0x29, 0x04, 0x58, 0x18, 0x2D, 0xC9, 0x47, 0x55, 0x6D, 0x74, 0xBE, 0xA9, 0xED, 0x81, 0xC4, 0xD8, + 0xD8, 0x4F, 0x11, 0x92, 0xA0, 0xC9, 0x21, 0xEF, 0x4F, 0xA8, 0xE1, 0x1F, 0xA6, 0x06, 0xBE, 0xE7, + 0x61, 0x6E, 0xDB, 0x09, 0x89, 0x28, 0xF4, 0x2A, 0x65, 0x82, 0x5D, 0x25, 0x52, 0xA5, 0xEE, 0xAA, + 0xEC, 0xB1, 0x79, 0xA1, 0x6D, 0x54, 0x2B, 0x75, 0xD0, 0x80, 0x9E, 0xC6, 0x80, 0xFA, 0xF9, 0x39, + 0xF0, 0x84, 0xD3, 0x55, 0xC2, 0x63, 0x72, 0x00, 0x14, 0xF2, 0x0B, 0x8A, 0x7B, 0x7F, 0xE3, 0xB9, + 0x50, 0x40, 0x1A, 0x16, 0x3B, 0x1A, 0x65, 0xA3, 0x96, 0x4C, 0xF9, 0x29, 0x32, 0x33, 0xAC, 0x85, + 0x61, 0x61, 0x3C, 0xB1, 0xEA, 0xB1, 0x20, 0xBC, 0xF5, 0x31, 0xD7, 0x81, 0x99, 0xFB, 0x2D, 0x10, + 0x7D, 0x29, 0x8D, 0x84, 0xD2, 0x55, 0xEF, 0x7A, 0xF2, 0xE1, 0x76, 0x25, 0x6E, 0xB1, 0xAE, 0x0A, + 0xD3, 0x4E, 0x09, 0xC3, 0x3C, 0x77, 0xD6, 0x75, 0x80, 0x35, 0x92, 0xEA, 0x1C, 0xD8, 0x5C, 0xF0, + 0x2C, 0x57, 0x4D, 0x6C, 0x9A, 0x49, 0x6C, 0xFA, 0x32, 0x61, 0xC1, 0x18, 0xEF, 0xB6, 0x89, 0x1A, + 0x2C, 0xD0, 0x69, 0xAE, 0x43, 0x47, 0xC8, 0xF3, 0x1C, 0x4D, 0xE3, 0xD8, 0xC4, 0xA8, 0xA3, 0xC3, + 0x63, 0x9C, 0x95, 0x69, 0xA0, 0xB1, 0xDD, 0x2D, 0x22, 0xAE, 0xF5, 0x87, 0x35, 0x17, 0xB0, 0x54, + 0x80, 0xEC, 0xB0, 0x02, 0x3D, 0x1C, 0x84, 0x10, 0x79, 0x1F, 0xB2, 0x54, 0x7E, 0x7F, 0x1F, 0x5D, + 0x5E, 0x52, 0xBF, 0xD7, 0xD1, 0x8D, 0x58, 0xD0, 0x01, 0x0A, 0xD1, 0xBC, 0xB5, 0x56, 0x27, 0xEB, + 0x6E, 0x14, 0x07, 0x6C, 0xBF, 0x1B, 0x32, 0xF6, 0x91, 0xA6, 0x34, 0x0D, 0x8F, 0x5E, 0x75, 0x3D, + 0x1A, 0xC3, 0x11, 0xE9, 0x42, 0x0F, 0x8C, 0x60, 0x32, 0x05, 0xF0, 0xDC, 0x44, 0xD2, 0xD6, 0x09, + 0xE3, 0x87, 0x7F, 0x9F, 0xD5, 0x80, 0x35, 0x0F, 0xCA, 0x0F, 0x50, 0x1E, 0x2D, 0x20, 0xD9, 0x45, + 0xEC, 0x70, 0xC8, 0x87, 0x15, 0x1A, 0xCD, 0x21, 0xD6, 0x47, 0xD7, 0x48, 0xDC, 0xD4, 0x30, 0x09, + 0x91, 0x95, 0xC0, 0xBE, 0x26, 0x67, 0x20, 0x7D, 0xA9, 0xCD, 0x05, 0xCA, 0xE8, 0x7A, 0x1A, 0x2B, + 0xF3, 0xFC, 0xD2, 0xCF, 0xA9, 0xE0, 0x09, 0x6D, 0xFE, 0xFB, 0x42, 0x93, 0x29, 0x4A, 0x80, 0x4D, + 0x7D, 0x79, 0x17, 0x5D, 0x06, 0xDB, 0x2B, 0x04, 0xD0, 0xF2, 0xA0, 0xA4, 0xC6, 0xF6, 0xF7, 0x37, + 0x07, 0x07, 0x02, 0x13, 0x30, 0x57, 0xD9, 0xE5, 0x21, 0xBA, 0x19, 0x46, 0x2B, 0x12, 0xE3, 0x53, + 0x77, 0x37, 0xDC, 0x23, 0xD3, 0xDC, 0x9D, 0x6C, 0xAE, 0xC4, 0x79, 0xBF, 0x94, 0x74, 0xB3, 0x39, + 0x74, 0x0F, 0x8F, 0x37, 0x81, 0x52, 0x4E, 0x60, 0x3D, 0xE7, 0x2C, 0xB4, 0x5E, 0xF2, 0x5F, 0x9F, + 0x7E, 0xA2, 0x9F, 0xA5, 0x79, 0x0A, 0x39, 0x4D, 0x5B, 0xD2, 0x19, 0xB8, 0x0C, 0xB6, 0xFA, 0x60, + 0x2B, 0xC4, 0x12, 0xE0, 0x4D, 0xD7, 0x8A, 0x48, 0x5E, 0xBC, 0x34, 0xC6, 0x89, 0xE3, 0xE3, 0xCD, + 0xF0, 0xCE, 0x7C, 0x35, 0xEC, 0x36, 0x50, 0x8F, 0x06, 0x66, 0x35, 0x36, 0x71, 0x13, 0xAD, 0x71, + 0xE4, 0xC0, 0xB7, 0x13, 0xC9, 0x38, 0x71, 0x6C, 0x92, 0x19, 0x9F, 0x31, 0x0F, 0x88, 0x92, 0xAD, + 0x75, 0xD5, 0x76, 0x61, 0x0D, 0x07, 0xA2, 0x5D, 0x14, 0x96, 0xC5, 0x51, 0xFD, 0x9E, 0x59, 0x13, + 0x80, 0x0E, 0x5D, 0x96, 0x07, 0x4C, 0x47, 0x2B, 0xD3, 0x0C, 0x99, 0xEE, 0xB8, 0xF7, 0xF0, 0xC7, + 0xC4, 0xB2, 0xA8, 0x11, 0xF0, 0x2C, 0x3D, 0x3D, 0xC6, 0xEC, 0xCF, 0x81, 0x31, 0xF8, 0x8F, 0xD9, + 0x68, 0xD6, 0xCA, 0xD5, 0x7B, 0xF5, 0x02, 0xED, 0x90, 0x5E, 0x21, 0x27, 0x4D, 0xB3, 0xE2, 0xF1, + 0xFF, 0xD8, 0xB8, 0xAC, 0xCA, 0x16, 0x55, 0xCE, 0xC9, 0x93, 0x4F, 0xC7, 0xEA, 0xE4, 0xA1, 0x36, + 0xD8, 0x71, 0x9B, 0x6F, 0x6E, 0x9F, 0xF8, 0xC6, 0x9E, 0x15, 0xF5, 0xB5, 0x45, 0x9F, 0x55, 0xA0, + 0x15, 0x11, 0xCE, 0xDD, 0x7C, 0x50, 0xB6, 0x6D, 0x23, 0x47, 0x61, 0x4F, 0x91, 0x5C, 0x23, 0x24, + 0xC9, 0xDD, 0xE9, 0xC1, 0x6D, 0xD3, 0xE8, 0xF3, 0x91, 0xEB, 0xA7, 0x44, 0x49, 0x58, 0x02, 0x20, + 0xF3, 0xBC, 0x23, 0x13, 0xA7, 0x7E, 0xD2, 0x10, 0xC9, 0x55, 0x97, 0x33, 0x90, 0x32, 0x8A, 0x1B, + 0x27, 0x28, 0x32, 0x41, 0xDD, 0x24, 0x87, 0x22, 0xE9, 0x71, 0x42, 0x8F, 0x0F, 0x8E, 0x48, 0x5F, + 0x62, 0xCF, 0x35, 0xC1, 0x23, 0x2F, 0xB3, 0x37, 0x46, 0x83, 0xFD, 0xA8, 0x2E, 0xC7, 0xE7, 0x9A, + 0x04, 0x4B, 0x05, 0x24, 0x77, 0x10, 0xF4, 0x19, 0xF9, 0x3D, 0x9D, 0x92, 0xB4, 0x21, 0xBA, 0xF7, + 0xF8, 0x0B, 0xEC, 0x41, 0xAD, 0x17, 0x52, 0x40, 0xE5, 0xAF, 0x00, 0xEB, 0x80, 0x20, 0x5C, 0x2A, + 0xF7, 0x0B, 0xB6, 0xAF, 0x02, 0x28, 0x91, 0xBF, 0x05, 0xDD, 0x9B, 0x1D, 0xBD, 0x60, 0x4F, 0x6C, + 0xAE, 0x19, 0x51, 0x74, 0xCA, 0x1D, 0xE2, 0x0B, 0x69, 0xDA, 0x88, 0x48, 0x99, 0xE5, 0xF2, 0x55, + 0xAB, 0x53, 0xD1, 0x35, 0x45, 0x82, 0x33, 0x58, 0xF8, 0xC6, 0xC2, 0x01, 0xD1, 0xB0, 0xD5, 0xDC, + 0x75, 0xC9, 0x6C, 0xFC, 0x80, 0xE1, 0xEB, 0x03, 0x57, 0xC1, 0xE9, 0x48, 0xEB, 0xAF, 0xB0, 0xDF, + 0x70, 0xFE, 0xD7, 0xD2, 0x4A, 0x1E, 0xFA, 0xEA, 0x5F, 0x03, 0x9C, 0x1B, 0x62, 0xC2, 0x96, 0x90, + 0x8A, 0xF2, 0xD3, 0x89, 0x89, 0x19, 0x54, 0x22, 0xAB, 0x9A, 0x85, 0xAC, 0x97, 0x49, 0x7A, 0x71, + 0x59, 0xD5, 0x37, 0xE9, 0x02, 0x1A, 0x5F, 0xD1, 0xA6, 0x23, 0xE5, 0xA1, 0xD6, 0x7F, 0x2F, 0xAD, + 0x12, 0x5C, 0x4D, 0x8F, 0xBC, 0x0B, 0x68, 0xFB, 0x69, 0xD0, 0xBA, 0x8B, 0xB8, 0xBF, 0xFF, 0x21, + 0xE3, 0x62, 0xC1, 0xDC, 0x13, 0x8C, 0xC7, 0x1D, 0x49, 0xCB, 0x08, 0x9F, 0xF6, 0x83, 0xBA, 0x6D, + 0xE1, 0x75, 0x07, 0xDD, 0xCD, 0xAA, 0x46, 0xAD, 0x3F, 0x77, 0x0C, 0x72, 0x7F, 0xFF, 0xFD, 0x52, + 0x41, 0x33, 0x11, 0xE6, 0x46, 0x2A, 0x55, 0x28, 0x97, 0x6C, 0xB7, 0x77, 0xDD, 0x56, 0x9F, 0xB4, + 0xDC, 0xDA, 0x74, 0xEB, 0xB5, 0xE8, 0xA5, 0xDE, 0x15, 0x41, 0x61, 0xFD, 0xBD, 0xB0, 0xAC, 0x67, + 0xD2, 0x9C, 0xEA, 0x7F, 0xCF, 0x42, 0x10, 0x04, 0x59, 0xB2, 0xC5, 0x1D, 0xDB, 0xA3, 0x53, 0xDE, + 0xF2, 0xD1, 0x82, 0xBD, 0x6A, 0xAE, 0x8B, 0x68, 0x61, 0xED, 0xBC, 0x54, 0xD4, 0x14, 0xF4, 0xF0, + 0x2C, 0x92, 0x61, 0x43, 0x92, 0x9E, 0xBA, 0x96, 0x7D, 0xFA, 0x03, 0x85, 0x6D, 0x2C, 0xB2, 0xB0, + 0xE3, 0xE9, 0x85, 0xB2, 0x9F, 0x43, 0x84, 0xBC, 0xDC, 0x8C, 0xCD, 0x80, 0x48, 0x0E, 0x22, 0xCA, + 0xD8, 0x7A, 0x2E, 0x4C, 0x10, 0xB1, 0xD2, 0x78, 0xD1, 0xD8, 0xBF, 0x2E, 0x54, 0x04, 0x9B, 0x68, + 0x5E, 0xB6, 0x4E, 0x92, 0x86, 0x6A, 0xCE, 0x58, 0x57, 0x66, 0x47, 0x5A, 0x7B, 0xC2, 0x93, 0x16, + 0x48, 0x57, 0x2C, 0x56, 0xF2, 0xD3, 0x95, 0x05, 0x31, 0x14, 0xE0, 0x88, 0x3B, 0xE5, 0x1B, 0x54, + 0xA5, 0x9F, 0x69, 0x95, 0xAB, 0x3A, 0xE2, 0xC1, 0x6D, 0x59, 0xB7, 0x86, 0x6F, 0x76, 0x79, 0x99, + 0x43, 0x37, 0x7A, 0xA4, 0xF3, 0x6D, 0x97, 0x08, 0x89, 0x74, 0xBD, 0x9A, 0xA9, 0x9A, 0x5D, 0x63, + 0xBB, 0x77, 0x6A, 0xAA, 0x25, 0xE6, 0x6D, 0x20, 0x7A, 0x2B, 0x2F, 0x7C, 0x4E, 0xAD, 0xFE, 0x62, + 0xAB, 0xA0, 0x66, 0x0C, 0xFB, 0xC9, 0xFE, 0x1A, 0x5E, 0xA1, 0x8E, 0xFD, 0x69, 0x7C, 0xF0, 0x8F, + 0x19, 0xFC, 0x20, 0x3D, 0x39, 0x3B, 0x7C, 0x16, 0xB2, 0xBB, 0xB9, 0x0A, 0x57, 0x78, 0x32, 0x5C, + 0xE0, 0x79, 0x1C, 0x4C, 0x9B, 0xDF, 0x67, 0x4F, 0x26, 0xCF, 0x70, 0x0D, 0xE9, 0x18, 0x79, 0xC7, + 0xCF, 0x6A, 0xF2, 0x3F, 0x65, 0x7B, 0xF5, 0x93, 0xAF, 0xFC, 0xEA, 0x68, 0xC6, 0x86, 0xBC, 0xBD, + 0x2A, 0xA7, 0xC2, 0x49, 0x03, 0xC4, 0xAD, 0x09, 0x48, 0xC3, 0x0B, 0x33, 0x59, 0xC5, 0x42, 0xB7, + 0x69, 0x05, 0x21, 0xB6, 0xF4, 0x0E, 0x4E, 0x39, 0x53, 0x45, 0x3A, 0x6E, 0x78, 0x32, 0x5B, 0xB4, + 0x97, 0x7F, 0x4E, 0x95, 0x97, 0x4E, 0xDB, 0xEA, 0xA9, 0x62, 0x19, 0x28, 0x8E, 0x64, 0x11, 0xFA, + 0x7B, 0x07, 0xB8, 0x55, 0x38, 0xF6, 0xCC, 0xB2, 0x91, 0x10, 0x02, 0x1E, 0xED, 0xA0, 0x36, 0xF6, + 0xA1, 0x14, 0x97, 0x97, 0xCE, 0xD8, 0xED, 0x85, 0xA5, 0xB0, 0x1A, 0xF7, 0xE1, 0x27, 0xBC, 0xED, + 0x3A, 0x06, 0xDC, 0x7E, 0x41, 0x76, 0xFB, 0xD3, 0x82, 0xAE, 0xDE, 0x86, 0xCD, 0x2C, 0x68, 0x5C, + 0xBB, 0x67, 0xEE, 0x9B, 0x8B, 0x26, 0x80, 0xB1, 0x93, 0x66, 0xC7, 0xBB, 0xB1, 0x7F, 0xCF, 0xC6, + 0x97, 0xBD, 0xB7, 0xF9, 0x1B, 0x7E, 0x9B, 0x53, 0xFE, 0x22, 0xBC, 0xD3, 0x99, 0xD0, 0x24, 0xD8, + 0xE1, 0x34, 0x1A, 0xB5, 0xE9, 0x99, 0x33, 0x0D, 0xC8, 0x0D, 0x51, 0x81, 0x9E, 0xAD, 0xC0, 0xC1, + 0x57, 0x92, 0x83, 0xB7, 0x9B, 0xDD, 0x4F, 0xC9, 0x6F, 0x30, 0x55, 0xE4, 0x48, 0x33, 0x41, 0xAE, + 0x4A, 0xC6, 0xBF, 0xD5, 0x00, 0x49, 0xC1, 0xDC, 0x9A, 0xE4, 0xBC, 0x60, 0x3E, 0xB8, 0x12, 0x2B, + 0x48, 0x1B, 0x1F, 0x17, 0x13, 0x17, 0x2D, 0xF9, 0x19, 0x20, 0x49, 0x5B, 0xE9, 0x35, 0xC6, 0xDE, + 0x3F, 0x90, 0x9F, 0x94, 0x83, 0x71, 0x26, 0x27, 0x07, 0x63, 0x9A, 0x42, 0xAC, 0xE7, 0x2F, 0x2E, + 0x78, 0x48, 0x05, 0xD3, 0xB2, 0x4F, 0xCD, 0x36, 0x50, 0x39, 0xD5, 0xDC, 0x36, 0xA8, 0xDC, 0x77, + 0x86, 0x8A, 0xA3, 0x09, 0x0D, 0x4D, 0x2F, 0xAC, 0xBE, 0x9D, 0x91, 0xDF, 0x0B, 0xF1, 0xB3, 0xF4, + 0xE3, 0x06, 0xF7, 0x6E, 0xFE, 0x31, 0x3C, 0xB7, 0x9D, 0xFD, 0x6D, 0x36, 0x7C, 0x16, 0x4C, 0xDF, + 0x3E, 0x9B, 0x3D, 0xAE, 0x7F, 0xED, 0xB8, 0x72, 0x3B, 0xF2, 0x8D, 0x77, 0xFB, 0x68, 0x87, 0x9C, + 0x9D, 0x8F, 0x97, 0xC6, 0xB2, 0x1A, 0x82, 0xFA, 0x45, 0x4F, 0x1F, 0x15, 0xC5, 0x5B, 0xE0, 0x8C, + 0x46, 0x2C, 0x1B, 0x4C, 0xE2, 0xC4, 0x7C, 0x26, 0xC3, 0xD6, 0xBB, 0x19, 0x22, 0x3F, 0x3A, 0x8E, + 0xF4, 0xF7, 0x64, 0x2A, 0x85, 0x3C, 0x82, 0x7E, 0x66, 0xE1, 0xCF, 0xCC, 0x86, 0x4B, 0x57, 0xC7, + 0x83, 0x62, 0x4A, 0x99, 0x2B, 0x0D, 0x01, 0x15, 0x59, 0xAD, 0x49, 0x6F, 0xF7, 0x13, 0x76, 0x52, + 0x11, 0xA8, 0x85, 0x0E, 0xC2, 0x8E, 0xA7, 0xEF, 0xCA, 0xA4, 0x51, 0x97, 0xD9, 0xA8, 0x47, 0xAA, + 0xE8, 0x71, 0xAD, 0x15, 0xB9, 0xE2, 0x6C, 0xCE, 0x2E, 0x5B, 0x27, 0xB4, 0x49, 0xC3, 0x4A, 0xB8, + 0x2E, 0xE9, 0xF1, 0x01, 0x92, 0xAC, 0x4C, 0x88, 0xDC, 0xE0, 0x92, 0x82, 0xD4, 0xDE, 0x76, 0x67, + 0x76, 0x6F, 0xEF, 0x84, 0x2F, 0x79, 0x55, 0xC5, 0xD7, 0x72, 0x89, 0x94, 0x71, 0x54, 0xCB, 0xB1, + 0x35, 0xBA, 0x64, 0xDF, 0x57, 0xA8, 0x82, 0x2B, 0x6A, 0x8B, 0xAF, 0x87, 0x4C, 0x54, 0xC0, 0xAF, + 0xF0, 0x47, 0x0E, 0x45, 0xDA, 0x21, 0x16, 0x2C, 0x09, 0x6E, 0x38, 0x47, 0xD7, 0x44, 0x6E, 0x1A, + 0xBD, 0x6C, 0x5F, 0x41, 0x27, 0x8D, 0x00, 0x9D, 0xE8, 0x5C, 0x2F, 0x71, 0x35, 0xA9, 0x70, 0x36, + 0x6E, 0x04, 0x9A, 0xDA, 0xA9, 0x60, 0xCF, 0xB9, 0x00, 0x66, 0x0A, 0x24, 0xCD, 0x02, 0x72, 0xFB, + 0x85, 0x57, 0x04, 0x72, 0xF6, 0xD4, 0x1C, 0x15, 0xE4, 0x1B, 0x7B, 0xA2, 0x7E, 0x19, 0x12, 0xFD, + 0x53, 0xC2, 0x8D, 0xC6, 0x77, 0xB2, 0x2F, 0xEB, 0x01, 0xE9, 0xE2, 0x48, 0xAD, 0x08, 0xD6, 0xE0, + 0xB6, 0xF5, 0xD4, 0x7F, 0x29, 0xC1, 0xF4, 0x4B, 0x09, 0xA4, 0x92, 0x06, 0x2F, 0x6B, 0x32, 0xD1, + 0xC3, 0xE7, 0x5F, 0xB2, 0x2A, 0x5D, 0xD5, 0x7C, 0x95, 0xF9, 0x89, 0x80, 0x2F, 0x38, 0xB6, 0x29, + 0x43, 0x0E, 0xD6, 0xBB, 0x49, 0xF3, 0x8F, 0x92, 0xC2, 0x74, 0x72, 0xD2, 0x2F, 0x15, 0x6B, 0xBE, + 0x81, 0xF1, 0x8D, 0x3C, 0x3C, 0xDA, 0x47, 0xFB, 0xB8, 0x79, 0xB4, 0x5B, 0xAD, 0x5F, 0x03, 0x07, + 0xC2, 0xD6, 0xA0, 0xEF, 0xB9, 0x02, 0x35, 0x20, 0x57, 0x5C, 0xE2, 0xAE, 0xC9, 0x0E, 0xDC, 0x46, + 0x85, 0x13, 0x60, 0xA5, 0x04, 0x8A, 0xB8, 0xA4, 0x83, 0xDF, 0xE4, 0x8B, 0x06, 0x50, 0xF4, 0xB9, + 0xF8, 0x6D, 0x2B, 0xE6, 0xAB, 0xBC, 0x04, 0x08, 0xED, 0x72, 0x8F, 0xAD, 0xD0, 0xAF, 0xEA, 0x84, + 0x96, 0xF0, 0x74, 0x31, 0xB1, 0xA4, 0x51, 0x78, 0xFB, 0x6A, 0xB4, 0x21, 0x81, 0xCF, 0x28, 0x1A, + 0x2C, 0x22, 0x25, 0xEF, 0xCF, 0x64, 0xF1, 0x41, 0x04, 0xC7, 0x51, 0xEB, 0xCA, 0x13, 0xED, 0x76, + 0x6B, 0x33, 0x30, 0x1E, 0x93, 0x04, 0x8F, 0x8C, 0x4E, 0x63, 0x69, 0x88, 0x42, 0x08, 0xB4, 0x7D, + 0xAF, 0xBE, 0xF7, 0xDC, 0xE2, 0x23, 0x93, 0xF8, 0xBC, 0x5C, 0xAB, 0x82, 0x15, 0xD7, 0xD6, 0x8F, + 0x2F, 0xC7, 0xC7, 0x9A, 0x4D, 0x6D, 0xE2, 0xEB, 0x1C, 0x22, 0x28, 0x4C, 0x12, 0xB7, 0xDD, 0x6B, + 0x53, 0x31, 0xE9, 0x71, 0xBA, 0xAD, 0xCE, 0x71, 0x9A, 0x10, 0xA1, 0x20, 0x3D, 0x08, 0x65, 0xA4, + 0x42, 0xCB, 0x1A, 0xA7, 0x24, 0x84, 0x0B, 0x14, 0x32, 0x61, 0xDC, 0xA5, 0x02, 0xCE, 0x8C, 0xF0, + 0x5E, 0x65, 0x5E, 0x82, 0xB8, 0x6E, 0x02, 0x3F, 0xBA, 0xDC, 0xAA, 0x71, 0x70, 0x48, 0x0E, 0x81, + 0xE1, 0x59, 0xA6, 0xB5, 0x7C, 0xFD, 0x83, 0x6C, 0x8C, 0xCC, 0xC5, 0x3F, 0xB4, 0xB1, 0xC1, 0x42, + 0x9E, 0x72, 0x61, 0xFC, 0xC3, 0x55, 0x7E, 0x8A, 0x22, 0x3B, 0x4F, 0x1A, 0xA4, 0xFB, 0xFA, 0x99, + 0x8E, 0xD6, 0x23, 0x08, 0x61, 0xEB, 0x5B, 0x03, 0x6B, 0x42, 0x5A, 0x10, 0xE3, 0x80, 0xFA, 0x5E, + 0x8E, 0xB0, 0xD7, 0xE0, 0xA2, 0x7B, 0x09, 0x54, 0x5F, 0xEC, 0x72, 0x71, 0xA2, 0xBA, 0x56, 0xA4, + 0xF6, 0x73, 0x5F, 0x4F, 0x2F, 0x31, 0x1E, 0x57, 0x83, 0x1C, 0x98, 0xDC, 0xBC, 0xEB, 0x5B, 0xBA, + 0xDE, 0x9D, 0xC5, 0x90, 0xB4, 0xE5, 0xFD, 0xDF, 0x6D, 0x43, 0xEE, 0xFE, 0x86, 0xB5, 0x2C, 0xF2, + 0xD2, 0xD8, 0x77, 0xE5, 0xED, 0x98, 0x06, 0xAB, 0xBA, 0xB1, 0xA8, 0xBD, 0x43, 0xD8, 0x51, 0xFD, + 0xAE, 0xEC, 0x0F, 0xB4, 0xF0, 0xFE, 0x31, 0xBB, 0xED, 0x20, 0x4D, 0xB6, 0xF4, 0xFE, 0x62, 0xBA, + 0xBD, 0x46, 0xD1, 0x52, 0x7E, 0xF4, 0xAE, 0xCB, 0x1B, 0xBF, 0xE1, 0x5E, 0x90, 0x1E, 0x9C, 0x08, + 0x1C, 0x6E, 0x99, 0x75, 0x48, 0x1A, 0x0F, 0xF7, 0x97, 0x77, 0x99, 0x6B, 0x64, 0x36, 0x88, 0x7A, + 0xF7, 0xBB, 0x47, 0x23, 0x95, 0xE7, 0x85, 0xB9, 0x29, 0x58, 0xF8, 0x9D, 0xC8, 0x60, 0xD2, 0x89, + 0x0A, 0xFD, 0xE7, 0x34, 0xC6, 0x0A, 0x46, 0x48, 0x2B, 0x08, 0x47, 0x3C, 0xD6, 0xED, 0xD9, 0xEA, + 0x48, 0xE4, 0xA7, 0x8F, 0x72, 0xE9, 0xD3, 0xCB, 0x1A, 0x25, 0x13, 0xBC, 0x5B, 0xBA, 0x98, 0x85, + 0xD0, 0x84, 0xFB, 0x81, 0xFD, 0x7A, 0x7D, 0xD0, 0x3A, 0x6F, 0xC3, 0xA4, 0xF6, 0x55, 0x1A, 0x8F, + 0xE7, 0xDF, 0x23, 0x7E, 0x40, 0x1B, 0xB7, 0x7F, 0x2C, 0x2F, 0x92, 0xA0, 0x61, 0x8C, 0xA2, 0x83, + 0x7E, 0x2A, 0xD6, 0x5A, 0x18, 0xC6, 0x81, 0x56, 0x8B, 0xF4, 0x4E, 0x0E, 0x12, 0x47, 0x86, 0x2F, + 0xF8, 0x9D, 0xA5, 0x16, 0x65, 0x2A, 0xBE, 0x54, 0xC4, 0x04, 0x3B, 0x74, 0xC1, 0xA2, 0x26, 0x05, + 0x8B, 0x17, 0x44, 0x13, 0x8D, 0x20, 0xA7, 0xA6, 0xFC, 0xBE, 0x85, 0x67, 0xDE, 0xB7, 0x0D, 0x5E, + 0x6C, 0xF8, 0xE4, 0xC2, 0x9E, 0x8C, 0xAF, 0x5D, 0x44, 0x69, 0x35, 0x97, 0x5F, 0x99, 0x5B, 0x7C, + 0x6E, 0xEC, 0xAB, 0x96, 0x0C, 0x96, 0xDD, 0x0F, 0x26, 0x74, 0xBB, 0x83, 0x6F, 0xF0, 0xC2, 0xAF, + 0x6E, 0x5A, 0x26, 0x81, 0xC6, 0xAD, 0xEC, 0x51, 0x2C, 0x4B, 0x98, 0xE8, 0x8A, 0xD3, 0x15, 0x1D, + 0x0B, 0x26, 0x2F, 0x9E, 0x62, 0xCB, 0x74, 0x46, 0xAD, 0x4A, 0xD4, 0x0F, 0x7C, 0x49, 0xAA, 0x82, + 0x6E, 0x77, 0x6A, 0xA5, 0x50, 0xE0, 0xF8, 0x35, 0xCC, 0x3A, 0xB9, 0x30, 0xD4, 0xD3, 0xD1, 0x09, + 0x2A, 0x3A, 0xC7, 0xC8, 0x9D, 0xCB, 0x9F, 0xAA, 0x97, 0x09, 0x3D, 0xAB, 0x11, 0x75, 0xD6, 0xA0, + 0x98, 0x90, 0x2C, 0x91, 0x5F, 0x0C, 0xD3, 0xF8, 0x4E, 0x5D, 0xA0, 0xF5, 0xBF, 0x65, 0x99, 0x6B, + 0xDB, 0x5B, 0x3E, 0x3D, 0x28, 0x42, 0xC7, 0x31, 0x75, 0xD5, 0x3E, 0x25, 0xB2, 0xC5, 0x64, 0x1C, + 0x29, 0xED, 0x6E, 0x47, 0xE9, 0x7F, 0x30, 0x86, 0x0C, 0xBA, 0x49, 0x00, 0x90, 0xAE, 0xAB, 0xAE, + 0x8B, 0x11, 0x3D, 0x7C, 0x41, 0x5A, 0x74, 0x54, 0x79, 0xB4, 0xD1, 0xE4, 0x03, 0x95, 0xC0, 0xB4, + 0x47, 0x1B, 0x25, 0xC1, 0xD5, 0x5A, 0x84, 0xE1, 0x70, 0x75, 0xAC, 0x21, 0x23, 0x18, 0x8C, 0xF9, + 0x6E, 0xF1, 0x4A, 0x6B, 0x43, 0x89, 0x56, 0x15, 0x95, 0x24, 0x2E, 0x01, 0x7F, 0xF0, 0x70, 0xF6, + 0x4D, 0xF6, 0x59, 0x0C, 0x3D, 0x2A, 0xBB, 0xC6, 0xD7, 0xC5, 0xC0, 0x0A, 0x0D, 0xE8, 0xA6, 0x22, + 0xB6, 0xC1, 0x55, 0x5E, 0x90, 0xF6, 0x9F, 0x23, 0xD4, 0x7B, 0x2A, 0xC8, 0x8A, 0xE7, 0xDA, 0xC8, + 0x0C, 0xC0, 0xF3, 0x58, 0xA7, 0x74, 0x1F, 0xF7, 0x3E, 0xFC, 0x53, 0x22, 0x23, 0xD9, 0x33, 0xA6, + 0x8D, 0xBA, 0x31, 0xCC, 0x8D, 0xD6, 0x54, 0x5A, 0x3B, 0xD8, 0x3D, 0xDE, 0xE1, 0x7A, 0x6A, 0x5B, + 0xB3, 0x79, 0x85, 0xAC, 0x93, 0x42, 0xC2, 0x36, 0xB9, 0xCC, 0x21, 0x83, 0x4E, 0xF6, 0x78, 0x5A, + 0xEA, 0x8A, 0xF8, 0x81, 0x10, 0xD5, 0x80, 0xDD, 0x42, 0xA4, 0x8E, 0x46, 0x22, 0x80, 0x1F, 0x7F, + 0x41, 0x1A, 0xF9, 0xF6, 0x1D, 0x25, 0xA1, 0x3A, 0x36, 0xD7, 0xDB, 0x61, 0x2B, 0xA4, 0x5F, 0xCB, + 0x9E, 0x71, 0xC8, 0x5E, 0xD9, 0xC2, 0xA2, 0x67, 0xB3, 0xAB, 0xB9, 0x07, 0x5D, 0xC3, 0x7B, 0x55, + 0x3D, 0xC7, 0x81, 0xCD, 0xCA, 0xDA, 0xDB, 0xA0, 0x34, 0x97, 0x40, 0xC5, 0x98, 0xB4, 0xB6, 0x11, + 0x99, 0xF5, 0x1C, 0x1C, 0x6C, 0x75, 0xDB, 0x18, 0x50, 0x3F, 0x31, 0x82, 0x0A, 0x4D, 0x6D, 0xAC, + 0xCC, 0x0B, 0x0F, 0xCD, 0xE2, 0x2A, 0x33, 0xCE, 0x1D, 0x33, 0x4F, 0xA0, 0xA5, 0xAB, 0x57, 0xB2, + 0xC0, 0x9E, 0x9C, 0x71, 0xC4, 0xD0, 0x87, 0xF5, 0x84, 0xC1, 0x57, 0x33, 0x77, 0xCF, 0x92, 0x0E, + 0x90, 0x0A, 0xA3, 0xDE, 0x77, 0xBB, 0x2B, 0x22, 0xCD, 0x18, 0xC0, 0x45, 0xD6, 0xC7, 0xCD, 0xAB, + 0x22, 0xBD, 0xD5, 0x0D, 0x62, 0xB6, 0x58, 0x4C, 0xFE, 0x86, 0x57, 0x08, 0xFB, 0x09, 0x1E, 0x80, + 0xBF, 0xB4, 0xEF, 0xC7, 0x5F, 0x7E, 0x3B, 0x45, 0x69, 0x33, 0x27, 0xDA, 0x86, 0x18, 0x4B, 0x3F, + 0xB3, 0x00, 0x25, 0xD0, 0xB8, 0x59, 0x52, 0xAE, 0xBB, 0x6F, 0xD0, 0xCB, 0x91, 0x6E, 0xB4, 0xFD, + 0xBC, 0x96, 0x2D, 0xD7, 0xDF, 0xDF, 0x7C, 0xBB, 0x35, 0xAE, 0xC7, 0xDC, 0x37, 0x82, 0x5E, 0xD0, + 0x5B, 0x0C, 0x45, 0x8F, 0x55, 0x30, 0xC8, 0xEB, 0xA9, 0x07, 0x0E, 0x3F, 0x5D, 0xDE, 0x79, 0x74, + 0xB0, 0xE6, 0x17, 0x74, 0x7D, 0xD9, 0x13, 0x0E, 0x0E, 0xF2, 0x3D, 0xB9, 0xCB, 0xBC, 0x60, 0x47, + 0x2C, 0xEE, 0xC7, 0x4C, 0x3D, 0x14, 0xCB, 0x57, 0xD7, 0x24, 0x8F, 0x26, 0x34, 0xD9, 0xAA, 0x80, + 0xF0, 0xC3, 0x9E, 0xCE, 0xFF, 0x50, 0xD2, 0xA1, 0xD0, 0x15, 0x2D, 0x3C, 0x59, 0x2B, 0x11, 0x74, + 0xA8, 0x94, 0x70, 0xEE, 0xBF, 0x5B, 0xE9, 0x58, 0xA8, 0x7A, 0xA8, 0x52, 0x92, 0x8F, 0x7A, 0xE4, + 0x20, 0x86, 0x09, 0x07, 0xBC, 0x2A, 0x05, 0x46, 0xB9, 0xEA, 0x5B, 0x04, 0x32, 0x3F, 0x59, 0xDD, + 0xC4, 0x77, 0x65, 0x1F, 0xFC, 0xCA, 0xB3, 0xC0, 0xAC, 0x8B, 0x3A, 0x13, 0x3A, 0xEB, 0xE4, 0x31, + 0xA2, 0xF7, 0x7A, 0x2F, 0x5D, 0xF0, 0xA9, 0xA1, 0xAC, 0x3F, 0x88, 0xF2, 0x49, 0xD7, 0x4E, 0x2F, + 0xD4, 0x91, 0x64, 0xDF, 0xB1, 0x3A, 0x32, 0x7B, 0x5D, 0x2F, 0xA5, 0x6F, 0x8F, 0x06, 0x83, 0x98, + 0xF2, 0xBE, 0xAD, 0x1F, 0x5D, 0x91, 0xA4, 0x09, 0x37, 0x46, 0x09, 0xA7, 0xAA, 0xD0, 0x51, 0x39, + 0x25, 0x24, 0x3B, 0xF3, 0xDB, 0x2F, 0x7E, 0xF1, 0x63, 0x64, 0xFD, 0x4F, 0xBB, 0x1D, 0xF1, 0x79, + 0xE7, 0x9C, 0x8C, 0x26, 0x08, 0xE6, 0x40, 0x43, 0x8E, 0x5F, 0x8C, 0x24, 0x34, 0xC9, 0xE3, 0x12, + 0x9F, 0x6A, 0x3D, 0xE5, 0x14, 0xF1, 0x37, 0x3F, 0x4D, 0x09, 0x3B, 0x06, 0xC2, 0xF5, 0x43, 0x8F, + 0xC0, 0xDC, 0x9B, 0xC9, 0xC6, 0x32, 0x7E, 0x3E, 0xC8, 0x36, 0xB9, 0xC5, 0xBF, 0x58, 0x2F, 0x29, + 0xAC, 0xB7, 0xAD, 0x6D, 0x31, 0xBD, 0x2E, 0xA1, 0xE6, 0x9C, 0xE2, 0x50, 0xB5, 0x5B, 0x72, 0x60, + 0xE5, 0x2B, 0xC5, 0x6A, 0xF7, 0x52, 0x87, 0x9E, 0x1B, 0x6E, 0xB6, 0xB0, 0xF3, 0x2C, 0x92, 0x9E, + 0x47, 0xD0, 0x98, 0xC9, 0xF4, 0xD3, 0xE3, 0x8D, 0xD4, 0xE2, 0xD1, 0xFB, 0x62, 0x56, 0x3D, 0xC7, + 0x97, 0x17, 0x72, 0xDB, 0x39, 0xE7, 0x71, 0x53, 0x62, 0x1B, 0x94, 0xCB, 0x80, 0x37, 0x97, 0xD2, + 0x6C, 0x9F, 0xDC, 0x88, 0x2D, 0x0F, 0x78, 0x4E, 0x36, 0xD4, 0x08, 0xF9, 0x62, 0x62, 0x17, 0xA6, + 0x1F, 0xFA, 0xF0, 0x23, 0xD6, 0x50, 0x12, 0x1C, 0x82, 0x4F, 0x9D, 0x72, 0x52, 0x69, 0xB2, 0x63, + 0xE5, 0x6F, 0x30, 0xDA, 0xD7, 0x18, 0xAD, 0x0A, 0x7E, 0x85, 0x60, 0x10, 0x42, 0xBF, 0x3E, 0x14, + 0x0F, 0x67, 0x32, 0xB1, 0xB9, 0x5A, 0x3C, 0x5A, 0x93, 0x80, 0xCA, 0xD2, 0xB3, 0x68, 0xAF, 0x99, + 0xBE, 0x53, 0x14, 0x4A, 0x11, 0x11, 0x8D, 0x22, 0xFC, 0x12, 0x36, 0x03, 0x7A, 0x69, 0x39, 0xB7, + 0x9C, 0x8A, 0x2D, 0x96, 0x26, 0x2A, 0x27, 0x0D, 0x48, 0x66, 0xB2, 0xCA, 0x77, 0x88, 0x1E, 0x0B, + 0xC3, 0xC0, 0x4F, 0xF3, 0x84, 0xEF, 0x16, 0x7C, 0x91, 0xE7, 0x30, 0x03, 0xDD, 0x95, 0x42, 0xD0, + 0x3C, 0x2A, 0x89, 0x16, 0x7C, 0x53, 0xC4, 0x73, 0xD0, 0xA1, 0x9B, 0x63, 0x5C, 0xE2, 0x04, 0xAB, + 0xCE, 0x1D, 0xFC, 0xAA, 0xA7, 0x83, 0xB9, 0x82, 0x33, 0xEA, 0x9B, 0xEE, 0xDA, 0x51, 0x3A, 0xA9, + 0x48, 0x53, 0x64, 0x5B, 0x21, 0x36, 0x94, 0x49, 0x4C, 0xD9, 0xBC, 0xEF, 0x36, 0x12, 0xED, 0xC8, + 0x46, 0xD8, 0xFB, 0x84, 0x54, 0x71, 0x6F, 0xD2, 0xAB, 0x24, 0xDF, 0x40, 0xE6, 0x45, 0x75, 0xEF, + 0xDE, 0x9E, 0x34, 0x62, 0xD2, 0x0E, 0xE3, 0xF2, 0x36, 0x33, 0xB1, 0x2B, 0xD8, 0x60, 0x25, 0x58, + 0xBC, 0x02, 0x24, 0x43, 0xF8, 0x5A, 0x24, 0x8D, 0x29, 0x17, 0xD0, 0xAD, 0x37, 0x73, 0xCA, 0x65, + 0xAE, 0xC2, 0xD7, 0x9C, 0xF8, 0xB4, 0x9D, 0x28, 0x9F, 0x92, 0xFB, 0x2A, 0x08, 0x9A, 0xFB, 0x43, + 0x05, 0x1F, 0x60, 0x4F, 0x26, 0xFA, 0x70, 0x20, 0x2B, 0x42, 0x79, 0xF1, 0x0B, 0x3E, 0x0D, 0xCC, + 0xE9, 0xF1, 0x20, 0x42, 0xA9, 0xE8, 0x4A, 0x64, 0x81, 0x9F, 0x8F, 0x67, 0x47, 0xB1, 0x44, 0x24, + 0x11, 0xBB, 0x9B, 0x15, 0xB0, 0x50, 0xE0, 0x80, 0x8B, 0x57, 0x52, 0x28, 0x16, 0xD0, 0xF5, 0x8F, + 0x0E, 0x92, 0x19, 0xCA, 0x69, 0x7F, 0x5C, 0x3A, 0xE6, 0x23, 0x37, 0xE6, 0x90, 0x73, 0xD0, 0x71, + 0x2C, 0x3F, 0x3E, 0x92, 0x1F, 0x81, 0x90, 0x95, 0x56, 0x88, 0xE0, 0xA3, 0x11, 0x8A, 0x3B, 0xC2, + 0x1E, 0x68, 0xB6, 0x07, 0x37, 0x77, 0xF1, 0x0A, 0x91, 0x98, 0x4A, 0xBC, 0x17, 0xF6, 0x1F, 0xA8, + 0x8D, 0x42, 0x18, 0x8C, 0x39, 0x80, 0xC9, 0xCA, 0xC9, 0x4C, 0x2C, 0xB2, 0x93, 0x72, 0x49, 0xEE, + 0xA5, 0x92, 0x36, 0x0E, 0x8A, 0xDE, 0x10, 0x2A, 0xEA, 0x7A, 0x04, 0x35, 0x4D, 0x58, 0x92, 0x95, + 0xC4, 0x5B, 0x8A, 0xEC, 0x22, 0xAC, 0xA6, 0x1E, 0x1B, 0x75, 0xFA, 0x92, 0x47, 0x0E, 0xFC, 0x90, + 0x79, 0xD5, 0xFB, 0xAE, 0xA5, 0x11, 0x15, 0x2F, 0xF9, 0x37, 0x0A, 0x8D, 0x8F, 0xDB, 0x8D, 0x4E, + 0x3A, 0x6D, 0xC0, 0xF1, 0xD5, 0xC1, 0x41, 0xD6, 0xDA, 0xBC, 0xCC, 0x2A, 0x6E, 0xB7, 0x2C, 0xF8, + 0x3E, 0x8E, 0x68, 0x5B, 0x11, 0x6F, 0x94, 0xAB, 0x43, 0x8E, 0x00, 0x5C, 0xE7, 0x36, 0xFB, 0x4A, + 0x0C, 0xC0, 0xBE, 0x99, 0x73, 0x94, 0x31, 0x20, 0x1F, 0xA4, 0x64, 0x9B, 0xC4, 0xE7, 0x0D, 0x44, + 0x4B, 0xF4, 0x97, 0x51, 0x95, 0x35, 0x6B, 0xC8, 0xF9, 0xDB, 0xB0, 0x02, 0x15, 0x4C, 0x33, 0x5E, + 0x49, 0x3D, 0x08, 0x35, 0x63, 0x6A, 0x37, 0xC2, 0x4B, 0x8B, 0x57, 0x0D, 0x0B, 0xF8, 0x37, 0x92, + 0xAA, 0xD2, 0x7B, 0x7C, 0xF5, 0xAB, 0x0C, 0xFC, 0x66, 0x86, 0xC0, 0x6B, 0xDC, 0x2E, 0x4B, 0xEA, + 0xD7, 0x34, 0x73, 0x09, 0x4E, 0xFE, 0x5A, 0xFA, 0x67, 0xA9, 0xD9, 0xB6, 0x1D, 0x4E, 0xA2, 0x03, + 0xC6, 0xC1, 0xD0, 0x06, 0xEC, 0x42, 0x2F, 0x2D, 0xEE, 0xF8, 0x84, 0xA5, 0xEC, 0x39, 0x79, 0x59, + 0x31, 0xC1, 0xD1, 0x4D, 0x5C, 0xE0, 0x68, 0x44, 0xD4, 0xDF, 0xCC, 0x5D, 0x1E, 0xBA, 0xC7, 0xD3, + 0xCE, 0xA2, 0x9F, 0xB2, 0x36, 0x2D, 0xED, 0x99, 0x96, 0xF8, 0xAA, 0xE8, 0xE8, 0x0A, 0x87, 0x64, + 0x7C, 0x81, 0xFB, 0xA0, 0x12, 0xD5, 0xB0, 0x9F, 0x9E, 0x53, 0x29, 0x68, 0x7E, 0xA9, 0x73, 0x36, + 0x9C, 0xAC, 0x34, 0x70, 0x8D, 0x49, 0xB0, 0xFC, 0x6C, 0xB0, 0xE5, 0x69, 0xF9, 0xAC, 0x01, 0x3B, + 0xF6, 0x0C, 0xFC, 0x14, 0x59, 0x5F, 0x2A, 0x9F, 0xF8, 0x2D, 0x6F, 0xD0, 0x2F, 0xBE, 0xF9, 0x4A, + 0xDD, 0x37, 0xFC, 0x12, 0x6E, 0x9E, 0xC9, 0xC3, 0xDD, 0xA7, 0x40, 0x6D, 0xFD, 0x79, 0x57, 0xC8, + 0x41, 0xE9, 0xBA, 0xAF, 0xBE, 0x7C, 0xA5, 0x57, 0x7E, 0xF4, 0xBD, 0xFE, 0xFC, 0x19, 0xAD, 0x34, + 0x45, 0x4C, 0x15, 0x11, 0x34, 0x6B, 0x60, 0x44, 0x77, 0xC8, 0x2A, 0xCE, 0x30, 0x1B, 0x96, 0x6C, + 0x35, 0xAF, 0x54, 0x8E, 0x05, 0x67, 0xFF, 0x5B, 0x8C, 0xFB, 0x24, 0x2A, 0xDC, 0xD8, 0x73, 0xBE, + 0x34, 0x0C, 0x98, 0x1C, 0x1C, 0xA0, 0x62, 0x93, 0xF3, 0x74, 0xA4, 0x2A, 0x20, 0xDB, 0x46, 0xF3, + 0x01, 0x26, 0x95, 0xB4, 0x9D, 0x6C, 0xFC, 0xD5, 0xC8, 0x5F, 0xD7, 0x9F, 0x35, 0x76, 0xC5, 0x4B, + 0x31, 0x3D, 0x25, 0x7E, 0xDC, 0x0C, 0x9A, 0x87, 0x14, 0xC9, 0x91, 0x09, 0x8F, 0xCC, 0x5A, 0xE8, + 0x81, 0x04, 0x82, 0xFE, 0x97, 0x32, 0xC3, 0x29, 0x6D, 0x81, 0xBA, 0xE6, 0xD9, 0x62, 0x06, 0xAF, + 0x99, 0x02, 0x96, 0xEE, 0x65, 0xDB, 0x13, 0x1D, 0xBE, 0x4F, 0xE7, 0x78, 0x2F, 0x6D, 0x35, 0x69, + 0x2C, 0xB4, 0x6A, 0x11, 0xA7, 0xD5, 0x4B, 0x42, 0x7D, 0x1F, 0xB6, 0x72, 0x3A, 0x63, 0x77, 0xD9, + 0xE4, 0xD6, 0x79, 0x64, 0x17, 0xAA, 0xF1, 0xB4, 0xAB, 0xC4, 0x59, 0xBD, 0xD7, 0x14, 0xC8, 0x86, + 0x20, 0x5D, 0x5A, 0xEA, 0x84, 0x38, 0xD6, 0x4C, 0xCA, 0xD9, 0x4B, 0x52, 0xF2, 0x10, 0xCB, 0x0F, + 0x0E, 0xF2, 0x11, 0x2A, 0x64, 0x5E, 0x72, 0x5A, 0x92, 0xF6, 0x8F, 0x2B, 0x3D, 0xD2, 0xB7, 0xD8, + 0x1B, 0x4F, 0x99, 0x52, 0x01, 0x3A, 0xFD, 0xE4, 0x9B, 0xAE, 0x6C, 0xDE, 0xCD, 0x12, 0x77, 0xDF, + 0x90, 0x28, 0x85, 0x7E, 0xB3, 0x01, 0x83, 0xC7, 0x55, 0x03, 0x7C, 0xEC, 0x90, 0x05, 0xAE, 0x1C, + 0xA9, 0x77, 0xC6, 0x7E, 0x16, 0x95, 0xFA, 0xC9, 0x5C, 0xAC, 0xC0, 0x36, 0x45, 0x87, 0x30, 0xD0, + 0x18, 0x67, 0x6D, 0xC1, 0xD9, 0x39, 0x06, 0x3D, 0xD5, 0x49, 0x81, 0x63, 0xEC, 0x0D, 0xBA, 0x65, + 0x65, 0xE8, 0xA9, 0x00, 0x5E, 0xD7, 0xA4, 0xFF, 0x15, 0x92, 0x91, 0xC0, 0x61, 0xEE, 0x8F, 0x40, + 0x3C, 0x07, 0x57, 0xE5, 0xC1, 0x13, 0xBC, 0x68, 0xF6, 0xE4, 0x40, 0x9A, 0x0B, 0x04, 0xAE, 0xF4, + 0xE9, 0x2F, 0x0D, 0xCE, 0x90, 0x0C, 0x0D, 0xFE, 0xB2, 0x5E, 0x1B, 0x43, 0x03, 0x93, 0xED, 0x7B, + 0x67, 0x97, 0xD8, 0xDB, 0xD3, 0x3F, 0x0A, 0x0F, 0x75, 0x3B, 0xEE, 0x73, 0xFE, 0x21, 0xFE, 0x22, + 0xAD, 0x13, 0xFE, 0x1A, 0xBD, 0x57, 0xD5, 0x0D, 0xF7, 0x4A, 0xAD, 0xEF, 0xC1, 0xD0, 0x7E, 0x39, + 0xAA, 0xA3, 0xCF, 0x81, 0x0D, 0x58, 0xB8, 0xA6, 0x1E, 0x8D, 0x8D, 0x4E, 0x75, 0x68, 0xF8, 0xF9, + 0x08, 0x2F, 0x40, 0x0E, 0x87, 0x5B, 0xFE, 0x85, 0x66, 0xEC, 0x73, 0xAB, 0xD9, 0x52, 0xBE, 0x8E, + 0xFA, 0x44, 0xE7, 0x53, 0xB7, 0xB6, 0xA6, 0x3B, 0x15, 0x92, 0x45, 0xFD, 0x55, 0xBE, 0x03, 0xEE, + 0x68, 0xE2, 0x5A, 0x25, 0xF0, 0x02, 0x82, 0x12, 0xDA, 0x4B, 0x47, 0x28, 0xF6, 0x2E, 0xBC, 0x70, + 0xF3, 0x09, 0x65, 0x86, 0x5C, 0x91, 0xE4, 0x75, 0x99, 0x5E, 0x6C, 0x0A, 0x50, 0x0C, 0xF2, 0x06, + 0x1A, 0x2D, 0x3B, 0x5D, 0x43, 0xAD, 0xFA, 0x6F, 0x52, 0x69, 0x75, 0x12, 0x8F, 0x40, 0x29, 0x8C, + 0xBA, 0x22, 0xCC, 0x20, 0x9D, 0x7E, 0x8F, 0x03, 0x68, 0x06, 0x68, 0x6F, 0xBC, 0xAD, 0xAA, 0x52, + 0x8A, 0xA0, 0xF9, 0x84, 0x68, 0xDA, 0x7C, 0x49, 0xBC, 0xB1, 0xEE, 0x12, 0xDA, 0xF9, 0xC6, 0x45, + 0xA3, 0xE1, 0xB0, 0x35, 0xF2, 0xFD, 0xFD, 0x56, 0x84, 0xEC, 0x01, 0xB8, 0xCE, 0xF9, 0x1C, 0x67, + 0x44, 0x7B, 0x34, 0xDD, 0xEA, 0xEB, 0x7A, 0x87, 0x34, 0xD6, 0x64, 0xC9, 0x26, 0x5A, 0xD7, 0xC2, + 0x3D, 0x0C, 0x7D, 0xFE, 0xC4, 0x54, 0xA9, 0x3A, 0x85, 0x55, 0x79, 0x82, 0x88, 0xAC, 0x02, 0x2D, + 0x63, 0xEA, 0x57, 0xCD, 0x75, 0x16, 0xBB, 0xB1, 0xB5, 0x83, 0x7B, 0xF7, 0x93, 0x44, 0x8C, 0xB8, + 0x22, 0xDA, 0x7C, 0x30, 0x99, 0x09, 0x59, 0x96, 0x39, 0x7F, 0x4F, 0x7D, 0x89, 0x68, 0xBC, 0x41, + 0x4A, 0xAE, 0x04, 0x41, 0x00, 0x84, 0x55, 0x43, 0x5E, 0x19, 0x74, 0x6D, 0xB6, 0xD5, 0x73, 0x34, + 0x20, 0x7A, 0xE8, 0x91, 0xEC, 0xAD, 0xEF, 0xCE, 0xC4, 0xE9, 0xA8, 0xF1, 0xCA, 0x2F, 0x16, 0xEC, + 0x3D, 0x60, 0xA7, 0xA8, 0x41, 0x55, 0x67, 0x2B, 0x35, 0xD8, 0xB2, 0xC4, 0x0B, 0xEF, 0x7B, 0xC6, + 0x1F, 0x0E, 0xF3, 0x76, 0xEC, 0x40, 0xF3, 0xED, 0xFE, 0x60, 0x6E, 0xE5, 0x31, 0xFD, 0x03, 0x5F, + 0x5D, 0xF8, 0x5C, 0x7C, 0xA7, 0x7E, 0xFF, 0xA4, 0x0C, 0x19, 0xEE, 0xA5, 0x15, 0xC3, 0xE3, 0xB3, + 0x6D, 0x7D, 0x36, 0xD5, 0xE1, 0x19, 0xBD, 0x46, 0xF7, 0x67, 0x88, 0xBC, 0x9F, 0x1F, 0xFC, 0x7D, + 0xE6, 0x62, 0x9A, 0xBF, 0xF7, 0x18, 0x31, 0xD8, 0x55, 0xEF, 0xDC, 0x8D, 0x47, 0x2A, 0x64, 0xC6, + 0x0B, 0x0C, 0xE7, 0xC0, 0xF5, 0xA3, 0xF3, 0x67, 0xE1, 0x1D, 0x3C, 0xDA, 0xF7, 0xDA, 0x57, 0xFF, + 0xDB, 0x20, 0xE5, 0x67, 0x2D, 0xCB, 0x3A, 0x9A, 0x5D, 0xC9, 0xD6, 0xC1, 0xF5, 0x1B, 0x8C, 0x11, + 0xF8, 0x04, 0x04, 0x62, 0xCF, 0xF8, 0xE9, 0x66, 0x12, 0xD6, 0xD2, 0xC1, 0x97, 0x12, 0x53, 0x4B, + 0x68, 0x9C, 0x52, 0x53, 0xE9, 0xC2, 0x2A, 0x45, 0x70, 0x98, 0xC2, 0xF3, 0xDE, 0x64, 0x98, 0x86, + 0xEA, 0x06, 0x75, 0x1A, 0x4C, 0xFE, 0x74, 0xFA, 0xCD, 0xD7, 0xD2, 0x1E, 0x01, 0x5F, 0x61, 0xEA, + 0xB0, 0x8A, 0xDB, 0xEF, 0x2C, 0xB8, 0x2A, 0x87, 0x6E, 0x9A, 0xD3, 0xB3, 0xD6, 0x62, 0x96, 0x5E, + 0x50, 0x8B, 0xD6, 0x7F, 0x05, 0x5B, 0x25, 0x22, 0xAE, 0xAE, 0x7F, 0x70, 0xBE, 0x20, 0x50, 0x74, + 0xCB, 0xA8, 0xC6, 0x6C, 0x39, 0xB9, 0x1F, 0x65, 0xB4, 0xD9, 0x22, 0x54, 0xB6, 0xB5, 0x4D, 0xBE, + 0x53, 0x94, 0x13, 0x7F, 0xE1, 0xB8, 0x78, 0xA8, 0xD6, 0x1F, 0xDA, 0xB5, 0xFE, 0xB8, 0xB3, 0xDA, + 0x1F, 0x1A, 0xD5, 0x6E, 0x83, 0xA6, 0x49, 0x53, 0xB3, 0x91, 0x4C, 0x68, 0x28, 0x55, 0x4E, 0xFC, + 0xA5, 0x76, 0x97, 0x2F, 0x61, 0xC1, 0x02, 0xDF, 0x31, 0x43, 0x6D, 0x42, 0x0C, 0x6F, 0xDE, 0xA6, + 0xC2, 0x26, 0x8D, 0xBE, 0x63, 0xC4, 0x91, 0x4B, 0x1B, 0xAE, 0xDC, 0x51, 0x7F, 0x0F, 0x7E, 0x90, + 0x29, 0xC2, 0x53, 0x93, 0x48, 0x90, 0x51, 0x7A, 0x0C, 0x14, 0x91, 0xBE, 0xE9, 0xE1, 0x50, 0xF8, + 0x31, 0x73, 0x03, 0xD4, 0x10, 0xC0, 0x90, 0x3E, 0x98, 0x74, 0xB6, 0xDE, 0x2D, 0x14, 0x64, 0x4A, + 0x73, 0xEB, 0xEF, 0x7D, 0xAD, 0x76, 0xFA, 0x18, 0x98, 0xFD, 0xEF, 0xF2, 0x2A, 0x24, 0x3F, 0x64, + 0x7D, 0xF4, 0xC3, 0xA8, 0xEC, 0x69, 0xD6, 0xF5, 0xA8, 0x99, 0xAA, 0x40, 0x47, 0x98, 0x22, 0x31, + 0x21, 0xF3, 0xB4, 0x2E, 0x0D, 0x2D, 0xA1, 0x8B, 0x92, 0x98, 0x80, 0x08, 0x1F, 0xC9, 0x70, 0x67, + 0xCF, 0xF3, 0xC3, 0x07, 0x0E, 0x62, 0x4D, 0x82, 0xF6, 0xA6, 0xF7, 0x2B, 0x3D, 0x5F, 0xA8, 0x89, + 0xF8, 0x75, 0x37, 0xE5, 0xEF, 0xAD, 0xD8, 0xA3, 0xF7, 0x76, 0x46, 0x24, 0xEC, 0xDB, 0x4B, 0x99, + 0x0D, 0x75, 0x19, 0x3B, 0x4E, 0xC1, 0xC8, 0x0D, 0x3C, 0xEE, 0x02, 0xFB, 0x1D, 0x2D, 0xC9, 0x42, + 0xDC, 0x18, 0x35, 0xC5, 0x70, 0x65, 0xB7, 0x11, 0xB4, 0x18, 0x38, 0x6B, 0x77, 0x5A, 0xE3, 0x5A, + 0x6B, 0x29, 0x0C, 0x8E, 0xB6, 0xFC, 0x2D, 0x79, 0xE3, 0xE2, 0x42, 0xE4, 0x08, 0xF2, 0x07, 0x73, + 0xE0, 0x08, 0xA0, 0x22, 0xF6, 0xA2, 0xDC, 0x79, 0x16, 0x1F, 0xB9, 0xDC, 0x1D, 0xE0, 0x5A, 0xD6, + 0x60, 0xA2, 0x42, 0xF3, 0x70, 0x01, 0xDC, 0xB6, 0xD0, 0x69, 0x80, 0x2D, 0x9A, 0x74, 0x3B, 0x45, + 0xE0, 0xA6, 0xDA, 0x3F, 0xD2, 0x1A, 0x4D, 0xCE, 0x45, 0x89, 0xEE, 0x63, 0x5E, 0x24, 0x42, 0x54, + 0x4A, 0x28, 0xC9, 0x64, 0xFF, 0xC8, 0xD9, 0x88, 0xF5, 0x2B, 0x39, 0xEF, 0x11, 0x6C, 0x9E, 0x8D, + 0xEC, 0x3B, 0x92, 0xB8, 0xCB, 0x2D, 0x43, 0x77, 0x82, 0x05, 0x45, 0x52, 0x5B, 0x91, 0xB4, 0x5A, + 0x35, 0x9E, 0x72, 0xDD, 0xB2, 0x81, 0x50, 0x27, 0x4A, 0xCE, 0xBA, 0x2B, 0x61, 0x74, 0x82, 0xCE, + 0xEC, 0x9F, 0x8E, 0xE4, 0x60, 0x34, 0xA2, 0xC8, 0x31, 0x4A, 0xBA, 0xDA, 0xC1, 0x3B, 0x94, 0x35, + 0x26, 0x24, 0x7D, 0x60, 0x31, 0x8E, 0xD3, 0xCF, 0xFE, 0xD7, 0xDA, 0x87, 0x9E, 0xCD, 0xE1, 0x69, + 0x8C, 0xA8, 0x17, 0x80, 0xF0, 0xB1, 0x33, 0xCD, 0x99, 0xB8, 0x97, 0xFA, 0x98, 0xDD, 0xA2, 0xF2, + 0xA6, 0xE8, 0xA6, 0x81, 0x7B, 0xA6, 0x95, 0x59, 0xE2, 0x6C, 0x46, 0x10, 0xD3, 0xC5, 0x45, 0xAD, + 0x15, 0x32, 0x40, 0x93, 0x44, 0x4F, 0x55, 0xD7, 0x8C, 0xE2, 0xAB, 0xA1, 0x3B, 0x66, 0x1A, 0x9F, + 0x66, 0x56, 0x24, 0x34, 0xCF, 0x6D, 0x58, 0x3F, 0x4E, 0x26, 0x7A, 0x5D, 0x15, 0x3A, 0x23, 0x5A, + 0xA6, 0x45, 0xE4, 0x84, 0x3D, 0x80, 0xAE, 0x5B, 0x77, 0x4B, 0xCB, 0x5B, 0x44, 0x0D, 0x08, 0x50, + 0xF1, 0x81, 0xB0, 0x8B, 0xEB, 0x2E, 0xE9, 0x40, 0x5B, 0xF9, 0x99, 0x75, 0x53, 0x05, 0x78, 0x87, + 0xAA, 0xA8, 0x0F, 0xDE, 0x7A, 0xCD, 0x4A, 0xD4, 0x36, 0x9F, 0xE3, 0x61, 0xF4, 0xE2, 0xBB, 0x87, + 0xEA, 0xD1, 0x70, 0x2D, 0xA1, 0x1D, 0x17, 0x97, 0x8D, 0x8C, 0x6F, 0x17, 0xB1, 0x36, 0x16, 0x69, + 0x43, 0x22, 0xA0, 0x0E, 0x03, 0x11, 0x37, 0xAC, 0xAB, 0x4A, 0x57, 0x80, 0x76, 0x70, 0x80, 0xCD, + 0x96, 0x36, 0x58, 0xE0, 0x5C, 0x4C, 0x89, 0x1C, 0x3A, 0xEA, 0x9A, 0x5F, 0x49, 0x8B, 0x00, 0x91, + 0xA8, 0xD3, 0x38, 0x40, 0x48, 0xF5, 0xCE, 0x75, 0xDE, 0x94, 0x29, 0x6C, 0x40, 0x2E, 0x9C, 0x44, + 0xE2, 0x42, 0xAB, 0x7C, 0xC5, 0x45, 0xC2, 0x3B, 0x21, 0x7E, 0x08, 0xA9, 0xD5, 0x17, 0x03, 0x61, + 0xE9, 0xF8, 0x91, 0xA2, 0xCB, 0x88, 0x46, 0x0E, 0x54, 0x19, 0x41, 0x90, 0x7C, 0xA6, 0xF7, 0x60, + 0x36, 0x21, 0xFA, 0x69, 0xF1, 0x18, 0xCF, 0x5F, 0x07, 0x78, 0xF4, 0x18, 0x1F, 0xD3, 0xE4, 0xE5, + 0x8C, 0x13, 0xF0, 0x59, 0x07, 0x4F, 0xD4, 0xD3, 0x7A, 0xED, 0xC7, 0x7C, 0xDF, 0xEA, 0x37, 0x93, + 0x83, 0xA8, 0x0E, 0xC8, 0x60, 0x99, 0x2C, 0x96, 0x99, 0xAB, 0xFB, 0xCD, 0xEC, 0xB1, 0xF3, 0x14, + 0x72, 0x34, 0xF5, 0xDE, 0xE4, 0x6B, 0x7C, 0xBE, 0xA6, 0x0B, 0x22, 0xF8, 0xFD, 0x34, 0x87, 0x7F, + 0x95, 0x2B, 0x04, 0xBE, 0x4C, 0x96, 0x95, 0x37, 0xA3, 0x87, 0x75, 0x3B, 0x5C, 0x3D, 0x3F, 0x9A, + 0xDB, 0x5D, 0x4A, 0xC7, 0x38, 0xAE, 0xFD, 0xBC, 0x6A, 0x42, 0x98, 0x00, 0x9C, 0x16, 0x49, 0x15, + 0x60, 0x1F, 0xB7, 0x60, 0x93, 0xBF, 0x82, 0x09, 0xAF, 0xD7, 0x79, 0xAE, 0x5C, 0xE8, 0xF8, 0xFF, + 0x46, 0xB5, 0x75, 0xDD, 0xA8, 0xC4, 0xCF, 0xD9, 0x73, 0x7A, 0x2B, 0x9F, 0x9A, 0xD2, 0xB8, 0xFF, + 0xA9, 0x5F, 0x28, 0x08, 0x33, 0x49, 0xE4, 0x25, 0x91, 0x7C, 0x5C, 0xAD, 0xAC, 0xEE, 0x56, 0xFC, + 0x58, 0x1D, 0x3F, 0xF9, 0x5E, 0xCB, 0xFB, 0x27, 0xCD, 0x58, 0xC8, 0xFF, 0x12, 0xE6, 0xFE, 0x4C, + 0x69, 0x74, 0x93, 0x11, 0x91, 0xA7, 0x72, 0x78, 0x00, 0x2C, 0xF7, 0x45, 0xDA, 0xEE, 0x95, 0xB8, + 0xE8, 0xE9, 0x21, 0x40, 0xB4, 0x98, 0x74, 0xA5, 0xBC, 0x60, 0xE6, 0x37, 0x85, 0xEF, 0x3A, 0x67, + 0x77, 0x67, 0x43, 0x1D, 0x2B, 0xAC, 0xFE, 0xDF, 0x44, 0x04, 0x40, 0x2B, 0x56, 0x6C, 0xB1, 0x57, + 0x57, 0x9F, 0x33, 0x7C, 0xBD, 0xB9, 0x3A, 0x4F, 0x48, 0x80, 0x3A, 0xF1, 0xC8, 0x7D, 0xE2, 0x2D, + 0xBD, 0x79, 0xD0, 0xB8, 0x9D, 0xD0, 0xCC, 0x86, 0x72, 0x94, 0x69, 0x10, 0x91, 0x66, 0x7C, 0xB8, + 0xC1, 0xB0, 0xAA, 0x44, 0x9A, 0xF1, 0x9A, 0xE6, 0xE4, 0xDD, 0xD7, 0x39, 0x74, 0xF6, 0x68, 0x86, + 0x32, 0x06, 0xF7, 0x9B, 0x27, 0xD1, 0x53, 0x34, 0x8D, 0xEB, 0x5A, 0x14, 0x87, 0x06, 0x86, 0x1B, + 0xB8, 0x9A, 0x75, 0x36, 0xC8, 0x29, 0xCF, 0x99, 0x1C, 0xFA, 0x7C, 0xB8, 0x82, 0x62, 0x65, 0x7C, + 0x90, 0x07, 0x8F, 0xF1, 0xD7, 0xCF, 0xA9, 0xDF, 0x4F, 0x90, 0x7F, 0x04, 0x42, 0xE8, 0x38, 0x3A, + 0xE4, 0x9B, 0xC5, 0x74, 0xCB, 0xFD, 0x49, 0x94, 0x1F, 0xCD, 0x1F, 0xA3, 0xE2, 0x4E, 0x61, 0x75, + 0x0D, 0x70, 0xEB, 0x18, 0xE4, 0xA3, 0xC9, 0x79, 0x5D, 0x53, 0xB3, 0x87, 0x74, 0x0A, 0x42, 0x54, + 0x3F, 0x99, 0x0F, 0x7D, 0xFA, 0x85, 0x2B, 0xCC, 0xC7, 0x19, 0xC4, 0xED, 0xE1, 0x90, 0xFE, 0x0A, + 0x92, 0xCD, 0xB0, 0x09, 0x43, 0x15, 0xE1, 0xBE, 0x3B, 0x09, 0x27, 0x71, 0xA7, 0x65, 0x8E, 0x10, + 0x50, 0x7B, 0x44, 0x4F, 0xC6, 0xA6, 0x5B, 0xD6, 0xAF, 0x01, 0x44, 0x9D, 0x55, 0xA3, 0xD6, 0xED, + 0xCD, 0x8F, 0xEE, 0x85, 0xEB, 0x29, 0x0D, 0x1A, 0x3E, 0x64, 0xAC, 0x67, 0xB7, 0xF9, 0xF1, 0xF2, + 0x68, 0x4E, 0x3E, 0x2F, 0x88, 0x9F, 0x9C, 0xCF, 0x14, 0x28, 0xC9, 0x8B, 0xCF, 0x4D, 0x00, 0x12, + 0xD5, 0xC4, 0x37, 0xB0, 0x43, 0x63, 0x59, 0x21, 0xBF, 0xC2, 0x27, 0x85, 0x03, 0x44, 0x75, 0xCD, + 0xC4, 0x10, 0x52, 0x69, 0x71, 0x5B, 0x95, 0x90, 0x96, 0x5F, 0xDD, 0x8F, 0x6A, 0xA5, 0xF0, 0x9B, + 0x8C, 0x45, 0xA0, 0xEB, 0xC5, 0xC5, 0x3F, 0x10, 0xC9, 0x5A, 0x8B, 0x14, 0x13, 0x3F, 0x53, 0x04, + 0xAD, 0xCD, 0x54, 0x46, 0xA9, 0xB9, 0x58, 0x22, 0x50, 0x60, 0xC3, 0x5E, 0x3B, 0xC8, 0x2D, 0x0A, + 0xC8, 0xDE, 0xF3, 0x7C, 0x71, 0xD7, 0x78, 0x7B, 0x24, 0x6E, 0x5D, 0x6F, 0x2B, 0xD1, 0x91, 0x8D, + 0xDA, 0x04, 0xB9, 0xD3, 0x7F, 0x91, 0xEF, 0x78, 0xBD, 0x94, 0x68, 0x6F, 0x33, 0x01, 0x1B, 0x7E, + 0x0C, 0xD9, 0x3B, 0x27, 0x7D, 0x08, 0xCA, 0x70, 0xD3, 0xB8, 0x2E, 0x8B, 0x7F, 0x42, 0x99, 0x67, + 0xE0, 0x4C, 0x92, 0x8C, 0x11, 0x20, 0x98, 0x9B, 0x73, 0x25, 0xC5, 0x5D, 0xB4, 0x58, 0x58, 0x15, + 0xB3, 0x14, 0x52, 0x23, 0x44, 0x05, 0x51, 0x9E, 0x16, 0xA5, 0x39, 0x51, 0x9C, 0xB2, 0xCB, 0x9C, + 0xB8, 0xBC, 0xCC, 0x6F, 0x7A, 0xF6, 0xE0, 0x4A, 0x9E, 0x6F, 0x92, 0x42, 0xC5, 0xAB, 0x32, 0xC9, + 0xEE, 0x3C, 0xA4, 0xCD, 0xCD, 0x2F, 0x2E, 0x56, 0x7D, 0x67, 0x9F, 0x47, 0x8F, 0xD5, 0x27, 0xB1, + 0xAB, 0xFF, 0x9C, 0x28, 0xF3, 0x4F, 0x6A, 0xD8, 0x57, 0x96, 0xE4, 0xD4, 0x80, 0x0C, 0x77, 0x0F, + 0xDC, 0x58, 0xB5, 0x32, 0x39, 0x95, 0xBF, 0xBA, 0xA0, 0xFA, 0x54, 0x65, 0xB7, 0xE6, 0x54, 0xC1, + 0x41, 0xB1, 0x4C, 0xC4, 0x3A, 0x91, 0x7C, 0xB9, 0x76, 0x84, 0x54, 0xB3, 0x6B, 0x24, 0x7E, 0x68, + 0x7E, 0x81, 0xA4, 0xE6, 0x65, 0xA1, 0xD6, 0x5D, 0x21, 0xE4, 0xB9, 0xA4, 0xE2, 0x8F, 0xEA, 0xB7, + 0x57, 0xF9, 0x62, 0xB3, 0x4A, 0x1E, 0xD5, 0x67, 0x4F, 0x50, 0xD7, 0x4F, 0xF1, 0x75, 0x5C, 0x27, + 0xF3, 0xAB, 0x38, 0x28, 0xE7, 0x45, 0xBA, 0xAE, 0x70, 0x69, 0x68, 0x8E, 0xA3, 0x43, 0x41, 0x89, + 0x06, 0xB2, 0xCF, 0x8A, 0xF8, 0x82, 0xC1, 0xA5, 0xF9, 0x84, 0xE2, 0xCB, 0x1D, 0x4F, 0x28, 0xFA, + 0x4B, 0x54, 0xF1, 0x81, 0xAF, 0x33, 0xA9, 0x47, 0x7E, 0x78, 0x28, 0x5E, 0x80, 0x61, 0xB6, 0xD2, + 0xF5, 0xD3, 0x48, 0x42, 0x87, 0x4C, 0xA6, 0xEE, 0xD3, 0x3F, 0x15, 0xE1, 0xCB, 0xE6, 0x2B, 0x41, + 0x4B, 0x7A, 0x79, 0x60, 0xC4, 0x45, 0x4F, 0x56, 0x80, 0xBF, 0x08, 0xE9, 0x73, 0x0A, 0xF0, 0xB1, + 0x03, 0x48, 0x68, 0x7E, 0x59, 0x27, 0xAE, 0xDA, 0xD1, 0x1C, 0x2A, 0x6C, 0x5C, 0x82, 0xD4, 0xCF, + 0xFC, 0x3F, 0xBB, 0x3D, 0x7E, 0x62, 0xC2, 0x1E, 0xDA, 0xC8, 0x72, 0x6E, 0xE0, 0x44, 0x16, 0xC3, + 0x85, 0x54, 0x14, 0xDC, 0x51, 0x77, 0xE3, 0x86, 0x62, 0xBB, 0x01, 0xF3, 0x88, 0x97, 0x0C, 0x70, + 0xDD, 0x32, 0xC8, 0x75, 0xDA, 0x5A, 0x18, 0x32, 0x2E, 0x80, 0xFA, 0x2A, 0x72, 0x08, 0x17, 0x4E, + 0x61, 0x51, 0x72, 0xCC, 0x17, 0x68, 0x9F, 0x79, 0x08, 0x3D, 0x51, 0x41, 0xE0, 0xBA, 0x7C, 0x15, + 0x4E, 0x9F, 0x9A, 0xC4, 0x63, 0x7C, 0x5F, 0x14, 0xF9, 0x66, 0x2D, 0xB3, 0x99, 0x2F, 0xA7, 0x44, + 0x55, 0x34, 0x0A, 0x54, 0x84, 0x45, 0x64, 0x6E, 0x0E, 0x36, 0xB2, 0xA2, 0xE1, 0x8F, 0x5A, 0x59, + 0xF1, 0x53, 0xC8, 0xEC, 0xF8, 0xED, 0x29, 0xF3, 0xA3, 0x1A, 0x7E, 0x38, 0x3D, 0x04, 0x12, 0xC4, + 0x7F, 0x33, 0x07, 0x79, 0x5F, 0xBB, 0x4F, 0x17, 0x1B, 0x71, 0x4A, 0xF4, 0xE1, 0x0F, 0xB9, 0x4E, + 0x76, 0xBD, 0x68, 0xCE, 0xAE, 0x7C, 0x82, 0xF0, 0xC3, 0xDE, 0xF7, 0x9C, 0x74, 0xA3, 0x4C, 0x0D, + 0x38, 0x3F, 0x5A, 0x02, 0x4F, 0x79, 0xE9, 0x6F, 0x62, 0x2C, 0x60, 0x13, 0x16, 0xCF, 0x67, 0xEE, + 0xBB, 0xB4, 0x1D, 0x2F, 0x6F, 0x8E, 0x9B, 0x1A, 0xE3, 0x4C, 0x85, 0x71, 0x23, 0x3F, 0x72, 0x2B, + 0xBC, 0x8B, 0x55, 0x7E, 0x1E, 0xAF, 0x48, 0xA7, 0xE8, 0xF1, 0x35, 0x6A, 0x79, 0xC6, 0x54, 0xED, + 0x34, 0x28, 0xD9, 0x2F, 0x92, 0x11, 0xCF, 0x71, 0x44, 0x81, 0x25, 0x48, 0x2A, 0x0A, 0xE8, 0x75, + 0xE5, 0x70, 0xCC, 0xF0, 0x43, 0x41, 0x86, 0x15, 0xC1, 0x01, 0xFE, 0x5C, 0x18, 0xE8, 0xC2, 0x89, + 0x81, 0x08, 0x84, 0x75, 0x31, 0x0D, 0x75, 0x04, 0x58, 0xFA, 0xA5, 0xAF, 0x2B, 0x2C, 0x5B, 0x0A, + 0x05, 0x53, 0xF4, 0x5B, 0x1D, 0xFA, 0xAD, 0x5C, 0x69, 0x99, 0x8E, 0xD5, 0x95, 0x08, 0xEB, 0x8A, + 0xF0, 0x51, 0xBD, 0xFF, 0xEB, 0xC9, 0xD9, 0xCD, 0xF0, 0xE8, 0x89, 0x5D, 0xDD, 0xDB, 0x5E, 0x97, + 0x54, 0xCD, 0x67, 0xFE, 0xAB, 0x9D, 0xC8, 0x47, 0xBA, 0x23, 0x5A, 0x60, 0xEE, 0x2E, 0xED, 0xDC, + 0x2D, 0x8E, 0xF1, 0xBF, 0xBC, 0xE6, 0xC1, 0x2E, 0xAB, 0x16, 0x74, 0xF8, 0x1D, 0xF2, 0x33, 0x34, + 0x6D, 0x1D, 0x11, 0x98, 0x5A, 0xBD, 0x44, 0x6B, 0x91, 0x3B, 0x3E, 0x9D, 0xF2, 0x59, 0x98, 0x5B, + 0xD5, 0xD0, 0x95, 0xBE, 0x3E, 0x0F, 0xD9, 0x11, 0xDB, 0x92, 0x2E, 0x9B, 0x0F, 0x8F, 0xED, 0xC0, + 0x7A, 0xF0, 0x88, 0xBC, 0x50, 0xE4, 0x57, 0x4E, 0xE2, 0x62, 0x09, 0xDA, 0x41, 0xF7, 0x71, 0x80, + 0x4D, 0x74, 0x41, 0x07, 0x65, 0x5D, 0x63, 0x8E, 0xF5, 0x5E, 0x10, 0xB1, 0x83, 0x04, 0x36, 0x44, + 0x09, 0x9D, 0x8E, 0x2E, 0xAB, 0xAB, 0xD5, 0xB7, 0x45, 0x22, 0x4D, 0x98, 0x51, 0xE7, 0x70, 0x43, + 0x34, 0xD1, 0x1C, 0xC9, 0x87, 0xDA, 0xCF, 0xF3, 0x1C, 0xE4, 0x1A, 0xBA, 0xE8, 0x20, 0x05, 0x3B, + 0xC0, 0xD8, 0x35, 0xC2, 0x16, 0xA0, 0xD3, 0x96, 0xAE, 0xB9, 0xB8, 0xEB, 0xD9, 0x15, 0xF4, 0x88, + 0x72, 0xCC, 0xC4, 0x22, 0x0E, 0x33, 0x42, 0xDC, 0x0D, 0x56, 0xE4, 0x39, 0x1D, 0xD0, 0xAD, 0x12, + 0x62, 0xE1, 0xBC, 0x90, 0xB9, 0x9E, 0x2E, 0x94, 0x0B, 0x0E, 0x90, 0x6C, 0x0D, 0xF3, 0xCB, 0x9C, + 0xCC, 0x5C, 0x52, 0xE3, 0x5F, 0xCA, 0x99, 0xE6, 0x55, 0x94, 0x52, 0xCD, 0x20, 0x6B, 0xB0, 0xE7, + 0xF5, 0x14, 0x5B, 0x1A, 0x43, 0x9E, 0x47, 0x1E, 0xAB, 0xE2, 0xEE, 0xC8, 0xC9, 0x0D, 0x71, 0xEE, + 0x73, 0xA7, 0xCD, 0x78, 0x3A, 0xA7, 0x36, 0x2F, 0xF5, 0x6A, 0x69, 0x57, 0x93, 0x1E, 0xF1, 0x70, + 0xBA, 0x35, 0x4D, 0x76, 0x2E, 0x99, 0x50, 0x3C, 0xE7, 0x13, 0x74, 0xFA, 0x76, 0x84, 0x13, 0x91, + 0x18, 0xB4, 0x91, 0x3F, 0x1A, 0x06, 0x75, 0xE0, 0x00, 0xE8, 0x4D, 0xE2, 0x3A, 0xFA, 0x37, 0xD1, + 0x6F, 0x9C, 0xE8, 0xB1, 0x8D, 0x3E, 0x61, 0x70, 0x6E, 0x5C, 0x7B, 0x6D, 0xDA, 0x18, 0xE9, 0x94, + 0x97, 0x4D, 0x6F, 0xAB, 0x8E, 0x2C, 0x79, 0x4B, 0x17, 0x80, 0x7D, 0x8F, 0x7D, 0xB5, 0x32, 0xA3, + 0xDF, 0x72, 0x6E, 0x65, 0x55, 0xA2, 0x12, 0x27, 0x62, 0xA7, 0x34, 0x74, 0x9F, 0x56, 0x5B, 0x74, + 0xAF, 0xF5, 0x9F, 0xDD, 0x1B, 0xB7, 0x2C, 0xC4, 0x2C, 0x40, 0x93, 0x0A, 0x2D, 0xA8, 0x26, 0xB5, + 0x24, 0x57, 0x5F, 0x72, 0xF5, 0x15, 0xA9, 0x1D, 0x73, 0x87, 0x9E, 0x42, 0x13, 0x52, 0xD3, 0x8A, + 0xF5, 0x94, 0x81, 0x74, 0xE2, 0xA7, 0x6C, 0x2D, 0x62, 0xAA, 0x08, 0x55, 0x02, 0x6A, 0xEF, 0x68, + 0x7C, 0x32, 0xCA, 0x5D, 0x20, 0xB7, 0xCE, 0xAB, 0x3E, 0x9D, 0x1E, 0x28, 0x67, 0xDF, 0x69, 0x40, + 0xFE, 0xCF, 0x0C, 0x54, 0x0C, 0xCC, 0x6B, 0x28, 0x0D, 0x67, 0xF5, 0x39, 0x73, 0x1A, 0xA9, 0x40, + 0x35, 0x7D, 0xBC, 0xA6, 0x0F, 0xAA, 0x78, 0xB9, 0x24, 0x9D, 0x6B, 0xDC, 0x67, 0x27, 0xC8, 0x77, + 0xE2, 0x2E, 0x48, 0x85, 0x18, 0xF3, 0x0F, 0xA9, 0x79, 0xE5, 0xF7, 0x29, 0xFF, 0x0C, 0x87, 0xFC, + 0x88, 0x44, 0x57, 0x2E, 0x92, 0x90, 0xE6, 0x9A, 0x78, 0x7F, 0x2D, 0xA7, 0x49, 0x31, 0x59, 0x59, + 0xC3, 0x03, 0xD9, 0x29, 0x66, 0x51, 0x2E, 0x4F, 0x3E, 0xF1, 0x19, 0x81, 0xF3, 0x37, 0x59, 0x7E, + 0x3B, 0xE5, 0x39, 0xEE, 0xDE, 0xBC, 0x12, 0x4E, 0xC6, 0x03, 0x97, 0xFC, 0xFE, 0x4A, 0xB1, 0xE3, + 0xEE, 0x2F, 0xA3, 0x7B, 0x75, 0x0F, 0x82, 0x79, 0xB8, 0xF1, 0x3E, 0x3D, 0x41, 0xFE, 0xA6, 0x48, + 0x2F, 0x2E, 0x92, 0x42, 0x5D, 0x91, 0x4A, 0xE5, 0xE5, 0xCA, 0x42, 0x1B, 0xAE, 0xFB, 0xAA, 0x45, + 0xE9, 0x5B, 0x20, 0x5E, 0x21, 0x83, 0xBC, 0x56, 0x42, 0x02, 0xC0, 0x8B, 0xB8, 0xD2, 0x77, 0xDC, + 0x59, 0x0E, 0x48, 0x5A, 0xCD, 0x18, 0x91, 0xD4, 0xB4, 0xDD, 0x95, 0x45, 0x8F, 0x09, 0x10, 0x46, + 0x65, 0xFA, 0x22, 0x35, 0xE1, 0xB9, 0xEE, 0x99, 0x50, 0xFD, 0x20, 0x79, 0x24, 0xC9, 0x95, 0xB3, + 0x66, 0xC7, 0xC9, 0xC1, 0xC7, 0xC4, 0x2D, 0x2E, 0x9F, 0xF8, 0x06, 0xBB, 0xC6, 0xD9, 0x8D, 0xF7, + 0x0A, 0xD9, 0xA3, 0x57, 0x78, 0x88, 0x7A, 0x91, 0xA2, 0x9B, 0x8D, 0xAE, 0x09, 0x32, 0xE9, 0xE4, + 0x61, 0xBD, 0x90, 0xB8, 0x12, 0x51, 0x99, 0x79, 0x6C, 0x57, 0x22, 0xAE, 0xC2, 0xEA, 0x08, 0xDC, + 0xD6, 0xB4, 0x86, 0x56, 0x4F, 0x4B, 0x25, 0x67, 0xCF, 0xD7, 0x52, 0x65, 0x76, 0x86, 0x85, 0x65, + 0x7A, 0x49, 0xA9, 0x56, 0xB5, 0x8C, 0xB1, 0x18, 0x67, 0xCB, 0xFA, 0x4A, 0xA5, 0x78, 0xA8, 0x8B, + 0x44, 0xA8, 0x5B, 0xE9, 0x9F, 0x11, 0x36, 0xA7, 0xC0, 0x44, 0x6D, 0x18, 0xB8, 0x81, 0x4C, 0x45, + 0xC5, 0x45, 0xF7, 0xF2, 0x1C, 0x0F, 0xEF, 0x5B, 0x97, 0xD7, 0x20, 0x21, 0xD1, 0x47, 0x64, 0xFB, + 0x78, 0x24, 0xBF, 0x81, 0xCA, 0xA3, 0xA2, 0x9E, 0x69, 0x86, 0x8E, 0xBF, 0xE2, 0x97, 0x6E, 0x6E, + 0x28, 0xA0, 0x62, 0xD6, 0xD5, 0xCF, 0x31, 0xC1, 0x3A, 0x06, 0x8C, 0x76, 0x6E, 0x1C, 0x48, 0xB0, + 0xDC, 0x78, 0xD7, 0x45, 0x77, 0xEA, 0xB7, 0xC8, 0xF4, 0x3E, 0xC9, 0x5A, 0xFB, 0x84, 0x58, 0xCC, + 0x6B, 0xD9, 0xFF, 0x12, 0xAB, 0xEB, 0x7C, 0x45, 0xAC, 0x56, 0xD4, 0x67, 0x07, 0xA3, 0x8F, 0x80, + 0xCF, 0x9D, 0x6B, 0xD5, 0x07, 0xCA, 0x6E, 0xBF, 0x7A, 0xF6, 0x71, 0x1F, 0x69, 0x76, 0xBA, 0xBF, + 0xDF, 0x5A, 0xBD, 0x64, 0x31, 0xD0, 0x8E, 0x7F, 0x27, 0x2A, 0x89, 0xB9, 0x42, 0x1A, 0x87, 0xDE, + 0xFC, 0x76, 0xE7, 0xEB, 0x65, 0x21, 0xAF, 0xA0, 0x24, 0x2A, 0xF2, 0xE5, 0x11, 0xE1, 0xEA, 0x75, + 0x3D, 0xAF, 0xAD, 0xD9, 0x5D, 0x91, 0x66, 0x37, 0xBA, 0xA0, 0xF7, 0x1B, 0xCE, 0xD5, 0x91, 0x9E, + 0xE0, 0x3A, 0x08, 0x65, 0xE6, 0x03, 0x5D, 0x5C, 0x22, 0x09, 0x87, 0x31, 0x57, 0xA5, 0xBD, 0xD5, + 0x8C, 0xB4, 0xBB, 0x1A, 0xC8, 0xDB, 0x31, 0xFF, 0xCB, 0xA8, 0xBD, 0x19, 0x17, 0xBC, 0x19, 0x71, + 0x6E, 0xFA, 0xE9, 0x64, 0xD9, 0xD8, 0x92, 0xE1, 0x72, 0x74, 0x8E, 0xB5, 0xA0, 0x20, 0xDA, 0x58, + 0x88, 0x9D, 0x65, 0xE7, 0x91, 0x55, 0x88, 0x54, 0x54, 0x70, 0x21, 0x72, 0xCC, 0x0A, 0xD7, 0x71, + 0x21, 0x55, 0x86, 0x85, 0x41, 0x2C, 0x99, 0xA0, 0x35, 0x0B, 0xE5, 0x0A, 0x0A, 0xBD, 0xF6, 0x61, + 0x2A, 0x5C, 0xCF, 0x15, 0x21, 0x20, 0x61, 0xA7, 0x5B, 0x0B, 0xAD, 0x0C, 0x15, 0x16, 0x71, 0x5D, + 0xB2, 0x2B, 0x1E, 0x1A, 0x2D, 0x6B, 0x09, 0x04, 0xFC, 0x38, 0x6E, 0x24, 0xCD, 0xE5, 0xAB, 0x20, + 0x5D, 0xA1, 0x30, 0xA3, 0x3B, 0xC9, 0x37, 0x59, 0x45, 0xC2, 0x16, 0xDA, 0x95, 0x9B, 0x35, 0xF4, + 0x74, 0x74, 0x81, 0x51, 0x7D, 0xA9, 0xBB, 0xA0, 0x02, 0x5D, 0x16, 0x31, 0xA1, 0x8A, 0x8E, 0xB1, + 0xD0, 0xFE, 0x7E, 0x37, 0xCE, 0x5F, 0x20, 0x33, 0x98, 0x40, 0x24, 0xD0, 0x3C, 0xF3, 0xAF, 0xAE, + 0x69, 0x0E, 0x06, 0x50, 0x43, 0xBC, 0x86, 0xE0, 0x66, 0x44, 0x24, 0xA7, 0x83, 0x64, 0x47, 0xD6, + 0xF9, 0xE4, 0xBA, 0xD9, 0x5F, 0x88, 0x7D, 0x0F, 0x51, 0x55, 0xA8, 0xC8, 0xA1, 0xB9, 0xC5, 0xE4, + 0x72, 0xC3, 0xD2, 0x20, 0x49, 0x88, 0xD0, 0x6B, 0x9A, 0xF0, 0x61, 0x3B, 0xD7, 0x6A, 0x7A, 0xF7, + 0xF7, 0x15, 0xBA, 0xE0, 0x7D, 0x0C, 0xCF, 0xA8, 0xEE, 0x06, 0x0B, 0xEE, 0x01, 0xB4, 0x15, 0xFE, + 0xFB, 0x40, 0xA0, 0x45, 0x0D, 0x2D, 0xB8, 0xAD, 0x7E, 0x11, 0xDC, 0x06, 0xF7, 0x3B, 0x41, 0x8F, + 0x17, 0x17, 0xCA, 0xD2, 0x07, 0x41, 0x97, 0xF2, 0xF2, 0xCD, 0x98, 0x92, 0x5F, 0xD6, 0x6A, 0xBD, + 0x5D, 0x74, 0x76, 0x36, 0xC2, 0xCB, 0x45, 0x1A, 0x82, 0xF0, 0x05, 0x1A, 0x6C, 0xF4, 0x18, 0xBF, + 0x75, 0x40, 0xCA, 0x3A, 0x9F, 0x42, 0x8F, 0x10, 0x04, 0x49, 0xD8, 0xEB, 0x6E, 0x74, 0xCE, 0xDE, + 0x46, 0x05, 0x5D, 0x41, 0xBA, 0x18, 0x90, 0x53, 0x31, 0x0D, 0xFB, 0x75, 0x8D, 0x03, 0x41, 0x2E, + 0x2C, 0xC5, 0xAB, 0x95, 0xC7, 0x85, 0x1E, 0xFD, 0xC4, 0xE2, 0x7C, 0xA4, 0x21, 0x98, 0x7A, 0x09, + 0xB0, 0x2A, 0x38, 0x9F, 0xDE, 0x13, 0x44, 0xC1, 0x3C, 0x7E, 0xEC, 0x0D, 0xE4, 0x1B, 0xAB, 0x36, + 0x9E, 0xA0, 0xDA, 0x00, 0x49, 0x4E, 0xB7, 0x90, 0xDC, 0x32, 0x2D, 0xA8, 0x81, 0x1B, 0xB9, 0xA5, + 0x92, 0x94, 0xED, 0xEF, 0xEB, 0x90, 0x56, 0xB5, 0xCD, 0x41, 0x41, 0xC7, 0xE8, 0x8F, 0x1E, 0x15, + 0x03, 0x6E, 0x05, 0x0D, 0xCB, 0x02, 0xD2, 0x3C, 0xB5, 0x2B, 0x6C, 0x84, 0x2E, 0x75, 0x29, 0x1C, + 0xDC, 0x79, 0xEA, 0x1A, 0x41, 0x22, 0x71, 0x61, 0x12, 0x8D, 0x82, 0x8F, 0x77, 0xE2, 0xD6, 0x18, + 0x0B, 0x2D, 0xC8, 0x58, 0x68, 0x13, 0xA8, 0x05, 0x55, 0xC5, 0xA9, 0xE4, 0x90, 0xA0, 0x82, 0x01, + 0x15, 0x60, 0x7C, 0xD4, 0xB6, 0x0B, 0xD9, 0x10, 0x4C, 0xDA, 0xDC, 0x9E, 0x6C, 0x64, 0x4F, 0x82, + 0xA4, 0x47, 0x60, 0xAF, 0xD1, 0x6D, 0x1F, 0xED, 0x62, 0x85, 0xAD, 0xAC, 0xD9, 0x90, 0x5C, 0x40, + 0x5B, 0x7D, 0x46, 0x4C, 0x90, 0xEE, 0xD6, 0x32, 0xBD, 0x45, 0x71, 0xC2, 0xCE, 0x0E, 0xE9, 0xE0, + 0xA9, 0xD6, 0x30, 0xEE, 0xBE, 0x63, 0x65, 0xBA, 0x61, 0xFC, 0x2F, 0xA1, 0x6D, 0xDE, 0x06, 0x59, + 0x9B, 0x7A, 0xBF, 0x65, 0xC1, 0x22, 0x7B, 0x59, 0xD8, 0x80, 0x6E, 0x19, 0x1F, 0x55, 0x1D, 0xB5, + 0x35, 0xBB, 0x2E, 0x29, 0xC9, 0x56, 0xD9, 0xA4, 0xA8, 0x77, 0x50, 0x37, 0x16, 0xD2, 0xF9, 0xED, + 0x86, 0x48, 0x8A, 0x0F, 0xE7, 0x44, 0x8F, 0xBC, 0x50, 0x53, 0x50, 0xD7, 0xBC, 0x76, 0x8D, 0x38, + 0xE7, 0x8E, 0xFB, 0x86, 0x78, 0x48, 0xDD, 0x3F, 0x8D, 0x88, 0x4A, 0x37, 0x83, 0x58, 0xB1, 0x13, + 0x15, 0xED, 0x14, 0x29, 0x25, 0x63, 0x04, 0xE2, 0x6B, 0xE8, 0x09, 0x22, 0x2C, 0x8C, 0x43, 0x67, + 0x9C, 0x82, 0xFE, 0x58, 0x93, 0xDA, 0x0C, 0x32, 0x7C, 0xD2, 0x34, 0x90, 0x78, 0x56, 0x75, 0x2C, + 0x1D, 0xA1, 0xA3, 0x57, 0x22, 0xB3, 0xF5, 0xE4, 0x88, 0xD3, 0xCD, 0x4D, 0x33, 0xA7, 0x42, 0x43, + 0xC1, 0xF4, 0xD6, 0xBC, 0x19, 0x15, 0x66, 0xA7, 0x28, 0xB0, 0xCC, 0x47, 0x6E, 0x8C, 0x9B, 0x41, + 0xEE, 0x2D, 0x27, 0x9D, 0x09, 0x02, 0xD5, 0x2C, 0x96, 0x2D, 0xCA, 0x71, 0xC9, 0x8C, 0x0E, 0x25, + 0xD4, 0x41, 0x3F, 0xC6, 0xB4, 0x8B, 0xDF, 0x0E, 0xEA, 0x10, 0xAB, 0xB9, 0xD9, 0xCF, 0x92, 0x6A, + 0x55, 0x15, 0xC1, 0x2E, 0x5C, 0x8F, 0x24, 0x50, 0xE7, 0xBB, 0x1A, 0x2E, 0xF0, 0x23, 0xF5, 0x91, + 0x74, 0x40, 0x1B, 0xD2, 0x10, 0x82, 0x36, 0x8C, 0x58, 0x78, 0xBE, 0xE9, 0x52, 0x8C, 0x9B, 0x2E, + 0xCD, 0x1B, 0x18, 0xEE, 0x10, 0xAB, 0x97, 0x97, 0x95, 0x5E, 0xBE, 0xFD, 0x7D, 0xF3, 0xDD, 0x5D, + 0x4E, 0xA1, 0x5B, 0xC2, 0x36, 0xD0, 0xF3, 0xDB, 0xAF, 0xED, 0x14, 0x0D, 0x17, 0xD5, 0x55, 0x13, + 0x59, 0xB0, 0xA3, 0x77, 0xF9, 0x22, 0x08, 0x43, 0x1A, 0x6E, 0x36, 0xBA, 0xC6, 0x2C, 0xF4, 0xA2, + 0x5A, 0x3A, 0x7F, 0xE7, 0x3E, 0x71, 0x30, 0x3E, 0x46, 0x50, 0x3E, 0xA3, 0xA0, 0x0C, 0x4A, 0x21, + 0x1F, 0x97, 0x4E, 0x81, 0xA0, 0xCC, 0x69, 0x5C, 0x24, 0xA7, 0x48, 0xED, 0x45, 0xCB, 0xAD, 0x56, + 0xD7, 0x6A, 0xE8, 0x27, 0x00, 0x2F, 0xAD, 0xC6, 0xCA, 0xBC, 0x47, 0x1E, 0x48, 0x2E, 0x52, 0xF9, + 0xB7, 0x00, 0xD6, 0x97, 0xCF, 0x11, 0x6C, 0x58, 0x3A, 0x65, 0xE8, 0xDB, 0x78, 0x9A, 0xD2, 0x0A, + 0xB2, 0xA7, 0x52, 0x83, 0x0F, 0xF5, 0xCB, 0xB6, 0x48, 0x84, 0x97, 0xF4, 0x06, 0x19, 0xC1, 0x17, + 0x32, 0xFD, 0x54, 0x92, 0xD2, 0xCA, 0x83, 0xC5, 0x2A, 0x50, 0x6E, 0x94, 0x54, 0xBC, 0x34, 0x1E, + 0xA1, 0xC3, 0xCA, 0xA0, 0x08, 0xAA, 0x09, 0xE6, 0x07, 0xE6, 0xC5, 0x80, 0xDC, 0xBA, 0xC1, 0x94, + 0x71, 0xF7, 0x04, 0x05, 0xE1, 0xCA, 0xAE, 0x43, 0xBE, 0x35, 0x4B, 0xBA, 0x8A, 0xE4, 0x82, 0x1D, + 0x57, 0xEF, 0x2D, 0xA5, 0x6F, 0x45, 0x6E, 0x88, 0xCC, 0x2F, 0x99, 0x16, 0x27, 0x38, 0x69, 0xD0, + 0xE3, 0xC1, 0x7D, 0xBF, 0x45, 0x68, 0x9B, 0x77, 0x20, 0x02, 0xE0, 0x3E, 0xC9, 0x80, 0x46, 0xB4, + 0x31, 0x68, 0xDB, 0x38, 0x94, 0x8C, 0x34, 0xA5, 0x6F, 0x17, 0x53, 0xBF, 0xB1, 0x74, 0xE2, 0x0D, + 0x90, 0x66, 0x24, 0xDA, 0x73, 0x5F, 0x1B, 0xED, 0x4B, 0xDB, 0x86, 0x1F, 0x5A, 0xBC, 0x9B, 0x02, + 0xF4, 0xD6, 0x32, 0x4E, 0xDD, 0x39, 0x3E, 0x2A, 0xFC, 0xFE, 0x21, 0xDD, 0x14, 0x69, 0xA5, 0xC3, + 0x92, 0xE3, 0x92, 0xCF, 0x3A, 0xD0, 0xA5, 0xD0, 0xDB, 0xFE, 0x77, 0x33, 0x4F, 0x8D, 0xAD, 0x22, + 0x0C, 0x9C, 0xE9, 0xA0, 0x50, 0x33, 0xC9, 0xB6, 0x6F, 0x0A, 0x23, 0x84, 0xF7, 0x64, 0x2A, 0x4E, + 0x6F, 0x3E, 0x7D, 0xBA, 0x39, 0x57, 0xA6, 0xB5, 0x82, 0x41, 0x38, 0xBC, 0x67, 0x02, 0xB2, 0x7B, + 0xFE, 0xF0, 0xAA, 0x93, 0x63, 0x7F, 0xF3, 0x04, 0xB5, 0x7A, 0x51, 0x59, 0xFB, 0xC1, 0xAC, 0x46, + 0x5C, 0x01, 0xC9, 0x6C, 0x2B, 0xA1, 0x94, 0x12, 0xC4, 0xA6, 0x25, 0xF8, 0x54, 0xDB, 0x83, 0x78, + 0x34, 0x41, 0x4F, 0x97, 0x2B, 0x86, 0xE3, 0x7F, 0xD3, 0x0A, 0x9A, 0x38, 0xDC, 0x1A, 0x59, 0x78, + 0xB7, 0x0D, 0x83, 0x1F, 0x7E, 0x69, 0x23, 0xF2, 0x14, 0xB5, 0xED, 0xD4, 0x35, 0xA7, 0xC7, 0x7C, + 0x72, 0x9F, 0x27, 0xD8, 0xDF, 0xC9, 0x26, 0x93, 0x13, 0xEB, 0x62, 0xB9, 0x46, 0x0F, 0x0C, 0xA2, + 0x4E, 0x14, 0xB6, 0x83, 0xD0, 0xA0, 0x09, 0x43, 0xD8, 0xE9, 0xAD, 0x98, 0x91, 0xEC, 0x27, 0x2B, + 0x35, 0x4C, 0x39, 0xB4, 0x89, 0x56, 0x5D, 0xD2, 0xA5, 0x6B, 0x39, 0xAF, 0xF5, 0x8B, 0x2D, 0xCA, + 0xBF, 0x37, 0x9A, 0x0D, 0x78, 0xA8, 0xC2, 0x6E, 0x55, 0xCA, 0x0B, 0x16, 0x2D, 0x8D, 0xEB, 0x6A, + 0x4A, 0x65, 0x35, 0xB7, 0x7D, 0x1C, 0x58, 0xA3, 0x52, 0x7C, 0x83, 0x54, 0x71, 0x93, 0x3D, 0x9B, + 0x48, 0xF9, 0x66, 0xE2, 0x74, 0x89, 0x34, 0x55, 0x44, 0x5A, 0xAA, 0xB3, 0xE5, 0x5B, 0x79, 0xD2, + 0x24, 0x0B, 0x24, 0x2F, 0x5A, 0x51, 0x75, 0x6D, 0x70, 0x66, 0x37, 0x51, 0x1D, 0x5D, 0x89, 0x3B, + 0x6F, 0x93, 0x9B, 0x24, 0x7C, 0x23, 0x5B, 0x90, 0xAB, 0x6F, 0xC1, 0x00, 0xCE, 0xBB, 0x90, 0x5B, + 0x7F, 0x59, 0xE1, 0xB5, 0x89, 0xB1, 0x27, 0x40, 0xA8, 0xE3, 0xB8, 0xA2, 0x16, 0xA9, 0x90, 0x98, + 0x6F, 0x9B, 0x85, 0xBD, 0xCF, 0xA2, 0x4B, 0x3A, 0x4B, 0xF3, 0x3B, 0x08, 0x9D, 0x29, 0x20, 0x1F, + 0x00, 0x9A, 0x07, 0x35, 0x46, 0x4A, 0x32, 0x1D, 0xB7, 0x44, 0x70, 0xA3, 0xE4, 0x6A, 0x1D, 0xD1, + 0x8C, 0xDA, 0x4F, 0xF8, 0x2C, 0x43, 0x5D, 0xE8, 0xF0, 0x8D, 0x2F, 0x73, 0xDA, 0xBD, 0x4F, 0x6C, + 0x94, 0x59, 0xCC, 0x86, 0x69, 0xBF, 0xF5, 0x28, 0x16, 0xAA, 0x64, 0xD1, 0x9D, 0x71, 0x9A, 0xAA, + 0x2E, 0x85, 0xA4, 0xE2, 0x2D, 0xA1, 0xB3, 0x2B, 0xC3, 0x69, 0x7A, 0xB5, 0xE1, 0x61, 0x92, 0x34, + 0xAE, 0x49, 0x30, 0x84, 0x1D, 0x7B, 0xAD, 0x2E, 0x68, 0x1C, 0xED, 0x82, 0x83, 0x9B, 0x44, 0xD0, + 0x19, 0x2E, 0x93, 0x6D, 0x2B, 0xFB, 0xFB, 0x5D, 0x41, 0xD6, 0x56, 0xB4, 0xA8, 0x92, 0x5F, 0xD2, + 0x70, 0x7B, 0x5C, 0x0F, 0x37, 0xDD, 0x6A, 0x48, 0xB7, 0x6D, 0x66, 0xE9, 0xDF, 0xEC, 0x84, 0x29, + 0xFF, 0xCB, 0x7B, 0xD3, 0x57, 0x54, 0x81, 0x48, 0x4F, 0x6F, 0xB7, 0xC6, 0x67, 0x52, 0xBC, 0xAA, + 0xFE, 0x9C, 0xDC, 0xD1, 0x59, 0x73, 0xCE, 0xC7, 0x42, 0xC9, 0x47, 0x12, 0x6D, 0xF7, 0x95, 0x39, + 0xA0, 0x70, 0xB8, 0x5F, 0x00, 0x7E, 0xF3, 0xCD, 0xFC, 0x52, 0xA5, 0x57, 0xC5, 0x4A, 0x95, 0x82, + 0xF3, 0x60, 0xDC, 0xFA, 0xA6, 0x10, 0x2F, 0xC6, 0xB7, 0xE0, 0xC5, 0xB9, 0xD0, 0x15, 0xE2, 0x55, + 0x16, 0xB4, 0x9B, 0x7C, 0xAF, 0x03, 0x3F, 0x50, 0x80, 0xCD, 0x2A, 0x55, 0xEA, 0x75, 0x9A, 0xDC, + 0xD0, 0xAF, 0x87, 0x66, 0x0A, 0x4F, 0x1E, 0x88, 0x0B, 0xDD, 0x6E, 0x71, 0xA2, 0xC2, 0xEF, 0x90, + 0x59, 0xFE, 0xE8, 0x18, 0x49, 0xBD, 0xD9, 0x90, 0xEC, 0xD8, 0x2A, 0x45, 0x27, 0xBE, 0xB7, 0x41, + 0x6E, 0x0E, 0xD2, 0x6C, 0x9C, 0x70, 0xDF, 0x9B, 0xA0, 0x8C, 0x5D, 0xE7, 0x29, 0x5D, 0x2F, 0x7C, + 0xB5, 0x70, 0x3E, 0x08, 0x09, 0x70, 0x07, 0xC1, 0x20, 0x25, 0xD9, 0xF7, 0x36, 0xC8, 0x25, 0x24, + 0x1E, 0x70, 0xE6, 0xA1, 0xCA, 0x95, 0x0E, 0x82, 0x3F, 0x6C, 0x3C, 0xD8, 0x86, 0xF9, 0x25, 0x1D, + 0xB0, 0xAE, 0xD4, 0x9A, 0x56, 0xC0, 0xFA, 0xAA, 0x92, 0xEF, 0xC9, 0x79, 0xFC, 0x03, 0x2D, 0x83, + 0x38, 0x5F, 0x6D, 0x0A, 0xF5, 0x99, 0xE3, 0xE8, 0xD9, 0xB6, 0xAE, 0xE5, 0xB6, 0xC8, 0x7B, 0x7A, + 0x5B, 0xA9, 0x7D, 0x6A, 0x5B, 0x01, 0xBE, 0x36, 0x3D, 0x14, 0x27, 0xCD, 0xB3, 0xF7, 0xA1, 0xBC, + 0xCD, 0x13, 0xB4, 0x9B, 0x93, 0x52, 0x1B, 0xA2, 0x8A, 0xCA, 0xF5, 0xBC, 0x75, 0x05, 0x5F, 0x4D, + 0x09, 0x6D, 0x58, 0x8C, 0x81, 0xC3, 0x38, 0x6A, 0x0A, 0x4F, 0x70, 0x10, 0xE6, 0x1D, 0xD7, 0x89, + 0x8A, 0xA6, 0xA1, 0xE9, 0xB9, 0x56, 0xD9, 0xD5, 0x97, 0x2C, 0xA0, 0x3E, 0x54, 0x11, 0xF5, 0xD5, + 0x99, 0x8F, 0xB4, 0x7F, 0x3E, 0x1A, 0xDD, 0x4B, 0x05, 0x49, 0x52, 0x64, 0x50, 0x51, 0xA7, 0x7D, + 0x0C, 0x76, 0x1B, 0x3D, 0x93, 0x22, 0xD9, 0xF2, 0x5C, 0xAE, 0xE3, 0xD3, 0x2C, 0x8A, 0x14, 0xA5, + 0xD2, 0x76, 0x80, 0x97, 0xF1, 0xCB, 0x0A, 0x89, 0xC4, 0xE9, 0x85, 0xE1, 0xBB, 0x04, 0xF8, 0x27, + 0xCD, 0x6B, 0xF5, 0xEB, 0x51, 0x84, 0x2A, 0x43, 0x92, 0xFF, 0x6D, 0xC7, 0xD8, 0x16, 0xC0, 0xDD, + 0xF1, 0x25, 0x6F, 0x54, 0x60, 0x6A, 0xD9, 0x54, 0x3C, 0x49, 0x0F, 0xB3, 0xE4, 0x83, 0xB3, 0x8B, + 0x31, 0x15, 0x58, 0x2E, 0xC3, 0x07, 0x3C, 0x66, 0x26, 0x5D, 0x0C, 0x8B, 0x18, 0x3B, 0x35, 0x9A, + 0x02, 0x68, 0xCC, 0x17, 0xB9, 0x7B, 0x6B, 0xB1, 0xF9, 0x52, 0x97, 0x54, 0x58, 0xB6, 0x76, 0x62, + 0x67, 0x68, 0x08, 0x71, 0xD9, 0xD0, 0x49, 0x0A, 0x9D, 0xC9, 0x2B, 0x0C, 0x2F, 0x24, 0xCC, 0x2C, + 0x32, 0x3A, 0xEB, 0xD7, 0xD8, 0xA9, 0x77, 0x6B, 0x52, 0x10, 0x2B, 0x08, 0x53, 0x36, 0x6E, 0x97, + 0xBC, 0x1C, 0xE1, 0xD9, 0x9C, 0x5E, 0xF7, 0x93, 0xCC, 0x8F, 0x63, 0x50, 0xD6, 0x07, 0x83, 0x35, + 0x5A, 0xAE, 0x6B, 0x65, 0xB4, 0x2C, 0x49, 0x0E, 0xA5, 0x5D, 0x93, 0x60, 0xF0, 0x46, 0xB9, 0xE0, + 0xDB, 0xA5, 0xE1, 0x6A, 0x9A, 0xDE, 0x63, 0xBE, 0x2B, 0xC7, 0xF8, 0xE8, 0x1D, 0xE9, 0xF2, 0xA5, + 0x36, 0xB6, 0x3E, 0x66, 0x23, 0xAC, 0xFA, 0x18, 0x4E, 0xCD, 0xDE, 0xC1, 0xA4, 0xE8, 0x39, 0x92, + 0x94, 0x9D, 0x0C, 0xBC, 0x9E, 0x92, 0x95, 0xEB, 0xDB, 0x68, 0x56, 0x47, 0x08, 0x8F, 0x54, 0xF4, + 0x88, 0x2C, 0x8F, 0xBE, 0x26, 0xB5, 0x2B, 0x22, 0x8F, 0x07, 0xA4, 0x74, 0x9D, 0x9E, 0xBC, 0x78, + 0xFE, 0xE6, 0xF9, 0xD9, 0xB4, 0x86, 0x44, 0xAF, 0xA6, 0x88, 0xD9, 0xD9, 0x8C, 0xC2, 0xCF, 0x90, + 0xE3, 0x91, 0x7B, 0xA1, 0xE8, 0xA7, 0xA6, 0x76, 0x55, 0x7A, 0x20, 0x23, 0x0C, 0xEF, 0x05, 0x44, + 0xF3, 0x76, 0x5D, 0xE1, 0x55, 0x8E, 0x82, 0x1B, 0x59, 0x0B, 0xE4, 0x93, 0x3E, 0xA3, 0xB5, 0x1B, + 0x35, 0xDF, 0x63, 0x43, 0x09, 0x8F, 0x9C, 0xFD, 0xD7, 0xB5, 0xE3, 0x76, 0xEB, 0x45, 0xE2, 0x30, + 0x27, 0x0A, 0xC4, 0x7D, 0x69, 0x82, 0xB6, 0xEB, 0x45, 0xC1, 0xA1, 0xF7, 0xC4, 0x1B, 0x2A, 0x8A, + 0xD0, 0xA9, 0xE9, 0x67, 0xA7, 0x26, 0xBE, 0x87, 0xF4, 0x44, 0xDA, 0xA8, 0x5A, 0xED, 0xB4, 0x79, + 0xF4, 0xE6, 0xE3, 0x60, 0xA2, 0x9A, 0x92, 0x3F, 0xE6, 0x8A, 0x49, 0xA8, 0x49, 0xDF, 0x4E, 0xAB, + 0x6E, 0x4B, 0x5F, 0x26, 0xBD, 0xE2, 0x86, 0x23, 0xC5, 0xF8, 0x57, 0x4D, 0xF7, 0xD9, 0x4D, 0x91, + 0x33, 0x04, 0xC4, 0x5A, 0xEA, 0x6C, 0x44, 0xCD, 0x06, 0x18, 0x7F, 0x30, 0xE0, 0xD0, 0x91, 0xFD, + 0x89, 0x92, 0xB3, 0x49, 0xF3, 0x15, 0xD2, 0xFB, 0xB5, 0x2D, 0x58, 0x1A, 0x2A, 0x53, 0x74, 0x89, + 0xF2, 0x90, 0x98, 0xE0, 0xE8, 0xBB, 0x66, 0xFB, 0x79, 0x64, 0xEF, 0x30, 0x81, 0xD3, 0x77, 0xD4, + 0x1A, 0xAC, 0x55, 0x50, 0xD7, 0x4E, 0x48, 0xD0, 0xEF, 0x3C, 0xA1, 0xF6, 0x45, 0xE2, 0x1B, 0x5D, + 0x78, 0x11, 0x5D, 0x90, 0x4C, 0x40, 0x39, 0x43, 0xB1, 0x72, 0x76, 0xD2, 0x35, 0xD8, 0x3B, 0x16, + 0xEB, 0x68, 0x79, 0x30, 0x86, 0xC6, 0x85, 0xF5, 0x7C, 0x97, 0x70, 0x3A, 0xB3, 0x60, 0x29, 0x3B, + 0xA4, 0x76, 0xE3, 0xE3, 0x65, 0xCF, 0x45, 0x45, 0x22, 0xCA, 0x5D, 0x73, 0x31, 0xC0, 0x99, 0xE2, + 0xBA, 0x16, 0x96, 0x81, 0xD0, 0xBB, 0xA8, 0xFB, 0x8E, 0x6B, 0xF2, 0x33, 0xBE, 0x8E, 0x48, 0xB8, + 0xCB, 0xAF, 0xC8, 0x49, 0xB5, 0x84, 0xC1, 0x63, 0x6C, 0xD0, 0x41, 0x62, 0x27, 0x81, 0x81, 0x54, + 0x6A, 0x20, 0x5B, 0xEE, 0xD0, 0x92, 0x2C, 0xD7, 0x49, 0x3D, 0x75, 0x9B, 0xD0, 0xFB, 0xC0, 0x28, + 0xDC, 0xB2, 0xF2, 0x04, 0x85, 0x9A, 0x21, 0x77, 0xC3, 0x41, 0x20, 0xAD, 0xB2, 0xEB, 0x69, 0xCF, + 0x8A, 0x96, 0x41, 0xB0, 0x05, 0xFC, 0x82, 0x8A, 0x92, 0xE7, 0xC0, 0x34, 0x25, 0x56, 0x9E, 0xF5, + 0x58, 0xA4, 0x6B, 0x8D, 0x2B, 0x5E, 0xC0, 0x1F, 0x65, 0xDB, 0x0A, 0x76, 0x03, 0x2A, 0x7F, 0x0E, + 0xC0, 0x5F, 0xB3, 0x2E, 0xE2, 0x54, 0x5A, 0xB1, 0xF9, 0x1B, 0x9C, 0xB6, 0xFC, 0xA2, 0x15, 0xC4, + 0xEA, 0xDA, 0xC8, 0x04, 0x12, 0x3D, 0x24, 0x98, 0xEA, 0x68, 0x68, 0xDA, 0x7B, 0x37, 0xCC, 0x33, + 0xB1, 0x20, 0x73, 0x1E, 0x9D, 0x04, 0x9A, 0x15, 0xE4, 0x46, 0xB1, 0xF1, 0x67, 0xD6, 0x1E, 0x20, + 0xF7, 0x0E, 0x15, 0xFE, 0x0C, 0x70, 0x90, 0x76, 0xA0, 0xA5, 0xEA, 0x4C, 0x4C, 0x55, 0x69, 0x2B, + 0x8F, 0x8D, 0x6B, 0xE5, 0x31, 0x30, 0x17, 0x49, 0x36, 0xAE, 0x4D, 0x14, 0x25, 0x39, 0x07, 0xDE, + 0x4A, 0x6C, 0xA4, 0x4C, 0xB0, 0x2C, 0xE6, 0x58, 0x72, 0x69, 0xFE, 0x48, 0x32, 0x30, 0xB7, 0xB2, + 0xD6, 0x1B, 0x09, 0xB8, 0x8B, 0x91, 0xA0, 0xA6, 0xBF, 0x14, 0x2B, 0x16, 0x79, 0x66, 0xF9, 0x57, + 0x5C, 0x0A, 0xF5, 0x9A, 0x04, 0x59, 0x21, 0x74, 0xFA, 0x74, 0x59, 0x25, 0x44, 0x1E, 0xFA, 0xC5, + 0x13, 0x46, 0x2D, 0x6C, 0xC1, 0xD1, 0xA4, 0x2B, 0x83, 0xC0, 0xEB, 0x1C, 0x65, 0xAC, 0x11, 0x8D, + 0xBD, 0xF3, 0xF8, 0x75, 0x42, 0x56, 0xE1, 0x04, 0xBF, 0x56, 0x24, 0xE9, 0x18, 0x8F, 0x7D, 0x93, + 0xB8, 0x8F, 0xD8, 0x99, 0x0B, 0xC4, 0x13, 0xC7, 0xBB, 0x21, 0xDD, 0xE4, 0x15, 0x39, 0xA6, 0x4D, + 0xBD, 0xAB, 0x53, 0xC8, 0x67, 0xE2, 0x8E, 0x72, 0xB2, 0xA4, 0x05, 0xB0, 0x0F, 0x5C, 0x17, 0xE7, + 0x4C, 0x0E, 0x90, 0x05, 0x2B, 0x6F, 0xC7, 0x6B, 0xBE, 0x3A, 0x29, 0x0A, 0x87, 0x7F, 0x94, 0x4F, + 0xFE, 0xA5, 0x94, 0x40, 0xB6, 0x37, 0x9C, 0xC5, 0x2C, 0xB2, 0xC9, 0xDB, 0xB5, 0x0D, 0x46, 0x7E, + 0xC7, 0x84, 0xC4, 0x6E, 0xE9, 0x86, 0x21, 0x53, 0xAF, 0x38, 0x68, 0x2B, 0x18, 0xC8, 0x1E, 0x78, + 0x77, 0xD2, 0x6E, 0xF0, 0x96, 0x59, 0x25, 0xB6, 0x3B, 0xDB, 0xF4, 0x4B, 0x7F, 0x48, 0x6D, 0x3B, + 0x4C, 0x1E, 0x7B, 0xC3, 0x84, 0x7E, 0x2C, 0x23, 0xEC, 0x64, 0xD8, 0xC7, 0x0F, 0x50, 0x8B, 0x44, + 0x83, 0x6C, 0x92, 0x34, 0x0F, 0x94, 0xE7, 0x5F, 0x3F, 0xC7, 0x27, 0xA5, 0x75, 0x1E, 0x0D, 0x8C, + 0x72, 0x7A, 0x31, 0x10, 0x20, 0x4A, 0x3F, 0xCA, 0x4A, 0xDB, 0xBE, 0x54, 0x01, 0x88, 0xDF, 0xEC, + 0x7A, 0xAC, 0x62, 0x7F, 0x5F, 0x8B, 0x73, 0x24, 0x63, 0x1D, 0x4C, 0x36, 0xFA, 0xC8, 0x8C, 0x4A, + 0x1D, 0x0A, 0x65, 0x5D, 0xCA, 0xC0, 0xDF, 0xD3, 0xE6, 0xA6, 0xFC, 0x4D, 0x92, 0xF6, 0x86, 0x1D, + 0x29, 0x8A, 0x39, 0x9F, 0x72, 0x3E, 0x2A, 0x76, 0x24, 0xCC, 0x43, 0xCA, 0xE9, 0xC5, 0x3B, 0x1E, + 0x86, 0x60, 0x9B, 0x36, 0x77, 0x80, 0x79, 0x7B, 0x60, 0x38, 0x5A, 0x78, 0x64, 0x34, 0x2E, 0x69, + 0x1A, 0xC2, 0xA7, 0x0D, 0x6D, 0x67, 0xF3, 0xEA, 0xB8, 0x9A, 0x24, 0x07, 0x2E, 0x54, 0x2D, 0xD2, + 0x5C, 0x4B, 0x0C, 0x80, 0xD3, 0x9A, 0xC8, 0x86, 0x5F, 0x26, 0x32, 0x80, 0xD7, 0x79, 0x48, 0xD9, + 0xD1, 0x1F, 0xB5, 0x15, 0x3A, 0x0C, 0xDA, 0x56, 0x79, 0x00, 0x12, 0xD8, 0x40, 0x37, 0xD9, 0x3E, + 0x60, 0x88, 0xCA, 0xC9, 0x78, 0x36, 0xFD, 0xC1, 0xC8, 0x16, 0x64, 0x94, 0x3E, 0xDE, 0xCC, 0xE5, + 0x79, 0x13, 0x43, 0x17, 0x38, 0x27, 0x2D, 0x02, 0x88, 0x28, 0xD0, 0xB0, 0xA9, 0x68, 0x63, 0xF3, + 0x28, 0xAD, 0x68, 0x3B, 0x72, 0x1B, 0x50, 0x94, 0xD6, 0x36, 0x9B, 0x7E, 0x67, 0xE2, 0xB0, 0x75, + 0xDC, 0x4F, 0x4D, 0x8C, 0x75, 0x49, 0xE5, 0x05, 0x3F, 0x51, 0xD3, 0xEB, 0x1F, 0x57, 0xD3, 0x61, + 0xEE, 0x55, 0xCA, 0x07, 0x33, 0x06, 0xFA, 0x49, 0xE5, 0x6E, 0x9E, 0xCE, 0xE5, 0xD1, 0xEE, 0x9D, + 0x7D, 0xB2, 0xA2, 0xA0, 0xE2, 0x9C, 0x93, 0x65, 0x3D, 0xFA, 0xB1, 0xE9, 0xA0, 0x43, 0x2F, 0x8E, + 0x95, 0xCA, 0xA1, 0xB1, 0x9F, 0xBA, 0x71, 0x7F, 0x6C, 0x47, 0x01, 0x5A, 0xE9, 0xBB, 0x61, 0x42, + 0xD8, 0xB8, 0x4D, 0xDA, 0x56, 0xCA, 0x21, 0x45, 0x1A, 0x04, 0xF6, 0x30, 0x80, 0x5F, 0xA8, 0x71, + 0xF7, 0xF9, 0x96, 0xFC, 0x4F, 0xBA, 0xF8, 0x93, 0x9E, 0xCF, 0xA6, 0x95, 0xBB, 0xEC, 0x27, 0xF8, + 0x8A, 0x7F, 0xAB, 0x3B, 0x4C, 0x8D, 0x75, 0x9A, 0x1A, 0xF7, 0xC4, 0xFD, 0xB1, 0x15, 0x65, 0xE8, + 0x0A, 0xDB, 0xAF, 0x23, 0x7A, 0x88, 0xBE, 0x84, 0x60, 0xFE, 0x53, 0x12, 0xE3, 0xD2, 0xE6, 0x72, + 0x49, 0x5E, 0xA2, 0xD8, 0xB5, 0x84, 0xF7, 0x17, 0xF7, 0x93, 0x92, 0x1A, 0x47, 0x40, 0x2B, 0xA2, + 0xDD, 0x30, 0x25, 0xF3, 0xC4, 0xC4, 0x4B, 0x20, 0xF5, 0xFF, 0x4B, 0x73, 0xEE, 0xBB, 0x2D, 0x68, + 0xBA, 0xE3, 0xC9, 0xD3, 0xA2, 0x11, 0xE4, 0xB7, 0x67, 0xA1, 0x94, 0x5E, 0x91, 0x3B, 0x7A, 0xD2, + 0xF4, 0x76, 0x1E, 0x7C, 0xF1, 0x5B, 0x87, 0x61, 0xC2, 0x2F, 0x4C, 0x88, 0xA4, 0x65, 0xEE, 0xDA, + 0x60, 0xC7, 0xDA, 0xC7, 0x55, 0xCB, 0x0A, 0x54, 0x36, 0x8C, 0xBA, 0x11, 0xCD, 0x1F, 0x58, 0x58, + 0x68, 0x39, 0xA4, 0xAC, 0x95, 0xE9, 0x9C, 0xEE, 0x74, 0x49, 0x32, 0xCB, 0x32, 0xBD, 0x3C, 0x40, + 0x3A, 0x38, 0x3F, 0x70, 0x57, 0x5B, 0xBD, 0x04, 0xF3, 0x36, 0x52, 0x75, 0xD7, 0x7C, 0x43, 0xC4, + 0xBD, 0x5E, 0x4F, 0xBD, 0x6B, 0x71, 0x09, 0x7A, 0x7C, 0xD6, 0x10, 0xB9, 0xD7, 0x2D, 0x09, 0x49, + 0xF4, 0xDE, 0x39, 0x4F, 0x7A, 0x0D, 0x60, 0xCC, 0x6C, 0x0C, 0x9F, 0x93, 0x87, 0x0C, 0x9F, 0x81, + 0x91, 0x93, 0xA8, 0x6D, 0xD6, 0x4C, 0x40, 0x5D, 0xDC, 0xF1, 0xCA, 0x19, 0x26, 0x82, 0x35, 0xBA, + 0x6A, 0x30, 0x99, 0xD4, 0x05, 0x3F, 0xB0, 0x60, 0x95, 0x5C, 0x30, 0xA7, 0xDF, 0x11, 0x55, 0x1A, + 0x1D, 0xBA, 0x56, 0xB6, 0x60, 0x9A, 0x1D, 0xC4, 0xA6, 0x37, 0x38, 0xD2, 0x1E, 0xC2, 0x40, 0x92, + 0x3E, 0x53, 0x8E, 0x56, 0xED, 0x8A, 0x99, 0xD7, 0x84, 0x3E, 0x08, 0xD2, 0xED, 0xC2, 0xB8, 0x1E, + 0xDD, 0x8D, 0x81, 0xB4, 0x96, 0xC7, 0x1C, 0x1F, 0x76, 0x47, 0xC6, 0x7B, 0x8C, 0xBD, 0xEE, 0x1A, + 0x5A, 0x91, 0x37, 0xBA, 0xDE, 0x7E, 0xD4, 0xF9, 0x86, 0x37, 0x79, 0x39, 0xAA, 0x37, 0x79, 0xE8, + 0xC9, 0x90, 0x47, 0x68, 0x4B, 0x47, 0x21, 0x28, 0xE3, 0xDC, 0xAD, 0x15, 0x7A, 0x12, 0x5F, 0xE8, + 0xD8, 0xE7, 0xB4, 0x9B, 0x51, 0x9C, 0x7E, 0x3C, 0x3D, 0x01, 0xE4, 0x66, 0xDD, 0x73, 0x26, 0xA3, + 0x29, 0xDA, 0x8A, 0xA5, 0x43, 0x6F, 0x92, 0x67, 0xF5, 0x1F, 0xE9, 0x34, 0x57, 0x80, 0x44, 0x76, + 0x89, 0x04, 0x2B, 0x60, 0xC3, 0x02, 0xF0, 0xA9, 0x9E, 0xC3, 0x69, 0x22, 0x1F, 0xE4, 0x30, 0x2A, + 0x25, 0x6B, 0x60, 0x7B, 0x71, 0x9A, 0x37, 0x84, 0x24, 0xF1, 0x4E, 0xFD, 0x82, 0x0E, 0x7C, 0xDC, + 0xD7, 0x25, 0x87, 0x6C, 0x1B, 0xE3, 0xE7, 0xB2, 0x62, 0x1E, 0xD6, 0x12, 0x9D, 0xBB, 0x5D, 0x74, + 0xB7, 0xAE, 0xD9, 0x9A, 0xAB, 0xB5, 0x93, 0xC1, 0xFA, 0x36, 0x90, 0xF7, 0x6B, 0x87, 0xFA, 0x7A, + 0xED, 0xEB, 0x24, 0xEA, 0x2C, 0x60, 0xFB, 0x6E, 0xAA, 0xA1, 0xB5, 0x20, 0x0A, 0xD6, 0xAD, 0xF3, + 0x3A, 0x41, 0x6F, 0x8A, 0x6C, 0xEC, 0x6A, 0xE8, 0x24, 0x90, 0x1D, 0x3C, 0xC1, 0xED, 0x59, 0xF0, + 0x02, 0x8B, 0x53, 0x92, 0x9D, 0x30, 0xC8, 0x7D, 0x65, 0x5A, 0xE8, 0x52, 0xBA, 0x74, 0x95, 0xD1, + 0xB0, 0xE2, 0x55, 0x40, 0xCF, 0x03, 0xEA, 0x5B, 0xAC, 0x08, 0x0A, 0x1B, 0x8C, 0x2A, 0xFC, 0xB1, + 0x59, 0x8B, 0x48, 0x9B, 0x0D, 0x91, 0x6D, 0xB7, 0x93, 0x2D, 0xA7, 0x6C, 0xF6, 0x7D, 0xAC, 0x57, + 0x8D, 0xA9, 0xC8, 0x12, 0x65, 0x8D, 0x55, 0x7B, 0x01, 0x8F, 0xDE, 0x8A, 0x5F, 0xFE, 0x96, 0xF4, + 0x52, 0xE1, 0xBA, 0x33, 0xAA, 0x4A, 0xF9, 0x2A, 0xCD, 0x6B, 0xA6, 0x8F, 0xC9, 0xC2, 0x80, 0x19, + 0xAB, 0x38, 0xCA, 0x68, 0xE0, 0x5A, 0x8B, 0xCC, 0x04, 0x29, 0xFB, 0x9E, 0x26, 0x17, 0xDF, 0xF8, + 0x01, 0xC1, 0xAE, 0xAC, 0x41, 0x9D, 0xEB, 0xA2, 0x80, 0x74, 0xF0, 0xDE, 0xEB, 0xF4, 0x36, 0x59, + 0x7D, 0x9A, 0xDF, 0xF2, 0x64, 0x95, 0x3E, 0xEA, 0xFC, 0x56, 0x21, 0x9B, 0x18, 0xE1, 0x57, 0x2A, + 0x5C, 0x21, 0xCC, 0xA6, 0xCA, 0xFC, 0x34, 0x1E, 0xFB, 0x2D, 0xBC, 0x4A, 0xB3, 0xBF, 0xF1, 0x47, + 0x4E, 0x1F, 0xF1, 0xAD, 0xFC, 0xB0, 0xF1, 0x4E, 0xAC, 0x2E, 0x17, 0xC5, 0x02, 0x3D, 0x55, 0x75, + 0xE8, 0xB8, 0xC2, 0x2D, 0x93, 0x0A, 0xA7, 0x54, 0x1E, 0x38, 0xCE, 0x89, 0xE2, 0x49, 0x3C, 0xC4, + 0x25, 0x5D, 0xE7, 0xAD, 0xF3, 0xCF, 0x1A, 0xE2, 0xAA, 0xFB, 0x86, 0x2F, 0x26, 0xA9, 0x9B, 0xB4, + 0xAF, 0xD4, 0x99, 0xF7, 0x26, 0xC0, 0xA8, 0xEF, 0x70, 0x6C, 0xAB, 0x4C, 0xBA, 0x74, 0x46, 0x50, + 0x90, 0xFF, 0xEA, 0xED, 0xDA, 0x9B, 0xDB, 0x36, 0x8E, 0xF8, 0xFF, 0xFD, 0x14, 0x12, 0x9A, 0x7A, + 0x00, 0xF3, 0x24, 0x51, 0x4E, 0xDA, 0x69, 0x21, 0xA3, 0x9C, 0xD4, 0xB5, 0x93, 0xB4, 0x75, 0xEC, + 0xDA, 0x4A, 0x9B, 0x96, 0x66, 0x3D, 0xB0, 0x08, 0x49, 0x48, 0x29, 0x40, 0x05, 0x40, 0x4B, 0x8E, + 0xC8, 0xEF, 0xDE, 0xDF, 0xEE, 0xED, 0xBD, 0x00, 0x50, 0x76, 0x9F, 0x99, 0x58, 0x04, 0xEE, 0x8D, + 0xBB, 0xBD, 0xBD, 0xBD, 0x7D, 0xEE, 0x7B, 0xCD, 0xD9, 0x5E, 0x0B, 0xDD, 0x36, 0xD9, 0xF5, 0xEA, + 0xA9, 0x24, 0x53, 0x4E, 0x32, 0xDC, 0xC8, 0xA2, 0x6B, 0x89, 0x69, 0x98, 0xE6, 0xEF, 0xD8, 0xB3, + 0x6B, 0x71, 0xB2, 0x2A, 0xCE, 0x3B, 0xC4, 0xCA, 0xA0, 0xFF, 0xAE, 0x6F, 0x4F, 0xF8, 0x7B, 0xD3, + 0x5F, 0x4C, 0xF1, 0x78, 0x85, 0x8E, 0xCB, 0xEA, 0x00, 0x42, 0x97, 0x94, 0x72, 0xAE, 0xF3, 0x25, + 0xB9, 0x8A, 0x4B, 0xA7, 0x27, 0xEF, 0xEA, 0x66, 0x09, 0x84, 0x30, 0x8D, 0xD4, 0x6A, 0x67, 0xF3, + 0x26, 0x74, 0xE5, 0x89, 0xD8, 0x66, 0xA6, 0x6C, 0x0F, 0x8A, 0xAA, 0xB7, 0x07, 0x6D, 0xF9, 0x23, + 0xB5, 0xA3, 0x5B, 0x39, 0x40, 0xCA, 0x09, 0x31, 0xCD, 0xCF, 0x57, 0xB0, 0xCF, 0x6C, 0xD9, 0xC1, + 0x9C, 0xF4, 0x9C, 0xE6, 0xEB, 0xAE, 0x36, 0x9D, 0xF9, 0x23, 0xF0, 0xC7, 0xF9, 0xB3, 0x13, 0x1E, + 0xDF, 0xCF, 0x08, 0x2F, 0x05, 0x14, 0xDA, 0x3A, 0x24, 0xD8, 0x56, 0xC2, 0x16, 0xCA, 0x9E, 0x0C, + 0x77, 0x1F, 0xF2, 0xAA, 0x2C, 0x42, 0x1B, 0x7C, 0x09, 0x44, 0x7B, 0x00, 0xE8, 0xE3, 0x47, 0x74, + 0x10, 0x82, 0x45, 0xA7, 0xC7, 0x42, 0x16, 0xF4, 0x89, 0xFD, 0xDC, 0x86, 0x0C, 0xEC, 0xB3, 0x08, + 0xBD, 0x47, 0x80, 0xAC, 0xCF, 0x7F, 0xC1, 0x45, 0x25, 0x19, 0xF7, 0x27, 0x97, 0xC2, 0xA3, 0x74, + 0xF5, 0xCC, 0xEC, 0x64, 0x91, 0x99, 0x7D, 0xA0, 0x58, 0xD3, 0xD5, 0xEA, 0x50, 0x0B, 0x75, 0x18, + 0xAE, 0x8E, 0x3E, 0x47, 0x3B, 0xE1, 0x3D, 0x7A, 0x9D, 0x88, 0xA7, 0x3B, 0x8F, 0xF9, 0xD5, 0x79, + 0xE7, 0xBF, 0x8E, 0xBB, 0x5B, 0xAF, 0x71, 0x82, 0xB1, 0x87, 0xA2, 0x67, 0x10, 0xE7, 0x53, 0x7E, + 0xB2, 0x1D, 0xD8, 0x3B, 0x67, 0xE3, 0x46, 0x9A, 0x6A, 0xB5, 0x23, 0xE3, 0x64, 0x65, 0x8D, 0x9F, + 0xCD, 0xA7, 0x90, 0x53, 0x8E, 0x0B, 0xEE, 0xED, 0xC9, 0xAA, 0xBC, 0xCE, 0x22, 0x09, 0xFA, 0x40, + 0xCB, 0x09, 0xB0, 0x08, 0x6F, 0xE3, 0x3B, 0xAA, 0x90, 0x11, 0x23, 0x3B, 0x94, 0xE0, 0xBB, 0x39, + 0x2F, 0x45, 0xD8, 0x0E, 0xA6, 0x65, 0xBC, 0x3B, 0xE7, 0x01, 0xE6, 0x83, 0xBA, 0x43, 0xC9, 0xD7, + 0x0C, 0x51, 0xAF, 0x8A, 0x55, 0xB9, 0xC3, 0x39, 0x3B, 0x59, 0x58, 0x01, 0xCD, 0x85, 0x78, 0x64, + 0x47, 0xB9, 0x5A, 0xCA, 0x99, 0xE8, 0x9F, 0x3B, 0x8A, 0x55, 0x20, 0x05, 0xA4, 0xC3, 0xE7, 0x16, + 0x44, 0x76, 0x94, 0x05, 0x6D, 0xA8, 0x21, 0x5B, 0x8F, 0x75, 0xD7, 0x08, 0x4B, 0xD7, 0xE4, 0x69, + 0xF3, 0xDB, 0x12, 0x0B, 0xD0, 0xA2, 0x4C, 0xDB, 0x27, 0x33, 0x44, 0xFA, 0x71, 0x12, 0x04, 0xAD, + 0xCC, 0x89, 0xC3, 0x37, 0x58, 0x3E, 0xC3, 0x1A, 0x57, 0xDD, 0x48, 0x56, 0x83, 0xF4, 0x6A, 0x17, + 0x28, 0x14, 0xFF, 0x2A, 0xE2, 0x90, 0xED, 0x7C, 0x86, 0xAF, 0xCC, 0xAF, 0x21, 0x3B, 0x6D, 0x0B, + 0x40, 0x61, 0x4E, 0x10, 0xDE, 0xF5, 0x9B, 0x72, 0x5B, 0x7A, 0x0F, 0x4D, 0x95, 0x4B, 0x57, 0xE4, + 0xB2, 0xD0, 0x3B, 0x0B, 0x59, 0x91, 0xAA, 0x7A, 0x89, 0xBF, 0xE2, 0xC4, 0xBE, 0xD5, 0x3C, 0xE3, + 0x95, 0xC1, 0xEE, 0xEF, 0x5D, 0xD7, 0xBA, 0xF0, 0xB5, 0x02, 0x34, 0x8C, 0x21, 0x82, 0x8E, 0xF8, + 0x20, 0xBC, 0x79, 0xBE, 0xC1, 0x4C, 0x34, 0xD2, 0xB3, 0x3A, 0x9E, 0x26, 0x13, 0x2F, 0x55, 0x0F, + 0x1F, 0xAE, 0x36, 0x78, 0xA7, 0x8E, 0xE7, 0x6A, 0xD7, 0x1B, 0xB6, 0x00, 0x6D, 0x70, 0xD9, 0xDE, + 0x5F, 0x73, 0x9B, 0xFD, 0xCD, 0x5D, 0x24, 0xE8, 0x9A, 0x0C, 0x46, 0xB6, 0xB1, 0x46, 0x54, 0xBF, + 0x21, 0x77, 0x1E, 0x7F, 0x2E, 0xDE, 0xFD, 0xBD, 0x24, 0x4F, 0x1E, 0xCF, 0xEB, 0x1F, 0xF1, 0xF7, + 0xAA, 0x8D, 0x16, 0xEA, 0xB3, 0x62, 0x7C, 0xC9, 0x64, 0x97, 0xA9, 0xB7, 0xA1, 0xDF, 0x83, 0x1F, + 0x0B, 0x47, 0xAE, 0xB0, 0x45, 0x3F, 0x9D, 0xBF, 0xED, 0x9C, 0xB4, 0x29, 0xDF, 0x16, 0xF8, 0xF1, + 0x1D, 0x21, 0x16, 0x44, 0x32, 0x7C, 0x56, 0xE0, 0x56, 0xC2, 0x59, 0x63, 0x04, 0x0F, 0xEE, 0x11, + 0xA1, 0xB7, 0xC8, 0x49, 0xE1, 0xAC, 0x62, 0xAA, 0xEC, 0x37, 0xC5, 0xD0, 0x1B, 0x1E, 0xFB, 0x21, + 0x47, 0x0E, 0xA8, 0xF6, 0x49, 0x97, 0x70, 0x0F, 0x89, 0x65, 0xF1, 0xF1, 0x91, 0x5F, 0x68, 0x14, + 0xF5, 0x1D, 0x1B, 0xE2, 0x91, 0x43, 0x80, 0x0D, 0x83, 0x2D, 0xE8, 0xB0, 0x83, 0xB3, 0x79, 0x91, + 0x43, 0xFB, 0x6F, 0x92, 0x1C, 0xA9, 0xEF, 0x29, 0xFB, 0x00, 0x4E, 0x2D, 0xFF, 0x84, 0x2F, 0xB4, + 0xD0, 0xE8, 0x61, 0xD2, 0xF7, 0x65, 0x5B, 0xBE, 0x2B, 0x57, 0x25, 0xAE, 0x7F, 0x11, 0x8C, 0xE5, + 0x97, 0x45, 0x15, 0x29, 0x73, 0xFA, 0x08, 0x98, 0x6C, 0xD5, 0x57, 0xA8, 0x8C, 0x33, 0x14, 0x94, + 0xEC, 0x6B, 0x88, 0xEA, 0xE8, 0x34, 0xA1, 0x68, 0xE7, 0xE7, 0xC0, 0x3A, 0x7F, 0xE6, 0x85, 0x49, + 0xA3, 0x2F, 0xA6, 0xD3, 0xC8, 0x9B, 0xC0, 0xBF, 0x14, 0xBD, 0x40, 0x92, 0x46, 0x87, 0x3E, 0xB1, + 0xF4, 0xD3, 0x8C, 0x51, 0x2F, 0x48, 0x02, 0xC8, 0x6F, 0x28, 0xF0, 0xDB, 0x41, 0x0C, 0xEA, 0x67, + 0x0A, 0x69, 0x50, 0xDC, 0xB0, 0x1F, 0x0F, 0x76, 0xDA, 0x91, 0x76, 0x0E, 0x6D, 0xFF, 0x71, 0xCC, + 0x86, 0x2F, 0x8B, 0x74, 0x48, 0x5F, 0x76, 0xC8, 0x78, 0x9C, 0x4E, 0x95, 0x76, 0x73, 0x3A, 0xD5, + 0x51, 0xFE, 0x32, 0xD2, 0xB2, 0x97, 0x9D, 0x13, 0xA5, 0x06, 0x4D, 0x46, 0x56, 0x00, 0x31, 0x3D, + 0xE1, 0xDB, 0x52, 0xFE, 0xF8, 0x8B, 0x93, 0x7C, 0x92, 0x3D, 0x4A, 0x22, 0x7D, 0x6E, 0x19, 0x8F, + 0x14, 0xEB, 0x89, 0x75, 0x6A, 0x52, 0x4D, 0xAA, 0x62, 0x9E, 0xB3, 0xF3, 0xD3, 0x92, 0xF8, 0xB6, + 0xB3, 0xD8, 0xB4, 0x66, 0x0B, 0x1F, 0xD8, 0xC2, 0x91, 0x9C, 0xB9, 0x51, 0x58, 0xC9, 0xB4, 0xBE, + 0x3F, 0xA8, 0x60, 0x86, 0xC8, 0xE5, 0xC9, 0x4F, 0x35, 0x7F, 0x93, 0xD4, 0x63, 0x1F, 0xEF, 0xF7, + 0xB7, 0xED, 0x52, 0xD9, 0xD9, 0xE3, 0x7A, 0xF2, 0x49, 0x2D, 0xA7, 0xED, 0xA7, 0x95, 0x33, 0x6B, + 0xB6, 0xDF, 0x90, 0x4F, 0xDD, 0xAC, 0xD6, 0x33, 0xE3, 0x2D, 0x1F, 0x3F, 0x9E, 0x15, 0x25, 0x79, + 0x5F, 0x8D, 0xF4, 0x86, 0x8D, 0x26, 0xDD, 0x10, 0xEA, 0x3B, 0x0B, 0xF5, 0x8B, 0x83, 0xFA, 0x60, + 0x7D, 0xD0, 0x1E, 0x90, 0xF3, 0x13, 0x5A, 0x75, 0xB5, 0x76, 0xEB, 0xFC, 0xBB, 0x1E, 0xEC, 0x30, + 0x45, 0x4C, 0xCC, 0x63, 0xD0, 0xB4, 0x83, 0x63, 0x2B, 0x26, 0x52, 0x98, 0xBC, 0xD0, 0x38, 0xC2, + 0x28, 0xF0, 0x45, 0x63, 0x2B, 0x44, 0x24, 0xF8, 0x69, 0xC8, 0x4F, 0x52, 0x09, 0x74, 0x25, 0x14, + 0x39, 0xDE, 0xDB, 0xEC, 0xD3, 0x46, 0x4C, 0x00, 0xE5, 0xE8, 0xE7, 0x5E, 0xC8, 0xE8, 0xFC, 0x04, + 0x80, 0x48, 0x84, 0x57, 0x24, 0x42, 0x87, 0x1D, 0x63, 0x85, 0x28, 0x60, 0xB3, 0x41, 0xD6, 0xD8, + 0x81, 0x45, 0xB9, 0x5A, 0x30, 0xDB, 0x90, 0xA6, 0xA1, 0x6E, 0x8E, 0x54, 0x84, 0x51, 0xC3, 0x91, + 0x26, 0xE8, 0x9B, 0xFD, 0x57, 0xAD, 0xCA, 0x71, 0xA7, 0x3B, 0xF2, 0x99, 0x28, 0x54, 0x30, 0xA6, + 0x66, 0x3D, 0x98, 0x57, 0x90, 0x96, 0xA3, 0x7D, 0xDF, 0xFB, 0xDF, 0x27, 0xCF, 0x17, 0x39, 0xAA, + 0xD1, 0x72, 0x75, 0xB6, 0x1B, 0x65, 0x97, 0x28, 0x6C, 0x8B, 0xE6, 0x8F, 0x89, 0xD7, 0x70, 0x62, + 0x36, 0x28, 0x90, 0x62, 0x39, 0xB6, 0xEB, 0x14, 0x39, 0xFB, 0xCB, 0x93, 0x09, 0xED, 0x6C, 0xB7, + 0xDC, 0xBF, 0x0F, 0x0C, 0xD9, 0x9D, 0x6A, 0x21, 0x32, 0x9C, 0x1E, 0x1B, 0xC7, 0xF8, 0xF4, 0xCA, + 0x79, 0x22, 0x11, 0x0C, 0x5C, 0x7B, 0x50, 0xBB, 0xAB, 0x81, 0x9B, 0x08, 0x8F, 0xF1, 0x85, 0x61, + 0xA0, 0xCF, 0x68, 0xE4, 0xBB, 0x7A, 0xED, 0x23, 0x29, 0x6C, 0xB9, 0x51, 0x11, 0xEF, 0xE3, 0x59, + 0x74, 0x1C, 0xA5, 0x15, 0x6B, 0x5A, 0x5A, 0xEF, 0x40, 0xE9, 0x5D, 0x5E, 0x95, 0x57, 0xAC, 0x3C, + 0xF5, 0x0D, 0x30, 0x20, 0x3F, 0xB0, 0xE6, 0xB9, 0x56, 0x57, 0x5A, 0xAD, 0xAF, 0xDC, 0x2B, 0x58, + 0x31, 0xAB, 0x17, 0xBA, 0x65, 0x7E, 0x5D, 0x15, 0xB7, 0x5F, 0xC1, 0x8F, 0xB7, 0x79, 0x7E, 0x7D, + 0xD9, 0x40, 0xDC, 0x8F, 0x37, 0x1F, 0x77, 0xE2, 0xED, 0xA2, 0x29, 0x97, 0x5F, 0xE2, 0x74, 0x32, + 0xCF, 0x4F, 0xB8, 0xD5, 0xF0, 0xED, 0x69, 0xB5, 0x0C, 0x13, 0x70, 0x33, 0x6F, 0x6C, 0xED, 0x57, + 0xE8, 0xC4, 0x3D, 0xA2, 0xAC, 0xF7, 0x66, 0x0B, 0x12, 0xD0, 0x7C, 0x6D, 0x3B, 0xAD, 0xDD, 0x38, + 0x79, 0xA9, 0xF4, 0xC3, 0x35, 0xF8, 0xF3, 0x5A, 0x8D, 0xA9, 0x84, 0xFD, 0x0C, 0x3F, 0xFD, 0xF8, + 0x0D, 0xE9, 0x99, 0xF3, 0x53, 0x5D, 0x5F, 0xB1, 0x66, 0x93, 0x39, 0x12, 0xC9, 0xE0, 0x92, 0x4E, + 0xD0, 0x31, 0xF5, 0x13, 0xAD, 0x47, 0xF2, 0x79, 0x4F, 0x68, 0xF4, 0xCB, 0xDE, 0xBB, 0x90, 0x4C, + 0x9E, 0x57, 0x28, 0xD5, 0xB2, 0xEB, 0x58, 0x60, 0xF1, 0xEF, 0xED, 0x1D, 0x55, 0xAD, 0xCC, 0x65, + 0x99, 0x75, 0xFC, 0x99, 0x29, 0x80, 0x43, 0xBA, 0x4D, 0xB4, 0xC4, 0xDB, 0xC0, 0x00, 0x3B, 0x72, + 0xF2, 0x5E, 0x5B, 0xCF, 0xBF, 0x84, 0xDB, 0xAC, 0xD8, 0x40, 0x80, 0x90, 0xA8, 0xE4, 0x47, 0x27, + 0x0B, 0x29, 0xB3, 0x5C, 0x0B, 0xF2, 0x65, 0x0B, 0xCD, 0xCA, 0x74, 0x45, 0xE6, 0x2B, 0x8E, 0x6F, + 0x07, 0xF9, 0x95, 0xB5, 0xCA, 0x4E, 0x68, 0x1B, 0xD9, 0x73, 0xAD, 0x4A, 0x38, 0xF8, 0xCA, 0x31, + 0x4B, 0x2D, 0x32, 0x71, 0x76, 0x55, 0x12, 0xB6, 0x89, 0x2A, 0x06, 0x22, 0x40, 0x3F, 0xF3, 0x32, + 0xD9, 0x4F, 0x95, 0xC6, 0xFF, 0x26, 0x07, 0x7D, 0x43, 0xA0, 0x44, 0xDF, 0x54, 0x4D, 0xB2, 0x12, + 0xCD, 0x0C, 0xDD, 0x58, 0xB5, 0xCE, 0x8D, 0x55, 0x32, 0xA4, 0xFF, 0x81, 0x2C, 0xA8, 0x11, 0x3A, + 0x3E, 0xF1, 0xD3, 0x39, 0xC7, 0x9B, 0x8E, 0xFE, 0x27, 0x8C, 0x12, 0xD3, 0xD7, 0x64, 0xF8, 0xEC, + 0xCB, 0xA2, 0x29, 0x69, 0x3B, 0xD2, 0x44, 0xB4, 0xBD, 0x89, 0xC8, 0x58, 0x28, 0x94, 0x6B, 0xE3, + 0x6B, 0x5E, 0x47, 0xAA, 0xB9, 0x9E, 0xAD, 0x28, 0xC5, 0xE9, 0xB2, 0x93, 0x53, 0x0E, 0x6E, 0x0E, + 0x1F, 0xAE, 0xF7, 0xCA, 0x00, 0x00, 0xFA, 0x2B, 0xEA, 0x8E, 0x7E, 0xB7, 0xAC, 0xFE, 0x3A, 0xC6, + 0xF7, 0x2F, 0x64, 0x12, 0xAC, 0x9A, 0xBF, 0x58, 0x53, 0x55, 0x59, 0x76, 0x82, 0x71, 0xEE, 0xE8, + 0x10, 0x3C, 0xFB, 0x58, 0x6A, 0xAE, 0x20, 0xCB, 0xD6, 0x79, 0x1D, 0x21, 0xB3, 0xAF, 0x0A, 0x2E, + 0xF4, 0x15, 0x87, 0x56, 0x55, 0x7A, 0xEB, 0xE3, 0x1C, 0x99, 0xC5, 0xB5, 0x8F, 0xD6, 0xCA, 0x04, + 0x8D, 0xEB, 0xAC, 0xB2, 0x7D, 0x46, 0x18, 0x88, 0xBC, 0x2C, 0xCC, 0x6A, 0xCC, 0x73, 0x4A, 0x8E, + 0x6C, 0x3D, 0x7E, 0xE1, 0x3C, 0xD2, 0x54, 0x6F, 0xA4, 0x84, 0x1E, 0x09, 0x22, 0xB2, 0xAD, 0x93, + 0x3B, 0xEF, 0x5B, 0xD6, 0x8B, 0x8C, 0x31, 0xD4, 0x48, 0x7C, 0x7E, 0x1B, 0x27, 0xF8, 0x3B, 0x99, + 0xA2, 0x01, 0x7A, 0xA7, 0xE5, 0xB8, 0x07, 0xB3, 0x73, 0xD6, 0x6F, 0x68, 0xC5, 0x01, 0xB1, 0xAE, + 0x08, 0x4A, 0xF0, 0xB0, 0x66, 0x7C, 0xB6, 0xAE, 0x69, 0xF5, 0x9E, 0xE3, 0x09, 0x44, 0xA1, 0x1A, + 0x5E, 0xAA, 0x6C, 0x99, 0x6D, 0x72, 0xAF, 0xE3, 0x6D, 0x39, 0x98, 0xEB, 0x0C, 0x07, 0x9A, 0x7F, + 0x51, 0xE3, 0x73, 0xCE, 0x52, 0x98, 0x3C, 0xED, 0xF6, 0x0E, 0xAF, 0x72, 0xEC, 0xA1, 0x7F, 0xE5, + 0xC4, 0xC6, 0x1A, 0xB4, 0x40, 0xCB, 0x7F, 0x94, 0x31, 0xA9, 0x1C, 0x29, 0x60, 0xA6, 0xB8, 0xAD, + 0x4C, 0xD4, 0x48, 0x7B, 0x90, 0x8D, 0x92, 0x20, 0xEB, 0xE1, 0x81, 0xBE, 0xF6, 0x48, 0x10, 0x7F, + 0xA9, 0xB1, 0x2A, 0xC9, 0x81, 0xF4, 0x22, 0x23, 0x93, 0xFE, 0x99, 0x46, 0x51, 0x2D, 0xBA, 0xF1, + 0xC9, 0x58, 0x7C, 0x80, 0x38, 0x8C, 0xF3, 0xC9, 0x55, 0x14, 0x12, 0x34, 0x85, 0xF6, 0x48, 0x85, + 0xCC, 0x7E, 0xD6, 0x1A, 0x6D, 0x80, 0x28, 0x9E, 0x92, 0x1B, 0x7D, 0x2D, 0xC3, 0x74, 0x10, 0xE1, + 0x71, 0x4B, 0x32, 0xB0, 0xBE, 0x3E, 0x1C, 0x0E, 0xAF, 0xC8, 0xA1, 0x42, 0xA8, 0x0F, 0x2A, 0x3E, + 0xDB, 0x42, 0x9F, 0x6C, 0xAE, 0x35, 0x86, 0x96, 0xDD, 0x20, 0x41, 0x57, 0xD1, 0x03, 0x86, 0x83, + 0x3B, 0x57, 0x27, 0x9D, 0x6E, 0x47, 0x60, 0xE2, 0xFE, 0x46, 0xB6, 0x89, 0x3E, 0xD4, 0xDD, 0x7E, + 0x90, 0x06, 0xD3, 0x28, 0x52, 0x42, 0x99, 0xD2, 0xA3, 0x5C, 0x61, 0x85, 0xB8, 0xF4, 0xFA, 0x01, + 0x96, 0x08, 0xB6, 0x48, 0x39, 0xA9, 0xB1, 0x49, 0xB4, 0xE8, 0x77, 0x5C, 0xBA, 0x0D, 0x2A, 0x5F, + 0xFB, 0x3E, 0xC8, 0x46, 0xE2, 0xE5, 0x16, 0xC6, 0x20, 0x77, 0x0F, 0x37, 0x08, 0xBA, 0xAE, 0x75, + 0x20, 0xF0, 0x49, 0x62, 0x55, 0xA1, 0xE5, 0x8A, 0x36, 0x3D, 0xB5, 0xAF, 0xDD, 0xFE, 0xE1, 0xEF, + 0xC1, 0x23, 0xFA, 0xF5, 0x43, 0xDD, 0x6F, 0xB7, 0x8E, 0x58, 0xD7, 0x18, 0xA5, 0x37, 0x38, 0xC2, + 0x86, 0xD9, 0x5F, 0x8A, 0xA4, 0x2F, 0x8B, 0xEE, 0x23, 0xC2, 0xA1, 0x9C, 0x69, 0x37, 0xAF, 0x5A, + 0xE5, 0xFA, 0xDA, 0xD2, 0x77, 0xAA, 0xAE, 0xBF, 0xDA, 0xD2, 0xC1, 0x2E, 0x80, 0x73, 0x0E, 0x1D, + 0x83, 0x1C, 0x5F, 0x55, 0xCF, 0x3B, 0xD0, 0xEC, 0x0B, 0x0B, 0x6B, 0xF4, 0xC6, 0x67, 0x98, 0x8B, + 0xB3, 0xB2, 0x95, 0x07, 0xCF, 0x23, 0xBC, 0xE3, 0x17, 0x33, 0x36, 0xB7, 0x95, 0xB1, 0xE9, 0x29, + 0x69, 0xE8, 0x94, 0x98, 0xE5, 0x23, 0x64, 0x6B, 0x78, 0x7A, 0x53, 0x14, 0x55, 0xF6, 0xFB, 0x22, + 0xD9, 0x65, 0x67, 0xF0, 0xFB, 0x42, 0x11, 0xB6, 0x4C, 0xC7, 0xC2, 0x51, 0x98, 0x00, 0x05, 0xB8, + 0x85, 0x1B, 0x8B, 0x10, 0xB4, 0x72, 0x9D, 0x55, 0x46, 0xAD, 0xB2, 0xC5, 0x62, 0x66, 0x25, 0xB0, + 0xBF, 0x3C, 0x5B, 0xC7, 0x3E, 0x5C, 0x40, 0x5C, 0x29, 0xB5, 0x99, 0xBC, 0xB2, 0x23, 0x43, 0x23, + 0xEC, 0xBD, 0xE1, 0x07, 0xED, 0x3A, 0x92, 0xB3, 0xD9, 0xBB, 0x61, 0xC3, 0x8F, 0xDA, 0xF7, 0x61, + 0xDD, 0x3B, 0x58, 0x2B, 0x77, 0xB0, 0x6E, 0x15, 0xEA, 0x0D, 0xF5, 0xF5, 0x35, 0x15, 0x7A, 0xAD, + 0x97, 0xDE, 0x0E, 0xD7, 0x82, 0x4A, 0x21, 0x38, 0x77, 0x46, 0x7F, 0x8D, 0xE0, 0xDF, 0xAF, 0x63, + 0x87, 0xEF, 0xF2, 0x01, 0xB5, 0xEB, 0x6A, 0x4C, 0x1F, 0xF9, 0x63, 0x9D, 0xF9, 0x13, 0x70, 0xB8, + 0x5C, 0x6B, 0x52, 0x74, 0xA6, 0xCB, 0xD5, 0x98, 0x92, 0xCC, 0xCC, 0xD9, 0xDC, 0x9B, 0xCB, 0x05, + 0x66, 0x7F, 0xB4, 0xE2, 0xC3, 0x42, 0x4D, 0xD5, 0xF1, 0x78, 0x5E, 0x92, 0x7A, 0xAD, 0x16, 0xCA, + 0xCE, 0x6F, 0x6C, 0x66, 0xF5, 0xC0, 0xCD, 0x7E, 0xF2, 0xB0, 0x9B, 0xB8, 0x37, 0xBF, 0x3D, 0x4A, + 0x2A, 0xA0, 0xFB, 0x36, 0x48, 0xD2, 0xE2, 0x15, 0x0B, 0x08, 0xB6, 0x7D, 0x7E, 0x10, 0x9F, 0x24, + 0x2D, 0x26, 0x15, 0x7F, 0xEF, 0x9F, 0x54, 0x97, 0xCF, 0x55, 0x01, 0xA1, 0x7C, 0x53, 0xF0, 0x00, + 0xD3, 0xBF, 0x45, 0xA8, 0xD8, 0x6F, 0x24, 0xBB, 0x33, 0xAD, 0xF0, 0x8D, 0x61, 0xB8, 0x20, 0x66, + 0xDA, 0x59, 0xCD, 0x89, 0x07, 0xEA, 0xE9, 0x18, 0x30, 0x35, 0x27, 0xC9, 0x73, 0xDD, 0xEA, 0x42, + 0x1C, 0xFC, 0x48, 0xAA, 0xDE, 0x5F, 0x26, 0x6F, 0x16, 0x16, 0x4D, 0x63, 0x7B, 0x36, 0xE8, 0x29, + 0xD0, 0xE9, 0x2A, 0x62, 0xC7, 0xB9, 0xFA, 0xF2, 0x47, 0xC4, 0xDC, 0x0C, 0x08, 0xB9, 0x7F, 0x0A, + 0xB3, 0x94, 0xF0, 0x96, 0x27, 0xD2, 0xB6, 0x3E, 0x48, 0x41, 0xB1, 0x74, 0x74, 0xE0, 0xFB, 0x0E, + 0x95, 0xDD, 0x3B, 0x6E, 0x9C, 0xE3, 0x3A, 0x3B, 0xE9, 0x8F, 0x9D, 0xE9, 0xF7, 0x9B, 0x54, 0x10, + 0x48, 0xEF, 0x03, 0x38, 0x6F, 0x52, 0xF0, 0x76, 0xE3, 0x83, 0x4E, 0x68, 0x03, 0x70, 0xFE, 0x02, + 0xF0, 0x96, 0x64, 0x3E, 0xF4, 0xEE, 0xFA, 0xDF, 0xD7, 0x1B, 0x34, 0xF6, 0x18, 0x27, 0x84, 0x8A, + 0x75, 0x63, 0x83, 0x62, 0xE3, 0x16, 0x83, 0x44, 0xEE, 0xE8, 0xBE, 0x93, 0xEF, 0x52, 0x93, 0x6B, + 0x6F, 0x50, 0x66, 0x24, 0xEF, 0xF0, 0xE7, 0x07, 0x9A, 0x98, 0xA8, 0xB1, 0x36, 0x0F, 0xF9, 0xF1, + 0xE5, 0x37, 0xC9, 0xD1, 0x23, 0xCF, 0x3E, 0x22, 0xE2, 0xBA, 0x38, 0xC0, 0x68, 0xD6, 0xB3, 0xC1, + 0x3D, 0x55, 0xD9, 0xC5, 0x00, 0x56, 0x67, 0xBE, 0xE4, 0x5F, 0x0B, 0x45, 0xF6, 0x61, 0xD8, 0xE2, + 0x20, 0x49, 0x3B, 0xED, 0x7B, 0x52, 0x7B, 0xC5, 0xDC, 0x90, 0xBF, 0xCA, 0x0D, 0x79, 0xA9, 0xA4, + 0x60, 0x10, 0x25, 0xF2, 0x9C, 0x0F, 0xE5, 0xCF, 0x3C, 0x6F, 0x5A, 0x75, 0x17, 0x63, 0x5A, 0x3A, + 0x72, 0xB8, 0x7E, 0xCC, 0x01, 0x89, 0x34, 0xAF, 0x8E, 0xE2, 0x53, 0x35, 0x05, 0xAA, 0xB4, 0xDD, + 0x97, 0xE6, 0x92, 0xFA, 0xAC, 0x21, 0xDF, 0x7D, 0x3B, 0xD2, 0xE3, 0xBA, 0x4B, 0xD2, 0x20, 0x14, + 0x51, 0x2D, 0xE3, 0x65, 0x6B, 0x0B, 0x28, 0x64, 0x26, 0xFA, 0xB5, 0x2B, 0x21, 0xF6, 0x4D, 0x3C, + 0xEF, 0x4C, 0x79, 0xE7, 0xC8, 0x81, 0x5D, 0x41, 0xAB, 0xFE, 0x5A, 0x64, 0xD6, 0x47, 0x0C, 0x9E, + 0x9D, 0xB5, 0x9A, 0x6B, 0xA6, 0xED, 0x3C, 0x5D, 0x6B, 0xD1, 0x9A, 0xBB, 0xD3, 0xC4, 0x73, 0x5A, + 0x68, 0x79, 0x6D, 0xA7, 0x59, 0x79, 0x27, 0x0D, 0x4E, 0xED, 0x06, 0x6C, 0xB9, 0x03, 0x8A, 0xD2, + 0x62, 0xCE, 0xE3, 0x09, 0x89, 0x49, 0x0B, 0xD2, 0xA6, 0x5B, 0x64, 0x48, 0x75, 0xDC, 0x30, 0x40, + 0x80, 0x27, 0x3E, 0x8E, 0xCB, 0x43, 0xB9, 0xDF, 0x66, 0xA5, 0x88, 0x23, 0xE9, 0xF0, 0x74, 0xE3, + 0x58, 0x77, 0x63, 0xAA, 0xA0, 0x31, 0x50, 0x4A, 0x47, 0xC7, 0x5B, 0xD1, 0xF0, 0x1D, 0x84, 0x43, + 0x95, 0xE0, 0x68, 0x83, 0xFE, 0x43, 0x90, 0x05, 0xEF, 0x85, 0x0B, 0x22, 0x7D, 0xC9, 0x95, 0x6C, + 0x69, 0xCE, 0xE3, 0xFA, 0x71, 0x2E, 0x8A, 0x74, 0xA2, 0x35, 0x2A, 0x0A, 0xBC, 0xE8, 0xA6, 0xB0, + 0x2C, 0xC7, 0xC6, 0x0D, 0x61, 0x85, 0xC9, 0x57, 0xDE, 0x64, 0xE4, 0x32, 0x1D, 0xE8, 0xE8, 0xDA, + 0x68, 0x75, 0x38, 0x57, 0xDE, 0x81, 0xBB, 0xEF, 0x43, 0x1D, 0xBB, 0xD4, 0x9F, 0x7B, 0xE3, 0xB8, + 0x82, 0x77, 0xC5, 0x96, 0xAE, 0xCF, 0xA1, 0xCC, 0x34, 0x37, 0x77, 0x8B, 0x63, 0x9E, 0x64, 0x7D, + 0xBA, 0xFD, 0x15, 0x08, 0x81, 0x16, 0x56, 0x75, 0x3E, 0x3F, 0x6F, 0xC5, 0xA8, 0x9C, 0x17, 0x78, + 0xB2, 0xB2, 0x67, 0xC2, 0x41, 0x41, 0xCC, 0x6A, 0xB8, 0x2E, 0xEE, 0x8E, 0x6C, 0x22, 0xB3, 0x6C, + 0xCD, 0xB0, 0xF5, 0xF4, 0xB4, 0x7D, 0x15, 0x48, 0x93, 0x8E, 0x35, 0x3B, 0xC4, 0xC1, 0x17, 0x57, + 0xCE, 0xF5, 0xB7, 0x0B, 0x8E, 0xC8, 0xBE, 0xC8, 0x57, 0x0A, 0x73, 0xB5, 0x40, 0x2F, 0x8F, 0x8F, + 0x71, 0x0D, 0x06, 0xEA, 0x8B, 0x41, 0x07, 0x0C, 0x0B, 0x1D, 0xAB, 0x29, 0x0A, 0xB5, 0x7D, 0x4F, + 0xE6, 0x2B, 0x24, 0x52, 0xD8, 0x68, 0xF0, 0x0A, 0x5A, 0xE3, 0x4F, 0x5C, 0x4C, 0xD4, 0x6B, 0x45, + 0x38, 0xA2, 0x0D, 0x42, 0x61, 0xE3, 0x6B, 0x70, 0x24, 0x79, 0x69, 0xB8, 0x48, 0xDE, 0x89, 0x96, + 0xE4, 0x53, 0x46, 0x20, 0x29, 0x95, 0xD2, 0x4F, 0x03, 0x62, 0x64, 0x8B, 0x65, 0x53, 0xC6, 0x8C, + 0x4F, 0xEE, 0xC5, 0x25, 0xA4, 0x63, 0x85, 0x4D, 0x7C, 0xA1, 0xCF, 0xBB, 0xB4, 0x53, 0x76, 0x32, + 0x53, 0x3B, 0xDD, 0x66, 0xFA, 0xD2, 0xCE, 0xCE, 0xA4, 0xD2, 0xB3, 0x44, 0x3E, 0x31, 0xC5, 0x67, + 0x20, 0xBD, 0xA7, 0xE3, 0x91, 0xDD, 0x39, 0x2F, 0xAE, 0xB1, 0x52, 0xF4, 0x0D, 0x04, 0x44, 0xF2, + 0x78, 0x18, 0x7C, 0x01, 0x4B, 0x2C, 0x74, 0x86, 0x7C, 0x80, 0x9D, 0x7B, 0xB3, 0x2A, 0x26, 0x14, + 0x83, 0xAA, 0xB4, 0x15, 0x63, 0x80, 0x11, 0x1D, 0xA9, 0x5D, 0xCC, 0x7A, 0xCB, 0x8B, 0xFD, 0xC9, + 0x40, 0xE5, 0x91, 0x26, 0x04, 0x57, 0x1C, 0xF4, 0x0B, 0xF4, 0x76, 0x75, 0xD2, 0xF9, 0x4B, 0xDF, + 0xE9, 0xA5, 0x3F, 0x76, 0x4A, 0xCD, 0xB3, 0xF8, 0xD3, 0x17, 0x56, 0x15, 0xE0, 0x39, 0xA6, 0xAD, + 0x8D, 0xF9, 0xE9, 0x92, 0xED, 0xB1, 0xAF, 0xCE, 0x00, 0x7F, 0xBC, 0xC8, 0x3C, 0x8A, 0xFD, 0x7B, + 0xFC, 0x48, 0x70, 0x81, 0x4A, 0xF3, 0x34, 0xF1, 0x09, 0x20, 0xA8, 0xE7, 0x14, 0x97, 0xA4, 0x4A, + 0x16, 0xD8, 0xD0, 0xEC, 0x31, 0x34, 0xA4, 0xC0, 0x6B, 0xCD, 0xEA, 0xA9, 0xE7, 0xC7, 0xA6, 0x00, + 0x9E, 0x69, 0xA0, 0x95, 0xC4, 0x3C, 0x03, 0x36, 0x42, 0x92, 0xF1, 0x1C, 0x43, 0x05, 0xFA, 0xDC, + 0x8C, 0x86, 0xD9, 0x17, 0xFA, 0x3E, 0xC3, 0x1C, 0x8C, 0xC4, 0x8E, 0x81, 0x7C, 0x4B, 0xEB, 0x0C, + 0x74, 0xE4, 0xDA, 0x68, 0xD0, 0x57, 0xC2, 0x05, 0x48, 0xE3, 0x53, 0x3A, 0xC5, 0xD0, 0x3A, 0x7A, + 0x2A, 0x45, 0x93, 0x18, 0x03, 0xC7, 0xCB, 0x36, 0x3E, 0x1B, 0x5D, 0xFC, 0xC4, 0xEE, 0x40, 0x92, + 0x85, 0x84, 0x58, 0x05, 0x15, 0x35, 0x72, 0x5A, 0x61, 0x46, 0x4C, 0x75, 0x8B, 0xA3, 0xAE, 0xE2, + 0x8A, 0xCD, 0x46, 0xE9, 0xC3, 0xC3, 0x58, 0x0A, 0x2B, 0xC6, 0x2E, 0xA6, 0x3B, 0xCE, 0x48, 0xB8, + 0x68, 0xA6, 0x6B, 0xB0, 0x07, 0x23, 0x0E, 0xEC, 0x61, 0x3D, 0xCE, 0x6A, 0x73, 0x84, 0x33, 0xB5, + 0x06, 0x88, 0x26, 0xEA, 0x2A, 0x96, 0xBA, 0xBC, 0x29, 0xD0, 0x81, 0xFF, 0xAA, 0x87, 0x54, 0x53, + 0xB9, 0x95, 0x0D, 0x39, 0x2C, 0x15, 0xEC, 0xBB, 0x8E, 0x3C, 0x2C, 0xA9, 0xFC, 0x6C, 0x86, 0x23, + 0x11, 0xFB, 0x4C, 0x2C, 0x62, 0x9D, 0xCA, 0xCF, 0x06, 0x5F, 0x4A, 0x9A, 0xBC, 0xD9, 0x93, 0xEE, + 0xCA, 0x77, 0x64, 0xB7, 0x56, 0x06, 0x67, 0x10, 0xDB, 0x37, 0x5D, 0x29, 0xFE, 0xCA, 0xD4, 0xFF, + 0xE4, 0x6D, 0x82, 0x01, 0x82, 0x05, 0x6D, 0x8F, 0xD6, 0xCC, 0xD6, 0xC6, 0x35, 0xE5, 0xCE, 0x9C, + 0x12, 0xE9, 0x1D, 0x8E, 0x89, 0x74, 0x3E, 0x02, 0x8A, 0xFA, 0x9E, 0xE2, 0xB6, 0x39, 0xE7, 0x39, + 0xB4, 0x08, 0x11, 0xA0, 0xD0, 0x54, 0xCA, 0x71, 0x1C, 0x14, 0xEF, 0xD1, 0xC5, 0x56, 0x49, 0xF3, + 0x3D, 0xCC, 0xC0, 0x7E, 0x30, 0x24, 0x66, 0x83, 0x3E, 0x9E, 0xD2, 0x22, 0x2B, 0xAC, 0x9F, 0x2B, + 0x06, 0x7A, 0xFF, 0xCC, 0x2D, 0xFA, 0x68, 0xBA, 0xCA, 0x18, 0xEA, 0xFC, 0x53, 0x0E, 0xA0, 0x16, + 0xBE, 0xE2, 0x3C, 0xEC, 0x97, 0xB0, 0x71, 0x52, 0xE8, 0xE2, 0xE8, 0x00, 0x2C, 0x9D, 0x7F, 0x82, + 0x01, 0x82, 0x3A, 0x37, 0xE2, 0xBB, 0x92, 0x65, 0xA9, 0xC2, 0x44, 0xE3, 0x37, 0x75, 0xCD, 0x93, + 0xA4, 0x96, 0x74, 0x33, 0xBE, 0x34, 0xDC, 0x5F, 0x75, 0x11, 0x30, 0x8F, 0x73, 0xBA, 0x12, 0x5B, + 0xFF, 0x7D, 0x05, 0x85, 0xF8, 0x20, 0x7A, 0x0A, 0x4C, 0x7D, 0xAB, 0xA4, 0x5E, 0xE9, 0x25, 0xDB, + 0x6C, 0xC4, 0x0B, 0x27, 0x36, 0x66, 0x08, 0xD2, 0x5C, 0x0B, 0x54, 0x37, 0x3E, 0x84, 0x53, 0x97, + 0x24, 0xE7, 0xB0, 0x2F, 0x19, 0x49, 0x14, 0x73, 0x2F, 0x6E, 0x8B, 0xF2, 0x5F, 0xFC, 0x23, 0xD6, + 0xD5, 0xC1, 0x99, 0x15, 0x13, 0x67, 0xC0, 0xA5, 0x4C, 0x26, 0xEA, 0x7A, 0xE4, 0xC8, 0x1E, 0x4B, + 0x73, 0x95, 0x0E, 0x0E, 0x00, 0x9F, 0xFC, 0x68, 0xC6, 0x28, 0x4B, 0x86, 0x33, 0x24, 0x0C, 0x24, + 0xA3, 0x83, 0x23, 0x5A, 0x64, 0xB6, 0x50, 0x8D, 0xF5, 0x37, 0xC7, 0x47, 0xBF, 0x60, 0x15, 0xCE, + 0x62, 0xEB, 0x85, 0x88, 0x89, 0x4F, 0xCD, 0x9B, 0x53, 0x25, 0xFE, 0xC6, 0x17, 0xB3, 0x88, 0x88, + 0x50, 0xDC, 0x85, 0x79, 0x02, 0x75, 0x3D, 0xFD, 0x0C, 0x2C, 0x47, 0x52, 0xAB, 0xF7, 0x9E, 0x0B, + 0x86, 0xF7, 0x84, 0xD1, 0x48, 0xAE, 0x53, 0x56, 0xEB, 0xE2, 0xE4, 0x82, 0x7C, 0x0F, 0x2C, 0x09, + 0x17, 0xC1, 0xE7, 0x1A, 0x65, 0x6D, 0x36, 0x8E, 0xB9, 0xD0, 0x24, 0x5B, 0x92, 0x65, 0xAF, 0xB3, + 0x61, 0xD0, 0x34, 0xE2, 0x4E, 0x0D, 0x52, 0x97, 0x89, 0x33, 0x30, 0x80, 0xDD, 0xC3, 0x40, 0xC7, + 0xB8, 0x3A, 0x34, 0x7A, 0x4F, 0xD9, 0xFC, 0xD2, 0x3E, 0x2B, 0xF7, 0xF8, 0xBD, 0xF7, 0xFC, 0x97, + 0x85, 0x92, 0x55, 0x5F, 0xF1, 0xD8, 0x8C, 0x02, 0x04, 0x87, 0x05, 0x70, 0x50, 0xE3, 0x98, 0xAB, + 0xCA, 0xC5, 0xC2, 0x38, 0x1B, 0x09, 0x68, 0xC1, 0xF5, 0x66, 0x38, 0x6A, 0xD2, 0x78, 0xA5, 0x9D, + 0x53, 0x73, 0x84, 0xCB, 0x7E, 0x6C, 0x0C, 0x9C, 0xB9, 0x6A, 0xAC, 0xBA, 0xD2, 0x95, 0x12, 0xE2, + 0x76, 0x7B, 0x32, 0xBC, 0xB3, 0xCD, 0x46, 0xDE, 0x0E, 0x58, 0xE0, 0xCE, 0x69, 0x0F, 0x1E, 0x88, + 0xDF, 0xFE, 0xD1, 0x10, 0x1B, 0xF8, 0xB8, 0x9C, 0x5D, 0x94, 0xB0, 0x2C, 0xE4, 0x9A, 0xD1, 0xA0, + 0x0F, 0x45, 0x97, 0xCE, 0xBB, 0xFF, 0x36, 0x91, 0x39, 0x58, 0x71, 0xBC, 0x08, 0x9B, 0x83, 0x61, + 0xDB, 0x76, 0xCF, 0x88, 0x0B, 0x72, 0x86, 0x61, 0xB9, 0x7A, 0xE1, 0x88, 0x90, 0xE5, 0x26, 0x1E, + 0xED, 0xB8, 0x29, 0xCE, 0x8C, 0xD6, 0xC0, 0x28, 0x84, 0x7B, 0xE5, 0x5C, 0x7D, 0x1C, 0x9E, 0xFE, + 0x7A, 0xF9, 0x39, 0xC7, 0x7E, 0xCE, 0x5F, 0xFC, 0x9C, 0x47, 0x0B, 0x02, 0x72, 0x80, 0xD1, 0xB1, + 0x5A, 0x26, 0xF4, 0xD1, 0xEF, 0x67, 0xA6, 0x67, 0x40, 0x0A, 0x39, 0xFB, 0xBB, 0xC8, 0xDE, 0xCB, + 0xAD, 0x08, 0x9E, 0x2A, 0x5D, 0x00, 0x2A, 0x87, 0x15, 0xD4, 0x9D, 0x51, 0x6C, 0xA0, 0x49, 0xA9, + 0x51, 0xC5, 0x54, 0xC8, 0xF6, 0x2F, 0x12, 0x75, 0xF1, 0xE0, 0x81, 0xB7, 0xA8, 0xC3, 0x39, 0xB5, + 0x60, 0x79, 0xB1, 0xD9, 0xC8, 0x42, 0x2A, 0xE7, 0xDC, 0xCD, 0xE1, 0x1E, 0x8C, 0xCF, 0x03, 0x7F, + 0x45, 0x1B, 0x23, 0xD1, 0x63, 0x5F, 0x77, 0xD8, 0x67, 0xB4, 0x41, 0xD2, 0x29, 0x32, 0xAE, 0x13, + 0xC5, 0xCD, 0xBD, 0xA7, 0x8F, 0xA1, 0xDD, 0xB3, 0x16, 0x0E, 0xCA, 0x05, 0x2D, 0x2B, 0x31, 0x59, + 0x6C, 0x8A, 0xFC, 0x66, 0x53, 0x5C, 0xC8, 0x16, 0x0E, 0xCB, 0xF6, 0xC3, 0x65, 0xCD, 0x82, 0x33, + 0xDE, 0x62, 0x66, 0x70, 0x02, 0xFC, 0x0C, 0x21, 0xF9, 0x0A, 0xD1, 0x77, 0x06, 0xCD, 0x50, 0x2C, + 0xC7, 0x95, 0x68, 0x49, 0xB5, 0x7D, 0x68, 0x6C, 0x3D, 0x0B, 0xA9, 0xE8, 0xF4, 0xCE, 0x9C, 0xBB, + 0x69, 0x85, 0xFD, 0x8C, 0x2B, 0x69, 0x47, 0x31, 0xA2, 0x39, 0x92, 0xBD, 0xA3, 0x73, 0x0B, 0x43, + 0x49, 0xEB, 0x7C, 0x0A, 0xA2, 0x48, 0x91, 0xCF, 0xF1, 0xE2, 0xC5, 0x24, 0x3F, 0xBF, 0x25, 0x75, + 0x9F, 0x59, 0x63, 0x69, 0xE2, 0x6C, 0x9A, 0x5A, 0x29, 0x99, 0xE9, 0xDF, 0xE5, 0x62, 0x9A, 0xDC, + 0xCB, 0x5E, 0x29, 0x2D, 0xF0, 0xF7, 0xB4, 0x7E, 0x23, 0x5E, 0xF2, 0xDC, 0x25, 0x2F, 0xD2, 0xF1, + 0x22, 0x96, 0xC4, 0x37, 0x12, 0xBB, 0x46, 0x23, 0x61, 0x8C, 0x98, 0x23, 0x61, 0xDA, 0xF3, 0x44, + 0x9E, 0x32, 0x46, 0xCD, 0xAA, 0x39, 0xAC, 0x57, 0x4B, 0xE4, 0x9A, 0xC9, 0x50, 0xEE, 0xD1, 0x3F, + 0x25, 0xAE, 0x62, 0x2E, 0x88, 0x0F, 0xE7, 0x5F, 0xC7, 0x08, 0x4B, 0x94, 0xB4, 0xD7, 0x8F, 0x44, + 0x65, 0xD2, 0x89, 0x6D, 0xB8, 0x0D, 0xB9, 0xCD, 0xE7, 0x39, 0xD0, 0x62, 0xBD, 0xDB, 0xF2, 0x9F, + 0xAA, 0x1B, 0xCB, 0xC6, 0xBC, 0x48, 0x18, 0x79, 0x58, 0xC1, 0xB7, 0x9A, 0x9A, 0x60, 0x19, 0x04, + 0x6A, 0xF8, 0xAB, 0xA5, 0xDC, 0x45, 0x6C, 0xE5, 0xE8, 0x1D, 0x96, 0x57, 0x1C, 0x0C, 0x48, 0x5E, + 0xE8, 0x58, 0xD7, 0x09, 0xF9, 0xB2, 0x21, 0x6E, 0xC7, 0x41, 0x23, 0xB0, 0x25, 0x72, 0x44, 0xDC, + 0x94, 0x07, 0xCC, 0xD5, 0x95, 0xF8, 0x28, 0xF4, 0x21, 0x8A, 0xAA, 0x26, 0x27, 0x74, 0xF3, 0xF3, + 0x9D, 0x18, 0x9E, 0x83, 0xB1, 0xD2, 0x5E, 0x02, 0x0F, 0x19, 0x97, 0x27, 0xA4, 0x22, 0x69, 0x41, + 0x27, 0x3F, 0xD4, 0xF9, 0x59, 0xAE, 0x50, 0x8F, 0x39, 0x24, 0xB5, 0x9E, 0x35, 0x2F, 0x30, 0x60, + 0x9E, 0xA4, 0x5E, 0xAC, 0x2D, 0xC9, 0x57, 0x79, 0xD2, 0xBF, 0xF7, 0x94, 0xAA, 0x70, 0xEA, 0x45, + 0xC3, 0xDB, 0x90, 0xEE, 0xDE, 0x28, 0x15, 0xEB, 0x37, 0xD5, 0x91, 0x83, 0xF4, 0x5D, 0x51, 0xD0, + 0x48, 0x7E, 0x50, 0x33, 0x61, 0x86, 0xC3, 0xD6, 0x3A, 0x15, 0xC0, 0x42, 0x7B, 0xC3, 0x29, 0x5D, + 0xE8, 0x2F, 0x75, 0x4F, 0xAC, 0x33, 0xF2, 0x34, 0xA2, 0x8D, 0x62, 0x58, 0x52, 0x1C, 0x44, 0xDC, + 0x52, 0x00, 0x63, 0x4D, 0xCC, 0xB6, 0x81, 0xD7, 0x6B, 0xB1, 0x5F, 0x6C, 0x38, 0x4C, 0x23, 0xFD, + 0xE5, 0x21, 0x83, 0x78, 0x8A, 0xE9, 0x25, 0x71, 0x11, 0x71, 0x59, 0x34, 0xDA, 0x0C, 0x0B, 0x96, + 0x9D, 0x91, 0xD7, 0xBA, 0x4A, 0x5C, 0xDE, 0x5A, 0x87, 0x9F, 0xC0, 0x65, 0xE7, 0x49, 0x52, 0x21, + 0x87, 0xE9, 0xD6, 0x7D, 0xE3, 0x2C, 0xC3, 0x8E, 0x94, 0xB3, 0x78, 0xAC, 0x9A, 0x9A, 0x88, 0x29, + 0x81, 0x21, 0x4E, 0x2F, 0x66, 0x8D, 0xF9, 0x20, 0xD4, 0x5E, 0x19, 0x17, 0xA1, 0x10, 0x40, 0x40, + 0xEF, 0x68, 0x1F, 0x93, 0x04, 0x72, 0xA5, 0xB7, 0x21, 0x4A, 0x96, 0x4C, 0xEA, 0x15, 0x77, 0xCB, + 0x96, 0x3B, 0xB7, 0xF2, 0xE8, 0x23, 0x67, 0x6D, 0x95, 0x5C, 0xCF, 0xEA, 0x7D, 0x53, 0xAA, 0x3A, + 0x7F, 0xAA, 0x30, 0x87, 0x90, 0x9E, 0xC8, 0xB4, 0x52, 0xF0, 0x2F, 0x7A, 0xF5, 0x67, 0x19, 0x69, + 0xA5, 0x9B, 0xE6, 0x9A, 0x22, 0xEB, 0xBA, 0x0B, 0x31, 0x4F, 0x8A, 0x01, 0x45, 0x2C, 0x55, 0x10, + 0x99, 0x2E, 0xE7, 0xB5, 0x25, 0x87, 0xA9, 0xFC, 0xC9, 0xE6, 0xD7, 0x61, 0x00, 0x3E, 0x76, 0x0A, + 0xC7, 0x4E, 0x2A, 0x68, 0x4E, 0xCB, 0x79, 0xA1, 0xE7, 0x54, 0x4C, 0xDA, 0xB0, 0x1A, 0x48, 0x11, + 0xCC, 0xA3, 0x3F, 0x13, 0x09, 0xFE, 0x54, 0x52, 0x2B, 0xA5, 0x99, 0xC6, 0x82, 0xA6, 0x91, 0xC7, + 0x55, 0x40, 0xB8, 0x54, 0x3C, 0xAE, 0x4F, 0x0A, 0x16, 0x83, 0x15, 0x58, 0x61, 0xFA, 0x2B, 0x83, + 0x0D, 0x5E, 0xDC, 0x88, 0x9C, 0xEA, 0xBC, 0x64, 0x6D, 0x13, 0x5F, 0xE6, 0x6D, 0xA8, 0x48, 0xA5, + 0xE9, 0x44, 0xC5, 0x14, 0x64, 0x20, 0xF9, 0xF6, 0x11, 0xC4, 0x79, 0x45, 0x11, 0x96, 0xE5, 0x77, + 0x57, 0xCC, 0x6F, 0xE1, 0x43, 0x6F, 0x36, 0x63, 0xA1, 0x81, 0xCA, 0x71, 0xFD, 0x7E, 0xDE, 0xD5, + 0x16, 0x7B, 0x01, 0x4E, 0x1B, 0x3D, 0x93, 0xAA, 0xEB, 0xD9, 0xF4, 0xB4, 0x2B, 0x8C, 0xEF, 0xB7, + 0x30, 0x3E, 0x49, 0x51, 0x48, 0xE8, 0x5C, 0xC5, 0x89, 0xDF, 0x5D, 0x53, 0x12, 0x8F, 0xDF, 0x24, + 0x9D, 0xF2, 0xB7, 0x71, 0x32, 0x7F, 0x26, 0x65, 0x10, 0xBE, 0xFD, 0xA6, 0x72, 0x8A, 0x46, 0xBA, + 0x8D, 0x2D, 0xA7, 0xBF, 0x58, 0x77, 0x2E, 0x83, 0x5B, 0xE2, 0x0C, 0xD3, 0x90, 0xCB, 0x33, 0xCD, + 0x6D, 0xB7, 0xE1, 0x44, 0x39, 0xD3, 0x9F, 0xF1, 0xA9, 0x09, 0xBE, 0xB2, 0xF1, 0x3E, 0xCF, 0x40, + 0x23, 0x6C, 0x83, 0x1C, 0xEB, 0x36, 0x1B, 0x81, 0xF3, 0xA9, 0x87, 0x21, 0x18, 0x26, 0x02, 0x16, + 0x2D, 0x31, 0x6D, 0x7C, 0x3F, 0xAE, 0x80, 0x18, 0xB6, 0x31, 0x89, 0xC5, 0xD8, 0x64, 0x9F, 0xD7, + 0xC5, 0x6D, 0x50, 0x5C, 0x4F, 0xC0, 0xCE, 0xA9, 0xEC, 0x9D, 0x84, 0xBB, 0x66, 0x08, 0x4C, 0x94, + 0xE3, 0x03, 0x7B, 0x57, 0xEC, 0x00, 0x97, 0xF2, 0x38, 0x3C, 0x5A, 0x84, 0xCB, 0x69, 0x2A, 0x07, + 0x17, 0x9A, 0x90, 0x23, 0x9D, 0x1D, 0x7F, 0xEE, 0x65, 0xFB, 0x5F, 0x56, 0xB0, 0x02, 0x6E, 0x47, + 0x7B, 0xAD, 0x46, 0x3D, 0x54, 0xB4, 0xA3, 0x08, 0x8B, 0x69, 0x13, 0x00, 0xE5, 0x1D, 0xF3, 0x10, + 0x3A, 0x80, 0xAC, 0x84, 0x2D, 0xC4, 0x14, 0xAB, 0xD4, 0x76, 0xE9, 0x23, 0x3C, 0x58, 0x06, 0x3F, + 0xB4, 0x59, 0xE5, 0xA8, 0x5D, 0x16, 0xA0, 0x17, 0x5D, 0x5B, 0x8D, 0x72, 0x32, 0x84, 0x86, 0xA9, + 0x86, 0x07, 0x0F, 0x02, 0xF2, 0x82, 0x24, 0xC9, 0x2E, 0x56, 0xA2, 0xF2, 0x83, 0x3B, 0xAA, 0x31, + 0x46, 0x80, 0xCF, 0x49, 0x67, 0x30, 0x38, 0xE9, 0x06, 0xC3, 0x7F, 0xA2, 0x15, 0x7E, 0x4C, 0x29, + 0x5E, 0x75, 0x9C, 0x93, 0xDD, 0xCE, 0xC8, 0x50, 0xAA, 0x1A, 0xC9, 0xD3, 0x1E, 0x75, 0xA2, 0x8F, + 0xC4, 0x9D, 0xD2, 0x52, 0x3A, 0x9C, 0xB3, 0x68, 0x5F, 0x7B, 0x54, 0x89, 0x4C, 0x90, 0xAC, 0xC8, + 0x84, 0x7F, 0x7A, 0x51, 0x65, 0xAC, 0x70, 0x84, 0x12, 0xEC, 0xA2, 0x52, 0x07, 0xBE, 0xD1, 0x6E, + 0xEF, 0x41, 0x4A, 0x56, 0xF8, 0x02, 0x79, 0x56, 0xF1, 0x3D, 0xA3, 0x4C, 0xB8, 0x36, 0x3A, 0xE8, + 0x22, 0xD7, 0x19, 0x47, 0xAF, 0xA2, 0x9E, 0xF8, 0xE1, 0x4F, 0xA6, 0x40, 0xE6, 0x7A, 0xD3, 0x61, + 0xBC, 0x3A, 0x05, 0xB9, 0x91, 0xF6, 0xB6, 0xCE, 0x61, 0x8F, 0xBF, 0x66, 0xAB, 0xF2, 0x93, 0x80, + 0x3C, 0xA2, 0xF4, 0x7B, 0xA5, 0xF1, 0xAF, 0xB9, 0xEA, 0x4E, 0x71, 0xB7, 0x72, 0xAE, 0x63, 0xFE, + 0x85, 0x00, 0xA0, 0xAE, 0xD2, 0x78, 0xFC, 0x5D, 0x37, 0xAE, 0x71, 0xF6, 0x88, 0x77, 0xDD, 0xA5, + 0x83, 0x9B, 0xF4, 0xE7, 0x6A, 0xA8, 0xCD, 0xE9, 0x9F, 0x47, 0xF4, 0x93, 0xF4, 0x7D, 0xFC, 0x3B, + 0x24, 0x19, 0x7A, 0xD8, 0x99, 0xBD, 0x66, 0x96, 0xA8, 0x11, 0xF1, 0xC7, 0x12, 0xFE, 0x23, 0x70, + 0xD3, 0x40, 0xCA, 0x9A, 0x99, 0x9E, 0x08, 0x11, 0x3B, 0xF7, 0x4C, 0x4D, 0x51, 0x20, 0xF0, 0x6A, + 0x4F, 0x88, 0xD9, 0xD0, 0x03, 0xB3, 0x33, 0x13, 0xF3, 0x38, 0x09, 0xA3, 0xCD, 0x33, 0x1A, 0xC7, + 0x03, 0xA7, 0xF9, 0x73, 0xC2, 0xCB, 0x90, 0x96, 0x4E, 0xF3, 0x0C, 0x8F, 0xBE, 0x2F, 0xE3, 0xD2, + 0x6A, 0x9E, 0x75, 0xD0, 0xC0, 0x6B, 0xD2, 0xB8, 0x17, 0xA0, 0x0C, 0x1F, 0x32, 0x89, 0x08, 0xC2, + 0x75, 0x23, 0x17, 0xB6, 0x11, 0xEA, 0xD2, 0x34, 0xC1, 0xD7, 0x7A, 0x69, 0x80, 0xD2, 0x39, 0x59, + 0xFB, 0xC8, 0xE5, 0x0F, 0x95, 0x4C, 0xDD, 0x6F, 0x4A, 0xF4, 0xAD, 0xFD, 0xFC, 0x54, 0x07, 0x03, + 0x60, 0x89, 0xE4, 0xD0, 0x25, 0xA6, 0x0F, 0x96, 0xE8, 0x9D, 0x5F, 0x18, 0x36, 0xB5, 0xEA, 0xAE, + 0x81, 0x6C, 0xB3, 0xB9, 0x0B, 0x0D, 0xB1, 0x96, 0xE7, 0x3D, 0x16, 0xB0, 0xCD, 0xC4, 0x51, 0x96, + 0xB2, 0x59, 0xA5, 0xDD, 0x81, 0x8D, 0x02, 0xDF, 0x50, 0x72, 0x86, 0x8E, 0x3B, 0xC7, 0xE3, 0x23, + 0x06, 0xD0, 0x20, 0x3A, 0xBC, 0xD8, 0x14, 0x40, 0x20, 0xD5, 0x90, 0xEF, 0xEA, 0xA1, 0x43, 0x24, + 0x7D, 0x8A, 0x9C, 0x89, 0x18, 0x76, 0xFC, 0xF4, 0x61, 0x92, 0xBA, 0x9B, 0xF5, 0x56, 0x12, 0x8B, + 0xD0, 0xFB, 0xA6, 0x8A, 0xB9, 0x94, 0xD6, 0xC3, 0xE0, 0x10, 0x72, 0x24, 0x5A, 0xAB, 0x8C, 0xF9, + 0x08, 0x41, 0xAD, 0x8E, 0x70, 0xF5, 0xF6, 0x3B, 0xB5, 0x94, 0x77, 0xC7, 0x02, 0x38, 0x6F, 0xD9, + 0x4E, 0x38, 0xE9, 0x1E, 0x7B, 0xCF, 0x1E, 0xE8, 0x5A, 0x75, 0x1E, 0x86, 0x71, 0x54, 0xAE, 0x17, + 0x8A, 0xFF, 0x72, 0xFC, 0x1C, 0x0D, 0x30, 0xB9, 0xB4, 0x32, 0xAB, 0x19, 0x52, 0x24, 0xBF, 0x04, + 0xA1, 0x66, 0x0C, 0x5E, 0xAF, 0x45, 0x5E, 0xCB, 0x6B, 0xBB, 0xD1, 0x98, 0x6D, 0x63, 0x1C, 0x88, + 0x6C, 0xC4, 0x8D, 0x34, 0x47, 0x0E, 0x94, 0x82, 0xF9, 0x86, 0x72, 0x28, 0xC9, 0x89, 0x6F, 0x2F, + 0x3D, 0x9B, 0xB3, 0xD8, 0x71, 0x65, 0x59, 0xBE, 0x28, 0x66, 0xA3, 0x7B, 0x91, 0x27, 0x25, 0xBD, + 0xF0, 0xCA, 0x87, 0x9B, 0x1A, 0x70, 0x17, 0xBE, 0x93, 0x2B, 0xEA, 0xBC, 0x6D, 0x59, 0x67, 0x3C, + 0x72, 0x2D, 0xBC, 0xF7, 0x5B, 0x08, 0x05, 0x1A, 0x60, 0x14, 0x17, 0xE9, 0x98, 0x35, 0x7A, 0x38, + 0xB0, 0x30, 0x12, 0x24, 0x21, 0x92, 0x8F, 0x20, 0x53, 0x2A, 0xF2, 0x31, 0x64, 0x4A, 0xA2, 0xB2, + 0x4F, 0x46, 0xA6, 0x9E, 0xE1, 0xE6, 0x5C, 0x37, 0xFF, 0xAC, 0xBC, 0x65, 0x99, 0x56, 0xB1, 0xE8, + 0x23, 0x55, 0xCA, 0xFD, 0x2F, 0x20, 0xD5, 0xBD, 0x1D, 0x38, 0xB2, 0xCB, 0xEC, 0x00, 0x18, 0x2C, + 0x3B, 0x55, 0x4A, 0x8A, 0xD1, 0x57, 0x0D, 0x10, 0xE0, 0xA7, 0x23, 0xB8, 0x82, 0xB5, 0x68, 0x3F, + 0x11, 0x99, 0x51, 0xE1, 0xAD, 0xB2, 0xBD, 0x02, 0x59, 0xE5, 0xEF, 0x58, 0x39, 0x7B, 0x87, 0x06, + 0x4B, 0x0F, 0xE9, 0x45, 0x28, 0x5E, 0x52, 0x71, 0xCF, 0x07, 0xC3, 0xCC, 0x5A, 0x61, 0xB1, 0xC9, + 0x56, 0x7A, 0x2D, 0x97, 0xBD, 0xC2, 0xFA, 0xDB, 0xC1, 0xE7, 0x2F, 0x87, 0x89, 0xEC, 0x0F, 0x10, + 0x2C, 0xA5, 0xD9, 0x14, 0xD6, 0x6C, 0x84, 0xAF, 0x64, 0x76, 0x20, 0xF4, 0x38, 0xAF, 0x49, 0xED, + 0x9F, 0x9C, 0x13, 0x3C, 0xC3, 0x93, 0x12, 0xF8, 0x4C, 0xF5, 0x2F, 0x55, 0x8E, 0xB0, 0x74, 0x21, + 0xD1, 0x80, 0x19, 0xF6, 0x66, 0xD3, 0xD2, 0x0F, 0xD9, 0x8E, 0xCF, 0x2A, 0x3C, 0x4D, 0x10, 0xDF, + 0x66, 0xDC, 0x4B, 0x0E, 0xDF, 0x6C, 0x8B, 0x3C, 0x5B, 0x4A, 0x53, 0x83, 0xED, 0xC7, 0xDB, 0x46, + 0xA3, 0x71, 0xD7, 0xAB, 0xFC, 0xA9, 0xBD, 0x84, 0x77, 0x25, 0xB3, 0x52, 0x98, 0x0F, 0x60, 0x87, + 0xE5, 0x8B, 0x6A, 0xF5, 0x01, 0x8F, 0x90, 0xB7, 0xFF, 0x81, 0x37, 0x08, 0x4D, 0x53, 0xB1, 0x5A, + 0x89, 0x79, 0x95, 0xBC, 0xBD, 0x14, 0xE5, 0x06, 0x54, 0xA9, 0x6F, 0x90, 0x55, 0x51, 0x7A, 0xBD, + 0x92, 0xA7, 0x75, 0x0B, 0x05, 0xD1, 0x6B, 0x3C, 0x9C, 0x93, 0x62, 0xC7, 0x6F, 0x44, 0x67, 0xD5, + 0x98, 0x5B, 0x3C, 0x5D, 0xB2, 0x23, 0xF1, 0xE0, 0x3E, 0x96, 0xDC, 0x39, 0x20, 0xA6, 0x4D, 0xD5, + 0x3B, 0xE9, 0xF9, 0x82, 0xD9, 0xD7, 0x68, 0xC4, 0x10, 0x9E, 0xD0, 0xBA, 0x79, 0xAC, 0x1F, 0x73, + 0xA3, 0x08, 0xAD, 0x63, 0x59, 0x1E, 0x4C, 0x3C, 0xBD, 0x64, 0xF7, 0x96, 0xC6, 0x24, 0x9B, 0xC0, + 0xAC, 0xA6, 0xDD, 0xB8, 0x0B, 0x5C, 0xBB, 0x5D, 0xC8, 0x45, 0x3C, 0x11, 0x77, 0x6E, 0x31, 0x6E, + 0x12, 0x48, 0xB2, 0x2E, 0x91, 0xEC, 0x71, 0xC6, 0x48, 0x61, 0x2D, 0xA1, 0xF2, 0xCA, 0x0C, 0xF5, + 0xD8, 0x3E, 0x91, 0x3D, 0x35, 0x7A, 0xF2, 0x02, 0xA0, 0xD5, 0xC9, 0x25, 0x09, 0x42, 0x26, 0x78, + 0x82, 0x74, 0xC5, 0x8B, 0x7A, 0x57, 0xCC, 0x73, 0xAA, 0xDE, 0x38, 0x7D, 0x79, 0x94, 0xAD, 0xB9, + 0x1C, 0xBB, 0x90, 0x80, 0xEA, 0x09, 0xBF, 0x61, 0x1C, 0xB4, 0x17, 0xDB, 0x0C, 0xED, 0x34, 0x3A, + 0xAC, 0x79, 0x3B, 0x82, 0x7F, 0x49, 0xAF, 0xD7, 0xFB, 0x72, 0x83, 0xF0, 0xFE, 0x27, 0x93, 0xA7, + 0x9B, 0xFE, 0x94, 0xF9, 0xDB, 0xEF, 0x63, 0x62, 0xBF, 0x03, 0xC6, 0x0B, 0x76, 0xFC, 0x51, 0xF4, + 0x7F, 0x9A, 0x71, 0xFD, 0x8A, 0x48, 0x06, 0x63, 0x53, 0x9F, 0x34, 0x59, 0x63, 0x5C, 0x74, 0xB8, + 0x64, 0xF5, 0x1F, 0xAC, 0x03, 0xDF, 0xB7, 0xFB, 0xEB, 0x50, 0x1A, 0x42, 0xA3, 0xB6, 0x5C, 0x3A, + 0x95, 0x7B, 0xBA, 0xC4, 0x24, 0x25, 0x0B, 0x4F, 0x4D, 0x08, 0xDD, 0x77, 0x45, 0x2D, 0x06, 0x96, + 0xC8, 0x67, 0x1D, 0xB3, 0x1A, 0x1D, 0x5C, 0x97, 0xC2, 0x98, 0x70, 0x6B, 0xC5, 0x69, 0x57, 0xF8, + 0x33, 0xBB, 0x7F, 0x75, 0xDD, 0x80, 0x51, 0x63, 0x74, 0x75, 0x55, 0x47, 0xFF, 0x6F, 0x07, 0x31, + 0x91, 0x87, 0xE6, 0xCE, 0xAC, 0xA3, 0x71, 0x27, 0x77, 0x7F, 0xC3, 0x68, 0xA6, 0x05, 0x2E, 0x13, + 0x59, 0x96, 0x22, 0x6B, 0x74, 0x98, 0x94, 0x8A, 0xFC, 0x4A, 0x72, 0xAF, 0x44, 0x21, 0x54, 0x6E, + 0xE0, 0x9C, 0x90, 0x56, 0xEE, 0xD3, 0x0A, 0x89, 0x8C, 0x63, 0xCF, 0x35, 0x1C, 0x59, 0x66, 0x56, + 0xC4, 0x9C, 0x85, 0xC0, 0xC8, 0x8C, 0x16, 0x4B, 0xE5, 0x85, 0x92, 0x8B, 0xDE, 0xBE, 0xB5, 0x07, + 0xC1, 0xDB, 0xB7, 0x91, 0x12, 0x87, 0xAA, 0xFE, 0x5A, 0x0A, 0x6B, 0x74, 0x7C, 0x79, 0x0B, 0x61, + 0xF0, 0x96, 0x24, 0xB3, 0x02, 0xEB, 0x6E, 0xBC, 0x5D, 0x4D, 0x0E, 0x25, 0xCC, 0x29, 0x34, 0x9F, + 0xB5, 0x23, 0x3E, 0x20, 0xD4, 0x55, 0x32, 0x02, 0xB4, 0x82, 0x00, 0xED, 0x24, 0x80, 0xF6, 0x46, + 0xA0, 0xBD, 0x0F, 0xE3, 0x80, 0xDD, 0x58, 0xE0, 0x9C, 0xB7, 0x81, 0x86, 0x75, 0x03, 0xCD, 0x6E, + 0x1B, 0x43, 0x15, 0xC6, 0xD0, 0xCF, 0x86, 0x9C, 0xFC, 0x00, 0x2A, 0xF1, 0x4D, 0x73, 0x74, 0x11, + 0x5E, 0x55, 0x41, 0xFB, 0xBB, 0xD1, 0x59, 0xC2, 0xA5, 0x50, 0x00, 0x53, 0x1E, 0x89, 0xA7, 0x9C, + 0xDE, 0xDF, 0xD0, 0x14, 0x1E, 0x12, 0x0A, 0x1B, 0x3E, 0x8F, 0x73, 0xA8, 0xC9, 0x7A, 0x9C, 0x0D, + 0xDC, 0x76, 0x19, 0x79, 0x78, 0x87, 0x99, 0xAC, 0x02, 0x28, 0x33, 0x70, 0x88, 0x41, 0xC5, 0x09, + 0x16, 0x1E, 0xF0, 0x8A, 0x0B, 0xBF, 0x95, 0xC4, 0x38, 0xB0, 0x9F, 0x75, 0x13, 0xCA, 0xE8, 0x2B, + 0xAC, 0xD3, 0x99, 0x29, 0x3E, 0x32, 0x7D, 0xC3, 0x85, 0x3E, 0x93, 0x8F, 0xD6, 0x0F, 0x73, 0x1E, + 0x91, 0x6C, 0x8C, 0xEF, 0x67, 0xE8, 0xCF, 0x69, 0x3E, 0x9B, 0xB8, 0x43, 0xFD, 0xE4, 0x71, 0x2F, + 0x80, 0x8B, 0xC4, 0x11, 0x5C, 0x8D, 0x47, 0x70, 0x35, 0x0E, 0xEE, 0x3A, 0x15, 0xF1, 0x0D, 0x8B, + 0x20, 0x83, 0x53, 0xE4, 0xC2, 0xD5, 0x11, 0x90, 0x60, 0x27, 0xCD, 0x7A, 0x63, 0x18, 0x1B, 0xC0, + 0x7D, 0xBD, 0x5F, 0x0C, 0x7B, 0xA7, 0xBD, 0xC5, 0x00, 0xEA, 0xFA, 0x1E, 0x23, 0xBD, 0x51, 0x4C, + 0x78, 0x1B, 0xC8, 0xB5, 0xB8, 0xEF, 0x43, 0x47, 0x18, 0x39, 0xF5, 0x27, 0x4B, 0xEE, 0xDA, 0x21, + 0xE1, 0x6B, 0x46, 0x97, 0xDE, 0x69, 0xDE, 0xCD, 0x27, 0x92, 0x82, 0x32, 0x1E, 0xDF, 0x1F, 0xC2, + 0x3E, 0xAB, 0x1B, 0x03, 0xA6, 0xC5, 0x8B, 0x5D, 0xC1, 0xB6, 0x5D, 0x9A, 0xA6, 0x19, 0xB6, 0xEA, + 0x3B, 0x19, 0x34, 0x11, 0x8E, 0x89, 0x23, 0xDE, 0xA7, 0xB5, 0x80, 0x55, 0x75, 0xC2, 0x81, 0x08, + 0x9B, 0xC5, 0xD3, 0x6F, 0x9B, 0xE5, 0xCC, 0x27, 0x20, 0x85, 0xB7, 0x35, 0x9E, 0xEB, 0xC9, 0x71, + 0x6A, 0x79, 0xDF, 0xDA, 0xE2, 0xA1, 0x7E, 0x3C, 0x9D, 0xAD, 0x53, 0x64, 0xB1, 0x32, 0xE8, 0xDA, + 0xE8, 0x32, 0xC5, 0xFA, 0x36, 0xBB, 0x48, 0x6C, 0x4F, 0x60, 0xBA, 0x11, 0xCA, 0x26, 0x1F, 0x5A, + 0x95, 0x8D, 0xC0, 0x03, 0x30, 0xC4, 0x9B, 0xA3, 0xCE, 0x6C, 0x06, 0x10, 0xC8, 0x97, 0xB1, 0x9F, + 0xA3, 0x22, 0x13, 0xAC, 0x39, 0x4A, 0x8C, 0xFF, 0xC3, 0xD7, 0x71, 0x25, 0x3B, 0x40, 0x39, 0x25, + 0xB7, 0x13, 0xE1, 0x57, 0x76, 0xF6, 0x88, 0x69, 0x99, 0x8C, 0x1C, 0xB9, 0xA1, 0x0F, 0x26, 0x86, + 0x77, 0xC4, 0xDF, 0x0B, 0xB3, 0x49, 0x3C, 0xC5, 0x51, 0xC1, 0x39, 0x39, 0xCC, 0xFE, 0x63, 0xD6, + 0x1C, 0xCD, 0xBD, 0x4F, 0xCB, 0x82, 0x18, 0xC0, 0x0E, 0x18, 0xA5, 0x61, 0x06, 0x2F, 0x32, 0x6B, + 0x66, 0xDD, 0x84, 0x0A, 0x9C, 0xD0, 0xE0, 0xDA, 0xDB, 0x5B, 0x0C, 0x34, 0x86, 0xA2, 0xDB, 0xAD, + 0x4F, 0x99, 0x1A, 0x96, 0x9B, 0xE5, 0xF3, 0x85, 0x14, 0x63, 0x6F, 0xFF, 0x2D, 0xB2, 0x71, 0xD6, + 0x48, 0x1F, 0x07, 0xB8, 0x70, 0xA4, 0xDC, 0xEC, 0xE0, 0x3B, 0xE2, 0xC2, 0xCC, 0x2E, 0x69, 0x89, + 0x3B, 0xCE, 0x22, 0xC6, 0xDC, 0xEF, 0x91, 0x9D, 0xFB, 0xEC, 0x42, 0x23, 0x03, 0xA7, 0xD2, 0x06, + 0xB0, 0x67, 0x51, 0x5D, 0x61, 0xCB, 0x48, 0x68, 0xD3, 0x84, 0x2F, 0x1C, 0xE2, 0x6F, 0x3F, 0x43, + 0x96, 0x3C, 0xD2, 0x8E, 0x7D, 0xA2, 0xE3, 0x6C, 0xCB, 0xED, 0x5D, 0x32, 0xF8, 0x67, 0x63, 0x3C, + 0xF2, 0x93, 0x7F, 0x7E, 0xD2, 0xCF, 0xBE, 0x0D, 0x07, 0x32, 0x16, 0x0A, 0xE2, 0xC4, 0xEE, 0x4C, + 0x71, 0x88, 0xA9, 0xEE, 0x9C, 0x03, 0xFE, 0x71, 0x0B, 0xC9, 0x5E, 0x6C, 0xC3, 0x6C, 0x8E, 0x59, + 0x78, 0x4A, 0xD1, 0xB9, 0xDF, 0x9B, 0xE8, 0x75, 0x9A, 0x55, 0x64, 0x9C, 0x5C, 0xA7, 0x85, 0xBA, + 0xF4, 0xF2, 0xAC, 0x07, 0x75, 0x2E, 0x60, 0xDF, 0xBC, 0xA0, 0x84, 0xD8, 0x62, 0x44, 0x08, 0xD4, + 0xD9, 0x79, 0x96, 0x67, 0x55, 0x46, 0xCD, 0x2B, 0xBA, 0x02, 0x57, 0xBE, 0x5D, 0x6E, 0xEF, 0x7D, + 0xFF, 0x4A, 0xEE, 0x7C, 0xCB, 0xC9, 0x20, 0x6A, 0x28, 0x81, 0x1A, 0x96, 0x73, 0xE9, 0xC8, 0x37, + 0x74, 0x82, 0xB4, 0x65, 0x16, 0x5F, 0x66, 0x4B, 0xAF, 0xE3, 0xE4, 0x50, 0x6B, 0x20, 0x40, 0xB1, + 0x44, 0x07, 0x46, 0x64, 0x35, 0x08, 0xAF, 0x5E, 0xAA, 0x69, 0x6D, 0x5A, 0xAB, 0xC9, 0x52, 0x01, + 0x11, 0xDE, 0x13, 0xCE, 0x68, 0xA9, 0x86, 0xBA, 0x08, 0xB8, 0x85, 0x26, 0x89, 0x0B, 0xE6, 0x9B, + 0x35, 0xB3, 0x47, 0xE9, 0xE7, 0xCA, 0x9B, 0x85, 0xCC, 0x8B, 0xB2, 0x89, 0xF4, 0xC6, 0x65, 0x78, + 0x85, 0x66, 0xFF, 0x66, 0x74, 0x45, 0xCD, 0x32, 0x32, 0x01, 0x7A, 0x44, 0xE4, 0xA1, 0x4C, 0xC8, + 0x18, 0x80, 0xB2, 0x79, 0x04, 0x57, 0xCF, 0xF3, 0x5F, 0x08, 0x0E, 0x46, 0x1A, 0x20, 0x03, 0xC5, + 0xBA, 0x24, 0x67, 0xBB, 0xE2, 0x45, 0x72, 0x24, 0x45, 0x33, 0xFB, 0x12, 0x25, 0xCF, 0xBE, 0x3B, + 0x0F, 0x6E, 0x82, 0xBA, 0xC8, 0x4B, 0x03, 0x8A, 0x9B, 0x48, 0x4F, 0x78, 0xB9, 0x8D, 0x2B, 0xEB, + 0xBD, 0xFA, 0x2C, 0x08, 0x39, 0x49, 0x11, 0x52, 0xCD, 0x3A, 0xB7, 0x93, 0x25, 0x9D, 0x8C, 0x75, + 0x56, 0x7B, 0x98, 0x11, 0x9E, 0x61, 0x4F, 0x7A, 0x29, 0x12, 0xCA, 0xB3, 0xE6, 0xD8, 0x92, 0x27, + 0x39, 0x1B, 0x0C, 0x87, 0x1E, 0xDD, 0x00, 0x5E, 0x80, 0x06, 0x29, 0x97, 0xFB, 0xFE, 0xDD, 0x36, + 0x9B, 0x5C, 0x9A, 0xFA, 0x73, 0x89, 0x15, 0xC6, 0xFB, 0x13, 0xA8, 0x87, 0xF9, 0xA1, 0xF6, 0xAE, + 0xE7, 0xA5, 0x44, 0xD8, 0x2B, 0x76, 0x85, 0xEC, 0x3B, 0xCF, 0x6A, 0x13, 0x3B, 0xE1, 0xF8, 0x71, + 0x39, 0x6B, 0xD3, 0x33, 0xD6, 0x3A, 0x35, 0xDF, 0x13, 0xDB, 0xF0, 0x87, 0xF5, 0x47, 0x63, 0x1F, + 0x72, 0x33, 0x24, 0x0B, 0x95, 0x0A, 0xC6, 0x4B, 0x3B, 0xEB, 0x19, 0xAC, 0x64, 0x6A, 0x6B, 0x4C, + 0x2D, 0xB5, 0xBA, 0x86, 0x2C, 0x1A, 0x66, 0x9D, 0x2E, 0xE7, 0xC1, 0x83, 0x3F, 0x69, 0xE5, 0x60, + 0x0B, 0x01, 0x41, 0x15, 0xA6, 0x52, 0x4D, 0xD6, 0x58, 0xC8, 0x1A, 0x8B, 0xA9, 0xE5, 0x6B, 0x96, + 0xB4, 0xD2, 0xC5, 0x48, 0x1C, 0x1C, 0x12, 0xC1, 0x9D, 0x59, 0xBD, 0x14, 0x09, 0x26, 0xE8, 0x12, + 0xA4, 0x53, 0x4C, 0x38, 0x0B, 0xDF, 0x3A, 0x94, 0xDE, 0x87, 0x27, 0x5F, 0xFC, 0x60, 0xC8, 0x20, + 0x0D, 0x01, 0x47, 0x89, 0x80, 0x01, 0x06, 0x4B, 0x2E, 0xE5, 0xF4, 0x67, 0xC4, 0xF4, 0x9B, 0xF1, + 0x54, 0xA8, 0xC1, 0x1E, 0xC7, 0x68, 0x76, 0xAC, 0x00, 0x6A, 0x9E, 0x8F, 0x85, 0x92, 0xBD, 0xC5, + 0x37, 0x53, 0x5F, 0x71, 0x72, 0x5F, 0x4D, 0xB9, 0x56, 0x0C, 0x2B, 0x8F, 0x8C, 0xC1, 0xEC, 0xA8, + 0xDC, 0x8C, 0x35, 0x4F, 0x12, 0xBB, 0xDD, 0x88, 0x34, 0x91, 0x38, 0x38, 0xE3, 0x7C, 0x3F, 0xA7, + 0x94, 0xEB, 0xB0, 0x08, 0x72, 0x95, 0xE6, 0xF0, 0xF7, 0xC2, 0x16, 0x4D, 0x41, 0x9E, 0xF7, 0xFA, + 0x8F, 0x1B, 0xE6, 0x13, 0x61, 0x3E, 0xFB, 0x2E, 0x87, 0x47, 0x31, 0xFA, 0xFD, 0xAC, 0xCC, 0x7E, + 0xDB, 0xA8, 0xE0, 0x1C, 0xBF, 0xEA, 0x44, 0x91, 0x61, 0x8D, 0xB2, 0xFB, 0xED, 0x25, 0x80, 0xBD, + 0x50, 0x4B, 0x47, 0x63, 0x6D, 0x56, 0xEC, 0xEA, 0xD8, 0x3F, 0xED, 0x36, 0x9B, 0x7F, 0x3D, 0xEA, + 0x8C, 0xA7, 0xD9, 0xE3, 0x06, 0xE3, 0x3E, 0xC2, 0xCC, 0x7B, 0xDC, 0x58, 0x64, 0xA7, 0xC2, 0x18, + 0xA4, 0x38, 0x07, 0xFB, 0xC8, 0xAC, 0x31, 0x41, 0x6B, 0xC6, 0x63, 0x22, 0xF5, 0x51, 0x08, 0xA7, + 0x2D, 0x83, 0x57, 0xD5, 0x59, 0xAD, 0x3E, 0x11, 0x95, 0x62, 0xBB, 0x0C, 0x21, 0xB1, 0x52, 0x25, + 0x2B, 0x00, 0xF8, 0x65, 0x55, 0xDC, 0xB1, 0x8B, 0x12, 0x32, 0x0F, 0x31, 0xA1, 0x61, 0xFF, 0x6B, + 0xE3, 0x38, 0x40, 0x2C, 0xD4, 0x99, 0x9F, 0x42, 0x22, 0xAE, 0xD8, 0x08, 0x5A, 0x76, 0x0C, 0x4E, + 0x72, 0xB9, 0x81, 0x64, 0x6B, 0x6E, 0x87, 0xEF, 0x3A, 0x48, 0x84, 0xA1, 0x71, 0xA9, 0x2D, 0x42, + 0x6E, 0x20, 0x93, 0xE1, 0xC0, 0xD3, 0x9E, 0x21, 0x95, 0x3A, 0xA5, 0x0B, 0xE4, 0xEC, 0x08, 0xF3, + 0xCB, 0x9C, 0x5B, 0x30, 0xA9, 0xB3, 0x11, 0xB2, 0x9C, 0x79, 0x3F, 0xC0, 0x87, 0x03, 0x65, 0xA4, + 0x22, 0xF1, 0xC8, 0x27, 0x76, 0x18, 0xDB, 0x65, 0xBC, 0x41, 0x9E, 0x1C, 0xFE, 0xF6, 0xC5, 0xF3, + 0x97, 0xD4, 0x64, 0x93, 0xE8, 0xA6, 0x9F, 0xC1, 0x86, 0xE6, 0x35, 0x57, 0x67, 0x12, 0x04, 0xC0, + 0x7F, 0x74, 0x7B, 0xB5, 0x8A, 0x12, 0xCF, 0x1B, 0xAC, 0x69, 0x4B, 0x0B, 0xA2, 0x00, 0x07, 0x22, + 0xE4, 0x6D, 0x7F, 0xF3, 0xE1, 0x34, 0xBF, 0xA0, 0x9B, 0x52, 0x1C, 0x71, 0x5B, 0x4D, 0xD1, 0x34, + 0x75, 0xC3, 0xE1, 0x42, 0x14, 0x61, 0x30, 0x06, 0x4B, 0x4E, 0x8B, 0xA3, 0x6F, 0x2A, 0x10, 0x69, + 0xD8, 0xE8, 0xF8, 0x94, 0x74, 0x8F, 0x0C, 0xBC, 0x66, 0xFA, 0xF2, 0x58, 0x79, 0x11, 0x19, 0xC6, + 0xEE, 0x91, 0x81, 0x63, 0xE1, 0x6D, 0x62, 0x8E, 0xEB, 0x0A, 0x07, 0x73, 0x41, 0x02, 0x6C, 0x6D, + 0x7B, 0xF7, 0x84, 0xE6, 0x6B, 0xFE, 0x66, 0x01, 0xF2, 0xED, 0x29, 0x3D, 0x36, 0xB3, 0x37, 0xD5, + 0xD1, 0x85, 0x7A, 0x2D, 0xF4, 0x5E, 0xBB, 0x7E, 0x77, 0x55, 0x76, 0x22, 0xC7, 0xD9, 0x40, 0xBB, + 0xE2, 0xA2, 0xD8, 0x00, 0xC1, 0xE0, 0xF8, 0x3E, 0xC7, 0x91, 0xC4, 0x82, 0x9D, 0xBF, 0xDF, 0x2B, + 0x01, 0x42, 0x54, 0xAA, 0x8B, 0xA2, 0x4A, 0x7C, 0x69, 0xCF, 0x97, 0xE4, 0xEB, 0xBC, 0x70, 0xC1, + 0xAB, 0xBB, 0xA1, 0x59, 0x37, 0x06, 0xA8, 0xB7, 0xE5, 0x40, 0x2F, 0xA0, 0xC1, 0xA1, 0x28, 0x47, + 0x32, 0x6E, 0xDE, 0x25, 0x9F, 0xEC, 0x29, 0xB5, 0x38, 0x89, 0xE6, 0x98, 0x9C, 0x01, 0x0D, 0xD4, + 0x19, 0x71, 0x41, 0x47, 0x57, 0x4B, 0xA2, 0x4F, 0x16, 0x91, 0xEA, 0xB8, 0xF3, 0xAD, 0x28, 0x77, + 0xA1, 0x77, 0xB4, 0x6A, 0x6A, 0xE2, 0xEC, 0xB8, 0xA1, 0xFE, 0xA9, 0xE9, 0xA2, 0xAF, 0xFE, 0x55, + 0x24, 0xB6, 0xAB, 0x8E, 0x5B, 0x22, 0x91, 0x83, 0x6E, 0x8C, 0x01, 0x2E, 0xBF, 0xCA, 0xC6, 0xA5, + 0x91, 0xB8, 0xC5, 0x95, 0x63, 0x59, 0x19, 0x71, 0x35, 0x67, 0x38, 0xED, 0x52, 0xD8, 0xD3, 0xCE, + 0x8D, 0xCB, 0xDA, 0x45, 0x56, 0x54, 0x67, 0x35, 0x14, 0x6F, 0x5E, 0x7D, 0x43, 0xCE, 0xE4, 0xEA, + 0x8A, 0x63, 0x08, 0x62, 0xEC, 0x59, 0x34, 0x19, 0xE6, 0x08, 0xAB, 0xA2, 0xA2, 0x4B, 0x30, 0xA4, + 0x95, 0x98, 0x4D, 0x49, 0x31, 0xC0, 0x1C, 0x45, 0x63, 0x33, 0x4C, 0xD8, 0xE1, 0x07, 0x28, 0x5F, + 0x34, 0x38, 0xA9, 0x59, 0xCF, 0xFC, 0xE5, 0x2A, 0x2F, 0x2B, 0x4D, 0x08, 0x8C, 0x2F, 0x00, 0x08, + 0x28, 0x66, 0x0D, 0x30, 0x69, 0xA8, 0x1C, 0x93, 0xC0, 0xCE, 0xA4, 0x67, 0x3A, 0xA4, 0x17, 0x99, + 0x6D, 0x72, 0x54, 0xE9, 0xDC, 0x9A, 0x09, 0x14, 0x3E, 0x88, 0x92, 0x9E, 0xB2, 0x26, 0xF6, 0x00, + 0xD0, 0xE0, 0xB8, 0x1F, 0x43, 0x99, 0x5A, 0xDD, 0xB7, 0x2D, 0xC8, 0xDF, 0xC1, 0x5A, 0x2D, 0x61, + 0xD2, 0x48, 0x03, 0xD6, 0x1D, 0x76, 0x1F, 0xA7, 0x89, 0x90, 0xCA, 0x84, 0x82, 0x96, 0xDD, 0x19, + 0x79, 0x76, 0x58, 0x3E, 0x25, 0x0A, 0x36, 0x9F, 0x96, 0x01, 0x88, 0xD6, 0xE8, 0xB0, 0x39, 0xCB, + 0x9A, 0x31, 0x0D, 0xD8, 0xE9, 0xC2, 0x1C, 0x1B, 0xC6, 0x51, 0xD9, 0x82, 0xB2, 0x37, 0xB7, 0x6D, + 0xBA, 0x19, 0xFC, 0x5D, 0x00, 0x3A, 0xE0, 0xE0, 0x24, 0x54, 0x43, 0x32, 0xE8, 0x85, 0x73, 0xCD, + 0x75, 0x91, 0x9C, 0x5E, 0x15, 0x26, 0x0F, 0xD3, 0xCF, 0x1F, 0x37, 0x06, 0x5E, 0x3E, 0xB3, 0x2A, + 0xE0, 0x66, 0x18, 0x3D, 0x84, 0x1E, 0x7B, 0xAA, 0x4A, 0x0C, 0x76, 0x19, 0x41, 0x29, 0x77, 0xF4, + 0x21, 0x69, 0xC7, 0xDF, 0x23, 0xE1, 0x57, 0x1D, 0x43, 0xE6, 0x69, 0xA7, 0xA2, 0x37, 0x0D, 0xA1, + 0x17, 0x60, 0xEB, 0x74, 0xA4, 0x68, 0x35, 0x5E, 0x94, 0x49, 0x4B, 0xEB, 0x3F, 0xF9, 0x5B, 0x20, + 0x93, 0x9F, 0x3D, 0x9A, 0x02, 0x03, 0xFD, 0x80, 0xA7, 0x9F, 0x1E, 0x3E, 0x04, 0x5E, 0xFA, 0x2D, + 0x9E, 0xE2, 0xF9, 0xEC, 0xC1, 0x22, 0x79, 0x9B, 0xCD, 0xFF, 0xF6, 0x60, 0xF1, 0xF0, 0x48, 0xFD, + 0x83, 0x71, 0xCE, 0xE1, 0xC3, 0x19, 0x2E, 0x6F, 0x7B, 0x6F, 0xBA, 0xC5, 0xC3, 0x78, 0xFE, 0x37, + 0x6A, 0x71, 0xF1, 0x10, 0x28, 0xE9, 0xE2, 0x4A, 0xFD, 0x41, 0x70, 0xD2, 0x57, 0x4F, 0x4F, 0x37, + 0x5F, 0x3F, 0xFD, 0xF2, 0xB7, 0x48, 0x55, 0x5F, 0x53, 0xDA, 0x9B, 0xA3, 0x37, 0x47, 0x47, 0xEA, + 0x45, 0x47, 0x66, 0x33, 0x2F, 0xF9, 0xEF, 0x2B, 0x70, 0xEF, 0x1E, 0x1E, 0x45, 0xC6, 0x8C, 0x15, + 0xB6, 0x41, 0x89, 0x7A, 0x3E, 0xA2, 0x96, 0x93, 0xFB, 0x2E, 0x8B, 0xBF, 0x01, 0x35, 0x6C, 0xA1, + 0x2B, 0x9C, 0xF7, 0xE1, 0x91, 0x42, 0x6B, 0xC7, 0xFA, 0xAD, 0xD4, 0xF6, 0x89, 0xAF, 0x9F, 0xD0, + 0x63, 0x94, 0x05, 0xF2, 0x65, 0x23, 0xF2, 0xE8, 0x6B, 0x28, 0x44, 0x13, 0xF6, 0xC7, 0x83, 0x93, + 0x62, 0x86, 0xC4, 0xCA, 0x38, 0x6C, 0x41, 0x1D, 0x34, 0xAF, 0xE2, 0xDA, 0xD8, 0xC2, 0xB1, 0xF0, + 0xDC, 0x33, 0x3F, 0x4A, 0xD2, 0x5E, 0x9E, 0xE5, 0xD5, 0x6C, 0x9D, 0x27, 0x66, 0x14, 0xD4, 0x62, + 0x17, 0x0D, 0x3F, 0x2D, 0x4D, 0xD0, 0x3A, 0xEB, 0xD0, 0xE1, 0xCB, 0xCE, 0x7D, 0xFD, 0xCA, 0x9C, + 0xA4, 0xD6, 0x5F, 0x28, 0x39, 0x7F, 0x04, 0x73, 0xC5, 0x70, 0x4D, 0xBA, 0xB9, 0x44, 0x4C, 0x1F, + 0xA5, 0xD5, 0x62, 0xDD, 0xC3, 0x2E, 0x7D, 0xE0, 0x8A, 0xBD, 0x3D, 0xB5, 0xE4, 0x7C, 0x62, 0x3D, + 0xDB, 0x8F, 0x1B, 0xDC, 0x0E, 0x85, 0xB3, 0x97, 0xC6, 0x25, 0x87, 0xF7, 0xA6, 0xBB, 0x8C, 0xD3, + 0xDF, 0xC7, 0xE5, 0x71, 0x85, 0x3F, 0x6C, 0xDC, 0x9A, 0xA8, 0xC6, 0xC6, 0x65, 0xF6, 0x0B, 0x63, + 0xB6, 0x88, 0xEC, 0xD7, 0x96, 0xC9, 0xB8, 0xA1, 0xF0, 0x32, 0x7B, 0x1E, 0x95, 0xBB, 0x3E, 0x67, + 0x0A, 0x2A, 0x3F, 0x3F, 0xE4, 0xB7, 0xAF, 0x8B, 0xAE, 0xC3, 0xD8, 0xA0, 0x25, 0x0E, 0x7A, 0x4E, + 0x2C, 0x56, 0x6D, 0xAC, 0xF7, 0x4A, 0xFB, 0xCD, 0xB6, 0xEC, 0xCC, 0x0E, 0x23, 0xA6, 0x0B, 0x44, + 0x49, 0x23, 0x2F, 0xD2, 0x86, 0x74, 0xE0, 0x31, 0x7F, 0x49, 0x82, 0x77, 0xCE, 0x74, 0x58, 0xF0, + 0xC1, 0x03, 0xDF, 0xAC, 0x96, 0x68, 0x1C, 0x55, 0x6C, 0x9F, 0x77, 0x2C, 0x31, 0xCE, 0xDE, 0xE9, + 0x5F, 0x5F, 0x23, 0xEA, 0x8C, 0xBC, 0x1E, 0xA7, 0x53, 0x05, 0x36, 0x7D, 0x87, 0x00, 0x41, 0xE5, + 0x79, 0x59, 0x2C, 0xD9, 0xFC, 0xB6, 0xCB, 0x2F, 0xE8, 0xD7, 0x1F, 0x6B, 0x7A, 0xB7, 0x6E, 0x56, + 0xA9, 0x69, 0x84, 0x89, 0xF9, 0x08, 0xDB, 0x20, 0x02, 0x41, 0xFF, 0x87, 0x1A, 0x4C, 0x91, 0x54, + 0x6B, 0x61, 0xBC, 0xAB, 0x71, 0x58, 0xE3, 0x66, 0x44, 0xFF, 0x0E, 0xC0, 0xA7, 0x69, 0xE8, 0x64, + 0x3F, 0x9C, 0x1C, 0x70, 0x9F, 0x2D, 0x3E, 0x94, 0x0E, 0x78, 0x3E, 0xEB, 0x37, 0xB0, 0x41, 0xE3, + 0xE0, 0xAC, 0x9F, 0x1D, 0x69, 0x8C, 0xF3, 0x4E, 0x1C, 0x48, 0x40, 0xD6, 0x9A, 0x28, 0x1D, 0x0D, + 0x29, 0xC5, 0x67, 0x20, 0x0D, 0x14, 0x1E, 0x87, 0xDE, 0xA2, 0xD7, 0xBC, 0xFD, 0x50, 0x9D, 0xD1, + 0x83, 0x48, 0x5C, 0x4F, 0x79, 0x24, 0x74, 0x17, 0x2B, 0x35, 0xFD, 0x76, 0x74, 0x7B, 0x70, 0x73, + 0x73, 0x73, 0x80, 0xA9, 0xBC, 0x3A, 0xC0, 0x90, 0xF5, 0xB9, 0xB6, 0x3C, 0xD9, 0xA3, 0xD0, 0x96, + 0xE4, 0x3F, 0xE6, 0xBB, 0xD3, 0x67, 0x07, 0xBF, 0x8C, 0x14, 0xD1, 0x8D, 0xD7, 0x9D, 0x58, 0x0B, + 0xBE, 0xEA, 0x74, 0x00, 0x12, 0x4D, 0x6E, 0x5D, 0xD3, 0x81, 0x15, 0x71, 0xF0, 0x02, 0x49, 0xA1, + 0xC7, 0x48, 0x81, 0x0C, 0x0B, 0x7B, 0x42, 0x82, 0xDA, 0xB3, 0x14, 0x9A, 0xFA, 0xA1, 0xAD, 0xAB, + 0xB0, 0x00, 0xA5, 0x48, 0x89, 0x1F, 0xF2, 0xF7, 0xB9, 0x04, 0x92, 0xD9, 0x9A, 0xB1, 0xA3, 0x77, + 0x6A, 0xF3, 0xE8, 0xCD, 0x3B, 0xFC, 0xBC, 0x79, 0x77, 0x84, 0x2E, 0xF5, 0x2B, 0xFD, 0x1E, 0x71, + 0x7B, 0xF4, 0x46, 0xBF, 0xC8, 0x25, 0xD9, 0x66, 0x8B, 0xA3, 0x19, 0x24, 0x62, 0x59, 0xAC, 0x96, + 0x52, 0x39, 0x32, 0x89, 0xA0, 0xE3, 0x22, 0xFE, 0x0A, 0x97, 0x74, 0x8A, 0x37, 0x3D, 0x2C, 0x97, + 0xF6, 0xBB, 0xD7, 0x2F, 0xBE, 0xD5, 0x23, 0x80, 0x8D, 0x4F, 0x27, 0xF6, 0x92, 0x3C, 0xC4, 0x28, + 0xD5, 0x64, 0xA7, 0x26, 0x3A, 0xF7, 0xF8, 0x9B, 0x31, 0xD1, 0xF2, 0x4A, 0xAD, 0x44, 0x29, 0xD5, + 0xD6, 0x64, 0xAA, 0x24, 0xDF, 0x52, 0x21, 0x47, 0x14, 0x6F, 0x95, 0x03, 0x69, 0x0D, 0x32, 0x66, + 0xA9, 0x50, 0x18, 0x8F, 0x5B, 0x0B, 0x54, 0xEB, 0xEB, 0x1D, 0xF7, 0xB8, 0x19, 0x76, 0x0D, 0x6F, + 0x9C, 0x70, 0xAF, 0x24, 0x44, 0x90, 0x21, 0x3D, 0x4C, 0x05, 0x29, 0xA5, 0x5B, 0x74, 0xE1, 0x99, + 0x80, 0x43, 0x5F, 0x74, 0x09, 0x27, 0x9E, 0x36, 0x79, 0x85, 0xAF, 0x6E, 0x3A, 0x4A, 0x7C, 0x29, + 0x89, 0xBD, 0x6E, 0x2D, 0x65, 0x37, 0xC0, 0xAB, 0xD6, 0x66, 0xA0, 0xCB, 0x70, 0x05, 0x11, 0x9F, + 0x12, 0xCC, 0x2B, 0x54, 0x95, 0x5A, 0x62, 0x3B, 0x5F, 0xAA, 0x0B, 0xC6, 0x6A, 0xEF, 0xB1, 0xAD, + 0xED, 0x57, 0x89, 0x31, 0xC5, 0x07, 0xB0, 0x0A, 0xE5, 0xB3, 0x61, 0xC1, 0xA7, 0xAE, 0xDC, 0x2B, + 0x9A, 0xFF, 0xE0, 0x39, 0x06, 0xF9, 0x20, 0xC4, 0x12, 0x0E, 0xC8, 0xF8, 0x43, 0x92, 0x1A, 0xF6, + 0xE5, 0x6D, 0xE0, 0x78, 0x40, 0xBD, 0xC3, 0xEB, 0x13, 0x70, 0x1E, 0xC9, 0xB7, 0x1C, 0x8E, 0x79, + 0x8A, 0xC9, 0xB5, 0x77, 0x85, 0xAB, 0x4C, 0x43, 0xF6, 0x6D, 0x37, 0x68, 0xBC, 0xED, 0xF2, 0x6E, + 0xDD, 0x3E, 0x41, 0xB3, 0x34, 0x54, 0x1D, 0xFB, 0x5E, 0xD0, 0x6C, 0xC4, 0xA1, 0x65, 0x89, 0x26, + 0x50, 0xA7, 0xD9, 0x1D, 0xA9, 0x41, 0x7C, 0x78, 0x8D, 0xD2, 0xB4, 0xF1, 0xB1, 0xFF, 0x5E, 0x09, + 0x5C, 0x7C, 0x8D, 0xF4, 0xB1, 0x50, 0xE1, 0x1C, 0x6E, 0xCE, 0x38, 0xDE, 0xBC, 0xAB, 0xD0, 0xA4, + 0xB0, 0x8D, 0xBA, 0xEC, 0x1F, 0x9D, 0xB6, 0x92, 0xBD, 0x4E, 0xC8, 0x08, 0x61, 0x10, 0x2E, 0x83, + 0x03, 0xEA, 0x67, 0xF1, 0xAE, 0xAC, 0xD0, 0xCD, 0x43, 0x07, 0x53, 0xB3, 0x64, 0x4B, 0x01, 0x90, + 0x8A, 0x91, 0xA2, 0xDB, 0x80, 0xC2, 0xE8, 0x34, 0x85, 0xD1, 0x09, 0xD1, 0xA7, 0x48, 0x87, 0x8B, + 0x3E, 0x05, 0x61, 0x1E, 0xC2, 0xAF, 0x19, 0x73, 0x9B, 0x7D, 0x39, 0xBB, 0x4E, 0xAD, 0x22, 0xCA, + 0x2B, 0xED, 0xAC, 0x43, 0x97, 0x1E, 0xC2, 0xA3, 0xEB, 0x52, 0x07, 0xA5, 0x6B, 0xFB, 0x83, 0x5B, + 0x8C, 0x24, 0x81, 0xFE, 0x55, 0x39, 0x9D, 0x5B, 0x9D, 0x98, 0xC9, 0x2B, 0xB2, 0xA6, 0x6B, 0xA0, + 0x9A, 0xFE, 0xBC, 0xBC, 0xE2, 0x45, 0xF7, 0x3A, 0x1A, 0xE9, 0xE6, 0xFD, 0xE1, 0x95, 0x94, 0xCB, + 0x0A, 0xD3, 0x82, 0x5B, 0xDD, 0xD1, 0x05, 0xC2, 0x33, 0xAF, 0xD2, 0xA9, 0xB1, 0x05, 0x2C, 0xE6, + 0xA7, 0x02, 0x11, 0x8B, 0xE1, 0x55, 0xE3, 0x86, 0x74, 0x9F, 0xE6, 0xF4, 0x97, 0xAF, 0x19, 0x8B, + 0x30, 0x30, 0x0C, 0x30, 0x78, 0x33, 0xAA, 0xA0, 0x83, 0xE3, 0xD3, 0x94, 0x84, 0x99, 0xE4, 0xD9, + 0x21, 0x17, 0x64, 0x27, 0x91, 0xF1, 0x54, 0x99, 0x6F, 0xE5, 0x5B, 0xC2, 0xAD, 0xF5, 0x03, 0x71, + 0x0A, 0xF5, 0xAC, 0x43, 0x20, 0x81, 0x2C, 0x8E, 0x51, 0x9F, 0x1F, 0x37, 0x1B, 0x39, 0x43, 0x12, + 0xD2, 0x28, 0xB5, 0xB4, 0xDB, 0xD7, 0x9D, 0xF2, 0xF0, 0x3F, 0x62, 0x53, 0x1E, 0x45, 0xA8, 0xAB, + 0x39, 0x74, 0xD0, 0xB5, 0x2C, 0xBA, 0xCB, 0x1A, 0xD4, 0x69, 0xC7, 0x09, 0x68, 0xC9, 0xA6, 0xE8, + 0x22, 0x28, 0x69, 0xCF, 0xE4, 0x2C, 0x76, 0x2F, 0x4C, 0xBC, 0x24, 0xBB, 0xA9, 0xA1, 0x28, 0x32, + 0xA6, 0xAA, 0xD8, 0x9E, 0x4D, 0x8D, 0x53, 0xA6, 0xBE, 0x02, 0xEE, 0xC7, 0xAA, 0x8C, 0x12, 0x6A, + 0x7C, 0x71, 0x6F, 0xF4, 0x71, 0xCA, 0x1F, 0xA3, 0xE4, 0x45, 0xFF, 0xA8, 0xA0, 0x91, 0xEC, 0x79, + 0xEF, 0x7B, 0x26, 0x74, 0x12, 0xD7, 0x6D, 0xB7, 0x9F, 0x35, 0xBD, 0x8C, 0x86, 0xD3, 0xDD, 0x05, + 0x3F, 0x6C, 0x87, 0xB0, 0x66, 0x79, 0x2E, 0x5F, 0x45, 0xE6, 0xB4, 0xEE, 0x44, 0xC4, 0xEB, 0x80, + 0xCE, 0x31, 0xE5, 0xA4, 0x42, 0x26, 0xB7, 0x1C, 0x79, 0xA5, 0x39, 0x25, 0xD1, 0x13, 0x2D, 0x6E, + 0x0E, 0x66, 0xAC, 0xFA, 0x33, 0xA1, 0x4B, 0xF5, 0x5E, 0x75, 0x0A, 0x8B, 0x65, 0xF5, 0x54, 0x4E, + 0x6D, 0x5C, 0x8E, 0xF8, 0xC2, 0x30, 0xC8, 0xA9, 0x6B, 0x7D, 0x2C, 0xE3, 0xDA, 0x30, 0xCD, 0x90, + 0xAC, 0x09, 0x88, 0xC9, 0xE4, 0xC1, 0x83, 0x3E, 0x53, 0x2C, 0x22, 0x7C, 0xC8, 0x1E, 0x50, 0xDD, + 0x32, 0xEA, 0x9F, 0xD0, 0x33, 0x20, 0xF2, 0xA0, 0x27, 0x60, 0xA2, 0x14, 0xED, 0xFF, 0x41, 0x2E, + 0x26, 0xBA, 0x68, 0xA2, 0x64, 0x9A, 0x2D, 0x94, 0xFC, 0x40, 0xD2, 0xD9, 0xB0, 0xD2, 0x6C, 0x74, + 0x5E, 0x78, 0x80, 0x04, 0x09, 0x96, 0x4A, 0x60, 0x60, 0xF0, 0xB4, 0x05, 0x3E, 0x4E, 0x32, 0x44, + 0x89, 0x9B, 0x44, 0xFD, 0xE3, 0x02, 0x1E, 0x62, 0x1C, 0x93, 0x88, 0x69, 0x5F, 0x3D, 0x42, 0x21, + 0x97, 0xCF, 0x8D, 0xF6, 0x8E, 0x72, 0xAB, 0xE0, 0x0F, 0xCB, 0x71, 0x86, 0xB2, 0x70, 0xB9, 0xA8, + 0xAB, 0xF3, 0x49, 0x16, 0x9F, 0xCA, 0x04, 0x9C, 0x27, 0x33, 0x5C, 0x66, 0xD3, 0x68, 0x06, 0xCE, + 0x82, 0x2E, 0x61, 0x9C, 0x54, 0xE8, 0x37, 0xCD, 0xE7, 0x66, 0x71, 0x14, 0xAE, 0x6E, 0x54, 0x39, + 0x3B, 0xB7, 0xA3, 0xFB, 0x2D, 0x46, 0xF7, 0xD9, 0x31, 0x79, 0xFE, 0x1D, 0x6F, 0x30, 0x7A, 0x8B, + 0xFB, 0xFE, 0x4D, 0x77, 0x48, 0x4C, 0xAE, 0xC9, 0x64, 0x52, 0x9B, 0x4D, 0x7A, 0xAE, 0x1F, 0xCB, + 0x73, 0x43, 0x0B, 0xB2, 0x23, 0x0A, 0x9F, 0x38, 0x9C, 0x9F, 0x83, 0x14, 0x3D, 0x3D, 0xEC, 0x63, + 0x4E, 0x30, 0x93, 0xCE, 0x0F, 0x4C, 0x99, 0x83, 0xD7, 0x25, 0x45, 0x8C, 0x54, 0x83, 0x9A, 0x09, + 0x92, 0x88, 0xB2, 0xBC, 0xAF, 0x91, 0x6F, 0x6B, 0x18, 0x3F, 0x3F, 0xA7, 0x7D, 0x10, 0xB9, 0xD2, + 0x80, 0x52, 0x07, 0xFD, 0x6E, 0xE9, 0x0D, 0x4B, 0xBE, 0xB7, 0xC8, 0x9D, 0xFF, 0x9A, 0x8C, 0xF7, + 0x24, 0x0D, 0x1C, 0x50, 0x91, 0x48, 0x05, 0x0D, 0x24, 0x6A, 0xAC, 0xC2, 0x97, 0x4C, 0x2D, 0xA2, + 0x68, 0x40, 0xFB, 0xD3, 0x70, 0x84, 0x8E, 0x9C, 0x87, 0x39, 0x8B, 0xD9, 0xCE, 0x9C, 0x09, 0x5D, + 0x12, 0x30, 0xEC, 0x30, 0x79, 0x46, 0xC7, 0xD8, 0xE4, 0x15, 0xB8, 0x3F, 0x27, 0x7B, 0xFF, 0xC8, + 0xA6, 0x87, 0xD3, 0xE3, 0x88, 0xD8, 0x4A, 0xA9, 0x6D, 0x86, 0xAF, 0x18, 0x0C, 0xFA, 0xFA, 0x68, + 0x4B, 0x46, 0x86, 0x59, 0xBA, 0x6C, 0x0E, 0xC0, 0xCC, 0x58, 0x43, 0xC7, 0x47, 0x7A, 0x0D, 0x8A, + 0xDF, 0xB8, 0xB1, 0xF2, 0xD3, 0xB4, 0x3C, 0xF3, 0x83, 0x3A, 0x55, 0xEF, 0x81, 0x10, 0x2F, 0x13, + 0x8B, 0x00, 0x04, 0xBF, 0x73, 0x23, 0xA0, 0x21, 0xF8, 0x0D, 0xCC, 0x6B, 0x0E, 0x8C, 0xFB, 0xDE, + 0x39, 0xFD, 0x50, 0xA7, 0x6C, 0xB1, 0x8D, 0xA4, 0x76, 0xCD, 0x9C, 0x58, 0xA4, 0x68, 0x47, 0x20, + 0xEF, 0x35, 0xA3, 0x91, 0x24, 0x6C, 0xC0, 0x31, 0x2F, 0x0D, 0x8E, 0x61, 0x52, 0xE2, 0xF4, 0xD0, + 0x91, 0x21, 0xD9, 0x31, 0x99, 0x5E, 0x5F, 0xF5, 0x50, 0x07, 0x07, 0x7A, 0x9A, 0x63, 0x54, 0x8B, + 0x00, 0x2B, 0xBD, 0x3F, 0x64, 0xFA, 0x9F, 0x3C, 0xA2, 0x03, 0x47, 0x68, 0xD3, 0x1C, 0x16, 0x81, + 0xEE, 0xF2, 0x8D, 0x65, 0xBE, 0x23, 0x92, 0xC2, 0x44, 0x2A, 0xD8, 0x9A, 0x89, 0xC6, 0xE8, 0x97, + 0x64, 0x03, 0x79, 0x86, 0x06, 0x70, 0x29, 0xCA, 0xD5, 0xCA, 0xE3, 0xB4, 0xF2, 0xA9, 0xDA, 0x5D, + 0x42, 0x0F, 0x74, 0x0F, 0x31, 0x24, 0x20, 0x7D, 0x25, 0x7A, 0x53, 0x6B, 0x61, 0xF1, 0x5B, 0xF4, + 0x6D, 0xBD, 0x67, 0x89, 0x4C, 0xFF, 0x22, 0xBF, 0xDA, 0x29, 0x6D, 0xCE, 0xBA, 0x93, 0x4B, 0x5C, + 0xE1, 0xD8, 0x52, 0x11, 0x2B, 0xD2, 0xB3, 0x32, 0x5A, 0x62, 0xBA, 0x8C, 0xFC, 0xE5, 0x3A, 0x6B, + 0x08, 0x65, 0xA9, 0x60, 0xB2, 0xA6, 0x8F, 0x8B, 0xD9, 0x17, 0x29, 0x5D, 0xF2, 0x61, 0x3A, 0xF5, + 0x98, 0x63, 0xB4, 0x3D, 0xFE, 0x7C, 0x3A, 0xDD, 0x6C, 0x3E, 0x9F, 0x7E, 0x41, 0x82, 0x2F, 0x55, + 0x71, 0x70, 0xE4, 0x7B, 0xDD, 0x80, 0x64, 0x85, 0x01, 0x78, 0x8C, 0x08, 0x2F, 0x16, 0x0E, 0x85, + 0xB2, 0x03, 0xA4, 0xA1, 0xA9, 0x35, 0x5D, 0x6E, 0xD7, 0x46, 0x94, 0xEC, 0x3C, 0xFD, 0x36, 0xEC, + 0x93, 0xB5, 0x60, 0x2A, 0xC5, 0xEC, 0x38, 0x8F, 0x8E, 0x1C, 0xDD, 0x62, 0x09, 0x83, 0x51, 0xE3, + 0x42, 0x34, 0xB7, 0x09, 0xDE, 0x01, 0xA4, 0xD8, 0x42, 0xF4, 0x57, 0xE3, 0xA8, 0x06, 0xC0, 0xB1, + 0xB6, 0xF7, 0x6F, 0x70, 0xF7, 0xDE, 0xE1, 0xC3, 0xFF, 0x4E, 0x67, 0x1F, 0x8D, 0x05, 0xB5, 0xAA, + 0xA4, 0xE6, 0x61, 0x31, 0x49, 0xE3, 0x82, 0x8F, 0x57, 0x9A, 0x3C, 0xA5, 0x1C, 0x62, 0x3E, 0xBA, + 0xAB, 0x0D, 0xA4, 0x9A, 0xA4, 0x2F, 0x46, 0x19, 0x88, 0xDD, 0x56, 0x67, 0xA5, 0xB4, 0x98, 0x73, + 0xE8, 0x26, 0x30, 0x57, 0xD9, 0x2D, 0x47, 0x4E, 0x3D, 0x58, 0x4D, 0xF4, 0x7A, 0x9F, 0x3F, 0x1D, + 0x03, 0x73, 0x43, 0xA9, 0x49, 0xC4, 0x56, 0x2F, 0xB6, 0xF1, 0x7B, 0x75, 0xCA, 0x6E, 0x8F, 0xF7, + 0xA1, 0x7A, 0x17, 0x68, 0x56, 0x70, 0x8C, 0xD0, 0x1E, 0x82, 0x00, 0xEE, 0xF1, 0x0A, 0xF0, 0xFD, + 0xC9, 0xCF, 0xD6, 0xEA, 0xAE, 0xEF, 0xFD, 0xE1, 0xEA, 0x2B, 0x95, 0x34, 0xB5, 0xF0, 0x8D, 0xD1, + 0xB6, 0xE4, 0x90, 0xF7, 0xA3, 0x9A, 0x0C, 0x44, 0xEF, 0x9F, 0xF9, 0x0B, 0x2A, 0xC7, 0x12, 0xCF, + 0xFE, 0x19, 0xA8, 0x70, 0x5E, 0x80, 0x9C, 0xE6, 0xCC, 0x9F, 0xA6, 0x64, 0x35, 0xCF, 0xFB, 0x74, + 0x6D, 0x30, 0x8D, 0xF9, 0x02, 0xE2, 0xE8, 0x33, 0x03, 0x0A, 0x46, 0xB1, 0x94, 0x96, 0xB0, 0x38, + 0x0C, 0xEF, 0xA4, 0x73, 0x89, 0x20, 0x3A, 0x92, 0xCE, 0x04, 0xF1, 0x3E, 0x64, 0xA3, 0x00, 0x20, + 0x3D, 0xC4, 0x67, 0x7C, 0x61, 0xE3, 0xEB, 0x96, 0x9F, 0x10, 0x77, 0xCA, 0x7D, 0x02, 0xAB, 0x2E, + 0xD4, 0xCA, 0xEB, 0x1E, 0xDD, 0x0A, 0x84, 0xD6, 0x04, 0x0E, 0x86, 0xD7, 0x2E, 0x58, 0x15, 0xED, + 0xAF, 0xF1, 0x53, 0x6B, 0x90, 0xC0, 0x2A, 0xAF, 0xE6, 0x6B, 0x06, 0x82, 0x9A, 0x7C, 0x72, 0x01, + 0x89, 0xF2, 0x63, 0xE2, 0x20, 0x71, 0x45, 0xED, 0x61, 0xC7, 0x94, 0x9E, 0xF7, 0xDA, 0x04, 0x53, + 0xA5, 0x0D, 0x12, 0xBC, 0x06, 0x08, 0x4B, 0xBB, 0x36, 0xF8, 0x0D, 0xF0, 0xCA, 0xAE, 0xAA, 0xF3, + 0x19, 0x15, 0x2B, 0x17, 0x29, 0xF9, 0x2A, 0xE0, 0x27, 0xB6, 0x3D, 0xA7, 0x32, 0xEA, 0xCC, 0x42, + 0x51, 0x4B, 0x0B, 0xE0, 0x01, 0x35, 0x17, 0xCE, 0xA9, 0xFB, 0x1C, 0x33, 0x32, 0x8F, 0x18, 0xCF, + 0xB4, 0x40, 0xF2, 0x5D, 0x96, 0xC7, 0x9D, 0xD0, 0xED, 0x84, 0x9F, 0xF8, 0xDD, 0x60, 0x25, 0xC7, + 0x35, 0x6D, 0xF9, 0x12, 0x17, 0x08, 0x7A, 0x14, 0xFF, 0x40, 0x77, 0x0B, 0xE9, 0x40, 0x4D, 0x7A, + 0x09, 0x5B, 0x42, 0x48, 0xE7, 0xA0, 0xC8, 0x31, 0x6C, 0xFA, 0x94, 0xAE, 0xC6, 0x03, 0xA9, 0x23, + 0x85, 0xED, 0x08, 0xFE, 0x8E, 0x14, 0x4D, 0x7D, 0xDA, 0x6D, 0x09, 0xDA, 0x5B, 0xC0, 0x7B, 0x99, + 0xA8, 0x72, 0x16, 0xF7, 0xC8, 0x02, 0x1C, 0x09, 0xA7, 0x63, 0xBB, 0xFE, 0x0F, 0x38, 0xF2, 0x2D, + 0x25, 0x80, 0x89, 0x1C, 0xA3, 0x20, 0xB2, 0x75, 0xA2, 0x76, 0xD5, 0xA7, 0x13, 0xDF, 0x54, 0xA3, + 0x67, 0x5D, 0x3C, 0x51, 0x8F, 0x34, 0x76, 0x03, 0x3E, 0x24, 0xB6, 0x2B, 0x16, 0x5F, 0xA8, 0xC9, + 0x19, 0xBB, 0x53, 0x31, 0xB1, 0x0E, 0x52, 0x8D, 0x04, 0x25, 0xB5, 0xBB, 0x32, 0xE3, 0x48, 0x63, + 0x72, 0x4E, 0xC7, 0xDF, 0xA9, 0xB0, 0x2C, 0x9A, 0x9E, 0x2A, 0x33, 0x02, 0x8F, 0x56, 0x4E, 0x29, + 0xD0, 0x72, 0xB4, 0xD6, 0x8A, 0x6C, 0xDF, 0x29, 0x36, 0x31, 0x5A, 0x30, 0x53, 0xCA, 0xFB, 0x14, + 0x98, 0x17, 0x65, 0x94, 0xB9, 0x56, 0x65, 0x85, 0x7D, 0xE4, 0x38, 0x41, 0x24, 0xE8, 0x5C, 0xD1, + 0x8D, 0x06, 0x73, 0x75, 0x1B, 0x78, 0x52, 0xFB, 0xA0, 0xE6, 0xB5, 0x5A, 0xA9, 0xD3, 0x45, 0x92, + 0xDE, 0x3A, 0x57, 0x6A, 0x9C, 0x7E, 0x8A, 0xF4, 0x7C, 0x81, 0x46, 0xBD, 0xDB, 0x7B, 0x7C, 0xC3, + 0xF7, 0x79, 0x46, 0xB6, 0xE1, 0xB9, 0x58, 0xCE, 0xF4, 0xC9, 0x28, 0xCB, 0x94, 0xF2, 0xDB, 0x53, + 0x1E, 0x23, 0x1F, 0x94, 0xE8, 0xB9, 0x4E, 0xA9, 0xB9, 0x77, 0xEC, 0xA0, 0xC8, 0xEB, 0x04, 0x69, + 0x68, 0x29, 0xEE, 0x1D, 0xB1, 0x4F, 0xE4, 0x08, 0xB7, 0xC7, 0xEC, 0xC1, 0x81, 0xA1, 0xE8, 0x37, + 0x9B, 0x1D, 0xF4, 0x7C, 0xCD, 0xAA, 0x7A, 0x5B, 0x73, 0x1C, 0xF3, 0xAD, 0x9B, 0x58, 0x42, 0x7D, + 0x55, 0x00, 0x27, 0x61, 0x41, 0x01, 0x41, 0x57, 0x8C, 0x01, 0xE5, 0xA6, 0xFE, 0x9A, 0x51, 0x5C, + 0x50, 0x6B, 0x50, 0x47, 0xE6, 0xA0, 0x73, 0xF1, 0x97, 0x03, 0xFD, 0x39, 0x14, 0x8A, 0x14, 0x45, + 0x7A, 0xEA, 0x42, 0xDB, 0xF7, 0x12, 0x02, 0x73, 0x8A, 0x1A, 0xB8, 0xD3, 0x3D, 0xC7, 0x95, 0x09, + 0xBE, 0x87, 0xD3, 0xB5, 0x52, 0x15, 0xB9, 0x34, 0x36, 0x9C, 0x1E, 0x61, 0x39, 0x39, 0xAF, 0x64, + 0xCC, 0xC0, 0x2A, 0x34, 0xB7, 0xB3, 0x54, 0x06, 0x27, 0xA5, 0x8D, 0xEC, 0x11, 0x25, 0x7B, 0x26, + 0xAD, 0xB6, 0x6A, 0x28, 0xE9, 0x22, 0x2D, 0x29, 0x19, 0x73, 0xC0, 0xA1, 0x1A, 0x51, 0x14, 0x76, + 0xB7, 0x73, 0x4B, 0xDE, 0xD9, 0xE8, 0x62, 0xD4, 0x39, 0x00, 0xBE, 0x6F, 0x66, 0x47, 0x30, 0xE9, + 0xD3, 0xAD, 0x99, 0xAD, 0x8A, 0x3B, 0x3D, 0x5F, 0x75, 0xD0, 0xB3, 0x8B, 0xF6, 0xBE, 0xCB, 0xBE, + 0x5D, 0x8F, 0xCD, 0x7C, 0xA8, 0x63, 0xEB, 0xBA, 0x8F, 0xB5, 0x47, 0x1B, 0x5F, 0x35, 0x3C, 0x16, + 0xEC, 0xB1, 0x65, 0xD2, 0x1E, 0x87, 0xEC, 0x44, 0xFF, 0x14, 0x4B, 0xFD, 0x43, 0x6C, 0xAB, 0x1C, + 0x96, 0xEF, 0x79, 0xD6, 0x75, 0xD1, 0xEF, 0x63, 0x63, 0x63, 0xDF, 0x17, 0xE5, 0xDD, 0x34, 0xF9, + 0x35, 0x58, 0x3C, 0xBB, 0x3D, 0x06, 0x8B, 0xB6, 0x06, 0x81, 0x3A, 0xF2, 0x78, 0xD3, 0x16, 0x9A, + 0xBE, 0x95, 0x9C, 0x24, 0x21, 0xDF, 0xF0, 0xE8, 0x40, 0xDE, 0x43, 0xAD, 0x83, 0xE4, 0xB0, 0xF8, + 0x47, 0x3C, 0x4D, 0x5C, 0xD8, 0x4E, 0x5B, 0xCC, 0xB7, 0x3C, 0xEA, 0x45, 0x30, 0x96, 0x22, 0x28, + 0x3B, 0x2E, 0x08, 0xA4, 0x7C, 0xA3, 0xF2, 0xCF, 0xA1, 0x8E, 0x45, 0x56, 0x2F, 0x11, 0x8F, 0x8B, + 0x6C, 0x24, 0xD5, 0x85, 0xA0, 0x37, 0x66, 0xE3, 0x5A, 0xDA, 0x66, 0xB8, 0x41, 0x34, 0x11, 0xDF, + 0x54, 0x81, 0x33, 0xBA, 0xCA, 0x03, 0xEF, 0xEA, 0x63, 0x96, 0x0E, 0xB6, 0x7E, 0x1C, 0x68, 0xA0, + 0x27, 0xF7, 0x9B, 0x36, 0x58, 0x43, 0x86, 0xCE, 0xA3, 0x1F, 0xC9, 0x81, 0x81, 0xD1, 0x88, 0xEF, + 0x0E, 0x65, 0x85, 0x30, 0x82, 0xB4, 0x30, 0x23, 0xAF, 0xD0, 0xAA, 0x1E, 0xB2, 0x1B, 0x6D, 0x20, + 0xA5, 0x3E, 0xF9, 0x24, 0xDB, 0x1B, 0xDB, 0x34, 0xBA, 0xF1, 0xC7, 0x9C, 0x76, 0xDC, 0xFE, 0xBA, + 0x42, 0x81, 0x9D, 0x26, 0x8E, 0xB2, 0x80, 0x48, 0x25, 0xE7, 0x97, 0x31, 0x8C, 0x26, 0x96, 0xE0, + 0x9B, 0xF6, 0x3A, 0x73, 0x7D, 0x79, 0x91, 0x62, 0x39, 0xC5, 0x53, 0x88, 0x48, 0xB6, 0xCE, 0xF7, + 0xA5, 0x58, 0xD9, 0x5E, 0xB7, 0xC5, 0x7A, 0x59, 0xB7, 0xC6, 0xA7, 0xD5, 0x70, 0x08, 0xFB, 0xBD, + 0x82, 0x1C, 0xFB, 0x4B, 0x02, 0xA9, 0x8E, 0x67, 0x8D, 0x35, 0xB2, 0x1F, 0x17, 0x7E, 0xD0, 0x45, + 0x10, 0xBA, 0x41, 0x90, 0xB6, 0x7B, 0xE2, 0x53, 0xA0, 0x9B, 0x9E, 0xD8, 0xE9, 0xF6, 0xB2, 0xF1, + 0x89, 0x4B, 0xD0, 0x18, 0x66, 0xB6, 0xB4, 0x4A, 0x0A, 0x18, 0xF9, 0x5F, 0x77, 0xDD, 0xB5, 0x5C, + 0x2E, 0xFD, 0x30, 0xC4, 0x3A, 0xDE, 0x1B, 0x04, 0x9C, 0x53, 0x76, 0xDD, 0x70, 0xFC, 0xE8, 0xD1, + 0xE7, 0x78, 0xF8, 0x62, 0xAB, 0x3E, 0xEB, 0xB2, 0x61, 0x27, 0x80, 0x8E, 0x0F, 0x00, 0x95, 0xA6, + 0xCD, 0xF6, 0xF7, 0x3F, 0xC3, 0x3D, 0x2D, 0xBA, 0xC1, 0x94, 0x3E, 0x01, 0x47, 0x1B, 0x63, 0x84, + 0x64, 0xBD, 0x8D, 0x28, 0xC2, 0x5A, 0xA7, 0x3E, 0x70, 0xC5, 0x0C, 0x4D, 0x50, 0x31, 0x19, 0xAC, + 0xBD, 0x56, 0xB9, 0x15, 0x12, 0xBD, 0x0E, 0x90, 0xBB, 0x44, 0xCF, 0xEA, 0x96, 0x37, 0x1B, 0x6A, + 0x78, 0xBF, 0x0C, 0xD8, 0x77, 0x86, 0xB0, 0x01, 0xFC, 0xED, 0xB0, 0xE2, 0x2E, 0x79, 0x78, 0xFA, + 0x56, 0xC2, 0x21, 0x6F, 0xE3, 0x52, 0x33, 0x12, 0x4B, 0x66, 0xEB, 0x95, 0xFA, 0x9A, 0x49, 0x6F, + 0x6D, 0xC1, 0xDA, 0xAB, 0x78, 0xBC, 0xCE, 0xDB, 0xF6, 0xA6, 0x6E, 0x70, 0x45, 0xE3, 0xDA, 0x9A, + 0xB0, 0x75, 0x2E, 0x45, 0xFD, 0xC4, 0x86, 0x64, 0x73, 0x5E, 0x02, 0x5E, 0x4F, 0x5C, 0x41, 0x7B, + 0x75, 0x22, 0x6F, 0x34, 0x8E, 0x39, 0xBC, 0x3B, 0x2D, 0x76, 0x55, 0x12, 0x15, 0x7C, 0x2A, 0x96, + 0x7D, 0x1E, 0x7D, 0x7F, 0x20, 0x2B, 0x05, 0xE6, 0x0B, 0x01, 0x2D, 0xB1, 0xD6, 0xE3, 0xD1, 0xF4, + 0x2C, 0x0A, 0x97, 0x96, 0xB4, 0x73, 0x93, 0x66, 0xC0, 0x48, 0x10, 0x15, 0x0B, 0xA8, 0xA1, 0x8E, + 0x69, 0x80, 0x7B, 0x90, 0x53, 0x33, 0x9D, 0x9B, 0x67, 0x18, 0x72, 0xB5, 0xAA, 0xF3, 0x25, 0x3F, + 0x30, 0xDD, 0xC4, 0x4F, 0x7C, 0xF5, 0xE6, 0x27, 0xB9, 0x70, 0xF3, 0x33, 0xDF, 0x66, 0x99, 0x18, + 0x83, 0x48, 0xAE, 0xBA, 0xD0, 0xE1, 0xC6, 0x95, 0x70, 0x18, 0x98, 0x7C, 0x6B, 0x0C, 0xF3, 0x21, + 0x15, 0x22, 0x8C, 0x53, 0x87, 0x3E, 0xC9, 0x84, 0x6E, 0x9A, 0x75, 0xE0, 0x40, 0x4B, 0x49, 0x20, + 0x84, 0xD8, 0xA4, 0xAB, 0xC6, 0x23, 0xD1, 0x28, 0xE3, 0x37, 0xDD, 0xDC, 0x24, 0x61, 0x8A, 0x46, + 0x8B, 0x69, 0xB1, 0x96, 0x8E, 0x30, 0x62, 0xEE, 0x30, 0x72, 0xCD, 0xD5, 0x39, 0xC9, 0x88, 0x76, + 0x97, 0x57, 0x12, 0x45, 0x66, 0x77, 0x50, 0x8C, 0xCD, 0x9B, 0x0F, 0xA9, 0x4B, 0xDE, 0xA6, 0x77, + 0x2C, 0x00, 0x0B, 0x0B, 0x6E, 0xD1, 0xF3, 0xA8, 0xD8, 0x22, 0x4E, 0xD8, 0x06, 0xC4, 0xCE, 0x6A, + 0x4D, 0xE6, 0x10, 0x6E, 0x6E, 0xFD, 0x19, 0x45, 0x9E, 0x7C, 0xB9, 0xB3, 0x84, 0x76, 0x73, 0x3F, + 0x73, 0xAB, 0x90, 0xA7, 0xA3, 0x73, 0xEF, 0x2D, 0x26, 0x91, 0xCE, 0x8D, 0xE3, 0x35, 0x00, 0x12, + 0x77, 0x31, 0x55, 0x6A, 0xF2, 0x48, 0xC5, 0x78, 0xB7, 0xA6, 0xFE, 0xB9, 0x03, 0xCB, 0x1D, 0x67, + 0x36, 0x4A, 0x19, 0xB0, 0xE9, 0xB4, 0xC4, 0x9C, 0x1D, 0x53, 0xF5, 0xB8, 0x2B, 0xB5, 0xE1, 0xAE, + 0x6C, 0x9D, 0xE4, 0xC1, 0xEF, 0xA6, 0x46, 0x37, 0x1F, 0x23, 0xA1, 0x0A, 0x7F, 0x43, 0xF8, 0x64, + 0x51, 0x7B, 0xA8, 0x89, 0x90, 0x4C, 0x8B, 0xF0, 0x03, 0x21, 0x9D, 0x15, 0xFF, 0xEA, 0x22, 0x69, + 0xD4, 0x13, 0xD2, 0xAA, 0x3D, 0x8F, 0x3F, 0xBC, 0x33, 0xBD, 0x38, 0xBB, 0x1A, 0x4D, 0xBF, 0x3D, + 0x40, 0xCE, 0x98, 0xB8, 0x57, 0x27, 0x91, 0x50, 0x17, 0xC2, 0x72, 0x6A, 0x75, 0x43, 0x05, 0x13, + 0x9D, 0x4C, 0x32, 0xDE, 0x4F, 0xA1, 0xA4, 0xDC, 0x56, 0x0C, 0xE9, 0x26, 0x6C, 0xE6, 0xD1, 0xC9, + 0xB2, 0x14, 0x9C, 0xDF, 0x82, 0x0B, 0xA4, 0x6F, 0x79, 0xC8, 0xF2, 0x44, 0xD3, 0xA5, 0x86, 0x93, + 0xCA, 0x3C, 0x7C, 0xA6, 0x0D, 0x31, 0x99, 0x03, 0x24, 0x3D, 0xEC, 0xC3, 0x71, 0x9D, 0x58, 0x73, + 0x2C, 0xC4, 0x5A, 0x95, 0x5E, 0x1A, 0xB6, 0x20, 0x69, 0xEF, 0xC1, 0xD7, 0x14, 0x75, 0x3D, 0x7A, + 0xAC, 0xCB, 0xFE, 0x3A, 0x4A, 0xB4, 0x1D, 0x55, 0x50, 0x99, 0xA3, 0xEB, 0x6B, 0x6D, 0xAB, 0x3B, + 0x11, 0xF1, 0xA7, 0xA6, 0xC0, 0x13, 0xFD, 0xAE, 0xDA, 0xE6, 0x0C, 0x69, 0x40, 0xEC, 0x28, 0x89, + 0x96, 0x23, 0xDA, 0x54, 0x7B, 0x72, 0xBD, 0x2B, 0x43, 0x14, 0x67, 0x14, 0x44, 0x81, 0x67, 0x35, + 0x5E, 0x22, 0x92, 0x2F, 0xF6, 0xD0, 0x90, 0xBE, 0x76, 0x7E, 0x31, 0xFD, 0x82, 0x0F, 0x40, 0xFD, + 0x4A, 0x13, 0xF2, 0x94, 0xC9, 0xEF, 0xC0, 0xCD, 0x4F, 0x43, 0x84, 0xE1, 0x08, 0x5C, 0x93, 0x0F, + 0xB9, 0xD8, 0x69, 0x9F, 0xBE, 0xED, 0xD4, 0x8F, 0x1D, 0x69, 0xF2, 0x7D, 0x47, 0x0A, 0x49, 0x59, + 0xF2, 0x66, 0x16, 0xCF, 0xB2, 0x07, 0x30, 0x75, 0xD8, 0xBC, 0x99, 0xB1, 0xCA, 0xA9, 0x0F, 0xB7, + 0x74, 0x89, 0xBA, 0x4E, 0xA3, 0x33, 0x91, 0x05, 0x47, 0x8A, 0x13, 0x8C, 0x68, 0x78, 0xA8, 0x5C, + 0xFB, 0x63, 0xA7, 0xF5, 0xD3, 0x37, 0x1B, 0x6B, 0xE1, 0x01, 0xFE, 0xBF, 0xC7, 0xFE, 0xF7, 0xC8, + 0x2F, 0xA3, 0x6D, 0x53, 0x8C, 0x02, 0x11, 0x75, 0xB4, 0xC7, 0xBD, 0x45, 0xEA, 0x1E, 0xF6, 0x62, + 0xB6, 0xAF, 0xE3, 0x5F, 0x70, 0x49, 0x00, 0xCE, 0x77, 0x46, 0xD7, 0x8C, 0xE6, 0x1F, 0x42, 0x08, + 0xFC, 0x8D, 0x46, 0xDC, 0x60, 0x1C, 0x2E, 0x9D, 0xD4, 0xA6, 0xF8, 0x4F, 0xA5, 0x36, 0xB6, 0x4F, + 0x23, 0x5B, 0x89, 0xE8, 0x57, 0x1B, 0x5A, 0x93, 0x0C, 0x86, 0xC7, 0xC6, 0xAB, 0x19, 0xAA, 0xF2, + 0x18, 0x3D, 0x1A, 0xA4, 0x07, 0xD3, 0x0A, 0x3A, 0xB5, 0x97, 0x92, 0xCC, 0x7A, 0x09, 0x38, 0xAB, + 0x7A, 0x29, 0x2A, 0x9F, 0x51, 0x50, 0xCF, 0x8C, 0xFE, 0x58, 0xD1, 0xCC, 0x77, 0x5A, 0x34, 0x33, + 0x69, 0x92, 0xB4, 0x37, 0x4F, 0x3C, 0x3F, 0x4E, 0x02, 0xA4, 0xDF, 0x3D, 0xA1, 0x8D, 0x94, 0x84, + 0x96, 0x16, 0xD5, 0x56, 0x01, 0x7F, 0x4E, 0x36, 0x1F, 0xAF, 0x4E, 0xC8, 0x37, 0x34, 0x4C, 0x4D, + 0xA7, 0xF7, 0xDB, 0x80, 0x03, 0x74, 0x93, 0xB7, 0x7B, 0x20, 0x84, 0xF7, 0x08, 0x8A, 0x68, 0xC6, + 0x54, 0x8D, 0xAF, 0xDF, 0xAA, 0x70, 0x36, 0x32, 0x61, 0x59, 0x96, 0xD9, 0x13, 0xF2, 0x60, 0x8C, + 0x3F, 0x41, 0xCB, 0x75, 0x66, 0x4D, 0x62, 0xB7, 0xAA, 0x1A, 0x71, 0x42, 0xEB, 0x42, 0xC3, 0x41, + 0xB3, 0xE1, 0x49, 0x72, 0xE8, 0x3C, 0x91, 0xC4, 0xF8, 0x7A, 0x6E, 0xAE, 0x54, 0x05, 0x7E, 0xF0, + 0xF1, 0xFD, 0xF9, 0xEE, 0xC2, 0x77, 0xEC, 0x0F, 0xD6, 0x2A, 0x43, 0x45, 0xF6, 0x20, 0x4B, 0xD6, + 0xDC, 0xB4, 0x87, 0x68, 0xD8, 0x1C, 0x5B, 0xD4, 0xC5, 0xBF, 0x30, 0x78, 0x08, 0x8F, 0x1F, 0x44, + 0x82, 0xFB, 0xF5, 0x29, 0x7B, 0x14, 0xE1, 0xB1, 0x42, 0x10, 0xFD, 0x96, 0x94, 0xF0, 0x4A, 0xE2, + 0x7D, 0x50, 0x02, 0x43, 0xD2, 0x48, 0xC1, 0x98, 0xA0, 0x8E, 0xEE, 0x09, 0x80, 0x3D, 0xBA, 0x2A, + 0x51, 0x5E, 0x16, 0x3D, 0x26, 0x60, 0xFB, 0xF5, 0xE3, 0x23, 0xFE, 0x09, 0x5E, 0x22, 0x45, 0x91, + 0xD1, 0xDF, 0x76, 0xDE, 0x5D, 0xC1, 0xCA, 0x02, 0x45, 0x3D, 0x86, 0x9B, 0x18, 0xBF, 0x86, 0x0F, + 0x95, 0xFB, 0x66, 0xF3, 0x45, 0x1A, 0x8F, 0xDA, 0xBC, 0xC7, 0xCC, 0xB2, 0x60, 0x24, 0x4D, 0xC6, + 0x4E, 0x63, 0x5F, 0x39, 0x23, 0x83, 0xC6, 0xF8, 0x93, 0xBF, 0x33, 0xE9, 0x8B, 0xBA, 0xDF, 0x81, + 0xC3, 0x80, 0x64, 0x2D, 0xDB, 0x7E, 0x6A, 0x35, 0xE3, 0x45, 0xF5, 0x6B, 0x04, 0xD1, 0x81, 0x7B, + 0x86, 0xDE, 0x68, 0x29, 0xC8, 0x13, 0x2D, 0x50, 0x59, 0x5C, 0x66, 0xDF, 0x6A, 0xE5, 0x10, 0x5C, + 0x2B, 0x67, 0xF3, 0xAE, 0xD7, 0x43, 0x49, 0xAC, 0xD0, 0x45, 0x8A, 0x52, 0xB7, 0xDA, 0xF7, 0x6F, + 0xA7, 0x6A, 0x5E, 0xD9, 0xDA, 0xC6, 0xD4, 0x7B, 0x0D, 0xDA, 0xC0, 0x21, 0x63, 0x28, 0x8F, 0x16, + 0xCD, 0x05, 0xCA, 0x2E, 0x54, 0xE9, 0x5F, 0xC7, 0xC0, 0x4F, 0xB5, 0xB8, 0x47, 0xD8, 0x05, 0x4C, + 0x31, 0xDD, 0x87, 0xA0, 0x08, 0xD3, 0x91, 0x10, 0xC4, 0x77, 0x83, 0x60, 0x6E, 0xA1, 0x60, 0xEA, + 0xEB, 0x98, 0x73, 0xE4, 0x3A, 0x48, 0xB8, 0xE7, 0x74, 0xF1, 0x2E, 0x32, 0xF3, 0x36, 0x55, 0xF4, + 0xCE, 0xEA, 0xD5, 0x71, 0xC0, 0x3C, 0x4A, 0xB1, 0x36, 0x63, 0x6A, 0xE2, 0xF8, 0xC8, 0xE8, 0xE5, + 0x8B, 0xD7, 0x38, 0x36, 0xD5, 0xF4, 0x71, 0xEE, 0xBE, 0x6F, 0xC8, 0x78, 0x29, 0x37, 0x9B, 0x3E, + 0xEF, 0x85, 0x94, 0xB0, 0x2C, 0x47, 0x36, 0xE9, 0x39, 0x47, 0x2E, 0x82, 0x5D, 0xA8, 0xF2, 0x43, + 0x2A, 0x1D, 0x37, 0x33, 0x3A, 0x32, 0x11, 0x66, 0xFB, 0xD7, 0x91, 0x65, 0x1F, 0x78, 0x10, 0x48, + 0x0B, 0xC2, 0x36, 0xC9, 0xB4, 0x0B, 0xE9, 0xE4, 0x32, 0x5B, 0x17, 0x0B, 0x17, 0x9E, 0xBD, 0xF9, + 0xE0, 0x82, 0x5C, 0xF9, 0xAE, 0x20, 0x81, 0x53, 0xE6, 0x45, 0x40, 0xCF, 0x62, 0xAA, 0x0B, 0x1C, + 0x78, 0xE6, 0x8E, 0xDC, 0xBB, 0xDD, 0x5A, 0x37, 0x8A, 0xDE, 0xEA, 0x04, 0x5C, 0x3F, 0xE0, 0xC8, + 0xD8, 0xFA, 0xF8, 0x1C, 0xBB, 0xC9, 0x67, 0x26, 0x56, 0xD2, 0xD6, 0x5C, 0x6D, 0xD1, 0x85, 0xDC, + 0x82, 0xD9, 0xCA, 0xE5, 0x05, 0x3F, 0xA6, 0x1F, 0xF5, 0x81, 0x6F, 0x3D, 0x7A, 0xDB, 0x48, 0xF1, + 0x11, 0x1B, 0x02, 0x12, 0x09, 0x75, 0x4E, 0x1A, 0x4E, 0x51, 0x4B, 0x1B, 0xE6, 0x8C, 0xA3, 0xFA, + 0xBB, 0xE8, 0x82, 0x87, 0xA6, 0x74, 0x16, 0x35, 0xC5, 0x0A, 0x05, 0xDE, 0xB3, 0xDF, 0xCA, 0xEC, + 0x4C, 0xC6, 0x10, 0x03, 0x84, 0x5D, 0xD3, 0xCC, 0x2C, 0x55, 0x6B, 0x97, 0x40, 0x31, 0xFB, 0x90, + 0x12, 0x07, 0x41, 0x1A, 0x57, 0x58, 0x73, 0x18, 0xEF, 0x14, 0x4B, 0x7E, 0x49, 0xB4, 0x93, 0x83, + 0x7A, 0xB2, 0x76, 0x47, 0x9E, 0x8E, 0x4A, 0x05, 0x68, 0xCB, 0xC9, 0x33, 0xD1, 0x99, 0x1D, 0x44, + 0x8C, 0x75, 0x44, 0x1F, 0xAA, 0x04, 0xD1, 0x4E, 0x4D, 0x27, 0x69, 0x18, 0x1E, 0xB8, 0xA6, 0xF0, + 0xC0, 0xAA, 0xF4, 0x93, 0xD6, 0x94, 0xA4, 0x81, 0x97, 0xA5, 0x2E, 0x9D, 0xB1, 0x66, 0xAD, 0x7C, + 0x5F, 0xC0, 0x04, 0xE0, 0xC6, 0x0B, 0x73, 0x47, 0x5D, 0xA0, 0xEC, 0x39, 0xFD, 0xEA, 0xB7, 0x83, + 0x96, 0xFE, 0x4E, 0x72, 0x5B, 0x84, 0x7B, 0xA7, 0x32, 0xFC, 0x20, 0xEF, 0x28, 0x45, 0x3F, 0x93, + 0x32, 0x21, 0x97, 0x39, 0x84, 0xDB, 0x48, 0xAB, 0x15, 0xCC, 0x1B, 0x7E, 0x31, 0xFD, 0x9E, 0x27, + 0xE9, 0x19, 0x4F, 0xCF, 0x79, 0xB2, 0xED, 0x71, 0xFB, 0xEA, 0xDE, 0x5A, 0x6A, 0x83, 0xE7, 0x5D, + 0x7E, 0x55, 0xEC, 0x61, 0xA3, 0xFD, 0x81, 0xA4, 0xE3, 0xCC, 0x24, 0x03, 0x2C, 0x87, 0x02, 0x2B, + 0xCE, 0x49, 0x01, 0xDB, 0x1A, 0x58, 0x17, 0x31, 0x7D, 0xE7, 0x0F, 0xCD, 0xAC, 0xD9, 0xC5, 0x5C, + 0x99, 0x91, 0xB7, 0x81, 0x5D, 0x61, 0x1A, 0x55, 0x85, 0xBC, 0x80, 0xC7, 0xE8, 0x9B, 0x65, 0xAA, + 0x3B, 0x72, 0x80, 0x5C, 0xF0, 0x6C, 0x92, 0x5D, 0xFC, 0x45, 0xF1, 0x17, 0x3D, 0x28, 0x45, 0x33, + 0x87, 0x0C, 0xFA, 0x91, 0x9C, 0xEF, 0x75, 0x0E, 0x18, 0x74, 0x5C, 0x69, 0xAA, 0x8B, 0x4C, 0xB7, + 0xC6, 0x37, 0x81, 0xB2, 0x41, 0xE6, 0xC3, 0xF0, 0x43, 0xF2, 0x21, 0xD6, 0x3B, 0x89, 0xFF, 0x75, + 0x80, 0x8D, 0xB0, 0x31, 0x22, 0x8F, 0x2C, 0x30, 0x0A, 0xDC, 0x36, 0xFE, 0x1E, 0x49, 0xBA, 0xDD, + 0xDF, 0xAA, 0x65, 0xB2, 0x1D, 0x37, 0xAE, 0xA7, 0x79, 0x74, 0x02, 0x14, 0xA6, 0x4B, 0xB2, 0x5F, + 0x32, 0x53, 0x8E, 0xEE, 0x02, 0x4B, 0xC9, 0x94, 0xE3, 0xC1, 0xF0, 0x4B, 0x69, 0xE7, 0x91, 0x4E, + 0x3A, 0x9F, 0xC3, 0x9B, 0x0D, 0x3D, 0x0F, 0xCA, 0x02, 0x90, 0xDD, 0x66, 0x1D, 0xDB, 0xD7, 0x49, + 0x11, 0xFA, 0x96, 0x22, 0x89, 0x39, 0x5D, 0x91, 0x47, 0x82, 0x1C, 0xC4, 0x25, 0xA3, 0x00, 0x3B, + 0x7A, 0xDE, 0x5F, 0x83, 0x60, 0xED, 0x08, 0xA9, 0x66, 0xC3, 0xB4, 0xE3, 0x96, 0xC0, 0xAB, 0x34, + 0x28, 0xF4, 0x07, 0x24, 0xDA, 0x52, 0x56, 0x62, 0xC2, 0xD3, 0xAD, 0x37, 0x51, 0xC9, 0x7F, 0x65, + 0x8E, 0x4D, 0xD8, 0x4B, 0x34, 0xCD, 0xE5, 0xF5, 0x82, 0xC8, 0x46, 0xD2, 0x3D, 0xF4, 0x8B, 0x52, + 0x07, 0x5C, 0x96, 0x9C, 0x80, 0xF9, 0xF3, 0xF9, 0x2F, 0x99, 0xA9, 0x70, 0x8E, 0xAB, 0xED, 0xA6, + 0xFE, 0x93, 0x66, 0xD5, 0xAF, 0x68, 0x79, 0xD6, 0xE0, 0xAB, 0x14, 0xDB, 0xC0, 0x01, 0xB0, 0x0B, + 0x39, 0x97, 0x46, 0x1E, 0x2C, 0x47, 0xCA, 0x86, 0xA8, 0x43, 0xBA, 0x83, 0x7E, 0xDF, 0x22, 0xB2, + 0x53, 0x86, 0xE7, 0x97, 0x05, 0x45, 0x88, 0xAA, 0x64, 0x57, 0x2E, 0xA1, 0x67, 0xC2, 0x4F, 0x8A, + 0xFB, 0xC9, 0x1A, 0x8A, 0x28, 0x3B, 0x03, 0x99, 0x9F, 0xFE, 0xAA, 0x0F, 0x03, 0x48, 0xF4, 0xF7, + 0x68, 0xE2, 0x47, 0xB2, 0x76, 0xD8, 0x80, 0x64, 0xBD, 0x05, 0xFA, 0x3E, 0xC1, 0xB3, 0x8D, 0xB4, + 0x17, 0xD7, 0x78, 0xF1, 0x3E, 0x30, 0xAD, 0x54, 0x3D, 0xAB, 0xD2, 0xC6, 0xDF, 0xDA, 0x89, 0x78, + 0x9E, 0xDB, 0x12, 0xCE, 0x51, 0x7D, 0x6C, 0xD6, 0xF7, 0xF3, 0x7C, 0x1D, 0xF1, 0xA9, 0xD1, 0x93, + 0x70, 0x55, 0x41, 0xD0, 0xD6, 0x6A, 0xA1, 0xA3, 0xD7, 0x5E, 0x63, 0xDF, 0xAE, 0x5E, 0xCA, 0x02, + 0xED, 0x0E, 0x5C, 0x8B, 0x53, 0xF4, 0xCF, 0x05, 0x37, 0xA2, 0x5E, 0x16, 0xD6, 0xA1, 0x28, 0x83, + 0xBD, 0x3B, 0x58, 0xD0, 0x28, 0x47, 0x96, 0x25, 0x9A, 0xC3, 0x5B, 0x48, 0x09, 0x62, 0x6E, 0x83, + 0x2E, 0x33, 0x80, 0xA7, 0xD1, 0x4D, 0x3F, 0xAE, 0x6C, 0x8E, 0x13, 0xE4, 0xCE, 0x54, 0xB2, 0x81, + 0x68, 0x99, 0x8C, 0x8E, 0x26, 0xB9, 0xE1, 0x86, 0xA4, 0xAD, 0x22, 0xF7, 0x36, 0x60, 0x2D, 0x71, + 0xB2, 0x57, 0xBF, 0x51, 0xB5, 0xF8, 0x65, 0xAE, 0x17, 0xA3, 0xC6, 0x6D, 0xFD, 0x79, 0xA3, 0x75, + 0x73, 0xCE, 0xAB, 0x3D, 0x2A, 0x9A, 0x6E, 0xF1, 0xC8, 0x81, 0x30, 0x47, 0x4B, 0x8F, 0xF9, 0xB7, + 0x9B, 0x99, 0xA8, 0x76, 0xA9, 0xEC, 0x56, 0x90, 0x7F, 0x9F, 0x02, 0x38, 0x52, 0x86, 0x81, 0x87, + 0x1A, 0xAA, 0xED, 0x19, 0x2D, 0x5F, 0x91, 0xE0, 0xC6, 0x67, 0xBF, 0x13, 0x10, 0x62, 0x91, 0x55, + 0x1F, 0x6B, 0xCD, 0xA3, 0x33, 0x46, 0x9D, 0x54, 0xAC, 0x07, 0x83, 0x33, 0x02, 0xC1, 0x7E, 0x79, + 0x65, 0xC3, 0xC4, 0x15, 0x8C, 0x0C, 0xE7, 0x91, 0x06, 0x3A, 0xAA, 0xAF, 0x9A, 0xE0, 0xCD, 0x14, + 0xD0, 0x9B, 0xD3, 0x14, 0xF0, 0xDF, 0xFC, 0xCE, 0x41, 0xB8, 0x3B, 0x00, 0x9F, 0x99, 0x8D, 0x4E, + 0xDB, 0x2E, 0x0D, 0x22, 0xD5, 0x22, 0x01, 0x74, 0x81, 0xAA, 0x66, 0x85, 0x94, 0x17, 0x09, 0x9D, + 0x03, 0x57, 0x4F, 0xE3, 0x53, 0x39, 0x69, 0xB1, 0xEA, 0x89, 0x9A, 0x7D, 0xA1, 0x75, 0x20, 0xCE, + 0x56, 0x4E, 0xED, 0xAB, 0x6F, 0xDE, 0x33, 0xBE, 0xCF, 0x1D, 0x66, 0xD3, 0x71, 0x11, 0x86, 0xE6, + 0xE2, 0xE4, 0x9E, 0xA0, 0x47, 0x0B, 0x0E, 0x2A, 0x16, 0x4C, 0xC4, 0x50, 0x16, 0x0B, 0x91, 0x06, + 0x55, 0x7A, 0x15, 0xCE, 0xCF, 0x6D, 0x0D, 0x94, 0x37, 0x3E, 0x1D, 0x3E, 0x12, 0x0A, 0xC2, 0xC5, + 0x6D, 0xE0, 0x3E, 0x76, 0xD4, 0xB2, 0x75, 0xE8, 0x58, 0x1A, 0x78, 0x9B, 0xF2, 0xBA, 0x8F, 0x1E, + 0x42, 0xA5, 0x39, 0xB5, 0x09, 0x68, 0x7A, 0xB3, 0xA1, 0x34, 0xFE, 0x84, 0x4B, 0x12, 0x1D, 0xDC, + 0xF3, 0x05, 0x57, 0x35, 0x04, 0x1A, 0x68, 0xB8, 0x68, 0x30, 0x8B, 0xFA, 0x0D, 0x5B, 0x06, 0x57, + 0x2C, 0x12, 0x20, 0xF9, 0xE8, 0x27, 0x22, 0x33, 0xF5, 0x3D, 0xB6, 0x52, 0xD7, 0x7F, 0x41, 0xBE, + 0x19, 0x9B, 0xF5, 0x3D, 0x10, 0xFD, 0xB0, 0x8A, 0xDC, 0xD3, 0x50, 0xB7, 0x07, 0x68, 0x3A, 0xFB, + 0xFB, 0xDE, 0xF2, 0xDD, 0x4A, 0x3F, 0x70, 0xA3, 0x64, 0xE9, 0xAD, 0x9F, 0xD6, 0xD7, 0xFC, 0xCB, + 0x17, 0x39, 0xFD, 0x44, 0x43, 0x94, 0x27, 0xB4, 0xE5, 0x46, 0xB4, 0xE7, 0x86, 0xB3, 0xA7, 0xB9, + 0xD3, 0x7B, 0xDA, 0xEA, 0x77, 0x4F, 0x5B, 0x0B, 0xEF, 0xC1, 0xE6, 0x97, 0xDB, 0xC5, 0xEF, 0x35, + 0x86, 0xD0, 0xD2, 0x03, 0x9A, 0x17, 0xBB, 0x0A, 0xCC, 0xD7, 0x3A, 0xF2, 0x94, 0x78, 0x06, 0x28, + 0xF3, 0xBC, 0x22, 0x64, 0x39, 0x3A, 0x37, 0xD3, 0xC7, 0xE3, 0x33, 0x8E, 0x62, 0x95, 0x5E, 0x6F, + 0x94, 0xD6, 0x93, 0x6E, 0x55, 0x22, 0x2A, 0x6B, 0x5C, 0xF8, 0x3D, 0x59, 0x00, 0xCE, 0xDF, 0xB4, + 0x6F, 0xD6, 0xCF, 0x9E, 0x3E, 0x7B, 0xF6, 0xE6, 0xF6, 0xCB, 0xE9, 0x62, 0xB2, 0xE9, 0xBD, 0x7F, + 0xC6, 0xBE, 0xC7, 0xC0, 0xA6, 0xBC, 0xFD, 0x30, 0x6A, 0xCA, 0x2B, 0x6C, 0x52, 0x61, 0x04, 0xF4, + 0x2F, 0xFC, 0x6C, 0x0E, 0xAC, 0x8D, 0x52, 0x2A, 0xA2, 0xDF, 0xBD, 0xC0, 0x96, 0x59, 0xAB, 0x89, + 0x69, 0x77, 0x0F, 0x7C, 0x94, 0xD0, 0xF5, 0x7B, 0x24, 0x9A, 0xB8, 0xDC, 0xDE, 0x8C, 0x3D, 0x7D, + 0x63, 0x6C, 0x2F, 0xFA, 0x2D, 0xB0, 0x1B, 0x43, 0x66, 0x0F, 0x66, 0x85, 0xFF, 0x03, 0x26, 0x92, + 0x30, 0x0D, 0x55, 0x49, 0xD4, 0xFA, 0x65, 0xBD, 0x5A, 0xBE, 0x22, 0x71, 0x41, 0xE8, 0x9E, 0x87, + 0xFD, 0x15, 0x23, 0xF5, 0xCF, 0x79, 0x09, 0x0F, 0x77, 0xA9, 0xBC, 0x91, 0x40, 0x1D, 0x95, 0x8C, + 0x45, 0x68, 0x16, 0xD8, 0x87, 0x1A, 0x16, 0x09, 0xE9, 0x92, 0x64, 0x9E, 0x8D, 0xD1, 0x6B, 0x6B, + 0xB6, 0x9A, 0x7D, 0xC9, 0x75, 0x9F, 0x49, 0x47, 0xD9, 0x15, 0xBF, 0x6A, 0x7F, 0x26, 0xD9, 0x2D, + 0x5E, 0xCE, 0x50, 0x68, 0x45, 0xAA, 0x11, 0xD9, 0xF7, 0x78, 0x63, 0x66, 0xF5, 0x0D, 0xD7, 0xBF, + 0xB1, 0xDE, 0xF6, 0xB9, 0xCA, 0xB7, 0xF8, 0xC6, 0xA6, 0x3C, 0x1B, 0x09, 0x31, 0xA2, 0x6B, 0xE1, + 0x5D, 0x10, 0x7D, 0xEC, 0x5C, 0xAF, 0x65, 0x9D, 0x95, 0xFA, 0xF0, 0x1B, 0x19, 0xD1, 0xA2, 0xA9, + 0xFC, 0xDB, 0xB8, 0xF0, 0xE2, 0xD9, 0xA3, 0x2A, 0x7F, 0x22, 0xCA, 0x5D, 0x65, 0xF7, 0x7B, 0x60, + 0x8B, 0x8B, 0xC0, 0xFA, 0xE1, 0x7B, 0xD2, 0x6B, 0x47, 0xDD, 0xC8, 0xD4, 0x72, 0x00, 0xC0, 0xCE, + 0xC6, 0x41, 0x99, 0xE8, 0xDF, 0xC3, 0xFC, 0x6A, 0x69, 0x9E, 0xE3, 0x48, 0x5B, 0x0E, 0x51, 0xF8, + 0x12, 0x35, 0x62, 0xDD, 0x2C, 0xD0, 0xF9, 0x27, 0x72, 0x6E, 0xF0, 0xC3, 0x1F, 0xA9, 0xA4, 0xFA, + 0x8A, 0x9E, 0x3F, 0x33, 0x07, 0x19, 0xCD, 0x0E, 0x64, 0x34, 0xE7, 0xD8, 0xB6, 0xA3, 0xAE, 0x9E, + 0x50, 0x94, 0xA8, 0x3F, 0x40, 0x20, 0x3D, 0x7D, 0xD5, 0x71, 0xF4, 0x14, 0xD3, 0x96, 0xE4, 0xD8, + 0xD7, 0x3F, 0x21, 0xFB, 0xF5, 0x56, 0x8D, 0xBA, 0x47, 0xF7, 0xCB, 0x51, 0x53, 0xAF, 0x13, 0x45, + 0xA3, 0xFB, 0xC9, 0x3F, 0x01, 0x0E, 0xC6, 0x32, 0x5D, 0x9D, 0x5D, 0x01, 0x00 +}; //jquery-3.6.0.min.js + +//Content of bootstrap.min.js with gzip compression +static const uint8_t bootstrap_min_js[] PROGMEM = { + 0x1F, 0x8B, 0x08, 0x08, 0xDC, 0x8A, 0x0C, 0x61, 0x04, 0x00, 0x62, 0x6F, 0x6F, 0x74, 0x73, 0x74, + 0x72, 0x61, 0x70, 0x2E, 0x6D, 0x69, 0x6E, 0x2E, 0x6A, 0x73, 0x00, 0xDD, 0x1D, 0x6B, 0x57, 0xDB, + 0xC8, 0xF5, 0x7B, 0x7F, 0x05, 0xA8, 0x29, 0x91, 0xCA, 0x60, 0xA0, 0xD9, 0xBE, 0x6C, 0xB4, 0x3E, + 0x04, 0xC8, 0x86, 0xDD, 0x04, 0x58, 0x1E, 0xC9, 0x66, 0x29, 0xE5, 0x08, 0x7B, 0x30, 0x4A, 0xCC, + 0xC8, 0x91, 0xC6, 0x21, 0x04, 0xBB, 0xBF, 0xBD, 0xF7, 0xCE, 0x53, 0x23, 0x8D, 0x6C, 0x93, 0x66, + 0x7B, 0x7A, 0xDA, 0xD3, 0x25, 0xD6, 0xBC, 0xE7, 0xCE, 0x9D, 0x3B, 0x77, 0xEE, 0x6B, 0xD6, 0xFF, + 0xB8, 0xFC, 0xBB, 0xA5, 0xA5, 0x3F, 0x2E, 0x3D, 0xCF, 0x32, 0x5E, 0xF0, 0x3C, 0x19, 0x2D, 0x7D, + 0xFA, 0xAE, 0xF5, 0xAC, 0xB5, 0xB9, 0x14, 0xDE, 0x70, 0x3E, 0x2A, 0xDA, 0xEB, 0xEB, 0x03, 0xCA, + 0xAF, 0x74, 0x66, 0xAB, 0x97, 0xDD, 0xAE, 0x47, 0xA2, 0xC2, 0x4E, 0x36, 0xBA, 0xCF, 0xD3, 0xC1, + 0x0D, 0x5F, 0xFA, 0xD3, 0xC6, 0xE6, 0xE6, 0x1A, 0xFC, 0xF9, 0xFB, 0xD2, 0xE9, 0x0D, 0x2D, 0x35, + 0xB4, 0x3D, 0xE6, 0x37, 0x59, 0x5E, 0x94, 0x5A, 0x4A, 0xF9, 0xCD, 0xF8, 0x4A, 0xB4, 0xC1, 0xEF, + 0xAE, 0x8A, 0x75, 0xD3, 0xEC, 0xFA, 0x00, 0xFE, 0xDC, 0x14, 0xEB, 0xBD, 0x8C, 0xF1, 0x3C, 0xBD, + 0x1A, 0x73, 0xA8, 0x26, 0x7B, 0x79, 0x95, 0xF6, 0x28, 0x2B, 0x68, 0x7F, 0x69, 0xCC, 0xFA, 0x34, + 0x5F, 0x7A, 0xBD, 0x7F, 0xBA, 0x48, 0x73, 0x57, 0xC3, 0xEC, 0x6A, 0xFD, 0x36, 0x29, 0x38, 0xCD, + 0xD7, 0x5F, 0xED, 0xEF, 0xEC, 0x1D, 0x9C, 0xEC, 0x89, 0xE6, 0xD6, 0x7F, 0xB7, 0x7C, 0x3D, 0x66, + 0x3D, 0x9E, 0x66, 0x2C, 0xE4, 0x84, 0x46, 0x0F, 0x41, 0x76, 0xF5, 0x9E, 0xF6, 0x78, 0x10, 0xC7, + 0xFC, 0x7E, 0x44, 0xB3, 0xEB, 0x25, 0xFA, 0x79, 0x94, 0xE5, 0xBC, 0x58, 0x59, 0x09, 0xB0, 0xC3, + 0xEB, 0x94, 0xD1, 0x7E, 0xB0, 0xAC, 0x33, 0x6F, 0xB3, 0xFE, 0x78, 0x48, 0xBB, 0x34, 0x54, 0xA5, + 0x48, 0x4E, 0x3F, 0x8E, 0xD3, 0x9C, 0x86, 0xC1, 0xFB, 0x8F, 0x63, 0x9A, 0xDF, 0x07, 0x91, 0x4D, + 0x19, 0x65, 0xA3, 0x11, 0xCD, 0x5B, 0xEF, 0x8B, 0x20, 0x8A, 0xDA, 0x81, 0xEE, 0xD5, 0x76, 0x24, + 0x1B, 0x5F, 0x59, 0x91, 0xFF, 0xB6, 0x92, 0xDB, 0x7E, 0x57, 0xFE, 0x0C, 0xCF, 0x03, 0xD5, 0x7C, + 0x40, 0x74, 0xBB, 0xA4, 0xD4, 0xDC, 0x05, 0x8C, 0xBB, 0x4D, 0xC3, 0x90, 0xC7, 0x7C, 0x32, 0x29, + 0xE8, 0xF0, 0x3A, 0x6A, 0x99, 0x89, 0xC7, 0x0F, 0x53, 0xC2, 0x5B, 0xEF, 0x7F, 0xC6, 0x4A, 0xF0, + 0xE3, 0x48, 0x54, 0x8A, 0xA6, 0x21, 0xBF, 0x49, 0x0B, 0x52, 0x9A, 0xF9, 0x80, 0x8C, 0x61, 0xEE, + 0xE3, 0x82, 0x2E, 0x41, 0xB5, 0x14, 0xE6, 0xDF, 0xD1, 0x99, 0x4B, 0xA9, 0x04, 0xCC, 0x75, 0x96, + 0x87, 0x9F, 0x92, 0x7C, 0x89, 0xC5, 0x1B, 0x1D, 0xB6, 0x45, 0x5B, 0x43, 0xCA, 0x06, 0xFC, 0xA6, + 0xC3, 0x56, 0x57, 0xA3, 0x07, 0x4C, 0x4F, 0x63, 0x7A, 0xCE, 0x2E, 0x3A, 0x69, 0x8B, 0xB2, 0xF1, + 0x2D, 0xCD, 0x93, 0xAB, 0x21, 0x8D, 0xCB, 0x1F, 0x93, 0xC9, 0xF2, 0x26, 0x49, 0x5B, 0xB0, 0xA0, + 0xD7, 0xE9, 0x60, 0x2C, 0xF3, 0x97, 0x37, 0x48, 0xF0, 0x29, 0x19, 0x8E, 0x69, 0x90, 0x42, 0x3F, + 0x2B, 0x2B, 0x61, 0xDA, 0xBA, 0xCB, 0x53, 0xAE, 0xF2, 0x22, 0x72, 0x28, 0xD6, 0xA2, 0x25, 0xC1, + 0x70, 0x94, 0x67, 0x30, 0x76, 0x7E, 0x1F, 0x72, 0x68, 0xE6, 0x03, 0xBD, 0x27, 0x69, 0x34, 0x9D, + 0x9A, 0x51, 0x16, 0x38, 0x4A, 0xC2, 0xA2, 0x87, 0x9C, 0xF2, 0x71, 0xCE, 0x96, 0x00, 0x90, 0x30, + 0xF0, 0xD6, 0x28, 0xCF, 0x78, 0x86, 0x00, 0x86, 0x29, 0x10, 0x26, 0xD2, 0xA0, 0x10, 0xE1, 0xB6, + 0xE2, 0x30, 0xCC, 0xEC, 0xE4, 0x78, 0xBC, 0xD9, 0xE1, 0x5B, 0x49, 0x3E, 0x80, 0x51, 0x33, 0x5E, + 0xE8, 0x49, 0x72, 0x3D, 0xC9, 0x3C, 0x66, 0xE3, 0xE1, 0x70, 0x39, 0x36, 0x25, 0xCE, 0xF9, 0x45, + 0xB7, 0xFC, 0xD1, 0x06, 0x78, 0xD3, 0x58, 0x8E, 0x1B, 0x07, 0x59, 0x84, 0x79, 0xD4, 0xF1, 0x2C, + 0xB6, 0x2A, 0x01, 0x3B, 0xE9, 0xF0, 0x8E, 0xE9, 0x99, 0x9D, 0xDC, 0xDF, 0x5E, 0x65, 0xC3, 0x02, + 0xE0, 0x40, 0x63, 0x8A, 0x90, 0xEA, 0x25, 0x3C, 0x9C, 0x51, 0x12, 0x1B, 0x6F, 0x5D, 0xA7, 0x43, + 0x40, 0xEA, 0xD0, 0xAE, 0xA5, 0x01, 0x81, 0xB7, 0xE6, 0x2E, 0x2D, 0x7A, 0x79, 0x3A, 0xE2, 0x30, + 0xE1, 0x9C, 0xF0, 0xA8, 0xB4, 0x40, 0xD3, 0x28, 0x8A, 0x08, 0x6D, 0x01, 0x28, 0xF6, 0x92, 0xDE, + 0x4D, 0xA9, 0x41, 0x39, 0x75, 0x00, 0x2E, 0x49, 0x3B, 0x34, 0xCE, 0x48, 0x1A, 0xE7, 0xE7, 0x2C, + 0xE6, 0x17, 0x04, 0xD6, 0x0C, 0x00, 0xDD, 0xF5, 0xAF, 0x12, 0x96, 0x7F, 0x10, 0x8B, 0xDB, 0x4E, + 0x89, 0xED, 0xA5, 0x0D, 0x6B, 0xAE, 0x70, 0xC0, 0x7C, 0xE3, 0x9A, 0xAB, 0xDF, 0xD3, 0xA8, 0x8D, + 0x68, 0x14, 0xA7, 0xD3, 0x68, 0xAA, 0xA6, 0x91, 0x4D, 0x07, 0xF1, 0x60, 0x65, 0x65, 0xD0, 0xBA, + 0x49, 0x8A, 0xD2, 0x44, 0xC2, 0x00, 0x3A, 0x4C, 0xC6, 0x43, 0x1E, 0x44, 0xDD, 0x41, 0x4B, 0xFD, + 0x6E, 0x03, 0x1E, 0xC7, 0xE3, 0x95, 0x95, 0xF1, 0x8C, 0xC2, 0x63, 0x53, 0x78, 0xDC, 0x11, 0x13, + 0x8B, 0x03, 0xD8, 0x29, 0xAC, 0x48, 0x71, 0xB6, 0x94, 0xF5, 0x4B, 0xA8, 0x6F, 0x27, 0x1F, 0x8B, + 0x0D, 0xC3, 0xE2, 0xE5, 0xCD, 0x8E, 0x1A, 0xD6, 0x40, 0xEC, 0xA1, 0xA8, 0x05, 0x75, 0xC2, 0xCB, + 0xD6, 0xE9, 0xF1, 0xF6, 0xC1, 0xC9, 0xFE, 0xE9, 0xFE, 0xE1, 0xC1, 0xE5, 0xDE, 0xC1, 0xAE, 0xDD, + 0x59, 0xD1, 0x03, 0xD4, 0x81, 0x59, 0x91, 0x82, 0xF2, 0xD3, 0xF4, 0x96, 0x66, 0x63, 0x1E, 0x96, + 0x33, 0x27, 0x93, 0xCB, 0x16, 0x6C, 0xB8, 0xC1, 0x80, 0xE6, 0xA7, 0x66, 0x10, 0x7B, 0xAC, 0x1F, + 0xD2, 0x68, 0x0A, 0xCB, 0x43, 0xB0, 0x8F, 0x29, 0x8E, 0xE0, 0x32, 0x7E, 0x70, 0xFB, 0x68, 0x07, + 0x57, 0x85, 0x53, 0x25, 0x20, 0xB0, 0xD2, 0x67, 0xFB, 0xBB, 0x6D, 0xBB, 0x72, 0x12, 0xB1, 0x01, + 0x7D, 0xE3, 0x7F, 0xFD, 0x2B, 0xDC, 0xA4, 0x7F, 0xF9, 0xE3, 0xEB, 0x84, 0xDF, 0xB4, 0xA0, 0x52, + 0x3F, 0xBB, 0x0D, 0x61, 0xB5, 0xFB, 0x59, 0x0F, 0x31, 0x57, 0xE0, 0xC8, 0xDE, 0x90, 0xE2, 0xCF, + 0xE7, 0xF7, 0xFB, 0x7D, 0xA8, 0xD8, 0x89, 0xF4, 0x3C, 0xF9, 0x14, 0xDB, 0x3D, 0xA1, 0x43, 0x58, + 0xE3, 0x2C, 0x7F, 0x91, 0x67, 0xB7, 0xAA, 0xA4, 0xE9, 0xC7, 0x02, 0x49, 0x34, 0xB4, 0xCD, 0x25, + 0xC5, 0xA6, 0x00, 0xF4, 0x84, 0x27, 0x6B, 0x1C, 0xF6, 0x07, 0x05, 0xC0, 0x77, 0xD2, 0xEB, 0x70, + 0x19, 0xC8, 0x40, 0xF0, 0x7B, 0xD8, 0x06, 0x31, 0x95, 0x75, 0x58, 0xAD, 0xCE, 0x4D, 0x4E, 0xAF, + 0xA1, 0x30, 0x8D, 0x61, 0xB7, 0x42, 0xD1, 0xE5, 0x38, 0x66, 0x5D, 0x86, 0x30, 0x82, 0x11, 0xB7, + 0x83, 0x60, 0xCA, 0xF3, 0x7B, 0x8D, 0xE0, 0x66, 0xF8, 0x82, 0x26, 0xEA, 0x21, 0x02, 0xE8, 0xBA, + 0xB4, 0x8D, 0xFB, 0x74, 0x0A, 0xFB, 0xA7, 0x77, 0x53, 0xDA, 0x11, 0x22, 0x51, 0xCC, 0xC7, 0x42, + 0x6E, 0x17, 0xF0, 0x10, 0xFF, 0x6D, 0x9A, 0x19, 0x8E, 0x9A, 0x47, 0xAA, 0x81, 0x0D, 0x85, 0x31, + 0x03, 0xC8, 0x69, 0xF5, 0x8A, 0x22, 0x2C, 0xA1, 0xCE, 0x5A, 0x5F, 0xB5, 0x14, 0x00, 0xA5, 0x69, + 0x28, 0x41, 0x87, 0x09, 0x9E, 0x09, 0x69, 0x3C, 0x4A, 0xF2, 0x82, 0xBE, 0x18, 0x66, 0xB0, 0xC1, + 0x81, 0x30, 0x65, 0xE5, 0x6F, 0x66, 0x40, 0x9F, 0x4E, 0x26, 0x59, 0x57, 0xD0, 0x82, 0x62, 0x34, + 0x4C, 0x79, 0x08, 0x54, 0x3F, 0x3A, 0xDF, 0xB8, 0x80, 0xD6, 0x59, 0x25, 0x65, 0x93, 0x3E, 0xFB, + 0x63, 0xE8, 0xB4, 0xB9, 0xEA, 0xB4, 0x08, 0x27, 0xCE, 0xC6, 0x94, 0x00, 0x64, 0x87, 0xD9, 0x9D, + 0x33, 0x3B, 0xBD, 0xC8, 0xAD, 0xEC, 0xFA, 0x1A, 0x50, 0xF4, 0x25, 0xC5, 0xF3, 0x7B, 0x4A, 0x7C, + 0x28, 0xE9, 0xD4, 0x13, 0xD3, 0x53, 0xA5, 0x04, 0xAE, 0x16, 0xE3, 0x91, 0x38, 0xA2, 0x1A, 0xAA, + 0xE8, 0x9E, 0x90, 0x19, 0x18, 0xD2, 0x84, 0x89, 0x3A, 0x69, 0x51, 0x07, 0xB8, 0x2A, 0x08, 0xBF, + 0x60, 0x5E, 0x93, 0x09, 0xF4, 0xC2, 0xB2, 0x3E, 0x3D, 0x05, 0xAA, 0x09, 0xA3, 0x82, 0xBF, 0x3B, + 0x37, 0xB4, 0xF7, 0x61, 0x47, 0x10, 0x10, 0x53, 0x4B, 0x92, 0x7E, 0x4B, 0xC5, 0x53, 0xA4, 0x4B, + 0x2C, 0x4A, 0xAF, 0x15, 0xF1, 0xB4, 0xA7, 0x40, 0x85, 0x2C, 0xB4, 0x7A, 0xC9, 0x70, 0x18, 0x32, + 0x38, 0x49, 0x24, 0x36, 0x66, 0x31, 0x3B, 0x4F, 0x2F, 0x48, 0x1E, 0x53, 0xFC, 0xA7, 0x88, 0xF3, + 0x95, 0x95, 0xCB, 0x96, 0x19, 0x25, 0x50, 0xDB, 0x6E, 0x40, 0xE5, 0xEF, 0xA0, 0x1D, 0x26, 0x71, + 0x4E, 0x1E, 0xA6, 0x2D, 0x9E, 0x9D, 0x00, 0x1C, 0xD8, 0x40, 0xB6, 0x95, 0x44, 0xAD, 0x5B, 0x81, + 0x74, 0xEB, 0xFF, 0x28, 0xC2, 0xF3, 0x64, 0xED, 0xCB, 0xC5, 0x6A, 0xB4, 0x9E, 0x46, 0xE7, 0x9B, + 0x17, 0x50, 0xF0, 0x55, 0x76, 0x47, 0xF3, 0x9D, 0xA4, 0xA0, 0x61, 0x24, 0x77, 0x03, 0xA3, 0x77, + 0x4B, 0xC7, 0x74, 0xB0, 0xF7, 0x79, 0x14, 0x66, 0x00, 0x4E, 0x5A, 0xF0, 0xB0, 0x88, 0x22, 0x7E, + 0x93, 0x67, 0x77, 0x4B, 0x98, 0xB7, 0x97, 0xE7, 0x30, 0x25, 0x0E, 0x55, 0xCF, 0x46, 0x23, 0x5D, + 0x75, 0xF5, 0x69, 0x7B, 0xE9, 0x70, 0x24, 0xE8, 0x53, 0xF0, 0x74, 0x35, 0x5D, 0x7D, 0x1A, 0x2C, + 0xC1, 0xFC, 0x3E, 0xA5, 0x7D, 0xE0, 0x89, 0x70, 0x8E, 0x98, 0x5A, 0x60, 0x2A, 0xEC, 0x27, 0xE4, + 0x5E, 0x00, 0x00, 0xA5, 0x9C, 0x0C, 0x72, 0x5A, 0x4F, 0x23, 0x41, 0x53, 0x92, 0x29, 0x01, 0x92, + 0xDD, 0x3F, 0xB9, 0x49, 0xFA, 0xD9, 0xDD, 0x71, 0x96, 0xD5, 0x11, 0xDF, 0xEC, 0x30, 0xFD, 0x43, + 0x81, 0xA2, 0x95, 0x70, 0x0E, 0x87, 0x84, 0xAC, 0x19, 0x95, 0x76, 0x17, 0x4E, 0xCB, 0x1C, 0x77, + 0x96, 0x4F, 0x12, 0x9B, 0x1C, 0x7B, 0x38, 0x80, 0xC5, 0xD4, 0xE5, 0x39, 0x2C, 0x53, 0xC1, 0x13, + 0xD6, 0xC3, 0x12, 0x76, 0x10, 0x5D, 0xDE, 0x86, 0x15, 0x4B, 0x72, 0xE8, 0x06, 0x4B, 0x77, 0x2F, + 0x5B, 0xEE, 0x20, 0xC3, 0x72, 0x6E, 0x24, 0x36, 0xBA, 0xDA, 0x95, 0x4E, 0x2F, 0xA1, 0xD9, 0x46, + 0xD4, 0xDF, 0x8F, 0x26, 0x12, 0xD3, 0xCE, 0xA0, 0x75, 0xCD, 0x5A, 0xF4, 0x76, 0x3C, 0x4C, 0x38, + 0x75, 0xF0, 0x37, 0x66, 0x64, 0xD0, 0xA2, 0x9F, 0x70, 0xC2, 0x05, 0xC0, 0x31, 0x4D, 0x86, 0xE7, + 0x55, 0x52, 0x7F, 0x11, 0x3F, 0x5C, 0xC1, 0xF0, 0x10, 0x41, 0xDB, 0x94, 0xF4, 0x01, 0x3D, 0x06, + 0xD0, 0x8A, 0xFA, 0xBC, 0x01, 0x4A, 0x3B, 0xA4, 0x55, 0xA8, 0x0E, 0x60, 0x06, 0x92, 0x28, 0x46, + 0x80, 0x5C, 0xF2, 0x18, 0x31, 0x30, 0x69, 0xC9, 0x3A, 0x80, 0xB8, 0xEA, 0x57, 0xDE, 0x4A, 0x46, + 0xA3, 0xE1, 0xBD, 0xE4, 0xD8, 0x0C, 0xAB, 0x11, 0xC1, 0xB0, 0x25, 0xC6, 0x06, 0x09, 0x94, 0xE1, + 0x01, 0x20, 0x2D, 0x1C, 0x08, 0x2D, 0xF5, 0x91, 0xC4, 0x41, 0x2B, 0x58, 0xCD, 0x49, 0x2F, 0xC6, + 0xB9, 0x9D, 0x67, 0x17, 0xE4, 0x26, 0x7E, 0xD8, 0x79, 0x75, 0x78, 0xB2, 0xD7, 0x0E, 0x7A, 0xC3, + 0xAC, 0xA0, 0xC1, 0x6A, 0x42, 0xC4, 0xF7, 0xAE, 0x4A, 0xE8, 0xCB, 0x94, 0xFD, 0x9D, 0x9F, 0x2E, + 0x77, 0xB7, 0x4F, 0xB7, 0x2F, 0xB7, 0x8F, 0xF6, 0x31, 0x27, 0xED, 0x7D, 0x80, 0x8C, 0xD5, 0xA0, + 0x25, 0x48, 0x79, 0x32, 0x4A, 0x03, 0xC0, 0x19, 0xD3, 0x67, 0x3F, 0x0E, 0xAE, 0x93, 0x3E, 0x0D, + 0xC8, 0x6D, 0x1C, 0x14, 0x37, 0xD9, 0x5D, 0x40, 0x46, 0x71, 0x69, 0xCB, 0x5B, 0x16, 0x12, 0x67, + 0x8E, 0x13, 0x68, 0x5D, 0xAA, 0xFD, 0x13, 0xF3, 0xA9, 0xE4, 0xB7, 0x52, 0xBB, 0x39, 0x3B, 0x06, + 0x06, 0x62, 0x44, 0x71, 0xFD, 0x7C, 0x71, 0x9A, 0xE8, 0x70, 0x60, 0x94, 0x74, 0x9A, 0x5A, 0x78, + 0xBD, 0x53, 0x39, 0x1C, 0x6E, 0x32, 0x43, 0xD1, 0xA8, 0x1D, 0x6C, 0x71, 0xEF, 0x13, 0xE6, 0x51, + 0x84, 0xFA, 0xAE, 0x64, 0x01, 0x8E, 0x72, 0xB1, 0xBA, 0xB4, 0x1F, 0x46, 0x93, 0x89, 0xAC, 0x90, + 0xD3, 0xDB, 0xEC, 0x13, 0x55, 0x0D, 0xC9, 0x63, 0xB8, 0xD5, 0x4F, 0x8B, 0x91, 0x33, 0x22, 0x20, + 0x80, 0x2D, 0x59, 0x70, 0x17, 0xE0, 0x12, 0x3A, 0xE3, 0x22, 0xB9, 0xE8, 0xDB, 0x26, 0x08, 0x9E, + 0x11, 0x9A, 0xA9, 0x8E, 0xD2, 0x33, 0xC1, 0xCB, 0x96, 0xFF, 0xAC, 0x85, 0x12, 0x0E, 0xF3, 0x41, + 0x61, 0xEA, 0x2C, 0x6E, 0x3C, 0xFF, 0xA0, 0xF0, 0x64, 0x12, 0xEA, 0x43, 0x08, 0xA6, 0x8E, 0x94, + 0x05, 0x31, 0xE2, 0x1A, 0x4F, 0x0B, 0xC8, 0x85, 0xD1, 0x54, 0x41, 0xE3, 0x1F, 0xD0, 0xA0, 0x25, + 0x81, 0x76, 0xD3, 0x12, 0xD8, 0x62, 0xF6, 0x54, 0x85, 0xFE, 0x13, 0xA4, 0xCE, 0x15, 0xD8, 0xD9, + 0xD6, 0xF4, 0x59, 0x2F, 0xD6, 0xAA, 0x83, 0x3B, 0x00, 0x92, 0x14, 0xFC, 0x76, 0x86, 0x09, 0x1C, + 0x92, 0xB7, 0x11, 0xC1, 0x34, 0x24, 0xCF, 0x32, 0xA1, 0xAF, 0x28, 0x32, 0x07, 0x90, 0xCC, 0x3B, + 0xAE, 0xA1, 0x62, 0x07, 0x6A, 0xCF, 0x61, 0xC5, 0xCA, 0x6C, 0x40, 0xEB, 0xB2, 0x0F, 0x10, 0xC9, + 0xB3, 0x7B, 0xD3, 0x02, 0xF0, 0x5A, 0xD3, 0xC8, 0x4B, 0x03, 0x42, 0xC8, 0xA1, 0x43, 0xB8, 0x18, + 0xE1, 0xE0, 0x4D, 0x45, 0x07, 0x3F, 0xAA, 0xA9, 0x71, 0xED, 0xA4, 0xEC, 0x53, 0x24, 0x97, 0xA1, + 0x01, 0x99, 0x86, 0xE7, 0xAE, 0x06, 0x43, 0x88, 0xC7, 0x61, 0xEB, 0x52, 0x5E, 0xD4, 0xF6, 0x01, + 0x23, 0xF3, 0xEB, 0xA4, 0x57, 0x42, 0x38, 0x73, 0xB1, 0x91, 0xC3, 0xA0, 0x0E, 0x87, 0xAE, 0x41, + 0xA5, 0x58, 0x52, 0x02, 0x9B, 0x42, 0xEC, 0x57, 0xBC, 0x7B, 0x00, 0xC3, 0x05, 0x9B, 0x04, 0x0F, + 0x93, 0x54, 0xE5, 0xEA, 0x3C, 0x82, 0x78, 0xA2, 0xA8, 0x41, 0x1C, 0x23, 0xBB, 0x85, 0x9C, 0xB7, + 0x2C, 0x34, 0x95, 0xC3, 0x91, 0x14, 0x68, 0x37, 0x2D, 0x6E, 0xD3, 0xA2, 0x28, 0xAD, 0xA7, 0x19, + 0x4C, 0x79, 0xA2, 0xB0, 0x1D, 0x81, 0x32, 0xCB, 0x0D, 0xA5, 0xB6, 0x57, 0x08, 0x63, 0x91, 0xF8, + 0xA7, 0x9A, 0x05, 0x3E, 0x21, 0x4C, 0x09, 0xEE, 0x07, 0x72, 0xFE, 0x00, 0xD7, 0xA3, 0x76, 0xF0, + 0x66, 0xEF, 0xF8, 0x04, 0x96, 0x4A, 0x30, 0xAF, 0x75, 0x76, 0x21, 0x10, 0x92, 0x87, 0x60, 0x3A, + 0x05, 0xA4, 0x4D, 0xA7, 0x21, 0xAE, 0xB3, 0x46, 0x7A, 0x5C, 0x6E, 0x00, 0xA3, 0x4B, 0xB2, 0xC8, + 0xD3, 0x73, 0x41, 0xA8, 0xFA, 0x72, 0xC8, 0x9A, 0x50, 0x5D, 0x3C, 0x25, 0xA3, 0xCA, 0x6C, 0x42, + 0x04, 0xC9, 0x08, 0x20, 0xA0, 0x08, 0x64, 0x3C, 0xAA, 0x41, 0x5F, 0x67, 0xB5, 0x80, 0xDD, 0x80, + 0xE5, 0x1D, 0xE3, 0xE6, 0x8A, 0x47, 0x26, 0x95, 0x65, 0xC8, 0x86, 0x00, 0x85, 0x2C, 0xAD, 0xB7, + 0x01, 0x8C, 0x6E, 0xB5, 0x47, 0xEA, 0xED, 0x4A, 0xCA, 0xFD, 0x29, 0x0E, 0xE0, 0x80, 0xE6, 0x70, + 0x4A, 0x92, 0x7B, 0x41, 0xBA, 0xF5, 0xD7, 0x9E, 0xA0, 0xDD, 0xF7, 0x64, 0x27, 0x2E, 0x51, 0x5D, + 0x72, 0x2A, 0x29, 0xF9, 0xA7, 0x0B, 0x72, 0x02, 0xB3, 0x82, 0xDE, 0x3E, 0x01, 0xD9, 0xBD, 0x82, + 0x8A, 0x1C, 0xAA, 0xEC, 0x03, 0x1D, 0x06, 0xB0, 0x80, 0x7C, 0x60, 0x37, 0x56, 0x10, 0xE0, 0xD9, + 0x60, 0x30, 0xA4, 0xFF, 0xD4, 0x9D, 0x20, 0x08, 0xEE, 0xDC, 0x3C, 0x9D, 0x55, 0x60, 0xDE, 0x76, + 0xFC, 0x34, 0x65, 0xA3, 0x31, 0x6F, 0x33, 0x38, 0x60, 0xCF, 0x81, 0x1A, 0x43, 0xF6, 0x4D, 0xDA, + 0xEF, 0x53, 0xA8, 0x19, 0x3D, 0x25, 0x07, 0x30, 0x16, 0xDD, 0xE9, 0x21, 0xFC, 0x16, 0xBD, 0x7E, + 0x88, 0x1F, 0x1A, 0x0E, 0x8C, 0xBD, 0xD5, 0x1D, 0xF2, 0xE2, 0x70, 0xE7, 0xEC, 0xE4, 0xF2, 0xF9, + 0xAB, 0xB3, 0x63, 0x9B, 0x2F, 0x47, 0x29, 0xF2, 0x57, 0x81, 0x3F, 0x19, 0x8E, 0x73, 0xF1, 0x7B, + 0x4A, 0x8E, 0xBC, 0xC7, 0x06, 0x6B, 0x3E, 0x36, 0x98, 0xEF, 0xD8, 0x50, 0xF3, 0xAA, 0xED, 0x0B, + 0xB8, 0x62, 0x52, 0xFC, 0x83, 0x04, 0xD1, 0x69, 0xCE, 0x12, 0xC7, 0x3B, 0x24, 0x8C, 0x48, 0x9A, + 0x98, 0x96, 0x66, 0xD8, 0x82, 0x75, 0x0A, 0xBB, 0x2D, 0xD8, 0xB7, 0x54, 0x9C, 0xE6, 0x41, 0x9E, + 0xF4, 0xD3, 0x0C, 0x77, 0x50, 0xDA, 0xC2, 0xE1, 0x44, 0x98, 0xD3, 0xEA, 0x21, 0x9B, 0x4A, 0xFB, + 0x2B, 0x2B, 0x6E, 0x33, 0x3D, 0x24, 0x6E, 0xAF, 0xD2, 0x02, 0x7E, 0x81, 0x60, 0x2B, 0x01, 0xBE, + 0x24, 0x3C, 0x01, 0x86, 0x0F, 0x29, 0x3B, 0xD2, 0x18, 0xCD, 0x84, 0x56, 0xBA, 0x3B, 0x88, 0x3A, + 0x19, 0x5C, 0x80, 0xC3, 0xAC, 0x4C, 0x34, 0xB1, 0xE6, 0x14, 0xFA, 0x92, 0x3C, 0x45, 0x8A, 0xA4, + 0xB3, 0x7C, 0xF3, 0x4A, 0x0B, 0xBC, 0x51, 0xF7, 0x03, 0x38, 0xD7, 0xD8, 0x8C, 0xBC, 0xD4, 0x33, + 0xA4, 0x4A, 0xED, 0xD9, 0x25, 0x34, 0xE7, 0xD2, 0x31, 0x73, 0x8E, 0x97, 0x17, 0x98, 0x33, 0x90, + 0xFB, 0xD4, 0x52, 0xC3, 0xA0, 0x07, 0x7B, 0x73, 0x40, 0x03, 0x98, 0x50, 0x4B, 0xA0, 0x08, 0x12, + 0x0E, 0x00, 0xCA, 0x74, 0x4A, 0xAB, 0x10, 0x2C, 0x9C, 0xCB, 0x62, 0x92, 0xA7, 0xC9, 0x1A, 0xD0, + 0x9C, 0x02, 0x19, 0x18, 0xB2, 0x48, 0xC7, 0x11, 0x01, 0x3A, 0x55, 0x45, 0x03, 0x89, 0x39, 0x1A, + 0xAC, 0x5F, 0x71, 0xE4, 0xDF, 0xFB, 0x8F, 0x7C, 0x36, 0x83, 0xA2, 0xD3, 0x47, 0x50, 0x74, 0x49, + 0xB1, 0xEF, 0xA3, 0x0E, 0x07, 0x6A, 0xCE, 0x05, 0x35, 0x67, 0x8A, 0x9A, 0xBB, 0x25, 0x08, 0x47, + 0x9A, 0x2E, 0x67, 0x83, 0x28, 0x89, 0xE0, 0x3B, 0xA7, 0x17, 0xA1, 0x20, 0xE8, 0x40, 0xF3, 0xBE, + 0x92, 0xF4, 0x32, 0x0F, 0xE9, 0xFD, 0x50, 0x25, 0xBD, 0xEE, 0x51, 0x5B, 0x3F, 0x0A, 0x14, 0x93, + 0xAE, 0x79, 0xDF, 0x8E, 0x7B, 0xE0, 0x5F, 0x45, 0xE2, 0xA0, 0x82, 0x44, 0xBB, 0x29, 0x0F, 0x61, + 0x32, 0x47, 0x35, 0x10, 0xCA, 0x8B, 0x16, 0x14, 0xB4, 0x33, 0x8D, 0xA6, 0x6A, 0x48, 0x96, 0xE8, + 0x34, 0x8D, 0x4B, 0x0E, 0xC2, 0xB2, 0xE0, 0xA5, 0xDE, 0x04, 0x09, 0x80, 0x76, 0x5D, 0x74, 0xD8, + 0x27, 0xEB, 0xFF, 0x94, 0x38, 0x99, 0xB2, 0xA8, 0xFB, 0x64, 0x5D, 0x5E, 0xD3, 0xB8, 0xDC, 0xEB, + 0xD0, 0x31, 0x51, 0x74, 0x39, 0xAE, 0x8F, 0x54, 0x65, 0xB9, 0x07, 0xC8, 0x91, 0x4A, 0x5D, 0xE4, + 0x00, 0x81, 0x56, 0x4F, 0xC9, 0x51, 0xC3, 0x01, 0xF2, 0x2A, 0x0E, 0x7A, 0x49, 0x9E, 0x8D, 0x41, + 0xE2, 0x1B, 0x90, 0xF7, 0xE2, 0x08, 0xB1, 0xDF, 0x2F, 0xC5, 0x21, 0xF2, 0x9E, 0x1C, 0x3B, 0x87, + 0xC8, 0x67, 0x79, 0x88, 0xBC, 0xBA, 0x20, 0x2F, 0xE2, 0x87, 0x14, 0x9B, 0x03, 0x01, 0x5D, 0xFB, + 0xCF, 0xF4, 0x19, 0x01, 0x84, 0xB8, 0xCA, 0x92, 0xBC, 0x8F, 0xE2, 0xB8, 0x62, 0x08, 0x77, 0xC9, + 0x36, 0x48, 0x6B, 0x47, 0x09, 0x34, 0xD6, 0x0E, 0x6E, 0x00, 0xE7, 0xF3, 0x80, 0xDC, 0x81, 0x38, + 0x19, 0xB3, 0x79, 0x36, 0xEE, 0xDD, 0xC0, 0x8F, 0x29, 0x39, 0x2B, 0x35, 0x12, 0x84, 0x20, 0xE2, + 0xBB, 0xA2, 0xF9, 0xE4, 0x4A, 0x5E, 0xF0, 0xA3, 0xC0, 0xB6, 0x19, 0xA8, 0xB4, 0x40, 0x36, 0x0D, + 0x65, 0x55, 0xC2, 0xA4, 0x10, 0x57, 0x67, 0x28, 0xAB, 0xBA, 0x0A, 0x65, 0x82, 0x69, 0x44, 0xF6, + 0x6A, 0x1B, 0x90, 0x9D, 0xDB, 0xEF, 0x29, 0x79, 0x1B, 0x07, 0x8C, 0x7E, 0x86, 0xBB, 0xC8, 0xC7, + 0x38, 0x40, 0x8C, 0x0B, 0xC8, 0xEB, 0x38, 0x18, 0xD2, 0x6B, 0x48, 0xF9, 0x29, 0x0E, 0x50, 0x1D, + 0x01, 0xBF, 0x7E, 0x8E, 0x1F, 0x4E, 0x5E, 0xED, 0xEF, 0xC2, 0xE5, 0x47, 0x0C, 0x20, 0x58, 0x7D, + 0x49, 0xF0, 0x5B, 0x7E, 0xE2, 0xD7, 0x4F, 0x7B, 0xEF, 0x76, 0x0F, 0xDF, 0x1E, 0xB4, 0x03, 0x18, + 0x33, 0x5C, 0x0F, 0x19, 0xA6, 0xBD, 0x3E, 0x3C, 0x3B, 0xD9, 0xDB, 0x3B, 0x38, 0xDD, 0x3B, 0x6E, + 0x07, 0xB7, 0x08, 0x56, 0x8A, 0x73, 0x35, 0x39, 0xAF, 0xF6, 0xB6, 0xDF, 0xEC, 0xA9, 0x1C, 0x18, + 0xCB, 0x27, 0xD1, 0xEA, 0xE9, 0xE1, 0xD9, 0xCE, 0xCB, 0x93, 0xD3, 0xED, 0xE3, 0xD3, 0x76, 0x20, + 0x86, 0x5A, 0x00, 0x96, 0x71, 0x93, 0xF3, 0xFA, 0x10, 0xAA, 0xC8, 0x8C, 0xDB, 0xAC, 0x54, 0x03, + 0xF8, 0x54, 0x95, 0x4C, 0x99, 0x18, 0xCF, 0xD1, 0xE1, 0x3E, 0x76, 0x2C, 0xC7, 0x34, 0xCA, 0x04, + 0x94, 0xC5, 0xB8, 0x6C, 0xDE, 0xD9, 0x91, 0xC9, 0x19, 0x8F, 0x30, 0x7D, 0xF7, 0x78, 0xFB, 0x87, + 0x4B, 0xD5, 0x77, 0x3F, 0x4F, 0x06, 0xA6, 0xEB, 0x57, 0x87, 0xDB, 0xBB, 0xA5, 0x33, 0x18, 0x44, + 0x42, 0xD8, 0xC7, 0xEA, 0xB1, 0xFF, 0xBA, 0x27, 0xB2, 0xA6, 0xE4, 0x79, 0x19, 0xBB, 0xDE, 0x58, + 0x96, 0xE3, 0x5D, 0xAC, 0x60, 0x48, 0xBE, 0xD8, 0x12, 0x6B, 0x29, 0xA7, 0xB7, 0x6B, 0x0A, 0xD8, + 0xBF, 0x54, 0xD3, 0xE5, 0x6A, 0x3C, 0xA9, 0x26, 0xCB, 0x65, 0xFB, 0xA1, 0x9A, 0x2C, 0x17, 0xF1, + 0xC7, 0x58, 0x4F, 0x6E, 0x0D, 0xA9, 0x08, 0x14, 0xFC, 0xB5, 0xC4, 0x82, 0x70, 0x6E, 0x3E, 0x5A, + 0x4E, 0xED, 0x80, 0x50, 0xC8, 0xAA, 0xA6, 0xB1, 0x5A, 0xDA, 0x52, 0x7A, 0x3B, 0x08, 0x48, 0x8A, + 0xE9, 0xF5, 0x41, 0x91, 0xA5, 0x96, 0x6F, 0x48, 0x99, 0x5B, 0x9A, 0xF5, 0xD3, 0x5E, 0x82, 0x6A, + 0x29, 0xB8, 0x7A, 0x43, 0x8E, 0xE4, 0xAC, 0x04, 0x68, 0x2E, 0xC8, 0x52, 0xE9, 0x0B, 0xB8, 0xAD, + 0x0B, 0x40, 0x7C, 0xAE, 0x99, 0xAF, 0x1C, 0xD2, 0xEC, 0x9C, 0x91, 0xF7, 0x4A, 0x78, 0xFC, 0x20, + 0xD0, 0x40, 0xE1, 0x40, 0x40, 0x8E, 0xF6, 0x70, 0xD9, 0x29, 0xE2, 0xF7, 0x90, 0x7B, 0x59, 0xA3, + 0x5C, 0x2A, 0x65, 0xE4, 0xA1, 0x83, 0x83, 0x2C, 0xC4, 0x91, 0xA3, 0x4E, 0x21, 0xBD, 0x2B, 0xCB, + 0x69, 0x08, 0x2E, 0x73, 0x37, 0x73, 0x0A, 0x17, 0x47, 0xB8, 0xF9, 0xFA, 0x70, 0xDE, 0x9A, 0x94, + 0x13, 0x18, 0x39, 0xEC, 0x43, 0x93, 0x24, 0xC6, 0x25, 0xE5, 0xD7, 0xAA, 0xAE, 0x4D, 0x3E, 0x41, + 0x3C, 0xFB, 0x25, 0xDE, 0x28, 0x25, 0xED, 0xD2, 0x21, 0x4F, 0x4C, 0xD2, 0xA5, 0x94, 0xF7, 0xDB, + 0x9B, 0xFA, 0x0E, 0x7E, 0x0B, 0x02, 0x5E, 0x61, 0xEE, 0xD4, 0xB7, 0x85, 0xAD, 0x1A, 0xEE, 0x4C, + 0x66, 0x2C, 0xE3, 0xBA, 0x1D, 0x39, 0x1C, 0x29, 0x88, 0x84, 0xF9, 0x04, 0x19, 0xB3, 0x7B, 0x10, + 0x35, 0x4C, 0x4D, 0xD2, 0xAC, 0xC9, 0x64, 0x63, 0x8B, 0x25, 0x9F, 0xD2, 0x01, 0xF6, 0x09, 0xF2, + 0xBB, 0xCF, 0xA7, 0x58, 0xED, 0x08, 0xF1, 0xAF, 0x50, 0x4D, 0x2B, 0x64, 0x94, 0xD7, 0x64, 0x2D, + 0xC3, 0xBC, 0x83, 0x81, 0x66, 0x77, 0xAD, 0x23, 0x9D, 0x27, 0x9A, 0x52, 0x89, 0xAF, 0x4F, 0x4A, + 0xC9, 0x66, 0x84, 0x49, 0xBF, 0x2F, 0xBE, 0x91, 0x2B, 0xA1, 0x8C, 0xE6, 0xC0, 0xE8, 0x28, 0x86, + 0x36, 0xF7, 0x31, 0xB4, 0x80, 0x8D, 0xCE, 0xF2, 0x57, 0x96, 0x47, 0x8B, 0x2C, 0x04, 0x9E, 0x85, + 0x6F, 0xE1, 0x88, 0x97, 0x55, 0xDE, 0x02, 0x19, 0x79, 0x93, 0x16, 0xE9, 0x95, 0xCB, 0x0C, 0x5B, + 0x69, 0x9E, 0xE4, 0xEC, 0x3D, 0x7C, 0x50, 0x0A, 0xAC, 0x5D, 0xFB, 0x93, 0xAC, 0x1A, 0x44, 0x20, + 0x80, 0x97, 0x25, 0x51, 0x0A, 0xEF, 0x96, 0xD5, 0xC2, 0x6D, 0x51, 0x36, 0x05, 0x51, 0xF4, 0x3D, + 0x16, 0xC7, 0x12, 0x62, 0x04, 0x78, 0x9D, 0x95, 0x1C, 0xC0, 0xC2, 0xC3, 0xFF, 0x28, 0xAB, 0x24, + 0xE3, 0x8A, 0xE4, 0x07, 0x59, 0x1E, 0x17, 0x51, 0x85, 0x6A, 0x70, 0x16, 0x46, 0xA4, 0x1C, 0xC6, + 0x12, 0x36, 0x28, 0x56, 0x9C, 0x8A, 0xAA, 0x9D, 0xDE, 0x7D, 0x6F, 0x48, 0x43, 0x68, 0x36, 0x22, + 0xF0, 0x23, 0xC9, 0xF7, 0xD5, 0xFE, 0x09, 0xDD, 0xED, 0x14, 0xF9, 0xB6, 0x17, 0x8C, 0x5A, 0xD4, + 0x9F, 0x3F, 0xEA, 0xCD, 0x6A, 0x7D, 0x18, 0xE4, 0xA3, 0xBB, 0x8B, 0x9C, 0x3D, 0xD5, 0xB2, 0x4D, + 0x2D, 0xBB, 0xDD, 0x41, 0xDB, 0x95, 0xDA, 0xC0, 0x39, 0x9B, 0x8E, 0x42, 0x83, 0x0A, 0x76, 0x01, + 0x61, 0x1F, 0x73, 0xDA, 0xC5, 0x4A, 0x55, 0x24, 0x6A, 0x9B, 0x44, 0xD0, 0x2C, 0xA7, 0x0A, 0x86, + 0x0D, 0x03, 0x89, 0xC4, 0x32, 0xF2, 0xAC, 0x41, 0x7A, 0xD7, 0x29, 0x13, 0xA3, 0x45, 0x76, 0x37, + 0xE7, 0x92, 0x71, 0x64, 0x96, 0x7C, 0xEC, 0x03, 0xB9, 0xDB, 0x07, 0x5D, 0xFC, 0xE7, 0xD0, 0xD3, + 0x98, 0x14, 0xAC, 0x87, 0xFC, 0x7B, 0x4B, 0x1A, 0xB5, 0x1A, 0x77, 0x6D, 0x13, 0x30, 0x6E, 0x0B, + 0x16, 0x19, 0x4A, 0x54, 0x50, 0x31, 0xAA, 0x60, 0xB7, 0x94, 0x25, 0xFD, 0xDC, 0x42, 0x2E, 0x81, + 0xD4, 0x99, 0x33, 0x0A, 0x13, 0x0C, 0x51, 0x62, 0x24, 0x6F, 0x6D, 0x78, 0x6D, 0x04, 0x66, 0xDB, + 0x68, 0x89, 0xB0, 0x29, 0x89, 0xC9, 0x70, 0x8F, 0xF9, 0x94, 0xA5, 0xFD, 0xA5, 0x12, 0x96, 0xC9, + 0xE9, 0xA4, 0x31, 0xDB, 0xE2, 0xDD, 0xB7, 0xED, 0x8F, 0x9D, 0xF2, 0x1E, 0x48, 0x89, 0x1D, 0x35, + 0x6A, 0x93, 0xA3, 0x69, 0xC3, 0x55, 0xA4, 0x3E, 0xDC, 0xEB, 0xEB, 0xF0, 0x65, 0x44, 0x66, 0x5C, + 0x51, 0xDE, 0x47, 0xA4, 0x7A, 0x5A, 0x38, 0x2B, 0x68, 0x52, 0x6C, 0x9D, 0x39, 0x47, 0x8A, 0x45, + 0x6D, 0x9B, 0x66, 0x41, 0xBA, 0xD8, 0xD9, 0x53, 0xA5, 0xF4, 0x66, 0x4B, 0xD9, 0x73, 0x22, 0xF6, + 0xE9, 0xAB, 0xE2, 0x61, 0x08, 0x9A, 0xF6, 0x17, 0xA8, 0x22, 0xBD, 0x6C, 0x55, 0xF4, 0x42, 0xE1, + 0x2B, 0xC2, 0xC9, 0x19, 0x4C, 0x17, 0xDA, 0xD1, 0x52, 0x9F, 0x93, 0xBB, 0x74, 0xE4, 0x91, 0x0C, + 0x08, 0x7D, 0x68, 0x72, 0x55, 0x84, 0xD5, 0xB3, 0x4B, 0xA3, 0xD1, 0x56, 0xFC, 0xDD, 0x46, 0x64, + 0x10, 0x78, 0xBD, 0x5A, 0xAC, 0xB3, 0xB1, 0xA5, 0x6E, 0xA8, 0x82, 0xD8, 0xE1, 0xB5, 0x75, 0x6B, + 0xC3, 0xA5, 0x83, 0x38, 0x88, 0x3A, 0xDD, 0xB7, 0x43, 0xA9, 0xEE, 0x0E, 0xBB, 0xA5, 0x34, 0xFF, + 0xEC, 0xA1, 0xD3, 0x50, 0xEF, 0xE7, 0x96, 0x62, 0x5A, 0xCB, 0xD7, 0x1B, 0x8B, 0xA0, 0x97, 0x8A, + 0x93, 0x15, 0x68, 0x4A, 0x14, 0x0F, 0x0F, 0x48, 0x5A, 0xEE, 0x41, 0xE2, 0x68, 0x53, 0xF3, 0x96, + 0xFF, 0xF5, 0xF6, 0x20, 0x2B, 0x63, 0xF3, 0xAA, 0xBC, 0xE5, 0x8A, 0xBD, 0xE5, 0x05, 0xFA, 0xCB, + 0xF2, 0x0E, 0xDE, 0x49, 0x70, 0x4A, 0x98, 0x09, 0x40, 0x89, 0xC3, 0xB7, 0x76, 0x4A, 0x12, 0x6E, + 0x33, 0x67, 0x83, 0xD2, 0x8A, 0x99, 0x7D, 0x5C, 0x81, 0x2C, 0xE3, 0x12, 0x6B, 0xE6, 0x9E, 0xEF, + 0x2B, 0x2B, 0x09, 0x3F, 0x07, 0x8D, 0x28, 0x9C, 0x1E, 0x29, 0x4B, 0x86, 0x22, 0xAD, 0xA5, 0x0A, + 0xA0, 0x62, 0xC7, 0x55, 0xC7, 0x5D, 0x74, 0x99, 0xC3, 0x07, 0x55, 0x2B, 0x02, 0x53, 0x0D, 0xFF, + 0xFC, 0xD2, 0x76, 0x3B, 0xC1, 0xF3, 0x61, 0x76, 0x3D, 0x79, 0x1D, 0x28, 0xE0, 0x4E, 0xAA, 0x9B, + 0x40, 0x99, 0xEC, 0x37, 0x1C, 0x37, 0xAA, 0x10, 0x4A, 0xB8, 0xDC, 0x38, 0xF2, 0x35, 0x67, 0x9C, + 0x20, 0x0C, 0x70, 0xF6, 0x54, 0x58, 0x46, 0x2E, 0xE6, 0x62, 0x16, 0xF6, 0x60, 0xE8, 0x20, 0x2B, + 0x33, 0x91, 0x30, 0x56, 0x71, 0xF2, 0xA9, 0xAF, 0xD0, 0xCD, 0xAC, 0x16, 0x8E, 0x1D, 0xE3, 0x89, + 0xBA, 0x38, 0xDF, 0xA2, 0x16, 0xF9, 0xF3, 0xC6, 0xC6, 0x2A, 0xF3, 0x1D, 0x4A, 0x70, 0xB1, 0x9F, + 0x71, 0xCC, 0x6C, 0xA3, 0x1A, 0x98, 0x47, 0x0A, 0x93, 0xED, 0x4D, 0xCA, 0x87, 0xC9, 0x35, 0xC9, + 0x86, 0xC6, 0x68, 0x77, 0x35, 0xBA, 0xA1, 0x7F, 0x5B, 0x95, 0xAE, 0x76, 0xBE, 0xD6, 0xD5, 0x0E, + 0x99, 0x59, 0xF7, 0xEC, 0xC8, 0x57, 0x33, 0x95, 0x35, 0x9B, 0x64, 0x60, 0xB0, 0x79, 0xC2, 0x1F, + 0xC1, 0x0E, 0xA0, 0x61, 0x58, 0xF6, 0xE6, 0xFA, 0xE8, 0x51, 0x99, 0xAB, 0x6D, 0x5D, 0xD0, 0xD2, + 0x41, 0x05, 0x5D, 0xE4, 0xC7, 0xEC, 0x95, 0x95, 0xCD, 0x2D, 0xEA, 0xCD, 0xD2, 0x07, 0x76, 0xD7, + 0xC5, 0xCF, 0x8D, 0xB6, 0xFB, 0x4D, 0xE7, 0xEE, 0x18, 0x17, 0x75, 0x1B, 0x27, 0xA0, 0xEF, 0xE0, + 0xCD, 0x50, 0x15, 0x64, 0xDC, 0xD0, 0xD4, 0xB8, 0xAA, 0x26, 0x5F, 0x17, 0xB2, 0xF3, 0x09, 0xA7, + 0x9F, 0xA1, 0x23, 0x9A, 0xAC, 0xA7, 0x42, 0x48, 0xA4, 0x65, 0x4C, 0xF8, 0xCF, 0xE0, 0x20, 0xB9, + 0x05, 0x71, 0x51, 0x71, 0x97, 0x0A, 0x8B, 0x94, 0xD6, 0xDD, 0x4D, 0xDA, 0xBB, 0x89, 0x1E, 0x7A, + 0xB0, 0x15, 0x97, 0x9E, 0xFD, 0xB5, 0xED, 0x51, 0x9C, 0x94, 0x0E, 0x95, 0xCE, 0x15, 0x34, 0xFA, + 0xA1, 0x23, 0x0B, 0xFF, 0xBD, 0xB1, 0xB0, 0x3D, 0x70, 0x5C, 0x36, 0xC9, 0x77, 0x80, 0x96, 0x59, + 0x01, 0xD8, 0x8C, 0x8E, 0xC2, 0xFD, 0xFC, 0xA2, 0x05, 0xEC, 0x88, 0x16, 0xB4, 0x95, 0xB3, 0xEA, + 0x9B, 0x86, 0x72, 0x40, 0xA9, 0xF3, 0x0B, 0x52, 0xE6, 0xB6, 0x52, 0xEC, 0xF4, 0x10, 0x25, 0xD3, + 0xE5, 0xA1, 0x3C, 0xBF, 0xDF, 0x05, 0x33, 0x48, 0x31, 0x8E, 0xD2, 0x80, 0x88, 0xD5, 0x0D, 0x02, + 0xFD, 0x78, 0x4B, 0x52, 0xF1, 0xEF, 0x47, 0x92, 0x79, 0xD8, 0x3D, 0xBC, 0x30, 0xE6, 0xB1, 0x8F, + 0xAF, 0x43, 0x62, 0x1F, 0x82, 0xFD, 0xE0, 0x06, 0xD4, 0xCD, 0x26, 0x13, 0xB8, 0xD9, 0x64, 0xF0, + 0x2B, 0x8F, 0x24, 0x57, 0x6C, 0x8F, 0x1A, 0x14, 0x26, 0x45, 0x1A, 0xA5, 0x05, 0x1F, 0x56, 0xC4, + 0x61, 0xB6, 0x1A, 0x8A, 0x3E, 0xBB, 0x6B, 0x9B, 0xED, 0xCD, 0x28, 0xFA, 0x43, 0xBD, 0x03, 0x75, + 0x21, 0x5B, 0xDB, 0x84, 0x62, 0x45, 0xB7, 0xCC, 0xA2, 0xF9, 0x06, 0x73, 0xD1, 0x2E, 0x97, 0x28, + 0x2E, 0xCA, 0xBA, 0x56, 0xE4, 0x8D, 0xA4, 0xAE, 0xB5, 0x02, 0x83, 0x66, 0x16, 0x17, 0x14, 0x5F, + 0xBE, 0xF4, 0x39, 0x3C, 0x33, 0xD8, 0x14, 0x69, 0xDD, 0xAD, 0x62, 0x63, 0xF7, 0x08, 0xAC, 0x3D, + 0x6A, 0x37, 0xFB, 0xA7, 0x02, 0x29, 0xDB, 0x9C, 0xF4, 0xF5, 0x82, 0x80, 0xA9, 0xC2, 0x35, 0xE8, + 0x53, 0xDB, 0xC0, 0x7F, 0x66, 0x6D, 0x36, 0xB5, 0x5A, 0x5E, 0xB7, 0x1B, 0x2B, 0xB0, 0xCF, 0xA0, + 0x7D, 0x31, 0x2F, 0x14, 0xC7, 0x0B, 0xE6, 0x6E, 0x5F, 0x73, 0x72, 0x8A, 0x91, 0xAB, 0xEE, 0x91, + 0x06, 0x7E, 0x4F, 0x1F, 0xC7, 0x0E, 0xCE, 0x35, 0x15, 0xAE, 0xA3, 0xDF, 0xAF, 0x60, 0x48, 0x53, + 0x53, 0x26, 0xBF, 0xD1, 0x37, 0x86, 0xA6, 0x66, 0x7A, 0x37, 0xE9, 0xB0, 0x0F, 0x38, 0x7D, 0xEE, + 0x85, 0xF7, 0x45, 0x47, 0xDC, 0x8C, 0x59, 0x84, 0x24, 0x53, 0xB5, 0x28, 0x76, 0x95, 0xE2, 0xD2, + 0x7D, 0x2B, 0x47, 0x52, 0x92, 0x29, 0xDC, 0x24, 0xC5, 0xBC, 0xFB, 0x0C, 0x49, 0x7C, 0x0B, 0x0A, + 0x17, 0xAA, 0x61, 0x0C, 0xDA, 0xDA, 0x42, 0xB2, 0x42, 0x95, 0x7D, 0xA3, 0x3B, 0x84, 0x52, 0x3D, + 0x5F, 0x6D, 0xB8, 0x17, 0xDE, 0x68, 0xB9, 0x44, 0xF5, 0x1A, 0x89, 0xBB, 0x23, 0x93, 0xFB, 0xAB, + 0x0B, 0x17, 0x95, 0x5F, 0x00, 0xA1, 0x9E, 0x90, 0xD7, 0x51, 0x1B, 0x7E, 0x7F, 0x81, 0xDF, 0x3F, + 0x90, 0x9F, 0xA0, 0x6B, 0x9C, 0xF2, 0x50, 0xC8, 0xE3, 0xF5, 0x94, 0xA3, 0x0A, 0x43, 0xAF, 0x95, + 0x54, 0x4B, 0x48, 0xF1, 0x44, 0x5E, 0x1D, 0xAB, 0xC3, 0x21, 0xC9, 0xFC, 0xE6, 0x15, 0x2B, 0x2B, + 0x30, 0xB1, 0x61, 0x4D, 0x06, 0x80, 0x6A, 0x39, 0xC9, 0xFC, 0xD9, 0x1B, 0x13, 0x7E, 0x34, 0x61, + 0x96, 0xE8, 0x43, 0xAE, 0xEF, 0xB8, 0x82, 0xE0, 0x55, 0xFC, 0x1E, 0x96, 0xF0, 0x3B, 0x93, 0xF8, + 0x9D, 0x20, 0x7E, 0xF7, 0xA6, 0x02, 0x22, 0x55, 0xDC, 0xB6, 0x53, 0x7F, 0x17, 0xE1, 0x05, 0x6B, + 0x58, 0x5A, 0xFF, 0x14, 0x6F, 0x19, 0xD2, 0x7A, 0x0E, 0xD2, 0xE1, 0x14, 0x29, 0x4A, 0x99, 0x0C, + 0x13, 0x86, 0xE5, 0x04, 0x39, 0xBE, 0x6B, 0x69, 0xD4, 0xB7, 0x8F, 0x23, 0xF6, 0xD9, 0x45, 0xEA, + 0xF5, 0x01, 0x7B, 0xC0, 0xCD, 0x8D, 0xA8, 0xE3, 0xBD, 0x4D, 0xC7, 0xD7, 0xDD, 0xB0, 0x94, 0x61, + 0xEC, 0x5C, 0xF7, 0x75, 0xFE, 0xAC, 0xCC, 0xC9, 0xC4, 0xDB, 0x26, 0xB9, 0x8E, 0xDA, 0x5F, 0x51, + 0x4D, 0xCC, 0xA9, 0xBF, 0x88, 0x39, 0x46, 0x81, 0x7B, 0x72, 0x01, 0xCB, 0x58, 0x80, 0x9A, 0xBB, + 0x71, 0x19, 0x28, 0x85, 0x83, 0xD5, 0xB4, 0xBC, 0xF1, 0x04, 0xB0, 0x2B, 0xDB, 0x5B, 0x96, 0x12, + 0x7F, 0x01, 0xF6, 0xB9, 0x8B, 0xA2, 0x0D, 0x86, 0xB6, 0x86, 0xA0, 0xE5, 0x1E, 0x6A, 0x36, 0x86, + 0x83, 0x6A, 0xA3, 0xD1, 0xFE, 0xA3, 0xAF, 0xEC, 0x3F, 0xEA, 0x03, 0xB1, 0x0B, 0x6F, 0x12, 0xEA, + 0x7B, 0xC6, 0xF2, 0x1C, 0xF5, 0x6E, 0x3B, 0x0A, 0xF5, 0x25, 0x03, 0x2B, 0xCE, 0xEE, 0x7C, 0x86, + 0xCA, 0x30, 0x7D, 0xB4, 0xCA, 0xF0, 0x3D, 0x5C, 0x48, 0xF5, 0x55, 0xD9, 0xC9, 0x00, 0xBA, 0x59, + 0xF5, 0x74, 0x10, 0x06, 0xF8, 0xAA, 0x34, 0x05, 0x93, 0x48, 0x4D, 0x45, 0x03, 0xA9, 0x93, 0x29, + 0x95, 0xEB, 0xA6, 0x6D, 0xDA, 0x12, 0xB4, 0x10, 0x37, 0x92, 0xD5, 0x49, 0xE6, 0xA2, 0x07, 0xB4, + 0xB4, 0x77, 0x07, 0x21, 0xB5, 0x92, 0x52, 0x3F, 0x54, 0x6A, 0x26, 0x42, 0x96, 0x0D, 0x26, 0x65, + 0xA8, 0x4A, 0xAD, 0x27, 0x26, 0x8E, 0x0E, 0xEB, 0x78, 0x61, 0x73, 0x38, 0x98, 0xAA, 0x94, 0x0C, + 0x25, 0xF1, 0x7A, 0x23, 0x8D, 0x25, 0x9F, 0x1E, 0x64, 0x4B, 0xB7, 0x14, 0xDC, 0x4C, 0xFA, 0x4B, + 0x0C, 0x58, 0xAE, 0x3E, 0xDA, 0x3C, 0x32, 0xB0, 0x79, 0x7C, 0x1A, 0x75, 0xB0, 0x4E, 0xA8, 0x16, + 0x93, 0x96, 0x24, 0x67, 0xB4, 0x85, 0x32, 0x79, 0x14, 0x98, 0x59, 0x12, 0xA4, 0x57, 0x04, 0xB0, + 0x42, 0xAC, 0x09, 0x4E, 0x64, 0x7B, 0x94, 0xEE, 0xA0, 0x86, 0xE4, 0xA5, 0xB4, 0xCB, 0x7B, 0x94, + 0xF9, 0x16, 0x42, 0x03, 0x81, 0x65, 0x8E, 0x7A, 0x38, 0xB5, 0xB4, 0xC9, 0x81, 0x3A, 0x6E, 0x2C, + 0x01, 0x7A, 0xAE, 0x24, 0x0E, 0xA9, 0x5C, 0x0C, 0xC8, 0xD4, 0xAB, 0x56, 0x5D, 0x44, 0xC5, 0x27, + 0xD5, 0xC9, 0x8B, 0x55, 0x3E, 0x04, 0xC2, 0x78, 0x20, 0x4C, 0xCD, 0x84, 0x85, 0x20, 0x32, 0x6F, + 0x54, 0xAB, 0xC2, 0x96, 0x4A, 0xA1, 0x5D, 0x31, 0x26, 0x83, 0x45, 0xB0, 0x50, 0x78, 0xE6, 0x7B, + 0xAE, 0x3E, 0xC2, 0xA4, 0x27, 0xFF, 0x1A, 0xBD, 0x32, 0x91, 0xA5, 0x55, 0x53, 0x0D, 0xA5, 0x97, + 0x5E, 0x08, 0x05, 0x74, 0xEE, 0x51, 0x40, 0xFF, 0x5C, 0x55, 0x40, 0xE7, 0x1C, 0xB4, 0x25, 0xDE, + 0xA5, 0x02, 0xB8, 0x29, 0x61, 0xBD, 0xAA, 0xE9, 0xA8, 0xC4, 0x2C, 0x4D, 0x2A, 0x39, 0x82, 0xB8, + 0x1C, 0x89, 0xE9, 0xB9, 0xCE, 0x82, 0x14, 0x3C, 0xC2, 0x4D, 0x86, 0xD6, 0x25, 0x5C, 0xF3, 0x8B, + 0x74, 0x8B, 0x75, 0xA8, 0x75, 0x8C, 0x19, 0x84, 0xA8, 0x84, 0x8F, 0x3A, 0x43, 0xDE, 0x00, 0xF5, + 0x94, 0xA4, 0x7A, 0x45, 0xA7, 0x5A, 0xA3, 0xFC, 0xEA, 0x22, 0xF6, 0x94, 0x57, 0x79, 0x8E, 0x4A, + 0x19, 0xCA, 0xA9, 0xE4, 0x45, 0x74, 0xCA, 0xD0, 0xEE, 0x67, 0x80, 0x53, 0x83, 0x52, 0x19, 0xEA, + 0x05, 0xBD, 0x6C, 0x38, 0x4C, 0x46, 0x05, 0xE8, 0xD9, 0x6E, 0xB8, 0x50, 0x2B, 0xDB, 0x84, 0x31, + 0x17, 0x7A, 0xE5, 0x1B, 0x4E, 0xAE, 0xB9, 0xD4, 0x25, 0xF7, 0xC0, 0x0F, 0xA4, 0x0F, 0x0A, 0x2C, + 0xA9, 0x32, 0x47, 0xFD, 0xB0, 0xBC, 0x2E, 0xA0, 0xA9, 0x3E, 0x19, 0x98, 0x0C, 0xAB, 0xB2, 0x55, + 0xF9, 0x56, 0xD7, 0xAB, 0xE9, 0x22, 0x94, 0xBF, 0x84, 0xF2, 0x27, 0x2F, 0x0F, 0xDF, 0xB6, 0xA5, + 0x35, 0xE9, 0xEA, 0x98, 0x13, 0xFC, 0x3C, 0x90, 0xDF, 0x4C, 0x24, 0xBC, 0x14, 0x7A, 0xDC, 0x1B, + 0x40, 0x6E, 0xFD, 0xB9, 0x0B, 0xDA, 0x32, 0xA5, 0xA8, 0xC0, 0x24, 0xBF, 0x5A, 0x13, 0x73, 0x5C, + 0x33, 0xD6, 0x5B, 0x6E, 0x8C, 0x56, 0x9D, 0x49, 0x7F, 0xB2, 0x5F, 0x48, 0x8C, 0xC8, 0xBD, 0xF9, + 0x16, 0x46, 0x26, 0x7B, 0xF0, 0x79, 0x97, 0xF6, 0x39, 0xA8, 0xE9, 0x76, 0xE0, 0xE7, 0x0D, 0x95, + 0x5A, 0xCF, 0x53, 0x84, 0x0C, 0x36, 0x07, 0xDA, 0xC3, 0x72, 0xED, 0x13, 0xEE, 0x5A, 0x5D, 0xD9, + 0xB6, 0x50, 0xF5, 0x77, 0xE5, 0xD7, 0xEE, 0x25, 0x68, 0x90, 0x68, 0x79, 0x25, 0x7B, 0x1E, 0xC9, + 0x23, 0xA5, 0x22, 0xA5, 0xA5, 0xB3, 0xD5, 0x6C, 0xBC, 0x62, 0x0A, 0xBB, 0x9D, 0xE7, 0xC9, 0xFD, + 0xC2, 0xE8, 0xDD, 0x38, 0xF8, 0x73, 0x74, 0xD4, 0x88, 0x83, 0xDF, 0x3F, 0x5D, 0x05, 0x5A, 0xDA, + 0x07, 0x0A, 0x7B, 0x41, 0x1A, 0x8B, 0x96, 0xFC, 0x40, 0x9C, 0x1A, 0x4F, 0xE1, 0x90, 0x31, 0x7E, + 0x65, 0x0B, 0x0F, 0xE9, 0x04, 0x77, 0x5C, 0x0A, 0x3B, 0x0E, 0x2C, 0xA5, 0xF4, 0x8E, 0x4B, 0xB7, + 0xB2, 0x4E, 0x5A, 0xF2, 0xD2, 0x12, 0xF6, 0xFB, 0xCD, 0xF4, 0x18, 0x28, 0x42, 0xB2, 0x70, 0x7F, + 0x45, 0x34, 0xCB, 0xCB, 0x0A, 0xF9, 0x6A, 0x0A, 0x2C, 0xA5, 0x74, 0x0C, 0x8B, 0x0B, 0xB8, 0x85, + 0x6E, 0x25, 0x6A, 0x58, 0x46, 0x09, 0x53, 0xA8, 0xE6, 0xE2, 0xC2, 0xB3, 0x16, 0xAD, 0xD1, 0xB8, + 0xB8, 0x81, 0x21, 0x45, 0x53, 0x99, 0x27, 0x37, 0x88, 0xCB, 0xDA, 0xC9, 0xB4, 0xAE, 0x59, 0xDB, + 0x23, 0xF1, 0x1D, 0x4A, 0xF3, 0x77, 0xE2, 0x29, 0x3A, 0x99, 0x18, 0x81, 0xEA, 0x36, 0x18, 0x49, + 0x6D, 0xB3, 0xFE, 0x8E, 0x5A, 0x0F, 0xC5, 0xAC, 0x38, 0x38, 0xE4, 0x19, 0x55, 0x45, 0x4A, 0x2B, + 0x57, 0x55, 0xB2, 0x2B, 0xEA, 0xC3, 0xE8, 0x2E, 0x93, 0xC5, 0x8C, 0xF1, 0x9A, 0x39, 0xEE, 0x5B, + 0x1E, 0xC9, 0xA9, 0xE1, 0xB6, 0x0E, 0x25, 0x7B, 0x2A, 0x36, 0x93, 0x10, 0xFC, 0x8A, 0x5F, 0x35, + 0xD9, 0x3D, 0xF0, 0x28, 0x56, 0xD2, 0xBB, 0xEC, 0xDD, 0x2A, 0x70, 0xFD, 0x9F, 0xD9, 0xA7, 0x58, + 0x1E, 0x0B, 0x71, 0x29, 0x3F, 0x08, 0x2B, 0xA4, 0xDF, 0x16, 0xF1, 0x22, 0xC7, 0x29, 0x9F, 0x85, + 0x1D, 0x75, 0x8E, 0xA6, 0xB6, 0xA2, 0xBE, 0xB3, 0x5B, 0xE6, 0x05, 0x11, 0x0C, 0xA7, 0x5A, 0xA1, + 0xED, 0xB5, 0x6B, 0x1B, 0x09, 0x21, 0x55, 0x09, 0xEB, 0x94, 0xAE, 0x10, 0x34, 0x18, 0xF0, 0xA5, + 0xFC, 0x95, 0xC0, 0xB4, 0xB3, 0x82, 0x8E, 0xEA, 0x7C, 0xBF, 0x81, 0x39, 0x00, 0x1B, 0x54, 0x83, + 0x5F, 0xA4, 0xF9, 0x10, 0x73, 0xD3, 0xBA, 0xE4, 0x2D, 0x24, 0xC8, 0x82, 0x95, 0x69, 0xE4, 0x6B, + 0x53, 0xE8, 0x37, 0xF5, 0x5E, 0x02, 0x85, 0x91, 0x70, 0x98, 0x34, 0x9C, 0x82, 0x4D, 0x83, 0x24, + 0x92, 0xDA, 0xC3, 0x29, 0x3B, 0x99, 0x40, 0x19, 0x33, 0x68, 0xC1, 0x74, 0x28, 0x36, 0xB5, 0x24, + 0x2F, 0xDA, 0x4D, 0x61, 0x38, 0x85, 0xC0, 0x94, 0xAA, 0xD0, 0xD7, 0xE5, 0xDF, 0x47, 0xBC, 0xC4, + 0xBB, 0x7F, 0xE2, 0x55, 0xD1, 0x69, 0xC1, 0xEF, 0x87, 0x14, 0x6D, 0x78, 0x37, 0x3C, 0xBB, 0xC3, + 0x80, 0x1A, 0x7A, 0xA8, 0x65, 0xBA, 0xDD, 0xDC, 0x63, 0x37, 0xB0, 0xC0, 0xCA, 0x5A, 0x11, 0x3C, + 0x72, 0x80, 0x29, 0xC1, 0x93, 0xC4, 0xA8, 0xB1, 0xE1, 0xCA, 0xE2, 0xC0, 0x1D, 0x35, 0xD1, 0x1D, + 0x49, 0xC8, 0x02, 0xF0, 0xD6, 0x84, 0x7D, 0x1B, 0xAC, 0x86, 0x19, 0x70, 0x8B, 0x15, 0x47, 0xA0, + 0x4C, 0xA2, 0x69, 0xB8, 0x09, 0xF4, 0xB0, 0x58, 0xE4, 0x66, 0xE6, 0x4C, 0xB0, 0x06, 0x9D, 0x45, + 0x6E, 0x6C, 0xAC, 0x01, 0x96, 0x9F, 0x14, 0x2C, 0xEB, 0x80, 0xBD, 0x15, 0xE2, 0xFC, 0x3A, 0x54, + 0x83, 0x00, 0x92, 0x6B, 0x33, 0x47, 0xDE, 0xD4, 0xED, 0x45, 0x01, 0x56, 0xA3, 0xDE, 0x41, 0xE3, + 0x3D, 0xAD, 0x68, 0x5C, 0x40, 0x27, 0xF9, 0x3C, 0xBF, 0x58, 0x0D, 0x46, 0x9F, 0x03, 0xE4, 0x5C, + 0x85, 0x25, 0x44, 0x5D, 0x2D, 0x38, 0x97, 0xAC, 0xCC, 0xA4, 0x2A, 0xEA, 0x32, 0x50, 0xDE, 0x33, + 0xC8, 0xB3, 0xCC, 0xDE, 0x33, 0x14, 0xF6, 0x0C, 0x6D, 0xD8, 0x33, 0xE2, 0x74, 0xF4, 0xE3, 0xB7, + 0x6F, 0xC2, 0xAC, 0x32, 0x61, 0x44, 0x8B, 0xE7, 0xD9, 0x98, 0xE1, 0x7D, 0x74, 0x47, 0xC8, 0xC5, + 0x8F, 0x61, 0x5F, 0x85, 0x11, 0x14, 0x14, 0x80, 0xB0, 0xA2, 0x0D, 0xA7, 0x5A, 0x55, 0x58, 0xEE, + 0x6C, 0x96, 0xEA, 0x4E, 0x32, 0xDF, 0x1A, 0x0A, 0x5A, 0xCB, 0xDD, 0xB8, 0x73, 0x10, 0x1A, 0x1B, + 0x5B, 0x69, 0xA4, 0x8F, 0xFF, 0x0C, 0xDC, 0xCA, 0xB3, 0xAD, 0xB4, 0x93, 0xD9, 0x43, 0xBC, 0x5E, + 0xF7, 0x3C, 0x9B, 0x73, 0xAA, 0x63, 0xAB, 0xFA, 0x20, 0x06, 0x9D, 0xFE, 0x23, 0x4E, 0x78, 0x77, + 0x0D, 0x91, 0xD0, 0xE4, 0x62, 0xC2, 0xF3, 0xF6, 0xF0, 0x66, 0x34, 0x6D, 0xDA, 0xC3, 0x8D, 0xCB, + 0x13, 0x04, 0x02, 0x3A, 0xC9, 0x7F, 0x63, 0xCB, 0xF2, 0xA6, 0x3D, 0xC6, 0x17, 0xDD, 0xC9, 0xA5, + 0x0D, 0x28, 0x98, 0xED, 0xE6, 0x1D, 0x98, 0x88, 0xEB, 0x60, 0xBD, 0x47, 0xBB, 0xC1, 0x1A, 0x99, + 0x5A, 0xFE, 0x78, 0x9B, 0x6B, 0x38, 0xB5, 0xBC, 0x06, 0x0C, 0xF6, 0xB4, 0x6E, 0x34, 0x69, 0xA8, + 0x63, 0x96, 0x49, 0xAF, 0x8F, 0x6D, 0x31, 0x93, 0x84, 0x50, 0x99, 0x24, 0xF4, 0x39, 0x4A, 0x3B, + 0x14, 0xF7, 0x63, 0x25, 0xB2, 0x2A, 0xC1, 0x67, 0xAD, 0xD0, 0x83, 0x1A, 0x70, 0x53, 0x92, 0xF6, + 0x0A, 0xEE, 0x0E, 0xF7, 0xDC, 0xE1, 0x9A, 0x49, 0xCF, 0x1E, 0x30, 0x51, 0x7B, 0xBC, 0xBD, 0x23, + 0x9A, 0xB1, 0x3C, 0x62, 0x8D, 0xBA, 0x69, 0xB6, 0xA9, 0xEC, 0xD9, 0x5A, 0x67, 0x20, 0xA1, 0xB1, + 0xD0, 0xCB, 0x83, 0x12, 0x5F, 0x1C, 0x0C, 0x4F, 0xB9, 0x96, 0x8C, 0x54, 0xB1, 0xB2, 0xE2, 0x6F, + 0x06, 0xFD, 0xD0, 0x80, 0xD1, 0x6B, 0xF2, 0x61, 0xF3, 0xD4, 0xD0, 0x06, 0xE4, 0x4F, 0x67, 0x5F, + 0x33, 0xD4, 0xD2, 0x83, 0xD8, 0xC7, 0xD3, 0x86, 0xB8, 0x74, 0x90, 0xB4, 0xCA, 0xE1, 0x79, 0xF4, + 0x5A, 0x51, 0x49, 0x09, 0x92, 0x46, 0x56, 0xD6, 0x66, 0x05, 0xFF, 0x6C, 0x06, 0x73, 0x9D, 0x88, + 0x05, 0x90, 0x82, 0x68, 0xD7, 0x53, 0x8D, 0xA0, 0x24, 0x60, 0x2A, 0x17, 0xBB, 0xB9, 0x01, 0xAF, + 0x76, 0x68, 0x10, 0x56, 0x4E, 0x9A, 0x0E, 0x35, 0x6C, 0x48, 0xCD, 0x6C, 0xFD, 0x9E, 0x93, 0x65, + 0xD6, 0x40, 0xB4, 0x18, 0x70, 0xD5, 0xD5, 0x11, 0x7E, 0x95, 0x4F, 0xA2, 0x71, 0x47, 0xEC, 0x36, + 0x2C, 0xA3, 0xF2, 0xCF, 0x85, 0xEE, 0xBE, 0x91, 0x58, 0xD3, 0xFA, 0xB6, 0xE1, 0xF6, 0x67, 0x76, + 0xCF, 0x69, 0x01, 0x99, 0x4F, 0xAA, 0x89, 0x02, 0xCB, 0x87, 0xA9, 0x0A, 0x40, 0xB0, 0xB2, 0xC2, + 0xCC, 0x65, 0x66, 0x1D, 0x6F, 0x16, 0x13, 0xE4, 0x01, 0x94, 0x59, 0x7F, 0x8A, 0x77, 0x02, 0x9D, + 0x2F, 0x04, 0x65, 0xD6, 0x7F, 0x4E, 0xD2, 0x1F, 0x8C, 0x1E, 0x62, 0x46, 0x20, 0x5C, 0xE8, 0xEA, + 0xF2, 0xD1, 0x46, 0xA9, 0x25, 0xB8, 0x95, 0x2F, 0x2E, 0xB5, 0x4C, 0xA5, 0xD4, 0x12, 0xEB, 0xC0, + 0x35, 0x48, 0x3A, 0x70, 0x24, 0xBF, 0x9D, 0xA0, 0xAD, 0xCF, 0x85, 0xA4, 0x2D, 0xF1, 0x48, 0xDA, + 0x2E, 0x79, 0x55, 0xD4, 0x76, 0xC2, 0x1D, 0x5D, 0x79, 0xB0, 0x2D, 0x4C, 0x8B, 0x5A, 0xBD, 0x71, + 0x8E, 0xBB, 0xEC, 0xD4, 0xD1, 0x7A, 0x7B, 0xDC, 0x02, 0x95, 0x68, 0xD9, 0x2C, 0xEA, 0x3C, 0xC9, + 0x29, 0x49, 0x17, 0xBE, 0xAC, 0xE3, 0xC6, 0xC5, 0x1D, 0xFB, 0x08, 0x34, 0xEA, 0x6A, 0x37, 0x92, + 0x36, 0x13, 0x69, 0xA8, 0x70, 0x6F, 0x92, 0xD4, 0xE1, 0x92, 0x4F, 0x71, 0xFF, 0x6A, 0xE1, 0x57, + 0xEC, 0x29, 0xAA, 0x33, 0x1D, 0x29, 0x1D, 0x14, 0xD4, 0xE9, 0x8B, 0x88, 0xE9, 0xB0, 0xE9, 0x6B, + 0x4E, 0xAE, 0x9A, 0x04, 0x75, 0xFB, 0x40, 0xE2, 0xFA, 0x10, 0xC6, 0x40, 0x78, 0x05, 0x90, 0x5D, + 0x29, 0xA8, 0xB3, 0x09, 0x77, 0x52, 0x50, 0xB7, 0xCB, 0xC9, 0x36, 0x77, 0x5C, 0x40, 0x0E, 0x94, + 0xDC, 0x6E, 0x1F, 0xE4, 0x76, 0x87, 0x3C, 0x2E, 0x45, 0x20, 0x08, 0x9E, 0xFD, 0x6D, 0xF2, 0xDD, + 0xC6, 0xE4, 0x4F, 0x7F, 0x85, 0x9B, 0xD6, 0x07, 0x10, 0xC4, 0x95, 0x05, 0x6D, 0x77, 0x55, 0x41, + 0x9B, 0x48, 0x2A, 0x8B, 0xEA, 0xEE, 0xAA, 0xA2, 0xBA, 0x3B, 0x25, 0x88, 0x33, 0xF2, 0xB7, 0xBB, + 0x9A, 0x64, 0xCE, 0xE6, 0xAC, 0x6E, 0x73, 0xED, 0x83, 0x61, 0xB2, 0xAD, 0x33, 0x86, 0x2D, 0x70, + 0x76, 0xE4, 0x66, 0x8F, 0x47, 0x2A, 0x13, 0x5C, 0x08, 0x79, 0x6C, 0xBD, 0xD2, 0xC8, 0x2B, 0x23, + 0xDC, 0x7B, 0xAF, 0x00, 0x05, 0x45, 0xC9, 0x4B, 0xF5, 0x5B, 0x79, 0x2A, 0x1C, 0xAB, 0x4F, 0xE9, + 0xA0, 0xF0, 0xB9, 0x04, 0xD1, 0x35, 0x40, 0xAE, 0xB1, 0x76, 0x68, 0x78, 0x01, 0x19, 0xA3, 0x4C, + 0x85, 0xF0, 0x28, 0x38, 0xB0, 0x68, 0xBD, 0x80, 0x9C, 0x55, 0xE4, 0x7A, 0xB6, 0x2E, 0x9E, 0x30, + 0x6F, 0x11, 0xE8, 0x3A, 0x61, 0x09, 0x78, 0xDC, 0x5B, 0x70, 0x50, 0x51, 0x69, 0xB6, 0x03, 0xF0, + 0x55, 0xC1, 0x34, 0x30, 0x3D, 0xBF, 0x4A, 0xF2, 0x35, 0xF8, 0x07, 0x7C, 0x56, 0x6A, 0x85, 0x96, + 0xCC, 0xA7, 0x70, 0x46, 0x10, 0x0E, 0x9A, 0x2D, 0x3D, 0xD1, 0x48, 0x7C, 0xB6, 0xCD, 0x27, 0xB8, + 0xBA, 0x40, 0x03, 0x3C, 0x1B, 0xE1, 0x38, 0xD1, 0x43, 0xFF, 0xB9, 0xFA, 0x44, 0xBF, 0x12, 0xF2, + 0x06, 0xB1, 0x24, 0x03, 0x9F, 0xCF, 0x5B, 0x9D, 0xFD, 0xCE, 0xA4, 0xC8, 0x12, 0x5F, 0xE0, 0x5B, + 0xCC, 0x5A, 0x17, 0xF8, 0x85, 0x0B, 0x6F, 0x1A, 0xF3, 0xFD, 0x04, 0x10, 0x43, 0x86, 0x0A, 0x69, + 0x6F, 0x10, 0x40, 0x60, 0xE1, 0x11, 0x74, 0x85, 0xB7, 0x8B, 0x24, 0xBF, 0x6F, 0xAB, 0xDB, 0xAA, + 0x64, 0x3C, 0x02, 0x8C, 0x37, 0x42, 0xE1, 0x57, 0x8F, 0xB6, 0xF5, 0x0E, 0x23, 0xC8, 0xE2, 0x0D, + 0x13, 0x28, 0xD9, 0xBF, 0x07, 0x42, 0x07, 0x80, 0x9C, 0x92, 0x1F, 0x4C, 0x93, 0xD6, 0x89, 0x48, + 0x89, 0x86, 0xF5, 0xDE, 0x88, 0x02, 0xD1, 0x97, 0x15, 0x22, 0xDB, 0x2E, 0xEB, 0x62, 0x64, 0xDB, + 0x6D, 0x3D, 0xB3, 0x34, 0x00, 0x99, 0x03, 0xFD, 0xFF, 0xE8, 0x97, 0xC1, 0xF6, 0xE4, 0xD1, 0xEB, + 0xF7, 0x50, 0x90, 0xC1, 0xB7, 0x04, 0x67, 0xB8, 0xA0, 0x8F, 0x03, 0x2E, 0xA5, 0xCD, 0x7D, 0x0D, + 0x5F, 0x9A, 0xC2, 0x59, 0x33, 0xEF, 0x03, 0x81, 0x08, 0xAA, 0x14, 0xB8, 0x8C, 0x03, 0x51, 0x93, + 0x49, 0xE1, 0x7C, 0xFF, 0x81, 0xDE, 0x62, 0x32, 0x38, 0x3C, 0x02, 0x9D, 0x29, 0x19, 0x54, 0x9A, + 0x29, 0x2C, 0x3B, 0xC2, 0x6B, 0xAD, 0xEE, 0xC8, 0xF2, 0x96, 0xCD, 0x17, 0x15, 0x82, 0x32, 0x27, + 0x3B, 0xF3, 0x52, 0x53, 0xAF, 0xA4, 0x89, 0x36, 0x34, 0x23, 0x2C, 0x00, 0x01, 0x12, 0xC2, 0xDD, + 0x73, 0xD9, 0xB0, 0x39, 0x15, 0x35, 0xBE, 0xD3, 0xEE, 0x94, 0x58, 0x59, 0xD4, 0x07, 0x29, 0x10, + 0x20, 0x4C, 0xDD, 0xAC, 0x17, 0x94, 0x40, 0x69, 0x08, 0x58, 0x80, 0x37, 0x1E, 0xD7, 0x63, 0xEF, + 0x59, 0x1D, 0x98, 0x78, 0x76, 0x4F, 0x8B, 0x25, 0xBD, 0x37, 0x8B, 0x25, 0x15, 0xE7, 0x6D, 0xE9, + 0x48, 0xC7, 0x65, 0xB3, 0x61, 0xE9, 0x46, 0x3A, 0x09, 0x0C, 0xD6, 0x06, 0xEB, 0x51, 0xA0, 0x44, + 0x56, 0x95, 0x70, 0x16, 0x81, 0x64, 0x51, 0x6B, 0x46, 0xBA, 0x06, 0x9D, 0xBB, 0x50, 0xA3, 0x5D, + 0xE5, 0xDA, 0xEB, 0xC5, 0x90, 0x85, 0xC9, 0x1A, 0x9A, 0x98, 0xCF, 0xBC, 0x9B, 0xA2, 0x8A, 0x7F, + 0x9F, 0xD1, 0x98, 0x60, 0xE1, 0x89, 0xBB, 0xE7, 0x97, 0x2B, 0x83, 0xD7, 0xFB, 0x14, 0x39, 0xD4, + 0xF2, 0xC5, 0xF2, 0x05, 0x8F, 0xDC, 0xAD, 0x84, 0x30, 0x1E, 0x87, 0x59, 0x69, 0xB7, 0x10, 0xB3, + 0x5B, 0x24, 0x48, 0xE5, 0x8E, 0x42, 0x15, 0xD9, 0xC2, 0x8E, 0x3D, 0x42, 0xA2, 0xEB, 0x46, 0xC1, + 0x78, 0x0D, 0xBF, 0x2D, 0xD3, 0xAC, 0xCB, 0xC3, 0x38, 0xFB, 0xF7, 0x91, 0x31, 0x0C, 0x0A, 0x05, + 0xDB, 0x23, 0x7D, 0xFB, 0xA4, 0xF3, 0xA3, 0xD8, 0xE9, 0x03, 0x38, 0xB7, 0xB3, 0x51, 0x45, 0xCC, + 0x64, 0x1C, 0x96, 0xE7, 0xFA, 0x29, 0xBB, 0x92, 0x3F, 0x67, 0x7B, 0xA8, 0x9D, 0xAA, 0x77, 0x88, + 0xC8, 0xAD, 0x26, 0x5A, 0xF4, 0xAE, 0xEC, 0x80, 0x03, 0xD8, 0x02, 0x78, 0xF3, 0xF6, 0x89, 0xCA, + 0x85, 0x15, 0xBB, 0x7F, 0xC3, 0x4F, 0x26, 0x33, 0xF7, 0xBB, 0xCD, 0xAE, 0x6F, 0x60, 0x4D, 0x0C, + 0xE6, 0xEC, 0x55, 0x5A, 0xDB, 0xAB, 0xC8, 0xB3, 0x2F, 0x48, 0x41, 0x80, 0x8B, 0x63, 0x6E, 0x88, + 0x91, 0x86, 0xB0, 0x2D, 0xE1, 0x3C, 0x48, 0xB2, 0x47, 0x40, 0x12, 0xE7, 0xA6, 0x05, 0x83, 0xDF, + 0x88, 0x70, 0xAE, 0xAC, 0x34, 0x02, 0xF2, 0x6B, 0xE0, 0x88, 0x6C, 0xD9, 0xFF, 0x32, 0x1C, 0x05, + 0x9F, 0xA8, 0x00, 0xF9, 0x78, 0xA1, 0xCE, 0x2E, 0xF4, 0xE4, 0x75, 0x6E, 0xB9, 0x83, 0x0C, 0x8F, + 0x24, 0xA7, 0x34, 0x64, 0xA9, 0xF2, 0x10, 0x04, 0xC8, 0xD2, 0x15, 0xA3, 0xE2, 0x51, 0x34, 0x58, + 0x05, 0x6A, 0x09, 0xA3, 0xFA, 0x41, 0x2E, 0x54, 0x4D, 0xE3, 0x11, 0xF0, 0x74, 0xCE, 0x60, 0x17, + 0x39, 0x9C, 0xB1, 0x7A, 0xAD, 0x63, 0xA7, 0xDF, 0x02, 0x2C, 0x93, 0x31, 0x54, 0xE8, 0x99, 0x68, + 0x3F, 0x8C, 0x1E, 0xE5, 0x15, 0xE2, 0xB3, 0x5A, 0xFE, 0xA0, 0xEE, 0x64, 0x73, 0x9C, 0xEE, 0x91, + 0x24, 0x00, 0xFF, 0x87, 0x01, 0xCF, 0x92, 0x41, 0x22, 0x9B, 0x26, 0xD4, 0x2A, 0xF0, 0xA2, 0xB9, + 0x82, 0x2D, 0xE5, 0x6B, 0xA3, 0x1C, 0xE5, 0xEC, 0x7D, 0xA6, 0xA5, 0xFA, 0xA8, 0x2D, 0x97, 0xBA, + 0x43, 0xF9, 0x5D, 0x73, 0xF6, 0x81, 0x83, 0x6A, 0x6C, 0x0A, 0xCF, 0x58, 0x29, 0x19, 0xA9, 0x32, + 0x49, 0xD5, 0x9D, 0x68, 0x57, 0xFD, 0x71, 0x6C, 0x09, 0xC6, 0x8C, 0x2A, 0xA3, 0x4C, 0x55, 0x5E, + 0xF1, 0x91, 0x47, 0x26, 0xEE, 0xA4, 0x2D, 0x66, 0x04, 0x6A, 0xC3, 0xA4, 0x57, 0x19, 0x8E, 0xEE, + 0xDE, 0x05, 0x82, 0x13, 0xA1, 0x0C, 0xB6, 0xEF, 0x1B, 0x6E, 0x78, 0x32, 0xBB, 0xFD, 0xDF, 0xA3, + 0x88, 0x8D, 0xC6, 0x3F, 0x73, 0xD2, 0x40, 0x1F, 0x3E, 0x23, 0xED, 0x80, 0x12, 0xCF, 0xD1, 0xC6, + 0xBA, 0x54, 0xF1, 0x25, 0x54, 0xA4, 0xF1, 0x17, 0x5E, 0x4E, 0x3B, 0x16, 0x69, 0xBF, 0xF0, 0xF6, + 0x9C, 0xB6, 0xDE, 0x71, 0x19, 0xAB, 0xC9, 0x45, 0x61, 0xCF, 0x25, 0x74, 0x63, 0xAB, 0x31, 0x56, + 0x8A, 0xBE, 0xB1, 0x04, 0xFA, 0x24, 0xD5, 0xF0, 0x39, 0x14, 0x5C, 0xBC, 0x1F, 0x89, 0x09, 0x10, + 0xB9, 0xA9, 0x82, 0x82, 0x1B, 0x0F, 0xB5, 0xCE, 0x83, 0xC8, 0xDB, 0x40, 0x17, 0x8E, 0x56, 0x16, + 0xCF, 0x08, 0x56, 0x58, 0x48, 0xC4, 0x34, 0x9F, 0x84, 0x56, 0x5A, 0x08, 0x4B, 0x59, 0x76, 0x1E, + 0x93, 0xC9, 0x83, 0x90, 0xBF, 0xB5, 0x75, 0x6E, 0xEC, 0xE9, 0x5C, 0x60, 0x61, 0x8D, 0xF9, 0xA8, + 0xAF, 0xFB, 0xC3, 0x48, 0xE1, 0x84, 0x24, 0xDA, 0x0E, 0x9A, 0xE0, 0xF6, 0x83, 0x00, 0xC1, 0xE9, + 0x75, 0x0A, 0x5B, 0xBB, 0x2D, 0xEF, 0x38, 0xB2, 0x98, 0x85, 0x16, 0x94, 0x91, 0x57, 0x9B, 0x07, + 0xCA, 0xC4, 0x69, 0xE2, 0x9A, 0x66, 0x62, 0xD6, 0x94, 0xA8, 0x4D, 0x7D, 0x08, 0x9C, 0x88, 0x08, + 0xE0, 0xF8, 0xA0, 0xB8, 0xAA, 0x94, 0x9A, 0x00, 0x8A, 0x5E, 0xA6, 0x6B, 0x3A, 0x35, 0x10, 0x97, + 0xD7, 0xD6, 0x1A, 0x6F, 0xA9, 0x6E, 0x43, 0xC2, 0xF6, 0xCD, 0x0C, 0x55, 0x06, 0x9C, 0x3B, 0x41, + 0x6D, 0x45, 0x6C, 0x86, 0xB5, 0xBC, 0x29, 0x85, 0x96, 0xBD, 0x6F, 0x1A, 0xD2, 0x04, 0xC8, 0xBD, + 0x6B, 0x42, 0xD8, 0x13, 0xB9, 0x75, 0x29, 0x1E, 0x55, 0xD1, 0xFA, 0x22, 0x52, 0xA9, 0x4F, 0xB8, + 0x4F, 0x04, 0x47, 0x67, 0x18, 0x0E, 0xD2, 0xC5, 0x45, 0x70, 0x54, 0x1B, 0x0E, 0x52, 0x2D, 0x82, + 0x73, 0xAE, 0x2D, 0x35, 0x3F, 0x10, 0x98, 0xC7, 0x33, 0x38, 0x07, 0x94, 0x7B, 0x07, 0x40, 0x55, + 0xC9, 0x28, 0x44, 0x1A, 0x0E, 0x60, 0x32, 0xF9, 0x7B, 0x6C, 0xF2, 0x23, 0xA3, 0xFC, 0xA2, 0x0B, + 0x8B, 0xB7, 0xCE, 0x70, 0xBA, 0x10, 0x82, 0x19, 0xEE, 0x41, 0x36, 0x04, 0xF3, 0x56, 0x6A, 0xC3, + 0x30, 0x67, 0x8D, 0x94, 0x10, 0x55, 0x4F, 0xE8, 0x40, 0x31, 0x10, 0xBF, 0xEC, 0x02, 0x90, 0xA2, + 0xCA, 0x7E, 0x60, 0xFE, 0x54, 0xAC, 0xCB, 0xCA, 0x8A, 0x92, 0xD1, 0xC4, 0x7A, 0x06, 0x30, 0xA9, + 0xA2, 0x25, 0xD2, 0xA4, 0x1B, 0x03, 0xD4, 0xCF, 0x65, 0xCF, 0x10, 0x0F, 0x53, 0x92, 0x1D, 0x79, + 0x1D, 0xCB, 0x5C, 0x4E, 0x67, 0x65, 0x45, 0xDA, 0x22, 0xD4, 0xDB, 0x5B, 0xD4, 0x79, 0x66, 0x32, + 0x91, 0xD0, 0x2C, 0xD7, 0x2D, 0x41, 0x13, 0xF9, 0x2C, 0x6B, 0x10, 0x91, 0x11, 0x55, 0xDF, 0x30, + 0xAB, 0xC3, 0x1A, 0x13, 0x25, 0xEC, 0x94, 0x33, 0xCB, 0xC4, 0x00, 0x72, 0x0D, 0x9B, 0x78, 0xA3, + 0x47, 0x5C, 0x39, 0x66, 0xDE, 0x2A, 0xAE, 0xAF, 0x9B, 0xAF, 0x15, 0x08, 0xF6, 0xD9, 0xF7, 0x06, + 0x88, 0xB6, 0x38, 0x04, 0x4D, 0x07, 0x6E, 0x82, 0xC4, 0x6A, 0xED, 0x2C, 0x87, 0x96, 0xA9, 0xC4, + 0x85, 0x38, 0xB4, 0x22, 0x52, 0xB7, 0x86, 0x06, 0x84, 0x89, 0x3D, 0x11, 0xA0, 0x17, 0x51, 0x06, + 0x30, 0x3C, 0x68, 0x9A, 0xF4, 0x3A, 0x2C, 0x12, 0x22, 0xF5, 0xF2, 0x01, 0x09, 0x03, 0x30, 0x46, + 0x9B, 0x3F, 0x49, 0x89, 0x9F, 0xCF, 0xC2, 0x16, 0x5D, 0x7C, 0x16, 0x45, 0x95, 0xEE, 0x72, 0xF8, + 0xEC, 0x4F, 0x16, 0x35, 0x26, 0x20, 0xD4, 0x74, 0xB6, 0xE5, 0x77, 0x1B, 0xE5, 0xCF, 0x67, 0x7F, + 0x5B, 0x2E, 0x15, 0xF5, 0x85, 0x06, 0xFA, 0x68, 0x2E, 0x8D, 0x70, 0x12, 0x1F, 0x72, 0xD9, 0xAB, + 0xD9, 0xC5, 0xD0, 0xE0, 0x82, 0x8C, 0x97, 0xE0, 0x5B, 0xEA, 0x17, 0x87, 0x8A, 0xA0, 0x45, 0x41, + 0x7B, 0x26, 0x4B, 0x23, 0x43, 0x0C, 0xD3, 0xBA, 0x5C, 0x05, 0xA1, 0xBF, 0xCC, 0xAA, 0x33, 0x7E, + 0xF6, 0x27, 0xFB, 0xA5, 0xDA, 0xAF, 0x8A, 0xD4, 0x3D, 0xCE, 0x5D, 0x3F, 0x71, 0x19, 0xA7, 0x16, + 0xA1, 0x95, 0x2A, 0x00, 0x68, 0x2A, 0x93, 0x6A, 0x37, 0x2F, 0x0B, 0xAE, 0xCE, 0xB3, 0xBF, 0x59, + 0x98, 0xA3, 0x19, 0x5C, 0x06, 0x7E, 0x58, 0x6B, 0x6B, 0xE4, 0xBB, 0x8D, 0x72, 0x32, 0x28, 0xF9, + 0x8D, 0xAB, 0x14, 0x7C, 0xAD, 0xAE, 0x92, 0x6C, 0x6B, 0x43, 0x48, 0x19, 0xE0, 0x52, 0x9C, 0x82, + 0x86, 0x5F, 0x5F, 0xA8, 0xA7, 0x53, 0xED, 0x5C, 0xFF, 0xA7, 0xBF, 0xDA, 0x06, 0xB4, 0x71, 0x40, + 0x65, 0xB8, 0xE1, 0x99, 0xB8, 0xE9, 0xE4, 0x16, 0xE1, 0x55, 0xAC, 0xB9, 0x68, 0x2A, 0x61, 0x6C, + 0x32, 0x34, 0x05, 0x52, 0xA6, 0xCB, 0xBD, 0xDF, 0x4E, 0xA3, 0xF2, 0x84, 0xDB, 0x92, 0x96, 0xC1, + 0x6D, 0x2A, 0xFD, 0x83, 0xD4, 0xBF, 0xF4, 0xEA, 0xFA, 0x17, 0xDC, 0xB3, 0x55, 0xE1, 0x38, 0xC8, + 0x9F, 0x41, 0x70, 0xD9, 0xB0, 0x6F, 0x1A, 0x2B, 0x7D, 0x5C, 0xA0, 0x92, 0x2B, 0xA3, 0x17, 0x5E, + 0x16, 0x90, 0xEA, 0xCA, 0xDE, 0x45, 0x2B, 0xF6, 0x1C, 0xAC, 0x54, 0xB5, 0x43, 0xFC, 0x9A, 0xCB, + 0xC9, 0x8F, 0x4D, 0xAA, 0x17, 0xB9, 0x8E, 0xB5, 0xA0, 0x5F, 0xF5, 0x7E, 0xDF, 0x56, 0xFA, 0xAD, + 0x77, 0x02, 0xAC, 0x8C, 0x56, 0x82, 0xC4, 0x9E, 0xFE, 0x54, 0xA6, 0xAB, 0xBF, 0x81, 0x82, 0x2A, + 0x7D, 0x21, 0xFD, 0x0D, 0x36, 0x7D, 0x00, 0xE0, 0x6E, 0xD2, 0xDF, 0xFC, 0x0A, 0x82, 0x75, 0xE0, + 0xB8, 0xC0, 0xFD, 0x87, 0x70, 0x2A, 0x94, 0x37, 0xEA, 0x8B, 0x52, 0xA1, 0xB9, 0xE1, 0x40, 0x6D, + 0xA9, 0x54, 0xD5, 0xFC, 0x0A, 0xAA, 0x9A, 0x14, 0x58, 0xB1, 0xAB, 0xA4, 0xF7, 0x01, 0xE4, 0x8D, + 0x28, 0x72, 0x77, 0xE2, 0x75, 0x09, 0x44, 0xC7, 0x1F, 0x28, 0xFC, 0x11, 0x81, 0xB9, 0xB2, 0x52, + 0xE9, 0x5A, 0xB4, 0x2D, 0x6F, 0x64, 0x2E, 0xD9, 0x88, 0xFD, 0x16, 0x6D, 0xD9, 0x4F, 0x8C, 0x1D, + 0xEE, 0x6A, 0x84, 0x28, 0xAD, 0x68, 0x84, 0x44, 0x92, 0xD5, 0x08, 0x99, 0xCF, 0x03, 0xF9, 0x2D, + 0xF2, 0x65, 0x50, 0xC8, 0x7D, 0x48, 0x12, 0xDD, 0xA5, 0x32, 0xF1, 0x78, 0xEF, 0x64, 0xFF, 0x57, + 0x68, 0x18, 0xC2, 0xF8, 0xA5, 0x5F, 0x64, 0xD3, 0x6A, 0x45, 0xF7, 0x4F, 0x5E, 0xEF, 0x9F, 0x9C, + 0x28, 0x3D, 0x51, 0x4B, 0x05, 0xF5, 0x14, 0x05, 0x14, 0x6A, 0xEB, 0x22, 0x46, 0x57, 0x54, 0x2E, + 0x24, 0xE3, 0x74, 0x9D, 0x1D, 0x99, 0x42, 0xF2, 0xE4, 0x1D, 0x8F, 0xEA, 0x85, 0x4C, 0x5B, 0xB6, + 0x58, 0xB5, 0x35, 0xBF, 0xF2, 0x0A, 0x73, 0x5C, 0xB3, 0xF2, 0x82, 0xAA, 0x85, 0x5D, 0xEB, 0x43, + 0xBC, 0xE8, 0x6C, 0xB0, 0x26, 0xE5, 0xA3, 0x48, 0xF1, 0x21, 0x2E, 0xB3, 0xCE, 0x54, 0xA9, 0xA8, + 0xF7, 0xB9, 0xA5, 0x49, 0x31, 0xCE, 0x21, 0x73, 0x68, 0x32, 0xF5, 0xDA, 0x05, 0xA4, 0x67, 0xD2, + 0x32, 0x8C, 0x07, 0x45, 0x6E, 0xA8, 0x0E, 0xB9, 0x3C, 0xA6, 0x5A, 0xC3, 0x75, 0x8D, 0x18, 0x53, + 0xEE, 0x12, 0xB4, 0x1C, 0x36, 0x09, 0x79, 0x10, 0xA4, 0x39, 0x55, 0x75, 0x95, 0xC8, 0x45, 0x5D, + 0xD5, 0xA5, 0xC9, 0x52, 0xB3, 0x2D, 0xE5, 0xDD, 0x62, 0x3B, 0xD7, 0xE9, 0x67, 0xDA, 0x87, 0x7A, + 0x23, 0xB2, 0xA4, 0x7E, 0x4B, 0x95, 0x11, 0x7C, 0xA6, 0xC5, 0x9A, 0x48, 0x81, 0x9F, 0x05, 0x07, + 0x70, 0xDC, 0x63, 0x31, 0x30, 0xA8, 0x87, 0x6A, 0x4E, 0xC2, 0x27, 0xEA, 0xD5, 0xB1, 0x64, 0x52, + 0xC7, 0xB2, 0x98, 0x06, 0xA5, 0xAA, 0x83, 0x91, 0x73, 0xAD, 0x5D, 0xE1, 0xAF, 0x4D, 0x05, 0x0D, + 0xC5, 0x4A, 0x78, 0x12, 0x44, 0xC6, 0x72, 0xAC, 0xAB, 0xE7, 0x00, 0x20, 0x7D, 0xB5, 0x72, 0xEC, + 0xEB, 0xD3, 0x01, 0xCB, 0x72, 0xFA, 0x5C, 0xB5, 0x22, 0xFC, 0x49, 0x30, 0x73, 0x8E, 0x55, 0xBE, + 0x59, 0xD8, 0xB7, 0xE8, 0x24, 0x10, 0x6F, 0x28, 0xFD, 0x4C, 0xB6, 0x88, 0x7E, 0xC6, 0xDE, 0x71, + 0x9D, 0xD1, 0x36, 0xD8, 0x46, 0x73, 0x63, 0x1C, 0x5D, 0x67, 0xD5, 0xAC, 0x09, 0xA3, 0xD3, 0x12, + 0xB0, 0x1C, 0xDE, 0xE1, 0xCF, 0x32, 0xCE, 0xBE, 0x11, 0x1A, 0x85, 0x86, 0x59, 0x6F, 0x28, 0x55, + 0xBE, 0x61, 0xAE, 0x73, 0x2A, 0x25, 0xBD, 0x55, 0x89, 0x26, 0x98, 0x5F, 0x34, 0x1A, 0x3F, 0xB2, + 0xC8, 0x5D, 0x1D, 0xF0, 0xE0, 0x6E, 0xE2, 0xC3, 0x2B, 0xAB, 0x68, 0x22, 0x8C, 0xA1, 0x88, 0xE9, + 0x44, 0x43, 0xBE, 0xEC, 0x43, 0x5A, 0x4F, 0x4C, 0xFA, 0xEF, 0xC7, 0x05, 0xDF, 0x15, 0xD8, 0x53, + 0x2E, 0xB9, 0x57, 0xF4, 0x92, 0x91, 0x74, 0x63, 0x2D, 0x27, 0x1F, 0x23, 0x55, 0x32, 0xC9, 0x1E, + 0x51, 0x1C, 0x4E, 0xD9, 0xA1, 0x57, 0xB0, 0xA3, 0xBC, 0x31, 0x14, 0xC4, 0x0A, 0xDA, 0x40, 0x0A, + 0x16, 0x87, 0x75, 0x2B, 0x35, 0x52, 0xE4, 0xDA, 0xDE, 0x52, 0xD7, 0xEC, 0xCF, 0xD4, 0xB0, 0x14, + 0xCE, 0xE9, 0xD6, 0x0D, 0x33, 0x6F, 0x6B, 0x4B, 0x49, 0x50, 0x03, 0x7A, 0x6F, 0xA0, 0x48, 0xD0, + 0xCC, 0x1E, 0x80, 0xAC, 0xF3, 0x3D, 0xCE, 0x94, 0x54, 0x96, 0xB0, 0x37, 0x80, 0x69, 0x49, 0x76, + 0x3E, 0x03, 0x25, 0xFD, 0x81, 0xA2, 0x17, 0xC4, 0xD3, 0x2A, 0xB6, 0x2D, 0x60, 0x5B, 0xCB, 0xEA, + 0x8D, 0xB3, 0x06, 0xE5, 0x60, 0x95, 0x48, 0x28, 0x2B, 0xD6, 0x59, 0x9B, 0x43, 0x86, 0x02, 0x9E, + 0xB1, 0x43, 0x1E, 0x8F, 0x62, 0x25, 0xE6, 0xEF, 0xFA, 0x1A, 0xE7, 0xA8, 0x8E, 0x4D, 0xC8, 0x9B, + 0x65, 0xCF, 0x3E, 0xA6, 0x7E, 0xD9, 0x79, 0x0D, 0x41, 0xAB, 0xF8, 0xA7, 0x4B, 0xD5, 0x11, 0x10, + 0x38, 0x72, 0xC5, 0xF0, 0xFF, 0xF6, 0xB6, 0xAA, 0x4E, 0xF4, 0x22, 0x44, 0xA1, 0xD7, 0x78, 0x0A, + 0x85, 0xCD, 0x81, 0xD9, 0x33, 0x27, 0x30, 0xBB, 0xAD, 0x11, 0x4D, 0x9B, 0x14, 0x0F, 0xE7, 0xD2, + 0x09, 0xD0, 0x39, 0x50, 0xDC, 0xE3, 0xE4, 0xC2, 0xF7, 0xFE, 0x91, 0xB5, 0xBD, 0x54, 0x00, 0xA5, + 0x60, 0x04, 0x34, 0x7B, 0xA1, 0x66, 0xA8, 0x38, 0x38, 0x8D, 0x1E, 0x13, 0x78, 0x4B, 0x8E, 0x4B, + 0xA4, 0x2C, 0x70, 0xAE, 0xD9, 0xA4, 0xFA, 0xC9, 0x66, 0x33, 0xBD, 0x9B, 0x7F, 0x9E, 0x09, 0xAC, + 0xF7, 0x7C, 0x53, 0xC6, 0xB1, 0xFA, 0xD9, 0x8A, 0xB3, 0xBA, 0xFA, 0xA4, 0x4E, 0x73, 0x17, 0xD7, + 0x39, 0xA4, 0xD4, 0xAF, 0x45, 0xF8, 0x15, 0xB5, 0x08, 0x99, 0xD2, 0x15, 0x38, 0x54, 0xA8, 0x81, + 0xEC, 0x10, 0x36, 0x67, 0x0F, 0x37, 0x49, 0xEF, 0x57, 0x56, 0x9A, 0x72, 0xCC, 0xBB, 0x34, 0x70, + 0x37, 0x15, 0xDF, 0x10, 0xFB, 0xEA, 0x35, 0x84, 0xCB, 0xBA, 0x3C, 0x38, 0xDC, 0xDD, 0x9B, 0x4C, + 0x1C, 0x19, 0x10, 0x4A, 0x54, 0x29, 0xD8, 0x78, 0xA2, 0x20, 0xC8, 0x19, 0x86, 0xDF, 0xE9, 0x44, + 0x8B, 0x65, 0xE1, 0x5A, 0x30, 0xCC, 0x80, 0xC7, 0x74, 0xCB, 0x28, 0xCC, 0xAA, 0x4A, 0x89, 0x14, + 0x17, 0xBE, 0x80, 0x1E, 0x5A, 0x5D, 0x34, 0x96, 0x37, 0x6A, 0x74, 0xC0, 0xC2, 0xA4, 0x00, 0x21, + 0x4A, 0x39, 0xAB, 0xC2, 0x64, 0xF5, 0x41, 0xF2, 0x20, 0x11, 0xE1, 0x14, 0xF0, 0x70, 0xC3, 0x55, + 0x58, 0x96, 0x73, 0xF0, 0xCD, 0xB5, 0xC7, 0xFB, 0x17, 0x8C, 0x2B, 0x5B, 0x44, 0x8A, 0x04, 0xCC, + 0x5A, 0x30, 0xD8, 0xA3, 0x3D, 0xFA, 0x02, 0xD3, 0x74, 0x38, 0xBD, 0x2A, 0xFF, 0x71, 0xE0, 0x61, + 0x40, 0x48, 0x56, 0x46, 0x4C, 0x5A, 0x6D, 0x9C, 0xD6, 0x55, 0xFA, 0xD4, 0xCB, 0xE7, 0xC9, 0x93, + 0xB8, 0xEE, 0xF5, 0x34, 0x2D, 0xC5, 0x8F, 0xCF, 0x17, 0xA7, 0x97, 0x0A, 0xF6, 0x1D, 0x77, 0x2D, + 0x1A, 0xA8, 0x65, 0xD6, 0x40, 0x0B, 0x73, 0x45, 0x0B, 0x33, 0xB1, 0xBB, 0x1C, 0x18, 0x35, 0x69, + 0x15, 0x9B, 0xC9, 0x17, 0xF6, 0x5D, 0xFA, 0x74, 0x48, 0xB4, 0xAE, 0x23, 0x45, 0xDA, 0x02, 0xB6, + 0x1A, 0x74, 0x6E, 0xB2, 0x31, 0xBE, 0xA0, 0xEE, 0x9E, 0xD3, 0x5C, 0x89, 0x11, 0xA9, 0xD5, 0x21, + 0xAF, 0xF4, 0x92, 0xD5, 0x23, 0xB3, 0x61, 0x1E, 0x95, 0xB3, 0xDD, 0x1B, 0x42, 0xAF, 0xDB, 0xC0, + 0xB7, 0x55, 0xAE, 0x91, 0xCE, 0x4C, 0xCB, 0x52, 0x27, 0xBF, 0x88, 0x4F, 0x70, 0x74, 0x32, 0x48, + 0x40, 0xBB, 0xC2, 0xC0, 0x7A, 0xCF, 0xE1, 0x7A, 0x87, 0x38, 0x4D, 0xCB, 0x02, 0x2C, 0x3E, 0xCD, + 0xAE, 0xF6, 0x67, 0xD7, 0x13, 0x91, 0x77, 0x68, 0xEF, 0x61, 0xEA, 0x90, 0x66, 0x71, 0x9C, 0xB6, + 0x75, 0x6D, 0x31, 0x2C, 0x5B, 0x5D, 0x8C, 0xC6, 0x1E, 0xA4, 0x0D, 0x7E, 0x51, 0x33, 0x09, 0x16, + 0x03, 0xAC, 0x0D, 0xE6, 0x93, 0x20, 0x49, 0xAC, 0x04, 0x0D, 0x5A, 0x88, 0xB8, 0x49, 0x92, 0x15, + 0xCD, 0xBD, 0x7B, 0x35, 0x30, 0xAC, 0x15, 0x91, 0xBC, 0xCB, 0x36, 0x81, 0xC5, 0x94, 0x7C, 0xBB, + 0x06, 0xC7, 0x29, 0xCE, 0x29, 0x2C, 0x58, 0x84, 0x2A, 0xB5, 0x72, 0x87, 0x80, 0x85, 0xAD, 0x6F, + 0x7E, 0x00, 0xA1, 0xF5, 0x8D, 0x29, 0xBD, 0x84, 0xA3, 0xC7, 0x52, 0x86, 0xA4, 0x7B, 0x86, 0x03, + 0x62, 0x41, 0x8B, 0x4E, 0x92, 0x7D, 0x12, 0xC6, 0x77, 0xDE, 0x63, 0xF3, 0xEE, 0x44, 0xBF, 0xF2, + 0xC8, 0xEB, 0xDE, 0x50, 0xF0, 0xD1, 0x47, 0x92, 0x35, 0x6B, 0x03, 0xE9, 0xBE, 0x85, 0xFC, 0xBD, + 0x32, 0x1C, 0x03, 0xD3, 0x1E, 0xC8, 0xE2, 0xB9, 0x79, 0x32, 0x09, 0xAC, 0x70, 0x3F, 0x05, 0xD5, + 0xB1, 0x4B, 0xA7, 0x55, 0x14, 0xCE, 0xC7, 0x43, 0x8A, 0x87, 0x42, 0x3D, 0xDB, 0xC6, 0xA9, 0x43, + 0x8E, 0xBD, 0x06, 0x15, 0x75, 0x86, 0x9E, 0x66, 0xEE, 0x5A, 0x2E, 0x78, 0x25, 0x2B, 0xC3, 0xC8, + 0x7F, 0xF7, 0xE9, 0xD2, 0xA6, 0x1B, 0x7F, 0x5B, 0x53, 0xAC, 0xBA, 0x8D, 0x3B, 0x2C, 0x5F, 0x49, + 0xB5, 0x4A, 0x6B, 0x60, 0xEB, 0xD6, 0x49, 0x5B, 0xBB, 0x44, 0x35, 0x3C, 0xA7, 0xA3, 0xAE, 0x59, + 0x87, 0x40, 0xE5, 0x7C, 0x34, 0x8F, 0x12, 0xCA, 0x97, 0xE4, 0xD4, 0xC7, 0x92, 0x0C, 0x37, 0xAB, + 0x8F, 0xC5, 0xC5, 0x4F, 0x21, 0xD3, 0x4B, 0xA7, 0xDA, 0x6F, 0xC3, 0x49, 0xC4, 0x1B, 0x4E, 0xA2, + 0x54, 0x9D, 0x44, 0x1E, 0xD9, 0x43, 0xA5, 0xDD, 0x87, 0x26, 0xCC, 0xD7, 0x93, 0x94, 0x93, 0x70, + 0x4F, 0xED, 0xEA, 0xD6, 0xC2, 0x5D, 0x02, 0x2D, 0x87, 0xF2, 0xF4, 0x9D, 0x89, 0xF1, 0x8F, 0x3E, + 0x99, 0x1F, 0x0D, 0x93, 0x45, 0x4E, 0x67, 0xF1, 0x43, 0x0D, 0x99, 0x70, 0x97, 0x35, 0xAE, 0x53, + 0x5C, 0x1F, 0x63, 0x25, 0x1F, 0x69, 0xFC, 0xDE, 0xAB, 0x81, 0xB4, 0x71, 0x3B, 0x65, 0xA9, 0xCE, + 0x72, 0xC3, 0x65, 0x60, 0x65, 0x05, 0x91, 0xD7, 0x47, 0xCB, 0x47, 0x80, 0x66, 0x50, 0xE0, 0x15, + 0x58, 0x7D, 0xC7, 0x3E, 0x96, 0x5F, 0x38, 0x8D, 0x46, 0xA4, 0xB1, 0xDD, 0xE5, 0x39, 0x0D, 0x1F, + 0xC3, 0xB8, 0x66, 0xB5, 0x3C, 0xF5, 0x51, 0xE3, 0x32, 0x64, 0xE6, 0x0C, 0x1A, 0x3D, 0x8B, 0xE7, + 0x76, 0x1F, 0x04, 0xD0, 0x4D, 0x55, 0x6E, 0x54, 0x07, 0xBF, 0x43, 0x6A, 0x9A, 0x1C, 0x69, 0x3B, + 0x0D, 0xA0, 0xC0, 0x78, 0x30, 0x30, 0x9E, 0x55, 0x38, 0xD4, 0xB0, 0xCF, 0x2D, 0x15, 0x19, 0x3E, + 0x65, 0x8C, 0xCA, 0xF9, 0x7A, 0x6F, 0x54, 0x56, 0x00, 0x7A, 0xE2, 0xA4, 0x87, 0x86, 0x57, 0x68, + 0x1C, 0x6F, 0x66, 0x44, 0x2C, 0x0D, 0x23, 0x8A, 0x1E, 0x1E, 0x17, 0xDB, 0xE6, 0x16, 0xFD, 0x9B, + 0x16, 0x37, 0x4E, 0x18, 0x49, 0xE7, 0x1B, 0x6E, 0x9C, 0x6F, 0xEA, 0x3E, 0x6C, 0xD4, 0xB3, 0x18, + 0x24, 0x45, 0x2E, 0x51, 0x05, 0x94, 0x57, 0x19, 0xCA, 0xF7, 0x42, 0x46, 0xF7, 0x13, 0xE2, 0xF5, + 0x6A, 0x16, 0x61, 0xDE, 0x1A, 0xA4, 0xF4, 0xEE, 0x69, 0x0A, 0x2E, 0xF2, 0x0D, 0x38, 0x26, 0xDE, + 0x9B, 0x5B, 0x60, 0x9C, 0xB7, 0x40, 0xE6, 0x53, 0x56, 0x1F, 0xA6, 0x4C, 0xAF, 0x8F, 0x52, 0xE7, + 0x54, 0x06, 0x69, 0x53, 0xEB, 0x63, 0x5C, 0x6B, 0x1A, 0xA3, 0x12, 0xAD, 0xBA, 0x48, 0xD8, 0x04, + 0x3F, 0xA7, 0x50, 0x03, 0x2C, 0x2B, 0xA5, 0xFE, 0x13, 0xB8, 0xCE, 0xD8, 0xBE, 0xB5, 0x5E, 0xA0, + 0x1D, 0xCD, 0x69, 0xE9, 0xCD, 0xED, 0xC7, 0xE2, 0xAF, 0x41, 0xCF, 0x39, 0xF8, 0xD6, 0x88, 0x3E, + 0x4E, 0xDC, 0xC8, 0x5D, 0x5F, 0x01, 0xE2, 0xC3, 0xD5, 0x18, 0x78, 0xFB, 0x20, 0x80, 0xA5, 0x79, + 0x9C, 0xD9, 0x4E, 0x10, 0xAC, 0xAA, 0xCD, 0x41, 0x17, 0x1B, 0x6C, 0x05, 0xBF, 0x7C, 0x66, 0xFB, + 0x70, 0xA0, 0xD6, 0xF0, 0xD1, 0x2E, 0xA1, 0x9D, 0x58, 0x25, 0xD3, 0xE2, 0xD5, 0x42, 0xE8, 0x50, + 0xC3, 0x9A, 0xD9, 0x30, 0x73, 0x8A, 0x36, 0xC3, 0x8F, 0x70, 0x0F, 0x85, 0xAB, 0x21, 0xC2, 0x6C, + 0x0E, 0xB3, 0xC3, 0x4B, 0x4C, 0x65, 0x42, 0xC9, 0x0C, 0x89, 0x8B, 0x5E, 0xAE, 0xC6, 0x58, 0x08, + 0x2D, 0x11, 0xD1, 0x69, 0x4D, 0x9F, 0x9D, 0x62, 0x38, 0x5A, 0x4D, 0xE3, 0xB4, 0xAB, 0x26, 0x2F, + 0xDB, 0x95, 0xB6, 0x94, 0xD9, 0xAC, 0x67, 0x1B, 0xC9, 0x63, 0x9C, 0x5B, 0xE5, 0x02, 0xA0, 0x7C, + 0x90, 0x1A, 0xF9, 0x97, 0xCA, 0xAA, 0xBB, 0xB7, 0x5A, 0x2C, 0x80, 0xF7, 0xB1, 0xB5, 0x7B, 0xAB, + 0x35, 0xA4, 0xCB, 0x4C, 0x2C, 0xBE, 0x4A, 0xE3, 0xC2, 0x60, 0xEE, 0xBF, 0x11, 0x69, 0x4F, 0x33, + 0x82, 0x54, 0xA8, 0xA8, 0x80, 0xDF, 0x10, 0xFF, 0x42, 0xB2, 0x74, 0x65, 0xCD, 0x7E, 0x3B, 0xC3, + 0x8B, 0x94, 0x0A, 0x53, 0x8A, 0xCC, 0x63, 0x4A, 0x91, 0xD3, 0x8A, 0xB5, 0x00, 0x34, 0x41, 0x7C, + 0x86, 0x4E, 0x02, 0x7A, 0xE9, 0xDC, 0x88, 0x7E, 0xB3, 0x6C, 0x9D, 0x52, 0x13, 0x9B, 0xC6, 0x6E, + 0x6C, 0x4E, 0x4B, 0xEE, 0xA6, 0x62, 0x8D, 0x6D, 0x5E, 0x54, 0x0F, 0xD1, 0xB8, 0xAD, 0x1D, 0x6A, + 0xAC, 0x33, 0x6D, 0xB0, 0x7D, 0xBC, 0x57, 0x4B, 0x9E, 0x4C, 0x1A, 0x7C, 0x6C, 0xF3, 0x58, 0xBF, + 0x8C, 0xAA, 0x25, 0x63, 0xAE, 0x6D, 0x44, 0x83, 0x9E, 0x2D, 0xC7, 0x1A, 0xF6, 0x56, 0x5B, 0x89, + 0x01, 0x53, 0x7F, 0x27, 0x86, 0x95, 0x04, 0x37, 0xD0, 0x31, 0x9D, 0xF5, 0x68, 0x9B, 0xF4, 0xF1, + 0xC1, 0x23, 0x58, 0x5B, 0x37, 0xC4, 0x9E, 0x0A, 0x2A, 0xB3, 0x62, 0x83, 0xF1, 0x49, 0xA7, 0x2F, + 0x64, 0x83, 0x81, 0x4D, 0x33, 0x0A, 0xCA, 0xE7, 0x06, 0x1B, 0x8C, 0x7B, 0x20, 0xE0, 0x01, 0xB2, + 0xF4, 0x83, 0x1C, 0x68, 0x02, 0xDA, 0xD7, 0xF5, 0xC0, 0xC5, 0x12, 0xFE, 0xC1, 0xD0, 0x66, 0xF0, + 0x0F, 0x7C, 0xDC, 0xE2, 0x2E, 0x80, 0x9F, 0xC3, 0x8C, 0x0D, 0xC0, 0x77, 0xA0, 0x07, 0x3F, 0x41, + 0xA9, 0x00, 0xAD, 0xC0, 0x8F, 0x22, 0xC7, 0xCF, 0xCF, 0xC3, 0x94, 0x7D, 0x68, 0x8B, 0x1A, 0x17, + 0x64, 0x0F, 0xAC, 0x24, 0x82, 0x3F, 0x06, 0xED, 0xF3, 0x40, 0x90, 0x26, 0xC8, 0x86, 0x18, 0xB4, + 0xD8, 0x52, 0x1F, 0xDB, 0x80, 0x37, 0x0E, 0xE1, 0x1F, 0x20, 0x79, 0xD0, 0xE2, 0xFA, 0x3F, 0x85, + 0x60, 0xE3, 0xFC, 0x1F, 0x77, 0x6B, 0x17, 0x7F, 0x7C, 0xB2, 0x0E, 0xE1, 0xCA, 0x12, 0xA8, 0x25, + 0xEF, 0x99, 0x76, 0x04, 0x3C, 0xE5, 0x58, 0x38, 0x00, 0x59, 0x26, 0xB4, 0x8E, 0x36, 0x6F, 0x18, + 0x6C, 0xFB, 0x4A, 0xFC, 0xC9, 0xF1, 0x2F, 0x44, 0x3F, 0x90, 0xFF, 0xF4, 0x29, 0xFE, 0x0B, 0x84, + 0x11, 0xFF, 0xA1, 0xB7, 0xF8, 0xF7, 0x26, 0x17, 0x7F, 0x37, 0xC5, 0xDF, 0x3F, 0x89, 0xBF, 0xCF, + 0xC4, 0xDF, 0xEF, 0xC4, 0xDF, 0x3F, 0x8B, 0xBF, 0x7F, 0xC1, 0xBF, 0xA9, 0xF8, 0x73, 0x3B, 0x68, + 0x9F, 0xAB, 0x49, 0x25, 0x43, 0x6E, 0x7B, 0xD7, 0xE1, 0xEE, 0x54, 0xAC, 0x3B, 0x18, 0xC8, 0x50, + 0x54, 0x90, 0x5D, 0x8F, 0xC4, 0x9F, 0x5C, 0x74, 0x5F, 0x88, 0x3F, 0xB7, 0xB0, 0xD4, 0xE2, 0x07, + 0x98, 0x2D, 0x8A, 0x7F, 0xC7, 0x57, 0xF2, 0x1F, 0x51, 0x14, 0xD6, 0x12, 0x60, 0x89, 0xBF, 0xC6, + 0xE2, 0x0F, 0x16, 0x9D, 0x92, 0x1D, 0x1A, 0xAF, 0xFF, 0x33, 0xEC, 0xB6, 0xE1, 0xFF, 0xC2, 0x6B, + 0xAE, 0x3B, 0xB9, 0x4D, 0x20, 0xA8, 0x56, 0x36, 0xB9, 0xE6, 0x23, 0xB0, 0xF7, 0x1B, 0x4E, 0x20, + 0xC4, 0x16, 0x04, 0x13, 0x98, 0x9C, 0xFF, 0x73, 0xA5, 0xBD, 0xDE, 0xFD, 0xFD, 0xC5, 0x1F, 0xA1, + 0xE0, 0x39, 0xFE, 0x98, 0x3C, 0x89, 0xA2, 0xF5, 0x41, 0x4A, 0x4E, 0xB1, 0x01, 0xDC, 0x37, 0xD8, + 0x44, 0x7A, 0x9B, 0x0C, 0xE8, 0x3F, 0xD6, 0xE1, 0xD7, 0xD5, 0xED, 0x68, 0x32, 0x48, 0xAF, 0x27, + 0xEF, 0x47, 0x74, 0x00, 0x7F, 0x06, 0x93, 0x11, 0x78, 0x83, 0xF2, 0xF4, 0xFA, 0x7A, 0x72, 0x47, + 0xAF, 0x46, 0xD1, 0x04, 0x9F, 0x4D, 0xCF, 0x44, 0xC9, 0x5B, 0x2C, 0x71, 0x3B, 0xFA, 0x6E, 0x02, + 0x9B, 0x13, 0x33, 0x6F, 0xA3, 0x49, 0x32, 0xEE, 0xA7, 0x3A, 0xF3, 0x19, 0xA4, 0x27, 0x22, 0x0F, + 0x5C, 0x89, 0x0B, 0xD8, 0x9B, 0x57, 0x10, 0x83, 0xE9, 0x2F, 0xDF, 0x11, 0x7C, 0xE3, 0x7D, 0x63, + 0xED, 0xEF, 0xAB, 0xEB, 0x17, 0xAB, 0x31, 0xAE, 0x63, 0xC7, 0x18, 0x37, 0x9C, 0x50, 0x8C, 0xD7, + 0x4C, 0xA4, 0x29, 0xB2, 0x30, 0x81, 0xD3, 0x26, 0x74, 0xFA, 0x40, 0x40, 0x8A, 0x8D, 0x3B, 0x5A, + 0x57, 0x29, 0xDB, 0x2F, 0xDB, 0xC0, 0xFB, 0xE5, 0x10, 0x79, 0xE2, 0xD5, 0x5A, 0x75, 0x8F, 0xD8, + 0x3D, 0x7C, 0x7D, 0x04, 0x3C, 0x19, 0x5A, 0x6E, 0x8D, 0xF0, 0x5F, 0xA4, 0x4E, 0xF2, 0x29, 0x7A, + 0xE8, 0x37, 0x40, 0x1B, 0xC9, 0xF5, 0x1B, 0x7E, 0x8B, 0x52, 0xB3, 0x24, 0x56, 0xEF, 0xE0, 0x03, + 0xF1, 0x2C, 0x44, 0xA0, 0xE9, 0x0A, 0xE3, 0xC2, 0xE4, 0xD9, 0x56, 0x67, 0x5B, 0xFE, 0x18, 0x88, + 0x50, 0x7B, 0x3E, 0x66, 0x65, 0x08, 0xEF, 0xD3, 0x40, 0x16, 0x13, 0x7A, 0x10, 0xA4, 0x41, 0xEE, + 0x03, 0xF7, 0x38, 0x37, 0x11, 0x30, 0x3D, 0x31, 0x86, 0x82, 0x4D, 0x45, 0x23, 0xF3, 0x80, 0x7C, + 0x49, 0xBD, 0xE2, 0x1C, 0xB4, 0x0C, 0x4E, 0x2C, 0x34, 0xED, 0x4D, 0xD9, 0x98, 0x06, 0x92, 0xA6, + 0xD6, 0x66, 0x90, 0x68, 0x91, 0x21, 0xCC, 0x2F, 0xC7, 0x5C, 0xA8, 0xD0, 0x03, 0x66, 0xB5, 0x38, + 0x87, 0x59, 0x5C, 0x4C, 0x26, 0x88, 0x7A, 0xE7, 0xA9, 0xF8, 0x01, 0x11, 0x51, 0xBD, 0x6A, 0x3E, + 0x2F, 0x53, 0xC6, 0x67, 0x4E, 0x10, 0xE8, 0x30, 0xB5, 0x13, 0xD4, 0x33, 0x11, 0x13, 0xBF, 0x2F, + 0x67, 0x4C, 0x26, 0x3A, 0x14, 0x8C, 0x6C, 0xEF, 0x4D, 0x32, 0x1C, 0xE3, 0xED, 0x02, 0x5F, 0x03, + 0xD8, 0xA1, 0x90, 0x5D, 0x4F, 0x3E, 0xA5, 0xA5, 0xE8, 0x88, 0x60, 0xEE, 0xED, 0x8D, 0x32, 0x57, + 0x7F, 0x49, 0x5F, 0xB9, 0xF3, 0x0B, 0x75, 0xC7, 0x06, 0xC9, 0x8D, 0xE1, 0x26, 0x44, 0x57, 0xCA, + 0x45, 0x74, 0x25, 0x54, 0x52, 0xA8, 0x2E, 0xD0, 0xDC, 0x52, 0x8F, 0x79, 0x79, 0x43, 0x31, 0x3C, + 0x68, 0xE9, 0x0F, 0x00, 0xC8, 0xF1, 0xED, 0xD7, 0x9A, 0x38, 0xD6, 0x42, 0x03, 0x49, 0xBD, 0xEA, + 0x62, 0x58, 0xEB, 0x22, 0xCC, 0x8C, 0x39, 0xB0, 0xC4, 0x2D, 0x79, 0xEB, 0x7D, 0x79, 0xFA, 0xFA, + 0x95, 0xB0, 0x87, 0xB9, 0xA2, 0xE8, 0x96, 0x9E, 0x0D, 0x79, 0x0A, 0xC6, 0x41, 0xFB, 0xD2, 0xFE, + 0xCD, 0x7C, 0xEF, 0x4A, 0x0B, 0xB8, 0x7D, 0x4A, 0xEE, 0x94, 0x05, 0xDC, 0x15, 0xBC, 0xA9, 0xB7, + 0x2D, 0x4A, 0xAD, 0x99, 0x52, 0x07, 0xD4, 0x09, 0x5E, 0x10, 0xFE, 0x73, 0xF2, 0x8F, 0x7F, 0x14, + 0x51, 0xB0, 0xBA, 0x0D, 0xF6, 0x58, 0xFF, 0xF8, 0xC7, 0xC9, 0x2A, 0xD0, 0xAB, 0x01, 0xE0, 0xFE, + 0x21, 0xD2, 0xF8, 0x22, 0x61, 0x20, 0x8B, 0xF9, 0x22, 0x68, 0xD9, 0x0D, 0x50, 0x75, 0x14, 0x34, + 0xC2, 0x6F, 0x9D, 0xFC, 0x82, 0x01, 0x4D, 0xFB, 0x00, 0xA4, 0x1B, 0xBE, 0x6F, 0x85, 0x2C, 0xA8, + 0xFC, 0xE2, 0x24, 0xBD, 0x1D, 0xA1, 0x44, 0xC7, 0x78, 0x8C, 0x13, 0x24, 0x8B, 0x75, 0xDF, 0xF2, + 0xB2, 0xB3, 0xBA, 0x12, 0x0B, 0xDB, 0x2A, 0x7D, 0x0A, 0xD2, 0x71, 0xEB, 0xDE, 0x2E, 0x99, 0x38, + 0x28, 0x88, 0x1B, 0xB4, 0xD4, 0x97, 0x0E, 0x79, 0xE7, 0x7B, 0x08, 0xD3, 0x38, 0x9A, 0x98, 0xCC, + 0x72, 0x8F, 0xF3, 0x1D, 0xE8, 0x95, 0x69, 0x3C, 0xCD, 0xEB, 0x23, 0xB7, 0x9D, 0x5C, 0xC3, 0x5E, + 0xC2, 0x03, 0xF1, 0xA8, 0xDE, 0x59, 0x22, 0x22, 0xDB, 0xCD, 0xF6, 0xBC, 0xD7, 0x00, 0x2D, 0x4D, + 0xC9, 0xC2, 0x58, 0x0C, 0x6E, 0x38, 0x2C, 0x8F, 0xC9, 0x2C, 0x46, 0x5B, 0xF3, 0xB5, 0x10, 0xC7, + 0x01, 0xD6, 0x61, 0xFB, 0xEC, 0xF4, 0xB0, 0x1D, 0x24, 0x63, 0x88, 0x5E, 0x0C, 0x0F, 0x64, 0xC2, + 0x43, 0x97, 0xC2, 0x88, 0xEC, 0x78, 0xFF, 0x87, 0x97, 0xA7, 0x6D, 0xFD, 0xA6, 0xE7, 0xF3, 0xC3, + 0xD3, 0xD3, 0xC3, 0xD7, 0x6D, 0x15, 0xBE, 0x00, 0x22, 0x3E, 0xEC, 0xBD, 0x80, 0x4C, 0x10, 0xB8, + 0x60, 0x23, 0xAF, 0x9C, 0xC5, 0x5C, 0xDE, 0xB0, 0xAB, 0xF8, 0x74, 0x0B, 0xCE, 0xC8, 0x25, 0x71, + 0x3A, 0x5B, 0x14, 0x5C, 0xC2, 0x33, 0xD9, 0x7E, 0x7E, 0x5F, 0x2E, 0x03, 0xF3, 0xCE, 0xEE, 0x20, + 0x69, 0x1D, 0xD2, 0xBE, 0xF7, 0x54, 0x5E, 0x13, 0x98, 0xAD, 0x0A, 0xA8, 0xBF, 0x4F, 0x35, 0x06, + 0xA8, 0xC7, 0x53, 0x97, 0x04, 0x77, 0x64, 0x30, 0x47, 0xE3, 0xC3, 0x86, 0x5C, 0xFF, 0xE5, 0x4D, + 0xB3, 0xF0, 0xF0, 0xDB, 0x2E, 0xB4, 0x9A, 0xB6, 0x0E, 0xB7, 0x60, 0x56, 0x10, 0x4B, 0xF9, 0x56, + 0x0A, 0x9D, 0x85, 0x82, 0xA6, 0x60, 0x0C, 0x76, 0x6D, 0x96, 0x37, 0xCA, 0x8B, 0x82, 0x4B, 0x52, + 0x5A, 0x87, 0x3D, 0xB8, 0xD0, 0xBC, 0x37, 0x96, 0x85, 0x2F, 0xE1, 0x57, 0x36, 0x86, 0xCA, 0xC7, + 0x15, 0xEB, 0xCF, 0xDD, 0xBA, 0xF5, 0xE7, 0xAE, 0x6B, 0xFD, 0xB9, 0xEB, 0x58, 0x7F, 0xCA, 0xFC, + 0xFD, 0x83, 0x93, 0xBD, 0xE3, 0xD3, 0x3D, 0x78, 0xEB, 0x14, 0xC8, 0x15, 0xC5, 0xD7, 0x98, 0x30, + 0xB9, 0x12, 0x26, 0x64, 0xB7, 0x6E, 0x25, 0x6A, 0x13, 0x0F, 0xCF, 0x4E, 0x55, 0x2A, 0x0C, 0x0B, + 0x93, 0x1B, 0x5E, 0x69, 0xC5, 0xAC, 0xC6, 0x67, 0x5A, 0x77, 0x61, 0x8E, 0x9F, 0x8D, 0x1D, 0xE5, + 0x0B, 0x33, 0xDB, 0x33, 0xA4, 0x3B, 0xEE, 0xAA, 0x92, 0xB7, 0x98, 0x26, 0x51, 0x80, 0x7C, 0x84, + 0xDF, 0xEA, 0x2D, 0xDC, 0xD7, 0xF0, 0x53, 0xAD, 0xE9, 0x4F, 0xF0, 0x53, 0x0E, 0x9D, 0xFC, 0x0C, + 0x3F, 0x6F, 0x13, 0x36, 0x46, 0xED, 0xFA, 0x73, 0xBF, 0xC5, 0x63, 0x8A, 0x47, 0xCB, 0x7F, 0x12, + 0x70, 0x40, 0x8D, 0xEF, 0x91, 0xF1, 0x06, 0xB4, 0x3C, 0x70, 0x4F, 0x7A, 0x73, 0x59, 0xC3, 0x39, + 0xAE, 0x5E, 0x58, 0xD2, 0xDF, 0x62, 0x7E, 0xE2, 0x25, 0x40, 0x29, 0x46, 0xB5, 0xAF, 0xB7, 0x9D, + 0x4A, 0x8C, 0x06, 0x2F, 0xBE, 0xA6, 0x18, 0x18, 0x15, 0x03, 0xCD, 0x39, 0xF6, 0x9C, 0x30, 0x09, + 0xC7, 0xAC, 0x84, 0x7A, 0x82, 0x5A, 0xA4, 0x3E, 0xA3, 0x49, 0xE1, 0x91, 0xE6, 0x40, 0xB7, 0x3E, + 0x3B, 0x65, 0xFF, 0x33, 0xB7, 0xE0, 0x26, 0x16, 0x94, 0xD7, 0x31, 0x95, 0x36, 0xB3, 0x78, 0x25, + 0xC1, 0x56, 0xAE, 0x7A, 0xAA, 0x54, 0x0A, 0xC2, 0x99, 0x6B, 0x95, 0x73, 0x35, 0xEF, 0x57, 0x71, + 0x15, 0x05, 0xC5, 0xB0, 0xD0, 0xD8, 0x55, 0x14, 0x4C, 0xEA, 0x2E, 0x08, 0xAA, 0x10, 0x86, 0x2F, + 0x86, 0x89, 0x23, 0xAF, 0xDA, 0x40, 0xB5, 0x0E, 0xB1, 0xA1, 0x22, 0x61, 0x49, 0x06, 0xB0, 0x96, + 0x26, 0x74, 0x02, 0x69, 0xEC, 0x80, 0x30, 0xC8, 0x65, 0x95, 0xC5, 0x6E, 0xF5, 0xA4, 0x0A, 0xCC, + 0x9F, 0x0E, 0xC5, 0x61, 0x8A, 0x6F, 0x53, 0x7E, 0xB3, 0x5D, 0xCE, 0x0C, 0xA3, 0x2E, 0x64, 0x88, + 0x6D, 0x28, 0x08, 0x3E, 0xB4, 0xDC, 0x86, 0x04, 0xB1, 0xF9, 0x74, 0x82, 0x71, 0xED, 0x90, 0x37, + 0x5E, 0xA1, 0x90, 0x49, 0x47, 0xFA, 0x6E, 0x5D, 0x0E, 0xC6, 0xF8, 0x82, 0x1A, 0xF6, 0xD0, 0xBC, + 0xA4, 0xE8, 0x34, 0x86, 0x09, 0x12, 0xC1, 0x55, 0xA7, 0x36, 0xB9, 0xD1, 0x01, 0xDD, 0x79, 0x7B, + 0xCC, 0xD9, 0x08, 0x3E, 0xCB, 0x2D, 0x6D, 0xB8, 0xD5, 0xB8, 0x70, 0x46, 0x57, 0xE7, 0x68, 0xFB, + 0x6B, 0xC5, 0xF7, 0xDE, 0x00, 0x9D, 0x72, 0xCB, 0xD7, 0x5D, 0x6B, 0xA5, 0xA6, 0x5B, 0xB6, 0x20, + 0x88, 0x6D, 0xCB, 0xB8, 0x06, 0xD8, 0x8D, 0x83, 0x92, 0x3F, 0xF5, 0xBB, 0xAE, 0x2E, 0xB6, 0xF8, + 0xAA, 0x01, 0x61, 0xA7, 0x27, 0x92, 0xEA, 0x5B, 0xDD, 0xBA, 0xD2, 0x57, 0x16, 0xBA, 0xC1, 0xA7, + 0x7E, 0x51, 0x97, 0x7A, 0x97, 0x2E, 0xD8, 0x94, 0x8A, 0xE1, 0x9B, 0x25, 0x06, 0x75, 0x83, 0xE6, + 0xAA, 0xF1, 0xA8, 0xB4, 0x30, 0x88, 0xCD, 0x7B, 0xB4, 0xEE, 0x73, 0xB4, 0xCA, 0x0E, 0x21, 0x88, + 0x4A, 0x84, 0x54, 0x11, 0xD1, 0x23, 0x58, 0x73, 0x90, 0x46, 0x8D, 0xE1, 0x3F, 0xEC, 0x61, 0x09, + 0x88, 0xB1, 0x12, 0x60, 0x2C, 0xA9, 0x36, 0x0A, 0x15, 0x98, 0x85, 0x6B, 0x63, 0xA2, 0xDA, 0x12, + 0x8A, 0x64, 0x1B, 0x02, 0x59, 0xE6, 0xCB, 0x2D, 0x00, 0x5B, 0x8C, 0x0B, 0xDC, 0x5D, 0x59, 0xA9, + 0x2C, 0x83, 0xD1, 0x73, 0xD6, 0xAC, 0x06, 0xCC, 0xD3, 0xA6, 0x97, 0x2D, 0x38, 0x08, 0xFA, 0x27, + 0x37, 0x09, 0xDC, 0xEB, 0x8E, 0x81, 0xD8, 0x3B, 0xE5, 0xF1, 0x02, 0x66, 0x3D, 0x1B, 0x75, 0x78, + 0x53, 0x14, 0xFA, 0x95, 0x4B, 0xB5, 0xB2, 0x3B, 0x20, 0x9D, 0xBB, 0x0D, 0x3A, 0x41, 0x67, 0x21, + 0xC4, 0xD0, 0x9B, 0x24, 0x3F, 0xCB, 0xA9, 0xDA, 0x6E, 0xEA, 0x9A, 0xE5, 0xDB, 0x9E, 0x44, 0xE9, + 0x50, 0xCF, 0xF6, 0x77, 0xEB, 0x20, 0x3A, 0xD8, 0x7E, 0xBD, 0x07, 0x17, 0x2C, 0xD7, 0xEA, 0x43, + 0x08, 0x40, 0x72, 0x85, 0x21, 0x33, 0xEC, 0x42, 0x50, 0xC4, 0x02, 0x09, 0xB4, 0x7F, 0x75, 0x6F, + 0xCA, 0x63, 0x39, 0x03, 0xDD, 0x32, 0xFE, 0xB4, 0x0C, 0x87, 0x87, 0xFB, 0x21, 0x2B, 0x29, 0x1A, + 0x3E, 0x2B, 0xD5, 0x71, 0x11, 0x7B, 0xEE, 0xD6, 0xE5, 0x16, 0x0C, 0xAF, 0xD5, 0xF5, 0xA6, 0xDA, + 0x00, 0xE0, 0x24, 0x73, 0x41, 0xD8, 0xF6, 0x96, 0xB7, 0x2F, 0x3B, 0x89, 0xA9, 0xC1, 0xFD, 0x52, + 0x00, 0x4C, 0xD1, 0x27, 0x1C, 0x9F, 0x4D, 0x55, 0x81, 0x22, 0xE5, 0x40, 0x87, 0xA5, 0x63, 0x52, + 0x73, 0x79, 0x42, 0x78, 0x99, 0x29, 0xFA, 0xDC, 0x7C, 0x68, 0x48, 0xA7, 0xA8, 0x12, 0x82, 0x3C, + 0x16, 0x2B, 0x60, 0xEF, 0x89, 0x60, 0x2F, 0x99, 0x35, 0xB1, 0x40, 0x57, 0xD8, 0x06, 0xA4, 0x6D, + 0xDA, 0x13, 0x9A, 0xB7, 0xF3, 0x85, 0xF5, 0x71, 0x5A, 0x02, 0x48, 0x96, 0x9C, 0xE6, 0x93, 0x47, + 0x78, 0xC8, 0x5F, 0xD1, 0x1B, 0x78, 0xA4, 0x3B, 0xCB, 0x1D, 0xD8, 0xD7, 0xF8, 0xE0, 0x29, 0x11, + 0xEC, 0x5A, 0xFB, 0x41, 0xF5, 0xD7, 0x7E, 0x4B, 0x17, 0xF7, 0xA0, 0xAF, 0x39, 0xD0, 0x93, 0x8C, + 0xED, 0x08, 0xDD, 0x43, 0xBB, 0x7C, 0xC2, 0xDB, 0x97, 0x32, 0x4D, 0xB7, 0x48, 0x23, 0x0D, 0x22, + 0x48, 0x93, 0x38, 0x69, 0x77, 0x25, 0xF9, 0x33, 0x53, 0x6E, 0x07, 0x52, 0x07, 0x28, 0xD5, 0xC1, + 0xA6, 0xA5, 0x4D, 0x56, 0xDB, 0x73, 0xA3, 0x9F, 0x5F, 0x1D, 0x2E, 0xF8, 0x1A, 0xE9, 0xCD, 0x49, + 0x49, 0xBE, 0x95, 0x9B, 0x72, 0x73, 0xF0, 0x23, 0xF9, 0x4A, 0x88, 0x63, 0x92, 0x51, 0xDB, 0x8E, + 0x62, 0xF6, 0xE0, 0x17, 0x64, 0x8D, 0x20, 0x42, 0x4D, 0x59, 0x69, 0xF9, 0xDC, 0xE9, 0xD0, 0xDA, + 0x29, 0x24, 0x8C, 0x2B, 0xEB, 0xD1, 0xB1, 0x1B, 0x88, 0x30, 0x18, 0x5A, 0xA3, 0x51, 0xCE, 0x4B, + 0x0A, 0x3D, 0x3A, 0x4C, 0x01, 0xD5, 0xE6, 0x20, 0x1A, 0xC3, 0xCB, 0xA1, 0x30, 0xB4, 0x25, 0xC8, + 0xCD, 0xC2, 0x96, 0x20, 0xD8, 0x40, 0xC7, 0x34, 0xD6, 0x64, 0xFD, 0xD1, 0x6B, 0xB0, 0xFE, 0xB8, + 0x51, 0x8A, 0x90, 0x5E, 0x38, 0xD7, 0x3D, 0x82, 0x30, 0x3F, 0xC1, 0x4D, 0xE7, 0x1D, 0x49, 0x78, + 0x39, 0xAB, 0x1B, 0xB8, 0x5A, 0xE0, 0x02, 0x7A, 0xBE, 0xC7, 0xD0, 0xA7, 0x8E, 0x69, 0xF3, 0x2C, + 0x79, 0x1B, 0x95, 0x0E, 0x9E, 0x0C, 0x86, 0x21, 0xC1, 0x86, 0x49, 0xAE, 0x91, 0xDE, 0x2C, 0xE2, + 0x1D, 0x2D, 0xBE, 0x90, 0xF2, 0x22, 0x69, 0x82, 0xE8, 0x50, 0xCB, 0x66, 0xD0, 0x2A, 0x8F, 0xE1, + 0x35, 0xF6, 0x79, 0xD4, 0xFB, 0x03, 0xA2, 0x9A, 0x51, 0x8F, 0x7E, 0xFB, 0x8D, 0x33, 0xC3, 0xBF, + 0xDF, 0x77, 0x9B, 0x3A, 0xFF, 0x89, 0x5E, 0xC0, 0x2D, 0xC4, 0x9B, 0xF5, 0xBA, 0x39, 0xEB, 0xA3, + 0xCC, 0xF2, 0xA3, 0xF7, 0xA3, 0x0C, 0x9D, 0x98, 0x88, 0x0F, 0xF5, 0xF5, 0xE6, 0x4C, 0x1D, 0xCF, + 0xB5, 0x71, 0xEA, 0x0F, 0xA4, 0xF4, 0x75, 0x41, 0x92, 0xCA, 0xDC, 0x95, 0x4F, 0xD1, 0xA3, 0xC5, + 0xAA, 0x76, 0xDB, 0xF0, 0x21, 0x54, 0x15, 0x75, 0xEB, 0xE7, 0xAD, 0xB3, 0xED, 0x1A, 0xEE, 0x1E, + 0x96, 0xA2, 0xA2, 0x28, 0x71, 0x0D, 0x5C, 0x71, 0x45, 0x63, 0xB6, 0x98, 0x7F, 0x20, 0x86, 0x8F, + 0xD5, 0x3F, 0xF0, 0x4C, 0x2D, 0x1F, 0x2A, 0x4A, 0x04, 0x25, 0x9E, 0xFD, 0x32, 0x27, 0x2F, 0xB6, + 0x6C, 0xF9, 0x9B, 0xBA, 0x06, 0xDC, 0x37, 0xC2, 0x8E, 0xE6, 0x8A, 0x64, 0x8A, 0xAE, 0x0C, 0x88, + 0xED, 0x0B, 0x18, 0x82, 0x26, 0x3C, 0x55, 0xE0, 0x20, 0xE6, 0xB8, 0x7B, 0xE0, 0x33, 0x15, 0x9E, + 0xDE, 0x2F, 0x84, 0x85, 0x46, 0xBD, 0xED, 0x8A, 0x06, 0x40, 0x4B, 0xEB, 0x96, 0x8D, 0xBE, 0x02, + 0x18, 0x47, 0xEB, 0x11, 0x01, 0x36, 0x60, 0x3A, 0x7C, 0xA0, 0xC3, 0x57, 0xA1, 0xB4, 0xAB, 0xEB, + 0xC0, 0x44, 0x0B, 0xA2, 0x84, 0x1A, 0x15, 0x14, 0x26, 0xD4, 0xE1, 0xEF, 0x8C, 0x58, 0xCA, 0x49, + 0xB5, 0xC2, 0x2B, 0x9C, 0x99, 0x68, 0x34, 0xA4, 0x22, 0x42, 0x12, 0xEA, 0x3C, 0xE0, 0x67, 0xBB, + 0xD6, 0x27, 0x4A, 0x1D, 0x14, 0xAD, 0x0B, 0x85, 0xF2, 0x92, 0x0B, 0x89, 0x3B, 0x2C, 0x08, 0xBF, + 0x0F, 0x35, 0xDB, 0x03, 0x65, 0x74, 0x1B, 0x58, 0x5E, 0xFC, 0x92, 0x88, 0x64, 0x40, 0x57, 0x5F, + 0x9F, 0x12, 0x05, 0xF2, 0xBE, 0xE4, 0xA2, 0xF9, 0x84, 0x35, 0xA9, 0x4B, 0x33, 0x52, 0x71, 0xA1, + 0xA8, 0x9F, 0xC7, 0x9D, 0x8A, 0x3A, 0xDD, 0x5A, 0x8A, 0xE5, 0x4A, 0x2D, 0x3B, 0x5A, 0x2B, 0x04, + 0xA0, 0xF9, 0x56, 0x61, 0x99, 0xFE, 0xE3, 0xA8, 0x4C, 0xCD, 0x41, 0x99, 0xE6, 0xC4, 0x64, 0xAA, + 0x87, 0x64, 0x72, 0x59, 0xE4, 0xFA, 0x36, 0x5C, 0x06, 0xCD, 0x8B, 0x53, 0xD7, 0x08, 0x4D, 0xBB, + 0x0E, 0xD5, 0xAE, 0xC6, 0xD1, 0xAC, 0x97, 0x8F, 0xBA, 0x83, 0x86, 0x8C, 0x76, 0xC9, 0xA6, 0x40, + 0x5C, 0xDC, 0x1A, 0xCA, 0xE9, 0xE1, 0x5A, 0x1A, 0xE4, 0x83, 0xDB, 0x11, 0x3D, 0xE7, 0x95, 0x97, + 0xD9, 0x95, 0xA1, 0x5F, 0x73, 0x5C, 0xB8, 0x54, 0x5E, 0x87, 0x9D, 0x45, 0x57, 0x92, 0x19, 0xB8, + 0xFF, 0xA6, 0x3C, 0x84, 0xBD, 0x1C, 0x79, 0x55, 0x5C, 0xE9, 0x75, 0x39, 0x02, 0x4F, 0x34, 0x80, + 0x77, 0x01, 0xCB, 0x76, 0xD3, 0x69, 0xED, 0x7C, 0xD6, 0x61, 0xE6, 0x52, 0xDD, 0x91, 0x16, 0x55, + 0x7B, 0x1F, 0xCB, 0x96, 0xE2, 0x30, 0xC1, 0x9F, 0xDA, 0x67, 0x1D, 0x91, 0x37, 0xFE, 0x99, 0x2A, + 0xAC, 0x93, 0xEF, 0x3B, 0xD3, 0xAE, 0xAF, 0xA7, 0x92, 0x2C, 0xD7, 0x93, 0x6D, 0xDC, 0x51, 0xD8, + 0xDC, 0x36, 0xA4, 0xD0, 0xD7, 0x97, 0xAD, 0xA5, 0xC8, 0x9D, 0xEA, 0xCC, 0xE9, 0xA2, 0x33, 0x94, + 0x42, 0x26, 0x9C, 0xA1, 0xA8, 0xC7, 0x16, 0xAE, 0x27, 0xD8, 0x53, 0x51, 0x6F, 0x3A, 0x5D, 0x48, + 0x02, 0xC4, 0xAA, 0x02, 0xA0, 0xB2, 0x19, 0x86, 0x19, 0x3D, 0x84, 0xB7, 0x17, 0x1C, 0xA5, 0x79, + 0x86, 0xBE, 0x3A, 0x9C, 0x32, 0x01, 0x51, 0x7B, 0xD3, 0x26, 0x90, 0x07, 0xAD, 0xA9, 0x30, 0x92, + 0x6B, 0xAB, 0x84, 0x0A, 0x94, 0x5F, 0x8B, 0x60, 0xE6, 0xD5, 0xD9, 0x01, 0xB5, 0xCD, 0x67, 0x9D, + 0x1E, 0x6A, 0xBA, 0xF1, 0x68, 0xB2, 0x18, 0xCE, 0xA8, 0xA1, 0xCA, 0x4C, 0x26, 0x5A, 0x9D, 0x06, + 0xF8, 0x24, 0x9C, 0x98, 0x5D, 0x69, 0xC2, 0xDC, 0x5E, 0xC8, 0x42, 0x7D, 0x04, 0x33, 0xA5, 0x14, + 0xAA, 0x25, 0x28, 0x85, 0x90, 0x50, 0xC8, 0xE0, 0xD5, 0x8F, 0x37, 0xDE, 0xD8, 0xF1, 0x4D, 0x7C, + 0x38, 0x2F, 0x9B, 0x24, 0xB1, 0x70, 0xAA, 0xE1, 0x93, 0x03, 0xBF, 0x99, 0xA4, 0x97, 0x89, 0x47, + 0x0B, 0xC4, 0xB3, 0x60, 0x55, 0xB6, 0x52, 0xEB, 0x5B, 0x6C, 0x84, 0xAD, 0xEE, 0x6B, 0xDA, 0x16, + 0xCC, 0xE6, 0x86, 0x60, 0xE8, 0x67, 0xCA, 0x69, 0x61, 0xD8, 0xEE, 0x7D, 0x0E, 0xAF, 0x1C, 0x5D, + 0x27, 0x09, 0x12, 0xDA, 0xA1, 0x23, 0x7C, 0xA5, 0x25, 0xC9, 0x6B, 0xB5, 0xA8, 0x3D, 0x39, 0x84, + 0xBA, 0x6C, 0x65, 0xC5, 0xFD, 0x16, 0x82, 0xC2, 0xAE, 0x6D, 0x20, 0xF6, 0xBF, 0xFC, 0x5B, 0x1F, + 0x13, 0x34, 0x64, 0x1E, 0x97, 0xF3, 0x34, 0x19, 0xB5, 0x55, 0xB6, 0x58, 0x62, 0xB5, 0x6F, 0xFF, + 0xBF, 0x96, 0x18, 0x40, 0x54, 0x5B, 0x63, 0xF9, 0xDA, 0x85, 0x5F, 0xA2, 0x0F, 0xC3, 0x5D, 0x74, + 0xD9, 0x5E, 0xCE, 0x5D, 0x36, 0x24, 0x56, 0x8F, 0x5F, 0x36, 0x71, 0xC7, 0xD7, 0x84, 0x8E, 0x78, + 0x9A, 0xD4, 0x8E, 0x35, 0x72, 0xD9, 0xBC, 0xF3, 0x88, 0x7D, 0x0F, 0xBD, 0x2E, 0xC1, 0x65, 0xCF, + 0x73, 0xC7, 0x12, 0x2A, 0x9B, 0x7A, 0x32, 0x18, 0xBD, 0xD4, 0xED, 0x25, 0x1A, 0xFD, 0x99, 0xF5, + 0x99, 0xE7, 0x10, 0x7B, 0xBD, 0x4A, 0x86, 0x17, 0x2C, 0x1B, 0xE7, 0x50, 0xFF, 0xB1, 0x2D, 0x0C, + 0x4D, 0x0E, 0xAD, 0x41, 0x09, 0x92, 0xBF, 0x3E, 0x34, 0xC7, 0x41, 0x78, 0x0D, 0x43, 0x9A, 0xD6, + 0x9F, 0x57, 0x0E, 0xE7, 0x85, 0x6D, 0xA5, 0x75, 0x5B, 0x52, 0xBC, 0x5B, 0x77, 0x39, 0xDA, 0x92, + 0x46, 0x7A, 0xED, 0x42, 0x2E, 0x7F, 0xC5, 0x0F, 0x22, 0x20, 0x90, 0xFA, 0x22, 0x08, 0x68, 0xFD, + 0x61, 0x3B, 0x2F, 0xB5, 0x24, 0x99, 0x50, 0xA8, 0xAF, 0x7E, 0xC5, 0xEA, 0x5F, 0x60, 0x10, 0x94, + 0x8D, 0x52, 0xE4, 0xAD, 0xD6, 0x93, 0xF7, 0x0D, 0xA8, 0x68, 0x7E, 0xC7, 0xE6, 0x97, 0x53, 0xB9, + 0xEE, 0x15, 0x7E, 0x45, 0xE7, 0xC7, 0x96, 0x2D, 0xDF, 0x38, 0xB8, 0xBE, 0x92, 0x89, 0xAB, 0x87, + 0xFD, 0x82, 0x52, 0xA5, 0x9B, 0x47, 0xE5, 0xBE, 0x21, 0xD7, 0xDA, 0xEE, 0xC6, 0xC6, 0xC0, 0xA0, + 0x42, 0x44, 0x51, 0x3A, 0x69, 0x6D, 0xB0, 0x47, 0x89, 0x71, 0x36, 0xA3, 0x61, 0xC8, 0x18, 0x75, + 0x72, 0xD9, 0x61, 0x66, 0x21, 0x01, 0x87, 0x0D, 0xFF, 0x54, 0x52, 0x0D, 0x2A, 0xE1, 0xF0, 0x2A, + 0x82, 0x9B, 0xFA, 0xD0, 0xFC, 0xF7, 0x5E, 0xF1, 0x26, 0x8A, 0x7C, 0x2F, 0x48, 0x19, 0x2F, 0x46, + 0xCA, 0x4A, 0xE8, 0x80, 0x9A, 0xD7, 0x4D, 0x01, 0x6C, 0xF6, 0xCD, 0x21, 0xEE, 0xDC, 0x20, 0xE1, + 0xC6, 0x97, 0xA5, 0x2C, 0x54, 0x27, 0xE3, 0x2C, 0xF1, 0xA5, 0x57, 0xF8, 0xD5, 0xD2, 0x16, 0x4C, + 0x1D, 0x73, 0x91, 0x06, 0xCB, 0x71, 0x51, 0x5B, 0xAC, 0x69, 0x5D, 0x1C, 0xD5, 0x24, 0x52, 0xF7, + 0x49, 0xE0, 0x4B, 0xA2, 0x59, 0x31, 0xBC, 0x8A, 0x78, 0x72, 0xB1, 0x7B, 0x37, 0x00, 0xC8, 0xAB, + 0x7D, 0x10, 0xC0, 0x01, 0x12, 0x55, 0xE5, 0x29, 0x3E, 0xAF, 0x99, 0x4E, 0x03, 0x64, 0x57, 0x3C, + 0x97, 0xEE, 0x06, 0x7D, 0x86, 0x96, 0xFB, 0x28, 0x82, 0x46, 0x6C, 0xCC, 0x9F, 0xA6, 0x0A, 0x78, + 0x6F, 0x4F, 0x67, 0x19, 0xA2, 0x3F, 0xDA, 0x0C, 0x7D, 0x5F, 0x98, 0xA1, 0x7B, 0xED, 0xCD, 0x11, + 0xB3, 0xD1, 0xD4, 0x7C, 0x79, 0x5D, 0xE9, 0x56, 0xCB, 0x4F, 0x28, 0x31, 0x11, 0x10, 0xD1, 0x1A, + 0xA2, 0xA7, 0x7E, 0x43, 0xF4, 0xFD, 0x46, 0x43, 0xF4, 0x6F, 0x6F, 0x89, 0xAE, 0x5F, 0x4F, 0x4A, + 0x7F, 0x3B, 0x93, 0xF3, 0x57, 0xD4, 0x94, 0x44, 0x05, 0x58, 0x53, 0xB1, 0x2B, 0x5B, 0x4C, 0xF3, + 0x0A, 0x4D, 0x45, 0xF7, 0x6D, 0x51, 0x71, 0x75, 0x69, 0x2A, 0x77, 0x6C, 0xCB, 0x19, 0x05, 0x73, + 0x53, 0xD9, 0x5D, 0xFA, 0x98, 0x88, 0x84, 0x1F, 0xA4, 0x19, 0x7D, 0x8A, 0x66, 0xF4, 0xDA, 0x0C, + 0x2F, 0x7E, 0xEE, 0x31, 0xD5, 0x56, 0x99, 0xAE, 0xA9, 0xF6, 0x73, 0x93, 0xBE, 0x88, 0xA9, 0x36, + 0x36, 0x7D, 0x47, 0xC9, 0xF3, 0x26, 0x53, 0xED, 0x37, 0x80, 0x8A, 0x40, 0x0D, 0xA4, 0x54, 0xF5, + 0x9D, 0x34, 0x18, 0x34, 0xDF, 0x5F, 0xA4, 0xC1, 0xE0, 0x3B, 0x4A, 0x7E, 0x51, 0x06, 0x83, 0x6F, + 0xC0, 0x60, 0xF0, 0x89, 0x34, 0x18, 0x34, 0xA5, 0x7E, 0x68, 0x30, 0x18, 0x7C, 0xE2, 0x1A, 0x0C, + 0xFE, 0x28, 0x7D, 0x2F, 0x70, 0x28, 0x0A, 0x46, 0x25, 0x7D, 0x95, 0xB1, 0x3E, 0xD3, 0xB7, 0x26, + 0x6D, 0xFA, 0xA3, 0x4E, 0x28, 0xB8, 0x33, 0x79, 0x4D, 0xCD, 0xCC, 0xE0, 0x1F, 0x61, 0x6A, 0x76, + 0xF3, 0xAC, 0x52, 0x77, 0xED, 0x86, 0x26, 0x7D, 0x69, 0x6A, 0x76, 0xF3, 0xEC, 0x7B, 0x4F, 0xE3, + 0x22, 0xD6, 0x5B, 0xC5, 0x12, 0x0D, 0x4E, 0xE7, 0x5F, 0xED, 0x8C, 0x4A, 0xAB, 0x4E, 0x1E, 0xD4, + 0x98, 0x67, 0x59, 0x32, 0xE2, 0x95, 0x92, 0x69, 0x4B, 0x29, 0xCA, 0xB4, 0xA5, 0x14, 0x83, 0x5F, + 0xAD, 0xCA, 0xA8, 0x48, 0x56, 0x4A, 0x54, 0x51, 0xE7, 0x72, 0xE6, 0x9A, 0x8D, 0x7D, 0xA9, 0x9B, + 0x8D, 0x7D, 0x71, 0xCD, 0xC6, 0xBE, 0x54, 0xCD, 0xC6, 0xBE, 0xF8, 0xCD, 0xC6, 0xBE, 0x54, 0xCD, + 0xC6, 0xBE, 0xF8, 0xCC, 0xC6, 0xBE, 0xF8, 0xCD, 0xC6, 0xBE, 0x34, 0x9B, 0x8D, 0x7D, 0x69, 0x32, + 0x1B, 0xC3, 0x2C, 0xA0, 0x21, 0xCC, 0x17, 0x15, 0xB7, 0x53, 0x32, 0xEF, 0xB2, 0x74, 0x56, 0x86, + 0xD1, 0x96, 0xD4, 0x0F, 0xB8, 0x73, 0x21, 0x2F, 0x2A, 0x22, 0xF9, 0x2E, 0xF7, 0x14, 0x2E, 0x0F, + 0x04, 0x6E, 0x00, 0x69, 0x64, 0x2D, 0x9C, 0xA4, 0x91, 0xB6, 0xF6, 0x75, 0x0A, 0x99, 0xCD, 0x89, + 0xA0, 0xA8, 0xFD, 0x2A, 0xB3, 0x0A, 0x40, 0xF7, 0x5B, 0x97, 0x97, 0x22, 0xEB, 0xF2, 0x32, 0x56, + 0x3A, 0x7B, 0x9F, 0xD9, 0x54, 0x36, 0x57, 0x90, 0x5E, 0x91, 0x11, 0xEB, 0xF7, 0xC3, 0x07, 0x56, + 0xF9, 0x8E, 0x2A, 0xCB, 0xFF, 0x58, 0xA6, 0xFE, 0xC4, 0xCA, 0xD4, 0xB3, 0xDF, 0x4C, 0xA6, 0x9E, + 0x95, 0x64, 0xEA, 0x0B, 0xF3, 0x3F, 0x4D, 0x62, 0x75, 0x2E, 0x65, 0x7C, 0x8C, 0xD5, 0xE4, 0xE8, + 0xDA, 0xAD, 0xAC, 0x0E, 0xA9, 0x8E, 0x47, 0x96, 0x2A, 0x85, 0xDC, 0x3E, 0xE9, 0x2D, 0x34, 0x3C, + 0xB3, 0x6B, 0xD8, 0x89, 0x78, 0x7E, 0xBA, 0xFC, 0x16, 0x67, 0x42, 0x62, 0x4F, 0x19, 0x42, 0x52, + 0xF4, 0xDE, 0xB8, 0xB6, 0x73, 0xC5, 0x31, 0x8A, 0x12, 0xA0, 0xFC, 0xA3, 0x0C, 0x5D, 0x95, 0x0C, + 0xED, 0x7F, 0x73, 0x9E, 0xF2, 0x07, 0x87, 0xA7, 0xDC, 0xD8, 0x5A, 0x88, 0xAB, 0xFC, 0xB6, 0x0C, + 0xCE, 0xBB, 0x06, 0x06, 0xA7, 0x2B, 0x8D, 0x62, 0xBF, 0x05, 0x93, 0xF3, 0xEE, 0xFF, 0x8B, 0xC9, + 0xF9, 0x71, 0x31, 0x26, 0xE7, 0xCD, 0xE2, 0x4C, 0xCE, 0xBB, 0x05, 0x99, 0x9C, 0x9C, 0x2D, 0xCE, + 0xE4, 0x7C, 0x79, 0x14, 0x93, 0xF3, 0xAB, 0x61, 0x72, 0x9E, 0x03, 0x4A, 0x6A, 0xE6, 0x21, 0x2E, + 0x58, 0x03, 0x9B, 0xF3, 0xC6, 0x65, 0x73, 0xA0, 0xA0, 0x4A, 0x5F, 0x88, 0xCD, 0xC1, 0xA6, 0x7F, + 0xA1, 0x70, 0x8C, 0x34, 0xB0, 0x39, 0x09, 0xD3, 0x2F, 0xBE, 0x17, 0x23, 0x38, 0x3F, 0x87, 0x4C, + 0x30, 0x3A, 0xA5, 0x94, 0x1E, 0x13, 0xAC, 0xCE, 0x90, 0x91, 0x1B, 0x26, 0x59, 0x9D, 0x84, 0x81, + 0x13, 0x15, 0x33, 0x4F, 0xE8, 0x6D, 0x6E, 0x10, 0x89, 0x25, 0xDA, 0x7E, 0x5E, 0xBA, 0x94, 0xA1, + 0x30, 0x97, 0x5C, 0xDB, 0x62, 0xFA, 0xEA, 0xAD, 0x0B, 0x2B, 0x14, 0xD5, 0xC5, 0xEB, 0xC6, 0xFD, + 0x50, 0xBD, 0x0F, 0xD5, 0xB7, 0x77, 0x4E, 0xF7, 0xDF, 0x6C, 0x9F, 0xC2, 0xD1, 0x28, 0xC4, 0x22, + 0x09, 0x87, 0x83, 0xB1, 0xC7, 0xC8, 0xC9, 0xCE, 0xF1, 0xE1, 0xAB, 0x57, 0xDA, 0xE4, 0x5C, 0x24, + 0xBD, 0x3A, 0xDC, 0xDE, 0x2D, 0x45, 0xCB, 0x05, 0xC7, 0xF1, 0x3E, 0xA6, 0xBB, 0xC1, 0x72, 0x07, + 0x30, 0x1D, 0xE7, 0x79, 0x43, 0x78, 0x8D, 0x1C, 0x92, 0xA4, 0xC8, 0x05, 0x06, 0xC7, 0x74, 0x68, + 0x5A, 0x98, 0xBC, 0x82, 0x8C, 0x88, 0x4B, 0x3B, 0x62, 0xF2, 0xDD, 0x44, 0x88, 0x3B, 0x3B, 0x84, + 0x5B, 0xFA, 0x1A, 0x3A, 0xF0, 0x61, 0x98, 0x59, 0x95, 0xBC, 0x86, 0x3E, 0x79, 0x01, 0xB9, 0x57, + 0x9F, 0xAA, 0xE5, 0x3D, 0xFC, 0xB4, 0xC5, 0x55, 0xEA, 0x0E, 0xA6, 0xDA, 0x87, 0x33, 0x4F, 0xCB, + 0x9F, 0xAA, 0xC8, 0x89, 0x93, 0xA6, 0x9F, 0x32, 0xBC, 0x82, 0x54, 0x09, 0x4D, 0x70, 0x61, 0x61, + 0xF6, 0x89, 0x48, 0x70, 0x60, 0x61, 0x5E, 0xDB, 0xEF, 0x9A, 0xEC, 0xB0, 0xE3, 0x0F, 0x6C, 0x2B, + 0xA7, 0xA9, 0x4F, 0xC5, 0xE0, 0xF9, 0xE1, 0xEE, 0x3B, 0x21, 0xAD, 0xD3, 0xCE, 0x9E, 0x5D, 0xE9, + 0x00, 0xD6, 0xE6, 0x0B, 0x3E, 0x3D, 0xA8, 0x05, 0xFA, 0xEE, 0xD3, 0x1F, 0x72, 0xA1, 0xC5, 0x19, + 0xF2, 0x89, 0x21, 0xF3, 0xBB, 0xDA, 0x94, 0xBD, 0x37, 0x3B, 0xFB, 0x94, 0xA9, 0x7E, 0xB4, 0xFE, + 0xEF, 0xFC, 0x42, 0x25, 0x60, 0x21, 0x9B, 0x60, 0x45, 0x69, 0x98, 0xEC, 0x98, 0xA2, 0x96, 0xE3, + 0x9A, 0xC4, 0x1B, 0x8A, 0x86, 0x5A, 0x40, 0x58, 0x2D, 0x4D, 0x9F, 0xB5, 0x24, 0xAA, 0xF9, 0x74, + 0x2C, 0xAC, 0x85, 0x9C, 0x50, 0x8F, 0xE2, 0xE1, 0x68, 0x94, 0x21, 0xE0, 0x47, 0x09, 0xE1, 0x06, + 0x6E, 0xD4, 0xC5, 0x59, 0x97, 0xB0, 0x26, 0xE6, 0xCC, 0x63, 0x62, 0xAE, 0x2B, 0x35, 0xAA, 0x4C, + 0x7D, 0xEB, 0x14, 0xFB, 0x52, 0x5B, 0x72, 0xA9, 0xBA, 0x57, 0xAC, 0xBD, 0xCF, 0xC0, 0x3C, 0x47, + 0xEC, 0xC9, 0xDA, 0x2B, 0x2C, 0x72, 0x0F, 0x82, 0xE4, 0xCD, 0x93, 0x0A, 0xB6, 0x96, 0x19, 0x94, + 0xDF, 0x67, 0xDD, 0x4A, 0x5C, 0x90, 0x53, 0x8C, 0x7D, 0xD3, 0xDE, 0xE8, 0x2C, 0x0A, 0x7B, 0x07, + 0xC6, 0xB6, 0xAD, 0x52, 0x2A, 0xC0, 0x68, 0xD1, 0x78, 0x06, 0x2E, 0x62, 0x45, 0x78, 0xAA, 0x8F, + 0xC2, 0xC7, 0xBE, 0x17, 0x01, 0x27, 0xE0, 0xFC, 0xB7, 0x22, 0xB4, 0xC2, 0x93, 0x36, 0xB8, 0xEE, + 0xCB, 0xF0, 0xAB, 0xD2, 0x7F, 0x7F, 0x32, 0x01, 0x55, 0x98, 0x98, 0x8A, 0x92, 0xD1, 0x9E, 0xE3, + 0x2E, 0x00, 0x1F, 0xB7, 0x10, 0x5F, 0x2E, 0x1B, 0xAD, 0xE6, 0x84, 0x5D, 0xE8, 0xD7, 0x9D, 0x84, + 0xB5, 0x71, 0x34, 0xCB, 0xA7, 0x0E, 0x72, 0x8B, 0x2C, 0xE7, 0x8E, 0x53, 0xA0, 0xCD, 0x05, 0x86, + 0x73, 0x8D, 0xC2, 0x9F, 0xA9, 0x5F, 0x60, 0x4B, 0xCD, 0x92, 0xB4, 0x46, 0x63, 0x40, 0x3F, 0x2C, + 0x0F, 0xB3, 0x51, 0xEB, 0x62, 0x53, 0x37, 0xF1, 0x31, 0xF0, 0xC7, 0x3F, 0x93, 0x36, 0x64, 0x51, + 0x7D, 0x97, 0x58, 0xD3, 0xF3, 0x1E, 0xF3, 0x3E, 0x96, 0xE6, 0x94, 0xAF, 0x67, 0x54, 0xAD, 0xB2, + 0xED, 0x02, 0xDB, 0x34, 0x33, 0x2D, 0x9B, 0x24, 0xE7, 0xA4, 0x93, 0x1E, 0xB3, 0xD3, 0xE7, 0x3C, + 0xAC, 0x0F, 0x0B, 0x6B, 0x34, 0x7E, 0x15, 0xA9, 0xF6, 0x98, 0xCD, 0x96, 0x5E, 0x8B, 0x11, 0x95, + 0xC5, 0xEF, 0x3A, 0x45, 0x31, 0xA3, 0x69, 0x1F, 0x74, 0x8E, 0xE2, 0x69, 0x6F, 0x6B, 0xC9, 0x9C, + 0x48, 0x98, 0xD6, 0x4B, 0x0A, 0x6D, 0x8A, 0x4E, 0x8F, 0x83, 0xDF, 0x03, 0xD3, 0xAD, 0xB1, 0xA8, + 0x2E, 0x84, 0x4E, 0x80, 0x1A, 0xC2, 0x41, 0x6B, 0x25, 0xC5, 0x76, 0xBB, 0x36, 0x30, 0xE6, 0x75, + 0x42, 0xA2, 0x88, 0x86, 0x8F, 0x9E, 0x8C, 0xC0, 0xB1, 0xF9, 0xDD, 0xA1, 0x3C, 0xC3, 0x9D, 0xFC, + 0x6A, 0xE8, 0x4D, 0xDB, 0xBB, 0x05, 0xF8, 0x62, 0x03, 0x70, 0xA2, 0x4C, 0x4D, 0x26, 0xAF, 0x13, + 0x7E, 0x03, 0xDB, 0xFB, 0xB3, 0x6B, 0xF5, 0xE6, 0x14, 0x22, 0x4D, 0xC6, 0x72, 0x4E, 0xA9, 0xC8, + 0xB5, 0x47, 0x79, 0xCC, 0x90, 0x2C, 0x4C, 0xCA, 0x31, 0x94, 0x44, 0x0B, 0x5E, 0x30, 0x34, 0xD1, + 0x0A, 0x45, 0x1D, 0x60, 0x20, 0xE6, 0x20, 0xA8, 0x5F, 0x65, 0x7C, 0x74, 0x76, 0xD5, 0xF7, 0x44, + 0x18, 0x6D, 0x24, 0xA3, 0x2C, 0xF6, 0x94, 0x5F, 0xA5, 0x6B, 0x98, 0x5A, 0x05, 0x81, 0x22, 0x61, + 0xF5, 0xED, 0x21, 0xAF, 0x46, 0x95, 0x43, 0x8C, 0x6D, 0xC5, 0x5C, 0x93, 0x44, 0x67, 0xF3, 0x9D, + 0x3B, 0x5F, 0xE6, 0x85, 0x95, 0x8B, 0x4E, 0x7D, 0x3F, 0xE2, 0x6B, 0x2E, 0xD0, 0xB0, 0x4D, 0x47, + 0x71, 0x43, 0x6A, 0x7D, 0x72, 0xEA, 0x35, 0xA0, 0xF4, 0x96, 0xB3, 0xFB, 0x81, 0x9A, 0xC1, 0xAD, + 0xAD, 0x96, 0xA6, 0x3D, 0x75, 0x9A, 0x68, 0x40, 0xC9, 0x83, 0x47, 0xA8, 0x13, 0x43, 0xEB, 0xE0, + 0x9C, 0xC5, 0x4E, 0x63, 0xC6, 0xA3, 0x78, 0x6D, 0xAD, 0x13, 0x3D, 0xF8, 0xE6, 0x50, 0x99, 0x7E, + 0x76, 0x01, 0x83, 0xFC, 0xBE, 0xD4, 0x88, 0x4E, 0xF4, 0xDE, 0xB2, 0x2A, 0xC5, 0x56, 0x37, 0x2F, + 0xE0, 0xE6, 0xBB, 0x55, 0x4F, 0x8D, 0x6A, 0x70, 0xAA, 0xF6, 0x2A, 0x83, 0x4D, 0x9B, 0x02, 0xEE, + 0x6B, 0x68, 0x1E, 0x38, 0x50, 0xE2, 0xCE, 0xDF, 0xE0, 0x9C, 0x25, 0xB7, 0xC6, 0x88, 0x87, 0x04, + 0xF5, 0x83, 0x55, 0x43, 0x78, 0x55, 0x3F, 0xE7, 0x20, 0x9B, 0x55, 0x8F, 0x95, 0x5D, 0x90, 0xA7, + 0xAB, 0x98, 0x85, 0xE1, 0x23, 0x4C, 0xDA, 0xD3, 0xA9, 0x7C, 0x2D, 0x68, 0xE1, 0xC3, 0x5D, 0xDC, + 0xB9, 0x45, 0xF7, 0xF0, 0xBF, 0x0E, 0xB3, 0x4A, 0xFE, 0x01, 0x83, 0x27, 0x0B, 0x99, 0xB1, 0x55, + 0xD9, 0x61, 0xCA, 0xFE, 0xE9, 0x84, 0x95, 0x24, 0x3C, 0x97, 0x0C, 0xBA, 0x73, 0x3E, 0xA3, 0x36, + 0x54, 0x72, 0xF2, 0xB5, 0xE5, 0x71, 0x11, 0x8E, 0xA0, 0x2A, 0x5A, 0xE7, 0x87, 0x82, 0x09, 0x15, + 0xDC, 0x66, 0x34, 0xB7, 0xEC, 0x3D, 0x2B, 0x59, 0xDD, 0x7E, 0xAA, 0x54, 0x68, 0x3C, 0x19, 0x95, + 0xCC, 0x16, 0x99, 0x48, 0x7D, 0x8D, 0x21, 0xD5, 0x97, 0xCF, 0xC4, 0x61, 0xAC, 0x56, 0xA7, 0x4C, + 0x1A, 0x16, 0x86, 0x9D, 0xCB, 0x18, 0xCD, 0x64, 0x30, 0x6C, 0xD0, 0x4B, 0x6D, 0x34, 0x26, 0xC6, + 0x6F, 0x39, 0x8A, 0x39, 0xB5, 0x14, 0x7B, 0x80, 0x75, 0x70, 0xD8, 0xEC, 0x9B, 0xBE, 0xCC, 0x37, + 0x64, 0x95, 0x80, 0x42, 0xAC, 0xE9, 0x65, 0xBE, 0x95, 0x15, 0xB8, 0x69, 0x54, 0xEA, 0xFE, 0x97, + 0x5F, 0xE5, 0x03, 0xAF, 0xA8, 0xDF, 0x4E, 0xEA, 0x31, 0x66, 0x42, 0x42, 0xC0, 0x64, 0x34, 0x21, + 0x79, 0x00, 0xE9, 0xCB, 0x88, 0x73, 0xC9, 0x25, 0x15, 0xF3, 0x82, 0x47, 0x06, 0x33, 0x63, 0x52, + 0x5C, 0xA6, 0x29, 0x1F, 0x05, 0xCA, 0x67, 0x03, 0x83, 0x09, 0x00, 0x75, 0x76, 0x99, 0x3F, 0xB0, + 0x0E, 0x4E, 0x5F, 0x19, 0x15, 0x08, 0xE3, 0x32, 0x2D, 0x18, 0x88, 0x3D, 0x15, 0x74, 0xA6, 0x23, + 0xC6, 0x80, 0x82, 0x3A, 0x7D, 0x11, 0x31, 0x06, 0x36, 0x7D, 0xC3, 0xC8, 0x6E, 0x93, 0x18, 0xE3, + 0x4E, 0xCA, 0x2D, 0x78, 0x72, 0x15, 0x90, 0x6D, 0x29, 0xB1, 0xB8, 0x63, 0xE4, 0x40, 0x4A, 0x2C, + 0x30, 0x99, 0x1C, 0x56, 0x34, 0x03, 0xDB, 0xAC, 0xA2, 0x19, 0x10, 0x49, 0x56, 0x33, 0x60, 0x3E, + 0x0F, 0xE4, 0xB7, 0xC8, 0x6F, 0x7A, 0x92, 0x67, 0xBB, 0x22, 0x65, 0xF8, 0x50, 0x96, 0x32, 0xE0, + 0x0B, 0x85, 0x01, 0x39, 0x2A, 0x49, 0x19, 0x5E, 0xC1, 0x6F, 0xFD, 0x26, 0x5B, 0x40, 0xDE, 0x1B, + 0x7D, 0xC7, 0x4B, 0xA3, 0xEF, 0x38, 0x76, 0x65, 0x04, 0x9F, 0xFD, 0xD2, 0x87, 0x17, 0x98, 0xAC, + 0x1B, 0x3D, 0x83, 0x8F, 0xEF, 0x97, 0x86, 0xE9, 0xD2, 0xF7, 0x4B, 0x26, 0xED, 0x2D, 0xAB, 0x3E, + 0xC2, 0x83, 0x10, 0xBA, 0x20, 0x4B, 0x6E, 0xE2, 0x28, 0x1D, 0x0E, 0xEB, 0xA9, 0xD8, 0x13, 0x8A, + 0x3E, 0x3E, 0x7A, 0x85, 0x11, 0xAF, 0x45, 0x77, 0x2D, 0x67, 0x96, 0xB6, 0xE3, 0x9F, 0x58, 0x93, + 0x33, 0x7A, 0xF4, 0x50, 0x91, 0x41, 0x34, 0x7B, 0x5E, 0xFB, 0x3D, 0x33, 0x99, 0x7D, 0x69, 0x26, + 0xFC, 0xC6, 0x41, 0xF4, 0x57, 0x56, 0x66, 0xBC, 0xAB, 0x8D, 0x21, 0x55, 0x9A, 0xB3, 0x5F, 0x31, + 0xF3, 0x46, 0x39, 0x49, 0x61, 0x57, 0x35, 0xBE, 0x65, 0xFB, 0x99, 0x09, 0xD5, 0x41, 0xD6, 0x78, + 0x51, 0xAD, 0x54, 0x14, 0x21, 0x7B, 0xB4, 0x7F, 0x45, 0x70, 0xF6, 0x0A, 0xEF, 0xF2, 0x72, 0x16, + 0x32, 0x0C, 0x57, 0x70, 0x58, 0x49, 0xEA, 0x9E, 0xB1, 0xF6, 0x0B, 0xD0, 0x98, 0xC7, 0x21, 0xFA, + 0xF0, 0xDC, 0x26, 0x1F, 0xE8, 0x36, 0x06, 0xD2, 0x40, 0x13, 0x73, 0x75, 0x78, 0xE6, 0x30, 0xD4, + 0xF3, 0xD4, 0x72, 0x6C, 0x62, 0x05, 0x0A, 0xE3, 0xF0, 0x73, 0xC8, 0xE4, 0x6B, 0x95, 0x33, 0x9F, + 0x08, 0xC7, 0x18, 0x3F, 0xB6, 0x82, 0xF7, 0x05, 0x9E, 0x74, 0xAA, 0x5F, 0x2A, 0x01, 0xD3, 0x57, + 0x7B, 0x18, 0x16, 0xB5, 0x00, 0xCA, 0x36, 0x2F, 0x01, 0x47, 0x9A, 0xC4, 0xEB, 0x48, 0x03, 0x16, + 0xFE, 0x45, 0x83, 0x87, 0x4D, 0x86, 0x82, 0xF1, 0xA6, 0x2B, 0x7D, 0x06, 0x34, 0xCE, 0x65, 0xA8, + 0x2A, 0x17, 0x5B, 0xE5, 0xB8, 0x59, 0x8F, 0x3D, 0x5E, 0x9E, 0x9E, 0xA4, 0x14, 0xD5, 0x09, 0xB2, + 0x32, 0x38, 0x68, 0x15, 0x1C, 0x07, 0x3E, 0x78, 0x38, 0x90, 0x90, 0x6F, 0x9C, 0x7B, 0xC0, 0x80, + 0xCE, 0x64, 0xBC, 0x5B, 0x1D, 0x36, 0x29, 0xE3, 0x31, 0x19, 0x46, 0x6D, 0x7C, 0x78, 0xE4, 0x2B, + 0x5E, 0x3C, 0xBF, 0xF3, 0xDE, 0xD5, 0x1B, 0x78, 0x4A, 0x34, 0xDB, 0x62, 0x65, 0xFE, 0x1F, 0x30, + 0x37, 0x5C, 0x06, 0xBC, 0x43, 0x54, 0x5C, 0xD6, 0x78, 0xA7, 0x02, 0xC5, 0x1D, 0x56, 0x92, 0x84, + 0x1B, 0x84, 0xE5, 0x9B, 0x5E, 0x80, 0x5F, 0xBE, 0xC5, 0xC2, 0x33, 0x16, 0x89, 0x9D, 0x00, 0x77, + 0xFD, 0x95, 0x15, 0x58, 0x43, 0xF7, 0x01, 0xD7, 0xF7, 0x30, 0x48, 0xE7, 0xAA, 0x64, 0xCD, 0x9A, + 0xB9, 0xB1, 0xA2, 0xD9, 0xC9, 0x40, 0x19, 0x47, 0x05, 0x6C, 0x20, 0xB8, 0x9F, 0xF4, 0xD1, 0x82, + 0x96, 0xF4, 0xF3, 0xB0, 0x8B, 0xB8, 0x23, 0x41, 0xD8, 0xA0, 0xDA, 0x2B, 0xA6, 0x2F, 0x9B, 0xDC, + 0x93, 0x8A, 0x06, 0xF7, 0xA4, 0x44, 0xB9, 0x27, 0x15, 0xD2, 0x5A, 0xB9, 0x3E, 0xC2, 0x3A, 0x40, + 0xE5, 0xAE, 0x2E, 0x45, 0x04, 0x35, 0x54, 0xC6, 0xBC, 0x00, 0x44, 0xED, 0x72, 0x6B, 0x98, 0xBD, + 0x16, 0xC4, 0xA3, 0x23, 0xB7, 0x54, 0xAD, 0x26, 0x41, 0xEA, 0x2E, 0x88, 0x41, 0x45, 0xE5, 0x96, + 0x67, 0x32, 0x68, 0x1E, 0xF5, 0x79, 0x40, 0x4B, 0xD6, 0x11, 0x0F, 0x22, 0xB0, 0xC7, 0x9C, 0x4A, + 0x2F, 0x37, 0xC1, 0xDF, 0xD6, 0xDB, 0xE5, 0x0D, 0xED, 0xF2, 0xD9, 0xED, 0xA2, 0x2D, 0xAF, 0x0D, + 0x3A, 0x1E, 0x11, 0x2F, 0x1F, 0xFA, 0x9E, 0x61, 0x43, 0x6E, 0x58, 0x76, 0x5C, 0x09, 0x07, 0xEB, + 0x71, 0xDE, 0xE5, 0xEF, 0x12, 0xC2, 0x7C, 0x40, 0x15, 0x97, 0x0E, 0x7E, 0x58, 0xA2, 0xB7, 0xC7, + 0x02, 0x64, 0x02, 0x33, 0x14, 0x15, 0xAD, 0x30, 0x48, 0x59, 0x9D, 0x33, 0xFA, 0x08, 0x6D, 0xE1, + 0xDB, 0x9E, 0x2E, 0x18, 0xA6, 0x7C, 0xF6, 0x5B, 0xB9, 0xF8, 0x94, 0x15, 0x1A, 0x2B, 0x85, 0xDF, + 0x50, 0x71, 0x88, 0xFC, 0x99, 0xE4, 0xB3, 0xEE, 0x98, 0x3C, 0x0A, 0x8C, 0x1D, 0x6F, 0xAA, 0x4A, + 0x98, 0x7C, 0x42, 0x1F, 0x17, 0x67, 0x93, 0x7E, 0x85, 0xE2, 0x8F, 0xFE, 0xC7, 0x8A, 0x3F, 0xA1, + 0xFA, 0xAA, 0x87, 0xC9, 0x3C, 0x64, 0xB5, 0x47, 0x35, 0xD9, 0xBC, 0xC7, 0x3C, 0x7F, 0x62, 0x73, + 0x1E, 0xEE, 0x94, 0xCC, 0x14, 0x4A, 0xE6, 0x35, 0x1B, 0x18, 0x7B, 0xEA, 0x98, 0xCC, 0x32, 0x7F, + 0x0A, 0x05, 0x4D, 0xFA, 0x7C, 0xFE, 0x54, 0x34, 0x7D, 0xC0, 0xF4, 0x88, 0xEA, 0xFC, 0xE9, 0xCF, + 0x0C, 0x2D, 0x70, 0x92, 0x02, 0xC3, 0x48, 0x49, 0x56, 0x55, 0x7D, 0xBD, 0x91, 0xCC, 0xEA, 0x73, + 0x46, 0xDE, 0x29, 0xF5, 0xDA, 0xCF, 0xA0, 0x5E, 0xFB, 0x02, 0xCC, 0xEA, 0xEC, 0x17, 0x29, 0xDF, + 0x30, 0x52, 0xE6, 0x66, 0xDF, 0xD4, 0xB9, 0xD9, 0x37, 0x2E, 0x37, 0xFB, 0xC6, 0xE1, 0x66, 0x45, + 0xFE, 0x94, 0xFC, 0x62, 0x58, 0xCF, 0x27, 0xF0, 0x4B, 0x34, 0x45, 0x7E, 0x30, 0x4C, 0xE8, 0x8F, + 0xEA, 0x97, 0x50, 0xD3, 0xFD, 0xCA, 0xFC, 0xD1, 0xCA, 0x50, 0xBF, 0x80, 0x15, 0x4D, 0x92, 0x8A, + 0x35, 0x65, 0x14, 0x7E, 0x40, 0x11, 0x53, 0x37, 0x36, 0x96, 0xA9, 0x03, 0xBF, 0x55, 0xE1, 0x3F, + 0x6F, 0x6C, 0x80, 0x8D, 0x75, 0x5A, 0x79, 0x17, 0x52, 0xC0, 0x4C, 0x32, 0xA1, 0x2C, 0x9D, 0x15, + 0xDD, 0xC8, 0x3D, 0xCE, 0x16, 0x55, 0x53, 0x29, 0x7B, 0xF0, 0xAF, 0x0B, 0x0A, 0x84, 0x80, 0x69, + 0x78, 0xAF, 0xA4, 0x91, 0xBD, 0xF9, 0x22, 0x39, 0x03, 0xDD, 0x7F, 0xDD, 0x8B, 0xBD, 0x5C, 0xAF, + 0x4A, 0x0B, 0x7F, 0x51, 0x47, 0x83, 0x1B, 0x5F, 0xC8, 0x94, 0xAE, 0xDF, 0xD2, 0x7F, 0x44, 0xE2, + 0xD9, 0xD4, 0xDA, 0x0F, 0x52, 0xF6, 0xEC, 0x1D, 0xA4, 0xF1, 0x74, 0x37, 0x43, 0xD4, 0x0B, 0x06, + 0x23, 0xD4, 0x16, 0xF1, 0x5A, 0x84, 0x38, 0x63, 0x00, 0x4F, 0xA0, 0x8F, 0x19, 0x33, 0x82, 0x01, + 0x36, 0x41, 0x42, 0x71, 0xFA, 0x8B, 0x1C, 0xE0, 0x5F, 0xF5, 0x04, 0x1C, 0x6D, 0x38, 0xC8, 0x99, + 0x3A, 0xC8, 0x61, 0x82, 0xF3, 0x9E, 0x15, 0x2C, 0x77, 0xE3, 0x3D, 0xD0, 0x7E, 0xC0, 0x03, 0x2D, + 0x9C, 0x81, 0x0B, 0xCA, 0x85, 0x5E, 0x33, 0x7A, 0x78, 0x60, 0xE9, 0xE7, 0x36, 0x17, 0xF0, 0x55, + 0x50, 0xC5, 0xA7, 0x2E, 0x04, 0xC5, 0x6E, 0x8A, 0xA2, 0xAF, 0x88, 0x42, 0xE4, 0xD9, 0x12, 0x8B, + 0x4D, 0x71, 0x36, 0x12, 0x40, 0x19, 0xFF, 0x1B, 0x81, 0x08, 0x00, 0x87, 0xC4, 0xCD, 0x7C, 0xC2, + 0xEE, 0xB9, 0x45, 0xA5, 0x99, 0x6A, 0xA4, 0x85, 0x1F, 0x7A, 0xE3, 0x69, 0x6D, 0x54, 0x4A, 0x9E, + 0x31, 0x43, 0xCF, 0xE3, 0xF3, 0x01, 0xF8, 0x99, 0xCD, 0xF7, 0x01, 0x98, 0xEB, 0x5E, 0xD9, 0x44, + 0x38, 0xA0, 0x40, 0x15, 0x4C, 0x40, 0x25, 0x49, 0xFD, 0x10, 0x52, 0xDB, 0x72, 0x79, 0x43, 0x2A, + 0xF7, 0x14, 0x7A, 0xF8, 0xBB, 0x21, 0xF3, 0x29, 0x08, 0xEE, 0xCE, 0x27, 0xB3, 0x28, 0x84, 0x3C, + 0x6C, 0x16, 0xA2, 0x02, 0x3F, 0xFC, 0x6F, 0xEF, 0xF2, 0x6F, 0xCF, 0xA9, 0x3D, 0xF7, 0x73, 0x6A, + 0xFE, 0x48, 0xEA, 0x86, 0x7F, 0x7B, 0xFE, 0xDF, 0xE3, 0xDF, 0x70, 0x34, 0xFF, 0x11, 0x0F, 0xF7, + 0x08, 0x63, 0x27, 0xB6, 0xA8, 0xD0, 0x93, 0xA7, 0x96, 0x37, 0xD4, 0x7C, 0x50, 0xCC, 0xEA, 0xAB, + 0x43, 0x74, 0x66, 0x99, 0x5B, 0x83, 0x82, 0x2A, 0x7D, 0x21, 0x69, 0x22, 0x36, 0xFD, 0x8E, 0x11, + 0x4F, 0xEB, 0x53, 0xBB, 0xB7, 0x1A, 0xE1, 0x3E, 0x98, 0x1F, 0x33, 0xF1, 0xC7, 0xE4, 0x53, 0x02, + 0x1A, 0xBA, 0x74, 0xC4, 0x75, 0xD4, 0xC4, 0x62, 0x49, 0xF6, 0xD4, 0x52, 0xFF, 0x2E, 0xDD, 0xC2, + 0x43, 0x33, 0x4B, 0x57, 0xE8, 0x9A, 0xD3, 0x1B, 0x8E, 0xE1, 0xFE, 0x00, 0xBF, 0x41, 0x84, 0x4B, + 0x97, 0xFC, 0xCD, 0xB4, 0x4C, 0xA4, 0x30, 0x9C, 0x80, 0x0A, 0x75, 0x60, 0x5D, 0xAF, 0xF1, 0x8A, + 0xA3, 0xBF, 0xA0, 0xA8, 0xD8, 0x95, 0x90, 0xB4, 0xF5, 0xA7, 0x95, 0x15, 0xD4, 0xF8, 0x6F, 0xFD, + 0x7D, 0x32, 0x11, 0x6E, 0xEA, 0x42, 0x8B, 0xF6, 0x77, 0xF8, 0x85, 0xA9, 0x98, 0xF7, 0xA7, 0x8B, + 0xAD, 0xCD, 0xC9, 0xE4, 0xBB, 0x2D, 0x91, 0x55, 0x9A, 0xD7, 0x82, 0x73, 0x4A, 0xF8, 0x12, 0x06, + 0x39, 0xE3, 0x7A, 0x52, 0x9F, 0x36, 0x5B, 0x7F, 0x6F, 0x6D, 0x2E, 0x5D, 0x8D, 0x31, 0xBD, 0x28, + 0x60, 0xDB, 0x24, 0x10, 0xF4, 0xEC, 0xBB, 0xD6, 0x46, 0x6B, 0x03, 0xD8, 0x70, 0xF1, 0xC0, 0xD9, + 0x19, 0x4F, 0x87, 0xF1, 0x25, 0xFC, 0xD8, 0x1E, 0x82, 0x65, 0x75, 0x3C, 0x82, 0x5F, 0xCF, 0xC7, + 0x9C, 0x83, 0x2D, 0xF7, 0x11, 0xFC, 0xDC, 0x49, 0x72, 0x61, 0x00, 0x1D, 0x0F, 0x39, 0x7E, 0xE1, + 0x33, 0xE6, 0x23, 0xA0, 0x63, 0x57, 0xF8, 0xB5, 0xAB, 0x44, 0x8D, 0xF1, 0x8F, 0xF8, 0x25, 0x1F, + 0x8C, 0xFB, 0x44, 0xE1, 0xE7, 0x91, 0x30, 0x01, 0x47, 0xCB, 0x38, 0xF8, 0x38, 0xD1, 0xA6, 0x6B, + 0xF1, 0x2E, 0x7E, 0x9E, 0x22, 0xCF, 0x2F, 0x7E, 0x20, 0x27, 0x09, 0x78, 0x82, 0x3F, 0xA5, 0x29, + 0x3C, 0x3A, 0x0C, 0x28, 0x0B, 0x68, 0xB9, 0xC6, 0xF8, 0x62, 0x3F, 0x8C, 0xE9, 0x1E, 0xA3, 0x59, + 0x5F, 0x5E, 0xD2, 0x02, 0xBA, 0x18, 0xA3, 0xA8, 0x13, 0x36, 0xFA, 0x70, 0x8C, 0xCC, 0xAA, 0x88, + 0x35, 0xFF, 0xBB, 0xF5, 0xF5, 0xDF, 0x2F, 0x81, 0x59, 0x37, 0x3C, 0x76, 0xF8, 0x1A, 0x0C, 0xAE, + 0x61, 0xB3, 0x9E, 0x1D, 0xBF, 0x8A, 0xAF, 0x34, 0xA4, 0x5A, 0xB7, 0x29, 0xAC, 0x4F, 0x81, 0x6A, + 0xB4, 0x7F, 0x03, 0x27, 0xCC, 0x5E, 0xE8, 0xD8, 0xE2, 0x00, 0x00 +}; //bootstrap.min.js + +//Content of favicon.ico with gzip compression +static const uint8_t favicon_ico[] PROGMEM = { + 0x1F, 0x8B, 0x08, 0x08, 0xE0, 0x2D, 0x0C, 0x61, 0x04, 0x00, 0x66, 0x61, 0x76, 0x69, 0x63, 0x6F, + 0x6E, 0x2E, 0x69, 0x63, 0x6F, 0x00, 0xED, 0x9B, 0x07, 0x68, 0xD5, 0x40, 0x18, 0xC7, 0xAF, 0x56, + 0xEB, 0x40, 0x70, 0xE0, 0x16, 0x37, 0x0E, 0xDC, 0x7B, 0x8B, 0x7B, 0xE3, 0x42, 0x05, 0x15, 0xB7, + 0x82, 0x5B, 0x04, 0x37, 0x28, 0x2E, 0xDC, 0x82, 0x22, 0x2A, 0xEE, 0x89, 0x7B, 0xE1, 0xC0, 0x05, + 0xA2, 0x20, 0x0E, 0xDC, 0x8A, 0x8A, 0x5B, 0xEB, 0xC0, 0x6D, 0xD5, 0x2A, 0x56, 0xED, 0xB3, 0xFE, + 0xEF, 0xFC, 0x3F, 0x1B, 0x62, 0x5E, 0x33, 0xFA, 0xF2, 0x92, 0x8A, 0x7F, 0xF8, 0xD1, 0xCB, 0xDD, + 0x77, 0xF7, 0x7D, 0xC9, 0xBB, 0xBB, 0x5C, 0x2E, 0xA9, 0x10, 0x51, 0x22, 0x5A, 0x64, 0xCF, 0x2E, + 0xFF, 0x16, 0x15, 0x83, 0xD3, 0x0B, 0x51, 0x53, 0x08, 0x51, 0xB4, 0xE8, 0xEF, 0xE3, 0x32, 0x39, + 0x84, 0x58, 0x8B, 0xBC, 0xCA, 0x95, 0x59, 0x5E, 0x4A, 0x88, 0x33, 0xB9, 0x85, 0x28, 0x03, 0x1B, + 0x54, 0x41, 0xCE, 0xEF, 0x7C, 0x87, 0xAA, 0x01, 0xEA, 0x09, 0x6B, 0xCA, 0x0D, 0x4A, 0xEB, 0xF2, + 0x9A, 0x82, 0xB6, 0xBA, 0xBC, 0xD2, 0xCA, 0xF6, 0x6F, 0x45, 0x81, 0xA5, 0x20, 0x97, 0x26, 0xAF, + 0x3D, 0xE8, 0x1E, 0x3C, 0x60, 0xD9, 0x12, 0x65, 0x6B, 0xAC, 0x82, 0x60, 0x2F, 0x88, 0xE1, 0x71, + 0x0F, 0x30, 0x88, 0xE9, 0x18, 0x96, 0x15, 0x0C, 0x79, 0x06, 0xC9, 0x75, 0xD6, 0x30, 0x3D, 0x10, + 0x8C, 0x65, 0x7A, 0x8D, 0x2A, 0xB3, 0xA6, 0x9D, 0x8C, 0x7D, 0x34, 0x98, 0x29, 0xD3, 0x2A, 0xCF, + 0xBA, 0xE4, 0x79, 0x6E, 0x05, 0x07, 0xC9, 0x56, 0x95, 0x67, 0x4F, 0xCB, 0xC1, 0x0E, 0xB0, 0x49, + 0xA5, 0xED, 0xAB, 0x01, 0x7F, 0x8F, 0xA5, 0x2A, 0x6D, 0x5F, 0x4D, 0xC0, 0x42, 0x89, 0x4A, 0xDB, + 0x53, 0x33, 0x90, 0x00, 0xE6, 0x82, 0x39, 0x32, 0xAD, 0xF2, 0xAC, 0xAB, 0xB3, 0xAA, 0x9B, 0xDC, + 0x4F, 0x66, 0xAB, 0x3C, 0xEB, 0x0A, 0xD6, 0x9F, 0x2F, 0xDB, 0x70, 0x50, 0xBF, 0x85, 0x3E, 0x7E, + 0x95, 0x67, 0x5D, 0x45, 0xC0, 0x62, 0xB0, 0x5E, 0xC2, 0x74, 0x31, 0x61, 0x5D, 0x43, 0x41, 0x92, + 0x8E, 0x51, 0xC2, 0x05, 0x95, 0xE1, 0x1C, 0xD3, 0x28, 0x75, 0xF3, 0x8C, 0x99, 0xFA, 0x83, 0x61, + 0xC2, 0x3B, 0x8D, 0x01, 0xD3, 0x84, 0x77, 0x9A, 0x0A, 0x16, 0x89, 0xF0, 0x4B, 0xF6, 0xE9, 0xB5, + 0xC2, 0x5C, 0x0B, 0x2C, 0xDA, 0x49, 0x9B, 0x2E, 0xC2, 0xBA, 0xA2, 0xC1, 0x69, 0x30, 0xDE, 0xC4, + 0x6E, 0x19, 0xD8, 0x65, 0x62, 0x33, 0x4E, 0xB6, 0xA5, 0xDA, 0xB4, 0xA7, 0x92, 0xE0, 0x13, 0xE8, + 0x90, 0x82, 0xCD, 0x06, 0x70, 0x2C, 0x54, 0x21, 0xEB, 0x7E, 0x52, 0x6D, 0x39, 0x93, 0x1C, 0xAF, + 0xF1, 0xA0, 0x52, 0x88, 0xF2, 0x3D, 0xE0, 0x6C, 0x88, 0xB2, 0x4A, 0xB2, 0xAE, 0x6A, 0xC3, 0xB9, + 0xA2, 0x78, 0x7E, 0xB1, 0x20, 0x9F, 0x41, 0xF9, 0x51, 0x70, 0xD3, 0x20, 0x3F, 0x1F, 0xEB, 0x1C, + 0x53, 0x6D, 0xA4, 0x4E, 0x85, 0x40, 0x1C, 0xCF, 0x33, 0x93, 0xAE, 0xEC, 0x14, 0x78, 0xAA, 0xCB, + 0xCB, 0x44, 0xDB, 0x38, 0x55, 0x37, 0x3C, 0xEA, 0xC5, 0xB9, 0x72, 0xB3, 0xEE, 0x7C, 0x2E, 0x83, + 0x8F, 0xBA, 0xEB, 0xB5, 0x99, 0xB6, 0xBD, 0x45, 0x78, 0xB5, 0x87, 0xED, 0x4E, 0xD2, 0xE4, 0xDD, + 0x05, 0x81, 0x60, 0x4C, 0x2C, 0x4B, 0x52, 0xB6, 0xE1, 0x57, 0x6E, 0xF0, 0x0A, 0xAC, 0x02, 0xC5, + 0x41, 0x36, 0xF0, 0x9C, 0xFE, 0xF2, 0x31, 0x6F, 0xA5, 0xB4, 0x51, 0xB6, 0xEE, 0xA8, 0x23, 0x18, + 0x44, 0x9F, 0x13, 0xE4, 0x6F, 0xCC, 0xF4, 0x10, 0xFE, 0x1D, 0xA4, 0x6C, 0xDC, 0x55, 0x2D, 0xFA, + 0x1A, 0x01, 0x7E, 0x30, 0x3D, 0x58, 0xFE, 0x55, 0x65, 0xEE, 0xAB, 0x0A, 0x7D, 0x8D, 0x03, 0x2F, + 0xC0, 0x77, 0x30, 0x40, 0xE6, 0xA9, 0x32, 0xF7, 0x15, 0xF4, 0xFF, 0x82, 0x7E, 0xFB, 0xCA, 0x71, + 0x10, 0x41, 0xFF, 0x25, 0xC1, 0x15, 0x23, 0x52, 0x31, 0xCF, 0x3A, 0x51, 0x79, 0x30, 0x4F, 0xA2, + 0xD2, 0x91, 0x57, 0x03, 0x70, 0x52, 0xE2, 0x70, 0x1D, 0xEE, 0x44, 0xE9, 0x38, 0xD6, 0xD6, 0x81, + 0xF5, 0x3A, 0xD6, 0xC9, 0x32, 0x65, 0xE3, 0x9E, 0x86, 0xEB, 0xD6, 0xAE, 0xCF, 0xC1, 0x33, 0x6D, + 0x9E, 0xB2, 0x71, 0x4F, 0x9B, 0x35, 0x7E, 0x1B, 0x6A, 0xF2, 0xEB, 0x83, 0xA7, 0x7F, 0xEE, 0x11, + 0xEE, 0x69, 0x0B, 0x7D, 0x34, 0x32, 0x28, 0xAB, 0x27, 0xCB, 0x94, 0x8D, 0x7B, 0x92, 0x6D, 0xBF, + 0xD0, 0x1C, 0xA7, 0xD7, 0x95, 0x3F, 0x89, 0xA0, 0xFF, 0x29, 0xBC, 0xD7, 0xC4, 0x44, 0xD8, 0xBF, + 0xBA, 0xFE, 0x64, 0x56, 0x84, 0xAF, 0xBF, 0xD7, 0xFD, 0x6F, 0xB4, 0x85, 0xF1, 0x37, 0x46, 0xB8, + 0xA7, 0x68, 0x3E, 0x13, 0xEC, 0x06, 0x7B, 0x74, 0xEC, 0x96, 0x65, 0x0E, 0xD6, 0xF9, 0x76, 0x34, + 0x11, 0x2C, 0x36, 0x61, 0xAA, 0x70, 0x4F, 0x37, 0xE4, 0x35, 0x36, 0xE1, 0x91, 0x70, 0x4F, 0x5E, + 0xFB, 0xFF, 0xE7, 0x54, 0x06, 0x54, 0x06, 0x7D, 0xDD, 0xDD, 0x27, 0x31, 0xD3, 0x52, 0x97, 0xE7, + 0x0D, 0xB7, 0x75, 0x0C, 0x5C, 0x12, 0x69, 0x57, 0x67, 0xC0, 0x4D, 0x97, 0xE7, 0x2E, 0x37, 0x75, + 0x03, 0xDC, 0x51, 0xCF, 0x84, 0xDE, 0xA9, 0x1D, 0xE8, 0xEC, 0x60, 0xFD, 0x19, 0x03, 0x6E, 0x81, + 0xFB, 0x20, 0xAF, 0xB0, 0x27, 0xE9, 0xAB, 0x8B, 0xF2, 0x9D, 0x7A, 0x4D, 0xE3, 0xBD, 0xB4, 0x95, + 0xB0, 0xA7, 0x6C, 0xBC, 0xF6, 0x8F, 0xD4, 0xFE, 0xB0, 0x3D, 0xB5, 0xA2, 0xCF, 0xE9, 0x22, 0xF5, + 0xCA, 0x04, 0x2E, 0x82, 0x0B, 0xA0, 0x94, 0xB0, 0xAE, 0x5C, 0xE0, 0x1E, 0xD7, 0x2F, 0xE5, 0x84, + 0x75, 0x49, 0x1F, 0xE7, 0xA5, 0x4F, 0xE5, 0x3B, 0x3C, 0xAA, 0xCB, 0x7D, 0xAB, 0xC3, 0x36, 0xFA, + 0x42, 0x61, 0xF0, 0x90, 0x6B, 0xA1, 0x6A, 0xC2, 0x9A, 0xF2, 0xD2, 0x47, 0xAC, 0xF2, 0x19, 0x3E, + 0x65, 0x00, 0x93, 0x41, 0x02, 0xF7, 0x45, 0xB3, 0x0A, 0x73, 0x95, 0x00, 0x8F, 0xC1, 0x2B, 0x8B, + 0xCF, 0x91, 0x59, 0xD9, 0x76, 0x82, 0xF4, 0xA5, 0x7C, 0x86, 0x57, 0xB9, 0x39, 0x1F, 0x26, 0x70, + 0x8D, 0x9F, 0xD1, 0xC4, 0xBE, 0x02, 0xFB, 0xCE, 0x1B, 0xD0, 0xDC, 0xC4, 0x36, 0x23, 0xDB, 0x4C, + 0x90, 0x3E, 0x5C, 0xDC, 0xA7, 0xAA, 0xC9, 0xEB, 0xF9, 0xC6, 0xC2, 0x3E, 0x60, 0x55, 0xF6, 0x9D, + 0x38, 0xB5, 0x5F, 0x9C, 0xB2, 0x7A, 0xB1, 0xCD, 0x57, 0xCA, 0x87, 0x7B, 0x8A, 0xE2, 0xBB, 0x95, + 0x44, 0xF0, 0xDE, 0xE4, 0x3D, 0x5D, 0x7D, 0xF0, 0x92, 0xFB, 0xDC, 0xDD, 0x53, 0xB0, 0x6B, 0xC2, + 0xB6, 0x12, 0x65, 0xDB, 0x61, 0xD8, 0x17, 0x36, 0x53, 0x76, 0xB0, 0x1D, 0x04, 0x38, 0x3E, 0x2B, + 0x85, 0xF0, 0xD9, 0x18, 0xBC, 0x06, 0x5F, 0x40, 0xBF, 0x10, 0xD7, 0xA2, 0x32, 0xDB, 0x08, 0xC8, + 0x36, 0x55, 0xDB, 0x91, 0x51, 0x6D, 0x3E, 0xD3, 0x06, 0x38, 0x5F, 0x14, 0x30, 0xB0, 0x69, 0x03, + 0xDE, 0xB1, 0x4F, 0x0F, 0x33, 0x7A, 0xF7, 0xCD, 0xBA, 0x01, 0xCE, 0x37, 0x75, 0x44, 0xE4, 0x14, + 0xAD, 0xD9, 0x1B, 0xFD, 0x01, 0xB6, 0x19, 0xDC, 0x9F, 0x3B, 0xCA, 0xBE, 0xCF, 0xF2, 0x31, 0x06, + 0xF7, 0xD7, 0x6D, 0xDA, 0x7D, 0x56, 0x8F, 0xD6, 0x48, 0x3B, 0xE4, 0xF5, 0x63, 0x0C, 0x73, 0x74, + 0xF7, 0x9B, 0xEE, 0xEC, 0xFB, 0x3F, 0xD5, 0x7C, 0x98, 0xAC, 0xCC, 0xB4, 0x4D, 0x92, 0x75, 0x55, + 0x1B, 0xDE, 0xA9, 0x24, 0xB8, 0xCA, 0x58, 0xDE, 0x82, 0xB6, 0x9A, 0xB1, 0xD0, 0x17, 0x7C, 0x66, + 0xD9, 0x2C, 0x5E, 0xF3, 0x28, 0xF6, 0xAB, 0xB7, 0xCC, 0xBF, 0x6A, 0xF3, 0x9E, 0x1E, 0x6E, 0xA5, + 0xE3, 0x9C, 0x11, 0xCF, 0x7D, 0xC6, 0x4A, 0xBC, 0xD7, 0x96, 0x65, 0x9F, 0xF8, 0xCA, 0x38, 0x17, + 0x82, 0x62, 0x2C, 0xAB, 0x48, 0xDB, 0x78, 0x59, 0xD7, 0xC1, 0xBA, 0x30, 0xDC, 0xCA, 0x08, 0x76, + 0x72, 0x8C, 0xEE, 0x63, 0xBC, 0x17, 0xC1, 0x48, 0xF0, 0x8D, 0xC7, 0x2B, 0xC0, 0x02, 0xA6, 0xF7, + 0xD1, 0x76, 0xA7, 0x85, 0x7B, 0x60, 0xA4, 0x54, 0x18, 0xB4, 0xD4, 0xC4, 0x7F, 0x9C, 0xEF, 0x21, + 0x12, 0x79, 0xBC, 0x11, 0x4C, 0xD5, 0xC4, 0xDF, 0xD2, 0xC1, 0x9A, 0xD4, 0x6D, 0xE5, 0xD2, 0xC4, + 0x7F, 0x90, 0x63, 0xF6, 0x27, 0x8F, 0x77, 0xCA, 0xF3, 0x09, 0xC6, 0xEF, 0xE0, 0xDB, 0x9A, 0x48, + 0x28, 0x87, 0x2E, 0xFE, 0x09, 0xBC, 0xFE, 0x3F, 0xB9, 0xF7, 0x38, 0x26, 0x18, 0xBF, 0xB2, 0xF5, + 0x9F, 0xB4, 0xF1, 0x3F, 0x02, 0x5D, 0xF9, 0x1E, 0xFD, 0x00, 0xE7, 0xD2, 0x43, 0x69, 0x28, 0x7E, + 0x79, 0xDD, 0xCF, 0xF1, 0x9D, 0x43, 0x1F, 0xEE, 0x41, 0x7E, 0xF3, 0x79, 0xFC, 0xD9, 0xB8, 0xD7, + 0x13, 0x67, 0xC2, 0x66, 0x8F, 0x9F, 0xE7, 0x43, 0x29, 0x3D, 0xA8, 0xCE, 0x75, 0x43, 0x27, 0xD0, + 0x0D, 0xF4, 0x90, 0x30, 0xDD, 0x89, 0x65, 0xD5, 0x95, 0xAD, 0xBF, 0x95, 0x97, 0xFD, 0xE4, 0x36, + 0xD9, 0x67, 0xE3, 0xB9, 0xD3, 0x0F, 0xCA, 0x0F, 0x4E, 0x80, 0xD7, 0xE4, 0xA4, 0xCA, 0xF3, 0xB7, + 0xD2, 0x71, 0x5E, 0x2F, 0x0A, 0x4A, 0x73, 0xFE, 0xE9, 0x47, 0xBA, 0x6A, 0x3E, 0x5D, 0xCB, 0xE5, + 0x83, 0x35, 0x83, 0xD1, 0xF3, 0xFD, 0x82, 0xE0, 0x7E, 0x8F, 0x09, 0x77, 0xA4, 0xAD, 0x0B, 0xCF, + 0xE7, 0x4E, 0x95, 0x8E, 0xEB, 0xB3, 0xA4, 0x3F, 0x24, 0xCF, 0xA1, 0x9F, 0x49, 0xA2, 0x41, 0xF9, + 0x42, 0x9F, 0xFC, 0x0E, 0xB2, 0x5F, 0xDF, 0xD7, 0xC5, 0x76, 0x83, 0xDF, 0xF1, 0xF6, 0xE1, 0x73, + 0xFE, 0x0C, 0x70, 0x5D, 0x67, 0x73, 0xDF, 0x27, 0x63, 0xA2, 0x14, 0x88, 0xD5, 0xC4, 0x75, 0x84, + 0x6B, 0xB3, 0x0C, 0xBA, 0xB9, 0xB5, 0x10, 0xD8, 0xAF, 0xB1, 0x8B, 0xF5, 0x78, 0xED, 0x1F, 0x54, + 0x69, 0x4D, 0xFC, 0x37, 0x4D, 0xD6, 0x95, 0x85, 0x34, 0xBF, 0x43, 0xAC, 0xAA, 0xEB, 0xBD, 0x82, + 0xF1, 0x27, 0xF2, 0xBB, 0xE1, 0x0C, 0x26, 0xF7, 0xB8, 0xE9, 0xD2, 0xD6, 0x87, 0xF1, 0xEB, 0xF7, + 0x49, 0x32, 0x86, 0x18, 0x9F, 0x3D, 0x41, 0xBC, 0xCF, 0xE3, 0xAF, 0xC8, 0xBD, 0xB7, 0x53, 0xF2, + 0x9A, 0xA7, 0x91, 0xF8, 0x03, 0x7F, 0xFA, 0x4F, 0xF2, 0xF7, 0xE4, 0x6F, 0x40, 0xD6, 0x34, 0xD2, + 0x7F, 0xB4, 0xE3, 0x37, 0x0B, 0xF7, 0x19, 0xAA, 0x19, 0x8C, 0xDF, 0x6B, 0x3E, 0x1D, 0xBF, 0xFA, + 0xF9, 0x33, 0xBD, 0xC1, 0xFC, 0x79, 0x40, 0xDA, 0xF8, 0x2C, 0xFE, 0x82, 0xE0, 0x81, 0xEE, 0xDE, + 0x74, 0xD3, 0xC2, 0xFD, 0xEB, 0x81, 0x85, 0xFF, 0x8D, 0x89, 0x84, 0xA2, 0xC1, 0x6A, 0x07, 0xEB, + 0x87, 0x35, 0x1E, 0xED, 0x19, 0xFE, 0x25, 0xF6, 0xF7, 0xF5, 0x5C, 0x9F, 0x3D, 0x4B, 0x09, 0xDA, + 0x6C, 0x50, 0x75, 0xFC, 0xA1, 0x18, 0x3E, 0xCF, 0xE6, 0xE1, 0x78, 0x6D, 0x6C, 0x42, 0x35, 0xDA, + 0xE6, 0xF4, 0xC9, 0xFE, 0x55, 0x6B, 0x7E, 0xA7, 0x7A, 0xD7, 0x26, 0xD7, 0xD4, 0x33, 0xA5, 0xF7, + 0xEA, 0x02, 0x3E, 0x58, 0xF8, 0xFE, 0x46, 0xCF, 0x67, 0xB5, 0xC7, 0xEB, 0xBD, 0xFE, 0xC7, 0xFF, + 0x5F, 0xFF, 0xE5, 0x91, 0x7E, 0x01, 0xE5, 0xF0, 0x8C, 0xE8, 0x2E, 0x3C, 0x00, 0x00 +}; //favicon.ico + +//Content of rtk-setup-wifi.png with gzip compression +static const uint8_t rtkSetupWiFi_png[] PROGMEM = { + 0x1F, 0x8B, 0x08, 0x08, 0xA8, 0xB4, 0x35, 0x64, 0x02, 0xFF, 0x72, 0x74, 0x6B, 0x2D, 0x73, 0x65, + 0x74, 0x75, 0x70, 0x2D, 0x77, 0x69, 0x66, 0x69, 0x2E, 0x70, 0x6E, 0x67, 0x2E, 0x67, 0x7A, 0x69, + 0x70, 0x00, 0x01, 0xB0, 0x16, 0x4F, 0xE9, 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, + 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x01, 0x22, 0x00, 0x00, 0x00, 0x59, 0x08, + 0x02, 0x00, 0x00, 0x00, 0x15, 0x4E, 0xBE, 0x57, 0x00, 0x00, 0x00, 0x19, 0x74, 0x45, 0x58, 0x74, + 0x53, 0x6F, 0x66, 0x74, 0x77, 0x61, 0x72, 0x65, 0x00, 0x41, 0x64, 0x6F, 0x62, 0x65, 0x20, 0x49, + 0x6D, 0x61, 0x67, 0x65, 0x52, 0x65, 0x61, 0x64, 0x79, 0x71, 0xC9, 0x65, 0x3C, 0x00, 0x00, 0x16, + 0x52, 0x49, 0x44, 0x41, 0x54, 0x78, 0xDA, 0xEC, 0x5D, 0x07, 0x58, 0x14, 0xD7, 0xDA, 0x66, 0x29, + 0x0B, 0x2E, 0x2C, 0x55, 0xA4, 0x28, 0xA0, 0x28, 0x2A, 0xD2, 0x04, 0x51, 0x73, 0x45, 0x4D, 0x8C, + 0x1A, 0x5B, 0x62, 0x54, 0x12, 0xC9, 0x0D, 0x76, 0x63, 0xC4, 0x5E, 0x40, 0xA3, 0x90, 0x6B, 0x8B, + 0xC4, 0x16, 0x44, 0x44, 0x10, 0x44, 0x14, 0xAC, 0x57, 0x12, 0x31, 0x12, 0x35, 0x6A, 0xEC, 0x28, + 0x4D, 0x10, 0x54, 0x94, 0xAA, 0x58, 0x50, 0x44, 0x8A, 0x94, 0x5D, 0x7A, 0xB9, 0x1F, 0x9C, 0x9F, + 0x71, 0x98, 0x99, 0x9D, 0x5D, 0x90, 0xE5, 0x67, 0xE5, 0xBC, 0xCF, 0x3E, 0x3C, 0xC3, 0xD9, 0xB3, + 0x67, 0x66, 0xBE, 0xF9, 0xDE, 0xF3, 0x95, 0x53, 0x86, 0x53, 0x5F, 0x5F, 0x2F, 0x87, 0x81, 0x81, + 0x21, 0x4D, 0xC8, 0x63, 0x11, 0x60, 0x60, 0x48, 0x1B, 0x8A, 0x58, 0x04, 0x9D, 0x13, 0x55, 0xB1, + 0x71, 0xC2, 0xB0, 0xB0, 0xAA, 0xB8, 0xBB, 0x75, 0x45, 0x45, 0x1C, 0x2E, 0x57, 0xC9, 0xDC, 0x5C, + 0xF9, 0xB3, 0x4F, 0x79, 0x8E, 0xD3, 0xE4, 0xB5, 0xB4, 0xB0, 0x70, 0xDA, 0x1C, 0x1C, 0xEC, 0x34, + 0x76, 0x36, 0xD4, 0x0B, 0x85, 0xC5, 0x9B, 0x36, 0x97, 0x5F, 0xBC, 0xC4, 0xA0, 0x0D, 0x3C, 0x1E, + 0x7F, 0xC9, 0x62, 0xD5, 0x99, 0x33, 0xE4, 0x38, 0x1C, 0x2C, 0x28, 0x4C, 0x33, 0x8C, 0xD6, 0x72, + 0xAC, 0xA2, 0xA2, 0x60, 0xDE, 0xFC, 0xEA, 0x87, 0xC9, 0x94, 0x72, 0xAE, 0xBD, 0xBD, 0xFA, 0xEA, + 0x55, 0x4A, 0x16, 0x03, 0x9A, 0x82, 0x09, 0x1C, 0x4D, 0x60, 0xA7, 0x11, 0xA3, 0xB5, 0x28, 0xF5, + 0xD9, 0x4B, 0xE7, 0x98, 0x92, 0x95, 0xA5, 0xF6, 0x81, 0x00, 0x8E, 0x92, 0x12, 0x96, 0x8F, 0x94, + 0x80, 0x3B, 0xAD, 0xCE, 0x64, 0xCA, 0xAA, 0xAB, 0xCB, 0x4E, 0x87, 0xD3, 0xCB, 0x79, 0xD3, 0xBF, + 0xC5, 0x1C, 0xC3, 0x34, 0xC3, 0x68, 0x1B, 0xD4, 0xE5, 0xE6, 0xD6, 0x97, 0x97, 0x33, 0x94, 0x17, + 0x16, 0x62, 0xE1, 0x60, 0x9A, 0x61, 0xB4, 0x51, 0x20, 0xCE, 0xE7, 0x33, 0x96, 0x0B, 0x8F, 0x1D, + 0xA7, 0x30, 0xAD, 0x36, 0x27, 0xA7, 0xF2, 0x4E, 0x14, 0x96, 0x18, 0xA6, 0x19, 0x46, 0xCB, 0x1F, + 0xB6, 0x86, 0x86, 0x92, 0x79, 0x7F, 0x06, 0x6B, 0x96, 0x97, 0x5F, 0x30, 0x7F, 0x41, 0x5D, 0x41, + 0xC1, 0xFB, 0x92, 0xA2, 0xA2, 0x42, 0x97, 0x45, 0xA5, 0x7B, 0x7C, 0xB0, 0xD0, 0x30, 0xCD, 0x30, + 0x5A, 0x0C, 0xD5, 0x39, 0x73, 0x18, 0xCB, 0x6B, 0x32, 0x33, 0xF3, 0xA6, 0x4E, 0x2B, 0x0B, 0x3F, + 0x53, 0x5F, 0x59, 0x09, 0xFF, 0x56, 0x34, 0xA6, 0xFB, 0x05, 0xC1, 0x87, 0x84, 0x27, 0x4F, 0x62, + 0xA1, 0xB5, 0x81, 0x1F, 0x81, 0x13, 0xFA, 0x9D, 0x2C, 0x3E, 0xAB, 0x2B, 0x74, 0x59, 0x5C, 0x19, + 0x1D, 0x2D, 0x52, 0x21, 0x78, 0x3C, 0x8E, 0x8A, 0x0A, 0xE1, 0x43, 0x72, 0x94, 0x95, 0x75, 0xFF, + 0x3A, 0xAB, 0x60, 0x60, 0x80, 0x25, 0x87, 0xAD, 0x19, 0x86, 0xE4, 0x0F, 0x5C, 0x5E, 0x73, 0xC7, + 0x36, 0x05, 0x23, 0x23, 0x51, 0xDF, 0xD7, 0x97, 0x95, 0x91, 0xE3, 0x34, 0x30, 0x6E, 0x82, 0x03, + 0x41, 0x58, 0x6C, 0x98, 0x66, 0x18, 0x2D, 0x7C, 0xE4, 0x5A, 0x5A, 0x3A, 0x87, 0x83, 0x59, 0x98, + 0x46, 0x41, 0xF9, 0xB9, 0xF3, 0x8C, 0xF9, 0x49, 0x0C, 0x4C, 0x33, 0x8C, 0x26, 0xD4, 0xD6, 0x56, + 0x25, 0x25, 0x95, 0xFF, 0x75, 0x0E, 0xD8, 0x52, 0x9D, 0x9A, 0x86, 0xCA, 0x14, 0xF4, 0xF4, 0x74, + 0x0E, 0x06, 0x49, 0x38, 0x7D, 0xB1, 0xBE, 0xA2, 0xA2, 0x32, 0x26, 0x06, 0x0B, 0x12, 0xD3, 0x0C, + 0x83, 0x91, 0x1F, 0xF5, 0xC2, 0x13, 0x27, 0x73, 0x47, 0x8F, 0x2D, 0x98, 0x39, 0xBB, 0xC8, 0xDD, + 0x43, 0x10, 0x1C, 0x5C, 0x5F, 0x5C, 0x4C, 0x7C, 0xA9, 0x60, 0x68, 0xA0, 0xEE, 0xBA, 0x4A, 0xC2, + 0x96, 0xAA, 0x93, 0x1E, 0x60, 0x71, 0x62, 0x9A, 0x61, 0x30, 0xA0, 0xF8, 0x97, 0xAD, 0x25, 0xDB, + 0xB6, 0xA3, 0x34, 0xBD, 0x42, 0x8F, 0x1E, 0x5D, 0x4F, 0x9E, 0xE0, 0x0E, 0x1D, 0x42, 0xAE, 0xA0, + 0x32, 0x66, 0x8C, 0x84, 0x4D, 0xD5, 0x64, 0x67, 0x63, 0x79, 0x62, 0x9A, 0x61, 0x50, 0x51, 0x19, + 0x19, 0x59, 0xF6, 0xFB, 0x1F, 0xC4, 0xBF, 0x5C, 0x2B, 0x4B, 0x8E, 0x8A, 0x0A, 0xA5, 0x4E, 0x9D, + 0xB0, 0x4C, 0xC2, 0xD6, 0xF0, 0x34, 0x11, 0x4C, 0x33, 0x0C, 0x06, 0x08, 0x4F, 0xFC, 0xB7, 0x19, + 0xEB, 0xA2, 0x63, 0xE8, 0x54, 0x11, 0x86, 0x84, 0x48, 0xDA, 0x9C, 0x02, 0xD6, 0x13, 0x4C, 0x33, + 0x0C, 0x1A, 0xAA, 0x12, 0x12, 0x9A, 0x99, 0xA3, 0xA2, 0xA2, 0x82, 0x39, 0xF3, 0x2A, 0xEF, 0x44, + 0xD5, 0x57, 0x57, 0x37, 0x38, 0x81, 0x4F, 0x9F, 0x16, 0x6F, 0xDE, 0x22, 0x3C, 0x7A, 0x4C, 0x52, + 0x96, 0xE9, 0xE8, 0x60, 0x91, 0x7E, 0x08, 0xF0, 0x42, 0x98, 0x8F, 0x10, 0x75, 0xF9, 0xF9, 0xF4, + 0x14, 0x7C, 0x4D, 0x56, 0x56, 0xA1, 0xCB, 0x22, 0x39, 0x45, 0x45, 0xB9, 0xDA, 0x5A, 0xB9, 0x16, + 0xCE, 0x49, 0x50, 0x34, 0x35, 0xC5, 0x52, 0xC5, 0x34, 0xC3, 0x68, 0x86, 0xFA, 0xAA, 0x2A, 0x91, + 0xDF, 0xD5, 0xD4, 0xB4, 0xA2, 0x41, 0xAE, 0xAD, 0x2D, 0x96, 0x2A, 0x76, 0x1A, 0x31, 0xA4, 0xA9, + 0x22, 0x5A, 0x5A, 0x5C, 0xFB, 0x41, 0x58, 0x0E, 0x98, 0x66, 0x18, 0x52, 0x04, 0x6F, 0xFA, 0xB7, + 0x78, 0xCF, 0x02, 0x4C, 0x33, 0x0C, 0x69, 0xEA, 0x07, 0x9F, 0xAF, 0xEA, 0xEC, 0x8C, 0xE5, 0x80, + 0x69, 0x86, 0x21, 0x45, 0xF0, 0x57, 0xAD, 0x94, 0xD7, 0xD2, 0xC4, 0x72, 0xC0, 0x34, 0xC3, 0x90, + 0x16, 0x54, 0x46, 0x7F, 0xCE, 0xFB, 0xC6, 0x11, 0xCB, 0x01, 0xD3, 0x0C, 0x43, 0x5A, 0x50, 0xB2, + 0xB2, 0xD4, 0xDC, 0xBA, 0x15, 0x6F, 0xD8, 0x88, 0x69, 0x86, 0x21, 0x35, 0x8E, 0x59, 0x5B, 0x6B, + 0xEF, 0xF7, 0xE7, 0xA8, 0xA9, 0x62, 0x51, 0x60, 0x9A, 0x61, 0x48, 0x01, 0x1C, 0x0E, 0xEF, 0x3B, + 0x27, 0x9D, 0x90, 0x43, 0xF2, 0x1A, 0x1A, 0x58, 0x18, 0x6D, 0x05, 0x3C, 0x3C, 0x8D, 0xF1, 0x1E, + 0xCA, 0xC3, 0x87, 0xF3, 0x17, 0xB9, 0x28, 0x59, 0x5B, 0x61, 0x51, 0x60, 0x9A, 0x61, 0x7C, 0xB0, + 0xC5, 0xEA, 0xD2, 0x85, 0xC8, 0x1F, 0xCA, 0x6B, 0x6A, 0x29, 0x1A, 0x1B, 0x29, 0xD9, 0xDA, 0xAA, + 0x7C, 0x3A, 0x52, 0xA1, 0x7B, 0x77, 0x2C, 0x1C, 0x4C, 0x33, 0x8C, 0x0F, 0x85, 0x82, 0x91, 0x91, + 0xC6, 0x4F, 0x6B, 0x95, 0x87, 0x3B, 0xC8, 0x29, 0x28, 0x60, 0x69, 0x60, 0x9A, 0x61, 0xB4, 0x3D, + 0xB8, 0x83, 0xED, 0xB5, 0xF7, 0xFA, 0x70, 0xD4, 0xD4, 0xB0, 0x28, 0x30, 0xCD, 0x30, 0xA4, 0xC3, + 0x31, 0x3B, 0x3B, 0x6D, 0x7F, 0x3F, 0xFA, 0xE2, 0x4E, 0x8C, 0x76, 0x00, 0xCE, 0x34, 0x76, 0x8E, + 0xDE, 0xD4, 0xCC, 0x4C, 0xDB, 0xCF, 0x17, 0x73, 0x0C, 0xD3, 0x0C, 0x43, 0x6A, 0xF1, 0x98, 0xA1, + 0xA1, 0x76, 0xC0, 0x7E, 0xEC, 0x2B, 0x62, 0x9A, 0x61, 0x48, 0xED, 0x01, 0x6B, 0x69, 0x6A, 0x1F, + 0x08, 0x50, 0xE8, 0xA6, 0x8B, 0x45, 0x81, 0x63, 0x33, 0x8C, 0xD6, 0xA0, 0xAE, 0xBE, 0xFE, 0xCA, + 0xB5, 0x9B, 0x9A, 0x9A, 0x1A, 0x43, 0x06, 0x31, 0x2F, 0xBB, 0xE4, 0x28, 0x2B, 0x6B, 0xEF, 0xDB, + 0xA7, 0x68, 0x62, 0x22, 0xB6, 0xA9, 0xCA, 0xCA, 0xAA, 0xC8, 0xA8, 0x98, 0xA4, 0x87, 0xC9, 0x39, + 0xB9, 0x6F, 0x05, 0x02, 0x21, 0x97, 0xCB, 0x55, 0x53, 0x55, 0x35, 0xD4, 0xD7, 0x5B, 0xE6, 0x32, + 0x1F, 0xCB, 0x19, 0xD3, 0xAC, 0xF3, 0x22, 0x21, 0xF1, 0xBE, 0xFF, 0xC1, 0xD0, 0xF4, 0xCC, 0x27, + 0xEE, 0xAE, 0xCB, 0x45, 0x18, 0x32, 0x79, 0x2D, 0x6F, 0x2F, 0x49, 0xC6, 0x9A, 0x2F, 0x5E, 0xB9, + 0xEE, 0x1F, 0x74, 0xF8, 0x5D, 0x51, 0x31, 0xA5, 0x5C, 0xBF, 0x5B, 0x37, 0x4C, 0x33, 0x4C, 0xB3, + 0x4E, 0x8A, 0x27, 0x59, 0xCF, 0xF6, 0x1F, 0x0C, 0x8D, 0x8D, 0xBF, 0xC7, 0x5E, 0x4D, 0x63, 0xE3, + 0x06, 0xE5, 0x11, 0x23, 0xC4, 0xB6, 0x16, 0x76, 0x26, 0xC2, 0x37, 0x20, 0x18, 0x4B, 0x15, 0xD3, + 0x0C, 0xE3, 0x3D, 0xBC, 0xFD, 0x02, 0xCF, 0xFC, 0xF5, 0xB7, 0xD8, 0xF7, 0xF8, 0xA8, 0xCE, 0x9D, + 0xC3, 0x9B, 0x36, 0x55, 0x6C, 0x6B, 0xD9, 0xAF, 0x73, 0xFC, 0x83, 0x42, 0xB0, 0x54, 0x31, 0xCD, + 0x30, 0x9A, 0x21, 0x2A, 0x26, 0x5E, 0x2C, 0xC7, 0xE4, 0x75, 0x75, 0xD5, 0x57, 0xAE, 0x90, 0xA4, + 0xB5, 0x33, 0x7F, 0x5D, 0xA8, 0xAD, 0xAD, 0xA5, 0x97, 0x6B, 0x69, 0x6A, 0x54, 0x55, 0x55, 0x63, + 0x69, 0x63, 0x9A, 0x61, 0x88, 0x84, 0xE4, 0xEF, 0x6B, 0x4F, 0xBC, 0xFF, 0x90, 0x52, 0x32, 0xA0, + 0x7F, 0xDF, 0x9D, 0x5B, 0xFE, 0xA3, 0xA1, 0xA1, 0x8E, 0xC5, 0xD8, 0x86, 0xC0, 0x09, 0xFD, 0x4E, + 0x8D, 0xBC, 0x7C, 0xEA, 0x56, 0xC4, 0x53, 0x26, 0x8D, 0xC7, 0x1C, 0xC3, 0x34, 0xC3, 0x68, 0x4B, + 0x54, 0xD7, 0x50, 0x3D, 0xC3, 0xAE, 0x5D, 0xF1, 0x06, 0xC3, 0xD8, 0x69, 0x6C, 0x78, 0xAB, 0x6B, + 0x5D, 0xD2, 0xC3, 0x47, 0x09, 0x89, 0xF7, 0xB3, 0x9E, 0xBF, 0xCC, 0x7D, 0x9B, 0x57, 0x2A, 0x10, + 0xD4, 0xD7, 0xD5, 0xAB, 0xA8, 0x28, 0x6B, 0x6A, 0x6A, 0x18, 0xF7, 0xE8, 0x6E, 0x63, 0x39, 0xE0, + 0x93, 0x21, 0xF6, 0xEA, 0x7C, 0x31, 0x33, 0x1E, 0xAA, 0xAB, 0xAB, 0x9F, 0xBD, 0xC8, 0x36, 0xEA, + 0x61, 0xA8, 0xA2, 0xAC, 0x0C, 0xA1, 0xCE, 0xDD, 0x7B, 0x49, 0x37, 0x22, 0xA3, 0xD2, 0x32, 0x9E, + 0x94, 0x94, 0x94, 0xCA, 0x2B, 0xC8, 0xEB, 0x68, 0x6B, 0xF5, 0x31, 0xED, 0x35, 0x6C, 0xA8, 0xFD, + 0x10, 0x7B, 0x3B, 0x79, 0x09, 0x56, 0xE9, 0x57, 0x54, 0x54, 0xC4, 0x27, 0x3E, 0x48, 0x7C, 0xF0, + 0xF0, 0x65, 0xF6, 0x2B, 0xB0, 0x0F, 0x02, 0x81, 0x10, 0x0A, 0x95, 0xB8, 0x8A, 0x5A, 0x9A, 0x9A, + 0xFA, 0x7A, 0xDD, 0xFA, 0x9B, 0xF5, 0x19, 0x3C, 0x68, 0x60, 0x4F, 0x63, 0x31, 0xAF, 0xED, 0x83, + 0x18, 0xE9, 0x45, 0xF6, 0x2B, 0x1D, 0x6D, 0x6D, 0x74, 0xF1, 0xE8, 0xAA, 0xAE, 0xDC, 0x88, 0xCC, + 0xC8, 0x7C, 0x0A, 0x0D, 0xDA, 0xDA, 0x58, 0xBA, 0xBB, 0xAD, 0x68, 0x09, 0x7F, 0x6A, 0x62, 0xEF, + 0x26, 0xD0, 0xA3, 0xB8, 0x01, 0xFD, 0xFB, 0xC1, 0xDD, 0x81, 0x0C, 0x63, 0xE3, 0x13, 0x6B, 0x6A, + 0xAA, 0x6B, 0x6B, 0xA8, 0x81, 0xD9, 0xE3, 0xD4, 0x74, 0xB8, 0x1D, 0x74, 0x6C, 0x6C, 0xD4, 0xC3, + 0xC4, 0xA8, 0xC7, 0xBD, 0xFB, 0x0F, 0x85, 0x42, 0x21, 0xB9, 0x4E, 0xDF, 0x3E, 0xBD, 0xF5, 0x44, + 0x8C, 0x77, 0x47, 0xC7, 0xC5, 0xD7, 0x34, 0xDF, 0x71, 0xD5, 0xDA, 0x62, 0x00, 0x32, 0x8F, 0xF0, + 0xB0, 0xD2, 0x33, 0x9F, 0x90, 0xBF, 0x42, 0xED, 0xA3, 0xAB, 0xBD, 0x7E, 0xEB, 0x4E, 0x54, 0x4C, + 0xDC, 0xD3, 0x67, 0x2F, 0xCA, 0xCB, 0x2B, 0x54, 0x55, 0x79, 0xF0, 0x34, 0xFB, 0x98, 0xF6, 0xFC, + 0x6C, 0xC4, 0x30, 0x4B, 0xA6, 0x77, 0xD2, 0xCB, 0x9E, 0x1B, 0x2F, 0x43, 0xEF, 0x9E, 0x86, 0x4B, + 0xFD, 0xFB, 0x9F, 0x6B, 0x21, 0xC7, 0x4F, 0xE5, 0xBC, 0xC9, 0x65, 0xA9, 0xC6, 0xE5, 0x72, 0x27, + 0x4F, 0xFC, 0x62, 0x8E, 0xF3, 0x77, 0x1A, 0xEA, 0x7C, 0x72, 0xF9, 0xBD, 0xA4, 0x07, 0xC9, 0x29, + 0x69, 0x4F, 0xB3, 0x9E, 0x65, 0x66, 0x3D, 0x7B, 0x99, 0xFD, 0x1A, 0x54, 0xED, 0xF7, 0x23, 0x0D, + 0xAF, 0x7B, 0xDD, 0xBC, 0xDD, 0x2B, 0xF9, 0x71, 0x2A, 0x63, 0x53, 0xC0, 0xC3, 0x55, 0x4B, 0x16, + 0x0E, 0xB6, 0x1B, 0x28, 0xEA, 0x5C, 0xC5, 0x25, 0xA5, 0xC7, 0xC3, 0x4E, 0x9F, 0x3D, 0x77, 0xB1, + 0x4C, 0xDC, 0xFB, 0x2C, 0x2D, 0xCC, 0xFB, 0x2D, 0x9A, 0x3F, 0xDB, 0xC6, 0xCA, 0x82, 0x5C, 0x98, + 0x92, 0x96, 0x91, 0xF4, 0x30, 0xF9, 0xC9, 0xD3, 0xE7, 0x4F, 0xB2, 0x9E, 0x3D, 0x7F, 0xF1, 0x12, + 0xB4, 0x6D, 0xEF, 0x2E, 0x4F, 0x5B, 0x6B, 0xCB, 0xEC, 0xD7, 0x39, 0x9E, 0x3B, 0xF7, 0x24, 0xA7, + 0xBC, 0xBF, 0x2A, 0xA0, 0xFD, 0xE1, 0xFD, 0x7B, 0xE0, 0xE0, 0xDB, 0x99, 0x0B, 0xDE, 0xBC, 0x7D, + 0x4B, 0x6E, 0xC4, 0xDD, 0x75, 0xF9, 0x84, 0x2F, 0x46, 0x53, 0x7A, 0x22, 0x8F, 0x2D, 0xDB, 0x6E, + 0x47, 0xC7, 0x51, 0xAE, 0x61, 0xEC, 0xA8, 0x91, 0x3F, 0xAF, 0x6D, 0x78, 0xA7, 0xD9, 0xD6, 0x9D, + 0xDE, 0xFF, 0x5C, 0xBF, 0x25, 0x56, 0xE0, 0x73, 0x67, 0x7C, 0x37, 0x6F, 0xE6, 0xBF, 0xE7, 0x2E, + 0x5A, 0x99, 0xF9, 0x34, 0x8B, 0xFD, 0x8C, 0x04, 0xC6, 0x4F, 0xFB, 0xB7, 0xB0, 0xF9, 0x8B, 0x66, + 0xD0, 0x1D, 0xC1, 0xC1, 0xDF, 0x97, 0xAF, 0xFE, 0xEA, 0xB5, 0x97, 0xDE, 0x3E, 0xF4, 0x26, 0x3B, + 0xBC, 0xF7, 0x01, 0x09, 0x19, 0x1B, 0xB4, 0xB3, 0xB1, 0xDA, 0xB0, 0xCE, 0x15, 0xBA, 0x06, 0x6C, + 0xCD, 0xDA, 0xC9, 0x88, 0x79, 0xEE, 0xDA, 0x73, 0xF9, 0xDA, 0x4D, 0xB1, 0x35, 0xAB, 0xAA, 0xAA, + 0xFE, 0xF8, 0xF3, 0x5C, 0x64, 0x54, 0xEC, 0x9E, 0x1D, 0xBF, 0xF4, 0x30, 0x7C, 0xFF, 0x6E, 0x72, + 0xDF, 0xC0, 0x43, 0x14, 0x75, 0x79, 0x9D, 0xF3, 0x66, 0xCB, 0x8E, 0xDD, 0x05, 0x85, 0xEF, 0x44, + 0x35, 0x05, 0x6C, 0x5C, 0xBD, 0x7E, 0x23, 0xD0, 0xE3, 0xFB, 0xE9, 0xD3, 0xE8, 0xDF, 0xA6, 0xA4, + 0x67, 0xAC, 0xDB, 0xE0, 0x59, 0xF8, 0xEE, 0x9D, 0x24, 0xD7, 0xFF, 0x28, 0x25, 0x6D, 0xD9, 0x1A, + 0x0F, 0xB7, 0xE5, 0x8B, 0x26, 0x4F, 0x1C, 0x47, 0x14, 0x1E, 0x39, 0xF9, 0xFB, 0xED, 0xE8, 0x58, + 0x4A, 0x4D, 0xE8, 0xF2, 0x97, 0xAF, 0xFD, 0x59, 0x28, 0xF1, 0x5B, 0x91, 0xE8, 0x82, 0x02, 0x16, + 0xD1, 0x39, 0x36, 0x6E, 0xF4, 0x28, 0x77, 0xB7, 0xE5, 0x92, 0x73, 0xAC, 0x7D, 0x70, 0xE9, 0xEA, + 0xF5, 0x5F, 0x77, 0xF9, 0xD4, 0x89, 0xEE, 0xEB, 0xC1, 0x96, 0x2E, 0x75, 0x5D, 0x1F, 0xE4, 0xEB, + 0xA5, 0x26, 0xCB, 0x1B, 0x93, 0xC8, 0x4C, 0x6C, 0x06, 0x1A, 0x29, 0x09, 0xC7, 0x08, 0x40, 0xEF, + 0xE8, 0xEA, 0xBE, 0x89, 0xF0, 0x7F, 0x18, 0x01, 0x9D, 0x2B, 0x0B, 0xC7, 0x08, 0xEC, 0x0F, 0x0E, + 0x3D, 0x7B, 0xFE, 0x12, 0xA5, 0xF0, 0x55, 0xCE, 0x9B, 0x55, 0xEB, 0x36, 0x48, 0xC8, 0x31, 0xC2, + 0x1A, 0x7B, 0xF9, 0x06, 0x80, 0xE1, 0x62, 0xA9, 0x53, 0x5A, 0x2A, 0x58, 0xB7, 0xD1, 0xB3, 0xD5, + 0x1C, 0x83, 0x53, 0x6C, 0xDF, 0xED, 0x4B, 0x67, 0xD1, 0x94, 0x49, 0xE3, 0x3D, 0xD6, 0xAC, 0xA8, + 0xAD, 0xAB, 0x83, 0x6E, 0xA5, 0xE3, 0x70, 0x2C, 0x35, 0x3D, 0x63, 0x9B, 0x97, 0x6F, 0x9D, 0x38, + 0x7F, 0x0A, 0x6C, 0x7B, 0xE0, 0xE1, 0xA3, 0xD8, 0x9A, 0x49, 0x1D, 0xC2, 0xB2, 0xB2, 0xE3, 0x61, + 0xE1, 0x94, 0x42, 0xD3, 0x9E, 0x26, 0xE3, 0xC7, 0x8E, 0x32, 0xEA, 0x6E, 0x08, 0xAE, 0x6F, 0x7E, + 0x61, 0x61, 0x74, 0x6C, 0x7C, 0x54, 0xEC, 0x5D, 0x8A, 0xB1, 0x3A, 0x15, 0x1E, 0x01, 0x96, 0x88, + 0x85, 0x8A, 0xC4, 0x31, 0xC4, 0x0F, 0xE0, 0x99, 0x00, 0xEB, 0x8A, 0x8B, 0x4B, 0xE8, 0x35, 0x7D, + 0xF6, 0x07, 0x81, 0xF7, 0x02, 0x3E, 0x24, 0x51, 0xE2, 0xBD, 0x2F, 0x90, 0x4E, 0x86, 0x21, 0x83, + 0x6C, 0x21, 0x9C, 0xE8, 0xAA, 0xA3, 0x5D, 0x59, 0x59, 0x05, 0x8D, 0x5F, 0xB8, 0x7C, 0xF5, 0xE9, + 0xB3, 0xE7, 0x14, 0x53, 0xF3, 0xC7, 0xD9, 0x73, 0x3F, 0xAD, 0x5C, 0x2A, 0xBA, 0x37, 0x09, 0xCB, + 0xCB, 0x2F, 0x68, 0x35, 0xC7, 0x76, 0xFB, 0x06, 0x80, 0x5F, 0x4D, 0x29, 0x9F, 0xFA, 0xD5, 0xC4, + 0x55, 0x4B, 0x7E, 0xE4, 0x70, 0x38, 0x7E, 0x07, 0x0E, 0x3F, 0x7A, 0x9C, 0xA6, 0xDF, 0xAD, 0xDB, + 0x7B, 0x09, 0xE4, 0xE5, 0x51, 0xA2, 0x06, 0x6D, 0x2D, 0x4D, 0xAE, 0x12, 0x17, 0x1D, 0x4B, 0xDB, + 0x80, 0x44, 0xC7, 0x25, 0x48, 0x58, 0x13, 0x24, 0xB9, 0x64, 0xC1, 0x1C, 0x15, 0x99, 0x5D, 0xC8, + 0x23, 0x1B, 0x34, 0x4B, 0xBC, 0xFF, 0x90, 0x62, 0x97, 0x40, 0x95, 0x03, 0xF7, 0xEE, 0x52, 0x51, + 0x56, 0x26, 0x77, 0xD8, 0xE1, 0x11, 0x17, 0xBC, 0xFD, 0x02, 0xC9, 0xD5, 0x2E, 0x5E, 0xB9, 0xCE, + 0x42, 0x33, 0xC2, 0xFB, 0x77, 0x99, 0x3F, 0xDB, 0xBC, 0x9F, 0x19, 0xFA, 0x17, 0xE2, 0x34, 0x50, + 0x47, 0x72, 0x5C, 0x84, 0x52, 0x26, 0x07, 0x42, 0x8E, 0xFE, 0xF2, 0xF3, 0x4F, 0x04, 0x81, 0xE9, + 0x73, 0x9D, 0xE6, 0x38, 0x3B, 0xCD, 0x9F, 0xF5, 0x3D, 0xB9, 0x64, 0xDA, 0xD7, 0x93, 0x16, 0xAF, + 0xFA, 0x29, 0x35, 0x3D, 0x93, 0x5C, 0x98, 0xFC, 0x28, 0x95, 0xE5, 0x62, 0xD2, 0x32, 0x9A, 0x25, + 0x09, 0x94, 0x14, 0x15, 0x35, 0x1B, 0x46, 0x8A, 0xAB, 0x24, 0x91, 0xD2, 0xBE, 0xC0, 0x43, 0x7F, + 0x9E, 0xBF, 0x48, 0x29, 0x74, 0x9E, 0xEE, 0xE8, 0x32, 0x7F, 0x16, 0x3A, 0x5E, 0xB9, 0x78, 0x01, + 0x7C, 0xD8, 0x43, 0xA9, 0x4D, 0xEE, 0x6B, 0x50, 0x28, 0xD5, 0x6E, 0xE8, 0xA6, 0xDB, 0x15, 0xE4, + 0x06, 0x3D, 0x14, 0xC4, 0xD2, 0x45, 0xC5, 0x25, 0xD7, 0x6E, 0xDE, 0x0E, 0x0A, 0x3D, 0x06, 0xFD, + 0x14, 0x25, 0x10, 0x88, 0x4F, 0x7C, 0x30, 0xFC, 0x5F, 0x43, 0x30, 0xCD, 0xA4, 0x88, 0x67, 0x2F, + 0xA8, 0xEF, 0x3E, 0xD6, 0xD7, 0xEB, 0x46, 0xE6, 0x58, 0x53, 0xB7, 0x3D, 0xE1, 0xD8, 0xA9, 0x3F, + 0xC8, 0xD6, 0x20, 0xFB, 0xD5, 0xEB, 0x77, 0x45, 0xC5, 0x5A, 0x9A, 0x22, 0xF7, 0x42, 0xB3, 0xB1, + 0xB2, 0xF8, 0xED, 0xD7, 0x4D, 0xA0, 0xCD, 0x44, 0x89, 0xE5, 0x80, 0xFE, 0x7B, 0x7F, 0xF3, 0x5C, + 0xE6, 0xE6, 0x0E, 0xD1, 0x14, 0xB9, 0xE6, 0xAD, 0xDB, 0xD1, 0x6F, 0xF3, 0xF2, 0x41, 0x27, 0x1A, + 0xB3, 0x29, 0xD4, 0x51, 0x5D, 0x30, 0x86, 0xB3, 0xBF, 0x77, 0xA2, 0x14, 0x42, 0xB3, 0x93, 0xC6, + 0x8D, 0xA1, 0xD0, 0xAC, 0xB8, 0xA4, 0x44, 0xBC, 0x2B, 0xCF, 0xE1, 0x4C, 0xF9, 0x6A, 0x02, 0xFC, + 0xB6, 0x6F, 0x9F, 0xDE, 0x44, 0xE6, 0x90, 0xFD, 0x27, 0x01, 0xC1, 0x47, 0xC2, 0xCE, 0x44, 0x50, + 0x0A, 0x67, 0x38, 0x39, 0x2E, 0x9C, 0x37, 0xAB, 0x23, 0x3F, 0x59, 0x78, 0x34, 0x01, 0x7B, 0x76, + 0xEA, 0x36, 0x8D, 0x22, 0xC0, 0x81, 0x93, 0xE3, 0xD7, 0xEA, 0xEA, 0xFC, 0x5F, 0x7F, 0xF3, 0xA1, + 0xD4, 0x84, 0xB8, 0x1A, 0xD3, 0x4C, 0xEA, 0xF9, 0x0F, 0x6A, 0xFA, 0x21, 0x2D, 0x03, 0xEC, 0xC9, + 0x50, 0x7B, 0x3B, 0x72, 0x21, 0xF8, 0x45, 0x9B, 0xD6, 0xBB, 0x51, 0xF4, 0x98, 0x2B, 0x7A, 0x4A, + 0x84, 0xBC, 0xBC, 0xFC, 0xBA, 0xD5, 0xCB, 0xC8, 0x1C, 0x23, 0xE8, 0xE1, 0xE1, 0xB6, 0x72, 0xC6, + 0x82, 0x25, 0xE4, 0xF3, 0x42, 0x08, 0x71, 0xF3, 0x76, 0xF4, 0xB7, 0x53, 0xBF, 0x82, 0x63, 0xD0, + 0x83, 0x09, 0x63, 0x3F, 0x27, 0xFF, 0xA4, 0xA7, 0xB1, 0x91, 0xA2, 0x22, 0xC3, 0x26, 0x36, 0xEA, + 0xCD, 0xB3, 0x9D, 0x0D, 0x1D, 0x73, 0x75, 0xB5, 0x58, 0x8E, 0x79, 0x6E, 0x74, 0xA7, 0xA8, 0x14, + 0xFD, 0x22, 0xC9, 0x08, 0x3D, 0x11, 0x76, 0x3C, 0xEC, 0x34, 0xA5, 0x10, 0x08, 0x06, 0x34, 0xEB, + 0xE0, 0x4F, 0x76, 0x86, 0xD3, 0x37, 0xBA, 0xB4, 0x91, 0xBA, 0xF1, 0x63, 0x46, 0x05, 0x1E, 0x3A, + 0x42, 0x09, 0x9B, 0xDF, 0xE4, 0xBE, 0xC5, 0xB1, 0x99, 0xD4, 0xFD, 0x0A, 0x4A, 0x49, 0x6D, 0x6D, + 0xAD, 0x9B, 0xC7, 0xE6, 0x7F, 0x0D, 0xB1, 0x87, 0x58, 0x68, 0xD0, 0x40, 0x6B, 0x62, 0x18, 0xC7, + 0xDA, 0x72, 0x80, 0xE4, 0xCD, 0x82, 0xA3, 0x42, 0x4E, 0x45, 0x92, 0x01, 0x61, 0xD8, 0x50, 0x7B, + 0x5B, 0x4A, 0xF0, 0xF0, 0x28, 0x25, 0x15, 0xD1, 0x6C, 0xA4, 0xC3, 0x27, 0xF0, 0x11, 0xDB, 0x7E, + 0x59, 0x79, 0x79, 0xC4, 0x85, 0xCB, 0x2D, 0xBD, 0xD9, 0xA9, 0x93, 0x27, 0xB5, 0xA8, 0xDB, 0x3E, + 0x73, 0xEE, 0x6F, 0xE8, 0x74, 0x64, 0x91, 0x63, 0x48, 0x92, 0xF4, 0x42, 0xE8, 0x2E, 0x2D, 0xCD, + 0xFB, 0xDF, 0xBC, 0x13, 0x4D, 0x89, 0xCF, 0x31, 0xCD, 0xA4, 0x8B, 0x61, 0x43, 0x07, 0x73, 0xB9, + 0x5C, 0x7A, 0x88, 0x12, 0x1D, 0x17, 0x0F, 0x1F, 0xE4, 0x43, 0x5A, 0x98, 0xF7, 0xB3, 0xE8, 0xDF, + 0x6F, 0xA0, 0xB5, 0x65, 0x6F, 0xD3, 0x9E, 0xF2, 0x92, 0xED, 0xFC, 0x6E, 0x6F, 0x6B, 0xC3, 0xF2, + 0x2D, 0x98, 0x4A, 0x0A, 0xCD, 0x28, 0x81, 0x13, 0x23, 0x0A, 0xDF, 0xBD, 0xCB, 0x7E, 0x95, 0xF3, + 0xFC, 0x65, 0x76, 0x7A, 0xC6, 0x13, 0x50, 0x14, 0xFA, 0x22, 0x2E, 0xB1, 0xF8, 0xF7, 0x37, 0x53, + 0x5A, 0x54, 0x9F, 0xCE, 0x31, 0x40, 0x77, 0x03, 0xFD, 0x8E, 0xFF, 0x58, 0x79, 0x5D, 0xBA, 0xC0, + 0x83, 0x63, 0xFC, 0x4A, 0x57, 0x97, 0x6A, 0xE2, 0x6A, 0x6A, 0x6A, 0x31, 0xCD, 0xA4, 0x0B, 0x75, + 0xBE, 0xDA, 0xB2, 0x85, 0xF3, 0xBC, 0x7C, 0x03, 0x44, 0x55, 0x00, 0x8F, 0x02, 0x3E, 0x57, 0x6F, + 0x44, 0xC2, 0xB1, 0x8E, 0xB6, 0xD6, 0x67, 0x23, 0x1C, 0x1C, 0x27, 0x4F, 0x22, 0x27, 0x06, 0x19, + 0x61, 0xDC, 0x38, 0x05, 0x41, 0x14, 0x7A, 0x99, 0x18, 0x53, 0x4A, 0xF2, 0x0A, 0xA8, 0x39, 0xC0, + 0xFA, 0xFA, 0xFA, 0xC7, 0xA9, 0xE9, 0xF1, 0x89, 0xF7, 0x53, 0xD3, 0x33, 0x20, 0x80, 0xCC, 0xCD, + 0x7D, 0x5B, 0xDD, 0xAA, 0xB7, 0xCE, 0x12, 0x30, 0xED, 0x65, 0xA2, 0xD7, 0x16, 0x1B, 0x0A, 0x78, + 0xED, 0x0B, 0xB0, 0xB5, 0xB1, 0xD4, 0xEC, 0xD8, 0x1B, 0x74, 0xAB, 0xF3, 0xF9, 0x2C, 0x0C, 0x94, + 0xFB, 0x88, 0x20, 0x33, 0xC3, 0xD3, 0x53, 0xBE, 0x9C, 0xA0, 0xA1, 0xAE, 0xBE, 0xC7, 0x3F, 0x48, + 0xEC, 0x50, 0x15, 0xF8, 0xF4, 0xA7, 0xCF, 0x9E, 0x3B, 0xF3, 0xD7, 0x85, 0xEF, 0xBE, 0x99, 0xB2, + 0x70, 0xEE, 0x4C, 0x79, 0xD1, 0x6F, 0x9A, 0xE4, 0xF3, 0xD9, 0x12, 0xD6, 0x7C, 0x9A, 0x12, 0x54, + 0x56, 0x42, 0x60, 0x55, 0xAD, 0xD4, 0x14, 0xEC, 0xDD, 0x88, 0x8C, 0x3A, 0x10, 0x72, 0xF4, 0x65, + 0xF6, 0xEB, 0x36, 0xBC, 0x4D, 0x3A, 0xB7, 0x5B, 0x87, 0xE2, 0xE2, 0x12, 0xE8, 0x95, 0x88, 0xD4, + 0x68, 0x07, 0xB5, 0x66, 0xAA, 0x22, 0xB9, 0xA4, 0xF0, 0x71, 0xED, 0xD6, 0x2A, 0x4B, 0x53, 0x87, + 0x47, 0x8D, 0x74, 0x38, 0x15, 0x12, 0xF0, 0xD3, 0xAA, 0xA5, 0x83, 0xED, 0x06, 0x82, 0x0F, 0x29, + 0x36, 0x6B, 0x72, 0x22, 0x2C, 0x7C, 0xCB, 0x76, 0x2F, 0xB6, 0x4A, 0x2D, 0x9F, 0x67, 0x56, 0xDB, + 0x94, 0x14, 0x01, 0xC2, 0xFF, 0x67, 0xEB, 0x0E, 0x16, 0x8E, 0x01, 0xBD, 0x21, 0xC0, 0x98, 0x34, + 0x6E, 0x4C, 0x8B, 0xDA, 0xD7, 0x54, 0x6F, 0xB3, 0xD9, 0xF1, 0xD0, 0x0B, 0x5C, 0xBF, 0x75, 0xA7, + 0x9D, 0x9F, 0x11, 0x3D, 0x59, 0xC5, 0x9A, 0xEC, 0xE9, 0x2C, 0x33, 0xD7, 0x65, 0x6C, 0xEA, 0xB0, + 0x8A, 0x8A, 0xCA, 0x97, 0xE3, 0xC7, 0xC2, 0x07, 0xE2, 0xB4, 0x07, 0x8F, 0x52, 0x1E, 0x24, 0x3F, + 0x4E, 0x7E, 0x9C, 0xFA, 0x28, 0x25, 0x4D, 0xD4, 0x94, 0xC2, 0xAB, 0x37, 0x6F, 0x4F, 0xFC, 0x62, + 0xCC, 0x10, 0x7B, 0xE6, 0x1D, 0x69, 0x8A, 0x8A, 0xD9, 0x72, 0xEB, 0xA5, 0x02, 0x01, 0xA5, 0x44, + 0x49, 0x51, 0x11, 0x8D, 0x22, 0x5C, 0xBE, 0x76, 0x13, 0x0C, 0x26, 0x3D, 0x70, 0xB7, 0xB6, 0x30, + 0xB7, 0xB5, 0xB1, 0xEA, 0x69, 0x62, 0x6C, 0xDC, 0xA3, 0x3B, 0x9A, 0x97, 0x1C, 0x19, 0x15, 0x73, + 0xFE, 0xD2, 0x95, 0x16, 0x68, 0x9E, 0x42, 0x6B, 0x34, 0x0F, 0x62, 0xD1, 0x1F, 0xE7, 0xCD, 0x0C, + 0x39, 0x76, 0xAA, 0xA2, 0xB2, 0xB2, 0x1D, 0x5C, 0xC7, 0x4A, 0xD1, 0xE3, 0x78, 0xE5, 0xE5, 0x15, + 0x72, 0x18, 0xB2, 0x4E, 0x33, 0x02, 0x60, 0xCD, 0xEC, 0x6D, 0x6D, 0x50, 0x0E, 0x03, 0x7A, 0x50, + 0x20, 0xDB, 0xE5, 0x6B, 0x37, 0xFE, 0xFE, 0xE7, 0x3A, 0x3D, 0x4D, 0x72, 0xE5, 0xC6, 0x2D, 0x51, + 0x34, 0xCB, 0x7A, 0xF6, 0x9C, 0x25, 0xA7, 0x97, 0xF5, 0xFC, 0x05, 0xA5, 0x44, 0xBB, 0x69, 0x02, + 0xEB, 0xF1, 0x53, 0xD4, 0xEC, 0xB9, 0xA1, 0x81, 0xFE, 0xB6, 0x8D, 0xEE, 0x10, 0x59, 0x7D, 0x48, + 0xEF, 0xDE, 0xCA, 0x47, 0xA8, 0xA8, 0xE0, 0xE1, 0xB6, 0x72, 0xCC, 0xA8, 0x91, 0x1C, 0x39, 0xCE, + 0xFE, 0xE0, 0xD0, 0x76, 0x70, 0x1D, 0xCB, 0x45, 0x4C, 0x61, 0x93, 0x64, 0xE6, 0x5A, 0xE7, 0x84, + 0x6C, 0x58, 0xED, 0xC4, 0x07, 0xC9, 0xE4, 0x0F, 0x65, 0xE0, 0x18, 0xDC, 0x33, 0x6B, 0xCB, 0x01, + 0x6E, 0xCB, 0x17, 0x1F, 0x3D, 0xE0, 0x4B, 0x1F, 0x84, 0x79, 0x95, 0xF3, 0x46, 0x54, 0xB3, 0xD1, + 0x77, 0xD9, 0x26, 0xFB, 0xC4, 0xD2, 0xBE, 0x1D, 0xD0, 0xBF, 0x2F, 0xFC, 0x2D, 0x29, 0x15, 0x50, + 0xA6, 0x50, 0x01, 0xC0, 0x95, 0xA5, 0x73, 0x4C, 0xAE, 0x71, 0x0A, 0xBF, 0xB4, 0x85, 0xF3, 0xE3, + 0x9C, 0x99, 0xC0, 0x31, 0x38, 0x70, 0x72, 0xFC, 0xDA, 0xB4, 0xA7, 0x49, 0x9B, 0xBB, 0x8E, 0x5C, + 0x2E, 0x75, 0xE0, 0x31, 0xE7, 0x0D, 0xF3, 0x10, 0x16, 0x65, 0x9D, 0x0B, 0x86, 0x8C, 0x59, 0x33, + 0x57, 0xF7, 0x4D, 0xD5, 0xA4, 0x51, 0x5D, 0x30, 0x65, 0xFF, 0x9C, 0x3D, 0x45, 0xCF, 0x6D, 0x80, + 0x49, 0x99, 0xF2, 0xE5, 0x84, 0xA0, 0x90, 0x63, 0xCD, 0xE2, 0x2F, 0xD1, 0x33, 0x53, 0x1F, 0x3E, + 0x4A, 0x49, 0x4E, 0x49, 0x65, 0x5C, 0xD1, 0x04, 0xA6, 0x2C, 0xE6, 0x2E, 0x75, 0x3A, 0x95, 0xE5, + 0x80, 0x86, 0x9A, 0xF9, 0x4C, 0x73, 0x0E, 0xAD, 0x2D, 0x98, 0xC7, 0xEB, 0x52, 0xD2, 0x33, 0xA4, + 0x2D, 0x1C, 0xCD, 0xA6, 0xC5, 0xCE, 0x0A, 0x0A, 0x0A, 0x6E, 0xCB, 0x17, 0x2D, 0x5E, 0xBD, 0x8E, + 0x52, 0xE1, 0x03, 0x5D, 0x47, 0x55, 0x1E, 0x8F, 0x52, 0x92, 0x90, 0x78, 0x1F, 0xA4, 0xCA, 0xA1, + 0x8D, 0x9A, 0xB4, 0x68, 0x6E, 0x37, 0xB6, 0x66, 0x1D, 0x0E, 0x3D, 0x8D, 0x9B, 0x65, 0xDE, 0xC1, + 0x33, 0xBC, 0x71, 0x3B, 0x8A, 0xB1, 0x66, 0x7E, 0x01, 0x75, 0xD5, 0x3D, 0x7B, 0x7E, 0x7C, 0xEB, + 0x0E, 0xEF, 0xA2, 0x62, 0xEA, 0xD0, 0x16, 0x44, 0x7A, 0x5B, 0x77, 0xEE, 0xA1, 0xF0, 0x13, 0x58, + 0x3D, 0x6A, 0x84, 0x83, 0xA8, 0x76, 0x18, 0xF3, 0x9F, 0x70, 0x31, 0x68, 0x8C, 0xA1, 0xDD, 0x60, + 0x65, 0x61, 0x0E, 0x81, 0x2B, 0xD5, 0xA2, 0x36, 0xBA, 0x8E, 0xAD, 0x6E, 0x93, 0x3E, 0x2E, 0xF2, + 0x22, 0xFB, 0xD5, 0x45, 0xDA, 0x04, 0xE5, 0xDB, 0xD1, 0xB1, 0xED, 0x76, 0xB3, 0x99, 0x4F, 0xB3, + 0xFC, 0x83, 0x42, 0xF6, 0xF8, 0x1D, 0x88, 0x4F, 0xBC, 0x8F, 0x69, 0xD6, 0x66, 0x70, 0xF8, 0x84, + 0x1A, 0x41, 0xED, 0xF4, 0xF6, 0x8B, 0xA1, 0x39, 0x75, 0xD1, 0x71, 0xF1, 0xE7, 0x2E, 0xFE, 0x43, + 0x29, 0x1C, 0x64, 0x63, 0xCD, 0xD2, 0x32, 0xB8, 0x94, 0xF3, 0x16, 0xAF, 0xFA, 0xF3, 0xFC, 0xC5, + 0xBC, 0xFC, 0x02, 0x88, 0xA3, 0x20, 0xBA, 0xB8, 0x78, 0xE5, 0xFA, 0x82, 0xA5, 0xAE, 0x74, 0xFF, + 0x67, 0xF4, 0xA7, 0xC3, 0x91, 0x47, 0xCA, 0xB8, 0x8C, 0x3F, 0xF0, 0xF0, 0x51, 0x4A, 0x18, 0x96, + 0xFB, 0x36, 0x6F, 0xFD, 0x26, 0xCF, 0xF6, 0x4F, 0x09, 0xB8, 0xCC, 0x9F, 0x4D, 0xDF, 0xCC, 0xE3, + 0x43, 0x5C, 0x47, 0x62, 0x5E, 0x25, 0x19, 0xDB, 0xBD, 0xF7, 0x79, 0xFB, 0x05, 0xC6, 0xC6, 0xDF, + 0x03, 0x07, 0xFE, 0xE6, 0x9D, 0xE8, 0x5F, 0x7F, 0xF3, 0xF1, 0xD8, 0xB2, 0xBD, 0x7D, 0x96, 0x08, + 0xC7, 0x25, 0x24, 0xFA, 0xEC, 0x3F, 0xC8, 0xE3, 0x75, 0xE9, 0x6E, 0x68, 0x10, 0x71, 0xE1, 0xD2, + 0xE9, 0x88, 0xF3, 0xD8, 0x69, 0x6C, 0x1B, 0x80, 0x2B, 0x18, 0x16, 0x1E, 0x41, 0x4E, 0x27, 0x0A, + 0xCB, 0xCA, 0xD6, 0xFC, 0xBC, 0xC5, 0xAC, 0x77, 0x2F, 0x5B, 0x6B, 0x2B, 0x3E, 0x5F, 0xAD, 0xB4, + 0x54, 0x00, 0xEE, 0xDF, 0xE3, 0xD4, 0x74, 0x9A, 0x43, 0xA5, 0x31, 0xFA, 0x33, 0x31, 0x5B, 0x82, + 0x02, 0xC1, 0xBC, 0xF6, 0xEE, 0xF7, 0x92, 0xDB, 0xCF, 0x52, 0xA7, 0x4B, 0x17, 0x95, 0x1F, 0xE7, + 0xCE, 0x44, 0xC7, 0xEA, 0x7C, 0x35, 0xF3, 0xBE, 0x66, 0x14, 0x6F, 0xF0, 0xF2, 0xD5, 0x1B, 0xE0, + 0x82, 0x0E, 0x1D, 0x64, 0xAB, 0xAD, 0xAD, 0x55, 0x5E, 0x51, 0xF1, 0xFC, 0x45, 0xF6, 0xDD, 0x84, + 0x44, 0xC6, 0xA1, 0xEA, 0xDA, 0x5A, 0xE9, 0x26, 0x45, 0x34, 0xD4, 0xF9, 0x4B, 0x7E, 0x98, 0x43, + 0x59, 0xA7, 0xFC, 0x21, 0xAE, 0xA3, 0xC3, 0x27, 0x83, 0xC1, 0x92, 0x53, 0x3A, 0x11, 0xF8, 0x37, + 0x3C, 0xE2, 0x02, 0x7C, 0xDA, 0x5F, 0x19, 0x8E, 0xFE, 0xF7, 0x8F, 0x25, 0x0B, 0xE6, 0x06, 0x85, + 0x1E, 0x33, 0x35, 0x31, 0xB1, 0xB7, 0xB5, 0x39, 0x72, 0xF2, 0x77, 0xC7, 0xC9, 0x93, 0xB0, 0x35, + 0x6B, 0x03, 0xE8, 0x68, 0x6B, 0xAD, 0x59, 0xB1, 0x98, 0x5E, 0x9E, 0xF1, 0x24, 0x2B, 0xEC, 0x4C, + 0x44, 0xF0, 0x91, 0x13, 0xF0, 0x97, 0xCE, 0x31, 0x79, 0x0E, 0x67, 0xDD, 0xEA, 0x65, 0xC0, 0x10, + 0x51, 0xCD, 0xD2, 0x13, 0x06, 0xA2, 0x00, 0xED, 0x90, 0xA7, 0x05, 0x7D, 0xD3, 0x38, 0xB3, 0x91, + 0x96, 0x18, 0xC8, 0x05, 0xAB, 0x78, 0xE8, 0xE8, 0xC9, 0x93, 0xBF, 0x9F, 0x89, 0x8A, 0xBD, 0x2B, + 0x6A, 0x3A, 0x48, 0x45, 0x45, 0x05, 0x65, 0x95, 0x47, 0x9B, 0x63, 0xFC, 0xD8, 0xCF, 0x6D, 0x68, + 0x73, 0x3B, 0x5B, 0xED, 0x3A, 0x02, 0x33, 0x27, 0x8E, 0x1B, 0x2D, 0x49, 0x4D, 0x03, 0x7D, 0x3D, + 0x49, 0xA6, 0x7A, 0x7E, 0x20, 0x8A, 0x8A, 0x8A, 0xBB, 0xEA, 0x68, 0x17, 0x16, 0x16, 0x81, 0x59, + 0xFB, 0x74, 0xF8, 0x30, 0x99, 0x18, 0xC8, 0x96, 0x99, 0xF1, 0xC1, 0x31, 0xA3, 0x46, 0x6E, 0x5C, + 0xE7, 0xCA, 0xC2, 0x19, 0x0A, 0x78, 0x5D, 0xBA, 0x6C, 0xF6, 0x58, 0x0B, 0x3D, 0x31, 0x4B, 0x9D, + 0x49, 0xE3, 0xC7, 0x88, 0x9D, 0x5F, 0xAB, 0xA4, 0xA8, 0xE8, 0xEE, 0xB6, 0xE2, 0xF3, 0x91, 0xC3, + 0xC9, 0x85, 0x5F, 0x7C, 0xFE, 0xE9, 0x97, 0x13, 0xC6, 0x8A, 0xBD, 0x06, 0xF8, 0xAD, 0xEB, 0xF2, + 0x45, 0xF4, 0xC9, 0xF5, 0xCF, 0x5F, 0xBE, 0x94, 0xAA, 0xAC, 0x38, 0x1C, 0x8E, 0xDB, 0x8A, 0xC5, + 0x74, 0xFD, 0x6B, 0xB5, 0xEB, 0xB8, 0x74, 0xC1, 0x3C, 0xB1, 0x5D, 0x12, 0x78, 0xD4, 0xBB, 0xB7, + 0x6D, 0xA6, 0xE7, 0x4B, 0xDA, 0x1C, 0x43, 0x06, 0xD9, 0xC2, 0x8D, 0x80, 0x1D, 0x9B, 0xF8, 0xC5, + 0xE8, 0xC8, 0x3B, 0x31, 0xA2, 0x26, 0x7F, 0x63, 0x9A, 0xB5, 0x9E, 0x69, 0xC7, 0x0F, 0xFA, 0x4F, + 0xFD, 0x6A, 0x22, 0x9F, 0xF5, 0x55, 0x5D, 0x6A, 0x6A, 0xAA, 0x50, 0xE7, 0xD8, 0x41, 0xBF, 0xCF, + 0x46, 0x0C, 0x13, 0xDB, 0xE6, 0xC2, 0x79, 0xB3, 0x36, 0xB9, 0xAF, 0xD1, 0x15, 0xB1, 0x6B, 0x9A, + 0xDD, 0x40, 0xEB, 0x03, 0xBE, 0xBF, 0x51, 0xD6, 0xBC, 0x20, 0xAC, 0x5D, 0xB1, 0x04, 0x28, 0xA4, + 0xA1, 0xCE, 0x3C, 0x2B, 0x0F, 0x0C, 0x29, 0xF4, 0xEB, 0x07, 0xFD, 0x76, 0x4F, 0x99, 0x34, 0xBE, + 0x9F, 0x59, 0x1F, 0xCA, 0xB7, 0x77, 0xEF, 0x25, 0x49, 0x3F, 0x69, 0x64, 0xF4, 0xFD, 0xB7, 0x0C, + 0x9B, 0x7B, 0x83, 0xEB, 0x48, 0x4F, 0xF9, 0x88, 0x85, 0xAA, 0x2A, 0xCF, 0xCF, 0x6B, 0x1B, 0xC8, + 0x81, 0x71, 0xE6, 0x1A, 0xB0, 0x1A, 0x1E, 0x4D, 0xB0, 0x9F, 0x77, 0xFB, 0x68, 0xFC, 0x82, 0xB9, + 0x33, 0x52, 0xD2, 0xD2, 0x5F, 0xBE, 0x7A, 0x05, 0x7F, 0x6F, 0x45, 0xC5, 0x80, 0xA3, 0xD1, 0xF1, + 0x55, 0x57, 0x96, 0x76, 0xB6, 0x22, 0x07, 0x06, 0x0F, 0x1F, 0xA7, 0xA6, 0x65, 0x64, 0xBE, 0xCC, + 0x7E, 0x2D, 0x10, 0x0A, 0xC1, 0x0B, 0x53, 0x56, 0xE6, 0xAA, 0xA9, 0xF2, 0x8C, 0x8D, 0x7A, 0xF4, + 0xED, 0x6D, 0x6A, 0x65, 0x61, 0xCE, 0xE8, 0x48, 0xD0, 0x77, 0x68, 0x5A, 0xE6, 0x32, 0x7F, 0xFA, + 0xD4, 0xC9, 0x8D, 0xF1, 0x52, 0x2D, 0x78, 0x20, 0x89, 0x0F, 0x92, 0xC1, 0xF1, 0xAB, 0xA9, 0xA9, + 0xD5, 0xD4, 0x50, 0xEF, 0x65, 0x62, 0x3C, 0xD4, 0xDE, 0xCE, 0xC4, 0xB8, 0x07, 0xFB, 0x95, 0x54, + 0x57, 0x57, 0xC7, 0x25, 0x24, 0x41, 0x54, 0xF6, 0x36, 0x2F, 0xAF, 0xBC, 0xA2, 0x12, 0xFA, 0x72, + 0x6D, 0x2D, 0xCD, 0x3E, 0xA6, 0x3D, 0x07, 0xD9, 0xDA, 0x10, 0x9B, 0x31, 0x81, 0xB7, 0x46, 0x19, + 0xCF, 0x55, 0x56, 0x56, 0x46, 0x2B, 0x4D, 0xDF, 0x15, 0x15, 0x57, 0x36, 0x9F, 0xB7, 0xC1, 0xE3, + 0xF1, 0xD8, 0x77, 0xBF, 0x83, 0x48, 0x92, 0xB2, 0x1D, 0xB7, 0x86, 0xBA, 0x3A, 0xA3, 0x91, 0x07, + 0xAF, 0xB5, 0x80, 0x96, 0x77, 0xA5, 0xD4, 0xCF, 0x7D, 0xCB, 0xB4, 0x49, 0x81, 0xE8, 0x89, 0x6C, + 0xF9, 0x05, 0x85, 0x31, 0x71, 0x09, 0x59, 0xCF, 0x5F, 0x14, 0x95, 0x94, 0x94, 0x95, 0x95, 0x43, + 0x54, 0x6C, 0x66, 0xDA, 0x6B, 0xC4, 0xB0, 0xA1, 0x84, 0x47, 0x4D, 0xBF, 0x59, 0xA2, 0xC1, 0xF2, + 0xF2, 0x0A, 0xCA, 0x52, 0x40, 0x45, 0x45, 0x45, 0x70, 0xFF, 0x18, 0x4F, 0x24, 0x10, 0x08, 0x05, + 0xCD, 0x77, 0xAD, 0x23, 0x84, 0x86, 0x00, 0x8E, 0x77, 0x4D, 0x4D, 0x0D, 0xF0, 0x5F, 0x26, 0x34, + 0x56, 0x26, 0x69, 0xD6, 0x3A, 0xB0, 0xD0, 0x0C, 0x03, 0x03, 0x3B, 0x8D, 0x18, 0x18, 0x98, 0x66, + 0x18, 0x18, 0x18, 0x98, 0x66, 0x1F, 0x2B, 0x22, 0x2E, 0x5C, 0x5A, 0xB5, 0x6E, 0x83, 0x24, 0x6B, + 0xBA, 0xE5, 0x1A, 0x97, 0x7E, 0x43, 0x65, 0xF8, 0x09, 0x96, 0x5B, 0xFB, 0x03, 0xBF, 0x78, 0xA9, + 0x19, 0xE2, 0x13, 0xEF, 0x83, 0x2E, 0x52, 0x0A, 0x0D, 0x0D, 0xF4, 0xED, 0x6D, 0x6D, 0x9C, 0xA7, + 0x4F, 0x83, 0x83, 0xE3, 0x61, 0xA7, 0x03, 0x82, 0x8F, 0x88, 0xFA, 0xB9, 0xF7, 0xF6, 0x2D, 0xF0, + 0x97, 0x68, 0x21, 0xF2, 0xD2, 0x59, 0x74, 0xB0, 0xCB, 0xC7, 0x9F, 0xD0, 0xEF, 0x35, 0x2B, 0x16, + 0x93, 0x37, 0x1E, 0x2E, 0x15, 0x08, 0x26, 0x3A, 0x3A, 0xA3, 0x63, 0x97, 0xF9, 0xB3, 0x9C, 0xA7, + 0xFF, 0xDF, 0x00, 0xC3, 0x0F, 0x4B, 0x57, 0x23, 0xFE, 0xC0, 0xA9, 0x51, 0xB3, 0xE4, 0x76, 0xFA, + 0x99, 0xF5, 0x3E, 0xB8, 0x6F, 0x37, 0xFC, 0x2B, 0xD7, 0xB0, 0xA7, 0x55, 0x28, 0xAA, 0x30, 0x62, + 0xDC, 0xD7, 0x8C, 0x57, 0x85, 0x5A, 0x80, 0x6A, 0x70, 0x77, 0xF0, 0x21, 0x9F, 0x9D, 0xC2, 0x43, + 0x68, 0x1C, 0x2A, 0xBC, 0x6E, 0x9C, 0x6C, 0x0D, 0xA7, 0xE8, 0x67, 0xD6, 0x07, 0x2A, 0xC3, 0x01, + 0x4B, 0xE3, 0xE8, 0xAE, 0xD3, 0x32, 0x32, 0xD9, 0xC5, 0x42, 0xC8, 0x04, 0x8E, 0x89, 0xBD, 0x21, + 0xC8, 0xD2, 0x06, 0x59, 0x31, 0x0A, 0x7F, 0xD4, 0x48, 0x87, 0x51, 0x23, 0x1C, 0xE0, 0x2F, 0xB6, + 0x66, 0x1F, 0x39, 0x40, 0xED, 0x40, 0xFF, 0x7E, 0x58, 0xEA, 0xFA, 0x5A, 0xF4, 0x64, 0x7F, 0x76, + 0x9B, 0x43, 0x70, 0x0C, 0x88, 0x44, 0xD1, 0x72, 0xBE, 0x9A, 0xDA, 0x7B, 0xB5, 0xBB, 0x77, 0x9F, + 0xE0, 0x1E, 0x61, 0xA3, 0xC8, 0xC6, 0x8A, 0x98, 0xC2, 0x87, 0xD4, 0x0E, 0x35, 0x25, 0xA1, 0x0A, + 0x92, 0x7F, 0xC2, 0x62, 0xEB, 0x88, 0x7B, 0x44, 0xAC, 0x03, 0xB6, 0xB7, 0xFF, 0xDA, 0x50, 0x32, + 0xE0, 0xEC, 0x1B, 0x3C, 0x77, 0xCA, 0xBA, 0x11, 0xC6, 0xD6, 0x4C, 0x24, 0xD0, 0x2E, 0xA2, 0xA5, + 0xA5, 0x42, 0x78, 0xC6, 0xA0, 0xF7, 0xF0, 0x09, 0x38, 0x74, 0x84, 0x58, 0xE4, 0x46, 0x66, 0x02, + 0xC1, 0x13, 0xE0, 0x0C, 0x65, 0x31, 0x28, 0x10, 0x03, 0xD9, 0x1C, 0xA4, 0xE2, 0x84, 0xB1, 0x6A, + 0x66, 0x6D, 0xEC, 0x6C, 0x10, 0x7F, 0x08, 0x16, 0x91, 0xA9, 0x85, 0x4E, 0x04, 0x26, 0x05, 0x0E, + 0x08, 0x0E, 0xD8, 0xDB, 0x0E, 0x44, 0x86, 0x91, 0x71, 0x72, 0x0C, 0x9C, 0xC8, 0xD0, 0x40, 0x8F, + 0xCC, 0x64, 0x54, 0x28, 0x8A, 0x63, 0x8D, 0x76, 0xD2, 0x0F, 0x5D, 0x39, 0x54, 0x46, 0xE7, 0x42, + 0xD7, 0xD0, 0xD0, 0x0B, 0xD8, 0xD9, 0xB0, 0x34, 0xDE, 0x60, 0xED, 0xF5, 0xF5, 0xE0, 0xC2, 0xD8, + 0xC5, 0xD2, 0x3A, 0xE1, 0x03, 0xC7, 0x50, 0x6B, 0x60, 0x2A, 0x59, 0x2E, 0x1E, 0xD3, 0xAC, 0x03, + 0x61, 0xF9, 0xA2, 0x1F, 0xD6, 0x6F, 0xF4, 0x94, 0x7C, 0x1F, 0x32, 0x82, 0x12, 0xA0, 0x55, 0x88, + 0x2A, 0x60, 0x6D, 0xB6, 0x78, 0xAC, 0x45, 0xCF, 0x9B, 0xEC, 0xE1, 0x10, 0x4E, 0x1D, 0x99, 0x2A, + 0x88, 0x2D, 0x1B, 0xB6, 0xEE, 0x24, 0x74, 0x8E, 0x91, 0x12, 0x0D, 0x76, 0x66, 0x84, 0x03, 0xE1, + 0x71, 0xC1, 0xCF, 0xA1, 0x26, 0xF8, 0x60, 0xCD, 0xB9, 0x9A, 0x04, 0xAA, 0x4F, 0xD8, 0x3A, 0xF0, + 0x5D, 0x1B, 0xFE, 0x6D, 0xEE, 0x71, 0x51, 0x0C, 0x17, 0x7D, 0xD3, 0x2E, 0xC2, 0xEB, 0x23, 0xBB, + 0x6D, 0xE4, 0x4B, 0x25, 0x58, 0x84, 0x54, 0x1C, 0x98, 0x73, 0x3C, 0x2C, 0x1C, 0xEE, 0x9D, 0x42, + 0x12, 0xC6, 0xC6, 0x27, 0x1B, 0xE8, 0xB3, 0x8B, 0xA5, 0xA5, 0x40, 0xC2, 0x07, 0xC9, 0x38, 0xCD, + 0x59, 0x48, 0xEE, 0x6B, 0xB0, 0xD3, 0xD8, 0xD1, 0x61, 0x6B, 0x6D, 0xE9, 0xEF, 0xBD, 0x5D, 0xB7, + 0xE5, 0xAF, 0xC9, 0x33, 0x6C, 0xDA, 0x8C, 0x8D, 0xBE, 0x6D, 0x01, 0x3B, 0x08, 0x13, 0x01, 0xFA, + 0xB1, 0xE5, 0xE7, 0xB5, 0x2C, 0xED, 0x13, 0x0A, 0x84, 0x08, 0x86, 0x18, 0x45, 0x2A, 0x7C, 0x42, + 0x66, 0x02, 0xCB, 0x7A, 0x9C, 0x56, 0x83, 0xB8, 0x47, 0x08, 0x3E, 0x81, 0x27, 0x68, 0x73, 0x55, + 0xE8, 0x17, 0x18, 0xCD, 0x6F, 0xBB, 0xC1, 0x90, 0xB4, 0x0D, 0x5E, 0x4B, 0x85, 0x8F, 0xAD, 0xD9, + 0xFF, 0x1B, 0x4C, 0x7B, 0x9A, 0x04, 0xFA, 0xEC, 0x72, 0xF3, 0xD8, 0x4C, 0x5F, 0xFE, 0xCC, 0x1E, + 0x9B, 0xB5, 0xCE, 0xF9, 0x21, 0x88, 0xF1, 0x3A, 0x27, 0x57, 0x6C, 0xE0, 0x84, 0x2A, 0x03, 0xC1, + 0x40, 0xB3, 0x91, 0x49, 0xEC, 0x67, 0xD6, 0xA7, 0x54, 0x20, 0x84, 0xB3, 0x23, 0xD6, 0x11, 0x26, + 0x4E, 0x6C, 0x30, 0xD6, 0xE8, 0x6B, 0x65, 0x92, 0xAD, 0xA5, 0xA1, 0xB8, 0x6D, 0x1B, 0x9D, 0xA7, + 0x4F, 0x23, 0x9C, 0x5B, 0x94, 0x29, 0x01, 0x03, 0x0B, 0x27, 0x72, 0x99, 0x37, 0x8B, 0xF2, 0x5B, + 0x4A, 0x96, 0x82, 0x9C, 0xA1, 0x91, 0x46, 0x54, 0x4C, 0x76, 0x4D, 0x31, 0xCD, 0x64, 0x06, 0x60, + 0xCD, 0xFC, 0x77, 0x6F, 0x5F, 0xBF, 0xC9, 0x33, 0xF1, 0x41, 0x32, 0x7B, 0x4D, 0xD4, 0xA3, 0xA3, + 0xD8, 0x4C, 0x42, 0xFD, 0x16, 0x05, 0xE8, 0x89, 0x8F, 0x9F, 0x0A, 0x27, 0x5E, 0x19, 0xC1, 0x10, + 0x9E, 0x35, 0xC4, 0x5A, 0x47, 0x10, 0x33, 0x09, 0x72, 0x22, 0x6B, 0x16, 0x91, 0xF3, 0x06, 0x45, + 0x65, 0x88, 0x7B, 0x28, 0x76, 0x12, 0x9B, 0x74, 0x21, 0xFF, 0x0B, 0x74, 0x15, 0x4B, 0x33, 0x70, + 0xF9, 0xA0, 0x65, 0xB8, 0x65, 0x72, 0x58, 0x08, 0x74, 0x05, 0x86, 0x9F, 0x0A, 0x0D, 0x6C, 0x45, + 0x70, 0xF5, 0x81, 0x40, 0xC2, 0x27, 0xB2, 0x2F, 0x70, 0xFD, 0x86, 0xB2, 0xB0, 0xC1, 0x2B, 0xA6, + 0xD9, 0x7B, 0xA8, 0xAA, 0xF2, 0xBC, 0xB6, 0x6D, 0xF6, 0xDC, 0xE9, 0xCD, 0x5E, 0x8D, 0x92, 0xA1, + 0x06, 0x55, 0x73, 0x66, 0x7A, 0x99, 0xA0, 0x58, 0xB7, 0x07, 0x75, 0xC9, 0xA0, 0x37, 0xF6, 0x76, + 0x36, 0xA2, 0xF6, 0x39, 0x06, 0xE6, 0xA0, 0x9A, 0xC0, 0x28, 0x82, 0x24, 0xE4, 0xCA, 0xC4, 0x7B, + 0xA7, 0xA4, 0x94, 0xDA, 0x06, 0x76, 0x35, 0xA4, 0xCE, 0x47, 0x3A, 0xC0, 0x35, 0x5C, 0x8F, 0xBC, + 0x13, 0x71, 0xE1, 0x32, 0xBA, 0x6C, 0xB8, 0x1E, 0xD0, 0x75, 0x72, 0xFA, 0x81, 0x31, 0xBF, 0xD2, + 0x82, 0x1E, 0xA7, 0x54, 0xC0, 0x78, 0xCC, 0x2E, 0x7C, 0x97, 0x8E, 0xFD, 0xC2, 0x0D, 0x4C, 0x33, + 0x66, 0x28, 0x29, 0x2A, 0x6E, 0x5C, 0xEF, 0xD6, 0xA2, 0xB7, 0x1F, 0x40, 0xA0, 0xD2, 0xD2, 0x0E, + 0x15, 0xCC, 0x17, 0x98, 0xA9, 0x1F, 0x96, 0xAE, 0x6E, 0x52, 0x9D, 0xD0, 0x83, 0xFB, 0x76, 0x8B, + 0xF4, 0x1B, 0x47, 0x38, 0x90, 0xBB, 0x70, 0xD4, 0x7F, 0xDB, 0xD3, 0x0C, 0x14, 0xFB, 0x86, 0xE4, + 0x2C, 0x49, 0x0E, 0x76, 0x6C, 0xF0, 0xDC, 0x09, 0x5E, 0x99, 0xB3, 0x93, 0x63, 0xE3, 0x08, 0xA1, + 0x23, 0x70, 0x89, 0x18, 0xCD, 0xA3, 0x04, 0x45, 0x8C, 0x29, 0x10, 0xB1, 0x80, 0x7E, 0x04, 0xD9, + 0x49, 0xD4, 0xDD, 0xA0, 0x94, 0x2C, 0x71, 0x53, 0xA2, 0xEC, 0x33, 0xCA, 0x73, 0xC2, 0xC5, 0xB4, + 0xE2, 0x8C, 0x98, 0x66, 0x1D, 0x02, 0x1C, 0x0E, 0xC7, 0x80, 0xD5, 0xDD, 0x47, 0xE9, 0x3B, 0x62, + 0x98, 0x18, 0x74, 0xA2, 0xA5, 0x96, 0x04, 0xE5, 0x0F, 0xE0, 0x2F, 0xE2, 0x0F, 0xB4, 0x03, 0x07, + 0xA2, 0x92, 0x0A, 0x84, 0xAA, 0x21, 0xB5, 0x46, 0x8A, 0x85, 0xC8, 0x46, 0x0E, 0x0E, 0xA5, 0x61, + 0xCD, 0xD0, 0x70, 0x19, 0xF2, 0x4B, 0x1B, 0xCE, 0xA8, 0xAF, 0x47, 0xA6, 0x96, 0xA1, 0xBE, 0x3E, + 0x4B, 0xE0, 0x27, 0x61, 0xEC, 0x07, 0x54, 0x41, 0xB1, 0x1F, 0x08, 0xC1, 0x69, 0xF6, 0x42, 0xC4, + 0x3A, 0xE2, 0x2C, 0xF4, 0x64, 0x3D, 0x25, 0x77, 0x2A, 0xEB, 0xC0, 0xE3, 0x66, 0x62, 0x2D, 0xD2, + 0x6C, 0x14, 0xF4, 0x83, 0x0A, 0xB2, 0x90, 0x84, 0x8D, 0x6C, 0x4E, 0xD3, 0xD0, 0xC8, 0x5B, 0x43, + 0x5F, 0x7E, 0x2A, 0x5C, 0x94, 0x52, 0x02, 0x7F, 0xF8, 0x3E, 0xEF, 0x87, 0xDD, 0x08, 0xD6, 0x01, + 0xDF, 0x22, 0x88, 0xE1, 0x32, 0x3B, 0xA9, 0x74, 0xEA, 0xC0, 0x5E, 0x62, 0xC4, 0x0F, 0xF1, 0x8D, + 0x9C, 0xE1, 0xA0, 0x10, 0x9B, 0x3E, 0x52, 0x2C, 0x61, 0xEC, 0x87, 0xC6, 0xBB, 0x51, 0x3F, 0x42, + 0x1E, 0xF6, 0x60, 0x1F, 0xD0, 0xFB, 0x38, 0x80, 0x67, 0x81, 0x88, 0x01, 0x31, 0x1E, 0x8D, 0x48, + 0x22, 0xE1, 0x04, 0x42, 0x8A, 0x12, 0x13, 0xC9, 0x0F, 0x34, 0xC6, 0x2D, 0xD2, 0x6F, 0x24, 0x29, + 0x34, 0x71, 0x52, 0x72, 0x20, 0x24, 0x25, 0xDF, 0xA9, 0x61, 0xD7, 0xF4, 0xD0, 0x40, 0xF0, 0x8A, + 0x1B, 0xA8, 0xDE, 0x14, 0x6B, 0x01, 0xCF, 0xE1, 0xB2, 0xDB, 0x30, 0x8B, 0x08, 0xED, 0x43, 0x6B, + 0x70, 0x0A, 0xC4, 0x49, 0xF8, 0x0B, 0xC7, 0xA2, 0x46, 0xD8, 0x3F, 0x36, 0xD7, 0xA9, 0xF3, 0xAC, + 0x37, 0xC3, 0xC0, 0xC0, 0xD6, 0x0C, 0x03, 0x03, 0xD3, 0x0C, 0x03, 0x03, 0xA3, 0xB5, 0xF8, 0x9F, + 0x00, 0x03, 0x00, 0xF2, 0x82, 0x33, 0x9A, 0xC5, 0x9A, 0xEA, 0x40, 0x00, 0x00, 0x00, 0x00, 0x49, + 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82, 0xB0, 0x61, 0x73, 0x28, 0xB0, 0x16, 0x00 +}; //rtk-setup-wifi.png + +//Content of rtk-setup.png with gzip compression +static const uint8_t rtkSetup_png[] PROGMEM = { + 0x1F, 0x8B, 0x08, 0x08, 0x72, 0xB4, 0x35, 0x64, 0x02, 0xFF, 0x72, 0x74, 0x6B, 0x2D, 0x73, 0x65, + 0x74, 0x75, 0x70, 0x2E, 0x70, 0x6E, 0x67, 0x2E, 0x67, 0x7A, 0x69, 0x70, 0x00, 0x01, 0x67, 0x17, + 0x98, 0xE8, 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, + 0x44, 0x52, 0x00, 0x00, 0x01, 0x22, 0x00, 0x00, 0x00, 0x59, 0x08, 0x06, 0x00, 0x00, 0x00, 0x9A, + 0x2C, 0x29, 0x00, 0x00, 0x00, 0x00, 0x01, 0x73, 0x52, 0x47, 0x42, 0x00, 0xAE, 0xCE, 0x1C, 0xE9, + 0x00, 0x00, 0x00, 0x04, 0x67, 0x41, 0x4D, 0x41, 0x00, 0x00, 0xB1, 0x8F, 0x0B, 0xFC, 0x61, 0x05, + 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x0E, 0xC4, 0x00, 0x00, 0x0E, 0xC4, + 0x01, 0x95, 0x2B, 0x0E, 0x1B, 0x00, 0x00, 0x00, 0x19, 0x74, 0x45, 0x58, 0x74, 0x53, 0x6F, 0x66, + 0x74, 0x77, 0x61, 0x72, 0x65, 0x00, 0x41, 0x64, 0x6F, 0x62, 0x65, 0x20, 0x49, 0x6D, 0x61, 0x67, + 0x65, 0x52, 0x65, 0x61, 0x64, 0x79, 0x71, 0xC9, 0x65, 0x3C, 0x00, 0x00, 0x16, 0xD7, 0x49, 0x44, + 0x41, 0x54, 0x78, 0x5E, 0xED, 0x9D, 0x09, 0x78, 0x4C, 0x57, 0x1B, 0xC7, 0xDF, 0x24, 0xB2, 0x08, + 0x91, 0x08, 0x6A, 0x29, 0xDA, 0x6A, 0xB5, 0xA8, 0xA5, 0x54, 0x69, 0xAD, 0xB5, 0xB4, 0xB6, 0x6E, + 0x96, 0xD6, 0x5E, 0xB1, 0xD5, 0xAE, 0x08, 0x8A, 0xA8, 0xAD, 0xD4, 0xBE, 0x0B, 0xA1, 0x62, 0x5F, + 0x8A, 0xA2, 0x54, 0x55, 0x77, 0xAD, 0xD6, 0xAE, 0x7C, 0x76, 0xA5, 0x54, 0xA9, 0x9D, 0xDA, 0x92, + 0x48, 0x44, 0xF2, 0xDD, 0xFF, 0xC9, 0x19, 0xB9, 0x33, 0x73, 0xEF, 0xCC, 0x4D, 0x32, 0x32, 0x4B, + 0xDE, 0xDF, 0xF3, 0xDC, 0x67, 0xCE, 0xB9, 0x73, 0x33, 0x73, 0x67, 0x32, 0xF7, 0x7F, 0xDF, 0xF3, + 0x2E, 0xE7, 0x78, 0x25, 0x2B, 0x10, 0xC3, 0x30, 0x8C, 0x13, 0xF1, 0x96, 0x8F, 0x0C, 0xC3, 0x30, + 0x4E, 0x83, 0x2D, 0x22, 0xC6, 0xA5, 0x49, 0xD8, 0xB5, 0x9B, 0x62, 0x56, 0xAF, 0xA6, 0x84, 0xDD, + 0x7B, 0x28, 0xE9, 0xE6, 0x4D, 0xF2, 0xF2, 0xF3, 0x23, 0xDF, 0x92, 0x25, 0xC9, 0xFF, 0xD5, 0x9A, + 0x14, 0xD8, 0xB4, 0x09, 0x79, 0xE7, 0xCE, 0x2D, 0x8F, 0x64, 0xDC, 0x19, 0x16, 0x22, 0xC6, 0x25, + 0x49, 0x8E, 0x89, 0xA1, 0x5B, 0x23, 0x46, 0x52, 0xDC, 0x96, 0x6F, 0xE5, 0x1E, 0x6B, 0xBC, 0x02, + 0x03, 0x29, 0xA8, 0x47, 0x77, 0xCA, 0xD1, 0xB6, 0x8D, 0xD2, 0xF1, 0x92, 0x7B, 0x19, 0x77, 0x84, + 0x85, 0x88, 0x71, 0x39, 0x92, 0xEF, 0xDD, 0xA3, 0xEB, 0x1D, 0x3A, 0xD2, 0xFD, 0x43, 0x87, 0xE5, + 0x1E, 0x73, 0xFC, 0x2A, 0x56, 0xA4, 0x5C, 0xFD, 0xFA, 0x92, 0xEF, 0xF3, 0xA5, 0xE4, 0x1E, 0x05, + 0x6F, 0xF6, 0x32, 0xB8, 0x33, 0xFC, 0xDF, 0x63, 0x5C, 0x8E, 0x3B, 0xD3, 0x67, 0xE8, 0x8A, 0x90, + 0x6F, 0x99, 0xD2, 0x14, 0x3A, 0x2F, 0x4A, 0x3C, 0x0A, 0xF1, 0x31, 0x6D, 0x8C, 0x5B, 0xC3, 0xFF, + 0x41, 0xC6, 0xA5, 0x48, 0xBE, 0x7F, 0x9F, 0x62, 0xD7, 0xAE, 0x93, 0x3D, 0x6B, 0x02, 0xDF, 0x7B, + 0x97, 0xBC, 0x7C, 0x7D, 0x65, 0x8F, 0xF1, 0x14, 0x58, 0x88, 0x18, 0x97, 0x22, 0xE9, 0xF2, 0x65, + 0x4A, 0x8E, 0x8B, 0x93, 0x3D, 0x6B, 0x92, 0x6E, 0xDC, 0x90, 0x2D, 0xC6, 0x93, 0x60, 0x21, 0x62, + 0x5C, 0x0A, 0xAF, 0xA0, 0x20, 0xD9, 0xD2, 0x26, 0x66, 0xD9, 0x72, 0x4D, 0x31, 0x7A, 0x70, 0xF1, + 0x22, 0xC5, 0xFF, 0xBE, 0x5D, 0xF6, 0x18, 0x77, 0x83, 0x85, 0x88, 0x71, 0x29, 0xBC, 0x83, 0x83, + 0xC9, 0xB7, 0x64, 0x09, 0xD9, 0xB3, 0x26, 0xE9, 0xEA, 0x35, 0xBA, 0xDE, 0xB1, 0x33, 0x25, 0x5D, + 0xBF, 0x2E, 0xF7, 0xA4, 0x80, 0xD0, 0xFE, 0x8D, 0xAE, 0xDD, 0xE8, 0xCE, 0xB4, 0xE9, 0x72, 0x0F, + 0xE3, 0x4E, 0xB0, 0x10, 0x31, 0x2E, 0x47, 0x8E, 0xB0, 0x30, 0xD9, 0xD2, 0x26, 0xF1, 0xD4, 0x29, + 0xBA, 0xDA, 0xB8, 0x09, 0xC5, 0xAE, 0x5B, 0x4F, 0xC9, 0xF1, 0xF1, 0x62, 0xDF, 0x3D, 0x19, 0xE6, + 0xBF, 0x1B, 0xBD, 0x80, 0x62, 0x56, 0xAE, 0x14, 0x6D, 0xC6, 0x7D, 0xE0, 0xF0, 0x3D, 0xE3, 0x7A, + 0x24, 0x25, 0x29, 0xD6, 0x4D, 0x77, 0x8A, 0xDF, 0xB1, 0x43, 0xEE, 0xD0, 0x07, 0xB9, 0x44, 0x5E, + 0x01, 0x01, 0x66, 0xC3, 0x35, 0x2F, 0x7F, 0x7F, 0xCA, 0xF7, 0xD5, 0x06, 0xF2, 0x29, 0x58, 0x50, + 0xEE, 0x61, 0x5C, 0x1D, 0xB6, 0x88, 0x18, 0xD7, 0xC3, 0xDB, 0x9B, 0x42, 0xC6, 0x8F, 0x25, 0x9F, + 0x22, 0x45, 0xE4, 0x0E, 0x7D, 0x92, 0x63, 0x63, 0xAD, 0x7C, 0x46, 0xB0, 0x92, 0xEE, 0xCE, 0xFB, + 0x4C, 0xF6, 0x18, 0x77, 0x80, 0x85, 0x88, 0x71, 0x49, 0x50, 0xBA, 0x91, 0x67, 0x61, 0xB4, 0x21, + 0x31, 0xD2, 0x22, 0x6E, 0xD3, 0xD7, 0x36, 0xA3, 0x6F, 0x8C, 0x6B, 0xC1, 0x42, 0xC4, 0x38, 0x97, + 0x07, 0x0F, 0x28, 0xE1, 0xC0, 0x01, 0x8A, 0xFB, 0x6A, 0x93, 0x10, 0x8F, 0xFB, 0xC7, 0x4F, 0xC8, + 0x27, 0x88, 0x7C, 0xF2, 0xE7, 0xA7, 0x3C, 0xF3, 0x3F, 0x4B, 0x57, 0x3D, 0x19, 0xB2, 0xB3, 0xE3, + 0x77, 0xEE, 0x94, 0x3D, 0xC6, 0xD5, 0x61, 0x21, 0x62, 0x9C, 0x43, 0x72, 0x32, 0xC5, 0xAC, 0x58, + 0x49, 0x97, 0xEB, 0xBC, 0x46, 0xD7, 0xDB, 0xB6, 0xA3, 0x9B, 0x43, 0x22, 0xE8, 0x6E, 0x74, 0x34, + 0x25, 0xDF, 0xBA, 0x25, 0x0F, 0x48, 0xC1, 0xA7, 0x50, 0x41, 0xCA, 0x15, 0xDE, 0x57, 0xF6, 0xD2, + 0xC6, 0xFD, 0x03, 0x07, 0x65, 0x8B, 0x71, 0x75, 0x58, 0x88, 0x18, 0xA7, 0x70, 0xEB, 0x93, 0xD1, + 0x74, 0x7B, 0xEC, 0xB8, 0x87, 0x61, 0x78, 0x9F, 0xC2, 0x85, 0x29, 0xEF, 0xCA, 0x15, 0xE4, 0x57, + 0xB9, 0x92, 0xE8, 0xAB, 0x09, 0xA8, 0x5B, 0x57, 0xB6, 0xD2, 0x46, 0xE2, 0xF9, 0xF3, 0xB2, 0xC5, + 0xB8, 0x3A, 0x2C, 0x44, 0x4C, 0xA6, 0x13, 0xBF, 0x6D, 0x1B, 0xC5, 0xAE, 0xF9, 0x42, 0xF6, 0x52, + 0xF0, 0x2B, 0x53, 0x5A, 0x44, 0xBF, 0xB4, 0x48, 0x8A, 0x89, 0x95, 0xAD, 0xB4, 0xC1, 0x59, 0xD8, + 0xEE, 0x03, 0x0B, 0x11, 0x93, 0xE9, 0xC4, 0xAC, 0xF8, 0x5C, 0xB6, 0x52, 0x89, 0xDF, 0xB1, 0x53, + 0x57, 0x38, 0x62, 0x16, 0x2D, 0x92, 0xAD, 0x34, 0xE2, 0xC3, 0x3F, 0x6F, 0x77, 0x81, 0xFF, 0x53, + 0x4C, 0xA6, 0x93, 0xB0, 0x6F, 0x9F, 0x6C, 0xA5, 0x82, 0xCC, 0xE8, 0xEB, 0x61, 0x1D, 0x44, 0x99, + 0x06, 0x0A, 0x5F, 0x41, 0xE2, 0xE9, 0xD3, 0x74, 0x6B, 0xE4, 0x28, 0x8A, 0x59, 0xBA, 0x4C, 0xF4, + 0xD3, 0x8A, 0x4F, 0x9E, 0x3C, 0xB2, 0xC5, 0xB8, 0x3A, 0x9C, 0xD0, 0xC8, 0x64, 0x2A, 0x49, 0xD7, + 0xAE, 0xD1, 0xE5, 0x5A, 0x75, 0x64, 0x4F, 0x87, 0x6C, 0xD9, 0x44, 0x34, 0x0D, 0x0E, 0xED, 0x8C, + 0x10, 0xD4, 0xB3, 0x07, 0xE5, 0xEC, 0xF2, 0x81, 0xEC, 0x31, 0xAE, 0x0C, 0x5B, 0x44, 0x4C, 0xA6, + 0x92, 0x9C, 0x90, 0x20, 0x5B, 0x36, 0x48, 0x4C, 0xCC, 0xB0, 0x08, 0x01, 0xBF, 0xF2, 0xE5, 0x65, + 0x8B, 0x71, 0x75, 0x58, 0x88, 0x18, 0x8F, 0x04, 0xB9, 0x47, 0x7E, 0x15, 0x5F, 0x94, 0x3D, 0xC6, + 0xD5, 0x61, 0x21, 0x62, 0x3C, 0x12, 0x4C, 0xA0, 0xC6, 0x33, 0x37, 0xBA, 0x0F, 0xFC, 0x9F, 0x62, + 0x3C, 0x0E, 0xEF, 0xA0, 0x20, 0xCA, 0xD1, 0xBA, 0xB5, 0xEC, 0x31, 0xEE, 0x00, 0x0B, 0x11, 0xE3, + 0x71, 0x04, 0xF5, 0xED, 0xA3, 0x0C, 0xCD, 0x42, 0x64, 0x8F, 0x71, 0x07, 0x58, 0x88, 0x18, 0x8F, + 0x22, 0xA0, 0x4E, 0x6D, 0x0A, 0x6C, 0xD6, 0x54, 0xF6, 0x18, 0x77, 0x81, 0x85, 0x88, 0xF1, 0x18, + 0xB0, 0xB2, 0x47, 0xC8, 0xE8, 0xD1, 0xC4, 0x6B, 0x9C, 0xB9, 0x1F, 0x2C, 0x44, 0x8C, 0x47, 0xE0, + 0x5B, 0xB6, 0x2C, 0x85, 0xCE, 0x99, 0x4D, 0x5E, 0x39, 0x73, 0xC8, 0x3D, 0x8C, 0x3B, 0xC1, 0x42, + 0xC4, 0xB8, 0x37, 0x8A, 0xF5, 0x13, 0xD8, 0xA2, 0x39, 0xE5, 0x59, 0xB4, 0x40, 0xCC, 0x77, 0xCD, + 0xB8, 0x27, 0x2C, 0x44, 0x8C, 0xDB, 0xE2, 0x5F, 0xAD, 0x1A, 0xE5, 0x5D, 0xB6, 0x94, 0x82, 0x23, + 0x86, 0xF0, 0x5A, 0x67, 0x6E, 0x0E, 0x97, 0x78, 0x30, 0x99, 0xCA, 0x83, 0x0B, 0x17, 0xE8, 0x4A, + 0xBD, 0x06, 0xB2, 0x67, 0x1B, 0xAF, 0xEC, 0xD9, 0xCD, 0xA2, 0x5F, 0xDE, 0x21, 0xB9, 0x29, 0x5B, + 0xD1, 0x22, 0xE4, 0x5B, 0xBE, 0x3C, 0x05, 0xD4, 0xAC, 0x41, 0x3E, 0x8F, 0x3F, 0x2E, 0x9F, 0x61, + 0xDC, 0x1D, 0x16, 0x22, 0x26, 0x53, 0x31, 0x22, 0x44, 0x98, 0x1E, 0x36, 0xF8, 0xA3, 0x81, 0x8A, + 0xC5, 0x53, 0x55, 0xE9, 0xF8, 0xC8, 0xBD, 0x8C, 0x27, 0xC3, 0x43, 0x33, 0xC6, 0xA5, 0xF0, 0x7B, + 0xA9, 0x22, 0xE5, 0x5B, 0xFD, 0x39, 0xF9, 0x2B, 0x16, 0x0F, 0x8B, 0x50, 0xD6, 0x81, 0x85, 0x88, + 0x71, 0x19, 0xFC, 0x2A, 0x54, 0xA0, 0xD0, 0xD9, 0x91, 0xE4, 0x95, 0x33, 0xA7, 0xDC, 0xC3, 0x64, + 0x15, 0x58, 0x88, 0x18, 0x97, 0x20, 0x5B, 0xF1, 0xE2, 0x14, 0x1A, 0x39, 0x53, 0x77, 0x96, 0x46, + 0xC6, 0xB3, 0x61, 0x21, 0x62, 0x9C, 0x8E, 0x4F, 0xA1, 0x42, 0x14, 0x1A, 0x35, 0x87, 0x2D, 0xA1, + 0x2C, 0x0C, 0x0B, 0x11, 0xE3, 0x54, 0x10, 0x15, 0x0B, 0x9D, 0x17, 0x45, 0x3E, 0x8F, 0xE5, 0x93, + 0x7B, 0x98, 0xAC, 0x08, 0x47, 0xCD, 0x18, 0x87, 0x92, 0xA4, 0xFC, 0x9C, 0x7E, 0xF8, 0xE9, 0x17, + 0x0A, 0x09, 0x09, 0xA6, 0x4A, 0x2F, 0x5A, 0x4F, 0x4C, 0xA6, 0x8E, 0x9A, 0x61, 0x69, 0xE8, 0x3C, + 0x0B, 0xA2, 0xC9, 0xB7, 0x6C, 0x19, 0xD1, 0x37, 0x4A, 0x7C, 0x7C, 0x02, 0x6D, 0xDB, 0xBE, 0x93, + 0x0E, 0x1C, 0x3A, 0x4C, 0x17, 0x2F, 0x5F, 0xA1, 0xBB, 0x77, 0x63, 0xC8, 0xCF, 0xCF, 0x8F, 0x72, + 0xE6, 0xC8, 0x41, 0x85, 0x0A, 0xE4, 0xA7, 0x5E, 0x5D, 0x3B, 0xCA, 0x23, 0x19, 0x77, 0x81, 0x85, + 0x88, 0x71, 0x18, 0xFB, 0xF6, 0xFF, 0x8F, 0x66, 0xCF, 0x5F, 0x4C, 0x7F, 0x9E, 0xFA, 0x8B, 0x86, + 0x84, 0xF7, 0xA6, 0x06, 0xAF, 0x5B, 0x4F, 0x09, 0xFB, 0x50, 0x88, 0xBC, 0xBD, 0x29, 0x74, 0xD6, + 0x0C, 0xF2, 0xAF, 0x5E, 0x5D, 0x3E, 0x63, 0x8C, 0x2D, 0x3F, 0xFC, 0x4C, 0xB3, 0x3F, 0x5B, 0x48, + 0xFF, 0xDD, 0x34, 0x5F, 0xFF, 0xCC, 0x44, 0x81, 0xC7, 0x1E, 0xA3, 0x35, 0x4B, 0x79, 0xB9, 0x69, + 0x77, 0x83, 0x87, 0x66, 0x4C, 0x86, 0xF9, 0xEB, 0xCC, 0xDF, 0xD4, 0x3F, 0x62, 0x24, 0xF5, 0x19, + 0x34, 0x4C, 0x88, 0x90, 0x11, 0x82, 0x87, 0x0F, 0x4B, 0xB3, 0x08, 0xAD, 0x5E, 0xBF, 0x91, 0xC6, + 0x4C, 0x9C, 0xA6, 0x2B, 0x42, 0x8C, 0xFB, 0xC2, 0x42, 0xC4, 0x64, 0x88, 0xA9, 0x91, 0x73, 0xA9, + 0x7D, 0xB7, 0x3E, 0xB4, 0x6B, 0xEF, 0x1F, 0x72, 0x8F, 0x7D, 0x72, 0xB4, 0x0F, 0xA3, 0xC0, 0x26, + 0x8D, 0x65, 0xCF, 0x18, 0xE7, 0x2F, 0x5C, 0x54, 0x2C, 0xA1, 0x74, 0x2E, 0x2B, 0xC4, 0xB8, 0x3C, + 0x2C, 0x44, 0x4C, 0x86, 0xD8, 0xBE, 0x73, 0x2F, 0xA5, 0x65, 0x74, 0xEF, 0x9D, 0x2F, 0x1F, 0xE5, + 0xEA, 0xF3, 0xA1, 0xEC, 0x19, 0x67, 0xFD, 0x57, 0x9B, 0xE9, 0x01, 0x56, 0xF6, 0xD0, 0x21, 0x77, + 0x48, 0x30, 0xE5, 0x08, 0x0C, 0x94, 0x3D, 0xC6, 0xDD, 0x60, 0x21, 0x62, 0x32, 0x15, 0x51, 0x9C, + 0x9A, 0x8E, 0xB9, 0xA4, 0xF7, 0xFF, 0xEF, 0x90, 0x6C, 0x99, 0x53, 0xAA, 0xC4, 0xB3, 0xB4, 0x69, + 0xF5, 0x52, 0xDA, 0xB8, 0x6A, 0x09, 0x6D, 0x59, 0xBF, 0x92, 0xFD, 0x43, 0x6E, 0x0A, 0x0B, 0x11, + 0xE3, 0x16, 0x5C, 0xBD, 0xA6, 0xBD, 0x0A, 0xEC, 0x3B, 0x8D, 0xEA, 0x53, 0x70, 0x70, 0x2E, 0xD9, + 0x63, 0xDC, 0x15, 0x16, 0x22, 0xC6, 0x2D, 0xB8, 0x9F, 0x98, 0xB2, 0xFA, 0xAB, 0x25, 0x79, 0xF3, + 0xF2, 0x6A, 0xAE, 0x9E, 0x40, 0x96, 0x0A, 0xDF, 0x27, 0x25, 0x25, 0xD1, 0x81, 0x43, 0x47, 0x44, + 0x98, 0xF9, 0xCC, 0xD9, 0x73, 0x74, 0xF9, 0xCA, 0x55, 0xBA, 0x73, 0xF7, 0x2E, 0x25, 0x27, 0x25, + 0x53, 0x40, 0x80, 0xBF, 0xC8, 0x7D, 0x29, 0x5A, 0xF8, 0x71, 0x2A, 0x57, 0xBA, 0x14, 0xBD, 0x5C, + 0xA9, 0x22, 0xE5, 0x0A, 0x32, 0x9E, 0xE9, 0x7B, 0xFF, 0xFE, 0x7D, 0xFA, 0xFB, 0x9F, 0xF3, 0x54, + 0xA4, 0x70, 0x21, 0x0A, 0xF0, 0xF7, 0x17, 0x7E, 0x93, 0x3D, 0x7F, 0x1C, 0xA0, 0xAD, 0xDB, 0xB6, + 0xD3, 0x89, 0x93, 0x7F, 0xD1, 0xED, 0xDB, 0x77, 0xC8, 0xDB, 0xC7, 0x9B, 0xF2, 0x84, 0xE6, 0xA6, + 0x67, 0x8A, 0x3D, 0x45, 0x55, 0x2A, 0x57, 0xA4, 0x4A, 0x15, 0x2B, 0x90, 0x77, 0x1A, 0xA7, 0x35, + 0xBD, 0x77, 0xEF, 0x1E, 0xED, 0xDD, 0x7F, 0x90, 0xF6, 0x1F, 0x3C, 0x44, 0xE7, 0xCE, 0xFF, 0x2B, + 0x2C, 0x05, 0xE4, 0xD1, 0x00, 0x5F, 0xBF, 0x6C, 0x94, 0x3B, 0x24, 0x84, 0x0A, 0xE4, 0x7F, 0x8C, + 0x4A, 0x14, 0x7F, 0x86, 0x5E, 0x7A, 0xF1, 0x05, 0x7A, 0xB2, 0x68, 0x11, 0xF1, 0x9C, 0x11, 0xE0, + 0x83, 0xF9, 0x47, 0x79, 0xCD, 0x3C, 0xA1, 0xA1, 0x0F, 0x3F, 0xBB, 0xE9, 0x73, 0xFC, 0xB0, 0x75, + 0x1B, 0x9D, 0x3C, 0x75, 0x5A, 0xBC, 0x57, 0xF9, 0x72, 0xA5, 0x69, 0x48, 0xFF, 0x14, 0x3F, 0xCF, + 0xBB, 0x6D, 0x3B, 0xD3, 0xA5, 0x2B, 0x57, 0x44, 0x5B, 0x8D, 0x5E, 0xF8, 0x1E, 0xDC, 0x4F, 0x4C, + 0xA4, 0x5D, 0x7B, 0xF6, 0xE9, 0xFA, 0x96, 0x4A, 0x95, 0x78, 0x4E, 0x7C, 0x4F, 0xF8, 0x7F, 0xED, + 0xDA, 0xBB, 0x9F, 0x12, 0x15, 0x11, 0x1A, 0x35, 0x6E, 0x0A, 0xDD, 0x8B, 0x8F, 0x97, 0x47, 0xA4, + 0xD2, 0xA9, 0x5D, 0x6B, 0x2A, 0xF6, 0x64, 0x51, 0xD9, 0x23, 0x2A, 0x5A, 0xA4, 0x30, 0x3D, 0xA1, + 0x6C, 0x7F, 0x28, 0x43, 0xB9, 0x98, 0x98, 0x94, 0xEF, 0x45, 0xCD, 0xB3, 0xCF, 0x3C, 0x4D, 0xF9, + 0xED, 0x24, 0x4F, 0xEE, 0xD8, 0xBD, 0x57, 0x79, 0xCF, 0x44, 0xD9, 0x4B, 0xA5, 0xEC, 0xF3, 0xA5, + 0x1E, 0x5A, 0x5F, 0xF8, 0xED, 0x68, 0x45, 0x08, 0x4D, 0xEF, 0x0F, 0xF0, 0x39, 0x7F, 0xFE, 0xF5, + 0x77, 0xDA, 0xBE, 0x73, 0x37, 0x9D, 0xFE, 0xFB, 0x1F, 0x8A, 0x8B, 0xBB, 0x47, 0x39, 0x72, 0x04, + 0x8A, 0xDF, 0xD9, 0x33, 0xC5, 0x9E, 0xA4, 0x57, 0xAB, 0x57, 0xA1, 0xD2, 0x25, 0x4B, 0x88, 0x63, + 0xB3, 0x3A, 0x59, 0x42, 0x88, 0xF0, 0x11, 0xBF, 0xF9, 0xFE, 0x27, 0x5A, 0xB4, 0x7C, 0x15, 0x5D, + 0xBC, 0x74, 0x59, 0xEE, 0xB5, 0x0D, 0x12, 0xE4, 0xDE, 0x6A, 0xF8, 0x3A, 0x85, 0xB5, 0x6E, 0x41, + 0xC1, 0xB9, 0x82, 0xE4, 0xDE, 0x54, 0xFE, 0x38, 0x70, 0x90, 0x0E, 0x1F, 0x3B, 0x41, 0xA7, 0xCF, + 0xFC, 0x4D, 0xA7, 0x94, 0xED, 0xDC, 0xF9, 0x0B, 0xE2, 0xC2, 0x59, 0xB3, 0x24, 0xC5, 0x47, 0x31, + 0x72, 0xDC, 0x64, 0x3A, 0x7C, 0xF4, 0xB8, 0x68, 0xEB, 0x01, 0xD1, 0xEA, 0xDB, 0xA3, 0x0B, 0xBD, + 0x54, 0xE1, 0x05, 0xB9, 0x47, 0x9F, 0x5B, 0x8A, 0x90, 0x2D, 0x5F, 0xBD, 0x96, 0x36, 0x6C, 0xDA, + 0x42, 0xB1, 0x71, 0x71, 0x72, 0xAF, 0x7D, 0x9E, 0x2F, 0xF9, 0x1C, 0x75, 0xEB, 0xD8, 0x8E, 0xCA, + 0x95, 0x79, 0x5E, 0xEE, 0x49, 0xE5, 0xD8, 0x89, 0x93, 0x22, 0x29, 0xF0, 0xAF, 0xD3, 0x67, 0x45, + 0x08, 0xFE, 0xEC, 0x3F, 0xE7, 0xC4, 0xC5, 0x33, 0x63, 0xE2, 0x18, 0x2A, 0x5F, 0xB6, 0xB4, 0x88, + 0x54, 0x8D, 0x99, 0x30, 0x4D, 0xF9, 0x9C, 0xE6, 0x9F, 0x03, 0x42, 0xBA, 0x70, 0xCE, 0x34, 0xD1, + 0x4E, 0xAB, 0x10, 0xE1, 0x3B, 0x8A, 0x18, 0x35, 0x96, 0x7E, 0xDB, 0xB1, 0x5B, 0xEE, 0x31, 0xE7, + 0xB5, 0x5A, 0x35, 0x68, 0xE8, 0xC0, 0xBE, 0xA2, 0x3D, 0x7A, 0xC2, 0x54, 0xFA, 0xFE, 0xE7, 0x5F, + 0x45, 0xDB, 0x28, 0xED, 0xDB, 0xB4, 0xA0, 0x0E, 0x6D, 0x5B, 0x8A, 0x48, 0xDE, 0xA9, 0xD3, 0x67, + 0xE4, 0xDE, 0x54, 0x6C, 0x09, 0xA4, 0x89, 0xFA, 0x4D, 0x5A, 0x2A, 0x22, 0x16, 0x2B, 0x7B, 0xA9, + 0x98, 0xBE, 0x17, 0xF0, 0xCD, 0x77, 0x3F, 0xD2, 0xA7, 0x93, 0x67, 0x88, 0xB6, 0x1A, 0xD3, 0xFB, + 0x43, 0xBC, 0xC7, 0x4F, 0x9D, 0x25, 0x04, 0xCB, 0x16, 0x15, 0xCA, 0x95, 0xA1, 0x61, 0x83, 0xC2, + 0x85, 0xF0, 0x66, 0x65, 0x3C, 0x7E, 0x68, 0x86, 0x1F, 0x3E, 0x7E, 0xD0, 0x63, 0x95, 0x1F, 0x8D, + 0x51, 0x11, 0x02, 0x09, 0x09, 0x09, 0xF4, 0xC5, 0x97, 0x9B, 0xA8, 0x63, 0x8F, 0xBE, 0xE2, 0x82, + 0xB4, 0x64, 0xE6, 0xDC, 0x05, 0xF4, 0xD9, 0xA2, 0x65, 0xF4, 0xE3, 0x2F, 0xBF, 0x29, 0x17, 0xF0, + 0x79, 0xF1, 0x3E, 0xE0, 0xC2, 0xC5, 0x4B, 0xD4, 0xB5, 0xCF, 0x40, 0xBB, 0x22, 0x04, 0x20, 0x5E, + 0xFD, 0x06, 0x0F, 0xA7, 0x15, 0xAB, 0xD7, 0xC9, 0x3D, 0xDA, 0x1C, 0xFB, 0xF3, 0x24, 0xBD, 0xFF, + 0x41, 0x2F, 0x5A, 0xB9, 0x66, 0x7D, 0x9A, 0x44, 0x08, 0x1C, 0x51, 0xC4, 0xB2, 0xD7, 0x80, 0x08, + 0xDA, 0xB8, 0xF9, 0x5B, 0xB9, 0x27, 0x95, 0x25, 0x2B, 0xD7, 0x88, 0x90, 0xF8, 0xB7, 0x3F, 0xFE, + 0x2C, 0x2E, 0x5A, 0x88, 0x90, 0x09, 0xDC, 0xED, 0x3B, 0xF5, 0xEC, 0x67, 0x25, 0x42, 0x19, 0xC1, + 0xF4, 0xBF, 0xD0, 0x13, 0xA1, 0x7A, 0x75, 0x6A, 0x65, 0x48, 0x84, 0x5C, 0x05, 0x7C, 0x9F, 0xFD, + 0x87, 0x8C, 0xB0, 0x2B, 0x42, 0x00, 0x96, 0x5B, 0xCF, 0xF0, 0xC1, 0x0F, 0xAD, 0xDA, 0xAC, 0x8A, + 0xC7, 0x0B, 0x11, 0x2E, 0xB6, 0xEF, 0x7E, 0xFA, 0x45, 0xF6, 0xD2, 0x0E, 0x7E, 0x4C, 0xE1, 0xCA, + 0x8F, 0x0A, 0x43, 0x22, 0x23, 0xE0, 0x2E, 0x79, 0xFD, 0xC6, 0x7F, 0xB2, 0x67, 0x8C, 0x39, 0xD1, + 0x8B, 0x69, 0xC3, 0xD7, 0xD6, 0x42, 0x01, 0xFE, 0x55, 0x84, 0xAD, 0xEF, 0xA0, 0x61, 0x74, 0xE3, + 0xBF, 0xB4, 0xBD, 0xA6, 0x1A, 0x58, 0x84, 0x93, 0x67, 0x46, 0x09, 0xAB, 0xC7, 0x08, 0x77, 0xEE, + 0xDC, 0xA5, 0x41, 0xC3, 0xC7, 0x68, 0x5A, 0x05, 0xE9, 0x05, 0xE7, 0x30, 0x6E, 0xCA, 0x4C, 0x5D, + 0x71, 0x81, 0xD3, 0x39, 0x62, 0xC0, 0x87, 0xF4, 0x40, 0x11, 0xAB, 0x51, 0xE3, 0xA7, 0xB8, 0xAD, + 0x08, 0x1D, 0x57, 0x6E, 0x1A, 0x63, 0x27, 0xCF, 0x14, 0xA5, 0x2E, 0x46, 0xC1, 0x8D, 0x6E, 0xEE, + 0xC2, 0xA5, 0xB2, 0x97, 0x35, 0xF1, 0x68, 0x21, 0x8A, 0x89, 0x8D, 0x55, 0x86, 0x33, 0xDA, 0xD6, + 0x46, 0xB1, 0x27, 0x9F, 0xA0, 0xEE, 0x9D, 0xC3, 0x68, 0xEC, 0x88, 0x21, 0xCA, 0x16, 0x41, 0xE1, + 0xBD, 0xBB, 0x51, 0x95, 0xCA, 0x2F, 0xA5, 0x3C, 0x69, 0x01, 0xAC, 0x9C, 0x55, 0xEB, 0x36, 0xCA, + 0x9E, 0x6D, 0x2C, 0xEF, 0x82, 0xF0, 0x29, 0x14, 0x7B, 0xEA, 0x09, 0xBB, 0x91, 0x9D, 0xE9, 0x73, + 0x3E, 0x13, 0x16, 0x92, 0x25, 0x53, 0x67, 0xCD, 0xD5, 0x15, 0x04, 0xD4, 0x72, 0x0D, 0xEC, 0xD3, + 0x83, 0x26, 0x7C, 0xF2, 0x31, 0x7D, 0x32, 0xF4, 0x23, 0xEA, 0xF9, 0x41, 0x07, 0xF1, 0xB9, 0xB4, + 0x80, 0x35, 0xF2, 0xC5, 0x86, 0x4D, 0xB2, 0x67, 0x9B, 0x25, 0x2B, 0x57, 0xD3, 0xD5, 0x6B, 0xD7, + 0x65, 0x2F, 0xE3, 0x40, 0x84, 0xA6, 0x28, 0x42, 0x88, 0xE1, 0xB1, 0x16, 0x8D, 0xDF, 0x6C, 0x48, + 0xFD, 0x7A, 0x75, 0x25, 0x2F, 0x2F, 0x2F, 0x8A, 0x9C, 0xB7, 0x90, 0x8E, 0x1C, 0x3D, 0x21, 0x4A, + 0x35, 0xD4, 0x1B, 0x9E, 0xD3, 0x22, 0x34, 0x77, 0x88, 0xD9, 0x71, 0x39, 0x9D, 0xBC, 0x8A, 0xC7, + 0x8E, 0xDD, 0xFB, 0x6C, 0xE6, 0x3B, 0xE9, 0xB1, 0x59, 0x19, 0xEA, 0x19, 0xBD, 0xD9, 0x79, 0x22, + 0x1E, 0x2D, 0x44, 0xC8, 0x3D, 0xD1, 0xFA, 0xE7, 0xE6, 0xCD, 0x13, 0x4A, 0x73, 0x67, 0x4C, 0xA4, + 0x96, 0xCD, 0x1A, 0x53, 0xB5, 0x57, 0x2A, 0x2B, 0x5B, 0x25, 0x71, 0x47, 0x1E, 0x3F, 0x6A, 0xA8, + 0xF0, 0xD9, 0x68, 0x81, 0x1A, 0xA7, 0xB4, 0x80, 0xB1, 0xFF, 0xBC, 0x19, 0x93, 0x44, 0x8E, 0xCB, + 0xE2, 0xA8, 0x19, 0xE2, 0x71, 0xCE, 0xD4, 0xF1, 0xBA, 0xCE, 0x49, 0x38, 0xBB, 0xE7, 0x2D, 0x32, + 0xBF, 0x2B, 0x42, 0x00, 0xF5, 0x32, 0x96, 0xC3, 0x5A, 0x37, 0xA7, 0xC9, 0x9F, 0x8E, 0xA0, 0x37, + 0x1B, 0xBC, 0x4E, 0xAF, 0x54, 0xAA, 0x28, 0x1C, 0x9F, 0xCD, 0x9B, 0xBE, 0x4D, 0xF3, 0x23, 0xA7, + 0x50, 0x89, 0x67, 0x9F, 0x91, 0x47, 0x99, 0x73, 0xF8, 0x88, 0xB1, 0x61, 0x16, 0x9C, 0xEB, 0x96, + 0xF8, 0x66, 0xCB, 0x46, 0xF9, 0xF2, 0xE6, 0xD1, 0xF4, 0x97, 0xD9, 0x63, 0x96, 0x32, 0x8C, 0xFD, + 0xF2, 0xEB, 0x2D, 0xB2, 0x67, 0x4E, 0xEB, 0xF7, 0x9A, 0x52, 0xBF, 0x9E, 0x5D, 0x1E, 0x0A, 0x4D, + 0x9F, 0xEE, 0x9D, 0x45, 0x2E, 0x90, 0xE5, 0x16, 0x18, 0x98, 0x5D, 0x3C, 0x6F, 0xC9, 0x88, 0x21, + 0x03, 0xCC, 0x8E, 0x7B, 0xAF, 0xF1, 0x5B, 0xF2, 0x19, 0xE7, 0xF2, 0x58, 0xBE, 0xBC, 0x34, 0x38, + 0xBC, 0x37, 0xAD, 0x5F, 0xB1, 0x90, 0x7E, 0xDA, 0xF4, 0x05, 0xAD, 0x5B, 0xBE, 0x40, 0xDC, 0x28, + 0xFC, 0xFD, 0xFD, 0xE4, 0x11, 0xE6, 0xC0, 0x15, 0x80, 0x20, 0x44, 0x56, 0xC5, 0xA3, 0x85, 0x08, + 0x51, 0x2C, 0x2D, 0x10, 0x55, 0x42, 0x64, 0x4B, 0x8B, 0xC6, 0x6F, 0x36, 0x10, 0x17, 0x9C, 0x25, + 0xE7, 0xFF, 0xBD, 0x60, 0xB8, 0xC6, 0x09, 0x8E, 0xE1, 0x49, 0x8A, 0x48, 0x94, 0x7C, 0xAE, 0xB8, + 0xDC, 0x93, 0x42, 0xE9, 0x52, 0x25, 0x68, 0xC6, 0xA4, 0x31, 0xC2, 0x81, 0xAC, 0xC5, 0xAF, 0xBF, + 0xED, 0xA0, 0x2B, 0x57, 0xAF, 0xC9, 0x1E, 0x1C, 0xE2, 0xDA, 0x49, 0x7C, 0xB0, 0xAE, 0xDA, 0xB5, + 0x6A, 0x2E, 0x7B, 0xE6, 0x40, 0x30, 0x1A, 0xD5, 0xAB, 0x2B, 0x7B, 0xE6, 0xDC, 0xBA, 0x7D, 0x5B, + 0xB6, 0x8C, 0x81, 0x88, 0x5E, 0x93, 0xB7, 0x1A, 0x52, 0xB4, 0x22, 0x6E, 0x3F, 0x7D, 0xBD, 0x56, + 0x5C, 0x4C, 0x9B, 0xD6, 0x2C, 0xA3, 0x79, 0x33, 0x27, 0xC9, 0x23, 0xEC, 0x13, 0x15, 0xBD, 0x44, + 0xD4, 0x88, 0x69, 0xD1, 0xA6, 0x79, 0x53, 0xEA, 0xDA, 0xF1, 0x7D, 0xD9, 0xF3, 0x1C, 0x90, 0xE5, + 0x1D, 0x35, 0x6D, 0x02, 0x35, 0x7C, 0xBD, 0x8E, 0xB8, 0xE9, 0xF9, 0xFA, 0xFA, 0x8A, 0xDF, 0x14, + 0x6E, 0x14, 0xE1, 0xBD, 0xBA, 0xC9, 0xA3, 0xAC, 0xD1, 0x72, 0xAE, 0x67, 0x15, 0x3C, 0x5A, 0x88, + 0x4C, 0x0E, 0x64, 0x4B, 0x10, 0x2D, 0xD2, 0xB3, 0x34, 0x70, 0x67, 0x1E, 0x31, 0xB8, 0x3F, 0x7D, + 0x3A, 0x7C, 0xB0, 0xD5, 0xE6, 0x67, 0x60, 0xC9, 0x1A, 0x6F, 0x6F, 0x6F, 0x1A, 0xD4, 0xAF, 0x97, + 0x10, 0x04, 0x2D, 0xB0, 0x3F, 0xA2, 0x7F, 0x1F, 0x71, 0x9C, 0x25, 0xF0, 0x2B, 0xFC, 0xA2, 0x88, + 0x91, 0x89, 0x5C, 0x8A, 0xF5, 0xD1, 0xE0, 0xB5, 0xDA, 0x56, 0x5B, 0x2B, 0xC5, 0x92, 0xCB, 0x96, + 0x4D, 0x7F, 0x3E, 0x67, 0xFC, 0x9D, 0x16, 0x09, 0x8A, 0xD5, 0x65, 0x14, 0x88, 0xD0, 0x98, 0xE1, + 0x43, 0x84, 0x85, 0x88, 0x90, 0xB7, 0x1A, 0xBD, 0xCF, 0x66, 0xC9, 0xE2, 0x15, 0xAB, 0x45, 0xA4, + 0x4F, 0x8B, 0x2E, 0x1D, 0xDE, 0x17, 0x9B, 0x27, 0xD2, 0xA6, 0x79, 0x33, 0xCD, 0x9B, 0x19, 0xA8, + 0x5F, 0xB7, 0x96, 0x6E, 0x84, 0xEC, 0xD2, 0x65, 0xEB, 0xE8, 0x63, 0x56, 0xC1, 0xA3, 0x85, 0x08, + 0xE6, 0xB1, 0x16, 0x18, 0xC3, 0xA3, 0x5A, 0x7C, 0xE0, 0xC7, 0x9F, 0x88, 0xB1, 0xB9, 0xA5, 0x5F, + 0xA7, 0x6C, 0xE9, 0x52, 0x54, 0xBD, 0xCA, 0xCB, 0x56, 0x1B, 0x72, 0x40, 0xEC, 0x01, 0xBF, 0x4D, + 0xE1, 0x42, 0x05, 0x65, 0x4F, 0x1B, 0x84, 0xED, 0x2B, 0x57, 0xB4, 0x9E, 0xAB, 0x07, 0x1C, 0x51, + 0x45, 0xA9, 0x6A, 0x54, 0x7D, 0x59, 0xE4, 0xEB, 0x58, 0x6E, 0xAD, 0xDE, 0x6B, 0x22, 0x8F, 0xB0, + 0x06, 0x51, 0xB5, 0x8D, 0x9B, 0xBF, 0x93, 0xBD, 0xF4, 0xD3, 0xF8, 0xAD, 0x46, 0x62, 0xC8, 0x9A, + 0x5E, 0xD6, 0x6F, 0xFA, 0x86, 0xE6, 0x2F, 0x5E, 0x2E, 0x7B, 0xE6, 0x40, 0x80, 0x60, 0x0D, 0x79, + 0x2A, 0xF8, 0xBF, 0xE9, 0x81, 0x1B, 0x9D, 0xDE, 0xF0, 0x1C, 0x3E, 0xCD, 0xAC, 0x8A, 0x47, 0x0B, + 0x11, 0x9C, 0xCF, 0xC8, 0x07, 0xD2, 0x03, 0x89, 0x6B, 0x08, 0xEB, 0x37, 0x6B, 0xDB, 0x89, 0xDE, + 0x7D, 0xBF, 0x33, 0x8D, 0x18, 0x3B, 0x89, 0xD6, 0xAC, 0xFF, 0x8A, 0x4E, 0xFE, 0x75, 0x26, 0x4D, + 0x51, 0x0F, 0x35, 0x15, 0xCB, 0x97, 0x93, 0x2D, 0xDB, 0x54, 0xAE, 0x58, 0x41, 0xB6, 0xCC, 0xD1, + 0xF2, 0xCF, 0xD8, 0x02, 0xD1, 0xB4, 0x83, 0x87, 0x8F, 0xD2, 0x57, 0xDF, 0x7C, 0x47, 0x93, 0x67, + 0xCC, 0xA1, 0x16, 0x61, 0x5D, 0x44, 0xC2, 0x66, 0x46, 0x69, 0xD9, 0xEC, 0x1D, 0xD9, 0x4A, 0x1F, + 0xB0, 0x3A, 0xF5, 0x78, 0xBC, 0x60, 0x01, 0xD9, 0xF2, 0x3C, 0x02, 0xB3, 0x67, 0x17, 0x43, 0x7F, + 0x5B, 0xE4, 0xCB, 0xA7, 0x6D, 0x2D, 0x25, 0x26, 0xA6, 0xDD, 0xC9, 0xED, 0x29, 0x78, 0xB4, 0x10, + 0x21, 0x3B, 0xB8, 0x57, 0x97, 0x0E, 0xB2, 0x67, 0x1B, 0x98, 0xC5, 0x3F, 0x6E, 0xDD, 0x46, 0x33, + 0xA2, 0xE6, 0x53, 0x87, 0xEE, 0x7D, 0xA8, 0x49, 0xAB, 0xF6, 0x34, 0x6D, 0xB6, 0x76, 0x24, 0xCB, + 0x16, 0xC8, 0xAC, 0x35, 0xC2, 0x53, 0x4F, 0xA4, 0x66, 0x03, 0xAB, 0xB9, 0x7A, 0x5D, 0x3B, 0x5A, + 0x85, 0xC8, 0x13, 0x72, 0x82, 0x30, 0xDC, 0x19, 0x3C, 0x62, 0x0C, 0xB5, 0xEC, 0xD0, 0x8D, 0x6A, + 0x37, 0x6A, 0x4A, 0x6F, 0xB7, 0x08, 0xA3, 0x1E, 0xE1, 0x83, 0x69, 0xC2, 0xB4, 0x48, 0xE1, 0x10, + 0x76, 0xC4, 0x5C, 0x3D, 0x88, 0xF2, 0xD9, 0xCB, 0x3E, 0xCE, 0x08, 0x93, 0x67, 0x45, 0xD1, 0xCD, + 0x5B, 0x9E, 0x39, 0xA7, 0x50, 0xAE, 0x20, 0xFB, 0xCE, 0x7C, 0x88, 0x15, 0x63, 0x8E, 0x47, 0x0B, + 0x11, 0x78, 0xE7, 0x8D, 0x06, 0x34, 0x2A, 0x62, 0x20, 0x85, 0xE6, 0x4E, 0x5B, 0xE6, 0x2A, 0x72, + 0x81, 0xD6, 0x6E, 0xD8, 0x44, 0x6D, 0x3A, 0xF7, 0x10, 0x79, 0x3E, 0x7A, 0xFE, 0x26, 0x4B, 0x82, + 0x82, 0x8C, 0x85, 0x8F, 0x83, 0x74, 0x7E, 0xB0, 0x98, 0x06, 0x15, 0x11, 0x34, 0x35, 0x28, 0x13, + 0x69, 0xDD, 0xA9, 0xBB, 0x48, 0x94, 0xC4, 0x70, 0x07, 0x09, 0x81, 0x70, 0x9E, 0xAB, 0x13, 0x10, + 0x1D, 0x89, 0x9E, 0x48, 0x3A, 0x8A, 0x5B, 0xB7, 0x6E, 0x8B, 0xBC, 0x26, 0x4F, 0x24, 0x30, 0x87, + 0x7D, 0x91, 0xF1, 0xE1, 0xF5, 0xDA, 0xAC, 0xF0, 0x78, 0x21, 0x02, 0xB5, 0x6A, 0x54, 0xA5, 0x55, + 0x8B, 0xA2, 0xE8, 0xA3, 0xBE, 0x3D, 0x45, 0x39, 0x85, 0xAD, 0xE1, 0x9A, 0x25, 0x10, 0x20, 0x64, + 0x3E, 0x8F, 0x1A, 0x37, 0x59, 0xEE, 0xB1, 0x83, 0x03, 0x0A, 0x66, 0x90, 0xD4, 0x67, 0x02, 0x56, + 0xD9, 0xC7, 0xA3, 0xC7, 0x1B, 0xB2, 0xCC, 0xE0, 0x00, 0x87, 0xFF, 0x41, 0x2F, 0x6A, 0x66, 0x94, + 0x90, 0x5C, 0x8F, 0xBE, 0x9A, 0x1D, 0xE2, 0x8A, 0x3A, 0x2C, 0x57, 0xC4, 0xE8, 0x4D, 0x47, 0x0B, + 0x6F, 0xAF, 0x2C, 0x71, 0x49, 0x39, 0x9C, 0x2C, 0xF3, 0xAD, 0x05, 0x04, 0x04, 0xD0, 0x1B, 0xF5, + 0x5F, 0xA3, 0x29, 0x63, 0x47, 0xD2, 0x37, 0x6B, 0x97, 0xD3, 0xD4, 0x71, 0xA3, 0x44, 0x5D, 0x10, + 0x84, 0xC9, 0x88, 0xA9, 0x8C, 0x52, 0x8E, 0xDD, 0x7B, 0xF7, 0xCB, 0x9E, 0x3E, 0x37, 0x95, 0xBB, + 0xBD, 0x11, 0x50, 0x6C, 0xAB, 0x05, 0x22, 0x52, 0xA6, 0xD4, 0x02, 0x64, 0x84, 0xC3, 0x2A, 0xD3, + 0x02, 0x4E, 0x4F, 0x14, 0xE7, 0x22, 0x9F, 0x08, 0xB9, 0x34, 0x0B, 0x66, 0x4F, 0xA3, 0x6F, 0xBF, + 0xFC, 0x9C, 0xE6, 0x4C, 0x1B, 0x4F, 0x55, 0x5F, 0xD6, 0x4E, 0xCC, 0x34, 0x0A, 0x8A, 0x73, 0x1D, + 0x01, 0x22, 0x6F, 0x08, 0xCF, 0xEB, 0xA5, 0x4A, 0x38, 0x63, 0x88, 0x16, 0x9F, 0x90, 0x20, 0x5B, + 0xFA, 0xA0, 0x38, 0x95, 0xC9, 0x5C, 0xB2, 0xA4, 0x7C, 0xC3, 0x22, 0x82, 0x53, 0x19, 0xC5, 0x89, + 0x42, 0x98, 0xD6, 0xAD, 0xA0, 0xC8, 0xC9, 0x63, 0xE9, 0xED, 0x46, 0xF5, 0x6C, 0x5A, 0x4B, 0x3F, + 0x6C, 0xB5, 0x5F, 0x76, 0x70, 0xE6, 0xEF, 0xB3, 0xB2, 0x65, 0x9B, 0x33, 0x67, 0xFF, 0x91, 0x2D, + 0x73, 0x42, 0x55, 0xA1, 0xDD, 0xE5, 0xAB, 0xB4, 0x43, 0xDF, 0x85, 0x0A, 0x16, 0xA0, 0x45, 0x73, + 0xA6, 0xD3, 0x2C, 0xE5, 0x9C, 0x3B, 0xBE, 0xDF, 0x8A, 0xEA, 0xD4, 0xAC, 0x46, 0xC5, 0x9F, 0x7E, + 0xEA, 0xE1, 0x05, 0x9F, 0x91, 0x3B, 0xBA, 0xA3, 0x40, 0x7A, 0xC1, 0xC7, 0x1F, 0xF5, 0x13, 0x09, + 0x8B, 0x10, 0x7C, 0x2D, 0x9C, 0x31, 0x44, 0x8B, 0xB3, 0x93, 0xBD, 0x9C, 0xD6, 0xF2, 0x1C, 0xC6, + 0x31, 0x78, 0xB4, 0x10, 0xED, 0x3F, 0x78, 0xD8, 0x6A, 0x83, 0xC3, 0xD7, 0x12, 0x0C, 0x69, 0x10, + 0xB2, 0xEF, 0xDF, 0xBB, 0x3B, 0x2D, 0x9D, 0x37, 0x53, 0x37, 0x07, 0x04, 0x75, 0x5F, 0xF6, 0xD8, + 0xB1, 0x67, 0x9F, 0x6C, 0xD9, 0x06, 0xD3, 0x60, 0x68, 0x81, 0x19, 0x07, 0xC1, 0xED, 0x3B, 0x77, + 0xE9, 0xB4, 0x8E, 0xA8, 0x61, 0x88, 0x09, 0x87, 0xB2, 0x1E, 0xA8, 0xD4, 0x77, 0x36, 0x1F, 0x84, + 0xB5, 0xA5, 0xBA, 0xB5, 0x6A, 0x88, 0x36, 0x12, 0xF9, 0xF4, 0x4A, 0x4F, 0x1E, 0xD5, 0x10, 0xCD, + 0xCF, 0x4F, 0x3B, 0xE7, 0xEB, 0xE2, 0x25, 0xDB, 0xB9, 0x3A, 0x5A, 0x53, 0x7B, 0x30, 0x8F, 0x1E, + 0x8F, 0x16, 0x22, 0x14, 0xAB, 0xF6, 0x1E, 0x10, 0x61, 0xBE, 0x0D, 0x1C, 0x6A, 0xD3, 0x62, 0x80, + 0xB5, 0x01, 0x07, 0xB7, 0x16, 0x88, 0x5C, 0xD9, 0xE3, 0xD0, 0x91, 0x63, 0x76, 0x2B, 0xD6, 0x61, + 0x0D, 0xED, 0xDC, 0xA3, 0x9D, 0x50, 0x89, 0xEC, 0x6B, 0x70, 0xCD, 0x46, 0xAD, 0x17, 0xE6, 0xC5, + 0xB1, 0x05, 0xAA, 0xF5, 0x9D, 0x4D, 0x88, 0xAA, 0xB6, 0x0E, 0xCE, 0xD9, 0xFE, 0xBD, 0xF5, 0x33, + 0x8A, 0x1F, 0xC5, 0x10, 0x4D, 0x6F, 0xFE, 0x6A, 0xA4, 0x36, 0xD8, 0xFA, 0x3F, 0x66, 0xA4, 0x40, + 0x9A, 0x49, 0x3F, 0x1E, 0x2D, 0x44, 0x4F, 0x16, 0xB5, 0x0E, 0xA5, 0xA3, 0xA6, 0x67, 0xEB, 0x6F, + 0xDB, 0x65, 0x4F, 0x9B, 0x6B, 0xD7, 0xB5, 0xA7, 0x25, 0x35, 0x1A, 0xD2, 0x1E, 0x3D, 0x7E, 0xAA, + 0xEE, 0x85, 0x85, 0x84, 0xC3, 0xD1, 0x13, 0xA6, 0x69, 0x5E, 0x0C, 0xB0, 0xCC, 0x6A, 0x55, 0xAF, + 0x2A, 0x7B, 0xFA, 0xD8, 0xAA, 0xC4, 0xC7, 0xB9, 0x23, 0x0D, 0xC1, 0xD5, 0x28, 0xF3, 0x7C, 0x49, + 0xE1, 0xA3, 0xD3, 0xE2, 0x51, 0x0C, 0xD1, 0x90, 0x34, 0xAA, 0x05, 0x26, 0x7E, 0xDB, 0xA2, 0x53, + 0x7C, 0xFB, 0xDB, 0x8E, 0x5D, 0x2E, 0xF5, 0xDD, 0xA1, 0xE4, 0x03, 0xD3, 0xB4, 0x4C, 0x8B, 0x9C, + 0x47, 0x7B, 0x1D, 0x90, 0x1B, 0xE6, 0xCA, 0x78, 0xB4, 0x10, 0x55, 0x7D, 0x59, 0x3B, 0x33, 0x78, + 0xC2, 0xD4, 0x48, 0xC5, 0x22, 0xD1, 0x1E, 0x1A, 0x21, 0xC9, 0x71, 0xD3, 0x96, 0xEF, 0x65, 0xCF, + 0x9C, 0x17, 0xCB, 0x95, 0x95, 0x2D, 0xDB, 0x60, 0x08, 0xD7, 0xA1, 0x7B, 0x5F, 0x91, 0xD7, 0x83, + 0x2A, 0x76, 0x58, 0x60, 0xF0, 0x3D, 0xA0, 0x70, 0xB6, 0x73, 0xCF, 0x70, 0x5D, 0xF3, 0x1F, 0xBE, + 0x1E, 0xD3, 0xB0, 0xD0, 0xD6, 0x14, 0xA8, 0x98, 0x32, 0x42, 0xCB, 0xAA, 0x43, 0x86, 0x38, 0x72, + 0x8C, 0x5C, 0xD5, 0xD9, 0xDA, 0xB5, 0x63, 0x3B, 0xDD, 0x59, 0x08, 0x1C, 0x3D, 0x44, 0xB3, 0x2C, + 0x4B, 0x51, 0x33, 0x6E, 0xEA, 0x2C, 0xB1, 0x0C, 0x12, 0xCA, 0x7C, 0x30, 0x54, 0xFF, 0xE5, 0xF7, + 0x1D, 0xF4, 0xE9, 0xA4, 0xE9, 0x14, 0x31, 0x6A, 0x9C, 0x21, 0xAB, 0x37, 0x33, 0xD8, 0xBD, 0x6F, + 0x3F, 0x4D, 0x9F, 0x33, 0x5F, 0x14, 0xFB, 0x3E, 0x5E, 0xA8, 0xA0, 0x98, 0x4F, 0x6A, 0xED, 0xC6, + 0xAF, 0xE5, 0xB3, 0x9E, 0x87, 0x47, 0x0B, 0x11, 0x86, 0x58, 0x5A, 0x11, 0x31, 0xA4, 0xD2, 0x0F, + 0x18, 0x3A, 0x4A, 0x24, 0x2E, 0xCE, 0x8C, 0x8A, 0x16, 0x33, 0x37, 0xE2, 0xB1, 0xCB, 0x87, 0x03, + 0x44, 0xD9, 0x87, 0x65, 0x1E, 0x0F, 0x08, 0x09, 0x0E, 0xA6, 0x3A, 0xAF, 0x1A, 0x5F, 0x10, 0x10, + 0x02, 0x84, 0x4C, 0xE7, 0x26, 0xAD, 0x3B, 0x50, 0xCD, 0x06, 0x8D, 0xE9, 0x9D, 0x96, 0x61, 0x62, + 0x71, 0x40, 0xDC, 0x91, 0xB5, 0xC8, 0x9E, 0x3D, 0x80, 0x3E, 0x68, 0xDF, 0x56, 0xF6, 0x52, 0x92, + 0x31, 0x4B, 0x3E, 0x6B, 0x5E, 0x34, 0x6B, 0xE2, 0xBB, 0x1F, 0xB7, 0x52, 0x8B, 0xF6, 0x5D, 0xC5, + 0xEB, 0x2F, 0x5C, 0xF6, 0x39, 0xCD, 0x9E, 0xBF, 0x88, 0x3E, 0x1A, 0x36, 0x9A, 0x5A, 0x2A, 0xFB, + 0x8E, 0xFF, 0x79, 0x4A, 0x1E, 0x65, 0xCD, 0x83, 0x07, 0xCE, 0x75, 0x62, 0xA3, 0x72, 0xBF, 0x47, + 0xA7, 0x30, 0xD9, 0xB3, 0xC6, 0x91, 0x43, 0x34, 0x44, 0x0E, 0xB5, 0xEA, 0xF9, 0x00, 0x44, 0x7C, + 0xDD, 0xC6, 0xCD, 0xA2, 0xCC, 0x07, 0xB9, 0x59, 0x43, 0x15, 0x01, 0xC2, 0x14, 0x25, 0xAE, 0xE0, + 0xE4, 0x37, 0xB1, 0xF4, 0xF3, 0x2F, 0xA8, 0x47, 0xE7, 0xF6, 0x74, 0xE8, 0xE8, 0x31, 0x51, 0x08, + 0x8D, 0xE0, 0x0A, 0x26, 0xC6, 0xF3, 0x54, 0x3C, 0x5A, 0x88, 0x50, 0x5C, 0x38, 0xE0, 0xC3, 0xEE, + 0xB2, 0x67, 0x0D, 0x4A, 0x39, 0x50, 0x19, 0x1E, 0xBD, 0x64, 0x85, 0x78, 0x3C, 0x7A, 0xFC, 0x4F, + 0xF9, 0x8C, 0x39, 0x08, 0x43, 0xA3, 0x90, 0x15, 0x62, 0x61, 0x0F, 0x3D, 0xA7, 0xAC, 0x3D, 0xF0, + 0xFA, 0x96, 0xA5, 0x01, 0xCD, 0x1A, 0xBF, 0x29, 0x5B, 0xD6, 0x60, 0xB6, 0x49, 0x58, 0x5C, 0x0B, + 0x96, 0xAE, 0x14, 0x3F, 0xD0, 0xED, 0xBB, 0xF6, 0xD8, 0x4D, 0x70, 0xC4, 0x94, 0x28, 0x48, 0x98, + 0x74, 0x26, 0xF5, 0x5F, 0xAB, 0x2D, 0xD2, 0x0E, 0xB4, 0x70, 0xE4, 0x10, 0x0D, 0x37, 0x8E, 0x86, + 0xF5, 0x6C, 0x4F, 0x09, 0xAB, 0x45, 0xC1, 0x02, 0xF9, 0x6D, 0xD6, 0x8A, 0x65, 0x16, 0x37, 0x6F, + 0xDE, 0x12, 0x95, 0xFB, 0x37, 0x6E, 0xDC, 0x14, 0xD6, 0x51, 0xCD, 0x6A, 0x55, 0x3C, 0x3A, 0x11, + 0xD2, 0xA3, 0x85, 0x08, 0x20, 0x72, 0x33, 0x7C, 0x50, 0xB8, 0x21, 0x11, 0xD1, 0x02, 0x16, 0xD5, + 0xC8, 0x88, 0x81, 0x86, 0x73, 0x73, 0x1A, 0xD5, 0xAF, 0x9B, 0xA6, 0x82, 0x4E, 0xE4, 0x0D, 0xA1, + 0x90, 0xB5, 0x76, 0x8D, 0x6A, 0x72, 0x4F, 0x2A, 0xAF, 0xD7, 0xAE, 0x49, 0x6F, 0x34, 0xD0, 0xF6, + 0xAB, 0xE8, 0x81, 0xD7, 0xC3, 0x24, 0x6F, 0x7A, 0x15, 0xF2, 0x67, 0xCF, 0x9D, 0x93, 0x2D, 0xE7, + 0x80, 0xFC, 0xA7, 0xFE, 0xCA, 0xCD, 0x41, 0xEF, 0xA2, 0x72, 0xE4, 0x10, 0xAD, 0x67, 0x67, 0xFD, + 0x89, 0xE2, 0xB4, 0xC0, 0xB0, 0x18, 0xE9, 0x1C, 0xAE, 0xB0, 0x50, 0x23, 0x8A, 0xA7, 0xF1, 0x5D, + 0xC0, 0x12, 0xC2, 0x74, 0x22, 0xDB, 0x7E, 0xDF, 0x69, 0xB7, 0x98, 0xDA, 0x9D, 0xF1, 0x78, 0x21, + 0x02, 0x10, 0xA3, 0xE5, 0xF3, 0x67, 0x8B, 0x99, 0x00, 0x83, 0x72, 0x1A, 0x5B, 0x99, 0x03, 0x33, + 0xFD, 0xE1, 0xF8, 0x65, 0xF3, 0x23, 0xC5, 0xA4, 0x63, 0x69, 0x01, 0xD5, 0xE5, 0x48, 0x32, 0xD4, + 0x4B, 0x03, 0x30, 0x51, 0xE1, 0x85, 0xB2, 0x62, 0x6E, 0x1F, 0x4C, 0xED, 0xA1, 0xC7, 0xC0, 0x0F, + 0x7B, 0x08, 0x61, 0xB1, 0x37, 0x21, 0x19, 0xAC, 0x36, 0xDC, 0xC9, 0x31, 0x31, 0x1A, 0x26, 0x79, + 0x7B, 0xAE, 0xB8, 0xF6, 0xE4, 0x68, 0x98, 0xD4, 0xDD, 0xD9, 0x60, 0x65, 0x91, 0x56, 0xEF, 0xEA, + 0x2F, 0x39, 0xED, 0xA8, 0x21, 0x1A, 0x66, 0x4B, 0x40, 0x7E, 0x18, 0xBE, 0x5F, 0xBD, 0x61, 0x1A, + 0x80, 0x38, 0xE2, 0x37, 0x12, 0x1D, 0x39, 0xD5, 0x65, 0x2E, 0xF6, 0xCE, 0xED, 0xDB, 0xD0, 0xB1, + 0x13, 0x7F, 0xD2, 0xB9, 0x7F, 0xFF, 0x15, 0x8F, 0xBF, 0x6E, 0xDF, 0x29, 0xAC, 0x66, 0x4F, 0x25, + 0x4B, 0x2D, 0x27, 0x04, 0xE0, 0x07, 0x38, 0x74, 0xF4, 0x38, 0x9D, 0x38, 0x79, 0x4A, 0x94, 0x4D, + 0xDC, 0x8D, 0x89, 0x51, 0x86, 0x2C, 0xF1, 0x62, 0xE6, 0xBC, 0x9C, 0xCA, 0x0F, 0x17, 0x45, 0xAB, + 0xCF, 0x3E, 0x5D, 0x4C, 0x44, 0x79, 0x6C, 0x99, 0xC2, 0x7A, 0xAB, 0x44, 0xF4, 0xEA, 0xDA, 0xF1, + 0xE1, 0x2C, 0x81, 0x98, 0x6E, 0x04, 0x66, 0x35, 0xF2, 0x97, 0x30, 0x94, 0x42, 0x75, 0x35, 0xC2, + 0xDA, 0xA8, 0xE5, 0x42, 0xF5, 0xFD, 0x13, 0x1A, 0x51, 0x3D, 0x3D, 0xE0, 0xB7, 0xDA, 0xBD, 0xEF, + 0x80, 0x48, 0x0F, 0xB8, 0x72, 0xF5, 0x2A, 0xC5, 0x29, 0xE7, 0x8C, 0x3B, 0x37, 0xA6, 0x4A, 0xC5, + 0xD2, 0x34, 0x2F, 0x2A, 0x77, 0x4E, 0xF5, 0x3C, 0x37, 0x18, 0xE6, 0x68, 0x25, 0xEF, 0xF9, 0xFB, + 0xFB, 0x8B, 0x89, 0xBB, 0x00, 0x0A, 0x64, 0xE3, 0x35, 0x96, 0xE8, 0x09, 0x54, 0x5E, 0xD7, 0xE8, + 0x52, 0x4A, 0xF0, 0x85, 0x69, 0x4D, 0x8D, 0x1A, 0x9C, 0x2B, 0x97, 0x4D, 0x2B, 0x14, 0xC3, 0xC8, + 0xEB, 0x3A, 0xD1, 0x49, 0x60, 0xF9, 0xF7, 0x70, 0xC4, 0x6B, 0xFD, 0x54, 0xF1, 0xF9, 0x8D, 0x94, + 0xEC, 0x20, 0x9A, 0xB8, 0x73, 0xF7, 0x3E, 0x91, 0x3A, 0x71, 0xF3, 0xF6, 0x6D, 0x8A, 0x8D, 0x8D, + 0xA3, 0x20, 0xE5, 0x33, 0x16, 0x2F, 0xF6, 0x14, 0x55, 0xAF, 0x52, 0xD9, 0x6C, 0x58, 0xAC, 0xF7, + 0xDD, 0xA9, 0xDF, 0x0B, 0x01, 0x01, 0xAD, 0x89, 0xE6, 0xB2, 0x29, 0x96, 0x28, 0x86, 0x54, 0xB6, + 0xC0, 0x44, 0xF9, 0xF8, 0xDD, 0x59, 0xA2, 0xFE, 0xDF, 0x98, 0xC0, 0x50, 0x1A, 0x4B, 0x1B, 0x19, + 0x99, 0x82, 0xC6, 0x9D, 0xC9, 0x72, 0x42, 0xE4, 0x28, 0x8C, 0x08, 0x11, 0xC3, 0x30, 0xC6, 0xC8, + 0x12, 0x43, 0x33, 0x86, 0x61, 0x5C, 0x1B, 0x16, 0x22, 0x86, 0x61, 0x9C, 0x0E, 0x0B, 0x11, 0xC3, + 0x30, 0x4E, 0x87, 0x7D, 0x44, 0xE9, 0xC4, 0x55, 0x7C, 0x44, 0x48, 0xFD, 0xC7, 0x02, 0x8C, 0x5A, + 0xA0, 0x6E, 0x0E, 0xE1, 0xDF, 0xD6, 0xEF, 0x35, 0x11, 0x6D, 0x4C, 0x64, 0x8F, 0x55, 0x35, 0xEC, + 0x81, 0x29, 0x52, 0x80, 0xFA, 0x75, 0xB7, 0x7D, 0xBB, 0x41, 0xB6, 0x88, 0x26, 0x4E, 0x9F, 0x6D, + 0xB6, 0x72, 0x2C, 0x72, 0xB5, 0xDE, 0x6A, 0x58, 0x4F, 0xF6, 0xB4, 0xC1, 0x14, 0xB8, 0xF8, 0x1B, + 0x9C, 0x2F, 0x96, 0x49, 0x02, 0xCF, 0x15, 0x7F, 0x5A, 0x44, 0xF7, 0xF0, 0xB7, 0x68, 0x83, 0xEA, + 0xF5, 0xDE, 0x16, 0x8F, 0xB6, 0xC0, 0xF9, 0x21, 0xD8, 0x60, 0xF4, 0xB3, 0xA8, 0x3F, 0x07, 0xFA, + 0xEA, 0xE9, 0x7C, 0x2D, 0xBF, 0x3F, 0x7C, 0x4E, 0x5B, 0xDF, 0x29, 0xE6, 0xB6, 0x42, 0x19, 0x0E, + 0x1E, 0x19, 0xC7, 0xC1, 0x16, 0x91, 0x07, 0x83, 0x0B, 0x1E, 0x17, 0x7F, 0xA7, 0x9E, 0xE1, 0x0F, + 0x2F, 0xFE, 0x8C, 0x82, 0xD7, 0x53, 0x8B, 0x10, 0xE6, 0x1B, 0x32, 0x22, 0x42, 0xB8, 0xB0, 0xF1, + 0x77, 0xEA, 0xF3, 0x30, 0x89, 0x13, 0x96, 0xB6, 0x76, 0xD5, 0x49, 0xD2, 0x2C, 0xC1, 0x79, 0x0E, + 0x1B, 0x33, 0xC1, 0xEC, 0x3B, 0x60, 0x32, 0x0E, 0x0B, 0x91, 0x87, 0x01, 0x61, 0xC0, 0x86, 0x79, + 0x80, 0x4C, 0x39, 0x53, 0x98, 0x84, 0x2D, 0x6A, 0xC1, 0x12, 0xD1, 0x87, 0x35, 0x80, 0xCD, 0x64, + 0x81, 0x98, 0x30, 0xED, 0xC7, 0xA6, 0x97, 0x6B, 0x05, 0x4B, 0x01, 0xD6, 0x90, 0x09, 0x08, 0x10, + 0xDE, 0xC7, 0x1E, 0x13, 0xA7, 0x47, 0x3E, 0x9C, 0x08, 0xCE, 0x74, 0x0E, 0xEA, 0xF7, 0x17, 0xFB, + 0x2A, 0x58, 0x2F, 0x3A, 0x80, 0xD7, 0x37, 0x7D, 0x1E, 0xF5, 0x56, 0xA8, 0x40, 0xFE, 0x0C, 0x7F, + 0x96, 0xB4, 0x60, 0x7A, 0x5F, 0xF5, 0xFB, 0x18, 0xB1, 0xC6, 0x18, 0xE3, 0xF0, 0xD0, 0x2C, 0x9D, + 0x20, 0x37, 0x68, 0x30, 0xD6, 0x87, 0xB7, 0x58, 0x02, 0xC6, 0xD9, 0x43, 0x33, 0xF5, 0x10, 0x0A, + 0x77, 0x6D, 0x93, 0x70, 0xE0, 0x82, 0xDC, 0xBC, 0x36, 0x75, 0x79, 0x1F, 0x5B, 0x7F, 0x07, 0x2C, + 0x9F, 0x9F, 0x3F, 0x6B, 0x8A, 0xE8, 0x9B, 0x04, 0x05, 0x17, 0xB9, 0x69, 0x08, 0x67, 0x0F, 0xF5, + 0x70, 0x0B, 0xC2, 0x85, 0x8B, 0x1A, 0xC0, 0x3A, 0xC2, 0x92, 0xE0, 0x85, 0x0A, 0xE6, 0x7F, 0x28, + 0x68, 0xEA, 0x63, 0x2D, 0x87, 0x51, 0x7A, 0xD8, 0xFB, 0x2C, 0xB6, 0x5E, 0x53, 0xEB, 0x6F, 0xF5, + 0x5E, 0x0F, 0xE7, 0xDB, 0x3C, 0x2C, 0x75, 0x25, 0x60, 0x7C, 0x27, 0x96, 0x22, 0xC8, 0xA4, 0x0F, + 0xB6, 0x88, 0xD2, 0x49, 0xF9, 0xB2, 0xA5, 0x69, 0xF6, 0xD4, 0x71, 0x76, 0xB3, 0xA7, 0x9D, 0x09, + 0xFC, 0x42, 0x26, 0xF4, 0xA6, 0xA6, 0x35, 0x8A, 0xDA, 0xAA, 0xC1, 0xC5, 0x37, 0x6A, 0xE8, 0x40, + 0xD1, 0x36, 0x82, 0xFA, 0x3C, 0xE0, 0xA7, 0xC2, 0x45, 0x6E, 0x5A, 0x78, 0x11, 0xFE, 0x25, 0x23, + 0x56, 0x95, 0x2B, 0xA0, 0xFE, 0x1C, 0x20, 0xA3, 0xDF, 0x29, 0x93, 0x0A, 0x0B, 0x51, 0x06, 0x40, + 0x1D, 0xD3, 0xDC, 0xE9, 0x13, 0xD3, 0x5D, 0xE8, 0xFA, 0xA8, 0x51, 0xFB, 0x63, 0x32, 0x3A, 0x44, + 0x51, 0xAF, 0xB7, 0x76, 0xE1, 0xE2, 0x65, 0xD9, 0x32, 0x06, 0x9C, 0xE5, 0x6A, 0x60, 0x71, 0x60, + 0x68, 0x03, 0xEB, 0x02, 0xFE, 0x16, 0x3D, 0xFF, 0x15, 0x04, 0x0B, 0xD6, 0x8C, 0x7A, 0x53, 0x5B, + 0x2A, 0x99, 0x8D, 0xE5, 0x79, 0x62, 0x88, 0xC8, 0x38, 0x06, 0x16, 0xA2, 0x0C, 0x02, 0x8B, 0x68, + 0xF6, 0x94, 0x71, 0xC2, 0x42, 0x72, 0x05, 0x60, 0x69, 0x98, 0xA2, 0x63, 0x6A, 0x3F, 0x86, 0x23, + 0xA3, 0x3C, 0xB0, 0x04, 0x96, 0xAF, 0x5A, 0x27, 0x7B, 0xF6, 0x81, 0xAF, 0x07, 0x4B, 0x3A, 0x69, + 0x0D, 0x63, 0xE0, 0xFC, 0x85, 0x33, 0xDD, 0x95, 0xAD, 0x0B, 0xD3, 0x77, 0x0A, 0xD1, 0x34, 0x01, + 0xEB, 0xC8, 0xD2, 0x42, 0x62, 0xD2, 0x0F, 0x0B, 0x91, 0x03, 0x40, 0x1D, 0xD0, 0xE4, 0xB1, 0x23, + 0xC5, 0xC4, 0x66, 0xCE, 0xC6, 0x24, 0x40, 0xB8, 0x70, 0x4C, 0x17, 0x37, 0xAC, 0x21, 0x4B, 0xAB, + 0x24, 0x3D, 0x58, 0x0E, 0xB1, 0x60, 0xD9, 0x18, 0x01, 0xD6, 0x14, 0x84, 0x10, 0x3E, 0x95, 0x55, + 0x8B, 0xE6, 0xA6, 0x38, 0x9C, 0x55, 0xAF, 0x85, 0xF3, 0xD4, 0x8A, 0x9A, 0x69, 0x39, 0xAB, 0x33, + 0x2A, 0xA8, 0x77, 0xEE, 0x98, 0x0B, 0x9E, 0x65, 0x5F, 0x0B, 0xD3, 0x77, 0xAA, 0xB6, 0x0A, 0xBB, + 0x7A, 0xE8, 0xBA, 0xFD, 0xCE, 0x82, 0x85, 0xC8, 0x41, 0x60, 0xDA, 0x8D, 0xE1, 0x83, 0xFB, 0x53, + 0xF5, 0x57, 0x2A, 0xCB, 0x3D, 0xAE, 0x03, 0xFC, 0x30, 0x19, 0xBD, 0x7B, 0x43, 0x04, 0x60, 0xD5, + 0xA8, 0x89, 0x8A, 0x5E, 0x2C, 0x5B, 0xB6, 0x81, 0x25, 0x81, 0x21, 0x15, 0x84, 0x0B, 0xE7, 0x01, + 0x9F, 0xD0, 0xFC, 0x59, 0xE6, 0xEB, 0xC4, 0x69, 0x59, 0x44, 0x10, 0x1D, 0x1C, 0xAB, 0xDE, 0xEC, + 0xA5, 0x0A, 0x68, 0xA1, 0xB6, 0xC4, 0xD4, 0x02, 0x8D, 0x47, 0x75, 0x18, 0xDE, 0x9E, 0xE3, 0x19, + 0x82, 0x8E, 0x73, 0x82, 0xC3, 0xDB, 0x91, 0x16, 0x26, 0xC3, 0x42, 0xE4, 0x50, 0x30, 0x9D, 0x04, + 0x26, 0xD6, 0x72, 0x26, 0x88, 0xF0, 0x60, 0x53, 0x5F, 0x54, 0x8E, 0xC8, 0x79, 0x81, 0x08, 0xE0, + 0x35, 0xF1, 0x68, 0x02, 0x16, 0x02, 0x2E, 0x6C, 0x5B, 0xE0, 0xBD, 0xE1, 0x5B, 0x31, 0x45, 0xA2, + 0xE0, 0x17, 0xC2, 0xA3, 0xA5, 0xAF, 0xA7, 0x50, 0x01, 0x6B, 0xA1, 0x84, 0x95, 0x84, 0xD7, 0xB7, + 0xDC, 0xF4, 0x7C, 0x4A, 0x7A, 0xA8, 0xC5, 0x0B, 0xE7, 0xDC, 0xBC, 0x5D, 0xCA, 0x39, 0xE0, 0x51, + 0x6D, 0xD5, 0xE9, 0x89, 0x9C, 0xE9, 0x3B, 0x45, 0xD4, 0x11, 0x62, 0x6C, 0x24, 0x92, 0xC7, 0xA4, + 0x0D, 0x16, 0x22, 0x0F, 0x05, 0xF3, 0x43, 0x9B, 0xC0, 0xC5, 0x86, 0x0B, 0xD8, 0x11, 0xB4, 0x6E, + 0xDE, 0xC4, 0xCC, 0xF1, 0x0D, 0x5F, 0x91, 0x2D, 0x61, 0xC0, 0xB1, 0xEA, 0xE3, 0x4D, 0xA2, 0xA4, + 0x1E, 0xE6, 0xE0, 0xC2, 0xD6, 0xB2, 0x30, 0x20, 0x62, 0xA6, 0x61, 0x91, 0x7A, 0xBB, 0x70, 0x29, + 0x6D, 0xCE, 0x72, 0x08, 0x8C, 0x5A, 0x64, 0x60, 0x09, 0xE1, 0x1C, 0xD4, 0x56, 0x98, 0xE5, 0x31, + 0x4C, 0xE6, 0xC2, 0x42, 0xE4, 0xA1, 0xE0, 0xE2, 0x56, 0xDF, 0xB9, 0x21, 0x18, 0xEA, 0x8B, 0x3F, + 0xBD, 0x40, 0x54, 0x30, 0x4C, 0x33, 0x81, 0x8B, 0x19, 0xC9, 0x92, 0x7A, 0x40, 0x60, 0x56, 0x2D, + 0x9E, 0x2B, 0x86, 0x87, 0x68, 0xAB, 0x45, 0x09, 0x16, 0x16, 0x5E, 0xCB, 0x68, 0x3E, 0x52, 0x46, + 0xC0, 0xFB, 0x9B, 0x86, 0x54, 0xA6, 0x61, 0x2A, 0x1E, 0xD1, 0xC7, 0x73, 0xD8, 0x18, 0xE7, 0xC1, + 0x09, 0x8D, 0x0C, 0xC3, 0x38, 0x1D, 0xB6, 0x88, 0x18, 0x86, 0x71, 0x3A, 0x2C, 0x44, 0x0C, 0xC3, + 0x38, 0x19, 0xA2, 0xFF, 0x03, 0x3E, 0x9F, 0xCD, 0xAF, 0x59, 0xF8, 0xCE, 0xCA, 0x00, 0x00, 0x00, + 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82, 0x2C, 0x30, 0x0E, 0x20, 0x67, 0x17, 0x00 +}; //rtk-setup.png + +//Content of Battery0.png with gzip compression +static const uint8_t battery0_png[] PROGMEM = { + 0x1F, 0x8B, 0x08, 0x08, 0x33, 0x16, 0x02, 0x64, 0x04, 0x00, 0x42, 0x61, 0x74, 0x74, 0x65, 0x72, + 0x79, 0x2D, 0x30, 0x2E, 0x70, 0x6E, 0x67, 0x00, 0x01, 0xB8, 0x01, 0x47, 0xFE, 0x89, 0x50, 0x4E, + 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, + 0x28, 0x00, 0x00, 0x00, 0x1B, 0x08, 0x03, 0x00, 0x00, 0x00, 0x39, 0xCE, 0x3D, 0x5C, 0x00, 0x00, + 0x00, 0x01, 0x73, 0x52, 0x47, 0x42, 0x00, 0xAE, 0xCE, 0x1C, 0xE9, 0x00, 0x00, 0x00, 0x04, 0x67, + 0x41, 0x4D, 0x41, 0x00, 0x00, 0xB1, 0x8F, 0x0B, 0xFC, 0x61, 0x05, 0x00, 0x00, 0x00, 0x8A, 0x50, + 0x4C, 0x54, 0x45, 0xFF, 0xFF, 0xFF, 0xFD, 0xFD, 0xFD, 0xFC, 0xFC, 0xFC, 0xFE, 0xFE, 0xFE, 0xB5, + 0xB5, 0xB5, 0x6A, 0x6A, 0x6A, 0x6D, 0x6D, 0x6D, 0x6C, 0x6C, 0x6C, 0xBA, 0xBA, 0xBA, 0xD3, 0xD3, + 0xD3, 0x00, 0x00, 0x00, 0x02, 0x02, 0x02, 0xDB, 0xDB, 0xDB, 0xB6, 0xB6, 0xB6, 0x9C, 0x9C, 0x9C, + 0xF1, 0xF1, 0xF1, 0xE1, 0xE1, 0xE1, 0xE4, 0xE4, 0xE4, 0x92, 0x92, 0x92, 0xBE, 0xBE, 0xBE, 0xFA, + 0xFA, 0xFA, 0xB1, 0xB1, 0xB1, 0xA5, 0xA5, 0xA5, 0xC9, 0xC9, 0xC9, 0xB9, 0xB9, 0xB9, 0xAC, 0xAC, + 0xAC, 0xA0, 0xA0, 0xA0, 0xA6, 0xA6, 0xA6, 0xE3, 0xE3, 0xE3, 0xD6, 0xD6, 0xD6, 0xD9, 0xD9, 0xD9, + 0xAD, 0xAD, 0xAD, 0x94, 0x94, 0x94, 0xA1, 0xA1, 0xA1, 0x60, 0x60, 0x60, 0x87, 0x87, 0x87, 0x72, + 0x72, 0x72, 0xFB, 0xFB, 0xFB, 0xCD, 0xCD, 0xCD, 0xF3, 0xF3, 0xF3, 0x71, 0x71, 0x71, 0xE2, 0xE2, + 0xE2, 0x70, 0x70, 0x70, 0xC1, 0xC1, 0xC1, 0x73, 0x73, 0x73, 0xF2, 0xF2, 0xF2, 0xCA, 0xE4, 0xF2, + 0x31, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x0E, 0xC1, 0x00, 0x00, 0x0E, + 0xC1, 0x01, 0xB8, 0x91, 0x6B, 0xED, 0x00, 0x00, 0x00, 0xB7, 0x49, 0x44, 0x41, 0x54, 0x38, 0x4F, + 0x95, 0x93, 0x69, 0x0F, 0x82, 0x30, 0x0C, 0x86, 0x5F, 0x86, 0x1C, 0xD6, 0x03, 0x10, 0xBC, 0x0F, + 0x14, 0xBC, 0x8F, 0xFF, 0xFF, 0xF7, 0xEC, 0x08, 0x6A, 0x4C, 0x06, 0x2B, 0xCF, 0x87, 0xAD, 0xC9, + 0x9E, 0x2D, 0xDD, 0xD6, 0xA2, 0x0B, 0x8E, 0x6A, 0xC1, 0xA9, 0x25, 0xC6, 0xAD, 0xE7, 0x06, 0xBE, + 0xCB, 0x2E, 0x7A, 0x9E, 0x1F, 0x34, 0xE0, 0x7B, 0xE1, 0xC7, 0x74, 0xD0, 0xA7, 0x36, 0x06, 0x43, + 0x56, 0x34, 0x0A, 0x23, 0x1A, 0x47, 0x71, 0x62, 0x24, 0x8E, 0x26, 0x94, 0x22, 0xAB, 0x4C, 0x85, + 0x90, 0xA6, 0xD5, 0x16, 0x23, 0x33, 0x9A, 0xD7, 0x91, 0xC2, 0x82, 0x96, 0xBC, 0xC9, 0x48, 0x86, + 0x15, 0xAD, 0x37, 0xDB, 0x9D, 0x3E, 0x52, 0x8B, 0x39, 0x8F, 0x46, 0x14, 0x8B, 0x9A, 0x3D, 0x47, + 0x16, 0xF1, 0x40, 0x45, 0x79, 0xA4, 0x00, 0x27, 0xBB, 0x78, 0xC6, 0x85, 0xAE, 0x1C, 0x59, 0xC5, + 0x14, 0x37, 0xBA, 0x4B, 0xC4, 0x07, 0x92, 0x2E, 0xA2, 0x20, 0x47, 0xB1, 0x28, 0xCE, 0x51, 0x7C, + 0xEB, 0xA2, 0x7C, 0xF2, 0x3B, 0x5A, 0xC5, 0xFF, 0x9F, 0x11, 0xFE, 0xB5, 0xB8, 0x7A, 0xDA, 0xEA, + 0xF1, 0xF5, 0xAB, 0x47, 0x71, 0x85, 0x8B, 0x7B, 0x46, 0xDE, 0x85, 0xF2, 0xBE, 0x96, 0x00, 0xBC, + 0x01, 0x39, 0x68, 0x15, 0xFA, 0x56, 0xD4, 0xD8, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, + 0x44, 0xAE, 0x42, 0x60, 0x82, 0x77, 0x0A, 0x6F, 0x96, 0xB8, 0x01, 0x00, 0x00 +}; //Battery0.png + +//Content of Battery1.png with gzip compression +static const uint8_t battery1_png[] PROGMEM = { + 0x1F, 0x8B, 0x08, 0x08, 0xCA, 0x15, 0x02, 0x64, 0x04, 0x00, 0x42, 0x61, 0x74, 0x74, 0x65, 0x72, + 0x79, 0x31, 0x2E, 0x70, 0x6E, 0x67, 0x00, 0x01, 0x28, 0x02, 0xD7, 0xFD, 0x89, 0x50, 0x4E, 0x47, + 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x28, + 0x00, 0x00, 0x00, 0x1B, 0x08, 0x03, 0x00, 0x00, 0x00, 0x39, 0xCE, 0x3D, 0x5C, 0x00, 0x00, 0x00, + 0x01, 0x73, 0x52, 0x47, 0x42, 0x00, 0xAE, 0xCE, 0x1C, 0xE9, 0x00, 0x00, 0x00, 0x04, 0x67, 0x41, + 0x4D, 0x41, 0x00, 0x00, 0xB1, 0x8F, 0x0B, 0xFC, 0x61, 0x05, 0x00, 0x00, 0x00, 0xBA, 0x50, 0x4C, + 0x54, 0x45, 0xFF, 0xFF, 0xFF, 0xFD, 0xFD, 0xFD, 0xFC, 0xFC, 0xFC, 0xFE, 0xFE, 0xFE, 0xB5, 0xB5, + 0xB5, 0x6A, 0x6A, 0x6A, 0x6D, 0x6D, 0x6D, 0x6C, 0x6C, 0x6C, 0xBA, 0xBA, 0xBA, 0xD3, 0xD3, 0xD3, + 0x00, 0x00, 0x00, 0x02, 0x02, 0x02, 0xDB, 0xDB, 0xDB, 0xB6, 0xB6, 0xB6, 0x9D, 0x9D, 0x9D, 0xF0, + 0xF0, 0xF0, 0xE9, 0xE9, 0xE9, 0xF2, 0xF2, 0xF2, 0xF1, 0xF1, 0xF1, 0xE3, 0xE3, 0xE3, 0xE4, 0xE4, + 0xE4, 0xE1, 0xE1, 0xE1, 0x92, 0x92, 0x92, 0xBE, 0xBE, 0xBE, 0xFA, 0xFA, 0xFA, 0xB0, 0xB0, 0xB0, + 0xDC, 0xDC, 0xDC, 0xE8, 0xE8, 0xE8, 0xA5, 0xA5, 0xA5, 0xC9, 0xC9, 0xC9, 0xB9, 0xB9, 0xB9, 0xAC, + 0xAC, 0xAC, 0x2C, 0x2C, 0x2C, 0x97, 0x97, 0x97, 0xA0, 0xA0, 0xA0, 0xA6, 0xA6, 0xA6, 0xD6, 0xD6, + 0xD6, 0xD9, 0xD9, 0xD9, 0xAF, 0xAF, 0xAF, 0x24, 0x24, 0x24, 0x04, 0x04, 0x04, 0x94, 0x94, 0x94, + 0x26, 0x26, 0x26, 0x03, 0x03, 0x03, 0xA1, 0xA1, 0xA1, 0x60, 0x60, 0x60, 0x87, 0x87, 0x87, 0x72, + 0x72, 0x72, 0xFB, 0xFB, 0xFB, 0xCD, 0xCD, 0xCD, 0xF3, 0xF3, 0xF3, 0x71, 0x71, 0x71, 0xE2, 0xE2, + 0xE2, 0x70, 0x70, 0x70, 0xC1, 0xC1, 0xC1, 0x25, 0x25, 0x25, 0x73, 0x73, 0x73, 0x2D, 0x2D, 0x2D, + 0xDD, 0xDD, 0xDD, 0xB8, 0xB8, 0xB8, 0xBC, 0xBC, 0xBC, 0xBB, 0xBB, 0xBB, 0xBF, 0x7C, 0x50, 0xBC, + 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x0E, 0xC1, 0x00, 0x00, 0x0E, 0xC1, + 0x01, 0xB8, 0x91, 0x6B, 0xED, 0x00, 0x00, 0x00, 0xF7, 0x49, 0x44, 0x41, 0x54, 0x38, 0x4F, 0xAD, + 0x93, 0xE9, 0x72, 0x82, 0x30, 0x14, 0x46, 0x3F, 0x02, 0x6A, 0xBC, 0xA8, 0x05, 0xC4, 0x4A, 0x5D, + 0x6A, 0xED, 0xA2, 0xB8, 0xB6, 0xD5, 0xEE, 0xEA, 0xFB, 0xBF, 0x56, 0x93, 0x08, 0x16, 0xA6, 0x2C, + 0x71, 0xC6, 0xF3, 0x87, 0x9B, 0xE1, 0x24, 0x84, 0xE4, 0xBB, 0x38, 0x07, 0x83, 0x15, 0x60, 0x44, + 0x92, 0xC0, 0x8C, 0x9E, 0x39, 0x9C, 0x5E, 0x9B, 0xB0, 0x2A, 0xD5, 0x5A, 0x0E, 0xD5, 0x0A, 0x8F, + 0x4D, 0x03, 0x75, 0x2A, 0xC2, 0x6E, 0x08, 0x45, 0xC2, 0xD0, 0xA4, 0xD6, 0x95, 0xE3, 0x7A, 0x9E, + 0xE7, 0x3A, 0x6D, 0x3F, 0x4D, 0xC7, 0xBB, 0xA6, 0x2E, 0x02, 0x65, 0x32, 0x70, 0xBA, 0x41, 0xCF, + 0xE2, 0x9C, 0x5B, 0x7D, 0x35, 0x35, 0xC5, 0x80, 0x86, 0x51, 0xC5, 0x70, 0x4B, 0x23, 0xDC, 0xA9, + 0xCF, 0x8C, 0xC5, 0xE4, 0x14, 0x01, 0xEE, 0xE9, 0xA1, 0xFD, 0xF8, 0x24, 0x97, 0x94, 0xE2, 0x04, + 0x53, 0x0A, 0xED, 0x90, 0x5A, 0x62, 0x94, 0x82, 0x09, 0x51, 0x32, 0x13, 0xD5, 0x51, 0x9C, 0x93, + 0x4D, 0x8B, 0x2C, 0x71, 0x49, 0xAB, 0xF5, 0x33, 0xD5, 0xF0, 0x92, 0x10, 0xED, 0x6C, 0xF1, 0x15, + 0x1B, 0xDA, 0x8A, 0xAA, 0x54, 0xEC, 0xE2, 0x8D, 0xDE, 0x75, 0xC4, 0x0F, 0xF8, 0x09, 0xF1, 0xB3, + 0x4C, 0x8C, 0xF7, 0xA8, 0x2D, 0x5E, 0x62, 0x8F, 0xFF, 0xFF, 0x3A, 0xF7, 0x1C, 0xBF, 0xC4, 0x39, + 0xC6, 0xE2, 0x94, 0x16, 0x3A, 0x37, 0x33, 0xC2, 0xB7, 0x1A, 0x97, 0xDC, 0xB5, 0x4C, 0xCF, 0xCF, + 0x6E, 0x7F, 0xD8, 0xEF, 0x9C, 0xE3, 0x3A, 0x49, 0x92, 0xE9, 0x91, 0x79, 0xEC, 0xE7, 0xE4, 0xD1, + 0xFD, 0xCB, 0xA3, 0x76, 0xC2, 0xB5, 0x7B, 0x46, 0xBF, 0x0B, 0xF5, 0xFB, 0x5A, 0x07, 0xE0, 0x17, + 0xAB, 0x60, 0x21, 0x45, 0x7A, 0x29, 0x95, 0xBA, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, + 0xAE, 0x42, 0x60, 0x82, 0x51, 0xB3, 0x1E, 0xF4, 0x28, 0x02, 0x00, 0x00 +}; //Battery1.png + +//Content of Battery2.png with gzip compression +static const uint8_t battery2_png[] PROGMEM = { + 0x1F, 0x8B, 0x08, 0x08, 0x5D, 0x10, 0x02, 0x64, 0x04, 0x00, 0x42, 0x61, 0x74, 0x74, 0x65, 0x72, + 0x79, 0x32, 0x2E, 0x70, 0x6E, 0x67, 0x00, 0x01, 0xB9, 0x02, 0x46, 0xFD, 0x89, 0x50, 0x4E, 0x47, + 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x28, + 0x00, 0x00, 0x00, 0x1A, 0x08, 0x03, 0x00, 0x00, 0x00, 0xF2, 0x92, 0xEE, 0xF9, 0x00, 0x00, 0x00, + 0x01, 0x73, 0x52, 0x47, 0x42, 0x00, 0xAE, 0xCE, 0x1C, 0xE9, 0x00, 0x00, 0x00, 0x04, 0x67, 0x41, + 0x4D, 0x41, 0x00, 0x00, 0xB1, 0x8F, 0x0B, 0xFC, 0x61, 0x05, 0x00, 0x00, 0x01, 0x17, 0x50, 0x4C, + 0x54, 0x45, 0xFF, 0xFF, 0xFF, 0xFC, 0xFC, 0xFC, 0xFD, 0xFD, 0xFD, 0xFE, 0xFE, 0xFE, 0x96, 0x96, + 0x96, 0x45, 0x45, 0x45, 0x47, 0x47, 0x47, 0x9C, 0x9C, 0x9C, 0xBC, 0xBC, 0xBC, 0x0A, 0x0A, 0x0A, + 0x16, 0x16, 0x16, 0x2E, 0x2E, 0x2E, 0x25, 0x25, 0x25, 0x26, 0x26, 0x26, 0x28, 0x28, 0x28, 0x29, + 0x29, 0x29, 0x13, 0x13, 0x13, 0x0E, 0x0E, 0x0E, 0xC5, 0xC5, 0xC5, 0xFB, 0xFB, 0xFB, 0xA7, 0xA7, + 0xA7, 0x08, 0x08, 0x08, 0xA6, 0xA6, 0xA6, 0xFA, 0xFA, 0xFA, 0xF3, 0xF3, 0xF3, 0xF7, 0xF7, 0xF7, + 0xF4, 0xF4, 0xF4, 0xF5, 0xF5, 0xF5, 0xF2, 0xF2, 0xF2, 0x9B, 0x9B, 0x9B, 0xAF, 0xAF, 0xAF, 0xF8, + 0xF8, 0xF8, 0xAB, 0xAB, 0xAB, 0x6E, 0x6E, 0x6E, 0x77, 0x77, 0x77, 0x75, 0x75, 0x75, 0xC6, 0xC6, + 0xC6, 0xCB, 0xCB, 0xCB, 0x71, 0x71, 0x71, 0x76, 0x76, 0x76, 0x73, 0x73, 0x73, 0x7C, 0x7C, 0x7C, + 0xE6, 0xE6, 0xE6, 0x0D, 0x0D, 0x0D, 0xBD, 0xBD, 0xBD, 0xAA, 0xAA, 0xAA, 0x06, 0x06, 0x06, 0xF0, + 0xF0, 0xF0, 0x31, 0x31, 0x31, 0x00, 0x00, 0x00, 0x8E, 0x8E, 0x8E, 0x95, 0x95, 0x95, 0xC9, 0xC9, + 0xC9, 0x9A, 0x9A, 0x9A, 0x05, 0x05, 0x05, 0x6D, 0x6D, 0x6D, 0x9D, 0x9D, 0x9D, 0x90, 0x90, 0x90, + 0xE3, 0xE3, 0xE3, 0xA9, 0xA9, 0xA9, 0xEF, 0xEF, 0xEF, 0x34, 0x34, 0x34, 0x03, 0x03, 0x03, 0x01, + 0x01, 0x01, 0x9E, 0x9E, 0x9E, 0x04, 0x04, 0x04, 0x18, 0x18, 0x18, 0xCE, 0xCE, 0xCE, 0x0F, 0x0F, + 0x0F, 0x0B, 0x0B, 0x0B, 0x78, 0x78, 0x78, 0x35, 0x35, 0x35, 0x02, 0x02, 0x02, 0x09, 0x09, 0x09, + 0xD0, 0xD0, 0xD0, 0xB4, 0xB4, 0xB4, 0x6F, 0x6F, 0x6F, 0x0C, 0x0C, 0x0C, 0xBB, 0xBB, 0xBB, 0xED, + 0xED, 0xED, 0x70, 0x70, 0x70, 0xB1, 0xB1, 0xB1, 0xDE, 0xDE, 0xDE, 0xB3, 0xB3, 0xB3, 0xE2, 0xE2, + 0xE2, 0x32, 0x32, 0x32, 0xA8, 0xA8, 0xA8, 0x79, 0x79, 0x79, 0x72, 0x72, 0x72, 0x74, 0x74, 0x74, + 0x7E, 0x7E, 0x7E, 0x97, 0x97, 0x97, 0x46, 0x46, 0x46, 0xC4, 0xCE, 0xCA, 0x12, 0x00, 0x00, 0x00, + 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x0E, 0xC1, 0x00, 0x00, 0x0E, 0xC1, 0x01, 0xB8, 0x91, + 0x6B, 0xED, 0x00, 0x00, 0x01, 0x2B, 0x49, 0x44, 0x41, 0x54, 0x38, 0x4F, 0xAD, 0x93, 0xE9, 0x52, + 0xC2, 0x30, 0x14, 0x46, 0xBF, 0x14, 0xB0, 0x16, 0xB4, 0x2E, 0x2D, 0xE0, 0x86, 0x4A, 0xEB, 0x8E, + 0x82, 0x01, 0xA9, 0x2B, 0x2D, 0xA8, 0x54, 0xDC, 0xEB, 0x8A, 0xDB, 0xFB, 0x3F, 0x87, 0x49, 0x68, + 0x1D, 0x45, 0xED, 0xB4, 0x33, 0x9C, 0x3F, 0xF7, 0x76, 0xE6, 0x4C, 0x92, 0xE6, 0x7E, 0x41, 0x1C, + 0x88, 0x44, 0x42, 0xF0, 0xA5, 0x18, 0x24, 0x90, 0x4C, 0x0D, 0xFD, 0x4B, 0x4A, 0x66, 0x82, 0x80, + 0x60, 0x58, 0x49, 0x67, 0x46, 0x46, 0x39, 0xEA, 0x98, 0x2A, 0x2A, 0x6B, 0x7A, 0xA8, 0x99, 0xF1, + 0x89, 0x49, 0x48, 0x42, 0xD4, 0xA0, 0x67, 0x73, 0xC8, 0x8B, 0x3E, 0x3F, 0xD5, 0xAB, 0x98, 0x9E, + 0x11, 0xCC, 0xCE, 0xA1, 0xA0, 0xCC, 0x63, 0x81, 0xF0, 0x45, 0x09, 0x16, 0xB3, 0x3A, 0x72, 0x45, + 0xC3, 0x34, 0x8A, 0x4B, 0x58, 0x5E, 0x31, 0xCD, 0xD5, 0xB5, 0xF5, 0x0D, 0x7F, 0x3F, 0x09, 0x72, + 0x69, 0x93, 0x37, 0xEC, 0x93, 0x60, 0xAB, 0xAC, 0x57, 0xB6, 0x29, 0xA7, 0x8A, 0x9A, 0xA8, 0xA5, + 0x1D, 0x68, 0x09, 0x8E, 0x86, 0xBA, 0xB5, 0xBB, 0xB7, 0x5F, 0x3F, 0x60, 0x26, 0x17, 0x0F, 0x8F, + 0x1A, 0xB4, 0x6C, 0x5B, 0x4E, 0x12, 0x4D, 0xAB, 0x65, 0x5B, 0xF4, 0xF8, 0xC4, 0x3F, 0x17, 0x41, + 0x81, 0x2A, 0xA7, 0x6D, 0xEA, 0xB2, 0x13, 0x0A, 0xB1, 0x72, 0x46, 0x6D, 0xDA, 0xA1, 0x35, 0xC8, + 0x2D, 0xC7, 0xE9, 0xD0, 0xF4, 0x37, 0xF1, 0xBC, 0x7A, 0x71, 0x49, 0xAF, 0xBE, 0xC4, 0x46, 0x20, + 0xDA, 0xFD, 0xE2, 0xF5, 0x0D, 0x6E, 0xA9, 0x17, 0x41, 0x6C, 0xDF, 0xE1, 0x3E, 0x9A, 0xF8, 0x80, + 0xC7, 0x01, 0x8B, 0x51, 0xB7, 0xEE, 0xFB, 0x99, 0xA8, 0xD7, 0x13, 0xF5, 0xC2, 0xF5, 0xCA, 0x93, + 0x18, 0xDD, 0xAF, 0x11, 0xE6, 0x7F, 0x8C, 0x90, 0x87, 0xE2, 0xD9, 0xEB, 0x1A, 0x5D, 0x8F, 0x85, + 0xE2, 0xC5, 0x70, 0xDD, 0xD7, 0xB7, 0xBF, 0x42, 0x11, 0x23, 0x66, 0x3C, 0xB8, 0x41, 0x5E, 0x43, + 0x82, 0xCB, 0x56, 0x7D, 0x0F, 0x79, 0x0A, 0x1F, 0x72, 0xE0, 0xC5, 0x61, 0x90, 0xCF, 0x15, 0xF8, + 0x04, 0xA3, 0x28, 0x52, 0xB8, 0x37, 0x7F, 0x84, 0x5F, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, + 0x44, 0xAE, 0x42, 0x60, 0x82, 0x0E, 0x75, 0xED, 0xEE, 0xB9, 0x02, 0x00, 0x00 +}; //Battery2.png + +//Content of Battery3.png with gzip compression +static const uint8_t battery3_png[] PROGMEM = { + 0x1F, 0x8B, 0x08, 0x08, 0xC2, 0x15, 0x02, 0x64, 0x04, 0x00, 0x42, 0x61, 0x74, 0x74, 0x65, 0x72, + 0x79, 0x33, 0x2E, 0x70, 0x6E, 0x67, 0x00, 0x01, 0xCC, 0x02, 0x33, 0xFD, 0x89, 0x50, 0x4E, 0x47, + 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x28, + 0x00, 0x00, 0x00, 0x1B, 0x08, 0x03, 0x00, 0x00, 0x00, 0x39, 0xCE, 0x3D, 0x5C, 0x00, 0x00, 0x00, + 0x01, 0x73, 0x52, 0x47, 0x42, 0x00, 0xAE, 0xCE, 0x1C, 0xE9, 0x00, 0x00, 0x00, 0x04, 0x67, 0x41, + 0x4D, 0x41, 0x00, 0x00, 0xB1, 0x8F, 0x0B, 0xFC, 0x61, 0x05, 0x00, 0x00, 0x01, 0x0B, 0x50, 0x4C, + 0x54, 0x45, 0xFF, 0xFF, 0xFF, 0xFD, 0xFD, 0xFD, 0xFC, 0xFC, 0xFC, 0xFE, 0xFE, 0xFE, 0xB5, 0xB5, + 0xB5, 0x6A, 0x6A, 0x6A, 0x6D, 0x6D, 0x6D, 0x6C, 0x6C, 0x6C, 0xBA, 0xBA, 0xBA, 0xD3, 0xD3, 0xD3, + 0x00, 0x00, 0x00, 0x02, 0x02, 0x02, 0xDB, 0xDB, 0xDB, 0xB6, 0xB6, 0xB6, 0x9D, 0x9D, 0x9D, 0xF0, + 0xF0, 0xF0, 0xE9, 0xE9, 0xE9, 0xF2, 0xF2, 0xF2, 0xF1, 0xF1, 0xF1, 0xE2, 0xE2, 0xE2, 0xE8, 0xE8, + 0xE8, 0xE6, 0xE6, 0xE6, 0xE3, 0xE3, 0xE3, 0xEC, 0xEC, 0xEC, 0x92, 0x92, 0x92, 0xBE, 0xBE, 0xBE, + 0xFA, 0xFA, 0xFA, 0xB0, 0xB0, 0xB0, 0xDC, 0xDC, 0xDC, 0xE7, 0xE7, 0xE7, 0xEB, 0xEB, 0xEB, 0xB9, + 0xB9, 0xB9, 0xBC, 0xBC, 0xBC, 0xF9, 0xF9, 0xF9, 0xD6, 0xD6, 0xD6, 0xE1, 0xE1, 0xE1, 0xA4, 0xA4, + 0xA4, 0xC9, 0xC9, 0xC9, 0xAC, 0xAC, 0xAC, 0x2C, 0x2C, 0x2C, 0x94, 0x94, 0x94, 0xA3, 0xA3, 0xA3, + 0xD4, 0xD4, 0xD4, 0x59, 0x59, 0x59, 0x3B, 0x3B, 0x3B, 0x9F, 0x9F, 0x9F, 0xA6, 0xA6, 0xA6, 0xD9, + 0xD9, 0xD9, 0xAF, 0xAF, 0xAF, 0x24, 0x24, 0x24, 0x04, 0x04, 0x04, 0x9A, 0x9A, 0x9A, 0xA8, 0xA8, + 0xA8, 0x03, 0x03, 0x03, 0x09, 0x09, 0x09, 0xD7, 0xD7, 0xD7, 0x62, 0x62, 0x62, 0x05, 0x05, 0x05, + 0x32, 0x32, 0x32, 0xA0, 0xA0, 0xA0, 0x26, 0x26, 0x26, 0xA7, 0xA7, 0xA7, 0x01, 0x01, 0x01, 0x08, + 0x08, 0x08, 0x61, 0x61, 0x61, 0x34, 0x34, 0x34, 0xA1, 0xA1, 0xA1, 0x60, 0x60, 0x60, 0x87, 0x87, + 0x87, 0x72, 0x72, 0x72, 0xFB, 0xFB, 0xFB, 0x33, 0x33, 0x33, 0xCD, 0xCD, 0xCD, 0xF3, 0xF3, 0xF3, + 0x71, 0x71, 0x71, 0x70, 0x70, 0x70, 0xC1, 0xC1, 0xC1, 0xE4, 0xE4, 0xE4, 0x25, 0x25, 0x25, 0x73, + 0x73, 0x73, 0x31, 0x31, 0x31, 0x2D, 0x2D, 0x2D, 0x5A, 0x5A, 0x5A, 0x3C, 0x3C, 0x3C, 0xDD, 0xDD, + 0xDD, 0xB8, 0xB8, 0xB8, 0xBB, 0xBB, 0xBB, 0xD8, 0xD8, 0xD8, 0xB7, 0xB7, 0xB7, 0x73, 0x2D, 0x27, + 0xCD, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x0E, 0xC2, 0x00, 0x00, 0x0E, + 0xC2, 0x01, 0x15, 0x28, 0x4A, 0x80, 0x00, 0x00, 0x01, 0x4A, 0x49, 0x44, 0x41, 0x54, 0x38, 0x4F, + 0xAD, 0x93, 0xE9, 0x56, 0xC2, 0x30, 0x14, 0x84, 0x07, 0x28, 0x10, 0x6F, 0x45, 0x20, 0x54, 0x10, + 0x14, 0xA9, 0xA8, 0x88, 0xBB, 0x20, 0x15, 0x77, 0xDC, 0xF7, 0x5D, 0x8B, 0xBE, 0xFF, 0x93, 0x98, + 0x84, 0x52, 0x28, 0x1E, 0x30, 0x9E, 0xE3, 0xFC, 0xE8, 0xA4, 0xE9, 0xD7, 0x34, 0xCD, 0x9D, 0x8B, + 0xBF, 0x28, 0x14, 0x1E, 0xA1, 0x90, 0x07, 0x09, 0x45, 0x3C, 0x1F, 0x22, 0xFF, 0x71, 0x04, 0x46, + 0x34, 0x16, 0x1F, 0xA2, 0x58, 0x94, 0x75, 0xC9, 0x10, 0xC6, 0x68, 0x94, 0xCC, 0x71, 0x81, 0x48, + 0x85, 0x91, 0xA0, 0x89, 0x64, 0x2A, 0xCD, 0x39, 0x4F, 0xA7, 0x32, 0x96, 0x74, 0xCE, 0x27, 0xB3, + 0x39, 0x35, 0x61, 0xF1, 0x29, 0xCA, 0xA3, 0xA0, 0xC8, 0x30, 0x18, 0x4D, 0x63, 0xC6, 0x60, 0x8C, + 0x19, 0x45, 0xCC, 0x1A, 0xAC, 0xC4, 0x4A, 0xF6, 0x1C, 0xCA, 0x72, 0x22, 0x31, 0x8F, 0x05, 0x5A, + 0x54, 0xEB, 0x49, 0xB0, 0x44, 0x15, 0x2C, 0xA9, 0xCF, 0x54, 0xB1, 0xAC, 0x9C, 0x56, 0xB0, 0xAA, + 0x7C, 0x0D, 0xEB, 0xB4, 0x91, 0x2D, 0x6F, 0xCA, 0x25, 0x25, 0xB8, 0x85, 0x1A, 0xD5, 0xCD, 0x3A, + 0x6D, 0xA3, 0x21, 0xDC, 0xA1, 0x9D, 0x26, 0x76, 0x69, 0x4F, 0x4C, 0xEC, 0xE3, 0x40, 0xBD, 0x50, + 0x15, 0x58, 0x07, 0x3C, 0x24, 0x93, 0x1C, 0x01, 0x1E, 0x89, 0xEB, 0x31, 0xB5, 0x9A, 0x38, 0x11, + 0x03, 0x87, 0x4E, 0x71, 0x46, 0xE7, 0x17, 0x97, 0x14, 0xC7, 0x55, 0x1F, 0x68, 0x0E, 0x82, 0x26, + 0x5D, 0x0B, 0xF0, 0x06, 0xB7, 0x74, 0x17, 0x58, 0x71, 0x08, 0x98, 0x47, 0x86, 0xEE, 0x75, 0xC0, + 0x07, 0x3C, 0xF6, 0x81, 0x4F, 0xBF, 0x81, 0xDD, 0x3D, 0x6A, 0x83, 0xFF, 0xB1, 0xC7, 0x9F, 0x7F, + 0x2D, 0xCF, 0xB1, 0xD1, 0x03, 0xEB, 0x62, 0xD8, 0x39, 0xC7, 0x67, 0x71, 0x8E, 0x5D, 0xB0, 0x46, + 0x8E, 0x5F, 0x19, 0xB3, 0x57, 0x99, 0x97, 0x81, 0xCA, 0x54, 0xF0, 0xEA, 0xDD, 0xFB, 0xB5, 0x7E, + 0x53, 0xFE, 0x1E, 0xA8, 0xB5, 0x4C, 0xCF, 0x87, 0x6B, 0xB7, 0x6D, 0xD7, 0x42, 0x4E, 0x7A, 0x3B, + 0x5F, 0xC0, 0xE7, 0x97, 0x6D, 0xDB, 0x6E, 0x36, 0x90, 0x1E, 0x99, 0x47, 0x95, 0x43, 0x3F, 0x8F, + 0x49, 0x2F, 0x8F, 0xBC, 0xD8, 0x97, 0x47, 0xED, 0x84, 0x6B, 0xF7, 0x8C, 0x7E, 0x17, 0xEA, 0xF7, + 0xB5, 0x8E, 0x80, 0x6F, 0xE5, 0x3C, 0x46, 0x4D, 0x8A, 0xE9, 0x72, 0x76, 0x00, 0x00, 0x00, 0x00, + 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82, 0x09, 0xE7, 0x9D, 0xBA, 0xCC, 0x02, 0x00, 0x00 +}; //Battery3.png + +//Content of BatteryBlank.png with gzip compression +static const uint8_t batteryBlank_png[] PROGMEM = { + 0x1F, 0x8B, 0x08, 0x08, 0xA9, 0x26, 0x02, 0x64, 0x04, 0x00, 0x42, 0x61, 0x74, 0x74, 0x65, 0x72, + 0x79, 0x42, 0x6C, 0x61, 0x6E, 0x6B, 0x2E, 0x70, 0x6E, 0x67, 0x00, 0x01, 0xAF, 0x00, 0x50, 0xFF, + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, + 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x1B, 0x04, 0x03, 0x00, 0x00, 0x00, 0xFC, 0x3E, 0xD0, + 0x5D, 0x00, 0x00, 0x00, 0x01, 0x73, 0x52, 0x47, 0x42, 0x00, 0xAE, 0xCE, 0x1C, 0xE9, 0x00, 0x00, + 0x00, 0x04, 0x67, 0x41, 0x4D, 0x41, 0x00, 0x00, 0xB1, 0x8F, 0x0B, 0xFC, 0x61, 0x05, 0x00, 0x00, + 0x00, 0x0C, 0x50, 0x4C, 0x54, 0x45, 0xFF, 0xFF, 0xFF, 0xFD, 0xFD, 0xFD, 0xFC, 0xFC, 0xFC, 0xFB, + 0xFB, 0xFB, 0xB9, 0x60, 0x3E, 0xF0, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, + 0x0E, 0xC0, 0x00, 0x00, 0x0E, 0xC0, 0x01, 0x6A, 0xD6, 0x89, 0x09, 0x00, 0x00, 0x00, 0x2C, 0x49, + 0x44, 0x41, 0x54, 0x28, 0xCF, 0x63, 0xA0, 0x05, 0x10, 0xC4, 0x04, 0x02, 0x0C, 0x4A, 0x98, 0x40, + 0x81, 0x32, 0x41, 0x25, 0x6C, 0x82, 0xCA, 0x44, 0xAB, 0x54, 0xA2, 0xCC, 0x4C, 0x5A, 0x58, 0xA4, + 0x44, 0x69, 0x28, 0x61, 0x0D, 0x79, 0xAA, 0x03, 0x06, 0x06, 0x00, 0xEC, 0x94, 0x2F, 0xC6, 0xFE, + 0xC5, 0x42, 0x72, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82, 0x2E, + 0xF7, 0x8A, 0xC2, 0xAF, 0x00, 0x00, 0x00 +}; //BatteryBlank.png + +//Content of Battery0_Charging.png with gzip compression +static const uint8_t battery0_Charging_png[] PROGMEM = { + 0x1F, 0x8B, 0x08, 0x08, 0x4F, 0x16, 0x02, 0x64, 0x04, 0x00, 0x42, 0x61, 0x74, 0x74, 0x65, 0x72, + 0x79, 0x30, 0x5F, 0x43, 0x68, 0x61, 0x72, 0x67, 0x69, 0x6E, 0x67, 0x2E, 0x70, 0x6E, 0x67, 0x00, + 0x01, 0xC4, 0x01, 0x3B, 0xFE, 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, + 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x1A, 0x08, 0x03, 0x00, + 0x00, 0x00, 0xF2, 0x92, 0xEE, 0xF9, 0x00, 0x00, 0x00, 0x01, 0x73, 0x52, 0x47, 0x42, 0x00, 0xAE, + 0xCE, 0x1C, 0xE9, 0x00, 0x00, 0x00, 0x04, 0x67, 0x41, 0x4D, 0x41, 0x00, 0x00, 0xB1, 0x8F, 0x0B, + 0xFC, 0x61, 0x05, 0x00, 0x00, 0x00, 0x87, 0x50, 0x4C, 0x54, 0x45, 0xFF, 0xFF, 0xFF, 0xFC, 0xFC, + 0xFC, 0xFD, 0xFD, 0xFD, 0xEB, 0xF4, 0xEC, 0xBC, 0xDD, 0xBF, 0xFE, 0xFE, 0xFE, 0x21, 0x84, 0x2C, + 0x00, 0x7F, 0x0E, 0x27, 0x86, 0x31, 0x5F, 0x9E, 0x66, 0x6A, 0xA4, 0x71, 0xFB, 0xFB, 0xFB, 0x49, + 0x90, 0x51, 0x30, 0x8A, 0x3A, 0xB4, 0xD7, 0xB8, 0xA9, 0xCF, 0xAD, 0xB0, 0xD4, 0xB4, 0xAA, 0xD0, + 0xAE, 0xAB, 0xD1, 0xB0, 0xA7, 0xCE, 0xAC, 0x25, 0x85, 0x30, 0x51, 0x95, 0x59, 0xF8, 0xF8, 0xF8, + 0x4D, 0x92, 0x54, 0x3E, 0x8A, 0x46, 0x49, 0x97, 0x51, 0x4B, 0x92, 0x53, 0x3C, 0x89, 0x44, 0x28, + 0x86, 0x32, 0x33, 0x85, 0x3C, 0xCA, 0xD8, 0xCB, 0x4A, 0x91, 0x52, 0x3D, 0x8A, 0x45, 0x0B, 0x7E, + 0x17, 0x1B, 0x81, 0x26, 0x77, 0xAD, 0x7D, 0x3F, 0x91, 0x48, 0x5E, 0x9D, 0x65, 0xA0, 0xC9, 0xA5, + 0x53, 0x96, 0x5A, 0x8A, 0xBA, 0x8F, 0x55, 0x97, 0x5C, 0x91, 0xBE, 0x96, 0xFA, 0xFA, 0xFA, 0x22, + 0x84, 0x2D, 0x45, 0x7D, 0x7F, 0x57, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, + 0x0E, 0xC0, 0x00, 0x00, 0x0E, 0xC0, 0x01, 0x6A, 0xD6, 0x89, 0x09, 0x00, 0x00, 0x00, 0xC6, 0x49, + 0x44, 0x41, 0x54, 0x38, 0x4F, 0xAD, 0x93, 0xD9, 0x12, 0x82, 0x30, 0x0C, 0x45, 0x6F, 0x37, 0x83, + 0x16, 0xF7, 0x5D, 0xDC, 0x77, 0xC5, 0xFF, 0xFF, 0x3E, 0x5B, 0xE8, 0x03, 0x8C, 0x88, 0x61, 0xC6, + 0xF3, 0xD2, 0x99, 0x70, 0x9A, 0x96, 0x34, 0x41, 0x13, 0x84, 0x14, 0x35, 0x04, 0xC9, 0xA3, 0x74, + 0x0D, 0x2A, 0x48, 0x80, 0x51, 0x2D, 0xAA, 0x21, 0x52, 0x26, 0xF7, 0x04, 0xDA, 0x21, 0xF4, 0x85, + 0x0E, 0x64, 0x26, 0x5A, 0xC4, 0xD4, 0xD5, 0xBD, 0xEC, 0x98, 0x5E, 0x3F, 0x5F, 0xF5, 0x60, 0x98, + 0x31, 0x1A, 0xEB, 0x09, 0x4D, 0x31, 0x13, 0x3E, 0xA9, 0xC0, 0x9C, 0x62, 0x63, 0x42, 0xFE, 0x22, + 0x46, 0x4A, 0x89, 0x05, 0x2D, 0xFD, 0x2D, 0xDD, 0x67, 0x81, 0x15, 0xC5, 0xD6, 0x5A, 0xE9, 0xE4, + 0x32, 0xD2, 0x45, 0x91, 0x10, 0xAD, 0x37, 0xC9, 0xD6, 0x99, 0x5E, 0xDC, 0xB9, 0xBD, 0x9F, 0x29, + 0x8D, 0xAF, 0xCD, 0x3E, 0xBB, 0xE7, 0x41, 0xD9, 0xDF, 0xE2, 0xF1, 0x74, 0x26, 0xD2, 0x0C, 0xF1, + 0x82, 0x2B, 0x4F, 0xBC, 0xE1, 0xCE, 0x13, 0x1F, 0x78, 0xFE, 0x59, 0x64, 0x1F, 0xCD, 0xFE, 0x19, + 0x76, 0x79, 0x8A, 0x05, 0xAF, 0x7C, 0x42, 0x29, 0xD2, 0xB4, 0xFC, 0x84, 0xCC, 0xA6, 0x68, 0xD0, + 0x66, 0xCC, 0xC6, 0x75, 0xA3, 0xF0, 0x0A, 0xA1, 0x4A, 0x22, 0x15, 0x3C, 0x07, 0x73, 0xB8, 0x1A, + 0x8C, 0x2B, 0x03, 0xE0, 0x0D, 0x08, 0xEF, 0x14, 0xF0, 0xC6, 0x56, 0x07, 0x1F, 0x00, 0x00, 0x00, + 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82, 0x66, 0x5C, 0xEC, 0xE1, 0xC4, 0x01, 0x00, + 0x00 +}; //Battery0_Charging.png + +//Content of Battery1_Charging.png with gzip compression +static const uint8_t battery1_Charging_png[] PROGMEM = { + 0x1F, 0x8B, 0x08, 0x08, 0x47, 0x16, 0x02, 0x64, 0x04, 0x00, 0x42, 0x61, 0x74, 0x74, 0x65, 0x72, + 0x79, 0x31, 0x5F, 0x43, 0x68, 0x61, 0x72, 0x67, 0x69, 0x6E, 0x67, 0x2E, 0x70, 0x6E, 0x67, 0x00, + 0x01, 0x0E, 0x02, 0xF1, 0xFD, 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, + 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x1A, 0x08, 0x03, 0x00, + 0x00, 0x00, 0xF2, 0x92, 0xEE, 0xF9, 0x00, 0x00, 0x00, 0x01, 0x73, 0x52, 0x47, 0x42, 0x00, 0xAE, + 0xCE, 0x1C, 0xE9, 0x00, 0x00, 0x00, 0x04, 0x67, 0x41, 0x4D, 0x41, 0x00, 0x00, 0xB1, 0x8F, 0x0B, + 0xFC, 0x61, 0x05, 0x00, 0x00, 0x00, 0xB1, 0x50, 0x4C, 0x54, 0x45, 0xFF, 0xFF, 0xFF, 0xFC, 0xFC, + 0xFC, 0xFD, 0xFD, 0xFD, 0xEB, 0xF4, 0xEC, 0xBC, 0xDD, 0xBF, 0xFE, 0xFE, 0xFE, 0x21, 0x84, 0x2C, + 0x00, 0x7F, 0x0E, 0x27, 0x86, 0x31, 0x5F, 0x9E, 0x66, 0x6A, 0xA4, 0x71, 0xFB, 0xFB, 0xFB, 0x49, + 0x90, 0x51, 0x30, 0x8A, 0x3A, 0xB4, 0xD7, 0xB8, 0xA9, 0xCF, 0xAD, 0xB0, 0xD4, 0xB4, 0xAA, 0xD0, + 0xAE, 0xAB, 0xD1, 0xB0, 0xA7, 0xCE, 0xAC, 0x25, 0x85, 0x30, 0x51, 0x95, 0x59, 0xF8, 0xF8, 0xF8, + 0x4D, 0x92, 0x54, 0xF5, 0xF9, 0xF5, 0x5D, 0x95, 0x63, 0x28, 0x78, 0x31, 0x30, 0x7B, 0x38, 0x2E, + 0x7B, 0x36, 0xB2, 0xBF, 0xB3, 0x3E, 0x8A, 0x46, 0x49, 0x97, 0x51, 0x4B, 0x92, 0x53, 0xC3, 0xDB, + 0xC6, 0x44, 0x86, 0x4B, 0x3C, 0x89, 0x44, 0x28, 0x86, 0x32, 0x33, 0x85, 0x3C, 0xCA, 0xD8, 0xCB, + 0x4A, 0x91, 0x52, 0xC2, 0xDA, 0xC5, 0x4C, 0x8A, 0x53, 0x3D, 0x8A, 0x45, 0x0B, 0x7E, 0x17, 0x4B, + 0x8A, 0x52, 0x1B, 0x81, 0x26, 0x77, 0xAD, 0x7D, 0x3F, 0x91, 0x48, 0x5E, 0x9D, 0x65, 0xA0, 0xC9, + 0xA5, 0x53, 0x96, 0x5A, 0x8A, 0xBA, 0x8F, 0x55, 0x97, 0x5C, 0x91, 0xBE, 0x96, 0xFA, 0xFA, 0xFA, + 0x5F, 0x96, 0x65, 0x2A, 0x79, 0x33, 0x31, 0x7C, 0x3A, 0x22, 0x84, 0x2D, 0x95, 0x79, 0x7B, 0x97, + 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x0E, 0xC0, 0x00, 0x00, 0x0E, 0xC0, + 0x01, 0x6A, 0xD6, 0x89, 0x09, 0x00, 0x00, 0x00, 0xE6, 0x49, 0x44, 0x41, 0x54, 0x38, 0x4F, 0xAD, + 0x93, 0x69, 0x4F, 0xC2, 0x40, 0x10, 0x40, 0x67, 0x2F, 0x07, 0xD9, 0x52, 0x45, 0x45, 0xA4, 0xC5, + 0xB3, 0x78, 0x80, 0x22, 0xA0, 0x58, 0x5B, 0xFE, 0xFF, 0x0F, 0x63, 0xAF, 0x04, 0x49, 0xDA, 0xCD, + 0x34, 0xF1, 0x7D, 0xD9, 0x64, 0xF6, 0x65, 0xA6, 0x9D, 0x9D, 0x81, 0x2E, 0x30, 0xCE, 0x22, 0x04, + 0xC9, 0x22, 0x64, 0x04, 0x11, 0x24, 0x00, 0x25, 0x4E, 0x30, 0x42, 0x4F, 0x28, 0xEF, 0x31, 0x38, + 0x0D, 0xA1, 0x16, 0xFA, 0xC0, 0x9D, 0xA8, 0x21, 0xC1, 0x81, 0x4C, 0x5D, 0x99, 0xF4, 0xCC, 0x9F, + 0xF2, 0x7C, 0xE8, 0xB8, 0xB8, 0x94, 0x57, 0x38, 0x82, 0x6B, 0x66, 0x93, 0x32, 0x18, 0x63, 0x72, + 0x33, 0xC9, 0xF2, 0x69, 0x9E, 0xDD, 0xAA, 0x50, 0xC6, 0xA3, 0x38, 0x87, 0x3B, 0xBC, 0xB7, 0x5F, + 0x69, 0xE2, 0x0C, 0x1E, 0x30, 0x79, 0xF4, 0x55, 0x9E, 0xB4, 0xE6, 0xEA, 0x00, 0xD7, 0x1A, 0x0A, + 0xC4, 0xD9, 0x73, 0xF1, 0x62, 0x4C, 0x2B, 0xBE, 0xBE, 0x79, 0x71, 0xCE, 0xF9, 0xDF, 0x94, 0xCA, + 0xF4, 0x66, 0xE1, 0x2E, 0xDE, 0x85, 0xF6, 0x62, 0xC8, 0xF8, 0xD1, 0x24, 0x2E, 0x3F, 0x57, 0x88, + 0x92, 0x20, 0xAE, 0x61, 0x43, 0x13, 0xBF, 0xE0, 0x9B, 0x26, 0x6E, 0xE1, 0xE7, 0x9F, 0x45, 0x72, + 0x69, 0xF2, 0xCF, 0x1C, 0xB5, 0x87, 0xDA, 0xF0, 0x96, 0x27, 0x64, 0x65, 0x79, 0xF4, 0x84, 0x76, + 0x28, 0x7E, 0xAB, 0x3A, 0xAF, 0xAB, 0xE8, 0x50, 0x74, 0x18, 0x33, 0xE2, 0xE0, 0x9A, 0x55, 0xD8, + 0x85, 0x50, 0x23, 0x3D, 0x11, 0x3C, 0x03, 0x71, 0xB9, 0x3A, 0xAC, 0x2B, 0x01, 0x80, 0x3D, 0x76, + 0xDE, 0x1E, 0x86, 0xD1, 0x89, 0x12, 0x4C, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, + 0x42, 0x60, 0x82, 0xE1, 0xFD, 0x90, 0x99, 0x0E, 0x02, 0x00, 0x00 +}; //Battery1_Charging.png + +//Content of Battery2_Charging.png with gzip compression +static const uint8_t battery2_Charging_png[] PROGMEM = { + 0x1F, 0x8B, 0x08, 0x08, 0x3B, 0x13, 0x02, 0x64, 0x04, 0x00, 0x42, 0x61, 0x74, 0x74, 0x65, 0x72, + 0x79, 0x32, 0x5F, 0x43, 0x68, 0x61, 0x72, 0x67, 0x69, 0x6E, 0x67, 0x2E, 0x70, 0x6E, 0x67, 0x00, + 0x01, 0x4E, 0x02, 0xB1, 0xFD, 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, + 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x1A, 0x08, 0x03, 0x00, + 0x00, 0x00, 0xF2, 0x92, 0xEE, 0xF9, 0x00, 0x00, 0x00, 0x01, 0x73, 0x52, 0x47, 0x42, 0x00, 0xAE, + 0xCE, 0x1C, 0xE9, 0x00, 0x00, 0x00, 0x04, 0x67, 0x41, 0x4D, 0x41, 0x00, 0x00, 0xB1, 0x8F, 0x0B, + 0xFC, 0x61, 0x05, 0x00, 0x00, 0x00, 0xDB, 0x50, 0x4C, 0x54, 0x45, 0xFF, 0xFF, 0xFF, 0xFC, 0xFC, + 0xFC, 0xFD, 0xFD, 0xFD, 0xEB, 0xF4, 0xEC, 0xBC, 0xDD, 0xBF, 0xFE, 0xFE, 0xFE, 0x21, 0x84, 0x2C, + 0x00, 0x7F, 0x0E, 0x27, 0x86, 0x31, 0x5F, 0x9E, 0x66, 0x6A, 0xA4, 0x71, 0xFB, 0xFB, 0xFB, 0x49, + 0x90, 0x51, 0x30, 0x8A, 0x3A, 0xB4, 0xD7, 0xB8, 0xA9, 0xCF, 0xAD, 0xB0, 0xD4, 0xB4, 0xAA, 0xD0, + 0xAE, 0xAB, 0xD1, 0xB0, 0xA7, 0xCE, 0xAC, 0x25, 0x85, 0x30, 0x51, 0x95, 0x59, 0xF8, 0xF8, 0xF8, + 0x4D, 0x92, 0x54, 0xF5, 0xF9, 0xF5, 0x5D, 0x95, 0x63, 0x28, 0x78, 0x31, 0x30, 0x7B, 0x38, 0x2E, + 0x7B, 0x36, 0xB2, 0xBF, 0xB3, 0xB7, 0xC3, 0xB9, 0x2B, 0x79, 0x34, 0x2F, 0x7B, 0x37, 0x2D, 0x7A, + 0x35, 0x34, 0x7D, 0x3C, 0xD6, 0xDF, 0xD7, 0x3E, 0x8A, 0x46, 0x49, 0x97, 0x51, 0x4B, 0x92, 0x53, + 0xC3, 0xDB, 0xC6, 0x44, 0x86, 0x4B, 0x4B, 0x8A, 0x52, 0x89, 0xB1, 0x8E, 0x3C, 0x89, 0x44, 0x28, + 0x86, 0x32, 0x33, 0x85, 0x3C, 0xCA, 0xD8, 0xCB, 0x4A, 0x91, 0x52, 0xC2, 0xDA, 0xC5, 0x4C, 0x8A, + 0x53, 0x54, 0x8F, 0x5B, 0x8F, 0xB6, 0x94, 0x3D, 0x8A, 0x45, 0x0B, 0x7E, 0x17, 0x51, 0x8E, 0x58, + 0x1B, 0x81, 0x26, 0x77, 0xAD, 0x7D, 0x3F, 0x91, 0x48, 0x5E, 0x9D, 0x65, 0xA0, 0xC9, 0xA5, 0x53, + 0x96, 0x5A, 0x8A, 0xBA, 0x8F, 0x55, 0x97, 0x5C, 0x91, 0xBE, 0x96, 0xFA, 0xFA, 0xFA, 0x5F, 0x96, + 0x65, 0x2A, 0x79, 0x33, 0x31, 0x7C, 0x3A, 0x2C, 0x79, 0x34, 0x30, 0x7C, 0x39, 0x2D, 0x7A, 0x36, + 0x35, 0x7E, 0x3D, 0x22, 0x84, 0x2D, 0xC5, 0x39, 0x3A, 0xC0, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, + 0x59, 0x73, 0x00, 0x00, 0x0E, 0xC1, 0x00, 0x00, 0x0E, 0xC1, 0x01, 0xB8, 0x91, 0x6B, 0xED, 0x00, + 0x00, 0x00, 0xFC, 0x49, 0x44, 0x41, 0x54, 0x38, 0x4F, 0xAD, 0x93, 0x69, 0x53, 0xC2, 0x30, 0x10, + 0x86, 0xDF, 0x34, 0x29, 0x0B, 0xB6, 0x22, 0x20, 0x5E, 0xB4, 0x8A, 0xA2, 0x80, 0x88, 0xF7, 0x7D, + 0x73, 0x89, 0xFA, 0xFF, 0x7F, 0x91, 0x49, 0x37, 0x1F, 0x64, 0x06, 0x3A, 0xCB, 0x0C, 0xCF, 0x97, + 0x6E, 0xD2, 0x67, 0xB2, 0x39, 0x76, 0xB1, 0x0C, 0x2A, 0x50, 0x39, 0x78, 0xC9, 0xA1, 0x4D, 0x0E, + 0xDA, 0x4B, 0x40, 0xA8, 0x0B, 0x94, 0x43, 0x51, 0x87, 0xEC, 0x29, 0x94, 0xFC, 0xD4, 0x02, 0xD6, + 0x10, 0x64, 0x62, 0x84, 0x98, 0xD6, 0x4D, 0x39, 0x4B, 0x53, 0xDE, 0xE0, 0xAF, 0xA9, 0x54, 0x33, + 0x6A, 0x9B, 0xA6, 0x4E, 0x5B, 0xD8, 0x56, 0x6E, 0x51, 0x85, 0x1D, 0x8A, 0x77, 0xF7, 0x1A, 0x49, + 0x9A, 0x34, 0xF6, 0x71, 0xD0, 0x4C, 0xD3, 0xC3, 0xA3, 0xD6, 0x31, 0x38, 0x5F, 0x80, 0x13, 0x6A, + 0xBB, 0x5D, 0xDA, 0xA1, 0x42, 0x87, 0xE2, 0x2E, 0x67, 0x39, 0x45, 0x8F, 0x83, 0x33, 0x44, 0xA1, + 0x23, 0x42, 0x9F, 0xE8, 0xFC, 0xA2, 0x7F, 0x69, 0x4D, 0x27, 0x5E, 0x5D, 0xF3, 0xFF, 0x1B, 0xDC, + 0x72, 0x70, 0xE7, 0xF7, 0xA5, 0x70, 0x9F, 0x8D, 0x1F, 0x74, 0xC4, 0xA2, 0x5F, 0xB1, 0x87, 0x47, + 0x0E, 0xFE, 0x8B, 0x4F, 0xCF, 0x2F, 0x44, 0x46, 0x20, 0xBE, 0xE2, 0x4D, 0x26, 0xBE, 0xE3, 0x43, + 0x26, 0x7E, 0x62, 0xB0, 0x62, 0x51, 0x9C, 0x5A, 0x7C, 0x98, 0x99, 0xEB, 0x91, 0x5E, 0xF8, 0xC2, + 0x27, 0x1C, 0xCE, 0x3C, 0xA1, 0x2B, 0x8A, 0xD1, 0x78, 0x92, 0x4C, 0xC6, 0xB6, 0x28, 0xBE, 0x92, + 0xE9, 0xF4, 0xFB, 0x67, 0x5E, 0x51, 0x2C, 0x51, 0x66, 0xC2, 0xC2, 0xB5, 0xAD, 0xF0, 0xEB, 0xA7, + 0xE6, 0x52, 0xD4, 0xDE, 0xB3, 0x08, 0x9B, 0xCB, 0x66, 0x97, 0xB6, 0xAB, 0x00, 0xE0, 0x0F, 0x62, + 0x8A, 0x29, 0x7A, 0x97, 0xAB, 0x67, 0x54, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, + 0x42, 0x60, 0x82, 0xAB, 0xD7, 0x1E, 0x8B, 0x4E, 0x02, 0x00, 0x00 +}; //Battery2_Charging.png + +//Content of Battery3_Charging.png with gzip compression +static const uint8_t battery3_Charging_png[] PROGMEM = { + 0x1F, 0x8B, 0x08, 0x08, 0xBA, 0x15, 0x02, 0x64, 0x04, 0x00, 0x42, 0x61, 0x74, 0x74, 0x65, 0x72, + 0x79, 0x33, 0x5F, 0x43, 0x68, 0x61, 0x72, 0x67, 0x69, 0x6E, 0x67, 0x2E, 0x70, 0x6E, 0x67, 0x00, + 0x01, 0x7C, 0x02, 0x83, 0xFD, 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, + 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x1B, 0x08, 0x03, 0x00, + 0x00, 0x00, 0x39, 0xCE, 0x3D, 0x5C, 0x00, 0x00, 0x00, 0x01, 0x73, 0x52, 0x47, 0x42, 0x00, 0xAE, + 0xCE, 0x1C, 0xE9, 0x00, 0x00, 0x00, 0x04, 0x67, 0x41, 0x4D, 0x41, 0x00, 0x00, 0xB1, 0x8F, 0x0B, + 0xFC, 0x61, 0x05, 0x00, 0x00, 0x00, 0xED, 0x50, 0x4C, 0x54, 0x45, 0xFF, 0xFF, 0xFF, 0xFD, 0xFD, + 0xFD, 0xFC, 0xFC, 0xFC, 0xFE, 0xFE, 0xFE, 0xF5, 0xF9, 0xF5, 0x6F, 0xA0, 0x74, 0x26, 0x77, 0x2F, + 0x28, 0x78, 0x31, 0x27, 0x77, 0x30, 0x75, 0xA4, 0x7A, 0x96, 0xBA, 0x9A, 0x00, 0x7F, 0x0E, 0xA3, + 0xC3, 0xA6, 0x70, 0xA1, 0x76, 0x41, 0x8B, 0x4A, 0xC3, 0xDB, 0xC6, 0xB9, 0xD3, 0xBC, 0xC7, 0xDD, + 0xC9, 0xC5, 0xDC, 0xC8, 0xAD, 0xCB, 0xB0, 0xB6, 0xD1, 0xB9, 0xB3, 0xCF, 0xB6, 0xAF, 0xCC, 0xB2, + 0xBB, 0xD5, 0xBE, 0x35, 0x85, 0x3E, 0x7A, 0xA7, 0x7F, 0xFA, 0xFA, 0xFA, 0x68, 0x9C, 0x6E, 0xA4, + 0xC4, 0xA8, 0xD7, 0xE0, 0xD8, 0xDC, 0xE4, 0xDD, 0x74, 0xA3, 0x79, 0x78, 0xA6, 0x7D, 0xED, 0xF3, + 0xED, 0xC4, 0xCE, 0xC5, 0xAC, 0xC9, 0xAF, 0x5B, 0x93, 0x61, 0x89, 0xB1, 0x8E, 0x64, 0x99, 0x6A, + 0xDD, 0xED, 0xDE, 0x4A, 0x89, 0x51, 0x5A, 0x92, 0x60, 0x98, 0xBC, 0x9C, 0x1A, 0x73, 0x24, 0x56, + 0x90, 0x5C, 0x4B, 0x90, 0x53, 0x9B, 0xBE, 0x9F, 0x9F, 0xC1, 0xA3, 0xF2, 0xF7, 0xF2, 0x68, 0x9B, + 0x6D, 0x50, 0x8D, 0x57, 0x5F, 0x96, 0x65, 0x9C, 0xBF, 0xA0, 0x20, 0x75, 0x2A, 0x57, 0x90, 0x5D, + 0x5E, 0x95, 0x65, 0x1F, 0x75, 0x29, 0x57, 0x91, 0x5E, 0x0E, 0x7A, 0x1A, 0x3D, 0x82, 0x45, 0x1A, + 0x7B, 0x25, 0xFB, 0xFB, 0xFB, 0x8F, 0xB5, 0x93, 0xC8, 0xDE, 0xCA, 0x2B, 0x79, 0x34, 0x2A, 0x79, + 0x33, 0x7E, 0xAA, 0x83, 0xB0, 0xCD, 0xB3, 0x1B, 0x7C, 0x26, 0x1B, 0x73, 0x25, 0xA5, 0xC5, 0xA9, + 0x72, 0xA2, 0x78, 0x76, 0xA5, 0x7B, 0xD9, 0xE1, 0xDA, 0xDD, 0xE5, 0xDE, 0xEE, 0xF4, 0xEE, 0xC7, + 0xD1, 0xC8, 0x71, 0xA1, 0x76, 0xB5, 0xD0, 0xB8, 0xC5, 0x47, 0x46, 0xC2, 0x00, 0x00, 0x00, 0x09, + 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x0E, 0xC2, 0x00, 0x00, 0x0E, 0xC2, 0x01, 0x15, 0x28, 0x4A, + 0x80, 0x00, 0x00, 0x01, 0x18, 0x49, 0x44, 0x41, 0x54, 0x38, 0x4F, 0x95, 0x93, 0xE7, 0x72, 0xC2, + 0x30, 0x10, 0x84, 0xD7, 0xC6, 0x06, 0x94, 0x0B, 0x01, 0x04, 0x71, 0xE2, 0x34, 0x2B, 0xD5, 0xA4, + 0x37, 0xD2, 0x20, 0xBD, 0x99, 0xD4, 0xF7, 0x7F, 0x9C, 0x58, 0x65, 0x9C, 0x78, 0xC6, 0x26, 0x62, + 0xFF, 0xDC, 0x8E, 0xFC, 0x8D, 0x24, 0xEB, 0xF6, 0x30, 0x89, 0x1C, 0x77, 0x8C, 0x1C, 0x03, 0xA5, + 0xAA, 0x98, 0x5A, 0xA2, 0xEC, 0x73, 0xC5, 0xF3, 0xAB, 0xB5, 0x7A, 0x89, 0x6A, 0x55, 0xE6, 0x19, + 0xD2, 0xC1, 0x14, 0x8D, 0xD5, 0x74, 0x8A, 0x48, 0xB9, 0x68, 0xD0, 0x4C, 0xB3, 0xD5, 0xE6, 0x9C, + 0xB7, 0x5B, 0x9D, 0xAE, 0xAC, 0x9C, 0xCF, 0x06, 0x73, 0x6A, 0xA1, 0xCB, 0xE7, 0x29, 0xC4, 0x82, + 0x22, 0x5D, 0x30, 0x5A, 0xF4, 0x96, 0x7C, 0xC6, 0x98, 0xBF, 0x8C, 0x15, 0x9F, 0x45, 0x2C, 0x12, + 0xAB, 0x58, 0x93, 0x0B, 0x8D, 0x75, 0x6F, 0x83, 0x36, 0xD5, 0x7E, 0x12, 0x8C, 0x68, 0x2B, 0xD6, + 0xA7, 0xF4, 0xB0, 0xAD, 0xCD, 0x0E, 0x76, 0xB5, 0x89, 0xF7, 0x68, 0x3F, 0x38, 0x38, 0x3C, 0x32, + 0xE0, 0xB1, 0x01, 0x4F, 0x70, 0xAA, 0x4D, 0x1F, 0x67, 0xDA, 0xC4, 0xE7, 0xAA, 0xF4, 0x52, 0x2C, + 0x0F, 0x5E, 0x68, 0xD3, 0xC7, 0xA5, 0x36, 0xF1, 0x15, 0x0D, 0x86, 0xD7, 0x54, 0xC7, 0xCD, 0xFF, + 0xE0, 0x2D, 0xEE, 0xE8, 0xDE, 0x66, 0xC7, 0x10, 0x1D, 0x7A, 0xB0, 0x01, 0x1F, 0xF1, 0x34, 0x09, + 0x68, 0x71, 0x47, 0x6B, 0xD0, 0xFA, 0x8E, 0xC5, 0x7F, 0x9D, 0x3D, 0xF8, 0x2F, 0x38, 0x18, 0x3E, + 0xA7, 0xEF, 0x58, 0x0A, 0x16, 0x77, 0xA6, 0xA0, 0xD7, 0x2F, 0xDA, 0xE4, 0x7A, 0x2D, 0xD3, 0xF3, + 0x9A, 0x88, 0x91, 0x48, 0xDE, 0xF0, 0x2E, 0xEB, 0x28, 0xFC, 0xC0, 0xE7, 0x97, 0x10, 0x22, 0x09, + 0x72, 0xE9, 0x91, 0x79, 0x54, 0x39, 0xCC, 0xF2, 0xD8, 0x34, 0x79, 0xE4, 0xDF, 0x7F, 0xF2, 0x68, + 0x9D, 0x70, 0xEB, 0x99, 0xB1, 0x9F, 0x42, 0xFB, 0xB9, 0xB6, 0x11, 0xF0, 0x03, 0x9C, 0x73, 0x3B, + 0x83, 0xC2, 0x60, 0x2B, 0x5D, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, + 0x82, 0x44, 0xD9, 0x6C, 0x7F, 0x7C, 0x02, 0x00, 0x00 +}; //Battery3_Charging.png + +//Content of style.css with gzip compression +static const uint8_t style_css[] PROGMEM = { + 0x1F, 0x8B, 0x08, 0x08, 0x73, 0x2D, 0x02, 0x64, 0x04, 0x00, 0x73, 0x74, 0x79, 0x6C, 0x65, 0x2E, + 0x63, 0x73, 0x73, 0x00, 0x8D, 0x53, 0xCD, 0x6E, 0xDA, 0x40, 0x10, 0xBE, 0x47, 0xE2, 0x1D, 0xA6, + 0xAE, 0x2A, 0x07, 0x14, 0x87, 0x44, 0x55, 0x14, 0xE4, 0x08, 0xB5, 0xEA, 0xA1, 0x0F, 0xD0, 0x6B, + 0xDA, 0x4A, 0xCB, 0x7A, 0x8C, 0x47, 0xAC, 0x77, 0xDD, 0xDD, 0x31, 0x86, 0x56, 0x7D, 0xF7, 0x8C, + 0x8D, 0x81, 0xA0, 0x00, 0xC1, 0x27, 0xEF, 0xF7, 0x33, 0xBF, 0xBB, 0x5F, 0x73, 0x67, 0x39, 0xC9, + 0x95, 0x46, 0xF8, 0x37, 0xB8, 0x02, 0xE8, 0x8F, 0x25, 0x99, 0x75, 0x0A, 0x31, 0x69, 0x57, 0x3A, + 0x67, 0xE3, 0xA7, 0x96, 0x0A, 0x5E, 0xA7, 0x00, 0xB5, 0x37, 0xD7, 0x71, 0xAB, 0x0A, 0xE3, 0x9E, + 0xBD, 0x45, 0xC7, 0x5F, 0x26, 0xF7, 0xCD, 0xEA, 0xCF, 0xE7, 0x78, 0x78, 0xA9, 0xF2, 0x23, 0x61, + 0x4E, 0xAB, 0x78, 0x28, 0x09, 0x7D, 0xA9, 0xF8, 0x3A, 0xC6, 0x72, 0x86, 0x59, 0x86, 0x59, 0xE2, + 0x2A, 0xB4, 0xBC, 0xAE, 0x30, 0x1E, 0xDE, 0xB4, 0xB1, 0x8E, 0xC6, 0x61, 0xCE, 0xB7, 0x19, 0xF7, + 0x11, 0xD8, 0xD7, 0xF8, 0x9E, 0xB1, 0x71, 0xF9, 0x11, 0x67, 0x8B, 0x9E, 0x73, 0x85, 0xE5, 0x7C, + 0x5B, 0x76, 0x0F, 0xBD, 0x32, 0x0B, 0x29, 0x6D, 0xEF, 0x66, 0xD7, 0x20, 0xCD, 0x0B, 0x4E, 0xC1, + 0xB6, 0xB4, 0xD9, 0xE3, 0x81, 0xD7, 0x06, 0xDF, 0xC2, 0x19, 0x85, 0xCA, 0x28, 0x99, 0xF5, 0xCC, + 0x38, 0xBD, 0x10, 0xFC, 0xFF, 0xE0, 0x6A, 0x70, 0xF5, 0xAC, 0x8D, 0x0A, 0xE1, 0xF7, 0x34, 0x92, + 0x74, 0x36, 0x89, 0x7E, 0xDD, 0xC0, 0x06, 0x19, 0x4D, 0x23, 0xE8, 0xA1, 0xCD, 0xBA, 0xC6, 0x23, + 0xA8, 0x03, 0xC2, 0x07, 0x2A, 0x2B, 0xE7, 0x59, 0x59, 0x06, 0x76, 0x50, 0x79, 0x5C, 0xA2, 0xFC, + 0x52, 0x08, 0x35, 0x06, 0x68, 0x88, 0x0B, 0x98, 0x79, 0xD7, 0x04, 0xF4, 0x80, 0x2B, 0x46, 0x1B, + 0xC8, 0xD9, 0x00, 0x5C, 0x28, 0x06, 0x5D, 0x28, 0x3B, 0x47, 0xE8, 0xFA, 0x85, 0xD1, 0xF8, 0xF4, + 0x15, 0x78, 0x95, 0x63, 0xB3, 0xE3, 0x0A, 0xD5, 0x42, 0xFA, 0x91, 0x54, 0xFE, 0xDD, 0x2E, 0x4F, + 0x0D, 0x65, 0xA9, 0x3C, 0x49, 0xC0, 0x03, 0x82, 0xA5, 0xC4, 0x84, 0xBD, 0xB2, 0xA1, 0x9D, 0x70, + 0x4B, 0x59, 0xEC, 0x08, 0x43, 0x16, 0x93, 0xA2, 0x8F, 0x74, 0x2F, 0x50, 0x3F, 0x80, 0x6F, 0xC8, + 0x2C, 0x9D, 0x7D, 0x97, 0x80, 0xF0, 0x03, 0x6D, 0x86, 0x9E, 0xEC, 0x1C, 0xA6, 0xFB, 0xAF, 0xEF, + 0x4B, 0xAA, 0x98, 0x2D, 0x88, 0x93, 0x4D, 0xA1, 0xD2, 0x14, 0x17, 0x22, 0x4C, 0x41, 0x0A, 0x20, + 0x65, 0x48, 0x05, 0xCC, 0x9E, 0x3A, 0x5D, 0xE9, 0xFE, 0x26, 0x2E, 0xAC, 0xDE, 0x08, 0xE7, 0x5E, + 0xAD, 0x83, 0x56, 0x06, 0xB7, 0x6B, 0x22, 0x5B, 0xD5, 0xFC, 0xDC, 0xDE, 0xB9, 0x69, 0x94, 0x93, + 0xC1, 0xED, 0x56, 0xFA, 0xA5, 0x76, 0xB5, 0xEF, 0xC4, 0xB7, 0xDD, 0xE2, 0xB4, 0x71, 0x01, 0xD3, + 0x19, 0x4A, 0x6F, 0xFD, 0x8B, 0x13, 0x94, 0xB1, 0x9D, 0x41, 0xF4, 0x33, 0xBF, 0xBB, 0xCB, 0xA2, + 0x56, 0xBF, 0x55, 0x7B, 0x2C, 0xDD, 0xF2, 0x72, 0x39, 0x53, 0x89, 0xE1, 0x62, 0x35, 0xD9, 0xDC, + 0x25, 0x9A, 0xBC, 0x36, 0x67, 0x32, 0x3C, 0xA8, 0x03, 0x4F, 0x6E, 0x5C, 0x55, 0xAD, 0x13, 0x77, + 0xDA, 0xA0, 0x1F, 0x0F, 0x0C, 0x41, 0x2D, 0xF1, 0x62, 0xB1, 0x56, 0x1E, 0xE5, 0x49, 0xB8, 0xC6, + 0x9E, 0xB6, 0x64, 0xC7, 0x2C, 0x75, 0x75, 0xC6, 0x30, 0x89, 0x76, 0x2B, 0x30, 0x98, 0xF3, 0x46, + 0xD2, 0x50, 0xC6, 0x45, 0x0A, 0x93, 0x87, 0x4F, 0x3B, 0xD2, 0xCB, 0xCD, 0x3A, 0x64, 0x1F, 0x3B, + 0xF2, 0x05, 0x66, 0x1A, 0xFF, 0xC3, 0x23, 0x05, 0x00, 0x00 +}; //style.css + +//Content of icomoon.eot with gzip compression +static const uint8_t icomoon_eot[] PROGMEM = { + 0x1F, 0x8B, 0x08, 0x08, 0x21, 0x7F, 0x4E, 0x61, 0x04, 0x00, 0x69, 0x63, 0x6F, 0x6D, 0x6F, 0x6F, + 0x6E, 0x2E, 0x65, 0x6F, 0x74, 0x00, 0x95, 0x55, 0x4D, 0x88, 0xD3, 0x40, 0x14, 0x7E, 0x33, 0x69, + 0x93, 0xB4, 0xD1, 0x8D, 0x2B, 0x4D, 0x03, 0xBA, 0x6A, 0xB6, 0x71, 0x15, 0x11, 0xD7, 0xBA, 0xDD, + 0xB6, 0x88, 0x3F, 0xD5, 0x8B, 0x18, 0x7F, 0x8B, 0x8A, 0x07, 0x15, 0xB4, 0xAE, 0xB5, 0x0A, 0x6D, + 0x5A, 0x6A, 0xC5, 0x1F, 0x44, 0x3C, 0x89, 0x78, 0x50, 0xC1, 0x8B, 0x9E, 0x14, 0x51, 0x28, 0x08, + 0x5E, 0x8B, 0xE0, 0x61, 0x41, 0x44, 0x10, 0xEF, 0x0A, 0xEA, 0x41, 0x94, 0xBD, 0x08, 0xD2, 0x8B, + 0x28, 0xE8, 0x5A, 0xBF, 0x49, 0x52, 0xAB, 0x76, 0x11, 0x9D, 0x76, 0xE6, 0x7D, 0xDF, 0x9B, 0xF7, + 0xDE, 0xCC, 0x9B, 0x99, 0xCC, 0x4C, 0x2A, 0x44, 0x43, 0xA8, 0x8C, 0x38, 0xFD, 0x5A, 0x84, 0xE6, + 0x1A, 0x13, 0x72, 0x5B, 0x9E, 0xFA, 0x7A, 0xFC, 0x72, 0x8B, 0x5F, 0xD8, 0x48, 0x7D, 0x65, 0x80, + 0x8E, 0xD3, 0x04, 0x55, 0xA9, 0x42, 0x55, 0xFC, 0x5C, 0x4F, 0xB3, 0x8B, 0x8A, 0x54, 0xA2, 0x93, + 0x54, 0xA6, 0x02, 0xD5, 0xA1, 0x89, 0xD3, 0x5E, 0x2A, 0x02, 0x9D, 0x80, 0xAD, 0xB0, 0xB1, 0x68, + 0x15, 0x8D, 0x52, 0xB2, 0xDF, 0xBB, 0x37, 0xA0, 0x46, 0x17, 0x48, 0xA2, 0xE4, 0xCE, 0xDD, 0x2B, + 0xC7, 0xF4, 0xB9, 0xB3, 0x9E, 0x40, 0xF3, 0x08, 0xF5, 0xD0, 0x44, 0xA5, 0x50, 0x7B, 0x7B, 0xF6, + 0xED, 0x1D, 0x18, 0x0D, 0x81, 0x97, 0x4B, 0x85, 0x13, 0x35, 0xC8, 0x39, 0xE0, 0x97, 0x20, 0xD5, + 0x52, 0xF9, 0xCC, 0xD1, 0x87, 0x6A, 0xF3, 0x0A, 0xF8, 0x35, 0x22, 0x29, 0x79, 0xAC, 0x58, 0x38, + 0xB2, 0x60, 0xD9, 0xBE, 0x15, 0x44, 0xA1, 0x49, 0xF4, 0x67, 0x8E, 0x41, 0xA1, 0x24, 0xA5, 0x67, + 0xE0, 0x5F, 0xC0, 0x17, 0x1F, 0xAB, 0x34, 0x4E, 0xC7, 0xAE, 0x53, 0x8E, 0x28, 0x3C, 0x24, 0x78, + 0xB9, 0x3A, 0x51, 0xE0, 0xE7, 0xA5, 0x1A, 0xF8, 0x06, 0xF0, 0x58, 0xA5, 0x70, 0xBA, 0x86, 0x49, + 0xEE, 0x00, 0xDF, 0x03, 0x6E, 0xB9, 0x85, 0x4A, 0xF1, 0xE6, 0x96, 0xC8, 0x57, 0xF0, 0x06, 0xC6, + 0xB8, 0x58, 0xAB, 0x9E, 0x68, 0x60, 0x9E, 0x28, 0xF2, 0x37, 0xD1, 0x4F, 0x12, 0x7F, 0x8C, 0x91, + 0xC3, 0x44, 0xFC, 0x26, 0x7F, 0x0E, 0xCD, 0x55, 0x5F, 0xB2, 0x0F, 0x94, 0x62, 0x91, 0x3F, 0x17, + 0x76, 0xCE, 0x9F, 0x8B, 0x89, 0x31, 0xDB, 0x2F, 0xA5, 0xC9, 0xCE, 0x24, 0x6D, 0x90, 0x50, 0xBD, + 0xB5, 0xE8, 0x15, 0xCB, 0x6B, 0xA5, 0xA0, 0x0E, 0xA1, 0xCF, 0x93, 0x68, 0x99, 0x27, 0x43, 0x94, + 0x17, 0x6B, 0x81, 0x9F, 0xE4, 0x79, 0x5A, 0xED, 0xD9, 0xED, 0x7D, 0xED, 0xA7, 0xED, 0x97, 0x9D, + 0xE9, 0x4E, 0x47, 0xB8, 0xFE, 0xD4, 0xBC, 0xF0, 0x34, 0xAC, 0xF3, 0x4E, 0xFF, 0xAC, 0xDF, 0xD7, + 0x73, 0x7A, 0xD2, 0x8B, 0xD1, 0xBF, 0xF1, 0x0C, 0x56, 0x7A, 0xAF, 0x47, 0x1C, 0x9A, 0xEC, 0x6A, + 0x41, 0xFF, 0x43, 0x97, 0xA3, 0x1C, 0x9F, 0xE2, 0x53, 0xB4, 0x96, 0x68, 0x24, 0x26, 0xEB, 0x6C, + 0x80, 0xD9, 0x89, 0x25, 0x2B, 0x99, 0x12, 0xC8, 0x51, 0x96, 0x1E, 0xCF, 0xE4, 0xD8, 0xD2, 0x40, + 0xAE, 0x67, 0xA9, 0xB1, 0xF8, 0x22, 0x96, 0x0D, 0xE4, 0x42, 0x66, 0x08, 0x1F, 0x13, 0x92, 0x4F, + 0x45, 0x94, 0x1D, 0x6A, 0x4C, 0xD3, 0x62, 0x6A, 0xB3, 0xA9, 0x18, 0x5A, 0xD4, 0x00, 0x45, 0x69, + 0x36, 0x45, 0xBB, 0x43, 0x31, 0xA2, 0x9A, 0xA1, 0x80, 0x08, 0x0B, 0xD0, 0x48, 0x04, 0x44, 0x89, + 0x3C, 0xFE, 0x37, 0xB3, 0xDF, 0x43, 0x7B, 0x2B, 0x2C, 0x1A, 0x57, 0x72, 0x69, 0x11, 0xAD, 0xA4, + 0xAD, 0x98, 0x7B, 0x7A, 0x7C, 0xC9, 0x72, 0x36, 0x28, 0x9A, 0x84, 0xBC, 0x80, 0xC5, 0xE2, 0x6B, + 0x98, 0x61, 0xFB, 0x28, 0x35, 0x3C, 0x96, 0x49, 0x4B, 0xE9, 0x5F, 0xBB, 0xC6, 0x32, 0xE1, 0x18, + 0x32, 0x54, 0x64, 0x3B, 0x81, 0xCC, 0x96, 0x2E, 0x49, 0x8F, 0x23, 0xA3, 0x6C, 0x26, 0x35, 0x86, + 0x4C, 0xCC, 0x38, 0x77, 0xA2, 0x6A, 0x56, 0x53, 0x5A, 0x6A, 0x14, 0x32, 0x2B, 0x5A, 0x46, 0x6A, + 0xD4, 0xD1, 0x14, 0x57, 0x60, 0x57, 0xD1, 0x98, 0x6B, 0x27, 0x4E, 0xE5, 0xF3, 0xFB, 0xF7, 0xE7, + 0xF3, 0xA7, 0xEC, 0x44, 0xC2, 0xEE, 0xE2, 0x84, 0x7D, 0x6F, 0xBF, 0x1A, 0x65, 0x23, 0xC2, 0xEC, + 0x80, 0xA2, 0xB5, 0xA2, 0xEA, 0x7E, 0x55, 0xD3, 0x54, 0x4E, 0xD0, 0xFA, 0x78, 0xCA, 0x37, 0x0B, + 0xDC, 0x7B, 0x58, 0xA4, 0x13, 0xEA, 0xE6, 0x04, 0x64, 0x53, 0x8A, 0x36, 0x63, 0x97, 0x86, 0xD3, + 0xC3, 0xC6, 0x70, 0x6A, 0x70, 0xDC, 0xDB, 0x09, 0x1B, 0x8B, 0x6D, 0x0F, 0x27, 0x96, 0xAC, 0x63, + 0xF6, 0x60, 0x0A, 0xDB, 0x81, 0xC4, 0xE2, 0x0B, 0x58, 0x5F, 0x66, 0x83, 0xBE, 0x55, 0x7A, 0x50, + 0x58, 0x04, 0xBB, 0xF4, 0x8A, 0xB5, 0xBE, 0x3B, 0x9C, 0xB6, 0xCC, 0x92, 0x6F, 0xCB, 0xF3, 0x23, + 0x96, 0xF9, 0xBD, 0x65, 0x5A, 0x8E, 0x63, 0x99, 0xEC, 0x75, 0xDC, 0x7A, 0xA5, 0x29, 0xC8, 0x09, + 0x8D, 0x48, 0xCC, 0x32, 0xA7, 0xC9, 0xB4, 0x2C, 0x93, 0xCF, 0x35, 0xB3, 0x73, 0x6E, 0xCF, 0x31, + 0x9D, 0xD7, 0xAF, 0x39, 0xA9, 0x43, 0x70, 0x9B, 0xF5, 0x51, 0xE8, 0x3F, 0x4E, 0x2F, 0xF6, 0x25, + 0x9F, 0xDB, 0x12, 0xB9, 0xB5, 0xE0, 0xA9, 0x4D, 0x7B, 0x2A, 0x09, 0x8E, 0x26, 0x7C, 0xB2, 0x26, + 0xCE, 0x95, 0xF8, 0x73, 0x87, 0x3B, 0x64, 0x00, 0xC5, 0x64, 0xC5, 0x3F, 0x57, 0x4B, 0x91, 0x45, + 0x30, 0x71, 0x83, 0x3B, 0x72, 0xB8, 0x43, 0xE1, 0xD9, 0x8A, 0x3A, 0x5B, 0x48, 0x39, 0xAE, 0x73, + 0xD2, 0x0D, 0x3E, 0x12, 0x50, 0x39, 0xCC, 0xD0, 0xA9, 0xEA, 0x86, 0xA1, 0x07, 0x67, 0xF7, 0x15, + 0xE2, 0x8D, 0x50, 0x0C, 0xC4, 0x4F, 0x10, 0x81, 0xB2, 0xFE, 0x81, 0x34, 0x91, 0x22, 0x77, 0x0C, + 0xFD, 0x3B, 0xE9, 0xF1, 0xC0, 0x4F, 0x99, 0x2D, 0xA4, 0xCC, 0xA0, 0x89, 0xEB, 0xCA, 0x80, 0x47, + 0xBC, 0xF1, 0x06, 0xBA, 0xDF, 0xC1, 0xC6, 0x0B, 0xFC, 0xD6, 0x41, 0x7D, 0xED, 0x27, 0xD2, 0x42, + 0x1E, 0x7F, 0xB3, 0x33, 0xFA, 0xB6, 0x2B, 0xBB, 0x3B, 0x21, 0xEE, 0xAC, 0xEE, 0x55, 0x2C, 0xFC, + 0xC4, 0xB7, 0x2F, 0x36, 0xCA, 0xEF, 0xEF, 0xFB, 0x26, 0x23, 0xA1, 0x1E, 0xF6, 0xDC, 0xA4, 0x11, + 0xCA, 0x89, 0x28, 0xA2, 0x72, 0xC7, 0xAB, 0x7E, 0x89, 0x22, 0x8F, 0x85, 0x74, 0x96, 0xDE, 0xB3, + 0x2D, 0xAC, 0xCE, 0x6E, 0x78, 0x91, 0x22, 0xB4, 0x0D, 0xA1, 0xBB, 0xAE, 0xFD, 0x17, 0xF9, 0x03, + 0x7F, 0x3C, 0xB4, 0x8A, 0x68, 0x7D, 0x4B, 0xE0, 0x43, 0x01, 0x96, 0x80, 0x33, 0x01, 0x0E, 0x01, + 0x9F, 0x0C, 0x70, 0x18, 0xB7, 0xB5, 0x11, 0x60, 0x19, 0xFA, 0xAD, 0xC0, 0xFE, 0x1C, 0xE6, 0xD1, + 0x65, 0x71, 0xB3, 0x84, 0x22, 0xD0, 0x0C, 0xA0, 0xC7, 0xC7, 0x1C, 0xB8, 0x14, 0x60, 0x09, 0x78, + 0x5D, 0x80, 0x43, 0xC0, 0xE7, 0x02, 0x1C, 0xC6, 0xA3, 0x61, 0x05, 0x58, 0x16, 0x4F, 0x8A, 0x8F, + 0x11, 0x73, 0x9C, 0xEE, 0x1E, 0x9F, 0xA8, 0x56, 0xAA, 0x55, 0xF7, 0xF7, 0xB7, 0x63, 0x6F, 0xB1, + 0x7E, 0xE2, 0x78, 0xD5, 0xB5, 0x56, 0x8D, 0x26, 0x67, 0x7E, 0x6E, 0x66, 0xF4, 0x9A, 0x51, 0xB9, + 0xAB, 0x58, 0x3A, 0x59, 0x2E, 0xD4, 0x7F, 0x7F, 0xC7, 0x66, 0xB4, 0xDC, 0x54, 0x75, 0x1B, 0x56, + 0xA9, 0xE8, 0x16, 0xEB, 0x85, 0x46, 0xF1, 0x88, 0x75, 0xF8, 0x8C, 0xE5, 0x4C, 0x54, 0xB7, 0xC3, + 0x6C, 0x94, 0x36, 0x79, 0x63, 0x37, 0x30, 0x7A, 0x09, 0x61, 0x5C, 0x31, 0x23, 0x84, 0x69, 0x40, + 0x1E, 0x81, 0xEE, 0x30, 0x9D, 0x41, 0xEB, 0x78, 0xB1, 0xB6, 0x07, 0x8F, 0xDF, 0xA8, 0xB7, 0xC6, + 0x7F, 0x2D, 0x3F, 0x00, 0x43, 0x96, 0xB1, 0x3B, 0xC0, 0x07, 0x00, 0x00 +}; //icomoon.eot + +//Content of icomoon.svg with gzip compression +static const uint8_t icomoon_svg[] PROGMEM = { + 0x1F, 0x8B, 0x08, 0x08, 0x21, 0x7F, 0x4E, 0x61, 0x04, 0x00, 0x69, 0x63, 0x6F, 0x6D, 0x6F, 0x6F, + 0x6E, 0x2E, 0x73, 0x76, 0x67, 0x00, 0xAD, 0x57, 0x59, 0x8F, 0xD3, 0x30, 0x10, 0x7E, 0xE7, 0x57, + 0x98, 0x20, 0xF1, 0xC4, 0x24, 0xB6, 0x73, 0x43, 0xBB, 0x48, 0x1C, 0x42, 0x48, 0x2C, 0x20, 0x71, + 0x89, 0xC7, 0x90, 0xA4, 0x9B, 0x48, 0x69, 0x52, 0x35, 0x21, 0x85, 0xFD, 0xF5, 0x8C, 0xED, 0x71, + 0xB6, 0x09, 0x94, 0x2E, 0xC7, 0x43, 0x34, 0xB1, 0xFD, 0xCD, 0xE7, 0xB9, 0xD3, 0xAE, 0x1E, 0x7F, + 0xDB, 0x36, 0x6C, 0x2C, 0xF7, 0x7D, 0xDD, 0xB5, 0x6B, 0x47, 0xB8, 0xDC, 0x61, 0xFD, 0x90, 0xB5, + 0x45, 0xD6, 0x74, 0x6D, 0xB9, 0x76, 0xDA, 0xCE, 0x79, 0x7C, 0x71, 0x67, 0x75, 0xF7, 0xD9, 0x9B, + 0xA7, 0xEF, 0x3F, 0xBF, 0x7D, 0xCE, 0xFA, 0xF1, 0x8A, 0xBD, 0xFD, 0xF0, 0xE4, 0xD5, 0xCB, 0xA7, + 0xCC, 0x01, 0xCF, 0xFB, 0xE4, 0x3F, 0xF5, 0xBC, 0x67, 0xEF, 0x9F, 0xB1, 0x77, 0x1F, 0x5F, 0x30, + 0xE1, 0x0A, 0xCF, 0x7B, 0xFE, 0xDA, 0x61, 0x4E, 0x35, 0x0C, 0xBB, 0x87, 0x9E, 0x77, 0x38, 0x1C, + 0xDC, 0x83, 0xEF, 0x76, 0xFB, 0x2B, 0xEF, 0xC5, 0x3E, 0xDB, 0x55, 0x75, 0xDE, 0x7B, 0x08, 0xF4, + 0x10, 0xA8, 0x94, 0x3C, 0x24, 0x13, 0xC2, 0x2D, 0x86, 0xC2, 0x61, 0x78, 0x87, 0xA2, 0x46, 0x63, + 0xDA, 0x7E, 0xFD, 0x0B, 0x7D, 0xC9, 0x39, 0x57, 0x78, 0x07, 0x81, 0xDB, 0x72, 0xC8, 0x8A, 0x6C, + 0xC8, 0x2E, 0x5E, 0x94, 0x6D, 0xB9, 0xCF, 0x86, 0xB2, 0x60, 0x5F, 0xBE, 0xB3, 0x97, 0x79, 0x77, + 0xD9, 0x75, 0xED, 0xCA, 0xB3, 0xC7, 0x88, 0x2C, 0xCA, 0x4D, 0x8F, 0x62, 0xD3, 0xB5, 0x03, 0xAB, + 0x8B, 0xB5, 0x53, 0xE7, 0xDD, 0x16, 0x31, 0x0E, 0xAB, 0xBA, 0x7D, 0x7D, 0x0D, 0x59, 0x31, 0xC2, + 0x37, 0x74, 0x9A, 0xCB, 0xC0, 0x21, 0x18, 0x6C, 0xB2, 0xBC, 0x64, 0x5F, 0xDB, 0x7A, 0xE8, 0x61, + 0x57, 0xEE, 0xA1, 0xDC, 0xD2, 0x39, 0xCB, 0xFA, 0xBC, 0x6C, 0x87, 0xB5, 0x93, 0x46, 0x18, 0xA2, + 0xA2, 0xA4, 0x15, 0x44, 0x78, 0xE4, 0x29, 0xA3, 0xEA, 0xBE, 0xAF, 0xDB, 0x2B, 0xB8, 0x6A, 0xBE, + 0xEF, 0xAA, 0x9F, 0xF8, 0x09, 0xA4, 0x0F, 0x91, 0x1D, 0xED, 0x28, 0x30, 0xB8, 0xF7, 0xEF, 0x7D, + 0x93, 0xFC, 0xD1, 0xC2, 0x9A, 0x50, 0x48, 0xE4, 0x5F, 0x3B, 0xA7, 0x34, 0x36, 0x9C, 0x17, 0xA8, + 0xA3, 0x0F, 0xA0, 0xCD, 0xB6, 0xB8, 0x9B, 0x37, 0x5D, 0x5F, 0x3E, 0x60, 0xFB, 0x72, 0xDB, 0x8D, + 0x28, 0x87, 0x7A, 0x5B, 0xF6, 0x0B, 0xD6, 0x84, 0x87, 0x9A, 0xF5, 0x32, 0x0E, 0x84, 0x1B, 0x8B, + 0x80, 0x89, 0x34, 0x74, 0x03, 0x99, 0xE4, 0x1C, 0x44, 0xE0, 0xCA, 0x24, 0x82, 0x50, 0x6D, 0x83, + 0x4C, 0xDC, 0x30, 0x16, 0x20, 0x22, 0xF0, 0x13, 0x37, 0x09, 0xE3, 0x06, 0xE2, 0x58, 0x1D, 0x90, + 0xC8, 0x41, 0x70, 0x85, 0xB6, 0x42, 0x06, 0x73, 0x38, 0xBE, 0xF5, 0xC4, 0xC1, 0x34, 0x21, 0xED, + 0x33, 0x11, 0x35, 0x78, 0x96, 0x30, 0x7C, 0x80, 0x9E, 0x7F, 0xE2, 0x32, 0xE6, 0xB0, 0xB9, 0x55, + 0xCC, 0x12, 0x46, 0x6C, 0x22, 0x63, 0x46, 0xA9, 0x0F, 0x35, 0x9E, 0xE8, 0xA6, 0xED, 0xC6, 0xDA, + 0x44, 0xF2, 0x1F, 0x98, 0xE6, 0x16, 0x1D, 0xD3, 0x58, 0x8E, 0x1B, 0x30, 0xBE, 0xF5, 0x14, 0x69, + 0xC3, 0x36, 0x79, 0xDC, 0x58, 0x63, 0xE8, 0xF9, 0x7B, 0xA2, 0x79, 0xDE, 0x8C, 0xFE, 0xE4, 0x9D, + 0x8D, 0xB6, 0x7A, 0x23, 0xB7, 0x4E, 0x16, 0x80, 0xB5, 0x88, 0xE4, 0x79, 0xAA, 0xEB, 0xD3, 0xA5, + 0x1B, 0x66, 0x8B, 0xD2, 0xAD, 0xDB, 0x4D, 0x07, 0x79, 0xBD, 0xCF, 0x9B, 0x72, 0x59, 0xB1, 0x71, + 0x62, 0x2A, 0x36, 0x4C, 0x42, 0x57, 0x04, 0x3E, 0xF2, 0xEB, 0x7B, 0xC6, 0x54, 0x60, 0xE1, 0xA6, + 0x39, 0xB7, 0x39, 0x42, 0xCB, 0x12, 0xFD, 0x62, 0x04, 0xAD, 0x2A, 0x08, 0x03, 0x65, 0xCC, 0x28, + 0x53, 0xA9, 0xD4, 0xCE, 0xE3, 0x45, 0x22, 0x95, 0xC2, 0x54, 0x02, 0x1C, 0x77, 0x8C, 0x02, 0xCC, + 0x14, 0x46, 0xB0, 0x16, 0x58, 0xA4, 0x05, 0xCC, 0x89, 0x2B, 0x32, 0xC0, 0x12, 0x2B, 0x8B, 0xFE, + 0xEF, 0x05, 0x32, 0x8C, 0x72, 0xCB, 0x45, 0x47, 0xCC, 0x7A, 0x47, 0xE2, 0xFA, 0x12, 0xE7, 0x09, + 0x8B, 0xE2, 0xE8, 0x8F, 0x42, 0x27, 0x78, 0xAA, 0x8A, 0xE1, 0xFF, 0x59, 0x4A, 0x84, 0xE7, 0xAD, + 0x4D, 0xA8, 0x8D, 0x02, 0x53, 0x4A, 0x48, 0x2C, 0x03, 0x69, 0x98, 0x52, 0xED, 0x03, 0xD0, 0xC9, + 0x52, 0xF6, 0xF6, 0x85, 0x2D, 0x80, 0x6C, 0xB1, 0x6F, 0xD7, 0xA7, 0xA4, 0xBD, 0x68, 0x5A, 0x07, + 0x67, 0xAB, 0x3A, 0x8F, 0x17, 0x55, 0xBD, 0x69, 0xBA, 0xDD, 0xEE, 0x3B, 0x74, 0x0F, 0x58, 0x9F, + 0x8D, 0x27, 0x0B, 0x5B, 0x8A, 0x54, 0x05, 0x8F, 0xC5, 0xBE, 0xAA, 0xEF, 0x8A, 0xEE, 0x19, 0x69, + 0xBB, 0x02, 0xBB, 0x01, 0xB4, 0x73, 0x7D, 0x19, 0xFB, 0xE2, 0x58, 0xC3, 0x88, 0x11, 0x33, 0xAC, + 0x53, 0xAA, 0xAD, 0x4D, 0xCD, 0x18, 0x08, 0x4C, 0xCC, 0x62, 0xDD, 0x39, 0x66, 0xE4, 0xAB, 0x4E, + 0xE6, 0xDA, 0x33, 0x92, 0x39, 0x24, 0x98, 0x2F, 0xDF, 0xA4, 0xC4, 0x40, 0xC1, 0x40, 0x69, 0x85, + 0x37, 0xFB, 0x66, 0x7C, 0x70, 0x03, 0xB3, 0xBD, 0x4E, 0x95, 0x3C, 0x17, 0x15, 0xF8, 0x32, 0x55, + 0x6A, 0xB9, 0xE5, 0xE4, 0x74, 0x42, 0x63, 0xCB, 0xAE, 0xA8, 0x2F, 0x88, 0xBB, 0x02, 0x72, 0x03, + 0xC8, 0x3B, 0xEB, 0xD6, 0x74, 0x37, 0xF3, 0x8F, 0x87, 0x1F, 0xA9, 0xCF, 0x45, 0x15, 0xC4, 0xA1, + 0x2E, 0x44, 0x7B, 0x33, 0xED, 0xDB, 0xD9, 0x34, 0x33, 0x74, 0x72, 0x8B, 0x9A, 0x83, 0xFB, 0xEA, + 0x4B, 0x38, 0x52, 0x9B, 0xE2, 0x7D, 0x3A, 0x86, 0xA0, 0x07, 0xE1, 0xB9, 0x06, 0xD1, 0x62, 0xEA, + 0x0F, 0xA3, 0xB3, 0xEC, 0x91, 0x89, 0x98, 0xD0, 0x1A, 0xF4, 0xFB, 0x3E, 0x21, 0x5A, 0x3A, 0x35, + 0x1A, 0xA7, 0x5A, 0x85, 0xE6, 0xE3, 0x08, 0xA1, 0x71, 0xFE, 0x96, 0xC9, 0x8A, 0xA3, 0xE4, 0x96, + 0x89, 0x42, 0xE4, 0xED, 0x92, 0x40, 0x06, 0xDC, 0x24, 0x21, 0x96, 0xB6, 0x04, 0x53, 0x5F, 0xC7, + 0x74, 0xFA, 0xEA, 0x72, 0x55, 0x8C, 0xB9, 0x14, 0x7A, 0x57, 0x89, 0x9B, 0x6F, 0x57, 0xE4, 0xAB, + 0x4C, 0xDA, 0x95, 0x51, 0xFC, 0x4D, 0xF3, 0x15, 0xCB, 0xE6, 0xCB, 0xB3, 0x7D, 0x39, 0x40, 0xD1, + 0x1D, 0x96, 0xBF, 0xF3, 0x30, 0x50, 0xB3, 0x2F, 0x0A, 0x9A, 0xAD, 0x03, 0x6B, 0x13, 0x03, 0x01, + 0x08, 0x73, 0x29, 0xF5, 0x92, 0x0C, 0x4D, 0xDF, 0xE0, 0x9C, 0x55, 0x4F, 0x0E, 0x91, 0x31, 0x90, + 0x3E, 0xB1, 0x73, 0x18, 0xAD, 0x7A, 0xE2, 0x60, 0x01, 0xED, 0x53, 0x63, 0x6A, 0x16, 0x76, 0xC3, + 0xC2, 0xA2, 0xA3, 0x9B, 0xD8, 0x44, 0x86, 0x08, 0x4A, 0xBD, 0xC4, 0xEC, 0x9B, 0x41, 0xE4, 0xFF, + 0x42, 0x54, 0xAA, 0xE9, 0x11, 0xC2, 0x69, 0x0D, 0x33, 0x2C, 0x18, 0xF1, 0xBB, 0xA8, 0x25, 0xBF, + 0x8C, 0xDA, 0xD7, 0xDD, 0x99, 0x98, 0x29, 0x0F, 0xD4, 0x50, 0xC6, 0xFC, 0xD9, 0x9B, 0x7E, 0x16, + 0x15, 0x28, 0xEB, 0x40, 0x99, 0x47, 0x3B, 0x8C, 0xD0, 0x73, 0x57, 0xA8, 0xD3, 0x58, 0xC0, 0x28, + 0x66, 0xB3, 0x18, 0x34, 0x36, 0x60, 0x47, 0xF1, 0x42, 0x9E, 0x39, 0x88, 0x56, 0x3D, 0x25, 0x2E, + 0x60, 0xB3, 0x64, 0x34, 0x36, 0x73, 0x47, 0x89, 0xB3, 0x83, 0x72, 0x62, 0x42, 0xC0, 0x51, 0x89, + 0x79, 0xEA, 0x5F, 0xC0, 0xC5, 0xCA, 0xD3, 0x7F, 0x1D, 0x56, 0xEA, 0x2F, 0xC7, 0xC5, 0x0F, 0x30, + 0x32, 0xAD, 0x05, 0x24, 0x0D, 0x00, 0x00 +}; //icomoon.svg + +//Content of icomoon.ttf with gzip compression +static const uint8_t icomoon_ttf[] PROGMEM = { + 0x1F, 0x8B, 0x08, 0x08, 0x21, 0x7F, 0x4E, 0x61, 0x04, 0x00, 0x69, 0x63, 0x6F, 0x6D, 0x6F, 0x6F, + 0x6E, 0x2E, 0x74, 0x74, 0x66, 0x00, 0x95, 0x54, 0x4B, 0xA8, 0xD3, 0x40, 0x14, 0xBD, 0x33, 0x69, + 0x93, 0xB4, 0xD1, 0xC6, 0x27, 0x4D, 0x03, 0x7E, 0xF3, 0x1A, 0x9F, 0x22, 0xE2, 0xB3, 0xF6, 0x8B, + 0xF8, 0xA9, 0x6E, 0xC4, 0xF8, 0x2D, 0x2A, 0x2E, 0x54, 0xD0, 0x5A, 0x6B, 0x15, 0xDA, 0xB4, 0xF4, + 0x55, 0xFC, 0x20, 0xE2, 0x4A, 0xC4, 0x85, 0x0A, 0x6E, 0x74, 0xA5, 0x88, 0x42, 0x41, 0x70, 0x5B, + 0x04, 0x17, 0x82, 0x88, 0x20, 0xEE, 0x15, 0xD4, 0x85, 0x28, 0x6F, 0x23, 0x48, 0x37, 0xA2, 0xA0, + 0xCF, 0x7A, 0x26, 0x49, 0xFD, 0x23, 0x3A, 0xED, 0xDC, 0x7B, 0xCE, 0xFD, 0xCD, 0xDC, 0x4C, 0x26, + 0xC4, 0x88, 0x48, 0xA3, 0x33, 0x24, 0x51, 0x6A, 0xDB, 0x8E, 0x65, 0x69, 0x7D, 0xE6, 0xB4, 0x87, + 0xB0, 0xDC, 0xC3, 0xDC, 0x5F, 0x69, 0x94, 0x5B, 0xAF, 0x4E, 0xBE, 0xBA, 0x41, 0xC4, 0xE6, 0x80, + 0xD7, 0x6B, 0xE5, 0x89, 0x16, 0xF4, 0x0C, 0xF0, 0x73, 0xD0, 0x6A, 0xAD, 0x7E, 0xE2, 0xD0, 0x5D, + 0xB5, 0x7B, 0x01, 0xFC, 0x12, 0x91, 0x94, 0x3A, 0x5C, 0x2D, 0x1F, 0x9C, 0xBB, 0x78, 0xF7, 0x52, + 0xA2, 0xD0, 0x7D, 0xF8, 0xF3, 0x87, 0x61, 0x50, 0x52, 0xD2, 0x63, 0xF0, 0x8F, 0xE0, 0x0B, 0x0E, + 0x37, 0x3A, 0xC7, 0xE3, 0x97, 0xA9, 0x48, 0x14, 0x9E, 0x23, 0x78, 0xBD, 0x59, 0x29, 0xF3, 0xD3, + 0x52, 0x0B, 0x7C, 0x2D, 0x78, 0xBC, 0x51, 0x3E, 0xDE, 0xA2, 0x18, 0x6D, 0x05, 0xDF, 0x09, 0x6E, + 0xB9, 0xE5, 0x46, 0xF5, 0xEA, 0xC6, 0xC8, 0x27, 0xF0, 0x0E, 0xD6, 0x38, 0xDB, 0x6A, 0x4E, 0x74, + 0xB0, 0x4F, 0x0C, 0xF9, 0xB3, 0xF0, 0x93, 0xC4, 0x1F, 0x60, 0xE5, 0x30, 0x11, 0xBF, 0xCA, 0x9F, + 0xC0, 0x72, 0xD1, 0xD7, 0xEC, 0x2D, 0x65, 0x58, 0x84, 0x7E, 0x1E, 0x6C, 0x06, 0xFD, 0x32, 0xB0, + 0x66, 0xFF, 0x99, 0x74, 0x7F, 0x70, 0x9F, 0xD6, 0x4A, 0x98, 0x22, 0xE6, 0x07, 0xAF, 0xE5, 0x49, + 0x29, 0x98, 0x73, 0xE0, 0xF3, 0x34, 0x24, 0xF3, 0x74, 0x88, 0x4A, 0xE2, 0x59, 0xE0, 0x27, 0x79, + 0x99, 0x56, 0x7F, 0x7A, 0x7F, 0x77, 0xFF, 0x51, 0xFF, 0xD9, 0x60, 0x6A, 0x30, 0x10, 0xA9, 0xDF, + 0x2C, 0x4F, 0x3D, 0x0B, 0x1B, 0xBC, 0xD6, 0x3F, 0xE8, 0xB7, 0xF5, 0xA2, 0x9E, 0xF2, 0x6A, 0xFC, + 0x32, 0x60, 0x61, 0x88, 0xD2, 0x7F, 0xF0, 0x70, 0xA2, 0xC2, 0x0A, 0x41, 0xFF, 0xC3, 0x56, 0xA4, + 0x22, 0x9F, 0xE4, 0x93, 0xB4, 0x8A, 0x68, 0x2C, 0x2E, 0xEB, 0x2C, 0xC6, 0xEC, 0xE4, 0xC2, 0x65, + 0x4C, 0x09, 0xF4, 0x38, 0xCB, 0x65, 0xF3, 0x45, 0xB6, 0x28, 0xD0, 0x6B, 0x58, 0x26, 0x9D, 0x98, + 0xCF, 0x0A, 0x81, 0x9E, 0xC7, 0x0C, 0x91, 0x63, 0x42, 0xF3, 0xC9, 0x88, 0xB2, 0x55, 0x8D, 0x6B, + 0x5A, 0x5C, 0xED, 0x76, 0x15, 0x43, 0x8B, 0x1A, 0xA0, 0x18, 0xDD, 0xAE, 0x90, 0x5B, 0x15, 0x23, + 0xAA, 0x19, 0x0A, 0x88, 0x88, 0x00, 0x8D, 0x44, 0x40, 0x94, 0xC8, 0x83, 0x7F, 0x0B, 0xFB, 0xB9, + 0xB4, 0xF7, 0x84, 0x85, 0x70, 0x25, 0x97, 0xE6, 0xD3, 0x32, 0xDA, 0x84, 0xBD, 0xE7, 0xB2, 0x0B, + 0x97, 0xB0, 0x11, 0x21, 0x92, 0xF2, 0x5C, 0x16, 0x4F, 0xAC, 0x64, 0x86, 0xED, 0xA3, 0xCC, 0x68, + 0x3A, 0x9F, 0x93, 0x72, 0x3F, 0xBA, 0xD2, 0xF9, 0x70, 0x1C, 0x1D, 0x2A, 0xB2, 0x9D, 0x44, 0x67, + 0x8B, 0x16, 0xE6, 0xB2, 0xE8, 0xA8, 0x90, 0xCF, 0xA4, 0xD1, 0x89, 0x99, 0xE0, 0x4E, 0x54, 0x2D, + 0x68, 0x4A, 0x4F, 0x8D, 0x42, 0x17, 0x84, 0x64, 0xA4, 0x46, 0x1D, 0x4D, 0x71, 0x05, 0x76, 0x15, + 0x8D, 0xB9, 0x76, 0xF2, 0x58, 0xA9, 0xB4, 0x67, 0x4F, 0xA9, 0x74, 0xCC, 0x4E, 0x26, 0xED, 0x21, + 0x4E, 0xDA, 0xB7, 0xF6, 0xA8, 0x51, 0x36, 0x26, 0xC2, 0xF6, 0x2A, 0x5A, 0x2F, 0xAA, 0xEE, 0x51, + 0x35, 0x4D, 0xE5, 0x04, 0xAB, 0x8F, 0x27, 0xFD, 0xB0, 0x20, 0xFD, 0x3B, 0x16, 0xED, 0x84, 0x86, + 0x3D, 0x01, 0xD9, 0x94, 0xA1, 0x0D, 0x38, 0xA5, 0xD1, 0xDC, 0xA8, 0x31, 0x9A, 0x19, 0xC9, 0x7A, + 0x27, 0x61, 0xE3, 0x61, 0xDB, 0xA3, 0xC9, 0x85, 0xAB, 0x99, 0x3D, 0x92, 0xC1, 0x71, 0xA0, 0xB1, + 0xC4, 0x5C, 0xF6, 0x5B, 0x67, 0x23, 0x7E, 0x54, 0x6E, 0x44, 0x44, 0x04, 0xA7, 0xF4, 0x9C, 0xF5, + 0xBE, 0x38, 0x9C, 0x36, 0x4E, 0x93, 0xAF, 0xCB, 0xB3, 0x23, 0x96, 0xF9, 0xA5, 0x67, 0x5A, 0x8E, + 0x63, 0x99, 0xEC, 0x45, 0xC2, 0x7A, 0xAE, 0x29, 0xE8, 0x09, 0x42, 0x34, 0x66, 0x99, 0x53, 0x64, + 0x5A, 0x96, 0xC9, 0x67, 0x9A, 0x85, 0x19, 0xD7, 0x67, 0x98, 0xCE, 0x8B, 0x17, 0x9C, 0xD4, 0x39, + 0x48, 0x9B, 0xF6, 0x4E, 0xD8, 0xDF, 0x4D, 0x2D, 0xF0, 0x35, 0x9F, 0xD9, 0x13, 0xBD, 0xF5, 0x90, + 0xA9, 0x4D, 0x79, 0x26, 0x09, 0x89, 0x26, 0x72, 0x0A, 0x26, 0xDE, 0x2B, 0xF1, 0xE7, 0x0E, 0x77, + 0xC8, 0x00, 0x8A, 0xCB, 0x8A, 0xFF, 0x5E, 0x2D, 0x42, 0x17, 0xC1, 0xC6, 0x0D, 0xEE, 0xC8, 0xE1, + 0x01, 0x85, 0xA7, 0x2B, 0xEA, 0x74, 0xA1, 0xE5, 0x84, 0xCE, 0x49, 0x37, 0xF8, 0x58, 0x40, 0xE5, + 0x30, 0x83, 0x53, 0xD5, 0x0D, 0x43, 0x0F, 0xDE, 0xDD, 0xE7, 0xA8, 0x37, 0x46, 0x71, 0x10, 0xBF, + 0x41, 0x14, 0x2A, 0xF8, 0x2F, 0xA4, 0x89, 0x16, 0xB9, 0x63, 0xE8, 0x5F, 0x48, 0x4F, 0x04, 0x79, + 0xCA, 0x74, 0xA1, 0x65, 0x06, 0x4B, 0x42, 0x57, 0x62, 0x1E, 0xF1, 0xD6, 0x8B, 0x0D, 0xEF, 0xC1, + 0xBA, 0x33, 0xFC, 0xDA, 0x3E, 0x7D, 0xD5, 0x7B, 0xD2, 0x42, 0x1E, 0x7F, 0xB9, 0x2D, 0xFA, 0x6A, + 0xA8, 0x87, 0x27, 0x21, 0xBE, 0x59, 0xB8, 0x2D, 0xDE, 0x10, 0x79, 0xE2, 0xEE, 0x8B, 0x83, 0xF2, + 0xFD, 0xBF, 0xDD, 0xC9, 0x48, 0xE8, 0x3B, 0xF6, 0xD2, 0xA4, 0x31, 0x2A, 0x8A, 0x2A, 0x62, 0x72, + 0xC7, 0x9B, 0xFE, 0x88, 0xA2, 0x8F, 0x79, 0x74, 0x92, 0xDE, 0xB0, 0x8D, 0xAC, 0xCD, 0xAE, 0x78, + 0x95, 0x22, 0xB4, 0x19, 0xA5, 0xFD, 0xD4, 0xDF, 0x47, 0x8C, 0xEE, 0xF8, 0xEB, 0x41, 0x2A, 0x42, + 0xFA, 0x91, 0xC0, 0xFB, 0x03, 0x2C, 0x01, 0xE7, 0x03, 0x1C, 0x02, 0x3E, 0x1A, 0xE0, 0x30, 0xBE, + 0xD6, 0x46, 0x80, 0x65, 0xD8, 0x37, 0x01, 0xFB, 0x7B, 0x98, 0x45, 0xE7, 0xC5, 0x97, 0x25, 0x14, + 0x81, 0x25, 0x06, 0x8F, 0x8F, 0x39, 0x70, 0x2D, 0xC0, 0x12, 0xF0, 0xEA, 0x00, 0x87, 0x80, 0x4F, + 0xF9, 0x18, 0x35, 0x13, 0x64, 0x05, 0x58, 0x86, 0x7D, 0xBB, 0x8F, 0x51, 0x33, 0x4B, 0x37, 0x8F, + 0x54, 0x9A, 0x8D, 0x66, 0xD3, 0xA5, 0x23, 0x54, 0xA1, 0x26, 0x35, 0xA8, 0x89, 0x9F, 0xBB, 0xAB, + 0xDA, 0x9E, 0x38, 0xD2, 0x74, 0xAD, 0xE5, 0xE3, 0x29, 0xDA, 0x45, 0x55, 0x6A, 0xD3, 0x04, 0x02, + 0xE0, 0x40, 0x9D, 0xE5, 0x34, 0x4E, 0xA9, 0x3F, 0x66, 0xFD, 0xD1, 0xB8, 0xBD, 0x5A, 0x3B, 0x5A, + 0x2F, 0xB7, 0x69, 0x3B, 0xCA, 0xD4, 0xD0, 0x66, 0x9D, 0xCA, 0xD4, 0xFE, 0x63, 0xE4, 0xFA, 0xA6, + 0xDB, 0xB1, 0x6A, 0x55, 0xB7, 0xDA, 0x2E, 0x77, 0xAA, 0x07, 0xAD, 0x03, 0x27, 0x2C, 0xA7, 0xD2, + 0xDC, 0x82, 0xB0, 0x71, 0x5A, 0xEF, 0xAD, 0xDD, 0xC1, 0xEA, 0x35, 0x94, 0x71, 0xC5, 0x8E, 0x50, + 0xA6, 0x03, 0x7D, 0x10, 0xB6, 0x03, 0x74, 0x02, 0xD2, 0xF1, 0x6A, 0x6D, 0xF1, 0x6B, 0xD1, 0xB8, + 0xF7, 0x8C, 0xFF, 0x3A, 0xBE, 0x02, 0x44, 0xC0, 0xB3, 0xAF, 0x1C, 0x07, 0x00, 0x00 +}; //icomoon.ttf + +//Content of icomoon.woof with gzip compression +static const uint8_t icomoon_woof[] PROGMEM = { + 0x1F, 0x8B, 0x08, 0x08, 0x21, 0x7F, 0x4E, 0x61, 0x04, 0x00, 0x69, 0x63, 0x6F, 0x6D, 0x6F, 0x6F, + 0x6E, 0x2E, 0x77, 0x6F, 0x66, 0x66, 0x00, 0x95, 0x55, 0x4D, 0x68, 0x13, 0x41, 0x14, 0x7E, 0x33, + 0x9B, 0xEC, 0x6E, 0xB2, 0x9A, 0xB5, 0x92, 0xCD, 0x82, 0xBF, 0xDB, 0xAC, 0xAD, 0x88, 0x58, 0x6B, + 0xD3, 0x24, 0x88, 0x3F, 0x51, 0x90, 0xE2, 0xFA, 0x5B, 0x14, 0x3C, 0x58, 0x41, 0x63, 0x8D, 0x89, + 0xD0, 0x6C, 0x4A, 0x1A, 0xF1, 0x07, 0x11, 0x4F, 0x1E, 0x44, 0x54, 0xF0, 0xA2, 0x27, 0x45, 0x14, + 0x02, 0x82, 0xD7, 0xE0, 0x4D, 0x10, 0x11, 0xC4, 0xA3, 0xA0, 0x60, 0x3D, 0x88, 0xD2, 0x8B, 0x20, + 0xB9, 0x88, 0x07, 0xA9, 0xF1, 0x9B, 0xD9, 0x8D, 0xFF, 0x8A, 0x4E, 0xFA, 0xDE, 0xFB, 0xDE, 0x37, + 0xEF, 0xBD, 0x99, 0x37, 0xB3, 0xBB, 0x3D, 0xB1, 0x7B, 0x64, 0x84, 0x18, 0x61, 0x68, 0x15, 0x32, + 0xA4, 0x5D, 0x44, 0x7F, 0x18, 0xBB, 0xF7, 0xAE, 0x1E, 0x22, 0x62, 0x3A, 0xE0, 0x21, 0x21, 0xE6, + 0xFC, 0x39, 0x8F, 0xC6, 0xAB, 0xC5, 0x49, 0x70, 0x15, 0xF8, 0x13, 0x42, 0x5E, 0x9F, 0x7E, 0x7D, + 0xAB, 0x5C, 0x9C, 0x12, 0xDC, 0x33, 0xF8, 0x7A, 0x28, 0xF3, 0xCA, 0x13, 0xA7, 0x8E, 0x82, 0x9B, + 0x26, 0x52, 0x06, 0x85, 0xDC, 0xD7, 0x9B, 0x97, 0x2A, 0xA5, 0xE2, 0x11, 0xA2, 0xE8, 0x1C, 0xCC, + 0xE7, 0x84, 0x2C, 0x5E, 0xB1, 0x7F, 0x55, 0x05, 0x24, 0xB8, 0x2D, 0xF0, 0x97, 0x09, 0xD1, 0x06, + 0x95, 0x27, 0x95, 0x6A, 0xE3, 0x24, 0xB8, 0x4A, 0x97, 0x4B, 0x5E, 0xA5, 0xC2, 0x44, 0x6D, 0x5C, + 0xC4, 0x5D, 0x84, 0x9F, 0x14, 0xC2, 0xCF, 0x2A, 0x93, 0xD5, 0xE2, 0xC9, 0x49, 0x70, 0x37, 0xE1, + 0x3B, 0x52, 0x12, 0xB4, 0xCB, 0x2F, 0x56, 0x4B, 0xE0, 0x1E, 0x60, 0xED, 0xF3, 0x42, 0xAE, 0x6F, + 0x8B, 0x7D, 0x9A, 0xAC, 0x4D, 0x35, 0xD0, 0xE7, 0xD6, 0xAF, 0x71, 0x0A, 0xB4, 0xC2, 0x1F, 0xB2, + 0x2B, 0x14, 0x25, 0xE2, 0xD7, 0xF9, 0x53, 0xF8, 0x97, 0x03, 0xCB, 0xDE, 0x51, 0x86, 0xC5, 0x7E, + 0x3A, 0x08, 0x36, 0xEF, 0xE7, 0xA3, 0xD9, 0x44, 0xD4, 0x7E, 0xA1, 0x3C, 0xE8, 0x3C, 0xA0, 0x4D, + 0x0A, 0x44, 0xC4, 0x7C, 0x37, 0xEB, 0x48, 0xAD, 0x84, 0xB2, 0x08, 0x73, 0xD2, 0x42, 0x33, 0x69, + 0x23, 0x34, 0x2A, 0xCE, 0x08, 0x3F, 0x45, 0x66, 0x3A, 0xED, 0xB9, 0xED, 0xFD, 0xED, 0xC7, 0xED, + 0x17, 0x9D, 0xD9, 0x4E, 0x47, 0xA4, 0x7E, 0x65, 0x9E, 0x4B, 0x86, 0x75, 0xDE, 0x98, 0x1F, 0xCD, + 0xBB, 0x66, 0xC1, 0x1C, 0x94, 0x35, 0x7E, 0x1A, 0x60, 0x18, 0xA2, 0xCC, 0xEF, 0x66, 0x38, 0x51, + 0x7E, 0xAD, 0x70, 0xFF, 0x83, 0x2B, 0x50, 0x81, 0xCF, 0xF0, 0x19, 0x5A, 0x4F, 0xD4, 0x97, 0x54, + 0x4D, 0x96, 0x60, 0x6E, 0xBA, 0x7F, 0x35, 0xD3, 0x42, 0x3B, 0xC0, 0xB2, 0xC3, 0xB9, 0x02, 0x5B, + 0x1E, 0xDA, 0x8D, 0x2C, 0x33, 0x94, 0x5A, 0xCA, 0xF2, 0xA1, 0x5D, 0xC2, 0x2C, 0x91, 0x63, 0xC3, + 0xF2, 0x99, 0x98, 0xB6, 0x4B, 0x4F, 0x1A, 0x46, 0x52, 0x6F, 0x36, 0x35, 0xCB, 0x88, 0x5B, 0x70, + 0x31, 0x9A, 0x4D, 0xA1, 0x77, 0x69, 0x56, 0xDC, 0xB0, 0x34, 0x38, 0x22, 0x02, 0x6E, 0x2C, 0x06, + 0x47, 0x8B, 0x3D, 0xFC, 0xB7, 0xB0, 0x1F, 0x4B, 0xCB, 0x13, 0x16, 0xCA, 0x57, 0x7C, 0x5A, 0x4A, + 0xAB, 0x69, 0x3B, 0xF6, 0x9E, 0x1D, 0xEE, 0x5F, 0xC9, 0x7A, 0x84, 0x4A, 0xAB, 0x8B, 0x59, 0x32, + 0xB5, 0x8E, 0x59, 0x6E, 0x80, 0x32, 0xBD, 0x43, 0xB9, 0xAC, 0x92, 0xFD, 0x7E, 0x6A, 0x28, 0x17, + 0x4D, 0xA2, 0x43, 0x4D, 0x75, 0xD3, 0xE8, 0x6C, 0x79, 0x7F, 0x76, 0x18, 0x1D, 0xE5, 0x73, 0x99, + 0x21, 0x74, 0x62, 0xA7, 0xB8, 0x17, 0xD7, 0xF3, 0x86, 0xD6, 0xD2, 0xE3, 0xB0, 0x79, 0xA1, 0x19, + 0xE9, 0x71, 0xCF, 0xD0, 0x7C, 0x81, 0x7D, 0xCD, 0x60, 0xBE, 0x9B, 0x3E, 0x31, 0x3A, 0x3A, 0x36, + 0x36, 0x3A, 0x7A, 0xC2, 0x4D, 0xA7, 0xDD, 0x2E, 0x4E, 0xBB, 0x77, 0xC6, 0xF4, 0x38, 0xEB, 0x13, + 0x61, 0x07, 0x34, 0xA3, 0x15, 0xD7, 0xC7, 0x74, 0xC3, 0xD0, 0x39, 0x81, 0x0D, 0xF0, 0x4C, 0x10, + 0x16, 0xA6, 0x7F, 0xC3, 0xA2, 0x9D, 0x48, 0xB7, 0x27, 0x20, 0x97, 0x32, 0xB4, 0x15, 0xB7, 0xD4, + 0x9B, 0xED, 0xB5, 0x7A, 0x33, 0x3D, 0xC3, 0xF2, 0x26, 0x5C, 0x1C, 0xB6, 0xDB, 0x9B, 0xEE, 0xDF, + 0xC0, 0xDC, 0x9E, 0x0C, 0xAE, 0x03, 0x8D, 0xA5, 0x16, 0xB3, 0x5F, 0x3A, 0xEB, 0x09, 0xA2, 0xB2, + 0x3D, 0x22, 0x22, 0xBC, 0xA5, 0x97, 0xAC, 0xF5, 0xD9, 0xE3, 0xB4, 0x6D, 0x8E, 0x7A, 0x53, 0x5D, + 0x18, 0x73, 0xEC, 0xCF, 0x2D, 0xDB, 0xF1, 0x3C, 0xC7, 0x66, 0xD3, 0x29, 0xE7, 0xA5, 0xA1, 0xA1, + 0x27, 0x28, 0xD1, 0x98, 0x63, 0xCF, 0x92, 0xED, 0x38, 0x36, 0x9F, 0x6F, 0xE7, 0xE7, 0xDD, 0x9C, + 0x67, 0x7B, 0xD3, 0xD3, 0x9C, 0xF4, 0x45, 0x48, 0x9B, 0xF3, 0x5E, 0xF0, 0xEF, 0x67, 0x97, 0x05, + 0x96, 0xCF, 0x6F, 0x89, 0xDE, 0x5A, 0xC8, 0x34, 0x66, 0x25, 0xA5, 0x20, 0xD1, 0x46, 0x4E, 0xDE, + 0xC6, 0x73, 0x25, 0xFE, 0xB8, 0xC7, 0x3D, 0xB2, 0x80, 0x92, 0xAA, 0x16, 0x3C, 0x57, 0xCB, 0xD1, + 0x45, 0xB8, 0x71, 0x8B, 0x7B, 0x6A, 0xB4, 0x43, 0xD1, 0xB9, 0x9A, 0x3E, 0x57, 0x58, 0x35, 0x65, + 0x72, 0x32, 0x2D, 0xDE, 0x17, 0xBA, 0x6A, 0x94, 0x61, 0x52, 0x37, 0x2D, 0xCB, 0x0C, 0x9F, 0xDD, + 0x97, 0xA8, 0xD7, 0x27, 0xBE, 0x03, 0x2C, 0x68, 0x10, 0x85, 0xF2, 0xC1, 0x03, 0x69, 0xA3, 0x45, + 0xEE, 0x59, 0xE6, 0x67, 0x32, 0x53, 0x61, 0x9E, 0x36, 0x57, 0x58, 0x95, 0x81, 0x49, 0x99, 0x5A, + 0x42, 0x3A, 0x72, 0xBD, 0x44, 0xF7, 0x3D, 0xD8, 0x7C, 0x8E, 0xDF, 0x38, 0x68, 0xAE, 0xFF, 0x40, + 0x46, 0x44, 0xFA, 0xAF, 0x76, 0xC7, 0x5F, 0x77, 0x6D, 0xF7, 0x26, 0xE4, 0x77, 0x8D, 0x7F, 0x7D, + 0xDF, 0x48, 0xBC, 0xFB, 0xE2, 0xA2, 0x82, 0xF9, 0x5F, 0xDE, 0xC9, 0x58, 0xE4, 0x1B, 0x96, 0x69, + 0x4A, 0x1F, 0x15, 0x44, 0x15, 0x21, 0xDC, 0x93, 0x12, 0x8C, 0x38, 0xFA, 0x58, 0x42, 0xA7, 0xE9, + 0x2D, 0xDB, 0xC6, 0xEA, 0xEC, 0x9A, 0xAC, 0x14, 0xA3, 0x1D, 0x28, 0x1D, 0xA4, 0xFE, 0x3A, 0x12, + 0x74, 0x2F, 0x58, 0x0F, 0x5A, 0x13, 0x3A, 0x88, 0x04, 0x3E, 0x14, 0x62, 0x05, 0x38, 0x17, 0xE2, + 0x08, 0xF0, 0xF1, 0x10, 0x47, 0xF1, 0xC5, 0xB7, 0x42, 0xAC, 0x82, 0xDF, 0x0E, 0x1C, 0xEC, 0x61, + 0x01, 0x5D, 0x10, 0x5F, 0x96, 0x48, 0x0C, 0x4C, 0x02, 0x33, 0x01, 0xE6, 0xC0, 0xE5, 0x10, 0x2B, + 0xC0, 0x1B, 0x42, 0x1C, 0x01, 0x3E, 0x13, 0x60, 0xD4, 0x4C, 0x91, 0x13, 0x62, 0x15, 0xFC, 0x9E, + 0x00, 0xA3, 0xE6, 0x30, 0xDD, 0x3E, 0x36, 0x5E, 0xAB, 0xD6, 0x6A, 0x3E, 0x1D, 0xA3, 0x71, 0xAA, + 0x51, 0x95, 0x6A, 0xF8, 0xF9, 0xFB, 0x4A, 0xF5, 0xA9, 0x63, 0x35, 0xDF, 0x59, 0x33, 0x30, 0x48, + 0xFB, 0xA8, 0x44, 0x75, 0x9A, 0x42, 0x00, 0x26, 0x50, 0x67, 0x0D, 0x0D, 0xD0, 0xE0, 0x6F, 0xB3, + 0x7E, 0x4B, 0xEE, 0x29, 0x95, 0x8F, 0x4F, 0x14, 0xEB, 0x58, 0xB4, 0x84, 0x8D, 0x1E, 0xC7, 0xBF, + 0xA4, 0x22, 0xD5, 0x7F, 0x1B, 0x39, 0x52, 0xF3, 0x1B, 0x4E, 0xB9, 0xE4, 0x97, 0xEA, 0xC5, 0x46, + 0xE9, 0x88, 0x73, 0xF8, 0x94, 0xE3, 0x8D, 0xD7, 0x76, 0x22, 0x6C, 0x80, 0x46, 0xE4, 0xDA, 0x0D, + 0xAC, 0x5E, 0x46, 0x19, 0x5F, 0xEC, 0x08, 0x65, 0x1A, 0xB0, 0x47, 0xC0, 0x1D, 0xA6, 0x53, 0xD0, + 0x9E, 0xAC, 0xB5, 0x33, 0xA8, 0x45, 0x03, 0xF2, 0x8C, 0xFF, 0x3A, 0xBE, 0x00, 0x66, 0xCF, 0x16, + 0x00, 0x68, 0x07, 0x00, 0x00 +}; //icomoon.woof + +//Content of xxx with gzip compression +//static const uint8_t rtkSetup_png[] PROGMEM = { +//}; // \ No newline at end of file diff --git a/Firmware/RTK_Surveyor/icons.h b/Firmware/RTK_Surveyor/icons.h index 398efbaa0..807f83705 100644 --- a/Firmware/RTK_Surveyor/icons.h +++ b/Firmware/RTK_Surveyor/icons.h @@ -1,123 +1,1498 @@ -//Create a bitmap then use http://en.radzio.dxp.pl/bitmap_converter/ to generate output -//Make sure the bitmap is n*8 pixels tall (pad white pixels to lower area as needed) -//Otherwise the bitmap bitmap_converter will compress some of the bytes together +// Create a bitmap then use http://en.radzio.dxp.pl/bitmap_converter/ to generate output +// Make sure the bitmap is n*8 pixels tall (pad white pixels to lower area as needed) +// Otherwise the bitmap bitmap_converter will compress some of the bytes together -uint8_t BT_Symbol [] = { -0x18, 0x30, 0xE0, 0xFF, 0xE6, 0x3C, 0x18, 0x06, 0x03, 0x01, 0x3F, 0x19, 0x0F, 0x06, -}; -int BT_Symbol_Height = 14; -int BT_Symbol_Width = 7; +/* + BT_Symbol [7, 14] -uint8_t WiFi_Symbol [] = { -0x08, 0x04, 0x12, 0x09, 0x25, 0x95, 0xD5, 0x95, 0x25, 0x09, 0x12, 0x04, 0x08, 0x00, 0x00, 0x00, -0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, -}; -int WiFi_Symbol_Height = 9; -int WiFi_Symbol_Width = 13; - -//uint8_t WiFi_Symbol [] = { -//0x78, 0x04, 0x72, 0x09, 0x65, 0x15, 0xD5, 0xD5, 0x15, 0x65, 0x09, 0x72, 0x04, 0x78, -//}; -//int WiFi_Symbol_Height = 8; -//int WiFi_Symbol_Width = 13; - -uint8_t CrossHair [] = { -0x80, 0x80, 0xF0, 0x88, 0x84, 0x84, 0x84, 0x7F, 0x84, 0x84, 0x84, 0x88, 0xF0, 0x80, 0x80, 0x00, -0x00, 0x07, 0x08, 0x10, 0x10, 0x10, 0x7F, 0x10, 0x10, 0x10, 0x08, 0x07, 0x00, 0x00, -}; -int CrossHair_Height = 15; -int CrossHair_Width = 15; + 1234567 + .-------. + 0x01| * | + 0x02| ** | + 0x04| *** | + 0x08|* * **| + 0x10|** * **| + 0x20| ***** | + 0x40| *** | + 0x80| *** | + 0x01| ***** | + 0x02|** * **| + 0x04|* * **| + 0x08| *** | + 0x10| ** | + 0x20| * | + '-------' +*/ -uint8_t CrossHairDual [] = { -0x80, 0x80, 0xF0, 0x88, 0xE4, 0x94, 0x94, 0x7F, 0x94, 0x94, 0xE4, 0x88, 0xF0, 0x80, 0x80, 0x00, -0x00, 0x07, 0x08, 0x13, 0x14, 0x14, 0x7F, 0x14, 0x14, 0x13, 0x08, 0x07, 0x00, 0x00, -}; -int CrossHairDual_Height = 15; -int CrossHairDual_Width = 15; +const int BT_Symbol_Height = 14; +const int BT_Symbol_Width = 7; +const uint8_t BT_Symbol[] = {0x18, 0x30, 0xE0, 0xFF, 0xE6, 0x3C, 0x18, 0x06, 0x03, 0x01, 0x3F, 0x19, 0x0F, 0x06}; -uint8_t Antenna [] = { -0x00, 0x1E, 0x62, 0x84, 0x08, 0x10, 0x20, 0x50, 0x88, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x1F, -0x1F, 0x12, 0x12, 0x04, 0x04, 0x05, 0x06, 0x00, -}; -int Antenna_Height = 13; -int Antenna_Width = 12; +/* + WiFi_Symbol_3 [13, 9] -uint8_t Rover [] = { -0x1E, 0x61, 0x91, 0x91, 0x61, 0x21, 0x21, 0x21, 0x21, 0x21, 0x62, 0x94, 0x94, 0x64, 0x1C, -}; -int Rover_Height = 8; -int Rover_Width = 15; + 1 + 1234567890123 + .-------------. + 0x01| ******* | + 0x02| * * | + 0x04| * ***** * | + 0x08|* * * *| + 0x10| * *** * | + 0x20| * * | + 0x40| * | + 0x80| *** | + 0x01| * | + '-------------' +*/ -uint8_t BaseTemporary [] = { -0x00, 0xFF, 0x99, 0x99, 0xE7, 0xCE, 0x32, 0x32, 0xE7, 0xE7, 0x99, 0x32, 0xFE, 0x00, 0x00, 0x1F, -0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, -}; -int BaseTemporary_Height = 12; -int BaseTemporary_Width = 14; +const int WiFi_Symbol_Height = 9; +const int WiFi_Symbol_Width = 13; +const uint8_t WiFi_Symbol_3[] = {0x08, 0x04, 0x12, 0x09, 0x25, 0x95, 0xD5, 0x95, 0x25, 0x09, 0x12, 0x04, 0x08, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; -uint8_t BaseFixed [] = { -0x00, 0xFF, 0x01, 0x0F, 0x01, 0x8F, 0x88, 0x88, 0x8F, 0x01, 0x0F, 0x01, 0xFF, 0x00, 0x0E, 0x09, -0x08, 0x08, 0x08, 0x0F, 0x00, 0x00, 0x0F, 0x08, 0x08, 0x08, 0x09, 0x0E, -}; -int BaseFixed_Height = 12; -int BaseFixed_Width = 14; +/* + WiFi_Symbol_2 [13, 9] -uint8_t Battery_3 [] = { -0xFF, 0x01, 0xFD, 0xFD, 0xFD, 0x01, 0x01, 0xFD, 0xFD, 0xFD, 0x01, 0x01, 0xFD, 0xFD, 0xFD, 0x01, -0x0F, 0x08, 0xF8, 0x0F, 0x08, 0x0B, 0x0B, 0x0B, 0x08, 0x08, 0x0B, 0x0B, 0x0B, 0x08, 0x08, 0x0B, -0x0B, 0x0B, 0x08, 0x0F, 0x01, 0x01, -}; -int Battery_3_Height = 12; -int Battery_3_Width = 19; + 1 + 1234567890123 + .-------------. + 0x01| | + 0x02| | + 0x04| ***** | + 0x08| * * | + 0x10| * *** * | + 0x20| * * | + 0x40| * | + 0x80| *** | + 0x01| * | + '-------------' +*/ -uint8_t Battery_2 [] = { -0xFF, 0x01, 0xFD, 0xFD, 0xFD, 0x01, 0x01, 0xFD, 0xFD, 0xFD, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, -0x0F, 0x08, 0xF8, 0x0F, 0x08, 0x0B, 0x0B, 0x0B, 0x08, 0x08, 0x0B, 0x0B, 0x0B, 0x08, 0x08, 0x08, -0x08, 0x08, 0x08, 0x0F, 0x01, 0x01, -}; -int Battery_2_Height = 12; -int Battery_2_Width = 19; +const uint8_t WiFi_Symbol_2[] = {0x00, 0x00, 0x10, 0x08, 0x24, 0x94, 0xD4, 0x94, 0x24, 0x08, 0x10, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; -uint8_t Battery_1 [] = { -0xFF, 0x01, 0xFD, 0xFD, 0xFD, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, -0x0F, 0x08, 0xF8, 0x0F, 0x08, 0x0B, 0x0B, 0x0B, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, -0x08, 0x08, 0x08, 0x0F, 0x01, 0x01, -}; -int Battery_1_Height = 12; -int Battery_1_Width = 19; +/* + WiFi_Symbol_1 [13, 9] -uint8_t Battery_0 [] = { -0xFF, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, -0x0F, 0x08, 0xF8, 0x0F, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, -0x08, 0x08, 0x08, 0x0F, 0x01, 0x01, -}; -int Battery_0_Height = 12; -int Battery_0_Width = 19; + 1 + 1234567890123 + .-------------. + 0x01| | + 0x02| | + 0x04| | + 0x08| | + 0x10| *** | + 0x20| * * | + 0x40| * | + 0x80| *** | + 0x01| * | + '-------------' +*/ -uint8_t Logging_3 [] = { -0xFF, 0x01, 0x51, 0x51, 0x51, 0x51, 0x53, 0x06, 0xFC, 0x0F, 0x08, 0x09, 0x09, 0x09, 0x09, 0x09, -0x08, 0x0F, -}; -int Logging_3_Height = 12; -int Logging_3_Width = 9; +const uint8_t WiFi_Symbol_1[] = {0x00, 0x00, 0x00, 0x00, 0x20, 0x90, 0xD0, 0x90, 0x20, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; -uint8_t Logging_2 [] = { -0xFF, 0x01, 0x41, 0x41, 0x41, 0x41, 0x43, 0x06, 0xFC, 0x0F, 0x08, 0x09, 0x09, 0x09, 0x09, 0x09, -0x08, 0x0F, -}; -int Logging_2_Height = 12; -int Logging_2_Width = 9; +/* + WiFi_Symbol_0 [13, 9] + + 1 + 1234567890123 + .-------------. + 0x01| | + 0x02| | + 0x04| | + 0x08| | + 0x10| | + 0x20| | + 0x40| * | + 0x80| *** | + 0x01| * | + '-------------' +*/ + +const uint8_t WiFi_Symbol_0[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xC0, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +/* + Clock [15, 15] + + 1 + 123456789012345 + .---------------. + 0x01| ***** | + 0x02| ** * ** | + 0x04| * * * * | + 0x08| * * * | + 0x10| ** * ** | + 0x20|* * *| + 0x40|* * *| + 0x80|** * **| + 0x01|* ** *| + 0x02|* * *| + 0x04| ** ** | + 0x08| * * | + 0x10| * * * * | + 0x20| ** * ** | + 0x40| ***** | + '---------------' +*/ + +const int Clock_Icon_Height = 15; +const int Clock_Icon_Width = 15; +const uint8_t Clock_Icon_1[] = {0xE0, 0x98, 0x14, 0x02, 0x06, 0x01, 0x01, 0xFB, 0x01, 0x01, + 0x06, 0x02, 0x14, 0x98, 0xE0, 0x03, 0x0C, 0x14, 0x20, 0x30, + 0x40, 0x40, 0x60, 0x41, 0x41, 0x32, 0x20, 0x14, 0x0C, 0x03}; + +/* + Clock [15, 15] + + 1 + 123456789012345 + .---------------. + 0x01| ***** | + 0x02| ** * ** | + 0x04| * * * * | + 0x08| * * | + 0x10| ** ** | + 0x20|* *| + 0x40|* *| + 0x80|** ***** **| + 0x01|* ** *| + 0x02|* * *| + 0x04| ** ** | + 0x08| * * | + 0x10| * * * * | + 0x20| ** * ** | + 0x40| ***** | + '---------------' +*/ + +const uint8_t Clock_Icon_2[] = {0xE0, 0x98, 0x14, 0x02, 0x06, 0x01, 0x01, 0x83, 0x81, 0x81, + 0x86, 0x82, 0x14, 0x98, 0xE0, 0x03, 0x0C, 0x14, 0x20, 0x30, + 0x40, 0x40, 0x60, 0x41, 0x41, 0x32, 0x20, 0x14, 0x0C, 0x03}; + +/* + Clock [15, 15] + + 1 + 123456789012345 + .---------------. + 0x01| ***** | + 0x02| ** * ** | + 0x04| * * * * | + 0x08| * * | + 0x10| ** ** | + 0x20|* *| + 0x40|* *| + 0x80|** * **| + 0x01|* ** *| + 0x02|* ** *| + 0x04| ** * * ** | + 0x08| * * * | + 0x10| * * * * | + 0x20| ** * ** | + 0x40| ***** | + '---------------' +*/ + +const uint8_t Clock_Icon_3[] = {0xE0, 0x98, 0x14, 0x02, 0x06, 0x01, 0x01, 0x83, 0x01, 0x01, + 0x06, 0x02, 0x14, 0x98, 0xE0, 0x03, 0x0C, 0x14, 0x20, 0x30, + 0x40, 0x40, 0x6F, 0x43, 0x44, 0x30, 0x20, 0x14, 0x0C, 0x03}; + +/* + Clock [15, 15] + + 1 + 123456789012345 + .---------------. + 0x01| ***** | + 0x02| ** * ** | + 0x04| * * * * | + 0x08| * * | + 0x10| ** ** | + 0x20|* *| + 0x40|* *| + 0x80|** ***** **| + 0x01|* * *| + 0x02|* * *| + 0x04| ** * ** | + 0x08| * * | + 0x10| * * * * | + 0x20| ** * ** | + 0x40| ***** | + '---------------' +*/ + +const uint8_t Clock_Icon_4[] = {0xE0, 0x98, 0x14, 0x82, 0x86, 0x81, 0x81, 0x83, 0x01, 0x01, + 0x06, 0x02, 0x14, 0x98, 0xE0, 0x03, 0x0C, 0x14, 0x20, 0x30, + 0x40, 0x40, 0x60, 0x43, 0x44, 0x30, 0x20, 0x14, 0x0C, 0x03}; + +/* + CrossHair [15, 15] + + 1 + 123456789012345 + .---------------. + 0x01| * | + 0x02| * | + 0x04| ******* | + 0x08| * * * | + 0x10| * * * | + 0x20| * * * | + 0x40| * * * | + 0x80|******* *******| + 0x01| * * * | + 0x02| * * * | + 0x04| * * * | + 0x08| * * * | + 0x10| ******* | + 0x20| * | + 0x40| * | + '---------------' +*/ + +const int CrossHair_Height = 15; +const int CrossHair_Width = 15; +const uint8_t CrossHair[] = {0x80, 0x80, 0xF0, 0x88, 0x84, 0x84, 0x84, 0x7F, 0x84, 0x84, 0x84, 0x88, 0xF0, 0x80, 0x80, + 0x00, 0x00, 0x07, 0x08, 0x10, 0x10, 0x10, 0x7F, 0x10, 0x10, 0x10, 0x08, 0x07, 0x00, 0x00}; + +/* + CrossHairDual [15, 15] + + 1 + 123456789012345 + .---------------. + 0x01| * | + 0x02| * | + 0x04| ******* | + 0x08| * * * | + 0x10| * ***** * | + 0x20| * * * * * | + 0x40| * * * * * | + 0x80|******* *******| + 0x01| * * * * * | + 0x02| * * * * * | + 0x04| * ***** * | + 0x08| * * * | + 0x10| ******* | + 0x20| * | + 0x40| * | + '---------------' +*/ + +const int CrossHairDual_Height = 15; +const int CrossHairDual_Width = 15; +const uint8_t CrossHairDual[] = {0x80, 0x80, 0xF0, 0x88, 0xE4, 0x94, 0x94, 0x7F, 0x94, 0x94, + 0xE4, 0x88, 0xF0, 0x80, 0x80, 0x00, 0x00, 0x07, 0x08, 0x13, + 0x14, 0x14, 0x7F, 0x14, 0x14, 0x13, 0x08, 0x07, 0x00, 0x00}; + +/* + SIV_Antenna [12, 13] + + 1 + 123456789012 + .------------. + 0x01| | + 0x02| ** | + 0x04| * * | + 0x08| * * * | + 0x10| * * * | + 0x20| * * | + 0x40| * * | + 0x80| * * | + 0x01| ** * | + 0x02| **** * | + 0x04| ** **** | + 0x08| ** | + 0x10| ****** | + '------------' +*/ + +const int SIV_Antenna_Height = 13; +const int SIV_Antenna_Width = 12; +const uint8_t SIV_Antenna[] = {0x00, 0x1E, 0x62, 0x84, 0x08, 0x10, 0x20, 0x50, 0x88, 0x00, 0x00, 0x00, + 0x00, 0x10, 0x10, 0x1F, 0x1F, 0x12, 0x12, 0x04, 0x04, 0x05, 0x06, 0x00}; + +/* + SIV_Antenna_LBand [12, 13] + + 1 + 123456789012 + .------------. + 0x01| | + 0x02| ** * | + 0x04| * * * | + 0x08| * * * | + 0x10| * * * | + 0x20| * * * | + 0x40| * * * | + 0x80| * * | + 0x01| ** * | + 0x02| **** * | + 0x04| ** **** | + 0x08| ** | + 0x10| ****** | + '------------' +*/ + +const int SIV_Antenna_LBand_Height = 13; +const int SIV_Antenna_LBand_Width = 12; +const uint8_t SIV_Antenna_LBand[] = {0x00, 0x1E, 0x62, 0x84, 0x08, 0x14, 0x22, 0x50, 0x88, 0x40, 0x20, 0x00, + 0x00, 0x10, 0x10, 0x1F, 0x1F, 0x12, 0x12, 0x04, 0x04, 0x05, 0x06, 0x00}; + +/* + Antenna_Short [12, 13] + + 1 + 123456789012 + .------------. + 0x01| * | + 0x02| * | + 0x04| * | + 0x08| ** | + 0x10| * * | + 0x20| * ***** | + 0x40| * * | + 0x80| ***** * | + 0x01| * * | + 0x02| ** | + 0x04| * | + 0x08| * | + 0x10| * | + '------------' +*/ + +const int Antenna_Short_Height = 13; +const int Antenna_Short_Width = 12; +const uint8_t Antenna_Short[] = {0x00, 0x80, 0xC0, 0xA0, 0x90, 0x88, 0x3F, 0x20, 0xA0, 0x60, 0x20, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x02, 0x01, 0x00, 0x00, 0x00, 0x00}; + +/* + Antenna_Open [12, 13] + + 1 + 123456789012 + .------------. + 0x01| ** | + 0x02| ** | + 0x04| ** | + 0x08| ** | + 0x10| ** | + 0x20| ** | + 0x40| ** ** | + 0x80| ** | + 0x01| ** | + 0x02| ** | + 0x04| ** | + 0x08| ** | + 0x10| ** | + '------------' +*/ + +const int Antenna_Open_Height = 13; +const int Antenna_Open_Width = 12; +const uint8_t Antenna_Open[] = {0x00, 0x00, 0x00, 0x60, 0x70, 0x1F, 0x0F, 0xC0, 0xC0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x1F, 0x01, 0x00, 0x00, 0x00, 0x00}; + +/* + Rover_Fusion [15, 9] + + 1 + 123456789012345 + .---------------. + 0x01| ********* | + 0x02|* * | + 0x04|* **** ****| + 0x08|* * *| + 0x10|* *** *| + 0x20|* ** * ** *| + 0x40| * ******* * | + 0x80| * * * * | + 0x01| ** ** | + '---------------' +*/ + +const int Rover_Fusion_Height = 9; +const int Rover_Fusion_Width = 15; +const uint8_t Rover_Fusion[] = {0x3E, 0xC1, 0x21, 0x21, 0xC1, 0x7D, 0x55, 0x55, 0x45, 0x41, + 0xC2, 0x24, 0x24, 0xC4, 0x3C, 0x00, 0x00, 0x01, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00}; + +/* + Rover_Fusion_Empty [15, 9] + + 1 + 123456789012345 + .---------------. + 0x01| ********* | + 0x02|* * | + 0x04|* ****| + 0x08|* *| + 0x10|* *| + 0x20|* ** ** *| + 0x40| * ******* * | + 0x80| * * * * | + 0x01| ** ** | + '---------------' +*/ + +const int Rover_Fusion_Empty_Height = 9; +const int Rover_Fusion_Empty_Width = 15; +const uint8_t Rover_Fusion_Empty[] = {0x3E, 0xC1, 0x21, 0x21, 0xC1, 0x41, 0x41, 0x41, 0x41, 0x41, + 0xC2, 0x24, 0x24, 0xC4, 0x3C, 0x00, 0x00, 0x01, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00}; + +/* + BaseTemporary [14, 12] + + 1 + 12345678901234 + .--------------. + 0x01| **** *** | + 0x02| * ****** ** | + 0x04| * ** ** * | + 0x08| *** * * * | + 0x10| *** ** *** | + 0x20| * * **** ** | + 0x40| * ** ** * | + 0x80| ***** *** * | + 0x01| * *** ** | + 0x02| * | + 0x04| * | + 0x08| * | + '--------------' +*/ + +const int BaseTemporary_Height = 12; +const int BaseTemporary_Width = 14; +const uint8_t BaseTemporary[] = {0x00, 0xFF, 0x99, 0x99, 0xE7, 0xCE, 0x32, 0x32, 0xE7, 0xE7, 0x99, 0x32, 0xFE, 0x00, + 0x00, 0x1F, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00}; + +/* + BaseFixed [14, 12] + + 1 + 12345678901234 + .--------------. + 0x01| ***** ***** | + 0x02| * * * * * * | + 0x04| * * * * * * | + 0x08| * * **** * * | + 0x10| * * | + 0x20| * * | + 0x40| * * | + 0x80| * **** * | + 0x01| * * * * | + 0x02|* * * *| + 0x04|* * * *| + 0x08|****** ******| + '--------------' +*/ + +const int BaseFixed_Height = 12; +const int BaseFixed_Width = 14; +const uint8_t BaseFixed[] = {0x00, 0xFF, 0x01, 0x0F, 0x01, 0x8F, 0x88, 0x88, 0x8F, 0x01, 0x0F, 0x01, 0xFF, 0x00, + 0x0E, 0x09, 0x08, 0x08, 0x08, 0x0F, 0x00, 0x00, 0x0F, 0x08, 0x08, 0x08, 0x09, 0x0E}; + +/* + Battery_3 [19, 12] + + 1 + 1234567890123456789 + .-------------------. + 0x01|***************** | + 0x02|* * | + 0x04|* *** *** *** * | + 0x08|* *** *** *** ***| + 0x10|* *** *** *** *| + 0x20|* *** *** *** *| + 0x40|* *** *** *** *| + 0x80|* *** *** *** *| + 0x01|* *** *** *** ***| + 0x02|* *** *** *** * | + 0x04|* * | + 0x08|***************** | + '-------------------' +*/ + +const int Battery_3_Height = 12; +const int Battery_3_Width = 19; +const uint8_t Battery_3[] = {0xFF, 0x01, 0xFD, 0xFD, 0xFD, 0x01, 0x01, 0xFD, 0xFD, 0xFD, 0x01, 0x01, 0xFD, + 0xFD, 0xFD, 0x01, 0x0F, 0x08, 0xF8, 0x0F, 0x08, 0x0B, 0x0B, 0x0B, 0x08, 0x08, + 0x0B, 0x0B, 0x0B, 0x08, 0x08, 0x0B, 0x0B, 0x0B, 0x08, 0x0F, 0x01, 0x01}; + +/* + Battery_2 [19, 12] + + 1 + 1234567890123456789 + .-------------------. + 0x01|***************** | + 0x02|* * | + 0x04|* *** *** * | + 0x08|* *** *** ***| + 0x10|* *** *** *| + 0x20|* *** *** *| + 0x40|* *** *** *| + 0x80|* *** *** *| + 0x01|* *** *** ***| + 0x02|* *** *** * | + 0x04|* * | + 0x08|***************** | + '-------------------' +*/ + +const int Battery_2_Height = 12; +const int Battery_2_Width = 19; +const uint8_t Battery_2[] = {0xFF, 0x01, 0xFD, 0xFD, 0xFD, 0x01, 0x01, 0xFD, 0xFD, 0xFD, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x0F, 0x08, 0xF8, 0x0F, 0x08, 0x0B, 0x0B, 0x0B, 0x08, 0x08, + 0x0B, 0x0B, 0x0B, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0F, 0x01, 0x01}; + +/* + Battery_1 [19, 12] + + 1 + 1234567890123456789 + .-------------------. + 0x01|***************** | + 0x02|* * | + 0x04|* *** * | + 0x08|* *** ***| + 0x10|* *** *| + 0x20|* *** *| + 0x40|* *** *| + 0x80|* *** *| + 0x01|* *** ***| + 0x02|* *** * | + 0x04|* * | + 0x08|***************** | + '-------------------' +*/ + +const int Battery_1_Height = 12; +const int Battery_1_Width = 19; +const uint8_t Battery_1[] = {0xFF, 0x01, 0xFD, 0xFD, 0xFD, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x0F, 0x08, 0xF8, 0x0F, 0x08, 0x0B, 0x0B, 0x0B, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0F, 0x01, 0x01}; + +/* + Battery_0 [19, 12] + + 1 + 1234567890123456789 + .-------------------. + 0x01|***************** | + 0x02|* * | + 0x04|* * | + 0x08|* ***| + 0x10|* *| + 0x20|* *| + 0x40|* *| + 0x80|* *| + 0x01|* ***| + 0x02|* * | + 0x04|* * | + 0x08|***************** | + '-------------------' +*/ + +const int Battery_0_Height = 12; +const int Battery_0_Width = 19; +const uint8_t Battery_0[] = {0xFF, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x0F, 0x08, 0xF8, 0x0F, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0F, 0x01, 0x01}; + +/* + Ethernet [19, 12] + + 1 + 1234567890123456789 + .-------------------. + 0x01| | + 0x02| ***** | + 0x04| * * | + 0x08| ***** | + 0x10| * | + 0x20| ***************** | + 0x40| * * | + 0x80| ***** ***** | + 0x01| * * * * | + 0x02| ***** ***** | + 0x04| | + 0x08| | + '-------------------' +*/ + +const int Ethernet_Icon_Height = 12; +const int Ethernet_Icon_Width = 19; +const uint8_t Ethernet_Icon[] = {0x00, 0x20, 0xA0, 0xA0, 0xE0, 0xA0, 0xA0, 0x2E, 0x2A, 0x3A, 0x2A, 0x2E, 0xA0, + 0xA0, 0xE0, 0xA0, 0xA0, 0x20, 0x00, 0x00, 0x00, 0x03, 0x02, 0x02, 0x02, 0x03, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x02, 0x02, 0x02, 0x03, 0x00, 0x00}; + +/* + Logging_3 [9, 12] + + 123456789 + .---------. + 0x01|******* | + 0x02|* ** | + 0x04|* **| + 0x08|* *| + 0x10|* ***** *| + 0x20|* *| + 0x40|* ***** *| + 0x80|* *| + 0x01|* ***** *| + 0x02|* *| + 0x04|* *| + 0x08|*********| + '---------' +*/ + +const int Logging_3_Height = 12; +const int Logging_3_Width = 9; +const uint8_t Logging_3[] = {0xFF, 0x01, 0x51, 0x51, 0x51, 0x51, 0x53, 0x06, 0xFC, + 0x0F, 0x08, 0x09, 0x09, 0x09, 0x09, 0x09, 0x08, 0x0F}; + +/* + Logging_2 [9, 12] + + 123456789 + .---------. + 0x01|******* | + 0x02|* ** | + 0x04|* **| + 0x08|* *| + 0x10|* *| + 0x20|* *| + 0x40|* ***** *| + 0x80|* *| + 0x01|* ***** *| + 0x02|* *| + 0x04|* *| + 0x08|*********| + '---------' +*/ + +const int Logging_2_Height = 12; +const int Logging_2_Width = 9; +const uint8_t Logging_2[] = {0xFF, 0x01, 0x41, 0x41, 0x41, 0x41, 0x43, 0x06, 0xFC, + 0x0F, 0x08, 0x09, 0x09, 0x09, 0x09, 0x09, 0x08, 0x0F}; + +/* + Logging_1 [9, 12] + + 123456789 + .---------. + 0x01|******* | + 0x02|* ** | + 0x04|* **| + 0x08|* *| + 0x10|* *| + 0x20|* *| + 0x40|* *| + 0x80|* *| + 0x01|* ***** *| + 0x02|* *| + 0x04|* *| + 0x08|*********| + '---------' +*/ + +const int Logging_1_Height = 12; +const int Logging_1_Width = 9; +const uint8_t Logging_1[] = {0xFF, 0x01, 0x01, 0x01, 0x01, 0x01, 0x03, 0x06, 0xFC, + 0x0F, 0x08, 0x09, 0x09, 0x09, 0x09, 0x09, 0x08, 0x0F}; + +/* + Logging_0 [9, 12] + + 123456789 + .---------. + 0x01|******* | + 0x02|* ** | + 0x04|* **| + 0x08|* *| + 0x10|* *| + 0x20|* *| + 0x40|* *| + 0x80|* *| + 0x01|* *| + 0x02|* *| + 0x04|* *| + 0x08|*********| + '---------' +*/ + +const int Logging_0_Height = 12; +const int Logging_0_Width = 9; +const uint8_t Logging_0[] = {0xFF, 0x01, 0x01, 0x01, 0x01, 0x01, 0x03, 0x06, 0xFC, + 0x0F, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0F}; + +/* + Logging_PPP_3 [9, 12] + + 123456789 + .---------. + 0x01|******* | + 0x02|* ** | + 0x04|* **| + 0x08|* **** *| + 0x10|* * * *| + 0x20|* * * *| + 0x40|* **** *| + 0x80|* * *| + 0x01|* * *| + 0x02|* * *| + 0x04|* *| + 0x08|*********| + '---------' +*/ + +const uint8_t Logging_PPP_3[] = {0xFF, 0x01, 0xF9, 0x49, 0x49, 0x49, 0x33, 0x06, 0xFC, + 0x0F, 0x08, 0x0B, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0F}; -uint8_t Logging_1 [] = { -0xFF, 0x01, 0x01, 0x01, 0x01, 0x01, 0x03, 0x06, 0xFC, 0x0F, 0x08, 0x09, 0x09, 0x09, 0x09, 0x09, -0x08, 0x0F, +/* + Logging_PPP_2 [9, 12] + + 123456789 + .---------. + 0x01|******* | + 0x02|* ** | + 0x04|* **| + 0x08|* * *| + 0x10|* * *| + 0x20|* * *| + 0x40|* **** *| + 0x80|* * *| + 0x01|* * *| + 0x02|* * *| + 0x04|* *| + 0x08|*********| + '---------' +*/ + +const uint8_t Logging_PPP_2[] = {0xFF, 0x01, 0xF9, 0x41, 0x41, 0x41, 0x03, 0x06, 0xFC, + 0x0F, 0x08, 0x0B, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0F}; + +/* + Logging_PPP_1 [9, 12] + + 123456789 + .---------. + 0x01|******* | + 0x02|* ** | + 0x04|* **| + 0x08|* *| + 0x10|* *| + 0x20|* *| + 0x40|* * *| + 0x80|* * *| + 0x01|* * *| + 0x02|* * *| + 0x04|* *| + 0x08|*********| + '---------' +*/ + +const uint8_t Logging_PPP_1[] = {0xFF, 0x01, 0xC1, 0x01, 0x01, 0x01, 0x03, 0x06, 0xFC, + 0x0F, 0x08, 0x0B, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0F}; + +/* + Logging_Custom_3 [9, 12] + + 123456789 + .---------. + 0x01|******* | + 0x02|* ** | + 0x04|* **| + 0x08|* *** *| + 0x10|* * * *| + 0x20|* * *| + 0x40|* * *| + 0x80|* * *| + 0x01|* * * *| + 0x02|* *** *| + 0x04|* *| + 0x08|*********| + '---------' +*/ + +const uint8_t Logging_Custom_3[] = {0xFF, 0x01, 0xF1, 0x09, 0x09, 0x09, 0x13, 0x06, 0xFC, + 0x0F, 0x08, 0x09, 0x0A, 0x0A, 0x0A, 0x09, 0x08, 0x0F}; + +/* + Logging_Custom_2 [9, 12] + + 123456789 + .---------. + 0x01|******* | + 0x02|* ** | + 0x04|* **| + 0x08|* *| + 0x10|* * *| + 0x20|* * *| + 0x40|* * *| + 0x80|* * *| + 0x01|* * * *| + 0x02|* *** *| + 0x04|* *| + 0x08|*********| + '---------' +*/ + +const uint8_t Logging_Custom_2[] = {0xFF, 0x01, 0xF1, 0x01, 0x01, 0x01, 0x03, 0x06, 0xFC, + 0x0F, 0x08, 0x09, 0x0A, 0x0A, 0x0A, 0x09, 0x08, 0x0F}; + +/* + Logging_Custom_1 [9, 12] + + 123456789 + .---------. + 0x01|******* | + 0x02|* ** | + 0x04|* **| + 0x08|* *| + 0x10|* *| + 0x20|* *| + 0x40|* *| + 0x80|* *| + 0x01|* * * *| + 0x02|* *** *| + 0x04|* *| + 0x08|*********| + '---------' +*/ + +const uint8_t Logging_Custom_1[] = {0xFF, 0x01, 0x01, 0x01, 0x01, 0x01, 0x03, 0x06, 0xFC, + 0x0F, 0x08, 0x09, 0x0A, 0x0A, 0x0A, 0x09, 0x08, 0x0F}; + +/* + Logging_NTP_3 [9, 12] + + 123456789 + .---------. + 0x01|******* | + 0x02|* ** | + 0x04|* **| + 0x08|* *| + 0x10|* * * *| + 0x20|* ** * *| + 0x40|* * * * *| + 0x80|* * * * *| + 0x01|* * ** *| + 0x02|* * * *| + 0x04|* *| + 0x08|*********| + '---------' +*/ + +const uint8_t Logging_NTP_3[] = {0xFF, 0x01, 0xF1, 0x21, 0xC1, 0x01, 0xF3, 0x06, 0xFC, + 0x0F, 0x08, 0x0B, 0x08, 0x08, 0x09, 0x0B, 0x08, 0x0F}; + +/* + Logging_NTP_2 [9, 12] + + 123456789 + .---------. + 0x01|******* | + 0x02|* ** | + 0x04|* **| + 0x08|* *| + 0x10|* * *| + 0x20|* ** *| + 0x40|* * * *| + 0x80|* * * *| + 0x01|* * * *| + 0x02|* * *| + 0x04|* *| + 0x08|*********| + '---------' +*/ + +const uint8_t Logging_NTP_2[] = {0xFF, 0x01, 0xF1, 0x21, 0xC1, 0x01, 0x03, 0x06, 0xFC, + 0x0F, 0x08, 0x0B, 0x08, 0x08, 0x09, 0x08, 0x08, 0x0F}; + +/* + Logging_NTP_1 [9, 12] + + 123456789 + .---------. + 0x01|******* | + 0x02|* ** | + 0x04|* **| + 0x08|* *| + 0x10|* * *| + 0x20|* * *| + 0x40|* * *| + 0x80|* * *| + 0x01|* * *| + 0x02|* * *| + 0x04|* *| + 0x08|*********| + '---------' +*/ + +const uint8_t Logging_NTP_1[] = {0xFF, 0x01, 0xF1, 0x01, 0x01, 0x01, 0x03, 0x06, 0xFC, + 0x0F, 0x08, 0x0B, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0F}; + +/* + DynamicModel_1_Portable [15, 12] + + 1 + 123456789012345 + .---------------. + 0x01| ** | + 0x02| ** | + 0x04| ****** | + 0x08| * * | + 0x10| * * **** * * | + 0x20| * * **** * * | + 0x40| * * * * | + 0x80| * * * * | + 0x01| * * * * | + 0x02| * * * * | + 0x04| * * | + 0x08| ****** | + '---------------' +*/ + +const int DynamicModel_Height = 12; +const int DynamicModel_Width = 15; +const uint8_t DynamicModel_1_Portable[] = {0x00, 0xF0, 0x00, 0xF8, 0x04, 0x34, 0x34, 0x37, 0x37, 0x04, + 0xF8, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x07, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x07, 0x00, 0x03, 0x00, 0x00}; + +/* + DynamicModel_2_Stationary [15, 12] + + 1 + 123456789012345 + .---------------. + 0x01| | + 0x02| ******* | + 0x04| ***** | + 0x08| *** | + 0x10| * | + 0x20| *** | + 0x40| ***** | + 0x80| ** * ** | + 0x01| ** * ** | + 0x02| ** * ** | + 0x04| ** * ** | + 0x08| | + '---------------' +*/ + +const uint8_t DynamicModel_2_Stationary[] = {0x00, 0x00, 0x00, 0x00, 0x82, 0xC6, 0x6E, 0xFE, 0x6E, 0xC6, + 0x82, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x06, 0x03, 0x01, + 0x00, 0x00, 0x07, 0x00, 0x00, 0x01, 0x03, 0x06, 0x04, 0x00}; + +/* + DynamicModel_3_Pedestrian [15, 12] + + 1 + 123456789012345 + .---------------. + 0x01| *** | + 0x02| * * | + 0x04| * * | + 0x08| * | + 0x10| ***** | + 0x20| ** * ** | + 0x40| * * | + 0x80| *** | + 0x01| ** ** | + 0x02| ** * | + 0x04| ** ** | + 0x08| | + '---------------' +*/ + +const uint8_t DynamicModel_3_Pedestrian[] = {0x00, 0x00, 0x00, 0x00, 0x20, 0x32, 0x95, 0xF9, 0x95, 0x32, + 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x06, + 0x03, 0x01, 0x00, 0x01, 0x07, 0x04, 0x00, 0x00, 0x00, 0x00}; + +/* + DynamicModel_4_Automotive [15, 12] + + 1 + 123456789012345 + .---------------. + 0x01| | + 0x02| | + 0x04| ********* | + 0x08|* * | + 0x10|* ****| + 0x20|* *| + 0x40|* ** ** *| + 0x80| * ******* * | + 0x01| * * * * | + 0x02| ** ** | + 0x04| | + 0x08| | + '---------------' +*/ + +const uint8_t DynamicModel_4_Automotive[] = {0x78, 0x84, 0x44, 0x44, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, + 0x88, 0x50, 0x50, 0x90, 0x70, 0x00, 0x01, 0x02, 0x02, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x02, 0x01, 0x00}; + +/* + DynamicModel_5_Sea [15, 12] + + 1 + 123456789012345 + .---------------. + 0x01| | + 0x02| * | + 0x04| *** | + 0x08| * * | + 0x10| * * | + 0x20| ************* | + 0x40| ** ** | + 0x80| * ** * | + 0x01| * * | + 0x02| ** ** | + 0x04| ********* | + 0x08| | + '---------------' +*/ + +const uint8_t DynamicModel_5_Sea[] = {0x00, 0x60, 0xE0, 0x3C, 0x26, 0x3C, 0x20, 0x20, 0x20, 0xA0, + 0xA0, 0x20, 0xE0, 0x60, 0x00, 0x00, 0x00, 0x03, 0x06, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x06, 0x03, 0x00, 0x00}; + +/* + DynamicModel_6_Airborne1g [15, 12] + + 1 + 123456789012345 + .---------------. + 0x01| | + 0x02| * | + 0x04| ** | + 0x08| *********** | + 0x10| * * ** | + 0x20| * * * ** | + 0x40| * * * | + 0x80| * * *** ** | + 0x01| ****** ***** | + 0x02| * * | + 0x04| * * | + 0x08| * | + '---------------' +*/ + +const uint8_t DynamicModel_6_Airborne1g[] = {0x00, 0xFE, 0x0C, 0xF8, 0x08, 0x08, 0x88, 0x88, 0x88, 0x28, + 0x08, 0x18, 0xB0, 0xE0, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x07, 0x08, 0x07, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00}; + +/* + DynamicModel_7_Airborne2g [15, 12] + + 1 + 123456789012345 + .---------------. + 0x01| | + 0x02| * | + 0x04| ** | + 0x08| *********** | + 0x10| * * ** | + 0x20| * * * * ** | + 0x40| * * * | + 0x80| * * *** ** | + 0x01| ****** ***** | + 0x02| * * | + 0x04| * * | + 0x08| * | + '---------------' +*/ + +const uint8_t DynamicModel_7_Airborne2g[] = {0x00, 0xFE, 0x0C, 0xF8, 0x08, 0x08, 0x88, 0xA8, 0x88, 0x28, + 0x08, 0x18, 0xB0, 0xE0, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x07, 0x08, 0x07, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00}; + +/* + DynamicModel_8_Airborne4g [15, 12] + + 1 + 123456789012345 + .---------------. + 0x01| | + 0x02| * | + 0x04| ** | + 0x08| *********** | + 0x10| * * ** | + 0x20| * * * * * ** | + 0x40| * * * | + 0x80| * * *** ** | + 0x01| ****** ***** | + 0x02| * * | + 0x04| * * | + 0x08| * | + '---------------' +*/ + +const uint8_t DynamicModel_8_Airborne4g[] = {0x00, 0xFE, 0x0C, 0xF8, 0x08, 0x28, 0x88, 0xA8, 0x88, 0x28, + 0x08, 0x18, 0xB0, 0xE0, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x07, 0x08, 0x07, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00}; + +/* + DynamicModel_9_Wrist [15, 12] + + 1 + 123456789012345 + .---------------. + 0x01| *** | + 0x02| *** | + 0x04| *** | + 0x08| ***** | + 0x10| * * | + 0x20| * * | + 0x40| * *** * | + 0x80| * * | + 0x01| * * | + 0x02| ***** | + 0x04| *** | + 0x08| *** | + '---------------' +*/ + +const uint8_t DynamicModel_9_Wrist[] = {0x00, 0x00, 0x00, 0xE0, 0x10, 0x08, 0x4F, 0x4F, 0x4F, 0x08, + 0x10, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x02, 0x1E, 0x1E, 0x1E, 0x02, 0x01, 0x00, 0x00, 0x00, 0x00}; + +/* + DynamicModel_10_Bike [15, 12] + + 1 + 123456789012345 + .---------------. + 0x01| | + 0x02| | + 0x04| ** | + 0x08| *** | + 0x10| *** * | + 0x20| * * | + 0x40| ** *** ** | + 0x80| * ******* * | + 0x01| * * * * | + 0x02| ** ** | + 0x04| | + 0x08| | + '---------------' +*/ + +const uint8_t DynamicModel_10_Bike[] = {0x00, 0x80, 0x40, 0x50, 0x90, 0xB0, 0xC0, 0xC0, 0xC0, 0xA0, + 0x98, 0x4C, 0x4C, 0x80, 0x00, 0x00, 0x01, 0x02, 0x02, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x02, 0x01, 0x00}; + +const uint8_t DynamicModel_11_Mower[] = { + 0x01, 0x03, 0x86, 0x8C, 0x98, 0xB0, 0xE0, 0xC0, 0xF4, 0xDC, 0xDC, 0xF4, 0xC0, 0x80, 0x00, + 0x00, 0x00, 0x03, 0x0E, 0x0A, 0x0E, 0x02, 0x02, 0x02, 0x02, 0x0E, 0x0A, 0x0E, 0x03, 0x00, }; -int Logging_1_Height = 12; -int Logging_1_Width = 9; -uint8_t Logging_0 [] = { -0xFF, 0x01, 0x01, 0x01, 0x01, 0x01, 0x03, 0x06, 0xFC, 0x0F, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, -0x08, 0x0F, +const uint8_t DynamicModel_12_EScooter[] = { + 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x02, 0x01, 0xFF, 0xFF, 0x01, 0x02, 0x00, 0x00, + 0x00, 0x03, 0x0F, 0x0B, 0x0F, 0x03, 0x03, 0x03, 0x03, 0x0F, 0x0B, 0x0F, 0x03, 0x00, 0x00, }; -int Logging_0_Height = 12; -int Logging_0_Width = 9; + +/* + DownloadArrow [8, 9] + + 12345678 + .--------. + 0x01| ** | + 0x02| ** | + 0x04| ** | + 0x08| ** | + 0x10| ** | + 0x20|** ** **| + 0x40| ****** | + 0x80| **** | + 0x01| ** | + '--------' +*/ + +const int DownloadArrow_Height = 9; +const int DownloadArrow_Width = 8; +const uint8_t DownloadArrow[] = {0x20, 0x60, 0xC0, 0xFF, 0xFF, 0xC0, 0x60, 0x20, + 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00}; + +/* + UploadArrow [8, 9] + + 12345678 + .--------. + 0x01| ** | + 0x02| **** | + 0x04| ****** | + 0x08|** ** **| + 0x10| ** | + 0x20| ** | + 0x40| ** | + 0x80| ** | + 0x01| ** | + '--------' +*/ + +const int UploadArrow_Height = 9; +const int UploadArrow_Width = 8; +const uint8_t UploadArrow[] = {0x08, 0x0C, 0x06, 0xFF, 0xFF, 0x06, 0x0C, 0x08, + 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00}; + +/* + logoSparkFun [64, 48] + + 1 2 3 4 5 6 + 1234567890123456789012345678901234567890123456789012345678901234 + .----------------------------------------------------------------. + 0x01| ********** | + 0x02| ************* | + 0x04| ************** | + 0x08| *********** | + 0x10| ********** | + 0x20| *********** | + 0x40| *********** | + 0x80| *********** ** | + 0x01| ************ *** | + 0x02| ************* **** | + 0x04| ********************* | + 0x08| ********************* | + 0x10| ******************** | + 0x20| ********************* | + 0x40| ******************* | + 0x80| ******* ******************* | + 0x01| ******* ****************** | + 0x02| ******* ***************** | + 0x04| ******** ****************** | + 0x08| ******** ****************** | + 0x10| ********* ******************* | + 0x20| ********************************* | + 0x40| ********************************* | + 0x80| ********************************* | + 0x01| ********************************* | + 0x02| ******************************** | + 0x04| ******************************** | + 0x08| ******************************* | + 0x10| ******************************* | + 0x20| ****************************** | + 0x40| ***************************** | + 0x80| **************************** | + 0x01| *************************** | + 0x02| ************************** | + 0x04| ************************ | + 0x08| ********************* | + 0x10| ************* | + 0x20| *********** | + 0x40| ********** | + 0x80| ********* | + 0x01| ******** | + 0x02| ******* | + 0x04| ****** | + 0x08| ***** | + 0x10| **** | + 0x20| *** | + 0x40| ** | + 0x80| * | + '----------------------------------------------------------------' +*/ + +const int logoSparkFun_Height = 48; +const int logoSparkFun_Width = 64; +const uint8_t logoSparkFun[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xF8, 0xFC, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0x07, 0x07, + 0x06, 0x06, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x81, 0x07, 0x0F, 0x3F, 0x3F, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFE, 0xFC, 0xFC, 0xFC, 0xFE, 0xFF, 0xFF, 0xFF, 0xFC, 0xF8, 0xE0, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF1, + 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xF0, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x3F, 0x1F, 0x07, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x3F, 0x1F, 0x1F, 0x0F, + 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x07, 0x07, 0x07, 0x03, 0x03, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x7F, 0x3F, 0x1F, 0x0F, 0x07, 0x03, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00}; + +/* + ESPNOW_Symbol_3 [8, 13] + + 12345678 + .--------. + 0x01| * | + 0x02| * | + 0x04| * * | + 0x08| * *| + 0x10| * * *| + 0x20|* * * *| + 0x40|** * * *| + 0x80|* * * *| + 0x01| * * *| + 0x02| * *| + 0x04| * * | + 0x08| * | + 0x10| * | + '--------' +*/ + +const int ESPNOW_Symbol_Height = 13; +const int ESPNOW_Symbol_Width = 8; +const uint8_t ESPNOW_Symbol_3[] = {0xE0, 0x40, 0x10, 0xE4, 0x09, 0xF2, 0x04, 0xF8, + 0x00, 0x00, 0x01, 0x04, 0x12, 0x09, 0x04, 0x03}; + +/* + ESPNOW_Symbol_2 [8, 13] + + 12345678 + .--------. + 0x01| | + 0x02| | + 0x04| * | + 0x08| * | + 0x10| * * | + 0x20|* * * | + 0x40|** * * | + 0x80|* * * | + 0x01| * * | + 0x02| * | + 0x04| * | + 0x08| | + 0x10| | + '--------' +*/ + +const uint8_t ESPNOW_Symbol_2[] = {0xE0, 0x40, 0x10, 0xE4, 0x08, 0xF0, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x04, 0x02, 0x01, 0x00, 0x00}; + +/* + ESPNOW_Symbol_1 [8, 13] + + 12345678 + .--------. + 0x01| | + 0x02| | + 0x04| | + 0x08| | + 0x10| * | + 0x20|* * | + 0x40|** * | + 0x80|* * | + 0x01| * | + 0x02| | + 0x04| | + 0x08| | + 0x10| | + '--------' +*/ + +const uint8_t ESPNOW_Symbol_1[] = {0xE0, 0x40, 0x10, 0xE0, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00}; + +/* + ESPNOW_Symbol_0 [8, 13] + + 12345678 + .--------. + 0x01| | + 0x02| | + 0x04| | + 0x08| | + 0x10| | + 0x20|* | + 0x40|** | + 0x80|* | + 0x01| | + 0x02| | + 0x04| | + 0x08| | + 0x10| | + '--------' +*/ + +const uint8_t ESPNOW_Symbol_0[] = {0xE0, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +/* + milliseconds [8, 12] + + + 12345678 + .--------. + 0x01|** * | + 0x02|* * * | + 0x04|* * * | + 0x08|* * * | + 0x10|* * * | + 0x20| | + 0x40| | + 0x80| **** | + 0x01| * | + 0x02| *** | + 0x04| * | + 0x08| **** | + '--------' +*/ + +const int Millis_Icon_Height = 12; +const int Millis_Icon_Width = 8; +const uint8_t Millis_Icon[] = {0x1F, 0x01, 0x1E, 0x81, 0x9E, 0x80, 0x80, 0x00, + 0x00, 0x00, 0x09, 0x0A, 0x0A, 0x0A, 0x04, 0x00}; + +/* + microseconds [8, 12] + + + 12345678 + .--------. + 0x01|* * | + 0x02|* * | + 0x04|** ** | + 0x08|* ** * | + 0x10|* | + 0x20|* | + 0x40| | + 0x80| **** | + 0x01| * | + 0x02| *** | + 0x04| * | + 0x08| **** | + '--------' +*/ + +const uint8_t Micros_Icon[] = {0x3F, 0x04, 0x08, 0x88, 0x84, 0x8F, 0x80, 0x00, + 0x00, 0x00, 0x09, 0x0A, 0x0A, 0x0A, 0x04, 0x00}; + +/* + nanoseconds [8, 12] + + + 12345678 + .--------. + 0x01|**** | + 0x02|* * | + 0x04|* * | + 0x08|* * | + 0x10|* * | + 0x20| | + 0x40| | + 0x80| **** | + 0x01| * | + 0x02| *** | + 0x04| * | + 0x08| **** | + '--------' +*/ + +const uint8_t Nanos_Icon[] = {0x1F, 0x01, 0x01, 0x81, 0x9E, 0x80, 0x80, 0x00, + 0x00, 0x00, 0x09, 0x0A, 0x0A, 0x0A, 0x04, 0x00}; diff --git a/Firmware/RTK_Surveyor/menuBase.ino b/Firmware/RTK_Surveyor/menuBase.ino index be8b739b4..288a6d6af 100644 --- a/Firmware/RTK_Surveyor/menuBase.ino +++ b/Firmware/RTK_Surveyor/menuBase.ino @@ -1,218 +1,993 @@ -//Configure the survey in settings (time and 3D dev max) -//Set the ECEF coordinates for a known location +/*------------------------------------------------------------------------------ +menuBase.ino +------------------------------------------------------------------------------*/ + +//---------------------------------------- +// Constants +//---------------------------------------- + +static const float maxObservationPositionAccuracy = 10.0; +static const float maxSurveyInStartingAccuracy = 10.0; + +//---------------------------------------- +// Menus +//---------------------------------------- + +// Configure the survey in settings (time and 3D dev max) +// Set the ECEF coordinates for a known location void menuBase() { - int menuTimeoutExtended = 30; //Increase time needed for complex data entry (mount point ID, ECEF coords, etc). - - while (1) - { - Serial.println(); - Serial.println(F("Menu: Base Menu")); - - Serial.print(F("1) Toggle Base Mode: ")); - if (settings.fixedBase == true) Serial.println(F("Fixed/Static Position")); - else Serial.println(F("Use Survey-In")); - - if (settings.fixedBase == true) - { - Serial.print(F("2) Toggle Coordinate System: ")); - if (settings.fixedBaseCoordinateType == COORD_TYPE_ECEF) Serial.println(F("ECEF")); - else Serial.println(F("Geographic")); - - if (settings.fixedBaseCoordinateType == COORD_TYPE_ECEF) - { - Serial.print(F("3) Set ECEF X/Y/Z coordinates: ")); - Serial.print(settings.fixedEcefX, 4); - Serial.print(F("m, ")); - Serial.print(settings.fixedEcefY, 4); - Serial.print(F("m, ")); - Serial.print(settings.fixedEcefZ, 4); - Serial.println(F("m")); - } - else if (settings.fixedBaseCoordinateType == COORD_TYPE_GEOGRAPHIC) - { - Serial.print(F("3) Set Lat/Long/Altitude coordinates: ")); - Serial.print(settings.fixedLat, 9); - Serial.print(F("°, ")); - Serial.print(settings.fixedLong, 9); - Serial.print(F("°, ")); - Serial.print(settings.fixedAltitude, 4); - Serial.println(F("m")); - } - } - else + int serverIndex = 0; + int value; + + while (1) { - Serial.print(F("2) Set minimum observation time: ")); - Serial.print(settings.observationSeconds); - Serial.println(F(" seconds")); + systemPrintln(); + systemPrintln("Menu: Base"); - Serial.print(F("3) Set required Mean 3D Standard Deviation: ")); - Serial.print(settings.observationPositionAccuracy, 3); - Serial.println(F(" meters")); - } + // Print the combined HAE APC if we are in the given mode + if (settings.fixedBase == true && settings.fixedBaseCoordinateType == COORD_TYPE_GEODETIC) + { + systemPrintf( + "Total Height Above Ellipsoid - Antenna Phase Center (HAE APC): %0.3fmm\r\n", + (((settings.fixedAltitude * 1000) + settings.antennaHeight + settings.antennaReferencePoint) / 1000)); + } - Serial.print(F("4) Toggle NTRIP Server: ")); - if (settings.enableNtripServer == true) Serial.println(F("Enabled")); - else Serial.println(F("Disabled")); + systemPrint("1) Toggle Base Mode: "); + if (settings.fixedBase == true) + systemPrintln("Fixed/Static Position"); + else + systemPrintln("Use Survey-In"); - if (settings.enableNtripServer == true) - { - Serial.print(F("5) Set WiFi SSID: ")); - Serial.println(settings.wifiSSID); + if (settings.fixedBase == true) + { + systemPrint("2) Toggle Coordinate System: "); + if (settings.fixedBaseCoordinateType == COORD_TYPE_ECEF) + systemPrintln("ECEF"); + else + systemPrintln("Geodetic"); - Serial.print(F("6) Set WiFi PW: ")); - Serial.println(settings.wifiPW); + if (settings.fixedBaseCoordinateType == COORD_TYPE_ECEF) + { + systemPrint("3) Set ECEF X/Y/Z coordinates: "); + systemPrint(settings.fixedEcefX, 4); + systemPrint("m, "); + systemPrint(settings.fixedEcefY, 4); + systemPrint("m, "); + systemPrint(settings.fixedEcefZ, 4); + systemPrintln("m"); + } + else if (settings.fixedBaseCoordinateType == COORD_TYPE_GEODETIC) + { + systemPrint("3) Set Lat/Long/Altitude coordinates: "); - Serial.print(F("7) Set Caster Address: ")); - Serial.println(settings.casterHost); + char coordinatePrintable[50]; + coordinateConvertInput(settings.fixedLat, settings.coordinateInputType, coordinatePrintable, + sizeof(coordinatePrintable)); + systemPrint(coordinatePrintable); - Serial.print(F("8) Set Caster Port: ")); - Serial.println(settings.casterPort); + systemPrint(", "); - Serial.print(F("9) Set Mountpoint: ")); - Serial.println(settings.mountPoint); + coordinateConvertInput(settings.fixedLong, settings.coordinateInputType, coordinatePrintable, + sizeof(coordinatePrintable)); + systemPrint(coordinatePrintable); - Serial.print(F("10) Set Mountpoint PW: ")); - Serial.println(settings.mountPointPW); - } + systemPrint(", "); + systemPrint(settings.fixedAltitude, 4); + systemPrint("m"); + systemPrintln(); - Serial.println(F("x) Exit")); + systemPrint("4) Set coordinate display format: "); + systemPrintln(coordinatePrintableInputType(settings.coordinateInputType)); - int incoming = getNumber(menuTimeoutExtended); //Timeout after x seconds + systemPrintf("5) Set Antenna Height: %dmm\r\n", settings.antennaHeight); - if (incoming == 1) - { - settings.fixedBase ^= 1; + systemPrintf("6) Set Antenna Reference Point: %0.1fmm\r\n", settings.antennaReferencePoint); + } + } + else + { + systemPrint("2) Set minimum observation time: "); + systemPrint(settings.observationSeconds); + systemPrintln(" seconds"); + + systemPrint("3) Set required Mean 3D Standard Deviation: "); + systemPrint(settings.observationPositionAccuracy, 3); + systemPrintln(" meters"); + + systemPrint("4) Set required initial positional accuracy before Survey-In: "); + systemPrint(settings.surveyInStartingAccuracy, 3); + systemPrintln(" meters"); + } + + systemPrint("7) Toggle NTRIP Server: "); + if (settings.enableNtripServer == true) + systemPrintln("Enabled"); + else + systemPrintln("Disabled"); + + if (settings.enableNtripServer == true) + { + systemPrintf("8) Select NTRIP server index: %d\r\n", serverIndex + 1); + + systemPrintf("9) Set Caster Host / Address %d: ", serverIndex + 1); + systemPrintln(&settings.ntripServer_CasterHost[serverIndex][0]); + + systemPrintf("10) Set Caster Port %d: ", serverIndex + 1); + systemPrintln(settings.ntripServer_CasterPort[serverIndex]); + + systemPrintf("11) Set Caster User %d: ", serverIndex + 1); + systemPrintln(&settings.ntripServer_CasterUser[serverIndex][0]); + + systemPrintf("12) Set Caster User PW %d: ", serverIndex + 1); + systemPrintln(settings.ntripServer_CasterUserPW[serverIndex]); + + systemPrintf("13) Set Mountpoint %d: ", serverIndex + 1); + systemPrintln(&settings.ntripServer_MountPoint[serverIndex][0]); + + systemPrintf("14) Set Mountpoint PW %d: ", serverIndex + 1); + systemPrintln(&settings.ntripServer_MountPointPW[serverIndex][0]); + + systemPrint("15) Set RTCM Message Rates\r\n"); + + if (settings.fixedBase == false) // Survey-in + { + systemPrint("16) Select survey-in radio: "); + systemPrintf("%s\r\n", settings.ntripServer_StartAtSurveyIn ? "WiFi" : "Bluetooth"); + } + } + else + { + systemPrintln("8) Set RTCM Message Rates"); + + if (settings.fixedBase == false) // Survey-in + { + systemPrint("9) Select survey-in radio: "); + systemPrintf("%s\r\n", settings.ntripServer_StartAtSurveyIn ? "WiFi" : "Bluetooth"); + } + } + + systemPrintln("x) Exit"); + + int incoming = getNumber(); // Returns EXIT, TIMEOUT, or long + + if (incoming == 1) + { + settings.fixedBase ^= 1; + restartBase = true; + } + else if (settings.fixedBase == true && incoming == 2) + { + settings.fixedBaseCoordinateType ^= 1; + restartBase = true; + } + + else if (settings.fixedBase == true && incoming == 3) + { + if (settings.fixedBaseCoordinateType == COORD_TYPE_ECEF) + { + systemPrintln("Enter the fixed ECEF coordinates that will be used in Base mode:"); + + systemPrint("ECEF X in meters (ex: -1280182.9200): "); + double fixedEcefX = getDouble(); + + // Progress with additional prompts only if the user enters valid data + if (fixedEcefX != INPUT_RESPONSE_GETNUMBER_TIMEOUT && fixedEcefX != INPUT_RESPONSE_GETNUMBER_EXIT) + { + settings.fixedEcefX = fixedEcefX; + + systemPrint("\nECEF Y in meters (ex: -4716808.5807): "); + double fixedEcefY = getDouble(); + if (fixedEcefY != INPUT_RESPONSE_GETNUMBER_TIMEOUT && fixedEcefY != INPUT_RESPONSE_GETNUMBER_EXIT) + { + settings.fixedEcefY = fixedEcefY; + + systemPrint("\nECEF Z in meters (ex: 4086669.6393): "); + double fixedEcefZ = getDouble(); + if (fixedEcefZ != INPUT_RESPONSE_GETNUMBER_TIMEOUT && + fixedEcefZ != INPUT_RESPONSE_GETNUMBER_EXIT) + settings.fixedEcefZ = fixedEcefZ; + } + } + } + else if (settings.fixedBaseCoordinateType == COORD_TYPE_GEODETIC) + { + // Progress with additional prompts only if the user enters valid data + char userEntry[50]; + + systemPrintln("Enter the fixed Lat/Long/Altitude coordinates that will be used in Base mode:"); + + systemPrint("Latitude in degrees (ex: 40.090335429, 40 05.4201257, 40-05.4201257, 4005.4201257, 40 05 " + "25.207544, etc): "); + if (getString(userEntry, sizeof(userEntry)) == INPUT_RESPONSE_VALID) + { + double fixedLat = 0.0; + + // Identify which type of method they used + CoordinateInputType latCoordinateInputType = coordinateIdentifyInputType(userEntry, &fixedLat); + if (latCoordinateInputType != COORDINATE_INPUT_TYPE_INVALID_UNKNOWN) + { + // Progress with additional prompts only if the user enters valid data + systemPrint("\r\nLongitude in degrees (ex: -105.184774720, -105 11.0864832, -105-11.0864832, " + "-105 11 05.188992, etc): "); + if (getString(userEntry, sizeof(userEntry)) == INPUT_RESPONSE_VALID) + { + double fixedLong = 0.0; + + // Identify which type of method they used + CoordinateInputType longCoordinateInputType = coordinateIdentifyInputType(userEntry, &fixedLong); + if (longCoordinateInputType != COORDINATE_INPUT_TYPE_INVALID_UNKNOWN) + { + if (latCoordinateInputType == longCoordinateInputType) + { + settings.fixedLat = fixedLat; + settings.fixedLong = fixedLong; + settings.coordinateInputType = latCoordinateInputType; + + systemPrint("\r\nAltitude in meters (ex: 1560.2284): "); + double fixedAltitude = getDouble(); + if (fixedAltitude != INPUT_RESPONSE_GETNUMBER_TIMEOUT && + fixedAltitude != INPUT_RESPONSE_GETNUMBER_EXIT) + settings.fixedAltitude = fixedAltitude; + } + else + { + systemPrintln("\r\nCoordinate types must match!"); + } + } // idInput on fixedLong + } // getString for fixedLong + } // idInput on fixedLat + } // getString for fixedLat + } // COORD_TYPE_GEODETIC + } // Fixed base and '3' + + else if (settings.fixedBase == true && settings.fixedBaseCoordinateType == COORD_TYPE_GEODETIC && incoming == 4) + { + menuBaseCoordinateType(); // Set coordinate display format + } + else if (settings.fixedBase == true && settings.fixedBaseCoordinateType == COORD_TYPE_GEODETIC && incoming == 5) + { + systemPrint("Enter the antenna height (a.k.a. pole length) in millimeters (-15000 to 15000mm): "); + int antennaHeight = getNumber(); // Returns EXIT, TIMEOUT, or long + if ((antennaHeight != INPUT_RESPONSE_GETNUMBER_EXIT) && (antennaHeight != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + { + if (antennaHeight < -15000 || antennaHeight > 15000) // Arbitrary 15m max + systemPrintln("Error: Antenna Height out of range"); + else + settings.antennaHeight = antennaHeight; // Recorded to NVM and file at main menu exit + } + } + else if (settings.fixedBase == true && settings.fixedBaseCoordinateType == COORD_TYPE_GEODETIC && incoming == 6) + { + systemPrint("Enter the antenna reference point (a.k.a. ARP) in millimeters (-200.0 to 200.0mm). Common " + "antennas Facet=71.8mm Facet L-Band=69.0mm TOP106=52.9: "); + float antennaReferencePoint = getDouble(); + if (antennaReferencePoint < -200.0 || antennaReferencePoint > 200.0) // Arbitrary 200mm max + systemPrintln("Error: Antenna Reference Point out of range"); + else + settings.antennaReferencePoint = antennaReferencePoint; // Recorded to NVM and file at main menu exit + } + + else if (settings.fixedBase == false && incoming == 2) + { + systemPrint("Enter the number of seconds for survey-in obseration time (60 to 600s): "); + int observationSeconds = getNumber(); // Returns EXIT, TIMEOUT, or long + if ((observationSeconds != INPUT_RESPONSE_GETNUMBER_EXIT) && + (observationSeconds != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + { + if (observationSeconds < 60 || observationSeconds > 60 * 10) // Arbitrary 10 minute limit + systemPrintln("Error: Observation seconds out of range"); + else + settings.observationSeconds = observationSeconds; // Recorded to NVM and file at main menu exit + } + } + else if (settings.fixedBase == false && incoming == 3) + { + systemPrintf("Enter the number of meters for survey-in required position accuracy (1.0 to %.1fm): ", maxObservationPositionAccuracy); + float observationPositionAccuracy = getDouble(); + + if (observationPositionAccuracy < 1.0 || + observationPositionAccuracy > maxObservationPositionAccuracy) // Arbitrary 1m minimum + systemPrintln("Error: Observation positional accuracy requirement out of range"); + else + settings.observationPositionAccuracy = + observationPositionAccuracy; // Recorded to NVM and file at main menu exit + } + else if (settings.fixedBase == false && incoming == 4) + { + systemPrintf("Enter the positional accuracy required before Survey-In begins (0.1 to %.1fm): ", maxSurveyInStartingAccuracy); + float surveyInStartingAccuracy = getDouble(); + if (surveyInStartingAccuracy < 0.1 || + surveyInStartingAccuracy > maxSurveyInStartingAccuracy) // Arbitrary 0.1m minimum + systemPrintln("Error: Starting accuracy out of range"); + else + settings.surveyInStartingAccuracy = + surveyInStartingAccuracy; // Recorded to NVM and file at main menu exit + } + + else if (incoming == 7) + { + settings.enableNtripServer ^= 1; + restartBase = true; + } + + else if ((incoming == 8) && settings.enableNtripServer == true) + { + serverIndex++; + if (serverIndex >= NTRIP_SERVER_MAX) + serverIndex = 0; + } + else if ((incoming == 9) && settings.enableNtripServer == true) + { + systemPrint("Enter new Caster Host / Address: "); + if (getString(&settings.ntripServer_CasterHost[serverIndex][0], + sizeof(settings.ntripServer_CasterHost[serverIndex]) + == INPUT_RESPONSE_VALID)) + restartBase = true; + } + else if ((incoming == 10) && settings.enableNtripServer == true) + { + systemPrint("Enter new Caster Port: "); + + int ntripServer_CasterPort = getNumber(); // Returns EXIT, TIMEOUT, or long + if ((ntripServer_CasterPort != INPUT_RESPONSE_GETNUMBER_EXIT) && + (ntripServer_CasterPort != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + { + if (ntripServer_CasterPort < 1 || ntripServer_CasterPort > 65535) + systemPrintln("Error: Caster port out of range"); + else + settings.ntripServer_CasterPort[serverIndex] = + ntripServer_CasterPort; // Recorded to NVM and file at main menu exit + restartBase = true; + } + } + else if ((incoming == 11) && settings.enableNtripServer == true) + { + systemPrint("Enter new Caster Username: "); + if (getString(&settings.ntripServer_CasterUser[serverIndex][0], + sizeof(settings.ntripServer_CasterUser[serverIndex])) + == INPUT_RESPONSE_VALID) + restartBase = true; + } + else if ((incoming == 12) && settings.enableNtripServer == true) + { + systemPrintf("Enter password for Caster User %s: ", settings.ntripServer_CasterUser[serverIndex]); + if (getString(&settings.ntripServer_CasterUserPW[serverIndex][0], + sizeof(settings.ntripServer_CasterUserPW[serverIndex])) + == INPUT_RESPONSE_VALID) + restartBase = true; + } + else if ((incoming == 13) && settings.enableNtripServer == true) + { + systemPrint("Enter new Mount Point: "); + if (getString(&settings.ntripServer_MountPoint[serverIndex][0], + sizeof(settings.ntripServer_MountPoint[serverIndex])) + == INPUT_RESPONSE_VALID) + restartBase = true; + } + else if ((incoming == 14) && settings.enableNtripServer == true) + { + systemPrintf("Enter password for Mount Point %s: ", settings.ntripServer_MountPoint[serverIndex]); + if (getString(&settings.ntripServer_MountPointPW[serverIndex][0], + sizeof(settings.ntripServer_MountPointPW[serverIndex])) + == INPUT_RESPONSE_VALID) + restartBase = true; + } + else if (((settings.enableNtripServer == true) && ((incoming == 15))) || + ((settings.enableNtripServer == false) && (incoming == 8))) + { + menuMessagesBaseRTCM(); // Set rates for RTCM during Base mode + } + else if (((settings.enableNtripServer == true) && (settings.fixedBase == false) && ((incoming == 16))) || + ((settings.enableNtripServer == false) && (settings.fixedBase == false) && (incoming == 9))) + { + settings.ntripServer_StartAtSurveyIn ^= 1; + restartBase = true; + } + else if (incoming == INPUT_RESPONSE_GETNUMBER_EXIT) + break; + else if (incoming == INPUT_RESPONSE_GETNUMBER_TIMEOUT) + break; + else + printUnknown(incoming); } - else if (settings.fixedBase == true && incoming == 2) + + clearBuffer(); // Empty buffer of any newline chars +} + +// Set coordinate display format +void menuBaseCoordinateType() +{ + while (1) { - settings.fixedBaseCoordinateType ^= 1; + systemPrintln(); + systemPrintln("Menu: Coordinate Display Type"); + + systemPrintln("The coordinate type is autodetected during entry but can be changed here."); + + systemPrint("Current display format: "); + systemPrintln(coordinatePrintableInputType(settings.coordinateInputType)); + + for (int x = 0; x < COORDINATE_INPUT_TYPE_INVALID_UNKNOWN; x++) + systemPrintf("%d) %s\r\n", x + 1, coordinatePrintableInputType((CoordinateInputType)x)); + + systemPrintln("x) Exit"); + + int incoming = getNumber(); // Returns EXIT, TIMEOUT, or long + + if (incoming >= 1 && incoming < (COORDINATE_INPUT_TYPE_INVALID_UNKNOWN + 1)) + { + settings.coordinateInputType = (CoordinateInputType)(incoming - 1); // Align from 1-9 to 0-8 + } + else if (incoming == INPUT_RESPONSE_GETNUMBER_EXIT) + break; + else if (incoming == INPUT_RESPONSE_GETNUMBER_TIMEOUT) + break; + else + printUnknown(incoming); } - else if (settings.fixedBase == true && incoming == 3) + + clearBuffer(); // Empty buffer of any newline chars +} + +// Configure ESF settings +void menuSensorFusion() +{ + while (1) { - if (settings.fixedBaseCoordinateType == COORD_TYPE_ECEF) - { - Serial.println(F("Enter the fixed ECEF coordinates that will be used in Base mode:")); + systemPrintln(); + systemPrintln("Menu: Sensor Fusion"); + + if (settings.enableSensorFusion == true) + { + // packetUBXESFSTATUS is sent automatically by the module + systemPrint("Fusion Mode: "); + systemPrint(theGNSS.packetUBXESFSTATUS->data.fusionMode); + systemPrint(" - "); + if (theGNSS.packetUBXESFSTATUS->data.fusionMode == 0) + systemPrint("Initializing"); + else if (theGNSS.packetUBXESFSTATUS->data.fusionMode == 1) + systemPrint("Calibrated"); + else if (theGNSS.packetUBXESFSTATUS->data.fusionMode == 2) + systemPrint("Suspended"); + else if (theGNSS.packetUBXESFSTATUS->data.fusionMode == 3) + systemPrint("Disabled"); + systemPrintln(); + + if (theGNSS.getEsfAlignment()) // Poll new ESF ALG data + { + systemPrint("Alignment Mode: "); + systemPrint(theGNSS.packetUBXESFALG->data.flags.bits.status); + systemPrint(" - "); + if (theGNSS.packetUBXESFALG->data.flags.bits.status == 0) + systemPrint("User Defined"); + else if (theGNSS.packetUBXESFALG->data.flags.bits.status == 1) + systemPrint("Alignment Roll/Pitch Ongoing"); + else if (theGNSS.packetUBXESFALG->data.flags.bits.status == 2) + systemPrint("Alignment Roll/Pitch/Yaw Ongoing"); + else if (theGNSS.packetUBXESFALG->data.flags.bits.status == 3) + systemPrint("Coarse Alignment Used"); + else if (theGNSS.packetUBXESFALG->data.flags.bits.status == 3) + systemPrint("Fine Alignment Used"); + systemPrintln(); + } + + if (settings.dynamicModel != DYN_MODEL_AUTOMOTIVE) + systemPrintln("Warning: Dynamic Model not set to Automotive. Sensor Fusion is best used with the " + "Automotive Dynamic Model."); + } - Serial.print(F("ECEF X in meters (ex: -1280182.920): ")); - double fixedEcefX = getDouble(menuTimeoutExtended); //Timeout after x seconds + systemPrintf("1) Toggle Sensor Fusion: %s\r\n", settings.enableSensorFusion ? "Enabled" : "Disabled"); - //Progress with additional prompts only if the user enters valid data - if (fixedEcefX != STATUS_GETNUMBER_TIMEOUT && fixedEcefX != STATUS_PRESSED_X) + if (settings.enableSensorFusion == true) { - settings.fixedEcefX = fixedEcefX; + if (settings.autoIMUmountAlignment == true) + { + systemPrintf("2) Toggle Automatic IMU-mount Alignment: True - Yaw: %0.2f Pitch: %0.2f Roll: %0.2f\r\n", + theGNSS.getESFyaw(), theGNSS.getESFpitch(), theGNSS.getESFroll()); + + systemPrintf("3) Disable automatic wheel tick direction pin polarity detection: %s\r\n", + settings.sfDisableWheelDirection ? "True" : "False"); + + systemPrintf("4) Use combined rear wheel ticks instead of the single tick: %s\r\n", + settings.sfCombineWheelTicks ? "True" : "False"); + + systemPrintf("5) Output rate of priority nav mode message: %d\r\n", settings.rateNavPrio); + + systemPrintf("6) Use speed measurements instead of single ticks: %s\r\n", + settings.sfUseSpeed ? "True" : "False"); + } + else + { + systemPrintf("2) Toggle Automatic IMU-mount Alignment: False\r\n"); + + systemPrintf("3) Manually set yaw: %0.2f\r\n", settings.imuYaw / 100.0); + + systemPrintf("4) Manually set pitch: %0.2f\r\n", settings.imuPitch / 100.0); - Serial.print(F("\nECEF Y in meters (ex: -4716808.5807): ")); - double fixedEcefY = getDouble(menuTimeoutExtended); - if (fixedEcefY != STATUS_GETNUMBER_TIMEOUT && fixedEcefY != STATUS_PRESSED_X) - { - settings.fixedEcefY = fixedEcefY; + systemPrintf("5) Manually set roll: %0.2f\r\n", settings.imuRoll / 100.0); - Serial.print(F("\nECEF Z in meters (ex: 4086669.6393): ")); - double fixedEcefZ = getDouble(menuTimeoutExtended); - if (fixedEcefZ != STATUS_GETNUMBER_TIMEOUT && fixedEcefZ != STATUS_PRESSED_X) - settings.fixedEcefZ = fixedEcefZ; - } + systemPrintf("6) Disable automatic wheel tick direction pin polarity detection: %s\r\n", + settings.sfDisableWheelDirection ? "True" : "False"); + + systemPrintf("7) Use combined rear wheel ticks instead of the single tick: %s\r\n", + settings.sfCombineWheelTicks ? "True" : "False"); + + systemPrintf("8) Output rate of priority nav mode message: %d\r\n", settings.rateNavPrio); + + systemPrintf("9) Use speed measurements instead of single ticks: %s\r\n", + settings.sfUseSpeed ? "True" : "False"); + + // CFG-SFIMU-IMU_MNTALG_YAW + // CFG-SFIMU-IMU_MNTALG_PITCH + // CFG-SFIMU-IMU_MNTALG_ROLL + // CFG-SFODO-DIS_AUTODIRPINPOL + // CFG-SFODO-COMBINE_TICKS + + // CFG-RATE-NAV_PRIO + // CFG-NAV2-OUT_ENABLED + // CFG-SFODO-USE_SPEED + } } - } - else if (settings.fixedBaseCoordinateType == COORD_TYPE_GEOGRAPHIC) - { - Serial.println(F("Enter the fixed Lat/Long/Altitude coordinates that will be used in Base mode:")); - Serial.print(F("Lat in degrees (ex: 40.090335429): ")); - double fixedLat = getDouble(menuTimeoutExtended); //Timeout after x seconds + systemPrintln("x) Exit"); + + int incoming = getNumber(); // Returns EXIT, TIMEOUT, or long + + if (incoming == 1) + { + settings.enableSensorFusion ^= 1; + setSensorFusion(settings.enableSensorFusion); // Enable/disable sensor fusion + } + else if (settings.enableSensorFusion == true && incoming == 2) + { + settings.autoIMUmountAlignment ^= 1; + } + else if (settings.enableSensorFusion == true && ((settings.autoIMUmountAlignment == true && incoming == 3) || + (settings.autoIMUmountAlignment == false && incoming == 6))) + { + settings.sfDisableWheelDirection ^= 1; + } + else if (settings.enableSensorFusion == true && ((settings.autoIMUmountAlignment == true && incoming == 4) || + (settings.autoIMUmountAlignment == false && incoming == 7))) + { + settings.sfCombineWheelTicks ^= 1; + } - //Progress with additional prompts only if the user enters valid data - if (fixedLat != STATUS_GETNUMBER_TIMEOUT && fixedLat != STATUS_PRESSED_X) + else if (settings.enableSensorFusion == true && settings.autoIMUmountAlignment == false && incoming == 3) { - settings.fixedLat = fixedLat; + systemPrint("Enter yaw alignment in degrees (0.00 to 360.00): "); + double yaw = getDouble(); + if (yaw < 0.00 || yaw > 360.00) // 0 to 36,000 + { + systemPrintln("Error: Yaw out of range"); + } + else + { + settings.imuYaw = yaw * 100; // 56.44 to 5644 + } + } + else if (settings.enableSensorFusion == true && settings.autoIMUmountAlignment == false && incoming == 4) + { + systemPrint("Enter pitch alignment in degrees (-90.00 to 90.00): "); + double pitch = getDouble(); + if (pitch < -90.00 || pitch > 90.00) //-9000 to 9000 + { + systemPrintln("Error: Pitch out of range"); + } + else + { + settings.imuPitch = pitch * 100; // 56.44 to 5644 + } + } + else if (settings.enableSensorFusion == true && settings.autoIMUmountAlignment == false && incoming == 5) + { + systemPrint("Enter roll alignment in degrees (-180.00 to 180.00): "); + double roll = getDouble(); + if (roll < -180.00 || roll > 180.0) //-18000 to 18000 + { + systemPrintln("Error: Roll out of range"); + } + else + { + settings.imuRoll = roll * 100; // 56.44 to 5644 + } + } - Serial.print(F("\nLong in degrees (ex: -105.184774720): ")); - double fixedLong = getDouble(menuTimeoutExtended); - if (fixedLong != STATUS_GETNUMBER_TIMEOUT && fixedLong != STATUS_PRESSED_X) - { - settings.fixedLong = fixedLong; + else if (settings.enableSensorFusion == true && ((settings.autoIMUmountAlignment == true && incoming == 5) || + (settings.autoIMUmountAlignment == false && incoming == 8))) + { + systemPrint("Enter the output rate of priority nav mode message (0 to 30Hz): "); // TODO check maximum + int rate = getNumber(); // Returns EXIT, TIMEOUT, or long + if ((rate != INPUT_RESPONSE_GETNUMBER_EXIT) && (rate != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + { + if (rate < 0 || rate > 30) + systemPrintln("Error: Output rate out of range"); + else + settings.rateNavPrio = rate; + } + } - Serial.print(F("\nAltitude in meters (ex: 1560.2284): ")); - double fixedAltitude = getDouble(menuTimeoutExtended); - if (fixedAltitude != STATUS_GETNUMBER_TIMEOUT && fixedAltitude != STATUS_PRESSED_X) - settings.fixedAltitude = fixedAltitude; - } + else if (settings.enableSensorFusion == true && ((settings.autoIMUmountAlignment == true && incoming == 6) || + (settings.autoIMUmountAlignment == false && incoming == 9))) + { + settings.sfUseSpeed ^= 1; } - } + + else if (incoming == INPUT_RESPONSE_GETNUMBER_EXIT) + break; + else if (incoming == INPUT_RESPONSE_GETNUMBER_TIMEOUT) + break; + else + printUnknown(incoming); } - else if (settings.fixedBase == false && incoming == 2) + + theGNSS.setVal8(UBLOX_CFG_SFCORE_USE_SF, settings.enableSensorFusion); // Enable/disable sensor fusion + theGNSS.setVal8(UBLOX_CFG_SFIMU_AUTO_MNTALG_ENA, + settings.autoIMUmountAlignment); // Enable/disable Automatic IMU-mount Alignment + theGNSS.setVal8(UBLOX_CFG_SFIMU_IMU_MNTALG_YAW, settings.imuYaw); + theGNSS.setVal8(UBLOX_CFG_SFIMU_IMU_MNTALG_PITCH, settings.imuPitch); + theGNSS.setVal8(UBLOX_CFG_SFIMU_IMU_MNTALG_ROLL, settings.imuRoll); + theGNSS.setVal8(UBLOX_CFG_SFODO_DIS_AUTODIRPINPOL, settings.sfDisableWheelDirection); + theGNSS.setVal8(UBLOX_CFG_SFODO_COMBINE_TICKS, settings.sfCombineWheelTicks); + theGNSS.setVal8(UBLOX_CFG_RATE_NAV_PRIO, settings.rateNavPrio); + theGNSS.setVal8(UBLOX_CFG_SFODO_USE_SPEED, settings.sfUseSpeed); + + clearBuffer(); // Empty buffer of any newline chars +} + +//---------------------------------------- +// Support functions +//---------------------------------------- + +// Enable or disable sensor fusion using keys +void setSensorFusion(bool enable) +{ + if (getSensorFusion() != enable) + theGNSS.setVal8(UBLOX_CFG_SFCORE_USE_SF, enable, VAL_LAYER_ALL); +} + +bool getSensorFusion() +{ + return (theGNSS.getVal8(UBLOX_CFG_SFCORE_USE_SF, VAL_LAYER_RAM, 1200)); +} + +// Open the given file and load a given line to the given pointer +bool getFileLineLFS(const char *fileName, int lineToFind, char *lineData, int lineDataLength) +{ + File file = LittleFS.open(fileName, FILE_READ); + if (!file) { - Serial.print(F("Enter the number of seconds for survey-in obseration time (60 to 600s): ")); - int observationSeconds = getNumber(menuTimeout); //Timeout after x seconds - if (observationSeconds < 60 || observationSeconds > 60 * 10) //Arbitrary 10 minute limit - { - Serial.println(F("Error: observation seconds out of range")); - } - else - { - settings.observationSeconds = observationSeconds; //Recorded to NVM and file at main menu exit - } + log_d("File %s not found", fileName); + return (false); } - else if (settings.fixedBase == false && incoming == 3) + + // We cannot be sure how the user will terminate their files so we avoid the use of readStringUntil + int lineNumber = 0; + int x = 0; + bool lineFound = false; + + while (file.available()) { - Serial.print(F("Enter the number of meters for survey-in required position accuracy (1.0 to 5.0m): ")); - float observationPositionAccuracy = getDouble(menuTimeout); //Timeout after x seconds - if (observationPositionAccuracy < 1.0 || observationPositionAccuracy > 5.0) //Arbitrary 1m minimum - { - Serial.println(F("Error: observation positional accuracy requirement out of range")); - } - else - { - settings.observationPositionAccuracy = observationPositionAccuracy; //Recorded to NVM and file at main menu exit - } + byte incoming = file.read(); + if (incoming == '\r' || incoming == '\n') + { + lineData[x] = '\0'; // Terminate + + if (lineNumber == lineToFind) + { + lineFound = true; // We found the line. We're done! + break; + } + + // Sometimes a line has multiple terminators + while (file.peek() == '\r' || file.peek() == '\n') + file.read(); // Dump it to prevent next line read corruption + + lineNumber++; // Advance + x = 0; // Reset + } + else + { + if (x == (lineDataLength - 1)) + { + lineData[x] = '\0'; // Terminate + break; // Max size hit + } + + // Record this character to the lineData array + lineData[x++] = incoming; + } } - else if (incoming == 4) + file.close(); + return (lineFound); +} + +// Given a fileName, return the given line number +// Returns true if line was loaded +// Returns false if a file was not opened/loaded +bool getFileLineSD(const char *fileName, int lineToFind, char *lineData, int lineDataLength) +{ + bool gotSemaphore = false; + bool lineFound = false; + bool wasSdCardOnline; + + // Try to gain access the SD card + wasSdCardOnline = online.microSD; + if (online.microSD != true) + beginSD(); + + while (online.microSD == true) { - settings.enableNtripServer ^= 1; - } - else if (incoming == 5 && settings.enableNtripServer == true) + // Attempt to access file system. This avoids collisions with file writing from other functions like + // recordSystemSettingsToFile() and F9PSerialReadTask() + if (xSemaphoreTake(sdCardSemaphore, fatSemaphore_longWait_ms) == pdPASS) + { + markSemaphore(FUNCTION_GETLINE); + + gotSemaphore = true; + + if (USE_SPI_MICROSD) + { + SdFile file; // FAT32 + if (file.open(fileName, O_READ) == false) + { + log_d("File %s not found", fileName); + break; + } + + int lineNumber = 0; + + while (file.available()) + { + // Get the next line from the file + int n = file.fgets(lineData, lineDataLength); + if (n <= 0) + { + systemPrintf("Failed to read line %d from settings file\r\n", lineNumber); + break; + } + else + { + if (lineNumber == lineToFind) + { + lineFound = true; + break; + } + } + + if (strlen(lineData) > 0) // Ignore single \n or \r + lineNumber++; + } + + file.close(); + } +#ifdef COMPILE_SD_MMC + else + { + File file = SD_MMC.open(fileName, FILE_READ); + + if (!file) + { + log_d("File %s not found", fileName); + break; + } + + int lineNumber = 0; + + while (file.available()) + { + // Get the next line from the file + int n = getLine(&file, lineData, lineDataLength); + if (n <= 0) + { + systemPrintf("Failed to read line %d from settings file\r\n", lineNumber); + break; + } + else + { + if (lineNumber == lineToFind) + { + lineFound = true; + break; + } + } + + if (strlen(lineData) > 0) // Ignore single \n or \r + lineNumber++; + } + + file.close(); + } +#endif // COMPILE_SD_MMC + break; + } // End Semaphore check + else + { + systemPrintf("sdCardSemaphore failed to yield, menuBase.ino line %d\r\n", __LINE__); + } + break; + } // End SD online + + // Release access the SD card + if (online.microSD && (!wasSdCardOnline)) + endSD(gotSemaphore, true); + else if (gotSemaphore) + xSemaphoreGive(sdCardSemaphore); + + return (lineFound); +} + +// Given a string, replace a single char with another char +void replaceCharacter(char *myString, char toReplace, char replaceWith) +{ + for (int i = 0; i < strlen(myString); i++) { - Serial.print(F("Enter local WiFi SSID: ")); - readLine(settings.wifiSSID, sizeof(settings.wifiSSID), menuTimeoutExtended); + if (myString[i] == toReplace) + myString[i] = replaceWith; } - else if (incoming == 6 && settings.enableNtripServer == true) +} + +// Remove a given filename from SD +bool removeFileSD(const char *fileName) +{ + bool removed = false; + + bool gotSemaphore = false; + bool wasSdCardOnline; + + // Try to gain access the SD card + wasSdCardOnline = online.microSD; + if (online.microSD != true) + beginSD(); + + while (online.microSD == true) { - Serial.printf("Enter password for WiFi network %s: ", settings.wifiSSID); - readLine(settings.wifiPW, sizeof(settings.wifiPW), menuTimeoutExtended); - } - else if (incoming == 7 && settings.enableNtripServer == true) + // Attempt to access file system. This avoids collisions with file writing from other functions like + // recordSystemSettingsToFile() and F9PSerialReadTask() + if (xSemaphoreTake(sdCardSemaphore, fatSemaphore_longWait_ms) == pdPASS) + { + markSemaphore(FUNCTION_REMOVEFILE); + + gotSemaphore = true; + + if (USE_SPI_MICROSD) + { + if (sd->exists(fileName)) + { + log_d("Removing from SD: %s", fileName); + sd->remove(fileName); + removed = true; + } + } +#ifdef COMPILE_SD_MMC + else + { + if (SD_MMC.exists(fileName)) + { + log_d("Removing from SD: %s", fileName); + SD_MMC.remove(fileName); + removed = true; + } + } +#endif // COMPILE_SD_MMC + + break; + } // End Semaphore check + else + { + systemPrintf("sdCardSemaphore failed to yield, menuBase.ino line %d\r\n", __LINE__); + } + break; + } // End SD online + + // Release access the SD card + if (online.microSD && (!wasSdCardOnline)) + endSD(gotSemaphore, true); + else if (gotSemaphore) + xSemaphoreGive(sdCardSemaphore); + + return (removed); +} + +// Remove a given filename from LFS +bool removeFileLFS(const char *fileName) +{ + if (LittleFS.exists(fileName)) { - Serial.print(F("Enter new Caster Address: ")); - readLine(settings.casterHost, sizeof(settings.casterHost), menuTimeoutExtended); + LittleFS.remove(fileName); + log_d("Removing LittleFS: %s", fileName); + return (true); } - else if (incoming == 8 && settings.enableNtripServer == true) - { - Serial.print(F("Enter new Caster Port: ")); - int casterPort = getNumber(menuTimeoutExtended); //Timeout after x seconds - if (casterPort < 1 || casterPort > 99999) //Arbitrary 99k max port # - Serial.println(F("Error: Caster Port out of range")); - else - settings.casterPort = casterPort; //Recorded to NVM and file at main menu exit - } - else if (incoming == 9 && settings.enableNtripServer == true) + return (false); +} + +// Remove a given filename from SD and LFS +bool removeFile(const char *fileName) +{ + bool removed = true; + + removed &= removeFileSD(fileName); + removed &= removeFileLFS(fileName); + + return (removed); +} + +// Given a filename and char array, append to file +void recordLineToSD(const char *fileName, const char *lineData) +{ + bool gotSemaphore = false; + bool wasSdCardOnline; + + // Try to gain access the SD card + wasSdCardOnline = online.microSD; + if (online.microSD != true) + beginSD(); + + while (online.microSD == true) { - Serial.print(F("Enter new Mount Point: ")); - readLine(settings.mountPoint, sizeof(settings.mountPoint), menuTimeoutExtended); - } - else if (incoming == 10 && settings.enableNtripServer == true) + // Attempt to access file system. This avoids collisions with file writing from other functions like + // recordSystemSettingsToFile() and F9PSerialReadTask() + if (xSemaphoreTake(sdCardSemaphore, fatSemaphore_longWait_ms) == pdPASS) + { + markSemaphore(FUNCTION_RECORDLINE); + + gotSemaphore = true; + + FileSdFatMMC file; + if (!file) + { + systemPrintln("ERROR - Failed to allocate file"); + break; + } + if (file.open(fileName, O_CREAT | O_APPEND | O_WRITE) == false) + { + log_d("File %s not found", fileName); + break; + } + + file.println(lineData); + file.close(); + break; + } // End Semaphore check + else + { + systemPrintf("sdCardSemaphore failed to yield, menuBase.ino line %d\r\n", __LINE__); + } + break; + } // End SD online + + // Release access the SD card + if (online.microSD && (!wasSdCardOnline)) + endSD(gotSemaphore, true); + else if (gotSemaphore) + xSemaphoreGive(sdCardSemaphore); +} + +// Given a filename and char array, append to file +void recordLineToLFS(const char *fileName, const char *lineData) +{ + File file = LittleFS.open(fileName, FILE_APPEND); + if (!file) { - Serial.printf("Enter password for Mount Point %s: ", settings.mountPoint); - readLine(settings.mountPointPW, sizeof(settings.mountPointPW), menuTimeoutExtended); + systemPrintf("File %s failed to create\r\n", fileName); + return; } - else if (incoming == STATUS_PRESSED_X) - break; - else if (incoming == STATUS_GETNUMBER_TIMEOUT) - break; - else - printUnknown(incoming); - } - - while (Serial.available()) Serial.read(); //Empty buffer of any newline chars + + file.println(lineData); + file.close(); +} + +// Remove ' ', \t, \v, \f, \r, \n from end of a char array +void trim(char *str) +{ + int x = 0; + for (; str[x] != '\0'; x++) + ; + if (x > 0) + x--; + + for (; isspace(str[x]); x--) + str[x] = '\0'; } diff --git a/Firmware/RTK_Surveyor/menuDebug.ino b/Firmware/RTK_Surveyor/menuDebug.ino deleted file mode 100644 index b1645bee4..000000000 --- a/Firmware/RTK_Surveyor/menuDebug.ino +++ /dev/null @@ -1,201 +0,0 @@ -//Toggle control of heap reports and I2C GNSS debug -void menuDebug() -{ - int maxWait = 2000; - - while (1) - { - Serial.println(); - Serial.println(F("Menu: Debug Menu")); - - //Check the firmware version of the ZED-F9P. Based on Example21_ModuleInfo. - if (i2cGNSS.getModuleInfo(maxWait) == true) // Try to get the module info - { - Serial.print(F("ZED-F9P firmware: ")); - Serial.println(i2cGNSS.minfo.extension[1]); - } - - Serial.print(F("1) I2C Debugging Output: ")); - if (settings.enableI2Cdebug == true) Serial.println(F("Enabled")); - else Serial.println(F("Disabled")); - - Serial.print(F("2) Heap Reporting: ")); - if (settings.enableHeapReport == true) Serial.println(F("Enabled")); - else Serial.println(F("Disabled")); - - Serial.print(F("3) Task Highwater Reporting: ")); - if (settings.enableTaskReports == true) Serial.println(F("Enabled")); - else Serial.println(F("Disabled")); - - Serial.print(F("4) Set SPI/SD Interface Frequency: ")); - Serial.print(settings.spiFrequency); - Serial.println(" MHz"); - - Serial.print(F("5) Set SPP RX Buffer Size: ")); - Serial.println(settings.sppRxQueueSize); - - Serial.print(F("6) Set SPP TX Buffer Size: ")); - Serial.println(settings.sppTxQueueSize); - - Serial.print(F("7) Throttle BT Transmissions During SPP Congestion: ")); - if (settings.throttleDuringSPPCongestion == true) Serial.println(F("Enabled")); - else Serial.println(F("Disabled")); - - Serial.println(F("x) Exit")); - - byte incoming = getByteChoice(menuTimeout); //Timeout after x seconds - - if (incoming == '1') - { - settings.enableI2Cdebug ^= 1; - - if (settings.enableI2Cdebug) - i2cGNSS.enableDebugging(Serial, true); //Enable only the critical debug messages over Serial - else - i2cGNSS.disableDebugging(); - } - else if (incoming == '2') - { - settings.enableHeapReport ^= 1; - } - else if (incoming == '3') - { - settings.enableTaskReports ^= 1; - } - else if (incoming == '4') - { - Serial.print(F("Enter SPI frequency in MHz (1 to 48): ")); - int freq = getNumber(menuTimeout); //Timeout after x seconds - if (freq < 1 || freq > 48) //Arbitrary 48 hour limit - { - Serial.println(F("Error: SPI frequency out of range")); - } - else - { - settings.spiFrequency = freq; //Recorded to NVM and file at main menu exit - } - } - else if (incoming == '5') - { - Serial.print(F("Enter SPP RX Queue Size in Bytes (32 to 16384): ")); - uint16_t queSize = getNumber(menuTimeout); //Timeout after x seconds - if (queSize < 32 || queSize > 16384) //Arbitrary 16k limit - { - Serial.println(F("Error: Queue size out of range")); - } - else - { - settings.sppRxQueueSize = queSize; //Recorded to NVM and file at main menu exit - } - } - else if (incoming == '6') - { - Serial.print(F("Enter SPP TX Queue Size in Bytes (32 to 16384): ")); - uint16_t queSize = getNumber(menuTimeout); //Timeout after x seconds - if (queSize < 32 || queSize > 16384) //Arbitrary 16k limit - { - Serial.println(F("Error: Queue size out of range")); - } - else - { - settings.sppTxQueueSize = queSize; //Recorded to NVM and file at main menu exit - } - } - else if (incoming == '7') - { - settings.throttleDuringSPPCongestion ^= 1; - } - else if (incoming == 'x') - break; - else if (incoming == STATUS_GETBYTE_TIMEOUT) - { - break; - } - else - printUnknown(incoming); - } - - while (Serial.available()) Serial.read(); //Empty buffer of any newline chars -} - -//Globals but only used for menuBubble -double averagedRoll = 0.0; -double averagedPitch = 0.0; - -//A bubble level -void menuBubble() -{ - Serial.println(); - Serial.println(F("Menu: Bubble Level")); - - Serial.print(F("Press any key to exit")); - - delay(10); - while (Serial.available()) Serial.read(); //Empty buffer of any newline chars - - while (1) - { - if (Serial.available()) break; - - getAngles(); - - if (online.display == true) - { - oled.clear(PAGE); // Clear the display's internal memory - - //Draw dot in middle - oled.pixel(LCDWIDTH / 2, LCDHEIGHT / 2); - oled.pixel(LCDWIDTH / 2 + 1, LCDHEIGHT / 2); - oled.pixel(LCDWIDTH / 2, LCDHEIGHT / 2 + 1); - oled.pixel(LCDWIDTH / 2 + 1, LCDHEIGHT / 2 + 1); - - //Draw circle relative to dot - const int radiusLarge = 10; - const int radiusSmall = 4; - - oled.circle(LCDWIDTH / 2 - averagedPitch, LCDHEIGHT / 2 + averagedRoll, radiusLarge); - oled.circle(LCDWIDTH / 2 - averagedPitch, LCDHEIGHT / 2 + averagedRoll, radiusSmall); - - oled.display(); - } - } - - displaySerialConfig(); //Display 'Serial Config' while user is configuring - - while (Serial.available()) Serial.read(); //Empty buffer of any newline chars -} - -void getAngles() -{ - if (online.accelerometer == true) - { - averagedRoll = 0.0; - averagedPitch = 0.0; - const int avgAmount = 16; - - //Take an average readings - for (int reading = 0 ; reading < avgAmount ; reading++) - { - while (accel.available() == false) delay(1); - - float accelX = accel.getX(); - float accelZ = accel.getY(); - float accelY = accel.getZ(); - accelZ *= -1.0; - accelX *= -1.0; - - double roll = atan2(accelY , accelZ) * 57.3; - double pitch = atan2((-accelX) , sqrt(accelY * accelY + accelZ * accelZ)) * 57.3; - - averagedRoll += roll; - averagedPitch += pitch; - } - - averagedRoll /= (float)avgAmount; - averagedPitch /= (float)avgAmount; - - //Avoid -0 since we're not printing the decimal portion - if (averagedRoll < 0.5 && averagedRoll > -0.5) averagedRoll = 0; - if (averagedPitch < 0.5 && averagedPitch > -0.5) averagedPitch = 0; - } -} diff --git a/Firmware/RTK_Surveyor/menuFirmware.ino b/Firmware/RTK_Surveyor/menuFirmware.ino index 7d8fefa54..3e4e7a5da 100644 --- a/Firmware/RTK_Surveyor/menuFirmware.ino +++ b/Firmware/RTK_Surveyor/menuFirmware.ino @@ -1,223 +1,851 @@ -//Update firmware if bin files found +/*------------------------------------------------------------------------------ +menuFirmware.ino + + This module implements the firmware menu and update code. +------------------------------------------------------------------------------*/ + +//---------------------------------------- +// Menu +//---------------------------------------- + +// Update firmware if bin files found void menuFirmware() { - if (online.microSD == false) - { - Serial.println(F("No SD card detected")); - } + bool newOTAFirmwareAvailable = false; + char reportedVersion[50] = {'\0'}; - if (binCount == 0) - { - Serial.println(F("No valid binary files found.")); - delay(2000); - return; - } + while (1) + { + systemPrintln(); + systemPrintln("Menu: Update Firmware"); - while (1) - { - Serial.println(); - Serial.println(F("Menu: Update Firmware Menu")); + if (btPrintEcho == true) + systemPrintln("Firmware update not available while configuration over Bluetooth is active"); - for (int x = 0 ; x < binCount ; x++) - { - Serial.printf("%d) Load %s\n\r", x + 1, binFileNames[x]); + char currentVersion[21]; + getFirmwareVersion(currentVersion, sizeof(currentVersion), enableRCFirmware); + systemPrintf("Current firmware: %s\r\n", currentVersion); + + // Automatic firmware updates + systemPrintf("a) Automatic firmware updates: %s\r\n", + settings.enableAutoFirmwareUpdate ? "Enabled" : "Disabled"); + + if (strlen(reportedVersion) > 0) + { + if (newOTAFirmwareAvailable == false) + systemPrintf("c) Check SparkFun for device firmware: Up to date\r\n"); + } + else + systemPrintln("c) Check SparkFun for device firmware"); + + systemPrintf("e) Allow Beta Firmware: %s\r\n", enableRCFirmware ? "Enabled" : "Disabled"); + + if (settings.enableAutoFirmwareUpdate) + systemPrintf("i) Automatic firmware check minutes: %d\r\n", settings.autoFirmwareCheckMinutes); + + if (newOTAFirmwareAvailable) + systemPrintf("u) Update to new firmware: v%s\r\n", reportedVersion); + + for (int x = 0; x < binCount; x++) + systemPrintf("%d) Load SD file: %s\r\n", x + 1, binFileNames[x]); + + systemPrintln("x) Exit"); + + byte incoming = getCharacterNumber(); + + if (incoming > 0 && incoming < (binCount + 1)) + { + // Adjust incoming to match array + incoming--; + updateFromSD(binFileNames[incoming]); + } + + else if (incoming == 'a') + settings.enableAutoFirmwareUpdate ^= 1; + + else if (incoming == 'c' && btPrintEcho == false) + { + if (wifiNetworkCount() == 0) + { + systemPrintln("Error: Please enter at least one SSID before updating firmware"); + } + else + { + bool previouslyConnected = wifiIsConnected(); + + bluetoothStop(); // Stop Bluetooth to allow for SSL on the heap + + // Attempt to connect to local WiFi + if (wifiConnect(10000) == true) + { + // Get firmware version from server + if (otaCheckVersion(reportedVersion, sizeof(reportedVersion))) + { + // We got a version number, now determine if it's newer or not + char currentVersion[21]; + getFirmwareVersion(currentVersion, sizeof(currentVersion), enableRCFirmware); + + // Allow update if locally compiled developer version + if (isReportedVersionNewer(reportedVersion, ¤tVersion[1]) == true || + FIRMWARE_VERSION_MAJOR == 99) + { + systemPrintln("New version detected"); + newOTAFirmwareAvailable = true; + } + else + { + systemPrintln("No new firmware available"); + } + } + else + { + // Failed to get version number + systemPrintln("Failed to get version number from server."); + } + } + else if (incoming == 'c' && btPrintEcho == false) + { + bool previouslyConnected = wifiIsConnected(); + + bool bluetoothOriginallyConnected = false; + if (bluetoothState == BT_CONNECTED) + bluetoothOriginallyConnected = true; + + bluetoothStop(); // Stop Bluetooth to allow for SSL on the heap + + // Attempt to connect to local WiFi + if (wifiConnect(10000) == true) + { + // Get firmware version from server + if (otaCheckVersion(reportedVersion, sizeof(reportedVersion))) + { + // We got a version number, now determine if it's newer or not + char currentVersion[21]; + getFirmwareVersion(currentVersion, sizeof(currentVersion), enableRCFirmware); + if (isReportedVersionNewer(reportedVersion, ¤tVersion[1]) == true) + { + systemPrintln("New version detected"); + newOTAFirmwareAvailable = true; + } + else + { + systemPrintln("No new firmware available"); + } + } + else + { + // Failed to get version number + systemPrintln("Failed to get version number from server."); + } + } + else + systemPrintln("Firmware update failed to connect to WiFi."); + + if (previouslyConnected == false) + WIFI_STOP(); + + if (bluetoothOriginallyConnected == true) + bluetoothStart(); // Restart BT according to settings + } + } // End wifiNetworkCount() check + } + else if (incoming == 'c' && btPrintEcho == true) + { + systemPrintln("Firmware update not available while configuration over Bluetooth is active"); + delay(2000); + } + + else if (incoming == 'e') + { + enableRCFirmware ^= 1; + strncpy(reportedVersion, "", sizeof(reportedVersion) - 1); // Reset to force c) menu + newOTAFirmwareAvailable = false; + } + + else if ((incoming == 'i') && settings.enableAutoFirmwareUpdate) + { + systemPrint("Enter minutes (1 - 999999) before next firmware check: "); + int minutes = getNumber(); // Returns EXIT, TIMEOUT, or long + if ((minutes != INPUT_RESPONSE_GETNUMBER_EXIT) && (minutes != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + { + if ((minutes < 1) || (minutes > 999999)) + systemPrintln("Error: Out of range (1 - 999999)"); + else + settings.autoFirmwareCheckMinutes = minutes; + } + } + + else if ((incoming == 'u') && newOTAFirmwareAvailable) + { + bool previouslyConnected = wifiIsConnected(); + + otaUpdate(); + + // We get here if WiFi failed or the server was not available + + if (previouslyConnected == false) + WIFI_STOP(); + } + + else if (incoming == 'x') + break; + else if (incoming == INPUT_RESPONSE_GETCHARACTERNUMBER_EMPTY) + break; + else if (incoming == INPUT_RESPONSE_GETCHARACTERNUMBER_TIMEOUT) + break; + else + printUnknown(incoming); } - Serial.println(F("x) Exit")); + clearBuffer(); // Empty buffer of any newline chars +} + +//---------------------------------------- +// Firmware update code +//---------------------------------------- - int incoming = getNumber(menuTimeout); //Timeout after x seconds +void mountSDThenUpdate(const char *firmwareFileName) +{ + bool gotSemaphore; + bool wasSdCardOnline; - if (incoming > 0 && incoming < (binCount + 1)) + // Try to gain access the SD card + gotSemaphore = false; + wasSdCardOnline = online.microSD; + if (online.microSD != true) + beginSD(); + + if (online.microSD != true) + systemPrintln("microSD card is offline!"); + else { - //Adjust incoming to match array - incoming--; - updateFromSD(binFileNames[incoming]); + // Attempt to access file system. This avoids collisions with file writing from other functions like + // recordSystemSettingsToFile() and F9PSerialReadTask() + if (xSemaphoreTake(sdCardSemaphore, fatSemaphore_longWait_ms) == pdPASS) + { + gotSemaphore = true; + updateFromSD(firmwareFileName); + } // End Semaphore check + else + { + systemPrintf("sdCardSemaphore failed to yield, menuFirmware.ino line %d\r\n", __LINE__); + } } - else if (incoming == STATUS_PRESSED_X) - break; - else if (incoming == STATUS_GETNUMBER_TIMEOUT) - break; - else - Serial.printf("Bad value: %d\n\r", incoming); - } - while (Serial.available()) Serial.read(); //Empty buffer of any newline chars + // Release access the SD card + if (online.microSD && (!wasSdCardOnline)) + endSD(gotSemaphore, true); + else if (gotSemaphore) + xSemaphoreGive(sdCardSemaphore); } -//Looks for matching binary files in root -//Loads a global called binCount +// Looks for matching binary files in root +// Loads a global called binCount +// Called from beginSD with microSD card mounted and sdCardsemaphore held void scanForFirmware() { - if (online.microSD == true) - { - //Attempt to access file system. This avoids collisions with file writing in F9PSerialReadTask() - //Wait up to 5s, this is important - if (xSemaphoreTake(xFATSemaphore, 5000 / portTICK_PERIOD_MS) == pdPASS) - { - //Count available binaries - SdFile tempFile; - SdFile dir; - const char* BIN_EXT = "bin"; - const char* BIN_HEADER = "RTK_Surveyor_Firmware"; - - char fname[50]; //Handle long file names - - dir.open("/"); //Open root - - while (tempFile.openNext(&dir, O_READ)) - { - if (tempFile.isFile()) - { - tempFile.getName(fname, sizeof(fname)); - - if (strcmp(forceFirmwareFileName, fname) == 0) - { - Serial.println(F("Forced firmware detected. Loading...")); - displayForcedFirmwareUpdate(); - updateFromSD((char *)forceFirmwareFileName); - } - - //Check 'bin' extension - if (strcmp(BIN_EXT, &fname[strlen(fname) - strlen(BIN_EXT)]) == 0) - { - //Check for 'RTK_Surveyor_Firmware' start of file name - if (strncmp(fname, BIN_HEADER, strlen(BIN_HEADER)) == 0) + // Count available binaries + if (USE_SPI_MICROSD) + { + SdFile tempFile; + SdFile dir; + const char *BIN_EXT = "bin"; + const char *BIN_HEADER = "RTK_Surveyor_Firmware"; + + char fname[50]; // Handle long file names + + dir.open("/"); // Open root + + binCount = 0; // Reset count in case scanForFirmware is called again + + while (tempFile.openNext(&dir, O_READ) && binCount < maxBinFiles) + { + if (tempFile.isFile()) { - strcpy(binFileNames[binCount++], fname); //Add this to the array + tempFile.getName(fname, sizeof(fname)); + + if (strcmp(forceFirmwareFileName, fname) == 0) + { + systemPrintln("Forced firmware detected. Loading..."); + displayForcedFirmwareUpdate(); + updateFromSD(forceFirmwareFileName); + } + + // Check 'bin' extension + if (strcmp(BIN_EXT, &fname[strlen(fname) - strlen(BIN_EXT)]) == 0) + { + // Check for 'RTK_Surveyor_Firmware' start of file name + if (strncmp(fname, BIN_HEADER, strlen(BIN_HEADER)) == 0) + { + strncpy(binFileNames[binCount++], fname, sizeof(binFileNames[0]) - 1); // Add this to the array + } + else + systemPrintf("Unknown: %s\r\n", fname); + } } - else - Serial.printf("Unknown: %s\n\r", fname); - } + tempFile.close(); } - tempFile.close(); - } - - xSemaphoreGive(xFATSemaphore); } +#ifdef COMPILE_SD_MMC + else + { + const char *BIN_EXT = "bin"; + const char *BIN_HEADER = "/RTK_Surveyor_Firmware"; - } + char fname[60]; // Handle long file names + + File dir = SD_MMC.open("/"); // Open root + if (!dir || !dir.isDirectory()) + return; + + binCount = 0; // Reset count in case scanForFirmware is called again + + File tempFile = dir.openNextFile(); + while (tempFile && (binCount < maxBinFiles)) + { + if (!tempFile.isDirectory()) + { + snprintf(fname, sizeof(fname), "%s", tempFile.name()); + + if (strcmp(forceFirmwareFileName, fname) == 0) + { + systemPrintln("Forced firmware detected. Loading..."); + displayForcedFirmwareUpdate(); + updateFromSD(forceFirmwareFileName); + } + + // Check 'bin' extension + if (strcmp(BIN_EXT, &fname[strlen(fname) - strlen(BIN_EXT)]) == 0) + { + // Check for 'RTK_Surveyor_Firmware' start of file name + if (strncmp(fname, BIN_HEADER, strlen(BIN_HEADER)) == 0) + { + strncpy(binFileNames[binCount++], fname, sizeof(binFileNames[0]) - 1); // Add this to the array + } + else + systemPrintf("Unknown: %s\r\n", fname); + } + } + tempFile.close(); + tempFile = dir.openNextFile(); + } + } +#endif // COMPILE_SD_MMC } -//Look for firmware file on SD card and update as needed -void updateFromSD(char *firmwareFileName) +// Look for firmware file on SD card and update as needed +// Called from scanForFirmware with microSD card mounted and sdCardsemaphore held +// Called from mountSDThenUpdate with microSD card mounted and sdCardsemaphore held +void updateFromSD(const char *firmwareFileName) { - //Turn off any tasks so that we are not disrupted - stopWiFi(); - endBluetooth(); - - Serial.printf("Loading %s\n\r", firmwareFileName); - if (sd.exists(firmwareFileName)) - { - SdFile firmwareFile; + // Count app partitions + int appPartitions = 0; + esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_ANY, nullptr); + while (it != nullptr) + { + appPartitions++; + it = esp_partition_next(it); + } + + // We cannot do OTA if there is only one partition + if (appPartitions < 2) + { + systemPrintln( + "SD firmware updates are not available on 4MB devices. Please use the GUI or CLI update methods."); + return; + } + + // Turn off any tasks so that we are not disrupted + espnowStop(); + WIFI_STOP(); + bluetoothStop(); + + // Delete tasks if running + tasksStopUART2(); + + systemPrintf("Loading %s\r\n", firmwareFileName); + + if (USE_SPI_MICROSD) + { + if (!sd->exists(firmwareFileName)) + { + systemPrintln("No firmware file found"); + return; + } + } +#ifdef COMPILE_SD_MMC + else + { + if (!SD_MMC.exists(firmwareFileName)) + { + systemPrintln("No firmware file found"); + return; + } + } +#endif // COMPILE_SD_MMC + + FileSdFatMMC firmwareFile; + if (!firmwareFile) + { + systemPrintln("ERROR - Failed to allocate firmwareFile"); + return; + } firmwareFile.open(firmwareFileName, O_READ); - size_t updateSize = firmwareFile.fileSize(); + size_t updateSize = firmwareFile.size(); + if (updateSize == 0) { - Serial.println(F("Error: Binary is empty")); - firmwareFile.close(); - return; + systemPrintln("Error: Binary is empty"); + firmwareFile.close(); + return; } if (Update.begin(updateSize) == false) { - Serial.println(F("Update begin failed. Not enough partition space available.")); - firmwareFile.close(); - return; + systemPrintln("Update begin failed. Not enough partition space available."); + firmwareFile.close(); + return; } - Serial.println(F("Moving file to OTA section")); - Serial.print(F("Bytes to write: ")); - Serial.print(updateSize); + systemPrintln("Moving file to OTA section"); + systemPrint("Bytes to write: "); + systemPrint(updateSize); const int pageSize = 512 * 4; byte dataArray[pageSize]; int bytesWritten = 0; - //Indicate progress - int barWidthInCharacters = 20; //Width of progress bar, ie [###### % complete + // Indicate progress + int barWidthInCharacters = 20; // Width of progress bar, ie [###### % complete long portionSize = updateSize / barWidthInCharacters; int barWidth = 0; - //Bulk write from the SD file to the EEPROM + // Bulk write from the SD file to flash while (firmwareFile.available()) { - if (productVariant == RTK_SURVEYOR) - digitalWrite(pin_baseStatusLED, !digitalRead(pin_baseStatusLED)); //Toggle LED to indcate activity + if (productVariant == RTK_SURVEYOR) + digitalWrite(pin_baseStatusLED, !digitalRead(pin_baseStatusLED)); // Toggle LED to indcate activity - int bytesToWrite = pageSize; //Max number of bytes to read - if (firmwareFile.available() < bytesToWrite) bytesToWrite = firmwareFile.available(); //Trim this read size as needed + int bytesToWrite = pageSize; // Max number of bytes to read + if (firmwareFile.available() < bytesToWrite) + bytesToWrite = firmwareFile.available(); // Trim this read size as needed - firmwareFile.read(dataArray, bytesToWrite); //Read the next set of bytes from file into our temp array - delay(10); //Give RTOS time + firmwareFile.read(dataArray, bytesToWrite); // Read the next set of bytes from file into our temp array - if (Update.write(dataArray, bytesToWrite) != bytesToWrite) - { - Serial.println(F("\nWrite failed. Binary may be incorrectly aligned.")); - break; - } - else - bytesWritten += bytesToWrite; - delay(10); //Give RTOS time - - //Indicate progress - if (bytesWritten > barWidth * portionSize) - { - //Advance the bar - barWidth++; - Serial.print(F("\n[")); - for (int x = 0 ; x < barWidth ; x++) - Serial.print("="); - Serial.printf("%d%%", bytesWritten * 100 / updateSize); - if (bytesWritten == updateSize) Serial.println("]"); - } - } - Serial.println(F("\nFile move complete")); + if (Update.write(dataArray, bytesToWrite) != bytesToWrite) + { + systemPrintln("\nWrite failed. Binary may be incorrectly aligned."); + break; + } + else + bytesWritten += bytesToWrite; + + // Indicate progress + if (bytesWritten > barWidth * portionSize) + { + // Advance the bar + barWidth++; + systemPrint("\n["); + for (int x = 0; x < barWidth; x++) + systemPrint("="); + systemPrintf("%d%%", bytesWritten * 100 / updateSize); + if (bytesWritten == updateSize) + systemPrintln("]"); + + displayFirmwareUpdateProgress(bytesWritten * 100 / updateSize); + } + } + systemPrintln("\nFile move complete"); if (Update.end()) { - if (Update.isFinished()) - { - Serial.println(F("Firmware updated successfully. Rebooting. Good bye!")); + if (Update.isFinished()) + { + displayFirmwareUpdateProgress(100); - //If forced firmware is detected, do a full reset of config as well - if (strcmp(forceFirmwareFileName, firmwareFileName) == 0) + // Clear all settings from LittleFS + LittleFS.format(); + + systemPrintln("Firmware updated successfully. Rebooting. Goodbye!"); + + // If forced firmware is detected, do a full reset of config as well + if (strcmp(forceFirmwareFileName, firmwareFileName) == 0) + { + systemPrintln("Removing firmware file"); + + // Remove forced firmware file to prevent endless loading + firmwareFile.close(); + + if (USE_SPI_MICROSD) + sd->remove(firmwareFileName); +#ifdef COMPILE_SD_MMC + else + SD_MMC.remove(firmwareFileName); +#endif // COMPILE_SD_MMC + + theGNSS.factoryDefault(); // Reset everything: baud rate, I2C address, update rate, everything. And save + // to BBR. + } + + delay(1000); + ESP.restart(); + } + else + systemPrintln("Update not finished? Something went wrong!"); + } + else + { + systemPrint("Error Occurred. Error #: "); + systemPrintln(String(Update.getError())); + } + + firmwareFile.close(); + + displayMessage("Update Failed", 0); + + systemPrintln("Firmware update failed. Please try again."); +} + +// Format the firmware version +void formatFirmwareVersion(uint8_t major, uint8_t minor, char *buffer, int bufferLength, bool includeDate) +{ + char prefix; + + // Construct the full or release candidate version number + prefix = ENABLE_DEVELOPER ? 'd' : 'v'; + if (enableRCFirmware && (bufferLength >= 21)) + // 123456789012345678901 + // pxxx.yyy-dd-mmm-yyyy0 + snprintf(buffer, bufferLength, "%c%d.%d-%s", prefix, major, minor, __DATE__); + + // Construct a truncated version number + else if (bufferLength >= 9) + // 123456789 + // pxxx.yyy0 + snprintf(buffer, bufferLength, "%c%d.%d", prefix, major, minor); + + // The buffer is too small for the version number + else + { + systemPrintf("ERROR: Buffer too small for version number!\r\n"); + if (bufferLength > 0) + *buffer = 0; + } +} + +// Get the current firmware version +void getFirmwareVersion(char *buffer, int bufferLength, bool includeDate) +{ + formatFirmwareVersion(FIRMWARE_VERSION_MAJOR, FIRMWARE_VERSION_MINOR, buffer, bufferLength, includeDate); +} + +const char *otaGetUrl() +{ + // Select the URL for the over-the-air (OTA) updates + return enableRCFirmware ? OTA_RC_FIRMWARE_JSON_URL : OTA_FIRMWARE_JSON_URL; +} + +// Returns true if we successfully got the versionAvailable +// Modifies versionAvailable with OTA getVersion response +// Connects to WiFi as needed +bool otaCheckVersion(char *versionAvailable, uint8_t versionAvailableLength) +{ + bool gotVersion = false; +#ifdef COMPILE_WIFI + bool previouslyConnected = wifiIsConnected(); + + if (wifiConnect(10000) == true) + { + char versionString[21]; + getFirmwareVersion(versionString, sizeof(versionString), enableRCFirmware); + systemPrintf("Current firmware version: %s\r\n", versionString); + + const char *url = otaGetUrl(); + systemPrintf("Checking to see if an update is available from %s\r\n", url); + + ESP32OTAPull ota; + + int response = ota.CheckForOTAUpdate(url, versionString, ESP32OTAPull::DONT_DO_UPDATE); + + // We don't care if the library thinks the available firmware is newer, we just need a successful JSON parse + if (response == ESP32OTAPull::UPDATE_AVAILABLE || response == ESP32OTAPull::NO_UPDATE_AVAILABLE) + { + gotVersion = true; + + // Call getVersion after original inquiry + String otaVersion = ota.GetVersion(); + otaVersion.toCharArray(versionAvailable, versionAvailableLength); + } + else if (response == ESP32OTAPull::HTTP_FAILED) + { + systemPrintln("Firmware server not available"); + } + else + { + systemPrintln("OTA failed"); + } + } + else + { + systemPrintln("WiFi not available."); + } + + if (systemState != STATE_WIFI_CONFIG) + { + // WIFI_STOP() turns off the entire radio including the webserver. We need to turn off Station mode only. + // For now, unit exits AP mode via reset so if we are in AP config mode, leave WiFi Station running. + + // If WiFi was originally off, turn it off again + if (previouslyConnected == false) + WIFI_STOP(); + } + + if (gotVersion == true) + log_d("Available OTA firmware version: %s\r\n", versionAvailable); + +#endif // COMPILE_WIFI + return (gotVersion); +} + +// Force updates firmware using OTA pull +// Exits by either updating firmware and resetting, or failing to connect +void otaUpdate() +{ +#ifdef COMPILE_WIFI + bool previouslyConnected = wifiIsConnected(); + + if (wifiConnect(10000) == true) + { + char versionString[9]; + formatFirmwareVersion(0, 0, versionString, sizeof(versionString), false); + + ESP32OTAPull ota; + + int response; + const char *url = otaGetUrl(); + response = ota.CheckForOTAUpdate(url, &versionString[1], ESP32OTAPull::DONT_DO_UPDATE); + + if (response == ESP32OTAPull::UPDATE_AVAILABLE) { - Serial.println(F("Removing firmware file")); + systemPrintln("Installing new firmware"); + ota.SetCallback(otaPullCallback); + ota.CheckForOTAUpdate(url, &versionString[1]); // Install new firmware, no reset - //Remove forced firmware file to prevent endless loading - firmwareFile.close(); - sd.remove(firmwareFileName); + if (apConfigFirmwareUpdateInProcess) + { +#ifdef COMPILE_AP + // Tell AP page to display reset info + websocket->textAll("confirmReset,1,"); +#endif // COMPILE_AP + } + ESP.restart(); + } + else if (response == ESP32OTAPull::NO_UPDATE_AVAILABLE) + { + systemPrintln("OTA Update: Current firmware is up to date"); + } + else if (response == ESP32OTAPull::HTTP_FAILED) + { + systemPrintln("OTA Update: Firmware server not available"); + } + else + { + systemPrintln("OTA Update: OTA failed"); + } + } + else + { + systemPrintln("WiFi not available."); + } - eepromErase(); + // Update failed. If WiFi was originally off, turn it off again + if (previouslyConnected == false) + WIFI_STOP(); - //Assemble settings file name - char settingsFileName[40]; //SFE_Surveyor_Settings.txt - strcpy(settingsFileName, platformFilePrefix); - strcat(settingsFileName, "_Settings.txt"); +#endif // COMPILE_WIFI +} - if (sd.exists(settingsFileName)) - sd.remove(settingsFileName); +// Called while the OTA Pull update is happening +void otaPullCallback(int bytesWritten, int totalLength) +{ + otaDisplayPercentage(bytesWritten, totalLength, false); +} - i2cGNSS.factoryReset(); //Reset everything: baud rate, I2C address, update rate, everything. +void otaDisplayPercentage(int bytesWritten, int totalLength, bool alwaysDisplay) +{ + static int previousPercent = -1; + int percent = 100 * bytesWritten / totalLength; + if (alwaysDisplay || (percent != previousPercent)) + { + // Indicate progress + int barWidthInCharacters = 20; // Width of progress bar, ie [###### % complete + long portionSize = totalLength / barWidthInCharacters; + + // Indicate progress + systemPrint("\r\n["); + int barWidth = bytesWritten / portionSize; + for (int x = 0; x < barWidth; x++) + systemPrint("="); + systemPrintf(" %d%%", percent); + if (bytesWritten == totalLength) + systemPrintln("]"); + + displayFirmwareUpdateProgress(percent); + + if (apConfigFirmwareUpdateInProcess == true) + { +#ifdef COMPILE_AP + char myProgress[50]; + snprintf(myProgress, sizeof(myProgress), "otaFirmwareStatus,%d,", percent); + websocket->textAll(myProgress); +#endif // COMPILE_AP } - delay(1000); - ESP.restart(); - } - else - Serial.println(F("Update not finished? Something went wrong!")); + previousPercent = percent; + } +} + +const char *otaPullErrorText(int code) +{ +#ifdef COMPILE_WIFI + switch (code) + { + case ESP32OTAPull::UPDATE_AVAILABLE: + return "An update is available but wasn't installed"; + case ESP32OTAPull::NO_UPDATE_PROFILE_FOUND: + return "No profile matches"; + case ESP32OTAPull::NO_UPDATE_AVAILABLE: + return "Profile matched, but update not applicable"; + case ESP32OTAPull::UPDATE_OK: + return "An update was done, but no reboot"; + case ESP32OTAPull::HTTP_FAILED: + return "HTTP GET failure"; + case ESP32OTAPull::WRITE_ERROR: + return "Write error"; + case ESP32OTAPull::JSON_PROBLEM: + return "Invalid JSON"; + case ESP32OTAPull::OTA_UPDATE_FAIL: + return "Update fail (no OTA partition?)"; + default: + if (code > 0) + return "Unexpected HTTP response code"; + break; + } +#endif // COMPILE_WIFI + return "Unknown error"; +} + +// Returns true if reportedVersion is newer than currentVersion +// Version number comes in as v2.7-Jan 5 2023 +// 2.7-Jan 5 2023 is newer than v2.7-Jan 1 2023 +// We can't use just the float number: v3.12 is a greater version than v3.9 but it is a smaller float number +bool isReportedVersionNewer(char *reportedVersion, char *currentVersion) +{ + int currentVersionNumberMajor = 0; + int currentVersionNumberMinor = 0; + int currentDay = 0; + int currentMonth = 0; + int currentYear = 0; + + int reportedVersionNumberMajor = 0; + int reportedVersionNumberMinor = 0; + int reportedDay = 0; + int reportedMonth = 0; + int reportedYear = 0; + + breakVersionIntoParts(currentVersion, ¤tVersionNumberMajor, ¤tVersionNumberMinor, ¤tYear, + ¤tMonth, ¤tDay); + breakVersionIntoParts(reportedVersion, &reportedVersionNumberMajor, &reportedVersionNumberMinor, &reportedYear, + &reportedMonth, &reportedDay); + + log_d("currentVersion (%s): %d.%d %d %d %d", currentVersion, currentVersionNumberMajor, currentVersionNumberMinor, + currentYear, currentMonth, currentDay); + log_d("reportedVersion (%s): %d.%d %d %d %d", reportedVersion, reportedVersionNumberMajor, + reportedVersionNumberMinor, reportedYear, reportedMonth, reportedDay); + if (enableRCFirmware) + log_d("RC firmware enabled"); + + // Production firmware is named "2.6" + // Release Candidate firmware is named "2.6-Dec 5 2022" + + // If the user is not using Release Candidate firmware, then check only the version number + if (enableRCFirmware == false) + { + if (reportedVersionNumberMajor > currentVersionNumberMajor) + return (true); + if (reportedVersionNumberMajor == currentVersionNumberMajor && + reportedVersionNumberMinor > currentVersionNumberMinor) + return (true); + return (false); + } + + // For RC firmware, compare firmware date as well + // Check version number + if (reportedVersionNumberMajor > currentVersionNumberMajor) + return (true); + if (reportedVersionNumberMajor == currentVersionNumberMajor && + reportedVersionNumberMinor > currentVersionNumberMinor) + return (true); + + // Check which date is more recent + // https://stackoverflow.com/questions/5283120/date-comparison-to-find-which-is-bigger-in-c + int reportedVersionScore = reportedDay + reportedMonth * 100 + reportedYear * 2000; + int currentVersionScore = currentDay + currentMonth * 100 + currentYear * 2000; + + if (reportedVersionScore > currentVersionScore) + { + log_d("Reported version is greater"); + return (true); + } + + return (false); +} + +// Version number comes in as v2.7-Jan 5 2023 +// Given a char string, break into version number major/minor, year, month, day +// Returns false if parsing failed +bool breakVersionIntoParts(char *version, int *versionNumberMajor, int *versionNumberMinor, int *year, int *month, + int *day) +{ + char monthStr[20]; + int placed = 0; + + if (enableRCFirmware == false) + { + placed = sscanf(version, "%d.%d", versionNumberMajor, versionNumberMinor); + if (placed != 2) + { + log_d("Failed to sscanf basic"); + return (false); // Something went wrong + } } else { - Serial.print(F("Error Occurred. Error #: ")); - Serial.println(String(Update.getError())); + placed = sscanf(version, "%d.%d-%s %d %d", versionNumberMajor, versionNumberMinor, monthStr, day, year); + + if (placed != 5) + { + log_d("Failed to sscanf RC"); + return (false); // Something went wrong + } + + (*month) = mapMonthName(monthStr); + if (*month == -1) + return (false); // Something went wrong } - firmwareFile.close(); - } - else - { - Serial.println(F("No firmware file found")); - } + return (true); +} + +// https://stackoverflow.com/questions/21210319/assign-month-name-and-integer-values-from-string-using-sscanf +int mapMonthName(char *mmm) +{ + static char const *months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; + for (size_t i = 0; i < sizeof(months) / sizeof(months[0]); i++) + { + if (strcmp(mmm, months[i]) == 0) + return i + 1; + } + return -1; } diff --git a/Firmware/RTK_Surveyor/menuGNSS.ino b/Firmware/RTK_Surveyor/menuGNSS.ino index 9f553d3f6..b101f77b4 100644 --- a/Firmware/RTK_Surveyor/menuGNSS.ino +++ b/Firmware/RTK_Surveyor/menuGNSS.ino @@ -1,273 +1,476 @@ -//Configure the basic GNSS reception settings -//Update rate, constellations, etc +// Configure the basic GNSS reception settings +// Update rate, constellations, etc void menuGNSS() { - while (1) - { - Serial.println(); - Serial.println(F("Menu: GNSS Menu")); + restartRover = false; // If user modifies any NTRIP settings, we need to restart the rover - //Because we may be in base mode (always 1Hz), do not get freq from module, use settings instead - float measurementFrequency = (1000.0 / settings.measurementRate) / settings.navigationRate; + while (1) + { + int minCNO = settings.minCNO_F9P; + if (zedModuleType == PLATFORM_F9R) + minCNO = settings.minCNO_F9R; + + systemPrintln(); + systemPrintln("Menu: GNSS Receiver"); + + // Because we may be in base mode, do not get freq from module, use settings instead + float measurementFrequency = (1000.0 / settings.measurementRate) / settings.navigationRate; + + systemPrint("1) Set measurement rate in Hz: "); + systemPrintln(measurementFrequency, 5); + + systemPrint("2) Set measurement rate in seconds between measurements: "); + systemPrintln(1 / measurementFrequency, 5); + + systemPrintln("\tNote: The measurement rate is overridden to 1Hz when in Base mode."); + + systemPrint("3) Set dynamic model: "); + switch (settings.dynamicModel) + { + case DYN_MODEL_PORTABLE: + systemPrint("Portable"); + break; + case DYN_MODEL_STATIONARY: + systemPrint("Stationary"); + break; + case DYN_MODEL_PEDESTRIAN: + systemPrint("Pedestrian"); + break; + case DYN_MODEL_AUTOMOTIVE: + systemPrint("Automotive"); + break; + case DYN_MODEL_SEA: + systemPrint("Sea"); + break; + case DYN_MODEL_AIRBORNE1g: + systemPrint("Airborne 1g"); + break; + case DYN_MODEL_AIRBORNE2g: + systemPrint("Airborne 2g"); + break; + case DYN_MODEL_AIRBORNE4g: + systemPrint("Airborne 4g"); + break; + case DYN_MODEL_WRIST: + systemPrint("Wrist"); + break; + case DYN_MODEL_BIKE: + systemPrint("Bike"); + break; + case DYN_MODEL_MOWER: + systemPrint("Mower"); + break; + case DYN_MODEL_ESCOOTER: + systemPrint("E-Scooter"); + break; + default: + systemPrint("Unknown"); + break; + } + systemPrintln(); + + systemPrintln("4) Set Constellations"); + + systemPrint("5) Toggle NTRIP Client: "); + if (settings.enableNtripClient == true) + systemPrintln("Enabled"); + else + systemPrintln("Disabled"); - Serial.print(F("1) Set measurement rate in Hz: ")); - Serial.println(measurementFrequency, 4); + if (settings.enableNtripClient == true) + { + systemPrint("6) Set Caster Address: "); + systemPrintln(settings.ntripClient_CasterHost); - Serial.print(F("2) Set measurement rate in seconds between measurements: ")); - Serial.println(1 / measurementFrequency, 2); + systemPrint("7) Set Caster Port: "); + systemPrintln(settings.ntripClient_CasterPort); - Serial.print(F("3) Set dynamic model: ")); - switch (settings.dynamicModel) - { - case DYN_MODEL_PORTABLE: - Serial.print(F("Portable")); - break; - case DYN_MODEL_STATIONARY: - Serial.print(F("Stationary")); - break; - case DYN_MODEL_PEDESTRIAN: - Serial.print(F("Pedestrian")); - break; - case DYN_MODEL_AUTOMOTIVE: - Serial.print(F("Automotive")); - break; - case DYN_MODEL_SEA: - Serial.print(F("Sea")); - break; - case DYN_MODEL_AIRBORNE1g: - Serial.print(F("Airborne 1g")); - break; - case DYN_MODEL_AIRBORNE2g: - Serial.print(F("Airborne 2g")); - break; - case DYN_MODEL_AIRBORNE4g: - Serial.print(F("Airborne 4g")); - break; - case DYN_MODEL_WRIST: - Serial.print(F("Wrist")); - break; - case DYN_MODEL_BIKE: - Serial.print(F("Bike")); - break; - default: - Serial.print(F("Unknown")); - break; - } - Serial.println(); + systemPrint("8) Set Caster User Name: "); + systemPrintln(settings.ntripClient_CasterUser); - Serial.println(F("4) Set Constellations ")); + systemPrint("9) Set Caster User Password: "); + systemPrintln(settings.ntripClient_CasterUserPW); - Serial.println(F("x) Exit")); + systemPrint("10) Set Mountpoint: "); + systemPrintln(settings.ntripClient_MountPoint); - int incoming = getNumber(menuTimeout); //Timeout after x seconds + systemPrint("11) Set Mountpoint PW: "); + systemPrintln(settings.ntripClient_MountPointPW); - if (incoming == 1) - { - Serial.print(F("Enter GNSS measurement rate in Hz: ")); - double rate = getDouble(menuTimeout); //Timeout after x seconds - if (rate < 0.0 || rate > 20.0) //20Hz limit with all constellations enabled. - { - Serial.println(F("Error: measurement rate out of range")); - } - else - { - setMeasurementRates(1.0 / rate); //Convert Hz to seconds. This will set settings.measurementRate and settings.navigationRate - //Settings recorded to NVM and file at main menu exit - } + systemPrint("12) Toggle sending GGA Location to Caster: "); + if (settings.ntripClient_TransmitGGA == true) + systemPrintln("Enabled"); + else + systemPrintln("Disabled"); + + systemPrintf("13) Minimum elevation for a GNSS satellite to be used in fix (degrees): %d\r\n", + settings.minElev); + + systemPrintf("14) Minimum satellite signal level for navigation (dBHz): %d\r\n", minCNO); + } + else + { + systemPrintf("6) Minimum elevation for a GNSS satellite to be used in fix (degrees): %d\r\n", + settings.minElev); + + systemPrintf("7) Minimum satellite signal level for navigation (dBHz): %d\r\n", minCNO); + } + + systemPrintln("x) Exit"); + + int incoming = getNumber(); // Returns EXIT, TIMEOUT, or long + + if (incoming == 1) + { + systemPrint("Enter GNSS measurement rate in Hz: "); + double rate = getDouble(); + if (rate < 0.00012 || rate > 20.0) // 20Hz limit with all constellations enabled + { + systemPrintln("Error: Measurement rate out of range"); + } + else + { + setRate(1.0 / rate); // Convert Hz to seconds. This will set settings.measurementRate, + // settings.navigationRate, and GSV message + // Settings recorded to NVM and file at main menu exit + } + } + else if (incoming == 2) + { + systemPrint("Enter GNSS measurement rate in seconds between measurements: "); + float rate = getDouble(); + if (rate < 0.0 || rate > 8255.0) // Limit of 127 (navRate) * 65000ms (measRate) = 137 minute limit. + { + systemPrintln("Error: Measurement rate out of range"); + } + else + { + setRate(rate); // This will set settings.measurementRate, settings.navigationRate, and GSV message + // Settings recorded to NVM and file at main menu exit + } + } + else if (incoming == 3) + { + systemPrintln("Enter the dynamic model to use: "); + systemPrintln("1) Portable"); + systemPrintln("2) Stationary"); + systemPrintln("3) Pedestrian"); + systemPrintln("4) Automotive"); + systemPrintln("5) Sea"); + systemPrintln("6) Airborne 1g"); + systemPrintln("7) Airborne 2g"); + systemPrintln("8) Airborne 4g"); + systemPrintln("9) Wrist"); + if (zedModuleType == PLATFORM_F9R) + { + systemPrintln("10) Bike"); + // F9R versions starting at 1.21 have Mower and E-Scooter dynamic models + if (zedFirmwareVersionInt >= 121) + { + systemPrintln("11) Mower"); + systemPrintln("12) E-Scooter"); + } + } + + int dynamicModel = getNumber(); // Returns EXIT, TIMEOUT, or long + if ((dynamicModel != INPUT_RESPONSE_GETNUMBER_EXIT) && (dynamicModel != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + { + uint8_t maxModel = DYN_MODEL_WRIST; + + if (zedModuleType == PLATFORM_F9R) + { + maxModel = DYN_MODEL_BIKE; + // F9R versions starting at 1.21 have Mower and E-Scooter dynamic models + if (zedFirmwareVersionInt >= 121) + maxModel = DYN_MODEL_ESCOOTER; + } + + if (dynamicModel < 1 || dynamicModel > maxModel) + systemPrintln("Error: Dynamic model out of range"); + else + { + if (dynamicModel == 1) + settings.dynamicModel = DYN_MODEL_PORTABLE; // The enum starts at 0 and skips 1. + else + settings.dynamicModel = dynamicModel; // Recorded to NVM and file at main menu exit + + theGNSS.setVal8(UBLOX_CFG_NAVSPG_DYNMODEL, (dynModel)settings.dynamicModel); // Set dynamic model + } + } + } + else if (incoming == 4) + { + menuConstellations(); + } + else if (incoming == 5) + { + settings.enableNtripClient ^= 1; + restartRover = true; + } + else if ((incoming == 6) && settings.enableNtripClient == true) + { + systemPrint("Enter new Caster Address: "); + getString(settings.ntripClient_CasterHost, sizeof(settings.ntripClient_CasterHost)); + restartRover = true; + } + else if ((incoming == 7) && settings.enableNtripClient == true) + { + systemPrint("Enter new Caster Port: "); + + int ntripClient_CasterPort = getNumber(); // Returns EXIT, TIMEOUT, or long + if ((ntripClient_CasterPort != INPUT_RESPONSE_GETNUMBER_EXIT) && + (ntripClient_CasterPort != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + { + if (ntripClient_CasterPort < 1 || ntripClient_CasterPort > 99999) // Arbitrary 99k max port # + systemPrintln("Error: Caster port out of range"); + else + settings.ntripClient_CasterPort = + ntripClient_CasterPort; // Recorded to NVM and file at main menu exit + restartRover = true; + } + } + else if ((incoming == 8) && settings.enableNtripClient == true) + { + systemPrintf("Enter user name for %s: ", settings.ntripClient_CasterHost); + getString(settings.ntripClient_CasterUser, sizeof(settings.ntripClient_CasterUser)); + restartRover = true; + } + else if ((incoming == 9) && settings.enableNtripClient == true) + { + systemPrintf("Enter user password for %s: ", settings.ntripClient_CasterHost); + getString(settings.ntripClient_CasterUserPW, sizeof(settings.ntripClient_CasterUserPW)); + restartRover = true; + } + else if ((incoming == 10) && settings.enableNtripClient == true) + { + systemPrint("Enter new Mount Point: "); + getString(settings.ntripClient_MountPoint, sizeof(settings.ntripClient_MountPoint)); + restartRover = true; + } + else if ((incoming == 11) && settings.enableNtripClient == true) + { + systemPrintf("Enter password for Mount Point %s: ", settings.ntripClient_MountPoint); + getString(settings.ntripClient_MountPointPW, sizeof(settings.ntripClient_MountPointPW)); + restartRover = true; + } + else if ((incoming == 12) && settings.enableNtripClient == true) + { + settings.ntripClient_TransmitGGA ^= 1; + restartRover = true; + } + else if (((incoming == 13) && settings.enableNtripClient == true) || + incoming == 6 && settings.enableNtripClient == false) + { + systemPrint("Enter minimum elevation in degrees: "); + + int minElev = getNumber(); // Returns EXIT, TIMEOUT, or long + if ((minElev != INPUT_RESPONSE_GETNUMBER_EXIT) && (minElev != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + { + if (minElev <= 0 || minElev > 90) // Arbitrary 90 degree max + systemPrintln("Error: Minimum elevation out of range"); + else + { + settings.minElev = minElev; // Recorded to NVM and file at main menu exit + + theGNSS.setVal8(UBLOX_CFG_NAVSPG_INFIL_MINELEV, settings.minElev); // Set minimum elevation + } + restartRover = true; + } + } + else if (((incoming == 14) && settings.enableNtripClient == true) || + incoming == 7 && settings.enableNtripClient == false) + { + systemPrint("Enter minimum satellite signal level for navigation in dBHz: "); + + int newMinCNO = getNumber(); // Returns EXIT, TIMEOUT, or long + if ((newMinCNO != INPUT_RESPONSE_GETNUMBER_EXIT) && (newMinCNO != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + { + if (newMinCNO <= 0 || newMinCNO > 90) // Arbitrary 90 dBHz max + systemPrintln("Error: Minimum dBHz out of range"); + else + { + if (zedModuleType == PLATFORM_F9R) + settings.minCNO_F9R = newMinCNO; // Recorded to NVM and file at main menu exit + else + settings.minCNO_F9P = newMinCNO; + + theGNSS.setVal8(UBLOX_CFG_NAVSPG_INFIL_MINCNO, newMinCNO); // Update minCNO + } + restartRover = true; + } + } + else if (incoming == INPUT_RESPONSE_GETNUMBER_EXIT) + break; + else if (incoming == INPUT_RESPONSE_GETNUMBER_TIMEOUT) + break; + else + printUnknown(incoming); } - else if (incoming == 2) + + // Error check for RTK2Go without email in user name + // First force tolower the host name + char lowerHost[51]; + strncpy(lowerHost, settings.ntripClient_CasterHost, sizeof(lowerHost) - 1); + for (int x = 0; x < 50; x++) { - Serial.print(F("Enter GNSS measurement rate in seconds between measurements: ")); - double rate = getDouble(menuTimeout); //Timeout after x seconds - if (rate < 0.0 || rate > 8255.0) //Limit of 127 (navRate) * 65000ms (measRate) = 137 minute limit. - { - Serial.println(F("Error: measurement rate out of range")); - } - else - { - setMeasurementRates(rate); //This will set settings.measurementRate and settings.navigationRate - //Settings recorded to NVM and file at main menu exit - } + if (lowerHost[x] == '\0') + break; + if (lowerHost[x] >= 'A' && lowerHost[x] <= 'Z') + lowerHost[x] = lowerHost[x] - 'A' + 'a'; } - else if (incoming == 3) + + if (strncmp(lowerHost, "rtk2go.com", strlen("rtk2go.com")) == 0 || + strncmp(lowerHost, "www.rtk2go.com", strlen("www.rtk2go.com")) == 0) { - Serial.println(F("Enter the dynamic model to use: ")); - Serial.println(F("1) Portable")); - Serial.println(F("2) Stationary")); - Serial.println(F("3) Pedestrian")); - Serial.println(F("4) Automotive")); - Serial.println(F("5) Sea")); - Serial.println(F("6) Airborne 1g")); - Serial.println(F("7) Airborne 2g")); - Serial.println(F("8) Airborne 4g")); - Serial.println(F("9) Wrist")); - Serial.println(F("10) Bike")); - - int dynamicModel = getNumber(menuTimeout); //Timeout after x seconds - if (dynamicModel < 1 || dynamicModel > DYN_MODEL_BIKE) - Serial.println(F("Error: Dynamic model out of range")); - else - { - if (dynamicModel == 1) - settings.dynamicModel = DYN_MODEL_PORTABLE; //The enum starts at 0 and skips 1. - else - settings.dynamicModel = dynamicModel; //Recorded to NVM and file at main menu exit - } + // Rudamentary user name length check + if (strlen(settings.ntripClient_CasterUser) == 0) + { + systemPrintln("WARNING: RTK2Go requires that you use your email address as the mountpoint user name"); + delay(2000); + } } - else if (incoming == 4) + + clearBuffer(); // Empty buffer of any newline chars +} + +// Controls the constellations that are used to generate a fix and logged +void menuConstellations() +{ + while (1) { - menuConstellations(); + systemPrintln(); + systemPrintln("Menu: Constellations"); + + for (int x = 0; x < MAX_CONSTELLATIONS; x++) + { + systemPrintf("%d) Constellation %s: ", x + 1, settings.ubxConstellations[x].textName); + if (settings.ubxConstellations[x].enabled == true) + systemPrint("Enabled"); + else + systemPrint("Disabled"); + systemPrintln(); + } + + systemPrintln("x) Exit"); + + int incoming = getNumber(); // Returns EXIT, TIMEOUT, or long + + if (incoming >= 1 && incoming <= MAX_CONSTELLATIONS) + { + incoming--; // Align choice to constallation array of 0 to 5 + + settings.ubxConstellations[incoming].enabled ^= 1; + + // 3.10.6: To avoid cross-correlation issues, it is recommended that GPS and QZSS are always both enabled or + // both disabled. + if (incoming == SFE_UBLOX_GNSS_ID_GPS || incoming == 4) // QZSS ID is 5 but array location is 4 + { + settings.ubxConstellations[SFE_UBLOX_GNSS_ID_GPS].enabled = + settings.ubxConstellations[incoming].enabled; // GPS ID is 0 and array location is 0 + settings.ubxConstellations[4].enabled = + settings.ubxConstellations[incoming].enabled; // QZSS ID is 5 but array location is 4 + } + } + else if (incoming == INPUT_RESPONSE_GETNUMBER_EXIT) + break; + else if (incoming == INPUT_RESPONSE_GETNUMBER_TIMEOUT) + break; + else + printUnknown(incoming); } - else if (incoming == STATUS_PRESSED_X) - break; - else if (incoming == STATUS_GETNUMBER_TIMEOUT) - break; - else - printUnknown(incoming); - } - - int maxWait = 2000; - // Set dynamic model - if (i2cGNSS.getDynamicModel(maxWait) != settings.dynamicModel) - { - if (i2cGNSS.setDynamicModel((dynModel)settings.dynamicModel, maxWait) == false) - Serial.println(F("menuGNSS: setDynamicModel failed")); - } + // Apply current settings to module + setConstellations(true); // Apply newCfg and sendCfg values to batch - while (Serial.available()) Serial.read(); //Empty buffer of any newline chars + clearBuffer(); // Empty buffer of any newline chars } -//Controls the constellations that are used to generate a fix and logged -void menuConstellations() +// Given the number of seconds between desired solution reports, determine measurementRate and navigationRate +// measurementRate > 25 & <= 65535 +// navigationRate >= 1 && <= 127 +// We give preference to limiting a measurementRate to 30s or below due to reported problems with measRates above 30. +bool setRate(double secondsBetweenSolutions) { - while (1) - { - Serial.println(); - Serial.println(F("Menu: Constellations Menu")); + uint16_t measRate = 0; // Calculate these locally and then attempt to apply them to ZED at completion + uint16_t navRate = 0; - for (int x = 0 ; x < MAX_CONSTELLATIONS ; x++) + // If we have more than an hour between readings, increase mesaurementRate to near max of 65,535 + if (secondsBetweenSolutions > 3600.0) { - Serial.printf("%d) Constellation %s: ", x + 1, ubxConstellations[x].textName); - if (ubxConstellations[x].enabled == true) - Serial.print("Enabled"); - else - Serial.print("Disabled"); - Serial.println(); + measRate = 65000; } - Serial.println(F("x) Exit")); - - int incoming = getNumber(menuTimeout); //Timeout after x seconds - - if (incoming >= 1 && incoming <= MAX_CONSTELLATIONS) + // If we have more than 30s, but less than 3600s between readings, use 30s measurement rate + else if (secondsBetweenSolutions > 30.0) { - ubxConstellations[incoming - 1].enabled ^= 1; - - //3.10.6: To avoid cross-correlation issues, it is recommended that GPS and QZSS are always both enabled or both disabled. - if((incoming - 1) == SFE_UBLOX_GNSS_ID_GPS || (incoming - 1) == SFE_UBLOX_GNSS_ID_QZSS) - { - ubxConstellations[SFE_UBLOX_GNSS_ID_GPS].enabled = ubxConstellations[incoming - 1].enabled; - ubxConstellations[SFE_UBLOX_GNSS_ID_QZSS].enabled = ubxConstellations[incoming - 1].enabled; - } + measRate = 30000; } - else if (incoming == STATUS_PRESSED_X) - break; - else if (incoming == STATUS_GETNUMBER_TIMEOUT) - break; + + // User wants measurements less than 30s (most common), set measRate to match user request + // This will make navRate = 1. else - printUnknown(incoming); - } + { + measRate = secondsBetweenSolutions * 1000.0; + } - //Apply current settings to module - configureConstellations(); + navRate = secondsBetweenSolutions * 1000.0 / measRate; // Set navRate to nearest int value + measRate = secondsBetweenSolutions * 1000.0 / navRate; // Adjust measurement rate to match actual navRate - while (Serial.available()) Serial.read(); //Empty buffer of any newline chars -} + // systemPrintf("measurementRate / navRate: %d / %d\r\n", measRate, navRate); -//Given the number of seconds between desired solution reports, determine measurementRate and navigationRate -//measurementRate > 25 & <= 65535 -//navigationRate >= 1 && <= 127 -//We give preference to limiting a measurementRate to 30s or below due to reported problems with measRates above 30. -void setMeasurementRates(float secondsBetweenSolutions) -{ - uint16_t measRate = 0; //Calculate these locally and then attempt to apply them to ZED at completion - uint16_t navRate = 0; - - //If we have more than an hour between readings, increase mesaurementRate to near max of 65,535 - if (secondsBetweenSolutions > 3600.0) - { - measRate = 65000; - } - - //If we have more than 30s, but less than 3600s between readings, use 30s measurement rate - else if (secondsBetweenSolutions > 30.0) - { - measRate = 30000; - } - - //User wants measurements less than 30s (most common), set measRate to match user request - //This will make navRate = 1. - else - { - measRate = secondsBetweenSolutions * 1000.0; - } - - navRate = secondsBetweenSolutions * 1000.0 / measRate; //Set navRate to nearest int value - measRate = secondsBetweenSolutions * 1000.0 / navRate; //Adjust measurement rate to match actual navRate - - //Serial.printf("measurementRate / navRate: %d / %d\n\r", measRate, navRate); - - //If we successfully set rates, only then record to settings - if (i2cGNSS.setMeasurementRate(measRate) == true && i2cGNSS.setNavigationRate(navRate) == true) - { - settings.measurementRate = measRate; - settings.navigationRate = navRate; - } - else - { - Serial.println(F("menuGNSS: Failed to set measurement and navigation rates")); - } -} + bool response = true; + response &= theGNSS.newCfgValset(); + response &= theGNSS.addCfgValset(UBLOX_CFG_RATE_MEAS, measRate); + response &= theGNSS.addCfgValset(UBLOX_CFG_RATE_NAV, navRate); -//We need to know our overall measurement frequency for things like setting the GSV NMEA sentence rate. -//This returns a float of the rate based on settings that is the readings per second (Hz). -float getMeasurementFrequency() -{ - uint16_t currentMeasurementRate = i2cGNSS.getMeasurementRate(); - uint16_t currentNavigationRate = i2cGNSS.getNavigationRate(); + int gsvRecordNumber = getMessageNumberByName("UBX_NMEA_GSV"); + + // If enabled, adjust GSV NMEA to be reported at 1Hz to avoid swamping SPP connection + if (settings.ubxMessageRates[gsvRecordNumber] > 0) + { + float measurementFrequency = (1000.0 / measRate) / navRate; + if (measurementFrequency < 1.0) + measurementFrequency = 1.0; + + log_d("Adjusting GSV setting to %f", measurementFrequency); - currentNavigationRate = i2cGNSS.getNavigationRate(); - //The ZED-F9P will report an incorrect nav rate if we have rececently changed it. - //Reading a second time insures a correct read. + setMessageRateByName("UBX_NMEA_GSV", measurementFrequency); // Update GSV setting in file + response &= theGNSS.addCfgValset(ubxMessages[gsvRecordNumber].msgConfigKey, + settings.ubxMessageRates[gsvRecordNumber]); // Update rate on module + } - //Serial.printf("currentMeasurementRate / currentNavigationRate: %d / %d\n\r", currentMeasurementRate, currentNavigationRate); + response &= theGNSS.sendCfgValset(); // Closing value - max 4 pairs - float measurementFrequency = (1000.0 / currentMeasurementRate) / currentNavigationRate; - return (measurementFrequency); + // If we successfully set rates, only then record to settings + if (response == true) + { + settings.measurementRate = measRate; + settings.navigationRate = navRate; + } + else + { + systemPrintln("Failed to set measurement and navigation rates"); + return (false); + } + + return (true); +} + +// Print the module type and firmware version +void printZEDInfo() +{ + if (zedModuleType == PLATFORM_F9P) + systemPrintf("ZED-F9P firmware: %s\r\n", zedFirmwareVersion); + else if (zedModuleType == PLATFORM_F9R) + systemPrintf("ZED-F9R firmware: %s\r\n", zedFirmwareVersion); + else + // This will never be printed as beginGNSS defaults zedModuleType to PLATFORM_F9P + systemPrintf("Unknown module with firmware: %s\r\n", zedFirmwareVersion); } -//Updates the enabled constellations -bool configureConstellations() +// Print the NEO firmware version +void printNEOInfo() { - bool response = true; - - //long startTime = millis(); - for (int x = 0 ; x < MAX_CONSTELLATIONS ; x++) - { - //Standard UBX protocol method takes ~533-783ms - uint8_t currentlyEnabled = getConstellation(ubxConstellations[x].gnssID); //Qeury the module for the current setting - if (currentlyEnabled != ubxConstellations[x].enabled) - response &= setConstellation(ubxConstellations[x].gnssID, ubxConstellations[x].enabled); - - //Get/set val method takes ~642ms but does not work because we don't send additional sigCfg keys at same time - // uint8_t currentlyEnabled = i2cGNSS.getVal8(ubxConstellations[x].configKey, VAL_LAYER_RAM, 1200); - // if (currentlyEnabled != ubxConstellations[x].enabled) - // response &= i2cGNSS.setVal(ubxConstellations[x].configKey, ubxConstellations[x].enabled); - } - //long stopTime = millis(); - - //Serial.printf("setConstellation time delta: %ld ms\n\r", stopTime - startTime); - - return (response); + if (productVariant == RTK_FACET_LBAND || productVariant == RTK_FACET_LBAND_DIRECT) + systemPrintf("NEO-D9S firmware: %s\r\n", neoFirmwareVersion); } diff --git a/Firmware/RTK_Surveyor/menuMain.ino b/Firmware/RTK_Surveyor/menuMain.ino index 31125739b..aa498b6b1 100644 --- a/Firmware/RTK_Surveyor/menuMain.ino +++ b/Firmware/RTK_Surveyor/menuMain.ino @@ -1,123 +1,533 @@ -//Display the options -//If user doesn't respond within a few seconds, return to main loop +// Check to see if we've received serial over USB +// Report status if ~ received, otherwise present config menu +void updateSerial() +{ + if (systemAvailable()) + { + byte incoming = systemRead(); + + if (incoming == '~') + { + // Output custom GNTXT message with all current system data + printCurrentConditionsNMEA(); + } + else + menuMain(); // Present user menu + } +} + +// Display the options +// If user doesn't respond within a few seconds, return to main loop void menuMain() { - displaySerialConfig(); //Display 'Serial Config' while user is configuring + inMainMenu = true; + displaySerialConfig(); // Display 'Serial Config' while user is configuring + + while (1) + { + systemPrintln(); + char versionString[21]; + getFirmwareVersion(versionString, sizeof(versionString), true); + systemPrintf("SparkFun RTK %s %s\r\n", platformPrefix, versionString); + +#ifdef COMPILE_BT + + if (settings.bluetoothRadioType == BLUETOOTH_RADIO_SPP) + systemPrint("** Bluetooth SPP broadcasting as: "); + else if (settings.bluetoothRadioType == BLUETOOTH_RADIO_BLE) + systemPrint("** Bluetooth Low-Energy broadcasting as: "); + systemPrint(deviceName); + systemPrintln(" **"); +#else // COMPILE_BT + systemPrintln("** Bluetooth Not Compiled **"); +#endif // COMPILE_BT + + systemPrintln("Menu: Main"); + + systemPrintln("1) Configure GNSS Receiver"); - while (1) - { - Serial.println(); - Serial.printf("SparkFun RTK %s v%d.%d-%s\r\n", platformPrefix, FIRMWARE_VERSION_MAJOR, FIRMWARE_VERSION_MINOR, __DATE__); + systemPrintln("2) Configure GNSS Messages"); - Serial.print(F("** Bluetooth broadcasting as: ")); - Serial.print(deviceName); - Serial.println(F(" **")); + if (zedModuleType == PLATFORM_F9P) + systemPrintln("3) Configure Base"); + else if (zedModuleType == PLATFORM_F9R) + systemPrintln("3) Configure Sensor Fusion"); - Serial.println(F("Menu: Main Menu")); + systemPrintln("4) Configure Ports"); - Serial.println(F("1) Configure GNSS Receiver")); + systemPrintln("5) Configure Logging"); - Serial.println(F("2) Configure GNSS Messages")); +#ifdef COMPILE_WIFI + systemPrintln("6) Configure WiFi"); +#else // COMPILE_WIFI + systemPrintln("6) **WiFi Not Compiled**"); +#endif // COMPILE_WIFI - Serial.println(F("3) Configure Base")); +#if COMPILE_NETWORK + systemPrintln("7) Configure Network"); +#else // COMPILE_NETWORK + systemPrintln("7) **Network Not Compiled**"); +#endif // COMPILE_NETWORK +#ifdef COMPILE_ETHERNET + if (HAS_ETHERNET) + { + systemPrintln("e) Configure Ethernet"); + systemPrintln("n) Configure NTP"); + } +#endif // COMPILE_ETHERNET + + systemPrintln("p) Configure User Profiles"); + +#ifdef COMPILE_ESPNOW + systemPrintln("r) Configure Radios"); +#else // COMPILE_ESPNOW + systemPrintln("r) **ESP-Now Not Compiled**"); +#endif // COMPILE_ESPNOW + + if (online.lband == true) + systemPrintln("P) Configure PointPerfect"); + + systemPrintln("s) Configure System"); + + systemPrintln("f) Firmware upgrade"); + + if (btPrintEcho) + systemPrintln("b) Exit Bluetooth Echo mode"); + + systemPrintln("x) Exit"); + + byte incoming = getCharacterNumber(); + + if (incoming == 1) + menuGNSS(); + else if (incoming == 2) + menuMessages(); + else if (incoming == 3 && zedModuleType == PLATFORM_F9P) + menuBase(); + else if (incoming == 3 && zedModuleType == PLATFORM_F9R) + menuSensorFusion(); + else if (incoming == 4) + menuPorts(); + else if (incoming == 5) + menuLog(); + else if (incoming == 6) + menuWiFi(); + else if (incoming == 7) + menuNetwork(); + else if (incoming == 'e' && (HAS_ETHERNET)) + menuEthernet(); + else if (incoming == 'n' && (HAS_ETHERNET)) + menuNTP(); + else if (incoming == 's') + menuSystem(); + else if (incoming == 'p') + menuUserProfiles(); + else if (incoming == 'P' && online.lband == true) + menuPointPerfect(); +#ifdef COMPILE_ESPNOW + else if (incoming == 'r') + menuRadio(); +#endif // COMPILE_ESPNOW + else if (incoming == 'f') + menuFirmware(); + else if (incoming == 'b') + { + printEndpoint = PRINT_ENDPOINT_SERIAL; + systemPrintln("BT device has exited echo mode"); + btPrintEcho = false; + break; // Exit config menu + } + else if (incoming == 'x') + break; + else if (incoming == INPUT_RESPONSE_GETCHARACTERNUMBER_EMPTY) + break; + else if (incoming == INPUT_RESPONSE_GETCHARACTERNUMBER_TIMEOUT) + break; + else + printUnknown(incoming); + } - Serial.println(F("4) Configure Ports")); + recordSystemSettings(); // Once all menus have exited, record the new settings to LittleFS and config file - Serial.println(F("5) Configure Logging")); + if (online.gnss == true) + theGNSS.saveConfiguration(); // Save the current settings to flash and BBR on the ZED-F9P - if (settings.enableSD == true && online.microSD == true) + // Reboot as base only if currently operating as a base station + if (restartBase && (systemState >= STATE_BASE_NOT_STARTED) && (systemState < STATE_BUBBLE_LEVEL)) { - Serial.println(F("6) Display microSD contents")); + restartBase = false; + requestChangeState(STATE_BASE_NOT_STARTED); // Restart base upon exit for latest changes to take effect } - if (online.accelerometer == true) - Serial.println(F("b) Bubble Level")); + if (restartRover == true) + { + restartRover = false; + requestChangeState(STATE_ROVER_NOT_STARTED); // Restart rover upon exit for latest changes to take effect + } + + clearBuffer(); // Empty buffer of any newline chars + btPrintEchoExit = false; // We are out of the menu system + inMainMenu = false; +} + +// Change system wide settings based on current user profile +// Ways to change the ZED settings: +// Menus - we apply ZED changes at the exit of each sub menu +// Settings file - we detect differences between NVM and settings txt file and updateZEDSettings = true +// Profile - Before profile is changed, set updateZEDSettings = true +// AP - once new settings are parsed, set updateZEDSettings = true +// Setup button - +// Factory reset - updatesZEDSettings = true by default +void menuUserProfiles() +{ + uint8_t originalProfileNumber = profileNumber; + + bool forceReset = + false; // If we reset a profile to default, the profile number has not changed, but we still need to reset + + while (1) + { + systemPrintln(); + systemPrintln("Menu: User Profiles"); + + // List available profiles + for (int x = 0; x < MAX_PROFILE_COUNT; x++) + { + if (activeProfiles & (1 << x)) + systemPrintf("%d) Select %s", x + 1, profileNames[x]); + else + systemPrintf("%d) Select (Empty)", x + 1); + + if (x == profileNumber) + systemPrint(" <- Current"); + + systemPrintln(); + } + + systemPrintf("%d) Edit profile name: %s\r\n", MAX_PROFILE_COUNT + 1, profileNames[profileNumber]); + + systemPrintf("%d) Set profile '%s' to factory defaults\r\n", MAX_PROFILE_COUNT + 2, + profileNames[profileNumber]); + + systemPrintf("%d) Delete profile '%s'\r\n", MAX_PROFILE_COUNT + 3, profileNames[profileNumber]); + + systemPrintln("x) Exit"); + + int incoming = getNumber(); // Returns EXIT, TIMEOUT, or long + + if (incoming >= 1 && incoming <= MAX_PROFILE_COUNT) + { + changeProfileNumber(incoming - 1); // Align inputs to array + } + else if (incoming == MAX_PROFILE_COUNT + 1) + { + systemPrint("Enter new profile name: "); + getString(settings.profileName, sizeof(settings.profileName)); + recordSystemSettings(); // We need to update this immediately in case user lists the available profiles + // again + setProfileName(profileNumber); + } + else if (incoming == MAX_PROFILE_COUNT + 2) + { + systemPrintf("\r\nReset profile '%s' to factory defaults. Press 'y' to confirm:", + profileNames[profileNumber]); + byte bContinue = getCharacterNumber(); + if (bContinue == 'y') + { + settingsToDefaults(); // Overwrite our current settings with defaults + + recordSystemSettings(); // Overwrite profile file and NVM with these settings + + // Get bitmask of active profiles + activeProfiles = loadProfileNames(); - Serial.println(F("d) Configure Debug")); + forceReset = true; // Upon exit of menu, reset the device + } + else + systemPrintln("Reset aborted"); + } + else if (incoming == MAX_PROFILE_COUNT + 3) + { + systemPrintf("\r\nDelete profile '%s'. Press 'y' to confirm:", profileNames[profileNumber]); + byte bContinue = getCharacterNumber(); + if (bContinue == 'y') + { + // Remove profile from LittleFS + if (LittleFS.exists(settingsFileName)) + LittleFS.remove(settingsFileName); + + // Remove profile from SD if available + if (online.microSD == true) + { + if (USE_SPI_MICROSD) + { + if (sd->exists(settingsFileName)) + sd->remove(settingsFileName); + } +#ifdef COMPILE_SD_MMC + else + { + if (SD_MMC.exists(settingsFileName)) + SD_MMC.remove(settingsFileName); + } +#endif // COMPILE_SD_MMC + } - Serial.println(F("r) Reset all settings to default")); + recordProfileNumber(0); // Move to Profile1 + profileNumber = 0; - if (binCount > 0) - Serial.println(F("f) Firmware upgrade")); + snprintf(settingsFileName, sizeof(settingsFileName), "/%s_Settings_%d.txt", platformFilePrefix, + profileNumber); // Update file name with new profileNumber - //Serial.println(F("t) Test menu")); + // We need to load these settings from file so that we can record a profile name change correctly + bool responseLFS = loadSystemSettingsFromFileLFS(settingsFileName, &settings); + bool responseSD = loadSystemSettingsFromFileSD(settingsFileName, &settings); - Serial.println(F("x) Exit")); + // If this is an empty/new profile slot, overwrite our current settings with defaults + if (responseLFS == false && responseSD == false) + { + settingsToDefaults(); + } - byte incoming = getByteChoice(menuTimeout); //Timeout after x seconds + // Get bitmask of active profiles + activeProfiles = loadProfileNames(); + } + else + systemPrintln("Delete aborted"); + } + + else if (incoming == INPUT_RESPONSE_GETNUMBER_EXIT) + break; + else if (incoming == INPUT_RESPONSE_GETNUMBER_TIMEOUT) + break; + else + printUnknown(incoming); + } - if (incoming == '1') - menuGNSS(); - else if (incoming == '2') - menuMessages(); - else if (incoming == '3') - menuBase(); - else if (incoming == '4') - menuPorts(); - else if (incoming == '5') - menuLog(); - else if (incoming == '6' && settings.enableSD == true && online.microSD == true) + if (originalProfileNumber != profileNumber || forceReset == true) { - //Attempt to write to file system. This avoids collisions with file writing from other functions like recordSystemSettingsToFile() and F9PSerialReadTask() - if (xSemaphoreTake(xFATSemaphore, fatSemaphore_longWait_ms) == pdPASS) - { - Serial.println(F("Files found (date time size name):\n\r")); - sd.ls(LS_R | LS_DATE | LS_SIZE); - - xSemaphoreGive(xFATSemaphore); - } + systemPrintln("Rebooting to apply new profile settings. Goodbye!"); + delay(2000); + ESP.restart(); } - else if (incoming == 'd') - menuDebug(); - else if (incoming == 'r') + + // A user may edit the name of a profile, but then switch back to original profile. + // Thus, no reset, and activeProfiles is not updated. Do it here. + // Get bitmask of active profiles + activeProfiles = loadProfileNames(); + + clearBuffer(); // Empty buffer of any newline chars +} + +// Change the active profile number, without unit reset +void changeProfileNumber(byte newProfileNumber) +{ + settings.updateZEDSettings = true; // When this profile is loaded next, force system to update ZED settings. + recordSystemSettings(); // Before switching, we need to record the current settings to LittleFS and SD + + recordProfileNumber(newProfileNumber); + profileNumber = newProfileNumber; + setSettingsFileName(); // Load the settings file name into memory (enabled profile name delete) + + // We need to load these settings from file so that we can record a profile name change correctly + bool responseLFS = loadSystemSettingsFromFileLFS(settingsFileName, &settings); + bool responseSD = loadSystemSettingsFromFileSD(settingsFileName, &settings); + + // If this is an empty/new profile slot, overwrite our current settings with defaults + if (responseLFS == false && responseSD == false) { - Serial.println(F("\r\nResetting to factory defaults. Press 'y' to confirm:")); - byte bContinue = getByteChoice(menuTimeout); - if (bContinue == 'y') - { - eepromErase(); - - //Assemble settings file name - char settingsFileName[40]; //SFE_Surveyor_Settings.txt - strcpy(settingsFileName, platformFilePrefix); - strcat(settingsFileName, "_Settings.txt"); - - //Attempt to write to file system. This avoids collisions with file writing from other functions like recordSystemSettingsToFile() and F9PSerialReadTask() - if (xSemaphoreTake(xFATSemaphore, fatSemaphore_longWait_ms) == pdPASS) - { - if (sd.exists(settingsFileName)) - sd.remove(settingsFileName); - xSemaphoreGive(xFATSemaphore); - } //End xFATSemaphore - - i2cGNSS.factoryReset(); //Reset everything: baud rate, I2C address, update rate, everything. - - Serial.println(F("Settings erased. Please reset RTK Surveyor. Freezing.")); - while (1) - delay(1); //Prevent CPU freakout - } - else - Serial.println(F("Reset aborted")); + systemPrintln("No profile found: Applying default settings"); + settingsToDefaults(); } - else if (incoming == 'f' && binCount > 0) - menuFirmware(); - else if (incoming == 't') - menuTest(); - else if (incoming == 'b') +} + +// Erase all settings. Upon restart, unit will use defaults +void factoryReset(bool alreadyHasSemaphore) +{ + displaySytemReset(); // Display friendly message on OLED + + tasksStopUART2(); + + // Attempt to write to file system. This avoids collisions with file writing from other functions like + // recordSystemSettingsToFile() and F9PSerialReadTask() if (settings.enableSD && online.microSD) + //Don't check settings.enableSD - it could be corrupt + if (online.microSD) { - if (online.accelerometer == true) menuBubble(); + if (alreadyHasSemaphore == true || xSemaphoreTake(sdCardSemaphore, fatSemaphore_longWait_ms) == pdPASS) + { + if (USE_SPI_MICROSD) + { + // Remove this specific settings file. Don't remove the other profiles. + sd->remove(settingsFileName); + + sd->remove(stationCoordinateECEFFileName); // Remove station files + sd->remove(stationCoordinateGeodeticFileName); + } +#ifdef COMPILE_SD_MMC + else + { + SD_MMC.remove(settingsFileName); + + SD_MMC.remove(stationCoordinateECEFFileName); // Remove station files + SD_MMC.remove(stationCoordinateGeodeticFileName); + } +#endif // COMPILE_SD_MMC + + xSemaphoreGive(sdCardSemaphore); + } // End sdCardSemaphore + else + { + char semaphoreHolder[50]; + getSemaphoreFunction(semaphoreHolder); + + // An error occurs when a settings file is on the microSD card and it is not + // deleted, as such the settings on the microSD card will be loaded when the + // RTK reboots, resulting in failure to achieve the factory reset condition + log_d("sdCardSemaphore failed to yield, held by %s, menuMain.ino line %d\r\n", semaphoreHolder, + __LINE__); + } } - else if (incoming == 'x') - break; - else if (incoming == STATUS_GETBYTE_TIMEOUT) - break; - else - printUnknown(incoming); - } - recordSystemSettings(); //Once all menus have exited, record the new settings to EEPROM and config file + systemPrintln("Formatting internal file system..."); + LittleFS.format(); + + if (online.gnss == true) + { + systemPrintln("Factory resetting the GNSS receiver..."); + theGNSS.factoryDefault(); // Reset everything: baud rate, I2C address, update rate, everything. And save to BBR. + theGNSS.saveConfiguration(); + theGNSS.hardReset(); // Perform a reset leading to a cold start (zero info start-up) + } + + systemPrintln("Settings erased successfully. Rebooting. Goodbye!"); + delay(2000); + ESP.restart(); +} + +// Configure the internal radio, if available +void menuRadio() +{ +#ifdef COMPILE_ESPNOW + while (1) + { + systemPrintln(); + systemPrintln("Menu: Radios"); + + systemPrint("1) Select Radio Type: "); + if (settings.radioType == RADIO_EXTERNAL) + systemPrintln("External only"); + else if (settings.radioType == RADIO_ESPNOW) + systemPrintln("Internal ESP-Now"); + + if (settings.radioType == RADIO_ESPNOW) + { + // Pretty print the MAC of all radios + systemPrint(" Radio MAC: "); + for (int x = 0; x < 5; x++) + systemPrintf("%02X:", wifiMACAddress[x]); + systemPrintf("%02X\r\n", wifiMACAddress[5]); + + if (settings.espnowPeerCount > 0) + { + systemPrintln(" Paired Radios: "); + for (int x = 0; x < settings.espnowPeerCount; x++) + { + systemPrint(" "); + for (int y = 0; y < 5; y++) + systemPrintf("%02X:", settings.espnowPeers[x][y]); + systemPrintf("%02X\r\n", settings.espnowPeers[x][5]); + } + } + else + systemPrintln(" No Paired Radios"); + + systemPrintln("2) Pair radios"); + systemPrintln("3) Forget all radios"); + if (ENABLE_DEVELOPER) + { + systemPrintln("4) Add dummy radio"); + systemPrintln("5) Send dummy data"); + systemPrintln("6) Broadcast dummy data"); + } + } + + systemPrintln("x) Exit"); + + int incoming = getNumber(); // Returns EXIT, TIMEOUT, or long + + if (incoming == 1) + { + if (settings.radioType == RADIO_EXTERNAL) + settings.radioType = RADIO_ESPNOW; + else if (settings.radioType == RADIO_ESPNOW) + settings.radioType = RADIO_EXTERNAL; + } + else if (settings.radioType == RADIO_ESPNOW && incoming == 2) + { + espnowStaticPairing(); + } + else if (settings.radioType == RADIO_ESPNOW && incoming == 3) + { + systemPrintln("\r\nForgetting all paired radios. Press 'y' to confirm:"); + byte bContinue = getCharacterNumber(); + if (bContinue == 'y') + { + if (espnowState > ESPNOW_OFF) + { + for (int x = 0; x < settings.espnowPeerCount; x++) + espnowRemovePeer(settings.espnowPeers[x]); + } + settings.espnowPeerCount = 0; + systemPrintln("Radios forgotten"); + } + } + else if (ENABLE_DEVELOPER && settings.radioType == RADIO_ESPNOW && incoming == 4) + { + uint8_t peer1[] = {0xB8, 0xD6, 0x1A, 0x0D, 0x8F, 0x9C}; // Random MAC + if (esp_now_is_peer_exist(peer1) == true) + log_d("Peer already exists"); + else + { + // Add new peer to system + espnowAddPeer(peer1); + + // Record this MAC to peer list + memcpy(settings.espnowPeers[settings.espnowPeerCount], peer1, 6); + settings.espnowPeerCount++; + settings.espnowPeerCount %= ESPNOW_MAX_PEERS; + recordSystemSettings(); + } + + espnowSetState(ESPNOW_PAIRED); + } + else if (ENABLE_DEVELOPER && settings.radioType == RADIO_ESPNOW && incoming == 5) + { + uint8_t espnowData[] = + "This is the long string to test how quickly we can send one string to the other unit. I am going to " + "need a much longer sentence if I want to get a long amount of data into one transmission. This is " + "nearing 200 characters but needs to be near 250."; + esp_now_send(0, (uint8_t *)&espnowData, sizeof(espnowData)); // Send packet to all peers + } + else if (ENABLE_DEVELOPER && settings.radioType == RADIO_ESPNOW && incoming == 6) + { + uint8_t espnowData[] = + "This is the long string to test how quickly we can send one string to the other unit. I am going to " + "need a much longer sentence if I want to get a long amount of data into one transmission. This is " + "nearing 200 characters but needs to be near 250."; + uint8_t broadcastMac[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + esp_now_send(broadcastMac, (uint8_t *)&espnowData, sizeof(espnowData)); // Send packet to all peers + } + + else if (incoming == INPUT_RESPONSE_GETNUMBER_EXIT) + break; + else if (incoming == INPUT_RESPONSE_GETNUMBER_TIMEOUT) + break; + else + printUnknown(incoming); + } - i2cGNSS.saveConfiguration(); //Save the current settings to flash and BBR on the ZED-F9P + radioStart(); - while (Serial.available()) Serial.read(); //Empty buffer of any newline chars + clearBuffer(); // Empty buffer of any newline chars +#endif // COMPILE_ESPNOW } diff --git a/Firmware/RTK_Surveyor/menuMessages.ino b/Firmware/RTK_Surveyor/menuMessages.ino index cfe84c065..75448f724 100644 --- a/Firmware/RTK_Surveyor/menuMessages.ino +++ b/Firmware/RTK_Surveyor/menuMessages.ino @@ -1,521 +1,1144 @@ -//Control the messages that get logged to SD -//Control max logging time (limit to a certain number of minutes) -//The main use case is the setup for a base station to log RAW sentences that then get post processed +// Control the messages that get logged to SD +// Control max logging time (limit to a certain number of minutes) +// The main use case is the setup for a base station to log RAW sentences that then get post processed void menuLog() { - while (1) - { - Serial.println(); - Serial.println(F("Menu: Logging Menu")); - - if (settings.enableSD && online.microSD) - Serial.println(F("microSD card is online")); - else + while (1) { - beginSD(); //Test if SD is present - if (online.microSD == true) - Serial.println(F("microSD card online")); - else - Serial.println(F("No microSD card is detected")); - } + systemPrintln(); + systemPrintln("Menu: Logging"); - Serial.print(F("1) Log to microSD: ")); - if (settings.enableLogging == true) Serial.println(F("Enabled")); - else Serial.println(F("Disabled")); + if (settings.enableSD && online.microSD) + { + char sdCardSizeChar[20]; + String cardSize; + stringHumanReadableSize(cardSize, sdCardSize); + cardSize.toCharArray(sdCardSizeChar, sizeof(sdCardSizeChar)); + char sdFreeSpaceChar[20]; + String freeSpace; + stringHumanReadableSize(freeSpace, sdFreeSpace); + freeSpace.toCharArray(sdFreeSpaceChar, sizeof(sdFreeSpaceChar)); + + char myString[60]; + snprintf(myString, sizeof(myString), "SD card size: %s / Free space: %s", sdCardSizeChar, sdFreeSpaceChar); + systemPrintln(myString); + + if (online.logging) + { + systemPrintf("Current log file name: %s\r\n", logFileName); + } + } + else + systemPrintln("No microSD card is detected"); - if (settings.enableLogging == true) - { - Serial.print(F("2) Set max logging time: ")); - Serial.print(settings.maxLogTime_minutes); - Serial.println(F(" minutes")); - } + if (bufferOverruns) + systemPrintf("Buffer overruns: %d\r\n", bufferOverruns); - Serial.println(F("x) Exit")); + systemPrint("1) Log to microSD: "); + if (settings.enableLogging == true) + systemPrintln("Enabled"); + else + systemPrintln("Disabled"); - byte incoming = getByteChoice(menuTimeout); //Timeout after x seconds + if (settings.enableLogging == true) + { + systemPrint("2) Set max logging time: "); + systemPrint(settings.maxLogTime_minutes); + systemPrintln(" minutes"); - if (incoming == '1') - { - settings.enableLogging ^= 1; - } - else if (incoming == '2' && settings.enableLogging == true) - { - Serial.print(F("Enter max minutes to log data: ")); - int maxMinutes = getNumber(menuTimeout); //Timeout after x seconds - if (maxMinutes < 0 || maxMinutes > 60 * 48) //Arbitrary 48 hour limit - { - Serial.println(F("Error: max minutes out of range")); - } - else - { - settings.maxLogTime_minutes = maxMinutes; //Recorded to NVM and file at main menu exit - } - } - else if (incoming == 'x') - break; - else if (incoming == STATUS_GETBYTE_TIMEOUT) - { - break; + systemPrint("3) Set max log length: "); + systemPrint(settings.maxLogLength_minutes); + systemPrintln(" minutes"); + + if (online.logging == true) + systemPrintln("4) Start new log"); + + systemPrint("5) Log Antenna Reference Position from RTCM 1005/1006: "); + if (settings.enableARPLogging == true) + systemPrintln("Enabled"); + else + systemPrintln("Disabled"); + + if (settings.enableARPLogging == true) + { + systemPrint("6) Set ARP logging interval: "); + systemPrint(settings.ARPLoggingInterval_s); + systemPrintln(" seconds"); + } + } + + systemPrint("7) Write Marks_date.csv file to microSD: "); + if (settings.enableMarksFile == true) + systemPrintln("Enabled"); + else + systemPrintln("Disabled"); + + systemPrint("8) Reset system if the SD card is detected but fails to initialize: "); + if (settings.forceResetOnSDFail == true) + systemPrintln("Enabled"); + else + systemPrintln("Disabled"); + + if (HAS_ETHERNET) + { + systemPrint("9) Write NTP requests to microSD: "); + if (settings.enableNTPFile == true) + systemPrintln("Enabled"); + else + systemPrintln("Disabled"); + } + + systemPrintln("x) Exit"); + + int incoming = getNumber(); // Returns EXIT, TIMEOUT, or long + + if (incoming == 1) + { + settings.enableLogging ^= 1; + + // Reset the maximum logging time when logging is disabled to ensure that + // the next time logging is enabled that the maximum amount of data can be + // captured. + if (settings.enableLogging == false) + startLogTime_minutes = 0; + } + else if (incoming == 2 && settings.enableLogging == true) + { + systemPrint("Enter max minutes before logging stops: "); + int maxMinutes = getNumber(); // Returns EXIT, TIMEOUT, or long + if ((maxMinutes != INPUT_RESPONSE_GETNUMBER_EXIT) && (maxMinutes != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + { + if (maxMinutes < 0 || + maxMinutes > + (60 * 24 * 365 * + 2)) // Arbitrary 2 year limit. See https://github.com/sparkfun/SparkFun_RTK_Firmware/issues/86 + systemPrintln("Error: Max minutes out of range"); + else + settings.maxLogTime_minutes = maxMinutes; // Recorded to NVM and file at main menu exit + } + } + else if (incoming == 3 && settings.enableLogging == true) + { + systemPrint("Enter max minutes of logging before new log is created: "); + int maxLogMinutes = getNumber(); // Returns EXIT, TIMEOUT, or long + if ((maxLogMinutes != INPUT_RESPONSE_GETNUMBER_EXIT) && (maxLogMinutes != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + { + if (maxLogMinutes < 0 || maxLogMinutes > 60 * 48) // Arbitrary 48 hour limit + systemPrintln("Error: Max minutes out of range"); + else + settings.maxLogLength_minutes = maxLogMinutes; // Recorded to NVM and file at main menu exit + } + } + else if (incoming == 4 && settings.enableLogging == true && online.logging == true) + { + endLogging(false, true); //(gotSemaphore, releaseSemaphore) Close file. Reset parser stats. + beginLogging(); // Create new file based on current RTC. + setLoggingType(); // Determine if we are standard, PPP, or custom. Changes logging icon accordingly. + } + else if (incoming == 5 && settings.enableLogging == true && online.logging == true) + { + settings.enableARPLogging ^= 1; + } + else if (incoming == 6 && settings.enableLogging == true && settings.enableARPLogging == true) + { + systemPrint("Enter the ARP logging interval in seconds: "); + int logSecs = getNumber(); // Returns EXIT, TIMEOUT, or long + if ((logSecs != INPUT_RESPONSE_GETNUMBER_EXIT) && (logSecs != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + { + if (logSecs < 1 || logSecs > 600) // Arbitrary 10 minute limit + systemPrintln("Error: Logging interval out of range"); + else + settings.ARPLoggingInterval_s = logSecs; // Recorded to NVM and file at main menu exit + } + } + else if (incoming == 7) + { + settings.enableMarksFile ^= 1; + } + else if (incoming == 8) + { + settings.forceResetOnSDFail ^= 1; + } + else if ((HAS_ETHERNET) && (incoming == 9)) + { + settings.enableNTPFile ^= 1; + } + else if (incoming == 'x') + break; + else if (incoming == INPUT_RESPONSE_GETNUMBER_EXIT) + break; + else if (incoming == INPUT_RESPONSE_GETNUMBER_TIMEOUT) + break; + else + printUnknown(incoming); } - else - printUnknown(incoming); - } - while (Serial.available()) Serial.read(); //Empty buffer of any newline chars + clearBuffer(); // Empty buffer of any newline chars } -//Control the messages that get broadcast over Bluetooth and logged (if enabled) +// Control the messages that get broadcast over Bluetooth and logged (if enabled) void menuMessages() { - while (1) - { - Serial.println(); - Serial.println(F("Menu: Messages Menu")); - - Serial.printf("Active messages: %d\n\r", getActiveMessageCount()); - - Serial.println(F("1) Set NMEA Messages")); - Serial.println(F("2) Set RTCM Messages")); - Serial.println(F("3) Set RXM Messages")); - Serial.println(F("4) Set NAV Messages")); - Serial.println(F("5) Set MON Messages")); - Serial.println(F("6) Set TIM Messages")); - Serial.println(F("7) Reset to Surveying Defaults (NMEAx5)")); - Serial.println(F("8) Reset to PPP Logging Defaults (NMEAx5 + RXMx2)")); - Serial.println(F("9) Turn off all messages")); - Serial.println(F("10) Turn on all messages")); - - Serial.println(F("x) Exit")); - - int incoming = getNumber(menuTimeout); //Timeout after x seconds - - if (incoming == 1) - menuMessagesSubtype((char*)"NMEA"); - else if (incoming == 2) - menuMessagesSubtype((char*)"RTCM"); - else if (incoming == 3) - menuMessagesSubtype((char*)"RXM"); - else if (incoming == 4) - menuMessagesSubtype((char*)"NAV"); - else if (incoming == 5) - menuMessagesSubtype((char*)"MON"); - else if (incoming == 6) - menuMessagesSubtype((char*)"TIM"); - else if (incoming == 7) - { - setGNSSMessageRates(ubxMessages, 0); //Turn off all messages - setMessageRateByName((char*)"UBX_NMEA_GGA", 1); - setMessageRateByName((char*)"UBX_NMEA_GSA", 1); - setMessageRateByName((char*)"UBX_NMEA_GST", 1); - setMessageRateByName((char*)"UBX_NMEA_GSV", 4); //One update per 4 fixes to avoid swamping SPP connection - setMessageRateByName((char*)"UBX_NMEA_RMC", 1); - Serial.println(F("Reset to Surveying Defaults (NMEAx5)")); - } - else if (incoming == 8) - { - setGNSSMessageRates(ubxMessages, 0); //Turn off all messages - setMessageRateByName((char*)"UBX_NMEA_GGA", 1); - setMessageRateByName((char*)"UBX_NMEA_GSA", 1); - setMessageRateByName((char*)"UBX_NMEA_GST", 1); - setMessageRateByName((char*)"UBX_NMEA_GSV", 4); //One update per 4 fixes to avoid swamping SPP connection - setMessageRateByName((char*)"UBX_NMEA_RMC", 1); - - setMessageRateByName((char*)"UBX_RXM_RAWX", 1); - setMessageRateByName((char*)"UBX_RXM_SFRBX", 1); - Serial.println(F("Reset to PPP Logging Defaults (NMEAx5 + RXMx2)")); - } - else if (incoming == 9) - { - setGNSSMessageRates(ubxMessages, 0); //Turn off all messages - Serial.println(F("All messages disabled")); - } - else if (incoming == 10) + while (1) { - setGNSSMessageRates(ubxMessages, 1); //Turn on all messages to report once per fix - Serial.println(F("All messages enabled")); + systemPrintln(); + systemPrintln("Menu: GNSS Messages"); + + systemPrintf("Active messages: %d\r\n", getActiveMessageCount()); + + systemPrintln("1) Set NMEA Messages"); + if (zedModuleType == PLATFORM_F9P) + systemPrintln("2) Set RTCM Messages"); + else if (zedModuleType == PLATFORM_F9R) + systemPrintln("2) Set ESF Messages"); + systemPrintln("3) Set RXM Messages"); + systemPrintln("4) Set NAV Messages"); + systemPrintln("5) Set NAV2 Messages"); + systemPrintln("6) Set NMEA NAV2 Messages"); + systemPrintln("7) Set MON Messages"); + systemPrintln("8) Set TIM Messages"); + systemPrintln("9) Set PUBX Messages"); + + systemPrintln("10) Reset to Surveying Defaults (NMEAx5)"); + systemPrintln("11) Reset to PPP Logging Defaults (NMEAx5 + RXMx2)"); + systemPrintln("12) Turn off all messages"); + systemPrintln("13) Turn on all messages"); + + systemPrintln("x) Exit"); + + int incoming = getNumber(); // Returns EXIT, TIMEOUT, or long + + if (incoming == 1) + menuMessagesSubtype(settings.ubxMessageRates, "NMEA_"); // The following _ avoids listing NMEANAV2 messages + else if (incoming == 2 && zedModuleType == PLATFORM_F9P) + menuMessagesSubtype(settings.ubxMessageRates, "RTCM"); + else if (incoming == 2 && zedModuleType == PLATFORM_F9R) + menuMessagesSubtype(settings.ubxMessageRates, "ESF"); + else if (incoming == 3) + menuMessagesSubtype(settings.ubxMessageRates, "RXM"); + else if (incoming == 4) + menuMessagesSubtype(settings.ubxMessageRates, "NAV_"); // The following _ avoids listing NAV2 messages + else if (incoming == 5) + menuMessagesSubtype(settings.ubxMessageRates, "NAV2"); + else if (incoming == 6) + menuMessagesSubtype(settings.ubxMessageRates, "NMEANAV2"); + else if (incoming == 7) + menuMessagesSubtype(settings.ubxMessageRates, "MON"); + else if (incoming == 8) + menuMessagesSubtype(settings.ubxMessageRates, "TIM"); + else if (incoming == 9) + menuMessagesSubtype(settings.ubxMessageRates, "PUBX"); + else if (incoming == 10) + { + setGNSSMessageRates(settings.ubxMessageRates, 0); // Turn off all messages + setMessageRateByName("UBX_NMEA_GGA", 1); + setMessageRateByName("UBX_NMEA_GSA", 1); + setMessageRateByName("UBX_NMEA_GST", 1); + + // We want GSV NMEA to be reported at 1Hz to avoid swamping SPP connection + float measurementFrequency = (1000.0 / settings.measurementRate) / settings.navigationRate; + if (measurementFrequency < 1.0) + measurementFrequency = 1.0; + setMessageRateByName("UBX_NMEA_GSV", measurementFrequency); // One report per second + + setMessageRateByName("UBX_NMEA_RMC", 1); + systemPrintln("Reset to Surveying Defaults (NMEAx5)"); + } + else if (incoming == 11) + { + setGNSSMessageRates(settings.ubxMessageRates, 0); // Turn off all messages + setMessageRateByName("UBX_NMEA_GGA", 1); + setMessageRateByName("UBX_NMEA_GSA", 1); + setMessageRateByName("UBX_NMEA_GST", 1); + + // We want GSV NMEA to be reported at 1Hz to avoid swamping SPP connection + float measurementFrequency = (1000.0 / settings.measurementRate) / settings.navigationRate; + if (measurementFrequency < 1.0) + measurementFrequency = 1.0; + setMessageRateByName("UBX_NMEA_GSV", measurementFrequency); // One report per second + + setMessageRateByName("UBX_NMEA_RMC", 1); + + setMessageRateByName("UBX_RXM_RAWX", 1); + setMessageRateByName("UBX_RXM_SFRBX", 1); + systemPrintln("Reset to PPP Logging Defaults (NMEAx5 + RXMx2)"); + } + else if (incoming == 12) + { + setGNSSMessageRates(settings.ubxMessageRates, 0); // Turn off all messages + systemPrintln("All messages disabled"); + } + else if (incoming == 13) + { + setGNSSMessageRates(settings.ubxMessageRates, 1); // Turn on all messages to report once per fix + systemPrintln("All messages enabled"); + } + else if (incoming == INPUT_RESPONSE_GETNUMBER_EXIT) + break; + else if (incoming == INPUT_RESPONSE_GETNUMBER_TIMEOUT) + break; + else + printUnknown(incoming); } - else if (incoming == STATUS_PRESSED_X) - break; - else if (incoming == STATUS_GETNUMBER_TIMEOUT) - break; - else - printUnknown(incoming); - } - while (Serial.available()) Serial.read(); //Empty buffer of any newline chars + clearBuffer(); // Empty buffer of any newline chars - bool response = configureGNSSMessageRates(COM_PORT_UART1, ubxMessages); //Make sure the appropriate messages are enabled - if (response == false) - { - Serial.println(F("menuMessages: Failed to enable UART1 messages - Try 1")); - //Try again - response = configureGNSSMessageRates(COM_PORT_UART1, ubxMessages); //Make sure the appropriate messages are enabled + // Make sure the appropriate messages are enabled + bool response = setMessages(MAX_SET_MESSAGES_RETRIES); // Does a complete open/closed val set if (response == false) - Serial.println(F("menuMessages: Failed to enable UART1 messages - Try 2")); + systemPrintf("menuMessages: Failed to enable messages - after %d tries", MAX_SET_MESSAGES_RETRIES); else - Serial.println(F("menuMessages: UART1 messages successfully enabled")); - } - else - { - Serial.println(F("menuMessages: UART1 messages successfully enabled")); - } + systemPrintln("menuMessages: Messages successfully enabled"); + setLoggingType(); // Update Standard, PPP, or custom for icon selection } -//Given a sub type (ie "RTCM", "NMEA") present menu showing messages with this subtype -//Controls the messages that get broadcast over Bluetooth and logged (if enabled) -void menuMessagesSubtype(char* messageType) +// Control the RTCM message rates when in Base mode +void menuMessagesBaseRTCM() { - while (1) - { - Serial.println(); - Serial.printf("Menu: Message %s Menu\n\r", messageType); - - int startOfBlock = 0; - int endOfBlock = 0; - setMessageOffsets(messageType, startOfBlock, endOfBlock); //Find start and stop of RTCM records in message array - for (int x = 0 ; x < (endOfBlock - startOfBlock) ; x++) + while (1) { - Serial.printf("%d) Message %s: ", x + 1, ubxMessages[x + startOfBlock].msgTextName); - Serial.println(ubxMessages[x + startOfBlock].msgRate); - } + systemPrintln(); + systemPrintln("Menu: GNSS Messages - Base RTCM"); - Serial.println(F("x) Exit")); + systemPrintln("1) Set RXM Messages for Base Mode"); + systemPrintln("2) Reset to Defaults (1005/74/84/94/124 1Hz & 1230 0.1Hz)"); + systemPrintln("3) Reset to Low Bandwidth Link (1074/84/94/124 0.5Hz & 1005/230 0.1Hz)"); - int incoming = getNumber(menuTimeout); //Timeout after x seconds + systemPrintln("x) Exit"); - if (incoming >= 1 && incoming <= (endOfBlock - startOfBlock)) - { - inputMessageRate(ubxMessages[ (incoming - 1) + startOfBlock]); + int incoming = getNumber(); // Returns EXIT, TIMEOUT, or long + + if (incoming == 1) + { + menuMessagesSubtype(settings.ubxMessageRatesBase, "RTCM-Base"); + restartBase = true; + } + else if (incoming == 2) + { + int firstRTCMRecord = getMessageNumberByName("UBX_RTCM_1005"); + settings.ubxMessageRatesBase[getMessageNumberByName("UBX_RTCM_1005") - firstRTCMRecord] = 1; // 1105 + settings.ubxMessageRatesBase[getMessageNumberByName("UBX_RTCM_1074") - firstRTCMRecord] = 1; // 1074 + settings.ubxMessageRatesBase[getMessageNumberByName("UBX_RTCM_1077") - firstRTCMRecord] = 0; // 1077 + settings.ubxMessageRatesBase[getMessageNumberByName("UBX_RTCM_1084") - firstRTCMRecord] = 1; // 1084 + settings.ubxMessageRatesBase[getMessageNumberByName("UBX_RTCM_1087") - firstRTCMRecord] = 0; // 1087 + + settings.ubxMessageRatesBase[getMessageNumberByName("UBX_RTCM_1094") - firstRTCMRecord] = 1; // 1094 + settings.ubxMessageRatesBase[getMessageNumberByName("UBX_RTCM_1097") - firstRTCMRecord] = 0; // 1097 + settings.ubxMessageRatesBase[getMessageNumberByName("UBX_RTCM_1124") - firstRTCMRecord] = 1; // 1124 + settings.ubxMessageRatesBase[getMessageNumberByName("UBX_RTCM_1127") - firstRTCMRecord] = 0; // 1127 + settings.ubxMessageRatesBase[getMessageNumberByName("UBX_RTCM_1230") - firstRTCMRecord] = 10; // 1230 + + settings.ubxMessageRatesBase[getMessageNumberByName("UBX_RTCM_4072_0") - firstRTCMRecord] = 0; // 4072_0 + settings.ubxMessageRatesBase[getMessageNumberByName("UBX_RTCM_4072_1") - firstRTCMRecord] = 0; // 4072_1 + + systemPrintln("Reset to Defaults (1005/74/84/94/124 1Hz & 1230 0.1Hz)"); + restartBase = true; + } + else if (incoming == 3) + { + int firstRTCMRecord = getMessageNumberByName("UBX_RTCM_1005"); + settings.ubxMessageRatesBase[getMessageNumberByName("UBX_RTCM_1005") - firstRTCMRecord] = 10; // 1105 0.1Hz + settings.ubxMessageRatesBase[getMessageNumberByName("UBX_RTCM_1074") - firstRTCMRecord] = 2; // 1074 0.5Hz + settings.ubxMessageRatesBase[getMessageNumberByName("UBX_RTCM_1077") - firstRTCMRecord] = 0; // 1077 + settings.ubxMessageRatesBase[getMessageNumberByName("UBX_RTCM_1084") - firstRTCMRecord] = 2; // 1084 0.5Hz + settings.ubxMessageRatesBase[getMessageNumberByName("UBX_RTCM_1087") - firstRTCMRecord] = 0; // 1087 + + settings.ubxMessageRatesBase[getMessageNumberByName("UBX_RTCM_1094") - firstRTCMRecord] = 2; // 1094 0.5Hz + settings.ubxMessageRatesBase[getMessageNumberByName("UBX_RTCM_1097") - firstRTCMRecord] = 0; // 1097 + settings.ubxMessageRatesBase[getMessageNumberByName("UBX_RTCM_1124") - firstRTCMRecord] = 2; // 1124 0.5Hz + settings.ubxMessageRatesBase[getMessageNumberByName("UBX_RTCM_1127") - firstRTCMRecord] = 0; // 1127 + settings.ubxMessageRatesBase[getMessageNumberByName("UBX_RTCM_1230") - firstRTCMRecord] = 10; // 1230 0.1Hz + + settings.ubxMessageRatesBase[getMessageNumberByName("UBX_RTCM_4072_0") - firstRTCMRecord] = 0; // 4072_0 + settings.ubxMessageRatesBase[getMessageNumberByName("UBX_RTCM_4072_1") - firstRTCMRecord] = 0; // 4072_1 + + systemPrintln("Reset to Low Bandwidth Link (1074/84/94/124 0.5Hz & 1005/230 0.1Hz)"); + restartBase = true; + } + else if (incoming == INPUT_RESPONSE_GETNUMBER_EXIT) + break; + else if (incoming == INPUT_RESPONSE_GETNUMBER_TIMEOUT) + break; + else + printUnknown(incoming); } - else if (incoming == STATUS_PRESSED_X) - break; - else if (incoming == STATUS_GETNUMBER_TIMEOUT) - break; - else - printUnknown(incoming); - } - while (Serial.available()) Serial.read(); //Empty buffer of any newline chars + clearBuffer(); // Empty buffer of any newline chars } -//Prompt the user to enter the message rate for a given ID -//Assign the given value to the message -void inputMessageRate(ubxMsg &localMessage) +// Given a sub type (ie "RTCM", "NMEA") present menu showing messages with this subtype +// Controls the messages that get broadcast over Bluetooth and logged (if enabled) +void menuMessagesSubtype(uint8_t *localMessageRate, const char *messageType) { - Serial.printf("Enter %s message rate (0 to disable): ", localMessage.msgTextName); - int64_t rate = getNumber(menuTimeout); //Timeout after x seconds + while (1) + { + systemPrintln(); + systemPrintf("Menu: Message %s\r\n", messageType); + + int startOfBlock = 0; + int endOfBlock = 0; + int rtcmOffset = 0; // Used to offset messageSupported lookup + + if (strcmp(messageType, "RTCM-Base") == 0) // The ubxMessageRatesBase array is 0 to MAX_UBX_MSG_RTCM - 1 + { + startOfBlock = 0; + endOfBlock = MAX_UBX_MSG_RTCM; + rtcmOffset = getMessageNumberByName("UBX_RTCM_1005"); + } + else + setMessageOffsets(&ubxMessages[0], messageType, startOfBlock, + endOfBlock); // Find start and stop of given messageType in message array + + for (int x = 0; x < (endOfBlock - startOfBlock); x++) + { + // Check to see if this ZED platform supports this message + if (messageSupported(x + startOfBlock + rtcmOffset) == true) + { + systemPrintf("%d) Message %s: ", x + 1, ubxMessages[x + startOfBlock + rtcmOffset].msgTextName); + systemPrintln(localMessageRate[x + startOfBlock]); + } + } + + systemPrintln("x) Exit"); - while (rate < 0 || rate > 60) //Arbitrary 60 fixes per report limit - { - Serial.println(F("Error: message rate out of range")); - Serial.printf("Enter %s message rate (0 to disable): ", localMessage.msgTextName); - rate = getNumber(menuTimeout); //Timeout after x seconds + int incoming = getNumber(); // Returns EXIT, TIMEOUT, or long - if (rate == STATUS_GETNUMBER_TIMEOUT || rate == STATUS_PRESSED_X) - return; //Give up - } + if (incoming >= 1 && incoming <= (endOfBlock - startOfBlock)) + { + // Check to see if this ZED platform supports this message + int msgNumber = (incoming - 1) + startOfBlock; - if (rate == STATUS_GETNUMBER_TIMEOUT || rate == STATUS_PRESSED_X) - return; + if (messageSupported(msgNumber + rtcmOffset) == true) + inputMessageRate(localMessageRate[msgNumber], msgNumber + rtcmOffset); + else + printUnknown(incoming); + } + else if (incoming == INPUT_RESPONSE_GETNUMBER_EXIT) + break; + else if (incoming == INPUT_RESPONSE_GETNUMBER_TIMEOUT) + break; + else + printUnknown(incoming); + } - localMessage.msgRate = rate; + clearBuffer(); // Empty buffer of any newline chars } -//Updates the message rates on the ZED-F9P for all known messages -//Any port and messages by reference can be passed in. This allows us to modify the USB -//port settings a separate (not NVM backed) message struct for testing -bool configureGNSSMessageRates(uint8_t portType, ubxMsg *localMessage) +// Prompt the user to enter the message rate for a given message ID +// Assign the given value to the message +void inputMessageRate(uint8_t &localMessageRate, uint8_t messageNumber) { - bool response = true; + systemPrintf("Enter %s message rate (0 to disable): ", ubxMessages[messageNumber].msgTextName); + int rate = getNumber(); // Returns EXIT, TIMEOUT, or long - for (int x = 0 ; x < MAX_UBX_MSG ; x++) - response &= configureMessageRate(portType, localMessage[x]); + if (rate == INPUT_RESPONSE_GETNUMBER_TIMEOUT || rate == INPUT_RESPONSE_GETNUMBER_EXIT) + return; - return (response); + while (rate < 0 || rate > 255) // 8 bit limit + { + systemPrintln("Error: Message rate out of range"); + systemPrintf("Enter %s message rate (0 to disable): ", ubxMessages[messageNumber].msgTextName); + rate = getNumber(); // Returns EXIT, TIMEOUT, or long + + if (rate == INPUT_RESPONSE_GETNUMBER_TIMEOUT || rate == INPUT_RESPONSE_GETNUMBER_EXIT) + return; // Give up + } + + localMessageRate = rate; } -//Set all GNSS message report rates to one value -//Useful for turning on or off all messages for resetting and testing -//We pass in the message array by reference so that we can modify a temp struct -//like a dummy struct for USB message changes (all on/off) for testing -void setGNSSMessageRates(ubxMsg *localMessage, uint8_t msgRate) +// Set all GNSS message report rates to one value +// Useful for turning on or off all messages for resetting and testing +// We pass in the message array by reference so that we can modify a temp struct +// like a dummy struct for USB message changes (all on/off) for testing +void setGNSSMessageRates(uint8_t *localMessageRate, uint8_t msgRate) { - for (int x = 0 ; x < MAX_UBX_MSG ; x++) - localMessage[x].msgRate = msgRate; + for (int x = 0; x < MAX_UBX_MSG; x++) + localMessageRate[x] = msgRate; } -//Given a message, set the message rate on the ZED-F9P -bool configureMessageRate(uint8_t portID, ubxMsg localMessage) +// Creates a log if logging is enabled, and SD is detected +// Based on GPS data/time, create a log file in the format SFE_Surveyor_YYMMDD_HHMMSS.ubx +void beginLogging() { - uint8_t currentSendRate = getMessageRate(localMessage.msgClass, localMessage.msgID, portID); //Qeury the module for the current setting - - bool response = true; - if (currentSendRate != localMessage.msgRate) - response &= i2cGNSS.configureMessage(localMessage.msgClass, localMessage.msgID, portID, localMessage.msgRate); //Update setting - return response; + beginLogging(nullptr); } -//Lookup the send rate for a given message+port -uint8_t getMessageRate(uint8_t msgClass, uint8_t msgID, uint8_t portID) +void beginLogging(const char *customFileName) { - ubxPacket customCfg = {0, 0, 0, 0, 0, settingPayload, 0, 0, SFE_UBLOX_PACKET_VALIDITY_NOT_DEFINED, SFE_UBLOX_PACKET_VALIDITY_NOT_DEFINED}; + if (online.microSD == false) + beginSD(); - customCfg.cls = UBX_CLASS_CFG; // This is the message Class - customCfg.id = UBX_CFG_MSG; // This is the message ID - customCfg.len = 2; - customCfg.startingSpot = 0; // Always set the startingSpot to zero (unless you really know what you are doing) - - uint16_t maxWait = 1250; // Wait for up to 1250ms (Serial may need a lot longer e.g. 1100) - - settingPayload[0] = msgClass; - settingPayload[1] = msgID; + if (online.logging == false) + { + if (online.microSD == true && settings.enableLogging == true && + online.rtc == true) // We can't create a file until we have date/time + { + if (customFileName == nullptr) + { + // Generate a standard log file name + if (reuseLastLog == true) // attempt to use previous log + { + reuseLastLog = false; // Don't reuse the file a second time + + // findLastLog does not add the preceding slash. We need to do it manually + logFileName[0] = '/'; // Erase any existing file name + logFileName[1] = 0; + + if (findLastLog(&logFileName[1], sizeof(logFileName) - 1) == false) + { + logFileName[0] = 0; // No file found. Erase the slash + log_d("Failed to find last log. Making new one."); + } + else + log_d("Using last log file."); + } + else + { + // We are not reusing the last log, so erase the global/original filename + logFileName[0] = 0; + } + + if (strlen(logFileName) == 0) + { + snprintf(logFileName, sizeof(logFileName), "/%s_%02d%02d%02d_%02d%02d%02d.ubx", // SdFat library + platformFilePrefix, rtc.getYear() - 2000, rtc.getMonth() + 1, + rtc.getDay(), // ESP32Time returns month:0-11 + rtc.getHour(true), rtc.getMinute(), + rtc.getSecond() // ESP32Time getHour(true) returns hour:0-23 + ); + } + } + else + { + strncpy(logFileName, customFileName, + sizeof(logFileName) - 1); // customFileName already has the preceding slash added + } - // Read the current setting. The results will be loaded into customCfg. - if (i2cGNSS.sendCommand(&customCfg, maxWait) != SFE_UBLOX_STATUS_DATA_RECEIVED) // We are expecting data and an ACK - { - Serial.printf("getMessageSetting failed: Class-0x%02X ID-0x%02X\n\r", msgClass, msgID); - return (false); - } + // Allocate the ubxFile + if (!ubxFile) + { + ubxFile = new FileSdFatMMC; + if (!ubxFile) + { + systemPrintln("Failed to allocate ubxFile!"); + return; + } + } - uint8_t sendRate = settingPayload[2 + portID]; + // Attempt to write to file system. This avoids collisions with file writing in F9PSerialReadTask() + if (xSemaphoreTake(sdCardSemaphore, fatSemaphore_longWait_ms) == pdPASS) + { + markSemaphore(FUNCTION_CREATEFILE); + + // O_CREAT - create the file if it does not exist + // O_APPEND - seek to the end of the file prior to each write + // O_WRITE - open for write + if (ubxFile->open(logFileName, O_CREAT | O_APPEND | O_WRITE) == false) + { + systemPrintf("Failed to create GNSS UBX data file: %s\r\n", logFileName); + online.logging = false; + xSemaphoreGive(sdCardSemaphore); + return; + } + + fileSize = 0; + lastLogSize = 0; // Reset counter - used for displaying active logging icon + + bufferOverruns = 0; // Reset counter + + ubxFile->updateFileCreateTimestamp(); // Update the file to create time & date + + startCurrentLogTime_minutes = millis() / 1000L / 60; // Mark now as start of logging + + // If it hasn't been done before, mark the initial start of logging for total run time + if (startLogTime_minutes == 0) + startLogTime_minutes = millis() / 1000L / 60; + + // Add NMEA txt message with restart reason + char rstReason[30]; + switch (esp_reset_reason()) + { + case ESP_RST_UNKNOWN: + strcpy(rstReason, "ESP_RST_UNKNOWN"); + break; + case ESP_RST_POWERON: + strcpy(rstReason, "ESP_RST_POWERON"); + break; + case ESP_RST_SW: + strcpy(rstReason, "ESP_RST_SW"); + break; + case ESP_RST_PANIC: + strcpy(rstReason, "ESP_RST_PANIC"); + break; + case ESP_RST_INT_WDT: + strcpy(rstReason, "ESP_RST_INT_WDT"); + break; + case ESP_RST_TASK_WDT: + strcpy(rstReason, "ESP_RST_TASK_WDT"); + break; + case ESP_RST_WDT: + strcpy(rstReason, "ESP_RST_WDT"); + break; + case ESP_RST_DEEPSLEEP: + strcpy(rstReason, "ESP_RST_DEEPSLEEP"); + break; + case ESP_RST_BROWNOUT: + strcpy(rstReason, "ESP_RST_BROWNOUT"); + break; + case ESP_RST_SDIO: + strcpy(rstReason, "ESP_RST_SDIO"); + break; + default: + strcpy(rstReason, "Unknown"); + } + + // Mark top of log with system information + char nmeaMessage[82]; // Max NMEA sentence length is 82 + createNMEASentence(CUSTOM_NMEA_TYPE_RESET_REASON, nmeaMessage, sizeof(nmeaMessage), + rstReason); // textID, buffer, sizeOfBuffer, text + ubxFile->println(nmeaMessage); + + // Record system firmware versions and info to log + + // SparkFun RTK Express v1.10-Feb 11 2022 + char firmwareVersion[30]; // v1.3 December 31 2021 + firmwareVersion[0] = 'v'; + getFirmwareVersion(&firmwareVersion[1], sizeof(firmwareVersion) -1, true); + createNMEASentence(CUSTOM_NMEA_TYPE_SYSTEM_VERSION, nmeaMessage, sizeof(nmeaMessage), + firmwareVersion); // textID, buffer, sizeOfBuffer, text + ubxFile->println(nmeaMessage); + + // ZED-F9P firmware: HPG 1.30 + createNMEASentence(CUSTOM_NMEA_TYPE_ZED_VERSION, nmeaMessage, sizeof(nmeaMessage), + zedFirmwareVersion); // textID, buffer, sizeOfBuffer, text + ubxFile->println(nmeaMessage); + + // ZED-F9 unique chip ID + createNMEASentence(CUSTOM_NMEA_TYPE_ZED_UNIQUE_ID, nmeaMessage, sizeof(nmeaMessage), + zedUniqueId); // textID, buffer, sizeOfBuffer, text + ubxFile->println(nmeaMessage); + + // Device BT MAC. See issue: https://github.com/sparkfun/SparkFun_RTK_Firmware/issues/346 + char macAddress[5]; + snprintf(macAddress, sizeof(macAddress), "%02X%02X", btMACAddress[4], btMACAddress[5]); + createNMEASentence(CUSTOM_NMEA_TYPE_DEVICE_BT_ID, nmeaMessage, sizeof(nmeaMessage), + macAddress); // textID, buffer, sizeOfBuffer, text + ubxFile->println(nmeaMessage); + + // Record today's time/date into log. This is in case a log is restarted. See issue 440: + // https://github.com/sparkfun/SparkFun_RTK_Firmware/issues/440 + char currentDate[sizeof("230101,120101")]; + snprintf(currentDate, sizeof(currentDate), "%02d%02d%02d,%02d%02d%02d", rtc.getYear() - 2000, + rtc.getMonth() + 1, rtc.getDay(), // ESP32Time returns month:0-11 + rtc.getHour(true), rtc.getMinute(), + rtc.getSecond() // ESP32Time getHour(true) returns hour:0-23 + ); + createNMEASentence(CUSTOM_NMEA_TYPE_CURRENT_DATE, nmeaMessage, sizeof(nmeaMessage), + currentDate); // textID, buffer, sizeOfBuffer, text + ubxFile->println(nmeaMessage); + + if (reuseLastLog == true) + { + systemPrintln("Appending last available log"); + } + + xSemaphoreGive(sdCardSemaphore); + } + else + { + // A retry will happen during the next loop, the log will eventually be opened + log_d("Failed to get file system lock to create GNSS UBX data file"); + online.logging = false; + return; + } - return (sendRate); + systemPrintf("Log file name: %s\r\n", logFileName); + online.logging = true; + } // online.sd, enable.logging, online.rtc + } // online.logging } -//Creates a log if logging is enabled, and SD is detected -//Based on GPS data/time, create a log file in the format SFE_Surveyor_YYMMDD_HHMMSS.ubx -void beginLogging() +// Stop writing to the log file on the microSD card +void endLogging(bool gotSemaphore, bool releaseSemaphore) { - if (online.logging == false) - { - if (online.microSD == true && settings.enableLogging == true) + if (online.logging == true) { - char fileName[40] = ""; + // Wait up to 1000ms to allow hanging SD writes to time out + if (gotSemaphore || (xSemaphoreTake(sdCardSemaphore, 1000 / portTICK_PERIOD_MS) == pdPASS)) + { + markSemaphore(FUNCTION_ENDLOGGING); - i2cGNSS.checkUblox(); + online.logging = false; - if (reuseLastLog == true) //attempt to use previous log - { - if (findLastLog(fileName) == false) + // Record the number of NMEA/RTCM/UBX messages that were filtered out + char parserStats[50]; + + snprintf(parserStats, sizeof(parserStats), "%d,%d,%d,", failedParserMessages_NMEA, + failedParserMessages_RTCM, failedParserMessages_UBX); + + char nmeaMessage[82]; // Max NMEA sentence length is 82 + createNMEASentence(CUSTOM_NMEA_TYPE_PARSER_STATS, nmeaMessage, sizeof(nmeaMessage), + parserStats); // textID, buffer, sizeOfBuffer, text + ubxFile->println(nmeaMessage); + ubxFile->sync(); + + // Reset stats in case a new log is created + failedParserMessages_NMEA = 0; + failedParserMessages_RTCM = 0; + failedParserMessages_UBX = 0; + + // Close down file system + ubxFile->close(); + // Done with the log file + delete ubxFile; + ubxFile = nullptr; + + systemPrintln("Log file closed"); + + // Release the semaphore if requested + if (releaseSemaphore) + xSemaphoreGive(sdCardSemaphore); + } // End sdCardSemaphore + else { - Serial.println(F("Failed to find last log. Making new one.")); - } - } - - if (strlen(fileName) == 0) - { - //Based on GPS data/time, create a log file in the format SFE_Surveyor_YYMMDD_HHMMSS.ubx - bool timeValid = false; - // if (i2cGNSS.getTimeValid() == true && i2cGNSS.getDateValid() == true) //Will pass if ZED's RTC is reporting (regardless of GNSS fix) - // timeValid = true; - if (i2cGNSS.getConfirmedTime() == true && i2cGNSS.getConfirmedDate() == true) //Requires GNSS fix - timeValid = true; - - if (timeValid == false) - { - Serial.println(F("beginLoggingUBX: No date/time available. No file created.")); - delay(1000); //Give the receiver time to get a lock - online.logging = false; - return; - } + char semaphoreHolder[50]; + getSemaphoreFunction(semaphoreHolder); - sprintf(fileName, "%s_%02d%02d%02d_%02d%02d%02d.ubx", //SdFat library - platformFilePrefix, - i2cGNSS.getYear() - 2000, i2cGNSS.getMonth(), i2cGNSS.getDay(), - i2cGNSS.getHour(), i2cGNSS.getMinute(), i2cGNSS.getSecond() - ); - } - - //Attempt to write to file system. This avoids collisions with file writing in F9PSerialReadTask() - if (xSemaphoreTake(xFATSemaphore, fatSemaphore_longWait_ms) == pdPASS) - { - // O_CREAT - create the file if it does not exist - // O_APPEND - seek to the end of the file prior to each write - // O_WRITE - open for write - if (ubxFile.open(fileName, O_CREAT | O_APPEND | O_WRITE) == false) - { - Serial.printf("Failed to create GNSS UBX data file: %s\n\r", fileName); - delay(1000); - online.logging = false; - xSemaphoreGive(xFATSemaphore); - return; + // This is OK because in the interim more data will be written to the log + // and the log file will eventually be closed by the next call in loop + log_d("sdCardSemaphore failed to yield, held by %s, menuMessages.ino line %d\r\n", semaphoreHolder, + __LINE__); } + } +} - updateDataFileCreate(&ubxFile); // Update the file to create time & date - - startLogTime_minutes = millis() / 1000L / 60; //Mark now as start of logging - - //Add NMEA txt message with restart reason - char rstReason[30]; - switch (esp_reset_reason()) - { - case ESP_RST_UNKNOWN: strcpy(rstReason, "ESP_RST_UNKNOWN"); break; - case ESP_RST_POWERON : strcpy(rstReason, "ESP_RST_POWERON"); break; - case ESP_RST_SW : strcpy(rstReason, "ESP_RST_SW"); break; - case ESP_RST_PANIC : strcpy(rstReason, "ESP_RST_PANIC"); break; - case ESP_RST_INT_WDT : strcpy(rstReason, "ESP_RST_INT_WDT"); break; - case ESP_RST_TASK_WDT : strcpy(rstReason, "ESP_RST_TASK_WDT"); break; - case ESP_RST_WDT : strcpy(rstReason, "ESP_RST_WDT"); break; - case ESP_RST_DEEPSLEEP : strcpy(rstReason, "ESP_RST_DEEPSLEEP"); break; - case ESP_RST_BROWNOUT : strcpy(rstReason, "ESP_RST_BROWNOUT"); break; - case ESP_RST_SDIO : strcpy(rstReason, "ESP_RST_SDIO"); break; - default : strcpy(rstReason, "Unknown"); - } +// Finds last log +// Returns true if successful +// lastLogName will contain the name of the last log file on return - ** but without the preceding slash ** +bool findLastLog(char *lastLogNamePrt, size_t lastLogNameSize) +{ + bool foundAFile = false; + + if (online.microSD == true) + { + // Attempt to access file system. This avoids collisions with file writing in F9PSerialReadTask() + // Wait up to 5s, this is important + if (xSemaphoreTake(sdCardSemaphore, 5000 / portTICK_PERIOD_MS) == pdPASS) + { + markSemaphore(FUNCTION_FINDLOG); - char nmeaMessage[82]; //Max NMEA sentence length is 82 - createNMEASentence(1, 1, nmeaMessage, rstReason); //sentenceNumber, textID - ubxFile.println(nmeaMessage); + // Count available binaries + if (USE_SPI_MICROSD) + { + SdFile tempFile; + SdFile dir; + const char *LOG_EXTENSION = "ubx"; + const char *LOG_PREFIX = platformFilePrefix; + char fname[100]; // Handle long file names + + dir.open("/"); // Open root + + while (tempFile.openNext(&dir, O_READ)) + { + if (tempFile.isFile()) + { + tempFile.getName(fname, sizeof(fname)); + + // Check for matching file name prefix and extension + if (strcmp(LOG_EXTENSION, &fname[strlen(fname) - strlen(LOG_EXTENSION)]) == 0) + { + if (strstr(fname, LOG_PREFIX) != nullptr) + { + strncpy(lastLogNamePrt, fname, + lastLogNameSize - 1); // Store this file as last known log file + foundAFile = true; + } + } + } + tempFile.close(); + } + } +#ifdef COMPILE_SD_MMC + else + { + File tempFile; + File dir; + const char *LOG_EXTENSION = "ubx"; + const char *LOG_PREFIX = platformFilePrefix; + char fname[100]; // Handle long file names + + dir = SD_MMC.open("/"); // Open root + + if (dir && dir.isDirectory()) + { + tempFile = dir.openNextFile(); + while (tempFile) + { + if (!tempFile.isDirectory()) + { + snprintf(fname, sizeof(fname), "%s", tempFile.name()); + + // Check for matching file name prefix and extension + if (strcmp(LOG_EXTENSION, &fname[strlen(fname) - strlen(LOG_EXTENSION)]) == 0) + { + if (strstr(fname, LOG_PREFIX) != nullptr) + { + strncpy(lastLogNamePrt, fname, + lastLogNameSize - 1); // Store this file as last known log file + foundAFile = true; + } + } + } + tempFile.close(); + tempFile = dir.openNextFile(); + } + } + } +#endif // COMPILE_SD_MMC - if (reuseLastLog == true) + xSemaphoreGive(sdCardSemaphore); + } + else { - Serial.println(F("Appending last available log")); + // Error when a log file exists on the microSD card, data should be appended + // to the existing log file + systemPrintf("sdCardSemaphore failed to yield, menuMessages.ino line %d\r\n", __LINE__); } + } + + return (foundAFile); +} - xSemaphoreGive(xFATSemaphore); - } - else - { - Serial.println(F("Failed to get file system lock to create GNSS UBX data file")); - online.logging = false; +// Given a unique string, find first and last records containing that string in message array +void setMessageOffsets(const ubxMsg *localMessage, const char *messageType, int &startOfBlock, int &endOfBlock) +{ + char messageNamePiece[40]; // UBX_RTCM + snprintf(messageNamePiece, sizeof(messageNamePiece), "UBX_%s", messageType); // Put UBX_ infront of type + + // Find the first occurrence + for (startOfBlock = 0; startOfBlock < MAX_UBX_MSG; startOfBlock++) + { + if (strstr(localMessage[startOfBlock].msgTextName, messageNamePiece) != nullptr) + break; + } + if (startOfBlock == MAX_UBX_MSG) + { + // Error out + startOfBlock = 0; + endOfBlock = 0; return; - } + } - Serial.printf("Log file created: %s\n\r", fileName); - online.logging = true; + // Find the last occurrence + for (endOfBlock = startOfBlock + 1; endOfBlock < MAX_UBX_MSG; endOfBlock++) + { + if (strstr(localMessage[endOfBlock].msgTextName, messageNamePiece) == nullptr) + break; } - else - online.logging = false; - } } -//Updates the timestemp on a given data file -void updateDataFileAccess(SdFile *dataFile) +// Return the number of active/enabled messages +uint8_t getActiveMessageCount() { - bool timeValid = false; - if (i2cGNSS.getTimeValid() == true && i2cGNSS.getDateValid() == true) //Will pass if ZED's RTC is reporting (regardless of GNSS fix) - timeValid = true; - if (i2cGNSS.getConfirmedTime() == true && i2cGNSS.getConfirmedDate() == true) //Requires GNSS fix - timeValid = true; - - if (timeValid == true) - { - //Update the file access time - dataFile->timestamp(T_ACCESS, i2cGNSS.getYear(), i2cGNSS.getMonth(), i2cGNSS.getDay(), i2cGNSS.getHour(), i2cGNSS.getMinute(), i2cGNSS.getSecond()); - //Update the file write time - dataFile->timestamp(T_WRITE, i2cGNSS.getYear(), i2cGNSS.getMonth(), i2cGNSS.getDay(), i2cGNSS.getHour(), i2cGNSS.getMinute(), i2cGNSS.getSecond()); - } + uint8_t count = 0; + for (int x = 0; x < MAX_UBX_MSG; x++) + if (settings.ubxMessageRates[x] > 0) + count++; + return (count); } -void updateDataFileCreate(SdFile *dataFile) +// Count the number of NAV2 messages with rates more than 0. Used for determining if we need the enable +// the global NAV2 feature. +uint8_t getNAV2MessageCount() { - bool timeValid = false; - if (i2cGNSS.getTimeValid() == true && i2cGNSS.getDateValid() == true) //Will pass if ZED's RTC is reporting (regardless of GNSS fix) - timeValid = true; - if (i2cGNSS.getConfirmedTime() == true && i2cGNSS.getConfirmedDate() == true) //Requires GNSS fix - timeValid = true; - - if (timeValid == true) - { - //Update the file create time - dataFile->timestamp(T_CREATE, i2cGNSS.getYear(), i2cGNSS.getMonth(), i2cGNSS.getDay(), i2cGNSS.getHour(), i2cGNSS.getMinute(), i2cGNSS.getSecond()); - } + int enabledMessages = 0; + int startOfBlock = 0; + int endOfBlock = 0; + + setMessageOffsets(&ubxMessages[0], "NAV2", startOfBlock, + endOfBlock); // Find start and stop of given messageType in message array + + for (int x = 0; x < (endOfBlock - startOfBlock); x++) + { + if (settings.ubxMessageRates[x + startOfBlock] > 0) + enabledMessages++; + } + + setMessageOffsets(&ubxMessages[0], "NMEANAV2", startOfBlock, + endOfBlock); // Find start and stop of given messageType in message array + + for (int x = 0; x < (endOfBlock - startOfBlock); x++) + { + if (settings.ubxMessageRates[x + startOfBlock] > 0) + enabledMessages++; + } + + return (enabledMessages); +} + +// Given the name of a message, find it, and set the rate +bool setMessageRateByName(const char *msgName, uint8_t msgRate) +{ + for (int x = 0; x < MAX_UBX_MSG; x++) + { + if (strcmp(ubxMessages[x].msgTextName, msgName) == 0) + { + settings.ubxMessageRates[x] = msgRate; + return (true); + } + } + + systemPrintf("setMessageRateByName: %s not found\r\n", msgName); + return (false); } -//Finds last log -//Returns true if succesful -bool findLastLog(char *lastLogName) +// Given the name of a message, find it, and return the rate +uint8_t getMessageRateByName(const char *msgName) { - bool foundAFile = false; + return (settings.ubxMessageRates[getMessageNumberByName(msgName)]); +} - if (online.microSD == true) - { - //Attempt to access file system. This avoids collisions with file writing in F9PSerialReadTask() - //Wait up to 5s, this is important - if (xSemaphoreTake(xFATSemaphore, 5000 / portTICK_PERIOD_MS) == pdPASS) +// Given the name of a message, return the array number +uint8_t getMessageNumberByName(const char *msgName) +{ + for (int x = 0; x < MAX_UBX_MSG; x++) { - //Count available binaries - SdFile tempFile; - SdFile dir; - const char* LOG_EXTENSION = "ubx"; - const char* LOG_PREFIX = platformFilePrefix; - char fname[50]; //Handle long file names + if (strcmp(ubxMessages[x].msgTextName, msgName) == 0) + return (x); + } - dir.open("/"); //Open root + systemPrintf("getMessageNumberByName: %s not found\r\n", msgName); + return (0); +} - while (tempFile.openNext(&dir, O_READ)) - { - if (tempFile.isFile()) +// Check rates to see if they need to be reset to defaults +void checkMessageRates() +{ + if (settings.ubxMessageRates[0] == 254) + { + // Reset rates to defaults + for (int x = 0; x < MAX_UBX_MSG; x++) { - tempFile.getName(fname, sizeof(fname)); - - //Check for matching file name prefix and extension - if (strcmp(LOG_EXTENSION, &fname[strlen(fname) - strlen(LOG_EXTENSION)]) == 0) - { - if (strstr(fname, LOG_PREFIX) != NULL) - { - strcpy(lastLogName, fname); //Store this file as last known log file - foundAFile = true; - } - } + if (ubxMessages[x].msgClass == UBX_RTCM_MSB) + settings.ubxMessageRates[x] = 0; // For general rover messages, RTCM should be zero by default. + // ubxMessageRatesBase will have the proper defaults. + else + settings.ubxMessageRates[x] = ubxMessages[x].msgDefaultRate; } - tempFile.close(); - } - - xSemaphoreGive(xFATSemaphore); } - } - return (foundAFile); + if (settings.ubxMessageRatesBase[0] == 254) + { + // Reset Base rates to defaults + int firstRTCMRecord = getMessageNumberByName("UBX_RTCM_1005"); + for (int x = 0; x < MAX_UBX_MSG_RTCM; x++) + settings.ubxMessageRatesBase[x] = ubxMessages[firstRTCMRecord + x].msgDefaultRate; + } } -//Given a unique string, find first and last records containing that string in message array -void setMessageOffsets(char* messageType, int& startOfBlock, int& endOfBlock) +// Determine logging type +// If user is logging basic 5 sentences, this is 'S'tandard logging +// If user is logging 7 PPP sentences, this is 'P'PP logging +// If user has other setences turned on, it's custom logging +// This controls the type of icon displayed +void setLoggingType() { - char messageNamePiece[40]; //UBX_RTCM - sprintf(messageNamePiece, "UBX_%s", messageType); //Put UBX_ infront of type - - //Find the first occurrence - for (startOfBlock = 0 ; startOfBlock < MAX_UBX_MSG ; startOfBlock++) - { - if (strstr(ubxMessages[startOfBlock].msgTextName, messageNamePiece) != NULL) break; - } - if (startOfBlock == MAX_UBX_MSG) - { - //Error out - startOfBlock = 0; - endOfBlock = 0; - return; - } - - //Find the last occurrence - for (endOfBlock = startOfBlock + 1 ; endOfBlock < MAX_UBX_MSG ; endOfBlock++) - { - if (strstr(ubxMessages[endOfBlock].msgTextName, messageNamePiece) == NULL) break; - } + loggingType = LOGGING_CUSTOM; + + int messageCount = getActiveMessageCount(); + if (messageCount == 5 || messageCount == 7) + { + if (getMessageRateByName("UBX_NMEA_GGA") > 0 && getMessageRateByName("UBX_NMEA_GSA") > 0 && + getMessageRateByName("UBX_NMEA_GST") > 0 && getMessageRateByName("UBX_NMEA_GSV") > 0 && + getMessageRateByName("UBX_NMEA_RMC") > 0) + { + loggingType = LOGGING_STANDARD; + + if (getMessageRateByName("UBX_RXM_RAWX") > 0 && getMessageRateByName("UBX_RXM_SFRBX") > 0) + loggingType = LOGGING_PPP; + } + } } -//Return the number of active/enabled messages -uint8_t getActiveMessageCount() +// During the logging test, we have to modify the messages and rate of the device +void setLogTestFrequencyMessages(int rate, int messages) { - uint8_t count = 0; - for (int x = 0 ; x < MAX_UBX_MSG ; x++) - if (ubxMessages[x].msgRate > 0) count++; - return (count); + // Set measurement frequency + setRate(1.0 / rate); // Convert Hz to seconds. This will set settings.measurementRate, settings.navigationRate, and + // GSV message + + // Set messages + setGNSSMessageRates(settings.ubxMessageRates, 0); // Turn off all messages + if (messages == 5) + { + setMessageRateByName("UBX_NMEA_GGA", 1); + setMessageRateByName("UBX_NMEA_GSA", 1); + setMessageRateByName("UBX_NMEA_GST", 1); + setMessageRateByName("UBX_NMEA_GSV", rate); // One report per second + setMessageRateByName("UBX_NMEA_RMC", 1); + + log_d("Messages: Surveying Defaults (NMEAx5)"); + } + else if (messages == 7) + { + setMessageRateByName("UBX_NMEA_GGA", 1); + setMessageRateByName("UBX_NMEA_GSA", 1); + setMessageRateByName("UBX_NMEA_GST", 1); + setMessageRateByName("UBX_NMEA_GSV", rate); // One report per second + setMessageRateByName("UBX_NMEA_RMC", 1); + setMessageRateByName("UBX_RXM_RAWX", 1); + setMessageRateByName("UBX_RXM_SFRBX", 1); + + log_d("Messages: PPP NMEAx5+RXMx2"); + } + else + log_d("Unknown message amount"); + + // Apply these message rates to both UART1 / SPI and USB + setMessages(MAX_SET_MESSAGES_RETRIES); // Does a complete open/closed val set + setMessagesUSB(MAX_SET_MESSAGES_RETRIES); } -//Given the name of a message, find it, and set the rate -bool setMessageRateByName(char *msgName, uint8_t msgRate) +// The log test allows us to record a series of different system configurations into +// one file. At the same time, we log the output of the ZED via the USB connection. +// Once complete, the SD log is compared against the USB log to verify both are identical. +// Be sure to set maxLogLength_minutes before running test. maxLogLength_minutes will +// set the length of each test. +void updateLogTest() { - for (int x = 0 ; x < MAX_UBX_MSG ; x++) - { - if (strcmp(ubxMessages[x].msgTextName, msgName) == 0) + // Log is complete, run next text + int rate = 4; + int messages = 5; + int semaphoreWait = 10; + + // Advance to next state + // Note: logTestState is LOGTEST_END by default. + // The increment causes the default switch case to be executed, resetting logTestState to LOGTEST_END. + // The test is started via the debug menu, setting logTestState to LOGTEST_START. + logTestState++; + + switch (logTestState) { - ubxMessages[x].msgRate = msgRate; - return (true); + default: + logTestState = LOGTEST_END; + settings.runLogTest = false; + break; + + case (LOGTEST_4HZ_5MSG_10MS): + // During the first test, create the log file + reuseLastLog = false; + char fileName[100]; + snprintf(fileName, sizeof(fileName), "/%s_LogTest_%02d%02d%02d_%02d%02d%02d.ubx", // SdFat library + platformFilePrefix, rtc.getYear() - 2000, rtc.getMonth() + 1, + rtc.getDay(), // ESP32Time returns month:0-11 + rtc.getHour(true), rtc.getMinute(), rtc.getSecond() // ESP32Time getHour(true) returns hour:0-23 + ); + endSD(false, true); // End previous log + + beginLogging(fileName); + + rate = 4; + messages = 5; + semaphoreWait = 10; + break; + case (LOGTEST_4HZ_7MSG_10MS): + rate = 4; + messages = 7; + semaphoreWait = 10; + break; + case (LOGTEST_10HZ_5MSG_10MS): + rate = 10; + messages = 5; + semaphoreWait = 10; + break; + case (LOGTEST_10HZ_7MSG_10MS): + rate = 10; + messages = 7; + semaphoreWait = 10; + break; + + case (LOGTEST_4HZ_5MSG_0MS): + rate = 4; + messages = 5; + semaphoreWait = 0; + break; + case (LOGTEST_4HZ_7MSG_0MS): + rate = 4; + messages = 7; + semaphoreWait = 0; + break; + case (LOGTEST_10HZ_5MSG_0MS): + rate = 10; + messages = 5; + semaphoreWait = 0; + break; + case (LOGTEST_10HZ_7MSG_0MS): + rate = 10; + messages = 7; + semaphoreWait = 0; + break; + + case (LOGTEST_4HZ_5MSG_50MS): + rate = 4; + messages = 5; + semaphoreWait = 50; + break; + case (LOGTEST_4HZ_7MSG_50MS): + rate = 4; + messages = 7; + semaphoreWait = 50; + break; + case (LOGTEST_10HZ_5MSG_50MS): + rate = 10; + messages = 5; + semaphoreWait = 50; + break; + case (LOGTEST_10HZ_7MSG_50MS): + rate = 10; + messages = 7; + semaphoreWait = 50; + break; + + case (LOGTEST_END): + // Reduce rate + rate = 4; + messages = 5; + semaphoreWait = 10; + setLogTestFrequencyMessages(rate, messages); // Set messages and rate for both UART1 / SPI and USB ports + log_d("Log Test Complete"); + break; } - } - Serial.printf("setMessageRateByName: %s not found\n\r", msgName); - return (false); + if (settings.runLogTest == true) + { + setLogTestFrequencyMessages(rate, messages); // Set messages and rate for both UART1 / SPI and USB ports + + loggingSemaphoreWait_ms = semaphoreWait / portTICK_PERIOD_MS; // Update variable + + startCurrentLogTime_minutes = millis() / 1000L / 60; // Mark now as start of logging + + char logMessage[100]; + snprintf(logMessage, sizeof(logMessage), "Start log test: %dHz, %dMsg, %dMS", rate, messages, semaphoreWait); + + char nmeaMessage[100]; // Max NMEA sentence length is 82 + createNMEASentence(CUSTOM_NMEA_TYPE_LOGTEST_STATUS, nmeaMessage, sizeof(nmeaMessage), + logMessage); // textID, buffer, sizeOfBuffer, text + + if (xSemaphoreTake(sdCardSemaphore, fatSemaphore_longWait_ms) == pdPASS) + { + markSemaphore(FUNCTION_LOGTEST); + + ubxFile->println(nmeaMessage); + + xSemaphoreGive(sdCardSemaphore); + } + else + { + log_w("sdCardSemaphore failed to yield, menuMessages.ino line %d", __LINE__); + } + + systemPrintf("%s\r\n", logMessage); + } } diff --git a/Firmware/RTK_Surveyor/menuPP.ino b/Firmware/RTK_Surveyor/menuPP.ino new file mode 100644 index 000000000..1054a5fae --- /dev/null +++ b/Firmware/RTK_Surveyor/menuPP.ino @@ -0,0 +1,1491 @@ +#ifdef COMPILE_L_BAND + +#include "mbedtls/ssl.h" //Needed for certificate validation + +//---------------------------------------- +// Locals - compiled out +//---------------------------------------- + +#define MQTT_CERT_SIZE 2000 + +// The PointPerfect token is provided at compile time via build flags +#define DEVELOPMENT_TOKEN 0xAA, 0xBB, 0xCC, 0xDD, 0x00, 0x11, 0x22, 0x33, 0x0A, 0x0B, 0x0C, 0x0D, 0x00, 0x01, 0x02, 0x03 +#ifndef POINTPERFECT_TOKEN +#warning Using the DEVELOPMENT_TOKEN for point perfect! +#define POINTPERFECT_TOKEN DEVELOPMENT_TOKEN +#endif // POINTPERFECT_TOKEN + +static const uint8_t developmentTokenArray[16] = {DEVELOPMENT_TOKEN}; // Token in HEX form +static const uint8_t pointPerfectTokenArray[16] = {POINTPERFECT_TOKEN}; // Token in HEX form + +static const char *pointPerfectAPI = "https://api.thingstream.io/ztp/pointperfect/credentials"; + +//---------------------------------------- +// Forward declarations - compiled out +//---------------------------------------- + +void checkRXMCOR(UBX_RXM_COR_data_t *ubxDataStruct); + +//---------------------------------------- +// L-Band Routines - compiled out +//---------------------------------------- + +void menuPointPerfectKeys() +{ + while (1) + { + systemPrintln(); + systemPrintln("Menu: PointPerfect Keys"); + + systemPrint("1) Set ThingStream Device Profile Token: "); + if (strlen(settings.pointPerfectDeviceProfileToken) > 0) + systemPrintln(settings.pointPerfectDeviceProfileToken); + else + systemPrintln("Use SparkFun Token"); + + systemPrint("2) Set Current Key: "); + if (strlen(settings.pointPerfectCurrentKey) > 0) + systemPrintln(settings.pointPerfectCurrentKey); + else + systemPrintln("N/A"); + + systemPrint("3) Set Current Key Expiration Date (DD/MM/YYYY): "); + if (strlen(settings.pointPerfectCurrentKey) > 0 && settings.pointPerfectCurrentKeyStart > 0 && + settings.pointPerfectCurrentKeyDuration > 0) + { + long long gpsEpoch = thingstreamEpochToGPSEpoch(settings.pointPerfectCurrentKeyStart); + + gpsEpoch += (settings.pointPerfectCurrentKeyDuration / 1000) - + 1; // Add key duration back to the key start date to get key expiration + + systemPrintf("%s\r\n", printDateFromGPSEpoch(gpsEpoch)); + + if (settings.debugLBand == true) + systemPrintf("settings.pointPerfectCurrentKeyDuration: %lld (%d)\r\n", + settings.pointPerfectCurrentKeyDuration, + settings.pointPerfectCurrentKeyDuration / (1000L * 60 * 60 * 24)); + } + else + systemPrintln("N/A"); + + systemPrint("4) Set Next Key: "); + if (strlen(settings.pointPerfectNextKey) > 0) + systemPrintln(settings.pointPerfectNextKey); + else + systemPrintln("N/A"); + + systemPrint("5) Set Next Key Expiration Date (DD/MM/YYYY): "); + if (strlen(settings.pointPerfectNextKey) > 0 && settings.pointPerfectNextKeyStart > 0 && + settings.pointPerfectNextKeyDuration > 0) + { + long long gpsEpoch = thingstreamEpochToGPSEpoch(settings.pointPerfectNextKeyStart); + + gpsEpoch += (settings.pointPerfectNextKeyDuration / + 1000); // Add key duration back to the key start date to get key expiration + + systemPrintf("%s\r\n", printDateFromGPSEpoch(gpsEpoch)); + } + else + systemPrintln("N/A"); + + systemPrintln("x) Exit"); + + int incoming = getNumber(); // Returns EXIT, TIMEOUT, or long + + if (incoming == 1) + { + systemPrint("Enter Device Profile Token: "); + getString(settings.pointPerfectDeviceProfileToken, sizeof(settings.pointPerfectDeviceProfileToken)); + } + else if (incoming == 2) + { + systemPrint("Enter Current Key: "); + getString(settings.pointPerfectCurrentKey, sizeof(settings.pointPerfectCurrentKey)); + } + else if (incoming == 3) + { + clearBuffer(); + + systemPrintln("Enter Current Key Expiration Date: "); + uint8_t expDay; + uint8_t expMonth; + uint16_t expYear; + while (getDate(expDay, expMonth, expYear) == false) + { + systemPrintln("Date invalid. Please try again."); + } + + dateToKeyStart(expDay, expMonth, expYear, &settings.pointPerfectCurrentKeyStart); + + // The u-blox API reports key durations of 5 weeks, but the web interface reports expiration dates + // that are 4 weeks. + // If the user has manually entered a date, force duration down to four weeks + settings.pointPerfectCurrentKeyDuration = (1000LL * 60 * 60 * 24 * 28); + + // Calculate the next key expiration date + settings.pointPerfectNextKeyStart = settings.pointPerfectCurrentKeyStart + + settings.pointPerfectCurrentKeyDuration + + 1; // Next key starts after current key + settings.pointPerfectNextKeyDuration = settings.pointPerfectCurrentKeyDuration; + + if (settings.debugLBand == true) + { + systemPrintf(" settings.pointPerfectCurrentKeyStart: %lld - %s\r\n", + settings.pointPerfectCurrentKeyStart, + printDateFromUnixEpoch(settings.pointPerfectCurrentKeyStart / 1000)); + systemPrintf(" settings.pointPerfectCurrentKeyDuration: %lld - %s\r\n", + settings.pointPerfectCurrentKeyDuration, + printDaysFromDuration(settings.pointPerfectCurrentKeyDuration)); + systemPrintf(" settings.pointPerfectNextKeyStart: %lld - %s\r\n", settings.pointPerfectNextKeyStart, + printDateFromUnixEpoch(settings.pointPerfectNextKeyStart / 1000)); + systemPrintf(" settings.pointPerfectNextKeyDuration: %lld - %s\r\n", + settings.pointPerfectNextKeyDuration, + printDaysFromDuration(settings.pointPerfectNextKeyDuration)); + } + } + else if (incoming == 4) + { + systemPrint("Enter Next Key: "); + getString(settings.pointPerfectNextKey, sizeof(settings.pointPerfectNextKey)); + } + else if (incoming == 5) + { + clearBuffer(); + + systemPrintln("Enter Next Key Expiration Date: "); + uint8_t expDay; + uint8_t expMonth; + uint16_t expYear; + while (getDate(expDay, expMonth, expYear) == false) + { + systemPrintln("Date invalid. Please try again."); + } + + dateToKeyStart(expDay, expMonth, expYear, &settings.pointPerfectNextKeyStart); + } + else if (incoming == INPUT_RESPONSE_GETNUMBER_EXIT) + break; + else if (incoming == INPUT_RESPONSE_GETNUMBER_TIMEOUT) + break; + else + printUnknown(incoming); + } + + clearBuffer(); // Empty buffer of any newline chars +} + +// Given a GPS Epoch, return a DD/MM/YYYY string +char *printDateFromGPSEpoch(long long gpsEpoch) +{ + uint16_t keyGPSWeek; + uint32_t keyGPSToW; + epochToWeekToW(gpsEpoch, &keyGPSWeek, &keyGPSToW); + + long expDay; + long expMonth; + long expYear; + gpsWeekToWToDate(keyGPSWeek, keyGPSToW, &expDay, &expMonth, &expYear); + + char *response = (char *)malloc(strlen("01/01/1010")); + + sprintf(response, "%02ld/%02ld/%ld", expDay, expMonth, expYear); + return (response); +} + +// Given a Unix Epoch, return a DD/MM/YYYY string +// https://www.epochconverter.com/programming/c +char *printDateFromUnixEpoch(long long unixEpoch) +{ + char *buf = (char *)malloc(strlen("01/01/2023") + 1); // Make room for terminator + time_t rawtime = unixEpoch; + + struct tm ts; + ts = *localtime(&rawtime); + + // Format time, "dd/mm/yyyy" + strftime(buf, strlen("01/01/2023") + 1, "%d/%m/%Y", &ts); + return (buf); +} + +// Given a duration in ms, print days +char *printDaysFromDuration(long long duration) +{ + float days = duration / (1000.0 * 60 * 60 * 24); // Convert ms to days + + char *response = (char *)malloc(strlen("34.9") + 1); // Make room for terminator + sprintf(response, "%0.2f", days); + return (response); +} + +// Connect to 'home' WiFi and then ThingStream API. This will attach this unique device to the ThingStream network. +bool pointperfectProvisionDevice() +{ +#ifdef COMPILE_WIFI + bool bluetoothOriginallyStarted = true; + if (bluetoothState == BT_OFF) + bluetoothOriginallyStarted = false; + + bluetoothStop(); // Free heap before starting secure client (requires ~70KB) + + DynamicJsonDocument *jsonZtp = nullptr; + char *tempHolderPtr = nullptr; + bool retVal = false; + + do + { + WiFiClientSecure client; + client.setCACert(AWS_PUBLIC_CERT); + + char hardwareID[13]; + snprintf(hardwareID, sizeof(hardwareID), "%02X%02X%02X%02X%02X%02X", lbandMACAddress[0], lbandMACAddress[1], + lbandMACAddress[2], lbandMACAddress[3], lbandMACAddress[4], + lbandMACAddress[5]); // Get ready for JSON + +#ifdef WHITELISTED_ID + // Override ID with testing ID + snprintf(hardwareID, sizeof(hardwareID), "%02X%02X%02X%02X%02X%02X", whitelistID[0], whitelistID[1], + whitelistID[2], whitelistID[3], whitelistID[4], whitelistID[5]); +#endif // WHITELISTED_ID + + // Given name must between 1 and 50 characters + char givenName[100]; + char versionString[9]; + getFirmwareVersion(versionString, sizeof(versionString), false); + + if (productVariant == RTK_FACET_LBAND) + { + // Facet L-Band v3.12 AABBCCDD1122 + snprintf(givenName, sizeof(givenName), "Facet LBand %s - %s", versionString, + hardwareID); // Get ready for JSON + } + else if (productVariant == RTK_FACET_LBAND_DIRECT) + { + // Facet L-Band Direct v3.12 AABBCCDD1122 + snprintf(givenName, sizeof(givenName), "Facet LBand Direct %s - %s", versionString, + hardwareID); // Get ready for JSON + } + + if (strlen(givenName) >= 50) + { + systemPrintf("Error: GivenName '%s' too long: %d bytes\r\n", givenName, strlen(givenName)); + } + + StaticJsonDocument<256> pointPerfectAPIPost; + + // Determine if we use the SparkFun token or custom token + char tokenString[37] = "\0"; + if (strlen(settings.pointPerfectDeviceProfileToken) == 0) + { + // Convert uint8_t array into string with dashes in spots + // We must assume u-blox will not change the position of their dashes or length of their token + if (!memcmp(pointPerfectTokenArray, developmentTokenArray, sizeof(developmentTokenArray))) + systemPrintln("Warning: Using the development token!"); + for (int x = 0; x < sizeof(pointPerfectTokenArray); x++) + { + char temp[3]; + snprintf(temp, sizeof(temp), "%02x", pointPerfectTokenArray[x]); + strcat(tokenString, temp); + if (x == 3 || x == 5 || x == 7 || x == 9) + strcat(tokenString, "-"); + } + } + else + { + // Use the user's custom token + strcpy(tokenString, settings.pointPerfectDeviceProfileToken); + systemPrintf("Using custom token: %s\r\n", tokenString); + } + + pointPerfectAPIPost["token"] = tokenString; + pointPerfectAPIPost["givenName"] = givenName; + pointPerfectAPIPost["hardwareId"] = hardwareID; + // pointPerfectAPIPost["tags"] = "mac"; + + String json; + serializeJson(pointPerfectAPIPost, json); + + systemPrintf("Connecting to: %s\r\n", pointPerfectAPI); + + HTTPClient http; + http.begin(client, pointPerfectAPI); + http.addHeader("Content-Type", "application/json"); + + int httpResponseCode = http.POST(json); + + String response = http.getString(); + + http.end(); + + if (httpResponseCode != 200) + { + systemPrintf("HTTP response error %d: ", httpResponseCode); + systemPrintln(response); + + // If a device has been deactivated, response will be: "HTTP response error 403: No plan for device + // device:9f49e97f-e6a7-4a08-8d58-ac7ecdc90e23" + if (response.indexOf("No plan for device") >= 0) + { + char hardwareID[13]; + snprintf(hardwareID, sizeof(hardwareID), "%02X%02X%02X%02X%02X%02X", lbandMACAddress[0], + lbandMACAddress[1], lbandMACAddress[2], lbandMACAddress[3], lbandMACAddress[4], + lbandMACAddress[5]); + + systemPrintf("This device has been deactivated. Please contact " + "support@sparkfun.com to renew the L-Band " + "subscription. Please reference device ID: %s\r\n", + hardwareID); + + displayAccountExpired(5000); + } + // If a device is not whitelisted, reponse will be: "HTTP response error 403: Device hardware code not + // whitelisted" + else if (response.indexOf("not whitelisted") >= 0) + { + char hardwareID[13]; + snprintf(hardwareID, sizeof(hardwareID), "%02X%02X%02X%02X%02X%02X", lbandMACAddress[0], + lbandMACAddress[1], lbandMACAddress[2], lbandMACAddress[3], lbandMACAddress[4], + lbandMACAddress[5]); + + systemPrintf( + "This device is not white-listed. Please contact " + "support@sparkfun.com to get your subscription activated. Please reference device ID: %s\r\n", + hardwareID); + + displayNotListed(5000); + } + break; + } + else + { + // Device is now active with ThingStream + // Pull pertinent values from response + jsonZtp = new DynamicJsonDocument(4096); + if (!jsonZtp) + { + systemPrintln("ERROR - Failed to allocate jsonZtp!\r\n"); + break; + } + + DeserializationError error = deserializeJson(*jsonZtp, response); + if (DeserializationError::Ok != error) + { + systemPrintln("JSON error"); + break; + } + else + { + tempHolderPtr = (char *)malloc(MQTT_CERT_SIZE); + if (!tempHolderPtr) + { + systemPrintln("ERROR - Failed to allocate tempHolderPtr buffer!\r\n"); + break; + } + strncpy(tempHolderPtr, (const char *)((*jsonZtp)["certificate"]), MQTT_CERT_SIZE - 1); + recordFile("certificate", tempHolderPtr, strlen(tempHolderPtr)); + + strncpy(tempHolderPtr, (const char *)((*jsonZtp)["privateKey"]), MQTT_CERT_SIZE - 1); + recordFile("privateKey", tempHolderPtr, strlen(tempHolderPtr)); + + // Validate the keys + if (!checkCertificates()) + { + systemPrintln("ERROR - Failed to validate the Point Perfect certificates!"); + } + else + { + if (settings.debugPpCertificate) + systemPrintln("Certificates written to the SD card."); + + strcpy(settings.pointPerfectClientID, (const char *)((*jsonZtp)["clientId"])); + strcpy(settings.pointPerfectBrokerHost, (const char *)((*jsonZtp)["brokerHost"])); + + // Note: from the ZTP documentation: + // ["subscriptions"][0] will contain the key distribution topic + // But, assuming the key distribution topic is always ["subscriptions"][0] is potentially brittle + // It is safer to check the "description" contains "key distribution topic" + int subscription = + findZtpJSONEntry("subscriptions", "description", "key distribution topic", jsonZtp); + if (subscription >= 0) + strncpy(settings.pointPerfectLBandTopic, + (const char *)((*jsonZtp)["subscriptions"][subscription]["path"]), + sizeof(settings.pointPerfectLBandTopic)); + + strcpy(settings.pointPerfectCurrentKey, + (const char *)((*jsonZtp)["dynamickeys"]["current"]["value"])); + settings.pointPerfectCurrentKeyDuration = (*jsonZtp)["dynamickeys"]["current"]["duration"]; + settings.pointPerfectCurrentKeyStart = (*jsonZtp)["dynamickeys"]["current"]["start"]; + + strcpy(settings.pointPerfectNextKey, (const char *)((*jsonZtp)["dynamickeys"]["next"]["value"])); + settings.pointPerfectNextKeyDuration = (*jsonZtp)["dynamickeys"]["next"]["duration"]; + settings.pointPerfectNextKeyStart = (*jsonZtp)["dynamickeys"]["next"]["start"]; + + if (settings.debugLBand == true) + { + systemPrintf(" pointPerfectCurrentKey: %s\r\n", settings.pointPerfectCurrentKey); + systemPrintf(" pointPerfectCurrentKeyStart: %lld - %s\r\n", + settings.pointPerfectCurrentKeyStart, + printDateFromUnixEpoch(settings.pointPerfectCurrentKeyStart / + 1000)); // printDateFromUnixEpoch expects seconds + systemPrintf(" pointPerfectCurrentKeyDuration: %lld - %s\r\n", + settings.pointPerfectCurrentKeyDuration, + printDaysFromDuration(settings.pointPerfectCurrentKeyDuration)); + systemPrintf(" pointPerfectNextKey: %s\r\n", settings.pointPerfectNextKey); + systemPrintf(" pointPerfectNextKeyStart: %lld - %s\r\n", settings.pointPerfectNextKeyStart, + printDateFromUnixEpoch(settings.pointPerfectNextKeyStart / 1000)); + systemPrintf(" pointPerfectNextKeyDuration: %lld - %s\r\n", + settings.pointPerfectNextKeyDuration, + printDaysFromDuration(settings.pointPerfectNextKeyDuration)); + } + } + } + } // HTTP Response was 200 + + systemPrintln("Device successfully provisioned. Keys obtained."); + + recordSystemSettings(); + retVal = true; + } while (0); + + // Free the allocated buffers + if (tempHolderPtr) + free(tempHolderPtr); + if (jsonZtp) + delete jsonZtp; + + if (bluetoothOriginallyStarted == true) + bluetoothStart(); + + return (retVal); +#else // COMPILE_WIFI + return (false); +#endif // COMPILE_WIFI +} + +// Find thing3 in (*jsonZtp)[thing1][n][thing2]. Return n on success. Return -1 on error / not found. +int findZtpJSONEntry(const char *thing1, const char *thing2, const char *thing3, DynamicJsonDocument *jsonZtp) +{ + if (!jsonZtp) + return (-1); + + int i = (*jsonZtp)[thing1].size(); + + if (i == 0) + return (-1); + + for (int j = 0; j < i; j++) + if (strstr((const char *)(*jsonZtp)[thing1][j][thing2], thing3) != nullptr) + { + return j; + } + + return (-1); +} + +// Check certificate and privatekey for valid formatting +// Return false if improperly formatted +bool checkCertificates() +{ + bool validCertificates = true; + char *certificateContents = nullptr; // Holds the contents of the keys prior to MQTT connection + char *keyContents = nullptr; + + // Allocate the buffers + certificateContents = (char *)malloc(MQTT_CERT_SIZE); + keyContents = (char *)malloc(MQTT_CERT_SIZE); + if ((!certificateContents) || (!keyContents)) + { + if (certificateContents) + free(certificateContents); + if (keyContents) + free(keyContents); + systemPrintln("Failed to allocate content buffers!"); + return (false); + } + + // Load the certificate + memset(certificateContents, 0, MQTT_CERT_SIZE); + loadFile("certificate", certificateContents); + + if (checkCertificateValidity(certificateContents, strlen(certificateContents)) == false) + { + if (settings.debugPpCertificate) + systemPrintln("Certificate is corrupt."); + validCertificates = false; + } + + // Load the private key + memset(keyContents, 0, MQTT_CERT_SIZE); + loadFile("privateKey", keyContents); + + if (checkPrivateKeyValidity(keyContents, strlen(keyContents)) == false) + { + if (settings.debugPpCertificate) + systemPrintln("PrivateKey is corrupt."); + validCertificates = false; + } + + // Free the content buffers + if (certificateContents) + free(certificateContents); + if (keyContents) + free(keyContents); + + if (settings.debugPpCertificate) + systemPrintln("Stored certificates are valid!"); + return (validCertificates); +} + +// Check if a given certificate is in a valid format +// This was created to detect corrupt or invalid certificates caused by bugs in v3.0 to and including v3.3. +bool checkCertificateValidity(char *certificateContent, int certificateContentSize) +{ + // Check for valid format of certificate + // From ssl_client.cpp + // https://stackoverflow.com/questions/70670070/mbedtls-cannot-parse-valid-x509-certificate + mbedtls_x509_crt certificate; + mbedtls_x509_crt_init(&certificate); + + int result_code = + mbedtls_x509_crt_parse(&certificate, (unsigned char *)certificateContent, certificateContentSize + 1); + + mbedtls_x509_crt_free(&certificate); + + if (result_code < 0) + { + if (settings.debugPpCertificate) + systemPrintln("ERROR - Invalid certificate format!"); + return (false); + } + + return (true); +} + +// Check if a given private key is in a valid format +// This was created to detect corrupt or invalid private keys caused by bugs in v3.0 to and including v3.3. +// See https://github.com/Mbed-TLS/mbedtls/blob/development/library/pkparse.c +bool checkPrivateKeyValidity(char *privateKey, int privateKeySize) +{ + // Check for valid format of private key + // From ssl_client.cpp + // https://stackoverflow.com/questions/70670070/mbedtls-cannot-parse-valid-x509-certificate + mbedtls_pk_context pk; + mbedtls_pk_init(&pk); + + int result_code = mbedtls_pk_parse_key(&pk, (unsigned char *)privateKey, privateKeySize + 1, nullptr, 0); + mbedtls_pk_free(&pk); + if (result_code < 0) + { + if (settings.debugPpCertificate) + systemPrintln("ERROR - Invalid private key format!"); + return (false); + } + return (true); +} + +// When called, removes the files used for SSL to PointPerfect obtained during provisioning +// Also deletes keys so the user can immediately re-provision +void erasePointperfectCredentials() +{ + char fileName[80]; + + snprintf(fileName, sizeof(fileName), "/%s_%s_%d.txt", platformFilePrefix, "certificate", profileNumber); + LittleFS.remove(fileName); + + snprintf(fileName, sizeof(fileName), "/%s_%s_%d.txt", platformFilePrefix, "privateKey", profileNumber); + LittleFS.remove(fileName); + strcpy(settings.pointPerfectCurrentKey, ""); // Clear contents + strcpy(settings.pointPerfectNextKey, ""); // Clear contents +} + +// Subscribe to MQTT channel, grab keys, then stop +bool pointperfectUpdateKeys() +{ +#ifdef COMPILE_WIFI + bool bluetoothOriginallyStarted = true; + if (bluetoothState == BT_OFF) + bluetoothOriginallyStarted = false; + + bluetoothStop(); // Release available heap to allow room for TLS + + char *certificateContents = nullptr; // Holds the contents of the keys prior to MQTT connection + char *keyContents = nullptr; + WiFiClientSecure secureClient; + bool gotKeys = false; + + do + { + // Allocate the buffers + certificateContents = (char *)malloc(MQTT_CERT_SIZE); + keyContents = (char *)malloc(MQTT_CERT_SIZE); + if ((!certificateContents) || (!keyContents)) + { + if (certificateContents) + free(certificateContents); + systemPrintln("Failed to allocate content buffers!"); + break; + } + + // Get the certificate + memset(certificateContents, 0, MQTT_CERT_SIZE); + loadFile("certificate", certificateContents); + secureClient.setCertificate(certificateContents); + + // Get the private key + memset(keyContents, 0, MQTT_CERT_SIZE); + loadFile("privateKey", keyContents); + secureClient.setPrivateKey(keyContents); + + secureClient.setCACert(AWS_PUBLIC_CERT); + + PubSubClient mqttClient(secureClient); + mqttClient.setCallback(mqttCallback); + mqttClient.setServer(settings.pointPerfectBrokerHost, 8883); + + systemPrintf("Attempting to connect to MQTT broker: %s\r\n", settings.pointPerfectBrokerHost); + + if (mqttClient.connect(settings.pointPerfectClientID) == true) + { + // Successful connection + systemPrintln("MQTT connected"); + + mqttClient.subscribe(settings.pointPerfectLBandTopic); + } + else + { + systemPrintln("Failed to connect to MQTT Broker"); + + // MQTT does not provide good error reporting. + // Throw out everything and attempt to provision the device to get better error checking. + pointperfectProvisionDevice(); + break; //Skip the remaining MQTT checking, release resources + } + + systemPrint("Waiting for keys"); + + mqttMessageReceived = false; + + // Wait for callback + startTime = millis(); + while (1) + { + mqttClient.loop(); + if (mqttMessageReceived == true) + break; + if (mqttClient.connected() == false) + { + if (settings.debugLBand == true) + systemPrintln("Client disconnected"); + break; + } + if (millis() - startTime > 8000) + { + if (settings.debugLBand == true) + systemPrintln("Channel failed to respond"); + break; + } + + // Continue waiting for the keys + delay(100); + systemPrint("."); + } + systemPrintln(); + + // Determine if the keys were updated + if (mqttMessageReceived) + { + systemPrintln("Keys successfully updated"); + gotKeys = true; + } + + // Done with the MQTT client + mqttClient.disconnect(); + } while (0); + + // Free the content buffers + if (keyContents) + free(keyContents); + if (certificateContents) + free(certificateContents); + + if (bluetoothOriginallyStarted == true) + bluetoothStart(); + + // Return the key status + return (gotKeys); +#else // COMPILE_WIFI + return (false); +#endif // COMPILE_WIFI +} + +char *ltrim(char *s) +{ + while (isspace(*s)) + s++; + return s; +} + +// Called when a subscribed to message arrives +void mqttCallback(char *topic, byte *message, unsigned int length) +{ + if (String(topic) == settings.pointPerfectLBandTopic) + { + // Separate the UBX message into its constituent Key/ToW/Week parts + // Obtained from SparkFun u-blox Arduino library - setDynamicSPARTNKeys() + byte *payLoad = &message[6]; + uint8_t currentKeyLength = payLoad[5]; + uint16_t currentWeek = (payLoad[7] << 8) | payLoad[6]; + uint32_t currentToW = + (payLoad[11] << 8 * 3) | (payLoad[10] << 8 * 2) | (payLoad[9] << 8 * 1) | (payLoad[8] << 8 * 0); + + char currentKey[currentKeyLength]; + memcpy(¤tKey, &payLoad[20], currentKeyLength); + + uint8_t nextKeyLength = payLoad[13]; + uint16_t nextWeek = (payLoad[15] << 8) | payLoad[14]; + uint32_t nextToW = + (payLoad[19] << 8 * 3) | (payLoad[18] << 8 * 2) | (payLoad[17] << 8 * 1) | (payLoad[16] << 8 * 0); + + char nextKey[nextKeyLength]; + memcpy(&nextKey, &payLoad[20 + currentKeyLength], nextKeyLength); + + // Convert byte array to HEX character array + strcpy(settings.pointPerfectCurrentKey, ""); // Clear contents + strcpy(settings.pointPerfectNextKey, ""); // Clear contents + for (int x = 0; x < 16; x++) // Force length to max of 32 bytes + { + char temp[3]; + snprintf(temp, sizeof(temp), "%02X", currentKey[x]); + strcat(settings.pointPerfectCurrentKey, temp); + + snprintf(temp, sizeof(temp), "%02X", nextKey[x]); + strcat(settings.pointPerfectNextKey, temp); + } + + // Convert from ToW and Week to key duration and key start + WeekToWToUnixEpoch(&settings.pointPerfectCurrentKeyStart, currentWeek, currentToW); + WeekToWToUnixEpoch(&settings.pointPerfectNextKeyStart, nextWeek, nextToW); + + settings.pointPerfectCurrentKeyStart -= getLeapSeconds(); // Remove GPS leap seconds to align with u-blox + settings.pointPerfectNextKeyStart -= getLeapSeconds(); + + settings.pointPerfectCurrentKeyStart *= 1000; // Convert to ms + settings.pointPerfectNextKeyStart *= 1000; + + settings.pointPerfectCurrentKeyDuration = + settings.pointPerfectNextKeyStart - settings.pointPerfectCurrentKeyStart - 1; + // settings.pointPerfectNextKeyDuration = + // settings.pointPerfectCurrentKeyDuration; // We assume next key duration is the same as current key + // duration because we have to + + settings.pointPerfectNextKeyDuration = (1000LL * 60 * 60 * 24 * 28) - 1; // Assume next key duration is 28 days + + if (settings.debugLBand == true) + { + systemPrintln(); + systemPrintf(" pointPerfectCurrentKey: %s\r\n", settings.pointPerfectCurrentKey); + systemPrintf(" pointPerfectCurrentKeyStart: %lld - %s\r\n", settings.pointPerfectCurrentKeyStart, + printDateFromUnixEpoch(settings.pointPerfectCurrentKeyStart)); + systemPrintf(" pointPerfectCurrentKeyDuration: %lld - %s\r\n", settings.pointPerfectCurrentKeyDuration, + printDaysFromDuration(settings.pointPerfectCurrentKeyDuration)); + systemPrintf(" pointPerfectNextKey: %s\r\n", settings.pointPerfectNextKey); + systemPrintf(" pointPerfectNextKeyStart: %lld - %s\r\n", settings.pointPerfectNextKeyStart, + printDateFromUnixEpoch(settings.pointPerfectNextKeyStart)); + systemPrintf(" pointPerfectNextKeyDuration: %lld - %s\r\n", settings.pointPerfectNextKeyDuration, + printDaysFromDuration(settings.pointPerfectNextKeyDuration)); + } + } + + mqttMessageReceived = true; +} + +// Get a date from a user +// Return true if validated +// https://www.includehelp.com/c-programs/validate-date.aspx +bool getDate(uint8_t &dd, uint8_t &mm, uint16_t &yy) +{ + systemPrint("Enter Day: "); + dd = getNumber(); // Returns EXIT, TIMEOUT, or long + + systemPrint("Enter Month: "); + mm = getNumber(); // Returns EXIT, TIMEOUT, or long + + systemPrint("Enter Year (YYYY): "); + yy = getNumber(); // Returns EXIT, TIMEOUT, or long + + // check year + if (yy >= 2022 && yy <= 9999) + { + // check month + if (mm >= 1 && mm <= 12) + { + // check days + if ((dd >= 1 && dd <= 31) && (mm == 1 || mm == 3 || mm == 5 || mm == 7 || mm == 8 || mm == 10 || mm == 12)) + return (true); + else if ((dd >= 1 && dd <= 30) && (mm == 4 || mm == 6 || mm == 9 || mm == 11)) + return (true); + else if ((dd >= 1 && dd <= 28) && (mm == 2)) + return (true); + else if (dd == 29 && mm == 2 && (yy % 400 == 0 || (yy % 4 == 0 && yy % 100 != 0))) + return (true); + else + { + printf("Day is invalid.\n"); + return (false); + } + } + else + { + printf("Month is not valid.\n"); + return (false); + } + } + + printf("Year is not valid.\n"); + return (false); +} + +// Given an epoch in ms, return the number of days from given and Epoch now +int daysFromEpoch(long long endEpoch) +{ + endEpoch /= 1000; // Convert PointPerfect ms Epoch to s + + if (online.rtc == false) + { + // If we don't have RTC we can't calculate days to expire + if (settings.debugLBand == true) + systemPrintln("No RTC available"); + return (0); + } + + long localEpoch = rtc.getEpoch(); + + long delta = endEpoch - localEpoch; // number of s between dates + delta /= (60 * 60); // hours + delta /= 24; // days + return ((int)delta); +} + +// Given the key's starting epoch time, and the key's duration +// Convert from ms to s +// Add leap seconds (the API reports start times with GPS leap seconds removed) +// Convert from unix epoch (the API reports unix epoch time) to GPS epoch (the NED-D9S expects) +// Note: I believe the Thingstream API is reporting startEpoch 18 seconds less than actual +long long thingstreamEpochToGPSEpoch(long long startEpoch) +{ + long long epoch = startEpoch; + epoch /= 1000; // Convert PointPerfect ms Epoch to s + + // Convert Unix Epoch time from PointPerfect to GPS Time Of Week needed for UBX message + long long gpsEpoch = epoch - 315964800 + getLeapSeconds(); // Shift to GPS Epoch. + return (gpsEpoch); +} + +// Query GNSS for current leap seconds +uint8_t getLeapSeconds() +{ + if (online.gnss == true) + { + if (leapSeconds == 0) // Check to see if we've already set it + { + sfe_ublox_ls_src_e leapSecSource; + leapSeconds = theGNSS.getCurrentLeapSeconds(leapSecSource); + return (leapSeconds); + } + } + return (18); // Default to 18 if GNSS is offline +} + +// Covert a given key's expiration date to a GPS Epoch, so that we can calculate GPS Week and ToW +// Add a millisecond to roll over from 11:59UTC to midnight of the following day +// Convert from unix epoch (time lib outputs unix) to GPS epoch (the NED-D9S expects) +long long dateToGPSEpoch(uint8_t day, uint8_t month, uint16_t year) +{ + long long unixEpoch = dateToUnixEpoch(day, month, year); // Returns Unix Epoch + + // Convert Unix Epoch time from PP to GPS Time Of Week needed for UBX message + long long gpsEpoch = unixEpoch - 315964800; // Shift to GPS Epoch. + + return (gpsEpoch); +} + +// Given an epoch, set the GPSWeek and GPSToW +void epochToWeekToW(long long epoch, uint16_t *GPSWeek, uint32_t *GPSToW) +{ + *GPSWeek = (uint16_t)(epoch / (7 * 24 * 60 * 60)); + *GPSToW = (uint32_t)(epoch % (7 * 24 * 60 * 60)); +} + +// Given an epoch, set the GPSWeek and GPSToW +void WeekToWToUnixEpoch(uint64_t *unixEpoch, uint16_t GPSWeek, uint32_t GPSToW) +{ + *unixEpoch = GPSWeek * (7 * 24 * 60 * 60); // 2192 + *unixEpoch += GPSToW; // 518400 + *unixEpoch += 315964800; +} + +// Given a GPS Week and ToW, convert to an expiration date +void gpsWeekToWToDate(uint16_t keyGPSWeek, uint32_t keyGPSToW, long *expDay, long *expMonth, long *expYear) +{ + long gpsDays = gpsToMjd(0, (long)keyGPSWeek, (long)keyGPSToW); // Covert ToW and Week to # of days since Jan 6, 1980 + mjdToDate(gpsDays, expYear, expMonth, expDay); +} + +// Given a date, convert into epoch +// https://www.epochconverter.com/programming/c +long dateToUnixEpoch(uint8_t day, uint8_t month, uint16_t year) +{ + struct tm t; + time_t t_of_day; + + t.tm_year = year - 1900; + t.tm_mon = month - 1; + t.tm_mday = day; + + t.tm_hour = 0; + t.tm_min = 0; + t.tm_sec = 0; + t.tm_isdst = -1; // Is DST on? 1 = yes, 0 = no, -1 = unknown + + t_of_day = mktime(&t); + + return (t_of_day); +} + +// Given a date, calculate and return the key start in unixEpoch +void dateToKeyStart(uint8_t expDay, uint8_t expMonth, uint16_t expYear, uint64_t *settingsKeyStart) +{ + long long expireUnixEpoch = dateToUnixEpoch(expDay, expMonth, expYear); + + // Thingstream lists the date that a key expires at midnight + // So if a user types in March 7th, 2022 as exp date the key's Week and ToW need to be + // calculated from (March 7th - 27 days). + long long startUnixEpoch = expireUnixEpoch - (27 * 24 * 60 * 60); // Move back 27 days + + // Additionally, Thingstream seems to be reporting Epochs that do not have leap seconds + startUnixEpoch -= getLeapSeconds(); // Modify our Epoch to match Point Perfect + + // PointPerfect uses/reports unix epochs in milliseconds + *settingsKeyStart = startUnixEpoch * 1000L; // Convert to ms + + uint16_t keyGPSWeek; + uint32_t keyGPSToW; + long long gpsEpoch = thingstreamEpochToGPSEpoch(*settingsKeyStart); + + epochToWeekToW(gpsEpoch, &keyGPSWeek, &keyGPSToW); + + // Print ToW and Week for debugging + if (settings.debugLBand == true) + { + systemPrintf(" expireUnixEpoch: %lld - %s\r\n", expireUnixEpoch, printDateFromUnixEpoch(expireUnixEpoch)); + systemPrintf(" startUnixEpoch: %lld - %s\r\n", startUnixEpoch, printDateFromUnixEpoch(startUnixEpoch)); + systemPrintf(" gpsEpoch: %lld - %s\r\n", gpsEpoch, printDateFromGPSEpoch(gpsEpoch)); + systemPrintf(" KeyStart: %lld - %s\r\n", *settingsKeyStart, printDateFromUnixEpoch(*settingsKeyStart)); + systemPrintf(" keyGPSWeek: %d\r\n", keyGPSWeek); + systemPrintf(" keyGPSToW: %d\r\n", keyGPSToW); + } +} + +/* + http://www.leapsecond.com/tools/gpsdate.c + Return Modified Julian Day given calendar year, + month (1-12), and day (1-31). + - Valid for Gregorian dates from 17-Nov-1858. + - Adapted from sci.astro FAQ. +*/ + +long dateToMjd(long Year, long Month, long Day) +{ + return 367 * Year - 7 * (Year + (Month + 9) / 12) / 4 - 3 * ((Year + (Month - 9) / 7) / 100 + 1) / 4 + + 275 * Month / 9 + Day + 1721028 - 2400000; +} + +/* + Convert Modified Julian Day to calendar date. + - Assumes Gregorian calendar. + - Adapted from Fliegel/van Flandern ACM 11/#10 p 657 Oct 1968. +*/ + +void mjdToDate(long Mjd, long *Year, long *Month, long *Day) +{ + long J, C, Y, M; + + J = Mjd + 2400001 + 68569; + C = 4 * J / 146097; + J = J - (146097 * C + 3) / 4; + Y = 4000 * (J + 1) / 1461001; + J = J - 1461 * Y / 4 + 31; + M = 80 * J / 2447; + *Day = J - 2447 * M / 80; + J = M / 11; + *Month = M + 2 - (12 * J); + *Year = 100 * (C - 49) + Y + J; +} + +/* + Convert GPS Week and Seconds to Modified Julian Day. + - Ignores UTC leap seconds. +*/ + +long gpsToMjd(long GpsCycle, long GpsWeek, long GpsSeconds) +{ + long GpsDays = ((GpsCycle * 1024) + GpsWeek) * 7 + (GpsSeconds / 86400); + // GpsDays -= 1; //Correction + return dateToMjd(1980, 1, 6) + GpsDays; +} + +// When new PMP message arrives from NEO-D9S push it back to ZED-F9P +void pushRXMPMP(UBX_RXM_PMP_message_data_t *pmpData) +{ + uint16_t payloadLen = ((uint16_t)pmpData->lengthMSB << 8) | (uint16_t)pmpData->lengthLSB; + + if (settings.debugLBand == true && !inMainMenu) + systemPrintf("Pushing %d bytes of RXM-PMP data to GNSS\r\n", payloadLen); + + theGNSS.pushRawData(&pmpData->sync1, (size_t)payloadLen + 6); // Push the sync chars, class, ID, length and payload + theGNSS.pushRawData(&pmpData->checksumA, (size_t)2); // Push the checksum bytes +} + +// If we have decryption keys, and L-Band is online, configure module +void pointperfectApplyKeys() +{ + if (online.lband == true) + { + if (online.gnss == false) + { + if (settings.debugLBand == true) + systemPrintln("ZED-F9P not available"); + return; + } + + // NEO-D9S encrypted PMP messages are only supported on ZED-F9P firmware v1.30 and above + if (zedModuleType != PLATFORM_F9P) + { + systemPrintln("Error: PointPerfect corrections currently only supported on the ZED-F9P."); + return; + } + if (zedFirmwareVersionInt < 130) + { + systemPrintln("Error: PointPerfect corrections currently supported by ZED-F9P firmware v1.30 and above. " + "Please upgrade your ZED firmware: " + "https://learn.sparkfun.com/tutorials/how-to-upgrade-firmware-of-a-u-blox-gnss-receiver"); + return; + } + + if (strlen(settings.pointPerfectNextKey) > 0) + { + const uint8_t currentKeyLengthBytes = 16; + const uint8_t nextKeyLengthBytes = 16; + + uint16_t currentKeyGPSWeek; + uint32_t currentKeyGPSToW; + long long epoch = thingstreamEpochToGPSEpoch(settings.pointPerfectCurrentKeyStart); + epochToWeekToW(epoch, ¤tKeyGPSWeek, ¤tKeyGPSToW); + + uint16_t nextKeyGPSWeek; + uint32_t nextKeyGPSToW; + epoch = thingstreamEpochToGPSEpoch(settings.pointPerfectNextKeyStart); + epochToWeekToW(epoch, &nextKeyGPSWeek, &nextKeyGPSToW); + + theGNSS.setVal8(UBLOX_CFG_SPARTN_USE_SOURCE, 1); // use LBAND PMP message + + theGNSS.setVal8(UBLOX_CFG_MSGOUT_UBX_RXM_COR_I2C, 1); // Enable UBX-RXM-COR messages on I2C + + theGNSS.setVal8(UBLOX_CFG_NAVHPG_DGNSSMODE, + 3); // Set the differential mode - ambiguities are fixed whenever possible + + bool response = theGNSS.setDynamicSPARTNKeys(currentKeyLengthBytes, currentKeyGPSWeek, currentKeyGPSToW, + settings.pointPerfectCurrentKey, nextKeyLengthBytes, + nextKeyGPSWeek, nextKeyGPSToW, settings.pointPerfectNextKey); + + if (response == false) + systemPrintln("setDynamicSPARTNKeys failed"); + else + { + if (settings.debugLBand == true) + systemPrintln("PointPerfect keys applied"); + online.lbandCorrections = true; + } + } + else + { + if (settings.debugLBand == true) + systemPrintln("No PointPerfect keys available"); + } + } +} + +// Check if the PMP data is being decrypted successfully +void checkRXMCOR(UBX_RXM_COR_data_t *ubxDataStruct) +{ + if (settings.debugLBand == true && !inMainMenu) + systemPrintf("L-Band Eb/N0[dB] (>9 is good): %0.2f\r\n", ubxDataStruct->ebno * pow(2, -3)); + + lBandEBNO = ubxDataStruct->ebno * pow(2, -3); + + if (ubxDataStruct->statusInfo.bits.msgDecrypted == 2) // Successfully decrypted + { + lbandCorrectionsReceived = true; + lastLBandDecryption = millis(); + } + else + { + if (settings.debugLBand == true && !inMainMenu) + systemPrintln("PMP decryption failed"); + } +} + +#endif // COMPILE_L_BAND + +//---------------------------------------- +// Global L-Band Routines +//---------------------------------------- + +// Check if NEO-D9S is connected. Configure if available. +void beginLBand() +{ + // Skip if going into configure-via-ethernet mode + if (configureViaEthernet) + { + if (settings.debugLBand == true) + systemPrintln("configureViaEthernet: skipping beginLBand"); + return; + } + +#ifdef COMPILE_L_BAND + if (i2cLBand.begin(Wire, 0x43) == + false) // Connect to the u-blox NEO-D9S using Wire port. The D9S default I2C address is 0x43 (not 0x42) + { + if (settings.debugLBand == true) + systemPrintln("L-Band not detected"); + return; + } + + // Check the firmware version of the NEO-D9S. Based on Example21_ModuleInfo. + if (i2cLBand.getModuleInfo(1100) == true) // Try to get the module info + { + // Reconstruct the firmware version + snprintf(neoFirmwareVersion, sizeof(neoFirmwareVersion), "%s %d.%02d", i2cLBand.getFirmwareType(), + i2cLBand.getFirmwareVersionHigh(), i2cLBand.getFirmwareVersionLow()); + + printNEOInfo(); // Print module firmware version + } + + if (online.gnss == true) + { + theGNSS.checkUblox(); // Regularly poll to get latest data and any RTCM + theGNSS.checkCallbacks(); // Process any callbacks: ie, eventTriggerReceived + } + + uint32_t LBandFreq; + // If we have a fix, check which frequency to use + if (fixType == 2 || fixType == 3 || fixType == 4 || fixType == 5) // 2D, 3D, 3D+DR, or Time + { + int r = 0; // Step through each geographic region + for (; r < numRegionalAreas; r++) + { + if ((longitude >= Regional_Information_Table[r].area.lonWest) + && (longitude <= Regional_Information_Table[r].area.lonEast) + && (latitude >= Regional_Information_Table[r].area.latSouth) + && (latitude <= Regional_Information_Table[r].area.latNorth)) + { + LBandFreq = Regional_Information_Table[r].frequency; + if (settings.debugLBand == true) + systemPrintf("Setting L-Band frequency to %s (%dHz)\r\n", Regional_Information_Table[r].name, LBandFreq); + break; + } + } + if (r == numRegionalAreas) // Geographic region not found + { + LBandFreq = Regional_Information_Table[settings.geographicRegion].frequency; + systemPrintf("Error: Unknown L-Band geographic region. Using %s (%dHz)\r\n", Regional_Information_Table[settings.geographicRegion].name, LBandFreq); + } + } + else + { + LBandFreq = Regional_Information_Table[settings.geographicRegion].frequency; + if (settings.debugLBand == true) + systemPrintf("No fix available for L-Band geographic region determination. Using %s (%dHz)\r\n", Regional_Information_Table[settings.geographicRegion].name, LBandFreq); + } + + bool response = true; + response &= i2cLBand.newCfgValset(); + response &= i2cLBand.addCfgValset(UBLOX_CFG_PMP_CENTER_FREQUENCY, LBandFreq); // Default 1539812500 Hz + response &= i2cLBand.addCfgValset(UBLOX_CFG_PMP_SEARCH_WINDOW, 2200); // Default 2200 Hz + response &= i2cLBand.addCfgValset(UBLOX_CFG_PMP_USE_SERVICE_ID, 0); // Default 1 + response &= i2cLBand.addCfgValset(UBLOX_CFG_PMP_SERVICE_ID, 21845); // Default 50821 + response &= i2cLBand.addCfgValset(UBLOX_CFG_PMP_DATA_RATE, 2400); // Default 2400 bps + response &= i2cLBand.addCfgValset(UBLOX_CFG_PMP_USE_DESCRAMBLER, 1); // Default 1 + response &= i2cLBand.addCfgValset(UBLOX_CFG_PMP_DESCRAMBLER_INIT, 26969); // Default 23560 + response &= i2cLBand.addCfgValset(UBLOX_CFG_PMP_USE_PRESCRAMBLING, 0); // Default 0 + response &= i2cLBand.addCfgValset(UBLOX_CFG_PMP_UNIQUE_WORD, 16238547128276412563ull); + response &= + i2cLBand.addCfgValset(UBLOX_CFG_MSGOUT_UBX_RXM_PMP_UART1, 0); // Diasable UBX-RXM-PMP on UART1. Not used. + + response &= i2cLBand.sendCfgValset(); + + lBandCommunicationEnabled = zedEnableLBandCommunication(); + + if (response == false) + systemPrintln("L-Band failed to configure"); + + i2cLBand.softwareResetGNSSOnly(); // Do a restart + + if (settings.debugLBand == true) + systemPrintln("L-Band online"); + + online.lband = true; +#endif // COMPILE_L_BAND +} + +// Set 'home' WiFi credentials +// Provision device on ThingStream +// Download keys +void menuPointPerfect() +{ +#ifdef COMPILE_L_BAND + while (1) + { + systemPrintln(); + systemPrintln("Menu: PointPerfect Corrections"); + + if (settings.debugLBand == true) + systemPrintf("Time to first L-Band fix: %ds Restarts: %d\r\n", lbandTimeToFix / 1000, lbandRestarts); + + if (settings.debugLBand == true) + systemPrintf("settings.pointPerfectLBandTopic: %s\r\n", settings.pointPerfectLBandTopic); + + systemPrint("Days until keys expire: "); + if (strlen(settings.pointPerfectCurrentKey) > 0) + { + if (online.rtc == false) + { + // If we don't have RTC we can't calculate days to expire + systemPrintln("No RTC"); + } + else + { + int daysRemaining = + daysFromEpoch(settings.pointPerfectNextKeyStart + settings.pointPerfectNextKeyDuration + 1); + + if (daysRemaining < 0) + systemPrintln("Expired"); + else + systemPrintln(daysRemaining); + } + } + else + systemPrintln("No keys"); + + systemPrint("1) Use PointPerfect Corrections: "); + if (settings.enablePointPerfectCorrections == true) + systemPrintln("Enabled"); + else + systemPrintln("Disabled"); + + systemPrint("2) Toggle Auto Key Renewal: "); + if (settings.autoKeyRenewal == true) + systemPrintln("Enabled"); + else + systemPrintln("Disabled"); + + if (strlen(settings.pointPerfectCurrentKey) == 0 || strlen(settings.pointPerfectLBandTopic) == 0) + systemPrintln("3) Provision Device"); + else + systemPrintln("3) Update Keys"); + + systemPrintln("4) Show device ID"); + + systemPrintln("c) Clear the Keys"); + + systemPrintln("k) Manual Key Entry"); + + systemPrint("g) Geographic Region: "); + systemPrintln(Regional_Information_Table[settings.geographicRegion].name); + + systemPrintln("x) Exit"); + + byte incoming = getCharacterNumber(); + + if (incoming == 1) + { + settings.enablePointPerfectCorrections ^= 1; + } + else if (incoming == 2) + { + settings.autoKeyRenewal ^= 1; + } + else if (incoming == 3) + { + if (wifiNetworkCount() == 0) + { + systemPrintln("Error: Please enter at least one SSID before getting keys"); + } + else + { + if (wifiConnect(10000) == true) + { + // Check if we have certificates + char fileName[80]; + snprintf(fileName, sizeof(fileName), "/%s_%s_%d.txt", platformFilePrefix, "certificate", + profileNumber); + if (LittleFS.exists(fileName) == false) + { + pointperfectProvisionDevice(); // Connect to ThingStream API and get keys + } + else if (strlen(settings.pointPerfectCurrentKey) == 0 || + strlen(settings.pointPerfectLBandTopic) == 0) + { + pointperfectProvisionDevice(); // Connect to ThingStream API and get keys + } + else // We have certs and keys + { + // Check that the certs are valid + if (checkCertificates() == true) + { + // Update the keys + pointperfectUpdateKeys(); + } + else + { + // Erase keys + erasePointperfectCredentials(); + + // Provision device + pointperfectProvisionDevice(); // Connect to ThingStream API and get keys + } + } + } + else + { + systemPrintln("Error: No WiFi available to get keys"); + break; + } + } + + WIFI_STOP(); + } + else if (incoming == 4) + { + char hardwareID[13]; + snprintf(hardwareID, sizeof(hardwareID), "%02X%02X%02X%02X%02X%02X", lbandMACAddress[0], lbandMACAddress[1], + lbandMACAddress[2], lbandMACAddress[3], lbandMACAddress[4], lbandMACAddress[5]); + systemPrintf("Device ID: %s\r\n", hardwareID); + } + else if (incoming == 'c') + { + settings.pointPerfectCurrentKey[0] = 0; + settings.pointPerfectNextKey[0] = 0; + } + else if (incoming == 'k') + { + menuPointPerfectKeys(); + } + else if (incoming == 'g') + { + settings.geographicRegion++; + if (settings.geographicRegion >= numRegionalAreas) + settings.geographicRegion = 0; + } + else if (incoming == 'x') + break; + else if (incoming == INPUT_RESPONSE_GETCHARACTERNUMBER_EMPTY) + break; + else if (incoming == INPUT_RESPONSE_GETCHARACTERNUMBER_TIMEOUT) + break; + else + printUnknown(incoming); + } + + if (strlen(settings.pointPerfectClientID) > 0) + { + pointperfectApplyKeys(); + } + + clearBuffer(); // Empty buffer of any newline chars +#endif // COMPILE_L_BAND +} + +// Process any new L-Band from I2C +void updateLBand() +{ + // Skip if in configure-via-ethernet mode + if (configureViaEthernet) + { + if (settings.debugLBand == true) + systemPrintln("configureViaEthernet: skipping updateLBand"); + return; + } + +#ifdef COMPILE_L_BAND + if (online.lbandCorrections == true) + { + i2cLBand.checkUblox(); // Check for the arrival of new PMP data and process it. + i2cLBand.checkCallbacks(); // Check if any L-Band callbacks are waiting to be processed. + + // If a certain amount of time has elapsed between last decryption, turn off L-Band icon + if (lbandCorrectionsReceived == true && millis() - lastLBandDecryption > 5000) + lbandCorrectionsReceived = false; + + // If we don't get an L-Band fix within Timeout, hot-start ZED-F9x + if (systemState == STATE_ROVER_RTK_FLOAT) + { + if (millis() - lbandLastReport > 1000) + { + lbandLastReport = millis(); + + if (settings.debugLBand == true) + systemPrintf("ZED restarts: %d Time remaining before L-Band forced restart: %ds\r\n", lbandRestarts, + settings.lbandFixTimeout_seconds - ((millis() - lbandTimeFloatStarted) / 1000)); + } + + if (settings.lbandFixTimeout_seconds > 0) + { + if ((millis() - lbandTimeFloatStarted) > (settings.lbandFixTimeout_seconds * 1000L)) + { + lbandRestarts++; + + lbandTimeFloatStarted = + millis(); // Restart timer for L-Band. Don't immediately reset ZED to achieve fix. + + // Hotstart ZED to try to get RTK lock + theGNSS.softwareResetGNSSOnly(); + + if (settings.debugLBand == true) + systemPrintf("Restarting ZED. Number of L-Band restarts: %d\r\n", lbandRestarts); + } + } + } + else if (carrSoln == 2 && lbandTimeToFix == 0) + { + lbandTimeToFix = millis(); + if (settings.debugLBand == true) + systemPrintf("Time to first L-Band fix: %ds\r\n", lbandTimeToFix / 1000); + } + + if ((millis() - rtcmLastPacketReceived) / 1000 > settings.rtcmTimeoutBeforeUsingLBand_s) + { + // If we have not received RTCM in a certain amount of time, + // and if communication was disabled because RTCM was being received at some point, + // re-enable L-Band communcation + if (lBandCommunicationEnabled == false) + { + if (settings.debugLBand == true) + systemPrintln("Enabling L-Band communication due to RTCM timeout"); + lBandCommunicationEnabled = zedEnableLBandCommunication(); + } + } + else + { + // If we *have* recently received RTCM then disable corrections from then NEO-D9S L-Band receiver + if (lBandCommunicationEnabled == true) + { + if (settings.debugLBand == true) + systemPrintln("Disabling L-Band communication due to RTCM reception"); + lBandCommunicationEnabled = !zedDisableLBandCommunication(); // zedDisableLBandCommunication() returns + // true if we successfully disabled + } + } + } + +#endif // COMPILE_L_BAND +} diff --git a/Firmware/RTK_Surveyor/menuPorts.ino b/Firmware/RTK_Surveyor/menuPorts.ino index 6651c23c4..d4e255f2b 100644 --- a/Firmware/RTK_Surveyor/menuPorts.ino +++ b/Firmware/RTK_Surveyor/menuPorts.ino @@ -1,159 +1,593 @@ void menuPorts() { - if(productVariant == RTK_SURVEYOR) - menuPortsSurveyor(); - else if(productVariant == RTK_EXPRESS) - menuPortsExpress(); + if (productVariant == RTK_SURVEYOR || productVariant == REFERENCE_STATION) + menuPortsSurveyor(); + else if (productVariant == RTK_EXPRESS || productVariant == RTK_EXPRESS_PLUS || productVariant == RTK_FACET || + productVariant == RTK_FACET_LBAND || productVariant == RTK_FACET_LBAND_DIRECT) + menuPortsMultiplexed(); } -//Set the baud rates for the radio and data ports +// Set the baud rates for the radio and data ports void menuPortsSurveyor() { - while (1) - { - Serial.println(); - Serial.println(F("Menu: Port Menu")); + while (1) + { + systemPrintln(); + systemPrintln("Menu: Ports"); - Serial.print(F("1) Set serial baud rate for Radio Port: ")); - Serial.print(getSerialRate(COM_PORT_UART2)); - Serial.println(F(" bps")); + systemPrint("1) Set serial baud rate for Radio Port: "); + if (settings.radioPortBaud == 0) + { + systemPrintln("Disabled"); + } + else + { + systemPrint(theGNSS.getVal32(UBLOX_CFG_UART2_BAUDRATE)); + systemPrintln(" bps"); + } - Serial.print(F("2) Set serial baud rate for Data Port: ")); - Serial.print(getSerialRate(COM_PORT_UART1)); - Serial.println(F(" bps")); + systemPrint("2) Set serial baud rate for Data Port: "); + if (settings.dataPortBaud == 0) + { + systemPrintln("Disabled"); + } + else + { + systemPrint(theGNSS.getVal32(UBLOX_CFG_UART1_BAUDRATE)); + systemPrintln(" bps"); + } - Serial.println(F("x) Exit")); + systemPrint("3) GNSS UART2 UBX Protocol In: "); + if (settings.enableUART2UBXIn == true) + systemPrintln("Enabled"); + else + systemPrintln("Disabled"); - byte incoming = getByteChoice(menuTimeout); //Timeout after x seconds + systemPrintln("x) Exit"); - if (incoming == '1') - { - Serial.print(F("Enter baud rate (4800 to 921600) for Radio Port: ")); - int newBaud = getNumber(menuTimeout); //Timeout after x seconds - if (newBaud < 4800 || newBaud > 921600) - { - Serial.println(F("Error: baud rate out of range")); - } - else - { - settings.radioPortBaud = newBaud; - i2cGNSS.setSerialRate(newBaud, COM_PORT_UART2); //Set Radio Port - } + int incoming = getNumber(); // Returns EXIT, TIMEOUT, or long + + if (incoming == 1) + { + systemPrint("Enter baud rate (4800 to 921600, 0 = disable) for Radio Port: "); + int newBaud = getNumber(); // Returns EXIT, TIMEOUT, or long + if ((newBaud != INPUT_RESPONSE_GETNUMBER_EXIT) && (newBaud != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + { + if (newBaud == 0 || newBaud == 4800 || newBaud == 9600 || newBaud == 19200 || newBaud == 38400 || + newBaud == 57600 || newBaud == 115200 || newBaud == 230400 || newBaud == 460800 || + newBaud == 921600) + { + settings.radioPortBaud = newBaud; + if (online.gnss == true) + { + if (newBaud == 0) + { + // Disable all protocols in/out of UART2 + bool response = true; + + response &= theGNSS.newCfgValset(); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART2OUTPROT_UBX, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART2OUTPROT_NMEA, 0); + if (commandSupported(UBLOX_CFG_UART2OUTPROT_RTCM3X) == true) + response &= theGNSS.addCfgValset(UBLOX_CFG_UART2OUTPROT_RTCM3X, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART2INPROT_UBX, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART2INPROT_NMEA, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART2INPROT_RTCM3X, 0); + if (commandSupported(UBLOX_CFG_UART2INPROT_SPARTN) == true) + response &= theGNSS.addCfgValset(UBLOX_CFG_UART2INPROT_SPARTN, 0); + response &= theGNSS.sendCfgValset(); + + if (response == false) + systemPrintln("Failed to set UART2 settings"); + } + else + { + bool response = true; + + response &= theGNSS.newCfgValset(); + + // Set the baud rate + response &= theGNSS.addCfgValset(UBLOX_CFG_UART2_BAUDRATE, settings.radioPortBaud); + + // Set the UART2 to only do RTCM (in case this device goes into base mode) + response &= theGNSS.addCfgValset(UBLOX_CFG_UART2OUTPROT_UBX, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART2OUTPROT_NMEA, 0); + if (commandSupported(UBLOX_CFG_UART2OUTPROT_RTCM3X) == true) + response &= theGNSS.addCfgValset(UBLOX_CFG_UART2OUTPROT_RTCM3X, 1); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART2INPROT_UBX, settings.enableUART2UBXIn); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART2INPROT_NMEA, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART2INPROT_RTCM3X, 1); + if (commandSupported(UBLOX_CFG_UART2INPROT_SPARTN) == true) + response &= theGNSS.addCfgValset(UBLOX_CFG_UART2INPROT_SPARTN, 0); + + response &= theGNSS.sendCfgValset(); + + if (response == false) + systemPrintln("Failed to set UART2 settings"); + } + } + } + else + { + systemPrintln("Error: Baud rate out of range"); + } + } + } + else if (incoming == 2) + { + systemPrint("Enter baud rate (4800 to 921600, 0 = disable) for Data Port: "); + int newBaud = getNumber(); // Returns EXIT, TIMEOUT, or long + if ((newBaud != INPUT_RESPONSE_GETNUMBER_EXIT) && (newBaud != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + { + if (newBaud == 0 || newBaud == 4800 || newBaud == 9600 || newBaud == 19200 || newBaud == 38400 || + newBaud == 57600 || newBaud == 115200 || newBaud == 230400 || newBaud == 460800 || + newBaud == 921600) + { + settings.dataPortBaud = newBaud; + if (online.gnss == true) + { + if (newBaud == 0) + { + // Disable all protocols in/out of UART1 + bool response = true; + + response &= theGNSS.newCfgValset(); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1OUTPROT_UBX, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1OUTPROT_NMEA, 0); + if (commandSupported(UBLOX_CFG_UART1OUTPROT_RTCM3X) == true) + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1OUTPROT_RTCM3X, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1INPROT_UBX, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1INPROT_NMEA, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1INPROT_RTCM3X, 0); + if (commandSupported(UBLOX_CFG_UART1INPROT_SPARTN) == true) + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1INPROT_SPARTN, 0); + response &= theGNSS.sendCfgValset(); + + if (response == false) + systemPrintln("Failed to set UART1 settings"); + } + else + { + bool response = true; + + response &= theGNSS.newCfgValset(); + + // Set the baud rate + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1_BAUDRATE, settings.dataPortBaud); + + // Turn on all protocols except SPARTN + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1OUTPROT_UBX, 1); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1OUTPROT_NMEA, 1); + if (commandSupported(UBLOX_CFG_UART1OUTPROT_RTCM3X) == true) + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1OUTPROT_RTCM3X, 1); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1INPROT_UBX, 1); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1INPROT_NMEA, 1); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1INPROT_RTCM3X, 1); + if (commandSupported(UBLOX_CFG_UART1INPROT_SPARTN) == true) + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1INPROT_SPARTN, 0); + response &= theGNSS.sendCfgValset(); + + if (response == false) + systemPrintln("Failed to set UART1 settings"); + } + } + } + else + { + systemPrintln("Error: Baud rate out of range"); + } + } + } + else if (incoming == 3) + { + settings.enableUART2UBXIn ^= 1; + systemPrintln("UART2 Protocol In updated. Changes will be applied at next restart"); + } + + else if (incoming == 'x') + break; + else if (incoming == INPUT_RESPONSE_GETNUMBER_EXIT) + break; + else if (incoming == INPUT_RESPONSE_GETNUMBER_TIMEOUT) + break; + else + printUnknown(incoming); } - else if (incoming == '2') + + clearBuffer(); // Empty buffer of any newline chars +} + +// Set the baud rates for the radio and data ports +void menuPortsMultiplexed() +{ + while (1) { - Serial.print(F("Enter baud rate (4800 to 921600) for Data Port: ")); - int newBaud = getNumber(menuTimeout); //Timeout after x seconds - if (newBaud < 4800 || newBaud > 921600) - { - Serial.println(F("Error: baud rate out of range")); - } - else - { - settings.dataPortBaud = newBaud; - i2cGNSS.setSerialRate(newBaud, COM_PORT_UART1); //Set Data Port - } - } + systemPrintln(); + systemPrintln("Menu: Ports"); + + systemPrint("1) Set Radio port serial baud rate: "); + if (settings.radioPortBaud == 0) + { + systemPrintln("Disabled"); + } + else + { + systemPrint(theGNSS.getVal32(UBLOX_CFG_UART2_BAUDRATE)); + systemPrintln(" bps"); + } + + systemPrint("2) Set Data port connections: "); + if (settings.dataPortChannel == MUX_UBLOX_NMEA) + systemPrintln("NMEA TX Out/RX In"); + else if (settings.dataPortChannel == MUX_PPS_EVENTTRIGGER) + systemPrintln("PPS OUT/Event Trigger In"); + else if (settings.dataPortChannel == MUX_I2C_WT) + { + if (zedModuleType == PLATFORM_F9P) + systemPrintln("I2C SDA/SCL"); + else if (zedModuleType == PLATFORM_F9R) + systemPrintln("Wheel Tick/Direction"); + } + else if (settings.dataPortChannel == MUX_ADC_DAC) + systemPrintln("ESP32 DAC Out/ADC In"); + + if (settings.dataPortChannel == MUX_UBLOX_NMEA) + { + systemPrint("3) Set Data port serial baud rate: "); + if (settings.dataPortBaud == 0) + { + systemPrintln("Disabled"); + } + else + { + systemPrint(theGNSS.getVal32(UBLOX_CFG_UART1_BAUDRATE)); + systemPrintln(" bps"); + } + } + else if (settings.dataPortChannel == MUX_PPS_EVENTTRIGGER) + { + systemPrintln("3) Configure External Triggers"); + } + + systemPrint("4) GNSS UART2 UBX Protocol In: "); + if (settings.enableUART2UBXIn == true) + systemPrintln("Enabled"); + else + systemPrintln("Disabled"); + + // The Facet L-Band Direct has ZED tied to NEO over serial. Attaching an external radio + // will cause the NEO to compete with the radio. If user wants to use external radio, we + // switch off the NEO and send PMP over I2C. By default, Facet L-Band v14 does not + // useI2cForLbandCorrections. + if (productVariant == RTK_FACET_LBAND_DIRECT) + { + systemPrint("5) Enable external radio: "); + if (settings.useI2cForLbandCorrections == true) + systemPrintln("Enabled"); + else + systemPrintln("Disabled"); + } + + systemPrintln("x) Exit"); + + int incoming = getNumber(); // Returns EXIT, TIMEOUT, or long + + if (incoming == 1) + { + systemPrint("Enter baud rate (4800 to 921600, 0 = disable) for Radio Port: "); + int newBaud = getNumber(); // Returns EXIT, TIMEOUT, or long + if ((newBaud != INPUT_RESPONSE_GETNUMBER_EXIT) && (newBaud != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + { + if (newBaud == 0 || newBaud == 4800 || newBaud == 9600 || newBaud == 19200 || newBaud == 38400 || + newBaud == 57600 || newBaud == 115200 || newBaud == 230400 || newBaud == 460800 || + newBaud == 921600) + { + settings.radioPortBaud = newBaud; + if (online.gnss == true) + { + if (newBaud == 0) + { + // Disable all protocols in/out of UART2 + bool response = true; + + response &= theGNSS.newCfgValset(); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART2OUTPROT_UBX, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART2OUTPROT_NMEA, 0); + if (commandSupported(UBLOX_CFG_UART2OUTPROT_RTCM3X) == true) + response &= theGNSS.addCfgValset(UBLOX_CFG_UART2OUTPROT_RTCM3X, 0); + + response &= theGNSS.addCfgValset(UBLOX_CFG_UART2INPROT_UBX, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART2INPROT_NMEA, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART2INPROT_RTCM3X, 0); + if (commandSupported(UBLOX_CFG_UART2INPROT_SPARTN) == true) + response &= theGNSS.addCfgValset(UBLOX_CFG_UART2INPROT_SPARTN, 0); + response &= theGNSS.sendCfgValset(); - else if (incoming == 'x') - break; - else if (incoming == STATUS_GETBYTE_TIMEOUT) - break; - else - printUnknown(incoming); - } + if (response == false) + systemPrintln("Failed to set UART2 settings"); + } + else + { + bool response = true; - while (Serial.available()) Serial.read(); //Empty buffer of any newline chars + response &= theGNSS.newCfgValset(); + + // Set the baud rate + response &= theGNSS.addCfgValset(UBLOX_CFG_UART2_BAUDRATE, settings.radioPortBaud); + + // Set the UART2 to only do RTCM (in case this device goes into base mode) + response &= theGNSS.addCfgValset(UBLOX_CFG_UART2OUTPROT_UBX, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART2OUTPROT_NMEA, 0); + if (commandSupported(UBLOX_CFG_UART2OUTPROT_RTCM3X) == true) + response &= theGNSS.addCfgValset(UBLOX_CFG_UART2OUTPROT_RTCM3X, 1); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART2INPROT_UBX, settings.enableUART2UBXIn); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART2INPROT_NMEA, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART2INPROT_RTCM3X, 1); + if (commandSupported(UBLOX_CFG_UART2INPROT_SPARTN) == true) + response &= theGNSS.addCfgValset(UBLOX_CFG_UART2INPROT_SPARTN, 0); + response &= theGNSS.sendCfgValset(); + + if (response == false) + systemPrintln("Failed to set UART2 settings"); + } + } + } + else + { + systemPrintln("Error: Baud rate out of range"); + } + } + } + else if (incoming == 2) + { + systemPrintln("\r\nEnter the pin connection to use (1 to 4) for Data Port: "); + systemPrintln("1) NMEA TX Out/RX In"); + systemPrintln("2) PPS OUT/Event Trigger In"); + if (zedModuleType == PLATFORM_F9P) + systemPrintln("3) I2C SDA/SCL"); + else if (zedModuleType == PLATFORM_F9R) + systemPrintln("3) Wheel Tick/Direction"); + systemPrintln("4) ESP32 DAC Out/ADC In"); + + int muxPort = getNumber(); // Returns EXIT, TIMEOUT, or long + if (muxPort < 1 || muxPort > 4) + { + systemPrintln("Error: Pin connection out of range"); + } + else + { + settings.dataPortChannel = (muxConnectionType_e)(muxPort - 1); // Adjust user input from 1-4 to 0-3 + setMuxport(settings.dataPortChannel); + } + } + else if (incoming == 3 && settings.dataPortChannel == MUX_UBLOX_NMEA) + { + systemPrint("Enter baud rate (4800 to 921600, 0 = disable) for Data Port: "); + int newBaud = getNumber(); // Returns EXIT, TIMEOUT, or long + if ((newBaud != INPUT_RESPONSE_GETNUMBER_EXIT) && (newBaud != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + { + if (newBaud == 0 || newBaud == 4800 || newBaud == 9600 || newBaud == 19200 || newBaud == 38400 || + newBaud == 57600 || newBaud == 115200 || newBaud == 230400 || newBaud == 460800 || + newBaud == 921600) + { + settings.dataPortBaud = newBaud; + if (online.gnss == true) + { + if (newBaud == 0) + { + // Disable all protocols in/out of UART1 + bool response = true; + + response &= theGNSS.newCfgValset(); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1OUTPROT_UBX, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1OUTPROT_NMEA, 0); + if (commandSupported(UBLOX_CFG_UART1OUTPROT_RTCM3X) == true) + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1OUTPROT_RTCM3X, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1INPROT_UBX, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1INPROT_NMEA, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1INPROT_RTCM3X, 0); + if (commandSupported(UBLOX_CFG_UART1INPROT_SPARTN) == true) + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1INPROT_SPARTN, 0); + response &= theGNSS.sendCfgValset(); + + if (response == false) + systemPrintln("Failed to set UART1 settings"); + } + else + { + bool response = true; + + response &= theGNSS.newCfgValset(); + + // Set the baud rate + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1_BAUDRATE, settings.dataPortBaud); + + // Turn on all protocols except SPARTN + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1OUTPROT_UBX, 1); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1OUTPROT_NMEA, 1); + if (commandSupported(UBLOX_CFG_UART1OUTPROT_RTCM3X) == true) + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1OUTPROT_RTCM3X, 1); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1INPROT_UBX, 1); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1INPROT_NMEA, 1); + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1INPROT_RTCM3X, 1); + if (commandSupported(UBLOX_CFG_UART1INPROT_SPARTN) == true) + response &= theGNSS.addCfgValset(UBLOX_CFG_UART1INPROT_SPARTN, 0); + + response &= theGNSS.sendCfgValset(); + + if (response == false) + systemPrintln("Failed to set UART1 settings"); + } + } + } + else + { + systemPrintln("Error: Baud rate out of range"); + } + } + } + else if (incoming == 3 && settings.dataPortChannel == MUX_PPS_EVENTTRIGGER) + { + menuPortHardwareTriggers(); + } + else if (incoming == 4) + { + settings.enableUART2UBXIn ^= 1; + systemPrintln("UART2 Protocol In updated. Changes will be applied at next restart."); + } + else if (productVariant == RTK_FACET_LBAND_DIRECT && incoming == 5) + { + settings.useI2cForLbandCorrectionsConfigured = + true; // Record that the user has manually modified the settings. + settings.useI2cForLbandCorrections ^= 1; + systemPrintln("External radio port updated. Changes will be applied at next restart."); + } + else if (incoming == 'x') + break; + else if (incoming == INPUT_RESPONSE_GETNUMBER_EXIT) + break; + else if (incoming == INPUT_RESPONSE_GETNUMBER_TIMEOUT) + break; + else + printUnknown(incoming); + } + + clearBuffer(); // Empty buffer of any newline chars } -//Set the baud rates for the radio and data ports -void menuPortsExpress() +// Configure the behavior of the PPS and INT pins on the ZED-F9P +// Most often used for logging events (inputs) and when external triggers (outputs) occur +void menuPortHardwareTriggers() { - while (1) - { - Serial.println(); - Serial.println(F("Menu: Port Menu")); - - Serial.print(F("1) Set Radio port serial baud rate: ")); - Serial.print(getSerialRate(COM_PORT_UART2)); - Serial.println(F(" bps")); - - Serial.print(F("2) Set Data port connections: ")); - if (settings.dataPortChannel == MUX_UBLOX_NMEA) - Serial.println(F("NMEA TX Out/RX In")); - else if (settings.dataPortChannel == MUX_PPS_EVENTTRIGGER) - Serial.println(F("PPS OUT/Event Trigger In")); - else if (settings.dataPortChannel == MUX_I2C) - Serial.println(F("I2C SDA/SCL")); - else if (settings.dataPortChannel == MUX_ADC_DAC) - Serial.println(F("ESP32 DAC Out/ADC In")); - - - if (settings.dataPortChannel == MUX_UBLOX_NMEA) + bool updateSettings = false; + while (1) { - Serial.print(F("3) Set Data port serial baud rate: ")); - Serial.print(getSerialRate(COM_PORT_UART1)); - Serial.println(F(" bps")); - } + systemPrintln(); + systemPrintln("Menu: Port Hardware Trigger"); - Serial.println(F("x) Exit")); + systemPrint("1) Enable External Pulse: "); + if (settings.enableExternalPulse == true) + systemPrintln("Enabled"); + else + systemPrintln("Disabled"); - byte incoming = getByteChoice(menuTimeout); //Timeout after x seconds + if (settings.enableExternalPulse == true) + { + systemPrint("2) Set time between pulses: "); + systemPrint(settings.externalPulseTimeBetweenPulse_us / 1000.0, 0); + systemPrintln("ms"); - if (incoming == '1') - { - Serial.print(F("Enter baud rate (4800 to 921600) for Radio Port: ")); - int newBaud = getNumber(menuTimeout); //Timeout after x seconds - if (newBaud < 4800 || newBaud > 921600) - { - Serial.println(F("Error: baud rate out of range")); - } - else - { - settings.radioPortBaud = newBaud; - i2cGNSS.setSerialRate(newBaud, COM_PORT_UART2); //Set Radio Port - } + systemPrint("3) Set pulse length: "); + systemPrint(settings.externalPulseLength_us / 1000.0, 0); + systemPrintln("ms"); + + systemPrint("4) Set pulse polarity: "); + if (settings.externalPulsePolarity == PULSE_RISING_EDGE) + systemPrintln("Rising"); + else + systemPrintln("Falling"); + } + + systemPrint("5) Log External Events: "); + if (settings.enableExternalHardwareEventLogging == true) + systemPrintln("Enabled"); + else + systemPrintln("Disabled"); + + systemPrintln("x) Exit"); + + int incoming = getNumber(); // Returns EXIT, TIMEOUT, or long + + if (incoming == 1) + { + settings.enableExternalPulse ^= 1; + updateSettings = true; + } + else if (incoming == 2 && settings.enableExternalPulse == true) + { + systemPrint("Time between pulses in milliseconds: "); + long pulseTime = getNumber(); // Returns EXIT, TIMEOUT, or long + + if (pulseTime != INPUT_RESPONSE_GETNUMBER_TIMEOUT && pulseTime != INPUT_RESPONSE_GETNUMBER_EXIT) + { + if (pulseTime < 1 || pulseTime > 60000) // 60s max + systemPrintln("Error: Time between pulses out of range"); + else + { + settings.externalPulseTimeBetweenPulse_us = pulseTime * 1000; + + if (pulseTime < + (settings.externalPulseLength_us / 1000)) // pulseTime must be longer than pulseLength + settings.externalPulseLength_us = settings.externalPulseTimeBetweenPulse_us / + 2; // Force pulse length to be 1/2 time between pulses + + updateSettings = true; + } + } + } + else if (incoming == 3 && settings.enableExternalPulse == true) + { + systemPrint("Pulse length in milliseconds: "); + long pulseLength = getNumber(); // Returns EXIT, TIMEOUT, or long + + if (pulseLength != INPUT_RESPONSE_GETNUMBER_TIMEOUT && pulseLength != INPUT_RESPONSE_GETNUMBER_EXIT) + { + if (pulseLength > + (settings.externalPulseTimeBetweenPulse_us / 1000)) // pulseLength must be shorter than pulseTime + systemPrintln("Error: Pulse length must be shorter than time between pulses"); + else + { + settings.externalPulseLength_us = pulseLength * 1000; + updateSettings = true; + } + } + } + else if (incoming == 4 && settings.enableExternalPulse == true) + { + if (settings.externalPulsePolarity == PULSE_RISING_EDGE) + settings.externalPulsePolarity = PULSE_FALLING_EDGE; + else + settings.externalPulsePolarity = PULSE_RISING_EDGE; + updateSettings = true; + } + else if (incoming == 5) + { + settings.enableExternalHardwareEventLogging ^= 1; + updateSettings = true; + } + else if (incoming == 'x') + break; + else if (incoming == INPUT_RESPONSE_GETNUMBER_EXIT) + break; + else if (incoming == INPUT_RESPONSE_GETNUMBER_TIMEOUT) + break; + else + printUnknown(incoming); } - else if (incoming == '2') + + clearBuffer(); // Empty buffer of any newline chars + + if (updateSettings) { - Serial.println(F("\n\rEnter the pin connection to use (1 to 4) for Data Port: ")); - Serial.println(F("1) NMEA TX Out/RX In")); - Serial.println(F("2) PPS OUT/Event Trigger In")); - Serial.println(F("3) I2C SDA/SCL")); - Serial.println(F("4) ESP32 DAC Out/ADC In")); - - int muxPort = getNumber(menuTimeout); //Timeout after x seconds - if (muxPort < 1 || muxPort > 4) - { - Serial.println(F("Error: Pin connection out of range")); - } - else - { - settings.dataPortChannel = (muxConnectionType_e)(muxPort - 1); //Adjust user input from 1-4 to 0-3 - setMuxport(settings.dataPortChannel); - } + settings.updateZEDSettings = true; // Force update + beginExternalTriggers(); // Update with new settings } - else if (incoming == '3' && settings.dataPortChannel == MUX_UBLOX_NMEA) +} + +void eventTriggerReceived(UBX_TIM_TM2_data_t *ubxDataStruct) +{ + // It is the rising edge of the sound event (TRIG) which is important + // The falling edge is less useful, as it will be "debounced" by the loop code + if (ubxDataStruct->flags.bits.newRisingEdge) // 1 if a new rising edge was detected { - Serial.print(F("Enter baud rate (4800 to 921600) for Data Port: ")); - int newBaud = getNumber(menuTimeout); //Timeout after x seconds - if (newBaud < 4800 || newBaud > 921600) - { - Serial.println(F("Error: baud rate out of range")); - } - else - { - settings.dataPortBaud = newBaud; - i2cGNSS.setSerialRate(newBaud, COM_PORT_UART1); //Set Data Port - } + systemPrintln("Rising Edge Event"); + + triggerCount = ubxDataStruct->count; + triggerTowMsR = ubxDataStruct->towMsR; // Time Of Week of rising edge (ms) + triggerTowSubMsR = + ubxDataStruct->towSubMsR; // Millisecond fraction of Time Of Week of rising edge in nanoseconds + triggerAccEst = ubxDataStruct->accEst; // Nanosecond accuracy estimate + + newEventToRecord = true; } - else if (incoming == 'x') - break; - else if (incoming == STATUS_GETBYTE_TIMEOUT) - break; - else - printUnknown(incoming); - } - - while (Serial.available()) Serial.read(); //Empty buffer of any newline chars } diff --git a/Firmware/RTK_Surveyor/menuSystem.ino b/Firmware/RTK_Surveyor/menuSystem.ino new file mode 100644 index 000000000..d914f75ac --- /dev/null +++ b/Firmware/RTK_Surveyor/menuSystem.ino @@ -0,0 +1,1625 @@ +// Display current system status +void menuSystem() +{ + while (1) + { + systemPrintln(); + systemPrintln("Menu: System"); + + beginI2C(); + if (online.i2c == false) + systemPrintln("I2C: Offline - Something is causing bus problems"); + + systemPrint("GNSS: "); + if (online.gnss == true) + { + systemPrint("Online - "); + + printZEDInfo(); + + systemPrintf("Module unique chip ID: %s\r\n", zedUniqueId); + + printCurrentConditions(); + } + else + systemPrintln("Offline"); + + systemPrint("Display: "); + if (online.display == true) + systemPrintln("Online"); + else + systemPrintln("Offline"); + + if (online.accelerometer == true) + systemPrintln("Accelerometer: Online"); + + systemPrint("Fuel Gauge: "); + if (online.battery == true) + { + systemPrint("Online - "); + + battLevel = lipo.getSOC(); + battVoltage = lipo.getVoltage(); + + systemPrintf("Batt (%d%%) / Voltage: %0.02fV", battLevel, battVoltage); + systemPrintln(); + } + else + systemPrintln("Offline"); + + systemPrint("microSD: "); + if (online.microSD == true) + systemPrintln("Online"); + else + systemPrintln("Offline"); + + if (online.lband == true) + { + systemPrint("L-Band: Online - "); + + if (online.lbandCorrections == true) + systemPrint("Keys Good"); + else + systemPrint("No Keys"); + + systemPrint(" / Corrections Received"); + if (lbandCorrectionsReceived == false) + systemPrint(" Failed"); + + systemPrintf(" / Eb/N0[dB] (>9 is good): %0.2f", lBandEBNO); + + systemPrint(" - "); + + printNEOInfo(); + } + + // Display the Bluetooth status + bluetoothTest(false); + +#ifdef COMPILE_WIFI + systemPrint("WiFi MAC Address: "); + systemPrintf("%02X:%02X:%02X:%02X:%02X:%02X\r\n", wifiMACAddress[0], wifiMACAddress[1], wifiMACAddress[2], + wifiMACAddress[3], wifiMACAddress[4], wifiMACAddress[5]); + if (wifiState == WIFI_STATE_CONNECTED) + wifiDisplayIpAddress(); +#endif // COMPILE_WIFI + +#ifdef COMPILE_ETHERNET + if (HAS_ETHERNET) + { + systemPrint("Ethernet cable: "); + if (Ethernet.linkStatus() == LinkON) + systemPrintln("connected"); + else + systemPrintln("disconnected"); + systemPrint("Ethernet MAC Address: "); + systemPrintf("%02X:%02X:%02X:%02X:%02X:%02X\r\n", ethernetMACAddress[0], ethernetMACAddress[1], + ethernetMACAddress[2], ethernetMACAddress[3], ethernetMACAddress[4], ethernetMACAddress[5]); + systemPrint("Ethernet IP Address: "); + systemPrintln(Ethernet.localIP()); + if (!settings.ethernetDHCP) + { + systemPrint("Ethernet DNS: "); + systemPrintf("%s\r\n", settings.ethernetDNS.toString()); + systemPrint("Ethernet Gateway: "); + systemPrintf("%s\r\n", settings.ethernetGateway.toString()); + systemPrint("Ethernet Subnet Mask: "); + systemPrintf("%s\r\n", settings.ethernetSubnet.toString()); + } + } +#endif // COMPILE_ETHERNET + + // Display the uptime + uint64_t uptimeMilliseconds = millis(); + uint32_t uptimeDays = 0; + byte uptimeHours = 0; + byte uptimeMinutes = 0; + byte uptimeSeconds = 0; + + uptimeDays = uptimeMilliseconds / MILLISECONDS_IN_A_DAY; + uptimeMilliseconds %= MILLISECONDS_IN_A_DAY; + + uptimeHours = uptimeMilliseconds / MILLISECONDS_IN_AN_HOUR; + uptimeMilliseconds %= MILLISECONDS_IN_AN_HOUR; + + uptimeMinutes = uptimeMilliseconds / MILLISECONDS_IN_A_MINUTE; + uptimeMilliseconds %= MILLISECONDS_IN_A_MINUTE; + + uptimeSeconds = uptimeMilliseconds / MILLISECONDS_IN_A_SECOND; + uptimeMilliseconds %= MILLISECONDS_IN_A_SECOND; + + systemPrint("System Uptime: "); + systemPrintf("%d %02d:%02d:%02d.%03lld (Resets: %d)\r\n", uptimeDays, uptimeHours, uptimeMinutes, uptimeSeconds, + uptimeMilliseconds, settings.resetCount); + + // Display NTRIP Client status and uptime + ntripClientPrintStatus(); + + // Display NTRIP Server status and uptime + for (int serverIndex = 0; serverIndex < NTRIP_SERVER_MAX; serverIndex++) + ntripServerPrintStatus(serverIndex); + + systemPrintf("Filtered by parser: %d NMEA / %d RTCM / %d UBX\r\n", failedParserMessages_NMEA, + failedParserMessages_RTCM, failedParserMessages_UBX); + + // Separate the menu from the status + systemPrintln("----- Mode Switch -----"); + + // Support mode switching + systemPrintln("B) Switch to Base mode"); + if (HAS_ETHERNET) + systemPrintln("N) Switch to NTP Server mode"); + systemPrintln("R) Switch to Rover mode"); + systemPrintln("W) Switch to WiFi Config mode"); + + systemPrintln("----- Settings -----"); + + systemPrint("b) Set Bluetooth Mode: "); + if (settings.bluetoothRadioType == BLUETOOTH_RADIO_SPP) + systemPrintln("Classic"); + else if (settings.bluetoothRadioType == BLUETOOTH_RADIO_BLE) + systemPrintln("BLE"); + else + systemPrintln("Off"); + + if (settings.shutdownNoChargeTimeout_s == 0) + systemPrintln("c) Shutdown if not charging: Disabled"); + else + systemPrintf("c) Shutdown if not charging after: %d seconds\r\n", settings.shutdownNoChargeTimeout_s); + + systemPrintln("d) Debug software"); + + systemPrint("e) Echo User Input: "); + if (settings.echoUserInput == true) + systemPrintln("On"); + else + systemPrintln("Off"); + + if (settings.enableSD == true && online.microSD == true) + { + systemPrintln("f) Display microSD Files"); + } + + systemPrintln("h) Debug hardware"); + + systemPrintln("n) Debug network"); + + systemPrintln("o) Configure RTK operation"); + + systemPrintln("p) Configure periodic print messages"); + + systemPrintln("r) Reset all settings to default"); + + systemPrintf("z) Set time zone offset: %02d:%02d:%02d\r\n", settings.timeZoneHours, settings.timeZoneMinutes, + settings.timeZoneSeconds); + + systemPrint("~) Setup button: "); + if (settings.disableSetupButton == true) + systemPrintln("Disabled"); + else + systemPrintln("Enabled"); + + systemPrintln("S) Shut down"); + + systemPrintln("x) Exit"); + + byte incoming = getCharacterNumber(); + + if (incoming == 'b') + { + // Restart Bluetooth + bluetoothStop(); + if (settings.bluetoothRadioType == BLUETOOTH_RADIO_SPP) + settings.bluetoothRadioType = BLUETOOTH_RADIO_BLE; + else if (settings.bluetoothRadioType == BLUETOOTH_RADIO_BLE) + settings.bluetoothRadioType = BLUETOOTH_RADIO_OFF; + else if (settings.bluetoothRadioType == BLUETOOTH_RADIO_OFF) + settings.bluetoothRadioType = BLUETOOTH_RADIO_SPP; + bluetoothStart(); + } + else if (incoming == 'c') + { + systemPrint("Enter time in seconds to shutdown unit if not charging (0 to disable): "); + int shutdownNoChargeTimeout_s = getNumber(); // Returns EXIT, TIMEOUT, or long + if ((shutdownNoChargeTimeout_s != INPUT_RESPONSE_GETNUMBER_EXIT) && + (shutdownNoChargeTimeout_s != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + { + if (shutdownNoChargeTimeout_s < 0 || + shutdownNoChargeTimeout_s > 60 * 60 * 24 * 7) // Arbitrary 7 day limit + systemPrintln("Error: Time out of range"); + else + settings.shutdownNoChargeTimeout_s = + shutdownNoChargeTimeout_s; // Recorded to NVM and file at main menu exit + } + } + else if (incoming == 'd') + menuDebugSoftware(); + else if (incoming == 'e') + { + settings.echoUserInput ^= 1; + } + else if ((incoming == 'f') && (settings.enableSD == true) && (online.microSD == true)) + { + printFileList(); + } + else if (incoming == 'h') + menuDebugHardware(); + else if (incoming == 'n') + menuDebugNetwork(); + else if (incoming == 'o') + menuOperation(); + else if (incoming == 'p') + menuPeriodicPrint(); + else if (incoming == 'r') + { + systemPrintln("\r\nResetting to factory defaults. Press 'y' to confirm:"); + byte bContinue = getCharacterNumber(); + if (bContinue == 'y') + { + factoryReset(false); // We do not have the SD semaphore + } + else + systemPrintln("Reset aborted"); + } + else if (incoming == 'z') + { + systemPrint("Enter time zone hour offset (-23 <= offset <= 23): "); + int value = getNumber(); // Returns EXIT, TIMEOUT, or long + if ((value != INPUT_RESPONSE_GETNUMBER_EXIT) && (value != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + { + if (value < -23 || value > 23) + systemPrintln("Error: -24 < hours < 24"); + else + { + settings.timeZoneHours = value; + + systemPrint("Enter time zone minute offset (-59 <= offset <= 59): "); + int value = getNumber(); // Returns EXIT, TIMEOUT, or long + if ((value != INPUT_RESPONSE_GETNUMBER_EXIT) && (value != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + { + if (value < -59 || value > 59) + systemPrintln("Error: -60 < minutes < 60"); + else + { + settings.timeZoneMinutes = value; + + systemPrint("Enter time zone second offset (-59 <= offset <= 59): "); + int value = getNumber(); // Returns EXIT, TIMEOUT, or long + if ((value != INPUT_RESPONSE_GETNUMBER_EXIT) && (value != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + { + if (value < -59 || value > 59) + systemPrintln("Error: -60 < seconds < 60"); + else + { + settings.timeZoneSeconds = value; + online.rtc = false; + syncRTCInterval = + 1000; // Reset syncRTCInterval to 1000ms (tpISR could have set it to 59000) + rtcSyncd = false; + updateRTC(); + } // Succesful seconds + } + } // Succesful minute + } + } // Succesful hours + } + } + else if (incoming == '~') + { + settings.disableSetupButton ^= 1; + } + + // Support mode switching + else if (incoming == 'B') + { + forceSystemStateUpdate = true; // Imediately go to this new state + changeState(STATE_BASE_NOT_STARTED); + } + else if ((incoming == 'N') && HAS_ETHERNET) + { + forceSystemStateUpdate = true; // Imediately go to this new state + changeState(STATE_NTPSERVER_NOT_STARTED); + } + else if (incoming == 'R') + { + forceSystemStateUpdate = true; // Imediately go to this new state + changeState(STATE_ROVER_NOT_STARTED); + } + else if (incoming == 'W') + { + forceSystemStateUpdate = true; // Imediately go to this new state + changeState(STATE_WIFI_CONFIG_NOT_STARTED); + } + + // Menu exit control + else if (incoming == 'S') + { + systemPrintln("Shutting down..."); + forceDisplayUpdate = true; + powerDown(true); + } + else if (incoming == 'x') + break; + else if (incoming == INPUT_RESPONSE_GETCHARACTERNUMBER_EMPTY) + break; + else if (incoming == INPUT_RESPONSE_GETCHARACTERNUMBER_TIMEOUT) + break; + else + printUnknown(incoming); + } + + clearBuffer(); // Empty buffer of any newline chars +} + +// Toggle debug settings for hardware +void menuDebugHardware() +{ + while (1) + { + systemPrintln(); + systemPrintln("Menu: Debug Hardware"); + + // Battery + systemPrint("1) Print battery status messages: "); + systemPrintf("%s\r\n", settings.enablePrintBatteryMessages ? "Enabled" : "Disabled"); + + // Bluetooth + systemPrintln("2) Run Bluetooth Test"); + + // RTC + systemPrint("3) Print RTC resyncs: "); + systemPrintf("%s\r\n", settings.enablePrintRtcSync ? "Enabled" : "Disabled"); + + // SD card + systemPrint("4) Print log file messages: "); + systemPrintf("%s\r\n", settings.enablePrintLogFileMessages ? "Enabled" : "Disabled"); + + systemPrint("5) Print log file status: "); + systemPrintf("%s\r\n", settings.enablePrintLogFileStatus ? "Enabled" : "Disabled"); + + systemPrint("6) Run Logging Test: "); + systemPrintf("%s\r\n", settings.runLogTest ? "Enabled" : "Disabled"); + + systemPrint("7) Print SD and UART buffer sizes: "); + systemPrintf("%s\r\n", settings.enablePrintSDBuffers ? "Enabled" : "Disabled"); + + // Ublox + systemPrint("8) Print messages with bad checksums or CRCs: "); + systemPrintf("%s\r\n", settings.enablePrintBadMessages ? "Enabled" : "Disabled"); + + systemPrint("9) u-blox I2C Debugging Output: "); + systemPrintf("%s\r\n", settings.enableI2Cdebug ? "Enabled" : "Disabled"); + + systemPrint("10) L-Band Debugging Output: "); + systemPrintf("%s\r\n", settings.debugLBand ? "Enabled" : "Disabled"); + + systemPrintln("e) Erase LittleFS"); + + systemPrintln("t) Test Screen"); + + systemPrintln("r) Force system reset"); + + systemPrintln("x) Exit"); + + byte incoming = getCharacterNumber(); + + if (incoming == 1) + settings.enablePrintBatteryMessages ^= 1; + else if (incoming == 2) + bluetoothTest(true); + else if (incoming == 3) + settings.enablePrintRtcSync ^= 1; + else if (incoming == 4) + settings.enablePrintLogFileMessages ^= 1; + else if (incoming == 5) + settings.enablePrintLogFileStatus ^= 1; + else if (incoming == 6) + { + settings.runLogTest ^= 1; + + logTestState = LOGTEST_START; // Start test + + // Mark current log file as complete to force test start + startCurrentLogTime_minutes = systemTime_minutes - settings.maxLogLength_minutes; + } + else if (incoming == 7) + settings.enablePrintSDBuffers ^= 1; + else if (incoming == 8) + settings.enablePrintBadMessages ^= 1; + else if (incoming == 9) + { + settings.enableI2Cdebug ^= 1; + + if (settings.enableI2Cdebug) + { +#if defined(REF_STN_GNSS_DEBUG) + if (ENABLE_DEVELOPER && productVariant == REFERENCE_STATION) + theGNSS.enableDebugging(serialGNSS); // Output all debug messages over serialGNSS + else +#endif // REF_STN_GNSS_DEBUG + theGNSS.enableDebugging(Serial, true); // Enable only the critical debug messages over Serial + } + else + theGNSS.disableDebugging(); + } + else if (incoming == 10) + { + settings.debugLBand ^= 1; + } + + else if (incoming == 'e') + { + systemPrintln("Erasing LittleFS and resetting"); + LittleFS.format(); + ESP.restart(); + } + else if (incoming == 't') + { + requestChangeState(STATE_TEST); // We'll enter test mode once exiting all serial menus + } + + // Menu exit control + else if (incoming == 'r') + { + recordSystemSettings(); + + ESP.restart(); + } + else if (incoming == 'x') + break; + else if (incoming == INPUT_RESPONSE_GETCHARACTERNUMBER_EMPTY) + break; + else if (incoming == INPUT_RESPONSE_GETCHARACTERNUMBER_TIMEOUT) + break; + else + printUnknown(incoming); + } + + clearBuffer(); // Empty buffer of any newline chars +} + +// Toggle debug settings for the network +void menuDebugNetwork() +{ + while (1) + { + systemPrintln(); + systemPrintln("Menu: Debug Network"); + + // Ethernet + systemPrint("1) Print Ethernet diagnostics: "); + systemPrintf("%s\r\n", settings.enablePrintEthernetDiag ? "Enabled" : "Disabled"); + + // ESP-Now + systemPrint("2) ESP-Now Broadcast Override: "); + systemPrintf("%s\r\n", settings.espnowBroadcast ? "Enabled" : "Disabled"); + + // WiFi + systemPrint("3) Debug WiFi state: "); + systemPrintf("%s\r\n", settings.debugWifiState ? "Enabled" : "Disabled"); + + // Network + systemPrint("10) Debug network layer: "); + systemPrintf("%s\r\n", settings.debugNetworkLayer ? "Enabled" : "Disabled"); + + systemPrint("11) Print network layer status: "); + systemPrintf("%s\r\n", settings.printNetworkStatus ? "Enabled" : "Disabled"); + + // NTP + systemPrint("20) Debug NTP: "); + systemPrintf("%s\r\n", settings.debugNtp ? "Enabled" : "Disabled"); + + // NTRIP Client + systemPrint("21) Debug NTRIP client state: "); + systemPrintf("%s\r\n", settings.debugNtripClientState ? "Enabled" : "Disabled"); + + systemPrint("22) Debug NTRIP client --> caster GGA messages: "); + systemPrintf("%s\r\n", settings.debugNtripClientRtcm ? "Enabled" : "Disabled"); + + // NTRIP Server + systemPrint("23) Debug NTRIP server state: "); + systemPrintf("%s\r\n", settings.debugNtripServerState ? "Enabled" : "Disabled"); + + systemPrint("24) Debug caster --> NTRIP server GNSS messages: "); + systemPrintf("%s\r\n", settings.debugNtripServerRtcm ? "Enabled" : "Disabled"); + + // PVT Client + systemPrint("25) Debug PVT client: "); + systemPrintf("%s\r\n", settings.debugPvtClient ? "Enabled" : "Disabled"); + + // PVT Server + systemPrint("26) Debug PVT server: "); + systemPrintf("%s\r\n", settings.debugPvtServer ? "Enabled" : "Disabled"); + + // PVT Server + systemPrint("27) Debug PVT UDP server: "); + systemPrintf("%s\r\n", settings.debugPvtUdpServer ? "Enabled" : "Disabled"); + + // WiFi Config + systemPrint("28) Debug WiFi Config: "); + systemPrintf("%s\r\n", settings.debugWiFiConfig ? "Enabled" : "Disabled"); + + systemPrintln("r) Force system reset"); + + systemPrintln("x) Exit"); + + byte incoming = getCharacterNumber(); + + if (incoming == 1) + settings.enablePrintEthernetDiag ^= 1; + else if (incoming == 2) + settings.espnowBroadcast ^= 1; + else if (incoming == 3) + settings.debugWifiState ^= 1; + else if (incoming == 10) + settings.debugNetworkLayer ^= 1; + else if (incoming == 11) + settings.printNetworkStatus ^= 1; + else if (incoming == 20) + settings.debugNtp ^= 1; + else if (incoming == 21) + settings.debugNtripClientState ^= 1; + else if (incoming == 22) + settings.debugNtripClientRtcm ^= 1; + else if (incoming == 23) + settings.debugNtripServerState ^= 1; + else if (incoming == 24) + settings.debugNtripServerRtcm ^= 1; + else if (incoming == 25) + settings.debugPvtClient ^= 1; + else if (incoming == 26) + settings.debugPvtServer ^= 1; + else if (incoming == 27) + settings.debugPvtUdpServer ^= 1; + else if (incoming == 28) + settings.debugWiFiConfig ^= 1; + + // Menu exit control + else if (incoming == 'r') + { + recordSystemSettings(); + + ESP.restart(); + } + else if (incoming == 'x') + break; + else if (incoming == INPUT_RESPONSE_GETCHARACTERNUMBER_EMPTY) + break; + else if (incoming == INPUT_RESPONSE_GETCHARACTERNUMBER_TIMEOUT) + break; + else + printUnknown(incoming); + } + + clearBuffer(); // Empty buffer of any newline chars +} + +// Toggle debug settings for software +void menuDebugSoftware() +{ + while (1) + { + systemPrintln(); + systemPrintln("Menu: Debug Software"); + + // Heap + systemPrint("1) Heap Reporting: "); + systemPrintf("%s\r\n", settings.enableHeapReport ? "Enabled" : "Disabled"); + + // Ring buffer - ZED Tx + systemPrint("10) Print ring buffer offsets: "); + systemPrintf("%s\r\n", settings.enablePrintRingBufferOffsets ? "Enabled" : "Disabled"); + + systemPrint("11) Print ring buffer overruns: "); + systemPrintf("%s\r\n", settings.enablePrintBufferOverrun ? "Enabled" : "Disabled"); + + systemPrint("12) RTCM message checking: "); + systemPrintf("%s\r\n", settings.enableRtcmMessageChecking ? "Enabled" : "Disabled"); + + // Rover + systemPrint("20) Print Rover accuracy messages: "); + systemPrintf("%s\r\n", settings.enablePrintRoverAccuracy ? "Enabled" : "Disabled"); + + // RTK + systemPrint("30) Print states: "); + systemPrintf("%s\r\n", settings.enablePrintStates ? "Enabled" : "Disabled"); + + systemPrint("31) Print duplicate states: "); + systemPrintf("%s\r\n", settings.enablePrintDuplicateStates ? "Enabled" : "Disabled"); + + systemPrint("32) Reboot RTK after uptime reaches: "); + if (settings.rebootSeconds > 4294967) + systemPrintln("Disabled"); + else + { + int days; + int hours; + int minutes; + int seconds; + + seconds = settings.rebootSeconds; + days = seconds / SECONDS_IN_A_DAY; + seconds -= days * SECONDS_IN_A_DAY; + hours = seconds / SECONDS_IN_AN_HOUR; + seconds -= hours * SECONDS_IN_AN_HOUR; + minutes = seconds / SECONDS_IN_A_MINUTE; + seconds -= minutes * SECONDS_IN_A_MINUTE; + + systemPrintf("%d (%d days %d:%02d:%02d)\r\n", settings.rebootSeconds, days, hours, minutes, seconds); + } + + systemPrintf("34) Print partition table\r\n"); + + // Tasks + systemPrint("50) Task Highwater Reporting: "); + if (settings.enableTaskReports == true) + systemPrintln("Enabled"); + else + systemPrintln("Disabled"); + + // Automatic Firmware Update + systemPrintf("60) Print firmware update states: %s\r\n", settings.debugFirmwareUpdate ? "Enabled" : "Disabled"); + + // Point Perfect + systemPrintf("70) Point Perfect certificate management: %s\r\n", + settings.debugPpCertificate ? "Enabled" : "Disabled"); + + systemPrintln("e) Erase LittleFS"); + + systemPrintln("r) Force system reset"); + + systemPrintln("x) Exit"); + + byte incoming = getCharacterNumber(); + + if (incoming == 1) + settings.enableHeapReport ^= 1; + else if (incoming == 10) + settings.enablePrintRingBufferOffsets ^= 1; + else if (incoming == 11) + settings.enablePrintBufferOverrun ^= 1; + else if (incoming == 12) + settings.enableRtcmMessageChecking ^= 1; + else if (incoming == 20) + settings.enablePrintRoverAccuracy ^= 1; + else if (incoming == 30) + settings.enablePrintStates ^= 1; + else if (incoming == 31) + settings.enablePrintDuplicateStates ^= 1; + else if (incoming == 32) + { + systemPrint("Enter uptime seconds before reboot, Disabled = 0, Reboot range (30 - 4294967): "); + int rebootSeconds = getNumber(); // Returns EXIT, TIMEOUT, or long + if ((rebootSeconds != INPUT_RESPONSE_GETNUMBER_EXIT) && (rebootSeconds != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + { + if (rebootSeconds < 30 || rebootSeconds > 4294967) // Disable the reboot + { + settings.rebootSeconds = (uint32_t)-1; + systemPrintln("Reset is disabled"); + } + else + { + int days; + int hours; + int minutes; + int seconds; + + // Set the reboot time + settings.rebootSeconds = rebootSeconds; + + seconds = settings.rebootSeconds; + days = seconds / SECONDS_IN_A_DAY; + seconds -= days * SECONDS_IN_A_DAY; + hours = seconds / SECONDS_IN_AN_HOUR; + seconds -= hours * SECONDS_IN_AN_HOUR; + minutes = seconds / SECONDS_IN_A_MINUTE; + seconds -= minutes * SECONDS_IN_A_MINUTE; + + systemPrintf("Reboot after uptime reaches %d days %d:%02d:%02d\r\n", days, hours, minutes, seconds); + } + } + } + else if (incoming == 34) + printPartitionTable(); + else if (incoming == 50) + settings.enableTaskReports ^= 1; + else if (incoming == 60) + settings.debugFirmwareUpdate ^= 1; + else if (incoming == 70) + settings.debugPpCertificate ^= 1; + else if (incoming == 'e') + { + systemPrintln("Erasing LittleFS and resetting"); + LittleFS.format(); + ESP.restart(); + } + + // Menu exit control + else if (incoming == 'r') + { + recordSystemSettings(); + + ESP.restart(); + } + else if (incoming == 'x') + break; + else if (incoming == INPUT_RESPONSE_GETCHARACTERNUMBER_EMPTY) + break; + else if (incoming == INPUT_RESPONSE_GETCHARACTERNUMBER_TIMEOUT) + break; + else + printUnknown(incoming); + } + + clearBuffer(); // Empty buffer of any newline chars +} + +// Configure the RTK operation +void menuOperation() +{ + while (1) + { + systemPrintln(); + systemPrintln("Menu: RTK Operation"); + + // Display + systemPrintf("1) Display Reset Counter: %d - ", settings.resetCount); + if (settings.enableResetDisplay == true) + systemPrintln("Enabled"); + else + systemPrintln("Disabled"); + + // GNSS + systemPrint("2) GNSS Serial Timeout: "); + systemPrintln(settings.serialTimeoutGNSS); + + systemPrint("3) GNSS Handler Buffer Size: "); + systemPrintln(settings.gnssHandlerBufferSize); + + systemPrint("4) GNSS Serial RX Full Threshold: "); + systemPrintln(settings.serialGNSSRxFullThreshold); + + // L-Band + systemPrint("5) Set L-Band RTK Fix Timeout (seconds): "); + if (settings.lbandFixTimeout_seconds > 0) + systemPrintln(settings.lbandFixTimeout_seconds); + else + systemPrintln("Disabled - no resets"); + + // SPI + systemPrint("6) SPI/SD Interface Frequency: "); + systemPrint(settings.spiFrequency); + systemPrintln(" MHz"); + + // SPP + systemPrint("7) SPP RX Buffer Size: "); + systemPrintln(settings.sppRxQueueSize); + + systemPrint("8) SPP TX Buffer Size: "); + systemPrintln(settings.sppTxQueueSize); + + // UART + systemPrint("9) UART Receive Buffer Size: "); + systemPrintln(settings.uartReceiveBufferSize); + + // ZED + systemPrintln("10) Mirror ZED-F9x's UART1 settings to USB"); + + systemPrint("11) Use I2C for L-Band Corrections: "); + systemPrintf("%s\r\n", settings.useI2cForLbandCorrections ? "Enabled" : "Disabled"); + + systemPrintf("12) RTCM timeout before L-Band override (seconds): %d\r\n", + settings.rtcmTimeoutBeforeUsingLBand_s); + + systemPrint("13) CONFIG UBLOX USB port: "); + systemPrintf("%s\r\n", settings.enableZedUsb ? "Enabled" : "Disabled"); + + systemPrintln("---- Interrupts ----"); + systemPrint("30) Bluetooth Interrupts Core: "); + systemPrintln(settings.bluetoothInterruptsCore); + + systemPrint("31) GNSS UART Interrupts Core: "); + systemPrintln(settings.gnssUartInterruptsCore); + + systemPrint("32) I2C Interrupts Core: "); + systemPrintln(settings.i2cInterruptsCore); + + // Tasks + systemPrintln("------- Tasks ------"); + systemPrint("50) BT Read Task Core: "); + systemPrintln(settings.btReadTaskCore); + systemPrint("51) BT Read Task Priority: "); + systemPrintln(settings.btReadTaskPriority); + + systemPrint("52) GNSS Data Handler Core: "); + systemPrintln(settings.handleGnssDataTaskCore); + systemPrint("53) GNSS Data Handler Task Priority: "); + systemPrintln(settings.handleGnssDataTaskPriority); + + systemPrint("54) GNSS Read Task Core: "); + systemPrintln(settings.gnssReadTaskCore); + systemPrint("55) GNSS Read Task Priority: "); + systemPrintln(settings.gnssReadTaskPriority); + + systemPrintln("x) Exit"); + + byte incoming = getCharacterNumber(); + + if (incoming == 1) + { + settings.enableResetDisplay ^= 1; + if (settings.enableResetDisplay == true) + { + settings.resetCount = 0; + recordSystemSettings(); // Record to NVM + } + } + else if (incoming == 2) + { + systemPrint("Enter GNSS Serial Timeout in milliseconds (0 to 1000): "); + int serialTimeoutGNSS = getNumber(); // Returns EXIT, TIMEOUT, or long + if ((serialTimeoutGNSS != INPUT_RESPONSE_GETNUMBER_EXIT) && + (serialTimeoutGNSS != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + { + if (serialTimeoutGNSS < 0 || serialTimeoutGNSS > 1000) // Arbitrary 1s limit + systemPrintln("Error: Timeout is out of range"); + else + settings.serialTimeoutGNSS = serialTimeoutGNSS; // Recorded to NVM and file at main menu exit + } + } + else if (incoming == 3) + { + systemPrintln("Warning: changing the Handler Buffer Size will restart the RTK. Enter 0 to abort"); + systemPrint("Enter GNSS Handler Buffer Size in Bytes (32 to 65535): "); + int queSize = getNumber(); // Returns EXIT, TIMEOUT, or long + if ((queSize != INPUT_RESPONSE_GETNUMBER_EXIT) && (queSize != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + { + if (queSize < 32 || queSize > 65535) // Arbitrary 64k limit + systemPrintln("Error: Queue size out of range"); + else + { + // Stop the UART2 tssks to prevent the system from crashing + tasksStopUART2(); + + // Update the buffer size + settings.gnssHandlerBufferSize = queSize; // Recorded to NVM and file + recordSystemSettings(); + + // Reboot the system + ESP.restart(); + } + } + } + else if (incoming == 4) + { + systemPrint("Enter Serial GNSS RX Full Threshold (1 to 127): "); + int serialGNSSRxFullThreshold = getNumber(); // Returns EXIT, TIMEOUT, or long + if ((serialGNSSRxFullThreshold != INPUT_RESPONSE_GETNUMBER_EXIT) && + (serialGNSSRxFullThreshold != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + { + if (serialGNSSRxFullThreshold < 1 || serialGNSSRxFullThreshold > 127) + systemPrintln("Error: Core out of range"); + else + { + settings.serialGNSSRxFullThreshold = serialGNSSRxFullThreshold; // Recorded to NVM and file + } + } + } + else if (incoming == 5) + { + systemPrint("Enter number of seconds in RTK float before hot-start (0-disable to 3600): "); + int timeout = getNumber(); // Returns EXIT, TIMEOUT, or long + if ((timeout != INPUT_RESPONSE_GETNUMBER_EXIT) && (timeout != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + { + if (timeout < 0 || timeout > 3600) // Arbitrary 60 minute limit + systemPrintln("Error: Timeout out of range"); + else + settings.lbandFixTimeout_seconds = timeout; // Recorded to NVM and file at main menu exit + } + } + else if (incoming == 6) + { + systemPrint("Enter SPI frequency in MHz (1 to 16): "); + int freq = getNumber(); // Returns EXIT, TIMEOUT, or long + if ((freq != INPUT_RESPONSE_GETNUMBER_EXIT) && (freq != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + { + if (freq < 1 || freq > 16) // Arbitrary 16MHz limit + systemPrintln("Error: SPI frequency out of range"); + else + settings.spiFrequency = freq; // Recorded to NVM and file at main menu exit + } + } + else if (incoming == 7) + { + systemPrint("Enter SPP RX Queue Size in Bytes (32 to 16384): "); + int queSize = getNumber(); // Returns EXIT, TIMEOUT, or long + if ((queSize != INPUT_RESPONSE_GETNUMBER_EXIT) && (queSize != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + { + if (queSize < 32 || queSize > 16384) // Arbitrary 16k limit + systemPrintln("Error: Queue size out of range"); + else + settings.sppRxQueueSize = queSize; // Recorded to NVM and file at main menu exit + } + } + else if (incoming == 8) + { + systemPrint("Enter SPP TX Queue Size in Bytes (32 to 16384): "); + int queSize = getNumber(); // Returns EXIT, TIMEOUT, or long + if ((queSize != INPUT_RESPONSE_GETNUMBER_EXIT) && (queSize != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + { + if (queSize < 32 || queSize > 16384) // Arbitrary 16k limit + systemPrintln("Error: Queue size out of range"); + else + settings.sppTxQueueSize = queSize; // Recorded to NVM and file at main menu exit + } + } + else if (incoming == 9) + { + systemPrintln("Warning: changing the Receive Buffer Size will restart the RTK. Enter 0 to abort"); + systemPrint("Enter UART Receive Buffer Size in Bytes (32 to 16384): "); + int queSize = getNumber(); // Returns EXIT, TIMEOUT, or long + if ((queSize != INPUT_RESPONSE_GETNUMBER_EXIT) && (queSize != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + { + if (queSize < 32 || queSize > 16384) // Arbitrary 16k limit + systemPrintln("Error: Queue size out of range"); + else + { + settings.uartReceiveBufferSize = queSize; // Recorded to NVM and file + recordSystemSettings(); + ESP.restart(); + } + } + } + else if (incoming == 10) + { + bool response = setMessagesUSB(MAX_SET_MESSAGES_RETRIES); + + if (response == false) + systemPrintln(F("Failed to enable USB messages")); + else + systemPrintln(F("USB messages successfully enabled")); + } + else if (incoming == 11) + { + settings.useI2cForLbandCorrectionsConfigured = + true; // Record that the user has manually modified the settings. + settings.useI2cForLbandCorrections ^= 1; + } + else if (incoming == 12) + { + systemPrint("Enter the number of seconds before L-Band is used once RTCM is absent (1 to 255): "); + int rtcmTimeoutBeforeUsingLBand_s = getNumber(); // Returns EXIT, TIMEOUT, or long + if ((rtcmTimeoutBeforeUsingLBand_s != INPUT_RESPONSE_GETNUMBER_EXIT) && + (rtcmTimeoutBeforeUsingLBand_s != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + { + if (rtcmTimeoutBeforeUsingLBand_s < 1 || rtcmTimeoutBeforeUsingLBand_s > 255) + systemPrintln("Error: RTCM timeout out of range"); + else + settings.rtcmTimeoutBeforeUsingLBand_s = rtcmTimeoutBeforeUsingLBand_s; // Recorded to NVM and file + } + } + else if (incoming == 13) + { + settings.enableZedUsb ^= 1; + + bool response = true; + + response &= theGNSS.newCfgValset(); + + if (settings.enableZedUsb == true) + { + // The USB port on the ZED may be used for RTCM to/from the computer (as an NTRIP caster or client) + // So let's be sure all protocols are on for the USB port + response &= theGNSS.addCfgValset(UBLOX_CFG_USBOUTPROT_UBX, 1); + response &= theGNSS.addCfgValset(UBLOX_CFG_USBOUTPROT_NMEA, 1); + if (commandSupported(UBLOX_CFG_USBOUTPROT_RTCM3X) == true) + response &= theGNSS.addCfgValset(UBLOX_CFG_USBOUTPROT_RTCM3X, 1); + response &= theGNSS.addCfgValset(UBLOX_CFG_USBINPROT_UBX, 1); + response &= theGNSS.addCfgValset(UBLOX_CFG_USBINPROT_NMEA, 1); + response &= theGNSS.addCfgValset(UBLOX_CFG_USBINPROT_RTCM3X, 1); + if (commandSupported(UBLOX_CFG_USBINPROT_SPARTN) == true) + { + // See issue: https://github.com/sparkfun/SparkFun_RTK_Firmware/issues/713 + response &= theGNSS.addCfgValset(UBLOX_CFG_USBINPROT_SPARTN, 1); + } + } + else + { + // Disable all protocols over USB + response &= theGNSS.addCfgValset(UBLOX_CFG_USBOUTPROT_UBX, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_USBOUTPROT_NMEA, 0); + if (commandSupported(UBLOX_CFG_USBOUTPROT_RTCM3X) == true) + response &= theGNSS.addCfgValset(UBLOX_CFG_USBOUTPROT_RTCM3X, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_USBINPROT_UBX, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_USBINPROT_NMEA, 0); + response &= theGNSS.addCfgValset(UBLOX_CFG_USBINPROT_RTCM3X, 0); + if (commandSupported(UBLOX_CFG_USBINPROT_SPARTN) == true) + { + // See issue: https://github.com/sparkfun/SparkFun_RTK_Firmware/issues/713 + response &= theGNSS.addCfgValset(UBLOX_CFG_USBINPROT_SPARTN, 0); + } + } + response &= theGNSS.sendCfgValset(); + + if (response == false) + systemPrintln("Failed to set UART2 settings"); + } + + else if (incoming == 30) + { + systemPrint("Not yet implemented! - Enter Core used for Bluetooth Interrupts (0 or 1): "); + int bluetoothInterruptsCore = getNumber(); // Returns EXIT, TIMEOUT, or long + if ((bluetoothInterruptsCore != INPUT_RESPONSE_GETNUMBER_EXIT) && + (bluetoothInterruptsCore != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + { + if (bluetoothInterruptsCore < 0 || bluetoothInterruptsCore > 1) + systemPrintln("Error: Core out of range"); + else + { + settings.bluetoothInterruptsCore = bluetoothInterruptsCore; // Recorded to NVM and file + } + } + } + else if (incoming == 31) + { + systemPrint("Enter Core used for GNSS UART Interrupts (0 or 1): "); + int gnssUartInterruptsCore = getNumber(); // Returns EXIT, TIMEOUT, or long + if ((gnssUartInterruptsCore != INPUT_RESPONSE_GETNUMBER_EXIT) && + (gnssUartInterruptsCore != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + { + if (gnssUartInterruptsCore < 0 || gnssUartInterruptsCore > 1) + systemPrintln("Error: Core out of range"); + else + { + settings.gnssUartInterruptsCore = gnssUartInterruptsCore; // Recorded to NVM and file + } + } + } + else if (incoming == 32) + { + systemPrint("Enter Core used for I2C Interrupts (0 or 1): "); + int i2cInterruptsCore = getNumber(); // Returns EXIT, TIMEOUT, or long + if ((i2cInterruptsCore != INPUT_RESPONSE_GETNUMBER_EXIT) && + (i2cInterruptsCore != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + { + if (i2cInterruptsCore < 0 || i2cInterruptsCore > 1) + systemPrintln("Error: Core out of range"); + else + { + settings.i2cInterruptsCore = i2cInterruptsCore; // Recorded to NVM and file + } + } + } + + else if (incoming == 50) + { + systemPrint("Enter BT Read Task Core (0 or 1): "); + int btReadTaskCore = getNumber(); // Returns EXIT, TIMEOUT, or long + if ((btReadTaskCore != INPUT_RESPONSE_GETNUMBER_EXIT) && + (btReadTaskCore != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + { + if (btReadTaskCore < 0 || btReadTaskCore > 1) + systemPrintln("Error: Core out of range"); + else + { + settings.btReadTaskCore = btReadTaskCore; // Recorded to NVM and file + } + } + } + else if (incoming == 51) + { + systemPrint("Enter BT Read Task Priority (0 to 3): "); + int btReadTaskPriority = getNumber(); // Returns EXIT, TIMEOUT, or long + if ((btReadTaskPriority != INPUT_RESPONSE_GETNUMBER_EXIT) && + (btReadTaskPriority != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + { + if (btReadTaskPriority < 0 || btReadTaskPriority > 3) + systemPrintln("Error: Task priority out of range"); + else + { + settings.btReadTaskPriority = btReadTaskPriority; // Recorded to NVM and file + } + } + } + else if (incoming == 52) + { + systemPrint("Enter GNSS Data Handler Task Core (0 or 1): "); + int handleGnssDataTaskCore = getNumber(); // Returns EXIT, TIMEOUT, or long + if ((handleGnssDataTaskCore != INPUT_RESPONSE_GETNUMBER_EXIT) && + (handleGnssDataTaskCore != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + { + if (handleGnssDataTaskCore < 0 || handleGnssDataTaskCore > 1) + systemPrintln("Error: Core out of range"); + else + { + settings.handleGnssDataTaskCore = handleGnssDataTaskCore; // Recorded to NVM and file + } + } + } + else if (incoming == 53) + { + systemPrint("Enter GNSS Data Handle Task Priority (0 to 3): "); + int handleGnssDataTaskPriority = getNumber(); // Returns EXIT, TIMEOUT, or long + if ((handleGnssDataTaskPriority != INPUT_RESPONSE_GETNUMBER_EXIT) && + (handleGnssDataTaskPriority != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + { + if (handleGnssDataTaskPriority < 0 || handleGnssDataTaskPriority > 3) + systemPrintln("Error: Task priority out of range"); + else + { + settings.handleGnssDataTaskPriority = handleGnssDataTaskPriority; // Recorded to NVM and file + } + } + } + else if (incoming == 54) + { + systemPrint("Enter GNSS Read Task Core (0 or 1): "); + int gnssReadTaskCore = getNumber(); // Returns EXIT, TIMEOUT, or long + if ((gnssReadTaskCore != INPUT_RESPONSE_GETNUMBER_EXIT) && + (gnssReadTaskCore != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + { + if (gnssReadTaskCore < 0 || gnssReadTaskCore > 1) + systemPrintln("Error: Core out of range"); + else + { + settings.gnssReadTaskCore = gnssReadTaskCore; // Recorded to NVM and file + } + } + } + else if (incoming == 55) + { + systemPrint("Enter GNSS Read Task Priority (0 to 3): "); + int gnssReadTaskPriority = getNumber(); // Returns EXIT, TIMEOUT, or long + if ((gnssReadTaskPriority != INPUT_RESPONSE_GETNUMBER_EXIT) && + (gnssReadTaskPriority != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + { + if (gnssReadTaskPriority < 0 || gnssReadTaskPriority > 3) + systemPrintln("Error: Task priority out of range"); + else + { + settings.gnssReadTaskPriority = gnssReadTaskPriority; // Recorded to NVM and file + } + } + } + + // Menu exit control + else if (incoming == 'x') + break; + else if (incoming == INPUT_RESPONSE_GETCHARACTERNUMBER_EMPTY) + break; + else if (incoming == INPUT_RESPONSE_GETCHARACTERNUMBER_TIMEOUT) + break; + else + printUnknown(incoming); + } + + clearBuffer(); // Empty buffer of any newline chars +} + +// Toggle periodic print message enables +void menuPeriodicPrint() +{ + while (1) + { + systemPrintln(); + systemPrintln("Menu: Periodic Print Messages"); + + systemPrintln("----- Hardware -----"); + systemPrint("1) Bluetooth RX: "); + systemPrintf("%s\r\n", PERIODIC_SETTING(PD_BLUETOOTH_DATA_RX) ? "Enabled" : "Disabled"); + + systemPrint("2) Bluetooth TX: "); + systemPrintf("%s\r\n", PERIODIC_SETTING(PD_BLUETOOTH_DATA_TX) ? "Enabled" : "Disabled"); + + systemPrint("3) Ethernet IP address: "); + systemPrintf("%s\r\n", PERIODIC_SETTING(PD_ETHERNET_IP_ADDRESS) ? "Enabled" : "Disabled"); + + systemPrint("4) Ethernet state: "); + systemPrintf("%s\r\n", PERIODIC_SETTING(PD_ETHERNET_STATE) ? "Enabled" : "Disabled"); + + systemPrint("5) SD log write data: "); + systemPrintf("%s\r\n", PERIODIC_SETTING(PD_SD_LOG_WRITE) ? "Enabled" : "Disabled"); + + systemPrint("6) WiFi IP Address: "); + systemPrintf("%s\r\n", PERIODIC_SETTING(PD_WIFI_IP_ADDRESS) ? "Enabled" : "Disabled"); + + systemPrint("7) WiFi state: "); + systemPrintf("%s\r\n", PERIODIC_SETTING(PD_WIFI_STATE) ? "Enabled" : "Disabled"); + + systemPrint("8) ZED RX data: "); + systemPrintf("%s\r\n", PERIODIC_SETTING(PD_ZED_DATA_RX) ? "Enabled" : "Disabled"); + + systemPrint("9) ZED TX data: "); + systemPrintf("%s\r\n", PERIODIC_SETTING(PD_ZED_DATA_TX) ? "Enabled" : "Disabled"); + + systemPrintln("----- Software -----"); + + systemPrintf("20) Periodic print: %d (0x%08x)\r\n", settings.periodicDisplay, settings.periodicDisplay); + + systemPrintf("21) Interval (seconds): %d\r\n", settings.periodicDisplayInterval / 1000); + + systemPrint("22) CPU idle time: "); + systemPrintf("%s\r\n", settings.enablePrintIdleTime ? "Enabled" : "Disabled"); + + systemPrint("23) Network state: "); + systemPrintf("%s\r\n", PERIODIC_SETTING(PD_NETWORK_STATE) ? "Enabled" : "Disabled"); + + systemPrint("24) Ring buffer consumer times: "); + systemPrintf("%s\r\n", PERIODIC_SETTING(PD_RING_BUFFER_MILLIS) ? "Enabled" : "Disabled"); + + systemPrint("25) RTK position: "); + systemPrintf("%s\r\n", settings.enablePrintPosition ? "Enabled" : "Disabled"); + + systemPrint("26) RTK state: "); + systemPrintf("%s\r\n", settings.enablePrintState ? "Enabled" : "Disabled"); + + systemPrintln("------ Clients -----"); + systemPrint("40) NTP server data: "); + systemPrintf("%s\r\n", PERIODIC_SETTING(PD_NTP_SERVER_DATA) ? "Enabled" : "Disabled"); + + systemPrint("41) NTP server state: "); + systemPrintf("%s\r\n", PERIODIC_SETTING(PD_NTP_SERVER_STATE) ? "Enabled" : "Disabled"); + + systemPrint("42) NTRIP client data: "); + systemPrintf("%s\r\n", PERIODIC_SETTING(PD_NTRIP_CLIENT_DATA) ? "Enabled" : "Disabled"); + + systemPrint("43) NTRIP client GGA writes: "); + systemPrintf("%s\r\n", PERIODIC_SETTING(PD_NTRIP_CLIENT_GGA) ? "Enabled" : "Disabled"); + + systemPrint("44) NTRIP client state: "); + systemPrintf("%s\r\n", PERIODIC_SETTING(PD_NTRIP_CLIENT_STATE) ? "Enabled" : "Disabled"); + + systemPrint("45) NTRIP server data: "); + systemPrintf("%s\r\n", PERIODIC_SETTING(PD_NTRIP_SERVER_DATA) ? "Enabled" : "Disabled"); + + systemPrint("46) NTRIP server state: "); + systemPrintf("%s\r\n", PERIODIC_SETTING(PD_NTRIP_SERVER_STATE) ? "Enabled" : "Disabled"); + + systemPrint("47) PVT client data: "); + systemPrintf("%s\r\n", PERIODIC_SETTING(PD_PVT_CLIENT_DATA) ? "Enabled" : "Disabled"); + + systemPrint("48) PVT client state: "); + systemPrintf("%s\r\n", PERIODIC_SETTING(PD_PVT_CLIENT_STATE) ? "Enabled" : "Disabled"); + + systemPrint("49) PVT server client data: "); + systemPrintf("%s\r\n", PERIODIC_SETTING(PD_PVT_SERVER_CLIENT_DATA) ? "Enabled" : "Disabled"); + + systemPrint("50) PVT server data: "); + systemPrintf("%s\r\n", PERIODIC_SETTING(PD_PVT_SERVER_DATA) ? "Enabled" : "Disabled"); + + systemPrint("51) PVT server state: "); + systemPrintf("%s\r\n", PERIODIC_SETTING(PD_PVT_SERVER_STATE) ? "Enabled" : "Disabled"); + + systemPrint("52) OTA client state: "); + systemPrintf("%s\r\n", PERIODIC_SETTING(PD_OTA_CLIENT_STATE) ? "Enabled" : "Disabled"); + + systemPrintln("------- Tasks ------"); + systemPrint("70) btReadTask state: "); + systemPrintf("%s\r\n", PERIODIC_SETTING(PD_TASK_BLUETOOTH_READ) ? "Enabled" : "Disabled"); + + systemPrint("71) ButtonCheckTask state: "); + systemPrintf("%s\r\n", PERIODIC_SETTING(PD_TASK_BUTTON_CHECK) ? "Enabled" : "Disabled"); + + systemPrint("72) gnssReadTask state: "); + systemPrintf("%s\r\n", PERIODIC_SETTING(PD_TASK_GNSS_READ) ? "Enabled" : "Disabled"); + + systemPrint("73) handleGnssDataTask state: "); + systemPrintf("%s\r\n", PERIODIC_SETTING(PD_TASK_HANDLE_GNSS_DATA) ? "Enabled" : "Disabled"); + + systemPrint("74) sdSizeCheckTask state: "); + systemPrintf("%s\r\n", PERIODIC_SETTING(PD_TASK_SD_SIZE_CHECK) ? "Enabled" : "Disabled"); + + systemPrintln("x) Exit"); + + byte incoming = getCharacterNumber(); + + if (incoming == 1) + PERIODIC_TOGGLE(PD_BLUETOOTH_DATA_RX); + else if (incoming == 2) + PERIODIC_TOGGLE(PD_BLUETOOTH_DATA_TX); + else if (incoming == 3) + PERIODIC_TOGGLE(PD_ETHERNET_IP_ADDRESS); + else if (incoming == 4) + PERIODIC_TOGGLE(PD_ETHERNET_STATE); + else if (incoming == 5) + PERIODIC_TOGGLE(PD_SD_LOG_WRITE); + else if (incoming == 6) + PERIODIC_TOGGLE(PD_WIFI_IP_ADDRESS); + else if (incoming == 7) + PERIODIC_TOGGLE(PD_WIFI_STATE); + else if (incoming == 8) + PERIODIC_TOGGLE(PD_ZED_DATA_RX); + else if (incoming == 9) + PERIODIC_TOGGLE(PD_ZED_DATA_TX); + + else if (incoming == 20) + { + int value = getNumber(); + if ((value != INPUT_RESPONSE_GETNUMBER_EXIT) && (value != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + settings.periodicDisplay = value; + } + else if (incoming == 21) + { + int seconds = getNumber(); + if ((seconds != INPUT_RESPONSE_GETNUMBER_EXIT) && (seconds != INPUT_RESPONSE_GETNUMBER_TIMEOUT)) + settings.periodicDisplayInterval = seconds * 1000; + } + else if (incoming == 22) + settings.enablePrintIdleTime ^= 1; + else if (incoming == 23) + PERIODIC_TOGGLE(PD_NETWORK_STATE); + else if (incoming == 24) + PERIODIC_TOGGLE(PD_RING_BUFFER_MILLIS); + else if (incoming == 25) + settings.enablePrintPosition ^= 1; + else if (incoming == 26) + settings.enablePrintState ^= 1; + + else if (incoming == 40) + PERIODIC_TOGGLE(PD_NTP_SERVER_DATA); + else if (incoming == 41) + PERIODIC_TOGGLE(PD_NTP_SERVER_STATE); + else if (incoming == 42) + PERIODIC_TOGGLE(PD_NTRIP_CLIENT_DATA); + else if (incoming == 43) + PERIODIC_TOGGLE(PD_NTRIP_CLIENT_GGA); + else if (incoming == 44) + PERIODIC_TOGGLE(PD_NTRIP_CLIENT_STATE); + else if (incoming == 45) + PERIODIC_TOGGLE(PD_NTRIP_SERVER_DATA); + else if (incoming == 46) + PERIODIC_TOGGLE(PD_NTRIP_SERVER_STATE); + else if (incoming == 47) + PERIODIC_TOGGLE(PD_PVT_CLIENT_DATA); + else if (incoming == 48) + PERIODIC_TOGGLE(PD_PVT_CLIENT_STATE); + else if (incoming == 49) + PERIODIC_TOGGLE(PD_PVT_SERVER_CLIENT_DATA); + else if (incoming == 50) + PERIODIC_TOGGLE(PD_PVT_SERVER_DATA); + else if (incoming == 51) + PERIODIC_TOGGLE(PD_PVT_SERVER_STATE); + else if (incoming == 52) + PERIODIC_TOGGLE(PD_OTA_CLIENT_STATE); + + else if (incoming == 70) + PERIODIC_TOGGLE(PD_TASK_BLUETOOTH_READ); + else if (incoming == 71) + PERIODIC_TOGGLE(PD_TASK_BUTTON_CHECK); + else if (incoming == 72) + PERIODIC_TOGGLE(PD_TASK_GNSS_READ); + else if (incoming == 73) + PERIODIC_TOGGLE(PD_TASK_HANDLE_GNSS_DATA); + else if (incoming == 74) + PERIODIC_TOGGLE(PD_TASK_SD_SIZE_CHECK); + + // Menu exit control + else if (incoming == 'x') + break; + else if (incoming == INPUT_RESPONSE_GETCHARACTERNUMBER_EMPTY) + break; + else if (incoming == INPUT_RESPONSE_GETCHARACTERNUMBER_TIMEOUT) + break; + else + printUnknown(incoming); + } + + clearBuffer(); // Empty buffer of any newline chars +} + +// Print the current long/lat/alt/HPA/SIV +// From Example11_GetHighPrecisionPositionUsingDouble +void printCurrentConditions() +{ + if (online.gnss == true) + { + systemPrint("SIV: "); + systemPrint(numSV); + + systemPrint(", HPA (m): "); + systemPrint(horizontalAccuracy, 3); + + systemPrint(", Lat: "); + systemPrint(latitude, haeNumberOfDecimals); + systemPrint(", Lon: "); + systemPrint(longitude, haeNumberOfDecimals); + + systemPrint(", Altitude (m): "); + systemPrint(altitude, 1); + + systemPrintln(); + } +} + +void printCurrentRTKState() +{ + if (online.gnss == true) + { + systemPrint("RTK solution: "); + + if (carrSoln == 0) // No RTK + systemPrint("NONE"); + + else if (carrSoln == 1) // RTK Float + systemPrint("FLOAT"); + + else if (carrSoln == 2) // RTK Fix + systemPrint("FIX"); + + else + systemPrint("UNKNOWN!"); + + systemPrintln(); + } +} + +void printCurrentConditionsNMEA() +{ + if (online.gnss == true) + { + char systemStatus[100]; + snprintf(systemStatus, sizeof(systemStatus), + "%02d%02d%02d.%02d,%02d%02d%02d,%0.3f,%d,%0.9f,%0.9f,%0.2f,%d,%d,%d", gnssHour, gnssMinute, gnssSecond, + mseconds, gnssDay, gnssMonth, gnssYear % 2000, // Limit to 2 digits + horizontalAccuracy, numSV, latitude, longitude, altitude, fixType, carrSoln, battLevel); + + char nmeaMessage[100]; // Max NMEA sentence length is 82 + createNMEASentence(CUSTOM_NMEA_TYPE_STATUS, nmeaMessage, sizeof(nmeaMessage), + systemStatus); // textID, buffer, sizeOfBuffer, text + systemPrintln(nmeaMessage); + } + else + { + char nmeaMessage[100]; // Max NMEA sentence length is 82 + createNMEASentence(CUSTOM_NMEA_TYPE_STATUS, nmeaMessage, sizeof(nmeaMessage), + (char *)"OFFLINE"); // textID, buffer, sizeOfBuffer, text + systemPrintln(nmeaMessage); + } +} + +// When called, prints the contents of root folder list of files on SD card +// This allows us to replace the sd.ls() function to point at Serial and BT outputs +void printFileList() +{ + bool sdCardAlreadyMounted = online.microSD; + if (!online.microSD) + beginSD(); + + // Notify the user if the microSD card is not available + if (!online.microSD) + systemPrintln("microSD card not online!"); + else + { + // Attempt to gain access to the SD card + if (xSemaphoreTake(sdCardSemaphore, fatSemaphore_longWait_ms) == pdPASS) + { + markSemaphore(FUNCTION_PRINT_FILE_LIST); + + if (USE_SPI_MICROSD) + { + SdFile dir; + dir.open("/"); // Open root + uint16_t fileCount = 0; + + SdFile tempFile; + + systemPrintln("Files found:"); + + while (tempFile.openNext(&dir, O_READ)) + { + if (tempFile.isFile()) + { + fileCount++; + + // 2017-05-19 187362648 800_0291.MOV + + // Get File Date from sdFat + uint16_t fileDate; + uint16_t fileTime; + tempFile.getCreateDateTime(&fileDate, &fileTime); + + // Convert sdFat file date fromat into YYYY-MM-DD + char fileDateChar[20]; + snprintf(fileDateChar, sizeof(fileDateChar), "%d-%02d-%02d", + ((fileDate >> 9) + 1980), // Year + ((fileDate >> 5) & 0b1111), // Month + (fileDate & 0b11111) // Day + ); + + char fileSizeChar[20]; + String fileSize; + stringHumanReadableSize(fileSize, tempFile.fileSize()); + fileSize.toCharArray(fileSizeChar, sizeof(fileSizeChar)); + + char fileName[50]; // Handle long file names + tempFile.getName(fileName, sizeof(fileName)); + + char fileRecord[100]; + snprintf(fileRecord, sizeof(fileRecord), "%s\t%s\t%s", fileDateChar, fileSizeChar, fileName); + + systemPrintln(fileRecord); + } + } + + dir.close(); + tempFile.close(); + + if (fileCount == 0) + systemPrintln("No files found"); + } +#ifdef COMPILE_SD_MMC + else + { + File dir = SD_MMC.open("/"); // Open root + uint16_t fileCount = 0; + + if (dir && dir.isDirectory()) + { + systemPrintln("Files found:"); + + File tempFile = dir.openNextFile(); + while (tempFile) + { + if (!tempFile.isDirectory()) + { + fileCount++; + + // 2017-05-19 187362648 800_0291.MOV + + // Get time of last write + time_t lastWrite = tempFile.getLastWrite(); + + struct tm *timeinfo = localtime(&lastWrite); + + char fileDateChar[20]; + snprintf(fileDateChar, sizeof(fileDateChar), "%.0f-%02.0f-%02.0f", + (float)timeinfo->tm_year + 1900, // Year - ESP32 2.0.2 starts the year at 1900... + (float)timeinfo->tm_mon + 1, // Month + (float)timeinfo->tm_mday // Day + ); + + char fileSizeChar[20]; + String fileSize; + stringHumanReadableSize(fileSize, tempFile.size()); + fileSize.toCharArray(fileSizeChar, sizeof(fileSizeChar)); + + char fileName[50]; // Handle long file names + snprintf(fileName, sizeof(fileName), "%s", tempFile.name()); + + char fileRecord[100]; + snprintf(fileRecord, sizeof(fileRecord), "%s\t%s\t%s", fileDateChar, fileSizeChar, + fileName); + + systemPrintln(fileRecord); + } + + tempFile.close(); + tempFile = dir.openNextFile(); + } + } + + dir.close(); + + if (fileCount == 0) + systemPrintln("No files found"); + } +#endif // COMPILE_SD_MMC + } + else + { + char semaphoreHolder[50]; + getSemaphoreFunction(semaphoreHolder); + + // This is an error because the current settings no longer match the settings + // on the microSD card, and will not be restored to the expected settings! + systemPrintf("sdCardSemaphore failed to yield, held by %s, menuSystem.ino line %d\r\n", semaphoreHolder, + __LINE__); + } + + // Release the SD card if not originally mounted + if (sdCardAlreadyMounted) + xSemaphoreGive(sdCardSemaphore); + else + endSD(true, true); + } +} diff --git a/Firmware/RTK_Surveyor/menuTest.ino b/Firmware/RTK_Surveyor/menuTest.ino deleted file mode 100644 index a8844980b..000000000 --- a/Firmware/RTK_Surveyor/menuTest.ino +++ /dev/null @@ -1,119 +0,0 @@ -//Production testing -//Allow operator to output NMEA on radio port for connector testing -//Scan for display -void menuTest() -{ - inTestMode = true; //Reroutes bluetooth bytes - - //Enable RTCM 1230. This is the GLONASS bias sentence and is transmitted - //even if there is no GPS fix. We use it to test serial output. - i2cGNSS.enableRTCMmessage(UBX_RTCM_1230, COM_PORT_UART2, 1); //Enable message every second - - while (1) - { - Serial.println(); - Serial.println(F("Menu: Test Menu")); - - Serial.print(F("Bluetooth broadcasting as: ")); - Serial.println(deviceName); - - Serial.println(F("Radio Port is now outputting RTCM")); - - if (settings.enableSD && online.microSD) - { - Serial.print(F("microSD card detected:")); - if (createTestFile() == false) - { - Serial.print(F(" Failed to create test file. Format SD card with 'SD Card Formatter'.")); - } - Serial.println(); - } - - //0x3D is default on Qwiic board - if (isConnected(0x3D) == true || isConnected(0x3C) == true) - Serial.println(F("Qwiic Good. Display detected.")); - else - Serial.println(F("Qwiic port failed! No display detected.")); - - Serial.println(F("Any character received over Blueooth connection will be displayed here")); - - Serial.println(F("1) Display microSD contents")); - Serial.println(F("2) Turn on all messages on USB port")); - Serial.println(F("3) Reset USB Messages to Defaults (NMEAx6)")); - Serial.println(F("4) Duplicate UART messages to USB")); - - Serial.println(F("x) Exit")); - - int incoming = getNumber(menuTimeout); //Timeout after x seconds - - if (incoming == 1) - { - if (settings.enableSD && online.microSD) - { - //Attempt to access file system. This avoids collisions with file writing from other functions like recordSystemSettingsToFile() and F9PSerialReadTask() - if (xSemaphoreTake(xFATSemaphore, fatSemaphore_longWait_ms) == pdPASS) - { - Serial.println(F("Files found (date time size name):\n\r")); - sd.ls(LS_R | LS_DATE | LS_SIZE); - - xSemaphoreGive(xFATSemaphore); - } - } - } - else if (incoming == 2) - { -// ubxMsgs usbMessage; //Create temp struct -// setGNSSMessageRates(usbMessage, 1); //Turn on all messages to report once per fix -// -// //Now send that struct -// bool response = configureGNSSMessageRates(COM_PORT_USB, usbMessage); //Make sure the appropriate messages are enabled -// if (response == false) -// Serial.println(F("menuTest: Failed to enable USB messages")); -// else -// Serial.println(F("All messages enabled")); - } - else if (incoming == 3) - { -// ubxMsgs usbMessage; //Create temp struct -// setGNSSMessageRates(usbMessage, 0); //Turn off all messages to report -// -// //Turn on default 6 -// usbMessage.nmea_gga.msgRate = 1; -// usbMessage.nmea_gsa.msgRate = 1; -// usbMessage.nmea_gst.msgRate = 1; -// usbMessage.nmea_gsv.msgRate = 1; -// usbMessage.nmea_rmc.msgRate = 1; -// usbMessage.nmea_vtg.msgRate = 1; -// -// //Now send that struct -// bool response = configureGNSSMessageRates(COM_PORT_USB, usbMessage); //Make sure the appropriate messages are enabled -// if (response == false) -// Serial.println(F("menuTest: Failed to enable USB messages")); -// else -// Serial.println(F("All messages enabled")); - } - else if (incoming == 4) - { - //Send the current settings to USB - bool response = configureGNSSMessageRates(COM_PORT_USB, ubxMessages); //Make sure the appropriate messages are enabled - if (response == false) - Serial.println(F("menuTest: Failed to enable USB messages")); - else - Serial.println(F("USB now matches UART messages")); - } - - else if (incoming == STATUS_PRESSED_X) - break; - else if (incoming == STATUS_GETNUMBER_TIMEOUT) - break; - else - printUnknown(incoming); - } - - inTestMode = false; //Reroutes bluetooth bytes - - //Disable RTCM sentences - i2cGNSS.enableRTCMmessage(UBX_RTCM_1230, COM_PORT_UART2, 0); - - while (Serial.available()) Serial.read(); //Empty buffer of any newline chars -} diff --git a/Firmware/RTK_Surveyor/settings.h b/Firmware/RTK_Surveyor/settings.h index f434e7410..2a792e2e8 100644 --- a/Firmware/RTK_Surveyor/settings.h +++ b/Firmware/RTK_Surveyor/settings.h @@ -1,325 +1,1274 @@ -//System can enter a variety of states starting at Rover_No_Fix at power on +#ifndef __SETTINGS_H__ +#define __SETTINGS_H__ + +// System can enter a variety of states +// See statemachine diagram at: +// https://lucid.app/lucidchart/53519501-9fa5-4352-aa40-673f88ca0c9b/edit?invitationId=inv_ebd4b988-513d-4169-93fd-c291851108f8 typedef enum { - STATE_ROVER_NOT_STARTED = 0, - STATE_ROVER_NO_FIX, - STATE_ROVER_FIX, - STATE_ROVER_RTK_FLOAT, - STATE_ROVER_RTK_FIX, - STATE_BASE_NOT_STARTED, - STATE_BASE_TEMP_SETTLE, //User has indicated base, but current pos accuracy is too low - STATE_BASE_TEMP_SURVEY_STARTED, - STATE_BASE_TEMP_TRANSMITTING, - STATE_BASE_TEMP_WIFI_STARTED, - STATE_BASE_TEMP_WIFI_CONNECTED, - STATE_BASE_TEMP_CASTER_STARTED, - STATE_BASE_TEMP_CASTER_CONNECTED, - STATE_BASE_FIXED_NOT_STARTED, - STATE_BASE_FIXED_TRANSMITTING, - STATE_BASE_FIXED_WIFI_STARTED, - STATE_BASE_FIXED_WIFI_CONNECTED, - STATE_BASE_FIXED_CASTER_STARTED, - STATE_BASE_FIXED_CASTER_CONNECTED, + STATE_ROVER_NOT_STARTED = 0, + STATE_ROVER_NO_FIX, + STATE_ROVER_FIX, + STATE_ROVER_RTK_FLOAT, + STATE_ROVER_RTK_FIX, + STATE_BASE_NOT_STARTED, + STATE_BASE_TEMP_SETTLE, // User has indicated base, but current pos accuracy is too low + STATE_BASE_TEMP_SURVEY_STARTED, + STATE_BASE_TEMP_TRANSMITTING, + STATE_BASE_FIXED_NOT_STARTED, + STATE_BASE_FIXED_TRANSMITTING, + STATE_BUBBLE_LEVEL, + STATE_MARK_EVENT, + STATE_DISPLAY_SETUP, + STATE_WIFI_CONFIG_NOT_STARTED, + STATE_WIFI_CONFIG, + STATE_TEST, + STATE_TESTING, + STATE_PROFILE, + STATE_KEYS_STARTED, + STATE_KEYS_NEEDED, + STATE_KEYS_WIFI_STARTED, + STATE_KEYS_WIFI_CONNECTED, + STATE_KEYS_WIFI_TIMEOUT, + STATE_KEYS_EXPIRED, + STATE_KEYS_DAYS_REMAINING, + STATE_KEYS_LBAND_CONFIGURE, + STATE_KEYS_LBAND_ENCRYPTED, + STATE_KEYS_PROVISION_WIFI_STARTED, + STATE_KEYS_PROVISION_WIFI_CONNECTED, + STATE_ESPNOW_PAIRING_NOT_STARTED, + STATE_ESPNOW_PAIRING, + STATE_NTPSERVER_NOT_STARTED, + STATE_NTPSERVER_NO_SYNC, + STATE_NTPSERVER_SYNC, + STATE_CONFIG_VIA_ETH_NOT_STARTED, + STATE_CONFIG_VIA_ETH_STARTED, + STATE_CONFIG_VIA_ETH, + STATE_CONFIG_VIA_ETH_RESTART_BASE, + STATE_SHUTDOWN, + STATE_NOT_SET, // Must be last on list } SystemState; -volatile SystemState systemState = STATE_ROVER_NOT_STARTED; +volatile SystemState systemState = STATE_NOT_SET; +SystemState lastSystemState = STATE_NOT_SET; +SystemState requestedSystemState = STATE_NOT_SET; +bool newSystemStateRequested = false; + +// The setup display can show a limited set of states +// When user pauses for X amount of time, system will enter that state +SystemState setupState = STATE_MARK_EVENT; + +// Base modes set with RTK_MODE +#define RTK_MODE_BASE_FIXED 0x0001 +#define RTK_MODE_BASE_SURVEY_IN 0x0002 +#define RTK_MODE_BUBBLE_LEVEL 0x0004 +#define RTK_MODE_ETHERNET_CONFIG 0x0008 +#define RTK_MODE_NTP 0x0010 +#define RTK_MODE_ROVER 0x0020 +#define RTK_MODE_TESTING 0x0040 +#define RTK_MODE_WIFI_CONFIG 0x0080 + +typedef uint8_t RtkMode_t; + +#define RTK_MODE(mode) rtkMode = mode; + +#define EQ_RTK_MODE(mode) (rtkMode && (rtkMode == (mode & rtkMode))) +#define NEQ_RTK_MODE(mode) (rtkMode && (rtkMode != (mode & rtkMode))) typedef enum { - RTK_SURVEYOR = 0, - RTK_EXPRESS, - RTK_FACET, + RTK_SURVEYOR = 0, + RTK_EXPRESS, + RTK_FACET, + RTK_EXPRESS_PLUS, + RTK_FACET_LBAND, + REFERENCE_STATION, + RTK_FACET_LBAND_DIRECT, + // Add new values just above this line + RTK_UNKNOWN, } ProductVariant; ProductVariant productVariant = RTK_SURVEYOR; +const char *const productDisplayNames[] = { + "Surveyor", + "Express", + "Facet", + "Express+", + "Facet LB", + "Ref Stn", + "Facet LD", + // Add new values just above this line + "Unknown", +}; +const int productDisplayNamesEntries = sizeof(productDisplayNames) / sizeof(productDisplayNames[0]); + +const char *const platformFilePrefixTable[] = { + "SFE_Surveyor", + "SFE_Express", + "SFE_Facet", + "SFE_Express_Plus", + "SFE_Facet_LBand", + "SFE_Reference_Station", + "SFE_Facet_LBand_Direct", + // Add new values just above this line + "SFE_Unknown", +}; +const int platformFilePrefixTableEntries = sizeof(platformFilePrefixTable) / sizeof(platformFilePrefixTable[0]); + +const char *const platformPrefixTable[] = { + "Surveyor", + "Express", + "Facet", + "Express Plus", + "Facet L-Band", + "Reference Station", + "Facet L-Band Direct", + // Add new values just above this line + "Unknown", +}; +const int platformPrefixTableEntries = sizeof(platformPrefixTable) / sizeof(platformPrefixTable[0]); + +// Macros to show if the GNSS is I2C or SPI +#define USE_SPI_GNSS (productVariant == REFERENCE_STATION) +#define USE_I2C_GNSS (!USE_SPI_GNSS) + +// Macros to show if the microSD is SPI or SDIO +#define USE_MMC_MICROSD (productVariant == REFERENCE_STATION) +#define USE_SPI_MICROSD (!USE_MMC_MICROSD) + +// Macro to show if the the RTK variant has Ethernet +#ifdef COMPILE_ETHERNET +#define HAS_ETHERNET (productVariant == REFERENCE_STATION) +#else // COMPILE_ETHERNET +#define HAS_ETHERNET false +#endif // COMPILE_ETHERNET + +// Macro to show if the the RTK variant has a GNSS TP interrupt - for accurate clock setting +// The GNSS UBX PVT message is sent ahead of the top-of-second +// The rising edge of the TP signal indicates the true top-of-second +#define HAS_GNSS_TP_INT (productVariant == REFERENCE_STATION) + +// Macro to show if the the RTK variant has no battery +#define HAS_NO_BATTERY (productVariant == REFERENCE_STATION) +#define HAS_BATTERY (!HAS_NO_BATTERY) + +// Macro to show if the the RTK variant has antenna short circuit / open circuit detection +#define HAS_ANTENNA_SHORT_OPEN (productVariant == REFERENCE_STATION) + typedef enum { - BUTTON_ROVER = 0, - BUTTON_BASE, - BUTTON_PRESSED, - BUTTON_RELEASED, + BUTTON_ROVER = 0, + BUTTON_BASE, } ButtonState; ButtonState buttonPreviousState = BUTTON_ROVER; -ButtonState setupButtonState = BUTTON_RELEASED; //RTK Express Setup Button -//Data port mux (RTK Express) can enter one of four different connections -typedef enum muxConnectionType_e +// Data port mux (RTK Express) can enter one of four different connections +typedef enum { - MUX_UBLOX_NMEA = 0, - MUX_PPS_EVENTTRIGGER, - MUX_I2C, - MUX_ADC_DAC, + MUX_UBLOX_NMEA = 0, + MUX_PPS_EVENTTRIGGER, + MUX_I2C_WT, + MUX_ADC_DAC, } muxConnectionType_e; -//User can enter fixed base coordinates in ECEF or degrees +// User can enter fixed base coordinates in ECEF or degrees typedef enum { - COORD_TYPE_ECEF = 0, - COORD_TYPE_GEOGRAPHIC, + COORD_TYPE_ECEF = 0, + COORD_TYPE_GEODETIC, } coordinateType_e; -//Freeze and blink LEDs if we hit a bad error +// User can select output pulse as either falling or rising edge +typedef enum +{ + PULSE_FALLING_EDGE = 0, + PULSE_RISING_EDGE, +} pulseEdgeType_e; + +// Custom NMEA sentence types output to the log file +typedef enum +{ + CUSTOM_NMEA_TYPE_RESET_REASON = 0, + CUSTOM_NMEA_TYPE_WAYPOINT, + CUSTOM_NMEA_TYPE_EVENT, + CUSTOM_NMEA_TYPE_SYSTEM_VERSION, + CUSTOM_NMEA_TYPE_ZED_VERSION, + CUSTOM_NMEA_TYPE_STATUS, + CUSTOM_NMEA_TYPE_LOGTEST_STATUS, + CUSTOM_NMEA_TYPE_DEVICE_BT_ID, + CUSTOM_NMEA_TYPE_PARSER_STATS, + CUSTOM_NMEA_TYPE_CURRENT_DATE, + CUSTOM_NMEA_TYPE_ARP_ECEF_XYZH, + CUSTOM_NMEA_TYPE_ZED_UNIQUE_ID, +} customNmeaType_e; + +// Freeze and blink LEDs if we hit a bad error typedef enum { - ERROR_NO_I2C = 2, //Avoid 0 and 1 as these are bad blink codes - ERROR_GPS_CONFIG_FAIL, + ERROR_NO_I2C = 2, // Avoid 0 and 1 as these are bad blink codes + ERROR_GPS_CONFIG_FAIL, } t_errorNumber; -//Radio status LED goes from off (LED off), no connection (blinking), to connected (solid) -enum RadioState +// Define the types of network +enum NetworkTypes { - RADIO_OFF = 0, - BT_ON_NOCONNECTION, //WiFi is off - BT_CONNECTED, - WIFI_ON_NOCONNECTION, //BT is off - WIFI_CONNECTED, + NETWORK_TYPE_WIFI = 0, + NETWORK_TYPE_ETHERNET, + // Last hardware network type + NETWORK_TYPE_MAX, + + // Special cases + NETWORK_TYPE_USE_DEFAULT = NETWORK_TYPE_MAX, + NETWORK_TYPE_ACTIVE, + // Last network type + NETWORK_TYPE_LAST, }; -volatile byte radioState = RADIO_OFF; -//Return values for getByteChoice() -enum returnStatus { - STATUS_GETBYTE_TIMEOUT = 255, - STATUS_GETNUMBER_TIMEOUT = -123455555, - STATUS_PRESSED_X = 254, +// Define the states of the network device +enum NetworkStates +{ + NETWORK_STATE_OFF = 0, + NETWORK_STATE_DELAY, + NETWORK_STATE_CONNECTING, + NETWORK_STATE_IN_USE, + NETWORK_STATE_WAIT_NO_USERS, + // Last network state + NETWORK_STATE_MAX }; -#include //http://librarymanager/All#SparkFun_u-blox_GNSS +// Define the network users +enum NetworkUsers +{ + NETWORK_USER_NTP_SERVER = 0, // NTP server + NETWORK_USER_NTRIP_CLIENT, // NTRIP client + NETWORK_USER_OTA_FIRMWARE_UPDATE, // Over-The-Air firmware updates + NETWORK_USER_PVT_CLIENT, // PVT client + NETWORK_USER_PVT_SERVER, // PVT server + NETWORK_USER_PVT_UDP_SERVER, // PVT UDP server -//Each constellation will have its config key, enable, and a visible name -typedef struct ubxConstellation + // Add new users above this line + NETWORK_USER_NTRIP_SERVER, // NTRIP server + // Last network user + NETWORK_USER_MAX = NETWORK_USER_NTRIP_SERVER + NTRIP_SERVER_MAX +}; + +typedef uint16_t NETWORK_USER; + +typedef struct _NETWORK_DATA +{ + uint8_t requestedNetwork; // Type of network requested + uint8_t type; // Type of network + NETWORK_USER activeUsers; // Active users of this network device + NETWORK_USER userOpens; // Users requesting access to this network + uint8_t connectionAttempt; // Number of previous connection attempts + bool restart; // Set if restart is allowed + bool shutdown; // Network is shutting down + uint8_t state; // Current state of the network + uint32_t timeout; // Timer timeout value + uint32_t timerStart; // Starting millis for the timer +} NETWORK_DATA; + +// Even though WiFi and ESP-Now operate on the same radio, we treat +// then as different states so that we can leave the radio on if +// either WiFi or ESP-Now are active +enum WiFiState +{ + WIFI_STATE_OFF = 0, + WIFI_STATE_START, + WIFI_STATE_CONNECTING, + WIFI_STATE_CONNECTED, +}; +volatile byte wifiState = WIFI_STATE_OFF; + +#include "NetworkClient.h" // Built-in - Supports both WiFiClient and EthernetClient +#include "NetworkUDP.h" //Built-in - Supports both WiFiUdp and EthernetUdp + +// NTRIP Server data +typedef struct _NTRIP_SERVER_DATA +{ + // Network connection used to push RTCM to NTRIP caster + NetworkClient *networkClient; + volatile uint8_t state; + + // Count of bytes sent by the NTRIP server to the NTRIP caster + uint32_t bytesSent; + + // Throttle the time between connection attempts + // ms - Max of 4,294,967,295 or 4.3M seconds or 71,000 minutes or 1193 hours or 49 days between attempts + uint32_t connectionAttemptTimeout; + uint32_t lastConnectionAttempt; + int connectionAttempts; // Count the number of connection attempts between restarts + + // NTRIP server timer usage: + // * Reconnection delay + // * Measure the connection response time + // * Receive RTCM correction data timeout + // * Monitor last RTCM byte received for frame counting + uint32_t timer; + uint32_t startTime; + int connectionAttemptsTotal; // Count the number of connection attempts absolutely + + // Additional count / times for ntripServerProcessRTCM + uint32_t zedBytesSent ; + uint32_t previousMilliseconds; +} NTRIP_SERVER_DATA; + +typedef enum +{ + ESPNOW_OFF, + ESPNOW_ON, + ESPNOW_PAIRING, + ESPNOW_MAC_RECEIVED, + ESPNOW_PAIRED, +} ESPNOWState; +volatile ESPNOWState espnowState = ESPNOW_OFF; + +typedef enum +{ + RTCM_TRANSPORT_STATE_WAIT_FOR_PREAMBLE_D3 = 0, + RTCM_TRANSPORT_STATE_READ_LENGTH_1, + RTCM_TRANSPORT_STATE_READ_LENGTH_2, + RTCM_TRANSPORT_STATE_READ_MESSAGE_1, + RTCM_TRANSPORT_STATE_READ_MESSAGE_2, + RTCM_TRANSPORT_STATE_READ_DATA, + RTCM_TRANSPORT_STATE_READ_CRC_1, + RTCM_TRANSPORT_STATE_READ_CRC_2, + RTCM_TRANSPORT_STATE_READ_CRC_3, + RTCM_TRANSPORT_STATE_CHECK_CRC +} RtcmTransportState; + +typedef enum { - uint32_t configKey; - uint8_t gnssID; - bool enabled; - char textName[30]; + RADIO_EXTERNAL = 0, + RADIO_ESPNOW, +} RadioType_e; + +typedef enum +{ + BLUETOOTH_RADIO_SPP = 0, + BLUETOOTH_RADIO_BLE, + BLUETOOTH_RADIO_OFF, +} BluetoothRadioType_e; + +// Don't make this a typedef enum as logTestState +// can be incremented beyond LOGTEST_END +enum LogTestState +{ + LOGTEST_START = 0, + LOGTEST_4HZ_5MSG_10MS, + LOGTEST_4HZ_7MSG_10MS, + LOGTEST_10HZ_5MSG_10MS, + LOGTEST_10HZ_7MSG_10MS, + LOGTEST_4HZ_5MSG_0MS, + LOGTEST_4HZ_7MSG_0MS, + LOGTEST_10HZ_5MSG_0MS, + LOGTEST_10HZ_7MSG_0MS, + LOGTEST_4HZ_5MSG_50MS, + LOGTEST_4HZ_7MSG_50MS, + LOGTEST_10HZ_5MSG_50MS, + LOGTEST_10HZ_7MSG_50MS, + LOGTEST_END, +}; +uint8_t logTestState = LOGTEST_END; + +typedef struct WiFiNetwork +{ + char ssid[50]; + char password[50]; +} WiFiNetwork; + +#define MAX_WIFI_NETWORKS 4 + +typedef uint16_t RING_BUFFER_OFFSET; + +typedef enum +{ + ETH_NOT_STARTED = 0, + ETH_STARTED_CHECK_CABLE, + ETH_STARTED_START_DHCP, + ETH_CONNECTED, + ETH_CAN_NOT_BEGIN, + // Add new states here + ETH_MAX_STATE +} ethernetStatus_e; + +const char *const ethernetStates[] = { + "ETH_NOT_STARTED", "ETH_STARTED_CHECK_CABLE", "ETH_STARTED_START_DHCP", "ETH_CONNECTED", "ETH_CAN_NOT_BEGIN", +}; + +const int ethernetStateEntries = sizeof(ethernetStates) / sizeof(ethernetStates[0]); + +// Radio status LED goes from off (LED off), no connection (blinking), to connected (solid) +typedef enum +{ + BT_OFF = 0, + BT_NOTCONNECTED, + BT_CONNECTED, +} BTState; + +// Return values for getString() +typedef enum +{ + INPUT_RESPONSE_GETNUMBER_EXIT = + -9999999, // Less than min ECEF. User may be prompted for number but wants to exit without entering data + INPUT_RESPONSE_GETNUMBER_TIMEOUT = -9999998, + INPUT_RESPONSE_GETCHARACTERNUMBER_TIMEOUT = 255, + INPUT_RESPONSE_GETCHARACTERNUMBER_EMPTY = 254, + INPUT_RESPONSE_INVALID = -4, + INPUT_RESPONSE_TIMEOUT = -3, + INPUT_RESPONSE_OVERFLOW = -2, + INPUT_RESPONSE_EMPTY = -1, + INPUT_RESPONSE_VALID = 1, +} InputResponse; + +typedef enum +{ + PRINT_ENDPOINT_SERIAL = 0, + PRINT_ENDPOINT_BLUETOOTH, + PRINT_ENDPOINT_ALL, +} PrintEndpoint; +PrintEndpoint printEndpoint = PRINT_ENDPOINT_SERIAL; // Controls where the configuration menu gets piped to + +typedef enum +{ + FUNCTION_NOT_SET = 0, + FUNCTION_SYNC, + FUNCTION_WRITESD, + FUNCTION_FILESIZE, + FUNCTION_EVENT, + FUNCTION_BEGINSD, + FUNCTION_RECORDSETTINGS, + FUNCTION_LOADSETTINGS, + FUNCTION_MARKEVENT, + FUNCTION_GETLINE, + FUNCTION_REMOVEFILE, + FUNCTION_RECORDLINE, + FUNCTION_CREATEFILE, + FUNCTION_ENDLOGGING, + FUNCTION_FINDLOG, + FUNCTION_LOGTEST, + FUNCTION_FILELIST, + FUNCTION_FILEMANAGER_OPEN1, + FUNCTION_FILEMANAGER_OPEN2, + FUNCTION_FILEMANAGER_OPEN3, + FUNCTION_FILEMANAGER_UPLOAD1, + FUNCTION_FILEMANAGER_UPLOAD2, + FUNCTION_FILEMANAGER_UPLOAD3, + FUNCTION_SDSIZECHECK, + FUNCTION_LOG_CLOSURE, + FUNCTION_PRINT_FILE_LIST, + FUNCTION_NTPEVENT, + +} SemaphoreFunction; + +#include //http://librarymanager/All#SparkFun_u-blox_GNSS_v3 + +// Each constellation will have its config key, enable, and a visible name +typedef struct +{ + uint32_t configKey; + uint8_t gnssID; + bool enabled; + char textName[30]; } ubxConstellation; -//These are the allowable constellations to receive from and log (if enabled) -//Tested with u-center v21.02 -#define MAX_CONSTELLATIONS 6 -ubxConstellation ubxConstellations[MAX_CONSTELLATIONS] = -{ - {UBLOX_CFG_SIGNAL_GPS_ENA, SFE_UBLOX_GNSS_ID_GPS, true, "GPS"}, - {UBLOX_CFG_SIGNAL_SBAS_ENA, SFE_UBLOX_GNSS_ID_SBAS, false, "SBAS"}, //Bug in ZED-F9P v1.13 firmware causes RTK LED to not light when RTK Floating with SBAS on. - {UBLOX_CFG_SIGNAL_GAL_ENA, SFE_UBLOX_GNSS_ID_GALILEO, true, "Galileo"}, - {UBLOX_CFG_SIGNAL_BDS_ENA, SFE_UBLOX_GNSS_ID_BEIDOU, true, "BeiDou"}, - //{UBLOX_CFG_SIGNAL_QZSS_ENA, SFE_UBLOX_GNSS_ID_IMES, false, "IMES"}, //Not yet supported? Config key does not exist? - {UBLOX_CFG_SIGNAL_QZSS_ENA, SFE_UBLOX_GNSS_ID_QZSS, true, "QZSS"}, - {UBLOX_CFG_SIGNAL_GLO_ENA, SFE_UBLOX_GNSS_ID_GLONASS, true, "GLONASS"}, +// These are the allowable constellations to receive from and log (if enabled) +// Tested with u-center v21.02 +#define MAX_CONSTELLATIONS 6 //(sizeof(ubxConstellations)/sizeof(ubxConstellation)) + +// Different ZED modules support different messages (F9P vs F9R vs F9T) +// Create binary packed struct for different platforms +typedef enum +{ + PLATFORM_F9P = 0b0001, + PLATFORM_F9R = 0b0010, + PLATFORM_F9T = 0b0100, +} ubxPlatform; + +// Print the base coordinates in different formats, depending on the type the user has entered +// These are the different supported types +typedef enum +{ + COORDINATE_INPUT_TYPE_DD = 0, // Default DD.ddddddddd + COORDINATE_INPUT_TYPE_DDMM, // DDMM.mmmmm + COORDINATE_INPUT_TYPE_DD_MM, // DD MM.mmmmm + COORDINATE_INPUT_TYPE_DD_MM_DASH, // DD-MM.mmmmm + COORDINATE_INPUT_TYPE_DD_MM_SYMBOL, // DD°MM.mmmmmmm' + COORDINATE_INPUT_TYPE_DDMMSS, // DD MM SS.ssssss + COORDINATE_INPUT_TYPE_DD_MM_SS, // DD MM SS.ssssss + COORDINATE_INPUT_TYPE_DD_MM_SS_DASH, // DD-MM-SS.ssssss + COORDINATE_INPUT_TYPE_DD_MM_SS_SYMBOL, // DD°MM'SS.ssssss" + COORDINATE_INPUT_TYPE_DDMMSS_NO_DECIMAL, // DDMMSS - No decimal + COORDINATE_INPUT_TYPE_DD_MM_SS_NO_DECIMAL, // DD MM SS - No decimal + COORDINATE_INPUT_TYPE_DD_MM_SS_DASH_NO_DECIMAL, // DD-MM-SS - No decimal + COORDINATE_INPUT_TYPE_INVALID_UNKNOWN, +} CoordinateInputType; + +#define UBX_ID_NOT_AVAILABLE 0xFF + +// Define the periodic display values +typedef uint32_t PeriodicDisplay_t; + +enum PeriodDisplayValues +{ + PD_BLUETOOTH_DATA_RX = 0, // 0 + PD_BLUETOOTH_DATA_TX, // 1 + + PD_ETHERNET_IP_ADDRESS, // 2 + PD_ETHERNET_STATE, // 3 + + PD_NETWORK_STATE, // 4 + + PD_NTP_SERVER_DATA, // 5 + PD_NTP_SERVER_STATE, // 6 + + PD_NTRIP_CLIENT_DATA, // 7 + PD_NTRIP_CLIENT_GGA, // 8 + PD_NTRIP_CLIENT_STATE, // 9 + + PD_NTRIP_SERVER_DATA, // 10 + PD_NTRIP_SERVER_STATE, // 11 + + PD_PVT_CLIENT_DATA, // 12 + PD_PVT_CLIENT_STATE, // 13 + + PD_PVT_SERVER_DATA, // 14 + PD_PVT_SERVER_STATE, // 15 + PD_PVT_SERVER_CLIENT_DATA, // 16 + + PD_PVT_UDP_SERVER_DATA, // 17 + PD_PVT_UDP_SERVER_STATE, // 18 + PD_PVT_UDP_SERVER_BROADCAST_DATA, // 19 + + PD_RING_BUFFER_MILLIS, // 20 + + PD_SD_LOG_WRITE, // 21 + + PD_TASK_BLUETOOTH_READ, // 22 + PD_TASK_BUTTON_CHECK, // 23 + PD_TASK_GNSS_READ, // 24 + PD_TASK_HANDLE_GNSS_DATA, // 25 + PD_TASK_SD_SIZE_CHECK, // 26 + + PD_WIFI_IP_ADDRESS, // 27 + PD_WIFI_STATE, // 28 + + PD_ZED_DATA_RX, // 29 + PD_ZED_DATA_TX, // 30 + + PD_OTA_CLIENT_STATE, // 31 + // Add new values before this line }; -//Each message will have a rate, a visible name, and a class -typedef struct ubxMsg +#define PERIODIC_MASK(x) (1 << x) +#define PERIODIC_DISPLAY(x) (periodicDisplay & PERIODIC_MASK(x)) +#define PERIODIC_CLEAR(x) periodicDisplay &= ~PERIODIC_MASK(x) +#define PERIODIC_SETTING(x) (settings.periodicDisplay & PERIODIC_MASK(x)) +#define PERIODIC_TOGGLE(x) settings.periodicDisplay ^= PERIODIC_MASK(x) + +// These are the allowable messages to broadcast and log (if enabled) + +// Struct to describe the necessary info for each type of UBX message +// Each message will have a key, ID, class, visible name, and various info about which platforms the message is +// supported on Message rates are store within NVM +typedef struct { - uint32_t msgConfigKey; - uint8_t msgID; - uint8_t msgClass; - uint8_t msgRate; - char msgTextName[30]; + const uint32_t msgConfigKey; + const uint8_t msgID; + const uint8_t msgClass; + const uint8_t msgDefaultRate; + const char msgTextName[20]; + const uint32_t filterMask; + const uint16_t f9pFirmwareVersionSupported; // The minimum version this message is supported. 0 = all versions. 9999 + // = Not supported + const uint16_t f9rFirmwareVersionSupported; } ubxMsg; -//These are the allowable messages to broadcast and log (if enabled) -//Tested with u-center v21.02 -#define MAX_UBX_MSG 67 -ubxMsg ubxMessages[MAX_UBX_MSG] = -{ - {UBLOX_CFG_MSGOUT_NMEA_ID_DTM_UART1, UBX_NMEA_DTM, UBX_CLASS_NMEA, 0, "UBX_NMEA_DTM"}, - {UBLOX_CFG_MSGOUT_NMEA_ID_GBS_UART1, UBX_NMEA_GBS, UBX_CLASS_NMEA, 0, "UBX_NMEA_GBS"}, - {UBLOX_CFG_MSGOUT_NMEA_ID_GGA_UART1, UBX_NMEA_GGA, UBX_CLASS_NMEA, 1, "UBX_NMEA_GGA"}, - {UBLOX_CFG_MSGOUT_NMEA_ID_GLL_UART1, UBX_NMEA_GLL, UBX_CLASS_NMEA, 0, "UBX_NMEA_GLL"}, - {UBLOX_CFG_MSGOUT_NMEA_ID_GNS_UART1, UBX_NMEA_GNS, UBX_CLASS_NMEA, 0, "UBX_NMEA_GNS"}, - - {UBLOX_CFG_MSGOUT_NMEA_ID_GRS_UART1, UBX_NMEA_GRS, UBX_CLASS_NMEA, 0, "UBX_NMEA_GRS"}, - {UBLOX_CFG_MSGOUT_NMEA_ID_GSA_UART1, UBX_NMEA_GSA, UBX_CLASS_NMEA, 1, "UBX_NMEA_GSA"}, - {UBLOX_CFG_MSGOUT_NMEA_ID_GST_UART1, UBX_NMEA_GST, UBX_CLASS_NMEA, 1, "UBX_NMEA_GST"}, - {UBLOX_CFG_MSGOUT_NMEA_ID_GSV_UART1, UBX_NMEA_GSV, UBX_CLASS_NMEA, 4, "UBX_NMEA_GSV"}, //Default to 1 update every 4 fixes - {UBLOX_CFG_MSGOUT_NMEA_ID_RMC_UART1, UBX_NMEA_RMC, UBX_CLASS_NMEA, 1, "UBX_NMEA_RMC"}, - - {UBLOX_CFG_MSGOUT_NMEA_ID_VLW_UART1, UBX_NMEA_VLW, UBX_CLASS_NMEA, 0, "UBX_NMEA_VLW"}, - {UBLOX_CFG_MSGOUT_NMEA_ID_VTG_UART1, UBX_NMEA_VTG, UBX_CLASS_NMEA, 0, "UBX_NMEA_VTG"}, - {UBLOX_CFG_MSGOUT_NMEA_ID_ZDA_UART1, UBX_NMEA_ZDA, UBX_CLASS_NMEA, 0, "UBX_NMEA_ZDA"}, - - // uint8_t nmea_msb = 0; //Not supported by u-center - // uint8_t nmea_gaq = 0; //Not supported by u-center - // uint8_t nmea_gbq = 0; //Not supported by u-center - // uint8_t nmea_glq = 0; //Not supported by u-center - // uint8_t nmea_gnq = 0; //Not supported by u-center - // uint8_t nmea_gpq = 0; //Not supported by u-center - // uint8_t nmea_gqq = 0; //Not supported by u-center - // uint8_t nmea_rlm = 0; //Not supported by u-center - // uint8_t nmea_txt = 0; //Not supported by u-center - // uint8_t nmea_ths = 0; //Not supported by ZED-F9P - - {UBLOX_CFG_MSGOUT_UBX_NAV_CLOCK_UART1, UBX_NAV_CLOCK, UBX_CLASS_NAV, 0, "UBX_NAV_CLOCK"}, - {UBLOX_CFG_MSGOUT_UBX_NAV_DOP_UART1, UBX_NAV_DOP, UBX_CLASS_NAV, 0, "UBX_NAV_DOP"}, - {UBLOX_CFG_MSGOUT_UBX_NAV_EOE_UART1, UBX_NAV_EOE, UBX_CLASS_NAV, 0, "UBX_NAV_EOE"}, - {UBLOX_CFG_MSGOUT_UBX_NAV_GEOFENCE_UART1, UBX_NAV_GEOFENCE, UBX_CLASS_NAV, 0, "UBX_NAV_GEOFENCE"}, - {UBLOX_CFG_MSGOUT_UBX_NAV_HPPOSECEF_UART1, UBX_NAV_HPPOSECEF, UBX_CLASS_NAV, 0, "UBX_NAV_HPPOSECEF"}, - - {UBLOX_CFG_MSGOUT_UBX_NAV_HPPOSLLH_UART1, UBX_NAV_HPPOSLLH, UBX_CLASS_NAV, 0, "UBX_NAV_HPPOSLLH"}, - {UBLOX_CFG_MSGOUT_UBX_NAV_ODO_UART1, UBX_NAV_ODO, UBX_CLASS_NAV, 0, "UBX_NAV_ODO"}, - {UBLOX_CFG_MSGOUT_UBX_NAV_ORB_UART1, UBX_NAV_ORB, UBX_CLASS_NAV, 0, "UBX_NAV_ORB"}, - {UBLOX_CFG_MSGOUT_UBX_NAV_POSECEF_UART1, UBX_NAV_POSECEF, UBX_CLASS_NAV, 0, "UBX_NAV_POSECEF"}, - {UBLOX_CFG_MSGOUT_UBX_NAV_POSLLH_UART1, UBX_NAV_POSLLH, UBX_CLASS_NAV, 0, "UBX_NAV_POSLLH"}, - - {UBLOX_CFG_MSGOUT_UBX_NAV_PVT_UART1, UBX_NAV_PVT, UBX_CLASS_NAV, 0, "UBX_NAV_PVT"}, - {UBLOX_CFG_MSGOUT_UBX_NAV_RELPOSNED_UART1, UBX_NAV_RELPOSNED, UBX_CLASS_NAV, 0, "UBX_NAV_RELPOSNED"}, - {UBLOX_CFG_MSGOUT_UBX_NAV_SAT_UART1, UBX_NAV_SAT, UBX_CLASS_NAV, 0, "UBX_NAV_SAT"}, - {UBLOX_CFG_MSGOUT_UBX_NAV_SIG_UART1, UBX_NAV_SIG, UBX_CLASS_NAV, 0, "UBX_NAV_SIG"}, - {UBLOX_CFG_MSGOUT_UBX_NAV_STATUS_UART1, UBX_NAV_STATUS, UBX_CLASS_NAV, 0, "UBX_NAV_STATUS"}, - - {UBLOX_CFG_MSGOUT_UBX_NAV_SVIN_UART1, UBX_NAV_SVIN, UBX_CLASS_NAV, 0, "UBX_NAV_SVIN"}, - {UBLOX_CFG_MSGOUT_UBX_NAV_TIMEBDS_UART1, UBX_NAV_TIMEBDS, UBX_CLASS_NAV, 0, "UBX_NAV_TIMEBDS"}, - {UBLOX_CFG_MSGOUT_UBX_NAV_TIMEGAL_UART1, UBX_NAV_TIMEGAL, UBX_CLASS_NAV, 0, "UBX_NAV_TIMEGAL"}, - {UBLOX_CFG_MSGOUT_UBX_NAV_TIMEGLO_UART1, UBX_NAV_TIMEGLO, UBX_CLASS_NAV, 0, "UBX_NAV_TIMEGLO"}, - {UBLOX_CFG_MSGOUT_UBX_NAV_TIMEGPS_UART1, UBX_NAV_TIMEGPS, UBX_CLASS_NAV, 0, "UBX_NAV_TIMEGPS"}, - - {UBLOX_CFG_MSGOUT_UBX_NAV_TIMELS_UART1, UBX_NAV_TIMELS, UBX_CLASS_NAV, 0, "UBX_NAV_TIMELS"}, - {UBLOX_CFG_MSGOUT_UBX_NAV_TIMEUTC_UART1, UBX_NAV_TIMEUTC, UBX_CLASS_NAV, 0, "UBX_NAV_TIMEUTC"}, - {UBLOX_CFG_MSGOUT_UBX_NAV_VELECEF_UART1, UBX_NAV_VELECEF, UBX_CLASS_NAV, 0, "UBX_NAV_VELECEF"}, - {UBLOX_CFG_MSGOUT_UBX_NAV_VELNED_UART1, UBX_NAV_VELNED, UBX_CLASS_NAV, 0, "UBX_NAV_VELNED"}, - - // uint8_t nav_aopstatus = 0; //Not supported by library or ZED-F9P - // uint8_t nav_att = 0; //Not supported by ZED-F9P - // uint8_t nav_cov = 0; //Not supported by library - // uint8_t nav_resetodo = 0; //Not supported u-center 21.02 - // uint8_t nav_sbas = 0; //Not supported by library - // uint8_t nav_slas = 0; //Not supported by library - // uint8_t nav_timeqzss = 0; //Not supported in library - // uint8_t nav_dgps = 0; //Not supported by ZED-F9P - // uint8_t nav_eell = 0; //Not supported in library - // uint8_t nav_ekfstatus = 0; //Not supported by ZED-F9P - // uint8_t nav_nmi = 0; //Not supported by ZED-F9P or library - // uint8_t nav_sol = 0; //Not supported by ZED-F9P or library - // uint8_t nav_svinfo = 0; //Not supported by ZED-F9P or library - - {UBLOX_CFG_MSGOUT_UBX_RXM_MEASX_UART1, UBX_RXM_MEASX, UBX_CLASS_RXM, 0, "UBX_RXM_MEASX"}, - {UBLOX_CFG_MSGOUT_UBX_RXM_RAWX_UART1, UBX_RXM_RAWX, UBX_CLASS_RXM, 0, "UBX_RXM_RAWX"}, - {UBLOX_CFG_MSGOUT_UBX_RXM_RLM_UART1, UBX_RXM_RLM, UBX_CLASS_RXM, 0, "UBX_RXM_RLM"}, - {UBLOX_CFG_MSGOUT_UBX_RXM_RTCM_UART1, UBX_RXM_RTCM, UBX_CLASS_RXM, 0, "UBX_RXM_RTCM"}, - {UBLOX_CFG_MSGOUT_UBX_RXM_SFRBX_UART1, UBX_RXM_SFRBX, UBX_CLASS_RXM, 0, "UBX_RXM_SFRBX"}, - -// uint8_t rxm_alm = 0; //Not supported by library or ZED-F9P -// uint8_t rxm_eph = 0; //Not supported by library or ZED-F9P -// uint8_t rxm_imes = 0; //Not supported by library or ZED-F9P -// uint8_t rxm_pmp = 0; //Not supported by library or ZED-F9P -// uint8_t rxm_raw = 0; //Not supported by library or ZED-F9P -// uint8_t rxm_sfrb = 0; //Not supported by library or ZED-F9P -// uint8_t rxm_spartn = 0; //Not supported by library or ZED-F9P -// uint8_t rxm_svsi = 0; //Not supported by library or ZED-F9P -// uint8_t rxm_tm = 0; //Not supported by library or ZED-F9P -// uint8_t rxm_pmreq = 0; //Not supported by u-center - -// uint8_t hnr_att = 0; //Not supported by ZED-F9P -// uint8_t hnr_ins = 0; //Not supported by ZED-F9P -// uint8_t hnr_pvt = 0; //Not supported by ZED-F9P - - {UBLOX_CFG_MSGOUT_UBX_MON_COMMS_UART1, UBX_MON_COMMS, UBX_CLASS_MON, 0, "UBX_MON_COMMS"}, - {UBLOX_CFG_MSGOUT_UBX_MON_HW2_UART1, UBX_MON_HW2, UBX_CLASS_MON, 0, "UBX_MON_HW2"}, - {UBLOX_CFG_MSGOUT_UBX_MON_HW3_UART1, UBX_MON_HW3, UBX_CLASS_MON, 0, "UBX_MON_HW3"}, - {UBLOX_CFG_MSGOUT_UBX_MON_HW_UART1, UBX_MON_HW, UBX_CLASS_MON, 0, "UBX_MON_HW"}, - {UBLOX_CFG_MSGOUT_UBX_MON_IO_UART1, UBX_MON_IO, UBX_CLASS_MON, 0, "UBX_MON_IO"}, - - {UBLOX_CFG_MSGOUT_UBX_MON_MSGPP_UART1, UBX_MON_MSGPP, UBX_CLASS_MON, 0, "UBX_MON_MSGPP"}, - {UBLOX_CFG_MSGOUT_UBX_MON_RF_UART1, UBX_MON_RF, UBX_CLASS_MON, 0, "UBX_MON_RF"}, - {UBLOX_CFG_MSGOUT_UBX_MON_RXBUF_UART1, UBX_MON_RXBUF, UBX_CLASS_MON, 0, "UBX_MON_RXBUF"}, - {UBLOX_CFG_MSGOUT_UBX_MON_RXR_UART1, UBX_MON_RXR, UBX_CLASS_MON, 0, "UBX_MON_RXR"}, - {UBLOX_CFG_MSGOUT_UBX_MON_TXBUF_UART1, UBX_MON_TXBUF, UBX_CLASS_MON, 0, "UBX_MON_TXBUF"}, - -// uint8_t mon_gnss = 0; //Not supported by u-center -// uint8_t mon_patch = 0; //Not supported by u-center - //uint8_t mon_smgr = 0; //Not supported by library or ZED-F9P - //uint8_t mon_span = 0; //Not supported by library -// uint8_t mon_ver = 0; //Not supported by u-center - - {UBLOX_CFG_MSGOUT_UBX_TIM_TM2_UART1, UBX_TIM_TM2, UBX_CLASS_TIM, 0, "UBX_TIM_TM2"}, - {UBLOX_CFG_MSGOUT_UBX_TIM_TP_UART1, UBX_TIM_TP, UBX_CLASS_TIM, 0, "UBX_TIM_TP"}, - {UBLOX_CFG_MSGOUT_UBX_TIM_VRFY_UART1, UBX_TIM_VRFY, UBX_CLASS_TIM, 0, "UBX_TIM_VRFY"}, - -// uint8_t tim_dosc = 0; //Not supported by library or ZED-F9P -// uint8_t tim_fchg = 0; //Not supported by library or ZED-F9P -// uint8_t tim_smeas = 0; //Not supported by library or ZED-F9P -// uint8_t tim_svin = 0; //Not supported by library or ZED-F9P -// uint8_t tim_tos = 0; //Not supported by library or ZED-F9P -// uint8_t tim_vcocal = 0; //Not supported by library or ZED-F9P - -// uint8_t esf_alg = 0; //Not supported by ZED-F9P -// uint8_t esf_ins = 0; //Not supported by ZED-F9P -// uint8_t esf_meas = 0; //Not supported by ZED-F9P -// uint8_t esf_raw = 0; //Not supported by ZED-F9P -// uint8_t esf_status = 0; //Not supported by ZED-F9P - //uint8_t esf_resetalg = 0; //Not supported by u-center - - {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE1005_UART1, UBX_RTCM_1005, UBX_RTCM_MSB, 0, "UBX_RTCM_1005"}, - {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE1074_UART1, UBX_RTCM_1074, UBX_RTCM_MSB, 0, "UBX_RTCM_1074"}, - {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE1077_UART1, UBX_RTCM_1077, UBX_RTCM_MSB, 0, "UBX_RTCM_1077"}, - {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE1084_UART1, UBX_RTCM_1084, UBX_RTCM_MSB, 0, "UBX_RTCM_1084"}, - {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE1087_UART1, UBX_RTCM_1087, UBX_RTCM_MSB, 0, "UBX_RTCM_1087"}, - - {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE1094_UART1, UBX_RTCM_1094, UBX_RTCM_MSB, 0, "UBX_RTCM_1094"}, - {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE1097_UART1, UBX_RTCM_1097, UBX_RTCM_MSB, 0, "UBX_RTCM_1097"}, - {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE1124_UART1, UBX_RTCM_1124, UBX_RTCM_MSB, 0, "UBX_RTCM_1124"}, - {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE1127_UART1, UBX_RTCM_1127, UBX_RTCM_MSB, 0, "UBX_RTCM_1127"}, - {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE1230_UART1, UBX_RTCM_1230, UBX_RTCM_MSB, 0, "UBX_RTCM_1230"}, - - {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE4072_0_UART1, UBX_RTCM_4072_0, UBX_RTCM_MSB, 0, "UBX_RTCM_4072_0"}, - {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE4072_1_UART1, UBX_RTCM_4072_1, UBX_RTCM_MSB, 0, "UBX_RTCM_4072_1"} +// Static array containing all the compatible messages +const ubxMsg ubxMessages[] = { + // NMEA + {UBLOX_CFG_MSGOUT_NMEA_ID_DTM_UART1, UBX_NMEA_DTM, UBX_CLASS_NMEA, 0, "UBX_NMEA_DTM", SFE_UBLOX_FILTER_NMEA_DTM, + 112, 120}, + {UBLOX_CFG_MSGOUT_NMEA_ID_GBS_UART1, UBX_NMEA_GBS, UBX_CLASS_NMEA, 0, "UBX_NMEA_GBS", SFE_UBLOX_FILTER_NMEA_GBS, + 112, 120}, + {UBLOX_CFG_MSGOUT_NMEA_ID_GGA_UART1, UBX_NMEA_GGA, UBX_CLASS_NMEA, 1, "UBX_NMEA_GGA", SFE_UBLOX_FILTER_NMEA_GGA, + 112, 120}, + {UBLOX_CFG_MSGOUT_NMEA_ID_GLL_UART1, UBX_NMEA_GLL, UBX_CLASS_NMEA, 0, "UBX_NMEA_GLL", SFE_UBLOX_FILTER_NMEA_GLL, + 112, 120}, + {UBLOX_CFG_MSGOUT_NMEA_ID_GNS_UART1, UBX_NMEA_GNS, UBX_CLASS_NMEA, 0, "UBX_NMEA_GNS", SFE_UBLOX_FILTER_NMEA_GNS, + 112, 120}, + + {UBLOX_CFG_MSGOUT_NMEA_ID_GRS_UART1, UBX_NMEA_GRS, UBX_CLASS_NMEA, 0, "UBX_NMEA_GRS", SFE_UBLOX_FILTER_NMEA_GRS, + 112, 120}, + {UBLOX_CFG_MSGOUT_NMEA_ID_GSA_UART1, UBX_NMEA_GSA, UBX_CLASS_NMEA, 1, "UBX_NMEA_GSA", SFE_UBLOX_FILTER_NMEA_GSA, + 112, 120}, + {UBLOX_CFG_MSGOUT_NMEA_ID_GST_UART1, UBX_NMEA_GST, UBX_CLASS_NMEA, 1, "UBX_NMEA_GST", SFE_UBLOX_FILTER_NMEA_GST, + 112, 120}, + {UBLOX_CFG_MSGOUT_NMEA_ID_GSV_UART1, UBX_NMEA_GSV, UBX_CLASS_NMEA, 4, "UBX_NMEA_GSV", SFE_UBLOX_FILTER_NMEA_GSV, + 112, 120}, // Default to 1 update every 4 fixes + {UBLOX_CFG_MSGOUT_NMEA_ID_RLM_UART1, UBX_NMEA_RLM, UBX_CLASS_NMEA, 0, "UBX_NMEA_RLM", SFE_UBLOX_FILTER_NMEA_RLM, + 113, 120}, // No F9P 112 support + + {UBLOX_CFG_MSGOUT_NMEA_ID_RMC_UART1, UBX_NMEA_RMC, UBX_CLASS_NMEA, 1, "UBX_NMEA_RMC", SFE_UBLOX_FILTER_NMEA_RMC, + 112, 120}, + {UBLOX_CFG_MSGOUT_NMEA_ID_THS_UART1, UBX_ID_NOT_AVAILABLE, UBX_CLASS_NMEA, 0, "UBX_NMEA_THS", + SFE_UBLOX_FILTER_NMEA_THS, 9999, 120}, // Not supported F9P 112, 113, 120, 130, 132 + {UBLOX_CFG_MSGOUT_NMEA_ID_VLW_UART1, UBX_NMEA_VLW, UBX_CLASS_NMEA, 0, "UBX_NMEA_VLW", SFE_UBLOX_FILTER_NMEA_VLW, + 112, 120}, + {UBLOX_CFG_MSGOUT_NMEA_ID_VTG_UART1, UBX_NMEA_VTG, UBX_CLASS_NMEA, 0, "UBX_NMEA_VTG", SFE_UBLOX_FILTER_NMEA_VTG, + 112, 120}, + {UBLOX_CFG_MSGOUT_NMEA_ID_ZDA_UART1, UBX_NMEA_ZDA, UBX_CLASS_NMEA, 0, "UBX_NMEA_ZDA", SFE_UBLOX_FILTER_NMEA_ZDA, + 112, 120}, + + // NMEA NAV2 + // F9P not supported 112, 113, 120. Supported starting 130. + // F9R not supported 120. Supported starting 130. + {UBLOX_CFG_MSGOUT_NMEA_NAV2_ID_GGA_UART1, UBX_NMEA_GGA, UBX_CLASS_NMEA, 0, "UBX_NMEANAV2_GGA", + SFE_UBLOX_FILTER_NMEA_GGA, 130, 130}, + {UBLOX_CFG_MSGOUT_NMEA_NAV2_ID_GLL_UART1, UBX_NMEA_GLL, UBX_CLASS_NMEA, 0, "UBX_NMEANAV2_GLL", + SFE_UBLOX_FILTER_NMEA_GLL, 130, 130}, + {UBLOX_CFG_MSGOUT_NMEA_NAV2_ID_GNS_UART1, UBX_NMEA_GNS, UBX_CLASS_NMEA, 0, "UBX_NMEANAV2_GNS", + SFE_UBLOX_FILTER_NMEA_GNS, 130, 130}, + {UBLOX_CFG_MSGOUT_NMEA_NAV2_ID_GSA_UART1, UBX_NMEA_GSA, UBX_CLASS_NMEA, 0, "UBX_NMEANAV2_GSA", + SFE_UBLOX_FILTER_NMEA_GSA, 130, 130}, + {UBLOX_CFG_MSGOUT_NMEA_NAV2_ID_RMC_UART1, UBX_NMEA_RMC, UBX_CLASS_NMEA, 0, "UBX_NMEANAV2_RMC", + SFE_UBLOX_FILTER_NMEA_RMC, 130, 130}, + + {UBLOX_CFG_MSGOUT_NMEA_NAV2_ID_VTG_UART1, UBX_NMEA_VTG, UBX_CLASS_NMEA, 0, "UBX_NMEANAV2_VTG", + SFE_UBLOX_FILTER_NMEA_VTG, 130, 130}, + {UBLOX_CFG_MSGOUT_NMEA_NAV2_ID_ZDA_UART1, UBX_NMEA_ZDA, UBX_CLASS_NMEA, 0, "UBX_NMEANAV2_ZDA", + SFE_UBLOX_FILTER_NMEA_ZDA, 130, 130}, + + // PUBX + // F9P support 130 + {UBLOX_CFG_MSGOUT_PUBX_ID_POLYP_UART1, UBX_ID_NOT_AVAILABLE, UBX_CLASS_PUBX, 0, "UBX_PUBX_POLYP", 0, 112, 120}, + {UBLOX_CFG_MSGOUT_PUBX_ID_POLYS_UART1, UBX_ID_NOT_AVAILABLE, UBX_CLASS_PUBX, 0, "UBX_PUBX_POLYS", 0, 112, 120}, + {UBLOX_CFG_MSGOUT_PUBX_ID_POLYT_UART1, UBX_ID_NOT_AVAILABLE, UBX_CLASS_PUBX, 0, "UBX_PUBX_POLYT", 0, 112, 120}, + + // RTCM + {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE1005_UART1, UBX_RTCM_1005, UBX_RTCM_MSB, 1, "UBX_RTCM_1005", + SFE_UBLOX_FILTER_RTCM_TYPE1005, 112, 9999}, // Not supported on F9R + {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE1074_UART1, UBX_RTCM_1074, UBX_RTCM_MSB, 1, "UBX_RTCM_1074", + SFE_UBLOX_FILTER_RTCM_TYPE1074, 112, 9999}, + {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE1077_UART1, UBX_RTCM_1077, UBX_RTCM_MSB, 0, "UBX_RTCM_1077", + SFE_UBLOX_FILTER_RTCM_TYPE1077, 112, 9999}, + {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE1084_UART1, UBX_RTCM_1084, UBX_RTCM_MSB, 1, "UBX_RTCM_1084", + SFE_UBLOX_FILTER_RTCM_TYPE1084, 112, 9999}, + {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE1087_UART1, UBX_RTCM_1087, UBX_RTCM_MSB, 0, "UBX_RTCM_1087", + SFE_UBLOX_FILTER_RTCM_TYPE1087, 112, 9999}, + + {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE1094_UART1, UBX_RTCM_1094, UBX_RTCM_MSB, 1, "UBX_RTCM_1094", + SFE_UBLOX_FILTER_RTCM_TYPE1094, 112, 9999}, + {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE1097_UART1, UBX_RTCM_1097, UBX_RTCM_MSB, 0, "UBX_RTCM_1097", + SFE_UBLOX_FILTER_RTCM_TYPE1097, 112, 9999}, + {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE1124_UART1, UBX_RTCM_1124, UBX_RTCM_MSB, 1, "UBX_RTCM_1124", + SFE_UBLOX_FILTER_RTCM_TYPE1124, 112, 9999}, + {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE1127_UART1, UBX_RTCM_1127, UBX_RTCM_MSB, 0, "UBX_RTCM_1127", + SFE_UBLOX_FILTER_RTCM_TYPE1127, 112, 9999}, + {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE1230_UART1, UBX_RTCM_1230, UBX_RTCM_MSB, 10, "UBX_RTCM_1230", + SFE_UBLOX_FILTER_RTCM_TYPE1230, 112, 9999}, + + {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE4072_0_UART1, UBX_RTCM_4072_0, UBX_RTCM_MSB, 0, "UBX_RTCM_4072_0", + SFE_UBLOX_FILTER_RTCM_TYPE4072_0, 112, 9999}, + {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE4072_1_UART1, UBX_RTCM_4072_1, UBX_RTCM_MSB, 0, "UBX_RTCM_4072_1", + SFE_UBLOX_FILTER_RTCM_TYPE4072_1, 112, 9999}, + + // AID + + // ESF + {UBLOX_CFG_MSGOUT_UBX_ESF_ALG_UART1, UBX_ESF_ALG, UBX_CLASS_ESF, 0, "UBX_ESF_ALG", 0, 9999, + 120}, // Not supported on F9P + {UBLOX_CFG_MSGOUT_UBX_ESF_INS_UART1, UBX_ESF_INS, UBX_CLASS_ESF, 0, "UBX_ESF_INS", 0, 9999, 120}, + {UBLOX_CFG_MSGOUT_UBX_ESF_MEAS_UART1, UBX_ESF_MEAS, UBX_CLASS_ESF, 0, "UBX_ESF_MEAS", 0, 9999, 120}, + {UBLOX_CFG_MSGOUT_UBX_ESF_RAW_UART1, UBX_ESF_RAW, UBX_CLASS_ESF, 0, "UBX_ESF_RAW", 0, 9999, 120}, + {UBLOX_CFG_MSGOUT_UBX_ESF_STATUS_UART1, UBX_ESF_STATUS, UBX_CLASS_ESF, 0, "UBX_ESF_STATUS", 0, 9999, 120}, + + // HNR + + // LOG + // F9P supports LOG_INFO at 112 + + // MON + {UBLOX_CFG_MSGOUT_UBX_MON_COMMS_UART1, UBX_MON_COMMS, UBX_CLASS_MON, 0, "UBX_MON_COMMS", 0, 112, 120}, + {UBLOX_CFG_MSGOUT_UBX_MON_HW2_UART1, UBX_MON_HW2, UBX_CLASS_MON, 0, "UBX_MON_HW2", 0, 112, 120}, + {UBLOX_CFG_MSGOUT_UBX_MON_HW3_UART1, UBX_MON_HW3, UBX_CLASS_MON, 0, "UBX_MON_HW3", 0, 112, 120}, + {UBLOX_CFG_MSGOUT_UBX_MON_HW_UART1, UBX_MON_HW, UBX_CLASS_MON, 0, "UBX_MON_HW", 0, 112, 120}, + {UBLOX_CFG_MSGOUT_UBX_MON_IO_UART1, UBX_MON_IO, UBX_CLASS_MON, 0, "UBX_MON_IO", 0, 112, 120}, + + {UBLOX_CFG_MSGOUT_UBX_MON_MSGPP_UART1, UBX_MON_MSGPP, UBX_CLASS_MON, 0, "UBX_MON_MSGPP", 0, 112, 120}, + {UBLOX_CFG_MSGOUT_UBX_MON_RF_UART1, UBX_MON_RF, UBX_CLASS_MON, 0, "UBX_MON_RF", 0, 112, 120}, + {UBLOX_CFG_MSGOUT_UBX_MON_RXBUF_UART1, UBX_MON_RXBUF, UBX_CLASS_MON, 0, "UBX_MON_RXBUF", 0, 112, 120}, + {UBLOX_CFG_MSGOUT_UBX_MON_RXR_UART1, UBX_MON_RXR, UBX_CLASS_MON, 0, "UBX_MON_RXR", 0, 112, 120}, + {UBLOX_CFG_MSGOUT_UBX_MON_SPAN_UART1, UBX_MON_SPAN, UBX_CLASS_MON, 0, "UBX_MON_SPAN", 0, 113, + 120}, // Not supported F9P 112 + + {UBLOX_CFG_MSGOUT_UBX_MON_SYS_UART1, UBX_MON_SYS, UBX_CLASS_MON, 0, "UBX_MON_SYS", 0, 9999, + 130}, // Not supported F9R 121, F9P 112, 113, 120, 130, 132 + {UBLOX_CFG_MSGOUT_UBX_MON_TXBUF_UART1, UBX_MON_TXBUF, UBX_CLASS_MON, 0, "UBX_MON_TXBUF", 0, 112, 120}, + + // NAV2 + // F9P not supported 112, 113, 120. Supported starting 130. F9P 130, 132 supports all but not EELL, PVAT, TIMENAVIC + // F9R not supported 120. Supported starting 130. F9R 130 supports EELL, PVAT but not SVIN, TIMENAVIC. + {UBLOX_CFG_MSGOUT_UBX_NAV2_CLOCK_UART1, UBX_ID_NOT_AVAILABLE, UBX_CLASS_NAV, 0, "UBX_NAV2_CLOCK", 0, 130, 130}, + {UBLOX_CFG_MSGOUT_UBX_NAV2_COV_UART1, UBX_ID_NOT_AVAILABLE, UBX_CLASS_NAV, 0, "UBX_NAV2_COV", 0, 130, 130}, + {UBLOX_CFG_MSGOUT_UBX_NAV2_DOP_UART1, UBX_ID_NOT_AVAILABLE, UBX_CLASS_NAV, 0, "UBX_NAV2_DOP", 0, 130, 130}, + {UBLOX_CFG_MSGOUT_UBX_NAV2_EELL_UART1, UBX_ID_NOT_AVAILABLE, UBX_CLASS_NAV, 0, "UBX_NAV2_EELL", 0, 9999, + 130}, // Not supported F9P + {UBLOX_CFG_MSGOUT_UBX_NAV2_EOE_UART1, UBX_ID_NOT_AVAILABLE, UBX_CLASS_NAV, 0, "UBX_NAV2_EOE", 0, 130, 130}, + + {UBLOX_CFG_MSGOUT_UBX_NAV2_ODO_UART1, UBX_ID_NOT_AVAILABLE, UBX_CLASS_NAV, 0, "UBX_NAV2_ODO", 0, 130, 130}, + {UBLOX_CFG_MSGOUT_UBX_NAV2_POSECEF_UART1, UBX_ID_NOT_AVAILABLE, UBX_CLASS_NAV, 0, "UBX_NAV2_POSECEF", 0, 130, 130}, + {UBLOX_CFG_MSGOUT_UBX_NAV2_POSLLH_UART1, UBX_ID_NOT_AVAILABLE, UBX_CLASS_NAV, 0, "UBX_NAV2_POSLLH", 0, 130, 130}, + {UBLOX_CFG_MSGOUT_UBX_NAV2_PVAT_UART1, UBX_ID_NOT_AVAILABLE, UBX_CLASS_NAV, 0, "UBX_NAV2_PVAT", 0, 9999, + 130}, // Not supported F9P + {UBLOX_CFG_MSGOUT_UBX_NAV2_PVT_UART1, UBX_ID_NOT_AVAILABLE, UBX_CLASS_NAV, 0, "UBX_NAV2_PVT", 0, 130, 130}, + + {UBLOX_CFG_MSGOUT_UBX_NAV2_SAT_UART1, UBX_ID_NOT_AVAILABLE, UBX_CLASS_NAV, 0, "UBX_NAV2_SAT", 0, 130, 130}, + {UBLOX_CFG_MSGOUT_UBX_NAV2_SBAS_UART1, UBX_ID_NOT_AVAILABLE, UBX_CLASS_NAV, 0, "UBX_NAV2_SBAS", 0, 130, 130}, + {UBLOX_CFG_MSGOUT_UBX_NAV2_SIG_UART1, UBX_ID_NOT_AVAILABLE, UBX_CLASS_NAV, 0, "UBX_NAV2_SIG", 0, 130, 130}, + {UBLOX_CFG_MSGOUT_UBX_NAV2_SLAS_UART1, UBX_ID_NOT_AVAILABLE, UBX_CLASS_NAV, 0, "UBX_NAV2_SLAS", 0, 130, 130}, + {UBLOX_CFG_MSGOUT_UBX_NAV2_STATUS_UART1, UBX_ID_NOT_AVAILABLE, UBX_CLASS_NAV, 0, "UBX_NAV2_STATUS", 0, 130, 130}, + + //{UBLOX_CFG_MSGOUT_UBX_NAV2_SVIN_UART1, UBX_ID_NOT_AVAILABLE, UBX_CLASS_NAV, 0, "UBX_NAV2_SVIN", 0, 9999, 9999}, + ////No support yet + {UBLOX_CFG_MSGOUT_UBX_NAV2_TIMEBDS_UART1, UBX_ID_NOT_AVAILABLE, UBX_CLASS_NAV, 0, "UBX_NAV2_TIMEBDS", 0, 130, 130}, + {UBLOX_CFG_MSGOUT_UBX_NAV2_TIMEGAL_UART1, UBX_ID_NOT_AVAILABLE, UBX_CLASS_NAV, 0, "UBX_NAV2_TIMEGAL", 0, 130, 130}, + {UBLOX_CFG_MSGOUT_UBX_NAV2_TIMEGLO_UART1, UBX_ID_NOT_AVAILABLE, UBX_CLASS_NAV, 0, "UBX_NAV2_TIMEGLO", 0, 130, 130}, + {UBLOX_CFG_MSGOUT_UBX_NAV2_TIMEGPS_UART1, UBX_ID_NOT_AVAILABLE, UBX_CLASS_NAV, 0, "UBX_NAV2_TIMEGPS", 0, 130, 130}, + + {UBLOX_CFG_MSGOUT_UBX_NAV2_TIMELS_UART1, UBX_ID_NOT_AVAILABLE, UBX_CLASS_NAV, 0, "UBX_NAV2_TIMELS", 0, 130, 130}, + //{UBLOX_CFG_MSGOUT_UBX_NAV2_TIMENAVIC_UART1, UBX_ID_NOT_AVAILABLE, UBX_CLASS_NAV, 0, "UBX_NAV2_TIMENAVIC", 0, 9999, + // 9999}, //No support yet + {UBLOX_CFG_MSGOUT_UBX_NAV2_TIMEQZSS_UART1, UBX_ID_NOT_AVAILABLE, UBX_CLASS_NAV, 0, "UBX_NAV2_TIMEQZSS", 0, 130, + 130}, + {UBLOX_CFG_MSGOUT_UBX_NAV2_TIMEUTC_UART1, UBX_ID_NOT_AVAILABLE, UBX_CLASS_NAV, 0, "UBX_NAV2_TIMEUTC", 0, 130, 130}, + {UBLOX_CFG_MSGOUT_UBX_NAV2_VELECEF_UART1, UBX_ID_NOT_AVAILABLE, UBX_CLASS_NAV, 0, "UBX_NAV2_VELECEF", 0, 130, 130}, + + {UBLOX_CFG_MSGOUT_UBX_NAV2_VELNED_UART1, UBX_ID_NOT_AVAILABLE, UBX_CLASS_NAV, 0, "UBX_NAV2_VELNED", 0, 130, 130}, + + // NAV + //{UBLOX_CFG_MSGOUT_UBX_NAV_AOPSTATUS_UART1, UBX_NAV_AOPSTATUS, UBX_CLASS_NAV, 0, "UBX_NAV_AOPSTATUS", 0, 9999, + // 9999}, //Not supported on F9R 121 or F9P 112, 113, 120, 130, 132 + {UBLOX_CFG_MSGOUT_UBX_NAV_ATT_UART1, UBX_NAV_ATT, UBX_CLASS_NAV, 0, "UBX_NAV_ATT", 0, 9999, + 120}, // Not supported on F9P 112, 113, 120, 130, 132 + {UBLOX_CFG_MSGOUT_UBX_NAV_CLOCK_UART1, UBX_NAV_CLOCK, UBX_CLASS_NAV, 0, "UBX_NAV_CLOCK", 0, 112, 120}, + {UBLOX_CFG_MSGOUT_UBX_NAV_COV_UART1, UBX_NAV_COV, UBX_CLASS_NAV, 0, "UBX_NAV_COV", 0, 112, 120}, + //{UBLOX_CFG_MSGOUT_UBX_NAV_DGPS_UART1, UBX_NAV_DGPS, UBX_CLASS_NAV, 0, "UBX_NAV_DGPS", 0, 9999, 9999}, //Not + // supported on F9R 121 or F9P 112, 113, 120, 130, 132 + {UBLOX_CFG_MSGOUT_UBX_NAV_DOP_UART1, UBX_NAV_DOP, UBX_CLASS_NAV, 0, "UBX_NAV_DOP", 0, 112, 120}, + + {UBLOX_CFG_MSGOUT_UBX_NAV_EELL_UART1, UBX_NAV_EELL, UBX_CLASS_NAV, 0, "UBX_NAV_EELL", 0, 9999, + 120}, // Not supported on F9P + {UBLOX_CFG_MSGOUT_UBX_NAV_EOE_UART1, UBX_NAV_EOE, UBX_CLASS_NAV, 0, "UBX_NAV_EOE", 0, 112, 120}, + {UBLOX_CFG_MSGOUT_UBX_NAV_GEOFENCE_UART1, UBX_NAV_GEOFENCE, UBX_CLASS_NAV, 0, "UBX_NAV_GEOFENCE", 0, 112, 120}, + {UBLOX_CFG_MSGOUT_UBX_NAV_HPPOSECEF_UART1, UBX_NAV_HPPOSECEF, UBX_CLASS_NAV, 0, "UBX_NAV_HPPOSECEF", 0, 112, 120}, + {UBLOX_CFG_MSGOUT_UBX_NAV_HPPOSLLH_UART1, UBX_NAV_HPPOSLLH, UBX_CLASS_NAV, 0, "UBX_NAV_HPPOSLLH", 0, 112, 120}, + + //{UBLOX_CFG_MSGOUT_UBX_NAV_NMI_UART1, UBX_NAV_NMI, UBX_CLASS_NAV, 0, "UBX_NAV_NMI", 0, 9999, 9999}, //Not supported + // on F9R 121 or F9P 112, 113, 120, 130, 132 + {UBLOX_CFG_MSGOUT_UBX_NAV_ODO_UART1, UBX_NAV_ODO, UBX_CLASS_NAV, 0, "UBX_NAV_ODO", 0, 112, 120}, + {UBLOX_CFG_MSGOUT_UBX_NAV_ORB_UART1, UBX_NAV_ORB, UBX_CLASS_NAV, 0, "UBX_NAV_ORB", 0, 112, 120}, + {UBLOX_CFG_MSGOUT_UBX_NAV_PL_UART1, UBX_NAV_PL, UBX_CLASS_NAV, 0, "UBX_NAV_PL", 0, 9999, + 130}, // Not supported F9R 121 or F9P 112, 113, 120, 130, 132 + {UBLOX_CFG_MSGOUT_UBX_NAV_POSECEF_UART1, UBX_NAV_POSECEF, UBX_CLASS_NAV, 0, "UBX_NAV_POSECEF", 0, 112, 120}, + + {UBLOX_CFG_MSGOUT_UBX_NAV_POSLLH_UART1, UBX_NAV_POSLLH, UBX_CLASS_NAV, 0, "UBX_NAV_POSLLH", 0, 112, 120}, + {UBLOX_CFG_MSGOUT_UBX_NAV_PVAT_UART1, UBX_NAV_PVAT, UBX_CLASS_NAV, 0, "UBX_NAV_PVAT", 0, 9999, + 121}, // Not supported on F9P 112, 113, 120, 130, F9R 120 + {UBLOX_CFG_MSGOUT_UBX_NAV_PVT_UART1, UBX_NAV_PVT, UBX_CLASS_NAV, 0, "UBX_NAV_PVT", 0, 112, 120}, + {UBLOX_CFG_MSGOUT_UBX_NAV_RELPOSNED_UART1, UBX_NAV_RELPOSNED, UBX_CLASS_NAV, 0, "UBX_NAV_RELPOSNED", 0, 112, 120}, + {UBLOX_CFG_MSGOUT_UBX_NAV_SAT_UART1, UBX_NAV_SAT, UBX_CLASS_NAV, 0, "UBX_NAV_SAT", 0, 112, 120}, + + {UBLOX_CFG_MSGOUT_UBX_NAV_SBAS_UART1, UBX_NAV_SBAS, UBX_CLASS_NAV, 0, "UBX_NAV_SBAS", 0, 113, + 120}, // Not supported F9P 112 + {UBLOX_CFG_MSGOUT_UBX_NAV_SIG_UART1, UBX_NAV_SIG, UBX_CLASS_NAV, 0, "UBX_NAV_SIG", 0, 112, 120}, + {UBLOX_CFG_MSGOUT_UBX_NAV_SLAS_UART1, UBX_NAV_SLAS, UBX_CLASS_NAV, 0, "UBX_NAV_SLAS", 0, 113, + 130}, // Not supported F9R 121 or F9P 112 + //{UBLOX_CFG_MSGOUT_UBX_NAV_SOL_UART1, UBX_NAV_SOL, UBX_CLASS_NAV, 0, "UBX_NAV_SOL", 0, 9999, 9999}, //Not + // supported F9R 121 or F9P 112, 113, 120, 130, 132 + {UBLOX_CFG_MSGOUT_UBX_NAV_STATUS_UART1, UBX_NAV_STATUS, UBX_CLASS_NAV, 0, "UBX_NAV_STATUS", 0, 112, 120}, + //{UBLOX_CFG_MSGOUT_UBX_NAV_SVINFO_UART1, UBX_NAV_SVINFO, UBX_CLASS_NAV, 0, "UBX_NAV_SVINFO", 0, 9999, 9999}, //Not + // supported F9R 120 or F9P 112, 113, 120, 130, 132 + {UBLOX_CFG_MSGOUT_UBX_NAV_SVIN_UART1, UBX_NAV_SVIN, UBX_CLASS_NAV, 0, "UBX_NAV_SVIN", 0, 112, + 9999}, // Not supported on F9R 120, 121, 130 + {UBLOX_CFG_MSGOUT_UBX_NAV_TIMEBDS_UART1, UBX_NAV_TIMEBDS, UBX_CLASS_NAV, 0, "UBX_NAV_TIMEBDS", 0, 112, 120}, + {UBLOX_CFG_MSGOUT_UBX_NAV_TIMEGAL_UART1, UBX_NAV_TIMEGAL, UBX_CLASS_NAV, 0, "UBX_NAV_TIMEGAL", 0, 112, 120}, + {UBLOX_CFG_MSGOUT_UBX_NAV_TIMEGLO_UART1, UBX_NAV_TIMEGLO, UBX_CLASS_NAV, 0, "UBX_NAV_TIMEGLO", 0, 112, 120}, + + {UBLOX_CFG_MSGOUT_UBX_NAV_TIMEGPS_UART1, UBX_NAV_TIMEGPS, UBX_CLASS_NAV, 0, "UBX_NAV_TIMEGPS", 0, 112, 120}, + {UBLOX_CFG_MSGOUT_UBX_NAV_TIMELS_UART1, UBX_NAV_TIMELS, UBX_CLASS_NAV, 0, "UBX_NAV_TIMELS", 0, 112, 120}, + //{UBLOX_CFG_MSGOUT_UBX_NAV_TIMENAVIC_UART1, UBX_NAV_TIMENAVIC, UBX_CLASS_NAV, 0, "UBX_NAV_TIMENAVIC", 0, 9999, + // 9999}, //Not supported F9R 121 or F9P 132 + {UBLOX_CFG_MSGOUT_UBX_NAV_TIMEQZSS_UART1, UBX_ID_NOT_AVAILABLE, UBX_CLASS_NAV, 0, "UBX_NAV_QZSS", 0, 113, + 130}, // Not supported F9R 121 or F9P 112 + {UBLOX_CFG_MSGOUT_UBX_NAV_TIMEUTC_UART1, UBX_NAV_TIMEUTC, UBX_CLASS_NAV, 0, "UBX_NAV_TIMEUTC", 0, 112, 120}, + {UBLOX_CFG_MSGOUT_UBX_NAV_VELECEF_UART1, UBX_NAV_VELECEF, UBX_CLASS_NAV, 0, "UBX_NAV_VELECEF", 0, 112, 120}, + {UBLOX_CFG_MSGOUT_UBX_NAV_VELNED_UART1, UBX_NAV_VELNED, UBX_CLASS_NAV, 0, "UBX_NAV_VELNED", 0, 112, 120}, + + // RXM + //{UBLOX_CFG_MSGOUT_UBX_RXM_ALM_UART1, UBX_RXM_ALM, UBX_CLASS_RXM, 0, "UBX_RXM_ALM", 0, 9999, 9999}, //Not supported + // F9R 121 or F9P 112, 113, 120, 130, 132 + {UBLOX_CFG_MSGOUT_UBX_RXM_COR_UART1, UBX_RXM_COR, UBX_CLASS_RXM, 0, "UBX_RXM_COR", 0, 9999, + 130}, // Not supported F9R 121 or F9P 112, 113, 120, 130, 132 + //{UBLOX_CFG_MSGOUT_UBX_RXM_EPH_UART1, UBX_RXM_EPH, UBX_CLASS_RXM, 0, "UBX_RXM_EPH", 0, 9999, 9999}, //Not + // supported F9R 121 or F9P 112, 113, 120, 130, 132 {UBLOX_CFG_MSGOUT_UBX_RXM_IMES_UART1, UBX_RXM_IMES, + // UBX_CLASS_RXM, 0, "UBX_RXM_IMES", 0, 9999, 9999}, //Not supported F9R 121 or F9P 112, 113, 120, 130, 132 + //{UBLOX_CFG_MSGOUT_UBX_RXM_MEAS20_UART1, UBX_RXM_MEAS20, UBX_CLASS_RXM, 0, "UBX_RXM_MEAS20", 0, 9999, 9999}, + ////Not supported F9R 121 or F9P 112, 113, 120, 130, 132 {UBLOX_CFG_MSGOUT_UBX_RXM_MEAS50_UART1, + // UBX_RXM_MEAS50, UBX_CLASS_RXM, 0, "UBX_RXM_MEAS50", 0, 9999, 9999}, //Not supported F9R 121 or F9P 112, + // 113, 120, 130, 132 {UBLOX_CFG_MSGOUT_UBX_RXM_MEASC12_UART1, UBX_RXM_MEASC12, UBX_CLASS_RXM, 0, + //"UBX_RXM_MEASC12", 0, 9999, 9999}, //Not supported F9R 121 or F9P 112, 113, 120, 130, 132 + //{UBLOX_CFG_MSGOUT_UBX_RXM_MEASD12_UART1, UBX_RXM_MEASD12, UBX_CLASS_RXM, 0, "UBX_RXM_MEASD12", 0, 9999, + // 9999}, //Not supported F9R 121 or F9P 112, 113, 120, 130, 132 + {UBLOX_CFG_MSGOUT_UBX_RXM_MEASX_UART1, UBX_RXM_MEASX, UBX_CLASS_RXM, 0, "UBX_RXM_MEASX", 0, 112, 120}, + //{UBLOX_CFG_MSGOUT_UBX_RXM_PMP_UART1, UBX_RXM_PMP, UBX_CLASS_RXM, 0, "UBX_RXM_PMP", 0, 9999, 9999}, //Not supported + // F9R 121 or F9P 112, 113, 120, 130, 132 {UBLOX_CFG_MSGOUT_UBX_RXM_QZSSL6_UART1, UBX_RXM_QZSSL6, UBX_CLASS_RXM, 0, + //"UBX_RXM_QZSSL6", 0, 9999, 9999}, //Not supported F9R 121, F9P 112, 113, 120, 130 + {UBLOX_CFG_MSGOUT_UBX_RXM_RAWX_UART1, UBX_RXM_RAWX, UBX_CLASS_RXM, 0, "UBX_RXM_RAWX", 0, 112, 120}, + {UBLOX_CFG_MSGOUT_UBX_RXM_RLM_UART1, UBX_RXM_RLM, UBX_CLASS_RXM, 0, "UBX_RXM_RLM", 0, 112, 120}, + {UBLOX_CFG_MSGOUT_UBX_RXM_RTCM_UART1, UBX_RXM_RTCM, UBX_CLASS_RXM, 0, "UBX_RXM_RTCM", 0, 112, 120}, + {UBLOX_CFG_MSGOUT_UBX_RXM_SFRBX_UART1, UBX_RXM_SFRBX, UBX_CLASS_RXM, 0, "UBX_RXM_SFRBX", 0, 112, 120}, + {UBLOX_CFG_MSGOUT_UBX_RXM_SPARTN_UART1, UBX_RXM_SPARTN, UBX_CLASS_RXM, 0, "UBX_RXM_SPARTN", 0, 9999, + 121}, // Not supported F9R 120 or F9P 112, 113, 120, 130 + //{UBLOX_CFG_MSGOUT_UBX_RXM_SVSI_UART1, UBX_RXM_SVSI, UBX_CLASS_RXM, 0, "UBX_RXM_SVSI", 0, 9999, 9999}, //Not + // supported F9R 121 or F9P 112, 113, 120, 130, 132 {UBLOX_CFG_MSGOUT_UBX_RXM_TM_UART1, UBX_RXM_TM, + // UBX_CLASS_RXM, 0, "UBX_RXM_TM", 0, 9999, 9999}, //Not supported F9R 121 or F9P 112, 113, 120, 130, 132 + + // SEC + // No support F9P 112. + + // TIM + //{UBLOX_CFG_MSGOUT_UBX_TIM_SVIN_UART1, UBX_TIM_SVIN, UBX_CLASS_TIM, 0, "UBX_TIM_SVIN", 0, 9999, 9999}, //Appears on + // F9P 132 but not supported + {UBLOX_CFG_MSGOUT_UBX_TIM_TM2_UART1, UBX_TIM_TM2, UBX_CLASS_TIM, 0, "UBX_TIM_TM2", 0, 112, 120}, + {UBLOX_CFG_MSGOUT_UBX_TIM_TP_UART1, UBX_TIM_TP, UBX_CLASS_TIM, 0, "UBX_TIM_TP", 0, 112, 120}, + {UBLOX_CFG_MSGOUT_UBX_TIM_VRFY_UART1, UBX_TIM_VRFY, UBX_CLASS_TIM, 0, "UBX_TIM_VRFY", 0, 112, 120}, + +}; + +#define MAX_UBX_MSG (sizeof(ubxMessages) / sizeof(ubxMsg)) +#define MAX_UBX_MSG_RTCM (12) + +#define MAX_SET_MESSAGES_RETRIES 5 // Try up to five times to set all the messages. Occasionally fails if set to 2 + +// Struct to describe the necessary info for each UBX command +// Each command will have a key, and minimum F9P/F9R versions that support that command +typedef struct +{ + const uint32_t cmdKey; + const char cmdTextName[30]; + const uint16_t f9pFirmwareVersionSupported; // The minimum version this message is supported. 0 = all versions. 9999 + // = Not supported + const uint16_t f9rFirmwareVersionSupported; +} ubxCmd; + +// Static array containing all the compatible commands +const ubxCmd ubxCommands[] = { + {UBLOX_CFG_TMODE_MODE, "CFG_TMODE_MODE", 0, 9999}, // Survey mode is only available on ZED-F9P modules + + //The F9R is unique WRT RTCM *output*. u-center can correctly enable/disable the RTCM output, but it cannot + //be set with setVal commands. Applies to HPS 120, 121, 130. + + {UBLOX_CFG_UART1OUTPROT_RTCM3X, "CFG_UART1OUTPROT_RTCM3X", 0, 9999}, // F9R: RTCM output not supported + {UBLOX_CFG_UART1INPROT_SPARTN, "CFG_UART1INPROT_SPARTN", 120, + 121}, // Supported on F9P 120 and up. F9R: SPARTN supported starting HPS 121 + + {UBLOX_CFG_UART2OUTPROT_RTCM3X, "CFG_UART2OUTPROT_RTCM3X", 0, 9999}, // F9R: RTCM output not supported + {UBLOX_CFG_UART2INPROT_SPARTN, "CFG_UART2INPROT_SPARTN", 120, 121}, // F9R: SPARTN supported starting HPS 121 + + {UBLOX_CFG_SPIOUTPROT_RTCM3X, "CFG_SPIOUTPROT_RTCM3X", 0, 9999}, // F9R: RTCM output not supported + {UBLOX_CFG_SPIINPROT_SPARTN, "CFG_SPIINPROT_SPARTN", 120, 121}, // F9R: SPARTN supported starting HPS 121 + + {UBLOX_CFG_I2COUTPROT_RTCM3X, "CFG_I2COUTPROT_RTCM3X", 0, 9999}, // F9R: RTCM output not supported + {UBLOX_CFG_I2CINPROT_SPARTN, "CFG_I2CINPROT_SPARTN", 120, 121}, // F9R: SPARTN supported starting HPS 121 + + {UBLOX_CFG_USBOUTPROT_RTCM3X, "CFG_USBOUTPROT_RTCM3X", 0, 9999}, // F9R: RTCM output not supported + {UBLOX_CFG_USBINPROT_SPARTN, "CFG_USBINPROT_SPARTN", 120, 121}, // F9R: SPARTN supported starting HPS 121 + + {UBLOX_CFG_NAV2_OUT_ENABLED, "CFG_NAV2_OUT_ENABLED", 130, + 130}, // Supported on F9P 130 and up. Supported on F9R 130 and up. + {UBLOX_CFG_NAVSPG_INFIL_MINCNO, "CFG_NAVSPG_INFIL_MINCNO", 0, 0}, // +}; + +#define MAX_UBX_CMD (sizeof(ubxCommands) / sizeof(ubxCmd)) + +// Regional Support +// Do we want the user to be able to specify which region they are in? +// Or do we want to figure it out based on position? +// If we define a simple 'square' area for each region, we can do both. +// Note: the best way to obtain the L-Band frequencies would be from the MQTT /pp/frequencies/Lb topic. +// But it is easier to record them here, in case we don't have access to MQTT... +// Note: the key distribution topic is provided during ZTP. We don't need to record it here. + +typedef struct +{ + const double latNorth; // Degrees + const double latSouth; // Degrees + const double lonEast; // Degrees + const double lonWest; // Degrees +} Regional_Area; + +typedef struct +{ + const char *name; // As defined in the ZTP subscriptions description: EU, US, KR, AU, Japan + const char *topicRegion; // As used in the corrections topic path + const Regional_Area area; + const uint32_t frequency; // L-Band frequency, Hz, if supported. 0 if not supported +} Regional_Information; + +const Regional_Information Regional_Information_Table[] = +{ + { "US", "us", { 50.0, 25.0, -60.0, -125.0}, 1556290000 }, + { "EU", "eu", { 72.0, 36.0, 32.0, -11.0}, 1545260000 }, + // Note: we only include regions with L-Band coverage. AU, KR and Japan are not included here. }; +const int numRegionalAreas = sizeof(Regional_Information_Table) / sizeof(Regional_Information_Table[0]); + +// This is all the settings that can be set on RTK Surveyor. It's recorded to NVM and the config file. +typedef struct +{ + int sizeOfSettings = 0; // sizeOfSettings **must** be the first entry and must be int + int rtkIdentifier = RTK_IDENTIFIER; // rtkIdentifier **must** be the second entry + bool printDebugMessages = false; + bool enableSD = true; + bool enableDisplay = true; + int maxLogTime_minutes = 60 * 24; // Default to 24 hours + int observationSeconds = 60; // Default survey in time of 60 seconds + float observationPositionAccuracy = 5.0; // Default survey in pos accy of 5m + bool fixedBase = false; // Use survey-in by default + bool fixedBaseCoordinateType = COORD_TYPE_ECEF; + double fixedEcefX = -1280206.568; + double fixedEcefY = -4716804.403; + double fixedEcefZ = 4086665.484; + double fixedLat = 40.09029479; + double fixedLong = -105.18505761; + double fixedAltitude = 1560.089; + uint32_t dataPortBaud = + (115200 * 2); // Default to 230400bps. This limits GNSS fixes at 4Hz but allows SD buffer to be reduced to 6k. + uint32_t radioPortBaud = 57600; // Default to 57600bps to support connection to SiK1000 type telemetry radios + float surveyInStartingAccuracy = 1.0; // Wait for 1m horizontal positional accuracy before starting survey in + uint16_t measurementRate = 250; // Elapsed ms between GNSS measurements. 25ms to 65535ms. Default 4Hz. + uint16_t navigationRate = + 1; // Ratio between number of measurements and navigation solutions. Default 1 for 4Hz (with measurementRate). + bool enableI2Cdebug = false; // Turn on to display GNSS library debug messages + bool enableHeapReport = false; // Turn on to display free heap + bool enableTaskReports = false; // Turn on to display task high water marks + muxConnectionType_e dataPortChannel = MUX_UBLOX_NMEA; // Mux default to ublox UART1 + uint16_t spiFrequency = 16; // By default, use 16MHz SPI + bool enableLogging = true; // If an SD card is present, log default sentences + bool enableARPLogging = false; // Log the Antenna Reference Position from RTCM 1005/1006 - if available + uint16_t ARPLoggingInterval_s = 10; // Log the ARP every 10 seconds - if available + uint16_t sppRxQueueSize = 512 * 4; + uint16_t sppTxQueueSize = 32; + uint8_t dynamicModel = DYN_MODEL_PORTABLE; + SystemState lastState = STATE_NOT_SET; // Start unit in last known state + bool enableSensorFusion = false; // If IMU is available, avoid using it unless user specifically selects automotive + bool autoIMUmountAlignment = true; // Allows unit to automatically establish device orientation in vehicle + bool enableResetDisplay = false; + uint8_t resetCount = 0; + bool enableExternalPulse = true; // Send pulse once lock is achieved + uint64_t externalPulseTimeBetweenPulse_us = 1000000; // us between pulses, max of 60s = 60 * 1000 * 1000 + uint64_t externalPulseLength_us = 100000; // us length of pulse, max of 60s = 60 * 1000 * 1000 + pulseEdgeType_e externalPulsePolarity = PULSE_RISING_EDGE; // Pulse rises for pulse length, then falls + bool enableExternalHardwareEventLogging = false; // Log when INT/TM2 pin goes low + bool enableMarksFile = false; // Log marks to the marks file + bool enableUART2UBXIn = false; // UBX Protocol In on UART2 + + uint8_t ubxMessageRates[MAX_UBX_MSG] = {254}; // Mark first record with key so defaults will be applied. + + // Constellations monitored/used for fix + ubxConstellation ubxConstellations[MAX_CONSTELLATIONS] = { + {UBLOX_CFG_SIGNAL_GPS_ENA, SFE_UBLOX_GNSS_ID_GPS, true, "GPS"}, + {UBLOX_CFG_SIGNAL_SBAS_ENA, SFE_UBLOX_GNSS_ID_SBAS, true, "SBAS"}, + {UBLOX_CFG_SIGNAL_GAL_ENA, SFE_UBLOX_GNSS_ID_GALILEO, true, "Galileo"}, + {UBLOX_CFG_SIGNAL_BDS_ENA, SFE_UBLOX_GNSS_ID_BEIDOU, true, "BeiDou"}, + //{UBLOX_CFG_SIGNAL_QZSS_ENA, SFE_UBLOX_GNSS_ID_IMES, false, "IMES"}, //Not yet supported? Config key does not + // exist? + {UBLOX_CFG_SIGNAL_QZSS_ENA, SFE_UBLOX_GNSS_ID_QZSS, true, "QZSS"}, + {UBLOX_CFG_SIGNAL_GLO_ENA, SFE_UBLOX_GNSS_ID_GLONASS, true, "GLONASS"}, + }; + + int maxLogLength_minutes = 60 * 24; // Default to 24 hours + char profileName[50] = ""; + + int16_t serialTimeoutGNSS = 1; // In ms - used during SerialGNSS.begin. Number of ms to pass of no data before + // hardware serial reports data available. + + // Point Perfect + char pointPerfectDeviceProfileToken[40] = ""; + bool enablePointPerfectCorrections = true; + bool autoKeyRenewal = true; // Attempt to get keys if we get under 28 days from the expiration date + char pointPerfectClientID[50] = ""; + char pointPerfectBrokerHost[50] = ""; // pp.services.u-blox.com + char pointPerfectLBandTopic[20] = ""; // /pp/ubx/0236/Lb + + char pointPerfectCurrentKey[33] = ""; // 32 hexadecimal digits = 128 bits = 16 Bytes + uint64_t pointPerfectCurrentKeyDuration = 0; + uint64_t pointPerfectCurrentKeyStart = 0; + + char pointPerfectNextKey[33] = ""; + uint64_t pointPerfectNextKeyDuration = 0; + uint64_t pointPerfectNextKeyStart = 0; + + uint64_t lastKeyAttempt = 0; // Epoch time of last attempt at obtaining keys + bool updateZEDSettings = true; // When in doubt, update the ZED with current settings + + bool debugPpCertificate = false; // Debug Point Perfect certificate management + + // Time Zone - Default to UTC + int8_t timeZoneHours = 0; + int8_t timeZoneMinutes = 0; + int8_t timeZoneSeconds = 0; -//This is all the settings that can be set on RTK Surveyor. It's recorded to NVM and the config file. -struct struct_settings { - int sizeOfSettings = 0; //sizeOfSettings **must** be the first entry and must be int - int rtkIdentifier = RTK_IDENTIFIER; // rtkIdentifier **must** be the second entry - bool printDebugMessages = false; - bool enableSD = true; - bool enableDisplay = true; - int maxLogTime_minutes = 60 * 10; //Default to 10 hours - int observationSeconds = 60; //Default survey in time of 60 seconds - float observationPositionAccuracy = 5.0; //Default survey in pos accy of 5m - bool fixedBase = false; //Use survey-in by default - bool fixedBaseCoordinateType = COORD_TYPE_ECEF; - double fixedEcefX = 0.0; - double fixedEcefY = 0.0; - double fixedEcefZ = 0.0; - double fixedLat = 0.0; - double fixedLong = 0.0; - double fixedAltitude = 0.0; - uint32_t dataPortBaud = 460800; //Default to 460800bps to support >10Hz update rates - uint32_t radioPortBaud = 57600; //Default to 57600bps to support connection to SiK1000 radios - bool enableNtripServer = false; - char casterHost[50] = "rtk2go.com"; //It's free... - uint16_t casterPort = 2101; - char mountPoint[50] = "bldr_dwntwn2"; - char mountPointPW[50] = "WR5wRo4H"; - char wifiSSID[50] = "TRex"; - char wifiPW[50] = "parachutes"; - float surveyInStartingAccuracy = 1.0; //Wait for 1m horizontal positional accuracy before starting survey in - uint16_t measurementRate = 250; //Elapsed ms between GNSS measurements. 25ms to 65535ms. Default 4Hz. - uint16_t navigationRate = 1; //Ratio between number of measurements and navigation solutions. Default 1 for 4Hz (with measurementRate). - ubxMsg ubxMessages; //Report rates for all known messages - bool enableI2Cdebug = false; //Turn on to display GNSS library debug messages - bool enableHeapReport = false; //Turn on to display free heap - bool enableTaskReports = false; //Turn on to display task high water marks - muxConnectionType_e dataPortChannel = MUX_UBLOX_NMEA; //Mux default to ublox UART1 - uint16_t spiFrequency = 8; //By default, use 8MHz SPI - bool enableLogging = true; //If an SD card is present, log default sentences - uint16_t sppRxQueueSize = 2048; - uint16_t sppTxQueueSize = 512; - uint8_t dynamicModel = DYN_MODEL_PORTABLE; - SystemState lastState = STATE_ROVER_NOT_STARTED; //For Express, start unit in last known state - bool throttleDuringSPPCongestion = true; - ubxConstellation ubxConstellations; //Constellations monitored/used for fix - -} settings; - -//These are the devices on board RTK Surveyor that may be on or offline. -struct struct_online { - bool microSD = false; - bool display = false; - bool gnss = false; - bool logging = false; - bool serialOutput = false; - bool eeprom = false; - bool rtc = false; - bool battery = false; - bool accelerometer = false; + // Debug settings + bool enablePrintState = false; + bool enablePrintPosition = false; + bool enablePrintIdleTime = false; + bool enablePrintBatteryMessages = true; + bool enablePrintRoverAccuracy = true; + bool enablePrintBadMessages = false; + bool enablePrintLogFileMessages = false; + bool enablePrintLogFileStatus = true; + bool enablePrintRingBufferOffsets = false; + bool enablePrintStates = true; + bool enablePrintDuplicateStates = false; + bool enablePrintRtcSync = false; + RadioType_e radioType = RADIO_EXTERNAL; + uint8_t espnowPeers[5][6] = {0}; // Max of 5 peers. Contains the MAC addresses (6 bytes) of paired units + uint8_t espnowPeerCount = 0; + bool enableRtcmMessageChecking = false; + BluetoothRadioType_e bluetoothRadioType = BLUETOOTH_RADIO_SPP; + bool runLogTest = false; // When set to true, device will create a series of test logs + bool espnowBroadcast = true; // When true, overrides peers and sends all data via broadcast + int16_t antennaHeight = 0; // in mm + float antennaReferencePoint = 0.0; // in mm + bool echoUserInput = true; + int uartReceiveBufferSize = 1024 * 2; // This buffer is filled automatically as the UART receives characters. + int gnssHandlerBufferSize = + 1024 * 4; // This buffer is filled from the UART receive buffer, and is then written to SD + + bool enablePrintBufferOverrun = false; + bool enablePrintSDBuffers = false; + PeriodicDisplay_t periodicDisplay = (PeriodicDisplay_t)0; // Turn off all periodic debug displays by default. + uint32_t periodicDisplayInterval = 15 * 1000; + + uint32_t rebootSeconds = (uint32_t)-1; // Disabled, reboots after uptime reaches this number of seconds + bool forceResetOnSDFail = false; // Set to true to reset system if SD is detected but fails to start. + + uint8_t minElev = 10; // Minimum elevation (in deg) for a GNSS satellite to be used in NAV + uint8_t ubxMessageRatesBase[MAX_UBX_MSG_RTCM] = { + 254}; // Mark first record with key so defaults will be applied. Int value for each supported message - Report + // rates for RTCM Base. Default to u-blox recommended rates. + uint32_t imuYaw = 0; // User defined IMU mount yaw angle (0 to 36,000) CFG-SFIMU-IMU_MNTALG_YAW + int16_t imuPitch = 0; // User defined IMU mount pitch angle (-9000 to 9000) CFG-SFIMU-IMU_MNTALG_PITCH + int16_t imuRoll = 0; // User defined IMU mount roll angle (-18000 to 18000) CFG-SFIMU-IMU_MNTALG_ROLL + bool sfDisableWheelDirection = false; // CFG-SFODO-DIS_AUTODIRPINPOL + bool sfCombineWheelTicks = false; // CFG-SFODO-COMBINE_TICKS + uint8_t rateNavPrio = 0; // Output rate of priority nav mode message - CFG-RATE-NAV_PRIO + // CFG-SFIMU-AUTO_MNTALG_ENA 0 = autoIMUmountAlignment + bool sfUseSpeed = false; // CFG-SFODO-USE_SPEED + + CoordinateInputType coordinateInputType = COORDINATE_INPUT_TYPE_DD; // Default DD.ddddddddd + uint16_t lbandFixTimeout_seconds = 180; // Number of seconds of no L-Band fix before resetting ZED + int16_t minCNO_F9P = 6; // Minimum satellite signal level for navigation. ZED-F9P default is 6 dBHz + int16_t minCNO_F9R = 20; // Minimum satellite signal level for navigation. ZED-F9R default is 20 dBHz + uint16_t serialGNSSRxFullThreshold = 50; // RX FIFO full interrupt. Max of ~128. See pinUART2Task(). + uint8_t btReadTaskPriority = 1; // Read from BT SPP and Write to GNSS. 3 being the highest, and 0 being the lowest + uint8_t gnssReadTaskPriority = + 1; // Read from ZED-F9x and Write to circular buffer (SD, TCP, BT). 3 being the highest, and 0 being the lowest + uint8_t handleGnssDataTaskPriority = 1; // Read from the cicular buffer and dole out to end points (SD, TCP, BT). + uint8_t btReadTaskCore = 1; // Core where task should run, 0=core, 1=Arduino + uint8_t gnssReadTaskCore = 1; // Core where task should run, 0=core, 1=Arduino + uint8_t handleGnssDataTaskCore = 1; // Core where task should run, 0=core, 1=Arduino + uint8_t gnssUartInterruptsCore = + 1; // Core where hardware is started and interrupts are assigned to, 0=core, 1=Arduino + uint8_t bluetoothInterruptsCore = + 1; // Core where hardware is started and interrupts are assigned to, 0=core, 1=Arduino + uint8_t i2cInterruptsCore = 1; // Core where hardware is started and interrupts are assigned to, 0=core, 1=Arduino + uint32_t shutdownNoChargeTimeout_s = 0; // If > 0, shut down unit after timeout if not charging + bool disableSetupButton = false; // By default, allow setup through the overlay button(s) + bool useI2cForLbandCorrections = + true; // Set to false to stop I2C callback. Corrections will require direct ZED to NEO UART2 connections. + bool useI2cForLbandCorrectionsConfigured = false; // If a user sets useI2cForLbandCorrections, this goes true. + + // Ethernet + bool enablePrintEthernetDiag = false; + bool ethernetDHCP = true; + IPAddress ethernetIP = {192, 168, 0, 123}; + IPAddress ethernetDNS = {194, 168, 4, 100}; + IPAddress ethernetGateway = {192, 168, 0, 1}; + IPAddress ethernetSubnet = {255, 255, 255, 0}; + uint16_t httpPort = 80; + + // WiFi + bool debugWifiState = false; + bool wifiConfigOverAP = true; // Configure device over Access Point or have it connect to WiFi + WiFiNetwork wifiNetworks[MAX_WIFI_NETWORKS] = { + {"", ""}, + {"", ""}, + {"", ""}, + {"", ""}, + }; + + // Network layer + uint8_t defaultNetworkType = NETWORK_TYPE_USE_DEFAULT; + bool debugNetworkLayer = false; // Enable debugging of the network layer + bool enableNetworkFailover = true; // Enable failover between Ethernet / WiFi + bool printNetworkStatus = true; // Print network status (delays, failovers, IP address) + + // Multicast DNS Server + bool mdnsEnable = true; // Allows locating of device from browser address 'rtk.local' + + // NTP + bool debugNtp = false; + uint16_t ethernetNtpPort = 123; + bool enableNTPFile = false; // Log NTP requests to file + uint8_t ntpPollExponent = 6; // NTPpacket::defaultPollExponent 2^6 = 64 seconds + int8_t ntpPrecision = -20; // NTPpacket::defaultPrecision 2^-20 = 0.95us + uint32_t ntpRootDelay = 0; // NTPpacket::defaultRootDelay = 0. ntpRootDelay is defined in microseconds. + // ntpProcessOneRequest will convert it to seconds and fraction. + uint32_t ntpRootDispersion = + 1000; // NTPpacket::defaultRootDispersion 1007us = 2^-16 * 66. ntpRootDispersion is defined in microseconds. + // ntpProcessOneRequest will convert it to seconds and fraction. + char ntpReferenceId[5] = {'G', 'P', 'S', 0, + 0}; // NTPpacket::defaultReferenceId. Ref ID is 4 chars. Add one extra for a NULL. + + // NTRIP Client + bool debugNtripClientRtcm = false; + bool debugNtripClientState = false; + bool enableNtripClient = false; + char ntripClient_CasterHost[50] = "rtk2go.com"; // It's free... + uint16_t ntripClient_CasterPort = 2101; + char ntripClient_CasterUser[50] = + "test@test.com"; // Some free casters require auth. User must provide their own email address to use RTK2Go + char ntripClient_CasterUserPW[50] = ""; + char ntripClient_MountPoint[50] = "bldr_SparkFun1"; + char ntripClient_MountPointPW[50] = ""; + bool ntripClient_TransmitGGA = true; + + // NTRIP Server + bool debugNtripServerRtcm = false; + bool debugNtripServerState = false; + bool enableNtripServer = false; + bool ntripServer_StartAtSurveyIn = false; // true = Start WiFi instead of Bluetooth at Survey-In + char ntripServer_CasterHost[NTRIP_SERVER_MAX][50] = // It's free... + { + "rtk2go.com", + "", + }; + uint16_t ntripServer_CasterPort[NTRIP_SERVER_MAX] = + { + 2101, + 0, + }; + char ntripServer_CasterUser[NTRIP_SERVER_MAX][50] = + { + "test@test.com" // Some free casters require auth. User must provide their own email address to use RTK2Go + "", + }; + char ntripServer_CasterUserPW[NTRIP_SERVER_MAX][50] = + { + "", + "", + }; + char ntripServer_MountPoint[NTRIP_SERVER_MAX][50] = + { + "bldr_dwntwn2", // NTRIP Server + "", + }; + char ntripServer_MountPointPW[NTRIP_SERVER_MAX][50] = + { + "WR5wRo4H", + "", + }; + + // TCP Client + bool debugPvtClient = false; + bool enablePvtClient = false; + uint16_t pvtClientPort = 2948; // PVT client port. 2948 is GPS Daemon: http://tcp-udp-ports.com/port-2948.htm + char pvtClientHost[50] = ""; + + // TCP Server + bool debugPvtServer = false; + bool enablePvtServer = false; + uint16_t pvtServerPort = 2948; // PVT server port, 2948 is GPS Daemon: http://tcp-udp-ports.com/port-2948.htm + + // UDP Server + bool debugPvtUdpServer = false; + bool enablePvtUdpServer = false; + uint16_t pvtUdpServerPort = + 10110; // https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=nmea + + uint8_t rtcmTimeoutBeforeUsingLBand_s = + 10; // If 10s have passed without RTCM, enable PMP corrections over L-Band if available + + // Automatic Firmware Update + bool debugFirmwareUpdate = false; + bool enableAutoFirmwareUpdate = false; + uint32_t autoFirmwareCheckMinutes = 24 * 60; + + bool debugLBand = false; + bool enableCaptivePortal = true; + bool enableZedUsb = true; //Can be used to disable ZED USB config + + bool debugWiFiConfig = false; + + int geographicRegion = 0; // Default to US - first entry in Regional_Information_Table + + // Add new settings above <------------------------------------------------------------> + +} Settings; +Settings settings; + +// Monitor which devices on the device are on or offline. +struct struct_online +{ + bool microSD = false; + bool display = false; + bool gnss = false; + bool logging = false; + bool serialOutput = false; + bool fs = false; + bool rtc = false; + bool battery = false; + bool accelerometer = false; + bool ntripClient = false; + bool ntripServer[NTRIP_SERVER_MAX] = {false, false}; + bool lband = false; + bool lbandCorrections = false; + bool i2c = false; + bool pvtClient = false; + bool pvtServer = false; + bool pvtUdpServer = false; + ethernetStatus_e ethernetStatus = ETH_NOT_STARTED; + bool NTPServer = false; // EthernetUDP + bool otaFirmwareUpdate = false; } online; + +#ifdef COMPILE_WIFI +#ifdef COMPILE_L_BAND +// AWS certificate for PointPerfect API +static const char *AWS_PUBLIC_CERT = R"=====( +-----BEGIN CERTIFICATE----- +MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj +ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM +9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw +IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 +VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L +93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm +jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA +A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI +U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs +N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv +o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU +5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy +rqXRfboQnoZsG4q5WTP468SQvvG5 +-----END CERTIFICATE----- +)====="; +#endif // COMPILE_L_BAND +#endif // COMPILE_WIFI +#endif // __SETTINGS_H__ diff --git a/Firmware/RTK_Surveyor/src/BluetoothSerial/BTAddress.cpp b/Firmware/RTK_Surveyor/src/BluetoothSerial/BTAddress.cpp new file mode 100644 index 000000000..7ef1eb1a8 --- /dev/null +++ b/Firmware/RTK_Surveyor/src/BluetoothSerial/BTAddress.cpp @@ -0,0 +1,96 @@ +/* + * BTAddress.cpp + * + * Created on: Jul 2, 2017 + * Author: kolban + * Ported on: Feb 5, 2021 + * Author: Thomas M. (ArcticSnowSky) + */ +#include "sdkconfig.h" +#if defined(CONFIG_BT_ENABLED) && defined(CONFIG_BLUEDROID_ENABLED) + +#include "BTAddress.h" +#include +#include +#include +#include +#include +#include +#ifdef ARDUINO_ARCH_ESP32 +#include "esp32-hal-log.h" +#endif + + +/** + * @brief Create an address from the native ESP32 representation. + * @param [in] address The native representation. + */ +BTAddress::BTAddress(esp_bd_addr_t address) { + memcpy(m_address, address, ESP_BD_ADDR_LEN); +} // BTAddress + + +/** + * @brief Create an address from a hex string + * + * A hex string is of the format: + * ``` + * 00:00:00:00:00:00 + * ``` + * which is 17 characters in length. + * + * @param [in] stringAddress The hex representation of the address. + */ +BTAddress::BTAddress(std::string stringAddress) { + if (stringAddress.length() != 17) return; + + int data[6]; + sscanf(stringAddress.c_str(), "%x:%x:%x:%x:%x:%x", &data[0], &data[1], &data[2], &data[3], &data[4], &data[5]); + m_address[0] = (uint8_t) data[0]; + m_address[1] = (uint8_t) data[1]; + m_address[2] = (uint8_t) data[2]; + m_address[3] = (uint8_t) data[3]; + m_address[4] = (uint8_t) data[4]; + m_address[5] = (uint8_t) data[5]; +} // BTAddress + + +/** + * @brief Determine if this address equals another. + * @param [in] otherAddress The other address to compare against. + * @return True if the addresses are equal. + */ +bool BTAddress::equals(BTAddress otherAddress) { + return memcmp(otherAddress.getNative(), m_address, 6) == 0; +} // equals + + +/** + * @brief Return the native representation of the address. + * @return The native representation of the address. + */ +esp_bd_addr_t *BTAddress::getNative() { + return &m_address; +} // getNative + + +/** + * @brief Convert a BT address to a string. + * + * A string representation of an address is in the format: + * + * ``` + * xx:xx:xx:xx:xx:xx + * ``` + * + * @return The string representation of the address. + */ +std::string BTAddress::toString() { + auto size = 18; + char *res = (char*)malloc(size); + snprintf(res, size, "%02x:%02x:%02x:%02x:%02x:%02x", m_address[0], m_address[1], m_address[2], m_address[3], m_address[4], m_address[5]); + std::string ret(res); + free(res); + return ret; +} // toString +#endif diff --git a/Firmware/RTK_Surveyor/src/BluetoothSerial/BTAddress.h b/Firmware/RTK_Surveyor/src/BluetoothSerial/BTAddress.h new file mode 100644 index 000000000..6213d01fd --- /dev/null +++ b/Firmware/RTK_Surveyor/src/BluetoothSerial/BTAddress.h @@ -0,0 +1,36 @@ +/* + * BTAddress.h + * + * Created on: Jul 2, 2017 + * Author: kolban + * Ported on: Feb 5, 2021 + * Author: Thomas M. (ArcticSnowSky) + */ + +#ifndef COMPONENTS_CPP_UTILS_BTADDRESS_H_ +#define COMPONENTS_CPP_UTILS_BTADDRESS_H_ +#include "sdkconfig.h" +#if defined(CONFIG_BT_ENABLED) && defined(CONFIG_BLUEDROID_ENABLED) +#include // ESP32 BT +#include + + +/** + * @brief A %BT device address. + * + * Every %BT device has a unique address which can be used to identify it and form connections. + */ +class BTAddress { +public: + BTAddress(esp_bd_addr_t address); + BTAddress(std::string stringAddress); + bool equals(BTAddress otherAddress); + esp_bd_addr_t* getNative(); + std::string toString(); + +private: + esp_bd_addr_t m_address; +}; + +#endif /* CONFIG_BT_ENABLED */ +#endif /* COMPONENTS_CPP_UTILS_BTADDRESS_H_ */ diff --git a/Firmware/RTK_Surveyor/src/BluetoothSerial/BTAdvertisedDevice.h b/Firmware/RTK_Surveyor/src/BluetoothSerial/BTAdvertisedDevice.h new file mode 100644 index 000000000..07e93622e --- /dev/null +++ b/Firmware/RTK_Surveyor/src/BluetoothSerial/BTAdvertisedDevice.h @@ -0,0 +1,65 @@ +/* + * BTAdvertisedDevice.h + * + * Created on: Feb 5, 2021 + * Author: Thomas M. (ArcticSnowSky) + */ + +#ifndef __BTADVERTISEDDEVICE_H__ +#define __BTADVERTISEDDEVICE_H__ + +#include "BTAddress.h" + + +class BTAdvertisedDevice { +public: + virtual ~BTAdvertisedDevice() = default; + + virtual BTAddress getAddress(); + virtual uint32_t getCOD(); + virtual std::string getName(); + virtual int8_t getRSSI(); + + + virtual bool haveCOD(); + virtual bool haveName(); + virtual bool haveRSSI(); + + virtual std::string toString(); +}; + +class BTAdvertisedDeviceSet : public virtual BTAdvertisedDevice { +public: + BTAdvertisedDeviceSet(); + //~BTAdvertisedDeviceSet() = default; + + + BTAddress getAddress(); + uint32_t getCOD(); + std::string getName(); + int8_t getRSSI(); + + + bool haveCOD(); + bool haveName(); + bool haveRSSI(); + + std::string toString(); + + void setAddress(BTAddress address); + void setCOD(uint32_t cod); + void setName(std::string name); + void setRSSI(int8_t rssi); + + bool m_haveCOD; + bool m_haveName; + bool m_haveRSSI; + + + BTAddress m_address = BTAddress((uint8_t*)"\0\0\0\0\0\0"); + uint32_t m_cod; + std::string m_name; + int8_t m_rssi; +}; + +#endif \ No newline at end of file diff --git a/Firmware/RTK_Surveyor/src/BluetoothSerial/BTAdvertisedDeviceSet.cpp b/Firmware/RTK_Surveyor/src/BluetoothSerial/BTAdvertisedDeviceSet.cpp new file mode 100644 index 000000000..c8f28e9c3 --- /dev/null +++ b/Firmware/RTK_Surveyor/src/BluetoothSerial/BTAdvertisedDeviceSet.cpp @@ -0,0 +1,78 @@ +/* + * BTAdvertisedDeviceSet.cpp + * + * Created on: Feb 5, 2021 + * Author: Thomas M. (ArcticSnowSky) + */ + +#include "sdkconfig.h" +#if defined(CONFIG_BT_ENABLED) && defined(CONFIG_BLUEDROID_ENABLED) + +//#include + +#include "BTAdvertisedDevice.h" +//#include "BTScan.h" + + +BTAdvertisedDeviceSet::BTAdvertisedDeviceSet() { + m_cod = 0; + m_name = ""; + m_rssi = 0; + + m_haveCOD = false; + m_haveName = false; + m_haveRSSI = false; +} // BTAdvertisedDeviceSet + +BTAddress BTAdvertisedDeviceSet::getAddress() { return m_address; } +uint32_t BTAdvertisedDeviceSet::getCOD() { return m_cod; } +std::string BTAdvertisedDeviceSet::getName() { return m_name; } +int8_t BTAdvertisedDeviceSet::getRSSI() { return m_rssi; } + + +bool BTAdvertisedDeviceSet::haveCOD() { return m_haveCOD; } +bool BTAdvertisedDeviceSet::haveName() { return m_haveName; } +bool BTAdvertisedDeviceSet::haveRSSI() { return m_haveRSSI; } + +/** + * @brief Create a string representation of this device. + * @return A string representation of this device. + */ +std::string BTAdvertisedDeviceSet::toString() { + std::string res = "Name: " + getName() + ", Address: " + getAddress().toString(); + if (haveCOD()) { + char val[6]; + snprintf(val, sizeof(val), "%d", getCOD()); + res += ", cod: "; + res += val; + } + if (haveRSSI()) { + char val[6]; + snprintf(val, sizeof(val), "%d", (int8_t)getRSSI()); + res += ", rssi: "; + res += val; + } + return res; +} // toString + + +void BTAdvertisedDeviceSet::setAddress(BTAddress address) { + m_address = address; +} + +void BTAdvertisedDeviceSet::setCOD(uint32_t cod) { + m_cod = cod; + m_haveCOD = true; +} + +void BTAdvertisedDeviceSet::setName(std::string name) { + m_name = name; + m_haveName = true; +} + +void BTAdvertisedDeviceSet::setRSSI(int8_t rssi) { + m_rssi = rssi; + m_haveRSSI = true; +} + +#endif /* CONFIG_BT_ENABLED */ diff --git a/Firmware/RTK_Surveyor/src/BluetoothSerial/BTScan.h b/Firmware/RTK_Surveyor/src/BluetoothSerial/BTScan.h new file mode 100644 index 000000000..3650d4162 --- /dev/null +++ b/Firmware/RTK_Surveyor/src/BluetoothSerial/BTScan.h @@ -0,0 +1,42 @@ +/* + * BTScan.h + * + * Created on: Feb 5, 2021 + * Author: Thomas M. (ArcticSnowSky) + */ + +#ifndef __BTSCAN_H__ +#define __BTSCAN_H__ + +#include +#include +#include +#include "BTAddress.h" +#include "BTAdvertisedDevice.h" + +class BTAdvertisedDevice; +class BTAdvertisedDeviceSet; + + +class BTScanResults { +public: + virtual ~BTScanResults() = default; + + virtual void dump(Print *print = nullptr); + virtual int getCount(); + virtual BTAdvertisedDevice* getDevice(uint32_t i); +}; + +class BTScanResultsSet : public BTScanResults { +public: + void dump(Print *print = nullptr); + int getCount(); + BTAdvertisedDevice* getDevice(uint32_t i); + + bool add(BTAdvertisedDeviceSet advertisedDevice, bool unique = true); + void clear(); + + std::map m_vectorAdvertisedDevices; +}; + +#endif \ No newline at end of file diff --git a/Firmware/RTK_Surveyor/src/BluetoothSerial/BTScanResultsSet.cpp b/Firmware/RTK_Surveyor/src/BluetoothSerial/BTScanResultsSet.cpp new file mode 100644 index 000000000..79d23e463 --- /dev/null +++ b/Firmware/RTK_Surveyor/src/BluetoothSerial/BTScanResultsSet.cpp @@ -0,0 +1,95 @@ +/* + * BTScanResultsSet.cpp + * + * Created on: Feb 5, 2021 + * Author: Thomas M. (ArcticSnowSky) + */ + +#include "sdkconfig.h" +#if defined(CONFIG_BT_ENABLED) && defined(CONFIG_BLUEDROID_ENABLED) + + +#include + +#include "BTAdvertisedDevice.h" +#include "BTScan.h" +//#include "GeneralUtils.h" +#include "esp32-hal-log.h" + + +class BTAdvertisedDevice; + +/** + * @brief Dump the scan results to the log. + */ +void BTScanResultsSet::dump(Print *print) { + int cnt = getCount(); + if (print == nullptr) { + log_v(">> Dump scan results : %d", cnt); + for (int i=0; i < cnt; i++) { + BTAdvertisedDevice* dev = getDevice(i); + if (dev) + log_d("- %d: %s\n", i+1, dev->toString().c_str()); + else + log_d("- %d is null\n", i+1); + } + log_v("-- dump finished --"); + } else { + print->printf(">> Dump scan results: %d\n", cnt); + for (int i=0; i < cnt; i++) { + BTAdvertisedDevice* dev = getDevice(i); + if (dev) + print->printf("- %d: %s\n", i+1, dev->toString().c_str()); + else + print->printf("- %d is null\n", i+1); + } + print->println("-- Dump finished --"); + } +} // dump + + +/** + * @brief Return the count of devices found in the last scan. + * @return The number of devices found in the last scan. + */ +int BTScanResultsSet::getCount() { + return m_vectorAdvertisedDevices.size(); +} // getCount + + +/** + * @brief Return the specified device at the given index. + * The index should be between 0 and getCount()-1. + * @param [in] i The index of the device. + * @return The device at the specified index. + */ +BTAdvertisedDevice* BTScanResultsSet::getDevice(uint32_t i) { + if (i < 0) + return nullptr; + + uint32_t x = 0; + BTAdvertisedDeviceSet* pDev = &m_vectorAdvertisedDevices.begin()->second; + for (auto it = m_vectorAdvertisedDevices.begin(); it != m_vectorAdvertisedDevices.end(); it++) { + pDev = &it->second; + if (x==i) break; + x++; + } + return x==i ? pDev : nullptr; +} + +void BTScanResultsSet::clear() { + //for(auto _dev : m_vectorAdvertisedDevices) + // delete _dev.second; + m_vectorAdvertisedDevices.clear(); +} + +bool BTScanResultsSet::add(BTAdvertisedDeviceSet advertisedDevice, bool unique) { + std::string key = advertisedDevice.getAddress().toString(); + if (!unique || m_vectorAdvertisedDevices.count(key) == 0) { + m_vectorAdvertisedDevices.insert(std::pair(key, advertisedDevice)); + return true; + } else + return false; +} + +#endif \ No newline at end of file diff --git a/Firmware/RTK_Surveyor/src/BluetoothSerial/BluetoothSerial.cpp b/Firmware/RTK_Surveyor/src/BluetoothSerial/BluetoothSerial.cpp index 0c1b54d0c..575070f18 100644 --- a/Firmware/RTK_Surveyor/src/BluetoothSerial/BluetoothSerial.cpp +++ b/Firmware/RTK_Surveyor/src/BluetoothSerial/BluetoothSerial.cpp @@ -41,10 +41,9 @@ const char * _spp_server_name = "ESP32SPP"; //Now passed in during begin() -//#define RX_QUEUE_SIZE (512 * 4) //Increase to facilitate larger NTRIP transfers -//#define RX_QUEUE_SIZE 512 -//#define TX_QUEUE_SIZE 512 //Increase to facilitate high transmission rates +//#define RX_QUEUE_SIZE 512 //Original //#define TX_QUEUE_SIZE 32 + #define SPP_TX_QUEUE_TIMEOUT 1000 #define SPP_TX_DONE_TIMEOUT 1000 #define SPP_CONGESTED_TIMEOUT 1000 @@ -55,6 +54,7 @@ static xQueueHandle _spp_tx_queue = NULL; static SemaphoreHandle_t _spp_tx_done = NULL; static TaskHandle_t _spp_task_handle = NULL; static EventGroupHandle_t _spp_event_group = NULL; +static EventGroupHandle_t _bt_event_group = NULL; static boolean secondConnectionAttempt; static esp_spp_cb_t * custom_spp_callback = NULL; static BluetoothSerialDataCb custom_data_callback = NULL; @@ -75,11 +75,18 @@ static int _pin_len; static bool _isPinSet; static bool _enableSSP; +static BTScanResultsSet scanResults; +static BTAdvertisedDeviceCb advertisedDeviceCb = nullptr; + #define SPP_RUNNING 0x01 #define SPP_CONNECTED 0x02 #define SPP_CONGESTED 0x04 #define SPP_DISCONNECTED 0x08 +#define BT_DISCOVERY_RUNNING 0x01 +#define BT_DISCOVERY_COMPLETED 0x02 + + typedef struct { size_t len; uint8_t data[]; @@ -158,18 +165,7 @@ static esp_err_t _spp_queue_packet(uint8_t *data, size_t len){ return ESP_OK; } -//SPP_TX_MAX seems to default to a lower amount (330) until there is congestion it then -//tries to catch up but at 330, it cannot and the heap begins to take the hit. -//Increasing the max to 2048 we see normal verbose xfers of 512 until congestion -//when it increases briefly to 2048 then returns to 512 with no heap hit. -//The rate at which we congest is dependant on how much we are attempting to TX and -//how much is coming in RX from the phone. -//At ~25kBps heap lowest point is 100k and stable -//At ~50kBps heap lowest point is ~78k and stable -//Above 50kbps it becomes unstable - -//const uint16_t SPP_TX_MAX = 330; //Original -const uint16_t SPP_TX_MAX = 512*4; +const uint16_t SPP_TX_MAX = 330; static uint8_t _spp_tx_buffer[SPP_TX_MAX]; static uint16_t _spp_tx_buffer_len = 0; @@ -255,7 +251,11 @@ static void esp_spp_cb(esp_spp_cb_event_t event, esp_spp_cb_param_t *param) { case ESP_SPP_INIT_EVT: log_i("ESP_SPP_INIT_EVT"); +#ifdef ESP_IDF_VERSION_MAJOR + esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE); +#else esp_bt_gap_set_scan_mode(ESP_BT_SCAN_MODE_CONNECTABLE_DISCOVERABLE); +#endif if (!_isMaster) { log_i("ESP_SPP_INIT_EVT: slave: start"); esp_spp_start_srv(ESP_SPP_SEC_NONE, ESP_SPP_ROLE_SLAVE, 0, _spp_server_name); @@ -299,13 +299,10 @@ static void esp_spp_cb(esp_spp_cb_event_t event, esp_spp_cb_param_t *param) case ESP_SPP_CONG_EVT://connection congestion status changed if(param->cong.cong){ xEventGroupClearBits(_spp_event_group, SPP_CONGESTED); - log_d("ESP_SPP_CONG_EVT: CONGESTED"); - } - else - { + } else { xEventGroupSetBits(_spp_event_group, SPP_CONGESTED); } - //log_v("ESP_SPP_CONG_EVT: %s", param->cong.cong ? "CONGESTED" : "FREE"); + log_v("ESP_SPP_CONG_EVT: %s", param->cong.cong?"CONGESTED":"FREE"); break; case ESP_SPP_WRITE_EVT://write operation completed @@ -330,7 +327,7 @@ static void esp_spp_cb(esp_spp_cb_event_t event, esp_spp_cb_param_t *param) } else if (_spp_rx_queue != NULL){ for (int i = 0; i < param->data_ind.len; i++){ if(xQueueSend(_spp_rx_queue, param->data_ind.data + i, (TickType_t)0) != pdTRUE){ - log_e("RX Full! Discarding %u bytes", param->data_ind.len - i); + Serial.printf("Bluetooth RX buffer full! Discarding %u bytes. Consider increasing SPP RX buffer size.\r\n", param->data_ind.len - i); break; } } @@ -381,15 +378,16 @@ void BluetoothSerial::onData(BluetoothSerialDataCb cb){ static void esp_bt_gap_cb(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t *param) { switch(event){ - case ESP_BT_GAP_DISC_RES_EVT: + case ESP_BT_GAP_DISC_RES_EVT: { log_i("ESP_BT_GAP_DISC_RES_EVT"); #if (ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_INFO) char bda_str[18]; log_i("Scanned device: %s", bda2str(param->disc_res.bda, bda_str, 18)); #endif + BTAdvertisedDeviceSet advertisedDevice; + uint8_t peer_bdname_len = 0; + char peer_bdname[ESP_BT_GAP_MAX_BDNAME_LEN + 1]; for (int i = 0; i < param->disc_res.num_prop; i++) { - uint8_t peer_bdname_len; - char peer_bdname[ESP_BT_GAP_MAX_BDNAME_LEN + 1]; switch(param->disc_res.prop[i].type) { case ESP_BT_GAP_DEV_PROP_EIR: if (get_name_from_eir((uint8_t*)param->disc_res.prop[i].val, peer_bdname, &peer_bdname_len)) { @@ -422,10 +420,24 @@ static void esp_bt_gap_cb(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t *pa case ESP_BT_GAP_DEV_PROP_COD: log_d("ESP_BT_GAP_DEV_PROP_COD"); + if (param->disc_res.prop[i].len <= sizeof(int)) { + uint32_t cod = 0; + memcpy(&cod, param->disc_res.prop[i].val, param->disc_res.prop[i].len); + advertisedDevice.setCOD(cod); + } else { + log_d("Value size larger than integer"); + } break; case ESP_BT_GAP_DEV_PROP_RSSI: log_d("ESP_BT_GAP_DEV_PROP_RSSI"); + if (param->disc_res.prop[i].len <= sizeof(int)) { + uint8_t rssi = 0; + memcpy(&rssi, param->disc_res.prop[i].val, param->disc_res.prop[i].len); + advertisedDevice.setRSSI(rssi); + } else { + log_d("Value size larger than integer"); + } break; default: @@ -434,17 +446,33 @@ static void esp_bt_gap_cb(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t *pa if (_isRemoteAddressSet) break; } - break; + if (peer_bdname_len) + advertisedDevice.setName(peer_bdname); + esp_bd_addr_t addr; + memcpy(addr, param->disc_res.bda, ESP_BD_ADDR_LEN); + advertisedDevice.setAddress(BTAddress(addr)); + if (scanResults.add(advertisedDevice) && advertisedDeviceCb) + advertisedDeviceCb(&advertisedDevice); + } + break; + case ESP_BT_GAP_DISC_STATE_CHANGED_EVT: log_i("ESP_BT_GAP_DISC_STATE_CHANGED_EVT"); + if (param->disc_st_chg.state == ESP_BT_GAP_DISCOVERY_STOPPED) { + xEventGroupClearBits(_bt_event_group, BT_DISCOVERY_RUNNING); + xEventGroupSetBits(_bt_event_group, BT_DISCOVERY_COMPLETED); + } else { // ESP_BT_GAP_DISCOVERY_STARTED + xEventGroupClearBits(_bt_event_group, BT_DISCOVERY_COMPLETED); + xEventGroupSetBits(_bt_event_group, BT_DISCOVERY_RUNNING); + } break; case ESP_BT_GAP_RMT_SRVCS_EVT: - log_i( "ESP_BT_GAP_RMT_SRVCS_EVT"); + log_i( "ESP_BT_GAP_RMT_SRVCS_EVT: status = %d, num_uuids = %d", param->rmt_srvcs.stat, param->rmt_srvcs.num_uuids); break; case ESP_BT_GAP_RMT_SRVC_REC_EVT: - log_i("ESP_BT_GAP_RMT_SRVC_REC_EVT"); + log_i("ESP_BT_GAP_RMT_SRVC_REC_EVT: status = %d", param->rmt_srvc_rec.stat); break; case ESP_BT_GAP_AUTH_CMPL_EVT: @@ -501,8 +529,17 @@ static void esp_bt_gap_cb(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t *pa } } +//static bool _init_bt(const char *deviceName) static bool _init_bt(const char *deviceName, uint16_t rxQueueSize, uint16_t txQueueSize) { + if(!_bt_event_group){ + _bt_event_group = xEventGroupCreate(); + if(!_bt_event_group){ + log_e("BT Event Group Create Failed!"); + return false; + } + xEventGroupClearBits(_bt_event_group, 0xFFFFFF); + } if(!_spp_event_group){ _spp_event_group = xEventGroupCreate(); if(!_spp_event_group){ @@ -514,6 +551,7 @@ static bool _init_bt(const char *deviceName, uint16_t rxQueueSize, uint16_t txQu xEventGroupSetBits(_spp_event_group, SPP_DISCONNECTED); } if (_spp_rx_queue == NULL){ + //_spp_rx_queue = xQueueCreate(RX_QUEUE_SIZE, sizeof(uint8_t)); //initialize the queue _spp_rx_queue = xQueueCreate(rxQueueSize, sizeof(uint8_t)); //initialize the queue if (_spp_rx_queue == NULL){ log_e("RX Queue Create Failed"); @@ -521,7 +559,8 @@ static bool _init_bt(const char *deviceName, uint16_t rxQueueSize, uint16_t txQu } } if (_spp_tx_queue == NULL){ - _spp_tx_queue = xQueueCreate(txQueueSize, sizeof(spp_packet_t *)); //initialize the queue + //_spp_tx_queue = xQueueCreate(TX_QUEUE_SIZE, sizeof(spp_packet_t*)); //initialize the queue + _spp_tx_queue = xQueueCreate(txQueueSize, sizeof(spp_packet_t*)); //initialize the queue if (_spp_tx_queue == NULL){ log_e("TX Queue Create Failed"); return false; @@ -537,7 +576,7 @@ static bool _init_bt(const char *deviceName, uint16_t rxQueueSize, uint16_t txQu } if(!_spp_task_handle){ - xTaskCreatePinnedToCore(_spp_tx_task, "spp_tx", 4096, NULL, 2, &_spp_task_handle, 0); + xTaskCreatePinnedToCore(_spp_tx_task, "spp_tx", 4096, NULL, 10, &_spp_task_handle, 0); if(!_spp_task_handle){ log_e("Network Event Task Start Failed!"); return false; @@ -647,6 +686,10 @@ static bool _stop_bt() vSemaphoreDelete(_spp_tx_done); _spp_tx_done = NULL; } + if (_bt_event_group) { + vEventGroupDelete(_bt_event_group); + _bt_event_group = NULL; + } return true; } @@ -655,6 +698,11 @@ static bool waitForConnect(int timeout) { return (xEventGroupWaitBits(_spp_event_group, SPP_CONNECTED, pdFALSE, pdTRUE, xTicksToWait) & SPP_CONNECTED) != 0; } +static bool waitForDiscovered(int timeout) { + TickType_t xTicksToWait = timeout / portTICK_PERIOD_MS; + return (xEventGroupWaitBits(_spp_event_group, BT_DISCOVERY_COMPLETED, pdFALSE, pdTRUE, xTicksToWait) & BT_DISCOVERY_COMPLETED) != 0; +} + /* * Serial Bluetooth Arduino * @@ -670,12 +718,14 @@ BluetoothSerial::~BluetoothSerial(void) _stop_bt(); } +//bool BluetoothSerial::begin(String localName, bool isMaster) bool BluetoothSerial::begin(String localName, bool isMaster, uint16_t rxQueueSize, uint16_t txQueueSize) { _isMaster = isMaster; if (localName.length()){ local_name = localName; } + //return _init_bt(local_name.c_str()); return _init_bt(local_name.c_str(), rxQueueSize, txQueueSize); } @@ -799,7 +849,11 @@ bool BluetoothSerial::connect(String remoteName) _remote_name[ESP_BT_GAP_MAX_BDNAME_LEN] = 0; log_i("master : remoteName"); // will first resolve name to address - esp_bt_gap_set_scan_mode(ESP_BT_SCAN_MODE_CONNECTABLE_DISCOVERABLE); +#ifdef ESP_IDF_VERSION_MAJOR + esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE); +#else + esp_bt_gap_set_scan_mode(ESP_BT_SCAN_MODE_CONNECTABLE_DISCOVERABLE); +#endif if (esp_bt_gap_start_discovery(ESP_BT_INQ_MODE_GENERAL_INQUIRY, INQ_LEN, INQ_NUM_RSPS) == ESP_OK) { return waitForConnect(SCAN_TIMEOUT); } @@ -839,7 +893,11 @@ bool BluetoothSerial::connect() disconnect(); log_i("master : remoteName"); // will resolve name to address first - it may take a while +#ifdef ESP_IDF_VERSION_MAJOR + esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE); +#else esp_bt_gap_set_scan_mode(ESP_BT_SCAN_MODE_CONNECTABLE_DISCOVERABLE); +#endif if (esp_bt_gap_start_discovery(ESP_BT_INQ_MODE_GENERAL_INQUIRY, INQ_LEN, INQ_NUM_RSPS) == ESP_OK) { return waitForConnect(SCAN_TIMEOUT); } @@ -886,8 +944,75 @@ bool BluetoothSerial::isReady(bool checkMaster, int timeout) { return (xEventGroupWaitBits(_spp_event_group, SPP_RUNNING, pdFALSE, pdTRUE, xTicksToWait) & SPP_RUNNING) != 0; } -bool BluetoothSerial::isCongested(){ - return(!(xEventGroupGetBits(_spp_event_group) & SPP_CONGESTED)); + +/** + * @brief RemoteName or address are not allowed to be set during discovery + * (otherwhise it might connect automatically and stop discovery) + * @param[in] timeoutMs can range from MIN_INQ_TIME to MAX_INQ_TIME + * @return in case of Error immediately Empty ScanResults. + */ +BTScanResults* BluetoothSerial::discover(int timeoutMs) { + scanResults.clear(); + if (timeoutMs < MIN_INQ_TIME || timeoutMs > MAX_INQ_TIME || strlen(_remote_name) || _isRemoteAddressSet) + return nullptr; + int timeout = timeoutMs / INQ_TIME; + log_i("discover::disconnect"); + disconnect(); + log_i("discovering"); + // will resolve name to address first - it may take a while + esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE); + if (esp_bt_gap_start_discovery(ESP_BT_INQ_MODE_GENERAL_INQUIRY, timeout, 0) == ESP_OK) { + waitForDiscovered(timeoutMs); + esp_bt_gap_cancel_discovery(); + } + return &scanResults; +} + +/** + * @brief RemoteName or address are not allowed to be set during discovery + * (otherwhise it might connect automatically and stop discovery) + * @param[in] cb called when a [b]new[/b] device has been discovered + * @param[in] timeoutMs can be 0 or range from MIN_INQ_TIME to MAX_INQ_TIME + * + * @return Wheter start was successfull or problems with params + */ +bool BluetoothSerial::discoverAsync(BTAdvertisedDeviceCb cb, int timeoutMs) { + scanResults.clear(); + if (strlen(_remote_name) || _isRemoteAddressSet) + return false; + int timeout = timeoutMs / INQ_TIME; + disconnect(); + advertisedDeviceCb = cb; + log_i("discovering"); + // will resolve name to address first - it may take a while + esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE); + if (timeout > 0) + return esp_bt_gap_start_discovery(ESP_BT_INQ_MODE_GENERAL_INQUIRY, timeout, 0) == ESP_OK; + else return esp_bt_gap_start_discovery(ESP_BT_INQ_MODE_GENERAL_INQUIRY, ESP_BT_GAP_MAX_INQ_LEN, 0) == ESP_OK; } +/** @brief Stops the asynchronous discovery and clears the callback */ +void BluetoothSerial::discoverAsyncStop() { + esp_bt_gap_cancel_discovery(); + advertisedDeviceCb = nullptr; +} + +/** @brief Clears scanresult entries */ +void BluetoothSerial::discoverClear() { + scanResults.clear(); +} + +/** @brief Can be used while discovering asynchronously + * Will be returned also on synchronous discovery. + * + * @return BTScanResults contains several information of found devices + */ +BTScanResults* BluetoothSerial::getScanResults() { + return &scanResults; +} + +BluetoothSerial::operator bool() const +{ + return true; +} #endif diff --git a/Firmware/RTK_Surveyor/src/BluetoothSerial/BluetoothSerial.h b/Firmware/RTK_Surveyor/src/BluetoothSerial/BluetoothSerial.h index 6c4d2d3e3..939c3f4ee 100644 --- a/Firmware/RTK_Surveyor/src/BluetoothSerial/BluetoothSerial.h +++ b/Firmware/RTK_Surveyor/src/BluetoothSerial/BluetoothSerial.h @@ -21,12 +21,15 @@ #include "Arduino.h" #include "Stream.h" +#include #include #include +#include "BTScan.h" typedef std::function BluetoothSerialDataCb; typedef std::function ConfirmRequestCb; typedef std::function AuthCompleteCb; +typedef std::function BTAdvertisedDeviceCb; class BluetoothSerial: public Stream { @@ -35,7 +38,10 @@ class BluetoothSerial: public Stream BluetoothSerial(void); ~BluetoothSerial(void); - bool begin(String localName=String(), bool isMaster=false, uint16_t rxQueueSize = 512 * 4, uint16_t txQueueSize = 512); + bool begin(String localName=String(), bool isMaster=false, uint16_t rxQueueSize = 512 * 2, uint16_t txQueueSize = 512); + bool begin(unsigned long baud){//compatibility + return begin(); + } int available(void); int peek(void); bool hasClient(void); @@ -61,8 +67,17 @@ class BluetoothSerial: public Stream bool disconnect(); bool unpairDevice(uint8_t remoteAddress[]); - bool isCongested(); - + BTScanResults* discover(int timeout=0x30*1280); + bool discoverAsync(BTAdvertisedDeviceCb cb, int timeout=0x30*1280); + void discoverAsyncStop(); + void discoverClear(); + BTScanResults* getScanResults(); + + const int INQ_TIME = 1280; // Inquire Time unit 1280 ms + const int MIN_INQ_TIME = (ESP_BT_GAP_MIN_INQ_LEN * INQ_TIME); + const int MAX_INQ_TIME = (ESP_BT_GAP_MAX_INQ_LEN * INQ_TIME); + + operator bool() const; private: String local_name; diff --git a/Firmware/RTK_Surveyor/support.ino b/Firmware/RTK_Surveyor/support.ino index 8088edea0..20606197c 100644 --- a/Firmware/RTK_Surveyor/support.ino +++ b/Firmware/RTK_Surveyor/support.ino @@ -1,306 +1,858 @@ -void printDebug(String thingToPrint) -{ - if (settings.printDebugMessages == true) - { - Serial.print(thingToPrint); - } -} +// Helper functions to support printing to eiter the serial port or bluetooth connection -//Option not known -void printUnknown(uint8_t unknownChoice) +// If we are printing to all endpoints, BT gets priority +int systemAvailable() { - Serial.print(F("Unknown choice: ")); - Serial.write(unknownChoice); - Serial.println(); + if (printEndpoint == PRINT_ENDPOINT_BLUETOOTH || printEndpoint == PRINT_ENDPOINT_ALL) + return (bluetoothRxDataAvailable()); + return (Serial.available()); } -void printUnknown(int unknownValue) + +// If we are printing to all endpoints, BT gets priority +int systemRead() { - Serial.print(F("Unknown value: ")); - Serial.write(unknownValue); - Serial.println(); + if (printEndpoint == PRINT_ENDPOINT_BLUETOOTH || printEndpoint == PRINT_ENDPOINT_ALL) + return (bluetoothRead()); + return (Serial.read()); } -//Get single byte from user -//Waits for and returns the character that the user provides -//Returns STATUS_GETNUMBER_TIMEOUT if input times out -//Returns 'x' if user presses 'x' -uint8_t getByteChoice(int numberOfSeconds) +// Output a buffer of the specified length to the serial port +void systemWrite(const uint8_t *buffer, uint16_t length) { - Serial.flush(); - delay(50);//Wait for any incoming chars to hit buffer - while (Serial.available() > 0) Serial.read(); //Clear buffer - - long startTime = millis(); - byte incoming; - while (1) - { - delay(10); //Yield to processor - i2cGNSS.checkUblox(); //Regularly poll to get latest data - - if (Serial.available() > 0) + if (printEndpoint == PRINT_ENDPOINT_ALL) { - incoming = Serial.read(); - if (incoming >= 'a' && incoming <= 'z') break; - if (incoming >= 'A' && incoming <= 'Z') break; - if (incoming >= '0' && incoming <= '9') break; + Serial.write(buffer, length); + bluetoothWrite(buffer, length); } + else if (printEndpoint == PRINT_ENDPOINT_BLUETOOTH) + bluetoothWrite(buffer, length); + else + Serial.write(buffer, length); +} - if ( (millis() - startTime) / 1000 >= numberOfSeconds) +// Ensure all serial output has been transmitted, FIFOs are empty +void systemFlush() +{ + if (printEndpoint == PRINT_ENDPOINT_ALL) { - Serial.println(F("No user input received.")); - return (STATUS_GETBYTE_TIMEOUT); //Timeout. No user input. + Serial.flush(); + bluetoothFlush(); } - } + else if (printEndpoint == PRINT_ENDPOINT_BLUETOOTH) + bluetoothFlush(); + else + Serial.flush(); +} + +// Output a byte to the serial port +void systemWrite(uint8_t value) +{ + systemWrite(&value, 1); +} + +// Point the string at the selected endpoint +void systemPrint(const char *string) +{ + systemWrite((const uint8_t *)string, strlen(string)); +} + +// Enable printfs to various endpoints +// https://stackoverflow.com/questions/42131753/wrapper-for-printf +void systemPrintf(const char *format, ...) +{ + va_list args; + va_start(args, format); + + va_list args2; + va_copy(args2, args); + char buf[vsnprintf(nullptr, 0, format, args) + 1]; + + vsnprintf(buf, sizeof buf, format, args2); + + systemPrint(buf); + + va_end(args); + va_end(args2); +} - return (incoming); +// Print a string with a carriage return and linefeed +void systemPrintln(const char *value) +{ + systemPrint(value); + systemPrintln(); +} + +// Print an integer value +void systemPrint(int value) +{ + char temp[20]; + snprintf(temp, sizeof(temp), "%d", value); + systemPrint(temp); +} + +// Print an integer value as HEX or decimal +void systemPrint(int value, uint8_t printType) +{ + char temp[20]; + + if (printType == HEX) + snprintf(temp, sizeof(temp), "%08X", value); + else if (printType == DEC) + snprintf(temp, sizeof(temp), "%d", value); + + systemPrint(temp); +} + +// Pretty print IP addresses +void systemPrint(IPAddress ipaddress) +{ + systemPrint(ipaddress[0], DEC); + systemPrint("."); + systemPrint(ipaddress[1], DEC); + systemPrint("."); + systemPrint(ipaddress[2], DEC); + systemPrint("."); + systemPrint(ipaddress[3], DEC); +} +void systemPrintln(IPAddress ipaddress) +{ + systemPrint(ipaddress); + systemPrintln(); +} + +// Print an integer value with a carriage return and line feed +void systemPrintln(int value) +{ + systemPrint(value); + systemPrintln(); +} + +// Print an 8-bit value as HEX or decimal +void systemPrint(uint8_t value, uint8_t printType) +{ + char temp[20]; + + if (printType == HEX) + snprintf(temp, sizeof(temp), "%02X", value); + else if (printType == DEC) + snprintf(temp, sizeof(temp), "%d", value); + + systemPrint(temp); +} + +// Print an 8-bit value as HEX or decimal with a carriage return and linefeed +void systemPrintln(uint8_t value, uint8_t printType) +{ + systemPrint(value, printType); + systemPrintln(); } -//Get a string/value from user, remove all non-numeric values -//Returns STATUS_GETNUMBER_TIMEOUT if input times out -//Returns STATUS_PRESSED_X if user presses 'x' -int64_t getNumber(int numberOfSeconds) +// Print a 16-bit value as HEX or decimal +void systemPrint(uint16_t value, uint8_t printType) { - delay(10); //Wait for any incoming chars to hit buffer - while (Serial.available() > 0) Serial.read(); //Clear buffer + char temp[20]; - //Get input from user - char cleansed[20]; //Good for very large numbers: 123,456,789,012,345,678\0 + if (printType == HEX) + snprintf(temp, sizeof(temp), "%04X", value); + else if (printType == DEC) + snprintf(temp, sizeof(temp), "%d", value); - long startTime = millis(); - int spot = 0; - while (spot < 20 - 1) //Leave room for terminating \0 - { - while (Serial.available() == 0) //Wait for user input + systemPrint(temp); +} + +// Print a 16-bit value as HEX or decimal with a carriage return and linefeed +void systemPrintln(uint16_t value, uint8_t printType) +{ + systemPrint(value, printType); + systemPrintln(); +} + +// Print a floating point value with a specified number of decimal places +void systemPrint(float value, uint8_t decimals) +{ + char temp[20]; + snprintf(temp, sizeof(temp), "%.*f", decimals, value); + systemPrint(temp); +} + +// Print a floating point value with a specified number of decimal places and a +// carriage return and linefeed +void systemPrintln(float value, uint8_t decimals) +{ + systemPrint(value, decimals); + systemPrintln(); +} + +// Print a double precision floating point value with a specified number of decimal places +void systemPrint(double value, uint8_t decimals) +{ + char temp[30]; + snprintf(temp, sizeof(temp), "%.*f", decimals, value); + systemPrint(temp); +} + +// Print a double precision floating point value with a specified number of decimal +// places and a carriage return and linefeed +void systemPrintln(double value, uint8_t decimals) +{ + systemPrint(value, decimals); + systemPrintln(); +} + +// Print a string +void systemPrint(String myString) +{ + systemPrint(myString.c_str()); +} +void systemPrintln(String myString) +{ + systemPrint(myString); + systemPrintln(); +} + +// Print a carriage return and linefeed +void systemPrintln() +{ + systemPrint("\r\n"); +} + +// Option not known +void printUnknown(uint8_t unknownChoice) +{ + systemPrint("Unknown choice: "); + systemWrite(unknownChoice); + systemPrintln(); +} +void printUnknown(int unknownValue) +{ + systemPrint("Unknown value: "); + systemPrintln((uint16_t)unknownValue, DEC); +} + +// Clear the Serial/Bluetooth RX buffer before we begin scanning for characters +void clearBuffer() +{ + systemFlush(); + delay(20); // Wait for any incoming chars to hit buffer + while (systemAvailable() > 0) + systemRead(); // Clear buffer +} + +// Gathers raw characters from user until \n or \r is received +// Handles backspace +// Used for raw mixed entry (SSID, pws, etc) +// Used by other menu input methods that use sscanf +// Returns INPUT_RESPONSE_TIMEOUT, INPUT_RESPONSE_OVERFLOW, INPUT_RESPONSE_EMPTY, or INPUT_RESPONSE_VALID +InputResponse getString(char *userString, uint8_t stringSize) +{ + clearBuffer(); + + long startTime = millis(); + uint8_t spot = 0; + + while ((millis() - startTime) / 1000 <= menuTimeout) { - delay(10); //Yield to processor - i2cGNSS.checkUblox(); //Regularly poll to get latest data + delay(1); // Yield to processor - if ( (millis() - startTime) / 1000 >= numberOfSeconds) - { - if (spot == 0) + //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + // Keep doing these important things while waiting for the user to enter data + + // Regularly poll GNSS to get latest data. Keep the GNSS time updated. + if (online.gnss == true) { - Serial.println(F("No user input received. Do you have line endings turned on?")); - return (STATUS_GETNUMBER_TIMEOUT); //Timeout. No user input. + theGNSS.checkUblox(); + theGNSS.checkCallbacks(); } - else if (spot > 0) + + // Keep processing NTP requests + if (online.NTPServer) { - break; //Timeout, but we have data + ntpServerUpdate(); } - } - } - //See if we timed out waiting for a line ending - if (spot > 0 && (millis() - startTime) / 1000 >= numberOfSeconds) - { - Serial.println(F("Do you have line endings turned on?")); - break; //Timeout, but we have data + //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + + if (btPrintEchoExit) // User has disconnected from BT. Force exit all menus. + return INPUT_RESPONSE_TIMEOUT; + + // Get the next input character + while (systemAvailable() > 0) + { + byte incoming = systemRead(); + + if ((incoming == '\r') || (incoming == '\n')) + { + if (settings.echoUserInput) + systemPrintln(); // Echo if needed + userString[spot] = '\0'; // Null terminate + + if (spot == 0) + return INPUT_RESPONSE_EMPTY; + + return INPUT_RESPONSE_VALID; + } + // Handle backspace + else if (incoming == '\b') + { + if (settings.echoUserInput == true && spot > 0) + { + systemWrite('\b'); // Move back one space + systemWrite(' '); // Put a blank there to erase the letter from the terminal + systemWrite('\b'); // Move back again + spot--; + } + } + else + { + if (settings.echoUserInput) + systemWrite(incoming); // Echo if needed + + userString[spot++] = incoming; + if (spot == stringSize) // Leave room for termination + return INPUT_RESPONSE_OVERFLOW; + } + } } - byte incoming = Serial.read(); - if (incoming == '\n' || incoming == '\r') + return INPUT_RESPONSE_TIMEOUT; +} + +// Get a valid IP Address (nnn.nnn.nnn.nnn) using getString +// Returns INPUT_RESPONSE_TIMEOUT, INPUT_RESPONSE_OVERFLOW, INPUT_RESPONSE_EMPTY, INPUT_RESPONSE_INVALID or +// INPUT_RESPONSE_VALID +InputResponse getIPAddress(char *userString, uint8_t stringSize) +{ + InputResponse result = getString(userString, stringSize); + if (result != INPUT_RESPONSE_VALID) + return result; + int dummy[4]; + if (sscanf(userString, "%d.%d.%d.%d", &dummy[0], &dummy[1], &dummy[2], &dummy[3]) != + 4) // Check that the user has entered nnn.nnn.nnn.nnn + return INPUT_RESPONSE_INVALID; + for (int i = 0; i <= 3; i++) { - Serial.println(); - break; + if ((dummy[i] < 0) || (dummy[i] > 255)) // Check each value is 0-255 + return INPUT_RESPONSE_INVALID; } + return INPUT_RESPONSE_VALID; +} - if ((isDigit(incoming) == true) || ((incoming == '-') && (spot == 0))) // Check for digits and a minus sign +// Gets a single character or number (0-32) from the user. Negative numbers become the positive equivalent. +// Numbers larger than 32 are allowed but will be confused with characters: ie, 74 = 'J'. +// Returns 255 if timeout +// Returns 0 if no data, only carriage return or newline +byte getCharacterNumber() +{ + char userEntry[50]; // Allow user to enter more than one char. sscanf will remove extra. + int userByte = 0; + + InputResponse response = getString(userEntry, sizeof(userEntry)); + if (response == INPUT_RESPONSE_VALID) { - Serial.write(incoming); //Echo user's typing - cleansed[spot++] = (char)incoming; + int filled = sscanf(userEntry, "%d", &userByte); + if (filled == 0) // Not a number + sscanf(userEntry, "%c", (byte *)&userByte); + else + { + if (userByte == 255) + userByte = 0; // Not allowed + else if (userByte > 128) + userByte *= -1; // Drop negative sign + } } - - if (incoming == 'x') + else if (response == INPUT_RESPONSE_TIMEOUT) { - return (STATUS_PRESSED_X); + systemPrintln("\r\nNo user response - Do you have line endings turned on?"); + userByte = 255; // Timeout } - } - - cleansed[spot] = '\0'; - - int64_t largeNumber = 0; - int x = 0; - if (cleansed[0] == '-') // If our number is negative - { - x = 1; // Skip the minus - } - for ( ; x < spot ; x++) - { - largeNumber *= 10; - largeNumber += (cleansed[x] - '0'); - } - if (cleansed[0] == '-') // If our number is negative - { - largeNumber = 0 - largeNumber; // Make it negative - } - return (largeNumber); -} - -//Get a string/value from user, remove all non-numeric values -//Returns STATUS_GETNUMBER_TIMEOUT if input times out -//Returns STATUS_PRESSED_X if user presses 'x' -double getDouble(int numberOfSeconds) -{ - delay(10); //Wait for any incoming chars to hit buffer - while (Serial.available() > 0) Serial.read(); //Clear buffer - - //Get input from user - char cleansed[20]; //Good for very large numbers: 123,456,789,012,345,678\0 - - long startTime = millis(); - int spot = 0; - bool dpSeen = false; - while (spot < 20 - 1) //Leave room for terminating \0 - { - while (Serial.available() == 0) //Wait for user input + else if (response == INPUT_RESPONSE_EMPTY) { - delay(10); //Yield to processor - i2cGNSS.checkUblox(); //Regularly poll to get latest data - - if ( (millis() - startTime) / 1000 >= numberOfSeconds) - { - if (spot == 0) - { - Serial.println(F("No user input received. Do you have line endings turned on?")); - return (STATUS_GETNUMBER_TIMEOUT); //Timeout. No user input. - } - else if (spot > 0) - { - break; //Timeout, but we have data - } - } + userByte = 0; // Empty } - //See if we timed out waiting for a line ending - if (spot > 0 && (millis() - startTime) / 1000 >= numberOfSeconds) + return userByte; +} + +// Get a long int from user, uses sscanf to obtain 64-bit int +// Returns INPUT_RESPONSE_GETNUMBER_EXIT if user presses 'x' or doesn't enter data +// Returns INPUT_RESPONSE_GETNUMBER_TIMEOUT if input times out +long getNumber() +{ + char userEntry[50]; // Allow user to enter more than one char. sscanf will remove extra. + long userNumber = 0; + + InputResponse response = getString(userEntry, sizeof(userEntry)); + if (response == INPUT_RESPONSE_VALID) { - Serial.println(F("Do you have line endings turned on?")); - break; //Timeout, but we have data + if (strcmp(userEntry, "x") == 0 || strcmp(userEntry, "X") == 0) + userNumber = INPUT_RESPONSE_GETNUMBER_EXIT; + else + sscanf(userEntry, "%ld", &userNumber); } - - byte incoming = Serial.read(); - if (incoming == '\n' || incoming == '\r') + else if (response == INPUT_RESPONSE_TIMEOUT) { - Serial.println(); - break; + systemPrintln("\r\nNo user response - Do you have line endings turned on?"); + userNumber = INPUT_RESPONSE_GETNUMBER_TIMEOUT; // Timeout } - - if ((isDigit(incoming) == true) || ((incoming == '-') && (spot == 0)) || ((incoming == '.') && (dpSeen == false))) // Check for digits/minus/dp + else if (response == INPUT_RESPONSE_EMPTY) { - Serial.write(incoming); //Echo user's typing - cleansed[spot++] = (char)incoming; + userNumber = INPUT_RESPONSE_GETNUMBER_EXIT; // Empty } - if (incoming == '.') - dpSeen = true; + return userNumber; +} + +// Gets a double (float) from the user +// Returns 0 for timeout and empty response +double getDouble() +{ + char userEntry[50]; + double userFloat = 0.0; - if (incoming == 'x') + InputResponse response = getString(userEntry, sizeof(userEntry)); + if (response == INPUT_RESPONSE_VALID) + sscanf(userEntry, "%lf", &userFloat); + else if (response == INPUT_RESPONSE_TIMEOUT) { - return (STATUS_PRESSED_X); + systemPrintln("No user response - Do you have line endings turned on?"); + userFloat = 0.0; } - } - - cleansed[spot] = '\0'; - - double largeNumber = 0; - int x = 0; - if (cleansed[0] == '-') // If our number is negative - { - x = 1; // Skip the minus - } - for ( ; x < spot ; x++) - { - if (cleansed[x] == '.') - break; - largeNumber *= 10; - largeNumber += (cleansed[x] - '0'); - } - if (x < spot) // Check if we found a '.' - { - x++; - double divider = 0.1; - for ( ; x < spot ; x++) + else if (response == INPUT_RESPONSE_EMPTY) + { + userFloat = 0.0; + } + + return userFloat; +} + +void printElapsedTime(const char *title) +{ + systemPrintf("%s: %ld\r\n", title, millis() - startTime); +} + +void printDebug(String thingToPrint) +{ + if (settings.printDebugMessages == true) { - largeNumber += (cleansed[x] - '0') * divider; - divider /= 10; + systemPrint(thingToPrint); } - } - if (cleansed[0] == '-') // If our number is negative - { - largeNumber = 0 - largeNumber; // Make it negative - } - return (largeNumber); -} - -//Reads a line until the \n enter character is found -//Returns STATUS_GETBYTE_TIMEOUT if input times out -//Returns STATUS_PRESSED_X if user presses 'x' -byte readLine(char* buffer, byte bufferLength, int numberOfSeconds) -{ - byte readLength = 0; - long startTime = millis(); - while (readLength < bufferLength - 1) - { - //See if we timed out waiting for a line ending - if (readLength > 0 && (millis() - startTime) / 1000 >= numberOfSeconds) +} + +#define TIMESTAMP_INTERVAL 1000 // Milliseconds + +// Print the timestamp +void printTimeStamp() +{ + uint32_t currentMilliseconds; + static uint32_t previousMilliseconds; + + // Timestamp the messages + currentMilliseconds = millis(); + if ((currentMilliseconds - previousMilliseconds) >= TIMESTAMP_INTERVAL) { - Serial.println(F("Do you have line endings turned on?")); - break; //Timeout, but we have data + // 1 2 3 + // 123456789012345678901234567890 + // YYYY-mm-dd HH:MM:SS.xxxrn0 + struct tm timeinfo = rtc.getTimeStruct(); + char timestamp[30]; + strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", &timeinfo); + systemPrintf("%s.%03ld\r\n", timestamp, rtc.getMillis()); + + // Select the next time to display the timestamp + previousMilliseconds = currentMilliseconds; } +} - while (Serial.available() == 0) //Wait for user input +// Parse the RTCM transport data +// Called by processRTCM in Base.ino - defines whether the data is passed to the NTRIP server +bool checkRtcmMessage(uint8_t data) +{ + static uint16_t bytesRemaining; + static uint16_t length; + static uint16_t message; + static bool sendMessage = false; + + // RTCM Standard 10403.2 - Chapter 4, Transport Layer + // + // |<------------- 3 bytes ------------>|<----- length ----->|<- 3 bytes ->| + // | | | | + // +----------+--------+----------------+---------+----------+-------------+ + // | Preamble | Fill | Message Length | Message | Fill | CRC-24Q | + // | 8 bits | 6 bits | 10 bits | n-bits | 0-7 bits | 24 bits | + // | 0xd3 | 000000 | (in bytes) | | zeros | | + // +----------+--------+----------------+---------+----------+-------------+ + // | | + // |<------------------------ CRC -------------------------->| + // + + switch (rtcmParsingState) { - delay(10); //Yield to processor - i2cGNSS.checkUblox(); //Regularly poll to get latest data + // Read the upper two bits of the length + case RTCM_TRANSPORT_STATE_READ_LENGTH_1: + // Verify the length byte - check the 6 MS bits are all zero + if (!(data & (~3))) + { + length = data << 8; + rtcmParsingState = RTCM_TRANSPORT_STATE_READ_LENGTH_2; + break; + } - if ( (millis() - startTime) / 1000 >= numberOfSeconds) - { - if (readLength == 0) + // Wait for the preamble byte + rtcmParsingState = RTCM_TRANSPORT_STATE_WAIT_FOR_PREAMBLE_D3; + + // Fall through + // | + // | + // V + + // Wait for the preamble byte (0xd3) + case RTCM_TRANSPORT_STATE_WAIT_FOR_PREAMBLE_D3: + sendMessage = false; + if (data == 0xd3) { - Serial.println(F("No user input received. Do you have line endings turned on?")); - return (STATUS_GETBYTE_TIMEOUT); //Timeout. No user input. + rtcmParsingState = RTCM_TRANSPORT_STATE_READ_LENGTH_1; + sendMessage = true; } - else if (readLength > 0) + break; + + // Read the lower 8 bits of the length + case RTCM_TRANSPORT_STATE_READ_LENGTH_2: + length |= data; + bytesRemaining = length; + rtcmParsingState = RTCM_TRANSPORT_STATE_READ_MESSAGE_1; + break; + + // Read the upper 8 bits of the message number + case RTCM_TRANSPORT_STATE_READ_MESSAGE_1: + message = data << 4; + bytesRemaining -= 1; + rtcmParsingState = RTCM_TRANSPORT_STATE_READ_MESSAGE_2; + break; + + // Read the lower 4 bits of the message number + case RTCM_TRANSPORT_STATE_READ_MESSAGE_2: + message |= data >> 4; + bytesRemaining -= 1; + rtcmParsingState = RTCM_TRANSPORT_STATE_READ_DATA; + break; + + // Read the rest of the message + case RTCM_TRANSPORT_STATE_READ_DATA: + bytesRemaining -= 1; + if (bytesRemaining <= 0) + rtcmParsingState = RTCM_TRANSPORT_STATE_READ_CRC_1; + break; + + // Read the upper 8 bits of the CRC + case RTCM_TRANSPORT_STATE_READ_CRC_1: + rtcmParsingState = RTCM_TRANSPORT_STATE_READ_CRC_2; + break; + + // Read the middle 8 bits of the CRC + case RTCM_TRANSPORT_STATE_READ_CRC_2: + rtcmParsingState = RTCM_TRANSPORT_STATE_READ_CRC_3; + break; + + // Read the lower 8 bits of the CRC + case RTCM_TRANSPORT_STATE_READ_CRC_3: + rtcmParsingState = RTCM_TRANSPORT_STATE_CHECK_CRC; + break; + } + + // Check the CRC. Note: this doesn't actually check the CRC! + if (rtcmParsingState == RTCM_TRANSPORT_STATE_CHECK_CRC) + { + rtcmParsingState = RTCM_TRANSPORT_STATE_WAIT_FOR_PREAMBLE_D3; + + // Display the RTCM message header + if (settings.debugNtripServerRtcm && (!inMainMenu)) { - break; //Timeout, but we have data + printTimeStamp(); + systemPrintf(" Tx RTCM %d, %2d bytes\r\n", message, 3 + length + 3); } - } } - byte incoming = Serial.read(); + // Let the upper layer know if this message should be sent + return sendMessage; +} - if (incoming == 0x08 || incoming == 0x7F) //Support backspace characters - { - if (readLength < 1) - continue; +const double WGS84_A = 6378137; // https://geographiclib.sourceforge.io/html/Constants_8hpp_source.html +const double WGS84_E = 0.081819190842622; // http://docs.ros.org/en/hydro/api/gps_common/html/namespacegps__common.html + // and https://gist.github.com/uhho/63750c4b54c7f90f37f958cc8af0c718 - readLength--; - buffer[readLength] = '\0'; //Put a terminator on the string in case we are finished +// From: https://stackoverflow.com/questions/19478200/convert-latitude-and-longitude-to-ecef-coordinates-system +void geodeticToEcef(double lat, double lon, double alt, double *x, double *y, double *z) +{ + double clat = cos(lat * DEG_TO_RAD); + double slat = sin(lat * DEG_TO_RAD); + double clon = cos(lon * DEG_TO_RAD); + double slon = sin(lon * DEG_TO_RAD); - Serial.print((char)0x08); //Move back one space - Serial.print(F(" ")); //Put a blank there to erase the letter from the terminal - Serial.print((char)0x08); //Move back again + double N = WGS84_A / sqrt(1.0 - WGS84_E * WGS84_E * slat * slat); - continue; - } + *x = (N + alt) * clat * clon; + *y = (N + alt) * clat * slon; + *z = (N * (1.0 - WGS84_E * WGS84_E) + alt) * slat; +} - Serial.write(incoming); //Echo +// From: https://danceswithcode.net/engineeringnotes/geodetic_to_ecef/geodetic_to_ecef.html +void ecefToGeodetic(double x, double y, double z, double *lat, double *lon, double *alt) +{ + double a = 6378137.0; // WGS-84 semi-major axis + double e2 = 6.6943799901377997e-3; // WGS-84 first eccentricity squared + double a1 = 4.2697672707157535e+4; // a1 = a*e2 + double a2 = 1.8230912546075455e+9; // a2 = a1*a1 + double a3 = 1.4291722289812413e+2; // a3 = a1*e2/2 + double a4 = 4.5577281365188637e+9; // a4 = 2.5*a2 + double a5 = 4.2840589930055659e+4; // a5 = a1+a3 + double a6 = 9.9330562000986220e-1; // a6 = 1-e2 + + double zp, w2, w, r2, r, s2, c2, s, c, ss; + double g, rg, rf, u, v, m, f, p; + + zp = abs(z); + w2 = x * x + y * y; + w = sqrt(w2); + r2 = w2 + z * z; + r = sqrt(r2); + *lon = atan2(y, x); // Lon (final) + + s2 = z * z / r2; + c2 = w2 / r2; + u = a2 / r; + v = a3 - a4 / r; + if (c2 > 0.3) + { + s = (zp / r) * (1.0 + c2 * (a1 + u + s2 * v) / r); + *lat = asin(s); // Lat + ss = s * s; + c = sqrt(1.0 - ss); + } + else + { + c = (w / r) * (1.0 - s2 * (a5 - u - c2 * v) / r); + *lat = acos(c); // Lat + ss = 1.0 - c * c; + s = sqrt(ss); + } - if (incoming == '\r' || incoming == '\n') + g = 1.0 - e2 * ss; + rg = a / sqrt(g); + rf = a6 * rg; + u = w - rg * c; + v = zp - rf * s; + f = c * u + s * v; + m = c * v - s * u; + p = m / (rf / g + f); + *lat = *lat + p; // Lat + *alt = f + m * p / 2.0; // Altitude + if (z < 0.0) { - Serial.println(); - buffer[readLength] = '\0'; - break; + *lat *= -1.0; // Lat } - else if (readLength == 0 && incoming == 'x') + + *lat *= RAD_TO_DEG; // Convert to degrees + *lon *= RAD_TO_DEG; +} + +// Convert nibble to ASCII +uint8_t nibbleToAscii(int nibble) +{ + nibble &= 0xf; + return (nibble > 9) ? nibble + 'a' - 10 : nibble + '0'; +} + +// Convert nibble to ASCII +int AsciiToNibble(int data) +{ + // Convert the value to lower case + data |= 0x20; + if ((data >= 'a') && (data <= 'f')) + return data - 'a' + 10; + if ((data >= '0') && (data <= '9')) + return data - '0'; + return -1; +} + +void dumpBuffer(uint8_t *buffer, uint16_t length) +{ + int bytes; + uint8_t *end; + int index; + uint16_t offset; + + end = &buffer[length]; + offset = 0; + while (buffer < end) { - return (STATUS_PRESSED_X); + // Determine the number of bytes to display on the line + bytes = end - buffer; + if (bytes > (16 - (offset & 0xf))) + bytes = 16 - (offset & 0xf); + + // Display the offset + systemPrintf("0x%08lx: ", offset); + + // Skip leading bytes + for (index = 0; index < (offset & 0xf); index++) + systemPrintf(" "); + + // Display the data bytes + for (index = 0; index < bytes; index++) + systemPrintf("%02x ", buffer[index]); + + // Separate the data bytes from the ASCII + for (; index < (16 - (offset & 0xf)); index++) + systemPrintf(" "); + systemPrintf(" "); + + // Skip leading bytes + for (index = 0; index < (offset & 0xf); index++) + systemPrintf(" "); + + // Display the ASCII values + for (index = 0; index < bytes; index++) + systemPrintf("%c", ((buffer[index] < ' ') || (buffer[index] >= 0x7f)) ? '.' : buffer[index]); + systemPrintf("\r\n"); + + // Set the next line of data + buffer += bytes; + offset += bytes; } +} + +// Make size of files human readable +void stringHumanReadableSize(String &returnText, uint64_t bytes) +{ + char suffix[5] = {'\0'}; + char readableSize[50] = {'\0'}; + float cardSize = 0.0; + + if (bytes < 1024) + strcpy(suffix, "B"); + else if (bytes < (1024 * 1024)) + strcpy(suffix, "KB"); + else if (bytes < (1024 * 1024 * 1024)) + strcpy(suffix, "MB"); + else + strcpy(suffix, "GB"); + + if (bytes < (1024)) + cardSize = bytes; // B + else if (bytes < (1024 * 1024)) + cardSize = bytes / 1024.0; // KB + else if (bytes < (1024 * 1024 * 1024)) + cardSize = bytes / 1024.0 / 1024.0; // MB else + cardSize = bytes / 1024.0 / 1024.0 / 1024.0; // GB + + if (strcmp(suffix, "GB") == 0) + snprintf(readableSize, sizeof(readableSize), "%0.1f %s", cardSize, suffix); // Print decimal portion + else if (strcmp(suffix, "MB") == 0) + snprintf(readableSize, sizeof(readableSize), "%0.1f %s", cardSize, suffix); // Print decimal portion + else if (strcmp(suffix, "KB") == 0) + snprintf(readableSize, sizeof(readableSize), "%0.1f %s", cardSize, suffix); // Print decimal portion + else + snprintf(readableSize, sizeof(readableSize), "%.0f %s", cardSize, suffix); // Don't print decimal portion + + returnText = String(readableSize); +} + +// Print the NMEA checksum error +void printNmeaChecksumError(PARSE_STATE *parse) +{ + printTimeStamp(); + systemPrintf(" %s NMEA %s, %2d bytes, bad checksum, expecting 0x%c%c, computed: 0x%02x\r\n", + parse->parserName, parse->nmeaMessageName, parse->length, parse->buffer[parse->nmeaLength - 2], + parse->buffer[parse->nmeaLength - 1], parse->crc); +} + +// Print the RTCM checksum error +void printRtcmChecksumError(PARSE_STATE *parse) +{ + printTimeStamp(); + systemPrintf(" %s RTCM %d, %2d bytes, bad CRC, expecting 0x%02x%02x%02x, computed: 0x%06x\r\n", + parse->parserName, parse->message, parse->length, parse->buffer[parse->length - 3], + parse->buffer[parse->length - 2], parse->buffer[parse->length - 1], parse->rtcmCrc); +} + +// Print the RTCM maximum length +void printRtcmMaxLength(PARSE_STATE *parse) +{ + systemPrintf("RTCM parser error maxLength: %d bytes\r\n", parse->maxLength); +} + +// Print the u-blox checksum error +void printUbloxChecksumError(PARSE_STATE *parse) +{ + printTimeStamp(); + systemPrintf(" %s u-blox %d.%d, %2d bytes, bad checksum, expecting 0x%02X%02X, computed: 0x%02X%02X\r\n", + parse->parserName, parse->message >> 8, parse->message & 0xff, parse->length, + parse->buffer[parse->nmeaLength - 2], parse->buffer[parse->nmeaLength - 1], parse->ck_a, + parse->ck_b); +} + +// Print the u-blox invalid data error +void printUbloxInvalidData(PARSE_STATE *parse) +{ + dumpBuffer(parse->buffer, parse->length - 1); + systemPrintf(" %s Invalid UBX data, %d bytes\r\n", parse->parserName, parse->length - 1); +} + +void printPartitionTable(void) +{ + systemPrintln("ESP32 Partition table:\n"); + + systemPrintln("| Type | Sub | Offset | Size | Label |"); + systemPrintln("| ---- | --- | -------- | -------- | ---------------- |"); + + esp_partition_iterator_t pi = esp_partition_find(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, NULL); + if (pi != NULL) + { + do + { + const esp_partition_t *p = esp_partition_get(pi); + systemPrintf("| %02x | %02x | 0x%06X | 0x%06X | %-16s |\r\n", p->type, p->subtype, p->address, p->size, + p->label); + } while ((pi = (esp_partition_next(pi)))); + } +} + +// Locate the partition for the little file system +bool findSpiffsPartition(void) +{ + esp_partition_iterator_t pi = esp_partition_find(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, NULL); + if (pi != NULL) { - buffer[readLength] = incoming; - readLength++; + do + { + const esp_partition_t *p = esp_partition_get(pi); + if (strcmp(p->label, "spiffs") == 0) + return true; + } while ((pi = (esp_partition_next(pi)))); } - } + return false; +} - return readLength; +// Verify table sizes match enum definitions +void verifyTables() +{ + // Verify the product name table + if (productDisplayNamesEntries != (RTK_UNKNOWN + 1)) + reportFatalError("Fix productDisplayNames to match ProductVariant"); + if (platformFilePrefixTableEntries != (RTK_UNKNOWN + 1)) + reportFatalError("Fix platformFilePrefixTable to match ProductVariant"); + if (platformPrefixTableEntries != (RTK_UNKNOWN + 1)) + reportFatalError("Fix platformPrefixTable to match ProductVariant"); + + // Verify the consistency of the internal tables + ethernetVerifyTables(); + networkVerifyTables(); + ntpValidateTables(); + ntripClientValidateTables(); + ntripServerValidateTables(); + otaVerifyTables(); + pvtClientValidateTables(); + pvtServerValidateTables(); + tasksValidateTables(); } diff --git a/Firmware/Test Sketches/BLE_Set_Config/BLE_Set_Config.ino b/Firmware/Test Sketches/BLE_Set_Config/BLE_Set_Config.ino deleted file mode 100644 index 6c7e9558d..000000000 --- a/Firmware/Test Sketches/BLE_Set_Config/BLE_Set_Config.ino +++ /dev/null @@ -1,189 +0,0 @@ -/* - Set a baud rate to 115200bps over BLE - - Adding descriptor - https://github.com/espressif/arduino-esp32/blob/master/libraries/BLE/src/BLECharacteristic.h - https://gist.github.com/heiko-r/f284d95141871e12ca0164d9070d61b4 - Roughly working Characteristic descriptor: https://github.com/espressif/arduino-esp32/issues/1038 - - Float to string: https://iotbyhvm.ooo/esp32-ble-tutorials/ - - Custom UUIDs? https://www.bluetooth.com/specifications/assigned-numbers/ - - ESP32 with chrome: https://github.com/kpatel122/ESP32-Web-Bluetooth-Terminal/blob/master/ESP32-BLE/ESP32-BLE.ino -*/ - -#include -#include -#include - -// See the following for generating UUIDs: -// https://www.uuidgenerator.net/ - - -#define BLE_BROADCAST_NAME "OpenLog" - -#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b" - -#define CHARACTERISTIC_UUID_LED "beb5483e-36e1-4688-b7f5-ea07361b26a9" -BLECharacteristic *ledCharacteristic; -bool ledState = 1; - -#define CHARACTERISTIC_UUID_BAUD "beb5483e-36e1-4688-b7f5-ea07361b26a8" -BLECharacteristic *baudCharacteristic; -int baudRate = 9600; - -#define CHARACTERISTIC_UUID_QWIIC_PWR_DOWN "beb5483e-36e1-4688-b7f5-ea07361b26ab" -BLECharacteristic *qwiicPowerCharacteristic; -bool qwiicPwrDown = 1; - -bool deviceConnected = false; -bool newConfig = true; - -class MyServerCallbacks: public BLEServerCallbacks { - void onConnect(BLEServer* pServer) { - Serial.println("Connect!"); - deviceConnected = true; - }; - - void onDisconnect(BLEServer* pServer) { - Serial.println("Disconnect"); - deviceConnected = false; - } -}; - -//4 bytes come in but they are little endian. Flip them around. -//Convert a std string to a int -int32_t stringToValue(std::string myString) -{ - int newValue = 0; - for (int i = myString.length() ; i > 0 ; i--) - { - newValue <<= 8; - newValue |= (myString[i - 1]); - } - - return (newValue); -} - -class cbSetBaud: public BLECharacteristicCallbacks { - void onWrite(BLECharacteristic *baudCharacteristic) - { - baudRate = stringToValue(baudCharacteristic->getValue()); - newConfig = true; - } -}; - -class cbSetLedState: public BLECharacteristicCallbacks { - void onWrite(BLECharacteristic *ledCharacteristic) - { - ledState = stringToValue(ledCharacteristic->getValue()); - newConfig = true; - } -}; - -class cbSetQwiicPower: public BLECharacteristicCallbacks { - void onWrite(BLECharacteristic *qwiicPowerCharacteristic) - { - qwiicPwrDown = stringToValue(qwiicPowerCharacteristic->getValue()); - newConfig = true; - } -}; - -void setup() { - Serial.begin(115200); - Serial.println("Starting BLE work!"); - - BLEDevice::init(BLE_BROADCAST_NAME); - - BLEServer *pServer = BLEDevice::createServer(); - pServer->setCallbacks(new MyServerCallbacks()); - BLEService *pService = pServer->createService(SERVICE_UUID); - - //Setup characterstics - - baudCharacteristic = pService->createCharacteristic( - CHARACTERISTIC_UUID_BAUD, - BLECharacteristic::PROPERTY_READ | - BLECharacteristic::PROPERTY_WRITE - ); - baudCharacteristic->setValue((uint8_t *)&baudRate, 4); - BLEDescriptor *pDescriptor3 = new BLEDescriptor((uint16_t)0x2901); // Characteristic User Description - baudCharacteristic->addDescriptor(pDescriptor3); - pDescriptor3->setValue("USB Interface Baud Rate"); - baudCharacteristic->setCallbacks(new cbSetBaud()); - - ledCharacteristic = pService->createCharacteristic( - CHARACTERISTIC_UUID_LED, - BLECharacteristic::PROPERTY_READ | - BLECharacteristic::PROPERTY_WRITE - ); - ledCharacteristic->setValue((uint8_t *)&ledState, 1); - ledCharacteristic->setCallbacks(new cbSetLedState()); - - - qwiicPowerCharacteristic = pService->createCharacteristic( - CHARACTERISTIC_UUID_QWIIC_PWR_DOWN, - BLECharacteristic::PROPERTY_READ | - BLECharacteristic::PROPERTY_WRITE - ); - qwiicPowerCharacteristic->setValue((uint8_t *)&qwiicPwrDown, 1); - qwiicPowerCharacteristic->setCallbacks(new cbSetQwiicPower()); - - - //Begin broadcasting - pService->start(); - BLEAdvertising *pAdvertising = BLEDevice::getAdvertising(); - pAdvertising->addServiceUUID(SERVICE_UUID); - pAdvertising->setScanResponse(true); - pAdvertising->setMinPreferred(0x06); // functions that help with iPhone connections issue - pAdvertising->setMinPreferred(0x12); - BLEDevice::startAdvertising(); - - Serial.println("BLE Started"); -} - -void loop() { - delay(200); - - if (newConfig == true) - { - newConfig = false; - - Serial.print("Baud rate:"); - Serial.println(baudRate); - - Serial.print("LED State:"); - Serial.println(ledState); - - if (qwiicPwrDown == true) - Serial.println("Qwiic Power On"); - else - Serial.println("Qwiic Power Off"); - - } - - if (Serial.available()) - { - byte incoming = Serial.read(); - - if (incoming == '1') - { - //baudCharacteristic->setValue("Value 1"); - Serial.println("Val set"); - } - else if (incoming == '2') - { - //baudCharacteristic->setValue("Value 2"); - Serial.println("Val set"); - } - else - { - Serial.println("Unknown"); - } - - delay(10); - while (Serial.available()) Serial.read(); //Clear buffer - - } -} diff --git a/Firmware/Test Sketches/Batt_Monitor/Batt_Monitor.ino b/Firmware/Test Sketches/Batt_Monitor/Batt_Monitor.ino deleted file mode 100644 index 2015cdc33..000000000 --- a/Firmware/Test Sketches/Batt_Monitor/Batt_Monitor.ino +++ /dev/null @@ -1,38 +0,0 @@ -/* - Red = 34 - Green = 35 - Blue = Used for Charging - - Battery level LED goes from Green (50-100%) to Yellow (10-50%) to Red (<10%) -*/ - -#include "MAX17048.h" //Click here to get the library: http://librarymanager/All#MAX17048 -MAX17048 battMonitor; - -const int batteryLevelLED_Red = 34; -const int batteryLevelLED_Green = 35; - -// setting PWM properties -const int freq = 5000; -const int ledChannel = 0; -const int resolution = 8; - -void setup() -{ - Serial.begin(115200); - Wire.begin(); - - battMonitor.attatch(Wire); - - ledcSetup(ledChannel, freq, resolution); - ledcAttachPin(batteryLevelLED_Red, ledChannel); - ledcAttachPin(batteryLevelLED_Green, ledChannel); - - xTaskCreate(batteryLEDTask, "batteryLEDs", 1000, NULL, 0, NULL); //1000 stack, 0 = Low priority -} - -void loop() -{ - Serial.println("."); - delay(1000); -} diff --git a/Firmware/Test Sketches/Batt_Monitor/system.ino b/Firmware/Test Sketches/Batt_Monitor/system.ino deleted file mode 100644 index ef564081f..000000000 --- a/Firmware/Test Sketches/Batt_Monitor/system.ino +++ /dev/null @@ -1,41 +0,0 @@ -//Update Battery level LEDs -void batteryLEDTask(void *e) -{ - while (1) - { - int battLevel = battMonitor.percent(); - - Serial.print("Batt ("); - Serial.print(battLevel); - Serial.print("%): "); - - if (battLevel < 10) - { - Serial.print("RED uh oh!"); - ledcWrite(batteryLevelLED_Red, 255); - ledcWrite(batteryLevelLED_Green, 0); - } - else if (battLevel < 50) - { - Serial.print("Yellow ok"); - ledcWrite(batteryLevelLED_Red, 128); - ledcWrite(batteryLevelLED_Green, 128); - } - else if (battLevel >= 50) - { - Serial.print("Green all good"); - ledcWrite(batteryLevelLED_Red, 0); - ledcWrite(batteryLevelLED_Green, 255); - } - else - { - Serial.print("No batt"); - ledcWrite(batteryLevelLED_Red, 0); - ledcWrite(batteryLevelLED_Green, 0); - } - Serial.println(); - - delay(5000); - taskYIELD(); - } -} diff --git a/Firmware/Test Sketches/Battery_Check/Battery_Check.ino b/Firmware/Test Sketches/Battery_Check/Battery_Check.ino new file mode 100644 index 000000000..ff6ca01c9 --- /dev/null +++ b/Firmware/Test Sketches/Battery_Check/Battery_Check.ino @@ -0,0 +1,49 @@ +/* + Basic demonstration of the MAX17048 fuel gauge IC used in the RTK line. + + On the RTK Surveyor, one LED is used to indicate status. Battery level LED goes + from Green (50-100%) to Yellow (10-50%) to Red (<10%) +*/ + +//Battery fuel gauge and PWM LEDs +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +#include // Click here to get the library: http://librarymanager/All#SparkFun_MAX1704x_Fuel_Gauge_Arduino_Library +SFE_MAX1704X lipo(MAX1704X_MAX17048); + +// setting PWM properties +const int pwmFreq = 5000; +const int ledRedChannel = 0; +const int ledGreenChannel = 1; +const int ledBTChannel = 2; +const int pwmResolution = 8; + +int pwmFadeAmount = 10; +int btFadeLevel = 0; + +int battLevel = 0; //SOC measured from fuel gauge, in %. Used in multiple places (display, serial debug, log) +float battVoltage = 0.0; +float battChangeRate = 0.0; +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +uint32_t lastBattUpdate = 0; + +void setup() +{ + Serial.begin(115200); + delay(500); + Serial.println("Battery example"); + + Wire.begin(); + + beginLEDs(); //LED and PWM setup + + beginFuelGauge(); //Configure battery fuel guage monitor + checkBatteryLevels(); //Force check so you see battery level immediately at power on +} + +void loop() +{ + updateBattLEDs(); + Serial.println("."); + delay(1000); +} diff --git a/Firmware/Test Sketches/Battery_Check/Begin.ino b/Firmware/Test Sketches/Battery_Check/Begin.ino new file mode 100644 index 000000000..7de47130a --- /dev/null +++ b/Firmware/Test Sketches/Battery_Check/Begin.ino @@ -0,0 +1,50 @@ +//Configure the on board MAX17048 fuel gauge +void beginFuelGauge() +{ + // Set up the MAX17048 LiPo fuel gauge + if (lipo.begin() == false) + { + Serial.println(F("MAX17048 not detected. Continuing.")); + return; + } + + //Always use hibernate mode + if (lipo.getHIBRTActThr() < 0xFF) lipo.setHIBRTActThr((uint8_t)0xFF); + if (lipo.getHIBRTHibThr() < 0xFF) lipo.setHIBRTHibThr((uint8_t)0xFF); + + Serial.println(F("MAX17048 configuration complete")); + + //online.battery = true; +} + +//Set LEDs for output and configure PWM +void beginLEDs() +{ +// if (productVariant == RTK_SURVEYOR) +// { +// pinMode(pin_positionAccuracyLED_1cm, OUTPUT); +// pinMode(pin_positionAccuracyLED_10cm, OUTPUT); +// pinMode(pin_positionAccuracyLED_100cm, OUTPUT); +// pinMode(pin_baseStatusLED, OUTPUT); +// pinMode(pin_bluetoothStatusLED, OUTPUT); +// pinMode(pin_setupButton, INPUT_PULLUP); //HIGH = rover, LOW = base +// +// digitalWrite(pin_positionAccuracyLED_1cm, LOW); +// digitalWrite(pin_positionAccuracyLED_10cm, LOW); +// digitalWrite(pin_positionAccuracyLED_100cm, LOW); +// digitalWrite(pin_baseStatusLED, LOW); +// digitalWrite(pin_bluetoothStatusLED, LOW); +// +// ledcSetup(ledRedChannel, pwmFreq, pwmResolution); +// ledcSetup(ledGreenChannel, pwmFreq, pwmResolution); +// ledcSetup(ledBTChannel, pwmFreq, pwmResolution); +// +// ledcAttachPin(pin_batteryLevelLED_Red, ledRedChannel); +// ledcAttachPin(pin_batteryLevelLED_Green, ledGreenChannel); +// ledcAttachPin(pin_bluetoothStatusLED, ledBTChannel); +// +// ledcWrite(ledRedChannel, 0); +// ledcWrite(ledGreenChannel, 0); +// ledcWrite(ledBTChannel, 0); +// } +} diff --git a/Firmware/Test Sketches/Battery_Check/system.ino b/Firmware/Test Sketches/Battery_Check/system.ino new file mode 100644 index 000000000..6033b12e8 --- /dev/null +++ b/Firmware/Test Sketches/Battery_Check/system.ino @@ -0,0 +1,63 @@ +//Update Battery level LEDs every 5s +void updateBattLEDs() +{ + if (millis() - lastBattUpdate > 5000) + { + lastBattUpdate = millis(); + + checkBatteryLevels(); + } +} + +//When called, checks level of battery and updates the LED brightnesses +//And outputs a serial message to USB +void checkBatteryLevels() +{ + battLevel = lipo.getSOC(); + battVoltage = lipo.getVoltage(); + battChangeRate = lipo.getChangeRate(); + + Serial.printf("Batt (%d%%): Voltage: %0.02fV", battLevel, battVoltage); + + char tempStr[25]; + if (battChangeRate > 0) + sprintf(tempStr, "C"); + else + sprintf(tempStr, "Disc"); + Serial.printf(" %sharging: %0.02f%%/hr ", tempStr, battChangeRate); + + if (battLevel < 10) + sprintf(tempStr, "Red"); + else if (battLevel < 50) + sprintf(tempStr, "Yellow"); + else if (battLevel >= 50) + sprintf(tempStr, "Green"); + else + sprintf(tempStr, "No batt"); + + Serial.printf("%s\n\r", tempStr); + +// if (productVariant == RTK_SURVEYOR) +// { +// if (battLevel < 10) +// { +// ledcWrite(ledRedChannel, 255); +// ledcWrite(ledGreenChannel, 0); +// } +// else if (battLevel < 50) +// { +// ledcWrite(ledRedChannel, 128); +// ledcWrite(ledGreenChannel, 128); +// } +// else if (battLevel >= 50) +// { +// ledcWrite(ledRedChannel, 0); +// ledcWrite(ledGreenChannel, 255); +// } +// else +// { +// ledcWrite(ledRedChannel, 10); +// ledcWrite(ledGreenChannel, 0); +// } +// } +} diff --git a/Firmware/Test Sketches/Button_Read/Begin.ino b/Firmware/Test Sketches/Button_Read/Begin.ino new file mode 100644 index 000000000..5a0d05c2c --- /dev/null +++ b/Firmware/Test Sketches/Button_Read/Begin.ino @@ -0,0 +1,130 @@ +//Initial startup functions for GNSS, SD, display, radio, etc + +//Based on hardware features, determine if this is RTK Surveyor or RTK Express hardware +//Must be called after Wire.begin so that we can do I2C tests +void beginBoard() +{ + //Use ADC to check 50% resistor divider + int pin_adc_rtk_facet = 35; + if (analogReadMilliVolts(pin_adc_rtk_facet) > (3300 / 2 * 0.9) && analogReadMilliVolts(pin_adc_rtk_facet) < (3300 / 2 * 1.1)) + { + productVariant = RTK_FACET; + } + else if (isConnected(0x19) == true) //Check for accelerometer + { + productVariant = RTK_EXPRESS; + } + else + { + productVariant = RTK_SURVEYOR; + } + + //Setup hardware pins + if (productVariant == RTK_SURVEYOR) + { + pin_batteryLevelLED_Red = 32; + pin_batteryLevelLED_Green = 33; + pin_positionAccuracyLED_1cm = 2; + pin_positionAccuracyLED_10cm = 15; + pin_positionAccuracyLED_100cm = 13; + pin_baseStatusLED = 4; + pin_bluetoothStatusLED = 12; + pin_setupButton = 5; + pin_microSD_CS = 25; + pin_zed_tx_ready = 26; + pin_zed_reset = 27; + pin_batteryLevel_alert = 36; + + strcpy(platformFilePrefix, "SFE_Surveyor"); + strcpy(platformPrefix, "Surveyor"); + } + else if (productVariant == RTK_EXPRESS || productVariant == RTK_EXPRESS_PLUS) + { + pin_muxA = 2; + pin_muxB = 4; + pin_powerSenseAndControl = 13; + pin_setupButton = 14; + pin_microSD_CS = 25; + pin_dac26 = 26; + pin_powerFastOff = 27; + pin_adc39 = 39; + + pinMode(pin_powerSenseAndControl, INPUT_PULLUP); + pinMode(pin_powerFastOff, INPUT); + + pinMode(pin_setupButton, INPUT_PULLUP); + + if (productVariant == RTK_EXPRESS) + { + strcpy(platformFilePrefix, "SFE_Express"); + strcpy(platformPrefix, "Express"); + } + else if (productVariant == RTK_EXPRESS_PLUS) + { + strcpy(platformFilePrefix, "SFE_Express_Plus"); + strcpy(platformPrefix, "Express Plus"); + } + } + else if (productVariant == RTK_FACET) + { + //v11 + pin_muxA = 2; + pin_muxB = 0; + pin_powerSenseAndControl = 13; + pin_peripheralPowerControl = 14; + pin_microSD_CS = 25; + pin_dac26 = 26; + pin_powerFastOff = 27; + pin_adc39 = 39; + + pin_radio_rx = 33; + pin_radio_tx = 32; + pin_radio_rst = 15; + pin_radio_pwr = 4; + pin_radio_cts = 5; + //pin_radio_rts = 255; //Not implemented + + pinMode(pin_powerSenseAndControl, INPUT_PULLUP); + pinMode(pin_powerFastOff, INPUT); + + pinMode(pin_peripheralPowerControl, OUTPUT); + digitalWrite(pin_peripheralPowerControl, HIGH); //Turn on SD, ZED, etc + + //CTS is active low. ESP32 pin 5 has pullup at POR. We must drive it low. + pinMode(pin_radio_cts, OUTPUT); + digitalWrite(pin_radio_cts, LOW); + + strcpy(platformFilePrefix, "SFE_Facet"); + strcpy(platformPrefix, "Facet"); + } + + Serial.printf("SparkFun RTK %s v%d.%d-%s\r\n", platformPrefix, FIRMWARE_VERSION_MAJOR, FIRMWARE_VERSION_MINOR, __DATE__); +} + +//Depending on platform and previous power down state, set system state +void beginSystemState() +{ + if (productVariant == RTK_SURVEYOR) + { + setupBtn = new Button(pin_setupButton); //Create the button in memory + } + else if (productVariant == RTK_EXPRESS || productVariant == RTK_EXPRESS_PLUS) + { + setupBtn = new Button(pin_setupButton); //Create the button in memory + powerBtn = new Button(pin_powerSenseAndControl); //Create the button in memory + } + else if (productVariant == RTK_FACET) + { + powerBtn = new Button(pin_powerSenseAndControl); //Create the button in memory + } + + //Starts task for monitoring button presses + if (ButtonCheckTaskHandle == NULL) + xTaskCreate( + ButtonCheckTask, + "BtnCheck", //Just for humans + buttonTaskStackSize, //Stack Size + NULL, //Task input parameter + ButtonCheckTaskPriority, + &ButtonCheckTaskHandle); //Task handle +} diff --git a/Firmware/Test Sketches/Button_Read/Button_Read.ino b/Firmware/Test Sketches/Button_Read/Button_Read.ino new file mode 100644 index 000000000..640f8f999 --- /dev/null +++ b/Firmware/Test Sketches/Button_Read/Button_Read.ino @@ -0,0 +1,94 @@ +/* + Example showing how to read the buttons or rocker switches on the RTK product line + + The platform is autodetected at power on (RTK Surveyor vs Express vs Express Plus vs Facet). + A task is spun up to monitor buttons and prints when a button is pressed/released +*/ + +#include "settings.h" + +#include + +const int FIRMWARE_VERSION_MAJOR = 1; +const int FIRMWARE_VERSION_MINOR = 11; + +char platformPrefix[40] = "Surveyor"; //Sets the prefix for broadcast names +char platformFilePrefix[40] = "SFE_Surveyor"; //Sets the prefix for logs and settings files + +//Hardware connections +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +//These pins are set in beginBoard() +int pin_batteryLevelLED_Red; +int pin_batteryLevelLED_Green; +int pin_positionAccuracyLED_1cm; +int pin_positionAccuracyLED_10cm; +int pin_positionAccuracyLED_100cm; +int pin_baseStatusLED; +int pin_bluetoothStatusLED; +int pin_microSD_CS; +int pin_zed_tx_ready; +int pin_zed_reset; +int pin_batteryLevel_alert; + +int pin_muxA; +int pin_muxB; +int pin_powerSenseAndControl; +int pin_setupButton; +int pin_powerFastOff; +int pin_dac26; +int pin_adc39; +int pin_peripheralPowerControl; + +int pin_radio_rx; +int pin_radio_tx; +int pin_radio_rst; +int pin_radio_pwr; +int pin_radio_cts; +int pin_radio_rts; +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +//Buttons - Interrupt driven and debounce +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +#include // http://librarymanager/All#JC_Button +Button *setupBtn = NULL; //We can't instantiate the buttons here because we don't yet know what pin numbers to use +Button *powerBtn = NULL; + +TaskHandle_t ButtonCheckTaskHandle = NULL; +const uint8_t ButtonCheckTaskPriority = 1; //3 being the highest, and 0 being the lowest +const int buttonTaskStackSize = 2000; + +const int shutDownButtonTime = 2000; //ms press and hold before shutdown +unsigned long lastRockerSwitchChange = 0; //If quick toggle is detected (less than 500ms), enter WiFi AP Config mode +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + +//Global variables +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +uint32_t powerPressedStartTime = 0; //Times how long user has been holding power button, used for power down +uint8_t debounceDelay = 20; //ms to delay between button reads + +uint64_t lastLogSize = 0; +bool logIncreasing = false; //Goes true when log file is greater than lastLogSize +bool reuseLastLog = false; //Goes true if we have a reset due to software (rather than POR) + +bool setupByPowerButton = false; //We can change setup via tapping power button + +unsigned long startTime = 0; //Used for checking longest running functions +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + +void setup() +{ + Serial.begin(115200); + delay(500); + Serial.println("RTK Button example"); + + Wire.begin(); + + beginBoard(); //Determine what hardware platform we are running on and check on button + + beginSystemState(); //Determine initial system state. Start task for button monitoring. +} + +void loop() +{ + delay(10); //A small delay prevents panic if no other I2C or functions are called +} diff --git a/Firmware/Test Sketches/Ticker/System.ino b/Firmware/Test Sketches/Button_Read/System.ino similarity index 100% rename from Firmware/Test Sketches/Ticker/System.ino rename to Firmware/Test Sketches/Button_Read/System.ino diff --git a/Firmware/Test Sketches/Button_Read/Tasks.ino b/Firmware/Test Sketches/Button_Read/Tasks.ino new file mode 100644 index 000000000..5ecd5c4f3 --- /dev/null +++ b/Firmware/Test Sketches/Button_Read/Tasks.ino @@ -0,0 +1,68 @@ + +//Monitor momentary buttons or rocker switches, depending on platform +void ButtonCheckTask(void *e) +{ + if (setupBtn != NULL) setupBtn->begin(); + if (powerBtn != NULL) powerBtn->begin(); + + while (true) + { + if (productVariant == RTK_SURVEYOR) + { + setupBtn->read(); + + //When switch is set to '1' = BASE, pin will be shorted to ground + if (setupBtn->isPressed()) //Switch is set to base mode + { + Serial.println("Rocker: Base"); + delay(100); + } + else if (setupBtn->wasReleased()) //Switch is set to Rover + { + Serial.println("Rocker: Rover"); + } + } + else if (productVariant == RTK_EXPRESS || productVariant == RTK_EXPRESS_PLUS) //Express: Check both of the momentary switches + { + setupBtn->read(); + powerBtn->read(); + + if (setupBtn->isPressed()) + { + Serial.println("Setup button pressed"); + delay(100); + } + else if (setupBtn->wasReleased()) + { + Serial.println("Setup button released"); + } + if (powerBtn->isPressed()) + { + Serial.println("Power button pressed"); + delay(100); + } + else if (powerBtn->wasReleased()) + { + Serial.println("Power button released"); + } + } //End Platform = RTK Express + + else if (productVariant == RTK_FACET) //Check one momentary button + { + powerBtn->read(); + + if (powerBtn->isPressed()) + { + Serial.println("Power button pressed"); + delay(100); + } + else if (powerBtn->wasReleased()) + { + Serial.println("Power button released"); + } + } //End Platform = RTK Facet + + delay(1); //Poor man's way of feeding WDT. Required to prevent Priority 1 tasks from causing WDT reset + taskYIELD(); + } +} diff --git a/Firmware/Test Sketches/Button_Read/settings.h b/Firmware/Test Sketches/Button_Read/settings.h new file mode 100644 index 000000000..75ec7c8fe --- /dev/null +++ b/Firmware/Test Sketches/Button_Read/settings.h @@ -0,0 +1,221 @@ +//System can enter a variety of states starting at Rover_No_Fix at power on +typedef enum +{ + STATE_ROVER_NOT_STARTED = 0, + STATE_ROVER_NO_FIX, + STATE_ROVER_FIX, + STATE_ROVER_RTK_FLOAT, + STATE_ROVER_RTK_FIX, + STATE_BASE_NOT_STARTED, + STATE_BASE_TEMP_SETTLE, //User has indicated base, but current pos accuracy is too low + STATE_BASE_TEMP_SURVEY_STARTED, + STATE_BASE_TEMP_TRANSMITTING, + STATE_BASE_TEMP_WIFI_STARTED, + STATE_BASE_TEMP_WIFI_CONNECTED, //10 + STATE_BASE_TEMP_CASTER_STARTED, + STATE_BASE_TEMP_CASTER_CONNECTED, + STATE_BASE_FIXED_NOT_STARTED, + STATE_BASE_FIXED_TRANSMITTING, + STATE_BASE_FIXED_WIFI_STARTED, + STATE_BASE_FIXED_WIFI_CONNECTED, + STATE_BASE_FIXED_CASTER_STARTED, + STATE_BASE_FIXED_CASTER_CONNECTED, + STATE_BUBBLE_LEVEL, + STATE_MARK_EVENT, //20 + STATE_DISPLAY_SETUP, + STATE_WIFI_CONFIG_NOT_STARTED, + STATE_WIFI_CONFIG, + STATE_TEST, + STATE_TESTING, //25 + STATE_PROFILE_1, + STATE_PROFILE_2, + STATE_PROFILE_3, + STATE_PROFILE_4, + STATE_SHUTDOWN, +} SystemState; +volatile SystemState systemState = STATE_ROVER_NOT_STARTED; +SystemState lastSystemState = STATE_ROVER_NOT_STARTED; +SystemState requestedSystemState = STATE_ROVER_NOT_STARTED; +bool newSystemStateRequested = false; + +//The setup display can show a limited set of states +//When user pauses for X amount of time, system will enter that state +SystemState setupState = STATE_MARK_EVENT; + +typedef enum +{ + RTK_SURVEYOR = 0, + RTK_EXPRESS, + RTK_FACET, + RTK_EXPRESS_PLUS, +} ProductVariant; +ProductVariant productVariant = RTK_SURVEYOR; + +typedef enum +{ + BUTTON_ROVER = 0, + BUTTON_BASE, +} ButtonState; +ButtonState buttonPreviousState = BUTTON_ROVER; + +//Data port mux (RTK Express) can enter one of four different connections +typedef enum muxConnectionType_e +{ + MUX_UBLOX_NMEA = 0, + MUX_PPS_EVENTTRIGGER, + MUX_I2C_WT, + MUX_ADC_DAC, +} muxConnectionType_e; + +//User can enter fixed base coordinates in ECEF or degrees +typedef enum +{ + COORD_TYPE_ECEF = 0, + COORD_TYPE_GEODETIC, +} coordinateType_e; + +//User can select output pulse as either falling or rising edge +typedef enum +{ + PULSE_FALLING_EDGE = 0, + PULSE_RISING_EDGE, +} pulseEdgeType_e; + +//Custom NMEA sentence types output to the log file +typedef enum +{ + CUSTOM_NMEA_TYPE_RESET_REASON = 0, + CUSTOM_NMEA_TYPE_WAYPOINT, + CUSTOM_NMEA_TYPE_EVENT, + CUSTOM_NMEA_TYPE_SYSTEM_VERSION, + CUSTOM_NMEA_TYPE_ZED_VERSION, + CUSTOM_NMEA_TYPE_STATUS, +} customNmeaType_e; + +//Freeze and blink LEDs if we hit a bad error +typedef enum +{ + ERROR_NO_I2C = 2, //Avoid 0 and 1 as these are bad blink codes + ERROR_GPS_CONFIG_FAIL, +} t_errorNumber; + +//Radio status LED goes from off (LED off), no connection (blinking), to connected (solid) +enum RadioState +{ + RADIO_OFF = 0, + BT_ON_NOCONNECTION, //WiFi is off + BT_CONNECTED, + WIFI_ON_NOCONNECTION, //BT is off + WIFI_CONNECTED, +}; +volatile byte radioState = RADIO_OFF; + +//Return values for getByteChoice() +enum returnStatus { + STATUS_GETBYTE_TIMEOUT = 255, + STATUS_GETNUMBER_TIMEOUT = -123455555, + STATUS_PRESSED_X = 254, +}; + +//These are the allowable constellations to receive from and log (if enabled) +//Tested with u-center v21.02 +#define MAX_CONSTELLATIONS 6 //(sizeof(ubxConstellations)/sizeof(ubxConstellation)) + + +//Different ZED modules support different messages (F9P vs F9R vs F9T) +//Create binary packed struct for different platforms +typedef enum ubxPlatform +{ + PLATFORM_F9P = 0b0001, + PLATFORM_F9R = 0b0010, + PLATFORM_F9T = 0b0100, +} ubxPlatform; + +//Each message will have a rate, a visible name, and a class +typedef struct ubxMsg +{ + uint32_t msgConfigKey; + uint8_t msgID; + uint8_t msgClass; + uint8_t msgRate; + char msgTextName[30]; + uint8_t supported; +} ubxMsg; + +//These are the allowable messages to broadcast and log (if enabled) +//Tested with u-center v21.02 +#define MAX_UBX_MSG (13 + 25 + 5 + 10 + 3 + 12 + 5) //(sizeof(ubxMessages)/sizeof(ubxMsg)) + +//This is all the settings that can be set on RTK Surveyor. It's recorded to NVM and the config file. +typedef struct struct_settings { + int sizeOfSettings = 0; //sizeOfSettings **must** be the first entry and must be int + //int rtkIdentifier = RTK_IDENTIFIER; // rtkIdentifier **must** be the second entry + bool printDebugMessages = false; + bool enableSD = true; + bool enableDisplay = true; + int maxLogTime_minutes = 60 * 24; //Default to 24 hours + int observationSeconds = 60; //Default survey in time of 60 seconds + float observationPositionAccuracy = 5.0; //Default survey in pos accy of 5m + bool fixedBase = false; //Use survey-in by default + bool fixedBaseCoordinateType = COORD_TYPE_ECEF; + double fixedEcefX = -1280206.568; + double fixedEcefY = -4716804.403; + double fixedEcefZ = 4086665.484; + double fixedLat = 40.09029479; + double fixedLong = -105.18505761; + double fixedAltitude = 1560.089; + uint32_t dataPortBaud = 460800; //Default to 460800bps to support >10Hz update rates + uint32_t radioPortBaud = 57600; //Default to 57600bps to support connection to SiK1000 radios + bool enableNtripServer = false; + char casterHost[50] = "rtk2go.com"; //It's free... + uint16_t casterPort = 2101; + char casterUser[50] = "test@test.com"; //Some free casters require auth. User must provide their own email address to use RTK2Go + char casterUserPW[50] = ""; + char mountPointUpload[50] = "bldr_dwntwn2"; + char mountPointUploadPW[50] = "WR5wRo4H"; + char mountPointDownload[50] = "bldr_SparkFun1"; + char mountPointDownloadPW[50] = ""; + bool casterTransmitGGA = true; + char wifiSSID[50] = "TRex"; + char wifiPW[50] = "parachutes"; + float surveyInStartingAccuracy = 1.0; //Wait for 1m horizontal positional accuracy before starting survey in + uint16_t measurementRate = 250; //Elapsed ms between GNSS measurements. 25ms to 65535ms. Default 4Hz. + uint16_t navigationRate = 1; //Ratio between number of measurements and navigation solutions. Default 1 for 4Hz (with measurementRate). + bool enableI2Cdebug = false; //Turn on to display GNSS library debug messages + bool enableHeapReport = false; //Turn on to display free heap + bool enableTaskReports = false; //Turn on to display task high water marks + muxConnectionType_e dataPortChannel = MUX_UBLOX_NMEA; //Mux default to ublox UART1 + uint16_t spiFrequency = 16; //By default, use 16MHz SPI + bool enableLogging = true; //If an SD card is present, log default sentences + uint16_t sppRxQueueSize = 2048; + uint16_t sppTxQueueSize = 512; + SystemState lastState = STATE_ROVER_NOT_STARTED; //For Express, start unit in last known state + bool throttleDuringSPPCongestion = true; + bool enableSensorFusion = false; //If IMU is available, avoid using it unless user specifically selects automotive + bool autoIMUmountAlignment = true; //Allows unit to automatically establish device orientation in vehicle + bool enableResetDisplay = false; + uint8_t resetCount = 0; + bool enableExternalPulse = true; //Send pulse once lock is achieved + uint32_t externalPulseTimeBetweenPulse_us = 1000000; //us between pulses, max of 65s + uint32_t externalPulseLength_us = 100000; //us length of pulse + pulseEdgeType_e externalPulsePolarity = PULSE_RISING_EDGE; //Pulse rises for pulse length, then falls + bool enableExternalHardwareEventLogging = false; //Log when INT/TM2 pin goes low + + int maxLogLength_minutes = 60 * 24; //Default to 24 hours + char profileName[50] = "Default"; + +} Settings; +Settings settings; + +//Monitor which devices on the device are on or offline. +struct struct_online { + bool microSD = false; + bool display = false; + bool gnss = false; + bool logging = false; + bool serialOutput = false; + bool fs = false; + bool rtc = false; + bool battery = false; + bool accelerometer = false; +} online; diff --git a/Firmware/Test Sketches/Core_Load/Core_Load.ino b/Firmware/Test Sketches/Core_Load/Core_Load.ino new file mode 100644 index 000000000..74e708863 --- /dev/null +++ b/Firmware/Test Sketches/Core_Load/Core_Load.ino @@ -0,0 +1,67 @@ +/* + https://esp32.com/viewtopic.php?t=9820 + https://esp32.com/viewtopic.php?t=7086 + https://github.com/pglen/esp32_cpu_load/blob/master/main/cpu_load.c + https://esp32.com/viewtopic.php?t=11371 + https://forums.freertos.org/t/how-can-a-task-with-lower-priority-to-recover-from-starvation/11705/2 + + The lowest priority should not block. +*/ + +#define MAX_CPU_CORES 2 + +TaskHandle_t idleTaskHandle0 = nullptr; +uint32_t maxIdleCount0 = 0; +uint32_t idleCount0 = 0; + +TaskHandle_t idleTaskHandle1 = nullptr; +uint32_t maxIdleCount1 = 0; +uint32_t idleCount1 = 0; + +unsigned long lastLoadTest = 0; + +void setup() +{ + Serial.begin(115200); + delay(250); + Serial.println("ESP32 Core Loads"); + + beginIdleTasks(); + + //Calculate the 100% idle amount + idleCount0 = 0; + idleCount1 = 0; + delay(10); //Allow the idle tasks to freely run up counter + maxIdleCount0 = idleCount0 * 100; + maxIdleCount1 = idleCount1 * 100; + + Serial.printf("idleCount0: %d\r\n", idleCount0); + Serial.printf("maxIdleCount0: %d\r\n", maxIdleCount0); + Serial.printf("idleCount1: %d\r\n", idleCount1); + Serial.printf("maxIdleCount1: %d\r\n", maxIdleCount1); +} + +void loop() +{ + // Every 5 seconds, we put the processor to work. + if (millis() - lastLoadTest > 5000) + { + lastLoadTest = millis(); + Serial.print("Loading core - "); + + //delayMicroseconds(100000); //Blocks the 0 priority task for 10% + + //Do a million floating point calcs to tie up the processor for ~20% + double counter = 1.0; + for (uint32_t x = 0 ; x < 1000000; x++) + counter *= 3.14159; + // counter *= 1.000001; + + //We have to actually use/print the variable otherwise compiler will dispose of unused chars/code + //and there will be no load on the processor + Serial.printf("counter: %0.2f\r\n", counter); + } + + lowerTaskYield(); //Allows lower priority (0) tasks to run - block the active task so the idle task can run and feed the watchdog + taskYIELD(); //Yields to tasks with the same priority or higher +} diff --git a/Firmware/Test Sketches/Core_Load/Idle.ino b/Firmware/Test Sketches/Core_Load/Idle.ino new file mode 100644 index 000000000..ddf5da8a0 --- /dev/null +++ b/Firmware/Test Sketches/Core_Load/Idle.ino @@ -0,0 +1,86 @@ + +void beginIdleTasks() +{ + if (idleTaskHandle0 == nullptr) + xTaskCreatePinnedToCore( + idleTask0, // Function to call + "IdleTask0", // Just for humans + 2000, // Stack Size + nullptr, // Task input parameter + 0, // Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest + &idleTaskHandle0, // Task handle + 0); // Core where task should run, 0=core, 1=Arduino + + if (idleTaskHandle1 == nullptr) + xTaskCreatePinnedToCore( + idleTask1, // Function to call + "IdleTask1", // Just for humans + 2000, // Stack Size + nullptr, // Task input parameter + 0, // Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest + &idleTaskHandle1, // Task handle + 1); // Core where task should run, 0=core, 1=Arduino +} + +void idleTask0(void *e) +{ + uint32_t lastDisplayIdleTime0 = 0; + + while (1) + { + idleCount0++; // Increment a count during the idle time + + // Report idle time periodically + if (millis() - lastDisplayIdleTime0 > 1000) + { + lastDisplayIdleTime0 = millis(); + + if (idleCount0 > maxIdleCount0) + maxIdleCount0 = idleCount0; + + Serial.printf("CPU 0 idle time: %d%% (idleCount0: %d/ maxIdleCount0: %d)\r\n", idleCount0 * 100 / maxIdleCount0, idleCount0, + maxIdleCount0); + + // Serial.printf("%d Tasks\r\n", uxTaskGetNumberOfTasks()); + + idleCount0 = 0; // Restart the idle count + } + //The idle task should NOT delay or yield + } +} + +void idleTask1(void *e) +{ + uint32_t lastDisplayIdleTime1 = 0; + + while (1) + { + idleCount1++; // Increment a count during the idle time + + // Report idle time periodically + if (millis() - lastDisplayIdleTime1 > 1000) + { + lastDisplayIdleTime1 = millis(); + + if (idleCount1 > maxIdleCount1) + maxIdleCount1 = idleCount1; + + Serial.printf("CPU 1 idle time: %d%% (idleCount1: %d/ maxIdleCount1: %d)\r\n", idleCount1 * 100 / maxIdleCount1, idleCount1, + maxIdleCount1); + + // Serial.printf("%d Tasks\r\n", uxTaskGetNumberOfTasks()); + + idleCount1 = 0; // Restart the idle count + } + //The idle task should NOT delay or yield + } +} + +// Normally a delay(1) will feed the WDT but if we don't want to wait that long, +// this allows lower priority tasks to run including feeding the WDT with minimum delay +void lowerTaskYield() +{ + vTaskDelay(1); //How long is vTaskDelay(1)? 1ms. Similar to delay(1) + + //delayMicroseconds(100); //By itself, does not allow lower priority tasks to run +} diff --git a/Firmware/Test Sketches/Display/Display.ino b/Firmware/Test Sketches/Display/Display.ino index 1c8538d38..4c56bea56 100644 --- a/Firmware/Test Sketches/Display/Display.ino +++ b/Firmware/Test Sketches/Display/Display.ino @@ -4,39 +4,40 @@ Distributed as-is; no warranty is given. */ #include -#include //Click here to get the library: http://librarymanager/All#SparkFun_Micro_OLED + +#include //http://librarymanager/All#SparkFun_Qwiic_Graphic_OLED +QwiicMicroOLED oled; + +// Fonts +#include +#include + +//#include //Click here to get the library: http://librarymanager/All#SparkFun_Micro_OLED #include "icons.h" -#define PIN_RESET 9 -#define DC_JUMPER 1 -MicroOLED oled(PIN_RESET, DC_JUMPER); +//#define PIN_RESET 9 +//#define DC_JUMPER 1 +//MicroOLED oled(PIN_RESET, DC_JUMPER); bool displayDetected = false; void setup() { - Wire.begin(); - Wire.setClock(400000); - Serial.begin(115200); delay(100); Serial.println("OLED example"); - //0x3D is default on Qwiic board - if (isConnected(0x3D) == true || isConnected(0x3C) == true) - { - Serial.println("Display detected"); - displayDetected = true; - } - else - Serial.println("No display detected"); + Wire.begin(); + Wire.setClock(400000); - if (displayDetected) + if (oled.begin() == false) { - oled.begin(); // Initialize the OLED - oled.clear(PAGE); // Clear the display's internal memory - oled.clear(ALL); // Clear the library's display buffer + Serial.println("Device begin failed. Freezing..."); + while (true) + ; } + Serial.println("Begin success"); + displayDetected = true; } void loop() @@ -48,12 +49,12 @@ void loop() // oled.drawIcon(4, 4, Battery_0_Width, Battery_0_Height, Battery_0, sizeof(Battery_0), true); // oled.drawIcon(4, 4, Rover_Width, Rover_Height, Rover, sizeof(Rover), true); - oled.setFontType(1); //Set font to type 1: 8x16 + oled.setFont(QW_FONT_8X16); //HPA -// oled.setCursor(0, 21); //x, y -// oled.print("HPA:"); -// oled.print("125"); + // oled.setCursor(0, 21); //x, y + // oled.print("HPA:"); + // oled.print("125"); //3D Mean Accuracy oled.setCursor(17, 19); //x, y: Squeeze against the colon @@ -61,17 +62,18 @@ void loop() //SIV oled.setCursor(16, 35); //x, y - oled.print(":24"); + printText(":24", QW_FONT_5X7); + //oled.print(":24"); //Bluetooth Address - oled.setFontType(0); //Set font to type 0: + oled.setFont(QW_FONT_5X7); oled.setCursor(0, 4); //x, y oled.print("BC5D"); - oled.drawIcon(45, 0, Battery_2_Width, Battery_2_Height, Battery_2, sizeof(Battery_2), true); - oled.drawIcon(28, 0, Base_Width, Base_Height, Base, sizeof(Base), true); - oled.drawIcon(0, 18, CrossHair_Width, CrossHair_Height, CrossHair, sizeof(CrossHair), true); - oled.drawIcon(1, 35, Antenna_Width, Antenna_Height, Antenna, sizeof(Antenna), true); + displayBitmap(45, 0, Battery_2_Width, Battery_2_Height, Battery_2); + displayBitmap(28, 0, Base_Width, Base_Height, Base); + displayBitmap(0, 18, CrossHair_Width, CrossHair_Height, CrossHair); + displayBitmap(1, 35, Antenna_Width, Antenna_Height, Antenna); oled.display(); while (1); @@ -87,3 +89,17 @@ void loop() Serial.print("."); delay(100); } + +//Wrapper to avoid needing to pass width/height data twice +void displayBitmap(uint8_t x, uint8_t y, uint8_t imageWidth, uint8_t imageHeight, uint8_t *imageData) +{ + oled.bitmap(x, y, x + imageWidth, y + imageHeight, imageData, imageWidth, imageHeight); +} + +void printText(const char *text, QwiicFont &fontType) +{ + oled.setFont(fontType); + oled.setDrawMode(grROPXOR); + + oled.print(text); +} diff --git a/Firmware/Test Sketches/Display/settings.h b/Firmware/Test Sketches/Display/settings.h new file mode 100644 index 000000000..aec6b5961 --- /dev/null +++ b/Firmware/Test Sketches/Display/settings.h @@ -0,0 +1,14 @@ +//Monitor which devices on the device are on or offline. +struct struct_online { + bool microSD = false; + bool display = false; + bool gnss = false; + bool logging = false; + bool serialOutput = false; + bool fs = false; + bool rtc = false; + bool battery = false; + bool accelerometer = false; + bool ntripClient = false; + bool lband = false; +} online; diff --git a/Firmware/Test Sketches/GNSS_GetPosition/GNSS_GetPosition.ino b/Firmware/Test Sketches/GNSS_GetPosition/GNSS_GetPosition.ino new file mode 100644 index 000000000..fc6432872 --- /dev/null +++ b/Firmware/Test Sketches/GNSS_GetPosition/GNSS_GetPosition.ino @@ -0,0 +1,102 @@ +/* + Get the high precision geodetic solution for latitude and longitude using double + By: Nathan Seidle + Modified by: Paul Clark (PaulZC) + SparkFun Electronics + Date: April 17th, 2020 + License: MIT. See license file for more information but you can + basically do whatever you want with this code. + + This example shows how to inspect the accuracy of the high-precision + positional solution. Please see below for information about the units. +*/ + +#include // Needed for I2C to GNSS + +#include //http://librarymanager/All#SparkFun_u-blox_GNSS +SFE_UBLOX_GNSS i2cGNSS; + +long lastTime = 0; //Simple local timer. Limits amount of I2C traffic to u-blox module. + +void setup() +{ + Serial.begin(115200); + delay(1000); + Serial.println("u-blox high precision example"); + + Wire.begin(); + //Wire.setClock(400000); + + //i2cGNSS.enableDebugging(Serial); // Uncomment this line to enable debug messages + + if (i2cGNSS.begin() == false) //Connect to the u-blox module using Wire port + { + Serial.println(F("u-blox GNSS not detected at default I2C address. Please check wiring. Freezing.")); + while (1) + { + if(Serial.available()) ESP.restart(); + delay(10); + } + } + + i2cGNSS.setI2COutput(COM_TYPE_UBX); //Set the I2C port to output UBX only (turn off NMEA noise) +} + +void loop() +{ + //Query module only every second. + //The module only responds when a new position is available. + if (millis() - lastTime > 1000) + { + lastTime = millis(); //Update the timer + + int32_t latitude = i2cGNSS.getHighResLatitude(); + int8_t latitudeHp = i2cGNSS.getHighResLatitudeHp(); + int32_t longitude = i2cGNSS.getHighResLongitude(); + int8_t longitudeHp = i2cGNSS.getHighResLongitudeHp(); + int32_t ellipsoid = i2cGNSS.getElipsoid(); + int8_t ellipsoidHp = i2cGNSS.getElipsoidHp(); + int32_t msl = i2cGNSS.getMeanSeaLevel(); + int8_t mslHp = i2cGNSS.getMeanSeaLevelHp(); + uint32_t accuracy = i2cGNSS.getHorizontalAccuracy(); + uint8_t siv = i2cGNSS.getSIV(); + + // Defines storage for the lat and lon as double + double d_lat; // latitude + double d_lon; // longitude + + // Assemble the high precision latitude and longitude + d_lat = ((double)latitude) / 10000000.0; // Convert latitude from degrees * 10^-7 to degrees + d_lat += ((double)latitudeHp) / 1000000000.0; // Now add the high resolution component (degrees * 10^-9 ) + d_lon = ((double)longitude) / 10000000.0; // Convert longitude from degrees * 10^-7 to degrees + d_lon += ((double)longitudeHp) / 1000000000.0; // Now add the high resolution component (degrees * 10^-9 ) + + float f_ellipsoid; + float f_accuracy; + + // Calculate the height above ellipsoid in mm * 10^-1 + f_ellipsoid = (ellipsoid * 10) + ellipsoidHp; + f_ellipsoid = f_ellipsoid / 10000.0; // Convert from mm * 10^-1 to m + + f_accuracy = accuracy / 10000.0; // Convert from mm * 10^-1 to m + + // Finally, do the printing + Serial.print("Lat (deg): "); + Serial.print(d_lat, 9); + Serial.print(", Lon (deg): "); + Serial.print(d_lon, 9); + + Serial.print(", SIV: "); + Serial.print(siv); + + Serial.print(", Accuracy (m): "); + Serial.print(f_accuracy, 4); // Print the accuracy with 4 decimal places + + Serial.print(", Altitude (m): "); + Serial.print(f_ellipsoid, 4); // Print the ellipsoid with 4 decimal places + + Serial.println(); + } + + if(Serial.available()) ESP.restart(); +} diff --git a/Firmware/Test Sketches/GNSS_GetPosition_v3/GNSS_GetPosition_v3.ino b/Firmware/Test Sketches/GNSS_GetPosition_v3/GNSS_GetPosition_v3.ino new file mode 100644 index 000000000..e45a8e423 --- /dev/null +++ b/Firmware/Test Sketches/GNSS_GetPosition_v3/GNSS_GetPosition_v3.ino @@ -0,0 +1,196 @@ +/* + RTK Surveyor Firmware - Test Sketch using u-blox GNSS Library v3 (I2C and SPI) +*/ + +#include // Needed for I2C to GNSS +#include // Needed for SPI to GNSS + +#include //http://librarymanager/All#SparkFun_u-blox_GNSS_v3 + +// In this test, the GNSS object is instantiated inside the loop - to check for memory leaks + +#define gnssSerial Serial1 + +// Define which hardware interface the module will use. This depends on the RTK hardware. +// Most are I2C. Reference Station is SPI. Future hardware _could_ be Serial... +typedef enum { + RTK_GNSS_IS_I2C, + RTK_GNSS_IS_SPI, + RTK_GNSS_IS_SERIAL +} RTK_GNSS_INTERFACE; +RTK_GNSS_INTERFACE theGNSSinterface; + +const int GNSS_SPI_CS = 4; // Chip select for the SPI interface - the "free" pin on Thing Plus C + +void setup() +{ + delay(1000); + + Serial.begin(115200); + Serial.println("RTK Surveyor Firmware - u-blox GNSS Test Sketch"); + + theGNSSinterface = RTK_GNSS_IS_I2C; // Select I2C for this test + + // Prepare the correct hardware interface for GNSS comms + + if (theGNSSinterface == RTK_GNSS_IS_SERIAL) + { + gnssSerial.begin(38400); + } + else if (theGNSSinterface == RTK_GNSS_IS_SPI) + { + SPI.begin(); // Redundant - just to show what could be done here + } + else // if (theGNSSinterface == RTK_GNSS_IS_I2C) // Catch-all. Default to I2C + { + Wire.begin(); + //Wire.setClock(400000); + } + + while (Serial.available()) // Make sure the Serial buffer is empty + Serial.read(); +} + +void loop() +{ + // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + // Create an object of the GNSS super-class inside the loop - just as a test to check for memory leaks + + SFE_UBLOX_GNSS_SUPER theGNSS; + + // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + + //theGNSS.enableDebugging(Serial); // Uncomment this line to enable debug messages + + // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + // Now begin the GNSS + + bool beginSuccess = false; + + if (theGNSSinterface == RTK_GNSS_IS_SERIAL) + { + beginSuccess = theGNSS.begin(gnssSerial); + } + + else if (theGNSSinterface == RTK_GNSS_IS_SPI) + { + beginSuccess = theGNSS.begin(SPI, GNSS_SPI_CS); // SPI, default to 4MHz + + //beginSuccess = theGNSS.begin(SPI, GNSS_SPI_CS, 4000000); // Custom + + //SPISettings customSPIsettings = SPISettings(4000000, MSBFIRST, SPI_MODE0); + //beginSuccess = theGNSS.begin(SPI, GNSS_SPI_CS, customSPIsettings); // Custom + } + + else // if (theGNSSinterface == RTK_GNSS_IS_I2C) // Catch-all. Default to I2C + { + beginSuccess = theGNSS.begin(); // Wire, 0x42 + + //beginSuccess = theGNSS.begin(Wire, 0x42); // Custom + } + + // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + // Check begin was successful + + if (!beginSuccess) + { + Serial.println(F("u-blox GNSS not detected. Please check wiring. Freezing.")); + while (1) + { + if (Serial.available()) + ESP.restart(); + delay(10); + } + } + + // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + // Disable the NMEA, just to show how to do it: + + if (theGNSSinterface == RTK_GNSS_IS_SERIAL) + { + // Assume we're connected to UART1. Could be UART2 + theGNSS.setUART1Output(COM_TYPE_UBX); //Set the UART1 port to output UBX only (turn off NMEA noise) + } + else if (theGNSSinterface == RTK_GNSS_IS_SPI) + { + theGNSS.setSPIOutput(COM_TYPE_UBX); //Set the SPI port to output UBX only (turn off NMEA noise) + } + else // if (theGNSSinterface == RTK_GNSS_IS_I2C) // Catch-all. Default to I2C + { + theGNSS.setI2COutput(COM_TYPE_UBX); //Set the I2C port to output UBX only (turn off NMEA noise) + } + + // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + //Query module info + + if (theGNSS.getModuleInfo()) + { + Serial.print(F("FWVER: ")); + Serial.print(theGNSS.getFirmwareVersionHigh()); + Serial.print(F(".")); + Serial.println(theGNSS.getFirmwareVersionLow()); + + Serial.print(F("Firmware: ")); + Serial.println(theGNSS.getFirmwareType()); + + Serial.print(F("PROTVER: ")); + Serial.print(theGNSS.getProtocolVersionHigh()); + Serial.print(F(".")); + Serial.println(theGNSS.getProtocolVersionLow()); + + Serial.print(F("MOD: ")); + Serial.println(theGNSS.getModuleName()); + } + + // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + //Query module for HPPOSLLH data + + if (theGNSS.getHPPOSLLH()) // Returns true when fresh data is available + { + int32_t latitude = theGNSS.getHighResLatitude(); + int8_t latitudeHp = theGNSS.getHighResLatitudeHp(); + int32_t longitude = theGNSS.getHighResLongitude(); + int8_t longitudeHp = theGNSS.getHighResLongitudeHp(); + int32_t ellipsoid = theGNSS.getElipsoid(); + int8_t ellipsoidHp = theGNSS.getElipsoidHp(); + int32_t msl = theGNSS.getMeanSeaLevel(); + int8_t mslHp = theGNSS.getMeanSeaLevelHp(); + uint32_t accuracy = theGNSS.getHorizontalAccuracy(); + + // Defines storage for the lat and lon as double + double d_lat; // latitude + double d_lon; // longitude + + // Assemble the high precision latitude and longitude + d_lat = ((double)latitude) / 10000000.0; // Convert latitude from degrees * 10^-7 to degrees + d_lat += ((double)latitudeHp) / 1000000000.0; // Now add the high resolution component (degrees * 10^-9 ) + d_lon = ((double)longitude) / 10000000.0; // Convert longitude from degrees * 10^-7 to degrees + d_lon += ((double)longitudeHp) / 1000000000.0; // Now add the high resolution component (degrees * 10^-9 ) + + float f_ellipsoid; + float f_accuracy; + + // Calculate the height above ellipsoid in mm * 10^-1 + f_ellipsoid = (ellipsoid * 10) + ellipsoidHp; + f_ellipsoid = f_ellipsoid / 10000.0; // Convert from mm * 10^-1 to m + + f_accuracy = accuracy / 10000.0; // Convert from mm * 10^-1 to m + + // Finally, do the printing + Serial.print("Lat (deg): "); + Serial.print(d_lat, 9); + Serial.print(", Lon (deg): "); + Serial.print(d_lon, 9); + + Serial.print(", Accuracy (m): "); + Serial.print(f_accuracy, 4); // Print the accuracy with 4 decimal places + + Serial.print(", Altitude (m): "); + Serial.print(f_ellipsoid, 4); // Print the ellipsoid with 4 decimal places + + Serial.println(); + } + + if (Serial.available()) + ESP.restart(); +} diff --git a/Firmware/Test Sketches/GNSS_Serial_Test/GNSS_Serial_Test.ino b/Firmware/Test Sketches/GNSS_Serial_Test/GNSS_Serial_Test.ino new file mode 100644 index 000000000..05539ba76 --- /dev/null +++ b/Firmware/Test Sketches/GNSS_Serial_Test/GNSS_Serial_Test.ino @@ -0,0 +1,95 @@ +/* + This example demonstrates how to connect to the ZED-F9x over UART1 on the ZED to UART2 on the ESP32. + We use this interface for passing NMEA from the ZED to the ESP32 that is then broadcast over BT SPP. + + By default, the ZED in the RTK product is configured at 460800bps for maximum logging +*/ + +const int FIRMWARE_VERSION_MAJOR = 1; +const int FIRMWARE_VERSION_MINOR = 11; + +#include + +//GNSS configuration +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +#include //http://librarymanager/All#SparkFun_u-blox_GNSS + +SFE_UBLOX_GNSS i2cGNSS; +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +HardwareSerial serialGNSS(2); //TX on 17, RX on 16 + +void setup() +{ + Serial.begin(115200); + delay(200); + Serial.println("GNSS UART2 Connection Test"); + + Wire.begin(); + //Wire.setClock(400000); + + if (i2cGNSS.begin() == false) //Connect to the u-blox module using Wire port + { + Serial.println(F("u-blox GNSS not detected at default I2C address. Trying again")); + + //On the RTK Facet the ESP32 controls the power to the ZED. During power cycles, the ZED can take up to ~1000ms to respond to I2C pings. + //delay(400); //Bad + delay(500); //Good - may be a combination of startup delay of 200ms + + if (i2cGNSS.begin() == false) //Connect to the u-blox module using Wire port + Serial.println(F("u-blox GNSS not detected at default I2C address.")); + else + Serial.println(F("ZED online over I2C")); + } + else + Serial.println(F("ZED online over I2C")); + + Serial.println(); + Serial.println("s) Single test"); + Serial.println("r) Reset ESP32"); + Serial.println("x) Reset u-blox module to factory defaults"); +} + +void loop() +{ + if (Serial.available()) + { + byte incoming = Serial.read(); + + if (incoming == 's') + { + Serial.println("Start ZED over serial connection"); + + i2cGNSS.setSerialRate(115200 * 4, COM_PORT_UART1); //Set u-blox UART1 to 460800 using I2C + + serialGNSS.begin(115200 * 4); //Open ESP32 UART1 at 460800 + + //See if ZED responds to serial + SFE_UBLOX_GNSS myGNSS; + + if (myGNSS.begin(serialGNSS) == false) + Serial.println(F("u-blox GNSS not detected over serial UART2.")); + else + Serial.println("u-blox detected over ESP32 UART2 / u-blox UART1. BT connection is good."); + } + else if (incoming == 'r') + { + Serial.println("ESP32 Reset"); + ESP.restart(); + } + else if (incoming == 'x') + { + Serial.println("Reset u-blox module to factory defaults. Please wait."); + i2cGNSS.factoryReset(); //Reset everything: baud rate, I2C address, update rate, everything. + + delay(5000); // Wait while the module restarts + } + else + { + Serial.println(); + Serial.println("s) Single test"); + Serial.println("r) Reset ESP32"); + Serial.println("x) Reset u-blox module to factory defaults"); + } + } +} diff --git a/Firmware/Test Sketches/Hookup_Display/Display.ino b/Firmware/Test Sketches/Hookup_Display/Display.ino new file mode 100644 index 000000000..8ee7d1665 --- /dev/null +++ b/Firmware/Test Sketches/Hookup_Display/Display.ino @@ -0,0 +1,2294 @@ +//---------------------------------------- +//Constants +//---------------------------------------- + +//A bitfield is used to flag which icon needs to be illuminated +//systemState will dictate most of the icons needed + +//The radio area (top left corner of display) has three spots for icons +//Left/Center/Right +//Left Radio spot +#define ICON_WIFI_SYMBOL_0_LEFT (1<<0) // 0, 0 +#define ICON_WIFI_SYMBOL_1_LEFT (1<<1) // 0, 0 +#define ICON_WIFI_SYMBOL_2_LEFT (1<<2) // 0, 0 +#define ICON_WIFI_SYMBOL_3_LEFT (1<<3) // 0, 0 +#define ICON_BT_SYMBOL_LEFT (1<<4) // 0, 0 +#define ICON_MAC_ADDRESS (1<<5) // 0, 3 +#define ICON_ESPNOW_SYMBOL_0_LEFT (1<<6) // 0, 0 +#define ICON_ESPNOW_SYMBOL_1_LEFT (1<<7) // 0, 0 +#define ICON_ESPNOW_SYMBOL_2_LEFT (1<<8) // 0, 0 +#define ICON_ESPNOW_SYMBOL_3_LEFT (1<<9) // 0, 0 +#define ICON_DOWN_ARROW_LEFT (1<<10) // 0, 0 +#define ICON_UP_ARROW_LEFT (1<<11) // 0, 0 +#define ICON_BLANK_LEFT (1<<12) // 0, 0 + +//Center Radio spot +#define ICON_MAC_ADDRESS_2DIGIT (1<<13) // 13, 3 +#define ICON_BT_SYMBOL_CENTER (1<<14) // 10, 0 +#define ICON_DOWN_ARROW_CENTER (1<<15) // 0, 0 +#define ICON_UP_ARROW_CENTER (1<<16) // 0, 0 + +//Right Radio Spot +#define ICON_WIFI_SYMBOL_0_RIGHT (1<<17) // center, 0 +#define ICON_WIFI_SYMBOL_1_RIGHT (1<<18) // center, 0 +#define ICON_WIFI_SYMBOL_2_RIGHT (1<<19) // center, 0 +#define ICON_WIFI_SYMBOL_3_RIGHT (1<<20) // center, 0 +#define ICON_BASE_TEMPORARY (1<<21) // center, 0 +#define ICON_BASE_FIXED (1<<22) // center, 0 +#define ICON_ROVER_FUSION (1<<23) // center, 2 +#define ICON_ROVER_FUSION_EMPTY (1<<24) // center, 2 +#define ICON_DYNAMIC_MODEL (1<<25) // 27, 0 +#define ICON_DOWN_ARROW_RIGHT (1<<26) // center, 0 +#define ICON_UP_ARROW_RIGHT (1<<27) // center, 0 +#define ICON_BLANK_RIGHT (1<<28) // center, 0 + +//Left + Center Radio spot +#define ICON_IP_ADDRESS (1<<29) + +//Right top +#define ICON_BATTERY (1<<0) // 45, 0 + +//Left center +#define ICON_CROSS_HAIR (1<<1) // 0, 18 +#define ICON_CROSS_HAIR_DUAL (1<<2) // 0, 18 + +//Right center +#define ICON_HORIZONTAL_ACCURACY (1<<3) // 16, 20 + +//Left bottom +#define ICON_SIV_ANTENNA (1<<4) // 2, 35 +#define ICON_SIV_ANTENNA_LBAND (1<<5) // 2, 35 + +//Right bottom +#define ICON_LOGGING (1<<6) // right, bottom + +//Left center +#define ICON_CLOCK (1<<7) +#define ICON_CLOCK_ACCURACY (1<<8) + +//Right top +#define ICON_ETHERNET (1<<9) + +//Right bottom +#define ICON_LOGGING_NTP (1<<10) + +//Left bottom +#define ICON_ANTENNA_SHORT (1<<11) +#define ICON_ANTENNA_OPEN (1<<12) + +//---------------------------------------- +//Locals +//---------------------------------------- + +static QwiicMicroOLED oled; +static uint32_t blinking_icons; +static uint32_t icons; +static uint32_t iconsRadio; + +unsigned long ssidDisplayTimer = 0; +bool ssidDisplayFirstHalf = false; + +//Fonts +#include +#include +#include + +//Icons +#include "icons.h" + +//---------------------------------------- +//Routines +//---------------------------------------- + +uint32_t setRadioIcons() +{ + uint32_t icons = 0; + + icons |= ICON_MAC_ADDRESS; + + icons |= setModeIcon(); + + return icons; +} + +uint32_t setWiFiIcon() +{ + return ICON_WIFI_SYMBOL_3_RIGHT; +} + +void beginDisplay() +{ + blinking_icons = 0; + + //At this point we have not identified the RTK platform + //If it's surveyor, there won't be a display and we have a 100ms delay + //If it's other platforms, we will try 3 times + int maxTries = 3; + for (int x = 0 ; x < maxTries ; x++) + { + if (oled.begin() == true) + { + online.display = true; + + Serial.println("Display started"); + + //Display the SparkFun LOGO + oled.erase(); + displayBitmap(0, 0, logoSparkFun_Width, logoSparkFun_Height, logoSparkFun); + oled.display(); + + delay(1000); + + return; + } + + delay(50); //Give display time to startup before attempting again + } + + Serial.println("Display not detected"); +} + +//Avoid code repetition +void displayBatteryVsEthernet() +{ + if (HAS_BATTERY) + icons |= ICON_BATTERY; //Top right + else //if (HAS_ETHERNET) + { + if (online.ethernetStatus == ETH_NOT_STARTED) + blinking_icons &= ~ICON_ETHERNET; //If Ethernet has not stated because not needed, don't display the icon + else if (online.ethernetStatus == ETH_CONNECTED) + blinking_icons |= ICON_ETHERNET; //Don't blink if link is up + else + blinking_icons ^= ICON_ETHERNET; + icons |= (blinking_icons & ICON_ETHERNET); //Top Right + } +} +void displaySivVsOpenShort(bool doUpdate) +{ + if (!HAS_ANTENNA_SHORT_OPEN) + icons |= paintSIV(); + else + { + if (aStatus == SFE_UBLOX_ANTENNA_STATUS_SHORT) + { + if (doUpdate) + blinking_icons ^= ICON_ANTENNA_SHORT; + else + blinking_icons |= ICON_ANTENNA_SHORT; + icons |= (blinking_icons & ICON_ANTENNA_SHORT); + } + else if (aStatus == SFE_UBLOX_ANTENNA_STATUS_OPEN) + { + if (doUpdate) + blinking_icons ^= ICON_ANTENNA_OPEN; + else + blinking_icons |= ICON_ANTENNA_OPEN; + icons |= (blinking_icons & ICON_ANTENNA_OPEN); + } + else + { + blinking_icons &= ~ICON_ANTENNA_SHORT; + blinking_icons &= ~ICON_ANTENNA_OPEN; + icons |= paintSIV(); + } + } +} + +//Given the system state, display the appropriate information +void updateDisplay(bool doUpdate) +{ + //Update the display if connected + if (online.display == true) + { + if (millis() - lastDisplayUpdate > 500) //Update display at 2Hz + { + lastDisplayUpdate = millis(); + + oled.reset(false); //Incase of previous corruption, force re-alignment of CGRAM. Do not init buffers as it takes time and causes screen to blink. + + oled.erase(); + + icons = 0; + iconsRadio = 0; + switch (systemState) + { + + /* + 111111111122222222223333333333444444444455555555556666 + 0123456789012345678901234567890123456789012345678901234567890123 + .---------------------------------------------------------------- + 0| ******* ** ** ***************** + 1| * * ** ** * * + 2| * ***** * ** ****** * *** *** *** * + 3|* * * * ** * * * *** *** *** *** + 4| * *** * ** * * **** * * * *** *** *** * + 5| * * ** ** ** * * **** * * * *** *** *** * + 6| * ****** * * * * * *** *** *** * + 7| *** **** * * * * * *** *** *** * + 8| * ** * * * * * *** *** *** *** + 9| * * * * * *** *** *** * + 10| * * * * + 11| ****** ***************** + 12| + 13| + 14| + 15| + 16| + 17| + 18| * + 19| * + 20| ******* + 21| * * * *** *** *** + 22| * * * * * * * * * + 23| * * * * * * * * * + 24| * * * ** * * * * * * + 25|******* ******* ** * * * + 26| * * * * * * * * * + 27| * * * * * * * * * + 28| * * * * * * * * * + 29| * * * ** * * ** * * * * + 30| ******* ** *** ** *** *** + 31| * + 32| * + 33| + 34| + 35| + 36| ** ******* + 37| * * *** *** * ** + 38| * * * * * * * * ** + 39| * * * * * * * * * + 40| * * ** * * * * * ***** * + 41| * * ** * * * * + 42| * * * * * * * ***** * + 43| ** * * * * * * * + 44| **** * * * * * * ***** * + 45| ** **** ** * * * * * * + 46| ** ** *** *** * * + 47| ****** ********* + */ + + case (STATE_ROVER_NOT_STARTED): + icons = ICON_CROSS_HAIR //Center left + | ICON_HORIZONTAL_ACCURACY //Center right + | ICON_LOGGING; //Bottom right + displaySivVsOpenShort(doUpdate); //Bottom left + displayBatteryVsEthernet(); //Top right + iconsRadio = setRadioIcons(); //Top left + break; + case (STATE_ROVER_NO_FIX): + icons = ICON_CROSS_HAIR //Center left + | ICON_HORIZONTAL_ACCURACY //Center right + | ICON_LOGGING; //Bottom right + displaySivVsOpenShort(doUpdate); //Bottom left + displayBatteryVsEthernet(); //Top right + iconsRadio = setRadioIcons(); //Top left + break; + case (STATE_ROVER_FIX): + icons = ICON_CROSS_HAIR //Center left + | ICON_HORIZONTAL_ACCURACY //Center right + | ICON_LOGGING; //Bottom right + displaySivVsOpenShort(doUpdate); //Bottom left + displayBatteryVsEthernet(); //Top right + iconsRadio = setRadioIcons(); //Top left + break; + case (STATE_ROVER_RTK_FLOAT): + blinking_icons ^= ICON_CROSS_HAIR_DUAL; + icons = (blinking_icons & ICON_CROSS_HAIR_DUAL) //Center left + | ICON_HORIZONTAL_ACCURACY //Center right + | ICON_LOGGING; //Bottom right + displaySivVsOpenShort(doUpdate); //Bottom left + displayBatteryVsEthernet(); //Top right + iconsRadio = setRadioIcons(); //Top left + break; + case (STATE_ROVER_RTK_FIX): + icons = ICON_CROSS_HAIR_DUAL//Center left + | ICON_HORIZONTAL_ACCURACY //Center right + | ICON_LOGGING; //Bottom right + displaySivVsOpenShort(doUpdate); //Bottom left + displayBatteryVsEthernet(); //Top right + iconsRadio = setRadioIcons(); //Top left + break; + + case (STATE_BASE_NOT_STARTED): + //Do nothing. Static display shown during state change. + break; + + //Start of base / survey in / NTRIP mode + //Screen is displayed while we are waiting for horz accuracy to drop to appropriate level + //Blink crosshair icon until we have we have horz accuracy < user defined level + case (STATE_BASE_TEMP_SETTLE): + blinking_icons ^= ICON_CROSS_HAIR; + icons = (blinking_icons & ICON_CROSS_HAIR) //Center left + | ICON_HORIZONTAL_ACCURACY //Center right + | ICON_LOGGING; //Bottom right + displaySivVsOpenShort(doUpdate); //Bottom left + displayBatteryVsEthernet(); //Top right + iconsRadio = setRadioIcons(); //Top left + break; + case (STATE_BASE_TEMP_SURVEY_STARTED): + icons = ICON_LOGGING; //Bottom right + displayBatteryVsEthernet(); //Top right + iconsRadio = setRadioIcons(); //Top left + paintBaseTempSurveyStarted(doUpdate); + break; + case (STATE_BASE_TEMP_TRANSMITTING): + icons = ICON_LOGGING; //Bottom right + displayBatteryVsEthernet(); //Top right + iconsRadio = setRadioIcons(); //Top left + paintRTCM(doUpdate); + break; + case (STATE_BASE_FIXED_NOT_STARTED): + icons = 0; //Top right + displayBatteryVsEthernet(); //Top right + iconsRadio = setRadioIcons(); //Top left + break; + case (STATE_BASE_FIXED_TRANSMITTING): + icons = ICON_LOGGING; //Bottom right + displayBatteryVsEthernet(); //Top right + iconsRadio = setRadioIcons(); //Top left + paintRTCM(doUpdate); + break; + + case (STATE_NTPSERVER_NOT_STARTED): + case (STATE_NTPSERVER_NO_SYNC): + blinking_icons ^= ICON_CLOCK; + icons = (blinking_icons & ICON_CLOCK) //Center left + | ICON_CLOCK_ACCURACY; //Center right + displaySivVsOpenShort(doUpdate); //Bottom left + if (online.ethernetStatus == ETH_CONNECTED) + blinking_icons |= ICON_ETHERNET; //Don't blink if link is up + else + blinking_icons ^= ICON_ETHERNET; + icons |= (blinking_icons & ICON_ETHERNET); //Top Right + iconsRadio = ICON_IP_ADDRESS; //Top left + break; + + case (STATE_NTPSERVER_SYNC): + icons = ICON_CLOCK //Center left + | ICON_CLOCK_ACCURACY //Center right + | ICON_LOGGING_NTP; //Bottom right + displaySivVsOpenShort(doUpdate); //Bottom left + if (online.ethernetStatus == ETH_CONNECTED) + blinking_icons |= ICON_ETHERNET; //Don't blink if link is up + else + blinking_icons ^= ICON_ETHERNET; + icons |= (blinking_icons & ICON_ETHERNET); //Top Right + iconsRadio = ICON_IP_ADDRESS; //Top left + break; + + case (STATE_CONFIG_VIA_ETH_NOT_STARTED): + break; + case (STATE_CONFIG_VIA_ETH_STARTED): + break; + case (STATE_CONFIG_VIA_ETH): + displayConfigViaEthernet(doUpdate); + break; + case (STATE_CONFIG_VIA_ETH_RESTART_BASE): + break; + + case (STATE_BUBBLE_LEVEL): + //paintBubbleLevel(); + break; + case (STATE_PROFILE): + paintProfile(displayProfile); + break; + case (STATE_MARK_EVENT): + //Do nothing. Static display shown during state change. + break; + case (STATE_DISPLAY_SETUP): + paintDisplaySetup(); + break; + case (STATE_WIFI_CONFIG_NOT_STARTED): + displayWiFiConfigNotStarted(); //Display 'WiFi Config' + break; + case (STATE_WIFI_CONFIG): + iconsRadio = setWiFiIcon(); //Blink WiFi in center + displayWiFiConfig(); //Display SSID and IP + break; + case (STATE_TEST): + //paintSystemTest(); + break; + case (STATE_TESTING): + //paintSystemTest(); + break; + + case (STATE_KEYS_STARTED): + //paintRTCWait(); + break; + case (STATE_KEYS_NEEDED): + //Do nothing. Quick, fall through state. + break; + case (STATE_KEYS_WIFI_STARTED): + iconsRadio = setWiFiIcon(); //Blink WiFi in center + //paintGettingKeys(); + break; + case (STATE_KEYS_WIFI_CONNECTED): + iconsRadio = setWiFiIcon(); //Blink WiFi in center + //paintGettingKeys(); + break; + case (STATE_KEYS_WIFI_TIMEOUT): + //Do nothing. Quick, fall through state. + break; + case (STATE_KEYS_EXPIRED): + //Do nothing. Quick, fall through state. + break; + case (STATE_KEYS_DAYS_REMAINING): + //Do nothing. Quick, fall through state. + break; + case (STATE_KEYS_LBAND_CONFIGURE): + //paintLBandConfigure(); + break; + case (STATE_KEYS_LBAND_ENCRYPTED): + //Do nothing. Quick, fall through state. + break; + case (STATE_KEYS_PROVISION_WIFI_STARTED): + iconsRadio = setWiFiIcon(); //Blink WiFi in center + //paintGettingKeys(); + break; + case (STATE_KEYS_PROVISION_WIFI_CONNECTED): + iconsRadio = setWiFiIcon(); //Blink WiFi in center + //paintGettingKeys(); + break; + case (STATE_KEYS_PROVISION_WIFI_TIMEOUT): + //Do nothing. Quick, fall through state. + break; + + case (STATE_ESPNOW_PAIRING_NOT_STARTED): + paintEspNowPairing(); + break; + case (STATE_ESPNOW_PAIRING): + paintEspNowPairing(); + break; + + case (STATE_SHUTDOWN): + displayShutdown(); + break; + default: + Serial.printf("Unknown display: %d\r\n", systemState); + displayError("Display"); + break; + } + + //Top left corner - Radio icon indicators take three spots (left/center/right) + //Allowed icon combinations: + //Bluetooth + Rover/Base + //WiFi + Bluetooth + Rover/Base + //ESP-Now + Bluetooth + Rover/Base + //ESP-Now + Bluetooth + WiFi + //See setRadioIcons() for the icon selection logic + + //Left spot + if (iconsRadio & ICON_MAC_ADDRESS) + { + char macAddress[5]; + const uint8_t * rtkMacAddress = getMacAddress(); + + //Print four characters of MAC + snprintf(macAddress, sizeof(macAddress), "%02X%02X", rtkMacAddress[4], rtkMacAddress[5]); + oled.setFont(QW_FONT_5X7); //Set font to smallest + oled.setCursor(0, 3); + oled.print(macAddress); + } + else if (iconsRadio & ICON_BT_SYMBOL_LEFT) + displayBitmap(1, 0, BT_Symbol_Width, BT_Symbol_Height, BT_Symbol); + else if (iconsRadio & ICON_WIFI_SYMBOL_0_LEFT) + displayBitmap(0, 0, WiFi_Symbol_Width, WiFi_Symbol_Height, WiFi_Symbol_0); + else if (iconsRadio & ICON_WIFI_SYMBOL_1_LEFT) + displayBitmap(0, 0, WiFi_Symbol_Width, WiFi_Symbol_Height, WiFi_Symbol_1); + else if (iconsRadio & ICON_WIFI_SYMBOL_2_LEFT) + displayBitmap(0, 0, WiFi_Symbol_Width, WiFi_Symbol_Height, WiFi_Symbol_2); + else if (iconsRadio & ICON_WIFI_SYMBOL_3_LEFT) + displayBitmap(0, 0, WiFi_Symbol_Width, WiFi_Symbol_Height, WiFi_Symbol_3); + else if (iconsRadio & ICON_ESPNOW_SYMBOL_0_LEFT) + displayBitmap(0, 0, ESPNOW_Symbol_Width, ESPNOW_Symbol_Height, ESPNOW_Symbol_0); + else if (iconsRadio & ICON_ESPNOW_SYMBOL_1_LEFT) + displayBitmap(0, 0, ESPNOW_Symbol_Width, ESPNOW_Symbol_Height, ESPNOW_Symbol_1); + else if (iconsRadio & ICON_ESPNOW_SYMBOL_2_LEFT) + displayBitmap(0, 0, ESPNOW_Symbol_Width, ESPNOW_Symbol_Height, ESPNOW_Symbol_2); + else if (iconsRadio & ICON_ESPNOW_SYMBOL_3_LEFT) + displayBitmap(0, 0, ESPNOW_Symbol_Width, ESPNOW_Symbol_Height, ESPNOW_Symbol_3); + else if (iconsRadio & ICON_DOWN_ARROW_LEFT) + displayBitmap(1, 0, DownloadArrow_Width, DownloadArrow_Height, DownloadArrow); + else if (iconsRadio & ICON_UP_ARROW_LEFT) + displayBitmap(1, 0, UploadArrow_Width, UploadArrow_Height, UploadArrow); + else if (iconsRadio & ICON_BLANK_LEFT) + { + ; + } + + //Center radio spots + if (iconsRadio & ICON_BT_SYMBOL_CENTER) + { + //Moved to center to give space for ESP NOW icon on far left + displayBitmap(16, 0, BT_Symbol_Width, BT_Symbol_Height, BT_Symbol); + } + else if (iconsRadio & ICON_MAC_ADDRESS_2DIGIT) + { + char macAddress[5]; + const uint8_t * rtkMacAddress = getMacAddress(); + + //Print only last two digits of MAC + snprintf(macAddress, sizeof(macAddress), "%02X", rtkMacAddress[5]); + oled.setFont(QW_FONT_5X7); //Set font to smallest + oled.setCursor(14, 3); + oled.print(macAddress); + } + else if (iconsRadio & ICON_DOWN_ARROW_CENTER) + displayBitmap(16, 0, DownloadArrow_Width, DownloadArrow_Height, DownloadArrow); + else if (iconsRadio & ICON_UP_ARROW_CENTER) + displayBitmap(16, 0, UploadArrow_Width, UploadArrow_Height, UploadArrow); + + //Radio third spot + if (iconsRadio & ICON_WIFI_SYMBOL_0_RIGHT) + displayBitmap(28, 0, WiFi_Symbol_Width, WiFi_Symbol_Height, WiFi_Symbol_0); + else if (iconsRadio & ICON_WIFI_SYMBOL_1_RIGHT) + displayBitmap(28, 0, WiFi_Symbol_Width, WiFi_Symbol_Height, WiFi_Symbol_1); + else if (iconsRadio & ICON_WIFI_SYMBOL_2_RIGHT) + displayBitmap(28, 0, WiFi_Symbol_Width, WiFi_Symbol_Height, WiFi_Symbol_2); + else if (iconsRadio & ICON_WIFI_SYMBOL_3_RIGHT) + displayBitmap(28, 0, WiFi_Symbol_Width, WiFi_Symbol_Height, WiFi_Symbol_3); + else if ((iconsRadio & ICON_DYNAMIC_MODEL) && (online.gnss == true)) + paintDynamicModel(); + else if (iconsRadio & ICON_BASE_TEMPORARY) + displayBitmap(28, 0, BaseTemporary_Width, BaseTemporary_Height, BaseTemporary); + else if (iconsRadio & ICON_BASE_FIXED) + displayBitmap(28, 0, BaseFixed_Width, BaseFixed_Height, BaseFixed); //true - blend with other pixels + else if (iconsRadio & ICON_DOWN_ARROW_RIGHT) + displayBitmap(31, 0, DownloadArrow_Width, DownloadArrow_Height, DownloadArrow); + else if (iconsRadio & ICON_UP_ARROW_RIGHT) + displayBitmap(31, 0, UploadArrow_Width, UploadArrow_Height, UploadArrow); + else if (iconsRadio & ICON_BLANK_RIGHT) + { + ; + } + + //Left + center spot + if (iconsRadio & ICON_IP_ADDRESS) + paintIPAddress(doUpdate); + + //Top right corner + if (icons & ICON_BATTERY) + paintBatteryLevel(); + else if (icons & ICON_ETHERNET) + displayBitmap(45, 0, Ethernet_Icon_Width, Ethernet_Icon_Height, Ethernet_Icon); + + //Center left + if (icons & ICON_CROSS_HAIR) + displayBitmap(0, 18, CrossHair_Width, CrossHair_Height, CrossHair); + else if (icons & ICON_CROSS_HAIR_DUAL) + displayBitmap(0, 18, CrossHairDual_Width, CrossHairDual_Height, CrossHairDual); + else if (icons & ICON_CLOCK) + paintClock(doUpdate); + + //Center right + if (icons & ICON_HORIZONTAL_ACCURACY) + paintHorizontalAccuracy(); + else if (icons & ICON_CLOCK_ACCURACY) + paintClockAccuracy(); + + //Bottom left corner + if (icons & ICON_SIV_ANTENNA) + displayBitmap(2, 35, SIV_Antenna_Width, SIV_Antenna_Height, SIV_Antenna); + else if (icons & ICON_SIV_ANTENNA_LBAND) + displayBitmap(2, 35, SIV_Antenna_LBand_Width, SIV_Antenna_LBand_Height, SIV_Antenna_LBand); + else if (icons & ICON_ANTENNA_SHORT) + displayBitmap(2, 35, Antenna_Short_Width, Antenna_Short_Height, Antenna_Short); + else if (icons & ICON_ANTENNA_OPEN) + displayBitmap(2, 35, Antenna_Open_Width, Antenna_Open_Height, Antenna_Open); + + //Bottom right corner + if (icons & ICON_LOGGING) + paintLogging(doUpdate); + else if (icons & ICON_LOGGING_NTP) + paintLoggingNTP(true, doUpdate); //NTP, no pulse + + oled.display(); //Push internal buffer to display + } + } //End display online +} + +void displayShutdown() +{ + displayMessage("Shutting Down...", 0); +} + +//Displays a small error message then hard freeze +//Text wraps and is small but legible +void displayError(const char * errorMessage) +{ + if (online.display == true) + { + oled.erase(); //Clear the display's internal buffer + + oled.setCursor(0, 0); //x, y + oled.setFont(QW_FONT_5X7); //Set font to smallest + oled.print("Error:"); + + oled.setCursor(2, 10); + //oled.setFont(QW_FONT_8X16); + oled.print(errorMessage); + + oled.display(); //Push internal buffer to display + + while (1) delay(10); //Hard freeze + } +} + +/* + 111111111122222222223333333333444444444455555555556666 + 0123456789012345678901234567890123456789012345678901234567890123 + .---------------------------------------------------------------- + 0| ***************** + 1| * * + 2| * *** *** *** * + 3| * *** *** *** *** + 4| * *** *** *** * + 5| * *** *** *** * + 6| * *** *** *** * + 7| * *** *** *** * + 8| * *** *** *** *** + 9| * *** *** *** * + 10| * * + 11| ***************** +*/ + +//Print the classic battery icon with levels +void paintBatteryLevel() +{ + if (online.display == true) + { + //Current battery charge level + if (battLevel < 25) + displayBitmap(45, 0, Battery_0_Width, Battery_0_Height, Battery_0); + else if (battLevel < 50) + displayBitmap(45, 0, Battery_1_Width, Battery_1_Height, Battery_1); + else if (battLevel < 75) + displayBitmap(45, 0, Battery_2_Width, Battery_2_Height, Battery_2); + else //batt level > 75 + displayBitmap(45, 0, Battery_3_Width, Battery_3_Height, Battery_3); + } +} + +//Based on system state, turn on the various Rover, Base, Fixed Base icons +uint32_t setModeIcon() +{ + uint32_t icons = 0; + + switch (systemState) + { + case (STATE_ROVER_NOT_STARTED): + break; + case (STATE_ROVER_NO_FIX): + icons |= ICON_DYNAMIC_MODEL; + break; + case (STATE_ROVER_FIX): + icons |= ICON_DYNAMIC_MODEL; + break; + case (STATE_ROVER_RTK_FLOAT): + icons |= ICON_DYNAMIC_MODEL; + break; + case (STATE_ROVER_RTK_FIX): + icons |= ICON_DYNAMIC_MODEL; + break; + + case (STATE_BASE_NOT_STARTED): + //Do nothing. Static display shown during state change. + break; + case (STATE_BASE_TEMP_SETTLE): + icons |= blinkBaseIcon(ICON_BASE_TEMPORARY); + break; + case (STATE_BASE_TEMP_SURVEY_STARTED): + icons |= blinkBaseIcon(ICON_BASE_TEMPORARY); + break; + case (STATE_BASE_TEMP_TRANSMITTING): + icons |= ICON_BASE_TEMPORARY; + break; + case (STATE_BASE_FIXED_NOT_STARTED): + //Do nothing. Static display shown during state change. + break; + case (STATE_BASE_FIXED_TRANSMITTING): + icons |= ICON_BASE_FIXED; + break; + + case (STATE_NTPSERVER_NOT_STARTED): + case (STATE_NTPSERVER_NO_SYNC): + case (STATE_NTPSERVER_SYNC): + break; + + default: + break; + } + return (icons); +} + +uint32_t blinkBaseIcon(uint32_t iconType) +{ + uint32_t icons = 0; + + //Limit how often we update this spot + if (millis() - thirdRadioSpotTimer > 1000) + { + thirdRadioSpotTimer = millis(); + thirdRadioSpotBlink ^= 1; //Share the spot + } + + if (thirdRadioSpotBlink == false) + icons |= iconType; + else + icons |= ICON_BLANK_RIGHT; + + return icons; +} + +/* + 111111111122222222223333333333444444444455555555556666 + 0123456789012345678901234567890123456789012345678901234567890123 + .---------------------------------------------------------------- + 17| + 18| + 19| + 20| + 21| *** *** *** + 22| * * * * * * + 23| * * * * * * + 24| ** * * * * * * + 25| ** * * * + 26| * * * * * * + 27| * * * * * * + 28| * * * * * * + 29| ** * * ** * * * * + 30| ** *** ** *** *** + 31| + 32| +*/ + +//Display horizontal accuracy +void paintHorizontalAccuracy() +{ + oled.setFont(QW_FONT_8X16); //Set font to type 1: 8x16 + oled.setCursor(16, 20); //x, y + oled.print(":"); + + if (horizontalAccuracy > 30.0) + { + oled.print(">30m"); + } + else if (horizontalAccuracy > 9.9) + { + oled.print(horizontalAccuracy, 1); //Print down to decimeter + } + else if (horizontalAccuracy > 1.0) + { + oled.print(horizontalAccuracy, 2); //Print down to centimeter + } + else + { + oled.print("."); //Remove leading zero + oled.printf("%03d", (int)(horizontalAccuracy * 1000)); //Print down to millimeter + } +} + +//Display clock with moving hands +void paintClock(int doUpdate) +{ + //Animate icon to show system running + static uint8_t clockIconDisplayed = 3; + + if (doUpdate) + { + clockIconDisplayed++; //Goto next icon + clockIconDisplayed %= 4; //Wrap + } + + if (clockIconDisplayed == 0) + displayBitmap(0, 18, Clock_Icon_Width, Clock_Icon_Height, Clock_Icon_1); + else if (clockIconDisplayed == 1) + displayBitmap(0, 18, Clock_Icon_Width, Clock_Icon_Height, Clock_Icon_2); + else if (clockIconDisplayed == 2) + displayBitmap(0, 18, Clock_Icon_Width, Clock_Icon_Height, Clock_Icon_3); + else + displayBitmap(0, 18, Clock_Icon_Width, Clock_Icon_Height, Clock_Icon_4); +} + +//Display clock accuracy tAcc +void paintClockAccuracy() +{ + oled.setFont(QW_FONT_8X16); //Set font to type 1: 8x16 + oled.setCursor(16, 20); //x, y + oled.print(":"); + + if (online.gnss == false) + { + oled.print(" N/A"); + } + else if (tAcc < 10) // 9 or less : show as 9ns + { + oled.print(tAcc); + displayBitmap(36, 20, Millis_Icon_Width, Millis_Icon_Height, Nanos_Icon); + } + else if (tAcc < 100) // 99 or less : show as 99ns + { + oled.print(tAcc); + displayBitmap(44, 20, Millis_Icon_Width, Millis_Icon_Height, Nanos_Icon); + } + else if (tAcc < 10000) // 9999 or less : show as 9.9μs + { + oled.print(tAcc / 1000); + oled.print("."); + oled.print((tAcc / 100) % 10); + displayBitmap(52, 20, Millis_Icon_Width, Millis_Icon_Height, Micros_Icon); + } + else if (tAcc < 100000) // 99999 or less : show as 99μs + { + oled.print(tAcc / 1000); + displayBitmap(44, 20, Millis_Icon_Width, Millis_Icon_Height, Micros_Icon); + } + else if (tAcc < 10000000) // 9999999 or less : show as 9.9ms + { + oled.print(tAcc / 1000000); + oled.print("."); + oled.print((tAcc / 100000) % 10); + displayBitmap(52, 20, Millis_Icon_Width, Millis_Icon_Height, Millis_Icon); + } + else //if (tAcc >= 100000) + { + oled.print(">10"); + displayBitmap(52, 20, Millis_Icon_Width, Millis_Icon_Height, Millis_Icon); + } +} + +/* + 111111111122222222223333333333444444444455555555556666 + 0123456789012345678901234567890123456789012345678901234567890123 + .---------------------------------------------------------------- + 0| ** + 1| ** + 2| ****** + 3| * * + 4| * * **** * * + 5| * * **** * * + 6| * * * * + 7| * * * * + 8| * * * * + 9| * * * * + 10| * * + 11| ****** + 12| +*/ + +//Draw the rover icon depending on screen +void paintDynamicModel() +{ + //Display icon associated with current Dynamic Model + switch (dynamicModel) + { + case (DYN_MODEL_PORTABLE): + displayBitmap(28, 0, DynamicModel_Width, DynamicModel_Height, DynamicModel_1_Portable); + break; + case (DYN_MODEL_STATIONARY): + displayBitmap(28, 0, DynamicModel_Width, DynamicModel_Height, DynamicModel_2_Stationary); + break; + case (DYN_MODEL_PEDESTRIAN): + displayBitmap(28, 0, DynamicModel_Width, DynamicModel_Height, DynamicModel_3_Pedestrian); + break; + case (DYN_MODEL_AUTOMOTIVE): + //Normal rover for ZED-F9P, fusion rover for ZED-F9R + if (zedModuleType == PLATFORM_F9P) + { + displayBitmap(28, 0, DynamicModel_Width, DynamicModel_Height, DynamicModel_4_Automotive); + } + else if (zedModuleType == PLATFORM_F9R) + { + //Blink fusion rover until we have calibration + if (fusionMode == 0) //Initializing + { + //Blink Fusion Rover icon until sensor calibration is complete + if (millis() - lastBaseIconUpdate > 500) + { + lastBaseIconUpdate = millis(); + if (baseIconDisplayed == false) + { + baseIconDisplayed = true; + + //Draw the icon + displayBitmap(28, 2, Rover_Fusion_Width, Rover_Fusion_Height, Rover_Fusion); + } + else + baseIconDisplayed = false; + } + } + else if (fusionMode == 1) //Calibrated + { + //Solid fusion rover + displayBitmap(28, 2, Rover_Fusion_Width, Rover_Fusion_Height, Rover_Fusion); + } + else if (fusionMode == 2 || fusionMode == 3) //Suspended or disabled + { + //Empty rover + displayBitmap(28, 2, Rover_Fusion_Empty_Width, Rover_Fusion_Empty_Height, Rover_Fusion_Empty); + } + } + break; + case (DYN_MODEL_SEA): + displayBitmap(28, 0, DynamicModel_Width, DynamicModel_Height, DynamicModel_5_Sea); + break; + case (DYN_MODEL_AIRBORNE1g): + displayBitmap(28, 0, DynamicModel_Width, DynamicModel_Height, DynamicModel_6_Airborne1g); + break; + case (DYN_MODEL_AIRBORNE2g): + displayBitmap(28, 0, DynamicModel_Width, DynamicModel_Height, DynamicModel_7_Airborne2g); + break; + case (DYN_MODEL_AIRBORNE4g): + displayBitmap(28, 0, DynamicModel_Width, DynamicModel_Height, DynamicModel_8_Airborne4g); + break; + case (DYN_MODEL_WRIST): + displayBitmap(28, 0, DynamicModel_Width, DynamicModel_Height, DynamicModel_9_Wrist); + break; + case (DYN_MODEL_BIKE): + displayBitmap(28, 0, DynamicModel_Width, DynamicModel_Height, DynamicModel_10_Bike); + break; + case (DYN_MODEL_MOWER): + displayBitmap(28, 0, DynamicModel_Width, DynamicModel_Height, DynamicModel_11_Mower); + break; + case (DYN_MODEL_ESCOOTER): + displayBitmap(28, 0, DynamicModel_Width, DynamicModel_Height, DynamicModel_12_EScooter); + break; + } +} + +/* + 111111111122222222223333333333444444444455555555556666 + 0123456789012345678901234567890123456789012345678901234567890123 + .---------------------------------------------------------------- + 35| + 36| ** + 37| * * *** *** + 38| * * * * * * * + 39| * * * * * * * + 40| * * ** * * * * + 41| * * ** * * + 42| * * * * * * + 43| ** * * * * * + 44| **** * * * * * + 45| ** **** ** * * * * + 46| ** ** *** *** + 47| ****** +*/ + +//Select satellite icon and draw sats in view +//Blink icon if no fix +uint32_t paintSIV() +{ + uint32_t blinking; + uint32_t icons; + + oled.setFont(QW_FONT_8X16); //Set font to type 1: 8x16 + oled.setCursor(16, 36); //x, y + oled.print(":"); + + if (online.gnss) + { + if (fixType == 0) //0 = No Fix + oled.print("0"); + else + oled.print(numSV); + + //paintResets(); + + //Determine which icon to display + icons = 0; + if (lbandCorrectionsReceived) + blinking = ICON_SIV_ANTENNA_LBAND; + else + blinking = ICON_SIV_ANTENNA; + + //Determine if there is a fix + if (fixType == 3 || fixType == 4 || fixType == 5) //3D, 3D+DR, or Time + { + //Fix, turn on icon + icons = blinking; + } + else + { + //Blink satellite dish icon if we don't have a fix + blinking_icons ^= blinking; + if (blinking_icons & blinking) + icons = blinking; + } + } //End gnss online + else + { + oled.print("X"); + + icons = ICON_SIV_ANTENNA; + } + return icons; +} + +/* + 111111111122222222223333333333444444444455555555556666 + 0123456789012345678901234567890123456789012345678901234567890123 + .---------------------------------------------------------------- + 35| + 36| ******* + 37| * ** + 38| * ** + 39| * * + 40| * ***** * + 41| * * + 42| * ***** * + 43| * * + 44| * ***** * + 45| * * + 46| * * + 47| ********* +*/ + +//Draw log icon +//Turn off icon if log file fails to get bigger +void paintLogging(bool doUpdate) +{ + //Animate icon to show system running + if (doUpdate) + { + loggingIconDisplayed++; //Goto next icon + loggingIconDisplayed %= 4; //Wrap + } + if ((online.logging == true) && (logIncreasing)) + { + if (loggingType == LOGGING_STANDARD) + { + if (loggingIconDisplayed == 0) + displayBitmap(64 - Logging_0_Width, 48 - Logging_0_Height, Logging_0_Width, Logging_0_Height, Logging_0); + else if (loggingIconDisplayed == 1) + displayBitmap(64 - Logging_1_Width, 48 - Logging_1_Height, Logging_1_Width, Logging_1_Height, Logging_1); + else if (loggingIconDisplayed == 2) + displayBitmap(64 - Logging_2_Width, 48 - Logging_2_Height, Logging_2_Width, Logging_2_Height, Logging_2); + else if (loggingIconDisplayed == 3) + displayBitmap(64 - Logging_3_Width, 48 - Logging_3_Height, Logging_3_Width, Logging_3_Height, Logging_3); + } + else if (loggingType == LOGGING_PPP) + { + if (loggingIconDisplayed == 0) + displayBitmap(64 - Logging_0_Width, 48 - Logging_0_Height, Logging_0_Width, Logging_0_Height, Logging_0); + else if (loggingIconDisplayed == 1) + displayBitmap(64 - Logging_1_Width, 48 - Logging_1_Height, Logging_1_Width, Logging_1_Height, Logging_PPP_1); + else if (loggingIconDisplayed == 2) + displayBitmap(64 - Logging_2_Width, 48 - Logging_2_Height, Logging_2_Width, Logging_2_Height, Logging_PPP_2); + else if (loggingIconDisplayed == 3) + displayBitmap(64 - Logging_3_Width, 48 - Logging_3_Height, Logging_3_Width, Logging_3_Height, Logging_PPP_3); + } + else if (loggingType == LOGGING_CUSTOM) + { + if (loggingIconDisplayed == 0) + displayBitmap(64 - Logging_0_Width, 48 - Logging_0_Height, Logging_0_Width, Logging_0_Height, Logging_0); + else if (loggingIconDisplayed == 1) + displayBitmap(64 - Logging_1_Width, 48 - Logging_1_Height, Logging_1_Width, Logging_1_Height, Logging_Custom_1); + else if (loggingIconDisplayed == 2) + displayBitmap(64 - Logging_2_Width, 48 - Logging_2_Height, Logging_2_Width, Logging_2_Height, Logging_Custom_2); + else if (loggingIconDisplayed == 3) + displayBitmap(64 - Logging_3_Width, 48 - Logging_3_Height, Logging_3_Width, Logging_3_Height, Logging_Custom_3); + } + } + else + { + const int pulseX = 64 - 4; + const int pulseY = oled.getHeight(); + int height; + + //Paint pulse to show system activity + height = loggingIconDisplayed << 2; + if (height) + { + oled.line(pulseX, pulseY, pulseX, pulseY - height); + oled.line(pulseX - 1, pulseY, pulseX - 1, pulseY - height); + } + } +} + +void paintLoggingNTP(bool noPulse, bool doUpdate) +{ + //Animate icon to show system running + if (doUpdate) + { + loggingIconDisplayed++; //Goto next icon + loggingIconDisplayed %= 4; //Wrap + } + if ((online.logging == true) && (logIncreasing)) + { + if (loggingIconDisplayed == 0) + displayBitmap(64 - Logging_0_Width, 48 - Logging_0_Height, Logging_0_Width, Logging_0_Height, Logging_0); + else if (loggingIconDisplayed == 1) + displayBitmap(64 - Logging_1_Width, 48 - Logging_1_Height, Logging_1_Width, Logging_1_Height, Logging_NTP_1); + else if (loggingIconDisplayed == 2) + displayBitmap(64 - Logging_2_Width, 48 - Logging_2_Height, Logging_2_Width, Logging_2_Height, Logging_NTP_2); + else if (loggingIconDisplayed == 3) + displayBitmap(64 - Logging_3_Width, 48 - Logging_3_Height, Logging_3_Width, Logging_3_Height, Logging_NTP_3); + } + else if (!noPulse) + { + const int pulseX = 64 - 4; + const int pulseY = oled.getHeight(); + int height; + + //Paint pulse to show system activity + height = loggingIconDisplayed << 2; + if (height) + { + oled.line(pulseX, pulseY, pulseX, pulseY - height); + oled.line(pulseX - 1, pulseY, pulseX - 1, pulseY - height); + } + } +} + +//Survey in is running. Show 3D Mean and elapsed time. +void paintBaseTempSurveyStarted(bool doUpdate) +{ + oled.setFont(QW_FONT_5X7); + oled.setCursor(0, 23); //x, y + oled.print("Mean:"); + + oled.setCursor(29, 20); //x, y + oled.setFont(QW_FONT_8X16); + if (svinMeanAccuracy < 10.0) //Error check + oled.print(svinMeanAccuracy, 2); + else + oled.print(">10"); + + if (!HAS_ANTENNA_SHORT_OPEN) + { + oled.setCursor(0, 39); //x, y + oled.setFont(QW_FONT_5X7); + oled.print("Time:"); + } + else + { + static uint32_t blinkers = 0; + if (aStatus == SFE_UBLOX_ANTENNA_STATUS_SHORT) + { + if (doUpdate) + blinkers ^= ICON_ANTENNA_SHORT; + else + blinkers |= ICON_ANTENNA_SHORT; + if (blinkers & ICON_ANTENNA_SHORT) + displayBitmap(2, 35, Antenna_Short_Width, Antenna_Short_Height, Antenna_Short); + } + else if (aStatus == SFE_UBLOX_ANTENNA_STATUS_OPEN) + { + if (doUpdate) + blinkers ^= ICON_ANTENNA_OPEN; + else + blinkers |= ICON_ANTENNA_OPEN; + if (blinkers & ICON_ANTENNA_OPEN) + displayBitmap(2, 35, Antenna_Open_Width, Antenna_Open_Height, Antenna_Open); + } + else + { + blinkers &= ~ICON_ANTENNA_SHORT; + blinkers &= ~ICON_ANTENNA_OPEN; + oled.setCursor(0, 39); //x, y + oled.setFont(QW_FONT_5X7); + oled.print("Time:"); + } + } + + + oled.setCursor(30, 36); //x, y + oled.setFont(QW_FONT_8X16); + if (svinObservationTime < 1000) //Error check + oled.print(svinObservationTime); + else + oled.print("0"); +} + +//Given text, a position, and kerning, print text to display +//This is helpful for squishing or stretching a string to appropriately fill the display +void printTextwithKerning(const char *newText, uint8_t xPos, uint8_t yPos, uint8_t kerning) +{ + for (int x = 0 ; x < strlen(newText) ; x++) + { + oled.setCursor(xPos, yPos); + oled.print(newText[x]); + xPos += kerning; + } +} + +//Show transmission of RTCM correction data packets to NTRIP caster +void paintRTCM(bool doUpdate) +{ + int yPos = 17; + if (ntripServerState == NTRIP_SERVER_CASTING) + printTextCenter("Casting", yPos, QW_FONT_8X16, 1, false); //text, y, font type, kerning, inverted + else + printTextCenter("Xmitting", yPos, QW_FONT_8X16, 1, false); //text, y, font type, kerning, inverted + + + if (!HAS_ANTENNA_SHORT_OPEN) + { + oled.setCursor(0, 39); //x, y + oled.setFont(QW_FONT_5X7); + oled.print("RTCM:"); + } + else + { + static uint32_t blinkers = 0; + if (aStatus == SFE_UBLOX_ANTENNA_STATUS_SHORT) + { + if (doUpdate) + blinkers ^= ICON_ANTENNA_SHORT; + else + blinkers |= ICON_ANTENNA_SHORT; + if (blinkers & ICON_ANTENNA_SHORT) + displayBitmap(2, 35, Antenna_Short_Width, Antenna_Short_Height, Antenna_Short); + } + else if (aStatus == SFE_UBLOX_ANTENNA_STATUS_OPEN) + { + if (doUpdate) + blinkers ^= ICON_ANTENNA_OPEN; + else + blinkers |= ICON_ANTENNA_OPEN; + if (blinkers & ICON_ANTENNA_OPEN) + displayBitmap(2, 35, Antenna_Open_Width, Antenna_Open_Height, Antenna_Open); + } + else + { + blinkers &= ~ICON_ANTENNA_SHORT; + blinkers &= ~ICON_ANTENNA_OPEN; + oled.setCursor(0, 39); //x, y + oled.setFont(QW_FONT_5X7); + oled.print("RTCM:"); + } + } + + if (rtcmPacketsSent < 100) + oled.setCursor(30, 36); //x, y - Give space for two digits + else + oled.setCursor(28, 36); //x, y - Push towards colon to make room for log icon + + oled.setFont(QW_FONT_8X16); //Set font to type 1: 8x16 + oled.print(rtcmPacketsSent); //rtcmPacketsSent is controlled in processRTCM() + + //paintResets(); +} + +//Show connecting to NTRIP caster service +void paintConnectingToNtripCaster() +{ + int yPos = 18; + printTextCenter("Caster", yPos, QW_FONT_8X16, 1, false); //text, y, font type, kerning, inverted + + int textX = 3; + int textY = 33; + int textKerning = 6; + oled.setFont(QW_FONT_8X16); + + printTextwithKerning("Connecting", textX, textY, textKerning); +} + +//Scroll through IP address. Wipe with spaces both ends. +void paintIPAddress(bool doUpdate) +{ + char ipAddress[32]; + snprintf(ipAddress, sizeof(ipAddress), " %d.%d.%d.%d ", + ETH.localIP[0], ETH.localIP[1], ETH.localIP[2], ETH.localIP[3]); + + static uint8_t ipAddressPosition = 0; + + //Check if IP address is all single digits and can be printed without scrolling + if (strlen(ipAddress) <= 21) + ipAddressPosition = 7; + + //Print seven characters of IP address + char printThis[9]; + snprintf(printThis, sizeof(printThis), "%c%c%c%c%c%c%c", + ipAddress[ipAddressPosition + 0], ipAddress[ipAddressPosition + 1], + ipAddress[ipAddressPosition + 2], ipAddress[ipAddressPosition + 3], + ipAddress[ipAddressPosition + 4], ipAddress[ipAddressPosition + 5], + ipAddress[ipAddressPosition + 6]); + + oled.setFont(QW_FONT_5X7); //Set font to smallest + oled.setCursor(0, 3); + oled.print(printThis); + + if (doUpdate) + { + ipAddressPosition++; //Increment the print position + if (ipAddress[ipAddressPosition + 7] == 0) //Wrap + ipAddressPosition = 0; + } +} + +void displayBaseStart(uint16_t displayTime) +{ + if (online.display == true) + { + oled.erase(); + + uint8_t fontHeight = 15; //Assume fontsize 1 + uint8_t yPos = oled.getHeight() / 2 - fontHeight; + + printTextCenter("Base", yPos, QW_FONT_8X16, 1, false); //text, y, font type, kerning, inverted + oled.display(); + + oled.display(); + + delay(displayTime); + } +} + +void displayBaseSuccess(uint16_t displayTime) +{ + if (online.display == true) + { + oled.erase(); + + uint8_t fontHeight = 15; //Assume fontsize 1 + uint8_t yPos = oled.getHeight() / 2 - fontHeight; + + printTextCenter("Base", yPos, QW_FONT_8X16, 1, false); //text, y, font type, kerning, inverted + printTextCenter("Started", yPos + fontHeight, QW_FONT_8X16, 1, false); //text, y, font type, kerning, inverted + + oled.display(); + + delay(displayTime); + } +} + +void displayBaseFail(uint16_t displayTime) +{ + if (online.display == true) + { + oled.erase(); + + uint8_t fontHeight = 15; //Assume fontsize 1 + uint8_t yPos = oled.getHeight() / 2 - fontHeight; + + printTextCenter("Base", yPos, QW_FONT_8X16, 1, false); //text, y, font type, kerning, inverted + printTextCenter("Failed", yPos + fontHeight, QW_FONT_8X16, 1, false); //text, y, font type, kerning, inverted + + oled.display(); + + delay(displayTime); + } +} + +void displayGNSSFail(uint16_t displayTime) +{ + displayMessage("GNSS Failed", displayTime); +} + +void displayNoWiFi(uint16_t displayTime) +{ + displayMessage("No WiFi", displayTime); +} + +void displayRoverStart(uint16_t displayTime) +{ + if (online.display == true) + { + oled.erase(); + + uint8_t fontHeight = 15; + uint8_t yPos = oled.getHeight() / 2 - fontHeight; + + printTextCenter("Rover", yPos, QW_FONT_8X16, 1, false); //text, y, font type, kerning, inverted + //printTextCenter("Started", yPos + fontHeight, QW_FONT_8X16, 1, false); //text, y, font type, kerning, inverted + + oled.display(); + + delay(displayTime); + } +} + +void displayRoverSuccess(uint16_t displayTime) +{ + if (online.display == true) + { + oled.erase(); + + uint8_t fontHeight = 15; + uint8_t yPos = oled.getHeight() / 2 - fontHeight; + + printTextCenter("Rover", yPos, QW_FONT_8X16, 1, false); //text, y, font type, kerning, inverted + printTextCenter("Started", yPos + fontHeight, QW_FONT_8X16, 1, false); //text, y, font type, kerning, inverted + + oled.display(); + + delay(displayTime); + } +} + +void displayRoverFail(uint16_t displayTime) +{ + if (online.display == true) + { + oled.erase(); + + uint8_t fontHeight = 15; + uint8_t yPos = oled.getHeight() / 2 - fontHeight; + + printTextCenter("Rover", yPos, QW_FONT_8X16, 1, false); //text, y, font type, kerning, inverted + printTextCenter("Failed", yPos + fontHeight, QW_FONT_8X16, 1, false); //text, y, font type, kerning, inverted + + oled.display(); + + delay(displayTime); + } +} + +void displayAccelFail(uint16_t displayTime) +{ + if (online.display == true) + { + oled.erase(); + + uint8_t fontHeight = 15; + uint8_t yPos = oled.getHeight() / 2 - fontHeight; + + printTextCenter("Accel", yPos, QW_FONT_8X16, 1, false); //text, y, font type, kerning, inverted + printTextCenter("Failed", yPos + fontHeight, QW_FONT_8X16, 1, false); //text, y, font type, kerning, inverted + + oled.display(); + + delay(displayTime); + } +} + +//When user enters serial config menu the display will freeze so show splash while config happens +void displaySerialConfig() +{ + displayMessage("Serial Config", 0); +} + +//Display during blocking stop during to prevent screen freeze +void displayWiFiConnect() +{ + displayMessage("WiFi Connect", 0); +} + +//When user enters WiFi Config mode from setup, show splash while config happens +void displayWiFiConfigNotStarted() +{ + displayMessage("WiFi Config", 0); +} + +void displayWiFiConfig() +{ + int yPos = WiFi_Symbol_Height + 2; + int fontHeight = 8; + + const int displayMaxCharacters = 10; //Characters before pixels start getting cut off. 11 characters can cut off a few pixels. + + printTextCenter("SSID:", yPos, QW_FONT_5X7, 1, false); //text, y, font type, kerning, inverted + + yPos = yPos + fontHeight + 1; + + //Toggle display back and forth for long SSIDs and IPs + //Run the timer no matter what, but load firstHalf/lastHalf with the same thing if strlen < maxWidth + if (millis() - ssidDisplayTimer > 2000) + { + ssidDisplayTimer = millis(); + + if (ssidDisplayFirstHalf == false) + ssidDisplayFirstHalf = true; + else + ssidDisplayFirstHalf = false; + } + + //Convert current SSID to string + char mySSID[50] = {'\0'}; + +#ifdef COMPILE_WIFI + if (settings.wifiConfigOverAP == true) + snprintf(mySSID, sizeof(mySSID), "%s", "RTK Config"); + else + snprintf(mySSID, sizeof(mySSID), "%s", WiFi.SSID().c_str()); +#else + snprintf(mySSID, sizeof(mySSID), "%s", "!Compiled"); +#endif + + char mySSIDFront[displayMaxCharacters + 1]; //1 for null terminator + char mySSIDBack[displayMaxCharacters + 1]; //1 for null terminator + + //Trim SSID to a max length + strncpy(mySSIDFront, mySSID, displayMaxCharacters); + + if (strlen(mySSID) > displayMaxCharacters) + strncpy(mySSIDBack, mySSID + (strlen(mySSID) - displayMaxCharacters), displayMaxCharacters); + else + strncpy(mySSIDBack, mySSID, displayMaxCharacters); + + mySSIDFront[displayMaxCharacters] = '\0'; + mySSIDBack[displayMaxCharacters] = '\0'; + + if (ssidDisplayFirstHalf == true) + printTextCenter(mySSIDFront, yPos, QW_FONT_5X7, 1, false); + else + printTextCenter(mySSIDBack, yPos, QW_FONT_5X7, 1, false); + + yPos = yPos + fontHeight + 3; + printTextCenter("IP:", yPos, QW_FONT_5X7, 1, false); + + yPos = yPos + fontHeight + 1; + +#ifdef COMPILE_AP + IPAddress myIpAddress; + if (settings.wifiConfigOverAP == true) + myIpAddress = WiFi.softAPIP(); + else + myIpAddress = WiFi.localIP(); + + //Convert to string + char myIP[20] = {'\0'}; + snprintf(myIP, sizeof(myIP), "%d.%d.%d.%d", myIpAddress[0], myIpAddress[1], myIpAddress[2], myIpAddress[3]); + + char myIPFront[displayMaxCharacters + 1]; //1 for null terminator + char myIPBack[displayMaxCharacters + 1]; //1 for null terminator + + strncpy(myIPFront, myIP, displayMaxCharacters); + + if (strlen(myIP) > displayMaxCharacters) + strncpy(myIPBack, myIP + (strlen(myIP) - displayMaxCharacters), displayMaxCharacters); + else + strncpy(myIPBack, myIP, displayMaxCharacters); + + myIPFront[displayMaxCharacters] = '\0'; + myIPBack[displayMaxCharacters] = '\0'; + + if (ssidDisplayFirstHalf == true) + printTextCenter(myIPFront, yPos, QW_FONT_5X7, 1, false); + else + printTextCenter(myIPBack, yPos, QW_FONT_5X7, 1, false); + +#else + printTextCenter("!Compiled", yPos, QW_FONT_5X7, 1, false); +#endif +} + +//When user does a factory reset, let us know +void displaySytemReset() +{ + displayMessage("Factory Reset", 0); +} + +void displaySurveyStart(uint16_t displayTime) +{ + if (online.display == true) + { + oled.erase(); + + uint8_t fontHeight = 15; + uint8_t yPos = oled.getHeight() / 2 - fontHeight; + + printTextCenter("Survey", yPos, QW_FONT_8X16, 1, false); //text, y, font type, kerning, inverted + //printTextCenter("Started", yPos + fontHeight, QW_FONT_8X16, 1, false); //text, y, font type, kerning, inverted + + oled.display(); + + delay(displayTime); + } +} + +void displaySurveyStarted(uint16_t displayTime) +{ + if (online.display == true) + { + oled.erase(); + + uint8_t fontHeight = 15; + uint8_t yPos = oled.getHeight() / 2 - fontHeight; + + printTextCenter("Survey", yPos, QW_FONT_8X16, 1, false); //text, y, font type, kerning, inverted + printTextCenter("Started", yPos + fontHeight, QW_FONT_8X16, 1, false); //text, y, font type, kerning, inverted + + oled.display(); + + delay(displayTime); + } +} + +//If the SD card is detected but is not formatted correctly, display warning +void displaySDFail(uint16_t displayTime) +{ + if (online.display == true) + { + oled.erase(); + + uint8_t fontHeight = 15; + uint8_t yPos = oled.getHeight() / 2 - fontHeight; + + printTextCenter("Format", yPos, QW_FONT_8X16, 1, false); //text, y, font type, kerning, inverted + printTextCenter("SD Card", yPos + fontHeight, QW_FONT_8X16, 1, false); //text, y, font type, kerning, inverted + + oled.display(); + + delay(displayTime); + } +} + +//Draw a frame at outside edge +void drawFrame() +{ + //Init and draw box at edge to see screen alignment + int xMax = 63; + int yMax = 47; + oled.line(0, 0, xMax, 0); //Top + oled.line(0, 0, 0, yMax); //Left + oled.line(0, yMax, xMax, yMax); //Bottom + oled.line(xMax, 0, xMax, yMax); //Right +} + +void displayForcedFirmwareUpdate() +{ + displayMessage("Forced Update", 0); +} + +void displayFirmwareUpdateProgress(int percentComplete) +{ + //Update the display if connected + if (online.display == true) + { + oled.erase(); //Clear the display's internal buffer + + int yPos = 3; + int fontHeight = 8; + + printTextCenter("Firmware", yPos, QW_FONT_5X7, 1, false); //text, y, font type, kerning, inverted + + yPos = yPos + fontHeight + 1; + printTextCenter("Update", yPos, QW_FONT_5X7, 1, false); //text, y, font type, kerning, inverted + + yPos = yPos + fontHeight + 3; + char temp[50]; + snprintf(temp, sizeof(temp), "%d%%", percentComplete); + printTextCenter(temp, yPos, QW_FONT_8X16, 1, false); //text, y, font type, kerning, inverted + + oled.display(); //Push internal buffer to display + } +} + +void displayEventMarked(uint16_t displayTime) +{ + displayMessage("Event Marked", displayTime); +} + +void displayNoLogging(uint16_t displayTime) +{ + displayMessage("No Logging", displayTime); +} + +void displayMarked(uint16_t displayTime) +{ + displayMessage("Marked", displayTime); +} + +void displayMarkFailure(uint16_t displayTime) +{ + displayMessage("Mark Failure", displayTime); +} + +void displayNotMarked(uint16_t displayTime) +{ + displayMessage("Not Marked", displayTime); +} + +//Show 'Loading Home2' profile identified +//Profiles may not be sequential (user might have empty profile #2, but filled #3) so we load the profile unit, not the number +void paintProfile(uint8_t profileUnit) +{ + char profileMessage[20]; //'Loading HomeStar' max of ~18 chars + + char profileName[8 + 1]; + //if (getProfileNameFromUnit(profileUnit, profileName, sizeof(profileName)) == true) //Load the profile name, limited to 8 chars + snprintf(profileName, sizeof(profileName), "Profile1"); + { + //settings.updateZEDSettings = true; //When this profile is loaded next, force system to update ZED settings. + //recordSystemSettings(); //Before switching, we need to record the current settings to LittleFS and SD + + //Lookup profileNumber based on unit + //uint8_t profileNumber = getProfileNumberFromUnit(profileUnit); + //recordProfileNumber(profileNumber); //Update internal settings with user's choice, mark unit for config update + + //log_d("Going to profile number %d from unit %d, name '%s'", profileNumber, profileUnit, profileName); + + snprintf(profileMessage, sizeof(profileMessage), "Loading %s", profileName); + displayMessage(profileMessage, 2000); + ESP.restart(); //Profiles require full restart to take effect + } +} + +//Display the setup profiles +void paintDisplaySetupProfile(const char * firstState) +{ + int index; + int itemsDisplayed; + char profileName[8 + 1]; + + //Display the first state if this is the first profile + itemsDisplayed = 0; + if (displayProfile == 0) + { + printTextCenter(firstState, 12 * itemsDisplayed, QW_FONT_8X16, 1, false); + itemsDisplayed++; + } + + //Display Bubble if this is the second profile + if (displayProfile <= 1) + { + printTextCenter("Bubble", 12 * itemsDisplayed, QW_FONT_8X16, 1, false); + itemsDisplayed++; + } + + //Display Config if this is the third profile + if (displayProfile <= 2) + { + printTextCenter("Config", 12 * itemsDisplayed, QW_FONT_8X16, 1, false); + itemsDisplayed++; + } + + // displayProfile itemsDisplayed index + // 0 3 0 + // 1 2 0 + // 2 1 0 + // 3 0 0 + // 4 0 1 + // 5 0 2 + // n >= 3 0 n - 3 + + //Display the profile names + for (index = (displayProfile >= 3) ? displayProfile - 3 : 0; itemsDisplayed < 4; itemsDisplayed++) + { + //Lookup next available profile, limit to 8 characters + //getProfileNameFromUnit(index, profileName, sizeof(profileName)); + snprintf(profileName, sizeof(profileName), "Profile1"); + printTextCenter(profileName, 12 * itemsDisplayed, QW_FONT_8X16, 1, itemsDisplayed == 3); + index++; + } +} + +//Show different menu 'buttons' to allow user to pause on one to select it +void paintDisplaySetup() +{ + if (zedModuleType == PLATFORM_F9P) + { + if (setupState == STATE_MARK_EVENT) + { + if (productVariant == REFERENCE_STATION) + { + //setupState defaults to STATE_MARK_EVENT, which is not a valid state for the Ref Stn. + //It will be corrected by ButtonCheckTask. Until then, display but don't highlight an option. + printTextCenter("Base", 12 * 0, QW_FONT_8X16, 1, false); //string, y, font type, kerning, inverted + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("NTP", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("Cfg Eth", 12 * 3, QW_FONT_8X16, 1, false); + } + else if (online.accelerometer) + { + printTextCenter("Mark", 12 * 0, QW_FONT_8X16, 1, true); //string, y, font type, kerning, inverted + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("Base", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("Bubble", 12 * 3, QW_FONT_8X16, 1, false); + } + else + { + printTextCenter("Mark", 12 * 0, QW_FONT_8X16, 1, true); //string, y, font type, kerning, inverted + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("Base", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("Config", 12 * 3, QW_FONT_8X16, 1, false); + } + } + else if (setupState == STATE_ROVER_NOT_STARTED) + { + if (productVariant == REFERENCE_STATION) + { + printTextCenter("Base", 12 * 0, QW_FONT_8X16, 1, false); //string, y, font type, kerning, inverted + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, true); + printTextCenter("NTP", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("Cfg Eth", 12 * 3, QW_FONT_8X16, 1, false); + } + else if (online.accelerometer) + { + printTextCenter("Mark", 12 * 0, QW_FONT_8X16, 1, false); + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, true); + printTextCenter("Base", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("Bubble", 12 * 3, QW_FONT_8X16, 1, false); + } + else + { + printTextCenter("Mark", 12 * 0, QW_FONT_8X16, 1, false); + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, true); + printTextCenter("Base", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("Config", 12 * 3, QW_FONT_8X16, 1, false); + } + } + else if (setupState == STATE_BASE_NOT_STARTED) + { + if (productVariant == REFERENCE_STATION) + { + printTextCenter("Base", 12 * 0, QW_FONT_8X16, 1, true); //string, y, font type, kerning, inverted + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("NTP", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("Cfg Eth", 12 * 3, QW_FONT_8X16, 1, false); + } + else if (online.accelerometer) + { + printTextCenter("Mark", 12 * 0, QW_FONT_8X16, 1, false); //string, y, font type, kerning, inverted + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("Base", 12 * 2, QW_FONT_8X16, 1, true); + printTextCenter("Bubble", 12 * 3, QW_FONT_8X16, 1, false); + } + else + { + printTextCenter("Mark", 12 * 0, QW_FONT_8X16, 1, false); //string, y, font type, kerning, inverted + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("Base", 12 * 2, QW_FONT_8X16, 1, true); + printTextCenter("Config", 12 * 3, QW_FONT_8X16, 1, false); + } + } + else if (setupState == STATE_NTPSERVER_NOT_STARTED) + { + { + printTextCenter("Base", 12 * 0, QW_FONT_8X16, 1, false); //string, y, font type, kerning, inverted + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("NTP", 12 * 2, QW_FONT_8X16, 1, true); + printTextCenter("Cfg Eth", 12 * 3, QW_FONT_8X16, 1, false); + } + } + else if (setupState == STATE_BUBBLE_LEVEL) + { + if (online.accelerometer) + { + printTextCenter("Mark", 12 * 0, QW_FONT_8X16, 1, false); //string, y, font type, kerning, inverted + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("Base", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("Bubble", 12 * 3, QW_FONT_8X16, 1, true); + } + else + { + //We should never get here, but just in case + printTextCenter("Mark", 12 * 0, QW_FONT_8X16, 1, false); //string, y, font type, kerning, inverted + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("Base", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("Config", 12 * 3, QW_FONT_8X16, 1, true); + } + } + else if (setupState == STATE_CONFIG_VIA_ETH_NOT_STARTED) + { + printTextCenter("Base", 12 * 0, QW_FONT_8X16, 1, false); //string, y, font type, kerning, inverted + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("NTP", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("Cfg Eth", 12 * 3, QW_FONT_8X16, 1, true); + } + else if (setupState == STATE_WIFI_CONFIG_NOT_STARTED) + { + if (productVariant == REFERENCE_STATION) + { + printTextCenter("Rover", 12 * 0, QW_FONT_8X16, 1, false); //string, y, font type, kerning, inverted + printTextCenter("NTP", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("Cfg Eth", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("CfgWiFi", 12 * 3, QW_FONT_8X16, 1, true); + } + else if (online.accelerometer) + { + printTextCenter("Rover", 12 * 0, QW_FONT_8X16, 1, false); + printTextCenter("Base", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("Bubble", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("Config", 12 * 3, QW_FONT_8X16, 1, true); + } + else + { + printTextCenter("Mark", 12 * 0, QW_FONT_8X16, 1, false); + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("Base", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("Config", 12 * 3, QW_FONT_8X16, 1, true); + } + } + else if (setupState == STATE_ESPNOW_PAIRING_NOT_STARTED) + { + if (productVariant == REFERENCE_STATION) + { + printTextCenter("NTP", 12 * 0, QW_FONT_8X16, 1, false); //string, y, font type, kerning, inverted + printTextCenter("Cfg Eth", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("CfgWiFi", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("E-Pair", 12 * 3, QW_FONT_8X16, 1, true); + } + else if (online.accelerometer) + { + printTextCenter("Base", 12 * 0, QW_FONT_8X16, 1, false); + printTextCenter("Bubble", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("Config", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("E-Pair", 12 * 3, QW_FONT_8X16, 1, true); + } + else + { + printTextCenter("Rover", 12 * 0, QW_FONT_8X16, 1, false); + printTextCenter("Base", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("Config", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("E-Pair", 12 * 3, QW_FONT_8X16, 1, true); + } + } + else if (setupState == STATE_PROFILE) + paintDisplaySetupProfile("Base"); + } //end type F9P + else if (zedModuleType == PLATFORM_F9R) + { + if (setupState == STATE_MARK_EVENT) + { + if (online.accelerometer) + { + printTextCenter("Mark", 12 * 0, QW_FONT_8X16, 1, true); //string, y, font type, kerning, inverted + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("Bubble", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("Config", 12 * 3, QW_FONT_8X16, 1, false); + } + else + { + printTextCenter("Mark", 12 * 0, QW_FONT_8X16, 1, true); //string, y, font type, kerning, inverted + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("Config", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("E-Pair", 12 * 3, QW_FONT_8X16, 1, false); + } + } + else if (setupState == STATE_ROVER_NOT_STARTED) + { + if (online.accelerometer) + { + printTextCenter("Mark", 12 * 0, QW_FONT_8X16, 1, false); + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, true); + printTextCenter("Bubble", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("Config", 12 * 3, QW_FONT_8X16, 1, false); + } + else + { + printTextCenter("Mark", 12 * 0, QW_FONT_8X16, 1, false); + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, true); + printTextCenter("Config", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("E-Pair", 12 * 3, QW_FONT_8X16, 1, false); + } + } + else if (setupState == STATE_BUBBLE_LEVEL) + { + if (online.accelerometer) + { + printTextCenter("Mark", 12 * 0, QW_FONT_8X16, 1, false); + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("Bubble", 12 * 2, QW_FONT_8X16, 1, true); + printTextCenter("Config", 12 * 3, QW_FONT_8X16, 1, false); + } + else + { + //We should never get here, but just in case + printTextCenter("Mark", 12 * 0, QW_FONT_8X16, 1, false); + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("Config", 12 * 2, QW_FONT_8X16, 1, true); + printTextCenter("E-Pair", 12 * 3, QW_FONT_8X16, 1, false); + } + } + else if (setupState == STATE_WIFI_CONFIG_NOT_STARTED) + { + if (online.accelerometer) + { + printTextCenter("Mark", 12 * 0, QW_FONT_8X16, 1, false); + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("Bubble", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("Config", 12 * 3, QW_FONT_8X16, 1, true); + } + else + { + printTextCenter("Mark", 12 * 0, QW_FONT_8X16, 1, false); + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("Config", 12 * 2, QW_FONT_8X16, 1, true); + printTextCenter("E-Pair", 12 * 3, QW_FONT_8X16, 1, false); + } + } + else if (setupState == STATE_ESPNOW_PAIRING_NOT_STARTED) + { + if (online.accelerometer) + { + printTextCenter("Rover", 12 * 0, QW_FONT_8X16, 1, false); + printTextCenter("Bubble", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("Config", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("E-Pair", 12 * 3, QW_FONT_8X16, 1, true); + } + else + { + printTextCenter("Mark", 12 * 0, QW_FONT_8X16, 1, false); + printTextCenter("Rover", 12 * 1, QW_FONT_8X16, 1, false); + printTextCenter("Config", 12 * 2, QW_FONT_8X16, 1, false); + printTextCenter("E-Pair", 12 * 3, QW_FONT_8X16, 1, true); + } + } + else if (setupState == STATE_PROFILE) + paintDisplaySetupProfile("Rover"); + } //end type F9R +} + +//Given text, and location, print text center of the screen +void printTextCenter(const char *text, uint8_t yPos, QwiicFont & fontType, uint8_t kerning, bool highlight) //text, y, font type, kearning, inverted +{ + oled.setFont(fontType); + oled.setDrawMode(grROPXOR); + + uint8_t fontWidth = fontType.width; + if (fontWidth == 8) fontWidth = 7; //8x16, but widest character is only 7 pixels. + + uint8_t xStart = (oled.getWidth() / 2) - ((strlen(text) * (fontWidth + kerning)) / 2) + 1; + + uint8_t xPos = xStart; + for (int x = 0 ; x < strlen(text) ; x++) + { + oled.setCursor(xPos, yPos); + oled.print(text[x]); + xPos += fontWidth + kerning; + } + + if (highlight) //Draw a box, inverted over text + { + uint8_t textPixelWidth = strlen(text) * (fontWidth + kerning); + + //Error check + int xBoxStart = xStart - 5; + if (xBoxStart < 0) xBoxStart = 0; + int xBoxEnd = textPixelWidth + 9; + if (xBoxEnd > oled.getWidth() - 1) xBoxEnd = oled.getWidth() - 1; + + oled.rectangleFill(xBoxStart, yPos, xBoxEnd, 12, 1); //x, y, width, height, color + } +} + +//Given a message (one or two words) display centered +void displayMessage(const char* message, uint16_t displayTime) +{ + if (online.display == true) + { + char temp[21]; + uint8_t fontHeight = 15; //Assume fontsize 1 + + //Count words based on spaces + uint8_t wordCount = 0; + strncpy(temp, message, sizeof(temp) - 1); //strtok modifies the message so make copy + char * token = strtok(temp, " "); + while (token != nullptr) + { + wordCount++; + token = strtok(nullptr, " "); + } + + uint8_t yPos = (oled.getHeight() / 2) - (fontHeight / 2); + if (wordCount == 2) yPos -= (fontHeight / 2); + + oled.erase(); + + //drawFrame(); + + strncpy(temp, message, sizeof(temp) - 1); + token = strtok(temp, " "); + while (token != nullptr) + { + printTextCenter(token, yPos, QW_FONT_8X16, 1, false); //text, y, font type, kerning, inverted + token = strtok(nullptr, " "); + yPos += fontHeight; + } + + oled.display(); + + delay(displayTime); + } +} + +//Wrapper to avoid needing to pass width/height data twice +void displayBitmap(uint8_t x, uint8_t y, uint8_t imageWidth, uint8_t imageHeight, const uint8_t *imageData) +{ + oled.bitmap(x, y, x + imageWidth, y + imageHeight, (uint8_t *)imageData, imageWidth, imageHeight); +} + +//Show screen while ESP-Now is pairing +void paintEspNowPairing() +{ + displayMessage("ESP-Now Pairing", 0); +} +void paintEspNowPaired() +{ + displayMessage("ESP-Now Paired", 2000); +} + +void displayNtpStart(uint16_t displayTime) +{ + if (online.display == true) + { + oled.erase(); + + uint8_t fontHeight = 15; + uint8_t yPos = oled.getHeight() / 2 - fontHeight; + + printTextCenter("NTP", yPos, QW_FONT_8X16, 1, false); //text, y, font type, kerning, inverted + + oled.display(); + + delay(displayTime); + } +} + +void displayNtpStarted(uint16_t displayTime) +{ + if (online.display == true) + { + oled.erase(); + + uint8_t fontHeight = 15; + uint8_t yPos = oled.getHeight() / 2 - fontHeight; + + printTextCenter("NTP", yPos, QW_FONT_8X16, 1, false); //text, y, font type, kerning, inverted + printTextCenter("Started", yPos + fontHeight, QW_FONT_8X16, 1, false); //text, y, font type, kerning, inverted + + oled.display(); + + delay(displayTime); + } +} + +void displayNtpNotReady(uint16_t displayTime) +{ + if (online.display == true) + { + oled.erase(); + + uint8_t fontHeight = 8; + uint8_t yPos = oled.getHeight() / 2 - fontHeight; + + printTextCenter("Ethernet", yPos, QW_FONT_5X7, 1, false); //text, y, font type, kerning, inverted + printTextCenter("Not Ready", yPos + fontHeight, QW_FONT_5X7, 1, false); //text, y, font type, kerning, inverted + + oled.display(); + + delay(displayTime); + } +} + +void displayNTPFail(uint16_t displayTime) +{ + if (online.display == true) + { + oled.erase(); + + uint8_t fontHeight = 8; + uint8_t yPos = oled.getHeight() / 2 - fontHeight; + + printTextCenter("NTP", yPos, QW_FONT_5X7, 1, false); //text, y, font type, kerning, inverted + printTextCenter("Failed", yPos + fontHeight, QW_FONT_5X7, 1, false); //text, y, font type, kerning, inverted + + oled.display(); + + delay(displayTime); + } +} + +void displayConfigViaEthNotStarted(uint16_t displayTime) +{ + if (online.display == true) + { + oled.erase(); + + uint8_t fontHeight = 8; + uint8_t yPos = fontHeight; + + printTextCenter("Configure", yPos, QW_FONT_5X7, 1, false); //text, y, font type, kerning, inverted + yPos += fontHeight; + printTextCenter("Via", yPos, QW_FONT_5X7, 1, false); //text, y, font type, kerning, inverted + yPos += fontHeight; + printTextCenter("Ethernet", yPos, QW_FONT_5X7, 1, false); //text, y, font type, kerning, inverted + yPos += fontHeight; + printTextCenter("Restart", yPos, QW_FONT_5X7, 1, true); //text, y, font type, kerning, inverted + + oled.display(); + + delay(displayTime); + } +} + +void displayConfigViaEthStarted(uint16_t displayTime) +{ + if (online.display == true) + { + oled.erase(); + + uint8_t fontHeight = 8; + uint8_t yPos = fontHeight; + + printTextCenter("Configure", yPos, QW_FONT_5X7, 1, false); //text, y, font type, kerning, inverted + yPos += fontHeight; + printTextCenter("Via", yPos, QW_FONT_5X7, 1, false); //text, y, font type, kerning, inverted + yPos += fontHeight; + printTextCenter("Ethernet", yPos, QW_FONT_5X7, 1, false); //text, y, font type, kerning, inverted + yPos += fontHeight; + printTextCenter("Started", yPos, QW_FONT_5X7, 1, false); //text, y, font type, kerning, inverted + + oled.display(); + + delay(displayTime); + } +} + +void displayConfigViaEthernet(bool doUpdate) +{ + if (online.display == true) + { + oled.erase(); + + uint8_t xPos = (oled.getWidth() / 2) - (Ethernet_Icon_Width / 2); + uint8_t yPos = Ethernet_Icon_Height / 2; + + static bool blink = 0; + blink ^= 1; + + if (ETH.linkUp || blink) + displayBitmap(xPos, yPos, Ethernet_Icon_Width, Ethernet_Icon_Height, Ethernet_Icon); + + yPos += Ethernet_Icon_Height * 1.5; + + printTextCenter("IP:", yPos, QW_FONT_5X7, 1, false); //text, y, font type, kerning, inverted + yPos += 8; + + char ipAddress[40]; + IPAddress localIP = ETH.localIP; + snprintf(ipAddress, sizeof(ipAddress), " %d.%d.%d.%d ", + localIP[0], localIP[1], localIP[2], localIP[3]); + + static uint8_t ipAddressPosition = 0; + + //Print ten characters of IP address + char printThis[12]; + + //Check if the IP address is <= 10 chars and will fit without scrolling + if (strlen(ipAddress) <= 28) + ipAddressPosition = 9; + else if (strlen(ipAddress) <= 30) + ipAddressPosition = 10; + + snprintf(printThis, sizeof(printThis), "%c%c%c%c%c%c%c%c%c%c", + ipAddress[ipAddressPosition + 0], ipAddress[ipAddressPosition + 1], + ipAddress[ipAddressPosition + 2], ipAddress[ipAddressPosition + 3], + ipAddress[ipAddressPosition + 4], ipAddress[ipAddressPosition + 5], + ipAddress[ipAddressPosition + 6], ipAddress[ipAddressPosition + 7], + ipAddress[ipAddressPosition + 8], ipAddress[ipAddressPosition + 9]); + + oled.setCursor(0, yPos); + oled.print(printThis); + + if (doUpdate) + { + ipAddressPosition++; //Increment the print position + if (ipAddress[ipAddressPosition + 10] == 0) //Wrap + ipAddressPosition = 0; + } + + oled.display(); + } +} + +const uint8_t * getMacAddress() +{ + static const uint8_t zero[6] = {0, 0, 0, 0, 0xAB, 0xCD}; + return zero; +} diff --git a/Firmware/Test Sketches/Hookup_Display/Hookup_Display.ino b/Firmware/Test Sketches/Hookup_Display/Hookup_Display.ino new file mode 100644 index 000000000..a5b7192ce --- /dev/null +++ b/Firmware/Test Sketches/Hookup_Display/Hookup_Display.ino @@ -0,0 +1,125 @@ +// RTK Display Test +// Configures the OLED display for Hookup Guide photos + +// Select one of the provided systemState's below +// Optionally select one of the antenna status (open / short) + +// Press and hold the MODE button to update / advance the display. Release to make the display static. +// I.e. push and hold MODE until the display looks nice! ;-) + + +#define HAS_BATTERY false +#define HAS_ANTENNA_SHORT_OPEN true + +#include "settings.h" + + + +//SystemState systemState = STATE_BASE_FIXED_TRANSMITTING; // <--- Use this one for "Base Casting" + +//SystemState systemState = STATE_ROVER_RTK_FIX; // <--- Use this one for "Rover Fixed" + +SystemState systemState = STATE_NTPSERVER_SYNC; // <--- Use this one for "NTP" + + + + +//GNSS configuration for Dynamic Model +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +#include //http://librarymanager/All#SparkFun_u-blox_GNSS_v3 v3.0.5 + +const int aStatus = 0; // <--- Change to SFE_UBLOX_ANTENNA_STATUS_SHORT or SFE_UBLOX_ANTENNA_STATUS_OPEN if desired + +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + + + +struct +{ + bool display = true; + bool gnss = true; + bool logging = true; + bool accelerometer = false; + ethernetStatus_e ethernetStatus = ETH_CONNECTED; +} online; + +SystemState setupState = STATE_MARK_EVENT; +const int displayProfile = 0; +const float battLevel = 70; +const float horizontalAccuracy = 0.0141; +const uint8_t tAcc = 26; +const int dynamicModel = DYN_MODEL_PORTABLE; +const int PLATFORM_F9P = 0; +const int PLATFORM_F9R = 1; +int zedModuleType = PLATFORM_F9P; +const int numSV = 30; +const int fusionMode = 1; +const int fixType = 3; +const bool lbandCorrectionsReceived = false; +uint8_t loggingIconDisplayed = 0; //Increases every 500ms while logging +const bool logIncreasing = true; +const float svinMeanAccuracy = 0.97; +const int svinObservationTime = 57; +const int rtcmPacketsSent = 123; + +struct +{ + bool linkUp = true; + IPAddress localIP = { 192, 168, 0, 45 }; +} ETH; + +//Hardware connections +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +int pin_peripheralPowerControl = 32; //Reference Station power control is on pin 32. Set to -1 if not needed. +int pin_setupButton = 0; //Reference Station MODE is on pin 0. Set to -1 if not needed. +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +//I2C for GNSS, battery gauge, display, accelerometer +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +#include +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +//External Display +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +#include //http://librarymanager/All#SparkFun_Qwiic_Graphic_OLED +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +uint32_t lastDisplayUpdate = 0; +bool firstRadioSpotBlink = false; //Controls when the shared icon space is toggled +unsigned long firstRadioSpotTimer = 0; +bool secondRadioSpotBlink = false; //Controls when the shared icon space is toggled +unsigned long secondRadioSpotTimer = 0; +bool thirdRadioSpotBlink = false; //Controls when the shared icon space is toggled +unsigned long thirdRadioSpotTimer = 0; +uint32_t lastBaseIconUpdate = 0; +bool baseIconDisplayed = false; //Toggles as lastBaseIconUpdate goes above 1000ms + +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + +void setup() +{ + Serial.begin(115200); //UART0 for programming and debugging + + if (pin_peripheralPowerControl >= 0) + { + pinMode(pin_peripheralPowerControl, OUTPUT); + digitalWrite(pin_peripheralPowerControl, HIGH); //Turn on SD, W5500, etc + } + + if (pin_setupButton >= 0) + { + pinMode(pin_setupButton, INPUT_PULLUP); + } + + Wire.begin(); + + beginDisplay(); //Start display to be able to display any errors + +} + +void loop() +{ + updateDisplay((pin_setupButton >= 0) ? digitalRead(pin_setupButton) == LOW : true); //Only 'increment' the display if the MODE / Setup button is pressed +} diff --git a/Firmware/Test Sketches/Hookup_Display/icons.h b/Firmware/Test Sketches/Hookup_Display/icons.h new file mode 100644 index 000000000..298501232 --- /dev/null +++ b/Firmware/Test Sketches/Hookup_Display/icons.h @@ -0,0 +1,1593 @@ +//Create a bitmap then use http://en.radzio.dxp.pl/bitmap_converter/ to generate output +//Make sure the bitmap is n*8 pixels tall (pad white pixels to lower area as needed) +//Otherwise the bitmap bitmap_converter will compress some of the bytes together + +/* + BT_Symbol [7, 14] + + 1234567 + .-------. + 0x01| * | + 0x02| ** | + 0x04| *** | + 0x08|* * **| + 0x10|** * **| + 0x20| ***** | + 0x40| *** | + 0x80| *** | + 0x01| ***** | + 0x02|** * **| + 0x04|* * **| + 0x08| *** | + 0x10| ** | + 0x20| * | + '-------' +*/ + +const int BT_Symbol_Height = 14; +const int BT_Symbol_Width = 7; +const uint8_t BT_Symbol [] = { + 0x18, 0x30, 0xE0, 0xFF, 0xE6, 0x3C, 0x18, + 0x06, 0x03, 0x01, 0x3F, 0x19, 0x0F, 0x06 +}; + +/* + WiFi_Symbol_3 [13, 9] + + 1 + 1234567890123 + .-------------. + 0x01| ******* | + 0x02| * * | + 0x04| * ***** * | + 0x08|* * * *| + 0x10| * *** * | + 0x20| * * | + 0x40| * | + 0x80| *** | + 0x01| * | + '-------------' +*/ + +const int WiFi_Symbol_Height = 9; +const int WiFi_Symbol_Width = 13; +const uint8_t WiFi_Symbol_3 [] = { + 0x08, 0x04, 0x12, 0x09, 0x25, 0x95, 0xD5, 0x95, 0x25, 0x09, 0x12, 0x04, 0x08, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + +/* + WiFi_Symbol_2 [13, 9] + + 1 + 1234567890123 + .-------------. + 0x01| | + 0x02| | + 0x04| ***** | + 0x08| * * | + 0x10| * *** * | + 0x20| * * | + 0x40| * | + 0x80| *** | + 0x01| * | + '-------------' +*/ + +const uint8_t WiFi_Symbol_2 [] = { + 0x00, 0x00, 0x10, 0x08, 0x24, 0x94, 0xD4, 0x94, 0x24, 0x08, 0x10, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + +/* + WiFi_Symbol_1 [13, 9] + + 1 + 1234567890123 + .-------------. + 0x01| | + 0x02| | + 0x04| | + 0x08| | + 0x10| *** | + 0x20| * * | + 0x40| * | + 0x80| *** | + 0x01| * | + '-------------' +*/ + +const uint8_t WiFi_Symbol_1 [] = { + 0x00, 0x00, 0x00, 0x00, 0x20, 0x90, 0xD0, 0x90, 0x20, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + +/* + WiFi_Symbol_0 [13, 9] + + 1 + 1234567890123 + .-------------. + 0x01| | + 0x02| | + 0x04| | + 0x08| | + 0x10| | + 0x20| | + 0x40| * | + 0x80| *** | + 0x01| * | + '-------------' +*/ + +const uint8_t WiFi_Symbol_0 [] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xC0, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + +/* + Clock [15, 15] + + 1 + 123456789012345 + .---------------. + 0x01| ***** | + 0x02| ** * ** | + 0x04| * * * * | + 0x08| * * * | + 0x10| ** * ** | + 0x20|* * *| + 0x40|* * *| + 0x80|** * **| + 0x01|* ** *| + 0x02|* * *| + 0x04| ** ** | + 0x08| * * | + 0x10| * * * * | + 0x20| ** * ** | + 0x40| ***** | + '---------------' +*/ + +const int Clock_Icon_Height = 15; +const int Clock_Icon_Width = 15; +const uint8_t Clock_Icon_1 [] = { + 0xE0, 0x98, 0x14, 0x02, 0x06, 0x01, 0x01, 0xFB, 0x01, 0x01, 0x06, 0x02, 0x14, 0x98, 0xE0, + 0x03, 0x0C, 0x14, 0x20, 0x30, 0x40, 0x40, 0x60, 0x41, 0x41, 0x32, 0x20, 0x14, 0x0C, 0x03 +}; + +/* + Clock [15, 15] + + 1 + 123456789012345 + .---------------. + 0x01| ***** | + 0x02| ** * ** | + 0x04| * * * * | + 0x08| * * | + 0x10| ** ** | + 0x20|* *| + 0x40|* *| + 0x80|** ***** **| + 0x01|* ** *| + 0x02|* * *| + 0x04| ** ** | + 0x08| * * | + 0x10| * * * * | + 0x20| ** * ** | + 0x40| ***** | + '---------------' +*/ + +const uint8_t Clock_Icon_2 [] = { + 0xE0, 0x98, 0x14, 0x02, 0x06, 0x01, 0x01, 0x83, 0x81, 0x81, 0x86, 0x82, 0x14, 0x98, 0xE0, + 0x03, 0x0C, 0x14, 0x20, 0x30, 0x40, 0x40, 0x60, 0x41, 0x41, 0x32, 0x20, 0x14, 0x0C, 0x03 +}; + +/* + Clock [15, 15] + + 1 + 123456789012345 + .---------------. + 0x01| ***** | + 0x02| ** * ** | + 0x04| * * * * | + 0x08| * * | + 0x10| ** ** | + 0x20|* *| + 0x40|* *| + 0x80|** * **| + 0x01|* ** *| + 0x02|* ** *| + 0x04| ** * * ** | + 0x08| * * * | + 0x10| * * * * | + 0x20| ** * ** | + 0x40| ***** | + '---------------' +*/ + +const uint8_t Clock_Icon_3 [] = { + 0xE0, 0x98, 0x14, 0x02, 0x06, 0x01, 0x01, 0x83, 0x01, 0x01, 0x06, 0x02, 0x14, 0x98, 0xE0, + 0x03, 0x0C, 0x14, 0x20, 0x30, 0x40, 0x40, 0x6F, 0x43, 0x44, 0x30, 0x20, 0x14, 0x0C, 0x03 +}; + +/* + Clock [15, 15] + + 1 + 123456789012345 + .---------------. + 0x01| ***** | + 0x02| ** * ** | + 0x04| * * * * | + 0x08| * * | + 0x10| ** ** | + 0x20|* *| + 0x40|* *| + 0x80|** ***** **| + 0x01|* * *| + 0x02|* * *| + 0x04| ** * ** | + 0x08| * * | + 0x10| * * * * | + 0x20| ** * ** | + 0x40| ***** | + '---------------' +*/ + +const uint8_t Clock_Icon_4 [] = { + 0xE0, 0x98, 0x14, 0x82, 0x86, 0x81, 0x81, 0x83, 0x01, 0x01, 0x06, 0x02, 0x14, 0x98, 0xE0, + 0x03, 0x0C, 0x14, 0x20, 0x30, 0x40, 0x40, 0x60, 0x43, 0x44, 0x30, 0x20, 0x14, 0x0C, 0x03 +}; + +/* + CrossHair [15, 15] + + 1 + 123456789012345 + .---------------. + 0x01| * | + 0x02| * | + 0x04| ******* | + 0x08| * * * | + 0x10| * * * | + 0x20| * * * | + 0x40| * * * | + 0x80|******* *******| + 0x01| * * * | + 0x02| * * * | + 0x04| * * * | + 0x08| * * * | + 0x10| ******* | + 0x20| * | + 0x40| * | + '---------------' +*/ + +const int CrossHair_Height = 15; +const int CrossHair_Width = 15; +const uint8_t CrossHair [] = { + 0x80, 0x80, 0xF0, 0x88, 0x84, 0x84, 0x84, 0x7F, 0x84, 0x84, 0x84, 0x88, 0xF0, 0x80, 0x80, + 0x00, 0x00, 0x07, 0x08, 0x10, 0x10, 0x10, 0x7F, 0x10, 0x10, 0x10, 0x08, 0x07, 0x00, 0x00 +}; + +/* + CrossHairDual [15, 15] + + 1 + 123456789012345 + .---------------. + 0x01| * | + 0x02| * | + 0x04| ******* | + 0x08| * * * | + 0x10| * ***** * | + 0x20| * * * * * | + 0x40| * * * * * | + 0x80|******* *******| + 0x01| * * * * * | + 0x02| * * * * * | + 0x04| * ***** * | + 0x08| * * * | + 0x10| ******* | + 0x20| * | + 0x40| * | + '---------------' +*/ + +const int CrossHairDual_Height = 15; +const int CrossHairDual_Width = 15; +const uint8_t CrossHairDual [] = { + 0x80, 0x80, 0xF0, 0x88, 0xE4, 0x94, 0x94, 0x7F, 0x94, 0x94, 0xE4, 0x88, 0xF0, 0x80, 0x80, + 0x00, 0x00, 0x07, 0x08, 0x13, 0x14, 0x14, 0x7F, 0x14, 0x14, 0x13, 0x08, 0x07, 0x00, 0x00 +}; + +/* + SIV_Antenna [12, 13] + + 1 + 123456789012 + .------------. + 0x01| | + 0x02| ** | + 0x04| * * | + 0x08| * * * | + 0x10| * * * | + 0x20| * * | + 0x40| * * | + 0x80| * * | + 0x01| ** * | + 0x02| **** * | + 0x04| ** **** | + 0x08| ** | + 0x10| ****** | + '------------' +*/ + +const int SIV_Antenna_Height = 13; +const int SIV_Antenna_Width = 12; +const uint8_t SIV_Antenna [] = { + 0x00, 0x1E, 0x62, 0x84, 0x08, 0x10, 0x20, 0x50, 0x88, 0x00, 0x00, 0x00, + 0x00, 0x10, 0x10, 0x1F, 0x1F, 0x12, 0x12, 0x04, 0x04, 0x05, 0x06, 0x00 +}; + +/* + SIV_Antenna_LBand [12, 13] + + 1 + 123456789012 + .------------. + 0x01| | + 0x02| ** * | + 0x04| * * * | + 0x08| * * * | + 0x10| * * * | + 0x20| * * * | + 0x40| * * * | + 0x80| * * | + 0x01| ** * | + 0x02| **** * | + 0x04| ** **** | + 0x08| ** | + 0x10| ****** | + '------------' +*/ + +const int SIV_Antenna_LBand_Height = 13; +const int SIV_Antenna_LBand_Width = 12; +const uint8_t SIV_Antenna_LBand [] = { + 0x00, 0x1E, 0x62, 0x84, 0x08, 0x14, 0x22, 0x50, 0x88, 0x40, 0x20, 0x00, + 0x00, 0x10, 0x10, 0x1F, 0x1F, 0x12, 0x12, 0x04, 0x04, 0x05, 0x06, 0x00 +}; + +/* + Antenna_Short [12, 13] + + 1 + 123456789012 + .------------. + 0x01| * | + 0x02| * | + 0x04| * | + 0x08| ** | + 0x10| * * | + 0x20| * ***** | + 0x40| * * | + 0x80| ***** * | + 0x01| * * | + 0x02| ** | + 0x04| * | + 0x08| * | + 0x10| * | + '------------' +*/ + +const int Antenna_Short_Height = 13; +const int Antenna_Short_Width = 12; +const uint8_t Antenna_Short [] = { + 0x00, 0x80, 0xC0, 0xA0, 0x90, 0x88, 0x3F, 0x20, 0xA0, 0x60, 0x20, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x02, 0x01, 0x00, 0x00, 0x00, 0x00 +}; + +/* + Antenna_Open [12, 13] + + 1 + 123456789012 + .------------. + 0x01| ** | + 0x02| ** | + 0x04| ** | + 0x08| ** | + 0x10| ** | + 0x20| ** | + 0x40| ** ** | + 0x80| ** | + 0x01| ** | + 0x02| ** | + 0x04| ** | + 0x08| ** | + 0x10| ** | + '------------' +*/ + +const int Antenna_Open_Height = 13; +const int Antenna_Open_Width = 12; +const uint8_t Antenna_Open [] = { + 0x00, 0x00, 0x00, 0x60, 0x70, 0x1F, 0x0F, 0xC0, 0xC0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x1F, 0x01, 0x00, 0x00, 0x00, 0x00 +}; + +/* + Rover_Fusion [15, 9] + + 1 + 123456789012345 + .---------------. + 0x01| ********* | + 0x02|* * | + 0x04|* **** ****| + 0x08|* * *| + 0x10|* *** *| + 0x20|* ** * ** *| + 0x40| * ******* * | + 0x80| * * * * | + 0x01| ** ** | + '---------------' +*/ + +const int Rover_Fusion_Height = 9; +const int Rover_Fusion_Width = 15; +const uint8_t Rover_Fusion [] = { + 0x3E, 0xC1, 0x21, 0x21, 0xC1, 0x7D, 0x55, 0x55, 0x45, 0x41, 0xC2, 0x24, 0x24, 0xC4, 0x3C, + 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00 +}; + +/* + Rover_Fusion_Empty [15, 9] + + 1 + 123456789012345 + .---------------. + 0x01| ********* | + 0x02|* * | + 0x04|* ****| + 0x08|* *| + 0x10|* *| + 0x20|* ** ** *| + 0x40| * ******* * | + 0x80| * * * * | + 0x01| ** ** | + '---------------' +*/ + +const int Rover_Fusion_Empty_Height = 9; +const int Rover_Fusion_Empty_Width = 15; +const uint8_t Rover_Fusion_Empty [] = { + 0x3E, 0xC1, 0x21, 0x21, 0xC1, 0x41, 0x41, 0x41, 0x41, 0x41, 0xC2, 0x24, 0x24, 0xC4, 0x3C, + 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00 +}; + +/* + BaseTemporary [14, 12] + + 1 + 12345678901234 + .--------------. + 0x01| **** *** | + 0x02| * ****** ** | + 0x04| * ** ** * | + 0x08| *** * * * | + 0x10| *** ** *** | + 0x20| * * **** ** | + 0x40| * ** ** * | + 0x80| ***** *** * | + 0x01| * *** ** | + 0x02| * | + 0x04| * | + 0x08| * | + '--------------' +*/ + +const int BaseTemporary_Height = 12; +const int BaseTemporary_Width = 14; +const uint8_t BaseTemporary [] = { + 0x00, 0xFF, 0x99, 0x99, 0xE7, 0xCE, 0x32, 0x32, 0xE7, 0xE7, 0x99, 0x32, 0xFE, 0x00, + 0x00, 0x1F, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00 +}; + +/* + BaseFixed [14, 12] + + 1 + 12345678901234 + .--------------. + 0x01| ***** ***** | + 0x02| * * * * * * | + 0x04| * * * * * * | + 0x08| * * **** * * | + 0x10| * * | + 0x20| * * | + 0x40| * * | + 0x80| * **** * | + 0x01| * * * * | + 0x02|* * * *| + 0x04|* * * *| + 0x08|****** ******| + '--------------' +*/ + +const int BaseFixed_Height = 12; +const int BaseFixed_Width = 14; +const uint8_t BaseFixed [] = { + 0x00, 0xFF, 0x01, 0x0F, 0x01, 0x8F, 0x88, 0x88, 0x8F, 0x01, 0x0F, 0x01, 0xFF, 0x00, + 0x0E, 0x09, 0x08, 0x08, 0x08, 0x0F, 0x00, 0x00, 0x0F, 0x08, 0x08, 0x08, 0x09, 0x0E +}; + +/* + Battery_3 [19, 12] + + 1 + 1234567890123456789 + .-------------------. + 0x01|***************** | + 0x02|* * | + 0x04|* *** *** *** * | + 0x08|* *** *** *** ***| + 0x10|* *** *** *** *| + 0x20|* *** *** *** *| + 0x40|* *** *** *** *| + 0x80|* *** *** *** *| + 0x01|* *** *** *** ***| + 0x02|* *** *** *** * | + 0x04|* * | + 0x08|***************** | + '-------------------' +*/ + +const int Battery_3_Height = 12; +const int Battery_3_Width = 19; +const uint8_t Battery_3 [] = { + 0xFF, 0x01, 0xFD, 0xFD, 0xFD, 0x01, 0x01, 0xFD, 0xFD, 0xFD, 0x01, 0x01, 0xFD, 0xFD, 0xFD, 0x01, 0x0F, 0x08, 0xF8, + 0x0F, 0x08, 0x0B, 0x0B, 0x0B, 0x08, 0x08, 0x0B, 0x0B, 0x0B, 0x08, 0x08, 0x0B, 0x0B, 0x0B, 0x08, 0x0F, 0x01, 0x01 +}; + +/* + Battery_2 [19, 12] + + 1 + 1234567890123456789 + .-------------------. + 0x01|***************** | + 0x02|* * | + 0x04|* *** *** * | + 0x08|* *** *** ***| + 0x10|* *** *** *| + 0x20|* *** *** *| + 0x40|* *** *** *| + 0x80|* *** *** *| + 0x01|* *** *** ***| + 0x02|* *** *** * | + 0x04|* * | + 0x08|***************** | + '-------------------' +*/ + +const int Battery_2_Height = 12; +const int Battery_2_Width = 19; +const uint8_t Battery_2 [] = { + 0xFF, 0x01, 0xFD, 0xFD, 0xFD, 0x01, 0x01, 0xFD, 0xFD, 0xFD, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x0F, 0x08, 0xF8, + 0x0F, 0x08, 0x0B, 0x0B, 0x0B, 0x08, 0x08, 0x0B, 0x0B, 0x0B, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0F, 0x01, 0x01 +}; + +/* + Battery_1 [19, 12] + + 1 + 1234567890123456789 + .-------------------. + 0x01|***************** | + 0x02|* * | + 0x04|* *** * | + 0x08|* *** ***| + 0x10|* *** *| + 0x20|* *** *| + 0x40|* *** *| + 0x80|* *** *| + 0x01|* *** ***| + 0x02|* *** * | + 0x04|* * | + 0x08|***************** | + '-------------------' +*/ + +const int Battery_1_Height = 12; +const int Battery_1_Width = 19; +const uint8_t Battery_1 [] = { + 0xFF, 0x01, 0xFD, 0xFD, 0xFD, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x0F, 0x08, 0xF8, + 0x0F, 0x08, 0x0B, 0x0B, 0x0B, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0F, 0x01, 0x01 +}; + +/* + Battery_0 [19, 12] + + 1 + 1234567890123456789 + .-------------------. + 0x01|***************** | + 0x02|* * | + 0x04|* * | + 0x08|* ***| + 0x10|* *| + 0x20|* *| + 0x40|* *| + 0x80|* *| + 0x01|* ***| + 0x02|* * | + 0x04|* * | + 0x08|***************** | + '-------------------' +*/ + +const int Battery_0_Height = 12; +const int Battery_0_Width = 19; +const uint8_t Battery_0 [] = { + 0xFF, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x0F, 0x08, 0xF8, + 0x0F, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0F, 0x01, 0x01 +}; + +/* + Ethernet [19, 12] + + 1 + 1234567890123456789 + .-------------------. + 0x01| | + 0x02| ***** | + 0x04| * * | + 0x08| ***** | + 0x10| * | + 0x20| ***************** | + 0x40| * * | + 0x80| ***** ***** | + 0x01| * * * * | + 0x02| ***** ***** | + 0x04| | + 0x08| | + '-------------------' +*/ + +const int Ethernet_Icon_Height = 12; +const int Ethernet_Icon_Width = 19; +const uint8_t Ethernet_Icon [] = { + 0x00, 0x20, 0xA0, 0xA0, 0xE0, 0xA0, 0xA0, 0x2E, 0x2A, 0x3A, 0x2A, 0x2E, 0xA0, 0xA0, 0xE0, 0xA0, 0xA0, 0x20, 0x00, + 0x00, 0x00, 0x03, 0x02, 0x02, 0x02, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x02, 0x02, 0x02, 0x03, 0x00, 0x00 +}; + +/* + Logging_3 [9, 12] + + 123456789 + .---------. + 0x01|******* | + 0x02|* ** | + 0x04|* **| + 0x08|* *| + 0x10|* ***** *| + 0x20|* *| + 0x40|* ***** *| + 0x80|* *| + 0x01|* ***** *| + 0x02|* *| + 0x04|* *| + 0x08|*********| + '---------' +*/ + +const int Logging_3_Height = 12; +const int Logging_3_Width = 9; +const uint8_t Logging_3 [] = { + 0xFF, 0x01, 0x51, 0x51, 0x51, 0x51, 0x53, 0x06, 0xFC, + 0x0F, 0x08, 0x09, 0x09, 0x09, 0x09, 0x09, 0x08, 0x0F +}; + +/* + Logging_2 [9, 12] + + 123456789 + .---------. + 0x01|******* | + 0x02|* ** | + 0x04|* **| + 0x08|* *| + 0x10|* *| + 0x20|* *| + 0x40|* ***** *| + 0x80|* *| + 0x01|* ***** *| + 0x02|* *| + 0x04|* *| + 0x08|*********| + '---------' +*/ + +const int Logging_2_Height = 12; +const int Logging_2_Width = 9; +const uint8_t Logging_2 [] = { + 0xFF, 0x01, 0x41, 0x41, 0x41, 0x41, 0x43, 0x06, 0xFC, + 0x0F, 0x08, 0x09, 0x09, 0x09, 0x09, 0x09, 0x08, 0x0F +}; + +/* + Logging_1 [9, 12] + + 123456789 + .---------. + 0x01|******* | + 0x02|* ** | + 0x04|* **| + 0x08|* *| + 0x10|* *| + 0x20|* *| + 0x40|* *| + 0x80|* *| + 0x01|* ***** *| + 0x02|* *| + 0x04|* *| + 0x08|*********| + '---------' +*/ + +const int Logging_1_Height = 12; +const int Logging_1_Width = 9; +const uint8_t Logging_1 [] = { + 0xFF, 0x01, 0x01, 0x01, 0x01, 0x01, 0x03, 0x06, 0xFC, + 0x0F, 0x08, 0x09, 0x09, 0x09, 0x09, 0x09, 0x08, 0x0F +}; + +/* + Logging_0 [9, 12] + + 123456789 + .---------. + 0x01|******* | + 0x02|* ** | + 0x04|* **| + 0x08|* *| + 0x10|* *| + 0x20|* *| + 0x40|* *| + 0x80|* *| + 0x01|* *| + 0x02|* *| + 0x04|* *| + 0x08|*********| + '---------' +*/ + +const int Logging_0_Height = 12; +const int Logging_0_Width = 9; +const uint8_t Logging_0 [] = { + 0xFF, 0x01, 0x01, 0x01, 0x01, 0x01, 0x03, 0x06, 0xFC, + 0x0F, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0F +}; + +/* + Logging_PPP_3 [9, 12] + + 123456789 + .---------. + 0x01|******* | + 0x02|* ** | + 0x04|* **| + 0x08|* **** *| + 0x10|* * * *| + 0x20|* * * *| + 0x40|* **** *| + 0x80|* * *| + 0x01|* * *| + 0x02|* * *| + 0x04|* *| + 0x08|*********| + '---------' +*/ + +const uint8_t Logging_PPP_3 [] = { + 0xFF, 0x01, 0xF9, 0x49, 0x49, 0x49, 0x33, 0x06, 0xFC, + 0x0F, 0x08, 0x0B, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0F +}; + +/* + Logging_PPP_2 [9, 12] + + 123456789 + .---------. + 0x01|******* | + 0x02|* ** | + 0x04|* **| + 0x08|* * *| + 0x10|* * *| + 0x20|* * *| + 0x40|* **** *| + 0x80|* * *| + 0x01|* * *| + 0x02|* * *| + 0x04|* *| + 0x08|*********| + '---------' +*/ + +const uint8_t Logging_PPP_2 [] = { + 0xFF, 0x01, 0xF9, 0x41, 0x41, 0x41, 0x03, 0x06, 0xFC, + 0x0F, 0x08, 0x0B, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0F +}; + +/* + Logging_PPP_1 [9, 12] + + 123456789 + .---------. + 0x01|******* | + 0x02|* ** | + 0x04|* **| + 0x08|* *| + 0x10|* *| + 0x20|* *| + 0x40|* * *| + 0x80|* * *| + 0x01|* * *| + 0x02|* * *| + 0x04|* *| + 0x08|*********| + '---------' +*/ + +const uint8_t Logging_PPP_1 [] = { + 0xFF, 0x01, 0xC1, 0x01, 0x01, 0x01, 0x03, 0x06, 0xFC, + 0x0F, 0x08, 0x0B, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0F +}; + +/* + Logging_Custom_3 [9, 12] + + 123456789 + .---------. + 0x01|******* | + 0x02|* ** | + 0x04|* **| + 0x08|* *** *| + 0x10|* * * *| + 0x20|* * *| + 0x40|* * *| + 0x80|* * *| + 0x01|* * * *| + 0x02|* *** *| + 0x04|* *| + 0x08|*********| + '---------' +*/ + +const uint8_t Logging_Custom_3 [] = { + 0xFF, 0x01, 0xF1, 0x09, 0x09, 0x09, 0x13, 0x06, 0xFC, + 0x0F, 0x08, 0x09, 0x0A, 0x0A, 0x0A, 0x09, 0x08, 0x0F +}; + +/* + Logging_Custom_2 [9, 12] + + 123456789 + .---------. + 0x01|******* | + 0x02|* ** | + 0x04|* **| + 0x08|* *| + 0x10|* * *| + 0x20|* * *| + 0x40|* * *| + 0x80|* * *| + 0x01|* * * *| + 0x02|* *** *| + 0x04|* *| + 0x08|*********| + '---------' +*/ + +const uint8_t Logging_Custom_2 [] = { + 0xFF, 0x01, 0xF1, 0x01, 0x01, 0x01, 0x03, 0x06, 0xFC, + 0x0F, 0x08, 0x09, 0x0A, 0x0A, 0x0A, 0x09, 0x08, 0x0F +}; + +/* + Logging_Custom_1 [9, 12] + + 123456789 + .---------. + 0x01|******* | + 0x02|* ** | + 0x04|* **| + 0x08|* *| + 0x10|* *| + 0x20|* *| + 0x40|* *| + 0x80|* *| + 0x01|* * * *| + 0x02|* *** *| + 0x04|* *| + 0x08|*********| + '---------' +*/ + +const uint8_t Logging_Custom_1 [] = { + 0xFF, 0x01, 0x01, 0x01, 0x01, 0x01, 0x03, 0x06, 0xFC, + 0x0F, 0x08, 0x09, 0x0A, 0x0A, 0x0A, 0x09, 0x08, 0x0F +}; + +/* + Logging_NTP_3 [9, 12] + + 123456789 + .---------. + 0x01|******* | + 0x02|* ** | + 0x04|* **| + 0x08|* *| + 0x10|* * * *| + 0x20|* ** * *| + 0x40|* * * * *| + 0x80|* * * * *| + 0x01|* * ** *| + 0x02|* * * *| + 0x04|* *| + 0x08|*********| + '---------' +*/ + +const uint8_t Logging_NTP_3 [] = { + 0xFF, 0x01, 0xF1, 0x21, 0xC1, 0x01, 0xF3, 0x06, 0xFC, + 0x0F, 0x08, 0x0B, 0x08, 0x08, 0x09, 0x0B, 0x08, 0x0F +}; + +/* + Logging_NTP_2 [9, 12] + + 123456789 + .---------. + 0x01|******* | + 0x02|* ** | + 0x04|* **| + 0x08|* *| + 0x10|* * *| + 0x20|* ** *| + 0x40|* * * *| + 0x80|* * * *| + 0x01|* * * *| + 0x02|* * *| + 0x04|* *| + 0x08|*********| + '---------' +*/ + +const uint8_t Logging_NTP_2 [] = { + 0xFF, 0x01, 0xF1, 0x21, 0xC1, 0x01, 0x03, 0x06, 0xFC, + 0x0F, 0x08, 0x0B, 0x08, 0x08, 0x09, 0x08, 0x08, 0x0F +}; + +/* + Logging_NTP_1 [9, 12] + + 123456789 + .---------. + 0x01|******* | + 0x02|* ** | + 0x04|* **| + 0x08|* *| + 0x10|* * *| + 0x20|* * *| + 0x40|* * *| + 0x80|* * *| + 0x01|* * *| + 0x02|* * *| + 0x04|* *| + 0x08|*********| + '---------' +*/ + +const uint8_t Logging_NTP_1 [] = { + 0xFF, 0x01, 0xF1, 0x01, 0x01, 0x01, 0x03, 0x06, 0xFC, + 0x0F, 0x08, 0x0B, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0F +}; + +/* + DynamicModel_1_Portable [15, 12] + + 1 + 123456789012345 + .---------------. + 0x01| ** | + 0x02| ** | + 0x04| ****** | + 0x08| * * | + 0x10| * * **** * * | + 0x20| * * **** * * | + 0x40| * * * * | + 0x80| * * * * | + 0x01| * * * * | + 0x02| * * * * | + 0x04| * * | + 0x08| ****** | + '---------------' +*/ + +const int DynamicModel_Height = 12; +const int DynamicModel_Width = 15; +const uint8_t DynamicModel_1_Portable [] = { + 0x00, 0xF0, 0x00, 0xF8, 0x04, 0x34, 0x34, 0x37, 0x37, 0x04, 0xF8, 0x00, 0xF0, 0x00, 0x00, + 0x00, 0x03, 0x00, 0x07, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x07, 0x00, 0x03, 0x00, 0x00 +}; + +/* + DynamicModel_2_Stationary [15, 12] + + 1 + 123456789012345 + .---------------. + 0x01| | + 0x02| ******* | + 0x04| ***** | + 0x08| *** | + 0x10| * | + 0x20| *** | + 0x40| ***** | + 0x80| ** * ** | + 0x01| ** * ** | + 0x02| ** * ** | + 0x04| ** * ** | + 0x08| | + '---------------' +*/ + +const uint8_t DynamicModel_2_Stationary [] = { + 0x00, 0x00, 0x00, 0x00, 0x82, 0xC6, 0x6E, 0xFE, 0x6E, 0xC6, 0x82, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x04, 0x06, 0x03, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x01, 0x03, 0x06, 0x04, 0x00 +}; + +/* + DynamicModel_3_Pedestrian [15, 12] + + 1 + 123456789012345 + .---------------. + 0x01| *** | + 0x02| * * | + 0x04| * * | + 0x08| * | + 0x10| ***** | + 0x20| ** * ** | + 0x40| * * | + 0x80| *** | + 0x01| ** ** | + 0x02| ** * | + 0x04| ** ** | + 0x08| | + '---------------' +*/ + +const uint8_t DynamicModel_3_Pedestrian [] = { + 0x00, 0x00, 0x00, 0x00, 0x20, 0x32, 0x95, 0xF9, 0x95, 0x32, 0x60, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x04, 0x06, 0x03, 0x01, 0x00, 0x01, 0x07, 0x04, 0x00, 0x00, 0x00, 0x00 +}; + +/* + DynamicModel_4_Automotive [15, 12] + + 1 + 123456789012345 + .---------------. + 0x01| | + 0x02| | + 0x04| ********* | + 0x08|* * | + 0x10|* ****| + 0x20|* *| + 0x40|* ** ** *| + 0x80| * ******* * | + 0x01| * * * * | + 0x02| ** ** | + 0x04| | + 0x08| | + '---------------' +*/ + +const uint8_t DynamicModel_4_Automotive [] = { + 0x78, 0x84, 0x44, 0x44, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x88, 0x50, 0x50, 0x90, 0x70, + 0x00, 0x01, 0x02, 0x02, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x02, 0x01, 0x00 +}; + +/* + DynamicModel_5_Sea [15, 12] + + 1 + 123456789012345 + .---------------. + 0x01| | + 0x02| * | + 0x04| *** | + 0x08| * * | + 0x10| * * | + 0x20| ************* | + 0x40| ** ** | + 0x80| * ** * | + 0x01| * * | + 0x02| ** ** | + 0x04| ********* | + 0x08| | + '---------------' +*/ + +const uint8_t DynamicModel_5_Sea [] = { + 0x00, 0x60, 0xE0, 0x3C, 0x26, 0x3C, 0x20, 0x20, 0x20, 0xA0, 0xA0, 0x20, 0xE0, 0x60, 0x00, + 0x00, 0x00, 0x03, 0x06, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x06, 0x03, 0x00, 0x00 +}; + +/* + DynamicModel_6_Airborne1g [15, 12] + + 1 + 123456789012345 + .---------------. + 0x01| | + 0x02| * | + 0x04| ** | + 0x08| *********** | + 0x10| * * ** | + 0x20| * * * ** | + 0x40| * * * | + 0x80| * * *** ** | + 0x01| ****** ***** | + 0x02| * * | + 0x04| * * | + 0x08| * | + '---------------' +*/ + +const uint8_t DynamicModel_6_Airborne1g [] = { + 0x00, 0xFE, 0x0C, 0xF8, 0x08, 0x08, 0x88, 0x88, 0x88, 0x28, 0x08, 0x18, 0xB0, 0xE0, 0x00, + 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x07, 0x08, 0x07, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00 +}; + +/* + DynamicModel_7_Airborne2g [15, 12] + + 1 + 123456789012345 + .---------------. + 0x01| | + 0x02| * | + 0x04| ** | + 0x08| *********** | + 0x10| * * ** | + 0x20| * * * * ** | + 0x40| * * * | + 0x80| * * *** ** | + 0x01| ****** ***** | + 0x02| * * | + 0x04| * * | + 0x08| * | + '---------------' +*/ + +const uint8_t DynamicModel_7_Airborne2g [] = { + 0x00, 0xFE, 0x0C, 0xF8, 0x08, 0x08, 0x88, 0xA8, 0x88, 0x28, 0x08, 0x18, 0xB0, 0xE0, 0x00, + 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x07, 0x08, 0x07, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00 +}; + +/* + DynamicModel_8_Airborne4g [15, 12] + + 1 + 123456789012345 + .---------------. + 0x01| | + 0x02| * | + 0x04| ** | + 0x08| *********** | + 0x10| * * ** | + 0x20| * * * * * ** | + 0x40| * * * | + 0x80| * * *** ** | + 0x01| ****** ***** | + 0x02| * * | + 0x04| * * | + 0x08| * | + '---------------' +*/ + +const uint8_t DynamicModel_8_Airborne4g [] = { + 0x00, 0xFE, 0x0C, 0xF8, 0x08, 0x28, 0x88, 0xA8, 0x88, 0x28, 0x08, 0x18, 0xB0, 0xE0, 0x00, + 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x07, 0x08, 0x07, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00 +}; + +/* + DynamicModel_9_Wrist [15, 12] + + 1 + 123456789012345 + .---------------. + 0x01| *** | + 0x02| *** | + 0x04| *** | + 0x08| ***** | + 0x10| * * | + 0x20| * * | + 0x40| * *** * | + 0x80| * * | + 0x01| * * | + 0x02| ***** | + 0x04| *** | + 0x08| *** | + '---------------' +*/ + +const uint8_t DynamicModel_9_Wrist [] = { + 0x00, 0x00, 0x00, 0xE0, 0x10, 0x08, 0x4F, 0x4F, 0x4F, 0x08, 0x10, 0xE0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x1E, 0x1E, 0x1E, 0x02, 0x01, 0x00, 0x00, 0x00, 0x00 +}; + +/* + DynamicModel_10_Bike [15, 12] + + 1 + 123456789012345 + .---------------. + 0x01| | + 0x02| | + 0x04| ** | + 0x08| *** | + 0x10| *** * | + 0x20| * * | + 0x40| ** *** ** | + 0x80| * ******* * | + 0x01| * * * * | + 0x02| ** ** | + 0x04| | + 0x08| | + '---------------' +*/ + +const uint8_t DynamicModel_10_Bike [] = { + 0x00, 0x80, 0x40, 0x50, 0x90, 0xB0, 0xC0, 0xC0, 0xC0, 0xA0, 0x98, 0x4C, 0x4C, 0x80, 0x00, + 0x00, 0x01, 0x02, 0x02, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x02, 0x01, 0x00 +}; + +const uint8_t DynamicModel_11_Mower [] = { + 0x01, 0x03, 0x86, 0x8C, 0x98, 0xB0, 0xE0, 0xC0, 0xF4, 0xDC, 0xDC, 0xF4, 0xC0, 0x80, 0x00, + 0x00, 0x00, 0x03, 0x0E, 0x0A, 0x0E, 0x02, 0x02, 0x02, 0x02, 0x0E, 0x0A, 0x0E, 0x03, 0x00, +}; + +const uint8_t DynamicModel_12_EScooter [] = { + 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x02, 0x01, 0xFF, 0xFF, 0x01, 0x02, 0x00, 0x00, + 0x00, 0x03, 0x0F, 0x0B, 0x0F, 0x03, 0x03, 0x03, 0x03, 0x0F, 0x0B, 0x0F, 0x03, 0x00, 0x00, +}; + +/* + DownloadArrow [8, 9] + + 12345678 + .--------. + 0x01| ** | + 0x02| ** | + 0x04| ** | + 0x08| ** | + 0x10| ** | + 0x20|** ** **| + 0x40| ****** | + 0x80| **** | + 0x01| ** | + '--------' +*/ + +const int DownloadArrow_Height = 9; +const int DownloadArrow_Width = 8; +const uint8_t DownloadArrow [] = { + 0x20, 0x60, 0xC0, 0xFF, 0xFF, 0xC0, 0x60, 0x20, + 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00 +}; + +/* + UploadArrow [8, 9] + + 12345678 + .--------. + 0x01| ** | + 0x02| **** | + 0x04| ****** | + 0x08|** ** **| + 0x10| ** | + 0x20| ** | + 0x40| ** | + 0x80| ** | + 0x01| ** | + '--------' +*/ + +const int UploadArrow_Height = 9; +const int UploadArrow_Width = 8; +const uint8_t UploadArrow [] = { + 0x08, 0x0C, 0x06, 0xFF, 0xFF, 0x06, 0x0C, 0x08, + 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00 +}; + +/* + logoSparkFun [64, 48] + + 1 2 3 4 5 6 + 1234567890123456789012345678901234567890123456789012345678901234 + .----------------------------------------------------------------. + 0x01| ********** | + 0x02| ************* | + 0x04| ************** | + 0x08| *********** | + 0x10| ********** | + 0x20| *********** | + 0x40| *********** | + 0x80| *********** ** | + 0x01| ************ *** | + 0x02| ************* **** | + 0x04| ********************* | + 0x08| ********************* | + 0x10| ******************** | + 0x20| ********************* | + 0x40| ******************* | + 0x80| ******* ******************* | + 0x01| ******* ****************** | + 0x02| ******* ***************** | + 0x04| ******** ****************** | + 0x08| ******** ****************** | + 0x10| ********* ******************* | + 0x20| ********************************* | + 0x40| ********************************* | + 0x80| ********************************* | + 0x01| ********************************* | + 0x02| ******************************** | + 0x04| ******************************** | + 0x08| ******************************* | + 0x10| ******************************* | + 0x20| ****************************** | + 0x40| ***************************** | + 0x80| **************************** | + 0x01| *************************** | + 0x02| ************************** | + 0x04| ************************ | + 0x08| ********************* | + 0x10| ************* | + 0x20| *********** | + 0x40| ********** | + 0x80| ********* | + 0x01| ******** | + 0x02| ******* | + 0x04| ****** | + 0x08| ***** | + 0x10| **** | + 0x20| *** | + 0x40| ** | + 0x80| * | + '----------------------------------------------------------------' +*/ + +const int logoSparkFun_Height = 48; +const int logoSparkFun_Width = 64; +const uint8_t logoSparkFun [] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xF8, 0xFC, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x0F, 0x07, 0x07, 0x06, 0x06, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x81, 0x07, 0x0F, 0x3F, 0x3F, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFE, 0xFC, 0xFC, 0xFC, 0xFE, 0xFF, 0xFF, 0xFF, 0xFC, 0xF8, 0xE0, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, + 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF1, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xF0, 0xFD, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x3F, 0x1F, 0x07, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x3F, 0x1F, 0x1F, 0x0F, 0x0F, 0x0F, 0x0F, + 0x0F, 0x0F, 0x0F, 0x0F, 0x07, 0x07, 0x07, 0x03, 0x03, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0x7F, 0x3F, 0x1F, 0x0F, 0x07, 0x03, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + +/* + ESPNOW_Symbol_3 [8, 13] + + 12345678 + .--------. + 0x01| * | + 0x02| * | + 0x04| * * | + 0x08| * *| + 0x10| * * *| + 0x20|* * * *| + 0x40|** * * *| + 0x80|* * * *| + 0x01| * * *| + 0x02| * *| + 0x04| * * | + 0x08| * | + 0x10| * | + '--------' +*/ + +const int ESPNOW_Symbol_Height = 13; +const int ESPNOW_Symbol_Width = 8; +const uint8_t ESPNOW_Symbol_3 [] = { + 0xE0, 0x40, 0x10, 0xE4, 0x09, 0xF2, 0x04, 0xF8, + 0x00, 0x00, 0x01, 0x04, 0x12, 0x09, 0x04, 0x03 +}; + +/* + ESPNOW_Symbol_2 [8, 13] + + 12345678 + .--------. + 0x01| | + 0x02| | + 0x04| * | + 0x08| * | + 0x10| * * | + 0x20|* * * | + 0x40|** * * | + 0x80|* * * | + 0x01| * * | + 0x02| * | + 0x04| * | + 0x08| | + 0x10| | + '--------' +*/ + +const uint8_t ESPNOW_Symbol_2 [] = { + 0xE0, 0x40, 0x10, 0xE4, 0x08, 0xF0, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x04, 0x02, 0x01, 0x00, 0x00 +}; + +/* + ESPNOW_Symbol_1 [8, 13] + + 12345678 + .--------. + 0x01| | + 0x02| | + 0x04| | + 0x08| | + 0x10| * | + 0x20|* * | + 0x40|** * | + 0x80|* * | + 0x01| * | + 0x02| | + 0x04| | + 0x08| | + 0x10| | + '--------' +*/ + +const uint8_t ESPNOW_Symbol_1 [] = { + 0xE0, 0x40, 0x10, 0xE0, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + +/* + ESPNOW_Symbol_0 [8, 13] + + 12345678 + .--------. + 0x01| | + 0x02| | + 0x04| | + 0x08| | + 0x10| | + 0x20|* | + 0x40|** | + 0x80|* | + 0x01| | + 0x02| | + 0x04| | + 0x08| | + 0x10| | + '--------' +*/ + +const uint8_t ESPNOW_Symbol_0 [] = { + 0xE0, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + +/* + milliseconds [8, 12] + + + 12345678 + .--------. + 0x01|** * | + 0x02|* * * | + 0x04|* * * | + 0x08|* * * | + 0x10|* * * | + 0x20| | + 0x40| | + 0x80| **** | + 0x01| * | + 0x02| *** | + 0x04| * | + 0x08| **** | + '--------' +*/ + +const int Millis_Icon_Height = 12; +const int Millis_Icon_Width = 8; +const uint8_t Millis_Icon [] = { + 0x1F, 0x01, 0x1E, 0x81, 0x9E, 0x80, 0x80, 0x00, + 0x00, 0x00, 0x09, 0x0A, 0x0A, 0x0A, 0x04, 0x00 +}; + +/* + microseconds [8, 12] + + + 12345678 + .--------. + 0x01|* * | + 0x02|* * | + 0x04|** ** | + 0x08|* ** * | + 0x10|* | + 0x20|* | + 0x40| | + 0x80| **** | + 0x01| * | + 0x02| *** | + 0x04| * | + 0x08| **** | + '--------' +*/ + +const uint8_t Micros_Icon [] = { + 0x3F, 0x04, 0x08, 0x88, 0x84, 0x8F, 0x80, 0x00, + 0x00, 0x00, 0x09, 0x0A, 0x0A, 0x0A, 0x04, 0x00 +}; + +/* + nanoseconds [8, 12] + + + 12345678 + .--------. + 0x01|**** | + 0x02|* * | + 0x04|* * | + 0x08|* * | + 0x10|* * | + 0x20| | + 0x40| | + 0x80| **** | + 0x01| * | + 0x02| *** | + 0x04| * | + 0x08| **** | + '--------' +*/ + +const uint8_t Nanos_Icon [] = { + 0x1F, 0x01, 0x01, 0x81, 0x9E, 0x80, 0x80, 0x00, + 0x00, 0x00, 0x09, 0x0A, 0x0A, 0x0A, 0x04, 0x00 +}; diff --git a/Firmware/Test Sketches/Hookup_Display/settings.h b/Firmware/Test Sketches/Hookup_Display/settings.h new file mode 100644 index 000000000..69e499b8a --- /dev/null +++ b/Firmware/Test Sketches/Hookup_Display/settings.h @@ -0,0 +1,87 @@ +typedef enum +{ + STATE_ROVER_NOT_STARTED = 0, + STATE_ROVER_NO_FIX, + STATE_ROVER_FIX, + STATE_ROVER_RTK_FLOAT, + STATE_ROVER_RTK_FIX, + STATE_BASE_NOT_STARTED, + STATE_BASE_TEMP_SETTLE, //User has indicated base, but current pos accuracy is too low + STATE_BASE_TEMP_SURVEY_STARTED, + STATE_BASE_TEMP_TRANSMITTING, + STATE_BASE_FIXED_NOT_STARTED, + STATE_BASE_FIXED_TRANSMITTING, + STATE_BUBBLE_LEVEL, + STATE_MARK_EVENT, + STATE_DISPLAY_SETUP, + STATE_WIFI_CONFIG_NOT_STARTED, + STATE_WIFI_CONFIG, + STATE_TEST, + STATE_TESTING, + STATE_PROFILE, + STATE_KEYS_STARTED, + STATE_KEYS_NEEDED, + STATE_KEYS_WIFI_STARTED, + STATE_KEYS_WIFI_CONNECTED, + STATE_KEYS_WIFI_TIMEOUT, + STATE_KEYS_EXPIRED, + STATE_KEYS_DAYS_REMAINING, + STATE_KEYS_LBAND_CONFIGURE, + STATE_KEYS_LBAND_ENCRYPTED, + STATE_KEYS_PROVISION_WIFI_STARTED, + STATE_KEYS_PROVISION_WIFI_CONNECTED, + STATE_KEYS_PROVISION_WIFI_TIMEOUT, + STATE_ESPNOW_PAIRING_NOT_STARTED, + STATE_ESPNOW_PAIRING, + STATE_NTPSERVER_NOT_STARTED, + STATE_NTPSERVER_NO_SYNC, + STATE_NTPSERVER_SYNC, + STATE_CONFIG_VIA_ETH_NOT_STARTED, + STATE_CONFIG_VIA_ETH_STARTED, + STATE_CONFIG_VIA_ETH, + STATE_CONFIG_VIA_ETH_RESTART_BASE, + STATE_SHUTDOWN, + STATE_NOT_SET, //Must be last on list +} SystemState; + +typedef enum +{ + ETH_NOT_STARTED, + ETH_STARTED_CHECK_CABLE, + ETH_STARTED_START_DHCP, + ETH_CONNECTED, + ETH_CAN_NOT_BEGIN, +} ethernetStatus_e; + +typedef enum LoggingType { + LOGGING_UNKNOWN = 0, + LOGGING_STANDARD, + LOGGING_PPP, + LOGGING_CUSTOM +} LoggingType; +LoggingType loggingType = LOGGING_STANDARD; + +typedef enum +{ + NTRIP_SERVER_OFF = 0, //Using Bluetooth or NTRIP client + NTRIP_SERVER_ON, //WIFI_START state + NTRIP_SERVER_WIFI_ETHERNET_STARTED, //Connecting to WiFi access point + NTRIP_SERVER_WIFI_ETHERNET_CONNECTED, //WiFi connected to an access point + NTRIP_SERVER_WAIT_GNSS_DATA, //Waiting for correction data from GNSS + NTRIP_SERVER_CONNECTING, //Attempting a connection to the NTRIP caster + NTRIP_SERVER_AUTHORIZATION, //Validate the credentials + NTRIP_SERVER_CASTING, //Sending correction data to the NTRIP caster +} NTRIPServerState; +NTRIPServerState ntripServerState = NTRIP_SERVER_CASTING; + +typedef enum +{ + RTK_SURVEYOR = 0, + RTK_EXPRESS, + RTK_FACET, + RTK_EXPRESS_PLUS, + RTK_FACET_LBAND, + REFERENCE_STATION, + RTK_UNKNOWN, +} ProductVariant; +ProductVariant productVariant = REFERENCE_STATION; diff --git a/Firmware/Test Sketches/Idle_Loop/Idle_Loop.ino b/Firmware/Test Sketches/Idle_Loop/Idle_Loop.ino new file mode 100644 index 000000000..7eaf7c767 --- /dev/null +++ b/Firmware/Test Sketches/Idle_Loop/Idle_Loop.ino @@ -0,0 +1,40 @@ +/* + Idle loop + By: Lee Leahy + SparkFun Electronics + Date: July 9th, 2022 + License: MIT. See license file for more information but you can + basically do whatever you want with this code. + + This example determines the count for the idle loop +*/ + +#define testTimeSecs (3 * 60) + +uint32_t startTime; +uint32_t idleCount; + +void setup() +{ + Serial.begin(115200); + idleCount = 0; + startTime = millis(); +} + +void loop() +{ + //Query module only every second. + //The module only responds when a new position is available. + while ((millis() - startTime) < (testTimeSecs * 1000)) + { + idleCount++; + yield(); + } + + //Display the idle count + Serial.printf("Count / Second = %d\r\n", idleCount / testTimeSecs); + + //Done + while(1) + delay(1000); +} diff --git a/Firmware/Test Sketches/LEDs/LEDs.ino b/Firmware/Test Sketches/LEDs/LEDs.ino deleted file mode 100644 index 187d0d915..000000000 --- a/Firmware/Test Sketches/LEDs/LEDs.ino +++ /dev/null @@ -1,52 +0,0 @@ - -const int batteryLevelLED_Red = 32; -const int batteryLevelLED_Green = 33; - -const int freq = 5000; -const int ledRedChannel = 0; -const int ledGreenChannel = 1; -const int resolution = 8; - -const int positionAccuracyLED_1cm = 2; -const int baseStatusLED = 4; -const int baseSwitch = 5; -const int bluetoothStatusLED = 12; -const int positionAccuracyLED_100cm = 13; -const int positionAccuracyLED_10cm = 15; -const byte PIN_MICROSD_CHIP_SELECT = 25; -const int zed_tx_ready = 26; -const int zed_reset = 27; -const int batteryLevel_alert = 36; - -void setup() { - ledcSetup(ledRedChannel, freq, resolution); - ledcSetup(ledGreenChannel, freq, resolution); - - ledcAttachPin(batteryLevelLED_Red, ledRedChannel); - ledcAttachPin(batteryLevelLED_Green, ledGreenChannel); - - //ledcWrite(ledRedChannel, 128); - //ledcWrite(ledGreenChannel, 128); - - pinMode(positionAccuracyLED_1cm, OUTPUT); - pinMode(positionAccuracyLED_10cm, OUTPUT); - pinMode(positionAccuracyLED_100cm, OUTPUT); - pinMode(baseStatusLED, OUTPUT); - pinMode(bluetoothStatusLED, OUTPUT); - - digitalWrite(positionAccuracyLED_1cm, HIGH); - digitalWrite(positionAccuracyLED_10cm, HIGH); - digitalWrite(positionAccuracyLED_100cm, HIGH); - digitalWrite(baseStatusLED, HIGH); - digitalWrite(bluetoothStatusLED, HIGH); - -} - -void loop() { - ledcWrite(ledRedChannel, 128); - ledcWrite(ledGreenChannel, 128); - delay(2500); // wait for a second - ledcWrite(ledRedChannel, 0); - ledcWrite(ledGreenChannel, 0); - delay(2500); // wait for a second -} diff --git a/Firmware/Test Sketches/PSRAM/PSRAM.ino b/Firmware/Test Sketches/PSRAM/PSRAM.ino new file mode 100644 index 000000000..592cba90a --- /dev/null +++ b/Firmware/Test Sketches/PSRAM/PSRAM.ino @@ -0,0 +1,122 @@ +/* + Lee Leahy + 12 June 2023 + + Determine the performance of the PSRAM. + + Based upon: https://thingpulse.com/esp32-how-to-use-psram/ + and: https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/external-ram.html +*/ + +#include + +#define MAX_LOOP_COUNT (1 * 1000 * 1000) +#define PSRAM_BYTES (2 * 1024 * 1024) + +volatile uint8_t * buffer; +volatile uint8_t internalRam; +int psramBytes; +uint32_t psramPageSize; + +void setup() +{ + Serial.begin(115200); + delay(1000); + Serial.println(); + Serial.println(); + Serial.print("PSRAM Size: "); + Serial.print(ESP.getPsramSize()); + Serial.println(); + Serial.println("PSRAM Access Times"); +} + +void loop() +{ + int32_t index; + uint8_t junk; + uint32_t loopOverhead; + uint32_t microsEnd; + uint32_t microsStart; + uint32_t offset; + + // Read data from the data cache + buffer = (volatile uint8_t *)ps_malloc(PSRAM_BYTES); + Serial.print("PSRAM buffer: "); + Serial.printf("%p\r\n", buffer); + Serial.flush(); + + // Compute the loop overhead + microsStart = micros(); + for (index = 0; index < MAX_LOOP_COUNT; index++) + { + } + microsEnd = micros(); + displayTime(microsStart, microsEnd, 0, "Loop overhead"); + loopOverhead = microsEnd - microsStart; + + // Prime the data + junk = internalRam; + + // Read data from the internal RAM + microsStart = micros(); + for (index = 0; index < MAX_LOOP_COUNT; index++) + junk = internalRam; + microsEnd = micros(); + displayTime(microsStart, microsEnd, loopOverhead, "Internal RAM access"); + + if (buffer) + { + // Measure cached PSRAM access + microsStart = micros(); + for (index = 0; index < MAX_LOOP_COUNT; index++) + junk = buffer[0]; + microsEnd = micros(); + displayTime(microsStart, microsEnd, loopOverhead, "Cached PSRAM access"); + + // Separate the two sets of measurements + Serial.println(); + + // Compute the loop overhead + microsStart = micros(); + for (index = 0; index < MAX_LOOP_COUNT; index++) + { + junk = buffer[offset]; + offset = (offset + psramPageSize) & (PSRAM_BYTES - 1); + } + microsEnd = micros(); + displayTime(microsStart, microsEnd, 0, "Loop overhead"); + loopOverhead = microsEnd - microsStart; + + // Measure sequential accesses to PSRAM + for (psramPageSize = 1; psramPageSize <= 4096; psramPageSize <<= 1) + { + microsStart = micros(); + for (index = 0; index < MAX_LOOP_COUNT; index++) + { + junk = buffer[offset]; + offset = (offset + psramPageSize) & (PSRAM_BYTES - 1); + } + microsEnd = micros(); + sprintf((char *)buffer, "Uncached PSRAM access, %4d byte page size", psramPageSize); + displayTime(microsStart, microsEnd, loopOverhead, (const char *)buffer); + } + } + + // Done + while (1); +} + +void displayTime(uint32_t microsStart, uint32_t microsEnd, uint32_t loopOverhead, const char * string) +{ + uint32_t delta; + float nanoSeconds; + char text[128]; + + // Display the cache read time + delta = microsEnd - microsStart - loopOverhead; + nanoSeconds = (double)delta / 1000.; + sprintf(text, "%s: %8d - %8d - %5d = %7d: %7.3f nSec", + string, microsEnd, microsStart, loopOverhead, delta, nanoSeconds); + Serial.println(text); + Serial.flush(); +} diff --git a/Firmware/Test Sketches/Radio_Distance_Test/Radio_Distance_Test.ino b/Firmware/Test Sketches/Radio_Distance_Test/Radio_Distance_Test.ino deleted file mode 100644 index b4b3c28f5..000000000 --- a/Firmware/Test Sketches/Radio_Distance_Test/Radio_Distance_Test.ino +++ /dev/null @@ -1,94 +0,0 @@ -/* - August 31, 2020 - SparkFun Electronics - Nathan Seidle - -*/ - -int ledPin = 13; //Status LED connected to digital pin 13 - -const char *testString = ":abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ#"; -int bytesPerSecond = 100; //Start at 100 and build up to 4k - - -void setup() -{ - pinMode(ledPin, OUTPUT); - - //Serial.begin(115200); - Serial.begin(57600); - Serial1.begin(57600); //Default speed for most SiK based radios - - delay(500); //Wait for radio to power up - - Serial.println(); - Serial.println("Radio Distance Test"); - Serial1.println(); - Serial1.println("Radio Distance Test"); -} - -void loop() -{ - //Run 10 transmissions at the current byte count amount - for (int x = 0 ; x < 2 ; x++) - { - int currentByteCounter = 0; - int lineNumber = 0; - unsigned long startTime = millis(); - - //Put the output together - char myChar[5000]; -// char myChar[1000]; - myChar[0] = '\0'; //Clear buffer - - while (currentByteCounter < bytesPerSecond) - { - char temp[2]; - sprintf(temp, "%d", lineNumber++); - if (lineNumber == 10) lineNumber = 0; - - strcat(myChar, temp); //Add the line number - strcat(myChar, testString); - - sprintf(temp, "\n"); - strcat(myChar, temp); - - currentByteCounter += strlen(testString); - } - - //Transmit the buffer - digitalWrite(ledPin, HIGH); - - Serial1.print(myChar); - Serial1.print("Characters pushed: "); - Serial1.print(currentByteCounter); - Serial1.println(); - Serial1.println(); - - Serial.print(myChar); - Serial.print("Characters pushed: "); - Serial.print(currentByteCounter); - Serial.println(); - Serial.println(); - - digitalWrite(ledPin, LOW); - - //Wait for second to expire - while (millis() - startTime < 1000) delay(1); - } - - //Increase byte count to next level - if (bytesPerSecond == 100) - bytesPerSecond = 300; - else if (bytesPerSecond == 300) - bytesPerSecond = 500; - else if (bytesPerSecond == 500) - bytesPerSecond = 1000; - else if (bytesPerSecond == 1000) -// bytesPerSecond = 100; - bytesPerSecond = 2000; - else if (bytesPerSecond == 2000) - bytesPerSecond = 4000; - else if (bytesPerSecond == 4000) - bytesPerSecond = 100; -} diff --git a/Firmware/Test Sketches/SD_FileListing/Begin.ino b/Firmware/Test Sketches/SD_FileListing/Begin.ino new file mode 100644 index 000000000..402ca57c7 --- /dev/null +++ b/Firmware/Test Sketches/SD_FileListing/Begin.ino @@ -0,0 +1,72 @@ +void beginSD() +{ + pinMode(pin_microSD_CS, OUTPUT); + digitalWrite(pin_microSD_CS, HIGH); //Be sure SD is deselected + + if (settings.enableSD == true) + { + //Max power up time is 250ms: https://www.kingston.com/datasheets/SDCIT-specsheet-64gb_en.pdf + //Max current is 200mA average across 1s, peak 300mA + delay(10); + + if (settings.spiFrequency > 16) + { + Serial.println(("Error: SPI Frequency out of range. Default to 16MHz")); + settings.spiFrequency = 16; + } + + if (sd.begin(SdSpiConfig(pin_microSD_CS, SHARED_SPI, SD_SCK_MHZ(settings.spiFrequency))) == false) + { + int tries = 0; + int maxTries = 1; + for ( ; tries < maxTries ; tries++) + { + log_d("SD init failed. Trying again %d out of %d", tries + 1, maxTries); + + delay(250); //Give SD more time to power up, then try again + if (sd.begin(SdSpiConfig(pin_microSD_CS, SHARED_SPI, SD_SCK_MHZ(settings.spiFrequency))) == true) break; + } + + if (tries == maxTries) + { + Serial.println(F("SD init failed. Is card present? Formatted?")); + digitalWrite(pin_microSD_CS, HIGH); //Be sure SD is deselected + online.microSD = false; + return; + } + } + + //Change to root directory. All new file creation will be in root. + if (sd.chdir() == false) + { + Serial.println(F("SD change directory failed")); + online.microSD = false; + return; + } + + //Setup FAT file access semaphore + if (xFATSemaphore == NULL) + { + xFATSemaphore = xSemaphoreCreateMutex(); + if (xFATSemaphore != NULL) + xSemaphoreGive(xFATSemaphore); //Make the file system available for use + } + + if (createTestFile() == false) + { + Serial.println(F("Failed to create test file. Format SD card with 'SD Card Formatter'.")); + //displaySDFail(5000); + online.microSD = false; + return; + } + + online.microSD = true; + + Serial.println(F("microSD online")); + //scanForFirmware(); //See if SD card contains new firmware that should be loaded at startup + } + else + { + online.microSD = false; + } +} diff --git a/Firmware/Test Sketches/SD_FileListing/SD_FileListing.ino b/Firmware/Test Sketches/SD_FileListing/SD_FileListing.ino new file mode 100644 index 000000000..2dfb483c2 --- /dev/null +++ b/Firmware/Test Sketches/SD_FileListing/SD_FileListing.ino @@ -0,0 +1,130 @@ +/* + Demonstrates reading an SD card and showing files on card. + + Uses semaphores to prevent hardware access collisions. +*/ + +#include "settings.h" + +#define ASCII_LF 0x0a +#define ASCII_CR 0x0d + +int pin_microSD_CS = 25; + +//microSD Interface +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +#include +#include "SdFat.h" //http://librarymanager/All#sdfat_exfat by Bill Greiman. Currently uses v2.1.1 + +SdFat sd; + +char platformFilePrefix[40] = "SFE_Surveyor"; //Sets the prefix for logs and settings files + +SdFile ubxFile; //File that all gnss ubx messages setences are written to +unsigned long lastUBXLogSyncTime = 0; //Used to record to SD every half second +int startLogTime_minutes = 0; //Mark when we start any logging so we can stop logging after maxLogTime_minutes +int startCurrentLogTime_minutes = 0; //Mark when we start this specific log file so we can close it after x minutes and start a new one + +SdFile newFirmwareFile; //File that is available if user uploads new firmware via web gui + +//System crashes if two tasks access a file at the same time +//So we use a semaphore to see if file system is available +SemaphoreHandle_t xFATSemaphore; +const TickType_t fatSemaphore_shortWait_ms = 10 / portTICK_PERIOD_MS; +const TickType_t fatSemaphore_longWait_ms = 200 / portTICK_PERIOD_MS; + +//Display used/free space in menu and config page +uint32_t sdCardSizeMB = 0; +uint32_t sdFreeSpaceMB = 0; +uint32_t sdUsedSpaceMB = 0; + +char filename[1024]; +uint8_t buffer[5701]; + +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + +void setup() +{ + Serial.begin(115200); + delay(500); + Serial.println("SD File Listing"); + + beginSD(); + + if (online.microSD == false) + Serial.println("SD not detected. Please check card. Is it formatted?"); + else + sd.ls(LS_R | LS_DATE | LS_SIZE); //Print files on card +} + +void loop() +{ + int bytesRead; + int bytesToRead; + char data; + int length; + SdFile sdFile; + SdFile sdRootDir; + + // Read the filename + Serial.println("\nPlease enter the filename:"); + length = 0; + do { + while (!Serial.available()); + data = Serial.read(); + if ((data == ASCII_LF) || (data == ASCII_CR)) + break; + filename[length++] = data; + } while (1); + filename[length] = 0; + + // Skip reading the SD card if no filename is specified + if (length) { + Serial.printf("filename: %s\n", filename); + do { + + // Attempt to open the root directory + Serial.println("Attempting to open the root directory"); + sdRootDir = SdFile(); + if (!sdRootDir.openRoot(sd.vol())) { + Serial.println("ERROR - Failed to open root directory!"); + break; + } + + // Attempt to open the file + Serial.printf("Attempting to open file %s\n", filename); + if (!sdFile.open(&sdRootDir, filename, O_RDONLY)) { + // File not found + Serial.println("ERROR - File not found!"); + sdRootDir.close(); + break; + } + Serial.printf("File %s opened successfully!\n", filename); + + // Close the root directory + Serial.println("Closing the root directory"); + sdRootDir.close(); + + // Read the file + do { + + // Read data from the file + bytesToRead = sizeof(buffer); + Serial.printf("Attempting to read %d bytes from %s\n", bytesToRead, filename); + bytesRead = sdFile.read(buffer, bytesToRead); + Serial.printf("bytesRead: %d\n", bytesRead); + } while (bytesRead > 0); + + // Close the file + Serial.printf("Closing %s\n", filename); + sdFile.close(); + Serial.println(); + } while (0); + } + + // Wait for user to confirm reset + Serial.println("Press a key to reset"); + + while (!Serial.available()); + ESP.restart(); +} diff --git a/Firmware/Test Sketches/SD_FileListing/System.ino b/Firmware/Test Sketches/SD_FileListing/System.ino new file mode 100644 index 000000000..0aa8bebe8 --- /dev/null +++ b/Firmware/Test Sketches/SD_FileListing/System.ino @@ -0,0 +1,29 @@ +//Create a test file in file structure to make sure we can +bool createTestFile() +{ + SdFile testFile; + char testFileName[40] = "testfile.txt"; + + if (xFATSemaphore == NULL) + { + log_d("xFATSemaphore is Null"); + return (false); + } + + //Attempt to write to file system. This avoids collisions with file writing from other functions like recordSystemSettingsToFile() and F9PSerialReadTask() + if (xSemaphoreTake(xFATSemaphore, fatSemaphore_shortWait_ms) == pdPASS) + { + if (testFile.open(testFileName, O_CREAT | O_APPEND | O_WRITE) == true) + { + testFile.close(); + + if (sd.exists(testFileName)) + sd.remove(testFileName); + xSemaphoreGive(xFATSemaphore); + return (true); + } + xSemaphoreGive(xFATSemaphore); + } + + return (false); +} diff --git a/Firmware/Test Sketches/SD_FileListing/settings.h b/Firmware/Test Sketches/SD_FileListing/settings.h new file mode 100644 index 000000000..f135aa612 --- /dev/null +++ b/Firmware/Test Sketches/SD_FileListing/settings.h @@ -0,0 +1,344 @@ +//System can enter a variety of states starting at Rover_No_Fix at power on +typedef enum +{ + STATE_ROVER_NOT_STARTED = 0, + STATE_ROVER_NO_FIX, + STATE_ROVER_FIX, + STATE_ROVER_RTK_FLOAT, + STATE_ROVER_RTK_FIX, + STATE_BASE_NOT_STARTED, + STATE_BASE_TEMP_SETTLE, //User has indicated base, but current pos accuracy is too low + STATE_BASE_TEMP_SURVEY_STARTED, + STATE_BASE_TEMP_TRANSMITTING, + STATE_BASE_TEMP_WIFI_STARTED, + STATE_BASE_TEMP_WIFI_CONNECTED, //10 + STATE_BASE_TEMP_CASTER_STARTED, + STATE_BASE_TEMP_CASTER_CONNECTED, + STATE_BASE_FIXED_NOT_STARTED, + STATE_BASE_FIXED_TRANSMITTING, + STATE_BASE_FIXED_WIFI_STARTED, + STATE_BASE_FIXED_WIFI_CONNECTED, + STATE_BASE_FIXED_CASTER_STARTED, + STATE_BASE_FIXED_CASTER_CONNECTED, + STATE_BUBBLE_LEVEL, + STATE_MARK_EVENT, //20 + STATE_DISPLAY_SETUP, + STATE_WIFI_CONFIG_NOT_STARTED, + STATE_WIFI_CONFIG, + STATE_TEST, + STATE_TESTING, //25 + STATE_PROFILE_1, + STATE_PROFILE_2, + STATE_PROFILE_3, + STATE_PROFILE_4, + STATE_SHUTDOWN, +} SystemState; +volatile SystemState systemState = STATE_ROVER_NOT_STARTED; +SystemState lastSystemState = STATE_ROVER_NOT_STARTED; +SystemState requestedSystemState = STATE_ROVER_NOT_STARTED; +bool newSystemStateRequested = false; + +//The setup display can show a limited set of states +//When user pauses for X amount of time, system will enter that state +SystemState setupState = STATE_MARK_EVENT; + +typedef enum +{ + RTK_SURVEYOR = 0, + RTK_EXPRESS, + RTK_FACET, + RTK_EXPRESS_PLUS, +} ProductVariant; +ProductVariant productVariant = RTK_SURVEYOR; + +typedef enum +{ + BUTTON_ROVER = 0, + BUTTON_BASE, +} ButtonState; +ButtonState buttonPreviousState = BUTTON_ROVER; + +//Data port mux (RTK Express) can enter one of four different connections +typedef enum muxConnectionType_e +{ + MUX_UBLOX_NMEA = 0, + MUX_PPS_EVENTTRIGGER, + MUX_I2C_WT, + MUX_ADC_DAC, +} muxConnectionType_e; + +//User can enter fixed base coordinates in ECEF or degrees +typedef enum +{ + COORD_TYPE_ECEF = 0, + COORD_TYPE_GEODETIC, +} coordinateType_e; + +//User can select output pulse as either falling or rising edge +typedef enum +{ + PULSE_FALLING_EDGE = 0, + PULSE_RISING_EDGE, +} pulseEdgeType_e; + +//Custom NMEA sentence types output to the log file +typedef enum +{ + CUSTOM_NMEA_TYPE_RESET_REASON = 0, + CUSTOM_NMEA_TYPE_WAYPOINT, + CUSTOM_NMEA_TYPE_EVENT, + CUSTOM_NMEA_TYPE_SYSTEM_VERSION, + CUSTOM_NMEA_TYPE_ZED_VERSION, + CUSTOM_NMEA_TYPE_STATUS, +} customNmeaType_e; + +//Freeze and blink LEDs if we hit a bad error +typedef enum +{ + ERROR_NO_I2C = 2, //Avoid 0 and 1 as these are bad blink codes + ERROR_GPS_CONFIG_FAIL, +} t_errorNumber; + +//Radio status LED goes from off (LED off), no connection (blinking), to connected (solid) +enum RadioState +{ + RADIO_OFF = 0, + BT_ON_NOCONNECTION, //WiFi is off + BT_CONNECTED, + WIFI_ON_NOCONNECTION, //BT is off + WIFI_CONNECTED, +}; +volatile byte radioState = RADIO_OFF; + +//Return values for getByteChoice() +enum returnStatus { + STATUS_GETBYTE_TIMEOUT = 255, + STATUS_GETNUMBER_TIMEOUT = -123455555, + STATUS_PRESSED_X = 254, +}; + +#include //http://librarymanager/All#SparkFun_u-blox_GNSS + +//Each constellation will have its config key, enable, and a visible name +typedef struct ubxConstellation +{ + uint32_t configKey; + uint8_t gnssID; + bool enabled; + char textName[30]; +} ubxConstellation; + +//These are the allowable constellations to receive from and log (if enabled) +//Tested with u-center v21.02 +#define MAX_CONSTELLATIONS 6 //(sizeof(ubxConstellations)/sizeof(ubxConstellation)) + + +//Different ZED modules support different messages (F9P vs F9R vs F9T) +//Create binary packed struct for different platforms +typedef enum ubxPlatform +{ + PLATFORM_F9P = 0b0001, + PLATFORM_F9R = 0b0010, + PLATFORM_F9T = 0b0100, +} ubxPlatform; + +//Each message will have a rate, a visible name, and a class +typedef struct ubxMsg +{ + uint32_t msgConfigKey; + uint8_t msgID; + uint8_t msgClass; + uint8_t msgRate; + char msgTextName[30]; + uint8_t supported; +} ubxMsg; + +//These are the allowable messages to broadcast and log (if enabled) +//Tested with u-center v21.02 +#define MAX_UBX_MSG (13 + 25 + 5 + 10 + 3 + 12 + 5) //(sizeof(ubxMessages)/sizeof(ubxMsg)) + +//This is all the settings that can be set on RTK Surveyor. It's recorded to NVM and the config file. +typedef struct struct_settings { + int sizeOfSettings = 0; //sizeOfSettings **must** be the first entry and must be int + //int rtkIdentifier = RTK_IDENTIFIER; // rtkIdentifier **must** be the second entry + bool printDebugMessages = false; + bool enableSD = true; + bool enableDisplay = true; + int maxLogTime_minutes = 60 * 24; //Default to 24 hours + int observationSeconds = 60; //Default survey in time of 60 seconds + float observationPositionAccuracy = 5.0; //Default survey in pos accy of 5m + bool fixedBase = false; //Use survey-in by default + bool fixedBaseCoordinateType = COORD_TYPE_ECEF; + double fixedEcefX = -1280206.568; + double fixedEcefY = -4716804.403; + double fixedEcefZ = 4086665.484; + double fixedLat = 40.09029479; + double fixedLong = -105.18505761; + double fixedAltitude = 1560.089; + uint32_t dataPortBaud = 460800; //Default to 460800bps to support >10Hz update rates + uint32_t radioPortBaud = 57600; //Default to 57600bps to support connection to SiK1000 radios + bool enableNtripServer = false; + char casterHost[50] = "rtk2go.com"; //It's free... + uint16_t casterPort = 2101; + char casterUser[50] = "test@test.com"; //Some free casters require auth. User must provide their own email address to use RTK2Go + char casterUserPW[50] = ""; + char mountPointUpload[50] = "bldr_dwntwn2"; + char mountPointUploadPW[50] = "WR5wRo4H"; + char mountPointDownload[50] = "bldr_SparkFun1"; + char mountPointDownloadPW[50] = ""; + bool casterTransmitGGA = true; + char wifiSSID[50] = "TRex"; + char wifiPW[50] = "parachutes"; + float surveyInStartingAccuracy = 1.0; //Wait for 1m horizontal positional accuracy before starting survey in + uint16_t measurementRate = 250; //Elapsed ms between GNSS measurements. 25ms to 65535ms. Default 4Hz. + uint16_t navigationRate = 1; //Ratio between number of measurements and navigation solutions. Default 1 for 4Hz (with measurementRate). + bool enableI2Cdebug = false; //Turn on to display GNSS library debug messages + bool enableHeapReport = false; //Turn on to display free heap + bool enableTaskReports = false; //Turn on to display task high water marks + muxConnectionType_e dataPortChannel = MUX_UBLOX_NMEA; //Mux default to ublox UART1 + uint16_t spiFrequency = 16; //By default, use 16MHz SPI + bool enableLogging = true; //If an SD card is present, log default sentences + uint16_t sppRxQueueSize = 2048; + uint16_t sppTxQueueSize = 512; + uint8_t dynamicModel = DYN_MODEL_PORTABLE; + SystemState lastState = STATE_ROVER_NOT_STARTED; //For Express, start unit in last known state + bool throttleDuringSPPCongestion = true; + bool enableSensorFusion = false; //If IMU is available, avoid using it unless user specifically selects automotive + bool autoIMUmountAlignment = true; //Allows unit to automatically establish device orientation in vehicle + bool enableResetDisplay = false; + uint8_t resetCount = 0; + bool enableExternalPulse = true; //Send pulse once lock is achieved + uint32_t externalPulseTimeBetweenPulse_us = 1000000; //us between pulses, max of 65s + uint32_t externalPulseLength_us = 100000; //us length of pulse + pulseEdgeType_e externalPulsePolarity = PULSE_RISING_EDGE; //Pulse rises for pulse length, then falls + bool enableExternalHardwareEventLogging = false; //Log when INT/TM2 pin goes low + + ubxMsg ubxMessages[MAX_UBX_MSG] = //Report rates for all known messages + { + //NMEA + {UBLOX_CFG_MSGOUT_NMEA_ID_DTM_UART1, UBX_NMEA_DTM, UBX_CLASS_NMEA, 0, "UBX_NMEA_DTM", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_NMEA_ID_GBS_UART1, UBX_NMEA_GBS, UBX_CLASS_NMEA, 0, "UBX_NMEA_GBS", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_NMEA_ID_GGA_UART1, UBX_NMEA_GGA, UBX_CLASS_NMEA, 1, "UBX_NMEA_GGA", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_NMEA_ID_GLL_UART1, UBX_NMEA_GLL, UBX_CLASS_NMEA, 0, "UBX_NMEA_GLL", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_NMEA_ID_GNS_UART1, UBX_NMEA_GNS, UBX_CLASS_NMEA, 0, "UBX_NMEA_GNS", (PLATFORM_F9P | PLATFORM_F9R)}, + + {UBLOX_CFG_MSGOUT_NMEA_ID_GRS_UART1, UBX_NMEA_GRS, UBX_CLASS_NMEA, 0, "UBX_NMEA_GRS", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_NMEA_ID_GSA_UART1, UBX_NMEA_GSA, UBX_CLASS_NMEA, 1, "UBX_NMEA_GSA", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_NMEA_ID_GST_UART1, UBX_NMEA_GST, UBX_CLASS_NMEA, 1, "UBX_NMEA_GST", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_NMEA_ID_GSV_UART1, UBX_NMEA_GSV, UBX_CLASS_NMEA, 4, "UBX_NMEA_GSV", (PLATFORM_F9P | PLATFORM_F9R)}, //Default to 1 update every 4 fixes + {UBLOX_CFG_MSGOUT_NMEA_ID_RMC_UART1, UBX_NMEA_RMC, UBX_CLASS_NMEA, 1, "UBX_NMEA_RMC", (PLATFORM_F9P | PLATFORM_F9R)}, + + {UBLOX_CFG_MSGOUT_NMEA_ID_VLW_UART1, UBX_NMEA_VLW, UBX_CLASS_NMEA, 0, "UBX_NMEA_VLW", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_NMEA_ID_VTG_UART1, UBX_NMEA_VTG, UBX_CLASS_NMEA, 0, "UBX_NMEA_VTG", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_NMEA_ID_ZDA_UART1, UBX_NMEA_ZDA, UBX_CLASS_NMEA, 0, "UBX_NMEA_ZDA", (PLATFORM_F9P | PLATFORM_F9R)}, + + //NAV + {UBLOX_CFG_MSGOUT_UBX_NAV_ATT_UART1, UBX_NAV_ATT, UBX_CLASS_NAV, 0, "UBX_NAV_ATT", (PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_NAV_CLOCK_UART1, UBX_NAV_CLOCK, UBX_CLASS_NAV, 0, "UBX_NAV_CLOCK", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_NAV_DOP_UART1, UBX_NAV_DOP, UBX_CLASS_NAV, 0, "UBX_NAV_DOP", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_NAV_EOE_UART1, UBX_NAV_EOE, UBX_CLASS_NAV, 0, "UBX_NAV_EOE", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_NAV_GEOFENCE_UART1, UBX_NAV_GEOFENCE, UBX_CLASS_NAV, 0, "UBX_NAV_GEOFENCE", (PLATFORM_F9P | PLATFORM_F9R)}, + + {UBLOX_CFG_MSGOUT_UBX_NAV_HPPOSECEF_UART1, UBX_NAV_HPPOSECEF, UBX_CLASS_NAV, 0, "UBX_NAV_HPPOSECEF", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_NAV_HPPOSLLH_UART1, UBX_NAV_HPPOSLLH, UBX_CLASS_NAV, 0, "UBX_NAV_HPPOSLLH", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_NAV_ODO_UART1, UBX_NAV_ODO, UBX_CLASS_NAV, 0, "UBX_NAV_ODO", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_NAV_ORB_UART1, UBX_NAV_ORB, UBX_CLASS_NAV, 0, "UBX_NAV_ORB", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_NAV_POSECEF_UART1, UBX_NAV_POSECEF, UBX_CLASS_NAV, 0, "UBX_NAV_POSECEF", (PLATFORM_F9P | PLATFORM_F9R)}, + + {UBLOX_CFG_MSGOUT_UBX_NAV_POSLLH_UART1, UBX_NAV_POSLLH, UBX_CLASS_NAV, 0, "UBX_NAV_POSLLH", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_NAV_PVT_UART1, UBX_NAV_PVT, UBX_CLASS_NAV, 0, "UBX_NAV_PVT", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_NAV_RELPOSNED_UART1, UBX_NAV_RELPOSNED, UBX_CLASS_NAV, 0, "UBX_NAV_RELPOSNED", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_NAV_SAT_UART1, UBX_NAV_SAT, UBX_CLASS_NAV, 0, "UBX_NAV_SAT", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_NAV_SIG_UART1, UBX_NAV_SIG, UBX_CLASS_NAV, 0, "UBX_NAV_SIG", (PLATFORM_F9P | PLATFORM_F9R)}, + + {UBLOX_CFG_MSGOUT_UBX_NAV_STATUS_UART1, UBX_NAV_STATUS, UBX_CLASS_NAV, 0, "UBX_NAV_STATUS", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_NAV_SVIN_UART1, UBX_NAV_SVIN, UBX_CLASS_NAV, 0, "UBX_NAV_SVIN", (PLATFORM_F9P)}, + {UBLOX_CFG_MSGOUT_UBX_NAV_TIMEBDS_UART1, UBX_NAV_TIMEBDS, UBX_CLASS_NAV, 0, "UBX_NAV_TIMEBDS", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_NAV_TIMEGAL_UART1, UBX_NAV_TIMEGAL, UBX_CLASS_NAV, 0, "UBX_NAV_TIMEGAL", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_NAV_TIMEGLO_UART1, UBX_NAV_TIMEGLO, UBX_CLASS_NAV, 0, "UBX_NAV_TIMEGLO", (PLATFORM_F9P | PLATFORM_F9R)}, + + {UBLOX_CFG_MSGOUT_UBX_NAV_TIMEGPS_UART1, UBX_NAV_TIMEGPS, UBX_CLASS_NAV, 0, "UBX_NAV_TIMEGPS", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_NAV_TIMELS_UART1, UBX_NAV_TIMELS, UBX_CLASS_NAV, 0, "UBX_NAV_TIMELS", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_NAV_TIMEUTC_UART1, UBX_NAV_TIMEUTC, UBX_CLASS_NAV, 0, "UBX_NAV_TIMEUTC", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_NAV_VELECEF_UART1, UBX_NAV_VELECEF, UBX_CLASS_NAV, 0, "UBX_NAV_VELECEF", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_NAV_VELNED_UART1, UBX_NAV_VELNED, UBX_CLASS_NAV, 0, "UBX_NAV_VELNED", (PLATFORM_F9P | PLATFORM_F9R)}, + + //RXM + {UBLOX_CFG_MSGOUT_UBX_RXM_MEASX_UART1, UBX_RXM_MEASX, UBX_CLASS_RXM, 0, "UBX_RXM_MEASX", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_RXM_RAWX_UART1, UBX_RXM_RAWX, UBX_CLASS_RXM, 0, "UBX_RXM_RAWX", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_RXM_RLM_UART1, UBX_RXM_RLM, UBX_CLASS_RXM, 0, "UBX_RXM_RLM", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_RXM_RTCM_UART1, UBX_RXM_RTCM, UBX_CLASS_RXM, 0, "UBX_RXM_RTCM", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_RXM_SFRBX_UART1, UBX_RXM_SFRBX, UBX_CLASS_RXM, 0, "UBX_RXM_SFRBX", (PLATFORM_F9P | PLATFORM_F9R)}, + + //MON + {UBLOX_CFG_MSGOUT_UBX_MON_COMMS_UART1, UBX_MON_COMMS, UBX_CLASS_MON, 0, "UBX_MON_COMMS", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_MON_HW2_UART1, UBX_MON_HW2, UBX_CLASS_MON, 0, "UBX_MON_HW2", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_MON_HW3_UART1, UBX_MON_HW3, UBX_CLASS_MON, 0, "UBX_MON_HW3", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_MON_HW_UART1, UBX_MON_HW, UBX_CLASS_MON, 0, "UBX_MON_HW", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_MON_IO_UART1, UBX_MON_IO, UBX_CLASS_MON, 0, "UBX_MON_IO", (PLATFORM_F9P | PLATFORM_F9R)}, + + {UBLOX_CFG_MSGOUT_UBX_MON_MSGPP_UART1, UBX_MON_MSGPP, UBX_CLASS_MON, 0, "UBX_MON_MSGPP", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_MON_RF_UART1, UBX_MON_RF, UBX_CLASS_MON, 0, "UBX_MON_RF", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_MON_RXBUF_UART1, UBX_MON_RXBUF, UBX_CLASS_MON, 0, "UBX_MON_RXBUF", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_MON_RXR_UART1, UBX_MON_RXR, UBX_CLASS_MON, 0, "UBX_MON_RXR", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_MON_TXBUF_UART1, UBX_MON_TXBUF, UBX_CLASS_MON, 0, "UBX_MON_TXBUF", (PLATFORM_F9P | PLATFORM_F9R)}, + + //TIM + {UBLOX_CFG_MSGOUT_UBX_TIM_TM2_UART1, UBX_TIM_TM2, UBX_CLASS_TIM, 0, "UBX_TIM_TM2", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_TIM_TP_UART1, UBX_TIM_TP, UBX_CLASS_TIM, 0, "UBX_TIM_TP", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_TIM_VRFY_UART1, UBX_TIM_VRFY, UBX_CLASS_TIM, 0, "UBX_TIM_VRFY", (PLATFORM_F9P | PLATFORM_F9R)}, + + //RTCM + {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE1005_UART1, UBX_RTCM_1005, UBX_RTCM_MSB, 0, "UBX_RTCM_1005", (PLATFORM_F9P)}, + {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE1074_UART1, UBX_RTCM_1074, UBX_RTCM_MSB, 0, "UBX_RTCM_1074", (PLATFORM_F9P)}, + {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE1077_UART1, UBX_RTCM_1077, UBX_RTCM_MSB, 0, "UBX_RTCM_1077", (PLATFORM_F9P)}, + {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE1084_UART1, UBX_RTCM_1084, UBX_RTCM_MSB, 0, "UBX_RTCM_1084", (PLATFORM_F9P)}, + {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE1087_UART1, UBX_RTCM_1087, UBX_RTCM_MSB, 0, "UBX_RTCM_1087", (PLATFORM_F9P)}, + + {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE1094_UART1, UBX_RTCM_1094, UBX_RTCM_MSB, 0, "UBX_RTCM_1094", (PLATFORM_F9P)}, + {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE1097_UART1, UBX_RTCM_1097, UBX_RTCM_MSB, 0, "UBX_RTCM_1097", (PLATFORM_F9P)}, + {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE1124_UART1, UBX_RTCM_1124, UBX_RTCM_MSB, 0, "UBX_RTCM_1124", (PLATFORM_F9P)}, + {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE1127_UART1, UBX_RTCM_1127, UBX_RTCM_MSB, 0, "UBX_RTCM_1127", (PLATFORM_F9P)}, + {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE1230_UART1, UBX_RTCM_1230, UBX_RTCM_MSB, 0, "UBX_RTCM_1230", (PLATFORM_F9P)}, + + {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE4072_0_UART1, UBX_RTCM_4072_0, UBX_RTCM_MSB, 0, "UBX_RTCM_4072_0", (PLATFORM_F9P)}, + {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE4072_1_UART1, UBX_RTCM_4072_1, UBX_RTCM_MSB, 0, "UBX_RTCM_4072_1", (PLATFORM_F9P)}, + + //ESF + {UBLOX_CFG_MSGOUT_UBX_ESF_MEAS_UART1, UBX_ESF_MEAS, UBX_CLASS_ESF, 0, "UBX_ESF_MEAS", (PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_ESF_RAW_UART1, UBX_ESF_RAW, UBX_CLASS_ESF, 0, "UBX_ESF_RAW", (PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_ESF_STATUS_UART1, UBX_ESF_STATUS, UBX_CLASS_ESF, 0, "UBX_ESF_STATUS", (PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_ESF_ALG_UART1, UBX_ESF_ALG, UBX_CLASS_ESF, 0, "UBX_ESF_ALG", (PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_ESF_INS_UART1, UBX_ESF_INS, UBX_CLASS_ESF, 0, "UBX_ESF_INS", (PLATFORM_F9R)}, + }; + + //Constellations monitored/used for fix + ubxConstellation ubxConstellations[MAX_CONSTELLATIONS] = + { + {UBLOX_CFG_SIGNAL_GPS_ENA, SFE_UBLOX_GNSS_ID_GPS, true, "GPS"}, + {UBLOX_CFG_SIGNAL_SBAS_ENA, SFE_UBLOX_GNSS_ID_SBAS, true, "SBAS"}, + {UBLOX_CFG_SIGNAL_GAL_ENA, SFE_UBLOX_GNSS_ID_GALILEO, true, "Galileo"}, + {UBLOX_CFG_SIGNAL_BDS_ENA, SFE_UBLOX_GNSS_ID_BEIDOU, true, "BeiDou"}, + //{UBLOX_CFG_SIGNAL_QZSS_ENA, SFE_UBLOX_GNSS_ID_IMES, false, "IMES"}, //Not yet supported? Config key does not exist? + {UBLOX_CFG_SIGNAL_QZSS_ENA, SFE_UBLOX_GNSS_ID_QZSS, true, "QZSS"}, + {UBLOX_CFG_SIGNAL_GLO_ENA, SFE_UBLOX_GNSS_ID_GLONASS, true, "GLONASS"}, + }; + + int maxLogLength_minutes = 60 * 24; //Default to 24 hours + char profileName[50] = "Default"; + +} Settings; +Settings settings; + +//Monitor which devices on the device are on or offline. +struct struct_online { + bool microSD = false; + bool display = false; + bool gnss = false; + bool logging = false; + bool serialOutput = false; + bool fs = false; + bool rtc = false; + bool battery = false; + bool accelerometer = false; +} online; diff --git a/Firmware/Test Sketches/SD_UART_Tasks/SD_UART_Tasks.ino b/Firmware/Test Sketches/SD_UART_Tasks/SD_UART_Tasks.ino new file mode 100644 index 000000000..4f7621e39 --- /dev/null +++ b/Firmware/Test Sketches/SD_UART_Tasks/SD_UART_Tasks.ino @@ -0,0 +1,180 @@ +/* + October 20, 2022 + By: Nathan Seidle + + This example demonstrates a simplified version of reading the ZED-F9P UART1 as quickly as possible + then recording that serial data to the SD card. Two tasks are used because the SD write task is blocking + up to ~150ms, with up to 1100ms write times seen on some apparently good cards. While the SD is recording, + we continue to harvest data from the UART into the circular buffer. + + Not all SD cards are equal. A Kingston 16GB card performed terribly with multiple >500ms writes, + where as a 8GB Kingston card did flawlessly. If you see poor performance, try + formatting the card with the SD Formatter: https://www.sdcard.org/downloads/formatter/ + + At 230400bps, we receive 23040 bytes/s, or a byte every 43.4us. If the ESP32 UART FIFO is ~127 bytes, we must check + Serial.available every 5.5ms or less to avoid data loss. Additionally, if SD blocks for 250ms, we need a + 23040 * 0.25 = 5760 byte buffer. + + This sketch creates a file called myTest.txt is created and GNSS NMEA and another other messages (RAWX, etc) are logged. + + This sketch does not configure the ZED-F9P. The ZED's settings are loaded from NVM at POR so we assume + the module has been previously configured for the desired test (usually this is 4Hz fix rate, NMEAx5 + RAWx2 messages). + + We can modify the various buffer sizes and verify log integrity. We can also modify the task priorities. + Additionally, we can switch between the built-in SD library and sdFat for ease of testing. In general, + sdFat seems to perform better than the built-in SD library. + + SD writes that take longer than 100ms are shown. This helps troubleshoot an SD card that has poor performance. + Low buffer space is displayed. When the buffer reaches 0 bytes left, we can assume the log will have a corruption. + + Use the UBX Integrity checker to check the validity of the generated log: + https://github.com/sparkfun/SparkFun_u-blox_GNSS_Arduino_Library/blob/main/Utils/UBX_Integrity_Checker.py +*/ + +#include "settings.h" + +#define USE_SDFAT //Comment out to us the built-in SD library + +#ifdef USE_SDFAT + +#include "SdFat.h" //http://librarymanager/All#sdfat_exfat by Bill Greiman. Currently uses v2.1.1 +SdFat SD; + +#else + +#include "SD.h" //Built-in SD library +SPIClass spi = SPIClass(VSPI); + +#endif + +HardwareSerial serialGNSS(2); //TX on 17, RX on 16 + +const int uartReceiveBufferSize = (1024 * 2); //This buffer is filled automatically as the UART receives characters. +const int gnssHandlerBufferSize = (1024 * 4); //This buffer is filled from the UART receive buffer, and is then written to SD + +uint8_t * rBuffer; + +int pin_SCK = 18; +int pin_MISO = 19; +int pin_MOSI = 23; +int pin_microSD_CS = 25; + +File ubxFile; + +char fileName[] = "/myTest.txt"; +long fileSize = 0; +unsigned long lastFileReport = 0; + +unsigned long lastUBXLogSyncTime = 0; + +TaskHandle_t F9PSerialReadTaskHandle = NULL; //Store handles so that we can kill them if user goes into WiFi NTRIP Server mode +const uint8_t F9PSerialReadTaskPriority = 1; //3 being the highest, and 0 being the lowest +const int readTaskStackSize = 2000; + +TaskHandle_t SDWriteTaskHandle = NULL; +const uint8_t SDWriteTaskPriority = 1; //3 being the highest, and 0 being the lowest +const int SDWriteTaskStackSize = 2000; + +void setup() +{ + Serial.begin(115200); + delay(250); + + Serial.println("Start basic UART test"); + Serial.println("b) Begin logging"); + Serial.println("c) Close log before extracting card"); + Serial.println("r) Reset"); +} + +void loop() +{ + if (Serial.available()) + { + byte incoming = Serial.read(); + + if (incoming == 'r') + { + ESP.restart(); + } + else if (incoming == 'c') + { + Serial.println("Closing file..."); + online.logging = false; + stopTasks(); + + delay(1000); + + ubxFile.close(); + Serial.println("File closed"); + } + else if (incoming == 'b') + { + + rBuffer = (uint8_t*)malloc(gnssHandlerBufferSize); + +#ifdef USE_SDFAT + // if (SD.begin(SdSpiConfig(pin_microSD_CS, SHARED_SPI, SD_SCK_MHZ(32))) == false) { //This fails on some SD cards + if (SD.begin(SdSpiConfig(pin_microSD_CS, SHARED_SPI, SD_SCK_MHZ(16))) == false) { +#else + //if (!SD.begin(pin_microSD_CS)) { //Works + spi.begin(pin_SCK, pin_MISO, pin_MOSI, pin_microSD_CS); //Needed when definiing SPI bus speed + if (!SD.begin(pin_microSD_CS, spi, 80000000)) { //80MHz bus is not actually achieved +#endif + + Serial.println("Card Mount Failed"); + while (1) + { + if (Serial.available()) ESP.restart(); + delay(1); + } + } + + if (SD.exists(fileName)) + SD.remove(fileName); + +#ifdef USE_SDFAT + ubxFile.open(fileName, O_CREAT | O_APPEND | O_WRITE); +#else + ubxFile = SD.open(fileName, FILE_APPEND); +#endif + + online.microSD = true; + online.logging = true; + + serialGNSS.setRxBufferSize(uartReceiveBufferSize); //Relatively small but is serviced by high priority task + serialGNSS.setTimeout(0); //Zero disables timeout, thus, onReceive callback will only be called when RX FIFO reaches 120 bytes: https://github.com/espressif/arduino-esp32/blob/dca1a1e6b3d766aae8d0a6b8305b8273ccf5077c/cores/esp32/HardwareSerial.cpp#L233 + serialGNSS.begin(115200 * 2); //UART2 on pins 16/17 for SPP. The ZED-F9P will be configured to output NMEA over its UART1 at the same rate. + + //Start task for harvesting bytes from UART2 + if (F9PSerialReadTaskHandle == NULL) + xTaskCreate( + F9PSerialReadTask, //Function to call + "F9Read", //Just for humans + readTaskStackSize, //Stack Size + NULL, //Task input parameter + F9PSerialReadTaskPriority, //Priority + &F9PSerialReadTaskHandle); //Task handle + + //Start task for writing to SD card + if (SDWriteTaskHandle == NULL) + xTaskCreate( + SDWriteTask, //Function to call + "SDWrite", //Just for humans + SDWriteTaskStackSize, //Stack Size + NULL, //Task input parameter + SDWriteTaskPriority, //Priority + &SDWriteTaskHandle); //Task handle + + Serial.println("Recording to log..."); + } + } + + //Report file sizes to show recording is working + if ((millis() - lastFileReport) > 5000) + { + lastFileReport = millis(); + Serial.printf("UBX file size: %ld\n\r", fileSize); + } + + delay(1); //Yield to other tasks +} diff --git a/Firmware/Test Sketches/SD_UART_Tasks/Tasks.ino b/Firmware/Test Sketches/SD_UART_Tasks/Tasks.ino new file mode 100644 index 000000000..cfb64d689 --- /dev/null +++ b/Firmware/Test Sketches/SD_UART_Tasks/Tasks.ino @@ -0,0 +1,162 @@ +volatile static uint16_t dataHead = 0; //Head advances as data comes in from GNSS's UART +volatile static uint16_t sdTail = 0; //SD Tail advances as it is recorded to SD + +int bufferOverruns = 0; + +//Read bytes from UART2 into circular buffer +void F9PSerialReadTask(void *e) +{ + bool newDataLost = false; + + while (true) + { + //Determine the amount of microSD card logging data in the buffer + if (online.logging) + { + if (serialGNSS.available()) + { + int usedBytes = dataHead - sdTail; //Distance between head and tail + if (usedBytes < 0) + usedBytes += gnssHandlerBufferSize; + + int availableHandlerSpace = gnssHandlerBufferSize - usedBytes; + + //Don't fill the last byte to prevent buffer overflow + if (availableHandlerSpace) + availableHandlerSpace -= 1; + + //Check for buffer overruns + //int availableUARTSpace = settings.uartReceiveBufferSize - serialGNSS.available(); + int availableUARTSpace = uartReceiveBufferSize - serialGNSS.available(); + int combinedSpaceRemaining = availableHandlerSpace + availableUARTSpace; + + //log_d("availableHandlerSpace = %d availableUARTSpace = %d bufferOverruns: %d", availableHandlerSpace, availableUARTSpace, bufferOverruns); + + if (combinedSpaceRemaining == 0) + { + if (newDataLost == false) + { + newDataLost = true; + bufferOverruns++; + //if (settings.enablePrintBufferOverrun) + //Serial.sprintf("Data loss likely! availableHandlerSpace = %d availableUARTSpace = %d bufferOverruns: %d\n\r", availableHandlerSpace, availableUARTSpace, bufferOverruns); + log_d("Data loss likely! availableHandlerSpace = %d availableUARTSpace = %d bufferOverruns: %d", availableHandlerSpace, availableUARTSpace, bufferOverruns); + } + } + //else if (combinedSpaceRemaining < ( (gnssHandlerBufferSize + settings.uartReceiveBufferSize) / 16)) + else if (combinedSpaceRemaining < ( (gnssHandlerBufferSize + uartReceiveBufferSize) / 16)) + { + //if (settings.enablePrintBufferOverrun) + log_d("Low space: availableHandlerSpace = %d availableUARTSpace = %d bufferOverruns: %d", availableHandlerSpace, availableUARTSpace, bufferOverruns); + } + else + newDataLost = false; //Reset + + //While there is free buffer space and UART2 has at least one RX byte + while (availableHandlerSpace && serialGNSS.available()) + { + //Fill the buffer to the end and then start at the beginning + int availableSlice = availableHandlerSpace; + if ((dataHead + availableHandlerSpace) >= gnssHandlerBufferSize) + availableSlice = gnssHandlerBufferSize - dataHead; + + //Add new data into circular buffer in front of the head + //availableHandlerSpace is already reduced to avoid buffer overflow + int newBytesToRecord = serialGNSS.read(&rBuffer[dataHead], availableSlice); + + //Check for negative (error) + if (newBytesToRecord <= 0) + break; + + //Account for the bytes read + availableHandlerSpace -= newBytesToRecord; + + dataHead += newBytesToRecord; + if (dataHead >= gnssHandlerBufferSize) + dataHead -= gnssHandlerBufferSize; + + delay(1); + taskYIELD(); + } //End Serial.available() + } + } + + delay(1); + taskYIELD(); + } +} + +//Log data to the SD card +void SDWriteTask(void *e) +{ + while (true) + { + int lastDataHead = dataHead; //dataHead may change during this task by the harvesting task. Use a snapshot. + + //If user wants to log, record to SD + if (!online.logging) + //Discard the data + sdTail = lastDataHead; + else + { + int sdBytesToRecord = lastDataHead - sdTail; //Amount of buffered microSD card logging data + if (sdBytesToRecord < 0) + sdBytesToRecord += gnssHandlerBufferSize; + + if (sdBytesToRecord > 0) + { + //Reduce bytes to record to SD if we have more to send then the end of the buffer + //We'll wrap next loop + if ((sdTail + sdBytesToRecord) > gnssHandlerBufferSize) + sdBytesToRecord = gnssHandlerBufferSize - sdTail; + + long startTime = millis(); + int recordedBytes = ubxFile.write(&rBuffer[sdTail], sdBytesToRecord); + long endTime = millis(); + + if (endTime - startTime > 150) log_d("Long Write! Delta time: %d / Recorded %d bytes", endTime - startTime, recordedBytes); + + fileSize = ubxFile.fileSize(); //Get updated filed size + + //Account for the sent data or dropped + if (recordedBytes > 0) + { + sdTail += recordedBytes; + if (sdTail >= gnssHandlerBufferSize) + sdTail -= gnssHandlerBufferSize; + } + + //Force file sync every 60s + if (millis() - lastUBXLogSyncTime > 60000) + { + ubxFile.sync(); + lastUBXLogSyncTime = millis(); + } //End sdCardSemaphore + + } //End logging + } + + delay(1); + taskYIELD(); + } +} + +//Stop UART and SD tasks - useful when running firmware update or WiFi AP is running +void stopTasks() +{ + //Delete tasks if running + if (F9PSerialReadTaskHandle != NULL) + { + vTaskDelete(F9PSerialReadTaskHandle); + F9PSerialReadTaskHandle = NULL; + } + if (SDWriteTaskHandle != NULL) + { + vTaskDelete(SDWriteTaskHandle); + SDWriteTaskHandle = NULL; + } + + //Give the other CPU time to finish + //Eliminates CPU bus hang condition + delay(100); +} diff --git a/Firmware/Test Sketches/SD_UART_Tasks/settings.h b/Firmware/Test Sketches/SD_UART_Tasks/settings.h new file mode 100644 index 000000000..0cc5d2415 --- /dev/null +++ b/Firmware/Test Sketches/SD_UART_Tasks/settings.h @@ -0,0 +1,19 @@ +//Monitor which devices on the device are on or offline. +struct struct_online { + bool microSD = false; + bool display = false; + bool gnss = false; + bool logging = false; + bool serialOutput = false; + bool fs = false; + bool rtc = false; + bool battery = false; + bool accelerometer = false; + bool ntripClient = false; + bool ntripServer = false; + bool lband = false; + bool lbandCorrections = false; + bool i2c = false; + bool nmeaClient = false; + bool nmeaServer = false; +} online; diff --git a/Firmware/Test Sketches/SD_Update/SD_Update.ino b/Firmware/Test Sketches/SD_Update/SD_Update.ino deleted file mode 100644 index 539063e27..000000000 --- a/Firmware/Test Sketches/SD_Update/SD_Update.ino +++ /dev/null @@ -1,84 +0,0 @@ -/* - Demonstrates reading a binary file from SD and reprogramming ESP32. - - To work, we must the use a partition scheme that includes OTA. - For RTK Surveyor, 'Minimal SPIFFS (1.9MB with OTA/190KB SPIFFS)' works well. -*/ - -#include - -#include "settings.h" - -const byte PIN_MICROSD_CHIP_SELECT = 25; - -//microSD Interface -//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- -#include -#include //SdFat (FAT32) by Bill Greiman: http://librarymanager/All#SdFat -SdFat sd; -SdFile gnssDataFile; //File that all gnss data is written to - -char settingsFileName[40] = "SFE_Surveyor_Settings.txt"; //File to read/write system settings to - -unsigned long lastDataLogSyncTime = 0; //Used to record to SD every half second -long startLogTime_minutes = 0; //Mark when we start logging so we can stop logging after maxLogTime_minutes -//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- - -const byte menuTimeout = 15; //Menus will exit/timeout after this number of seconds - -int binCount = 0; -char binFileNames[10][50]; -const char* forceFirmwareFileName = "RTK_Surveyor_Firmware_Force.bin"; - -void setup() { - Serial.begin(115200); - delay(1000); - Serial.println("SD Update"); - - beginSD(); //Test if SD is present - - if (online.microSD == true) - { - Serial.println("SD online"); - scanForFirmware(); - } - - createTestFile(1); -} - - -void loop() { - if (Serial.available()) - { - byte incoming = Serial.read(); - - if (incoming == '1') - { - menuFirmware(); - } - else - { - Serial.println("Unknown command"); - } - - delay(10); - while (Serial.available()) Serial.read(); //Remote extra chars - } - -} - -void createTestFile(int callNumber) -{ - SdFile testFile; - - if (testFile.open("testfile.txt", O_CREAT | O_APPEND | O_WRITE) == false) - { - Serial.printf("Failed to create test file: %d\n", callNumber); - } - else - { - Serial.println("Test file created!"); - if (sd.exists("testfile.txt")) - sd.remove("testfile.txt"); - } -} diff --git a/Firmware/Test Sketches/SD_Update/begin.ino b/Firmware/Test Sketches/SD_Update/begin.ino deleted file mode 100644 index c98a0a4c4..000000000 --- a/Firmware/Test Sketches/SD_Update/begin.ino +++ /dev/null @@ -1,40 +0,0 @@ -void beginSD() -{ - pinMode(PIN_MICROSD_CHIP_SELECT, OUTPUT); - digitalWrite(PIN_MICROSD_CHIP_SELECT, HIGH); //Be sure SD is deselected - - if (settings.enableSD == true) - { - //Max power up time is 250ms: https://www.kingston.com/datasheets/SDCIT-specsheet-64gb_en.pdf - //Max current is 200mA average across 1s, peak 300mA - delay(10); - - if (sd.begin(PIN_MICROSD_CHIP_SELECT, SD_SCK_MHZ(24)) == false) //Standard SdFat - { - Serial.println("SD init failed (first attempt). Trying again...\r\n"); - //Give SD more time to power up, then try again - delay(250); - if (sd.begin(PIN_MICROSD_CHIP_SELECT, SD_SCK_MHZ(24)) == false) //Standard SdFat - { - Serial.println(F("SD init failed (second attempt). Is card present? Formatted?")); - digitalWrite(PIN_MICROSD_CHIP_SELECT, HIGH); //Be sure SD is deselected - online.microSD = false; - return; - } - } - - //Change to root directory. All new file creation will be in root. - if (sd.chdir() == false) - { - Serial.println(F("SD change directory failed")); - online.microSD = false; - return; - } - - online.microSD = true; - } - else - { - online.microSD = false; - } -} diff --git a/Firmware/Test Sketches/SD_Update/menuFirmware.ino b/Firmware/Test Sketches/SD_Update/menuFirmware.ino deleted file mode 100644 index abb1fe292..000000000 --- a/Firmware/Test Sketches/SD_Update/menuFirmware.ino +++ /dev/null @@ -1,174 +0,0 @@ -//Update firmware if bin files found -void menuFirmware() -{ - if (online.microSD == false) - { - Serial.println(F("No SD card detected")); - } - - if (binCount == 0) - { - Serial.println("No valid binary files found."); - delay(2000); - return; - } - - while (1) - { - Serial.println(); - Serial.println(F("Menu: Update Firmware Menu")); - - for (int x = 0 ; x < binCount ; x++) - { - Serial.printf("%d) Load %s\n", x + 1, binFileNames[x]); - } - - Serial.println(F("x) Exit")); - - int incoming = getNumber(menuTimeout); //Timeout after x seconds - - if (incoming > 0 && incoming < (binCount + 1)) - { - //Adjust incoming to match array - incoming--; - Serial.printf("Loading %s\n", binFileNames[incoming]); - updateFromSD(binFileNames[incoming]); - } - else if (incoming == STATUS_PRESSED_X) - break; - else if (incoming == STATUS_GETNUMBER_TIMEOUT) - break; - else - Serial.printf("Bad value: %d\n", incoming); - } - - while (Serial.available()) Serial.read(); //Empty buffer of any newline chars -} - -//Looks for matching binary files in root -//Loads a global called binCount -void scanForFirmware() -{ - if (online.microSD == true) - { - //Count available binaries - SdFile tempFile; - SdFile dir; - const char* BIN_EXT = "bin"; - const char* BIN_HEADER = "RTK_Surveyor_Firmware"; - char fname[50]; //Handle long file names - - dir.open("/"); //Open root - - while (tempFile.openNext(&dir, O_READ)) - { - if (tempFile.isFile()) - { - tempFile.getName(fname, sizeof(fname)); - - if (strcmp(forceFirmwareFileName, fname) == 0) - updateFromSD((char *)forceFirmwareFileName); - - //Check for 'sfe_rtk' and 'bin' extension - if (strcmp(BIN_EXT, &fname[strlen(fname) - strlen(BIN_EXT)]) == 0) - { - if (strstr(fname, BIN_HEADER) != NULL) - { - strcpy(binFileNames[binCount++], fname); //Add this to the array - } - else - Serial.printf("Unknown: %s\n", fname); - } - } - tempFile.close(); - } - } -} - -//Look for firmware file on SD card and update as needed -void updateFromSD(char *firmwareFileName) -{ - Serial.printf("Loading %s\n", firmwareFileName); - if (sd.exists(firmwareFileName)) - { - SdFile firmwareFile; - firmwareFile.open(firmwareFileName, O_READ); - - size_t updateSize = firmwareFile.fileSize(); - if (updateSize == 0) - { - Serial.println(F("Error: Binary is empty")); - firmwareFile.close(); - return; - } - - if (Update.begin(updateSize) == false) - { - Serial.println(F("Update begin failed. Not enough partition space available.")); - firmwareFile.close(); - return; - } - - Serial.print(F("Moving file to OTA section")); - - const int pageSize = 512; - byte dataArray[pageSize]; - int bytesWritten = 0; - - //Indicate progress - int barWidthInCharacters = 20; //Width of progress bar, ie [###### % complete - long portionSize = updateSize / barWidthInCharacters; - int barWidth = 0; - - //Bulk write from the SD file to the EEPROM - while (firmwareFile.available()) - { - int bytesToWrite = pageSize; //Max number of bytes to read - if (firmwareFile.available() < bytesToWrite) bytesToWrite = firmwareFile.available(); //Trim this read size as needed - - firmwareFile.read(dataArray, bytesToWrite); //Read the next set of bytes from file into our temp array - - if (Update.write(dataArray, bytesToWrite) != bytesToWrite) - Serial.println(F("Write failed")); - else - bytesWritten += bytesToWrite; - - //Indicate progress - if (bytesWritten > barWidth * portionSize) - { - //Advance the bar - barWidth++; - Serial.print("\n["); - for (int x = 0 ; x < barWidth ; x++) - Serial.print("="); - Serial.printf("%d%%", bytesWritten * 100 / updateSize); - if (bytesWritten == updateSize) Serial.println("]"); - } - } - - Serial.println(F("\nFile move complete")); - - if (Update.end()) - { - if (Update.isFinished()) - { - Serial.println(F("Firmware updated successfully. Rebooting. Good bye!")); - delay(1000); - ESP.restart(); - } - else - Serial.println(F("Update not finished? Something went wrong!")); - } - else - { - Serial.print(F("Error Occurred. Error #: ")); - Serial.println(String(Update.getError())); - } - - firmwareFile.close(); - } - else - { - Serial.println(F("No firmware file found")); - } -} diff --git a/Firmware/Test Sketches/SD_Update/settings.h b/Firmware/Test Sketches/SD_Update/settings.h deleted file mode 100644 index a6ae9d627..000000000 --- a/Firmware/Test Sketches/SD_Update/settings.h +++ /dev/null @@ -1,54 +0,0 @@ -//Return values for getByteChoice() -enum returnStatus { - STATUS_GETBYTE_TIMEOUT = 255, - STATUS_GETNUMBER_TIMEOUT = -123455555, - STATUS_PRESSED_X, -}; - -//This is all the settings that can be set on RTK Surveyor. It's recorded to NVM and the config file. -struct struct_settings { - int sizeOfSettings = 0; //sizeOfSettings **must** be the first entry and must be int - //int rtkIdentifier = RTK_IDENTIFIER; // rtkIdentifier **must** be the second entry - uint8_t gnssMeasurementFrequency = 4; //Number of fixes per second - bool printDebugMessages = false; - bool enableSD = true; - bool enableDisplay = true; - bool zedOutputLogging = false; - bool gnssRAWOutput = false; - bool frequentFileAccessTimestamps = false; - int maxLogTime_minutes = 60*10; //Default to 10 hours - int observationSeconds = 60; //Default survey in time of 60 seconds - float observationPositionAccuracy = 5.0; //Default survey in pos accy of 5m - bool fixedBase = false; //Use survey-in by default - //bool fixedBaseCoordinateType = COORD_TYPE_ECEF; - double fixedEcefX = 0.0; - double fixedEcefY = 0.0; - double fixedEcefZ = 0.0; - double fixedLat = 0.0; - double fixedLong = 0.0; - double fixedAltitude = 0.0; - uint32_t dataPortBaud = 115200; //Default to 115200bps - uint32_t radioPortBaud = 57600; //Default to 57600bps to support connection to SiK1000 radios - bool outputSentenceGGA = true; - bool outputSentenceGSA = true; - bool outputSentenceGSV = true; - bool outputSentenceRMC = true; - bool outputSentenceGST = true; - bool enableSBAS = false; //Bug in ZED-F9P v1.13 firmware causes RTK LED to not light when RTK Floating with SBAS on. - bool enableNtripServer = false; - char casterHost[50] = "rtk2go.com"; //It's free... - uint16_t casterPort = 2101; - char mountPoint[50] = "bldr_dwntwn2"; - char mountPointPW[50] = "WR5wRo4H"; - char wifiSSID[50] = "TRex"; - char wifiPW[50] = "parachutes"; -} settings; - -//These are the devices on board RTK Surveyor that may be on or offline. -struct struct_online { - bool microSD = false; - bool display = false; - bool dataLogging = false; - bool serialOutput = false; - bool eeprom = false; -} online; diff --git a/Firmware/Test Sketches/SetConstellations/SetConstellations.ino b/Firmware/Test Sketches/SetConstellations/SetConstellations.ino deleted file mode 100644 index b86a990ce..000000000 --- a/Firmware/Test Sketches/SetConstellations/SetConstellations.ino +++ /dev/null @@ -1,284 +0,0 @@ -/* - Set correct bits to enable/disable constallations on the ZED-F9P - - See -CFG-GNSS in ZED's protocol doc - - 21:20:31 0000 B5 62 06 3E (len)34 00(len) - 00(ver) 3C(tracking chan) 3C(chan) 06(config blocks) - 00(gnssid) 08 10 00 01(enable) 00 11(sigCfgMask) 11 GPS - 01 03 03 00 01 00 01 01 SBAS - 02 0A 12 00 01 00 21 21 Gal - 03 02 05 00 01 00 11 11 Bei - 05 00 04 00 01 00 11 15 QZSS <-- Note 04 IMES is missing - 06 08 0C 00 01 00 11 11 GLO - Above is poll of default config from u-center - - 21:20:31 0000 B5 62 06 3E 34 00 - 00 00 3C 06 - 00 08 10 00 00 00 11 01 GPS - 01 03 03 00 01 00 01 01 SBAS - 02 0A 12 00 01 00 21 01 Gal - 03 02 05 00 01 00 11 01 Bei - 05 00 04 00 00 00 11 01 QZSS - 06 08 0C 00 01 00 11 01 GLO - Above is command for GPS and QZSS turned off - - Ugh. The issue is that the doc says IMES is gnssid 4 but really QZSS is in 4th position but with ID 5. - - Works: - GPS - GLONASS - SBAS - Galileo - BeiDou - QZSS - - Not working: - IMES - Does not seem to be supported - -*/ - -#include //Needed for I2C to GPS - -#include //http://librarymanager/All#SparkFun_u-blox_GNSS -SFE_UBLOX_GNSS i2cGNSS; - -#define MAX_PAYLOAD_SIZE 384 // Override MAX_PAYLOAD_SIZE for getModuleInfo which can return up to 348 bytes - -void setup() -{ - Serial.begin(115200); - delay(200); //Wait for ESP32 - Serial.println("SparkFun u-blox Example"); - - Wire.begin(); - - //i2cGNSS.enableDebugging(); // Uncomment this line to enable debug messages - - if (i2cGNSS.begin() == false) //Connect to the Ublox module using Wire port - { - Serial.println(F("u-blox GPS not detected at default I2C address. Please check wiring. Freezing.")); - while (1); - } - -} - -void loop() -{ - if (Serial.available()) - { - byte incoming = Serial.read(); - - if (incoming == 'G') - { - if (setConstellation(SFE_UBLOX_GNSS_ID_GPS, true) == false) - Serial.println("Enable GPS Fail"); - else - Serial.println("Enable GPS Success"); - } - else if (incoming == 'g') - { - if (setConstellation(SFE_UBLOX_GNSS_ID_GPS, false) == false) - Serial.println("Disable GPS Fail"); - else - Serial.println("Disable GPS Success"); - } - else if (incoming == 'R') - { - if (setConstellation(SFE_UBLOX_GNSS_ID_GLONASS, true) == false) - Serial.println("Enable GLONASS Fail"); - else - Serial.println("Enable GLONASS Success"); - } - else if (incoming == 'r') - { - if (setConstellation(SFE_UBLOX_GNSS_ID_GLONASS, false) == false) - Serial.println("Disable GLONASS Fail"); - else - Serial.println("Disable GLONASS Success"); - } - else if (incoming == 'S') - { - if (setConstellation(SFE_UBLOX_GNSS_ID_SBAS, true) == false) - Serial.println("Enable SBAS Fail"); - else - Serial.println("Enable SBAS Success"); - } - else if (incoming == 's') - { - if (setConstellation(SFE_UBLOX_GNSS_ID_SBAS, false) == false) - Serial.println("Disable SBAS Fail"); - else - Serial.println("Disable SBAS Success"); - } - else if (incoming == 'A') - { - if (setConstellation(SFE_UBLOX_GNSS_ID_GALILEO, true) == false) - Serial.println("Enable Galileo Fail"); - else - Serial.println("Enable Galileo Success"); - } - else if (incoming == 'a') - { - if (setConstellation(SFE_UBLOX_GNSS_ID_GALILEO, false) == false) - Serial.println("Disable Galileo Fail"); - else - Serial.println("Disable Galileo Success"); - } - else if (incoming == 'B') - { - if (setConstellation(SFE_UBLOX_GNSS_ID_BEIDOU, true) == false) - Serial.println("Enable BeiDou Fail"); - else - Serial.println("Enable BeiDou Success"); - } - else if (incoming == 'b') - { - if (setConstellation(SFE_UBLOX_GNSS_ID_BEIDOU, false) == false) - Serial.println("Disable BeiDou Fail"); - else - Serial.println("Disable BeiDou Success"); - } - else if (incoming == 'Q') - { - if (setConstellation(SFE_UBLOX_GNSS_ID_QZSS, true) == false) - Serial.println("Enable QZSS Fail"); - else - Serial.println("Enable QZSS Success"); - } - else if (incoming == 'q') - { - if (setConstellation(SFE_UBLOX_GNSS_ID_QZSS, false) == false) - Serial.println("Disable QZSS Fail"); - else - Serial.println("Disable QZSS Success"); - } - else - { - //Serial.println("Unknown"); - } - } - -} - -//The u-blox library doesn't directly support constellation control so let's do it manually -//Also allows the enable/disable of any constellation (BeiDou, Galileo, etc) -bool setConstellation(uint8_t constellation, bool enable) -{ - uint8_t customPayload[MAX_PAYLOAD_SIZE]; // This array holds the payload data bytes - ubxPacket customCfg = {0, 0, 0, 0, 0, customPayload, 0, 0, SFE_UBLOX_PACKET_VALIDITY_NOT_DEFINED, SFE_UBLOX_PACKET_VALIDITY_NOT_DEFINED}; - - customCfg.cls = UBX_CLASS_CFG; // This is the message Class - customCfg.id = UBX_CFG_GNSS; // This is the message ID - customCfg.len = 0; // Setting the len (length) to zero lets us poll the current settings - customCfg.startingSpot = 0; // Always set the startingSpot to zero (unless you really know what you are doing) - - uint16_t maxWait = 1250; // Wait for up to 250ms (Serial may need a lot longer e.g. 1100) - - // Read the current setting. The results will be loaded into customCfg. - if (i2cGNSS.sendCommand(&customCfg, maxWait) != SFE_UBLOX_STATUS_DATA_RECEIVED) // We are expecting data and an ACK - { - Serial.println(F("Set Constellation failed")); - return (false); - } - - if (enable) - { - if (constellation == SFE_UBLOX_GNSS_ID_GPS || constellation == SFE_UBLOX_GNSS_ID_QZSS) - { - //QZSS must follow GPS - customPayload[locateGNSSID(customPayload, SFE_UBLOX_GNSS_ID_GPS) + 4] |= (1 << 0); //Set the enable bit - customPayload[locateGNSSID(customPayload, SFE_UBLOX_GNSS_ID_QZSS) + 4] |= (1 << 0); //Set the enable bit - } - else - { - customPayload[locateGNSSID(customPayload, constellation) + 4] |= (1 << 0); //Set the enable bit - } - - //Set sigCfgMask as well - if (constellation == SFE_UBLOX_GNSS_ID_GPS || constellation == SFE_UBLOX_GNSS_ID_QZSS) - { - customPayload[locateGNSSID(customPayload, SFE_UBLOX_GNSS_ID_GPS) + 6] |= 0x11; //Enable GPS L1C/A, and L2C - - //QZSS must follow GPS - customPayload[locateGNSSID(customPayload, SFE_UBLOX_GNSS_ID_QZSS) + 6] = 0x11; //Enable QZSS L1C/A, and L2C - Follow u-center - //customPayload[locateGNSSID(customPayload, SFE_UBLOX_GNSS_ID_QZSS) + 6] = 0x15; //Enable QZSS L1C/A, L1S, and L2C - } - else if (constellation == SFE_UBLOX_GNSS_ID_SBAS) - { - customPayload[locateGNSSID(customPayload, constellation) + 6] |= 0x01; //Enable SBAS L1C/A - } - else if (constellation == SFE_UBLOX_GNSS_ID_GALILEO) - { - customPayload[locateGNSSID(customPayload, constellation) + 6] |= 0x21; //Enable Galileo E1/E5b - } - else if (constellation == SFE_UBLOX_GNSS_ID_BEIDOU) - { - customPayload[locateGNSSID(customPayload, constellation) + 6] |= 0x11; //Enable BeiDou B1I/B2I - } - // else if (constellation == SFE_UBLOX_GNSS_ID_IMES) //Does not exist in u-center v21.02 - // { - // customPayload[locateGNSSID(customPayload, constellation) + 6] |= 0x01; //Enable IMES L1 - // } - else if (constellation == SFE_UBLOX_GNSS_ID_GLONASS) - { - customPayload[locateGNSSID(customPayload, constellation) + 6] |= 0x11; //Enable GLONASS L1 and L2 - } - } - else //Disable - { - //QZSS must follow GPS - if (constellation == SFE_UBLOX_GNSS_ID_GPS || constellation == SFE_UBLOX_GNSS_ID_QZSS) - { - customPayload[locateGNSSID(customPayload, SFE_UBLOX_GNSS_ID_GPS) + 4] &= ~(1 << 0); //Clear the enable bit - - customPayload[locateGNSSID(customPayload, SFE_UBLOX_GNSS_ID_QZSS) + 4] &= ~(1 << 0); //Clear the enable bit - } - else - { - customPayload[locateGNSSID(customPayload, constellation) + 4] &= ~(1 << 0); //Clear the enable bit - } - - } - - Serial.println("Custom payload:"); - for (int x = 0 ; x < 4 + 8 * 6 ; x++) - { - if (x == 4 + 8 * 0) Serial.println(); //Hdr - if (x == 4 + 8 * 1) Serial.println(); //GPS - if (x == 4 + 8 * 2) Serial.println(); - if (x == 4 + 8 * 3) Serial.println(); - if (x == 4 + 8 * 4) Serial.println(); - if (x == 4 + 8 * 5) Serial.println(); - if (x == 4 + 8 * 6) Serial.println(); - if (x == 4 + 8 * 7) Serial.println(); - Serial.print(" "); - if (customPayload[x] < 0x10) Serial.print("0"); - Serial.print(customPayload[x], HEX); - } - Serial.println(); - - // Now we write the custom packet back again to change the setting - if (i2cGNSS.sendCommand(&customCfg, maxWait) != SFE_UBLOX_STATUS_DATA_SENT) // This time we are only expecting an ACK - { - Serial.println(F("Constellation setting failed")); - return (false); - } - - return (true); -} - -//Given a payload, return the location of a given constellation -//This is needed because IMES is not currently returned in the query packet -//so QZSS and GLONAS are offset by -8 bytes. -uint8_t locateGNSSID(uint8_t *customPayload, uint8_t constellation) -{ - for (int x = 0 ; x < 7 ; x++) //Assume max of 7 constellations - { - if (customPayload[4 + 8 * x] == constellation) //Test gnssid - return (4 + x * 8); - } - - Serial.println(F("locateGNSSID failed")); - return (0); -} diff --git a/Firmware/Test Sketches/SetSBAS/SetSBAS.ino b/Firmware/Test Sketches/SetSBAS/SetSBAS.ino deleted file mode 100644 index 86223519a..000000000 --- a/Firmware/Test Sketches/SetSBAS/SetSBAS.ino +++ /dev/null @@ -1,84 +0,0 @@ -/* - -*/ - -#include //Needed for I2C to GPS - -#include "SparkFun_Ublox_Arduino_Library.h" //http://librarymanager/All#SparkFun_Ublox_GPS -SFE_UBLOX_GPS myGPS; - -void setup() -{ - Serial.begin(115200); // You may need to increase this for high navigation rates! - while (!Serial) ; - Serial.println("SparkFun Ublox Example"); - - Wire.begin(); - - //myGPS.enableDebugging(); // Uncomment this line to enable debug messages - - if (myGPS.begin() == false) //Connect to the Ublox module using Wire port - { - Serial.println(F("Ublox GPS not detected at default I2C address. Please check wiring. Freezing.")); - while (1) - ; - } - - setSBAS(true); - - Serial.println("Done"); - while (1); -} - -void loop() -{ - //Query the module as fast as possible - int32_t latitude = myGPS.getLatitude(); - Serial.print(F("Lat: ")); - Serial.print(latitude); - - int32_t longitude = myGPS.getLongitude(); - Serial.print(F(" Lon: ")); - Serial.print(longitude); - Serial.print(F(" (degrees * 10^-7)")); - - int32_t altitude = myGPS.getAltitude(); - Serial.print(F(" Alt: ")); - Serial.print(altitude); - Serial.println(F(" (mm)")); -} - -//The Ublox library doesn't directly support SBAS control so let's do it manually -bool setSBAS(bool enableSBAS) -{ - uint8_t customPayload[MAX_PAYLOAD_SIZE]; // This array holds the payload data bytes - ubxPacket customCfg = {0, 0, 0, 0, 0, customPayload, 0, 0, SFE_UBLOX_PACKET_VALIDITY_NOT_DEFINED, SFE_UBLOX_PACKET_VALIDITY_NOT_DEFINED}; - - customCfg.cls = UBX_CLASS_CFG; // This is the message Class - customCfg.id = UBX_CFG_GNSS; // This is the message ID - customCfg.len = 0; // Setting the len (length) to zero lets us poll the current settings - customCfg.startingSpot = 0; // Always set the startingSpot to zero (unless you really know what you are doing) - - uint16_t maxWait = 250; // Wait for up to 250ms (Serial may need a lot longer e.g. 1100) - - // Read the current setting. The results will be loaded into customCfg. - if (myGPS.sendCommand(&customCfg, maxWait) != SFE_UBLOX_STATUS_DATA_RECEIVED) // We are expecting data and an ACK - { - Serial.println(F("Set SBAS failed!")); - return (false); - } - - if (enableSBAS) - customPayload[8 + 1 * 8] |= (1 << 0); //Set the enable bit - else - customPayload[8 + 1 * 8] &= ~(1 << 0); //Clear the enable bit - - // Now we write the custom packet back again to change the setting - if (myGPS.sendCommand(&customCfg, maxWait) != SFE_UBLOX_STATUS_DATA_SENT) // This time we are only expecting an ACK - { - Serial.println(F("SBAS setting failed!")); - return (false); - } - - return (true); -} diff --git a/Firmware/Test Sketches/StayOn/StayOn.bin b/Firmware/Test Sketches/StayOn/StayOn.bin new file mode 100644 index 000000000..3c2a0d026 Binary files /dev/null and b/Firmware/Test Sketches/StayOn/StayOn.bin differ diff --git a/Firmware/Test Sketches/StayOn/StayOn.ino b/Firmware/Test Sketches/StayOn/StayOn.ino new file mode 100644 index 000000000..88834f3cc --- /dev/null +++ b/Firmware/Test Sketches/StayOn/StayOn.ino @@ -0,0 +1,15 @@ +/* + Keeps ESP32 running in infinite loop +*/ + +void setup() +{ + Serial.begin(115200); + Serial.println("Power on"); +} + +void loop() +{ + delay(100); + Serial.print("."); +} diff --git a/Firmware/Test Sketches/StayOn/Successful Loading of StayOn.jpg b/Firmware/Test Sketches/StayOn/Successful Loading of StayOn.jpg new file mode 100644 index 000000000..bc5cae5e1 Binary files /dev/null and b/Firmware/Test Sketches/StayOn/Successful Loading of StayOn.jpg differ diff --git a/Binaries/batch_program.bat b/Firmware/Test Sketches/StayOn/batch_program.bat similarity index 72% rename from Binaries/batch_program.bat rename to Firmware/Test Sketches/StayOn/batch_program.bat index 117b21192..1ec98c12f 100644 --- a/Binaries/batch_program.bat +++ b/Firmware/Test Sketches/StayOn/batch_program.bat @@ -7,10 +7,10 @@ if [%1]==[] goto usage :loop @echo - -@echo Programming binary: %1 on %2 +@echo Programming binary on %1 rem @esptool.exe --chip esp32 --port COM6 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size detect 0 RTK_Surveyor_Firmware_v13_combined.bin -@esptool.exe --chip esp32 --port %2 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size detect 0 %1 +@esptool.exe --chip esp32 --port %1 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size detect 0 StayOn.bin @echo Done programming! Ready for next board. @pause @@ -18,4 +18,4 @@ rem @esptool.exe --chip esp32 --port COM6 --baud 921600 --before default_reset - goto loop :usage -@echo Missing the binary file and com port arguments. Ex: batch_program.bat RTK_Surveyor_Firmware_v11_combined.bin COM6 \ No newline at end of file +@echo Missing the binary file and com port arguments. Ex: batch_program.bat COM4 \ No newline at end of file diff --git a/Binaries/esptool.exe b/Firmware/Test Sketches/StayOn/esptool.exe similarity index 100% rename from Binaries/esptool.exe rename to Firmware/Test Sketches/StayOn/esptool.exe diff --git a/Firmware/Test Sketches/StayOn/readme.md b/Firmware/Test Sketches/StayOn/readme.md new file mode 100644 index 000000000..daa9540de --- /dev/null +++ b/Firmware/Test Sketches/StayOn/readme.md @@ -0,0 +1,8 @@ +This folder contains a 'StayOn' firmware that forces the ESP32 into standby mode regardless of the rest of the system. This is useful on units that require troubleshooting or alternate firmware to be loaded. A USB cable must be connected between the RTK unit and the 'CONFIG ESP32' port. Run 'batch_program.bat COM4' where *COM4* is replaced with the COM port that RTK unit enumerated at. Once the batch file is running, press return to attempt to load the firmware. You will need to press and hold the power button on the unit then press enter to send new firmware while the ESP32 is awake. Once the firmware is loaded, you will no longer need to hold the power button. + +Once the firmware is loaded, the unit will not have a display, or respond to Bluetooth or do anything useful, other than stay available for a new firmware to be loaded. + +For those who want to run the CLI directly, use the following command: + +esptool.exe --chip esp32 --port COM4 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size detect 0x00 StayOn.bin + diff --git a/Firmware/Test Sketches/SurveyIn/Base.ino b/Firmware/Test Sketches/SurveyIn/Base.ino deleted file mode 100644 index 619fde8b6..000000000 --- a/Firmware/Test Sketches/SurveyIn/Base.ino +++ /dev/null @@ -1,152 +0,0 @@ - -//Wait for survey in to complete -bool updateSurveyInStatus() -{ - //delay(100); //Don't pound the I2C bus too hard - - bool response = myGPS.getSurveyStatus(2000); //Query module for SVIN status with 2000ms timeout (req can take a long time) - if (response == true) - { - if (myGPS.svin.valid == true) - { - Serial.println(F("Survey valid!")); - Serial.println(F("Base survey complete! RTCM now broadcasting.")); - baseState = BASE_TRANSMITTING; - - digitalWrite(baseStatusLED, HIGH); //Turn on LED - } - else - { - Serial.print(F("Time elapsed: ")); - Serial.print((String)myGPS.svin.observationTime); - - Serial.print(F(" Accuracy: ")); - Serial.print((String)myGPS.svin.meanAccuracy); - - byte SIV = myGPS.getSIV(); - Serial.print(F(" SIV: ")); - Serial.print(SIV); - - Serial.println(); - - if (myGPS.svin.meanAccuracy > 6.0) - baseState = BASE_SURVEYING_IN_SLOW; - else - baseState = BASE_SURVEYING_IN_FAST; - - if (myGPS.svin.observationTime > maxSurveyInWait_s) - { - Serial.println(F("Survey-In took more than 5 minutes. Restarting survey in.")); - - resetSurvey(); - - surveyIn(); - } - } - } - else - { - Serial.println(F("SVIN request failed")); - } - - //Update the Base LED accordingly - if (baseState == BASE_SURVEYING_IN_SLOW) - { - if (millis() - baseStateBlinkTime > 500) - { - baseStateBlinkTime += 500; - Serial.println(F("Slow blink")); - - if (digitalRead(baseStatusLED) == LOW) - digitalWrite(baseStatusLED, HIGH); - else - digitalWrite(baseStatusLED, LOW); - } - } - else if (baseState == BASE_SURVEYING_IN_FAST) - { - if (millis() - baseStateBlinkTime > 100) - { - baseStateBlinkTime += 100; - Serial.println(F("Fast blink")); - - if (digitalRead(baseStatusLED) == LOW) - digitalWrite(baseStatusLED, HIGH); - else - digitalWrite(baseStatusLED, LOW); - } - } -} - -//Configure specific aspects of the receiver for base mode -bool configureUbloxModuleBase() -{ - bool response = true; - - // Set dynamic model - if (myGPS.getDynamicModel() != DYN_MODEL_STATIONARY) - { - if (myGPS.setDynamicModel(DYN_MODEL_STATIONARY) == false) - Serial.println(F("Warning: setDynamicModel failed!")); - return (false); - } -} - -if (getRTCMSettings(UBX_RTCM_1005, COM_PORT_UART2) != 1) - response &= myGPS.enableRTCMmessage(UBX_RTCM_1005, COM_PORT_UART2, 1); //Enable message 1005 to output through UART2, message every second -if (getRTCMSettings(UBX_RTCM_1074, COM_PORT_UART2) != 1) - response &= myGPS.enableRTCMmessage(UBX_RTCM_1074, COM_PORT_UART2, 1); -if (getRTCMSettings(UBX_RTCM_1084, COM_PORT_UART2) != 1) - response &= myGPS.enableRTCMmessage(UBX_RTCM_1084, COM_PORT_UART2, 1); -if (getRTCMSettings(UBX_RTCM_1094, COM_PORT_UART2) != 1) - response &= myGPS.enableRTCMmessage(UBX_RTCM_1094, COM_PORT_UART2, 1); -if (getRTCMSettings(UBX_RTCM_1124, COM_PORT_UART2) != 1) - response &= myGPS.enableRTCMmessage(UBX_RTCM_1124, COM_PORT_UART2, 1); -if (getRTCMSettings(UBX_RTCM_1230, COM_PORT_UART2) != 10) - response &= myGPS.enableRTCMmessage(UBX_RTCM_1230, COM_PORT_UART2, 10); //Enable message every 10 seconds - -if (response == false) -{ - Serial.println(F("RTCM failed to enable.")); - return (false); -} - -return (response); -} - -//Start survey -//The ZED-F9P is slightly different than the NEO-M8P. See the Integration manual 3.5.8 for more info. -void surveyIn() -{ - boolean response = true; - - resetSurvey(); - - if (response == false) - { - Serial.println(F("RTCM failed to enable. Are you sure you have an ZED-F9P?")); - return; - } - Serial.println(F("RTCM messages enabled")); - - response = myGPS.enableSurveyMode(60, 5.000); //Enable Survey in, 60 seconds, 5.0m - if (response == false) - { - Serial.println(F("Survey start failed")); - return; - } - Serial.println(F("Survey started. This will run until 60s has passed and less than 5m accuracy is achieved.")); - - baseState = BASE_SURVEYING_IN_SLOW; -} - -void resetSurvey() -{ - //Slightly modified method for restarting survey-in from: https://portal.u-blox.com/s/question/0D52p00009IsVoMCAV/restarting-surveyin-on-an-f9p - bool response = myGPS.disableSurveyMode(); //Disable survey - delay(500); - response = myGPS.enableSurveyMode(1000, 400.000); //Enable Survey in with bogus values - delay(500); - response = myGPS.disableSurveyMode(); //Disable survey - delay(500); -} diff --git a/Firmware/Test Sketches/SurveyIn/Rover.ino b/Firmware/Test Sketches/SurveyIn/Rover.ino deleted file mode 100644 index 5f837f5b1..000000000 --- a/Firmware/Test Sketches/SurveyIn/Rover.ino +++ /dev/null @@ -1,96 +0,0 @@ - -//Based on position accuracy, update the green LEDs -bool updateRoverStatus() -{ - //We're in rover mode so update the accuracy LEDs - uint32_t accuracy = myGPS.getHorizontalAccuracy(); - - // Convert the horizontal accuracy (mm * 10^-1) to a float - float f_accuracy = accuracy; - // Now convert to m - f_accuracy = f_accuracy / 10000.0; // Convert from mm * 10^-1 to m - - Serial.print("Rover Accuracy (m): "); - Serial.print(f_accuracy, 4); // Print the accuracy with 4 decimal places - - if (f_accuracy <= 0.02) - { - Serial.print(" 0.02m LED"); - digitalWrite(positionAccuracyLED_20mm, HIGH); - digitalWrite(positionAccuracyLED_100mm, HIGH); - digitalWrite(positionAccuracyLED_1000mm, HIGH); - } - else if (f_accuracy <= 0.100) - { - Serial.print(" 0.1m LED"); - digitalWrite(positionAccuracyLED_20mm, LOW); - digitalWrite(positionAccuracyLED_100mm, HIGH); - digitalWrite(positionAccuracyLED_1000mm, HIGH); - } - else if (f_accuracy <= 1.0000) - { - Serial.print(" 1m LED"); - digitalWrite(positionAccuracyLED_20mm, LOW); - digitalWrite(positionAccuracyLED_100mm, LOW); - digitalWrite(positionAccuracyLED_1000mm, HIGH); - } - else if (f_accuracy > 1.0) - { - Serial.print(" No LEDs"); - digitalWrite(positionAccuracyLED_20mm, LOW); - digitalWrite(positionAccuracyLED_100mm, LOW); - digitalWrite(positionAccuracyLED_1000mm, LOW); - } - Serial.println(); -} - -//Configure specific aspects of the receiver for rover mode -bool configureUbloxModuleRover() -{ - bool response = myGPS.disableSurveyMode(); //Disable survey - - // Set dynamic model - if (myGPS.getDynamicModel() != DYN_MODEL_PORTABLE) - { - if (myGPS.setDynamicModel(DYN_MODEL_PORTABLE) == false) - { - Serial.println(F("Warning: setDynamicModel failed!")); - return (false); - } - } - - return (setNMEASettings()); -} - -//The Ublox library doesn't directly support NMEA configuration so let's do it manually -bool setNMEASettings() -{ - uint8_t customPayload[MAX_PAYLOAD_SIZE]; // This array holds the payload data bytes - ubxPacket customCfg = {0, 0, 0, 0, 0, customPayload, 0, 0, SFE_UBLOX_PACKET_VALIDITY_NOT_DEFINED, SFE_UBLOX_PACKET_VALIDITY_NOT_DEFINED}; - - customCfg.cls = UBX_CLASS_CFG; // This is the message Class - customCfg.id = UBX_CFG_NMEA; // This is the message ID - customCfg.len = 0; // Setting the len (length) to zero let's us poll the current settings - customCfg.startingSpot = 0; // Always set the startingSpot to zero (unless you really know what you are doing) - - uint16_t maxWait = 250; // Wait for up to 250ms (Serial may need a lot longer e.g. 1100) - - // Read the current setting. The results will be loaded into customCfg. - if (myGPS.sendCommand(&customCfg, maxWait) != SFE_UBLOX_STATUS_DATA_RECEIVED) // We are expecting data and an ACK - { - Serial.println(F("NMEA setting failed!")); - return (false); - } - - customPayload[3] |= (1 << 3); //Set the highPrec flag - - customPayload[8] = 1; //Enable extended satellite numbering - - // Now we write the custom packet back again to change the setting - if (myGPS.sendCommand(&customCfg, maxWait) != SFE_UBLOX_STATUS_DATA_SENT) // This time we are only expecting an ACK - { - Serial.println(F("NMEA setting failed!")); - return (false); - } - return (true); -} diff --git a/Firmware/Test Sketches/SurveyIn/SurveyIn.ino b/Firmware/Test Sketches/SurveyIn/SurveyIn.ino deleted file mode 100644 index 71208c020..000000000 --- a/Firmware/Test Sketches/SurveyIn/SurveyIn.ino +++ /dev/null @@ -1,163 +0,0 @@ -/* - When flipping from Rover to Base, we'll have to cold start. - How long does it take to survey in from cold start? - - 9-12-20: 174s - 1000+ is the antenna in a bad position? - Passed 5000. After a cold start, base after 782s. Perhaps we auto-cold start after 1000s (900s = 15 minutes). - Then 297. - Then 408. - 176. - 222 - 184 - - (Done) Create local function to read the port settings using the custom command so we can compare against it and only change settings - as required. This is an attempt to prevent the module from getting its I2C address overwritten - - (Done) Factory reset a device then make sure it configs correctly - -*/ - -#include //Needed for I2C to GPS - -#include "SparkFun_Ublox_Arduino_Library.h" //http://librarymanager/All#SparkFun_Ublox_GPS -SFE_UBLOX_GPS myGPS; - -typedef enum -{ - ERROR_NO_I2C = 2, -} t_errorNumber; - - -//Base status goes from Rover-Mode (LED off), surveying in (blinking), to survey is complete/trasmitting RTCM (solid) -typedef enum -{ - BASE_OFF = 0, - BASE_SURVEYING_IN_SLOW, - BASE_SURVEYING_IN_FAST, - BASE_TRANSMITTING, -} BaseState; -volatile BaseState baseState = BASE_OFF; -unsigned long baseStateBlinkTime = 0; -const unsigned long maxSurveyInWait_s = 60L * 5L; //Re-start survey-in after X seconds - -const int positionAccuracyLED_20mm = 2; //POSACC1 -const int positionAccuracyLED_100mm = 15; //POSACC2 -const int positionAccuracyLED_1000mm = 13; //POSACC3 -const int baseStatusLED = 4; -const int baseSwitch = 5; -const int bluetoothStatusLED = 12; - -void setup() -{ - Serial.begin(115200); - delay(2000); //Wait for USB port to show up - Serial.println(F("Ublox Base station example")); - - pinMode(positionAccuracyLED_20mm, OUTPUT); - pinMode(positionAccuracyLED_100mm, OUTPUT); - pinMode(positionAccuracyLED_1000mm, OUTPUT); - pinMode(baseStatusLED, OUTPUT); - pinMode(baseSwitch, INPUT_PULLUP); //HIGH = rover, LOW = base - - Wire.begin(); - - if (myGPS.begin() == false) //Connect to the Ublox module using Wire port - { - Serial.println(F("Ublox GPS not detected at default I2C address. Please check wiring. Freezing.")); - blinkError(ERROR_NO_I2C); - } - - myGPS.setI2COutput(COM_TYPE_UBX); //Set the I2C port to output UBX only (turn off NMEA noise) - - Serial.println(F("Set switch to 1 for Base, 0 for Rover")); - - //By default, module should be configured for rover but we'll check the switch during config - configureUbloxModule(); - - danceLEDs(); //Turn on LEDs like a car dashboard -} - -void loop() -{ - if (Serial.available()) - { - byte incoming = Serial.read(); - - if (incoming == 'x') - { - Serial.println(F("Restart Survey in")); - - resetSurvey(); - - surveyIn(); - } - else if (incoming == 'r') - { - //Configure for rover mode - Serial.println(F("Rover Mode")); - - baseState = BASE_OFF; - - //If we are survey'd in, but switch is rover then disable survey - if (configureUbloxModuleRover() == false) - { - Serial.println("Rover config failed!"); - } - - } - else if (incoming == 'b') - { - //Configure for base mode - Serial.println(F("Base Mode")); - - if (configureUbloxModuleBase() == false) - { - Serial.println("Base config failed!"); - } - - //Begin Survey in - surveyIn(); - - } - } - - //Check rover switch and configure module accordingly - //When switch is set to '1' = BASE, pin will be shorted to ground - if (digitalRead(baseSwitch) == HIGH && baseState != BASE_OFF) - { - //Configure for rover mode - Serial.println(F("Rover Mode")); - - baseState = BASE_OFF; - - //If we are survey'd in, but switch is rover then disable survey - if (configureUbloxModuleRover() == false) - { - Serial.println("Rover config failed!"); - } - } - else if (digitalRead(baseSwitch) == LOW && baseState == BASE_OFF) - { - //Configure for base mode - Serial.println(F("Base Mode")); - - if (configureUbloxModuleBase() == false) - { - Serial.println("Base config failed!"); - } - - //Begin Survey in - surveyIn(); - } - - if (baseState == BASE_SURVEYING_IN_SLOW || baseState == BASE_SURVEYING_IN_FAST) - { - updateSurveyInStatus(); - - } - else if (baseState == BASE_OFF) - { - updateRoverStatus(); - } -} diff --git a/Firmware/Test Sketches/SurveyIn/System.ino b/Firmware/Test Sketches/SurveyIn/System.ino deleted file mode 100644 index f9bf0b13e..000000000 --- a/Firmware/Test Sketches/SurveyIn/System.ino +++ /dev/null @@ -1,277 +0,0 @@ - -uint8_t settingPayload[MAX_PAYLOAD_SIZE]; // This array holds the payload data bytes - -//Setup the Ublox module for any setup (base or rover) -//In general we check if the setting is incorrect before writing it. Otherwise, the set commands have, on rare occasion, become -//corrupt. The worst is when the I2C port gets turned off or the I2C address gets borked. We should only have to configure -//a fresh Ublox module once and never again. -bool configureUbloxModule() -{ - boolean response = true; - -#define OUTPUT_SETTING 14 -#define INPUT_SETTING 12 - - //UART1 will primarily be used to pass NMEA from ZED to ESP32/Cell phone but the phone - //can also provide RTCM data. So let's be sure to enable RTCM on UART1 input. - - getPortSettings(COM_PORT_UART1); //Load the settingPayload with this port's settings - if (settingPayload[OUTPUT_SETTING] != COM_TYPE_NMEA || settingPayload[INPUT_SETTING] != COM_TYPE_RTCM3) - { - Serial.println("Updating UART1 configuration"); - response &= myGPS.setPortOutput(COM_PORT_UART1, COM_TYPE_NMEA); //Set the UART1 to output NMEA - response &= myGPS.setPortInput(COM_PORT_UART1, COM_TYPE_RTCM3); //Set the UART1 to input RTCM - } - - //Disable SPI port - This is just to remove some overhead by ZED - getPortSettings(COM_PORT_SPI); //Load the settingPayload with this port's settings - if (settingPayload[OUTPUT_SETTING] != 0 || settingPayload[INPUT_SETTING] != 0) - { - Serial.println("Updating SPI configuration"); - response &= myGPS.setPortOutput(COM_PORT_SPI, 0); //Disable all protocols - response &= myGPS.setPortInput(COM_PORT_SPI, 0); //Disable all protocols - } - - getPortSettings(COM_PORT_UART2); //Load the settingPayload with this port's settings - if (settingPayload[OUTPUT_SETTING] != COM_TYPE_RTCM3 || settingPayload[INPUT_SETTING] != COM_TYPE_RTCM3) - { - response &= myGPS.setPortOutput(COM_PORT_UART2, COM_TYPE_RTCM3); //Set the UART2 to output RTCM (in case this device goes into base mode) - response &= myGPS.setPortInput(COM_PORT_UART2, COM_TYPE_RTCM3); //Set the UART2 to input RTCM - } - - getPortSettings(COM_PORT_I2C); //Load the settingPayload with this port's settings - if (settingPayload[OUTPUT_SETTING] != COM_TYPE_UBX || settingPayload[INPUT_SETTING] != COM_TYPE_UBX) - { - response &= myGPS.setPortOutput(COM_PORT_I2C, COM_TYPE_UBX); //Set the I2C port to output UBX only (turn off NMEA noise) - response &= myGPS.setPortInput(COM_PORT_I2C, COM_TYPE_UBX); //Set the I2C port to output UBX only (turn off NMEA noise) - } - - //myGPS.setNavigationFrequency(10); //Set output to 10 times a second - if (myGPS.getNavigationFrequency() != 4) - { - response &= myGPS.setNavigationFrequency(4); //Set output in Hz - } - - //Make sure the appropriate sentences are enabled - if (getNMEASettings(UBX_NMEA_GGA, COM_PORT_UART1) != 1) - response &= myGPS.enableNMEAMessage(UBX_NMEA_GGA, COM_PORT_UART1); - if (getNMEASettings(UBX_NMEA_GSA, COM_PORT_UART1) != 1) - response &= myGPS.enableNMEAMessage(UBX_NMEA_GSA, COM_PORT_UART1); - if (getNMEASettings(UBX_NMEA_GSV, COM_PORT_UART1) != 1) - response &= myGPS.enableNMEAMessage(UBX_NMEA_GSV, COM_PORT_UART1); - if (getNMEASettings(UBX_NMEA_RMC, COM_PORT_UART1) != 1) - response &= myGPS.enableNMEAMessage(UBX_NMEA_RMC, COM_PORT_UART1); - if (getNMEASettings(UBX_NMEA_GST, COM_PORT_UART1) != 1) - response &= myGPS.enableNMEAMessage(UBX_NMEA_GST, COM_PORT_UART1); - - response &= myGPS.setAutoPVT(true); //Tell the GPS to "send" each solution - - if (getSerialRate(COM_PORT_UART1) != 115200) - { - Serial.println("Updating UART1 rate"); - myGPS.setSerialRate(115200, COM_PORT_UART1); //Set UART1 to 115200 - } - if (getSerialRate(COM_PORT_UART2) != 57600) - { - Serial.println("Updating UART2 rate"); - myGPS.setSerialRate(57600, COM_PORT_UART2); //Set UART2 to 57600 to match SiK firmware default - } - - if (response == false) - { - Serial.println(F("Module failed initial config.")); - return (false); - } - - //Check rover switch and configure module accordingly - //When switch is set to '1' = BASE, pin will be shorted to ground - if (digitalRead(baseSwitch) == HIGH) - { - //Configure for rover mode - if (configureUbloxModuleRover() == false) - { - Serial.println("Rover config failed!"); - return (false); - } - } - else - { - //Configure for base mode - if (configureUbloxModuleBase() == false) - { - Serial.println("Base config failed!"); - return (false); - } - } - - response &= myGPS.saveConfiguration(); //Save the current settings to flash and BBR - - if (response == false) - { - Serial.println(F("Module failed to save.")); - return (false); - } - - return (true); -} - -//Given a portID, load the settings associated -bool getPortSettings(uint8_t portID) -{ - ubxPacket customCfg = {0, 0, 0, 0, 0, settingPayload, 0, 0, SFE_UBLOX_PACKET_VALIDITY_NOT_DEFINED, SFE_UBLOX_PACKET_VALIDITY_NOT_DEFINED}; - - customCfg.cls = UBX_CLASS_CFG; // This is the message Class - customCfg.id = UBX_CFG_PRT; // This is the message ID - customCfg.len = 1; - customCfg.startingSpot = 0; // Always set the startingSpot to zero (unless you really know what you are doing) - - uint16_t maxWait = 250; // Wait for up to 250ms (Serial may need a lot longer e.g. 1100) - - settingPayload[0] = portID; //Request the caller's portID from GPS module - - // Read the current setting. The results will be loaded into customCfg. - if (myGPS.sendCommand(&customCfg, maxWait) != SFE_UBLOX_STATUS_DATA_RECEIVED) // We are expecting data and an ACK - { - Serial.println(F("getPortSettings failed!")); - return (false); - } - - return (true); -} - -//Given a portID and a NMEA message type, load the settings associated -uint8_t getNMEASettings(uint8_t msgID, uint8_t portID) -{ - ubxPacket customCfg = {0, 0, 0, 0, 0, settingPayload, 0, 0, SFE_UBLOX_PACKET_VALIDITY_NOT_DEFINED, SFE_UBLOX_PACKET_VALIDITY_NOT_DEFINED}; - - customCfg.cls = UBX_CLASS_CFG; // This is the message Class - customCfg.id = UBX_CFG_MSG; // This is the message ID - customCfg.len = 2; - customCfg.startingSpot = 0; // Always set the startingSpot to zero (unless you really know what you are doing) - - uint16_t maxWait = 250; // Wait for up to 250ms (Serial may need a lot longer e.g. 1100) - - settingPayload[0] = UBX_CLASS_NMEA; - settingPayload[1] = msgID; - - // Read the current setting. The results will be loaded into customCfg. - if (myGPS.sendCommand(&customCfg, maxWait) != SFE_UBLOX_STATUS_DATA_RECEIVED) // We are expecting data and an ACK - { - Serial.println(F("getNMEASettings failed!")); - return (false); - } - - return (settingPayload[2 + portID]); //Return just the byte associated with this portID -} - -//Given a portID and a RTCM message type, load the settings associated -uint8_t getRTCMSettings(uint8_t msgID, uint8_t portID) -{ - ubxPacket customCfg = {0, 0, 0, 0, 0, settingPayload, 0, 0, SFE_UBLOX_PACKET_VALIDITY_NOT_DEFINED, SFE_UBLOX_PACKET_VALIDITY_NOT_DEFINED}; - - customCfg.cls = UBX_CLASS_CFG; // This is the message Class - customCfg.id = UBX_CFG_MSG; // This is the message ID - customCfg.len = 2; - customCfg.startingSpot = 0; // Always set the startingSpot to zero (unless you really know what you are doing) - - uint16_t maxWait = 250; // Wait for up to 250ms (Serial may need a lot longer e.g. 1100) - - settingPayload[0] = UBX_RTCM_MSB; - settingPayload[1] = msgID; - - // Read the current setting. The results will be loaded into customCfg. - if (myGPS.sendCommand(&customCfg, maxWait) != SFE_UBLOX_STATUS_DATA_RECEIVED) // We are expecting data and an ACK - { - Serial.println(F("getRTCMSettings failed!")); - return (false); - } - - return (settingPayload[2 + portID]); //Return just the byte associated with this portID -} - -//Given a portID and a NMEA message type, load the settings associated -uint32_t getSerialRate(uint8_t portID) -{ - ubxPacket customCfg = {0, 0, 0, 0, 0, settingPayload, 0, 0, SFE_UBLOX_PACKET_VALIDITY_NOT_DEFINED, SFE_UBLOX_PACKET_VALIDITY_NOT_DEFINED}; - - customCfg.cls = UBX_CLASS_CFG; // This is the message Class - customCfg.id = UBX_CFG_PRT; // This is the message ID - customCfg.len = 1; - customCfg.startingSpot = 0; // Always set the startingSpot to zero (unless you really know what you are doing) - - uint16_t maxWait = 250; // Wait for up to 250ms (Serial may need a lot longer e.g. 1100) - - settingPayload[0] = portID; - - // Read the current setting. The results will be loaded into customCfg. - if (myGPS.sendCommand(&customCfg, maxWait) != SFE_UBLOX_STATUS_DATA_RECEIVED) // We are expecting data and an ACK - { - Serial.println(F("getSerialRate failed!")); - return (false); - } - - return (((uint32_t)settingPayload[10] << 16) | ((uint32_t)settingPayload[9] << 8) | settingPayload[8]); -} - -//Freeze displaying a given error code -void blinkError(t_errorNumber errorNumber) -{ - while (1) - { - for (int x = 0 ; x < errorNumber ; x++) - { - digitalWrite(positionAccuracyLED_20mm, HIGH); - digitalWrite(positionAccuracyLED_100mm, HIGH); - digitalWrite(positionAccuracyLED_1000mm, HIGH); - digitalWrite(baseStatusLED, HIGH); - digitalWrite(bluetoothStatusLED, HIGH); - delay(200); - digitalWrite(positionAccuracyLED_20mm, LOW); - digitalWrite(positionAccuracyLED_100mm, LOW); - digitalWrite(positionAccuracyLED_1000mm, LOW); - digitalWrite(baseStatusLED, LOW); - digitalWrite(bluetoothStatusLED, LOW); - delay(200); - } - - delay(2000); - } -} - -//Turn on indicator LEDs to verify LED function and indicate setup sucess -void danceLEDs() -{ - for (int x = 0 ; x < 2 ; x++) - { - digitalWrite(positionAccuracyLED_20mm, HIGH); - digitalWrite(positionAccuracyLED_100mm, HIGH); - digitalWrite(positionAccuracyLED_1000mm, HIGH); - digitalWrite(baseStatusLED, HIGH); - digitalWrite(bluetoothStatusLED, HIGH); - delay(100); - digitalWrite(positionAccuracyLED_20mm, LOW); - digitalWrite(positionAccuracyLED_100mm, LOW); - digitalWrite(positionAccuracyLED_1000mm, LOW); - digitalWrite(baseStatusLED, LOW); - digitalWrite(bluetoothStatusLED, LOW); - delay(100); - } - - digitalWrite(positionAccuracyLED_20mm, HIGH); - digitalWrite(positionAccuracyLED_100mm, HIGH); - digitalWrite(positionAccuracyLED_1000mm, HIGH); - digitalWrite(baseStatusLED, HIGH); - digitalWrite(bluetoothStatusLED, HIGH); - - delay(250); - digitalWrite(positionAccuracyLED_20mm, LOW); - delay(250); - digitalWrite(positionAccuracyLED_100mm, LOW); - delay(250); - digitalWrite(positionAccuracyLED_1000mm, LOW); - - delay(250); - digitalWrite(baseStatusLED, LOW); - delay(250); - digitalWrite(bluetoothStatusLED, LOW); -} diff --git a/Firmware/Test Sketches/System_Check/Begin.ino b/Firmware/Test Sketches/System_Check/Begin.ino new file mode 100644 index 000000000..0073adaee --- /dev/null +++ b/Firmware/Test Sketches/System_Check/Begin.ino @@ -0,0 +1,505 @@ + +#define MAX_ADC_VOLTAGE 3300 // Millivolts + +// Testing shows the combined ADC+resistors is under a 1% window +#define TOLERANCE 5.20 // Percent: 94.8% - 105.2% + +//---------------------------------------- +// Hardware initialization functions +//---------------------------------------- +// Determine if the measured value matches the product ID value +// idWithAdc applies resistor tolerance using worst-case tolerances: +// Upper threshold: R1 down by TOLERANCE, R2 up by TOLERANCE +// Lower threshold: R1 up by TOLERANCE, R2 down by TOLERANCE +bool idWithAdc(uint16_t mvMeasured, float r1, float r2) +{ + float lowerThreshold; + float upperThreshold; + + // ADC input + // r1 KOhms | r2 KOhms + // MAX_ADC_VOLTAGE -----/\/\/\/\-----+-----/\/\/\/\----- Ground + + // Return true if the mvMeasured value is within the tolerance range + // of the mvProduct value + upperThreshold = ceil(MAX_ADC_VOLTAGE * (r2 * (1.0 + (TOLERANCE / 100.0))) / + ((r1 * (1.0 - (TOLERANCE / 100.0))) + (r2 * (1.0 + (TOLERANCE / 100.0))))); + lowerThreshold = floor(MAX_ADC_VOLTAGE * (r2 * (1.0 - (TOLERANCE / 100.0))) / + ((r1 * (1.0 + (TOLERANCE / 100.0))) + (r2 * (1.0 - (TOLERANCE / 100.0))))); + + // Serial.printf("r1: %0.2f r2: %0.2f lowerThreshold: %0.0f mvMeasured: %d upperThreshold: %0.0f\r\n", r1, r2, + // lowerThreshold, mvMeasured, upperThreshold); + + return (upperThreshold > mvMeasured) && (mvMeasured > lowerThreshold); +} + +// Use a pair of resistors on pin 35 to ID the board type +// If the ID resistors are not available then use a variety of other methods +// (I2C, GPIO test, etc) to ID the board. +// Assume no hardware interfaces have been started so we need to start/stop any hardware +// used in tests accordingly. +void identifyBoard() +{ + // Use ADC to check the resistor divider + int pin_deviceID = 35; + uint16_t idValue = analogReadMilliVolts(pin_deviceID); + Serial.printf("Board ADC ID (mV): %d\r\n", idValue); + + // Order the following ID checks, by millivolt values high to low + + // Facet L-Band Direct: 4.7/1 --> 534mV < 579mV < 626mV + if (idWithAdc(idValue, 4.7, 1)) + { + Serial.println("Found LBand Direct"); + productVariant = RTK_FACET_LBAND_DIRECT; + } + + // Express: 10/3.3 --> 761mV < 819mV < 879mV + else if (idWithAdc(idValue, 10, 3.3)) + { + Serial.println("Found Express"); + productVariant = RTK_EXPRESS; + } + + // Reference Station: 20/10 --> 1031mV < 1100mV < 1171mV + else if (idWithAdc(idValue, 20, 10)) + { + productVariant = REFERENCE_STATION; + // We can't auto-detect the ZED version if the firmware is in configViaEthernet mode, + // so fake it here - otherwise messageSupported always returns false + zedFirmwareVersionInt = 112; + } + // Facet: 10/10 --> 1571mV < 1650mV < 1729mV + else if (idWithAdc(idValue, 10, 10)) + productVariant = RTK_FACET; + + // Facet L-Band: 10/20 --> 2129mV < 2200mV < 2269mV + else if (idWithAdc(idValue, 10, 20)) + productVariant = RTK_FACET_LBAND; + + // Express+: 3.3/10 --> 2421mV < 2481mV < 2539mV + else if (idWithAdc(idValue, 3.3, 10)) + productVariant = RTK_EXPRESS_PLUS; + + // ID resistors do not exist for the following: + // Surveyor + // Unknown + else + { + Serial.println("Out of band or nonexistent resistor IDs"); + productVariant = RTK_UNKNOWN; // Need to wait until the GNSS and Accel have been initialized + } +} + +void beginBoard() +{ + if (productVariant == RTK_UNKNOWN) + { + if (isConnected(0x19) == true) // Check for accelerometer + { + if (zedModuleType == PLATFORM_F9P) + productVariant = RTK_EXPRESS; + else if (zedModuleType == PLATFORM_F9R) + productVariant = RTK_EXPRESS_PLUS; + } + else + { + // Detect RTK Expresses (v1.3 and below) that do not have an accel or device ID resistors + + // On a Surveyor, pin 34 is not connected. On Express, 34 is connected to ZED_TX_READY + const int pin_ZedTxReady = 34; + uint16_t pinValue = analogReadMilliVolts(pin_ZedTxReady); + log_d("Alternate ID pinValue (mV): %d\r\n", pinValue); // Surveyor = 142 to 152, //Express = 3129 + if (pinValue > 3000) + { + if (zedModuleType == PLATFORM_F9P) + productVariant = RTK_EXPRESS; + else if (zedModuleType == PLATFORM_F9R) + productVariant = RTK_EXPRESS_PLUS; + } + else + productVariant = RTK_SURVEYOR; + } + } + + // Setup hardware pins + if (productVariant == RTK_SURVEYOR) + { + pin_batteryLevelLED_Red = 32; + pin_batteryLevelLED_Green = 33; + pin_positionAccuracyLED_1cm = 2; + pin_positionAccuracyLED_10cm = 15; + pin_positionAccuracyLED_100cm = 13; + pin_baseStatusLED = 4; + pin_bluetoothStatusLED = 12; + pin_setupButton = 5; + pin_microSD_CS = 25; + pin_zed_tx_ready = 26; + pin_zed_reset = 27; + pin_batteryLevel_alert = 36; + + // Bug in ZED-F9P v1.13 firmware causes RTK LED to not light when RTK Floating with SBAS on. + // The following changes the POR default but will be overwritten by settings in NVM or settings file + settings.ubxConstellations[1].enabled = false; + } + else if (productVariant == RTK_EXPRESS || productVariant == RTK_EXPRESS_PLUS) + { + pin_muxA = 2; + pin_muxB = 4; + pin_powerSenseAndControl = 13; + pin_setupButton = 14; + pin_microSD_CS = 25; + pin_dac26 = 26; + pin_powerFastOff = 27; + pin_adc39 = 39; + + pinMode(pin_powerSenseAndControl, INPUT_PULLUP); + pinMode(pin_powerFastOff, INPUT); + + // if (esp_reset_reason() == ESP_RST_POWERON) + // { + // powerOnCheck(); // Only do check if we POR start + // } + + pinMode(pin_setupButton, INPUT_PULLUP); + + //setMuxport(settings.dataPortChannel); // Set mux to user's choice: NMEA, I2C, PPS, or DAC + } + else if (productVariant == RTK_FACET || productVariant == RTK_FACET_LBAND || + productVariant == RTK_FACET_LBAND_DIRECT) + { + // v11 + pin_muxA = 2; + pin_muxB = 0; + pin_powerSenseAndControl = 13; + pin_peripheralPowerControl = 14; + pin_microSD_CS = 25; + pin_dac26 = 26; + pin_powerFastOff = 27; + pin_adc39 = 39; + + pin_radio_rx = 33; + pin_radio_tx = 32; + pin_radio_rst = 15; + pin_radio_pwr = 4; + pin_radio_cts = 5; + // pin_radio_rts = 255; //Not implemented + + pinMode(pin_powerSenseAndControl, INPUT_PULLUP); + pinMode(pin_powerFastOff, INPUT); + + // if (esp_reset_reason() == ESP_RST_POWERON) + // { + // powerOnCheck(); // Only do check if we POR start + // } + + pinMode(pin_peripheralPowerControl, OUTPUT); + digitalWrite(pin_peripheralPowerControl, HIGH); // Turn on SD, ZED, etc + + //setMuxport(settings.dataPortChannel); // Set mux to user's choice: NMEA, I2C, PPS, or DAC + + // CTS is active low. ESP32 pin 5 has pullup at POR. We must drive it low. + pinMode(pin_radio_cts, OUTPUT); + digitalWrite(pin_radio_cts, LOW); + + if (productVariant == RTK_FACET_LBAND_DIRECT) + { + // Override the default setting if a user has not explicitly configured the setting + // if (settings.useI2cForLbandCorrectionsConfigured == false) + // settings.useI2cForLbandCorrections = false; + } + } + else if (productVariant == REFERENCE_STATION) + { + // No powerOnCheck + + //settings.enablePrintBatteryMessages = false; // No pesky battery messages + } + + char versionString[21]; + getFirmwareVersion(versionString, sizeof(versionString), true); + Serial.printf("SparkFun RTK %s %s\r\n", platformPrefix, versionString); + + // Get unit MAC address + esp_read_mac(wifiMACAddress, ESP_MAC_WIFI_STA); + memcpy(btMACAddress, wifiMACAddress, sizeof(wifiMACAddress)); + btMACAddress[5] += + 2; // Convert MAC address to Bluetooth MAC (add 2): + // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/system.html#mac-address + memcpy(ethernetMACAddress, wifiMACAddress, sizeof(wifiMACAddress)); + ethernetMACAddress[5] += 3; // Convert MAC address to Ethernet MAC (add 3) + + // For all boards, check reset reason. If reset was due to wdt or panic, append last log + //loadSettingsPartial(); // Loads settings from LFS + if ((esp_reset_reason() == ESP_RST_POWERON) || (esp_reset_reason() == ESP_RST_SW)) + { + //reuseLastLog = false; // Start new log + + if (settings.enableResetDisplay == true) + { + settings.resetCount = 0; + // recordSystemSettingsToFileLFS(settingsFileName); // Avoid overwriting LittleFS settings onto SD + } + settings.resetCount = 0; + } + else + { + //reuseLastLog = true; // Attempt to reuse previous log + + if (settings.enableResetDisplay == true) + { + settings.resetCount++; + Serial.printf("resetCount: %d\r\n", settings.resetCount); + //recordSystemSettingsToFileLFS(settingsFileName); // Avoid overwriting LittleFS settings onto SD + } + + Serial.print("Reset reason: "); + switch (esp_reset_reason()) + { + case ESP_RST_UNKNOWN: + Serial.println("ESP_RST_UNKNOWN"); + break; + case ESP_RST_POWERON: + Serial.println("ESP_RST_POWERON"); + break; + case ESP_RST_SW: + Serial.println("ESP_RST_SW"); + break; + case ESP_RST_PANIC: + Serial.println("ESP_RST_PANIC"); + break; + case ESP_RST_INT_WDT: + Serial.println("ESP_RST_INT_WDT"); + break; + case ESP_RST_TASK_WDT: + Serial.println("ESP_RST_TASK_WDT"); + break; + case ESP_RST_WDT: + Serial.println("ESP_RST_WDT"); + break; + case ESP_RST_DEEPSLEEP: + Serial.println("ESP_RST_DEEPSLEEP"); + break; + case ESP_RST_BROWNOUT: + Serial.println("ESP_RST_BROWNOUT"); + break; + case ESP_RST_SDIO: + Serial.println("ESP_RST_SDIO"); + break; + default: + Serial.println("Unknown"); + } + } +} + + +//Connect to ZED module and identify particulars +void beginGNSS() +{ + if (theGNSS.begin() == false) + { + log_d("GNSS Failed to begin. Trying again."); + + //Try again with power on delay + delay(1000); //Wait for ZED-F9P to power up before it can respond to ACK + if (theGNSS.begin() == false) + { + //displayGNSSFail(1000); + online.gnss = false; + return; + } + } + + //Increase transactions to reduce transfer time + theGNSS.i2cTransactionSize = 128; + + //Auto-send Valset messages before the buffer is completely full + theGNSS.autoSendCfgValsetAtSpaceRemaining(16); + + //Check the firmware version of the ZED-F9P. Based on Example21_ModuleInfo. + if (theGNSS.getModuleInfo(1100) == true) //Try to get the module info + { + //Reconstruct the firmware version + snprintf(zedFirmwareVersion, sizeof(zedFirmwareVersion), "%s %d.%02d", theGNSS.getFirmwareType(), theGNSS.getFirmwareVersionHigh(), theGNSS.getFirmwareVersionLow()); + + //Construct the firmware version as uint8_t. Note: will fail above 2.55! + zedFirmwareVersionInt = (theGNSS.getFirmwareVersionHigh() * 100) + theGNSS.getFirmwareVersionLow(); + + //Check this is known firmware + //"1.20" - Mostly for F9R HPS 1.20, but also F9P HPG v1.20 + //"1.21" - F9R HPS v1.21 + //"1.30" - ZED-F9P (HPG) released Dec, 2021. Also ZED-F9R (HPS) released Sept, 2022 + //"1.32" - ZED-F9P released May, 2022 + + const uint8_t knownFirmwareVersions[] = { 100, 112, 113, 120, 121, 130, 132 }; + bool knownFirmware = false; + for (uint8_t i = 0; i < (sizeof(knownFirmwareVersions) / sizeof(uint8_t)); i++) + { + if (zedFirmwareVersionInt == knownFirmwareVersions[i]) + knownFirmware = true; + } + + if (!knownFirmware) + { + Serial.printf("Unknown firmware version: %s\r\n", zedFirmwareVersion); + zedFirmwareVersionInt = 99; //0.99 invalid firmware version + } + + //Determine if we have a ZED-F9P (Express/Facet) or an ZED-F9R (Express Plus/Facet Plus) + if (strstr(theGNSS.getModuleName(), "ZED-F9P") != nullptr) + zedModuleType = PLATFORM_F9P; + else if (strstr(theGNSS.getModuleName(), "ZED-F9R") != nullptr) + zedModuleType = PLATFORM_F9R; + else + { + Serial.printf("Unknown ZED module: %s\r\n", theGNSS.getModuleName()); + zedModuleType = PLATFORM_F9P; + } + + printZEDInfo(); //Print module type and firmware version + } + + online.gnss = true; +} + +//Configure the on board MAX17048 fuel gauge +void beginFuelGauge() +{ + // Set up the MAX17048 LiPo fuel gauge + if (lipo.begin() == false) + { + Serial.println(F("MAX17048 not detected. Continuing.")); + return; + } + + //Always use hibernate mode + if (lipo.getHIBRTActThr() < 0xFF) lipo.setHIBRTActThr((uint8_t)0xFF); + if (lipo.getHIBRTHibThr() < 0xFF) lipo.setHIBRTHibThr((uint8_t)0xFF); + + Serial.println(F("MAX17048 configuration complete")); + + online.battery = true; +} + +void beginSD() +{ + bool gotSemaphore; + + online.microSD = false; + gotSemaphore = false; + while (settings.enableSD == true) + { + //Setup SD card access semaphore + if (sdCardSemaphore == NULL) + sdCardSemaphore = xSemaphoreCreateMutex(); + else if (xSemaphoreTake(sdCardSemaphore, fatSemaphore_shortWait_ms) != pdPASS) + { + //This is OK since a retry will occur next loop + log_d("sdCardSemaphore failed to yield, Begin.ino line %d\r\n", __LINE__); + break; + } + gotSemaphore = true; + + pinMode(pin_microSD_CS, OUTPUT); + digitalWrite(pin_microSD_CS, HIGH); //Be sure SD is deselected + + //Allocate the data structure that manages the microSD card + if (!sd) + { + sd = new SdFat(); + if (!sd) + { + log_d("Failed to allocate the SdFat structure!"); + break; + } + } + + //Do a quick test to see if a card is present + int tries = 0; + int maxTries = 5; + while (tries < maxTries) + { + if (sdPresent() == true) break; + log_d("SD present failed. Trying again %d out of %d", tries + 1, maxTries); + + //Max power up time is 250ms: https://www.kingston.com/datasheets/SDCIT-specsheet-64gb_en.pdf + //Max current is 200mA average across 1s, peak 300mA + delay(10); + tries++; + } + if (tries == maxTries) break; + + //If an SD card is present, allow SdFat to take over + log_d("SD card detected"); + + if (settings.spiFrequency > 16) + { + Serial.println("Error: SPI Frequency out of range. Default to 16MHz"); + settings.spiFrequency = 16; + } + + if (sd->begin(SdSpiConfig(pin_microSD_CS, SHARED_SPI, SD_SCK_MHZ(settings.spiFrequency))) == false) + { + tries = 0; + maxTries = 1; + for ( ; tries < maxTries ; tries++) + { + log_d("SD init failed. Trying again %d out of %d", tries + 1, maxTries); + + delay(250); //Give SD more time to power up, then try again + if (sd->begin(SdSpiConfig(pin_microSD_CS, SHARED_SPI, SD_SCK_MHZ(settings.spiFrequency))) == true) break; + } + + if (tries == maxTries) + { + Serial.println(F("SD init failed. Is card present? Formatted?")); + digitalWrite(pin_microSD_CS, HIGH); //Be sure SD is deselected + break; + } + } + + //Change to root directory. All new file creation will be in root. + if (sd->chdir() == false) + { + Serial.println(F("SD change directory failed")); + break; + } + + // if (createTestFile() == false) + // { + // Serial.println(F("Failed to create test file. Format SD card with 'SD Card Formatter'.")); + // displaySDFail(5000); + // break; + // } + // + // //Load firmware file from the microSD card if it is present + // scanForFirmware(); + + Serial.println(F("microSD: Online")); + online.microSD = true; + break; + } + + //Free the semaphore + if (sdCardSemaphore && gotSemaphore) + xSemaphoreGive(sdCardSemaphore); //Make the file system available for use +} + +//Begin accelerometer if available +void beginAccelerometer() +{ + if (accel.begin() == false) + { + online.accelerometer = false; + + return; + } + + //The larger the avgAmount the faster we should read the sensor + //accel.setDataRate(LIS2DH12_ODR_100Hz); //6 measurements a second + accel.setDataRate(LIS2DH12_ODR_400Hz); //25 measurements a second + + Serial.println("Accelerometer configuration complete"); + + online.accelerometer = true; +} diff --git a/Firmware/Test Sketches/System_Check/Buttons.ino b/Firmware/Test Sketches/System_Check/Buttons.ino new file mode 100644 index 000000000..5eb051769 --- /dev/null +++ b/Firmware/Test Sketches/System_Check/Buttons.ino @@ -0,0 +1,25 @@ +//If we have a power button tap, or if the display is not yet started (no I2C!) +//then don't display a shutdown screen +void powerDown(bool displayInfo) +{ + //Disable SD card use + //endSD(false, false); + + //Prevent other tasks from logging, even if access to the microSD card was denied + online.logging = false; + + if (displayInfo == true) + { + //displayShutdown(); + //delay(2000); + } + + pinMode(pin_powerSenseAndControl, OUTPUT); + digitalWrite(pin_powerSenseAndControl, LOW); + + pinMode(pin_powerFastOff, OUTPUT); + digitalWrite(pin_powerFastOff, LOW); + + while (1) + delay(1); +} diff --git a/Firmware/Test Sketches/System_Check/Display.ino b/Firmware/Test Sketches/System_Check/Display.ino new file mode 100644 index 000000000..c2aae7d75 --- /dev/null +++ b/Firmware/Test Sketches/System_Check/Display.ino @@ -0,0 +1,75 @@ +static uint32_t blinking_icons; +static uint32_t icons; + +void beginDisplay() +{ + blinking_icons = 0; + if (oled.begin() == true) + { + online.display = true; + + Serial.println(F("Display started")); + //displaySplash(); + splashStart = millis(); + } + else + { + if (productVariant == RTK_SURVEYOR) + { + Serial.println(F("Display not detected")); + } + else if (productVariant == RTK_EXPRESS || productVariant == RTK_EXPRESS_PLUS || productVariant == RTK_FACET || productVariant == RTK_FACET_LBAND) + { + Serial.println(F("Display Error: Not detected.")); + } + } +} + +void displayHelloWorld() +{ + if (online.display == true) + { + oled.erase(); + + uint8_t fontHeight = 15; + uint8_t yPos = oled.getHeight() / 2 - fontHeight; + + printTextCenter("Hello", yPos, QW_FONT_8X16, 1, false); //text, y, font type, kerning, inverted + printTextCenter("World", yPos + fontHeight, QW_FONT_8X16, 1, false); //text, y, font type, kerning, inverted + + oled.display(); + } +} + +//Given text, and location, print text center of the screen +void printTextCenter(const char *text, uint8_t yPos, QwiicFont & fontType, uint8_t kerning, bool highlight) //text, y, font type, kearning, inverted +{ + oled.setFont(fontType); + oled.setDrawMode(grROPXOR); + + uint8_t fontWidth = fontType.width; + if (fontWidth == 8) fontWidth = 7; //8x16, but widest character is only 7 pixels. + + uint8_t xStart = (oled.getWidth() / 2) - ((strlen(text) * (fontWidth + kerning)) / 2) + 1; + + uint8_t xPos = xStart; + for (int x = 0 ; x < strlen(text) ; x++) + { + oled.setCursor(xPos, yPos); + oled.print(text[x]); + xPos += fontWidth + kerning; + } + + if (highlight) //Draw a box, inverted over text + { + uint8_t textPixelWidth = strlen(text) * (fontWidth + kerning); + + //Error check + int xBoxStart = xStart - 5; + if (xBoxStart < 0) xBoxStart = 0; + int xBoxEnd = textPixelWidth + 9; + if (xBoxEnd > oled.getWidth() - 1) xBoxEnd = oled.getWidth() - 1; + + oled.rectangleFill(xBoxStart, yPos, xBoxEnd, 12, 1); //x, y, width, height, color + } +} diff --git a/Firmware/Test Sketches/System_Check/SD.ino b/Firmware/Test Sketches/System_Check/SD.ino new file mode 100644 index 000000000..31a0d935c --- /dev/null +++ b/Firmware/Test Sketches/System_Check/SD.ino @@ -0,0 +1,150 @@ +/* + These are low level functions to aid in detecting whether a card is present or not. + Because of ESP32 v2 core, SdFat can only operate using Shared SPI. This makes the sd->begin test take over 1s + which causes the RTK product to boot slowly. To circumvent this, we will ping the SD card directly to see if it responds. + Failures take 2ms, successes take 1ms. + + From Prototype puzzle: https://github.com/sparkfunX/ThePrototype/blob/master/Firmware/TestSketches/sdLocker/sdLocker.ino + License: Public domain. This code is based on Karl Lunt's work: https://www.seanet.com/~karllunt/sdlocker2.html +*/ + +//Define commands for the SD card +#define SD_GO_IDLE (0x40 + 0) // CMD0 - go to idle state +#define SD_INIT (0x40 + 1) // CMD1 - start initialization +#define SD_SEND_IF_COND (0x40 + 8) // CMD8 - send interface (conditional), works for SDHC only +#define SD_SEND_STATUS (0x40 + 13) // CMD13 - send card status +#define SD_SET_BLK_LEN (0x40 + 16) // CMD16 - set length of block in bytes +#define SD_LOCK_UNLOCK (0x40 + 42) // CMD42 - lock/unlock card +#define CMD55 (0x40 + 55) // multi-byte preface command +#define SD_READ_OCR (0x40 + 58) // read OCR +#define SD_ADV_INIT (0xc0 + 41) // ACMD41, for SDHC cards - advanced start initialization + +//Define options for accessing the SD card's PWD (CMD42) +#define MASK_ERASE 0x08 //erase the entire card +#define MASK_LOCK_UNLOCK 0x04 //lock or unlock the card with password +#define MASK_CLR_PWD 0x02 //clear password +#define MASK_SET_PWD 0x01 //set password + +//Define bit masks for fields in the lock/unlock command (CMD42) data structure +#define SET_PWD_MASK (1<<0) +#define CLR_PWD_MASK (1<<1) +#define LOCK_UNLOCK_MASK (1<<2) +#define ERASE_MASK (1<<3) + +//Begin initialization by sending CMD0 and waiting until SD card +//responds with In Idle Mode (0x01). If the response is not 0x01 +//within a reasonable amount of time, there is no SD card on the bus. +//Returns false if not card is detected +//Returns true if a card responds +bool sdPresent(void) +{ + byte response = 0; + + SPI.begin(); + SPI.setClockDivider(SPI_CLOCK_DIV2); + SPI.setDataMode(SPI_MODE0); + SPI.setBitOrder(MSBFIRST); + pinMode(pin_microSD_CS, OUTPUT); + + //Sending clocks while card power stabilizes... + deselectCard(); // always make sure + for (byte i = 0; i < 30; i++) // send several clocks while card power stabilizes + xchg(0xff); + + //Sending CMD0 - GO IDLE... + for (byte i = 0; i < 0x10; i++) //Attempt to go idle + { + response = sdSendCommand(SD_GO_IDLE, 0); // send CMD0 - go to idle state + if (response == 1) break; + } + if (response != 1) return (false); //Card failed to respond to idle + + return (true); +} + +/* + sdSendCommand send raw command to SD card, return response + + This routine accepts a single SD command and a 4-byte argument. It sends + the command plus argument, adding the appropriate CRC. It then returns + the one-byte response from the SD card. + + For advanced commands (those with a command byte having bit 7 set), this + routine automatically sends the required preface command (CMD55) before + sending the requested command. + + Upon exit, this routine returns the response byte from the SD card. + Possible responses are: + 0xff No response from card; card might actually be missing + 0x01 SD card returned 0x01, which is OK for most commands + 0x?? other responses are command-specific +*/ +byte sdSendCommand(byte command, unsigned long arg) +{ + byte response; + + if (command & 0x80) // special case, ACMD(n) is sent as CMD55 and CMDn + { + command &= 0x7f; // strip high bit for later + response = sdSendCommand(CMD55, 0); // send first part (recursion) + if (response > 1) return (response); + } + + deselectCard(); + xchg(0xFF); + selectCard(); // enable CS + xchg(0xFF); + + xchg(command | 0x40); // command always has bit 6 set! + xchg((byte)(arg >> 24)); // send data, starting with top byte + xchg((byte)(arg >> 16)); + xchg((byte)(arg >> 8)); + xchg((byte)(arg & 0xFF)); + + byte crc = 0x01; // good for most cases + if (command == SD_GO_IDLE) crc = 0x95; // this will be good enough for most commands + if (command == SD_SEND_IF_COND) crc = 0x87; // special case, have to use different CRC + xchg(crc); // send final byte + + for (int i = 0; i < 30; i++) // loop until timeout or response + { + response = xchg(0xFF); + if ((response & 0x80) == 0) break; // high bit cleared means we got a response + } + + /* + We have issued the command but the SD card is still selected. We + only deselectCard the card if the command we just sent is NOT a command + that requires additional data exchange, such as reading or writing + a block. + */ + if ((command != SD_READ_OCR) && + (command != SD_SEND_STATUS) && + (command != SD_SEND_IF_COND) && + (command != SD_LOCK_UNLOCK)) + { + deselectCard(); // all done + xchg(0xFF); // close with eight more clocks + } + + return (response); // let the caller sort it out +} + +//Select (enable) the SD card +void selectCard(void) +{ + digitalWrite(pin_microSD_CS, LOW); +} + +//Deselect (disable) the SD card +void deselectCard(void) +{ + digitalWrite(pin_microSD_CS, HIGH); +} + +//Exchange a byte of data with the SD card via host's SPI bus +byte xchg(byte val) +{ + byte receivedVal = SPI.transfer(val); + return receivedVal; +} diff --git a/Firmware/Test Sketches/System_Check/System.ino b/Firmware/Test Sketches/System_Check/System.ino new file mode 100644 index 000000000..5b73d5a39 --- /dev/null +++ b/Firmware/Test Sketches/System_Check/System.ino @@ -0,0 +1,61 @@ +//Ping an I2C device and see if it responds +bool isConnected(uint8_t deviceAddress) +{ + Wire.beginTransmission(deviceAddress); + if (Wire.endTransmission() == 0) + return true; + return false; +} + +//boolean SFE_UBLOX_GNSS_ADD::getModuleInfo(uint16_t maxWait) +//{ +// theGNSS.minfo.hwVersion[0] = 0; +// theGNSS.minfo.swVersion[0] = 0; +// for (int i = 0; i < 10; i++) +// theGNSS.minfo.extension[i][0] = 0; +// theGNSS.minfo.extensionNo = 0; +// +// // Let's create our custom packet +// uint8_t customPayload[MAX_PAYLOAD_SIZE]; // This array holds the payload data bytes +// +// // The next line creates and initialises the packet information which wraps around the payload +// ubxPacket customCfg = {0, 0, 0, 0, 0, customPayload, 0, 0, SFE_UBLOX_PACKET_VALIDITY_NOT_DEFINED, SFE_UBLOX_PACKET_VALIDITY_NOT_DEFINED}; +// +// customCfg.cls = UBX_CLASS_MON; // This is the message Class +// customCfg.id = UBX_MON_VER; // This is the message ID +// customCfg.len = 0; // Setting the len (length) to zero let's us poll the current settings +// customCfg.startingSpot = 0; // Always set the startingSpot to zero (unless you really know what you are doing) +// +// // Now let's send the command. The module info is returned in customPayload +// +// if (sendCommand(&customCfg, maxWait) != SFE_UBLOX_STATUS_DATA_RECEIVED) +// return (false); //If command send fails then bail +// +// // Now let's extract the module info from customPayload +// +// uint16_t position = 0; +// for (int i = 0; i < 30; i++) +// { +// minfo.swVersion[i] = customPayload[position]; +// position++; +// } +// for (int i = 0; i < 10; i++) +// { +// minfo.hwVersion[i] = customPayload[position]; +// position++; +// } +// +// while (customCfg.len >= position + 30) +// { +// for (int i = 0; i < 30; i++) +// { +// minfo.extension[minfo.extensionNo][i] = customPayload[position]; +// position++; +// } +// minfo.extensionNo++; +// if (minfo.extensionNo > 9) +// break; +// } +// +// return (true); //Success! +//} diff --git a/Firmware/Test Sketches/System_Check/System_Check.ino b/Firmware/Test Sketches/System_Check/System_Check.ino new file mode 100644 index 000000000..8d7cbd6f9 --- /dev/null +++ b/Firmware/Test Sketches/System_Check/System_Check.ino @@ -0,0 +1,377 @@ +/* + Stay on and check each subsystem via I2C, SPI, and serial + + Board ID + GNSS + Display + Battery Gauge + Accelerometer + SD + + No: + Bluetooth + WiFi + + TODO: + L-Band + LoRaSerial +*/ + +const int FIRMWARE_VERSION_MAJOR = 2; +const int FIRMWARE_VERSION_MINOR = 3; + +#define COMPILE_WIFI //Comment out to remove WiFi functionality +#define COMPILE_BT //Comment out to remove Bluetooth functionality +#define COMPILE_AP //Comment out to remove Access Point functionality +#define ENABLE_DEVELOPER //Uncomment this line to enable special developer modes (don't check power button at startup) + +//Define the RTK board identifier: +// This is an int which is unique to this variant of the RTK Surveyor hardware which allows us +// to make sure that the settings stored in flash (LittleFS) are correct for this version of the RTK +// (sizeOfSettings is not necessarily unique and we want to avoid problems when swapping from one variant to another) +// It is the sum of: +// the major firmware version * 0x10 +// the minor firmware version +#define RTK_IDENTIFIER (FIRMWARE_VERSION_MAJOR * 0x10 + FIRMWARE_VERSION_MINOR) + +#include "settings.h" + +//Hardware connections +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +//These pins are set in beginBoard() +int pin_batteryLevelLED_Red = -1; +int pin_batteryLevelLED_Green = -1; +int pin_positionAccuracyLED_1cm = -1; +int pin_positionAccuracyLED_10cm = -1; +int pin_positionAccuracyLED_100cm = -1; +int pin_baseStatusLED = -1; +int pin_bluetoothStatusLED = -1; +int pin_microSD_CS = -1; +int pin_zed_tx_ready = -1; +int pin_zed_reset = -1; +int pin_batteryLevel_alert = -1; + +int pin_muxA = -1; +int pin_muxB = -1; +int pin_powerSenseAndControl = -1; +int pin_setupButton = -1; +int pin_powerFastOff = -1; +int pin_dac26 = -1; +int pin_adc39 = -1; +int pin_peripheralPowerControl = -1; + +int pin_radio_rx = -1; +int pin_radio_tx = -1; +int pin_radio_rst = -1; +int pin_radio_pwr = -1; +int pin_radio_cts = -1; +int pin_radio_rts = -1; +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +//GNSS configuration +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +#include //http://librarymanager/All#SparkFun_u-blox_GNSS_v3 v3.0.2 + +#define SENTENCE_TYPE_NMEA DevUBLOXGNSS::SFE_UBLOX_SENTENCE_TYPE_NMEA +#define SENTENCE_TYPE_NONE DevUBLOXGNSS::SFE_UBLOX_SENTENCE_TYPE_NONE +#define SENTENCE_TYPE_RTCM DevUBLOXGNSS::SFE_UBLOX_SENTENCE_TYPE_RTCM +#define SENTENCE_TYPE_UBX DevUBLOXGNSS::SFE_UBLOX_SENTENCE_TYPE_UBX + +char zedFirmwareVersion[20]; //The string looks like 'HPG 1.12'. Output to system status menu and settings file. +char neoFirmwareVersion[20]; //Output to system status menu. +uint8_t zedFirmwareVersionInt = 0; //Controls which features (constellations) can be configured (v1.12 doesn't support SBAS) +uint8_t zedModuleType = PLATFORM_F9P; //Controls which messages are supported and configured + +//// Extend the class for getModuleInfo. Used to diplay ZED-F9P firmware version in debug menu. +//class SFE_UBLOX_GNSS_ADD : public SFE_UBLOX_GNSS +//{ +// public: +// boolean getModuleInfo(uint16_t maxWait = 1100); //Queries module, texts +// +// struct minfoStructure // Structure to hold the module info (uses 341 bytes of RAM) +// { +// char swVersion[30]; +// char hwVersion[10]; +// uint8_t extensionNo = 0; +// char extension[10][30]; +// } minfo; +//}; + +//SFE_UBLOX_GNSS_ADD theGNSS; + +class SFE_UBLOX_GNSS_SUPER_DERIVED : public SFE_UBLOX_GNSS_SUPER +{ + public: + volatile bool _iAmLocked = false; + bool lock(void) + { + if (_iAmLocked) + { + unsigned long startTime = millis(); + while (_iAmLocked && (millis() < (startTime + 2100))) + delay(1); //YIELD + if (_iAmLocked) + return false; + } + _iAmLocked = true; + return true; + } + void unlock(void) + { + _iAmLocked = false; + } +}; +SFE_UBLOX_GNSS_SUPER_DERIVED theGNSS; + +//Used for config ZED for things not supported in library: getPortSettings, getSerialRate, getNMEASettings, getRTCMSettings +//This array holds the payload data bytes. Global so that we can use between config functions. +#ifdef MAX_PAYLOAD_SIZE +#undef MAX_PAYLOAD_SIZE +#define MAX_PAYLOAD_SIZE 384 // Override MAX_PAYLOAD_SIZE for getModuleInfo which can return up to 348 bytes +#endif +uint8_t settingPayload[MAX_PAYLOAD_SIZE]; + +//These globals are updated regularly via the storePVTdata callback +bool pvtUpdated = false; +double latitude; +double longitude; +float altitude; +float horizontalAccuracy; +bool validDate; +bool validTime; +bool confirmedDate; +bool confirmedTime; +uint8_t gnssDay; +uint8_t gnssMonth; +uint16_t gnssYear; +uint8_t gnssHour; +uint8_t gnssMinute; +uint8_t gnssSecond; +uint16_t mseconds; +uint8_t numSV; +uint8_t fixType; +uint8_t carrSoln; +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +//Accelerometer for bubble leveling +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +#include "SparkFun_LIS2DH12.h" //Click here to get the library: http://librarymanager/All#SparkFun_LIS2DH12 +SPARKFUN_LIS2DH12 accel; +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +//Battery fuel gauge and PWM LEDs +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +#include // Click here to get the library: http://librarymanager/All#SparkFun_MAX1704x_Fuel_Gauge_Arduino_Library +SFE_MAX1704X lipo(MAX1704X_MAX17048); + +// setting PWM properties +const int pwmFreq = 5000; +const int ledRedChannel = 0; +const int ledGreenChannel = 1; +const int ledBTChannel = 2; +const int pwmResolution = 8; + +int pwmFadeAmount = 10; +int btFadeLevel = 0; + +int battLevel = 0; //SOC measured from fuel gauge, in %. Used in multiple places (display, serial debug, log) +float battVoltage = 0.0; +float battChangeRate = 0.0; +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +//External Display +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +#include //http://librarymanager/All#SparkFun_Qwiic_Graphic_OLED +QwiicMicroOLED oled; + +#include +#include +#include +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +//microSD Interface +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +#include +#include "SdFat.h" //http://librarymanager/All#sdfat_exfat by Bill Greiman. Currently uses v2.1.1 + +SdFat * sd; + +char platformFilePrefix[40] = "SFE_Surveyor"; //Sets the prefix for logs and settings files + +SdFile * ubxFile; //File that all GNSS ubx messages sentences are written to +unsigned long lastUBXLogSyncTime = 0; //Used to record to SD every half second +int startLogTime_minutes = 0; //Mark when we start any logging so we can stop logging after maxLogTime_minutes +int startCurrentLogTime_minutes = 0; //Mark when we start this specific log file so we can close it after x minutes and start a new one + +//System crashes if two tasks access a file at the same time +//So we use a semaphore to see if file system is available +SemaphoreHandle_t sdCardSemaphore; +const TickType_t fatSemaphore_shortWait_ms = 10 / portTICK_PERIOD_MS; +const TickType_t fatSemaphore_longWait_ms = 200 / portTICK_PERIOD_MS; + +//Display used/free space in menu and config page +uint32_t sdCardSizeMB = 0; +uint32_t sdFreeSpaceMB = 0; +uint32_t sdUsedSpaceMB = 0; +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + +//Hardware serial and BT buffers +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +HardwareSerial serialGNSS(2); //TX on 17, RX on 16 +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +//Global variables +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +uint8_t unitMACAddress[6]; //Use MAC address in BT broadcast and display +char deviceName[30]; //The serial string that is broadcast. Ex: 'Surveyor Base-BC61' +const byte menuTimeout = 250; //Menus will exit/timeout after this number of seconds +unsigned long splashStart = 0; //Controls how long the splash is displayed for. Currently min of 2s. + +uint8_t wifiMACAddress[6]; // Display this address in the system menu +uint8_t btMACAddress[6]; // Display this address when Bluetooth is enabled, otherwise display wifiMACAddress +uint8_t ethernetMACAddress[6]; // Display this address when Ethernet is enabled, otherwise display wifiMACAddress + +#define platformPrefix platformPrefixTable[productVariant] // Sets the prefix for broadcast names + +bool zedUartPassed = false; //Goes true during testing if ESP can communicate with ZED over UART +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + +void setup() +{ + Serial.begin(115200); + delay(250); + Serial.println(); + Serial.println(F("SparkFun RTK System Check Tool")); + + // int pinNumber1 = 21; + // int pinNumber2 = 22; + // clearBuffer(); + // pinMode(pinNumber1, OUTPUT); + // pinMode(pinNumber2, OUTPUT); + // + // Serial.printf("\n\rToggling pin %d. Press x to exit\n\r", pinNumber1); + // Serial.printf("\n\rToggling pin %d. Press x to exit\n\r", pinNumber2); + // + // while (Serial.available() == 0) + // { + // digitalWrite(pinNumber1, HIGH); + // digitalWrite(pinNumber2, HIGH); + // for (int x = 0 ; x < 100 ; x++) + // { + // delay(30); + // if (Serial.available()) break; + // } + // + // digitalWrite(pinNumber1, LOW); + // digitalWrite(pinNumber2, LOW); + // for (int x = 0 ; x < 100 ; x++) + // { + // delay(30); + // if (Serial.available()) break; + // } + // } + // pinMode(pinNumber1, INPUT); + // pinMode(pinNumber2, INPUT); + // + // Serial.println("Done"); + + Wire.begin(); + + //begin/end wire transmission to see if bus is responding correctly + //All good: 0ms, response 2 + //SDA/SCL shorted: 1000ms timeout, response 5 + //SCL/VCC shorted: 14ms, response 5 + //SCL/GND shorted: 1000ms, response 5 + //SDA/VCC shorted: 1000ms, reponse 5 + //SDA/GND shorted: 14ms, response 5 + unsigned long startTime = millis(); + Wire.beginTransmission(0x15); //Dummy address + int endValue = Wire.endTransmission(); + Serial.printf("Response time: %ld endValue: %d\n\r", millis() - startTime, endValue); + if (endValue == 2) + online.i2c = true; + else if (endValue == 5) + Serial.println("It appears something is shorting the I2C lines."); + + identifyBoard(); // Determine what hardware platform we are running on + + beginBoard(); //Determine what hardware platform we are running on and check on button + + beginSD(); //Test if SD is present + + if (online.i2c == true) + { + beginGNSS(); //Connect to GNSS to get module type + + beginDisplay(); //Start display first to be able to display any errors + + beginAccelerometer(); + + beginFuelGauge(); //Configure battery fuel guage monitor + + oled.erase(); + + uint8_t fontHeight = 8; + uint8_t yPos = 0; + + if (online.accelerometer) + printTextCenter("Accel: OK", yPos, QW_FONT_5X7, 1, false); //text, y, font type, kerning, inverted + else + printTextCenter("Accel: BAD", yPos, QW_FONT_5X7, 1, false); //text, y, font type, kerning, inverted + + yPos += fontHeight; + + if (online.microSD) + printTextCenter("SD: OK", yPos, QW_FONT_5X7, 1, false); //text, y, font type, kerning, inverted + else + printTextCenter("SD: BAD", yPos, QW_FONT_5X7, 1, false); //text, y, font type, kerning, inverted + + yPos += fontHeight; + + if (online.gnss) + printTextCenter("GNSS: OK", yPos, QW_FONT_5X7, 1, false); //text, y, font type, kerning, inverted + else + printTextCenter("GNSS: BAD", yPos, QW_FONT_5X7, 1, false); //text, y, font type, kerning, inverted + + yPos += fontHeight; + + if (online.battery) + printTextCenter("Batt: OK", yPos, QW_FONT_5X7, 1, false); //text, y, font type, kerning, inverted + else + printTextCenter("Batt: BAD", yPos, QW_FONT_5X7, 1, false); //text, y, font type, kerning, inverted + + yPos += fontHeight; + + oled.display(); + } + + //Select port 0 on MUX so UART1 is connected to ZED if bootloading is needed + if (pin_muxA >= 0) + { + pinMode(pin_muxA, OUTPUT); + digitalWrite(pin_muxA, LOW); + } + if (pin_muxB >= 0) + { + pinMode(pin_muxB, OUTPUT); + digitalWrite(pin_muxB, LOW); + Serial.println("MUX should now be UART"); + } + + menuSystem(); +} + +void loop() +{ + delay(10); + + if (pin_powerSenseAndControl >= 0) + { + if (digitalRead(pin_powerSenseAndControl) == LOW) + { + Serial.println("Power button pressed"); + ESP.restart(); //Use the power button as a reset + } + + } +} diff --git a/Firmware/Test Sketches/System_Check/System_Check.ino.esp32.bin b/Firmware/Test Sketches/System_Check/System_Check.ino.esp32.bin new file mode 100644 index 000000000..61469a81d Binary files /dev/null and b/Firmware/Test Sketches/System_Check/System_Check.ino.esp32.bin differ diff --git a/Firmware/Test Sketches/System_Check/batch_program.bat b/Firmware/Test Sketches/System_Check/batch_program.bat new file mode 100644 index 000000000..421c254dc --- /dev/null +++ b/Firmware/Test Sketches/System_Check/batch_program.bat @@ -0,0 +1,24 @@ +@echo off + +if [%1]==[] goto usage + +@echo Programming for SparkFun RTK Surveyor +@pause +:loop + +@echo - +@echo Programming binary: %1 on %2 + +esptool.exe --chip esp32 --port %2 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size detect ^ +0x1000 ./bin/RTK_Surveyor.ino.bootloader.bin ^ +0x8000 ./bin/RTK_Surveyor.ino.partitions.bin ^ +0xe000 ./bin/boot_app0.bin ^ +0x10000 ./%1 + +@echo Done programming! Ready for next board. +@pause + +goto loop + +:usage +@echo Missing the binary file and com port arguments. Ex: batch_program.bat RTK_Surveyor_Firmware_v1_10.bin COM6 \ No newline at end of file diff --git a/Firmware/Test Sketches/System_Check/bin/RTK_Surveyor.ino.bootloader.bin b/Firmware/Test Sketches/System_Check/bin/RTK_Surveyor.ino.bootloader.bin new file mode 100644 index 000000000..186c11d9e Binary files /dev/null and b/Firmware/Test Sketches/System_Check/bin/RTK_Surveyor.ino.bootloader.bin differ diff --git a/Firmware/Test Sketches/System_Check/bin/RTK_Surveyor.ino.partitions.bin b/Firmware/Test Sketches/System_Check/bin/RTK_Surveyor.ino.partitions.bin new file mode 100644 index 000000000..fb15384cd Binary files /dev/null and b/Firmware/Test Sketches/System_Check/bin/RTK_Surveyor.ino.partitions.bin differ diff --git a/Firmware/Test Sketches/System_Check/bin/boot_app0.bin b/Firmware/Test Sketches/System_Check/bin/boot_app0.bin new file mode 100644 index 000000000..13562cabb Binary files /dev/null and b/Firmware/Test Sketches/System_Check/bin/boot_app0.bin differ diff --git a/Firmware/Test Sketches/System_Check/esptool.exe b/Firmware/Test Sketches/System_Check/esptool.exe new file mode 100644 index 000000000..b8d92d7c9 Binary files /dev/null and b/Firmware/Test Sketches/System_Check/esptool.exe differ diff --git a/Firmware/Test Sketches/System_Check/menuFirmware.ino b/Firmware/Test Sketches/System_Check/menuFirmware.ino new file mode 100644 index 000000000..7986e8fde --- /dev/null +++ b/Firmware/Test Sketches/System_Check/menuFirmware.ino @@ -0,0 +1,32 @@ +// Format the firmware version +void formatFirmwareVersion(uint8_t major, uint8_t minor, char *buffer, int bufferLength, bool includeDate) +{ + char prefix; + + // Construct the full or release candidate version number + prefix = 'd'; + // if (enableRCFirmware && (bufferLength >= 21)) + // // 123456789012345678901 + // // pxxx.yyy-dd-mmm-yyyy0 + // snprintf(buffer, bufferLength, "%c%d.%d-%s", prefix, major, minor, __DATE__); + + // Construct a truncated version number + if (bufferLength >= 9) + // 123456789 + // pxxx.yyy0 + snprintf(buffer, bufferLength, "%c%d.%d", prefix, major, minor); + + // The buffer is too small for the version number + else + { + Serial.printf("ERROR: Buffer too small for version number!\r\n"); + if (bufferLength > 0) + *buffer = 0; + } +} + +// Get the current firmware version +void getFirmwareVersion(char *buffer, int bufferLength, bool includeDate) +{ + formatFirmwareVersion(FIRMWARE_VERSION_MAJOR, FIRMWARE_VERSION_MINOR, buffer, bufferLength, includeDate); +} diff --git a/Firmware/Test Sketches/System_Check/menuGNSS.ino b/Firmware/Test Sketches/System_Check/menuGNSS.ino new file mode 100644 index 000000000..b207bfacc --- /dev/null +++ b/Firmware/Test Sketches/System_Check/menuGNSS.ino @@ -0,0 +1,18 @@ +//Print the module type and firmware version +void printZEDInfo() +{ + if (zedModuleType == PLATFORM_F9P) + Serial.printf("ZED-F9P firmware: %s\n\r", zedFirmwareVersion); + else if (zedModuleType == PLATFORM_F9R) + Serial.printf("ZED-F9R firmware: %s\n\r", zedFirmwareVersion); + else + Serial.printf("Unknown module with firmware: %s\n\r", zedFirmwareVersion); +} + + +//Print the NEO firmware version +void printNEOInfo() +{ + if (productVariant == RTK_FACET_LBAND) + Serial.printf("NEO-D9S firmware: %s\n\r", neoFirmwareVersion); +} diff --git a/Firmware/Test Sketches/System_Check/menuSystem.ino b/Firmware/Test Sketches/System_Check/menuSystem.ino new file mode 100644 index 000000000..4ca21dfff --- /dev/null +++ b/Firmware/Test Sketches/System_Check/menuSystem.ino @@ -0,0 +1,274 @@ +//Display current system status +void menuSystem() +{ + bool sdCardAlreadyMounted; + + while (1) + { + if (online.i2c == false) + { + Serial.println("I2C: Offline - Something is causing bus problems"); + } + else + { + Serial.println("I2C: Online"); + + Serial.print(F("GNSS: ")); + if (online.gnss == true) + { + Serial.print(F("Online - ")); + + printZEDInfo(); + + printCurrentConditions(); + } + else Serial.println(F("Offline")); + + Serial.print(F("Display: ")); + if (online.display == true) Serial.println(F("Online")); + else Serial.println(F("Offline")); + + Serial.print(F("Accelerometer: ")); + if (online.accelerometer == true) Serial.println(F("Online")); + else Serial.println(F("Offline")); + + Serial.print(F("Fuel Gauge: ")); + if (online.battery == true) + { + Serial.print(F("Online - ")); + + battLevel = lipo.getSOC(); + battVoltage = lipo.getVoltage(); + battChangeRate = lipo.getChangeRate(); + + Serial.printf("Batt (%d%%) / Voltage: %0.02fV", battLevel, battVoltage); + Serial.println(); + } + else Serial.println(F("Offline")); + } + + Serial.print(F("microSD: ")); + if (online.microSD == true) Serial.println(F("Online")); + else Serial.println(F("Offline")); + + // if (online.lband == true) + // { + // Serial.print(F("L-Band: Online - ")); + // + // if (online.lbandCorrections == true) Serial.print(F("Keys Good")); + // else Serial.print(F("No Keys")); + // + // Serial.print(F(" / Corrections Received")); + // if (lbandCorrectionsReceived == false) Serial.print(F(" Failed")); + // + // Serial.printf(" / Eb/N0[dB] (>9 is good): %0.2f", lBandEBNO); + // + // Serial.print(F(" - ")); + // + // printNEOInfo(); + // } + + //Display MAC address + char macAddress[5]; + sprintf(macAddress, "%02X%02X", unitMACAddress[4], unitMACAddress[5]); + + Serial.print(F("Bluetooth (")); + Serial.print(macAddress); + Serial.print(F("): ")); + + Serial.println(F("s) Scan I2C")); + Serial.println(F("S) Verbose scan of I2C")); + Serial.println(F("t) Toggle pin")); + Serial.println(F("r) Reset")); + Serial.println(F("p) Power Down")); + + byte incoming = getByteChoice(menuTimeout); //Timeout after x seconds + + if (incoming == 'r') + { + ESP.restart(); + } + else if (incoming == 's') + { + Serial.println("Scan I2C bus:"); + for (byte address = 0; address < 127; address++ ) + { + Wire.beginTransmission(address); + if (Wire.endTransmission() == 0) + { + Serial.print("Device found at address 0x"); + if (address < 0x10) + Serial.print("0"); + Serial.print(address, HEX); + + if (address == 0x42) Serial.print(" GNSS"); + if (address == 0x43) Serial.print(" NEO"); + if (address == 0x2C) Serial.print(" USB Hub"); + if (address == 0x36) Serial.print(" Fuel Gauge"); + if (address == 0x19) Serial.print(" Accelerometer"); + if (address == 0x3D) Serial.print(" Display Main"); + if (address == 0x3C) Serial.print(" Display Alternate"); + Serial.println(); + } + } + Serial.println("Done"); + } + else if (incoming == 'S') + { + Serial.println("Verbose scan I2C bus:"); + for (byte address = 0; address < 127; address++ ) + { + Serial.print("Address 0x"); + if (address < 0x10) + Serial.print("0"); + Serial.print(address, HEX); + Serial.print(" "); + + unsigned long startTime = millis(); + + //begin/end wire transmission should take a few ms. If it's taking longer, + //it's likely the I2C bus being shorted or pulled in + + Wire.beginTransmission(address); + int endValue = Wire.endTransmission(); + + if (millis() - startTime > 100) + Serial.print("(The I2C bus is borked! Something is shorting SDA/SCL pins. Check accelerometer.) "); + + if (endValue != 0) + { + Serial.print("Nothing"); + } + else if (endValue == 0) + { + Serial.print("Found "); + + if (address == 0x42) Serial.print("GNSS"); + if (address == 0x43) Serial.print("NEO"); + if (address == 0x2C) Serial.print("USB Hub"); + if (address == 0x36) Serial.print("Fuel Gauge"); + if (address == 0x19) Serial.print("Accelerometer"); + if (address == 0x3D) Serial.print("Dsiplay Main"); + if (address == 0x3C) Serial.print("Dsiplay Alternate"); + } + Serial.println(); + } + Serial.println("Done"); + } + else if (incoming == 't') + { + Serial.println("Select pin to toggle:"); + Serial.println("0 - Stat LED"); + Serial.println("21 - SDA"); + Serial.println("22 - SCL"); + Serial.println("23 - SD COPI"); + Serial.println("13 - Power Control"); + int pinNumber = getNumber(menuTimeout); //Timeout after x seconds + + if (pinNumber >= 0 && pinNumber < STATUS_PRESSED_X) + { + Wire.end(); + delete sd; + + clearBuffer(); + pinMode(pinNumber, OUTPUT); + + Serial.printf("\n\rToggling pin %d. Press x to exit\n\r", pinNumber); + + while (Serial.available() == 0) + { + digitalWrite(pinNumber, HIGH); + for (int x = 0 ; x < 100 ; x++) + { + delay(30); + if (Serial.available()) break; + } + + digitalWrite(pinNumber, LOW); + for (int x = 0 ; x < 100 ; x++) + { + delay(30); + if (Serial.available()) break; + } + } + pinMode(pinNumber, INPUT); + + Serial.println("Done"); + ESP.restart(); + } + } + else if (incoming == 'p') + { + Serial.println("Power down"); + + powerDown(false); //No display + } + else if (incoming == STATUS_GETBYTE_TIMEOUT) + { + //break; + } + else + printUnknown(incoming); + } +} + +//Print the current long/lat/alt/HPA/SIV +//From Example11_GetHighPrecisionPositionUsingDouble +void printCurrentConditions() +{ + if (online.gnss == true) + { + Serial.print(F("SIV: ")); + Serial.print(numSV); + + Serial.print(", HPA (m): "); + Serial.print(horizontalAccuracy, 3); + + Serial.print(", Lat: "); + Serial.print(latitude, 9); + Serial.print(", Lon: "); + Serial.print(longitude, 9); + + Serial.print(", Altitude (m): "); + Serial.print(altitude, 1); + + Serial.println(); + } +} + +void testGNSS() +{ + //The following ZED test blocks the usage of UART1 for bootloading. + //Verify the ESP UART2 can communicate TX/RX to ZED UART1 + if (online.gnss == true) + { + if (zedUartPassed == false) + { + //stopUART2Tasks(); //Stop absoring ZED serial via task + + theGNSS.setSerialRate(460800, COM_PORT_UART1); //Defaults to 460800 to maximize message output support + serialGNSS.begin(460800); //UART2 on pins 16/17 for SPP. The ZED-F9P will be configured to output NMEA over its UART1 at the same rate. + + SFE_UBLOX_GNSS myGNSS; + if (myGNSS.begin(serialGNSS) == true) //begin() attempts 3 connections + { + zedUartPassed = true; + Serial.print(F("Online")); + } + else + Serial.print(F("Offline")); + + theGNSS.setSerialRate(settings.dataPortBaud, COM_PORT_UART1); //Defaults to 460800 to maximize message output support + serialGNSS.begin(settings.dataPortBaud); //UART2 on pins 16/17 for SPP. The ZED-F9P will be configured to output NMEA over its UART1 at the same rate. + + //startUART2Tasks(); //Return to normal operation + } + else + Serial.print(F("Online")); + } + else + { + Serial.print("Can't check (GNSS offline)"); + } + Serial.println(); +} diff --git a/Firmware/Test Sketches/System_Check/settings.h b/Firmware/Test Sketches/System_Check/settings.h new file mode 100644 index 000000000..702fdb99d --- /dev/null +++ b/Firmware/Test Sketches/System_Check/settings.h @@ -0,0 +1,430 @@ +//System can enter a variety of states +//See statemachine diagram at: https://lucid.app/lucidchart/53519501-9fa5-4352-aa40-673f88ca0c9b/edit?invitationId=inv_ebd4b988-513d-4169-93fd-c291851108f8 +typedef enum +{ + STATE_ROVER_NOT_STARTED = 0, + STATE_ROVER_NO_FIX, + STATE_ROVER_FIX, + STATE_ROVER_RTK_FLOAT, + STATE_ROVER_RTK_FIX, + STATE_BASE_NOT_STARTED, + STATE_BASE_TEMP_SETTLE, //User has indicated base, but current pos accuracy is too low + STATE_BASE_TEMP_SURVEY_STARTED, + STATE_BASE_TEMP_TRANSMITTING, + STATE_BASE_TEMP_WIFI_STARTED, + STATE_BASE_TEMP_WIFI_CONNECTED, + STATE_BASE_TEMP_CASTER_STARTED, + STATE_BASE_TEMP_CASTER_CONNECTED, + STATE_BASE_FIXED_NOT_STARTED, + STATE_BASE_FIXED_TRANSMITTING, + STATE_BASE_FIXED_WIFI_STARTED, + STATE_BASE_FIXED_WIFI_CONNECTED, + STATE_BASE_FIXED_CASTER_STARTED, + STATE_BASE_FIXED_CASTER_CONNECTED, + STATE_BUBBLE_LEVEL, + STATE_MARK_EVENT, + STATE_DISPLAY_SETUP, + STATE_WIFI_CONFIG_NOT_STARTED, + STATE_WIFI_CONFIG, + STATE_TEST, + STATE_TESTING, + STATE_PROFILE_1, + STATE_PROFILE_2, + STATE_PROFILE_3, + STATE_PROFILE_4, + STATE_KEYS_STARTED, + STATE_KEYS_NEEDED, + STATE_KEYS_WIFI_STARTED, + STATE_KEYS_WIFI_CONNECTED, + STATE_KEYS_WIFI_TIMEOUT, + STATE_KEYS_EXPIRED, + STATE_KEYS_DAYS_REMAINING, + STATE_KEYS_LBAND_CONFIGURE, + STATE_KEYS_LBAND_ENCRYPTED, + STATE_KEYS_PROVISION_WIFI_STARTED, + STATE_KEYS_PROVISION_WIFI_CONNECTED, + STATE_KEYS_PROVISION_WIFI_TIMEOUT, + STATE_SHUTDOWN, +} SystemState; +volatile SystemState systemState = STATE_ROVER_NOT_STARTED; +SystemState lastSystemState = STATE_ROVER_NOT_STARTED; +SystemState requestedSystemState = STATE_ROVER_NOT_STARTED; +bool newSystemStateRequested = false; + +const char *const platformPrefixTable[] = { + "Surveyor", + "Express", + "Facet", + "Express Plus", + "Facet L-Band", + "Reference Station", + "Facet L-Band Direct", + // Add new values just above this line + "Unknown", +}; +const int platformPrefixTableEntries = sizeof(platformPrefixTable) / sizeof(platformPrefixTable[0]); + +//The setup display can show a limited set of states +//When user pauses for X amount of time, system will enter that state +SystemState setupState = STATE_MARK_EVENT; + +typedef enum +{ + RTK_SURVEYOR = 0, + RTK_EXPRESS, + RTK_FACET, + RTK_EXPRESS_PLUS, + RTK_FACET_LBAND, + REFERENCE_STATION, + RTK_FACET_LBAND_DIRECT, + // Add new values just above this line + RTK_UNKNOWN, +} ProductVariant; +ProductVariant productVariant = RTK_SURVEYOR; + +typedef enum +{ + BUTTON_ROVER = 0, + BUTTON_BASE, +} ButtonState; +ButtonState buttonPreviousState = BUTTON_ROVER; + +//Data port mux (RTK Express) can enter one of four different connections +typedef enum muxConnectionType_e +{ + MUX_UBLOX_NMEA = 0, + MUX_PPS_EVENTTRIGGER, + MUX_I2C_WT, + MUX_ADC_DAC, +} muxConnectionType_e; + +//User can enter fixed base coordinates in ECEF or degrees +typedef enum +{ + COORD_TYPE_ECEF = 0, + COORD_TYPE_GEODETIC, +} coordinateType_e; + +//User can select output pulse as either falling or rising edge +typedef enum +{ + PULSE_FALLING_EDGE = 0, + PULSE_RISING_EDGE, +} pulseEdgeType_e; + +//Custom NMEA sentence types output to the log file +typedef enum +{ + CUSTOM_NMEA_TYPE_RESET_REASON = 0, + CUSTOM_NMEA_TYPE_WAYPOINT, + CUSTOM_NMEA_TYPE_EVENT, + CUSTOM_NMEA_TYPE_SYSTEM_VERSION, + CUSTOM_NMEA_TYPE_ZED_VERSION, + CUSTOM_NMEA_TYPE_STATUS, +} customNmeaType_e; + +//Freeze and blink LEDs if we hit a bad error +typedef enum +{ + ERROR_NO_I2C = 2, //Avoid 0 and 1 as these are bad blink codes + ERROR_GPS_CONFIG_FAIL, +} t_errorNumber; + +enum WiFiState +{ + WIFI_OFF = 0, + WIFI_ON, + WIFI_NOTCONNECTED, + WIFI_CONNECTED, +}; +volatile byte wifiState = WIFI_OFF; + +enum NTRIPClientState +{ + NTRIP_CLIENT_OFF = 0, //Using Bluetooth or NTRIP server + NTRIP_CLIENT_ON, //WIFI_ON state + NTRIP_CLIENT_WIFI_CONNECTING, //Connecting to WiFi access point + NTRIP_CLIENT_WIFI_CONNECTED, //WiFi connected to an access point + NTRIP_CLIENT_CONNECTING, //Attempting a connection to the NTRIP caster + NTRIP_CLIENT_CONNECTED, //Connected to the NTRIP caster +}; +volatile byte ntripClientState = NTRIP_CLIENT_OFF; + +//Radio status LED goes from off (LED off), no connection (blinking), to connected (solid) +enum BTState +{ + BT_OFF = 0, + BT_NOTCONNECTED, + BT_CONNECTED, +}; +volatile byte btState = BT_OFF; + +//Return values for getByteChoice() +enum returnStatus { + STATUS_GETBYTE_TIMEOUT = 255, + STATUS_GETNUMBER_TIMEOUT = -123455555, + STATUS_PRESSED_X = 254, +}; + +#include //http://librarymanager/All#SparkFun_u-blox_GNSS_v3 + +//Each constellation will have its config key, enable, and a visible name +typedef struct ubxConstellation +{ + uint32_t configKey; + uint8_t gnssID; + bool enabled; + char textName[30]; +} ubxConstellation; + +//These are the allowable constellations to receive from and log (if enabled) +//Tested with u-center v21.02 +#define MAX_CONSTELLATIONS 6 //(sizeof(ubxConstellations)/sizeof(ubxConstellation)) + + +//Different ZED modules support different messages (F9P vs F9R vs F9T) +//Create binary packed struct for different platforms +typedef enum ubxPlatform +{ + PLATFORM_F9P = 0b0001, + PLATFORM_F9R = 0b0010, + PLATFORM_F9T = 0b0100, +} ubxPlatform; + +//Each message will have a rate, a visible name, and a class +typedef struct ubxMsg +{ + uint32_t msgConfigKey; + uint8_t msgID; + uint8_t msgClass; + uint8_t msgRate; + char msgTextName[30]; + uint8_t supported; +} ubxMsg; + +//These are the allowable messages to broadcast and log (if enabled) +//Tested with u-center v21.02 +#define MAX_UBX_MSG (13 + 25 + 5 + 10 + 3 + 12 + 5) //(sizeof(ubxMessages)/sizeof(ubxMsg)) + +//This is all the settings that can be set on RTK Surveyor. It's recorded to NVM and the config file. +typedef struct { + int sizeOfSettings = 0; //sizeOfSettings **must** be the first entry and must be int + int rtkIdentifier = RTK_IDENTIFIER; // rtkIdentifier **must** be the second entry + bool printDebugMessages = false; + bool enableSD = true; + bool enableDisplay = true; + int maxLogTime_minutes = 60 * 24; //Default to 24 hours + int observationSeconds = 60; //Default survey in time of 60 seconds + float observationPositionAccuracy = 5.0; //Default survey in pos accy of 5m + bool fixedBase = false; //Use survey-in by default + bool fixedBaseCoordinateType = COORD_TYPE_ECEF; + double fixedEcefX = -1280206.568; + double fixedEcefY = -4716804.403; + double fixedEcefZ = 4086665.484; + double fixedLat = 40.09029479; + double fixedLong = -105.18505761; + double fixedAltitude = 1560.089; + uint32_t dataPortBaud = 460800; //Default to 460800bps to support >10Hz update rates + uint32_t radioPortBaud = 57600; //Default to 57600bps to support connection to SiK1000 radios + float surveyInStartingAccuracy = 1.0; //Wait for 1m horizontal positional accuracy before starting survey in + uint16_t measurementRate = 250; //Elapsed ms between GNSS measurements. 25ms to 65535ms. Default 4Hz. + uint16_t navigationRate = 1; //Ratio between number of measurements and navigation solutions. Default 1 for 4Hz (with measurementRate). + bool enableI2Cdebug = false; //Turn on to display GNSS library debug messages + bool enableHeapReport = false; //Turn on to display free heap + bool enableTaskReports = false; //Turn on to display task high water marks + muxConnectionType_e dataPortChannel = MUX_UBLOX_NMEA; //Mux default to ublox UART1 + uint16_t spiFrequency = 16; //By default, use 16MHz SPI + bool enableLogging = true; //If an SD card is present, log default sentences + uint16_t sppRxQueueSize = 2048; + uint16_t sppTxQueueSize = 512; + uint8_t dynamicModel = DYN_MODEL_PORTABLE; + SystemState lastState = STATE_ROVER_NOT_STARTED; //For Express, start unit in last known state + bool throttleDuringSPPCongestion = true; + bool enableSensorFusion = false; //If IMU is available, avoid using it unless user specifically selects automotive + bool autoIMUmountAlignment = true; //Allows unit to automatically establish device orientation in vehicle + bool enableResetDisplay = false; + uint8_t resetCount = 0; + bool enableExternalPulse = true; //Send pulse once lock is achieved + uint32_t externalPulseTimeBetweenPulse_us = 900000; //us between pulses, max of 65s + uint32_t externalPulseLength_us = 100000; //us length of pulse + pulseEdgeType_e externalPulsePolarity = PULSE_RISING_EDGE; //Pulse rises for pulse length, then falls + bool enableExternalHardwareEventLogging = false; //Log when INT/TM2 pin goes low + + ubxMsg ubxMessages[MAX_UBX_MSG] = //Report rates for all known messages + { + //NMEA + {UBLOX_CFG_MSGOUT_NMEA_ID_DTM_UART1, UBX_NMEA_DTM, UBX_CLASS_NMEA, 0, "UBX_NMEA_DTM", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_NMEA_ID_GBS_UART1, UBX_NMEA_GBS, UBX_CLASS_NMEA, 0, "UBX_NMEA_GBS", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_NMEA_ID_GGA_UART1, UBX_NMEA_GGA, UBX_CLASS_NMEA, 1, "UBX_NMEA_GGA", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_NMEA_ID_GLL_UART1, UBX_NMEA_GLL, UBX_CLASS_NMEA, 0, "UBX_NMEA_GLL", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_NMEA_ID_GNS_UART1, UBX_NMEA_GNS, UBX_CLASS_NMEA, 0, "UBX_NMEA_GNS", (PLATFORM_F9P | PLATFORM_F9R)}, + + {UBLOX_CFG_MSGOUT_NMEA_ID_GRS_UART1, UBX_NMEA_GRS, UBX_CLASS_NMEA, 0, "UBX_NMEA_GRS", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_NMEA_ID_GSA_UART1, UBX_NMEA_GSA, UBX_CLASS_NMEA, 1, "UBX_NMEA_GSA", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_NMEA_ID_GST_UART1, UBX_NMEA_GST, UBX_CLASS_NMEA, 1, "UBX_NMEA_GST", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_NMEA_ID_GSV_UART1, UBX_NMEA_GSV, UBX_CLASS_NMEA, 4, "UBX_NMEA_GSV", (PLATFORM_F9P | PLATFORM_F9R)}, //Default to 1 update every 4 fixes + {UBLOX_CFG_MSGOUT_NMEA_ID_RMC_UART1, UBX_NMEA_RMC, UBX_CLASS_NMEA, 1, "UBX_NMEA_RMC", (PLATFORM_F9P | PLATFORM_F9R)}, + + {UBLOX_CFG_MSGOUT_NMEA_ID_VLW_UART1, UBX_NMEA_VLW, UBX_CLASS_NMEA, 0, "UBX_NMEA_VLW", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_NMEA_ID_VTG_UART1, UBX_NMEA_VTG, UBX_CLASS_NMEA, 0, "UBX_NMEA_VTG", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_NMEA_ID_ZDA_UART1, UBX_NMEA_ZDA, UBX_CLASS_NMEA, 0, "UBX_NMEA_ZDA", (PLATFORM_F9P | PLATFORM_F9R)}, + + //NAV + {UBLOX_CFG_MSGOUT_UBX_NAV_ATT_UART1, UBX_NAV_ATT, UBX_CLASS_NAV, 0, "UBX_NAV_ATT", (PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_NAV_CLOCK_UART1, UBX_NAV_CLOCK, UBX_CLASS_NAV, 0, "UBX_NAV_CLOCK", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_NAV_DOP_UART1, UBX_NAV_DOP, UBX_CLASS_NAV, 0, "UBX_NAV_DOP", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_NAV_EOE_UART1, UBX_NAV_EOE, UBX_CLASS_NAV, 0, "UBX_NAV_EOE", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_NAV_GEOFENCE_UART1, UBX_NAV_GEOFENCE, UBX_CLASS_NAV, 0, "UBX_NAV_GEOFENCE", (PLATFORM_F9P | PLATFORM_F9R)}, + + {UBLOX_CFG_MSGOUT_UBX_NAV_HPPOSECEF_UART1, UBX_NAV_HPPOSECEF, UBX_CLASS_NAV, 0, "UBX_NAV_HPPOSECEF", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_NAV_HPPOSLLH_UART1, UBX_NAV_HPPOSLLH, UBX_CLASS_NAV, 0, "UBX_NAV_HPPOSLLH", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_NAV_ODO_UART1, UBX_NAV_ODO, UBX_CLASS_NAV, 0, "UBX_NAV_ODO", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_NAV_ORB_UART1, UBX_NAV_ORB, UBX_CLASS_NAV, 0, "UBX_NAV_ORB", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_NAV_POSECEF_UART1, UBX_NAV_POSECEF, UBX_CLASS_NAV, 0, "UBX_NAV_POSECEF", (PLATFORM_F9P | PLATFORM_F9R)}, + + {UBLOX_CFG_MSGOUT_UBX_NAV_POSLLH_UART1, UBX_NAV_POSLLH, UBX_CLASS_NAV, 0, "UBX_NAV_POSLLH", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_NAV_PVT_UART1, UBX_NAV_PVT, UBX_CLASS_NAV, 0, "UBX_NAV_PVT", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_NAV_RELPOSNED_UART1, UBX_NAV_RELPOSNED, UBX_CLASS_NAV, 0, "UBX_NAV_RELPOSNED", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_NAV_SAT_UART1, UBX_NAV_SAT, UBX_CLASS_NAV, 0, "UBX_NAV_SAT", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_NAV_SIG_UART1, UBX_NAV_SIG, UBX_CLASS_NAV, 0, "UBX_NAV_SIG", (PLATFORM_F9P | PLATFORM_F9R)}, + + {UBLOX_CFG_MSGOUT_UBX_NAV_STATUS_UART1, UBX_NAV_STATUS, UBX_CLASS_NAV, 0, "UBX_NAV_STATUS", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_NAV_SVIN_UART1, UBX_NAV_SVIN, UBX_CLASS_NAV, 0, "UBX_NAV_SVIN", (PLATFORM_F9P)}, + {UBLOX_CFG_MSGOUT_UBX_NAV_TIMEBDS_UART1, UBX_NAV_TIMEBDS, UBX_CLASS_NAV, 0, "UBX_NAV_TIMEBDS", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_NAV_TIMEGAL_UART1, UBX_NAV_TIMEGAL, UBX_CLASS_NAV, 0, "UBX_NAV_TIMEGAL", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_NAV_TIMEGLO_UART1, UBX_NAV_TIMEGLO, UBX_CLASS_NAV, 0, "UBX_NAV_TIMEGLO", (PLATFORM_F9P | PLATFORM_F9R)}, + + {UBLOX_CFG_MSGOUT_UBX_NAV_TIMEGPS_UART1, UBX_NAV_TIMEGPS, UBX_CLASS_NAV, 0, "UBX_NAV_TIMEGPS", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_NAV_TIMELS_UART1, UBX_NAV_TIMELS, UBX_CLASS_NAV, 0, "UBX_NAV_TIMELS", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_NAV_TIMEUTC_UART1, UBX_NAV_TIMEUTC, UBX_CLASS_NAV, 0, "UBX_NAV_TIMEUTC", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_NAV_VELECEF_UART1, UBX_NAV_VELECEF, UBX_CLASS_NAV, 0, "UBX_NAV_VELECEF", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_NAV_VELNED_UART1, UBX_NAV_VELNED, UBX_CLASS_NAV, 0, "UBX_NAV_VELNED", (PLATFORM_F9P | PLATFORM_F9R)}, + + //RXM + {UBLOX_CFG_MSGOUT_UBX_RXM_MEASX_UART1, UBX_RXM_MEASX, UBX_CLASS_RXM, 0, "UBX_RXM_MEASX", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_RXM_RAWX_UART1, UBX_RXM_RAWX, UBX_CLASS_RXM, 0, "UBX_RXM_RAWX", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_RXM_RLM_UART1, UBX_RXM_RLM, UBX_CLASS_RXM, 0, "UBX_RXM_RLM", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_RXM_RTCM_UART1, UBX_RXM_RTCM, UBX_CLASS_RXM, 0, "UBX_RXM_RTCM", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_RXM_SFRBX_UART1, UBX_RXM_SFRBX, UBX_CLASS_RXM, 0, "UBX_RXM_SFRBX", (PLATFORM_F9P | PLATFORM_F9R)}, + + //MON + {UBLOX_CFG_MSGOUT_UBX_MON_COMMS_UART1, UBX_MON_COMMS, UBX_CLASS_MON, 0, "UBX_MON_COMMS", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_MON_HW2_UART1, UBX_MON_HW2, UBX_CLASS_MON, 0, "UBX_MON_HW2", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_MON_HW3_UART1, UBX_MON_HW3, UBX_CLASS_MON, 0, "UBX_MON_HW3", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_MON_HW_UART1, UBX_MON_HW, UBX_CLASS_MON, 0, "UBX_MON_HW", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_MON_IO_UART1, UBX_MON_IO, UBX_CLASS_MON, 0, "UBX_MON_IO", (PLATFORM_F9P | PLATFORM_F9R)}, + + {UBLOX_CFG_MSGOUT_UBX_MON_MSGPP_UART1, UBX_MON_MSGPP, UBX_CLASS_MON, 0, "UBX_MON_MSGPP", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_MON_RF_UART1, UBX_MON_RF, UBX_CLASS_MON, 0, "UBX_MON_RF", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_MON_RXBUF_UART1, UBX_MON_RXBUF, UBX_CLASS_MON, 0, "UBX_MON_RXBUF", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_MON_RXR_UART1, UBX_MON_RXR, UBX_CLASS_MON, 0, "UBX_MON_RXR", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_MON_TXBUF_UART1, UBX_MON_TXBUF, UBX_CLASS_MON, 0, "UBX_MON_TXBUF", (PLATFORM_F9P | PLATFORM_F9R)}, + + //TIM + {UBLOX_CFG_MSGOUT_UBX_TIM_TM2_UART1, UBX_TIM_TM2, UBX_CLASS_TIM, 0, "UBX_TIM_TM2", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_TIM_TP_UART1, UBX_TIM_TP, UBX_CLASS_TIM, 0, "UBX_TIM_TP", (PLATFORM_F9P | PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_TIM_VRFY_UART1, UBX_TIM_VRFY, UBX_CLASS_TIM, 0, "UBX_TIM_VRFY", (PLATFORM_F9P | PLATFORM_F9R)}, + + //RTCM + {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE1005_UART1, UBX_RTCM_1005, UBX_RTCM_MSB, 0, "UBX_RTCM_1005", (PLATFORM_F9P)}, + {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE1074_UART1, UBX_RTCM_1074, UBX_RTCM_MSB, 0, "UBX_RTCM_1074", (PLATFORM_F9P)}, + {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE1077_UART1, UBX_RTCM_1077, UBX_RTCM_MSB, 0, "UBX_RTCM_1077", (PLATFORM_F9P)}, + {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE1084_UART1, UBX_RTCM_1084, UBX_RTCM_MSB, 0, "UBX_RTCM_1084", (PLATFORM_F9P)}, + {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE1087_UART1, UBX_RTCM_1087, UBX_RTCM_MSB, 0, "UBX_RTCM_1087", (PLATFORM_F9P)}, + + {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE1094_UART1, UBX_RTCM_1094, UBX_RTCM_MSB, 0, "UBX_RTCM_1094", (PLATFORM_F9P)}, + {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE1097_UART1, UBX_RTCM_1097, UBX_RTCM_MSB, 0, "UBX_RTCM_1097", (PLATFORM_F9P)}, + {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE1124_UART1, UBX_RTCM_1124, UBX_RTCM_MSB, 0, "UBX_RTCM_1124", (PLATFORM_F9P)}, + {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE1127_UART1, UBX_RTCM_1127, UBX_RTCM_MSB, 0, "UBX_RTCM_1127", (PLATFORM_F9P)}, + {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE1230_UART1, UBX_RTCM_1230, UBX_RTCM_MSB, 0, "UBX_RTCM_1230", (PLATFORM_F9P)}, + + {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE4072_0_UART1, UBX_RTCM_4072_0, UBX_RTCM_MSB, 0, "UBX_RTCM_4072_0", (PLATFORM_F9P)}, + {UBLOX_CFG_MSGOUT_RTCM_3X_TYPE4072_1_UART1, UBX_RTCM_4072_1, UBX_RTCM_MSB, 0, "UBX_RTCM_4072_1", (PLATFORM_F9P)}, + + //ESF + {UBLOX_CFG_MSGOUT_UBX_ESF_MEAS_UART1, UBX_ESF_MEAS, UBX_CLASS_ESF, 0, "UBX_ESF_MEAS", (PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_ESF_RAW_UART1, UBX_ESF_RAW, UBX_CLASS_ESF, 0, "UBX_ESF_RAW", (PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_ESF_STATUS_UART1, UBX_ESF_STATUS, UBX_CLASS_ESF, 0, "UBX_ESF_STATUS", (PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_ESF_ALG_UART1, UBX_ESF_ALG, UBX_CLASS_ESF, 0, "UBX_ESF_ALG", (PLATFORM_F9R)}, + {UBLOX_CFG_MSGOUT_UBX_ESF_INS_UART1, UBX_ESF_INS, UBX_CLASS_ESF, 0, "UBX_ESF_INS", (PLATFORM_F9R)}, + }; + + //Constellations monitored/used for fix + ubxConstellation ubxConstellations[MAX_CONSTELLATIONS] = + { + {UBLOX_CFG_SIGNAL_GPS_ENA, SFE_UBLOX_GNSS_ID_GPS, true, "GPS"}, + {UBLOX_CFG_SIGNAL_SBAS_ENA, SFE_UBLOX_GNSS_ID_SBAS, true, "SBAS"}, + {UBLOX_CFG_SIGNAL_GAL_ENA, SFE_UBLOX_GNSS_ID_GALILEO, true, "Galileo"}, + {UBLOX_CFG_SIGNAL_BDS_ENA, SFE_UBLOX_GNSS_ID_BEIDOU, true, "BeiDou"}, + //{UBLOX_CFG_SIGNAL_QZSS_ENA, SFE_UBLOX_GNSS_ID_IMES, false, "IMES"}, //Not yet supported? Config key does not exist? + {UBLOX_CFG_SIGNAL_QZSS_ENA, SFE_UBLOX_GNSS_ID_QZSS, true, "QZSS"}, + {UBLOX_CFG_SIGNAL_GLO_ENA, SFE_UBLOX_GNSS_ID_GLONASS, true, "GLONASS"}, + }; + + int maxLogLength_minutes = 60 * 24; //Default to 24 hours + char profileName[50] = ""; + + //NTRIP Server + bool enableNtripServer = false; + char ntripServer_CasterHost[50] = "rtk2go.com"; //It's free... + uint16_t ntripServer_CasterPort = 2101; + char ntripServer_CasterUser[50] = "test@test.com"; //Some free casters require auth. User must provide their own email address to use RTK2Go + char ntripServer_CasterUserPW[50] = ""; + char ntripServer_MountPoint[50] = "bldr_dwntwn2"; //NTRIP Server + char ntripServer_MountPointPW[50] = "WR5wRo4H"; + char ntripServer_wifiSSID[50] = "TRex"; //NTRIP Server Wifi + char ntripServer_wifiPW[50] = "parachutes"; + + //NTRIP Client + bool enableNtripClient = false; + char ntripClient_CasterHost[50] = "rtk2go.com"; //It's free... + uint16_t ntripClient_CasterPort = 2101; + char ntripClient_CasterUser[50] = "test@test.com"; //Some free casters require auth. User must provide their own email address to use RTK2Go + char ntripClient_CasterUserPW[50] = ""; + char ntripClient_MountPoint[50] = "bldr_SparkFun1"; + char ntripClient_MountPointPW[50] = ""; + char ntripClient_wifiSSID[50] = "TRex"; //NTRIP Server Wifi + char ntripClient_wifiPW[50] = "parachutes"; + bool ntripClient_TransmitGGA = true; + + int16_t serialTimeoutGNSS = 1; //In ms - used during SerialGNSS.begin. Number of ms to pass of no data before hardware serial reports data available. + + char pointPerfectDeviceProfileToken[40] = ""; + bool enablePointPerfectCorrections = true; + char home_wifiSSID[50] = ""; //WiFi network to use when attempting to obtain PointPerfect keys and ThingStream provisioning + char home_wifiPW[50] = ""; + bool autoKeyRenewal = true; //Attempt to get keys if we get under 28 days from the expiration date + char pointPerfectClientID[50] = ""; + char pointPerfectBrokerHost[50] = ""; // pp.services.u-blox.com + char pointPerfectLBandTopic[20] = ""; // /pp/key/Lb + + char pointPerfectCurrentKey[33] = ""; //32 hexadecimal digits = 128 bits = 16 Bytes + uint64_t pointPerfectCurrentKeyDuration = 0; + uint64_t pointPerfectCurrentKeyStart = 0; + + char pointPerfectNextKey[33] = ""; + uint64_t pointPerfectNextKeyDuration = 0; + uint64_t pointPerfectNextKeyStart = 0; + + uint64_t lastKeyAttempt = 0; //Epoch time of last attempt at obtaining keys + bool updateZEDSettings = true; //When in doubt, update the ZED with current settings + uint32_t LBandFreq = 1556290000; //Default to US band +} Settings; +Settings settings; + +//Monitor which devices on the device are on or offline. +struct struct_online { + bool microSD = false; + bool display = false; + bool gnss = false; + bool logging = false; + bool serialOutput = false; + bool fs = false; + bool rtc = false; + bool battery = false; + bool accelerometer = false; + bool ntripClient = false; + bool lband = false; + bool lbandCorrections = false; + bool i2c = false; +} online; diff --git a/Firmware/Test Sketches/SD_Update/support.ino b/Firmware/Test Sketches/System_Check/support.ino similarity index 63% rename from Firmware/Test Sketches/SD_Update/support.ino rename to Firmware/Test Sketches/System_Check/support.ino index 3719833f2..b042db5a0 100644 --- a/Firmware/Test Sketches/SD_Update/support.ino +++ b/Firmware/Test Sketches/System_Check/support.ino @@ -12,14 +12,60 @@ void printUnknown(int unknownValue) Serial.println(); } +//Clear the Serial RX buffer before we begin scanning for characters +void clearBuffer() +{ + Serial.flush(); + delay(20);//Wait for any incoming chars to hit buffer + while (Serial.available() > 0) Serial.read(); //Clear buffer +} + +//Get single byte from user +//Waits for and returns the character that the user provides +//Returns STATUS_GETNUMBER_TIMEOUT if input times out +//Returns 'x' if user presses 'x' +uint8_t getByteChoice(int numberOfSeconds) +{ + clearBuffer(); + + long startTime = millis(); + byte incoming; + while (1) + { + delay(10); //Yield to processor + if (online.gnss == true) + theGNSS.checkUblox(); //Regularly poll to get latest data + + if (pin_powerSenseAndControl >= 0) + { + if (digitalRead(pin_powerSenseAndControl) == LOW) + ESP.restart(); //Use the power button as a reset + } + + if (Serial.available() > 0) + { + incoming = Serial.read(); + if (incoming >= 'a' && incoming <= 'z') break; + if (incoming >= 'A' && incoming <= 'Z') break; + if (incoming >= '0' && incoming <= '9') break; + } + + if ( (millis() - startTime) / 1000 >= numberOfSeconds) + { + Serial.println(F("No user input received.")); + return (STATUS_GETBYTE_TIMEOUT); //Timeout. No user input. + } + } + + return (incoming); +} //Get a string/value from user, remove all non-numeric values //Returns STATUS_GETNUMBER_TIMEOUT if input times out //Returns STATUS_PRESSED_X if user presses 'x' int64_t getNumber(int numberOfSeconds) { - delay(10); //Wait for any incoming chars to hit buffer - while (Serial.available() > 0) Serial.read(); //Clear buffer + clearBuffer(); //Get input from user char cleansed[20]; //Good for very large numbers: 123,456,789,012,345,678\0 @@ -31,6 +77,8 @@ int64_t getNumber(int numberOfSeconds) while (Serial.available() == 0) //Wait for user input { delay(10); //Yield to processor + if (online.gnss == true) + theGNSS.checkUblox(); //Regularly poll to get latest data if ( (millis() - startTime) / 1000 >= numberOfSeconds) { diff --git a/Firmware/Test Sketches/Ticker/Ticker.ino b/Firmware/Test Sketches/Ticker/Ticker.ino deleted file mode 100644 index 70ef19db8..000000000 --- a/Firmware/Test Sketches/Ticker/Ticker.ino +++ /dev/null @@ -1,166 +0,0 @@ -/* - Use Ticker library to periodically check: - Blink BT LED - Check battery - Update display - Check GPS - - Update GPS data - We are going to use autoPVT and setAutoHPPOSLLH with Explicit Update. We will checkUblox() every 100ms. - This will cause all dataums (SIV, HPA, etc) to update regularly but when we call getSIV() we will not force wait for the most recent - data. This will also have the added benefit of regularly feeding processRTCM. Every checkUblox() call will pass any waiting RTCM - bytes to a future NTRIP server. - - Because tickers can interrupt other tickers, we use a mutex semphore for the I2C hardware. - - This implementation still has display errors for unknown reasons. Going to try xTasks next. -*/ - - -const int FIRMWARE_VERSION_MAJOR = 1; -const int FIRMWARE_VERSION_MINOR = 1; - -#include "settings.h" - -SemaphoreHandle_t xI2CSemaphore; -const int i2cSemaphore_maxWait = 5; //TickType_t - -//Hardware connections -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= -const int positionAccuracyLED_1cm = 2; -const int baseStatusLED = 4; -const int baseSwitch = 5; -const int bluetoothStatusLED = 12; -const int positionAccuracyLED_100cm = 13; -const int positionAccuracyLED_10cm = 15; -const byte PIN_MICROSD_CHIP_SELECT = 25; -const int zed_tx_ready = 26; -const int zed_reset = 27; -const int batteryLevelLED_Red = 32; -const int batteryLevelLED_Green = 33; -const int batteryLevel_alert = 36; -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - -//Low frequency tasks -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= -#include - -Ticker checkUbloxTask; -float checkUbloxTaskPace = 0.1; //Seconds - -Ticker btLEDTask; -float btLEDTaskPace = 0.5; //Seconds - -Ticker updateDisplayTask; -float updateDisplayTaskPace = 0.5; //Seconds - -Ticker battCheckTask; -float battCheckTaskPace = 2.0; //Seconds -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - -//External Display -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= -#include //Click here to get the library: http://librarymanager/All#SparkFun_Micro_OLED -#include "icons.h" - -#define PIN_RESET 9 -#define DC_JUMPER 1 -MicroOLED oled(PIN_RESET, DC_JUMPER); -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - -//GNSS configuration -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= -#define MAX_PAYLOAD_SIZE 384 // Override MAX_PAYLOAD_SIZE for getModuleInfo which can return up to 348 bytes - -#include "SparkFun_Ublox_Arduino_Library.h" //Click here to get the library: http://librarymanager/All#SparkFun_Ublox_GPS -//SFE_UBLOX_GPS myGPS; - -// Extend the class for getModuleInfo - See Example21_ModuleInfo -class SFE_UBLOX_GPS_ADD : public SFE_UBLOX_GPS -{ - public: - boolean getModuleInfo(uint16_t maxWait = 1100); //Queries module, texts - - struct minfoStructure // Structure to hold the module info (uses 341 bytes of RAM) - { - char swVersion[30]; - char hwVersion[10]; - uint8_t extensionNo = 0; - char extension[10][30]; - } minfo; -}; - -SFE_UBLOX_GPS_ADD myGPS; - -//This string is used to verify the firmware on the ZED-F9P. This -//firmware relies on various features of the ZED and may require the latest -//u-blox firmware to work correctly. We check the module firmware at startup but -//don't prevent operation if firmware is mismatched. -char latestZEDFirmware[] = "FWVER=HPG 1.13"; - -//Used for config ZED for things not supported in library: getPortSettings, getSerialRate, getNMEASettings, getRTCMSettings -//This array holds the payload data bytes. Global so that we can use between config functions. -uint8_t settingPayload[MAX_PAYLOAD_SIZE]; -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - - -//Battery fuel gauge and PWM LEDs -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= -#include // Click here to get the library: http://librarymanager/All#SparkFun_MAX1704x_Fuel_Gauge_Arduino_Library -SFE_MAX1704X lipo(MAX1704X_MAX17048); - -// setting PWM properties -const int freq = 5000; -const int ledRedChannel = 0; -const int ledGreenChannel = 1; -const int resolution = 8; - -int battLevel = 0; //SOC measured from fuel gauge, in %. Used in multiple places (display, serial debug, log) -float battVoltage = 0.0; -float battChangeRate = 0.0; - -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - - -void setup() -{ - Serial.begin(115200); - delay(100); - Serial.println("Testing tasks"); - - pinMode(bluetoothStatusLED, OUTPUT); - - Wire.begin(); - //Wire.setClock(100000); - - beginLEDs(); //LED and PWM setup - - beginDisplay(); //Check if an external Qwiic OLED is attached - - beginFuelGauge(); //Start I2C device - - beginGNSS(); //Connect and configure ZED-F9P - - bluetoothState = BT_ON_NOCONNECTION; - - //Wire.setClock(400000); //The battery and ublox modules seem to dislike 400kHz - - if (xI2CSemaphore == NULL) - { - xI2CSemaphore = xSemaphoreCreateMutex(); - if (xI2CSemaphore != NULL) - xSemaphoreGive(xI2CSemaphore); //Make the I2C hardware available for use - } - - //Start tasks - btLEDTask.attach(btLEDTaskPace, updateBTled); //Rate in seconds, callback - battCheckTask.attach(battCheckTaskPace, checkBatteryLevels); - checkUbloxTask.attach(checkUbloxTaskPace, checkUblox); //Call the checkUblox function - - if (online.display == true) - updateDisplayTask.attach(updateDisplayTaskPace, updateDisplay); - -} - -void loop() { - -} diff --git a/Firmware/Test Sketches/Ticker/begin.ino b/Firmware/Test Sketches/Ticker/begin.ino deleted file mode 100644 index 5c02a71f2..000000000 --- a/Firmware/Test Sketches/Ticker/begin.ino +++ /dev/null @@ -1,136 +0,0 @@ -//Connect to and configure ZED-F9P -void beginGNSS() -{ - if (myGPS.begin() == false) - { - //Try again with power on delay - delay(1000); //Wait for ZED-F9P to power up before it can respond to ACK - if (myGPS.begin() == false) - { - Serial.println(F("u-blox GNSS not detected at default I2C address. Hard stop.")); - while(1); - } - } - - //myGPS.enableDebugging(); - - bool response = true; - - //response &= myGPS.setAutoPVT(true); //Tell the GPS to "send" each solution - response &= myGPS.setAutoPVT(true, false); //Tell the GPS to "send" each solution, but do not update stale data when accessed - response &= myGPS.setAutoHPPOSLLH(true, false); //Tell the GPS to "send" each high res solution, but do not update stale data when accessed - //response &= myGPS.setAutoPVT(false); //Turn off PVT - - if(response == false) - { - Serial.println("PVT failed!"); - while(1); - } - - Serial.println(F("GNSS configuration complete")); -} - -//Configure the on board MAX17048 fuel gauge -void beginFuelGauge() -{ - // Set up the MAX17048 LiPo fuel gauge - if (lipo.begin() == false) - { - Serial.println(F("MAX17048 not detected. Continuing.")); - return; - } - - //Always use hibernate mode - if (lipo.getHIBRTActThr() < 0xFF) lipo.setHIBRTActThr((uint8_t)0xFF); - if (lipo.getHIBRTHibThr() < 0xFF) lipo.setHIBRTHibThr((uint8_t)0xFF); - - Serial.println(F("MAX17048 configuration complete")); -} - -void beginDisplay() -{ - //0x3D is default on Qwiic board - if (isConnected(0x3D) == true || isConnected(0x3C) == true) - { - online.display = true; - - //Init and display splash - oled.begin(); // Initialize the OLED - oled.clear(PAGE); // Clear the display's internal memory - - oled.setCursor(10, 2); //x, y - oled.setFontType(0); //Set font to smallest - oled.print("SparkFun"); - - oled.setCursor(21, 13); - oled.setFontType(1); - oled.print("RTK"); - - int surveyorTextY = 25; - int surveyorTextX = 2; - int surveyorTextKerning = 8; - oled.setFontType(1); - - oled.setCursor(surveyorTextX, surveyorTextY); - oled.print("S"); - - surveyorTextX += surveyorTextKerning; - oled.setCursor(surveyorTextX, surveyorTextY); - oled.print("u"); - - surveyorTextX += surveyorTextKerning; - oled.setCursor(surveyorTextX, surveyorTextY); - oled.print("r"); - - surveyorTextX += surveyorTextKerning; - oled.setCursor(surveyorTextX, surveyorTextY); - oled.print("v"); - - surveyorTextX += surveyorTextKerning; - oled.setCursor(surveyorTextX, surveyorTextY); - oled.print("e"); - - surveyorTextX += surveyorTextKerning; - oled.setCursor(surveyorTextX, surveyorTextY); - oled.print("y"); - - surveyorTextX += surveyorTextKerning; - oled.setCursor(surveyorTextX, surveyorTextY); - oled.print("o"); - - surveyorTextX += surveyorTextKerning; - oled.setCursor(surveyorTextX, surveyorTextY); - oled.print("r"); - - oled.setCursor(20, 41); - oled.setFontType(0); //Set font to smallest - oled.printf("v%d.%d", FIRMWARE_VERSION_MAJOR, FIRMWARE_VERSION_MINOR); - oled.display(); - } -} - -//Set LEDs for output and configure PWM -void beginLEDs() -{ - pinMode(positionAccuracyLED_1cm, OUTPUT); - pinMode(positionAccuracyLED_10cm, OUTPUT); - pinMode(positionAccuracyLED_100cm, OUTPUT); - pinMode(baseStatusLED, OUTPUT); - pinMode(bluetoothStatusLED, OUTPUT); - pinMode(baseSwitch, INPUT_PULLUP); //HIGH = rover, LOW = base - - digitalWrite(positionAccuracyLED_1cm, LOW); - digitalWrite(positionAccuracyLED_10cm, LOW); - digitalWrite(positionAccuracyLED_100cm, LOW); - digitalWrite(baseStatusLED, LOW); - digitalWrite(bluetoothStatusLED, LOW); - - ledcSetup(ledRedChannel, freq, resolution); - ledcSetup(ledGreenChannel, freq, resolution); - - ledcAttachPin(batteryLevelLED_Red, ledRedChannel); - ledcAttachPin(batteryLevelLED_Green, ledGreenChannel); - - ledcWrite(ledRedChannel, 0); - ledcWrite(ledGreenChannel, 0); -} diff --git a/Firmware/Test Sketches/Ticker/icons.h b/Firmware/Test Sketches/Ticker/icons.h deleted file mode 100644 index 55b5df44c..000000000 --- a/Firmware/Test Sketches/Ticker/icons.h +++ /dev/null @@ -1,68 +0,0 @@ -//Create a bitmap then use http://en.radzio.dxp.pl/bitmap_converter/ to generate output -//Make sure the bitmap is n*8 pixels tall (pad white pixels to lower area as needed) -//Otherwise the bitmap bitmap_converter will compress some of the bytes together - -uint8_t BT_Symbol [] = { -0x18, 0x30, 0xE0, 0xFF, 0xE6, 0x3C, 0x18, 0x06, 0x03, 0x01, 0x3F, 0x19, 0x0F, 0x06, -}; -int BT_Symbol_Height = 14; -int BT_Symbol_Width = 7; - -uint8_t CrossHair [] = { -0x80, 0x80, 0xF0, 0x88, 0x84, 0x84, 0x84, 0x7F, 0x84, 0x84, 0x84, 0x88, 0xF0, 0x80, 0x80, 0x00, -0x00, 0x07, 0x08, 0x10, 0x10, 0x10, 0x7F, 0x10, 0x10, 0x10, 0x08, 0x07, 0x00, 0x00, -}; -int CrossHair_Height = 15; -int CrossHair_Width = 15; - -uint8_t Antenna [] = { -0x7E, 0xC3, 0x03, 0x06, 0x04, 0x0C, 0x18, 0x38, 0x7C, 0xCE, 0x84, 0x00, 0x00, 0x01, 0x1F, 0x1E, -0x1C, 0x0C, 0x08, 0x08, 0x0C, 0x04, 0x07, 0x07, -}; -int Antenna_Height = 13; -int Antenna_Width = 12; - -uint8_t Rover [] = { -0x3C, 0x24, 0x64, 0xF4, 0xF7, 0x61, 0x21, 0x21, 0x21, 0x61, 0xF7, 0xF4, 0x64, 0x3C, 0x18, -}; -int Rover_Height = 8; -int Rover_Width = 15; - -uint8_t Base [] = { -0x00, 0xFF, 0x23, 0x13, 0x08, 0x88, 0x88, 0x88, 0x88, 0x08, 0x10, 0x20, 0xC0, 0x00, 0x0E, 0x09, -0x08, 0x08, 0x08, 0x0F, 0x00, 0x00, 0x0F, 0x08, 0x08, 0x08, 0x09, 0x0E, -}; -int Base_Height = 12; -int Base_Width = 14; - -uint8_t Battery_3 [] = { -0xFF, 0x01, 0xFD, 0xFD, 0xFD, 0x01, 0x01, 0xFD, 0xFD, 0xFD, 0x01, 0x01, 0xFD, 0xFD, 0xFD, 0x01, -0x0F, 0x08, 0xF8, 0x0F, 0x08, 0x0B, 0x0B, 0x0B, 0x08, 0x08, 0x0B, 0x0B, 0x0B, 0x08, 0x08, 0x0B, -0x0B, 0x0B, 0x08, 0x0F, 0x01, 0x01, -}; -int Battery_3_Height = 12; -int Battery_3_Width = 19; - -uint8_t Battery_2 [] = { -0xFF, 0x01, 0xFD, 0xFD, 0xFD, 0x01, 0x01, 0xFD, 0xFD, 0xFD, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, -0x0F, 0x08, 0xF8, 0x0F, 0x08, 0x0B, 0x0B, 0x0B, 0x08, 0x08, 0x0B, 0x0B, 0x0B, 0x08, 0x08, 0x08, -0x08, 0x08, 0x08, 0x0F, 0x01, 0x01, -}; -int Battery_2_Height = 12; -int Battery_2_Width = 19; - -uint8_t Battery_1 [] = { -0xFF, 0x01, 0xFD, 0xFD, 0xFD, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, -0x0F, 0x08, 0xF8, 0x0F, 0x08, 0x0B, 0x0B, 0x0B, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, -0x08, 0x08, 0x08, 0x0F, 0x01, 0x01, -}; -int Battery_1_Height = 12; -int Battery_1_Width = 19; - -uint8_t Battery_0 [] = { -0xFF, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, -0x0F, 0x08, 0xF8, 0x0F, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, -0x08, 0x08, 0x08, 0x0F, 0x01, 0x01, -}; -int Battery_0_Height = 12; -int Battery_0_Width = 19; diff --git a/Firmware/Test Sketches/Ticker/settings.h b/Firmware/Test Sketches/Ticker/settings.h deleted file mode 100644 index e8555a251..000000000 --- a/Firmware/Test Sketches/Ticker/settings.h +++ /dev/null @@ -1,17 +0,0 @@ -//Bluetooth status LED goes from off (LED off), no connection (blinking), to connected (solid) -enum BluetoothState -{ - BT_OFF = 0, - BT_ON_NOCONNECTION, - BT_CONNECTED, -}; -volatile byte bluetoothState = BT_OFF; - -//These are the devices on board RTK Surveyor that may be on or offline. -struct struct_online { - bool microSD = false; - bool display = false; - bool dataLogging = false; - bool serialOutput = false; - bool eeprom = false; -} online; diff --git a/Firmware/Test Sketches/Ticker/tasks.ino b/Firmware/Test Sketches/Ticker/tasks.ino deleted file mode 100644 index 68487a065..000000000 --- a/Firmware/Test Sketches/Ticker/tasks.ino +++ /dev/null @@ -1,176 +0,0 @@ -//These are all the low frequency tasks that are called by Ticker - -//Display battery level and various datums based on system state (is BT connected? are we in base mode? etc) -//This task is only activated if a display is detected at POR -void updateDisplay() -{ - long startTime = millis(); - - oled.clear(PAGE); // Clear the display's internal buffer - - //Current battery charge level - if (battLevel < 25) - oled.drawIcon(45, 0, Battery_0_Width, Battery_0_Height, Battery_0, sizeof(Battery_0), true); - else if (battLevel < 50) - oled.drawIcon(45, 0, Battery_1_Width, Battery_1_Height, Battery_1, sizeof(Battery_1), true); - else if (battLevel < 75) - oled.drawIcon(45, 0, Battery_2_Width, Battery_2_Height, Battery_2, sizeof(Battery_2), true); - else //batt level > 75 - oled.drawIcon(45, 0, Battery_3_Width, Battery_3_Height, Battery_3, sizeof(Battery_3), true); - - //Bluetooth Address or RSSI - if (bluetoothState == BT_CONNECTED) - { - oled.drawIcon(4, 0, BT_Symbol_Width, BT_Symbol_Height, BT_Symbol, sizeof(BT_Symbol), true); - } - else - { - char macAddress[5] = "BC4D"; - // sprintf(macAddress, "%02X%02X", unitMACAddress[4], unitMACAddress[5]); - oled.setFontType(0); //Set font to smallest - oled.setCursor(0, 4); - oled.print(macAddress); - } - - if (digitalRead(baseSwitch) == LOW) - oled.drawIcon(27, 0, Base_Width, Base_Height, Base, sizeof(Base), true); //true - blend with other pixels - else - oled.drawIcon(27, 3, Rover_Width, Rover_Height, Rover, sizeof(Rover), true); - - //Horz positional accuracy - oled.setFontType(1); //Set font to type 1: 8x16 - oled.drawIcon(0, 18, CrossHair_Width, CrossHair_Height, CrossHair, sizeof(CrossHair), true); - oled.setCursor(16, 20); //x, y - oled.print(":"); - float hpa = myGPS.getHorizontalAccuracy() / 10000.0; - // float hpa = 10000.0; - if (hpa > 30.0) - { - oled.print(">30"); - } - else if (hpa > 9.9) - { - oled.print(hpa, 1); //Print down to decimeter - } - else if (hpa > 1.0) - { - oled.print(hpa, 2); //Print down to centimeter - } - else - { - oled.print("."); //Remove leading zero - oled.printf("%03d", (int)(hpa * 1000)); //Print down to millimeter - } - - //SIV - oled.drawIcon(2, 35, Antenna_Width, Antenna_Height, Antenna, sizeof(Antenna), true); - oled.setCursor(16, 36); //x, y - oled.print(":"); - - if (myGPS.getFixType() == 0) //0 = No Fix - { - oled.print("0"); - } - else - { - oled.print(myGPS.getSIV()); - } - - //Check I2C semaphore - if (xSemaphoreTake(xI2CSemaphore, i2cSemaphore_maxWait) == pdPASS) - { - oled.display(); - xSemaphoreGive(xI2CSemaphore); - } - - //Serial.printf("Display time to update: %d\n", millis() - startTime); -} - -//AutoPVT without implicit updates is used -//This means we regularly (every 100ms) call checkUblox and all the global datums are updated as they are reported (4Hz default) -//If we ask for something like myGPS.getSIV(), there is no blocking wait, we simply get the last reported value -void checkUblox() -{ - long startTime = millis(); - //Check I2C semaphore - if (xSemaphoreTake(xI2CSemaphore, i2cSemaphore_maxWait) == pdPASS) - { - myGPS.checkUblox(); - xSemaphoreGive(xI2CSemaphore); - } - //Serial.printf("GPS time to update: %d\n", millis() - startTime); -} - -//Control BT status LED according to bluetoothState -void updateBTled() -{ - if (bluetoothState == BT_ON_NOCONNECTION) - digitalWrite(bluetoothStatusLED, !digitalRead(bluetoothStatusLED)); - else if (bluetoothState == BT_CONNECTED) - digitalWrite(bluetoothStatusLED, HIGH); - else - digitalWrite(bluetoothStatusLED, LOW); -} - -//When called, checks level of battery and updates the LED brightnesses -//And outputs a serial message to USB and BT -void checkBatteryLevels() -{ - String battMsg = ""; - - long startTime = millis(); - - //Check I2C semaphore - if (xSemaphoreTake(xI2CSemaphore, i2cSemaphore_maxWait) == pdPASS) - { - battLevel = lipo.getSOC(); - battVoltage = lipo.getVoltage(); - battChangeRate = lipo.getChangeRate(); - xSemaphoreGive(xI2CSemaphore); - } - //Serial.printf("Batt time to update: %d\n", millis() - startTime); - - battMsg += "Batt ("; - battMsg += battLevel; - battMsg += "%): "; - - battMsg += "Voltage: "; - battMsg += battVoltage; - battMsg += "V"; - - if (battChangeRate > 0) - battMsg += " Charging: "; - else - battMsg += " Discharging: "; - battMsg += battChangeRate; - battMsg += "%/hr "; - - if (battLevel < 10) - { - battMsg += "RED uh oh!"; - ledcWrite(ledRedChannel, 255); - ledcWrite(ledGreenChannel, 0); - } - else if (battLevel < 50) - { - battMsg += "Yellow ok"; - ledcWrite(ledRedChannel, 128); - ledcWrite(ledGreenChannel, 128); - } - else if (battLevel >= 50) - { - battMsg += "Green all good"; - ledcWrite(ledRedChannel, 0); - ledcWrite(ledGreenChannel, 255); - } - else - { - battMsg += "No batt"; - ledcWrite(ledRedChannel, 10); - ledcWrite(ledGreenChannel, 0); - } - battMsg += "\n\r"; - //SerialBT.print(battMsg); - Serial.print(battMsg); - -} diff --git a/Firmware/Test Sketches/WebServer/Begin.ino b/Firmware/Test Sketches/WebServer/Begin.ino new file mode 100644 index 000000000..ccc233327 --- /dev/null +++ b/Firmware/Test Sketches/WebServer/Begin.ino @@ -0,0 +1,136 @@ +const int pin_microSD_CS = 25; + +typedef struct struct_settings { + bool enableSD = true; + uint16_t spiFrequency = 16; //By default, use 16MHz SPI +} Settings; + +Settings settings; + +const TickType_t fatSemaphore_shortWait_ms = 10 / portTICK_PERIOD_MS; +const TickType_t fatSemaphore_longWait_ms = 200 / portTICK_PERIOD_MS; +SemaphoreHandle_t xFATSemaphore; + +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +void displaySDFail(uint16_t displayTime) +{ + Serial.println ("SD card failed to initialize!"); +} + +//Create a test file in file structure to make sure we can +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +bool createTestFile() +{ + SdFile testFile; + char testFileName[40] = "testfile.txt"; + + if (xFATSemaphore == NULL) + { + log_d("xFATSemaphore is Null"); + return (false); + } + + //Attempt to write to file system. This avoids collisions with file writing from other functions like recordSystemSettingsToFile() and F9PSerialReadTask() + if (xSemaphoreTake(xFATSemaphore, fatSemaphore_shortWait_ms) == pdPASS) + { + if (testFile.open(testFileName, O_CREAT | O_APPEND | O_WRITE) == true) + { + testFile.close(); + + if (sd.exists(testFileName)) + sd.remove(testFileName); + xSemaphoreGive(xFATSemaphore); + return (true); + } + xSemaphoreGive(xFATSemaphore); + } + + return (false); +} + +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +void beginSD() +{ + pinMode(pin_microSD_CS, OUTPUT); + digitalWrite(pin_microSD_CS, HIGH); //Be sure SD is deselected + + if (settings.enableSD == true) + { + //Do a quick test to see if a card is present + int tries = 0; + int maxTries = 5; + while (tries < maxTries) + { + if (sdPresent() == true) break; + log_d("SD present failed. Trying again %d out of %d", tries + 1, maxTries); + + //Max power up time is 250ms: https://www.kingston.com/datasheets/SDCIT-specsheet-64gb_en.pdf + //Max current is 200mA average across 1s, peak 300mA + delay(10); + tries++; + } + if (tries == maxTries) return; + + //If an SD card is present, allow SdFat to take over + log_d("SD card detected"); + + if (settings.spiFrequency > 16) + { + Serial.println("Error: SPI Frequency out of range. Default to 16MHz"); + settings.spiFrequency = 16; + } + + if (sd.begin(SdSpiConfig(pin_microSD_CS, SHARED_SPI, SD_SCK_MHZ(settings.spiFrequency))) == false) + { + tries = 0; + maxTries = 1; + for ( ; tries < maxTries ; tries++) + { + log_d("SD init failed. Trying again %d out of %d", tries + 1, maxTries); + + delay(250); //Give SD more time to power up, then try again + if (sd.begin(SdSpiConfig(pin_microSD_CS, SHARED_SPI, SD_SCK_MHZ(settings.spiFrequency))) == true) break; + } + + if (tries == maxTries) + { + Serial.println(F("SD init failed. Is card present? Formatted?")); + digitalWrite(pin_microSD_CS, HIGH); //Be sure SD is deselected + online.microSD = false; + return; + } + } + + //Change to root directory. All new file creation will be in root. + if (sd.chdir() == false) + { + Serial.println(F("SD change directory failed")); + online.microSD = false; + return; + } + + //Setup FAT file access semaphore + if (xFATSemaphore == NULL) + { + xFATSemaphore = xSemaphoreCreateMutex(); + if (xFATSemaphore != NULL) + xSemaphoreGive(xFATSemaphore); //Make the file system available for use + } + + if (createTestFile() == false) + { + Serial.println(F("Failed to create test file. Format SD card with 'SD Card Formatter'.")); + displaySDFail(5000); + online.microSD = false; + return; + } + + online.microSD = true; + + Serial.println(F("microSD online")); + } + else + { + online.microSD = false; + } +} diff --git a/Firmware/Test Sketches/WebServer/Print.ino b/Firmware/Test Sketches/WebServer/Print.ino new file mode 100644 index 000000000..0bef82fbc --- /dev/null +++ b/Firmware/Test Sketches/WebServer/Print.ino @@ -0,0 +1,64 @@ +void printIpAddress(IPAddress ip) { + + // Display the IP address + Serial.println(ip); +} + +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +void printWiFiGatewayIp() { + + // Display the subnet mask + Serial.print ("Gateway: "); + IPAddress ip = WiFi.gatewayIP(); + printIpAddress(ip); +} + +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +void printWiFiIpAddress() { + + // Display the IP address + Serial.print ("IP Address: "); + IPAddress ip = WiFi.localIP(); + printIpAddress(ip); +} + +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +void printWiFiMacAddress() { + // Display the MAC address + byte mac[6]; + WiFi.macAddress(mac); + Serial.print("MAC address: "); + Serial.print(mac[5], HEX); + Serial.print(":"); + Serial.print(mac[4], HEX); + Serial.print(":"); + Serial.print(mac[3], HEX); + Serial.print(":"); + Serial.print(mac[2], HEX); + Serial.print(":"); + Serial.print(mac[1], HEX); + Serial.print(":"); + Serial.println(mac[0], HEX); +} + +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +void printWiFiNetwork() { + + // Display the SSID + Serial.print("SSID: "); + Serial.println(WiFi.SSID()); + + // Display the receive signal strength + long rssi = WiFi.RSSI(); + Serial.print("Signal strength (RSSI):"); + Serial.println(rssi); +} + +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +void printWiFiSubnetMask() { + + // Display the subnet mask + Serial.print ("Subnet Mask: "); + IPAddress ip = WiFi.subnetMask(); + printIpAddress(ip); +} diff --git a/Firmware/Test Sketches/WebServer/SD.ino b/Firmware/Test Sketches/WebServer/SD.ino new file mode 100644 index 000000000..4d2692a68 --- /dev/null +++ b/Firmware/Test Sketches/WebServer/SD.ino @@ -0,0 +1,150 @@ +/* + These are low level functions to aid in detecting whether a card is present or not. + Because of ESP32 v2 core, SdFat can only operate using Shared SPI. This makes the sd.begin test take over 1s + which causes the RTK product to boot slowly. To circumvent this, we will ping the SD card directly to see if it responds. + Failures take 2ms, successes take 1ms. + + From Prototype puzzle: https://github.com/sparkfunX/ThePrototype/blob/master/Firmware/TestSketches/sdLocker/sdLocker.ino + License: Public domain. This code is based on Karl Lunt's work: https://www.seanet.com/~karllunt/sdlocker2.html +*/ + +//Define commands for the SD card +#define SD_GO_IDLE (0x40 + 0) // CMD0 - go to idle state +#define SD_INIT (0x40 + 1) // CMD1 - start initialization +#define SD_SEND_IF_COND (0x40 + 8) // CMD8 - send interface (conditional), works for SDHC only +#define SD_SEND_STATUS (0x40 + 13) // CMD13 - send card status +#define SD_SET_BLK_LEN (0x40 + 16) // CMD16 - set length of block in bytes +#define SD_LOCK_UNLOCK (0x40 + 42) // CMD42 - lock/unlock card +#define CMD55 (0x40 + 55) // multi-byte preface command +#define SD_READ_OCR (0x40 + 58) // read OCR +#define SD_ADV_INIT (0xc0 + 41) // ACMD41, for SDHC cards - advanced start initialization + +//Define options for accessing the SD card's PWD (CMD42) +#define MASK_ERASE 0x08 //erase the entire card +#define MASK_LOCK_UNLOCK 0x04 //lock or unlock the card with password +#define MASK_CLR_PWD 0x02 //clear password +#define MASK_SET_PWD 0x01 //set password + +//Define bit masks for fields in the lock/unlock command (CMD42) data structure +#define SET_PWD_MASK (1<<0) +#define CLR_PWD_MASK (1<<1) +#define LOCK_UNLOCK_MASK (1<<2) +#define ERASE_MASK (1<<3) + +//Begin initialization by sending CMD0 and waiting until SD card +//responds with In Idle Mode (0x01). If the response is not 0x01 +//within a reasonable amount of time, there is no SD card on the bus. +//Returns false if not card is detected +//Returns true if a card responds +bool sdPresent(void) +{ + byte response = 0; + + SPI.begin(); + SPI.setClockDivider(SPI_CLOCK_DIV2); + SPI.setDataMode(SPI_MODE0); + SPI.setBitOrder(MSBFIRST); + pinMode(pin_microSD_CS, OUTPUT); + + //Sending clocks while card power stabilizes... + deselectCard(); // always make sure + for (byte i = 0; i < 30; i++) // send several clocks while card power stabilizes + xchg(0xff); + + //Sending CMD0 - GO IDLE... + for (byte i = 0; i < 0x10; i++) //Attempt to go idle + { + response = sdSendCommand(SD_GO_IDLE, 0); // send CMD0 - go to idle state + if (response == 1) break; + } + if (response != 1) return (false); //Card failed to respond to idle + + return (true); +} + +/* + sdSendCommand send raw command to SD card, return response + + This routine accepts a single SD command and a 4-byte argument. It sends + the command plus argument, adding the appropriate CRC. It then returns + the one-byte response from the SD card. + + For advanced commands (those with a command byte having bit 7 set), this + routine automatically sends the required preface command (CMD55) before + sending the requested command. + + Upon exit, this routine returns the response byte from the SD card. + Possible responses are: + 0xff No response from card; card might actually be missing + 0x01 SD card returned 0x01, which is OK for most commands + 0x?? other responses are command-specific +*/ +byte sdSendCommand(byte command, unsigned long arg) +{ + byte response; + + if (command & 0x80) // special case, ACMD(n) is sent as CMD55 and CMDn + { + command &= 0x7f; // strip high bit for later + response = sdSendCommand(CMD55, 0); // send first part (recursion) + if (response > 1) return (response); + } + + deselectCard(); + xchg(0xFF); + selectCard(); // enable CS + xchg(0xFF); + + xchg(command | 0x40); // command always has bit 6 set! + xchg((byte)(arg >> 24)); // send data, starting with top byte + xchg((byte)(arg >> 16)); + xchg((byte)(arg >> 8)); + xchg((byte)(arg & 0xFF)); + + byte crc = 0x01; // good for most cases + if (command == SD_GO_IDLE) crc = 0x95; // this will be good enough for most commands + if (command == SD_SEND_IF_COND) crc = 0x87; // special case, have to use different CRC + xchg(crc); // send final byte + + for (int i = 0; i < 30; i++) // loop until timeout or response + { + response = xchg(0xFF); + if ((response & 0x80) == 0) break; // high bit cleared means we got a response + } + + /* + We have issued the command but the SD card is still selected. We + only deselectCard the card if the command we just sent is NOT a command + that requires additional data exchange, such as reading or writing + a block. + */ + if ((command != SD_READ_OCR) && + (command != SD_SEND_STATUS) && + (command != SD_SEND_IF_COND) && + (command != SD_LOCK_UNLOCK)) + { + deselectCard(); // all done + xchg(0xFF); // close with eight more clocks + } + + return (response); // let the caller sort it out +} + +//Select (enable) the SD card +void selectCard(void) +{ + digitalWrite(pin_microSD_CS, LOW); +} + +//Deselect (disable) the SD card +void deselectCard(void) +{ + digitalWrite(pin_microSD_CS, HIGH); +} + +//Exchange a byte of data with the SD card via host's SPI bus +byte xchg(byte val) +{ + byte receivedVal = SPI.transfer(val); + return receivedVal; +} diff --git a/Firmware/Test Sketches/WebServer/WebServer.ino b/Firmware/Test Sketches/WebServer/WebServer.ino new file mode 100644 index 000000000..e7ff53bb2 --- /dev/null +++ b/Firmware/Test Sketches/WebServer/WebServer.ino @@ -0,0 +1,162 @@ +/* + Web server test program +*/ + +#include //Get from: https://github.com/me-no-dev/ESPAsyncWebServer +#include //Get v1.0 from: https://github.com/LeeLeahy2/SdCardServer +#include +#include +#include + +#include "SdFat.h" //http://librarymanager/All#sdfat_exfat by Bill Greiman. Currently uses v2.1.1 + +#define ASCII_LF 0x0a +#define ASCII_CR 0x0d + +int keyIndex = 0; +char password[1024]; // WiFi network password +char ssid[1024]; // WiFi network name + +typedef struct struct_online { + bool microSD = false; +} Online; + +Online online; +SdFat sd; +int sdCardMounted; +AsyncWebServer server(80); +int status = WL_IDLE_STATUS; // the Wifi radio's status +int wifiBeginCalled; +int wifiConnected; + +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +int mountSdCard(void) { + // Mount the SD card + if (!sdCardMounted) { + beginSD(); + if (online.microSD) { + sdCardMounted = true; + sd.volumeBegin(); + } + } + return sdCardMounted; +} + +SdCardServer sdCardServer(&sd, mountSdCard, "/sd/", "SD Card Files"); + +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +void setup() { + char data; + int length; + + // Initialize serial and wait for port to open: + Serial.begin(115200); + while (!Serial); // Wait for native USB serial port to connect + Serial.println("\n"); + + // Read the WiFi network name (SSID) + length = 0; + do { + Serial.println("Please enter the WiFi network name (SSID):"); + do { + while (!Serial.available()); + data = Serial.read(); + if ((data == ASCII_LF) || (data == ASCII_CR)) + break; + ssid[length++] = data; + } while (1); + ssid[length] = 0; + } while (!length); + Serial.printf("SSID: %s\n", ssid); + Serial.println(); + + // Read the WiFi network password + length = 0; + do { + Serial.println("Please enter the WiFi network password:"); + do { + while (!Serial.available()); + data = Serial.read(); + if ((data == ASCII_LF) || (data == ASCII_CR)) + break; + password[length++] = data; + } while (1); + password[length] = 0; + } while (!length); + Serial.printf("Password: %s\n", password); + Serial.println(); + + // The SD card needs to be mounted + sdCardMounted = 0; + online.microSD = 0; + if (mountSdCard()) + Serial.println(); + + // Wait for WiFi connection + wifiBeginCalled = false; + wifiConnected = false; +} + +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +void loop() { + + // Determine the WiFi status + status = WiFi.status(); + switch (status) { + default: + Serial.printf("Unknown WiFi status: %d\n", status); + delay (100); + break; + + case WL_DISCONNECTED: + case WL_IDLE_STATUS: + case WL_NO_SHIELD: + case WL_SCAN_COMPLETED: + break; + + case WL_NO_SSID_AVAIL: + Serial.println("Please set SSID and pass values!\n"); + while (1); + + case WL_CONNECTED: + if (!wifiConnected) { + wifiConnected = true; + Serial.println("WiFi Connected"); + + // Display the WiFi info + printWiFiNetwork(); + printWiFiIpAddress(); + printWiFiSubnetMask(); + printWiFiGatewayIp(); + + // index.html + sdCardServer.sdCardWebSite(&server); + + // All other pages + sdCardServer.onNotFound(&server); + + // Start server + server.begin(); + } + break; + + case WL_CONNECTION_LOST: + Serial.println("WiFi connection lost"); + WiFi.disconnect(); + wifiBeginCalled = false; + wifiConnected = false; + break; + + case WL_CONNECT_FAILED: + wifiBeginCalled = false; + wifiConnected = false; + break;; + } + + // Attempt to connect to Wifi network + if (!wifiBeginCalled) { + wifiBeginCalled = true; + WiFi.begin(ssid, password); + Serial.println("Waiting for WiFi connection..."); + } +} diff --git a/Firmware/Test Sketches/WiFiConnect/WiFiConnect.ino b/Firmware/Test Sketches/WiFiConnect/WiFiConnect.ino new file mode 100644 index 000000000..65d99fdc2 --- /dev/null +++ b/Firmware/Test Sketches/WiFiConnect/WiFiConnect.ino @@ -0,0 +1,184 @@ +/* + Verify connection to the specified WiFi network +*/ + +#include +#include + +#define ASCII_LF 0x0a +#define ASCII_CR 0x0d + +int keyIndex = 0; +char password[1024]; // WiFi network password +char ssid[1024]; // WiFi network name +int status = WL_IDLE_STATUS; // the WiFi radio's status +int wifiBeginCalled; +int wifiConnected; + +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +void displayIpAddress(IPAddress ip) { + + // Display the IP address + Serial.println(ip); +} + +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +void displayWiFiGatewayIp() { + + // Display the subnet mask + Serial.print ("Gateway: "); + IPAddress ip = WiFi.gatewayIP(); + displayIpAddress(ip); +} + +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +void displayWiFiIpAddress() { + + // Display the IP address + Serial.print ("IP Address: "); + IPAddress ip = WiFi.localIP(); + displayIpAddress(ip); +} + +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +void displayWiFiMacAddress() { + // Display the MAC address + byte mac[6]; + WiFi.macAddress(mac); + Serial.print("MAC address: "); + Serial.print(mac[5], HEX); + Serial.print(":"); + Serial.print(mac[4], HEX); + Serial.print(":"); + Serial.print(mac[3], HEX); + Serial.print(":"); + Serial.print(mac[2], HEX); + Serial.print(":"); + Serial.print(mac[1], HEX); + Serial.print(":"); + Serial.println(mac[0], HEX); +} + +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +void displayWiFiNetwork() { + + // Display the SSID + Serial.print("SSID: "); + Serial.println(WiFi.SSID()); + + // Display the receive signal strength + long rssi = WiFi.RSSI(); + Serial.print("Signal strength (RSSI):"); + Serial.println(rssi); +} + +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +void displayWiFiSubnetMask() { + + // Display the subnet mask + Serial.print ("Subnet Mask: "); + IPAddress ip = WiFi.subnetMask(); + displayIpAddress(ip); +} + +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +void setup() { + char data; + int length; + + // Initialize serial and wait for port to open: + Serial.begin(115200); + while (!Serial); // Wait for native USB serial port to connect + + // Wait for WiFi connection + wifiBeginCalled = false; + wifiConnected = false; + Serial.println(); + + // Read the WiFi network name (SSID) + length = 0; + do { + Serial.println("Please enter the WiFi network name (SSID):"); + do { + while (!Serial.available()); + data = Serial.read(); + if ((data == ASCII_LF) || (data == ASCII_CR)) + break; + ssid[length++] = data; + } while (1); + ssid[length] = 0; + } while (!length); + Serial.printf("SSID: %s\n", ssid); + Serial.println(); + + // Read the WiFi network password + length = 0; + do { + Serial.println("Please enter the WiFi network password:"); + do { + while (!Serial.available()); + data = Serial.read(); + if ((data == ASCII_LF) || (data == ASCII_CR)) + break; + password[length++] = data; + } while (1); + password[length] = 0; + } while (!length); + Serial.printf("Password: %s\n", password); + Serial.println(); +} + +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +void loop() { + + // Determine the WiFi status + status = WiFi.status(); + switch (status) { + default: + Serial.printf("Unknown WiFi status: %d\n", status); + delay (100); + break; + + case WL_DISCONNECTED: + case WL_IDLE_STATUS: + case WL_NO_SHIELD: + case WL_SCAN_COMPLETED: + break; + + case WL_NO_SSID_AVAIL: + Serial.println("Please set SSID and pass values!\n"); + while (1); + + case WL_CONNECTED: + if (!wifiConnected) { + wifiConnected = true; + Serial.println("WiFi Connected"); + + // Display the WiFi info + displayWiFiNetwork(); + displayWiFiIpAddress(); + displayWiFiSubnetMask(); + displayWiFiGatewayIp(); + } + break; + + case WL_CONNECTION_LOST: + Serial.println("WiFi connection lost"); + WiFi.disconnect(); + wifiBeginCalled = false; + wifiConnected = false; + break; + + case WL_CONNECT_FAILED: + wifiBeginCalled = false; + wifiConnected = false; + break;; + } + + // Attempt to connect to Wifi network + if (!wifiBeginCalled) { + wifiBeginCalled = true; + WiFi.begin(ssid, password); + Serial.println("Waiting for WiFi connection..."); + } +} diff --git a/Firmware/Test Sketches/xTask/System.ino b/Firmware/Test Sketches/xTask/System.ino deleted file mode 100644 index a3272665e..000000000 --- a/Firmware/Test Sketches/xTask/System.ino +++ /dev/null @@ -1,8 +0,0 @@ -//Ping an I2C device and see if it responds -bool isConnected(uint8_t deviceAddress) -{ - Wire.beginTransmission(deviceAddress); - if (Wire.endTransmission() == 0) - return true; - return false; -} diff --git a/Firmware/Test Sketches/xTask/begin.ino b/Firmware/Test Sketches/xTask/begin.ino deleted file mode 100644 index b9fa2686b..000000000 --- a/Firmware/Test Sketches/xTask/begin.ino +++ /dev/null @@ -1,136 +0,0 @@ -//Connect to and configure ZED-F9P -void beginGNSS() -{ - if (myGPS.begin() == false) - { - //Try again with power on delay - delay(1000); //Wait for ZED-F9P to power up before it can respond to ACK - if (myGPS.begin() == false) - { - Serial.println(F("u-blox GNSS not detected at default I2C address. Hard stop.")); - while(1); - } - } - - //myGPS.enableDebugging(); - - bool response = true; - - response &= myGPS.setAutoPVT(true, false); //Tell the GPS to "send" each solution, but do not update stale data when accessed - response &= myGPS.setAutoHPPOSLLH(true, false); //Tell the GPS to "send" each high res solution, but do not update stale data when accessed - - response &= myGPS.setNavigationFrequency(4); //Set output in Hz - - if(response == false) - { - Serial.println("PVT failed!"); - while(1); - } - - Serial.println(F("GNSS configuration complete")); -} - -//Configure the on board MAX17048 fuel gauge -void beginFuelGauge() -{ - // Set up the MAX17048 LiPo fuel gauge - if (lipo.begin() == false) - { - Serial.println(F("MAX17048 not detected. Continuing.")); - return; - } - - //Always use hibernate mode - if (lipo.getHIBRTActThr() < 0xFF) lipo.setHIBRTActThr((uint8_t)0xFF); - if (lipo.getHIBRTHibThr() < 0xFF) lipo.setHIBRTHibThr((uint8_t)0xFF); - - Serial.println(F("MAX17048 configuration complete")); -} - -void beginDisplay() -{ - //0x3D is default on Qwiic board - if (isConnected(0x3D) == true || isConnected(0x3C) == true) - { - online.display = true; - - //Init and display splash - oled.begin(); // Initialize the OLED - oled.clear(PAGE); // Clear the display's internal memory - - oled.setCursor(10, 2); //x, y - oled.setFontType(0); //Set font to smallest - oled.print("SparkFun"); - - oled.setCursor(21, 13); - oled.setFontType(1); - oled.print("RTK"); - - int surveyorTextY = 25; - int surveyorTextX = 2; - int surveyorTextKerning = 8; - oled.setFontType(1); - - oled.setCursor(surveyorTextX, surveyorTextY); - oled.print("S"); - - surveyorTextX += surveyorTextKerning; - oled.setCursor(surveyorTextX, surveyorTextY); - oled.print("u"); - - surveyorTextX += surveyorTextKerning; - oled.setCursor(surveyorTextX, surveyorTextY); - oled.print("r"); - - surveyorTextX += surveyorTextKerning; - oled.setCursor(surveyorTextX, surveyorTextY); - oled.print("v"); - - surveyorTextX += surveyorTextKerning; - oled.setCursor(surveyorTextX, surveyorTextY); - oled.print("e"); - - surveyorTextX += surveyorTextKerning; - oled.setCursor(surveyorTextX, surveyorTextY); - oled.print("y"); - - surveyorTextX += surveyorTextKerning; - oled.setCursor(surveyorTextX, surveyorTextY); - oled.print("o"); - - surveyorTextX += surveyorTextKerning; - oled.setCursor(surveyorTextX, surveyorTextY); - oled.print("r"); - - oled.setCursor(20, 41); - oled.setFontType(0); //Set font to smallest - oled.printf("v%d.%d", FIRMWARE_VERSION_MAJOR, FIRMWARE_VERSION_MINOR); - oled.display(); - } -} - -//Set LEDs for output and configure PWM -void beginLEDs() -{ - pinMode(positionAccuracyLED_1cm, OUTPUT); - pinMode(positionAccuracyLED_10cm, OUTPUT); - pinMode(positionAccuracyLED_100cm, OUTPUT); - pinMode(baseStatusLED, OUTPUT); - pinMode(bluetoothStatusLED, OUTPUT); - pinMode(baseSwitch, INPUT_PULLUP); //HIGH = rover, LOW = base - - digitalWrite(positionAccuracyLED_1cm, LOW); - digitalWrite(positionAccuracyLED_10cm, LOW); - digitalWrite(positionAccuracyLED_100cm, LOW); - digitalWrite(baseStatusLED, LOW); - digitalWrite(bluetoothStatusLED, LOW); - - ledcSetup(ledRedChannel, freq, resolution); - ledcSetup(ledGreenChannel, freq, resolution); - - ledcAttachPin(batteryLevelLED_Red, ledRedChannel); - ledcAttachPin(batteryLevelLED_Green, ledGreenChannel); - - ledcWrite(ledRedChannel, 0); - ledcWrite(ledGreenChannel, 0); -} diff --git a/Firmware/Test Sketches/xTask/icons.h b/Firmware/Test Sketches/xTask/icons.h deleted file mode 100644 index 55b5df44c..000000000 --- a/Firmware/Test Sketches/xTask/icons.h +++ /dev/null @@ -1,68 +0,0 @@ -//Create a bitmap then use http://en.radzio.dxp.pl/bitmap_converter/ to generate output -//Make sure the bitmap is n*8 pixels tall (pad white pixels to lower area as needed) -//Otherwise the bitmap bitmap_converter will compress some of the bytes together - -uint8_t BT_Symbol [] = { -0x18, 0x30, 0xE0, 0xFF, 0xE6, 0x3C, 0x18, 0x06, 0x03, 0x01, 0x3F, 0x19, 0x0F, 0x06, -}; -int BT_Symbol_Height = 14; -int BT_Symbol_Width = 7; - -uint8_t CrossHair [] = { -0x80, 0x80, 0xF0, 0x88, 0x84, 0x84, 0x84, 0x7F, 0x84, 0x84, 0x84, 0x88, 0xF0, 0x80, 0x80, 0x00, -0x00, 0x07, 0x08, 0x10, 0x10, 0x10, 0x7F, 0x10, 0x10, 0x10, 0x08, 0x07, 0x00, 0x00, -}; -int CrossHair_Height = 15; -int CrossHair_Width = 15; - -uint8_t Antenna [] = { -0x7E, 0xC3, 0x03, 0x06, 0x04, 0x0C, 0x18, 0x38, 0x7C, 0xCE, 0x84, 0x00, 0x00, 0x01, 0x1F, 0x1E, -0x1C, 0x0C, 0x08, 0x08, 0x0C, 0x04, 0x07, 0x07, -}; -int Antenna_Height = 13; -int Antenna_Width = 12; - -uint8_t Rover [] = { -0x3C, 0x24, 0x64, 0xF4, 0xF7, 0x61, 0x21, 0x21, 0x21, 0x61, 0xF7, 0xF4, 0x64, 0x3C, 0x18, -}; -int Rover_Height = 8; -int Rover_Width = 15; - -uint8_t Base [] = { -0x00, 0xFF, 0x23, 0x13, 0x08, 0x88, 0x88, 0x88, 0x88, 0x08, 0x10, 0x20, 0xC0, 0x00, 0x0E, 0x09, -0x08, 0x08, 0x08, 0x0F, 0x00, 0x00, 0x0F, 0x08, 0x08, 0x08, 0x09, 0x0E, -}; -int Base_Height = 12; -int Base_Width = 14; - -uint8_t Battery_3 [] = { -0xFF, 0x01, 0xFD, 0xFD, 0xFD, 0x01, 0x01, 0xFD, 0xFD, 0xFD, 0x01, 0x01, 0xFD, 0xFD, 0xFD, 0x01, -0x0F, 0x08, 0xF8, 0x0F, 0x08, 0x0B, 0x0B, 0x0B, 0x08, 0x08, 0x0B, 0x0B, 0x0B, 0x08, 0x08, 0x0B, -0x0B, 0x0B, 0x08, 0x0F, 0x01, 0x01, -}; -int Battery_3_Height = 12; -int Battery_3_Width = 19; - -uint8_t Battery_2 [] = { -0xFF, 0x01, 0xFD, 0xFD, 0xFD, 0x01, 0x01, 0xFD, 0xFD, 0xFD, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, -0x0F, 0x08, 0xF8, 0x0F, 0x08, 0x0B, 0x0B, 0x0B, 0x08, 0x08, 0x0B, 0x0B, 0x0B, 0x08, 0x08, 0x08, -0x08, 0x08, 0x08, 0x0F, 0x01, 0x01, -}; -int Battery_2_Height = 12; -int Battery_2_Width = 19; - -uint8_t Battery_1 [] = { -0xFF, 0x01, 0xFD, 0xFD, 0xFD, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, -0x0F, 0x08, 0xF8, 0x0F, 0x08, 0x0B, 0x0B, 0x0B, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, -0x08, 0x08, 0x08, 0x0F, 0x01, 0x01, -}; -int Battery_1_Height = 12; -int Battery_1_Width = 19; - -uint8_t Battery_0 [] = { -0xFF, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, -0x0F, 0x08, 0xF8, 0x0F, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, -0x08, 0x08, 0x08, 0x0F, 0x01, 0x01, -}; -int Battery_0_Height = 12; -int Battery_0_Width = 19; diff --git a/Firmware/Test Sketches/xTask/settings.h b/Firmware/Test Sketches/xTask/settings.h deleted file mode 100644 index e8555a251..000000000 --- a/Firmware/Test Sketches/xTask/settings.h +++ /dev/null @@ -1,17 +0,0 @@ -//Bluetooth status LED goes from off (LED off), no connection (blinking), to connected (solid) -enum BluetoothState -{ - BT_OFF = 0, - BT_ON_NOCONNECTION, - BT_CONNECTED, -}; -volatile byte bluetoothState = BT_OFF; - -//These are the devices on board RTK Surveyor that may be on or offline. -struct struct_online { - bool microSD = false; - bool display = false; - bool dataLogging = false; - bool serialOutput = false; - bool eeprom = false; -} online; diff --git a/Firmware/Test Sketches/xTask/tasks.ino b/Firmware/Test Sketches/xTask/tasks.ino deleted file mode 100644 index a7565f304..000000000 --- a/Firmware/Test Sketches/xTask/tasks.ino +++ /dev/null @@ -1,196 +0,0 @@ -//These are all the low frequency tasks that are run via the xTask scheduler - -//Display battery level and various datums based on system state (is BT connected? are we in base mode? etc) -//This task is only activated if a display is detected at POR -void updateDisplayTask(void *e) -//void updateDisplayTask() -{ - while (true) - { - long startTime = millis(); - - oled.clear(PAGE); // Clear the display's internal buffer - - //Current battery charge level - if (battLevel < 25) - oled.drawIcon(45, 0, Battery_0_Width, Battery_0_Height, Battery_0, sizeof(Battery_0), true); - else if (battLevel < 50) - oled.drawIcon(45, 0, Battery_1_Width, Battery_1_Height, Battery_1, sizeof(Battery_1), true); - else if (battLevel < 75) - oled.drawIcon(45, 0, Battery_2_Width, Battery_2_Height, Battery_2, sizeof(Battery_2), true); - else //batt level > 75 - oled.drawIcon(45, 0, Battery_3_Width, Battery_3_Height, Battery_3, sizeof(Battery_3), true); - - //Bluetooth Address or RSSI - if (bluetoothState == BT_CONNECTED) - { - oled.drawIcon(4, 0, BT_Symbol_Width, BT_Symbol_Height, BT_Symbol, sizeof(BT_Symbol), true); - } - else - { - char macAddress[5] = "BC4D"; - // sprintf(macAddress, "%02X%02X", unitMACAddress[4], unitMACAddress[5]); - oled.setFontType(0); //Set font to smallest - oled.setCursor(0, 4); - oled.print(macAddress); - } - - if (digitalRead(baseSwitch) == LOW) - oled.drawIcon(27, 0, Base_Width, Base_Height, Base, sizeof(Base), true); //true - blend with other pixels - else - oled.drawIcon(27, 3, Rover_Width, Rover_Height, Rover, sizeof(Rover), true); - - //Horz positional accuracy - oled.setFontType(1); //Set font to type 1: 8x16 - oled.drawIcon(0, 18, CrossHair_Width, CrossHair_Height, CrossHair, sizeof(CrossHair), true); - oled.setCursor(16, 20); //x, y - oled.print(":"); - float hpa = myGPS.getHorizontalAccuracy() / 10000.0; - // float hpa = 10000.0; - if (hpa > 30.0) - { - oled.print(">30"); - } - else if (hpa > 9.9) - { - oled.print(hpa, 1); //Print down to decimeter - } - else if (hpa > 1.0) - { - oled.print(hpa, 2); //Print down to centimeter - } - else - { - oled.print("."); //Remove leading zero - oled.printf("%03d", (int)(hpa * 1000)); //Print down to millimeter - } - - //SIV - oled.drawIcon(2, 35, Antenna_Width, Antenna_Height, Antenna, sizeof(Antenna), true); - oled.setCursor(16, 36); //x, y - oled.print(":"); - - if (myGPS.getFixType() == 0) //0 = No Fix - { - oled.print("0"); - } - else - { - oled.print(myGPS.getSIV()); - } - - //Check I2C semaphore - if (xSemaphoreTake(xI2CSemaphore, i2cSemaphore_maxWait) == pdPASS) - { - oled.display(); - xSemaphoreGive(xI2CSemaphore); - } - - //Serial.printf("Display time to update: %d\n", millis() - startTime); - - vTaskDelay(pdMS_TO_TICKS(500)); //Update the display every 500ms - } -} - -//AutoPVT without implicit updates is used -//This means we regularly (every 100ms) call checkUblox and all the global datums are updated as they are reported (4Hz default) -//If we ask for something like myGPS.getSIV(), there is no blocking wait, we simply get the last reported value -void checkUbloxTask(void *e) -{ - while (true) - { - long startTime = millis(); - //Check I2C semaphore - if (xSemaphoreTake(xI2CSemaphore, i2cSemaphore_maxWait) == pdPASS) - { - myGPS.checkUblox(); - xSemaphoreGive(xI2CSemaphore); - } - - vTaskDelay(pdMS_TO_TICKS(100)); //Check for new u-blox I2C data every 100ms - //Serial.printf("GPS time to update: %d\n", millis() - startTime); - } -} - -//Control BT status LED according to bluetoothState -void updateBTLEDTask(void *e) -{ - while (1) - { - if (bluetoothState == BT_ON_NOCONNECTION) - digitalWrite(bluetoothStatusLED, !digitalRead(bluetoothStatusLED)); - else if (bluetoothState == BT_CONNECTED) - digitalWrite(bluetoothStatusLED, HIGH); - else - digitalWrite(bluetoothStatusLED, LOW); - - vTaskDelay(pdMS_TO_TICKS(500)); //Blink BT LED every 500ms - } -} - -//When called, checks level of battery and updates the LED brightnesses -//And outputs a serial message to USB and BT -void checkBatteryTask(void *e) -{ - while (true) - { - String battMsg = ""; - - long startTime = millis(); - - //Check I2C semaphore - if (xSemaphoreTake(xI2CSemaphore, i2cSemaphore_maxWait) == pdPASS) - { - battLevel = lipo.getSOC(); - battVoltage = lipo.getVoltage(); - battChangeRate = lipo.getChangeRate(); - xSemaphoreGive(xI2CSemaphore); - } - //Serial.printf("Batt time to update: %d\n", millis() - startTime); - - battMsg += "Batt ("; - battMsg += battLevel; - battMsg += "%): "; - - battMsg += "Voltage: "; - battMsg += battVoltage; - battMsg += "V"; - - if (battChangeRate > 0) - battMsg += " Charging: "; - else - battMsg += " Discharging: "; - battMsg += battChangeRate; - battMsg += "%/hr "; - - if (battLevel < 10) - { - battMsg += "RED uh oh!"; - ledcWrite(ledRedChannel, 255); - ledcWrite(ledGreenChannel, 0); - } - else if (battLevel < 50) - { - battMsg += "Yellow ok"; - ledcWrite(ledRedChannel, 128); - ledcWrite(ledGreenChannel, 128); - } - else if (battLevel >= 50) - { - battMsg += "Green all good"; - ledcWrite(ledRedChannel, 0); - ledcWrite(ledGreenChannel, 255); - } - else - { - battMsg += "No batt"; - ledcWrite(ledRedChannel, 10); - ledcWrite(ledGreenChannel, 0); - } - battMsg += "\n\r"; - //SerialBT.print(battMsg); - Serial.print(battMsg); - - vTaskDelay(pdMS_TO_TICKS(2000)); //Check batt levels every 2s - } -} diff --git a/Firmware/Test Sketches/xTask/xTask.ino b/Firmware/Test Sketches/xTask/xTask.ino deleted file mode 100644 index 8eba83f45..000000000 --- a/Firmware/Test Sketches/xTask/xTask.ino +++ /dev/null @@ -1,157 +0,0 @@ -/* - Use tasks to periodically check: - Blink BT LED - Check battery - Update display - Check GPS - - Update GPS data - We are going to use autoPVT and setAutoHPPOSLLH with Explicit Update. We will checkUblox() every 100ms. - This will cause all dataums (SIV, HPA, etc) to update regularly but when we call getSIV() we will not force wait for the most recent - data. This will also have the added benefit of regularly feeding processRTCM. Every checkUblox() call will pass any waiting RTCM - bytes to a future NTRIP server. - - xTasks need a semaphore for hardware resources so we have a mutex semaphore for I2C. - - This implementation still has display errors for unknown reasons. The size of the stack buffers is also a concern. -*/ - - -const int FIRMWARE_VERSION_MAJOR = 1; -const int FIRMWARE_VERSION_MINOR = 1; - -#include "settings.h" - -SemaphoreHandle_t xI2CSemaphore; -const int i2cSemaphore_maxWait = 0; //TickType_t - -TaskHandle_t updateBTLEDTaskHandle = NULL; -TaskHandle_t checkBatteryTaskHandle = NULL; -TaskHandle_t updateDisplayTaskHandle = NULL; -TaskHandle_t checkUbloxTaskHandle = NULL; - -//Hardware connections -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= -const int positionAccuracyLED_1cm = 2; -const int baseStatusLED = 4; -const int baseSwitch = 5; -const int bluetoothStatusLED = 12; -const int positionAccuracyLED_100cm = 13; -const int positionAccuracyLED_10cm = 15; -const byte PIN_MICROSD_CHIP_SELECT = 25; -const int zed_tx_ready = 26; -const int zed_reset = 27; -const int batteryLevelLED_Red = 32; -const int batteryLevelLED_Green = 33; -const int batteryLevel_alert = 36; -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - -//External Display -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= -#include //Click here to get the library: http://librarymanager/All#SparkFun_Micro_OLED -#include "icons.h" - -#define PIN_RESET 9 -#define DC_JUMPER 1 -MicroOLED oled(PIN_RESET, DC_JUMPER); -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - -//GNSS configuration -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= -#define MAX_PAYLOAD_SIZE 384 // Override MAX_PAYLOAD_SIZE for getModuleInfo which can return up to 348 bytes - -#include "SparkFun_Ublox_Arduino_Library.h" //Click here to get the library: http://librarymanager/All#SparkFun_Ublox_GPS -//SFE_UBLOX_GPS myGPS; - -// Extend the class for getModuleInfo - See Example21_ModuleInfo -class SFE_UBLOX_GPS_ADD : public SFE_UBLOX_GPS -{ - public: - boolean getModuleInfo(uint16_t maxWait = 1100); //Queries module, texts - - struct minfoStructure // Structure to hold the module info (uses 341 bytes of RAM) - { - char swVersion[30]; - char hwVersion[10]; - uint8_t extensionNo = 0; - char extension[10][30]; - } minfo; -}; - -SFE_UBLOX_GPS_ADD myGPS; - -//This string is used to verify the firmware on the ZED-F9P. This -//firmware relies on various features of the ZED and may require the latest -//u-blox firmware to work correctly. We check the module firmware at startup but -//don't prevent operation if firmware is mismatched. -char latestZEDFirmware[] = "FWVER=HPG 1.13"; - -//Used for config ZED for things not supported in library: getPortSettings, getSerialRate, getNMEASettings, getRTCMSettings -//This array holds the payload data bytes. Global so that we can use between config functions. -uint8_t settingPayload[MAX_PAYLOAD_SIZE]; -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - - -//Battery fuel gauge and PWM LEDs -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= -#include // Click here to get the library: http://librarymanager/All#SparkFun_MAX1704x_Fuel_Gauge_Arduino_Library -SFE_MAX1704X lipo(MAX1704X_MAX17048); - -// setting PWM properties -const int freq = 5000; -const int ledRedChannel = 0; -const int ledGreenChannel = 1; -const int resolution = 8; - -int battLevel = 0; //SOC measured from fuel gauge, in %. Used in multiple places (display, serial debug, log) -float battVoltage = 0.0; -float battChangeRate = 0.0; - -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - - -void setup() -{ - Serial.begin(115200); - delay(100); - Serial.println("Testing tasks"); - - pinMode(bluetoothStatusLED, OUTPUT); - - Wire.begin(); - //Wire.setClock(100000); - - //beginLEDs(); //LED and PWM setup - - beginDisplay(); //Check if an external Qwiic OLED is attached - - beginFuelGauge(); //Start I2C device - - beginGNSS(); //Connect and configure ZED-F9P - - bluetoothState = BT_ON_NOCONNECTION; - - //Wire.setClock(400000); //The battery and ublox modules seem to dislike 400kHz - - if (xI2CSemaphore == NULL) - { - xI2CSemaphore = xSemaphoreCreateMutex(); - if (xI2CSemaphore != NULL) - xSemaphoreGive(xI2CSemaphore); //Make the I2C hardware available for use - } - - //Start tasks - xTaskCreate(updateBTLEDTask, "BTLED", 1000, NULL, 0, &updateBTLEDTaskHandle); - xTaskCreate(checkBatteryTask, "CheckBatt", 1000, NULL, 0, &checkBatteryTaskHandle); - xTaskCreate(checkUbloxTask, "CheckUblox", 4000, NULL, 0, &checkUbloxTaskHandle); - - if (online.display == true) - xTaskCreate(updateDisplayTask, "UpdateDisplay", 2000, NULL, 0, &updateDisplayTaskHandle); -} - -long lastDisplayUpdate = 0; - -void loop() { - Serial.print("."); - - delay(100); -} diff --git a/Firmware/Tools/Compare.c b/Firmware/Tools/Compare.c new file mode 100644 index 000000000..fc597fe9a --- /dev/null +++ b/Firmware/Tools/Compare.c @@ -0,0 +1,1480 @@ +// Compare.c + +#include +#include +#include +#include +#include +#include +#include + +#include "crc24q.h" +#include "crc24q.c" +//#include "Crc24q.h" + +#define COMPUTE_CRC24Q(parse, data) (((parse)->crc << 8) ^ crc24q[data ^ (((parse)->crc >> 16) & 0xff)]) + +#define DISPLAY_BAD_CHARACTERS 0 +#define DISPLAY_BAD_CHARACTER_OFFSETS 1 +#define DISPLAY_RTCM_MESSAGE_LIST 1 +#define DISPLAY_UBX_MESSAGE_LIST 1 +#define DISPLAY_BOUNDARY 0 +#define DISPLAY_DATA_BYTES 0 +#define DISPLAY_LIST 0 +#define DISPLAY_MESSAGE_TYPE 0 +#define DISPLAY_MESSAGE_TYPE_LIST 1 +#define DISPLAY_NMEA_MESSAGES 1 +#define DISPLAY_STRINGS 0 + +#define MESSAGE_LENGTH(length) (3 + length + 3) + +#define BINARY_MESSAGE_START 0xd3 + +#define MAX_BAD_CHARACTERS 1000 + +uint32_t bad_characters[256>>5]; +uint32_t bad_character_count[256]; +uint32_t bad_character_offset[MAX_BAD_CHARACTERS]; +uint32_t bad_character_length[MAX_BAD_CHARACTERS]; +int32_t bad_character_offset_count = -1; + +char buffer[65536]; +char string[256]; +uint8_t * file_data; +uint32_t rtcm_messages[4096 >> 5]; +uint32_t rtcm_message_count[4096]; +uint32_t rtcm_max_message_length[4096]; +uint32_t ubx_messages[65536 >> 5]; +uint32_t ubx_message_count[65536]; +uint32_t ubx_max_message_length[65536]; +int bad_checksum_header; +int nmea_checksum_errors; +int rtcm_crc_errors; +int ubx_checksum_errors; + +typedef struct _NMEA_MESSAGE { + struct _NMEA_MESSAGE * next; + uint8_t * message; + uint32_t count; + uint32_t max_length; +} NMEA_MESSAGE; + +NMEA_MESSAGE * nmea_list; + +enum SentenceTypes +{ +SENTENCE_TYPE_NONE = 0, +SENTENCE_TYPE_NMEA, +SENTENCE_TYPE_UBX, +SENTENCE_TYPE_RTCM +} currentSentence = SENTENCE_TYPE_NONE; + +typedef struct _PARSE_STATE * P_PARSE_STATE; + +//Parse routine +typedef uint8_t (* PARSE_ROUTINE)(P_PARSE_STATE parse, //Parser state + uint8_t data); //Incoming data byte + +//End of message callback routine +typedef void (* EOM_CALLBACK)(P_PARSE_STATE parse, //Parser state + uint8_t type); //Message type + +#define PARSE_BUFFER_LENGTH 0x10000 + +typedef struct _PARSE_STATE +{ + PARSE_ROUTINE state; //Parser state routine + EOM_CALLBACK eomCallback; //End of message callback routine + const char * parserName; //Name of parser + uint32_t crc; //RTCM computed CRC + uint32_t rtcmCrc; //Computed CRC value for the RTCM message + uint32_t invalidRtcmCrcs; //Number of bad RTCM CRCs detected + uint16_t bytesRemaining; //Bytes remaining in RTCM CRC calculation + uint16_t length; //Message length including line termination + uint16_t maxLength; //Maximum message length including line termination + uint16_t message; //RTCM message number + uint16_t nmeaLength; //Length of the NMEA message without line termination + uint8_t buffer[PARSE_BUFFER_LENGTH]; //Buffer containing the message + uint8_t nmeaMessageName[16]; //Message name + uint8_t nmeaMessageNameLength; //Length of the message name + uint8_t ck_a; //U-blox checksum byte 1 + uint8_t ck_b; //U-blox checksum byte 2 + bool computeCrc; //Compute the CRC when true +} PARSE_STATE; + +//Forward declaration +uint8_t waitForPreamble(PARSE_STATE * parse, uint8_t data); +uint64_t offset; +uint64_t file_offset; + +void +dump_message ( + unsigned char * data + ) +{ + unsigned int actual; + unsigned int crc; + int index; + int length; + int offset; + + // + // +----------+--------+----------------+---------+----------+---------+ + // | Preamble | Fill | Message Length | Message | Fill | CRC-24Q | + // | 8 bits | 6 bits | 10 bits | n-bits | 0-7 bits | 24 bits | + // | 0xd3 | 000000 | | | zeros | | + // +----------+--------+----------------+---------+----------+---------+ + // + + // Get the number of bytes + length = (data[1] << 8) | data[2]; + if (DISPLAY_DATA_BYTES) { + // Dump the message + offset = data - file_data; + printf ("0x%08x: %02x %02x %02x\n", offset, data[0], data[1], data[2]); + offset += 3; + index = 0; + if (length > 0) { + do + { + // Display the offset at the beginning of the line + if (!(index & 0xf)) + printf ("0x%08x: ", offset); + + // Display the data bytes + printf ("%02x ", data[index+3]); + offset += 1; + + // Terminate the lines + if (!(++index % (DISPLAY_DATA_BYTES ? DISPLAY_DATA_BYTES : 1))) + printf ("\n"); + } while (index < length); + + // Terminate a short line + if (index % (DISPLAY_DATA_BYTES ? DISPLAY_DATA_BYTES : 1)) + printf ("\n"); + } + + // Display the Cyclic Redundancy Check (CRC) + printf ("0x%08x: %02x %02x %02x CRC: %s\n", + offset, data[3 + index], data[3 + index + 1], data[3 + index + 2], + crc24q_check (data, MESSAGE_LENGTH(length)) ? "Matches" : "Does not match!"); + } else { + if (!crc24q_check (data, MESSAGE_LENGTH(length))) { + if (!bad_checksum_header) { + bad_checksum_header = 1; + printf ("Bad checksums:\n"); + } + length = MESSAGE_LENGTH(length); + crc = crc24q_hash(data, length - 3); + actual = (data[length - 3] << 16) | (data[length - 2] << 8) | data[length - 1]; + printf (" 0x%08lx: Binary message, expected CRC: 0x%06x, actual CRC: 0x%06x\n", + data - file_data, crc, actual); + rtcm_crc_errors += 1; + } + } +} + +void +display_string ( + unsigned char * string, + int length + ) +{ + char * temp; + char * temp_end; + + // Copy the strings into the buffer + memcpy (buffer, string, length); + buffer[length] = 0; + + // Terminate the strings + temp = buffer; + temp_end = &temp[length]; + do { + if ((*temp == '\r') || (*temp == '\n')) + *temp = 0; + } while (++temp < temp_end); + + // Display the strings + temp = buffer; + while (temp < temp_end) { + if (*temp) + printf ("%s\n", temp); + temp += strlen(temp) + 1; + } +} + +unsigned char * +process_nmea_message ( + unsigned char * data, + unsigned char * data_end + ) +{ + unsigned char checksum; + char checksum_char[2]; + NMEA_MESSAGE * message; + NMEA_MESSAGE * previous; + unsigned char * start; + + // Check for the beginning of a NMEA message ($) + if (*data != '$') { + if ((*data != '\r') && (*data != '\n')) { + bad_character_count[*data] += 1; + bad_characters[*data >> 5] |= 1 << (*data & 0x1f); + if ((bad_character_offset_count < 0) + || ((bad_character_offset[bad_character_offset_count] + bad_character_length[bad_character_offset_count]) != (data - file_data))) { + bad_character_offset_count += 1; + bad_character_offset[bad_character_offset_count] = data - file_data; + } + bad_character_length[bad_character_offset_count] += 1; + } + return data + 1; + } + + // Skip over the dollar sign ($) + start = data++; + + // Scan for comma or end of message + checksum = 0; + while ((*data != ',') && (*data != '\r') && (*data != '\n') + && (*data != BINARY_MESSAGE_START)) + checksum ^= *data++; + + // Return if this is the start of a binary message + if (*data == BINARY_MESSAGE_START) + return data; + + // Remember the message if a comma was found + if (*data == ',') { + if (DISPLAY_LIST) + printf ("----------------------\r\n"); + + // Build the zero terminated string + memset (string, 0, sizeof(string)); + strncpy (string, (char *)start, data - start); + + /* + list --> NULL + + previous = NULL + string: $GNGST --. + V + +------------+ + list --> | $GNGST (1) | --> NULL + +------------+ + + previous = $GNGST + string: $GNGGA --. + V + +------------+ +------------+ + list --> | $GNGGA (1) | --> | $GNGST (1) | --> NULL + +------------+ +------------+ + + previous = $GNGSA + string: $GNGSA --------------------. + V + +------------+ +------------+ +------------+ + list --> | $GNGGA (1) | --> | $GNGSA (1) | --> | $GNGST (1) | --> NULL + +------------+ +------------+ +------------+ + + previous = $GNGSA + string: $GNGSA --------------------. + V + +------------+ +------------+ +------------+ + list --> | $GNGGA (1) | --> | $GNGSA (2) | --> | $GNGST (1) | --> NULL + +------------+ +------------+ +------------+ + + previous = $GNGST + string: $GNRMC ---------------------------------------------------------. + V + +------------+ +------------+ +------------+ +------------+ + list --> | $GNGGA (1) | --> | $GNGSA (2) | --> | $GNGST (1) | --> | $GNRMC (1) | --> NULL + +------------+ +------------+ +------------+ +------------+ + + */ + + // Display the list + if (DISPLAY_LIST) { + printf ("list: "); + for (previous = nmea_list; previous; previous = previous->next) + printf ("%s(%d)-->", previous->message, previous->count); + printf ("NULL\n"); + } + + // Find the location for insertion + // Check for something in the list + previous = NULL; + if (nmea_list) { + if (strcmp((char *)nmea_list->message, string) <= 0) { + previous = nmea_list; + while (previous->next && (strcmp((char *)previous->next->message, string) <= 0)) + previous = previous->next; + } + } + + // Display the insertion decision + if (DISPLAY_LIST) { + printf ("previous: %s\n", ((previous != NULL) ? (char *)previous->message : (char *)"NULL")); + printf ("string: %s\n", string); + } + + // Check for a duplicate message + if (previous && (strcmp ((char *)previous->message, string) == 0)) { + if (DISPLAY_LIST) + printf ("Duplicate found: %d\n", previous->count); + previous->count += 1; + message = previous; + } else { + // Add the new message + message = malloc (sizeof(NMEA_MESSAGE) + strlen(string) + 1); + message->message = (uint8_t *)(message + 1); + strcpy((char *)message->message, string); + message->count = 1; + + // Message insertion position + // previous == NULL; ==> list head + // previous != NULL; ==> middle or end of list + if (!previous) { + if (DISPLAY_LIST) + printf ("Add to head of the list\n"); + + // Add this message at the start of the list + message->next = nmea_list; + nmea_list = message; + } else { + if (DISPLAY_LIST) + printf ("Add to middle of the list\n"); + + // Insert this message into the middle of the list + message->next = previous->next; + previous->next = message; + } + } + + // Display the new list + if (DISPLAY_LIST) { + printf ("New list: "); + for (previous = nmea_list; previous; previous = previous->next) + printf ("%s(%d)-->", previous->message, previous->count); + printf ("NULL\n"); + } + } + + // Scan for asterisk or end of message + while ((*data != '*') && (*data != '\r') && (*data != '\n') + && (*data != BINARY_MESSAGE_START)) + checksum ^= *data++; + + // Check for end of NMEA message, validate the checksum + if (*data == '*') { + checksum_char[0] = ((checksum >> 4) & 0xf) + '0'; + if (checksum_char[0] > '9') + checksum_char[0] += 'A' - '0' - 10; + checksum_char[1] = (checksum & 0xf) + '0'; + if (checksum_char[1] > '9') + checksum_char[1] += 'A' - '0' - 10; + if ((toupper(data[1]) != checksum_char[0]) || (toupper(data[2]) != checksum_char[1])) { + if (!bad_checksum_header) { + bad_checksum_header = 1; + printf ("Bad checksums:\n"); + } + printf (" 0x%08lx: NMEA %s, expected CRC: 0x%02x, actual CRC: 0x%c%c\n", + data - file_data, &string[1], checksum, data[1], data[2]); + nmea_checksum_errors += 1; + } + data += 3; + } + + // Scan for end of message + while ((*data != '\r') && (*data != '\n') && (*data != BINARY_MESSAGE_START)) + data++; + + return data; +} + +uint8_t * +find_gnss_header ( + uint8_t * data, + uint8_t * data_end + ) +{ + int length; + + do { + // From RTCM 10403.2 Section 4 + // + // +----------+--------+----------------+---------+----------+---------+ + // | Preamble | Fill | Message Length | Message | Fill | CRC-24Q | + // | 8 bits | 6 bits | 10 bits | n-bits | 0-7 bits | 24 bits | + // | 0xd3 | 000000 | | | zeros | | + // +----------+--------+----------------+---------+----------+---------+ + // + + // Locate the beginning of the message + while ((data < data_end) && (*data != BINARY_MESSAGE_START)) + data = process_nmea_message (data, data_end); + + // Check for end of data + if (data >= data_end) + break; + + // Get the number of bytes + length = (data[1] << 8) | data[2]; + if ((data + MESSAGE_LENGTH(length)) <= data_end) { + + // Verify the CRC + if (crc24q_check (data, MESSAGE_LENGTH(length))) + break; + } + + // Skip this preamble byte + data++; + } while (data < data_end); + + // Return the address of the next preamble byte or end of data + return data; +} + +void processNemaMessage(PARSE_STATE * parse) +{ + NMEA_MESSAGE * message; + NMEA_MESSAGE * previous; + + //Display the NMEA message +// dumpBuffer(parse->buffer, parse->length); +// printf(" %s NMEA %s, %d bytes\r\n", parse->parserName, parse->nmeaMessageName, parse->length); + + // Display the list + if (DISPLAY_LIST) { + printf ("list: "); + for (previous = nmea_list; previous; previous = previous->next) + printf ("%s(%d)-->", previous->message, previous->count); + printf ("NULL\n"); + } + + // Find the location for insertion + // Check for something in the list + previous = NULL; + if (nmea_list) { + if (strcmp((char *)nmea_list->message, (char *)parse->nmeaMessageName) <= 0) { + previous = nmea_list; + while (previous->next && (strcmp((char *)previous->next->message, (char *)parse->nmeaMessageName) <= 0)) + previous = previous->next; + } + } + + // Display the insertion decision + if (DISPLAY_LIST) { + printf ("previous: %s\n", ((previous != NULL) ? (char *)previous->message : (char *)"NULL")); + printf ("string: %s\n", parse->nmeaMessageName); + } + + // Check for a duplicate message + if (previous && (strcmp ((char *)previous->message, (char *)parse->nmeaMessageName) == 0)) { + if (DISPLAY_LIST) + printf ("Duplicate found: %d\n", previous->count); + previous->count += 1; + message = previous; + if (message->max_length < parse->length) + message->max_length = parse->length; + } else { + // Add the new message + message = malloc (sizeof(NMEA_MESSAGE) + strlen((char *)parse->nmeaMessageName) + 1); + message->message = (uint8_t *)(message + 1); + strcpy((char *)message->message, (char *)parse->nmeaMessageName); + message->count = 1; + message->max_length = parse->length; + + // Message insertion position + // previous == NULL; ==> list head + // previous != NULL; ==> middle or end of list + if (!previous) { + if (DISPLAY_LIST) + printf ("Add to head of the list\n"); + + // Add this message at the start of the list + message->next = nmea_list; + nmea_list = message; + } else { + if (DISPLAY_LIST) + printf ("Add to middle of the list\n"); + + // Insert this message into the middle of the list + message->next = previous->next; + previous->next = message; + } + } + + // Display the new list + if (DISPLAY_LIST) { + printf ("New list: "); + for (previous = nmea_list; previous; previous = previous->next) + printf ("%s(%d)-->", previous->message, previous->count); + printf ("NULL\n"); + } +} + +void processRtcmMessage(PARSE_STATE * parse) +{ +// dumpBuffer(parse->buffer, parse->length); +// printf(" %s RTCM %d, %d bytes\r\n", parse->parserName, parse->message, parse->length); + rtcm_messages[parse->message >> 5] |= 1 << (parse->message & 0x1f); + rtcm_message_count[parse->message] += 1; + if (rtcm_max_message_length[parse->message] < parse->length) + rtcm_max_message_length[parse->message] = parse->length; +} + +void processUbxMessage(PARSE_STATE * parse) +{ +// dumpBuffer(parse->buffer, parse->length); +// printf(" %s UBX %d.%d, %d bytes\r\n", parse->parserName, parse->message >> 8, parse->message & 0xff, parse->length); + ubx_messages[parse->message >> 5] |= 1 << (parse->message & 0x1f); + ubx_message_count[parse->message] += 1; + if (ubx_max_message_length[parse->message] < parse->length) + ubx_max_message_length[parse->message] = parse->length; +} + +//Process the message +void processMessage(PARSE_STATE * parse, uint8_t type) +{ + switch (type) + { + case SENTENCE_TYPE_NMEA: + processNemaMessage(parse); + break; + + case SENTENCE_TYPE_RTCM: + processRtcmMessage(parse); + break; + + case SENTENCE_TYPE_UBX: + processUbxMessage(parse); + break; + + default: + printf ("Unknown message type: %d\r\n", type); + break; + } +} + +//Convert nibble to ASCII +uint8_t nibbleToAscii(int nibble) +{ + nibble &= 0xf; + return (nibble > 9) ? nibble + 'a' - 10 : nibble + '0'; +} + +//Convert nibble to ASCII +int AsciiToNibble(int data) +{ + //Convert the value to lower case + data |= 0x20; + if ((data >= 'a') && (data <= 'f')) + return data - 'a' + 10; + if ((data >= '0') && (data <= '9')) + return data - '0'; + return -1; +} + +void dumpBuffer(uint8_t * buffer, uint16_t length) +{ + unsigned int bytes; + uint8_t * end; + unsigned int index; + + end = &buffer[length]; + while (buffer < end) + { + //Determine the number of bytes to display on the line + bytes = end - buffer; + if (bytes > (16 - (offset & 0xf))) + bytes = 16 - (offset & 0xf); + + //Display the offset + printf("0x%08lx: ", offset); + + //Skip leading bytes + for (index = 0; index < (offset & 0xf); index++) + printf(" "); + + //Display the data bytes + for (index = 0; index < bytes; index++) + printf("%02x ", buffer[index]); + + //Separate the data bytes from the ASCII + for (; index < (16 - (offset & 0xf)); index++) + printf(" "); + printf(" "); + + //Skip leading bytes + for (index = 0; index < (offset & 0xf); index++) + printf(" "); + + //Display the ASCII values + for (index = 0; index < bytes; index++) + printf("%c", ((buffer[index] < ' ') || (buffer[index] >= 0x7f)) + ? '.' : buffer[index]); + printf("\r\n"); + + //Set the next line of data + buffer += bytes; + offset += bytes; + } +} + +//Read the line termination +uint8_t nmeaLineTermination(PARSE_STATE * parse, uint8_t data) +{ + unsigned int checksum; + + //Process the line termination + if ((data != '\r') && (data != '\n')) + { + //Don't include this character in the buffer + parse->length--; + + //Convert the checksum characters into binary + checksum = AsciiToNibble(parse->buffer[parse->nmeaLength - 2]) << 4; + checksum |= AsciiToNibble(parse->buffer[parse->nmeaLength - 1]); + + //Validate the checksum + if (checksum == parse->crc) + parse->crc = 0; + if (parse->crc) + { + nmea_checksum_errors += 1; + dumpBuffer(parse->buffer, parse->length); + if ((AsciiToNibble(parse->buffer[parse->nmeaLength-2]) >= 0) + && (AsciiToNibble(parse->buffer[parse->nmeaLength-1]) >= 0)) + printf (" %s NMEA %s, %2d bytes, bad checksum, expecting 0x%c%c, computed: 0x%02x\r\n", + parse->parserName, + parse->nmeaMessageName, + parse->length, + parse->buffer[parse->nmeaLength-2], + parse->buffer[parse->nmeaLength-1], + parse->crc); + else + printf (" %s NMEA %s, %2d bytes, invalid checksum bytes 0x%02x 0x%02x, computed: 0x%02x\r\n", + parse->parserName, + parse->nmeaMessageName, + parse->length, + parse->buffer[parse->nmeaLength-2], + parse->buffer[parse->nmeaLength-1], + parse->crc); + } + + //Process this message + parse->eomCallback(parse, SENTENCE_TYPE_NMEA); + + //Add this character to the beginning of the buffer + parse->buffer[0] = data; + parse->length = 1; + return waitForPreamble(parse, data); + } + return SENTENCE_TYPE_NMEA; +} + +//Read the linefeed +uint8_t nmeaLinefeed(PARSE_STATE * parse, uint8_t data) +{ + unsigned int checksum; + uint8_t sentenceType; + + //Convert the checksum characters into binary + checksum = AsciiToNibble(parse->buffer[parse->nmeaLength - 2]) << 4; + checksum |= AsciiToNibble(parse->buffer[parse->nmeaLength - 1]); + + //Update the maximum message length + if (parse->length > parse->maxLength) + { + parse->maxLength = parse->length; + printf("maxLength: %d bytes\r\n", parse->maxLength); + } + + //Validate the checksum + if (checksum == parse->crc) + parse->crc = 0; + if (parse->crc) + { + nmea_checksum_errors += 1; + dumpBuffer(parse->buffer, parse->length); + printf (" %s NMEA %s, %2d bytes, bad checksum, expecting 0x%c%c, computed: 0x%02x\r\n", + parse->parserName, + parse->nmeaMessageName, + parse->length, + parse->buffer[parse->nmeaLength-2], + parse->buffer[parse->nmeaLength-1], + parse->crc); + } + + //Process this message + parse->state = waitForPreamble; + parse->eomCallback(parse, SENTENCE_TYPE_NMEA); + + //Search for another preamble byte + parse->length = 0; + parse->crc = 0; + return SENTENCE_TYPE_NONE; +} + +//Read the carriage return +uint8_t nmeaCarriageReturn(PARSE_STATE * parse, uint8_t data) +{ + parse->state = nmeaLinefeed; + return SENTENCE_TYPE_NMEA; +} + +//Read the second checksum byte +uint8_t nmeaChecksumByte2(PARSE_STATE * parse, uint8_t data) +{ + parse->nmeaLength = parse->length; +// parse->state = nmeaLineTermination; + parse->state = nmeaCarriageReturn; + return SENTENCE_TYPE_NMEA; +} + +//Read the first checksum byte +uint8_t nmeaChecksumByte1(PARSE_STATE * parse, uint8_t data) +{ + parse->state = nmeaChecksumByte2; + return SENTENCE_TYPE_NMEA; +} + +//Read the message data +uint8_t nmeaFindAsterisk(PARSE_STATE * parse, uint8_t data) +{ + if (data != '*') + parse->crc ^= data; + else + parse->state = nmeaChecksumByte1; + return SENTENCE_TYPE_NMEA; +} + +//Read the message name +uint8_t nmeaFindFirstComma(PARSE_STATE * parse, uint8_t data) +{ + parse->crc ^= data; + if ((data != ',') || (parse->nmeaMessageNameLength == 0)) + { + if ((data < 'A') || (data > 'Z')) + { + //Display the invalid data + dumpBuffer(parse->buffer, parse->length - 1); + printf (" %s Invalid NMEA data, %d bytes\r\n", + parse->parserName, parse->length - 1); + + parse->buffer[0] = data; + parse->crc = 0; + parse->length = 1; + return waitForPreamble (parse, data); + } + + //Save the message name + parse->nmeaMessageName[parse->nmeaMessageNameLength++] = data; + } + else + { + //Zero terminate the message name + parse->nmeaMessageName[parse->nmeaMessageNameLength++] = 0; + parse->state = nmeaFindAsterisk; + } + return SENTENCE_TYPE_NMEA; +} + +//Read the CRC +uint8_t rtcmReadCrc(PARSE_STATE * parse, uint8_t data) +{ + uint16_t dataSent; + + //Account for this data byte + parse->bytesRemaining -= 1; + + //Wait until all the data is received + if (parse->bytesRemaining > 0) + return SENTENCE_TYPE_RTCM; + + //Update the maximum message length + if (parse->length > parse->maxLength) + { + parse->maxLength = parse->length; + printf("maxLength: %d bytes\r\n", parse->maxLength); + } + + //Display the RTCM messages with bad CRC + parse->crc &= 0x00ffffff; + if (parse->crc) + { + rtcm_crc_errors += 1; + dumpBuffer(parse->buffer, parse->length); + printf (" %s RTCM %d, %2d bytes, bad CRC, expecting 0x%02x%02x%02x, computed: 0x%06x\r\n", + parse->parserName, + parse->message, + parse->length, + parse->buffer[parse->length-3], + parse->buffer[parse->length-2], + parse->buffer[parse->length-1], + parse->rtcmCrc); + } + + //Process the message + parse->state = waitForPreamble; + parse->eomCallback(parse, SENTENCE_TYPE_RTCM); + + //Search for another preamble byte + parse->length = 0; + parse->computeCrc = false; + parse->crc = 0; + return SENTENCE_TYPE_NONE; +} + +//Read the rest of the message +uint8_t rtcmReadData(PARSE_STATE * parse, uint8_t data) +{ + uint16_t dataSent; + + //Account for this data byte + parse->bytesRemaining -= 1; + + //Wait until all the data is received + if (parse->bytesRemaining <= 0) + { + parse->rtcmCrc = parse->crc & 0x00ffffff; + parse->bytesRemaining = 3; + parse->state = rtcmReadCrc; + } + return SENTENCE_TYPE_RTCM; +} + +//Read the lower 4 bits of the message number +uint8_t rtcmReadMessage2(PARSE_STATE * parse, uint8_t data) +{ + parse->message |= data >> 4; + parse->bytesRemaining -= 1; + parse->state = rtcmReadData; + return SENTENCE_TYPE_RTCM; +} + +//Read the upper 8 bits of the message number +uint8_t rtcmReadMessage1(PARSE_STATE * parse, uint8_t data) +{ + parse->message = data << 4; + parse->bytesRemaining -= 1; + parse->state = rtcmReadMessage2; + return SENTENCE_TYPE_RTCM; +} + +//Read the lower 8 bits of the length +uint8_t rtcmReadLength2(PARSE_STATE * parse, uint8_t data) +{ + parse->bytesRemaining |= data; + parse->state = rtcmReadMessage1; + return SENTENCE_TYPE_RTCM; +} + +//Read the upper two bits of the length +uint8_t rtcmReadLength1(PARSE_STATE * parse, uint8_t data) +{ + //Verify the length byte + if (data & (~3)) + { + //Display the invalid data + dumpBuffer(parse->buffer, parse->length - 1); + printf (" %s Invalid RTCM data, %d bytes\r\n", parse->parserName, parse->length - 1); + + //Invalid length, place this byte at the beginning of the buffer + parse->buffer[0] = data; + parse->length = 1; + parse->computeCrc = false; + parse->crc = 0; + + //Start searching for a preamble byte + return waitForPreamble(parse, data); + } + + //Save the upper 2 bits of the length + parse->bytesRemaining = data << 8; + parse->state = rtcmReadLength2; + return SENTENCE_TYPE_RTCM; +} + +//Read the CK_B byte +uint8_t ubloxCkB(PARSE_STATE * parse, uint8_t data) +{ + bool badChecksum; + + //Update the maximum message length + if (parse->length > parse->maxLength) + { + parse->maxLength = parse->length; + printf("maxLength: %d bytes\r\n", parse->maxLength); + } + + //Validate the checksum + badChecksum = ((parse->buffer[parse->length - 2] != parse->ck_a) + || (parse->buffer[parse->length - 1] != parse->ck_b)); + if (badChecksum) + { + ubx_checksum_errors += 1; + dumpBuffer(parse->buffer, parse->length); + printf (" %s U-Blox %d.%d, %2d bytes, bad checksum, expecting 0x%02x%02x, computed: 0x%02x%02x\r\n", + parse->parserName, + parse->message >> 8, + parse->message & 0xff, + parse->length, + parse->buffer[parse->nmeaLength-2], + parse->buffer[parse->nmeaLength-1], + parse->ck_a, + parse->ck_b); + } + + //Process this message + parse->state = waitForPreamble; + parse->eomCallback(parse, SENTENCE_TYPE_UBX); + + //Search for the next preamble byte + parse->length = 0; + parse->crc = 0; + parse->ck_a = 0; + parse->ck_b = 0; + return SENTENCE_TYPE_NONE; +} + +//Read the CK_A byte +uint8_t ubloxCkA(PARSE_STATE * parse, uint8_t data) +{ + parse->state = ubloxCkB; + return SENTENCE_TYPE_UBX; +} + +//Read the payload +uint8_t ubloxPayload(PARSE_STATE * parse, uint8_t data) +{ + //Compute the checksum over the payload + if (parse->bytesRemaining--) + { + //Calculate the checksum + parse->ck_a += data; + parse->ck_b += parse->ck_a; + return SENTENCE_TYPE_UBX; + } + return ubloxCkA(parse, data); +} + +//Read the second length byte +uint8_t ubloxLength2(PARSE_STATE * parse, uint8_t data) +{ + //Calculate the checksum + parse->ck_a += data; + parse->ck_b += parse->ck_a; + + //Save the second length byte + parse->bytesRemaining |= ((uint16_t)data) << 8; + parse->state = ubloxPayload; + return SENTENCE_TYPE_UBX; +} + +//Read the first length byte +uint8_t ubloxLength1(PARSE_STATE * parse, uint8_t data) +{ + //Calculate the checksum + parse->ck_a += data; + parse->ck_b += parse->ck_a; + + //Save the first length byte + parse->bytesRemaining = data; + parse->state = ubloxLength2; + return SENTENCE_TYPE_UBX; +} + +//Read the ID byte +uint8_t ubloxId(PARSE_STATE * parse, uint8_t data) +{ + //Calculate the checksum + parse->ck_a += data; + parse->ck_b += parse->ck_a; + + //Save the ID as the lower 8-bits of the message + parse->message |= data; + parse->state = ubloxLength1; + return SENTENCE_TYPE_UBX; +} + +//Read the class byte +uint8_t ubloxClass(PARSE_STATE * parse, uint8_t data) +{ + //Start the checksum calculation + parse->ck_a = data; + parse->ck_b = data; + + //Save the class as the upper 8-bits of the message + parse->message = ((uint16_t)data) << 8; + parse->state = ubloxId; + return SENTENCE_TYPE_UBX; +} + +//Read the second sync byte +uint8_t ubloxSync2(PARSE_STATE * parse, uint8_t data) +{ + //Verify the sync 2 byte + if (data != 0x62) + { + //Display the invalid data + dumpBuffer(parse->buffer, parse->length - 1); + printf (" %s Invalid UBX data, %d bytes\r\n", parse->parserName, parse->length - 1); + + //Invalid sync 2 byte, place this byte at the beginning of the buffer + parse->length = 0; + parse->buffer[parse->length++] = data; + + //Start searching for a preamble byte + return waitForPreamble(parse, data); + } + + parse->state = ubloxClass; + return SENTENCE_TYPE_UBX; +} + +//Wait for the preamble byte (0xd3) +uint8_t waitForPreamble(PARSE_STATE * parse, uint8_t data) +{ + //Verify that this is the preamble byte + offset = file_offset; + switch(data) + { + case '$': + + // + // NMEA Message + // + // +----------+---------+--------+---------+----------+----------+ + // | Preamble | Name | Comma | Data | Asterisk | Checksum | + // | 8 bits | n bytes | 8 bits | n bytes | 8 bits | 2 bytes | + // | $ | | , | | | | + // +----------+---------+--------+---------+----------+----------+ + // | | + // |<------------------- Checksum ------------------->| + // + + parse->crc = 0; + parse->computeCrc = false; + parse->nmeaMessageNameLength = 0; + parse->state = nmeaFindFirstComma; + return SENTENCE_TYPE_NMEA; + + case 0xb5: + + // + // U-BLOX Message + // + // |<-- Preamble --->| + // | | + // +--------+--------+---------+--------+---------+---------+--------+--------+ + // | SYNC | SYNC | Class | ID | Length | Payload | CK_A | CK_B | + // | 8 bits | 8 bits | 8 bits | 8 bits | 2 bytes | n bytes | 8 bits | 8 bits | + // | 0xb5 | 0x62 | | | | | | | + // +--------+--------+---------+--------+---------+---------+--------+--------+ + // | | + // |<------------- Checksum ------------->| + // + // 8-Bit Fletcher Algorithm, which is used in the TCP standard (RFC 1145) + // http://www.ietf.org/rfc/rfc1145.txt + // Checksum calculation + // Initialization: CK_A = CK_B = 0 + // CK_A += data + // CK_B += CK_A + // + + parse->state = ubloxSync2; + return SENTENCE_TYPE_UBX; + + case 0xd3: + + // + // RTCM Standard 10403.2 - Chapter 4, Transport Layer + // + // |<------------- 3 bytes ------------>|<----- length ----->|<- 3 bytes ->| + // | | | | + // +----------+--------+----------------+---------+----------+-------------+ + // | Preamble | Fill | Message Length | Message | Fill | CRC-24Q | + // | 8 bits | 6 bits | 10 bits | n-bits | 0-7 bits | 24 bits | + // | 0xd3 | 000000 | (in bytes) | | zeros | | + // +----------+--------+----------------+---------+----------+-------------+ + // | | + // |<-------------------------------- CRC -------------------------------->| + // + + //Start the CRC with this byte + parse->crc = 0; + parse->crc = COMPUTE_CRC24Q(parse, data); + parse->computeCrc = true; + + //Get the message length + parse->state = rtcmReadLength1; + return SENTENCE_TYPE_RTCM; + } + + //preamble byte not found + dumpBuffer(parse->buffer, parse->length); + printf (" %s invalid byte 0x%02x\r\n", parse->parserName, data); + parse->length = 0; + parse->state = waitForPreamble; + return SENTENCE_TYPE_NONE; +} + +uint8_t * +get_file ( + const char * filename, + off_t * length + ) +{ + int file; + uint8_t * file_data; + off_t file_size; + + file_data = NULL; + do { + file = open (filename, O_RDONLY); + if (file < 0) { + perror ("ERROR - Failed to open the file"); + break; + } + + // Determine the file length + file_size = lseek (file, 0, SEEK_END); + + // Get the file buffer + file_data = malloc (file_size); + if (!file_data) { + fprintf (stderr, "ERROR - Failed to allocate file buffer!\n"); + break; + } + + // Read the file into memory + lseek (file, 0, SEEK_SET); + if (read (file, file_data, file_size) != file_size) { + fprintf (stderr, "ERROR - Failed to read the file into memory!\n"); + free (file_data); + file_data = NULL; + break; + } + + // Return the file length + *length = file_size; + } while (0); + + // Close the file + if (file > 0) + close (file); + return file_data; +} + +const char * gnrmc = "$GNRMC,"; +const char * gprmc = "$GPRMC,"; +const char * timestamp; + +uint8_t * +find_time_stamp ( + uint8_t * data, + uint8_t * data_end + ) +{ + int offset; + int timestamp_length; + + timestamp_length = strlen (timestamp); + offset = 0; + while (data < data_end) { + if (*data++ != timestamp[offset++]) + offset = 0; + else + if (offset == timestamp_length) { + data -= timestamp_length; + break; + } + } + + // Found a match or end of the data + if (data >= data_end) + data = NULL; + return data; +} + +uint8_t * +write_temp_file ( + const char * filename, + uint8_t * data, + uint8_t * data_end + ) +{ + int file; + ssize_t file_size; + + do { + file = creat (filename, S_IRWXU | S_IRGRP | S_IROTH); + if (file < 0) { + perror ("ERROR - Failed to create the file"); + break; + } + + // Write the data to the file + file_size = data_end - data; + if (write (file, data, file_size) != file_size) { + fprintf (stderr, "ERROR - Failed to write to %s!\n", filename); + break; + } + + // Done with the data + data = NULL; + } while (0); + + // Close the file + if (file > 0) + close (file); + return data; +} + +int +main ( + int argc, + char ** argv + ) +{ + uint8_t * a; + off_t a_length; + uint8_t * b; + off_t b_length; + int delta; + char * filename_a; + char * filename_b; + int match_length; + uint8_t * ts_a; + uint8_t * ts_b; + int ts_offset; + +/* + int bit; + unsigned char * data; + unsigned char * data_end; + int file; + off_t file_size; + int index; + int length; + int message_number; + int message_type; + static PARSE_STATE parse = {waitForPreamble, processMessage, "Tx"}; + unsigned char * string; + int total; + uint8_t value; +*/ + + // Dispay the help text + if (argc != 3) { + fprintf (stderr, "%s file1 file2\n", argv[0]); + return -1; + } + + // Open the first binary file + filename_a = argv[1]; + a = get_file (filename_a, &a_length); + if (!a) { + return -2; + } + + // Open the second binary file + filename_b = argv[2]; + b = get_file (filename_b, &b_length); + if (!b) { + return -3; + } + + // Find the time stamps + timestamp = gnrmc; + ts_offset = strlen (timestamp); + ts_a = find_time_stamp (a, &a[a_length]); + if (!ts_a) { + timestamp = gprmc; + ts_offset = strlen (timestamp); + ts_a = find_time_stamp (a, &a[a_length]); + if (!ts_a) { + fprintf (stderr, "ERROR - Failed to find timestamp in %s\n", filename_a); + return -4; + } + } + + ts_b = find_time_stamp (b, &b[b_length]); + if (!ts_b) { + fprintf (stderr, "ERROR - Failed to find timestamp in %s\n", filename_b); + return -4; + } + + // Synchronize the time stamps + match_length = ts_offset + 9; + do { + delta = strncmp ((char *)ts_a, (char *)ts_b, match_length); + if (delta == 0) + break; + if (delta < 0) { + ts_a = find_time_stamp (&ts_a[match_length], &a[a_length]); + if (!ts_a) { + fprintf (stderr, "ERROR - Failed to find matching timestamp in %s\n", filename_a); + return -5; + } + continue; + } + ts_b = find_time_stamp (&ts_b[match_length], &b[b_length]); + if (!ts_b) { + fprintf (stderr, "ERROR - Failed to find matching timestamp in %s\n", filename_b); + return -6; + } + } while (1); + + printf ("Timestamp: %c%c%c%c%c%c%c%c%c\n", + ts_a[ts_offset], + ts_a[ts_offset + 1], + ts_a[ts_offset + 2], + ts_a[ts_offset + 3], + ts_a[ts_offset + 4], + ts_a[ts_offset + 5], + ts_a[ts_offset + 6], + ts_a[ts_offset + 7], + ts_a[ts_offset + 8]); + + // Create the temporary files + if (write_temp_file ("a.txt", ts_a, &a[a_length])) + return -7; + if (write_temp_file ("b.txt", ts_b, &b[b_length])) + return -8; + +/* + // Open the log file + file = open (filename, O_RDONLY); + if (file < 0) { + perror ("ERROR - Failed to open the file"); + return -1; + } + + // Determine the file length + file_size = lseek (file, 0, SEEK_END); + + // Get the file buffer + file_data = malloc (file_size); + if (!file_data) { + fprintf (stderr, "ERROR - Failed to allocate file buffer!\n"); + return -2; + } + + // Read the file into memory + lseek (file, 0, SEEK_SET); + if (read (file, file_data, file_size) != file_size) { + fprintf (stderr, "ERROR - Failed to read the file into memory!\n"); + } + + // Close the file + close (file); + + // Skip the first byte to force unaligned start + data = file_data; + data_end = &data[file_size]; + string = data; + while (data < data_end) { + file_offset = data - file_data; + + //Save the data byte + value = *data; + parse.buffer[parse.length++] = value; + + //Compute the CRC value for the message + if (parse.computeCrc) + parse.crc = COMPUTE_CRC24Q(&parse, value); + + //Parse this message + parse.state(&parse, value); + data++; + } + + // Display the checksum and CRC errors + if (nmea_checksum_errors) + printf (" Total NMEA checksum errors: %d\n", nmea_checksum_errors); + if (rtcm_crc_errors) + printf (" Total RTCM message CRC errors: %d\n", rtcm_crc_errors); + if (ubx_checksum_errors) + printf (" Total UBX message checksum errors: %d\n", ubx_checksum_errors); + + // Display the NMEA message list + if (DISPLAY_NMEA_MESSAGES) { + printf ("NMEA Message List:\n"); +fflush(stdout); + NMEA_MESSAGE * message = nmea_list; + while (message) { + printf (" %s: %d %s, max length: %d bytes\n", message->message, message->count, + (message->count == 1) ? "time" : "times", message->max_length); + message = message->next; + } + } + + // Display the RTCM message type list + if (DISPLAY_RTCM_MESSAGE_LIST) { + printf ("RTCM Message List:\n"); +fflush(stdout); + for (index = 0; index < (int)(sizeof(rtcm_messages) / sizeof(rtcm_messages[0])); index++) { + if (rtcm_messages[index]) + for (bit = 0; bit < 32; bit++) { + message_number = (index << 5) | bit; + if (rtcm_messages[index] & (1 << bit)) + printf (" %d (%02x %xx): %d %s, max length: %d bytes\n", message_number, + message_number >> 4, message_number & 0xf, + rtcm_message_count[message_number], + (rtcm_message_count[message_number] == 1) ? "time" : "times", + rtcm_max_message_length[message_number]); + } + } + } + + // Display the UBX message type list + if (DISPLAY_UBX_MESSAGE_LIST) { + printf ("UBX Message List:\n"); +fflush(stdout); + for (index = 0; index < (int)(sizeof(ubx_messages) / sizeof(ubx_messages[0])); index++) { + if (ubx_messages[index]) + for (bit = 0; bit < 32; bit++) { + message_number = (index << 5) | bit; + if (ubx_messages[index] & (1 << bit)) + printf (" %d.%d (0x%02x.%02x): %d %s, max length: %d bytes\n", + message_number >> 8, message_number & 0xff, + message_number >> 8, message_number & 0xff, + ubx_message_count[message_number], + (ubx_message_count[message_number] == 1) ? "time" : "times", + ubx_max_message_length[message_number]); + } + } + } + + // Display the bad characters + if (DISPLAY_BAD_CHARACTERS) { + printf ("Bad characters:\n"); +fflush(stdout); + total = 0; + for (index = 0; index < 256; index++) { + if (bad_characters[index >> 5] & (1 << (index & 0x1f))) { + printf (" 0x%02x: %d\n", index, bad_character_count[index]); + total += bad_character_count[index]; + } + } + printf (" Total: %d\n", total); + } + + // Display the bad character offsets + if (DISPLAY_BAD_CHARACTER_OFFSETS) { + printf ("Bad character offsets:\n"); +fflush(stdout); + total = 0; + for (index = 0; index <= bad_character_offset_count; index++) { + printf (" 0x%08x: %d bytes\n", bad_character_offset[index], bad_character_length[index]); + total += bad_character_length[index]; + } + printf (" Total: %d\n", total); + } +*/ + return 0; +} diff --git a/Firmware/Tools/NMEA_Client.c b/Firmware/Tools/NMEA_Client.c new file mode 100644 index 000000000..5dd63df5b --- /dev/null +++ b/Firmware/Tools/NMEA_Client.c @@ -0,0 +1,139 @@ +/* + * NMEA_Client.c + * + * Program to display the NMEA messages from the RTK Express + */ + +#include +#include +#include +#include +#include +#include + +uint8_t rxBuffer[2048]; + +int +main ( + int argc, + char ** argv + ) +{ + int bytesRead; + int bytesWritten; + int displayHelp; + struct sockaddr_in rtkServerIpAddress; + int rtkSocket; + int status; + struct sockaddr_in vespucciServerIpAddress; + int vespucciSocket; + + do { + displayHelp = 1; + status = 0; + if ((argc < 2) || (argc > 3)) + { + status = -1; + break; + } + + // Initialize the RTK server address + memset (&rtkServerIpAddress, '0', sizeof(rtkServerIpAddress)); + rtkServerIpAddress.sin_family = AF_INET; + rtkServerIpAddress.sin_addr.s_addr = htonl(INADDR_ANY); + rtkServerIpAddress.sin_port = htons(1958); + if (inet_pton (AF_INET, argv[1], &rtkServerIpAddress.sin_addr) <= 0) + { + perror ("ERROR - Invalid RTK server IPv4 address!\n"); + status = -2; + break; + } + + if (argc == 3) + { + // Initialize the Vespucci server address + memset (&vespucciServerIpAddress, '0', sizeof(vespucciServerIpAddress)); + vespucciServerIpAddress.sin_family = AF_INET; + vespucciServerIpAddress.sin_addr.s_addr = htonl(INADDR_ANY); + vespucciServerIpAddress.sin_port = htons(1958); + if (inet_pton (AF_INET, argv[2], &vespucciServerIpAddress.sin_addr) <= 0) + { + perror ("ERROR - Invalid Vespucci IPv4 address!\n"); + status = -3; + break; + } + displayHelp = 0; + + //Create the vespucci socket + vespucciSocket = socket(AF_INET, SOCK_STREAM, 0); + if (vespucciSocket < 0) + { + perror ("ERROR - Unable to create the Vespucci client socket!\n"); + status = -4; + break; + } + + if (connect (vespucciSocket, (struct sockaddr *)&vespucciServerIpAddress, sizeof(vespucciServerIpAddress)) < 0) + { + perror("Error : Failed to connect to Vespucci NMEA server!\n"); + status = -5; + break; + } + } + displayHelp = 0; + + //Create the RTK socket + rtkSocket = socket(AF_INET, SOCK_STREAM, 0); + if (rtkSocket < 0) + { + perror ("ERROR - Unable to create the RTK client socket!\n"); + status = -6; + break; + } + + if (connect (rtkSocket, (struct sockaddr *)&rtkServerIpAddress, sizeof(rtkServerIpAddress)) < 0) + { + perror("Error : Failed to connect to RTK NMEA server!\n"); + status = -7; + break; + } + + // Read the NMEA data from the RTK server + while ((bytesRead = read (rtkSocket, rxBuffer, sizeof(rxBuffer)-1)) > 0) + { + // Zero terminate the NMEA string + rxBuffer[bytesRead] = 0; + + // Output the NMEA buffer + if (fputs ((char *)rxBuffer, stdout) == EOF) + { + perror ("ERROR - Failed to write to stdout!\n"); + status = -8; + break; + } + + // Forward the NMEA data to the Vespucci server + if (argc == 3) + { + if ((bytesWritten = write (vespucciSocket, rxBuffer, bytesRead)) != bytesRead) + { + perror ("ERROR - Failed to write to Vespucci socket"); + status = -9; + break; + } + } + } + + if (bytesRead <= 0) + { + perror ("ERROR - Failed reading from NMEA server!\n"); + status = -10; + } + } while (0); + + // Display the help text + if (displayHelp) + printf ("%s RtkServerIpAddress [VespucciServerIpAddress]\n", argv[0]); + + return status; +} diff --git a/Firmware/Tools/PVT_Server.py b/Firmware/Tools/PVT_Server.py new file mode 100644 index 000000000..8713322b6 --- /dev/null +++ b/Firmware/Tools/PVT_Server.py @@ -0,0 +1,40 @@ +# TCP Server: +# https://realpython.com/python-sockets/#echo-server +# https://stackoverflow.com/a/61539628 + +import socket + +HOST = "0.0.0.0" # Connect to all network adapters on the system +PORT = 2948 + +print("Listening on {}:{}".format(HOST,PORT)) + +s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) +s.bind((HOST, PORT)) +s.listen() +s.settimeout(2) + +try: + while True: + try: + conn, addr = s.accept() + with conn: + print("Connection from {}:".format(addr[0])) + try: + while True: + data = conn.recv(1024) + if data: + print("{}".format(data.decode())) + except KeyboardInterrupt: + break + except KeyboardInterrupt: + break + except socket.timeout: + pass + +except KeyboardInterrupt: + pass + +finally: + s.close() diff --git a/Firmware/Tools/RTK_Reset.c b/Firmware/Tools/RTK_Reset.c new file mode 100644 index 000000000..324e0ae89 --- /dev/null +++ b/Firmware/Tools/RTK_Reset.c @@ -0,0 +1,154 @@ +//------------------------------------------------------------------------------ +// Lee Leahy +// 12 June 2023 +// +// Program to reset the ESP32 +//------------------------------------------------------------------------------ +// +// Starting from the SparkFun RTK Surveyor +// +// https://www.sparkfun.com/products/18443 +// +// Schematic +// +// https://cdn.sparkfun.com/assets/c/1/b/d/8/SparkFun_RTK_Surveyor-v13.pdf +// +// ___ ___ GPIO-0 PU +// DTR RTS BOOT EN +// 0 0 1 1 Run flash code +// 0 1 0 1 Download code, write to flash +// 1 0 1 0 Power off +// 1 1 0 0 Power off +// +// To the ESP-WROOM-32 datasheet +// +// https://www.espressif.com/sites/default/files/documentation/esp32-wroom-32_datasheet_en.pdf +// +// To the ESP-32 Series datasheet +// +// https://www.espressif.com/sites/default/files/documentation/esp32_datasheet_en.pdf +// +// PU - 0: Power off, 1: Power on/up +// +// GPIO-O: Saved at power up, 0 = download image, 1 = boot from flash +// +//------------------------------------------------------------------------------ + +#define MICROSECONDS 1 +#define MILLISECONDS (1000 * MICROSECONDS) +#define POWER_OFF_DELAY (250 * MILLISECONDS) + +#include +#include +#include +#include +#include +#include + +const int dtrPin = TIOCM_DTR; +const int rtsPin = TIOCM_RTS; + +// GPIO-0 PU +// DTR RTS BOOT EN +// 1 1 1 1 Run flash code +// 1 0 0 1 Download code, write to flash +// 0 1 1 0 Power off +// 0 0 0 0 Power off + +int bootFromFlash(int serialPort) +{ + int status; + + status = ioctl(serialPort, TIOCMBIS, &rtsPin); + if (status) + fprintf(stderr, "ERROR: Failed to select the boot from flash\n"); + return status; +} + +int downloadImage(int serialPort) +{ + int status; + + status = ioctl(serialPort, TIOCMBIC, &rtsPin); + if (status) + fprintf(stderr, "ERROR: Failed to select download image\n"); + return status; +} + +int powerOff(int serialPort) +{ + int status; + + status = ioctl(serialPort, TIOCMBIC, &dtrPin); + if (status) + fprintf(stderr, "ERROR: Failed to power off the ESP-32\n"); + return status; +} + +int powerOn(int serialPort) +{ + int status; + + status = ioctl(serialPort, TIOCMBIS, &dtrPin); + if (status) + fprintf(stderr, "ERROR: Failed to power on the ESP-32\n"); + return status; +} + +int main (int argc, const char ** argv) +{ + struct termios params; + int serialPort; + const char * serialPortName; + int status; + + serialPort = -1; + status = 0; + do + { + // Display the help if necessary + if (argc != 2) + { + status = -1; + printf("%s serial_port\n", argv[0]); + break; + } + + // Open the serial port + serialPortName = argv[1]; + serialPort = open (serialPortName, O_RDWR); + if (serialPort < 0) + { + status = errno; + perror ("ERROR: Failed to open the terminal"); + break; + } + + // Get the terminal attributes + if (tcgetattr(serialPort, ¶ms) != 0) + { + status = errno; + perror("ERROR: tcgetattr failed!"); + break; + } + + // Power off the RTK's ESP-32 + status = powerOff(serialPort); + if (status) + break; + usleep(POWER_OFF_DELAY); + + // Select the boot device + status = bootFromFlash(serialPort); + if (status) + break; + + // Power on the RTK's ESP-32 + status = powerOn(serialPort); + } while (0); + + // Close the terminal + if (serialPort >= 0) + close(serialPort); + return status; +} diff --git a/Firmware/Tools/Read_Map_File.c b/Firmware/Tools/Read_Map_File.c new file mode 100644 index 000000000..4b90ec827 --- /dev/null +++ b/Firmware/Tools/Read_Map_File.c @@ -0,0 +1,789 @@ +/********************************************************************** +* Read_Map_File.c +* +* Program to read the map file and process the ESP32 backtrace +**********************************************************************/ +/* + Linux: + + 1. From the top level RTK directory, build the RTK_Surveyor.ino.bin file + using the Arduino CLI, an example: + + export DEBUG_LEVEL=debug + export ENABLE_DEVELOPER=true + export FIRMWARE_VERSION_MAJOR=3 + export FIRMWARE_VERSION_MINOR=7 + + ~/Arduino/arduino-cli compile --fqbn "esp32:esp32:esp32":DebugLevel=$DEBUG_LEVEL \ + ./Firmware/RTK_Surveyor/RTK_Surveyor.ino --build-property \ + build.partitions=app3M_fat9M_16MB --build-property upload.maximum_size=3145728 \ + --build-property "compiler.cpp.extra_flags=\"-DPOINTPERFECT_TOKEN=$POINTPERFECT_TOKEN\" \"-DFIRMWARE_VERSION_MAJOR=$FIRMWARE_VERSION_MAJOR\" \"-DFIRMWARE_VERSION_MINOR=$FIRMWARE_VERSION_MINOR\" \"-DENABLE_DEVELOPER=$ENABLE_DEVELOPER\"" \ + --export-binaries + + 2. Load the firmware into the RTK, an example: + + ~/SparkFun/new-firmware.sh ttyUSB0 Firmware/RTK_Surveyor/build/esp32.esp32.esp32/RTK_Surveyor.ino.bin + + 3. Capture the backtrace using a terminal connected to the RTK + + 4. Run the Read_Map_File application and give it the backtrace, an example: + + Firmware/Tools/Read_Map_File Firmware/RTK_Surveyor/build/esp32.esp32.esp32/RTK_Surveyor.ino.map + Backtrace:0x40117bc4:0x3ffec6600x40117c05:0x3ffec680 0x400d941e:0x3ffec6a0 0x400dbedb:0x3ffec6c0 0x400dc413:0x3ffec6f0 +*/ + +#include +#include +#include +#include +#include +#include +#include +#include + +//---------------------------------------- +// Data structures +//---------------------------------------- + +typedef struct _SYMBOL_TYPE +{ + struct _SYMBOL_TYPE * nextSymbol; + uint64_t address; + uint64_t length; + char * name; +} SYMBOL_TYPE; + +typedef struct _PARSE_TABLE_ENTRY +{ + const char * string; + size_t stringLength; + void (* routine)(char * line); +} PARSE_TABLE_ENTRY; + +//---------------------------------------- +// Constants +//---------------------------------------- + +#define C_PLUS_PLUS_HEADER "_Z" +#define HALF_READ_BUFFER_SIZE 65536 +#define STDIN 0 + +#define nullptr ((void *)0) + +//---------------------------------------- +// Globals +//---------------------------------------- + +int exitStatus; +size_t fileLength; +char line[HALF_READ_BUFFER_SIZE]; +int lineNumber; +int mapFile; +char readBuffer[HALF_READ_BUFFER_SIZE * 2]; +size_t readBufferBytes; +bool readBufferEndOfFile; +size_t readBufferHead; +size_t readBufferTail; +char symbolBuffer[HALF_READ_BUFFER_SIZE]; +int symbolEntries; +SYMBOL_TYPE * symbolListHead; +SYMBOL_TYPE * symbolListTail; + +//---------------------------------------- +// Macros +//---------------------------------------- + +#define PARSE_ENTRY(string, routine) \ + {string, sizeof(string) - 1, routine} + +//---------------------------------------- +// Support routines +//---------------------------------------- + +void dumpBuffer(char * buffer, size_t length) +{ + ssize_t bytes; + char * end; + unsigned int index; + ssize_t offset; + + offset = 0; + end = &buffer[length]; + while (buffer < end) + { + //Determine the number of bytes to display on the line + bytes = end - buffer; + if (bytes > (16 - (offset & 0xf))) + bytes = 16 - (offset & 0xf); + + //Display the offset + printf("0x%08lx: ", offset); + + //Skip leading bytes + for (index = 0; index < (offset & 0xf); index++) + printf(" "); + + //Display the data bytes + for (index = 0; index < bytes; index++) + printf("%02x ", buffer[index]); + + //Separate the data bytes from the ASCII + for (; index < (16 - (offset & 0xf)); index++) + printf(" "); + printf(" "); + + //Skip leading bytes + for (index = 0; index < (offset & 0xf); index++) + printf(" "); + + //Display the ASCII values + for (index = 0; index < bytes; index++) + printf("%c", ((buffer[index] < ' ') || (buffer[index] >= 0x7f)) + ? '.' : buffer[index]); + printf("\r\n"); + + //Set the next line of data + buffer += bytes; + offset += bytes; + } +} + +// Remove the white space +char * removeWhiteSpace(char * buffer) +{ + while (*buffer && ((*buffer == ' ') || (*buffer == '\t'))) + buffer++; + return buffer; +} + +// Skip over the text +char * skipOverText(char * buffer) +{ + while (*buffer && (*buffer != ' ') && (*buffer != '\t')) + buffer++; + return buffer; +} + +//---------------------------------------- +// File read routines +//---------------------------------------- + +int readFromFile() +{ + ssize_t bytesRead; + + exitStatus = 0; + bytesRead = read(mapFile, &readBuffer[readBufferHead], HALF_READ_BUFFER_SIZE); + if (bytesRead < 0) + { + exitStatus = errno; + perror("ERROR: Failed to read from file!\n"); + } + else + { + // Adjust the read pointer + readBufferBytes += bytesRead; + readBufferHead += bytesRead; + fileLength += bytesRead; + if (readBufferHead >= sizeof(readBuffer)) + readBufferHead -= sizeof(readBuffer); + + // Flag the end-of-file + if (bytesRead < HALF_READ_BUFFER_SIZE) + readBufferEndOfFile = true; + } + return exitStatus; +} + +ssize_t readLineFromFile(char * buffer, size_t maxLineLength) +{ + char * bufferEnd; + char * bufferStart; + char byte; + int status; + static size_t offset; + + bufferStart = buffer; + bufferEnd = &buffer[maxLineLength - 1]; + while (buffer < bufferEnd) + { + // Fill the readBuffer if necessary + if ((!readBufferEndOfFile) + && (readBufferBytes <= HALF_READ_BUFFER_SIZE)) + { + status = readFromFile(); + if (status) + return -1; + } + + // Check for end-of-file + if (readBufferEndOfFile && (readBufferHead == readBufferTail)) + break; + + // Get the next byte from the buffer + readBufferBytes -= 1; + byte = readBuffer[readBufferTail++]; + if (readBufferTail >= sizeof(readBuffer)) + readBufferTail = 0; + + // Done when the line feed or NULL is found + if ((byte == '\n') || (!byte)) + break; + + // Skip carriage returns + if (byte == '\r') + continue; + + // Place the rest of the data into the buffer + *buffer++ = byte; + } + + // Zero terminate the string + *buffer = 0; + return bufferEnd - bufferStart; +} + +//---------------------------------------- +// Map file parsing routines +//---------------------------------------- + +void symbolAdd(const char * symbol, uint64_t baseAddress, uint64_t length) +{ + SYMBOL_TYPE * entry; + + entry = malloc(sizeof(SYMBOL_TYPE) + 7 + strlen(symbol) + 1); + if (entry) + { + // Build the symbol entry + entry->nextSymbol = nullptr; + entry->address = baseAddress; + entry->length = length; + entry->name = &((char *)entry)[sizeof(SYMBOL_TYPE)]; + strcpy(entry->name, symbol); + + // Add the symbol to the list + if (!symbolListHead) + { + // This is the first entry in the list + symbolListHead = entry; + symbolListTail = entry; + } + else + { + // Add this entry of the end of the list + symbolListTail->nextSymbol = entry; + symbolListTail = entry; + } + symbolEntries += 1; +// printf("0x%08lx-0x%08lx: %s\n", +// baseAddress, baseAddress + length - 1, symbol); + } +} + +char * symbolGetName(char * buffer) +{ + int c; + size_t headerLength; + char * symbolName; + + // Determine if this is a c symbol + headerLength = sizeof(C_PLUS_PLUS_HEADER) - 1; + c = strncmp(buffer, C_PLUS_PLUS_HEADER, headerLength); + + // Remove the c++ header from the symbol + if (!c) + { + buffer += headerLength; + if (*buffer == 'N') + buffer++; + if (*buffer == 'K') + buffer++; + + // Remove the value + while ((*buffer >= '0') && (*buffer <= '9')) + buffer++; + } + + // Get the symbol name + symbolName = &symbolBuffer[0]; + while (*buffer && (*buffer != ' ') && (*buffer != '\t')) + *symbolName++ = *buffer++; + *symbolName = 0; + + // Remove the last character of a c++ symbol + if ((!c) && (symbolBuffer[0])) + *--symbolName = 0; + + // Return the next position in the buffer + return buffer; +} + +void parseIram1(char * buffer) +{ + uint64_t baseAddress; + uint64_t length; + + /* There are 4 different forms of iram1 lines: + + .iram1.30 0x0000000040084f00 0x5c /home/.../libheap.a(heap_caps.c.obj) + 0x0000000040084f00 heap_caps_malloc_prefer + + .iram1.4 0x00000000400851b0 0x21 /home/.../libesp_hw_support.a(cpu_util.c.obj) + 0x00000000400851b0 esp_cpu_reset + *fill* 0x00000000400851d1 0x3 + + .iram1.34 0x0000000040084f5c 0x36 /home/.../libheap.a(heap_caps.c.obj) + 0x3e (size before relaxing) + 0x0000000040084f5c heap_caps_free + + .iram1.5 0x00000000400851d4 0x31 /home/.../libesp_hw_support.a(cpu_util.c.obj) + 0x35 (size before relaxing) + 0x00000000400851d4 esp_cpu_set_watchpoint + *fill* 0x0000000040085205 0x3 + */ + + do + { + // Skip the rest of the iram1 stuff + buffer = skipOverText(buffer); + + // Remove the white space + buffer = removeWhiteSpace(buffer); + + // Get the symbol address + if (sscanf(buffer, "0x%016lx", &baseAddress) != 1) + break; + + // Validate the symbol address + if (baseAddress < 0x40000000) + break; + + // Get the symbol length + buffer = skipOverText(buffer); + buffer = removeWhiteSpace(buffer); + if (sscanf(buffer, "0x%lx", &length) != 1) + break; + + // Read a line from the map file + if (readLineFromFile(line, sizeof(line)) <= 0) + break; + lineNumber += 1; + buffer = &line[16]; + + // Locate the symbol value + buffer += 16; + if (*buffer != '0') + { + // Read a line from the map file + if (readLineFromFile(line, sizeof(line)) <= 0) + break; + lineNumber += 1; + buffer = &line[16]; + } + + // Get the symbol name + buffer = skipOverText(buffer); + buffer = removeWhiteSpace(buffer); + buffer = symbolGetName(buffer); + symbolAdd(symbolBuffer, baseAddress, length); + } while (0); +} + +void parseTextLine(char * buffer) +{ + uint64_t baseAddress; + uint64_t length; + size_t lineOffset; + int offset; + char * symbol; + int value; + + /* There are 6 different forms of iram1 lines: + .text.i2cRead 0x0000000040132094 0xfc /tmp/.../core.a(esp32-hal-i2c.c.o) + 0x107 (size before relaxing) + 0x0000000040132094 i2cRead + + .text.i2cRead 0x0000000040132094 0xff /tmp/.../core.a(esp32-hal-i2c.c.o) + 0x107 (size before relaxing) + 0x0000000040132094 i2cRead + *fill* 0x0000000040132193 0x1 + + .text._Z21createMessageListBaseR6String + 0x00000000400dc990 0x150 /tmp/.../RTK_Surveyor.ino.cpp.o + 0x00000000400dc990 _Z21createMessageListBaseR6String + + .text._Z17ntripClientUpdatev + 0x00000000400dfe24 0x462 /tmp/.../RTK_Surveyor.ino.cpp.o + 0x00000000400dfe24 _Z17ntripClientUpdatev + *fill* 0x00000000400e0286 0x2 + + .text._Z12printUnknownh + 0x00000000400e1058 0x18 /tmp/.../RTK_Surveyor.ino.cpp.o + 0x20 (size before relaxing) + 0x00000000400e1058 _Z12printUnknownh + + .text._Z12printUnknowni + 0x00000000400e1070 0x13 /tmp/.../RTK_Surveyor.ino.cpp.o + 0x1a (size before relaxing) + 0x00000000400e1070 _Z12printUnknowni + *fill* 0x00000000400e1083 0x1 + */ + + do + { + // Get the symbol name + buffer = symbolGetName(buffer); + + // Remove the white space + buffer = removeWhiteSpace(buffer); + + // Read the symbol address and length from next line if necessary + if (!*buffer) + { + // Read a line from the map file + if (readLineFromFile(line, sizeof(line)) <= 0) + break; + lineNumber += 1; + buffer = line; + } + + // Remove the white space + buffer = removeWhiteSpace(buffer); + + // Get the symbol address + if (sscanf(buffer, "0x%016lx", &baseAddress) != 1) + break; + + // Remove the white space + buffer = skipOverText(buffer); + buffer = removeWhiteSpace(buffer); + + // Get the symbol length + if (sscanf(buffer, "0x%lx", &length) == 1) + { + // Routines are in flash which starts at 0x40000000 + if (baseAddress >= 0x40000000) + symbolAdd(symbolBuffer, baseAddress, length); + } + } while (0); +} + +// Define the search text and routine to process the line when found +const PARSE_TABLE_ENTRY parseTable[] = +{ + PARSE_ENTRY(" .iram1.", parseIram1), + PARSE_ENTRY(" .text.", parseTextLine), +}; +const int parseTableEntries = sizeof(parseTable) / sizeof(parseTable[0]); + +// Parse the map file to locate the symbols and their addresses and lengths +int parseMapFile() +{ + const PARSE_TABLE_ENTRY * entry; + int index; + ssize_t lineLength; + + do + { + // Read a line from the map file + lineLength = readLineFromFile(line, sizeof(line)); + if (lineLength <= 0) + break; + lineNumber += 1; + + // Locate the matching text in the map file + for (index = 0; index < parseTableEntries; index++) + { + // Locate the routine names + entry = &parseTable[index]; + if (strncmp(line, entry->string, entry->stringLength) == 0) + { + parseTable[index].routine(&line[entry->stringLength]); + break; + } + } + } while (readBufferBytes); + +/* + uint64_t baseAddress; + char * buffer; + uint64_t length; + static size_t lineNumber; + size_t lineOffset; + int offset; + char * symbol; + static char symbolBuffer[HALF_READ_BUFFER_SIZE]; + const char * text = " .text._Z"; + int textLength; + int value; + + textLength = strlen(text); + + do + { + // Read a line from the map file + lineLength = readLineFromFile(line, sizeof(line)); + if (lineLength <= 0) + break; + lineNumber += 1; + + // Locate the routine names + if (strncmp(line, text, textLength) == 0) + { + // Remove the value + buffer = &line[textLength]; + value = 0; + while ((*buffer >= '0') && (*buffer <= '9')) + { + value *= 10; + value += *buffer++ - '0'; + } + if (!value) + continue; + + // Get the symbol name + symbol = &symbolBuffer[0]; + while (*buffer && (*buffer != ' ') && (*buffer != '\t')) + *symbol++ = *buffer++; + *symbol = 0; + + // Remove the last character of the symbol + if (symbolBuffer[0]) + *--symbol = 0; + + // Remove the white space + buffer = removeWhiteSpace(buffer); + + // Read the symbol address and length from next line if necessary + if (!*buffer) + { + // Read a line from the map file + lineLength = readLineFromFile(line, sizeof(line)); + if (lineLength <= 0) + break; + lineNumber += 1; + buffer = line; + } + + // Remove the white space + buffer = removeWhiteSpace(buffer); + + // Get the symbol address + if (sscanf(buffer, "0x%016lx", &baseAddress) != 1) + continue; + + // Remove the white space + buffer = skipOverText(buffer); + buffer = removeWhiteSpace(buffer); + + // Get the symbol length + if (sscanf(buffer, "0x%lx", &length) == 1) + { + // Routines are in flash which starts at 0x40000000 + if (baseAddress >= 0x40000000) + { + SYMBOL_TYPE * entry; + + entry = malloc(sizeof(SYMBOL_TYPE) + 7 + strlen(symbolBuffer) + 1); + if (entry) + { + // Build the symbol entry + entry->nextSymbol = nullptr; + entry->address = baseAddress; + entry->length = length; + entry->name = &((char *)entry)[sizeof(SYMBOL_TYPE)]; + strcpy(entry->name, symbolBuffer); + + // Add the symbol to the list + if (!symbolListHead) + { + // This is the first entry in the list + symbolListHead = entry; + symbolListTail = entry; + } + else + { + // Add this entry of the end of the list + symbolListTail->nextSymbol = entry; + symbolListTail = entry; + } + symbolEntries += 1; +// printf("0x%08lx-0x%08lx: %s\n", +// baseAddress, baseAddress + length - 1, symbolBuffer); + } + } + } + } + } while (readBufferBytes); +*/ + + return exitStatus; +} + +// Find the matching symbol +char * findSymbolName(uint64_t pc) +{ + SYMBOL_TYPE * symbol; + + // Walk the symbol list + symbol = symbolListHead; + while (symbol) + { + if ((symbol->address <= pc) && ((symbol->address + symbol->length) > pc)) + return symbol->name; + symbol = symbol->nextSymbol; + } + return nullptr; +} + +// Enable the user to enter the backtrace and display the symbols +int processBacktrace() +{ + char * buffer; + const char * const backtrace = "Backtrace:"; + SYMBOL_TYPE * backtraceEntry; + SYMBOL_TYPE * backtraceList; + ssize_t bytesRead; + char * line; + size_t lineLength; + uint64_t pc; + uint64_t stackPointer; + int status; + char * symbolName; + + do + { + // Read the backtrace from the input + backtraceList = nullptr; + line = nullptr; + lineLength = 0; + status = 0; + bytesRead = getline(&line, &lineLength, stdin); + if (bytesRead < 0) + { + status = errno; + break; + } + + // Skip over the Backtrace: + buffer = line; + if (strncmp(buffer, backtrace, strlen(backtrace)) == 0) + buffer += strlen(backtrace); + + // Skip any white space + buffer = removeWhiteSpace(buffer); + + // The backtrace line looks like the following: + // Backtrace:0x4010d9b4:0x3ffebe300x4010d9f5:0x3ffebe50 0x400d8c81:0x3ffebe70 0x400db357:0x3ffebe90 0x400db81f:0x3ffebec0 + + // Parse the backtrace + while (1) + { + // Get the program counter address + if (sscanf(&buffer[2], "%08lx", &pc) != 1) + break; + + // Skip over the previous value + // 0x value : 0x + buffer += 2 + 8; + if (*buffer == ':') + buffer++; + buffer += 2; + + // Get the stack pointer value + if (sscanf(buffer, "%08lx", &stackPointer) != 1) + break; + + // Process the PC value + backtraceEntry = malloc(sizeof(*backtraceEntry)); + if (backtraceEntry) + { + // Reverse the order of the backtrace entries + backtraceEntry->nextSymbol = backtraceList; + backtraceEntry->address = pc; + backtraceEntry->length = stackPointer; + backtraceList = backtraceEntry; + } + + // Skip any white space + buffer += 8; + buffer = removeWhiteSpace(buffer); + } + } while(0); + + // Display the backtrace + if (backtraceList) + { + // Table header + printf("\n"); + printf(" PC Stack Ptr Symbol\n"); + printf("---------- ---------- --------------------\n"); + + // Walk the backtrace symbol list + backtraceEntry = backtraceList; + while (backtraceEntry) + { + symbolName = findSymbolName(backtraceEntry->address); + printf("0x%08lx 0x%08lx", backtraceEntry->address, backtraceEntry->length); + if (symbolName) + printf(" %s", symbolName); + printf("\n"); + backtraceEntry = backtraceEntry->nextSymbol; + } + } + + // Done with the line + if (line) + free(line); + return status; +} + +//---------------------------------------- +// Application +//---------------------------------------- + +int main(int argc, char ** argv) +{ + char * filename; + int status; + + do + { + status = -1; + + // Display the help text + if (argc != 2) + { + printf ("%s filename\n", argv[0]); + return -1; + } + + // Open the file + filename = argv[1]; + mapFile = open(filename, O_RDONLY); + if (mapFile < 0) + { + status = errno; + perror("ERROR: Unable to open the file\n"); + } + + // Parse the map file + status = parseMapFile(); + } while (0); + + if (!status) + { + // Display the number of symbols + printf("symbolEntries: %d\n", symbolEntries); + + // Process the backtrace + printf("Enter the backtrace:\n"); + status = processBacktrace(); + } + + // Close the file + if (mapFile >= 0) + close(mapFile); + + return status; +} diff --git a/Firmware/Tools/Split_Messages.c b/Firmware/Tools/Split_Messages.c new file mode 100644 index 000000000..767d6c617 --- /dev/null +++ b/Firmware/Tools/Split_Messages.c @@ -0,0 +1,1273 @@ +// Split_Messages.c + +#include +#include +#include +#include +#include +#include +#include + +#include "crc24q.h" +#include "crc24q.c" +//#include "Crc24q.h" + +#define COMPUTE_CRC24Q(parse, data) (((parse)->crc << 8) ^ crc24q[data ^ (((parse)->crc >> 16) & 0xff)]) + +#define DISPLAY_BAD_CHARACTERS 0 +#define DISPLAY_BAD_CHARACTER_OFFSETS 1 +#define DISPLAY_RTCM_MESSAGE_LIST 1 +#define DISPLAY_UBX_MESSAGE_LIST 1 +#define DISPLAY_BOUNDARY 0 +#define DISPLAY_DATA_BYTES 0 +#define DISPLAY_LIST 0 +#define DISPLAY_MESSAGE_TYPE 0 +#define DISPLAY_MESSAGE_TYPE_LIST 1 +#define DISPLAY_NMEA_MESSAGES 1 +#define DISPLAY_STRINGS 0 + +#define MESSAGE_LENGTH(length) (3 + length + 3) + +#define BINARY_MESSAGE_START 0xd3 + +#define MAX_BAD_CHARACTERS 1000 + +uint32_t bad_characters[256>>5]; +uint32_t bad_character_count[256]; +uint32_t bad_character_offset[MAX_BAD_CHARACTERS]; +uint32_t bad_character_length[MAX_BAD_CHARACTERS]; +int32_t bad_character_offset_count = -1; + +char buffer[65536]; +char string[256]; +uint8_t * file_data; +uint32_t rtcm_messages[4096 >> 5]; +uint32_t rtcm_message_count[4096]; +uint32_t rtcm_max_message_length[4096]; +uint32_t ubx_messages[65536 >> 5]; +uint32_t ubx_message_count[65536]; +uint32_t ubx_max_message_length[65536]; +int bad_checksum_header; +int nmea_checksum_errors; +int rtcm_crc_errors; +int ubx_checksum_errors; + +typedef struct _NMEA_MESSAGE { + struct _NMEA_MESSAGE * next; + uint8_t * message; + uint32_t count; + uint32_t max_length; +} NMEA_MESSAGE; + +NMEA_MESSAGE * nmea_list; + +enum SentenceTypes +{ +SENTENCE_TYPE_NONE = 0, +SENTENCE_TYPE_NMEA, +SENTENCE_TYPE_UBX, +SENTENCE_TYPE_RTCM +} currentSentence = SENTENCE_TYPE_NONE; + +typedef struct _PARSE_STATE * P_PARSE_STATE; + +//Parse routine +typedef uint8_t (* PARSE_ROUTINE)(P_PARSE_STATE parse, //Parser state + uint8_t data); //Incoming data byte + +//End of message callback routine +typedef void (* EOM_CALLBACK)(P_PARSE_STATE parse, //Parser state + uint8_t type); //Message type + +#define PARSE_BUFFER_LENGTH 0x10000 + +typedef struct _PARSE_STATE +{ + PARSE_ROUTINE state; //Parser state routine + EOM_CALLBACK eomCallback; //End of message callback routine + const char * parserName; //Name of parser + uint32_t crc; //RTCM computed CRC + uint32_t rtcmCrc; //Computed CRC value for the RTCM message + uint32_t invalidRtcmCrcs; //Number of bad RTCM CRCs detected + uint16_t bytesRemaining; //Bytes remaining in RTCM CRC calculation + uint16_t length; //Message length including line termination + uint16_t maxLength; //Maximum message length including line termination + uint16_t message; //RTCM message number + uint16_t nmeaLength; //Length of the NMEA message without line termination + uint8_t buffer[PARSE_BUFFER_LENGTH]; //Buffer containing the message + uint8_t nmeaMessageName[16]; //Message name + uint8_t nmeaMessageNameLength; //Length of the message name + uint8_t ck_a; //U-blox checksum byte 1 + uint8_t ck_b; //U-blox checksum byte 2 + bool computeCrc; //Compute the CRC when true +} PARSE_STATE; + +//Forward declaration +uint8_t waitForPreamble(PARSE_STATE * parse, uint8_t data); +uint64_t offset; +uint64_t file_offset; + +void +dump_message ( + unsigned char * data + ) +{ + unsigned int actual; + unsigned int crc; + int index; + int length; + int offset; + + // + // +----------+--------+----------------+---------+----------+---------+ + // | Preamble | Fill | Message Length | Message | Fill | CRC-24Q | + // | 8 bits | 6 bits | 10 bits | n-bits | 0-7 bits | 24 bits | + // | 0xd3 | 000000 | | | zeros | | + // +----------+--------+----------------+---------+----------+---------+ + // + + // Get the number of bytes + length = (data[1] << 8) | data[2]; + if (DISPLAY_DATA_BYTES) { + // Dump the message + offset = data - file_data; + printf ("0x%08x: %02x %02x %02x\n", offset, data[0], data[1], data[2]); + offset += 3; + index = 0; + if (length > 0) { + do + { + // Display the offset at the beginning of the line + if (!(index & 0xf)) + printf ("0x%08x: ", offset); + + // Display the data bytes + printf ("%02x ", data[index+3]); + offset += 1; + + // Terminate the lines + if (!(++index % (DISPLAY_DATA_BYTES ? DISPLAY_DATA_BYTES : 1))) + printf ("\n"); + } while (index < length); + + // Terminate a short line + if (index % (DISPLAY_DATA_BYTES ? DISPLAY_DATA_BYTES : 1)) + printf ("\n"); + } + + // Display the Cyclic Redundancy Check (CRC) + printf ("0x%08x: %02x %02x %02x CRC: %s\n", + offset, data[3 + index], data[3 + index + 1], data[3 + index + 2], + crc24q_check (data, MESSAGE_LENGTH(length)) ? "Matches" : "Does not match!"); + } else { + if (!crc24q_check (data, MESSAGE_LENGTH(length))) { + if (!bad_checksum_header) { + bad_checksum_header = 1; + printf ("Bad checksums:\n"); + } + length = MESSAGE_LENGTH(length); + crc = crc24q_hash(data, length - 3); + actual = (data[length - 3] << 16) | (data[length - 2] << 8) | data[length - 1]; + printf (" 0x%08lx: Binary message, expected CRC: 0x%06x, actual CRC: 0x%06x\n", + data - file_data, crc, actual); + rtcm_crc_errors += 1; + } + } +} + +void +display_string ( + unsigned char * string, + int length + ) +{ + char * temp; + char * temp_end; + + // Copy the strings into the buffer + memcpy (buffer, string, length); + buffer[length] = 0; + + // Terminate the strings + temp = buffer; + temp_end = &temp[length]; + do { + if ((*temp == '\r') || (*temp == '\n')) + *temp = 0; + } while (++temp < temp_end); + + // Display the strings + temp = buffer; + while (temp < temp_end) { + if (*temp) + printf ("%s\n", temp); + temp += strlen(temp) + 1; + } +} + +unsigned char * +process_nmea_message ( + unsigned char * data, + unsigned char * data_end + ) +{ + unsigned char checksum; + char checksum_char[2]; + NMEA_MESSAGE * message; + NMEA_MESSAGE * previous; + unsigned char * start; + + // Check for the beginning of a NMEA message ($) + if (*data != '$') { + if ((*data != '\r') && (*data != '\n')) { + bad_character_count[*data] += 1; + bad_characters[*data >> 5] |= 1 << (*data & 0x1f); + if ((bad_character_offset_count < 0) + || ((bad_character_offset[bad_character_offset_count] + bad_character_length[bad_character_offset_count]) != (data - file_data))) { + bad_character_offset_count += 1; + bad_character_offset[bad_character_offset_count] = data - file_data; + } + bad_character_length[bad_character_offset_count] += 1; + } + return data + 1; + } + + // Skip over the dollar sign ($) + start = data++; + + // Scan for comma or end of message + checksum = 0; + while ((*data != ',') && (*data != '\r') && (*data != '\n') + && (*data != BINARY_MESSAGE_START)) + checksum ^= *data++; + + // Return if this is the start of a binary message + if (*data == BINARY_MESSAGE_START) + return data; + + // Remember the message if a comma was found + if (*data == ',') { + if (DISPLAY_LIST) + printf ("----------------------\r\n"); + + // Build the zero terminated string + memset (string, 0, sizeof(string)); + strncpy (string, (char *)start, data - start); + + /* + list --> NULL + + previous = NULL + string: $GNGST --. + V + +------------+ + list --> | $GNGST (1) | --> NULL + +------------+ + + previous = $GNGST + string: $GNGGA --. + V + +------------+ +------------+ + list --> | $GNGGA (1) | --> | $GNGST (1) | --> NULL + +------------+ +------------+ + + previous = $GNGSA + string: $GNGSA --------------------. + V + +------------+ +------------+ +------------+ + list --> | $GNGGA (1) | --> | $GNGSA (1) | --> | $GNGST (1) | --> NULL + +------------+ +------------+ +------------+ + + previous = $GNGSA + string: $GNGSA --------------------. + V + +------------+ +------------+ +------------+ + list --> | $GNGGA (1) | --> | $GNGSA (2) | --> | $GNGST (1) | --> NULL + +------------+ +------------+ +------------+ + + previous = $GNGST + string: $GNRMC ---------------------------------------------------------. + V + +------------+ +------------+ +------------+ +------------+ + list --> | $GNGGA (1) | --> | $GNGSA (2) | --> | $GNGST (1) | --> | $GNRMC (1) | --> NULL + +------------+ +------------+ +------------+ +------------+ + + */ + + // Display the list + if (DISPLAY_LIST) { + printf ("list: "); + for (previous = nmea_list; previous; previous = previous->next) + printf ("%s(%d)-->", previous->message, previous->count); + printf ("NULL\n"); + } + + // Find the location for insertion + // Check for something in the list + previous = NULL; + if (nmea_list) { + if (strcmp((char *)nmea_list->message, string) <= 0) { + previous = nmea_list; + while (previous->next && (strcmp((char *)previous->next->message, string) <= 0)) + previous = previous->next; + } + } + + // Display the insertion decision + if (DISPLAY_LIST) { + printf ("previous: %s\n", ((previous != NULL) ? (char *)previous->message : (char *)"NULL")); + printf ("string: %s\n", string); + } + + // Check for a duplicate message + if (previous && (strcmp ((char *)previous->message, string) == 0)) { + if (DISPLAY_LIST) + printf ("Duplicate found: %d\n", previous->count); + previous->count += 1; + message = previous; + } else { + // Add the new message + message = malloc (sizeof(NMEA_MESSAGE) + strlen(string) + 1); + message->message = (uint8_t *)(message + 1); + strcpy((char *)message->message, string); + message->count = 1; + + // Message insertion position + // previous == NULL; ==> list head + // previous != NULL; ==> middle or end of list + if (!previous) { + if (DISPLAY_LIST) + printf ("Add to head of the list\n"); + + // Add this message at the start of the list + message->next = nmea_list; + nmea_list = message; + } else { + if (DISPLAY_LIST) + printf ("Add to middle of the list\n"); + + // Insert this message into the middle of the list + message->next = previous->next; + previous->next = message; + } + } + + // Display the new list + if (DISPLAY_LIST) { + printf ("New list: "); + for (previous = nmea_list; previous; previous = previous->next) + printf ("%s(%d)-->", previous->message, previous->count); + printf ("NULL\n"); + } + } + + // Scan for asterisk or end of message + while ((*data != '*') && (*data != '\r') && (*data != '\n') + && (*data != BINARY_MESSAGE_START)) + checksum ^= *data++; + + // Check for end of NMEA message, validate the checksum + if (*data == '*') { + checksum_char[0] = ((checksum >> 4) & 0xf) + '0'; + if (checksum_char[0] > '9') + checksum_char[0] += 'A' - '0' - 10; + checksum_char[1] = (checksum & 0xf) + '0'; + if (checksum_char[1] > '9') + checksum_char[1] += 'A' - '0' - 10; + if ((toupper(data[1]) != checksum_char[0]) || (toupper(data[2]) != checksum_char[1])) { + if (!bad_checksum_header) { + bad_checksum_header = 1; + printf ("Bad checksums:\n"); + } + printf (" 0x%08lx: NMEA %s, expected CRC: 0x%02x, actual CRC: 0x%c%c\n", + data - file_data, &string[1], checksum, data[1], data[2]); + nmea_checksum_errors += 1; + } + data += 3; + } + + // Scan for end of message + while ((*data != '\r') && (*data != '\n') && (*data != BINARY_MESSAGE_START)) + data++; + + return data; +} + +uint8_t * +find_gnss_header ( + uint8_t * data, + uint8_t * data_end + ) +{ + int length; + + do { + // From RTCM 10403.2 Section 4 + // + // +----------+--------+----------------+---------+----------+---------+ + // | Preamble | Fill | Message Length | Message | Fill | CRC-24Q | + // | 8 bits | 6 bits | 10 bits | n-bits | 0-7 bits | 24 bits | + // | 0xd3 | 000000 | | | zeros | | + // +----------+--------+----------------+---------+----------+---------+ + // + + // Locate the beginning of the message + while ((data < data_end) && (*data != BINARY_MESSAGE_START)) + data = process_nmea_message (data, data_end); + + // Check for end of data + if (data >= data_end) + break; + + // Get the number of bytes + length = (data[1] << 8) | data[2]; + if ((data + MESSAGE_LENGTH(length)) <= data_end) { + + // Verify the CRC + if (crc24q_check (data, MESSAGE_LENGTH(length))) + break; + } + + // Skip this preamble byte + data++; + } while (data < data_end); + + // Return the address of the next preamble byte or end of data + return data; +} + +void processNemaMessage(PARSE_STATE * parse) +{ + NMEA_MESSAGE * message; + NMEA_MESSAGE * previous; + + //Display the NMEA message +// dumpBuffer(parse->buffer, parse->length); +// printf(" %s NMEA %s, %d bytes\r\n", parse->parserName, parse->nmeaMessageName, parse->length); + + // Display the list + if (DISPLAY_LIST) { + printf ("list: "); + for (previous = nmea_list; previous; previous = previous->next) + printf ("%s(%d)-->", previous->message, previous->count); + printf ("NULL\n"); + } + + // Find the location for insertion + // Check for something in the list + previous = NULL; + if (nmea_list) { + if (strcmp((char *)nmea_list->message, (char *)parse->nmeaMessageName) <= 0) { + previous = nmea_list; + while (previous->next && (strcmp((char *)previous->next->message, (char *)parse->nmeaMessageName) <= 0)) + previous = previous->next; + } + } + + // Display the insertion decision + if (DISPLAY_LIST) { + printf ("previous: %s\n", ((previous != NULL) ? (char *)previous->message : (char *)"NULL")); + printf ("string: %s\n", parse->nmeaMessageName); + } + + // Check for a duplicate message + if (previous && (strcmp ((char *)previous->message, (char *)parse->nmeaMessageName) == 0)) { + if (DISPLAY_LIST) + printf ("Duplicate found: %d\n", previous->count); + previous->count += 1; + message = previous; + if (message->max_length < parse->length) + message->max_length = parse->length; + } else { + // Add the new message + message = malloc (sizeof(NMEA_MESSAGE) + strlen((char *)parse->nmeaMessageName) + 1); + message->message = (uint8_t *)(message + 1); + strcpy((char *)message->message, (char *)parse->nmeaMessageName); + message->count = 1; + message->max_length = parse->length; + + // Message insertion position + // previous == NULL; ==> list head + // previous != NULL; ==> middle or end of list + if (!previous) { + if (DISPLAY_LIST) + printf ("Add to head of the list\n"); + + // Add this message at the start of the list + message->next = nmea_list; + nmea_list = message; + } else { + if (DISPLAY_LIST) + printf ("Add to middle of the list\n"); + + // Insert this message into the middle of the list + message->next = previous->next; + previous->next = message; + } + } + + // Display the new list + if (DISPLAY_LIST) { + printf ("New list: "); + for (previous = nmea_list; previous; previous = previous->next) + printf ("%s(%d)-->", previous->message, previous->count); + printf ("NULL\n"); + } +} + +void processRtcmMessage(PARSE_STATE * parse) +{ +// dumpBuffer(parse->buffer, parse->length); +// printf(" %s RTCM %d, %d bytes\r\n", parse->parserName, parse->message, parse->length); + rtcm_messages[parse->message >> 5] |= 1 << (parse->message & 0x1f); + rtcm_message_count[parse->message] += 1; + if (rtcm_max_message_length[parse->message] < parse->length) + rtcm_max_message_length[parse->message] = parse->length; +} + +void processUbxMessage(PARSE_STATE * parse) +{ +// dumpBuffer(parse->buffer, parse->length); +// printf(" %s UBX %d.%d, %d bytes\r\n", parse->parserName, parse->message >> 8, parse->message & 0xff, parse->length); + ubx_messages[parse->message >> 5] |= 1 << (parse->message & 0x1f); + ubx_message_count[parse->message] += 1; + if (ubx_max_message_length[parse->message] < parse->length) + ubx_max_message_length[parse->message] = parse->length; +} + +//Process the message +void processMessage(PARSE_STATE * parse, uint8_t type) +{ + switch (type) + { + case SENTENCE_TYPE_NMEA: + processNemaMessage(parse); + break; + + case SENTENCE_TYPE_RTCM: + processRtcmMessage(parse); + break; + + case SENTENCE_TYPE_UBX: + processUbxMessage(parse); + break; + + default: + printf ("Unknown message type: %d\r\n", type); + break; + } +} + +//Convert nibble to ASCII +uint8_t nibbleToAscii(int nibble) +{ + nibble &= 0xf; + return (nibble > 9) ? nibble + 'a' - 10 : nibble + '0'; +} + +//Convert nibble to ASCII +int AsciiToNibble(int data) +{ + //Convert the value to lower case + data |= 0x20; + if ((data >= 'a') && (data <= 'f')) + return data - 'a' + 10; + if ((data >= '0') && (data <= '9')) + return data - '0'; + return -1; +} + +void dumpBuffer(uint8_t * buffer, uint16_t length) +{ + unsigned int bytes; + uint8_t * end; + unsigned int index; + + end = &buffer[length]; + while (buffer < end) + { + //Determine the number of bytes to display on the line + bytes = end - buffer; + if (bytes > (16 - (offset & 0xf))) + bytes = 16 - (offset & 0xf); + + //Display the offset + printf("0x%08lx: ", offset); + + //Skip leading bytes + for (index = 0; index < (offset & 0xf); index++) + printf(" "); + + //Display the data bytes + for (index = 0; index < bytes; index++) + printf("%02x ", buffer[index]); + + //Separate the data bytes from the ASCII + for (; index < (16 - (offset & 0xf)); index++) + printf(" "); + printf(" "); + + //Skip leading bytes + for (index = 0; index < (offset & 0xf); index++) + printf(" "); + + //Display the ASCII values + for (index = 0; index < bytes; index++) + printf("%c", ((buffer[index] < ' ') || (buffer[index] >= 0x7f)) + ? '.' : buffer[index]); + printf("\r\n"); + + //Set the next line of data + buffer += bytes; + offset += bytes; + } +} + +//Read the line termination +uint8_t nmeaLineTermination(PARSE_STATE * parse, uint8_t data) +{ + unsigned int checksum; + + //Process the line termination + if ((data != '\r') && (data != '\n')) + { + //Don't include this character in the buffer + parse->length--; + + //Convert the checksum characters into binary + checksum = AsciiToNibble(parse->buffer[parse->nmeaLength - 2]) << 4; + checksum |= AsciiToNibble(parse->buffer[parse->nmeaLength - 1]); + + //Validate the checksum + if (checksum == parse->crc) + parse->crc = 0; + if (parse->crc) + { + nmea_checksum_errors += 1; + dumpBuffer(parse->buffer, parse->length); + if ((AsciiToNibble(parse->buffer[parse->nmeaLength-2]) >= 0) + && (AsciiToNibble(parse->buffer[parse->nmeaLength-1]) >= 0)) + printf (" %s NMEA %s, %2d bytes, bad checksum, expecting 0x%c%c, computed: 0x%02x\r\n", + parse->parserName, + parse->nmeaMessageName, + parse->length, + parse->buffer[parse->nmeaLength-2], + parse->buffer[parse->nmeaLength-1], + parse->crc); + else + printf (" %s NMEA %s, %2d bytes, invalid checksum bytes 0x%02x 0x%02x, computed: 0x%02x\r\n", + parse->parserName, + parse->nmeaMessageName, + parse->length, + parse->buffer[parse->nmeaLength-2], + parse->buffer[parse->nmeaLength-1], + parse->crc); + } + + //Process this message + parse->eomCallback(parse, SENTENCE_TYPE_NMEA); + + //Add this character to the beginning of the buffer + parse->buffer[0] = data; + parse->length = 1; + return waitForPreamble(parse, data); + } + return SENTENCE_TYPE_NMEA; +} + +//Read the linefeed +uint8_t nmeaLinefeed(PARSE_STATE * parse, uint8_t data) +{ + unsigned int checksum; + uint8_t sentenceType; + + //Convert the checksum characters into binary + checksum = AsciiToNibble(parse->buffer[parse->nmeaLength - 2]) << 4; + checksum |= AsciiToNibble(parse->buffer[parse->nmeaLength - 1]); + + //Update the maximum message length + if (parse->length > parse->maxLength) + { + parse->maxLength = parse->length; + printf("maxLength: %d bytes\r\n", parse->maxLength); + } + + //Validate the checksum + if (checksum == parse->crc) + parse->crc = 0; + if (parse->crc) + { + nmea_checksum_errors += 1; + dumpBuffer(parse->buffer, parse->length); + printf (" %s NMEA %s, %2d bytes, bad checksum, expecting 0x%c%c, computed: 0x%02x\r\n", + parse->parserName, + parse->nmeaMessageName, + parse->length, + parse->buffer[parse->nmeaLength-2], + parse->buffer[parse->nmeaLength-1], + parse->crc); + } + + //Process this message + parse->state = waitForPreamble; + parse->eomCallback(parse, SENTENCE_TYPE_NMEA); + + //Search for another preamble byte + parse->length = 0; + parse->crc = 0; + return SENTENCE_TYPE_NONE; +} + +//Read the carriage return +uint8_t nmeaCarriageReturn(PARSE_STATE * parse, uint8_t data) +{ + parse->state = nmeaLinefeed; + return SENTENCE_TYPE_NMEA; +} + +//Read the second checksum byte +uint8_t nmeaChecksumByte2(PARSE_STATE * parse, uint8_t data) +{ + parse->nmeaLength = parse->length; +// parse->state = nmeaLineTermination; + parse->state = nmeaCarriageReturn; + return SENTENCE_TYPE_NMEA; +} + +//Read the first checksum byte +uint8_t nmeaChecksumByte1(PARSE_STATE * parse, uint8_t data) +{ + parse->state = nmeaChecksumByte2; + return SENTENCE_TYPE_NMEA; +} + +//Read the message data +uint8_t nmeaFindAsterisk(PARSE_STATE * parse, uint8_t data) +{ + if (data != '*') + parse->crc ^= data; + else + parse->state = nmeaChecksumByte1; + return SENTENCE_TYPE_NMEA; +} + +//Read the message name +uint8_t nmeaFindFirstComma(PARSE_STATE * parse, uint8_t data) +{ + parse->crc ^= data; + if ((data != ',') || (parse->nmeaMessageNameLength == 0)) + { + if ((data < 'A') || (data > 'Z')) + { + //Display the invalid data + dumpBuffer(parse->buffer, parse->length - 1); + printf (" %s Invalid NMEA data, %d bytes\r\n", + parse->parserName, parse->length - 1); + + parse->buffer[0] = data; + parse->crc = 0; + parse->length = 1; + return waitForPreamble (parse, data); + } + + //Save the message name + parse->nmeaMessageName[parse->nmeaMessageNameLength++] = data; + } + else + { + //Zero terminate the message name + parse->nmeaMessageName[parse->nmeaMessageNameLength++] = 0; + parse->state = nmeaFindAsterisk; + } + return SENTENCE_TYPE_NMEA; +} + +//Read the CRC +uint8_t rtcmReadCrc(PARSE_STATE * parse, uint8_t data) +{ + uint16_t dataSent; + + //Account for this data byte + parse->bytesRemaining -= 1; + + //Wait until all the data is received + if (parse->bytesRemaining > 0) + return SENTENCE_TYPE_RTCM; + + //Update the maximum message length + if (parse->length > parse->maxLength) + { + parse->maxLength = parse->length; + printf("maxLength: %d bytes\r\n", parse->maxLength); + } + + //Display the RTCM messages with bad CRC + parse->crc &= 0x00ffffff; + if (parse->crc) + { + rtcm_crc_errors += 1; + dumpBuffer(parse->buffer, parse->length); + printf (" %s RTCM %d, %2d bytes, bad CRC, expecting 0x%02x%02x%02x, computed: 0x%06x\r\n", + parse->parserName, + parse->message, + parse->length, + parse->buffer[parse->length-3], + parse->buffer[parse->length-2], + parse->buffer[parse->length-1], + parse->rtcmCrc); + } + + //Process the message + parse->state = waitForPreamble; + parse->eomCallback(parse, SENTENCE_TYPE_RTCM); + + //Search for another preamble byte + parse->length = 0; + parse->computeCrc = false; + parse->crc = 0; + return SENTENCE_TYPE_NONE; +} + +//Read the rest of the message +uint8_t rtcmReadData(PARSE_STATE * parse, uint8_t data) +{ + uint16_t dataSent; + + //Account for this data byte + parse->bytesRemaining -= 1; + + //Wait until all the data is received + if (parse->bytesRemaining <= 0) + { + parse->rtcmCrc = parse->crc & 0x00ffffff; + parse->bytesRemaining = 3; + parse->state = rtcmReadCrc; + } + return SENTENCE_TYPE_RTCM; +} + +//Read the lower 4 bits of the message number +uint8_t rtcmReadMessage2(PARSE_STATE * parse, uint8_t data) +{ + parse->message |= data >> 4; + parse->bytesRemaining -= 1; + parse->state = rtcmReadData; + return SENTENCE_TYPE_RTCM; +} + +//Read the upper 8 bits of the message number +uint8_t rtcmReadMessage1(PARSE_STATE * parse, uint8_t data) +{ + parse->message = data << 4; + parse->bytesRemaining -= 1; + parse->state = rtcmReadMessage2; + return SENTENCE_TYPE_RTCM; +} + +//Read the lower 8 bits of the length +uint8_t rtcmReadLength2(PARSE_STATE * parse, uint8_t data) +{ + parse->bytesRemaining |= data; + parse->state = rtcmReadMessage1; + return SENTENCE_TYPE_RTCM; +} + +//Read the upper two bits of the length +uint8_t rtcmReadLength1(PARSE_STATE * parse, uint8_t data) +{ + //Verify the length byte + if (data & (~3)) + { + //Display the invalid data + dumpBuffer(parse->buffer, parse->length - 1); + printf (" %s Invalid RTCM data, %d bytes\r\n", parse->parserName, parse->length - 1); + + //Invalid length, place this byte at the beginning of the buffer + parse->buffer[0] = data; + parse->length = 1; + parse->computeCrc = false; + parse->crc = 0; + + //Start searching for a preamble byte + return waitForPreamble(parse, data); + } + + //Save the upper 2 bits of the length + parse->bytesRemaining = data << 8; + parse->state = rtcmReadLength2; + return SENTENCE_TYPE_RTCM; +} + +//Read the CK_B byte +uint8_t ubloxCkB(PARSE_STATE * parse, uint8_t data) +{ + bool badChecksum; + + //Update the maximum message length + if (parse->length > parse->maxLength) + { + parse->maxLength = parse->length; + printf("maxLength: %d bytes\r\n", parse->maxLength); + } + + //Validate the checksum + badChecksum = ((parse->buffer[parse->length - 2] != parse->ck_a) + || (parse->buffer[parse->length - 1] != parse->ck_b)); + if (badChecksum) + { + ubx_checksum_errors += 1; + dumpBuffer(parse->buffer, parse->length); + printf (" %s U-Blox %d.%d, %2d bytes, bad checksum, expecting 0x%02x%02x, computed: 0x%02x%02x\r\n", + parse->parserName, + parse->message >> 8, + parse->message & 0xff, + parse->length, + parse->buffer[parse->nmeaLength-2], + parse->buffer[parse->nmeaLength-1], + parse->ck_a, + parse->ck_b); + } + + //Process this message + parse->state = waitForPreamble; + parse->eomCallback(parse, SENTENCE_TYPE_UBX); + + //Search for the next preamble byte + parse->length = 0; + parse->crc = 0; + parse->ck_a = 0; + parse->ck_b = 0; + return SENTENCE_TYPE_NONE; +} + +//Read the CK_A byte +uint8_t ubloxCkA(PARSE_STATE * parse, uint8_t data) +{ + parse->state = ubloxCkB; + return SENTENCE_TYPE_UBX; +} + +//Read the payload +uint8_t ubloxPayload(PARSE_STATE * parse, uint8_t data) +{ + //Compute the checksum over the payload + if (parse->bytesRemaining--) + { + //Calculate the checksum + parse->ck_a += data; + parse->ck_b += parse->ck_a; + return SENTENCE_TYPE_UBX; + } + return ubloxCkA(parse, data); +} + +//Read the second length byte +uint8_t ubloxLength2(PARSE_STATE * parse, uint8_t data) +{ + //Calculate the checksum + parse->ck_a += data; + parse->ck_b += parse->ck_a; + + //Save the second length byte + parse->bytesRemaining |= ((uint16_t)data) << 8; + parse->state = ubloxPayload; + return SENTENCE_TYPE_UBX; +} + +//Read the first length byte +uint8_t ubloxLength1(PARSE_STATE * parse, uint8_t data) +{ + //Calculate the checksum + parse->ck_a += data; + parse->ck_b += parse->ck_a; + + //Save the first length byte + parse->bytesRemaining = data; + parse->state = ubloxLength2; + return SENTENCE_TYPE_UBX; +} + +//Read the ID byte +uint8_t ubloxId(PARSE_STATE * parse, uint8_t data) +{ + //Calculate the checksum + parse->ck_a += data; + parse->ck_b += parse->ck_a; + + //Save the ID as the lower 8-bits of the message + parse->message |= data; + parse->state = ubloxLength1; + return SENTENCE_TYPE_UBX; +} + +//Read the class byte +uint8_t ubloxClass(PARSE_STATE * parse, uint8_t data) +{ + //Start the checksum calculation + parse->ck_a = data; + parse->ck_b = data; + + //Save the class as the upper 8-bits of the message + parse->message = ((uint16_t)data) << 8; + parse->state = ubloxId; + return SENTENCE_TYPE_UBX; +} + +//Read the second sync byte +uint8_t ubloxSync2(PARSE_STATE * parse, uint8_t data) +{ + //Verify the sync 2 byte + if (data != 0x62) + { + //Display the invalid data + dumpBuffer(parse->buffer, parse->length - 1); + printf (" %s Invalid UBX data, %d bytes\r\n", parse->parserName, parse->length - 1); + + //Invalid sync 2 byte, place this byte at the beginning of the buffer + parse->length = 0; + parse->buffer[parse->length++] = data; + + //Start searching for a preamble byte + return waitForPreamble(parse, data); + } + + parse->state = ubloxClass; + return SENTENCE_TYPE_UBX; +} + +//Wait for the preamble byte (0xd3) +uint8_t waitForPreamble(PARSE_STATE * parse, uint8_t data) +{ + //Verify that this is the preamble byte + offset = file_offset; + switch(data) + { + case '$': + + // + // NMEA Message + // + // +----------+---------+--------+---------+----------+----------+ + // | Preamble | Name | Comma | Data | Asterisk | Checksum | + // | 8 bits | n bytes | 8 bits | n bytes | 8 bits | 2 bytes | + // | $ | | , | | | | + // +----------+---------+--------+---------+----------+----------+ + // | | + // |<------------------- Checksum ------------------->| + // + + parse->crc = 0; + parse->computeCrc = false; + parse->nmeaMessageNameLength = 0; + parse->state = nmeaFindFirstComma; + return SENTENCE_TYPE_NMEA; + + case 0xb5: + + // + // U-BLOX Message + // + // |<-- Preamble --->| + // | | + // +--------+--------+---------+--------+---------+---------+--------+--------+ + // | SYNC | SYNC | Class | ID | Length | Payload | CK_A | CK_B | + // | 8 bits | 8 bits | 8 bits | 8 bits | 2 bytes | n bytes | 8 bits | 8 bits | + // | 0xb5 | 0x62 | | | | | | | + // +--------+--------+---------+--------+---------+---------+--------+--------+ + // | | + // |<------------- Checksum ------------->| + // + // 8-Bit Fletcher Algorithm, which is used in the TCP standard (RFC 1145) + // http://www.ietf.org/rfc/rfc1145.txt + // Checksum calculation + // Initialization: CK_A = CK_B = 0 + // CK_A += data + // CK_B += CK_A + // + + parse->state = ubloxSync2; + return SENTENCE_TYPE_UBX; + + case 0xd3: + + // + // RTCM Standard 10403.2 - Chapter 4, Transport Layer + // + // |<------------- 3 bytes ------------>|<----- length ----->|<- 3 bytes ->| + // | | | | + // +----------+--------+----------------+---------+----------+-------------+ + // | Preamble | Fill | Message Length | Message | Fill | CRC-24Q | + // | 8 bits | 6 bits | 10 bits | n-bits | 0-7 bits | 24 bits | + // | 0xd3 | 000000 | (in bytes) | | zeros | | + // +----------+--------+----------------+---------+----------+-------------+ + // | | + // |<-------------------------------- CRC -------------------------------->| + // + + //Start the CRC with this byte + parse->crc = 0; + parse->crc = COMPUTE_CRC24Q(parse, data); + parse->computeCrc = true; + + //Get the message length + parse->state = rtcmReadLength1; + return SENTENCE_TYPE_RTCM; + } + + //preamble byte not found + dumpBuffer(parse->buffer, parse->length); + printf (" %s invalid byte 0x%02x\r\n", parse->parserName, data); + parse->length = 0; + parse->state = waitForPreamble; + return SENTENCE_TYPE_NONE; +} + +int +main ( + int argc, + char ** argv + ) +{ + int bit; + unsigned char * data; + unsigned char * data_end; + int file; + char * filename; + off_t file_size; + int index; + int length; + int message_number; + int message_type; + static PARSE_STATE parse; + int total; + uint8_t value; + + // Initialize the parser + parse.state = waitForPreamble; + parse.eomCallback = processMessage; + parse.parserName = "Tx"; + + // Open the log file + filename = argv[1]; + file = open (filename, O_RDONLY); + if (file < 0) { + perror ("ERROR - Failed to open the file"); + return -1; + } + + // Determine the file length + file_size = lseek (file, 0, SEEK_END); + + // Get the file buffer + file_data = malloc (file_size); + if (!file_data) { + fprintf (stderr, "ERROR - Failed to allocate file buffer!\n"); + return -2; + } + + // Read the file into memory + lseek (file, 0, SEEK_SET); + if (read (file, file_data, file_size) != file_size) { + fprintf (stderr, "ERROR - Failed to read the file into memory!\n"); + } + + // Close the file + close (file); + + // Skip the first byte to force unaligned start + data = file_data; + data_end = &data[file_size]; + while (data < data_end) { + file_offset = data - file_data; + + //Save the data byte + value = *data; + parse.buffer[parse.length++] = value; + + //Compute the CRC value for the message + if (parse.computeCrc) + parse.crc = COMPUTE_CRC24Q(&parse, value); + + //Parse this message + parse.state(&parse, value); + data++; + } + + // Display the checksum and CRC errors + if (nmea_checksum_errors) + printf (" Total NMEA checksum errors: %d\n", nmea_checksum_errors); + if (rtcm_crc_errors) + printf (" Total RTCM message CRC errors: %d\n", rtcm_crc_errors); + if (ubx_checksum_errors) + printf (" Total UBX message checksum errors: %d\n", ubx_checksum_errors); + + // Display the NMEA message list + if (DISPLAY_NMEA_MESSAGES) { + printf ("NMEA Message List:\n"); + NMEA_MESSAGE * message = nmea_list; + while (message) { + printf (" %s: %d %s, max length: %d bytes\n", message->message, message->count, + (message->count == 1) ? "time" : "times", message->max_length); + message = message->next; + } + } + + // Display the RTCM message type list + if (DISPLAY_RTCM_MESSAGE_LIST) { + printf ("RTCM Message List:\n"); + for (index = 0; index < (int)(sizeof(rtcm_messages) / sizeof(rtcm_messages[0])); index++) { + if (rtcm_messages[index]) + for (bit = 0; bit < 32; bit++) { + message_number = (index << 5) | bit; + if (rtcm_messages[index] & (1 << bit)) + printf (" %d (%02x %xx): %d %s, max length: %d bytes\n", message_number, + message_number >> 4, message_number & 0xf, + rtcm_message_count[message_number], + (rtcm_message_count[message_number] == 1) ? "time" : "times", + rtcm_max_message_length[message_number]); + } + } + } + + // Display the UBX message type list + if (DISPLAY_UBX_MESSAGE_LIST) { + printf ("UBX Message List:\n"); + for (index = 0; index < (int)(sizeof(ubx_messages) / sizeof(ubx_messages[0])); index++) { + if (ubx_messages[index]) + for (bit = 0; bit < 32; bit++) { + message_number = (index << 5) | bit; + if (ubx_messages[index] & (1 << bit)) + printf (" %d.%d (0x%02x.%02x): %d %s, max length: %d bytes\n", + message_number >> 8, message_number & 0xff, + message_number >> 8, message_number & 0xff, + ubx_message_count[message_number], + (ubx_message_count[message_number] == 1) ? "time" : "times", + ubx_max_message_length[message_number]); + } + } + } + + // Display the bad characters + if (DISPLAY_BAD_CHARACTERS) { + printf ("Bad characters:\n"); + total = 0; + for (index = 0; index < 256; index++) { + if (bad_characters[index >> 5] & (1 << (index & 0x1f))) { + printf (" 0x%02x: %d\n", index, bad_character_count[index]); + total += bad_character_count[index]; + } + } + printf (" Total: %d\n", total); + } + + // Display the bad character offsets + if (DISPLAY_BAD_CHARACTER_OFFSETS) { + printf ("Bad character offsets:\n"); + total = 0; + for (index = 0; index <= bad_character_offset_count; index++) { + printf (" 0x%08x: %d bytes\n", bad_character_offset[index], bad_character_length[index]); + total += bad_character_length[index]; + } + printf (" Total: %d\n", total); + } +} diff --git a/Firmware/Tools/X.509_crt_bundle_bin_to_c.c b/Firmware/Tools/X.509_crt_bundle_bin_to_c.c new file mode 100644 index 000000000..3b0aeb24b --- /dev/null +++ b/Firmware/Tools/X.509_crt_bundle_bin_to_c.c @@ -0,0 +1,99 @@ +/* + * X.509_crt_bundle_bin_to_c.c + * + * Program to convert the .bin file into a "c" data structure + */ + +#include +#include +#include +#include +#include + +#define BYTES_MAX 16 + +int main(int argc, char ** argv) +{ + uint8_t buffer[1024]; + int bundleFile; + ssize_t bytesRead; + int bytesToDisplay; + uint8_t * data; + uint8_t * dataEnd; + int fileOffset; + int index; + ssize_t offset; + int status; + + bundleFile = 0; + do + { + // Display the help text + if (argc != 2) + { + printf("%s x509_crt_bundle_bin_file\n", argv[0]); + status = -1; + break; + } + + // Open the binary file containing the certificate bundle + bundleFile = open(argv[1], O_RDONLY); + if (bundleFile < 0) + { + status = errno; + perror("ERROR: Unable to open certificate bundle file\n"); + break; + } + + // Display the header + printf("const uint8_t x509CertificateBundle[] =\n"); + printf("{\n"); + + // Assume success + status = 0; + + // Walk through the BIN file data + fileOffset = 0; + while (1) + { + // Read more data from the file + bytesRead = read(bundleFile, buffer, sizeof(buffer)); + if (bytesRead < 0) + { + status = errno; + perror("ERROR: Failed during read of BIN file\n"); + break; + } + + // Check for end of file + if (!bytesRead) + { + printf("};\n\n"); + break; + } + + // Display the data read from the file + data = &buffer[0]; + dataEnd = &data[bytesRead]; + while (data < dataEnd) + { + // Display the row of data + bytesToDisplay = dataEnd - data; + if (bytesToDisplay > BYTES_MAX) + bytesToDisplay = BYTES_MAX; + printf(" "); + for ( index = 0; index < bytesToDisplay; index++) + printf("%s0x%02x", index ? ", " : "", *data++); + printf(","); + fileOffset += bytesToDisplay; + printf(" // %5d\n", fileOffset); + } + }; + } while (0); + + // Close the certificate bundle + if (bundleFile > 0) + close(bundleFile); + return status; +} + diff --git a/Firmware/Tools/crc24q.c b/Firmware/Tools/crc24q.c new file mode 100644 index 000000000..c50cb87ba --- /dev/null +++ b/Firmware/Tools/crc24q.c @@ -0,0 +1,185 @@ +/* + * This is an implementation of the CRC-24Q cyclic redundancy checksum + * used by Qualcomm, RTCM104V3, and PGP 6.5.1. According to the RTCM104V3 + * standard, it uses the error polynomial + * + * x^24+ x^23+ x^18+ x^17+ x^14+ x^11+ x^10+ x^7+ x^6+ x^5+ x^4+ x^3+ x+1 + * + * This corresponds to a mask of 0x1864CFB. For a primer on CRC theory, + * including detailed discussion of how and why the error polynomial is + * expressed by this mask, see . + * + * 1) It detects all single bit errors per 24-bit code word. + * 2) It detects all double bit error combinations in a code word. + * 3) It detects any odd number of errors. + * 4) It detects any burst error for which the length of the burst is less than + * or equal to 24 bits. + * 5) It detects most large error bursts with length greater than 24 bits; + * the odds of a false positive are at most 2^-23. + * + * This hash should not be considered cryptographically secure, but it + * is extremely good at detecting noise errors. + * + * Note that this version has a seed of 0 wired in. The RTCM104V3 standard + * requires this. + * + * This file is Copyright 2008 by the GPSD project + * SPDX-License-Identifier: BSD-2-clause + */ + +//This file is originally from: https://gitlab.com/gpsd/gpsd/-/blob/master/gpsd/crc24q.c + +//#include "../include/gpsd_config.h" /* must be before all includes */ + +#include +#include +#include + +//#include "../include/crc24q.h" + +#ifdef REBUILD_CRC_TABLE +/* + * The crc24q code table below can be regenerated with the following code: + */ +#include +#include + +#define CRCSEED 0 /* could be NZ to detect leading zeros */ +#define CRCPOLY 0x1864CFBu /* encodes all info about the polynomial */ + +static void crc_init(unsigned int table[256]) +{ + unsigned i, j; + unsigned h; + + table[0] = CRCSEED; + table[1] = h = CRCPOLY; + + for (i = 2; i < 256; i *= 2) { + if ((h <<= 1) & 0x1000000) + h ^= CRCPOLY; + for (j = 0; j < i; j++) + table[i + j] = table[j] ^ h; + } +} + +int main(int argc, char *argv[]) +{ + int i; + + crc_init(table); + + for (i = 0; i < 256; i++) { + printf("0x%08X, ", table[i]); + if ((i % 4) == 3) + putchar('\n'); + } + + exit(EXIT_SUCCESS); +} +#endif + +static const int unsigned crc24q[256] = { + 0x00000000u, 0x01864CFBu, 0x028AD50Du, 0x030C99F6u, + 0x0493E6E1u, 0x0515AA1Au, 0x061933ECu, 0x079F7F17u, + 0x08A18139u, 0x0927CDC2u, 0x0A2B5434u, 0x0BAD18CFu, + 0x0C3267D8u, 0x0DB42B23u, 0x0EB8B2D5u, 0x0F3EFE2Eu, + 0x10C54E89u, 0x11430272u, 0x124F9B84u, 0x13C9D77Fu, + 0x1456A868u, 0x15D0E493u, 0x16DC7D65u, 0x175A319Eu, + 0x1864CFB0u, 0x19E2834Bu, 0x1AEE1ABDu, 0x1B685646u, + 0x1CF72951u, 0x1D7165AAu, 0x1E7DFC5Cu, 0x1FFBB0A7u, + 0x200CD1E9u, 0x218A9D12u, 0x228604E4u, 0x2300481Fu, + 0x249F3708u, 0x25197BF3u, 0x2615E205u, 0x2793AEFEu, + 0x28AD50D0u, 0x292B1C2Bu, 0x2A2785DDu, 0x2BA1C926u, + 0x2C3EB631u, 0x2DB8FACAu, 0x2EB4633Cu, 0x2F322FC7u, + 0x30C99F60u, 0x314FD39Bu, 0x32434A6Du, 0x33C50696u, + 0x345A7981u, 0x35DC357Au, 0x36D0AC8Cu, 0x3756E077u, + 0x38681E59u, 0x39EE52A2u, 0x3AE2CB54u, 0x3B6487AFu, + 0x3CFBF8B8u, 0x3D7DB443u, 0x3E712DB5u, 0x3FF7614Eu, + 0x4019A3D2u, 0x419FEF29u, 0x429376DFu, 0x43153A24u, + 0x448A4533u, 0x450C09C8u, 0x4600903Eu, 0x4786DCC5u, + 0x48B822EBu, 0x493E6E10u, 0x4A32F7E6u, 0x4BB4BB1Du, + 0x4C2BC40Au, 0x4DAD88F1u, 0x4EA11107u, 0x4F275DFCu, + 0x50DCED5Bu, 0x515AA1A0u, 0x52563856u, 0x53D074ADu, + 0x544F0BBAu, 0x55C94741u, 0x56C5DEB7u, 0x5743924Cu, + 0x587D6C62u, 0x59FB2099u, 0x5AF7B96Fu, 0x5B71F594u, + 0x5CEE8A83u, 0x5D68C678u, 0x5E645F8Eu, 0x5FE21375u, + 0x6015723Bu, 0x61933EC0u, 0x629FA736u, 0x6319EBCDu, + 0x648694DAu, 0x6500D821u, 0x660C41D7u, 0x678A0D2Cu, + 0x68B4F302u, 0x6932BFF9u, 0x6A3E260Fu, 0x6BB86AF4u, + 0x6C2715E3u, 0x6DA15918u, 0x6EADC0EEu, 0x6F2B8C15u, + 0x70D03CB2u, 0x71567049u, 0x725AE9BFu, 0x73DCA544u, + 0x7443DA53u, 0x75C596A8u, 0x76C90F5Eu, 0x774F43A5u, + 0x7871BD8Bu, 0x79F7F170u, 0x7AFB6886u, 0x7B7D247Du, + 0x7CE25B6Au, 0x7D641791u, 0x7E688E67u, 0x7FEEC29Cu, + 0x803347A4u, 0x81B50B5Fu, 0x82B992A9u, 0x833FDE52u, + 0x84A0A145u, 0x8526EDBEu, 0x862A7448u, 0x87AC38B3u, + 0x8892C69Du, 0x89148A66u, 0x8A181390u, 0x8B9E5F6Bu, + 0x8C01207Cu, 0x8D876C87u, 0x8E8BF571u, 0x8F0DB98Au, + 0x90F6092Du, 0x917045D6u, 0x927CDC20u, 0x93FA90DBu, + 0x9465EFCCu, 0x95E3A337u, 0x96EF3AC1u, 0x9769763Au, + 0x98578814u, 0x99D1C4EFu, 0x9ADD5D19u, 0x9B5B11E2u, + 0x9CC46EF5u, 0x9D42220Eu, 0x9E4EBBF8u, 0x9FC8F703u, + 0xA03F964Du, 0xA1B9DAB6u, 0xA2B54340u, 0xA3330FBBu, + 0xA4AC70ACu, 0xA52A3C57u, 0xA626A5A1u, 0xA7A0E95Au, + 0xA89E1774u, 0xA9185B8Fu, 0xAA14C279u, 0xAB928E82u, + 0xAC0DF195u, 0xAD8BBD6Eu, 0xAE872498u, 0xAF016863u, + 0xB0FAD8C4u, 0xB17C943Fu, 0xB2700DC9u, 0xB3F64132u, + 0xB4693E25u, 0xB5EF72DEu, 0xB6E3EB28u, 0xB765A7D3u, + 0xB85B59FDu, 0xB9DD1506u, 0xBAD18CF0u, 0xBB57C00Bu, + 0xBCC8BF1Cu, 0xBD4EF3E7u, 0xBE426A11u, 0xBFC426EAu, + 0xC02AE476u, 0xC1ACA88Du, 0xC2A0317Bu, 0xC3267D80u, + 0xC4B90297u, 0xC53F4E6Cu, 0xC633D79Au, 0xC7B59B61u, + 0xC88B654Fu, 0xC90D29B4u, 0xCA01B042u, 0xCB87FCB9u, + 0xCC1883AEu, 0xCD9ECF55u, 0xCE9256A3u, 0xCF141A58u, + 0xD0EFAAFFu, 0xD169E604u, 0xD2657FF2u, 0xD3E33309u, + 0xD47C4C1Eu, 0xD5FA00E5u, 0xD6F69913u, 0xD770D5E8u, + 0xD84E2BC6u, 0xD9C8673Du, 0xDAC4FECBu, 0xDB42B230u, + 0xDCDDCD27u, 0xDD5B81DCu, 0xDE57182Au, 0xDFD154D1u, + 0xE026359Fu, 0xE1A07964u, 0xE2ACE092u, 0xE32AAC69u, + 0xE4B5D37Eu, 0xE5339F85u, 0xE63F0673u, 0xE7B94A88u, + 0xE887B4A6u, 0xE901F85Du, 0xEA0D61ABu, 0xEB8B2D50u, + 0xEC145247u, 0xED921EBCu, 0xEE9E874Au, 0xEF18CBB1u, + 0xF0E37B16u, 0xF16537EDu, 0xF269AE1Bu, 0xF3EFE2E0u, + 0xF4709DF7u, 0xF5F6D10Cu, 0xF6FA48FAu, 0xF77C0401u, + 0xF842FA2Fu, 0xF9C4B6D4u, 0xFAC82F22u, 0xFB4E63D9u, + 0xFCD11CCEu, 0xFD575035u, 0xFE5BC9C3u, 0xFFDD8538u, +}; + +unsigned crc24q_hash(unsigned char *data, int len) +{ + int i; + unsigned crc = 0; + + for (i = 0; i < len; i++) { + crc = (crc << 8) ^ crc24q[data[i] ^ (unsigned char)(crc >> 16)]; + } + + crc = (crc & 0x00ffffff); + + return crc; +} + +#define LO(x) (unsigned char)((x) & 0xff) +#define MID(x) (unsigned char)(((x) >> 8) & 0xff) +#define HI(x) (unsigned char)(((x) >> 16) & 0xff) + +#ifdef __UNUSED__ +void crc24q_sign(unsigned char *data, int len) +{ + unsigned crc = crc24q_hash(data, len); + + data[len] = HI(crc); + data[len + 1] = MID(crc); + data[len + 2] = LO(crc); +} +#endif /* __UNUSED__ */ + +bool crc24q_check(unsigned char *data, int len) +{ + unsigned crc = crc24q_hash(data, len - 3); + + return (((data[len - 3] == HI(crc)) && + (data[len - 2] == MID(crc)) && (data[len - 1] == LO(crc)))); +} +// vim: set expandtab shiftwidth=4 diff --git a/Firmware/Tools/crc24q.h b/Firmware/Tools/crc24q.h new file mode 100644 index 000000000..66972e602 --- /dev/null +++ b/Firmware/Tools/crc24q.h @@ -0,0 +1,20 @@ +/* Interface for CRC-24Q cyclic redundancy chercksum code + * + * This file is Copyright 2010 by the GPSD project + * SPDX-License-Identifier: BSD-2-clause + */ + +//This file is originally from: https://gitlab.com/gpsd/gpsd/-/blob/master/include/crc24q.h + +#include + +#ifndef _CRC24Q_H_ +#define _CRC24Q_H_ + +extern void crc24q_sign(unsigned char *data, int len); + +extern bool crc24q_check(unsigned char *data, int len); + +extern unsigned crc24q_hash(unsigned char *data, int len); +#endif /* _CRC24Q_H_ */ +// vim: set expandtab shiftwidth=4 diff --git a/Firmware/Tools/index_html_zipper.py b/Firmware/Tools/index_html_zipper.py new file mode 100644 index 000000000..2e3e51093 --- /dev/null +++ b/Firmware/Tools/index_html_zipper.py @@ -0,0 +1,154 @@ +# Opens ..\RTK_Surveyor\AP-Config\index.html, gzip's the contents and pastes into ..\RTK_Surveyor\form.h + +# Written by: Paul Clark +# Last update: April 10th, 2023 + +# To convert AP-Config\index_html to index_html[], run the Python index_html_zipper.py script in the Tools folder: +# cd Firmware\Tools +# python index_html_zipper.py + + +# SparkFun code, firmware, and software is released under the MIT License (http://opensource.org/licenses/MIT) +# +# The MIT License (MIT) +# +# Copyright (c) 2023 SparkFun Electronics +# +# 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. + +import sys +import os +import gzip +import shutil + +defaultsource = '../RTK_Surveyor/AP-Config/index.html' +defaultdest = '../RTK_Surveyor/form.h' +headersearch = 'static const uint8_t index_html[] PROGMEM = {' +footersearch = '}; ///index_html' + +print() +print('SparkFun RTK: gzip index.html into form.h') +print() + +sourcefilename = '' + +if sourcefilename == '': + # Check if the bin filename was passed in argv + if len(sys.argv) > 1: sourcefilename = sys.argv[1] + +# Ask user for filename offering firstfile as the default +if sourcefilename == '': + sourcefilename = input('Enter the source filename (default: ' + defaultsource + '): ') # Get the filename + print() +if sourcefilename == '': sourcefilename = defaultsource + +destfilename = '' + +if destfilename == '': + # Check if the bin filename was passed in argv + if len(sys.argv) > 2: destfilename = sys.argv[2] + +# Ask user for filename offering firstfile as the default +if destfilename == '': + destfilename = input('Enter the destination filename (default: ' + defaultdest + '): ') # Get the filename + print() +if destfilename == '': destfilename = defaultdest + +zippedfilename = sourcefilename + '.gzip' + +print('Step 1: gzip',sourcefilename,'into',zippedfilename) +print() + +with open(sourcefilename, 'rb') as f_in: + with gzip.open(zippedfilename, 'wb') as f_out: + shutil.copyfileobj(f_in, f_out) + +headerfilename = destfilename + '.header' + +print('Step 2: create',headerfilename,'from',destfilename) +print() + +with open(destfilename, 'rb') as f_in: + with open(headerfilename, 'wb') as f_out: + content = f_in.read() + + try: + pos = content.index(bytes(headersearch, 'utf-8')) + except: + raise Exception('Invalid destination file - could not find start of index_html!') + + pos += len(headersearch) + + f_out.write(content[:pos]) + +footerfilename = destfilename + '.footer' + +print('Step 3: create',footerfilename,'from',destfilename) +print() + +with open(destfilename, 'rb') as f_in: + with open(footerfilename, 'wb') as f_out: + content = f_in.read() + + try: + pos = content.index(bytes(footersearch, 'utf-8')) + except: + raise Exception('Invalid destination file - could not find end of index_html!') + + f_out.write(content[pos:]) + +print('Step 4: create',destfilename,'from',headerfilename,'+',zippedfilename,'+',footerfilename) +print() + +with open(destfilename, 'wb') as f_out: + with open(headerfilename, 'rb') as f_in_1: + with open(zippedfilename, 'rb') as f_in_2: + with open(footerfilename, 'rb') as f_in_3: + f_out.write(f_in_1.read()) + + f_out.write(bytes('\r\n', 'utf-8')) + + content = f_in_2.read() + count = 0 + + for c in content[:-2]: + if count == 0: + f_out.write(bytes(' ', 'utf-8')) + f_out.write(bytes("0x{:02X},".format(c), 'utf-8')) + count += 1 + if count == 16: + count = 0 + f_out.write(bytes('\r\n', 'utf-8')) + else: + f_out.write(bytes(' ', 'utf-8')) + + if count == 0: + f_out.write(bytes(' ', 'utf-8')) + f_out.write(bytes("0x{:02X}\r\n".format(content[-1]), 'utf-8')) + + f_out.write(f_in_3.read()) + +print('Step 5: delete',headerfilename,'+',zippedfilename,'+',footerfilename) +print() + +os.remove(headerfilename) +os.remove(zippedfilename) +os.remove(footerfilename) + +print('Done!') diff --git a/Firmware/Tools/main_js_zipper.py b/Firmware/Tools/main_js_zipper.py new file mode 100644 index 000000000..56c27dcff --- /dev/null +++ b/Firmware/Tools/main_js_zipper.py @@ -0,0 +1,154 @@ +# Opens ..\RTK_Surveyor\AP-Config\src\main.js, gzip's the contents and pastes into ..\RTK_Surveyor\form.h + +# Written by: Paul Clark +# Last update: April 10th, 2023 + +# To convert AP-Config\src\main.js to main_js[], run the Python main_js_zipper.py script in the Tools folder: +# cd Firmware\Tools +# python main_js_zipper.py + + +# SparkFun code, firmware, and software is released under the MIT License (http://opensource.org/licenses/MIT) +# +# The MIT License (MIT) +# +# Copyright (c) 2023 SparkFun Electronics +# +# 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. + +import sys +import os +import gzip +import shutil + +defaultsource = '../RTK_Surveyor/AP-Config/src/main.js' +defaultdest = '../RTK_Surveyor/form.h' +headersearch = 'static const uint8_t main_js[] PROGMEM = {' +footersearch = '}; ///main_js' + +print() +print('SparkFun RTK: gzip main.js into form.h') +print() + +sourcefilename = '' + +if sourcefilename == '': + # Check if the bin filename was passed in argv + if len(sys.argv) > 1: sourcefilename = sys.argv[1] + +# Ask user for filename offering firstfile as the default +if sourcefilename == '': + sourcefilename = input('Enter the source filename (default: ' + defaultsource + '): ') # Get the filename + print() +if sourcefilename == '': sourcefilename = defaultsource + +destfilename = '' + +if destfilename == '': + # Check if the bin filename was passed in argv + if len(sys.argv) > 2: destfilename = sys.argv[2] + +# Ask user for filename offering firstfile as the default +if destfilename == '': + destfilename = input('Enter the destination filename (default: ' + defaultdest + '): ') # Get the filename + print() +if destfilename == '': destfilename = defaultdest + +zippedfilename = sourcefilename + '.gzip' + +print('Step 1: gzip',sourcefilename,'into',zippedfilename) +print() + +with open(sourcefilename, 'rb') as f_in: + with gzip.open(zippedfilename, 'wb') as f_out: + shutil.copyfileobj(f_in, f_out) + +headerfilename = destfilename + '.header' + +print('Step 2: create',headerfilename,'from',destfilename) +print() + +with open(destfilename, 'rb') as f_in: + with open(headerfilename, 'wb') as f_out: + content = f_in.read() + + try: + pos = content.index(bytes(headersearch, 'utf-8')) + except: + raise Exception('Invalid destination file - could not find start of main_js!') + + pos += len(headersearch) + + f_out.write(content[:pos]) + +footerfilename = destfilename + '.footer' + +print('Step 3: create',footerfilename,'from',destfilename) +print() + +with open(destfilename, 'rb') as f_in: + with open(footerfilename, 'wb') as f_out: + content = f_in.read() + + try: + pos = content.index(bytes(footersearch, 'utf-8')) + except: + raise Exception('Invalid destination file - could not find end of main_js!') + + f_out.write(content[pos:]) + +print('Step 4: create',destfilename,'from',headerfilename,'+',zippedfilename,'+',footerfilename) +print() + +with open(destfilename, 'wb') as f_out: + with open(headerfilename, 'rb') as f_in_1: + with open(zippedfilename, 'rb') as f_in_2: + with open(footerfilename, 'rb') as f_in_3: + f_out.write(f_in_1.read()) + + f_out.write(bytes('\r\n', 'utf-8')) + + content = f_in_2.read() + count = 0 + + for c in content[:-2]: + if count == 0: + f_out.write(bytes(' ', 'utf-8')) + f_out.write(bytes("0x{:02X},".format(c), 'utf-8')) + count += 1 + if count == 16: + count = 0 + f_out.write(bytes('\r\n', 'utf-8')) + else: + f_out.write(bytes(' ', 'utf-8')) + + if count == 0: + f_out.write(bytes(' ', 'utf-8')) + f_out.write(bytes("0x{:02X}\r\n".format(content[-1]), 'utf-8')) + + f_out.write(f_in_3.read()) + +print('Step 5: delete',headerfilename,'+',zippedfilename,'+',footerfilename) +print() + +os.remove(headerfilename) +os.remove(zippedfilename) +os.remove(footerfilename) + +print('Done!') diff --git a/Firmware/Tools/makefile b/Firmware/Tools/makefile new file mode 100644 index 000000000..292ba343a --- /dev/null +++ b/Firmware/Tools/makefile @@ -0,0 +1,224 @@ +###################################################################### +# makefile +# +# Builds the RTK support programs +###################################################################### + +.ONESHELL: +SHELL=/bin/bash + +########## +# Source files +########## + +EXECUTABLES = Compare +EXECUTABLES += NMEA_Client +EXECUTABLES += Read_Map_File +EXECUTABLES += RTK_Reset +EXECUTABLES += Split_Messages +EXECUTABLES += X.509_crt_bundle_bin_to_c + +INCLUDES = crc24q.h + +########## +# Buid tools and rules +########## + +GCC = gcc +CFLAGS = -flto -O3 -Wpedantic -pedantic-errors -Wall -Wextra -Werror -Wno-unused-variable -Wno-unused-parameter +CC = $(GCC) $(CFLAGS) + +%.o: %.c $(INCLUDES) + $(CC) -c -o $@ $< + +%: %.c $(INCLUDES) + $(CC) $(CFLAGS) -o $@ $< + +########## +# Buid all the sources - must be first +########## + +.PHONY: all + +all: $(EXECUTABLES) + +########## +# Buid RTK firmware +########## + +DEBUG_LEVEL=debug +ENABLE_DEVELOPER=false +FIRMWARE_VERSION_MAJOR=99 +FIRMWARE_VERSION_MINOR=99 +POINTPERFECT_TOKEN= + +libraries: + echo ---------------------------------- + cd ~/Arduino/libraries + + echo ArduinoJson + cd ArduinoJson + git checkout --quiet 7.x + git pull --quiet origin 7.x + git checkout --quiet 6.x + git pull --quiet origin 6.x + git checkout --quiet v6.19.4 + + echo ArduinoMqttClient + cd ../ArduinoMqttClient + git checkout --quiet master + git pull --quiet origin master + git checkout --quiet 0.1.7 + + echo ArduinoWebsockets + cd ../ArduinoWebsockets + git checkout --quiet master + git pull --quiet origin master + git checkout --quiet 0.5.3 + + echo AsyncTCP + cd ../AsyncTCP + git checkout --quiet master + git pull --quiet origin master + + # Crypto@0.4.0 + + echo ESP32_BleSerial + cd ../ESP32_BleSerial + git checkout --quiet master + git pull --quiet origin master + git checkout --quiet v1.0.4 + + echo ESP32-OTA-Pull + cd ../ESP32-OTA-Pull + git checkout --quiet main + git pull --quiet origin main + git checkout --quiet v1.0.0-beta.1 + + echo ESP32Time + cd ../ESP32Time + git checkout --quiet main + git pull --quiet origin main + git checkout --quiet v2.0.0 + + echo ESPAsyncWebServer + cd ../ESPAsyncWebServer + git checkout --quiet master + git pull --quiet origin master + + echo Ethernet + cd ../Ethernet + git checkout --quiet master + git pull --quiet origin master + git checkout --quiet 2.0.2 + + echo JC_Button + cd ../JC_Button + git checkout --quiet master + git pull --quiet origin master + git checkout --quiet 2.1.2 + + echo NimBLE-Arduino + cd ../NimBLE-Arduino + git checkout --quiet release/1.4 + git pull --quiet origin release/1.4 + git checkout --quiet 1.4.1 + + echo PubSubClient + cd ../PubSubClient + git checkout --quiet master + git pull --quiet origin master + git checkout --quiet v2.8 + + echo RadioLib + cd ../RadioLib + git checkout --quiet master + git pull --quiet origin master + git checkout --quiet 5.6.0 + + echo SdFat + cd ../SdFat + git checkout --quiet master + git pull --quiet origin master + git checkout --quiet 2.1.1 + + echo SparkFun_LIS2DH12_Arduino_Library + cd ../SparkFun_LIS2DH12_Arduino_Library + git checkout --quiet master + git pull --quiet origin master + git checkout --quiet v1.0.3 + + echo SparkFun_MAX1704x_Fuel_Gauge_Arduino_Library + cd ../SparkFun_MAX1704x_Fuel_Gauge_Arduino_Library + git checkout --quiet main + git pull --quiet origin main + git checkout --quiet v1.0.4 + + echo SparkFun_Qwiic_OLED_Arduino_Library + cd ../SparkFun_Qwiic_OLED_Arduino_Library + git checkout --quiet main + git pull --quiet origin main + git checkout --quiet v1.0.9 + + echo SparkFun_u-blox_GNSS_Arduino_Library + cd ../SparkFun_u-blox_GNSS_Arduino_Library + git checkout --quiet main + git pull --quiet origin main + git checkout --quiet v2.2.24 + + echo SparkFun_u-blox_GNSS_v3 + cd ../SparkFun_u-blox_GNSS_v3 + git checkout --quiet main + git pull --quiet origin main + git checkout --quiet v3.0.14 + + echo SparkFun_u-blox_SARA-R5_Arduino_Library + cd ../SparkFun_u-blox_SARA-R5_Arduino_Library + git checkout --quiet main + git pull --quiet origin main + git checkout --quiet v1.1.6 + + echo SparkFun_WebServer_ESP32_W5500 + cd ../SparkFun_WebServer_ESP32_W5500 + git checkout --quiet main + git pull --quiet origin main + git checkout --quiet v1.5.5 + + echo SSLClientESP32 + cd ../SSLClientESP32 + git checkout --quiet master + git pull --quiet origin master + git checkout --quiet v2.0.0 + + echo WiFiManager + cd ../WiFiManager + git checkout --quiet master + git pull --quiet origin master + git checkout --quiet v2.0.16-rc.2 + + echo ---------------------------------- + ~/Arduino/arduino-cli lib list + +partition: ../app3M_fat9M_16MB.csv + ESP_PATH=/home/$$USER/.arduino15/packages/esp32/hardware/esp32/* + for file in $$ESP_PATH; do \ + ESP_VERSION_PATH=$${file}; \ + done + ESP_VERSION=$$(basename $$ESP_VERSION_PATH) + cp --verbose $< ~/.arduino15/packages/esp32/hardware/esp32/$$ESP_VERSION/tools/partitions/app3M_fat9M_16MB.csv + +../RTK_Surveyor/form.h: makefile ../RTK_Surveyor/* ../RTK_Surveyor/AP-Config/* ../RTK_Surveyor/AP-Config/src/* ../RTK_Surveyor/AP-Config/src/fonts/* + python index_html_zipper.py ../RTK_Surveyor/AP-Config/index.html ../RTK_Surveyor/form.h + python main_js_zipper.py ../RTK_Surveyor/AP-Config/src/main.js ../RTK_Surveyor/form.h + +RTK: ../RTK_Surveyor/RTK_Surveyor.ino ../RTK_Surveyor/*.h + ~/Arduino/arduino-cli compile --fqbn "esp32:esp32:esp32":DebugLevel=$(DEBUG_LEVEL) ../RTK_Surveyor/RTK_Surveyor.ino --build-property build.partitions=app3M_fat9M_16MB --build-property upload.maximum_size=3145728 --build-property "compiler.cpp.extra_flags=\"-DPOINTPERFECT_TOKEN=$(POINTPERFECT_TOKEN)\" \"-DFIRMWARE_VERSION_MAJOR=$(FIRMWARE_VERSION_MAJOR)\" \"-DFIRMWARE_VERSION_MINOR=$(FIRMWARE_VERSION_MINOR)\" \"-DENABLE_DEVELOPER=$(ENABLE_DEVELOPER)\"" --export-binaries + +######## +# Clean the build directory +########## + +.PHONY: clean + +clean: + rm -f *.o *.a $(EXECUTABLES) diff --git a/Firmware/Tools/png_zipper.py b/Firmware/Tools/png_zipper.py new file mode 100644 index 000000000..12ecf9c85 --- /dev/null +++ b/Firmware/Tools/png_zipper.py @@ -0,0 +1,92 @@ +# Opens the selected .png, gzip's the contents, converts to hex and pastes into a new file + +# Written by: Paul Clark +# Last update: April 10th, 2023 + +# To convert rtk-setup.png into rtk-setup.png.gzip_hex, run the Python png_zipper.py script in the Tools folder: +# cd Firmware\Tools +# python png_zipper.py ..\RTK_Surveyor\AP-Config\src\rtk-setup.png + + +# SparkFun code, firmware, and software is released under the MIT License (http://opensource.org/licenses/MIT) +# +# The MIT License (MIT) +# +# Copyright (c) 2023 SparkFun Electronics +# +# 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. + +import sys +import os +import gzip +import shutil + +print() +print('SparkFun RTK: convert png to gzip_hex') +print() + +sourcefilename = '' + +if sourcefilename == '': + # Check if the bin filename was passed in argv + if len(sys.argv) > 1: sourcefilename = sys.argv[1] + +# Ask user for filename offering firstfile as the default +if sourcefilename == '': + sourcefilename = input('Enter the source filename: ') # Get the filename + print() + +zippedfilename = sourcefilename + '.gzip' +destfilename = sourcefilename + '.gzip_hex' + +print('Step 1: convert',sourcefilename,'into',zippedfilename) +print() + +with open(sourcefilename, 'rb') as f_in: + with gzip.open(zippedfilename, 'wb') as f_out: + shutil.copyfileobj(f_in, f_out) + +print('Step 2: create',destfilename,'from',zippedfilename) +print() + +with open(destfilename, 'wb') as f_out: + with open(zippedfilename, 'rb') as f_in: + content = f_in.read() + count = 0 + for c in content[:-2]: + if count == 0: + f_out.write(bytes(' ', 'utf-8')) + f_out.write(bytes("0x{:02X},".format(c), 'utf-8')) + count += 1 + if count == 16: + count = 0 + f_out.write(bytes('\r\n', 'utf-8')) + else: + f_out.write(bytes(' ', 'utf-8')) + + if count == 0: + f_out.write(bytes(' ', 'utf-8')) + f_out.write(bytes("0x{:02X}\r\n".format(content[-1]), 'utf-8')) + +print('Step 3: delete',zippedfilename) +print() + +os.remove(zippedfilename) + +print('Done!') diff --git a/Firmware/app3M_fat9M_16MB - Future.csv b/Firmware/app3M_fat9M_16MB - Future.csv new file mode 100644 index 000000000..67d773728 --- /dev/null +++ b/Firmware/app3M_fat9M_16MB - Future.csv @@ -0,0 +1,7 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xe000, 0x2000, +app0, app, ota_0, 0x10000, 0x640000, +app1, app, ota_1, 0x650000,0x640000, +spiffs, data, spiffs, 0xc90000,0x360000, +coredump, data, coredump,0xFF0000,0x10000, diff --git a/Firmware/app3M_fat9M_16MB.csv b/Firmware/app3M_fat9M_16MB.csv new file mode 100644 index 000000000..7b89daee9 --- /dev/null +++ b/Firmware/app3M_fat9M_16MB.csv @@ -0,0 +1,6 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xe000, 0x2000, +app0, app, ota_0, 0x10000, 0x640000, +app1, app, ota_1, 0x650000,0x640000, +spiffs, data, spiffs, 0xc90000,0x370000, diff --git a/Firmware/readme.md b/Firmware/readme.md index 9172e35a2..61ecd2bb5 100644 --- a/Firmware/readme.md +++ b/Firmware/readme.md @@ -1,24 +1,10 @@ -### Firmware +# Firmware -There are two firmwares that operate on the device: - -* Firmware on the ZED-F9P Receiver -* Firmware on the ESP32 microcontroller - -This folder contains the firmware for the ESP32 +This folder contains the Arduino sketches that make up the firmware that runs on the ESP32. Go [here](https://docs.sparkfun.com/SparkFun_RTK_Firmware/firmware_update/#updating-u-blox-firmware) for more information about the firmware that runs the ZED-F9x Receiver. * **RTK_Surveyor** - The main firmware for the RTK Surveyor * **Test Sketches** - Various sketches used in the making of the main firmware. Used internally to verify different features. Reader beware. ----- -### A note about ZED-F9P firmware - -The firmware loaded onto the ZED-F9P receiver is currently one of two versions: v1.12 or v1.13. All field testing and device specific performance parameters were obtained with v1.12. - -v1.12 has the benefit of working with SBAS and an operational RTK status signal (the LED illuminates correctly). - -v1.13 has a few RTK and receiver performance improvements but introduces a bug that causes the RTK Status LED to fail when SBAS is enabled. - -A tutorial with step-by-step instructions for locating the firmware version as well as changing the firmware can be found [here](https://learn.sparkfun.com/tutorials/how-to-upgrade-firmware-of-a-u-blox-gnss-receiver). +## Compilation Instructions -More information about the differences can be found [here](https://www.u-blox.com/sites/default/files/ZED-F9P-FW100-HPG113_RN_%28UBX-20019211%29.pdf). \ No newline at end of file +See [Compiling Source](https://docs.sparkfun.com/SparkFun_RTK_Firmware/firmware_update/#compiling-source) for detailed steps. diff --git a/Graphics/Base-Fixed.bmp b/Graphics/Base-Fixed.bmp new file mode 100644 index 000000000..f0b002d66 Binary files /dev/null and b/Graphics/Base-Fixed.bmp differ diff --git a/Graphics/Base-Temporary.bmp b/Graphics/Base-Temporary.bmp new file mode 100644 index 000000000..692c68e22 Binary files /dev/null and b/Graphics/Base-Temporary.bmp differ diff --git a/Graphics/Battery-0.bmp b/Graphics/Battery-0.bmp new file mode 100644 index 000000000..363fbd919 Binary files /dev/null and b/Graphics/Battery-0.bmp differ diff --git a/Graphics/Battery-1.bmp b/Graphics/Battery-1.bmp new file mode 100644 index 000000000..9892c4737 Binary files /dev/null and b/Graphics/Battery-1.bmp differ diff --git a/Graphics/Battery-2.bmp b/Graphics/Battery-2.bmp new file mode 100644 index 000000000..418fe939e Binary files /dev/null and b/Graphics/Battery-2.bmp differ diff --git a/Graphics/Battery-3.bmp b/Graphics/Battery-3.bmp new file mode 100644 index 000000000..57f098449 Binary files /dev/null and b/Graphics/Battery-3.bmp differ diff --git a/Graphics/Bluetooth Symbol.bmp b/Graphics/Bluetooth Symbol.bmp new file mode 100644 index 000000000..5f390ba15 Binary files /dev/null and b/Graphics/Bluetooth Symbol.bmp differ diff --git a/Graphics/C/Fonts.c b/Graphics/C/Fonts.c new file mode 100644 index 000000000..eb85dc961 --- /dev/null +++ b/Graphics/C/Fonts.c @@ -0,0 +1,349 @@ +#include +#include +#include +#include +#include +#include + +#define DRAW_OUTLINE 1 + +typedef struct _CHARACTER_ENTRY { + struct _CHARACTER_ENTRY * next; + int character; + uint8_t data[]; +} CHARACTER_ENTRY; + +int bytes_high; +CHARACTER_ENTRY * character_list; +int data_bytes; +char * font_text; +uint8_t * font_data; +int height; +int map_width; +int nchar; +int start; +int width; + +int +read_value ( + const char * string, + const char * text_end + ) +{ + const char * text; + int length; + int value; + + // Find the map width + text = font_text; + length = strlen(string); + while (text < text_end) { + if (strncmp (text, string, length) == 0) { + text += length; + break; + } + text++; + } + + // Skip over the white space + while (text < text_end) { + if ((*text != ' ') && (*text != '\t')) + break; + text++; + } + + // Get the map width + if (sscanf (text, "%d", &value) != 1) { + fprintf (stderr, "ERROR - Invalid map width!\n"); + return -1; + } + return value; +} + +const char * +find_next_character ( + const char * text, + const char * text_end + ) +{ + // Walk through the font file + while (text < (text_end - 2)) { + // Determine if this is an icon name + if ((text[0] == '0') && ((text[1] == 'x') || (text[1] == 'X'))) + return text; + text++; + } + + // No more data + return NULL; +} + +int +read_data ( + const char * text_end + ) +{ + int character_count; + const char * text; + int data_offset; + int index; + unsigned int value; + + // Read the data array + character_count = 0; + text = font_text; + while (text < text_end) { + for (index = 0; index < data_bytes; index++) { + + // Find the next data value + text = find_next_character (text, text_end); + if ((text == NULL) && (index == 0)) + return character_count; + else if (text == NULL) { + fprintf (stderr, "ERROR - Missing data!\n"); + return 0; + } + + // Get the value + if (sscanf (text, "0x%02x", &value) != 1) { + fprintf (stderr, "ERROR - Invalid data value!\n"); + return 0; + } + text += 4; + + // Save the data value + data_offset = (data_bytes * character_count) + index; + font_data[data_offset] = value; + } + character_count++; + } + return 0; +} + +void +add_entry ( + CHARACTER_ENTRY * new_entry + ) +{ + CHARACTER_ENTRY * previous_entry; + + // Walk to the end of the list + previous_entry = character_list; + while (previous_entry && previous_entry->next) + previous_entry = previous_entry->next; + + // Add this entry to the list + new_entry->next = NULL; + if (previous_entry) + previous_entry->next = new_entry; + else + character_list = new_entry; +} + +int +process_data ( + int character_count + ) +{ + int font_index; + int index; + int map_characters; + int map_offset; + int next_character; + CHARACTER_ENTRY * new_entry; + ssize_t offset; + unsigned int value; + int x; + int y; + + next_character = start; + map_characters = map_width / width; + while ((next_character - start) < character_count) { + // Allocate the data structure + new_entry = malloc (sizeof(*new_entry) + data_bytes); + + if (!new_entry) { + fprintf (stderr, "ERROR - Failed to allocate next_entry!\n"); + return -1; + } + new_entry->character = next_character++; + map_offset = new_entry->character - start; + map_offset = ((map_offset / map_characters) * map_characters * data_bytes) + + ((map_offset % map_characters) * width); + for (y = 0; y < bytes_high; y++) { + for (x = 0; x < width; x++) { + + // Save the data value + font_index = map_offset + (y * map_width) + x; + value = font_data[font_index]; + index = (y * width) + x; + new_entry->data[index] = value; + } + } + + // Add the entry to the list + add_entry (new_entry); + } + return 0; +} + +void +display_character ( + CHARACTER_ENTRY * character + ) +{ + int bit; + const char * indent = " "; + int x; + int y; + + printf ("/*\n"); + printf (" 0x%02x, %d [%d, %d]\n", character->character, character->character, width, height); + printf ("\n"); +#ifdef DRAW_OUTLINE + printf ("%s.", indent); + for (x = 0; x < width; x++) + printf ("-"); + printf (".\n"); +#endif // DRAW_OUTLINE + for (y = 0; y < height; y++) { + printf ("%s", indent); +#ifdef DRAW_OUTLINE + printf ("|"); +#endif // DRAW_OUTLINE + for (x = 0; x < width; x++) { + bit = character->data[((y >> 3) * width) + x]; + bit >>= y & 7; + printf ("%c", (bit & 1) ? '*' : ' '); + } +#ifdef DRAW_OUTLINE + printf ("|"); +#endif // DRAW_OUTLINE + printf ("\n"); + } +#ifdef DRAW_OUTLINE + printf ("%s'", indent); + for (x = 0; x < width; x++) + printf ("-"); + printf ("'\n"); +#endif // DRAW_OUTLINE + printf ("*/\n"); + printf ("\n"); +} + +int +main ( + int argc, + char ** argv + ) +{ + CHARACTER_ENTRY * character; + int character_count; + const char * text_end; + char * filename; + int font_file; + off_t file_length; + int status; + ssize_t valid_data; + + do { + // Assume failure + font_file = -1; + status = -1; + + // Verify the argument count + if (argc != 2) { + fprintf (stderr, "%s font_fliename\n", argv[0]); + break; + } + + // Get the font file name + filename = argv[1]; + + // Open the font file + font_file = open (filename, O_RDONLY); + if (font_file < 0) { + perror("ERROR - File open failed!"); + break; + } + + // Determine the length of the file + file_length = lseek (font_file, 0, SEEK_END); + + // Go the the beginning of the file + lseek (font_file, 0, SEEK_SET); + + // Allocate the text buffer + font_text = malloc (file_length); + if (!font_text) { + fprintf (stderr, "ERROR - Failed to allocate font text buffer!\n"); + break; + } + + // Fill the text buffer + valid_data = read (font_file, font_text, file_length); + if (valid_data < 0) { + fprintf (stderr, "ERROR - File read failed!\n"); + break; + } + text_end = &font_text[valid_data]; + + // Find the font values + width = read_value ("WIDTH", text_end); + if (width < 0) { + fprintf (stderr, "ERROR - Failed to read font width!\n"); + } + height = read_value ("HEIGHT", text_end); + if (height < 0) { + fprintf (stderr, "ERROR - Failed to read font height!\n"); + break; + } + start = read_value ("START", text_end); + if (start < 0) { + fprintf (stderr, "ERROR - Failed to read font start!\n"); + break; + } + nchar = read_value ("NCHAR", text_end); + if (nchar < 0) { + fprintf (stderr, "ERROR - Failed to read font nchar!\n"); + break; + } + map_width = read_value ("MAP_WIDTH", text_end); + if (map_width < 0) { + fprintf (stderr, "ERROR - Failed to read map width!\n"); + break; + } + bytes_high = (height + 7) / 8; + data_bytes = width * bytes_high; + + // Allocate the data buffer + font_data = malloc (sizeof(*font_data) * 256 * data_bytes); + if (!font_data) { + fprintf (stderr, "ERROR - Failed to allocate font data buffer!\n"); + break; + } + + // Fill the data buffer + character_count = read_data (text_end); + + // Process the data + if (process_data(character_count)) + break; + + // Display the characters in the font file + character = character_list; + while (character) { + display_character (character); + character = character->next; + } + + // Indicate success + status = 0; + } while (0); + + // Close the file + if (font_file >= 0) + close(font_file); + + return status; +} diff --git a/Graphics/C/Icons.c b/Graphics/C/Icons.c new file mode 100644 index 000000000..b34ca7dcc --- /dev/null +++ b/Graphics/C/Icons.c @@ -0,0 +1,547 @@ +#include +#include +#include +#include +#include +#include + +#define ALPHABETICAL_ORDER 0 +#define DISPLAY_COMMENT 1 +#define DISPLAY_VARIABLES 1 +#define DRAW_OUTLINE 1 +#define PRINT_FILE_LINES 0 +#define PRINT_HEIGHT 0 +#define PRINT_SYMBOL 0 +#define PRINT_VALUES 0 +#define PRINT_WIDTH 0 +#define WRAP_AT_16 0 +#define USE_UPPERCASE_A 1 +#define ADD_TRAILING_COMMA 0 + +typedef struct _ICON_ENTRY { + struct _ICON_ENTRY * next; + const char * name; + int height; + const char * height_name; + int width; + const char * width_name; + int bytes_high; + int bytes_wide; + uint8_t data[]; +} ICON_ENTRY; + +int height; +char * icon_text; +ICON_ENTRY * icon_list; +int width; + +// Replace CR and LF with zeros +char * +terminate_end_of_line ( + char * text, + char * text_end + ) +{ + // Locate the end of line + while ((text < text_end) && (*text != '\r') && (*text != '\n')) + text++; + + // Skip over the end of line + while ((text < text_end) && ((*text == '\r') || (*text == '\n'))) + *text++ = 0; + + // Return the start of the next line + return text; +} + +// Skip over white space +char * +skip_white_space ( + char * text, + char * text_end + ) +{ + // Locate the first non-white space character + while ((text < text_end) && ((*text == ' ') || (*text == '\t'))) + text++; + + // Return the address of the first non-white space character + return text; +} + +// Find the icon name +char * +find_icon_data ( + char * text, + char * text_end + ) +{ + // Locate the end of the icon symbol name + while ((text < text_end) && (*text != ' ') && (*text != '\t') && (*text != '[')) + text++; + + // Zero terminate the symbol name + if (text < text_end) + *text++ = 0; + + // Locate the beginning of the icon data + while ((text < text_end) && (*text != '{')) + text++; + + // Skip over the left parenthesis + if (text < text_end) + text++; + + // Return the beginning of the data + return skip_white_space (text, text_end); +} + +void +add_entry ( + ICON_ENTRY * new_entry + ) +{ + ICON_ENTRY * next_entry; + + if (ALPHABETICAL_ORDER) { + // Add to the head of the list + if ((icon_list == NULL) || (strcasecmp(icon_list->name, new_entry->name) > 0)) { + new_entry->next = icon_list; + icon_list = new_entry; + } else { + + // Locate the point in the list to insert the new entry + next_entry = icon_list; + while (next_entry->next && (strcasecmp(next_entry->next->name, new_entry->name) < 0)) + next_entry = next_entry->next; + + new_entry->next = next_entry->next; + next_entry->next = new_entry; + } + } else { + // Add to the head of the list + if (icon_list == NULL) { + new_entry->next = icon_list; + icon_list = new_entry; + } else { + + // Locate the point in the list to insert the new entry + next_entry = icon_list; + while (next_entry->next) + next_entry = next_entry->next; + + new_entry->next = next_entry->next; + next_entry->next = new_entry; + } + } +} + +char * +read_icon_data ( + char * name, + char * height_name, + char * width_name, + char * text, + char * text_end + ) +{ + int bytes_high; + int bytes_wide; + int data_bytes; + ICON_ENTRY * icon_entry; + int index; + char * number; + unsigned int value; + int x; + int y; + + // Allocate the data structure + bytes_high = (height + 7) / 8; + bytes_wide = width; + data_bytes = width * bytes_high; + icon_entry = malloc (sizeof(*icon_entry) + data_bytes); + if (icon_entry) { + icon_entry->name = name; + icon_entry->height = height; + icon_entry->height_name = height_name; + icon_entry->width = width; + icon_entry->width_name = width_name; + icon_entry->bytes_high = bytes_high; + icon_entry->bytes_wide = bytes_wide; + add_entry (icon_entry); + + // Read the data bytes + for (y = 0; y < bytes_high; y++) { + for (x = 0; x < bytes_wide; x++) { + + // Skip any white space + while ((text < text_end) && (*text != '0')) { + // Treat comments as white space + if ((*text != '/') || ((text[1] != '/') && (text[1] != '*'))) + text++; + else { + // Skip over C++ style comment // + if (text[1] == '/') + while ((*text != '\r') && (*text != '\n')) + text++; + // Skip over the C style comment /* .. */ + else { + text += 2; + while ((*text != '*') && (text[1] != '/')) + text++; + } + } + } + + // Save the number location + number = text; + + // Zero terminate the number + while (((*text >= '0') && (*text <= '9')) + || ((*text >= 'a') && (*text <= 'f')) + || ((*text >= 'A') && (*text <= 'F')) + || (*text == 'x') || (*text == 'X')) + text++; + *text++ = 0; + + // Get the value + sscanf (number, "0x%x", &value); + + // Display the value if requested + if (PRINT_VALUES) + printf("0x%02x\r\n", value); + + // Place the value in the array + index = (y * bytes_wide) + x; + icon_entry->data[index] = value; + icon_entry->data[(y * bytes_wide) + x] = value; + } + } + } + + // Skip over the data + return text; +} + +const char * +display_name ( + ICON_ENTRY * icon + ) +{ + return icon ? icon->name : "NULL"; +} + +int +process_data ( + int icon_file, + ssize_t valid_data + ) +{ + char * data; + char * data_end; + char * height_name; + const char * height_string = "_Height = "; + int height_string_length; + const char * int_string = "const int "; + int int_string_length; + char * name; + char * next_line; + int status; + int temp; + char * text; + const char * uint8_t_string = "const uint8_t "; + int uint8_t_string_length; + char * width_name; + const char * width_string = "_Width = "; + int width_string_length; + + data = icon_text; + data_end = &data[valid_data]; + status = -1; + height_string_length = strlen (height_string); + int_string_length = strlen (int_string); + uint8_t_string_length = strlen (uint8_t_string); + width_string_length = strlen (width_string); + width_name = NULL; + height_name = NULL; + while (data < data_end) { + text = data; + next_line = terminate_end_of_line (text, data_end); + + // Display the line if requested + if (PRINT_FILE_LINES) + printf ("%s\r\n", text); + + // Locate the text at the beginning of the line + text = skip_white_space (text, data_end); + + // Determine if this is a width or height line + if (strncmp (text, int_string, int_string_length) == 0) + { + text = skip_white_space (&text[int_string_length], data_end); + name = text; + while (1) { + // Locate the underscore + while (*text && (*text != '_')) + text++; + + // Not a width or height line when end of line is reached + if (!*text) + break; + + // Determine if this is a height line + if (strncmp (text, height_string, height_string_length) == 0) + { + // Zero terminate the name + name[&text[height_string_length] - 3 - name] = 0; + height_name = name; + + // Skip over any additional white space + text = skip_white_space (&text[height_string_length], data_end); + + // Get the height + if (sscanf (text, "%d", &temp) == 1) + { + height = temp; + if (PRINT_HEIGHT) + printf ("Height: %d\r\n", height); + } + break; + } + + // Determine if this is a width line + else if (strncmp (text, width_string, width_string_length) == 0) + { + // Zero terminate the name + name[&text[width_string_length] - 3 - name] = 0; + width_name = name; + + // Skip over any additional white space + text = skip_white_space (&text[width_string_length], data_end); + + // Get the width + if (sscanf (text, "%d", &temp) == 1) + { + width = temp; + if (PRINT_WIDTH) + printf ("Width: %d\r\n", width); + } + break; + } + + // Continue the underscore search + text++; + } + } + + // Determine if this is an icon line + else if (strncmp (text, uint8_t_string, uint8_t_string_length) == 0) + { + // Skip over any additional white space + text = skip_white_space (&text[uint8_t_string_length], data_end); + name = text; + + // Locate the beginning of the data + text = find_icon_data (text, data_end); + + // Print the symbol name if requested + if (PRINT_SYMBOL) + printf ("Symbol: %s\r\n", name); + + // Read the icon data + text = read_icon_data (name, height_name, width_name, text, data_end); + height_name = NULL; + width_name = NULL; + } + + // Skip to the next line + data = next_line; + } + if (data >= data_end) + status = 0; + return status; +} + +void +display_icon ( + ICON_ENTRY * icon + ) +{ + int bit; + int display_width; + const char * indent = " "; + int index; + int max_index; + int x; + int y; + + /* + 0x20, 0x60, 0xC0, 0xFF, 0xFF, 0xC0, 0x60, 0x20, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00 + + ** + ** + ** + ** + ** + ** ** ** + ****** + **** + ** + */ + + printf ("/*\r\n"); + printf (" %s [%d, %d]\r\n", icon->name, icon->width, icon->height); + printf ("\r\n"); + if (DRAW_OUTLINE) { + if (icon->width > 9) { + printf ("%s ", indent); + for (x = 1; x <= icon->width; x++) + printf ("%c", (x % 10) ? ' ' : ('0' + (x / 10))); + printf ("\r\n"); + } + printf ("%s ", indent); + for (x = 1; x <= icon->width; x++) + printf ("%d", x % 10); + printf ("\r\n"); + printf ("%s .", indent); + for (x = 0; x < icon->width; x++) + printf ("-"); + printf (".\r\n"); + } + for (y = 0; y < icon->height; y++) { + printf ("%s", indent); + if (DRAW_OUTLINE) + printf ("0x%02x|", 1 << (y & 7)); + for (x = 0; x < icon->width; x++) { + bit = icon->data[((y >> 3) * icon->bytes_wide) + x]; + bit >>= y & 7; + printf ("%c", (bit & 1) ? '*' : ' '); + } + if (DRAW_OUTLINE) + printf ("|"); + printf ("\r\n"); + } + if (DRAW_OUTLINE) { + printf ("%s '", indent); + for (x = 0; x < icon->width; x++) + printf ("-"); + printf ("'\r\n"); + } + printf ("*/\r\n"); + printf ("\r\n"); + + // Display the symbols if requested + if (DISPLAY_VARIABLES) { + if (icon->height_name) + printf ("const int %s = %d;\r\n", icon->height_name, icon->height); + if (icon->width_name) + printf ("const int %s = %d;\r\n", icon->width_name, icon->width); + printf ("const uint8_t %s [] = {", icon->name); + display_width = icon->bytes_wide; + if (WRAP_AT_16 || (display_width > 20)) + display_width = 16; + max_index = icon->bytes_high * icon->bytes_wide; + for (index = 0; index < max_index; index++) { + if ((index % display_width) == 0) + printf ("\r\n "); + if (USE_UPPERCASE_A) + printf (" 0x%02X", icon->data[index]); + else + printf (" 0x%02x", icon->data[index]); + if (index != (max_index - 1)) + printf (","); + } + if (ADD_TRAILING_COMMA) + printf (","); + printf ("\r\n"); + printf ("};\r\n"); + printf ("\r\n"); + } +} + +int +main ( + int argc, + char ** argv + ) +{ + char * filename; + ICON_ENTRY * icon; + char * icon_name; + int icon_file; + off_t file_length; + int status; + ssize_t valid_data; + + do { + // Assume failure + icon_file = -1; + status = -1; + + // Get the icon name + if (argc != 2) { + fprintf (stderr, "ERROR - Icon file name not specified!\r\n"); + break; + } + filename = argv[1]; + + // Open the icon file + icon_file = open (filename, O_RDONLY); + if (icon_file < 0) { + perror("ERROR - File open failed!"); + break; + } + + // Determine the length of the file + file_length = lseek (icon_file, 0, SEEK_END); + + // Go the the beginning of the file + lseek (icon_file, 0, SEEK_SET); + + // Allocate the buffer + icon_text = malloc (file_length + 1); + if (!icon_text) { + fprintf (stderr, "ERROR - Failed to allocate icon buffer!\r\n"); + break; + } + + // Zero terminate the last line in the file + icon_text[file_length] = 0; + + // Fill the buffer + valid_data = read(icon_file, icon_text, file_length); + if (valid_data < 0) { + fprintf (stderr, "ERROR - File read failed!\r\n"); + break; + } + + // Process the data + if (valid_data) + if (process_data(icon_file, valid_data)) + break; + + // Display the initial comment + if (DISPLAY_COMMENT) { + printf ("//Create a bitmap then use http://en.radzio.dxp.pl/bitmap_converter/ to generate output\r\n"); + printf ("//Make sure the bitmap is n*8 pixels tall (pad white pixels to lower area as needed)\r\n"); + printf ("//Otherwise the bitmap bitmap_converter will compress some of the bytes together\r\n"); + printf ("\r\n"); + } + + // Display the icon names + icon = icon_list; + while (icon) { + display_icon (icon); + icon = icon->next; + } + + // Indicate success + status = 0; + } while (0); + + // Close the file + if (icon_file >= 0) + close(icon_file); + + return status; +} diff --git a/Graphics/C/makefile b/Graphics/C/makefile new file mode 100644 index 000000000..8ce498b47 --- /dev/null +++ b/Graphics/C/makefile @@ -0,0 +1,43 @@ +###################################################################### +# makefile +# +# Builds the RTK support programs +###################################################################### + +########## +# Source files +########## + +EXECUTABLES = Fonts +EXECUTABLES += Icons + +########## +# Buid tools and rules +########## + +GCC = gcc +CFLAGS = -flto -O3 -Wpedantic -pedantic-errors -Wall -Wextra -Werror -Wno-unused-variable -Wno-unused-parameter +CC = $(GCC) $(CFLAGS) + +%.o: %.c $(INCLUDES) + $(CC) -c -o $@ $< + +%: %.c $(INCLUDES) + $(CC) $(CFLAGS) -o $@ $< + +########## +# Buid all the sources - must be first +########## + +.PHONY: all + +all: $(EXECUTABLES) + +######## +# Clean the build directory +########## + +.PHONY: clean + +clean: + rm -f *.o *.a $(EXECUTABLES) diff --git a/Graphics/CrossHair-Dual.bmp b/Graphics/CrossHair-Dual.bmp new file mode 100644 index 000000000..fcc9fdb0a Binary files /dev/null and b/Graphics/CrossHair-Dual.bmp differ diff --git a/Graphics/CrossHair.bmp b/Graphics/CrossHair.bmp new file mode 100644 index 000000000..be93b8a57 Binary files /dev/null and b/Graphics/CrossHair.bmp differ diff --git a/Graphics/DownloadArrow.bmp b/Graphics/DownloadArrow.bmp new file mode 100644 index 000000000..e9ee5b828 Binary files /dev/null and b/Graphics/DownloadArrow.bmp differ diff --git a/Graphics/DynamicModel-1-Portable.bmp b/Graphics/DynamicModel-1-Portable.bmp new file mode 100644 index 000000000..898b31811 Binary files /dev/null and b/Graphics/DynamicModel-1-Portable.bmp differ diff --git a/Graphics/DynamicModel-10-Bike.bmp b/Graphics/DynamicModel-10-Bike.bmp new file mode 100644 index 000000000..a1e3f90cc Binary files /dev/null and b/Graphics/DynamicModel-10-Bike.bmp differ diff --git a/Graphics/DynamicModel-11-Mower.bmp b/Graphics/DynamicModel-11-Mower.bmp new file mode 100644 index 000000000..6813dc2f9 Binary files /dev/null and b/Graphics/DynamicModel-11-Mower.bmp differ diff --git a/Graphics/DynamicModel-12-EScooter.bmp b/Graphics/DynamicModel-12-EScooter.bmp new file mode 100644 index 000000000..291bb6016 Binary files /dev/null and b/Graphics/DynamicModel-12-EScooter.bmp differ diff --git a/Graphics/DynamicModel-2-Stationary.bmp b/Graphics/DynamicModel-2-Stationary.bmp new file mode 100644 index 000000000..b42214676 Binary files /dev/null and b/Graphics/DynamicModel-2-Stationary.bmp differ diff --git a/Graphics/DynamicModel-3-Pedestrian.bmp b/Graphics/DynamicModel-3-Pedestrian.bmp new file mode 100644 index 000000000..6c68cf2db Binary files /dev/null and b/Graphics/DynamicModel-3-Pedestrian.bmp differ diff --git a/Graphics/DynamicModel-4-Automotive.bmp b/Graphics/DynamicModel-4-Automotive.bmp new file mode 100644 index 000000000..1224159c3 Binary files /dev/null and b/Graphics/DynamicModel-4-Automotive.bmp differ diff --git a/Graphics/DynamicModel-5-Sea.bmp b/Graphics/DynamicModel-5-Sea.bmp new file mode 100644 index 000000000..56c5a9f27 Binary files /dev/null and b/Graphics/DynamicModel-5-Sea.bmp differ diff --git a/Graphics/DynamicModel-6-Airborne-1g.bmp b/Graphics/DynamicModel-6-Airborne-1g.bmp new file mode 100644 index 000000000..a26dc00b2 Binary files /dev/null and b/Graphics/DynamicModel-6-Airborne-1g.bmp differ diff --git a/Graphics/DynamicModel-7-Airborne-2g.bmp b/Graphics/DynamicModel-7-Airborne-2g.bmp new file mode 100644 index 000000000..e46d90c14 Binary files /dev/null and b/Graphics/DynamicModel-7-Airborne-2g.bmp differ diff --git a/Graphics/DynamicModel-8-Airborne-4g.bmp b/Graphics/DynamicModel-8-Airborne-4g.bmp new file mode 100644 index 000000000..169508aef Binary files /dev/null and b/Graphics/DynamicModel-8-Airborne-4g.bmp differ diff --git a/Graphics/DynamicModel-9-Wrist.bmp b/Graphics/DynamicModel-9-Wrist.bmp new file mode 100644 index 000000000..54041c0bd Binary files /dev/null and b/Graphics/DynamicModel-9-Wrist.bmp differ diff --git a/Graphics/ESP NOW Symbol-0.bmp b/Graphics/ESP NOW Symbol-0.bmp new file mode 100644 index 000000000..3e85c99db Binary files /dev/null and b/Graphics/ESP NOW Symbol-0.bmp differ diff --git a/Graphics/ESP NOW Symbol-1.bmp b/Graphics/ESP NOW Symbol-1.bmp new file mode 100644 index 000000000..6a390c4e5 Binary files /dev/null and b/Graphics/ESP NOW Symbol-1.bmp differ diff --git a/Graphics/ESP NOW Symbol-2.bmp b/Graphics/ESP NOW Symbol-2.bmp new file mode 100644 index 000000000..33e157be7 Binary files /dev/null and b/Graphics/ESP NOW Symbol-2.bmp differ diff --git a/Graphics/ESP NOW Symbol-3.bmp b/Graphics/ESP NOW Symbol-3.bmp new file mode 100644 index 000000000..88b9b60bc Binary files /dev/null and b/Graphics/ESP NOW Symbol-3.bmp differ diff --git a/Graphics/LCDAssistant.exe b/Graphics/LCDAssistant.exe new file mode 100644 index 000000000..428e034ca Binary files /dev/null and b/Graphics/LCDAssistant.exe differ diff --git a/Graphics/Logging-0.bmp b/Graphics/Logging-0.bmp new file mode 100644 index 000000000..6b304ffd1 Binary files /dev/null and b/Graphics/Logging-0.bmp differ diff --git a/Graphics/Logging-1.bmp b/Graphics/Logging-1.bmp new file mode 100644 index 000000000..6d4b942e4 Binary files /dev/null and b/Graphics/Logging-1.bmp differ diff --git a/Graphics/Logging-2.bmp b/Graphics/Logging-2.bmp new file mode 100644 index 000000000..20d8a30d6 Binary files /dev/null and b/Graphics/Logging-2.bmp differ diff --git a/Graphics/Logging-3.bmp b/Graphics/Logging-3.bmp new file mode 100644 index 000000000..7a501b4ec Binary files /dev/null and b/Graphics/Logging-3.bmp differ diff --git a/Graphics/Logging-Custom-1.bmp b/Graphics/Logging-Custom-1.bmp new file mode 100644 index 000000000..2b4e3a7e4 Binary files /dev/null and b/Graphics/Logging-Custom-1.bmp differ diff --git a/Graphics/Logging-Custom-2.bmp b/Graphics/Logging-Custom-2.bmp new file mode 100644 index 000000000..7c37e16a1 Binary files /dev/null and b/Graphics/Logging-Custom-2.bmp differ diff --git a/Graphics/Logging-Custom-3.bmp b/Graphics/Logging-Custom-3.bmp new file mode 100644 index 000000000..efaa4b195 Binary files /dev/null and b/Graphics/Logging-Custom-3.bmp differ diff --git a/Graphics/Logging-PPP-1.bmp b/Graphics/Logging-PPP-1.bmp new file mode 100644 index 000000000..fc499e00c Binary files /dev/null and b/Graphics/Logging-PPP-1.bmp differ diff --git a/Graphics/Logging-PPP-2.bmp b/Graphics/Logging-PPP-2.bmp new file mode 100644 index 000000000..1e9be2ee4 Binary files /dev/null and b/Graphics/Logging-PPP-2.bmp differ diff --git a/Graphics/Logging-PPP-3.bmp b/Graphics/Logging-PPP-3.bmp new file mode 100644 index 000000000..ffb05bfcf Binary files /dev/null and b/Graphics/Logging-PPP-3.bmp differ diff --git a/Graphics/RTK Surveyor Logo.png b/Graphics/RTK Surveyor Logo.png new file mode 100644 index 000000000..5a1cad566 Binary files /dev/null and b/Graphics/RTK Surveyor Logo.png differ diff --git a/Graphics/RTK Surveyor Logo.svg b/Graphics/RTK Surveyor Logo.svg new file mode 100644 index 000000000..6a0a3f00e --- /dev/null +++ b/Graphics/RTK Surveyor Logo.svg @@ -0,0 +1,73 @@ + +image/svg+xml + + + + + + + + + + + + + + + + + + diff --git a/Graphics/Rover-Fusion-Empty.bmp b/Graphics/Rover-Fusion-Empty.bmp new file mode 100644 index 000000000..ce2c1da99 Binary files /dev/null and b/Graphics/Rover-Fusion-Empty.bmp differ diff --git a/Graphics/Rover-Fusion.bmp b/Graphics/Rover-Fusion.bmp new file mode 100644 index 000000000..275e20b96 Binary files /dev/null and b/Graphics/Rover-Fusion.bmp differ diff --git a/Graphics/SIV-Antenna-LBand.bmp b/Graphics/SIV-Antenna-LBand.bmp new file mode 100644 index 000000000..306068dee Binary files /dev/null and b/Graphics/SIV-Antenna-LBand.bmp differ diff --git a/Graphics/SIV-Antenna.bmp b/Graphics/SIV-Antenna.bmp new file mode 100644 index 000000000..1ddbbe453 Binary files /dev/null and b/Graphics/SIV-Antenna.bmp differ diff --git a/Graphics/Unused/RTCM Antenna.bmp b/Graphics/Unused/RTCM Antenna.bmp new file mode 100644 index 000000000..b3df53a2e Binary files /dev/null and b/Graphics/Unused/RTCM Antenna.bmp differ diff --git a/Graphics/WiFi Symbol-0.bmp b/Graphics/WiFi Symbol-0.bmp new file mode 100644 index 000000000..c4620a85a Binary files /dev/null and b/Graphics/WiFi Symbol-0.bmp differ diff --git a/Graphics/WiFi Symbol-1.bmp b/Graphics/WiFi Symbol-1.bmp new file mode 100644 index 000000000..ad3b848e9 Binary files /dev/null and b/Graphics/WiFi Symbol-1.bmp differ diff --git a/Graphics/WiFi Symbol-2.bmp b/Graphics/WiFi Symbol-2.bmp new file mode 100644 index 000000000..dc6494cc8 Binary files /dev/null and b/Graphics/WiFi Symbol-2.bmp differ diff --git a/Graphics/WiFi Symbol-3.bmp b/Graphics/WiFi Symbol-3.bmp new file mode 100644 index 000000000..d0be0258d Binary files /dev/null and b/Graphics/WiFi Symbol-3.bmp differ diff --git a/Graphics/WiFi Symbol.bmp b/Graphics/WiFi Symbol.bmp new file mode 100644 index 000000000..d0be0258d Binary files /dev/null and b/Graphics/WiFi Symbol.bmp differ diff --git a/Graphics/icon.h b/Graphics/icon.h new file mode 100644 index 000000000..92274d4b4 --- /dev/null +++ b/Graphics/icon.h @@ -0,0 +1,9 @@ +//------------------------------------------------------------------------------ +// File generated by LCD Assistant +// http://en.radzio.dxp.pl/bitmap_converter/ +//------------------------------------------------------------------------------ + +const unsigned char WiFi Symbol-0 [] = { +0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xC0, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; diff --git a/Issue_Template.md b/Issue_Template.md index c5aa9d153..dbb223762 100644 --- a/Issue_Template.md +++ b/Issue_Template.md @@ -1,14 +1,13 @@ ### Subject of the issue -Describe your issue here. If you reference a datasheet please specify which one and in which section (ie, the protocol manual, section 5.1.2). Additionally, screenshots are easy to paste into github. +Describe your issue here. Additionally, screenshots are easy to paste into github. ### Your workbench -* What version of RTK Surveyor firmware are you running? This can be found when the serial menu is opened (also in the settings.txt file, and in the serial output at power up). -* Are you connected to the device over Bluetooth? What app? Are you transmitting NTRIP back to the device? -* How is everything being powered? +* What version of RTK firmware are you running? This can be found when the serial menu is opened (also in the settings.txt file, and in the serial output at power up). +* What radios are you using: Bluetooth, WiFi, and/or ESP-Now? What app are you using to connect over Bluetooth? Are you transmitting NTRIP back to the device? * Are there any additional details that may help us help you? ### Steps to reproduce -Tell us how to reproduce this issue. Please include a copy of your settings.txt file used on the unit. +Tell us how to reproduce this issue. Please post any log files from serial output that may have been generated. ### Expected behavior Tell us what should happen diff --git a/README.md b/README.md index 5654a4ff8..11f0f9d18 100644 --- a/README.md +++ b/README.md @@ -3,46 +3,113 @@ SparkFun RTK Firmware - - + + + - - + + + + + + + + + + + + + + + + + + + + + + +
SparkFun RTK Surveyor (SPX-17369)SparkFun RTK Express (SPX-18019)SparkFun RTK Facet L-Band (GPS-20000)SparkFun RTK Facet (GPS-19029)SparkFun RTK Reference Station (GPS-22429)
Hookup GuideHookup GuideHookup Guide
SparkFun RTK Express Plus (GPS-18590)SparkFun RTK Express (GPS-18442)SparkFun RTK Surveyor (GPS-18443)
Hookup GuideHookup GuideHookup Guide
-The [SparkFun RTK Surveyor](https://www.sparkfun.com/products/17369) and [SparkFun RTK Express](https://www.sparkfun.com/products/18019) are centimeter-level GNSS receivers. With RTK enabled, these devices can output your location with 14mm horizontal and vertical *accuracy* at up to 20Hz! +The [SparkFun RTK Surveyor](https://www.sparkfun.com/products/18443), [SparkFun RTK Express](https://www.sparkfun.com/products/18442), [SparkFun RTK Express Plus](https://www.sparkfun.com/products/18590), [SparkFun RTK Facet](https://www.sparkfun.com/products/19029), [SparkFun RTK Facet L-Band](https://www.sparkfun.com/products/20000) and [SparkFun RTK Reference Station](https://www.sparkfun.com/products/22429) are centimeter-level GNSS receivers. With RTK enabled, these devices can output your location with 14mm horizontal and vertical [*accuracy*](https://docs.sparkfun.com/SparkFun_RTK_Firmware/accuracy_verification/) at up to 20Hz! + +This repo houses the [RTK Product Manual](https://docs.sparkfun.com/SparkFun_RTK_Firmware) and the firmware that runs on the SparkFun RTK product line including: -This repo houses the firmware that runs on the SparkFun RTK product line including: +* [SparkFun RTK Facet L-Band](https://www.sparkfun.com/products/20000) +* [SparkFun RTK Facet](https://www.sparkfun.com/products/19029) +* [SparkFun RTK Reference Station](https://www.sparkfun.com/products/22429) +* [SparkFun RTK Express Plus](https://www.sparkfun.com/products/18590) +* [SparkFun RTK Express](https://www.sparkfun.com/products/18442) +* [SparkFun RTK Surveyor](https://www.sparkfun.com/products/18443) -* [SparkFun RTK Surveyor](https://www.sparkfun.com/products/17369) -* [SparkFun RTK Express](https://www.sparkfun.com/products/18019) +For compiled binaries of the firmware, please see [SparkFun RTK Firmware Binaries](https://github.com/sparkfun/SparkFun_RTK_Firmware_Binaries). If you're interested in the PCB, enclosure, or overlay on each product please see the hardware repos: -* [SparkFun RTK Surveyor Hardware](https://github.com/sparkfun/SparkFun_RTK_Surveyor) +* [SparkFun RTK Facet L-Band Hardware](https://github.com/sparkfun/SparkFun_RTK_Facet) +* [SparkFun RTK Facet Hardware](https://github.com/sparkfun/SparkFun_RTK_Facet) +* [SparkFun RTK Reference Station Hardware](https://github.com/sparkfun/SparkFun_RTK_Reference_Station) +* [SparkFun RTK Express Plus Hardware](https://github.com/sparkfun/SparkFun_RTK_Express_Plus) * [SparkFun RTK Express Hardware](https://github.com/sparkfun/SparkFun_RTK_Express) +* [SparkFun RTK Surveyor Hardware](https://github.com/sparkfun/SparkFun_RTK_Surveyor) Thanks: * Special thanks to [Avinab Malla](https://github.com/avinabmalla) for the creation of [SW Maps](https://play.google.com/store/apps/details?id=np.com.softwel.swmaps&hl=en_US&gl=US) and for pointers on handling the ESP32 read/write tasks. +Documentation +-------------- + +* **[RTK Product Manual](https://docs.sparkfun.com/SparkFun_RTK_Firmware/)** - A detail guide describing all the various software features of the RTK product line. Essentially it is a manual for the firmware in this repository. +* **[RTK Facet L-Band Hookup Guide](https://learn.sparkfun.com/tutorials/sparkfun-rtk-facet-l-band-hookup-guide)** - Hookup guide for the SparkFun RTK Facet L-Band. +* **[RTK Facet Hookup Guide](https://learn.sparkfun.com/tutorials/sparkfun-rtk-facet-hookup-guide)** - Hookup guide for the SparkFun RTK Facet. +* **[RTK Reference Station Hookup Guide](https://learn.sparkfun.com/tutorials/sparkfun-rtk-reference-station-hookup-guide)** - Hookup guide for the SparkFun RTK Reference Station. +* **[RTK Express Hookup Guide](https://learn.sparkfun.com/tutorials/sparkfun-rtk-express-hookup-guide)** - Hookup guide for the SparkFun RTK Express and Express Plus. +* **[RTK Surveyor Hookup Guide](https://learn.sparkfun.com/tutorials/sparkfun-rtk-surveyor-hookup-guide)** - Hookup guide for the SparkFun RTK Surveyor. + Repository Contents ------------------- -* **/Binaries** - Loadable firmware either over USB or via SD card -* **/Firmware** - Main firmware as well as various feature unit tests +* **/Firmware** - Source code for SparkFun RTK firmware as well as various feature unit tests +* **/Graphics** - Original bitmap icons for the display +* **/docs** - Markdown pages for the [RTK Product Manual](https://docs.sparkfun.com/SparkFun_RTK_Firmware/) -Documentation --------------- +Repository Branch Structure +--------------------------- + +This repository has two long-term branches: `main` and `release_candidate`. + +With respect to the firmware, `main` is a branch where only changes that are appropriate for all users are applied. Thus, following `main` means updating to normal releases, and perhaps bugfixes to those releases. + +In contrast, `release_candidate` is where new code is added as it is developed. + +The documentation source code is in docs/ on `main`. It is built automatically on push and stored in the branch `gh-pages`, from which it is served at the above URL. Documentation changes are pushed directly to main. + +Release Process +--------------- + +A release is made by merging `release_candidate` back to `main`, and then applying a tag to that commit on `main`. + +A pre-release is often created using the latest stable release candidate. These binaries will have extra debug statements turned on that will not be present in a formal release, but should not affect behavior of the firmware. + +Building from Source +-------------------- + +For building the firmware, see the [Firmware README](Firmware/readme.md). For compiled binaries of the firmware, please see [SparkFun RTK Firmware Binaries](https://github.com/sparkfun/SparkFun_RTK_Firmware_Binaries). + +For the documentation, see [mkdocs.yml](https://github.com/sparkfun/SparkFun_RTK_Firmware/blob/main/mkdocs.yml) and [/workflows/mkdocs.yml](https://github.com/sparkfun/SparkFun_RTK_Firmware/blob/main/.github/workflows/mkdocs.yml). + +For building the Uploader_GUI see [SparkFun_RTK_Firmware_Uploader](https://github.com/sparkfun/SparkFun_RTK_Firmware_Uploader). The pyinstaller executables are generated by the [/workflows](https://github.com/sparkfun/SparkFun_RTK_Firmware_Uploader/tree/main/.github/workflows) -* **[Hookup Guide](https://learn.sparkfun.com/tutorials/sparkfun-rtk-surveyor-hookup-guide)** - Hookup guide for the SparkFun RTK Surveyor. +For building the u-blox_Update_GUI see [u-blox_Update_GUI](https://github.com/sparkfun/SparkFun_RTK_Firmware_Binaries/tree/main/u-blox_Update_GUI) and the header comments of [RTK_u-blox_Update_GUI.py](https://github.com/sparkfun/SparkFun_RTK_Firmware/blob/main/u-blox_Update_GUI/RTK_u-blox_Update_GUI.py) License Information ------------------- -This product is _**open source**_! +This product is _**open source**_! Please feel free to [contribute](https://docs.sparkfun.com/SparkFun_RTK_Firmware/contribute/) to both the firmware and documentation. Various bits of the code have different licenses applied. Anything SparkFun wrote is beerware; if you see me (or any other SparkFun employee) at the local, and you've found our code helpful, please buy us a round! diff --git a/docs/accuracy_verification.md b/docs/accuracy_verification.md new file mode 100644 index 000000000..6f8726ed6 --- /dev/null +++ b/docs/accuracy_verification.md @@ -0,0 +1,199 @@ +# Accuracy Verification + +Surveyor: ![Feature Supported](img/Icons/GreenDot.png) / Express: ![Feature Supported](img/Icons/GreenDot.png) / Express Plus: ![Feature Supported](img/Icons/GreenDot.png) / Facet: ![Feature Supported](img/Icons/GreenDot.png) / Facet L-Band: ![Feature Supported](img/Icons/GreenDot.png) / Reference Station: ![Feature Supported](img/Icons/GreenDot.png) + +![Facet in the field](img/VerifyAccuracy/SparkFun%20Verify%20RTK%20-%2016%20Facet%20in%20the%20Field.jpg) + +*Facet in the field* + +You’ve got an incredibly powerful GNSS receiver in your hands. How do you verify that you can get really accurate location readings? It's a bit of work but it's a lot of fun and you'll learn a tremendous amount about surveying along the way. + +This is, admittedly, a very US-centric tutorial. We hope that it will provide some of the tools and basic guidance to be replicated in other countries. If you have additional sources for GPS/GNSS surveyed monuments in your own country, consider [adding them](/contribute) to this document! + +This is a replication and confirmation of the procedure done by [RTKLibExplorer in 2018](https://rtklibexplorer.wordpress.com/2018/03/17/measuring-a-survey-marker-with-the-datagnss-d302-rtk/). We modified it to demonstrate a similar process but using u-blox hardware and with a few updates. + +The process goes like this: + +* Find a local monument +* Convert the coordinates +* Take measurements +* Calculate differences + +## Get Used to RTK + +![RTK Fix Mode](img/Displays/SparkFun_RTK_Express_-_Display_-_Rover_RTK_Fixed.jpg) + +*RTK Fix Mode* + +Before we can consider doing anything in the field, we need to get really comfortable using the RTK product. Verify you can get your device into RTK Fix mode. This includes setting up a permanent base and/or using a service like Skylark to provide the correction data to the RTK product. Before planning a trip to the field get used to using the RTK product in Rover mode with NTRIP corrections being passed over Bluetooth to your device. + +## Locate GPS Monument + +[![A common metal surveyor's mark](img/VerifyAccuracy/SparkFun%20Verify%20RTK%20-%2017%20Surveyor%20Monument.jpg)](img/VerifyAccuracy/SparkFun%20Verify%20RTK%20-%2017%20Surveyor%20Monument - Big.jpg) + +*A common metal surveyor's mark* + +Locate a GPS monument. These are the little metal caps, placed by surveyors, embedded into the sidewalk and roads around populated areas. While monuments are fairly common, we need a monument whose location is precisely known. + +![Boulder has a large number of monuments](img/VerifyAccuracy/SparkFun%20Verify%20RTK%20-%201%20Boulder%20Sites.jpg) + +*Boulder has a large number of monuments* + +Thankfully, in the USA the National Geodetic Survey has an incredible database of public monuments. Enter your location into the [NGS Data Explorer](https://www.ngs.noaa.gov/NGSDataExplorer/) to find the nearest monument. + +![Boulder's GPS monuments](img/VerifyAccuracy/SparkFun%20Verify%20RTK%20-%202%20Boulder%20GPS%20Sites.jpg) + +*Boulder's GPS monuments* + +While the Data Explorer will show many marks, turn off all but the GPS marks. These were surveyed with extreme precision and have published coordinates. + +## Convert Monument Location + +![Monument near SparkFun](img/VerifyAccuracy/SparkFun%20Verify%20RTK%20-%203%20SparkFun%20HQ.jpg) + +*Monument near SparkFun* + +Find a GPS monument that is easiest for you to get to, click on it, and open the datasheet. You'll see a large amount of text and data for that specific location. + +![Position in both NAD83 and ECEF](img/VerifyAccuracy/SparkFun%20Verify%20RTK%20-%2013%20Datasheet%20for%20Monument.jpg) + +*Position in both NAD83 and ECEF* + +My respect for the surveying industry grows daily, but that doesn't mean they are free from competing and confusing standards. What you need to know is that the SparkFun RTK product line outputs coordinates in the **WGS84** coordinate system by default and can output **ECEF** as well. Most of the coordinates by the NGS are **NAD83** which has about a 1.5-meter difference from the WGS84 coordinate system. No big deal for general mapping but it'll throw a wrench in your testing if you're not careful. + +The SparkFun example monument is at: + +* Latitude: 40 05 14.86880 (NAD83 in 2012) +* Longitude: -105 09 01.68689 (NAD83 in 2012) +* Elliptical Height: 1613.737 meters (NAD83 in 2012) + +### Convert NAD83 to Today + +So we know the NAD83 Lat/Long of our monument, right? Not quite. + +![Plate movement map](img/VerifyAccuracy/SparkFun%20Verify%20RTK%20-%206%20Plate%20Movements.jpg) + +*Example plate movement map* + +The earth is not static and the tectonic plates have this [annoying habit of moving](https://www.ngs.noaa.gov/TOOLS/Htdp/Htdp.shtml). Note that the coordinates from our datasheet are from 6/27/2012. Ten years of movement can affect millimeter measurements. + +![Plate tectonic time machine](img/VerifyAccuracy/SparkFun%20Verify%20RTK%20-%207%20HTDP%20Conversion%20Page.jpg) + +*Plate tectonic time machine* + +Thankfully the NGS has a tool called [**Horizontal Time-Dependent Positioning**](https://www.ngs.noaa.gov/cgi-bin/HTDP/htdp.prl?f1=4&f2=1). This allows both the conversion between coordinate systems and adjusting a given location to a given start and end time. Use the tool to convert the NAD83 coordinates of your monument from the time they were taken (June 27, 2012, in our example) to WGS84(G2139) coordinates on today's date. If you convert the location for your monument on a Tuesday and visit it 5 days later, the coordinates should still be perfectly fine. This tool is needed both for the coordinate change (NAD83 to WGS84) and for long (months or years) periods between when the monument was surveyed. + +![Monument converted to WGS84 corrected to 2022](img/VerifyAccuracy/SparkFun%20Verify%20RTK%20-%205%20Conversion%20to%20WGS84.jpg) + +*Monument converted to WGS84 corrected to 2022* + +Once we enter all the pertinent data, we receive a nice output showing us our modern-day WGS84 coordinates! Also, note the X/Y/Z ECEF coordinates. + +The SparkFun example monument is at: + +* Latitude: 40 05 14.88667 (WGS84 in 2022) +* Longitude: -105 09 01.74023 (WGS84 in 2022) +* Elliptical Height: 1612.873 meters (WGS84 in 2022) + +And in ECEF (this will be handy in a minute): + +* X: -1277423.441 m (ECEF in 2022) +* Y: -4717810.159 m (ECEF in 2022) +* Z: 4086459.331 m (ECEF in 2022) + +### Convert from HH:MM:SS to Decimal + +![Conversion to Decimal](img/VerifyAccuracy/SparkFun%20Verify%20RTK%20-%204%20Conversion%20to%20Decimal.jpg) + +*Conversion to Decimal* + +The NGS coordinates are in the hour/minute/second format (ie, 40 05 14.86880). We need decimal format when we're in the field viewing locations in SW Maps. We recommend the [LatLong.net](https://www.latlong.net/degrees-minutes-seconds-to-decimal-degrees) converter, but there are many options. Make sure the tools, converters, and calculators you use maintain 8 decimal places. + +The SparkFun example monument is at: + +* Latitude: 40.08746852 (WGS84 in 2022) +* Longitude: -105.15048340 (WGS84 in 2022) +* Elliptical Height: 1613.737 meters (WGS84 in 2022) + +These are the coordinates we hope to see using SW Maps once we get out into the field. Write down your monument coordinates so that you have some idea of how close your unit is to the ideal in real-time. + +## Field Trip! + +![Cheap tripod above the monument](img/VerifyAccuracy/SparkFun%20Verify%20RTK%20-%208%20Facet%20above%20Monument.jpg) + +*Cheap tripod above the monument* + +Not a bad view! + +You will need to decide how cheap you want your setup to be. I went too cheap; my tripod doesn’t have a hook on the bottom so the string with a bolt (I didn’t even have a plumb bob) to center above the marker was not central to the Facet. The height measurement from the mark to the ARP (bottom of the Facet) was done with a tape measure, in other words, not very accurate. But *it works*! + +Find the monument and locate your Facet (or RTK Surveyor, Express, Express Plus, Facet L-Band, etc) over the monument. Using a tape measure or other tool, measure the distance from the top of the monument to the bottom of the Facet. In this example, it was 45 ¾” or 1162mm. Obviously, millimeters matter here but don't let 'perfection' be the enemy of 'done'. + +![L-Band Facet ARP](img/SparkFun_RTK_Facet_L-Band_ARP.jpg) + +*L-Band Facet ARP* + +Locate the ARP of your given RTK product ([53mm](https://geodesy.noaa.gov/ANTCAL/LoadFile?file=SFETOP106_NONE.atx) for units using the TOP106 Antenna, [61mm](https://learn.sparkfun.com/tutorials/sparkfun-rtk-facet-hookup-guide/all#hardware-assembly) for Facet, [69mm](https://learn.sparkfun.com/tutorials/sparkfun-rtk-facet-l-band-hookup-guide/all) for Facet L-Band). Add your ARP to the height above the monument you measured previously. In this example 1416 + 69 = 1.485m. Enter that total height into SW Maps as the ‘Instrument Height’. This will allow the software to subtract the antenna location height from the current 3D location to gain the location of the point where the plumb bob (or bolt) below your apparatus is located. + +With your instrument height determined, connect to the RTK product, begin sending RTCM corrections (either over NTRIP or radio link) and enter RTK Fix. + +## Record Readings + +![SW Maps screenshot of monument location](img/VerifyAccuracy/SparkFun%20Verify%20RTK%20-%2012%20Marker%20on%20Map.jpg) + +*SW Maps screenshot of monument location* + +We can see the approximate location of the monument in the above location. + +![Screenshot of a point in time](img/VerifyAccuracy/SparkFun%20Verify%20RTK%20-%209%20SW%20Maps%20Point.jpg) + +*Screenshot of a point in time* + +Note the 8 decimal places on the Lat/Long. + +Screenshots are an easy way to record lat/long/alt but SW Maps (and other GIS software) allows the averaging of a position. Choose your own adventure. For our example, we took screenshots/snapshots of the location. Some surveyors hold a position for multiple minutes to get a point; we can do the same in under a second. + +![Comparison of three RTK correction sources](img/VerifyAccuracy/SparkFun%20Verify%20RTK%20-%2013%20Compare%20Points.jpg) + +*Comparison of three RTK correction sources* + +Off the shelf, we regularly see 300 down to 150mm horizontal positional accuracy using any RTK product with a good L1/L2 antenna. This is shown in the picture above as the circle with 'No Corrections'. + +With corrections turned on, the benefit of an RTK fix is obvious. The two surveyed points overlap each other so closely they are nearly indistinguishable. The SparkFun base station is documented [here](https://learn.sparkfun.com/tutorials/how-to-build-a-diy-gnss-reference-station) and has a location accuracy of approximately 8.4mm. Using a base station is more accurate (as we will see) but L-Band corrections will also get you *incredibly* similar accuracy with a lot less hassle. + +## Why doesn’t it match the image? + +![Actual location vs image](img/VerifyAccuracy/SparkFun%20Verify%20RTK%20-%2014%20Image%20Pixel%20Comparison.jpg) + +*Actual location vs image* + +Your location bubble may not fall directly over the pixels representing the monument. Why? Imagine you are a satellite 50 miles above the earth’s surface. Now take a photo that is *many* megapixels. Now align all those pixels within a few millimeters of reality. Google maps (and all terrestrial imagery as an industry) does an incredible job of aligning the surface imagery but it is not perfect, and it is certainly not millimeter accurate. Do not assume the google maps image is where your monument actually exists. + +## Spreadsheet Party + +We’ve established the monument’s location, we’ve captured the location of the RTK Facet, and they are different, but by how much? Calculating the difference between Lat/Long coordinates is not trivial. We’ve found converting to the ECEF coordinate system is the easiest way to calculate the difference between GPS coordinates. + +![Convert LLA to ECEF](img/VerifyAccuracy/SparkFun%20Verify%20RTK%20-%2011%20LLA%20to%20ECEF.jpg) + +*Convert LLA to ECEF* + +Enter your lat, long, and altitude coordinates into an LLA to ECEF converter. We found the [Sysense calculator](http://www.sysense.com/products/ecef_lla_converter/index.html) to work very well. How do we know it’s accurate? Take the [original coordinates](img/VerifyAccuracy/SparkFun%20Verify%20RTK%20-%2013%20Datasheet%20for%20Monument.jpg) from the NGS Datasheet, and use the calculator to convert them to ECEF. They are identical. + +![ECEF difference between monument and readings](img/VerifyAccuracy/SparkFun%20Verify%20RTK%20-%2015%20Spreadsheet%20Results.jpg) + +*ECEF difference between monument and readings* + +Feel free to look at and make a copy of the [SparkFun example](https://docs.google.com/spreadsheets/d/1uEGnceLoAVwG3xnyWp8XTN8BBa__z4pg0l7IQRBcj8c/edit?usp=sharing) spreadsheet. ECEF is a wonderfully simplistic frame of reference; the comparison between two points is simply X/Y/Z in meters. We can use the Pythagorean theorem to calculate the 3D variance. In our example, it is 52mm using corrections from a fixed base, and 189mm for an L-Band corrected base. + +52mm off a professional mark is a clear indicator we are *very close* to the limit of our equipment. The sheer amount of geoscience, coordinate math, and relativistic physics that very smart people have contributed to enable any part of this experiment is awe-inspiring. It gave me great satisfaction and reassurance that our base at SparkFun HQ is set up well, and that, in the hands of a professional, the RTK product line is quite capable of providing *very* accurate readings. + +## How do I get 14mm?! + +* Use the best equipment. Our mechanical setup was rickety and cheap. Use a surveyor’s bipod setup, with a bubble level, and a prism pole to accurately level the RTK receiver and measure the distance to the monument. +* Use an antenna that is NGS calibrated to obtain accurate ARPs. The [SparkFun TOP106 antenna](https://www.sparkfun.com/products/17751) has been calibrated and we are in the process of calibrating the RTK Facet and RTK Facet L-Band. +* Use an accurate base. A temporary or ‘survey-in’ base will not be accurate. The base needs 24 hours of logging with a [PPP analysis](https://learn.sparkfun.com/tutorials/how-to-build-a-diy-gnss-reference-station/all#gather-raw-gnss-data). +* Be within 10km of your base. A baseline that is more than 10km will introduce inaccuracies to the RTK fix readings. +* Correction services are not as accurate as a fixed base. While services such as Skylark and PointPerfect are *convenient*, they use models to estimate the overall isotropic disturbance. A local, fixed base will outperform a correction service. +* Take an average of points. All the points taken in this example were single snapshots. Average a few seconds' worth of readings. + +This was a lot of fun and a good excuse to get outdoors. We hope you enjoy finding some new points in your world. diff --git a/docs/configure_with_bluetooth.md b/docs/configure_with_bluetooth.md new file mode 100644 index 000000000..e5e00c50e --- /dev/null +++ b/docs/configure_with_bluetooth.md @@ -0,0 +1,59 @@ +# Configure with Bluetooth + +Surveyor: ![Feature Supported](img/Icons/GreenDot.png) / Express: ![Feature Supported](img/Icons/GreenDot.png) / Express Plus: ![Feature Supported](img/Icons/GreenDot.png) / Facet: ![Feature Supported](img/Icons/GreenDot.png) / Facet L-Band: ![Feature Supported](img/Icons/GreenDot.png) / Reference Station: ![Feature Supported](img/Icons/GreenDot.png) + +![Configuration menu open over Bluetooth](img/Bluetooth/SparkFun%20RTK%20BEM%20-%20Config%20Menu.png) + +*Configuration menu via Bluetooth* + +Starting with firmware v3.0, Bluetooth-based configuration is supported. For more information about updating the firmware on your device, please see [Updating RTK Firmware](firmware_update.md). + +The RTK device will be a discoverable Bluetooth device (both BT SPP and BLE are supported). For information about Bluetooth pairing, please see [Connecting Bluetooth](connecting_bluetooth.md). + +## Entering Bluetooth Echo Mode + +![Entering the BEM escape characters](img/Bluetooth/SparkFun%20RTK%20BEM%20-%20EscapeCharacters.png) + +Once connected, the RTK device will report a large amount of NMEA data over the link. To enter Bluetooth Echo Mode send the characters +++. + +**Note:** There must be a 2 second gap between any serial sent from a phone to the RTK device, and any escape characters. In almost all cases this is not a problem. Just be sure it's been 2 seconds since an NTRIP source has been turned off and attempting to enter Bluetooth Echo Mode. + +![The GNSS message menu over BEM](img/Bluetooth/SparkFun%20RTK%20BEM%20-%20Config%20Menu.png) + +*The GNSS Messages menu shown over Bluetooth Echo Mode* + +Once in Bluetooth Echo Mode, any character sent from the RTK unit will be shown in the Bluetooth app, and any character sent from the connected device (cell phone, laptop, etc) will be received by the RTK device. This allows the opening of the config menu as well as the viewing of all regular system output. + +For more information about the Serial Config menu please see [Configure with Serial](configure_with_serial.md). + +![System output over Bluetooth](img/Bluetooth/SparkFun%20RTK%20BEM%20-%20System%20Output.png) + +*Exit from the Serial Config Menu* + +Bluetooth can also be used to view system status and output. Simply exit the config menu using option 'x' and the system output can be seen. + +## Exit Bluetooth Echo Mode + +To exit Bluetooth Echo Mode simply disconnect Bluetooth. In the Bluetooth Serial Terminal app, this is done by pressing the 'two plugs' icon in the upper right corner. + +![Exiting Bluetooth Echo Mode](img/Bluetooth/SparkFun%20RTK%20BEM%20-%20Exit%20BEM.png) + +*Menu option 'b' for exiting Bluetooth Echo Mode* + +Alternatively, if you wish to stay connected over Bluetooth but need to exit Bluetooth Echo Mode, use the 'b' menu option from the main menu. + +## Serial Bluetooth Terminal Settings + +Here we provide some settings recommendations to make the terminal navigation of the RTK device a bit easier. + +![Disable Time stamps](img/Bluetooth/SparkFun%20RTK%20BEM%20-%20Settings%20Terminal.png) + +*Terminal Settings with Timestamps disabled* + +Disable timestamps to make the window a bit wider, allowing the display of longer menu items without wrapping. + +![Clear on send](img/Bluetooth/SparkFun%20RTK%20BEM%20-%20Settings.png) + +*Clear on send and echo off* + +Clearing the input box when sending is very handy as well as turning off local echo. diff --git a/docs/configure_with_serial.md b/docs/configure_with_serial.md new file mode 100644 index 000000000..e8eee7fbd --- /dev/null +++ b/docs/configure_with_serial.md @@ -0,0 +1,95 @@ +# Configure with Serial + +Surveyor: ![Feature Supported](img/Icons/GreenDot.png) / Express: ![Feature Supported](img/Icons/GreenDot.png) / Express Plus: ![Feature Supported](img/Icons/GreenDot.png) / Facet: ![Feature Supported](img/Icons/GreenDot.png) / Facet L-Band: ![Feature Supported](img/Icons/GreenDot.png) / Reference Station: ![Feature Supported](img/Icons/GreenDot.png) + +**Note:** Starting with v3.0 of the firmware any serial menu that is shown can also be accessed over Bluetooth. This makes any configuration of a device much easier in the field. Please see [Configure With Bluetooth](configure_with_bluetooth.md) for more information. + +To configure an RTK device using serial attach a [USB C cable](https://www.sparkfun.com/products/15425) to the device. The device can be on or off. + +## RTK Surveyor / Express / Express+ + +![RTK Surveyor Connectors and label](img/Serial/SparkFun_RTK_Surveyor_-_Connectors1.jpg) + +*The SparkFun RTK Surveyor has a variety of connectors* + +Connect the USB cable to the connector labeled **Config ESP32**. + +Once connected a COM port will enumerate. Open the `Device Manager` in Windows and look under the Ports branch to see what COM port the device is assigned to. + +## RTK Facet + +![RTK Facet USB C Connector](img/Serial/SparkFun_RTK_Facet_-_Ports_-_USB.jpg) + +Connect the USB cable to the USB connector. + +There is a USB hub built into the RTK Facet. When you attach the device to your computer it will enumerate two COM ports. + +![Two COM ports from one USB device](img/Serial/SparkFun_RTK_Facet_-_Multiple_COM_Ports.jpg) + +In the image above, the `USB Serial Device` is the ZED-F9P and the `USB-SERIAL CH340` is the ESP32. + +**Don't See 'USB-Serial CH340'?** If you've never connected a CH340 device to your computer before, you may need to install drivers for the USB-to-serial converter. Check out our section on "How to Install CH340 Drivers" for help with the installation. + +**Don't See 'USB Serial Device'?** The first time a u-blox module is connected to a computer you may need to adjust the COM driver. Check out our section on "How to Install u-blox Drivers" for help with the installation. + +Configuring the RTK device is done over the *USB-Serial CH340* COM port via the serial text menu. Various debug messages are printed to this port at 115200bps and a serial menu can be opened to configure advanced settings. + +Configuring the ZED-F9P is done over the *USB Serial Device* port using [u-center](https://learn.sparkfun.com/tutorials/getting-started-with-u-center-for-u-blox/all). It’s not necessary for normal operation but is handy for tailoring the receiver to specific applications. As an added perk, the ZED-F9P can be detected automatically by some mobile phones and tablets. If desired, the receiver can be directly connected to a compatible phone or tablet removing the need for a Bluetooth connection. + +## Terminal Window + +Open a terminal window at 115200bps; you should see various status messages every second. Press any key to open the configuration menu. Not sure how to use a terminal? Check out our [Serial Terminal Basics](https://learn.sparkfun.com/tutorials/terminal-basics) tutorial. + +Note that some Windows terminal programs (e.g. Tera Term) may reboot the Facet when the terminal connection is closed. You can disconnect the USB cable first to prevent this from happening. + +![Terminal showing menu](img/Terminal/SparkFun_RTK_ExpressPlus_MainMenu.jpg) + +*Main Menu* + +Pressing any button will display the Main menu. The Main menu will display the current firmware version and the Bluetooth broadcast name. Note: When powered on, the RTK device will broadcast itself as either *[Platform] Rover-XXXX* or *[Platform] Base-XXXX* depending on which state it is in. The Platform is 'Facet', 'Express', 'Surveyor', etc. + +Pressing '1' or 's' for example, will open those submenus. + +The menus will timeout after 10 minutes of inactivity, so if you do not press a key the device will exit the menu and return to reporting status messages. + +![Configuration menu open over Bluetooth](img/Bluetooth/SparkFun%20RTK%20BEM%20-%20Exit%20BEM.png) + +*Configuration menu via Bluetooth* + +**Note:** Starting with firmware v3.0, Bluetooth-based configuration is supported. Please see [Configure With Bluetooth](configure_with_bluetooth.md) for more information. + +## System Report + +Sending the `~` character to the device over the serial port will trigger a system status report. This is a custom NMEA-style sentence, complete with CRC. + +![System status NMEA outputted to terminal](img/Terminal/SparkFun RTK System Status Trigger.png) + +*Terminal showing System Status* + +Below is an example system status report sentence: + +> $GNTXT,01,01,05,202447.00,270522,0.380,29,40.090355193,-105.184764700,1560.56,3,0,86*71 + +* $GNTXT : Start of custom NMEA sentence +* 01 : Number of sentences +* 01 : Sentence number +* 05 : Sentence type ID (5 is for System Status messages) +* 202447.00 : Current hour, minute, second, milliseconds +* 270522 : Current day, month, year +* 0.380 : Current horizontal positional accuracy (m) +* 29 : Satellites in view +* 40.090355193 : Latitude +* -105.184764700 : Longitude +* 1560.56 : Altitude +* 3 : Fix type (0 = no fix, 2 = 2D fix, 3 = 3D fix, 4 = 3D + Dead Reackoning, 5 = Time) +* 0 : Carrier solution (0 = No RTK, 1 = RTK Float, 2 = RTK Fix) +* 86 : Battery level (% remaining) +* *71 : The completion of the sentence and a [CRC](http://engineeringnotes.blogspot.com/2015/02/generate-crc-for-nmea-strings-arduino.html) + +**Note:** This is a custom NMEA sentence, can vary in length, and may exceed the [maximum permitted sentence length](https://www.nmea.org/Assets/20160520%20txt%20amendment.pdf) of 61 characters. + +## L-Band Performance During Serial Configuration + +Because of the way corrections are provided between the sub modules (NEO-D9S and ZED-F9P), the corrections will be interrupted while the configuration menu is open. RTK Fix may be lost if the menu is open for more than ~30s. RTK Fix will return once the configuration is complete and the menu is closed. + +Note: This only affects the RTK Facet L-Band model. \ No newline at end of file diff --git a/docs/configure_with_settings_file.md b/docs/configure_with_settings_file.md new file mode 100644 index 000000000..d9fbccbc1 --- /dev/null +++ b/docs/configure_with_settings_file.md @@ -0,0 +1,38 @@ +# Configure with Settings File + +Surveyor: ![Feature Partially Supported](img/Icons/YellowDot.png) / Express: ![Feature Partially Supported](img/Icons/YellowDot.png) / Express Plus: ![Feature Partially Supported](img/Icons/YellowDot.png) / Facet: ![Feature Partially Supported](img/Icons/YellowDot.png) / Facet L-Band: ![Feature Partially Supported](img/Icons/YellowDot.png) / Reference Station: ![Feature Partially Supported](img/Icons/YellowDot.png) + +![SparkFun RTK Facet Settings File](img/SparkFun_RTK_Express_-_Settings_File.jpg) + +*SparkFun RTK Settings File* + +All device settings are stored both in internal memory and an SD card if one is detected. The device will load the latest settings at each power on. If there is a discrepancy between the internal settings and a settings file then the settings file will be used. This allows a collection of RTK products to be identically configured using one 'golden' settings file loaded onto an SD card. + +All system configuration can be done by editing the *SFE_[Platform]_Settings_0.txt* file (example shown above) where [Platform] is Facet, Express, Surveyor, etc and 0 is the profile number (0, 1, 2, 3). This file is created when a microSD card is installed. The settings are clear text but there are no safety guards against setting illegal states. It is not recommended to use this method unless You Know What You're Doing®. + +Keep in mind: + +* The settings file contains hundreds of settings. +* The SD card file "SFE_Express_Settings_0.txt" is used for Profile 1, SD card file "SFE_Express_Settings_1.txt" is used for Profile 2, etc. (note that setting 0 is for profile 1, ...) +* When switching to a new profile, the settings file on the SD card with all settings will be created or updated. The internal settings will not be updated until you switch to the profile. Additionally, the file for a particular profile will not be created on the SD card until you switch to that profile. +* It is not necessary that the settings file on the SD card have all of the settings. + +For example, if you only wanted to set up two wireless networks for profile 2, you could create a file named "SFE_Express_Settings_1.txt" that only contained the following settings: + + profileName=a name you choose + enablePvtServer=1 + wifiNetwork0SSID=your SSID name 1 + wifiNetwork0Password=your SSID password 1 + wifiNetwork1SSID=your SSID name 2 + wifiNetwork1Password=your SSID password 2 + wifiConfigOverAP=0 + +These settings on the SD card will overwrite the settings in the RTK Express internal memory. Once you select this profile on your RTK Express, the SD card file will be overwritten with all of the merged settings. + +## Forcing a Factory Reset + +![Setting size of settings to -1 to force reset]() + +If the device has been configured into an unknown state the device can be reset to factory defaults. Power down the RTK device and remove the SD card. Using a computer and an SD card reader, open the SFE_[Platform]_Settings_0.txt file where [Platform] is Facet, Express, Surveyor, etc and 0 is the profile number (0, 1, 2, 3). Modify the **sizeOfSettings** value to -1 and save the file to the SD card. Reinsert the SD card into the RTK unit and power up the device. Upon power up, the device will display 'Factory Reset' while it clears the settings. + +Note: A device may have multiple profiles, ie multiple settings files (SFE_Express_Settings_**0**.txt, SFE_Express_Settings_**1**.txt, etc). All settings files found on the SD card must be modified to guarantee the factory reset. \ No newline at end of file diff --git a/docs/configure_with_ucenter.md b/docs/configure_with_ucenter.md new file mode 100644 index 000000000..6d1f3acb9 --- /dev/null +++ b/docs/configure_with_ucenter.md @@ -0,0 +1,7 @@ +# Configure with u-center + +Surveyor: ![Feature Partially Supported](img/Icons/YellowDot.png) / Express: ![Feature Partially Supported](img/Icons/YellowDot.png) / Express Plus: ![Feature Partially Supported](img/Icons/YellowDot.png) / Facet: ![Feature Partially Supported](img/Icons/YellowDot.png) / Facet L-Band: ![Feature Partially Supported](img/Icons/YellowDot.png) / Reference Station: ![Feature Partially Supported](img/Icons/YellowDot.png) + +The ZED-F9P module can be configured independently using the u-center software from u-blox by connecting a USB cable to the *Config u-blox* USB connector. Settings can be saved to the module between power cycles. For more information please see SparkFun’s [Getting Started with u-center by u-blox](https://learn.sparkfun.com/tutorials/getting-started-with-u-center-for-u-blox/all). + +However, because the ESP32 does considerable configuration of the ZED-F9P at power on it is not recommended to modify the settings of the ZED-F9P using u-center. Nothing will break but your changes will likely be overwritten at the next power cycle. diff --git a/docs/configure_with_wifi.md b/docs/configure_with_wifi.md new file mode 100644 index 000000000..9ae5207c0 --- /dev/null +++ b/docs/configure_with_wifi.md @@ -0,0 +1,71 @@ +# Configure with WiFi + +Surveyor: ![Feature Supported](img/Icons/GreenDot.png) / Express: ![Feature Supported](img/Icons/GreenDot.png) / Express Plus: ![Feature Supported](img/Icons/GreenDot.png) / Facet: ![Feature Supported](img/Icons/GreenDot.png) / Facet L-Band: ![Feature Supported](img/Icons/GreenDot.png) / Reference Station: ![Feature Supported](img/Icons/GreenDot.png) + +![WiFi configuration over AP](img/WiFi Config/SparkFun%20RTK%20Header%20Information.png) + +*Configuration page via WiFi* + +Starting with firmware v1.7, WiFi-based configuration is supported. For more information about updating the firmware on your device, please see [Updating RTK Firmware](firmware_update.md). + +The RTK device will present a webpage that is viewable from either a desktop/laptop with WiFi or a cell phone. For advanced configurations, a desktop is recommended. For quick in-field changes, a cell phone works great. + +![Desktop vs Phone display size configuration](img/WiFi Config/SparkFun_RTK_Facet_-_Desktop_vs_Phone_Config.jpg) + +*Desktop vs Phone display size configuration* + +## RTK Express / Express Plus / Facet +To get into WiFi configuration follow these steps: + +1. Power on the RTK Express, Express Plus, or Facet. +2. Once the device has started press the Setup button repeatedly until the *Config* menu is highlighted. +3. The display will blink a WiFi icon indicating it is waiting for incoming connections. +4. Connect to WiFi network named ‘RTK Config’. +5. Open a browser (Chrome is preferred) and type **192.168.4.1** into the address bar. + +![Display showing IP address](img/Displays/SparkFun_RTK_Facet_-_Display_WiFi_Config.jpg) + +*Device ready for cellphone configuration* + +## RTK Surveyor + +To get into WiFi configuration follow these steps: + +1. Power the RTK Surveyor on in Rover mode. +2. Once the device has started the Bluetooth status LED should be blinking at 1Hz. Now toggle the **SETUP** switch from Base and back to Rover within 1 second. If successful, the Bluetooth status LED will begin fading in/out. The device is now broadcasting as a WiFi access point. +3. Connect to WiFi network named ‘RTK Config’. +4. Open a browser (Chrome is preferred) and type **192.168.4.1** into the address bar. + +## Connecting to WiFi Network + +![Discovered WiFi networks](img/WiFi Config/RTK_Surveyor_-_WiFi_Config_-_Networks.jpg) + +*The WiFi network RTK Config as seen from a cellphone* + +Note: Upon connecting, your phone may warn you that this WiFi network has no internet. That's ok. Stay connected to the network and open a browser. If you still have problems turn off Mobile Data so that the phone does not default to cellular for internet connectivity and instead connects to the RTK Device. + +![Webpage showing the RTK Configuration options](img/WiFi Config/SparkFun_RTK_Facet_-_WiFi_Config_Main_Page.jpg) + +*Connected to the RTK WiFi Setup Page* + +Clicking on the category 'carrot' will open or close that section. Clicking on an ‘i’ will give you a brief description of the options within that section. + +![Firmware highlighted](img/WiFi Config/SparkFun_RTK_Facet_-_WiFi_Config_Main_Page_-_Firmware.jpg) + +*This unit has firmware version 1.8 and a ZED-F9P receiver* + +Please note that the firmware for the RTK device and the firmware for the ZED receiver is shown at the top of the page. This can be helpful when troubleshooting or requesting new features. + +## File Manager + +![List of files in file manager](img/WiFi Config/SparkFun%20RTK%20WiFi%20Config%20File%20Manager.png) + +Added in v3.0 firmware, a file manager is shown if an SD card is detected. This is a handy way to download files to a local device (cell phone or laptop) as well as delete any unneeded files. The SD size and free space are shown. And files may be uploaded to the SD card if needed. + +Additionally, clicking on the top checkbox will select all files for easy removal of a large number of files. + +## Saving and Exit + +![Save and Exit buttons](img/WiFi Config//RTK_Surveyor_-_WiFi_Config_-_System_Save_Exit.jpg) + +Once settings are input, please press ‘Save Configuration’. This will validate any settings, show any errors that need adjustment, and send the settings to the unit. The page will remain active until the user presses ‘Exit to Rover Mode’ at which point the unit will exit WiFi configuration and return to standard Rover mode. diff --git a/docs/connecting_bluetooth.md b/docs/connecting_bluetooth.md new file mode 100644 index 000000000..9ab99f9b3 --- /dev/null +++ b/docs/connecting_bluetooth.md @@ -0,0 +1,99 @@ +# Connecting Bluetooth + +Surveyor: ![Feature Supported](img/Icons/GreenDot.png) / Express: ![Feature Supported](img/Icons/GreenDot.png) / Express Plus: ![Feature Supported](img/Icons/GreenDot.png) / Facet: ![Feature Supported](img/Icons/GreenDot.png) / Facet L-Band: ![Feature Supported](img/Icons/GreenDot.png) / Reference Station: ![Feature Supported](img/Icons/GreenDot.png) + +SparkFun RTK products transmit full NMEA sentences over Bluetooth serial port profile (SPP) at 4Hz and 115200bps. This means that nearly any GIS application that can receive NMEA data over a serial port (almost all do) can be used with the RTK Express. As long as your device can open a serial port over Bluetooth (also known as SPP) your device can retrieve industry-standard NMEA positional data. The following steps show how to connect an external tablet, or cell phone to the RTK device so that any serial port-based GIS application can be used. + +## Android + +![Pairing with the RTK Express over Bluetooth]() + +*Pairing with the 'Express Rover-5556' over Bluetooth* + +Open Android's system settings and find the 'Bluetooth' or 'Connected devices' options. Scan for devices and pair with the device in the list that matches the Bluetooth MAC address on your RTK device. + +When powered on, the RTK product will broadcast itself as either '[Platform] Rover-5556' or '[Platform] Base-5556' depending on which state it is in. [Platform] is Facet, Express, Surveyor, etc. Discover and pair with this device from your phone or tablet. Once paired, open SW Maps. + +![Bluetooth MAC address B022 is shown in the upper left corner](img/Displays/SparkFun%20RTK%20Rover%20Display.png) + +*Bluetooth MAC address B022 is shown in the upper left corner* + +**Note:** *B022* is the last four digits of your unit's MAC address and will be unique to the device in front of you. This is helpful in case there are multiple RTK devices within Bluetooth range. + +### Enable Mock Location + +Most GIS applications will gracefully handle the Bluetooth connection to the RTK device and provide an NTRIP Client for getting the RTCM corrections so this section can be skipped. If, in the rare case, a GIS app does not allow NTRIP corrections, Mock Locations can be enabled under Android. Then a data provider like Lefebure or GNSS Master can be used to act as a middle-man. + +Before proceeding, it is recommended to have the mock location provider app already installed. So if you haven't already, consider installing [Lefebure](gis_software_android.md/#lefebure), [GNSS Master](gis_software_android.md/#gnss-master), etc. + +To enable **Mock Locations**, *Developer Mode* in Android must be enabled. It is best to google the [most recent procedure for this](https://www.google.com/search?q=how+to+allow+mock+location+on+android) but the following procedure should work: + +1) Open Android settings ![alt text]() + +2) Open *About phone* + + ![Build Number box]() + +3) Scroll to the bottom and click on *Build number* five or more times. The device will prompt as more taps are required. + +Once Developer Mode is enabled: + +1) Open Android settings ![alt text]() + +2) Open *System* + + ![Develop options menu]() + +3) Open *Developer options* + + ![Mock Location button]() + +4) Scroll all the way to the bottom of a very long list of developer options. + +5) Select the app to use for Mock Location. This is usually [Lefebure](gis_software_android.md/#lefebure) or [GNSS Master](gis_software_android.md/#gnss-master) but can be tailored as needed. + +## Apple iOS + +Please see [iOS GIS Software](gis_software_ios.md) for information about how to connect to individual GIS apps. Some require a BLE connection and some require a WiFi hotspot connection. + +More information is available on the [System Menu](menu_system.md) for switching between Bluetooth SPP and BLE. + +## Windows + +Open settings and navigate to Bluetooth. Click **Add device**. + +![Adding Bluetooth Device](img/Bluetooth/SparkFun%20RTK%20Software%20-%20Add%20Bluetooth%20Device.jpg) + +*Adding Bluetooth Device* + +Click Bluetooth 'Mice, Keyboards, ...' + +![Viewing available Bluetooth Devices](img/Bluetooth/SparkFun%20RTK%20Software%20-%20Add%20Bluetooth%20Device%202.jpg) + +*Viewing available Bluetooth Devices* + +Click on the RTK device. When powered on, the RTK product will broadcast itself as either '[Platform] Rover-5556' or '[Platform] Base-5556' depending on which state it is in. [Platform] is Facet, Express, Surveyor, etc. Discover and pair with this device from your phone or tablet. Once paired, open SW Maps. + +![Bluetooth MAC address B022 is shown in the upper left corner](img/Displays/SparkFun%20RTK%20Rover%20Display.png) + +*Bluetooth MAC address B022 is shown in the upper left corner* + +**Note:** *B022* is the last four digits of your unit's MAC address and will be unique to the device in front of you. This is helpful in case there are multiple RTK devices within Bluetooth range. + +![Bluetooth Connection Success](img/Bluetooth/SparkFun%20RTK%20Software%20-%20Add%20Bluetooth%20Device%203.jpg) + +*Bluetooth Connection Success* + +The device will begin pairing. After a few seconds, Windows should report that you are ready to go. + +![Bluetooth COM ports](img/Bluetooth/SparkFun%20RTK%20Software%20-%20Add%20Bluetooth%20Device%204.jpg) + +*Bluetooth COM ports* + +The device is now paired and a series of COM ports will be added under 'Device Manager'. + +![NMEA received over the Bluetooth COM port](img/Terminal/SparkFun%20RTK%20Software%20-%20Add%20Bluetooth%20Device%205.jpg) + +*NMEA received over the Bluetooth COM port* + +If necessary, you can open a terminal connection to one of the COM ports. Because the Bluetooth driver creates multiple COM ports, it's impossible to tell which is the serial stream so it's easiest to just try each port until you see a stream of NMEA sentences (shown above). You're all set! Be sure to close out the terminal window so that other software can use that COM port. \ No newline at end of file diff --git a/docs/contribute.md b/docs/contribute.md new file mode 100644 index 000000000..c7c68b37e --- /dev/null +++ b/docs/contribute.md @@ -0,0 +1,11 @@ +# Fix That Typo! + +All of this documentation can be modified by you! Please help us make it better. + +![Edit button on page]() + +*The edit button at the top of every page* + +Does something not make sense? Find a typo? Hit the edit button and make it better. If a section is confusing please [open an issue](https://github.com/sparkfun/SparkFun_RTK_Firmware/issues) and let us know. + +These pages are contained in the [docs folder](https://github.com/sparkfun/SparkFun_RTK_Firmware/tree/main/docs) of the [SparkFun RTK Firmware](https://github.com/sparkfun/SparkFun_RTK_Firmware) repository. Fork this repo, make changes to the markdown, then create a pull request with your changes, and enjoy making the ~~words~~ ~~worlds~~ world a better place. \ No newline at end of file diff --git a/docs/correction_sources.md b/docs/correction_sources.md new file mode 100644 index 000000000..4df49316b --- /dev/null +++ b/docs/correction_sources.md @@ -0,0 +1,129 @@ +# Correction Sources + +Surveyor: ![Feature Supported](img/Icons/GreenDot.png) / Express: ![Feature Supported](img/Icons/GreenDot.png) / Express Plus: ![Feature Not Supported](img/Icons/GreenDot.png) / Facet: ![Feature Supported](img/Icons/GreenDot.png) / Facet L-Band: ![Feature Supported](img/Icons/YellowDot.png) / Reference Station: ![Feature Supported](img/Icons/GreenDot.png) + +To achieve an RTK Fix, SparkFun RTK products must be provided with a correction source. This correction data, sometimes called RTCM (see [What is RTCM?](https://learn.sparkfun.com/tutorials/what-is-gps-rtk/all#what-is-rtcm)), can be produced from a variety of sources. + +* [Paid Services](correction_sources.md#paid-services) +* [Government Provided Corrections](correction_sources.md#government-provided-corrections) +* [Permanent Base](correction_sources.md#permanent-base) + +**Note:** The RTK Facet L-Band is capable of receiving RTCM corrections from a terrestrial source but because it has a built-in L-Band receiver, we recommend using the satellite-based corrections. + +## Paid Services + +These services cover entire countries and regions but charge a monthly fee. Easy to use, but the most expensive. + +* [PointOneNav](https://app.pointonenav.com/trial?src=sparkfun) ($50/month) - US, EU +* [Onocoy](https://console.onocoy.com/explorer) ($25/month) - US, EU, Australia, and many other partial areas +* [Skylark](https://www.swiftnav.com/skylark) ($29 to $69/month) - US, EU, Japan, Australia +* [SensorCloud RTK](https://rtk.sensorcloud.com/pricing/) ($100/month) partial US, EU +* [Premium Positioning](https://www.premium-positioning.com) (~$315/month) partial EU +* [KeyNetGPS](https://www.keypre.com/KeynetGPS) ($375/month) North Eastern US +* [Hexagon/Leica](https://hxgnsmartnet.com/en-US) ($500/month) - partial US, EU + +Using PointOneNav is discussed in the [Quick Start guide](https://docs.sparkfun.com/SparkFun_RTK_Firmware/intro/#ntrip-example). We'll discuss using Skylark below. All services have the same basic interface: as long as the service has NTRIP, the SparkFun RTK product can use it. + +**Skylark** + +![Skylark coverage area](img/Corrections/Skylark-Coverage.png) + +*Skylark Coverage Area* + +A company called SwiftNav offers a service called [Skylark](https://www.swiftnav.com/skylark). As of writing, for $29 to $69 per month, you will get corrections covering North America, Europe, and the Asia Pacific. This is a very simple method for obtaining NTRIP corrections. + +![Skylark website showing credentials](img/Corrections/SparkFun%20NTRIP%20Skylark%201%20-%20Credentials.png) + +Upon creating an account, you'll be issued NTRIP credentials that can immediately be used with Lefebure, SW Maps, or any GIS app that supports NTRIP. + +![Entering credentials into SW maps](img/SWMaps/SparkFun%20NTRIP%20Skylark%202%20-%20SW%20Maps%20Credentials.png) + +*Entering credentials into SW maps* + +The most difficult part of using Skylark for corrections is entering the auto-generated NTRIP Password. While we understand security is important, it's not trivial manually entering these types of credentials into a GIS application. + +![GNSS Status showing positional accuracy](img/SWMaps/SparkFun%20NTRIP%20Skylark%202%20-%20SW%20Maps%20HPA.png) + +*SW Maps showing Positional Accuracy* + +One downside is that with a 'regional' provider such as Skylark the distance to the correction station may be larger than 10km. While we've always gotten an RTK fix, we often see horizontal positional accuracy of ~30mm instead of the 14mm when using our fixed GNSS reference station. Your mileage may vary. + +**PointPerfect** + +![PointPerfacet Coverage Map](img/Corrections/SparkFun_RTK_Facet_L-Band_Coverage_Area.jpg) + +PointPerfect is a correction service run by u-blox. The service runs about $44 per month and covers the contiguous USA. Unfortunately, it does not have NTRIP access at the time of writing. Instead, they use an API and encrypted packets in a format called SPARTN. SparkFun uses the PointPerfect service to provide satellite-based corrections to the [RTK Facet L-Band](https://www.sparkfun.com/products/20000). This service works very well for the RTK Facet L-Band, but because no 3rd party GIS software is known to exist that can communicate with PointPerfect, we don't currently recommend using PointPerfect with SW Maps, Lefebure, Field Genius, SurvPC, Survey Master, etc. + +## Government Provided Corrections + +![Wisconsin network of CORS]() + +*State Wide Network of Continuously Operating Reference Stations (CORS)* + +Be sure to check if your state or country provides corrections for free. Many do! Currently, there are 21 states in the USA that provide this for free as a department of transportation service. Search ‘Wisconsin CORS’ as an example. Similarly, in France, check out [CentipedeRTK](https://docs.centipede.fr/). + +[![UNAVO map](img/Corrections/SparkFun%20NTRIP%204%20-%20UNAVCO%20Map.png)](https://www.unavco.org/instrumentation/networks/status/all/realtime) + +[UNAVCO](https://www.unavco.org/) is a US-based governmental organization that runs a [network of publicly available NTRIP sources](https://www.unavco.org/instrumentation/networks/status/all/realtime). If you're lucky there's a station within 10km (6 miles) of you. + +![Map of European stations](img/Corrections/SparkFun%20NTRIP%206%20-%20EUREF%20Map.png) + +[EUREF](http://www.epncb.oma.be/_networkdata/data_access/real_time/map.php) is a permanent GNSS network in the EU. + +There are several public networks across the globe, be sure to google around! + +## Permanent Base + +![SparkFun Base Station Enclosure](img/Corrections/Roof_Enclosure.jpg) + +*The base station at SparkFun* + +A permanent base is a user-owned and operated base station. See [Creating a Permanent Base](permanent_base.md) for more information. A permanent base has the benefit of being the most accurate, with relatively low cost, but requires at least 24 hours of initial logging and some mechanical setup time (attaching the antenna, connecting a device to the internet, etc). + +## Temporary Base + +[![Temporary RTK Express Base setup](img/Corrections/SparkFun_RTK_Express_-_Base_Radio.jpg)](img/Corrections/SparkFun_RTK_Express_-_Base_Radio - Big.jpg) + +*Temporary RTK Express Base setup with serial radio* + +A temporary or mobile base setup is handy when you are in the field too far away from a correction source, or if your measurements do not require absolute accuracy. + +To set up a temporary base, a 2nd RTK device is mounted to a tripod and it is configured to complete a survey-in (aka, locate itself). It will then begin broadcasting RTCM correction data. This data (~1000 bytes a second) is sent over a data link to one or multiple rovers that can then obtain RTK Fix. + +Any tripod with a ¼” camera thread will work. The [Amazon Basics tripod](https://www.amazon.com/AmazonBasics-Lightweight-Camera-Mount-Tripod/dp/B00XI87KV8) works well enough but is a bit lightweight and rickety. + +For RTK products with an external antenna (ie, RTK Surveyor, RTK Express, RTK Express Plus) a cell phone holder is clamped to the tripod and the RTK device is held in the clamp. The ¼” camera thread is [adapted to ⅝” 11-TPI](https://www.sparkfun.com/products/17546) and an [L1/L2 antenna](https://www.sparkfun.com/products/17751) is attached. A [Male TNC to Male SMA adapter](https://www.sparkfun.com/products/17833) connects the antenna to the RTK device. + +Any of the RTK Products (excluding the RTK Express Plus) can be set up to operate in **Base** mode. Once the base has been set up with a clear view of the sky, turn on the RTK device. + +On the RTK Surveyor, toggle the *Setup* switch to **BASE**. The device will then enter either 'Fixed' or 'Survey-In' type **Base** mode depending on the system configuration. If the type has been set to Survey-In, the red BASE LED will blink while a survey-in is active. Once complete, the LED will turn solid red and begin transmitting RTCM out the **RADIO** port. + +![RTK Facet in Survey-In Mode](img/Displays/SparkFun_RTK_Express_-_Display_-_Survey-In.jpg) + +*RTK device in Survey-In Mode* + +On the RTK Facet, RTK Facet L-Band, and RTK Express press the **SETUP** button until *Base* is illuminated then stop pressing the Setup button. The device will then enter either 'Fixed' or 'Survey-In' type **Base** mode depending on the system configuration. If the type has been set to Survey-In, the display will show the Survey-In screen. + +*Note:* Base mode is not possible on the RTK Express Plus. + +![External Serial Radio attached to the back of the RTK Express](img/Corrections/SparkFun_RTK_Surveyor_-_Radio.jpg) + +*External Serial Radio attached to the back of the RTK Express* + +Once the survey is complete the device will begin producing RTCM correction data. If you are using a serial radio, data should start flowing across the link. RTK devices are designed to follow the u-blox recommended survey-in of 60 seconds and a mean 3D standard deviation of 5m of all fixes. If a survey fails to achieve these requirements it will auto-restart after 10 minutes. + +More expensive surveyor bases have a ⅝” 11-TPI thread but the top of the surveyor base will often interfere with the antenna’s TNC connector. If you chose to use a surveyor’s ‘stick’ (often called a Prism Pole) be sure to obtain an extension to raise the antenna at least an inch. + +If you’re shopping for a cell phone clamp be sure to get one that is compatible with the diameter of your tripod and has a knob to increase clamp pressure. Our tripod is 18mm in diameter and we’ve had a good experience with [this clamp](https://www.amazon.com/gp/product/B072DSRF3J). Your mileage may vary. + +Note: A mobile base station works well for quick trips to the field. However, the survey-in method is not recommended for the highest accuracy measurements because the positional accuracy of the base will directly translate to the accuracy of the rover. Said differently, if your base's calculated position is off by 100cm, so will every reading your rover makes. For many applications, such as surveying, this is acceptable since the Surveyor may only be concerned with measuring the lengths of property lines or features. If you’re looking for maximum accuracy consider installing a [permanent static base with a fixed antenna](permanent_base.md). We were able to pinpoint the antenna on the top of SparkFun with an incredible accuracy [+/-2mm of accuracy](img/Corrections/SparkFun_PPP_Results.png) using PPP! + + + +## Other Sources + +There are a large number of networks run throughout the world. Be sure to dig a bit to find a local correction source near you. + +[![Map of RTK2Go Stations](img/Corrections/SparkFun%20NTRIP%205%20-%20RTK2Go%20Map.png)](http://monitor.use-snip.com/?hostUrl=rtk2go.com&port=2101) + +* RTK2go offers a [list](http://monitor.use-snip.com/?hostUrl=rtk2go.com&port=2101) and map (click 'View all' from the list) of stations using their public casting service. While none of these stations have been verified as accurate, it can be a decent starting point to do a 'quick' test of your system. \ No newline at end of file diff --git a/docs/correction_transport.md b/docs/correction_transport.md new file mode 100644 index 000000000..db2f36282 --- /dev/null +++ b/docs/correction_transport.md @@ -0,0 +1,67 @@ +# Correction Transport + +Surveyor: ![Feature Supported](img/Icons/GreenDot.png) / Express: ![Feature Supported](img/Icons/GreenDot.png) / Express Plus: ![Feature Not Supported](img/Icons/GreenDot.png) / Facet: ![Feature Supported](img/Icons/GreenDot.png) / Facet L-Band: ![Feature Supported](img/Icons/YellowDot.png) / Reference Station: ![Feature Supported](img/Icons/GreenDot.png) + +Once a [correction source](correction_sources.md) is chosen, the correction data must be transported from the base to the rover. The RTCM serial data is approximately 530 bytes per second and is transmitted at 57600bps out of the **RADIO** port on a SparkFun RTK device. + +There are a variety of ways to move data from a base to a rover. We will cover the most common below. + +Note: RTK calculations require RTCM data to be delivered approximately once per second. If RTCM data is lost or not received by a rover, RTK Fix can still be maintained for many seconds before the device will enter RTK Float mode. This is beneficial where devices like Serial Radios may drop packets due to RF congestion. + +**Note:** The RTK Facet L-Band is capable of receiving RTCM corrections from a terrestrial source but because it has a built-in L-Band receiver, we recommend using the satellite-based corrections. + +## WiFi + +![NTRIP Server setup](img/WiFi Config/RTK_Surveyor_-_WiFi_Config_-_Base_Config2.jpg) + +Any SparkFun RTK device can be set up as an [NTRIP Server](menu_base.md#ntrip-server). This means the device will connect to local WiFi and broadcast its correction data to the internet. The data is delivered to something called an NTRIP Caster. Any number of rovers can then access this data using something called an NTRIP Client. Nearly *every* GIS application has an NTRIP Client built into it so this makes it very handy. + +WiFi broadcasting is the most common transport method of getting RTCM correction data to the internet and to rovers via NTRIP Clients. + +![RTK product in NTRIP Client mode](img/Displays/SparkFun_RTK_Rover_NTRIP_Client_Connection.png) + +Similarly, any SparkFun RTK device can be set up as an [NTRIP Client](menu_gnss.md#ntrip-client). The RTK device will connect to the local WiFi and begin downloading the RTCM data from the given NTRIP Caster and RTK Fix will be achieved. This is useful only if the Rover remains in RF range of the WiFi access point. Because of the limited range, we recommend using a cellphone rather than WiFi for NTRIP Clients. + +## Cellular + +![SW Maps NTRIP Client](img/SWMaps/SW_Maps_-_NTRIP_Client.jpg) + +Using a cellphone is the most common way of transporting correction data from the internet to a rover. This method uses the cell phone's built-in internet connection to obtain data from an NTRIP Caster and then pass those corrections over Bluetooth to the RTK device. + +Shown above are SW Map's NTRIP Client Settings. Nearly all GIS applications have an NTRIP Client built in so we recommend leveraging the device you already own to save money. Additionally, a cell phone gives your rover incredible range: a rover can obtain RTCM corrections anywhere there is cellular coverage. + +Cellular can even be used in Base mode. We have seen some very inventive users use an old cell phone as a WiFi access point. The base unit is configured as an NTRIP Server with the cellphone's WiFi AP credentials. The base performs a survey-in, connects to the WiFi, and the RTCM data is pushed over WiFi, over cellular, to an NTRIP Caster. + +## L-Band + +What if you are in the field, far away from WiFi, cellular, radio, or any other data connection? Look to the sky! + +A variety of companies provide GNSS RTK corrections broadcast from satellites over a spectrum called L-Band. [L-Band](https://en.wikipedia.org/wiki/L_band) is any frequency from 1 to 2 GHz. These frequencies have the ability to penetrate clouds, fog, and other natural weather phenomena making them particularly useful for location applications. + +These corrections are not as accurate as a fixed base station, and the corrections can require a monthly subscription fee, but you cannot beat the ease of use! + +L-Band reception requires specialized RF receivers capable of demodulating the satellite transmissions. Currently, the [RTK Facet L-Band](https://www.sparkfun.com/products/20000) is the only product that supports L-Band correction reception. + +## Serial Radios + +![Two serial radios](img/Corrections/19032-SiK_Telemetry_Radio_V3_-_915MHz__100mW-01.jpg) + +Serial radios, sometimes called telemetry radios, provide what is essentially a serial cable between the base and rover devices. Transmission distance, frequency, maximum data rate, configurability, and price vary widely, but all behave functionally the same. SparkFun recommends the [HolyBro 100mW](https://www.sparkfun.com/products/19032) and the [SparkFun LoRaSerial 1W](https://www.sparkfun.com/products/19311) radios for RTK use. + +![Serial radio cable](img/Corrections/17239-GHR-04V-S_to_GHR-06V-S_Cable_-_150mm-01.jpg) + +All SparkFun RTK products include a [4-pin to 6-pin cable](https://www.sparkfun.com/products/17239) that will allow you to connect the HolyBro branded radio or the SparkFun LoRaSerial radios to a base and rover RTK device. + +![Radio attached to RTK device](img/Corrections/SparkFun_RTK_Surveyor_-_Radio.jpg) + +These radios attach nicely to the back or bottom of an RTK device. + +The benefit of a serial telemetry radio link is that you do not need to configure anything; simply plug two radios onto two RTK devices and turn them on. + +The downside to serial telemetry radios is that they generally have a much shorter range (often slightly more than a 1-kilometer functional range) than a cellular link can provide. + +## Ethernet + +The Reference Station send and receive correction data via Ethernet. (Note: it cannot currently send or receive correction data via WiFi) + +Please see [Ethernet Menu](menu_ethernet.md) for more details. \ No newline at end of file diff --git a/docs/displays.md b/docs/displays.md new file mode 100644 index 000000000..922977b79 --- /dev/null +++ b/docs/displays.md @@ -0,0 +1,117 @@ +# Displays + +Surveyor: ![Feature Partially Supported](img/Icons/YellowDot.png) / Express: ![Feature Supported](img/Icons/GreenDot.png) / Express Plus: ![Feature Supported](img/Icons/GreenDot.png) / Facet: ![Feature Supported](img/Icons/GreenDot.png) / Facet L-Band: ![Feature Supported](img/Icons/GreenDot.png) / Reference Station: ![Feature Supported](img/Icons/GreenDot.png) + +The RTK Facet, Facet L-Band, Express, and Express Plus utilize a 0.96" high-contrast OLED display. While small, it packs various situational data that can be helpful in the field. We will walk you through each display. + +## Power On/Off + +![Startup and Shutdown Screens](img/Displays/SparkFun_RTK_Facet_-_Display_On_Off.jpg) + +*RTK Facet Startup and Shutdown Screens* + +Press and hold the power button until the display illuminates to turn on the device. Similarly, press and hold the power button to turn off the device. + +The device's firmware version is shown during the Power On display. + +### Force Power Off + +In the event that a device becomes unresponsive, the device can be completely powered off by holding the power button for 10 seconds or more. The force-power-off method is hardware-based and will therefore work regardless of what firmware the device may be running. + +![Multiple COM ports shown](img/Serial/SparkFun_RTK_Facet_-_Multiple_COM_Ports.jpg) + +If the power state of a device is not known (for example, because a display may be malfunctioning) the device can be connected to USB. If one or more COM ports enumerate, the device is on (shown above). If no COM ports are seen, the device is fully powered off. + +## Rover Fix + +![Rover with location fix](img/Displays/SparkFun_RTK_Facet_-_Main_Display_Icons.jpg) + +*Rover with location fix* + +Upon power up the device will enter either Rover mode or Base mode. Above, the Rover mode is displayed. + +* **MAC:** The MAC address of the internal Bluetooth module. This is helpful knowledge when attempting to connect to the device from your phone. This will change to a Bluetooth symbol once connected. +* **HPA:** Horizontal positional accuracy is an estimate of how accurate the current positional readings are. This number will decrease rapidly after the first power-up and settle around 0.3m depending on your antenna and view of the sky. When RTK fix is achieved this icon will change to a double circle and the HPA number will decrease even further to as low as 0.014m. +* **SIV:** Satellites in view is the number of satellites used for the fix calculation. This symbol will blink before a location fix is generated and become solid when the device has a good location fix. SIV is a good indicator of how good of a view the antenna has. This number will vary but anything above 10 is adequate. We've seen as high as 31. +* **Model:** This icon will change depending on the selected dynamic model: Portable (default) Pedestrian, Sea, Bike, Stationary, etc. +* **Log:** This icon will remain animated while the log file is increasing. This is a good visual indication that you have an SD card inserted and RTK Facet can successfully record to it. There are three logging icons ![Logging icons](img/Displays/SparkFun%20RTK%20Logging%20Types.png) + * Standard (three lines) is shown when the standard 5 NMEA messages are being logged + * PPP (capital P) is shown when the standard 5 NMEA + RAWX and SFRBX messages are recorded. This is most often used for post process positioning (PPP) and 12 to 24-hour logs for [fixed permanent bases](permanent_base.md). + * Custom (capital C) is shown when a custom set of messages are being recorded (not standard, and not PPP). + +## Rover RTK Fix + +![Rover with RTK Fix and Bluetooth connected](img/Displays/SparkFun_RTK_Express_-_Display_-_Rover_RTK_Fixed.jpg) + +*Rover with RTK Fix and Bluetooth connected* + +Once NTRIP is enabled on your phone or RTCM data is being streamed into the **Radio** port the device will gain an RTK Fix. You should see the HPA drop to 14mm with a double circle bulls-eye as shown above. + +## Base Survey-In + +![RTK Facet in Survey-In Mode](img/Displays/SparkFun_RTK_Express_-_Display_-_Survey-In.jpg) + +*RTK device in Survey-In Mode* + +Pressing the Setup button will change the device to Base mode. If the device is configured for *Survey-In* base mode, a flag icon will be shown and the survey will begin. The mean standard deviation will be shown as well as the time elapsed. For most Survey-In setups, the survey will complete when both 60 seconds have elapsed *and* a mean of 5m or less is obtained. + +## Base Transmitting + +![RTK Facet in Fixed Transmit Mode](img/Displays/SparkFun_RTK_Express_-_Display_-_FixedBase-Xmitting.jpg) + +*RTK Facet in Fixed Transmit Mode* + +Once the *survey-in* is complete the device enters RTCM Transmit mode. The number of RTCM transmissions is displayed. By default, this is one per second. + +The *Fixed Base* mode is similar but uses a structure icon (shown above) to indicate a fixed base. + +## Base Transmitting NTRIP + +If the NTRIP server is enabled the device will first attempt to connect over WiFi. The WiFi icon will blink until a WiFi connection is obtained. If the WiFi icon continually blinks be sure to check your SSID and PW for the local WiFi. + +![RTK Facet in Transmit Mode with NTRIP](img/Displays/SparkFun_RTK_Express_-_Display_-_FixedBase-Casting.jpg) + +*RTK Facet in Transmit Mode with NTRIP* + + +Once WiFi connects the device will attempt to connect to the NTRIP mount point. Once successful the display will show 'Casting' along with a solid WiFi icon. The number of successful RTCM transmissions will increase every second. + +Note: During NTRIP transmission WiFi is turned on and Bluetooth is turned off. You should not need to know the location information of the base so Bluetooth should not be needed. If necessary, USB can be connected to the USB port to view detailed location and ZED-F9P configuration information. + +## L-Band + +L-Band decryption keys are valid for a maximum of 56 days. During that time, the RTK Facet L-Band can operate normally without the need for WiFi access. However, when the keys are set to expire in 28 days or less, the RTK Facet L-Band will attempt to log in to the 'Home' WiFi at each power on. If WiFi is not available, it will continue normal operation. + +![Display showing 14 days until L-Band Keys Expire](img/Displays/SparkFun_RTK_LBand_DayToExpire.jpg) + +*Display showing 14 days until L-Band Keys Expire* + +The unit will display various messages to aid the user in obtaining keys as needed. + +![Three-pronged satellite dish indicating L-Band reception](img/Displays/SparkFun_RTK_LBand_Indicator.jpg) + +*Three-pronged satellite dish indicating L-Band reception* + +Upon successful reception and decryption of L-Band corrections, the satellite dish icon will increase to a three-pronged icon. As the unit's fix increases the cross-hair will indicate a basic 3D solution, a double blinking cross-hair will indicate a floating RTK solution, and a solid double cross-hair will indicate a fixed RTK solution. + +## Reference Station + +The Reference Station is able to detect an open circuit or a short circuit on the GNSS antenna connection. + +![Reference Station indicating antenna open circuit](img/Displays/Antenna_Open.png) + +*Reference Station with the GNSS antenna disconnected (open circuit)* + +![Reference Station indicating antenna short circuit](img/Displays/Antenna_Short.png) + +*Reference Station with a GNSS antenna cable fault (short circuit)* + +When the Reference Station is in Network Time Protocol (NTP) mode, the display also shows a clock symbol - as shown above. +The value next to the clock symbol is the Time Accuracy Estimate (tAcc) from the UBX-NAV-PVT message. + +Note: tAcc is the time accuracy estimate for the navigation position solution. The timing accuracy of the TP pulse is significantly better than this. +We show the tAcc as we believe it is more meaningful than the TIM-TP time pulse quantization error (qErr) - which is generally zero. + +## Adding a Display to the RTK Surveyor + +While the RTK Surveyor works very well using only LEDs, it is possible to add an external display. The [SparkFun Micro OLED Breakout (Qwiic)](https://www.sparkfun.com/products/14532) can be attached to the Qwiic connector on the end of the Surveyor. At power on, the display will be automatically detected and used. \ No newline at end of file diff --git a/docs/embeddedsystem_connection.md b/docs/embeddedsystem_connection.md new file mode 100644 index 000000000..27816bf82 --- /dev/null +++ b/docs/embeddedsystem_connection.md @@ -0,0 +1,44 @@ +# Output to an Embedded System + +Surveyor: ![Feature Supported](img/Icons/GreenDot.png) / Express: ![Feature Supported](img/Icons/GreenDot.png) / Express Plus: ![Feature Supported](img/Icons/GreenDot.png) / Facet: ![Feature Supported](img/Icons/GreenDot.png) / Facet L-Band: ![Feature Supported](img/Icons/GreenDot.png) / Reference Station: ![Feature Supported](img/Icons/GreenDot.png) + +Many applications using the RTK products will use a 3rd party GIS application or mobile app like SW Maps and receive the data over Bluetooth. Alternatively, for embedded applications, a user can obtain the NMEA data over serial directly. + +For this example, we will connect the output from the **Data** port to a [USB to Serial adapter](https://www.sparkfun.com/products/15096) so that we can view the serial data over a terminal connection. + +The **Data** port on the RTK Facet, Express, and Express Plus can be configured to output a variety of different signals including NMEA Serial data. Be sure to check out the [Ports Menu](menu_ports.md) section to be sure your device is configured to output NMEA. + +Connect the included [4-pin JST to breadboard cable](https://www.sparkfun.com/products/17240) to the **Data** port. The cable has the following pinout: + +* **Red** - 3.3V +* **Green** - TX (output from the RTK device) +* **Orange** - RX (input into the RTK device) +* **Black** - GND + +![Wires connected to a SparkFun USB C to Serial adapter](img/SparkFun_RTK_Facet_-_Data_Port_to_USB.jpg) + +![Wires connected to a SparkFun USB C to Serial adapter](img/SparkFun_RTK_Express_-_Data_Port_USB.jpg) + +![Wires connected to a SparkFun USB C to Serial adapter](img/SparkFun_RTK_Surveyor_-_Data_Port_HiRes.jpg) + +[Open a terminal](https://learn.sparkfun.com/tutorials/terminal-basics) at 115200bps and you should see NMEA sentences: + +![NMEA output from the RTK Surveyor](img/Terminal/SparkFun_RTK_Surveyor_-_Data_Output.jpg) + +The Data connector on all RTK products is a 4-pin locking 1.25mm JST SMD connector (part#: SM04B-GHS-TB, mating connector part#: GHR-04V-S). **3.3V** is provided by this connector to power a remote device if needed. While the port is capable of sourcing up to 600mA, we do not recommend more than 300mA. This port should not be connected to a power source, so if your embedded device has its own power do not connect the red wire. + +**Warning!** All data in and out of RTK products is **3.3V**. Exposing these pins to **5V** or higher voltage logic will damage the device. + +The parsing of NMEA sentences is straightforward and left to the reader. There are ample NMEA parsing libraries available in C++, Arduino, Python, and many more languages. + +## Reference Station + +The Reference Station provides direct access to the u-blox GNSS TX and RX signals via the 3.5mm screw terminal I/O header: + +![Reference Station I/O screw terminals](img/SparkFun_GNSS_RTK_Reference_Station_IO.jpg) + +* TX2 : u-blox ZED-F9P UART2 transmit: 3.3V OUTPUT + +* RX2 : u-blox ZED-F9P UART2 receive: 3.3V INPUT + +Please see the [Reference Station Hookup Guide](https://learn.sparkfun.com/tutorials/sparkfun-rtk-reference-station-hookup-guide#hardware-overview) for more details. \ No newline at end of file diff --git a/docs/firmware_update.md b/docs/firmware_update.md new file mode 100644 index 000000000..0f8175bfd --- /dev/null +++ b/docs/firmware_update.md @@ -0,0 +1,619 @@ +# Updating RTK Firmware + +Surveyor: ![Feature Supported](img/Icons/GreenDot.png) / Express: ![Feature Supported](img/Icons/GreenDot.png) / Express Plus: ![Feature Supported](img/Icons/GreenDot.png) / Facet: ![Feature Supported](img/Icons/GreenDot.png) / Facet L-Band: ![Feature Supported](img/Icons/GreenDot.png) / Reference Station: ![Feature Supported](img/Icons/GreenDot.png) + +The device has two primary firmwares: + +* Firmware on the ESP32 microcontroller. Keep reading. +* Firmware on the u-blox ZED-F9P, ZED-F9P, or NEO-D9S Receiver. [See below](firmware_update.md#updating-u-blox-firmware). + +The device firmware is displayed in a variety of places: + +* Power On +* Serial Config Menu +* WiFi Config + +![RTK Express with firmware v3.0](img/Displays/SparkFun%20RTK%20Boot%20Screen%20Version%20Number.png) + +*RTK Express with firmware v3.0* + +During power-on, the display will show the current device Firmware. + +![Main Menu showing RTK Firmware v3.0-Jan 19 2023](img/Terminal/SparkFun%20RTK%20Main%20Menu.png) + +*Main Menu showing RTK Firmware v3.0-Jan 19 2023* + +The firmware is displayed when the main menu is opened over a serial connection. + +![WiFi Config page showing device firmware v2.7](img/WiFi Config/SparkFun%20RTK%20WiFi%20Config%20Screen%20Version%20Number.png) + +*WiFi Config page showing device firmware v2.7 and ZED-F9P firmware HPG 1.32* + +The firmware is shown at the top of the WiFi config page. + +From time to time SparkFun will release new firmware for the RTK product line to add and improve functionality. For most users, firmware can be upgraded over WiFi using the OTA method. + +* [OTA Method](firmware_update.md#updating-firmware-over-the-air): Connect over WiFi to SparkFun to download the latest firmware *over-the-air*. This can be done using the serial menu or while in WiFi AP Config Mode. Requires a local WiFi network. +* [GUI Method](firmware_update.md#updating-firmware-using-windows-gui): Use the [Windows, Linux, MacOS or Python GUI](https://github.com/sparkfun/SparkFun_RTK_Firmware_Uploader) and a USB cable. (The Python package has been tested on Raspberry Pi) +* [SD Method](firmware_update.md#updating-firmware-from-the-sd-card): Load the firmware on an SD card, then use a serial terminal with the *Firmware Upgrade* menu +* [WiFi Method](firmware_update.md#updating-firmware-from-wifi): Load the firmware over WiFi when the device is in WiFi AP Config Mode +* [CLI Method](firmware_update.md#updating-firmware-from-cli): Use the command line *batch_program.bat* + +The OTA method is generally recommended. For more information see [here](firmware_update.md#updating-firmware-over-the-air). + +Remember, all SparkFun RTK devices are open source hardware meaning you have total access to the [firmware](https://github.com/sparkfun/SparkFun_RTK_Firmware) and [hardware](https://github.com/sparkfun/SparkFun_RTK_Facet). Be sure to check out each repo for the latest firmware and hardware information. + +## Updating Firmware Over-The-Air +![Updating Firmware from WiFi config page]() + +*Updating the firmware via WiFi config page* + +![Updating the firmware via Firmware serial menu](img/Terminal/SparkFun%20RTK%20Firmware%20Update%20Menu.png) + +*Updating the firmware via Firmware serial menu* + +Introduced with version 3.0, firmware can be updated by pressing a button in the System Configuration section of the WiFi Config page, or over the Firmware menu of the serial interface. This makes checking and upgrading a unit very easy. + +Additionally, users may opt into checking for Beta firmware. This is the latest firmware that may have new features and is meant for testing. Beta firmware is not recommended for units deployed into the field as it may not be stable. + +If you have a device with firmware lower than v3.0, you will need to use the [GUI](firmware_update.md#updating-firmware-using-the-uploader-gui) or a method listed below to get to v3.x. + +With version 3.10 automatic release firmware update is supported over WiFi. Enabling this feature is done using the serial firmware menu. The polling period is speified in minutes and defaults to once a day. The automatic firmware update only checks for and installs the current SparkFun released firmware versions over top of any: + +* Older released versions (continual upgrade) +* Beta firmware versions (newer or older, restore to released version) +* Locally built versions (newer or older, restore to released version) + +## Updating Firmware Using The Uploader GUI + +![RTK Firmware GUI](img/RTK_Uploader_Windows.png) + +*RTK Firmware GUI* + +This GUI makes it easy to point and click your way through a firmware update. There are versions for Windows, Linux, MacOS and a Python package installer. + +The latest GUI release can be downloaded [here](https://github.com/sparkfun/SparkFun_RTK_Firmware_Uploader/releases). + +Download the latest RTK firmware binary file located on the [releases page](https://github.com/sparkfun/SparkFun_RTK_Firmware/releases) or from the [binaries repo](https://github.com/sparkfun/SparkFun_RTK_Firmware_Binaries). + +**To Use** + +* Attach the RTK device to your computer using a USB cable. +* Turn the RTK device on. +* On Windows, open the Device Manager to confirm which COM port the device is operating on. On other platforms, check ```/dev```. + +![Device Manager showing USB-Serial CH340 port on COM27](img/Serial/SparkFun%20RTK%20Firmware%20Uploader%20COM%20Port.jpg) + +*Device Manager showing 'USB-Serial CH340' port on COM27* + +* Get the latest binary file located on the [releases page](https://github.com/sparkfun/SparkFun_RTK_Firmware/releases) or from the [binaries repo](https://github.com/sparkfun/SparkFun_RTK_Firmware_Binaries). +* Run *RTKUploader.exe* (it takes a few seconds to start) +* Click *Browse* and select the binary file to upload +* Select the COM port previously seen in the Device Manager +* Click *Upload Firmware* + +Once complete, the device will reset and power down. + +If your RTK 'freezes' after the update, press ```Reset ESP32``` to get it going again. + +## Updating Firmware From the SD Card + +![Firmware update menu](img/Terminal/SparkFun_RTK_Firmware_Update-ProgressBar.jpg) + +*Firmware update taking place* + +Download the latest binary file located on the [releases page](https://github.com/sparkfun/SparkFun_RTK_Firmware/releases) or from the [binaries repo](https://github.com/sparkfun/SparkFun_RTK_Firmware_Binaries). + +The firmware upgrade menu will only display files that have the "RTK_Surveyor_Firmware*.bin" file name format so don't change the file names once loaded onto the SD card. Select the firmware you'd like to load and the system will proceed to load the new firmware, then reboot. + +**Note:** The firmware is called `RTK_Surveyor_Firmware_vXX.bin` even though there are various RTK products (Facet, Express, Surveyor, etc). We united the different platforms into one. The [RTK Firmware](https://github.com/sparkfun/SparkFun_RTK_Firmware) runs on all our RTK products. + +### Force Firmware Loading + +In the rare event that a unit is not staying on long enough for new firmware to be loaded into a COM port, the RTK Firmware (as of version 1.2) has an override function. If a file named *RTK_Surveyor_Firmware_Force.bin* is detected on the SD card at boot that file will be used to overwrite the current firmware, and then be deleted. This update path is generally not recommended. Use the [GUI](firmware_update.md#updating-firmware-using-windows-gui) or [WiFi OTA](firmware_update.md#updating-firmware-from-wifi) methods as the first resort. + +## Updating Firmware From WiFi + +![Advanced system settings](img/WiFi Config/SparkFun%20RTK%20System%20Config%20Upload%20BIN.png) + +**Note:** Firmware versions 1.1 to 1.9 have an issue that severely limits firmware upload over WiFi and is not recommended; use the [GUI](firmware_update.md#updating-firmware-using-the-uploader-gui) method instead. Firmware versions v1.10 and beyond support direct firmware updates via WiFi. + +Firmware may be uploaded to the unit by clicking on 'Upload BIN', selecting the binary such as 'RTK_Surveyor_Firmware_v3_x.bin' and pressing upload. The unit will automatically reset once the firmware upload is complete. + +## Updating Firmware From CLI + +The command-line interface is also available. You’ll need to download the [RTK Firmware Binaries](https://github.com/sparkfun/SparkFun_RTK_Firmware_Binaries) repo. This repo contains the binaries but also various supporting tools including esptool.exe and the three binaries required along with the firmware (bootloader, partitions, and app0). + +### Windows + +Connect a USB A to C cable from your computer to the ESP32 port on the RTK device. Turn the unit on. Now identify the COM port the RTK enumerated at. The easiest way to do this is to open the Device Manager: + +![CH340 is on COM6 as shown in Device Manager](img/Serial/RTK_Surveyor_-_Firmware_Update_COM_Port.jpg) + +*CH340 is on COM6 as shown in Device Manager* + +If the COM port is not showing be sure the unit is turned **On**. If an unknown device is appearing, you’ll need to [install drivers for the CH340](https://learn.sparkfun.com/tutorials/how-to-install-ch340-drivers/all). Once you know the COM port, open a command prompt (Windows button + r then type ‘cmd’). + +![batch_program.bat running esptool](img/Terminal/SparkFun%20RTK%20Firmware%20Update%20CLI.png) + +*batch_program.bat running esptool* + +Once the correct COM is identified, run 'batch_program.bat' along with the binary file name and COM port. For example *batch_program.bat RTK_Surveyor_Firmware_v2_0.bin COM6*. COM6 should be replaced by the COM port you identified earlier. + +The batch file runs the following commands: + +``` +esptool.exe --chip esp32 --port COM6 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size detect 0x1000 ./bin/RTK_Surveyor.ino.bootloader.bin 0x8000 ./bin/RTK_Surveyor_Partitions_16MB.bin 0xe000 ./bin/boot_app0.bin 0x10000 ./RTK_Surveyor_Firmware_vxx.bin +``` + +Where *COM6* is replaced with the COM port that the RTK product enumerated at and *RTK_Surveyor_Firmware_vxx.bin* is the firmware you would like to load. + +**Note:** Some users have reported the 921600bps baud rate does not work. Decrease this to 115200 as needed. + +Upon completion, your RTK device will reset and power down. + +### macOS / Linux + +Get [esptool.py](https://github.com/espressif/esptool). Connect a USB A to C cable from your computer to the ESP32 port on the RTK device. Turn the unit on. Now identify the COM port the RTK enumerated at. + +If the COM port is not showing be sure the unit is turned **On**. If an unknown device is appearing, you’ll need to [install drivers for the CH340](https://learn.sparkfun.com/tutorials/how-to-install-ch340-drivers/all). Once you know the COM port, run the following command: + +py esptool.py --chip esp32 --port /dev/ttyUSB0 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size detect 0x1000 ./bin/RTK_Surveyor.ino.bootloader.bin 0x8000 ./bin/RTK_Surveyor_Partitions_16MB.bin 0xe000 ./bin/boot_app0.bin 0x10000 ./RTK_Surveyor_Firmware_vxx.bin + +Where */dev/ttyUSB0* is replaced with the port that the RTK product enumerated at and *RTK_Surveyor_Firmware_vxx.bin* is the firmware you would like to load. + +**Note:** Some users have reported the 921600bps baud rate does not work. Decrease this to 115200 as needed. + +Upon completion, your RTK device will reset and power down. + +## Updating 4MB Surveyors + +RTK Surveyors sold before September 2021 may have an ESP32 WROOM module with 4MB flash instead of 16MB flash. These units still support all the functionality of other RTK products with the following limitations: + +* There is not enough flash space for OTA. Upgrading the firmware must be done via [CLI](firmware_update.md#updating-firmware-from-cli) or [GUI](firmware_update.md#updating-firmware-using-windows-gui). OTA, WiFi, or SD update paths are not possible. + +The GUI (as of v1.3) will autodetect the ESP32's flash size and load the appropriate partition file. No user interaction is required. + +If you are using the CLI method, be sure to point to the [4MB partition file](https://github.com/sparkfun/SparkFun_RTK_Firmware_Binaries/blob/main/bin/RTK_Surveyor_Partitions_4MB.bin?raw=true). For example: + +``` +esptool.exe --chip esp32 --port COM6 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size detect 0x1000 ./bin/RTK_Surveyor.ino.bootloader.bin 0x8000 ./bin/**RTK_Surveyor_Partitions_4MB**.bin 0xe000 ./bin/boot_app0.bin 0x10000 ./RTK_Surveyor_Firmware_vxx.bin +``` + +### Determining The Size of Flash + +To determine if the device has a 4MB module: + +* Use the esptool via CLI. Please see the [flash_id](https://docs.espressif.com/projects/esptool/en/latest/esp32s3/esptool/basic-commands.html#read-spi-flash-id-flash-id) command for usage. +* Use the GUI and attempt a firmware update. The output will auto-detect and show the flash size, as shown below: + +![Module with 4MB flash](img/SparkFun%20RTK%20Firmware%20Update%20GUI%20-%204MB.png) + +## Updating u-blox Firmware + +The following products contain the following u-blox receivers: + +* RTK Surveyor: ZED-F9P +* RTK Express: ZED-F9P +* RTK Express Plus: ZED-F9R +* RTK Facet: ZED-F9P +* RTK Facet L-Band: ZED-F9P and NEO-D9S + +The firmware loaded onto the ZED-F9P, ZED-F9R, and NEO-D9S receivers is written by u-blox and can vary depending on the manufacture date. The RTK Firmware (that runs on the ESP32) is designed to flexibly work with any u-blox firmware. Upgrading the ZED-F9x/NEO-D9S is a good thing to consider but is not crucial to the use of RTK products. + +Not sure what firmware is loaded onto your RTK product? Open the [System Menu](menu_system.md) to display the module's current firmware version. + +The firmware on u-blox devices can be updated using a [Windows-based GUI](firmware_update.md#updating-using-windows-gui) or [u-center](firmware_update.md#updating-using-u-center). A CLI method is also possible using the `ubxfwupdate.exe` tool provided with u-center. Additionally, u-blox offers the source for the ubxfwupdate tool that is written in C. It is currently released only under an NDA so contact your local u-blox Field Applications Engineer if you need a different method. + +## Updating Using Windows GUI + +![SparkFun u-blox firmware update tool](img/SparkFun%20RTK%20Facet%20L-Band%20u-blox%20Firmware%20Update%20GUI.png) + +*SparkFun RTK u-blox Firmware Update Tool* + +The [SparkFun RTK u-blox Firmware Update Tool](https://github.com/sparkfun/SparkFun_RTK_Firmware_Binaries/tree/main/u-blox_Update_GUI) is a simple Windows GUI and python script that runs the ubxfwupdate.exe tool. This allows users to directly update module firmware without the need for u-center. Additionally, this tool queries the module to verify that the firmware type matches the module. Because the RTK Facet L-Band contains two u-blox modules that both appear as identical serial ports, it can be difficult and perilous to know which port to load firmware. This tool prevents ZED-F9P firmware from being accidentally loaded onto a NEO-D9S receiver and vice versa. + +The SparkFun RTK u-blox Firmware Update Tool will only run on Windows as it relies upon u-blox's `ubxfwupdate.exe`. The full, integrated executable for Windows is available [here](https://github.com/sparkfun/SparkFun_RTK_Firmware_Binaries/raw/main/u-blox_Update_GUI/Windows_exe/RTK_u-blox_Update_GUI.exe). + +* Attach the RTK device's USB port to your computer using a USB cable +* Turn the RTK device on +* Open Device Manager to confirm which COM port the device is operating on + +![Device Manager showing USB Serial port on COM14](https://github.com/sparkfun/SparkFun_RTK_Firmware_Binaries/raw/main/u-blox_Update_GUI/SparkFun_RTK_u-blox_Updater_COM_Port.jpg) + +*Device Manager showing USB Serial port on COM14* + +* Get the latest binary firmware file from the [ZED Firmware](https://github.com/sparkfun/SparkFun_RTK_Firmware_Binaries/tree/main/ZED%20Firmware), [NEO Firmware](https://github.com/sparkfun/SparkFun_RTK_Firmware_Binaries/tree/main/NEO%20Firmware) folder, or the [u-blox](https://www.u-blox.com/) website +* Run *RTK_u-blox_Update_GUI.exe* (it takes a few seconds to start) +* Click the Firmware File *Browse* and select the binary file for the update +* Select the COM port previously seen in the Device Manager +* Click *Update Firmware* + +Once complete, the u-blox module will restart. + +### Updating Using u-center + +If you're familiar with u-center a tutorial with step-by-step instructions for locating the firmware version as well as changing the firmware can be found in [How to Upgrade Firmware of a u-blox Receiver](https://learn.sparkfun.com/tutorials/how-to-upgrade-firmware-of-a-u-blox-gnss-receiver/all). + +### ZED-F9P Firmware Changes + +This module is used in the Surveyor, Express, and Facet. It is capable of both Rover *and* base modes. + +Most of these binaries can be found in the [ZED Firmware/ZED-F9P](https://github.com/sparkfun/SparkFun_RTK_Firmware_Binaries/tree/main/ZED%20Firmware/ZED-F9P) folder. + +All field testing and device-specific performance parameters were obtained with ZED-F9P v1.30. + +* v1.12 has the benefit of working with SBAS and an operational RTK status signal (the LED illuminates correctly). See [release notes](https://content.u-blox.com/sites/default/files/ZED-F9P-FW100-HPG112_RN_%28UBX-19026698%29.pdf). + +* v1.13 has a few RTK and receiver performance improvements but introduces a bug that causes the RTK Status LED to fail when SBAS is enabled. See [release notes](https://content.u-blox.com/sites/default/files/ZED-F9P-FW100-HPG113_RN_%28UBX-20019211%29.pdf). + +* v1.30 has a few RTK and receiver performance improvements, I2C communication improvements, and most importantly support for SPARTN PMP packets. See [release notes](https://www.u-blox.com/sites/default/files/ZED-F9P-FW100-HPG130_RN_UBX-21047459.pdf). + +* v1.32 has a few SPARTN protocol-specific improvements. See [release notes](https://www.u-blox.com/sites/default/files/documents/ZED-F9P-FW100-HPG132_RN_UBX-22004887.pdf). This firmware is required for use with the NEO-D9S and the decryption of PMP messages. + +### ZED-F9R Firmware Changes + +This module is used in the Express Plus. It contains an internal IMU and additional algorithms to support high-precision location fixes using dead reckoning. The ZED-F9R is not capable of operating in base mode. + +Most of these binaries can be found in the [ZED Firmware/ZED-F9R](https://github.com/sparkfun/SparkFun_RTK_Firmware_Binaries/tree/main/ZED%20Firmware/ZED-F9R) folder. + +* v1.00 Initial release. + +* v1.21 SPARTN support as well as adding E-scooter and robotic lawnmower dynamic models. See [release notes](https://www.u-blox.com/sites/default/files/ZED-F9R-02B_FW1.00HPS1.21_RN_UBX-21035491_1.3.pdf). + +### NEO-D9S Firmware Changes + +This module is used in the Facet L-Band to receive encrypted PMP messages over ~1.55GHz broadcast via a geosynchronous Inmarsat. + +This binary file can be found in the [NEO Firmware](https://github.com/sparkfun/SparkFun_RTK_Firmware_Binaries/tree/main/NEO%20Firmware) folder. + +* v1.04 Initial release. + +As of writing, no additional releases of the NEO-D9S firmware have been made. + +## Compiling Source + +### Windows + +The SparkFun RTK firmware is compiled using Arduino (currently v1.8.15). To compile: + +1. Install [Arduino](https://www.arduino.cc/en/software). + +2. Install ESP32 for Arduino. [Here](https://learn.sparkfun.com/tutorials/esp32-thing-hookup-guide#installing-via-arduino-ide-boards-manager) are some good instructions for installing it via the Arduino Boards Manager. **Note**: Use v2.0.2 of the core. **Note:** We use the 'ESP32 Dev Module' for pin numbering. Select the correct board under Tools->Board->ESP32 Arduino->ESP32 Dev Module. + +3. Change the Partition table. Replace + + ```C:\Users\\[user name]\AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.2\tools\partitions\app3M_fat9M_16MB.csv``` + + with the app3M_fat9M_16MB.csv [file](https://github.com/sparkfun/SparkFun_RTK_Firmware/blob/main/Firmware/app3M_fat9M_16MB.csv?raw=true) found in the [Firmware folder](https://github.com/sparkfun/SparkFun_RTK_Firmware/tree/main/Firmware). This will increase the program partition from a maximum of 1.9MB to 3MB. + +4. From the Arduino IDE, set the core settings from the **Tools** menu: + + A. Set the 'Partition Scheme' to *16M Flash (3MB APP/9MB FATFS)*. This will use the 'app3M_fat9M_16MB.csv' updated partition table. + + B. Set the 'Flash Size' to 16MB (128mbit) + +5. Obtain all the [required libraries](firmware_update.md#required-libraries). + +Once compiled, firmware can be uploaded directly to a unit when the RTK unit is on and the correct COM port is selected under the Arduino IDE Tools->Port menu. + +If you are seeing the error: + +> text section exceeds available space ... + +You have not replaced the partition file correctly. See the 'Change Partition table' step inside the [Windows instructions](firmware_update.md#windows_1). + +**Note:** There are a variety of compile guards (COMPILE_WIFI, COMPILE_AP, etc) at the top of RTK_Surveyor.ino that can be commented out to remove them from compilation. This will greatly reduce the firmware size and allow for faster development of functions that do not rely on these features (serial menus, system configuration, logging, etc). + +#### Required Libraries + +**Note:** You should click on the link next to each of the #includes at the top of RTK_Surveyor.ino within the Arduino IDE to open the library manager and download them. Getting them directly from Github also works but may not be 'official' releases. + +Using the library manager in the Arduino IDE, for each of the libraries below: + + 1. Locate the library by typing the libary name into the search box + + 2. Click on the library + + 3. Select the version listed in the compile-rtk-firmware.yml file for the [main](https://github.com/sparkfun/SparkFun_RTK_Firmware/blob/main/.github/workflows/compile-rtk-firmware.yml) or the [release_candidate](https://github.com/sparkfun/SparkFun_RTK_Firmware/blob/release_candidate/.github/workflows/compile-rtk-firmware.yml) branch + + 4. Click on the Install button in the lower right + +The RTK firmware requires the following libraries: + + * [Arduino JSON](https://github.com/bblanchon/ArduinoJson) + + * [ESP32Time](https://github.com/fbiego/ESP32Time) + + * [ESP32 BleSerial](https://github.com/avinabmalla/ESP32_BleSerial) + + * [ESP32-OTA-Pull](https://github.com/mikalhart/ESP32-OTA-Pull) + + * Ethernet + + * [JC_Button](https://github.com/JChristensen/JC_Button) + + * [PubSub Client for MQTT](https://github.com/knolleary/pubsubclient) + + * [SdFat](https://github.com/greiman/SdFat) + + * [SparkFun LIS2DH12 Arduino Library](https://github.com/sparkfun/SparkFun_LIS2DH12_Arduino_Library) + + * [SparkFun MAX1704x Fuel Gauge Arduino Library](https://github.com/sparkfun/SparkFun_MAX1704x_Fuel_Gauge_Arduino_Library) + + * [SparkFun u-blox GNSS v3](https://github.com/sparkfun/SparkFun_u-blox_GNSS_v3) + + * [SparkFun_WebServer_ESP32_W5500](https://github.com/SparkFun/SparkFun_WebServer_ESP32_W5500) + +The following libraries are only available via GitHub: + + * [AsyncTCP](https://github.com/me-no-dev/AsyncTCP) (not available via library manager) + + * [ESPAsyncWebServer](https://github.com/me-no-dev/ESPAsyncWebServer) (not available via library manager) + + * [SparkFun Micro OLED Breakout](https://github.com/sparkfun/SparkFun_Micro_OLED_Arduino_Library) + +### Ubuntu 20.04 + +#### Virtual Machine + +Execute the following commands to create the Linux virtual machine: + +1. Using a browser, download the Ubuntu 20.04 Desktop image + +2. virtualbox + + 1. Click on the new button + 2. Specify the machine Name, e.g.: Sparkfun_RTK_20.04 + 3. Select Type: Linux + 4. Select Version: Ubuntu (64-bit) + 5. Click the Next> button + 6. Select the memory size: 7168 + 7. Click the Next> button + 8. Click on Create a virtual hard disk now + 9. Click the Create button + 10. Select VDI (VirtualBox Disk Image) + 11. Click the Next> button + 12. Select Dynamically allocated + 13. Click the Next> button + 14. Select the disk size: 128 GB + 15. Click the Create button + 16. Click on Storage + 17. Click the empty CD icon + 18. On the right-hand side, click the CD icon + 19. Click on Choose a disk file... + 20. Choose the ubuntu-20.04... iso file + 21. Click the Open button + 22. Click on Network + 23. Under 'Attached to:' select Bridged Adapter + 24. Click the OK button + 25. Click the Start button + +3. Install Ubuntu 20.04 + +4. Log into Ubuntu + +5. Click on Activities + +6. Type terminal into the search box + +7. Optionally install the SSH server + + 1. In the terminal window + 1. sudo apt install -y net-tools openssh-server + 2. ifconfig + + Write down the IP address + + 2. On the PC + 1. ssh-keygen -t rsa -f ~/.ssh/Sparkfun_RTK_20.04 + 2. ssh-copy-id -o IdentitiesOnly=yes -i ~/.ssh/Sparkfun_RTK_20.04 <username>@<IP address> + 3. ssh -Y <username>@<IP address> + +#### Build Environment + +Execute the following commands to create the build environment for the SparkFun RTK Firmware: + +1. sudo adduser $USER dialout +2. sudo shutdown -r 0 + + Reboot to ensure that the dialout privilege is available to the user + +3. sudo apt update +4. sudo apt install -y git gitk git-cola minicom python3-pip +5. sudo pip3 install pyserial +6. mkdir ~/SparkFun +7. mkdir ~/SparkFun/esptool +8. cd ~/SparkFun/esptool +9. git clone https://github.com/espressif/esptool . +10. cd ~/SparkFun +11. nano serial-port.sh + + Insert the following text into the file: + + ```C++ + #!/bin/bash + # serial-port.sh + # + # Shell script to read the serial data from the RTK Express ESP32 port + # + # Parameters: + # 1: ttyUSBn + # + sudo minicom -b 115200 -8 -D /dev/$1 < /dev/tty + ``` + +12. chmod +x serial-port.sh +13. nano new-firmware.sh + + Insert the following text into the file: + + ```C++ + #!/bin/bash + # new-firmware.sh + # + # Shell script to load firmware into the RTK Express via the ESP32 port + # + # Parameters: + # 1: ttyUSBn + # 2: Firmware file + # + sudo python3 ~/SparkFun/RTK_Binaries/Uploader_GUI/esptool.py --chip esp32 --port /dev/$1 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size detect \ + 0x1000 ~/SparkFun/RTK_Binaries/bin/RTK_Surveyor.ino.bootloader.bin \ + 0x8000 ~/SparkFun/RTK_Binaries/bin/RTK_Surveyor_Partitions_16MB.bin \ + 0xe000 ~/SparkFun/RTK_Binaries/bin/boot_app0.bin \ + 0x10000 $2 + ``` + +14. chmod +x new-firmware.sh +15. nano new-firmware-4mb.sh + + Insert the following text into the file: + + ```C++ + #!/bin/bash + # new-firmware-4mb.sh + # + # Shell script to load firmware into the 4MB RTK Express via the ESP32 port + # + # Parameters: + # 1: ttyUSBn + # 2: Firmware file + # + sudo python3 ~/SparkFun/RTK_Binaries/Uploader_GUI/esptool.py --chip esp32 --port /dev/$1 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size detect \ + 0x1000 ~/SparkFun/RTK_Binaries/bin/RTK_Surveyor.ino.bootloader.bin \ + 0x8000 ~/SparkFun/RTK_Binaries/bin/RTK_Surveyor_Partitions_4MB.bin \ + 0xe000 ~/SparkFun/RTK_Binaries/bin/boot_app0.bin \ + 0x10000 $2 + ``` + +16. chmod +x new-firmware-4mb.sh + + Get the SparkFun RTK Firmware sources + +17. mkdir ~/SparkFun/RTK +18. cd ~/SparkFun/RTK +19. git clone https://github.com/sparkfun/SparkFun_RTK_Firmware . + + Get the SparkFun RTK binaries + +20. mkdir ~/SparkFun/RTK_Binaries +21. cd ~/SparkFun/RTK_Binaries +22. git clone https://github.com/sparkfun/SparkFun_RTK_Firmware_Binaries.git . + + Install the Arduino IDE + +23. mkdir ~/SparkFun/arduino +24. cd ~/SparkFun/arduino +25. wget https://downloads.arduino.cc/arduino-1.8.15-linux64.tar.xz +26. tar -xvf ./arduino-1.8.15-linux64.tar.xz +27. cd arduino-1.8.15/ +28. sudo ./install.sh + + Add the ESP32 support + +29. Arduino + + 1. Click on File in the menu bar + 2. Click on Preferences + 3. Go down to the Additional Boards Manager URLs text box + 4. Only if the textbox already has a value, go to the end of the value or values and add a comma + 5. Add the link: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json + 6. Note the value in Sketchbook location + 7. Click the OK button + 8. Click on File in the menu bar + 9. Click on Quit + + Get the required external libraries, then add to the Sketchbook location from above + +30. cd ~/Arduino/libraries +31. mkdir AsyncTCP +32. cd AsyncTCP/ +33. git clone https://github.com/me-no-dev/AsyncTCP.git . +34. cd .. +35. mkdir ESPAsyncWebServer +36. cd ESPAsyncWebServer +37. git clone https://github.com/me-no-dev/ESPAsyncWebServer . + + Connect the Config ESP32 port of the RTK to a USB port on the computer + +38. ls /dev/ttyUSB* + + Enable the libraries in the Arduino IDE + +39. Arduino + + 1. From the menu, click on File + 2. Click on Open... + 3. Select the ~/SparkFun/RTK/Firmware/RTK_Surveyor/RTK_Surveyor.ino file + 4. Click on the Open button + + Select the ESP32 development module + + 5. From the menu, click on Tools + 6. Click on Board + 7. Click on Board Manager… + 8. Click on esp32 + 9. Select version 2.0.2 + 10. Click on the Install button in the lower right + 11. Close the Board Manager... + 12. From the menu, click on Tools + 13. Click on Board + 14. Click on ESP32 Arduino + 15. Click on ESP32 Dev Module + + Load the required libraries + + 16. From the menu, click on Tools + 17. Click on Manage Libraries… + 18. For each of the following libraries: + + 1. Locate the library + 2. Click on the library + 3. Select the version listed in the compile-rtk-firmware.yml file for the [main](https://github.com/sparkfun/SparkFun_RTK_Firmware/blob/main/.github/workflows/compile-rtk-firmware.yml) or the [release_candidate](https://github.com/sparkfun/SparkFun_RTK_Firmware/blob/release_candidate/.github/workflows/compile-rtk-firmware.yml) branch + 4. Click on the Install button in the lower right + + Library List: + + * ArduinoJson + * ESP32Time + * ESP32-OTA-Pull + * ESP32_BleSerial + * Ethernet + * JC_Button + * MAX17048 - Used for “Test Sketch/Batt_Monitor” + * PubSubClient + * SdFat + * SparkFun LIS2DH12 Arduino Library + * SparkFun MAX1704x Fuel Gauge Arduino Library + * SparkFun Qwiic OLED Graphics Library + * SparkFun u-blox GNSS v3 + * SparkFun_WebServer_ESP32_W5500 + + 19. Click on the Close button + + Select the terminal port + + 20. From the menu, click on Tools + 21. Click on Port, Select the port that was displayed in step 38 above + 22. Select /dev/ttyUSB0 + 23. Click on Upload Speed + 24. Select 230400 + + Setup the partitions for the 16 MB flash + + 25. From the menu, click on Tools + 26. Click on Flash Size + 27. Select 16MB + 28. From the menu, click on Tools + 29. Click on Partition Scheme + 30. Click on 16M Flash (3MB APP/9MB FATFS) + 31. From the menu click on File + 32. Click on Quit + +40. cd ~/SparkFun/RTK/ +41. cp Firmware/app3M_fat9M_16MB.csv ~/.arduino15/packages/esp32/hardware/esp32/2.0.2/tools/partitions/app3M_fat9M_16MB.csv + +### Arduino CLI + +The firmware can be compiled using [Arduino CLI](https://github.com/arduino/arduino-cli). This makes compilation fairly platform independent and flexible. All release candidates and firmware releases are compiled using Arduino CLI using a github action. You can see the source of the action [here](https://github.com/sparkfun/SparkFun_RTK_Firmware/blob/main/.github/workflows/compile-release.yml), and use it as a starting point for Arduino CLI compilation. diff --git a/docs/gis_software.md b/docs/gis_software.md new file mode 100644 index 000000000..954e7e0d8 --- /dev/null +++ b/docs/gis_software.md @@ -0,0 +1,521 @@ +# GIS Software + +Surveyor: ![Feature Supported](img/Icons/GreenDot.png) / Express: ![Feature Supported](img/Icons/GreenDot.png) / Express Plus: ![Feature Supported](img/Icons/GreenDot.png) / Facet: ![Feature Supported](img/Icons/GreenDot.png) / Facet L-Band: ![Feature Supported](img/Icons/GreenDot.png) / Reference Station: ![Feature Supported](img/Icons/GreenDot.png) + +While we recommend SW Maps for Android, there are a variety of 3rd party apps available for GIS and surveying. We will cover a few examples below that should give you an idea of how to get the incoming NMEA data over Bluetooth into the software of your choice. + +## SW Maps + +The best mobile app that we’ve found is the powerful, free, and easy-to-use [SW Maps](https://play.google.com/store/apps/details?id=np.com.softwel.swmaps) by Softwel. It is compatible with Android and iOS, either phone or tablet with Bluetooth. What makes SW Maps truly powerful is its built-in NTRIP client. This is a fancy way of saying that we’ll be showing you how to get RTCM correction data over the cellular network. + +Be sure your device is [paired over Bluetooth](connecting_bluetooth.md#android). + +![List of BT Devices in SW Maps](img/SWMaps/SparkFun%20RTK%20SWMaps%20Bluetooth%20Connect.png) + +*List of available Bluetooth devices* + +From SW Map's main menu, select *Bluetooth GNSS*. This will display a list of available Bluetooth devices. Select the Rover or Base you just paired with. If you are taking height measurements (altitude) in addition to position (lat/long) be sure to enter the height of your antenna off the ground including any [ARP offsets](https://geodesy.noaa.gov/ANTCAL/FAQ.xhtml#faq4) of your antenna (this should be printed on the side). + +Click on 'CONNECT' to open a Bluetooth connection. Assuming this process takes a few seconds, you should immediately have a location fix. + +![SW Maps with RTK Fix](img/SWMaps/SparkFun%20RTK%20SWMaps%20GNSS%20Status.png) + +*SW Maps with RTK Fix* + +You can open the GNSS Status sub-menu to view the current data. + +**NTRIP Client** + +If you’re using a serial radio to connect a Base to a Rover for your correction data, or if you're using the RTK Facet L-Band with built-in corrections, you can skip this part. + +We need to send RTCM correction data from the phone back to the RTK device so that it can improve its fix accuracy. This is the amazing power of the SparkFun RTK products and SW Maps. Your phone can be the radio link! From the main SW Maps menu select NTRIP Client. Not there? Be sure the 'SparkFun RTK' instrument was automatically selected connecting. Disconnect and change the instrument to 'SparkFun RTK' to enable the NTRIP Connection option. + +![SW Maps NTRIP Connection menu](img/SWMaps/SparkFun_RTK_Surveyor_-_SW_Maps_NTRIP_Connection.jpg) + +*NTRIP Connection - Not there? Be sure to select 'SparkFun RTK' was selected as the instrument* + +![SW Maps NTRIP client](img/SWMaps/SW_Maps_-_NTRIP_Client.jpg) + +*Connecting to an NTRIP Caster* + +Enter your NTRIP Caster credentials and click connect. You will see bytes begin to transfer from your phone to the RTK Express. Within a few seconds, the RTK Express will go from ~300mm accuracy to 14mm. Pretty nifty, no? + +Once you have a full RTK fix you'll notice the location bubble in SW Maps turns green. Just for fun, rock your rover monopole back and forth on a fixed point. You'll see your location accurately reflected in SW Maps. Millimeter location precision is a truly staggering thing. + +## Field Genius + +[Field Genius for Android](https://www.microsurvey.com/products/fieldgenius-for-android/) is another good solution, albeit a lot more expensive than free. + +Be sure your device is [paired over Bluetooth](connecting_bluetooth.md#android). + +![Main Menu](img/FieldGenius/Field%20Genius%202.png) + +From the Main Menu open `Select Instrument`. + +![Add Profile](img/FieldGenius/Field%20Genius%203.png) + +Click the 'Add Profile' button. + +![New Instrument Profile](img/FieldGenius/Field%20Genius%204.png) + +Click `GNSS Rover` and select *NMEA* as the Make. Set your Profile Name to something memorable like 'RTK-Express' then click the 'Create' button. + +![Set up communication](img/FieldGenius/Field%20Genius%205.png) + +Click on 'SET UP COMMUNICATION'. + +![Bluetooth Search Button](img/FieldGenius/Field%20Genius%207.png) + +From the Bluetooth communication page, click the 'Search' button. + +![List of paired Bluetooth devices](img/FieldGenius/Field%20Genius%206.png) + +You will be shown a list of paired devices. Select the RTK device you'd like to connect to then click 'Connect'. The RTK device will connect and the MAC address shown on the RTK device OLED will change to the Bluetooth icon indicating a link is open. + +**NTRIP Client** + +If you’re using a serial radio to connect a Base to a Rover for your correction data, or if you're using the RTK Facet L-Band with built-in corrections, you can skip this part. + +![Set up corrections](img/FieldGenius/Field%20Genius%208.png) + +We need to send RTCM correction data from the phone back to the RTK device so that it can improve its fix accuracy. Your phone can be the radio link! Click on 'SET UP CORRECTIONS'. + +![RTK via Internet](img/FieldGenius/Field%20Genius%209.png) + +Click on 'RTK via Internet' then 'SET UP INTERNET', then 'Done'. + +![Set up NTRIP data source](img/FieldGenius/Field%20Genius%2010.png) + +Click on 'SET UP DATA SOURCE'. + +![Adding a new source](img/FieldGenius/Field%20Genius%2011.png) + +Click 'Add New Source'. + +![NTRIP Credential Entry](img/FieldGenius/Field%20Genius%2012.png) + +Enter your NTRIP Caster credentials and click 'DONE'. + +What's an NTRIP Caster? In a nutshell, it's a server that is sending out correction data every second. There are thousands of sites around the globe that calculate the perturbations in the ionosphere and troposphere that decrease the accuracy of GNSS accuracy. Once the inaccuracies are known, correction values are encoded into data packets in the RTCM format. You, the user, don't need to know how to decode or deal with RTCM, you simply need to get RTCM from a source within 10km of your location into the RTK Express. The NTRIP client logs into the server (also known as the NTRIP caster) and grabs that data, every second, and sends it over Bluetooth to the RTK Express. + +Don't have access to an NTRIP Caster? You can use a 2nd RTK product operating in Base mode to provide the correction data. Checkout [Creating a Permanent Base](permanent_base.md). If you're the DIY sort, you can create your own low-cost base station using an ESP32 and a ZED-F9P breakout board. Check out [How to](https://learn.sparkfun.com/tutorials/how-to-build-a-diy-gnss-reference-station) Build a DIY GNSS Reference Station](https://learn.sparkfun.com/tutorials/how-to-build-a-diy-gnss-reference-station). If you'd just like a service, [Syklark](https://www.swiftnav.com/skylark) provides RTCM coverage for $49 a month (as of writing) and is extremely easy to set up and use. Remember, you can always use a 2nd RTK device in *Base* mode to provide RTCM correction data but it will be less accurate than a fixed position caster. + +![Selecting data source](img/FieldGenius/Field%20Genius%2011.png) + +Click 'My NTRIP1' then 'Done' and 'Connect'. + +You will then be presented with a list of Mount Points. Select the mount point you'd like to use then click 'Select' then 'Confirm'. + +Select 'Done' then from the main menu select 'Survey' to begin using the device. + +![Surveying Screen](img/FieldGenius/Field%20Genius%201.png) + +Now you can begin using the SparkFun RTK device with Field Genius. + +## SurvPC + +Note: The company behind SurvPC, Carlson Software, is not always welcoming to competitors of their $18,000 devices, so be warned. + +Be sure your device is [paired over Bluetooth](connecting_bluetooth.md#windows). + +![Equip Sub Menu](img/SurvPC/SparkFun%20RTK%20Software%20-%20SurvPC%20Equip%20Menu.jpg) + +*Equip Sub Menu* + +Select the *Equip* sub menu then `GPS Rover` + +![Select NMEA GPS Receiver](img/SurvPC/SparkFun%20RTK%20Software%20-%20SurvPC%20Rover%20NMEA.jpg) + +*Select NMEA GPS Receiver* + +From the drop down, select `NMEA GPS Receiver`. + +![Select Model: DGPS](img/SurvPC/SparkFun%20RTK%20Software%20-%20SurvPC%20Rover%20DGPS.jpg) + +*Select Model: DGPS* + +Select DGPS if you'd like to connect to an NTRIP Caster. If you are using the RTK Facet L-Band, or do not need RTK fix type precision, leave the model as Generic. + +![Bluetooth Settings](img/SurvPC/SparkFun%20RTK%20Software%20-%20SurvPC%20Rover%20Comms.jpg) + +*Bluetooth Settings Button* + +From the `Comms` submenu, click the Blueooth settings button. + +![SurvPC Bluetooth Devices](img/SurvPC/SparkFun%20RTK%20Software%20-%20SurvPC%20Rover%20Find%20Device.jpg) + +*SurvPC Bluetooth Devices* + +Click `Find Device`. + +![List of Paired Bluetooth Devices](img/SurvPC/SparkFun%20RTK%20Software%20-%20SurvPC%20Rover%20Select%20Bluetooth%20Device.jpg) + +*List of Paired Bluetooth Devices* + +You will be shown a list of devices that have been paired. Select the RTK device you want to connect to. + +![Connect to Device](img/SurvPC/SparkFun%20RTK%20Software%20-%20SurvPC%20Rover%20Select%20Bluetooth%20Device%20With%20MAC.jpg) + +*Connect to Device* + +Click the `Connect Bluetooth` button, shown in red in the top right corner. The software will begin a connection to the RTK device. You'll see the MAC address on the RTK device changes to the Bluetooth icon indicating it's connected. + +If SurvPC detects NMEA, it will report a successful connection. + +![Receiver Submenu](img/SurvPC/SparkFun%20RTK%20Software%20-%20SurvPC%20Rover%20Receiver.jpg) + +*Receiver Submenu* + +You are welcome to enter the ARP (antenna reference point) and surveying stick length for your particular setup. + +**NTRIP Client** + +Note: If you are using a radio to connect Base to Rover, or if you are using the RTK Facet L-Band you do not need to set up NTRIP; the device will achieve RTK fixes and output extremely accurate location data by itself. But if L-Band corrections are not available, or you are not using a radio link, the NTRIP Client can provide corrections to this Rover. + +![RTK Submenu](img/SurvPC/SparkFun%20RTK%20Software%20-%20SurvPC%20NTRIP%20Client.jpg) + +*RTK Submenu* + +If you selected 'DGPS' as the Model type, the RTK submenu will be shown. This is where you give the details about your NTRIP Caster such as your mount point, user name/pw, etc. For more information about creating your own NTRIP mount point please see [Creating a Permanent Base](permanent_base.md) + + +Enter your NTRIP Caster credentials and click connect. You will see bytes begin to transfer from your phone to the RTK Express. Within a few seconds, the RTK Express will go from ~300mm accuracy to 14mm. Pretty nifty, no? + +What's an NTRIP Caster? In a nutshell, it's a server that is sending out correction data every second. There are thousands of sites around the globe that calculate the perturbations in the ionosphere and troposphere that decrease the accuracy of GNSS accuracy. Once the inaccuracies are known, correction values are encoded into data packets in the RTCM format. You, the user, don't need to know how to decode or deal with RTCM, you simply need to get RTCM from a source within 10km of your location into the RTK Express. The NTRIP client logs into the server (also known as the NTRIP caster) and grabs that data, every second, and sends it over Bluetooth to the RTK Express. + +Don't have access to an NTRIP Caster? You can use a 2nd RTK product operating in Base mode to provide the correction data. Checkout [Creating a Permanent Base](permanent_base.md). If you're the DIY sort, you can create your own low-cost base station using an ESP32 and a ZED-F9P breakout board. Check out [How to](https://learn.sparkfun.com/tutorials/how-to-build-a-diy-gnss-reference-station) Build a DIY GNSS Reference Station](https://learn.sparkfun.com/tutorials/how-to-build-a-diy-gnss-reference-station). If you'd just like a service, [Syklark](https://www.swiftnav.com/skylark) provides RTCM coverage for $49 a month (as of writing) and is extremely easy to set up and use. Remember, you can always use a 2nd RTK device in *Base* mode to provide RTCM correction data but it will be less accurate than a fixed position caster. + +Once everything is connected up, click the Green check in the top right corner. + +![Storing Points](img/SurvPC/SparkFun%20RTK%20Software%20-%20SurvPC%20Survey.jpg) + +*Storing Points* + +Now that we have a connection, you can use the device, as usual, storing points and calculating distances. + +![SurvPC Skyplot](img/SurvPC/SparkFun%20RTK%20Software%20-%20SurvPC%20Skyplot.jpg) + +*SurvPC Skyplot* + +Opening the Skyplot will allow you to see your GNSS details in real-time. + +If you are a big fan of SurvPC please contact your sales rep and ask them to include SparkFun products in their Manufacturer drop-down list. + +## Survey Master + +[Survey Master](https://www.comnavtech.com/companyfile/4/) by ComNam / SinoGNSS is an Android-based option. The download location can vary so google 'Survey Master ComNav Download' if the link above fails. Download the zip file, send the APK file to a phone and install the program. + +![Startup wizard](img/SurveyMaster/SparkFun%20RTK%20Survey%20Master%20-%2001.png) + +By default, a wizard will guide you through the setup. The Project step will ask you for the name of the project, the datum, etc. + +![Connection Setup +](img/SurveyMaster/SparkFun%20RTK%20Survey%20Master%20-%2027.png) + +Next select your connection. + +![Connection specifics](img/SurveyMaster/SparkFun%20RTK%20Survey%20Master%20-%2029.png) + +For the Device Model select 'NMEA Device'. + +![TOP106 Antenna Parameters](img/SurveyMaster/SparkFun%20RTK%20Survey%20Master%20-%2002.png) + +If you are just getting started, use one of the default antenna types. If you are attempting to get sub-centimeter accuracy, enter the parameters of your antenna and add it. Above are the NGS-certified parameters for the [TOP106 antenna](https://www.sparkfun.com/products/17751). + +![List of Bluetooth devices](img/SurveyMaster/SparkFun%20RTK%20Survey%20Master%20-%2003.png) + +Click the 'Target Device' option to get a list of available Bluetooth devices. Make sure your RTK product is on and you should see the device. In this example 'Express Rover-B022' was chosen. + +To finish, click 'Connect'. You should see the Bluetooth MAC address on your RTK product change to the Bluetooth icon indicating a connection is established. + +![Rover Work Mode Configuration](img/SurveyMaster/SparkFun%20RTK%20Survey%20Master%20-%2004.png) + +Next is configuring the 'Work mode' of the device. The step is where we set up our NTRIP correction source. + +![Empty mode list](img/SurveyMaster/SparkFun%20RTK%20Survey%20Master%20-%2005.png) + +Click 'Add' to create a new work mode. + +![NTRIP Client](img/SurveyMaster/SparkFun%20RTK%20Survey%20Master%20-%2009.png) + +Shown above, we configure the NTRIP Client. Survey Master calls this the 'SinoGNSS' Protocol. Click on the three bars to the right of 'Server' to enter a new NTRIP connection. + +![List of Services](img/SurveyMaster/SparkFun%20RTK%20Survey%20Master%20-%2030.png) + +Here you can add different NTRIP Caster providers. If you're using RTK2Go be sure to enter your contact email into the user name. + +![Server and mount point selected](img/SurveyMaster/SparkFun%20RTK%20Survey%20Master%20-%2009.png) + +Return to the 'Datalink type' window and select the Server you just entered. Re-enter the server address and port for your NTRIP Caster. Once complete, click on the down-pointing arrow. This will ping the Caster and obtain the mount point table. Select your mount point. + +![Rover with work list in place](img/SurveyMaster/SparkFun%20RTK%20Survey%20Master%20-%2011.png) + +Select the newly created work mode and press the 'Apply' button. + +![Connecting to service](img/SurveyMaster/SparkFun%20RTK%20Survey%20Master%20-%2033.png) + +Survey Master will attempt to connect to your specified RTK corrections source (NTRIP Caster). Upon success, you will be located on the Project menu. + +Survey Master expects many more NMEA sentences than most GIS software. We must enable some additional messages on the RTK device to correctly communicate with Survey Master. + +![Configured NMEA messages](img/SurveyMaster/SparkFun%20RTK%20Survey%20Master%20-%2026%20.jpg) + +Note above: There are 9 enabled messages and GSV is set to '1'. + +Connect to the RTK device either over [WiFi AP config](/configure_with_wifi/) or via [Serial](/configure_with_serial/). Above is shown the serial method. + +Open a terminal at 115200bps and press a key to open the serial configuration menu. Press '2' for GNSS Messages, press '1' for NMEA messages, now be sure to enable 9 messages to a rate of 1: + +* GGA +* GLL +* GRS +* GSA +* GST +* GSV +* RMC +* VTG +* ZDA + +Once complete, press x until you exit the serial menus. Now we may return to Survey Master. + +![Survey Master showing the location of RTK Express](img/SurveyMaster/SparkFun%20RTK%20Survey%20Master%20-%2025.png) + +Click on the 'Survey' menu and then 'Topo Survey'. Above we can see a device with RTK float, and 117mm horizontal positional accuracy. + +Known Issues: + +* Survey Master parses the GxGSV sentence improperly and will only indicate GPS satellites even though the fix solution is using all satellites. + +![NMEA Sentences](img/SurveyMaster/SparkFun%20RTK%20Survey%20Master%20-%2015.png) + +To verify the NMEA sentences are being delivered correctly, Survey Master has a built-in tool. Select the Device->Rover->More->'H-Terminal'. + +## Vespucci + +[Vespucci](https://play.google.com/store/apps/details?id=de.blau.android&hl=en_US&gl=US) is an Open Street Map editor for Android. + +This software requires the RTK device to connect over TCP. Be sure you have a local WiFi network entered into the [WiFi Config menu](menu_wifi.md), have a TCP Client or Server enabled, and have noted the TCP port (it's 2947 by default). + +![Vespucci Gear Button](img/Vespucci/SparkFun%20RTK%20Vespucci%20-%20Main%20Gear.png) + +With a map open, select the gear icon on the bottom bar. + +![Vespucci Preferences menu](img/Vespucci/SparkFun%20RTK%20Vespucci%20-%20Preferences.png) + +From the Preferences menu, scroll to the bottom and select 'Advanced Preferences'. + +![Preferences menu showing Location Settings](img/Vespucci/SparkFun%20RTK%20Vespucci%20-%20Location%20Settings.png) + +Select **Location settings**. + +![GPS source menu](img/Vespucci/SparkFun%20RTK%20Vespucci%20-%20GPS%20Source.png) + +Select **GPS/GNSS source**. Select **NMEA from TCP client**. TCP server is also supported. + +![Vespucci NMEA network source menu](img/Vespucci/SparkFun%20RTK%20Vespucci%20-%20NMEA%20Network%20Source.png) + +Select **NMEA network source**. Enter the IP address and TCP port of the RTK device. The IP address can be found by opening a serial terminal while connected to WiFi (it is reported every few seconds). The TCP port is entered into the [WiFi Config menu](menu_wifi.md). + +![Vespucci showing location on map](img/Vespucci/SparkFun%20RTK%20Vespucci%20-%20Point%20on%20Map.png) + +Close all menus and you should see your location within Vespucci. + +## QGIS + +QGIS is a free and open-source geographic information system software for desktops. It's available [here](https://qgis.org/). + +Once the software is installed open QGIS Desktop. + +![View Menu](img/QGIS/SparkFun%20RTK%20QGIS%20-%20View%20Menu.png) + +Open the View Menu, then look for the 'Panels' submenu. + +![Panels submenu](img/QGIS/SparkFun%20RTK%20QGIS%20-%20Enable%20GPS%20Info%20Panel.png) + +From the Panels submenu, enable 'GPS Information'. This will show a new panel on the left side. + +At this point, you will need to enable *TCP Server* mode on your RTK device from the [WiFi Config menu](menu_wifi.md). Once the RTK device is connected to local WiFi QGIS will be able to connect to the given IP address and TCP port. + +![Select GPSD](img/QGIS/SparkFun%20RTK%20QGIS%20-%20GPS%20Panel.png) + +Above: From the subpanel, select 'gpsd'. + +![Entering gpsd specifics](img/QGIS/SparkFun%20RTK%20QGIS%20-%20GPS%20Panel%20Entering%20IP%20and%20port.png) + +Enter the IP address of your RTK device. This can be found by opening a serial connection to the device. The IP address will be displayed every few seconds. Enter the TCP port to use. By default an RTK device uses 2947. + +Press 'Connect'. + +![Viewing location in QGIS](img/QGIS/SparkFun%20RTK%20QGIS%20-%20Location%20on%20Map.png) + +The device location will be shown on the map. To see a map, be sure to enable OpenStreetMap under the XYZ Tiles on the Browser. + +![Connecting over Serial](img/QGIS/SparkFun%20RTK%20QGIS%20-%20Direct%20Serial%20Connection.png) + +Alternatively, a direct serial connection to the RTK device can be obtained. Use a USB cable to connect to the 'CONFIG UBLOX' port on RTK Surveyor/Express/Plus and the single USB C port on the RTK Facet/L-Band. Be sure you have the u-blox driver installed. Then select the appropriate COM port for the u-blox module. See [Configure with Serial](configure_with_serial.md) for more information. + +## QField + +![Opening page of QField](img/QField/SparkFun%20RTK%20QField%20-%20Open%20Project.png) + +[QField](https://docs.qfield.org/get-started/) is a free GIS the Android app that runs QGIS. + +![NMEA message configuration](img/WiFi Config/RTK_Surveyor_-_WiFi_Config_-_GNSS_Config_Messages.jpg) + +*The 'Reset to Surveying Defaults' button* + +First, configure the RTK device to output *only* NMEA messages. QField currently does not correctly parse other messages such as RAWX or RTCM so these will interfere with communication if they are enabled. + +These RTK device settings can be found under the [Messages menu](menu_messages.md) through the [WiFi config page](configure_with_wifi.md) or through the [Serial Config menu](configure_with_serial.md). + +![QField creating a project](img/QField/SparkFun%20RTK%20QField%20-%20Create%20Project.png) + +Create an account and project on [QFieldCloud](https://qfield.cloud/). This project will be synchronized and viewable on the QField app. + +![Open Test Project](img/QField/SparkFun%20RTK%20QField%20-%20Refresh%20Project.png) + +*Refresh Projects button* + +Once the project is created, press the Refresh projects list button to update the list. Then select your project. + +![Hamburger Menu](img/QField/SparkFun%20RTK%20QField%20-%20Open%20Settings.png) + +*'Hamburger' menu in upper right corner* + +Press the icon in the top left corner of the app to open the project settings. + +![Project Settings Menu](img/QField/SparkFun%20RTK%20QField%20-%20Project%20Settings%202.png) + +*Project settings* + +From the project settings menu, press the gear icon to open the device settings dropdown menu. + +![Project Settings Submenu](img/QField/SparkFun%20RTK%20QField%20-%20Project%20Settings.png) + +*Project settings submenu* + +From the submenu, select 'Settings'. + +![Position Menu](img/QField/SparkFun%20RTK%20QField%20-%20Select%20Positioning%20Devce.png) + +*Positioning Menu* + +Select the Positioning Menu. Then, with your RTK device on and in normal mode (not AP Config) press the Scan button in the QField app to update the dropdown list of available Bluetooth devices. If your device is not detected, be sure you've [paired your cellphone or laptop with Bluetooth](connecting_bluetooth.md). + +Once connected exit out of the menus and see position information within your project. + +## Apple iOS + +The software options for Apple iOS are much more limited because Apple products do not support Bluetooth SPP. That's ok! The SparkFun RTK devices support Bluetooth Low Energy (BLE) which *does* work with iOS. + +We recommend SWMaps for iOS. SWMaps is available for iOS [here](https://apps.apple.com/us/app/sw-maps/id6444248083). + +More information is available on the [System Menu](menu_system.md) for switching between Bluetooth SPP and BLE. + +To begin: + +Make sure your RTK device is switched on, in Rover mode and operating in Bluetooth BLE mode. + +Make sure Bluetooth is enabled on your iOS device Settings. + +The RTK device will not appear in the _OTHER DEVICES_ list. That is OK. + +![iOS Settings Bluetooth](img/iOS/Screenshot1.PNG) + +*iOS Settings Bluetooth* + +Open SWMaps. + +Open or continue a Project if desired. + +SWMaps will show your approximate location based on your iOS device's location. + +![iOS SWMaps Initial Location](img/iOS/Screenshot2.PNG) + +*iOS SWMaps Initial Location* + +Press the 'SWMaps' icon at the top left of the screen to open the menu. + +![iOS SWMaps Menu](img/iOS/Screenshot3.PNG) + +*iOS SWMaps Menu* + +Select Bluetooth GNSS. + +![iOS SWMaps Bluetooth Connection](img/iOS/Screenshot4.PNG) + +*iOS SWMaps Bluetooth Connection* + +Set the **Instrument Model** to **Generic NMEA (Bluetooth LE)**. + +![iOS SWMaps Instrument Model](img/iOS/Screenshot5.PNG) + +*iOS SWMaps Instrument Model* + +Press 'Scan' and your RTK device should appear. + +![iOS SWMaps Bluetooth Scan](img/iOS/Screenshot6.PNG) + +*iOS SWMaps Bluetooth Scan* + +Select (tick) the RTK device and press 'Connect'. + +![iOS SWMaps Bluetooth Connected](img/iOS/Screenshot7.PNG) + +*iOS SWMaps Bluetooth Connected* + +Close the menu and your RTK location will be displayed on the map. + +You can now use the other features of SWMaps, including the built-in NTRIP Client. + +Re-open the menu and select 'NTRIP Client'. + +Enter the details for your NTRIP Caster - as shown in the [SWMaps section above](#sw-maps). + +![iOS SWMaps NTRIP Client](img/iOS/Screenshot8.PNG) + +*iOS SWMaps NTRIP Client* + +Click 'Connect' + +At this point, you should see a Bluetooth Pairing Request. Select 'Pair' to pair your RTK with your iOS device. + +![iOS Bluetooth Pairing](img/iOS/Screenshot9.PNG) + +*iOS Bluetooth Pairing* + +SWMaps will now receive NTRIP correction data from the caster and push it to your RTK over Bluetooth BLE. + +From the SWMaps menu, open 'GNSS Status' to see your position, fix type and accuracy. + +![iOS SWMaps GNSS Status](img/iOS/Screenshot10.PNG) + +*iOS SWMaps GNSS Status* + +If you return to the iOS Bluetooth Settings, you will see that your iOS and RTK devices are now paired. + +![iOS Settings Bluetooth Paired](img/iOS/Screenshot11.PNG) + +*iOS Settings Bluetooth - Paired* + +## Other GIS Packages + +Hopefully, these examples give you an idea of how to connect the RTK product line to most any GIS software. If there is other GIS software that you'd like to see configuration information about, please open an issue on the [RTK Firmware repo](https://github.com/sparkfun/SparkFun_RTK_Firmware/issues) and we'll add it. + +## What's an NTRIP Caster? + +In a nutshell, it's a server that is sending out correction data every second. There are thousands of sites around the globe that calculate the perturbations in the ionosphere and troposphere that decrease the accuracy of GNSS accuracy. Once the inaccuracies are known, correction values are encoded into data packets in the RTCM format. You, the user, don't need to know how to decode or deal with RTCM, you simply need to get RTCM from a source within 10km of your location into the RTK Express. The NTRIP client logs into the server (also known as the NTRIP caster) and grabs that data, every second, and sends it over Bluetooth to the RTK Express. + +## Where do I get RTK Corrections? + +Be sure to see [Correction Sources](correction_sources.md). + +Don't have access to an NTRIP Caster or other RTCM correction source? There are a few options. + +The [SparkFun RTK Facet L-Band](https://www.sparkfun.com/products/20000) gets corrections via an encrypted signal from geosynchronous satellites. This device gets RTK Fix without the need for a WiFi or cellular connection. + +Also, you can use a 2nd RTK product operating in Base mode to provide the correction data. Check out [Creating a Permanent Base](permanent_base.md). + +If you're the DIY sort, you can create your own low-cost base station using an ESP32 and a ZED-F9P breakout board. Check out [How to Build a DIY GNSS Reference Station](https://learn.sparkfun.com/tutorials/how-to-build-a-diy-gnss-reference-station). + +There are services available as well. [Syklark](https://www.swiftnav.com/skylark) provides RTCM coverage for $49 a month (as of writing) and is extremely easy to set up and use. [Point One](https://app.pointonenav.com/trial?utm_source=sparkfun) also offers RTK NTRIP service with a free 14 day trial and easy to use front end. diff --git a/docs/gis_software_android.md b/docs/gis_software_android.md new file mode 100644 index 000000000..92cf0c8df --- /dev/null +++ b/docs/gis_software_android.md @@ -0,0 +1,614 @@ +# Android + +Surveyor: ![Feature Supported](img/Icons/GreenDot.png) / Express: ![Feature Supported](img/Icons/GreenDot.png) / Express Plus: ![Feature Supported](img/Icons/GreenDot.png) / Facet: ![Feature Supported](img/Icons/GreenDot.png) / Facet L-Band: ![Feature Supported](img/Icons/GreenDot.png) / Reference Station: ![Feature Supported](img/Icons/GreenDot.png) + +While we recommend [SW Maps for Android](gis_software_android/#sw-maps), there are a variety of 3rd party apps available for GIS and surveying for [Android](gis_software_android.md), [iOS](gis_software_ios.md), and [Windows](gis_software_windows.md). We will cover a few examples below that should give you an idea of how to get the incoming NMEA data into the software of your choice. + +## ArcGIS Field Maps + +[ArcGIS Field Maps](https://play.google.com/store/apps/details?id=com.esri.fieldmaps&hl=en_US) by Esri is a popular GIS app. Unfortunately it does not have a built in NTRIP Client to allow high precision corrections down to the RTK device. To enable high-precision, a [mock location](connecting_bluetooth.md/#enable-mock-location) and an intermediary app such as [GNSS Master](gis_software_android.md/#gnss-master) or [Lefebure](gis_software_android.md/#lefebure) is needed. + +Once a [mock location](connecting_bluetooth.md/#enable-mock-location) provider is setup, open Field Maps. + +![Field Maps main menu]() + +Select **World Imagery**. + +![ArcGIS Field Maps with 12mm accuracy]() + +*ArcGIS Field Maps with 12mm accuracy* + +Field Maps will use the device's internal location as its default location provider. With GNSS Master or Lefebure providing the mock location to the phone, Field Maps will have a super precise GNSS location and data collection can begin. + +## ArcGIS QuickCapture + +[ArcGIS QuickCapture](https://play.google.com/store/apps/details?id=com.esri.arcgisquickcapture&hl=en_US) by Esri is a popular GIS app. Unfortunately it does not allow Bluetooth connections to 3rd party RTK devices. To enable a connection to a SparkFun RTK device, a [mock location](connecting_bluetooth.md/#enable-mock-location) and an intermediary app such as [GNSS Master](gis_software_android.md/#gnss-master) or [Lefebure](gis_software_android.md/#lefebure) is needed. + +Once a [mock location](connecting_bluetooth.md/#enable-mock-location) provider is setup, open QuickCapture. + +![QuickCapture Main Window]() + +For the purposes of this demonstration, click *Continue without signing in*. + +![Add project button]() + +Select the **+** then **Browse Projects**. + +![Select BioBlitz]() + +Select a project. + +![BioBlitz options]() + +From the BioBlitz project screen we can see we have a GPS accuracy of less than 1 ft. The RTK device has RTK fix and is providing extremely accurate (better than 20mm or 1") positional data. + +Click the map icon in the upper right. + +![BioBlitz Map]() + +The location of the receiver is shown on a map. With GNSS Master or Lefebure providing the mock location to the phone, QuickCapture will have a very precise GNSS location and data collection can begin. + +## ArcGIS Survey123 + +[ArcGIS Survey123](https://play.google.com/store/apps/details?id=com.esri.survey123&hl=en_US) by Esri is a popular GIS app. Unfortunately it does not allow Bluetooth connections to 3rd party RTK devices. To enable a connection to a SparkFun RTK device, a [mock location](connecting_bluetooth.md/#enable-mock-location) and an intermediary app such as [GNSS Master](gis_software_android.md/#gnss-master) or [Lefebure](gis_software_android.md/#lefebure) is needed. + +Once a [mock location](connecting_bluetooth.md/#enable-mock-location) provider is setup, open Survey123. + +![Survey123 Splash]() + +For the purposes of this demonstration, click *Continue without signing in*. + +![Main window]() + +Select the satellite icon in the upper right corner. + +![Location status showing RTK Fix]() + +If the mock location provider app is running, you should see the Lat/Lon/Alt from the RTK device. In the above image, RTK Fix is achieved with 0.033ft (10mm) accuracy. Click on the map icon. + +![Survey123 Map]() + +The location of the receiver is shown on a map. With GNSS Master or Lefebure providing the mock location to the phone, Survey123 will have a very precise GNSS location and data collection can begin. + +## Diamond Maps + +[Diamond Maps](https://diamondmaps.com/) is a great solution for utilities and municipalities. $20/month GIS software with many great features. Get the Android app [here](https://play.google.com/store/apps/details?id=com.diamondmaps.OfflineApp&hl=en_US). + +Be sure your device is [paired over Bluetooth](connecting_bluetooth.md#android). + +![Home Screen]() + +From the Home Screen, click on the 'hamburger' settings button in the top left corner. + +![Settings Menu]() + +Select **GPS Status**. + +![Click GPS Source]() + +Click on the **Select a GPS Source** box and select the RTK device that was previously paired with. + +![GPS details]() + +Once a receiver is selected, its status will be shown in the GPS Setup window. Additionally, an NTRIP Client is available for corrections. + +**NTRIP Client** + +If you’re using a serial radio to connect a Base to a Rover for your correction data, or if you're using the RTK Facet L-Band with built-in corrections, you can skip this part. + +![NTRIP Settings]() + +From this window, an NTRIP Client can be configured. Enter your NTRIP Caster information then click on **START**. Click *Close* to exit out to the main window. + +![RTK Fix at SparkFun]() + +*0.03ft accuracy shown in green* + +Closing the GPS Source window will show the map as well as the relative accuracy in feet. + +## Field Genius + +[Field Genius for Android](https://www.microsurvey.com/products/fieldgenius-for-android/) is another good solution, albeit a lot more expensive than free. + +Be sure your device is [paired over Bluetooth](connecting_bluetooth.md#android). + +![Main Menu](img/FieldGenius/Field%20Genius%202.png) + +From the Main Menu open `Select Instrument`. + +![Add Profile](img/FieldGenius/Field%20Genius%203.png) + +Click the 'Add Profile' button. + +![New Instrument Profile](img/FieldGenius/Field%20Genius%204.png) + +Click `GNSS Rover` and select *NMEA* as the Make. Set your Profile Name to something memorable like 'RTK-Express' then click the 'Create' button. + +![Set up communication](img/FieldGenius/Field%20Genius%205.png) + +Click on 'SET UP COMMUNICATION'. + +![Bluetooth Search Button](img/FieldGenius/Field%20Genius%207.png) + +From the Bluetooth communication page, click the 'Search' button. + +![List of paired Bluetooth devices](img/FieldGenius/Field%20Genius%206.png) + +You will be shown a list of paired devices. Select the RTK device you'd like to connect to then click 'Connect'. The RTK device will connect and the MAC address shown on the RTK device OLED will change to the Bluetooth icon indicating a link is open. + +**NTRIP Client** + +If you’re using a serial radio to connect a Base to a Rover for your correction data, or if you're using the RTK Facet L-Band with built-in corrections, you can skip this part. + +![Set up corrections](img/FieldGenius/Field%20Genius%208.png) + +We need to send RTCM correction data from the phone back to the RTK device so that it can improve its fix accuracy. Your phone can be the radio link! Click on 'SET UP CORRECTIONS'. + +![RTK via Internet](img/FieldGenius/Field%20Genius%209.png) + +Click on 'RTK via Internet' then 'SET UP INTERNET', then 'Done'. + +![Set up NTRIP data source](img/FieldGenius/Field%20Genius%2010.png) + +Click on 'SET UP DATA SOURCE'. + +![Adding a new source](img/FieldGenius/Field%20Genius%2011.png) + +Click 'Add New Source'. + +![NTRIP Credential Entry](img/FieldGenius/Field%20Genius%2012.png) + +Enter your NTRIP Caster credentials and click 'DONE'. + +What's an NTRIP Caster? In a nutshell, it's a server that is sending out correction data every second. There are thousands of sites around the globe that calculate the perturbations in the ionosphere and troposphere that decrease the accuracy of GNSS accuracy. Once the inaccuracies are known, correction values are encoded into data packets in the RTCM format. You, the user, don't need to know how to decode or deal with RTCM, you simply need to get RTCM from a source within 10km of your location into the RTK device. The NTRIP client logs into the server (also known as the NTRIP caster) and grabs that data, every second, and sends it over Bluetooth to the RTK device. + +Don't have access to an NTRIP Caster? You can use a 2nd RTK product operating in Base mode to provide the correction data. Check out [Creating a Permanent Base](permanent_base.md). If you're the DIY sort, you can create your own low-cost base station using an ESP32 and a ZED-F9P breakout board. Check out [How to Build a DIY GNSS Reference Station](https://learn.sparkfun.com/tutorials/how-to-build-a-diy-gnss-reference-station). If you'd just like a service, [Syklark](https://www.swiftnav.com/skylark) provides RTCM coverage for $49 a month (as of writing) and is extremely easy to set up and use. Remember, you can always use a 2nd RTK device in *Base* mode to provide RTCM correction data but it will be less accurate than a fixed position caster. + +![Selecting data source](img/FieldGenius/Field%20Genius%2011.png) + +Click 'My NTRIP1' then 'Done' and 'Connect'. + +You will then be presented with a list of Mount Points. Select the mount point you'd like to use then click 'Select' then 'Confirm'. + +Select 'Done' then from the main menu select 'Survey' to begin using the device. + +![Surveying Screen](img/FieldGenius/Field%20Genius%201.png) + +Now you can begin using the SparkFun RTK device with Field Genius. + +## GNSS Master + +[GNSS Master](https://play.google.com/store/apps/details?id=com.gnssmaster&hl=en_US) is a great utility when a given GIS app does not have an NTRIP Client or a way to connect over Bluetooth. GNSS Master connects to a RTK device over Bluetooth (or Bluetooth BLE) as well as any correction source (NTRIP, PointPerfect, even USB Serial), and then acts as the phone's location using [Mock Location](connecting_bluetooth.md/#enable-mock-location). + +**Note:** Most GIS apps will not need GNSS Master or Mock Location enabled and this section can be skipped. + +Read how to [Enable Mock Location](connecting_bluetooth.md/#enable-mock-location). + +![GNSS Master main menu]() + +From the GNSS Master main screen, select **GNSS Receiver Connection**. + +![GNSS Receiver Selection]() + +Pick the RTK device to connect to from the list, then click *Connect*. The **Data Rate** should increase indicating data flowing from the RTK device to the GNSS Master app. Click the back button to return to the main screen. + +![Correction Input]() + +Select **Correction Input** to setup an NTRIP Client. + +![Corrections List]() + +This is one of the powerful features of GNSS Master - multiple connections can be entered. This is helpful if you regularly switch between locations or NTRIP Casters and your GIS software only allows entry of a single NTRIP source. GNSS Master supports corrections from NTRIP Casters but also PointPerfect and a direct serial connection to a GNSS receiver. This can be really helpful in advanced setups. + +![NTRIP Client information]() + +Enter your NTRIP Client information then click **SAVE**. + +![Data from Caster]() + +Once connected the *Data Rate* should increase above 0 bytes per second. Return to the home screen by hitting the back button. + +![Enable Mock Location]() + +Enable mock location. If GNSS Master throws an error, re-enable GNSS Master as your [Mock Location provider](connecting_bluetooth.md/#enable-mock-location) in Developer Options. + +Once enabled, any GIS app that selects 'Internal' or 'Phone Location' as its source will instead be fed the high precision NMEA being generated by the RTK device connected over Bluetooth. + +## Lefebure + +[Lefebure NTRIP Client](https://play.google.com/store/apps/details?id=com.lefebure.ntripclient&hl=en_US) is the *original* app for getting correction from an NTRIP caster and down over Bluetooth. It's an oldie but a goodie. + +**Note:** Most GIS apps will not need Lefebure or Mock Location enabled and this section can be skipped. + +The problem is that if Lefebure is connected to the RTK device providing RTCM corrections over Bluetooth, then other GIS applications cannot use the same Bluetooth connection at the same time. That's where mock locations save the day. Lefebure can be setup to take over or 'mock' the GPS location being reported by the phone. Nearly all GIS apps can use the phone's GPS location. So if the phone's location is magically super precise, then Lefebure can be the NTRIP Client and data provide, and your GIS app is none the wiser, and uses the phone's location. + +Read how to [Enable Mock Location](connecting_bluetooth.md/#enable-mock-location). + +![LEfebure settings]() + +Once mock locations are enabled, click on the *Settings* gear in the top left corner. + +![NTRIP Settings]() + +If needed, an NTRIP Client can be setup to provide corrections over Bluetooth to the RTK device. + +![NTRIP Client Settings]() + +Enter the Caster information and hit the back button. + +![Receive Settings]() + +Select *Receiver Settings*. + +![Bluetooth and Mock location enable]() + +Select the RTK device that has been paired over Bluetooth. Also enable Mock Locations. Hit the back button to return to the main screen. + +![alt text]() + +Press the **Connect** button. The app will connect to the NTRIP Caster. Now, any GIS app that selects 'Internal' or 'Phone Location' as its source will instead be fed the high precision NMEA being generated by the RTK device connected over Bluetooth. + +## QField + +![Opening page of QField](img/QField/SparkFun%20RTK%20QField%20-%20Open%20Project.png) + +[QField](https://docs.qfield.org/get-started/) is a free GIS Android app that runs QGIS. + +![NMEA message configuration](img/WiFi Config/RTK_Surveyor_-_WiFi_Config_-_GNSS_Config_Messages.jpg) + +*The 'Reset to Surveying Defaults' button* + +First, configure the RTK device to output *only* NMEA messages. QField currently does not correctly parse other messages such as RAWX or RTCM so these will interfere with communication if they are enabled. + +These RTK device settings can be found under the [Messages menu](menu_messages.md) through the [WiFi config page](configure_with_wifi.md) or through the [Serial Config menu](configure_with_serial.md). + +![QField creating a project](img/QField/SparkFun%20RTK%20QField%20-%20Create%20Project.png) + +Create an account and project on [QFieldCloud](https://qfield.cloud/). This project will be synchronized and viewable on the QField app. + +![Open Test Project](img/QField/SparkFun%20RTK%20QField%20-%20Refresh%20Project.png) + +*Refresh Projects button* + +Once the project is created, press the Refresh projects list button to update the list. Then select your project. + +![Hamburger Menu](img/QField/SparkFun%20RTK%20QField%20-%20Open%20Settings.png) + +*'Hamburger' menu in upper right corner* + +Press the icon in the top left corner of the app to open the project settings. + +![Project Settings Menu](img/QField/SparkFun%20RTK%20QField%20-%20Project%20Settings%202.png) + +*Project settings* + +From the project settings menu, press the gear icon to open the device settings dropdown menu. + +![Project Settings Submenu](img/QField/SparkFun%20RTK%20QField%20-%20Project%20Settings.png) + +*Project settings submenu* + +From the submenu, select 'Settings'. + +![Position Menu](img/QField/SparkFun%20RTK%20QField%20-%20Select%20Positioning%20Devce.png) + +*Positioning Menu* + +Select the Positioning Menu. Then, with your RTK device on and in normal mode (not AP Config) press the Scan button in the QField app to update the dropdown list of available Bluetooth devices. If your device is not detected, be sure you've [paired your cellphone or laptop with Bluetooth](connecting_bluetooth.md). + +Once connected exit out of the menus and see position information within your project. + +## Survey Master + +[Survey Master](https://www.comnavtech.com/companyfile/4/) by ComNam / SinoGNSS is an Android-based option. The download location can vary so google 'Survey Master ComNav Download' if the link above fails. Download the zip file, send the APK file to a phone and install the program. + +![Startup wizard](img/SurveyMaster/SparkFun%20RTK%20Survey%20Master%20-%2001.png) + +By default, a wizard will guide you through the setup. The Project step will ask you for the name of the project, the datum, etc. + +![Connection Setup +](img/SurveyMaster/SparkFun%20RTK%20Survey%20Master%20-%2027.png) + +Next select your connection. + +![Connection specifics](img/SurveyMaster/SparkFun%20RTK%20Survey%20Master%20-%2029.png) + +For the Device Model select 'NMEA Device'. + +![TOP106 Antenna Parameters](img/SurveyMaster/SparkFun%20RTK%20Survey%20Master%20-%2002.png) + +If you are just getting started, use one of the default antenna types. If you are attempting to get sub-centimeter accuracy, enter the parameters of your antenna and add it. Above are the NGS-certified parameters for the [TOP106 antenna](https://www.sparkfun.com/products/17751). + +![List of Bluetooth devices](img/SurveyMaster/SparkFun%20RTK%20Survey%20Master%20-%2003.png) + +Click the 'Target Device' option to get a list of available Bluetooth devices. Make sure your RTK product is on and you should see the device. In this example 'Express Rover-B022' was chosen. + +To finish, click 'Connect'. You should see the Bluetooth MAC address on your RTK product change to the Bluetooth icon indicating a connection is established. + +![Rover Work Mode Configuration](img/SurveyMaster/SparkFun%20RTK%20Survey%20Master%20-%2004.png) + +Next is configuring the 'Work mode' of the device. The step is where we set up our NTRIP correction source. + +![Empty mode list](img/SurveyMaster/SparkFun%20RTK%20Survey%20Master%20-%2005.png) + +Click 'Add' to create a new work mode. + +![NTRIP Client](img/SurveyMaster/SparkFun%20RTK%20Survey%20Master%20-%2009.png) + +Shown above, we configure the NTRIP Client. Survey Master calls this the 'SinoGNSS' Protocol. Click on the three bars to the right of 'Server' to enter a new NTRIP connection. + +![List of Services](img/SurveyMaster/SparkFun%20RTK%20Survey%20Master%20-%2030.png) + +Here you can add different NTRIP Caster providers. If you're using RTK2Go be sure to enter your contact email into the user name. + +![Server and mount point selected](img/SurveyMaster/SparkFun%20RTK%20Survey%20Master%20-%2009.png) + +Return to the 'Datalink type' window and select the Server you just entered. Re-enter the server address and port for your NTRIP Caster. Once complete, click on the down-pointing arrow. This will ping the Caster and obtain the mount point table. Select your mount point. + +![Rover with work list in place](img/SurveyMaster/SparkFun%20RTK%20Survey%20Master%20-%2011.png) + +Select the newly created work mode and press the 'Apply' button. + +![Connecting to service](img/SurveyMaster/SparkFun%20RTK%20Survey%20Master%20-%2033.png) + +Survey Master will attempt to connect to your specified RTK corrections source (NTRIP Caster). Upon success, you will be located on the Project menu. + +Survey Master expects many more NMEA sentences than most GIS software. We must enable some additional messages on the RTK device to correctly communicate with Survey Master. + +![Configured NMEA messages](img/SurveyMaster/SparkFun%20RTK%20Survey%20Master%20-%2026%20.jpg) + +Note above: There are 9 enabled messages and GSV is set to '1'. + +Connect to the RTK device either over [WiFi AP config](/configure_with_wifi/) or via [Serial](/configure_with_serial/). Above is shown the serial method. + +Open a terminal at 115200bps and press a key to open the serial configuration menu. Press '2' for GNSS Messages, press '1' for NMEA messages, now be sure to enable 9 messages to a rate of 1: + +* GGA +* GLL +* GRS +* GSA +* GST +* GSV +* RMC +* VTG +* ZDA + +Once complete, press x until you exit the serial menus. Now we may return to Survey Master. + +![Survey Master showing the location of RTK Express](img/SurveyMaster/SparkFun%20RTK%20Survey%20Master%20-%2025.png) + +Click on the 'Survey' menu and then 'Topo Survey'. Above we can see a device with RTK float, and 117mm horizontal positional accuracy. + +Known Issues: + +* Survey Master parses the GxGSV sentence improperly and will only indicate GPS satellites even though the fix solution is using all satellites. + +![NMEA Sentences](img/SurveyMaster/SparkFun%20RTK%20Survey%20Master%20-%2015.png) + +To verify the NMEA sentences are being delivered correctly, Survey Master has a built-in tool. Select the Device->Rover->More->'H-Terminal'. + +## SurPad + +[SurPad](https://surpadapp.com/) is an Android app available as a free trial for 30-days. It's loaded as an APK (rather than through Google Play). + +Be sure your RTK device has been [paired over Bluetooth](connecting_bluetooth.md#android) to your phone. + +![SurPad Home Screen]() + +*SurPad Home Screen* + +Create a project and get to the home screen. Shown above, click on the GNSS receiver icon. + +![SurPad connecting over Bluetooth]() + +*SurPad connecting over Bluetooth* + +Set the **Device manufacturer** to *Other*, **Device type** to *RTK(NMEA0183)*, and **Communication Mode** to *Bluetooth*. Select the SparkFun RTK device that you would like to connect to on the **Paired Devices** list and then click *Connect*. + +Once connected to the device a *Debug* button will appear. This is one of the nice features of SurPad: Running debug will allow you to inspect the NMEA coming across the link. + +Once done, press the back arrow (top left corner) to return to the home screen. + +![SurPad Point Survey map]() + +*SurPad Point Survey map* + +Above: From the home screen press the **Survey** button at the bottom, then **Point Survey** to bring up the map. + +In the top left corner, press the green hamburger + cell phone icon. This will open the NTRIP settings. + +![SurPad Data Link NTRIP Configuration]() + +*SurPad Data Link NTRIP Configuration* + +Change the **Connect Mode** from *TCP Client* to *NTRIP*. If you are unable to edit or change the **Connect Mode** from TCP Client be sure the TCP Client is stopped by pressing the *Stop* button in the lower left corner (located in the same spot as the highlighted *Start*). + +![SurPad NTRIP Connection]() + +*SurPad NTRIP Connection* + +Enter the information for your NTRIP caster. In the above example, we are connected to the SparkFun base station on RTK2Go. For RTK2Go you will need to enter a valid email address for a user name but a password is not required. + +Click on *Start* and you should see the 'Receive data' progress bar (highlighted above) increase each second indicating a connection. Once complete, press 'Apply' to return to the map. + +![SurPad with RTK Fix]() + +*SurPad with RTK Fix* + +Above: After a few moments, the RTK device should move to RTK Float, then RTK Fix. You can see the age of the RTCM data in the upper bar, along with the horizontal (23mm) and vertical (31mm) accuracy estimates. Now you can begin taking points. + +## SurvPC + +Be sure your device is [paired over Bluetooth](connecting_bluetooth.md#windows). + +![Equip Sub Menu](img/SurvPC/SparkFun%20RTK%20Software%20-%20SurvPC%20Equip%20Menu.jpg) + +*Equip Sub Menu* + +Select the *Equip* sub menu then `GPS Rover` + +![Select NMEA GPS Receiver](img/SurvPC/SparkFun%20RTK%20Software%20-%20SurvPC%20Rover%20NMEA.jpg) + +*Select NMEA GPS Receiver* + +From the drop down, select `NMEA GPS Receiver`. + +![Select Model: DGPS](img/SurvPC/SparkFun%20RTK%20Software%20-%20SurvPC%20Rover%20DGPS.jpg) + +*Select Model: DGPS* + +Select DGPS if you'd like to connect to an NTRIP Caster. If you are using the RTK Facet L-Band, or do not need RTK fix type precision, leave the model as Generic. + +![Bluetooth Settings](img/SurvPC/SparkFun%20RTK%20Software%20-%20SurvPC%20Rover%20Comms.jpg) + +*Bluetooth Settings Button* + +From the `Comms` submenu, click the Blueooth settings button. + +![SurvPC Bluetooth Devices](img/SurvPC/SparkFun%20RTK%20Software%20-%20SurvPC%20Rover%20Find%20Device.jpg) + +*SurvPC Bluetooth Devices* + +Click `Find Device`. + +![List of Paired Bluetooth Devices](img/SurvPC/SparkFun%20RTK%20Software%20-%20SurvPC%20Rover%20Select%20Bluetooth%20Device.jpg) + +*List of Paired Bluetooth Devices* + +You will be shown a list of devices that have been paired. Select the RTK device you want to connect to. + +![Connect to Device](img/SurvPC/SparkFun%20RTK%20Software%20-%20SurvPC%20Rover%20Select%20Bluetooth%20Device%20With%20MAC.jpg) + +*Connect to Device* + +Click the `Connect Bluetooth` button, shown in red in the top right corner. The software will begin a connection to the RTK device. You'll see the MAC address on the RTK device changes to the Bluetooth icon indicating it's connected. + +If SurvPC detects NMEA, it will report a successful connection. + +![Receiver Submenu](img/SurvPC/SparkFun%20RTK%20Software%20-%20SurvPC%20Rover%20Receiver.jpg) + +*Receiver Submenu* + +You are welcome to enter the ARP (antenna reference point) and surveying stick length for your particular setup. + +**NTRIP Client** + +Note: If you are using a radio to connect Base to Rover, or if you are using the RTK Facet L-Band you do not need to set up NTRIP; the device will achieve RTK fixes and output extremely accurate location data by itself. But if L-Band corrections are not available, or you are not using a radio link, the NTRIP Client can provide corrections to this Rover. + +![RTK Submenu](img/SurvPC/SparkFun%20RTK%20Software%20-%20SurvPC%20NTRIP%20Client.jpg) + +*RTK Submenu* + +If you selected 'DGPS' as the Model type, the RTK submenu will be shown. This is where you give the details about your NTRIP Caster such as your mount point, user name/pw, etc. For more information about creating your own NTRIP mount point please see [Creating a Permanent Base](permanent_base.md) + +Enter your NTRIP Caster credentials and click connect. You will see bytes begin to transfer from your phone to the RTK device. Within a few seconds, the RTK device will go from ~300mm accuracy to 14mm. Pretty nifty, no? + +What's an NTRIP Caster? In a nutshell, it's a server that is sending out correction data every second. There are thousands of sites around the globe that calculate the perturbations in the ionosphere and troposphere that decrease the accuracy of GNSS accuracy. Once the inaccuracies are known, correction values are encoded into data packets in the RTCM format. You, the user, don't need to know how to decode or deal with RTCM, you simply need to get RTCM from a source within 10km of your location into the RTK device. The NTRIP client logs into the server (also known as the NTRIP caster) and grabs that data, every second, and sends it over Bluetooth to the RTK device. + +Don't have access to an NTRIP Caster? You can use a 2nd RTK product operating in Base mode to provide the correction data. Checkout [Creating a Permanent Base](permanent_base.md). If you're the DIY sort, you can create your own low-cost base station using an ESP32 and a ZED-F9P breakout board. Check out [How to](https://learn.sparkfun.com/tutorials/how-to-build-a-diy-gnss-reference-station) Build a DIY GNSS Reference Station](https://learn.sparkfun.com/tutorials/how-to-build-a-diy-gnss-reference-station). If you'd just like a service, [Syklark](https://www.swiftnav.com/skylark) provides RTCM coverage for $49 a month (as of writing) and is extremely easy to set up and use. Remember, you can always use a 2nd RTK device in *Base* mode to provide RTCM correction data but it will be less accurate than a fixed position caster. + +Once everything is connected up, click the Green check in the top right corner. + +![Storing Points](img/SurvPC/SparkFun%20RTK%20Software%20-%20SurvPC%20Survey.jpg) + +*Storing Points* + +Now that we have a connection, you can use the device, as usual, storing points and calculating distances. + +![SurvPC Skyplot](img/SurvPC/SparkFun%20RTK%20Software%20-%20SurvPC%20Skyplot.jpg) + +*SurvPC Skyplot* + +Opening the Skyplot will allow you to see your GNSS details in real-time. + +If you are a big fan of SurvPC please contact your sales rep and ask them to include SparkFun products in their Manufacturer drop-down list. + +## SW Maps + +The best mobile app that we’ve found is the powerful, free, and easy-to-use [SW Maps](https://play.google.com/store/apps/details?id=np.com.softwel.swmaps) by Softwel. It is compatible with Android and iOS, either phone or tablet with Bluetooth. What makes SW Maps truly powerful is its built-in NTRIP client. This is a fancy way of saying that we’ll be showing you how to get RTCM correction data over the cellular network. + +Be sure your device is [paired over Bluetooth](connecting_bluetooth.md#android). + +![List of BT Devices in SW Maps](img/SWMaps/SparkFun%20RTK%20SWMaps%20Bluetooth%20Connect.png) + +*List of available Bluetooth devices* + +From SW Map's main menu, select *Bluetooth GNSS*. This will display a list of available Bluetooth devices. Select the Rover or Base you just paired with. If you are taking height measurements (altitude) in addition to position (lat/long) be sure to enter the height of your antenna off the ground including any [ARP offsets](https://geodesy.noaa.gov/ANTCAL/FAQ.xhtml#faq4) of your antenna (this should be printed on the side). + +Click on 'CONNECT' to open a Bluetooth connection. Assuming this process takes a few seconds, you should immediately have a location fix. + +![SW Maps with RTK Fix](img/SWMaps/SparkFun%20RTK%20SWMaps%20GNSS%20Status.png) + +*SW Maps with RTK Fix* + +You can open the GNSS Status sub-menu to view the current data. + +**NTRIP Client** + +If you’re using a serial radio to connect a Base to a Rover for your correction data, or if you're using the RTK Facet L-Band with built-in corrections, you can skip this part. + +We need to send RTCM correction data from the phone back to the RTK device so that it can improve its fix accuracy. This is the amazing power of the SparkFun RTK products and SW Maps. Your phone can be the radio link! From the main SW Maps menu select NTRIP Client. Not there? Be sure the 'SparkFun RTK' instrument was automatically selected connecting. Disconnect and change the instrument to 'SparkFun RTK' to enable the NTRIP Connection option. + +![SW Maps NTRIP Connection menu](img/SWMaps/SparkFun_RTK_Surveyor_-_SW_Maps_NTRIP_Connection.jpg) + +*NTRIP Connection - Not there? Be sure to select 'SparkFun RTK' was selected as the instrument* + +![SW Maps NTRIP client](img/SWMaps/SW_Maps_-_NTRIP_Client.jpg) + +*Connecting to an NTRIP Caster* + +Enter your NTRIP Caster credentials and click connect. You will see bytes begin to transfer from your phone to the RTK device. Within a few seconds, the RTK device will go from ~300mm accuracy to 14mm. Pretty nifty, no? + +Once you have a full RTK fix you'll notice the location bubble in SW Maps turns green. Just for fun, rock your rover monopole back and forth on a fixed point. You'll see your location accurately reflected in SW Maps. Millimeter location precision is a truly staggering thing. + +## Vespucci + +[Vespucci](https://play.google.com/store/apps/details?id=de.blau.android&hl=en_US&gl=US) is an Open Street Map editor for Android. + +This software requires the RTK device to connect over TCP. Be sure you have a local WiFi network entered into the [WiFi Config menu](menu_wifi.md), have a TCP Client or Server enabled, and have noted the TCP port (it's 2947 by default). + +![Vespucci Gear Button](img/Vespucci/SparkFun%20RTK%20Vespucci%20-%20Main%20Gear.png) + +With a map open, select the gear icon on the bottom bar. + +![Vespucci Preferences menu](img/Vespucci/SparkFun%20RTK%20Vespucci%20-%20Preferences.png) + +From the Preferences menu, scroll to the bottom and select 'Advanced Preferences'. + +![Preferences menu showing Location Settings](img/Vespucci/SparkFun%20RTK%20Vespucci%20-%20Location%20Settings.png) + +Select **Location settings**. + +![GPS source menu](img/Vespucci/SparkFun%20RTK%20Vespucci%20-%20GPS%20Source.png) + +Select **GPS/GNSS source**. Select **NMEA from TCP client**. TCP server is also supported. + +![Vespucci NMEA network source menu](img/Vespucci/SparkFun%20RTK%20Vespucci%20-%20NMEA%20Network%20Source.png) + +Select **NMEA network source**. Enter the IP address and TCP port of the RTK device. The IP address can be found by opening a serial terminal while connected to WiFi (it is reported every few seconds). The TCP port is entered into the [WiFi Config menu](menu_wifi.md). + +![Vespucci showing location on map](img/Vespucci/SparkFun%20RTK%20Vespucci%20-%20Point%20on%20Map.png) + +Close all menus and you should see your location within Vespucci. + +## Other GIS Packages + +Hopefully, these examples give you an idea of how to connect the RTK product line to most any GIS software. If there is other GIS software that you'd like to see configuration information about, please open an issue on the [RTK Firmware repo](https://github.com/sparkfun/SparkFun_RTK_Everywhere_Firmware/issues) and we'll add it. + +## What's an NTRIP Caster? + +In a nutshell, it's a server that is sending out correction data every second. There are thousands of sites around the globe that calculate the perturbations in the ionosphere and troposphere that decrease the accuracy of GNSS accuracy. Once the inaccuracies are known, correction values are encoded into data packets in the RTCM format. You, the user, don't need to know how to decode or deal with RTCM, you simply need to get RTCM from a source within 10km of your location into the RTK device. The NTRIP client logs into the server (also known as the NTRIP caster) and grabs that data, every second, and sends it over Bluetooth to the RTK device. + +## Where do I get RTK Corrections? + +Be sure to see [Correction Sources](correction_sources.md). + +Don't have access to an NTRIP Caster or other RTCM correction source? There are a few options. + +The [SparkFun RTK Facet L-Band](https://www.sparkfun.com/products/20000) gets corrections via an encrypted signal from geosynchronous satellites. This device gets RTK Fix without the need for a WiFi or cellular connection. + +Also, you can use a 2nd RTK product operating in Base mode to provide the correction data. Check out [Creating a Permanent Base](permanent_base.md). + +If you're the DIY sort, you can create your own low-cost base station using an ESP32 and a ZED-F9P breakout board. Check out [How to Build a DIY GNSS Reference Station](https://learn.sparkfun.com/tutorials/how-to-build-a-diy-gnss-reference-station). + +There are services available as well. [Syklark](https://www.swiftnav.com/skylark) provides RTCM coverage for $49 a month (as of writing) and is extremely easy to set up and use. [Point One](https://app.pointonenav.com/trial?utm_source=sparkfun) also offers RTK NTRIP service with a free 14 day trial and easy to use front end. diff --git a/docs/gis_software_ios.md b/docs/gis_software_ios.md new file mode 100644 index 000000000..4c41a186e --- /dev/null +++ b/docs/gis_software_ios.md @@ -0,0 +1,362 @@ +# iOS + +Surveyor: ![Feature Supported](img/Icons/GreenDot.png) / Express: ![Feature Supported](img/Icons/GreenDot.png) / Express Plus: ![Feature Supported](img/Icons/GreenDot.png) / Facet: ![Feature Supported](img/Icons/GreenDot.png) / Facet L-Band: ![Feature Supported](img/Icons/GreenDot.png) / Reference Station: ![Feature Supported](img/Icons/GreenDot.png) + +There are a variety of 3rd party apps available for GIS and surveying for [Android](gis_software_android.md), [iOS](gis_software_ios.md), and [Windows](gis_software_windows.md). We will cover a few examples below that should give you an idea of how to get the incoming NMEA data into the software of your choice. + +The software options for Apple iOS are much more limited because Apple products do not support Bluetooth SPP. That's ok! The SparkFun RTK products support additional connection options including TCP and Bluetooth Low Energy (BLE). + +## ArcGIS Field Maps + +For reasons unknown, Esri removed TCP support from Field Maps for iOS and is therefore not usable by SparkFun RTK devices at this time. + +If you must use iOS, checkout [SW Maps](gis_software_ios.md/#sw-maps), [ArcGIS QuickCapture](gis_software_ios.md/#arcgis-quickcapture), or [ArcGIS Survey123](gis_software_ios.md/#arcgis-survey123). + +[Field Maps for Android](gis_software_android.md/#arcgis-field-maps) is supported. +## ArcGIS QuickCapture + +![ArcGIS QuickCapture splash screen]() + +[ArcGIS QuickCapture](https://apps.apple.com/us/app/arcgis-quickcapture/id1451433781) is a popular offering from Esri that works well with SparkFun RTK products. + +ArcGIS QuickCapture connects to the RTK device over TCP. In other words, the RTK device needs to be connected to the same WiFi network as the device running QuickCapture. Generally, this is an iPhone or iPad operating as a hotspot. + +**Note:** The iOS hotspot defaults to 5.5GHz. This must be changed to 2.4GHz. Please see [Hotspot Settings](gis_software_ios.md/#hotspot-settings). + +![WiFi network setup via Web Config]() + +![Adding WiFi network to settings]() + +The RTK device must use WiFi to connect to the iPhone or iPad. In the above image, the device will attempt to connect to *iPhone* (a cell phone hotspot) when WiFi is needed. + +![PVT Server Enabled on port 2948]() + + +Next, the RTK device must be configured as a *PVT Server*. The default port of 2948 works well. See [TCP/UDP Menu](menu_tcp_udp.md) for more information. + +![RTK device showing IP address]() + +Once the RTK device connects to the WiFi hotspot, its IP address can be found in the [System Menu](menu_system.md). This is the number that needs to be entered into QuickCapture. You can now proceed to the QuickCapture app to set up the software connection. + +![Main screen]() + +From the main screen, press the hamburger icon in the top left corner. + +![Settings button]() + +Press the **Settings** button. + +![Location Provider button]() + +Select the **Location Provider** option. + +Select **Via Network**. + +![TCP Network Information]() + +Enter the IP address and port previously obtained from the RTK device and click **ADD**. + +![List of providers showing TCP connection]() + +That provider should now be shown connected. + +![Browse project button]() + +From the main screen, click on the plus in the lower left corner and then **BROWSE PROJECTS**. + +![List of projects]() + +For this example, add the BioBlitz project. + +![GPS Accuracy and map in BioBlitz project]() + +Above, we can see the GPS accuracy is better than 1ft. Click on the map icon in the top right corner. + +![QuickCapture map]() + +From the map view, we can see our location with very high accuracy. We can now begin gathering point information with millimeter accuracy. + +## ArcGIS Survey123 + +![ArcGIS Survey123 Home Screen]() + +*ArcGIS Survey123 Home Screen* + +[ArcGIS Survey123](https://apps.apple.com/us/app/arcgis-survey123/id993015031) is a popular offering from Esri that works well with SparkFun RTK products. + +ArcGIS Survey123 connects to the RTK device over TCP. In other words, the RTK device needs to be connected to the same WiFi network as the device running ArcGIS. Generally, this is an iPhone or iPad. + +![WiFi network setup via Web Config]() + + +![Adding WiFi network to settings]() + +*Adding WiFi network to settings* + +The RTK device must use WiFi to connect to the data collector. Using a cellular hotspot or cellphone is recommended. In the above image, the device will attempt to connect to *iPhone* (a cell phone hotspot) when WiFi is needed. + +![TCP Server Enabled on port 2948]() + +*TCP Server Enabled on port 2948* + +Next, the RTK device must be configured as a *TCP Server*. The default port of 2948 works well. See [TCP/UDP Menu](menu_tcp_udp.md) for more information. + +![RTK device showing IP address]() + +*RTK device showing IP address* + +Once the RTK device connects to the WiFi hotspot, its IP address can be found in the [System Menu](menu_system.md). This is the number that needs to be entered into ArcGIS Survey123. You can now proceed to the ArcGIS Survey123 app to set up the software connection. + +![ArcGIS Survey123 Home Screen]() + +*ArcGIS Survey123 Home Screen* + +From the home screen, click on the 'hamburger' icon in the upper right corner. + +![ArcGIS Survey123 Settings Menu]() + +*ArcGIS Survey123 Settings Menu* + +From the settings menu, click on the *Settings* gear. + +![ArcGIS Survey123 Settings List]() + +*ArcGIS Survey123 Settings List* + +From the settings list, click on *Location*. + +![ArcGIS Survey123 List of Location Providers]() + +*ArcGIS Survey123 List of Location Providers* + +Click on the *Add location provider*. + +![ArcGIS Survey123 Network Connection Type]() + +*ArcGIS Survey123 Network Connection Type* + +Select *Network*. + +![ArcGIS Survey123 TCP Connection Information]() + +*ArcGIS Survey123 TCP Connection Information* + +Enter the IP address previously found along with the TCP port. Once complete, click *Add*. + +![ArcGIS Survey123 Sensor Settings]() + +*ArcGIS Survey123 Sensor Settings* + +You may enter various sensor-specific settings including antenna height, if desired. To view real-time sensor information, click on the satellite icon in the upper right corner. + +![ArcGIS Survey123 Sensor Data]() + +*ArcGIS Survey123 Sensor Data* + +The SparkFun RTK device's data should now be seen. Click on the *Map* icon to return to the mapping interface. + +![ArcGIS Survey123 Map Interface]() + +*ArcGIS Survey123 Map Interface* + +Returning to the map view, we can now begin gathering point information with millimeter accuracy. + +## QField + +![Opening page of QField]() + +[QField](https://apps.apple.com/us/app/qfield-for-qgis/id1531726814) is a free iOS app that runs QGIS. + +![Modified NMEA messages on RTK Torch]() + +*Modified NMEA messages on RTK Torch* + +First, configure the RTK device to output *only* the following NMEA messages: + +* GPGGA +* GPGSA +* GPGST +* GPGSV + +QField currently does not correctly parse other messages such as **GPRMC**, or **RTCM**. These messages will prevent communication if they are enabled. + +These NMEA message settings can be found under the [Messages menu](menu_messages.md), using the [web config page](configure_with_wifi.md) or the [serial config interface](configure_with_serial.md). + +![WiFi network setup via Web Config]() + + +![Adding WiFi network to settings]() + +*Adding WiFi network to settings* + +Next, the RTK device must use WiFi to connect to the data collector. Using a cellular hotspot or cellphone is recommended. In the above image, the device will attempt to connect to *iPhone* (a cell phone hotspot) when WiFi is needed. + +![TCP Server Enabled on port 9000]() + +*TCP Server Enabled on port 9000* + +Next, the RTK device must be configured as a *TCP Server*. QField uses a default port of 9000 so that is what we recommend using. See [TCP/UDP Menu](menu_tcp_udp.md) for more information. + +![RTK device showing IP address]() + +*RTK device showing IP address* + +Once the RTK device connects to the WiFi hotspot, its IP address can be found in the [System Menu](menu_system.md). This is the number that needs to be entered into QField. You can now proceed to the QField app to set up the software connection. + +![QField Opening Screen]() + +*QField Opening Screen* + +Click on *QFieldCloud projects* to open your project that was previously created on the [QField Cloud](https://app.qfield.cloud/) or skip this step by using one of the default projects (*Bee Farming*, *Wastewater*, etc). + +![QField Main Map]() + +*QField Main Map* + +From the main map, click on the 'hamburger' icon in the upper left corner. + +![QField Settings Gear]() + +*QField Settings Gear* + +Click on the gear to open settings. + +![QField Settings Menu]() + +Click on the *Settings* menu. + +![QField Positioning Menu]() + +*QField Positioning Menu* + +From the *Positioning* menu, click Add. + +![QField Entering TCP Information]() + +*QField Entering TCP Information* + +Select TCP as the connection type. Enter the IP address of the RTK device and the port number. Finally, hit the small check box in the upper left corner (shown in pink above) to close the window. + +Once this information is entered, QField will automatically attempt to connect to that IP and port. + +![QField TCP Connected]() + +*QField TCP Connected* + +Above, we see the port is successfully connected. Exit out of all menus. + +![QField Connected via TCP with RTK Fix]() + +*QField Connected via TCP with RTK Fix* + +Returning to the map view, we see an RTK Fix with 11mm positional accuracy. + +## SW Maps + +SWMaps is available for iOS [here](https://apps.apple.com/us/app/sw-maps/id6444248083). + +Make sure your RTK device is switched on and operating in Rover mode. + +Make sure Bluetooth is enabled on your iOS device Settings. + +The RTK device will not appear in the _OTHER DEVICES_ list. That is OK. + +![iOS Settings Bluetooth](img/iOS/Screenshot1.PNG) + +*iOS Settings Bluetooth* + +Open SWMaps. + +Open or continue a Project if desired. + +SWMaps will show your approximate location based on your iOS device's location. + +![iOS SWMaps Initial Location](img/iOS/Screenshot2.PNG) + +*iOS SWMaps Initial Location* + +Press the 'SWMaps' icon at the top left of the screen to open the menu. + +![iOS SWMaps Menu](img/iOS/Screenshot3.PNG) + +*iOS SWMaps Menu* + +Select Bluetooth GNSS. + +![iOS SWMaps Bluetooth Connection](img/iOS/Screenshot4.PNG) + +*iOS SWMaps Bluetooth Connection* + +Set the **Instrument Model** to **Generic NMEA (Bluetooth LE)**. + +![iOS SWMaps Instrument Model](img/iOS/Screenshot5.PNG) + +*iOS SWMaps Instrument Model* + +Press 'Scan' and your RTK device should appear. + +![iOS SWMaps Bluetooth Scan](img/iOS/Screenshot6.PNG) + +*iOS SWMaps Bluetooth Scan* + +Select (tick) the RTK device and press 'Connect'. + +![iOS SWMaps Bluetooth Connected](img/iOS/Screenshot7.PNG) + +*iOS SWMaps Bluetooth Connected* + +Close the menu and your RTK location will be displayed on the map. + +You can now use the other features of SWMaps, including the built-in NTRIP Client. + +Re-open the menu and select 'NTRIP Client'. + +Enter the details for your NTRIP Caster - as shown in the [SWMaps section above](#sw-maps). + +![iOS SWMaps NTRIP Client](img/iOS/Screenshot8.PNG) + +*iOS SWMaps NTRIP Client* + +Click 'Connect' + +At this point, you should see a Bluetooth Pairing Request. Select 'Pair' to pair your RTK with your iOS device. + +![iOS Bluetooth Pairing](img/iOS/Screenshot9.PNG) + +*iOS Bluetooth Pairing* + +SWMaps will now receive NTRIP correction data from the caster and push it to your RTK over Bluetooth BLE. + +From the SWMaps menu, open 'GNSS Status' to see your position, fix type and accuracy. + +![iOS SWMaps GNSS Status](img/iOS/Screenshot10.PNG) + +*iOS SWMaps GNSS Status* + +If you return to the iOS Bluetooth Settings, you will see that your iOS and RTK devices are now paired. + +![iOS Settings Bluetooth Paired](img/iOS/Screenshot11.PNG) + +*iOS Settings Bluetooth - Paired* + +## Other GIS Packages + +Hopefully, these examples give you an idea of how to connect the RTK product line to most any GIS software. If there is other GIS software that you'd like to see configuration information about, please open an issue on the [RTK Firmware repo](https://github.com/sparkfun/SparkFun_RTK_Everywhere_Firmware/issues) and we'll add it. + +## What's an NTRIP Caster? + +In a nutshell, it's a server that is sending out correction data every second. There are thousands of sites around the globe that calculate the perturbations in the ionosphere and troposphere that decrease the accuracy of GNSS accuracy. Once the inaccuracies are known, correction values are encoded into data packets in the RTCM format. You, the user, don't need to know how to decode or deal with RTCM, you simply need to get RTCM from a source within 10km of your location into the RTK device. The NTRIP client logs into the server (also known as the NTRIP caster) and grabs that data, every second, and sends it over Bluetooth to the RTK device. + +## Where do I get RTK Corrections? + +Be sure to see [Correction Sources](correction_sources.md). + +Don't have access to an NTRIP Caster or other RTCM correction source? There are a few options. + +The [SparkFun RTK Facet L-Band](https://www.sparkfun.com/products/20000) gets corrections via an encrypted signal from geosynchronous satellites. This device gets RTK Fix without the need for a WiFi or cellular connection. + +Also, you can use a 2nd RTK product operating in Base mode to provide the correction data. Check out [Creating a Permanent Base](permanent_base.md). + +If you're the DIY sort, you can create your own low-cost base station using an ESP32 and a ZED-F9P breakout board. Check out [How to Build a DIY GNSS Reference Station](https://learn.sparkfun.com/tutorials/how-to-build-a-diy-gnss-reference-station). + +There are services available as well. [Syklark](https://www.swiftnav.com/skylark) provides RTCM coverage for $49 a month (as of writing) and is extremely easy to set up and use. [Point One](https://app.pointonenav.com/trial?utm_source=sparkfun) also offers RTK NTRIP service with a free 14 day trial and easy to use front end. diff --git a/docs/gis_software_windows.md b/docs/gis_software_windows.md new file mode 100644 index 000000000..f5ff5bda2 --- /dev/null +++ b/docs/gis_software_windows.md @@ -0,0 +1,61 @@ +# Windows + +Torch: ![Feature Supported](img/Icons/GreenDot.png) + +There are a variety of 3rd party apps available for GIS and surveying. We will cover a few examples below that should give you an idea of how to get the incoming NMEA data into the software of your choice. + +## QGIS + +QGIS is a free and open-source geographic information system software for desktops. It's available [here](https://qgis.org/). + +Once the software is installed open QGIS Desktop. + +![View Menu](img/QGIS/SparkFun%20RTK%20QGIS%20-%20View%20Menu.png) + +Open the View Menu, then look for the 'Panels' submenu. + +![Panels submenu](img/QGIS/SparkFun%20RTK%20QGIS%20-%20Enable%20GPS%20Info%20Panel.png) + +From the Panels submenu, enable 'GPS Information'. This will show a new panel on the left side. + +At this point, you will need to enable *TCP Server* mode on your RTK device from the [WiFi Config menu](menu_wifi.md). Once the RTK device is connected to local WiFi QGIS will be able to connect to the given IP address and TCP port. + +![Select GPSD](img/QGIS/SparkFun%20RTK%20QGIS%20-%20GPS%20Panel.png) + +Above: From the subpanel, select 'gpsd'. + +![Entering gpsd specifics](img/QGIS/SparkFun%20RTK%20QGIS%20-%20GPS%20Panel%20Entering%20IP%20and%20port.png) + +Enter the IP address of your RTK device. This can be found by opening a serial connection to the device. The IP address will be displayed every few seconds. Enter the TCP port to use. By default an RTK device uses 2947. + +Press 'Connect'. + +![Viewing location in QGIS](img/QGIS/SparkFun%20RTK%20QGIS%20-%20Location%20on%20Map.png) + +The device location will be shown on the map. To see a map, be sure to enable OpenStreetMap under the XYZ Tiles on the Browser. + +![Connecting over Serial](img/QGIS/SparkFun%20RTK%20QGIS%20-%20Direct%20Serial%20Connection.png) + +Alternatively, a direct serial connection to the RTK device can be obtained. Use a USB cable to connect to the 'CONFIG UBLOX' port on RTK Surveyor/Express/Plus and the single USB C port on the RTK Facet/L-Band. Be sure you have the u-blox driver installed. Then select the appropriate COM port for the u-blox module. See [Configure with Serial](configure_with_serial.md) for more information. + +## Other GIS Packages + +Hopefully, these examples give you an idea of how to connect the RTK product line to most any GIS software. If there is other GIS software that you'd like to see configuration information about, please open an issue on the [RTK Firmware repo](https://github.com/sparkfun/SparkFun_RTK_Everywhere_Firmware/issues) and we'll add it. + +## What's an NTRIP Caster? + +In a nutshell, it's a server that is sending out correction data every second. There are thousands of sites around the globe that calculate the perturbations in the ionosphere and troposphere that decrease the accuracy of GNSS accuracy. Once the inaccuracies are known, correction values are encoded into data packets in the RTCM format. You, the user, don't need to know how to decode or deal with RTCM, you simply need to get RTCM from a source within 10km of your location into the RTK device. The NTRIP client logs into the server (also known as the NTRIP caster) and grabs that data, every second, and sends it over Bluetooth to the RTK device. + +## Where do I get RTK Corrections? + +Be sure to see [Correction Sources](correction_sources.md). + +Don't have access to an NTRIP Caster or other RTCM correction source? There are a few options. + +The [SparkFun RTK Facet L-Band](https://www.sparkfun.com/products/20000) gets corrections via an encrypted signal from geosynchronous satellites. This device gets RTK Fix without the need for a WiFi or cellular connection. + +Also, you can use a 2nd RTK product operating in Base mode to provide the correction data. Check out [Creating a Permanent Base](permanent_base.md). + +If you're the DIY sort, you can create your own low-cost base station using an ESP32 and a ZED-F9P breakout board. Check out [How to Build a DIY GNSS Reference Station](https://learn.sparkfun.com/tutorials/how-to-build-a-diy-gnss-reference-station). + +There are services available as well. [Syklark](https://www.swiftnav.com/skylark) provides RTCM coverage for $49 a month (as of writing) and is extremely easy to set up and use. [Point One](https://app.pointonenav.com/trial?utm_source=sparkfun) also offers RTK NTRIP service with a free 14 day trial and easy to use front end. diff --git a/docs/img/ArcGIS/SparkFun RTK - ArcGIS Location Network.png b/docs/img/ArcGIS/SparkFun RTK - ArcGIS Location Network.png new file mode 100644 index 000000000..39f0b5b52 Binary files /dev/null and b/docs/img/ArcGIS/SparkFun RTK - ArcGIS Location Network.png differ diff --git a/docs/img/ArcGIS/SparkFun RTK - ArcGIS Location Providers.png b/docs/img/ArcGIS/SparkFun RTK - ArcGIS Location Providers.png new file mode 100644 index 000000000..b2808d78f Binary files /dev/null and b/docs/img/ArcGIS/SparkFun RTK - ArcGIS Location Providers.png differ diff --git a/docs/img/ArcGIS/SparkFun RTK - ArcGIS Main Screen.png b/docs/img/ArcGIS/SparkFun RTK - ArcGIS Main Screen.png new file mode 100644 index 000000000..8ad18cc6e Binary files /dev/null and b/docs/img/ArcGIS/SparkFun RTK - ArcGIS Main Screen.png differ diff --git a/docs/img/ArcGIS/SparkFun RTK - ArcGIS Map Interface.png b/docs/img/ArcGIS/SparkFun RTK - ArcGIS Map Interface.png new file mode 100644 index 000000000..d207f4866 Binary files /dev/null and b/docs/img/ArcGIS/SparkFun RTK - ArcGIS Map Interface.png differ diff --git a/docs/img/ArcGIS/SparkFun RTK - ArcGIS Network Information.png b/docs/img/ArcGIS/SparkFun RTK - ArcGIS Network Information.png new file mode 100644 index 000000000..4296adbf7 Binary files /dev/null and b/docs/img/ArcGIS/SparkFun RTK - ArcGIS Network Information.png differ diff --git a/docs/img/ArcGIS/SparkFun RTK - ArcGIS Sensor Data.png b/docs/img/ArcGIS/SparkFun RTK - ArcGIS Sensor Data.png new file mode 100644 index 000000000..9ed643bc8 Binary files /dev/null and b/docs/img/ArcGIS/SparkFun RTK - ArcGIS Sensor Data.png differ diff --git a/docs/img/ArcGIS/SparkFun RTK - ArcGIS Sensor Settings.png b/docs/img/ArcGIS/SparkFun RTK - ArcGIS Sensor Settings.png new file mode 100644 index 000000000..662ce437e Binary files /dev/null and b/docs/img/ArcGIS/SparkFun RTK - ArcGIS Sensor Settings.png differ diff --git a/docs/img/ArcGIS/SparkFun RTK - ArcGIS Settings List.png b/docs/img/ArcGIS/SparkFun RTK - ArcGIS Settings List.png new file mode 100644 index 000000000..8cc9f9b7e Binary files /dev/null and b/docs/img/ArcGIS/SparkFun RTK - ArcGIS Settings List.png differ diff --git a/docs/img/ArcGIS/SparkFun RTK - ArcGIS Settings.png b/docs/img/ArcGIS/SparkFun RTK - ArcGIS Settings.png new file mode 100644 index 000000000..428321943 Binary files /dev/null and b/docs/img/ArcGIS/SparkFun RTK - ArcGIS Settings.png differ diff --git a/docs/img/ArcGIS/SparkFun RTK - ArcGIS TCP Config.png b/docs/img/ArcGIS/SparkFun RTK - ArcGIS TCP Config.png new file mode 100644 index 000000000..fd580cd6b Binary files /dev/null and b/docs/img/ArcGIS/SparkFun RTK - ArcGIS TCP Config.png differ diff --git a/docs/img/ArcGIS/SparkFun RTK - ArcGIS WiFi Hotspot Web Config.png b/docs/img/ArcGIS/SparkFun RTK - ArcGIS WiFi Hotspot Web Config.png new file mode 100644 index 000000000..989fdf8fe Binary files /dev/null and b/docs/img/ArcGIS/SparkFun RTK - ArcGIS WiFi Hotspot Web Config.png differ diff --git a/docs/img/ArcGIS/SparkFun RTK - ArcGIS WiFi Hotspot.png b/docs/img/ArcGIS/SparkFun RTK - ArcGIS WiFi Hotspot.png new file mode 100644 index 000000000..72e8f0da3 Binary files /dev/null and b/docs/img/ArcGIS/SparkFun RTK - ArcGIS WiFi Hotspot.png differ diff --git a/docs/img/ArcGIS/SparkFun RTK - ArcGIS WiFi IP Address.png b/docs/img/ArcGIS/SparkFun RTK - ArcGIS WiFi IP Address.png new file mode 100644 index 000000000..5c9fc14aa Binary files /dev/null and b/docs/img/ArcGIS/SparkFun RTK - ArcGIS WiFi IP Address.png differ diff --git a/docs/img/Bluetooth/SparkFun RTK BEM - Config Menu.png b/docs/img/Bluetooth/SparkFun RTK BEM - Config Menu.png new file mode 100644 index 000000000..866328750 Binary files /dev/null and b/docs/img/Bluetooth/SparkFun RTK BEM - Config Menu.png differ diff --git a/docs/img/Bluetooth/SparkFun RTK BEM - EscapeCharacters.png b/docs/img/Bluetooth/SparkFun RTK BEM - EscapeCharacters.png new file mode 100644 index 000000000..8b245680f Binary files /dev/null and b/docs/img/Bluetooth/SparkFun RTK BEM - EscapeCharacters.png differ diff --git a/docs/img/Bluetooth/SparkFun RTK BEM - Exit BEM.png b/docs/img/Bluetooth/SparkFun RTK BEM - Exit BEM.png new file mode 100644 index 000000000..aa865f711 Binary files /dev/null and b/docs/img/Bluetooth/SparkFun RTK BEM - Exit BEM.png differ diff --git a/docs/img/Bluetooth/SparkFun RTK BEM - Settings Terminal.png b/docs/img/Bluetooth/SparkFun RTK BEM - Settings Terminal.png new file mode 100644 index 000000000..9a916feea Binary files /dev/null and b/docs/img/Bluetooth/SparkFun RTK BEM - Settings Terminal.png differ diff --git a/docs/img/Bluetooth/SparkFun RTK BEM - Settings.png b/docs/img/Bluetooth/SparkFun RTK BEM - Settings.png new file mode 100644 index 000000000..72d37f2c6 Binary files /dev/null and b/docs/img/Bluetooth/SparkFun RTK BEM - Settings.png differ diff --git a/docs/img/Bluetooth/SparkFun RTK BEM - System Output.png b/docs/img/Bluetooth/SparkFun RTK BEM - System Output.png new file mode 100644 index 000000000..b6026460e Binary files /dev/null and b/docs/img/Bluetooth/SparkFun RTK BEM - System Output.png differ diff --git a/docs/img/Bluetooth/SparkFun RTK Bluetooth List Connect.png b/docs/img/Bluetooth/SparkFun RTK Bluetooth List Connect.png new file mode 100644 index 000000000..ea15de0df Binary files /dev/null and b/docs/img/Bluetooth/SparkFun RTK Bluetooth List Connect.png differ diff --git a/docs/img/Bluetooth/SparkFun RTK Software - Add Bluetooth Device 2.jpg b/docs/img/Bluetooth/SparkFun RTK Software - Add Bluetooth Device 2.jpg new file mode 100644 index 000000000..fa2dadaec Binary files /dev/null and b/docs/img/Bluetooth/SparkFun RTK Software - Add Bluetooth Device 2.jpg differ diff --git a/docs/img/Bluetooth/SparkFun RTK Software - Add Bluetooth Device 3.jpg b/docs/img/Bluetooth/SparkFun RTK Software - Add Bluetooth Device 3.jpg new file mode 100644 index 000000000..615d55bc3 Binary files /dev/null and b/docs/img/Bluetooth/SparkFun RTK Software - Add Bluetooth Device 3.jpg differ diff --git a/docs/img/Bluetooth/SparkFun RTK Software - Add Bluetooth Device 4.jpg b/docs/img/Bluetooth/SparkFun RTK Software - Add Bluetooth Device 4.jpg new file mode 100644 index 000000000..0a3820b0d Binary files /dev/null and b/docs/img/Bluetooth/SparkFun RTK Software - Add Bluetooth Device 4.jpg differ diff --git a/docs/img/Bluetooth/SparkFun RTK Software - Add Bluetooth Device.jpg b/docs/img/Bluetooth/SparkFun RTK Software - Add Bluetooth Device.jpg new file mode 100644 index 000000000..241cb23b6 Binary files /dev/null and b/docs/img/Bluetooth/SparkFun RTK Software - Add Bluetooth Device.jpg differ diff --git a/docs/img/Corrections/17239-GHR-04V-S_to_GHR-06V-S_Cable_-_150mm-01.jpg b/docs/img/Corrections/17239-GHR-04V-S_to_GHR-06V-S_Cable_-_150mm-01.jpg new file mode 100644 index 000000000..4ffe2504f Binary files /dev/null and b/docs/img/Corrections/17239-GHR-04V-S_to_GHR-06V-S_Cable_-_150mm-01.jpg differ diff --git a/docs/img/Corrections/19032-SiK_Telemetry_Radio_V3_-_915MHz__100mW-01.jpg b/docs/img/Corrections/19032-SiK_Telemetry_Radio_V3_-_915MHz__100mW-01.jpg new file mode 100644 index 000000000..152d7c50a Binary files /dev/null and b/docs/img/Corrections/19032-SiK_Telemetry_Radio_V3_-_915MHz__100mW-01.jpg differ diff --git a/docs/img/Corrections/Antenna_Semi-Fixed_to_roof - Big.jpg b/docs/img/Corrections/Antenna_Semi-Fixed_to_roof - Big.jpg new file mode 100644 index 000000000..9db2b5d4c Binary files /dev/null and b/docs/img/Corrections/Antenna_Semi-Fixed_to_roof - Big.jpg differ diff --git a/docs/img/Corrections/Antenna_Semi-Fixed_to_roof.jpg b/docs/img/Corrections/Antenna_Semi-Fixed_to_roof.jpg new file mode 100644 index 000000000..0a96667b4 Binary files /dev/null and b/docs/img/Corrections/Antenna_Semi-Fixed_to_roof.jpg differ diff --git a/docs/img/Corrections/Base_Antenna_-_Anchor_installed.jpg b/docs/img/Corrections/Base_Antenna_-_Anchor_installed.jpg new file mode 100644 index 000000000..61ca056f1 Binary files /dev/null and b/docs/img/Corrections/Base_Antenna_-_Anchor_installed.jpg differ diff --git a/docs/img/Corrections/Base_Antenna_-_Antenna_attached.jpg b/docs/img/Corrections/Base_Antenna_-_Antenna_attached.jpg new file mode 100644 index 000000000..81d0122c0 Binary files /dev/null and b/docs/img/Corrections/Base_Antenna_-_Antenna_attached.jpg differ diff --git a/docs/img/Corrections/Base_Antenna_-_Broken_Block.jpg b/docs/img/Corrections/Base_Antenna_-_Broken_Block.jpg new file mode 100644 index 000000000..b14125802 Binary files /dev/null and b/docs/img/Corrections/Base_Antenna_-_Broken_Block.jpg differ diff --git a/docs/img/Corrections/Base_Antenna_-_Drill.jpg b/docs/img/Corrections/Base_Antenna_-_Drill.jpg new file mode 100644 index 000000000..f6daa84d3 Binary files /dev/null and b/docs/img/Corrections/Base_Antenna_-_Drill.jpg differ diff --git a/docs/img/Corrections/Base_Antenna_-_SparkFun_u-blox_Antenna1.jpg b/docs/img/Corrections/Base_Antenna_-_SparkFun_u-blox_Antenna1.jpg new file mode 100644 index 000000000..b4af7676a Binary files /dev/null and b/docs/img/Corrections/Base_Antenna_-_SparkFun_u-blox_Antenna1.jpg differ diff --git a/docs/img/Corrections/Convert_UBX_to_OBS_with_time_22_hour_window.jpg b/docs/img/Corrections/Convert_UBX_to_OBS_with_time_22_hour_window.jpg new file mode 100644 index 000000000..733b3199a Binary files /dev/null and b/docs/img/Corrections/Convert_UBX_to_OBS_with_time_22_hour_window.jpg differ diff --git a/docs/img/Corrections/Convert_UBX_to_OBS_with_time_22_hour_window2.jpg b/docs/img/Corrections/Convert_UBX_to_OBS_with_time_22_hour_window2.jpg new file mode 100644 index 000000000..b7455ede0 Binary files /dev/null and b/docs/img/Corrections/Convert_UBX_to_OBS_with_time_22_hour_window2.jpg differ diff --git a/docs/img/Corrections/Email_from_CSRS_Summary_.jpg b/docs/img/Corrections/Email_from_CSRS_Summary_.jpg new file mode 100644 index 000000000..c63c8d8cc Binary files /dev/null and b/docs/img/Corrections/Email_from_CSRS_Summary_.jpg differ diff --git a/docs/img/Corrections/Old Weather Setup-4.jpg b/docs/img/Corrections/Old Weather Setup-4.jpg new file mode 100644 index 000000000..2cc6e0ab1 Binary files /dev/null and b/docs/img/Corrections/Old Weather Setup-4.jpg differ diff --git a/docs/img/Corrections/PPP_record_time_vs_error.jpg b/docs/img/Corrections/PPP_record_time_vs_error.jpg new file mode 100644 index 000000000..c567a2abe Binary files /dev/null and b/docs/img/Corrections/PPP_record_time_vs_error.jpg differ diff --git a/docs/img/Corrections/RTKCNV_-_OBS_Time_stamps1.jpg b/docs/img/Corrections/RTKCNV_-_OBS_Time_stamps1.jpg new file mode 100644 index 000000000..ba9177a67 Binary files /dev/null and b/docs/img/Corrections/RTKCNV_-_OBS_Time_stamps1.jpg differ diff --git a/docs/img/Corrections/Roof_Enclosure.jpg b/docs/img/Corrections/Roof_Enclosure.jpg new file mode 100644 index 000000000..9d1aa6f77 Binary files /dev/null and b/docs/img/Corrections/Roof_Enclosure.jpg differ diff --git a/docs/img/Corrections/Skylark-Coverage.png b/docs/img/Corrections/Skylark-Coverage.png new file mode 100644 index 000000000..777df67d5 Binary files /dev/null and b/docs/img/Corrections/Skylark-Coverage.png differ diff --git a/docs/img/Corrections/SparkFun NTRIP 4 - UNAVCO Map.png b/docs/img/Corrections/SparkFun NTRIP 4 - UNAVCO Map.png new file mode 100644 index 000000000..8f7451c76 Binary files /dev/null and b/docs/img/Corrections/SparkFun NTRIP 4 - UNAVCO Map.png differ diff --git a/docs/img/Corrections/SparkFun NTRIP 5 - RTK2Go Map.png b/docs/img/Corrections/SparkFun NTRIP 5 - RTK2Go Map.png new file mode 100644 index 000000000..11754534e Binary files /dev/null and b/docs/img/Corrections/SparkFun NTRIP 5 - RTK2Go Map.png differ diff --git a/docs/img/Corrections/SparkFun NTRIP 6 - EUREF Map.png b/docs/img/Corrections/SparkFun NTRIP 6 - EUREF Map.png new file mode 100644 index 000000000..175ae15c0 Binary files /dev/null and b/docs/img/Corrections/SparkFun NTRIP 6 - EUREF Map.png differ diff --git a/docs/img/Corrections/SparkFun NTRIP 7 - Wisconsin Map.png b/docs/img/Corrections/SparkFun NTRIP 7 - Wisconsin Map.png new file mode 100644 index 000000000..6343fe72b Binary files /dev/null and b/docs/img/Corrections/SparkFun NTRIP 7 - Wisconsin Map.png differ diff --git a/docs/img/Corrections/SparkFun NTRIP Skylark 1 - Credentials.png b/docs/img/Corrections/SparkFun NTRIP Skylark 1 - Credentials.png new file mode 100644 index 000000000..c919dfe63 Binary files /dev/null and b/docs/img/Corrections/SparkFun NTRIP Skylark 1 - Credentials.png differ diff --git a/docs/img/Corrections/SparkFun RTK Emlid Mount Points.png b/docs/img/Corrections/SparkFun RTK Emlid Mount Points.png new file mode 100644 index 000000000..7b62d623b Binary files /dev/null and b/docs/img/Corrections/SparkFun RTK Emlid Mount Points.png differ diff --git a/docs/img/Corrections/SparkFun RTK Facet SD RAWX Log Files.png b/docs/img/Corrections/SparkFun RTK Facet SD RAWX Log Files.png new file mode 100644 index 000000000..2d76f9d14 Binary files /dev/null and b/docs/img/Corrections/SparkFun RTK Facet SD RAWX Log Files.png differ diff --git a/docs/img/Corrections/SparkFun RTK Facet Text Editor RAWX packets.png b/docs/img/Corrections/SparkFun RTK Facet Text Editor RAWX packets.png new file mode 100644 index 000000000..37bb9c793 Binary files /dev/null and b/docs/img/Corrections/SparkFun RTK Facet Text Editor RAWX packets.png differ diff --git a/docs/img/Corrections/SparkFun RTK Facet u-center RAWX packets.png b/docs/img/Corrections/SparkFun RTK Facet u-center RAWX packets.png new file mode 100644 index 000000000..9094d8971 Binary files /dev/null and b/docs/img/Corrections/SparkFun RTK Facet u-center RAWX packets.png differ diff --git a/docs/img/Corrections/SparkFun RTK Facet u-center view of Log Files.png b/docs/img/Corrections/SparkFun RTK Facet u-center view of Log Files.png new file mode 100644 index 000000000..cd02fb50f Binary files /dev/null and b/docs/img/Corrections/SparkFun RTK Facet u-center view of Log Files.png differ diff --git a/docs/img/Corrections/SparkFun RTK RTK2Go SparkFun Mount Point.png b/docs/img/Corrections/SparkFun RTK RTK2Go SparkFun Mount Point.png new file mode 100644 index 000000000..6c78814e3 Binary files /dev/null and b/docs/img/Corrections/SparkFun RTK RTK2Go SparkFun Mount Point.png differ diff --git a/docs/img/Corrections/SparkFun-PPP.pdf b/docs/img/Corrections/SparkFun-PPP.pdf new file mode 100644 index 000000000..5152494ec Binary files /dev/null and b/docs/img/Corrections/SparkFun-PPP.pdf differ diff --git a/docs/img/Corrections/SparkFun_PPP_Results.png b/docs/img/Corrections/SparkFun_PPP_Results.png new file mode 100644 index 000000000..7958a126c Binary files /dev/null and b/docs/img/Corrections/SparkFun_PPP_Results.png differ diff --git a/docs/img/Corrections/SparkFun_RTK_Express_-_Base_Radio - Big.jpg b/docs/img/Corrections/SparkFun_RTK_Express_-_Base_Radio - Big.jpg new file mode 100644 index 000000000..06489cecf Binary files /dev/null and b/docs/img/Corrections/SparkFun_RTK_Express_-_Base_Radio - Big.jpg differ diff --git a/docs/img/Corrections/SparkFun_RTK_Express_-_Base_Radio.jpg b/docs/img/Corrections/SparkFun_RTK_Express_-_Base_Radio.jpg new file mode 100644 index 000000000..cdd0f21c0 Binary files /dev/null and b/docs/img/Corrections/SparkFun_RTK_Express_-_Base_Radio.jpg differ diff --git a/docs/img/Corrections/SparkFun_RTK_Facet_-_Ports_-_microSD.jpg b/docs/img/Corrections/SparkFun_RTK_Facet_-_Ports_-_microSD.jpg new file mode 100644 index 000000000..69b34a0b7 Binary files /dev/null and b/docs/img/Corrections/SparkFun_RTK_Facet_-_Ports_-_microSD.jpg differ diff --git a/docs/img/Corrections/SparkFun_RTK_Facet_L-Band_Coverage_Area.jpg b/docs/img/Corrections/SparkFun_RTK_Facet_L-Band_Coverage_Area.jpg new file mode 100644 index 000000000..526ae257c Binary files /dev/null and b/docs/img/Corrections/SparkFun_RTK_Facet_L-Band_Coverage_Area.jpg differ diff --git a/docs/img/Corrections/SparkFun_RTK_Surveyor_-_Radio.jpg b/docs/img/Corrections/SparkFun_RTK_Surveyor_-_Radio.jpg new file mode 100644 index 000000000..87a1310a8 Binary files /dev/null and b/docs/img/Corrections/SparkFun_RTK_Surveyor_-_Radio.jpg differ diff --git a/docs/img/Corrections/Uploading_file_to_CSRS.jpg b/docs/img/Corrections/Uploading_file_to_CSRS.jpg new file mode 100644 index 000000000..2e8bce983 Binary files /dev/null and b/docs/img/Corrections/Uploading_file_to_CSRS.jpg differ diff --git a/docs/img/DiamondMaps/SparkFun RTK Diamond Maps - GNSS Source Selected.png b/docs/img/DiamondMaps/SparkFun RTK Diamond Maps - GNSS Source Selected.png new file mode 100644 index 000000000..c0d15f2b6 Binary files /dev/null and b/docs/img/DiamondMaps/SparkFun RTK Diamond Maps - GNSS Source Selected.png differ diff --git a/docs/img/DiamondMaps/SparkFun RTK Diamond Maps - GNSS Source.png b/docs/img/DiamondMaps/SparkFun RTK Diamond Maps - GNSS Source.png new file mode 100644 index 000000000..baa181376 Binary files /dev/null and b/docs/img/DiamondMaps/SparkFun RTK Diamond Maps - GNSS Source.png differ diff --git a/docs/img/DiamondMaps/SparkFun RTK Diamond Maps - Home Screen.png b/docs/img/DiamondMaps/SparkFun RTK Diamond Maps - Home Screen.png new file mode 100644 index 000000000..c29790281 Binary files /dev/null and b/docs/img/DiamondMaps/SparkFun RTK Diamond Maps - Home Screen.png differ diff --git a/docs/img/DiamondMaps/SparkFun RTK Diamond Maps - NTRIP Settings.png b/docs/img/DiamondMaps/SparkFun RTK Diamond Maps - NTRIP Settings.png new file mode 100644 index 000000000..f5d8acac8 Binary files /dev/null and b/docs/img/DiamondMaps/SparkFun RTK Diamond Maps - NTRIP Settings.png differ diff --git a/docs/img/DiamondMaps/SparkFun RTK Diamond Maps - RTK Fix.png b/docs/img/DiamondMaps/SparkFun RTK Diamond Maps - RTK Fix.png new file mode 100644 index 000000000..07560c24a Binary files /dev/null and b/docs/img/DiamondMaps/SparkFun RTK Diamond Maps - RTK Fix.png differ diff --git a/docs/img/DiamondMaps/SparkFun RTK Diamond Maps - Settings Menu.png b/docs/img/DiamondMaps/SparkFun RTK Diamond Maps - Settings Menu.png new file mode 100644 index 000000000..2186476f9 Binary files /dev/null and b/docs/img/DiamondMaps/SparkFun RTK Diamond Maps - Settings Menu.png differ diff --git a/docs/img/Displays/Antenna_Open.png b/docs/img/Displays/Antenna_Open.png new file mode 100644 index 000000000..2c92ffb37 Binary files /dev/null and b/docs/img/Displays/Antenna_Open.png differ diff --git a/docs/img/Displays/Antenna_Short.png b/docs/img/Displays/Antenna_Short.png new file mode 100644 index 000000000..70a36ea1b Binary files /dev/null and b/docs/img/Displays/Antenna_Short.png differ diff --git a/docs/img/Displays/SparkFun RTK - NTP Select.gif b/docs/img/Displays/SparkFun RTK - NTP Select.gif new file mode 100644 index 000000000..0674fbb40 Binary files /dev/null and b/docs/img/Displays/SparkFun RTK - NTP Select.gif differ diff --git a/docs/img/Displays/SparkFun RTK Boot Screen Version Number.png b/docs/img/Displays/SparkFun RTK Boot Screen Version Number.png new file mode 100644 index 000000000..1cc604850 Binary files /dev/null and b/docs/img/Displays/SparkFun RTK Boot Screen Version Number.png differ diff --git a/docs/img/Displays/SparkFun RTK Config Display.png b/docs/img/Displays/SparkFun RTK Config Display.png new file mode 100644 index 000000000..0e0064a62 Binary files /dev/null and b/docs/img/Displays/SparkFun RTK Config Display.png differ diff --git a/docs/img/Displays/SparkFun RTK Display - Bluetooth.png b/docs/img/Displays/SparkFun RTK Display - Bluetooth.png new file mode 100644 index 000000000..8330d6593 Binary files /dev/null and b/docs/img/Displays/SparkFun RTK Display - Bluetooth.png differ diff --git a/docs/img/Displays/SparkFun RTK Display - Double Crosshair.png b/docs/img/Displays/SparkFun RTK Display - Double Crosshair.png new file mode 100644 index 000000000..40937138a Binary files /dev/null and b/docs/img/Displays/SparkFun RTK Display - Double Crosshair.png differ diff --git a/docs/img/Displays/SparkFun RTK Facet Boot Display.png b/docs/img/Displays/SparkFun RTK Facet Boot Display.png new file mode 100644 index 000000000..1d3acce8a Binary files /dev/null and b/docs/img/Displays/SparkFun RTK Facet Boot Display.png differ diff --git a/docs/img/Displays/SparkFun RTK Logging Types.png b/docs/img/Displays/SparkFun RTK Logging Types.png new file mode 100644 index 000000000..edc43aea3 Binary files /dev/null and b/docs/img/Displays/SparkFun RTK Logging Types.png differ diff --git a/docs/img/Displays/SparkFun RTK Radio Display.png b/docs/img/Displays/SparkFun RTK Radio Display.png new file mode 100644 index 000000000..e2a8615b2 Binary files /dev/null and b/docs/img/Displays/SparkFun RTK Radio Display.png differ diff --git a/docs/img/Displays/SparkFun RTK Radio E-Pair.png b/docs/img/Displays/SparkFun RTK Radio E-Pair.png new file mode 100644 index 000000000..1e8ca57ab Binary files /dev/null and b/docs/img/Displays/SparkFun RTK Radio E-Pair.png differ diff --git a/docs/img/Displays/SparkFun RTK Rover Display.png b/docs/img/Displays/SparkFun RTK Rover Display.png new file mode 100644 index 000000000..c2ed63579 Binary files /dev/null and b/docs/img/Displays/SparkFun RTK Rover Display.png differ diff --git a/docs/img/Displays/SparkFun RTK WiFi Config IP.png b/docs/img/Displays/SparkFun RTK WiFi Config IP.png new file mode 100644 index 000000000..fb02df2c7 Binary files /dev/null and b/docs/img/Displays/SparkFun RTK WiFi Config IP.png differ diff --git a/docs/img/Displays/SparkFun_RTK_Express_-_Display_-_FixedBase-Casting.jpg b/docs/img/Displays/SparkFun_RTK_Express_-_Display_-_FixedBase-Casting.jpg new file mode 100644 index 000000000..f00e16957 Binary files /dev/null and b/docs/img/Displays/SparkFun_RTK_Express_-_Display_-_FixedBase-Casting.jpg differ diff --git a/docs/img/Displays/SparkFun_RTK_Express_-_Display_-_FixedBase-Xmitting.jpg b/docs/img/Displays/SparkFun_RTK_Express_-_Display_-_FixedBase-Xmitting.jpg new file mode 100644 index 000000000..406241499 Binary files /dev/null and b/docs/img/Displays/SparkFun_RTK_Express_-_Display_-_FixedBase-Xmitting.jpg differ diff --git a/docs/img/Displays/SparkFun_RTK_Express_-_Display_-_Rover_RTK_Fixed.jpg b/docs/img/Displays/SparkFun_RTK_Express_-_Display_-_Rover_RTK_Fixed.jpg new file mode 100644 index 000000000..2e2a87fc9 Binary files /dev/null and b/docs/img/Displays/SparkFun_RTK_Express_-_Display_-_Rover_RTK_Fixed.jpg differ diff --git a/docs/img/Displays/SparkFun_RTK_Express_-_Display_-_Survey-In.jpg b/docs/img/Displays/SparkFun_RTK_Express_-_Display_-_Survey-In.jpg new file mode 100644 index 000000000..556a8a107 Binary files /dev/null and b/docs/img/Displays/SparkFun_RTK_Express_-_Display_-_Survey-In.jpg differ diff --git a/docs/img/Displays/SparkFun_RTK_Facet_-_Display_On_Off.jpg b/docs/img/Displays/SparkFun_RTK_Facet_-_Display_On_Off.jpg new file mode 100644 index 000000000..36808829b Binary files /dev/null and b/docs/img/Displays/SparkFun_RTK_Facet_-_Display_On_Off.jpg differ diff --git a/docs/img/Displays/SparkFun_RTK_Facet_-_Display_WiFi_Config.jpg b/docs/img/Displays/SparkFun_RTK_Facet_-_Display_WiFi_Config.jpg new file mode 100644 index 000000000..28afdbc67 Binary files /dev/null and b/docs/img/Displays/SparkFun_RTK_Facet_-_Display_WiFi_Config.jpg differ diff --git a/docs/img/Displays/SparkFun_RTK_Facet_-_Main_Display_Icons.jpg b/docs/img/Displays/SparkFun_RTK_Facet_-_Main_Display_Icons.jpg new file mode 100644 index 000000000..835d767b1 Binary files /dev/null and b/docs/img/Displays/SparkFun_RTK_Facet_-_Main_Display_Icons.jpg differ diff --git a/docs/img/Displays/SparkFun_RTK_LBand_DayToExpire.jpg b/docs/img/Displays/SparkFun_RTK_LBand_DayToExpire.jpg new file mode 100644 index 000000000..41e38d93e Binary files /dev/null and b/docs/img/Displays/SparkFun_RTK_LBand_DayToExpire.jpg differ diff --git a/docs/img/Displays/SparkFun_RTK_LBand_Indicator.jpg b/docs/img/Displays/SparkFun_RTK_LBand_Indicator.jpg new file mode 100644 index 000000000..eca2effc0 Binary files /dev/null and b/docs/img/Displays/SparkFun_RTK_LBand_Indicator.jpg differ diff --git a/docs/img/Displays/SparkFun_RTK_Rover_NTRIP_Client_Connection.png b/docs/img/Displays/SparkFun_RTK_Rover_NTRIP_Client_Connection.png new file mode 100644 index 000000000..584c0cbbb Binary files /dev/null and b/docs/img/Displays/SparkFun_RTK_Rover_NTRIP_Client_Connection.png differ diff --git a/docs/img/Edit Page.png b/docs/img/Edit Page.png new file mode 100644 index 000000000..512016fd6 Binary files /dev/null and b/docs/img/Edit Page.png differ diff --git a/docs/img/FieldGenius/Field Genius 1.png b/docs/img/FieldGenius/Field Genius 1.png new file mode 100644 index 000000000..72a0cd5be Binary files /dev/null and b/docs/img/FieldGenius/Field Genius 1.png differ diff --git a/docs/img/FieldGenius/Field Genius 10.png b/docs/img/FieldGenius/Field Genius 10.png new file mode 100644 index 000000000..ae4ac4d0a Binary files /dev/null and b/docs/img/FieldGenius/Field Genius 10.png differ diff --git a/docs/img/FieldGenius/Field Genius 11.png b/docs/img/FieldGenius/Field Genius 11.png new file mode 100644 index 000000000..ec2ab507a Binary files /dev/null and b/docs/img/FieldGenius/Field Genius 11.png differ diff --git a/docs/img/FieldGenius/Field Genius 12.png b/docs/img/FieldGenius/Field Genius 12.png new file mode 100644 index 000000000..d6b67831a Binary files /dev/null and b/docs/img/FieldGenius/Field Genius 12.png differ diff --git a/docs/img/FieldGenius/Field Genius 13.png b/docs/img/FieldGenius/Field Genius 13.png new file mode 100644 index 000000000..94d774355 Binary files /dev/null and b/docs/img/FieldGenius/Field Genius 13.png differ diff --git a/docs/img/FieldGenius/Field Genius 2.png b/docs/img/FieldGenius/Field Genius 2.png new file mode 100644 index 000000000..be0a104e8 Binary files /dev/null and b/docs/img/FieldGenius/Field Genius 2.png differ diff --git a/docs/img/FieldGenius/Field Genius 3.png b/docs/img/FieldGenius/Field Genius 3.png new file mode 100644 index 000000000..463f228f0 Binary files /dev/null and b/docs/img/FieldGenius/Field Genius 3.png differ diff --git a/docs/img/FieldGenius/Field Genius 4.png b/docs/img/FieldGenius/Field Genius 4.png new file mode 100644 index 000000000..59e64fb24 Binary files /dev/null and b/docs/img/FieldGenius/Field Genius 4.png differ diff --git a/docs/img/FieldGenius/Field Genius 5.png b/docs/img/FieldGenius/Field Genius 5.png new file mode 100644 index 000000000..cd5d00473 Binary files /dev/null and b/docs/img/FieldGenius/Field Genius 5.png differ diff --git a/docs/img/FieldGenius/Field Genius 6.png b/docs/img/FieldGenius/Field Genius 6.png new file mode 100644 index 000000000..058c997e5 Binary files /dev/null and b/docs/img/FieldGenius/Field Genius 6.png differ diff --git a/docs/img/FieldGenius/Field Genius 7.png b/docs/img/FieldGenius/Field Genius 7.png new file mode 100644 index 000000000..c27316bc4 Binary files /dev/null and b/docs/img/FieldGenius/Field Genius 7.png differ diff --git a/docs/img/FieldGenius/Field Genius 8.png b/docs/img/FieldGenius/Field Genius 8.png new file mode 100644 index 000000000..a0618e0d0 Binary files /dev/null and b/docs/img/FieldGenius/Field Genius 8.png differ diff --git a/docs/img/FieldGenius/Field Genius 9.png b/docs/img/FieldGenius/Field Genius 9.png new file mode 100644 index 000000000..c8f1dfbef Binary files /dev/null and b/docs/img/FieldGenius/Field Genius 9.png differ diff --git a/docs/img/FieldMaps/SparkFun RTK Field Maps - Main.png b/docs/img/FieldMaps/SparkFun RTK Field Maps - Main.png new file mode 100644 index 000000000..9a59cce58 Binary files /dev/null and b/docs/img/FieldMaps/SparkFun RTK Field Maps - Main.png differ diff --git a/docs/img/FieldMaps/SparkFun RTK Field Maps - RTK Fix.png b/docs/img/FieldMaps/SparkFun RTK Field Maps - RTK Fix.png new file mode 100644 index 000000000..38a7f4de8 Binary files /dev/null and b/docs/img/FieldMaps/SparkFun RTK Field Maps - RTK Fix.png differ diff --git a/docs/img/GNSSMaster/SparkFun RTK GNSS Master - Correction Input.png b/docs/img/GNSSMaster/SparkFun RTK GNSS Master - Correction Input.png new file mode 100644 index 000000000..6df790843 Binary files /dev/null and b/docs/img/GNSSMaster/SparkFun RTK GNSS Master - Correction Input.png differ diff --git a/docs/img/GNSSMaster/SparkFun RTK GNSS Master - Correction Source Data Flowing.png b/docs/img/GNSSMaster/SparkFun RTK GNSS Master - Correction Source Data Flowing.png new file mode 100644 index 000000000..0319adbaf Binary files /dev/null and b/docs/img/GNSSMaster/SparkFun RTK GNSS Master - Correction Source Data Flowing.png differ diff --git a/docs/img/GNSSMaster/SparkFun RTK GNSS Master - Correction Source List.png b/docs/img/GNSSMaster/SparkFun RTK GNSS Master - Correction Source List.png new file mode 100644 index 000000000..c61cb53e9 Binary files /dev/null and b/docs/img/GNSSMaster/SparkFun RTK GNSS Master - Correction Source List.png differ diff --git a/docs/img/GNSSMaster/SparkFun RTK GNSS Master - Main.png b/docs/img/GNSSMaster/SparkFun RTK GNSS Master - Main.png new file mode 100644 index 000000000..018798565 Binary files /dev/null and b/docs/img/GNSSMaster/SparkFun RTK GNSS Master - Main.png differ diff --git a/docs/img/GNSSMaster/SparkFun RTK GNSS Master - Mock Location.png b/docs/img/GNSSMaster/SparkFun RTK GNSS Master - Mock Location.png new file mode 100644 index 000000000..9fe71b5f9 Binary files /dev/null and b/docs/img/GNSSMaster/SparkFun RTK GNSS Master - Mock Location.png differ diff --git a/docs/img/GNSSMaster/SparkFun RTK GNSS Master - NTRIP Client Input.png b/docs/img/GNSSMaster/SparkFun RTK GNSS Master - NTRIP Client Input.png new file mode 100644 index 000000000..ce06bfa75 Binary files /dev/null and b/docs/img/GNSSMaster/SparkFun RTK GNSS Master - NTRIP Client Input.png differ diff --git a/docs/img/GNSSMaster/SparkFun RTK GNSS Master - Receiver Selection.png b/docs/img/GNSSMaster/SparkFun RTK GNSS Master - Receiver Selection.png new file mode 100644 index 000000000..0d3973add Binary files /dev/null and b/docs/img/GNSSMaster/SparkFun RTK GNSS Master - Receiver Selection.png differ diff --git a/docs/img/Icons/GreenDot.png b/docs/img/Icons/GreenDot.png new file mode 100644 index 000000000..c5ada637b Binary files /dev/null and b/docs/img/Icons/GreenDot.png differ diff --git a/docs/img/Icons/RedDot.png b/docs/img/Icons/RedDot.png new file mode 100644 index 000000000..6ff13b5c5 Binary files /dev/null and b/docs/img/Icons/RedDot.png differ diff --git a/docs/img/Icons/YellowDot.png b/docs/img/Icons/YellowDot.png new file mode 100644 index 000000000..caeb87961 Binary files /dev/null and b/docs/img/Icons/YellowDot.png differ diff --git a/docs/img/Icons/sfe_logo_sm.png b/docs/img/Icons/sfe_logo_sm.png new file mode 100644 index 000000000..cca6dcb78 Binary files /dev/null and b/docs/img/Icons/sfe_logo_sm.png differ diff --git a/docs/img/Icons/sfe_logo_sq.png b/docs/img/Icons/sfe_logo_sq.png new file mode 100644 index 000000000..3003abf59 Binary files /dev/null and b/docs/img/Icons/sfe_logo_sq.png differ diff --git a/docs/img/Lefebure/SparkFun RTK Lefebure - Getting Data with Mock Location.png b/docs/img/Lefebure/SparkFun RTK Lefebure - Getting Data with Mock Location.png new file mode 100644 index 000000000..8af25ae10 Binary files /dev/null and b/docs/img/Lefebure/SparkFun RTK Lefebure - Getting Data with Mock Location.png differ diff --git a/docs/img/Lefebure/SparkFun RTK Lefebure - Main.png b/docs/img/Lefebure/SparkFun RTK Lefebure - Main.png new file mode 100644 index 000000000..fd14f7066 Binary files /dev/null and b/docs/img/Lefebure/SparkFun RTK Lefebure - Main.png differ diff --git a/docs/img/Lefebure/SparkFun RTK Lefebure - NTRIP Client Settings.png b/docs/img/Lefebure/SparkFun RTK Lefebure - NTRIP Client Settings.png new file mode 100644 index 000000000..4da263f3f Binary files /dev/null and b/docs/img/Lefebure/SparkFun RTK Lefebure - NTRIP Client Settings.png differ diff --git a/docs/img/Lefebure/SparkFun RTK Lefebure - NTRIP Settings.png b/docs/img/Lefebure/SparkFun RTK Lefebure - NTRIP Settings.png new file mode 100644 index 000000000..250fbc793 Binary files /dev/null and b/docs/img/Lefebure/SparkFun RTK Lefebure - NTRIP Settings.png differ diff --git a/docs/img/Lefebure/SparkFun RTK Lefebure - Receiver Settings Bluetooth.png b/docs/img/Lefebure/SparkFun RTK Lefebure - Receiver Settings Bluetooth.png new file mode 100644 index 000000000..69fa4d565 Binary files /dev/null and b/docs/img/Lefebure/SparkFun RTK Lefebure - Receiver Settings Bluetooth.png differ diff --git a/docs/img/Lefebure/SparkFun RTK Lefebure - Receiver Settings.png b/docs/img/Lefebure/SparkFun RTK Lefebure - Receiver Settings.png new file mode 100644 index 000000000..e2551cedf Binary files /dev/null and b/docs/img/Lefebure/SparkFun RTK Lefebure - Receiver Settings.png differ diff --git a/docs/img/MockLocation/SparkFun RTK Mock Location - Build Number.png b/docs/img/MockLocation/SparkFun RTK Mock Location - Build Number.png new file mode 100644 index 000000000..1a96d571f Binary files /dev/null and b/docs/img/MockLocation/SparkFun RTK Mock Location - Build Number.png differ diff --git a/docs/img/MockLocation/SparkFun RTK Mock Location - Developer Options.png b/docs/img/MockLocation/SparkFun RTK Mock Location - Developer Options.png new file mode 100644 index 000000000..2b7f30cca Binary files /dev/null and b/docs/img/MockLocation/SparkFun RTK Mock Location - Developer Options.png differ diff --git a/docs/img/MockLocation/SparkFun RTK Mock Location - Select Mock Location App.png b/docs/img/MockLocation/SparkFun RTK Mock Location - Select Mock Location App.png new file mode 100644 index 000000000..355ebafc4 Binary files /dev/null and b/docs/img/MockLocation/SparkFun RTK Mock Location - Select Mock Location App.png differ diff --git a/docs/img/MockLocation/SparkFun RTK Mock Location - Settings.png b/docs/img/MockLocation/SparkFun RTK Mock Location - Settings.png new file mode 100644 index 000000000..42a4e4f4c Binary files /dev/null and b/docs/img/MockLocation/SparkFun RTK Mock Location - Settings.png differ diff --git a/docs/img/NTP/NTP_Config_1.png b/docs/img/NTP/NTP_Config_1.png new file mode 100644 index 000000000..bf3533f91 Binary files /dev/null and b/docs/img/NTP/NTP_Config_1.png differ diff --git a/docs/img/NTP/NTP_Config_1_small.png b/docs/img/NTP/NTP_Config_1_small.png new file mode 100644 index 000000000..1f19526a0 Binary files /dev/null and b/docs/img/NTP/NTP_Config_1_small.png differ diff --git a/docs/img/NTP/NTP_Config_2.png b/docs/img/NTP/NTP_Config_2.png new file mode 100644 index 000000000..c14ced336 Binary files /dev/null and b/docs/img/NTP/NTP_Config_2.png differ diff --git a/docs/img/NTP/NTP_Config_2_small.png b/docs/img/NTP/NTP_Config_2_small.png new file mode 100644 index 000000000..f187a4579 Binary files /dev/null and b/docs/img/NTP/NTP_Config_2_small.png differ diff --git a/docs/img/NTP/NTP_Config_3.png b/docs/img/NTP/NTP_Config_3.png new file mode 100644 index 000000000..3bad952a8 Binary files /dev/null and b/docs/img/NTP/NTP_Config_3.png differ diff --git a/docs/img/NTP/NTP_Config_3_small.png b/docs/img/NTP/NTP_Config_3_small.png new file mode 100644 index 000000000..de094c1e9 Binary files /dev/null and b/docs/img/NTP/NTP_Config_3_small.png differ diff --git a/docs/img/NTP/NTP_Config_4.png b/docs/img/NTP/NTP_Config_4.png new file mode 100644 index 000000000..17e1983c7 Binary files /dev/null and b/docs/img/NTP/NTP_Config_4.png differ diff --git a/docs/img/NTP/NTP_Config_5.png b/docs/img/NTP/NTP_Config_5.png new file mode 100644 index 000000000..8f21f94c9 Binary files /dev/null and b/docs/img/NTP/NTP_Config_5.png differ diff --git a/docs/img/NTP/NTP_Diagnostics.png b/docs/img/NTP/NTP_Diagnostics.png new file mode 100644 index 000000000..6dcd9c4c9 Binary files /dev/null and b/docs/img/NTP/NTP_Diagnostics.png differ diff --git a/docs/img/NTP/NTP_Install_1.png b/docs/img/NTP/NTP_Install_1.png new file mode 100644 index 000000000..941214725 Binary files /dev/null and b/docs/img/NTP/NTP_Install_1.png differ diff --git a/docs/img/NTP/NTP_Install_2.png b/docs/img/NTP/NTP_Install_2.png new file mode 100644 index 000000000..a5bdaf703 Binary files /dev/null and b/docs/img/NTP/NTP_Install_2.png differ diff --git a/docs/img/NTP/NTP_Log.png b/docs/img/NTP/NTP_Log.png new file mode 100644 index 000000000..a81f40d66 Binary files /dev/null and b/docs/img/NTP/NTP_Log.png differ diff --git a/docs/img/NTP/NTP_Logging.png b/docs/img/NTP/NTP_Logging.png new file mode 100644 index 000000000..ca659d06c Binary files /dev/null and b/docs/img/NTP/NTP_Logging.png differ diff --git a/docs/img/PointPerfect/SparkFun RTK Everywhere - PointPerfect Coverage Map Small.png b/docs/img/PointPerfect/SparkFun RTK Everywhere - PointPerfect Coverage Map Small.png new file mode 100644 index 000000000..cc03214d6 Binary files /dev/null and b/docs/img/PointPerfect/SparkFun RTK Everywhere - PointPerfect Coverage Map Small.png differ diff --git a/docs/img/PointPerfect/SparkFun RTK Everywhere - PointPerfect Coverage Map.png b/docs/img/PointPerfect/SparkFun RTK Everywhere - PointPerfect Coverage Map.png new file mode 100644 index 000000000..5ca45c7a2 Binary files /dev/null and b/docs/img/PointPerfect/SparkFun RTK Everywhere - PointPerfect Coverage Map.png differ diff --git a/docs/img/PointPerfect/SparkFun RTK Everywhere - PointPerfect Localized Distribution.png b/docs/img/PointPerfect/SparkFun RTK Everywhere - PointPerfect Localized Distribution.png new file mode 100644 index 000000000..a0fced791 Binary files /dev/null and b/docs/img/PointPerfect/SparkFun RTK Everywhere - PointPerfect Localized Distribution.png differ diff --git a/docs/img/QField/SparkFun RTK QField - Connected with RTK Fix.png b/docs/img/QField/SparkFun RTK QField - Connected with RTK Fix.png new file mode 100644 index 000000000..8ac7f2204 Binary files /dev/null and b/docs/img/QField/SparkFun RTK QField - Connected with RTK Fix.png differ diff --git a/docs/img/QField/SparkFun RTK QField - Create Project.png b/docs/img/QField/SparkFun RTK QField - Create Project.png new file mode 100644 index 000000000..b52408783 Binary files /dev/null and b/docs/img/QField/SparkFun RTK QField - Create Project.png differ diff --git a/docs/img/QField/SparkFun RTK QField - Main Map.png b/docs/img/QField/SparkFun RTK QField - Main Map.png new file mode 100644 index 000000000..2519fe9b3 Binary files /dev/null and b/docs/img/QField/SparkFun RTK QField - Main Map.png differ diff --git a/docs/img/QField/SparkFun RTK QField - NMEA Messages.png b/docs/img/QField/SparkFun RTK QField - NMEA Messages.png new file mode 100644 index 000000000..38fafde6c Binary files /dev/null and b/docs/img/QField/SparkFun RTK QField - NMEA Messages.png differ diff --git a/docs/img/QField/SparkFun RTK QField - Open Project.png b/docs/img/QField/SparkFun RTK QField - Open Project.png new file mode 100644 index 000000000..9f2e730e4 Binary files /dev/null and b/docs/img/QField/SparkFun RTK QField - Open Project.png differ diff --git a/docs/img/QField/SparkFun RTK QField - Open Settings.png b/docs/img/QField/SparkFun RTK QField - Open Settings.png new file mode 100644 index 000000000..e44db2ee3 Binary files /dev/null and b/docs/img/QField/SparkFun RTK QField - Open Settings.png differ diff --git a/docs/img/QField/SparkFun RTK QField - Opening Page.png b/docs/img/QField/SparkFun RTK QField - Opening Page.png new file mode 100644 index 000000000..7cd3ea7a8 Binary files /dev/null and b/docs/img/QField/SparkFun RTK QField - Opening Page.png differ diff --git a/docs/img/QField/SparkFun RTK QField - Project Settings 2.png b/docs/img/QField/SparkFun RTK QField - Project Settings 2.png new file mode 100644 index 000000000..64aeb0d04 Binary files /dev/null and b/docs/img/QField/SparkFun RTK QField - Project Settings 2.png differ diff --git a/docs/img/QField/SparkFun RTK QField - Project Settings.png b/docs/img/QField/SparkFun RTK QField - Project Settings.png new file mode 100644 index 000000000..e9d3c46c7 Binary files /dev/null and b/docs/img/QField/SparkFun RTK QField - Project Settings.png differ diff --git a/docs/img/QField/SparkFun RTK QField - Refresh Project.png b/docs/img/QField/SparkFun RTK QField - Refresh Project.png new file mode 100644 index 000000000..a38437ee6 Binary files /dev/null and b/docs/img/QField/SparkFun RTK QField - Refresh Project.png differ diff --git a/docs/img/QField/SparkFun RTK QField - Select Positioning Devce.png b/docs/img/QField/SparkFun RTK QField - Select Positioning Devce.png new file mode 100644 index 000000000..1cdd308d0 Binary files /dev/null and b/docs/img/QField/SparkFun RTK QField - Select Positioning Devce.png differ diff --git a/docs/img/QField/SparkFun RTK QField - Settings Gear.png b/docs/img/QField/SparkFun RTK QField - Settings Gear.png new file mode 100644 index 000000000..1bfde255e Binary files /dev/null and b/docs/img/QField/SparkFun RTK QField - Settings Gear.png differ diff --git a/docs/img/QField/SparkFun RTK QField - Settings Menu.png b/docs/img/QField/SparkFun RTK QField - Settings Menu.png new file mode 100644 index 000000000..7a04c76e0 Binary files /dev/null and b/docs/img/QField/SparkFun RTK QField - Settings Menu.png differ diff --git a/docs/img/QField/SparkFun RTK QField - Settings Positioning Menu.png b/docs/img/QField/SparkFun RTK QField - Settings Positioning Menu.png new file mode 100644 index 000000000..9a1c272c3 Binary files /dev/null and b/docs/img/QField/SparkFun RTK QField - Settings Positioning Menu.png differ diff --git a/docs/img/QField/SparkFun RTK QField - TCP Connected.png b/docs/img/QField/SparkFun RTK QField - TCP Connected.png new file mode 100644 index 000000000..fc9c2e8b7 Binary files /dev/null and b/docs/img/QField/SparkFun RTK QField - TCP Connected.png differ diff --git a/docs/img/QField/SparkFun RTK QField - TCP Connection Type.png b/docs/img/QField/SparkFun RTK QField - TCP Connection Type.png new file mode 100644 index 000000000..c6bb83215 Binary files /dev/null and b/docs/img/QField/SparkFun RTK QField - TCP Connection Type.png differ diff --git a/docs/img/QField/SparkFun RTK QField - TCP Connection.png b/docs/img/QField/SparkFun RTK QField - TCP Connection.png new file mode 100644 index 000000000..2764159a1 Binary files /dev/null and b/docs/img/QField/SparkFun RTK QField - TCP Connection.png differ diff --git a/docs/img/QField/SparkFun RTK QField - TCP Server.png b/docs/img/QField/SparkFun RTK QField - TCP Server.png new file mode 100644 index 000000000..2b3741e12 Binary files /dev/null and b/docs/img/QField/SparkFun RTK QField - TCP Server.png differ diff --git a/docs/img/QGIS/SparkFun RTK QGIS - Direct Serial Connection.png b/docs/img/QGIS/SparkFun RTK QGIS - Direct Serial Connection.png new file mode 100644 index 000000000..206f838c7 Binary files /dev/null and b/docs/img/QGIS/SparkFun RTK QGIS - Direct Serial Connection.png differ diff --git a/docs/img/QGIS/SparkFun RTK QGIS - Enable GPS Info Panel.png b/docs/img/QGIS/SparkFun RTK QGIS - Enable GPS Info Panel.png new file mode 100644 index 000000000..7f48cfd80 Binary files /dev/null and b/docs/img/QGIS/SparkFun RTK QGIS - Enable GPS Info Panel.png differ diff --git a/docs/img/QGIS/SparkFun RTK QGIS - GPS Panel Entering IP and port.png b/docs/img/QGIS/SparkFun RTK QGIS - GPS Panel Entering IP and port.png new file mode 100644 index 000000000..759b7107b Binary files /dev/null and b/docs/img/QGIS/SparkFun RTK QGIS - GPS Panel Entering IP and port.png differ diff --git a/docs/img/QGIS/SparkFun RTK QGIS - GPS Panel.png b/docs/img/QGIS/SparkFun RTK QGIS - GPS Panel.png new file mode 100644 index 000000000..d17330711 Binary files /dev/null and b/docs/img/QGIS/SparkFun RTK QGIS - GPS Panel.png differ diff --git a/docs/img/QGIS/SparkFun RTK QGIS - Location on Map.png b/docs/img/QGIS/SparkFun RTK QGIS - Location on Map.png new file mode 100644 index 000000000..a6ecbfa60 Binary files /dev/null and b/docs/img/QGIS/SparkFun RTK QGIS - Location on Map.png differ diff --git a/docs/img/QGIS/SparkFun RTK QGIS - View Menu.png b/docs/img/QGIS/SparkFun RTK QGIS - View Menu.png new file mode 100644 index 000000000..0b40fb463 Binary files /dev/null and b/docs/img/QGIS/SparkFun RTK QGIS - View Menu.png differ diff --git a/docs/img/QuickCapture/SparkFun RTK QuickCapture - BioBlitz Map.png b/docs/img/QuickCapture/SparkFun RTK QuickCapture - BioBlitz Map.png new file mode 100644 index 000000000..da87608c5 Binary files /dev/null and b/docs/img/QuickCapture/SparkFun RTK QuickCapture - BioBlitz Map.png differ diff --git a/docs/img/QuickCapture/SparkFun RTK QuickCapture - BioBlitz.png b/docs/img/QuickCapture/SparkFun RTK QuickCapture - BioBlitz.png new file mode 100644 index 000000000..c5673e051 Binary files /dev/null and b/docs/img/QuickCapture/SparkFun RTK QuickCapture - BioBlitz.png differ diff --git a/docs/img/QuickCapture/SparkFun RTK QuickCapture - Main Window.png b/docs/img/QuickCapture/SparkFun RTK QuickCapture - Main Window.png new file mode 100644 index 000000000..9e828291c Binary files /dev/null and b/docs/img/QuickCapture/SparkFun RTK QuickCapture - Main Window.png differ diff --git a/docs/img/QuickCapture/SparkFun RTK QuickCapture - Select Project.png b/docs/img/QuickCapture/SparkFun RTK QuickCapture - Select Project.png new file mode 100644 index 000000000..e5f7ddf53 Binary files /dev/null and b/docs/img/QuickCapture/SparkFun RTK QuickCapture - Select Project.png differ diff --git a/docs/img/QuickCapture/SparkFun RTK QuickCapture - WiFi Credentials.png b/docs/img/QuickCapture/SparkFun RTK QuickCapture - WiFi Credentials.png new file mode 100644 index 000000000..e4018e169 Binary files /dev/null and b/docs/img/QuickCapture/SparkFun RTK QuickCapture - WiFi Credentials.png differ diff --git a/docs/img/QuickCapture/SparkFun RTK QuickCapture - Workspace.png b/docs/img/QuickCapture/SparkFun RTK QuickCapture - Workspace.png new file mode 100644 index 000000000..37104e5e1 Binary files /dev/null and b/docs/img/QuickCapture/SparkFun RTK QuickCapture - Workspace.png differ diff --git a/docs/img/QuickCapture/iOS/SparkFun RTK QuickCapture - Add Project.png b/docs/img/QuickCapture/iOS/SparkFun RTK QuickCapture - Add Project.png new file mode 100644 index 000000000..8973580ca Binary files /dev/null and b/docs/img/QuickCapture/iOS/SparkFun RTK QuickCapture - Add Project.png differ diff --git a/docs/img/QuickCapture/iOS/SparkFun RTK QuickCapture - BioBlitz Project.png b/docs/img/QuickCapture/iOS/SparkFun RTK QuickCapture - BioBlitz Project.png new file mode 100644 index 000000000..f4a086d45 Binary files /dev/null and b/docs/img/QuickCapture/iOS/SparkFun RTK QuickCapture - BioBlitz Project.png differ diff --git a/docs/img/QuickCapture/iOS/SparkFun RTK QuickCapture - Main Add Project.png b/docs/img/QuickCapture/iOS/SparkFun RTK QuickCapture - Main Add Project.png new file mode 100644 index 000000000..8ec705de1 Binary files /dev/null and b/docs/img/QuickCapture/iOS/SparkFun RTK QuickCapture - Main Add Project.png differ diff --git a/docs/img/QuickCapture/iOS/SparkFun RTK QuickCapture - Main Screen.png b/docs/img/QuickCapture/iOS/SparkFun RTK QuickCapture - Main Screen.png new file mode 100644 index 000000000..61a4802f1 Binary files /dev/null and b/docs/img/QuickCapture/iOS/SparkFun RTK QuickCapture - Main Screen.png differ diff --git a/docs/img/QuickCapture/iOS/SparkFun RTK QuickCapture - Map.png b/docs/img/QuickCapture/iOS/SparkFun RTK QuickCapture - Map.png new file mode 100644 index 000000000..cf3337df7 Binary files /dev/null and b/docs/img/QuickCapture/iOS/SparkFun RTK QuickCapture - Map.png differ diff --git a/docs/img/QuickCapture/iOS/SparkFun RTK QuickCapture - Settings Menu Location Provider.png b/docs/img/QuickCapture/iOS/SparkFun RTK QuickCapture - Settings Menu Location Provider.png new file mode 100644 index 000000000..817bc1e8b Binary files /dev/null and b/docs/img/QuickCapture/iOS/SparkFun RTK QuickCapture - Settings Menu Location Provider.png differ diff --git a/docs/img/QuickCapture/iOS/SparkFun RTK QuickCapture - Settings Menu.png b/docs/img/QuickCapture/iOS/SparkFun RTK QuickCapture - Settings Menu.png new file mode 100644 index 000000000..4e665b357 Binary files /dev/null and b/docs/img/QuickCapture/iOS/SparkFun RTK QuickCapture - Settings Menu.png differ diff --git a/docs/img/QuickCapture/iOS/SparkFun RTK QuickCapture - Splash.png b/docs/img/QuickCapture/iOS/SparkFun RTK QuickCapture - Splash.png new file mode 100644 index 000000000..863c845da Binary files /dev/null and b/docs/img/QuickCapture/iOS/SparkFun RTK QuickCapture - Splash.png differ diff --git a/docs/img/QuickCapture/iOS/SparkFun RTK QuickCapture - TCP Added.png b/docs/img/QuickCapture/iOS/SparkFun RTK QuickCapture - TCP Added.png new file mode 100644 index 000000000..081b86f66 Binary files /dev/null and b/docs/img/QuickCapture/iOS/SparkFun RTK QuickCapture - TCP Added.png differ diff --git a/docs/img/QuickCapture/iOS/SparkFun RTK QuickCapture - TCP Settings.png b/docs/img/QuickCapture/iOS/SparkFun RTK QuickCapture - TCP Settings.png new file mode 100644 index 000000000..2b14ff925 Binary files /dev/null and b/docs/img/QuickCapture/iOS/SparkFun RTK QuickCapture - TCP Settings.png differ diff --git a/docs/img/QuickCapture/iOS/SparkFun RTK QuickCapture iOS - Enable PVT Server.png b/docs/img/QuickCapture/iOS/SparkFun RTK QuickCapture iOS - Enable PVT Server.png new file mode 100644 index 000000000..48a7a835f Binary files /dev/null and b/docs/img/QuickCapture/iOS/SparkFun RTK QuickCapture iOS - Enable PVT Server.png differ diff --git a/docs/img/QuickCapture/iOS/SparkFun RTK QuickCapture iOS - IP Address.png b/docs/img/QuickCapture/iOS/SparkFun RTK QuickCapture iOS - IP Address.png new file mode 100644 index 000000000..cb30fb174 Binary files /dev/null and b/docs/img/QuickCapture/iOS/SparkFun RTK QuickCapture iOS - IP Address.png differ diff --git a/docs/img/QuickCapture/iOS/SparkFun RTK QuickCapture iOS - WiFi Settings.png b/docs/img/QuickCapture/iOS/SparkFun RTK QuickCapture iOS - WiFi Settings.png new file mode 100644 index 000000000..6afaf1491 Binary files /dev/null and b/docs/img/QuickCapture/iOS/SparkFun RTK QuickCapture iOS - WiFi Settings.png differ diff --git a/docs/img/RTK Express with External Microphones.png b/docs/img/RTK Express with External Microphones.png new file mode 100644 index 000000000..f3f40c545 Binary files /dev/null and b/docs/img/RTK Express with External Microphones.png differ diff --git a/docs/img/RTK_Uploader_Windows.png b/docs/img/RTK_Uploader_Windows.png new file mode 100644 index 000000000..36690dae8 Binary files /dev/null and b/docs/img/RTK_Uploader_Windows.png differ diff --git a/docs/img/Radios/SparkFun RTK ESP-Now Distance Testing.png b/docs/img/Radios/SparkFun RTK ESP-Now Distance Testing.png new file mode 100644 index 000000000..7a71ede06 Binary files /dev/null and b/docs/img/Radios/SparkFun RTK ESP-Now Distance Testing.png differ diff --git a/docs/img/Repair/RTK-Facet-Repair-3.jpg b/docs/img/Repair/RTK-Facet-Repair-3.jpg new file mode 100644 index 000000000..bd822fab6 Binary files /dev/null and b/docs/img/Repair/RTK-Facet-Repair-3.jpg differ diff --git a/docs/img/Repair/RTK_Surveyor_Internal_-_NMEA_Switches.jpg b/docs/img/Repair/RTK_Surveyor_Internal_-_NMEA_Switches.jpg new file mode 100644 index 000000000..b29a32f88 Binary files /dev/null and b/docs/img/Repair/RTK_Surveyor_Internal_-_NMEA_Switches.jpg differ diff --git a/docs/img/Repair/Ref_Station_Disassembly.png b/docs/img/Repair/Ref_Station_Disassembly.png new file mode 100644 index 000000000..0c5818c9f Binary files /dev/null and b/docs/img/Repair/Ref_Station_Disassembly.png differ diff --git a/docs/img/Repair/SparkFun-RTK-Repair-1.jpg b/docs/img/Repair/SparkFun-RTK-Repair-1.jpg new file mode 100644 index 000000000..699595d0d Binary files /dev/null and b/docs/img/Repair/SparkFun-RTK-Repair-1.jpg differ diff --git a/docs/img/Repair/SparkFun-RTK-Repair-10.jpg b/docs/img/Repair/SparkFun-RTK-Repair-10.jpg new file mode 100644 index 000000000..bcf561071 Binary files /dev/null and b/docs/img/Repair/SparkFun-RTK-Repair-10.jpg differ diff --git a/docs/img/Repair/SparkFun-RTK-Repair-11.jpg b/docs/img/Repair/SparkFun-RTK-Repair-11.jpg new file mode 100644 index 000000000..325fc13cd Binary files /dev/null and b/docs/img/Repair/SparkFun-RTK-Repair-11.jpg differ diff --git a/docs/img/Repair/SparkFun-RTK-Repair-12.jpg b/docs/img/Repair/SparkFun-RTK-Repair-12.jpg new file mode 100644 index 000000000..3ac135ace Binary files /dev/null and b/docs/img/Repair/SparkFun-RTK-Repair-12.jpg differ diff --git a/docs/img/Repair/SparkFun-RTK-Repair-13.jpg b/docs/img/Repair/SparkFun-RTK-Repair-13.jpg new file mode 100644 index 000000000..1e2772759 Binary files /dev/null and b/docs/img/Repair/SparkFun-RTK-Repair-13.jpg differ diff --git a/docs/img/Repair/SparkFun-RTK-Repair-14.jpg b/docs/img/Repair/SparkFun-RTK-Repair-14.jpg new file mode 100644 index 000000000..79a7fa65f Binary files /dev/null and b/docs/img/Repair/SparkFun-RTK-Repair-14.jpg differ diff --git a/docs/img/Repair/SparkFun-RTK-Repair-15.jpg b/docs/img/Repair/SparkFun-RTK-Repair-15.jpg new file mode 100644 index 000000000..9fbcd6ab1 Binary files /dev/null and b/docs/img/Repair/SparkFun-RTK-Repair-15.jpg differ diff --git a/docs/img/Repair/SparkFun-RTK-Repair-16.jpg b/docs/img/Repair/SparkFun-RTK-Repair-16.jpg new file mode 100644 index 000000000..ce2b7f408 Binary files /dev/null and b/docs/img/Repair/SparkFun-RTK-Repair-16.jpg differ diff --git a/docs/img/Repair/SparkFun-RTK-Repair-17.jpg b/docs/img/Repair/SparkFun-RTK-Repair-17.jpg new file mode 100644 index 000000000..25599c2c0 Binary files /dev/null and b/docs/img/Repair/SparkFun-RTK-Repair-17.jpg differ diff --git a/docs/img/Repair/SparkFun-RTK-Repair-18.jpg b/docs/img/Repair/SparkFun-RTK-Repair-18.jpg new file mode 100644 index 000000000..acf8cbdef Binary files /dev/null and b/docs/img/Repair/SparkFun-RTK-Repair-18.jpg differ diff --git a/docs/img/Repair/SparkFun-RTK-Repair-19.jpg b/docs/img/Repair/SparkFun-RTK-Repair-19.jpg new file mode 100644 index 000000000..73226aa50 Binary files /dev/null and b/docs/img/Repair/SparkFun-RTK-Repair-19.jpg differ diff --git a/docs/img/Repair/SparkFun-RTK-Repair-2.jpg b/docs/img/Repair/SparkFun-RTK-Repair-2.jpg new file mode 100644 index 000000000..3fdc9ee6a Binary files /dev/null and b/docs/img/Repair/SparkFun-RTK-Repair-2.jpg differ diff --git a/docs/img/Repair/SparkFun-RTK-Repair-20.jpg b/docs/img/Repair/SparkFun-RTK-Repair-20.jpg new file mode 100644 index 000000000..b7a9df719 Binary files /dev/null and b/docs/img/Repair/SparkFun-RTK-Repair-20.jpg differ diff --git a/docs/img/Repair/SparkFun-RTK-Repair-21.jpg b/docs/img/Repair/SparkFun-RTK-Repair-21.jpg new file mode 100644 index 000000000..6186fbf26 Binary files /dev/null and b/docs/img/Repair/SparkFun-RTK-Repair-21.jpg differ diff --git a/docs/img/Repair/SparkFun-RTK-Repair-22.jpg b/docs/img/Repair/SparkFun-RTK-Repair-22.jpg new file mode 100644 index 000000000..31be97353 Binary files /dev/null and b/docs/img/Repair/SparkFun-RTK-Repair-22.jpg differ diff --git a/docs/img/Repair/SparkFun-RTK-Repair-23.jpg b/docs/img/Repair/SparkFun-RTK-Repair-23.jpg new file mode 100644 index 000000000..e8de3c34f Binary files /dev/null and b/docs/img/Repair/SparkFun-RTK-Repair-23.jpg differ diff --git a/docs/img/Repair/SparkFun-RTK-Repair-24.jpg b/docs/img/Repair/SparkFun-RTK-Repair-24.jpg new file mode 100644 index 000000000..604e33c99 Binary files /dev/null and b/docs/img/Repair/SparkFun-RTK-Repair-24.jpg differ diff --git a/docs/img/Repair/SparkFun-RTK-Repair-25.jpg b/docs/img/Repair/SparkFun-RTK-Repair-25.jpg new file mode 100644 index 000000000..8383257a8 Binary files /dev/null and b/docs/img/Repair/SparkFun-RTK-Repair-25.jpg differ diff --git a/docs/img/Repair/SparkFun-RTK-Repair-26.jpg b/docs/img/Repair/SparkFun-RTK-Repair-26.jpg new file mode 100644 index 000000000..a3426a25e Binary files /dev/null and b/docs/img/Repair/SparkFun-RTK-Repair-26.jpg differ diff --git a/docs/img/Repair/SparkFun-RTK-Repair-3.jpg b/docs/img/Repair/SparkFun-RTK-Repair-3.jpg new file mode 100644 index 000000000..8bb2da53c Binary files /dev/null and b/docs/img/Repair/SparkFun-RTK-Repair-3.jpg differ diff --git a/docs/img/Repair/SparkFun-RTK-Repair-4.jpg b/docs/img/Repair/SparkFun-RTK-Repair-4.jpg new file mode 100644 index 000000000..876054bc1 Binary files /dev/null and b/docs/img/Repair/SparkFun-RTK-Repair-4.jpg differ diff --git a/docs/img/Repair/SparkFun-RTK-Repair-5.jpg b/docs/img/Repair/SparkFun-RTK-Repair-5.jpg new file mode 100644 index 000000000..7b2270665 Binary files /dev/null and b/docs/img/Repair/SparkFun-RTK-Repair-5.jpg differ diff --git a/docs/img/Repair/SparkFun-RTK-Repair-6.jpg b/docs/img/Repair/SparkFun-RTK-Repair-6.jpg new file mode 100644 index 000000000..980bbad12 Binary files /dev/null and b/docs/img/Repair/SparkFun-RTK-Repair-6.jpg differ diff --git a/docs/img/Repair/SparkFun-RTK-Repair-7.jpg b/docs/img/Repair/SparkFun-RTK-Repair-7.jpg new file mode 100644 index 000000000..da2b78517 Binary files /dev/null and b/docs/img/Repair/SparkFun-RTK-Repair-7.jpg differ diff --git a/docs/img/Repair/SparkFun-RTK-Repair-8.jpg b/docs/img/Repair/SparkFun-RTK-Repair-8.jpg new file mode 100644 index 000000000..a7a70f5f2 Binary files /dev/null and b/docs/img/Repair/SparkFun-RTK-Repair-8.jpg differ diff --git a/docs/img/Repair/SparkFun-RTK-Repair-9.jpg b/docs/img/Repair/SparkFun-RTK-Repair-9.jpg new file mode 100644 index 000000000..37f027b48 Binary files /dev/null and b/docs/img/Repair/SparkFun-RTK-Repair-9.jpg differ diff --git a/docs/img/SWMaps/SW_Maps_-_NTRIP_Client.jpg b/docs/img/SWMaps/SW_Maps_-_NTRIP_Client.jpg new file mode 100644 index 000000000..13842f967 Binary files /dev/null and b/docs/img/SWMaps/SW_Maps_-_NTRIP_Client.jpg differ diff --git a/docs/img/SWMaps/SparkFun NTRIP Skylark 2 - SW Maps Credentials.png b/docs/img/SWMaps/SparkFun NTRIP Skylark 2 - SW Maps Credentials.png new file mode 100644 index 000000000..f865768d1 Binary files /dev/null and b/docs/img/SWMaps/SparkFun NTRIP Skylark 2 - SW Maps Credentials.png differ diff --git a/docs/img/SWMaps/SparkFun NTRIP Skylark 2 - SW Maps HPA.png b/docs/img/SWMaps/SparkFun NTRIP Skylark 2 - SW Maps HPA.png new file mode 100644 index 000000000..1722817ce Binary files /dev/null and b/docs/img/SWMaps/SparkFun NTRIP Skylark 2 - SW Maps HPA.png differ diff --git a/docs/img/SWMaps/SparkFun RTK SW Maps - Green Bubble-1.png b/docs/img/SWMaps/SparkFun RTK SW Maps - Green Bubble-1.png new file mode 100644 index 000000000..1d9335952 Binary files /dev/null and b/docs/img/SWMaps/SparkFun RTK SW Maps - Green Bubble-1.png differ diff --git a/docs/img/SWMaps/SparkFun RTK SW Maps - Green Bubble.png b/docs/img/SWMaps/SparkFun RTK SW Maps - Green Bubble.png new file mode 100644 index 000000000..ab67f155f Binary files /dev/null and b/docs/img/SWMaps/SparkFun RTK SW Maps - Green Bubble.png differ diff --git a/docs/img/SWMaps/SparkFun RTK SW Maps - NTRIP Credentials.png b/docs/img/SWMaps/SparkFun RTK SW Maps - NTRIP Credentials.png new file mode 100644 index 000000000..4481b0488 Binary files /dev/null and b/docs/img/SWMaps/SparkFun RTK SW Maps - NTRIP Credentials.png differ diff --git a/docs/img/SWMaps/SparkFun RTK SW Maps for Android QR Code.png b/docs/img/SWMaps/SparkFun RTK SW Maps for Android QR Code.png new file mode 100644 index 000000000..9abf4c7b9 Binary files /dev/null and b/docs/img/SWMaps/SparkFun RTK SW Maps for Android QR Code.png differ diff --git a/docs/img/SWMaps/SparkFun RTK SW Maps for Apple QR Code.png b/docs/img/SWMaps/SparkFun RTK SW Maps for Apple QR Code.png new file mode 100644 index 000000000..c53f5a4f1 Binary files /dev/null and b/docs/img/SWMaps/SparkFun RTK SW Maps for Apple QR Code.png differ diff --git a/docs/img/SWMaps/SparkFun RTK SWMaps Bluetooth Connect.png b/docs/img/SWMaps/SparkFun RTK SWMaps Bluetooth Connect.png new file mode 100644 index 000000000..4df942b9a Binary files /dev/null and b/docs/img/SWMaps/SparkFun RTK SWMaps Bluetooth Connect.png differ diff --git a/docs/img/SWMaps/SparkFun RTK SWMaps GNSS Status.png b/docs/img/SWMaps/SparkFun RTK SWMaps GNSS Status.png new file mode 100644 index 000000000..e22b4b0ff Binary files /dev/null and b/docs/img/SWMaps/SparkFun RTK SWMaps GNSS Status.png differ diff --git a/docs/img/SWMaps/SparkFun RTK SWMaps Sky Plot.png b/docs/img/SWMaps/SparkFun RTK SWMaps Sky Plot.png new file mode 100644 index 000000000..aaa11f74e Binary files /dev/null and b/docs/img/SWMaps/SparkFun RTK SWMaps Sky Plot.png differ diff --git a/docs/img/SWMaps/SparkFun_RTK_Surveyor_-_SW_Maps_NTRIP_Connection.jpg b/docs/img/SWMaps/SparkFun_RTK_Surveyor_-_SW_Maps_NTRIP_Connection.jpg new file mode 100644 index 000000000..bc9eb7cc0 Binary files /dev/null and b/docs/img/SWMaps/SparkFun_RTK_Surveyor_-_SW_Maps_NTRIP_Connection.jpg differ diff --git a/docs/img/Serial/RTK_Surveyor_-_Firmware_Update_COM_Port.jpg b/docs/img/Serial/RTK_Surveyor_-_Firmware_Update_COM_Port.jpg new file mode 100644 index 000000000..2b41ab205 Binary files /dev/null and b/docs/img/Serial/RTK_Surveyor_-_Firmware_Update_COM_Port.jpg differ diff --git a/docs/img/Serial/SparkFun RTK Firmware Uploader COM Port.jpg b/docs/img/Serial/SparkFun RTK Firmware Uploader COM Port.jpg new file mode 100644 index 000000000..03e78b2da Binary files /dev/null and b/docs/img/Serial/SparkFun RTK Firmware Uploader COM Port.jpg differ diff --git a/docs/img/Serial/SparkFun_RTK_Facet_-_Multiple_COM_Ports.jpg b/docs/img/Serial/SparkFun_RTK_Facet_-_Multiple_COM_Ports.jpg new file mode 100644 index 000000000..a80edf1a2 Binary files /dev/null and b/docs/img/Serial/SparkFun_RTK_Facet_-_Multiple_COM_Ports.jpg differ diff --git a/docs/img/Serial/SparkFun_RTK_Facet_-_Ports_-_USB.jpg b/docs/img/Serial/SparkFun_RTK_Facet_-_Ports_-_USB.jpg new file mode 100644 index 000000000..d3dd5eaf1 Binary files /dev/null and b/docs/img/Serial/SparkFun_RTK_Facet_-_Ports_-_USB.jpg differ diff --git a/docs/img/Serial/SparkFun_RTK_Surveyor_-_Connectors1.jpg b/docs/img/Serial/SparkFun_RTK_Surveyor_-_Connectors1.jpg new file mode 100644 index 000000000..64c475d70 Binary files /dev/null and b/docs/img/Serial/SparkFun_RTK_Surveyor_-_Connectors1.jpg differ diff --git a/docs/img/SparkFun RTK Device Attached to Monopole.png b/docs/img/SparkFun RTK Device Attached to Monopole.png new file mode 100644 index 000000000..cd34d7f72 Binary files /dev/null and b/docs/img/SparkFun RTK Device Attached to Monopole.png differ diff --git a/docs/img/SparkFun RTK Express Plus.png b/docs/img/SparkFun RTK Express Plus.png new file mode 100644 index 000000000..27f7d5c88 Binary files /dev/null and b/docs/img/SparkFun RTK Express Plus.png differ diff --git a/docs/img/SparkFun RTK Express.png b/docs/img/SparkFun RTK Express.png new file mode 100644 index 000000000..ddfec7a37 Binary files /dev/null and b/docs/img/SparkFun RTK Express.png differ diff --git a/docs/img/SparkFun RTK Facet L-Band u-blox Firmware Update GUI.png b/docs/img/SparkFun RTK Facet L-Band u-blox Firmware Update GUI.png new file mode 100644 index 000000000..d24b2a12a Binary files /dev/null and b/docs/img/SparkFun RTK Facet L-Band u-blox Firmware Update GUI.png differ diff --git a/docs/img/SparkFun RTK Facet L-Band.png b/docs/img/SparkFun RTK Facet L-Band.png new file mode 100644 index 000000000..4af441ac9 Binary files /dev/null and b/docs/img/SparkFun RTK Facet L-Band.png differ diff --git a/docs/img/SparkFun RTK Facet.png b/docs/img/SparkFun RTK Facet.png new file mode 100644 index 000000000..c8447713a Binary files /dev/null and b/docs/img/SparkFun RTK Facet.png differ diff --git a/docs/img/SparkFun RTK Firmware Update GUI - 4MB.png b/docs/img/SparkFun RTK Firmware Update GUI - 4MB.png new file mode 100644 index 000000000..db10c5277 Binary files /dev/null and b/docs/img/SparkFun RTK Firmware Update GUI - 4MB.png differ diff --git a/docs/img/SparkFun RTK Reference Station.png b/docs/img/SparkFun RTK Reference Station.png new file mode 100644 index 000000000..cbf5d0315 Binary files /dev/null and b/docs/img/SparkFun RTK Reference Station.png differ diff --git a/docs/img/SparkFun RTK Settings File - Factory Reset.png b/docs/img/SparkFun RTK Settings File - Factory Reset.png new file mode 100644 index 000000000..82ce96779 Binary files /dev/null and b/docs/img/SparkFun RTK Settings File - Factory Reset.png differ diff --git a/docs/img/SparkFun RTK Surveyor.png b/docs/img/SparkFun RTK Surveyor.png new file mode 100644 index 000000000..2064332f8 Binary files /dev/null and b/docs/img/SparkFun RTK Surveyor.png differ diff --git a/docs/img/SparkFun ZED-F9P Navigation Rates.png b/docs/img/SparkFun ZED-F9P Navigation Rates.png new file mode 100644 index 000000000..a0b7c5034 Binary files /dev/null and b/docs/img/SparkFun ZED-F9P Navigation Rates.png differ diff --git a/docs/img/SparkFun_GNSS_RTK_Reference_Station_IO.jpg b/docs/img/SparkFun_GNSS_RTK_Reference_Station_IO.jpg new file mode 100644 index 000000000..39a0e55be Binary files /dev/null and b/docs/img/SparkFun_GNSS_RTK_Reference_Station_IO.jpg differ diff --git a/docs/img/SparkFun_RTK_Express_-_Data_Port_USB.jpg b/docs/img/SparkFun_RTK_Express_-_Data_Port_USB.jpg new file mode 100644 index 000000000..6cabaf657 Binary files /dev/null and b/docs/img/SparkFun_RTK_Express_-_Data_Port_USB.jpg differ diff --git a/docs/img/SparkFun_RTK_Express_-_Ports_Menu_MON-COMM_Overrun.jpg b/docs/img/SparkFun_RTK_Express_-_Ports_Menu_MON-COMM_Overrun.jpg new file mode 100644 index 000000000..ca07cab19 Binary files /dev/null and b/docs/img/SparkFun_RTK_Express_-_Ports_Menu_MON-COMM_Overrun.jpg differ diff --git a/docs/img/SparkFun_RTK_Express_-_Settings_File.jpg b/docs/img/SparkFun_RTK_Express_-_Settings_File.jpg new file mode 100644 index 000000000..2a0563980 Binary files /dev/null and b/docs/img/SparkFun_RTK_Express_-_Settings_File.jpg differ diff --git a/docs/img/SparkFun_RTK_Facet_-_Data_Port_to_USB.jpg b/docs/img/SparkFun_RTK_Facet_-_Data_Port_to_USB.jpg new file mode 100644 index 000000000..9312f73ac Binary files /dev/null and b/docs/img/SparkFun_RTK_Facet_-_Data_Port_to_USB.jpg differ diff --git a/docs/img/SparkFun_RTK_Facet_L-Band_ARP.jpg b/docs/img/SparkFun_RTK_Facet_L-Band_ARP.jpg new file mode 100644 index 000000000..b303e084b Binary files /dev/null and b/docs/img/SparkFun_RTK_Facet_L-Band_ARP.jpg differ diff --git a/docs/img/SparkFun_RTK_Facet_Profile.jpg b/docs/img/SparkFun_RTK_Facet_Profile.jpg new file mode 100644 index 000000000..e104395f6 Binary files /dev/null and b/docs/img/SparkFun_RTK_Facet_Profile.jpg differ diff --git a/docs/img/SparkFun_RTK_Reference_Station.jpg b/docs/img/SparkFun_RTK_Reference_Station.jpg new file mode 100644 index 000000000..5d29bbb81 Binary files /dev/null and b/docs/img/SparkFun_RTK_Reference_Station.jpg differ diff --git a/docs/img/SparkFun_RTK_Surveyor_-_Data_Port_HiRes.jpg b/docs/img/SparkFun_RTK_Surveyor_-_Data_Port_HiRes.jpg new file mode 100644 index 000000000..b81f439a4 Binary files /dev/null and b/docs/img/SparkFun_RTK_Surveyor_-_Data_Port_HiRes.jpg differ diff --git a/docs/img/SurPad/SparkFun RTK - SurPad - Communication NTRIP Connected.png b/docs/img/SurPad/SparkFun RTK - SurPad - Communication NTRIP Connected.png new file mode 100644 index 000000000..50eb6e752 Binary files /dev/null and b/docs/img/SurPad/SparkFun RTK - SurPad - Communication NTRIP Connected.png differ diff --git a/docs/img/SurPad/SparkFun RTK - SurPad - Communication.png b/docs/img/SurPad/SparkFun RTK - SurPad - Communication.png new file mode 100644 index 000000000..748b261cf Binary files /dev/null and b/docs/img/SurPad/SparkFun RTK - SurPad - Communication.png differ diff --git a/docs/img/SurPad/SparkFun RTK - SurPad - Data Link.png b/docs/img/SurPad/SparkFun RTK - SurPad - Data Link.png new file mode 100644 index 000000000..12b23ee2c Binary files /dev/null and b/docs/img/SurPad/SparkFun RTK - SurPad - Data Link.png differ diff --git a/docs/img/SurPad/SparkFun RTK - SurPad - Home Screen.png b/docs/img/SurPad/SparkFun RTK - SurPad - Home Screen.png new file mode 100644 index 000000000..69204aa4f Binary files /dev/null and b/docs/img/SurPad/SparkFun RTK - SurPad - Home Screen.png differ diff --git a/docs/img/SurPad/SparkFun RTK - SurPad - Map with RTK Fix.png b/docs/img/SurPad/SparkFun RTK - SurPad - Map with RTK Fix.png new file mode 100644 index 000000000..b3dce5f35 Binary files /dev/null and b/docs/img/SurPad/SparkFun RTK - SurPad - Map with RTK Fix.png differ diff --git a/docs/img/SurPad/SparkFun RTK - SurPad - Point Survey.png b/docs/img/SurPad/SparkFun RTK - SurPad - Point Survey.png new file mode 100644 index 000000000..f40fa8ca4 Binary files /dev/null and b/docs/img/SurPad/SparkFun RTK - SurPad - Point Survey.png differ diff --git a/docs/img/SurvPC/SparkFun RTK Software - SurvPC Equip Menu.jpg b/docs/img/SurvPC/SparkFun RTK Software - SurvPC Equip Menu.jpg new file mode 100644 index 000000000..83c9fbd8d Binary files /dev/null and b/docs/img/SurvPC/SparkFun RTK Software - SurvPC Equip Menu.jpg differ diff --git a/docs/img/SurvPC/SparkFun RTK Software - SurvPC NTRIP Client.jpg b/docs/img/SurvPC/SparkFun RTK Software - SurvPC NTRIP Client.jpg new file mode 100644 index 000000000..eed337d6b Binary files /dev/null and b/docs/img/SurvPC/SparkFun RTK Software - SurvPC NTRIP Client.jpg differ diff --git a/docs/img/SurvPC/SparkFun RTK Software - SurvPC Rover Antenna.jpg b/docs/img/SurvPC/SparkFun RTK Software - SurvPC Rover Antenna.jpg new file mode 100644 index 000000000..1b3b2dfbc Binary files /dev/null and b/docs/img/SurvPC/SparkFun RTK Software - SurvPC Rover Antenna.jpg differ diff --git a/docs/img/SurvPC/SparkFun RTK Software - SurvPC Rover Comms.jpg b/docs/img/SurvPC/SparkFun RTK Software - SurvPC Rover Comms.jpg new file mode 100644 index 000000000..c494e7b80 Binary files /dev/null and b/docs/img/SurvPC/SparkFun RTK Software - SurvPC Rover Comms.jpg differ diff --git a/docs/img/SurvPC/SparkFun RTK Software - SurvPC Rover DGPS.jpg b/docs/img/SurvPC/SparkFun RTK Software - SurvPC Rover DGPS.jpg new file mode 100644 index 000000000..acfc95c81 Binary files /dev/null and b/docs/img/SurvPC/SparkFun RTK Software - SurvPC Rover DGPS.jpg differ diff --git a/docs/img/SurvPC/SparkFun RTK Software - SurvPC Rover Find Device.jpg b/docs/img/SurvPC/SparkFun RTK Software - SurvPC Rover Find Device.jpg new file mode 100644 index 000000000..bacc7c5b6 Binary files /dev/null and b/docs/img/SurvPC/SparkFun RTK Software - SurvPC Rover Find Device.jpg differ diff --git a/docs/img/SurvPC/SparkFun RTK Software - SurvPC Rover NMEA.jpg b/docs/img/SurvPC/SparkFun RTK Software - SurvPC Rover NMEA.jpg new file mode 100644 index 000000000..606a5645c Binary files /dev/null and b/docs/img/SurvPC/SparkFun RTK Software - SurvPC Rover NMEA.jpg differ diff --git a/docs/img/SurvPC/SparkFun RTK Software - SurvPC Rover NTRIP Mount Point.jpg b/docs/img/SurvPC/SparkFun RTK Software - SurvPC Rover NTRIP Mount Point.jpg new file mode 100644 index 000000000..562c98a0c Binary files /dev/null and b/docs/img/SurvPC/SparkFun RTK Software - SurvPC Rover NTRIP Mount Point.jpg differ diff --git a/docs/img/SurvPC/SparkFun RTK Software - SurvPC Rover NTRIP.jpg b/docs/img/SurvPC/SparkFun RTK Software - SurvPC Rover NTRIP.jpg new file mode 100644 index 000000000..92ab7f297 Binary files /dev/null and b/docs/img/SurvPC/SparkFun RTK Software - SurvPC Rover NTRIP.jpg differ diff --git a/docs/img/SurvPC/SparkFun RTK Software - SurvPC Rover Receiver.jpg b/docs/img/SurvPC/SparkFun RTK Software - SurvPC Rover Receiver.jpg new file mode 100644 index 000000000..824b802ca Binary files /dev/null and b/docs/img/SurvPC/SparkFun RTK Software - SurvPC Rover Receiver.jpg differ diff --git a/docs/img/SurvPC/SparkFun RTK Software - SurvPC Rover Select Bluetooth Connect.jpg b/docs/img/SurvPC/SparkFun RTK Software - SurvPC Rover Select Bluetooth Connect.jpg new file mode 100644 index 000000000..f1f68859b Binary files /dev/null and b/docs/img/SurvPC/SparkFun RTK Software - SurvPC Rover Select Bluetooth Connect.jpg differ diff --git a/docs/img/SurvPC/SparkFun RTK Software - SurvPC Rover Select Bluetooth Device With MAC.jpg b/docs/img/SurvPC/SparkFun RTK Software - SurvPC Rover Select Bluetooth Device With MAC.jpg new file mode 100644 index 000000000..aa0ccdc63 Binary files /dev/null and b/docs/img/SurvPC/SparkFun RTK Software - SurvPC Rover Select Bluetooth Device With MAC.jpg differ diff --git a/docs/img/SurvPC/SparkFun RTK Software - SurvPC Rover Select Bluetooth Device.jpg b/docs/img/SurvPC/SparkFun RTK Software - SurvPC Rover Select Bluetooth Device.jpg new file mode 100644 index 000000000..270d7fcd9 Binary files /dev/null and b/docs/img/SurvPC/SparkFun RTK Software - SurvPC Rover Select Bluetooth Device.jpg differ diff --git a/docs/img/SurvPC/SparkFun RTK Software - SurvPC Skyplot.jpg b/docs/img/SurvPC/SparkFun RTK Software - SurvPC Skyplot.jpg new file mode 100644 index 000000000..578290c61 Binary files /dev/null and b/docs/img/SurvPC/SparkFun RTK Software - SurvPC Skyplot.jpg differ diff --git a/docs/img/SurvPC/SparkFun RTK Software - SurvPC Survey.jpg b/docs/img/SurvPC/SparkFun RTK Software - SurvPC Survey.jpg new file mode 100644 index 000000000..0c21a569b Binary files /dev/null and b/docs/img/SurvPC/SparkFun RTK Software - SurvPC Survey.jpg differ diff --git a/docs/img/Survey123/SparkFun RTK Survey123 - Location Status.png b/docs/img/Survey123/SparkFun RTK Survey123 - Location Status.png new file mode 100644 index 000000000..de589681a Binary files /dev/null and b/docs/img/Survey123/SparkFun RTK Survey123 - Location Status.png differ diff --git a/docs/img/Survey123/SparkFun RTK Survey123 - Main.png b/docs/img/Survey123/SparkFun RTK Survey123 - Main.png new file mode 100644 index 000000000..d5361540f Binary files /dev/null and b/docs/img/Survey123/SparkFun RTK Survey123 - Main.png differ diff --git a/docs/img/Survey123/SparkFun RTK Survey123 - Map.png b/docs/img/Survey123/SparkFun RTK Survey123 - Map.png new file mode 100644 index 000000000..2cb085f03 Binary files /dev/null and b/docs/img/Survey123/SparkFun RTK Survey123 - Map.png differ diff --git a/docs/img/Survey123/SparkFun RTK Survey123 - Splash.png b/docs/img/Survey123/SparkFun RTK Survey123 - Splash.png new file mode 100644 index 000000000..e9a50d4b6 Binary files /dev/null and b/docs/img/Survey123/SparkFun RTK Survey123 - Splash.png differ diff --git a/docs/img/SurveyMaster/SparkFun RTK Survey Master - 01.png b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 01.png new file mode 100644 index 000000000..cad3ae156 Binary files /dev/null and b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 01.png differ diff --git a/docs/img/SurveyMaster/SparkFun RTK Survey Master - 02.png b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 02.png new file mode 100644 index 000000000..92ddb7627 Binary files /dev/null and b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 02.png differ diff --git a/docs/img/SurveyMaster/SparkFun RTK Survey Master - 03.png b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 03.png new file mode 100644 index 000000000..1b84abc85 Binary files /dev/null and b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 03.png differ diff --git a/docs/img/SurveyMaster/SparkFun RTK Survey Master - 04.png b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 04.png new file mode 100644 index 000000000..02dc32a47 Binary files /dev/null and b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 04.png differ diff --git a/docs/img/SurveyMaster/SparkFun RTK Survey Master - 05.png b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 05.png new file mode 100644 index 000000000..2662556a5 Binary files /dev/null and b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 05.png differ diff --git a/docs/img/SurveyMaster/SparkFun RTK Survey Master - 09.png b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 09.png new file mode 100644 index 000000000..2638c9089 Binary files /dev/null and b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 09.png differ diff --git a/docs/img/SurveyMaster/SparkFun RTK Survey Master - 10.png b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 10.png new file mode 100644 index 000000000..ea6f54efa Binary files /dev/null and b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 10.png differ diff --git a/docs/img/SurveyMaster/SparkFun RTK Survey Master - 11.png b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 11.png new file mode 100644 index 000000000..a7f047d2a Binary files /dev/null and b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 11.png differ diff --git a/docs/img/SurveyMaster/SparkFun RTK Survey Master - 12.png b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 12.png new file mode 100644 index 000000000..8dffb7401 Binary files /dev/null and b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 12.png differ diff --git a/docs/img/SurveyMaster/SparkFun RTK Survey Master - 13.png b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 13.png new file mode 100644 index 000000000..9288f0445 Binary files /dev/null and b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 13.png differ diff --git a/docs/img/SurveyMaster/SparkFun RTK Survey Master - 14.png b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 14.png new file mode 100644 index 000000000..75fb883f4 Binary files /dev/null and b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 14.png differ diff --git a/docs/img/SurveyMaster/SparkFun RTK Survey Master - 15.png b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 15.png new file mode 100644 index 000000000..216cc7996 Binary files /dev/null and b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 15.png differ diff --git a/docs/img/SurveyMaster/SparkFun RTK Survey Master - 16.png b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 16.png new file mode 100644 index 000000000..be9aa561e Binary files /dev/null and b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 16.png differ diff --git a/docs/img/SurveyMaster/SparkFun RTK Survey Master - 21.png b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 21.png new file mode 100644 index 000000000..9b3d34c3c Binary files /dev/null and b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 21.png differ diff --git a/docs/img/SurveyMaster/SparkFun RTK Survey Master - 22.png b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 22.png new file mode 100644 index 000000000..da353a8e2 Binary files /dev/null and b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 22.png differ diff --git a/docs/img/SurveyMaster/SparkFun RTK Survey Master - 23.png b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 23.png new file mode 100644 index 000000000..ba4194c0c Binary files /dev/null and b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 23.png differ diff --git a/docs/img/SurveyMaster/SparkFun RTK Survey Master - 25.png b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 25.png new file mode 100644 index 000000000..06243861b Binary files /dev/null and b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 25.png differ diff --git a/docs/img/SurveyMaster/SparkFun RTK Survey Master - 26 .jpg b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 26 .jpg new file mode 100644 index 000000000..551326a57 Binary files /dev/null and b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 26 .jpg differ diff --git a/docs/img/SurveyMaster/SparkFun RTK Survey Master - 27.png b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 27.png new file mode 100644 index 000000000..5c0481167 Binary files /dev/null and b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 27.png differ diff --git a/docs/img/SurveyMaster/SparkFun RTK Survey Master - 29.png b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 29.png new file mode 100644 index 000000000..00864328f Binary files /dev/null and b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 29.png differ diff --git a/docs/img/SurveyMaster/SparkFun RTK Survey Master - 30.png b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 30.png new file mode 100644 index 000000000..b97dc5591 Binary files /dev/null and b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 30.png differ diff --git a/docs/img/SurveyMaster/SparkFun RTK Survey Master - 33.png b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 33.png new file mode 100644 index 000000000..9d2d40731 Binary files /dev/null and b/docs/img/SurveyMaster/SparkFun RTK Survey Master - 33.png differ diff --git a/docs/img/Terminal/Ethernet_DHCP.png b/docs/img/Terminal/Ethernet_DHCP.png new file mode 100644 index 000000000..632da78a0 Binary files /dev/null and b/docs/img/Terminal/Ethernet_DHCP.png differ diff --git a/docs/img/Terminal/Ethernet_Fixed_IP.png b/docs/img/Terminal/Ethernet_Fixed_IP.png new file mode 100644 index 000000000..7dd3f35d1 Binary files /dev/null and b/docs/img/Terminal/Ethernet_Fixed_IP.png differ diff --git a/docs/img/Terminal/Ethernet_TCP_Client_1.png b/docs/img/Terminal/Ethernet_TCP_Client_1.png new file mode 100644 index 000000000..cada17a8b Binary files /dev/null and b/docs/img/Terminal/Ethernet_TCP_Client_1.png differ diff --git a/docs/img/Terminal/RTK_Surveyor_-_Device_Configuration_-_NTRIP_Server_Broadcasting_Bytes_v11.jpg b/docs/img/Terminal/RTK_Surveyor_-_Device_Configuration_-_NTRIP_Server_Broadcasting_Bytes_v11.jpg new file mode 100644 index 000000000..8002e19f1 Binary files /dev/null and b/docs/img/Terminal/RTK_Surveyor_-_Device_Configuration_-_NTRIP_Server_Broadcasting_Bytes_v11.jpg differ diff --git a/docs/img/Terminal/RTK_Surveyor_-_Device_Configuration_-_NTRIP_Server_Broadcasting_v11.jpg b/docs/img/Terminal/RTK_Surveyor_-_Device_Configuration_-_NTRIP_Server_Broadcasting_v11.jpg new file mode 100644 index 000000000..f10dfe00c Binary files /dev/null and b/docs/img/Terminal/RTK_Surveyor_-_Device_Configuration_-_NTRIP_Server_Broadcasting_v11.jpg differ diff --git a/docs/img/Terminal/SparkFun RTK - Alternate Coordinate Types for Fixed Base Serial.png b/docs/img/Terminal/SparkFun RTK - Alternate Coordinate Types for Fixed Base Serial.png new file mode 100644 index 000000000..a9a64764c Binary files /dev/null and b/docs/img/Terminal/SparkFun RTK - Alternate Coordinate Types for Fixed Base Serial.png differ diff --git a/docs/img/Terminal/SparkFun RTK - Sensor Menu.png b/docs/img/Terminal/SparkFun RTK - Sensor Menu.png new file mode 100644 index 000000000..d74cabf46 Binary files /dev/null and b/docs/img/Terminal/SparkFun RTK - Sensor Menu.png differ diff --git a/docs/img/Terminal/SparkFun RTK Debug Menu.png b/docs/img/Terminal/SparkFun RTK Debug Menu.png new file mode 100644 index 000000000..b6f575322 Binary files /dev/null and b/docs/img/Terminal/SparkFun RTK Debug Menu.png differ diff --git a/docs/img/Terminal/SparkFun RTK Firmware Update CLI.png b/docs/img/Terminal/SparkFun RTK Firmware Update CLI.png new file mode 100644 index 000000000..90e45741e Binary files /dev/null and b/docs/img/Terminal/SparkFun RTK Firmware Update CLI.png differ diff --git a/docs/img/Terminal/SparkFun RTK Firmware Update Menu.png b/docs/img/Terminal/SparkFun RTK Firmware Update Menu.png new file mode 100644 index 000000000..6d0fbfdd0 Binary files /dev/null and b/docs/img/Terminal/SparkFun RTK Firmware Update Menu.png differ diff --git a/docs/img/Terminal/SparkFun RTK Firmware Update OTA.png b/docs/img/Terminal/SparkFun RTK Firmware Update OTA.png new file mode 100644 index 000000000..4a89fe331 Binary files /dev/null and b/docs/img/Terminal/SparkFun RTK Firmware Update OTA.png differ diff --git a/docs/img/Terminal/SparkFun RTK Logging Menu.png b/docs/img/Terminal/SparkFun RTK Logging Menu.png new file mode 100644 index 000000000..4a339e310 Binary files /dev/null and b/docs/img/Terminal/SparkFun RTK Logging Menu.png differ diff --git a/docs/img/Terminal/SparkFun RTK Main Menu.png b/docs/img/Terminal/SparkFun RTK Main Menu.png new file mode 100644 index 000000000..407339520 Binary files /dev/null and b/docs/img/Terminal/SparkFun RTK Main Menu.png differ diff --git a/docs/img/Terminal/SparkFun RTK Network Menu.png b/docs/img/Terminal/SparkFun RTK Network Menu.png new file mode 100644 index 000000000..753b455e6 Binary files /dev/null and b/docs/img/Terminal/SparkFun RTK Network Menu.png differ diff --git a/docs/img/Terminal/SparkFun RTK PointPerfect Menu.png b/docs/img/Terminal/SparkFun RTK PointPerfect Menu.png new file mode 100644 index 000000000..3409d6c3f Binary files /dev/null and b/docs/img/Terminal/SparkFun RTK PointPerfect Menu.png differ diff --git a/docs/img/Terminal/SparkFun RTK Radio Menu.png b/docs/img/Terminal/SparkFun RTK Radio Menu.png new file mode 100644 index 000000000..06c9d09bf Binary files /dev/null and b/docs/img/Terminal/SparkFun RTK Radio Menu.png differ diff --git a/docs/img/Terminal/SparkFun RTK Software - Add Bluetooth Device 5.jpg b/docs/img/Terminal/SparkFun RTK Software - Add Bluetooth Device 5.jpg new file mode 100644 index 000000000..3f1ce0df8 Binary files /dev/null and b/docs/img/Terminal/SparkFun RTK Software - Add Bluetooth Device 5.jpg differ diff --git a/docs/img/Terminal/SparkFun RTK System Menu - Factory Reset.png b/docs/img/Terminal/SparkFun RTK System Menu - Factory Reset.png new file mode 100644 index 000000000..d7e9736a4 Binary files /dev/null and b/docs/img/Terminal/SparkFun RTK System Menu - Factory Reset.png differ diff --git a/docs/img/Terminal/SparkFun RTK System Menu.png b/docs/img/Terminal/SparkFun RTK System Menu.png new file mode 100644 index 000000000..2d9ad822d Binary files /dev/null and b/docs/img/Terminal/SparkFun RTK System Menu.png differ diff --git a/docs/img/Terminal/SparkFun RTK System Status Trigger.png b/docs/img/Terminal/SparkFun RTK System Status Trigger.png new file mode 100644 index 000000000..47c15b1be Binary files /dev/null and b/docs/img/Terminal/SparkFun RTK System Status Trigger.png differ diff --git a/docs/img/Terminal/SparkFun RTK WiFi Menu Terminal.png b/docs/img/Terminal/SparkFun RTK WiFi Menu Terminal.png new file mode 100644 index 000000000..48cb4bc0f Binary files /dev/null and b/docs/img/Terminal/SparkFun RTK WiFi Menu Terminal.png differ diff --git a/docs/img/Terminal/SparkFun_RTK_ExpressPlus_MainMenu.jpg b/docs/img/Terminal/SparkFun_RTK_ExpressPlus_MainMenu.jpg new file mode 100644 index 000000000..4ad35548b Binary files /dev/null and b/docs/img/Terminal/SparkFun_RTK_ExpressPlus_MainMenu.jpg differ diff --git a/docs/img/Terminal/SparkFun_RTK_ExpressPlus_Profiles.jpg b/docs/img/Terminal/SparkFun_RTK_ExpressPlus_Profiles.jpg new file mode 100644 index 000000000..ad6217699 Binary files /dev/null and b/docs/img/Terminal/SparkFun_RTK_ExpressPlus_Profiles.jpg differ diff --git a/docs/img/Terminal/SparkFun_RTK_ExpressPlus_ReceiverNTRIP.jpg b/docs/img/Terminal/SparkFun_RTK_ExpressPlus_ReceiverNTRIP.jpg new file mode 100644 index 000000000..ecc3925c4 Binary files /dev/null and b/docs/img/Terminal/SparkFun_RTK_ExpressPlus_ReceiverNTRIP.jpg differ diff --git a/docs/img/Terminal/SparkFun_RTK_ExpressPlus_Receiver_Constellations.jpg b/docs/img/Terminal/SparkFun_RTK_ExpressPlus_Receiver_Constellations.jpg new file mode 100644 index 000000000..e84f7c8a9 Binary files /dev/null and b/docs/img/Terminal/SparkFun_RTK_ExpressPlus_Receiver_Constellations.jpg differ diff --git a/docs/img/Terminal/SparkFun_RTK_Express_-_Base_Menu.jpg b/docs/img/Terminal/SparkFun_RTK_Express_-_Base_Menu.jpg new file mode 100644 index 000000000..b55fd96a0 Binary files /dev/null and b/docs/img/Terminal/SparkFun_RTK_Express_-_Base_Menu.jpg differ diff --git a/docs/img/Terminal/SparkFun_RTK_Express_-_Base_Menu_-_Fixed_NTRIP.jpg b/docs/img/Terminal/SparkFun_RTK_Express_-_Base_Menu_-_Fixed_NTRIP.jpg new file mode 100644 index 000000000..9092ddce6 Binary files /dev/null and b/docs/img/Terminal/SparkFun_RTK_Express_-_Base_Menu_-_Fixed_NTRIP.jpg differ diff --git a/docs/img/Terminal/SparkFun_RTK_Express_-_Messages_Menu.jpg b/docs/img/Terminal/SparkFun_RTK_Express_-_Messages_Menu.jpg new file mode 100644 index 000000000..2a7e4e153 Binary files /dev/null and b/docs/img/Terminal/SparkFun_RTK_Express_-_Messages_Menu.jpg differ diff --git a/docs/img/Terminal/SparkFun_RTK_Express_-_Messages_Menu_-_NMEA.jpg b/docs/img/Terminal/SparkFun_RTK_Express_-_Messages_Menu_-_NMEA.jpg new file mode 100644 index 000000000..e046f3052 Binary files /dev/null and b/docs/img/Terminal/SparkFun_RTK_Express_-_Messages_Menu_-_NMEA.jpg differ diff --git a/docs/img/Terminal/SparkFun_RTK_Express_-_Ports_Menu.jpg b/docs/img/Terminal/SparkFun_RTK_Express_-_Ports_Menu.jpg new file mode 100644 index 000000000..9f4048593 Binary files /dev/null and b/docs/img/Terminal/SparkFun_RTK_Express_-_Ports_Menu.jpg differ diff --git a/docs/img/Terminal/SparkFun_RTK_Express_-_Ports_Menu_Mux.jpg b/docs/img/Terminal/SparkFun_RTK_Express_-_Ports_Menu_Mux.jpg new file mode 100644 index 000000000..a6e11240b Binary files /dev/null and b/docs/img/Terminal/SparkFun_RTK_Express_-_Ports_Menu_Mux.jpg differ diff --git a/docs/img/Terminal/SparkFun_RTK_Firmware_Update-ProgressBar.jpg b/docs/img/Terminal/SparkFun_RTK_Firmware_Update-ProgressBar.jpg new file mode 100644 index 000000000..6e07f3f3d Binary files /dev/null and b/docs/img/Terminal/SparkFun_RTK_Firmware_Update-ProgressBar.jpg differ diff --git a/docs/img/Terminal/SparkFun_RTK_LBand_ManualKeysA.jpg b/docs/img/Terminal/SparkFun_RTK_LBand_ManualKeysA.jpg new file mode 100644 index 000000000..1daee5e98 Binary files /dev/null and b/docs/img/Terminal/SparkFun_RTK_LBand_ManualKeysA.jpg differ diff --git a/docs/img/Terminal/SparkFun_RTK_Surveyor_-_Data_Output.jpg b/docs/img/Terminal/SparkFun_RTK_Surveyor_-_Data_Output.jpg new file mode 100644 index 000000000..66f5b2f93 Binary files /dev/null and b/docs/img/Terminal/SparkFun_RTK_Surveyor_-_Data_Output.jpg differ diff --git a/docs/img/Terminal/TCP_Client.gif b/docs/img/Terminal/TCP_Client.gif new file mode 100644 index 000000000..6280e4976 Binary files /dev/null and b/docs/img/Terminal/TCP_Client.gif differ diff --git a/docs/img/VerifyAccuracy/SparkFun Verify RTK - 1 Boulder Sites.jpg b/docs/img/VerifyAccuracy/SparkFun Verify RTK - 1 Boulder Sites.jpg new file mode 100644 index 000000000..ddf65d1c6 Binary files /dev/null and b/docs/img/VerifyAccuracy/SparkFun Verify RTK - 1 Boulder Sites.jpg differ diff --git a/docs/img/VerifyAccuracy/SparkFun Verify RTK - 10 Dots on Map.jpg b/docs/img/VerifyAccuracy/SparkFun Verify RTK - 10 Dots on Map.jpg new file mode 100644 index 000000000..10125adb3 Binary files /dev/null and b/docs/img/VerifyAccuracy/SparkFun Verify RTK - 10 Dots on Map.jpg differ diff --git a/docs/img/VerifyAccuracy/SparkFun Verify RTK - 11 LLA to ECEF.jpg b/docs/img/VerifyAccuracy/SparkFun Verify RTK - 11 LLA to ECEF.jpg new file mode 100644 index 000000000..5257bed0b Binary files /dev/null and b/docs/img/VerifyAccuracy/SparkFun Verify RTK - 11 LLA to ECEF.jpg differ diff --git a/docs/img/VerifyAccuracy/SparkFun Verify RTK - 12 Marker on Map.jpg b/docs/img/VerifyAccuracy/SparkFun Verify RTK - 12 Marker on Map.jpg new file mode 100644 index 000000000..803d4f22b Binary files /dev/null and b/docs/img/VerifyAccuracy/SparkFun Verify RTK - 12 Marker on Map.jpg differ diff --git a/docs/img/VerifyAccuracy/SparkFun Verify RTK - 13 Compare Points.jpg b/docs/img/VerifyAccuracy/SparkFun Verify RTK - 13 Compare Points.jpg new file mode 100644 index 000000000..f770b5d71 Binary files /dev/null and b/docs/img/VerifyAccuracy/SparkFun Verify RTK - 13 Compare Points.jpg differ diff --git a/docs/img/VerifyAccuracy/SparkFun Verify RTK - 13 Datasheet for Monument.jpg b/docs/img/VerifyAccuracy/SparkFun Verify RTK - 13 Datasheet for Monument.jpg new file mode 100644 index 000000000..2f87b8666 Binary files /dev/null and b/docs/img/VerifyAccuracy/SparkFun Verify RTK - 13 Datasheet for Monument.jpg differ diff --git a/docs/img/VerifyAccuracy/SparkFun Verify RTK - 14 Image Pixel Comparison.jpg b/docs/img/VerifyAccuracy/SparkFun Verify RTK - 14 Image Pixel Comparison.jpg new file mode 100644 index 000000000..9692ae54f Binary files /dev/null and b/docs/img/VerifyAccuracy/SparkFun Verify RTK - 14 Image Pixel Comparison.jpg differ diff --git a/docs/img/VerifyAccuracy/SparkFun Verify RTK - 15 Spreadsheet Results.jpg b/docs/img/VerifyAccuracy/SparkFun Verify RTK - 15 Spreadsheet Results.jpg new file mode 100644 index 000000000..fbbcaeeae Binary files /dev/null and b/docs/img/VerifyAccuracy/SparkFun Verify RTK - 15 Spreadsheet Results.jpg differ diff --git a/docs/img/VerifyAccuracy/SparkFun Verify RTK - 16 Facet in the Field.jpg b/docs/img/VerifyAccuracy/SparkFun Verify RTK - 16 Facet in the Field.jpg new file mode 100644 index 000000000..faa364a5b Binary files /dev/null and b/docs/img/VerifyAccuracy/SparkFun Verify RTK - 16 Facet in the Field.jpg differ diff --git a/docs/img/VerifyAccuracy/SparkFun Verify RTK - 17 Surveyor Monument - Big.jpg b/docs/img/VerifyAccuracy/SparkFun Verify RTK - 17 Surveyor Monument - Big.jpg new file mode 100644 index 000000000..b676dd0bb Binary files /dev/null and b/docs/img/VerifyAccuracy/SparkFun Verify RTK - 17 Surveyor Monument - Big.jpg differ diff --git a/docs/img/VerifyAccuracy/SparkFun Verify RTK - 17 Surveyor Monument.jpg b/docs/img/VerifyAccuracy/SparkFun Verify RTK - 17 Surveyor Monument.jpg new file mode 100644 index 000000000..620725ac5 Binary files /dev/null and b/docs/img/VerifyAccuracy/SparkFun Verify RTK - 17 Surveyor Monument.jpg differ diff --git a/docs/img/VerifyAccuracy/SparkFun Verify RTK - 2 Boulder GPS Sites.jpg b/docs/img/VerifyAccuracy/SparkFun Verify RTK - 2 Boulder GPS Sites.jpg new file mode 100644 index 000000000..4c4011d62 Binary files /dev/null and b/docs/img/VerifyAccuracy/SparkFun Verify RTK - 2 Boulder GPS Sites.jpg differ diff --git a/docs/img/VerifyAccuracy/SparkFun Verify RTK - 3 SparkFun HQ.jpg b/docs/img/VerifyAccuracy/SparkFun Verify RTK - 3 SparkFun HQ.jpg new file mode 100644 index 000000000..8df6f49d7 Binary files /dev/null and b/docs/img/VerifyAccuracy/SparkFun Verify RTK - 3 SparkFun HQ.jpg differ diff --git a/docs/img/VerifyAccuracy/SparkFun Verify RTK - 4 Conversion to Decimal.jpg b/docs/img/VerifyAccuracy/SparkFun Verify RTK - 4 Conversion to Decimal.jpg new file mode 100644 index 000000000..36d75e1a5 Binary files /dev/null and b/docs/img/VerifyAccuracy/SparkFun Verify RTK - 4 Conversion to Decimal.jpg differ diff --git a/docs/img/VerifyAccuracy/SparkFun Verify RTK - 5 Conversion to WGS84.jpg b/docs/img/VerifyAccuracy/SparkFun Verify RTK - 5 Conversion to WGS84.jpg new file mode 100644 index 000000000..0d78a162c Binary files /dev/null and b/docs/img/VerifyAccuracy/SparkFun Verify RTK - 5 Conversion to WGS84.jpg differ diff --git a/docs/img/VerifyAccuracy/SparkFun Verify RTK - 6 Plate Movements.jpg b/docs/img/VerifyAccuracy/SparkFun Verify RTK - 6 Plate Movements.jpg new file mode 100644 index 000000000..b4802a327 Binary files /dev/null and b/docs/img/VerifyAccuracy/SparkFun Verify RTK - 6 Plate Movements.jpg differ diff --git a/docs/img/VerifyAccuracy/SparkFun Verify RTK - 7 HTDP Conversion Page.jpg b/docs/img/VerifyAccuracy/SparkFun Verify RTK - 7 HTDP Conversion Page.jpg new file mode 100644 index 000000000..10a20c1fd Binary files /dev/null and b/docs/img/VerifyAccuracy/SparkFun Verify RTK - 7 HTDP Conversion Page.jpg differ diff --git a/docs/img/VerifyAccuracy/SparkFun Verify RTK - 8 Facet above Monument.jpg b/docs/img/VerifyAccuracy/SparkFun Verify RTK - 8 Facet above Monument.jpg new file mode 100644 index 000000000..3e240d903 Binary files /dev/null and b/docs/img/VerifyAccuracy/SparkFun Verify RTK - 8 Facet above Monument.jpg differ diff --git a/docs/img/VerifyAccuracy/SparkFun Verify RTK - 9 SW Maps Point.jpg b/docs/img/VerifyAccuracy/SparkFun Verify RTK - 9 SW Maps Point.jpg new file mode 100644 index 000000000..8ceb02cdf Binary files /dev/null and b/docs/img/VerifyAccuracy/SparkFun Verify RTK - 9 SW Maps Point.jpg differ diff --git a/docs/img/Vespucci/SparkFun RTK Vespucci - Advanced Preferences.png b/docs/img/Vespucci/SparkFun RTK Vespucci - Advanced Preferences.png new file mode 100644 index 000000000..0c1952667 Binary files /dev/null and b/docs/img/Vespucci/SparkFun RTK Vespucci - Advanced Preferences.png differ diff --git a/docs/img/Vespucci/SparkFun RTK Vespucci - GPS Source.png b/docs/img/Vespucci/SparkFun RTK Vespucci - GPS Source.png new file mode 100644 index 000000000..35449cb76 Binary files /dev/null and b/docs/img/Vespucci/SparkFun RTK Vespucci - GPS Source.png differ diff --git a/docs/img/Vespucci/SparkFun RTK Vespucci - Location Settings.png b/docs/img/Vespucci/SparkFun RTK Vespucci - Location Settings.png new file mode 100644 index 000000000..e35a69b6a Binary files /dev/null and b/docs/img/Vespucci/SparkFun RTK Vespucci - Location Settings.png differ diff --git a/docs/img/Vespucci/SparkFun RTK Vespucci - Main Gear.png b/docs/img/Vespucci/SparkFun RTK Vespucci - Main Gear.png new file mode 100644 index 000000000..e682b4a0c Binary files /dev/null and b/docs/img/Vespucci/SparkFun RTK Vespucci - Main Gear.png differ diff --git a/docs/img/Vespucci/SparkFun RTK Vespucci - NMEA Network Source.png b/docs/img/Vespucci/SparkFun RTK Vespucci - NMEA Network Source.png new file mode 100644 index 000000000..39b7e116f Binary files /dev/null and b/docs/img/Vespucci/SparkFun RTK Vespucci - NMEA Network Source.png differ diff --git a/docs/img/Vespucci/SparkFun RTK Vespucci - Point on Map.png b/docs/img/Vespucci/SparkFun RTK Vespucci - Point on Map.png new file mode 100644 index 000000000..390d25442 Binary files /dev/null and b/docs/img/Vespucci/SparkFun RTK Vespucci - Point on Map.png differ diff --git a/docs/img/Vespucci/SparkFun RTK Vespucci - Preferences.png b/docs/img/Vespucci/SparkFun RTK Vespucci - Preferences.png new file mode 100644 index 000000000..de3a1805b Binary files /dev/null and b/docs/img/Vespucci/SparkFun RTK Vespucci - Preferences.png differ diff --git a/docs/img/WiFi Config/RTK-Firmware-Update-OTA.gif b/docs/img/WiFi Config/RTK-Firmware-Update-OTA.gif new file mode 100644 index 000000000..fd233a486 Binary files /dev/null and b/docs/img/WiFi Config/RTK-Firmware-Update-OTA.gif differ diff --git a/docs/img/WiFi Config/RTK_Surveyor_-_WiFi_Config_-_Base_Config1.jpg b/docs/img/WiFi Config/RTK_Surveyor_-_WiFi_Config_-_Base_Config1.jpg new file mode 100644 index 000000000..dfb986537 Binary files /dev/null and b/docs/img/WiFi Config/RTK_Surveyor_-_WiFi_Config_-_Base_Config1.jpg differ diff --git a/docs/img/WiFi Config/RTK_Surveyor_-_WiFi_Config_-_Base_Config2.jpg b/docs/img/WiFi Config/RTK_Surveyor_-_WiFi_Config_-_Base_Config2.jpg new file mode 100644 index 000000000..b68e19e0d Binary files /dev/null and b/docs/img/WiFi Config/RTK_Surveyor_-_WiFi_Config_-_Base_Config2.jpg differ diff --git a/docs/img/WiFi Config/RTK_Surveyor_-_WiFi_Config_-_Express_Ports_Config.jpg b/docs/img/WiFi Config/RTK_Surveyor_-_WiFi_Config_-_Express_Ports_Config.jpg new file mode 100644 index 000000000..291a8e38e Binary files /dev/null and b/docs/img/WiFi Config/RTK_Surveyor_-_WiFi_Config_-_Express_Ports_Config.jpg differ diff --git a/docs/img/WiFi Config/RTK_Surveyor_-_WiFi_Config_-_GNSS_Config_Messages.jpg b/docs/img/WiFi Config/RTK_Surveyor_-_WiFi_Config_-_GNSS_Config_Messages.jpg new file mode 100644 index 000000000..db64f81e3 Binary files /dev/null and b/docs/img/WiFi Config/RTK_Surveyor_-_WiFi_Config_-_GNSS_Config_Messages.jpg differ diff --git a/docs/img/WiFi Config/RTK_Surveyor_-_WiFi_Config_-_Networks.jpg b/docs/img/WiFi Config/RTK_Surveyor_-_WiFi_Config_-_Networks.jpg new file mode 100644 index 000000000..29e3954a4 Binary files /dev/null and b/docs/img/WiFi Config/RTK_Surveyor_-_WiFi_Config_-_Networks.jpg differ diff --git a/docs/img/WiFi Config/RTK_Surveyor_-_WiFi_Config_-_System_Save_Exit.jpg b/docs/img/WiFi Config/RTK_Surveyor_-_WiFi_Config_-_System_Save_Exit.jpg new file mode 100644 index 000000000..361d56120 Binary files /dev/null and b/docs/img/WiFi Config/RTK_Surveyor_-_WiFi_Config_-_System_Save_Exit.jpg differ diff --git a/docs/img/WiFi Config/SparkFun RTK - Alternate Coordinate Types for Fixed Base.png b/docs/img/WiFi Config/SparkFun RTK - Alternate Coordinate Types for Fixed Base.png new file mode 100644 index 000000000..e7aa66df6 Binary files /dev/null and b/docs/img/WiFi Config/SparkFun RTK - Alternate Coordinate Types for Fixed Base.png differ diff --git a/docs/img/WiFi Config/SparkFun RTK - Base RTCM Rates Menu.png b/docs/img/WiFi Config/SparkFun RTK - Base RTCM Rates Menu.png new file mode 100644 index 000000000..88fd167ab Binary files /dev/null and b/docs/img/WiFi Config/SparkFun RTK - Base RTCM Rates Menu.png differ diff --git a/docs/img/WiFi Config/SparkFun RTK - GNSS Menu.png b/docs/img/WiFi Config/SparkFun RTK - GNSS Menu.png new file mode 100644 index 000000000..42287dd31 Binary files /dev/null and b/docs/img/WiFi Config/SparkFun RTK - GNSS Menu.png differ diff --git a/docs/img/WiFi Config/SparkFun RTK AP Main Page over Local WiFi.png b/docs/img/WiFi Config/SparkFun RTK AP Main Page over Local WiFi.png new file mode 100644 index 000000000..1fd066c7d Binary files /dev/null and b/docs/img/WiFi Config/SparkFun RTK AP Main Page over Local WiFi.png differ diff --git a/docs/img/WiFi Config/SparkFun RTK AP WiFi Menu.png b/docs/img/WiFi Config/SparkFun RTK AP WiFi Menu.png new file mode 100644 index 000000000..8c533ff76 Binary files /dev/null and b/docs/img/WiFi Config/SparkFun RTK AP WiFi Menu.png differ diff --git a/docs/img/WiFi Config/SparkFun RTK Base Configure - Commonly Used Points Menu.png b/docs/img/WiFi Config/SparkFun RTK Base Configure - Commonly Used Points Menu.png new file mode 100644 index 000000000..ed6d57d9a Binary files /dev/null and b/docs/img/WiFi Config/SparkFun RTK Base Configure - Commonly Used Points Menu.png differ diff --git a/docs/img/WiFi Config/SparkFun RTK Base Survey In.png b/docs/img/WiFi Config/SparkFun RTK Base Survey In.png new file mode 100644 index 000000000..887e4778f Binary files /dev/null and b/docs/img/WiFi Config/SparkFun RTK Base Survey In.png differ diff --git a/docs/img/WiFi Config/SparkFun RTK Change Bluetooth to BLE.png b/docs/img/WiFi Config/SparkFun RTK Change Bluetooth to BLE.png new file mode 100644 index 000000000..eeb8805c8 Binary files /dev/null and b/docs/img/WiFi Config/SparkFun RTK Change Bluetooth to BLE.png differ diff --git a/docs/img/WiFi Config/SparkFun RTK Config - Configure Mode.png b/docs/img/WiFi Config/SparkFun RTK Config - Configure Mode.png new file mode 100644 index 000000000..1b26b8e20 Binary files /dev/null and b/docs/img/WiFi Config/SparkFun RTK Config - Configure Mode.png differ diff --git a/docs/img/WiFi Config/SparkFun RTK Config - TCP Port.png b/docs/img/WiFi Config/SparkFun RTK Config - TCP Port.png new file mode 100644 index 000000000..40c3e6beb Binary files /dev/null and b/docs/img/WiFi Config/SparkFun RTK Config - TCP Port.png differ diff --git a/docs/img/WiFi Config/SparkFun RTK Header Information.png b/docs/img/WiFi Config/SparkFun RTK Header Information.png new file mode 100644 index 000000000..48b5bebc9 Binary files /dev/null and b/docs/img/WiFi Config/SparkFun RTK Header Information.png differ diff --git a/docs/img/WiFi Config/SparkFun RTK OTA Firmware Update.gif b/docs/img/WiFi Config/SparkFun RTK OTA Firmware Update.gif new file mode 100644 index 000000000..c44ceedc0 Binary files /dev/null and b/docs/img/WiFi Config/SparkFun RTK OTA Firmware Update.gif differ diff --git a/docs/img/WiFi Config/SparkFun RTK PointPerfect Config.png b/docs/img/WiFi Config/SparkFun RTK PointPerfect Config.png new file mode 100644 index 000000000..87279f7dc Binary files /dev/null and b/docs/img/WiFi Config/SparkFun RTK PointPerfect Config.png differ diff --git a/docs/img/WiFi Config/SparkFun RTK Ports Menu Mux Config.png b/docs/img/WiFi Config/SparkFun RTK Ports Menu Mux Config.png new file mode 100644 index 000000000..a4cc868f9 Binary files /dev/null and b/docs/img/WiFi Config/SparkFun RTK Ports Menu Mux Config.png differ diff --git a/docs/img/WiFi Config/SparkFun RTK Ports PPS Config.png b/docs/img/WiFi Config/SparkFun RTK Ports PPS Config.png new file mode 100644 index 000000000..1ea27ed18 Binary files /dev/null and b/docs/img/WiFi Config/SparkFun RTK Ports PPS Config.png differ diff --git a/docs/img/WiFi Config/SparkFun RTK Profiles Menu.png b/docs/img/WiFi Config/SparkFun RTK Profiles Menu.png new file mode 100644 index 000000000..35b832b89 Binary files /dev/null and b/docs/img/WiFi Config/SparkFun RTK Profiles Menu.png differ diff --git a/docs/img/WiFi Config/SparkFun RTK Radio Config.png b/docs/img/WiFi Config/SparkFun RTK Radio Config.png new file mode 100644 index 000000000..c4699a628 Binary files /dev/null and b/docs/img/WiFi Config/SparkFun RTK Radio Config.png differ diff --git a/docs/img/WiFi Config/SparkFun RTK Sensor Menu WiFi Config.png b/docs/img/WiFi Config/SparkFun RTK Sensor Menu WiFi Config.png new file mode 100644 index 000000000..c90ae2fe1 Binary files /dev/null and b/docs/img/WiFi Config/SparkFun RTK Sensor Menu WiFi Config.png differ diff --git a/docs/img/WiFi Config/SparkFun RTK System Config AP Menu.png b/docs/img/WiFi Config/SparkFun RTK System Config AP Menu.png new file mode 100644 index 000000000..6f260b90c Binary files /dev/null and b/docs/img/WiFi Config/SparkFun RTK System Config AP Menu.png differ diff --git a/docs/img/WiFi Config/SparkFun RTK System Config Upload BIN.png b/docs/img/WiFi Config/SparkFun RTK System Config Upload BIN.png new file mode 100644 index 000000000..35524984e Binary files /dev/null and b/docs/img/WiFi Config/SparkFun RTK System Config Upload BIN.png differ diff --git a/docs/img/WiFi Config/SparkFun RTK System and Data Logging Configuration.png b/docs/img/WiFi Config/SparkFun RTK System and Data Logging Configuration.png new file mode 100644 index 000000000..b13a07719 Binary files /dev/null and b/docs/img/WiFi Config/SparkFun RTK System and Data Logging Configuration.png differ diff --git a/docs/img/WiFi Config/SparkFun RTK WiFi Config File Manager.png b/docs/img/WiFi Config/SparkFun RTK WiFi Config File Manager.png new file mode 100644 index 000000000..03d3fb502 Binary files /dev/null and b/docs/img/WiFi Config/SparkFun RTK WiFi Config File Manager.png differ diff --git a/docs/img/WiFi Config/SparkFun RTK WiFi Config Screen Version Number.png b/docs/img/WiFi Config/SparkFun RTK WiFi Config Screen Version Number.png new file mode 100644 index 000000000..57f654d2e Binary files /dev/null and b/docs/img/WiFi Config/SparkFun RTK WiFi Config Screen Version Number.png differ diff --git a/docs/img/WiFi Config/SparkFun RTK WiFi Config System.png b/docs/img/WiFi Config/SparkFun RTK WiFi Config System.png new file mode 100644 index 000000000..074f4cc15 Binary files /dev/null and b/docs/img/WiFi Config/SparkFun RTK WiFi Config System.png differ diff --git a/docs/img/WiFi Config/SparkFun RTK WiFi Factory Defaults.png b/docs/img/WiFi Config/SparkFun RTK WiFi Factory Defaults.png new file mode 100644 index 000000000..f620e92ac Binary files /dev/null and b/docs/img/WiFi Config/SparkFun RTK WiFi Factory Defaults.png differ diff --git a/docs/img/WiFi Config/SparkFun RTK WiFi MDNS.png b/docs/img/WiFi Config/SparkFun RTK WiFi MDNS.png new file mode 100644 index 000000000..544655c1d Binary files /dev/null and b/docs/img/WiFi Config/SparkFun RTK WiFi MDNS.png differ diff --git a/docs/img/WiFi Config/SparkFun_RTK_Facet_-_Desktop_vs_Phone_Config.jpg b/docs/img/WiFi Config/SparkFun_RTK_Facet_-_Desktop_vs_Phone_Config.jpg new file mode 100644 index 000000000..aa7e1909c Binary files /dev/null and b/docs/img/WiFi Config/SparkFun_RTK_Facet_-_Desktop_vs_Phone_Config.jpg differ diff --git a/docs/img/WiFi Config/SparkFun_RTK_Facet_-_WiFi_Config_Main_Page.jpg b/docs/img/WiFi Config/SparkFun_RTK_Facet_-_WiFi_Config_Main_Page.jpg new file mode 100644 index 000000000..509fe3fa4 Binary files /dev/null and b/docs/img/WiFi Config/SparkFun_RTK_Facet_-_WiFi_Config_Main_Page.jpg differ diff --git a/docs/img/WiFi Config/SparkFun_RTK_Facet_-_WiFi_Config_Main_Page_-_Firmware.jpg b/docs/img/WiFi Config/SparkFun_RTK_Facet_-_WiFi_Config_Main_Page_-_Firmware.jpg new file mode 100644 index 000000000..229d550ca Binary files /dev/null and b/docs/img/WiFi Config/SparkFun_RTK_Facet_-_WiFi_Config_Main_Page_-_Firmware.jpg differ diff --git a/docs/img/iOS/Screenshot1.PNG b/docs/img/iOS/Screenshot1.PNG new file mode 100644 index 000000000..40a5ab59b Binary files /dev/null and b/docs/img/iOS/Screenshot1.PNG differ diff --git a/docs/img/iOS/Screenshot10.PNG b/docs/img/iOS/Screenshot10.PNG new file mode 100644 index 000000000..4d9b8feaf Binary files /dev/null and b/docs/img/iOS/Screenshot10.PNG differ diff --git a/docs/img/iOS/Screenshot11.PNG b/docs/img/iOS/Screenshot11.PNG new file mode 100644 index 000000000..eaab740e8 Binary files /dev/null and b/docs/img/iOS/Screenshot11.PNG differ diff --git a/docs/img/iOS/Screenshot2.PNG b/docs/img/iOS/Screenshot2.PNG new file mode 100644 index 000000000..a111907b3 Binary files /dev/null and b/docs/img/iOS/Screenshot2.PNG differ diff --git a/docs/img/iOS/Screenshot3.PNG b/docs/img/iOS/Screenshot3.PNG new file mode 100644 index 000000000..0228bc571 Binary files /dev/null and b/docs/img/iOS/Screenshot3.PNG differ diff --git a/docs/img/iOS/Screenshot4.PNG b/docs/img/iOS/Screenshot4.PNG new file mode 100644 index 000000000..7441b9fde Binary files /dev/null and b/docs/img/iOS/Screenshot4.PNG differ diff --git a/docs/img/iOS/Screenshot5.PNG b/docs/img/iOS/Screenshot5.PNG new file mode 100644 index 000000000..abeb03b3a Binary files /dev/null and b/docs/img/iOS/Screenshot5.PNG differ diff --git a/docs/img/iOS/Screenshot6.PNG b/docs/img/iOS/Screenshot6.PNG new file mode 100644 index 000000000..d9e9040cb Binary files /dev/null and b/docs/img/iOS/Screenshot6.PNG differ diff --git a/docs/img/iOS/Screenshot7.PNG b/docs/img/iOS/Screenshot7.PNG new file mode 100644 index 000000000..61156292f Binary files /dev/null and b/docs/img/iOS/Screenshot7.PNG differ diff --git a/docs/img/iOS/Screenshot8.PNG b/docs/img/iOS/Screenshot8.PNG new file mode 100644 index 000000000..98d362a4a Binary files /dev/null and b/docs/img/iOS/Screenshot8.PNG differ diff --git a/docs/img/iOS/Screenshot9.PNG b/docs/img/iOS/Screenshot9.PNG new file mode 100644 index 000000000..03b5afcbe Binary files /dev/null and b/docs/img/iOS/Screenshot9.PNG differ diff --git a/docs/img/iOS/SparkFun RTK iOS - Hotspot Settings.png b/docs/img/iOS/SparkFun RTK iOS - Hotspot Settings.png new file mode 100644 index 000000000..c58d9d6a8 Binary files /dev/null and b/docs/img/iOS/SparkFun RTK iOS - Hotspot Settings.png differ diff --git a/docs/img/iOS/SparkFun RTK iOS Bluetooth Devices.png b/docs/img/iOS/SparkFun RTK iOS Bluetooth Devices.png new file mode 100644 index 000000000..e927f1392 Binary files /dev/null and b/docs/img/iOS/SparkFun RTK iOS Bluetooth Devices.png differ diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 000000000..c82616091 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,53 @@ +# Introduction + +The SparkFun RTK products are exceptional GNSS receivers out-of-box and can be used with little or no configuration. This RTK Product Manual provides detailed descriptions of all the available features of the RTK products. + +The line of RTK products offered by SparkFun all run identical firmware. The [RTK firmware](https://github.com/sparkfun/SparkFun_RTK_Firmware) and this guide cover the following products: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SparkFun RTK Facet L-Band (GPS-20000)SparkFun RTK Facet (GPS-19029)SparkFun RTK Reference Station (GPS-22429)
Hookup GuideHookup GuideHookup Guide
SparkFun RTK Express Plus (GPS-18590)SparkFun RTK Express (GPS-18442)SparkFun RTK Surveyor (GPS-18443)
Hookup GuideHookup GuideHookup Guide
+ +Depending on the hardware platform different features may or may not be supported. We will denote each product in each section so that you know what is supported. + +There are multiple ways to configure an RTK product: + +* [Bluetooth](configure_with_bluetooth.md) - Good for in-field changes +* [WiFi](configure_with_wifi.md) - Good for in-field changes +* [Serial Terminal](configure_with_serial.md) - Requires a computer but allows for all configuration settings +* [Settings File](configure_with_settings_file.md) - Used for configuring multiple RTK devices identically + +The Bluetooth or Serial Terminal methods are recommended for most advanced configurations. Most, but not all settings are also available over WiFi but can be tricky to input via mobile phone. + +If you have an issue, feature request, bug report, or a general question about the RTK firmware specifically we encourage you to post your comments on the [firmware's repository](https://github.com/sparkfun/SparkFun_RTK_Firmware/issues). If you feel like bragging or showing off what you did with your RTK product, we'd be thrilled to hear about it on the issues list as well! + +Things like how to attach an antenna or other hardware-specific topics are best read on the Hookup Guides for the individual products. diff --git a/docs/intro.md b/docs/intro.md new file mode 100644 index 000000000..a5929395f --- /dev/null +++ b/docs/intro.md @@ -0,0 +1,254 @@ +# Quick Start + +Surveyor: ![Feature Supported](img/Icons/GreenDot.png) / Express: ![Feature Supported](img/Icons/GreenDot.png) / Express Plus: ![Feature Supported](img/Icons/GreenDot.png) / Facet: ![Feature Supported](img/Icons/GreenDot.png) / Facet L-Band: ![Feature Supported](img/Icons/GreenDot.png) / Reference Station: ![Feature Supported](img/Icons/GreenDot.png) + +This quick start guide will get you started in 10 minutes or less. For the full product manual, please proceed to the [**Introduction**](index.md). + +Are you using [Android](https://docs.sparkfun.com/SparkFun_RTK_Firmware/intro/#android) or [iOS](https://docs.sparkfun.com/SparkFun_RTK_Firmware/intro/#ios)? + +## Android + +1. Download [SW Maps](https://play.google.com/store/apps/details?id=np.com.softwel.swmaps). This may not be the GIS software you intend to do your data collection, but SW Maps is free and makes sure everything is working correctly out of the box. + + ![Download SW Maps]() + + *Download SW Maps for Android* + +2. Mount the hardware: + + * For RTK Surveyor/Express/Express Plus: Attach the included [antenna](https://www.sparkfun.com/products/17751) to a [monopole](https://www.amazon.com/AmazonBasics-WT1003-67-Inch-Monopod/dp/B00FAYL1YU) using the included [thread adapter](https://www.sparkfun.com/products/17546). [Clamp](https://www.amazon.com/gp/product/B072DSRF3J) the RTK device to the monopole. Use the included [cable](https://www.sparkfun.com/products/21739) to connect the antenna to the RTK Surveyor/Express/Express Plus (Figure 1). + * For RTK Facet/Facet L-Band: Attach the Facet to a [monopole](https://www.amazon.com/AmazonBasics-WT1003-67-Inch-Monopod/dp/B00FAYL1YU) using the included [thread adapter](https://www.sparkfun.com/products/17546) (Figure 1). + + ![RTK devices attached to a monopole]() + + *Figure 1* + +3. Turn on your RTK device by pressing the POWER button until the display shows ‘SparkFun RTK’ then you can release it (Figure 2). + + ![RTK Boot Display]() + + *Figure 2* + + +4. Please note the four-digit code in the top left corner of the display (**B022** in the picture below). This is the MAC address you will want to pair with (Figure 3). + + ![Display showing MAC address]() + + *Figure 3* + +5. From your cell phone, open Bluetooth settings and pair it with a new device. You will see a list of available Bluetooth devices. Select the ‘Facet Rover-3AF1’ where 'Facet' is the type of device you have (Surveyor, Express, Facet, etc) and 3AF1 is the MAC address you see on the device’s display (Figure 4). + + ![List of Bluetooth devices on Android]() + + *Figure 4* + +6. Once paired, open SW Maps. Select ‘New Project’ and give your project a name like ‘RTK Project’. + +7. Press the SW Maps icon in the top left corner of the home screen and select Bluetooth GNSS. You should see the ‘Facet Rover-3AF1’ in the list. Select it then press the ‘Connect’ button in the bottom left corner (Figure 5). SW Maps will show a warning that the instrument height is 0m. That’s ok. + + ![SW Map list of Bluetooth devices]() + + *Figure 5* + +8. Once connected, have a look at the display on the RTK device. You should see the MAC address disappear and be replaced by the Bluetooth icon (Figure 6). You’re connected! + + ![Display showing Bluetooth connection]() + + *Figure 6* + +9. Now put the device outside with a clear view of the sky. GNSS doesn’t work indoors or near windows. Within about 30 seconds you should see 10 or more satellites in view (SIV) (Figure 7). More SIV is better. We regularly see 30 or more SIV. The horizontal positional accuracy (HPA) will start at around 10 meters and begin to decrease. The lower the HPA the more accurate your position. If you wait a few moments, this will drop dramatically to around 0.3 meters (300mm = 1ft) or better. + + ![RTK Display Explanation](img/Displays/SparkFun_RTK_Facet_-_Main_Display_Icons.jpg) + + *Figure 7* + +You can now use your RTK device to measure points with very good (sub-meter) accuracy. If you need extreme accuracy (down to 10mm) continue reading the [RTK Crash Course](https://docs.sparkfun.com/SparkFun_RTK_Firmware/intro/#rtk-crash-course). + +## iOS + +The software options for Apple iOS are much more limited because Apple products do not support Bluetooth SPP. That's ok! The SparkFun RTK products support Bluetooth Low Energy (BLE) which *does* work with iOS. + +1. Download [SW Maps for iOS](https://apps.apple.com/us/app/sw-maps/id6444248083). This may not be the GIS software you intend to do your data collection, but SW Maps is free and makes sure everything is working correctly out of the box. + + ![SW Maps for Apple]() + + *Download SW Maps for iOS* + +2. Mount the hardware: + + * For RTK Surveyor/Express/Express Plus: Attach the included [antenna](https://www.sparkfun.com/products/17751) to a [monopole](https://www.amazon.com/AmazonBasics-WT1003-67-Inch-Monopod/dp/B00FAYL1YU) using the included [thread adapter](https://www.sparkfun.com/products/17546). [Clamp](https://www.amazon.com/gp/product/B072DSRF3J) the RTK device to the monopole. Use the included [cable](https://www.sparkfun.com/products/21739) to connect the antenna to the RTK Surveyor/Express/Express Plus (Figure 1). + * For RTK Facet/Facet L-Band: Attach the Facet to a [monopole](https://www.amazon.com/AmazonBasics-WT1003-67-Inch-Monopod/dp/B00FAYL1YU) using the included [thread adapter](https://www.sparkfun.com/products/17546) (Figure 1). + + ![RTK devices attached to a monopole]() + + *Figure 1* + +3. Turn on your RTK device by pressing the POWER button until the display shows ‘SparkFun RTK' then you can release it (Figure 2). + + ![RTK Facet Boot Display]() + + *Figure 2* + +4. Put the RTK device into configuration mode by tapping the POWER or SETUP button multiple times until the Config menu is highlighted (Figure 3). + + ![Config menu highlighted on the display]() + + *Figure 3* + +5. From your phone, connect to the WiFi network *RTK Config*. + +6. Open a browser (Chrome is preferred) and type **rtk.local** into the address bar. Note: Devices with older firmware may still need to enter **192.168.4.1**. + +7. Under the *System Configuration* menu, change the **Bluetooth Protocol** to **BLE** (Figure 4). Then click **Save Configuration** and then **Exit and Reset**. The unit will now reboot. + + ![Configure Bluetooth Protocol in WiFi Config]() + + *Figure 4* + +8. You should now be disconnected from the *RTK Config* WiFi network. Make sure Bluetooth is enabled on your iOS device Settings (Figure 5). The RTK device will not appear in the OTHER DEVICES list. That is OK. + + ![iOS Bluetooth settings]() + + *Figure 5* + +9. Open SW Maps. Select ‘New Project’ and give your project a name like ‘RTK Project’. + +10. Press the SW Maps icon in the top left corner of the home screen and select Bluetooth GNSS. You will need to agree to allow a Bluetooth connection. Set the *Instrument Model* to **Generic NMEA (Bluetooth LE)**. Press 'Scan' and your RTK device should appear. Select it then press the ‘Connect’ button in the bottom left corner. + +11. Once connected, have a look at the display on the RTK device. You should see the MAC address disappear and be replaced by the Bluetooth icon (Figure 6). You’re connected! + + ![Display showing Bluetooth connection]() + + *Figure 6* + +12. Now put the device outside with a clear view of the sky. GNSS doesn’t work indoors or near windows. Within about 30 seconds you should see 10 or more satellites in view (SIV) (Figure 7). More SIV is better. We regularly see 30 or more SIV. The horizontal positional accuracy (HPA) will start at around 10 meters and begin to decrease. The lower the HPA the more accurate your position. If you wait a few moments, this will drop dramatically to around 0.3 meters (300mm = 1ft) or better. + + ![RTK Display Explanation](img/Displays/SparkFun_RTK_Facet_-_Main_Display_Icons.jpg) + + *Figure 7* + +You can now use your RTK device to measure points with very good (sub-meter) accuracy. If you need extreme accuracy (down to 10mm) continue reading the [RTK Crash Course](https://docs.sparkfun.com/SparkFun_RTK_Firmware/intro/#rtk-crash-course). + +## RTK Crash Course + +To get millimeter accuracy we need to provide the RTK unit with correction values. Corrections, often called RTCM, help the RTK unit refine its position calculations. RTCM (Radio Technical Commission for Maritime Services) can be obtained from a variety of sources but they fall into three buckets: Commercial, Public, and Civilian Reference Stations. + +**Commercial Reference Networks** + +These companies set up a large number of reference stations that cover entire regions and countries, but charge a monthly fee. They are often easy to use but can be expensive. + +* [PointOneNav](https://app.pointonenav.com/trial?src=sparkfun) ($50/month) - US, EU, Australia, South Korea +* [Skylark](https://www.swiftnav.com/skylark) ($29 to $69/month) - US, EU, Japan, Australia +* [SensorCloud RTK](https://rtk.sensorcloud.com/pricing/) ($100/month) partial US, EU +* [Premium Positioning](https://www.premium-positioning.com) (~$315/month) partial EU +* [KeyNetGPS](https://www.keypre.com/KeynetGPS) ($375/month) North Eastern US +* [Hexagon/Leica](https://hxgnsmartnet.com/en-US) ($500/month) - partial US, EU + +**Public Reference Stations** + +![Wisconsin network of CORS]() + +*State Wide Network of Continuously Operating Reference Stations (CORS)* + +Be sure to check if your state or country provides corrections for free. Many do! Currently, there are 21 states in the USA that provide this for free as a department of transportation service. Search ‘Wisconsin CORS’ as an example. Similarly, in France, check out [CentipedeRTK](https://docs.centipede.fr/). There are several public networks across the globe, be sure to google around! + +**Civilian Reference Stations** + +![SparkFun Base Station Enclosure](img/Corrections/Roof_Enclosure.jpg) + +*The base station at SparkFun* + +You can set up your own correction source. This is done with a 2nd GNSS receiver that is stationary, often called a Base Station. There is just the one-time upfront cost of the Base Station hardware. See the [Creating a Permanent Base](https://docs.sparkfun.com/SparkFun_RTK_Firmware/permanent_base/) document for more information. + +## NTRIP Example + +Once you have decided on a correction source we need to feed that data into your SparkFun RTK device. In this example, we will use PointOneNav and SW Maps. + +1. Create an account on [PointOneNav](https://app.pointonenav.com/trial?src=sparkfun). **Note:** This service costs $50 per month at the time of writing. + +2. Open SW Maps and connect to the RTK device over Bluetooth. + +3. Once connected, open the SW Maps menu again (top left corner) and you will see a new option; click on ‘NTRIP Client'. + +4. Enter the credentials provided by PointOneNav and click Connect (Figure 1). Verify that *Send NMEA GGA* is checked. + + ![NTRIP credentials in SW Maps]() + + *Figure 1* + +5. Corrections will be downloaded every second from PointOneNav using your phone’s cellular connection and then sent down to the RTK device over Bluetooth. You don't need a very fast internet connection or a lot of data; it's only about 530 bytes per second. + +Assuming you are outside, as soon as corrections are sent to the device, the Crosshair icon will become double and begin flashing. Once RTK Fix is achieved (usually under 30 seconds) the double crosshairs will become solid and the HPA will be below 20mm (Figure 2). You can now take positional readings with millimeter accuracy! + +![Double crosshair indicating RTK Fix]() + +*Figure 2* + +In SW Maps, the position bubble will turn from Blue (regular GNSS fix), then to Orange (RTK Float), then to Green (RTK Fix) (Figure 3). + +![Green bubble indicating RTK Fix]() + +*Figure 3* + +RTK Fix will be maintained as long as there is a clear view of the sky and corrections are delivered to the device every few seconds. + +## Common Gotchas + +* High-precision GNSS only works with dual frequency L1/L2 antennas. This means that GPS antenna you got in the early 2000s with your TomTom is not going to work. Please use the L1/L2 antennas provided by SparkFun. + +* High-precision GNSS works best with a clear view of the sky; it does not work indoors or near a window. GNSS performance is generally *not* affected by clouds or storms. Trees and buildings *can* degrade performance but usually only in very thick canopies or very near tall building walls. GNSS reception is very possible in dense urban centers with skyscrapers but high-precision RTK may be impossible. + +* The location reported by the RTK device is the location of the antenna element; it's *not* the location of the pointy end of the stick. Lat and Long are fairly easy to obtain but if you're capturing altitude be sure to do additional reading on ARPs (antenna reference points) and how to account for the antenna height in your data collection software. + +* An internet connection is required for most types of RTK. RTCM corrections can be transmitted over other types of connections (such as serial telemetry radios). See [Correction Transport](correction_transport.md) for more details. + +## RTK Facet L-Band Keys + +The RTK Facet L-Band is unique in that it must obtain keys to decrypt the signal from a geosynchronous satellite. Here are the steps to do so: + +1. Turn on your RTK Facet L-Band by pressing the POWER button until the display shows ‘SparkFun RTK' then you can release it (Figure 1). + + ![RTK Boot Display]() + + *Figure 1* + +2. Put the RTK device into configuration mode by tapping the POWER button multiple times until the Config menu is highlighted (Figure 2). + + ![Config menu highlighted on the display]() + + *Figure 2* + +3. From your phone or laptop, connect to the WiFi network *RTK Config*. + +4. Open a browser (Chrome is preferred) and type **rtk.local** into the address bar. Note: Devices with older firmware may still need to enter **192.168.4.1**. + +5. Under the *WiFi Configuration* menu, enter the SSID and password for your local WiFi network (Figure 3). You can enter up to four. This can be a home, office, cellular hotspot, or any other WiFi network. The unit will attempt to connect to the internet periodically to obtain new keys, including this first day. Then click **Save Configuration** and then **Exit and Reset**. The unit will now reboot. + + ![WiFi settings]() + + *Figure 3* + +6. After reboot, the device will connect to WiFi and obtain keys. You should see a series of displays indicating the automatic process (Figure 4). + + ![Days until L-Band keys expire]() + + *Figure 4* + + Keys are valid for a minimum of 29 days and a maximum of 60. The device will automatically attempt to connect to WiFi to obtain new keys. If WiFi is not available during that period the keys will expire. The device will continue to operate with expired keys, with ~0.3m accuracy but not be able to obtain RTK Fix mode. + +7. Now put the device outside with a clear view of the sky. GNSS doesn’t work indoors or near windows. Within about 30 seconds you should see 10 or more satellites in view (SIV). More SIV is better. We regularly see 30 or more SIV. The horizontal positional accuracy (HPA) will start at around 10 meters and begin to decrease. The lower the HPA the more accurate your position. + + ![Days until L-Band keys expire]() + + *Figure 5* + + Upon successful reception and decryption of L-Band corrections, the satellite dish icon will increase to a three-pronged icon (Figure 5). As the unit's accuracy increases a normal cross-hair will turn to a double blinking cross-hair indicating a floating RTK solution, and a solid double cross-hair will indicate a fixed RTK solution. The HPA will be below 0.030 (30mm) or better once RTK Fix is achieved. + +You can now use your RTK device to measure points with millimeter accuracy. Please see [Android](https://docs.sparkfun.com/SparkFun_RTK_Firmware/intro/#android) or [iOS](https://docs.sparkfun.com/SparkFun_RTK_Firmware/intro/#ios) for guidance on getting the RTK device connected to GIS software over Bluetooth. + +## RTK Reference Station + +![The SparkFun RTK Reference Station](img/SparkFun_RTK_Reference_Station.jpg) + +While most of this Quick Start guide can be used with the [RTK Reference Station](https://www.sparkfun.com/products/22429), the [Reference Station Hookup Guide](https://learn.sparkfun.com/tutorials/sparkfun-rtk-reference-station-hookup-guide) is the best place to get started. + diff --git a/docs/menu_base.md b/docs/menu_base.md new file mode 100644 index 000000000..ae7b6590c --- /dev/null +++ b/docs/menu_base.md @@ -0,0 +1,151 @@ +# Base Menu + +Surveyor: ![Feature Supported](img/Icons/GreenDot.png) / Express: ![Feature Supported](img/Icons/GreenDot.png) / Express Plus: ![Feature Not Supported](img/Icons/RedDot.png) / Facet: ![Feature Supported](img/Icons/GreenDot.png) / Facet L-Band: ![Feature Supported](img/Icons/GreenDot.png) / Reference Station: ![Feature Supported](img/Icons/GreenDot.png) + +In addition to providing accurate local location fixes, the SparkFun RTK devices can also serve as a correction source, also called a *Base*. The Base doesn't move and 'knows' where it is so it can calculate the discrepancies between the signals it is receiving and what it should be receiving. Said differently, the 'Base' is told where it is, and that it's not moving. If the GPS signals say otherwise, the Base knows there was a disturbance in the ~~Force~~ ionosphere. These differences are the correction values passed to the Rover so that the Rover can have millimeter-level accuracy. + +There are two types of bases: *Surveyed* and *Fixed*. A surveyed base is often a temporary base set up in the field. Called a 'Survey-In', this is less accurate but requires only 60 seconds to complete. The 'Fixed' base is much more accurate but the precise location at which the antenna is located must be known. A fixed base is often a structure with an antenna bolted to the side. Raw satellite signals are gathered for a few hours and then processed using Precision Point Position. We have a variety of tutorials that go into depth on these subjects but all you need to know is that the RTK Facet supports both Survey-In and Fixed Base techniques. + +**Note:** The RTK Express Plus does not support Base mode. The Express Press contains an internal IMU and additional algorithms to support high-precision location fixes using dead reckoning. + +**Note:** The RTK Facet L-Band is designed to use corrections provided via u-blox's PointPerfect system therefore, a Base/Rover setup is not needed. However, if the service is not available the RTK Facet L-Band can still be used in a traditional Base/Rover setup. Here we’ll describe how to assemble a Rover and Base. + +Please see the following tutorials for more information: + + + + + + + + + + + + + + +
What is GPS RTK?Getting Started with u-center for u-bloxSetting up a Rover Base RTK SystemHow to build a DIY GNSS reference station
+ + +The Base Menu allows the user to select between Survey-In or Fixed Base setups. + +![Base type and location configuration](img/WiFi Config/SparkFun%20RTK%20Base%20Survey%20In.png) + +*Controlling the type of Base from WiFi AP Config* + +![CMD window showing Base menu options](img/Terminal/SparkFun_RTK_Express_-_Base_Menu.jpg) + +*Base Menu Options* + +## Mode + +In **Survey-In** mode, the minimum observation time and Mean 3D Standard Deviation can be set. The defaults are 60 seconds and 5 meters as directed by u-blox. The device will wait for the position accuracy to be better than 1 meter before a Survey-In is started. Don't be fooled; setting the observation time to 4 hours or an initial positional accuracy of 0.3m is not going to significantly improve the accuracy of the survey - use [PPP](https://learn.sparkfun.com/tutorials/how-to-build-a-diy-gnss-reference-station#gather-raw-gnss-data) instead. + +![Fixed Base Coordinate input](img/WiFi Config/SparkFun%20RTK%20Base%20Configure%20-%20Commonly%20Used%20Points%20Menu.png) + +*Fixed base coordinate input* + +In **Fixed** mode, the coordinates of the antenna need to be set. These can be entered in ECEF or Geographic coordinates. Whenever a user enters Base mode by pressing the SETUP button the GNSS receiver will immediately go into Base mode with these coordinates and immediately begin outputting RTCM correction data. + +**Note:** The 'Paste Current XYZ' button will copy the current base coordinates and paste them into the X/Y/Z boxes. This shortcut allows the user to skip writing down coordinates just to re-enter them. However, taking a snap-shot of the unit's position in time is a very inaccurate way to assign the unit's base position. + +![RTK Facet in Survey-In Mode](img/Displays/SparkFun_RTK_Express_-_Display_-_Survey-In.jpg) + +*RTK Facet in Survey-In Mode* + +Once the device has been configured, pressing the Setup button will change the device to Base mode. If the device is configured for *Survey-In* base mode, a flag icon will be shown and the survey will begin. The mean standard deviation will be shown as well as the time elapsed. For most Survey-In setups, the survey will complete when both 60 seconds have elapsed *and* a mean of 5m or less is obtained. + +![RTK Facet in Fixed Transmit Mode](img/Displays/SparkFun_RTK_Express_-_Display_-_FixedBase-Xmitting.jpg) + +*RTK Facet in Fixed Transmit Mode* + +Once the *survey-in* is complete the device enters RTCM Transmit mode. The number of RTCM transmissions is displayed. By default, this is one per second. During this phase, the ZED-F9P is outputting the RTCM corrections out of the **RADIO** port. Attaching an external serial radio to this port will allow the Base to send corrections to any Rover. + +The *Fixed Base* mode is similar but uses a structure icon (shown above) to indicate a fixed base. + +## NTRIP Server + +**NTRIP** is where the real fun begins. The Base needs a method for getting the correction data to the Rover. This can be done using radios but that's limited to a few kilometers at best. If you've got WiFi reception, use the internet! + +Enabling NTRIP will present a handful of new options seen below: + +![NTRIP Server Settings](img/WiFi Config/RTK_Surveyor_-_WiFi_Config_-_Base_Config2.jpg) + +*Configuring NTRIP Server settings via WiFi Config AP* + +![SparkFun RTK Facet NTRIP Settings](img/Terminal/SparkFun_RTK_Express_-_Base_Menu_-_Fixed_NTRIP.jpg) + +*Settings for the NTRIP Server* + +This is a powerful feature of the RTK line of products. The RTK device can be configured to transmit its RTCM directly over WiFi to the user's mount point. This eliminates the need for a radio link. + +Once the NTRIP server is enabled you will need a handful of credentials: + +* Local WiFi SSID and password +* A casting service such as [RTK2Go](http://www.rtk2go.com) or [Emlid](http://caster.emlid.com) (the port is almost always 2101) +* A mount point and password + +![RTK Facet in Transmit Mode with NTRIP](img/Displays/SparkFun_RTK_Express_-_Display_-_FixedBase-Casting.jpg) + +*RTK Facet in Transmit Mode with NTRIP Enabled* + +![NTRIP Server Connected](img/Terminal/RTK_Surveyor_-_Device_Configuration_-_NTRIP_Server_Broadcasting_v11.jpg) + +*NTRIP Server Connected!* + +If the NTRIP server is enabled the device will first attempt to connect over WiFi. The WiFi icon will blink until a WiFi connection is obtained. If the WiFi icon continually blinks be sure to check your SSID and PW for the local WiFi. + +Once WiFi connects the device will attempt to connect to the NTRIP mount point. Once successful the display will show 'Casting' along with a solid WiFi icon. The number of successful RTCM transmissions will increase every second. + +![Transmitting to mount point](img/Terminal/RTK_Surveyor_-_Device_Configuration_-_NTRIP_Server_Broadcasting_Bytes_v11.jpg) + +Every second a few hundred bytes, up to ~2k, will be transmitted to your mount point. + +Note: During NTRIP transmission WiFi is turned on and Bluetooth is turned off. You should not need to know the location information of the base so Bluetooth should not be needed. If necessary, USB can be connected to view detailed location information using the [System Report](configure_with_serial.md#system-report) command. + +## Commonly Use Coordinates + +![List of common coordinates](img/WiFi Config/SparkFun%20RTK%20Base%20Configure%20-%20Commonly%20Used%20Points%20Menu.png) + +*A list of common coordinates* + +For users who return to the same base position or monument, the coordinates can be saved to a 'Commonly Used Coordinates' list. A nickname and the X/Y/Z positions are saved to the list. Any record on the list can be loaded from the list into the X/Y/Z fields allowing quick switching without the need to hand record or re-enter coordinates from day-to-day repositioning of the base. + +## RTCM Message Rates + +![The RTCM Menu under Base](img/WiFi Config/SparkFun%20RTK%20Base%20Survey%20In.png) + +When the device is in Base mode, the fix rate is set to 1Hz. This will override any Rover setting. + +![The list of supported RTCM messages](img/WiFi Config/SparkFun%20RTK%20-%20Base%20RTCM%20Rates%20Menu.png) + +Additionally, RTCM messages are generated at a rate of 1Hz. If lower RTCM rates are desired the RTCM Rates menu can be used to modify the rates of any supported RTCM message. This can be helpful when using longer-range radios that have lower bandwidth. + +## Supported Lat/Long Coordinate Formats + +![Entering coordinates in alternate formats](img/WiFi Config/SparkFun%20RTK%20-%20Alternate%20Coordinate%20Types%20for%20Fixed%20Base.png) + +When entering coordinates for a fixed Base in Geodetic format, the following formats are supported: + +* DD.ddddddddd (ie -105.184774720, 40.090335429) +* DDMM.mmmmmmm (ie -10511.0864832) +* DD MM.mmmmmmm (ie 40 05.42013) +* DD-MM.mmmmmmm (40-05.42013) +* DDMMSS.ssssss (-1051105.188992) +* DD MM SS.ssssss (-105 11 05.188992) +* DD-MM-SS.ssssss (40-05-25.2075) + +![Coordinate formats in the Base serial menu](img/Terminal/SparkFun%20RTK%20-%20Alternate%20Coordinate%20Types%20for%20Fixed%20Base%20Serial.png) + +These coordinate formats are automatically detected and converted as needed. The coordinates are displayed in the format they are entered. If a different format is desired, the coordinate display format can be changed via the serial Base menu. + +For more information about coordinate formats, check out this [online converter](https://www.earthpoint.us/convert.aspx). + +## Assisted Base + +An Assisted Base is where a temporary base is set up to Survey-In its location but is simultaneously provided RTCM corrections so that its Survey-In is done with very precise readings. An assisted base running a Survey-In removes much of the relative inaccuracies from a Rover-Base system. We've found an Assisted Base varies as little as 50mm RMS between intra-day tests, with accuracy within 65mm of a PPP of the same location, same day. + +To set up an assisted base the RTK device should be located in a good reception area and provided with RTCM corrections. Let it obtain RTK Fix from a fixed position (on a tripod, for example) in *Rover* mode. Once an RTK fix is achieved, change the device to temporary *Base* mode (also called Survey-In). The device will take 60 seconds of positional readings, at which point the fixed position of the base will be set using RTK augmented coordinates. At this point, corrections provided to the base can be discontinued. The Base will begin outputting very accurate RTCM corrections that can be relayed to a rover that is in a less optimal reception setting. + +Similarly, the RTK Facet L-Band can be set up as a relay: the L-Band device can be located in a good reception area, and then transmit very accurate corrections to a rover via Radio or internet link. Because the RTK Facet L-Band can generate its own corrections, you do not need to provide them during Survey-In. To set up an assisted base, set up an RTK Facet L-Band unit with a clear view of the sky, and let it obtain RTK Fix from a fixed position in *Rover* mode. Once an RTK fix is achieved, change the device to temporary *Base* mode. The device will take 60 seconds of positional readings, at which point the fixed position will be set using RTK fixed coordinates. The RTK Facet L-Band will then output very accurate RTCM corrections that can be relayed to a rover that is in a less optimal reception setting. diff --git a/docs/menu_data_logging.md b/docs/menu_data_logging.md new file mode 100644 index 000000000..540f1d128 --- /dev/null +++ b/docs/menu_data_logging.md @@ -0,0 +1,28 @@ +# Data Logging Menu + +Surveyor: ![Feature Supported](img/Icons/GreenDot.png) / Express: ![Feature Supported](img/Icons/GreenDot.png) / Express Plus: ![Feature Supported](img/Icons/GreenDot.png) / Facet: ![Feature Supported](img/Icons/GreenDot.png) / Facet L-Band: ![Feature Supported](img/Icons/GreenDot.png) / Reference Station: ![Feature Supported](img/Icons/GreenDot.png) + +## WiFi Interface + +Please see the System Menu [WiFi Interface](menu_system.md#wifi-interface). + +## Serial Interface + +![RTK Data Logging Configuration Menu](img/Terminal/SparkFun%20RTK%20Logging%20Menu.png) + +*RTK Data Logging Configuration Menu* + +From the Main Menu, pressing 5 will enter the Logging Menu. This menu will report the status of the microSD card. While you can enable logging, you cannot begin logging until a microSD card is inserted. Any FAT16 or FAT32 formatted microSD card up to 128GB will work. We regularly use the [SparkX brand 1GB cards](https://www.sparkfun.com/products/15107) but note that these log files can get very large (>500MB) so plan accordingly. + +* Option 1 will enable/disable logging. If logging is enabled, all messages from the ZED-F9P will be recorded to microSD. A log file is created at power on with the format *SFE_[DeviceName]_YYMMDD_HHMMSS.txt* based on current GPS data/time. The `[DeviceName]` is Surveyor, Express, etc. +* Option 2 allows a user to set the max logging time. This is convenient to determine the location of a fixed antenna or a receiver on a repeatable landmark. Set the RTK Facet to log RAWX data for 10 hours, convert to RINEX, run through an observation processing station and you’ll get the corrected position with <10mm accuracy. Please see the [How to Build a DIY GNSS Reference Station](https://learn.sparkfun.com/tutorials/how-to-build-a-diy-gnss-reference-station) tutorial for more information. +* Option 3 allows a user to set the max logging length in minutes. Every 'max long length' amount of time the current log will be closed and a new log will be started. This is known as cyclic logging and is convenient on *very* long surveys (ie, months or years) to prevent logs from getting too unwieldy and helps limit the risk of log corruption. This will continue until the unit is powered down or the *max logging time* is reached. +* Option 4 will close the current log and start a new log. + +* Option 5 will record the coordinates of the base antenna to a custom NMEA message within the log if the RTCM1005 or RTCM1006 message is received. This can be helpful when doing field work and the location of the base is needed; the log on the roving device will contain the location of the base preventing the user from needing to record the base location separately. The ARP is logged in a custom GNTXT,01,01,10 message as ECEF-X, ECEF-Y, ECEF-Z, Antenna Height. The Antenna Height will be zero if the data was extracted from RTCM1005. + +* Option 7 will enable/disable creating a comma separated file (Marks_date.csv) that is written each time the mark state is selected with the setup button on the RTK Surveyor, RTK Express or RTK Express Plus, or the power button on the RTK Facet. + +* Option 8 will enable/disable the resetting of the system if an SD card is detected but fails to initialize. This can be helpful to harden a system that may be deployed for long periods of time. Without intervention, if an SD card is detected but fails to respond, the system will reset in an attempt to re-mount the faulty SD card interface. + +**Note:** If you are wanting to log RAWX sentences to create RINEX files useful for post-processing the position of the receiver please see the GNSS Configuration Menu. For more information on how to use a RAWX GNSS log to get a higher accuracy base location please see the [How to Build a DIY GNSS Reference Station](https://learn.sparkfun.com/tutorials/how-to-build-a-diy-gnss-reference-station#gather-raw-gnss-data) tutorial. diff --git a/docs/menu_debug.md b/docs/menu_debug.md new file mode 100644 index 000000000..38fb7094b --- /dev/null +++ b/docs/menu_debug.md @@ -0,0 +1,30 @@ +# Debug Menu + +Surveyor: ![Feature Supported](img/Icons/GreenDot.png) / Express: ![Feature Supported](img/Icons/GreenDot.png) / Express Plus: ![Feature Supported](img/Icons/GreenDot.png) / Facet: ![Feature Supported](img/Icons/GreenDot.png) / Facet L-Band: ![Feature Supported](img/Icons/GreenDot.png) / Reference Station: ![Feature Supported](img/Icons/GreenDot.png) + + +![System Debug Menu](img/Terminal/SparkFun%20RTK%20Debug%20Menu.png) + +*Showing the debug menu* + +The Debug menu enables the user to enable and disable various debug features. None of these options are needed for normal users or daily use. These are provided for faster software development and troubleshooting. + +1. **I2C Debugging Output** - Enable additional ZED-F9P interface debug messages +2. **Heap Reporting** - Display currently available bytes, lowest value and the largest block +3. **Task Highwater Reporting** - Shows stack usage of select tasks +4. **Set the SPI / microSD card frequency** - SD card interface speed. Default is 16MHz. +5. **Set SPP RX buffer size** - Default 128 bytes +6. **Set SPP TX buffer size** - Controls how large the buffer used to communicate over Bluetooth +7. **Throttle Bluetooth transmissions during SPP congestion** - Reduce bytes transmitted if Bluetooth link becomes busy +8. **Display reset counter** - Enable to display a small number indicating non-power on reset count +9. **Set GNSS serial timeout in seconds** - Sets the number of milliseconds before reporting serial available +10. Periodically display WiFi IP Address +11. Periodically display system states +12. Periodically display WiFi states +13. Periodically display NTRIP Client states +14. Periodically display NTRIP Server states + +* **t** - Display the test screen +* **e** - Erase LittleFS: Clear settings and profiles saved internally (not on microSD card) +* **r** - Reset the system +* **x** - Exit the debug menu diff --git a/docs/menu_ethernet.md b/docs/menu_ethernet.md new file mode 100644 index 000000000..08cd23389 --- /dev/null +++ b/docs/menu_ethernet.md @@ -0,0 +1,30 @@ +# Ethernet Menu + +Surveyor: ![Feature Not Supported](img/Icons/RedDot.png) / Express: ![Feature Not Supported](img/Icons/RedDot.png) / Express Plus: ![Feature Not Supported](img/Icons/RedDot.png) / Facet: ![Feature Not Supported](img/Icons/RedDot.png) / Facet L-Band: ![Feature Not Supported](img/Icons/RedDot.png) / Reference Station: ![Feature Supported](img/Icons/GreenDot.png) + +The Reference Station sends and receives NTRIP correction data via Ethernet. It can also send NMEA and RTCM navigation messages to an external TCP Server via Ethernet. +It also has a dedicated Configure-Via-Ethernet (*Cfg Eth*) mode which is accessed via the MODE button and OLED display. + +By default, the Reference Station will use DHCP to request an IP Address from the network Gateway. But you can optionally configure it with a fixed IP Address. + +![Reference Station in DHCP mode](img/Terminal/Ethernet_DHCP.png) + +*The Reference Station Ethernet menu - with DHCP selected* + +![Reference Station in fixed IP address mode](img/Terminal/Ethernet_Fixed_IP.png) + +*The Reference Station Ethernet menu - with a fixed IP address selected* + +### Ethernet TCP Client + +The Reference Station can act as an Ethernet TCP Client, sending NMEA and / or UBX data to a remote TCP Server. + +This is similar to the WiFi TCP Client mode on our other RTK products, but the data can be sent to any server based on its IP Address or URL. + +E.g. to connect to a local machine via its IP Address, select option "c" and then enter the IP Address using option "h" + +![Ethernet TCP Client configuration](img/Terminal/Ethernet_TCP_Client_1.png) + +![Ethernet TCP Client connection](img/Terminal/TCP_Client.gif) + +The above animation was generated using [TCP_Server.py](https://github.com/sparkfun/SparkFun_RTK_Firmware/blob/main/Firmware/Tools/TCP_Server.py). \ No newline at end of file diff --git a/docs/menu_gnss.md b/docs/menu_gnss.md new file mode 100644 index 000000000..78ff9e2b1 --- /dev/null +++ b/docs/menu_gnss.md @@ -0,0 +1,73 @@ +# GNSS Menu + +Surveyor: ![Feature Supported](img/Icons/GreenDot.png) / Express: ![Feature Supported](img/Icons/GreenDot.png) / Express Plus: ![Feature Supported](img/Icons/GreenDot.png) / Facet: ![Feature Supported](img/Icons/GreenDot.png) / Facet L-Band: ![Feature Supported](img/Icons/GreenDot.png) / Reference Station: ![Feature Supported](img/Icons/GreenDot.png) + +The ZED-F9P is immensely configurable. The RTK device will, by default, put the ZED-F9P into the most common configuration for rover/base RTK for use with *SW Maps* and other GIS applications. + +The GNSS Configuration menu allows a user to change the report rate, dynamic model, and select which constellations should be used for fix calculations. + +![GNSS Configuration menu](img/WiFi Config/SparkFun%20RTK%20-%20GNSS%20Menu.png) + +*The most common settings on the RTK Device WiFi AP Config* + +From the main menu, pressing 1 will bring up the GNSS configuration menu. + +![GNSS menu showing measurement rates and dynamic model](img/Terminal/SparkFun_RTK_ExpressPlus_ReceiverNTRIP.jpg) + +*GNSS menu showing measurement rates and dynamic model* + +## Measurement Frequency + +Measurement Frequency can be set by either Hz or by seconds between measurements. Some users need many measurements per second; RTK devices support up to 20Hz with RTK enabled. Some users are doing very long static surveys that require many seconds between measurements; the ZED-F9P supports up to 8255 seconds (137 minutes) between readings. + +![Table showing fix rates](img/SparkFun ZED-F9P Navigation Rates.png) + +Note: When in **Base** mode, the measurement frequency is set to 1Hz. This is because RTK transmission does not benefit from faster updates, nor does logging of RAWX for PPP. + +## Dynamic Model + +The Dynamic Model can be changed but it is recommended to leave it as *Portable*. For more information, please refer to the [ZED-F9P Integration Manual](https://cdn.sparkfun.com/assets/learn_tutorials/1/8/5/7/ZED-F9P_IntegrationManual__UBX-18010802_.pdf). + +## Min SV Elevation and C/N0 + +![Elevation and C/N0](img/WiFi Config/SparkFun%20RTK%20-%20GNSS%20Menu.png) + +*GNSS menu showing Minimum SV Elevation and C/N0* + +A minimum elevation is set in degrees. If a satellite is detected that is below this elevation, it will be *excluded* from any GNSS position calculation. + +A minimum C/N0 is set in dB. If a satellite is detected that is below this signal strength, it will be *excluded* from any GNSS position calculation. + +## Constellations Menu + +![Enable or disable the constellations used for fixes](img/Terminal/SparkFun_RTK_ExpressPlus_Receiver_Constellations.jpg) + +*Enable or disable the constellations used for fixes* + +The ZED-F9P is capable of tracking 184 channels across four constellations and two bands (L1/L2) including GPS (USA), Galileo (EU), BeiDou (China), and GLONASS (Russia). SBAS (satellite-based augmentation system) is also supported. By default, all constellations are used. Some users may want to study, log, or monitor a subset. Disabling a constellation will cause the ZED to ignore those signals when calculating a location fix. + +## NTRIP Client + +![NTRIP Client enabled showing settings](img/Terminal/SparkFun_RTK_ExpressPlus_ReceiverNTRIP.jpg) + +*NTRIP Client enabled showing settings* + +The SparkFun RTK devices can obtain their correction data over a few different methods. For detailed information see [Correction Sources](correction_sources.md). + +* Bluetooth - This is the most common. An app running on a tablet or phone has an NTRIP client built into it. Once the phone is connected over Bluetooth SPP, the RTCM is sent from the phone to the RTK device. +* WiFi - The rover uses WiFi to be an NTRIP Client and connect to an NTRIP Caster. WiFi and Bluetooth can run simultaneously. This is helpful in situations where a GIS software does not have an NTRIP Client; a cellular hotspot can be used to provide WiFi to the RTK device setup to use NTRIP Client an obtain RTK Fix, while Bluetooth is used to connect to the GIS software for data mapping and collection. +* Radio - A base RTK unit and a rover have serial radios plugged into the **RADIO** port. RTCM data generated by the base station is set over the radio to the rover. + +Once the NTRIP Client is enabled you will need a handful of credentials: + +* Local WiFi SSID and password (WPA2) +* A casting service and port such as [RTK2Go](http://rtk2go.com/) or [Emlid](https://emlid.com/ntrip-caster/) (the port is almost always 2101) +* A mount point and password + +With these credentials set, the RTK device will attempt to connect to WiFi, then connect to your caster of choice, and then begin downloading the RTCM data over WiFi. We tried to make it as easy as possible. Every second a few hundred bytes, up to ~2k, will be downloaded from the mount point you've entered. Remember, the rover must be in WiFi range to connect in this mode. + +![Rover with Active NTRIP Client Connection](img/Displays/SparkFun_RTK_Rover_NTRIP_Client_Connection.png) + +*Rover with Active NTRIP Client Connection* + +Once the device connects to WiFi, it will attempt to connect to the user's chosen NTRIP Caster. If WiFi or the NTRIP connection fails, the rover will return to normal operation. diff --git a/docs/menu_messages.md b/docs/menu_messages.md new file mode 100644 index 000000000..eeda4107f --- /dev/null +++ b/docs/menu_messages.md @@ -0,0 +1,268 @@ +# Messages Menu + +Surveyor: ![Feature Supported](img/Icons/GreenDot.png) / Express: ![Feature Supported](img/Icons/GreenDot.png) / Express Plus: ![Feature Supported](img/Icons/GreenDot.png) / Facet: ![Feature Supported](img/Icons/GreenDot.png) / Facet L-Band: ![Feature Supported](img/Icons/GreenDot.png) / Reference Station: ![Feature Supported](img/Icons/GreenDot.png) + +![Message rate configuration boxes](img/WiFi Config/RTK_Surveyor_-_WiFi_Config_-_GNSS_Config_Messages.jpg) + +*Message rate configuration from WiFi AP Config* + +![The Messages configuration menu](img/Terminal/SparkFun_RTK_Express_-_Messages_Menu.jpg) + +*The messages configuration menu* + +From this menu, a user can control the output of various NMEA, RTCM, RXM, and other messages. Any enabled message will be broadcast over Bluetooth *and* recorded to SD (if available). + +Because of the large number of configurations possible, we provide a few common settings: + +* Reset to Surveying Defaults (NMEAx5) +* Reset to PPP Logging Defaults (NMEAx5 + RXMx2) +* Turn off all messages (serial command only) +* Turn on all messages (serial command only) + +## Reset to Surveying Defaults (NMEAx5) + +This will turn off all messages and enable the following messages: + +* NMEA-GGA, NMEA-GSA, NMEA-GST, NMEA-GSV, NMEA-RMC + +These five NMEA sentences are commonly used with SW Maps for general surveying. + +## Reset to PPP Logging Defaults (NMEAx5 + RXMx2) + +This will turn off all messages and enable the following messages: + +* NMEA-GGA, NMEA-GSA, NMEA-GST, NMEA-GSV, NMEA-RMC, RXM-RAWX, RXM-SFRBX + +These seven sentences are commonly used when logging and doing Precise Point Positioning (PPP) or Post Processed Kinematics (PPK). You can read more about PPP [here](https://learn.sparkfun.com/tutorials/how-to-build-a-diy-gnss-reference-station). + +## Individual Messages + +![Configuring the NMEA messages](img/Terminal/SparkFun_RTK_Express_-_Messages_Menu_-_NMEA.jpg) + +*Configuring the NMEA messages* + +There are a large number of messages supported (listed below). Each message sub-menu will present the user with the ability to set the message report rate. + +Each message rate input controls which messages are disabled (0) and how often the message is reported (1 = one message reported per 1 fix, 5 = one report every 5 fixes). The message rate range is 0 to 20. + +**Note:** The message report rate is the *number of fixes* between message reports. In the image above, with GSV set to 4, the NMEA GSV message will be produced once every 4 fixes. Because the device defaults to a 4Hz fix rate, the GSV message will appear once per second. + +The following 120 messages are supported for Bluetooth output and logging: + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
• NMEA-DTM• NMEA-GBS• NMEA-GGA
• NMEA-GLL• NMEA-GNS• NMEA-GRS
• NMEA-GSA• NMEA-GST• NMEA-GSV
• NMEA-RLM• NMEA-RMC• NMEA-THS
• NMEA-VLW• NMEA-VTG• NMEA-ZDA
• NMEA-NAV2-GGA• NMEA-NAV2-GLL• NMEA-NAV2-GNS
• NMEA-NAV2-GSA• NMEA-NAV2-RMC• NMEA-NAV2-VTG
• NMEA-NAV2-ZDA• PUBX-POLYP• PUBX-POLYS
• PUBX-POLYT• RTCM3x-1005• RTCM3x-1074
• RTCM3x-1077• RTCM3x-1084• RTCM3x-1087
• RTCM3x-1094• RTCM3x-1097• RTCM3x-1124
• RTCM3x-1127• RTCM3x-1230• RTCM3x-4072-0
• RTCM3x-4072-1• ESF-ALG• ESF-INS
• ESF-MEAS• ESF-RAW• ESF-STATUS
• MON-COMMS• MON-HW2• MON-HW3
• MON-HW• MON-IO• MON-MSGPP
• MON-RF• MON-RXBUF• MON-RXR
• MON-SPAN• MON-SYS• MON-TXBUF
• NAV2-CLOCK• NAV2-COV• NAV2-DOP
• NAV2-EELL• NAV2-EOE• NAV2-POSECEF
• NAV2-POSLLH• NAV2-PVAT• NAV2-PVT
• NAV2-SAT• NAV2-SBAS• NAV2-SIG
• NAV2-STATUS• NAV2-TIMEBDS• NAV2-TIMEGAL
• NAV2-TIMEGLO• NAV2-TIMEGPS• NAV2-TIMELS
• NAV2-TIMEQZSS• NAV2-TIMEUTC• NAV2-VELECEF
• NAV2-VELNED• NAV-ATT• NAV-CLOCK
• NAV-COV• NAV-DOP• NAV-EELL
• NAV-EOE• NAV-GEOFENCE• NAV-HPPOSECEF
• NAV-HPPOSLLH• NAV-ODO• NAV-ORB
• NAV-PL• NAV-POSECEF• NAV-POSLLH
• NAV-PVAT• NAV-PVT• NAV-RELPOSNED
• NAV-SAT• NAV-SBAS• NAV-SIG
• NAV-SLAS• NAV-STATUS• NAV-SVIN
• NAV-TIMEBDS• NAV-TIMEGAL• NAV-TIMEGLO
• NAV-TIMEGPS• NAV-TIMELS• NAV-TIMEQZSS
• NAV-TIMEUTC• NAV-VELECEF• NAV-VELNED
• RXM-COR• RXM-MEASX• RXM-RAWX
• RXM-RLM• RXM-RTCM• RXM-SFRBX
• RXM-SPARTN• TIM-TM2• TIM-TP
• TIM-VRFY
+ +## Turn off all messages + +This will turn off all messages. This is handy for advanced users who need to start from a blank slate. This setting is only available over serial configuration. + +## Turn on all messages + +This will turn on all messages. This is a setting used for firmware testing and should not be needed in normal use. This setting is only available over serial configuration. + +## ESF Messages + +The ZED-F9R module, found only on the Express Plus, supports additional External Sensor Fusion messages. These messages show the raw accelerometer and gyroscope values of the internal IMU. These messages can consume up to 34,000 bytes of bandwidth. Please see [here](https://github.com/sparkfun/SparkFun_RTK_Firmware/issues/81#issuecomment-1059338377) for more information. \ No newline at end of file diff --git a/docs/menu_ntp.md b/docs/menu_ntp.md new file mode 100644 index 000000000..99c898696 --- /dev/null +++ b/docs/menu_ntp.md @@ -0,0 +1,106 @@ +# Network Time Protocol Menu + +Surveyor: ![Feature Not Supported](img/Icons/RedDot.png) / Express: ![Feature Not Supported](img/Icons/RedDot.png) / Express Plus: ![Feature Not Supported](img/Icons/RedDot.png) / Facet: ![Feature Not Supported](img/Icons/RedDot.png) / Facet L-Band: ![Feature Not Supported](img/Icons/RedDot.png) / Reference Station: ![Feature Supported](img/Icons/GreenDot.png) + +The Reference Station can act as an Ethernet Network Time Protocol (NTP) server. + +Network Time Protocol has been around since 1985. It is a simple way for computers to synchronize their clocks with each other, allowing the network latency (delay) to be subtracted: + +* A client sends a NTP request (packet) to the chosen or designated server + * The request contains the client's current clock time - for identification + +* The server logs the time the client's request arrived and then sends a reply containing: + * The client's clock time - for identification + * The server's clock time - when the request arrived at the server + * The server's clock time - when the reply is sent + * The time the server's clock was last synchronized - providing the age of the synchronization + +* The client logs the time the reply is received - using its own clock + +When the client receives the reply, it can deduce the total round-trip delay which is the sum of: + +* How long the request took to reach the server + +* How long the server took to construct the reply + +* How long the reply took to reach the client + +This exchange is repeated typically five times, before the client synchronizes its clock to the server's clock, subtracting the latency (delay) introduced by the network. + +Having your own NTP server on your network allows tighter clock synchronization as the network latency is minimized. + +The Reference Station can be placed into its dedicated NTP mode, by pressing the **MODE** button until NTP is highlighted in the display and pausing there. + +![Animation of selecting NTP mode](img/Displays/SparkFun RTK - NTP Select.gif) + +*Selecting NTP mode* + +The Reference Station will first synchronize its Real Time Clock (RTC) using the very accurate time provided by the u-blox GNSS module. The module's Time Pulse (Pulse-Per-Second) signal is connected to the ESP32 as an interrupt. The ESP32's RTC is synchronized to Universal Time Coordinate (UTC) on the rising edge of the TP signal using the time contained in the UBX-TIM-TP message. + +The WIZnet W5500 interrupt signal is also connected to the ESP32, allowing the ESP32 to accurately log when each NTP request arrives. + +The Reference Station will respond to each NTP request within a few 10s of milliseconds. + +If desired, you can log all NTP requests to a file on the microSD card, and/or print them as diagnostic messages. The log and messages contain the NTP timing information and the IP Address and port of the Client. + +[![The system debug menu showing how to enable the NTP diagnostics](img/NTP/NTP_Diagnostics.png)](img/NTP/NTP_Diagnostics.png) + +*System Debug Menu - NTP Diagnostics (Click for a closer look)* + +[![The logging menu showing how to log the NTP requests](img/NTP/NTP_Logging.png)](img/NTP/NTP_Logging.png) + +*Logging Menu - Log NTP Requests* + +### Logged NTP Requests + +![NTP requests log](img/NTP/NTP_Log.png) + +NTP uses its own epoch - midnight January 1st, 1900. This is different than the standard Unix epoch - midnight January 1st, 1970 - and the GPS epoch - midnight January 6th, 1980. The times shown in the log and diagnostic messages use the NTP epoch. You can use online calculators to convert between the different epochs: + +* [https://weirdo.cloud/](https://weirdo.cloud/) + +* [https://www.unixtimestamp.com/](https://www.unixtimestamp.com/) + +* [https://www.labsat.co.uk/index.php/en/gps-time-calculator](https://www.labsat.co.uk/index.php/en/gps-time-calculator) + +### NTP on Windows + +If you want to synchronize your Windows PC to a Reference Station NTP Server, here's how to do it: + +* Install [Meinberg NTP](https://www.meinbergglobal.com/english/sw/ntp.htm) - this replaces the Windows built-in Time Service + +![Meinberg NTP initial configuration](img/NTP/NTP_Install_1.png) + +* During the installation, select "Create an initial configuration file" and select the NTP Pool server for your location +* Select "Use fast initial sync mode" for faster first synchronization + +![Meinberg NTP service settings](img/NTP/NTP_Install_2.png) + +* The next step is to edit the NTP Configuration File + * Editing the file requires Administrator privileges + * Open the *Start* menu, navigate to *Meinberg*, right-click on *Edit NTP Configuration* and select *Run as administrator* + +[![Meinberg NTP configuration](img/NTP/NTP_Config_1_small.png)](img/NTP/NTP_Config_1.png) + +* Comment the lines in *ntp.conf* which name the pool.ntp servers +* Add an extra *server* line and include the IP Address for your Reference Station. It helps to give your Reference Station a fixed IP Address first - see [Menu Ethernet](menu_ethernet.md) +* Save the file + +[![Meinberg NTP configuration](img/NTP/NTP_Config_2_small.png)](img/NTP/NTP_Config_2.png) + +* Finally, restart the NTP Service + * Again this needs to be performed with Administrator privileges + * Open the *Start* menu, navigate to *Meinberg*, right-click on *Restart NTP Service* and select *Open file loctaion* + +[![Meinberg NTP configuration](img/NTP/NTP_Config_3_small.png)](img/NTP/NTP_Config_3.png) + +* Right-click on the *Restart NTP Service* and select *Run as administrator* + +![Meinberg NTP configuration](img/NTP/NTP_Config_4.png) + +* You can check if your PC clock synchronized successfully by opening a *Command Prompt (cmd)* and running *ntpq -pd* + +![Meinberg NTP configuration](img/NTP/NTP_Config_5.png) + +If enabled, your Windows PC NTP requests will be printed and logged by the reference station. See [above](#logged-ntp-requests). + diff --git a/docs/menu_pointperfect.md b/docs/menu_pointperfect.md new file mode 100644 index 000000000..b890e5ec9 --- /dev/null +++ b/docs/menu_pointperfect.md @@ -0,0 +1,56 @@ +# PointPerfect Menu + +Surveyor: ![Feature Not Supported](img/Icons/RedDot.png) / Express: ![Feature Not Supported](img/Icons/RedDot.png) / Express Plus: ![Feature Not Supported](img/Icons/RedDot.png) / Facet: ![Feature Not Supported](img/Icons/RedDot.png) / Facet L-Band: ![Feature Supported](img/Icons/GreenDot.png) / Reference Station: ![Feature Not Supported](img/Icons/RedDot.png) + +**Note:** This section only applies to RTK Facet *L-Band* products. Regular RTK Facet, Surveyor, Express, and Express Plus products do not have L-Band antennas or receivers built-in. + +![PointPerfect Menu](img/WiFi Config/SparkFun%20RTK%20PointPerfect%20Config.png) + +*Configuring PointPerfect settings over WiFi* + +![PointPerfect Menu](img/Terminal/SparkFun%20RTK%20PointPerfect%20Menu.png) + +*Configuring PointPerfect settings over serial* + +*RTK Facet L-Band* products are equipped with a special antenna and extra receiver to decrypt the L-Band corrections using the PointPerfect service from u-blox. The PointPerfect sub-menu allows a user to enter their 'Home' WiFi settings. This WiFi SSID and password are used to regularly obtain the needed decryption keys from u-blox. This is normally your home WiFi or other accessible WiFi. + +PointPerfect L-Band decryption keys are valid for a maximum of 56 days. During that time, the RTK Facet L-Band can operate normally without the need for WiFi access. However, when the keys are set to expire in 28 days or less, the RTK Facet L-Band will attempt to log in to WiFi at each power on. If WiFi is not available, it will continue normal operation. If the keys fully expire, the device will continue to receive the L-Band signal but will be unable to decrypt the signal, disabling high-precision GNSS. The RTK Facet L-Band will continue to have extraordinary accuracy (we've seen better than 0.15m HPA) but not the centimeter-level accuracy that comes with RTK. + +**Note:** The RTK Facet L-Band is capable of receiving RTCM corrections over traditional means including NTRIP data over Bluetooth or a serial radio. But the real point of L-Band and PointPerfect is that you can be *anywhere*, without cellular or radio cover, and still enjoy millimeter accuracy. + +![Display showing 14 days until Keys Expire](img/Displays/SparkFun_RTK_LBand_DayToExpire.jpg) + +*Display showing 14 days until keys expire* + +The unit will display various messages to aid the user in obtaining keys as needed. + +![Three-pronged satellite dish indicating L-Band reception](img/Displays/SparkFun_RTK_LBand_Indicator.jpg) + +*Three pronged satellite dish indicating L-Band reception* + +Upon successful reception and decryption of PointPerfect corrections, the satellite dish icon will increase to a three-pronged icon. As the unit's fix increases the cross-hair will indicate a basic 3D solution, a double blinking cross-hair will indicate a floating RTK solution, and a solid double cross-hair will indicate a fixed RTK solution. + +![PointPerfect Menu](img/Terminal/SparkFun%20RTK%20PointPerfect%20Menu.png) + +*PointPerfect Menu* + +The *Days until keys expire* inform the user how many days the unit has until it needs to connect to WiFi to obtain new keys. + +* Option '1' disables the use of PointPerfect corrections. + +* Option '2' disables the automatic attempts at WiFi connections when key expiry is less than 28 days. + +* Option '3' will trigger an immediate attempt to connect over WiFi and update the keys. + +* Option '4' will display the Device ID. This is needed when a SparkFun RTK Facet L-Band product needs to be added to the PointPerfect system. This is normally taken care of when you purchase the L-Band unit with PointPerfect service added, but for customers who wish to extend their subscription beyond the initial year, or did not purchase the service and want to add it at a later date, this Device ID is what customer service needs. + +* Option 'k' will bring up the Manual Key Entry menu. + +![Manual Key Entry menu](img/Terminal/SparkFun_RTK_LBand_ManualKeysA.jpg) + +*Manual Key Entry Menu* + +Because of the length and complexity of the keys, we do not recommend you manually enter them. This menu is most helpful for displaying the current keys. + +Option '1' will allow a user to enter their Device Profile Token. This is the token that is used to provision a device on a PointPerfect account. By default, users may use the SparkFun token but must pay SparkFun for the annual service fee. If an organization would like to administer its own devices, the token can be changed here. + diff --git a/docs/menu_ports.md b/docs/menu_ports.md new file mode 100644 index 000000000..0464c9695 --- /dev/null +++ b/docs/menu_ports.md @@ -0,0 +1,124 @@ +# Ports Menu + +Surveyor: ![Feature Partially Supported](img/Icons/YellowDot.png) / Express: ![Feature Supported](img/Icons/GreenDot.png) / Express Plus: ![Feature Supported](img/Icons/GreenDot.png) / Facet: ![Feature Supported](img/Icons/GreenDot.png) / Facet L-Band: ![Feature Supported](img/Icons/GreenDot.png) / Reference Station: ![Feature Partially Supported](img/Icons/YellowDot.png) + +![Setting the baud rate of the ports](img/WiFi Config/RTK_Surveyor_-_WiFi_Config_-_Express_Ports_Config.jpg) + +*Setting the baud rates of the two available external ports* + +![Baud rate configuration of Radio and Data ports](img/Terminal/SparkFun_RTK_Express_-_Ports_Menu.jpg) + +*Baud rate configuration of Radio and Data ports* + +## Radio Port + +By default, the **Radio** port is set to 57600bps to match the [Serial Telemetry Radios](https://www.sparkfun.com/products/19032) that are recommended to be used with the RTK Facet (it is a plug-and-play solution). This can be set from 4800bps to 921600bps. + +The radio port is connected to the F9P's UART2. + +## Mux Channel + +The **Data** port on the RTK Facet, Express, and Express Plus is very flexible. Internally the **Data** connector is connected to a digital mux allowing one of four software-selectable setups. By default, the Data port will be connected to the UART1 of the ZED-F9P and output any messages via serial. + +* **NMEA** - The TX pin outputs any enabled messages (NMEA, UBX, and RTCM) at a default of 460,800bps (configurable 9600 to 921600bps). The RX pin can receive RTCM for RTK and can also receive UBX configuration commands if desired. +* **PPS/Trigger** - The TX pin outputs the pulse-per-second signal that is accurate to 30ns RMS. This pin can be configured as an extremely accurate time base. The pulse length and time between pulses are configurable down to 1us. The RX pin is connected to the EXTINT pin on the ZED-F9P allowing for events to be measured with incredibly accurate nano-second resolution. Useful for things like audio triangulation. See the [External Event Logging](#surveyor-data-port) section below and the Timemark section of the [ZED-F9P Integration Manual](https://cdn.sparkfun.com/assets/learn_tutorials/1/8/5/7/ZED-F9P_IntegrationManual__UBX-18010802_.pdf) for more information. +* **I2C** - (On Express, Facet, and Facet L-Band) The TX pin operates as SCL, RX pin as SDA on the I2C bus. This allows additional sensors to be connected to the I2C bus. +* **Wheel/Dir Encoder** - (On Express Plus) Connect the DATA port to the wheel tick inputs on the ZED-F9R. This aids the Sensor Fusion engine for IMU based location fixes when installed in an automobile. Signals must be limited to 3.3V. +* **GPIO** - The TX pin operates as a DAC-capable GPIO on the ESP32. The RX pin operates as an ADC-capable input on the ESP32. This is useful for custom applications. + +## Data Port + +By default, the **Data** port is set to 460800bps and can be configured from 4800bps to 921600bps. The 460800bps baud rate was chosen to support applications where a large number of messages are enabled and a large amount of data is sent. If you need to decrease the baud rate to 115200bps or other, be sure to monitor the MON-COMM message within u-center for buffer overruns. A baud rate of 115200bps and the NMEA+RXM default configuration at 4Hz *will* cause buffer overruns. + +![Monitoring the COM ports on the ZED-F9P](img/SparkFun_RTK_Express_-_Ports_Menu_MON-COMM_Overrun.jpg) + +*Monitoring the COM ports on the ZED-F9P* + +If you must run the data port at lower than 460800bps, and you need to enable a large number of messages and/or increase the fix frequency beyond 4Hz, be sure to verify that UART1 usage stays below 99%. The image above shows the UART1 becoming overwhelmed because the ZED cannot transmit at 115200bps fast enough. + +Most applications do not need to plug anything into the **Data** port. Most users will get their NMEA position data over Bluetooth. However, this port can be useful for sending position data to an embedded microcontroller or single-board computer. The pinout is 3.3V / TX / RX / GND. **3.3V** is provided by this connector to power a remote device if needed. While the port is capable of sourcing up to 600mA, we do not recommend more than 300mA. This port should not be connected to a power source. + +### Wheel Ticks + +![Wheel/Direction Encoder drop down](img/WiFi Config/SparkFun%20RTK%20Ports%20Menu%20Mux%20Config.png) + +*On the RTK Express Plus only.* This dropdown is made available if users wish to connect wheel ticks and a direction encoder as inputs to the ZED-F9R. This aids the Sensor Fusion engine for IMU based location fixes when installed in an automobile. Signals must be limited to 3.3V. + +### Pulse Per Second + +![Configuring the External Pulse and External Events](img/WiFi Config/SparkFun%20RTK%20Ports%20PPS%20Config.png) + +*Configuring the External Pulse and External Events over WiFi* + +![RTK Mux Menu](img/Terminal/SparkFun_RTK_Express_-_Ports_Menu_Mux.jpg) + +*Port menu showing mux data port connections* + +When PPS/Event Trigger is selected, the Pulse-Per-Second output from the ZED-F9x is sent out of the TX pin of the DATA port. Once the RTK device has GNSS reception, this can be used as a *very* accurate time base. + +The time between pulses can be configured down to 100ns (10MHz) with an accuracy of 30ns RMS and 60ns 99%. The pulse width and polarity are also configurable. + +![Wires connected to a SparkFun USB C to Serial adapter](img/SparkFun_RTK_Facet_-_Data_Port_to_USB.jpg) + +For PPS, only the Black and Green wires are needed. If you need to provide 3.3V to your system, the red wire can supply up to 600mA but we do not recommend sourcing more than 300mA. + +* **Red** - 3.3V +* **Green** - TX (output from the RTK device) +* **Orange** - RX (input into the RTK device) +* **Black** - GND + +Similarly, the RX pin of the DATA port can be used for event logging. See [External Event Logging](menu_ports.md#external-event-logging) for more information. + +### External Event Logging + +![Three RTK Express with External Triggers](img/RTK%20Express%20with%20External%20Microphones.png) + +*Three RTK Expresses wired with external microphones* + +The external triggering system is a powerful feature enabling a variety of scientific applications. Above is three RTK Expresses wired with external microphones used in a 'popped balloon' audio triangulation experiment. + +The ZED-F9P has the ability to mark a detected event with +/-30 *nanosecond accuracy*. When enabled, an event on the RX pin (a low-to-high or high-to-low transition) on the DATA port, will trigger a message in the log with a very accurate timestamp. + +* **Red** - 3.3V +* **Green** - TX (output from the RTK device) +* **Orange** - RX (input into the RTK device) +* **Black** - GND + +![Wires connected to a SparkFun USB C to Serial adapter](img/SparkFun_RTK_Facet_-_Data_Port_to_USB.jpg) + +For event logging, only the Black and Orange wires are needed. If you need to provide 3.3V to your system, the red wire can supply up to 600mA but we do not recommend sourcing more than 300mA. + +![Configuring the External Pulse and External Events](img/WiFi Config/SparkFun%20RTK%20Ports%20PPS%20Config.png) + +*Configuring the External Pulse and External Events over WiFi* + +Be sure to select 'Enable External Event Logging' through the ports menu. + +Events within the log have the following format: + +> $GNTXT,numberOfSentences,sentenceNumber,CUSTOM_NMEA_TYPE_EVENT,triggerCount,towMsR,towSubMsR,accEst*CRC + +For example: + + $GNTXT,01,01,02,5,494326906,136292,31*74 + +Where + +* $GNTXT: Custom NMEA text message +* 01: numberOfSentences in this report +* 01: sentenceNumber +* 02: sentenceType - Externally triggered events are type 0x02 +* 5: triggerCount +* 494326906: towMsR - Time Of Week of rising edge (ms) +* 136292: towSubMsR - Millisecond fraction of Time Of Week of rising edge (ns) +* 31: accEst - Accuracy estimate (ns) +* 74: NMEA CRC + +The event timestamps can be analyzed to precisely coordinate or triangulate a past event. In the case of the three RTK Expresses with microphones, the three units' locations were known with RTK 14mm accuracy. The air temperature was taken to obtain the speed of sound. From these data points, we can solve for the location of a sound such as a popped balloon. + +## Surveyor Data Port + +By default, the Data port is set to 460800bps and can be configured from 4800bps to 921600bps. + +Note: The Data port does not output NMEA by default. The unit must be opened and the *Serial NMEA Connection* switch must be moved to 'Ext Connector'. See [Hardware Overview - Advanced Features](https://learn.sparkfun.com/tutorials/sparkfun-rtk-surveyor-hookup-guide/all#hardware-overview---advanced-features) for the location of the switch. + diff --git a/docs/menu_profiles.md b/docs/menu_profiles.md new file mode 100644 index 000000000..e8e0cbce2 --- /dev/null +++ b/docs/menu_profiles.md @@ -0,0 +1,23 @@ +# Profiles Menu + +Surveyor: ![Feature Not Supported](img/Icons/RedDot.png) / Express: ![Feature Supported](img/Icons/GreenDot.png) / Express Plus: ![Feature Supported](img/Icons/GreenDot.png) / Facet: ![Feature Supported](img/Icons/GreenDot.png) / Facet L-Band: ![Feature Supported](img/Icons/GreenDot.png) / Reference Station: ![Feature Supported](img/Icons/GreenDot.png) + +![List of system profiles](img/WiFi Config/SparkFun%20RTK%20Profiles%20Menu.png) + +*Profiles Menu on the WiFi config page* + +![Profiles Menu](img/Terminal/SparkFun_RTK_ExpressPlus_Profiles.jpg) + +*User Profiles Menu* + +Profiles are a very powerful feature. A profile is a complete copy of all the settings on the RTK product. Switching profiles changes all the settings in one step. This is handy for creating a complex setup for surveying, and a different setup for an NTRIP-enabled base station. Rather than changing the variety of parameters, a user can simply switch profiles. + +![Multiple Profiles on Menu](img/SparkFun_RTK_Facet_Profile.jpg) + +*Multiple Profiles on Menu* + +If more than one profile is defined, the profiles will be displayed and selectable by using the **Power/Setup** on the Facet or **Setup** on the Express and Express Plus. Profiles can be named up to 50 characters; only the first 7 characters will be shown on the menu. + +## Surveyor Profiles + +Because the Surveyor does not have a display, profiles are not available. The Base/Rover switch allows simple pre-set configurations to be toggled. \ No newline at end of file diff --git a/docs/menu_radios.md b/docs/menu_radios.md new file mode 100644 index 000000000..abf19be0b --- /dev/null +++ b/docs/menu_radios.md @@ -0,0 +1,59 @@ +# Radios Menu + +## ESP-Now + +Surveyor: ![Feature Supported](img/Icons/GreenDot.png) / Express: ![Feature Supported](img/Icons/GreenDot.png) / Express Plus: ![Feature Supported](img/Icons/GreenDot.png) / Facet: ![Feature Supported](img/Icons/GreenDot.png) / Facet L-Band: ![Feature Supported](img/Icons/GreenDot.png) / Reference Station: ![Feature Partially Supported](img/Icons/YellowDot.png) + +![Radio menu during AP-Config](img/WiFi Config/SparkFun%20RTK%20Radio%20Config.png) + +*Radio configuration through WiFi* + +![Radio menu showing ESP-Now](img/Terminal/SparkFun%20RTK%20Radio%20Menu.png) + +*Radio menu showing ESP-Now* + +Pressing 'r' from the main menu will open the Configure Radios menu. This allows a user to enable or disable the use of the internal ESP-Now radio. + +ESP-Now is a 2.4GHz protocol that is built into the internal ESP32 microcontroller; the same microcontroller that provides Bluetooth and WiFi. ESP-Now does not require WiFi or an Access Point. This is most useful for connecting a Base to Rover without the need for an external radio. Simply turn two SparkFun RTK products on, enable their radios, pair them, and data will be passed between units. + +Additionally, ESP-Now supports point-to-multipoint transmissions. This means a Base can transmit to multiple Rovers simultaneously. + +The ESP-Now radio feature was added in firmware release v2.4. If the **Configure Radio** menu is not visible, consider upgrading your firmware. + +ESP-Now is a free radio included in every RTK product, and works well, but it has a few limitations: + +1. Limited use with Bluetooth SPP. The ESP32 is capable of [simultaneously transmitting](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/coexist.html) ESP-Now and Bluetooth LE, but not classic Bluetooth SPP. Unfortunately SPP (Serial Port Profile) is the most common method for moving data between a GNSS receiver and the GIS software. Because of this, using ESP-Now while connecting to the RTK product using Bluetooth SPP is not stable. SparkFun RTK products support Bluetooth LE and ESP-Now works flawlessly with Bluetooth LE. There are a few GIS applications that support Bluetooth LE including SW Maps. + + ![Max transmission range of about 250m](img/Radios/SparkFun%20RTK%20ESP-Now%20Distance%20Testing.png) + +2. Limited range. You can expect two RTK devices to be able to communicate approximately 250m (845 ft) line of sight but any trees, buildings, or objects between the Base and Rover will degrade reception. This range is useful for many applications but may not be acceptable for some applications. We recommend using ESP-Now as a quick, free, and easy way to get started with Base/Rover setups. If your application needs longer RF distances consider cellular NTRIP, WiFi NTRIP, or an external serial telemetry radio plugged into the **RADIO** port. + +3. Bug limited to Point to Point. There is a known bug in the ESP32 Arduino core that prevents a base from [transmitting to multiple rovers](https://github.com/espressif/esp-idf/issues/8992). This will be fixed in future releases of the RTK firmware once the ESP32 core is updated. For now, a base can be paired successfully with a single rover. + +## Pairing + +![Pairing Menu](img/Displays/SparkFun%20RTK%20Radio%20E-Pair.png) + +Pressing the Setup button (Express or Express Plus) or the Power/Setup button (Facet or Facet L-Band) will display the various submenus. Pausing on E-Pair will put the unit into ESP-Now pairing mode. If another RTK device is detected nearby in pairing mode, they will exchange MAC addresses and pair with each other. Multiple Rover units can be paired to a Base in the same fashion. + +![Radio menu during AP-Config](img/WiFi Config/SparkFun%20RTK%20Radio%20Config.png) + +*Radio configuration through WiFi* + +The radio system can be configured over WiFi. The radios subsystem is disabled by default. Enabling the radio to ESP-Now will expose the above options. The unit's radio MAC can be seen as well as a button to forget all paired radios. This button is disabled until the 'Enable Forget All Radios' checkbox is checked. The 'Broadcast Override' function changes all data transmitted by this radio to be sent to all radios in the vicinity, instead of only the radios it is paired with. This override feature is helpful if using a base that has not been paired: a base can transmit to multiple rovers regardless if they are paired or not. + +![Serial Radio menu](img/Terminal/SparkFun%20RTK%20Radio%20Menu.png) + +A serial menu is also available. This menu allows users to enter pairing mode, view the unit's current Radio MAC, the MAC addresses of any paired radios, as well as the ability to remove all paired radios from memory. + +## Reference Station + +There is not enough RAM available on the Reference Station to run ESP-Now and Bluetooth simultaneously. ESP-Now does work on the Reference Station but you need to disable Bluetooth in order to use it. + +* Disable Bluetooth via the System Menu. Select "b" twice to: first select BLE mode; and then to disable Bluetooth completely + +* Restart the system using the System Menu \ Debug Menu: enter "s" followed by "d" followed by "r" to restart the Reference Station. This ensures the RAM used by Bluetooth is released + +* Select the E-Pair option by pressing the MODE button until "E-Pair" is displayed + +* Pair the Reference Station Base with an RTK Rover and the Rover will achieve RTK-Fix diff --git a/docs/menu_sensor.md b/docs/menu_sensor.md new file mode 100644 index 000000000..bdcb92e5d --- /dev/null +++ b/docs/menu_sensor.md @@ -0,0 +1,19 @@ +# Sensor Menu + +Surveyor: ![Feature Not Supported](img/Icons/RedDot.png) / Express: ![Feature Not Supported](img/Icons/RedDot.png) / Express Plus: ![Feature Supported](img/Icons/GreenDot.png) / Facet: ![Feature Not Supported](img/Icons/RedDot.png) / Facet L-Band: ![Feature Not Supported](img/Icons/RedDot.png) / Reference Station: ![Feature Not Supported](img/Icons/RedDot.png) + +![Sensor menu is shown in WiFi config](img/WiFi Config/SparkFun%20RTK%20Sensor%20Menu%20WiFi%20Config.png) + +![Sensor menu from serial prompt](img/Terminal/SparkFun%20RTK%20-%20Sensor%20Menu.png) + +*Setting the Sensor options over WiFi config and serial connections* + +The [RTK Express Plus](https://www.sparkfun.com/products/18589) utilizes the ZED-F9R GNSS receiver with built-in IMU. This allows the RTK device to continue to output high-precision location information even if GNSS reception goes down or becomes unavailable. This was designed for and is especially helpful in automotive environments, such as tunnels or parking garages, where GNSS reception because sparse. + +Enable 'Sensor Fusion' to begin using the onboard IMU when GNSS is avaialble. Sensor Fusion will only aid position information when used with an automobile and may lead to degraded position fixes when used in other situations (ie, surveying, pedestrian, etc). + +'Automatic IMU-Mount Alignment' will allow the device to automatically determine how the product is mounted within the vehicle's frame of reference. + +Additionally, wheel ticks should be provided to the unit to enhance the positional fixes. Please see [Mux Channel](menu_ports.md#mux-channel) of the Ports Menu for more information. + + diff --git a/docs/menu_system.md b/docs/menu_system.md new file mode 100644 index 000000000..935882f97 --- /dev/null +++ b/docs/menu_system.md @@ -0,0 +1,105 @@ +# System Menu + +Surveyor: ![Feature Supported](img/Icons/GreenDot.png) / Express: ![Feature Supported](img/Icons/GreenDot.png) / Express Plus: ![Feature Supported](img/Icons/GreenDot.png) / Facet: ![Feature Supported](img/Icons/GreenDot.png) / Facet L-Band: ![Feature Supported](img/Icons/GreenDot.png) / Reference Station: ![Feature Supported](img/Icons/GreenDot.png) + +## WiFi Interface + +Because of the nature of these controls, the AP config page is different than the terminal menu. + +![System Config Menu on WiFi Config Page](img/WiFi Config/SparkFun%20RTK%20WiFi%20Config%20System.png) + +*System Config Menu on WiFi Config Page* + +### Check for New Firmware + +This feature allows over-the-air updates of the RTK device's firmware. Please see [Updating RTK Firmware](firmware_update.md) for more information. + +### System Initial State + +At power on, the device will enter either Rover or Base state. + +### Log to SD + +If a microSD card is detected, all messages will be logged. + +### Max Log Time + +Once the max log time is achieved, logging will cease. This is useful for limiting long-term, overnight, static surveys to a certain length of time. Default: 1440 minutes (24 hours). Limit: 1 to 2880 minutes. + +### Max Log Length + +Every 'max long length' amount of time the current log will be closed and a new log will be started. This is known as cyclic logging and is convenient on *very* long surveys (ie, months or years) to prevent logs from getting too unwieldy and helps limit the risk of log corruption. This will continue until the unit is powered down or the *max logging time* is reached. + +### Start New Log + +Pressing the 'Start New Log' button will close the current log. A new log will be opened immediately and the file name will be shown. This can be helpful in the field when a certain set of coordinates or feature marks need to be recorded in close proximity to one another. By dividing up the logs, the work can be more easily identified. + +### Bluetooth Protocol + +By default, the RTK products use Bluetooth v2.0 SPP (Serial Port Profile) to connect to data collectors. Nearly all data collectors support this protocol. The RTK product line also supports BLE (Bluetooth Low Energy). The BLE protocol has a variety of improvements but very few data collectors support it. + +**Note:** Bluetooth SPP cannot operate concurrently with ESP-Now radio transmissions. Therefore, if you plan to use the ESP-Now radio system to connect RTK products, the BLE protocol must be used to communicate over Bluetooth to data collectors. Alternatively, ESP-Now works concurrently with WiFi so connecting to a data collector over WiFi can be used. + +### Enable Factory Defaults + +See [Factory Reset](menu_system.md#factory-reset). + +### SD Card + +Various stats for the SD card are shown. + +### Update Firmware + +New firmware may be uploaded via WiFi to the unit. See [Updating Firmware from WiFi](firmware_update.md#updating-firmware-from-wifi) for more information. + +### Reset Counter + +A counter is displayed indicating the number of non-power-on-resets since the last power-on. + +## Serial Interface + +![System menu](img/Terminal/SparkFun%20RTK%20System%20Menu.png) + +*Menu showing various attributes of the system* + +The System Status menu will show a large number of system parameters including a full system check to verify what is and what is not online. For example, if an SD card is detected it will be shown as online. Not all systems have all hardware. For example, the RTK Surveyor does not have an accelerometer so it will always be shown offline. + +This menu is helpful when reporting technical issues or requesting support as it displays helpful information about the current ZED-F9x firmware version, and which parts of the unit are online. + +* **z** - A local timezone in hours, minutes and seconds may be set by pressing 'z'. The timezone values change the RTC clock setting and the file system's timestamps for new files. + +* **d** - Enters the [debug menu](menu_debug.md) that is for advanced users. + +* **f** - Show any files on the microSD card (if present). + +* **b** - Change the Bluetooth protocol. By default, Serial Port Profile (SPP) for Bluetooth v2.0 is used. This can be changed to BLE if desired at which time serial is sent over BLESerial. Additionally, Bluetooth can be turned off. This state is normally used for debugging. + +* **r** - Reset all settings to default including a factory reset of the ZED-F9x receiver. This can be helpful if the unit has been configured into an unknown or problematic state. + +* **B, R, W, or S** - Change the mode the device is in without needing to press the external SETUP or POWER buttons. + +![System Config over WiFi](img/WiFi Config/SparkFun%20RTK%20WiFi%20Config%20System.png) + +*System Config over WiFi Config* + +The WiFi Config page also allows various aspects of the system to be configured but it is limited by design. + +## Factory Reset + +If a device gets into an unknown state it can be returned to default settings using the WiFi or Serial interfaces. + +![Factory Default button](img/WiFi Config/SparkFun%20RTK%20WiFi%20Factory%20Defaults.png) + +*Enabling and Starting a Factory Reset* + +Factory Defaults will erase any user settings and reset the internal receiver to stock settings. To prevent accidental reset the checkbox must first be checked before the button is pressed. Any logs on SD are maintained. Any settings file and commonly used coordinate files on the SD card associated with the current profile will be removed. + +![Issuing a factory reset](img/Terminal/SparkFun%20RTK%20System%20Menu%20-%20Factory%20Reset.png) + +*Issuing and confirming a Factory Reset* + +If a device gets into an unknown state it can be returned to default settings. Press 'r' then 'y' to confirm. Factory Default will erase any user settings and reset the internal receiver to stock settings. Any settings file and commonly used coordinate files on the SD card associated with the current profile will be removed. + +**Note:** Log files and any other files on the SD card are *not* removed or modified. + +Note: A factory reset can also be accomplished by editing the settings files. See Force a [Factory Reset](configure_with_settings_file.md#forcing-a-factory-reset) for more information. \ No newline at end of file diff --git a/docs/menu_tcp_udp.md b/docs/menu_tcp_udp.md new file mode 100644 index 000000000..51884dd8d --- /dev/null +++ b/docs/menu_tcp_udp.md @@ -0,0 +1,28 @@ +# Network Menu + +Surveyor: ![Feature Supported](img/Icons/GreenDot.png) / Express: ![Feature Supported](img/Icons/GreenDot.png) / Express Plus: ![Feature Supported](img/Icons/GreenDot.png) / Facet: ![Feature Supported](img/Icons/GreenDot.png) / Facet L-Band: ![Feature Supported](img/Icons/GreenDot.png) / Reference Station: ![Feature Supported](img/Icons/GreenDot.png) + +![Client and Server settings]() + +*Client and Server settings* + +As an alternative to serial ports, serial over USB, or Bluetooth, the RTK device can send and receive GNSS data over TCP, and can send data over UDP. These mechanisms sit on top of the network layer (WiFi or Ethernet). The data could be NMEA, UBX, RTCM, and is the same data that would be sent and received over Bluetooth. The mechanism can be used in Rover or Base mode. There are three mechanisms, two TCP and one UDP, described below. + +## TCP Client and Server + +Configuring a TCP Client will cause the RTK device to open a TCP connection to a given address and port, and then send and receive data. Configuring a TCP Server will cause the RTK device to listen on the given port for an incoming connection. In either case, when a connection is established the device will send and receive data. +Some Data Collector software (such as [Vespucci](gis_software.md#vespucci)) requires that the SparkFun RTK device connect as a TCP Client. Other software (such as [QGIS](gis_software.md#qgis)) requires that the SparkFun RTK device acts as a TCP Server. Both are supported. + +**Note:** Currently for WiFi: TCP is only supported while connected to local WiFi, not AP mode. This means the device will need to be connected to a WiFi network, such as a mobile hotspot, before TCP connections can occur. + +![TCP Port Entry](img/WiFi%20Config/SparkFun%20RTK%20Config%20-%20TCP%20Port.png) + +If either Client or Server is enabled, a port can be designated. By default, the port is 2947 (registered as [*GPS Daemon request/response*](https://tcp-udp-ports.com/port-2948.htm)) but any port 0 to 65535 is supported. + +![Ethernet TCP Client connection](img/Terminal/TCP_Client.gif) + +The above animation was generated using [TCP_Server.py](https://github.com/sparkfun/SparkFun_RTK_Everywhere_Firmware/blob/main/Firmware/Tools/TCP_Server.py). + +## UDP Server + +Data can also be broadcast via UDP on Ethernet and WiFi, rather than TCP. If enabled, the UDP Server will begin broadcasting NMEA data over the specific port (default 10110). diff --git a/docs/menu_wifi.md b/docs/menu_wifi.md new file mode 100644 index 000000000..35af5e9e9 --- /dev/null +++ b/docs/menu_wifi.md @@ -0,0 +1,46 @@ +# WiFi Menu + +Surveyor: ![Feature Supported](img/Icons/GreenDot.png) / Express: ![Feature Supported](img/Icons/GreenDot.png) / Express Plus: ![Feature Supported](img/Icons/GreenDot.png) / Facet: ![Feature Supported](img/Icons/GreenDot.png) / Facet L-Band: ![Feature Supported](img/Icons/GreenDot.png) / Reference Station: ![Feature Partially Supported](img/Icons/YellowDot.png) + +![WiFi Menu in AP Config page](img/WiFi Config/SparkFun%20RTK%20AP%20WiFi%20Menu.png) + +*WiFi Menu in the WiFi config page* + +![WiFi Network Entry](img/Terminal/SparkFun%20RTK%20WiFi%20Menu%20Terminal.png) + + +*WiFi Menu containing one network* + +Beginning in firmware version 3.0, the WiFi menu allows a user to input credentials of up to four WiFi networks. WiFi is used for a variety of features on the RTK device. When WiFi is needed, the RTK device will attempt to connect to any network on the list of WiFi networks. For example, if you enter your home WiFi, work WiFi, and the WiFi for a mobile hotspot, the RTK device will automatically detect and connect to the network with the strongest signal. + +Additionally, the device will continue to try to connect to WiFi if a connection is not made. The connection timeout starts at 15 seconds and increases by 15 seconds with each failed attempt. For example, 15, 30, 45, etc seconds are delayed between each new WiFi connection attempt. Once a successful connection is made, the timeout is reset. + +WiFi is used for the following features: + +* NTRIP Client or Server +* TCP Client or Server +* Firmware Updates +* Device Configuration (WiFi mode only) +* PointPerfect Key renewal (RTK Facet L-Band only) + +## Configure Mode: AP vs WiFi + +![Configure Mode in WiFi menu](img/WiFi%20Config/SparkFun%20RTK%20Config%20-%20Configure%20Mode.png) + +![WiFi Network Entry](img/Terminal/SparkFun%20RTK%20WiFi%20Menu%20Terminal.png) + +By default, the device will become an Access Point when the user selects 'Config' from the front panel controls. This is handy for in-field device configuration. Alternatively, changing this setting to 'WiFi' will cause the device to connect to local WiFi. + +![Configuring RTK device over local WiFi](img/WiFi%20Config/SparkFun%20RTK%20AP%20Main%20Page%20over%20Local%20WiFi.png) + +Configuring over WiFi allows the device to be configured from any desktop computer that has access to the same WiFi network. This method allows for greater control from a full size keyboard and mouse. + +![RTK display showing local IP and SSID](img/Displays/SparkFun%20RTK%20WiFi%20Config%20IP.png) + +When the device enters WiFi config mode it will display the WiFi network it is connected to as well as its assigned IP address. + +## MDNS + +![Access using rtk.local](img/WiFi Config/SparkFun%20RTK%20WiFi%20MDNS.png) + +Multicast DNS or MDNS allows the RTK device to be discovered over wireless networks without needing to know the IP. For example, when MDNS is enabled, simply type 'rtk.local' into a browser to connect to the RTK Config page. This feature works both for 'WiFi Access Point' or direct WiFi config. Note: When using WiFi config, you must be on the same subdomain (in other words, the same WiFi or Ethernet network) as the RTK device. diff --git a/docs/overrides/main.html b/docs/overrides/main.html new file mode 100644 index 000000000..fda5f8a6b --- /dev/null +++ b/docs/overrides/main.html @@ -0,0 +1,11 @@ +{% extends "base.html" %} + +{% block content %} +{% if true %} + + {% include ".icons/material/file-pdf-box.svg" %} + +{% endif %} + +{{ super() }} +{% endblock content %} \ No newline at end of file diff --git a/docs/permanent_base.md b/docs/permanent_base.md new file mode 100644 index 000000000..92b256af1 --- /dev/null +++ b/docs/permanent_base.md @@ -0,0 +1,338 @@ +# Creating a Permanent Base + +Surveyor: ![Feature Supported](img/Icons/GreenDot.png) / Express: ![Feature Supported](img/Icons/GreenDot.png) / Express Plus: ![Feature Not Supported](img/Icons/RedDot.png) / Facet: ![Feature Partially Supported](img/Icons/YellowDot.png) / Facet L-Band: ![Feature Partially Supported](img/Icons/YellowDot.png) / Reference Station: ![Feature Supported](img/Icons/GreenDot.png) + +![SparkFun Base Station Enclosure](img/Corrections/Roof_Enclosure.jpg) + +*The base station at SparkFun* + +This section goes into depth on how to gather the data using an RTK product to create a permanent fixed base. + +**Note:** The RTK Facet and RTK Facet L-Band are rated IP53 - Protected from limited dust ingress and water spray. As such it is not recommended for permanent outdoor deployment. Use the RTK Surveyor or Express placed inside (or protected from the elements) with a fully sealed [TOP106 antenna](https://www.sparkfun.com/products/21801) placed outside with a clear view of the sky. + +**Note:** The RTK Express Plus does not support Base mode. + +## Temporary vs. Fixed Base + +There are two types of bases: *Surveyed* and *Fixed*. A surveyed base is often a temporary base set up in the field. Called a 'Survey-In', this is less accurate but requires only 60 seconds to complete. The 'Fixed' base is much more accurate but the precise location at which the antenna is located must be known. A fixed base is often a structure with an antenna bolted to the side. Raw satellite signals are gathered for a few hours and then processed using Precision Point Position. + +In a separate tutorial, we described how to [create a temporary base station](https://learn.sparkfun.com/tutorials/setting-up-a-rover-base-rtk-system) with the 1 to 10-minute survey-in method. The temporary base method is flexible, but it is not as accurate and can vary dramatically in the time required. The ZED-F9P has a much faster way to provide base corrections: if you know the location of your antenna, you can set the coordinates of the receiver and it will immediately start providing RTCM corrections. The problem is ‘what is the location of the antenna?’. It’s as if you need a soldering iron to assemble your [soldering iron kit](https://www.sparkfun.com/products/retired/10624). Where do we start? + +**Why don’t I just survey-in my fixed antenna to get its location?** + +While a survey-in is easy to set up and fine for an in-the-field way to establish the location of a base, it’s not recommended for getting the fixed location of a static base station as it is less accurate. Instead, PPP or Precise Point Positioning is far more accurate and is recommended for obtaining your antenna’s position. It’s a similar process but involves bouncing frick’n lasers off of satellites! + +> A major problem is that the predicted orbits are often off by one meter or more. Ground stations bounce lasers off the individual satellites as they pass overhead and use this new data to compute the actual orbits of the satellites. Using this new ephemeris data, when it becomes available, combined with the receiver’s raw data, better fixes can be computed. This is the basis of PPP. + +*From Gary Miller’s [PPP HOWTO](https://gpsd.gitlab.io/gpsd/ppp-howto.html)* + +[![L1/L2 antenna attached to the roof](img/Corrections/Antenna_Semi-Fixed_to_roof.jpg)](img/Corrections/Antenna_Semi-Fixed_to_roof - Big.jpg) + +*[L1/L2 antenna](https://www.sparkfun.com/products/21801) semi-fixed to a flat roof* + +The PPP process works like this: + +* Install an antenna in a fixed location +* Gather 24 hours' worth of raw GNSS data from that antenna +* Pass the raw data to a processing center for PPP +* Obtain a highly accurate position of the antenna we use to set a ‘Fixed Mode’ on a receiver + +There are some great articles written about PPP. We’ll scrape the surface but for more information check out: + +* Gary Miller’s great [PPP HOWTO](https://gpsd.gitlab.io/gpsd/ppp-howto.html) +* Emlid’s [PPP](https://docs.emlid.com/reachm2/tutorials/post-processing-workflow/ppp-introduction/) +* Suelynn Choy, [GNSS PPP](https://www.unoosa.org/documents/pdf/icg/2018/ait-gnss/16_PPP.pdf) + +## Affix Your Antenna + +You don’t want your antenna moving once you’ve determined its position. Consider investing in a [premium antenna](https://www.sparkfun.com/products/21801) but we’ve used the classic [u-blox L1/L2 antenna](https://www.sparkfun.com/products/15192) with good success. Mount the antenna to a proper ground plane on a fixed surface that has a very clear view of the sky. No nearby anything. + +[![u-blox antenna on SparkFun parapet](img/Corrections/Base_Antenna_-_SparkFun_u-blox_Antenna1.jpg)](img/Corrections/Base_Antenna_-_SparkFun_u-blox_Antenna1.jpg) + +*The u-blox antenna attached to SparkFun’s parapet* + +We mounted the [u-blox antenna](https://www.sparkfun.com/products/15192) to the ferrous flashing around the top of the SparkFun building. While not completely permanent, the magnets on the u-blox antenna are tested to survive automobile strength winds so it should be fine in the 100+ MPH winds experienced in the front range of Colorado. The u-blox ANN-MB-00 antenna has a 5m cable attached but this was not long enough to get from the SparkFun roof to the receiver so we attached a 10m SMA extension. It’s true that most L1/L2 antennas have a built-in amplifier but every meter of extension and every connector will slightly degrade the GNSS signal. Limit the use of connector converters and use an extension as short as possible to get where you need. + +If you want to use a [higher-grade antenna](https://www.sparkfun.com/products/21801) that doesn’t have a magnetic base we’ve come up with a great way to create a stable fix point without the need for poking holes in your roof! + +[![An antenna on the roof attached to cinderblock](img/Corrections/Antenna_Semi-Fixed_to_roof.jpg)](img/Corrections/Antenna_Semi-Fixed_to_roof.jpg) + +*Yes, that’s a cinder block. Don’t laugh. It works!* + +Most surveying grade antennas have a ⅝” 11-TPI (threads per inch) thread on the bottom of the antenna. Luckily, ⅝” 11-TPI is the thread found on wedge anchors in hardware stores in the US. Wedge anchors are designed to hold walls to foundations but luckily for us, we can use the same hardware to anchor an antenna. (We’ve also heard of concrete anchors that use epoxy so be sure to shop around.) + +![Old Weather Station with concrete blocks](img/Corrections/Old Weather Setup-4.jpg) + +I needed to mount an antenna to my roof. Luckily, I had two, leftover cinder blocks from a weather station that, [based on the Electric Imp](https://learn.sparkfun.com/tutorials/weather-station-wirelessly-connected-to-wunderground/all), had long since been retired. + +![Drilling a hole in the cinder block](img/Corrections/Base_Antenna_-_Drill.jpg) + +Step one is drilling the ⅝” hole into the cinder block. The masonry bit cost me $20 but cheaper, less fancy ones can be had for [less than $10](https://www.homedepot.com/p/Drill-America-5-8-in-x-4-in-Carbide-Tipped-Masonry-Drill-Bit-DAM4X5-8/305252434). The blue tape shows me the depth I’m trying to hit. The cinder block is 3.5” thick so I settled on ~2.5” deep. Once the hole is drilled, tip the block upside down to get most of the cement dust out. Then pound the anchor into place. + +![A broken cinder block](img/Corrections/Base_Antenna_-_Broken_Block.jpg) + +*Oops!* + +Don’t get greedy! I pounded the anchor so far that it split the block. Luckily, I had a second block! + +![Foundation anchor in place](img/Corrections/Base_Antenna_-_Anchor_installed.jpg) + +Once the anchor is ~2 inches into the hole tighten the bolt. This will draw the anchor back up compressing the collar into place. **Note:** I finger tightened the bolt and added a ½ turn with a wrench. If you really go after the bolt and tighten it too much you risk pushing the collar out further and breaking the cinder block in half (see Ooops! picture above). We are not anchoring a wall here, just a [400g antenna](https://www.sparkfun.com/products/21801). + +![Antenna affixed to the anchor](img/Corrections/Base_Antenna_-_Antenna_attached.jpg) + +I used a 2nd bolt, tightened against the antenna base, to lock it into place and prevent rotation in either direction. Astute readers will notice my TNC to SMA adapter in the picture above. It’s the wrong gender. Originally, I used an [SMA extension](https://www.sparkfun.com/products/17495) to connect my [GPS-RTK-SMA](https://www.sparkfun.com/products/16481) to my u-blox L1/L2 antenna on my roof. The GPS-RTK-SMA expects a regular SMA connection so the end of the extension would not connect to this adapter. So before you get out the ladder, test connect everything! Luckily I have a set of adapters and found the right TNC to SMA converter to suit my needs. + +[![An antenna on the roof with Boulder Flatirons](img/Corrections/Antenna_Semi-Fixed_to_roof.jpg)](img/Corrections/Antenna_Semi-Fixed_to_roof.jpg) + +*It’s a bit of work getting 35lbs of concrete onto a roof but the view is pretty spectacular!* + +I wrapped the SMA extension once around the base. In case anything pulls on the SMA cable the tension will be transferred to the bolt rather than the TNC connection to the antenna. + +**Lightning Warning:** My antenna profile is lower than the parapet so lightning strikes are unlikely. Your antenna may be the highest point around so consider lightning protection. + +## Gather Raw GNSS Data + +Once you’ve got the antenna into a location where it *will not move or be moved* we need to establish its location. Power on your RTK unit and verify that you can get a lock and see 25+ satellites. Assuming you’ve got good reception, we now need to set the receiver to output raw data from the satellites. + +You will need a microSD card that is 1GB up to 32GB formatted for FAT16 or FAT32. + +![The microSD slot on the bottom of the RTK Facet](img/Corrections/SparkFun_RTK_Facet_-_Ports_-_microSD.jpg) + +*The microSD slot on the bottom of the RTK Facet* + +**Enable RAWX and SFRBX** + +Power on the unit and using the [serial](configure_with_serial.md) or [WiFi method](configure_with_wifi.md), connect to the device. + +**Configure via WiFi** + +![Enable the RAWX message](img/WiFi Config/RTK_Surveyor_-_WiFi_Config_-_GNSS_Config_Messages.jpg) + +Expand the Message Rates sub-menu under the GNSS Config menu. Pressing the NMEAx5 + RXMx2 message button will turn off all messages and enable the following messages: + +* NMEA-GGA, NMEA-SGA, NMEA-GST, NMEA-GSV, NMEA-RMC, RXM-RAWX, RXM-SFRBX + +These seven sentences are commonly used when logging and doing Precise Point Positioning (PPP) or Post Processed Kinematics (PPK). + +Press the 'Save and Exit' button. Upon reset, the unit should begin displaying a gradually increasing [logging icon](displays.md#rover-fix) indicating successful logging. + +**Configure via Serial** + +![Press 2 and then 8 to enable the PPP logging defaults](img/Terminal/SparkFun_RTK_Express_-_Messages_Menu.jpg) + +*Press 2 and then 8 to enable the PPP logging defaults* + +After enabling the NMEA and RXM messages, exit from the serial menu by pressing x repeatedly. The system will save and apply the settings. + +Once the RTK product is configured, power it up with microSD inserted, and leave the unit in **Rover** mode. This will record all the data (NMEA, UBX, and RAWX) from the receiver to a *.ubx file. We do not yet know the location of the antenna so we stay in Rover mode to allow it to compile a large amount of satellite data. Only after we have confirmed its location should you enter **Base** mode. + +![The logging icon will remain animated while the log file is increasing](img/Displays/SparkFun_RTK_Facet_-_Main_Display_Icons.jpg) + +*The logging icon will remain animated while the log file is increasing* + +**Confirm Recording** + +Before leaving the unit for 6 to 24 hours, it is recommended that you capture a few minutes of RAWX log data, with the antenna located with a clear view of the sky, and then inspect the log to confirm everything is working correctly. + +![Getting UBX file from SD card](img/Corrections/SparkFun%20RTK%20Facet%20SD%20RAWX%20Log%20Files.png) + +*Getting UBX file from SD card* + +Remove the microSD from the RTK unit and open it on a computer. The latest log file is shown above. Note the file shown above is 492kB because it logged only ~60 seconds. Log files with RAWX and SFRBX and NMEA will grow in size to over 1GB across 24 hours. + +The quickest method to verify RAWX logging is to open the UBX file with a text editor. + +![Viewing a RAWX log in a text editor](img/Corrections/SparkFun%20RTK%20Facet%20Text%20Editor%20RAWX%20packets.png) + +*NMEA and UBX binary data viewed in Visual Studio Code* + +Your editor may render the binary UBX RAWX data in unknown ways. If you see NMEA ASCII sentences combined with large chunks of binary data, the RTK product is correctly logging RAWX data. + +If you have u-center installed, you can more easily inspect for successful logging. Double click on a UBX file to open them in u-center. + +![Viewing a RAWX log in u-center](img/Corrections/SparkFun%20RTK%20Facet%20u-center%20view%20of%20Log%20Files.png) + +*Viewing a RAWX log in u-center* + +Press the play button (shown above) and you should see satellites quickly come in and out of view as u-center 'plays' back the log file. + +![RAWX packet within the Packet Console](img/Corrections/SparkFun%20RTK%20Facet%20u-center%20RAWX%20packets.png) + +*RAWX packet within the Packet Console* + +Open the Packet Console and verify the UBX RXM-RAWX and SFRBX packets are logged. You are now ready to do a long survey of the antenna's position. Allow this to run for 24 hours. Don’t worry if you go long but do realize that a 24-hour file will be ~1GB so don’t let it run for a month. + +![Graph of record time vs position error](img/Corrections/PPP_record_time_vs_error.jpg) + +*From Suelynn Choy's ‘[GNSS Precision Point Positioning](https://www.unoosa.org/documents/pdf/icg/2018/ait-gnss/16_PPP.pdf)’ presentation 2018* + +Capturing 6 hours is good, 24 is slightly better (note the logarithmic scale for position error in the graph above). Most PPP analysis services will accept more than 24 hours of data but they may truncate it to 24 hours. If you capture 30 hours of RAWX data, that’s ok, we will show you how to trim a file that is too long. + +## Converting UBX to RINEX + +![RTKLIB conversion of ubx to obs](img/Corrections/Convert_UBX_to_OBS_with_time_22_hour_window.jpg) + +Once the 24-hour log file is obtained, the 1GB UBX file will need to be converted to RINEX (Receiver Independent Exchange Format). The popular [RTKLIB](http://www.rtklib.com/) is here to help. We recommend the rtklibexplorer’s modified version of RTKLIB (available for download [here](http://rtkexplorer.com/downloads/rtklib-code/)) but you can obtain the original RTKLIB [here](http://www.rtklib.com/). Open RTKCONV. Select your UBX file and hit ‘Convert’. Our 300MB file took ~30 seconds to convert. You should see a *.obs file once complete. + +![Opening an OBS file to view the start and stop time](img/Corrections/RTKCNV_-_OBS_Time_stamps1.jpg) + +*An OBS file with 14 hours of data* + +If your data file is 25 hours or a little more, that’s fine. If you need to cut your RINEX file down because it’s too large (or 40 hours long) you can trim the time window. Convert the entire file then click on the notepad icon to open the OBS file. You’ll see the GPS start time and stop time for this capture. + +![Limiting the time window of the conversion](img/Corrections/Convert_UBX_to_OBS_with_time_22_hour_window2.jpg) + +Using these times, you can limit the time window to whatever you need and re-convert the file. + +**Why don’t we crank up the fix rate? Moar is better!™** + +The RTK products can log fix rates up to 20Hz. Why not get RAWX data at greater than 1Hz? Because nature doesn’t move that fast. Most PPP analysis services will ignore anything greater than 1Hz. OPUS goes so far as to “decimate all recording rates to 30 seconds”. And, your OBS files will be monstrously large. If 24 hours is 1GB at 1Hz, it follows that 24 hours at 30Hz will be ~30 gigs. So no, keep it at 1Hz. + +We now need to pass the raw GNSS satellite data in RINEX format (*\*.obs*) through a post-processing center to try to get the actual location of the antenna. There are a handful of services but we’ve had great luck using the Canadian [CSRS-PPP service](https://webapp.geod.nrcan.gc.ca/geod/tools-outils/ppp.php?locale=en). The US National Geodetic Service provides a service called [OPUS](https://www.ngs.noaa.gov/OPUS/) but we found it to be frustratingly limited by file size and format issues. Your mileage may vary. + +![Selecting ITRF upload on CSRS for PPP](img/Corrections/Uploading_file_to_CSRS.jpg) + +Zip your obs file then create an account with [CSRS](https://webapp.geod.nrcan.gc.ca/geod/tools-outils/ppp.php?locale=en). Select ITRF then upload your file. Twiddle your thumbs for a few hours and you should receive an email that looks like this: + +![Email from CSRS Summary](img/Corrections/Email_from_CSRS_Summary_.jpg) + +Click the 'Summary' link to open a summary of results. This summary contains the coordinates of your antenna in Geodetic, UTM, and Cartesian formats. + +![Output from CSRS](img/Corrections/SparkFun_PPP_Results.png) + +*The SparkFun antenna with +/-2mm of accuracy! :O* + +The email will also include a [fancy PDF report](img/Corrections/SparkFun-PPP.pdf) of your antenna’s location but does not include the Cartesian coordinates we will need later. + +If all goes well you should have a very precise location for your antenna. For SparkFun RTK products we are most interested in ECEF coordinates. [ECEF](https://en.wikipedia.org/wiki/ECEF) is *fascinating*. Rather than lat and long, ECEF is the number of meters from the internationally agreed-upon reference frame of the center of mass of the Earth. Basically, your ECEF coordinates are the distance you are from the *center of the Earth*. Neat. + +## Setting Fixed Location + +Now that you’ve got the ECEF position of your antenna, let’s tell the RTK product where its antenna is located with a few millimeters of accuracy. + +**Configure via WiFi** + +Enter the WiFi AP config page or connect over Serial. + +![Setting ECEF coordinates over WiFi](img/WiFi Config/RTK_Surveyor_-_WiFi_Config_-_Base_Config1.jpg) + +*Setting ECEF coordinates over WiFi* + +Select **Fixed** and **ECEF Coordinates** then enter the coordinates obtained from the CSRS-PPP email. + +![Configuring NTRIP Server settings via WiFi Config AP](img/WiFi Config/RTK_Surveyor_-_WiFi_Config_-_Base_Config2.jpg) + +*Configuring NTRIP Server settings via WiFi Config AP* + +If your RTK product has access to a WiFi network, consider enabling the NTRIP Server. This will allow the RTK device to automatically begin transmitting its RTCM data to an NTRIP Caster once it has entered Fixed Base mode. See [Creating NTRIP Caster](permanent_base.md#creating-ntrip-caster) for more information. + +Save the settings to the RTK unit and exit. + +**Configure via Serial** + +![Base Menu Options](img/Terminal/SparkFun_RTK_Express_-_Base_Menu.jpg) + +*Base Menu Options* + +To configure over serial, press 3 to open the Base menu, then 1 to toggle the Base Mode to Fixed/Static Position, then enter the three ECEF coordinates. + +![Settings for the NTRIP Server](img/Terminal/SparkFun_RTK_Express_-_Base_Menu_-_Fixed_NTRIP.jpg) + +*Settings for the NTRIP Server* + +If your RTK product has access to a WiFi network, consider enabling the NTRIP Server. This will allow the RTK device to automatically begin transmitting its RTCM data to an NTRIP Caster once it has entered Fixed Base mode. See [Creating NTRIP Caster](permanent_base.md#creating-ntrip-caster) for more information. + +Press x multiple times to exit the serial menu. + +**Beginning Base Mode** + +Power cycle the unit to load the Fixed ECEF coordinates. After boot, use the **Setup** button to enter **Base** mode. + +![RTK Facet in Fixed Transmit Mode](img/Displays/SparkFun_RTK_Express_-_Display_-_FixedBase-Xmitting.jpg) + +*RTK Facet in Fixed Transmit Mode* + +Almost immediately after entering Base mode, the unit will begin outputting RTCM messages. These RTCM messages are sent to the **RADIO** port on the device. This is helpful for transmitting corrections via serial radio to any Rover units within a 10km baseline. + +![RTK Facet in Transmit Mode with NTRIP Enabled](img/Displays/SparkFun_RTK_Express_-_Display_-_FixedBase-Casting.jpg) + +*RTK Facet in Transmit Mode with NTRIP Server Enabled* + +If the NTRIP server is enabled the device will first attempt to connect over WiFi. The WiFi icon will blink until a WiFi connection is obtained. If the WiFi icon continually blinks be sure to check your SSID and PW for the local WiFi. + +Once WiFi connects the device will attempt to connect to the NTRIP mount point. Once successful the display will show 'Casting' along with a solid WiFi icon. The number of successful RTCM transmissions will increase every second. + +## Creating NTRIP Caster + +Your RTK device can both serve RTCM correction data (aka Server) to an NTRIP Caster and get RTCM correction data (aka Client) from an NTRIP Caster. But how does one get an NTRIP Caster? + +There are a variety of Windows applications out there that claim to be an NTRIP caster. We found them to be generally terrible. The easiest solutions we've found are [RTK2GO](http://rtk2go.com) or [Emlid Caster](https://emlid.com/ntrip-caster/). Both are free and available to the public. + +**RTK2Go** + +[RTK2Go](http://rtk2go.com/) seems to be a pet project of SNIP. We recommend creating a mount point and a password through RTK2GO.com. Yes, the RTK2go website looks spammy but the service works well and is used widely. Please see [RTK2Go](http://rtk2go.com/) for details about creating an account. It's free and takes only a few minutes. Once activated you will be provided with your Mount Point name and Mount Point PW. These two credentials are used in an NTRIP Server setup: + +**NTRIP Server:** + +* Caster Host: rtk2go.com +* Caster Port: 2101 +* Caster User Name: Not needed +* Caster User PW: Not needed +* Mount Point: Provided by RTK2Go +* Mount Point PW: Required and provided by RTK2Go + +**NTRIP Client:** + +* Caster Host: rtk2go.com +* Caster Port: 2101 +* Caster User Name: **Your Valid Email Address** +* Caster User PW: Not needed +* Mount Point: Provided by RTK2Go +* Mount Point PW: **Not Needed** + +The differences between Server and Client are small. The Server needs to know the mount point PW as it needs to be authorized to push data there. The Client needs to know which mount point but does not need a mount point password. + +**Note:** You must provide a valid email address to RTK2Go. From RTK2Go: + +> It is now REQUIRED that all data consumers (Rover devices) provide a valid email address in the NTRIP Client user account name field when accessing the Caster. + +![The SparkFun Mount Point bldr_SparkFun1](img/Corrections/SparkFun%20RTK%20RTK2Go%20SparkFun%20Mount%20Point.png) + +*The SparkFun Mount Point 'bldr_SparkFun1'* + +To verify that your RTK product is correctly broadcasting RTCM data, you can access RTK2Go from a browser on Port 2101. This link [RTK2Go.com:2101](http://www.rtk2go.com:2101/) will show a list of all current NTRIP Servers that are pushing data to the RTK2Go caster, and are available to be accessed, free of charge, by any NTRIP Client in the world. + +**Emlid Caster** + +[Emlid Caster](https://emlid.com/ntrip-caster/) is also very easy to set up and has a bit more user-friendly-looking website. Creating an account is very straightforward. + +![Emlid Mount Points](img/Corrections/SparkFun%20RTK%20Emlid%20Mount%20Points.png) + +*Emlid Mount Points with PWs removed* + +Once your account is created, you'll be presented with Mount Points and Rovers. + +**NTRIP Server:** + +* Caster Host: caster.emlid.com +* Caster Port: 2101 +* Caster User Name: Not needed +* Caster User PW: Not needed +* Mount Point: Required. MP1979 for example. Shown in your mount point dashboard. +* Mount Point PW: Required. Shown in your mount point dashboard. + +**NTRIP Client:** + +* Caster Host: caster.emlid.com +* Caster Port: 2101 +* Caster User Name: Required. Shown in your rover dashboard. +* Caster User PW: Required. Shown in your rover dashboard. +* Mount Point: Required. Shown in your rover dashboard. +* Mount Point PW: Not Needed + +![Emlid Mount Points](img/Corrections/SparkFun%20RTK%20Emlid%20Mount%20Points.png) + +*'Online' Badge Illuminated* + +To verify that your RTK product is correctly broadcasting RTCM data open the Emlid Caster dashboard. Once a device is successfully connected as a mount point or rover, the Emlid dashboard will turn green. diff --git a/docs/readme.md b/docs/readme.md new file mode 100644 index 000000000..614209f50 --- /dev/null +++ b/docs/readme.md @@ -0,0 +1 @@ +This folder contains the markdown files for the [RTK Product Manual](https://docs.sparkfun.com/SparkFun_RTK_Firmware/) that are rendered by [mkdocs](https://www.mkdocs.org/). \ No newline at end of file diff --git a/docs/repair.md b/docs/repair.md new file mode 100644 index 000000000..a3bd4011a --- /dev/null +++ b/docs/repair.md @@ -0,0 +1,258 @@ +# Disassembly / Repair + +Surveyor: ![Feature Supported](img/Icons/GreenDot.png) / Express: ![Feature Supported](img/Icons/GreenDot.png) / Express Plus: ![Feature Supported](img/Icons/GreenDot.png) / Facet: ![Feature Supported](img/Icons/GreenDot.png) / Facet L-Band: ![Feature Supported](img/Icons/GreenDot.png) / Reference Station: ![Feature Supported](img/Icons/GreenDot.png) + +The RTK product line is fully open-source hardware. This allows users to view schematics, code, and repair manuals. This section documents how to safely disassemble the RTK Facet and [Reference Station](#reference-station). + +Repair Parts: + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SparkFun RTK Replacement Parts - Facet L-Band Main Board v14 (SPX-24675)SparkFun RTK Replacement Parts - Facet Main Board v13 (SPX-24064)SparkFun RTK Replacement Parts - Facet Housing (SPX-24673)
SparkFun RTK Replacement Parts - Facet L-Band Housing (SPX-24674)SparkFun RTK Replacement Parts - Facet Rubber Sock (SPX-24707)SparkFun RTK Replacement Parts - Facet Connector Assembly v12 (SPX-24706)
SparkFun RTK Replacement Parts - Facet Display/Button (SPX-24705)
+ +Tools Needed: + +* [Small Philips Head Screwdriver](https://www.sparkfun.com/products/9146) +* [Curved Tweezers](https://www.sparkfun.com/products/10602) +* [U.FL Puller](https://www.sparkfun.com/products/20687) - *Recommended* +* [Wire Cutters](https://www.sparkfun.com/products/10447) - *Recommended* + +## Opening Facet + +![Remove the protective silicone boot](img/Repair/SparkFun-RTK-Repair-1.jpg) + +Starting from the back of the unit, remove the protective silicone boot. If your boot has gotten particularly dirty from field use, rinse it with warm water and soap to clean it up. + +![Remove four Philips head screws](img/Repair/SparkFun-RTK-Repair-2.jpg) + +Remove the four Philips head screws. They may not come all the way out of the lower enclosure. + +![Lid removed](img/Repair/SparkFun-RTK-Repair-3.jpg) + +The top lid should then come off. The front overlay is adhesive and may adhere slightly to the 'tooth' on the lid. You will not damage anything by gently prying it loose from the lid as you lift the lid. + +**Tip:** The lid makes a great screw bin. + +Before starting to remove the antenna, be aware that the antenna material is susceptible to fingerprints. You won't likely damage the reception but it's best to just avoid touching the elements. + +![Remove the antenna](img/Repair/SparkFun-RTK-Repair-4.jpg) + +Note the antenna orientation so that it can be re-mounted in the same way. A sharpie dot towards the display is a handy method. However, if you forget, this is the correct orientation: + +![image](https://github.com/user-attachments/assets/2d3c4f1e-6d20-4b78-9bcf-5399af5d2093) + +Remove the four screws holding the antenna in place. + +![Antenna to the side](img/Repair/SparkFun-RTK-Repair-5.jpg) + +The antenna will be attached to the main board and must stay that way for the next few steps. Without pulling on the thin RF cable, gently set the antenna to the side. + +![Remove battery boat](img/Repair/SparkFun-RTK-Repair-6.jpg) + +The battery and vertical PCBs are held in place using a retention PCB. Remove the four screws holding the PCB in place and lift off the foam top of the battery holder. + +**Note:** v1.0 of the retention plate is not symmetrical. Meaning, if the plate is installed in reverse, the retention PCB will be just short of the connector board and will not properly hold it in place. Reinstall the retention plate as shown in the picture. + +**Note:** The foam is held to the PCB using an adhesive. Some of that adhesive is exposed to catch material that may enter into the enclosure. Try to avoid getting stuck. + +![Holder PCB to the side](img/Repair/SparkFun-RTK-Repair-7.jpg) + +Set the retention PCB to the side. + +## Removing Antenna Connection + +![Removing the antenna connection](img/Repair/SparkFun-RTK-Repair-8.jpg) + +This is the most dangerous step. The cable connecting the antenna to the main board uses something called a U.FL or IPEX connector. These tend to be fragile. You can damage the connector rendering the unit inoperable. Just be sure to take your time. + +Using the U.FL removal tool, slide the tool onto the U.FL connector and gently pull away from the main board. If it won't give, you may need to angle the tool slightly while pulling. + +**Note:** If you do not have a U.FL tool this [tutorial on U.FL connectors](https://learn.sparkfun.com/tutorials/three-quick-tips-about-using-ufl/all#disconnect) has three alternative methods using tweezers, wire cutters, and a skinny PCB that may also work. + +![U.FL connector removed](img/Repair/SparkFun-RTK-Repair-9.jpg) + +The U.FL connector will disconnect. The antenna can now be set to the side. + +## Opening Backflip Connectors + +![Flipping connector](img/Repair/SparkFun-RTK-Repair-10.jpg) + +Many of the connections made within the RTK product line use this 'back flip' style of FPC connector. To open the connector and release the flex printed circuit (FPC) cable, use a curved pair of tweezers to gently flip up the arm. The arm in the connector above has been flipped, the FPC can now be removed. + +As shown above, remove the FPC connecting to the 4th connector on the main board. The connector is labeled 'SD Display'. Leave all other FPCs in place. + +![Removing main board](img/Repair/SparkFun-RTK-Repair-13.jpg) + +The main board is attached to the battery and the connector board. Lift the mainboard and connector board together, bringing the battery with the assembly. + +## Removing the Battery + +![Removing the JST connector](img/Repair/SparkFun-RTK-Repair-14.jpg) + +**Note:** This step is not needed for general repair. Only disconnect the battery if you are replacing the battery. + +The battery is plugged into the mainboard using a JST connector. These are very strong connectors. *Do not* pull on the wires. We recommend using the mouth of wire cutters (also known as diagonal cutters) to pry the connector sideways. + +Once removed, the battery can be set aside. + +## Removing the Front Overlay + +![Disconnecting overlay](img/Repair/SparkFun-RTK-Repair-15.jpg) + +The front overlay (the sticker with the Power button) is connected to the display board using the same style 'back flip' FPC connector. Flip up the arm and disconnect the overlay. + +![Peeling off the overlay](img/Repair/SparkFun-RTK-Repair-16.jpg) + +Gently peel off the adhesive overlay from the front face. This cannot be saved. + +## Inserting New Display Board + +![New display board in place](img/Repair/SparkFun-RTK-Repair-17.jpg) + +Slide the old display board out. Remove the brown FPC from the old display board and move over to the new display board. Insert the new display board into the slot. + +![Remove film](img/Repair/SparkFun-RTK-Repair-18.jpg) + +With the new display board in place, remove the protective film from the display. + +![Apply new overlay](img/Repair/SparkFun-RTK-Repair-19.jpg) + +Remove the backing from the new overlay. Stick the overlay into the center of the front face area. + +![Insert tab into connector](img/Repair/SparkFun-RTK-Repair-20.jpg) + +Be sure to flip up the arm on the overlay connector before trying to insert the new overlay FPC. + +Using tweezers, and holding the FPC by the cable stiffener, insert the overlay FPC into the display board. + +![Modern display board with 6-pin friction connector](img/Repair/RTK-Facet-Repair-3.jpg) + +Shown above, the modern display boards use a 6-pin friction fit connector. There is no backflip arm that needs to be raised. Hold the FPC by the cable stiffener and push it down into the black connector on the display board. + +## Closing The Backflip Connector + +![Closing the arm on an FPC](img/Repair/SparkFun-RTK-Repair-21.jpg) + +Use the nose of the tweezers to press the arm down, securing the FPC in place. + +![FPC in the display board connector](img/Repair/SparkFun-RTK-Repair-22.jpg) + +If you haven't already done so, move the brown FPC from the original display board over to the new display board. Be sure to open the connector before inserting the FPC, and then press down on the arm to secure it in place. + +## Reinstalling Main Board + +![Reinstalling Main board](img/Repair/SparkFun-RTK-Repair-23.jpg) + +Slide the main board and connector boards back into place along with the battery. We find it easier to partially insert the connector board, then the main board, and then adjust them down together. + +![Handling FPCs](img/Repair/SparkFun-RTK-Repair-11.jpg) + +Reconnect the display board to the main board. Be sure to close the arm on the main board to secure the FPC in place. + +## Testing the Overlay + +![Internal Power Button](img/Repair/SparkFun-RTK-Repair-24.jpg) + +The RTK Facet has two power buttons: the external button on the overlay and an internal button on the back of the display board (shown above). Pressing and holding the internal button will verify the connection between the display board and the main board. + +If the internal button is not working, remove and reinsert the FPC connecting the display board to the main board. + +Press and hold the internal power button to power down the unit. + +![Using overlay power button](img/Repair/SparkFun-RTK-Repair-26.jpg) + +Repeat the process using the overlay button to verify the external power button is working. + +If the external overlay button is not working, but the internal button is, remove and reinsert the FPC connecting the overlay to the display board. + +If the external button is working, proceed with re-assembling the unit. + +## Reassembly + +Confirm that all FPC armatures are in the down and locked position. + +![Attach U.FL connector](img/Repair/SparkFun-RTK-Repair-9.jpg) + +Carefully line the U.FL connector up with the main board and gently press the connector in place. A tool is useful in this step but an index finger works just as reliably. + +![Protect the battery](img/Repair/SparkFun-RTK-Repair-5.jpg) + +Place the retention plate and foam over the battery. The battery may need to be nudged slightly to align with the upper cavity. + +**Note:** v1.0 of the retention plate is not symmetrical. Meaning, if the plate is installed in reverse, the retention PCB will be just short of the connector board and will not properly hold it in place. Reinstall the retention plate as shown in the picture above. + +Secure the retention plate with the four *small* screws. + +![Reattach antenna](img/Repair/SparkFun-RTK-Repair-4.jpg) + +Place the antenna over top of the retention plate in the same orientation as it was removed. Secure in place with the four *large* screws. + +![Dome showing the front tooth](img/Repair/SparkFun-RTK-Repair-3.jpg) + +Plate the dome over the antenna with the front 'tooth' aligning over the display. + +![Insert four screws into dome](img/Repair/SparkFun-RTK-Repair-2.jpg) + +Secure the dome in place using four *small* screws. + +Replace the silicone boot around the device. + +Power on the RTK Facet and take outside to confirm SIV reaches above ~20 satellites and HPA is below ~1.0m. + +## Reference Station + +Taking the Reference Station apart is really easy: + +* Disconnect all cables + +* Unplug the green 10-way 3.5mm I/O connector + * This makes it easy to remove the main PCB from the enclosure + * The connector is a firm fit. You may need to rock it from side to side as you unplug it + +* Unscrew the four screws holding the front panel in place + * We recommend removing the front panel first, so you can unplug the OLED display + +* Remove the front panel + +![Image of the Reference Station with the front panel removed](img/Repair/Ref_Station_Disassembly.png) + +* Unplug the OLED Qwiic cable + +* Slide out the main PCB + +## Surveyor + +Disassembly of the RTK Surveyor is achieved by removing two Philips head screws and gently lifting the cover. + +![Internal Surveyor Switches](img/Repair/RTK_Surveyor_Internal_-_NMEA_Switches.jpg) + +Within the RTK Surveyor, two internal slide switches control the flow of NMEA data over Bluetooth and should be in the position shown above. If these switches get moved, the device will fail to correctly push NMEA data over Bluetooth. Placing a bit of tape over the top of the switches can help keep them in place. + + + + diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 000000000..8ed2fe980 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,167 @@ + +site_name: SparkFun RTK Product Manual +site_description: A guide to the SparkFun RTK hardware, firmware, and features. +site_url: https://docs.sparkfun.com/SparkFun_RTK_Firmware/ + +repo_name: sparkfun/SparkFun_RTK_Firmware +repo_url: https://github.com/sparkfun/SparkFun_RTK_Firmware +edit_uri: edit/main/docs/ + +theme: + name: material + custom_dir: docs/overrides + logo: img/Icons/sfe_logo_sm.png + palette: + primary: grey + accent: red + font: + text: Roboto + code: Roboto Mono + features: + - content.code.annotate + + # Enable "view source" and "edit this page" buttons + - content.action.edit + + # Enables anchor tracking (updates page url with the section user is on) + # i.e. https://docs.sparkfun.com//# + - navigation.tracking + - navigation.tabs.sticky + + # Enables tabs for navigating the website + # - navigation.tabs + # Keeps tabs visible in the header when scrolling + #- navigation.tabs.sticky + + # Adds pop-up button just below the header (when the user starts to scroll up) + # Allows users to easily jump to the beginning of the page + - navigation.top + + # Renders path for page navigation at top of the page (makes page navigation more mobile friendly) + - navigation.path + + + # Icon in Browser Tab (must be square img - i.e. 32x32 pixels) + favicon: img/Icons/sfe_logo_sq.png + + icon: + repo: fontawesome/brands/github + + # Sets icon for "edit this page" buttons + edit: material/file-document-edit-outline + + admonition: + note: octicons/tag-16 + abstract: octicons/checklist-16 + info: octicons/info-16 + tip: octicons/squirrel-16 + success: octicons/check-16 + question: octicons/question-16 + warning: octicons/alert-16 + failure: octicons/x-circle-16 + danger: octicons/zap-16 + bug: octicons/bug-16 + example: octicons/beaker-16 + quote: octicons/quote-16 + +plugins: + - search + - monorepo + + #Generates PDF + - with-pdf: + copyright: SparkFun Electronics - 2023 + cover_subtitle: Simple and Cost Effective High-Precision Navigation + output_path: pdf/SparkFun_RTK_User_Manual.pdf + author: This PDF is automatically generated. See https://docs.sparkfun.com/SparkFun_RTK_Firmware/ for the latest version. + toc_title: Table Of Contents + #cover_title: TITLE TEXT + #heading_shift: false + #toc_level: 3 + #ordered_chapter_level: 2 + #exclude_pages: + # - 'bugs/' + # - 'appendix/contribute/' + #convert_iframe: + # - src: IFRAME SRC + # img: POSTER IMAGE URL + # text: ALTERNATE TEXT + # - src: ... + #two_columns_level: 3 + #debug_html: true + #show_anchors: true + #verbose: true + +copyright: + Copyright 2023 - SparkFun Electronics®
+
6333 Dry Creek Parkway, Niwot, Colorado 80503 + +markdown_extensions: + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences + - pymdownx.details + - pymdownx.betterem: + smart_enable: all + - pymdownx.mark + - pymdownx.caret + - pymdownx.tilde + - tables + - admonition + +extra: + social: + - icon: fontawesome/brands/youtube + link: https://www.youtube.com/sparkfun + - icon: fontawesome/brands/instagram + link: https://www.instagram.com/sparkfun + - icon: fontawesome/brands/github + link: https://www.github.com/sparkfun + - icon: fontawesome/brands/facebook + link: https://www.facebook.com/SparkFun + - icon: fontawesome/brands/twitter + link: https://twitter.com/sparkfun + +nav: + - Introduction: index.md + - intro.md + - connecting_bluetooth.md + - GIS Software: + - gis_software_android.md + - gis_software_ios.md + - gis_software_windows.md + - Configuration Methods: + - configure_with_wifi.md + - configure_with_bluetooth.md + - configure_with_serial.md + - configure_with_settings_file.md + - configure_with_ucenter.md + - Configuration Menus: + - menu_base.md + - menu_data_logging.md + - menu_debug.md + - menu_ethernet.md + - menu_gnss.md + - menu_messages.md + - menu_tcp_udp.md + - menu_ntp.md + - menu_pointperfect.md + - menu_ports.md + - menu_profiles.md + - menu_radios.md + - menu_sensor.md + - menu_system.md + - menu_wifi.md + - Hardware: + - firmware_update.md + - displays.md + - embeddedsystem_connection.md + - repair.md + - RTK Corrections: + - correction_sources.md + - correction_transport.md + - permanent_base.md + - accuracy_verification.md + - contribute.md