diff --git a/.gitignore b/.gitignore index b9f3806..7f2aa95 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .pio .vscode +src/compile_time.h +data/user.ini diff --git a/README.md b/README.md index 4427c08..7ec72a3 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,23 @@ Spotify controller application for the [ThingPulse Color Kit Grande](https://thingpulse.com/product/esp32-wifi-color-display-kit-grande/). -[![Color Kit Grande with sample application: weather station](https://thingpulse.com/wp-content/uploads/2022/10/ThingPulse-Color-Kit-Grand-with-sample-application.jpg)](https://thingpulse.com/product/esp32-wifi-color-display-kit-grande/) + + + HomeView UI Example + + + +

+ + Cover View + + + Clock View + + + Diagnostic View + +

## Purpose of this project @@ -15,11 +31,53 @@ The currently playing song can be paused, resumed and skipped to the next or pre A full OAuth 2.0 web flow is used to acquire the necessary access and refresh tokens to permit the user to control the player. In order to run this project on your device, you will need to setup an application on your Spotify dashboard (instructions below). -## Features + ## Features - - Artwork download - - Control player on touch screen: play, Pause, next, prev - - Authentication and authorization (OAuth 2.0 flow) on device. +- **Spotify Playback Control** + - Play, pause, skip to next/previous track from the touch screen + - Control playback on any active Spotify Connect device linked to your account (e.g., phone, browser, smart speaker) + +- **Album Art Display** + - Downloads and displays album artwork via Spotify Web API + - Syncs background color with album art + - Caches artwork locally for performance + +- **Multiple UI Modes** + - Home view with track metadata and album art + - Cover Art only view + - Clock view with time and playback progress + - Supports 12-hour and 24-hour time formats + - Diagnostics view with system stats and Spotify state + +- **OAuth 2.0 Authorization Flow** + - Authentication and authorization (OAuth 2.0 flow) on device + +- **Designed for ESP32 + TFT Touch** + - Built using PlatformIO and Arduino + - Touch event handling for UI buttons and screen navigation + - Takes advantage of ESP32 dual-core architecture: UI logic runs on one core and background song and album art refreshing runs on the other + +- **Extensible Design** + - Modular architecture allows easy addition of new Views (UI screens) + - Built-in monitoring tools for measuring system performance and UI responsiveness + - Structured and tag-based logging system for easier debugging and analysis + - Optional `user.ini` file for easy credential and time zone management + + + + + Design + + +## Using the Spotify Controller + +- Tap the **Prev**, **Pause/Play**, and **Next** buttons to control music playback. +- Tap the **album art** on the Home view to switch to the **Cover Art view**. +- Tap the **clock** to switch to the **Clock view**. +- Tap the **network status box** in the lower-right corner to open the **Diagnostics view**. +- Tap the top-left corner for **Prev**, the top-center for **Pause/Play**, and the top-right for **Next** when using views other than Home. + +> For detailed display logic and diagnostics layout, see `DiagnosticsView.cpp`. ## Service level promise @@ -37,28 +95,52 @@ See our [instructions](https://docs.thingpulse.com/guides/esp32-color-kit-grande 1. Go to [https://developer.spotify.com/dashboard/login](https://developer.spotify.com/dashboard/login) and login to or sign up for the Spotify Developer Dashboard -2. Select "My New App" +2. Select "Create app" 3. Fill out the form. Give your new app a name you can attribute to this project. It's safe to select "I don't know" for the type of application. +Add "http://tp-spotify.local/callback/" to the Redirect URIs section. + + **NOTE** If you are running more than ThingPulse Spotify Remote in the same WiFi network, you should choose a unique name rather than "tp-spotify". Regardless of what you choose it has to reflect what you set for `SPOTIFY_ESPOTIFIER_NODE_NAME` in `spotify.h` in the project. + + + + **Don't forget to save your settings.** + +4. Set the unique Client ID and Client Secret as values for the respective variables in `src/spotify.h`. + + + +### Filesystem setup + +5. Upload the file system to the device + +- Hit the PlatformIO icon on the navigation bar on the left side (alien face). + +- Select the Platform > Upload Filesystem Image task. Unless you later erase the flash or modify certain files, you only need to do this once if it succeeds. Pay attention to the output in the VS Code console that opens. If it reports any errors like e.g. if it cannot connect to the board or if stops midway, close VS Code completely, restart it, and then repeat the process. + + + +- If startup fails with the following message displayed: "FATAL ERROR - Filesystem Not Initialized", this step was not successful or done. + +### User settings - +6. The fastest way to get up and running is to open the `src/settings.h` file and adjust the handful of configuration parameters in the "User settings" section at the top. They are all documented inside the file directly. Everything should be self-explanatory. The spotify settings were updated in step 4 above. -4. At the end of the 3 steps click "Submit" + - +See [full user settings documentation](./documentation/UserSettings.md) for details about all available fields, encryption options, and using `user.ini`. -5. Set the unique Client ID and Client Secret as values for the respective variables in `settings.h`. + ### Upload code to device - + 7. Select the General > Upload and Monitor task. You do this every time you change code or settings.h. -6. Click on "Edit Settings". Add "http://tp-spotify.local/callback/" to the Redirect URIs section. + - **NOTE** If you are running more than ThingPulse Spotify Remote in the same WiFi network, you should choose a unique name rather than "tp-spotify". Regardless of what you choose it has to reflect what you set for `SPOTIFY_ESPOTIFIER_NODE_NAME` in `settings.h` in the project. +See [instructions](https://docs.thingpulse.com/guides/esp32-color-kit-grande/#development-environment) if you encounter problems and need Trouble Shooting tips. - +## Tips and Known Issues -7. Don't forget to save your settings. - +To see a list of tips and known issues, see [Tips and Known Issues](./documentation/TipsAndKnownIssues.md). \ No newline at end of file diff --git a/boards/custom_esp-wrover-kit.json b/boards/custom_esp-wrover-kit.json new file mode 100644 index 0000000..b86262e --- /dev/null +++ b/boards/custom_esp-wrover-kit.json @@ -0,0 +1,54 @@ +{ + "_comment": "This is a custom board configuration for an ESP32 with 8MB PSRAM. - Jan 8, 2025.", + "build": { + "arduino":{ + "ldscript": "esp32_out.ld" + }, + "core": "esp32", + "extra_flags": "-DARDUINO_ESP32_DEV", + "f_cpu": "240000000L", + "f_flash": "40000000L", + "flash_mode": "dio", + "hwids": [ + [ + "0x0403", + "0x6010" + ] + ], + "mcu": "esp32", + "variant": "esp32" + }, + "connectivity": [ + "wifi", + "bluetooth", + "ethernet", + "can" + ], + "debug": { + "default_tool": "ftdi", + "onboard_tools": [ + "ftdi" + ], + "openocd_board": "esp32-wrover.cfg" + }, + "frameworks": [ + "arduino", + "espidf" + ], + "name": "Espressif ESP-WROVER-KIT", + "upload": { + "flash_size": "8MB", + "maximum_ram_size": 327680, + "maximum_size": 8388608, + "protocols": [ + "esptool", + "espota", + "ftdi" + ], + "require_upload_port": true, + "speed": 460800 + }, + "url": "https://espressif.com/en/products/hardware/esp-wrover-kit/overview", + "vendor": "Espressif" + } + \ No newline at end of file diff --git a/data/DefaultCoverArt.jpg b/data/DefaultCoverArt.jpg new file mode 100644 index 0000000..82d3a2c Binary files /dev/null and b/data/DefaultCoverArt.jpg differ diff --git a/data/user.ini.template b/data/user.ini.template new file mode 100644 index 0000000..27affc9 --- /dev/null +++ b/data/user.ini.template @@ -0,0 +1,34 @@ +; +; User settings for Spotify Controller. Make sure to Upload Filesystem +; Image after updating. +; +; ---------------------------------------------------------------------- +; Privacy Levels: +; 0 - None (credentials in clear text) +; 1 - Good (encrypted, reusable across devices) +; 2 - Better (encrypted, tied to this device) +; ---------------------------------------------------------------------- +[vault] +privacy_level = 0 +; ---------------------------------------------------------------------- +; WiFi: +; ---------------------------------------------------------------------- +[wifi] +ssid = Your WiFi Name Here +password = Your WiFi Password Here +; ---------------------------------------------------------------------- +; Spotify: +; ---------------------------------------------------------------------- +[spotify] +client_id = Enter your Spotify Client ID here +client_secret = Enter your Spotify Client Secret here +; ---------------------------------------------------------------------- +; System Settings: +; Timezone format - see +; https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv +; For US date and time formatting, include: +; ui_date_time_format = US +; ---------------------------------------------------------------------- +[system] +timezone = CST6CDT,M3.2.0,M11.1.0 +ui_date_time_format = US \ No newline at end of file diff --git a/documentation/SCDesign.drawio b/documentation/SCDesign.drawio new file mode 100644 index 0000000..6f2c2ee --- /dev/null +++ b/documentation/SCDesign.drawio @@ -0,0 +1,311 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/documentation/SCDesign.jpg b/documentation/SCDesign.jpg new file mode 100644 index 0000000..2cc9df1 Binary files /dev/null and b/documentation/SCDesign.jpg differ diff --git a/documentation/TipsAndKnownIssues.md b/documentation/TipsAndKnownIssues.md new file mode 100644 index 0000000..dfb8fe2 --- /dev/null +++ b/documentation/TipsAndKnownIssues.md @@ -0,0 +1,35 @@ +# Tips +- To surpress logging from the SpotifyAruduion library, goto the `SpotifyArduino.h` and commend out the following lines: +``` +#define SPOTIFY_DEBUG 1 + +// Comment out if you want to disable any serial output from this library (also comment out DEBUG and PRINT_JSON_PARSE) +#define SPOTIFY_SERIAL_OUTPUT 1 + +// Prints the JSON received to serial (only use for debugging as it will be slow) +#define SPOTIFY_PRINT_JSON_PARSE 1 +``` + +- If you have a Color Kit Grande with 8 MB or more of Flash memory, you can expand the album art cache from 10 albums to 60 albums by using these settings in `platformio.ini`. See the file for additional comments. +``` +board = custom_esp-wrover-kit +board_build.partitions = partitions/custom_no_ota.csv +``` + +- If you want to update your WiFi and Spotify credentials without modifying the source code, use the optional `user.ini` file. The file is ignored by Git and does not require changing code. See [full user settings documentation](./UserSettings.md) for details. + +# Known Issues +- Some capabilities require a Spotify Premium subscription and may not work fully on the ad supported tier. https://developer.spotify.com/documentation/web-playback-sdk + +- At startup if nothing is playing the following may be logged repeatedly: +`20:13:34.148 > [ 15318][E][ssl_client.cpp:37] _handle_error(): [data_to_read():361]: (-76) UNKNOWN ERROR CODE (004C)` +When there isn't currently an active device or a device has been stopped for a period of time, playback controls may not work and these errors may be seen in the logs. Once music is started/resumed on the active device the errors will go away. + +- When playback operations are performed, the SpotifyArdiuno library may log the following. It doesn't appear to affect the operation from actually working. +``` +23:24:36.283 > [ 67244][V][ssl_client.cpp:369] send_ssl_data(): Writing HTTP request with 0 bytes... +23:24:36.440 > [ 67407][V][ssl_client.cpp:381] send_ssl_data(): Handling error -80 +23:24:36.446 > [ 67408][E][ssl_client.cpp:37] _handle_error(): [send_ssl_data():382]: (-80) UNKNOWN ERROR CODE (0050) +23:24:36.452 > [ 67412][V][ssl_client.cpp:321] stop_ssl_socket(): Cleaning SSL connection. +23:24:36.457 > Failed to send request +``` diff --git a/documentation/UserSettings.md b/documentation/UserSettings.md new file mode 100644 index 0000000..8496435 --- /dev/null +++ b/documentation/UserSettings.md @@ -0,0 +1,122 @@ +# Color Kit Grande Spotify Controller User Settings + +In order for this project to function, it needs to be able to connect to a WiFi network and authenticate with Spotify. This document explains how to configure it to do these things. + +These values must be set before the device can connect to your network or Spotify account. + +--- + +## Configuration Options + +You can configure the settings using one of the following methods: + +### 1. **Edit the `settings.h` file (Quick Start)** + +Open the `src/settings.h` file and look for the **User Settings** section at the top. This contains the fields for Wi-Fi and Spotify credentials, as well as your timezone. + +This is the fastest way to get started. However, values in `settings.h` will be compiled into the firmware and are not encrypted. They will also be tracked by git unless you modify `.gitignore`. If your system backs up files and/or you copy the files to network locations, the credentials may be visible in clear text by others. To avoid these limitations, this project uses an optional `user.ini` file discussed in the next section. + +### 2. **Use a `user.ini` File (Recommended for Privacy and Flexibility)** + +A `user.ini.template` file is provided in the `/data` folder. You can copy it and rename the copy to `user.ini`, then update the values inside. This file will be uploaded to the device's filesystem using the PlatformIO “Upload Filesystem Image” task. It has already been added to `.gitignore` and will not be tracked by git by default. **NOTE: If `user.ini` is found and loaded, the conflicting values in `settings.h` will be ignored and not used.** + +The template includes guidance on privacy levels and where to enter your credentials. + +--- + +## Required Settings + +| Section | Key | Description | +|-----------|------------------|---------------------------------------------------------------------------| +| `[vault]` | `privacy_level` | `0` = None (plaintext), `1` = Good (encrypted, portable), `2` = Better (encrypted, device-tied) | +| `[wifi]` | `ssid` | Your Wi-Fi network name | +| | `password` | Your Wi-Fi password | +| `[spotify]` | `client_id` | Your Spotify Developer App Client ID | +| | `client_secret` | Your Spotify Developer App Client Secret | +| `[system]` | `timezone` | POSIX timezone format string (see example or link below) | +| | `ui_date_time_format` | Optional: `US` for 12-hour clock and month-first date format; omit or use other values for international formatting | + +> 🔗 Refer to the [POSIX timezone list](https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv) for formatting guidance. + + +## Understanding Privacy Levels + +The `privacy_level` setting controls how credentials are stored and handled by the device. It directly affects whether the values in your `user.ini` file are used in plaintext or must be encrypted before use. + +### Level 0 – None (plaintext) +- Credentials in `user.ini` are stored and used in clear text. +- This is the easiest option for beginners and debugging. +- **At startup, the device will print encrypted alternatives to the serial console, which can be copy-pasted into the file if you choose to increase privacy later.** + +```text +00:30:06.427 > ====================================================================== +00:30:06.432 > Paste these into your user.ini file to enable encrypted credentials. +00:30:06.438 > ====================================================================== +00:30:06.443 > +00:30:06.443 > ; Values to use for Good Privacy (encrypted, reusable across devices): +00:30:06.449 > privacy_level = 1 +00:30:06.449 > ssid = kfHankF998h4coKUwRMj6w== +00:30:06.454 > password = sFnXUDMIsmci6KV5mlh2rw== +00:30:06.454 > client_id = NMwdXOJ8JNvkekePuwow53nHnlcbJqNKZuQSnASx/DuJBxChGn+u7rVSwVf0sf34 +00:30:06.466 > client_secret = NMwdXOJ8JNvkekePuwow53nHnlcbJqNKZuQSnASx/DuJBxChGn+u7rVSwVf0sf34 +00:30:06.471 > +00:30:06.471 > ; Values to use for Better Privacy (encrypted, tied to this device): +00:30:06.476 > privacy_level = 2 +00:30:06.476 > ssid = bzvfcf5V5qpzfd251wqlDQ== +00:30:06.482 > password = /PybaE2diyJwdlH4kZC8bQ== +00:30:06.482 > client_id = yub/jzKomMIza3IP8OAKDB04QHNvqLLtDfwWTcy/WpIg8PahhpE5gXVMYO65677j +00:30:06.493 > client_secret = yub/jzKomMIza3IP8OAKDB04QHNvqLLtDfwWTcy/WpIg8PahhpE5gXVMYO65677j +``` + +### Level 1 – Good (encrypted, reusable across devices) +- Encrypted credentials are required in `user.ini`. +- Encryption uses AES-128 in ECB mode with PKCS#7 padding. +- A static internal base key is used to encrypt and decrypt values. +- Encrypted values can be reused on any compatible device. + +### Level 2 – Better (encrypted, tied to device) +- Same encryption method as Level 1, but the AES key is derived from both the internal base key and the device's MAC address. +- Encrypted values can only be decrypted by the device that created them. +- Provides stronger security, but values must be regenerated for each device. + +Good and Better levels support an optional `key_salt` setting in the `[vault]` section to increase key complexity and further personalize the encryption key derivation. + +--- + +## Example `user.ini` + +```ini +; +; User settings for Spotify Controller. Make sure to Upload Filesystem +; Image after updating. +; +; ---------------------------------------------------------------------- +; Privacy Levels: +; 0 - None (credentials in clear text) +; 1 - Good (encrypted, reusable across devices) +; 2 - Better (encrypted, tied to this device) +; ---------------------------------------------------------------------- +[vault] +privacy_level = 0 +; ---------------------------------------------------------------------- +; WiFi: +; ---------------------------------------------------------------------- +[wifi] +ssid = MyNetworkName +password = MyPassword123 +; ---------------------------------------------------------------------- +; Spotify: +; ---------------------------------------------------------------------- +[spotify] +client_id = abcdefghijklmnopqrstuv1234567890 +client_secret = abcdefghijklmnopqrstuv1234567890 +; ---------------------------------------------------------------------- +; System Settings: +; Timezone format - see +; https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv +; For US date and time formatting, include: +; ui_date_time_format = US +; ---------------------------------------------------------------------- +[system] +timezone = CST6CDT,M3.2.0,M11.1.0 +ui_date_time_format = US \ No newline at end of file diff --git a/generate_compile_time.py b/generate_compile_time.py new file mode 100644 index 0000000..6510713 --- /dev/null +++ b/generate_compile_time.py @@ -0,0 +1,61 @@ +import datetime +""" +======================================================================== +File: generate_compile_time.py + +Description: + This script generates a compile-time timestamp and writes it to a + header file (`compile_time.h`). The timestamp is formatted in + 12-hour time with an AM/PM indicator. + +Usage in PlatformIO: + - This script is executed as a pre-build step in PlatformIO. + - It is referenced in the `platformio.ini` file under `extra_scripts`: + ``` + extra_scripts = pre:generate_compile_time.py + ``` + - This ensures that every build updates the compile timestamp. + +Generated Output: + - The script writes a `#define` statement to `src/compile_time.h`: + ``` + #define SC_COMPILE_TIME "MM-DD-YYYY HH:MM:SS AM/PM" + ``` + - Example output: + ``` + #define SC_COMPILE_TIME "03-02-2025 12:15:00 AM" + ``` + +How to Use in Code: + - Include the generated `compile_time.h` in your source files: + ```c + #include "compile_time.h" + ``` + - Print or log the compile time: + ```c + Serial.println(SC_COMPILE_TIME); + ``` + +Notes: + - The timestamp is dynamically updated at each build. + - The script must be located in the root project directory for + PlatformIO to execute it correctly. +======================================================================== +""" + +# Generate the current timestamp +compile_time = datetime.datetime.now().strftime("%m-%d-%Y %I:%M:%S %p") + +# Write it to a header file with a warning comment +with open("src/compile_time.h", "w") as f: + f.write("""/* + * DO NOT EDIT THIS FILE MANUALLY. + * This file is automatically generated by generate_compile_time.py + * and will be overwritten during each build. + * + * SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com + * SPDX-License-Identifier: MIT + */ + +""") + f.write(f'#define SC_COMPILE_TIME "{compile_time}"\n') \ No newline at end of file diff --git a/images/ClockView.jpg b/images/ClockView.jpg new file mode 100644 index 0000000..3401aa1 Binary files /dev/null and b/images/ClockView.jpg differ diff --git a/images/CoverArtView.jpg b/images/CoverArtView.jpg new file mode 100644 index 0000000..7c5aa02 Binary files /dev/null and b/images/CoverArtView.jpg differ diff --git a/images/DiagnosticView.jpg b/images/DiagnosticView.jpg new file mode 100644 index 0000000..d1a6c57 Binary files /dev/null and b/images/DiagnosticView.jpg differ diff --git a/images/HomeView.jpg b/images/HomeView.jpg new file mode 100644 index 0000000..3be2320 Binary files /dev/null and b/images/HomeView.jpg differ diff --git a/images/SpotifyAppSettings.png b/images/SpotifyAppSettings.png deleted file mode 100644 index 3ad3c08..0000000 Binary files a/images/SpotifyAppSettings.png and /dev/null differ diff --git a/images/SpotifyAppSettingsSave.png b/images/SpotifyAppSettingsSave.png deleted file mode 100644 index fb32fa7..0000000 Binary files a/images/SpotifyAppSettingsSave.png and /dev/null differ diff --git a/images/SpotifyAppSignUp1.png b/images/SpotifyAppSignUp1.png deleted file mode 100644 index 53f589d..0000000 Binary files a/images/SpotifyAppSignUp1.png and /dev/null differ diff --git a/images/SpotifyClientId.png b/images/SpotifyClientId.png new file mode 100644 index 0000000..fad73e1 Binary files /dev/null and b/images/SpotifyClientId.png differ diff --git a/images/SpotifyClientId.png_original b/images/SpotifyClientId.png_original new file mode 100644 index 0000000..905a7f5 Binary files /dev/null and b/images/SpotifyClientId.png_original differ diff --git a/images/SpotifyConnectScreen.png b/images/SpotifyConnectScreen.png deleted file mode 100644 index 263a2ca..0000000 Binary files a/images/SpotifyConnectScreen.png and /dev/null differ diff --git a/images/SpotifyCreateApp.png b/images/SpotifyCreateApp.png new file mode 100644 index 0000000..f56921f Binary files /dev/null and b/images/SpotifyCreateApp.png differ diff --git a/images/SpotifyCredentials.png b/images/SpotifyCredentials.png deleted file mode 100644 index f13f6ac..0000000 Binary files a/images/SpotifyCredentials.png and /dev/null differ diff --git a/images/SpotifyDashboard.png b/images/SpotifyDashboard.png index 2443966..93b7ff5 100644 Binary files a/images/SpotifyDashboard.png and b/images/SpotifyDashboard.png differ diff --git a/images/SpotifyppSignUp3.png b/images/SpotifyppSignUp3.png deleted file mode 100644 index 044102f..0000000 Binary files a/images/SpotifyppSignUp3.png and /dev/null differ diff --git a/images/UserSettings.png b/images/UserSettings.png new file mode 100644 index 0000000..973b315 Binary files /dev/null and b/images/UserSettings.png differ diff --git a/images/platformio-filesystem.png b/images/platformio-filesystem.png new file mode 100644 index 0000000..0f49121 Binary files /dev/null and b/images/platformio-filesystem.png differ diff --git a/images/platformio-task-upload.png b/images/platformio-task-upload.png new file mode 100644 index 0000000..7a52ce2 Binary files /dev/null and b/images/platformio-task-upload.png differ diff --git a/partitions/custom_no_ota.csv b/partitions/custom_no_ota.csv new file mode 100644 index 0000000..1a56b99 --- /dev/null +++ b/partitions/custom_no_ota.csv @@ -0,0 +1,17 @@ +# This is a custom partition table for this ESP32 project. +# The custom partition is used to get access to all 8 MB +# of the available flash memory. +# Created 1/8/2025 +# +# Name, Type, SubType, Offset, Size, Flags +# Name, Type, SubType, Offset, Size, Flags +# Non-volatile storage +nvs, data, nvs, 0x9000, 0x5000, +# OTA metadata (not used in no_ota) +otadata, data, ota, 0xe000, 0x2000, +# Application (4MB) +app0, app, factory, 0x10000, 0x400000, +# Filesystem (3.8125MB) +spiffs, data, spiffs, 0x410000,0x3D0000, +# Core dump (128KB) +coredump, data, coredump,0x7E0000,0x20000, \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index b1b8041..1367ffc 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1,58 +1,60 @@ -; PlatformIO Project Configuration File for ThingPulse Color Kit Grande +; PlatformIO Project Configuration File ; -; Documentation: https://docs.thingpulse.com/guides/esp32-color-kit-grande/ +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting ; -; Additional PlatformIO options and examples: https://docs.platformio.org/page/projectconf.html +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html [env:thingpulse-color-kit-grande] -platform = espressif32@~6.4.0 +platform = espressif32@~6.10.0 +; esp-wrover-kit is 4MB ESP32-WROVER-B compatible. board = esp-wrover-kit +; custom_esp-wrover-kit to enable 8MB flash and 60 album art cache +;board = custom_esp-wrover-kit framework = arduino -; Adjust port and speed to your system and its capabilities e.g. "upload_port = COM3" on Windows. -; To list all availble ports you may also run 'pio device list' in the Visual Studio Code terminal window. -; In most cases you should be able to leave this commented out and thus rely on the auto-detect mode. -; upload_port = /dev/tty.wchusbserial54790238451 -; monitor_port = /dev/tty.wchusbserial54790238451 monitor_speed = 115200 -; For your OS & driver combination you might have to lower this to 921600 or even 460800. upload_speed = 1500000 -monitor_filters = esp32_exception_decoder, time -build_flags = - ; core flags - -DCORE_DEBUG_LEVEL=5 - -DBOARD_HAS_PSRAM - -mfix-esp32-psram-cache-issue - ; TFT_eSPI flags - ; Below we replicate the flags from TFT_eSPI/User_Setups/Setup21_ILI9488.h. - ; You can't mix'n match from their .h and -D here. - -D USER_SETUP_LOADED=1 # 1 => will not load User_Setup.h from TFT_eSPI but rely on the flags defined here - -D ILI9488_DRIVER=1 - -D TFT_MISO=19 - -D TFT_MOSI=18 - -D TFT_SCLK=05 - -D TFT_CS=15 - -D TFT_DC=2 - -D TFT_RST=4 - -D TFT_BL=32 - ; As we're using OpenFontRender we don't need any of the TFT_eSPI built-in fonts. - ; Font descriptions at TFT_eSPI/User_Setups/Setup21_ILI9488.h - -D LOAD_GLCD=0 - -D LOAD_FONT2=0 - -D LOAD_FONT4=0 - -D LOAD_FONT6=0 - -D LOAD_FONT7=0 - -D LOAD_FONT8=0 - -D LOAD_GFXFF=0 - -D SMOOTH_FONT=1 - -D SPI_FREQUENCY=27000000 - ; required if you include OpenFontRender and build on macOS - -I /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/** +monitor_filters = + esp32_exception_decoder + time +build_unflags = -std=gnu++11 ; Enable C++ 14 added 1/25/2025 +build_flags = + -std=gnu++14 ; Enable C++ 14 to pick up std::make_unique added 1/25/2025 + -DCORE_DEBUG_LEVEL=3 ; was 5 1/2/2025 + -DBOARD_HAS_PSRAM + -mfix-esp32-psram-cache-issue + -D USER_SETUP_LOADED=1 + -D ILI9488_DRIVER=1 + -D TFT_MISO=19 + -D TFT_MOSI=18 + -D TFT_SCLK=05 + -D TFT_CS=15 + -D TFT_DC=2 + -D TFT_RST=4 + -D TFT_BL=32 + -D LOAD_GLCD=0 + -D LOAD_FONT2=0 + -D LOAD_FONT4=0 + -D LOAD_FONT6=0 + -D LOAD_FONT7=0 + -D LOAD_FONT8=0 + -D LOAD_GFXFF=0 + -D SMOOTH_FONT=1 + -D SPI_FREQUENCY=27000000 + -I /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/** +extra_scripts = pre:generate_compile_time.py +; no_ota.csv is 4MB ESP32-WROVER-B compatible. limits art cache to 10 albums. board_build.partitions = no_ota.csv +; custom_no_ota.csv is 8MB Flash ESP32-WROVER-E compatible. art cache supports 60 albums. +; board_build.partitions = partitions/custom_no_ota.csv board_build.filesystem = littlefs -lib_deps = - arkhipenko/TaskScheduler@~3.7.0 - bodmer/TFT_eSPI@~2.5.30 - bodmer/TJpg_Decoder@~1.0.8 - https://github.com/Bodmer/OpenFontRender#f163cc6 ; no tags or releases to reference :( -> pin to Git revision - bblanchon/ArduinoJson@~6.21.3 - https://github.com/witnessmenow/spotify-api-arduino#899502c ; https://github.com/witnessmenow/spotify-api-arduino/issues/61 +lib_deps = + arkhipenko/TaskScheduler @ ^3.8.5 + bodmer/TFT_eSPI @ ^2.5.43 + bodmer/TJpg_Decoder @ ^1.1.0 + bblanchon/ArduinoJson @ ^7.2.1 + https://github.com/Bodmer/OpenFontRender#5e0ec22eef4bc93f963360438a9eb8e5407ef1c4 + https://github.com/witnessmenow/spotify-api-arduino#62612780f40521e9833353eeefcaa0c5d16a97a5 diff --git a/src/DisplayUI.cpp b/src/DisplayUI.cpp new file mode 100644 index 0000000..f803b48 --- /dev/null +++ b/src/DisplayUI.cpp @@ -0,0 +1,1873 @@ +/*------------------------------------------------------------------- +** +** DisplayUI.cpp +** +** Provides methods for drawing UI elements including album art, buttons, +** progress indicators, and text, using the TFT_eSPI and OpenFontRender libraries. +** +** Originally based on https://github.com/Bodmer/OpenWeather/blob/main/examples/TFT_eSPI_OpenWeather_LittleFS/GfxUi.h +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2024-12-26 - Electric Diversions - Renamed from GfxUI.h.h to tpGfxUi.h +** 2024-12-27 - Electric Diversions - Renamed to DisplayUI and promoted to base class +** ------------------------------------------------------------------------------------------------ +** +*/ + +/* +** ------------------------------------------------------------------------------------------------ +** +** NOTE: DMA (Direct Memory Access) is *not* enabled for ILI9488. +** +** The ILI9488 display controller requires 24-bit (RGB888) color +** over SPI, while the ESP32's SPI DMA engine only supports 8-bit +** and 16-bit transfers. Because of this, the TFT_eSPI library +** disables DMA support for ILI9488 by default. +** +** Enabling DMA would require additional processing to convert +** 16-bit (RGB565) color data into the 24-bit format, eliminating +** any potential performance benefits. It could also introduce +** graphical artifacts or instability. +** +** For this project, standard SPI transfers are used instead. +** +** 02/15/2025 - Documented DMA decision +** +** ------------------------------------------------------------------------------------------------ +*/ + +#include // for fmod, fabs +#include // for std::min, std::max +#include + +#include "DisplayUI.h" +#include "settings.h" +#include "logTags.h" +#include "SCLogger.h" +#include "Monitor.h" +#include "ThingPulse/util.h" + +#define FS_TP_LOGO "/ThingPulse-logo-260.jpeg" + +/* +** =================================================================== +** toValue() +** Helper function to convert TFTColor to standard color value +** =================================================================== +*/ +constexpr uint16_t toValue(TFTColor color) +{ + return static_cast(color); +} + +/* +** =================================================================== +** toRGB565() +** Converts a color from RGB565 (16-bit) or RGB888 (24-bit) to +** 16-bit RGB565 format. +** +** Parameters: +** color - Can be either: +** - A 16-bit RGB565 color (TFTColor enum) +** - A 24-bit RGB888 color (0xRRGGBB) +** +** Returns: +** A 16-bit RGB565 color. +** +** Notes: +** - If the input is already RGB565 (16-bit), it is returned as-is. +** - If the input is RGB888 (24-bit), it is converted to RGB565. +** - This function is useful for converting colors for TFT displays. +** +** Example Usage: +** uint16_t red565 = toRGB565(TFTColor::Red); +** uint16_t customBlue = toRGB565(0x0067B0); // Custom RGB888 color +** +** _ofr->cdrawString("RED", posX, posY, red565, toRGB565(TFTColor::Black)); +** =================================================================== +*/ +uint16_t toRGB565(TFTColor tftColor) +{ + + // Convert to unsigned int + uint32_t color = toValue(tftColor); + + // If color is 16-bit (RGB565), return it as-is + if (color <= 0xFFFF) { + return static_cast(color); + } + + // If color is 24-bit (RGB888), convert it to RGB565 + uint8_t r = (color >> 16) & 0xFF; // Extract Red (8-bit) + uint8_t g = (color >> 8) & 0xFF; // Extract Green (8-bit) + uint8_t b = color & 0xFF; // Extract Blue (8-bit) + + // Convert 8-bit RGB888 to 5-6-5 RGB565 format + return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3); +} + +// TODO: Standardize singleton instantiation across the project. +static DisplayUI *pInstance = nullptr; + +/* +** =================================================================== +** DisplayUI() Constructor +** =================================================================== +*/ +DisplayUI::DisplayUI(TFT_eSPI *tft, OpenFontRender *ofr, OpenFontRender *clockFont) +{ + _tft = tft; + _ofr = ofr; + _clockFont = clockFont; + + TJpgDec.setJpgScale(1); + TJpgDec.setCallback(DisplayUI::jpgCallback); +} + +/* +** =================================================================== +** init() +** Initialize the singleton. +** =================================================================== +*/ +void DisplayUI::init() +{ + pInstance = this; +} + +/* +** =================================================================== +** isUIDirty() +** Determines if the UI needs to be updated. Returns true if the +** UI is marked as dirty, false otherwise. +** =================================================================== +*/ +bool DisplayUI::isUIDirty() +{ + bool answer = false; + + if (xSemaphoreTake(xSemaphoreDisplay, portMAX_DELAY)) + { + answer = _isUIDirty; + xSemaphoreGive(xSemaphoreDisplay); + } + else + { + spLogI(LOGTAG_MULTITASK,"DisplayUI::isUIDirty - Unable to take xSemaphoreDisplay."); + } + + return answer; +} + +/* +** =================================================================== +** markUIDirty() +** Marks the UI as dirty or not dirty, based on the value of +** the isDirty parameter. +** =================================================================== +*/ +void DisplayUI::markUIDirty(bool isDirty) +{ + if (xSemaphoreTake(xSemaphoreDisplay, portMAX_DELAY)) + { + _isUIDirty = isDirty; + xSemaphoreGive(xSemaphoreDisplay); + } + else + { + spLogI(LOGTAG_MULTITASK,"DisplayUI::markUIDirty - Unable to take xSemaphoreDisplay."); + } +} + +/* +** =================================================================== +** getCenterWidth() +** =================================================================== +*/ +int16_t DisplayUI::getCenterWidth() +{ + return _tft->width() / 2; +} + +/* +** =================================================================== +** getCenterHeight() +** =================================================================== +*/ +int16_t DisplayUI::getCenterHeight() +{ + return _tft->height() / 2; +} + +/* +** =================================================================== +** getWidth() +** =================================================================== +*/ +int16_t DisplayUI::getWidth() +{ + return _tft->width(); +} + +/* +** =================================================================== +** getHeight() +** =================================================================== +*/ +int16_t DisplayUI::getHeight() +{ + return _tft->height(); +} + +/* +** =================================================================== +** drawBmp() +** Renders a 24-bit BMP image from the LittleFS filesystem onto +** the display at the specified (x, y) position. Optimized for +** performance using a no-seek buffer and direct line decoding. +** +** Parameters: +** filename - Path to the BMP file in the LittleFS filesystem. +** x - X-coordinate for the top-left corner of the image. +** y - Y-coordinate for the top-left corner of the image. +** +** Notes: +** - Only uncompressed 24-bit BMP files are supported. +** - Image is drawn bottom-up as per BMP specification. +** - Invalid formats or missing files will generate log errors. +** +** Example Usage: +** drawBmp("/splash.bmp", 10, 20); +** =================================================================== +*/ + +// Bodmer's streamlined x2 faster "no seek" version +void DisplayUI::drawBmp(String filename, uint16_t x, uint16_t y) { + + if ((x >= _tft->width()) || (y >= _tft->height())) + return; + + fs::File bmpFS; + + // Note: ESP32 passes "open" test even if file does not exist, whereas ESP8266 + // returns NULL + if (!LittleFS.exists(filename)) { + spLogE(LOGTAG_GUI, " File not found"); + return; + } + + // Open requested file + bmpFS = LittleFS.open(filename, "r"); + + uint32_t seekOffset; + uint16_t w, h, row; + uint8_t r, g, b; + bool oldSwap = false; + + if (read16(bmpFS) == 0x4D42) { + read32(bmpFS); + read32(bmpFS); + seekOffset = read32(bmpFS); + read32(bmpFS); + w = read32(bmpFS); + h = read32(bmpFS); + + if ((read16(bmpFS) == 1) && (read16(bmpFS) == 24) && (read32(bmpFS) == 0)) { + y += h - 1; + + oldSwap = _tft->getSwapBytes(); + _tft->setSwapBytes(true); + bmpFS.seek(seekOffset); + + // Calculate padding to avoid seek + uint16_t padding = (4 - ((w * 3) & 3)) & 3; + uint8_t lineBuffer[w * 3 + padding]; + + for (row = 0; row < h; row++) { + + bmpFS.read(lineBuffer, sizeof(lineBuffer)); + uint8_t *bptr = lineBuffer; + uint16_t *tptr = (uint16_t *)lineBuffer; + // Convert 24 to 16 bit colours using the same line buffer for results + for (uint16_t col = 0; col < w; col++) { + b = *bptr++; + g = *bptr++; + r = *bptr++; + *tptr++ = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3); + } + + // Push the pixel row to screen, pushImage will crop the line if needed + // y is decremented as the BMP image is drawn bottom up + _tft->pushImage(x, y--, w, 1, (uint16_t *)lineBuffer); + } + } else + spLogE(LOGTAG_GUI, "BMP format not recognized."); + } + _tft->setSwapBytes(oldSwap); + bmpFS.close(); +} + +/* +** =================================================================== +** drawLogo() +** =================================================================== +*/ +void DisplayUI::drawLogo() { + if (LittleFS.exists(FS_TP_LOGO)) { + uint16_t w = 0, h = 0; + TJpgDec.getFsJpgSize(&w, &h, FS_TP_LOGO, LittleFS); + TJpgDec.drawFsJpg((_tft->width() - w) / 2, 30, FS_TP_LOGO, LittleFS); + } +} + +/* +** =================================================================== +** drawAppInfo() +** =================================================================== +*/ +void DisplayUI::drawAppInfo() { + _ofr->setFontSize(16); + _ofr->cdrawString(APP_NAME, getCenterWidth(), _tft->height() - 50); + _ofr->cdrawString(VERSION, getCenterWidth(), _tft->height() - 30); +} + +/* +** =================================================================== +** drawAlbumArt() +** =================================================================== +*/ +void DisplayUI::drawAlbumArt(int32_t x, int32_t y, String filename) +{ + spLogI(LOGTAG_GUI, "Entering drawAlbumArt. ttf width is %d. filename is %s.", _tft->width(), filename.c_str()); + + if (LittleFS.exists(filename)) + { + uint16_t w = 0, h = 0; + if (xSemaphoreTake(xSemaphoreDisplay, portMAX_DELAY)) + { + TJpgDec.getFsJpgSize(&w, &h, filename, LittleFS); + spLogI(LOGTAG_GUI, "After getFsJpgSize(). ttf width is %d. w is %d. filename is %s.", _tft->width(), w, filename.c_str()); + xSemaphoreGive(xSemaphoreDisplay); + } + else + { + spLogI(LOGTAG_MULTITASK,"Unable to take xSemaphoreDisplay."); + } + + Monitor::start(MONITOR_ID_SPOTIFY_IMAGE_FILE_LOAD, LOGTAG_METRICS, "drawFsJpg(...)"); + drawFsJpg(x, y, filename.c_str(), LittleFS); + + Monitor::stop(MONITOR_ID_SPOTIFY_IMAGE_FILE_LOAD); + + } + +} + +/* +** =================================================================== +** drawProgressBar() +** =================================================================== +*/ +void DisplayUI::drawProgressBar(uint16_t x0, uint16_t y0, uint16_t w, uint16_t h, + uint8_t percentage, TFTColor frameColor, + TFTColor barColor) +{ + if (percentage == 0) + { + _tft->fillRoundRect(x0, y0, w, h, 3, TFT_BLACK); + } + uint8_t margin = 2; + uint16_t barHeight = h - 2 * margin; + uint16_t barWidth = w - 2 * margin; + + _tft->drawRoundRect(x0, y0, w, h, 3, toValue(frameColor)); + + _tft->fillRect(x0 + margin, y0 + margin, barWidth * percentage / 100.0, + barHeight, toValue(barColor)); +} + +/* +** =================================================================== +** drawStatusBox() +** This call should be thread safe. +** =================================================================== +*/ +void DisplayUI::drawStatusBox(TFTColor statusColor) +{ + uint8_t margin = 2; + int32_t boxX = 450; //460; //10; + int32_t boxY = 290; //300; + + if (xSemaphoreTake(xSemaphoreDisplay, portMAX_DELAY)) + { + _tft->drawRoundRect(boxX, boxY, 20, 20, 3, toValue(TFTColor::DarkGrey)); + _tft->fillRect(boxX + margin, boxY + margin, 16, 16, toValue(statusColor)); + xSemaphoreGive(xSemaphoreDisplay); + } + else + { + spLogI(LOGTAG_MULTITASK,"Unable to take xSemaphoreDisplay in drawStatusBox()."); + } + } + +/* +** =================================================================== +** drawDownloadIndicator() +** This call should be thread safe. +** =================================================================== +*/ +void DisplayUI::drawDownloadIndicator(bool bDownloadComplete) +{ + uint8_t margin = 2; + int32_t boxX = 425; //460; //10; + int32_t boxY = 290; //300; + + TFTColor lineColor = TFTColor::Black; + TFTColor fillColor = TFTColor::Black; + + if (!isSplitBackground()) + { + lineColor = getBackground(); + fillColor = getBackground(); + } + + if (!bDownloadComplete) + { + lineColor = TFTColor::White; + fillColor = TFTColor::SkyBlue; + } + + if (xSemaphoreTake(xSemaphoreDisplay, portMAX_DELAY)) + { + _tft->drawRoundRect(boxX, boxY, 20, 20, 3, toValue(lineColor)); + _tft->fillRect(boxX + margin, boxY + margin, 16, 16, toValue(fillColor)); + xSemaphoreGive(xSemaphoreDisplay); + } + else + { + spLogI(LOGTAG_MULTITASK,"Unable to take xSemaphoreDisplay in drawStatusBox()."); + } + } + +/* +** =================================================================== +** drawButton() +** This call should be thread safe. +** =================================================================== +*/ +void DisplayUI::drawButton(int32_t x, int32_t y, bool isPressed) +{ + const uint8_t margin = 2; + const int32_t width = 90; + const int32_t height = 90; + + drawBlankButton(x,y,width,height,margin,TFTColor::White,isPressed); + + if (xSemaphoreTake(xSemaphoreDisplay, portMAX_DELAY)) + { + // Draw rewind symbol (|<) + const int32_t centerX = x + width / 2; + const int32_t centerY = y + height / 2; + const int32_t triangleSize = 20; + const int32_t lineThickness = 5; // Adjust thickness of vertical line + const int32_t lineX = centerX - 20; // Shifted left for proper placement + const int32_t triangleX = lineX + lineThickness + 20;//2; // Right next to the line + const int32_t triangleY = centerY - 15; + const TFTColor symbolColor = TFTColor::White; + + // Draw vertical line '|' using a filled rectangle for thickness + _tft->fillRect(lineX, centerY - 15, lineThickness, 30, toValue(symbolColor)); + + // Draw left-facing triangle '<' + _tft->fillTriangle( + triangleX + 15, triangleY, // Top point + triangleX + 15, triangleY + 30, // Bottom point + triangleX - triangleSize, centerY, // Leftmost point + toValue(symbolColor) + ); + + xSemaphoreGive(xSemaphoreDisplay); + } + else + { + spLogI(LOGTAG_MULTITASK,"Unable to take xSemaphoreDisplay in drawStatusBox()."); + } + } + + +/* +** =================================================================== +** drawBlankButton() +** Draws a blank button with optional pressed effect and specified border color. +** Thread-safe via xSemaphoreDisplay. +** =================================================================== +*/ +void DisplayUI::drawBlankButton(int32_t x, int32_t y, int32_t width, int32_t height, uint8_t margin, TFTColor borderColor, bool isPressed) +{ + + TFTColor bodyColor = getBackground(); + // Invert the color + TFTColor pressColor = static_cast(~static_cast(bodyColor)); + + if (xSemaphoreTake(xSemaphoreDisplay, portMAX_DELAY)) + { + _tft->drawRoundRect(x, y, width, height, 3, toValue(borderColor)); + + if (isPressed) + { + _tft->fillRect(x + margin, y + margin, width - margin * 2, height - margin * 2, toValue(pressColor)); + } + else + { + _tft->fillRect(x + margin, y + margin, width - margin * 2, height - margin * 2, toValue(bodyColor)); + } + + xSemaphoreGive(xSemaphoreDisplay); + } + else + { + spLogI(LOGTAG_MULTITASK,"Unable to take xSemaphoreDisplay in drawStatusBox()."); + } +} + +/* +** =================================================================== +** drawSkipTrackIcon() +** Draws a skip track icon. Direction is determined by isReversed flag. +** Thread-safe via xSemaphoreDisplay. +** =================================================================== +*/ +void DisplayUI::drawSkipTrackIcon(int32_t x, int32_t y, int32_t width, int32_t height, bool isReversed) +{ + if (xSemaphoreTake(xSemaphoreDisplay, portMAX_DELAY)) + { + // Define basic geometry + const int32_t centerX = x + width / 2; + const int32_t centerY = y + height / 2; + const int32_t triangleSize = 20; + const int32_t lineThickness = 5; // Thickness of the vertical line + const int32_t offsetX = 20; // Offset to adjust positioning + + // Determine positions based on isReversed + const int32_t lineX = isReversed ? centerX - offsetX : centerX + offsetX - lineThickness; + const int32_t triangleX = isReversed ? (lineX + lineThickness + 20) : (lineX - 20); + const int32_t triangleY = centerY - 15; + const TFTColor symbolColor = TFTColor::White; + + // Draw vertical line '|' + _tft->fillRect(lineX, centerY - 15, lineThickness, 30, toValue(symbolColor)); + + // Draw triangle '<' or '>' + if (isReversed) + { + // Left-facing triangle '<' + _tft->fillTriangle( + triangleX + 15, triangleY, // Top point + triangleX + 15, triangleY + 30, // Bottom point + triangleX - triangleSize, centerY, // Leftmost point + toValue(symbolColor) + ); + } + else + { + // Right-facing triangle '>' + _tft->fillTriangle( + triangleX - 15, triangleY, // Top point + triangleX - 15, triangleY + 30, // Bottom point + triangleX + triangleSize, centerY, // Rightmost point + toValue(symbolColor) + ); + } + + xSemaphoreGive(xSemaphoreDisplay); + } + else + { + spLogI(LOGTAG_MULTITASK,"Unable to take xSemaphoreDisplay in drawSkipTrackIcon()."); + } +} + +/* +** =================================================================== +** drawPlayTrackIcon() +** Draws a right-facing triangle icon for the "play" action. +** =================================================================== +*/ +void DisplayUI::drawPlayTrackIcon(int32_t x, int32_t y, int32_t width, int32_t height) +{ + if (xSemaphoreTake(xSemaphoreDisplay, portMAX_DELAY)) + { + // Define basic geometry + const int32_t centerX = x + width / 2; + const int32_t centerY = y + height / 2; + const int32_t triangleSize = 20; + const int32_t lineThickness = 0; // Thickness of the vertical line + const int32_t offsetX = 20; // Offset to adjust positioning + + // Determine positions based on isReversed + const int32_t lineX = centerX + offsetX - lineThickness; + const int32_t triangleX = lineX - 20; + const int32_t triangleY = centerY - 15; + const TFTColor symbolColor = TFTColor::White; + + // Right-facing triangle '>' + _tft->fillTriangle( + triangleX - 15, triangleY, // Top point + triangleX - 15, triangleY + 30, // Bottom point + triangleX + triangleSize, centerY, // Rightmost point + toValue(symbolColor) + ); + + xSemaphoreGive(xSemaphoreDisplay); + } + else + { + spLogI(LOGTAG_MULTITASK,"Unable to take xSemaphoreDisplay in drawSkipTrackIcon()."); + } +} + +/* +** =================================================================== +** drawPauseTrackIcon() +** Draws two vertical bars to represent the "pause" icon. +** =================================================================== +*/ +void DisplayUI::drawPauseTrackIcon(int32_t x, int32_t y, int32_t width, int32_t height) +{ + if (xSemaphoreTake(xSemaphoreDisplay, portMAX_DELAY)) + { + // Define basic geometry + const int32_t centerX = x + width / 2; + const int32_t centerY = y + height / 2; + const int32_t lineThickness = 5; // Thickness of the vertical line + const TFTColor symbolColor = TFTColor::White; + + // Draw vertical line '|' + _tft->fillRect(centerX - lineThickness * 2, centerY - 15, lineThickness, 30, toValue(symbolColor)); + _tft->fillRect(centerX + lineThickness * 2, centerY - 15, lineThickness, 30, toValue(symbolColor)); + + xSemaphoreGive(xSemaphoreDisplay); + } + else + { + spLogI(LOGTAG_MULTITASK,"Unable to take xSemaphoreDisplay in drawSkipTrackIcon()."); + } +} + +/* +** =================================================================== +** drawBackIcon() +** Draws a back arrow icon pointing to the left. +** =================================================================== +*/ +void DisplayUI::drawBackIcon(int32_t x, int32_t y, int32_t width, int32_t height) +{ + if (xSemaphoreTake(xSemaphoreDisplay, portMAX_DELAY)) + { + // Define basic geometry + const int32_t centerX = x + width / 2; + const int32_t centerY = y + height / 2; + const int32_t arrowWidth = 20; + const int32_t arrowHeight = 10; + const int32_t lineThickness = 5; + const TFTColor symbolColor = TFTColor::SC_NetworkSuccess; //::White; + + // Draw left-facing arrow '<-' + // Draw the line part of the arrow + _tft->fillRect(centerX - lineThickness, centerY - lineThickness / 2, arrowWidth, lineThickness, toValue(symbolColor)); + + // Draw the triangular tip of the arrow + _tft->fillTriangle( + centerX - arrowWidth, centerY, // Tip of the arrow + centerX, centerY - arrowHeight, // Top point of triangle + centerX, centerY + arrowHeight, // Bottom point of triangle + toValue(symbolColor) + ); + + xSemaphoreGive(xSemaphoreDisplay); + } + else + { + spLogI(LOGTAG_MULTITASK,"Unable to take xSemaphoreDisplay in drawBackTrackIcon()."); + } +} + +/* +** =================================================================== +** read16() +** =================================================================== +*/ + +// These read 16- and 32-bit types from the SD card file. +// BMP data is stored little-endian, Arduino is little-endian too. +// May need to reverse subscript order if porting elsewhere. + +uint16_t DisplayUI::read16(fs::File &f) +{ + uint16_t result; + ((uint8_t *)&result)[0] = f.read(); // LSB + ((uint8_t *)&result)[1] = f.read(); // MSB + return result; +} + +/* +** =================================================================== +** read32() +** =================================================================== +*/ +uint32_t DisplayUI::read32(fs::File &f) +{ + uint32_t result; + ((uint8_t *)&result)[0] = f.read(); // LSB + ((uint8_t *)&result)[1] = f.read(); + ((uint8_t *)&result)[2] = f.read(); + ((uint8_t *)&result)[3] = f.read(); // MSB + return result; +} + +/* +** =================================================================== +** drawProgress() +** =================================================================== +*/ +void DisplayUI::drawProgress(const char *text, int8_t percentage) { + _ofr->setFontSize(24); + int pbWidth = _tft->width() - 100; + int pbX = (_tft->width() - pbWidth)/2; + int pbY = 210; //260; + int progressTextY = 160; //210; + + _tft->fillRect(0, progressTextY, _tft->width(), 40, TFT_BLACK); + _ofr->cdrawString(text, getCenterWidth(), progressTextY); + drawProgressBar(pbX, pbY, pbWidth, 15, percentage, TFTColor::White, TFTColor::BlueThinkPulse); +} + +/* +** =================================================================== +** drawSeparator() +** =================================================================== +*/ +void DisplayUI::drawSeparator(uint16_t y) +{ + _tft->drawFastHLine(10, y, _tft->width() - 2 * 15, 0x4228); +} + +/* +** =================================================================== +** clearScreen() +** =================================================================== +*/ +void DisplayUI::clearScreen() +{ + if (xSemaphoreTake(xSemaphoreDisplay, portMAX_DELAY)) + { + + _tft->fillScreen(toValue(getBackground())); + + xSemaphoreGive(xSemaphoreDisplay); + } + else + { + spLogI(LOGTAG_MULTITASK,"Unable to take xSemaphoreDisplay."); + } + +} + +/* +** =================================================================== +** clearScreenKeepArt() - clear area around art +** =================================================================== +*/ +void DisplayUI::clearScreenKeepArt() +{ + const int32_t lineSize = 10; + const int32_t borderSize = (_tft->width() - 300) / 2; // 300 is cover art size + if (xSemaphoreTake(xSemaphoreDisplay, portMAX_DELAY)) + { + // Top + _tft->fillRect(0, 0, _tft->width() , lineSize, toValue(getBackground())); + + // Left + _tft->fillRect(0, 0, borderSize, _tft->height(), toValue(getBackground())); + + // Right + _tft->fillRect(_tft->width() - borderSize, 0, borderSize, _tft->height(), toValue(getBackground())); + + // Bottom + _tft->fillRect(0, _tft->height() - lineSize, _tft->width() , lineSize, toValue(getBackground())); + + xSemaphoreGive(xSemaphoreDisplay); + } + else + { + spLogI(LOGTAG_MULTITASK,"Unable to take xSemaphoreDisplay."); + } + +} + +/* +** =================================================================== +** clearScreenKeepArt() - clear area around art +** =================================================================== +*/ +void DisplayUI::clearScreenHome() +{ + + if (xSemaphoreTake(xSemaphoreDisplay, portMAX_DELAY)) + { + // Top + _tft->fillRect(0, 0, _tft->width() , 200, toValue(getBackground())); + + // Bottom + _tft->fillRect(0,200, _tft->width() , 320, toValue(TFTColor::Black)); + + xSemaphoreGive(xSemaphoreDisplay); + } + else + { + spLogI(LOGTAG_MULTITASK,"Unable to take xSemaphoreDisplay."); + } + +} + +/* +** =================================================================== +** getBackground() +** Returns the current background color of the UI. +** =================================================================== +*/ +TFTColor DisplayUI::getBackground() const +{ + return _bgColor; +} + + +/* +** =================================================================== +** isSplitBackground() +** =================================================================== +*/ +bool DisplayUI::isSplitBackground() const +{ + // spLogI(LOGTAG_GENERAL,"isSplitBackground() returning %u", _isSplitBackground); + return _isSplitBackground; +} + +/* +** =================================================================== +** setSplitBackground() +** =================================================================== +*/ +void DisplayUI::setSplitBackground(bool isSplitBackground) +{ + // spLogI(LOGTAG_GENERAL,"setting _isSplitBackground to %u", isSplitBackground); + _isSplitBackground = isSplitBackground; +} + +/* +** =================================================================== +** setBackground() +** =================================================================== +*/ +void DisplayUI::setBackground(TFTColor color, bool bRepaint) +{ + if (xSemaphoreTake(xSemaphoreDisplay, portMAX_DELAY)) + { + _bgColor = color; + if (bRepaint) + { + _tft->fillScreen(toValue(color)); + } + + xSemaphoreGive(xSemaphoreDisplay); + } + else + { + spLogI(LOGTAG_MULTITASK,"Unable to take xSemaphoreDisplay."); + } + +} + +/* +** =================================================================== +** drawTextToLCD() +** Displays text on the LCD at the specified vertical position, +** using a default font size. +** =================================================================== +*/ +void DisplayUI::drawTextToLCD(const char *text, int posY) +{ + + int pbWidth = _tft->width() - 100; + int pbX = (_tft->width() - pbWidth)/2; + int pbY = 260; + int textY = posY; + + if (xSemaphoreTake(xSemaphoreDisplay, portMAX_DELAY)) + { + _ofr->setFontSize(24); + _tft->fillRect(0, textY, _tft->width(), 40, toValue(TFTColor::Black)); + _ofr->cdrawString(text, getCenterWidth(), textY); + xSemaphoreGive(xSemaphoreDisplay); + } + else + { + spLogI(LOGTAG_MULTITASK,"Unable to take xSemaphoreDisplay."); + } + +} + +/* +** =================================================================== +** drawTextToLCD() +** Overloaded version of drawTextToLCD that allows specifying +** font size and whether to ignore rendering the text. +** =================================================================== +*/ +void DisplayUI::drawTextToLCD(const char *text, int posY, int fontSize, bool ignoreText) +{ + + if (xSemaphoreTake(xSemaphoreDisplay, portMAX_DELAY)) + { + _ofr->setFontSize(fontSize); + + if (ignoreText) + { + _tft->fillRect(0, posY, _tft->width(), fontSize + 10, toValue(getBackground())); + } + else + { + _tft->fillRect(0, posY, _tft->width(), fontSize + 10, toValue(TFTColor::Black)); + _ofr->cdrawString(text, getCenterWidth(), posY); + } + + + xSemaphoreGive(xSemaphoreDisplay); + } + else + { + spLogI(LOGTAG_MULTITASK,"Unable to take xSemaphoreDisplay."); + } + +} + +/* +** =================================================================== +** rgb565() +** Converts 24-bit RGB888 color to 16-bit RGB565 format. +** =================================================================== +*/ +uint16_t rgb565(uint32_t color) { + uint8_t r = (color >> 16) & 0xFF; // Extract Red + uint8_t g = (color >> 8) & 0xFF; // Extract Green + uint8_t b = color & 0xFF; // Extract Blue + return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3); +} + +/* +** =================================================================== +** bgr565() +** Converts 24-bit RGB888 color to 16-bit RGB565 with red and blue swapped. +** =================================================================== +*/ +uint16_t bgr565(uint32_t color) { + uint8_t r = (color >> 16) & 0xFF; // Extract Red + uint8_t g = (color >> 8) & 0xFF; // Extract Green + uint8_t b = color & 0xFF; // Extract Blue + return ((b & 0xF8) << 8) | ((g & 0xFC) << 3) | (r >> 3); // Swap Red and Blue +} + +/* +** =================================================================== +** grb565() +** Converts 24-bit RGB888 color to 16-bit RGB565 with red and green swapped. +** =================================================================== +*/ +uint16_t grb565(uint32_t color) { + uint8_t r = (color >> 16) & 0xFF; // Extract Red + uint8_t g = (color >> 8) & 0xFF; // Extract Green + uint8_t b = color & 0xFF; // Extract Blue + return ((g & 0xF8) << 8) | ((r & 0xFC) << 3) | (b >> 3); // Swap Red and Green +} + +/* +** =================================================================== +** brg565() +** Converts 24-bit RGB888 color to 16-bit RGB565 with blue and green swapped. +** =================================================================== +*/ +uint16_t brg565(uint32_t color) { + uint8_t r = (color >> 16) & 0xFF; // Extract Red + uint8_t g = (color >> 8) & 0xFF; // Extract Green + uint8_t b = color & 0xFF; // Extract Blue + return ((b & 0xF8) << 8) | ((r & 0xFC) << 3) | (g >> 3); // Swap Blue and Green +} + +/* +** =================================================================== +** rgb888_to_rgb565() +** Converts a 24-bit RGB888 color value to 16-bit RGB565 format. +** =================================================================== +*/ +uint16_t rgb888_to_rgb565(uint32_t color) { + uint8_t r = (color >> 16) & 0xFF; // Extract Red (8-bit) + uint8_t g = (color >> 8) & 0xFF; // Extract Green (8-bit) + uint8_t b = color & 0xFF; // Extract Blue (8-bit) + + // Convert 8-bit RGB888 to 5-6-5 RGB565 format + return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3); +} + +/* +** =================================================================== +** convertColorForILI9488() +** Converts a color from RGB565 (16-bit) or RGB888 (24-bit) to +** the correct format for the ILI9488 display. Ensures that colors +** defined in the TFTColor enum render correctly. +** +** Parameters: +** color - Can be either: +** - A 16-bit RGB565 color (from TFTColor enum) +** - A 24-bit RGB888 color (0xRRGGBB) +** +** Returns: +** 16-bit RGB565 color, correctly formatted for the ILI9488. +** +** Notes: +** - If the input is already RGB565 (16-bit), it is returned as-is. +** - If the input is RGB888 (24-bit), it is converted to RGB565. +** - This ensures that colors from the TFTColor enum render correctly. +** +** Example Usage: +** uint16_t red565 = convertColorForILI9488(TFTColor::Red); +** uint16_t customBlue = convertColorForILI9488(0x0067B0); // Custom RGB888 color +** +** _ofr->cdrawString("RED", posX, posY, red565, convertColorForILI9488(TFTColor::Black)); +** =================================================================== +*/ +uint16_t convertColorForILI9488(uint32_t color) { + // If color is 16-bit (RGB565), return it as-is + if (color <= 0xFFFF) { + return static_cast(color); + } + + // If color is 24-bit (RGB888), convert it to RGB565 + uint8_t r = (color >> 16) & 0xFF; // Extract Red (8-bit) + uint8_t g = (color >> 8) & 0xFF; // Extract Green (8-bit) + uint8_t b = color & 0xFF; // Extract Blue (8-bit) + + // Convert 8-bit RGB888 to 5-6-5 RGB565 format + return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3); +} + +/* +** =================================================================== +** drawText() +** Draws text on the display at a given position and font size. +** =================================================================== +*/ +void DisplayUI::drawText(const char *text, int posX, int posY, int fontSize, TFTColor color) +{ + + if (xSemaphoreTake(xSemaphoreDisplay, portMAX_DELAY)) + { + _ofr->setFontSize(fontSize); + _ofr->setAlignment(Align::BottomLeft); + _tft->fillRect(posX - 70, posY, 140, fontSize + 10, toValue(TFTColor::Black)); + _ofr->cdrawString(text, posX, posY, toRGB565(color), toRGB565(TFTColor::Black)); + + xSemaphoreGive(xSemaphoreDisplay); + } + else + { + spLogI(LOGTAG_MULTITASK,"Unable to take xSemaphoreDisplay."); + } + +} + +/* +** =================================================================== +** drawString() +** Draws a string at the specified location with the given font, +** foreground and background colors, alignment, and optional clear mask. +** =================================================================== +*/ +void DisplayUI::drawString(const char *str, + int32_t x, + int32_t y, + unsigned int fontSize, + TFTColor fg, + TFTColor bg, + Align alignment, + const char *clearMask) +{ + _ofr->setAlignment(alignment); + _ofr->setFontSize(fontSize); + uint16_t fgColor = toRGB565(fg); + uint16_t bgColor = toRGB565(bg); + + // figure out what to clear + FT_BBox bbox; + if (strlen(clearMask) > 0) + { + // clear just the mask + bbox = _ofr->calculateBoundingBox(x, y, _ofr->getFontSize(), Align::Left, Layout::Horizontal, clearMask); + int width = bbox.xMax - bbox.xMin; + int height = bbox.yMax - bbox.yMin; + _tft->fillRect(x, bbox.yMin, width, height, toValue(bg)); + } + else + { + // clear entire line + bbox = _ofr->calculateBoundingBox(x, y, _ofr->getFontSize(), Align::Left, Layout::Horizontal, "AG123jt"); + int width = bbox.xMax - bbox.xMin; + int height = bbox.yMax - bbox.yMin; + _tft->fillRect(0, y, _tft->width(), height, toValue(bg)); + } + + _ofr->drawString(str, x, y, fgColor, bgColor, Layout::Horizontal); +} + +/* +** =================================================================== +** cDrawString() +** Draws a centered string with foreground and background color. +** =================================================================== +*/ +void DisplayUI::cDrawString( const char *str, + int32_t x, + int32_t y, + unsigned int fontSize, + TFTColor fg, + TFTColor bg, + const char *clearMask) +{ + drawString(str, x, y, fontSize, fg, bg, Align::Center, clearMask); +} + +/* +** =================================================================== +** lDrawString() +** Draws a left-aligned string with foreground and background color. +** =================================================================== +*/ +void DisplayUI::lDrawString( const char *str, + int32_t x, + int32_t y, + unsigned int fontSize, + TFTColor fg, + TFTColor bg, + const char *clearMask) +{ + drawString(str, x, y, fontSize, fg, bg, Align::Left, clearMask); +} + +/* +** =================================================================== +** rDrawString() +** Draws a right-aligned string with foreground and background color. +** =================================================================== +*/ +void DisplayUI::rDrawString( const char *str, + int32_t x, + int32_t y, + unsigned int fontSize, + TFTColor fg, + TFTColor bg, + const char *clearMask) +{ + _ofr->setAlignment(Align::Right); + _ofr->setFontSize(fontSize); + uint16_t fgColor = toRGB565(fg); + uint16_t bgColor = toRGB565(bg); + + // figure out what to clear + FT_BBox bbox = _ofr->calculateBoundingBox(x, y, _ofr->getFontSize(), Align::Right, Layout::Horizontal, clearMask); + int width = bbox.xMax - bbox.xMin; + int height = bbox.yMax - bbox.yMin; + _tft->fillRect(x - width, bbox.yMin, width, height, toValue(bg)); + + _ofr->drawString(str, x, y, fgColor, bgColor, Layout::Horizontal); +} + +/* +** =================================================================== +** drawClockTime() +** Draws the current time using clock font and updates only when values change. +** =================================================================== +*/ +void DisplayUI::drawClockTime(bool isUSFormat, bool isForceRepaint) +{ + const int fontSize = 128; + const int fontWidth = 76; + + int offset = 0; + std::string hours = ""; + if (isUSFormat) + { + hours = getCurrentTimestamp("%l").c_str(); + } + else + { + hours = getCurrentTimestamp("%H").c_str(); + offset = 50; + } + std::string minutes = getCurrentTimestamp("%M").c_str(); + std::string ampm = getCurrentTimestamp("%p").c_str(); + std::string seconds = getCurrentTimestamp("%S").c_str(); + + static std::string lastHours = ""; + static std::string lastMinutes = ""; + static std::string lastAmpm = ""; + + // Hours + if ((lastHours != hours) + || isForceRepaint) + { + _tft->fillRect(20 + offset, 35, fontWidth * 2, 100, toValue(getBackground())); + drawClockString(hours.c_str(), 20 + offset, 25, fontSize, TFTColor::White, getBackground()); + lastHours = hours; + } + + // Seconds indicator : + static bool isLastColonVisible = true; + int iSeconds = std::stoi(seconds); + bool isCurrentColonVisible = (iSeconds % 2 == 0); + if (isCurrentColonVisible != isLastColonVisible + || isForceRepaint) + { + _tft->fillRect(180 + offset, 53, 35, 84, toValue(getBackground())); + const char* colonChar = isCurrentColonVisible ? ":" : " "; + drawClockString(colonChar, 160 + offset, 20, fontSize, TFTColor::Yellow, getBackground()); + isLastColonVisible = isCurrentColonVisible; + } + + // Minutes + if ((lastMinutes != minutes) + || isForceRepaint) + { + _tft->fillRect(225 + offset, 35, fontWidth * 2, 100, toValue(getBackground())); + drawClockString(minutes.c_str(), 220 + offset, 25, fontSize, TFTColor::White, getBackground()); + lastMinutes = minutes; + } + + // AM/PM if US Format + if (isUSFormat) + { + if ((lastAmpm != ampm) + || isForceRepaint) + { + _tft->fillRect(380, 40, fontWidth, 100, toValue(getBackground())); //toValue(getBackground())); + if (ampm == "AM") + { + drawClockString(ampm.c_str(), 380, 35, fontSize / 2, TFTColor::Yellow, getBackground()); + } + else + { + drawClockString(ampm.c_str(), 380, 80, fontSize / 2, TFTColor::Yellow, getBackground()); + } + + lastAmpm = ampm; + } + } +} + +/* +** =================================================================== +** drawClockString() +** Draws a string using the clock font renderer with a specified +** foreground and background color at a given position. +** +** Parameters: +** str - The string to draw. +** x - X-coordinate on the screen. +** y - Y-coordinate on the screen. +** fontSize - Font size to use for rendering. +** fg - Foreground color (text color). +** bg - Background color. +** +** Notes: +** - This method does not clear the area before drawing. The caller +** must clear the area manually if needed. +** - Intended for use in clock display rendering. +** +** Example Usage: +** drawClockString("12", 30, 30, 96, TFTColor::Yellow, TFTColor::Black); +** =================================================================== +*/ +void DisplayUI::drawClockString( + const char *str, + int32_t x, + int32_t y, + unsigned int fontSize, + TFTColor fg, + TFTColor bg) +{ + _clockFont->setAlignment(Align::Left); + _clockFont->setFontSize(fontSize); + _clockFont->drawString(str, x, y, toRGB565(fg), toRGB565(bg), Layout::Horizontal); +} + +/* +** =================================================================== +** formatTime() +** Converts a time duration from milliseconds to a string in the +** format HH:MM:SS or MM:SS if hours are zero. +** +** Parameters: +** millis - Time duration in milliseconds. +** +** Returns: +** A string representing the time: +** - "H:MM:SS" if hours > 0 +** - "M:SS" if hours == 0 +** +** Notes: +** - Removes leading zeros (e.g., "1:05" instead of "01:05"). +** - Skips hours if they are zero (e.g., "5:30" instead of "00:05:30"). +** +** Example Usage: +** Serial.println(formatTime(3661000)); // Output: "1:01:01" +** Serial.println(formatTime(59000)); // Output: "59" +** Serial.println(formatTime(3600000)); // Output: "1:00:00" +** Serial.println(formatTime(300000)); // Output: "5:00" +** =================================================================== +*/ +std::string DisplayUI::formatTime(long millis) { + long totalSeconds = millis / 1000; + int hours = totalSeconds / 3600; + int minutes = (totalSeconds % 3600) / 60; + int seconds = totalSeconds % 60; + + std::string result; + + if (hours > 0) + { + result += std::to_string(hours) + ":"; + } + + result += std::to_string(minutes) + ":"; // No leading zero for minutes + if (seconds < 10) + { + result += "0"; // Ensure seconds always have two digits + } + result += std::to_string(seconds); + + return result; +} + + +/* +** =================================================================== +** showTouchDown() +** Highlights UI borders in the specified color to indicate a touch-down event. +** =================================================================== +*/ +void DisplayUI::showTouchDown(TFTColor color) +{ + paintTouch(true, color); +} + +/* +** =================================================================== +** showTouchUp() +** Clears UI border highlights to indicate a touch-up event. +** =================================================================== +*/ +void DisplayUI::showTouchUp() +{ + paintTouch(false, getBackground()); +} + +/* +** =================================================================== +** paintTouch() +** Helper method to paint or clear UI border highlights based on touch state. +** =================================================================== +*/ +void DisplayUI::paintTouch(bool isPress, TFTColor color) +{ + const int32_t lineSize = 10; + + if (xSemaphoreTake(xSemaphoreDisplay, portMAX_DELAY)) + { + // Top + _tft->fillRect(0, 0, _tft->width() , lineSize, toValue(color)); + + if (!isSplitBackground()) + { + // Left + _tft->fillRect(0, lineSize, lineSize, _tft->height(), toValue(color)); + + // Right + _tft->fillRect(_tft->width() - lineSize, lineSize, lineSize, _tft->height(), toValue(color)); + + // Bottom + _tft->fillRect(0, _tft->height() - lineSize, _tft->width() , lineSize, toValue(color)); + } + + xSemaphoreGive(xSemaphoreDisplay); + } + else + { + spLogI(LOGTAG_MULTITASK,"Unable to take xSemaphoreDisplay."); + } + +} + +/* +** =================================================================== +** getPreviousActionPoint() +** top third of the display, left of the cover art +** =================================================================== +*/ +Point DisplayUI::getPreviousActionPoint() +{ + // top third of the display, left of the cover art + int x = (_tft->width() - COVER_ART_SIZE) / 2; + int y = (_tft->height()) / 3; + return Point(x,y); +} + +/* +** =================================================================== +** getNextActionPoint() +** top third of the display, right of the cover art +** =================================================================== +*/ +Point DisplayUI::getNextActionPoint() +{ + int x = _tft->width() - ((_tft->width() - COVER_ART_SIZE) / 2); + int y = (_tft->height()) / 3; + return Point(x,y); +} + +/* +** =================================================================== +** pushImageToTft() +** +** Function will be called as a callback during decoding of a JPEG file to +** render each block to the TFT. +** =================================================================== +*/ + +bool DisplayUI::pushImageToTft(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t *bitmap) { + // Stop further decoding as image is running off bottom of screen + if (y >= _tft->height()) { + return 0; + } + + // Automatically clips the image block rendering at the TFT boundaries. + _tft->pushImage(x, y, w, h, bitmap); + + // Return 1 to decode next block + return 1; +} + +/* +** =================================================================== +** setJpgScaleToSmall() +** +** Adjusts the JPEG decoder scaling factor to either full scale (1:1) +** or half scale (1/2) based on the provided flag. Ensures thread +** safety by using a semaphore to coordinate access to the display. +** +** Parameters: +** isSmall - If true, sets the decoder to half scale (80x80 resolution). +** If false, sets the decoder to full scale (160x160 resolution). +** +** Notes: +** - Uses `xSemaphoreDisplay` to ensure exclusive access to the display. +** - If the semaphore cannot be acquired, a log message is generated. +** - JPEG scaling is controlled using `TJpgDec.setJpgScale()`. +** +** =================================================================== +*/ +void DisplayUI::setJpgScaleToSmall(bool isSmall) +{ + if (xSemaphoreTake(xSemaphoreDisplay, portMAX_DELAY)) + { + if (isSmall) + { + TJpgDec.setJpgScale(2); // half scale + } + else + { + TJpgDec.setJpgScale(1); // full scale + } + xSemaphoreGive(xSemaphoreDisplay); + } + else + { + spLogI(LOGTAG_MULTITASK,"Unable to take xSemaphoreDisplay."); + } +} + +/* +** =================================================================== +** setJpgScaleToTiny() +** Sets the JPEG decoder to use 1/8th scaling for rendering. +** =================================================================== +*/ +void DisplayUI::setJpgScaleToTiny() +{ + if (xSemaphoreTake(xSemaphoreDisplay, portMAX_DELAY)) + { + TJpgDec.setJpgScale(8); // 1/8th scale + xSemaphoreGive(xSemaphoreDisplay); + } + else + { + spLogI(LOGTAG_MULTITASK,"Unable to take xSemaphoreDisplay."); + } +} + +/* +** =================================================================== +** drawFsJpg() +** +** Draws a JPEG image from the filesystem to the display at a +** specified location. Ensures thread safety by using a semaphore +** to coordinate access to the display. Returns a result code +** indicating success or failure. +** +** Parameters: +** x - X-coordinate on the display where the image will be drawn. +** y - Y-coordinate on the display where the image will be drawn. +** pFilename - Path to the JPEG file in the filesystem. +** fs - Filesystem instance (e.g., LittleFS, SPIFFS) containing +** the image file. +** +** Returns: +** JRESULT - JPEG decoding result code (e.g., JDR_OK for success). +** +** Notes: +** - If the file does not exist, the method logs an error and returns +** a parameter error code (JDR_PAR). +** - If the semaphore cannot be taken, the method logs a message and +** skips the drawing operation. +** - The method is thread-safe due to semaphore protection. +** +** =================================================================== +*/ + +JRESULT DisplayUI::drawFsJpg(int32_t x, int32_t y, const char *pFilename, fs::FS &fs) +{ + JRESULT result = JDR_PAR; // Default to parametere error + + if (LittleFS.exists(pFilename)) + { + if (xSemaphoreTake(xSemaphoreDisplay, portMAX_DELAY)) + { + result = TJpgDec.drawFsJpg(x, y, pFilename, fs); + xSemaphoreGive(xSemaphoreDisplay); + } + else + { + spLogI(LOGTAG_MULTITASK,"Unable to take xSemaphoreDisplay."); + } + } + else + { + spLogE(LOGTAG_MULTITASK,"Filename does not exist."); + } + + return result; + +} + +/* +** =================================================================== +** jpgCallback() +** JPEG decoder callback to render or analyze JPEG pixels. +** =================================================================== +*/ +bool DisplayUI::jpgCallback(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t *bitmap) +{ + // Implementation for handling the callback + bool result = false; + if (pInstance->_isPainting) + { + result = pInstance->pushImageToTft(x, y, w, h, bitmap); + } + else + { + result = pInstance->jpgCalculateAverageColorCallback(x, y, w, h, bitmap); + } + + return result; // Placeholder +} + + +/* +** =================================================================== +** Helper Function: fromRGB() +** Converts 8-bit R, G, B values to a 16-bit RGB565 TFTColor. +** =================================================================== +*/ +constexpr TFTColor fromRGB(uint8_t red, uint8_t green, uint8_t blue) { + return static_cast(((red & 0xF8) << 8) | ((green & 0xFC) << 3) | (blue >> 3)); +} + +// Globals for color calculation +static uint32_t totalR = 0, totalG = 0, totalB = 0; +static uint32_t pixelCount = 0; + +/* +** =================================================================== +** jpgCalculateAverageColorCallback() +** Callback used during JPEG decoding to compute the average color +** of the image for background styling or theme adaptation. +** =================================================================== +*/ + +// Callback function for pixel processing +bool DisplayUI::jpgCalculateAverageColorCallback(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t* bitmap) +{ + + // Sampling factor (e.g., process every 4th pixel) + + const uint16_t samplingFactor = 4; // 16; //4; + + for (uint16_t i = 0; i < w * h; i += samplingFactor) + { + uint16_t pixel = bitmap[i]; + + uint8_t r8 = (pixel >> 11) & 0x1F; // 5-bit R + uint8_t g8 = (pixel >> 5) & 0x3F; // 6-bit G + uint8_t b8 = pixel & 0x1F; // 5-bit B + + // Scale up to 8-bit + uint8_t r = r8 << 3; + uint8_t g = g8 << 2; + uint8_t b = b8 << 3; + + int brightness = (r + g + b) / 3; + + // Skip extremely bright or extremely dark + if (brightness < 240 + && brightness > 20) + { + + totalR += r; + totalG += g; + totalB += b; + pixelCount++; + } + } + + return true; // Continue processing + +} + +/* +** =================================================================== +** calculateAverageColor() +** =================================================================== +*/ +// Calculate average color of JPEG file +TFTColor DisplayUI::calculateAverageColor(const char* filename) +{ + totalR = 0; + totalG = 0; + totalB = 0; + pixelCount = 0; + + spLogV(LOGTAG_PLAYER, "Entering calculateAverageColor(%s)", filename); + + Monitor::start(MONITOR_ID_CALCULATE_AVG_BACKGROUND_COLOR, LOGTAG_METRICS, "- drawFsJpg(0, 0, filename, LittleFS)"); + _isPainting = false; // TODO: wrap to make thread safe... maybe this block until it is reset. + drawFsJpg(0, 0, filename, LittleFS); + _isPainting = true; + spLogV(LOGTAG_PLAYER,"decoder.drawFsJpg() result is %d",filename); + Monitor::stop(MONITOR_ID_CALCULATE_AVG_BACKGROUND_COLOR); + + if (pixelCount == 0) { + spLogE(LOGTAG_SONG_DATA, "No pixels processed from the image."); + return TFTColor::Black; + } + + uint8_t avgR = totalR / pixelCount; + uint8_t avgG = totalG / pixelCount; + uint8_t avgB = totalB / pixelCount; + + spLogV(LOGTAG_PLAYER, "Average color: R=%d, G=%d, B=%d", avgR, avgG, avgB); + spLogV(LOGTAG_PLAYER, "pixelCount = %d", pixelCount); + + // Convert to HSV, boost saturation, convert back --- + float h, s, v; + DisplayUI::rgbToHsv(avgR, avgG, avgB, h, s, v); + + // Increase saturation (example: 1.5x) + s = std::min(s * 1.5f, 1.0f); // clamp at 1.0 + + // Convert back to RGB + uint8_t finalR, finalG, finalB; + DisplayUI::hsvToRgb(h, s, v, finalR, finalG, finalB); + + spLogV(LOGTAG_PLAYER, "Boosted color: R=%d, G=%d, B=%d", finalR, finalG, finalB); + + // Convert to 565 + TFTColor answer = fromRGB(finalR, finalG, finalB); + + return answer; +} + +/* +** =================================================================== +** rgbToHsv() +** Converts an RGB color value to HSV (Hue, Saturation, Value). +** This function modifies the hue, saturation, and value floats +** passed in by reference. +** +** Parameters: +** r - The red component in [0..255]. +** g - The green component in [0..255]. +** b - The blue component in [0..255]. +** h - Output for hue in degrees [0..360). +** s - Output for saturation in [0..1]. +** v - Output for value (brightness) in [0..1]. +** +** Returns: +** None. The results are written to h, s, and v by reference. +** =================================================================== +*/ +void DisplayUI::rgbToHsv(uint8_t r, uint8_t g, uint8_t b, float& h, float& s, float& v) +{ + // Convert from 8-bit [0..255] to floating-point [0..1] + float fr = r / 255.0f; + float fg = g / 255.0f; + float fb = b / 255.0f; + + // Find the maximum and minimum among R, G, B + float maxVal = std::max({fr, fg, fb}); + float minVal = std::min({fr, fg, fb}); + float delta = maxVal - minVal; // Range between max and min + + // "Value" (brightness) in HSV is simply the maximum component + v = maxVal; + + // If delta is extremely small, color is essentially grayscale + if (delta < 0.00001f) + { + s = 0.0f; // No saturation + h = 0.0f; // Hue is undefined, treat as 0 + return; + } + + // Saturation is the ratio of the color range to the brightness + s = (maxVal > 0.0f) ? (delta / maxVal) : 0.0f; + + // Determine hue based on which channel is the maximum + if (maxVal == fr) + { + // If red is max, hue depends on (green - blue) relative to delta + h = 60.0f * fmod(((fg - fb) / delta), 6.0f); + // Wrap negative angles into the [0..360) range + if (h < 0.0f) + { + h += 360.0f; + } + } + else if (maxVal == fg) + { + // If green is max, hue is in the 120..180 range + h = 60.0f * (((fb - fr) / delta) + 2.0f); + } + else // maxVal == fb + { + // If blue is max, hue is in the 240..300 range + h = 60.0f * (((fr - fg) / delta) + 4.0f); + } +} + +/* +** =================================================================== +** hsvToRgb() +** Converts an HSV (Hue, Saturation, Value) color value to RGB. +** This function modifies the red, green, and blue bytes passed +** in by reference. +** +** Parameters: +** h - The hue in degrees [0..360). +** s - The saturation in [0..1]. +** v - The value (brightness) in [0..1]. +** r - Output for red in [0..255]. +** g - Output for green in [0..255]. +** b - Output for blue in [0..255]. +** +** Returns: +** None. The results are written to r, g, and b by reference. +** =================================================================== +*/ +void DisplayUI::hsvToRgb(float h, float s, float v, uint8_t& r, uint8_t& g, uint8_t& b) +{ + // c is the "chroma"—the intensity of the color (range [0..v]) + float c = v * s; + + // x is an intermediate value used to spread out the color + // based on how far H is into each 60-degree segment + float x = c * (1.0f - fabs(fmod(h / 60.0f, 2.0f) - 1.0f)); + + // m adjusts the final values to match the requested Value (v) + float m = v - c; + + // Temporary floats for the "pure" R, G, B before adding m + float fr = 0; + float fg = 0; + float fb = 0; + + // Determine which 60-degree segment of the 360-degree Hue range we're in + if (h < 60) + { + // Segment 0: Red is at full chroma, green is partial, blue is 0 + fr = c; + fg = x; + fb = 0; + } + else if (h < 120) + { + // Segment 1: Green is at full chroma, red is partial, blue is 0 + fr = x; + fg = c; + fb = 0; + } + else if (h < 180) + { + // Segment 2: Green is at full chroma, blue is partial, red is 0 + fr = 0; + fg = c; + fb = x; + } + else if (h < 240) + { + // Segment 3: Blue is at full chroma, green is partial, red is 0 + fr = 0; + fg = x; + fb = c; + } + else if (h < 300) + { + // Segment 4: Blue is at full chroma, red is partial, green is 0 + fr = x; + fg = 0; + fb = c; + } + else + { + // Segment 5: Red is at full chroma, blue is partial, green is 0 + fr = c; + fg = 0; + fb = x; + } + + // Convert the float values [0..1] into 8-bit [0..255] range + // and add m to each channel to match the intended brightness (Value) + r = static_cast((fr + m) * 255); + g = static_cast((fg + m) * 255); + b = static_cast((fb + m) * 255); +} diff --git a/src/DisplayUI.h b/src/DisplayUI.h new file mode 100644 index 0000000..da5e18e --- /dev/null +++ b/src/DisplayUI.h @@ -0,0 +1,215 @@ +/*------------------------------------------------------------------------------------------------- +** +** DisplayUI.h +** +** Provides methods for drawing UI elements including album art, buttons, +** progress indicators, and text, using the TFT_eSPI and OpenFontRender libraries. +** +** Originally based on https://github.com/Bodmer/OpenWeather/blob/main/examples/TFT_eSPI_OpenWeather_LittleFS/GfxUi.h +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2024-12-26 - Electric Diversions - Renamed from GfxUI.h.h to tpGfxUi.h +** 2024-12-27 - Electric Diversions - Renamed to DisplayUI and promoted to base class +** ------------------------------------------------------------------------------------------------ +*/ + +#pragma once + +#include +#include +#include + +// JPEG decoder library +#include + +#include + +#include "Point.h" + + +// Maximum of 85 for BUFFPIXEL as 3 x this value is stored in an 8 bit variable! +// 32 is an efficient size for LittleFS due to SPI hardware pipeline buffer size +// A larger value of 80 is better for SD cards +#define BUFFPIXEL 32 + +// Size of cover art square side. +#define COVER_ART_SIZE 300 + +/* +** =================================================================== +** Predefined colors to use on the UI +** =================================================================== +*/ +enum class TFTColor : uint16_t +{ + Black = 0x0000, // 0, 0, 0 + Blue = 0x001F, // 0, 0, 255 + BlueThinkPulse = 0x0336, // The medium blue in the TP logo is 0x0067B0 which converts to 0x0336 in 16bit RGB565 + Brown = 0x9A60, // 150, 75, 0 + Cyan = 0x07FF, // 0, 255, 255 + DarkCyan = 0x03EF, // 0, 128, 128 + DarkGreen = 0x03E0, // 0, 128, 0 + DarkestGreen = 0x0280, + DarkGrey = 0x7BEF, // 128, 128, 128 + Gold = 0xFEA0, // 255, 215, 0 + Green = 0x07E0, // 0, 255, 0 + GreenYellow = 0xB7E0, // 180, 255, 0 + LightGrey = 0xD69A, // 211, 211, 211 + LightPink = 0xFC9F, // + Magenta = 0xF81F, // 255, 0, 255 + Maroon = 0x7800, // 128, 0, 0 + Navy = 0x000F, // 0, 0, 128 + Olive = 0x7BE0, // 128, 128, 0 + Orange = 0xFDA0, // 255, 180, 0 + Pink = 0xFE19, // 255, 192, 203 + Purple = 0x780F, // 128, 0, 128 + Red = 0xF800, // 255, 0, 0 + Silver = 0xC618, // 192, 192, 192 + SkyBlue = 0x867D, // 135, 206, 235 + Violet = 0x915C, // 180, 46, 226 + White = 0xFFFF, // 255, 255, 255 + Yellow = 0xFFE0, // 255, 255, 0 + + SC_LowHeap = 0xAB04, // 174, 98, 37 + SC_DJX_BG = 0x22D6, // 39, 88, 178 + SC_DJX_FG = 0x6F11, // 108, 227, 148 + SC_PreviousTrack = 0xFEA0, // 255, 215, 0 + SC_NextTrack = 0xB7E0, // 180, 255, 0 + SC_PauseTrack = 0x780F, // 128, 0, 128 + SC_CacheSave = 0x867D, // 135, 206, 235 + SC_TouchDown = 0x867D, // 135, 206, 235 + SC_AlertStatus = 0xFFE0, // 255, 255, 0 + SC_NetworkInProgress = 0x7BEF, // 128, 128, 128 + SC_NetworkSuccess = 0x03E0, // 0, 128, 0 + SC_NetworkFailure = 0xF800 // 255, 0, 0 +}; + +class DisplayUI { +public: + DisplayUI(TFT_eSPI *tft, OpenFontRender *render, OpenFontRender *clockFont); + void init(); + + // Original Thing Pulse methods + void drawBmp(String filename, uint16_t x, uint16_t y); + void drawLogo(); + void drawAppInfo(); + void drawProgressBar(uint16_t x, uint16_t y, uint16_t w, uint16_t h, + uint8_t percentage, TFTColor frameColor, + TFTColor barColor); + // Added 12/2024: + void drawAlbumArt(int32_t x, int32_t y, String filename); + void setJpgScaleToSmall(bool isSmall); + void setJpgScaleToTiny(); + void drawStatusBox(TFTColor statusColor); + void drawDownloadIndicator(bool bDownloadComplate); + void drawProgress(const char *text, int8_t percentage); + void drawSeparator(uint16_t y); + void drawButton(int32_t x, int32_t y, bool isPressed); + void drawBlankButton(int32_t x, int32_t y, int32_t width, int32_t height, uint8_t margin, TFTColor borderColor, bool isPressed); + void drawSkipTrackIcon(int32_t x, int32_t y, int32_t width, int32_t height, bool isReversed); + void drawPlayTrackIcon(int32_t x, int32_t y, int32_t width, int32_t height); + void drawPauseTrackIcon(int32_t x, int32_t y, int32_t width, int32_t height); + void drawBackIcon(int32_t x, int32_t y, int32_t width, int32_t height); + + void setBackground(TFTColor color, bool bRepaint=true); + TFTColor getBackground() const; + bool isSplitBackground() const; + void setSplitBackground(bool isSplitBackground = false); + void clearScreen(); + void clearScreenKeepArt(); + void clearScreenHome(); + std::string formatTime(long millis); + + void drawTextToLCD(const char *text, int posY); + void drawTextToLCD(const char *text, int posY, int fontSize, bool ignoreText); + void drawText(const char *text, int posX, int posY, int fontSize, TFTColor color); + + void drawString(const char *str, + int32_t x, + int32_t y, + unsigned int fontSize, + TFTColor fg, + TFTColor bg, + Align alignment, + const char *clearMask); + + void cDrawString(const char *str, + int32_t x, + int32_t y, + unsigned int fontSize, + TFTColor fg, + TFTColor bg, + const char *clearMask); + + void lDrawString(const char *str, + int32_t x, + int32_t y, + unsigned int fontSize, + TFTColor fg, + TFTColor bg, + const char *clearMask); + + void rDrawString(const char *str, + int32_t x, + int32_t y, + unsigned int fontSize, + TFTColor fg, + TFTColor bg, + const char *clearMask); + + void drawClockTime(bool isUSFormat, bool isForceRepaint); + + void showTouchDown(TFTColor color = TFTColor::SC_TouchDown); + void showTouchUp(); + + bool pushImageToTft(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t *bitmap); + + // display properties + int16_t getCenterWidth(); + int16_t getCenterHeight(); + int16_t getWidth(); + int16_t getHeight(); + Point getPreviousActionPoint(); + Point getNextActionPoint(); + + bool isUIDirty(); + void markUIDirty(bool isDirty); + + JRESULT drawFsJpg(int32_t x, int32_t y, const char *pFilename, fs::FS &fs); + TFTColor calculateAverageColor(const char* filename); + + // utility methods + static void rgbToHsv(uint8_t r, uint8_t g, uint8_t b, float& h, float& s, float& v); + static void hsvToRgb(float h, float s, float v, uint8_t& r, uint8_t& g, uint8_t& b); + + +private: + // Member variables + TFT_eSPI *_tft; + OpenFontRender *_ofr; + OpenFontRender *_clockFont; + TFTColor _bgColor = TFTColor::BlueThinkPulse; + bool _isSplitBackground = false; + bool _isUIDirty = false; + bool _isPainting = true; + SemaphoreHandle_t xSemaphoreDisplay = xSemaphoreCreateMutex(); + + // Methods + uint16_t read16(fs::File &f); + uint32_t read32(fs::File &f); + void paintTouch(bool isPress, TFTColor color); + static bool jpgCallback(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t *bitmap); + bool jpgCalculateAverageColorCallback(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t* bitmap); + void drawClockString( + const char *str, + int32_t x, + int32_t y, + unsigned int fontSize, + TFTColor fg, + TFTColor bg); + +}; diff --git a/src/FT6236.cpp b/src/FT6236TouchController/FT6236.cpp similarity index 100% rename from src/FT6236.cpp rename to src/FT6236TouchController/FT6236.cpp diff --git a/src/FT6236.h b/src/FT6236TouchController/FT6236.h similarity index 100% rename from src/FT6236.h rename to src/FT6236TouchController/FT6236.h diff --git a/src/GfxUi.cpp b/src/GfxUi.cpp deleted file mode 100644 index 4b4618d..0000000 --- a/src/GfxUi.cpp +++ /dev/null @@ -1,121 +0,0 @@ -// SPDX-FileCopyrightText: 2023 ThingPulse Ltd., https://thingpulse.com -// SPDX-License-Identifier: MIT - -// Based on https://github.com/Bodmer/OpenWeather/blob/main/examples/TFT_eSPI_OpenWeather_LittleFS/GfxUi.cpp - -#include "GfxUi.h" - -#define FS_TP_LOGO "/ThingPulse-logo-260.jpeg" - -GfxUi::GfxUi(TFT_eSPI *tft, OpenFontRender *ofr) { - _tft = tft; - _ofr = ofr; -} - -// Bodmer's streamlined x2 faster "no seek" version -void GfxUi::drawBmp(String filename, uint16_t x, uint16_t y) { - - if ((x >= _tft->width()) || (y >= _tft->height())) - return; - - fs::File bmpFS; - - // Note: ESP32 passes "open" test even if file does not exist, whereas ESP8266 - // returns NULL - if (!LittleFS.exists(filename)) { - log_e(" File not found"); - return; - } - - // Open requested file - bmpFS = LittleFS.open(filename, "r"); - - uint32_t seekOffset; - uint16_t w, h, row; - uint8_t r, g, b; - bool oldSwap = false; - - if (read16(bmpFS) == 0x4D42) { - read32(bmpFS); - read32(bmpFS); - seekOffset = read32(bmpFS); - read32(bmpFS); - w = read32(bmpFS); - h = read32(bmpFS); - - if ((read16(bmpFS) == 1) && (read16(bmpFS) == 24) && (read32(bmpFS) == 0)) { - y += h - 1; - - oldSwap = _tft->getSwapBytes(); - _tft->setSwapBytes(true); - bmpFS.seek(seekOffset); - - // Calculate padding to avoid seek - uint16_t padding = (4 - ((w * 3) & 3)) & 3; - uint8_t lineBuffer[w * 3 + padding]; - - for (row = 0; row < h; row++) { - - bmpFS.read(lineBuffer, sizeof(lineBuffer)); - uint8_t *bptr = lineBuffer; - uint16_t *tptr = (uint16_t *)lineBuffer; - // Convert 24 to 16 bit colours using the same line buffer for results - for (uint16_t col = 0; col < w; col++) { - b = *bptr++; - g = *bptr++; - r = *bptr++; - *tptr++ = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3); - } - - // Push the pixel row to screen, pushImage will crop the line if needed - // y is decremented as the BMP image is drawn bottom up - _tft->pushImage(x, y--, w, 1, (uint16_t *)lineBuffer); - } - } else - log_e("BMP format not recognized."); - } - _tft->setSwapBytes(oldSwap); - bmpFS.close(); -} - -void GfxUi::drawLogo() { - if (LittleFS.exists(FS_TP_LOGO)) { - uint16_t w = 0, h = 0; - TJpgDec.getFsJpgSize(&w, &h, FS_TP_LOGO, LittleFS); - TJpgDec.drawFsJpg((_tft->width() - w) / 2, 30, FS_TP_LOGO, LittleFS); - } -} - -void GfxUi::drawProgressBar(uint16_t x0, uint16_t y0, uint16_t w, uint16_t h, - uint8_t percentage, uint16_t frameColor, - uint16_t barColor) { - if (percentage == 0) { - _tft->fillRoundRect(x0, y0, w, h, 3, TFT_BLACK); - } - uint8_t margin = 2; - uint16_t barHeight = h - 2 * margin; - uint16_t barWidth = w - 2 * margin; - _tft->drawRoundRect(x0, y0, w, h, 3, frameColor); - _tft->fillRect(x0 + margin, y0 + margin, barWidth * percentage / 100.0, - barHeight, barColor); -} - -// These read 16- and 32-bit types from the SD card file. -// BMP data is stored little-endian, Arduino is little-endian too. -// May need to reverse subscript order if porting elsewhere. - -uint16_t GfxUi::read16(fs::File &f) { - uint16_t result; - ((uint8_t *)&result)[0] = f.read(); // LSB - ((uint8_t *)&result)[1] = f.read(); // MSB - return result; -} - -uint32_t GfxUi::read32(fs::File &f) { - uint32_t result; - ((uint8_t *)&result)[0] = f.read(); // LSB - ((uint8_t *)&result)[1] = f.read(); - ((uint8_t *)&result)[2] = f.read(); - ((uint8_t *)&result)[3] = f.read(); // MSB - return result; -} diff --git a/src/GfxUi.h b/src/GfxUi.h deleted file mode 100644 index ef86b30..0000000 --- a/src/GfxUi.h +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-FileCopyrightText: 2023 ThingPulse Ltd., https://thingpulse.com -// SPDX-License-Identifier: MIT - -// Based on https://github.com/Bodmer/OpenWeather/blob/main/examples/TFT_eSPI_OpenWeather_LittleFS/GfxUi.h - -#pragma once - -#include -#include -#include -#include - -// JPEG decoder library -#include - -// Maximum of 85 for BUFFPIXEL as 3 x this value is stored in an 8 bit variable! -// 32 is an efficient size for LittleFS due to SPI hardware pipeline buffer size -// A larger value of 80 is better for SD cards -#define BUFFPIXEL 32 - -class GfxUi { -public: - GfxUi(TFT_eSPI *tft, OpenFontRender *render); - void drawBmp(String filename, uint16_t x, uint16_t y); - void drawLogo(); - void drawProgressBar(uint16_t x, uint16_t y, uint16_t w, uint16_t h, - uint8_t percentage, uint16_t frameColor, - uint16_t barColor); - -private: - TFT_eSPI *_tft; - OpenFontRender *_ofr; - uint16_t read16(fs::File &f); - uint32_t read32(fs::File &f); -}; diff --git a/src/Monitor.cpp b/src/Monitor.cpp new file mode 100644 index 0000000..0782d53 --- /dev/null +++ b/src/Monitor.cpp @@ -0,0 +1,441 @@ +/*------------------------------------------------------------------------------------------------- +** +** Monitor.cpp +** +** Utility class for performance and resource monitoring. Provides timing +** instrumentation, heap tracking, and queue depth monitoring using static +** methods and internal tracking structures. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2025-01-07 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ + +#include "Monitor.h" +#include "SCLogger.h" +#include "logTags.h" +#include + +// Initialize the static timers map +std::map Monitor::timers; + +// Initialize id descriptors +std::map Monitor::idDescriptions; + +// Initialize the static HeapStats struct +Monitor::HeapStats Monitor::heapStats; + + +uint32_t Monitor::maxQueueDepth = 0; // Max queue depth + +/* +** =================================================================== +** registerDescription() +** Associates a human-readable description with a monitor ID. +** This description is included in the statistics dump for better +** clarity and context. +** +** Parameters: +** id - Unique identifier for the timer. +** description - A human-readable string describing the purpose +** or context of the timer. +** +** Example Usage: +** Monitor::registerDescription(MONITOR_ID_SPOTIFY_GET_CURRENTLY_PLAYING, +** "Spotify: Get Currently Playing"); +** +** Notes: +** - Descriptions should be registered early in the application's +** lifecycle, such as during initialization. +** - If a description is not registered for an ID, "Unknown" will +** appear in the statistics dump for that ID. +** +** =================================================================== +*/ +void Monitor::registerDescription(int id, const std::string& description) +{ + idDescriptions[id] = description; +} + +/* +** =================================================================== +** start() +** Starts the monitor for a given timer ID. +** +** Parameters: +** id - Unique identifier for the timer +** tag - Logging tag for the timer +** msg - Message to log when starting the timer +** +** =================================================================== +*/ +void Monitor::start(int id, const char* tag, const char* msg) +{ + TimerInfo& info = timers[id]; + info.startTime = std::chrono::steady_clock::now(); + info.tag = tag; + + spLogV(tag, "Monitor started (ID: %d). %s", id, msg); +} + +/* +** =================================================================== +** stop() +** Stops the monitor for a given timer ID and logs the elapsed time. +** +** Parameters: +** id - Unique identifier for the timer +** +** =================================================================== +*/ +void Monitor::stop(int id) +{ + auto it = timers.find(id); + if (it != timers.end()) + { + // Extract tag and start time + TimerInfo& info = it->second; + const std::string& tag = info.tag; + auto startTime = info.startTime; + + // Calculate the elapsed time + auto endTime = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast(endTime - startTime).count(); + + // Update statistics + info.totalDuration += elapsed; + info.stopCount++; + if (elapsed < info.minDuration) info.minDuration = elapsed; + if (elapsed > info.maxDuration) info.maxDuration = elapsed; + + // Calculate the average duration + long long avgDuration = info.totalDuration / info.stopCount; + + // Log the results + spLogV(tag.c_str(), "Monitor stopped (ID: %d). Elapsed: %lld ms. Avg: %lld ms. Min: %lld ms. Max: %lld ms.", + id, elapsed, avgDuration, info.minDuration, info.maxDuration); + } + else + { + spLogW(LOGTAG_GENERAL, "Monitor stop called for an unknown ID: %d", id); + } +} + +/* +** =================================================================== +** dumpStats() +** Logs a table of statistics for all currently tracked timers. +** +** Parameters: +** tag - Logging tag for the summary log entry +** +** =================================================================== +*/ +void Monitor::dumpStats(const char* tag) +{ + // Define column widths for alignment + constexpr int idWidth = 6; // ID column width + constexpr int descWidth = 20; // Description column width + constexpr int countWidth = 12; // Stop Count column width + constexpr int avgWidth = 10; // Avg(ms) column width + constexpr int minWidth = 7; // Min column width + constexpr int maxWidth = 7; // Max column width + + spLogI(tag, "Monitor Statistics Dump:"); + spLogI(tag, "---------------------------------------------------------------------------------"); + spLogI(tag, "| %*s | %-*s | %*s | %*s | %*s | %*s |", + idWidth, "ID", + descWidth, "Description", + countWidth, "Stop Count", + avgWidth, "Avg(ms)", + minWidth, "Min", + maxWidth, "Max"); + spLogI(tag, "---------------------------------------------------------------------------------"); + + for (const auto& entry : timers) + { + const int id = entry.first; + const TimerInfo& info = entry.second; + + // Get the description from idDescriptions, or use "Unknown ID" if not found + const std::string& description = idDescriptions.count(id) ? idDescriptions.at(id) : "Unknown ID"; + + if (info.stopCount > 0) + { + long long avgDuration = info.totalDuration / info.stopCount; + + spLogI(tag, "| %*d | %-*s | %*d | %*lld | %*lld | %*lld |", + idWidth, id, + descWidth, description.c_str(), + countWidth, info.stopCount, + avgWidth, avgDuration, + minWidth, info.minDuration, + maxWidth, info.maxDuration); + } + else + { + spLogI(tag, "| %*d | %-*s | %*d | %*s | %*s | %*s |", + idWidth, id, + descWidth, description.c_str(), + countWidth, info.stopCount, + avgWidth, "N/A", + minWidth, "N/A", + maxWidth, "N/A"); + } + } + + spLogI(tag, "---------------------------------------------------------------------------------"); +} + +/* +** =================================================================== +** getStats() +** Retrieves statistics for a specific monitor ID. +** +** Parameters: +** monitorID - The unique ID of the monitor. +** +** Returns: +** MonitorStats struct containing: +** - ID +** - Description (if registered, otherwise "Unknown ID") +** - Stop count +** - Average duration (ms) +** - Min duration (ms) +** - Max duration (ms) +** +** Notes: +** - If the monitor ID is not found, it returns default values. +** +** =================================================================== +*/ +MonitorStats Monitor::getStats(int monitorID) +{ + MonitorStats stats = {monitorID, "Unknown ID", 0, 0, LLONG_MAX, 0}; + + auto it = timers.find(monitorID); + if (it != timers.end()) + { + const TimerInfo& info = it->second; + + stats.description = idDescriptions.count(monitorID) ? idDescriptions.at(monitorID) : "Unknown ID"; + stats.stopCount = info.stopCount; + stats.minMs = info.minDuration; + stats.maxMs = info.maxDuration; + + // Calculate the average only if stopCount > 0 to avoid division by zero + stats.averageMs = (info.stopCount > 0) ? (info.totalDuration / info.stopCount) : 0; + } + + spLogV(LOGTAG_METRICS, "Returned ID: %s Avg: %lld", stats.description.c_str(),stats.averageMs); + return stats; +} + + +/* +** =================================================================== +** watchHeap() +** Logs the heap size and alerts if it falls below a threshold. +** +** Parameters: +** threshold - The heap size threshold to warn about +** +** Returns: +** true if over the heap threshold, false is under it +** +** =================================================================== +*/ +bool Monitor::watchHeap(uint32_t threshold) +{ + // Get the current heap size + uint32_t currentHeap = ESP.getFreeHeap(); + bool bAnswer = true; + + // Update stats + if (currentHeap < heapStats.minHeap) + { + heapStats.minHeap = currentHeap; + } + + if (currentHeap > heapStats.maxHeap) + { + heapStats.maxHeap = currentHeap; + } + + heapStats.totalHeap += currentHeap; + heapStats.sampleCount++; + + uint32_t avgHeap = heapStats.sampleCount > 0 ? heapStats.totalHeap / heapStats.sampleCount : 0; + + // Log the current heap size + spLogV(LOGTAG_HEAP," "); + spLogI(LOGTAG_HEAP, "Heap size: %u bytes. Avg: %u bytes. Min: %u bytes. Max: %u bytes.", + currentHeap, avgHeap, heapStats.minHeap, heapStats.maxHeap); + spLogV(LOGTAG_HEAP," "); + + // Warn if the heap size is below the threshold + if (currentHeap < threshold) + { + spLogW(LOGTAG_HEAP, "Heap size below threshold! Current: %u bytes, Threshold: %u bytes.", + currentHeap, threshold); + bAnswer = false; + } + return bAnswer; +} + +/* +** =================================================================== +** watchHeap() +** Logs the heap size and alerts if it falls below a threshold. +** +** Parameters: +** threshold - The heap size threshold to warn about +** +** Returns: +** true if over the heap threshold, false is under it +** +** =================================================================== +*/ +bool Monitor::watchQueue(QueueHandle_t queueHandle, uint32_t threshold) +{ + // Get the current queue depth + UBaseType_t messagesInQueue = uxQueueMessagesWaiting(queueHandle); + bool bAnswer = false; + + if (messagesInQueue > threshold) + { + spLogV(LOGTAG_GENERAL, "*** queue count over threshold of %u. count: %u", threshold, messagesInQueue); + bAnswer = true; + } + + if (messagesInQueue > maxQueueDepth) + { + maxQueueDepth = messagesInQueue; + } + + return bAnswer; +} + +uint32_t Monitor::getMaxQueueDepth() +{ + return maxQueueDepth; +} + +/* +** ------------------------------------------------------------------- +** getFreeHeap +** +** Returns the current heap size and remember the minimum heap. +** +** ------------------------------------------------------------------- +*/ +uint32_t Monitor::getFreeHeap() +{ + // Get the current heap size + uint32_t currentHeap = ESP.getFreeHeap(); + + // Update stats + if (currentHeap < heapStats.minHeap) + { + heapStats.minHeap = currentHeap; + } + + return currentHeap; +} + +/* +** ------------------------------------------------------------------- +** getHeapStatus +** +** Returns a copy of the current heap statistics. +** +** Returns: +** HeapStats - A struct containing heap information. +** +** ------------------------------------------------------------------- +*/ +Monitor::HeapStats Monitor::getHeapStats() +{ + return heapStats; +} + +/* +/* +** ------------------------------------------------------------------- +** getFormattedUptime +** +** Returns the system uptime as a formatted string based on the +** provided format string. +** +** Parameters: +** formatStr - A C-style format string that specifies the output +** format. Must contain four unsigned long format +** specifiers (e.g., "%lu") for days, hours, minutes, +** and seconds. +** +** Format Example: +** "System Uptime: %lu days, %lu hours, %lu minutes, %lu seconds." +** "Uptime: %luD %luH %luM %luS" +** +** Returns: +** std::string - A human-readable string representation of uptime. +** +** Notes: +** - The caller must ensure `formatStr` is a valid null-terminated +** format string with appropriate placeholders. +** - If `formatStr` is null, a default format will be used. +** +** ------------------------------------------------------------------- +*/ +std::string Monitor::getFormattedUptime(const char *pszFormatStr) +{ + + // Get the current uptime in milliseconds + unsigned long uptimeMillis = millis(); + + // Calculate days, hours, minutes, and seconds + unsigned long seconds = (uptimeMillis / 1000) % 60; + unsigned long minutes = (uptimeMillis / (1000 * 60)) % 60; + unsigned long hours = (uptimeMillis / (1000 * 60 * 60)) % 24; + unsigned long days = (uptimeMillis / (1000 * 60 * 60 * 24)); + + char buffer[100] = {0}; + snprintf(buffer, sizeof(buffer), pszFormatStr, + static_cast(days), + static_cast(hours), + static_cast(minutes), + static_cast(seconds)); + + return std::string(buffer); + +} + +/* +** =================================================================== +** logUptime() +** Logs the system uptime in a human-readable format (days, hours, +** minutes, and seconds). +** +** Parameters: +** tag - Logging tag for the uptime log entry +** +** Notes: +** - Uses millis() to calculate the system uptime. +** - Wraps around after approximately 49.7 days due to the size of +** the unsigned long returned by millis(). +** +** Example Output: +** [General] System Uptime: 3 days, 4 hours, 23 minutes, 15 seconds. +** +** =================================================================== +*/ +void Monitor::logUptime(const char* tag) +{ + // Log the uptime + spLogI(tag, "%s", Monitor::getFormattedUptime("System Uptime: %lu days, %lu hours, %lu minutes, %lu seconds.").c_str()); + +} \ No newline at end of file diff --git a/src/Monitor.h b/src/Monitor.h new file mode 100644 index 0000000..fed0ca5 --- /dev/null +++ b/src/Monitor.h @@ -0,0 +1,147 @@ +/*------------------------------------------------------------------------------------------------- +** +** Monitor.h +** +** Utility class for performance and resource monitoring. Provides timing +** instrumentation, heap tracking, and queue depth monitoring using static +** methods and internal tracking structures. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2025-01-07 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ + +#pragma once + +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/queue.h" + +#include "SCLogger.h" + +#define MONITOR_ID_CALCULATE_AVG_BACKGROUND_COLOR 100 +#define MONITOR_ID_SPOTIFY_GET_CURRENTLY_PLAYING 200 +#define MONITOR_ID_SPOTIFY_IMAGE_HTTP_GET 300 +#define MONITOR_ID_SPOTIFY_IMAGE_FILE_SAVE 310 +#define MONITOR_ID_SPOTIFY_IMAGE_FILE_LOAD 320 +#define MONITOR_ID_SPOTIFY_IMAGE_CACHE_LOAD 390 +#define MONITOR_ID_SPOTIFY_IMAGE_CACHE_SAVE 391 +#define MONITOR_ID_FETCH_ALBUM_ART 399 +#define MONITOR_ID_SCUI_QUEUE_DELAY 500 + +// Value was picked after after running multi-hour +// duration test and heap not going lower than +// 64304. - 1/19/2025 +// 51048. - 2/14/2025 (after swapping cores, etc.) +#define HEAP_LOW_THRESHOLD 50000 +#define HEAP_WATCH_SIZE_BASE 60000 + +// TODO: clean this up... +#define LLONG_MAX 9223372036854775807LL + +/* +** =================================================================== +** MonitorStats +** +** A struct to encapsulate statistics for a specific monitored event. +** +** Fields: +** id - The unique monitor ID +** description - A readable description of the monitor +** stopCount - The number of times the event has been stopped +** averageMs - The average duration in milliseconds +** minMs - The minimum recorded duration in milliseconds +** maxMs - The maximum recorded duration in milliseconds +** +** =================================================================== +*/ +struct MonitorStats +{ + int id; + std::string description; + int stopCount; + long long averageMs; + long long minMs; + long long maxMs; +}; + +/* +** =================================================================== +** Monitor +** +** A utility class to measure time intervals with support for +** multiple active timers. Each timer is identified by a unique ID. +** Methods are static for utility-style usage. +** +** =================================================================== +*/ +class Monitor +{ +public: + // Starts the monitor for a given timer ID + static void start(int id, const char* tag, const char* msg); + + // Stops the monitor for a given timer ID and logs the elapsed time + static void stop(int id); + + // Dumps the current stats for all timers to the log + static void dumpStats(const char* tag); + + // Retrieves statistics for a specific monitor ID. + static MonitorStats getStats(int monitorID); + + // Monitors and logs heap size stats + static bool watchHeap(uint32_t threshold); + + // Answer the current heap size and remember the lowest value + static uint32_t getFreeHeap(); + + // Monitors and logs heap size stats + static bool watchQueue(QueueHandle_t queueHandle, uint32_t threshold); + + // Answer the max monitored queue depth + static uint32_t getMaxQueueDepth(); + + // Register an ID description + static void registerDescription(int id, const std::string& description); + + // Answers the system uptime in a human-readable format + static std::string getFormattedUptime(const char *pszFormatStr); + + // Logs the system uptime in a human-readable format + static void logUptime(const char* tag); + + // Struct defining what is tracked for the heap + struct HeapStats + { + uint32_t minHeap = UINT32_MAX; + uint32_t maxHeap = 0; + uint64_t totalHeap = 0; + uint32_t sampleCount = 0; + }; + + // Return the current Heap Stats + static Monitor::HeapStats getHeapStats(); + +private: + struct TimerInfo + { + std::chrono::steady_clock::time_point startTime; + std::string tag; + long long totalDuration = 0; // Total time in milliseconds + int stopCount = 0; // Number of times stop() was called + long long minDuration = LLONG_MAX; // Minimum elapsed time + long long maxDuration = 0; // Maximum elapsed time + }; + + static std::map timers; // Active timers + static std::map idDescriptions; // Readabile descriptions + static HeapStats heapStats; // Heap stats tracker + static uint32_t maxQueueDepth; +}; diff --git a/src/PlayingMetadata.cpp b/src/PlayingMetadata.cpp new file mode 100644 index 0000000..074b243 --- /dev/null +++ b/src/PlayingMetadata.cpp @@ -0,0 +1,123 @@ +/*------------------------------------------------------------------------------------------------- +** +** PlayingMetadata.cpp +** +** Defines the PlayingMetadata structure used to represent Spotify playback data. +** Includes song, artist, album, and playback timing metadata. Supports copying +** from Spotify API responses and formatting artist display strings. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2025-02-02 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ + +#include "PlayingMetadata.h" +#include +#include +#include "SpotifyArduino.h" + +/* +** =================================================================== +** copyFrom() +** Copies data from a CurrentlyPlaying instance into a +** PlayingMetadata instance. lastRefreshMs set to time +** of operation. +** =================================================================== +*/ +void PlayingMetadata::copyFrom(const CurrentlyPlaying &source) +{ + numArtists = source.numArtists; + numImages = source.numImages; + isPlaying = source.isPlaying; + progressMs = source.progressMs; + durationMs = source.durationMs; + lastRefreshMs = millis(); + currentlyPlayingType = static_cast(source.currentlyPlayingType); + + strncpy(albumName, source.albumName ? source.albumName : "", PLAYING_NAME_CHAR_LENGTH); + strncpy(albumUri, source.albumUri ? source.albumUri : "", PLAYING_URI_CHAR_LENGTH); + strncpy(trackName, source.trackName ? source.trackName : "", PLAYING_NAME_CHAR_LENGTH); + strncpy(trackUri, source.trackUri ? source.trackUri : "", PLAYING_URI_CHAR_LENGTH); + strncpy(contextUri, source.contextUri ? source.contextUri : "", PLAYING_URI_CHAR_LENGTH); + + for (int i = 0; i < numArtists; i++) + { + strncpy(artists[i].artistName, source.artists[i].artistName ? source.artists[i].artistName : "", PLAYING_NAME_CHAR_LENGTH); + strncpy(artists[i].artistUri, source.artists[i].artistUri ? source.artists[i].artistUri : "", PLAYING_URI_CHAR_LENGTH); + } + + for (int i = 0; i < numImages; i++) + { + albumImages[i].height = source.albumImages[i].height; + albumImages[i].width = source.albumImages[i].width; + strncpy(albumImages[i].url, source.albumImages[i].url ? source.albumImages[i].url : "", PLAYING_URL_CHAR_LENGTH); + } +} + +/* +** =================================================================== +** copyFrom() +** Copies data from one PlayingMetadata instance to another. +** =================================================================== +*/ +void PlayingMetadata::copyFrom(const PlayingMetadata &source) +{ + numArtists = source.numArtists; + numImages = source.numImages; + isPlaying = source.isPlaying; + progressMs = source.progressMs; + durationMs = source.durationMs; + lastRefreshMs = source.lastRefreshMs; + currentlyPlayingType = source.currentlyPlayingType; + + strncpy(albumName, source.albumName, PLAYING_NAME_CHAR_LENGTH); + strncpy(albumUri, source.albumUri, PLAYING_URI_CHAR_LENGTH); + strncpy(trackName, source.trackName, PLAYING_NAME_CHAR_LENGTH); + strncpy(trackUri, source.trackUri, PLAYING_URI_CHAR_LENGTH); + strncpy(contextUri, source.contextUri, PLAYING_URI_CHAR_LENGTH); + + for (int i = 0; i < numArtists; i++) + { + strncpy(artists[i].artistName, source.artists[i].artistName, PLAYING_NAME_CHAR_LENGTH); + strncpy(artists[i].artistUri, source.artists[i].artistUri, PLAYING_URI_CHAR_LENGTH); + } + + for (int i = 0; i < numImages; i++) + { + albumImages[i].height = source.albumImages[i].height; + albumImages[i].width = source.albumImages[i].width; + strncpy(albumImages[i].url, source.albumImages[i].url, PLAYING_URL_CHAR_LENGTH); + } +} + +/* +** =================================================================== +** getArtistsList() +** Returns a String of all the artists for the track, adding an +** ellipsis ("...") if the length exceeds maxLength. +** =================================================================== +*/ +String PlayingMetadata::getArtistsList(size_t maxLength) +{ + String result = ""; + for (int i = 0; i < numArtists; i++) + { + if (!result.isEmpty()) + { + result += ", "; // Add a comma and space before each artist after the first + } + result += artists[i].artistName; + + // Check if the current result exceeds the max length + if (result.length() > maxLength) + { + result = result.substring(0, maxLength - 3) + "..."; // Add ellipsis + break; + } + } + return result; +} \ No newline at end of file diff --git a/src/PlayingMetadata.h b/src/PlayingMetadata.h new file mode 100644 index 0000000..886b5fe --- /dev/null +++ b/src/PlayingMetadata.h @@ -0,0 +1,76 @@ +/*------------------------------------------------------------------------------------------------- +** +** PlayingMetadata.h +** +** Defines the PlayingMetadata structure used to represent Spotify playback data. +** Includes song, artist, album, and playback timing metadata. Supports copying +** from Spotify API responses and formatting artist display strings. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2025-02-02 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ + +#include + +// Forward declaration (so we don't include SpotifyArduino.h here) +struct CurrentlyPlaying; + +// These need to stay in sync with what is defined in SpotifyArduino.h +#define PLAYING_NAME_CHAR_LENGTH 100 +#define PLAYING_URI_CHAR_LENGTH 40 +#define PLAYING_URL_CHAR_LENGTH 70 +#define PLAYING_MAX_NUM_ARTISTS 5 +#define PLAYING_NUM_ALBUM_IMAGES 3 + + +// Define PlayingMetaData struct with nested types +struct PlayingMetadata +{ + struct SongArtist + { + char artistName[PLAYING_NAME_CHAR_LENGTH]; + char artistUri[PLAYING_URI_CHAR_LENGTH]; + }; + + struct AlbumImage + { + int height; + int width; + char url[PLAYING_URL_CHAR_LENGTH]; + }; + + enum PlayingType + { + track, + episode, + other + }; + + SongArtist artists[PLAYING_MAX_NUM_ARTISTS]; + int numArtists; + char albumName[PLAYING_NAME_CHAR_LENGTH]; + char albumUri[PLAYING_URI_CHAR_LENGTH]; + char trackName[PLAYING_NAME_CHAR_LENGTH]; + char trackUri[PLAYING_URI_CHAR_LENGTH]; + AlbumImage albumImages[PLAYING_NUM_ALBUM_IMAGES]; + int numImages; + bool isPlaying; + long progressMs; + long durationMs; + long lastRefreshMs; + char contextUri[PLAYING_URI_CHAR_LENGTH]; + PlayingType currentlyPlayingType; + + String getArtistsList(size_t maxLength); + +private: + friend class SpotifyPlayer; + void copyFrom(const CurrentlyPlaying &source); + void copyFrom(const PlayingMetadata &source); + +}; diff --git a/src/Point.cpp b/src/Point.cpp new file mode 100644 index 0000000..3667c25 --- /dev/null +++ b/src/Point.cpp @@ -0,0 +1,31 @@ +/*------------------------------------------------------------------------------------------------- +** +** Point.cpp +** +** Defines a simple 2D Point class with integer coordinates. +** Provides methods for displaying the point and computing the +** Euclidean distance to another point. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2024-12-31 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ + +#include "Point.h" + +// Constructor implementation +Point::Point(int x, int y) : x(x), y(y) {} + +// Display the point +void Point::display() const { + std::cout << "(" << x << ", " << y << ")" << std::endl; +} + +// Calculate the distance to another point +double Point::distanceTo(const Point& other) const { + return std::sqrt(std::pow(x - other.x, 2) + std::pow(y - other.y, 2)); +} \ No newline at end of file diff --git a/src/Point.h b/src/Point.h new file mode 100644 index 0000000..8fac8d5 --- /dev/null +++ b/src/Point.h @@ -0,0 +1,36 @@ +/*------------------------------------------------------------------------------------------------- +** +** Point.h +** +** Defines a simple 2D Point class with integer coordinates. +** Provides methods for displaying the point and computing the +** Euclidean distance to another point. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2024-12-31 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ + +#pragma once + +#include +#include // For std::sqrt and std::pow + +class Point { +public: + int x; // X-coordinate + int y; // Y-coordinate + + // Constructor + Point(int x = 0, int y = 0); + + // Method to display the point + void display() const; + + // Method to calculate the distance to another point + double distanceTo(const Point& other) const; +}; \ No newline at end of file diff --git a/src/SCFileIO.cpp b/src/SCFileIO.cpp new file mode 100644 index 0000000..ec5388d --- /dev/null +++ b/src/SCFileIO.cpp @@ -0,0 +1,497 @@ +/*------------------------------------------------------------------------------------------------- +** +** SCFileIO.cpp +** +** A singleton class to manage file I/O operations safely using semaphores. +** Wraps LittleFS to provide thread-safe access to the file system, offering +** methods for reading, writing, and managing files on the ESP32. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2025-01-12 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ + +#include "SCFileIO.h" +#include "SCLogger.h" +#include "logTags.h" + +/* +** =================================================================== +** getInstance() +** Returns the singleton instance of the SCFileIO class. +** +** =================================================================== +*/ +SCFileIO& SCFileIO::getInstance() +{ + static SCFileIO instance; + return instance; +} + +/* +** =================================================================== +** SCFileIO Constructor +** =================================================================== +*/ +SCFileIO::SCFileIO() +{ + xSemaphoreFileIO = nullptr; +} + +/* +** =================================================================== +** SCFileIO Destructor +** =================================================================== +*/ +SCFileIO::~SCFileIO() +{ + if (xSemaphoreFileIO) + { + vSemaphoreDelete(xSemaphoreFileIO); + } +} + +/* +** =================================================================== +** initialize() +** Initializes the semaphore and the LittleFS file system. +** Lists all files in the file system after initialization. +** +** =================================================================== +*/ +bool SCFileIO::initialize() +{ + if (!xSemaphoreFileIO) + { + xSemaphoreFileIO = xSemaphoreCreateMutex(); + if (!xSemaphoreFileIO) + { + spLogE(LOGTAG_FILEIO, "Failed to create semaphore!"); + return false; + } + } + + if (!takeSemaphore()) + { + spLogE(LOGTAG_FILEIO, "Failed to acquire semaphore for initialize()."); + return false; + } + + if (LittleFS.begin()) + { + spLogI(LOGTAG_FILEIO, "Flash FS available!"); + giveSemaphore(); + listFiles(); + return true; + } + else + { + giveSemaphore(); + spLogE(LOGTAG_FILEIO, "Flash FS initialization failed!"); + return false; + } + +} + +/* +** =================================================================== +** getPartitionSize() +** Returns the total size in bytes of the mounted LittleFS partition. +** +** Returns: +** Size of the partition in bytes. Returns 0 if the value can't be retrieved +** or the semaphore acquisition fails. +** =================================================================== +*/ +size_t SCFileIO::getPartitionSize() +{ + if (!takeSemaphore()) + { + spLogE(LOGTAG_FILEIO, "Failed to acquire semaphore for getPartitionSize()."); + return 0; + } + + size_t partitionSize = LittleFS.totalBytes(); // No need for FSInfo + + giveSemaphore(); + return partitionSize; +} + +/* +** =================================================================== +** open() +** Opens a file safely using LittleFS. +** +** Parameters: +** path - Path to the file +** mode - File mode (e.g., "r", "w", etc.) +** +** Returns: +** File object, or an invalid file if the operation fails. +** =================================================================== +*/ +File SCFileIO::open(const char *path, const char *mode) +{ + if (!takeSemaphore()) + { + spLogE(LOGTAG_FILEIO, "Failed to acquire semaphore for open()."); + return File(); + } + + File file = LittleFS.open(path, mode); + giveSemaphore(); + + if (!file) + { + spLogE(LOGTAG_FILEIO, "Failed to open file: %s", path); + } + return file; +} + +/* +** =================================================================== +** write() +** Writes data to a file safely. +** +** Parameters: +** file - File object +** buffer - Data buffer +** size - Size of data to write +** +** Returns: +** Number of bytes written +** =================================================================== +*/ +size_t SCFileIO::write(File &file, const uint8_t *buffer, size_t size) +{ + if (!file) + { + spLogE(LOGTAG_FILEIO, "Invalid file object."); + return 0; + } + + if (!takeSemaphore()) + { + spLogE(LOGTAG_FILEIO, "Failed to acquire semaphore for write()."); + return 0; + } + + size_t bytesWritten = file.write(buffer, size); + giveSemaphore(); + return bytesWritten; +} + +/* +** =================================================================== +** read() +** Reads data from a file safely. +** +** Parameters: +** file - File object +** buffer - Buffer to read data into +** size - Size of data to read +** +** Returns: +** Number of bytes read +** =================================================================== +*/ +size_t SCFileIO::read(File &file, uint8_t *buffer, size_t size) +{ + if (!file) + { + spLogE(LOGTAG_FILEIO, "Invalid file object."); + return 0; + } + + if (!takeSemaphore()) + { + spLogE(LOGTAG_FILEIO, "Failed to acquire semaphore for read()."); + return 0; + } + + size_t bytesRead = file.read(buffer, size); + giveSemaphore(); + return bytesRead; +} + +/* +** =================================================================== +** close() +** Closes a file safely. +** +** Parameters: +** file - File object +** =================================================================== +*/ +void SCFileIO::close(File &file) +{ + if (!file) + { + spLogE(LOGTAG_FILEIO, "Invalid file object."); + return; + } + + if (!takeSemaphore()) + { + spLogE(LOGTAG_FILEIO, "Failed to acquire semaphore for close()."); + return; + } + + file.close(); + giveSemaphore(); +} + +/* +** =================================================================== +** remove() +** Deletes a file safely. +** +** Parameters: +** path - Path to the file +** +** Returns: +** True if the file was deleted, false otherwise +** =================================================================== +*/ +bool SCFileIO::remove(const char *path) +{ + if (!takeSemaphore()) + { + spLogE(LOGTAG_FILEIO, "Failed to acquire semaphore for remove()."); + return false; + } + + bool result = LittleFS.remove(path); + giveSemaphore(); + return result; +} + +/* +** =================================================================== +** exists() +** Checks if a file exists. +** +** Parameters: +** path - Path to the file +** +** Returns: +** True if the file exists, false otherwise +** =================================================================== +*/ +bool SCFileIO::exists(const char *path) +{ + if (!takeSemaphore()) + { + spLogE(LOGTAG_FILEIO, "Failed to acquire semaphore for exists()."); + return false; + } + + bool result = LittleFS.exists(path); + giveSemaphore(); + return result; +} + +/* +** =================================================================== +** listFiles() +** Lists all files in the LittleFS file system. +** =================================================================== +*/ +void SCFileIO::listFiles() +{ + spLogI(LOGTAG_FILEIO, "Flash FS files found:"); + + if (!takeSemaphore()) + { + spLogE(LOGTAG_FILEIO, "Failed to acquire semaphore for listFiles()."); + return; + } + + File root = LittleFS.open("/"); + while (true) + { + File entry = root.openNextFile(); + if (!entry) + { + break; + } + spLogI(LOGTAG_FILEIO, "- %s, %d bytes", entry.name(), entry.size()); + entry.close(); + } + + giveSemaphore(); +} + +/* +** =================================================================== +** readFsString() +** Reads a string from the specified file path in LittleFS. +** +** Parameters: +** path - The file path to read from. +** +** Returns: +** String - The string read from the file. Empty if read fails. +** =================================================================== +*/ +String SCFileIO::readFsString(const char *path) +{ + String token = ""; + spLogI(LOGTAG_FILEIO, "Loading string from '%s'.", path); + + if (!takeSemaphore()) + { + spLogE(LOGTAG_FILEIO, "Failed to acquire semaphore for readFsString()."); + return token; + } + + File f = LittleFS.open(path, "r"); + if (f) + { + token = f.readString(); + spLogD(LOGTAG_FILEIO, "Persisted string: %s", token.c_str()); + f.close(); + } + else + { + spLogE(LOGTAG_FILEIO, "Failed to load string from file system, returning empty."); + } + + giveSemaphore(); + return token; +} + +/* +** =================================================================== +** saveFsString() +** Saves a string to the specified file path in LittleFS. +** +** Parameters: +** path - The file path to save to. +** string - The string to save. +** =================================================================== +*/ +void SCFileIO::saveFsString(const char *path, String string) +{ + spLogI(LOGTAG_FILEIO, "Saving string to '%s'.", path); + + if (!takeSemaphore()) + { + spLogE(LOGTAG_FILEIO, "Failed to acquire semaphore for saveFsString()."); + return; + } + + File f = LittleFS.open(path, "w+"); + if (f) + { + f.print(string); + f.close(); + } + else + { + spLogE(LOGTAG_FILEIO, "Failed to open file."); + } + + giveSemaphore(); +} + + +/* +** =================================================================== +** takeSemaphore() +** Acquires the semaphore for file I/O operations. +** +** Returns: +** True if the semaphore was successfully taken, false otherwise +** =================================================================== +*/ +bool SCFileIO::takeSemaphore() +{ + return xSemaphoreTake(xSemaphoreFileIO, portMAX_DELAY) == pdTRUE; +} + +/* +** =================================================================== +** giveSemaphore() +** Releases the semaphore for file I/O operations. +** =================================================================== +*/ +void SCFileIO::giveSemaphore() +{ + xSemaphoreGive(xSemaphoreFileIO); +} + +/* +** =================================================================== +** hexDump() +** +** Outputs a formatted hexadecimal and ASCII dump of a file's contents +** to the serial console for inspection. The dump includes offsets, hex +** values, and printable ASCII characters. Intended for debugging file +** contents stored in LittleFS. +** +** Parameters: +** tag - Logging tag used for context and log filtering. +** filePath - Path to the file to be dumped. +** +** Notes: +** - Only runs if the logger's verbosity level for the tag is set to verbose. +** - Skips execution if the file cannot be opened or is empty. +** =================================================================== +*/ +void SCFileIO::hexDump(const char* tag, const char* filePath) +{ + + spLogV(tag, "attempting hexDump in SCFileIO::hexDump()... "); + // Check if logging level is at least verbose + if (SCLogger::getInstance().getLogLevel(tag) < ESP_LOG_VERBOSE) + { + return; // Skip if the log level is less than verbose + } + + // Open the file + File file = SCFileIO::getInstance().open(filePath, "r"); + if (!file || file.size() == 0) + { + spLogE(tag, "Failed to open file: %s or file is empty.", filePath); + return; + } + + // Start hex dump + Serial.printf("Hex Dump of file: %s\n", filePath); + Serial.printf("-----------------------------------------------------------------------------\n"); + + uint8_t buffer[16]; // Buffer to hold 16 bytes per line + size_t fileOffset = 0; // Tracks current offset in the file + char hexLine[50]; // 16 bytes * 3 chars (hex) + 1 space after 8th byte + null terminator + char asciiLine[17]; // 16 bytes + null terminator + + while (file.available()) + { + size_t bytesRead = file.read(buffer, sizeof(buffer)); // Read up to 16 bytes + memset(hexLine, ' ', sizeof(hexLine)); // Initialize with spaces for alignment + memset(asciiLine, '\0', sizeof(asciiLine)); // Null-terminate ASCII buffer + + for (size_t i = 0; i < bytesRead; ++i) + { + // Append hex value to the line + // snprintf(hexLine + (i * 3) + (i >= 8 ? 1 : 0), 4, "%02X ", buffer[i]); + snprintf(hexLine + (i * 3), 4, "%02X ", buffer[i]); + + // Append ASCII representation to the line + asciiLine[i] = isprint(buffer[i]) ? buffer[i] : '.'; + } + + // Log the formatted hex dump line + Serial.printf("%08X %s |%s|\n", fileOffset, hexLine, asciiLine); + + fileOffset += bytesRead; // Increment file offset + } + + Serial.printf("-----------------------------------------------------------------------------\n"); + file.close(); +} \ No newline at end of file diff --git a/src/SCFileIO.h b/src/SCFileIO.h new file mode 100644 index 0000000..2e6e13f --- /dev/null +++ b/src/SCFileIO.h @@ -0,0 +1,81 @@ +/*------------------------------------------------------------------------------------------------- +** +** SCFileIO.h +** +** A singleton class to manage file I/O operations safely using semaphores. +** Wraps LittleFS to provide thread-safe access to the file system, offering +** methods for reading, writing, and managing files on the ESP32. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2025-01-12 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ +#pragma once + +#include +#include +#include + +/* +** =================================================================== +** SCFileIO +** =================================================================== +*/ +class SCFileIO +{ +public: + // Access the singleton instance + static SCFileIO& getInstance(); + + // Initializes the semaphore. Must be called before using the class. + bool initialize(); + + // Get partition size + size_t getPartitionSize(); + + // Opens a file safely using LittleFS + File open(const char *path, const char *mode); + + // Writes data to a file safely + size_t write(File &file, const uint8_t *buffer, size_t size); + + // Reads data from a file safely + size_t read(File &file, uint8_t *buffer, size_t size); + + // Closes a file safely + void close(File &file); + + // Deletes a file safely + bool remove(const char *path); + + // Returns true if a file exists + bool exists(const char *path); + + // String read/write methods + String readFsString(const char *path); + void saveFsString(const char *path, String string); + void listFiles(); + void hexDump(const char* tag, const char* filePath); + + // Public semaphore for external use if needed + SemaphoreHandle_t xSemaphoreFileIO; + +private: + // Private constructor and destructor to enforce singleton pattern + SCFileIO(); + ~SCFileIO(); + + // Deleted copy constructor and assignment operator + SCFileIO(const SCFileIO&) = delete; + SCFileIO& operator=(const SCFileIO&) = delete; + + // Acquires the semaphore + bool takeSemaphore(); + + // Releases the semaphore + void giveSemaphore(); +}; \ No newline at end of file diff --git a/src/SCLogger.cpp b/src/SCLogger.cpp new file mode 100644 index 0000000..9929415 --- /dev/null +++ b/src/SCLogger.cpp @@ -0,0 +1,166 @@ +/*------------------------------------------------------------------------------------------------- +** +** SCLogger.h +** +** A singleton logging utility that wraps ESP_LOGx macros. Allows dynamic +** log level configuration per tag and provides formatted output with +** file and line references. Intended as a tactical workaround for tag-based +** log filtering issues. Created due to not being able to get log_x and ESP_LOGx +** macros to respect the tags and levels. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2025-01-03 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ + +#include "SCLogger.h" +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +/* +** =================================================================== +** getInstance() +** Returns the singleton instance of the SCLogger class. +** =================================================================== +*/ +SCLogger& SCLogger::getInstance() { + static SCLogger instance; + return instance; +} + +/* +** =================================================================== +** setLogLevel() +** Sets the log level for a specific tag. +** =================================================================== +*/ +void SCLogger::setLogLevel(const char* tag, esp_log_level_t level) { + std::lock_guard lock(mutex); + tagLevels[tag] = level; +} + +/* +** =================================================================== +** getLogLevel() +** Gets the current log level for a specific tag. +** =================================================================== +*/ +esp_log_level_t SCLogger::getLogLevel(const char* tag) { + std::lock_guard lock(mutex); + auto it = tagLevels.find(tag); + if (it != tagLevels.end()) { + return it->second; + } + return ESP_LOG_NONE; // Default to no logging if not set +} + +/* +** =================================================================== +** Helper Method: logMessage() +** Logs a message at the specified log level if enabled. +** =================================================================== +*/ +void SCLogger::logMessage(esp_log_level_t level, const char* tag, const char* format, va_list args) +{ + // For debugging + // Serial.printf("SCLogger::logMessage called with level: %d (%d), tag: %s\n", level, getLogLevel(tag), tag); + + // Switched VERBOSE and DEBUG to really use ESP_LOGI + // so global setting can be raised to 3 and it + // will still honor SCLogger settings. + + if (level <= getLogLevel(tag)) { + char buffer[256]; // Adjust size as needed + vsnprintf(buffer, sizeof(buffer), format, args); + + // Log the message at the appropriate ESP_LOG level + // Have Verbose and Debug at I so that they will + // show if enabled and system logging is surpressed + // at that level. + switch (level) { + case ESP_LOG_VERBOSE: + ESP_LOGI(tag, "%s[V] (%s)", buffer, pcTaskGetTaskName(nullptr)); + break; + case ESP_LOG_DEBUG: + ESP_LOGI(tag, "%s[D] (%s)", buffer, pcTaskGetTaskName(nullptr)); + break; + case ESP_LOG_INFO: + ESP_LOGI(tag, "%s[I] (%s)", buffer, pcTaskGetTaskName(nullptr)); + break; + case ESP_LOG_WARN: + ESP_LOGW(tag, "***>> %s[W] (%s)", buffer, pcTaskGetTaskName(nullptr)); + break; + case ESP_LOG_ERROR: + ESP_LOGE(tag, "***>> %s[E] (%s)", buffer, pcTaskGetTaskName(nullptr)); + break; + default: + break; + } + } +} + +/* +** =================================================================== +** logVerbose() +** =================================================================== +*/ +void SCLogger::logVerbose(const char* tag, const char* format, ...) { + va_list args; + va_start(args, format); + logMessage(ESP_LOG_VERBOSE, tag, format, args); + va_end(args); +} + +/* +** =================================================================== +** logDebug() +** =================================================================== +*/ +void SCLogger::logDebug(const char* tag, const char* format, ...) { + va_list args; + va_start(args, format); + logMessage(ESP_LOG_DEBUG, tag, format, args); + va_end(args); +} + +/* +** =================================================================== +** logInfo() +** =================================================================== +*/ +void SCLogger::logInfo(const char* tag, const char* format, ...) { + va_list args; + va_start(args, format); + logMessage(ESP_LOG_INFO, tag, format, args); + va_end(args); +} + +/* +** =================================================================== +** logWarn() +** =================================================================== +*/ +void SCLogger::logWarn(const char* tag, const char* format, ...) { + va_list args; + va_start(args, format); + logMessage(ESP_LOG_WARN, tag, format, args); + va_end(args); +} + +/* +** =================================================================== +** logError() +** =================================================================== +*/ +void SCLogger::logError(const char* tag, const char* format, ...) { + va_list args; + va_start(args, format); + logMessage(ESP_LOG_ERROR, tag, format, args); + va_end(args); +} \ No newline at end of file diff --git a/src/SCLogger.h b/src/SCLogger.h new file mode 100644 index 0000000..bc70ba3 --- /dev/null +++ b/src/SCLogger.h @@ -0,0 +1,106 @@ +/*------------------------------------------------------------------------------------------------- +** +** SCLogger.h +** +** A singleton logging utility that wraps ESP_LOGx macros. Allows dynamic +** log level configuration per tag and provides formatted output with +** file and line references. Intended as a tactical workaround for tag-based +** log filtering issues. Created due to not being able to get log_x and ESP_LOGx +** macros to respect the tags and levels. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2025-01-03 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ + +#pragma once + +#include +#include +#include +#include + +/* +** =================================================================== +** SCLogger +** =================================================================== +*/ +class SCLogger { +public: + /* + ** =================================================================== + ** getInstance() + ** Returns the singleton instance of the SCLogger class. + ** =================================================================== + */ + static SCLogger& getInstance(); + + /* + ** =================================================================== + ** setLogLevel() + ** Sets the log level for a specific tag. + ** Parameters: + ** tag - The tag for which the log level is being set. + ** level - The log level to set (ESP_LOG_VERBOSE, ESP_LOG_INFO, etc.). + ** =================================================================== + */ + void setLogLevel(const char* tag, esp_log_level_t level); + + /* + ** =================================================================== + ** getLogLevel() + ** Gets the current log level for a specific tag. + ** Parameters: + ** tag - The tag for which to get the log level. + ** Returns: + ** The current log level for the specified tag. + ** =================================================================== + */ + esp_log_level_t getLogLevel(const char* tag); + + /* + ** =================================================================== + ** Logging Methods + ** Logs a message at the specified log level if the level is enabled + ** for the given tag. + ** =================================================================== + */ + void logVerbose(const char* tag, const char* format, ...); + void logDebug(const char* tag, const char* format, ...); + void logInfo(const char* tag, const char* format, ...); + void logWarn(const char* tag, const char* format, ...); + void logError(const char* tag, const char* format, ...); + +private: + SCLogger() = default; // Private constructor + SCLogger(const SCLogger&) = delete; // Prevent copy + SCLogger& operator=(const SCLogger&) = delete; // Prevent assignment + void logMessage(esp_log_level_t level, const char* tag, const char* format, va_list args); + std::map tagLevels; // Map to store log levels per tag + std::mutex mutex; // Mutex to protect the map +}; +// Define this to enable logging; comment out to disable +#define ENABLE_SCLOGGING + +// Define macros for logging +#ifdef ENABLE_SCLOGGING + +#define spLogV(tag, format, ...) SCLogger::getInstance().logVerbose(tag, format " [%s:%d]", ##__VA_ARGS__, __FILE__, __LINE__) +#define spLogD(tag, format, ...) SCLogger::getInstance().logDebug(tag, format " [%s:%d]", ##__VA_ARGS__, __FILE__, __LINE__) +#define spLogI(tag, format, ...) SCLogger::getInstance().logInfo(tag, format " [%s:%d]", ##__VA_ARGS__, __FILE__, __LINE__) +#define spLogW(tag, format, ...) SCLogger::getInstance().logWarn(tag, format " [%s:%d]", ##__VA_ARGS__, __FILE__, __LINE__) +#define spLogE(tag, format, ...) SCLogger::getInstance().logError(tag, format " [%s:%d]", ##__VA_ARGS__, __FILE__, __LINE__) + +#else // If logging is disabled + +#define spLogV(tag, ...) (void)0 +#define spLogD(tag, ...) (void)0 +#define spLogI(tag, ...) (void)0 +#define spLogW(tag, ...) (void)0 +#define spLogE(tag, ...) (void)0 + +#endif // ENABLE_SCLOGGING \ No newline at end of file diff --git a/src/SpotifyArtMgr.cpp b/src/SpotifyArtMgr.cpp new file mode 100644 index 0000000..ddec2a9 --- /dev/null +++ b/src/SpotifyArtMgr.cpp @@ -0,0 +1,954 @@ +/*------------------------------------------------------------------------------------------------- +** +** SpotifyArtMgr.cpp +** +** Handles downloading and caching of Spotify album art. Ensures reliable +** display by managing secure certificate validation and efficient image +** caching with LRU eviction. Designed as a Singleton for global access. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2025-01-11 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ + +#include "SpotifyArtMgr.h" +#include "SCLogger.h" +#include "logTags.h" +#include "Monitor.h" +#include "SCFileIO.h" +#include + +/* +** =================================================================== +** Certificates for Secure Spotify Image Downloads +** +** DigiCert Global Root G2 Certificate: +** Source: https://www.digicert.com/kb/digicert-root-certificates.htm +** +** GlobalSign Root R2 Certificate: +** Source: https://www.globalsign.com/en/repository +** Command to convert the downloaded CRT file to PEM format: +** zsh> openssl x509 -inform DER -in Root-R3.crt -out GlobalSignRoot.pem +** +** Context: +** Spotify's image server (i.scdn.co) occasionally returns certificates +** signed by either the DigiCert Global Root G2 or the GlobalSign Root R2 +** certificate authorities. Previously, the system was configured to trust +** only the DigiCert Global Root G2 certificate. When the server returned +** a GlobalSign certificate, the connection would fail due to an +** unrecognized CA. +** +** Purpose: +** This implementation includes both the DigiCert Global Root G2 and +** GlobalSign Root R2 certificates to ensure compatibility with the +** Spotify image server, regardless of which CA is used for signing. The +** certificates are programmatically combined and passed to WiFiClientSecure +** for validation. +** +** Notes: +** - The certificates were last pulled and verified on 1/12/2025. +** - If future errors arise, verify both certificates for validity and +** ensure they are updated as needed. +** - Use `WiFiClientSecure::setCACert()` to set the combined certificates +** for secure HTTPS connections. +** +** Maintenance: +** 1. Regularly check the above URLs for updated root certificates. +** 2. Use the OpenSSL command provided above to convert new certificates +** to PEM format. +** 3. Update this code with any new certificates and adjust their +** combination logic if necessary. +** +** SPDX-License-Identifier: MIT +** =================================================================== +*/ +const char* gCombinedCerts = R"EOF( +-----BEGIN CERTIFICATE----- +MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH +MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI +2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx +1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ +q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz +tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ +vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV +5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY +1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4 +NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG +Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91 +8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe +pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl +MrY= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G +A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp +Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4 +MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG +A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8 +RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT +gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm +KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd +QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ +XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw +DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o +LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU +RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp +jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK +6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX +mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs +Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH +WD9f +-----END CERTIFICATE----- +)EOF"; + +// Initialize the singleton instance +SpotifyArtMgr* SpotifyArtMgr::gInstance = nullptr; + +const char* gCacheFileroot = "/cache_index_v5"; +// const size_t gJSONDocSize = 16384; //8192; + +/* +** =================================================================== +** Constructor +** Initializes the SpotifyArtMgr object. +** =================================================================== +*/ +SpotifyArtMgr::SpotifyArtMgr() +{ + // Initialize anything as needed (currently empty) +} + +/* +** =================================================================== +** getInstance() +** Returns the singleton instance of the SpotifyArtMgr class. +** =================================================================== +*/ +SpotifyArtMgr* SpotifyArtMgr::getInstance() +{ + + if (gInstance == nullptr) + { + gInstance = new SpotifyArtMgr(); + // figure out if configured with more than 4MB of file space + gInstance->determineCacheSize(); + // populate the index to last run + String filePath = String(gCacheFileroot) + ".txt"; + SCFileIO::getInstance().hexDump(LOGTAG_CACHE, filePath.c_str()); + Monitor::start(MONITOR_ID_SPOTIFY_IMAGE_CACHE_LOAD, LOGTAG_CACHE, "loadCacheIndex()"); + gInstance->loadCacheIndex(); + Monitor::stop(MONITOR_ID_SPOTIFY_IMAGE_CACHE_LOAD); + } + return gInstance; +} + + + +/* +** =================================================================== +** getLocalFileName() +** Retrieves the local file name for a given URL. Checks an internal +** cache to see if the URL is already known. If found, returns the +** cached file name; otherwise returns a fallback image name. +** +** Parameters: +** url - The URL whose corresponding file name should be looked up. +** +** Returns: +** String containing the cached file name if available, or a fallback +** image file name if not found in the cache. +** =================================================================== +*/ + +String SpotifyArtMgr::getLocalFileName(const String& url) +{ + spLogD(LOGTAG_CACHE, "getLocalFileName() for URL: %s", url.c_str()); + + // ------------------------------------------------------------------- + // If a download is pending, delay in small increments until either + // it clears or 3 seconds have elapsed. + // ------------------------------------------------------------------- + if (_isDownloadPending) + { + // const int totalWaitMs = 3000; + // const int stepMs = 100; + // int waitedMs = 0; + + // while (_isDownloadPending && waitedMs < totalWaitMs) + // { + // vTaskDelay(pdMS_TO_TICKS(stepMs)); + // waitedMs += stepMs; + // } + } + + auto it = _urlToFileMap.find(url); + if (it != _urlToFileMap.end()) + { + // The iterator points to an entry in _cacheList; access the actual value + String fileName = it->second; // Dereference the iterator to get the cached URL + spLogD(LOGTAG_CACHE, "Returning cached file name: %s", fileName.c_str()); + return fileName; // Return the cached file name + } + + spLogD(LOGTAG_CACHE, "URL not found in cache. Returning fallback image: %s", SP_NO_COVER_JPG_FILENAME); + return SP_NO_COVER_JPG_FILENAME; // Return fallback image +} + +/* +** =================================================================== +** acquireAlbumArt() +** Retrieves album art for the specified URL. Checks whether the URL +** is already cached; if so, moves it to the front of the cache and +** returns the existing file name. Otherwise, downloads the file, +** adds it to the cache, and evicts the oldest entry if necessary. +** +** Parameters: +** url - The URL of the album art to retrieve. +** +** Returns: +** A String representing the local file name of the album art, or +** a fallback file name (SP_NO_COVER_JPG_FILENAME) if the download +** fails. +** =================================================================== +*/ + +String SpotifyArtMgr::acquireAlbumArt(const String& url) +{ + spLogD(LOGTAG_CACHE, "acquireAlbumArt() for URL: %s", url.c_str()); + + _stats.totalFetches++; + + auto it = _urlToFileMap.find(url); + + // If the URL is already cached, move it to the front of the cache list + bool isCacheHit = false; + if (it != _urlToFileMap.end()) + { + isCacheHit = true; + _cacheList.remove(url); // Remove from current position + _cacheList.push_front(url); // Move to the front + _isDirty = true; // Write the updated cache index + _stats.cacheHits++; + _stats.currentHitStreak++; + spLogD(LOGTAG_CACHE, "Cache hit. Moved URL to the front: %s", url.c_str()); + } + _stats.averageHitRate = (_stats.totalFetches > 0) ? ((static_cast(_stats.cacheHits) / _stats.totalFetches) * 100.0) : 0; + if (_stats.currentHitStreak > _stats.longestHitStreak) + { + _stats.longestHitStreak = _stats.currentHitStreak; + } + + if (isCacheHit) + { + return it->second; // Return the cached file name associated with the URL + } + + _stats.currentHitStreak = 0; + + // Generate local file name and download + String localFileName = generateLocalFileName(); + + if (!downloadFile(url, localFileName)) + { + spLogE(LOGTAG_GENERAL, "Failed to download album art for URL: %s", url.c_str()); + return SP_NO_COVER_JPG_FILENAME; // Return fallback image + } + + // Add the URL and local file name to the cache + if (_cacheList.size() >= _maxCacheSize) + { + evictOldestFile(); // Evict if necessary + } + + _cacheList.push_front(url); + _urlToFileMap[url] = localFileName; + _isDirty = true; + + spLogD(LOGTAG_CACHE, "Returning downloaded file name: %s", localFileName.c_str()); + return localFileName; +} + +/* +** =================================================================== +** setMaxCacheSize() +** Sets the maximum size of the cache. +** +** Parameters: +** maxSize - The maximum number of cached items +** =================================================================== +*/ +void SpotifyArtMgr::setMaxCacheSize(size_t maxSize) +{ + _maxCacheSize = maxSize; +} + +/* +** =================================================================== +** generateLocalFileName() +** Generates a local file name for storing album art. If the cache +** is full, it reuses the file name from the oldest cached entry. +** Otherwise, it creates a new file name based on an internal counter. +** +** Parameters: +** None. +** +** Returns: +** A String containing the reused or newly generated local file name. +** =================================================================== +*/ +String SpotifyArtMgr::generateLocalFileName() +{ + char buffer[32]; + + // Check if the cache is full + if (_cacheList.size() >= _maxCacheSize) + { + // Use the file name of the oldest (to be evicted) item in the list + String oldestUrl = _cacheList.back(); // Back because LRU is the least recently used + auto it = _urlToFileMap.find(oldestUrl); + if (it != _urlToFileMap.end()) + { + String fileName = it->second; + spLogD(LOGTAG_CACHE, "Reusing file name of oldest cache entry: %s", fileName.c_str()); + return fileName; // Return the file name associated with the oldest URL + } + } + + // If the cache is not full, generate a new file name + snprintf(buffer, sizeof(buffer), "/sc_album_art_%03zu.dat", _nextFileId++); + spLogD(LOGTAG_CACHE, "Generated new file name: %s", buffer); + return String(buffer); +} + + +/* +** =================================================================== +** evictOldestFile() +** Removes the least recently used (LRU) entry from the cache. This +** involves popping the oldest URL from the back of the cache list +** and removing its corresponding file entry from the map. +** +** Parameters: +** None. +** +** Returns: +** None. +** =================================================================== +*/ +void SpotifyArtMgr::evictOldestFile() +{ + if (_cacheList.empty()) + { + spLogW(LOGTAG_CACHE, "Eviction attempted but cache list is empty."); + return; + } + + // Get the oldest URL (back of the list) + String oldestUrl = _cacheList.back(); + _cacheList.pop_back(); // Remove from the back of the list + + // Find and remove the URL in the map + auto it = _urlToFileMap.find(oldestUrl); + if (it != _urlToFileMap.end()) + { + spLogD(LOGTAG_CACHE, "Evicting URL: %s associated with file: %s", + oldestUrl.c_str(), it->second.c_str()); + _urlToFileMap.erase(it); // Remove from the map + } + else + { + spLogW(LOGTAG_CACHE, "Attempted to evict a URL not found in _urlToFileMap."); + } +} + +/* +** =================================================================== +** downloadFile() +** Downloads a file from the given URL and saves it locally. +** +** Parameters: +** url - The URL of the file to download +** filename - The local file name to save the file as +** +** Returns: +** True if the file was successfully downloaded, false otherwise. +** =================================================================== +*/ +bool SpotifyArtMgr::downloadFile(String url, String filename) +{ + _isDownloadPending = true; + + // Placeholder for actual implementation + spLogD(LOGTAG_GENERAL, "Downloading file: %s -> %s", url.c_str(), filename.c_str()); + + // Was MAX_RETRIES = 2; however, retries seemed to fail + // plus current code will try to refetch art anyway + // during next refresh cycle + constexpr int MAX_RETRIES = 0; + constexpr int RETRY_DELAY_MS = 500; + int retryCount = 0; + + spLogI(LOGTAG_SONG_DATA, "Downloading %s and saving as %s", url.c_str(), filename.c_str()); + + while (retryCount <= MAX_RETRIES) { + spLogD(LOGTAG_SONG_DATA, "Attempt %d of %d", retryCount + 1, MAX_RETRIES + 1); + spLogD(LOGTAG_SONG_DATA, "Free heap before request: %d", Monitor::getFreeHeap()); + + HTTPClient http; + WiFiClientSecure wifiClient; + + spLogV(LOGTAG_SONG_DATA, "[HTTP] begin..."); + wifiClient.setCACert(gCombinedCerts); //(spotify_image_server_cert); + //http.setTimeout(10000); // 5 seconds is the default + http.begin(wifiClient, url); + //http.setReuse(false); + + spLogV(LOGTAG_SONG_DATA, "[HTTP] GET..."); + Monitor::start(MONITOR_ID_SPOTIFY_IMAGE_HTTP_GET, LOGTAG_METRICS, "int httpCode = http.GET();"); + int httpCode = http.GET(); + Monitor::stop(MONITOR_ID_SPOTIFY_IMAGE_HTTP_GET); + spLogD(LOGTAG_SONG_DATA, "Free heap before request: %d", Monitor::getFreeHeap()); + + if (httpCode == HTTP_CODE_OK) { + spLogV(LOGTAG_SONG_DATA, "[HTTP] GET succeeded with code: %d", httpCode); + Monitor::start(MONITOR_ID_SPOTIFY_IMAGE_FILE_SAVE, LOGTAG_METRICS, "saveFileFromHTTPResponse(filename, &http);"); + + // Wrap call that is going to use the file system + spLogD(LOGTAG_MULTITASK,"Taking xSemaphoreFileIO."); + if (xSemaphoreTake(SCFileIO::getInstance().xSemaphoreFileIO, portMAX_DELAY)) + { + saveFileFromHTTPResponse(filename, &http); + _isDirty = true; + xSemaphoreGive(SCFileIO::getInstance().xSemaphoreFileIO); + } + else + { + spLogI(LOGTAG_MULTITASK,"Unable to take xSemaphoreFileIO."); + } + Monitor::stop(MONITOR_ID_SPOTIFY_IMAGE_FILE_SAVE); + http.end(); + wifiClient.stop(); + return true; // Exit on success + } else { + spLogE(LOGTAG_SONG_DATA, "[HTTP] GET failed. Code: %d, Error: %s", httpCode, http.errorToString(httpCode).c_str()); + } + + http.end(); + wifiClient.stop(); + + retryCount++; + if (retryCount <= MAX_RETRIES) { + spLogD(LOGTAG_SONG_DATA, "Retrying after %d ms...", RETRY_DELAY_MS); + delay(RETRY_DELAY_MS); + } + } + + spLogE(LOGTAG_SONG_DATA, "Download failed after %d attempts. _isCoverArtAvailable = %", MAX_RETRIES + 1); + + _isDownloadPending = false; + + return false; +} + +/* +** =================================================================== +** saveFileFromHTTPResponse() +** Helper method that saves the file. Put in own method to allow +** for retry logic. +** +** NOTE: This method is not thread safe and needs to be called +** called by something that wraps the call. +** =================================================================== +*/ +void SpotifyArtMgr::saveFileFromHTTPResponse(String filename, HTTPClient *pHttp ) +{ + fs::File f = LittleFS.open(filename, "w+"); + if (!f) + { + spLogE(LOGTAG_SONG_DATA, "file open failed in SpotifyPlayer::downloadFile"); + return; + } + + // get lenght of document (is -1 when Server sends no Content-Length + // header) + int total = pHttp->getSize(); + int len = total; + + // create buffer for read + uint8_t buff[128] = {0}; + + // get tcp stream + WiFiClient *stream = pHttp->getStreamPtr(); + + // read all data from server + while (pHttp->connected() && (len > 0 || len == -1)) + { + // get available data size + size_t size = stream->available(); + + if (size) + { + // read up to 128 byte + int c = stream->readBytes(buff, ((size > sizeof(buff)) ? sizeof(buff) : size)); + + // write it to Serial + f.write(buff, c); + + if (len > 0) + { + len -= c; + } + } + delay(1); + } + spLogI(LOGTAG_SONG_DATA, "\n[HTTP] connection closed or file end.\n"); + + f.close(); + +} + +/* +** =================================================================== +** isCacheDirty() +** Answers whether the cache index file has been changed +** since the last save. Value can be used to assist with UI indicators. +** =================================================================== +*/ +bool SpotifyArtMgr::isCacheDirty() +{ + return _isDirty; +} + +/* +** =================================================================== +** getStats() +** Retrieves a copy of the current SpotifyArtMgr statistics. +** +** Returns: +** ArtMgrStats - A struct containing various statistics related to +** album art fetches, cache hits, and hit streaks. +** +** Notes: +** - This method returns a copy of the statistics, ensuring the +** original data remains unchanged. +** - The returned struct includes total fetches, cache hit count, +** current and longest hit streaks, and the average hit rate. +** +** Example Usage: +** ArtMgrStats stats = SpotifyArtMgr::getInstance().getStats(); +** spLogI(LOGTAG_GENERAL, "Cache Hits: %u", stats.cacheHits); +** =================================================================== +*/ +ArtMgrStats SpotifyArtMgr::getStats() +{ + ArtMgrStats answerStats; + + answerStats.totalFetches = _stats.totalFetches; + answerStats.cacheHits = _stats.cacheHits; + answerStats.currentHitStreak = _stats.currentHitStreak; + answerStats.longestHitStreak = _stats.longestHitStreak; + answerStats.averageHitRate = _stats.averageHitRate; + + return answerStats; +} + +/* +** =================================================================== +** saveCacheIndex() +** Saves the current cache state to a file in a simple text format. +** This includes the mapping of URLs to file names and the order of +** cached items for proper eviction handling. +** +** Format: +** Each line of the file contains a URL and its associated local +** file name, separated by a '|'. The final line contains a checksum +** in the format "CHECKSUM:". +** +** Notes: +** - The cache state is saved to the file "/cache_index.txt". +** - This method calculates a checksum of the serialized cache +** data to validate file integrity during loading. +** - A temporary file "/cache_index.tmp" is used to ensure atomic +** updates, and the original file is replaced only if the +** operation succeeds. +** +** Integrity: +** - The checksum is written as the last line of the file. +** - Any changes to the file format should account for checksum +** validation logic. +** +** Preconditions: +** - The `_urlToFileMap` and `_cacheList` structures must be in sync. +** - The `SCFileIO` singleton handles thread-safe file operations. +** +** =================================================================== +*/ +void SpotifyArtMgr::saveCacheIndex() +{ + spLogV(LOGTAG_CACHE, "Saving cache index to a simple text format with checksum."); + + if (!_isDirty) + { + spLogV(LOGTAG_CACHE, "No changes to save. Returning."); + return; + } + + Monitor::start(MONITOR_ID_SPOTIFY_IMAGE_CACHE_SAVE, LOGTAG_CACHE, "saveCacheIndex()"); + + String tempFilePath = String(gCacheFileroot) + ".tmp"; + String finalFilePath = String(gCacheFileroot) + ".txt"; + + // Write to a temporary file + File tempFile = SCFileIO::getInstance().open(tempFilePath.c_str(), "w+"); + if (!tempFile) + { + spLogE(LOGTAG_CACHE, "Failed to open temporary file for cache index."); + Monitor::stop(MONITOR_ID_SPOTIFY_IMAGE_CACHE_SAVE); + return; + } + + String serializedData; + + // Write cache entries and construct serialized data for checksum calculation + for (const auto& entry : _urlToFileMap) + { + serializedData += entry.first + "|" + entry.second + "\n"; + tempFile.printf("%s|%s\n", entry.first.c_str(), entry.second.c_str()); + } + + // Calculate checksum + uint32_t checksum = calculateChecksum(serializedData.c_str(), serializedData.length()); + + // Write checksum as the last line + tempFile.printf("CHECKSUM:%u\n", checksum); + tempFile.close(); + spLogI(LOGTAG_CACHE, "Temporary cache index saved successfully."); + + // Rename the temporary file to the final file + if (SCFileIO::getInstance().exists(finalFilePath.c_str())) + { + SCFileIO::getInstance().remove(finalFilePath.c_str()); + } + + if (LittleFS.rename(tempFilePath, finalFilePath)) + { + _isDirty = false; + spLogI(LOGTAG_CACHE, "Cache index successfully updated."); + } + else + { + spLogE(LOGTAG_CACHE, "Failed to rename temporary file to final file."); + } + + Monitor::stop(MONITOR_ID_SPOTIFY_IMAGE_CACHE_SAVE); + + printCacheIndex(LOGTAG_CACHE); + //SCFileIO::getInstance().listFiles(); +} + +/* +** =================================================================== +** determineCacheSize() +** Determines the maximum number of album art files to cache based +** on the size of the LittleFS partition. If the partition is at +** least 3,000,000 bytes, this is assumed to be a custom partition +** layout and the cache size is increased to 60. Otherwise, the +** default cache size (typically 10) is used. +** +** This method should be called during initialization to adjust the +** cache size dynamically based on the hardware configuration. +** +** Notes: +** - SCFileIO::getPartitionSize() is used to obtain the partition size +** in a thread-safe manner. +** - A partition size >= 3,000,000 bytes indicates use of the custom +** no_ota.csv layout with a larger cache capacity. +** - The default cache size should be initialized before this is called. +** +** =================================================================== +*/ + +void SpotifyArtMgr::determineCacheSize() +{ + size_t partitionSize = SCFileIO::getInstance().getPartitionSize(); + + if (partitionSize >= 3000000) + { + _maxCacheSize = 60; + spLogI(LOGTAG_CACHE, "Custom partition detected (size: %zu). Setting max cache size to 60.", partitionSize); + } + else + { + spLogI(LOGTAG_CACHE, "Standard partition detected (size: %zu). Using default cache size: %zu.", partitionSize, _maxCacheSize); + } +} + +/* +** =================================================================== +** getMaxCacheSize() +** Returns the current maximum cache size used for album art. +** +** Returns: +** size_t - The maximum number of cached album art entries. +** =================================================================== +*/ +size_t SpotifyArtMgr::getMaxCacheSize() +{ + return _maxCacheSize; +} + +/* +** =================================================================== +** loadCacheIndex() +** Loads the cache state from a previously saved file in a simple +** text format. This method restores the mapping of URLs to file +** names and the order of cached items. +** +** Format: +** Each line of the file contains a URL and its associated local +** file name, separated by a '|'. The final line contains a checksum +** in the format "CHECKSUM:". +** +** Validation: +** - The checksum stored in the file is compared with a calculated +** checksum of the serialized data to ensure integrity. +** - If the checksum validation fails, the file is removed, and the +** cache state is reset. +** +** Notes: +** - The cache state is read from the file "/cache_index.txt". +** - If the file is missing, empty, or invalid, the cache is started fresh. +** - This method clears and repopulates `_urlToFileMap` and `_cacheList`. +** +** Preconditions: +** - The `SCFileIO` singleton handles thread-safe file operations. +** +** Error Handling: +** - Logs warnings for invalid or missing entries in the file. +** - Removes the cache file if checksum validation fails or +** deserialization encounters an error. +** +** =================================================================== +*/ +void SpotifyArtMgr::loadCacheIndex() +{ + spLogI(LOGTAG_CACHE, "Loading cache index from a simple text file with checksum."); + + String finalFilePath = String(gCacheFileroot) + ".txt"; + + if (!SCFileIO::getInstance().exists(finalFilePath.c_str())) + { + spLogW(LOGTAG_CACHE, "Cache index file not found. Starting fresh."); + return; + } + + File file = SCFileIO::getInstance().open(finalFilePath.c_str(), "r"); + if (!file || file.size() == 0) + { + spLogE(LOGTAG_CACHE, "Cache index file is empty or inaccessible. Removing it and starting fresh."); + SCFileIO::getInstance().remove(finalFilePath.c_str()); + return; + } + + String serializedData; + uint32_t storedChecksum = 0; + uint32_t maxFileId = 0; // Track the highest file ID + + _cacheList.clear(); + _urlToFileMap.clear(); + + // Read file line by line + while (file.available()) + { + String line = file.readStringUntil('\n'); + line.trim(); + + // Check for checksum line + if (line.startsWith("CHECKSUM:")) + { + storedChecksum = line.substring(9).toInt(); // Extract checksum value + continue; + } + + // Parse URL and file name + int separatorIndex = line.indexOf('|'); + if (separatorIndex != -1) + { + String url = line.substring(0, separatorIndex); + String localFile = line.substring(separatorIndex + 1); + _urlToFileMap[url] = localFile; + _cacheList.push_back(url); + + // Extract numeric part of file name (e.g., "003" from "/sc_album_art_003") + int startIdx = localFile.lastIndexOf('_') + 1; + int endIdx = localFile.lastIndexOf('.'); + + // Log the start and end indexes before any logic + spLogD(LOGTAG_CACHE, "Parsing file name: %s", localFile.c_str()); + spLogD(LOGTAG_CACHE, "Initial startIdx: %d, endIdx: %d (before adjustments)", startIdx, endIdx); + + // Check if there's a period after the underscore + if (localFile.lastIndexOf('.') > startIdx) { + endIdx = localFile.lastIndexOf('.'); + spLogD(LOGTAG_CACHE, "Adjusted endIdx to: %d (period found after underscore)", endIdx); + } + + // Log indexes before the substring operation + spLogD(LOGTAG_CACHE, "Final startIdx: %d, endIdx: %d", startIdx, endIdx); + + if (startIdx > 0 && endIdx > startIdx) + { + String fileIdStr = localFile.substring(startIdx, endIdx); + uint32_t fileId = fileIdStr.toInt(); + + // Update maxFileId + if (fileId > maxFileId) + { + maxFileId = fileId; + } + + spLogD(LOGTAG_CACHE, "Parsed file ID: %u from localFile: %s", fileId, localFile.c_str()); + } + + serializedData += line + "\n"; // Accumulate serialized data + } + } + file.close(); + + // Validate checksum + uint32_t calculatedChecksum = calculateChecksum(serializedData.c_str(), serializedData.length()); + if (storedChecksum != calculatedChecksum) + { + spLogE(LOGTAG_CACHE, "Checksum mismatch! Stored: %u, Calculated: %u", storedChecksum, calculatedChecksum); + SCFileIO::getInstance().remove(finalFilePath.c_str()); + return; + } + + // Update _nextFileId + _nextFileId = maxFileId + 1; + spLogI(LOGTAG_CACHE, "Next file ID set to: %u", _nextFileId); + + spLogI(LOGTAG_CACHE, "Checksum validation passed. Cache index loaded successfully."); + + printCacheIndex(LOGTAG_CACHE); +} + +/* +** =================================================================== +** printCacheIndex() +** Logs the current cache state in a formatted and readable format. +** Check code for log levels used. +** +** The log includes: +** - List of URLs and their associated file names in the LRU order. +** - The calculated checksum for the cache state. +** +** Parameters: +** tag - The logging tag to identify the context of the log. +** +** Format: +** Cache: +** "URL1": "FileName1" +** "URL2": "FileName2" +** ... +** Checksum: +** +** Example Output: +** Cache: +** "https://i.scdn.co/image/ab67616d00001e02203e4c6a048df02a21cdd813": "/sc_album_art_002" +** "https://i.scdn.co/image/ab67616d00001e02ccdddd46119a4ff53eaf1f5d": "/sc_album_art_003" +** "https://i.scdn.co/image/ab67616d00001e02e85259a1cae29a8d91f2093d": "/sc_album_art_001" +** Checksum: 61 +** +** Notes: +** - The checksum is recalculated for the current cache state to +** ensure data integrity and consistency. +** +** =================================================================== +*/ +void SpotifyArtMgr::printCacheIndex(const char* tag) +{ + spLogD(tag, "------------------------------------------------------------------------------------------"); + spLogD(tag, "Cache:"); + + // If the cache is empty, indicate that + if (_cacheList.empty()) + { + spLogI(tag, " [Cache is empty]"); + return; + } + + // Prepare a string to calculate the checksum + String serializedData; + + // Print and serialize the cache data + for (const auto& url : _cacheList) + { + auto it = _urlToFileMap.find(url); + if (it != _urlToFileMap.end()) + { + spLogD(tag, " \"%s\": \"%s\"", url.c_str(), it->second.c_str()); + serializedData += url + ":" + it->second + "\n"; + } + else + { + spLogW(tag, " \"%s\": [No associated file found]", url.c_str()); + } + } + + // Calculate and print the checksum + // uint32_t checksum = calculateChecksum(serializedData.c_str(), serializedData.length()); + // spLogI(tag, "Checksum: %u", checksum); + spLogD(tag, "------------------------------------------------------------------------------------------"); + + String filePath = String(gCacheFileroot) + ".txt"; + SCFileIO::getInstance().hexDump(LOGTAG_CACHE, filePath.c_str()); +} + +/* +** =================================================================== +** calculateChecksum() +** Calculates a simple XOR-based checksum for a given data buffer. +** +** Parameters: +** data - Pointer to the data buffer to calculate the checksum for +** length - The length of the data buffer +** +** Returns: +** The calculated checksum as a 32-bit unsigned integer. +** +** Notes: +** - This checksum is based on a simple XOR operation, where each +** byte in the data buffer is XORed with the checksum value. +** - This method is lightweight and suitable for basic integrity +** checks, but it is not cryptographically secure. +** - Ensure that the checksum field itself is excluded from the data +** buffer being validated to avoid circular dependency issues. +** +** Example Usage: +** const char* data = "example data"; +** size_t length = strlen(data); +** uint32_t checksum = calculateChecksum(data, length); +** +** =================================================================== +*/ +uint32_t SpotifyArtMgr::calculateChecksum(const char* data, size_t length) +{ + uint32_t checksum = 0; + for (size_t i = 0; i < length; ++i) + { + checksum ^= data[i]; + } + return checksum; +} \ No newline at end of file diff --git a/src/SpotifyArtMgr.h b/src/SpotifyArtMgr.h new file mode 100644 index 0000000..7b1a072 --- /dev/null +++ b/src/SpotifyArtMgr.h @@ -0,0 +1,118 @@ +/*------------------------------------------------------------------------------------------------- +** +** SpotifyArtMgr.h +** +** Handles downloading and caching of Spotify album art. Ensures reliable +** display by managing secure certificate validation and efficient image +** caching with LRU eviction. Designed as a Singleton for global access. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2025-01-11 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ + +#pragma once + +#include +#include +#include +#include +#include + +#define SP_NO_COVER_JPG_FILENAME "/DefaultCoverArt.jpg" + +/* +** =================================================================== +** ArtMgrStats +** +** A struct to encapsulate statistics related to album art fetching +** and cache performance. +** +** Fields: +** totalFetches - The total number of album art fetch attempts +** cacheHits - The number of successful cache hits +** currentHitStreak - The current streak of consecutive cache hits +** longestHitStreak - The longest recorded streak of cache hits +** averageHitRate - The average cache hit rate over time +** +** Notes: +** - This struct is used to track the effectiveness of caching +** and provide insights into network requests vs. local cache use. +** - `cacheHits` should always be ≤ `totalFetches`. +** - The hit streak fields help identify caching efficiency trends. +** +** Example Usage: +** ArtMgrStats stats = SpotifyArtMgr::getInstance().getStats(); +** spLogI(LOGTAG_GENERAL, "Cache Hit Rate: %lld%%", stats.averageHitRate); +** +** =================================================================== +*/ +struct ArtMgrStats +{ + long long totalFetches = 0; + long long cacheHits = 0; + long long currentHitStreak = 0; + long long longestHitStreak = 0; + long long averageHitRate = 0; +}; + +/* +** =================================================================== +** SpotifyArtMgr +** +** Singleton class to manage downloading and caching of album art. +** Maintains a mapping of URLs to local file names. The cache has a +** configurable maximum size and follows a FIFO replacement policy +** when full. +** =================================================================== +*/ +class SpotifyArtMgr +{ +public: + static SpotifyArtMgr* getInstance(); + + void setMaxCacheSize(size_t maxSize); + size_t getMaxCacheSize(); + void saveCacheIndex(); + bool isCacheDirty(); + + ArtMgrStats getStats(); + + String acquireAlbumArt(const String& url); + String getLocalFileName(const String& url); + +private: + // Constructor (private for Singleton pattern) + SpotifyArtMgr(); + + // Private methods + void determineCacheSize(); + void loadCacheIndex(); + bool downloadFile(String url, String filename); + String generateLocalFileName(); + void evictOldestFile(); + void saveFileFromHTTPResponse(String filename, HTTPClient *pHttp ); + void printCacheIndex(const char* tag); + uint32_t calculateChecksum(const char* data, size_t length); + + // Singleton instance + static SpotifyArtMgr* gInstance; + + // Cache configuration + size_t _maxCacheSize = 10; // Default maximum cache size + + SemaphoreHandle_t xSemaphoreArtFetchCompleted = xSemaphoreCreateMutex();; + bool _isDownloadPending = false; + + // Data structures for caching + std::list _cacheList; // Maintain LRU order + std::map _urlToFileMap; // Map URLs to list iterators + size_t _nextFileId = 1; // Counter for generating file names + bool _isDirty = false; // Indicate whether there is something new to save + ArtMgrStats _stats; + bool _isHitStreakInProgress = false; +}; \ No newline at end of file diff --git a/src/SpotifyCertWrapper.cpp b/src/SpotifyCertWrapper.cpp new file mode 100644 index 0000000..292cbf8 --- /dev/null +++ b/src/SpotifyCertWrapper.cpp @@ -0,0 +1,53 @@ +/* +** =================================================================== +** SpotifyCertWrapper.cpp +** +** Purpose: +** This file is created to resolve multiple definition errors +** arising from the inclusion of `SpotifyArduinoCert.h` in +** multiple source files. The header file defines symbols +** (`spotify_server_cert` and `spotify_image_server_cert`) +** directly, which leads to multiple definitions at the linking +** stage when the header is included in more than one source file. +** +** Resolution: +** To avoid this issue, this wrapper file includes +** `SpotifyArduinoCert.h` in a single compilation unit. This +** ensures that the symbols are defined exactly once in the +** compiled output. +** +** Usage: +** - Include this file in the build system as part of the +** project compilation. +** - In other source files that need access to these symbols, +** use `extern` declarations instead of including +** `SpotifyArduinoCert.h` directly. +** +** Example: +** Instead of including `SpotifyArduinoCert.h`: +** #include "SpotifyArduinoCert.h" +** +** Use the following `extern` declarations: +** extern const char *spotify_server_cert; +** extern const char *spotify_image_server_cert; +** +** Rationale: +** By limiting the inclusion of `SpotifyArduinoCert.h` to this +** file, the project adheres to the "One Definition Rule" (ODR), +** a key principle in C++ that ensures symbols are defined +** exactly once. +** +** Notes: +** Failure to follow these guidelines will reintroduce the +** multiple definition errors during the linking stage. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2025-01-11 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ + +#include "SpotifyArduinoCert.h" \ No newline at end of file diff --git a/src/SpotifyPlayer.cpp b/src/SpotifyPlayer.cpp new file mode 100644 index 0000000..5a15253 --- /dev/null +++ b/src/SpotifyPlayer.cpp @@ -0,0 +1,792 @@ +/*------------------------------------------------------------------------------------------------- +** +** SpotifyPlayer.cpp +** +** Instances of this class control Spotify once it has started. +** This includes UI interactions. Due to dependencies used by +** this class it should be treated as a Singleton. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2024-12-28 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ + +#include +#include +#include +#include "esp_task_wdt.h" +#include "SpotifyPlayer.h" +#include "logTags.h" +#include "SCLogger.h" +#include "ThingPulse/spotify.h" +#include "Monitor.h" +#include "SpotifyArtMgr.h" +#include "SCFileIO.h" + +#include // Ensure you include the required decoder library + +// Spotify related +#define SP_SPOTIFY_MARKET "IE" + +/* +** =================================================================== +** Callback routine for spotify.getCurrentlyPlaying() method. +** =================================================================== +*/ +void SpotifyPlayer::getCurrentlyPlayingCallback(CurrentlyPlaying currentlyPlaying) +{ + spLogD(LOGTAG_PLAYER, "getCurrentlyPlayingCallback - callback routine for spotify.getCurrentlyPlaying() method."); + SpotifyPlayer::getInstance().refreshCurrentSong(currentlyPlaying); +} + +/* +** =================================================================== +** Constructor and Destructor +** =================================================================== +*/ +SpotifyPlayer::SpotifyPlayer() +{ + // Initialization code (if needed) +} + +SpotifyPlayer::~SpotifyPlayer() +{ +} + +/* +** =================================================================== +** getInstance() +** =================================================================== +*/ +SpotifyPlayer& SpotifyPlayer::getInstance() +{ + static SpotifyPlayer instance; // Guaranteed to be thread-safe in C++11 and later + return instance; +} + +/* +** =================================================================== +** initialize() +** =================================================================== +*/ + +void SpotifyPlayer::initialize(QueueHandle_t *pScuiQueue) +{ + _pScuiQueue = pScuiQueue; + + // keep reference since SpotifyArduino is dependent on values + // remaining in memory + _spotifyClientId = Vault::getInstance().getSpotifyClientID(); + _spotifyClientSecret = Vault::getInstance().getSpotifyClientSecret(); + + spotify.lateInit(_spotifyClientId.c_str(), _spotifyClientSecret.c_str(), _spotifyRefreshToken.c_str()); + + // client is defined in ThingPulse/spotify.h + client.setCACert(spotify_server_cert); + +} + +/* +** =================================================================== +** startBackgroundRefreshes() +** =================================================================== +*/ + +void SpotifyPlayer::startBackgroundRefreshes() +{ + + // The ESP32 is a dual-core processor with two Xtensa LX6 cores: + // + // - **Core 0 (PRO CPU)** → Handles system tasks (Wi-Fi, Bluetooth, FreeRTOS) + // - **Core 1 (APP CPU)** → Runs the Arduino framework (`setup()` and `loop()`) + // + // By default, all Arduino code executes on **Core 1**. + + // Create task to refresh song information + spLogI(LOGTAG_MULTITASK, "creating background task - pinned to core 1"); + + // Note: Core 0 had stability issues despite best attempts + // to address. Moved UI loop which doesn't block the way + // that the song refresh logic does to run on Core 0. + xTaskCreatePinnedToCore( + SpotifyPlayer::refreshCurrentSongTask, // Task function + "SongRefresh", // Task name + 8192, // Stack size + NULL, // Task parameter + 1, // Task priority + &_refreshTaskHandle, // Task handle + 1 // Core ID + ); + + spLogI(LOGTAG_MULTITASK, "background task created"); + +} + +/* +** =================================================================== +** isRefreshTokenAvailable() +** +** Returns whether the refresh token is available. This will also +** internally initialize the token if it exists. +** =================================================================== +*/ +bool SpotifyPlayer::isRefreshTokenAvailable() +{ + _spotifyRefreshToken = SCFileIO::getInstance().readFsString(SPOTIFY_REFRESH_TOKEN_FILE_NAME); + + if (_spotifyRefreshToken == "") + { + spLogI(LOGTAG_PLAYER, "Spotify refresh token not found."); + return false; + } + else + { + spLogI(LOGTAG_PLAYER, "Spotify refresh token found."); + return true; + } +} + +/* +** =================================================================== +** requestRefreshToken() +** +** Requests a fresh refresh token. A refresh token is a security +** credential that allows client applications to obtain new access +** tokens without requiring users to reauthorize the application. +** =================================================================== +*/ +bool SpotifyPlayer::requestRefreshToken() +{ + + spLogI(LOGTAG_PLAYER, "Requesting Spotify refresh token through the browser via auth code."); + + String spotifyAuthCode = fetchSpotifyAuthCode(); + _spotifyRefreshToken = spotify.requestAccessTokens(spotifyAuthCode.c_str(), SPOTIFY_REDIRECT_URI); + SCFileIO::getInstance().saveFsString(SPOTIFY_REFRESH_TOKEN_FILE_NAME, _spotifyRefreshToken); + + return true; + +} + +/* +** =================================================================== +** getNodeName() +** =================================================================== +*/ +String SpotifyPlayer::getNodeName() +{ + return SPOTIFY_ESPOTIFIER_NODE_NAME; +} + +/* +** =================================================================== +** login() +** =================================================================== +*/ +void SpotifyPlayer::login() +{ + // The Spotify library + // - keeps track of the refresh token and its TTL internally + // - automatically renews the actual access token using the refresh token + // -> see SpotifyArduino.h#autoTokenRefresh and SpotifyArduino::checkAndRefreshAccessToken() (called before every API function) + spotify.setRefreshToken(_spotifyRefreshToken.c_str()); + spotify.refreshAccessToken(); + spLogI(LOGTAG_PLAYER, "Authentication against Spotify done. Refresh token: %s", _spotifyRefreshToken.c_str()); +} + +/* +** =================================================================== +** postScuiMessage() +** +** Sends a message to the SCUI queue with a specified type, +** string data, and an integer value. The integer is appended +** to the message data. +** +** Parameters: +** type - The type of SCUI message. +** str - The string data to send. +** num - The integer value to append to the message. +** +** Returns: +** None +** =================================================================== +*/ +void SpotifyPlayer::postScuiMessage(SCUIMessageType type, const String& str, int num) +{ + if (_pScuiQueue != NULL) + { + SCUIMessage msg; + msg.type = type; + msg.str = str; + msg.num = num; + + Monitor::start(MONITOR_ID_SCUI_QUEUE_DELAY, LOGTAG_MULTITASK, "postScuiMessage()"); + if (xQueueSend(*_pScuiQueue, &msg, pdMS_TO_TICKS(10)) != pdPASS) + { + spLogE(LOGTAG_PLAYER, "Failed to send SCUI message to queue"); + } + } +} + +/* +** =================================================================== +** nextSong() +** =================================================================== +*/ +void SpotifyPlayer::nextSong() +{ + // Code to skip to the next song using Spotify's API + // Example: Sending an API request or invoking a method in the client + spLogI(LOGTAG_PLAYER, "Skipping to the next song."); + + postScuiMessage(SCUIMessageType::UM_STATUS_BOX, + "Making Call", + static_cast(TFTColor::SC_NetworkInProgress)); + if (xSemaphoreTake(_xSemaphoreNetwork, portMAX_DELAY)) + { + if (spotify.nextTrack()) + { + postScuiMessage(SCUIMessageType::UM_STATUS_BOX, + "", + static_cast(TFTColor::SC_NetworkSuccess)); + spLogD(LOGTAG_PLAYER, "next track call succesful"); + } + else + { + postScuiMessage(SCUIMessageType::UM_STATUS_BOX, + "", + static_cast(TFTColor::SC_NetworkFailure)); + spLogD(LOGTAG_PLAYER, "next track call failed"); // + } + xSemaphoreGive(_xSemaphoreNetwork); + } + else + { + spLogI(LOGTAG_MULTITASK,"Unable to take _xSemaphoreNetwork."); + } + +} + +/* +** =================================================================== +** previousSong() +** =================================================================== +*/ +void SpotifyPlayer::previousSong() +{ + // Code to go back to the previous song using Spotify's API + // Example: Sending an API request or invoking a method in the client + spLogI(LOGTAG_PLAYER, "Going back to the previous song."); + + postScuiMessage(SCUIMessageType::UM_STATUS_BOX, + "Making Call", + static_cast(TFTColor::SC_NetworkInProgress)); + + if (xSemaphoreTake(_xSemaphoreNetwork, portMAX_DELAY)) + { + if (spotify.previousTrack()) + { + postScuiMessage(SCUIMessageType::UM_STATUS_BOX, + "", + static_cast(TFTColor::SC_NetworkSuccess)); + spLogD(LOGTAG_PLAYER, "previous track call succesful"); + } + else + { + postScuiMessage(SCUIMessageType::UM_STATUS_BOX, + "", + static_cast(TFTColor::SC_NetworkFailure)); + spLogD(LOGTAG_PLAYER, "previous track call failed"); + } + xSemaphoreGive(_xSemaphoreNetwork); + } + else + { + spLogI(LOGTAG_MULTITASK,"Unable to take _xSemaphoreNetwork."); + } + + +} + +/* +** =================================================================== +** pauseSong() +** =================================================================== +*/ +void SpotifyPlayer::pauseSong() +{ + // Code to pause playback using Spotify's API + // Example: Sending an API request or invoking a method in the client + spLogI(LOGTAG_PLAYER, "Pausing the current song."); + + if (xSemaphoreTake(_xSemaphoreNetwork, portMAX_DELAY)) + { + if (_isPlaying) + { + postScuiMessage(SCUIMessageType::UM_STATUS_BOX, + "Making Call", + static_cast(TFTColor::SC_NetworkInProgress)); + if (spotify.pause()) + { + postScuiMessage(SCUIMessageType::UM_STATUS_BOX, + "", + static_cast(TFTColor::SC_NetworkSuccess)); + spLogD(LOGTAG_PLAYER, "pause track call succesful"); + } + else + { + postScuiMessage(SCUIMessageType::UM_STATUS_BOX, + "", + static_cast(TFTColor::SC_NetworkFailure)); + spLogD(LOGTAG_PLAYER, "pause track call failed"); // + } + } + else + { + postScuiMessage(SCUIMessageType::UM_STATUS_BOX, + "Making Call", + static_cast(TFTColor::SC_NetworkInProgress)); + if (spotify.play()) + { + postScuiMessage(SCUIMessageType::UM_STATUS_BOX, + "", + static_cast(TFTColor::SC_NetworkSuccess)); + spLogD(LOGTAG_PLAYER, "pause track call succesful"); + } + else + { + postScuiMessage(SCUIMessageType::UM_STATUS_BOX, + "", + static_cast(TFTColor::SC_NetworkFailure)); + spLogD(LOGTAG_PLAYER, "pause track call failed"); // + } + } + xSemaphoreGive(_xSemaphoreNetwork); + } + else + { + spLogI(LOGTAG_MULTITASK,"Unable to take _xSemaphoreNetwork."); + } +} + +/* +** =================================================================== +** refreshCurrentTrack() +** =================================================================== +*/ + +void SpotifyPlayer::refreshCurrentTrack() +{ + spLogD(LOGTAG_MULTITASK, "Free Heap: "); + spLogD(LOGTAG_MULTITASK, "%d", ESP.getFreeHeap()); + + postScuiMessage(SCUIMessageType::UM_STATUS_BOX, + "Making Call", + static_cast(TFTColor::SC_NetworkInProgress)); + + spLogI(LOGTAG_MULTITASK, "getting currently playing song:"); + // Market can be excluded if you want e.g. spotify.getCurrentlyPlaying() + int status = -777; + + if (xSemaphoreTake(_xSemaphoreNetwork, portMAX_DELAY)) + { + spLogI(LOGTAG_MULTITASK, "Invoking spotify.getCurrentlyPlaying(...)"); + Monitor::start(MONITOR_ID_SPOTIFY_GET_CURRENTLY_PLAYING, LOGTAG_METRICS, "spotify.getCurrentlyPlaying(...)"); + status = spotify.getCurrentlyPlaying(SpotifyPlayer::getCurrentlyPlayingCallback, SP_SPOTIFY_MARKET); + Monitor::stop(MONITOR_ID_SPOTIFY_GET_CURRENTLY_PLAYING); + xSemaphoreGive(_xSemaphoreNetwork); + } + else + { + spLogI(LOGTAG_MULTITASK,"Unable to take _xSemaphoreNetwork."); + } + + + if (status == 200) + { + spLogI(LOGTAG_MULTITASK, "Successfully refreshed current song."); + postScuiMessage(SCUIMessageType::UM_STATUS_BOX, + "Successful", + static_cast(TFTColor::SC_NetworkSuccess)); + } + else if (status == 204) + { + spLogI(LOGTAG_MULTITASK, "Doesn't seem to be anything playing"); + postScuiMessage(SCUIMessageType::UM_STATUS_BOX, + "Nothing playing", + static_cast(TFTColor::SC_AlertStatus)); + + } + else if (status == -777) + { + spLogI(LOGTAG_MULTITASK, "No, really, unable to get semaphore. status still -777"); + postScuiMessage(SCUIMessageType::UM_STATUS_BOX, + "Unable to get semaphore", + static_cast(TFTColor::SC_AlertStatus)); + } + else + { + spLogE(LOGTAG_MULTITASK, "Error: "); + spLogE(LOGTAG_MULTITASK, "Status: %d", status); + postScuiMessage(SCUIMessageType::UM_STATUS_BOX, + "Failure", + static_cast(TFTColor::SC_NetworkFailure)); + } + + refreshCoverArt(); + + if (_isNewTrack) + { + // Track is loaded and copied. + // At least one pass made at downloading the art. + _isNewTrackReady = true; + } + + postScuiMessage(SCUIMessageType::UM_PLAYER_REFRESH, + "Refresh", + _isNewTrackReady); +} + +/* +** =================================================================== +** refreshCoverArt() +** +** This method handles refreshing the cover art for the currently +** playing track. It checks if a new track is detected or if cover +** art is unavailable and downloads the appropriate album art. If +** the download is successful, it updates the background color to +** match the average color of the album art. It also ensures the +** UI is marked dirty to trigger a repaint. +** +** Parameters: +** None +** +** Returns: +** None +** +** Notes: +** - The method determines if the currently playing track has +** changed by comparing track URIs. +** - If cover art is downloaded successfully, the background +** color is updated based on the average color of the image. +** - UI is marked dirty whenever the cover art is refreshed. +** +** =================================================================== +*/ + +void SpotifyPlayer::refreshCoverArt() +{ + bool bNewTrack = false; + + if (_currentTrackUri != _currentlyPlayingMetadata.trackUri) + { + spLogI(LOGTAG_GENERAL, "New song detected: %s", _currentlyPlayingMetadata.trackName); + bNewTrack = true; + _isCoverArtAvailable = false; + _currentTrackUri = _currentlyPlayingMetadata.trackUri; + } + + // if new track, download and draw it + spLogD(LOGTAG_GENERAL, "if (bNewTrack || !_isCoverArtAvailable)... bNewTrack=%d, _isCoverArtAvailable=%d", (int)bNewTrack, (int)_isCoverArtAvailable); + if (bNewTrack + || !_isCoverArtAvailable) + { + const int TARGET_IMAGE_SIZE = 300; + String filePath; + + for (int i = 0; i < _currentlyPlayingMetadata.numImages; i++) + { + if ((_currentlyPlayingMetadata.albumImages[i].height == TARGET_IMAGE_SIZE) + && (_currentlyPlayingMetadata.albumImages[i].width == TARGET_IMAGE_SIZE)) + { + spLogV(LOGTAG_GENERAL, "*** Attempting image download ***"); + + postScuiMessage(SCUIMessageType::UM_DOWNLOAD_BOX,"",false); + Monitor::start(MONITOR_ID_FETCH_ALBUM_ART, LOGTAG_METRICS, "Download Art If Needed."); + filePath = SpotifyArtMgr::getInstance()->acquireAlbumArt(_currentlyPlayingMetadata.albumImages[i].url); + Monitor::stop(MONITOR_ID_FETCH_ALBUM_ART); + postScuiMessage(SCUIMessageType::UM_DOWNLOAD_BOX,"",true); + + // Check if the acquired file is valid + if (filePath != SP_NO_COVER_JPG_FILENAME + && !filePath.isEmpty()) + { + _isCoverArtAvailable = true; + spLogV(LOGTAG_GENERAL, "*** Cover art is available: %s ***", filePath.c_str()); + } + else + { + _isCoverArtAvailable = false; + spLogW(LOGTAG_GENERAL, "*** Cover art is unavailable ***"); + } + + spLogV(LOGTAG_GENERAL, "*** downloadFile() finished executing ***"); + break; + } + } + if (_isCoverArtAvailable) + { + // TFTColor c = _pUI->calculateAverageColor(filePath.c_str()); + // _pUI->setBackground(c, false); + } + else + { + spLogI(LOGTAG_MULTITASK, "*** %dx%d image not found to download. ***",TARGET_IMAGE_SIZE,TARGET_IMAGE_SIZE); + } + + // request updated UI + postScuiMessage(SCUIMessageType::UM_MARK_DIRTY, + "", + true); + + } + +} + +/* +** =================================================================== +** refreshCurrentSong() +** +** This is the callback for what is playing from the Spotify API. +** =================================================================== +*/ +void SpotifyPlayer::refreshCurrentSong(CurrentlyPlaying currentlyPlaying) +{ + + spLogI(LOGTAG_MULTITASK, "Refreshing current song. SpotifyPlayer::refreshCurrentSong(CurrentlyPlaying currentlyPlaying)"); + + // If not a track or episode, update the UI accordingly and indicate music isn't available + if ((!currentlyPlaying.currentlyPlayingType == SpotifyPlayingType::track) + && (!currentlyPlaying.currentlyPlayingType == SpotifyPlayingType::episode)) + { + spLogI(LOGTAG_GENERAL, " _isMusicAvailable set to false. currentlyPlayingType is not a supported type." ); + _isMusicAvailable = false; + // force a refresh to get the waiting message + postScuiMessage(SCUIMessageType::UM_MARK_DIRTY, + "", + true); + return; + } + + if (xSemaphoreTake(_xSemaphoreDataCopy, portMAX_DELAY)) + { + // copy currentlyPlaying over for use + if (currentlyPlaying.trackUri == nullptr) + { + spLogV(LOGTAG_GENERAL, "currentlyPlaying.trackUri == nullptr. skipping new track ready logic."); + } + else + { + if (strcmp(currentlyPlaying.trackUri, _currentlyPlayingMetadata.trackUri) != 0) + { + // New track has just been loaded + _isNewTrack = true; + spLogV(LOGTAG_GENERAL, "New Track!!! _isNewTrack = true"); + } + else + { + _isNewTrack = false; + } + } + _currentlyPlayingMetadata.copyFrom(currentlyPlaying); + xSemaphoreGive(_xSemaphoreDataCopy); + } + else + { + spLogI(LOGTAG_MULTITASK, "Unable to take _xSemaphoreDataCopy."); + } + + _isPlaying = currentlyPlaying.isPlaying; + + if (currentlyPlaying.trackName == nullptr) + { + spLogI(LOGTAG_MULTITASK, "refreshCurrentSong() - Empty track detected"); + _isMusicAvailable = false; + // force a refresh to get the waiting message + postScuiMessage(SCUIMessageType::UM_MARK_DIRTY, + "", + true); + return; + } + else + { + // spLogI(LOGTAG_GENERAL, " _isMusicAvailable = true; currentlyPlaying.trackName ='%s' with length = %u", currentlyPlaying.trackName, strlen(currentlyPlaying.trackName) ); + _isMusicAvailable = true; + printCurrentlyPlayingToSerial(currentlyPlaying); + } + +} + +/* +** =================================================================== +** isMusicAvailable() +** Answer whether music is currently available to display. +** =================================================================== +*/ +bool SpotifyPlayer::isMusicAvailable() +{ + return _isMusicAvailable; +} + +/* +** =================================================================== +** printCurrentlyPlayingToSerial() +** Prints the currently playing track details to serial output. +** =================================================================== +*/ +void SpotifyPlayer::printCurrentlyPlayingToSerial(CurrentlyPlaying currentlyPlaying) +{ + + spLogI(LOGTAG_SONG_DATA, "--------- Currently Playing ---------"); + + spLogI(LOGTAG_SONG_DATA, "Track: %s", currentlyPlaying.trackName); + spLogV(LOGTAG_SONG_DATA, ""); + + spLogV(LOGTAG_SONG_DATA, "Artists: "); + for (int i = 0; i < currentlyPlaying.numArtists; i++) + { + spLogV(LOGTAG_SONG_DATA, " Name: %s", currentlyPlaying.artists[i].artistName); + spLogV(LOGTAG_SONG_DATA, ""); + } + + spLogV(LOGTAG_SONG_DATA, "Album: %s", currentlyPlaying.albumName); + spLogV(LOGTAG_SONG_DATA, ""); + + spLogI(LOGTAG_SONG_DATA, "------------------------"); + +} + +/* +** =================================================================== +** getCurrentlyPlayingMetadata() +** Returns a stable copy of the currently playing metadata. +** This method ensures that the returned data does not change +** unexpectedly while in use. It will have the side effect +** of reseting isNewTrackReady() to false. +** +** Returns: +** A reference to the stable DTO copy of PlayingMetadata. +** =================================================================== +*/ +const PlayingMetadata &SpotifyPlayer::getCurrentlyPlayingMetadata() +{ + copyMetadataToDTO(); + return _currentlyPlayingMetadataDTO; +} + +/* +** =================================================================== +** copyMetadataToDTO() +** Copies the currently playing metadata into a stable DTO. +** This method ensures thread safety by using a semaphore +** to prevent data races. +** +** Locks: +** - _xSemaphoreDataCopy: Ensures safe access to shared data. +** +** Behavior: +** - If the semaphore is available, it performs a deep copy. +** - If the semaphore is not available, logs an informational message. +** =================================================================== +*/ +void SpotifyPlayer::copyMetadataToDTO() +{ + if (xSemaphoreTake(_xSemaphoreDataCopy, portMAX_DELAY)) + { + // copy currentlyPlaying over for use + _currentlyPlayingMetadataDTO.copyFrom(_currentlyPlayingMetadata); + _isNewTrackReady = false; + xSemaphoreGive(_xSemaphoreDataCopy); + } + else + { + spLogI(LOGTAG_MULTITASK, "Unable to take _xSemaphoreDataCopy."); + } + +} + +/* +** =================================================================== +** isNewTrackReady() +** Answers whether a new track has become ready and +** getCurrentlyPlayingMetadata() should be called to populdate +** the DTO. +** +** =================================================================== +*/ +const bool SpotifyPlayer::isNewTrackReady() +{ + return _isNewTrackReady; +} + +void SpotifyPlayer::saveCache() +{ + + SpotifyArtMgr *pSAM = SpotifyArtMgr::getInstance(); + + if (pSAM->isCacheDirty()) + { + postScuiMessage(SCUIMessageType::UM_STATUS_BOX, + "", + static_cast(TFTColor::SC_CacheSave)); + pSAM->saveCacheIndex(); // persist the updated file index + } + + // clear status. this assumes that this is the last + // update which slightly breaks encapsulation and + // is also brittle; however, not sure there is a cleaner + // approach that isn't worse. + postScuiMessage(SCUIMessageType::UM_STATUS_BOX, + "", + -1); // -1 will use background color + +} + +/* +** =================================================================== +** refreshCurrentSongTask() +** Background task responsible for periodically refreshing the +** currently playing song information from Spotify. The task +** includes a blocking HTTP call to fetch data and resets the +** Task Watchdog Timer to prevent timeout. +** +** Parameters: +** pvParameters - Pointer to task parameters (not used in this task). +** +** Notes: +** - Executes in an infinite loop with a specified delay between +** iterations to avoid overloading the system. +** - Calls the refreshCurrentTrack() method of SpotifyPlayer to +** perform the actual data retrieval. +** - Resets the Task Watchdog Timer on each iteration. +** =================================================================== +*/ + +void SpotifyPlayer::refreshCurrentSongTask(void *pvParameters) +{ + spLogI(LOGTAG_MULTITASK, "background task executing. about to enter loop."); + // TODO: work this out better so it just simply starts when ready + vTaskDelay(pdMS_TO_TICKS(2000)); // 2000 ms delay to get started and wait for everything to process + while (true) { + spLogI(LOGTAG_MULTITASK, "refreshCurrentSongTask is running"); + + // Feed the Task Watchdog to avoid timeout + esp_task_wdt_reset(); + + // Perform the task (blocking HTTP call) + SpotifyPlayer::getInstance().refreshCurrentTrack(); + + // Give the UI time to refresh since might be marked dirty + // Also give time for other tasks to do work. + vTaskDelay(pdMS_TO_TICKS(1500)); + + SpotifyPlayer::getInstance().saveCache(); + + // Allow other tasks to execute + vTaskDelay(pdMS_TO_TICKS(500)); + } +} \ No newline at end of file diff --git a/src/SpotifyPlayer.h b/src/SpotifyPlayer.h new file mode 100644 index 0000000..e2b8e16 --- /dev/null +++ b/src/SpotifyPlayer.h @@ -0,0 +1,85 @@ +/*------------------------------------------------------------------------------------------------- +** +** SpotifyPlayer.cpp +** +** Instances of this class control Spotify once it has started. +** This includes UI interactions. Due to dependencies used by +** this class it should be treated as a Singleton. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2024-12-28 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ +#pragma once + +#include +#include "DisplayUI.h" +#include "PlayingMetadata.h" +#include "scui.h" +class SpotifyPlayer { +public: + // Public method to access the singleton instance + static SpotifyPlayer& getInstance(); + + // initialization related + void initialize(QueueHandle_t *pScuiQueue); + bool isRefreshTokenAvailable(); + bool requestRefreshToken(); + String getNodeName(); + void login(); + void startBackgroundRefreshes(); + + + // controls + void nextSong(); + void previousSong(); + void pauseSong(); + + // status + bool isMusicAvailable(); + + // Call this to get the stable DTO copy + const PlayingMetadata &getCurrentlyPlayingMetadata(); + + const bool isNewTrackReady(); + +private: + // Member variables + String _spotifyRefreshToken = ""; + QueueHandle_t *_pScuiQueue; + String _currentTrackUri = ""; // Tracks the currently playing song + bool _isPlaying = false; + bool _isCoverArtAvailable = false; + bool _isMusicAvailable = false; + bool _isNewTrackReady = false; + bool _isNewTrack = false; + PlayingMetadata _currentlyPlayingMetadata; // Frequently updated instance + PlayingMetadata _currentlyPlayingMetadataDTO; // Stable DTO instance + SemaphoreHandle_t _xSemaphoreNetwork = xSemaphoreCreateMutex(); + SemaphoreHandle_t _xSemaphoreDataCopy = xSemaphoreCreateMutex(); + TaskHandle_t _refreshTaskHandle; + String _spotifyClientId; + String _spotifyClientSecret; + + // Methods + + // Private constructor and destructor + SpotifyPlayer(); + ~SpotifyPlayer(); + + static void getCurrentlyPlayingCallback(CurrentlyPlaying currentlyPlaying); + static void refreshCurrentSongTask(void *pvParameters); + + void copyMetadataToDTO(); + void printCurrentlyPlayingToSerial(CurrentlyPlaying currentlyPlaying); + void refreshCurrentTrack(); + void refreshCurrentSong(CurrentlyPlaying currentlyPlaying); // Call back + void refreshCoverArt(); + void postScuiMessage(SCUIMessageType type, const String& str, int num); + void saveCache(); + +}; diff --git a/src/ThingPulse/connectivity.cpp b/src/ThingPulse/connectivity.cpp new file mode 100644 index 0000000..3863e74 --- /dev/null +++ b/src/ThingPulse/connectivity.cpp @@ -0,0 +1,49 @@ +/*------------------------------------------------------------------------------------------------- +** +** connectivity.cpp +** +** Provides connectivity routines for initializing Wi-Fi with encrypted +** credentials. Ensures safe access to network parameters and reports +** connection status for the ESP32 Spotify Remote. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2024-12-26 - Electric Diversions - Copied and renamed to tpConnectivity.h from connectivity.h +** 2024-12-27 - Electric Diversions - Reintroduced connectivity.cpp from separated header. +** 2025-05-04 - Electric Diversions - Moved to ThingPulse folder. +** ------------------------------------------------------------------------------------------------ +*/ + +#include + +#include "connectivity.h" +#include "ThingPulse/util.h" +#include "../Vault.h" + +/* +** =================================================================== +** startWiFi() +** Attempt to connect to the configured WiFi network +** =================================================================== +*/ +void startWiFi() { + + String ssid = Vault::getInstance().getSSID(); + +// log_i("WiFi ssid/pw '%s' '%s'", ssid.c_str(), Vault::getInstance().getWiFiPassword().c_str()); + + WiFi.begin(ssid, Vault::getInstance().getWiFiPassword()); + + log_i("Connecting to WiFi '%s'...", ssid); + while (WiFi.status() != WL_CONNECTED) { + //log_i("."); + Serial.print("."); + delay(200); + } + log_i(""); + log_i("...done. IP: %s, WiFi RSSI: %d.", WiFi.localIP().toString().c_str(), WiFi.RSSI()); +} + diff --git a/src/ThingPulse/connectivity.h b/src/ThingPulse/connectivity.h new file mode 100644 index 0000000..7c72290 --- /dev/null +++ b/src/ThingPulse/connectivity.h @@ -0,0 +1,24 @@ +/*------------------------------------------------------------------------------------------------- +** +** connectivity.h +** +** Provides connectivity routines for initializing Wi-Fi with encrypted +** credentials. Ensures safe access to network parameters and reports +** connection status for the ESP32 Spotify Remote. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2024-12-26 - Electric Diversions - Copied and renamed to tpConnectivity.h from connectivity.h +** 2024-12-27 - Electric Diversions - Reintroduced connectivity.cpp from separated header. +** 2025-05-04 - Electric Diversions - Moved to ThingPulse folder. +** ------------------------------------------------------------------------------------------------ +*/ + + +#pragma once + +// Start the WiFi +void startWiFi(); \ No newline at end of file diff --git a/src/display.h b/src/ThingPulse/display.h similarity index 64% rename from src/display.h rename to src/ThingPulse/display.h index 06802d2..87a7d06 100644 --- a/src/display.h +++ b/src/ThingPulse/display.h @@ -1,9 +1,24 @@ -// SPDX-FileCopyrightText: 2023 ThingPulse Ltd., https://thingpulse.com -// SPDX-License-Identifier: MIT +/*------------------------------------------------------------------------------------------------- +** +** display.h +** +** Display routines provided by the original ThingPulse project. +** Includes TFT and touch screen initialization, backlight control, +** and display diagnostics for the ESP32 Spotify Remote. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2024-12-26 - Electric Diversions - Copied and renamed to tpDisplay.h from display.h +** 2025-05-04 - Electric Diversions - Renamed back to display.h and moved to ThingPulse folder +** ------------------------------------------------------------------------------------------------ +*/ #pragma once -#include +#include "FT6236TouchController/FT6236.h" #include #include "settings.h" @@ -12,8 +27,14 @@ // Calling tft.getSetup(user) populates it with the settings setup_t user; +// declare function prototype uint8_t readRegister8(uint8_t reg); +/* +** =================================================================== +** initTft - Initialize the TFT display +** =================================================================== +*/ void initTft(TFT_eSPI *tft) { tft->init(); tft->setRotation(TFT_ROTATION); @@ -34,7 +55,13 @@ void initTft(TFT_eSPI *tft) { tft->fillScreen(TFT_BLACK); } +/* +** =================================================================== +** initTouchScreen - Initialize the touch screen +** =================================================================== +*/ void initTouchScreen(FT6236 *ts) { + log_i("***** Entered... initTouchScreen()"); if (ts->begin(TOUCH_SENSITIVITY, TOUCH_SDA, TOUCH_SCL)) { log_i("Capacitive touch started."); } else { @@ -43,6 +70,11 @@ void initTouchScreen(FT6236 *ts) { ts->setRotation(TOUCH_ROTATION); } +/* +** =================================================================== +** logDisplayDebugInfo - dump what we know about the TFT display +** =================================================================== +*/ void logDisplayDebugInfo(TFT_eSPI *tft) { tft->getSetup(user); @@ -77,6 +109,11 @@ void logDisplayDebugInfo(TFT_eSPI *tft) { log_i("Sensitivity: %d (threshold)", readRegister8(FT6236_REG_THRESHHOLD)); } +/* +** =================================================================== +** readRegister8 - +** =================================================================== +*/ uint8_t readRegister8(uint8_t reg) { uint8_t x; diff --git a/src/ThingPulse/spotify.h b/src/ThingPulse/spotify.h new file mode 100644 index 0000000..f42f626 --- /dev/null +++ b/src/ThingPulse/spotify.h @@ -0,0 +1,153 @@ +/*------------------------------------------------------------------------------------------------- +** +** spotify.h +** +** Utility routines from ThingPulse for Spotify integration. This includes +** OAuth authentication, certificate setup, and serving a local web +** interface to acquire an auth code via redirect URI. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2024-12-27 - Electric Diversions - Copied and renamed to tpSpotify.h from spotify.h +** 2025-05-04 - Electric Diversions - Renamed back to spotify.h and moved to ThingPulse folder +** 2025-06-07 - Electric Diversions - Refactor to scope SpotifyArduino instance to SpotifyPlayer +** ------------------------------------------------------------------------------------------------ +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include "Vault.h" + +extern const char *spotify_server_cert; +extern const char *spotify_image_server_cert; + +// Use http://.local/callback/ as the redirect URI for the app on Spotify. +// Hence, the default URI is http://tp-spotify.local/callback/. +// If you change the value here, you need to modify the redirect URI on Spotify as well. +#define SPOTIFY_ESPOTIFIER_NODE_NAME "tp-spotify" + +#define SPOTIFY_REFRESH_TOKEN_FILE_NAME "/refresh-token.txt" +// the '/callback/' path is essential as spotify.h#fetchSpotifyAuthCode() registers a handler for it +#define SPOTIFY_REDIRECT_URI "http%3A%2F%2F" SPOTIFY_ESPOTIFIER_NODE_NAME ".local%2Fcallback%2F" + +String authCode = ""; +String scope = "user-read-playback-state%20user-modify-playback-state"; +WebServer server(80); +WiFiClientSecure client; + +// Note: SpotifyArduino does not initialize its _refreshToken pointer. +// When declared as a global, this works because globals are zero-initialized by default, +// making _refreshToken safely nullptr. If the instance is created dynamically or locally, +// this assumption can lead to heap corruption when delete is called on an uninitialized +// _refreshToken in setRefreshToken(). +SpotifyArduino spotify(client); + +const char *webpageTemplate = + R"( + + + + + + + + +
+ Click to load Spotify authentication code +
+ + +)"; + +void handleCallback() { + log_i("###### handleCallback()."); + String code = ""; + for (uint8_t i = 0; i < server.args(); i++) { + if (server.argName(i) == "code") { + authCode = server.arg(i); + } + } + + if (authCode == "") { + server.send(404, "text/plain", "Failed to fetch Spotify authentication code, check serial monitor. Maybe go back in browser history and try again."); + } else { + server.send(200, "text/plain", "Succesfully fetched Spotify authentication code. Follow instructions on device."); + } +} + +void handleFavicon() { + log_i("*** Entering handleFavicon()"); + server.send(200, "image/vnd.microsoft.icon", "00000100"); +} + +void handleNotFound() { + log_i("*** Entering handleNotFound()"); + String message = "File Not Found\n\n"; + message += "URI: "; + message += server.uri(); + message += "\nMethod: "; + message += (server.method() == HTTP_GET) ? "GET" : "POST"; + message += "\nArguments: "; + message += server.args(); + message += "\n"; + + for (uint8_t i = 0; i < server.args(); i++) { + message += " " + server.argName(i) + ": " + server.arg(i) + "\n"; + } + + log_e("%s", message.c_str()); + server.send(404, "text/plain", message); +} + +void printRootWebpage() { + char webpage[800]; + sprintf(webpage, webpageTemplate, Vault::getInstance().getSpotifyClientID().c_str(), SPOTIFY_REDIRECT_URI, scope.c_str()); + log_i("webpage: '%s",webpage); +} + +void handleRoot() { + log_i("*** Entering handleRoot()"); + char webpage[800]; + sprintf(webpage, webpageTemplate, Vault::getInstance().getSpotifyClientID().c_str(), SPOTIFY_REDIRECT_URI, scope.c_str()); + server.send(200, "text/html", webpage); +} + +String fetchSpotifyAuthCode() { + log_i("*** Entering fetchSpotifyAuthCode()"); + if (MDNS.begin(SPOTIFY_ESPOTIFIER_NODE_NAME)) { + log_i("MDNS responder started for node name '%s'.", SPOTIFY_ESPOTIFIER_NODE_NAME); + log_i("Open browser at http://%s.local", SPOTIFY_ESPOTIFIER_NODE_NAME); + } + + server.on("/", handleRoot); + server.on("/callback/", handleCallback); + server.on("/favicon.ico", handleFavicon); + server.onNotFound(handleNotFound); + server.begin(); + log_i("HTTP server started"); + + while (authCode == "") { + //log_i("--- calling server.handleClient()."); + server.handleClient(); + yield(); + } + + log_i("Successfully loaded Spotify authentication code: '%s'.", authCode.c_str()); + + log_i("Stopping HTTP server"); + server.stop(); + log_i("Stopping MDNS responder"); + MDNS.end(); + + return authCode; +} diff --git a/src/ThingPulse/util.cpp b/src/ThingPulse/util.cpp new file mode 100644 index 0000000..35d8914 --- /dev/null +++ b/src/ThingPulse/util.cpp @@ -0,0 +1,185 @@ +/*------------------------------------------------------------------------------------------------- +** +** util.cpp +** +** Utility routines from ThingPulse. Includes time synchronization, logging, +** timezone handling, memory diagnostics, and general-purpose helpers for +** the ESP32 Spotify Remote. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2024-12-26 - Electric Diversions - Copied and renamed to tpUtil.h from util.h +** 2024-12-27 - Electric Diversions - Added source file and split out function declarations +** 2025-05-04 - Electric Diversions - Renamed back to util and moved to ThingPulse folder +** ------------------------------------------------------------------------------------------------ +*/ + +#include // Brings in ESP32 logging and standard C library includes + +#include "util.h" +#include "time.h" +#include "settings.h" + +// Buffer to hold timestamp +char timestampBuffer[26]; + +/* +** =================================================================== +** getCurrentTimestamp() - Answer the current timestamp as a String +** =================================================================== +*/ +String getCurrentTimestamp(const char* format) { + struct tm timeinfo; + if (!getLocalTime(&timeinfo)) { + log_e("Failed to obtain time."); + return ""; + } + strftime(timestampBuffer, sizeof(timestampBuffer), format, &timeinfo); + return String(timestampBuffer); +} + +/* +** =================================================================== +** initTime() - initialize time from pool.ntp.org +** =================================================================== +*/ +boolean initTime() { + struct tm timeinfo; + + log_i("Synchronizing time."); + // Connect to NTP server with 0 TZ offset, call setTimezone() later + configTime(0, 0, "pool.ntp.org"); + // getLocalTime() uses a default timeout of 5s -> the loop takes at most 3*5s to + for (int i = 0; i < 3; i++) { + if (getLocalTime(&timeinfo)) { + log_i("UTC time: %s.", getCurrentTimestamp(SYSTEM_TIMESTAMP_FORMAT).c_str()); + return true; + } + } + + log_e("Failed to obtain time."); + return false; +} + +/* +** =================================================================== +** logBanner() - log the app and version in the log as info entries +** =================================================================== +*/ +void logBanner() { + log_i("=============================================="); + log_i("* Spotify Companion v%s *", VERSION); + log_i("* settings.h compile time: %s", COMPILE_TIME); + log_i("=============================================="); +} + +/* +** =================================================================== +** logMemoryStats() - log memory stats as info entries +** =================================================================== +*/ +void logMemoryStats() { + log_i("Total heap: %d", ESP.getHeapSize()); + log_i("Free heap: %d", ESP.getFreeHeap()); + log_i("Total PSRAM: %d", ESP.getPsramSize()); + log_i("Free PSRAM: %d", ESP.getFreePsram()); +} + +/* +** =================================================================== +** setTimezone() - set timezone +** =================================================================== +*/ +void setTimezone(const char* timezone) { + log_i("Setting timezone to '%s'.", timezone); + // Clock settings are adjusted to show the new local time + setenv("TZ", timezone, 1); + tzset(); +} + +/* +** =================================================================== +** setTimezone() - Answer days from epoch +** Algorithm: http://howardhinnant.github.io/date_algorithms.html +** =================================================================== +*/ +int days_from_epoch(int y, int m, int d) { + y -= m <= 2; + int era = y / 400; + int yoe = y - era * 400; // [0, 399] + int doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; // [0, 365] + int doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; // [0, 146096] + return era * 146097 + doe - 719468; +} + +/* +** =================================================================== +** mkgmtime() - Not sure... +** https://stackoverflow.com/a/58037981/131929 +** aka timegm() but that's already defined in the Weather Station +** lib but not accessible +** =================================================================== +*/ + +time_t mkgmtime(struct tm const *t) { + int year = t->tm_year + 1900; + int month = t->tm_mon; // 0-11 + if (month > 11) { + year += month / 12; + month %= 12; + } else if (month < 0) { + int years_diff = (11 - month) / 12; + year -= years_diff; + month += 12 * years_diff; + } + int days_since_epoch = days_from_epoch(year, month + 1, t->tm_mday); + + return 60 * (60 * (24L * days_since_epoch + t->tm_hour) + t->tm_min) + t->tm_sec; +} + +// ================== Electric Diversions added functons =========================== + +/* +** =================================================================== +** simpleDecrypt - Performs a simple decryption of provided string +** and key. +** +** Replace logic with decryption routing of choice. By default, +** this will just return back the same value that was passed +** in unmodified. +** +** =================================================================== +*/ + +void simpleDecrypt(const char *hex, const char *key, char *output) { + (void)key; // Mark key as unused to avoid compiler warnings + strcpy(output, hex); +} + +/* +** =================================================================== +** truncateString() +** Ensures a string isn't too long to display. This probably +** could go to something more general, but leaving it here for now. +** =================================================================== +*/ +String truncateString(const char* psz, size_t maxLength) +{ + // Check if the input string is shorter than or equal to maxLength + size_t inputLength = strlen(psz); + if (inputLength <= maxLength) { + return String(psz); // No truncation needed + } + + // Calculate the maximum length for characters before adding "..." + size_t truncatedLength = maxLength > 3 ? maxLength - 3 : 0; + + // Create a new String object with the truncated content and append "..." + String result = String(psz).substring(0, truncatedLength); + result += "..."; + + return result; // Return the truncated string +} \ No newline at end of file diff --git a/src/ThingPulse/util.h b/src/ThingPulse/util.h new file mode 100644 index 0000000..537e358 --- /dev/null +++ b/src/ThingPulse/util.h @@ -0,0 +1,102 @@ +/*------------------------------------------------------------------------------------------------- +** +** util.h +** +** Utility routines from ThingPulse. Includes time synchronization, logging, +** timezone handling, memory diagnostics, and general-purpose helpers for +** the ESP32 Spotify Remote. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2024-12-26 - Electric Diversions - Copied and renamed to tpUtil.h from util.h +** 2024-12-27 - Electric Diversions - Added source file and split out function declarations +** 2025-05-04 - Electric Diversions - Renamed back to util and moved to ThingPulse folder +** ------------------------------------------------------------------------------------------------ +*/ + + +#pragma once + +#include "ThingPulse/util.h" +#include "time.h" +#include "settings.h" + +/* +** =================================================================== +** getCurrentTimestamp() - Answer the current timestamp as a String +** =================================================================== +*/ +String getCurrentTimestamp(const char* format); + +/* +** =================================================================== +** initTime() - initialize time from pool.ntp.org +** =================================================================== +*/ +boolean initTime(); + +/* +** =================================================================== +** logBanner() - log the app and version in the log as info entries +** =================================================================== +*/ +void logBanner(); +/* +** =================================================================== +** logMemoryStats() - log memory stats as info entries +** =================================================================== +*/ +void logMemoryStats(); + +/* +** =================================================================== +** setTimezone() - set timezone +** =================================================================== +*/ +void setTimezone(const char* timezone); + +/* +** =================================================================== +** setTimezone() - Answer days from epoch +** Algorithm: http://howardhinnant.github.io/date_algorithms.html +** =================================================================== +*/ +int days_from_epoch(int y, int m, int d); + +/* +** =================================================================== +** mkgmtime() - Not sure... +** https://stackoverflow.com/a/58037981/131929 +** aka timegm() but that's already defined in the Weather Station +** lib but not accessible +** =================================================================== +*/ + +time_t mkgmtime(struct tm const *t); + +// ================== Electric Diversions added functons =========================== + +/* +** =================================================================== +** simpleDecrypt - Performs a simple decryption of provided string +** and key. +** +** Decryption portion of command line routine used to do simple encryption +** Note: This is largely Gen AI generated code. In the event of any weird +** point/memory/crash behavior dig deeper here. +** =================================================================== +*/ + +void simpleDecrypt(const char *hex, const char *key, char *output); + +/* +** =================================================================== +** truncateString() +** Ensures a string isn't too long to display. This probably +** could go to something more general, but leaving it here for now. +** =================================================================== +*/ +String truncateString(const char* psz, size_t maxLength); \ No newline at end of file diff --git a/src/UIViews/ClockView.cpp b/src/UIViews/ClockView.cpp new file mode 100644 index 0000000..c764c32 --- /dev/null +++ b/src/UIViews/ClockView.cpp @@ -0,0 +1,416 @@ +/*------------------------------------------------------------------------------------------------- +** +** ClockView.cpp +** +** Declares the ClockView class, a UIView subclass that displays the +** current time and date alongside minimal Spotify playback information. +** Includes progress bar handling, play status indication, and a return +** button for navigation. Intended as an idle display mode. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2025-02-12 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ + +#include "ClockView.h" +#include "SCLogger.h" +#include "logTags.h" +#include "ThingPulse/util.h" +#include "SpotifyArtMgr.h" +#include "Renderers/BackButtonRenderer.h" +#include "Vault.h" + +/* +** =================================================================== +** Constructor +** Initializes the CoverView with the provided DisplayUI instance. +** +** Parameters: +** pUI - Pointer to the DisplayUI instance. +** =================================================================== +*/ +ClockView::ClockView(DisplayUI *pUI) + : UIView(pUI) // Pass the DisplayUI pointer to the base class constructor +{ + // Initialization specific to CoverView, if any +} + +/* +** =================================================================== +** initializeUIElements() +** +** Initializes the interactive UI elements used in the ClockView, +** including the return button and album art tap zone. Enlarges the +** touch area of the back button to improve tap reliability. Also calls +** the base UIView initializer to set up any shared UI elements. +** +** Notes: +** - `_pReturnViewElement` uses BackButtonRenderer and a padded touch zone. +** - `_pGotoCoverArtElement` is a simple zone intended for tapping the album art. +** - Be sure to call this during view setup before rendering. +** =================================================================== +*/ +void ClockView::initializeUIElements() +{ + + std::shared_ptr backButtonRenderer = std::make_shared(_pUI); + // Make the touch rectangle 10 pixels larger on all sides. The renderer will take off the 10 pixels when rendering + // Doing to make a bigger, more forgiving touch target. + const int MARGIN = 10; + _pReturnViewElement = std::make_unique( 10 - MARGIN, 276 - MARGIN, 70 + (MARGIN * 2), 34 + (MARGIN * 2), backButtonRenderer); + _pGotoCoverArtElement = std::make_unique( 0, 160, 40, 40); + + // initialize other elements not set above + UIView::initializeUIElements(); + +} + +/* +** =================================================================== +** drawUI() +** Renders the UI for the default mode. +** =================================================================== +*/ +void ClockView::drawUI() +{ + + SpotifyPlayer *pSP = &SpotifyPlayer::getInstance(); + bool isNewTrack = pSP->isNewTrackReady(); // copy this before resetting it with getCurrentlyPlayingMetadata() + PlayingMetadata playing = pSP->getCurrentlyPlayingMetadata(); + bool isDisplayPercentageUpdated = false; + static TFTColor progressBarColor = TFTColor::DarkGreen; + + // Update clock + if (millis() > _requestDueTime) + { + handleDate(false); + _pUI->drawClockTime(Vault::getInstance().isUSDateTimeFormattingUsed(),false); + char s[50]; + snprintf(s, sizeof(s), "%s", getCurrentTimestamp("%Y-%m-%d %H:%M:%S").c_str()); + } + + isDisplayPercentageUpdated = refreshPlayingProgress(); + + static bool isNoMusicMsgShown = false; + if ((pSP->isMusicAvailable() == false) + && !isNoMusicMsgShown) + { + _pUI->drawTextToLCD("No Music Playing...", 200, 24, false); + _pUI->drawTextToLCD("", 230, 18, false); + _pUI->drawTextToLCD("", 254, 18, false); + isNoMusicMsgShown = true; + + // Render back button + TFTColor c = _pUI->getBackground(); + _pUI->setBackground(TFTColor::Black,false); + _pReturnViewElement->render(false); + _pUI->setBackground(c,false); + handlePlayStatus(true); + + return; + } + + isNoMusicMsgShown = false; + + if (isNewTrack + || _pUI->isUIDirty()) + { + + handleDate(true); + + _pUI->drawTextToLCD(truncateString(playing.trackName, 36).c_str(), 200, 24, false); + _pUI->drawTextToLCD(truncateString(playing.albumName, 50).c_str(),230, 18, false); + _pUI->drawTextToLCD(truncateString(playing.getArtistsList(800).c_str(), 50).c_str(),254, 18, false); + + // Render back button + TFTColor c = _pUI->getBackground(); + _pUI->setBackground(TFTColor::Black,false); + _pReturnViewElement->render(false); + _pUI->setBackground(c,false); + + String filePath = SP_NO_COVER_JPG_FILENAME; + + const int TARGET_IMAGE_SIZE = 300; + + for (int i = 0; i < playing.numImages; i++) + { + if ((playing.albumImages[i].height == TARGET_IMAGE_SIZE) + && (playing.albumImages[i].width == TARGET_IMAGE_SIZE)) + { + filePath = SpotifyArtMgr::getInstance()->getLocalFileName(playing.albumImages[i].url); + break; + } + } + + ///// Draw album art + progressBarColor = _pUI->calculateAverageColor(filePath.c_str()); + + _pUI->drawProgressBar(60, 170, 410, 20, 0, TFTColor::White, TFTColor::Black); + _pUI->drawProgressBar(60, 170, 410, 20, _displayPercentage, TFTColor::White, progressBarColor); + + _pUI->setJpgScaleToTiny(); + _pUI->drawAlbumArt(10, 160, filePath); + + ///// Draw play duration + _pUI->rDrawString("-:--", 330, 287, 20, TFTColor::DarkGreen, TFTColor::Black,"XX:XX:XX"); + std::string duration = "/"+_pUI->formatTime(playing.durationMs); + _pUI->lDrawString(duration.c_str(), 333, 287, 20, TFTColor::DarkGreen, TFTColor::Black,"XX:XX:XX"); + + handlePlayStatus(true); + + _pUI->markUIDirty(false); + } + + handleProgressBar(progressBarColor); + handlePlayStatus(false); + +} + +/* +** =================================================================== +** handleProgressBar() +** +** Updates the visual progress bar on the ClockView screen to reflect +** the current playback position. Uses `_displayPercentage` to determine +** how much of the bar to fill and updates the on-screen timestamp if it +** has changed since the last frame. +** +** Parameters: +** progressBarColor - The color to use for the filled portion of the bar. +** +** Notes: +** - Uses a cached `lastProgress` string to avoid unnecessary redraws. +** - The timestamp is displayed to the right of the progress bar. +** =================================================================== +*/ +void ClockView::handleProgressBar(TFTColor progressBarColor) +{ + // spLogI(LOGTAG_GENERAL, " ==== ENTERING HomeView::handleProgressBar() ==="); + spLogV(LOGTAG_GUI, "Updating progress bar with percentage: %u", _displayPercentage); + _pUI->drawProgressBar(60, 170, 410, 20, _displayPercentage, TFTColor::White, progressBarColor); + + std::string progress = _pUI->formatTime(_forecastedProgressMS); + + static std::string lastProgress = "X:XX/X:XX"; + if (lastProgress != progress) + { + _pUI->rDrawString(progress.c_str(), 330, 287, 20, TFTColor::DarkGreen, TFTColor::Black,"XX:XX:XX"); + lastProgress = progress; + } + + // spLogI(LOGTAG_GENERAL, " ==== EXITING HomeView::handleProgressBar() ==="); +} + +/* +** =================================================================== +** handlePlayStatus() +** Handles the logic for updating the UI based on the current +** playback status of the Spotify player, including whether a track +** is playing, paused, or stopped. It also updates the play/pause +** button icon when the playback state changes. +** +** Parameters: +** None +** +** Returns: +** None +** +** Notes: +** - This method updates the UI text to display the current playback +** status (e.g., "Track Playing", "Paused", or "Stopped"). +** - It ensures that the play/pause button icon is refreshed whenever +** the playback status changes. +** =================================================================== +*/ +void ClockView::handlePlayStatus(bool isForcedUpdate) +{ + static bool lastIsPlaying = false; + SpotifyPlayer *pSP = &SpotifyPlayer::getInstance(); + PlayingMetadata playing = pSP->getCurrentlyPlayingMetadata(); + const int32_t posX = 100; + + if ((lastIsPlaying != playing.isPlaying) + || (isForcedUpdate)) + { + if (playing.isPlaying) + { + std::string label; + switch (playing.currentlyPlayingType) + { + case PlayingMetadata::PlayingType::track: + label = "Track Playing"; + break; + case PlayingMetadata::PlayingType::episode: + label = "Ep. Playing"; + break; + default: + label = "Other Playing"; + break; + } + + _pUI->lDrawString(label.c_str(), posX, 287, 20, TFTColor::DarkGreen, TFTColor::Black,"Track Playing"); + + } + else + { + + if (playing.progressMs > 0) + { + _pUI->lDrawString("Paused", posX, 287, 20, TFTColor::Red, TFTColor::Black,"Track Playing"); + } + else + { + _pUI->lDrawString("Stopped", posX, 287, 20, TFTColor::Red, TFTColor::Black,"Track Playing"); + } + } + + } + + if (lastIsPlaying != playing.isPlaying) + { + _pPauseElement->render(false); // refresh button icon + } + + lastIsPlaying = playing.isPlaying; +} + +/* +** =================================================================== +** handleDate() +** Draws the current date centered above the clock time. Only +** updates the display if the date string has changed or if +** forced refresh logic is added later. +** +** Notes: +** - Uses the format: "Saturday March 30 2025" +** =================================================================== +*/ +void ClockView::handleDate(bool isForceRefresh) +{ + std::string format = UI_DATE_FORMAT; + if (Vault::getInstance().isUSDateTimeFormattingUsed()) + { + format = UI_DATE_FORMAT_US; + } + std::string dateStr = getCurrentTimestamp(format.c_str()).c_str(); + static std::string lastDate = ""; + + if ((lastDate != dateStr) + || isForceRefresh) + { + _pUI->cDrawString(dateStr.c_str(), 240, 8, 24, TFTColor::Yellow, _pUI->getBackground(), ""); + lastDate = dateStr; + } +} + +/* +** =================================================================== +** refreshPlayingProgress() +** Updates the current progress of the playing media, calculating +** the forecasted progress and updating the display percentage. +** The _forecastedProgressMS member variable will be updated +** during the execution of this method. +** +** Returns: +** A boolean indicating whether the display percentage has been +** updated. +** +** Notes: +** - The progress is only updated if the media is currently playing. +** - This method skips progress updates if called too frequently +** (within a 100 ms threshold). +** - The updated forecasted progress is used to calculate the +** display percentage based on the media's total duration. +** =================================================================== +*/ +bool ClockView::refreshPlayingProgress() +{ + SpotifyPlayer *pSP = &SpotifyPlayer::getInstance(); + bool isNewTrack = pSP->isNewTrackReady(); + PlayingMetadata playing = pSP->getCurrentlyPlayingMetadata(); + + + static long lastInvokeMs = 0; + bool isDisplayPercentageUpdated = false; + long currentMs = millis(); + + if (isNewTrack) + { + // Reset back to zero + lastInvokeMs = 0; + _forecastedProgressMS = playing.progressMs; + } + + if (currentMs < (lastInvokeMs + 25)) + { + // Not ready to update yet + isDisplayPercentageUpdated = false; + spLogV(LOGTAG_GUI, "bSkipProgressBarRefresh = true. currentMS: %u lastInvokeMs: %u", currentMs, lastInvokeMs); + } + else if (playing.isPlaying) // 🔹 Only update if song is actually playing + { + // Song is playing, so update forecasted progress normally + if (playing.lastRefreshMs < lastInvokeMs) + { + long difference = currentMs - lastInvokeMs; + _forecastedProgressMS += difference; + if (_forecastedProgressMS > playing.durationMs) + { + _forecastedProgressMS = playing.durationMs; + } + spLogV(LOGTAG_GUI, "Updating forecastedProgressMS. prev forecastedProgressMS: %u difference: %u", _forecastedProgressMS, difference); + } + else + { + // Song progress has changed naturally (not paused), so trust new progressMs + _forecastedProgressMS = playing.progressMs; + } + _displayPercentage = (playing.durationMs > 0) ? (_forecastedProgressMS * 100) / playing.durationMs : 0; + _displayPercentage = (_displayPercentage > 100) ? 100 : _displayPercentage; + isDisplayPercentageUpdated = true; + } + + // Update tracking variables + + lastInvokeMs = millis(); + + return isDisplayPercentageUpdated; +} + +/* +** =================================================================== +** enteringView() +** =================================================================== +*/ +void ClockView::enteringView() +{ + _pUI->setSplitBackground(true); + // Make sure the UI is painted fresh + _pUI->setBackground(TFTColor::DarkestGreen, false); + _pUI->clearScreenHome(); + _pUI->drawClockTime(Vault::getInstance().isUSDateTimeFormattingUsed(), true); + _pUI->markUIDirty(true); // force refresh to get album details + +} + +/* +** =================================================================== +** handle_UUM_IDLE() +** =================================================================== +*/ +void ClockView::handle_UM_IDLE(SCUIMessage *pMessage) +{ + const unsigned long delayBetweenRequests = 50; + + // Every period refresh the display + if (millis() > _requestDueTime) + { + drawUI(); + _requestDueTime = millis() + delayBetweenRequests; + } + +} \ No newline at end of file diff --git a/src/UIViews/ClockView.h b/src/UIViews/ClockView.h new file mode 100644 index 0000000..64b6a14 --- /dev/null +++ b/src/UIViews/ClockView.h @@ -0,0 +1,61 @@ +/*------------------------------------------------------------------------------------------------- +** +** ClockView.h +** +** Declares the ClockView class, a UIView subclass that displays the +** current time and date alongside minimal Spotify playback information. +** Includes progress bar handling, play status indication, and a return +** button for navigation. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2025-02-12 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ + +#pragma once +#include "UIView.h" + +/* +** =================================================================== +** Class: ClockView +** +** Purpose: +** Implements the "Clock" display mode for the Spotify controller UI. +** Displays the current time and date alongside minimal Spotify playback +** information such as progress and playback status. +** =================================================================== +*/ + +class ClockView : public UIView +{ +public: + explicit ClockView(DisplayUI *pUI); // Constructor + virtual void initializeUIElements() override; + + /* + ** =================================================================== + ** drawUI() + ** Renders the UI for the art-only mode. + ** =================================================================== + */ + void drawUI() override; + +protected: + // Transitions + virtual void enteringView() override; + virtual void handle_UM_IDLE(SCUIMessage *pMessage) override; + +private: + void handleProgressBar(TFTColor progressBarColor); + void handlePlayStatus(bool isForcedUpdate); + void handleDate(bool isForceRefresh); + bool refreshPlayingProgress(); + // Time management variables + unsigned long _requestDueTime = 0; // time when request due + uint8_t _displayPercentage = 0; // what is being displayed as the percentage + long _forecastedProgressMS = 0; // forcasted progress into the song +}; \ No newline at end of file diff --git a/src/UIViews/CoverView.cpp b/src/UIViews/CoverView.cpp new file mode 100644 index 0000000..c5ae04b --- /dev/null +++ b/src/UIViews/CoverView.cpp @@ -0,0 +1,124 @@ +/*------------------------------------------------------------------------------------------------- +** +** CoverView.h +** +** Declares the CoverView class, a UIView subclass that implements the +** "Art Only" display mode for the Spotify controller. It shows only the +** album cover art along with minimal track information and a return button. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2025-01-21 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ + +#include "CoverView.h" +#include "SCLogger.h" +#include "logTags.h" +#include "ThingPulse/util.h" +#include "SpotifyArtMgr.h" +#include "Renderers/BackButtonRenderer.h" + +/* +** =================================================================== +** Constructor +** Initializes the CoverView with the provided DisplayUI instance. +** +** Parameters: +** pUI - Pointer to the DisplayUI instance. +** =================================================================== +*/ +CoverView::CoverView(DisplayUI *pUI) + : UIView(pUI) // Pass the DisplayUI pointer to the base class constructor +{ + // Initialization specific to CoverView, if any +} + +void CoverView::initializeUIElements() +{ + + std::shared_ptr backButtonRenderer = std::make_shared(_pUI); + // Make the touch rectangle 10 pixels larger on all sides. The renderer will take off the 10 pixels when rendering + // Doing to make a bigger, more forgiving touch target. + const int MARGIN = 10; + _pReturnViewElement = std::make_unique( 10 - MARGIN, 270 - MARGIN, 70 + (MARGIN * 2), 40 + (MARGIN * 2), backButtonRenderer); + + // initialize other elements not set above + UIView::initializeUIElements(); + +} + +/* +** =================================================================== +** drawUI() +** Renders the UI for the default mode. +** =================================================================== +*/ +void CoverView::drawUI() +{ + SpotifyPlayer *pSP = &SpotifyPlayer::getInstance(); + bool isNewTrack = pSP->isNewTrackReady(); + PlayingMetadata playing = pSP->getCurrentlyPlayingMetadata(); + + // Render back button + _pReturnViewElement->render(false); + + const int trackY = 110; + const int albumY = 150; + const int artistY = 190; + + static bool isWaitingShowing = false; + if (pSP->isMusicAvailable() == false) + { + if (!isWaitingShowing) + { + spLogI(LOGTAG_GUI, "drawCurrentlyPlayingToLCD() - Empty track. Draw Waiting for song..."); + _pUI->setBackground(TFTColor::SC_DJX_BG, true); + _pUI->drawTextToLCD("Music is unavailable",trackY); + _pUI->drawTextToLCD("Waiting for song...",albumY); + isWaitingShowing = true; + } + return; + } + isWaitingShowing = false; + + // If this isn't a new song and the UI + // isn't dirty, nothing to do so leave. + if ((isNewTrack == false) + && (_pUI->isUIDirty() == false)) + { + spLogI(LOGTAG_GUI, "Song has not changed and the UI is not dirty. Leaving drawUI() method."); + return; + } + + //start repaintCoverArt(); + String filePath = SP_NO_COVER_JPG_FILENAME; + + const int TARGET_IMAGE_SIZE = 300; + + for (int i = 0; i < playing.numImages; i++) + { + if ((playing.albumImages[i].height == TARGET_IMAGE_SIZE) + && (playing.albumImages[i].width == TARGET_IMAGE_SIZE)) + { + filePath = SpotifyArtMgr::getInstance()->getLocalFileName(playing.albumImages[i].url); + break; + } + } + TFTColor c = _pUI->calculateAverageColor(filePath.c_str()); + _pUI->setBackground(c, false); + _pUI->clearScreenKeepArt(); // Clear any left over text + _pUI->drawTextToLCD("Uno", 200, 20, true); + _pUI->drawTextToLCD("Dos", 230, 14, true); + _pUI->drawTextToLCD("Tres", 254, 14, true); + + _pUI->setJpgScaleToSmall(false); + _pUI->drawAlbumArt(90, 10, filePath); + + _pUI->markUIDirty(false); + + +} \ No newline at end of file diff --git a/src/UIViews/CoverView.h b/src/UIViews/CoverView.h new file mode 100644 index 0000000..11889f7 --- /dev/null +++ b/src/UIViews/CoverView.h @@ -0,0 +1,43 @@ +/*------------------------------------------------------------------------------------------------- +** +** CoverView.h +** +** Declares the CoverView class, a UIView subclass that implements the +** "Art Only" display mode for the Spotify controller. It shows only the +** album cover art along with minimal track information and a return button. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2025-01-21 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ + +#pragma once +#include "UIView.h" + +/* +** =================================================================== +** Class: CoverView +** +** Purpose: +** Implements the "Art Only" display mode, showing only the album +** cover art. +** =================================================================== +*/ +class CoverView : public UIView +{ +public: + explicit CoverView(DisplayUI *pUI); // Constructor + virtual void initializeUIElements() override; + + /* + ** =================================================================== + ** drawUI() + ** Renders the UI for the art-only mode. + ** =================================================================== + */ + void drawUI() override; +}; \ No newline at end of file diff --git a/src/UIViews/DiagnosticsView.cpp b/src/UIViews/DiagnosticsView.cpp new file mode 100644 index 0000000..aec3032 --- /dev/null +++ b/src/UIViews/DiagnosticsView.cpp @@ -0,0 +1,435 @@ +/*------------------------------------------------------------------------------------------------- +** +** DiagnosticsView.h +** +** Declares the DiagnosticsView class, a UIView subclass that provides a +** diagnostics interface for the Spotify controller. Displays metadata, +** system status, and playback diagnostics, primarily for debugging purposes. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2025-01-21 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ + +#include "DiagnosticsView.h" +#include "SCLogger.h" +#include "logTags.h" +#include "ThingPulse/util.h" +#include "Monitor.h" +#include "SpotifyArtMgr.h" +#include "settings.h" +#include "Renderers/BackButtonRenderer.h" +#include "esp_heap_caps.h" +#include "Vault.h" +#include "SpotifyArtMgr.h" + +/* +** =================================================================== +** Constructor +** Initializes the CoverView with the provided DisplayUI instance. +** +** Parameters: +** pUI - Pointer to the DisplayUI instance. +** =================================================================== +*/ +DiagnosticsView::DiagnosticsView(DisplayUI *pUI) + : UIView(pUI) // Pass the DisplayUI pointer to the base class constructor +{ + // Initialization specific to CoverView, if any +} + +void DiagnosticsView::initializeUIElements() +{ + + std::shared_ptr backButtonRenderer = std::make_shared(_pUI); + + // Make the touch rectangle 10 pixels larger on all sides. The renderer will take off the 10 pixels when rendering + // Doing to make a bigger, more forgiving touch target. + const int MARGIN = 10; + _pReturnViewElement = std::make_unique( 10 - MARGIN, 270 - MARGIN, 70 + (MARGIN * 2), 40 + (MARGIN * 2), backButtonRenderer); + + // initialize other elements not set above + UIView::initializeUIElements(); + +} + +/* +** =================================================================== +** drawUI() +** Renders the UI for the default mode. +** =================================================================== +*/ +void DiagnosticsView::drawUI() +{ + + SpotifyPlayer *pSP = &SpotifyPlayer::getInstance(); + bool isNewTrack = pSP->isNewTrackReady(); // copy this before resetting it with getCurrentlyPlayingMetadata() + PlayingMetadata playing = pSP->getCurrentlyPlayingMetadata(); + + if ((isNewTrack) + || _pUI->isUIDirty()) + { + clearAndPaintScreen(playing); + refreshScreen(playing, true); + } + else if (millis() > _requestDueTime) + { + refreshScreen(playing, false); + } + + _pUI->markUIDirty(false); + +} + +/* +** =================================================================== +** clearAndPaintScreen() +** Clears the screen and repaints the UI with the provided track +** metadata. This method is responsible for drawing album details, +** compile time, and version information. +** +** Parameters: +** playing - The metadata of the currently playing track. +** +** Returns: +** None. +** =================================================================== +*/ +void DiagnosticsView::clearAndPaintScreen(PlayingMetadata playing) +{ + const int fontSize = 18; + const int32_t xStart = 240; + char s[100]; + + spLogV(LOGTAG_GUI, "==== ENTERING clearAndPaintScreen ==="); + + // Clear screen + _pUI->clearScreen(); + + // **** Back Button ***** + _pReturnViewElement->render(false); + + // **** Album Information ***** + _pUI->cDrawString(truncateString(playing.trackName, 50).c_str(), xStart, rowToY(0), fontSize, TFTColor::Green, TFTColor::Black,"."); + _pUI->cDrawString(truncateString(playing.albumName, 50).c_str(), xStart, rowToY(1), fontSize, TFTColor::Green, TFTColor::Black,"."); + _pUI->cDrawString(truncateString(playing.getArtistsList(800).c_str(), 50).c_str(), xStart, rowToY(2), fontSize, TFTColor::Green, TFTColor::Black,"."); + + // **** Track Progress Header *****= + _pUI->lDrawString("progressMs / durationMs :: lastRefreshMs (Pct)", 10, rowToY(4), fontSize, TFTColor::DarkGreen, TFTColor::Black,""); + + // **** Compile / Version / Config Information ***** + char buffer[40]; + snprintf(buffer, sizeof(buffer), "Config: %c %c %d", + toString(Vault::getInstance().getCredentialSource())[0], + toString(Vault::getInstance().getPrivacyLevel())[0], + SpotifyArtMgr::getInstance()->getMaxCacheSize()); + std::string configInfo = std::string(buffer); + + std::string versionStr = std::string("Version: ") + VERSION + " - " + configInfo; + _pUI->lDrawString(versionStr.c_str(), 90, rowToY(12), fontSize, TFTColor::DarkGrey, TFTColor::Black,"#### ################## ####"); + versionStr = std::string("Compile: ") + COMPILE_TIME; + _pUI->lDrawString(versionStr.c_str(), 90, rowToY(13), fontSize, TFTColor::DarkGrey, TFTColor::Black,"#### ################## ####"); + +} + +/* +** =================================================================== +** refreshScreen() +** Updates the diagnostics screen with the latest track metadata +** and system statistics. This method ensures only changed values +** are redrawn to optimize performance and minimize flicker. +** +** Parameters: +** playing - The metadata of the currently playing track. +** isForcedRefresh - If true, forces a full repaint regardless +** of whether values have changed. +** +** Returns: +** None. +** =================================================================== +*/ +void DiagnosticsView::refreshScreen(PlayingMetadata playing, bool isForcedRefresh) +{ + const int fontSize = 18; + char s[100]; + std::string label; + std::string label2; + MonitorStats stats; + TFTColor newColor = TFTColor::Purple; + + // **** Remaining Track Information ***** + + static std::string trackInfoLine = ""; + if ((playing.isPlaying) || isForcedRefresh) + { + label = "Y"; + newColor = TFTColor::DarkGreen; + } + else + { + label = "N"; + newColor = TFTColor::Red; + } + switch (playing.currentlyPlayingType) + { + case PlayingMetadata::PlayingType::track: + label2 = "track"; + break; + case PlayingMetadata::PlayingType::episode: + label2 = "episode"; + break; + default: + label2 = "other"; + break; + } + + snprintf(s, sizeof(s), "# Artist: %u # Images: %u isPlaying: %s type: %s", playing.numArtists, playing.numImages, label.c_str(), label2.c_str()); + if ((trackInfoLine != s) || isForcedRefresh) + { + _pUI->lDrawString(s, 10, rowToY(3), fontSize, newColor, TFTColor::Black,""); + trackInfoLine = s; + } + + static std::string playingTypeLine = ""; + uint8_t percentPlayed = (playing.durationMs > 0) ? (playing.progressMs * 100) / playing.durationMs : 0; + percentPlayed = (percentPlayed > 100) ? 100 : percentPlayed; + snprintf(s, sizeof(s), "%u / %u :: %u (%u%%)", playing.progressMs, playing.durationMs, playing.lastRefreshMs, percentPlayed); + if ((playingTypeLine != s) || isForcedRefresh) + { + _pUI->lDrawString(s, 10, rowToY(5), fontSize, TFTColor::Cyan, TFTColor::Black,"lastRefreshMs: ##### playingType: episode"); + playingTypeLine = s; + } + + // **** Metrics on Getting Song Information ***** + static std::string statsLine200 = ""; + stats = Monitor::getStats(MONITOR_ID_SPOTIFY_GET_CURRENTLY_PLAYING); + if (stats.maxMs > 2500) + { + newColor = TFTColor::Yellow; + } + else + { + newColor = TFTColor::DarkGreen; + } + if (stats.averageMs > 2500) + { + newColor = TFTColor::Red; + } + if (stats.minMs == LLONG_MAX) + { + snprintf(s, sizeof(s), "%u: A:%lld M:--- X:%lld C:%d", stats.id, stats.averageMs, stats.maxMs, stats.stopCount); + } + else + { + snprintf(s, sizeof(s), "%u: A:%lld M:%lld X:%lld C:%d", stats.id, stats.averageMs, stats.minMs, stats.maxMs, stats.stopCount); + } + if ((statsLine200 != s) || isForcedRefresh) + { + _pUI->lDrawString(s, 10, rowToY(6), fontSize, newColor, TFTColor::Black,""); + statsLine200 = s; + } + + // **** Metrics on Fetching Album Art ***** + static std::string statsLine300 = ""; + stats = Monitor::getStats(MONITOR_ID_SPOTIFY_IMAGE_HTTP_GET); + + if (stats.minMs == LLONG_MAX) + { + snprintf(s, sizeof(s), "%u: A:%lld M:--- X:%lld C:%d", stats.id, stats.averageMs, stats.maxMs, stats.stopCount); + } + else + { + snprintf(s, sizeof(s), "%u: A:%lld M:%lld X:%lld C:%d", stats.id, stats.averageMs, stats.minMs, stats.maxMs, stats.stopCount); + } + if ((statsLine300 != s) || isForcedRefresh) + { + if (stats.maxMs > 2500) + { + newColor = TFTColor::Yellow; + } + else + { + newColor = TFTColor::DarkGreen; + } + if (stats.averageMs > 2500) + { + newColor = TFTColor::Red; + } + _pUI->lDrawString(s, 10, rowToY(7), fontSize, newColor, TFTColor::Black,""); + statsLine300 = s; + } + + // **** Art Cache Hits Metrics ***** + static std::string artCacheLine = ""; + ArtMgrStats artMgrStats = SpotifyArtMgr::getInstance()->getStats(); + snprintf(s, sizeof(s), "AHR: %lld LHS: %lld CHS: %lld Hits: %lld TF: %lld", + artMgrStats.averageHitRate, + artMgrStats.longestHitStreak, + artMgrStats.currentHitStreak, + artMgrStats.cacheHits, + artMgrStats.totalFetches); + if ((artCacheLine != s) || isForcedRefresh) + { + _pUI->lDrawString(s, 10, rowToY(8), fontSize, TFTColor::DarkGreen, TFTColor::Black,"Heap: ####### Low: #######"); + artCacheLine = s; + } + + // **** Heap Stats ***** + static std::string heapLine = ""; + uint32_t heapSize = Monitor::getFreeHeap(); + Monitor::HeapStats heapStats = Monitor::getHeapStats(); + + // Approximate percentage remaining based on max we've seen + float remainingPercentage = (float)heapSize / (float)heapStats.maxHeap * 100.0f; + + + snprintf(s, sizeof(s), "Heap: %u Low: %u Hi: %u (%.0f%%)", heapSize, heapStats.minHeap, heapStats.maxHeap, remainingPercentage); + if ((heapLine != s) || isForcedRefresh) + { + if (heapSize < HEAP_LOW_THRESHOLD) + { + newColor = TFTColor::Red; + } + else if (heapSize < HEAP_WATCH_SIZE_BASE) + { + newColor = TFTColor::Yellow; + } + else + { + if (heapStats.minHeap < HEAP_LOW_THRESHOLD) + { + newColor = TFTColor::Pink; + } + else if ((heapStats.minHeap < HEAP_WATCH_SIZE_BASE)) + { + newColor = TFTColor::SC_LowHeap; + } + else + { + newColor = TFTColor::Cyan; + } + + } + _pUI->lDrawString(s, 10, rowToY(9), fontSize, newColor, TFTColor::Black,"Heap: ####### Low: ####### High: #######"); + heapLine = s; + } + + // **** Message Queue Stats ***** + static std::string mqLine = ""; + uint32_t depth = Monitor::getMaxQueueDepth(); + snprintf(s, sizeof(s), "MQD: %u%", depth); + + if ((mqLine != s) || isForcedRefresh) + { + if (depth > 5) + { + if (depth >= 7) + { + newColor = TFTColor::Red; + } + else + { + newColor = TFTColor::Yellow; + } + } + else + { + newColor = TFTColor::DarkGreen; + } + _pUI->lDrawString(s, 10, rowToY(10), fontSize, newColor, TFTColor::Black,"MQD: ##"); + mqLine = s; + } + + static std::string queueDelayLine = ""; + stats = Monitor::getStats(MONITOR_ID_SCUI_QUEUE_DELAY); + + snprintf(s, sizeof(s), "A: %lld M: %lld X: %lld", stats.averageMs, stats.minMs, stats.maxMs); + if ((queueDelayLine != s) || isForcedRefresh) + { + if (stats.maxMs > 2000) + { + newColor = TFTColor::Yellow; + } + else + { + newColor = TFTColor::DarkGreen; + } + _pUI->lDrawString(s, 100, rowToY(10), fontSize, newColor, TFTColor::Black,"Avg(ms): ##### Max(ms) #####"); + queueDelayLine = s; + } + + // **** Uptime Stats ***** + static std::string uptimeLine = ""; + snprintf(s, sizeof(s), "%s", Monitor::getFormattedUptime("Up %lu days, %lu hrs, %lu mins, %lu secs").c_str()); + if ((uptimeLine != s) || isForcedRefresh) + { + _pUI->lDrawString(s, 90, rowToY(11), fontSize, TFTColor::DarkGreen, TFTColor::Black,"#### ################## ####"); + uptimeLine = s; + } +} + +/* +** =================================================================== +** rowToY() +** Calculates the Y pixel position based on the given row index. +** The calculation considers the default font size and spacing rules. +** +** Parameters: +** row - The row index for which to calculate the Y position. +** +** Returns: +** The Y pixel coordinate corresponding to the given row index. +** =================================================================== +*/ +constexpr int DiagnosticsView::rowToY(int row) +{ + const int fontSize = 18; + const int rowSpacing = 22; + const int margin = 10; + + return margin + row * rowSpacing; + +} + +/* +** =================================================================== +** enteringView() +** =================================================================== +*/ +void DiagnosticsView::enteringView() +{ + // spLogI(LOGTAG_GENERAL, "*** ENTERING DiagnosticsView::enteringView() ==="); + _pUI->setSplitBackground(false); + + _pUI->setBackground(TFTColor::Black, false); + + // force repaint to get latest album details and to get background updates + _pUI->markUIDirty(true); + + drawUI(); + + // spLogI(LOGTAG_GENERAL, "*** EXITING DiagnosticsView::enteringView() ==="); +} + +/* +** =================================================================== +** handle_UUM_IDLE() +** =================================================================== +*/ +void DiagnosticsView::handle_UM_IDLE(SCUIMessage *pMessage) +{ + const unsigned long delayBetweenRequests = 3000; + + // Every period refresh the display + if (millis() > _requestDueTime) + { + drawUI(); + _requestDueTime = millis() + delayBetweenRequests; + } + +} + diff --git a/src/UIViews/DiagnosticsView.h b/src/UIViews/DiagnosticsView.h new file mode 100644 index 0000000..75c3371 --- /dev/null +++ b/src/UIViews/DiagnosticsView.h @@ -0,0 +1,62 @@ +/*------------------------------------------------------------------------------------------------- +** +** DiagnosticsView.h +** +** Declares the DiagnosticsView class, a UIView subclass that provides a +** diagnostics interface for the Spotify controller. Displays metadata, +** system status, and playback diagnostics, primarily for debugging purposes. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2025-01-21 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ + +#pragma once +#include "UIView.h" + +/* +** =================================================================== +** Class: DiagnosticsView +** +** Purpose: +** Provides a diagnostics screen for the Spotify controller UI. +** Displays system status information such as network status, +** current track data, playback state, and performance metrics. +** +** This view is accessible via the HomeView and is intended +** primarily for debugging or advanced user feedback. It inherits +** from UIView and implements methods for initialization, UI +** rendering, and message handling during idle time. +** =================================================================== +*/ +class DiagnosticsView : public UIView +{ +public: + explicit DiagnosticsView(DisplayUI *pUI); // Constructor + virtual void initializeUIElements() override; + + /* + ** =================================================================== + ** drawUI() + ** Renders the UI for the art-only mode. + ** =================================================================== + */ + void drawUI() override; + +protected: + // Transitions + virtual void enteringView() override; + virtual void handle_UM_IDLE(SCUIMessage *pMessage) override; + +private: + // Time management variables + unsigned long _requestDueTime = 0; // time when request due + + void clearAndPaintScreen(PlayingMetadata playing); + void refreshScreen(PlayingMetadata playing, bool isForcedRefresh); + constexpr int rowToY(int row); +}; \ No newline at end of file diff --git a/src/UIViews/HomeView.cpp b/src/UIViews/HomeView.cpp new file mode 100644 index 0000000..05228bb --- /dev/null +++ b/src/UIViews/HomeView.cpp @@ -0,0 +1,477 @@ +/*------------------------------------------------------------------------------------------------- +** +** HomeView.cpp +** +** Implements the default HomeView for the Spotify controller UI, displaying +** track metadata, album art, control buttons, and current playback progress. +** Inherits from UIView and handles rendering and touch interactions for the +** main screen. Includes methods for managing UI refresh cycles and state. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2025-01-21 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ + +#include "HomeView.h" +#include "SCLogger.h" +#include "logTags.h" +#include "ThingPulse/util.h" +#include "SpotifyArtMgr.h" +#include "Renderers/PreviousTrackButtonRenderer.h" +#include "Renderers/PlayPauseButtonRenderer.h" +#include "Renderers/NextTrackButtonRenderer.h" +#include "Vault.h" + +/* +** =================================================================== +** Constructor +** Initializes the HomeView class. +** +** Parameters: +** pUI - Pointer to the DisplayUI instance. +** =================================================================== +*/ +HomeView::HomeView(DisplayUI *pUI) + : UIView(pUI) // Call the base class constructor +{ + // Additional initialization (if needed) + _pUI->setBackground(TFTColor::Navy, true); +} + +/* +** =================================================================== +** initializeUIElements() +** +** Sets up and positions all interactive UIElements used in the HomeView, +** including media control buttons, navigation targets, and cover art tap zones. +** +** Notes: +** - Creates shared_ptr instances of IUIRenderer for each control. +** - Defines specific coordinates and dimensions for button layout. +** - This method is called during the initial view setup. +** =================================================================== +*/ +void HomeView::initializeUIElements() +{ + + // spLogI(LOGTAG_GENERAL,"HomeView::initializeUIElements() "); + + const int32_t buttonSize = 90; + const int32_t spacer = 11; + const int32_t buttonStartX = 170; + const int32_t buttonStartY = 40; + const int32_t coverArtSize = COVER_ART_SIZE / 2; + + std::shared_ptr previousButtonRenderer = std::make_shared(_pUI); + _pPreviousElement = std::make_unique(buttonStartX, buttonStartY, buttonSize, buttonSize, previousButtonRenderer); + + std::shared_ptr playPauseButtonRenderer = std::make_shared(_pUI); + _pPauseElement = std::make_unique(buttonStartX + spacer + buttonSize, buttonStartY, buttonSize, buttonSize, playPauseButtonRenderer); + + std::shared_ptr nextButtonRenderer = std::make_shared(_pUI); + _pNextElement = std::make_unique(buttonStartX + (spacer + buttonSize) * 2, buttonStartY, buttonSize, buttonSize, nextButtonRenderer); + + _pGotoCoverArtElement = std::make_unique(10, 10, coverArtSize, coverArtSize); + + + _pReturnViewElement = std::make_unique( 0, 121, 119, 200); + _pGotoClockElement = std::make_unique(300, 221, 100, 200); + _pGotoDiagnosticElement = std::make_unique(400, 221, 80, 200); + + +} + + +/* +** =================================================================== +** enteringView() +** =================================================================== +*/ +void HomeView::enteringView() +{ + + // spLogI(LOGTAG_GENERAL, "*** ENTERING HomeView::enteringView() ==="); + + _pUI->setSplitBackground(true); + + // Make sure the UI is painted fresh + _pUI->markUIDirty(true); + + drawUI(); + + // spLogI(LOGTAG_GENERAL, "*** EXITING HomeView::enteringView() ==="); +} + +/* +** =================================================================== +** drawUI() +** Renders the UI for the default mode. +** =================================================================== +*/ +void HomeView::drawUI() +{ + // spLogI(LOGTAG_GENERAL, "==== ENTERING drawUI for HomeView ==="); + + SpotifyPlayer *pSP = &SpotifyPlayer::getInstance(); + bool isNewTrack = pSP->isNewTrackReady(); + bool isDisplayPercentageUpdated = false; + + // Check if music is playing and if not + // display message and exit + if (checkAndHandleNoMusicAvailable()) + { + return; + } + + isDisplayPercentageUpdated = refreshPlayingProgress(); + + // If this isn't a new song and the UI + // isn't dirty, nothing to do so leave. + if (isNewTrack + || _pUI->isUIDirty()) + { + clearAndPaintScreen(); + _pUI->markUIDirty(false); + } + else + { + if (isDisplayPercentageUpdated) + { + // handleProgressBar(); + } + handleProgressBar(); + handleClock(false); + handlePlayStatus(); + } + + // spLogI(LOGTAG_GENERAL, "==== EXITING drawUI for HomeView ==="); + +} + +/* +** =================================================================== +** refreshPlayingProgress() +** Updates the current progress of the playing media, calculating +** the forecasted progress and updating the display percentage. +** The _forecastedProgressMS member variable will be updated +** during the execution of this method. +** +** Returns: +** A boolean indicating whether the display percentage has been +** updated. +** +** Notes: +** - The progress is only updated if the media is currently playing. +** - This method skips progress updates if called too frequently +** (within a 100 ms threshold). +** - The updated forecasted progress is used to calculate the +** display percentage based on the media's total duration. +** =================================================================== +*/ +bool HomeView::refreshPlayingProgress() +{ + SpotifyPlayer *pSP = &SpotifyPlayer::getInstance(); + bool isNewTrack = pSP->isNewTrackReady(); + PlayingMetadata playing = pSP->getCurrentlyPlayingMetadata(); + + + static long lastInvokeMs = 0; + bool isDisplayPercentageUpdated = false; + long currentMs = millis(); + + if (isNewTrack) + { + // Reset back to zero + lastInvokeMs = 0; + _forecastedProgressMS = playing.progressMs; + } + + if (currentMs < (lastInvokeMs + 100)) + { + // Not ready to update yet + isDisplayPercentageUpdated = false; + spLogV(LOGTAG_GUI, "bSkipProgressBarRefresh = true. currentMS: %u lastInvokeMs: %u", currentMs, lastInvokeMs); + } + else if (playing.isPlaying) // 🔹 Only update if song is actually playing + { + // Song is playing, so update forecasted progress normally + if (playing.lastRefreshMs < lastInvokeMs) + { + long difference = currentMs - lastInvokeMs; + _forecastedProgressMS += difference; + if (_forecastedProgressMS > playing.durationMs) + { + _forecastedProgressMS = playing.durationMs; + } + spLogV(LOGTAG_GUI, "Updating forecastedProgressMS. prev forecastedProgressMS: %u difference: %u", _forecastedProgressMS, difference); + } + else + { + // Song progress has changed naturally (not paused), so trust new progressMs + _forecastedProgressMS = playing.progressMs; + } + _displayPercentage = (playing.durationMs > 0) ? (_forecastedProgressMS * 100) / playing.durationMs : 0; + _displayPercentage = (_displayPercentage > 100) ? 100 : _displayPercentage; + isDisplayPercentageUpdated = true; + } + + // Update tracking variables + + lastInvokeMs = millis(); + + return isDisplayPercentageUpdated; +} + +/* +** =================================================================== +** handleProgressBar() +** Updates the progress bar on the screen based on the current +** display percentage. Additionally, updates and displays the +** current progress time if it has changed. +** +** Notes: +** - The progress bar is updated with the current display percentage. +** - If the progress time has changed, the new progress time is +** drawn on the screen. +** =================================================================== +*/ +void HomeView::handleProgressBar() +{ + // spLogI(LOGTAG_GENERAL, " ==== ENTERING HomeView::handleProgressBar() ==="); + spLogV(LOGTAG_GUI, "Updating progress bar with percentage: %u", _displayPercentage); + _pUI->drawProgressBar(10, 170, 460, 20, _displayPercentage, TFTColor::White, TFTColor::DarkGreen); + + std::string progress = _pUI->formatTime(_forecastedProgressMS); + + static std::string lastProgress = "X:XX/X:XX"; + if (lastProgress != progress) + { + _pUI->rDrawString(progress.c_str(), 220, 287, 20, TFTColor::DarkGreen, TFTColor::Black,"XX:XX:XX"); + lastProgress = progress; + } + // spLogI(LOGTAG_GENERAL, " ==== EXITING HomeView::handleProgressBar() ==="); +} + +/* +** =================================================================== +** handlePlayStatus() +** Handles the logic for updating the UI based on the current +** playback status of the Spotify player, including whether a track +** is playing, paused, or stopped. It also updates the play/pause +** button icon when the playback state changes. +** +** Parameters: +** None +** +** Returns: +** None +** +** Notes: +** - This method updates the UI text to display the current playback +** status (e.g., "Track Playing", "Paused", or "Stopped"). +** - It ensures that the play/pause button icon is refreshed whenever +** the playback status changes. +** =================================================================== +*/ +void HomeView::handlePlayStatus() +{ + static bool lastIsPlaying = false; + SpotifyPlayer *pSP = &SpotifyPlayer::getInstance(); + PlayingMetadata playing = pSP->getCurrentlyPlayingMetadata(); + + if ((lastIsPlaying != playing.isPlaying) + || (_forceUpdate)) + { + if (playing.isPlaying) + { + std::string label; + switch (playing.currentlyPlayingType) + { + case PlayingMetadata::PlayingType::track: + label = "Track Playing"; + break; + case PlayingMetadata::PlayingType::episode: + label = "Ep. Playing"; + break; + default: + label = "Other Playing"; + break; + } + + //_pUI->drawText(label.c_str(), 100, 290, 22, TFTColor::DarkGreen); + _pUI->lDrawString(label.c_str(), 10, 287, 20, TFTColor::DarkGreen, TFTColor::Black,"Track Playing"); + + } + else + { + //_pUI->drawText("Not Playing", 100, 290, 22, TFTColor::Red); + if (playing.progressMs > 0) + { + _pUI->lDrawString("Paused", 10, 287, 20, TFTColor::Red, TFTColor::Black,"Track Playing"); + } + else + { + _pUI->lDrawString("Stopped", 10, 287, 20, TFTColor::Red, TFTColor::Black,"Track Playing"); + } + } + _forceUpdate = false; + } + + if (lastIsPlaying != playing.isPlaying) + { + _pPauseElement->render(false); // refresh button icon + } + + lastIsPlaying = playing.isPlaying; +} + +/* +** =================================================================== +** handleClock() +** Updates and displays the current time on the screen. If the +** time has changed or a repaint is explicitly requested, the +** time is redrawn on the display. +** +** Parameters: +** isPaintForced - A boolean flag indicating whether a forced repaint +** should occur, regardless of whether the time has +** changed. +** +** Notes: +** - The time is displayed in the format "XX:XX PM" if US formatting is used. +** - If the current time differs from the last displayed time or +** if a forced repaint is requested, the time is redrawn. +** =================================================================== +*/ +void HomeView::handleClock(bool isPaintForced) +{ + std::string format = UI_TIME_FORMAT; + int offset = 10; + if (Vault::getInstance().isUSDateTimeFormattingUsed()) + { + format = UI_TIME_FORMAT_US; + offset = 0; + } + std::string timeStr = getCurrentTimestamp(format.c_str()).c_str(); + static std::string lastTime = "X:XX/X:XX"; + if ((lastTime != timeStr) + || (isPaintForced)) + { + _pUI->lDrawString(timeStr.c_str(), 310 + offset, 287, 20, TFTColor::DarkGreen, TFTColor::Black,"XX:XX PM"); + lastTime = timeStr; + } +} + +/* +** =================================================================== +** clearAndPaintScreen() +** Clears the screen and paints the current playback information. +** +** Description: +** This method clears the display and updates it with the currently +** playing track's metadata, including album art, control buttons, +** progress bar, track details, duration, and the current clock time. +** It retrieves the album art file path, calculates the average +** background color, and paints the UI elements to display a fresh +** view of the current playback state. +** +** Notes: +** - This method forces a UI update even if the content has not changed. +** - Progress bar is drawn twice to ensure proper clearing of the previous state. +** - The background color is dynamically calculated based on the album art. +** +** Parameters: +** None +** +** Returns: +** void +** =================================================================== +*/ +void HomeView::clearAndPaintScreen() +{ + SpotifyPlayer *pSP = &SpotifyPlayer::getInstance(); + PlayingMetadata playing = pSP->getCurrentlyPlayingMetadata(); + + spLogV(LOGTAG_GUI, "Song has changed or the UI is dirty. Painting."); + + _forceUpdate = true; + + ///// Get cover art file path + + String filePath = SP_NO_COVER_JPG_FILENAME; + + const int TARGET_IMAGE_SIZE = 300; + + for (int i = 0; i < playing.numImages; i++) + { + if ((playing.albumImages[i].height == TARGET_IMAGE_SIZE) + && (playing.albumImages[i].width == TARGET_IMAGE_SIZE)) + { + filePath = SpotifyArtMgr::getInstance()->getLocalFileName(playing.albumImages[i].url); + break; + } + } + + ///// Calculate background color and clear screen + + TFTColor c = _pUI->calculateAverageColor(filePath.c_str()); + _pUI->setBackground(c, false); //true); + _pUI->clearScreenHome(); + + + ///// Draw album art + + _pUI->setJpgScaleToSmall(true); + _pUI->drawAlbumArt(10, 10, filePath); + + ///// Draw control buttons + + _pPreviousElement->render(false); + _pPauseElement->render(false); + _pNextElement->render(false); + + ///// Draw progress bar. Draw 0 percent one to force bar to be fully cleared + ///// otherwise the black will be erased by the screen clear + spLogV(LOGTAG_GUI, "Updating progress bar with percentage: %u", _displayPercentage); + _pUI->drawProgressBar(10, 170, 460, 20, 0, TFTColor::White, TFTColor::DarkGreen); + + ///// Draw album details + _pUI->drawTextToLCD(truncateString(playing.trackName, 36).c_str(), 200, 24, false); + _pUI->drawTextToLCD(truncateString(playing.albumName, 50).c_str(),230, 18, false); + _pUI->drawTextToLCD(truncateString(playing.getArtistsList(800).c_str(), 50).c_str(),254, 18, false); + + ///// Draw play duration + _pUI->rDrawString("-:--", 220, 287, 20, TFTColor::DarkGreen, TFTColor::Black,"XX:XX:XX"); + std::string duration = "/"+_pUI->formatTime(playing.durationMs); + _pUI->lDrawString(duration.c_str(), 223, 287, 20, TFTColor::DarkGreen, TFTColor::Black,"XX:XX:XX"); + handleProgressBar(); + + ///// Draw play status + _pUI->lDrawString("Refreshing", 10, 287, 20, TFTColor::DarkGreen, TFTColor::Black,"Track Playing"); + + ///// Draw clock + handleClock(true); + +} + +/* +** =================================================================== +** handle_UUM_IDLE() +** =================================================================== +*/ +void HomeView::handle_UM_IDLE(SCUIMessage *pMessage) +{ + const unsigned long delayBetweenRequests = 500; + + // Every period refresh the display + if (millis() > _requestDueTime) + { + drawUI(); + _requestDueTime = millis() + delayBetweenRequests; + } + +} \ No newline at end of file diff --git a/src/UIViews/HomeView.h b/src/UIViews/HomeView.h new file mode 100644 index 0000000..cc5e7fe --- /dev/null +++ b/src/UIViews/HomeView.h @@ -0,0 +1,58 @@ +/*------------------------------------------------------------------------------------------------- +** +** HomeView.h +** +** Implements the default HomeView for the Spotify controller UI, displaying +** track metadata, album art, control buttons, and current playback progress. +** Inherits from UIView and handles rendering and touch interactions for the +** main screen. Includes methods for managing UI refresh cycles and state. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2025-01-21 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ + +#pragma once +#include "UIView.h" + +/* +** =================================================================== +** Class: HomeView +** +** Purpose: +** Implements the default display mode, showing song, album, and +** artist details over cover art. +** =================================================================== +*/ +class HomeView : public UIView +{ +public: + explicit HomeView(DisplayUI *pUI); // Constructor + virtual void initializeUIElements() override; + void drawUI() override; + virtual void handle_UM_IDLE(SCUIMessage *pMessage) override; + +protected: + void enteringView() override; + +private: + // Time management variables + unsigned long _requestDueTime = 0; // time when request due + bool _forceUpdate = false; // should an UI refresh be forced? + uint8_t _displayPercentage = 0; // what is being displayed as the percentage + long _forecastedProgressMS = 0; // forcasted progress into the song + + bool refreshPlayingProgress(); + void handleProgressBar(); + void handlePlayStatus(); + void handleClock(bool isPaintForced); + void clearAndPaintScreen(); + + + + +}; \ No newline at end of file diff --git a/src/UIViews/Renderers/BackButtonRenderer.cpp b/src/UIViews/Renderers/BackButtonRenderer.cpp new file mode 100644 index 0000000..e9562ba --- /dev/null +++ b/src/UIViews/Renderers/BackButtonRenderer.cpp @@ -0,0 +1,57 @@ +/*------------------------------------------------------------------------------------------------- +** +** BackButtonRenderer.cpp +** +** Renders a "Back" or "Previous Track" UI button for the Spotify controller interface. +** Changes its appearance when pressed and draws a corresponding back arrow icon. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2025-02-22 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ + + +#include "BackButtonRenderer.h" +#include "../UIElement.h" + +/* +** =================================================================== +** render() +** Renders the "Previous Track" button. +** +** Parameters: +** element - Reference to the UI element being rendered. +** isPressed - Indicates whether the button is pressed. +** +** Notes: +** - Uses TFT_eSPI to draw a "Previous Track" icon. +** - Changes appearance when pressed. +** =================================================================== +*/ +void BackButtonRenderer::render(const UIElement& element, bool isPressed) +{ + // MARGIN is used here and in the including view to provide a larger touch target + const int MARGIN = 10; + _pUI->drawBlankButton(element.getX() + MARGIN, element.getY() + MARGIN, element.getWidth() - (MARGIN * 2), element.getHeight() - (MARGIN * 2), 2, TFTColor::SC_NetworkSuccess, isPressed); + _pUI->drawBackIcon(element.getX() + MARGIN, element.getY() + MARGIN, element.getWidth() - (MARGIN * 2), element.getHeight() - (MARGIN * 2)); +} + +/* +** =================================================================== +** drawIcon() +** Abstract method for rendering a button's unique icon. +** +** Parameters: +** x, y - Top-left coordinates for the icon. +** width, height - Dimensions for the icon. +** color - Color to use for the icon. +** =================================================================== +*/ +void BackButtonRenderer::drawIcon(int16_t x, int16_t y, int16_t width, int16_t height, uint16_t color) +{ + _pUI->drawSkipTrackIcon(x, y, width, height, false); +} diff --git a/src/UIViews/Renderers/BackButtonRenderer.h b/src/UIViews/Renderers/BackButtonRenderer.h new file mode 100644 index 0000000..fd22420 --- /dev/null +++ b/src/UIViews/Renderers/BackButtonRenderer.h @@ -0,0 +1,52 @@ +/*------------------------------------------------------------------------------------------------- +** +** BackButtonRenderer.cpp +** +** Renders a "Back" or "Previous Track" UI button for the Spotify controller interface. +** Changes its appearance when pressed and draws a corresponding back arrow icon. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2025-02-22 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ + +#pragma once + +#include "IUIRenderer.h" +#include "ButtonRenderer.h" + +/* +** =================================================================== +** BackButtonRenderer.h +** +** Handles rendering of the "Previous Track" button in the UI. +** +** Responsibilities: +** - Draws the button on the screen. +** - Adjusts appearance based on its pressed state. +** =================================================================== +*/ +class BackButtonRenderer : public ButtonRenderer +{ +public: + using ButtonRenderer::ButtonRenderer; + + /* + ** =================================================================== + ** render() + ** Renders the "Previous Track" button. + ** + ** Parameters: + ** element - Reference to the UI element being rendered. + ** isPressed - Indicates whether the button is pressed. + ** =================================================================== + */ + void render(const UIElement& element, bool isPressed) override; + +protected: + void drawIcon(int16_t x, int16_t y, int16_t width, int16_t height, uint16_t color) override; +}; \ No newline at end of file diff --git a/src/UIViews/Renderers/ButtonRenderer.cpp b/src/UIViews/Renderers/ButtonRenderer.cpp new file mode 100644 index 0000000..5318c7d --- /dev/null +++ b/src/UIViews/Renderers/ButtonRenderer.cpp @@ -0,0 +1,79 @@ +/*------------------------------------------------------------------------------------------------- +** +** ButtonRenderer.cpp +** +** Base renderer for generic UI buttons. Handles button background and border +** drawing based on press state, and delegates icon rendering to derived classes. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2025-02-22 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ + +#include "ButtonRenderer.h" +#include "../UIElement.h" +#include "TFT_eSPI.h" + +extern TFT_eSPI tft; // Assume an external TFT instance is used + +/* +** =================================================================== +** Constructor (default) +** Initializes the ButtonRenderer without a UI reference. +** =================================================================== +*/ +ButtonRenderer::ButtonRenderer() : _pUI(nullptr) +{ +} + +/* +** =================================================================== +** Constructor (with DisplayUI) +** Initializes the ButtonRenderer with a reference to DisplayUI. +** +** Parameters: +** pUI - Pointer to the DisplayUI instance. +** =================================================================== +*/ +ButtonRenderer::ButtonRenderer(DisplayUI* pUI) : _pUI(pUI) +{ +} + +/* +** =================================================================== +** render() +** Renders a button with a customizable icon. +** +** Parameters: +** element - Reference to the UI element being rendered. +** isPressed - Indicates whether the button is pressed. +** +** Notes: +** - Draws a button with a rounded rectangle. +** - Calls `drawIcon()` to render the specific button symbol. +** =================================================================== +*/ +void ButtonRenderer::render(const UIElement& element, bool isPressed) +{ + int16_t x = element.getX(); + int16_t y = element.getY(); + int16_t width = element.getWidth(); + int16_t height = element.getHeight(); + + // Define colors based on button state + uint16_t bgColor = isPressed ? TFT_DARKGREY : TFT_BLACK; + uint16_t borderColor = isPressed ? TFT_WHITE : TFT_LIGHTGREY; + + // Draw button background + tft.fillRoundRect(x, y, width, height, 5, bgColor); + + // Draw button border + tft.drawRoundRect(x, y, width, height, 5, borderColor); + + // Draw the icon (specific to the derived class) + drawIcon(x + width / 4, y + height / 4, width / 2, height / 2, borderColor); +} \ No newline at end of file diff --git a/src/UIViews/Renderers/ButtonRenderer.h b/src/UIViews/Renderers/ButtonRenderer.h new file mode 100644 index 0000000..a6fe6e6 --- /dev/null +++ b/src/UIViews/Renderers/ButtonRenderer.h @@ -0,0 +1,100 @@ +/*------------------------------------------------------------------------------------------------- +** +** ButtonRenderer.h +** +** Base renderer for generic UI buttons. Handles button background and border +** drawing based on press state, and delegates icon rendering to derived classes. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2025-02-22 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ + +#pragma once + +#include "IUIRenderer.h" +#include "../../DisplayUI.h" + +/* +** =================================================================== +** ButtonRenderer +** Base class for all button renderers. +** +** Responsibilities: +** - Provides common rendering logic for UI buttons. +** - Handles default button drawing, including borders and backgrounds. +** - Allows derived classes to customize icons. +** =================================================================== +*/ +class ButtonRenderer : public IUIRenderer +{ +public: + /* + ** =================================================================== + ** Constructor (default) + ** Initializes the ButtonRenderer without a UI reference. + ** =================================================================== + */ + ButtonRenderer(); + + /* + ** =================================================================== + ** Constructor (with DisplayUI) + ** Initializes the ButtonRenderer with a reference to DisplayUI. + ** + ** Parameters: + ** pUI - Pointer to the DisplayUI instance. + ** =================================================================== + */ + explicit ButtonRenderer(DisplayUI* pUI); + + /* + ** =================================================================== + ** Destructor + ** Cleans up any allocated resources. + ** =================================================================== + */ + virtual ~ButtonRenderer() = default; + + /* + ** =================================================================== + ** render() + ** Renders a button with a customizable icon. + ** + ** Parameters: + ** element - Reference to the UI element being rendered. + ** isPressed - Indicates whether the button is pressed. + ** + ** Notes: + ** - Draws a button with a rounded rectangle. + ** - Calls `drawIcon()` to render the specific button symbol. + ** =================================================================== + */ + void render(const UIElement& element, bool isPressed) override; + +protected: + /* + ** =================================================================== + ** drawIcon() + ** Abstract method for rendering a button's unique icon. + ** + ** Parameters: + ** x, y - Top-left coordinates for the icon. + ** width, height - Dimensions for the icon. + ** color - Color to use for the icon. + ** =================================================================== + */ + virtual void drawIcon(int16_t x, int16_t y, int16_t width, int16_t height, uint16_t color) = 0; + + /* + ** =================================================================== + ** Protected Members + ** _pUI - Pointer to the DisplayUI instance. + ** =================================================================== + */ + DisplayUI* _pUI; +}; \ No newline at end of file diff --git a/src/UIViews/Renderers/IUIRenderer.h b/src/UIViews/Renderers/IUIRenderer.h new file mode 100644 index 0000000..1ccab1f --- /dev/null +++ b/src/UIViews/Renderers/IUIRenderer.h @@ -0,0 +1,53 @@ +/*------------------------------------------------------------------------------------------------- +** +** IUIRenderer.h +** +** Interface for rendering UI elements on the display. Classes implementing +** this interface must define a `render` method to draw a UIElement, either +** in its normal or pressed state. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2025-02-22 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ + +#pragma once + +/* +** =================================================================== +** Interface: IUIRenderer +** +** Purpose: +** Provides an interface for rendering UI elements. +** =================================================================== +*/ +class UIElement; // Forward declaration + +class IUIRenderer +{ +public: + /* + ** =================================================================== + ** Destructor + ** Ensures proper cleanup of derived classes. + ** =================================================================== + */ + virtual ~IUIRenderer() = default; + + /* + ** =================================================================== + ** Method: render + ** Renders a UIElement. + ** + ** Parameters: + ** element - Reference to the UIElement to render. + ** isPressed - Indicates whether the element is in a pressed state. + ** =================================================================== + */ + virtual void render(const UIElement& element, bool isPressed) = 0; +}; + diff --git a/src/UIViews/Renderers/NextTrackButtonRenderer.cpp b/src/UIViews/Renderers/NextTrackButtonRenderer.cpp new file mode 100644 index 0000000..b9e9768 --- /dev/null +++ b/src/UIViews/Renderers/NextTrackButtonRenderer.cpp @@ -0,0 +1,54 @@ +/*------------------------------------------------------------------------------------------------- +** +** NextTrackButtonRenderer.cpp +** +** Renders the "Next Track" button in the Spotify controller UI. Draws a forward-skip +** icon and adjusts visual state when pressed, using the DisplayUI drawing helpers. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2025-02-22 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ + +#include "NextTrackButtonRenderer.h" +#include "../UIElement.h" + +/* +** =================================================================== +** render() +** Renders the "Next Track" button. +** +** Parameters: +** element - Reference to the UI element being rendered. +** isPressed - Indicates whether the button is pressed. +** +** Notes: +** - Uses TFT_eSPI to draw a "Previous Track" icon. +** - Changes appearance when pressed. +** =================================================================== +*/ +void NextTrackButtonRenderer::render(const UIElement& element, bool isPressed) +{ + _pUI->drawBlankButton(element.getX(), element.getY(), element.getWidth(), element.getHeight(), 2, TFTColor::White, isPressed); + _pUI->drawSkipTrackIcon(element.getX(), element.getY(), element.getWidth(), element.getHeight(), false); +} + +/* +** =================================================================== +** drawIcon() +** Abstract method for rendering a button's unique icon. +** +** Parameters: +** x, y - Top-left coordinates for the icon. +** width, height - Dimensions for the icon. +** color - Color to use for the icon. +** =================================================================== +*/ +void NextTrackButtonRenderer::drawIcon(int16_t x, int16_t y, int16_t width, int16_t height, uint16_t color) +{ + _pUI->drawSkipTrackIcon(x, y, width, height, false); +} diff --git a/src/UIViews/Renderers/NextTrackButtonRenderer.h b/src/UIViews/Renderers/NextTrackButtonRenderer.h new file mode 100644 index 0000000..b513f39 --- /dev/null +++ b/src/UIViews/Renderers/NextTrackButtonRenderer.h @@ -0,0 +1,52 @@ +/*------------------------------------------------------------------------------------------------- +** +** NextTrackButtonRenderer.h +** +** Renders the "Next Track" button in the Spotify controller UI. Draws a forward-skip +** icon and adjusts visual state when pressed, using the DisplayUI drawing helpers. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2025-02-22 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ + +#pragma once + +#include "IUIRenderer.h" +#include "ButtonRenderer.h" + +/* +** =================================================================== +** NextTrackButtonRenderer.h +** +** Handles rendering of the "Previous Track" button in the UI. +** +** Responsibilities: +** - Draws the button on the screen. +** - Adjusts appearance based on its pressed state. +** =================================================================== +*/ +class NextTrackButtonRenderer : public ButtonRenderer +{ +public: + using ButtonRenderer::ButtonRenderer; + + /* + ** =================================================================== + ** render() + ** Renders the "Previous Track" button. + ** + ** Parameters: + ** element - Reference to the UI element being rendered. + ** isPressed - Indicates whether the button is pressed. + ** =================================================================== + */ + void render(const UIElement& element, bool isPressed) override; + +protected: + void drawIcon(int16_t x, int16_t y, int16_t width, int16_t height, uint16_t color) override; +}; \ No newline at end of file diff --git a/src/UIViews/Renderers/PlayPauseButtonRenderer.cpp b/src/UIViews/Renderers/PlayPauseButtonRenderer.cpp new file mode 100644 index 0000000..d1856e2 --- /dev/null +++ b/src/UIViews/Renderers/PlayPauseButtonRenderer.cpp @@ -0,0 +1,64 @@ +/*------------------------------------------------------------------------------------------------- +** +** PlayPauseButtonRenderer.cpp +** +** Renders the play/pause toggle button in the Spotify controller UI. +** Displays either a play or pause icon based on the playback state +** retrieved from the SpotifyPlayer singleton. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2025-02-22 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ + +#include "PlayPauseButtonRenderer.h" +#include "SpotifyPlayer.h" +#include "../UIElement.h" + +/* +** =================================================================== +** render() +** Renders the "Previous Track" button. +** +** Parameters: +** element - Reference to the UI element being rendered. +** isPressed - Indicates whether the button is pressed. +** +** Notes: +** - Uses TFT_eSPI to draw a "Previous Track" icon. +** - Changes appearance when pressed. +** =================================================================== +*/ +void PlayPauseButtonRenderer::render(const UIElement& element, bool isPressed) +{ + _pUI->drawBlankButton(element.getX(), element.getY(), element.getWidth(), element.getHeight(), 2, TFTColor::White, isPressed); + + if (SpotifyPlayer::getInstance().getCurrentlyPlayingMetadata().isPlaying) + { + _pUI->drawPauseTrackIcon(element.getX(), element.getY(), element.getWidth(), element.getHeight()); + } + else + { + _pUI->drawPlayTrackIcon(element.getX(), element.getY(), element.getWidth(), element.getHeight()); + } + +} + +/* +** =================================================================== +** drawIcon() +** Abstract method for rendering a button's unique icon. +** +** Parameters: +** x, y - Top-left coordinates for the icon. +** width, height - Dimensions for the icon. +** color - Color to use for the icon. +** =================================================================== +*/ +void PlayPauseButtonRenderer::drawIcon(int16_t x, int16_t y, int16_t width, int16_t height, uint16_t color) +{ +} diff --git a/src/UIViews/Renderers/PlayPauseButtonRenderer.h b/src/UIViews/Renderers/PlayPauseButtonRenderer.h new file mode 100644 index 0000000..c28fc92 --- /dev/null +++ b/src/UIViews/Renderers/PlayPauseButtonRenderer.h @@ -0,0 +1,53 @@ +/*------------------------------------------------------------------------------------------------- +** +** PlayPauseButtonRenderer.h +** +** Renders the play/pause toggle button in the Spotify controller UI. +** Displays either a play or pause icon based on the playback state +** retrieved from the SpotifyPlayer singleton. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2025-02-22 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ + +#pragma once + +#include "IUIRenderer.h" +#include "ButtonRenderer.h" + +/* +** =================================================================== +** PlayPauseButtonRenderer +** +** Handles rendering of the "Previous Track" button in the UI. +** +** Responsibilities: +** - Draws the button on the screen. +** - Adjusts appearance based on its pressed state. +** =================================================================== +*/ +class PlayPauseButtonRenderer : public ButtonRenderer +{ +public: + using ButtonRenderer::ButtonRenderer; + + /* + ** =================================================================== + ** render() + ** Renders the "Previous Track" button. + ** + ** Parameters: + ** element - Reference to the UI element being rendered. + ** isPressed - Indicates whether the button is pressed. + ** =================================================================== + */ + void render(const UIElement& element, bool isPressed) override; + +protected: + void drawIcon(int16_t x, int16_t y, int16_t width, int16_t height, uint16_t color) override; +}; \ No newline at end of file diff --git a/src/UIViews/Renderers/PreviousTrackButtonRenderer.cpp b/src/UIViews/Renderers/PreviousTrackButtonRenderer.cpp new file mode 100644 index 0000000..5e2042e --- /dev/null +++ b/src/UIViews/Renderers/PreviousTrackButtonRenderer.cpp @@ -0,0 +1,53 @@ +/*------------------------------------------------------------------------------------------------- +** +** PreviousTrackButtonRenderer.cpp +** +** Renders the "Previous Track" button in the Spotify controller UI. Draws a back-skip +** icon and adjusts visual state when pressed, using the DisplayUI drawing helpers. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2025-02-22 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ + +#include "PreviousTrackButtonRenderer.h" +#include "../UIElement.h" + +/* +** =================================================================== +** render() +** Renders the "Previous Track" button. +** +** Parameters: +** element - Reference to the UI element being rendered. +** isPressed - Indicates whether the button is pressed. +** +** Notes: +** - Uses TFT_eSPI to draw a "Previous Track" icon. +** - Changes appearance when pressed. +** =================================================================== +*/ +void PreviousTrackButtonRenderer::render(const UIElement& element, bool isPressed) +{ + _pUI->drawBlankButton(element.getX(), element.getY(), element.getWidth(), element.getHeight(), 2, TFTColor::White, isPressed); + _pUI->drawSkipTrackIcon(element.getX(), element.getY(), element.getWidth(), element.getHeight(), true); +} + +/* +** =================================================================== +** drawIcon() +** Abstract method for rendering a button's unique icon. +** +** Parameters: +** x, y - Top-left coordinates for the icon. +** width, height - Dimensions for the icon. +** color - Color to use for the icon. +** =================================================================== +*/ +void PreviousTrackButtonRenderer::drawIcon(int16_t x, int16_t y, int16_t width, int16_t height, uint16_t color) +{ +} diff --git a/src/UIViews/Renderers/PreviousTrackButtonRenderer.h b/src/UIViews/Renderers/PreviousTrackButtonRenderer.h new file mode 100644 index 0000000..03ecc97 --- /dev/null +++ b/src/UIViews/Renderers/PreviousTrackButtonRenderer.h @@ -0,0 +1,51 @@ +/*------------------------------------------------------------------------------------------------- +** +** PreviousTrackButtonRenderer.h +** +** Renders the "Previous Track" button in the Spotify controller UI. Draws a back-skip +** icon and adjusts visual state when pressed, using the DisplayUI drawing helpers. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2025-02-22 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ +#pragma once + +#include "IUIRenderer.h" +#include "ButtonRenderer.h" + +/* +** =================================================================== +** PreviousTrackButtonRenderer +** +** Handles rendering of the "Previous Track" button in the UI. +** +** Responsibilities: +** - Draws the button on the screen. +** - Adjusts appearance based on its pressed state. +** =================================================================== +*/ +class PreviousTrackButtonRenderer : public ButtonRenderer +{ +public: + using ButtonRenderer::ButtonRenderer; + + /* + ** =================================================================== + ** render() + ** Renders the "Previous Track" button. + ** + ** Parameters: + ** element - Reference to the UI element being rendered. + ** isPressed - Indicates whether the button is pressed. + ** =================================================================== + */ + void render(const UIElement& element, bool isPressed) override; + +protected: + void drawIcon(int16_t x, int16_t y, int16_t width, int16_t height, uint16_t color) override; +}; \ No newline at end of file diff --git a/src/UIViews/UIElement.cpp b/src/UIViews/UIElement.cpp new file mode 100644 index 0000000..a09c733 --- /dev/null +++ b/src/UIViews/UIElement.cpp @@ -0,0 +1,78 @@ +/*------------------------------------------------------------------------------------------------- +** +** UIElement.cpp +** +** Represents a rectangular UI element on the display, providing basic +** hit testing and rendering capabilities. Can be used as a pressable +** touch zone or paired with a renderer to draw visual elements. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2025-01-26 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ + +#include "UIElement.h" + +/* +** Constructor +** Initializes the UI element with its position and dimensions. +*/ +UIElement::UIElement(int16_t x, int16_t y, int16_t width, int16_t height, std::shared_ptr renderer) + : _x(x), + _y(y), + _width(width), + _height(height), + _renderer(std::move(renderer)) +{ +} + +/* +** Constructor (touch zone only, no renderer) +** Initializes the UI element as a non-rendering touch zone. +*/ +UIElement::UIElement(int16_t x, int16_t y, int16_t width, int16_t height) + : _x(x), + _y(y), + _width(width), + _height(height), + _renderer(nullptr) +{ +} + +/* +** isPressed() +** Determines if a touch point is within the bounds of this UI element. +** +** Returns: +** True if the point lies within the rectangle, otherwise false. +*/ +bool UIElement::isPressed(const TS_Point& point) const +{ + return (point.x >= _x + && point.x <= (_x + _width) + && point.y >= _y + && point.y <= (_y + _height)); +} + +/* +** render() +** Calls the assigned renderer to render this UIElement. +** +** Parameters: +** isPressed - Indicates whether the element is in a pressed state. +** +** Notes: +** If a renderer is assigned, it will be invoked to draw the element. +** Otherwise, this function does nothing. +*/ +void UIElement::render(bool isPressed) const +{ + if (_renderer) + { + _renderer->render(*this, isPressed); + } +} diff --git a/src/UIViews/UIElement.h b/src/UIViews/UIElement.h new file mode 100644 index 0000000..a6b4f6e --- /dev/null +++ b/src/UIViews/UIElement.h @@ -0,0 +1,78 @@ +/*------------------------------------------------------------------------------------------------- +** +** UIElement.h +** +** Represents a rectangular UI element on the display, providing basic +** hit testing and rendering capabilities. Can be used as a pressable +** touch zone or paired with a renderer to draw visual elements. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2025-01-26 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ + +#pragma once +#include +#include "FT6236TouchController/FT6236.h" +#include "Renderers/IUIRenderer.h" + +/* +** =================================================================== +** UIElement +** Represents a rectangular UI element on the display. +** =================================================================== +*/ +class UIElement +{ +public: + // Constructor to define the UI element's position and dimensions + UIElement(int16_t x, int16_t y, int16_t width, int16_t height, std::shared_ptr renderer); + UIElement(int16_t x, int16_t y, int16_t width, int16_t height); + + /* + ** =================================================================== + ** isPressed() + ** Checks if a given point is within the bounds of this UI element. + ** + ** Parameters: + ** point - The touch point to check. + ** + ** Returns: + ** True if the point is within the bounds, otherwise false. + ** =================================================================== + */ + bool isPressed(const TS_Point& point) const; + + /* + ** =================================================================== + ** render() + ** Calls the assigned renderer to render this UIElement. + ** + ** Parameters: + ** isPressed - Indicates whether the element is in a pressed state. + ** =================================================================== + */ + void render(bool isPressed) const; + + /* + ** =================================================================== + ** Accessors + ** Provides read-only access to UIElement properties. + ** =================================================================== + */ + int16_t getX() const { return _x; } + int16_t getY() const { return _y; } + int16_t getWidth() const { return _width; } + int16_t getHeight() const { return _height; } + +private: + int16_t _x; // X-coordinate of the top-left corner + int16_t _y; // Y-coordinate of the top-left corner + int16_t _width; // Width of the element + int16_t _height; // Height of the element + std::shared_ptr _renderer; // Renderer instance for drawing the element. +}; \ No newline at end of file diff --git a/src/UIViews/UIView.cpp b/src/UIViews/UIView.cpp new file mode 100644 index 0000000..f99c4cb --- /dev/null +++ b/src/UIViews/UIView.cpp @@ -0,0 +1,391 @@ +/*------------------------------------------------------------------------------------------------- +** +** UIView.cpp +** +** Abstract base class for UI views in the Spotify controller. Each view defines +** its own rendering behavior and touch handling logic. Provides shared logic +** for message handling, initialization, and screen transitions. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2025-01-21 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ + +#include "UIView.h" +#include "DisplayUI.h" +#include "SCLogger.h" +#include "logTags.h" +#include "UIViewManager.h" + +#include // Assuming Arduino framework for Serial.print +#include // Required for std::make_unique and std::unique_ptr + +/* +** =================================================================== +** initialize() +** Prepares the display mode by displaying a setup message. +** =================================================================== +*/ +void UIView::initialize() +{ + initializeUIElements(); +} + +/* +** =================================================================== +** UIView Constructor +** Initializes the UIView object with the provided DisplayUI +** object and sets up default UI elements. +** +** Parameters: +** pUI - Pointer to the DisplayUI object, which provides rendering +** capabilities and access to the display context. +** +** Purpose: +** - Store the reference to the DisplayUI object for use in UI +** rendering and interaction. +** - Call the initializeUIElements() method to set up default +** positions and sizes for the UI elements. +** +** Notes: +** - This constructor assumes that the DisplayUI object passed in +** has been properly initialized by the caller. +** - The initializeUIElements() method can be overridden in +** subclasses to customize the UI layout for specific display modes. +** +** =================================================================== +*/ +UIView::UIView(DisplayUI *pUI) +{ + _pUI = pUI; +} + +/* +** =================================================================== +** initializeUIElements +** Initializes the UI elements with default positions and sizes. +** +** Purpose: +** - Assign default values to the UI elements (e.g., pause, previous, +** next, and toggle art buttons). +** - This method can be overridden or extended by subclasses to +** provide a custom layout specific to the display mode. +** +** Notes: +** - Default values are provided as placeholders; real positions +** and dimensions should be adjusted as needed. +** - Uses std::unique_ptr to manage the UIElement objects, ensuring +** proper memory management and avoiding manual cleanup. +** +** =================================================================== +*/ +void UIView::initializeUIElements() +{ + + // spLogI(LOGTAG_GENERAL,"UIView::initializeUIElements() "); + + // Assign default positions and sizes for UI elements + // 480x320 + + if (!_pPauseElement) + { + _pPauseElement = std::make_unique(101, 0, 259, 120); + } + + if (!_pPreviousElement) + { + _pPreviousElement = std::make_unique( 0, 0, 100, 120); + } + + if (!_pNextElement) + { + _pNextElement = std::make_unique(360, 0, 100, 120); + } + + if (!_pGotoCoverArtElement) + { + _pGotoCoverArtElement = std::make_unique(120, 121, 160, 200); + } + + if (!_pGotoClockElement) + { + _pGotoClockElement = std::make_unique(280, 121, 200, 100); + } + + if (!_pGotoDiagnosticElement) + { + _pGotoDiagnosticElement = std::make_unique(280, 221, 200, 100); + } + + if (!_pReturnViewElement) + { + _pReturnViewElement = std::make_unique( 0, 121, 119, 200); + } + +} + +/* +** =================================================================== +** enteringView() +** =================================================================== +*/ +void UIView::enteringView() +{ + + _pUI->setSplitBackground(false); + + // Make sure the UI is painted fresh + _pUI->markUIDirty(true); + + drawUI(); +} + +/* +** =================================================================== +** leavingView() +** =================================================================== +*/ +void UIView::leavingView() +{ + // spLogI(LOGTAG_GENERAL, "*** ENTERING UIView::leavingView() ==="); + _pUI->clearScreen(); + // spLogI(LOGTAG_GENERAL, "*** EXITING UIView::leavingView() ==="); + +} + +/* +** =================================================================== +** onTouchDown() +** Handles the event when the screen is touched (press event). +** +** Parameters: +** point - The touch point representing the coordinates of the press +** event on the screen. +** +** Notes: +** - Default implementation logs the touch-down event. Derived classes +** can override this to provide specific behavior. +** =================================================================== +*/ +void UIView::onTouchDown(const TS_Point& point) +{ + spLogD(LOGTAG_DISPLAY_MODE, "Default onTouchDown at (%d, %d)", point.x, point.y); + + // Spotify Player + SpotifyPlayer& spotifyPlayer = SpotifyPlayer::getInstance(); + + if (_pPauseElement + && _pPauseElement->isPressed(point)) + { + _pUI->showTouchDown(TFTColor::SC_PauseTrack); + _pPauseElement->render(true); + spotifyPlayer.pauseSong(); + } + else if (_pPreviousElement + && _pPreviousElement->isPressed(point)) + { + _pUI->showTouchDown(TFTColor::SC_PreviousTrack); + _pPreviousElement->render(true); + spotifyPlayer.previousSong(); + } + else if (_pNextElement + && _pNextElement->isPressed(point)) + { + _pUI->showTouchDown(TFTColor::SC_NextTrack); + _pNextElement->render(true); + spotifyPlayer.nextSong(); + } + else if (_pGotoCoverArtElement + && _pGotoCoverArtElement->isPressed(point)) + { + // Handle art button press + UIViewManager::getInstance().gotoView(UIViewManager::ViewID::Cover); + } + else if (_pGotoDiagnosticElement + && _pGotoDiagnosticElement->isPressed(point)) + { + // Handle goto Diagnostic view + UIViewManager::getInstance().gotoView(UIViewManager::ViewID::Diagnostics); + } + else if (_pReturnViewElement + && _pReturnViewElement->isPressed(point)) + { + // Handle return view + _pReturnViewElement->render(true); + if (!UIViewManager::getInstance().exitView()) + { + // if top view, simply force a refresh + UIViewManager::getInstance().getActiveView()->drawUI(); + } + } + else if (_pGotoClockElement + && _pGotoClockElement->isPressed(point)) + { + // Handle got Clock view + UIViewManager::getInstance().gotoView(UIViewManager::ViewID::Clock); + } +} + +/* +** =================================================================== +** onTouchUp() +** Handles the event when the screen touch is lifted (release event). +** +** Parameters: +** point - The touch point representing the coordinates of the release +** event on the screen. +** +** Notes: +** - Default implementation logs the touch-up event. Derived classes +** can override this to provide specific behavior. +** =================================================================== +*/ +void UIView::onTouchUp() +{ + spLogD(LOGTAG_DISPLAY_MODE, "Default onTouchUp()"); + _pUI->showTouchUp(); + _pPreviousElement->render(false); + _pPauseElement->render(false); + _pNextElement->render(false); + _pReturnViewElement->render(false); +} + +/* +** =================================================================== +** handleMessage() +** =================================================================== +*/ +void UIView::handleMessage(SCUIMessage *pMessage) +{ + switch (pMessage->type) + { + case SCUIMessageType::UM_IDLE: + handle_UM_IDLE(pMessage); + break; + case SCUIMessageType::UM_STATUS_BOX: + handle_UM_STATUS_BOX(pMessage); + break; + case SCUIMessageType::UM_DOWNLOAD_BOX: + handle_UM_DOWNLOAD_BOX(pMessage); + break; + case SCUIMessageType::UM_MARK_DIRTY: + handle_UM_MARK_DIRTY(pMessage); + break; + case SCUIMessageType::UM_PLAYER_REFRESH: + handle_UM_PLAYER_REFRESH(pMessage); + break; + default: + spLogW(LOGTAG_GUI, "Unknown message type received."); + break; + } +} + +/* +** =================================================================== +** handle_UM_STATUS_BOX() +** is the provided color is negative, use the existing background +** color. +** =================================================================== +*/ +void UIView::handle_UM_STATUS_BOX(SCUIMessage *pMessage) +{ + TFTColor c = TFTColor::Black; + + if (!_pUI->isSplitBackground()) + { + c = _pUI->getBackground(); + } + + if (pMessage->num > 0) + { + c = static_cast(pMessage->num); + } + _pUI->drawStatusBox(c); +} + +/* +** =================================================================== +** handle_UM_DOWNLOAD_BOX() +** =================================================================== +*/ +void UIView::handle_UM_DOWNLOAD_BOX(SCUIMessage *pMessage) +{ + bool isDownloadComplete = pMessage->num; // Get the status + _pUI->drawDownloadIndicator(isDownloadComplete); +} + +/* +** =================================================================== +** handle_UM_MARK_DIRTY() +** =================================================================== +*/ +void UIView::handle_UM_MARK_DIRTY(SCUIMessage *pMessage) +{ + bool isDirty = pMessage->num; // Get the status + _pUI->markUIDirty(isDirty); +} + +/* +** =================================================================== +** handle_UM_PLAYER_REFRESH() +** =================================================================== +*/ +void UIView::handle_UM_PLAYER_REFRESH(SCUIMessage *pMessage) +{ + bool isNewTrackReady = pMessage->num; // Get whether this is a new track + + drawUI(); + +} + +/* +** =================================================================== +** checkAndHandleNoMusicAvailable() +** Checks if music is available and handles the "Waiting for song" +** message if necessary. +** +** Description: +** This method checks if music is currently available through the +** SpotifyPlayer instance. If no music is available, it updates the +** UI to display a "Waiting for song..." message, sets the background, +** and logs the event. +** +** Returns: +** true - If the "Waiting for song" message was shown. +** false - If music is available, or no action was taken. +** =================================================================== +*/ +bool UIView::checkAndHandleNoMusicAvailable() +{ + SpotifyPlayer *pSP = &SpotifyPlayer::getInstance(); + + if (pSP->isMusicAvailable() == false) + { + if (!_isWaitingMessageShowing) + { + spLogI(LOGTAG_GUI, "drawCurrentlyPlayingToLCD() - Empty track. Draw Waiting for song..."); + _pUI->setBackground(TFTColor::SC_DJX_BG, true); + _pUI->drawTextToLCD("Music is unavailable",110); + _pUI->drawTextToLCD("Waiting for song...",150); + _isWaitingMessageShowing = true; + } + return _isWaitingMessageShowing; + } + _isWaitingMessageShowing = false; + + return _isWaitingMessageShowing; +} + +/* +** =================================================================== +** handle_UUM_IDLE() +** =================================================================== +*/ +void UIView::handle_UM_IDLE(SCUIMessage *pMessage) +{ + +} \ No newline at end of file diff --git a/src/UIViews/UIView.h b/src/UIViews/UIView.h new file mode 100644 index 0000000..4186ac5 --- /dev/null +++ b/src/UIViews/UIView.h @@ -0,0 +1,96 @@ +/*------------------------------------------------------------------------------------------------- +** +** UIView.h +** +** Abstract base class for UI views in the Spotify controller. Each view defines +** its own rendering behavior and touch handling logic. Provides shared logic +** for message handling, initialization, and screen transitions. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2025-01-21 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ + +#pragma once + +#include +#include "FT6236TouchController/FT6236.h" +#include "SpotifyPlayer.h" +#include "UIElement.h" + +/* +** =================================================================== +** Abstract Base Class: UIView +** +** Purpose: +** Provides an interface for defining display modes in the UI. Each +** display mode defines specific rendering behavior and may include +** common setup logic shared across modes. +** +** Notes: +** - Derived classes must implement the `drawUI` method. +** - Includes a concrete utility method for displaying messages. +** =================================================================== +*/ +class UIView +{ +public: + + UIView(DisplayUI *pUI); + virtual ~UIView() = default; + + // =================================================================== + virtual void drawUI() = 0; + virtual void handleMessage(SCUIMessage *pMessage); + + // =================================================================== + virtual void onTouchDown(const TS_Point& point); + virtual void onTouchUp(); + + // =================================================================== + void initialize(); + +protected: + + // Player + SpotifyPlayer& _spotifyPlayer = SpotifyPlayer::getInstance(); + + // UI Elements + std::unique_ptr _pPauseElement; + std::unique_ptr _pPreviousElement; + std::unique_ptr _pNextElement; + std::unique_ptr _pGotoCoverArtElement; + std::unique_ptr _pGotoDiagnosticElement; + std::unique_ptr _pGotoClockElement; + std::unique_ptr _pReturnViewElement; + + // UI + DisplayUI *_pUI; + bool _isWaitingMessageShowing = false; + + // Protected method to initialize UI elements + virtual void initializeUIElements(); + + // Transitions + virtual void enteringView(); + virtual void leavingView(); + + // UI Messages to support + virtual void handle_UM_IDLE(SCUIMessage *pMessage); // UM_IDLE + virtual void handle_UM_STATUS_BOX(SCUIMessage *pMessage); // UM_STATUS_BOX + virtual void handle_UM_DOWNLOAD_BOX(SCUIMessage *pMessage); // UM_DOWNLOAD_BOX + virtual void handle_UM_MARK_DIRTY(SCUIMessage *pMessage); // UM_MAKE_DIRTY + virtual void handle_UM_PLAYER_REFRESH(SCUIMessage *pMessage); // UM_PLAYER_REFRESH + + // UI Refresh methods + virtual bool checkAndHandleNoMusicAvailable(); + + // Let the UIViewManager to call the transition methods. + friend class UIViewManager; + + +}; diff --git a/src/UIViews/UIViewManager.cpp b/src/UIViews/UIViewManager.cpp new file mode 100644 index 0000000..952c32d --- /dev/null +++ b/src/UIViews/UIViewManager.cpp @@ -0,0 +1,208 @@ +/*------------------------------------------------------------------------------------------------- +** +** UIViewManager.cpp +** +** Manages display modes for the Spotify controller UI. Implements a singleton +** that lazily initializes and switches between different UIView screens using +** a stack to track navigation history and enable return functionality. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2025-01-26 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ + + +#include "UIViewManager.h" +#include "CoverView.h" +#include "HomeView.h" +#include "DiagnosticsView.h" +#include "ClockView.h" + +/* +** =================================================================== +** UIViewManager::UIViewManager() +** Constructor initializes the display manager. +** =================================================================== +*/ +UIViewManager::UIViewManager() +{ +} + +/* +** =================================================================== +** getInstance() +** Returns the singleton instance of UIViewManager. +** =================================================================== +*/ +UIViewManager& UIViewManager::getInstance() +{ + static UIViewManager instance; + return instance; +} + +/* +** =================================================================== +** setDisplayUI() +** Sets the DisplayUI instance to be used by the display modes and +** initializes the display modes if they haven't been initialized. +** +** Parameters: +** pUI - Pointer to the DisplayUI instance. +** =================================================================== +*/ +void UIViewManager::setDisplayUI(DisplayUI *pUI) +{ + if (!_isInitialized + && pUI != nullptr) + { + initializeViews(pUI); + _isInitialized = true; + } +} + +/* +** =================================================================== +** getActiveView() +** Returns the current display mode. +** +** Returns: +** The currently active display mode as a UIView pointer. +** =================================================================== +*/ +UIView* UIViewManager::getActiveView() const +{ + return _pCurrentView; +} + +/* +** =================================================================== +** setViewID() +** Changes the active display mode. +** +** Parameters: +** id - ViewID representing the new display mode. +** =================================================================== +*/ +void UIViewManager::setViewID(ViewID id) +{ + if (_pCurrentView) + { + _pCurrentView->leavingView(); + } + + _pCurrentView = _views[static_cast(id)].get(); + _currentViewID = id; + + if (_pCurrentView) + { + _pCurrentView->enteringView(); + } +} + +/* +** =================================================================== +** advanceView() +** Advances to the next display mode in sequence. +** =================================================================== +*/ +void UIViewManager::advanceView() +{ + if (!_isInitialized) + { + return; // Do nothing if not initialized + } + + // Determine the next view in the sequence + ViewID nextView = static_cast((static_cast(_currentViewID) + 1) % NUM_DISPLAY_MODES); + + // Switch to the next view + setViewID(nextView); +} + +/* +** =================================================================== +** gotoView() +** Sets the active view to the given view ID, pushing the current +** view onto the stack before switching. +** +** Parameters: +** id - ViewID representing the new view to switch to. +** =================================================================== +*/ +void UIViewManager::gotoView(ViewID id) +{ + if (_currentViewID == id) + { + return; // Already in the requested view + } + + if (static_cast(id) < NUM_DISPLAY_MODES) + { + // Push current view onto stack + _viewStack.push(_currentViewID); + + // Set new active view + setViewID(id); + } +} + +/* +** =================================================================== +** exitView() +** Returns to the previous view if possible. If the stack is empty, +** no action is taken and false is returned. +** +** Returns: +** true - If successfully switched to a previous view. +** false - If already at the root view. +** =================================================================== +*/ +bool UIViewManager::exitView() +{ + if (!_viewStack.empty()) + { + // Get the last view from the stack + ViewID previousView = _viewStack.top(); + _viewStack.pop(); + + // Set the active view to the previous view + setViewID(previousView); + return true; + } + + return false; // No previous view to return to +} + +/* +** =================================================================== +** initializeViews() +** Initializes the display modes using the provided DisplayUI +** instance. +** +** Parameters: +** pUI - Pointer to the DisplayUI instance. +** =================================================================== +*/ +void UIViewManager::initializeViews(DisplayUI *pUI) +{ + _views = { + std::make_unique(pUI), + std::make_unique(pUI), + std::make_unique(pUI), + std::make_unique(pUI) + }; + + // Call initialize() on each view to ensure UI elements are properly set up + for (auto& view : _views) { + view->initialize(); + } + + // Set the initial mode to Home + _currentViewID = ViewID::Home; + _pCurrentView = _views[static_cast(_currentViewID)].get(); + _pCurrentView->enteringView(); +} \ No newline at end of file diff --git a/src/UIViews/UIViewManager.h b/src/UIViews/UIViewManager.h new file mode 100644 index 0000000..2d57067 --- /dev/null +++ b/src/UIViews/UIViewManager.h @@ -0,0 +1,92 @@ +/*------------------------------------------------------------------------------------------------- +** +** UIViewManager.h +** +** Manages display modes for the Spotify controller UI. Implements a singleton +** that lazily initializes and switches between different UIView screens using +** a stack to track navigation history and enable return functionality. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2025-01-26 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ + +#pragma once + +#include "UIView.h" +#include +#include +#include + +/* +** =================================================================== +** UIViewManager +** +** Manages the different display modes for the UI. This class is +** implemented as a singleton and lazily initializes its display +** modes when `setDisplayUI()` is called with a valid DisplayUI +** instance. +** +** Responsibilities: +** - Manages and switches between different display modes. +** - Lazily initializes modes based on the provided DisplayUI. +** - Supports navigation history with a stack for returning to +** the previous view. +** =================================================================== +*/ +class UIViewManager +{ +public: + // Enum representing supported display modes + enum class ViewID : uint8_t + { + Home, // Show Song, Album, and Artist(s) over cover art + Cover, // Show only the cover art + Diagnostics, // System Stats + Clock // Clock View + }; + + // Returns the singleton instance of UIViewManager + static UIViewManager &getInstance(); + + // Sets the DisplayUI instance to be used by the display modes and + // initializes the display modes if they haven’t been initialized. + void setDisplayUI(DisplayUI *pUI); + + // Retrieves the current display mode + UIView *getActiveView() const; + + // Sets the current display mode + void setViewID(ViewID id); + + // Advances to the next display mode + void advanceView(); + + // Sets the active view to the given view ID, pushing the current + // view onto the stack before switching. + void gotoView(ViewID id); + + // Returns to the previous view if possible. Returns false if + // already at the root view. + bool exitView(); + +private: + // Private constructor to enforce singleton pattern + UIViewManager(); + + // Initializes all display modes + void initializeViews(DisplayUI *pUI); + + // Number of display modes (static constexpr to avoid compile errors) + static constexpr uint8_t NUM_DISPLAY_MODES = static_cast(ViewID::Clock) + 1; + + std::array, NUM_DISPLAY_MODES> _views; // List of all display modes + UIView * _pCurrentView = nullptr; // Active display mode + ViewID _currentViewID = ViewID::Home; // Tracks current ViewID + bool _isInitialized = false; // Initialization flag + std::stack _viewStack; // Stack for tracking navigation history +}; \ No newline at end of file diff --git a/src/Vault.cpp b/src/Vault.cpp new file mode 100644 index 0000000..dd5801c --- /dev/null +++ b/src/Vault.cpp @@ -0,0 +1,737 @@ +/*------------------------------------------------------------------------------------------------- +** +** Vault.cpp +** +** Implementation of the Vault class, which securely manages Spotify and Wi-Fi credentials +** for the ESP32 Spotify controller. Supports plaintext and encrypted storage models using +** AES-128 encryption with optional device binding via MAC address. +** +** Credentials can be sourced from either a user-editable /user.ini file or hardcoded values +** from settings.h. Privacy levels (None, Good, Better) determine how credentials are stored, +** decrypted, and exposed to the runtime. +** +** Encryption is done using AES-128 in Electronic Codebook (ECB) mode with Public-Key Cryptography +** Standards #7 (PKCS#7) padding. ECB mode encrypts each 16-byte block independently, which is simple +** and efficient but less secure for large or repetitive data due to potential pattern leakage. +** PKCS#7 padding appends a series of bytes—each equal to the number of bytes added—to ensure the +** input length is a multiple of the AES block size. The derived encryption key is generated from a +** fixed internal base key and can be optionally augmented with user-defined salt and/or the device’s +** MAC address to enhance uniqueness and security. See implementations of encrypt() and decrypt() +** for more details. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2025-06-03 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ + +#include +#include "Vault.h" +#include "settings.h" +#include "SCLogger.h" +#include "logTags.h" +#include +#include "mbedtls/base64.h" + +#include "mbedtls/aes.h" + +/* +** =================================================================== +** toString(VaultPrivacyLevel) +** +** Converts a VaultPrivacyLevel enum value to its corresponding +** string representation for logging, diagnostics, and debugging. +** =================================================================== +*/ +const char* toString(VaultPrivacyLevel level) +{ + switch (level) + { + case VaultPrivacyLevel::None: return "None"; + case VaultPrivacyLevel::Good: return "Good"; + case VaultPrivacyLevel::Better: return "Better"; + default: return "Unknown"; + } +} + +/* +** =================================================================== +** toString(CredentialSource) +** +** Converts a CredentialSource enum value to its corresponding +** string representation for logging, diagnostics, and debugging. +** =================================================================== +*/ +const char* toString(CredentialSource source) +{ + switch (source) + { + case CredentialSource::Hardcoded: return "Hardcoded"; + case CredentialSource::INIFile: return "INIFile"; + default: return "Unknown"; + } +} + + +/* +** =================================================================== +** getInstance() +** +** Returns the singleton instance of Vault. Initializes it if needed. +** =================================================================== +*/ +Vault& Vault::getInstance() +{ + static Vault instance; // Guaranteed to be thread-safe in C++11 and later + return instance; +} + +/* +** =================================================================== +** Vault() +** +** Private constructor for the singleton class. +** =================================================================== +*/ +Vault::Vault() +{ + memset(_ssid.data(), 0, _ssid.size()); + memset(_wifiPwd.data(), 0, _wifiPwd.size()); + memset(_clientId.data(), 0, _clientId.size()); + memset(_clientSecret.data(), 0, _clientSecret.size()); + memset(_timezone.data(), 0, _timezone.size()); + memset(_salt.data(), 0, _salt.size()); +} + +/* +** =================================================================== +** initialize() +** +** Loads credential and configuration values from /user.ini into +** internal secure buffers. Falls back to hardcoded values from +** settings.h if the file does not exist or cannot be opened. +** +** The file is parsed section by section (e.g., [wifi], [spotify], +** [system], [vault]) and supports optional configuration values +** such as key_salt and privacy_level. +** +** If privacy_level is set to None, encryption hints will be +** printed to the serial console to assist with migration to +** encrypted credentials. +** +** Effects: +** - Sets _useHardcodedValues flag +** - Populates _ssid, _wifiPwd, _clientId, _clientSecret, _timezone +** - May set _salt and _privacyLevel if defined +** =================================================================== +*/ +void Vault::initialize() +{ + constexpr const char* USER_INI_PATH = "/user.ini"; + + if (!LittleFS.exists(USER_INI_PATH)) + { + _useHardcodedValues = true; + spLogI(LOGTAG_VAULT, "%s does not exist. Using hardcoded credentials.", USER_INI_PATH); + return; + } + + File file = LittleFS.open(USER_INI_PATH, "r"); + if (!file) + { + _useHardcodedValues = true; + spLogE(LOGTAG_VAULT, "Unable to open %s. Unable to load network credentials.", USER_INI_PATH); + return; + } + + _useHardcodedValues = false; + + String section; + while (file.available()) + { + String line = file.readStringUntil('\n'); + line.trim(); + + if (line.startsWith(";") || line.isEmpty()) + { + continue; // Skip comments and empty lines + } + + if (line.startsWith("[") && line.endsWith("]")) + { + section = line.substring(1, line.length() - 1); + section.toLowerCase(); + continue; + } + + int equalsIndex = line.indexOf('='); + if (equalsIndex == -1) continue; + + String key = line.substring(0, equalsIndex); + String value = line.substring(equalsIndex + 1); + key.trim(); + value.trim(); + + if (section == "wifi") + { + if (key == "ssid") + { + strncpy(_ssid.data(), value.c_str(), _ssid.size() - 1); + } + else if (key == "password") + { + strncpy(_wifiPwd.data(), value.c_str(), _wifiPwd.size() - 1); + } + } + else if (section == "spotify") + { + if (key == "client_id") + { + strncpy(_clientId.data(), value.c_str(), _clientId.size() - 1); + } + else if (key == "client_secret") + { + strncpy(_clientSecret.data(), value.c_str(), _clientSecret.size() - 1); + } + } + else if (section == "system") + { + if (key == "timezone") + { + strncpy(_timezone.data(), value.c_str(), _timezone.size() - 1); + } + else if (key == "ui_date_time_format") + { + value.toLowerCase(); + _dateTimeFormatUS = (value == "us"); + } + } + else if (section == "vault") + { + if (key == "key_salt") + { + if (value.length() > (_salt.size() - 1)) + { + value = value.substring(0, _salt.size() - 1); + } + strncpy(_salt.data(), value.c_str(), _salt.size() - 1); + _salt[_salt.size() - 1] = '\0'; // ensure null-termination + } + else if (key == "privacy_level") + { + int levelValue = value.toInt(); + if (levelValue == 1) + { + _privacyLevel = VaultPrivacyLevel::Good; + } + else if (levelValue == 2) + { + _privacyLevel = VaultPrivacyLevel::Better; + } + else + { + _privacyLevel = VaultPrivacyLevel::None; + } + } + } + } + + file.close(); + + if (_privacyLevel == VaultPrivacyLevel::None) + { + printEncryptionHints(); + } + + spLogI(LOGTAG_VAULT, "Initialization complete. Privacy Level: %s. _useHardcodedValues: %s", toString(_privacyLevel), _useHardcodedValues ? "true" : "false"); + +} + +/* +** =================================================================== +** setPrivacyLevel() +** +** Sets the active privacy level for credential handling. +** +** Privacy levels: +** - None (0): Use plaintext credentials from user.ini or settings.h +** - Good (1): Use encrypted credentials not tied to device (reusable) +** - Better (2): Use encrypted credentials tied to this device's MAC address +** +** Parameters: +** level - The desired privacy level +** =================================================================== +*/ +void Vault::setPrivacyLevel(VaultPrivacyLevel level) +{ + _privacyLevel = level; +} + +/* +** =================================================================== +** getPrivacyLevel() +** +** Returns the currently active privacy level that determines +** how credentials are retrieved and decrypted. +** +** Privacy levels: +** - None (0): Use plaintext credentials from user.ini or settings.h +** - Good (1): Use encrypted credentials not tied to device (reusable) +** - Better (2): Use encrypted credentials tied to this device's MAC address +** +** =================================================================== +*/ +VaultPrivacyLevel Vault::getPrivacyLevel() +{ + return _privacyLevel; +} + +/* +** =================================================================== +** getCredentialSource() +** +** Returns the current source of credentials in use. +** If _useHardcodedValues is true, credentials are coming from +** settings.h; otherwise, they are sourced from user.ini. +** +** Returns: +** CredentialSource enum value indicating the source. +** =================================================================== +*/ +CredentialSource Vault::getCredentialSource() +{ + return _useHardcodedValues ? CredentialSource::Hardcoded : CredentialSource::INIFile; +} + +/* +** =================================================================== +** getSSID() +** +** Retrieves the Wi-Fi SSID. If user.ini is not present or unreadable, +** the SSID defined in settings.h is used directly. +** =================================================================== +*/ +String Vault::getSSID() +{ + if (_useHardcodedValues) + { + return SSID; + } + switch (_privacyLevel) + { + case VaultPrivacyLevel::Good: + return decrypt(_ssid.data(), false); + case VaultPrivacyLevel::Better: + return decrypt(_ssid.data(), true); + default: + // Privacy Level None + return String(_ssid.data()); + } +} + +/* +** =================================================================== +** getWiFiPassword() +** +** Retrieves the Wi-Fi password. If user.ini is not present or unreadable, +** the password defined in settings.h is used directly. +** =================================================================== +*/ +String Vault::getWiFiPassword() +{ + if (_useHardcodedValues) + { + return WIFI_PWD; + } + switch (_privacyLevel) + { + case VaultPrivacyLevel::Good: + return decrypt(_wifiPwd.data(), false); + case VaultPrivacyLevel::Better: + return decrypt(_wifiPwd.data(), true); + default: + // Privacy Level None + return String(_wifiPwd.data()); + } +} + +/* +** =================================================================== +** getSpotifyClientID() +** +** Retrieves the Spotify Client ID. If user.ini is not present or unreadable, +** the client ID defined in settings.h is used directly. +** =================================================================== +*/ +String Vault::getSpotifyClientID() +{ + spLogV(LOGTAG_VAULT, "_clientId is %s", String(_clientId.data()).c_str()); + if (_useHardcodedValues) + { + spLogV(LOGTAG_VAULT, "Returning hardcode SPOTIFY_CLIENT_ID value."); + return SPOTIFY_CLIENT_ID; + } + switch (_privacyLevel) + { + case VaultPrivacyLevel::Good: + return decrypt(_clientId.data(), false); + case VaultPrivacyLevel::Better: + return decrypt(_clientId.data(), true); + default: + // Privacy Level None + return String(_clientId.data()); + } +} + +/* +** =================================================================== +** getSpotifyClientSecret() +** +** Retrieves the Spotify Client Secret. If user.ini is not present or unreadable, +** the client secret defined in settings.h is used directly. +** =================================================================== +*/ +String Vault::getSpotifyClientSecret() +{ + if (_useHardcodedValues) + { + return SPOTIFY_CLIENT_SECRET; + } + switch (_privacyLevel) + { + case VaultPrivacyLevel::Good: + return decrypt(_clientSecret.data(), false); + case VaultPrivacyLevel::Better: + return decrypt(_clientSecret.data(), true); + default: + // Privacy Level None + return String(_clientSecret.data()); + } +} + +/* +** =================================================================== +** getTimezone() +** +** Returns the configured timezone. If user.ini is not present or unreadable, +** the timezone defined in settings.h is used directly. +** =================================================================== +*/ +String Vault::getTimezone() +{ + if (_useHardcodedValues) + { + return TIMEZONE; + } + return String(_timezone.data()); +} + + +/** + * =================================================================== + * encrypt() + * + * Encrypts a plaintext input string using AES-128 in ECB mode, + * then encodes the encrypted binary output as a Base64 string. + * + * The encryption key is derived from a fixed internal base key, + * optionally mixed with a user-defined salt and/or the device's + * MAC address depending on the privacy level. + * + * PKCS#7 padding is applied manually to make the input length + * a multiple of the AES block size (16 bytes). + * + * ECB mode encrypts each 16-byte block independently using the + * same key. While simple and fast, ECB mode is less secure than + * modes like CBC for long or repetitive inputs. + * + * Parameters: + * input - Null-terminated C string to encrypt + * tiedToDevice - Whether to incorporate the device MAC address + * into the key derivation for device-specific encryption + * + * Returns: + * A Base64-encoded encrypted string + * =================================================================== + */ +String Vault::encrypt(const char* input, bool tiedToDevice) +{ + // Define key and block sizes for AES-128 encryption + const size_t AES_KEY_SIZE = 16; + const size_t AES_BLOCK_SIZE = 16; + + // Get the AES key to use for the encryption + std::array finalKeyArr = getAesKey(tiedToDevice); + const uint8_t* finalKey = finalKeyArr.data(); + + // Pad the input string using PKCS#7 to ensure its length is a multiple of the AES block size. + // PKCS#7 (Public-Key Cryptography Standards #7) defines a padding scheme that appends N bytes, + // each with the value N, where N is the number of padding bytes needed to reach the next multiple + // of the block size. For example, if 5 bytes of padding are required, the value 0x05 will be + // written five times at the end of the input. This is implemented manually here and is standard + // practice in symmetric encryption to support inputs of arbitrary length. + size_t inputLen = strlen(input); + size_t paddedLen = ((inputLen / AES_BLOCK_SIZE) + 1) * AES_BLOCK_SIZE; + uint8_t* paddedInput = new uint8_t[paddedLen]; + memcpy(paddedInput, input, inputLen); + uint8_t padValue = paddedLen - inputLen; + memset(paddedInput + inputLen, padValue, padValue); + + // Encrypt the padded input using AES-128 in ECB (Electronic Codebook) mode. + // ECB mode encrypts each 16-byte block independently using the same key. + // While simple and fast, ECB mode is less secure for long or patterned inputs, + // as identical plaintext blocks produce identical ciphertext blocks. + uint8_t* cipherText = new uint8_t[paddedLen]; + mbedtls_aes_context aes; + mbedtls_aes_init(&aes); + mbedtls_aes_setkey_enc(&aes, finalKey, 128); + + for (size_t i = 0; i < paddedLen; i += AES_BLOCK_SIZE) + { + mbedtls_aes_crypt_ecb(&aes, MBEDTLS_AES_ENCRYPT, paddedInput + i, cipherText + i); + } + + mbedtls_aes_free(&aes); + + // Encode the resulting ciphertext in Base64 to produce a printable string + size_t base64Len = 0; + size_t base64BufLen = (paddedLen * 4) / 3 + 4; + uint8_t* base64Buf = new uint8_t[base64BufLen]; + mbedtls_base64_encode(base64Buf, base64BufLen, &base64Len, cipherText, paddedLen); + + // Convert the Base64 output buffer to a String to return from the function + String result(reinterpret_cast(base64Buf)); + + // Free all dynamically allocated memory + delete[] paddedInput; + delete[] cipherText; + delete[] base64Buf; + + return result; +} + +/** + * =================================================================== + * decrypt() + * + * Decrypts a Base64-encoded AES-128 encrypted string using the + * same ECB mode and key derivation logic as encrypt(). + * + * The encryption key is derived from the internal base key and + * may also include a user-defined salt and/or the device MAC + * address, depending on the tiedToDevice parameter. + * + * The input string is first Base64-decoded to obtain the raw + * encrypted bytes, then decrypted in ECB mode using AES-128. + * Finally, PKCS#7 padding is removed to recover the original + * plaintext. + * + * Parameters: + * encryptedBase64 - The Base64-encoded AES-encrypted string + * tiedToDevice - Whether the key derivation includes the + * device MAC address (for Better privacy) + * + * Returns: + * The decrypted plaintext as a String + * =================================================================== + */ +String Vault::decrypt(const String& encryptedBase64, bool tiedToDevice) +{ + // Define key and block sizes for AES-128 encryption + const size_t AES_KEY_SIZE = 16; + const size_t AES_BLOCK_SIZE = 16; + + // Get the AES key to use for the decryption + std::array finalKeyArr = getAesKey(tiedToDevice); + const uint8_t* finalKey = finalKeyArr.data(); + + // Decode the Base64-encoded input back into raw encrypted bytes + size_t encryptedLen = (encryptedBase64.length() * 3) / 4; + uint8_t* encryptedBytes = new uint8_t[encryptedLen]; + size_t actualLen = 0; + mbedtls_base64_decode(encryptedBytes, encryptedLen, &actualLen, + reinterpret_cast(encryptedBase64.c_str()), + encryptedBase64.length()); + + // Decrypt the raw encrypted bytes using AES-128 in ECB (Electronic Codebook) mode + // This reverses the block-by-block encryption performed in encrypt() + uint8_t* plainText = new uint8_t[actualLen]; + mbedtls_aes_context aes; + mbedtls_aes_init(&aes); + mbedtls_aes_setkey_dec(&aes, finalKey, 128); + + for (size_t i = 0; i < actualLen; i += AES_BLOCK_SIZE) + { + mbedtls_aes_crypt_ecb(&aes, MBEDTLS_AES_DECRYPT, encryptedBytes + i, plainText + i); + } + + mbedtls_aes_free(&aes); + + // Remove the PKCS#7 padding added during encryption + // Each padding byte should equal the number of padding bytes added (e.g., 0x05 five times) + // This determines the true original length of the decrypted plaintext + uint8_t padValue = plainText[actualLen - 1]; + if (padValue > AES_BLOCK_SIZE) + { + padValue = 0; + } + size_t unpaddedLen = actualLen - padValue; + + // Convert the unpadded plaintext bytes to a String + String result(reinterpret_cast(plainText), unpaddedLen); + + // Free all dynamically allocated memory + delete[] encryptedBytes; + delete[] plainText; + + return result; +} + +/* +** =================================================================== +** eraseEncryptedCredentials() +** +** Zeroes out the secure credential arrays. +** =================================================================== +*/ +void Vault::eraseEncryptedCredentials() +{ + memset(_ssid.data(), 0, _ssid.size()); + memset(_wifiPwd.data(), 0, _wifiPwd.size()); + memset(_clientId.data(), 0, _clientId.size()); + memset(_clientSecret.data(), 0, _clientSecret.size()); +} + +/* +** =================================================================== +** printEncryptionHints() +** +** Prints hints on what encrypted values to use for the +** various privacy controlled fields. The hints are only printed +** when the privacy level is None and credentials are assumed to be +** in the clear within the user.ini file. If hardcoded values from +** settings.h are in use, hints will not print. +** +** =================================================================== +*/ +void Vault::printEncryptionHints() +{ + if (_useHardcodedValues + || (_privacyLevel != VaultPrivacyLevel::None)) + { + return; + } + + Serial.println(); + Serial.println("======================================================================"); + Serial.println("Paste these into your user.ini file to enable encrypted credentials."); + Serial.println("======================================================================"); + Serial.println(); + Serial.println("; Values to use for Good Privacy (encrypted, reusable across devices):"); + Serial.println("privacy_level = 1"); + Serial.printf("ssid = %s\n", encrypt(_ssid.data(), false).c_str()); + Serial.printf("password = %s\n", encrypt(_wifiPwd.data(), false).c_str()); + Serial.printf("client_id = %s\n", encrypt(_clientId.data(), false).c_str()); + Serial.printf("client_secret = %s\n", encrypt(_clientSecret.data(), false).c_str()); + Serial.println(); + Serial.println("; Values to use for Better Privacy (encrypted, tied to this device):"); + Serial.println("privacy_level = 2"); + Serial.printf("ssid = %s\n", encrypt(_ssid.data(), true).c_str()); + Serial.printf("password = %s\n", encrypt(_wifiPwd.data(), true).c_str()); + Serial.printf("client_id = %s\n", encrypt(_clientId.data(), true).c_str()); + Serial.printf("client_secret = %s\n", encrypt(_clientSecret.data(), true).c_str()); + Serial.println("======================================================================"); +} + +/** + * =================================================================== + * getAesKey() + * + * Derives the AES-128 encryption key used for securing credentials. + * This key is constructed from a fixed internal base key and can be + * optionally augmented with a user-defined salt and the device's MAC + * address to increase uniqueness and security. + * + * - The base key is obscured via a transformation using a static + * pattern to avoid storing it directly in firmware. + * - If a salt is defined in user.ini, it is XORed into the key. + * - If tiedToDevice is true, the MAC address is also XORed into the key. + * + * Parameters: + * tiedToDevice - Whether to incorporate the device's MAC address + * into the key derivation (for Better privacy). + * + * Returns: + * A fully derived 128-bit AES key as a std::array. + * =================================================================== + */ +std::array Vault::getAesKey(bool tiedToDevice) +{ + // The baseKey is a static 128-bit value used as the foundation for AES key derivation. + // It is intentionally non-obvious and will be transformed before use to avoid storing + // the working encryption key directly in firmware. + const uint8_t baseKey[16] = { 0x25, 0xA1, 0x33, 0x42, 0x59, 0x1C, 0xEF, 0x73, + 0x91, 0xF0, 0xAB, 0xCC, 0x14, 0xD5, 0x63, 0x1E }; + + std::array finalKey; + memcpy(finalKey.data(), baseKey, 16); + + // Transform the base key using a fixed pattern to derive the working key. + // This step ensures the actual AES key is not directly stored in memory as-is. + for (int i = 0; i < 16; ++i) + { + // XOR each byte of the base key with a fixed constant (0xA5) and a variable + // offset (i * 13). This step transforms the base key into the working AES key, + // obscuring it from plain inspection + finalKey[i] ^= 0xA5 ^ (i * 13); + finalKey[i] ^= 0xA5 ^ (i * 13); + } + + // If a user-defined salt is present, incorporate it into the key derivation + if (_salt[0] != '\0') + { + // XOR each byte of the salt into the key, wrapping if the salt is longer than 16 bytes + for (size_t i = 0; i < strlen(_salt.data()); ++i) + { + finalKey[i % 16] ^= _salt[i]; + } + } + + // If the key should be tied to the device, incorporate the ESP32's MAC address + // This introduces a unique device-specific element into the key derivation + if (tiedToDevice) + { + // esp_read_mac returns a 6-byte MAC for ESP_MAC_WIFI_STA, ESP_MAC_BT, etc. + // enumerator ESP_MAC_WIFI_STA - MAC for WiFi Station (6 bytes) + // https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/misc_system_api.html + uint8_t mac[6]; + esp_read_mac(mac, ESP_MAC_WIFI_STA); + for (int i = 0; i < 6; ++i) + { + finalKey[i] ^= mac[i]; + finalKey[15 - i] ^= mac[i]; + } + } + + return finalKey; +} + +/* +** =================================================================== +** isUSDateTimeFormattingUsed() +** +** Determines whether US-style date/time formatting should be used. +** =================================================================== +*/ +bool Vault::isUSDateTimeFormattingUsed() +{ + if (_useHardcodedValues) + { + return Use_US_Date_Time_Format; + } + + return _dateTimeFormatUS; +} \ No newline at end of file diff --git a/src/Vault.h b/src/Vault.h new file mode 100644 index 0000000..90ca130 --- /dev/null +++ b/src/Vault.h @@ -0,0 +1,99 @@ +/*------------------------------------------------------------------------------------------------- +** +** Vault.h +** +** Definition of the Vault class, which securely manages Spotify and Wi-Fi credentials +** for the ESP32 Spotify controller. Supports plaintext and encrypted storage models using +** AES-128 encryption with optional device binding via MAC address. +** +** Credentials can be sourced from either a user-editable /user.ini file or hardcoded values +** from settings.h. Privacy levels (None, Good, Better) determine how credentials are stored, +** decrypted, and exposed to the runtime. +** +** Encryption is done using AES-128 in Electronic Codebook (ECB) mode with Public-Key Cryptography +** Standards #7 (PKCS#7) padding. ECB mode encrypts each 16-byte block independently, which is simple +** and efficient but less secure for large or repetitive data due to potential pattern leakage. +** PKCS#7 padding appends a series of bytes—each equal to the number of bytes added—to ensure the +** input length is a multiple of the AES block size. The derived encryption key is generated from a +** fixed internal base key and can be optionally augmented with user-defined salt and/or the device’s +** MAC address to enhance uniqueness and security. See implementations of encrypt() and decrypt() +** for more details. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2025-06-03 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ + +#pragma once + +#include +#include + +// Max sizes (unencrypted): +// SSID - Per 802.11 standard, 32 characters +// WiFi Password - WPA2/WPA3 maxiumum password length 64 characters +// Spotify Client Id - 32 characters +// Spotify Client Secret - 40 characters +// With encryption and base64 encoding, lengths could go as high a 108 characters. +#define VAULT_MAX_CRED_LENGTH 150 + +// Defines the levels of credential privacy supported by the Vault. +// These levels determine how credentials are retrieved, encrypted, +// and protected during runtime. +enum class VaultPrivacyLevel +{ + None, // Use plaintext credentials; either from user.ini or hardcoded settings + Good, // Use AES-encrypted credentials; not tied to device, reusable across units + Better // Use AES-encrypted credentials; tied to device MAC address, non-reusable +}; + +// Defines credential source +enum class CredentialSource +{ + Hardcoded, // Plaintext hardcoded values + INIFile, // Loaded values from the ini file +}; + +const char* toString(VaultPrivacyLevel level); +const char* toString(CredentialSource source); + +// Vault definition +class Vault +{ +public: + static Vault& getInstance(); + + void setPrivacyLevel(VaultPrivacyLevel level); + VaultPrivacyLevel getPrivacyLevel(); + CredentialSource getCredentialSource(); + + void initialize(); + void eraseEncryptedCredentials(); + + String getSSID(); + String getWiFiPassword(); + String getSpotifyClientID(); + String getSpotifyClientSecret(); + String getTimezone(); + bool isUSDateTimeFormattingUsed(); + +private: + Vault(); + std::array getAesKey(bool tiedToDevice); + String encrypt(const char* input, bool tiedToDevice); + String decrypt(const String& encryptedBase64, bool tiedToDevice); + void printEncryptionHints(); + bool _useHardcodedValues = true; + VaultPrivacyLevel _privacyLevel = VaultPrivacyLevel::None; + bool _dateTimeFormatUS = false; + std::array _ssid{}; + std::array _wifiPwd{}; + std::array _clientId{}; + std::array _clientSecret{}; + std::array _timezone{}; + std::array _salt{}; +}; \ No newline at end of file diff --git a/src/connectivity.h b/src/connectivity.h deleted file mode 100644 index c9cddf7..0000000 --- a/src/connectivity.h +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-FileCopyrightText: 2023 ThingPulse Ltd., https://thingpulse.com -// SPDX-License-Identifier: MIT - -#pragma once - -#include - -#include "settings.h" - -void startWiFi() { - WiFi.begin(SSID, WIFI_PWD); - log_i("Connecting to WiFi '%s'...", SSID); - while (WiFi.status() != WL_CONNECTED) { - log_i("."); - delay(200); - } - log_i("...done. IP: %s, WiFi RSSI: %d.", WiFi.localIP().toString().c_str(), WiFi.RSSI()); -} diff --git a/src/fonts/cousine-bold.h b/src/fonts/cousine-bold.h new file mode 100644 index 0000000..fee3652 --- /dev/null +++ b/src/fonts/cousine-bold.h @@ -0,0 +1,1049 @@ +/*------------------------------------------------------------------------------------------------- +** +** cousine-bold.h +** +** Contains the binary font data for the Cousine Bold typeface used in the Spotify display UI. +** This header defines the glyph data for rendering fixed-width bold text on screen. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2025-03-21 - Electric Diversions - Initial version extracted from font resources +** ------------------------------------------------------------------------------------------------ +*/ + + +/* + * This file contains a C array representation of the binary file 'Cousine-Bold-subset.ttf'. + * Generated on: 2025-03-21 23:06:58 + * + * Usage: + * 1. Include this header file in your C project. + * 2. Access the binary data via the array: 'Cousine-Bold-subset[]'. + * 3. The total size of the data is stored in 'Cousine-Bold-subset_len'. + * + * Characters 'AMP1234567890 :' + * + * This file includes data from the Cousine font, which is licensed under the Apache License, Version 2.0. + * + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file contains a binary representation of the Cousine font, + * suitable for embedded use in C projects. + */ + + #ifndef COUSINEBOLDSUBSET_H + #define COUSINEBOLDSUBSET_H + + const unsigned char cousineBold[9928] = { + 0x00, 0x01, 0x00, 0x00, 0x00, 0x12, 0x01, 0x00, 0x00, 0x04, + 0x00, 0x20, 0x46, 0x46, 0x54, 0x4D, 0x86, 0x87, 0x97, 0x7C, + 0x00, 0x00, 0x26, 0xAC, 0x00, 0x00, 0x00, 0x1C, 0x47, 0x44, + 0x45, 0x46, 0x00, 0x29, 0x00, 0x18, 0x00, 0x00, 0x21, 0x04, + 0x00, 0x00, 0x00, 0x1E, 0x47, 0x50, 0x4F, 0x53, 0x0E, 0x3F, + 0x08, 0xE2, 0x00, 0x00, 0x21, 0x78, 0x00, 0x00, 0x05, 0x34, + 0x47, 0x53, 0x55, 0x42, 0xCC, 0x4B, 0xD0, 0x25, 0x00, 0x00, + 0x21, 0x24, 0x00, 0x00, 0x00, 0x54, 0x4F, 0x53, 0x2F, 0x32, + 0xFC, 0xCF, 0xDB, 0x6D, 0x00, 0x00, 0x01, 0xA8, 0x00, 0x00, + 0x00, 0x60, 0x63, 0x6D, 0x61, 0x70, 0x21, 0xD2, 0x30, 0xF3, + 0x00, 0x00, 0x02, 0x34, 0x00, 0x00, 0x01, 0x62, 0x63, 0x76, + 0x74, 0x20, 0x8E, 0x54, 0x7C, 0x7B, 0x00, 0x00, 0x11, 0x48, + 0x00, 0x00, 0x02, 0xB2, 0x66, 0x70, 0x67, 0x6D, 0x36, 0x0B, + 0x16, 0x0C, 0x00, 0x00, 0x03, 0x98, 0x00, 0x00, 0x07, 0xB4, + 0x67, 0x61, 0x73, 0x70, 0x00, 0x16, 0x00, 0x23, 0x00, 0x00, + 0x20, 0xF4, 0x00, 0x00, 0x00, 0x10, 0x67, 0x6C, 0x79, 0x66, + 0xAB, 0xE9, 0xAD, 0xBE, 0x00, 0x00, 0x14, 0x24, 0x00, 0x00, + 0x05, 0xF4, 0x68, 0x65, 0x61, 0x64, 0x10, 0x42, 0x27, 0x3B, + 0x00, 0x00, 0x01, 0x2C, 0x00, 0x00, 0x00, 0x36, 0x68, 0x68, + 0x65, 0x61, 0x0B, 0x78, 0x02, 0x6B, 0x00, 0x00, 0x01, 0x64, + 0x00, 0x00, 0x00, 0x24, 0x68, 0x6D, 0x74, 0x78, 0x12, 0xC1, + 0x03, 0x10, 0x00, 0x00, 0x02, 0x08, 0x00, 0x00, 0x00, 0x2C, + 0x6C, 0x6F, 0x63, 0x61, 0x0D, 0x26, 0x0B, 0xD4, 0x00, 0x00, + 0x13, 0xFC, 0x00, 0x00, 0x00, 0x26, 0x6D, 0x61, 0x78, 0x70, + 0x04, 0x5B, 0x00, 0x96, 0x00, 0x00, 0x01, 0x88, 0x00, 0x00, + 0x00, 0x20, 0x6E, 0x61, 0x6D, 0x65, 0x59, 0x8D, 0xD5, 0x9F, + 0x00, 0x00, 0x1A, 0x18, 0x00, 0x00, 0x06, 0x93, 0x70, 0x6F, + 0x73, 0x74, 0xFF, 0x0B, 0x01, 0x90, 0x00, 0x00, 0x20, 0xAC, + 0x00, 0x00, 0x00, 0x46, 0x70, 0x72, 0x65, 0x70, 0x0F, 0x27, + 0xA4, 0x89, 0x00, 0x00, 0x0B, 0x4C, 0x00, 0x00, 0x05, 0xFA, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x36, 0x04, 0xB7, 0xB2, + 0x77, 0xC7, 0x5F, 0x0F, 0x3C, 0xF5, 0x00, 0x0B, 0x08, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xC8, 0x50, 0xBF, 0x5E, 0x00, 0x00, + 0x00, 0x00, 0xE4, 0x03, 0xE7, 0x99, 0x00, 0x00, 0xFF, 0xE9, + 0x04, 0xCD, 0x05, 0x5A, 0x00, 0x01, 0x00, 0x08, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x06, 0xA9, 0xFD, 0x99, 0x00, 0x00, 0x04, 0xCD, 0x00, 0x00, + 0x00, 0x00, 0x04, 0xCD, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x12, 0x00, 0x37, + 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x10, + 0x00, 0x2F, 0x00, 0x5C, 0x00, 0x00, 0x03, 0xD9, 0x00, 0x2E, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x04, 0xCD, 0x02, 0xBC, + 0x00, 0x05, 0x00, 0x00, 0x05, 0x9A, 0x05, 0x33, 0x00, 0x00, + 0x01, 0x1D, 0x05, 0x9A, 0x05, 0x33, 0x00, 0x00, 0x03, 0x61, + 0x00, 0x66, 0x02, 0x12, 0x08, 0x05, 0x02, 0x07, 0x07, 0x09, + 0x02, 0x02, 0x05, 0x02, 0x04, 0x04, 0xE0, 0x00, 0x0A, 0xFF, + 0x40, 0x00, 0x78, 0xFF, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x4D, 0x4F, 0x4E, 0x4F, 0x00, 0x20, 0x00, 0x20, + 0x00, 0x50, 0x05, 0x11, 0xFE, 0x54, 0x00, 0x00, 0x06, 0xA9, + 0x02, 0x67, 0x60, 0x00, 0x01, 0xBF, 0xDF, 0xF7, 0x00, 0x00, + 0x04, 0x3A, 0x05, 0x45, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, + 0x04, 0xCD, 0x00, 0x44, 0x00, 0x00, 0x00, 0x00, 0x04, 0xCD, + 0x00, 0x00, 0x04, 0xCD, 0x00, 0x00, 0x00, 0x75, 0x00, 0x8A, + 0x00, 0x7B, 0x00, 0x5D, 0x00, 0x47, 0x00, 0x68, 0x00, 0x7D, + 0x00, 0x83, 0x00, 0x6D, 0x00, 0x72, 0x01, 0xD6, 0x00, 0x00, + 0x00, 0x63, 0x00, 0x88, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, + 0x00, 0x03, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x5C, 0x00, 0x03, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x1C, 0x00, 0x04, 0x00, 0x40, 0x00, 0x00, 0x00, 0x0C, + 0x00, 0x08, 0x00, 0x02, 0x00, 0x04, 0x00, 0x20, 0x00, 0x3A, + 0x00, 0x41, 0x00, 0x4D, 0x00, 0x50, 0xFF, 0xFF, 0x00, 0x00, + 0x00, 0x20, 0x00, 0x30, 0x00, 0x41, 0x00, 0x4D, 0x00, 0x50, + 0xFF, 0xFF, 0xFF, 0xE3, 0xFF, 0xD4, 0xFF, 0xCE, 0xFF, 0xC3, + 0xFF, 0xC1, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x06, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, + 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, + 0x00, 0x00, 0x11, 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, 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, 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, 0x03, 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, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x40, 0x47, 0x5B, 0x5A, 0x59, 0x58, 0x55, 0x54, 0x53, 0x52, + 0x51, 0x50, 0x4F, 0x4E, 0x4D, 0x4C, 0x4B, 0x4A, 0x49, 0x48, + 0x47, 0x46, 0x45, 0x44, 0x43, 0x42, 0x41, 0x40, 0x3F, 0x3E, + 0x3D, 0x3C, 0x3B, 0x3A, 0x39, 0x38, 0x37, 0x36, 0x35, 0x31, + 0x30, 0x2F, 0x2E, 0x2D, 0x2C, 0x28, 0x27, 0x26, 0x25, 0x24, + 0x23, 0x22, 0x21, 0x1F, 0x18, 0x14, 0x11, 0x10, 0x0F, 0x0E, + 0x0D, 0x0B, 0x0A, 0x09, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, + 0x02, 0x01, 0x00, 0x2C, 0x20, 0xB0, 0x01, 0x60, 0x45, 0xB0, + 0x03, 0x25, 0x20, 0x11, 0x46, 0x61, 0x23, 0x45, 0x23, 0x61, + 0x48, 0x2D, 0x2C, 0x20, 0x45, 0x18, 0x68, 0x44, 0x2D, 0x2C, + 0x45, 0x23, 0x46, 0x60, 0xB0, 0x20, 0x61, 0x20, 0xB0, 0x46, + 0x60, 0xB0, 0x04, 0x26, 0x23, 0x48, 0x48, 0x2D, 0x2C, 0x45, + 0x23, 0x46, 0x23, 0x61, 0xB0, 0x20, 0x60, 0x20, 0xB0, 0x26, + 0x61, 0xB0, 0x20, 0x61, 0xB0, 0x04, 0x26, 0x23, 0x48, 0x48, + 0x2D, 0x2C, 0x45, 0x23, 0x46, 0x60, 0xB0, 0x40, 0x61, 0x20, + 0xB0, 0x66, 0x60, 0xB0, 0x04, 0x26, 0x23, 0x48, 0x48, 0x2D, + 0x2C, 0x45, 0x23, 0x46, 0x23, 0x61, 0xB0, 0x40, 0x60, 0x20, + 0xB0, 0x26, 0x61, 0xB0, 0x40, 0x61, 0xB0, 0x04, 0x26, 0x23, + 0x48, 0x48, 0x2D, 0x2C, 0x01, 0x10, 0x20, 0x3C, 0x00, 0x3C, + 0x2D, 0x2C, 0x20, 0x45, 0x23, 0x20, 0xB0, 0xCD, 0x44, 0x23, + 0x20, 0xB8, 0x01, 0x5A, 0x51, 0x58, 0x23, 0x20, 0xB0, 0x8D, + 0x44, 0x23, 0x59, 0x20, 0xB0, 0xED, 0x51, 0x58, 0x23, 0x20, + 0xB0, 0x4D, 0x44, 0x23, 0x59, 0x20, 0xB0, 0x04, 0x26, 0x51, + 0x58, 0x23, 0x20, 0xB0, 0x0D, 0x44, 0x23, 0x59, 0x21, 0x21, + 0x2D, 0x2C, 0x20, 0x20, 0x45, 0x18, 0x68, 0x44, 0x20, 0xB0, + 0x01, 0x60, 0x20, 0x45, 0xB0, 0x46, 0x76, 0x68, 0x8A, 0x45, + 0x60, 0x44, 0x2D, 0x2C, 0x01, 0xB1, 0x0B, 0x0A, 0x43, 0x23, + 0x43, 0x65, 0x0A, 0x2D, 0x2C, 0x00, 0xB1, 0x0A, 0x0B, 0x43, + 0x23, 0x43, 0x0B, 0x2D, 0x2C, 0x00, 0xB0, 0x28, 0x23, 0x70, + 0xB1, 0x01, 0x28, 0x3E, 0x01, 0xB0, 0x28, 0x23, 0x70, 0xB1, + 0x02, 0x28, 0x45, 0x3A, 0xB1, 0x02, 0x00, 0x08, 0x0D, 0x2D, + 0x2C, 0x20, 0x45, 0xB0, 0x03, 0x25, 0x45, 0x61, 0x64, 0xB0, + 0x50, 0x51, 0x58, 0x45, 0x44, 0x1B, 0x21, 0x21, 0x59, 0x2D, + 0x2C, 0x49, 0xB0, 0x0E, 0x23, 0x44, 0x2D, 0x2C, 0x20, 0x45, + 0xB0, 0x00, 0x43, 0x60, 0x44, 0x2D, 0x2C, 0x01, 0xB0, 0x06, + 0x43, 0xB0, 0x07, 0x43, 0x65, 0x0A, 0x2D, 0x2C, 0x20, 0x69, + 0xB0, 0x40, 0x61, 0xB0, 0x00, 0x8B, 0x20, 0xB1, 0x2C, 0xC0, + 0x8A, 0x8C, 0xB8, 0x10, 0x00, 0x62, 0x60, 0x2B, 0x0C, 0x64, + 0x23, 0x64, 0x61, 0x5C, 0x58, 0xB0, 0x03, 0x61, 0x59, 0x2D, + 0x2C, 0x8A, 0x03, 0x45, 0x8A, 0x8A, 0x87, 0xB0, 0x11, 0x2B, + 0xB0, 0x29, 0x23, 0x44, 0xB0, 0x29, 0x7A, 0xE4, 0x18, 0x2D, + 0x2C, 0x45, 0x65, 0xB0, 0x2C, 0x23, 0x44, 0x45, 0xB0, 0x2B, + 0x23, 0x44, 0x2D, 0x2C, 0x4B, 0x52, 0x58, 0x45, 0x44, 0x1B, + 0x21, 0x21, 0x59, 0x2D, 0x2C, 0x4B, 0x51, 0x58, 0x45, 0x44, + 0x1B, 0x21, 0x21, 0x59, 0x2D, 0x2C, 0x01, 0xB0, 0x05, 0x25, + 0x10, 0x23, 0x20, 0x8A, 0xF5, 0x00, 0xB0, 0x01, 0x60, 0x23, + 0xED, 0xEC, 0x2D, 0x2C, 0x01, 0xB0, 0x05, 0x25, 0x10, 0x23, + 0x20, 0x8A, 0xF5, 0x00, 0xB0, 0x01, 0x61, 0x23, 0xED, 0xEC, + 0x2D, 0x2C, 0x01, 0xB0, 0x06, 0x25, 0x10, 0xF5, 0x00, 0xED, + 0xEC, 0x2D, 0x2C, 0xB0, 0x02, 0x43, 0xB0, 0x01, 0x52, 0x58, + 0x21, 0x21, 0x21, 0x21, 0x21, 0x1B, 0x46, 0x23, 0x46, 0x60, + 0x8A, 0x8A, 0x46, 0x23, 0x20, 0x46, 0x8A, 0x60, 0x8A, 0x61, + 0xB8, 0xFF, 0x80, 0x62, 0x23, 0x20, 0x10, 0x23, 0x8A, 0xB1, + 0x0C, 0x0C, 0x8A, 0x70, 0x45, 0x60, 0x20, 0xB0, 0x00, 0x50, + 0x58, 0xB0, 0x01, 0x61, 0xB8, 0xFF, 0xBA, 0x8B, 0x1B, 0xB0, + 0x46, 0x8C, 0x59, 0xB0, 0x10, 0x60, 0x68, 0x01, 0x3A, 0x59, + 0x2D, 0x2C, 0x20, 0x45, 0xB0, 0x03, 0x25, 0x46, 0x52, 0x4B, + 0xB0, 0x13, 0x51, 0x5B, 0x58, 0xB0, 0x02, 0x25, 0x46, 0x20, + 0x68, 0x61, 0xB0, 0x03, 0x25, 0xB0, 0x03, 0x25, 0x3F, 0x23, + 0x21, 0x38, 0x1B, 0x21, 0x11, 0x59, 0x2D, 0x2C, 0x20, 0x45, + 0xB0, 0x03, 0x25, 0x46, 0x50, 0x58, 0xB0, 0x02, 0x25, 0x46, + 0x20, 0x68, 0x61, 0xB0, 0x03, 0x25, 0xB0, 0x03, 0x25, 0x3F, + 0x23, 0x21, 0x38, 0x1B, 0x21, 0x11, 0x59, 0x2D, 0x2C, 0x00, + 0xB0, 0x07, 0x43, 0xB0, 0x06, 0x43, 0x0B, 0x2D, 0x2C, 0x20, + 0xB0, 0x03, 0x25, 0x45, 0x50, 0x58, 0x8A, 0x20, 0x45, 0x8A, + 0x8B, 0x44, 0x21, 0x1B, 0x21, 0x45, 0x44, 0x59, 0x2D, 0x2C, + 0x21, 0xB0, 0x80, 0x51, 0x58, 0x0C, 0x64, 0x23, 0x64, 0x8B, + 0xB8, 0x20, 0x00, 0x62, 0x1B, 0xB2, 0x00, 0x40, 0x2F, 0x2B, + 0x59, 0xB0, 0x02, 0x60, 0x2D, 0x2C, 0x21, 0xB0, 0xC0, 0x51, + 0x58, 0x0C, 0x64, 0x23, 0x64, 0x8B, 0xB8, 0x15, 0x55, 0x62, + 0x1B, 0xB2, 0x00, 0x80, 0x2F, 0x2B, 0x59, 0xB0, 0x02, 0x60, + 0x2D, 0x2C, 0x0C, 0x64, 0x23, 0x64, 0x8B, 0xB8, 0x40, 0x00, + 0x62, 0x60, 0x23, 0x21, 0x2D, 0x2C, 0x4B, 0x53, 0x58, 0x8A, + 0xB0, 0x04, 0x25, 0x49, 0x64, 0x23, 0x45, 0x69, 0xB0, 0x40, + 0x8B, 0x61, 0xB0, 0x80, 0x62, 0xB0, 0x20, 0x61, 0x6A, 0xB0, + 0x0E, 0x23, 0x44, 0x23, 0x10, 0xB0, 0x0E, 0xF6, 0x1B, 0x21, + 0x23, 0x8A, 0x12, 0x11, 0x20, 0x39, 0x2F, 0x59, 0x2D, 0x2C, + 0x4B, 0x53, 0x58, 0x20, 0xB0, 0x03, 0x25, 0x49, 0x64, 0x69, + 0x20, 0xB0, 0x05, 0x26, 0xB0, 0x06, 0x25, 0x49, 0x64, 0x23, + 0x61, 0xB0, 0x80, 0x62, 0xB0, 0x20, 0x61, 0x6A, 0xB0, 0x0E, + 0x23, 0x44, 0xB0, 0x04, 0x26, 0x10, 0xB0, 0x0E, 0xF6, 0x8A, + 0x10, 0xB0, 0x0E, 0x23, 0x44, 0xB0, 0x0E, 0xF6, 0xB0, 0x0E, + 0x23, 0x44, 0xB0, 0x0E, 0xED, 0x1B, 0x8A, 0xB0, 0x04, 0x26, + 0x11, 0x12, 0x20, 0x39, 0x23, 0x20, 0x39, 0x2F, 0x2F, 0x59, + 0x2D, 0x2C, 0x45, 0x23, 0x45, 0x60, 0x23, 0x45, 0x60, 0x23, + 0x45, 0x60, 0x23, 0x76, 0x68, 0x18, 0xB0, 0x80, 0x62, 0x20, + 0x2D, 0x2C, 0xB0, 0x48, 0x2B, 0x2D, 0x2C, 0x20, 0x45, 0xB0, + 0x00, 0x54, 0x58, 0xB0, 0x40, 0x44, 0x20, 0x45, 0xB0, 0x40, + 0x61, 0x44, 0x1B, 0x21, 0x21, 0x59, 0x2D, 0x2C, 0x45, 0xB1, + 0x30, 0x2F, 0x45, 0x23, 0x45, 0x61, 0x60, 0xB0, 0x01, 0x60, + 0x69, 0x44, 0x2D, 0x2C, 0x4B, 0x51, 0x58, 0xB0, 0x2F, 0x23, + 0x70, 0xB0, 0x14, 0x23, 0x42, 0x1B, 0x21, 0x21, 0x59, 0x2D, + 0x2C, 0x4B, 0x51, 0x58, 0x20, 0xB0, 0x03, 0x25, 0x45, 0x69, + 0x53, 0x58, 0x44, 0x1B, 0x21, 0x21, 0x59, 0x1B, 0x21, 0x21, + 0x59, 0x2D, 0x2C, 0x45, 0xB0, 0x14, 0x43, 0xB0, 0x00, 0x60, + 0x63, 0xB0, 0x01, 0x60, 0x69, 0x44, 0x2D, 0x2C, 0xB0, 0x2F, + 0x45, 0x44, 0x2D, 0x2C, 0x45, 0x23, 0x20, 0x45, 0x8A, 0x60, + 0x44, 0x2D, 0x2C, 0x45, 0x23, 0x45, 0x60, 0x44, 0x2D, 0x2C, + 0x4B, 0x23, 0x51, 0x58, 0xB9, 0x00, 0x33, 0xFF, 0xE0, 0xB1, + 0x34, 0x20, 0x1B, 0xB3, 0x33, 0x00, 0x34, 0x00, 0x59, 0x44, + 0x44, 0x2D, 0x2C, 0xB0, 0x16, 0x43, 0x58, 0xB0, 0x03, 0x26, + 0x45, 0x8A, 0x58, 0x64, 0x66, 0xB0, 0x1F, 0x60, 0x1B, 0x64, + 0xB0, 0x20, 0x60, 0x66, 0x20, 0x58, 0x1B, 0x21, 0xB0, 0x40, + 0x59, 0xB0, 0x01, 0x61, 0x59, 0x23, 0x58, 0x65, 0x59, 0xB0, + 0x29, 0x23, 0x44, 0x23, 0x10, 0xB0, 0x29, 0xE0, 0x1B, 0x21, + 0x21, 0x21, 0x21, 0x21, 0x59, 0x2D, 0x2C, 0xB0, 0x02, 0x43, + 0x54, 0x58, 0x4B, 0x53, 0x23, 0x4B, 0x51, 0x5A, 0x58, 0x38, + 0x1B, 0x21, 0x21, 0x59, 0x1B, 0x21, 0x21, 0x21, 0x21, 0x59, + 0x2D, 0x2C, 0xB0, 0x16, 0x43, 0x58, 0xB0, 0x04, 0x25, 0x45, + 0x64, 0xB0, 0x20, 0x60, 0x66, 0x20, 0x58, 0x1B, 0x21, 0xB0, + 0x40, 0x59, 0xB0, 0x01, 0x61, 0x23, 0x58, 0x1B, 0x65, 0x59, + 0xB0, 0x29, 0x23, 0x44, 0xB0, 0x05, 0x25, 0xB0, 0x08, 0x25, + 0x08, 0x20, 0x58, 0x02, 0x1B, 0x03, 0x59, 0xB0, 0x04, 0x25, + 0x10, 0xB0, 0x05, 0x25, 0x20, 0x46, 0xB0, 0x04, 0x25, 0x23, + 0x42, 0x3C, 0xB0, 0x04, 0x25, 0xB0, 0x07, 0x25, 0x08, 0xB0, + 0x07, 0x25, 0x10, 0xB0, 0x06, 0x25, 0x20, 0x46, 0xB0, 0x04, + 0x25, 0xB0, 0x01, 0x60, 0x23, 0x42, 0x3C, 0x20, 0x58, 0x01, + 0x1B, 0x00, 0x59, 0xB0, 0x04, 0x25, 0x10, 0xB0, 0x05, 0x25, + 0xB0, 0x29, 0xE0, 0xB0, 0x29, 0x20, 0x45, 0x65, 0x44, 0xB0, + 0x07, 0x25, 0x10, 0xB0, 0x06, 0x25, 0xB0, 0x29, 0xE0, 0xB0, + 0x05, 0x25, 0xB0, 0x08, 0x25, 0x08, 0x20, 0x58, 0x02, 0x1B, + 0x03, 0x59, 0xB0, 0x05, 0x25, 0xB0, 0x03, 0x25, 0x43, 0x48, + 0xB0, 0x04, 0x25, 0xB0, 0x07, 0x25, 0x08, 0xB0, 0x06, 0x25, + 0xB0, 0x03, 0x25, 0xB0, 0x01, 0x60, 0x43, 0x48, 0x1B, 0x21, + 0x59, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x2D, 0x2C, + 0x02, 0xB0, 0x04, 0x25, 0x20, 0x20, 0x46, 0xB0, 0x04, 0x25, + 0x23, 0x42, 0xB0, 0x05, 0x25, 0x08, 0xB0, 0x03, 0x25, 0x45, + 0x48, 0x21, 0x21, 0x21, 0x21, 0x2D, 0x2C, 0x02, 0xB0, 0x03, + 0x25, 0x20, 0xB0, 0x04, 0x25, 0x08, 0xB0, 0x02, 0x25, 0x43, + 0x48, 0x21, 0x21, 0x21, 0x2D, 0x2C, 0x45, 0x23, 0x20, 0x45, + 0x18, 0x20, 0xB0, 0x00, 0x50, 0x20, 0x58, 0x23, 0x65, 0x23, + 0x59, 0x23, 0x68, 0x20, 0xB0, 0x40, 0x50, 0x58, 0x21, 0xB0, + 0x40, 0x59, 0x23, 0x58, 0x65, 0x59, 0x8A, 0x60, 0x44, 0x2D, + 0x2C, 0x4B, 0x53, 0x23, 0x4B, 0x51, 0x5A, 0x58, 0x20, 0x45, + 0x8A, 0x60, 0x44, 0x1B, 0x21, 0x21, 0x59, 0x2D, 0x2C, 0x4B, + 0x54, 0x58, 0x20, 0x45, 0x8A, 0x60, 0x44, 0x1B, 0x21, 0x21, + 0x59, 0x2D, 0x2C, 0x4B, 0x53, 0x23, 0x4B, 0x51, 0x5A, 0x58, + 0x38, 0x1B, 0x21, 0x21, 0x59, 0x2D, 0x2C, 0xB0, 0x00, 0x21, + 0x4B, 0x54, 0x58, 0x38, 0x1B, 0x21, 0x21, 0x59, 0x2D, 0x2C, + 0xB0, 0x02, 0x43, 0x54, 0x58, 0xB0, 0x46, 0x2B, 0x1B, 0x21, + 0x21, 0x21, 0x21, 0x59, 0x2D, 0x2C, 0xB0, 0x02, 0x43, 0x54, + 0x58, 0xB0, 0x47, 0x2B, 0x1B, 0x21, 0x21, 0x21, 0x59, 0x2D, + 0x2C, 0x20, 0xB0, 0x02, 0x54, 0x23, 0xB0, 0x00, 0x54, 0x5B, + 0x58, 0xB0, 0x80, 0xB0, 0x02, 0x43, 0x50, 0xB0, 0x01, 0xB0, + 0x02, 0x43, 0x54, 0x5B, 0x58, 0x21, 0x21, 0x21, 0x21, 0x1B, + 0xB0, 0x48, 0x2B, 0x59, 0x1B, 0xB0, 0x80, 0xB0, 0x02, 0x43, + 0x50, 0xB0, 0x01, 0xB0, 0x02, 0x43, 0x54, 0x5B, 0x58, 0xB0, + 0x48, 0x2B, 0x1B, 0x21, 0x21, 0x21, 0x21, 0x59, 0x59, 0x2D, + 0x2C, 0x20, 0xB0, 0x02, 0x54, 0x23, 0xB0, 0x00, 0x54, 0x5B, + 0x58, 0xB0, 0x80, 0xB0, 0x02, 0x43, 0x50, 0xB0, 0x01, 0xB0, + 0x02, 0x43, 0x54, 0x5B, 0x58, 0x21, 0x21, 0x21, 0x1B, 0xB0, + 0x49, 0x2B, 0x59, 0x1B, 0xB0, 0x80, 0xB0, 0x02, 0x43, 0x50, + 0xB0, 0x01, 0xB0, 0x02, 0x43, 0x54, 0x5B, 0x58, 0xB0, 0x49, + 0x2B, 0x1B, 0x21, 0x21, 0x21, 0x59, 0x59, 0x2D, 0x2C, 0x20, + 0x8A, 0x08, 0x23, 0x4B, 0x53, 0x8A, 0x4B, 0x51, 0x5A, 0x58, + 0x23, 0x38, 0x1B, 0x21, 0x21, 0x59, 0x2D, 0x2C, 0x00, 0xB0, + 0x02, 0x25, 0x11, 0xB0, 0x02, 0x25, 0x49, 0x6A, 0x20, 0xB0, + 0x00, 0x53, 0x58, 0xB0, 0x40, 0x60, 0x38, 0x1B, 0x21, 0x21, + 0x59, 0x2D, 0x2C, 0x00, 0xB0, 0x02, 0x25, 0x11, 0xB0, 0x02, + 0x25, 0x49, 0x6A, 0x20, 0xB0, 0x00, 0x51, 0x58, 0xB0, 0x40, + 0x61, 0x38, 0x1B, 0x21, 0x21, 0x59, 0x2D, 0x2C, 0x20, 0x8A, + 0x23, 0x49, 0x64, 0x8A, 0x23, 0x53, 0x58, 0x3C, 0x1B, 0x21, + 0x59, 0x2D, 0x2C, 0x4B, 0x52, 0x58, 0x7D, 0x1B, 0x7A, 0x59, + 0x2D, 0x2C, 0xB0, 0x12, 0x00, 0x4B, 0x01, 0x4B, 0x54, 0x42, + 0x2D, 0x2C, 0xB1, 0x02, 0x01, 0x42, 0xB1, 0x23, 0x01, 0x88, + 0x51, 0xB1, 0x40, 0x01, 0x88, 0x53, 0x5A, 0x58, 0xB1, 0x02, + 0x00, 0x42, 0xB9, 0x10, 0x00, 0x00, 0x20, 0x88, 0x54, 0x58, + 0xB2, 0x02, 0x01, 0x02, 0x43, 0x60, 0x42, 0x59, 0xB1, 0x24, + 0x01, 0x88, 0x51, 0x58, 0xB9, 0x20, 0x00, 0x00, 0x40, 0x88, + 0x54, 0x58, 0xB2, 0x02, 0x02, 0x02, 0x43, 0x60, 0x42, 0xB1, + 0x24, 0x01, 0x88, 0x54, 0x58, 0xB2, 0x02, 0x20, 0x02, 0x43, + 0x60, 0x42, 0x00, 0x4B, 0x01, 0x4B, 0x52, 0x58, 0xB2, 0x02, + 0x08, 0x02, 0x43, 0x60, 0x42, 0x59, 0x1B, 0xB9, 0x40, 0x00, + 0x00, 0x80, 0x88, 0x54, 0x58, 0xB2, 0x02, 0x04, 0x02, 0x43, + 0x60, 0x42, 0x59, 0xB9, 0x40, 0x00, 0x00, 0x80, 0x63, 0xB8, + 0x01, 0x00, 0x88, 0x54, 0x58, 0xB2, 0x02, 0x08, 0x02, 0x43, + 0x60, 0x42, 0x59, 0xB9, 0x40, 0x00, 0x01, 0x00, 0x63, 0xB8, + 0x02, 0x00, 0x88, 0x54, 0x58, 0xB2, 0x02, 0x10, 0x02, 0x43, + 0x60, 0x42, 0x59, 0xB1, 0x26, 0x01, 0x88, 0x51, 0x58, 0xB9, + 0x40, 0x00, 0x02, 0x00, 0x63, 0xB8, 0x04, 0x00, 0x88, 0x54, + 0x58, 0xB2, 0x02, 0x40, 0x02, 0x43, 0x60, 0x42, 0x59, 0xB9, + 0x40, 0x00, 0x04, 0x00, 0x63, 0xB8, 0x08, 0x00, 0x88, 0x54, + 0x58, 0xB2, 0x02, 0x80, 0x02, 0x43, 0x60, 0x42, 0x59, 0x59, + 0x59, 0x59, 0x59, 0x59, 0xB1, 0x00, 0x02, 0x43, 0x54, 0x58, + 0xB1, 0x02, 0x01, 0x42, 0x59, 0x2D, 0x2C, 0x45, 0x18, 0x68, + 0x23, 0x4B, 0x51, 0x58, 0x23, 0x20, 0x45, 0x20, 0x64, 0xB0, + 0x40, 0x50, 0x58, 0x7C, 0x59, 0x68, 0x8A, 0x60, 0x59, 0x44, + 0x2D, 0x2C, 0xB0, 0x00, 0x16, 0xB0, 0x02, 0x25, 0xB0, 0x02, + 0x25, 0x01, 0xB0, 0x01, 0x23, 0x3E, 0x00, 0xB0, 0x02, 0x23, + 0x3E, 0xB1, 0x01, 0x02, 0x06, 0x0C, 0xB0, 0x0A, 0x23, 0x65, + 0x42, 0xB0, 0x0B, 0x23, 0x42, 0x01, 0xB0, 0x01, 0x23, 0x3F, + 0x00, 0xB0, 0x02, 0x23, 0x3F, 0xB1, 0x01, 0x02, 0x06, 0x0C, + 0xB0, 0x06, 0x23, 0x65, 0x42, 0xB0, 0x07, 0x23, 0x42, 0xB0, + 0x01, 0x16, 0x01, 0x2D, 0x2C, 0xB0, 0x80, 0xB0, 0x02, 0x43, + 0x50, 0xB0, 0x01, 0xB0, 0x02, 0x43, 0x54, 0x5B, 0x58, 0x21, + 0x23, 0x10, 0xB0, 0x20, 0x1A, 0xC9, 0x1B, 0x8A, 0x10, 0xED, + 0x59, 0x2D, 0x2C, 0xB0, 0x59, 0x2B, 0x2D, 0x2C, 0x8A, 0x10, + 0xE5, 0x2D, 0x41, 0x2B, 0x01, 0x53, 0x00, 0x01, 0x01, 0x4D, + 0x00, 0x55, 0x01, 0x52, 0x00, 0x01, 0x01, 0x4D, 0x00, 0x55, + 0x01, 0x56, 0x01, 0x54, 0x00, 0x14, 0x00, 0x1F, 0x01, 0x55, + 0x01, 0x54, 0x00, 0x1F, 0x00, 0x1F, 0x01, 0x4F, 0x00, 0x33, + 0x01, 0x4E, 0x00, 0x55, 0x01, 0x4C, 0x00, 0x33, 0x01, 0x4D, + 0x00, 0x55, 0x00, 0xA4, 0x01, 0x4D, 0x00, 0xF4, 0x01, 0x4D, + 0x00, 0x02, 0x01, 0x3D, 0x00, 0x3D, 0x01, 0x3C, 0x00, 0x55, + 0x01, 0x3C, 0x00, 0x01, 0x01, 0x3A, 0x00, 0x55, 0x01, 0x3B, + 0x00, 0x3D, 0x01, 0x3A, 0x00, 0x55, 0x01, 0x35, 0x01, 0x34, + 0xB2, 0x80, 0x1F, 0x00, 0x41, 0x2D, 0x01, 0x34, 0x00, 0x10, + 0x01, 0x34, 0x00, 0x02, 0x01, 0x34, 0x00, 0x02, 0x01, 0x2E, + 0x00, 0x55, 0x01, 0x33, 0x00, 0x48, 0x01, 0x32, 0x00, 0x55, + 0x00, 0x80, 0x01, 0x32, 0x00, 0x01, 0x01, 0x32, 0x00, 0x02, + 0x01, 0x2E, 0x00, 0x55, 0x01, 0x31, 0x00, 0x3D, 0x01, 0x30, + 0x00, 0x55, 0x00, 0x0F, 0x01, 0x30, 0x00, 0x01, 0x01, 0x30, + 0x00, 0x02, 0x01, 0x2E, 0x00, 0x55, 0x01, 0x2F, 0x00, 0x3D, + 0x01, 0x2E, 0x00, 0x55, 0x00, 0x20, 0x01, 0x2E, 0x00, 0x60, + 0x01, 0x2E, 0x00, 0x02, 0x00, 0x00, 0x01, 0x2E, 0x00, 0x20, + 0x01, 0x2E, 0x00, 0x02, 0x01, 0x2E, 0xB2, 0x01, 0x00, 0x55, + 0xB8, 0x01, 0x2D, 0xB2, 0x3D, 0x00, 0x55, 0xB8, 0x01, 0x2C, + 0xB3, 0x00, 0x80, 0x1F, 0x80, 0x41, 0x19, 0x01, 0x44, 0x00, + 0x01, 0x01, 0x44, 0x00, 0x01, 0x01, 0x3E, 0x00, 0x55, 0x01, + 0x43, 0x01, 0x42, 0x00, 0xFF, 0x00, 0x1F, 0x01, 0x42, 0x00, + 0x01, 0x01, 0x3E, 0x00, 0x55, 0x01, 0x41, 0x00, 0x02, 0x01, + 0x40, 0x00, 0x55, 0x01, 0x40, 0x00, 0x01, 0x01, 0x3E, 0x00, + 0x55, 0x01, 0x3F, 0x00, 0x3D, 0x01, 0x3E, 0x40, 0x1B, 0x55, + 0x10, 0xFD, 0x01, 0xF1, 0x46, 0x3C, 0x1F, 0xF0, 0x46, 0xFF, + 0x1F, 0x3F, 0xEF, 0x6F, 0xEF, 0x02, 0x4F, 0xEC, 0x5F, 0xEC, + 0x02, 0x2F, 0xEB, 0x3F, 0xEB, 0x02, 0xBA, 0x01, 0x2B, 0x00, + 0x3D, 0x01, 0x2A, 0xB6, 0x55, 0xEA, 0x3D, 0xE9, 0x55, 0xE8, + 0x01, 0x41, 0x11, 0x01, 0x29, 0x00, 0x55, 0x00, 0x00, 0x01, + 0x29, 0x00, 0x01, 0x00, 0xAF, 0x01, 0x29, 0x00, 0x01, 0x00, + 0x20, 0x01, 0x29, 0x00, 0x50, 0x01, 0x29, 0x00, 0x60, 0x01, + 0x29, 0x00, 0x03, 0x00, 0x0F, 0x01, 0x29, 0x40, 0x16, 0x01, + 0xB0, 0xE7, 0x01, 0x7F, 0xE6, 0x8F, 0xE6, 0x9F, 0xE6, 0x03, + 0xCF, 0xE5, 0x01, 0x40, 0xE5, 0x20, 0x24, 0x46, 0xEF, 0xE4, + 0x01, 0xB8, 0x01, 0x28, 0x40, 0x18, 0xE3, 0x10, 0x1F, 0x60, + 0xE3, 0x01, 0xE2, 0xE0, 0x1A, 0x1F, 0xE1, 0xE0, 0x28, 0x1F, + 0xDF, 0x3D, 0xDE, 0x55, 0xDD, 0x3D, 0x03, 0x55, 0xE9, 0x01, + 0x41, 0x0D, 0x01, 0x2A, 0x00, 0x55, 0x00, 0x20, 0x01, 0x2A, + 0x00, 0x50, 0x01, 0x2A, 0x00, 0x80, 0x01, 0x2A, 0x00, 0xB0, + 0x01, 0x2A, 0x00, 0x04, 0x00, 0x0F, 0x01, 0x2A, 0x40, 0x1F, + 0x01, 0xAF, 0xDE, 0x01, 0xDC, 0x03, 0xFF, 0x1F, 0x9F, 0xD3, + 0xAF, 0xD3, 0x02, 0xC9, 0xC8, 0x3C, 0x1F, 0x0F, 0xC8, 0x1F, + 0xC8, 0x02, 0xC4, 0xC3, 0x1A, 0x1F, 0x40, 0xC3, 0x13, 0x18, + 0x46, 0xB8, 0x01, 0x21, 0xB2, 0xC2, 0x3C, 0x1F, 0xB8, 0x01, + 0x20, 0x40, 0x09, 0xC0, 0x3C, 0x1F, 0x0F, 0xBF, 0x01, 0x60, + 0xBF, 0x01, 0xB8, 0x01, 0x06, 0x40, 0x1D, 0xBE, 0x28, 0x1F, + 0x2F, 0xBE, 0x01, 0xBD, 0xBC, 0x1A, 0x1F, 0x0F, 0xBC, 0x1F, + 0xBC, 0x7F, 0xBC, 0x8F, 0xBC, 0x04, 0xBA, 0xB9, 0x1A, 0x1F, + 0x4F, 0xB7, 0x7F, 0xB7, 0x02, 0x0F, 0xBE, 0x01, 0x05, 0x00, + 0x1F, 0x01, 0x05, 0x00, 0x9F, 0x01, 0x05, 0x00, 0xAF, 0x01, + 0x05, 0x40, 0x0B, 0x04, 0xD0, 0xB3, 0x01, 0x0F, 0xB3, 0x01, + 0xB2, 0x03, 0x32, 0x1F, 0xB8, 0x01, 0x1B, 0xB3, 0xAA, 0x48, + 0x1F, 0x1F, 0x41, 0x17, 0x01, 0x1C, 0x00, 0x2F, 0x01, 0x1C, + 0x00, 0x02, 0x00, 0x4F, 0x01, 0x1C, 0x00, 0x5F, 0x01, 0x1C, + 0x00, 0x6F, 0x01, 0x1C, 0x00, 0xAF, 0x01, 0x1C, 0x00, 0xBF, + 0x01, 0x1C, 0x00, 0xCF, 0x01, 0x1C, 0x00, 0xEF, 0x01, 0x1C, + 0x00, 0xFF, 0x01, 0x1C, 0x00, 0x08, 0x00, 0x40, 0x01, 0x1C, + 0x40, 0x11, 0x1D, 0x27, 0x46, 0xB0, 0xAD, 0x3C, 0x1F, 0x50, + 0xAD, 0x60, 0xAD, 0xE0, 0xAD, 0xF0, 0xAD, 0x04, 0x40, 0xB8, + 0x01, 0x1E, 0xB2, 0x10, 0x13, 0x46, 0xB9, 0xFF, 0xC0, 0x01, + 0x1D, 0x40, 0x15, 0x0C, 0x0F, 0x46, 0xAB, 0xAA, 0x3C, 0x1F, + 0x50, 0xAA, 0x60, 0xAA, 0xE0, 0xAA, 0xF0, 0xAA, 0x04, 0xA9, + 0x32, 0xA8, 0x55, 0x2F, 0xBE, 0x01, 0x27, 0x00, 0x8F, 0x01, + 0x27, 0x00, 0x9F, 0x01, 0x27, 0x00, 0xAF, 0x01, 0x27, 0x40, + 0x22, 0x04, 0x00, 0xA8, 0x10, 0xA8, 0x20, 0xA8, 0x03, 0xA0, + 0x9C, 0x28, 0x1F, 0x50, 0x9F, 0x60, 0x9F, 0x02, 0x9E, 0x9B, + 0x18, 0x1F, 0x20, 0x9C, 0x50, 0x9C, 0x02, 0x80, 0x9B, 0x90, + 0x9B, 0xF0, 0x9B, 0x03, 0x30, 0x41, 0x0E, 0x01, 0x24, 0x00, + 0x40, 0x01, 0x24, 0x00, 0x50, 0x01, 0x24, 0x00, 0x03, 0x00, + 0x5F, 0x01, 0x23, 0x00, 0x01, 0x00, 0x7F, 0x01, 0x23, 0x00, + 0x01, 0x00, 0xAF, 0x01, 0x23, 0x40, 0x1C, 0x01, 0x10, 0x9A, + 0x80, 0x9A, 0x90, 0x9A, 0x03, 0x90, 0x99, 0x01, 0x98, 0x97, + 0x28, 0x1F, 0x60, 0x97, 0x01, 0x6F, 0x96, 0xCF, 0x96, 0xDF, + 0x96, 0x03, 0x90, 0x96, 0x01, 0xBD, 0x01, 0x47, 0x01, 0x45, + 0x00, 0x3C, 0x00, 0x1F, 0x01, 0x46, 0x01, 0x45, 0xB2, 0x64, + 0x1F, 0x80, 0x41, 0x1B, 0x01, 0x10, 0x00, 0x01, 0x00, 0x4F, + 0x01, 0x10, 0x00, 0x7F, 0x01, 0x10, 0x00, 0x02, 0x00, 0x30, + 0x01, 0x10, 0x00, 0x01, 0x00, 0xBF, 0x01, 0x10, 0x00, 0x01, + 0x00, 0x70, 0x01, 0x10, 0x00, 0x90, 0x01, 0x10, 0x00, 0x02, + 0x00, 0xD0, 0x01, 0x10, 0x00, 0x01, 0x00, 0x0F, 0x01, 0x10, + 0x00, 0x7F, 0x01, 0x10, 0x00, 0xAF, 0x01, 0x10, 0x40, 0x16, + 0x03, 0x0F, 0x95, 0x3F, 0x95, 0x02, 0x0F, 0x94, 0x01, 0x0F, + 0x94, 0x2F, 0x94, 0x3F, 0x94, 0x7F, 0x94, 0x9F, 0x94, 0xEF, + 0x94, 0x06, 0xB8, 0x01, 0x12, 0x40, 0x1D, 0x8C, 0x27, 0x1F, + 0x93, 0x92, 0x27, 0x1F, 0x90, 0x8C, 0x27, 0x1F, 0xEF, 0x8D, + 0x01, 0x0F, 0x8C, 0x1F, 0x8C, 0x02, 0x0F, 0x8C, 0x1F, 0x8C, + 0x2F, 0x8C, 0x9F, 0x8C, 0x04, 0x0F, 0xBC, 0x01, 0x11, 0x00, + 0x1F, 0x01, 0x11, 0x00, 0x02, 0xFF, 0xC0, 0x40, 0x0A, 0x8A, + 0x0F, 0x12, 0x46, 0x4F, 0x87, 0x01, 0x5F, 0x82, 0x01, 0xB8, + 0x01, 0x1A, 0x40, 0x13, 0x77, 0x2C, 0x1F, 0x76, 0x73, 0x28, + 0x1F, 0x75, 0x73, 0x19, 0x1F, 0x74, 0x73, 0x19, 0x1F, 0x73, + 0x50, 0x12, 0x1F, 0xBD, 0x01, 0x19, 0x01, 0x16, 0x00, 0x2C, + 0x00, 0x1F, 0x01, 0x18, 0x01, 0x16, 0xB2, 0x2C, 0x1F, 0x7F, + 0xBF, 0x01, 0x16, 0x00, 0x8F, 0x01, 0x16, 0x00, 0x9F, 0x01, + 0x16, 0x00, 0x03, 0x00, 0x40, 0x01, 0x15, 0xB2, 0x19, 0x1C, + 0x46, 0xB8, 0x01, 0x14, 0x40, 0x30, 0x46, 0x2C, 0x1F, 0x70, + 0x46, 0x32, 0x1F, 0x6F, 0x46, 0x32, 0x1F, 0x6E, 0x46, 0xFF, + 0x1F, 0x1A, 0x01, 0x18, 0x55, 0x19, 0x33, 0x18, 0x55, 0x07, + 0x33, 0x03, 0x55, 0x06, 0x03, 0xFF, 0x1F, 0x62, 0x5F, 0x24, + 0x1F, 0x61, 0x5F, 0x24, 0x1F, 0x60, 0x5F, 0x24, 0x1F, 0x5F, + 0x50, 0x19, 0x1F, 0x70, 0xBA, 0x01, 0x0A, 0x00, 0x80, 0x01, + 0x0A, 0xB5, 0x02, 0x5E, 0x46, 0x24, 0x1F, 0x4F, 0xB8, 0x01, + 0x13, 0x40, 0x19, 0x01, 0x5C, 0x46, 0x19, 0x1F, 0x5B, 0x5A, + 0x32, 0x1F, 0x5A, 0x46, 0x1A, 0x1F, 0x13, 0x32, 0x12, 0x55, + 0x05, 0x01, 0x03, 0x55, 0x04, 0x32, 0x03, 0x55, 0xB8, 0x01, + 0x08, 0x40, 0x21, 0x1B, 0x20, 0x1F, 0x0F, 0x03, 0x01, 0x53, + 0x50, 0x12, 0x1F, 0x52, 0x50, 0x12, 0x1F, 0x51, 0x50, 0x1E, + 0x1F, 0x4E, 0x46, 0x1E, 0x1F, 0x4C, 0x46, 0x2E, 0x1F, 0x7F, + 0x4B, 0x01, 0x4B, 0x46, 0x19, 0x1F, 0xB8, 0x01, 0x0E, 0xB3, + 0x46, 0x28, 0x1F, 0x40, 0xB8, 0x01, 0x0C, 0x40, 0x6F, 0x0B, + 0x0E, 0x46, 0x4A, 0x46, 0x1C, 0x1F, 0xEF, 0x49, 0xFF, 0x49, + 0x02, 0x50, 0x49, 0x01, 0x49, 0x46, 0x0B, 0x1F, 0x3F, 0x48, + 0x4F, 0x48, 0x02, 0x48, 0x46, 0x16, 0x1F, 0x47, 0x46, 0x13, + 0x1F, 0x0F, 0x46, 0x7F, 0x46, 0xEF, 0x46, 0xFF, 0x46, 0x04, + 0x1C, 0x64, 0x1B, 0x55, 0x17, 0x01, 0x15, 0x55, 0x16, 0x32, + 0x15, 0x55, 0x11, 0x01, 0x0F, 0x55, 0x10, 0x32, 0x0F, 0x55, + 0x02, 0x01, 0x00, 0x55, 0x01, 0x32, 0x00, 0x55, 0x1F, 0x0F, + 0x3F, 0x0F, 0x5F, 0x0F, 0x7F, 0x0F, 0x04, 0x0F, 0x0F, 0x2F, + 0x0F, 0x4F, 0x0F, 0x6F, 0x0F, 0x8F, 0x0F, 0xDF, 0x0F, 0xFF, + 0x0F, 0x07, 0x3F, 0x0F, 0x7F, 0x0F, 0xEF, 0x0F, 0x03, 0x6F, + 0x00, 0x01, 0x4F, 0x00, 0x01, 0x80, 0x16, 0x01, 0x05, 0x01, + 0xB8, 0x01, 0x90, 0xB1, 0x54, 0x53, 0x2B, 0x2B, 0x4B, 0xB8, + 0x07, 0xFF, 0x52, 0x4B, 0xB0, 0x09, 0x50, 0x5B, 0xB0, 0x01, + 0x88, 0xB0, 0x25, 0x53, 0xB0, 0x01, 0x88, 0xB0, 0x40, 0x51, + 0x5A, 0xB0, 0x06, 0x88, 0xB0, 0x00, 0x55, 0x5A, 0x5B, 0x58, + 0xB1, 0x01, 0x01, 0x8E, 0x59, 0x85, 0x8D, 0x8D, 0x00, 0x42, + 0x1D, 0x4B, 0xB0, 0x32, 0x53, 0x58, 0xB0, 0x60, 0x1D, 0x59, + 0x4B, 0xB0, 0x64, 0x53, 0x58, 0xB0, 0x40, 0x1D, 0x59, 0x4B, + 0xB0, 0x80, 0x53, 0x58, 0xB0, 0x10, 0x1D, 0xB1, 0x16, 0x00, + 0x42, 0x59, 0x73, 0x74, 0x73, 0x74, 0x75, 0x2B, 0x2B, 0x2B, + 0x2B, 0x2B, 0x2B, 0x2B, 0x01, 0x74, 0x2B, 0x2B, 0x74, 0x2B, + 0x73, 0x74, 0x2B, 0x2B, 0x2B, 0x00, 0x2B, 0x74, 0x2B, 0x2B, + 0x2B, 0x2B, 0x2B, 0x73, 0x2B, 0x2B, 0x2B, 0x2B, 0x01, 0x2B, + 0x2B, 0x2B, 0x73, 0x00, 0x2B, 0x73, 0x2B, 0x2B, 0x2B, 0x2B, + 0x2B, 0x2B, 0x2B, 0x2B, 0x01, 0x2B, 0x2B, 0x2B, 0x2B, 0x2B, + 0x73, 0x2B, 0x2B, 0x00, 0x2B, 0x2B, 0x2B, 0x2B, 0x2B, 0x01, + 0x73, 0x73, 0x2B, 0x73, 0x00, 0x73, 0x74, 0x73, 0x2B, 0x2B, + 0x2B, 0x73, 0x74, 0x73, 0x73, 0x73, 0x74, 0x74, 0x75, 0x75, + 0x75, 0x2B, 0x2B, 0x01, 0x73, 0x74, 0x73, 0x2B, 0x73, 0x73, + 0x73, 0x74, 0x75, 0x73, 0x00, 0x73, 0x73, 0x2B, 0x73, 0x2B, + 0x73, 0x73, 0x2B, 0x01, 0x73, 0x2B, 0x2B, 0x2B, 0x00, 0x73, + 0x2B, 0x2B, 0x73, 0x74, 0x2B, 0x2B, 0x73, 0x73, 0x01, 0x73, + 0x00, 0x73, 0x2B, 0x73, 0x2B, 0x01, 0x73, 0x2B, 0x00, 0x73, + 0x74, 0x01, 0x2B, 0x00, 0x2B, 0x01, 0x2B, 0x2B, 0x00, 0x74, + 0x2B, 0x01, 0x73, 0x00, 0x2B, 0x73, 0x73, 0x73, 0x2B, 0x2B, + 0x2B, 0x01, 0x2B, 0x2B, 0x73, 0x2B, 0x00, 0x73, 0x2B, 0x73, + 0x74, 0x73, 0x73, 0x73, 0x73, 0x74, 0x2B, 0x2B, 0x2B, 0x01, + 0x73, 0x73, 0x00, 0x73, 0x01, 0x2B, 0x2B, 0x00, 0x73, 0x2B, + 0x2B, 0x2B, 0x2B, 0x2B, 0x2B, 0x73, 0x2B, 0x2B, 0x2B, 0x73, + 0x74, 0x2B, 0x2B, 0x73, 0x2B, 0x2B, 0x73, 0x2B, 0x2B, 0x73, + 0x2B, 0x2B, 0x2B, 0x2B, 0x73, 0x2B, 0x2B, 0x2B, 0x2B, 0x2B, + 0x2B, 0x18, 0x00, 0x00, 0x05, 0xCC, 0x00, 0x1E, 0x00, 0x7D, + 0x05, 0x45, 0x00, 0x15, 0x00, 0x4E, 0x05, 0x45, 0x00, 0x15, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x04, 0x3A, 0x00, 0x14, 0x00, 0x77, + 0x00, 0x00, 0xFF, 0xEC, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xEC, + 0xFF, 0x62, 0x00, 0x00, 0xFF, 0xEC, 0x00, 0x00, 0xFE, 0x57, + 0xFF, 0xF7, 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, 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, 0x08, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x19, 0x01, 0x26, 0x00, 0xF1, + 0x00, 0xDE, 0x01, 0x02, 0x00, 0xDC, 0x01, 0x04, 0x00, 0xAC, + 0x00, 0xF0, 0x00, 0xF8, 0x00, 0xC1, 0x00, 0xAD, 0x00, 0xD3, + 0x00, 0xCF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x27, 0x01, 0x1F, 0x01, 0x01, + 0x00, 0xF0, 0x01, 0x04, 0x00, 0xE4, 0x00, 0xF4, 0x00, 0xDC, + 0x00, 0xC8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x19, 0x01, 0x0C, 0x01, 0x25, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xD1, 0x00, 0xDE, 0x00, 0xE7, + 0x00, 0xC6, 0x00, 0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0xFE, 0x00, 0xA4, 0x01, 0x02, + 0x00, 0xE1, 0x00, 0x88, 0x01, 0x84, 0x00, 0xB6, 0x01, 0x94, + 0x01, 0x5A, 0x00, 0x73, 0x01, 0x14, 0x00, 0xDB, 0x00, 0x90, + 0x00, 0xAA, 0x01, 0x1B, 0x00, 0x9E, 0x00, 0x80, 0x00, 0x75, + 0x04, 0xA0, 0x04, 0xC3, 0x01, 0x21, 0x01, 0xE4, 0x01, 0xEE, + 0x00, 0xAD, 0x01, 0x3B, 0x01, 0x31, 0x02, 0x96, 0x02, 0x6E, + 0x01, 0x0E, 0x00, 0xE2, 0x02, 0x81, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFE, 0xF2, 0xFF, 0xEE, 0x00, 0xE1, 0x00, 0xEE, 0x01, 0x90, + 0x00, 0xE1, 0x00, 0xE9, 0x01, 0x98, 0x00, 0xCF, 0x00, 0x00, + 0x05, 0x81, 0x03, 0x19, 0x00, 0xB0, 0x02, 0x3E, 0x00, 0x87, + 0x02, 0xA5, 0x02, 0x55, 0x00, 0xCB, 0x00, 0xF4, 0x00, 0x60, + 0x02, 0x8D, 0x02, 0x6D, 0x00, 0xFF, 0x00, 0xCC, 0x01, 0x3A, + 0x00, 0x00, 0x01, 0x5B, 0x00, 0x7F, 0x00, 0x97, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x73, 0x00, 0x7D, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x26, 0x00, 0x85, 0x00, 0x9B, 0x00, 0x65, + 0x00, 0x7D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x45, 0x00, 0x0C, 0x02, 0x4D, + 0xFF, 0xF3, 0x00, 0xB6, 0x00, 0xC0, 0x00, 0xCA, 0x00, 0xDA, + 0x00, 0x82, 0x00, 0x8C, 0x00, 0x96, 0x00, 0xA0, 0xFC, 0xE2, + 0xFC, 0xB1, 0xFF, 0xF4, 0x00, 0xEE, 0x01, 0xEF, 0x01, 0x90, + 0x00, 0x00, 0x02, 0x19, 0x01, 0x15, 0x01, 0x0A, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xBE, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x06, 0xA9, 0x03, 0x70, 0x02, 0xBC, + 0x02, 0x08, 0xFD, 0x99, 0x00, 0x91, 0x00, 0x91, 0x00, 0x4D, + 0x00, 0x4D, 0x00, 0x64, 0x00, 0x64, 0x01, 0xD0, 0x00, 0xD7, + 0x01, 0x20, 0xFE, 0x6D, 0x00, 0xF4, 0x00, 0xC0, 0x00, 0x95, + 0x00, 0xD0, 0x00, 0x79, 0x01, 0x0D, 0x01, 0x63, 0x04, 0xB1, + 0x01, 0x28, 0x01, 0x23, 0x00, 0xDF, 0x01, 0x02, 0x00, 0x6D, + 0x00, 0xA2, 0x00, 0x6C, 0x00, 0x92, 0x00, 0xB5, 0x00, 0xAA, + 0x00, 0xD8, 0x01, 0x54, 0x01, 0x06, 0x00, 0x9A, 0x00, 0x94, + 0x01, 0x44, 0x01, 0x45, 0xFF, 0x2C, 0x00, 0x9B, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x03, 0x00, 0xE6, 0x05, 0xA1, + 0x05, 0x82, 0x00, 0x0C, 0x05, 0xCB, 0x00, 0x10, 0xFC, 0x1E, + 0xFF, 0xF3, 0x02, 0xFB, 0x00, 0x0F, 0x04, 0x16, 0x00, 0x07, + 0xFE, 0xD3, 0x00, 0xBF, 0xFE, 0x68, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x03, 0x63, 0x00, 0x0B, 0xFD, 0x0F, 0xFF, 0xF5, + 0xFF, 0x78, 0xFF, 0xF0, 0xFE, 0xF6, 0xFF, 0xE2, 0x03, 0x03, + 0x02, 0x7B, 0x04, 0x19, 0x05, 0xA5, 0x05, 0xB2, 0x05, 0xC3, + 0x00, 0x00, 0x00, 0x00, 0x06, 0x81, 0x04, 0x77, 0x00, 0x15, + 0x04, 0xD9, 0x00, 0x00, 0xFF, 0xEC, 0xFF, 0xC5, 0xFE, 0x7F, + 0x00, 0x83, 0x00, 0xDB, 0x00, 0xF2, 0x01, 0x02, 0x00, 0xD5, + 0x00, 0x44, 0x05, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2C, + 0x00, 0x2C, 0x00, 0x2C, 0x00, 0x2C, 0x00, 0x6C, 0x00, 0x8A, + 0x00, 0xC2, 0x01, 0x0C, 0x01, 0x34, 0x01, 0x6C, 0x01, 0xB0, + 0x01, 0xCE, 0x02, 0x20, 0x02, 0x64, 0x02, 0x7A, 0x02, 0xA0, + 0x02, 0xD0, 0x02, 0xFA, 0x00, 0x00, 0x00, 0x02, 0x00, 0x44, + 0x00, 0x00, 0x02, 0x64, 0x05, 0x55, 0x00, 0x03, 0x00, 0x07, + 0x00, 0x2E, 0xB1, 0x01, 0x00, 0x2F, 0x3C, 0xB2, 0x07, 0x04, + 0x57, 0xED, 0x32, 0xB1, 0x06, 0x05, 0xDC, 0x3C, 0xB2, 0x03, + 0x02, 0x57, 0xED, 0x32, 0x00, 0xB1, 0x03, 0x00, 0x2F, 0x3C, + 0xB2, 0x05, 0x04, 0x57, 0xED, 0x32, 0xB2, 0x07, 0x06, 0x58, + 0xFC, 0x3C, 0xB2, 0x01, 0x02, 0x57, 0xED, 0x32, 0x33, 0x11, + 0x21, 0x11, 0x25, 0x21, 0x11, 0x21, 0x44, 0x02, 0x20, 0xFE, + 0x24, 0x01, 0x98, 0xFE, 0x68, 0x05, 0x55, 0xFA, 0xAB, 0x44, + 0x04, 0xCD, 0x00, 0x00, 0x00, 0x03, 0x00, 0x75, 0xFF, 0xEC, + 0x04, 0x57, 0x05, 0x5A, 0x00, 0x0E, 0x00, 0x22, 0x00, 0x26, + 0x00, 0x00, 0x01, 0x10, 0x07, 0x06, 0x23, 0x22, 0x02, 0x11, + 0x10, 0x37, 0x36, 0x21, 0x32, 0x17, 0x16, 0x01, 0x34, 0x2E, + 0x01, 0x23, 0x22, 0x07, 0x06, 0x07, 0x06, 0x15, 0x14, 0x17, + 0x16, 0x17, 0x16, 0x33, 0x32, 0x37, 0x36, 0x25, 0x35, 0x33, + 0x15, 0x04, 0x57, 0x80, 0x7F, 0xF5, 0xF4, 0xFA, 0x79, 0x7A, + 0x01, 0x03, 0xF9, 0x7A, 0x79, 0xFE, 0xE6, 0x2A, 0x58, 0x52, + 0x55, 0x2F, 0x2E, 0x15, 0x14, 0x16, 0x15, 0x2D, 0x2D, 0x52, + 0x75, 0x31, 0x32, 0xFE, 0xBD, 0xD7, 0x02, 0xA3, 0xFE, 0xAE, + 0xB3, 0xB2, 0x01, 0x64, 0x01, 0x53, 0x01, 0x61, 0xAA, 0xAC, + 0xAE, 0xAD, 0xFE, 0xA4, 0xB1, 0xCC, 0x5E, 0x2F, 0x2E, 0x67, + 0x61, 0xB6, 0xA9, 0x6C, 0x68, 0x2E, 0x2F, 0x6D, 0x6F, 0x86, + 0xF9, 0xF9, 0x00, 0x01, 0x00, 0x8A, 0x00, 0x00, 0x04, 0x76, + 0x05, 0x45, 0x00, 0x10, 0x00, 0x00, 0x33, 0x35, 0x21, 0x11, + 0x06, 0x07, 0x06, 0x23, 0x35, 0x32, 0x37, 0x36, 0x37, 0x21, + 0x11, 0x21, 0x15, 0x95, 0x01, 0x85, 0x27, 0x75, 0x76, 0x7E, + 0x8C, 0x6D, 0x6F, 0x37, 0x01, 0x0A, 0x01, 0x43, 0xD1, 0x03, + 0x7B, 0x52, 0x38, 0x38, 0xD6, 0x3D, 0x3E, 0x6A, 0xFB, 0x8C, + 0xD1, 0x00, 0x00, 0x01, 0x00, 0x7B, 0x00, 0x00, 0x04, 0x55, + 0x05, 0x5A, 0x00, 0x23, 0x00, 0x00, 0x33, 0x35, 0x36, 0x37, + 0x36, 0x37, 0x3E, 0x01, 0x37, 0x36, 0x35, 0x34, 0x26, 0x23, + 0x22, 0x06, 0x07, 0x25, 0x36, 0x37, 0x36, 0x33, 0x32, 0x17, + 0x16, 0x15, 0x14, 0x07, 0x06, 0x07, 0x06, 0x07, 0x06, 0x07, + 0x21, 0x15, 0x7B, 0x31, 0x5E, 0x5D, 0xB0, 0x8C, 0x4E, 0x19, + 0x18, 0x5E, 0x5B, 0x59, 0x60, 0x0E, 0xFE, 0xE5, 0x18, 0x7B, + 0x7A, 0xD3, 0xD8, 0x81, 0x7F, 0x45, 0x47, 0x86, 0xC1, 0x44, + 0x42, 0x1F, 0x02, 0x8E, 0xC3, 0x6D, 0x70, 0x6E, 0x90, 0x72, + 0x54, 0x2A, 0x28, 0x2A, 0x48, 0x52, 0x60, 0x62, 0x10, 0xC3, + 0x68, 0x67, 0x64, 0x63, 0xA7, 0x6C, 0x68, 0x6A, 0x69, 0x96, + 0x42, 0x41, 0x45, 0xE7, 0x00, 0x01, 0x00, 0x5D, 0xFF, 0xE9, + 0x04, 0x65, 0x05, 0x5A, 0x00, 0x30, 0x00, 0x00, 0x01, 0x14, + 0x04, 0x23, 0x22, 0x27, 0x26, 0x27, 0x25, 0x16, 0x33, 0x32, + 0x37, 0x36, 0x34, 0x27, 0x26, 0x2B, 0x01, 0x35, 0x33, 0x32, + 0x37, 0x36, 0x35, 0x34, 0x26, 0x23, 0x22, 0x07, 0x06, 0x07, + 0x25, 0x36, 0x24, 0x33, 0x32, 0x16, 0x17, 0x16, 0x15, 0x14, + 0x07, 0x06, 0x07, 0x15, 0x16, 0x17, 0x16, 0x04, 0x65, 0xFE, + 0xF2, 0xF0, 0xE5, 0x88, 0x87, 0x16, 0x01, 0x1E, 0x1A, 0xD1, + 0x6A, 0x3A, 0x3B, 0x49, 0x47, 0x88, 0x62, 0x5C, 0x78, 0x43, + 0x42, 0x6C, 0x5C, 0x57, 0x37, 0x39, 0x07, 0xFE, 0xE7, 0x17, + 0x01, 0x00, 0xD5, 0x95, 0xD8, 0x3A, 0x39, 0x4E, 0x4D, 0x92, + 0x9C, 0x5B, 0x5A, 0x01, 0x78, 0xBD, 0xD2, 0x65, 0x64, 0xBE, + 0x19, 0xBC, 0x33, 0x33, 0xAC, 0x2F, 0x2E, 0xE3, 0x2F, 0x2E, + 0x53, 0x4B, 0x60, 0x28, 0x29, 0x58, 0x14, 0xB3, 0xC2, 0x5A, + 0x52, 0x51, 0x6B, 0x7A, 0x53, 0x51, 0x1D, 0x04, 0x12, 0x55, + 0x54, 0x00, 0x00, 0x02, 0x00, 0x47, 0x00, 0x00, 0x04, 0x90, + 0x05, 0x45, 0x00, 0x0A, 0x00, 0x13, 0x00, 0x00, 0x01, 0x11, + 0x21, 0x11, 0x21, 0x35, 0x01, 0x21, 0x11, 0x33, 0x15, 0x01, + 0x34, 0x37, 0x36, 0x37, 0x06, 0x07, 0x01, 0x21, 0x03, 0xD4, + 0xFE, 0xF4, 0xFD, 0x7F, 0x02, 0x53, 0x01, 0x3A, 0xBC, 0xFE, + 0x38, 0x04, 0x02, 0x03, 0x17, 0x47, 0xFE, 0xC0, 0x01, 0x95, + 0x01, 0x1F, 0xFE, 0xE1, 0x01, 0x1F, 0xD3, 0x03, 0x53, 0xFC, + 0xAB, 0xD1, 0x02, 0x6C, 0x21, 0x54, 0x31, 0x20, 0x32, 0x70, + 0xFE, 0x41, 0x00, 0x01, 0x00, 0x68, 0xFF, 0xEC, 0x04, 0x63, + 0x05, 0x45, 0x00, 0x22, 0x00, 0x00, 0x01, 0x14, 0x07, 0x06, + 0x07, 0x06, 0x23, 0x22, 0x27, 0x26, 0x27, 0x25, 0x16, 0x17, + 0x16, 0x33, 0x32, 0x36, 0x35, 0x34, 0x27, 0x26, 0x23, 0x22, + 0x07, 0x21, 0x13, 0x21, 0x15, 0x21, 0x03, 0x36, 0x33, 0x32, + 0x16, 0x04, 0x63, 0x3F, 0x3E, 0x76, 0x76, 0xA1, 0xD3, 0x80, + 0x80, 0x1E, 0x01, 0x19, 0x17, 0x37, 0x38, 0x55, 0x6C, 0x7A, + 0x3B, 0x3B, 0x6A, 0x76, 0x49, 0xFE, 0xEE, 0x31, 0x03, 0x4F, + 0xFD, 0xB0, 0x17, 0x66, 0x99, 0xC8, 0xF2, 0x01, 0xC6, 0x8C, + 0x6C, 0x6A, 0x3C, 0x3C, 0x60, 0x61, 0xB3, 0x17, 0x5B, 0x28, + 0x29, 0x80, 0x75, 0x66, 0x40, 0x3F, 0x5B, 0x02, 0xFB, 0xD1, + 0xFE, 0xBA, 0x5A, 0xF8, 0x00, 0x02, 0x00, 0x7D, 0xFF, 0xEC, + 0x04, 0x5B, 0x05, 0x5A, 0x00, 0x1C, 0x00, 0x2A, 0x00, 0x00, + 0x01, 0x14, 0x06, 0x23, 0x22, 0x27, 0x26, 0x11, 0x10, 0x00, + 0x33, 0x32, 0x17, 0x16, 0x17, 0x05, 0x26, 0x23, 0x22, 0x07, + 0x06, 0x15, 0x36, 0x37, 0x36, 0x33, 0x32, 0x17, 0x16, 0x05, + 0x34, 0x27, 0x26, 0x23, 0x22, 0x06, 0x15, 0x14, 0x17, 0x16, + 0x33, 0x32, 0x36, 0x04, 0x5B, 0xFE, 0xDC, 0xF5, 0x88, 0x87, + 0x01, 0x0E, 0xFD, 0xB3, 0x67, 0x68, 0x2B, 0xFE, 0xF7, 0x25, + 0x85, 0x72, 0x40, 0x40, 0x2C, 0x51, 0x4F, 0x66, 0xB8, 0x71, + 0x70, 0xFE, 0xE6, 0x37, 0x38, 0x61, 0x59, 0x74, 0x3B, 0x3B, + 0x5E, 0x5D, 0x6C, 0x01, 0xBE, 0xDA, 0xF8, 0xB8, 0xB7, 0x01, + 0x45, 0x01, 0x62, 0x01, 0x58, 0x4F, 0x50, 0xA5, 0x25, 0x8B, + 0x6C, 0x6B, 0xD3, 0x4A, 0x29, 0x28, 0x75, 0x74, 0xCE, 0x6D, + 0x3D, 0x3E, 0x70, 0x5A, 0x70, 0x4F, 0x4E, 0x80, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x83, 0x00, 0x00, 0x04, 0x44, 0x05, 0x45, + 0x00, 0x0C, 0x00, 0x00, 0x01, 0x00, 0x03, 0x02, 0x15, 0x21, + 0x10, 0x13, 0x12, 0x13, 0x21, 0x35, 0x21, 0x04, 0x44, 0xFF, + 0x00, 0x67, 0x68, 0xFE, 0xDB, 0x78, 0x7A, 0xFD, 0xFD, 0x44, + 0x03, 0xC1, 0x04, 0x66, 0xFE, 0xA3, 0xFE, 0xFC, 0xFE, 0xFA, + 0xFF, 0x01, 0x05, 0x01, 0x11, 0x01, 0x16, 0x01, 0x32, 0xE7, + 0x00, 0x03, 0x00, 0x6D, 0xFF, 0xEC, 0x04, 0x60, 0x05, 0x5A, + 0x00, 0x1F, 0x00, 0x29, 0x00, 0x36, 0x00, 0x00, 0x01, 0x14, + 0x07, 0x06, 0x23, 0x22, 0x27, 0x26, 0x35, 0x34, 0x37, 0x36, + 0x37, 0x35, 0x2E, 0x01, 0x35, 0x34, 0x36, 0x33, 0x32, 0x17, + 0x16, 0x15, 0x14, 0x07, 0x06, 0x07, 0x15, 0x16, 0x17, 0x16, + 0x01, 0x34, 0x23, 0x22, 0x15, 0x14, 0x17, 0x16, 0x33, 0x32, + 0x13, 0x34, 0x27, 0x26, 0x23, 0x22, 0x06, 0x15, 0x14, 0x33, + 0x32, 0x37, 0x36, 0x04, 0x60, 0x85, 0x86, 0xEE, 0xED, 0x86, + 0x87, 0x4F, 0x50, 0x80, 0x72, 0x8C, 0xF8, 0xDD, 0xE5, 0x7A, + 0x7B, 0x47, 0x47, 0x72, 0x88, 0x4D, 0x4E, 0xFE, 0xBC, 0xB9, + 0xB6, 0x2F, 0x30, 0x59, 0xB7, 0x21, 0x37, 0x38, 0x6D, 0x65, + 0x6E, 0xDB, 0x6B, 0x34, 0x35, 0x01, 0x7E, 0xBD, 0x6A, 0x6B, + 0x6A, 0x6A, 0xBC, 0x81, 0x58, 0x59, 0x15, 0x04, 0x1A, 0xA4, + 0x6F, 0xA4, 0xC2, 0x5E, 0x5F, 0xAB, 0x69, 0x55, 0x54, 0x17, + 0x04, 0x17, 0x56, 0x57, 0x01, 0xE2, 0xB2, 0xB2, 0x5C, 0x2F, + 0x30, 0xFE, 0x6D, 0x65, 0x34, 0x35, 0x6E, 0x64, 0xDF, 0x36, + 0x37, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x72, 0xFF, 0xEC, + 0x04, 0x52, 0x05, 0x5A, 0x00, 0x1C, 0x00, 0x2B, 0x00, 0x00, + 0x01, 0x10, 0x07, 0x06, 0x23, 0x22, 0x26, 0x27, 0x25, 0x16, + 0x33, 0x32, 0x37, 0x36, 0x37, 0x06, 0x07, 0x06, 0x23, 0x22, + 0x26, 0x35, 0x34, 0x37, 0x36, 0x33, 0x32, 0x17, 0x16, 0x05, + 0x34, 0x27, 0x26, 0x23, 0x22, 0x07, 0x06, 0x15, 0x14, 0x17, + 0x16, 0x32, 0x37, 0x36, 0x04, 0x52, 0x88, 0x87, 0xFF, 0xBA, + 0xD4, 0x2B, 0x01, 0x08, 0x27, 0x8D, 0x71, 0x42, 0x43, 0x01, + 0x26, 0x56, 0x59, 0x62, 0xB7, 0xDE, 0x82, 0x83, 0xE7, 0xF5, + 0x7F, 0x80, 0xFE, 0xD7, 0x3B, 0x3A, 0x5D, 0x5B, 0x36, 0x37, + 0x37, 0x36, 0xB8, 0x3A, 0x3B, 0x02, 0xB9, 0xFE, 0xA6, 0xBA, + 0xB9, 0xA0, 0xAB, 0x25, 0x93, 0x69, 0x6A, 0xCF, 0x4B, 0x2A, + 0x2B, 0xF6, 0xD0, 0xD6, 0x79, 0x7A, 0xA6, 0xA7, 0x9E, 0x79, + 0x4C, 0x4A, 0x40, 0x42, 0x6D, 0x6A, 0x45, 0x44, 0x3A, 0x3B, + 0x00, 0x02, 0x01, 0xD6, 0x00, 0x00, 0x02, 0xF7, 0x04, 0x3D, + 0x00, 0x03, 0x00, 0x07, 0x00, 0x00, 0x21, 0x11, 0x21, 0x11, + 0x01, 0x11, 0x21, 0x11, 0x01, 0xD6, 0x01, 0x21, 0xFE, 0xDF, + 0x01, 0x21, 0x01, 0x31, 0xFE, 0xCF, 0x03, 0x0C, 0x01, 0x31, + 0xFE, 0xCF, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x04, 0xCD, 0x05, 0x45, 0x00, 0x07, 0x00, 0x10, 0x00, 0x00, + 0x29, 0x01, 0x03, 0x21, 0x03, 0x21, 0x01, 0x21, 0x07, 0x06, + 0x07, 0x06, 0x03, 0x21, 0x02, 0x27, 0x26, 0x04, 0xCD, 0xFE, + 0xDA, 0x5C, 0xFE, 0x38, 0x5C, 0xFE, 0xD9, 0x01, 0xBB, 0x01, + 0x58, 0xAC, 0x06, 0x17, 0x10, 0x79, 0x01, 0x4C, 0x6B, 0x1E, + 0x16, 0x01, 0x4A, 0xFE, 0xB6, 0x05, 0x45, 0xB6, 0x1F, 0x5E, + 0x41, 0xFE, 0x57, 0x01, 0x79, 0x74, 0x56, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x63, 0x00, 0x00, 0x04, 0x6A, 0x05, 0x45, + 0x00, 0x19, 0x00, 0x00, 0x21, 0x11, 0x34, 0x37, 0x02, 0x07, + 0x03, 0x23, 0x03, 0x26, 0x27, 0x17, 0x16, 0x15, 0x11, 0x23, + 0x11, 0x21, 0x13, 0x16, 0x17, 0x36, 0x37, 0x13, 0x21, 0x11, + 0x03, 0x8C, 0x10, 0x3B, 0x10, 0x7C, 0xDA, 0x7E, 0x20, 0x2C, + 0x07, 0x07, 0xDC, 0x01, 0x5B, 0x87, 0x10, 0x16, 0x13, 0x15, + 0x88, 0x01, 0x4F, 0x03, 0x64, 0x6E, 0xBF, 0xFE, 0xDD, 0x32, + 0xFE, 0x2C, 0x01, 0xD4, 0x7A, 0xDB, 0x64, 0x62, 0x67, 0xFC, + 0x9C, 0x05, 0x45, 0xFE, 0x0D, 0x31, 0xB6, 0x94, 0x52, 0x01, + 0xF4, 0xFA, 0xBB, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x88, + 0x00, 0x00, 0x04, 0x76, 0x05, 0x45, 0x00, 0x0D, 0x00, 0x17, + 0x00, 0x00, 0x01, 0x14, 0x06, 0x07, 0x06, 0x2B, 0x01, 0x11, + 0x21, 0x11, 0x21, 0x20, 0x17, 0x16, 0x05, 0x34, 0x26, 0x2B, + 0x01, 0x11, 0x33, 0x32, 0x37, 0x36, 0x04, 0x76, 0x7A, 0x74, + 0x72, 0xA2, 0xC5, 0xFE, 0xD9, 0x01, 0xE0, 0x01, 0x00, 0x88, + 0x86, 0xFE, 0xD7, 0x82, 0x84, 0x98, 0xA0, 0x82, 0x3F, 0x3D, + 0x03, 0x9B, 0x86, 0xCA, 0x38, 0x37, 0xFE, 0x24, 0x05, 0x45, + 0x6D, 0x6B, 0xD7, 0x6E, 0x5C, 0xFE, 0x5F, 0x37, 0x36, 0x00, + 0x00, 0x00, 0x00, 0x1C, 0x01, 0x56, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x3A, 0x00, 0x76, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x07, 0x00, 0xC1, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x04, + 0x00, 0xD3, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, + 0x00, 0x16, 0x01, 0x06, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x04, 0x00, 0x0C, 0x01, 0x37, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x05, 0x00, 0x0C, 0x01, 0x5E, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x0C, 0x01, 0x85, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x55, + 0x02, 0x3E, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, + 0x00, 0x15, 0x02, 0xC0, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x09, 0x00, 0x0E, 0x02, 0xF4, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0B, 0x00, 0x1E, 0x03, 0x41, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x44, 0x03, 0xEA, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0x00, 0x2E, + 0x04, 0x8D, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, + 0x00, 0x2A, 0x05, 0x12, 0x00, 0x03, 0x00, 0x01, 0x04, 0x09, + 0x00, 0x00, 0x00, 0x74, 0x00, 0x00, 0x00, 0x03, 0x00, 0x01, + 0x04, 0x09, 0x00, 0x01, 0x00, 0x0E, 0x00, 0xB1, 0x00, 0x03, + 0x00, 0x01, 0x04, 0x09, 0x00, 0x02, 0x00, 0x08, 0x00, 0xC9, + 0x00, 0x03, 0x00, 0x01, 0x04, 0x09, 0x00, 0x03, 0x00, 0x2C, + 0x00, 0xD8, 0x00, 0x03, 0x00, 0x01, 0x04, 0x09, 0x00, 0x04, + 0x00, 0x18, 0x01, 0x1D, 0x00, 0x03, 0x00, 0x01, 0x04, 0x09, + 0x00, 0x05, 0x00, 0x18, 0x01, 0x44, 0x00, 0x03, 0x00, 0x01, + 0x04, 0x09, 0x00, 0x06, 0x00, 0x18, 0x01, 0x6B, 0x00, 0x03, + 0x00, 0x01, 0x04, 0x09, 0x00, 0x07, 0x00, 0xAA, 0x01, 0x92, + 0x00, 0x03, 0x00, 0x01, 0x04, 0x09, 0x00, 0x08, 0x00, 0x2A, + 0x02, 0x94, 0x00, 0x03, 0x00, 0x01, 0x04, 0x09, 0x00, 0x09, + 0x00, 0x1C, 0x02, 0xD6, 0x00, 0x03, 0x00, 0x01, 0x04, 0x09, + 0x00, 0x0B, 0x00, 0x3C, 0x03, 0x03, 0x00, 0x03, 0x00, 0x01, + 0x04, 0x09, 0x00, 0x0C, 0x00, 0x88, 0x03, 0x60, 0x00, 0x03, + 0x00, 0x01, 0x04, 0x09, 0x00, 0x0D, 0x00, 0x5C, 0x04, 0x2F, + 0x00, 0x03, 0x00, 0x01, 0x04, 0x09, 0x00, 0x0E, 0x00, 0x54, + 0x04, 0xBC, 0x00, 0x44, 0x00, 0x69, 0x00, 0x67, 0x00, 0x69, + 0x00, 0x74, 0x00, 0x69, 0x00, 0x7A, 0x00, 0x65, 0x00, 0x64, + 0x00, 0x20, 0x00, 0x64, 0x00, 0x61, 0x00, 0x74, 0x00, 0x61, + 0x00, 0x20, 0x00, 0x63, 0x00, 0x6F, 0x00, 0x70, 0x00, 0x79, + 0x00, 0x72, 0x00, 0x69, 0x00, 0x67, 0x00, 0x68, 0x00, 0x74, + 0x00, 0x20, 0x00, 0x28, 0x00, 0x63, 0x00, 0x29, 0x00, 0x20, + 0x00, 0x32, 0x00, 0x30, 0x00, 0x31, 0x00, 0x30, 0x00, 0x2D, + 0x00, 0x32, 0x00, 0x30, 0x00, 0x31, 0x00, 0x32, 0x00, 0x20, + 0x00, 0x47, 0x00, 0x6F, 0x00, 0x6F, 0x00, 0x67, 0x00, 0x6C, + 0x00, 0x65, 0x00, 0x20, 0x00, 0x43, 0x00, 0x6F, 0x00, 0x72, + 0x00, 0x70, 0x00, 0x6F, 0x00, 0x72, 0x00, 0x61, 0x00, 0x74, + 0x00, 0x69, 0x00, 0x6F, 0x00, 0x6E, 0x00, 0x2E, 0x00, 0x00, + 0x44, 0x69, 0x67, 0x69, 0x74, 0x69, 0x7A, 0x65, 0x64, 0x20, + 0x64, 0x61, 0x74, 0x61, 0x20, 0x63, 0x6F, 0x70, 0x79, 0x72, + 0x69, 0x67, 0x68, 0x74, 0x20, 0x28, 0x63, 0x29, 0x20, 0x32, + 0x30, 0x31, 0x30, 0x2D, 0x32, 0x30, 0x31, 0x32, 0x20, 0x47, + 0x6F, 0x6F, 0x67, 0x6C, 0x65, 0x20, 0x43, 0x6F, 0x72, 0x70, + 0x6F, 0x72, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x2E, 0x00, 0x00, + 0x43, 0x00, 0x6F, 0x00, 0x75, 0x00, 0x73, 0x00, 0x69, 0x00, + 0x6E, 0x00, 0x65, 0x00, 0x00, 0x43, 0x6F, 0x75, 0x73, 0x69, + 0x6E, 0x65, 0x00, 0x00, 0x42, 0x00, 0x6F, 0x00, 0x6C, 0x00, + 0x64, 0x00, 0x00, 0x42, 0x6F, 0x6C, 0x64, 0x00, 0x00, 0x31, + 0x00, 0x2E, 0x00, 0x32, 0x00, 0x31, 0x00, 0x3B, 0x00, 0x4D, + 0x00, 0x4F, 0x00, 0x4E, 0x00, 0x4F, 0x00, 0x3B, 0x00, 0x43, + 0x00, 0x6F, 0x00, 0x75, 0x00, 0x73, 0x00, 0x69, 0x00, 0x6E, + 0x00, 0x65, 0x00, 0x2D, 0x00, 0x42, 0x00, 0x6F, 0x00, 0x6C, + 0x00, 0x64, 0x00, 0x00, 0x31, 0x2E, 0x32, 0x31, 0x3B, 0x4D, + 0x4F, 0x4E, 0x4F, 0x3B, 0x43, 0x6F, 0x75, 0x73, 0x69, 0x6E, + 0x65, 0x2D, 0x42, 0x6F, 0x6C, 0x64, 0x00, 0x00, 0x43, 0x00, + 0x6F, 0x00, 0x75, 0x00, 0x73, 0x00, 0x69, 0x00, 0x6E, 0x00, + 0x65, 0x00, 0x20, 0x00, 0x42, 0x00, 0x6F, 0x00, 0x6C, 0x00, + 0x64, 0x00, 0x00, 0x43, 0x6F, 0x75, 0x73, 0x69, 0x6E, 0x65, + 0x20, 0x42, 0x6F, 0x6C, 0x64, 0x00, 0x00, 0x56, 0x00, 0x65, + 0x00, 0x72, 0x00, 0x73, 0x00, 0x69, 0x00, 0x6F, 0x00, 0x6E, + 0x00, 0x20, 0x00, 0x31, 0x00, 0x2E, 0x00, 0x32, 0x00, 0x31, + 0x00, 0x00, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x20, + 0x31, 0x2E, 0x32, 0x31, 0x00, 0x00, 0x43, 0x00, 0x6F, 0x00, + 0x75, 0x00, 0x73, 0x00, 0x69, 0x00, 0x6E, 0x00, 0x65, 0x00, + 0x2D, 0x00, 0x42, 0x00, 0x6F, 0x00, 0x6C, 0x00, 0x64, 0x00, + 0x00, 0x43, 0x6F, 0x75, 0x73, 0x69, 0x6E, 0x65, 0x2D, 0x42, + 0x6F, 0x6C, 0x64, 0x00, 0x00, 0x43, 0x00, 0x6F, 0x00, 0x75, + 0x00, 0x73, 0x00, 0x69, 0x00, 0x6E, 0x00, 0x65, 0x00, 0x20, + 0x00, 0x69, 0x00, 0x73, 0x00, 0x20, 0x00, 0x61, 0x00, 0x20, + 0x00, 0x74, 0x00, 0x72, 0x00, 0x61, 0x00, 0x64, 0x00, 0x65, + 0x00, 0x6D, 0x00, 0x61, 0x00, 0x72, 0x00, 0x6B, 0x00, 0x20, + 0x00, 0x6F, 0x00, 0x66, 0x00, 0x20, 0x00, 0x47, 0x00, 0x6F, + 0x00, 0x6F, 0x00, 0x67, 0x00, 0x6C, 0x00, 0x65, 0x00, 0x20, + 0x00, 0x49, 0x00, 0x6E, 0x00, 0x63, 0x00, 0x2E, 0x00, 0x20, + 0x00, 0x61, 0x00, 0x6E, 0x00, 0x64, 0x00, 0x20, 0x00, 0x6D, + 0x00, 0x61, 0x00, 0x79, 0x00, 0x20, 0x00, 0x62, 0x00, 0x65, + 0x00, 0x20, 0x00, 0x72, 0x00, 0x65, 0x00, 0x67, 0x00, 0x69, + 0x00, 0x73, 0x00, 0x74, 0x00, 0x65, 0x00, 0x72, 0x00, 0x65, + 0x00, 0x64, 0x00, 0x20, 0x00, 0x69, 0x00, 0x6E, 0x00, 0x20, + 0x00, 0x63, 0x00, 0x65, 0x00, 0x72, 0x00, 0x74, 0x00, 0x61, + 0x00, 0x69, 0x00, 0x6E, 0x00, 0x20, 0x00, 0x6A, 0x00, 0x75, + 0x00, 0x72, 0x00, 0x69, 0x00, 0x73, 0x00, 0x64, 0x00, 0x69, + 0x00, 0x63, 0x00, 0x74, 0x00, 0x69, 0x00, 0x6F, 0x00, 0x6E, + 0x00, 0x73, 0x00, 0x2E, 0x00, 0x00, 0x43, 0x6F, 0x75, 0x73, + 0x69, 0x6E, 0x65, 0x20, 0x69, 0x73, 0x20, 0x61, 0x20, 0x74, + 0x72, 0x61, 0x64, 0x65, 0x6D, 0x61, 0x72, 0x6B, 0x20, 0x6F, + 0x66, 0x20, 0x47, 0x6F, 0x6F, 0x67, 0x6C, 0x65, 0x20, 0x49, + 0x6E, 0x63, 0x2E, 0x20, 0x61, 0x6E, 0x64, 0x20, 0x6D, 0x61, + 0x79, 0x20, 0x62, 0x65, 0x20, 0x72, 0x65, 0x67, 0x69, 0x73, + 0x74, 0x65, 0x72, 0x65, 0x64, 0x20, 0x69, 0x6E, 0x20, 0x63, + 0x65, 0x72, 0x74, 0x61, 0x69, 0x6E, 0x20, 0x6A, 0x75, 0x72, + 0x69, 0x73, 0x64, 0x69, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x73, + 0x2E, 0x00, 0x00, 0x4D, 0x00, 0x6F, 0x00, 0x6E, 0x00, 0x6F, + 0x00, 0x74, 0x00, 0x79, 0x00, 0x70, 0x00, 0x65, 0x00, 0x20, + 0x00, 0x49, 0x00, 0x6D, 0x00, 0x61, 0x00, 0x67, 0x00, 0x69, + 0x00, 0x6E, 0x00, 0x67, 0x00, 0x20, 0x00, 0x49, 0x00, 0x6E, + 0x00, 0x63, 0x00, 0x2E, 0x00, 0x00, 0x4D, 0x6F, 0x6E, 0x6F, + 0x74, 0x79, 0x70, 0x65, 0x20, 0x49, 0x6D, 0x61, 0x67, 0x69, + 0x6E, 0x67, 0x20, 0x49, 0x6E, 0x63, 0x2E, 0x00, 0x00, 0x53, + 0x00, 0x74, 0x00, 0x65, 0x00, 0x76, 0x00, 0x65, 0x00, 0x20, + 0x00, 0x4D, 0x00, 0x61, 0x00, 0x74, 0x00, 0x74, 0x00, 0x65, + 0x00, 0x73, 0x00, 0x6F, 0x00, 0x6E, 0x00, 0x00, 0x53, 0x74, + 0x65, 0x76, 0x65, 0x20, 0x4D, 0x61, 0x74, 0x74, 0x65, 0x73, + 0x6F, 0x6E, 0x00, 0x00, 0x68, 0x00, 0x74, 0x00, 0x74, 0x00, + 0x70, 0x00, 0x3A, 0x00, 0x2F, 0x00, 0x2F, 0x00, 0x77, 0x00, + 0x77, 0x00, 0x77, 0x00, 0x2E, 0x00, 0x6D, 0x00, 0x6F, 0x00, + 0x6E, 0x00, 0x6F, 0x00, 0x74, 0x00, 0x79, 0x00, 0x70, 0x00, + 0x65, 0x00, 0x69, 0x00, 0x6D, 0x00, 0x61, 0x00, 0x67, 0x00, + 0x69, 0x00, 0x6E, 0x00, 0x67, 0x00, 0x2E, 0x00, 0x63, 0x00, + 0x6F, 0x00, 0x6D, 0x00, 0x00, 0x68, 0x74, 0x74, 0x70, 0x3A, + 0x2F, 0x2F, 0x77, 0x77, 0x77, 0x2E, 0x6D, 0x6F, 0x6E, 0x6F, + 0x74, 0x79, 0x70, 0x65, 0x69, 0x6D, 0x61, 0x67, 0x69, 0x6E, + 0x67, 0x2E, 0x63, 0x6F, 0x6D, 0x00, 0x00, 0x68, 0x00, 0x74, + 0x00, 0x74, 0x00, 0x70, 0x00, 0x3A, 0x00, 0x2F, 0x00, 0x2F, + 0x00, 0x77, 0x00, 0x77, 0x00, 0x77, 0x00, 0x2E, 0x00, 0x6D, + 0x00, 0x6F, 0x00, 0x6E, 0x00, 0x6F, 0x00, 0x74, 0x00, 0x79, + 0x00, 0x70, 0x00, 0x65, 0x00, 0x69, 0x00, 0x6D, 0x00, 0x61, + 0x00, 0x67, 0x00, 0x69, 0x00, 0x6E, 0x00, 0x67, 0x00, 0x2E, + 0x00, 0x63, 0x00, 0x6F, 0x00, 0x6D, 0x00, 0x2F, 0x00, 0x50, + 0x00, 0x72, 0x00, 0x6F, 0x00, 0x64, 0x00, 0x75, 0x00, 0x63, + 0x00, 0x74, 0x00, 0x73, 0x00, 0x53, 0x00, 0x65, 0x00, 0x72, + 0x00, 0x76, 0x00, 0x69, 0x00, 0x63, 0x00, 0x65, 0x00, 0x73, + 0x00, 0x2F, 0x00, 0x54, 0x00, 0x79, 0x00, 0x70, 0x00, 0x65, + 0x00, 0x44, 0x00, 0x65, 0x00, 0x73, 0x00, 0x69, 0x00, 0x67, + 0x00, 0x6E, 0x00, 0x65, 0x00, 0x72, 0x00, 0x53, 0x00, 0x68, + 0x00, 0x6F, 0x00, 0x77, 0x00, 0x63, 0x00, 0x61, 0x00, 0x73, + 0x00, 0x65, 0x00, 0x00, 0x68, 0x74, 0x74, 0x70, 0x3A, 0x2F, + 0x2F, 0x77, 0x77, 0x77, 0x2E, 0x6D, 0x6F, 0x6E, 0x6F, 0x74, + 0x79, 0x70, 0x65, 0x69, 0x6D, 0x61, 0x67, 0x69, 0x6E, 0x67, + 0x2E, 0x63, 0x6F, 0x6D, 0x2F, 0x50, 0x72, 0x6F, 0x64, 0x75, + 0x63, 0x74, 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x73, 0x2F, 0x54, 0x79, 0x70, 0x65, 0x44, 0x65, 0x73, 0x69, + 0x67, 0x6E, 0x65, 0x72, 0x53, 0x68, 0x6F, 0x77, 0x63, 0x61, + 0x73, 0x65, 0x00, 0x00, 0x4C, 0x00, 0x69, 0x00, 0x63, 0x00, + 0x65, 0x00, 0x6E, 0x00, 0x73, 0x00, 0x65, 0x00, 0x64, 0x00, + 0x20, 0x00, 0x75, 0x00, 0x6E, 0x00, 0x64, 0x00, 0x65, 0x00, + 0x72, 0x00, 0x20, 0x00, 0x74, 0x00, 0x68, 0x00, 0x65, 0x00, + 0x20, 0x00, 0x41, 0x00, 0x70, 0x00, 0x61, 0x00, 0x63, 0x00, + 0x68, 0x00, 0x65, 0x00, 0x20, 0x00, 0x4C, 0x00, 0x69, 0x00, + 0x63, 0x00, 0x65, 0x00, 0x6E, 0x00, 0x73, 0x00, 0x65, 0x00, + 0x2C, 0x00, 0x20, 0x00, 0x56, 0x00, 0x65, 0x00, 0x72, 0x00, + 0x73, 0x00, 0x69, 0x00, 0x6F, 0x00, 0x6E, 0x00, 0x20, 0x00, + 0x32, 0x00, 0x2E, 0x00, 0x30, 0x00, 0x00, 0x4C, 0x69, 0x63, + 0x65, 0x6E, 0x73, 0x65, 0x64, 0x20, 0x75, 0x6E, 0x64, 0x65, + 0x72, 0x20, 0x74, 0x68, 0x65, 0x20, 0x41, 0x70, 0x61, 0x63, + 0x68, 0x65, 0x20, 0x4C, 0x69, 0x63, 0x65, 0x6E, 0x73, 0x65, + 0x2C, 0x20, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x20, + 0x32, 0x2E, 0x30, 0x00, 0x00, 0x68, 0x00, 0x74, 0x00, 0x74, + 0x00, 0x70, 0x00, 0x3A, 0x00, 0x2F, 0x00, 0x2F, 0x00, 0x77, + 0x00, 0x77, 0x00, 0x77, 0x00, 0x2E, 0x00, 0x61, 0x00, 0x70, + 0x00, 0x61, 0x00, 0x63, 0x00, 0x68, 0x00, 0x65, 0x00, 0x2E, + 0x00, 0x6F, 0x00, 0x72, 0x00, 0x67, 0x00, 0x2F, 0x00, 0x6C, + 0x00, 0x69, 0x00, 0x63, 0x00, 0x65, 0x00, 0x6E, 0x00, 0x73, + 0x00, 0x65, 0x00, 0x73, 0x00, 0x2F, 0x00, 0x4C, 0x00, 0x49, + 0x00, 0x43, 0x00, 0x45, 0x00, 0x4E, 0x00, 0x53, 0x00, 0x45, + 0x00, 0x2D, 0x00, 0x32, 0x00, 0x2E, 0x00, 0x30, 0x00, 0x00, + 0x68, 0x74, 0x74, 0x70, 0x3A, 0x2F, 0x2F, 0x77, 0x77, 0x77, + 0x2E, 0x61, 0x70, 0x61, 0x63, 0x68, 0x65, 0x2E, 0x6F, 0x72, + 0x67, 0x2F, 0x6C, 0x69, 0x63, 0x65, 0x6E, 0x73, 0x65, 0x73, + 0x2F, 0x4C, 0x49, 0x43, 0x45, 0x4E, 0x53, 0x45, 0x2D, 0x32, + 0x2E, 0x30, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xFE, 0x24, 0x00, 0xCD, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x13, 0x00, 0x14, + 0x00, 0x15, 0x00, 0x16, 0x00, 0x17, 0x00, 0x18, 0x00, 0x19, + 0x00, 0x1A, 0x00, 0x1B, 0x00, 0x1C, 0x00, 0x1D, 0x00, 0x24, + 0x00, 0x30, 0x00, 0x33, 0x00, 0x00, 0x00, 0x01, 0x00, 0x03, + 0x00, 0x08, 0x00, 0x0A, 0x00, 0x0E, 0x00, 0x07, 0xFF, 0xFF, + 0x00, 0x0F, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, + 0x00, 0x16, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00, 0x03, + 0x00, 0x11, 0x00, 0x01, 0x00, 0x04, 0x00, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0A, + 0x00, 0x1E, 0x00, 0x2C, 0x00, 0x01, 0x68, 0x65, 0x62, 0x72, + 0x00, 0x08, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x63, 0x63, 0x6D, 0x70, + 0x00, 0x08, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x04, 0x00, 0x06, 0x00, 0x01, 0x00, 0x01, 0x00, 0x08, + 0x00, 0x03, 0x00, 0x00, 0x00, 0x02, 0x00, 0x10, 0x00, 0x14, + 0x00, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x0A, 0x00, 0x1E, 0x00, 0x38, 0x00, 0x01, + 0x68, 0x65, 0x62, 0x72, 0x00, 0x08, 0x00, 0x04, 0x00, 0x00, + 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, + 0x6D, 0x61, 0x72, 0x6B, 0x00, 0x08, 0x00, 0x00, 0x00, 0x07, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x04, + 0x00, 0x05, 0x00, 0x06, 0x00, 0x07, 0x00, 0x10, 0x00, 0x1E, + 0x00, 0x2C, 0x00, 0x3A, 0x00, 0x48, 0x00, 0x5E, 0x00, 0x7E, + 0x00, 0x08, 0x00, 0x01, 0x00, 0x04, 0x00, 0x78, 0x00, 0x94, + 0x00, 0xB0, 0x00, 0xCC, 0x00, 0x08, 0x00, 0x01, 0x00, 0x04, + 0x00, 0xDA, 0x00, 0xF6, 0x01, 0x12, 0x01, 0x2E, 0x00, 0x08, + 0x00, 0x01, 0x00, 0x04, 0x01, 0x3C, 0x01, 0x58, 0x01, 0x74, + 0x01, 0x90, 0x00, 0x08, 0x00, 0x01, 0x00, 0x04, 0x01, 0x9E, + 0x01, 0xBA, 0x01, 0xD6, 0x01, 0xF2, 0x00, 0x08, 0x00, 0x01, + 0x00, 0x08, 0x02, 0x00, 0x02, 0x1C, 0x02, 0x3E, 0x02, 0x60, + 0x02, 0x82, 0x02, 0xA4, 0x02, 0xC6, 0x02, 0xE8, 0x00, 0x08, + 0x00, 0x01, 0x00, 0x0D, 0x02, 0xEE, 0x03, 0x0A, 0x03, 0x26, + 0x03, 0x42, 0x03, 0x5E, 0x03, 0x7A, 0x03, 0x96, 0x03, 0xB2, + 0x03, 0xCE, 0x03, 0xEA, 0x04, 0x06, 0x04, 0x22, 0x04, 0x3E, + 0x00, 0x08, 0x00, 0x01, 0x00, 0x02, 0x04, 0x3A, 0x04, 0x5C, + 0x00, 0x03, 0x00, 0x01, 0x00, 0x14, 0x00, 0x01, 0x00, 0x10, + 0x00, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, + 0x00, 0x02, 0x00, 0x14, 0x00, 0x18, 0x00, 0x01, 0x00, 0x10, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x01, + 0x00, 0x14, 0x00, 0x01, 0x00, 0x10, 0x00, 0x01, 0x00, 0x18, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00, 0x14, + 0x00, 0x18, 0x00, 0x01, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x03, 0x00, 0x01, 0x00, 0x14, 0x00, 0x01, + 0x00, 0x10, 0x00, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x03, 0x00, 0x01, 0x00, 0x14, 0x00, 0x01, 0x00, 0x10, + 0x00, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, + 0x00, 0x01, 0x00, 0x14, 0x00, 0x01, 0x00, 0x10, 0x00, 0x01, + 0x00, 0x18, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x01, + 0x00, 0x14, 0x00, 0x01, 0x00, 0x10, 0x00, 0x01, 0x00, 0x18, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00, 0x14, + 0x00, 0x18, 0x00, 0x01, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x03, 0x00, 0x01, 0x00, 0x14, 0x00, 0x01, + 0x00, 0x10, 0x00, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x03, 0x00, 0x02, 0x00, 0x14, 0x00, 0x18, 0x00, 0x01, + 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, + 0x00, 0x01, 0x00, 0x14, 0x00, 0x01, 0x00, 0x10, 0x00, 0x01, + 0x00, 0x18, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x02, + 0x00, 0x14, 0x00, 0x18, 0x00, 0x01, 0x00, 0x10, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00, 0x14, + 0x00, 0x18, 0x00, 0x01, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00, 0x14, 0x00, 0x18, + 0x00, 0x01, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x03, 0x00, 0x02, 0x00, 0x14, 0x00, 0x18, 0x00, 0x01, + 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, + 0x00, 0x02, 0x00, 0x14, 0x00, 0x18, 0x00, 0x01, 0x00, 0x10, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x03, + 0x00, 0x16, 0x00, 0x1A, 0x00, 0x1E, 0x00, 0x01, 0x00, 0x12, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x03, 0x00, 0x03, 0x00, 0x16, 0x00, 0x1A, 0x00, 0x1E, + 0x00, 0x01, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x03, 0x00, 0x16, + 0x00, 0x1A, 0x00, 0x1E, 0x00, 0x01, 0x00, 0x12, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, + 0x00, 0x03, 0x00, 0x16, 0x00, 0x1A, 0x00, 0x1E, 0x00, 0x01, + 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x03, 0x00, 0x03, 0x00, 0x16, 0x00, 0x1A, + 0x00, 0x1E, 0x00, 0x01, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x03, + 0x00, 0x16, 0x00, 0x1A, 0x00, 0x1E, 0x00, 0x01, 0x00, 0x12, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x03, 0x00, 0x02, 0x00, 0x14, 0x00, 0x18, 0x00, 0x01, + 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, + 0x00, 0x02, 0x00, 0x14, 0x00, 0x18, 0x00, 0x01, 0x00, 0x10, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x02, + 0x00, 0x14, 0x00, 0x18, 0x00, 0x01, 0x00, 0x10, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00, 0x14, + 0x00, 0x18, 0x00, 0x01, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00, 0x14, 0x00, 0x18, + 0x00, 0x01, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x03, 0x00, 0x02, 0x00, 0x14, 0x00, 0x18, 0x00, 0x01, + 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, + 0x00, 0x02, 0x00, 0x14, 0x00, 0x18, 0x00, 0x01, 0x00, 0x10, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x02, + 0x00, 0x14, 0x00, 0x18, 0x00, 0x01, 0x00, 0x10, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00, 0x14, + 0x00, 0x18, 0x00, 0x01, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00, 0x14, 0x00, 0x18, + 0x00, 0x01, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x03, 0x00, 0x02, 0x00, 0x14, 0x00, 0x18, 0x00, 0x01, + 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, + 0x00, 0x02, 0x00, 0x14, 0x00, 0x18, 0x00, 0x01, 0x00, 0x10, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x02, + 0x00, 0x14, 0x00, 0x18, 0x00, 0x01, 0x00, 0x10, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00, 0x14, + 0x00, 0x18, 0x00, 0x01, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00, 0x1A, 0x00, 0x1E, + 0x00, 0x02, 0x00, 0x12, 0x00, 0x16, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x02, + 0x00, 0x1A, 0x00, 0x1E, 0x00, 0x02, 0x00, 0x12, 0x00, 0x16, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xDA, 0x32, + 0xF0, 0x84, 0x00, 0x00, 0x00, 0x00, 0xC8, 0x50, 0xBF, 0x5E, + 0x00, 0x00, 0x00, 0x00, 0xE4, 0x03, 0xE7, 0x99, + }; + + #endif // COUSINEBOLDSUBSET_H + \ No newline at end of file diff --git a/src/fonts/jost.h b/src/fonts/jost.h deleted file mode 100644 index 037f7fc..0000000 --- a/src/fonts/jost.h +++ /dev/null @@ -1,1869 +0,0 @@ -// SPDX-FileCopyrightText: 2023 ThingPulse Ltd., https://thingpulse.com -// SPDX-License-Identifier: MIT - -// This was created as per https://github.com/takkaO/OpenFontRender/blob/master/examples/TFT_eSPI/load_from_binary/load_from_binary.ino#L1 -// The generated font supports the following characters; basic ASCII, accented chars from extended ASCII plus €: -// !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~€ÇüéâäàåçêëèïîìÄÅɧÆôöòûùÿÖÜ¢£¥₧ƒáíóúñѰ∞ - -#ifndef _JOSTTTF_H_ -#define _JOSTTTF_H_ - -const unsigned char jost[18556] = { -0x00, 0x01, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x80, 0x00, 0x03, -0x00, 0x30, 0x63, 0x6D, 0x61, 0x70, 0xF8, 0xC0, 0x18, 0x8E, -0x00, 0x00, 0x00, 0xBC, 0x00, 0x00, 0x10, 0x3C, 0x67, 0x6C, -0x79, 0x66, 0xA5, 0x7E, 0x96, 0xCB, 0x00, 0x00, 0x10, 0xF8, -0x00, 0x00, 0x30, 0xB0, 0x68, 0x65, 0x61, 0x64, 0x25, 0x6B, -0x73, 0xE2, 0x00, 0x00, 0x41, 0xA8, 0x00, 0x00, 0x00, 0x36, -0x68, 0x68, 0x65, 0x61, 0x03, 0x67, 0x04, 0x48, 0x00, 0x00, -0x41, 0xE0, 0x00, 0x00, 0x00, 0x24, 0x68, 0x6D, 0x74, 0x78, -0x13, 0xA1, 0x17, 0x4B, 0x00, 0x00, 0x42, 0x04, 0x00, 0x00, -0x02, 0x20, 0x6C, 0x6F, 0x63, 0x61, 0x00, 0x0B, 0xD2, 0x6D, -0x00, 0x00, 0x44, 0x24, 0x00, 0x00, 0x02, 0x24, 0x6D, 0x61, -0x78, 0x70, 0x07, 0x86, 0x11, 0x1F, 0x00, 0x00, 0x46, 0x48, -0x00, 0x00, 0x00, 0x20, 0x6E, 0x61, 0x6D, 0x65, 0x7B, 0xB1, -0x4F, 0xE5, 0x00, 0x00, 0x46, 0x68, 0x00, 0x00, 0x01, 0x8C, -0x4F, 0x53, 0x2F, 0x32, 0x5F, 0x7E, 0x35, 0x3F, 0x00, 0x00, -0x47, 0xF4, 0x00, 0x00, 0x00, 0x60, 0x70, 0x6F, 0x73, 0x74, -0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x48, 0x54, 0x00, 0x00, -0x00, 0x20, 0x70, 0x72, 0x65, 0x70, 0x68, 0x06, 0x8C, 0x85, -0x00, 0x00, 0x48, 0x74, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, -0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x1C, -0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x05, 0x7C, 0x00, 0x03, -0x00, 0x01, 0x00, 0x00, 0x0A, 0xDC, 0x00, 0x04, 0x05, 0x60, -0x00, 0x00, 0x01, 0x10, 0x01, 0x00, 0x00, 0x07, 0x00, 0x10, -0x00, 0x21, 0x00, 0x22, 0x00, 0x23, 0x00, 0x24, 0x00, 0x25, -0x00, 0x26, 0x00, 0x27, 0x00, 0x28, 0x00, 0x29, 0x00, 0x2A, -0x00, 0x2B, 0x00, 0x2C, 0x00, 0x2D, 0x00, 0x2E, 0x00, 0x2F, -0x00, 0x30, 0x00, 0x31, 0x00, 0x32, 0x00, 0x33, 0x00, 0x34, -0x00, 0x35, 0x00, 0x36, 0x00, 0x37, 0x00, 0x38, 0x00, 0x39, -0x00, 0x3A, 0x00, 0x3B, 0x00, 0x3C, 0x00, 0x3D, 0x00, 0x3E, -0x00, 0x3F, 0x00, 0x40, 0x00, 0x41, 0x00, 0x42, 0x00, 0x43, -0x00, 0x44, 0x00, 0x45, 0x00, 0x46, 0x00, 0x47, 0x00, 0x48, -0x00, 0x49, 0x00, 0x4A, 0x00, 0x4B, 0x00, 0x4C, 0x00, 0x4D, -0x00, 0x4E, 0x00, 0x4F, 0x00, 0x50, 0x00, 0x51, 0x00, 0x52, -0x00, 0x53, 0x00, 0x54, 0x00, 0x55, 0x00, 0x56, 0x00, 0x57, -0x00, 0x58, 0x00, 0x59, 0x00, 0x5A, 0x00, 0x5B, 0x00, 0x5C, -0x00, 0x5D, 0x00, 0x5E, 0x00, 0x5F, 0x00, 0x60, 0x00, 0x61, -0x00, 0x62, 0x00, 0x63, 0x00, 0x64, 0x00, 0x65, 0x00, 0x66, -0x00, 0x67, 0x00, 0x68, 0x00, 0x69, 0x00, 0x6A, 0x00, 0x6B, -0x00, 0x6C, 0x00, 0x6D, 0x00, 0x6E, 0x00, 0x6F, 0x00, 0x70, -0x00, 0x71, 0x00, 0x72, 0x00, 0x73, 0x00, 0x74, 0x00, 0x75, -0x00, 0x76, 0x00, 0x77, 0x00, 0x78, 0x00, 0x79, 0x00, 0x7A, -0x00, 0x7B, 0x00, 0x7C, 0x00, 0x7D, 0x00, 0x7E, 0x00, 0xA2, -0x00, 0xA3, 0x00, 0xA5, 0x00, 0xA7, 0x00, 0xB0, 0x00, 0xC4, -0x00, 0xC5, 0x00, 0xC6, 0x00, 0xC7, 0x00, 0xC9, 0x00, 0xD1, -0x00, 0xD6, 0x00, 0xDC, 0x00, 0xE0, 0x00, 0xE1, 0x00, 0xE2, -0x00, 0xE4, 0x00, 0xE5, 0x00, 0xE7, 0x00, 0xE8, 0x00, 0xE9, -0x00, 0xEA, 0x00, 0xEB, 0x00, 0xEC, 0x00, 0xED, 0x00, 0xEE, -0x00, 0xEF, 0x00, 0xF1, 0x00, 0xF2, 0x00, 0xF3, 0x00, 0xF4, -0x00, 0xF6, 0x00, 0xF9, 0x00, 0xFA, 0x00, 0xFB, 0x00, 0xFC, -0x00, 0xFF, 0x01, 0x92, 0x20, 0xA7, 0x20, 0xAC, 0x22, 0x1E, -0xFF, 0xFF, 0x00, 0x00, 0x00, 0x21, 0x00, 0x22, 0x00, 0x23, -0x00, 0x24, 0x00, 0x25, 0x00, 0x26, 0x00, 0x27, 0x00, 0x28, -0x00, 0x29, 0x00, 0x2A, 0x00, 0x2B, 0x00, 0x2C, 0x00, 0x2D, -0x00, 0x2E, 0x00, 0x2F, 0x00, 0x30, 0x00, 0x31, 0x00, 0x32, -0x00, 0x33, 0x00, 0x34, 0x00, 0x35, 0x00, 0x36, 0x00, 0x37, -0x00, 0x38, 0x00, 0x39, 0x00, 0x3A, 0x00, 0x3B, 0x00, 0x3C, -0x00, 0x3D, 0x00, 0x3E, 0x00, 0x3F, 0x00, 0x40, 0x00, 0x41, -0x00, 0x42, 0x00, 0x43, 0x00, 0x44, 0x00, 0x45, 0x00, 0x46, -0x00, 0x47, 0x00, 0x48, 0x00, 0x49, 0x00, 0x4A, 0x00, 0x4B, -0x00, 0x4C, 0x00, 0x4D, 0x00, 0x4E, 0x00, 0x4F, 0x00, 0x50, -0x00, 0x51, 0x00, 0x52, 0x00, 0x53, 0x00, 0x54, 0x00, 0x55, -0x00, 0x56, 0x00, 0x57, 0x00, 0x58, 0x00, 0x59, 0x00, 0x5A, -0x00, 0x5B, 0x00, 0x5C, 0x00, 0x5D, 0x00, 0x5E, 0x00, 0x5F, -0x00, 0x60, 0x00, 0x61, 0x00, 0x62, 0x00, 0x63, 0x00, 0x64, -0x00, 0x65, 0x00, 0x66, 0x00, 0x67, 0x00, 0x68, 0x00, 0x69, -0x00, 0x6A, 0x00, 0x6B, 0x00, 0x6C, 0x00, 0x6D, 0x00, 0x6E, -0x00, 0x6F, 0x00, 0x70, 0x00, 0x71, 0x00, 0x72, 0x00, 0x73, -0x00, 0x74, 0x00, 0x75, 0x00, 0x76, 0x00, 0x77, 0x00, 0x78, -0x00, 0x79, 0x00, 0x7A, 0x00, 0x7B, 0x00, 0x7C, 0x00, 0x7D, -0x00, 0x7E, 0x00, 0xA2, 0x00, 0xA3, 0x00, 0xA5, 0x00, 0xA7, -0x00, 0xB0, 0x00, 0xC4, 0x00, 0xC5, 0x00, 0xC6, 0x00, 0xC7, -0x00, 0xC9, 0x00, 0xD1, 0x00, 0xD6, 0x00, 0xDC, 0x00, 0xE0, -0x00, 0xE1, 0x00, 0xE2, 0x00, 0xE4, 0x00, 0xE5, 0x00, 0xE7, -0x00, 0xE8, 0x00, 0xE9, 0x00, 0xEA, 0x00, 0xEB, 0x00, 0xEC, -0x00, 0xED, 0x00, 0xEE, 0x00, 0xEF, 0x00, 0xF1, 0x00, 0xF2, -0x00, 0xF3, 0x00, 0xF4, 0x00, 0xF6, 0x00, 0xF9, 0x00, 0xFA, -0x00, 0xFB, 0x00, 0xFC, 0x00, 0xFF, 0x01, 0x92, 0x20, 0xA7, -0x20, 0xAC, 0x22, 0x1E, 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, 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, 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, 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, -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, 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, 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, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x00, 0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x04, 0x00, 0x05, -0x00, 0x06, 0x00, 0x07, 0x00, 0x08, 0x00, 0x09, 0x00, 0x0A, -0x00, 0x0B, 0x00, 0x0C, 0x00, 0x0D, 0x00, 0x0E, 0x00, 0x0F, -0x00, 0x10, 0x00, 0x11, 0x00, 0x12, 0x00, 0x13, 0x00, 0x14, -0x00, 0x15, 0x00, 0x16, 0x00, 0x17, 0x00, 0x18, 0x00, 0x19, -0x00, 0x1A, 0x00, 0x1B, 0x00, 0x1C, 0x00, 0x1D, 0x00, 0x1E, -0x00, 0x1F, 0x00, 0x20, 0x00, 0x21, 0x00, 0x22, 0x00, 0x23, -0x00, 0x24, 0x00, 0x25, 0x00, 0x26, 0x00, 0x27, 0x00, 0x28, -0x00, 0x29, 0x00, 0x2A, 0x00, 0x2B, 0x00, 0x2C, 0x00, 0x2D, -0x00, 0x2E, 0x00, 0x2F, 0x00, 0x30, 0x00, 0x31, 0x00, 0x32, -0x00, 0x33, 0x00, 0x34, 0x00, 0x35, 0x00, 0x36, 0x00, 0x37, -0x00, 0x38, 0x00, 0x39, 0x00, 0x3A, 0x00, 0x3B, 0x00, 0x3C, -0x00, 0x3D, 0x00, 0x3E, 0x00, 0x3F, 0x00, 0x40, 0x00, 0x41, -0x00, 0x42, 0x00, 0x43, 0x00, 0x44, 0x00, 0x45, 0x00, 0x46, -0x00, 0x47, 0x00, 0x48, 0x00, 0x49, 0x00, 0x4A, 0x00, 0x4B, -0x00, 0x4C, 0x00, 0x4D, 0x00, 0x4E, 0x00, 0x4F, 0x00, 0x50, -0x00, 0x51, 0x00, 0x52, 0x00, 0x53, 0x00, 0x54, 0x00, 0x55, -0x00, 0x56, 0x00, 0x57, 0x00, 0x58, 0x00, 0x59, 0x00, 0x5A, -0x00, 0x5B, 0x00, 0x5C, 0x00, 0x5D, 0x00, 0x5E, 0x00, 0x5F, -0x00, 0x60, 0x00, 0x61, 0x00, 0x62, 0x00, 0x63, 0x00, 0x64, -0x00, 0x65, 0x00, 0x66, 0x00, 0x67, 0x00, 0x68, 0x00, 0x69, -0x00, 0x6A, 0x00, 0x6B, 0x00, 0x6C, 0x00, 0x6D, 0x00, 0x6E, -0x00, 0x6F, 0x00, 0x70, 0x00, 0x71, 0x00, 0x72, 0x00, 0x73, -0x00, 0x74, 0x00, 0x75, 0x00, 0x76, 0x00, 0x77, 0x00, 0x78, -0x00, 0x79, 0x00, 0x7A, 0x00, 0x7B, 0x00, 0x7C, 0x00, 0x7D, -0x00, 0x7E, 0x00, 0x7F, 0x00, 0x80, 0x00, 0x81, 0x00, 0x82, -0x00, 0x83, 0x00, 0x84, 0x00, 0x85, 0x00, 0x86, 0x00, 0x87, -0x00, 0x00, 0x00, 0x04, 0x05, 0x60, 0x00, 0x00, 0x01, 0x10, -0x01, 0x00, 0x00, 0x07, 0x00, 0x10, 0x00, 0x21, 0x00, 0x22, -0x00, 0x23, 0x00, 0x24, 0x00, 0x25, 0x00, 0x26, 0x00, 0x27, -0x00, 0x28, 0x00, 0x29, 0x00, 0x2A, 0x00, 0x2B, 0x00, 0x2C, -0x00, 0x2D, 0x00, 0x2E, 0x00, 0x2F, 0x00, 0x30, 0x00, 0x31, -0x00, 0x32, 0x00, 0x33, 0x00, 0x34, 0x00, 0x35, 0x00, 0x36, -0x00, 0x37, 0x00, 0x38, 0x00, 0x39, 0x00, 0x3A, 0x00, 0x3B, -0x00, 0x3C, 0x00, 0x3D, 0x00, 0x3E, 0x00, 0x3F, 0x00, 0x40, -0x00, 0x41, 0x00, 0x42, 0x00, 0x43, 0x00, 0x44, 0x00, 0x45, -0x00, 0x46, 0x00, 0x47, 0x00, 0x48, 0x00, 0x49, 0x00, 0x4A, -0x00, 0x4B, 0x00, 0x4C, 0x00, 0x4D, 0x00, 0x4E, 0x00, 0x4F, -0x00, 0x50, 0x00, 0x51, 0x00, 0x52, 0x00, 0x53, 0x00, 0x54, -0x00, 0x55, 0x00, 0x56, 0x00, 0x57, 0x00, 0x58, 0x00, 0x59, -0x00, 0x5A, 0x00, 0x5B, 0x00, 0x5C, 0x00, 0x5D, 0x00, 0x5E, -0x00, 0x5F, 0x00, 0x60, 0x00, 0x61, 0x00, 0x62, 0x00, 0x63, -0x00, 0x64, 0x00, 0x65, 0x00, 0x66, 0x00, 0x67, 0x00, 0x68, -0x00, 0x69, 0x00, 0x6A, 0x00, 0x6B, 0x00, 0x6C, 0x00, 0x6D, -0x00, 0x6E, 0x00, 0x6F, 0x00, 0x70, 0x00, 0x71, 0x00, 0x72, -0x00, 0x73, 0x00, 0x74, 0x00, 0x75, 0x00, 0x76, 0x00, 0x77, -0x00, 0x78, 0x00, 0x79, 0x00, 0x7A, 0x00, 0x7B, 0x00, 0x7C, -0x00, 0x7D, 0x00, 0x7E, 0x00, 0xA2, 0x00, 0xA3, 0x00, 0xA5, -0x00, 0xA7, 0x00, 0xB0, 0x00, 0xC4, 0x00, 0xC5, 0x00, 0xC6, -0x00, 0xC7, 0x00, 0xC9, 0x00, 0xD1, 0x00, 0xD6, 0x00, 0xDC, -0x00, 0xE0, 0x00, 0xE1, 0x00, 0xE2, 0x00, 0xE4, 0x00, 0xE5, -0x00, 0xE7, 0x00, 0xE8, 0x00, 0xE9, 0x00, 0xEA, 0x00, 0xEB, -0x00, 0xEC, 0x00, 0xED, 0x00, 0xEE, 0x00, 0xEF, 0x00, 0xF1, -0x00, 0xF2, 0x00, 0xF3, 0x00, 0xF4, 0x00, 0xF6, 0x00, 0xF9, -0x00, 0xFA, 0x00, 0xFB, 0x00, 0xFC, 0x00, 0xFF, 0x01, 0x92, -0x20, 0xA7, 0x20, 0xAC, 0x22, 0x1E, 0xFF, 0xFF, 0x00, 0x00, -0x00, 0x21, 0x00, 0x22, 0x00, 0x23, 0x00, 0x24, 0x00, 0x25, -0x00, 0x26, 0x00, 0x27, 0x00, 0x28, 0x00, 0x29, 0x00, 0x2A, -0x00, 0x2B, 0x00, 0x2C, 0x00, 0x2D, 0x00, 0x2E, 0x00, 0x2F, -0x00, 0x30, 0x00, 0x31, 0x00, 0x32, 0x00, 0x33, 0x00, 0x34, -0x00, 0x35, 0x00, 0x36, 0x00, 0x37, 0x00, 0x38, 0x00, 0x39, -0x00, 0x3A, 0x00, 0x3B, 0x00, 0x3C, 0x00, 0x3D, 0x00, 0x3E, -0x00, 0x3F, 0x00, 0x40, 0x00, 0x41, 0x00, 0x42, 0x00, 0x43, -0x00, 0x44, 0x00, 0x45, 0x00, 0x46, 0x00, 0x47, 0x00, 0x48, -0x00, 0x49, 0x00, 0x4A, 0x00, 0x4B, 0x00, 0x4C, 0x00, 0x4D, -0x00, 0x4E, 0x00, 0x4F, 0x00, 0x50, 0x00, 0x51, 0x00, 0x52, -0x00, 0x53, 0x00, 0x54, 0x00, 0x55, 0x00, 0x56, 0x00, 0x57, -0x00, 0x58, 0x00, 0x59, 0x00, 0x5A, 0x00, 0x5B, 0x00, 0x5C, -0x00, 0x5D, 0x00, 0x5E, 0x00, 0x5F, 0x00, 0x60, 0x00, 0x61, -0x00, 0x62, 0x00, 0x63, 0x00, 0x64, 0x00, 0x65, 0x00, 0x66, -0x00, 0x67, 0x00, 0x68, 0x00, 0x69, 0x00, 0x6A, 0x00, 0x6B, -0x00, 0x6C, 0x00, 0x6D, 0x00, 0x6E, 0x00, 0x6F, 0x00, 0x70, -0x00, 0x71, 0x00, 0x72, 0x00, 0x73, 0x00, 0x74, 0x00, 0x75, -0x00, 0x76, 0x00, 0x77, 0x00, 0x78, 0x00, 0x79, 0x00, 0x7A, -0x00, 0x7B, 0x00, 0x7C, 0x00, 0x7D, 0x00, 0x7E, 0x00, 0xA2, -0x00, 0xA3, 0x00, 0xA5, 0x00, 0xA7, 0x00, 0xB0, 0x00, 0xC4, -0x00, 0xC5, 0x00, 0xC6, 0x00, 0xC7, 0x00, 0xC9, 0x00, 0xD1, -0x00, 0xD6, 0x00, 0xDC, 0x00, 0xE0, 0x00, 0xE1, 0x00, 0xE2, -0x00, 0xE4, 0x00, 0xE5, 0x00, 0xE7, 0x00, 0xE8, 0x00, 0xE9, -0x00, 0xEA, 0x00, 0xEB, 0x00, 0xEC, 0x00, 0xED, 0x00, 0xEE, -0x00, 0xEF, 0x00, 0xF1, 0x00, 0xF2, 0x00, 0xF3, 0x00, 0xF4, -0x00, 0xF6, 0x00, 0xF9, 0x00, 0xFA, 0x00, 0xFB, 0x00, 0xFC, -0x00, 0xFF, 0x01, 0x92, 0x20, 0xA7, 0x20, 0xAC, 0x22, 0x1E, -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, 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, 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, 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, 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, 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, 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, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x00, 0x01, 0x00, 0x02, -0x00, 0x03, 0x00, 0x04, 0x00, 0x05, 0x00, 0x06, 0x00, 0x07, -0x00, 0x08, 0x00, 0x09, 0x00, 0x0A, 0x00, 0x0B, 0x00, 0x0C, -0x00, 0x0D, 0x00, 0x0E, 0x00, 0x0F, 0x00, 0x10, 0x00, 0x11, -0x00, 0x12, 0x00, 0x13, 0x00, 0x14, 0x00, 0x15, 0x00, 0x16, -0x00, 0x17, 0x00, 0x18, 0x00, 0x19, 0x00, 0x1A, 0x00, 0x1B, -0x00, 0x1C, 0x00, 0x1D, 0x00, 0x1E, 0x00, 0x1F, 0x00, 0x20, -0x00, 0x21, 0x00, 0x22, 0x00, 0x23, 0x00, 0x24, 0x00, 0x25, -0x00, 0x26, 0x00, 0x27, 0x00, 0x28, 0x00, 0x29, 0x00, 0x2A, -0x00, 0x2B, 0x00, 0x2C, 0x00, 0x2D, 0x00, 0x2E, 0x00, 0x2F, -0x00, 0x30, 0x00, 0x31, 0x00, 0x32, 0x00, 0x33, 0x00, 0x34, -0x00, 0x35, 0x00, 0x36, 0x00, 0x37, 0x00, 0x38, 0x00, 0x39, -0x00, 0x3A, 0x00, 0x3B, 0x00, 0x3C, 0x00, 0x3D, 0x00, 0x3E, -0x00, 0x3F, 0x00, 0x40, 0x00, 0x41, 0x00, 0x42, 0x00, 0x43, -0x00, 0x44, 0x00, 0x45, 0x00, 0x46, 0x00, 0x47, 0x00, 0x48, -0x00, 0x49, 0x00, 0x4A, 0x00, 0x4B, 0x00, 0x4C, 0x00, 0x4D, -0x00, 0x4E, 0x00, 0x4F, 0x00, 0x50, 0x00, 0x51, 0x00, 0x52, -0x00, 0x53, 0x00, 0x54, 0x00, 0x55, 0x00, 0x56, 0x00, 0x57, -0x00, 0x58, 0x00, 0x59, 0x00, 0x5A, 0x00, 0x5B, 0x00, 0x5C, -0x00, 0x5D, 0x00, 0x5E, 0x00, 0x5F, 0x00, 0x60, 0x00, 0x61, -0x00, 0x62, 0x00, 0x63, 0x00, 0x64, 0x00, 0x65, 0x00, 0x66, -0x00, 0x67, 0x00, 0x68, 0x00, 0x69, 0x00, 0x6A, 0x00, 0x6B, -0x00, 0x6C, 0x00, 0x6D, 0x00, 0x6E, 0x00, 0x6F, 0x00, 0x70, -0x00, 0x71, 0x00, 0x72, 0x00, 0x73, 0x00, 0x74, 0x00, 0x75, -0x00, 0x76, 0x00, 0x77, 0x00, 0x78, 0x00, 0x79, 0x00, 0x7A, -0x00, 0x7B, 0x00, 0x7C, 0x00, 0x7D, 0x00, 0x7E, 0x00, 0x7F, -0x00, 0x80, 0x00, 0x81, 0x00, 0x82, 0x00, 0x83, 0x00, 0x84, -0x00, 0x85, 0x00, 0x86, 0x00, 0x87, 0x00, 0x00, 0x00, 0x04, -0x05, 0x60, 0x00, 0x00, 0x01, 0x10, 0x01, 0x00, 0x00, 0x07, -0x00, 0x10, 0x00, 0x21, 0x00, 0x22, 0x00, 0x23, 0x00, 0x24, -0x00, 0x25, 0x00, 0x26, 0x00, 0x27, 0x00, 0x28, 0x00, 0x29, -0x00, 0x2A, 0x00, 0x2B, 0x00, 0x2C, 0x00, 0x2D, 0x00, 0x2E, -0x00, 0x2F, 0x00, 0x30, 0x00, 0x31, 0x00, 0x32, 0x00, 0x33, -0x00, 0x34, 0x00, 0x35, 0x00, 0x36, 0x00, 0x37, 0x00, 0x38, -0x00, 0x39, 0x00, 0x3A, 0x00, 0x3B, 0x00, 0x3C, 0x00, 0x3D, -0x00, 0x3E, 0x00, 0x3F, 0x00, 0x40, 0x00, 0x41, 0x00, 0x42, -0x00, 0x43, 0x00, 0x44, 0x00, 0x45, 0x00, 0x46, 0x00, 0x47, -0x00, 0x48, 0x00, 0x49, 0x00, 0x4A, 0x00, 0x4B, 0x00, 0x4C, -0x00, 0x4D, 0x00, 0x4E, 0x00, 0x4F, 0x00, 0x50, 0x00, 0x51, -0x00, 0x52, 0x00, 0x53, 0x00, 0x54, 0x00, 0x55, 0x00, 0x56, -0x00, 0x57, 0x00, 0x58, 0x00, 0x59, 0x00, 0x5A, 0x00, 0x5B, -0x00, 0x5C, 0x00, 0x5D, 0x00, 0x5E, 0x00, 0x5F, 0x00, 0x60, -0x00, 0x61, 0x00, 0x62, 0x00, 0x63, 0x00, 0x64, 0x00, 0x65, -0x00, 0x66, 0x00, 0x67, 0x00, 0x68, 0x00, 0x69, 0x00, 0x6A, -0x00, 0x6B, 0x00, 0x6C, 0x00, 0x6D, 0x00, 0x6E, 0x00, 0x6F, -0x00, 0x70, 0x00, 0x71, 0x00, 0x72, 0x00, 0x73, 0x00, 0x74, -0x00, 0x75, 0x00, 0x76, 0x00, 0x77, 0x00, 0x78, 0x00, 0x79, -0x00, 0x7A, 0x00, 0x7B, 0x00, 0x7C, 0x00, 0x7D, 0x00, 0x7E, -0x00, 0xA2, 0x00, 0xA3, 0x00, 0xA5, 0x00, 0xA7, 0x00, 0xB0, -0x00, 0xC4, 0x00, 0xC5, 0x00, 0xC6, 0x00, 0xC7, 0x00, 0xC9, -0x00, 0xD1, 0x00, 0xD6, 0x00, 0xDC, 0x00, 0xE0, 0x00, 0xE1, -0x00, 0xE2, 0x00, 0xE4, 0x00, 0xE5, 0x00, 0xE7, 0x00, 0xE8, -0x00, 0xE9, 0x00, 0xEA, 0x00, 0xEB, 0x00, 0xEC, 0x00, 0xED, -0x00, 0xEE, 0x00, 0xEF, 0x00, 0xF1, 0x00, 0xF2, 0x00, 0xF3, -0x00, 0xF4, 0x00, 0xF6, 0x00, 0xF9, 0x00, 0xFA, 0x00, 0xFB, -0x00, 0xFC, 0x00, 0xFF, 0x01, 0x92, 0x20, 0xA7, 0x20, 0xAC, -0x22, 0x1E, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x21, 0x00, 0x22, -0x00, 0x23, 0x00, 0x24, 0x00, 0x25, 0x00, 0x26, 0x00, 0x27, -0x00, 0x28, 0x00, 0x29, 0x00, 0x2A, 0x00, 0x2B, 0x00, 0x2C, -0x00, 0x2D, 0x00, 0x2E, 0x00, 0x2F, 0x00, 0x30, 0x00, 0x31, -0x00, 0x32, 0x00, 0x33, 0x00, 0x34, 0x00, 0x35, 0x00, 0x36, -0x00, 0x37, 0x00, 0x38, 0x00, 0x39, 0x00, 0x3A, 0x00, 0x3B, -0x00, 0x3C, 0x00, 0x3D, 0x00, 0x3E, 0x00, 0x3F, 0x00, 0x40, -0x00, 0x41, 0x00, 0x42, 0x00, 0x43, 0x00, 0x44, 0x00, 0x45, -0x00, 0x46, 0x00, 0x47, 0x00, 0x48, 0x00, 0x49, 0x00, 0x4A, -0x00, 0x4B, 0x00, 0x4C, 0x00, 0x4D, 0x00, 0x4E, 0x00, 0x4F, -0x00, 0x50, 0x00, 0x51, 0x00, 0x52, 0x00, 0x53, 0x00, 0x54, -0x00, 0x55, 0x00, 0x56, 0x00, 0x57, 0x00, 0x58, 0x00, 0x59, -0x00, 0x5A, 0x00, 0x5B, 0x00, 0x5C, 0x00, 0x5D, 0x00, 0x5E, -0x00, 0x5F, 0x00, 0x60, 0x00, 0x61, 0x00, 0x62, 0x00, 0x63, -0x00, 0x64, 0x00, 0x65, 0x00, 0x66, 0x00, 0x67, 0x00, 0x68, -0x00, 0x69, 0x00, 0x6A, 0x00, 0x6B, 0x00, 0x6C, 0x00, 0x6D, -0x00, 0x6E, 0x00, 0x6F, 0x00, 0x70, 0x00, 0x71, 0x00, 0x72, -0x00, 0x73, 0x00, 0x74, 0x00, 0x75, 0x00, 0x76, 0x00, 0x77, -0x00, 0x78, 0x00, 0x79, 0x00, 0x7A, 0x00, 0x7B, 0x00, 0x7C, -0x00, 0x7D, 0x00, 0x7E, 0x00, 0xA2, 0x00, 0xA3, 0x00, 0xA5, -0x00, 0xA7, 0x00, 0xB0, 0x00, 0xC4, 0x00, 0xC5, 0x00, 0xC6, -0x00, 0xC7, 0x00, 0xC9, 0x00, 0xD1, 0x00, 0xD6, 0x00, 0xDC, -0x00, 0xE0, 0x00, 0xE1, 0x00, 0xE2, 0x00, 0xE4, 0x00, 0xE5, -0x00, 0xE7, 0x00, 0xE8, 0x00, 0xE9, 0x00, 0xEA, 0x00, 0xEB, -0x00, 0xEC, 0x00, 0xED, 0x00, 0xEE, 0x00, 0xEF, 0x00, 0xF1, -0x00, 0xF2, 0x00, 0xF3, 0x00, 0xF4, 0x00, 0xF6, 0x00, 0xF9, -0x00, 0xFA, 0x00, 0xFB, 0x00, 0xFC, 0x00, 0xFF, 0x01, 0x92, -0x20, 0xA7, 0x20, 0xAC, 0x22, 0x1E, 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, 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, -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, 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, 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, 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, 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, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0x10, -0x01, 0x10, 0x00, 0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x04, -0x00, 0x05, 0x00, 0x06, 0x00, 0x07, 0x00, 0x08, 0x00, 0x09, -0x00, 0x0A, 0x00, 0x0B, 0x00, 0x0C, 0x00, 0x0D, 0x00, 0x0E, -0x00, 0x0F, 0x00, 0x10, 0x00, 0x11, 0x00, 0x12, 0x00, 0x13, -0x00, 0x14, 0x00, 0x15, 0x00, 0x16, 0x00, 0x17, 0x00, 0x18, -0x00, 0x19, 0x00, 0x1A, 0x00, 0x1B, 0x00, 0x1C, 0x00, 0x1D, -0x00, 0x1E, 0x00, 0x1F, 0x00, 0x20, 0x00, 0x21, 0x00, 0x22, -0x00, 0x23, 0x00, 0x24, 0x00, 0x25, 0x00, 0x26, 0x00, 0x27, -0x00, 0x28, 0x00, 0x29, 0x00, 0x2A, 0x00, 0x2B, 0x00, 0x2C, -0x00, 0x2D, 0x00, 0x2E, 0x00, 0x2F, 0x00, 0x30, 0x00, 0x31, -0x00, 0x32, 0x00, 0x33, 0x00, 0x34, 0x00, 0x35, 0x00, 0x36, -0x00, 0x37, 0x00, 0x38, 0x00, 0x39, 0x00, 0x3A, 0x00, 0x3B, -0x00, 0x3C, 0x00, 0x3D, 0x00, 0x3E, 0x00, 0x3F, 0x00, 0x40, -0x00, 0x41, 0x00, 0x42, 0x00, 0x43, 0x00, 0x44, 0x00, 0x45, -0x00, 0x46, 0x00, 0x47, 0x00, 0x48, 0x00, 0x49, 0x00, 0x4A, -0x00, 0x4B, 0x00, 0x4C, 0x00, 0x4D, 0x00, 0x4E, 0x00, 0x4F, -0x00, 0x50, 0x00, 0x51, 0x00, 0x52, 0x00, 0x53, 0x00, 0x54, -0x00, 0x55, 0x00, 0x56, 0x00, 0x57, 0x00, 0x58, 0x00, 0x59, -0x00, 0x5A, 0x00, 0x5B, 0x00, 0x5C, 0x00, 0x5D, 0x00, 0x5E, -0x00, 0x5F, 0x00, 0x60, 0x00, 0x61, 0x00, 0x62, 0x00, 0x63, -0x00, 0x64, 0x00, 0x65, 0x00, 0x66, 0x00, 0x67, 0x00, 0x68, -0x00, 0x69, 0x00, 0x6A, 0x00, 0x6B, 0x00, 0x6C, 0x00, 0x6D, -0x00, 0x6E, 0x00, 0x6F, 0x00, 0x70, 0x00, 0x71, 0x00, 0x72, -0x00, 0x73, 0x00, 0x74, 0x00, 0x75, 0x00, 0x76, 0x00, 0x77, -0x00, 0x78, 0x00, 0x79, 0x00, 0x7A, 0x00, 0x7B, 0x00, 0x7C, -0x00, 0x7D, 0x00, 0x7E, 0x00, 0x7F, 0x00, 0x80, 0x00, 0x81, -0x00, 0x82, 0x00, 0x83, 0x00, 0x84, 0x00, 0x85, 0x00, 0x86, -0x00, 0x87, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, -0x02, 0x58, 0x08, 0x00, 0x00, 0x03, 0x00, 0x07, 0x00, 0x00, -0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, -0x00, 0x00, 0x02, 0x58, 0x00, 0x00, 0xFD, 0xAD, 0x02, 0x4E, -0x00, 0x00, 0xFD, 0xB2, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, -0xF8, 0x00, 0x00, 0x05, 0x00, 0x00, 0x07, 0xF6, 0x00, 0x00, -0x00, 0x02, 0x00, 0x5F, 0xFF, 0xF1, 0x00, 0xCD, 0x02, 0xBC, -0x00, 0x03, 0x00, 0x0F, 0x00, 0x00, 0x13, 0x13, 0x33, 0x13, -0x03, 0x14, 0x16, 0x33, 0x32, 0x36, 0x35, 0x34, 0x26, 0x23, -0x22, 0x06, 0x64, 0x14, 0x3C, 0x14, 0x69, 0x21, 0x16, 0x17, -0x20, 0x20, 0x17, 0x16, 0x21, 0x02, 0xBC, 0xFE, 0x0C, 0x01, -0xF4, 0xFD, 0x6C, 0x16, 0x21, 0x21, 0x16, 0x17, 0x20, 0x20, -0x00, 0x02, 0x00, 0x50, 0x01, 0x90, 0x01, 0x95, 0x02, 0xBC, -0x00, 0x03, 0x00, 0x07, 0x00, 0x00, 0x13, 0x03, 0x33, 0x13, -0x33, 0x03, 0x33, 0x13, 0x82, 0x32, 0x37, 0x5A, 0x55, 0x32, -0x37, 0x5A, 0x02, 0xBC, 0xFE, 0xD4, 0x01, 0x2C, 0xFE, 0xD4, -0x01, 0x2C, 0x00, 0x04, 0x00, 0x23, 0x00, 0x00, 0x02, 0x3A, -0x02, 0xBC, 0x00, 0x07, 0x00, 0x0F, 0x00, 0x17, 0x00, 0x1F, -0x00, 0x00, 0x13, 0x3B, 0x04, 0x35, 0x21, 0x03, 0x21, 0x35, -0x2B, 0x04, 0x01, 0x0F, 0x04, 0x33, 0x13, 0x21, 0x03, 0x33, -0x3F, 0x04, 0x4B, 0x87, 0x0E, 0xC0, 0x0E, 0x8C, 0xFE, 0x11, -0x28, 0x01, 0xEF, 0x80, 0x14, 0xC8, 0x10, 0x83, 0x01, 0xA4, -0x36, 0x04, 0x34, 0x05, 0x37, 0x4B, 0xAA, 0xFE, 0xE3, 0xAA, -0x4B, 0x37, 0x04, 0x36, 0x04, 0x35, 0x01, 0xB8, 0x41, 0xFE, -0xCF, 0x41, 0x01, 0xB3, 0xDE, 0x10, 0xD8, 0x12, 0xE4, 0x02, -0xBC, 0xFD, 0x44, 0xE2, 0x12, 0xDE, 0x10, 0xDA, 0x00, 0x03, -0x00, 0x37, 0xFF, 0x90, 0x02, 0x06, 0x03, 0x32, 0x00, 0x03, -0x00, 0x07, 0x00, 0x3B, 0x00, 0x00, 0x01, 0x15, 0x33, 0x35, -0x03, 0x15, 0x33, 0x35, 0x13, 0x37, 0x2E, 0x02, 0x23, 0x22, -0x06, 0x06, 0x15, 0x14, 0x1E, 0x02, 0x17, 0x1E, 0x02, 0x15, -0x14, 0x06, 0x06, 0x23, 0x22, 0x26, 0x26, 0x27, 0x07, 0x1E, -0x02, 0x33, 0x32, 0x36, 0x36, 0x35, 0x34, 0x2E, 0x02, 0x27, -0x2E, 0x02, 0x35, 0x34, 0x36, 0x36, 0x33, 0x32, 0x16, 0x16, -0x01, 0x03, 0x4A, 0x4A, 0x4A, 0x6B, 0x45, 0x0E, 0x3B, 0x56, -0x39, 0x39, 0x5C, 0x36, 0x23, 0x36, 0x3D, 0x1A, 0x28, 0x4B, -0x31, 0x1B, 0x35, 0x28, 0x2B, 0x43, 0x34, 0x15, 0x48, 0x16, -0x49, 0x62, 0x39, 0x46, 0x5E, 0x31, 0x22, 0x36, 0x3E, 0x1B, -0x2A, 0x4A, 0x2E, 0x18, 0x33, 0x28, 0x27, 0x36, 0x26, 0x03, -0x32, 0x96, 0x96, 0xFC, 0xFE, 0xA0, 0xA0, 0x01, 0xD2, 0x2E, -0x24, 0x45, 0x2D, 0x2D, 0x4D, 0x30, 0x30, 0x44, 0x2F, 0x1E, -0x09, 0x0E, 0x26, 0x3A, 0x2D, 0x23, 0x33, 0x1B, 0x26, 0x46, -0x30, 0x2B, 0x3A, 0x56, 0x31, 0x32, 0x5B, 0x3B, 0x30, 0x45, -0x2F, 0x20, 0x0A, 0x10, 0x23, 0x31, 0x26, 0x1F, 0x2A, 0x16, -0x21, 0x35, 0x00, 0x05, 0x00, 0x23, 0xFF, 0xF6, 0x02, 0xCC, -0x02, 0xC6, 0x00, 0x0F, 0x00, 0x1F, 0x00, 0x2F, 0x00, 0x3F, -0x00, 0x43, 0x00, 0x00, 0x13, 0x14, 0x16, 0x16, 0x33, 0x32, -0x36, 0x36, 0x35, 0x34, 0x26, 0x26, 0x23, 0x22, 0x06, 0x06, -0x17, 0x34, 0x36, 0x36, 0x33, 0x32, 0x16, 0x16, 0x15, 0x14, -0x06, 0x06, 0x23, 0x22, 0x26, 0x26, 0x01, 0x14, 0x16, 0x16, -0x33, 0x32, 0x36, 0x36, 0x35, 0x34, 0x26, 0x26, 0x23, 0x22, -0x06, 0x06, 0x17, 0x34, 0x36, 0x36, 0x33, 0x32, 0x16, 0x16, -0x15, 0x14, 0x06, 0x06, 0x23, 0x22, 0x26, 0x26, 0x13, 0x01, -0x33, 0x01, 0x23, 0x27, 0x48, 0x2F, 0x30, 0x47, 0x27, 0x27, -0x47, 0x30, 0x2F, 0x48, 0x27, 0x47, 0x15, 0x27, 0x1B, 0x1C, -0x26, 0x15, 0x15, 0x26, 0x1C, 0x1B, 0x27, 0x15, 0x01, 0x26, -0x27, 0x48, 0x2F, 0x30, 0x47, 0x27, 0x27, 0x47, 0x30, 0x2F, -0x48, 0x27, 0x47, 0x15, 0x27, 0x1B, 0x1C, 0x26, 0x15, 0x15, -0x26, 0x1C, 0x1B, 0x27, 0x15, 0x5C, 0xFE, 0x3E, 0x4B, 0x01, -0xC2, 0x02, 0x26, 0x2C, 0x49, 0x2B, 0x2B, 0x49, 0x2C, 0x2D, -0x48, 0x2B, 0x2B, 0x48, 0x2D, 0x1A, 0x2B, 0x1A, 0x1A, 0x2B, -0x1A, 0x19, 0x2C, 0x1A, 0x1A, 0x2C, 0xFE, 0x89, 0x2C, 0x49, -0x2B, 0x2B, 0x49, 0x2C, 0x2D, 0x48, 0x2B, 0x2B, 0x48, 0x2D, -0x1A, 0x2B, 0x1A, 0x1A, 0x2B, 0x1A, 0x19, 0x2C, 0x1A, 0x1A, -0x2C, 0x02, 0x3F, 0xFD, 0x44, 0x02, 0xBC, 0x00, 0x01, 0x00, -0x28, 0xFF, 0xF6, 0x02, 0x98, 0x02, 0xC7, 0x00, 0x3E, 0x00, -0x00, 0x13, 0x01, 0x33, 0x01, 0x26, 0x26, 0x35, 0x34, 0x36, -0x33, 0x32, 0x16, 0x16, 0x15, 0x14, 0x06, 0x07, 0x0E, 0x04, -0x15, 0x14, 0x16, 0x16, 0x33, 0x32, 0x3E, 0x02, 0x37, 0x27, -0x0E, 0x03, 0x23, 0x22, 0x26, 0x26, 0x35, 0x34, 0x3E, 0x02, -0x37, 0x3E, 0x03, 0x35, 0x34, 0x26, 0x26, 0x23, 0x22, 0x06, -0x06, 0x15, 0x14, 0x16, 0x16, 0xDF, 0x01, 0x50, 0x60, 0xFE, -0x8E, 0x1E, 0x36, 0x2E, 0x29, 0x1C, 0x26, 0x15, 0x41, 0x38, -0x14, 0x38, 0x3A, 0x31, 0x1F, 0x41, 0x67, 0x39, 0x43, 0x70, -0x60, 0x54, 0x28, 0x3E, 0x24, 0x4D, 0x52, 0x59, 0x30, 0x28, -0x42, 0x27, 0x24, 0x39, 0x40, 0x1B, 0x23, 0x36, 0x26, 0x14, -0x28, 0x4B, 0x35, 0x32, 0x4D, 0x2C, 0x27, 0x33, 0x01, 0x80, -0xFE, 0x80, 0x01, 0xA9, 0x23, 0x3E, 0x22, 0x26, 0x2E, 0x16, -0x26, 0x1A, 0x22, 0x47, 0x20, 0x0B, 0x1B, 0x24, 0x32, 0x42, -0x2C, 0x3B, 0x57, 0x2F, 0x32, 0x52, 0x67, 0x34, 0x27, 0x31, -0x5A, 0x47, 0x2A, 0x1E, 0x38, 0x26, 0x26, 0x36, 0x29, 0x21, -0x10, 0x15, 0x2B, 0x2F, 0x30, 0x19, 0x2A, 0x47, 0x2C, 0x26, -0x46, 0x2F, 0x26, 0x40, 0x33, 0x00, 0x01, 0x00, 0x50, 0x01, -0x90, 0x00, 0xE1, 0x02, 0xBC, 0x00, 0x03, 0x00, 0x00, 0x13, -0x03, 0x33, 0x13, 0x82, 0x32, 0x37, 0x5A, 0x02, 0xBC, 0xFE, -0xD4, 0x01, 0x2C, 0x00, 0x01, 0x00, 0x5A, 0xFF, 0x38, 0x01, -0x18, 0x03, 0x0C, 0x00, 0x0D, 0x00, 0x00, 0x13, 0x06, 0x06, -0x15, 0x14, 0x16, 0x17, 0x33, 0x26, 0x26, 0x35, 0x34, 0x36, -0x37, 0xCD, 0x3C, 0x37, 0x37, 0x3C, 0x4B, 0x34, 0x30, 0x30, -0x34, 0x03, 0x0C, 0x70, 0xFA, 0x80, 0x7F, 0xFA, 0x71, 0x71, -0xFA, 0x7F, 0x80, 0xFA, 0x70, 0x00, 0x01, 0x00, 0x1E, 0xFF, -0x38, 0x00, 0xDC, 0x03, 0x0C, 0x00, 0x0D, 0x00, 0x00, 0x17, -0x36, 0x36, 0x35, 0x34, 0x26, 0x27, 0x23, 0x16, 0x16, 0x15, -0x14, 0x06, 0x07, 0x69, 0x3C, 0x37, 0x37, 0x3C, 0x4B, 0x35, -0x2F, 0x2F, 0x35, 0xC8, 0x71, 0xFA, 0x7F, 0x80, 0xFA, 0x70, -0x70, 0xFA, 0x80, 0x7F, 0xFA, 0x71, 0x00, 0x05, 0x00, 0x64, -0x01, 0x47, 0x01, 0xEC, 0x02, 0xBC, 0x00, 0x03, 0x00, 0x07, -0x00, 0x0B, 0x00, 0x0F, 0x00, 0x13, 0x00, 0x00, 0x01, 0x07, -0x17, 0x37, 0x07, 0x27, 0x07, 0x17, 0x23, 0x37, 0x27, 0x07, -0x27, 0x17, 0x37, 0x27, 0x37, 0x17, 0x33, 0x37, 0x01, 0xD7, -0xB7, 0x10, 0xBC, 0x35, 0x7B, 0x28, 0x6A, 0xAC, 0x6A, 0x28, -0x7B, 0x35, 0xBC, 0x10, 0xB7, 0x8C, 0x0A, 0x32, 0x0A, 0x02, -0x57, 0x46, 0x30, 0x33, 0xA4, 0x98, 0x1E, 0xA3, 0xA3, 0x1E, -0x98, 0xA4, 0x33, 0x30, 0x46, 0x65, 0xC3, 0xC3, 0x00, 0x02, -0x00, 0x41, 0x00, 0x0A, 0x02, 0x26, 0x01, 0xF4, 0x00, 0x03, -0x00, 0x07, 0x00, 0x00, 0x37, 0x21, 0x35, 0x21, 0x37, 0x11, -0x33, 0x11, 0x41, 0x01, 0xE5, 0xFE, 0x1B, 0xC8, 0x55, 0xD7, -0x50, 0xCD, 0xFE, 0x16, 0x01, 0xEA, 0x00, 0x01, 0x00, 0x3C, -0xFF, 0x56, 0x00, 0xE4, 0x00, 0x6E, 0x00, 0x03, 0x00, 0x00, -0x37, 0x03, 0x17, 0x37, 0x94, 0x58, 0x3A, 0x6E, 0x6E, 0xFE, -0xFD, 0x15, 0xFA, 0x00, 0x01, 0x00, 0x05, 0x00, 0xC8, 0x00, -0xCD, 0x01, 0x13, 0x00, 0x03, 0x00, 0x00, 0x37, 0x33, 0x35, -0x23, 0x05, 0xC8, 0xC8, 0xC8, 0x4B, 0x00, 0x01, 0x00, 0x5F, -0xFF, 0xF1, 0x00, 0xCD, 0x00, 0x5F, 0x00, 0x0B, 0x00, 0x00, -0x37, 0x14, 0x16, 0x33, 0x32, 0x36, 0x35, 0x34, 0x26, 0x23, -0x22, 0x06, 0x5F, 0x21, 0x16, 0x17, 0x20, 0x20, 0x17, 0x16, -0x21, 0x28, 0x16, 0x21, 0x21, 0x16, 0x17, 0x20, 0x20, 0x00, -0x01, 0x00, 0x0F, 0xFF, 0x6A, 0x01, 0xF4, 0x02, 0xBC, 0x00, -0x03, 0x00, 0x00, 0x01, 0x01, 0x33, 0x01, 0x01, 0xA4, 0xFE, -0x6B, 0x50, 0x01, 0x95, 0x02, 0xBC, 0xFC, 0xAE, 0x03, 0x52, -0x00, 0x02, 0x00, 0x28, 0xFF, 0xF6, 0x02, 0x30, 0x02, 0xC6, -0x00, 0x13, 0x00, 0x23, 0x00, 0x00, 0x13, 0x34, 0x3E, 0x02, -0x33, 0x32, 0x1E, 0x02, 0x15, 0x14, 0x0E, 0x02, 0x23, 0x22, -0x2E, 0x02, 0x27, 0x14, 0x16, 0x16, 0x33, 0x32, 0x36, 0x36, -0x35, 0x34, 0x26, 0x26, 0x23, 0x22, 0x06, 0x06, 0x7D, 0x18, -0x2E, 0x41, 0x28, 0x29, 0x40, 0x2E, 0x18, 0x18, 0x2E, 0x40, -0x29, 0x28, 0x41, 0x2E, 0x18, 0x55, 0x42, 0x75, 0x4D, 0x4D, -0x75, 0x42, 0x42, 0x75, 0x4D, 0x4D, 0x75, 0x42, 0x01, 0x5E, -0x3D, 0x67, 0x4B, 0x29, 0x29, 0x4B, 0x67, 0x3D, 0x3D, 0x67, -0x4B, 0x29, 0x29, 0x4B, 0x67, 0x3D, 0x69, 0xA2, 0x5D, 0x5D, -0xA2, 0x69, 0x69, 0xA2, 0x5D, 0x5D, 0xA2, 0x00, 0x01, 0x00, -0x50, 0x00, 0x00, 0x01, 0x40, 0x02, 0xC6, 0x00, 0x05, 0x00, -0x00, 0x13, 0x37, 0x11, 0x33, 0x11, 0x07, 0x50, 0x9B, 0x55, -0xF0, 0x02, 0x32, 0x2E, 0xFD, 0xA0, 0x02, 0xC6, 0x3C, 0x00, -0x01, 0x00, 0x14, 0x00, 0x00, 0x02, 0x02, 0x02, 0xC7, 0x00, -0x1E, 0x00, 0x00, 0x33, 0x21, 0x35, 0x21, 0x37, 0x36, 0x36, -0x35, 0x34, 0x2E, 0x02, 0x23, 0x22, 0x06, 0x06, 0x15, 0x33, -0x34, 0x36, 0x36, 0x33, 0x32, 0x1E, 0x02, 0x15, 0x14, 0x06, -0x06, 0x07, 0x14, 0x01, 0xEE, 0xFE, 0xC1, 0xDF, 0x29, 0x34, -0x1A, 0x36, 0x54, 0x3A, 0x48, 0x67, 0x36, 0x55, 0x22, 0x41, -0x2D, 0x20, 0x32, 0x23, 0x12, 0x11, 0x21, 0x17, 0x50, 0xE3, -0x2A, 0x6A, 0x36, 0x20, 0x46, 0x3D, 0x27, 0x3F, 0x71, 0x4B, -0x35, 0x4D, 0x29, 0x16, 0x24, 0x2C, 0x17, 0x1C, 0x33, 0x31, -0x19, 0x00, 0x02, 0x00, 0x3C, 0xFF, 0xF6, 0x01, 0xF5, 0x02, -0xC6, 0x00, 0x19, 0x00, 0x36, 0x00, 0x00, 0x13, 0x32, 0x36, -0x36, 0x35, 0x34, 0x26, 0x26, 0x23, 0x22, 0x06, 0x06, 0x15, -0x33, 0x34, 0x36, 0x33, 0x32, 0x16, 0x16, 0x15, 0x14, 0x0E, -0x02, 0x23, 0x13, 0x32, 0x36, 0x36, 0x35, 0x34, 0x2E, 0x02, -0x23, 0x15, 0x32, 0x1E, 0x02, 0x15, 0x14, 0x06, 0x06, 0x23, -0x22, 0x26, 0x26, 0x35, 0x23, 0x14, 0x1E, 0x02, 0xFD, 0x44, -0x66, 0x3A, 0x2F, 0x57, 0x3C, 0x39, 0x59, 0x33, 0x51, 0x44, -0x31, 0x23, 0x2F, 0x18, 0x13, 0x24, 0x34, 0x22, 0x1E, 0x3F, -0x62, 0x39, 0x27, 0x45, 0x59, 0x33, 0x22, 0x3A, 0x2C, 0x19, -0x20, 0x3B, 0x28, 0x28, 0x3E, 0x23, 0x56, 0x20, 0x3A, 0x52, -0x01, 0x59, 0x30, 0x52, 0x31, 0x32, 0x54, 0x34, 0x2F, 0x52, -0x37, 0x2E, 0x3D, 0x1C, 0x33, 0x21, 0x19, 0x2C, 0x21, 0x13, -0xFE, 0x66, 0x30, 0x58, 0x3B, 0x34, 0x4A, 0x2F, 0x16, 0x37, -0x10, 0x21, 0x31, 0x20, 0x26, 0x38, 0x1F, 0x1E, 0x34, 0x21, -0x29, 0x47, 0x35, 0x1E, 0x00, 0x01, 0x00, 0x14, 0x00, 0x00, -0x02, 0x29, 0x02, 0xBC, 0x00, 0x0C, 0x00, 0x00, 0x37, 0x21, -0x35, 0x2B, 0x02, 0x13, 0x11, 0x15, 0x15, 0x33, 0x11, 0x23, -0x14, 0x02, 0x15, 0x8F, 0x11, 0xE9, 0xC6, 0x55, 0x23, 0x8C, -0x4B, 0x01, 0x28, 0xFE, 0xBC, 0x0B, 0xB0, 0x02, 0xBC, 0x00, -0x01, 0x00, 0x1E, 0xFF, 0xF6, 0x02, 0x0C, 0x02, 0xBC, 0x00, -0x25, 0x00, 0x00, 0x25, 0x34, 0x26, 0x26, 0x23, 0x22, 0x06, -0x07, 0x37, 0x21, 0x35, 0x21, 0x03, 0x3E, 0x02, 0x33, 0x32, -0x16, 0x16, 0x15, 0x14, 0x06, 0x06, 0x23, 0x22, 0x26, 0x26, -0x27, 0x07, 0x1E, 0x02, 0x33, 0x32, 0x3E, 0x02, 0x02, 0x0C, -0x36, 0x5F, 0x3C, 0x1A, 0x35, 0x19, 0x2F, 0x01, 0x08, 0xFE, -0xB6, 0x5C, 0x24, 0x37, 0x37, 0x21, 0x2D, 0x47, 0x29, 0x25, -0x46, 0x32, 0x26, 0x46, 0x35, 0x10, 0x48, 0x17, 0x43, 0x5F, -0x40, 0x33, 0x58, 0x44, 0x26, 0xE6, 0x46, 0x6A, 0x3C, 0x0D, -0x0D, 0xB9, 0x4B, 0xFE, 0x8B, 0x19, 0x1B, 0x0B, 0x28, 0x48, -0x30, 0x30, 0x48, 0x28, 0x21, 0x35, 0x1E, 0x31, 0x28, 0x43, -0x28, 0x1E, 0x3C, 0x5A, 0x00, 0x02, 0x00, 0x32, 0xFF, 0xF6, -0x02, 0x0E, 0x02, 0xBC, 0x00, 0x0F, 0x00, 0x26, 0x00, 0x00, -0x37, 0x34, 0x36, 0x36, 0x33, 0x32, 0x16, 0x16, 0x15, 0x14, -0x06, 0x06, 0x23, 0x22, 0x26, 0x26, 0x13, 0x03, 0x06, 0x06, -0x15, 0x14, 0x16, 0x16, 0x33, 0x32, 0x36, 0x36, 0x35, 0x34, -0x2E, 0x02, 0x23, 0x22, 0x06, 0x07, 0x17, 0x13, 0x83, 0x29, -0x47, 0x2D, 0x2D, 0x47, 0x29, 0x29, 0x47, 0x2D, 0x2D, 0x47, -0x29, 0xC7, 0xEC, 0x14, 0x18, 0x3F, 0x6C, 0x43, 0x44, 0x6B, -0x3F, 0x20, 0x39, 0x4B, 0x2B, 0x22, 0x35, 0x14, 0x06, 0xDB, -0xE6, 0x30, 0x4B, 0x2A, 0x2A, 0x4B, 0x30, 0x30, 0x49, 0x2A, -0x2A, 0x49, 0x02, 0x06, 0xFE, 0xBB, 0x1C, 0x4A, 0x2B, 0x48, -0x6C, 0x3C, 0x3C, 0x6C, 0x48, 0x30, 0x53, 0x3E, 0x22, 0x10, -0x19, 0x05, 0x01, 0x21, 0x00, 0x01, 0x00, 0x1E, 0x00, 0x00, -0x02, 0x08, 0x02, 0xBC, 0x00, 0x05, 0x00, 0x00, 0x13, 0x21, -0x01, 0x33, 0x01, 0x21, 0x1E, 0x01, 0x6B, 0xFE, 0xD7, 0x59, -0x01, 0x4F, 0xFE, 0x16, 0x02, 0x6C, 0xFD, 0x94, 0x02, 0xBC, -0x00, 0x04, 0x00, 0x37, 0xFF, 0xF6, 0x01, 0xEB, 0x02, 0xC6, -0x00, 0x11, 0x00, 0x21, 0x00, 0x35, 0x00, 0x45, 0x00, 0x00, -0x13, 0x14, 0x1E, 0x02, 0x33, 0x32, 0x3E, 0x02, 0x35, 0x34, -0x26, 0x26, 0x23, 0x22, 0x06, 0x06, 0x17, 0x34, 0x36, 0x36, -0x33, 0x32, 0x16, 0x16, 0x15, 0x14, 0x06, 0x06, 0x23, 0x22, -0x26, 0x26, 0x03, 0x14, 0x1E, 0x02, 0x33, 0x32, 0x3E, 0x02, -0x35, 0x34, 0x2E, 0x02, 0x23, 0x22, 0x0E, 0x02, 0x17, 0x34, -0x36, 0x36, 0x33, 0x32, 0x16, 0x16, 0x15, 0x14, 0x06, 0x06, -0x23, 0x22, 0x26, 0x26, 0x50, 0x1C, 0x33, 0x47, 0x2B, 0x2B, -0x47, 0x33, 0x1C, 0x33, 0x57, 0x37, 0x37, 0x57, 0x33, 0x51, -0x1D, 0x33, 0x20, 0x21, 0x32, 0x1D, 0x1F, 0x33, 0x1E, 0x1E, -0x33, 0x1F, 0x6A, 0x21, 0x3B, 0x4F, 0x2F, 0x2F, 0x4F, 0x3B, -0x21, 0x23, 0x3D, 0x4F, 0x2B, 0x2B, 0x4F, 0x3D, 0x23, 0x51, -0x27, 0x3F, 0x23, 0x23, 0x3F, 0x27, 0x27, 0x3F, 0x23, 0x23, -0x3F, 0x27, 0x02, 0x13, 0x29, 0x41, 0x2E, 0x18, 0x18, 0x2E, -0x41, 0x29, 0x32, 0x51, 0x30, 0x30, 0x51, 0x3C, 0x21, 0x34, -0x1D, 0x1D, 0x34, 0x21, 0x21, 0x32, 0x1C, 0x1C, 0x32, 0xFE, -0xD6, 0x29, 0x48, 0x38, 0x1F, 0x1F, 0x38, 0x48, 0x29, 0x30, -0x4B, 0x33, 0x1A, 0x1A, 0x33, 0x4B, 0x26, 0x26, 0x3D, 0x24, -0x24, 0x3D, 0x26, 0x2B, 0x3C, 0x20, 0x20, 0x3C, 0x00, 0x02, -0x00, 0x32, 0x00, 0x00, 0x02, 0x0E, 0x02, 0xC6, 0x00, 0x0F, -0x00, 0x26, 0x00, 0x00, 0x01, 0x14, 0x06, 0x06, 0x23, 0x22, -0x26, 0x26, 0x35, 0x34, 0x36, 0x36, 0x33, 0x32, 0x16, 0x16, -0x03, 0x13, 0x36, 0x36, 0x35, 0x34, 0x26, 0x26, 0x23, 0x22, -0x06, 0x06, 0x15, 0x14, 0x1E, 0x02, 0x33, 0x32, 0x36, 0x37, -0x27, 0x03, 0x01, 0xBD, 0x29, 0x47, 0x2D, 0x2D, 0x47, 0x29, -0x29, 0x47, 0x2D, 0x2D, 0x47, 0x29, 0xC7, 0xEC, 0x14, 0x18, -0x3F, 0x6B, 0x44, 0x43, 0x6C, 0x3F, 0x20, 0x39, 0x4B, 0x2B, -0x22, 0x35, 0x14, 0x06, 0xDB, 0x01, 0xD6, 0x30, 0x4A, 0x2B, -0x2B, 0x4A, 0x30, 0x30, 0x49, 0x2A, 0x2A, 0x49, 0xFD, 0xFA, -0x01, 0x45, 0x1C, 0x4A, 0x2B, 0x48, 0x6C, 0x3C, 0x3C, 0x6C, -0x48, 0x30, 0x53, 0x3E, 0x22, 0x10, 0x19, 0x05, 0xFE, 0xDF, -0x00, 0x02, 0x00, 0x5F, 0xFF, 0xF1, 0x00, 0xCD, 0x01, 0xDB, -0x00, 0x0B, 0x00, 0x17, 0x00, 0x00, 0x37, 0x14, 0x16, 0x33, -0x32, 0x36, 0x35, 0x34, 0x26, 0x23, 0x22, 0x06, 0x11, 0x14, -0x16, 0x33, 0x32, 0x36, 0x35, 0x34, 0x26, 0x23, 0x22, 0x06, -0x5F, 0x21, 0x16, 0x17, 0x20, 0x20, 0x17, 0x16, 0x21, 0x21, -0x16, 0x17, 0x20, 0x20, 0x17, 0x16, 0x21, 0x28, 0x16, 0x21, -0x21, 0x16, 0x17, 0x20, 0x20, 0x01, 0x65, 0x16, 0x21, 0x21, -0x16, 0x17, 0x20, 0x20, 0x00, 0x02, 0x00, 0x3C, 0xFF, 0x56, -0x00, 0xEB, 0x01, 0xDB, 0x00, 0x0B, 0x00, 0x0F, 0x00, 0x00, -0x13, 0x14, 0x16, 0x33, 0x32, 0x36, 0x35, 0x34, 0x26, 0x23, -0x22, 0x06, 0x13, 0x03, 0x17, 0x37, 0x7D, 0x21, 0x16, 0x17, -0x20, 0x20, 0x17, 0x16, 0x21, 0x17, 0x58, 0x3A, 0x6E, 0x01, -0xA4, 0x16, 0x21, 0x21, 0x16, 0x17, 0x20, 0x20, 0xFE, 0xB3, -0xFE, 0xFD, 0x15, 0xFA, 0x00, 0x01, 0x00, 0x41, 0x00, 0x0F, -0x02, 0x3F, 0x01, 0xF4, 0x00, 0x06, 0x00, 0x00, 0x13, 0x25, -0x35, 0x05, 0x15, 0x05, 0x35, 0xC5, 0x01, 0x7A, 0xFE, 0x02, -0x01, 0xFE, 0x01, 0x06, 0x9E, 0x50, 0xDC, 0x2D, 0xDC, 0x50, -0x00, 0x02, 0x00, 0x41, 0x00, 0x8C, 0x02, 0x26, 0x01, 0x6D, -0x00, 0x03, 0x00, 0x07, 0x00, 0x00, 0x37, 0x21, 0x35, 0x21, -0x35, 0x21, 0x35, 0x21, 0x41, 0x01, 0xE5, 0xFE, 0x1B, 0x01, -0xE5, 0xFE, 0x1B, 0x8C, 0x4B, 0x4B, 0x4B, 0x00, 0x01, 0x00, -0x41, 0x00, 0x0F, 0x02, 0x3F, 0x01, 0xF4, 0x00, 0x06, 0x00, -0x00, 0x25, 0x05, 0x15, 0x25, 0x35, 0x25, 0x15, 0x01, 0xBB, -0xFE, 0x86, 0x01, 0xFE, 0xFE, 0x02, 0xFD, 0x9E, 0x50, 0xDC, -0x2D, 0xDC, 0x50, 0x00, 0x02, 0x00, 0x3C, 0xFF, 0xF1, 0x01, -0xF1, 0x02, 0xC6, 0x00, 0x1C, 0x00, 0x28, 0x00, 0x00, 0x01, -0x14, 0x06, 0x06, 0x23, 0x23, 0x17, 0x33, 0x37, 0x3E, 0x02, -0x35, 0x34, 0x2E, 0x02, 0x23, 0x22, 0x06, 0x06, 0x07, 0x17, -0x36, 0x36, 0x33, 0x32, 0x16, 0x16, 0x03, 0x14, 0x16, 0x33, -0x32, 0x36, 0x35, 0x34, 0x26, 0x23, 0x22, 0x06, 0x01, 0xA0, -0x2E, 0x56, 0x3C, 0x1A, 0x14, 0x3C, 0x07, 0x38, 0x60, 0x3C, -0x21, 0x3B, 0x4F, 0x2F, 0x3B, 0x51, 0x38, 0x17, 0x41, 0x17, -0x47, 0x32, 0x2D, 0x42, 0x24, 0xDD, 0x21, 0x16, 0x17, 0x20, -0x20, 0x17, 0x16, 0x21, 0x01, 0xF4, 0x26, 0x47, 0x2E, 0xAF, -0x78, 0x03, 0x39, 0x5D, 0x39, 0x2D, 0x4C, 0x39, 0x20, 0x1D, -0x35, 0x23, 0x29, 0x26, 0x2D, 0x20, 0x3C, 0xFE, 0x09, 0x16, -0x21, 0x21, 0x16, 0x17, 0x20, 0x20, 0x00, 0x03, 0x00, 0x23, -0xFF, 0xF6, 0x02, 0xDF, 0x02, 0xC6, 0x00, 0x0E, 0x00, 0x1E, -0x00, 0x59, 0x00, 0x00, 0x01, 0x34, 0x36, 0x36, 0x33, 0x32, -0x16, 0x16, 0x15, 0x14, 0x06, 0x06, 0x23, 0x22, 0x26, 0x27, -0x14, 0x16, 0x16, 0x33, 0x32, 0x36, 0x36, 0x35, 0x34, 0x26, -0x26, 0x23, 0x22, 0x06, 0x06, 0x07, 0x14, 0x16, 0x16, 0x33, -0x32, 0x36, 0x37, 0x27, 0x06, 0x06, 0x23, 0x22, 0x26, 0x26, -0x35, 0x34, 0x3E, 0x02, 0x33, 0x32, 0x1E, 0x02, 0x15, 0x14, -0x06, 0x06, 0x23, 0x22, 0x26, 0x26, 0x35, 0x34, 0x36, 0x37, -0x37, 0x23, 0x07, 0x33, 0x06, 0x16, 0x16, 0x33, 0x32, 0x3E, -0x02, 0x35, 0x34, 0x2E, 0x02, 0x23, 0x22, 0x0E, 0x02, 0x01, -0x0A, 0x21, 0x34, 0x1B, 0x19, 0x27, 0x17, 0x26, 0x36, 0x19, -0x29, 0x29, 0x4C, 0x27, 0x3F, 0x24, 0x30, 0x4E, 0x2F, 0x1A, -0x36, 0x2B, 0x2A, 0x57, 0x3B, 0x9B, 0x4C, 0x93, 0x6B, 0x56, -0x69, 0x2D, 0x23, 0x31, 0x63, 0x35, 0x4F, 0x75, 0x40, 0x2C, -0x51, 0x73, 0x46, 0x3F, 0x5E, 0x3E, 0x1F, 0x25, 0x2F, 0x0F, -0x10, 0x11, 0x06, 0x01, 0x01, 0x29, 0x4B, 0x24, 0x07, 0x05, -0x14, 0x2D, 0x1E, 0x1C, 0x3E, 0x37, 0x22, 0x28, 0x50, 0x78, -0x50, 0x54, 0x8B, 0x66, 0x37, 0x01, 0x40, 0x2B, 0x3F, 0x22, -0x16, 0x28, 0x1C, 0x35, 0x3D, 0x1A, 0x30, 0x27, 0x31, 0x47, -0x25, 0x2F, 0x5E, 0x45, 0x28, 0x49, 0x2F, 0x31, 0x5E, 0x43, -0x63, 0x95, 0x52, 0x21, 0x1B, 0x39, 0x1F, 0x15, 0x41, 0x77, -0x51, 0x45, 0x76, 0x58, 0x32, 0x2A, 0x48, 0x5B, 0x32, 0x3F, -0x4D, 0x22, 0x0D, 0x15, 0x0C, 0x06, 0x10, 0x05, 0xE7, 0xFA, -0x21, 0x36, 0x21, 0x1A, 0x38, 0x5C, 0x42, 0x3C, 0x72, 0x5C, -0x36, 0x3C, 0x6A, 0x8E, 0x00, 0x02, 0x00, 0x05, 0x00, 0x00, -0x02, 0x8F, 0x02, 0xDF, 0x00, 0x03, 0x00, 0x0D, 0x00, 0x00, -0x37, 0x21, 0x27, 0x21, 0x37, 0x13, 0x17, 0x17, 0x33, 0x01, -0x01, 0x33, 0x37, 0x37, 0x93, 0x01, 0x72, 0x1E, 0xFE, 0xCA, -0x99, 0x78, 0x08, 0x66, 0x5F, 0xFE, 0xBB, 0xFE, 0xBB, 0x5F, -0x68, 0x08, 0xD2, 0x50, 0xFC, 0xFE, 0xE6, 0x14, 0xF0, 0x02, -0xDF, 0xFD, 0x21, 0xF6, 0x12, 0x00, 0x01, 0x00, 0x50, 0x00, -0x00, 0x01, 0xFC, 0x02, 0xBC, 0x00, 0x2C, 0x00, 0x00, 0x13, -0x33, 0x32, 0x36, 0x36, 0x35, 0x34, 0x26, 0x26, 0x23, 0x23, -0x11, 0x33, 0x32, 0x36, 0x36, 0x35, 0x34, 0x2E, 0x02, 0x23, -0x23, 0x15, 0x33, 0x32, 0x1E, 0x02, 0x15, 0x14, 0x06, 0x06, -0x23, 0x23, 0x11, 0x33, 0x32, 0x16, 0x15, 0x14, 0x06, 0x06, -0x23, 0x23, 0x93, 0x71, 0x3F, 0x5D, 0x34, 0x34, 0x5D, 0x3F, -0xB4, 0xBE, 0x44, 0x6B, 0x3F, 0x24, 0x41, 0x56, 0x33, 0x7B, -0x7B, 0x22, 0x38, 0x29, 0x16, 0x27, 0x45, 0x2D, 0x69, 0x5F, -0x35, 0x46, 0x20, 0x38, 0x23, 0x71, 0x01, 0x72, 0x24, 0x46, -0x31, 0x3C, 0x4D, 0x26, 0xFD, 0x44, 0x2D, 0x5B, 0x45, 0x30, -0x46, 0x2D, 0x16, 0x2F, 0x11, 0x22, 0x32, 0x20, 0x2B, 0x3C, -0x20, 0x02, 0x26, 0x37, 0x32, 0x21, 0x2E, 0x18, 0x00, 0x01, -0x00, 0x28, 0xFF, 0xF6, 0x02, 0x67, 0x02, 0xC6, 0x00, 0x21, -0x00, 0x00, 0x13, 0x34, 0x36, 0x36, 0x33, 0x32, 0x16, 0x16, -0x17, 0x35, 0x26, 0x26, 0x23, 0x22, 0x0E, 0x02, 0x15, 0x14, -0x1E, 0x02, 0x33, 0x32, 0x36, 0x37, 0x35, 0x0E, 0x02, 0x23, -0x22, 0x26, 0x26, 0x82, 0x49, 0x76, 0x45, 0x32, 0x53, 0x44, -0x18, 0x2C, 0x68, 0x4D, 0x49, 0x7F, 0x60, 0x36, 0x36, 0x60, -0x7F, 0x49, 0x4D, 0x68, 0x2C, 0x18, 0x44, 0x53, 0x32, 0x45, -0x76, 0x49, 0x01, 0x5E, 0x52, 0x7B, 0x46, 0x1A, 0x2F, 0x1F, -0x70, 0x27, 0x26, 0x35, 0x61, 0x84, 0x4E, 0x4E, 0x84, 0x61, -0x35, 0x26, 0x27, 0x70, 0x1F, 0x2F, 0x1A, 0x46, 0x7C, 0x00, -0x02, 0x00, 0x50, 0x00, 0x00, 0x02, 0x80, 0x02, 0xBC, 0x00, -0x03, 0x00, 0x1B, 0x00, 0x00, 0x13, 0x11, 0x33, 0x11, 0x13, -0x32, 0x36, 0x36, 0x35, 0x34, 0x26, 0x26, 0x23, 0x23, 0x15, -0x33, 0x32, 0x1E, 0x02, 0x15, 0x14, 0x0E, 0x02, 0x23, 0x23, -0x15, 0x50, 0x55, 0x7D, 0x66, 0x9E, 0x5A, 0x5A, 0x9E, 0x66, -0xA3, 0xA3, 0x3B, 0x62, 0x46, 0x26, 0x26, 0x46, 0x62, 0x3B, -0xA3, 0x02, 0xBC, 0xFD, 0x44, 0x02, 0xBC, 0xFD, 0x44, 0x58, -0x9D, 0x69, 0x69, 0x9D, 0x58, 0x55, 0x25, 0x45, 0x62, 0x3D, -0x3D, 0x62, 0x45, 0x25, 0x55, 0x00, 0x04, 0x00, 0x50, 0x00, -0x00, 0x01, 0xE0, 0x02, 0xBC, 0x00, 0x03, 0x00, 0x07, 0x00, -0x0B, 0x00, 0x0F, 0x00, 0x00, 0x33, 0x21, 0x35, 0x21, 0x11, -0x21, 0x35, 0x21, 0x11, 0x21, 0x35, 0x21, 0x03, 0x11, 0x33, -0x11, 0x7F, 0x01, 0x61, 0xFE, 0x9F, 0x01, 0x61, 0xFE, 0x9F, -0x01, 0x4D, 0xFE, 0xB3, 0x2F, 0x55, 0x50, 0x02, 0x1C, 0x50, -0xFE, 0x98, 0x50, 0x01, 0x18, 0xFD, 0x44, 0x02, 0xBC, 0x00, -0x03, 0x00, 0x50, 0x00, 0x00, 0x01, 0xB8, 0x02, 0xBC, 0x00, -0x03, 0x00, 0x07, 0x00, 0x0B, 0x00, 0x00, 0x13, 0x21, 0x35, -0x21, 0x11, 0x21, 0x35, 0x21, 0x03, 0x11, 0x33, 0x11, 0x7F, -0x01, 0x39, 0xFE, 0xC7, 0x01, 0x2F, 0xFE, 0xD1, 0x2F, 0x55, -0x02, 0x6C, 0x50, 0xFE, 0x98, 0x50, 0x01, 0x18, 0xFD, 0x44, -0x02, 0xBC, 0x00, 0x01, 0x00, 0x28, 0xFF, 0xF6, 0x02, 0xDA, -0x02, 0xC6, 0x00, 0x26, 0x00, 0x00, 0x01, 0x33, 0x0E, 0x03, -0x23, 0x22, 0x26, 0x26, 0x35, 0x34, 0x36, 0x36, 0x33, 0x32, -0x16, 0x17, 0x37, 0x2E, 0x02, 0x23, 0x22, 0x0E, 0x02, 0x15, -0x14, 0x1E, 0x02, 0x33, 0x32, 0x3E, 0x02, 0x35, 0x21, 0x01, -0x95, 0xE9, 0x09, 0x27, 0x3C, 0x53, 0x34, 0x4A, 0x78, 0x47, -0x47, 0x78, 0x4A, 0x50, 0x76, 0x21, 0x3C, 0x1E, 0x57, 0x6D, -0x41, 0x4C, 0x82, 0x60, 0x35, 0x35, 0x60, 0x82, 0x4C, 0x50, -0x7D, 0x56, 0x2C, 0xFE, 0xBB, 0x01, 0x13, 0x29, 0x48, 0x38, -0x1F, 0x46, 0x7C, 0x51, 0x52, 0x7B, 0x46, 0x3F, 0x38, 0x37, -0x31, 0x42, 0x22, 0x35, 0x61, 0x84, 0x4E, 0x4E, 0x84, 0x61, -0x35, 0x3B, 0x66, 0x83, 0x49, 0x00, 0x03, 0x00, 0x50, 0x00, -0x00, 0x02, 0x7B, 0x02, 0xBC, 0x00, 0x03, 0x00, 0x07, 0x00, -0x0B, 0x00, 0x00, 0x13, 0x21, 0x35, 0x21, 0x01, 0x11, 0x33, -0x11, 0x21, 0x11, 0x33, 0x11, 0x7F, 0x01, 0xE0, 0xFE, 0x20, -0x01, 0xA7, 0x55, 0xFD, 0xD5, 0x55, 0x01, 0x54, 0x50, 0x01, -0x18, 0xFD, 0x44, 0x02, 0xBC, 0xFD, 0x44, 0x02, 0xBC, 0x00, -0x01, 0x00, 0x50, 0x00, 0x00, 0x00, 0xA5, 0x02, 0xBC, 0x00, -0x03, 0x00, 0x00, 0x13, 0x11, 0x33, 0x11, 0x50, 0x55, 0x02, -0xBC, 0xFD, 0x44, 0x02, 0xBC, 0x00, 0x01, 0xFF, 0x91, 0xFF, -0x24, 0x00, 0xA0, 0x02, 0xBC, 0x00, 0x12, 0x00, 0x00, 0x07, -0x07, 0x1E, 0x02, 0x33, 0x32, 0x3E, 0x02, 0x35, 0x11, 0x23, -0x11, 0x14, 0x06, 0x23, 0x22, 0x26, 0x43, 0x2C, 0x08, 0x23, -0x31, 0x1A, 0x23, 0x39, 0x28, 0x15, 0x50, 0x2D, 0x17, 0x1B, -0x29, 0x68, 0x42, 0x0A, 0x17, 0x11, 0x13, 0x28, 0x3C, 0x29, -0x02, 0xF8, 0xFD, 0x12, 0x31, 0x29, 0x13, 0x00, 0x02, 0x00, -0x50, 0x00, 0x00, 0x02, 0x30, 0x02, 0xBC, 0x00, 0x03, 0x00, -0x09, 0x00, 0x00, 0x13, 0x11, 0x33, 0x11, 0x21, 0x01, 0x01, -0x33, 0x01, 0x01, 0x50, 0x55, 0x01, 0x13, 0xFE, 0xD3, 0x01, -0x3C, 0x69, 0xFE, 0xC2, 0x01, 0x2F, 0x02, 0xBC, 0xFD, 0x44, -0x02, 0xBC, 0xFE, 0xB6, 0xFE, 0x8E, 0x01, 0x74, 0x01, 0x48, -0x00, 0x01, 0x00, 0x50, 0x00, 0x00, 0x01, 0xB8, 0x02, 0xBC, -0x00, 0x05, 0x00, 0x00, 0x13, 0x11, 0x21, 0x35, 0x21, 0x11, -0x50, 0x01, 0x68, 0xFE, 0xED, 0x02, 0xBC, 0xFD, 0x44, 0x50, -0x02, 0x6C, 0x00, 0x01, 0x00, 0x28, 0x00, 0x00, 0x03, 0x02, -0x02, 0xDF, 0x00, 0x09, 0x00, 0x00, 0x1B, 0x03, 0x33, 0x03, -0x01, 0x01, 0x03, 0x33, 0xB1, 0xE4, 0xE4, 0x34, 0x55, 0x55, -0xFE, 0xE8, 0xFE, 0xE8, 0x55, 0x55, 0x01, 0xD9, 0xFE, 0x68, -0x01, 0x98, 0xFE, 0x27, 0x02, 0xDF, 0xFE, 0x05, 0x01, 0xFB, -0xFD, 0x21, 0x00, 0x01, 0x00, 0x50, 0xFF, 0xDD, 0x02, 0xB7, -0x02, 0xDF, 0x00, 0x07, 0x00, 0x00, 0x01, 0x11, 0x01, 0x11, -0x33, 0x11, 0x01, 0x11, 0x02, 0x62, 0xFD, 0xEE, 0x55, 0x02, -0x12, 0x02, 0xBC, 0xFD, 0xF7, 0x02, 0x2C, 0xFD, 0x21, 0x02, -0x09, 0xFD, 0xD4, 0x02, 0xDF, 0x00, 0x02, 0x00, 0x28, 0xFF, -0xF6, 0x02, 0xEE, 0x02, 0xC6, 0x00, 0x0F, 0x00, 0x23, 0x00, -0x00, 0x13, 0x34, 0x36, 0x36, 0x33, 0x32, 0x16, 0x16, 0x15, -0x14, 0x06, 0x06, 0x23, 0x22, 0x26, 0x26, 0x27, 0x14, 0x1E, -0x02, 0x33, 0x32, 0x3E, 0x02, 0x35, 0x34, 0x2E, 0x02, 0x23, -0x22, 0x0E, 0x02, 0x82, 0x45, 0x78, 0x4C, 0x4D, 0x77, 0x45, -0x45, 0x77, 0x4D, 0x4C, 0x78, 0x45, 0x5A, 0x35, 0x60, 0x82, -0x4C, 0x4D, 0x81, 0x60, 0x35, 0x35, 0x60, 0x81, 0x4D, 0x4C, -0x82, 0x60, 0x35, 0x01, 0x5E, 0x4F, 0x7C, 0x48, 0x48, 0x7C, -0x4F, 0x4F, 0x7C, 0x48, 0x48, 0x7C, 0x4F, 0x4C, 0x84, 0x61, -0x37, 0x37, 0x61, 0x84, 0x4C, 0x4D, 0x83, 0x61, 0x37, 0x37, -0x61, 0x83, 0x00, 0x02, 0x00, 0x50, 0x00, 0x00, 0x01, 0xFC, -0x02, 0xBC, 0x00, 0x03, 0x00, 0x17, 0x00, 0x00, 0x13, 0x11, -0x33, 0x11, 0x07, 0x33, 0x32, 0x16, 0x15, 0x14, 0x06, 0x23, -0x23, 0x15, 0x33, 0x32, 0x36, 0x36, 0x35, 0x34, 0x26, 0x26, -0x23, 0x23, 0x50, 0x55, 0x26, 0x8F, 0x44, 0x55, 0x55, 0x44, -0x8F, 0x8F, 0x44, 0x6B, 0x3F, 0x3F, 0x6B, 0x44, 0x8F, 0x02, -0xBC, 0xFD, 0x44, 0x02, 0xBC, 0x50, 0x41, 0x41, 0x40, 0x42, -0x50, 0x33, 0x5F, 0x40, 0x41, 0x5E, 0x33, 0x00, 0x03, 0x00, -0x28, 0xFF, 0xF6, 0x03, 0x15, 0x02, 0xC6, 0x00, 0x0F, 0x00, -0x23, 0x00, 0x27, 0x00, 0x00, 0x13, 0x34, 0x36, 0x36, 0x33, -0x32, 0x16, 0x16, 0x15, 0x14, 0x06, 0x06, 0x23, 0x22, 0x26, -0x26, 0x27, 0x14, 0x1E, 0x02, 0x33, 0x32, 0x3E, 0x02, 0x35, -0x34, 0x2E, 0x02, 0x23, 0x22, 0x0E, 0x02, 0x05, 0x01, 0x33, -0x01, 0x82, 0x45, 0x78, 0x4C, 0x4D, 0x77, 0x45, 0x45, 0x77, -0x4D, 0x4C, 0x78, 0x45, 0x5A, 0x35, 0x60, 0x82, 0x4C, 0x4D, -0x81, 0x60, 0x35, 0x35, 0x60, 0x81, 0x4D, 0x4C, 0x82, 0x60, -0x35, 0x01, 0x53, 0x01, 0x2C, 0x6E, 0xFE, 0xD4, 0x01, 0x5E, -0x4F, 0x7C, 0x48, 0x48, 0x7C, 0x4F, 0x4F, 0x7C, 0x48, 0x48, -0x7C, 0x4F, 0x4C, 0x84, 0x61, 0x37, 0x37, 0x61, 0x84, 0x4C, -0x4D, 0x83, 0x61, 0x37, 0x37, 0x61, 0x83, 0x7F, 0xFE, 0xD4, -0x01, 0x2C, 0x00, 0x03, 0x00, 0x50, 0x00, 0x00, 0x02, 0x26, -0x02, 0xBC, 0x00, 0x03, 0x00, 0x07, 0x00, 0x1D, 0x00, 0x00, -0x13, 0x13, 0x33, 0x03, 0x03, 0x11, 0x33, 0x11, 0x07, 0x33, -0x32, 0x16, 0x16, 0x15, 0x14, 0x06, 0x06, 0x23, 0x23, 0x15, -0x33, 0x32, 0x36, 0x36, 0x35, 0x34, 0x26, 0x26, 0x23, 0x23, -0xCD, 0xF0, 0x69, 0xFA, 0xDC, 0x55, 0x26, 0x8F, 0x2D, 0x45, -0x27, 0x27, 0x45, 0x2D, 0x8F, 0x8F, 0x44, 0x6B, 0x3F, 0x3F, -0x6B, 0x44, 0x8F, 0x01, 0x59, 0xFE, 0xA7, 0x01, 0x59, 0x01, -0x63, 0xFD, 0x44, 0x02, 0xBC, 0x4B, 0x20, 0x3C, 0x2B, 0x2B, -0x3C, 0x20, 0x4B, 0x33, 0x5F, 0x40, 0x41, 0x5E, 0x33, 0x00, -0x01, 0x00, 0x26, 0xFF, 0xF6, 0x01, 0xF7, 0x02, 0xC6, 0x00, -0x32, 0x00, 0x00, 0x37, 0x07, 0x1E, 0x02, 0x33, 0x32, 0x3E, -0x02, 0x35, 0x34, 0x2E, 0x02, 0x27, 0x2E, 0x02, 0x35, 0x34, -0x36, 0x33, 0x32, 0x16, 0x16, 0x17, 0x37, 0x2E, 0x02, 0x23, -0x22, 0x06, 0x06, 0x15, 0x14, 0x1E, 0x02, 0x17, 0x1E, 0x02, -0x15, 0x14, 0x06, 0x23, 0x22, 0x26, 0x26, 0x6A, 0x44, 0x17, -0x49, 0x61, 0x39, 0x2A, 0x4D, 0x3D, 0x23, 0x22, 0x35, 0x3E, -0x1C, 0x3D, 0x47, 0x1E, 0x34, 0x39, 0x29, 0x39, 0x28, 0x0D, -0x49, 0x11, 0x3C, 0x55, 0x39, 0x3B, 0x5C, 0x34, 0x25, 0x3A, -0x40, 0x1A, 0x28, 0x47, 0x2C, 0x44, 0x38, 0x2A, 0x42, 0x35, -0xD4, 0x2E, 0x2E, 0x51, 0x31, 0x1C, 0x34, 0x4A, 0x2E, 0x30, -0x45, 0x31, 0x20, 0x0A, 0x15, 0x2B, 0x31, 0x1D, 0x22, 0x37, -0x20, 0x32, 0x1A, 0x2A, 0x25, 0x43, 0x2B, 0x2F, 0x52, 0x34, -0x30, 0x42, 0x2D, 0x1D, 0x09, 0x0E, 0x23, 0x38, 0x2F, 0x30, -0x3E, 0x24, 0x40, 0x00, 0x01, 0x00, 0x0A, 0x00, 0x00, 0x01, -0xC7, 0x02, 0xBC, 0x00, 0x07, 0x00, 0x00, 0x13, 0x33, 0x11, -0x33, 0x11, 0x33, 0x35, 0x21, 0x0A, 0xB4, 0x55, 0xB4, 0xFE, -0x43, 0x02, 0x6C, 0xFD, 0x94, 0x02, 0x6C, 0x50, 0x00, 0x01, -0x00, 0x4B, 0xFF, 0xF6, 0x02, 0x27, 0x02, 0xBC, 0x00, 0x15, -0x00, 0x00, 0x13, 0x11, 0x14, 0x1E, 0x02, 0x33, 0x32, 0x3E, -0x02, 0x35, 0x11, 0x23, 0x11, 0x14, 0x06, 0x23, 0x22, 0x26, -0x35, 0x11, 0x4B, 0x21, 0x3F, 0x58, 0x36, 0x36, 0x58, 0x3F, -0x21, 0x55, 0x4E, 0x4B, 0x4B, 0x4E, 0x02, 0xBC, 0xFE, 0x2A, -0x34, 0x58, 0x40, 0x24, 0x24, 0x40, 0x58, 0x34, 0x01, 0xD6, -0xFE, 0x2A, 0x48, 0x58, 0x58, 0x48, 0x01, 0xD6, 0x00, 0x01, -0x00, 0x05, 0xFF, 0xDD, 0x02, 0x8F, 0x02, 0xBC, 0x00, 0x05, -0x00, 0x00, 0x25, 0x03, 0x23, 0x01, 0x01, 0x23, 0x01, 0x4A, -0xE6, 0x5F, 0x01, 0x45, 0x01, 0x45, 0x5F, 0x9E, 0x02, 0x1E, -0xFD, 0x21, 0x02, 0xDF, 0x00, 0x01, 0x00, 0x0A, 0xFF, 0xDD, -0x03, 0xD4, 0x02, 0xDF, 0x00, 0x09, 0x00, 0x00, 0x25, 0x0B, -0x02, 0x23, 0x01, 0x13, 0x13, 0x01, 0x23, 0x02, 0xBF, 0xD0, -0xCC, 0xBA, 0x5F, 0x01, 0x13, 0xD2, 0xD2, 0x01, 0x13, 0x5F, -0xB9, 0x02, 0x26, 0xFD, 0xDA, 0x02, 0x03, 0xFD, 0x21, 0x02, -0x20, 0xFD, 0xE0, 0x02, 0xDF, 0x00, 0x01, 0x00, 0x0A, 0x00, -0x00, 0x02, 0x21, 0x02, 0xBC, 0x00, 0x0B, 0x00, 0x00, 0x01, -0x03, 0x03, 0x23, 0x13, 0x03, 0x33, 0x13, 0x13, 0x33, 0x03, -0x13, 0x01, 0xAE, 0x95, 0x92, 0x64, 0xCB, 0xE4, 0x64, 0xAA, -0xA5, 0x64, 0xDE, 0xCF, 0x02, 0xBC, 0xFE, 0xFD, 0x01, 0x03, -0xFE, 0xB3, 0xFE, 0x91, 0x01, 0x25, 0xFE, 0xDB, 0x01, 0x6F, -0x01, 0x4D, 0x00, 0x01, 0x00, 0x05, 0x00, 0x00, 0x02, 0x2C, -0x02, 0xBC, 0x00, 0x08, 0x00, 0x00, 0x01, 0x03, 0x03, 0x23, -0x13, 0x11, 0x33, 0x11, 0x13, 0x01, 0xCD, 0xB5, 0xB4, 0x5F, -0xE9, 0x55, 0xE9, 0x02, 0xBC, 0xFE, 0xBC, 0x01, 0x44, 0xFE, -0x71, 0xFE, 0xD3, 0x01, 0x2E, 0x01, 0x8E, 0x00, 0x01, 0x00, -0x0F, 0x00, 0x00, 0x02, 0x0D, 0x02, 0xBC, 0x00, 0x07, 0x00, -0x00, 0x13, 0x21, 0x01, 0x21, 0x35, 0x21, 0x01, 0x21, 0x23, -0x01, 0x5C, 0xFE, 0x90, 0x01, 0xEA, 0xFE, 0xA4, 0x01, 0x70, -0xFE, 0x16, 0x02, 0x6C, 0xFD, 0x94, 0x50, 0x02, 0x6C, 0x00, -0x01, 0x00, 0x64, 0xFF, 0x24, 0x01, 0x18, 0x03, 0x0C, 0x00, -0x07, 0x00, 0x00, 0x13, 0x33, 0x35, 0x23, 0x11, 0x33, 0x35, -0x23, 0xB3, 0x65, 0xB4, 0xB4, 0x65, 0x02, 0xC1, 0x4B, 0xFC, -0x18, 0x4B, 0x00, 0x01, 0x00, 0x0F, 0x00, 0x00, 0x01, 0xF4, -0x02, 0xBC, 0x00, 0x03, 0x00, 0x00, 0x13, 0x01, 0x33, 0x01, -0x0F, 0x01, 0x90, 0x55, 0xFE, 0x70, 0x02, 0xBC, 0xFD, 0x44, -0x02, 0xBC, 0x00, 0x01, 0x00, 0x14, 0xFF, 0x24, 0x00, 0xC8, -0x03, 0x0C, 0x00, 0x07, 0x00, 0x00, 0x17, 0x23, 0x15, 0x33, -0x11, 0x23, 0x15, 0x33, 0x79, 0x65, 0xB4, 0xB4, 0x65, 0x91, -0x4B, 0x03, 0xE8, 0x4B, 0x00, 0x01, 0x00, 0x41, 0x01, 0xCC, -0x01, 0xBD, 0x02, 0xC6, 0x00, 0x06, 0x00, 0x00, 0x13, 0x17, -0x33, 0x27, 0x23, 0x07, 0x33, 0xFF, 0x6E, 0x50, 0xAA, 0x28, -0xAA, 0x50, 0x02, 0x6D, 0xA1, 0xFA, 0xFA, 0x00, 0x01, 0x00, -0x00, 0xFF, 0x7B, 0x01, 0xF4, 0xFF, 0xB2, 0x00, 0x03, 0x00, -0x00, 0x15, 0x21, 0x35, 0x21, 0x01, 0xF4, 0xFE, 0x0C, 0x85, -0x37, 0x00, 0x01, 0x00, 0x4B, 0x01, 0xFE, 0x01, 0x2C, 0x02, -0xBC, 0x00, 0x03, 0x00, 0x00, 0x13, 0x17, 0x37, 0x27, 0x4B, -0xB4, 0x2D, 0x96, 0x02, 0x94, 0x96, 0x1E, 0xA0, 0x00, 0x02, -0x00, 0x1E, 0xFF, 0xF6, 0x01, 0xA5, 0x01, 0xD6, 0x00, 0x1F, -0x00, 0x33, 0x00, 0x00, 0x37, 0x34, 0x36, 0x36, 0x33, 0x32, -0x16, 0x17, 0x35, 0x2E, 0x02, 0x23, 0x22, 0x06, 0x15, 0x14, -0x16, 0x16, 0x33, 0x32, 0x36, 0x36, 0x35, 0x27, 0x14, 0x06, -0x06, 0x23, 0x22, 0x26, 0x26, 0x27, 0x3E, 0x02, 0x33, 0x32, -0x1E, 0x02, 0x15, 0x11, 0x33, 0x11, 0x34, 0x26, 0x26, 0x23, -0x22, 0x06, 0x07, 0x6F, 0x18, 0x31, 0x27, 0x2A, 0x4A, 0x23, -0x07, 0x28, 0x43, 0x32, 0x55, 0x5F, 0x2C, 0x49, 0x2A, 0x26, -0x4D, 0x35, 0x10, 0x23, 0x3B, 0x25, 0x1D, 0x2D, 0x19, 0x0C, -0x0B, 0x26, 0x37, 0x22, 0x15, 0x26, 0x1D, 0x10, 0x50, 0x2D, -0x51, 0x36, 0x40, 0x5B, 0x1A, 0x8E, 0x19, 0x24, 0x14, 0x15, -0x18, 0x2F, 0x09, 0x1D, 0x17, 0x51, 0x43, 0x2F, 0x42, 0x23, -0x1C, 0x3A, 0x2C, 0x3C, 0x24, 0x37, 0x1E, 0x13, 0x26, 0xEE, -0x08, 0x16, 0x10, 0x08, 0x12, 0x1F, 0x18, 0xFE, 0xC1, 0x01, -0x4A, 0x2D, 0x3E, 0x21, 0x26, 0x13, 0x00, 0x03, 0x00, 0x46, -0xFF, 0xF6, 0x02, 0x17, 0x03, 0x0C, 0x00, 0x03, 0x00, 0x13, -0x00, 0x23, 0x00, 0x00, 0x13, 0x23, 0x11, 0x33, 0x25, 0x34, -0x26, 0x26, 0x23, 0x22, 0x06, 0x06, 0x15, 0x14, 0x16, 0x16, -0x33, 0x32, 0x36, 0x36, 0x27, 0x14, 0x06, 0x06, 0x23, 0x22, -0x26, 0x26, 0x35, 0x34, 0x36, 0x36, 0x33, 0x32, 0x16, 0x16, -0x96, 0x50, 0x50, 0x01, 0x81, 0x3E, 0x67, 0x3F, 0x39, 0x57, -0x31, 0x31, 0x57, 0x39, 0x3F, 0x67, 0x3E, 0x51, 0x2B, 0x47, -0x2B, 0x23, 0x44, 0x2C, 0x2C, 0x44, 0x23, 0x2B, 0x47, 0x2B, -0x03, 0x0C, 0xFC, 0xF4, 0xE6, 0x4B, 0x6B, 0x3A, 0x3A, 0x6B, -0x4B, 0x4A, 0x6C, 0x3A, 0x3A, 0x6C, 0x4A, 0x35, 0x4A, 0x26, -0x26, 0x4A, 0x35, 0x35, 0x4A, 0x26, 0x26, 0x4A, 0x00, 0x01, -0x00, 0x1E, 0xFF, 0xF6, 0x01, 0xAC, 0x01, 0xD6, 0x00, 0x1F, -0x00, 0x00, 0x37, 0x34, 0x36, 0x36, 0x33, 0x32, 0x16, 0x16, -0x17, 0x35, 0x26, 0x26, 0x23, 0x22, 0x06, 0x06, 0x15, 0x14, -0x16, 0x16, 0x33, 0x32, 0x36, 0x37, 0x35, 0x0E, 0x02, 0x23, -0x22, 0x26, 0x26, 0x6F, 0x29, 0x47, 0x2D, 0x25, 0x41, 0x30, -0x0A, 0x18, 0x59, 0x2F, 0x43, 0x6C, 0x3F, 0x3F, 0x6C, 0x43, -0x2F, 0x59, 0x18, 0x0A, 0x30, 0x41, 0x25, 0x2D, 0x47, 0x29, -0xE6, 0x30, 0x4B, 0x2A, 0x17, 0x26, 0x17, 0x64, 0x1C, 0x1F, -0x3E, 0x6C, 0x46, 0x45, 0x6D, 0x3E, 0x1F, 0x1C, 0x64, 0x16, -0x27, 0x17, 0x2B, 0x4A, 0x00, 0x03, 0x00, 0x23, 0xFF, 0xF6, -0x01, 0xF4, 0x03, 0x0C, 0x00, 0x03, 0x00, 0x13, 0x00, 0x23, -0x00, 0x00, 0x01, 0x11, 0x33, 0x11, 0x01, 0x14, 0x16, 0x16, -0x33, 0x32, 0x36, 0x36, 0x35, 0x34, 0x26, 0x26, 0x23, 0x22, -0x06, 0x06, 0x17, 0x34, 0x36, 0x36, 0x33, 0x32, 0x16, 0x16, -0x15, 0x14, 0x06, 0x06, 0x23, 0x22, 0x26, 0x26, 0x01, 0xA4, -0x50, 0xFE, 0x2F, 0x3E, 0x68, 0x3E, 0x3A, 0x56, 0x31, 0x31, -0x56, 0x3A, 0x3E, 0x68, 0x3E, 0x51, 0x2B, 0x48, 0x2A, 0x23, -0x44, 0x2C, 0x2C, 0x44, 0x23, 0x2A, 0x48, 0x2B, 0x03, 0x0C, -0xFC, 0xF4, 0x03, 0x0C, 0xFD, 0xDA, 0x4A, 0x6C, 0x3A, 0x3A, -0x6C, 0x4A, 0x4B, 0x6B, 0x3A, 0x3A, 0x6B, 0x4B, 0x35, 0x4A, -0x26, 0x26, 0x4A, 0x35, 0x35, 0x4A, 0x26, 0x26, 0x4A, 0x00, -0x01, 0x00, 0x23, 0xFF, 0xF6, 0x01, 0xE0, 0x01, 0xD6, 0x00, -0x28, 0x00, 0x00, 0x17, 0x32, 0x36, 0x37, 0x27, 0x06, 0x06, -0x23, 0x22, 0x26, 0x26, 0x35, 0x3E, 0x02, 0x33, 0x32, 0x16, -0x16, 0x15, 0x14, 0x06, 0x07, 0x37, 0x21, 0x15, 0x21, 0x34, -0x36, 0x35, 0x34, 0x26, 0x26, 0x23, 0x22, 0x06, 0x06, 0x15, -0x14, 0x16, 0x16, 0xFD, 0x4A, 0x6D, 0x24, 0x41, 0x19, 0x48, -0x2F, 0x2F, 0x42, 0x22, 0x01, 0x23, 0x41, 0x2E, 0x27, 0x3A, -0x21, 0x06, 0x03, 0x20, 0xFE, 0xB6, 0x01, 0x89, 0x01, 0x34, -0x61, 0x44, 0x44, 0x67, 0x39, 0x37, 0x63, 0x0A, 0x3B, 0x3A, -0x29, 0x2A, 0x29, 0x28, 0x4C, 0x36, 0x35, 0x49, 0x27, 0x22, -0x3C, 0x29, 0x07, 0x15, 0x05, 0x2E, 0x44, 0x02, 0x0F, 0x07, -0x49, 0x6A, 0x39, 0x3C, 0x6C, 0x48, 0x47, 0x6C, 0x3D, 0x00, -0x02, 0x00, 0x28, 0x00, 0x00, 0x01, 0x32, 0x03, 0x16, 0x00, -0x03, 0x00, 0x16, 0x00, 0x00, 0x13, 0x15, 0x33, 0x35, 0x27, -0x37, 0x2E, 0x02, 0x23, 0x22, 0x06, 0x06, 0x15, 0x11, 0x33, -0x11, 0x34, 0x36, 0x36, 0x33, 0x32, 0x16, 0x28, 0xE6, 0x08, -0x2C, 0x07, 0x19, 0x22, 0x16, 0x2A, 0x3B, 0x20, 0x50, 0x0C, -0x15, 0x0F, 0x0C, 0x1B, 0x01, 0xCC, 0x4B, 0x4B, 0xE0, 0x42, -0x0A, 0x12, 0x0C, 0x23, 0x46, 0x37, 0xFD, 0x8A, 0x02, 0x6C, -0x21, 0x28, 0x11, 0x09, 0x00, 0x03, 0x00, 0x23, 0xFF, 0x1A, -0x01, 0xF4, 0x01, 0xD6, 0x00, 0x13, 0x00, 0x23, 0x00, 0x34, -0x00, 0x00, 0x17, 0x14, 0x16, 0x16, 0x33, 0x32, 0x36, 0x36, -0x35, 0x11, 0x23, 0x11, 0x14, 0x06, 0x06, 0x23, 0x22, 0x26, -0x26, 0x35, 0x03, 0x14, 0x16, 0x16, 0x33, 0x32, 0x36, 0x36, -0x35, 0x34, 0x26, 0x26, 0x23, 0x22, 0x06, 0x06, 0x17, 0x34, -0x36, 0x36, 0x33, 0x32, 0x1E, 0x02, 0x15, 0x14, 0x06, 0x06, -0x23, 0x22, 0x26, 0x26, 0x2C, 0x3A, 0x67, 0x43, 0x3A, 0x68, -0x42, 0x50, 0x2D, 0x44, 0x23, 0x2D, 0x42, 0x24, 0x5A, 0x3E, -0x68, 0x3E, 0x3A, 0x56, 0x31, 0x31, 0x56, 0x3A, 0x3E, 0x68, -0x3E, 0x51, 0x2B, 0x48, 0x2A, 0x1A, 0x34, 0x2B, 0x1A, 0x2C, -0x44, 0x23, 0x2A, 0x48, 0x2B, 0x1E, 0x3B, 0x5B, 0x32, 0x36, -0x6B, 0x4F, 0x01, 0xC2, 0xFE, 0x3E, 0x3A, 0x49, 0x22, 0x23, -0x39, 0x21, 0x01, 0x04, 0x4A, 0x6C, 0x3A, 0x3A, 0x6C, 0x4A, -0x4B, 0x6B, 0x3A, 0x3A, 0x6B, 0x4B, 0x35, 0x4A, 0x26, 0x16, -0x2A, 0x3D, 0x28, 0x35, 0x4A, 0x26, 0x26, 0x4A, 0x00, 0x02, -0x00, 0x4B, 0x00, 0x00, 0x01, 0xC2, 0x03, 0x0C, 0x00, 0x03, -0x00, 0x16, 0x00, 0x00, 0x13, 0x23, 0x11, 0x33, 0x13, 0x11, -0x33, 0x11, 0x34, 0x26, 0x26, 0x23, 0x22, 0x06, 0x06, 0x15, -0x33, 0x34, 0x36, 0x36, 0x33, 0x32, 0x16, 0x9B, 0x50, 0x50, -0xD7, 0x50, 0x27, 0x47, 0x30, 0x30, 0x49, 0x2A, 0x1A, 0x1D, -0x35, 0x23, 0x31, 0x31, 0x03, 0x0C, 0xFC, 0xF4, 0x01, 0x18, -0xFE, 0xE8, 0x01, 0x22, 0x3C, 0x50, 0x28, 0x32, 0x55, 0x37, -0x21, 0x34, 0x1E, 0x3A, 0x00, 0x02, 0x00, 0x4B, 0x00, 0x00, -0x00, 0xB9, 0x02, 0xC1, 0x00, 0x0B, 0x00, 0x0F, 0x00, 0x00, -0x13, 0x14, 0x16, 0x33, 0x32, 0x36, 0x35, 0x34, 0x26, 0x23, -0x22, 0x06, 0x17, 0x11, 0x33, 0x11, 0x4B, 0x21, 0x16, 0x17, -0x20, 0x20, 0x17, 0x16, 0x21, 0x0F, 0x50, 0x02, 0x8A, 0x16, -0x21, 0x21, 0x16, 0x17, 0x20, 0x20, 0xD5, 0xFE, 0x34, 0x01, -0xCC, 0x00, 0x02, 0xFF, 0xC3, 0xFF, 0x24, 0x00, 0xAA, 0x02, -0xC0, 0x00, 0x0B, 0x00, 0x1E, 0x00, 0x00, 0x13, 0x14, 0x16, -0x33, 0x32, 0x36, 0x35, 0x34, 0x26, 0x23, 0x22, 0x06, 0x03, -0x07, 0x1E, 0x02, 0x33, 0x32, 0x36, 0x36, 0x35, 0x11, 0x23, -0x11, 0x14, 0x06, 0x06, 0x23, 0x22, 0x26, 0x3E, 0x20, 0x16, -0x17, 0x1F, 0x1F, 0x17, 0x16, 0x20, 0x4F, 0x2C, 0x0A, 0x1A, -0x20, 0x14, 0x2A, 0x3C, 0x1F, 0x50, 0x0A, 0x15, 0x11, 0x10, -0x19, 0x02, 0x8A, 0x16, 0x20, 0x20, 0x16, 0x17, 0x1F, 0x1F, -0xFC, 0xED, 0x42, 0x0D, 0x12, 0x09, 0x26, 0x48, 0x32, 0x02, -0x08, 0xFE, 0x02, 0x20, 0x28, 0x12, 0x0E, 0x00, 0x02, 0x00, -0x46, 0x00, 0x00, 0x01, 0xB8, 0x03, 0x0C, 0x00, 0x03, 0x00, -0x09, 0x00, 0x00, 0x13, 0x11, 0x33, 0x11, 0x13, 0x07, 0x13, -0x33, 0x03, 0x37, 0x46, 0x50, 0xAA, 0xC8, 0xDC, 0x64, 0xDC, -0xC8, 0x03, 0x0C, 0xFC, 0xF4, 0x03, 0x0C, 0xFE, 0xC0, 0xBE, -0xFE, 0xF2, 0x01, 0x0E, 0xBE, 0x00, 0x01, 0x00, 0x4B, 0x00, -0x00, 0x00, 0x9B, 0x03, 0x0C, 0x00, 0x03, 0x00, 0x00, 0x13, -0x11, 0x33, 0x11, 0x4B, 0x50, 0x03, 0x0C, 0xFC, 0xF4, 0x03, -0x0C, 0x00, 0x01, 0x00, 0x4B, 0x00, 0x00, 0x02, 0xC1, 0x01, -0xD6, 0x00, 0x25, 0x00, 0x00, 0x01, 0x34, 0x26, 0x26, 0x23, -0x22, 0x06, 0x07, 0x26, 0x26, 0x23, 0x22, 0x06, 0x07, 0x35, -0x23, 0x11, 0x33, 0x11, 0x34, 0x36, 0x36, 0x33, 0x32, 0x16, -0x15, 0x11, 0x33, 0x11, 0x34, 0x36, 0x36, 0x33, 0x32, 0x16, -0x15, 0x11, 0x33, 0x02, 0xC1, 0x23, 0x42, 0x2F, 0x2E, 0x48, -0x18, 0x10, 0x45, 0x30, 0x2B, 0x3F, 0x15, 0x50, 0x50, 0x1A, -0x30, 0x21, 0x2D, 0x2B, 0x50, 0x1A, 0x30, 0x21, 0x2D, 0x2B, -0x50, 0x01, 0x22, 0x39, 0x51, 0x2A, 0x2B, 0x2B, 0x29, 0x2D, -0x26, 0x27, 0x43, 0xFE, 0x34, 0x01, 0x18, 0x24, 0x34, 0x1B, -0x38, 0x3B, 0xFE, 0xE8, 0x01, 0x18, 0x24, 0x34, 0x1B, 0x38, -0x3B, 0xFE, 0xE8, 0x00, 0x01, 0x00, 0x4B, 0x00, 0x00, 0x01, -0xC2, 0x01, 0xD6, 0x00, 0x14, 0x00, 0x00, 0x01, 0x11, 0x33, -0x11, 0x34, 0x26, 0x23, 0x22, 0x06, 0x07, 0x35, 0x23, 0x11, -0x33, 0x11, 0x34, 0x36, 0x36, 0x33, 0x32, 0x16, 0x01, 0x72, -0x50, 0x55, 0x49, 0x2D, 0x45, 0x17, 0x50, 0x50, 0x1D, 0x35, -0x23, 0x30, 0x32, 0x01, 0x18, 0xFE, 0xE8, 0x01, 0x22, 0x53, -0x61, 0x28, 0x2A, 0x48, 0xFE, 0x34, 0x01, 0x18, 0x22, 0x34, -0x1D, 0x3A, 0x00, 0x02, 0x00, 0x23, 0xFF, 0xF6, 0x01, 0xFF, -0x01, 0xD6, 0x00, 0x0F, 0x00, 0x1F, 0x00, 0x00, 0x37, 0x14, -0x16, 0x16, 0x33, 0x32, 0x36, 0x36, 0x35, 0x34, 0x26, 0x26, -0x23, 0x22, 0x06, 0x06, 0x17, 0x34, 0x36, 0x36, 0x33, 0x32, -0x16, 0x16, 0x15, 0x14, 0x06, 0x06, 0x23, 0x22, 0x26, 0x26, -0x23, 0x3F, 0x6C, 0x43, 0x44, 0x6B, 0x3F, 0x3F, 0x6B, 0x44, -0x43, 0x6C, 0x3F, 0x51, 0x29, 0x47, 0x2D, 0x2D, 0x47, 0x29, -0x29, 0x47, 0x2D, 0x2D, 0x47, 0x29, 0xE6, 0x45, 0x6D, 0x3E, -0x3E, 0x6D, 0x45, 0x46, 0x6C, 0x3E, 0x3E, 0x6C, 0x46, 0x30, -0x4B, 0x2A, 0x2A, 0x4B, 0x30, 0x30, 0x4A, 0x2B, 0x2B, 0x4A, -0x00, 0x03, 0x00, 0x46, 0xFF, 0x24, 0x02, 0x17, 0x01, 0xD6, -0x00, 0x03, 0x00, 0x13, 0x00, 0x23, 0x00, 0x00, 0x17, 0x11, -0x23, 0x11, 0x01, 0x34, 0x26, 0x26, 0x23, 0x22, 0x06, 0x06, -0x15, 0x14, 0x16, 0x16, 0x33, 0x32, 0x36, 0x36, 0x27, 0x14, -0x06, 0x06, 0x23, 0x22, 0x26, 0x26, 0x35, 0x34, 0x36, 0x36, -0x33, 0x32, 0x16, 0x16, 0x96, 0x50, 0x01, 0xD1, 0x3E, 0x67, -0x3F, 0x39, 0x57, 0x31, 0x31, 0x57, 0x39, 0x3F, 0x67, 0x3E, -0x51, 0x2B, 0x47, 0x2B, 0x23, 0x44, 0x2C, 0x2C, 0x44, 0x23, -0x2B, 0x47, 0x2B, 0xDC, 0x02, 0xA8, 0xFD, 0x58, 0x01, 0xC2, -0x4B, 0x6B, 0x3A, 0x3A, 0x6B, 0x4B, 0x4A, 0x6C, 0x3A, 0x3A, -0x6C, 0x4A, 0x35, 0x4A, 0x26, 0x26, 0x4A, 0x35, 0x35, 0x4A, -0x26, 0x26, 0x4A, 0x00, 0x03, 0x00, 0x23, 0xFF, 0x24, 0x01, -0xF4, 0x01, 0xD6, 0x00, 0x03, 0x00, 0x13, 0x00, 0x23, 0x00, -0x00, 0x05, 0x33, 0x11, 0x23, 0x05, 0x14, 0x16, 0x16, 0x33, -0x32, 0x36, 0x36, 0x35, 0x34, 0x26, 0x26, 0x23, 0x22, 0x06, -0x06, 0x17, 0x34, 0x36, 0x36, 0x33, 0x32, 0x16, 0x16, 0x15, -0x14, 0x06, 0x06, 0x23, 0x22, 0x26, 0x26, 0x01, 0xA4, 0x50, -0x50, 0xFE, 0x7F, 0x3E, 0x68, 0x3E, 0x3A, 0x56, 0x31, 0x31, -0x56, 0x3A, 0x3E, 0x68, 0x3E, 0x51, 0x2B, 0x48, 0x2A, 0x23, -0x44, 0x2C, 0x2C, 0x44, 0x23, 0x2A, 0x48, 0x2B, 0xDC, 0x02, -0xA8, 0xE6, 0x4A, 0x6C, 0x3A, 0x3A, 0x6C, 0x4A, 0x4B, 0x6B, -0x3A, 0x3A, 0x6B, 0x4B, 0x35, 0x4A, 0x26, 0x26, 0x4A, 0x35, -0x35, 0x4A, 0x26, 0x26, 0x4A, 0x00, 0x02, 0x00, 0x4B, 0x00, -0x00, 0x01, 0x52, 0x01, 0xD6, 0x00, 0x03, 0x00, 0x13, 0x00, -0x00, 0x13, 0x23, 0x11, 0x33, 0x13, 0x37, 0x26, 0x26, 0x23, -0x22, 0x06, 0x06, 0x15, 0x33, 0x34, 0x36, 0x36, 0x33, 0x32, -0x16, 0x9B, 0x50, 0x50, 0x8B, 0x2C, 0x12, 0x2B, 0x19, 0x20, -0x3E, 0x27, 0x24, 0x0D, 0x22, 0x1E, 0x14, 0x1A, 0x01, 0xCC, -0xFE, 0x34, 0x01, 0x74, 0x42, 0x13, 0x0D, 0x32, 0x55, 0x37, -0x21, 0x34, 0x1E, 0x0B, 0x00, 0x01, 0x00, 0x1F, 0xFF, 0xF6, -0x01, 0x75, 0x01, 0xD6, 0x00, 0x2D, 0x00, 0x00, 0x37, 0x07, -0x1E, 0x02, 0x33, 0x32, 0x36, 0x35, 0x34, 0x26, 0x26, 0x27, -0x2E, 0x02, 0x35, 0x34, 0x36, 0x33, 0x32, 0x16, 0x17, 0x37, -0x2E, 0x02, 0x23, 0x22, 0x06, 0x06, 0x15, 0x14, 0x16, 0x16, -0x17, 0x1E, 0x02, 0x15, 0x14, 0x06, 0x23, 0x22, 0x26, 0x26, -0x5E, 0x3F, 0x0D, 0x33, 0x4A, 0x2E, 0x47, 0x57, 0x27, 0x42, -0x28, 0x18, 0x2C, 0x1C, 0x24, 0x19, 0x24, 0x39, 0x13, 0x40, -0x0E, 0x32, 0x41, 0x25, 0x25, 0x45, 0x2B, 0x29, 0x3D, 0x1E, -0x1B, 0x30, 0x1F, 0x27, 0x22, 0x1C, 0x2F, 0x24, 0x8D, 0x27, -0x1B, 0x34, 0x21, 0x50, 0x3C, 0x29, 0x34, 0x25, 0x10, 0x0A, -0x16, 0x1C, 0x12, 0x17, 0x16, 0x22, 0x1A, 0x29, 0x1A, 0x28, -0x18, 0x1A, 0x36, 0x29, 0x28, 0x35, 0x21, 0x0C, 0x0A, 0x17, -0x21, 0x18, 0x1B, 0x1F, 0x15, 0x24, 0x00, 0x02, 0x00, 0x05, -0x00, 0x00, 0x00, 0xEB, 0x02, 0x6C, 0x00, 0x03, 0x00, 0x07, -0x00, 0x00, 0x13, 0x15, 0x33, 0x35, 0x27, 0x11, 0x33, 0x11, -0x05, 0xE6, 0x9B, 0x50, 0x01, 0xCC, 0x4B, 0x4B, 0xA0, 0xFD, -0x94, 0x02, 0x6C, 0x00, 0x01, 0x00, 0x4B, 0xFF, 0xF6, 0x01, -0xC2, 0x01, 0xCC, 0x00, 0x14, 0x00, 0x00, 0x37, 0x11, 0x23, -0x11, 0x14, 0x16, 0x33, 0x32, 0x36, 0x37, 0x15, 0x33, 0x11, -0x23, 0x11, 0x14, 0x06, 0x06, 0x23, 0x22, 0x26, 0x9B, 0x50, -0x56, 0x48, 0x2E, 0x44, 0x17, 0x50, 0x50, 0x1D, 0x35, 0x23, -0x30, 0x32, 0xB4, 0x01, 0x18, 0xFE, 0xDE, 0x52, 0x62, 0x29, -0x29, 0x48, 0x01, 0xCC, 0xFE, 0xE8, 0x22, 0x34, 0x1D, 0x3C, -0x00, 0x01, 0x00, 0x00, 0xFF, 0xDD, 0x01, 0xB8, 0x01, 0xCC, -0x00, 0x05, 0x00, 0x00, 0x11, 0x13, 0x13, 0x23, 0x03, 0x03, -0xDC, 0xDC, 0x5A, 0x82, 0x82, 0x01, 0xCC, 0xFE, 0x11, 0x01, -0xEF, 0xFE, 0xBB, 0x01, 0x45, 0x00, 0x01, 0x00, 0x05, 0xFF, -0xDD, 0x02, 0xA3, 0x01, 0xEF, 0x00, 0x09, 0x00, 0x00, 0x1B, -0x04, 0x23, 0x0B, 0x03, 0x05, 0xC8, 0x83, 0x8B, 0xC8, 0x5A, -0x74, 0x86, 0x7D, 0x73, 0x01, 0xCC, 0xFE, 0x11, 0x01, 0x4F, -0xFE, 0xB1, 0x01, 0xEF, 0xFE, 0xD3, 0x01, 0x50, 0xFE, 0xB2, -0x01, 0x2B, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0xA4, -0x01, 0xCC, 0x00, 0x0B, 0x00, 0x00, 0x01, 0x07, 0x27, 0x23, -0x17, 0x07, 0x33, 0x37, 0x17, 0x33, 0x27, 0x37, 0x01, 0x40, -0x6E, 0x6E, 0x5A, 0x9D, 0xA7, 0x5A, 0x78, 0x78, 0x5A, 0xA7, -0x9D, 0x01, 0xCC, 0xA0, 0xA0, 0xDF, 0xED, 0xAE, 0xAE, 0xED, -0xDF, 0x00, 0x01, 0x00, 0x00, 0xFF, 0x24, 0x01, 0xB8, 0x01, -0xCC, 0x00, 0x08, 0x00, 0x00, 0x01, 0x23, 0x03, 0x17, 0x03, -0x23, 0x13, 0x03, 0x33, 0x01, 0xB8, 0x5A, 0x8E, 0x21, 0x92, -0x5F, 0xB0, 0x7E, 0x5A, 0x01, 0xCC, 0xFE, 0x9C, 0x04, 0x01, -0x68, 0xFE, 0x75, 0xFE, 0xE3, 0x00, 0x01, 0x00, 0x0F, 0x00, -0x00, 0x01, 0xBD, 0x01, 0xCC, 0x00, 0x07, 0x00, 0x00, 0x01, -0x01, 0x21, 0x35, 0x21, 0x01, 0x21, 0x15, 0x01, 0x2C, 0xFE, -0xE3, 0x01, 0x9A, 0xFE, 0xF7, 0x01, 0x1D, 0xFE, 0x70, 0x01, -0x81, 0xFE, 0x7F, 0x4B, 0x01, 0x81, 0x4B, 0x00, 0x01, 0x00, -0x32, 0xFF, 0x38, 0x01, 0x05, 0x03, 0x0C, 0x00, 0x29, 0x00, -0x00, 0x13, 0x34, 0x36, 0x33, 0x33, 0x35, 0x23, 0x22, 0x06, -0x06, 0x15, 0x15, 0x14, 0x06, 0x07, 0x15, 0x16, 0x16, 0x15, -0x15, 0x14, 0x16, 0x16, 0x33, 0x33, 0x35, 0x23, 0x22, 0x26, -0x35, 0x35, 0x34, 0x2E, 0x02, 0x23, 0x15, 0x32, 0x3E, 0x02, -0x35, 0xB2, 0x1A, 0x16, 0x23, 0x3C, 0x16, 0x30, 0x21, 0x19, -0x17, 0x17, 0x19, 0x21, 0x30, 0x16, 0x3C, 0x23, 0x16, 0x1A, -0x05, 0x15, 0x2E, 0x29, 0x29, 0x2E, 0x15, 0x05, 0x02, 0x94, -0x14, 0x1E, 0x46, 0x1D, 0x3D, 0x32, 0xD2, 0x39, 0x2D, 0x08, -0x3C, 0x07, 0x2E, 0x39, 0xD2, 0x31, 0x3E, 0x1D, 0x46, 0x1F, -0x13, 0xAA, 0x21, 0x4A, 0x3F, 0x28, 0x14, 0x28, 0x3F, 0x4A, -0x21, 0x00, 0x01, 0x00, 0x64, 0xFF, 0x29, 0x00, 0xAE, 0x03, -0x11, 0x00, 0x03, 0x00, 0x00, 0x13, 0x11, 0x33, 0x11, 0x64, -0x4A, 0x03, 0x11, 0xFC, 0x18, 0x03, 0xE8, 0x00, 0x01, 0x00, -0x3B, 0xFF, 0x38, 0x01, 0x0E, 0x03, 0x0C, 0x00, 0x29, 0x00, -0x00, 0x17, 0x14, 0x06, 0x23, 0x23, 0x15, 0x33, 0x32, 0x36, -0x36, 0x35, 0x35, 0x34, 0x36, 0x37, 0x35, 0x26, 0x26, 0x35, -0x35, 0x34, 0x26, 0x26, 0x23, 0x23, 0x15, 0x33, 0x32, 0x16, -0x15, 0x15, 0x14, 0x1E, 0x02, 0x33, 0x35, 0x22, 0x0E, 0x02, -0x15, 0x8E, 0x19, 0x17, 0x23, 0x3C, 0x16, 0x30, 0x21, 0x1A, -0x16, 0x16, 0x1A, 0x21, 0x30, 0x16, 0x3C, 0x23, 0x17, 0x19, -0x05, 0x15, 0x2E, 0x29, 0x29, 0x2E, 0x15, 0x05, 0x50, 0x13, -0x1F, 0x46, 0x1D, 0x3E, 0x31, 0xD2, 0x39, 0x2E, 0x07, 0x3C, -0x08, 0x2D, 0x39, 0xD2, 0x32, 0x3D, 0x1D, 0x46, 0x1E, 0x14, -0xAA, 0x21, 0x4A, 0x3F, 0x28, 0x14, 0x28, 0x3F, 0x4A, 0x21, -0x00, 0x01, 0x00, 0x41, 0x00, 0xAC, 0x01, 0xFE, 0x01, 0x47, -0x00, 0x2C, 0x00, 0x00, 0x37, 0x26, 0x26, 0x35, 0x34, 0x36, -0x33, 0x32, 0x16, 0x16, 0x17, 0x16, 0x16, 0x33, 0x32, 0x36, -0x37, 0x36, 0x36, 0x35, 0x34, 0x26, 0x27, 0x27, 0x16, 0x16, -0x15, 0x14, 0x06, 0x23, 0x22, 0x26, 0x27, 0x2E, 0x02, 0x23, -0x22, 0x06, 0x07, 0x06, 0x06, 0x15, 0x14, 0x17, 0x8E, 0x02, -0x02, 0x1C, 0x1B, 0x0D, 0x1C, 0x1E, 0x0F, 0x20, 0x3A, 0x19, -0x1B, 0x30, 0x13, 0x0D, 0x09, 0x02, 0x02, 0x4F, 0x03, 0x02, -0x14, 0x16, 0x0D, 0x25, 0x17, 0x15, 0x2A, 0x28, 0x14, 0x1F, -0x36, 0x14, 0x0D, 0x0B, 0x03, 0xB0, 0x06, 0x0B, 0x05, 0x1A, -0x1D, 0x07, 0x0E, 0x0C, 0x18, 0x18, 0x16, 0x19, 0x11, 0x27, -0x0F, 0x08, 0x0E, 0x06, 0x05, 0x07, 0x0E, 0x06, 0x18, 0x19, -0x0F, 0x11, 0x10, 0x15, 0x0B, 0x1A, 0x1A, 0x12, 0x24, 0x10, -0x0D, 0x0B, 0x00, 0x03, 0x00, 0x51, 0xFF, 0x90, 0x01, 0xDF, -0x02, 0x42, 0x00, 0x03, 0x00, 0x21, 0x00, 0x25, 0x00, 0x00, -0x25, 0x15, 0x33, 0x35, 0x27, 0x34, 0x36, 0x36, 0x33, 0x32, -0x16, 0x17, 0x35, 0x26, 0x26, 0x23, 0x22, 0x06, 0x06, 0x15, -0x14, 0x16, 0x16, 0x33, 0x32, 0x36, 0x37, 0x35, 0x06, 0x06, -0x23, 0x22, 0x26, 0x26, 0x13, 0x15, 0x33, 0x35, 0x01, 0x18, -0x40, 0xB6, 0x29, 0x47, 0x2D, 0x38, 0x58, 0x10, 0x18, 0x59, -0x2F, 0x43, 0x6C, 0x3F, 0x3F, 0x6C, 0x43, 0x2F, 0x59, 0x18, -0x10, 0x58, 0x38, 0x2D, 0x47, 0x29, 0x76, 0x40, 0x26, 0x96, -0x96, 0xC0, 0x30, 0x4B, 0x2A, 0x32, 0x22, 0x64, 0x1C, 0x1F, -0x3E, 0x6C, 0x46, 0x45, 0x6D, 0x3E, 0x1F, 0x1C, 0x64, 0x22, -0x32, 0x2B, 0x4A, 0x01, 0x8C, 0x96, 0x96, 0x00, 0x02, 0x00, -0x40, 0x00, 0x00, 0x02, 0x09, 0x02, 0xC6, 0x00, 0x03, 0x00, -0x2F, 0x00, 0x00, 0x13, 0x15, 0x21, 0x35, 0x37, 0x37, 0x2E, -0x03, 0x23, 0x22, 0x06, 0x06, 0x15, 0x14, 0x1E, 0x03, 0x15, -0x14, 0x06, 0x06, 0x07, 0x21, 0x35, 0x21, 0x07, 0x3E, 0x03, -0x35, 0x34, 0x2E, 0x03, 0x35, 0x34, 0x36, 0x36, 0x33, 0x32, -0x1E, 0x02, 0x43, 0x01, 0x68, 0x17, 0x47, 0x02, 0x1A, 0x30, -0x4A, 0x32, 0x3B, 0x5D, 0x35, 0x14, 0x1F, 0x1E, 0x14, 0x24, -0x44, 0x31, 0x01, 0xC5, 0xFE, 0x95, 0x02, 0x1C, 0x34, 0x2A, -0x18, 0x13, 0x1C, 0x1D, 0x13, 0x22, 0x36, 0x1E, 0x20, 0x30, -0x1F, 0x11, 0x01, 0x72, 0x4B, 0x4B, 0x6D, 0x1D, 0x1E, 0x46, -0x3E, 0x28, 0x2E, 0x56, 0x3C, 0x29, 0x3D, 0x31, 0x2C, 0x2E, -0x1C, 0x2C, 0x4B, 0x4F, 0x33, 0x50, 0x1E, 0x08, 0x26, 0x39, -0x47, 0x28, 0x22, 0x32, 0x2A, 0x2B, 0x34, 0x23, 0x26, 0x2F, -0x15, 0x19, 0x2A, 0x35, 0x00, 0x03, 0x00, 0x0F, 0x00, 0x00, -0x02, 0x35, 0x02, 0xBC, 0x00, 0x03, 0x00, 0x07, 0x00, 0x10, -0x00, 0x00, 0x13, 0x21, 0x35, 0x21, 0x15, 0x21, 0x35, 0x21, -0x01, 0x03, 0x03, 0x23, 0x13, 0x11, 0x33, 0x11, 0x13, 0x2D, -0x01, 0xE5, 0xFE, 0x1B, 0x01, 0xE5, 0xFE, 0x1B, 0x01, 0xAA, -0xB5, 0xB4, 0x5F, 0xE9, 0x56, 0xE7, 0x01, 0x22, 0x4B, 0xE1, -0x4B, 0x01, 0xE5, 0xFE, 0xBC, 0x01, 0x44, 0xFE, 0x73, 0xFE, -0xD1, 0x01, 0x32, 0x01, 0x8A, 0x00, 0x02, 0x00, 0x1E, 0xFF, -0xF6, 0x01, 0x7A, 0x02, 0xC6, 0x00, 0x2C, 0x00, 0x59, 0x00, -0x00, 0x01, 0x37, 0x2E, 0x02, 0x23, 0x22, 0x06, 0x06, 0x15, -0x14, 0x1E, 0x02, 0x17, 0x1E, 0x02, 0x15, 0x14, 0x06, 0x06, -0x23, 0x15, 0x32, 0x3E, 0x02, 0x35, 0x34, 0x26, 0x26, 0x27, -0x2E, 0x03, 0x35, 0x34, 0x36, 0x36, 0x33, 0x32, 0x16, 0x16, -0x03, 0x07, 0x1E, 0x02, 0x33, 0x32, 0x3E, 0x02, 0x35, 0x34, -0x26, 0x26, 0x27, 0x2E, 0x03, 0x35, 0x34, 0x36, 0x36, 0x37, -0x35, 0x22, 0x0E, 0x02, 0x15, 0x14, 0x16, 0x16, 0x17, 0x1E, -0x02, 0x15, 0x14, 0x06, 0x06, 0x23, 0x22, 0x26, 0x26, 0x01, -0x36, 0x40, 0x09, 0x33, 0x45, 0x25, 0x25, 0x44, 0x2C, 0x17, -0x27, 0x2E, 0x18, 0x1B, 0x2F, 0x1E, 0x16, 0x26, 0x19, 0x25, -0x3E, 0x2D, 0x18, 0x24, 0x3A, 0x21, 0x13, 0x28, 0x21, 0x14, -0x12, 0x20, 0x14, 0x13, 0x27, 0x21, 0xCC, 0x42, 0x07, 0x35, -0x4E, 0x2A, 0x25, 0x3E, 0x2D, 0x18, 0x24, 0x3A, 0x21, 0x13, -0x28, 0x21, 0x14, 0x13, 0x28, 0x1E, 0x2B, 0x3F, 0x2A, 0x15, -0x28, 0x3D, 0x1F, 0x1B, 0x2F, 0x1E, 0x13, 0x24, 0x19, 0x1C, -0x32, 0x23, 0x02, 0x43, 0x29, 0x1A, 0x28, 0x18, 0x1D, 0x36, -0x26, 0x1D, 0x2A, 0x1F, 0x17, 0x09, 0x0A, 0x18, 0x21, 0x19, -0x14, 0x29, 0x1B, 0x23, 0x0D, 0x1E, 0x32, 0x25, 0x22, 0x33, -0x26, 0x0D, 0x08, 0x12, 0x14, 0x18, 0x0D, 0x10, 0x13, 0x0A, -0x0C, 0x18, 0xFE, 0x4B, 0x26, 0x25, 0x3D, 0x23, 0x10, 0x22, -0x35, 0x25, 0x22, 0x33, 0x26, 0x0D, 0x08, 0x10, 0x15, 0x21, -0x19, 0x15, 0x1E, 0x18, 0x0A, 0x1A, 0x10, 0x20, 0x2C, 0x1D, -0x26, 0x38, 0x25, 0x0D, 0x0A, 0x18, 0x21, 0x19, 0x14, 0x1A, -0x0C, 0x19, 0x2B, 0x00, 0x02, 0x00, 0x14, 0x01, 0xE0, 0x01, -0x02, 0x02, 0xD0, 0x00, 0x0B, 0x00, 0x17, 0x00, 0x00, 0x13, -0x14, 0x16, 0x33, 0x32, 0x36, 0x35, 0x34, 0x26, 0x23, 0x22, -0x06, 0x17, 0x34, 0x36, 0x33, 0x32, 0x16, 0x15, 0x14, 0x06, -0x23, 0x22, 0x26, 0x14, 0x44, 0x33, 0x33, 0x44, 0x44, 0x33, -0x33, 0x44, 0x28, 0x2D, 0x22, 0x22, 0x2D, 0x2D, 0x22, 0x22, -0x2D, 0x02, 0x58, 0x34, 0x44, 0x44, 0x34, 0x35, 0x43, 0x43, -0x35, 0x24, 0x2E, 0x2E, 0x24, 0x24, 0x2E, 0x2E, 0x00, 0x04, -0x00, 0x05, 0x00, 0x00, 0x02, 0x8F, 0x03, 0x89, 0x00, 0x03, -0x00, 0x0D, 0x00, 0x19, 0x00, 0x25, 0x00, 0x00, 0x37, 0x21, -0x27, 0x21, 0x37, 0x13, 0x17, 0x17, 0x33, 0x01, 0x01, 0x33, -0x37, 0x37, 0x13, 0x14, 0x16, 0x33, 0x32, 0x36, 0x35, 0x34, -0x26, 0x23, 0x22, 0x06, 0x07, 0x14, 0x16, 0x33, 0x32, 0x36, -0x35, 0x34, 0x26, 0x23, 0x22, 0x06, 0x93, 0x01, 0x72, 0x1E, -0xFE, 0xCA, 0x99, 0x78, 0x08, 0x66, 0x5F, 0xFE, 0xBB, 0xFE, -0xBB, 0x5F, 0x68, 0x08, 0xA3, 0x21, 0x16, 0x17, 0x20, 0x20, -0x17, 0x16, 0x21, 0xC8, 0x21, 0x16, 0x17, 0x20, 0x20, 0x17, -0x16, 0x21, 0xD2, 0x50, 0xFC, 0xFE, 0xE6, 0x14, 0xF0, 0x02, -0xDF, 0xFD, 0x21, 0xF6, 0x12, 0x02, 0x4A, 0x16, 0x21, 0x21, -0x16, 0x17, 0x20, 0x20, 0x17, 0x16, 0x21, 0x21, 0x16, 0x17, -0x20, 0x20, 0x00, 0x04, 0x00, 0x05, 0x00, 0x00, 0x02, 0x8F, -0x03, 0x82, 0x00, 0x03, 0x00, 0x0D, 0x00, 0x19, 0x00, 0x25, -0x00, 0x00, 0x37, 0x21, 0x27, 0x21, 0x37, 0x13, 0x17, 0x17, -0x33, 0x01, 0x01, 0x33, 0x37, 0x37, 0x13, 0x14, 0x16, 0x33, -0x32, 0x36, 0x35, 0x34, 0x26, 0x23, 0x22, 0x06, 0x17, 0x34, -0x36, 0x33, 0x32, 0x16, 0x15, 0x14, 0x06, 0x23, 0x22, 0x26, -0x93, 0x01, 0x72, 0x1E, 0xFE, 0xCA, 0x99, 0x78, 0x08, 0x66, -0x5F, 0xFE, 0xBB, 0xFE, 0xBB, 0x5F, 0x68, 0x08, 0x10, 0x3F, -0x27, 0x27, 0x3F, 0x3F, 0x27, 0x27, 0x3F, 0x33, 0x20, 0x13, -0x13, 0x20, 0x20, 0x13, 0x13, 0x20, 0xD2, 0x50, 0xFC, 0xFE, -0xE6, 0x14, 0xF0, 0x02, 0xDF, 0xFD, 0x21, 0xF6, 0x12, 0x02, -0x16, 0x2D, 0x37, 0x37, 0x2D, 0x2D, 0x37, 0x37, 0x2D, 0x19, -0x1C, 0x1C, 0x19, 0x19, 0x1C, 0x1C, 0x00, 0x06, 0x00, 0x05, -0x00, 0x00, 0x03, 0x7A, 0x02, 0xBC, 0x00, 0x03, 0x00, 0x07, -0x00, 0x0B, 0x00, 0x0F, 0x00, 0x13, 0x00, 0x17, 0x00, 0x00, -0x37, 0x21, 0x35, 0x21, 0x13, 0x01, 0x33, 0x01, 0x13, 0x21, -0x35, 0x21, 0x13, 0x21, 0x35, 0x21, 0x13, 0x21, 0x35, 0x21, -0x03, 0x11, 0x33, 0x11, 0xD9, 0x01, 0x2C, 0xFE, 0xD4, 0xDA, -0xFE, 0x52, 0x5F, 0x01, 0x94, 0x21, 0x01, 0x61, 0xFE, 0x9F, -0x08, 0x01, 0x59, 0xFE, 0x39, 0x66, 0x01, 0x4D, 0xFE, 0xB3, -0x2F, 0x55, 0xD2, 0x50, 0x01, 0x9A, 0xFD, 0x44, 0x02, 0x94, -0xFD, 0x6C, 0x50, 0x02, 0x1C, 0x50, 0xFE, 0x98, 0x50, 0x01, -0x18, 0xFD, 0x44, 0x02, 0xBC, 0x00, 0x02, 0x00, 0x28, 0xFF, -0x23, 0x02, 0x67, 0x02, 0xC6, 0x00, 0x21, 0x00, 0x3F, 0x00, -0x00, 0x13, 0x34, 0x36, 0x36, 0x33, 0x32, 0x16, 0x16, 0x17, -0x35, 0x26, 0x26, 0x23, 0x22, 0x0E, 0x02, 0x15, 0x14, 0x1E, -0x02, 0x33, 0x32, 0x36, 0x37, 0x35, 0x0E, 0x02, 0x23, 0x22, -0x26, 0x26, 0x17, 0x07, 0x36, 0x36, 0x33, 0x32, 0x16, 0x15, -0x14, 0x06, 0x23, 0x22, 0x26, 0x27, 0x07, 0x16, 0x16, 0x33, -0x32, 0x36, 0x36, 0x35, 0x34, 0x26, 0x26, 0x23, 0x22, 0x22, -0x23, 0x37, 0x82, 0x49, 0x76, 0x45, 0x32, 0x53, 0x44, 0x18, -0x2C, 0x68, 0x4D, 0x49, 0x7F, 0x60, 0x36, 0x36, 0x60, 0x7F, -0x49, 0x4D, 0x68, 0x2C, 0x18, 0x44, 0x53, 0x32, 0x45, 0x76, -0x49, 0xFA, 0x56, 0x10, 0x23, 0x07, 0x1A, 0x18, 0x18, 0x1A, -0x13, 0x17, 0x0E, 0x22, 0x15, 0x29, 0x1C, 0x1C, 0x30, 0x1E, -0x16, 0x21, 0x11, 0x02, 0x05, 0x02, 0x3F, 0x01, 0x5E, 0x52, -0x7B, 0x46, 0x1A, 0x2F, 0x1F, 0x70, 0x27, 0x26, 0x35, 0x61, -0x84, 0x4E, 0x4E, 0x84, 0x61, 0x35, 0x26, 0x27, 0x70, 0x1F, -0x2F, 0x1A, 0x46, 0x7C, 0xD2, 0xA8, 0x0B, 0x07, 0x12, 0x12, -0x12, 0x16, 0x0C, 0x0C, 0x28, 0x15, 0x11, 0x16, 0x2A, 0x1E, -0x19, 0x20, 0x11, 0x70, 0x00, 0x05, 0x00, 0x50, 0x00, 0x00, -0x01, 0xE0, 0x03, 0xC0, 0x00, 0x03, 0x00, 0x07, 0x00, 0x0B, -0x00, 0x0F, 0x00, 0x13, 0x00, 0x00, 0x33, 0x21, 0x35, 0x21, -0x11, 0x21, 0x35, 0x21, 0x11, 0x21, 0x35, 0x21, 0x03, 0x11, -0x33, 0x11, 0x37, 0x27, 0x07, 0x17, 0x7F, 0x01, 0x61, 0xFE, -0x9F, 0x01, 0x61, 0xFE, 0x9F, 0x01, 0x4D, 0xFE, 0xB3, 0x2F, -0x55, 0xF5, 0x4B, 0x96, 0x2D, 0x50, 0x02, 0x1C, 0x50, 0xFE, -0x98, 0x50, 0x01, 0x18, 0xFD, 0x44, 0x02, 0xBC, 0xDC, 0x28, -0xA0, 0x1E, 0x00, 0x02, 0x00, 0x50, 0xFF, 0xDD, 0x02, 0xB7, -0x03, 0x6B, 0x00, 0x07, 0x00, 0x21, 0x00, 0x00, 0x01, 0x11, -0x01, 0x11, 0x33, 0x11, 0x01, 0x11, 0x25, 0x17, 0x34, 0x36, -0x36, 0x33, 0x32, 0x16, 0x16, 0x33, 0x32, 0x36, 0x36, 0x37, -0x27, 0x06, 0x06, 0x23, 0x22, 0x2E, 0x02, 0x23, 0x22, 0x06, -0x06, 0x02, 0x62, 0xFD, 0xEE, 0x55, 0x02, 0x12, 0xFE, 0x20, -0x35, 0x08, 0x16, 0x14, 0x17, 0x21, 0x26, 0x1B, 0x19, 0x2A, -0x1F, 0x08, 0x2E, 0x0F, 0x1B, 0x0E, 0x14, 0x1A, 0x17, 0x1F, -0x18, 0x16, 0x2D, 0x20, 0x02, 0xBC, 0xFD, 0xF7, 0x02, 0x2C, -0xFD, 0x21, 0x02, 0x09, 0xFD, 0xD4, 0x02, 0xDF, 0x5C, 0x25, -0x08, 0x18, 0x13, 0x17, 0x18, 0x15, 0x22, 0x12, 0x2B, 0x18, -0x18, 0x0E, 0x13, 0x0E, 0x15, 0x24, 0x00, 0x04, 0x00, 0x28, -0xFF, 0xF6, 0x02, 0xEE, 0x03, 0x70, 0x00, 0x0F, 0x00, 0x23, -0x00, 0x2F, 0x00, 0x3B, 0x00, 0x00, 0x13, 0x34, 0x36, 0x36, -0x33, 0x32, 0x16, 0x16, 0x15, 0x14, 0x06, 0x06, 0x23, 0x22, -0x26, 0x26, 0x27, 0x14, 0x1E, 0x02, 0x33, 0x32, 0x3E, 0x02, -0x35, 0x34, 0x2E, 0x02, 0x23, 0x22, 0x0E, 0x02, 0x01, 0x14, -0x16, 0x33, 0x32, 0x36, 0x35, 0x34, 0x26, 0x23, 0x22, 0x06, -0x07, 0x14, 0x16, 0x33, 0x32, 0x36, 0x35, 0x34, 0x26, 0x23, -0x22, 0x06, 0x82, 0x45, 0x78, 0x4C, 0x4D, 0x77, 0x45, 0x45, -0x77, 0x4D, 0x4C, 0x78, 0x45, 0x5A, 0x35, 0x60, 0x82, 0x4C, -0x4D, 0x81, 0x60, 0x35, 0x35, 0x60, 0x81, 0x4D, 0x4C, 0x82, -0x60, 0x35, 0x01, 0x90, 0x21, 0x16, 0x17, 0x20, 0x20, 0x17, -0x16, 0x21, 0xC8, 0x21, 0x16, 0x17, 0x20, 0x20, 0x17, 0x16, -0x21, 0x01, 0x5E, 0x4F, 0x7C, 0x48, 0x48, 0x7C, 0x4F, 0x4F, -0x7C, 0x48, 0x48, 0x7C, 0x4F, 0x4C, 0x84, 0x61, 0x37, 0x37, -0x61, 0x84, 0x4C, 0x4D, 0x83, 0x61, 0x37, 0x37, 0x61, 0x83, -0x01, 0x8E, 0x16, 0x21, 0x21, 0x16, 0x17, 0x20, 0x20, 0x17, -0x16, 0x21, 0x21, 0x16, 0x17, 0x20, 0x20, 0x00, 0x03, 0x00, -0x4B, 0xFF, 0xF6, 0x02, 0x27, 0x03, 0x70, 0x00, 0x15, 0x00, -0x21, 0x00, 0x2D, 0x00, 0x00, 0x13, 0x11, 0x14, 0x1E, 0x02, -0x33, 0x32, 0x3E, 0x02, 0x35, 0x11, 0x23, 0x11, 0x14, 0x06, -0x23, 0x22, 0x26, 0x35, 0x11, 0x37, 0x14, 0x16, 0x33, 0x32, -0x36, 0x35, 0x34, 0x26, 0x23, 0x22, 0x06, 0x07, 0x14, 0x16, -0x33, 0x32, 0x36, 0x35, 0x34, 0x26, 0x23, 0x22, 0x06, 0x4B, -0x21, 0x3F, 0x58, 0x36, 0x36, 0x58, 0x3F, 0x21, 0x55, 0x4E, -0x4B, 0x4B, 0x4E, 0xC6, 0x21, 0x16, 0x17, 0x20, 0x20, 0x17, -0x16, 0x21, 0xC8, 0x21, 0x16, 0x17, 0x20, 0x20, 0x17, 0x16, -0x21, 0x02, 0xBC, 0xFE, 0x2A, 0x34, 0x58, 0x40, 0x24, 0x24, -0x40, 0x58, 0x34, 0x01, 0xD6, 0xFE, 0x2A, 0x48, 0x58, 0x58, -0x48, 0x01, 0xD6, 0x7D, 0x16, 0x21, 0x21, 0x16, 0x17, 0x20, -0x20, 0x17, 0x16, 0x21, 0x21, 0x16, 0x17, 0x20, 0x20, 0x00, -0x03, 0x00, 0x1E, 0xFF, 0xF6, 0x01, 0xA9, 0x02, 0xD0, 0x00, -0x20, 0x00, 0x32, 0x00, 0x36, 0x00, 0x00, 0x37, 0x34, 0x36, -0x36, 0x33, 0x32, 0x16, 0x17, 0x35, 0x2E, 0x02, 0x23, 0x22, -0x06, 0x06, 0x15, 0x14, 0x16, 0x16, 0x33, 0x32, 0x36, 0x36, -0x35, 0x27, 0x14, 0x06, 0x06, 0x23, 0x22, 0x26, 0x26, 0x35, -0x3E, 0x02, 0x33, 0x32, 0x16, 0x15, 0x11, 0x33, 0x11, 0x34, -0x26, 0x26, 0x23, 0x22, 0x06, 0x07, 0x13, 0x17, 0x37, 0x27, -0x6F, 0x1A, 0x35, 0x28, 0x2C, 0x46, 0x22, 0x07, 0x26, 0x3F, -0x2C, 0x37, 0x59, 0x34, 0x27, 0x48, 0x2F, 0x30, 0x4E, 0x2F, -0x10, 0x23, 0x3E, 0x28, 0x20, 0x2B, 0x16, 0x0B, 0x27, 0x36, -0x21, 0x26, 0x3B, 0x50, 0x2D, 0x4F, 0x35, 0x3E, 0x58, 0x1A, -0x10, 0xB4, 0x2D, 0x96, 0x87, 0x1C, 0x28, 0x16, 0x17, 0x18, -0x2F, 0x09, 0x1D, 0x17, 0x24, 0x47, 0x36, 0x2A, 0x3C, 0x21, -0x21, 0x3F, 0x2C, 0x32, 0x2B, 0x34, 0x18, 0x11, 0x21, 0xF1, -0x08, 0x17, 0x11, 0x23, 0x23, 0xFE, 0xB6, 0x01, 0x4A, 0x2D, -0x3E, 0x21, 0x26, 0x13, 0x01, 0x0B, 0x96, 0x1E, 0xA0, 0x00, -0x03, 0x00, 0x1E, 0xFF, 0xF6, 0x01, 0xA9, 0x02, 0xD0, 0x00, -0x20, 0x00, 0x32, 0x00, 0x36, 0x00, 0x00, 0x37, 0x34, 0x36, -0x36, 0x33, 0x32, 0x16, 0x17, 0x35, 0x2E, 0x02, 0x23, 0x22, -0x06, 0x06, 0x15, 0x14, 0x16, 0x16, 0x33, 0x32, 0x36, 0x36, -0x35, 0x27, 0x14, 0x06, 0x06, 0x23, 0x22, 0x26, 0x26, 0x35, -0x3E, 0x02, 0x33, 0x32, 0x16, 0x15, 0x11, 0x33, 0x11, 0x34, -0x26, 0x26, 0x23, 0x22, 0x06, 0x07, 0x01, 0x27, 0x07, 0x17, -0x6F, 0x1A, 0x35, 0x28, 0x2C, 0x46, 0x22, 0x07, 0x26, 0x3F, -0x2C, 0x37, 0x59, 0x34, 0x27, 0x48, 0x2F, 0x30, 0x4E, 0x2F, -0x10, 0x23, 0x3E, 0x28, 0x20, 0x2B, 0x16, 0x0B, 0x27, 0x36, -0x21, 0x26, 0x3B, 0x50, 0x2D, 0x4F, 0x35, 0x3E, 0x58, 0x1A, -0x01, 0x50, 0x4B, 0x96, 0x2D, 0x87, 0x1C, 0x28, 0x16, 0x17, -0x18, 0x2F, 0x09, 0x1D, 0x17, 0x24, 0x47, 0x36, 0x2A, 0x3C, -0x21, 0x21, 0x3F, 0x2C, 0x32, 0x2B, 0x34, 0x18, 0x11, 0x21, -0xF1, 0x08, 0x17, 0x11, 0x23, 0x23, 0xFE, 0xB6, 0x01, 0x4A, -0x2D, 0x3E, 0x21, 0x26, 0x13, 0x01, 0x0B, 0x28, 0xA0, 0x1E, -0x00, 0x03, 0x00, 0x1E, 0xFF, 0xF6, 0x01, 0xA9, 0x02, 0xC6, -0x00, 0x20, 0x00, 0x32, 0x00, 0x38, 0x00, 0x00, 0x37, 0x34, -0x36, 0x36, 0x33, 0x32, 0x16, 0x17, 0x35, 0x2E, 0x02, 0x23, -0x22, 0x06, 0x06, 0x15, 0x14, 0x16, 0x16, 0x33, 0x32, 0x36, -0x36, 0x35, 0x27, 0x14, 0x06, 0x06, 0x23, 0x22, 0x26, 0x26, -0x35, 0x3E, 0x02, 0x33, 0x32, 0x16, 0x15, 0x11, 0x33, 0x11, -0x34, 0x26, 0x26, 0x23, 0x22, 0x06, 0x07, 0x37, 0x17, 0x37, -0x27, 0x07, 0x17, 0x6F, 0x1A, 0x35, 0x28, 0x2C, 0x46, 0x22, -0x07, 0x26, 0x3F, 0x2C, 0x37, 0x59, 0x34, 0x27, 0x48, 0x2F, -0x30, 0x4E, 0x2F, 0x10, 0x23, 0x3E, 0x28, 0x20, 0x2B, 0x16, -0x0B, 0x27, 0x36, 0x21, 0x26, 0x3B, 0x50, 0x2D, 0x4F, 0x35, -0x3E, 0x58, 0x1A, 0xB0, 0x6E, 0x32, 0xA0, 0xA0, 0x32, 0x87, -0x1C, 0x28, 0x16, 0x17, 0x18, 0x2F, 0x09, 0x1D, 0x17, 0x24, -0x47, 0x36, 0x2A, 0x3C, 0x21, 0x21, 0x3F, 0x2C, 0x32, 0x2B, -0x34, 0x18, 0x11, 0x21, 0xF1, 0x08, 0x17, 0x11, 0x23, 0x23, -0xFE, 0xB6, 0x01, 0x4A, 0x2D, 0x3E, 0x21, 0x26, 0x13, 0xD4, -0x5F, 0x28, 0x8C, 0x8C, 0x28, 0x00, 0x04, 0x00, 0x1E, 0xFF, -0xF6, 0x01, 0xA9, 0x02, 0x80, 0x00, 0x20, 0x00, 0x32, 0x00, -0x3E, 0x00, 0x4A, 0x00, 0x00, 0x37, 0x34, 0x36, 0x36, 0x33, -0x32, 0x16, 0x17, 0x35, 0x2E, 0x02, 0x23, 0x22, 0x06, 0x06, -0x15, 0x14, 0x16, 0x16, 0x33, 0x32, 0x36, 0x36, 0x35, 0x27, -0x14, 0x06, 0x06, 0x23, 0x22, 0x26, 0x26, 0x35, 0x3E, 0x02, -0x33, 0x32, 0x16, 0x15, 0x11, 0x33, 0x11, 0x34, 0x26, 0x26, -0x23, 0x22, 0x06, 0x07, 0x37, 0x14, 0x16, 0x33, 0x32, 0x36, -0x35, 0x34, 0x26, 0x23, 0x22, 0x06, 0x07, 0x14, 0x16, 0x33, -0x32, 0x36, 0x35, 0x34, 0x26, 0x23, 0x22, 0x06, 0x6F, 0x1A, -0x35, 0x28, 0x2C, 0x46, 0x22, 0x07, 0x26, 0x3F, 0x2C, 0x37, -0x59, 0x34, 0x27, 0x48, 0x2F, 0x30, 0x4E, 0x2F, 0x10, 0x23, -0x3E, 0x28, 0x20, 0x2B, 0x16, 0x0B, 0x27, 0x36, 0x21, 0x26, -0x3B, 0x50, 0x2D, 0x4F, 0x35, 0x3E, 0x58, 0x1A, 0xDD, 0x21, -0x16, 0x17, 0x20, 0x20, 0x17, 0x16, 0x21, 0xC8, 0x21, 0x16, -0x17, 0x20, 0x20, 0x17, 0x16, 0x21, 0x87, 0x1C, 0x28, 0x16, -0x17, 0x18, 0x2F, 0x09, 0x1D, 0x17, 0x24, 0x47, 0x36, 0x2A, -0x3C, 0x21, 0x21, 0x3F, 0x2C, 0x32, 0x2B, 0x34, 0x18, 0x11, -0x21, 0xF1, 0x08, 0x17, 0x11, 0x23, 0x23, 0xFE, 0xB6, 0x01, -0x4A, 0x2D, 0x3E, 0x21, 0x26, 0x13, 0xAC, 0x16, 0x21, 0x21, -0x16, 0x17, 0x20, 0x20, 0x17, 0x16, 0x21, 0x21, 0x16, 0x17, -0x20, 0x20, 0x00, 0x04, 0x00, 0x1E, 0xFF, 0xF6, 0x01, 0xA9, -0x02, 0xDA, 0x00, 0x20, 0x00, 0x32, 0x00, 0x3E, 0x00, 0x4A, -0x00, 0x00, 0x37, 0x34, 0x36, 0x36, 0x33, 0x32, 0x16, 0x17, -0x35, 0x2E, 0x02, 0x23, 0x22, 0x06, 0x06, 0x15, 0x14, 0x16, -0x16, 0x33, 0x32, 0x36, 0x36, 0x35, 0x27, 0x14, 0x06, 0x06, -0x23, 0x22, 0x26, 0x26, 0x35, 0x3E, 0x02, 0x33, 0x32, 0x16, -0x15, 0x11, 0x33, 0x11, 0x34, 0x26, 0x26, 0x23, 0x22, 0x06, -0x07, 0x37, 0x14, 0x16, 0x33, 0x32, 0x36, 0x35, 0x34, 0x26, -0x23, 0x22, 0x06, 0x17, 0x34, 0x36, 0x33, 0x32, 0x16, 0x15, -0x14, 0x06, 0x23, 0x22, 0x26, 0x6F, 0x1A, 0x35, 0x28, 0x2C, -0x46, 0x22, 0x07, 0x26, 0x3F, 0x2C, 0x37, 0x59, 0x34, 0x27, -0x48, 0x2F, 0x30, 0x4E, 0x2F, 0x10, 0x23, 0x3E, 0x28, 0x20, -0x2B, 0x16, 0x0B, 0x27, 0x36, 0x21, 0x26, 0x3B, 0x50, 0x2D, -0x4F, 0x35, 0x3E, 0x58, 0x1A, 0x4A, 0x3F, 0x27, 0x27, 0x3F, -0x3F, 0x27, 0x27, 0x3F, 0x33, 0x20, 0x13, 0x13, 0x20, 0x20, -0x13, 0x13, 0x20, 0x87, 0x1C, 0x28, 0x16, 0x17, 0x18, 0x2F, -0x09, 0x1D, 0x17, 0x24, 0x47, 0x36, 0x2A, 0x3C, 0x21, 0x21, -0x3F, 0x2C, 0x32, 0x2B, 0x34, 0x18, 0x11, 0x21, 0xF1, 0x08, -0x17, 0x11, 0x23, 0x23, 0xFE, 0xB6, 0x01, 0x4A, 0x2D, 0x3E, -0x21, 0x26, 0x13, 0xD9, 0x2D, 0x37, 0x37, 0x2D, 0x2D, 0x37, -0x37, 0x2D, 0x19, 0x1C, 0x1C, 0x19, 0x19, 0x1C, 0x1C, 0x00, -0x02, 0x00, 0x1E, 0xFF, 0x23, 0x01, 0xAC, 0x01, 0xD6, 0x00, -0x1F, 0x00, 0x3D, 0x00, 0x00, 0x37, 0x34, 0x36, 0x36, 0x33, -0x32, 0x16, 0x16, 0x17, 0x35, 0x26, 0x26, 0x23, 0x22, 0x06, -0x06, 0x15, 0x14, 0x16, 0x16, 0x33, 0x32, 0x36, 0x37, 0x35, -0x0E, 0x02, 0x23, 0x22, 0x26, 0x26, 0x17, 0x07, 0x36, 0x36, -0x33, 0x32, 0x16, 0x15, 0x14, 0x06, 0x23, 0x22, 0x26, 0x27, -0x07, 0x16, 0x16, 0x33, 0x32, 0x36, 0x36, 0x35, 0x34, 0x26, -0x26, 0x23, 0x22, 0x22, 0x23, 0x37, 0x6F, 0x29, 0x47, 0x2D, -0x25, 0x41, 0x30, 0x0A, 0x18, 0x59, 0x2F, 0x43, 0x6C, 0x3F, -0x3F, 0x6C, 0x43, 0x2F, 0x59, 0x18, 0x0A, 0x30, 0x41, 0x25, -0x2D, 0x47, 0x29, 0x93, 0x56, 0x10, 0x23, 0x07, 0x1A, 0x18, -0x18, 0x1A, 0x13, 0x17, 0x0E, 0x22, 0x15, 0x29, 0x1C, 0x1C, -0x30, 0x1E, 0x16, 0x21, 0x11, 0x02, 0x05, 0x02, 0x3F, 0xE6, -0x30, 0x4B, 0x2A, 0x17, 0x26, 0x17, 0x64, 0x1C, 0x1F, 0x3E, -0x6C, 0x46, 0x45, 0x6D, 0x3E, 0x1F, 0x1C, 0x64, 0x16, 0x27, -0x17, 0x2B, 0x4A, 0x7B, 0xA8, 0x0B, 0x07, 0x12, 0x12, 0x12, -0x16, 0x0C, 0x0C, 0x28, 0x15, 0x11, 0x16, 0x2A, 0x1E, 0x19, -0x20, 0x11, 0x70, 0x00, 0x02, 0x00, 0x23, 0xFF, 0xF6, 0x01, -0xE0, 0x02, 0xDA, 0x00, 0x2A, 0x00, 0x2E, 0x00, 0x00, 0x37, -0x21, 0x36, 0x34, 0x35, 0x34, 0x26, 0x26, 0x23, 0x22, 0x06, -0x06, 0x07, 0x06, 0x06, 0x15, 0x14, 0x16, 0x16, 0x33, 0x32, -0x36, 0x36, 0x37, 0x27, 0x0E, 0x02, 0x23, 0x22, 0x26, 0x26, -0x35, 0x37, 0x34, 0x36, 0x36, 0x33, 0x32, 0x16, 0x16, 0x17, -0x21, 0x13, 0x17, 0x37, 0x27, 0x56, 0x01, 0x89, 0x01, 0x36, -0x62, 0x41, 0x3B, 0x5F, 0x3C, 0x0A, 0x02, 0x02, 0x39, 0x63, -0x3E, 0x3C, 0x50, 0x38, 0x17, 0x41, 0x0F, 0x2A, 0x35, 0x22, -0x2D, 0x42, 0x24, 0x02, 0x27, 0x42, 0x28, 0x28, 0x37, 0x1F, -0x04, 0xFE, 0xCD, 0x25, 0xB4, 0x2D, 0x96, 0xD2, 0x06, 0x0C, -0x06, 0x49, 0x69, 0x3A, 0x30, 0x56, 0x39, 0x0C, 0x18, 0x0D, -0x45, 0x6D, 0x3E, 0x1D, 0x35, 0x23, 0x29, 0x19, 0x25, 0x15, -0x26, 0x4A, 0x35, 0x1E, 0x2B, 0x3F, 0x22, 0x1F, 0x37, 0x24, -0x01, 0x9C, 0x96, 0x1E, 0xA0, 0x00, 0x02, 0x00, 0x23, 0xFF, -0xF6, 0x01, 0xE0, 0x02, 0xDA, 0x00, 0x2A, 0x00, 0x2E, 0x00, -0x00, 0x37, 0x21, 0x36, 0x34, 0x35, 0x34, 0x26, 0x26, 0x23, -0x22, 0x06, 0x06, 0x07, 0x06, 0x06, 0x15, 0x14, 0x16, 0x16, -0x33, 0x32, 0x36, 0x36, 0x37, 0x27, 0x0E, 0x02, 0x23, 0x22, -0x26, 0x26, 0x35, 0x37, 0x34, 0x36, 0x36, 0x33, 0x32, 0x16, -0x16, 0x17, 0x21, 0x01, 0x27, 0x07, 0x17, 0x56, 0x01, 0x89, -0x01, 0x36, 0x62, 0x41, 0x3B, 0x5F, 0x3C, 0x0A, 0x02, 0x02, -0x39, 0x63, 0x3E, 0x3C, 0x50, 0x38, 0x17, 0x41, 0x0F, 0x2A, -0x35, 0x22, 0x2D, 0x42, 0x24, 0x02, 0x27, 0x42, 0x28, 0x28, -0x37, 0x1F, 0x04, 0xFE, 0xCD, 0x01, 0x51, 0x4B, 0x96, 0x2D, -0xD2, 0x06, 0x0C, 0x06, 0x49, 0x69, 0x3A, 0x30, 0x56, 0x39, -0x0C, 0x18, 0x0D, 0x45, 0x6D, 0x3E, 0x1D, 0x35, 0x23, 0x29, -0x19, 0x25, 0x15, 0x26, 0x4A, 0x35, 0x1E, 0x2B, 0x3F, 0x22, -0x1F, 0x37, 0x24, 0x01, 0x9C, 0x28, 0xA0, 0x1E, 0x00, 0x02, -0x00, 0x23, 0xFF, 0xF6, 0x01, 0xE0, 0x02, 0xC6, 0x00, 0x2A, -0x00, 0x30, 0x00, 0x00, 0x37, 0x21, 0x36, 0x34, 0x35, 0x34, -0x26, 0x26, 0x23, 0x22, 0x06, 0x06, 0x07, 0x06, 0x06, 0x15, -0x14, 0x16, 0x16, 0x33, 0x32, 0x36, 0x36, 0x37, 0x27, 0x0E, -0x02, 0x23, 0x22, 0x26, 0x26, 0x35, 0x37, 0x34, 0x36, 0x36, -0x33, 0x32, 0x16, 0x16, 0x17, 0x21, 0x13, 0x17, 0x37, 0x27, -0x07, 0x17, 0x56, 0x01, 0x89, 0x01, 0x36, 0x62, 0x41, 0x3B, -0x5F, 0x3C, 0x0A, 0x02, 0x02, 0x39, 0x63, 0x3E, 0x3C, 0x50, -0x38, 0x17, 0x41, 0x0F, 0x2A, 0x35, 0x22, 0x2D, 0x42, 0x24, -0x02, 0x27, 0x42, 0x28, 0x28, 0x37, 0x1F, 0x04, 0xFE, 0xCD, -0xB1, 0x6E, 0x32, 0xA0, 0xA0, 0x32, 0xD2, 0x06, 0x0C, 0x06, -0x49, 0x69, 0x3A, 0x30, 0x56, 0x39, 0x0C, 0x18, 0x0D, 0x45, -0x6D, 0x3E, 0x1D, 0x35, 0x23, 0x29, 0x19, 0x25, 0x15, 0x26, -0x4A, 0x35, 0x1E, 0x2B, 0x3F, 0x22, 0x1F, 0x37, 0x24, 0x01, -0x5B, 0x5F, 0x28, 0x8C, 0x8C, 0x28, 0x00, 0x03, 0x00, 0x23, -0xFF, 0xF6, 0x01, 0xE0, 0x02, 0x80, 0x00, 0x2A, 0x00, 0x36, -0x00, 0x42, 0x00, 0x00, 0x37, 0x21, 0x36, 0x34, 0x35, 0x34, -0x26, 0x26, 0x23, 0x22, 0x06, 0x06, 0x07, 0x06, 0x06, 0x15, -0x14, 0x16, 0x16, 0x33, 0x32, 0x36, 0x36, 0x37, 0x27, 0x0E, -0x02, 0x23, 0x22, 0x26, 0x26, 0x35, 0x37, 0x34, 0x36, 0x36, -0x33, 0x32, 0x16, 0x16, 0x17, 0x21, 0x13, 0x14, 0x16, 0x33, -0x32, 0x36, 0x35, 0x34, 0x26, 0x23, 0x22, 0x06, 0x07, 0x14, -0x16, 0x33, 0x32, 0x36, 0x35, 0x34, 0x26, 0x23, 0x22, 0x06, -0x56, 0x01, 0x89, 0x01, 0x36, 0x62, 0x41, 0x3B, 0x5F, 0x3C, -0x0A, 0x02, 0x02, 0x39, 0x63, 0x3E, 0x3C, 0x50, 0x38, 0x17, -0x41, 0x0F, 0x2A, 0x35, 0x22, 0x2D, 0x42, 0x24, 0x02, 0x27, -0x42, 0x28, 0x28, 0x37, 0x1F, 0x04, 0xFE, 0xCD, 0xDE, 0x21, -0x16, 0x17, 0x20, 0x20, 0x17, 0x16, 0x21, 0xC8, 0x21, 0x16, -0x17, 0x20, 0x20, 0x17, 0x16, 0x21, 0xD2, 0x06, 0x0C, 0x06, -0x49, 0x69, 0x3A, 0x30, 0x56, 0x39, 0x0C, 0x18, 0x0D, 0x45, -0x6D, 0x3E, 0x1D, 0x35, 0x23, 0x29, 0x19, 0x25, 0x15, 0x26, -0x4A, 0x35, 0x1E, 0x2B, 0x3F, 0x22, 0x1F, 0x37, 0x24, 0x01, -0x33, 0x16, 0x21, 0x21, 0x16, 0x17, 0x20, 0x20, 0x17, 0x16, -0x21, 0x21, 0x16, 0x17, 0x20, 0x20, 0x00, 0x02, 0xFF, 0xE2, -0x00, 0x00, 0x00, 0xC3, 0x02, 0xE4, 0x00, 0x03, 0x00, 0x07, -0x00, 0x00, 0x13, 0x11, 0x33, 0x11, 0x27, 0x17, 0x37, 0x27, -0x5A, 0x50, 0xC8, 0xB4, 0x2D, 0x96, 0x01, 0xCC, 0xFE, 0x34, -0x01, 0xCC, 0xF0, 0x96, 0x1E, 0xA0, 0x00, 0x02, 0x00, 0x41, -0x00, 0x00, 0x01, 0x22, 0x02, 0xE4, 0x00, 0x03, 0x00, 0x07, -0x00, 0x00, 0x13, 0x11, 0x33, 0x11, 0x37, 0x27, 0x07, 0x17, -0x5A, 0x50, 0x78, 0x4B, 0x96, 0x2D, 0x01, 0xCC, 0xFE, 0x34, -0x01, 0xCC, 0xF0, 0x28, 0xA0, 0x1E, 0x00, 0x02, 0xFF, 0xE2, -0x00, 0x00, 0x01, 0x22, 0x02, 0xD0, 0x00, 0x03, 0x00, 0x09, -0x00, 0x00, 0x13, 0x11, 0x33, 0x11, 0x27, 0x17, 0x37, 0x27, -0x07, 0x17, 0x5A, 0x50, 0x28, 0x6E, 0x32, 0xA0, 0xA0, 0x32, -0x01, 0xCC, 0xFE, 0x34, 0x01, 0xCC, 0xAF, 0x5F, 0x28, 0x8C, -0x8C, 0x28, 0x00, 0x03, 0xFF, 0xE7, 0x00, 0x00, 0x01, 0x1D, -0x02, 0x9E, 0x00, 0x03, 0x00, 0x0F, 0x00, 0x1B, 0x00, 0x00, -0x13, 0x11, 0x33, 0x11, 0x37, 0x14, 0x16, 0x33, 0x32, 0x36, -0x35, 0x34, 0x26, 0x23, 0x22, 0x06, 0x07, 0x14, 0x16, 0x33, -0x32, 0x36, 0x35, 0x34, 0x26, 0x23, 0x22, 0x06, 0x5A, 0x50, -0x05, 0x21, 0x16, 0x17, 0x20, 0x20, 0x17, 0x16, 0x21, 0xC8, -0x21, 0x16, 0x17, 0x20, 0x20, 0x17, 0x16, 0x21, 0x01, 0xCC, -0xFE, 0x34, 0x01, 0xCC, 0x9B, 0x16, 0x21, 0x21, 0x16, 0x17, -0x20, 0x20, 0x17, 0x16, 0x21, 0x21, 0x16, 0x17, 0x20, 0x20, -0x00, 0x03, 0x00, 0x4B, 0x00, 0x00, 0x01, 0xC2, 0x02, 0x8A, -0x00, 0x03, 0x00, 0x15, 0x00, 0x2F, 0x00, 0x00, 0x13, 0x23, -0x11, 0x33, 0x13, 0x11, 0x33, 0x11, 0x34, 0x26, 0x23, 0x22, -0x06, 0x06, 0x15, 0x33, 0x34, 0x36, 0x36, 0x33, 0x32, 0x16, -0x25, 0x17, 0x34, 0x36, 0x36, 0x33, 0x32, 0x16, 0x16, 0x33, -0x32, 0x36, 0x36, 0x37, 0x27, 0x06, 0x06, 0x23, 0x22, 0x2E, -0x02, 0x23, 0x22, 0x06, 0x06, 0x9B, 0x50, 0x50, 0xD7, 0x50, -0x57, 0x47, 0x30, 0x4E, 0x2F, 0x24, 0x1D, 0x35, 0x23, 0x31, -0x31, 0xFE, 0xF0, 0x35, 0x08, 0x16, 0x14, 0x17, 0x21, 0x26, -0x1B, 0x19, 0x2A, 0x1F, 0x08, 0x2E, 0x0F, 0x1B, 0x0E, 0x14, -0x1A, 0x17, 0x1F, 0x18, 0x16, 0x2D, 0x20, 0x01, 0xCC, 0xFE, -0x34, 0x01, 0x18, 0xFE, 0xE8, 0x01, 0x22, 0x59, 0x5B, 0x32, -0x55, 0x37, 0x21, 0x34, 0x1E, 0x3A, 0xE6, 0x25, 0x08, 0x18, -0x13, 0x17, 0x18, 0x15, 0x22, 0x12, 0x2B, 0x18, 0x18, 0x0E, -0x13, 0x0E, 0x15, 0x24, 0x00, 0x03, 0x00, 0x23, 0xFF, 0xF6, -0x01, 0xFF, 0x02, 0xE4, 0x00, 0x0F, 0x00, 0x1F, 0x00, 0x23, -0x00, 0x00, 0x37, 0x14, 0x16, 0x16, 0x33, 0x32, 0x36, 0x36, -0x35, 0x34, 0x26, 0x26, 0x23, 0x22, 0x06, 0x06, 0x17, 0x34, -0x36, 0x36, 0x33, 0x32, 0x16, 0x16, 0x15, 0x14, 0x06, 0x06, -0x23, 0x22, 0x26, 0x26, 0x03, 0x17, 0x37, 0x27, 0x23, 0x3F, -0x6C, 0x43, 0x44, 0x6B, 0x3F, 0x3F, 0x6B, 0x44, 0x43, 0x6C, -0x3F, 0x51, 0x29, 0x47, 0x2D, 0x2D, 0x47, 0x29, 0x29, 0x47, -0x2D, 0x2D, 0x47, 0x29, 0x03, 0xB4, 0x2D, 0x96, 0xE6, 0x45, -0x6D, 0x3E, 0x3E, 0x6D, 0x45, 0x46, 0x6C, 0x3E, 0x3E, 0x6C, -0x46, 0x30, 0x4B, 0x2A, 0x2A, 0x4B, 0x30, 0x30, 0x4A, 0x2B, -0x2B, 0x4A, 0x02, 0x06, 0x96, 0x1E, 0xA0, 0x00, 0x03, 0x00, -0x23, 0xFF, 0xF6, 0x01, 0xFF, 0x02, 0xE4, 0x00, 0x0F, 0x00, -0x1F, 0x00, 0x23, 0x00, 0x00, 0x37, 0x14, 0x16, 0x16, 0x33, -0x32, 0x36, 0x36, 0x35, 0x34, 0x26, 0x26, 0x23, 0x22, 0x06, -0x06, 0x17, 0x34, 0x36, 0x36, 0x33, 0x32, 0x16, 0x16, 0x15, -0x14, 0x06, 0x06, 0x23, 0x22, 0x26, 0x26, 0x01, 0x27, 0x07, -0x17, 0x23, 0x3F, 0x6C, 0x43, 0x44, 0x6B, 0x3F, 0x3F, 0x6B, -0x44, 0x43, 0x6C, 0x3F, 0x51, 0x29, 0x47, 0x2D, 0x2D, 0x47, -0x29, 0x29, 0x47, 0x2D, 0x2D, 0x47, 0x29, 0x01, 0x3D, 0x4B, -0x96, 0x2D, 0xE6, 0x45, 0x6D, 0x3E, 0x3E, 0x6D, 0x45, 0x46, -0x6C, 0x3E, 0x3E, 0x6C, 0x46, 0x30, 0x4B, 0x2A, 0x2A, 0x4B, -0x30, 0x30, 0x4A, 0x2B, 0x2B, 0x4A, 0x02, 0x06, 0x28, 0xA0, -0x1E, 0x00, 0x03, 0x00, 0x23, 0xFF, 0xF6, 0x01, 0xFF, 0x02, -0xDA, 0x00, 0x0F, 0x00, 0x1F, 0x00, 0x25, 0x00, 0x00, 0x37, -0x14, 0x16, 0x16, 0x33, 0x32, 0x36, 0x36, 0x35, 0x34, 0x26, -0x26, 0x23, 0x22, 0x06, 0x06, 0x17, 0x34, 0x36, 0x36, 0x33, -0x32, 0x16, 0x16, 0x15, 0x14, 0x06, 0x06, 0x23, 0x22, 0x26, -0x26, 0x13, 0x17, 0x37, 0x27, 0x07, 0x17, 0x23, 0x3F, 0x6C, -0x43, 0x44, 0x6B, 0x3F, 0x3F, 0x6B, 0x44, 0x43, 0x6C, 0x3F, -0x51, 0x29, 0x47, 0x2D, 0x2D, 0x47, 0x29, 0x29, 0x47, 0x2D, -0x2D, 0x47, 0x29, 0x9D, 0x6E, 0x32, 0xA0, 0xA0, 0x32, 0xE6, -0x45, 0x6D, 0x3E, 0x3E, 0x6D, 0x45, 0x46, 0x6C, 0x3E, 0x3E, -0x6C, 0x46, 0x30, 0x4B, 0x2A, 0x2A, 0x4B, 0x30, 0x30, 0x4A, -0x2B, 0x2B, 0x4A, 0x01, 0xCF, 0x5F, 0x28, 0x8C, 0x8C, 0x28, -0x00, 0x04, 0x00, 0x23, 0xFF, 0xF6, 0x01, 0xFF, 0x02, 0x9E, -0x00, 0x0F, 0x00, 0x1F, 0x00, 0x2B, 0x00, 0x37, 0x00, 0x00, -0x37, 0x14, 0x16, 0x16, 0x33, 0x32, 0x36, 0x36, 0x35, 0x34, -0x26, 0x26, 0x23, 0x22, 0x06, 0x06, 0x17, 0x34, 0x36, 0x36, -0x33, 0x32, 0x16, 0x16, 0x15, 0x14, 0x06, 0x06, 0x23, 0x22, -0x26, 0x26, 0x13, 0x14, 0x16, 0x33, 0x32, 0x36, 0x35, 0x34, -0x26, 0x23, 0x22, 0x06, 0x07, 0x14, 0x16, 0x33, 0x32, 0x36, -0x35, 0x34, 0x26, 0x23, 0x22, 0x06, 0x23, 0x3F, 0x6C, 0x43, -0x44, 0x6B, 0x3F, 0x3F, 0x6B, 0x44, 0x43, 0x6C, 0x3F, 0x51, -0x29, 0x47, 0x2D, 0x2D, 0x47, 0x29, 0x29, 0x47, 0x2D, 0x2D, -0x47, 0x29, 0xCA, 0x21, 0x16, 0x17, 0x20, 0x20, 0x17, 0x16, -0x21, 0xC8, 0x21, 0x16, 0x17, 0x20, 0x20, 0x17, 0x16, 0x21, -0xE6, 0x45, 0x6D, 0x3E, 0x3E, 0x6D, 0x45, 0x46, 0x6C, 0x3E, -0x3E, 0x6C, 0x46, 0x30, 0x4B, 0x2A, 0x2A, 0x4B, 0x30, 0x30, -0x4A, 0x2B, 0x2B, 0x4A, 0x01, 0xB1, 0x16, 0x21, 0x21, 0x16, -0x17, 0x20, 0x20, 0x17, 0x16, 0x21, 0x21, 0x16, 0x17, 0x20, -0x20, 0x00, 0x03, 0x00, 0x4B, 0xFF, 0xF6, 0x01, 0xC2, 0x02, -0xE4, 0x00, 0x03, 0x00, 0x15, 0x00, 0x19, 0x00, 0x00, 0x21, -0x33, 0x11, 0x23, 0x03, 0x11, 0x23, 0x11, 0x14, 0x16, 0x33, -0x32, 0x36, 0x36, 0x35, 0x23, 0x14, 0x06, 0x06, 0x23, 0x22, -0x26, 0x03, 0x17, 0x37, 0x27, 0x01, 0x72, 0x50, 0x50, 0xD7, -0x50, 0x57, 0x47, 0x30, 0x4E, 0x2F, 0x24, 0x1D, 0x35, 0x23, -0x31, 0x31, 0x0D, 0xB4, 0x2E, 0x96, 0x01, 0xCC, 0xFE, 0xE8, -0x01, 0x18, 0xFE, 0xDE, 0x59, 0x5B, 0x32, 0x55, 0x37, 0x21, -0x34, 0x1E, 0x3A, 0x02, 0x41, 0x96, 0x1E, 0xA0, 0x00, 0x03, -0x00, 0x4B, 0xFF, 0xF6, 0x01, 0xC2, 0x02, 0xE4, 0x00, 0x03, -0x00, 0x15, 0x00, 0x19, 0x00, 0x00, 0x21, 0x33, 0x11, 0x23, -0x03, 0x11, 0x23, 0x11, 0x14, 0x16, 0x33, 0x32, 0x36, 0x36, -0x35, 0x23, 0x14, 0x06, 0x06, 0x23, 0x22, 0x26, 0x13, 0x27, -0x07, 0x17, 0x01, 0x72, 0x50, 0x50, 0xD7, 0x50, 0x57, 0x47, -0x30, 0x4E, 0x2F, 0x24, 0x1D, 0x35, 0x23, 0x31, 0x31, 0xE3, -0x4A, 0x96, 0x2C, 0x01, 0xCC, 0xFE, 0xE8, 0x01, 0x18, 0xFE, -0xDE, 0x59, 0x5B, 0x32, 0x55, 0x37, 0x21, 0x34, 0x1E, 0x3A, -0x02, 0x41, 0x28, 0xA0, 0x1E, 0x00, 0x03, 0x00, 0x4B, 0xFF, -0xF6, 0x01, 0xC2, 0x02, 0xDA, 0x00, 0x03, 0x00, 0x15, 0x00, -0x1B, 0x00, 0x00, 0x21, 0x33, 0x11, 0x23, 0x03, 0x11, 0x23, -0x11, 0x14, 0x16, 0x33, 0x32, 0x36, 0x36, 0x35, 0x23, 0x14, -0x06, 0x06, 0x23, 0x22, 0x26, 0x13, 0x17, 0x37, 0x27, 0x07, -0x17, 0x01, 0x72, 0x50, 0x50, 0xD7, 0x50, 0x57, 0x47, 0x30, -0x4E, 0x2F, 0x24, 0x1D, 0x35, 0x23, 0x31, 0x31, 0x6B, 0x6E, -0x32, 0xA0, 0xA0, 0x32, 0x01, 0xCC, 0xFE, 0xE8, 0x01, 0x18, -0xFE, 0xDE, 0x59, 0x5B, 0x32, 0x55, 0x37, 0x21, 0x34, 0x1E, -0x3A, 0x02, 0x0A, 0x5F, 0x28, 0x8C, 0x8C, 0x28, 0x00, 0x04, -0x00, 0x4B, 0xFF, 0xF6, 0x01, 0xC2, 0x02, 0x94, 0x00, 0x03, -0x00, 0x15, 0x00, 0x21, 0x00, 0x2D, 0x00, 0x00, 0x21, 0x33, -0x11, 0x23, 0x03, 0x11, 0x23, 0x11, 0x14, 0x16, 0x33, 0x32, -0x36, 0x36, 0x35, 0x23, 0x14, 0x06, 0x06, 0x23, 0x22, 0x26, -0x13, 0x14, 0x16, 0x33, 0x32, 0x36, 0x35, 0x34, 0x26, 0x23, -0x22, 0x06, 0x07, 0x14, 0x16, 0x33, 0x32, 0x36, 0x35, 0x34, -0x26, 0x23, 0x22, 0x06, 0x01, 0x72, 0x50, 0x50, 0xD7, 0x50, -0x57, 0x47, 0x30, 0x4E, 0x2F, 0x24, 0x1D, 0x35, 0x23, 0x31, -0x31, 0x99, 0x20, 0x16, 0x17, 0x21, 0x21, 0x17, 0x16, 0x20, -0xC8, 0x20, 0x16, 0x17, 0x21, 0x21, 0x17, 0x16, 0x20, 0x01, -0xCC, 0xFE, 0xE8, 0x01, 0x18, 0xFE, 0xDE, 0x59, 0x5B, 0x32, -0x55, 0x37, 0x21, 0x34, 0x1E, 0x3A, 0x01, 0xE2, 0x16, 0x21, -0x21, 0x16, 0x17, 0x20, 0x20, 0x17, 0x16, 0x21, 0x21, 0x16, -0x17, 0x20, 0x20, 0x00, 0x03, 0x00, 0x00, 0xFF, 0x24, 0x01, -0xB8, 0x02, 0x80, 0x00, 0x08, 0x00, 0x14, 0x00, 0x20, 0x00, -0x00, 0x01, 0x23, 0x03, 0x17, 0x03, 0x23, 0x13, 0x03, 0x33, -0x13, 0x14, 0x16, 0x33, 0x32, 0x36, 0x35, 0x34, 0x26, 0x23, -0x22, 0x06, 0x07, 0x14, 0x16, 0x33, 0x32, 0x36, 0x35, 0x34, -0x26, 0x23, 0x22, 0x06, 0x01, 0xB8, 0x5A, 0x8E, 0x21, 0x92, -0x5F, 0xB0, 0x7E, 0x5A, 0x7D, 0x21, 0x16, 0x17, 0x20, 0x20, -0x17, 0x16, 0x21, 0xC8, 0x21, 0x16, 0x17, 0x20, 0x20, 0x17, -0x16, 0x21, 0x01, 0xCC, 0xFE, 0x9C, 0x04, 0x01, 0x68, 0xFE, -0x75, 0xFE, 0xE3, 0x03, 0x25, 0x16, 0x21, 0x21, 0x16, 0x17, -0x20, 0x20, 0x17, 0x16, 0x21, 0x21, 0x16, 0x17, 0x20, 0x20, -0x00, 0x01, 0x00, 0x73, 0xFF, 0x37, 0x01, 0xFF, 0x03, 0x17, -0x00, 0x24, 0x00, 0x00, 0x17, 0x16, 0x36, 0x36, 0x37, 0x13, -0x33, 0x37, 0x23, 0x37, 0x36, 0x36, 0x33, 0x32, 0x16, 0x17, -0x37, 0x26, 0x26, 0x27, 0x26, 0x06, 0x07, 0x07, 0x23, 0x07, -0x33, 0x03, 0x06, 0x06, 0x23, 0x22, 0x26, 0x27, 0x07, 0x16, -0x16, 0xCC, 0x25, 0x39, 0x22, 0x03, 0x17, 0x4E, 0x06, 0x50, -0x0A, 0x03, 0x1B, 0x16, 0x09, 0x13, 0x10, 0x2B, 0x19, 0x29, -0x17, 0x38, 0x46, 0x05, 0x09, 0x3C, 0x06, 0x3E, 0x18, 0x03, -0x1A, 0x17, 0x09, 0x13, 0x10, 0x2B, 0x1A, 0x29, 0xC8, 0x01, -0x27, 0x49, 0x33, 0x01, 0xA7, 0x4B, 0xA0, 0x2C, 0x2F, 0x0B, -0x0F, 0x3E, 0x17, 0x12, 0x01, 0x02, 0x58, 0x4D, 0xA6, 0x4B, -0xFE, 0x5F, 0x2B, 0x2F, 0x0B, 0x0F, 0x3E, 0x16, 0x13, 0x00, -0x03, 0x00, 0x0F, 0xFF, 0xF6, 0x02, 0x08, 0x02, 0xC6, 0x00, -0x03, 0x00, 0x07, 0x00, 0x24, 0x00, 0x00, 0x13, 0x21, 0x35, -0x21, 0x15, 0x21, 0x35, 0x21, 0x37, 0x34, 0x36, 0x36, 0x33, -0x32, 0x17, 0x35, 0x26, 0x26, 0x23, 0x22, 0x0E, 0x02, 0x15, -0x14, 0x16, 0x16, 0x33, 0x32, 0x36, 0x37, 0x35, 0x06, 0x23, -0x22, 0x26, 0x26, 0x0F, 0x01, 0xF5, 0xFE, 0x0B, 0x01, 0xDB, -0xFE, 0x25, 0x8C, 0x36, 0x61, 0x40, 0x56, 0x40, 0x22, 0x4B, -0x29, 0x41, 0x6E, 0x51, 0x2C, 0x4E, 0x87, 0x57, 0x29, 0x4B, -0x22, 0x40, 0x56, 0x40, 0x61, 0x36, 0x01, 0x97, 0x30, 0xA8, -0x30, 0x0F, 0x52, 0x7E, 0x48, 0x3E, 0x65, 0x14, 0x15, 0x35, -0x61, 0x84, 0x4E, 0x69, 0xA2, 0x5D, 0x16, 0x13, 0x65, 0x3E, -0x48, 0x7F, 0x00, 0x03, 0x00, 0x16, 0x00, 0x30, 0x02, 0xD8, -0x01, 0x9A, 0x00, 0x18, 0x00, 0x26, 0x00, 0x34, 0x00, 0x00, -0x37, 0x32, 0x37, 0x16, 0x16, 0x33, 0x32, 0x36, 0x36, 0x35, -0x34, 0x26, 0x26, 0x23, 0x22, 0x07, 0x26, 0x23, 0x22, 0x06, -0x06, 0x15, 0x14, 0x16, 0x16, 0x37, 0x22, 0x26, 0x26, 0x35, -0x34, 0x36, 0x36, 0x33, 0x32, 0x16, 0x17, 0x06, 0x06, 0x21, -0x22, 0x26, 0x27, 0x36, 0x36, 0x33, 0x32, 0x16, 0x16, 0x15, -0x14, 0x06, 0x06, 0xBF, 0x5B, 0x5E, 0x2F, 0x5B, 0x2E, 0x2D, -0x4D, 0x2E, 0x2E, 0x4D, 0x2D, 0x5B, 0x5D, 0x5D, 0x5C, 0x2D, -0x4D, 0x2F, 0x2F, 0x4D, 0x38, 0x19, 0x2C, 0x1B, 0x1B, 0x2C, -0x19, 0x21, 0x44, 0x1E, 0x1F, 0x43, 0x01, 0x3A, 0x21, 0x44, -0x1E, 0x1E, 0x44, 0x21, 0x19, 0x2B, 0x1B, 0x1B, 0x2B, 0x30, -0x78, 0x3C, 0x3C, 0x33, 0x53, 0x30, 0x31, 0x51, 0x32, 0x78, -0x78, 0x32, 0x51, 0x31, 0x30, 0x53, 0x33, 0x4E, 0x1E, 0x2F, -0x1B, 0x1C, 0x2E, 0x1D, 0x35, 0x32, 0x32, 0x36, 0x37, 0x31, -0x32, 0x35, 0x1D, 0x2E, 0x1C, 0x1B, 0x2F, 0x1E, 0x00, 0x01, -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xEA, 0x28, 0x17, 0x17, -0x5F, 0x0F, 0x3C, 0xF5, 0x00, 0x03, 0x08, 0x00, 0x00, 0x00, -0x00, 0x00, 0xE0, 0x80, 0x16, 0x1F, 0x00, 0x00, 0x00, 0x00, -0xE0, 0x80, 0x16, 0x1F, 0xFF, 0x91, 0xFE, 0xA2, 0x05, 0xC4, -0x04, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x02, 0x00, 0x01, -0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, -0x00, 0x00, 0x00, 0x00, 0x03, 0xDE, 0xFF, 0x91, 0xFF, 0xE2, -0x03, 0xD4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x88, -0x02, 0x58, 0x00, 0x00, 0x01, 0x2C, 0x00, 0x5F, 0x01, 0xD1, -0x00, 0x50, 0x02, 0x5D, 0x00, 0x23, 0x02, 0x44, 0x00, 0x37, -0x02, 0xEF, 0x00, 0x23, 0x02, 0xA3, 0x00, 0x28, 0x01, 0x1D, -0x00, 0x50, 0x01, 0x36, 0x00, 0x5A, 0x01, 0x36, 0x00, 0x1E, -0x02, 0x50, 0x00, 0x64, 0x02, 0x67, 0x00, 0x41, 0x01, 0x34, -0x00, 0x3C, 0x00, 0xD2, 0x00, 0x05, 0x01, 0x2C, 0x00, 0x5F, -0x02, 0x03, 0x00, 0x0F, 0x02, 0x58, 0x00, 0x28, 0x01, 0xC2, -0x00, 0x50, 0x02, 0x39, 0x00, 0x14, 0x02, 0x22, 0x00, 0x3C, -0x02, 0x47, 0x00, 0x14, 0x02, 0x34, 0x00, 0x1E, 0x02, 0x40, -0x00, 0x32, 0x02, 0x0D, 0x00, 0x1E, 0x02, 0x22, 0x00, 0x37, -0x02, 0x40, 0x00, 0x32, 0x01, 0x2C, 0x00, 0x5F, 0x01, 0x4A, -0x00, 0x3C, 0x02, 0x80, 0x00, 0x41, 0x02, 0x67, 0x00, 0x41, -0x02, 0x80, 0x00, 0x41, 0x02, 0x2D, 0x00, 0x3C, 0x03, 0x02, -0x00, 0x23, 0x02, 0x94, 0x00, 0x05, 0x02, 0x2E, 0x00, 0x50, -0x02, 0xAD, 0x00, 0x28, 0x02, 0xA8, 0x00, 0x50, 0x02, 0x26, -0x00, 0x50, 0x01, 0xF4, 0x00, 0x50, 0x03, 0x07, 0x00, 0x28, -0x02, 0xCB, 0x00, 0x50, 0x00, 0xF5, 0x00, 0x50, 0x00, 0xF0, -0xFF, 0x91, 0x02, 0x35, 0x00, 0x50, 0x01, 0xC2, 0x00, 0x50, -0x03, 0x2A, 0x00, 0x28, 0x03, 0x07, 0x00, 0x50, 0x03, 0x16, -0x00, 0x28, 0x02, 0x24, 0x00, 0x50, 0x03, 0x16, 0x00, 0x28, -0x02, 0x30, 0x00, 0x50, 0x02, 0x24, 0x00, 0x26, 0x01, 0xD1, -0x00, 0x0A, 0x02, 0x72, 0x00, 0x4B, 0x02, 0x94, 0x00, 0x05, -0x03, 0xDE, 0x00, 0x0A, 0x02, 0x2B, 0x00, 0x0A, 0x02, 0x31, -0x00, 0x05, 0x02, 0x21, 0x00, 0x0F, 0x01, 0x2C, 0x00, 0x64, -0x02, 0x03, 0x00, 0x0F, 0x01, 0x2C, 0x00, 0x14, 0x01, 0xFE, -0x00, 0x41, 0x01, 0xF4, 0x00, 0x00, 0x01, 0x77, 0x00, 0x4B, -0x01, 0xE1, 0x00, 0x1E, 0x02, 0x3A, 0x00, 0x46, 0x01, 0xDE, -0x00, 0x1E, 0x02, 0x3A, 0x00, 0x23, 0x02, 0x03, 0x00, 0x23, -0x01, 0x1E, 0x00, 0x28, 0x02, 0x3A, 0x00, 0x23, 0x02, 0x0D, -0x00, 0x4B, 0x01, 0x04, 0x00, 0x4B, 0x00, 0xFA, 0xFF, 0xC3, -0x01, 0xB8, 0x00, 0x46, 0x00, 0xE6, 0x00, 0x4B, 0x03, 0x0C, -0x00, 0x4B, 0x02, 0x0D, 0x00, 0x4B, 0x02, 0x22, 0x00, 0x23, -0x02, 0x3A, 0x00, 0x46, 0x02, 0x3A, 0x00, 0x23, 0x01, 0x57, -0x00, 0x4B, 0x01, 0x93, 0x00, 0x1F, 0x00, 0xF0, 0x00, 0x05, -0x02, 0x0D, 0x00, 0x4B, 0x01, 0xB8, 0x00, 0x00, 0x02, 0xA8, -0x00, 0x05, 0x01, 0xA4, 0x00, 0x00, 0x01, 0xB8, 0x00, 0x00, -0x01, 0xCC, 0x00, 0x0F, 0x01, 0x37, 0x00, 0x32, 0x01, 0x12, -0x00, 0x64, 0x01, 0x37, 0x00, 0x3B, 0x02, 0x3F, 0x00, 0x41, -0x02, 0x44, 0x00, 0x51, 0x02, 0x44, 0x00, 0x40, 0x02, 0x44, -0x00, 0x0F, 0x01, 0x98, 0x00, 0x1E, 0x01, 0x16, 0x00, 0x14, -0x02, 0x94, 0x00, 0x05, 0x02, 0x94, 0x00, 0x05, 0x03, 0xC0, -0x00, 0x05, 0x02, 0xAD, 0x00, 0x28, 0x02, 0x26, 0x00, 0x50, -0x03, 0x07, 0x00, 0x50, 0x03, 0x16, 0x00, 0x28, 0x02, 0x72, -0x00, 0x4B, 0x01, 0xE5, 0x00, 0x1E, 0x01, 0xE5, 0x00, 0x1E, -0x01, 0xE5, 0x00, 0x1E, 0x01, 0xE5, 0x00, 0x1E, 0x01, 0xE5, -0x00, 0x1E, 0x01, 0xDE, 0x00, 0x1E, 0x02, 0x03, 0x00, 0x23, -0x02, 0x03, 0x00, 0x23, 0x02, 0x03, 0x00, 0x23, 0x02, 0x03, -0x00, 0x23, 0x01, 0x04, 0xFF, 0xE2, 0x01, 0x04, 0x00, 0x41, -0x01, 0x04, 0xFF, 0xE2, 0x01, 0x04, 0xFF, 0xE7, 0x02, 0x0D, -0x00, 0x4B, 0x02, 0x22, 0x00, 0x23, 0x02, 0x22, 0x00, 0x23, -0x02, 0x22, 0x00, 0x23, 0x02, 0x22, 0x00, 0x23, 0x02, 0x0D, -0x00, 0x4B, 0x02, 0x0D, 0x00, 0x4B, 0x02, 0x0D, 0x00, 0x4B, -0x02, 0x0D, 0x00, 0x4B, 0x01, 0xB8, 0x00, 0x00, 0x02, 0x58, -0x00, 0x73, 0x01, 0xF4, 0x00, 0x00, 0x02, 0x44, 0x00, 0x0F, -0x02, 0xEE, 0x00, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, -0x00, 0x38, 0x00, 0x00, 0x00, 0x74, 0x00, 0x00, 0x00, 0x9E, -0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x01, 0xAC, 0x00, 0x00, -0x02, 0x79, 0x00, 0x00, 0x03, 0x2B, 0x00, 0x00, 0x03, 0x47, -0x00, 0x00, 0x03, 0x7B, 0x00, 0x00, 0x03, 0xAE, 0x00, 0x00, -0x04, 0x00, 0x00, 0x00, 0x04, 0x26, 0x00, 0x00, 0x04, 0x41, -0x00, 0x00, 0x04, 0x58, 0x00, 0x00, 0x04, 0x83, 0x00, 0x00, -0x04, 0xA2, 0x00, 0x00, 0x05, 0x0D, 0x00, 0x00, 0x05, 0x2D, -0x00, 0x00, 0x05, 0x89, 0x00, 0x00, 0x06, 0x22, 0x00, 0x00, -0x06, 0x4F, 0x00, 0x00, 0x06, 0xC2, 0x00, 0x00, 0x07, 0x3A, -0x00, 0x00, 0x07, 0x5E, 0x00, 0x00, 0x08, 0x24, 0x00, 0x00, -0x08, 0x9E, 0x00, 0x00, 0x08, 0xE8, 0x00, 0x00, 0x09, 0x24, -0x00, 0x00, 0x09, 0x48, 0x00, 0x00, 0x09, 0x6D, 0x00, 0x00, -0x09, 0x91, 0x00, 0x00, 0x0A, 0x0C, 0x00, 0x00, 0x0B, 0x04, -0x00, 0x00, 0x0B, 0x41, 0x00, 0x00, 0x0B, 0xBC, 0x00, 0x00, -0x0C, 0x21, 0x00, 0x00, 0x0C, 0x77, 0x00, 0x00, 0x0C, 0xB7, -0x00, 0x00, 0x0C, 0xEC, 0x00, 0x00, 0x0D, 0x5D, 0x00, 0x00, -0x0D, 0x93, 0x00, 0x00, 0x0D, 0xAD, 0x00, 0x00, 0x0D, 0xEB, -0x00, 0x00, 0x0E, 0x20, 0x00, 0x00, 0x0E, 0x40, 0x00, 0x00, -0x0E, 0x72, 0x00, 0x00, 0x0E, 0x9D, 0x00, 0x00, 0x0F, 0x08, -0x00, 0x00, 0x0F, 0x53, 0x00, 0x00, 0x0F, 0xD0, 0x00, 0x00, -0x10, 0x31, 0x00, 0x00, 0x10, 0xC1, 0x00, 0x00, 0x10, 0xE4, -0x00, 0x00, 0x11, 0x2A, 0x00, 0x00, 0x11, 0x4E, 0x00, 0x00, -0x11, 0x81, 0x00, 0x00, 0x11, 0xBA, 0x00, 0x00, 0x11, 0xE7, -0x00, 0x00, 0x12, 0x11, 0x00, 0x00, 0x12, 0x32, 0x00, 0x00, -0x12, 0x50, 0x00, 0x00, 0x12, 0x70, 0x00, 0x00, 0x12, 0x91, -0x00, 0x00, 0x12, 0xA9, 0x00, 0x00, 0x12, 0xC4, 0x00, 0x00, -0x13, 0x58, 0x00, 0x00, 0x13, 0xC8, 0x00, 0x00, 0x14, 0x28, -0x00, 0x00, 0x14, 0x9B, 0x00, 0x00, 0x15, 0x13, 0x00, 0x00, -0x15, 0x5E, 0x00, 0x00, 0x15, 0xF8, 0x00, 0x00, 0x16, 0x44, -0x00, 0x00, 0x16, 0x7D, 0x00, 0x00, 0x16, 0xDD, 0x00, 0x00, -0x17, 0x0D, 0x00, 0x00, 0x17, 0x27, 0x00, 0x00, 0x17, 0x97, -0x00, 0x00, 0x17, 0xDC, 0x00, 0x00, 0x18, 0x3E, 0x00, 0x00, -0x18, 0xAF, 0x00, 0x00, 0x19, 0x1F, 0x00, 0x00, 0x19, 0x64, -0x00, 0x00, 0x19, 0xE8, 0x00, 0x00, 0x1A, 0x0D, 0x00, 0x00, -0x1A, 0x50, 0x00, 0x00, 0x1A, 0x73, 0x00, 0x00, 0x1A, 0xA2, -0x00, 0x00, 0x1A, 0xD3, 0x00, 0x00, 0x1A, 0xFF, 0x00, 0x00, -0x1B, 0x29, 0x00, 0x00, 0x1B, 0x9B, 0x00, 0x00, 0x1B, 0xB5, -0x00, 0x00, 0x1C, 0x26, 0x00, 0x00, 0x1C, 0xAA, 0x00, 0x00, -0x1D, 0x1D, 0x00, 0x00, 0x1D, 0xA6, 0x00, 0x00, 0x1D, 0xED, -0x00, 0x00, 0x1E, 0xE5, 0x00, 0x00, 0x1F, 0x30, 0x00, 0x00, -0x1F, 0xAC, 0x00, 0x00, 0x20, 0x28, 0x00, 0x00, 0x20, 0x8B, -0x00, 0x00, 0x21, 0x3E, 0x00, 0x00, 0x21, 0x8C, 0x00, 0x00, -0x21, 0xFE, 0x00, 0x00, 0x22, 0xA9, 0x00, 0x00, 0x23, 0x2D, -0x00, 0x00, 0x23, 0xCD, 0x00, 0x00, 0x24, 0x6E, 0x00, 0x00, -0x25, 0x13, 0x00, 0x00, 0x25, 0xE2, 0x00, 0x00, 0x26, 0xB1, -0x00, 0x00, 0x27, 0x5F, 0x00, 0x00, 0x27, 0xED, 0x00, 0x00, -0x28, 0x7C, 0x00, 0x00, 0x29, 0x10, 0x00, 0x00, 0x29, 0xCE, -0x00, 0x00, 0x29, 0xF6, 0x00, 0x00, 0x2A, 0x1E, 0x00, 0x00, -0x2A, 0x4C, 0x00, 0x00, 0x2A, 0xA4, 0x00, 0x00, 0x2B, 0x34, -0x00, 0x00, 0x2B, 0xA5, 0x00, 0x00, 0x2C, 0x17, 0x00, 0x00, -0x2C, 0x8E, 0x00, 0x00, 0x2D, 0x2F, 0x00, 0x00, 0x2D, 0x86, -0x00, 0x00, 0x2D, 0xDD, 0x00, 0x00, 0x2E, 0x3A, 0x00, 0x00, -0x2E, 0xC1, 0x00, 0x00, 0x2F, 0x2C, 0x00, 0x00, 0x2F, 0xA3, -0x00, 0x00, 0x2F, 0xA3, 0x00, 0x00, 0x30, 0x14, 0x00, 0x00, -0x30, 0xB0, 0x00, 0x01, 0x00, 0x00, 0x00, 0x88, 0x10, 0x00, -0x04, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x02, 0x00, 0x00, -0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, -0x00, 0xFF, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x96, -0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x0D, -0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, -0x00, 0x07, 0x00, 0x0D, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, -0x00, 0x03, 0x00, 0x21, 0x00, 0x14, 0x00, 0x01, 0x00, 0x00, -0x00, 0x00, 0x00, 0x04, 0x00, 0x0D, 0x00, 0x35, 0x00, 0x01, -0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x42, -0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x0D, -0x00, 0x45, 0x00, 0x03, 0x00, 0x01, 0x04, 0x09, 0x00, 0x01, -0x00, 0x1A, 0x00, 0x52, 0x00, 0x03, 0x00, 0x01, 0x04, 0x09, -0x00, 0x02, 0x00, 0x0E, 0x00, 0x6C, 0x00, 0x03, 0x00, 0x01, -0x04, 0x09, 0x00, 0x03, 0x00, 0x42, 0x00, 0x7A, 0x00, 0x03, -0x00, 0x01, 0x04, 0x09, 0x00, 0x04, 0x00, 0x1A, 0x00, 0xBC, -0x00, 0x03, 0x00, 0x01, 0x04, 0x09, 0x00, 0x05, 0x00, 0x06, -0x00, 0xD6, 0x00, 0x03, 0x00, 0x01, 0x04, 0x09, 0x00, 0x06, -0x00, 0x1A, 0x00, 0xDC, 0x47, 0x65, 0x6E, 0x65, 0x72, 0x61, -0x74, 0x65, 0x64, 0x46, 0x6F, 0x6E, 0x74, 0x52, 0x65, 0x67, -0x75, 0x6C, 0x61, 0x72, 0x47, 0x65, 0x6E, 0x65, 0x72, 0x61, -0x74, 0x65, 0x64, 0x46, 0x6F, 0x6E, 0x74, 0x20, 0x52, 0x65, -0x67, 0x75, 0x6C, 0x61, 0x72, 0x3A, 0x56, 0x65, 0x72, 0x73, -0x69, 0x6F, 0x6E, 0x20, 0x31, 0x2E, 0x30, 0x47, 0x65, 0x6E, -0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x46, 0x6F, 0x6E, 0x74, -0x31, 0x2E, 0x30, 0x47, 0x65, 0x6E, 0x65, 0x72, 0x61, 0x74, -0x65, 0x64, 0x46, 0x6F, 0x6E, 0x74, 0x00, 0x47, 0x00, 0x65, -0x00, 0x6E, 0x00, 0x65, 0x00, 0x72, 0x00, 0x61, 0x00, 0x74, -0x00, 0x65, 0x00, 0x64, 0x00, 0x46, 0x00, 0x6F, 0x00, 0x6E, -0x00, 0x74, 0x00, 0x52, 0x00, 0x65, 0x00, 0x67, 0x00, 0x75, -0x00, 0x6C, 0x00, 0x61, 0x00, 0x72, 0x00, 0x47, 0x00, 0x65, -0x00, 0x6E, 0x00, 0x65, 0x00, 0x72, 0x00, 0x61, 0x00, 0x74, -0x00, 0x65, 0x00, 0x64, 0x00, 0x46, 0x00, 0x6F, 0x00, 0x6E, -0x00, 0x74, 0x00, 0x20, 0x00, 0x52, 0x00, 0x65, 0x00, 0x67, -0x00, 0x75, 0x00, 0x6C, 0x00, 0x61, 0x00, 0x72, 0x00, 0x3A, -0x00, 0x56, 0x00, 0x65, 0x00, 0x72, 0x00, 0x73, 0x00, 0x69, -0x00, 0x6F, 0x00, 0x6E, 0x00, 0x20, 0x00, 0x31, 0x00, 0x2E, -0x00, 0x30, 0x00, 0x47, 0x00, 0x65, 0x00, 0x6E, 0x00, 0x65, -0x00, 0x72, 0x00, 0x61, 0x00, 0x74, 0x00, 0x65, 0x00, 0x64, -0x00, 0x46, 0x00, 0x6F, 0x00, 0x6E, 0x00, 0x74, 0x00, 0x31, -0x00, 0x2E, 0x00, 0x30, 0x00, 0x47, 0x00, 0x65, 0x00, 0x6E, -0x00, 0x65, 0x00, 0x72, 0x00, 0x61, 0x00, 0x74, 0x00, 0x65, -0x00, 0x64, 0x00, 0x46, 0x00, 0x6F, 0x00, 0x6E, 0x00, 0x74, -0x00, 0x04, 0x02, 0x22, 0x01, 0x90, 0x00, 0x05, 0x00, 0x08, -0x02, 0x8A, 0x02, 0x58, 0x00, 0x00, 0x00, 0x4B, 0x02, 0x8A, -0x02, 0x58, 0x00, 0x00, 0x01, 0x5E, 0x00, 0x32, 0x01, 0x14, -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, -0x00, 0x00, 0xA0, 0x00, 0x02, 0xEF, 0x00, 0x00, 0x20, 0x5B, -0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x69, 0x74, -0x2A, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xFF, 0x19, 0x04, 0x2E, -0xFE, 0x89, 0x00, 0x00, 0x04, 0x2E, 0x01, 0x77, 0x20, 0x00, -0x00, 0x97, 0x00, 0x00, 0x00, 0x00, 0x01, 0xCC, 0x02, 0xBC, -0x00, 0x00, 0x00, 0x20, 0x00, 0x02, 0x00, 0x03, 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, 0xB8, 0x01, -0xFF, 0x85, 0xB0, 0x04, 0x8D, 0x00, }; - -#endif diff --git a/src/fonts/noto-sans.h b/src/fonts/noto-sans.h deleted file mode 100644 index d5b16e3..0000000 --- a/src/fonts/noto-sans.h +++ /dev/null @@ -1,1797 +0,0 @@ -// SPDX-FileCopyrightText: 2023 ThingPulse Ltd., https://thingpulse.com -// SPDX-License-Identifier: MIT - -// This was created as per https://github.com/takkaO/OpenFontRender/blob/master/examples/TFT_eSPI/load_from_binary/load_from_binary.ino#L1 -// The generated font supports the following characters; basic ASCII, accented chars from extended ASCII plus €: -// !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~€ÇüéâäàåçêëèïîìÄÅɧÆôöòûùÿÖÜ¢£¥₧ƒáíóúñѰ∞ - -#ifndef _NOTOSANSTTF_H_ -#define _NOTOSANSTTF_H_ - -const unsigned char notosans[17836] = { -0x00, 0x01, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x80, 0x00, 0x03, -0x00, 0x30, 0x63, 0x6D, 0x61, 0x70, 0x7C, 0xEA, 0x97, 0x47, -0x00, 0x00, 0x00, 0xBC, 0x00, 0x00, 0x12, 0xEE, 0x67, 0x6C, -0x79, 0x66, 0x3C, 0xA3, 0xF9, 0x63, 0x00, 0x00, 0x13, 0xAC, -0x00, 0x00, 0x2A, 0x3A, 0x68, 0x65, 0x61, 0x64, 0x28, 0x97, -0x68, 0xCF, 0x00, 0x00, 0x3D, 0xE8, 0x00, 0x00, 0x00, 0x36, -0x68, 0x68, 0x65, 0x61, 0x03, 0x49, 0x04, 0x22, 0x00, 0x00, -0x3E, 0x20, 0x00, 0x00, 0x00, 0x24, 0x68, 0x6D, 0x74, 0x78, -0x65, 0x54, 0x21, 0x2E, 0x00, 0x00, 0x3E, 0x44, 0x00, 0x00, -0x02, 0x98, 0x6C, 0x6F, 0x63, 0x61, 0x00, 0x0E, 0xE4, 0x47, -0x00, 0x00, 0x40, 0xDC, 0x00, 0x00, 0x02, 0x9C, 0x6D, 0x61, -0x78, 0x70, 0x07, 0xA4, 0x11, 0x1F, 0x00, 0x00, 0x43, 0x78, -0x00, 0x00, 0x00, 0x20, 0x6E, 0x61, 0x6D, 0x65, 0x7B, 0xB1, -0x4F, 0xE5, 0x00, 0x00, 0x43, 0x98, 0x00, 0x00, 0x01, 0x8C, -0x4F, 0x53, 0x2F, 0x32, 0x0E, 0x5D, 0x86, 0xE0, 0x00, 0x00, -0x45, 0x24, 0x00, 0x00, 0x00, 0x60, 0x70, 0x6F, 0x73, 0x74, -0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x45, 0x84, 0x00, 0x00, -0x00, 0x20, 0x70, 0x72, 0x65, 0x70, 0x68, 0x06, 0x8C, 0x85, -0x00, 0x00, 0x45, 0xA4, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, -0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x1C, -0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x06, 0x62, 0x00, 0x03, -0x00, 0x01, 0x00, 0x00, 0x0C, 0xA8, 0x00, 0x04, 0x06, 0x46, -0x00, 0x00, 0x01, 0x3E, 0x01, 0x00, 0x00, 0x07, 0x00, 0x3E, -0x00, 0x21, 0x00, 0x22, 0x00, 0x23, 0x00, 0x24, 0x00, 0x25, -0x00, 0x26, 0x00, 0x27, 0x00, 0x28, 0x00, 0x29, 0x00, 0x2A, -0x00, 0x2B, 0x00, 0x2C, 0x00, 0x2D, 0x00, 0x2E, 0x00, 0x2F, -0x00, 0x30, 0x00, 0x31, 0x00, 0x32, 0x00, 0x33, 0x00, 0x34, -0x00, 0x35, 0x00, 0x36, 0x00, 0x37, 0x00, 0x38, 0x00, 0x39, -0x00, 0x3A, 0x00, 0x3B, 0x00, 0x3C, 0x00, 0x3D, 0x00, 0x3E, -0x00, 0x3F, 0x00, 0x40, 0x00, 0x41, 0x00, 0x42, 0x00, 0x43, -0x00, 0x44, 0x00, 0x45, 0x00, 0x46, 0x00, 0x47, 0x00, 0x48, -0x00, 0x49, 0x00, 0x4A, 0x00, 0x4B, 0x00, 0x4C, 0x00, 0x4D, -0x00, 0x4E, 0x00, 0x4F, 0x00, 0x50, 0x00, 0x51, 0x00, 0x52, -0x00, 0x53, 0x00, 0x54, 0x00, 0x55, 0x00, 0x56, 0x00, 0x57, -0x00, 0x58, 0x00, 0x59, 0x00, 0x5A, 0x00, 0x5B, 0x00, 0x5C, -0x00, 0x5D, 0x00, 0x5E, 0x00, 0x5F, 0x00, 0x60, 0x00, 0x61, -0x00, 0x62, 0x00, 0x63, 0x00, 0x64, 0x00, 0x65, 0x00, 0x66, -0x00, 0x67, 0x00, 0x68, 0x00, 0x69, 0x00, 0x6A, 0x00, 0x6B, -0x00, 0x6C, 0x00, 0x6D, 0x00, 0x6E, 0x00, 0x6F, 0x00, 0x70, -0x00, 0x71, 0x00, 0x72, 0x00, 0x73, 0x00, 0x74, 0x00, 0x75, -0x00, 0x76, 0x00, 0x77, 0x00, 0x78, 0x00, 0x79, 0x00, 0x7A, -0x00, 0x7B, 0x00, 0x7C, 0x00, 0x7D, 0x00, 0x7E, 0x00, 0xC0, -0x00, 0xC1, 0x00, 0xC2, 0x00, 0xC3, 0x00, 0xC4, 0x00, 0xC5, -0x00, 0xC6, 0x00, 0xC7, 0x00, 0xC8, 0x00, 0xC9, 0x00, 0xCA, -0x00, 0xCB, 0x00, 0xCC, 0x00, 0xCD, 0x00, 0xCE, 0x00, 0xCF, -0x00, 0xD0, 0x00, 0xD1, 0x00, 0xD2, 0x00, 0xD3, 0x00, 0xD4, -0x00, 0xD5, 0x00, 0xD6, 0x00, 0xD7, 0x00, 0xD8, 0x00, 0xD9, -0x00, 0xDA, 0x00, 0xDB, 0x00, 0xDC, 0x00, 0xDD, 0x00, 0xDE, -0x00, 0xDF, 0x00, 0xE0, 0x00, 0xE1, 0x00, 0xE2, 0x00, 0xE3, -0x00, 0xE4, 0x00, 0xE5, 0x00, 0xE6, 0x00, 0xE7, 0x00, 0xE8, -0x00, 0xE9, 0x00, 0xEA, 0x00, 0xEB, 0x00, 0xEC, 0x00, 0xED, -0x00, 0xEE, 0x00, 0xEF, 0x00, 0xF0, 0x00, 0xF1, 0x00, 0xF2, -0x00, 0xF3, 0x00, 0xF4, 0x00, 0xF5, 0x00, 0xF6, 0x00, 0xF7, -0x00, 0xF8, 0x00, 0xF9, 0x00, 0xFA, 0x00, 0xFB, 0x00, 0xFC, -0x00, 0xFD, 0x00, 0xFE, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0x00, -0x00, 0x21, 0x00, 0x22, 0x00, 0x23, 0x00, 0x24, 0x00, 0x25, -0x00, 0x26, 0x00, 0x27, 0x00, 0x28, 0x00, 0x29, 0x00, 0x2A, -0x00, 0x2B, 0x00, 0x2C, 0x00, 0x2D, 0x00, 0x2E, 0x00, 0x2F, -0x00, 0x30, 0x00, 0x31, 0x00, 0x32, 0x00, 0x33, 0x00, 0x34, -0x00, 0x35, 0x00, 0x36, 0x00, 0x37, 0x00, 0x38, 0x00, 0x39, -0x00, 0x3A, 0x00, 0x3B, 0x00, 0x3C, 0x00, 0x3D, 0x00, 0x3E, -0x00, 0x3F, 0x00, 0x40, 0x00, 0x41, 0x00, 0x42, 0x00, 0x43, -0x00, 0x44, 0x00, 0x45, 0x00, 0x46, 0x00, 0x47, 0x00, 0x48, -0x00, 0x49, 0x00, 0x4A, 0x00, 0x4B, 0x00, 0x4C, 0x00, 0x4D, -0x00, 0x4E, 0x00, 0x4F, 0x00, 0x50, 0x00, 0x51, 0x00, 0x52, -0x00, 0x53, 0x00, 0x54, 0x00, 0x55, 0x00, 0x56, 0x00, 0x57, -0x00, 0x58, 0x00, 0x59, 0x00, 0x5A, 0x00, 0x5B, 0x00, 0x5C, -0x00, 0x5D, 0x00, 0x5E, 0x00, 0x5F, 0x00, 0x60, 0x00, 0x61, -0x00, 0x62, 0x00, 0x63, 0x00, 0x64, 0x00, 0x65, 0x00, 0x66, -0x00, 0x67, 0x00, 0x68, 0x00, 0x69, 0x00, 0x6A, 0x00, 0x6B, -0x00, 0x6C, 0x00, 0x6D, 0x00, 0x6E, 0x00, 0x6F, 0x00, 0x70, -0x00, 0x71, 0x00, 0x72, 0x00, 0x73, 0x00, 0x74, 0x00, 0x75, -0x00, 0x76, 0x00, 0x77, 0x00, 0x78, 0x00, 0x79, 0x00, 0x7A, -0x00, 0x7B, 0x00, 0x7C, 0x00, 0x7D, 0x00, 0x7E, 0x00, 0xC0, -0x00, 0xC1, 0x00, 0xC2, 0x00, 0xC3, 0x00, 0xC4, 0x00, 0xC5, -0x00, 0xC6, 0x00, 0xC7, 0x00, 0xC8, 0x00, 0xC9, 0x00, 0xCA, -0x00, 0xCB, 0x00, 0xCC, 0x00, 0xCD, 0x00, 0xCE, 0x00, 0xCF, -0x00, 0xD0, 0x00, 0xD1, 0x00, 0xD2, 0x00, 0xD3, 0x00, 0xD4, -0x00, 0xD5, 0x00, 0xD6, 0x00, 0xD7, 0x00, 0xD8, 0x00, 0xD9, -0x00, 0xDA, 0x00, 0xDB, 0x00, 0xDC, 0x00, 0xDD, 0x00, 0xDE, -0x00, 0xDF, 0x00, 0xE0, 0x00, 0xE1, 0x00, 0xE2, 0x00, 0xE3, -0x00, 0xE4, 0x00, 0xE5, 0x00, 0xE6, 0x00, 0xE7, 0x00, 0xE8, -0x00, 0xE9, 0x00, 0xEA, 0x00, 0xEB, 0x00, 0xEC, 0x00, 0xED, -0x00, 0xEE, 0x00, 0xEF, 0x00, 0xF0, 0x00, 0xF1, 0x00, 0xF2, -0x00, 0xF3, 0x00, 0xF4, 0x00, 0xF5, 0x00, 0xF6, 0x00, 0xF7, -0x00, 0xF8, 0x00, 0xF9, 0x00, 0xFA, 0x00, 0xFB, 0x00, 0xFC, -0x00, 0xFD, 0x00, 0xFE, 0x00, 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, 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, -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, 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, 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, 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, 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, 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, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x00, 0x01, 0x00, 0x02, 0x00, 0x03, -0x00, 0x04, 0x00, 0x05, 0x00, 0x06, 0x00, 0x07, 0x00, 0x08, -0x00, 0x09, 0x00, 0x0A, 0x00, 0x0B, 0x00, 0x0C, 0x00, 0x0D, -0x00, 0x0E, 0x00, 0x0F, 0x00, 0x10, 0x00, 0x11, 0x00, 0x12, -0x00, 0x13, 0x00, 0x14, 0x00, 0x15, 0x00, 0x16, 0x00, 0x17, -0x00, 0x18, 0x00, 0x19, 0x00, 0x1A, 0x00, 0x1B, 0x00, 0x1C, -0x00, 0x1D, 0x00, 0x1E, 0x00, 0x1F, 0x00, 0x20, 0x00, 0x21, -0x00, 0x22, 0x00, 0x23, 0x00, 0x24, 0x00, 0x25, 0x00, 0x26, -0x00, 0x27, 0x00, 0x28, 0x00, 0x29, 0x00, 0x2A, 0x00, 0x2B, -0x00, 0x2C, 0x00, 0x2D, 0x00, 0x2E, 0x00, 0x2F, 0x00, 0x30, -0x00, 0x31, 0x00, 0x32, 0x00, 0x33, 0x00, 0x34, 0x00, 0x35, -0x00, 0x36, 0x00, 0x37, 0x00, 0x38, 0x00, 0x39, 0x00, 0x3A, -0x00, 0x3B, 0x00, 0x3C, 0x00, 0x3D, 0x00, 0x3E, 0x00, 0x3F, -0x00, 0x40, 0x00, 0x41, 0x00, 0x42, 0x00, 0x43, 0x00, 0x44, -0x00, 0x45, 0x00, 0x46, 0x00, 0x47, 0x00, 0x48, 0x00, 0x49, -0x00, 0x4A, 0x00, 0x4B, 0x00, 0x4C, 0x00, 0x4D, 0x00, 0x4E, -0x00, 0x4F, 0x00, 0x50, 0x00, 0x51, 0x00, 0x52, 0x00, 0x53, -0x00, 0x54, 0x00, 0x55, 0x00, 0x56, 0x00, 0x57, 0x00, 0x58, -0x00, 0x59, 0x00, 0x5A, 0x00, 0x5B, 0x00, 0x5C, 0x00, 0x5D, -0x00, 0x5E, 0x00, 0x5F, 0x00, 0x61, 0x00, 0x63, 0x00, 0x65, -0x00, 0x67, 0x00, 0x69, 0x00, 0x6A, 0x00, 0x6C, 0x00, 0x6D, -0x00, 0x6E, 0x00, 0x6F, 0x00, 0x70, 0x00, 0x71, 0x00, 0x72, -0x00, 0x73, 0x00, 0x74, 0x00, 0x75, 0x00, 0x76, 0x00, 0x77, -0x00, 0x78, 0x00, 0x79, 0x00, 0x7A, 0x00, 0x7B, 0x00, 0x7C, -0x00, 0x7D, 0x00, 0x7E, 0x00, 0x7F, 0x00, 0x80, 0x00, 0x81, -0x00, 0x82, 0x00, 0x83, 0x00, 0x84, 0x00, 0x85, 0x00, 0x86, -0x00, 0x87, 0x00, 0x88, 0x00, 0x89, 0x00, 0x8A, 0x00, 0x8B, -0x00, 0x8C, 0x00, 0x8D, 0x00, 0x8E, 0x00, 0x8F, 0x00, 0x90, -0x00, 0x92, 0x00, 0x93, 0x00, 0x94, 0x00, 0x95, 0x00, 0x96, -0x00, 0x97, 0x00, 0x98, 0x00, 0x99, 0x00, 0x9A, 0x00, 0x9B, -0x00, 0x9C, 0x00, 0x9D, 0x00, 0x9E, 0x00, 0x9F, 0x00, 0xA0, -0x00, 0xA1, 0x00, 0xA2, 0x00, 0xA3, 0x00, 0xA4, 0x00, 0xA5, -0x00, 0x00, 0x00, 0x04, 0x06, 0x46, 0x00, 0x00, 0x01, 0x3E, -0x01, 0x00, 0x00, 0x07, 0x00, 0x3E, 0x00, 0x21, 0x00, 0x22, -0x00, 0x23, 0x00, 0x24, 0x00, 0x25, 0x00, 0x26, 0x00, 0x27, -0x00, 0x28, 0x00, 0x29, 0x00, 0x2A, 0x00, 0x2B, 0x00, 0x2C, -0x00, 0x2D, 0x00, 0x2E, 0x00, 0x2F, 0x00, 0x30, 0x00, 0x31, -0x00, 0x32, 0x00, 0x33, 0x00, 0x34, 0x00, 0x35, 0x00, 0x36, -0x00, 0x37, 0x00, 0x38, 0x00, 0x39, 0x00, 0x3A, 0x00, 0x3B, -0x00, 0x3C, 0x00, 0x3D, 0x00, 0x3E, 0x00, 0x3F, 0x00, 0x40, -0x00, 0x41, 0x00, 0x42, 0x00, 0x43, 0x00, 0x44, 0x00, 0x45, -0x00, 0x46, 0x00, 0x47, 0x00, 0x48, 0x00, 0x49, 0x00, 0x4A, -0x00, 0x4B, 0x00, 0x4C, 0x00, 0x4D, 0x00, 0x4E, 0x00, 0x4F, -0x00, 0x50, 0x00, 0x51, 0x00, 0x52, 0x00, 0x53, 0x00, 0x54, -0x00, 0x55, 0x00, 0x56, 0x00, 0x57, 0x00, 0x58, 0x00, 0x59, -0x00, 0x5A, 0x00, 0x5B, 0x00, 0x5C, 0x00, 0x5D, 0x00, 0x5E, -0x00, 0x5F, 0x00, 0x60, 0x00, 0x61, 0x00, 0x62, 0x00, 0x63, -0x00, 0x64, 0x00, 0x65, 0x00, 0x66, 0x00, 0x67, 0x00, 0x68, -0x00, 0x69, 0x00, 0x6A, 0x00, 0x6B, 0x00, 0x6C, 0x00, 0x6D, -0x00, 0x6E, 0x00, 0x6F, 0x00, 0x70, 0x00, 0x71, 0x00, 0x72, -0x00, 0x73, 0x00, 0x74, 0x00, 0x75, 0x00, 0x76, 0x00, 0x77, -0x00, 0x78, 0x00, 0x79, 0x00, 0x7A, 0x00, 0x7B, 0x00, 0x7C, -0x00, 0x7D, 0x00, 0x7E, 0x00, 0xC0, 0x00, 0xC1, 0x00, 0xC2, -0x00, 0xC3, 0x00, 0xC4, 0x00, 0xC5, 0x00, 0xC6, 0x00, 0xC7, -0x00, 0xC8, 0x00, 0xC9, 0x00, 0xCA, 0x00, 0xCB, 0x00, 0xCC, -0x00, 0xCD, 0x00, 0xCE, 0x00, 0xCF, 0x00, 0xD0, 0x00, 0xD1, -0x00, 0xD2, 0x00, 0xD3, 0x00, 0xD4, 0x00, 0xD5, 0x00, 0xD6, -0x00, 0xD7, 0x00, 0xD8, 0x00, 0xD9, 0x00, 0xDA, 0x00, 0xDB, -0x00, 0xDC, 0x00, 0xDD, 0x00, 0xDE, 0x00, 0xDF, 0x00, 0xE0, -0x00, 0xE1, 0x00, 0xE2, 0x00, 0xE3, 0x00, 0xE4, 0x00, 0xE5, -0x00, 0xE6, 0x00, 0xE7, 0x00, 0xE8, 0x00, 0xE9, 0x00, 0xEA, -0x00, 0xEB, 0x00, 0xEC, 0x00, 0xED, 0x00, 0xEE, 0x00, 0xEF, -0x00, 0xF0, 0x00, 0xF1, 0x00, 0xF2, 0x00, 0xF3, 0x00, 0xF4, -0x00, 0xF5, 0x00, 0xF6, 0x00, 0xF7, 0x00, 0xF8, 0x00, 0xF9, -0x00, 0xFA, 0x00, 0xFB, 0x00, 0xFC, 0x00, 0xFD, 0x00, 0xFE, -0x00, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x21, 0x00, 0x22, -0x00, 0x23, 0x00, 0x24, 0x00, 0x25, 0x00, 0x26, 0x00, 0x27, -0x00, 0x28, 0x00, 0x29, 0x00, 0x2A, 0x00, 0x2B, 0x00, 0x2C, -0x00, 0x2D, 0x00, 0x2E, 0x00, 0x2F, 0x00, 0x30, 0x00, 0x31, -0x00, 0x32, 0x00, 0x33, 0x00, 0x34, 0x00, 0x35, 0x00, 0x36, -0x00, 0x37, 0x00, 0x38, 0x00, 0x39, 0x00, 0x3A, 0x00, 0x3B, -0x00, 0x3C, 0x00, 0x3D, 0x00, 0x3E, 0x00, 0x3F, 0x00, 0x40, -0x00, 0x41, 0x00, 0x42, 0x00, 0x43, 0x00, 0x44, 0x00, 0x45, -0x00, 0x46, 0x00, 0x47, 0x00, 0x48, 0x00, 0x49, 0x00, 0x4A, -0x00, 0x4B, 0x00, 0x4C, 0x00, 0x4D, 0x00, 0x4E, 0x00, 0x4F, -0x00, 0x50, 0x00, 0x51, 0x00, 0x52, 0x00, 0x53, 0x00, 0x54, -0x00, 0x55, 0x00, 0x56, 0x00, 0x57, 0x00, 0x58, 0x00, 0x59, -0x00, 0x5A, 0x00, 0x5B, 0x00, 0x5C, 0x00, 0x5D, 0x00, 0x5E, -0x00, 0x5F, 0x00, 0x60, 0x00, 0x61, 0x00, 0x62, 0x00, 0x63, -0x00, 0x64, 0x00, 0x65, 0x00, 0x66, 0x00, 0x67, 0x00, 0x68, -0x00, 0x69, 0x00, 0x6A, 0x00, 0x6B, 0x00, 0x6C, 0x00, 0x6D, -0x00, 0x6E, 0x00, 0x6F, 0x00, 0x70, 0x00, 0x71, 0x00, 0x72, -0x00, 0x73, 0x00, 0x74, 0x00, 0x75, 0x00, 0x76, 0x00, 0x77, -0x00, 0x78, 0x00, 0x79, 0x00, 0x7A, 0x00, 0x7B, 0x00, 0x7C, -0x00, 0x7D, 0x00, 0x7E, 0x00, 0xC0, 0x00, 0xC1, 0x00, 0xC2, -0x00, 0xC3, 0x00, 0xC4, 0x00, 0xC5, 0x00, 0xC6, 0x00, 0xC7, -0x00, 0xC8, 0x00, 0xC9, 0x00, 0xCA, 0x00, 0xCB, 0x00, 0xCC, -0x00, 0xCD, 0x00, 0xCE, 0x00, 0xCF, 0x00, 0xD0, 0x00, 0xD1, -0x00, 0xD2, 0x00, 0xD3, 0x00, 0xD4, 0x00, 0xD5, 0x00, 0xD6, -0x00, 0xD7, 0x00, 0xD8, 0x00, 0xD9, 0x00, 0xDA, 0x00, 0xDB, -0x00, 0xDC, 0x00, 0xDD, 0x00, 0xDE, 0x00, 0xDF, 0x00, 0xE0, -0x00, 0xE1, 0x00, 0xE2, 0x00, 0xE3, 0x00, 0xE4, 0x00, 0xE5, -0x00, 0xE6, 0x00, 0xE7, 0x00, 0xE8, 0x00, 0xE9, 0x00, 0xEA, -0x00, 0xEB, 0x00, 0xEC, 0x00, 0xED, 0x00, 0xEE, 0x00, 0xEF, -0x00, 0xF0, 0x00, 0xF1, 0x00, 0xF2, 0x00, 0xF3, 0x00, 0xF4, -0x00, 0xF5, 0x00, 0xF6, 0x00, 0xF7, 0x00, 0xF8, 0x00, 0xF9, -0x00, 0xFA, 0x00, 0xFB, 0x00, 0xFC, 0x00, 0xFD, 0x00, 0xFE, -0x00, 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, 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, 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, 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, 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, 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, -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, 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, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x00, 0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x04, 0x00, 0x05, -0x00, 0x06, 0x00, 0x07, 0x00, 0x08, 0x00, 0x09, 0x00, 0x0A, -0x00, 0x0B, 0x00, 0x0C, 0x00, 0x0D, 0x00, 0x0E, 0x00, 0x0F, -0x00, 0x10, 0x00, 0x11, 0x00, 0x12, 0x00, 0x13, 0x00, 0x14, -0x00, 0x15, 0x00, 0x16, 0x00, 0x17, 0x00, 0x18, 0x00, 0x19, -0x00, 0x1A, 0x00, 0x1B, 0x00, 0x1C, 0x00, 0x1D, 0x00, 0x1E, -0x00, 0x1F, 0x00, 0x20, 0x00, 0x21, 0x00, 0x22, 0x00, 0x23, -0x00, 0x24, 0x00, 0x25, 0x00, 0x26, 0x00, 0x27, 0x00, 0x28, -0x00, 0x29, 0x00, 0x2A, 0x00, 0x2B, 0x00, 0x2C, 0x00, 0x2D, -0x00, 0x2E, 0x00, 0x2F, 0x00, 0x30, 0x00, 0x31, 0x00, 0x32, -0x00, 0x33, 0x00, 0x34, 0x00, 0x35, 0x00, 0x36, 0x00, 0x37, -0x00, 0x38, 0x00, 0x39, 0x00, 0x3A, 0x00, 0x3B, 0x00, 0x3C, -0x00, 0x3D, 0x00, 0x3E, 0x00, 0x3F, 0x00, 0x40, 0x00, 0x41, -0x00, 0x42, 0x00, 0x43, 0x00, 0x44, 0x00, 0x45, 0x00, 0x46, -0x00, 0x47, 0x00, 0x48, 0x00, 0x49, 0x00, 0x4A, 0x00, 0x4B, -0x00, 0x4C, 0x00, 0x4D, 0x00, 0x4E, 0x00, 0x4F, 0x00, 0x50, -0x00, 0x51, 0x00, 0x52, 0x00, 0x53, 0x00, 0x54, 0x00, 0x55, -0x00, 0x56, 0x00, 0x57, 0x00, 0x58, 0x00, 0x59, 0x00, 0x5A, -0x00, 0x5B, 0x00, 0x5C, 0x00, 0x5D, 0x00, 0x5E, 0x00, 0x5F, -0x00, 0x61, 0x00, 0x63, 0x00, 0x65, 0x00, 0x67, 0x00, 0x69, -0x00, 0x6A, 0x00, 0x6C, 0x00, 0x6D, 0x00, 0x6E, 0x00, 0x6F, -0x00, 0x70, 0x00, 0x71, 0x00, 0x72, 0x00, 0x73, 0x00, 0x74, -0x00, 0x75, 0x00, 0x76, 0x00, 0x77, 0x00, 0x78, 0x00, 0x79, -0x00, 0x7A, 0x00, 0x7B, 0x00, 0x7C, 0x00, 0x7D, 0x00, 0x7E, -0x00, 0x7F, 0x00, 0x80, 0x00, 0x81, 0x00, 0x82, 0x00, 0x83, -0x00, 0x84, 0x00, 0x85, 0x00, 0x86, 0x00, 0x87, 0x00, 0x88, -0x00, 0x89, 0x00, 0x8A, 0x00, 0x8B, 0x00, 0x8C, 0x00, 0x8D, -0x00, 0x8E, 0x00, 0x8F, 0x00, 0x90, 0x00, 0x92, 0x00, 0x93, -0x00, 0x94, 0x00, 0x95, 0x00, 0x96, 0x00, 0x97, 0x00, 0x98, -0x00, 0x99, 0x00, 0x9A, 0x00, 0x9B, 0x00, 0x9C, 0x00, 0x9D, -0x00, 0x9E, 0x00, 0x9F, 0x00, 0xA0, 0x00, 0xA1, 0x00, 0xA2, -0x00, 0xA3, 0x00, 0xA4, 0x00, 0xA5, 0x00, 0x00, 0x00, 0x04, -0x06, 0x46, 0x00, 0x00, 0x01, 0x3E, 0x01, 0x00, 0x00, 0x07, -0x00, 0x3E, 0x00, 0x21, 0x00, 0x22, 0x00, 0x23, 0x00, 0x24, -0x00, 0x25, 0x00, 0x26, 0x00, 0x27, 0x00, 0x28, 0x00, 0x29, -0x00, 0x2A, 0x00, 0x2B, 0x00, 0x2C, 0x00, 0x2D, 0x00, 0x2E, -0x00, 0x2F, 0x00, 0x30, 0x00, 0x31, 0x00, 0x32, 0x00, 0x33, -0x00, 0x34, 0x00, 0x35, 0x00, 0x36, 0x00, 0x37, 0x00, 0x38, -0x00, 0x39, 0x00, 0x3A, 0x00, 0x3B, 0x00, 0x3C, 0x00, 0x3D, -0x00, 0x3E, 0x00, 0x3F, 0x00, 0x40, 0x00, 0x41, 0x00, 0x42, -0x00, 0x43, 0x00, 0x44, 0x00, 0x45, 0x00, 0x46, 0x00, 0x47, -0x00, 0x48, 0x00, 0x49, 0x00, 0x4A, 0x00, 0x4B, 0x00, 0x4C, -0x00, 0x4D, 0x00, 0x4E, 0x00, 0x4F, 0x00, 0x50, 0x00, 0x51, -0x00, 0x52, 0x00, 0x53, 0x00, 0x54, 0x00, 0x55, 0x00, 0x56, -0x00, 0x57, 0x00, 0x58, 0x00, 0x59, 0x00, 0x5A, 0x00, 0x5B, -0x00, 0x5C, 0x00, 0x5D, 0x00, 0x5E, 0x00, 0x5F, 0x00, 0x60, -0x00, 0x61, 0x00, 0x62, 0x00, 0x63, 0x00, 0x64, 0x00, 0x65, -0x00, 0x66, 0x00, 0x67, 0x00, 0x68, 0x00, 0x69, 0x00, 0x6A, -0x00, 0x6B, 0x00, 0x6C, 0x00, 0x6D, 0x00, 0x6E, 0x00, 0x6F, -0x00, 0x70, 0x00, 0x71, 0x00, 0x72, 0x00, 0x73, 0x00, 0x74, -0x00, 0x75, 0x00, 0x76, 0x00, 0x77, 0x00, 0x78, 0x00, 0x79, -0x00, 0x7A, 0x00, 0x7B, 0x00, 0x7C, 0x00, 0x7D, 0x00, 0x7E, -0x00, 0xC0, 0x00, 0xC1, 0x00, 0xC2, 0x00, 0xC3, 0x00, 0xC4, -0x00, 0xC5, 0x00, 0xC6, 0x00, 0xC7, 0x00, 0xC8, 0x00, 0xC9, -0x00, 0xCA, 0x00, 0xCB, 0x00, 0xCC, 0x00, 0xCD, 0x00, 0xCE, -0x00, 0xCF, 0x00, 0xD0, 0x00, 0xD1, 0x00, 0xD2, 0x00, 0xD3, -0x00, 0xD4, 0x00, 0xD5, 0x00, 0xD6, 0x00, 0xD7, 0x00, 0xD8, -0x00, 0xD9, 0x00, 0xDA, 0x00, 0xDB, 0x00, 0xDC, 0x00, 0xDD, -0x00, 0xDE, 0x00, 0xDF, 0x00, 0xE0, 0x00, 0xE1, 0x00, 0xE2, -0x00, 0xE3, 0x00, 0xE4, 0x00, 0xE5, 0x00, 0xE6, 0x00, 0xE7, -0x00, 0xE8, 0x00, 0xE9, 0x00, 0xEA, 0x00, 0xEB, 0x00, 0xEC, -0x00, 0xED, 0x00, 0xEE, 0x00, 0xEF, 0x00, 0xF0, 0x00, 0xF1, -0x00, 0xF2, 0x00, 0xF3, 0x00, 0xF4, 0x00, 0xF5, 0x00, 0xF6, -0x00, 0xF7, 0x00, 0xF8, 0x00, 0xF9, 0x00, 0xFA, 0x00, 0xFB, -0x00, 0xFC, 0x00, 0xFD, 0x00, 0xFE, 0x00, 0xFF, 0xFF, 0xFF, -0x00, 0x00, 0x00, 0x21, 0x00, 0x22, 0x00, 0x23, 0x00, 0x24, -0x00, 0x25, 0x00, 0x26, 0x00, 0x27, 0x00, 0x28, 0x00, 0x29, -0x00, 0x2A, 0x00, 0x2B, 0x00, 0x2C, 0x00, 0x2D, 0x00, 0x2E, -0x00, 0x2F, 0x00, 0x30, 0x00, 0x31, 0x00, 0x32, 0x00, 0x33, -0x00, 0x34, 0x00, 0x35, 0x00, 0x36, 0x00, 0x37, 0x00, 0x38, -0x00, 0x39, 0x00, 0x3A, 0x00, 0x3B, 0x00, 0x3C, 0x00, 0x3D, -0x00, 0x3E, 0x00, 0x3F, 0x00, 0x40, 0x00, 0x41, 0x00, 0x42, -0x00, 0x43, 0x00, 0x44, 0x00, 0x45, 0x00, 0x46, 0x00, 0x47, -0x00, 0x48, 0x00, 0x49, 0x00, 0x4A, 0x00, 0x4B, 0x00, 0x4C, -0x00, 0x4D, 0x00, 0x4E, 0x00, 0x4F, 0x00, 0x50, 0x00, 0x51, -0x00, 0x52, 0x00, 0x53, 0x00, 0x54, 0x00, 0x55, 0x00, 0x56, -0x00, 0x57, 0x00, 0x58, 0x00, 0x59, 0x00, 0x5A, 0x00, 0x5B, -0x00, 0x5C, 0x00, 0x5D, 0x00, 0x5E, 0x00, 0x5F, 0x00, 0x60, -0x00, 0x61, 0x00, 0x62, 0x00, 0x63, 0x00, 0x64, 0x00, 0x65, -0x00, 0x66, 0x00, 0x67, 0x00, 0x68, 0x00, 0x69, 0x00, 0x6A, -0x00, 0x6B, 0x00, 0x6C, 0x00, 0x6D, 0x00, 0x6E, 0x00, 0x6F, -0x00, 0x70, 0x00, 0x71, 0x00, 0x72, 0x00, 0x73, 0x00, 0x74, -0x00, 0x75, 0x00, 0x76, 0x00, 0x77, 0x00, 0x78, 0x00, 0x79, -0x00, 0x7A, 0x00, 0x7B, 0x00, 0x7C, 0x00, 0x7D, 0x00, 0x7E, -0x00, 0xC0, 0x00, 0xC1, 0x00, 0xC2, 0x00, 0xC3, 0x00, 0xC4, -0x00, 0xC5, 0x00, 0xC6, 0x00, 0xC7, 0x00, 0xC8, 0x00, 0xC9, -0x00, 0xCA, 0x00, 0xCB, 0x00, 0xCC, 0x00, 0xCD, 0x00, 0xCE, -0x00, 0xCF, 0x00, 0xD0, 0x00, 0xD1, 0x00, 0xD2, 0x00, 0xD3, -0x00, 0xD4, 0x00, 0xD5, 0x00, 0xD6, 0x00, 0xD7, 0x00, 0xD8, -0x00, 0xD9, 0x00, 0xDA, 0x00, 0xDB, 0x00, 0xDC, 0x00, 0xDD, -0x00, 0xDE, 0x00, 0xDF, 0x00, 0xE0, 0x00, 0xE1, 0x00, 0xE2, -0x00, 0xE3, 0x00, 0xE4, 0x00, 0xE5, 0x00, 0xE6, 0x00, 0xE7, -0x00, 0xE8, 0x00, 0xE9, 0x00, 0xEA, 0x00, 0xEB, 0x00, 0xEC, -0x00, 0xED, 0x00, 0xEE, 0x00, 0xEF, 0x00, 0xF0, 0x00, 0xF1, -0x00, 0xF2, 0x00, 0xF3, 0x00, 0xF4, 0x00, 0xF5, 0x00, 0xF6, -0x00, 0xF7, 0x00, 0xF8, 0x00, 0xF9, 0x00, 0xFA, 0x00, 0xFB, -0x00, 0xFC, 0x00, 0xFD, 0x00, 0xFE, 0x00, 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, 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, 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, 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, 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, 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, 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, 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, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, -0x01, 0x3E, 0x01, 0x3E, 0x01, 0x3E, 0x00, 0x01, 0x00, 0x02, -0x00, 0x03, 0x00, 0x04, 0x00, 0x05, 0x00, 0x06, 0x00, 0x07, -0x00, 0x08, 0x00, 0x09, 0x00, 0x0A, 0x00, 0x0B, 0x00, 0x0C, -0x00, 0x0D, 0x00, 0x0E, 0x00, 0x0F, 0x00, 0x10, 0x00, 0x11, -0x00, 0x12, 0x00, 0x13, 0x00, 0x14, 0x00, 0x15, 0x00, 0x16, -0x00, 0x17, 0x00, 0x18, 0x00, 0x19, 0x00, 0x1A, 0x00, 0x1B, -0x00, 0x1C, 0x00, 0x1D, 0x00, 0x1E, 0x00, 0x1F, 0x00, 0x20, -0x00, 0x21, 0x00, 0x22, 0x00, 0x23, 0x00, 0x24, 0x00, 0x25, -0x00, 0x26, 0x00, 0x27, 0x00, 0x28, 0x00, 0x29, 0x00, 0x2A, -0x00, 0x2B, 0x00, 0x2C, 0x00, 0x2D, 0x00, 0x2E, 0x00, 0x2F, -0x00, 0x30, 0x00, 0x31, 0x00, 0x32, 0x00, 0x33, 0x00, 0x34, -0x00, 0x35, 0x00, 0x36, 0x00, 0x37, 0x00, 0x38, 0x00, 0x39, -0x00, 0x3A, 0x00, 0x3B, 0x00, 0x3C, 0x00, 0x3D, 0x00, 0x3E, -0x00, 0x3F, 0x00, 0x40, 0x00, 0x41, 0x00, 0x42, 0x00, 0x43, -0x00, 0x44, 0x00, 0x45, 0x00, 0x46, 0x00, 0x47, 0x00, 0x48, -0x00, 0x49, 0x00, 0x4A, 0x00, 0x4B, 0x00, 0x4C, 0x00, 0x4D, -0x00, 0x4E, 0x00, 0x4F, 0x00, 0x50, 0x00, 0x51, 0x00, 0x52, -0x00, 0x53, 0x00, 0x54, 0x00, 0x55, 0x00, 0x56, 0x00, 0x57, -0x00, 0x58, 0x00, 0x59, 0x00, 0x5A, 0x00, 0x5B, 0x00, 0x5C, -0x00, 0x5D, 0x00, 0x5E, 0x00, 0x5F, 0x00, 0x61, 0x00, 0x63, -0x00, 0x65, 0x00, 0x67, 0x00, 0x69, 0x00, 0x6A, 0x00, 0x6C, -0x00, 0x6D, 0x00, 0x6E, 0x00, 0x6F, 0x00, 0x70, 0x00, 0x71, -0x00, 0x72, 0x00, 0x73, 0x00, 0x74, 0x00, 0x75, 0x00, 0x76, -0x00, 0x77, 0x00, 0x78, 0x00, 0x79, 0x00, 0x7A, 0x00, 0x7B, -0x00, 0x7C, 0x00, 0x7D, 0x00, 0x7E, 0x00, 0x7F, 0x00, 0x80, -0x00, 0x81, 0x00, 0x82, 0x00, 0x83, 0x00, 0x84, 0x00, 0x85, -0x00, 0x86, 0x00, 0x87, 0x00, 0x88, 0x00, 0x89, 0x00, 0x8A, -0x00, 0x8B, 0x00, 0x8C, 0x00, 0x8D, 0x00, 0x8E, 0x00, 0x8F, -0x00, 0x90, 0x00, 0x92, 0x00, 0x93, 0x00, 0x94, 0x00, 0x95, -0x00, 0x96, 0x00, 0x97, 0x00, 0x98, 0x00, 0x99, 0x00, 0x9A, -0x00, 0x9B, 0x00, 0x9C, 0x00, 0x9D, 0x00, 0x9E, 0x00, 0x9F, -0x00, 0xA0, 0x00, 0xA1, 0x00, 0xA2, 0x00, 0xA3, 0x00, 0xA4, -0x00, 0xA5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, -0x00, 0x00, 0x02, 0x58, 0x08, 0x00, 0x00, 0x03, 0x00, 0x07, -0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, -0x00, 0x00, 0x00, 0x00, 0x02, 0x58, 0x00, 0x00, 0xFD, 0xAD, -0x02, 0x4E, 0x00, 0x00, 0xFD, 0xB2, 0x00, 0x00, 0x08, 0x00, -0x00, 0x00, 0xF8, 0x00, 0x00, 0x05, 0x00, 0x00, 0x07, 0xF6, -0x00, 0x00, 0x00, 0x02, 0x00, 0x48, 0xFF, 0xF2, 0x00, 0xC4, -0x02, 0xCA, 0x00, 0x03, 0x00, 0x0F, 0x00, 0x00, 0x37, 0x23, -0x03, 0x33, 0x03, 0x34, 0x36, 0x33, 0x32, 0x16, 0x15, 0x14, -0x06, 0x23, 0x22, 0x26, 0xA3, 0x39, 0x19, 0x6B, 0x74, 0x24, -0x1A, 0x19, 0x25, 0x25, 0x19, 0x1A, 0x24, 0xC9, 0x02, 0x01, -0xFD, 0x6C, 0x25, 0x1E, 0x1E, 0x25, 0x24, 0x20, 0x20, 0x00, -0x02, 0x00, 0x41, 0x01, 0xC8, 0x01, 0x57, 0x02, 0xCA, 0x00, -0x03, 0x00, 0x07, 0x00, 0x00, 0x13, 0x03, 0x23, 0x03, 0x21, -0x03, 0x23, 0x03, 0xA0, 0x14, 0x37, 0x14, 0x01, 0x16, 0x14, -0x37, 0x14, 0x02, 0xCA, 0xFE, 0xFE, 0x01, 0x02, 0xFE, 0xFE, -0x01, 0x02, 0x00, 0x02, 0x00, 0x19, 0x00, 0x00, 0x02, 0x6C, -0x02, 0xCA, 0x00, 0x1B, 0x00, 0x1F, 0x00, 0x00, 0x01, 0x07, -0x33, 0x15, 0x23, 0x07, 0x23, 0x37, 0x23, 0x07, 0x23, 0x37, -0x23, 0x35, 0x33, 0x37, 0x23, 0x35, 0x33, 0x37, 0x33, 0x07, -0x33, 0x37, 0x33, 0x07, 0x33, 0x15, 0x05, 0x33, 0x37, 0x23, -0x01, 0xE0, 0x1F, 0x89, 0x96, 0x29, 0x47, 0x29, 0x8F, 0x27, -0x46, 0x26, 0x7E, 0x8B, 0x20, 0x86, 0x92, 0x28, 0x48, 0x28, -0x90, 0x28, 0x45, 0x28, 0x7F, 0xFE, 0x7F, 0x8F, 0x1F, 0x8F, -0x01, 0xB4, 0xA0, 0x43, 0xD1, 0xD1, 0xD1, 0xD1, 0x43, 0xA0, -0x42, 0xD4, 0xD4, 0xD4, 0xD4, 0x42, 0xA0, 0xA0, 0x00, 0x03, -0x00, 0x3E, 0xFF, 0xC6, 0x02, 0x04, 0x02, 0xF7, 0x00, 0x22, -0x00, 0x29, 0x00, 0x30, 0x00, 0x00, 0x37, 0x26, 0x26, 0x27, -0x35, 0x16, 0x16, 0x17, 0x35, 0x26, 0x26, 0x35, 0x34, 0x36, -0x37, 0x35, 0x33, 0x15, 0x16, 0x16, 0x17, 0x07, 0x26, 0x26, -0x27, 0x15, 0x1E, 0x02, 0x15, 0x14, 0x06, 0x07, 0x15, 0x23, -0x11, 0x06, 0x06, 0x15, 0x14, 0x16, 0x17, 0x13, 0x36, 0x36, -0x35, 0x34, 0x26, 0x27, 0xFD, 0x37, 0x68, 0x20, 0x22, 0x6A, -0x33, 0x63, 0x5C, 0x67, 0x58, 0x40, 0x35, 0x57, 0x24, 0x1B, -0x20, 0x4D, 0x28, 0x42, 0x58, 0x2D, 0x68, 0x5F, 0x40, 0x36, -0x33, 0x2D, 0x3C, 0x40, 0x3B, 0x36, 0x30, 0x41, 0x31, 0x01, -0x11, 0x0F, 0x55, 0x10, 0x18, 0x01, 0xCA, 0x1B, 0x52, 0x47, -0x4A, 0x54, 0x05, 0x58, 0x57, 0x01, 0x15, 0x0F, 0x4A, 0x0D, -0x13, 0x03, 0xC9, 0x13, 0x2B, 0x3F, 0x32, 0x46, 0x57, 0x0A, -0x6F, 0x02, 0x8C, 0x04, 0x2A, 0x21, 0x28, 0x2B, 0x0F, 0xFE, -0xE2, 0x06, 0x2B, 0x22, 0x26, 0x27, 0x10, 0x00, 0x05, 0x00, -0x31, 0xFF, 0xF6, 0x03, 0x0E, 0x02, 0xD4, 0x00, 0x0B, 0x00, -0x0F, 0x00, 0x19, 0x00, 0x25, 0x00, 0x2F, 0x00, 0x00, 0x13, -0x32, 0x16, 0x15, 0x14, 0x06, 0x23, 0x22, 0x26, 0x35, 0x34, -0x36, 0x05, 0x01, 0x23, 0x01, 0x05, 0x22, 0x06, 0x15, 0x14, -0x16, 0x33, 0x32, 0x35, 0x34, 0x05, 0x32, 0x16, 0x15, 0x14, -0x06, 0x23, 0x22, 0x26, 0x35, 0x34, 0x36, 0x17, 0x22, 0x06, -0x15, 0x14, 0x16, 0x33, 0x32, 0x35, 0x34, 0xC3, 0x4A, 0x4C, -0x49, 0x4D, 0x47, 0x4B, 0x46, 0x02, 0x15, 0xFE, 0x74, 0x4D, -0x01, 0x8C, 0xFE, 0x84, 0x26, 0x23, 0x23, 0x26, 0x4D, 0x01, -0x68, 0x49, 0x4D, 0x49, 0x4D, 0x47, 0x4B, 0x46, 0x4C, 0x26, -0x23, 0x23, 0x26, 0x4D, 0x02, 0xD4, 0x75, 0x6A, 0x6A, 0x77, -0x77, 0x6A, 0x6A, 0x75, 0x0A, 0xFD, 0x36, 0x02, 0xCA, 0x34, -0x51, 0x50, 0x50, 0x52, 0xA2, 0xA1, 0xE0, 0x75, 0x6A, 0x6A, -0x77, 0x77, 0x6A, 0x6A, 0x75, 0x3F, 0x50, 0x50, 0x51, 0x51, -0xA2, 0xA0, 0x00, 0x03, 0x00, 0x35, 0xFF, 0xF6, 0x02, 0xDA, -0x02, 0xD5, 0x00, 0x1F, 0x00, 0x2B, 0x00, 0x35, 0x00, 0x00, -0x01, 0x32, 0x16, 0x15, 0x14, 0x06, 0x07, 0x17, 0x36, 0x36, -0x37, 0x33, 0x06, 0x06, 0x07, 0x17, 0x23, 0x27, 0x06, 0x06, -0x23, 0x22, 0x26, 0x35, 0x34, 0x36, 0x37, 0x26, 0x26, 0x35, -0x34, 0x36, 0x17, 0x22, 0x06, 0x15, 0x14, 0x16, 0x17, 0x36, -0x36, 0x35, 0x34, 0x26, 0x03, 0x06, 0x06, 0x15, 0x14, 0x16, -0x33, 0x32, 0x36, 0x37, 0x01, 0x30, 0x50, 0x5D, 0x51, 0x3E, -0xC1, 0x1A, 0x21, 0x0B, 0x59, 0x10, 0x30, 0x26, 0x92, 0x77, -0x57, 0x2F, 0x74, 0x53, 0x67, 0x7A, 0x53, 0x47, 0x20, 0x37, -0x63, 0x52, 0x2A, 0x35, 0x26, 0x24, 0x3B, 0x33, 0x30, 0x52, -0x36, 0x3D, 0x4A, 0x3E, 0x40, 0x5C, 0x1F, 0x02, 0xD5, 0x51, -0x49, 0x3F, 0x58, 0x24, 0xBA, 0x1F, 0x51, 0x2F, 0x40, 0x6E, -0x29, 0x8E, 0x54, 0x2A, 0x34, 0x66, 0x5E, 0x4D, 0x5D, 0x28, -0x24, 0x52, 0x37, 0x4A, 0x52, 0x48, 0x2C, 0x27, 0x24, 0x3D, -0x25, 0x22, 0x3D, 0x28, 0x24, 0x2E, 0xFE, 0xC8, 0x20, 0x42, -0x36, 0x37, 0x42, 0x2A, 0x1D, 0x00, 0x01, 0x00, 0x41, 0x01, -0xC8, 0x00, 0xA0, 0x02, 0xCA, 0x00, 0x03, 0x00, 0x00, 0x13, -0x03, 0x23, 0x03, 0xA0, 0x14, 0x37, 0x14, 0x02, 0xCA, 0xFE, -0xFE, 0x01, 0x02, 0x00, 0x01, 0x00, 0x28, 0xFF, 0x62, 0x01, -0x0E, 0x02, 0xCA, 0x00, 0x0D, 0x00, 0x00, 0x13, 0x34, 0x36, -0x37, 0x33, 0x06, 0x06, 0x15, 0x14, 0x16, 0x17, 0x23, 0x26, -0x26, 0x28, 0x47, 0x4C, 0x53, 0x46, 0x47, 0x47, 0x45, 0x52, -0x4C, 0x47, 0x01, 0x12, 0x7A, 0xE3, 0x5B, 0x5E, 0xE2, 0x77, -0x74, 0xDF, 0x5E, 0x58, 0xDF, 0x00, 0x01, 0x00, 0x1E, 0xFF, -0x62, 0x01, 0x04, 0x02, 0xCA, 0x00, 0x0D, 0x00, 0x00, 0x01, -0x14, 0x06, 0x07, 0x23, 0x36, 0x36, 0x35, 0x34, 0x26, 0x27, -0x33, 0x16, 0x16, 0x01, 0x04, 0x47, 0x4C, 0x52, 0x45, 0x47, -0x47, 0x46, 0x53, 0x4C, 0x47, 0x01, 0x12, 0x79, 0xDF, 0x58, -0x5E, 0xDF, 0x74, 0x77, 0xE2, 0x5E, 0x5B, 0xE3, 0x00, 0x01, -0x00, 0x29, 0x01, 0x36, 0x01, 0xFC, 0x02, 0xF8, 0x00, 0x0E, -0x00, 0x00, 0x01, 0x07, 0x37, 0x17, 0x07, 0x17, 0x07, 0x27, -0x07, 0x27, 0x37, 0x27, 0x37, 0x17, 0x27, 0x01, 0x42, 0x14, -0xC0, 0x0E, 0xB8, 0x77, 0x56, 0x55, 0x4D, 0x59, 0x75, 0xB6, -0x0E, 0xBE, 0x15, 0x02, 0xF8, 0xC0, 0x36, 0x5C, 0x0F, 0x9E, -0x2F, 0xAF, 0xAF, 0x2F, 0x9E, 0x0F, 0x5C, 0x36, 0xC0, 0x00, -0x01, 0x00, 0x32, 0x00, 0x6F, 0x02, 0x08, 0x02, 0x53, 0x00, -0x0B, 0x00, 0x00, 0x01, 0x33, 0x15, 0x23, 0x15, 0x23, 0x35, -0x23, 0x35, 0x33, 0x35, 0x33, 0x01, 0x41, 0xC7, 0xC7, 0x48, -0xC7, 0xC7, 0x48, 0x01, 0x84, 0x47, 0xCE, 0xCE, 0x47, 0xCF, -0x00, 0x01, 0x00, 0x29, 0xFF, 0x7F, 0x00, 0xC0, 0x00, 0x74, -0x00, 0x08, 0x00, 0x00, 0x37, 0x06, 0x06, 0x07, 0x23, 0x36, -0x36, 0x37, 0x33, 0xC0, 0x0D, 0x31, 0x18, 0x41, 0x0E, 0x1D, -0x07, 0x5E, 0x69, 0x35, 0x7F, 0x36, 0x39, 0x88, 0x34, 0x00, -0x01, 0x00, 0x28, 0x00, 0xE5, 0x01, 0x1A, 0x01, 0x33, 0x00, -0x03, 0x00, 0x00, 0x37, 0x35, 0x33, 0x15, 0x28, 0xF2, 0xE5, -0x4E, 0x4E, 0x00, 0x01, 0x00, 0x48, 0xFF, 0xF2, 0x00, 0xC4, -0x00, 0x79, 0x00, 0x0B, 0x00, 0x00, 0x37, 0x34, 0x36, 0x33, -0x32, 0x16, 0x15, 0x14, 0x06, 0x23, 0x22, 0x26, 0x48, 0x24, -0x19, 0x1A, 0x25, 0x25, 0x1A, 0x19, 0x24, 0x36, 0x25, 0x1E, -0x1E, 0x25, 0x24, 0x20, 0x20, 0x00, 0x01, 0x00, 0x0A, 0x00, -0x00, 0x01, 0x6A, 0x02, 0xCA, 0x00, 0x03, 0x00, 0x00, 0x01, -0x01, 0x23, 0x01, 0x01, 0x6A, 0xFE, 0xF6, 0x56, 0x01, 0x0A, -0x02, 0xCA, 0xFD, 0x36, 0x02, 0xCA, 0x00, 0x02, 0x00, 0x31, -0xFF, 0xF6, 0x02, 0x0B, 0x02, 0xD5, 0x00, 0x0D, 0x00, 0x19, -0x00, 0x00, 0x01, 0x14, 0x06, 0x06, 0x23, 0x22, 0x26, 0x35, -0x34, 0x36, 0x36, 0x33, 0x32, 0x16, 0x05, 0x14, 0x16, 0x33, -0x32, 0x36, 0x35, 0x34, 0x26, 0x23, 0x22, 0x06, 0x02, 0x0B, -0x30, 0x68, 0x56, 0x79, 0x73, 0x2F, 0x68, 0x55, 0x78, 0x76, -0xFE, 0x7E, 0x43, 0x51, 0x50, 0x45, 0x45, 0x50, 0x51, 0x43, -0x01, 0x66, 0x73, 0xA5, 0x58, 0xC3, 0xAD, 0x74, 0xA4, 0x57, -0xC1, 0xAE, 0x93, 0x92, 0x91, 0x94, 0x92, 0x92, 0x92, 0x00, -0x01, 0x00, 0x59, 0x00, 0x00, 0x01, 0x63, 0x02, 0xCA, 0x00, -0x0C, 0x00, 0x00, 0x21, 0x23, 0x11, 0x34, 0x36, 0x37, 0x06, -0x06, 0x07, 0x07, 0x27, 0x37, 0x33, 0x01, 0x63, 0x56, 0x02, -0x02, 0x10, 0x1A, 0x14, 0x4C, 0x2E, 0xC1, 0x49, 0x01, 0xF3, -0x2B, 0x34, 0x1C, 0x10, 0x16, 0x11, 0x3E, 0x3B, 0x96, 0x00, -0x01, 0x00, 0x30, 0x00, 0x00, 0x02, 0x08, 0x02, 0xD4, 0x00, -0x1B, 0x00, 0x00, 0x21, 0x21, 0x35, 0x37, 0x3E, 0x02, 0x35, -0x34, 0x26, 0x23, 0x22, 0x06, 0x07, 0x27, 0x36, 0x36, 0x33, -0x32, 0x16, 0x15, 0x14, 0x06, 0x06, 0x07, 0x07, 0x15, 0x21, -0x02, 0x08, 0xFE, 0x28, 0xBB, 0x36, 0x4A, 0x26, 0x46, 0x38, -0x34, 0x4F, 0x29, 0x2F, 0x2A, 0x6D, 0x44, 0x64, 0x74, 0x2E, -0x52, 0x37, 0x95, 0x01, 0x69, 0x49, 0xBD, 0x36, 0x54, 0x51, -0x30, 0x3B, 0x3D, 0x24, 0x20, 0x3B, 0x23, 0x31, 0x65, 0x59, -0x38, 0x62, 0x5F, 0x36, 0x93, 0x04, 0x00, 0x01, 0x00, 0x2D, -0xFF, 0xF6, 0x02, 0x03, 0x02, 0xD4, 0x00, 0x2A, 0x00, 0x00, -0x01, 0x14, 0x06, 0x07, 0x15, 0x16, 0x16, 0x15, 0x14, 0x06, -0x06, 0x23, 0x22, 0x26, 0x27, 0x35, 0x16, 0x16, 0x33, 0x32, -0x36, 0x35, 0x34, 0x26, 0x23, 0x23, 0x35, 0x33, 0x32, 0x36, -0x35, 0x34, 0x26, 0x23, 0x22, 0x06, 0x07, 0x27, 0x36, 0x36, -0x33, 0x32, 0x16, 0x01, 0xED, 0x50, 0x44, 0x56, 0x54, 0x3A, -0x79, 0x5F, 0x38, 0x60, 0x2C, 0x2D, 0x68, 0x30, 0x60, 0x55, -0x69, 0x5F, 0x45, 0x46, 0x58, 0x5B, 0x46, 0x3C, 0x3A, 0x52, -0x28, 0x2C, 0x26, 0x71, 0x48, 0x70, 0x6D, 0x02, 0x23, 0x48, -0x55, 0x0E, 0x04, 0x0A, 0x58, 0x47, 0x3E, 0x61, 0x36, 0x11, -0x16, 0x52, 0x16, 0x19, 0x4B, 0x42, 0x43, 0x3B, 0x4B, 0x4A, -0x3D, 0x34, 0x39, 0x22, 0x1A, 0x3C, 0x1E, 0x2C, 0x64, 0x00, -0x02, 0x00, 0x15, 0x00, 0x00, 0x02, 0x28, 0x02, 0xCE, 0x00, -0x0A, 0x00, 0x14, 0x00, 0x00, 0x25, 0x23, 0x15, 0x23, 0x35, -0x21, 0x35, 0x01, 0x33, 0x11, 0x33, 0x27, 0x34, 0x36, 0x37, -0x23, 0x06, 0x06, 0x07, 0x03, 0x21, 0x02, 0x28, 0x68, 0x55, -0xFE, 0xAA, 0x01, 0x50, 0x5B, 0x68, 0xBD, 0x04, 0x01, 0x04, -0x08, 0x18, 0x0B, 0xD6, 0x01, 0x00, 0xA2, 0xA2, 0xA2, 0x4B, -0x01, 0xE1, 0xFE, 0x23, 0xE1, 0x34, 0x49, 0x21, 0x13, 0x2C, -0x0F, 0xFE, 0xCF, 0x00, 0x01, 0x00, 0x3F, 0xFF, 0xF6, 0x02, -0x03, 0x02, 0xCA, 0x00, 0x1E, 0x00, 0x00, 0x01, 0x32, 0x16, -0x15, 0x14, 0x06, 0x23, 0x22, 0x26, 0x27, 0x35, 0x16, 0x16, -0x33, 0x32, 0x36, 0x35, 0x34, 0x26, 0x23, 0x22, 0x06, 0x07, -0x27, 0x13, 0x21, 0x15, 0x21, 0x07, 0x36, 0x36, 0x01, 0x13, -0x6E, 0x82, 0x8D, 0x7E, 0x37, 0x61, 0x21, 0x24, 0x67, 0x2F, -0x4F, 0x61, 0x56, 0x5D, 0x1C, 0x48, 0x16, 0x2C, 0x1B, 0x01, -0x66, 0xFE, 0xE5, 0x11, 0x11, 0x3A, 0x01, 0xB6, 0x6E, 0x64, -0x6F, 0x7F, 0x14, 0x13, 0x53, 0x16, 0x19, 0x4B, 0x4F, 0x46, -0x4B, 0x0A, 0x05, 0x1C, 0x01, 0x51, 0x50, 0xCF, 0x03, 0x08, -0x00, 0x02, 0x00, 0x37, 0xFF, 0xF6, 0x02, 0x0D, 0x02, 0xD4, -0x00, 0x1E, 0x00, 0x2C, 0x00, 0x00, 0x13, 0x34, 0x3E, 0x02, -0x33, 0x32, 0x16, 0x17, 0x15, 0x26, 0x26, 0x23, 0x22, 0x0E, -0x02, 0x07, 0x33, 0x36, 0x36, 0x33, 0x32, 0x16, 0x15, 0x14, -0x06, 0x23, 0x22, 0x26, 0x26, 0x17, 0x32, 0x36, 0x35, 0x34, -0x26, 0x23, 0x22, 0x06, 0x06, 0x15, 0x14, 0x16, 0x16, 0x37, -0x1B, 0x47, 0x80, 0x65, 0x15, 0x33, 0x10, 0x12, 0x2D, 0x17, -0x45, 0x5C, 0x35, 0x18, 0x03, 0x06, 0x17, 0x52, 0x40, 0x5D, -0x72, 0x7B, 0x68, 0x44, 0x6E, 0x41, 0xF2, 0x3F, 0x4E, 0x45, -0x45, 0x2F, 0x46, 0x27, 0x22, 0x44, 0x01, 0x31, 0x4D, 0x95, -0x79, 0x48, 0x04, 0x05, 0x4B, 0x06, 0x06, 0x2E, 0x50, 0x68, -0x3B, 0x23, 0x31, 0x71, 0x68, 0x70, 0x80, 0x44, 0x8C, 0x86, -0x51, 0x55, 0x44, 0x50, 0x27, 0x3C, 0x20, 0x2B, 0x55, 0x37, -0x00, 0x01, 0x00, 0x2C, 0x00, 0x00, 0x02, 0x0B, 0x02, 0xCA, -0x00, 0x06, 0x00, 0x00, 0x33, 0x01, 0x21, 0x35, 0x21, 0x15, -0x01, 0x88, 0x01, 0x25, 0xFE, 0x7F, 0x01, 0xDF, 0xFE, 0xDE, -0x02, 0x7A, 0x50, 0x44, 0xFD, 0x7A, 0x00, 0x03, 0x00, 0x31, -0xFF, 0xF6, 0x02, 0x0A, 0x02, 0xD4, 0x00, 0x1B, 0x00, 0x28, -0x00, 0x35, 0x00, 0x00, 0x01, 0x32, 0x16, 0x15, 0x14, 0x06, -0x06, 0x07, 0x1E, 0x02, 0x15, 0x14, 0x06, 0x23, 0x22, 0x26, -0x35, 0x34, 0x36, 0x36, 0x37, 0x26, 0x26, 0x35, 0x34, 0x36, -0x36, 0x17, 0x22, 0x06, 0x15, 0x14, 0x16, 0x16, 0x17, 0x36, -0x36, 0x35, 0x34, 0x26, 0x03, 0x14, 0x16, 0x33, 0x32, 0x36, -0x35, 0x34, 0x26, 0x27, 0x27, 0x06, 0x06, 0x01, 0x1D, 0x5E, -0x78, 0x25, 0x3E, 0x25, 0x2C, 0x48, 0x2B, 0x7F, 0x6B, 0x73, -0x7C, 0x29, 0x44, 0x27, 0x34, 0x49, 0x38, 0x60, 0x3C, 0x37, -0x47, 0x23, 0x3C, 0x24, 0x34, 0x47, 0x46, 0xCF, 0x4A, 0x4D, -0x49, 0x4D, 0x52, 0x44, 0x10, 0x42, 0x45, 0x02, 0xD4, 0x58, -0x53, 0x2B, 0x40, 0x31, 0x13, 0x15, 0x35, 0x46, 0x31, 0x5A, -0x69, 0x65, 0x5B, 0x31, 0x48, 0x34, 0x12, 0x1E, 0x55, 0x42, -0x37, 0x4B, 0x28, 0x47, 0x35, 0x32, 0x25, 0x32, 0x23, 0x10, -0x16, 0x3E, 0x36, 0x32, 0x35, 0xFE, 0x28, 0x34, 0x45, 0x45, -0x37, 0x34, 0x45, 0x1A, 0x06, 0x1C, 0x49, 0x00, 0x02, 0x00, -0x32, 0xFF, 0xF6, 0x02, 0x08, 0x02, 0xD4, 0x00, 0x1E, 0x00, -0x2C, 0x00, 0x00, 0x01, 0x14, 0x0E, 0x02, 0x23, 0x22, 0x26, -0x27, 0x35, 0x16, 0x33, 0x32, 0x3E, 0x02, 0x37, 0x23, 0x06, -0x06, 0x23, 0x22, 0x26, 0x35, 0x34, 0x36, 0x36, 0x33, 0x32, -0x16, 0x16, 0x27, 0x22, 0x06, 0x15, 0x14, 0x16, 0x33, 0x32, -0x36, 0x36, 0x35, 0x34, 0x26, 0x26, 0x02, 0x08, 0x1B, 0x47, -0x81, 0x65, 0x14, 0x35, 0x11, 0x27, 0x31, 0x46, 0x5B, 0x36, -0x18, 0x02, 0x06, 0x16, 0x53, 0x41, 0x5C, 0x71, 0x39, 0x66, -0x45, 0x44, 0x6E, 0x40, 0xF2, 0x3E, 0x4F, 0x43, 0x46, 0x30, -0x46, 0x27, 0x22, 0x44, 0x01, 0x99, 0x4D, 0x95, 0x79, 0x48, -0x05, 0x05, 0x4B, 0x0D, 0x2E, 0x4F, 0x69, 0x3A, 0x22, 0x31, -0x71, 0x67, 0x4B, 0x6C, 0x3A, 0x45, 0x8B, 0x86, 0x52, 0x54, -0x45, 0x4F, 0x27, 0x3C, 0x20, 0x2B, 0x54, 0x38, 0x00, 0x02, -0x00, 0x48, 0xFF, 0xF2, 0x00, 0xC4, 0x02, 0x26, 0x00, 0x0B, -0x00, 0x17, 0x00, 0x00, 0x13, 0x34, 0x36, 0x33, 0x32, 0x16, -0x15, 0x14, 0x06, 0x23, 0x22, 0x26, 0x11, 0x34, 0x36, 0x33, -0x32, 0x16, 0x15, 0x14, 0x06, 0x23, 0x22, 0x26, 0x48, 0x24, -0x19, 0x1A, 0x25, 0x25, 0x1A, 0x19, 0x24, 0x24, 0x19, 0x1A, -0x25, 0x25, 0x1A, 0x19, 0x24, 0x01, 0xE2, 0x26, 0x1E, 0x1E, -0x26, 0x24, 0x20, 0x20, 0xFE, 0x78, 0x25, 0x1E, 0x1E, 0x25, -0x24, 0x20, 0x20, 0x00, 0x02, 0x00, 0x1F, 0xFF, 0x7F, 0x00, -0xC2, 0x02, 0x26, 0x00, 0x0B, 0x00, 0x15, 0x00, 0x00, 0x13, -0x34, 0x36, 0x33, 0x32, 0x16, 0x15, 0x14, 0x06, 0x23, 0x22, -0x26, 0x13, 0x06, 0x06, 0x07, 0x23, 0x3E, 0x02, 0x37, 0x33, -0x46, 0x24, 0x19, 0x1A, 0x25, 0x25, 0x1A, 0x19, 0x24, 0x71, -0x0D, 0x31, 0x18, 0x42, 0x0A, 0x13, 0x11, 0x05, 0x5E, 0x01, -0xE2, 0x26, 0x1E, 0x1E, 0x26, 0x24, 0x20, 0x20, 0xFE, 0xAB, -0x34, 0x81, 0x35, 0x26, 0x57, 0x55, 0x23, 0x00, 0x01, 0x00, -0x32, 0x00, 0x74, 0x02, 0x09, 0x02, 0x60, 0x00, 0x06, 0x00, -0x00, 0x25, 0x25, 0x35, 0x25, 0x15, 0x05, 0x05, 0x02, 0x09, -0xFE, 0x29, 0x01, 0xD7, 0xFE, 0x87, 0x01, 0x79, 0x74, 0xCF, -0x32, 0xEB, 0x4E, 0xB2, 0x9E, 0x00, 0x02, 0x00, 0x38, 0x00, -0xD9, 0x02, 0x02, 0x01, 0xE7, 0x00, 0x03, 0x00, 0x07, 0x00, -0x00, 0x13, 0x35, 0x21, 0x15, 0x05, 0x35, 0x21, 0x15, 0x38, -0x01, 0xCA, 0xFE, 0x36, 0x01, 0xCA, 0x01, 0xA0, 0x47, 0x47, -0xC7, 0x47, 0x47, 0x00, 0x01, 0x00, 0x32, 0x00, 0x74, 0x02, -0x09, 0x02, 0x60, 0x00, 0x06, 0x00, 0x00, 0x37, 0x25, 0x25, -0x35, 0x05, 0x15, 0x05, 0x32, 0x01, 0x79, 0xFE, 0x87, 0x01, -0xD7, 0xFE, 0x29, 0xC2, 0x9D, 0xB3, 0x4E, 0xEB, 0x32, 0xCF, -0x00, 0x02, 0x00, 0x0C, 0xFF, 0xF2, 0x01, 0x98, 0x02, 0xD4, -0x00, 0x1F, 0x00, 0x2B, 0x00, 0x00, 0x37, 0x34, 0x36, 0x36, -0x37, 0x3E, 0x02, 0x35, 0x34, 0x26, 0x23, 0x22, 0x06, 0x07, -0x27, 0x36, 0x36, 0x33, 0x32, 0x16, 0x15, 0x14, 0x06, 0x06, -0x07, 0x0E, 0x02, 0x15, 0x15, 0x23, 0x07, 0x34, 0x36, 0x33, -0x32, 0x16, 0x15, 0x14, 0x06, 0x23, 0x22, 0x26, 0x8C, 0x0F, -0x25, 0x20, 0x27, 0x2B, 0x12, 0x3E, 0x3B, 0x31, 0x4C, 0x23, -0x1F, 0x28, 0x61, 0x3C, 0x5F, 0x68, 0x1D, 0x35, 0x24, 0x21, -0x23, 0x0C, 0x46, 0x17, 0x23, 0x1B, 0x19, 0x24, 0x24, 0x19, -0x1B, 0x23, 0xE4, 0x26, 0x37, 0x32, 0x1B, 0x21, 0x2C, 0x2A, -0x1E, 0x30, 0x34, 0x19, 0x11, 0x46, 0x15, 0x1C, 0x5E, 0x51, -0x2D, 0x3F, 0x35, 0x1E, 0x1C, 0x2A, 0x29, 0x1D, 0x11, 0x93, -0x25, 0x1E, 0x1E, 0x25, 0x24, 0x20, 0x20, 0x00, 0x02, 0x00, -0x3A, 0xFF, 0xA7, 0x03, 0x49, 0x02, 0xCA, 0x00, 0x3F, 0x00, -0x4D, 0x00, 0x00, 0x01, 0x14, 0x0E, 0x02, 0x23, 0x22, 0x26, -0x27, 0x23, 0x06, 0x06, 0x23, 0x22, 0x26, 0x35, 0x34, 0x36, -0x36, 0x33, 0x32, 0x16, 0x17, 0x07, 0x06, 0x14, 0x15, 0x14, -0x16, 0x33, 0x32, 0x36, 0x36, 0x35, 0x34, 0x26, 0x26, 0x23, -0x22, 0x06, 0x06, 0x15, 0x14, 0x16, 0x33, 0x32, 0x36, 0x37, -0x15, 0x06, 0x06, 0x23, 0x22, 0x26, 0x26, 0x35, 0x34, 0x3E, -0x02, 0x33, 0x32, 0x16, 0x16, 0x05, 0x14, 0x16, 0x33, 0x32, -0x36, 0x37, 0x37, 0x26, 0x26, 0x23, 0x22, 0x06, 0x06, 0x03, -0x49, 0x15, 0x2C, 0x40, 0x2C, 0x2E, 0x35, 0x06, 0x05, 0x12, -0x46, 0x35, 0x4C, 0x53, 0x34, 0x5F, 0x41, 0x2C, 0x55, 0x18, -0x0A, 0x01, 0x25, 0x19, 0x1F, 0x2B, 0x17, 0x4B, 0x83, 0x53, -0x72, 0x9D, 0x51, 0x9C, 0x93, 0x3D, 0x6F, 0x2B, 0x2B, 0x6B, -0x41, 0x76, 0xA8, 0x59, 0x3A, 0x6E, 0x9D, 0x63, 0x68, 0xA2, -0x5D, 0xFE, 0x07, 0x33, 0x2B, 0x38, 0x31, 0x04, 0x06, 0x0D, -0x28, 0x15, 0x31, 0x3C, 0x1A, 0x01, 0x65, 0x2E, 0x58, 0x47, -0x2B, 0x35, 0x22, 0x25, 0x32, 0x66, 0x54, 0x42, 0x65, 0x3A, -0x0F, 0x09, 0xCB, 0x12, 0x0F, 0x03, 0x34, 0x22, 0x33, 0x55, -0x33, 0x5D, 0x81, 0x44, 0x5E, 0xA5, 0x6A, 0x94, 0x9E, 0x1B, -0x10, 0x44, 0x12, 0x17, 0x58, 0xA5, 0x74, 0x5D, 0x9F, 0x75, -0x41, 0x56, 0xA0, 0xAF, 0x40, 0x3A, 0x54, 0x43, 0x7D, 0x04, -0x06, 0x30, 0x4B, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x02, -0x7E, 0x02, 0xCD, 0x00, 0x07, 0x00, 0x11, 0x00, 0x00, 0x21, -0x27, 0x21, 0x07, 0x23, 0x01, 0x33, 0x01, 0x01, 0x2E, 0x02, -0x27, 0x06, 0x06, 0x07, 0x07, 0x33, 0x02, 0x21, 0x56, 0xFE, -0xE5, 0x55, 0x5B, 0x01, 0x17, 0x51, 0x01, 0x16, 0xFE, 0xE2, -0x03, 0x0E, 0x0D, 0x04, 0x07, 0x12, 0x06, 0x51, 0xE2, 0xDD, -0xDD, 0x02, 0xCD, 0xFD, 0x33, 0x02, 0x05, 0x08, 0x2A, 0x2D, -0x0C, 0x1F, 0x3B, 0x11, 0xD8, 0x00, 0x03, 0x00, 0x61, 0x00, -0x00, 0x02, 0x54, 0x02, 0xCA, 0x00, 0x10, 0x00, 0x19, 0x00, -0x22, 0x00, 0x00, 0x01, 0x32, 0x16, 0x15, 0x14, 0x06, 0x07, -0x15, 0x1E, 0x02, 0x15, 0x14, 0x06, 0x23, 0x23, 0x11, 0x13, -0x32, 0x36, 0x35, 0x34, 0x26, 0x23, 0x23, 0x15, 0x15, 0x11, -0x33, 0x32, 0x36, 0x35, 0x34, 0x26, 0x23, 0x01, 0x2D, 0x86, -0x89, 0x46, 0x42, 0x2D, 0x49, 0x2A, 0x85, 0x73, 0xFB, 0xDE, -0x5C, 0x44, 0x53, 0x5B, 0x76, 0x90, 0x5F, 0x4A, 0x4D, 0x63, -0x02, 0xCA, 0x4F, 0x62, 0x3F, 0x53, 0x0C, 0x05, 0x07, 0x26, -0x46, 0x38, 0x61, 0x6A, 0x02, 0xCA, 0xFE, 0xD0, 0x3B, 0x3A, -0x3B, 0x33, 0xE3, 0x4B, 0xFE, 0xFD, 0x4A, 0x3C, 0x38, 0x45, -0x00, 0x01, 0x00, 0x3D, 0xFF, 0xF6, 0x02, 0x59, 0x02, 0xD4, -0x00, 0x1A, 0x00, 0x00, 0x01, 0x22, 0x06, 0x15, 0x14, 0x16, -0x33, 0x32, 0x36, 0x37, 0x15, 0x06, 0x06, 0x23, 0x22, 0x26, -0x26, 0x35, 0x34, 0x36, 0x36, 0x33, 0x32, 0x17, 0x07, 0x26, -0x26, 0x01, 0x93, 0x73, 0x84, 0x7B, 0x7B, 0x2F, 0x54, 0x28, -0x28, 0x55, 0x3B, 0x6D, 0x92, 0x49, 0x4F, 0x9A, 0x6E, 0x71, -0x54, 0x24, 0x21, 0x51, 0x02, 0x85, 0x9A, 0x86, 0x85, 0x9B, -0x10, 0x0C, 0x4E, 0x0F, 0x0E, 0x5A, 0xA6, 0x70, 0x6C, 0xA5, -0x5D, 0x2A, 0x4C, 0x0F, 0x18, 0x00, 0x02, 0x00, 0x61, 0x00, -0x00, 0x02, 0x9D, 0x02, 0xCA, 0x00, 0x09, 0x00, 0x11, 0x00, -0x00, 0x01, 0x14, 0x06, 0x23, 0x23, 0x11, 0x33, 0x32, 0x16, -0x16, 0x07, 0x34, 0x26, 0x23, 0x23, 0x11, 0x33, 0x20, 0x02, -0x9D, 0xC5, 0xB0, 0xC7, 0xDC, 0x6C, 0x9E, 0x56, 0x5F, 0x8D, -0x81, 0x75, 0x61, 0x01, 0x22, 0x01, 0x6C, 0xB5, 0xB7, 0x02, -0xCA, 0x50, 0x9B, 0x76, 0x8F, 0x85, 0xFD, 0xD0, 0x00, 0x01, -0x00, 0x61, 0x00, 0x00, 0x01, 0xF0, 0x02, 0xCA, 0x00, 0x0B, -0x00, 0x00, 0x21, 0x21, 0x11, 0x21, 0x15, 0x21, 0x15, 0x21, -0x15, 0x21, 0x15, 0x21, 0x01, 0xF0, 0xFE, 0x71, 0x01, 0x8F, -0xFE, 0xCB, 0x01, 0x23, 0xFE, 0xDD, 0x01, 0x35, 0x02, 0xCA, -0x4F, 0xDF, 0x4E, 0xFF, 0x00, 0x01, 0x00, 0x61, 0x00, 0x00, -0x01, 0xF0, 0x02, 0xCA, 0x00, 0x09, 0x00, 0x00, 0x33, 0x23, -0x11, 0x21, 0x15, 0x21, 0x15, 0x21, 0x15, 0x21, 0xBB, 0x5A, -0x01, 0x8F, 0xFE, 0xCB, 0x01, 0x22, 0xFE, 0xDE, 0x02, 0xCA, -0x4F, 0xFD, 0x4F, 0x00, 0x01, 0x00, 0x3D, 0xFF, 0xF6, 0x02, -0x8E, 0x02, 0xD4, 0x00, 0x20, 0x00, 0x00, 0x01, 0x33, 0x11, -0x06, 0x06, 0x23, 0x22, 0x26, 0x26, 0x35, 0x34, 0x36, 0x36, -0x33, 0x32, 0x16, 0x17, 0x07, 0x26, 0x26, 0x23, 0x22, 0x06, -0x15, 0x14, 0x16, 0x16, 0x33, 0x32, 0x36, 0x37, 0x35, 0x23, -0x01, 0x97, 0xF7, 0x3A, 0x76, 0x4B, 0x6F, 0x98, 0x4F, 0x58, -0xA5, 0x75, 0x3C, 0x6B, 0x2E, 0x22, 0x26, 0x5F, 0x33, 0x80, -0x8F, 0x37, 0x76, 0x60, 0x2F, 0x42, 0x1B, 0x9D, 0x01, 0x79, -0xFE, 0xA2, 0x13, 0x12, 0x59, 0xA5, 0x71, 0x70, 0xA4, 0x5B, -0x16, 0x14, 0x4E, 0x11, 0x18, 0x9A, 0x86, 0x55, 0x83, 0x49, -0x0A, 0x07, 0xD4, 0x00, 0x01, 0x00, 0x61, 0x00, 0x00, 0x02, -0x83, 0x02, 0xCA, 0x00, 0x0B, 0x00, 0x00, 0x21, 0x23, 0x11, -0x21, 0x11, 0x23, 0x11, 0x33, 0x11, 0x21, 0x11, 0x33, 0x02, -0x83, 0x5A, 0xFE, 0x92, 0x5A, 0x5A, 0x01, 0x6E, 0x5A, 0x01, -0x4D, 0xFE, 0xB3, 0x02, 0xCA, 0xFE, 0xD2, 0x01, 0x2E, 0x00, -0x01, 0x00, 0x28, 0x00, 0x00, 0x01, 0x2A, 0x02, 0xCA, 0x00, -0x0B, 0x00, 0x00, 0x21, 0x21, 0x35, 0x37, 0x11, 0x27, 0x35, -0x21, 0x15, 0x07, 0x11, 0x17, 0x01, 0x2A, 0xFE, 0xFE, 0x54, -0x54, 0x01, 0x02, 0x54, 0x54, 0x34, 0x13, 0x02, 0x3B, 0x14, -0x34, 0x34, 0x14, 0xFD, 0xC5, 0x13, 0x00, 0x01, 0xFF, 0xB2, -0xFF, 0x42, 0x00, 0xB6, 0x02, 0xCA, 0x00, 0x10, 0x00, 0x00, -0x07, 0x22, 0x26, 0x27, 0x35, 0x16, 0x16, 0x33, 0x32, 0x36, -0x36, 0x35, 0x11, 0x33, 0x11, 0x14, 0x06, 0x04, 0x18, 0x24, -0x0E, 0x10, 0x24, 0x14, 0x19, 0x2D, 0x1C, 0x5A, 0x66, 0xBE, -0x07, 0x06, 0x4C, 0x04, 0x06, 0x14, 0x32, 0x2D, 0x02, 0xC6, -0xFD, 0x41, 0x67, 0x62, 0x00, 0x01, 0x00, 0x61, 0x00, 0x00, -0x02, 0x6B, 0x02, 0xCA, 0x00, 0x0E, 0x00, 0x00, 0x21, 0x23, -0x03, 0x07, 0x11, 0x23, 0x11, 0x33, 0x11, 0x36, 0x36, 0x37, -0x37, 0x33, 0x01, 0x02, 0x6B, 0x6A, 0xFD, 0x49, 0x5A, 0x5A, -0x1E, 0x3E, 0x1F, 0xC1, 0x69, 0xFE, 0xE5, 0x01, 0x55, 0x40, -0xFE, 0xEB, 0x02, 0xCA, 0xFE, 0xA0, 0x22, 0x44, 0x22, 0xD8, -0xFE, 0xC9, 0x00, 0x01, 0x00, 0x61, 0x00, 0x00, 0x01, 0xF3, -0x02, 0xCA, 0x00, 0x05, 0x00, 0x00, 0x33, 0x11, 0x33, 0x11, -0x21, 0x15, 0x61, 0x5A, 0x01, 0x38, 0x02, 0xCA, 0xFD, 0x86, -0x50, 0x00, 0x01, 0x00, 0x61, 0x00, 0x00, 0x03, 0x2A, 0x02, -0xCA, 0x00, 0x15, 0x00, 0x00, 0x21, 0x03, 0x23, 0x16, 0x16, -0x15, 0x11, 0x23, 0x11, 0x33, 0x13, 0x33, 0x13, 0x33, 0x11, -0x23, 0x11, 0x34, 0x36, 0x37, 0x23, 0x03, 0x01, 0x9C, 0xEB, -0x04, 0x03, 0x04, 0x53, 0x85, 0xDC, 0x04, 0xE0, 0x84, 0x59, -0x05, 0x02, 0x04, 0xEE, 0x02, 0x72, 0x1F, 0x69, 0x39, 0xFE, -0x4F, 0x02, 0xCA, 0xFD, 0xB7, 0x02, 0x49, 0xFD, 0x36, 0x01, -0xB7, 0x34, 0x66, 0x20, 0xFD, 0x8F, 0x00, 0x01, 0x00, 0x61, -0x00, 0x00, 0x02, 0x97, 0x02, 0xCA, 0x00, 0x12, 0x00, 0x00, -0x21, 0x23, 0x01, 0x23, 0x16, 0x16, 0x15, 0x11, 0x23, 0x11, -0x33, 0x01, 0x33, 0x2E, 0x02, 0x35, 0x11, 0x33, 0x02, 0x97, -0x69, 0xFE, 0x82, 0x04, 0x02, 0x06, 0x53, 0x68, 0x01, 0x7D, -0x04, 0x01, 0x03, 0x03, 0x54, 0x02, 0x51, 0x23, 0x68, 0x37, -0xFE, 0x71, 0x02, 0xCA, 0xFD, 0xB1, 0x10, 0x40, 0x4C, 0x20, -0x01, 0x93, 0x00, 0x02, 0x00, 0x3D, 0xFF, 0xF6, 0x02, 0xD0, -0x02, 0xD5, 0x00, 0x0F, 0x00, 0x1B, 0x00, 0x00, 0x01, 0x14, -0x06, 0x06, 0x23, 0x22, 0x26, 0x26, 0x35, 0x34, 0x36, 0x36, -0x33, 0x32, 0x16, 0x16, 0x05, 0x14, 0x16, 0x33, 0x32, 0x36, -0x35, 0x34, 0x26, 0x23, 0x22, 0x06, 0x02, 0xD0, 0x4B, 0x92, -0x6C, 0x6F, 0x93, 0x48, 0x48, 0x93, 0x70, 0x6B, 0x92, 0x4B, -0xFD, 0xCC, 0x72, 0x79, 0x7A, 0x70, 0x70, 0x79, 0x79, 0x73, -0x01, 0x66, 0x6F, 0xA5, 0x5C, 0x5C, 0xA6, 0x6F, 0x6E, 0xA4, -0x5C, 0x5B, 0xA5, 0x6F, 0x87, 0x9B, 0x9B, 0x87, 0x87, 0x99, -0x99, 0x00, 0x02, 0x00, 0x61, 0x00, 0x00, 0x02, 0x2A, 0x02, -0xCA, 0x00, 0x0B, 0x00, 0x14, 0x00, 0x00, 0x01, 0x32, 0x16, -0x15, 0x14, 0x06, 0x06, 0x23, 0x23, 0x11, 0x23, 0x11, 0x17, -0x23, 0x11, 0x33, 0x32, 0x36, 0x35, 0x34, 0x26, 0x01, 0x1E, -0x8C, 0x80, 0x35, 0x7D, 0x6B, 0x52, 0x5A, 0xB5, 0x5B, 0x48, -0x66, 0x64, 0x58, 0x02, 0xCA, 0x6E, 0x64, 0x3B, 0x67, 0x40, -0xFE, 0xEA, 0x02, 0xCA, 0x4D, 0xFE, 0xE6, 0x42, 0x4F, 0x45, -0x44, 0x00, 0x02, 0x00, 0x3D, 0xFF, 0x56, 0x02, 0xD0, 0x02, -0xD5, 0x00, 0x14, 0x00, 0x20, 0x00, 0x00, 0x01, 0x14, 0x06, -0x07, 0x17, 0x23, 0x27, 0x22, 0x06, 0x23, 0x22, 0x26, 0x26, -0x35, 0x34, 0x36, 0x36, 0x33, 0x32, 0x16, 0x16, 0x05, 0x14, -0x16, 0x33, 0x32, 0x36, 0x35, 0x34, 0x26, 0x23, 0x22, 0x06, -0x02, 0xD0, 0x69, 0x67, 0xAB, 0x81, 0x8A, 0x06, 0x0D, 0x06, -0x6F, 0x93, 0x48, 0x48, 0x93, 0x70, 0x6B, 0x92, 0x4B, 0xFD, -0xCC, 0x72, 0x79, 0x7A, 0x70, 0x70, 0x79, 0x79, 0x73, 0x01, -0x66, 0x83, 0xB8, 0x23, 0xB2, 0xA1, 0x01, 0x5C, 0xA6, 0x6F, -0x6E, 0xA4, 0x5C, 0x5B, 0xA5, 0x6F, 0x87, 0x9B, 0x9B, 0x87, -0x87, 0x99, 0x99, 0x00, 0x02, 0x00, 0x61, 0x00, 0x00, 0x02, -0x5F, 0x02, 0xCA, 0x00, 0x0E, 0x00, 0x17, 0x00, 0x00, 0x01, -0x32, 0x16, 0x15, 0x14, 0x06, 0x06, 0x07, 0x13, 0x23, 0x03, -0x23, 0x11, 0x23, 0x11, 0x17, 0x23, 0x11, 0x33, 0x32, 0x36, -0x35, 0x34, 0x26, 0x01, 0x26, 0x85, 0x7F, 0x2A, 0x41, 0x24, -0xC4, 0x69, 0xAD, 0x8E, 0x5A, 0xC0, 0x66, 0x6B, 0x57, 0x50, -0x54, 0x02, 0xCA, 0x65, 0x66, 0x39, 0x4C, 0x2D, 0x0D, 0xFE, -0xC0, 0x01, 0x27, 0xFE, 0xD9, 0x02, 0xCA, 0x4E, 0xFE, 0xF7, -0x45, 0x43, 0x46, 0x3B, 0x00, 0x01, 0x00, 0x33, 0xFF, 0xF6, -0x01, 0xF6, 0x02, 0xD4, 0x00, 0x29, 0x00, 0x00, 0x25, 0x14, -0x06, 0x23, 0x22, 0x26, 0x27, 0x35, 0x16, 0x16, 0x33, 0x32, -0x36, 0x35, 0x34, 0x26, 0x26, 0x27, 0x26, 0x26, 0x35, 0x34, -0x36, 0x36, 0x33, 0x32, 0x16, 0x17, 0x07, 0x26, 0x26, 0x23, -0x22, 0x06, 0x15, 0x14, 0x16, 0x16, 0x17, 0x1E, 0x02, 0x01, -0xF6, 0x8A, 0x75, 0x3C, 0x66, 0x22, 0x24, 0x6B, 0x39, 0x50, -0x51, 0x1E, 0x49, 0x41, 0x5B, 0x5D, 0x3A, 0x67, 0x43, 0x3B, -0x62, 0x28, 0x1C, 0x25, 0x57, 0x2F, 0x43, 0x44, 0x1E, 0x44, -0x3A, 0x3F, 0x57, 0x2D, 0xBF, 0x5F, 0x6A, 0x12, 0x10, 0x56, -0x10, 0x1A, 0x3E, 0x35, 0x23, 0x30, 0x29, 0x17, 0x21, 0x60, -0x53, 0x39, 0x51, 0x2C, 0x16, 0x12, 0x4D, 0x10, 0x16, 0x39, -0x2F, 0x24, 0x30, 0x26, 0x16, 0x17, 0x35, 0x4A, 0x00, 0x01, -0x00, 0x0A, 0x00, 0x00, 0x02, 0x21, 0x02, 0xCA, 0x00, 0x07, -0x00, 0x00, 0x21, 0x23, 0x11, 0x23, 0x35, 0x21, 0x15, 0x23, -0x01, 0x43, 0x5A, 0xDF, 0x02, 0x17, 0xDE, 0x02, 0x7B, 0x4F, -0x4F, 0x00, 0x01, 0x00, 0x5A, 0xFF, 0xF6, 0x02, 0x80, 0x02, -0xCA, 0x00, 0x12, 0x00, 0x00, 0x25, 0x14, 0x06, 0x06, 0x23, -0x22, 0x26, 0x35, 0x11, 0x33, 0x11, 0x14, 0x16, 0x33, 0x32, -0x36, 0x35, 0x11, 0x33, 0x02, 0x80, 0x3C, 0x7B, 0x5F, 0x85, -0x8B, 0x5A, 0x5D, 0x5E, 0x61, 0x57, 0x59, 0xFC, 0x4A, 0x77, -0x45, 0x91, 0x77, 0x01, 0xCC, 0xFE, 0x31, 0x57, 0x60, 0x67, -0x51, 0x01, 0xCE, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x02, -0x58, 0x02, 0xCA, 0x00, 0x0C, 0x00, 0x00, 0x01, 0x03, 0x23, -0x03, 0x33, 0x13, 0x16, 0x16, 0x17, 0x36, 0x36, 0x37, 0x13, -0x02, 0x58, 0xFF, 0x5A, 0xFF, 0x5E, 0xA1, 0x10, 0x16, 0x07, -0x07, 0x16, 0x10, 0xA0, 0x02, 0xCA, 0xFD, 0x36, 0x02, 0xCA, -0xFE, 0x36, 0x2C, 0x4D, 0x23, 0x23, 0x4E, 0x2D, 0x01, 0xC8, -0x00, 0x01, 0x00, 0x0C, 0x00, 0x00, 0x03, 0x95, 0x02, 0xCA, -0x00, 0x1F, 0x00, 0x00, 0x01, 0x03, 0x23, 0x03, 0x2E, 0x02, -0x27, 0x06, 0x06, 0x07, 0x03, 0x23, 0x03, 0x33, 0x13, 0x16, -0x16, 0x17, 0x36, 0x36, 0x37, 0x13, 0x33, 0x13, 0x16, 0x16, -0x17, 0x36, 0x36, 0x37, 0x13, 0x03, 0x95, 0xBE, 0x5B, 0x8B, -0x08, 0x10, 0x0A, 0x02, 0x01, 0x13, 0x0E, 0x87, 0x5B, 0xBD, -0x5E, 0x6F, 0x0C, 0x11, 0x05, 0x05, 0x14, 0x0D, 0x7E, 0x5D, -0x83, 0x0E, 0x14, 0x05, 0x05, 0x12, 0x0C, 0x6E, 0x02, 0xCA, -0xFD, 0x36, 0x01, 0xD4, 0x1D, 0x3A, 0x2D, 0x09, 0x0D, 0x55, -0x2E, 0xFE, 0x2F, 0x02, 0xCA, 0xFE, 0x4C, 0x2E, 0x56, 0x26, -0x27, 0x5C, 0x2C, 0x01, 0xAF, 0xFE, 0x4E, 0x2E, 0x5B, 0x23, -0x25, 0x57, 0x2F, 0x01, 0xB3, 0x00, 0x01, 0x00, 0x04, 0x00, -0x00, 0x02, 0x46, 0x02, 0xCA, 0x00, 0x0B, 0x00, 0x00, 0x21, -0x23, 0x03, 0x03, 0x23, 0x13, 0x03, 0x33, 0x13, 0x13, 0x33, -0x03, 0x02, 0x46, 0x66, 0xBD, 0xC0, 0x5F, 0xED, 0xDE, 0x64, -0xAF, 0xB0, 0x5F, 0xDD, 0x01, 0x36, 0xFE, 0xCA, 0x01, 0x74, -0x01, 0x56, 0xFE, 0xE8, 0x01, 0x18, 0xFE, 0xAC, 0x00, 0x01, -0x00, 0x00, 0x00, 0x00, 0x02, 0x36, 0x02, 0xCA, 0x00, 0x08, -0x00, 0x00, 0x01, 0x13, 0x33, 0x03, 0x11, 0x23, 0x11, 0x03, -0x33, 0x01, 0x1B, 0xBA, 0x61, 0xEE, 0x5A, 0xEE, 0x62, 0x01, -0x6B, 0x01, 0x5F, 0xFE, 0x4B, 0xFE, 0xEB, 0x01, 0x11, 0x01, -0xB9, 0x00, 0x01, 0x00, 0x26, 0x00, 0x00, 0x02, 0x15, 0x02, -0xCA, 0x00, 0x09, 0x00, 0x00, 0x21, 0x21, 0x35, 0x01, 0x21, -0x35, 0x21, 0x15, 0x01, 0x21, 0x02, 0x15, 0xFE, 0x11, 0x01, -0x78, 0xFE, 0x94, 0x01, 0xD9, 0xFE, 0x88, 0x01, 0x82, 0x44, -0x02, 0x36, 0x50, 0x44, 0xFD, 0xCA, 0x00, 0x01, 0x00, 0x50, -0xFF, 0x62, 0x01, 0x30, 0x02, 0xCA, 0x00, 0x07, 0x00, 0x00, -0x05, 0x23, 0x11, 0x33, 0x15, 0x23, 0x11, 0x33, 0x01, 0x30, -0xE0, 0xE0, 0x8A, 0x8A, 0x9E, 0x03, 0x68, 0x48, 0xFD, 0x28, -0x00, 0x01, 0x00, 0x0A, 0x00, 0x00, 0x01, 0x6B, 0x02, 0xCA, -0x00, 0x03, 0x00, 0x00, 0x13, 0x01, 0x23, 0x01, 0x60, 0x01, -0x0B, 0x57, 0xFE, 0xF6, 0x02, 0xCA, 0xFD, 0x36, 0x02, 0xCA, -0x00, 0x01, 0x00, 0x19, 0xFF, 0x62, 0x00, 0xF9, 0x02, 0xCA, -0x00, 0x07, 0x00, 0x00, 0x17, 0x33, 0x11, 0x23, 0x35, 0x33, -0x11, 0x23, 0x19, 0x8A, 0x8A, 0xE0, 0xE0, 0x56, 0x02, 0xD8, -0x48, 0xFC, 0x98, 0x00, 0x01, 0x00, 0x26, 0x01, 0x0B, 0x02, -0x16, 0x02, 0xCF, 0x00, 0x06, 0x00, 0x00, 0x13, 0x13, 0x33, -0x13, 0x23, 0x03, 0x03, 0x26, 0xD4, 0x32, 0xEA, 0x4E, 0xB4, -0xA0, 0x01, 0x0B, 0x01, 0xC4, 0xFE, 0x3C, 0x01, 0x67, 0xFE, -0x99, 0x00, 0x01, 0xFF, 0xFE, 0xFF, 0x66, 0x01, 0xBE, 0xFF, -0xA6, 0x00, 0x03, 0x00, 0x00, 0x05, 0x21, 0x35, 0x21, 0x01, -0xBE, 0xFE, 0x40, 0x01, 0xC0, 0x9A, 0x40, 0x00, 0x01, 0x00, -0x28, 0x02, 0x5E, 0x00, 0xF1, 0x02, 0xFE, 0x00, 0x0B, 0x00, -0x00, 0x13, 0x1E, 0x02, 0x17, 0x15, 0x23, 0x2E, 0x02, 0x27, -0x35, 0x91, 0x0B, 0x21, 0x25, 0x0F, 0x3B, 0x17, 0x3A, 0x31, -0x0C, 0x02, 0xFE, 0x16, 0x37, 0x34, 0x13, 0x0C, 0x12, 0x39, -0x39, 0x12, 0x0A, 0x00, 0x02, 0x00, 0x2E, 0xFF, 0xF6, 0x01, -0xE0, 0x02, 0x21, 0x00, 0x1B, 0x00, 0x26, 0x00, 0x00, 0x01, -0x32, 0x16, 0x15, 0x11, 0x23, 0x27, 0x23, 0x06, 0x06, 0x23, -0x22, 0x26, 0x35, 0x34, 0x36, 0x37, 0x37, 0x35, 0x34, 0x26, -0x23, 0x22, 0x06, 0x07, 0x27, 0x36, 0x36, 0x13, 0x06, 0x06, -0x15, 0x14, 0x16, 0x33, 0x32, 0x36, 0x35, 0x35, 0x01, 0x20, -0x62, 0x5E, 0x40, 0x11, 0x04, 0x23, 0x4D, 0x44, 0x49, 0x60, -0x7E, 0x83, 0x5B, 0x3A, 0x35, 0x2A, 0x4C, 0x21, 0x1B, 0x23, -0x60, 0x4E, 0x64, 0x4D, 0x37, 0x2B, 0x44, 0x5A, 0x02, 0x21, -0x56, 0x5E, 0xFE, 0x93, 0x4C, 0x2C, 0x2A, 0x4D, 0x52, 0x50, -0x57, 0x04, 0x03, 0x20, 0x43, 0x34, 0x19, 0x10, 0x42, 0x13, -0x1B, 0xFE, 0xE2, 0x04, 0x38, 0x33, 0x2D, 0x2A, 0x4B, 0x4E, -0x30, 0x00, 0x02, 0x00, 0x55, 0xFF, 0xF6, 0x02, 0x30, 0x02, -0xF8, 0x00, 0x15, 0x00, 0x21, 0x00, 0x00, 0x13, 0x14, 0x06, -0x07, 0x33, 0x36, 0x36, 0x33, 0x32, 0x16, 0x15, 0x14, 0x06, -0x23, 0x22, 0x26, 0x27, 0x23, 0x07, 0x23, 0x11, 0x33, 0x13, -0x22, 0x06, 0x15, 0x15, 0x14, 0x16, 0x33, 0x32, 0x36, 0x35, -0x34, 0xAD, 0x03, 0x02, 0x05, 0x17, 0x50, 0x3F, 0x64, 0x79, -0x7A, 0x63, 0x3F, 0x50, 0x17, 0x07, 0x12, 0x3F, 0x58, 0x97, -0x55, 0x42, 0x41, 0x58, 0x48, 0x47, 0x02, 0x3F, 0x22, 0x3B, -0x11, 0x22, 0x2E, 0x8B, 0x8A, 0x8A, 0x8C, 0x2E, 0x20, 0x44, -0x02, 0xF8, 0xFE, 0xE0, 0x62, 0x67, 0x04, 0x63, 0x69, 0x6A, -0x64, 0xCB, 0x00, 0x01, 0x00, 0x37, 0xFF, 0xF6, 0x01, 0xBF, -0x02, 0x22, 0x00, 0x1A, 0x00, 0x00, 0x05, 0x22, 0x26, 0x26, -0x35, 0x34, 0x36, 0x36, 0x33, 0x32, 0x16, 0x17, 0x07, 0x26, -0x26, 0x23, 0x22, 0x15, 0x14, 0x16, 0x33, 0x32, 0x36, 0x37, -0x15, 0x06, 0x06, 0x01, 0x2C, 0x47, 0x6F, 0x3F, 0x42, 0x71, -0x48, 0x29, 0x4C, 0x18, 0x1B, 0x18, 0x40, 0x1C, 0x9E, 0x4D, -0x4C, 0x2C, 0x43, 0x1C, 0x1B, 0x41, 0x0A, 0x3A, 0x7A, 0x5F, -0x63, 0x7C, 0x3A, 0x11, 0x0C, 0x49, 0x09, 0x10, 0xCB, 0x61, -0x67, 0x12, 0x0D, 0x4E, 0x0E, 0x0F, 0x00, 0x02, 0x00, 0x37, -0xFF, 0xF6, 0x02, 0x12, 0x02, 0xF8, 0x00, 0x15, 0x00, 0x22, -0x00, 0x00, 0x05, 0x22, 0x26, 0x35, 0x34, 0x36, 0x33, 0x32, -0x16, 0x17, 0x33, 0x26, 0x26, 0x35, 0x35, 0x33, 0x11, 0x23, -0x27, 0x23, 0x06, 0x06, 0x27, 0x32, 0x36, 0x35, 0x35, 0x34, -0x26, 0x23, 0x22, 0x06, 0x15, 0x14, 0x16, 0x01, 0x13, 0x64, -0x78, 0x79, 0x64, 0x3E, 0x4F, 0x19, 0x06, 0x01, 0x05, 0x58, -0x47, 0x0D, 0x04, 0x18, 0x50, 0x31, 0x55, 0x45, 0x42, 0x59, -0x47, 0x47, 0x47, 0x0A, 0x8B, 0x8A, 0x8A, 0x8D, 0x2E, 0x21, -0x0D, 0x33, 0x0F, 0xD6, 0xFD, 0x08, 0x48, 0x22, 0x30, 0x49, -0x5D, 0x5E, 0x10, 0x64, 0x6B, 0x71, 0x5F, 0x60, 0x6A, 0x00, -0x02, 0x00, 0x37, 0xFF, 0xF6, 0x02, 0x01, 0x02, 0x22, 0x00, -0x17, 0x00, 0x1E, 0x00, 0x00, 0x01, 0x32, 0x16, 0x16, 0x15, -0x15, 0x21, 0x16, 0x16, 0x33, 0x32, 0x36, 0x37, 0x15, 0x06, -0x06, 0x23, 0x22, 0x26, 0x26, 0x35, 0x34, 0x36, 0x36, 0x17, -0x22, 0x06, 0x07, 0x21, 0x26, 0x26, 0x01, 0x24, 0x45, 0x63, -0x35, 0xFE, 0x91, 0x02, 0x59, 0x50, 0x33, 0x4F, 0x2A, 0x29, -0x50, 0x37, 0x4C, 0x75, 0x41, 0x3B, 0x6B, 0x46, 0x3F, 0x49, -0x07, 0x01, 0x11, 0x01, 0x3E, 0x02, 0x22, 0x3C, 0x6D, 0x49, -0x35, 0x5B, 0x5F, 0x13, 0x12, 0x4D, 0x12, 0x11, 0x3E, 0x7B, -0x59, 0x58, 0x7E, 0x44, 0x48, 0x51, 0x48, 0x44, 0x55, 0x00, -0x01, 0x00, 0x0F, 0x00, 0x00, 0x01, 0x83, 0x02, 0xFD, 0x00, -0x17, 0x00, 0x00, 0x01, 0x23, 0x11, 0x23, 0x11, 0x23, 0x35, -0x37, 0x35, 0x34, 0x36, 0x33, 0x32, 0x16, 0x17, 0x07, 0x26, -0x26, 0x23, 0x22, 0x06, 0x15, 0x15, 0x33, 0x01, 0x4C, 0x87, -0x58, 0x5E, 0x5E, 0x5C, 0x52, 0x20, 0x35, 0x13, 0x17, 0x10, -0x2A, 0x16, 0x2C, 0x2B, 0x87, 0x01, 0xD4, 0xFE, 0x2C, 0x01, -0xD4, 0x29, 0x1E, 0x1F, 0x68, 0x5B, 0x0B, 0x07, 0x45, 0x05, -0x0A, 0x3B, 0x3F, 0x23, 0x00, 0x02, 0x00, 0x37, 0xFF, 0x10, -0x02, 0x12, 0x02, 0x22, 0x00, 0x1E, 0x00, 0x2B, 0x00, 0x00, -0x01, 0x32, 0x16, 0x17, 0x33, 0x37, 0x33, 0x11, 0x14, 0x06, -0x23, 0x22, 0x27, 0x35, 0x16, 0x33, 0x32, 0x36, 0x35, 0x35, -0x34, 0x36, 0x37, 0x23, 0x06, 0x23, 0x22, 0x26, 0x35, 0x34, -0x36, 0x17, 0x22, 0x06, 0x15, 0x14, 0x16, 0x33, 0x32, 0x36, -0x35, 0x35, 0x34, 0x26, 0x01, 0x13, 0x35, 0x55, 0x1E, 0x05, -0x0C, 0x46, 0x75, 0x7B, 0x76, 0x4B, 0x4F, 0x77, 0x45, 0x4F, -0x02, 0x01, 0x04, 0x36, 0x70, 0x68, 0x75, 0x75, 0x73, 0x43, -0x4A, 0x49, 0x46, 0x51, 0x4A, 0x4C, 0x02, 0x22, 0x28, 0x29, -0x47, 0xFD, 0xDF, 0x73, 0x74, 0x22, 0x51, 0x2A, 0x51, 0x46, -0x15, 0x0C, 0x2D, 0x09, 0x51, 0x92, 0x83, 0x80, 0x97, 0x4A, -0x6B, 0x63, 0x63, 0x69, 0x57, 0x61, 0x15, 0x6E, 0x5F, 0x00, -0x01, 0x00, 0x55, 0x00, 0x00, 0x02, 0x19, 0x02, 0xF8, 0x00, -0x15, 0x00, 0x00, 0x13, 0x14, 0x07, 0x33, 0x36, 0x36, 0x33, -0x32, 0x16, 0x15, 0x11, 0x23, 0x11, 0x34, 0x23, 0x22, 0x06, -0x15, 0x11, 0x23, 0x11, 0x33, 0xAD, 0x05, 0x06, 0x1A, 0x59, -0x34, 0x62, 0x62, 0x57, 0x78, 0x5A, 0x43, 0x58, 0x58, 0x02, -0x19, 0x28, 0x23, 0x29, 0x2A, 0x5D, 0x67, 0xFE, 0xA3, 0x01, -0x57, 0x81, 0x65, 0x5E, 0xFE, 0xEB, 0x02, 0xF8, 0x00, 0x02, -0x00, 0x4E, 0x00, 0x00, 0x00, 0xB5, 0x02, 0xE1, 0x00, 0x0B, -0x00, 0x0F, 0x00, 0x00, 0x13, 0x32, 0x16, 0x15, 0x14, 0x06, -0x23, 0x22, 0x26, 0x35, 0x34, 0x36, 0x17, 0x11, 0x23, 0x11, -0x82, 0x14, 0x1F, 0x1F, 0x14, 0x16, 0x1E, 0x1E, 0x41, 0x58, -0x02, 0xE1, 0x1B, 0x1D, 0x1C, 0x1C, 0x1C, 0x1C, 0x1D, 0x1B, -0xC9, 0xFD, 0xE8, 0x02, 0x18, 0x00, 0x02, 0xFF, 0xC9, 0xFF, -0x10, 0x00, 0xB5, 0x02, 0xE1, 0x00, 0x0B, 0x00, 0x1B, 0x00, -0x00, 0x13, 0x34, 0x36, 0x33, 0x32, 0x16, 0x15, 0x14, 0x06, -0x23, 0x22, 0x26, 0x03, 0x22, 0x26, 0x27, 0x35, 0x16, 0x16, -0x33, 0x32, 0x36, 0x35, 0x11, 0x33, 0x11, 0x14, 0x06, 0x4E, -0x1E, 0x16, 0x14, 0x1F, 0x1F, 0x14, 0x16, 0x1E, 0x38, 0x19, -0x26, 0x0E, 0x0F, 0x20, 0x13, 0x20, 0x2A, 0x58, 0x48, 0x02, -0xA9, 0x1D, 0x1B, 0x1B, 0x1D, 0x1C, 0x1C, 0x1C, 0xFC, 0x83, -0x07, 0x05, 0x47, 0x04, 0x06, 0x23, 0x31, 0x02, 0x6B, 0xFD, -0x98, 0x4B, 0x55, 0x00, 0x01, 0x00, 0x55, 0x00, 0x00, 0x02, -0x0D, 0x02, 0xF8, 0x00, 0x13, 0x00, 0x00, 0x13, 0x14, 0x06, -0x07, 0x33, 0x3E, 0x02, 0x37, 0x37, 0x33, 0x07, 0x13, 0x23, -0x27, 0x07, 0x15, 0x23, 0x11, 0x33, 0xAC, 0x03, 0x01, 0x04, -0x06, 0x18, 0x19, 0x09, 0xAB, 0x67, 0xD9, 0xE8, 0x6A, 0xBA, -0x3D, 0x57, 0x57, 0x01, 0x6B, 0x10, 0x34, 0x13, 0x08, 0x1E, -0x1F, 0x0A, 0xB5, 0xE5, 0xFE, 0xCD, 0xFA, 0x35, 0xC5, 0x02, -0xF8, 0x00, 0x01, 0x00, 0x55, 0x00, 0x00, 0x00, 0xAD, 0x02, -0xF8, 0x00, 0x03, 0x00, 0x00, 0x33, 0x23, 0x11, 0x33, 0xAD, -0x58, 0x58, 0x02, 0xF8, 0x00, 0x01, 0x00, 0x55, 0x00, 0x00, -0x03, 0x56, 0x02, 0x22, 0x00, 0x21, 0x00, 0x00, 0x01, 0x32, -0x16, 0x15, 0x11, 0x23, 0x11, 0x34, 0x23, 0x22, 0x06, 0x15, -0x11, 0x23, 0x11, 0x34, 0x23, 0x22, 0x06, 0x15, 0x11, 0x23, -0x11, 0x33, 0x17, 0x33, 0x36, 0x36, 0x33, 0x32, 0x17, 0x33, -0x36, 0x36, 0x02, 0xA1, 0x5B, 0x5A, 0x57, 0x6D, 0x4E, 0x43, -0x57, 0x6E, 0x51, 0x3E, 0x58, 0x47, 0x0D, 0x05, 0x19, 0x55, -0x30, 0x7E, 0x26, 0x05, 0x1B, 0x5D, 0x02, 0x22, 0x5D, 0x68, -0xFE, 0xA3, 0x01, 0x59, 0x7F, 0x5A, 0x56, 0xFE, 0xD8, 0x01, -0x59, 0x7F, 0x64, 0x5E, 0xFE, 0xEA, 0x02, 0x18, 0x49, 0x2A, -0x29, 0x5A, 0x2E, 0x2C, 0x00, 0x01, 0x00, 0x55, 0x00, 0x00, -0x02, 0x19, 0x02, 0x22, 0x00, 0x13, 0x00, 0x00, 0x01, 0x32, -0x16, 0x15, 0x11, 0x23, 0x11, 0x34, 0x23, 0x22, 0x06, 0x15, -0x11, 0x23, 0x11, 0x33, 0x17, 0x33, 0x36, 0x36, 0x01, 0x57, -0x60, 0x62, 0x57, 0x78, 0x59, 0x44, 0x58, 0x47, 0x0D, 0x05, -0x1A, 0x5C, 0x02, 0x22, 0x5D, 0x68, 0xFE, 0xA3, 0x01, 0x57, -0x81, 0x64, 0x5E, 0xFE, 0xEA, 0x02, 0x18, 0x49, 0x2A, 0x29, -0x00, 0x02, 0x00, 0x37, 0xFF, 0xF6, 0x02, 0x27, 0x02, 0x22, -0x00, 0x0D, 0x00, 0x19, 0x00, 0x00, 0x01, 0x14, 0x06, 0x23, -0x22, 0x26, 0x26, 0x35, 0x34, 0x36, 0x33, 0x32, 0x16, 0x16, -0x05, 0x14, 0x16, 0x33, 0x32, 0x36, 0x35, 0x34, 0x26, 0x23, -0x22, 0x06, 0x02, 0x27, 0x87, 0x73, 0x47, 0x6F, 0x40, 0x86, -0x73, 0x49, 0x6F, 0x3F, 0xFE, 0x6B, 0x4B, 0x52, 0x51, 0x4C, -0x4C, 0x52, 0x52, 0x4A, 0x01, 0x0D, 0x85, 0x92, 0x41, 0x7D, -0x59, 0x85, 0x90, 0x41, 0x7B, 0x59, 0x5F, 0x6F, 0x6F, 0x5F, -0x5F, 0x6C, 0x6C, 0x00, 0x02, 0x00, 0x55, 0xFF, 0x10, 0x02, -0x30, 0x02, 0x22, 0x00, 0x15, 0x00, 0x23, 0x00, 0x00, 0x01, -0x32, 0x16, 0x15, 0x14, 0x06, 0x23, 0x22, 0x26, 0x27, 0x23, -0x16, 0x16, 0x15, 0x15, 0x23, 0x11, 0x33, 0x17, 0x33, 0x36, -0x36, 0x17, 0x22, 0x06, 0x07, 0x15, 0x14, 0x16, 0x33, 0x32, -0x36, 0x36, 0x35, 0x34, 0x26, 0x01, 0x54, 0x63, 0x79, 0x79, -0x64, 0x3E, 0x51, 0x17, 0x06, 0x02, 0x04, 0x58, 0x48, 0x0C, -0x04, 0x18, 0x4E, 0x31, 0x52, 0x43, 0x02, 0x41, 0x58, 0x31, -0x3F, 0x1F, 0x47, 0x02, 0x22, 0x8A, 0x8B, 0x89, 0x8E, 0x2F, -0x1F, 0x11, 0x34, 0x13, 0xDC, 0x03, 0x08, 0x49, 0x23, 0x30, -0x4A, 0x5C, 0x5E, 0x11, 0x63, 0x6B, 0x36, 0x5D, 0x3C, 0x5C, -0x6E, 0x00, 0x02, 0x00, 0x37, 0xFF, 0x10, 0x02, 0x12, 0x02, -0x22, 0x00, 0x15, 0x00, 0x22, 0x00, 0x00, 0x05, 0x34, 0x36, -0x37, 0x23, 0x06, 0x06, 0x23, 0x22, 0x26, 0x35, 0x34, 0x36, -0x33, 0x32, 0x16, 0x17, 0x33, 0x37, 0x33, 0x11, 0x23, 0x03, -0x32, 0x36, 0x37, 0x35, 0x34, 0x26, 0x23, 0x22, 0x06, 0x15, -0x14, 0x16, 0x01, 0xBA, 0x02, 0x03, 0x06, 0x17, 0x51, 0x40, -0x61, 0x79, 0x7B, 0x62, 0x3F, 0x50, 0x18, 0x04, 0x0D, 0x46, -0x58, 0x98, 0x53, 0x45, 0x01, 0x44, 0x57, 0x48, 0x46, 0x47, -0x0B, 0x12, 0x30, 0x11, 0x22, 0x30, 0x8B, 0x8A, 0x8A, 0x8D, -0x30, 0x23, 0x49, 0xFC, 0xF8, 0x01, 0x2F, 0x5B, 0x5E, 0x12, -0x66, 0x69, 0x71, 0x5F, 0x5F, 0x6B, 0x00, 0x01, 0x00, 0x55, -0x00, 0x00, 0x01, 0x8E, 0x02, 0x22, 0x00, 0x13, 0x00, 0x00, -0x01, 0x32, 0x16, 0x17, 0x07, 0x26, 0x26, 0x23, 0x22, 0x06, -0x06, 0x15, 0x11, 0x23, 0x11, 0x33, 0x17, 0x33, 0x36, 0x36, -0x01, 0x4F, 0x0F, 0x23, 0x0D, 0x0B, 0x0D, 0x1F, 0x0E, 0x29, -0x48, 0x2B, 0x58, 0x48, 0x0A, 0x04, 0x1A, 0x52, 0x02, 0x22, -0x03, 0x03, 0x51, 0x03, 0x04, 0x2D, 0x51, 0x36, 0xFE, 0xE2, -0x02, 0x18, 0x62, 0x2C, 0x40, 0x00, 0x01, 0x00, 0x33, 0xFF, -0xF6, 0x01, 0xB2, 0x02, 0x22, 0x00, 0x29, 0x00, 0x00, 0x25, -0x14, 0x06, 0x23, 0x22, 0x26, 0x27, 0x35, 0x16, 0x16, 0x33, -0x32, 0x36, 0x35, 0x34, 0x26, 0x26, 0x27, 0x2E, 0x02, 0x35, -0x34, 0x36, 0x33, 0x32, 0x16, 0x17, 0x07, 0x26, 0x26, 0x23, -0x22, 0x06, 0x15, 0x14, 0x16, 0x16, 0x17, 0x1E, 0x02, 0x01, -0xB2, 0x74, 0x62, 0x38, 0x51, 0x1F, 0x20, 0x5B, 0x2F, 0x43, -0x3C, 0x16, 0x39, 0x35, 0x34, 0x4A, 0x28, 0x6F, 0x5A, 0x31, -0x55, 0x25, 0x1E, 0x22, 0x4A, 0x27, 0x36, 0x39, 0x1A, 0x3D, -0x33, 0x33, 0x48, 0x26, 0x94, 0x4E, 0x50, 0x12, 0x10, 0x50, -0x10, 0x1B, 0x2B, 0x24, 0x14, 0x20, 0x20, 0x14, 0x14, 0x28, -0x38, 0x2C, 0x44, 0x4A, 0x13, 0x11, 0x46, 0x0E, 0x14, 0x23, -0x1E, 0x16, 0x1F, 0x1D, 0x14, 0x13, 0x28, 0x39, 0x00, 0x01, -0x00, 0x10, 0xFF, 0xF6, 0x01, 0x53, 0x02, 0x93, 0x00, 0x18, -0x00, 0x00, 0x25, 0x32, 0x36, 0x37, 0x15, 0x06, 0x06, 0x23, -0x22, 0x26, 0x26, 0x35, 0x11, 0x23, 0x35, 0x37, 0x37, 0x33, -0x15, 0x33, 0x15, 0x23, 0x11, 0x14, 0x16, 0x01, 0x08, 0x14, -0x2A, 0x0D, 0x0E, 0x34, 0x18, 0x2A, 0x47, 0x2C, 0x4C, 0x4D, -0x23, 0x34, 0x9B, 0x9B, 0x2F, 0x3E, 0x07, 0x04, 0x43, 0x07, -0x09, 0x1D, 0x48, 0x41, 0x01, 0x38, 0x2A, 0x23, 0x72, 0x7B, -0x44, 0xFE, 0xCA, 0x31, 0x2F, 0x00, 0x01, 0x00, 0x4F, 0xFF, -0xF6, 0x02, 0x15, 0x02, 0x18, 0x00, 0x13, 0x00, 0x00, 0x01, -0x11, 0x23, 0x27, 0x23, 0x06, 0x06, 0x23, 0x22, 0x26, 0x35, -0x11, 0x33, 0x11, 0x14, 0x33, 0x32, 0x36, 0x35, 0x11, 0x02, -0x15, 0x48, 0x0D, 0x04, 0x1A, 0x5C, 0x34, 0x61, 0x62, 0x59, -0x77, 0x59, 0x45, 0x02, 0x18, 0xFD, 0xE8, 0x47, 0x2A, 0x27, -0x5D, 0x66, 0x01, 0x5F, 0xFE, 0xA7, 0x80, 0x64, 0x5E, 0x01, -0x17, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFC, 0x02, -0x18, 0x00, 0x0F, 0x00, 0x00, 0x33, 0x03, 0x33, 0x13, 0x1E, -0x02, 0x17, 0x33, 0x3E, 0x02, 0x37, 0x13, 0x33, 0x03, 0xCB, -0xCB, 0x5E, 0x72, 0x08, 0x12, 0x0E, 0x03, 0x04, 0x04, 0x0F, -0x13, 0x07, 0x72, 0x5E, 0xCC, 0x02, 0x18, 0xFE, 0xC4, 0x16, -0x36, 0x31, 0x11, 0x11, 0x32, 0x36, 0x15, 0x01, 0x3C, 0xFD, -0xE8, 0x00, 0x01, 0x00, 0x0B, 0x00, 0x01, 0x03, 0x07, 0x02, -0x19, 0x00, 0x22, 0x00, 0x00, 0x01, 0x26, 0x26, 0x27, 0x23, -0x06, 0x06, 0x07, 0x03, 0x23, 0x03, 0x33, 0x13, 0x16, 0x16, -0x17, 0x33, 0x3E, 0x02, 0x37, 0x13, 0x33, 0x13, 0x16, 0x16, -0x17, 0x33, 0x36, 0x36, 0x37, 0x13, 0x33, 0x03, 0x23, 0x01, -0xAF, 0x0D, 0x13, 0x05, 0x04, 0x04, 0x12, 0x0E, 0x60, 0x64, -0x93, 0x5B, 0x4A, 0x0B, 0x14, 0x04, 0x04, 0x04, 0x0B, 0x0E, -0x07, 0x5F, 0x60, 0x5C, 0x0B, 0x15, 0x04, 0x04, 0x03, 0x15, -0x0C, 0x4B, 0x5A, 0x95, 0x67, 0x01, 0x2F, 0x29, 0x4F, 0x16, -0x16, 0x4F, 0x2A, 0xFE, 0xD3, 0x02, 0x18, 0xFE, 0xE2, 0x2B, -0x58, 0x1D, 0x11, 0x32, 0x37, 0x16, 0x01, 0x2E, 0xFE, 0xD2, -0x22, 0x50, 0x1D, 0x19, 0x58, 0x2E, 0x01, 0x1E, 0xFD, 0xE8, -0x00, 0x01, 0x00, 0x12, 0x00, 0x00, 0x01, 0xFF, 0x02, 0x18, -0x00, 0x0B, 0x00, 0x00, 0x13, 0x03, 0x33, 0x17, 0x37, 0x33, -0x03, 0x13, 0x23, 0x27, 0x07, 0x23, 0xD4, 0xB9, 0x64, 0x8A, -0x89, 0x63, 0xB9, 0xC3, 0x64, 0x92, 0x94, 0x63, 0x01, 0x12, -0x01, 0x06, 0xCA, 0xCA, 0xFE, 0xFA, 0xFE, 0xEE, 0xD6, 0xD6, -0x00, 0x01, 0x00, 0x01, 0xFF, 0x10, 0x01, 0xFE, 0x02, 0x18, -0x00, 0x1A, 0x00, 0x00, 0x13, 0x33, 0x13, 0x16, 0x16, 0x17, -0x33, 0x36, 0x36, 0x37, 0x13, 0x33, 0x03, 0x06, 0x06, 0x23, -0x22, 0x26, 0x27, 0x35, 0x16, 0x16, 0x33, 0x32, 0x36, 0x37, -0x37, 0x01, 0x5E, 0x74, 0x0F, 0x18, 0x06, 0x04, 0x06, 0x1A, -0x0E, 0x6D, 0x5F, 0xE7, 0x1C, 0x59, 0x4E, 0x18, 0x24, 0x0D, -0x0B, 0x1F, 0x11, 0x2E, 0x39, 0x10, 0x1C, 0x02, 0x18, 0xFE, -0xCF, 0x28, 0x49, 0x21, 0x19, 0x51, 0x29, 0x01, 0x30, 0xFD, -0x9E, 0x4C, 0x5A, 0x05, 0x03, 0x46, 0x02, 0x04, 0x34, 0x2B, -0x47, 0x00, 0x01, 0x00, 0x27, 0x00, 0x00, 0x01, 0xAF, 0x02, -0x18, 0x00, 0x09, 0x00, 0x00, 0x21, 0x21, 0x35, 0x01, 0x21, -0x35, 0x21, 0x15, 0x01, 0x21, 0x01, 0xAF, 0xFE, 0x78, 0x01, -0x20, 0xFE, 0xF1, 0x01, 0x70, 0xFE, 0xE4, 0x01, 0x23, 0x3A, -0x01, 0x9A, 0x44, 0x42, 0xFE, 0x6E, 0x00, 0x01, 0x00, 0x1C, -0xFF, 0x62, 0x01, 0x5C, 0x02, 0xCA, 0x00, 0x1D, 0x00, 0x00, -0x05, 0x26, 0x26, 0x35, 0x35, 0x34, 0x26, 0x23, 0x35, 0x36, -0x36, 0x35, 0x35, 0x34, 0x36, 0x33, 0x15, 0x06, 0x06, 0x15, -0x15, 0x14, 0x07, 0x15, 0x16, 0x15, 0x15, 0x14, 0x16, 0x17, -0x01, 0x5C, 0x5C, 0x6A, 0x3F, 0x3B, 0x3B, 0x3F, 0x6E, 0x58, -0x34, 0x3B, 0x6D, 0x6D, 0x3A, 0x35, 0x9E, 0x01, 0x4E, 0x50, -0x93, 0x33, 0x2B, 0x49, 0x01, 0x2A, 0x32, 0x94, 0x50, 0x4E, -0x48, 0x01, 0x2C, 0x31, 0x90, 0x67, 0x13, 0x06, 0x13, 0x67, -0x93, 0x31, 0x2B, 0x01, 0x00, 0x01, 0x00, 0xEF, 0xFF, 0x0F, -0x01, 0x38, 0x02, 0xF8, 0x00, 0x03, 0x00, 0x00, 0x13, 0x33, -0x11, 0x23, 0xEF, 0x49, 0x49, 0x02, 0xF8, 0xFC, 0x17, 0x00, -0x01, 0x00, 0x20, 0xFF, 0x62, 0x01, 0x60, 0x02, 0xCA, 0x00, -0x1D, 0x00, 0x00, 0x17, 0x36, 0x36, 0x35, 0x35, 0x34, 0x37, -0x35, 0x26, 0x35, 0x35, 0x34, 0x26, 0x27, 0x35, 0x16, 0x16, -0x15, 0x15, 0x14, 0x16, 0x33, 0x15, 0x06, 0x06, 0x15, 0x15, -0x14, 0x06, 0x23, 0x20, 0x34, 0x3B, 0x6D, 0x6D, 0x3A, 0x35, -0x5C, 0x6A, 0x3F, 0x3B, 0x3B, 0x3F, 0x6E, 0x58, 0x56, 0x02, -0x2B, 0x31, 0x91, 0x67, 0x13, 0x06, 0x13, 0x67, 0x92, 0x31, -0x2B, 0x01, 0x48, 0x01, 0x4E, 0x50, 0x92, 0x33, 0x2B, 0x49, -0x01, 0x2A, 0x32, 0x95, 0x4F, 0x4F, 0x00, 0x01, 0x00, 0x32, -0x01, 0x1F, 0x02, 0x09, 0x01, 0xA2, 0x00, 0x17, 0x00, 0x00, -0x01, 0x26, 0x26, 0x23, 0x22, 0x06, 0x07, 0x35, 0x36, 0x33, -0x32, 0x16, 0x17, 0x16, 0x16, 0x33, 0x32, 0x36, 0x37, 0x15, -0x06, 0x23, 0x22, 0x26, 0x01, 0x0D, 0x24, 0x2F, 0x16, 0x1C, -0x3E, 0x18, 0x30, 0x48, 0x1D, 0x39, 0x2E, 0x24, 0x2F, 0x15, -0x1D, 0x3E, 0x18, 0x31, 0x47, 0x1C, 0x3B, 0x01, 0x3F, 0x10, -0x0B, 0x22, 0x19, 0x4E, 0x35, 0x0C, 0x14, 0x10, 0x0B, 0x22, -0x19, 0x4D, 0x36, 0x0D, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, -0x02, 0x7E, 0x03, 0xB0, 0x00, 0xA2, 0x00, 0x21, 0x00, 0x00, -0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x83, -0x00, 0x40, 0x00, 0x94, 0x00, 0xB2, 0x40, 0x00, 0x00, 0x00, -0x00, 0x00, 0x40, 0x00, 0x00, 0x01, 0x00, 0x28, 0x02, 0x5E, -0x00, 0xF1, 0x02, 0xFE, 0x00, 0x0B, 0x00, 0x00, 0x13, 0x0E, -0x02, 0x07, 0x23, 0x35, 0x3E, 0x02, 0x37, 0x33, 0xF1, 0x0C, -0x32, 0x39, 0x18, 0x3A, 0x0F, 0x23, 0x22, 0x0B, 0x6A, 0x02, -0xF4, 0x12, 0x39, 0x39, 0x12, 0x0C, 0x13, 0x34, 0x37, 0x16, -0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x02, 0x7E, 0x03, 0xB0, -0x00, 0xA2, 0x00, 0x21, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, -0x00, 0x00, 0x40, 0x00, 0x00, 0x83, 0x00, 0x60, 0x00, 0xE1, -0x00, 0xB2, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, -0x00, 0x01, 0x00, 0x28, 0x02, 0x5E, 0x01, 0x7A, 0x02, 0xFE, -0x00, 0x12, 0x00, 0x00, 0x13, 0x1E, 0x02, 0x17, 0x15, 0x23, -0x26, 0x26, 0x27, 0x06, 0x06, 0x07, 0x23, 0x35, 0x3E, 0x02, -0x37, 0xFD, 0x0C, 0x2D, 0x31, 0x13, 0x3E, 0x1A, 0x38, 0x1B, -0x1B, 0x36, 0x1A, 0x3C, 0x13, 0x2F, 0x2C, 0x0D, 0x02, 0xFE, -0x16, 0x37, 0x35, 0x13, 0x0B, 0x10, 0x2F, 0x1B, 0x1B, 0x2E, -0x11, 0x0B, 0x14, 0x34, 0x37, 0x16, 0xFF, 0xFF, 0x00, 0x00, -0x00, 0x00, 0x02, 0x7E, 0x03, 0xB0, 0x00, 0xA2, 0x00, 0x21, -0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, -0x00, 0x83, 0x00, 0x62, 0x00, 0x6D, 0x00, 0xB2, 0x40, 0x00, -0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x01, 0x00, 0x28, -0x02, 0x5E, 0x01, 0x97, 0x02, 0xDF, 0x00, 0x15, 0x00, 0x00, -0x13, 0x36, 0x36, 0x33, 0x32, 0x16, 0x16, 0x33, 0x32, 0x36, -0x37, 0x33, 0x06, 0x06, 0x23, 0x22, 0x26, 0x26, 0x23, 0x22, -0x06, 0x07, 0x28, 0x06, 0x39, 0x2F, 0x1E, 0x35, 0x30, 0x15, -0x17, 0x19, 0x07, 0x32, 0x06, 0x38, 0x2F, 0x1C, 0x35, 0x31, -0x16, 0x18, 0x18, 0x07, 0x02, 0x5E, 0x3B, 0x45, 0x1D, 0x1C, -0x1D, 0x1D, 0x3A, 0x46, 0x1C, 0x1D, 0x1D, 0x1D, 0xFF, 0xFF, -0x00, 0x00, 0x00, 0x00, 0x02, 0x7E, 0x03, 0x91, 0x00, 0xA2, -0x00, 0x21, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, -0x40, 0x00, 0x00, 0x83, 0x00, 0x64, 0x00, 0x5F, 0x00, 0xB2, -0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x02, -0x00, 0x95, 0x02, 0x77, 0x01, 0xAE, 0x02, 0xDA, 0x00, 0x0B, -0x00, 0x17, 0x00, 0x00, 0x13, 0x34, 0x36, 0x33, 0x32, 0x16, -0x15, 0x14, 0x06, 0x23, 0x22, 0x26, 0x37, 0x34, 0x36, 0x33, -0x32, 0x16, 0x15, 0x14, 0x06, 0x23, 0x22, 0x26, 0x95, 0x1C, -0x13, 0x13, 0x1C, 0x1C, 0x13, 0x13, 0x1C, 0xBC, 0x1B, 0x13, -0x13, 0x1C, 0x1C, 0x13, 0x13, 0x1B, 0x02, 0xA9, 0x1A, 0x17, -0x17, 0x1A, 0x19, 0x19, 0x19, 0x19, 0x1A, 0x17, 0x17, 0x1A, -0x19, 0x19, 0x19, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x02, -0x7E, 0x03, 0x8C, 0x00, 0xA2, 0x00, 0x21, 0x00, 0x00, 0x40, -0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x83, 0x00, -0x66, 0x00, 0x1D, 0x00, 0xB2, 0x40, 0x00, 0x00, 0x00, 0x00, -0x00, 0x40, 0x00, 0x00, 0x02, 0x00, 0x28, 0x02, 0x5E, 0x01, -0x04, 0x03, 0x31, 0x00, 0x0B, 0x00, 0x17, 0x00, 0x00, 0x13, -0x22, 0x26, 0x35, 0x34, 0x36, 0x33, 0x32, 0x16, 0x15, 0x14, -0x06, 0x27, 0x32, 0x36, 0x35, 0x34, 0x26, 0x23, 0x22, 0x06, -0x15, 0x14, 0x16, 0x95, 0x31, 0x3C, 0x3C, 0x31, 0x2F, 0x40, -0x3F, 0x30, 0x19, 0x1F, 0x20, 0x18, 0x18, 0x20, 0x1D, 0x02, -0x5E, 0x38, 0x32, 0x32, 0x37, 0x37, 0x31, 0x33, 0x38, 0x32, -0x1E, 0x1A, 0x1A, 0x1E, 0x1E, 0x1A, 0x1A, 0x1E, 0xFF, 0xFF, -0x00, 0x00, 0x00, 0x00, 0x02, 0x7E, 0x03, 0x6E, 0x00, 0xA2, -0x00, 0x21, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, -0x40, 0x00, 0x00, 0x83, 0x00, 0x68, 0x00, 0xA8, 0x00, 0x3D, -0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x02, -0xFF, 0xFF, 0x00, 0x00, 0x03, 0x35, 0x02, 0xCA, 0x00, 0x0F, -0x00, 0x13, 0x00, 0x00, 0x21, 0x21, 0x35, 0x23, 0x07, 0x23, -0x01, 0x21, 0x15, 0x21, 0x15, 0x21, 0x15, 0x21, 0x15, 0x21, -0x25, 0x33, 0x11, 0x23, 0x03, 0x35, 0xFE, 0x8C, 0xFA, 0x6B, -0x5D, 0x01, 0x53, 0x01, 0xE3, 0xFE, 0xE6, 0x01, 0x07, 0xFE, -0xF9, 0x01, 0x1A, 0xFD, 0xB5, 0xD7, 0x3A, 0xDD, 0xDD, 0x02, -0xCA, 0x4F, 0xDF, 0x4E, 0xFF, 0xDE, 0x01, 0x4D, 0x00, 0x01, -0x00, 0x0E, 0xFF, 0x10, 0x00, 0xD4, 0x00, 0x00, 0x00, 0x14, -0x00, 0x00, 0x17, 0x14, 0x06, 0x23, 0x22, 0x27, 0x35, 0x16, -0x16, 0x33, 0x32, 0x36, 0x35, 0x34, 0x26, 0x27, 0x37, 0x33, -0x07, 0x16, 0x16, 0xD4, 0x4A, 0x4A, 0x20, 0x12, 0x09, 0x1E, -0x0E, 0x24, 0x26, 0x35, 0x26, 0x2B, 0x3A, 0x1A, 0x24, 0x33, -0x8B, 0x30, 0x35, 0x05, 0x37, 0x02, 0x03, 0x13, 0x19, 0x1A, -0x18, 0x05, 0x56, 0x35, 0x08, 0x28, 0xFF, 0xFF, 0x00, 0x3D, -0xFF, 0x10, 0x02, 0x59, 0x02, 0xD4, 0x00, 0xA2, 0x00, 0x23, -0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, -0x00, 0x83, 0x00, 0x6B, 0x01, 0x05, 0x00, 0x00, 0x40, 0x00, -0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0xFF, 0xFF, 0x00, 0x61, -0x00, 0x00, 0x01, 0xF0, 0x03, 0xB0, 0x00, 0xA2, 0x00, 0x25, -0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, -0x00, 0x83, 0x00, 0x40, 0x00, 0x87, 0x00, 0xB2, 0x40, 0x00, -0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0xFF, 0xFF, 0x00, 0x61, -0x00, 0x00, 0x01, 0xF0, 0x03, 0xB0, 0x00, 0xA2, 0x00, 0x25, -0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, -0x00, 0x83, 0x00, 0x60, 0x00, 0xD4, 0x00, 0xB2, 0x40, 0x00, -0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0xFF, 0xFF, 0x00, 0x61, -0x00, 0x00, 0x01, 0xF0, 0x03, 0xB0, 0x00, 0xA2, 0x00, 0x25, -0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, -0x00, 0x83, 0x00, 0x62, 0x00, 0x60, 0x00, 0xB2, 0x40, 0x00, -0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0xFF, 0xFF, 0x00, 0x61, -0x00, 0x00, 0x01, 0xF0, 0x03, 0x8C, 0x00, 0xA2, 0x00, 0x25, -0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, -0x00, 0x83, 0x00, 0x66, 0x00, 0x10, 0x00, 0xB2, 0x40, 0x00, -0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0xFF, 0xFF, 0x00, 0x28, -0x00, 0x00, 0x01, 0x2A, 0x03, 0xB0, 0x00, 0xA2, 0x00, 0x29, -0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, -0x00, 0x83, 0x00, 0x40, 0x00, 0x00, 0x00, 0xB2, 0x40, 0x00, -0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0xFF, 0xFF, 0x00, 0x28, -0x00, 0x00, 0x01, 0x3E, 0x03, 0xB0, 0x00, 0xA2, 0x00, 0x29, -0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, -0x00, 0x83, 0x00, 0x60, 0x00, 0x4D, 0x00, 0xB2, 0x40, 0x00, -0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0xFF, 0xFF, 0x00, 0x01, -0x00, 0x00, 0x01, 0x53, 0x03, 0xB0, 0x00, 0xA2, 0x00, 0x29, -0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, -0x00, 0x83, 0x00, 0x62, 0xFF, 0xD9, 0x00, 0xB2, 0x40, 0x00, -0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0xFF, 0xFF, 0x00, 0x1E, -0x00, 0x00, 0x01, 0x37, 0x03, 0x8C, 0x00, 0xA2, 0x00, 0x29, -0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, -0x00, 0x83, 0x00, 0x66, 0xFF, 0x89, 0x00, 0xB2, 0x40, 0x00, -0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x02, 0x00, 0x1E, -0x00, 0x00, 0x02, 0x9D, 0x02, 0xCA, 0x00, 0x0D, 0x00, 0x19, -0x00, 0x00, 0x01, 0x32, 0x16, 0x16, 0x15, 0x14, 0x06, 0x23, -0x23, 0x11, 0x23, 0x35, 0x33, 0x11, 0x17, 0x23, 0x15, 0x33, -0x15, 0x23, 0x15, 0x33, 0x20, 0x11, 0x34, 0x26, 0x01, 0x3D, -0x6B, 0x9E, 0x57, 0xC5, 0xB1, 0xBF, 0x4A, 0x4A, 0xC8, 0x6E, -0xB2, 0xB2, 0x5A, 0x01, 0x22, 0x8E, 0x02, 0xCA, 0x50, 0x9B, -0x73, 0xB5, 0xB7, 0x01, 0x3A, 0x4E, 0x01, 0x42, 0x4D, 0xF5, -0x4E, 0xED, 0x01, 0x1C, 0x8F, 0x85, 0xFF, 0xFF, 0x00, 0x61, -0x00, 0x00, 0x02, 0x97, 0x03, 0x91, 0x00, 0xA2, 0x00, 0x2E, -0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, -0x00, 0x83, 0x00, 0x64, 0x00, 0x9D, 0x00, 0xB2, 0x40, 0x00, -0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0xFF, 0xFF, 0x00, 0x3D, -0xFF, 0xF6, 0x02, 0xD0, 0x03, 0xB0, 0x00, 0xA2, 0x00, 0x2F, -0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, -0x00, 0x83, 0x00, 0x40, 0x00, 0xDD, 0x00, 0xB2, 0x40, 0x00, -0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0xFF, 0xFF, 0x00, 0x3D, -0xFF, 0xF6, 0x02, 0xD0, 0x03, 0xB0, 0x00, 0xA2, 0x00, 0x2F, -0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, -0x00, 0x83, 0x00, 0x60, 0x01, 0x2A, 0x00, 0xB2, 0x40, 0x00, -0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0xFF, 0xFF, 0x00, 0x3D, -0xFF, 0xF6, 0x02, 0xD0, 0x03, 0xB0, 0x00, 0xA2, 0x00, 0x2F, -0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, -0x00, 0x83, 0x00, 0x62, 0x00, 0xB6, 0x00, 0xB2, 0x40, 0x00, -0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0xFF, 0xFF, 0x00, 0x3D, -0xFF, 0xF6, 0x02, 0xD0, 0x03, 0x91, 0x00, 0xA2, 0x00, 0x2F, -0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, -0x00, 0x83, 0x00, 0x64, 0x00, 0xA8, 0x00, 0xB2, 0x40, 0x00, -0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0xFF, 0xFF, 0x00, 0x3D, -0xFF, 0xF6, 0x02, 0xD0, 0x03, 0x8C, 0x00, 0xA2, 0x00, 0x2F, -0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, -0x00, 0x83, 0x00, 0x66, 0x00, 0x66, 0x00, 0xB2, 0x40, 0x00, -0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x01, 0x00, 0x40, -0x00, 0x84, 0x01, 0xFA, 0x02, 0x3E, 0x00, 0x0B, 0x00, 0x00, -0x01, 0x17, 0x07, 0x17, 0x07, 0x27, 0x07, 0x27, 0x37, 0x27, -0x37, 0x17, 0x01, 0xC8, 0x32, 0xAA, 0xA9, 0x32, 0xAB, 0xA7, -0x34, 0xA9, 0xAA, 0x34, 0xA9, 0x02, 0x3E, 0x33, 0xAA, 0xAA, -0x33, 0xA9, 0xA9, 0x33, 0xAA, 0xA9, 0x34, 0xAB, 0x00, 0x03, -0x00, 0x3D, 0xFF, 0xE1, 0x02, 0xD0, 0x02, 0xEA, 0x00, 0x17, -0x00, 0x20, 0x00, 0x29, 0x00, 0x00, 0x01, 0x14, 0x06, 0x06, -0x23, 0x22, 0x27, 0x07, 0x27, 0x37, 0x26, 0x26, 0x35, 0x34, -0x36, 0x36, 0x33, 0x32, 0x16, 0x17, 0x37, 0x17, 0x07, 0x16, -0x05, 0x14, 0x16, 0x17, 0x01, 0x26, 0x23, 0x22, 0x06, 0x05, -0x34, 0x27, 0x01, 0x16, 0x16, 0x33, 0x32, 0x36, 0x02, 0xD0, -0x4B, 0x92, 0x6C, 0x70, 0x49, 0x30, 0x3D, 0x34, 0x2C, 0x2C, -0x48, 0x93, 0x70, 0x34, 0x59, 0x25, 0x2E, 0x3D, 0x33, 0x5E, -0xFD, 0xCC, 0x17, 0x18, 0x01, 0x3F, 0x34, 0x4E, 0x79, 0x73, -0x01, 0xD5, 0x33, 0xFE, 0xC0, 0x1A, 0x45, 0x2A, 0x7A, 0x70, -0x01, 0x66, 0x6F, 0xA5, 0x5C, 0x2F, 0x44, 0x28, 0x4A, 0x31, -0x8C, 0x57, 0x6E, 0xA4, 0x5C, 0x18, 0x15, 0x42, 0x29, 0x47, -0x63, 0xB1, 0x3D, 0x64, 0x25, 0x01, 0xC3, 0x23, 0x99, 0x87, -0x81, 0x49, 0xFE, 0x3A, 0x12, 0x14, 0x9B, 0xFF, 0xFF, 0x00, -0x5A, 0xFF, 0xF6, 0x02, 0x80, 0x03, 0xB0, 0x00, 0xA2, 0x00, -0x35, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, -0x00, 0x00, 0x83, 0x00, 0x40, 0x00, 0xC4, 0x00, 0xB2, 0x40, -0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0xFF, 0xFF, 0x00, -0x5A, 0xFF, 0xF6, 0x02, 0x80, 0x03, 0xB0, 0x00, 0xA2, 0x00, -0x35, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, -0x00, 0x00, 0x83, 0x00, 0x60, 0x01, 0x11, 0x00, 0xB2, 0x40, -0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0xFF, 0xFF, 0x00, -0x5A, 0xFF, 0xF6, 0x02, 0x80, 0x03, 0xB0, 0x00, 0xA2, 0x00, -0x35, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, -0x00, 0x00, 0x83, 0x00, 0x62, 0x00, 0x9D, 0x00, 0xB2, 0x40, -0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0xFF, 0xFF, 0x00, -0x5A, 0xFF, 0xF6, 0x02, 0x80, 0x03, 0x8C, 0x00, 0xA2, 0x00, -0x35, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, -0x00, 0x00, 0x83, 0x00, 0x66, 0x00, 0x4D, 0x00, 0xB2, 0x40, -0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0xFF, 0xFF, 0x00, -0x00, 0x00, 0x00, 0x02, 0x36, 0x03, 0xB0, 0x00, 0xA2, 0x00, -0x39, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, -0x00, 0x00, 0x83, 0x00, 0x60, 0x00, 0xBE, 0x00, 0xB2, 0x40, -0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x02, 0x00, -0x61, 0x00, 0x00, 0x02, 0x2A, 0x02, 0xCA, 0x00, 0x0D, 0x00, -0x16, 0x00, 0x00, 0x01, 0x14, 0x06, 0x06, 0x23, 0x23, 0x15, -0x23, 0x11, 0x33, 0x15, 0x33, 0x32, 0x16, 0x05, 0x32, 0x36, -0x35, 0x34, 0x26, 0x23, 0x23, 0x11, 0x02, 0x2A, 0x34, 0x7D, -0x6D, 0x51, 0x5A, 0x5A, 0x60, 0x91, 0x7E, 0xFE, 0xD9, 0x69, -0x61, 0x57, 0x62, 0x59, 0x01, 0x7E, 0x3C, 0x67, 0x40, 0x9B, -0x02, 0xCA, 0x7C, 0x6E, 0xF9, 0x43, 0x4F, 0x45, 0x43, 0xFE, -0xE6, 0x00, 0x01, 0x00, 0x55, 0xFF, 0xF6, 0x02, 0x4A, 0x02, -0xFD, 0x00, 0x36, 0x00, 0x00, 0x01, 0x14, 0x0E, 0x03, 0x15, -0x14, 0x16, 0x16, 0x17, 0x16, 0x16, 0x15, 0x14, 0x06, 0x23, -0x22, 0x26, 0x27, 0x35, 0x16, 0x16, 0x33, 0x32, 0x36, 0x35, -0x34, 0x26, 0x27, 0x26, 0x26, 0x35, 0x34, 0x3E, 0x03, 0x35, -0x34, 0x26, 0x23, 0x22, 0x06, 0x06, 0x15, 0x11, 0x23, 0x11, -0x34, 0x36, 0x36, 0x33, 0x32, 0x16, 0x02, 0x0A, 0x1C, 0x2A, -0x2A, 0x1C, 0x0D, 0x26, 0x25, 0x36, 0x3E, 0x67, 0x53, 0x2F, -0x48, 0x1A, 0x1A, 0x4C, 0x28, 0x37, 0x30, 0x29, 0x35, 0x3F, -0x2E, 0x1B, 0x29, 0x29, 0x1B, 0x47, 0x38, 0x23, 0x3D, 0x25, -0x58, 0x3A, 0x64, 0x3F, 0x61, 0x77, 0x02, 0x69, 0x22, 0x33, -0x27, 0x20, 0x1F, 0x12, 0x0D, 0x16, 0x1D, 0x19, 0x24, 0x4B, -0x3B, 0x55, 0x4E, 0x12, 0x10, 0x4F, 0x10, 0x1A, 0x2E, 0x28, -0x24, 0x32, 0x22, 0x29, 0x3B, 0x28, 0x1F, 0x2C, 0x21, 0x20, -0x26, 0x1B, 0x2A, 0x26, 0x13, 0x2E, 0x2B, 0xFD, 0xB8, 0x02, -0x48, 0x43, 0x4F, 0x23, 0x4A, 0xFF, 0xFF, 0x00, 0x2E, 0xFF, -0xF6, 0x01, 0xE0, 0x02, 0xFE, 0x00, 0xA2, 0x00, 0x41, 0x00, -0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, -0x82, 0x00, 0x40, 0x6F, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, -0x00, 0x40, 0x00, 0xFF, 0xFF, 0x00, 0x2E, 0xFF, 0xF6, 0x01, -0xE0, 0x02, 0xFE, 0x00, 0xA2, 0x00, 0x41, 0x00, 0x00, 0x40, -0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x83, 0x00, -0x60, 0x00, 0xBC, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, -0x00, 0x40, 0x00, 0xFF, 0xFF, 0x00, 0x2E, 0xFF, 0xF6, 0x01, -0xE0, 0x02, 0xFE, 0x00, 0xA2, 0x00, 0x41, 0x00, 0x00, 0x40, -0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x82, 0x00, -0x62, 0x48, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, -0x00, 0xFF, 0xFF, 0x00, 0x2E, 0xFF, 0xF6, 0x01, 0xE0, 0x02, -0xDF, 0x00, 0xA2, 0x00, 0x41, 0x00, 0x00, 0x40, 0x00, 0x00, -0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x82, 0x00, 0x64, 0x3A, -0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0xFF, -0xFF, 0x00, 0x2E, 0xFF, 0xF6, 0x01, 0xE0, 0x02, 0xDA, 0x00, -0xA2, 0x00, 0x41, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, -0x00, 0x40, 0x00, 0x00, 0x82, 0x00, 0x66, 0xF8, 0x00, 0x40, -0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0xFF, 0xFF, 0x00, -0x2E, 0xFF, 0xF6, 0x01, 0xE0, 0x03, 0x31, 0x00, 0xA2, 0x00, -0x41, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, -0x00, 0x00, 0x83, 0x00, 0x68, 0x00, 0x83, 0x00, 0x00, 0x40, -0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x03, 0x00, -0x2E, 0xFF, 0xF6, 0x03, 0x2D, 0x02, 0x22, 0x00, 0x2C, 0x00, -0x33, 0x00, 0x3E, 0x00, 0x00, 0x01, 0x32, 0x16, 0x16, 0x15, -0x15, 0x21, 0x16, 0x16, 0x33, 0x32, 0x36, 0x37, 0x15, 0x06, -0x06, 0x23, 0x22, 0x27, 0x06, 0x06, 0x23, 0x22, 0x26, 0x35, -0x34, 0x36, 0x37, 0x37, 0x35, 0x34, 0x26, 0x23, 0x22, 0x06, -0x07, 0x27, 0x36, 0x36, 0x33, 0x32, 0x16, 0x17, 0x36, 0x36, -0x17, 0x22, 0x06, 0x07, 0x33, 0x34, 0x26, 0x05, 0x06, 0x06, -0x15, 0x14, 0x16, 0x33, 0x32, 0x36, 0x35, 0x35, 0x02, 0x5B, -0x41, 0x5E, 0x33, 0xFE, 0xA9, 0x02, 0x4F, 0x4A, 0x32, 0x4C, -0x26, 0x28, 0x4D, 0x32, 0x8D, 0x3E, 0x22, 0x5C, 0x4D, 0x49, -0x61, 0x78, 0x7C, 0x5A, 0x3D, 0x33, 0x28, 0x4D, 0x21, 0x1B, -0x23, 0x64, 0x31, 0x3E, 0x51, 0x15, 0x1A, 0x54, 0x35, 0x3A, -0x43, 0x05, 0xF8, 0x39, 0xFE, 0x98, 0x5E, 0x48, 0x33, 0x2A, -0x3F, 0x55, 0x02, 0x22, 0x3C, 0x6C, 0x48, 0x36, 0x60, 0x5B, -0x13, 0x12, 0x4D, 0x12, 0x11, 0x71, 0x34, 0x3D, 0x4D, 0x52, -0x50, 0x57, 0x04, 0x03, 0x22, 0x41, 0x34, 0x18, 0x11, 0x42, -0x14, 0x1A, 0x29, 0x2D, 0x29, 0x2E, 0x48, 0x4F, 0x4A, 0x45, -0x54, 0xD7, 0x04, 0x38, 0x33, 0x2D, 0x2A, 0x4B, 0x4E, 0x30, -0xFF, 0xFF, 0x00, 0x37, 0xFF, 0x10, 0x01, 0xBF, 0x02, 0x22, -0x00, 0xA2, 0x00, 0x43, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, -0x00, 0x00, 0x40, 0x00, 0x00, 0x83, 0x00, 0x6B, 0x00, 0xAA, -0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, -0xFF, 0xFF, 0x00, 0x37, 0xFF, 0xF6, 0x02, 0x01, 0x02, 0xFE, -0x00, 0xA2, 0x00, 0x45, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, -0x00, 0x00, 0x40, 0x00, 0x00, 0x82, 0x00, 0x40, 0x73, 0x00, -0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0xFF, 0xFF, -0x00, 0x37, 0xFF, 0xF6, 0x02, 0x01, 0x02, 0xFE, 0x00, 0xA2, -0x00, 0x45, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, -0x40, 0x00, 0x00, 0x83, 0x00, 0x60, 0x00, 0xC0, 0x00, 0x00, -0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0xFF, 0xFF, -0x00, 0x37, 0xFF, 0xF6, 0x02, 0x01, 0x02, 0xFE, 0x00, 0xA2, -0x00, 0x45, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, -0x40, 0x00, 0x00, 0x82, 0x00, 0x62, 0x4C, 0x00, 0x40, 0x00, -0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0xFF, 0xFF, 0x00, 0x37, -0xFF, 0xF6, 0x02, 0x01, 0x02, 0xDA, 0x00, 0xA2, 0x00, 0x45, -0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, -0x00, 0x82, 0x00, 0x66, 0xFC, 0x00, 0x40, 0x00, 0x00, 0x00, -0x00, 0x00, 0x40, 0x00, 0x00, 0x01, 0x00, 0x55, 0x00, 0x00, -0x00, 0xAD, 0x02, 0x18, 0x00, 0x03, 0x00, 0x00, 0x33, 0x23, -0x11, 0x33, 0xAD, 0x58, 0x58, 0x02, 0x18, 0xFF, 0xFF, 0xFF, -0xFF, 0x00, 0x00, 0x00, 0xC8, 0x02, 0xFE, 0x00, 0xA2, 0x00, -0x91, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, -0x00, 0x00, 0x82, 0x00, 0x40, 0xD7, 0x00, 0x40, 0x00, 0x00, -0x00, 0x00, 0x00, 0x40, 0x00, 0xFF, 0xFF, 0x00, 0x4C, 0x00, -0x00, 0x01, 0x15, 0x02, 0xFE, 0x00, 0xA2, 0x00, 0x91, 0x00, -0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, -0x82, 0x00, 0x60, 0x24, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, -0x00, 0x40, 0x00, 0xFF, 0xFF, 0xFF, 0xD8, 0x00, 0x00, 0x01, -0x2A, 0x02, 0xFE, 0x00, 0xA2, 0x00, 0x91, 0x00, 0x00, 0x40, -0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x82, 0x00, -0x62, 0xB0, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, -0x00, 0xFF, 0xFF, 0xFF, 0xF5, 0x00, 0x00, 0x01, 0x0E, 0x02, -0xDA, 0x00, 0xA2, 0x00, 0x91, 0x00, 0x00, 0x40, 0x00, 0x00, -0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x83, 0x00, 0x66, 0xFF, -0x60, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, -0x00, 0x00, 0x02, 0x00, 0x37, 0xFF, 0xF6, 0x02, 0x27, 0x02, -0xFD, 0x00, 0x20, 0x00, 0x2C, 0x00, 0x00, 0x13, 0x16, 0x16, -0x17, 0x37, 0x17, 0x07, 0x16, 0x16, 0x15, 0x14, 0x06, 0x23, -0x22, 0x26, 0x26, 0x35, 0x34, 0x36, 0x33, 0x32, 0x16, 0x17, -0x37, 0x26, 0x26, 0x27, 0x07, 0x27, 0x37, 0x26, 0x26, 0x27, -0x13, 0x22, 0x06, 0x15, 0x14, 0x16, 0x33, 0x32, 0x36, 0x35, -0x34, 0x26, 0xD8, 0x20, 0x41, 0x1D, 0x73, 0x26, 0x63, 0x44, -0x57, 0x86, 0x74, 0x48, 0x6F, 0x3F, 0x7F, 0x6C, 0x35, 0x4F, -0x18, 0x04, 0x10, 0x42, 0x2A, 0x82, 0x26, 0x70, 0x15, 0x2E, -0x17, 0x7B, 0x54, 0x4B, 0x4C, 0x53, 0x53, 0x4C, 0x4E, 0x02, -0xFD, 0x0F, 0x24, 0x15, 0x43, 0x36, 0x39, 0x40, 0xBC, 0x7A, -0x8E, 0x8F, 0x3B, 0x6D, 0x4B, 0x70, 0x80, 0x1C, 0x1E, 0x02, -0x39, 0x60, 0x26, 0x4B, 0x37, 0x40, 0x0E, 0x1B, 0x0C, 0xFE, -0xD1, 0x59, 0x53, 0x49, 0x5F, 0x61, 0x5C, 0x3E, 0x59, 0xFF, -0xFF, 0x00, 0x55, 0x00, 0x00, 0x02, 0x19, 0x02, 0xDF, 0x00, -0xA2, 0x00, 0x4E, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, -0x00, 0x40, 0x00, 0x00, 0x82, 0x00, 0x64, 0x56, 0x00, 0x40, -0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0xFF, 0xFF, 0x00, -0x37, 0xFF, 0xF6, 0x02, 0x27, 0x02, 0xFE, 0x00, 0xA2, 0x00, -0x4F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, -0x00, 0x00, 0x83, 0x00, 0x40, 0x00, 0x85, 0x00, 0x00, 0x40, -0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0xFF, 0xFF, 0x00, -0x37, 0xFF, 0xF6, 0x02, 0x27, 0x02, 0xFE, 0x00, 0xA2, 0x00, -0x4F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, -0x00, 0x00, 0x83, 0x00, 0x60, 0x00, 0xD2, 0x00, 0x00, 0x40, -0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0xFF, 0xFF, 0x00, -0x37, 0xFF, 0xF6, 0x02, 0x27, 0x02, 0xFE, 0x00, 0xA2, 0x00, -0x4F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, -0x00, 0x00, 0x82, 0x00, 0x62, 0x5E, 0x00, 0x40, 0x00, 0x00, -0x00, 0x00, 0x00, 0x40, 0x00, 0xFF, 0xFF, 0x00, 0x37, 0xFF, -0xF6, 0x02, 0x27, 0x02, 0xDF, 0x00, 0xA2, 0x00, 0x4F, 0x00, -0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, -0x82, 0x00, 0x64, 0x50, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, -0x00, 0x40, 0x00, 0xFF, 0xFF, 0x00, 0x37, 0xFF, 0xF6, 0x02, -0x27, 0x02, 0xDA, 0x00, 0xA2, 0x00, 0x4F, 0x00, 0x00, 0x40, -0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x82, 0x00, -0x66, 0x0E, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, -0x00, 0x00, 0x03, 0x00, 0x32, 0x00, 0x79, 0x02, 0x09, 0x02, -0x47, 0x00, 0x0B, 0x00, 0x0F, 0x00, 0x1B, 0x00, 0x00, 0x01, -0x22, 0x26, 0x35, 0x34, 0x36, 0x33, 0x32, 0x16, 0x15, 0x14, -0x06, 0x05, 0x35, 0x21, 0x15, 0x07, 0x22, 0x26, 0x35, 0x34, -0x36, 0x33, 0x32, 0x16, 0x15, 0x14, 0x06, 0x01, 0x1D, 0x17, -0x21, 0x21, 0x17, 0x17, 0x20, 0x20, 0xFE, 0xFE, 0x01, 0xD7, -0xEC, 0x17, 0x21, 0x21, 0x17, 0x17, 0x20, 0x20, 0x01, 0xCE, -0x1D, 0x20, 0x22, 0x1A, 0x1A, 0x22, 0x20, 0x1D, 0x91, 0x47, -0x47, 0xC4, 0x1D, 0x20, 0x22, 0x1A, 0x1A, 0x22, 0x20, 0x1D, -0x00, 0x03, 0x00, 0x37, 0xFF, 0xDF, 0x02, 0x27, 0x02, 0x36, -0x00, 0x15, 0x00, 0x1E, 0x00, 0x26, 0x00, 0x00, 0x01, 0x14, -0x06, 0x23, 0x22, 0x27, 0x07, 0x27, 0x37, 0x26, 0x26, 0x35, -0x34, 0x36, 0x33, 0x32, 0x17, 0x37, 0x17, 0x07, 0x16, 0x16, -0x05, 0x14, 0x16, 0x17, 0x13, 0x26, 0x23, 0x22, 0x06, 0x05, -0x34, 0x27, 0x03, 0x16, 0x33, 0x32, 0x36, 0x02, 0x27, 0x87, -0x73, 0x49, 0x38, 0x28, 0x3A, 0x2D, 0x1F, 0x21, 0x86, 0x73, -0x49, 0x3A, 0x27, 0x3B, 0x2D, 0x1D, 0x22, 0xFE, 0x6B, 0x0B, -0x0D, 0xDC, 0x24, 0x34, 0x52, 0x4A, 0x01, 0x3A, 0x17, 0xDC, -0x22, 0x34, 0x51, 0x4C, 0x01, 0x0D, 0x85, 0x92, 0x21, 0x38, -0x27, 0x3E, 0x24, 0x65, 0x40, 0x85, 0x90, 0x24, 0x38, 0x26, -0x3F, 0x23, 0x63, 0x3E, 0x26, 0x41, 0x19, 0x01, 0x32, 0x19, -0x6C, 0x5F, 0x4A, 0x31, 0xFE, 0xCE, 0x17, 0x6F, 0xFF, 0xFF, -0x00, 0x4F, 0xFF, 0xF6, 0x02, 0x15, 0x02, 0xFE, 0x00, 0xA2, -0x00, 0x55, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, -0x40, 0x00, 0x00, 0x83, 0x00, 0x40, 0x00, 0x8B, 0x00, 0x00, -0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0xFF, 0xFF, -0x00, 0x4F, 0xFF, 0xF6, 0x02, 0x15, 0x02, 0xFE, 0x00, 0xA2, -0x00, 0x55, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, -0x40, 0x00, 0x00, 0x83, 0x00, 0x60, 0x00, 0xD8, 0x00, 0x00, -0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0xFF, 0xFF, -0x00, 0x4F, 0xFF, 0xF6, 0x02, 0x15, 0x02, 0xFE, 0x00, 0xA2, -0x00, 0x55, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, -0x40, 0x00, 0x00, 0x82, 0x00, 0x62, 0x64, 0x00, 0x40, 0x00, -0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0xFF, 0xFF, 0x00, 0x4F, -0xFF, 0xF6, 0x02, 0x15, 0x02, 0xDA, 0x00, 0xA2, 0x00, 0x55, -0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, -0x00, 0x82, 0x00, 0x66, 0x14, 0x00, 0x40, 0x00, 0x00, 0x00, -0x00, 0x00, 0x40, 0x00, 0xFF, 0xFF, 0x00, 0x01, 0xFF, 0x10, -0x01, 0xFE, 0x02, 0xFE, 0x00, 0xA2, 0x00, 0x59, 0x00, 0x00, -0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x83, -0x00, 0x60, 0x00, 0xA2, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, -0x00, 0x00, 0x40, 0x00, 0x00, 0x02, 0x00, 0x55, 0xFF, 0x10, -0x02, 0x30, 0x02, 0xF8, 0x00, 0x19, 0x00, 0x26, 0x00, 0x00, -0x01, 0x14, 0x06, 0x23, 0x22, 0x26, 0x27, 0x23, 0x1E, 0x02, -0x15, 0x15, 0x23, 0x11, 0x33, 0x15, 0x14, 0x06, 0x07, 0x33, -0x36, 0x36, 0x33, 0x32, 0x16, 0x07, 0x34, 0x26, 0x23, 0x22, -0x06, 0x07, 0x15, 0x14, 0x16, 0x33, 0x32, 0x36, 0x02, 0x30, -0x79, 0x63, 0x3F, 0x50, 0x18, 0x06, 0x01, 0x03, 0x02, 0x58, -0x58, 0x02, 0x01, 0x04, 0x18, 0x4E, 0x40, 0x63, 0x79, 0x5B, -0x46, 0x4A, 0x52, 0x44, 0x02, 0x41, 0x58, 0x4A, 0x45, 0x01, -0x0D, 0x89, 0x8E, 0x2E, 0x20, 0x07, 0x20, 0x22, 0x0B, 0xE0, -0x03, 0xE8, 0xE0, 0x0E, 0x2D, 0x0D, 0x22, 0x30, 0x8C, 0x88, -0x65, 0x65, 0x5C, 0x5C, 0x13, 0x63, 0x6B, 0x6B, 0xFF, 0xFF, -0x00, 0x01, 0xFF, 0x10, 0x01, 0xFE, 0x02, 0xDA, 0x00, 0xA2, -0x00, 0x59, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, -0x40, 0x00, 0x00, 0x82, 0x00, 0x66, 0xDE, 0x00, 0x40, 0x00, -0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x01, -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xAC, 0x05, 0xAC, 0xE3, -0x5F, 0x0F, 0x3C, 0xF5, 0x00, 0x03, 0x08, 0x00, 0x00, 0x00, -0x00, 0x00, 0xE0, 0x7F, 0x10, 0x0D, 0x00, 0x00, 0x00, 0x00, -0xE0, 0x7F, 0x10, 0x0D, 0xFD, 0x93, 0xFE, 0x76, 0x0A, 0xF0, -0x05, 0x43, 0x00, 0x00, 0x00, 0x06, 0x00, 0x02, 0x00, 0x01, -0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, -0x00, 0x00, 0x00, 0x00, 0x03, 0xA7, 0xFF, 0xB2, 0xFF, 0xD5, -0x03, 0x95, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA6, -0x02, 0x58, 0x00, 0x00, 0x01, 0x0D, 0x00, 0x48, 0x01, 0x98, -0x00, 0x41, 0x02, 0x86, 0x00, 0x19, 0x02, 0x3C, 0x00, 0x3E, -0x03, 0x3F, 0x00, 0x31, 0x02, 0xDC, 0x00, 0x35, 0x00, 0xE1, -0x00, 0x41, 0x01, 0x2C, 0x00, 0x28, 0x01, 0x2C, 0x00, 0x1E, -0x02, 0x27, 0x00, 0x29, 0x02, 0x3C, 0x00, 0x32, 0x01, 0x0C, -0x00, 0x29, 0x01, 0x42, 0x00, 0x28, 0x01, 0x0C, 0x00, 0x48, -0x01, 0x74, 0x00, 0x0A, 0x02, 0x3C, 0x00, 0x31, 0x02, 0x3C, -0x00, 0x59, 0x02, 0x3C, 0x00, 0x30, 0x02, 0x3C, 0x00, 0x2D, -0x02, 0x3C, 0x00, 0x15, 0x02, 0x3C, 0x00, 0x3F, 0x02, 0x3C, -0x00, 0x37, 0x02, 0x3C, 0x00, 0x2C, 0x02, 0x3C, 0x00, 0x31, -0x02, 0x3C, 0x00, 0x32, 0x01, 0x0C, 0x00, 0x48, 0x01, 0x0C, -0x00, 0x1F, 0x02, 0x3C, 0x00, 0x32, 0x02, 0x3C, 0x00, 0x38, -0x02, 0x3C, 0x00, 0x32, 0x01, 0xB2, 0x00, 0x0C, 0x03, 0x83, -0x00, 0x3A, 0x02, 0x7F, 0x00, 0x00, 0x02, 0x8A, 0x00, 0x61, -0x02, 0x78, 0x00, 0x3D, 0x02, 0xDA, 0x00, 0x61, 0x02, 0x2C, -0x00, 0x61, 0x02, 0x07, 0x00, 0x61, 0x02, 0xD8, 0x00, 0x3D, -0x02, 0xE5, 0x00, 0x61, 0x01, 0x53, 0x00, 0x28, 0x01, 0x11, -0xFF, 0xB2, 0x02, 0x6B, 0x00, 0x61, 0x02, 0x0C, 0x00, 0x61, -0x03, 0x8B, 0x00, 0x61, 0x02, 0xF8, 0x00, 0x61, 0x03, 0x0D, -0x00, 0x3D, 0x02, 0x5D, 0x00, 0x61, 0x03, 0x0D, 0x00, 0x3D, -0x02, 0x6E, 0x00, 0x61, 0x02, 0x25, 0x00, 0x33, 0x02, 0x2C, -0x00, 0x0A, 0x02, 0xDB, 0x00, 0x5A, 0x02, 0x58, 0x00, 0x00, -0x03, 0xA2, 0x00, 0x0C, 0x02, 0x4A, 0x00, 0x04, 0x02, 0x36, -0x00, 0x00, 0x02, 0x3C, 0x00, 0x26, 0x01, 0x49, 0x00, 0x50, -0x01, 0x74, 0x00, 0x0A, 0x01, 0x49, 0x00, 0x19, 0x02, 0x3C, -0x00, 0x26, 0x01, 0xBC, 0xFF, 0xFE, 0x01, 0x19, 0x00, 0x28, -0x02, 0x31, 0x00, 0x2E, 0x02, 0x67, 0x00, 0x55, 0x01, 0xE0, -0x00, 0x37, 0x02, 0x67, 0x00, 0x37, 0x02, 0x34, 0x00, 0x37, -0x01, 0x58, 0x00, 0x0F, 0x02, 0x67, 0x00, 0x37, 0x02, 0x6A, -0x00, 0x55, 0x01, 0x02, 0x00, 0x4E, 0x01, 0x02, 0xFF, 0xC9, -0x02, 0x16, 0x00, 0x55, 0x01, 0x02, 0x00, 0x55, 0x03, 0xA7, -0x00, 0x55, 0x02, 0x6A, 0x00, 0x55, 0x02, 0x5D, 0x00, 0x37, -0x02, 0x67, 0x00, 0x55, 0x02, 0x67, 0x00, 0x37, 0x01, 0x9D, -0x00, 0x55, 0x01, 0xDF, 0x00, 0x33, 0x01, 0x69, 0x00, 0x10, -0x02, 0x6A, 0x00, 0x4F, 0x01, 0xFC, 0x00, 0x00, 0x03, 0x12, -0x00, 0x0B, 0x02, 0x11, 0x00, 0x12, 0x01, 0xFE, 0x00, 0x01, -0x01, 0xD6, 0x00, 0x27, 0x01, 0x7C, 0x00, 0x1C, 0x02, 0x27, -0x00, 0xEF, 0x01, 0x7C, 0x00, 0x20, 0x02, 0x3C, 0x00, 0x32, -0x02, 0x7F, 0x00, 0x00, 0x01, 0x19, 0x00, 0x28, 0x02, 0x7F, -0x00, 0x00, 0x01, 0xA2, 0x00, 0x28, 0x02, 0x7F, 0x00, 0x00, -0x01, 0xBF, 0x00, 0x28, 0x02, 0x7F, 0x00, 0x00, 0x02, 0x44, -0x00, 0x95, 0x02, 0x7F, 0x00, 0x00, 0x01, 0x2C, 0x00, 0x28, -0x02, 0x7F, 0x00, 0x00, 0x03, 0x71, 0xFF, 0xFF, 0x00, 0xE1, -0x00, 0x0E, 0x02, 0x78, 0x00, 0x3D, 0x02, 0x2C, 0x00, 0x61, -0x02, 0x2C, 0x00, 0x61, 0x02, 0x2C, 0x00, 0x61, 0x02, 0x2C, -0x00, 0x61, 0x01, 0x53, 0x00, 0x28, 0x01, 0x53, 0x00, 0x28, -0x01, 0x53, 0x00, 0x01, 0x01, 0x53, 0x00, 0x1E, 0x02, 0xDA, -0x00, 0x1E, 0x02, 0xF8, 0x00, 0x61, 0x03, 0x0D, 0x00, 0x3D, -0x03, 0x0D, 0x00, 0x3D, 0x03, 0x0D, 0x00, 0x3D, 0x03, 0x0D, -0x00, 0x3D, 0x03, 0x0D, 0x00, 0x3D, 0x02, 0x3C, 0x00, 0x40, -0x03, 0x0D, 0x00, 0x3D, 0x02, 0xDB, 0x00, 0x5A, 0x02, 0xDB, -0x00, 0x5A, 0x02, 0xDB, 0x00, 0x5A, 0x02, 0xDB, 0x00, 0x5A, -0x02, 0x36, 0x00, 0x00, 0x02, 0x5D, 0x00, 0x61, 0x02, 0x77, -0x00, 0x55, 0x02, 0x31, 0x00, 0x2E, 0x02, 0x31, 0x00, 0x2E, -0x02, 0x31, 0x00, 0x2E, 0x02, 0x31, 0x00, 0x2E, 0x02, 0x31, -0x00, 0x2E, 0x02, 0x31, 0x00, 0x2E, 0x03, 0x60, 0x00, 0x2E, -0x01, 0xE0, 0x00, 0x37, 0x02, 0x34, 0x00, 0x37, 0x02, 0x34, -0x00, 0x37, 0x02, 0x34, 0x00, 0x37, 0x02, 0x34, 0x00, 0x37, -0x01, 0x02, 0x00, 0x55, 0x01, 0x02, 0xFF, 0xFF, 0x01, 0x02, -0x00, 0x4C, 0x01, 0x02, 0xFF, 0xD8, 0x01, 0x02, 0xFF, 0xF5, -0x02, 0x5D, 0x00, 0x37, 0x02, 0x6A, 0x00, 0x55, 0x02, 0x5D, -0x00, 0x37, 0x02, 0x5D, 0x00, 0x37, 0x02, 0x5D, 0x00, 0x37, -0x02, 0x5D, 0x00, 0x37, 0x02, 0x5D, 0x00, 0x37, 0x02, 0x3C, -0x00, 0x32, 0x02, 0x5D, 0x00, 0x37, 0x02, 0x6A, 0x00, 0x4F, -0x02, 0x6A, 0x00, 0x4F, 0x02, 0x6A, 0x00, 0x4F, 0x02, 0x6A, -0x00, 0x4F, 0x01, 0xFE, 0x00, 0x01, 0x02, 0x67, 0x00, 0x55, -0x01, 0xFE, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, -0x00, 0x38, 0x00, 0x00, 0x00, 0x71, 0x00, 0x00, 0x00, 0x9C, -0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x01, 0x91, 0x00, 0x00, -0x02, 0x22, 0x00, 0x00, 0x02, 0xC5, 0x00, 0x00, 0x02, 0xE1, -0x00, 0x00, 0x03, 0x15, 0x00, 0x00, 0x03, 0x4A, 0x00, 0x00, -0x03, 0x87, 0x00, 0x00, 0x03, 0xB0, 0x00, 0x00, 0x03, 0xD7, -0x00, 0x00, 0x03, 0xEE, 0x00, 0x00, 0x04, 0x19, 0x00, 0x00, -0x04, 0x38, 0x00, 0x00, 0x04, 0x8B, 0x00, 0x00, 0x04, 0xBD, -0x00, 0x00, 0x05, 0x14, 0x00, 0x00, 0x05, 0x8F, 0x00, 0x00, -0x05, 0xD9, 0x00, 0x00, 0x06, 0x3A, 0x00, 0x00, 0x06, 0xBC, -0x00, 0x00, 0x06, 0xE0, 0x00, 0x00, 0x07, 0x81, 0x00, 0x00, -0x08, 0x04, 0x00, 0x00, 0x08, 0x4F, 0x00, 0x00, 0x08, 0x99, -0x00, 0x00, 0x08, 0xBF, 0x00, 0x00, 0x08, 0xE5, 0x00, 0x00, -0x09, 0x0A, 0x00, 0x00, 0x09, 0x89, 0x00, 0x00, 0x0A, 0x61, -0x00, 0x00, 0x0A, 0xA9, 0x00, 0x00, 0x0B, 0x12, 0x00, 0x00, -0x0B, 0x67, 0x00, 0x00, 0x0B, 0xA6, 0x00, 0x00, 0x0B, 0xD4, -0x00, 0x00, 0x0B, 0xFB, 0x00, 0x00, 0x0C, 0x5F, 0x00, 0x00, -0x0C, 0x8D, 0x00, 0x00, 0x0C, 0xBC, 0x00, 0x00, 0x0C, 0xF6, -0x00, 0x00, 0x0D, 0x30, 0x00, 0x00, 0x0D, 0x4D, 0x00, 0x00, -0x0D, 0x98, 0x00, 0x00, 0x0D, 0xDA, 0x00, 0x00, 0x0E, 0x33, -0x00, 0x00, 0x0E, 0x79, 0x00, 0x00, 0x0E, 0xDF, 0x00, 0x00, -0x0F, 0x30, 0x00, 0x00, 0x0F, 0xAC, 0x00, 0x00, 0x0F, 0xCD, -0x00, 0x00, 0x10, 0x0B, 0x00, 0x00, 0x10, 0x44, 0x00, 0x00, -0x10, 0xB7, 0x00, 0x00, 0x10, 0xEC, 0x00, 0x00, 0x11, 0x17, -0x00, 0x00, 0x11, 0x44, 0x00, 0x00, 0x11, 0x66, 0x00, 0x00, -0x11, 0x84, 0x00, 0x00, 0x11, 0xA5, 0x00, 0x00, 0x11, 0xCB, -0x00, 0x00, 0x11, 0xE5, 0x00, 0x00, 0x12, 0x13, 0x00, 0x00, -0x12, 0x89, 0x00, 0x00, 0x12, 0xEE, 0x00, 0x00, 0x13, 0x42, -0x00, 0x00, 0x13, 0xA9, 0x00, 0x00, 0x14, 0x0D, 0x00, 0x00, -0x14, 0x58, 0x00, 0x00, 0x14, 0xD5, 0x00, 0x00, 0x15, 0x1A, -0x00, 0x00, 0x15, 0x53, 0x00, 0x00, 0x15, 0xAB, 0x00, 0x00, -0x15, 0xEF, 0x00, 0x00, 0x16, 0x06, 0x00, 0x00, 0x16, 0x6A, -0x00, 0x00, 0x16, 0xAC, 0x00, 0x00, 0x16, 0xFF, 0x00, 0x00, -0x17, 0x6B, 0x00, 0x00, 0x17, 0xD4, 0x00, 0x00, 0x18, 0x19, -0x00, 0x00, 0x18, 0x94, 0x00, 0x00, 0x18, 0xE1, 0x00, 0x00, -0x19, 0x23, 0x00, 0x00, 0x19, 0x5F, 0x00, 0x00, 0x19, 0xD6, -0x00, 0x00, 0x1A, 0x08, 0x00, 0x00, 0x1A, 0x63, 0x00, 0x00, -0x1A, 0x90, 0x00, 0x00, 0x1A, 0xE8, 0x00, 0x00, 0x1B, 0x01, -0x00, 0x00, 0x1B, 0x58, 0x00, 0x00, 0x1B, 0xA6, 0x00, 0x00, -0x1B, 0xCE, 0x00, 0x00, 0x1B, 0xFC, 0x00, 0x00, 0x1C, 0x24, -0x00, 0x00, 0x1C, 0x66, 0x00, 0x00, 0x1C, 0x8E, 0x00, 0x00, -0x1C, 0xD6, 0x00, 0x00, 0x1C, 0xFE, 0x00, 0x00, 0x1D, 0x49, -0x00, 0x00, 0x1D, 0x71, 0x00, 0x00, 0x1D, 0xBC, 0x00, 0x00, -0x1D, 0xE4, 0x00, 0x00, 0x1E, 0x2A, 0x00, 0x00, 0x1E, 0x6E, -0x00, 0x00, 0x1E, 0x96, 0x00, 0x00, 0x1E, 0xBE, 0x00, 0x00, -0x1E, 0xE6, 0x00, 0x00, 0x1F, 0x0E, 0x00, 0x00, 0x1F, 0x36, -0x00, 0x00, 0x1F, 0x5E, 0x00, 0x00, 0x1F, 0x86, 0x00, 0x00, -0x1F, 0xAE, 0x00, 0x00, 0x1F, 0xD6, 0x00, 0x00, 0x20, 0x26, -0x00, 0x00, 0x20, 0x4E, 0x00, 0x00, 0x20, 0x76, 0x00, 0x00, -0x20, 0x9E, 0x00, 0x00, 0x20, 0xC6, 0x00, 0x00, 0x20, 0xEE, -0x00, 0x00, 0x21, 0x16, 0x00, 0x00, 0x21, 0x4A, 0x00, 0x00, -0x21, 0xD5, 0x00, 0x00, 0x21, 0xFD, 0x00, 0x00, 0x22, 0x25, -0x00, 0x00, 0x22, 0x4D, 0x00, 0x00, 0x22, 0x75, 0x00, 0x00, -0x22, 0x9D, 0x00, 0x00, 0x22, 0xE7, 0x00, 0x00, 0x23, 0x81, -0x00, 0x00, 0x23, 0xA7, 0x00, 0x00, 0x23, 0xCF, 0x00, 0x00, -0x23, 0xF5, 0x00, 0x00, 0x24, 0x1B, 0x00, 0x00, 0x24, 0x41, -0x00, 0x00, 0x24, 0x69, 0x00, 0x00, 0x25, 0x20, 0x00, 0x00, -0x25, 0x48, 0x00, 0x00, 0x25, 0x6E, 0x00, 0x00, 0x25, 0x96, -0x00, 0x00, 0x25, 0xBC, 0x00, 0x00, 0x25, 0xE2, 0x00, 0x00, -0x25, 0xF9, 0x00, 0x00, 0x26, 0x1F, 0x00, 0x00, 0x26, 0x45, -0x00, 0x00, 0x26, 0x6B, 0x00, 0x00, 0x26, 0x93, 0x00, 0x00, -0x27, 0x1D, 0x00, 0x00, 0x27, 0x43, 0x00, 0x00, 0x27, 0x6B, -0x00, 0x00, 0x27, 0x93, 0x00, 0x00, 0x27, 0xB9, 0x00, 0x00, -0x27, 0xDF, 0x00, 0x00, 0x28, 0x05, 0x00, 0x00, 0x28, 0x5E, -0x00, 0x00, 0x28, 0xDE, 0x00, 0x00, 0x29, 0x06, 0x00, 0x00, -0x29, 0x2E, 0x00, 0x00, 0x29, 0x54, 0x00, 0x00, 0x29, 0x7A, -0x00, 0x00, 0x29, 0xA2, 0x00, 0x00, 0x2A, 0x14, 0x00, 0x00, -0x2A, 0x3A, 0x00, 0x01, 0x00, 0x00, 0x00, 0xA6, 0x10, 0x00, -0x04, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x02, 0x00, 0x00, -0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, -0x00, 0xFF, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x96, -0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x0D, -0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, -0x00, 0x07, 0x00, 0x0D, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, -0x00, 0x03, 0x00, 0x21, 0x00, 0x14, 0x00, 0x01, 0x00, 0x00, -0x00, 0x00, 0x00, 0x04, 0x00, 0x0D, 0x00, 0x35, 0x00, 0x01, -0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x42, -0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x0D, -0x00, 0x45, 0x00, 0x03, 0x00, 0x01, 0x04, 0x09, 0x00, 0x01, -0x00, 0x1A, 0x00, 0x52, 0x00, 0x03, 0x00, 0x01, 0x04, 0x09, -0x00, 0x02, 0x00, 0x0E, 0x00, 0x6C, 0x00, 0x03, 0x00, 0x01, -0x04, 0x09, 0x00, 0x03, 0x00, 0x42, 0x00, 0x7A, 0x00, 0x03, -0x00, 0x01, 0x04, 0x09, 0x00, 0x04, 0x00, 0x1A, 0x00, 0xBC, -0x00, 0x03, 0x00, 0x01, 0x04, 0x09, 0x00, 0x05, 0x00, 0x06, -0x00, 0xD6, 0x00, 0x03, 0x00, 0x01, 0x04, 0x09, 0x00, 0x06, -0x00, 0x1A, 0x00, 0xDC, 0x47, 0x65, 0x6E, 0x65, 0x72, 0x61, -0x74, 0x65, 0x64, 0x46, 0x6F, 0x6E, 0x74, 0x52, 0x65, 0x67, -0x75, 0x6C, 0x61, 0x72, 0x47, 0x65, 0x6E, 0x65, 0x72, 0x61, -0x74, 0x65, 0x64, 0x46, 0x6F, 0x6E, 0x74, 0x20, 0x52, 0x65, -0x67, 0x75, 0x6C, 0x61, 0x72, 0x3A, 0x56, 0x65, 0x72, 0x73, -0x69, 0x6F, 0x6E, 0x20, 0x31, 0x2E, 0x30, 0x47, 0x65, 0x6E, -0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x46, 0x6F, 0x6E, 0x74, -0x31, 0x2E, 0x30, 0x47, 0x65, 0x6E, 0x65, 0x72, 0x61, 0x74, -0x65, 0x64, 0x46, 0x6F, 0x6E, 0x74, 0x00, 0x47, 0x00, 0x65, -0x00, 0x6E, 0x00, 0x65, 0x00, 0x72, 0x00, 0x61, 0x00, 0x74, -0x00, 0x65, 0x00, 0x64, 0x00, 0x46, 0x00, 0x6F, 0x00, 0x6E, -0x00, 0x74, 0x00, 0x52, 0x00, 0x65, 0x00, 0x67, 0x00, 0x75, -0x00, 0x6C, 0x00, 0x61, 0x00, 0x72, 0x00, 0x47, 0x00, 0x65, -0x00, 0x6E, 0x00, 0x65, 0x00, 0x72, 0x00, 0x61, 0x00, 0x74, -0x00, 0x65, 0x00, 0x64, 0x00, 0x46, 0x00, 0x6F, 0x00, 0x6E, -0x00, 0x74, 0x00, 0x20, 0x00, 0x52, 0x00, 0x65, 0x00, 0x67, -0x00, 0x75, 0x00, 0x6C, 0x00, 0x61, 0x00, 0x72, 0x00, 0x3A, -0x00, 0x56, 0x00, 0x65, 0x00, 0x72, 0x00, 0x73, 0x00, 0x69, -0x00, 0x6F, 0x00, 0x6E, 0x00, 0x20, 0x00, 0x31, 0x00, 0x2E, -0x00, 0x30, 0x00, 0x47, 0x00, 0x65, 0x00, 0x6E, 0x00, 0x65, -0x00, 0x72, 0x00, 0x61, 0x00, 0x74, 0x00, 0x65, 0x00, 0x64, -0x00, 0x46, 0x00, 0x6F, 0x00, 0x6E, 0x00, 0x74, 0x00, 0x31, -0x00, 0x2E, 0x00, 0x30, 0x00, 0x47, 0x00, 0x65, 0x00, 0x6E, -0x00, 0x65, 0x00, 0x72, 0x00, 0x61, 0x00, 0x74, 0x00, 0x65, -0x00, 0x64, 0x00, 0x46, 0x00, 0x6F, 0x00, 0x6E, 0x00, 0x74, -0x00, 0x04, 0x02, 0x3B, 0x01, 0x90, 0x00, 0x05, 0x00, 0x08, -0x02, 0x8A, 0x02, 0x58, 0x00, 0x00, 0x00, 0x4B, 0x02, 0x8A, -0x02, 0x58, 0x00, 0x00, 0x01, 0x5E, 0x00, 0x32, 0x01, 0x42, -0x00, 0x00, 0x02, 0x0B, 0x05, 0x02, 0x04, 0x05, 0x04, 0x02, -0x02, 0x04, 0xE0, 0x00, 0x82, 0xFF, 0x40, 0x00, 0x20, 0x5F, -0x08, 0x00, 0x00, 0x29, 0x00, 0x10, 0x00, 0x00, 0x47, 0x4F, -0x4F, 0x47, 0x00, 0xC0, 0x00, 0x00, 0xFF, 0xFD, 0x04, 0x2D, -0xFE, 0xDB, 0x00, 0x00, 0x05, 0x43, 0x01, 0x8B, 0x00, 0x00, -0x01, 0x9F, 0x00, 0x00, 0x00, 0x00, 0x02, 0x18, 0x02, 0xCA, -0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x03, 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, 0xB8, 0x01, -0xFF, 0x85, 0xB0, 0x04, 0x8D, 0x00, }; - -#endif diff --git a/src/fonts/open-sans.h b/src/fonts/open-sans.h index df95f21..87f6ca1 100644 --- a/src/fonts/open-sans.h +++ b/src/fonts/open-sans.h @@ -1,5 +1,15 @@ -// SPDX-FileCopyrightText: 2023 ThingPulse Ltd., https://thingpulse.com -// SPDX-License-Identifier: MIT +/*------------------------------------------------------------------------------------------------- +** +** open-sans.h +** +** Contains the binary font data for the Open Sans typeface used in the Spotify display UI. +** This header defines the glyph data for rendering fixed-width bold text on screen. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +*/ // This was created as per https://github.com/takkaO/OpenFontRender/blob/master/examples/TFT_eSPI/load_from_binary/load_from_binary.ino#L1 // The generated font supports the following characters; basic ASCII, accented chars from extended ASCII plus €: diff --git a/src/logTags.h b/src/logTags.h new file mode 100644 index 0000000..8bae597 --- /dev/null +++ b/src/logTags.h @@ -0,0 +1,47 @@ +/*------------------------------------------------------------------------------------------------- +** +** logTags.h +** +** Defines logging tags for various modules in the project. Centralizing +** logging tags ensures consistency and supports structured log filtering +** during development and diagnostics. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2025-01-03 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ + +#pragma once + +#define LOGTAG_GENERAL "General" // General logging +#define LOGTAG_GUI "GUI" // Logging info +#define LOGTAG_INPUT "Input" // Input related +#define LOGTAG_MULTITASK "Multitask" // General logging +#define LOGTAG_PLAYER "Player" // Spotify playing +#define LOGTAG_SONG_DATA "SongData" // Current song data +#define LOGTAG_METRICS "Metrics" // Timings +#define LOGTAG_HEAP "Heap" // Heap +#define LOGTAG_TRACE "Trace" // Explicit tracing for troubleshooting +#define LOGTAG_FILEIO "FileIO" // File IO Library +#define LOGTAG_CACHE "Cache" // Album Cache Logging +#define LOGTAG_DISPLAY_MODE "DispMode" // Display Modes +#define LOGTAG_VAULT "Vault" // Credential Vaulting + +/* +** =================================================================== +** ESP-IDF Log Macros +** These macros are used to log messages at different severity levels. +** Each macro allows for formatted output and optionally supports tags +** for categorizing log messages. +** +** log_e: Error level, used for serious issues or failures. +** log_w: Warning level, used for recoverable issues or potential problems. +** log_i: Info level, used for general informational messages. +** log_d: Debug level, used for detailed debug information during development. +** log_v: Verbose level, used for very detailed and granular debug information. +** =================================================================== +*/ diff --git a/src/main.cpp b/src/main.cpp index def29bb..01ec0e9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,203 +1,408 @@ -// SPDX-FileCopyrightText: 2023 ThingPulse Ltd., https://thingpulse.com -// SPDX-License-Identifier: MIT - -#include - +/*------------------------------------------------------------------------------------------------- +** +** main.cpp +** +** Main entry point for the Spotify Companion application. Initializes system +** components, configures logging, synchronizes time, handles Wi-Fi and Spotify +** authentication, and launches UI and background tasks on the ESP32. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2025-02-09 - Electric Diversions - Major update with refined UI task, touch input, and queue handling. +** ------------------------------------------------------------------------------------------------ +*/ + +/* +** =================================================================== +** Include main library dependencies +** =================================================================== +*/ + +// +// *** Task Scheduler Include *** +// Cooperative multitasking for Arduino, ESPx, STM32 +// and other microcontrollers +// https://github.com/arkhipenko/TaskScheduler +// // Specify all #define statements for task scheduler first #define _TASK_SCHEDULING_OPTIONS //#define _TASK_TIMECRITICAL //#define _TASK_SLEEP_ON_IDLE_RUN #include +// +// *** Open Font Render Include *** +// TTF font render support library for microcomputer using Arduino IDE. +// This library can render TTF font files in the SD card or TTF font files +// embedded in the program. +// https://github.com/takkaO/OpenFontRender +// #include -#include +// +// *** WiFi Include *** +// esp32 Wifi support. Based on WiFi.h from Arduino WiFi shield library. +// Modified by Ivan Grokhotkov, December 2014 +// +#include + +// +// *** WiFi Secure Client Include *** +// Base class that provides Client SSL to ESP32 +// Additions Copyright (C) 2017 Evandro Luis Copercini. +// +#include + +// +// *** HTTP Client Include *** +// 2015 Markus Sattler. This file is part of the HTTPClient for Arduino. +// Port to ESP32 by Evandro Luis Copercini (2017) changed fingerprints to CA verification. +#include + +/* +** =================================================================== +** Local Includes +** =================================================================== +*/ + +#include "esp_task_wdt.h" // FreeRTOS Watchdog Timer + +#include "settings.h" // Global settings to configure app +#include "Vault.h" // Credential management +#include "DisplayUI.h" // Base class for the UI +#include "ThingPulse/connectivity.h" // Connectivity support +#include "SpotifyPlayer.h" // Spotify Player for all the controls +#include "logTags.h" // Tags for logging +#include "SCLogger.h" // Logging framework +#include "Monitor.h" // Monitoring + +#include "ThingPulse/display.h" // ThingPulse display routines +#include "ThingPulse/util.h" // ThingPulse utility routines +#include "SCFileIO.h" // File IO routines - thread safe +#include "SpotifyArtMgr.h" +#include "UIViews/UIViewManager.h" +#include "scui.h" + +/* +** =================================================================== +** Used Fonts +** =================================================================== +*/ #include "fonts/open-sans.h" -#include "GfxUi.h" - -#include "connectivity.h" -#include "display.h" -#include "persistence.h" -#include "settings.h" -#include "util.h" -#include "spotify.h" - - - -// ---------------------------------------------------------------------------- -// Function prototypes (declarations) -// ---------------------------------------------------------------------------- -void drawProgress(const char *text, int8_t percentage); -void initJpegDecoder(); -void initOpenFontRender(); -void initScheduler(); -bool pushImageToTft(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t *bitmap); -void syncTime(); -void repaint(); - - - -// ---------------------------------------------------------------------------- -// Globals -// ---------------------------------------------------------------------------- -OpenFontRender ofr; -FT6236 ts = FT6236(TFT_HEIGHT, TFT_WIDTH); -TFT_eSPI tft = TFT_eSPI(); -TFT_eSprite timeSprite = TFT_eSprite(&tft); -GfxUi ui = GfxUi(&tft, &ofr); - -Scheduler schedule; -// Create a task to run every hour to update the 'clock' -Task clockTask(CLOCK_TASK_INTERVAL_MILLIS, TASK_FOREVER, &syncTime); - -// time management variables -unsigned long lastTimeSyncMillis = 0; - -const int16_t centerWidth = tft.width() / 2; - -String spotifyRefreshToken = ""; - - - -// ---------------------------------------------------------------------------- -// setup() & loop() -// ---------------------------------------------------------------------------- -void setup(void) { - Serial.begin(115200); - delay(1000); - - logBanner(); - logMemoryStats(); - - initJpegDecoder(); - initTouchScreen(&ts); - initTft(&tft); - logDisplayDebugInfo(&tft); - - initFileSystem(); - initOpenFontRender(); - - initSpotifiy(); - - initScheduler(); - - tft.fillScreen(TFT_BLACK); - ui.drawLogo(); - - ofr.setFontSize(16); - ofr.cdrawString(APP_NAME, centerWidth, tft.height() - 50); - ofr.cdrawString(VERSION, centerWidth, tft.height() - 30); - - drawProgress("Starting WiFi...", 10); - if (WiFi.status() != WL_CONNECTED) { - startWiFi(); - } - - drawProgress("Synchronizing time...", 40); - syncTime(); - - drawProgress("Checking Spotify status...", 60); - spotifyRefreshToken = readFsString(SPOTIFY_REFRESH_TOKEN_FILE_NAME); - if (spotifyRefreshToken == "") { - log_i("No Spotify refresh token found. Requesting one through the browser via auth code."); - - drawProgress("Getting Spotify token...", 70); - ofr.cdrawString(String("Open browser at\nhttp://" SPOTIFY_ESPOTIFIER_NODE_NAME ".local").c_str(), centerWidth, 280); - - String spotifyAuthCode = fetchSpotifyAuthCode(); - spotifyRefreshToken = spotify.requestAccessTokens(spotifyAuthCode.c_str(), SPOTIFY_REDIRECT_URI); - saveFsString(SPOTIFY_REFRESH_TOKEN_FILE_NAME, spotifyRefreshToken); - - // clear text beneath the progress bar - tft.fillRect(0, 290, tft.width(), 80, TFT_BLACK); - } else { - log_i("Using previously saved Spotify refresh token."); - } - - drawProgress("Logging into Spotify...", 90); - // The Spotify library - // - keeps track of the refresh token and its TTL internally - // - automatically renews the actual access token using the refresh token - // -> see SpotifyArduino.h#autoTokenRefresh and SpotifyArduino::checkAndRefreshAccessToken() (called before every API function) - spotify.setRefreshToken(spotifyRefreshToken.c_str()); - spotify.refreshAccessToken(); - log_i("Authentication against Spotify done. Refresh token: %s", spotifyRefreshToken.c_str()); - - drawProgress("Startup completed!", 100); -} - -void loop(void) { - // Task execution - schedule.execute(); - - // if (ts.touched()) { - // TS_Point p = ts.getPoint(); - - // uint16_t touchX = p.x; - // uint16_t touchY = p.y; - - // log_d("Touch coordinates: x=%d, y=%d", touchX, touchY); - // // Debouncing; avoid returning the same touch multiple times. - // delay(50); - // } +#include "fonts/cousine-bold.h" + +/* +** =================================================================== +** Local function prototypes +** =================================================================== +*/ +void syncTime(); // syncs the sytem time +void setupLogging(); // set up logging +void initOpenFontRender(); // initialize the font renderer +void initScheduler(); // Initialize the task scheduler +void registerMonitorDescriptions(); // Initialize the monitor descriptions +void processPerformanceMetrics(); // Show metrics on schedule +void handleTouchInput(); // Check if LCD pressed +void dispatchSCUIQueueMessages(); // Dispatch messages to active view + +// Task handles +TaskHandle_t UITaskHandle; +void uiHandlerTask(void *pvParameters); +QueueHandle_t scuiQueue; + +unsigned long delayBetweenRequests = 60000; // Time between requests (1 minute) +unsigned long requestDueTime; // time when request due + + +bool bIsInitialPress = false; +bool bIsInitialLift = false; + +/* +** =================================================================== +** Globals +** =================================================================== +*/ +OpenFontRender ofr; +OpenFontRender clockFont; +FT6236 ts = FT6236(TFT_HEIGHT, TFT_WIDTH); // Touch Controller +TFT_eSPI tft = TFT_eSPI(); // LCD display +DisplayUI ui = DisplayUI(&tft, &ofr, &clockFont); // Routines to update UI +SpotifyPlayer& spotifyPlayer = SpotifyPlayer::getInstance(); // Spotify Player +Vault& vault = Vault::getInstance(); // Credential management + + +// Task Scheduler to use for time sync. clockTask runs every hour to update +// the 'clock'. +Scheduler schedule; +Task clockTask(CLOCK_TASK_INTERVAL_MILLIS, TASK_FOREVER, &syncTime); + +// Time management variables +unsigned long lastTimeSyncMillis = 0; // last time was synced + +/* +** =================================================================== +** setup() - called before the loop. runs just once. +** =================================================================== +*/ + +void setup() +{ + // Initialize serial output + Serial.begin(115200); + delay(3000); // Extended due to early logging being clipped or missing + + // Set up logging levels + setupLogging(); + registerMonitorDescriptions(); + + // Initializd DisplayUI instance + ui.init(); + + // Log the banner and memory stats + logBanner(); + logMemoryStats(); + + // Initialize everything + initTouchScreen(&ts); + initTft(&tft); + logDisplayDebugInfo(&tft); + initOpenFontRender(); + + if (!SCFileIO::getInstance().initialize()) + { + // initialization failed + ui.setBackground(TFTColor::Yellow, true); + ui.cDrawString("FATAL ERROR - Filesystem Not Initialized", 240, 115, 24, TFTColor::Black, ui.getBackground(), ""); + ui.cDrawString("Please verify that the filesystem image", 240, 145, 24, TFTColor::Black, ui.getBackground(), ""); + ui.cDrawString("was uploaded.", 240, 175, 24, TFTColor::Black, ui.getBackground(), ""); + while (true) + { + delay(1000); // Sit here indefinitely + } + } + + vault.initialize(); + + spotifyPlayer.initialize(&scuiQueue); + initScheduler(); + + // Draw logo and app info to screen + ui.setBackground(TFTColor::Black); + ui.drawLogo(); + ui.drawAppInfo(); + + // === Start the start up sequence === + + // Connect WiFi + ui.drawProgress("Starting WiFi...", 10); + if (WiFi.status() != WL_CONNECTED) { + startWiFi(); + } + + // Sync Time + ui.drawProgress("Synchronizing time...", 40); + syncTime(); + + // Prep to log into Spotify + ui.drawProgress("Checking Spotify status...", 60); + if (spotifyPlayer.isRefreshTokenAvailable()) + { + // token found + } + else + { + // clear logo + tft.fillRect(60, 20, tft.width() - 120, 130, TFT_BLACK); + // token not found + ui.drawProgress("Getting Spotify token...", 70); + + String msg; + msg += "From another device\n"; + msg += "on the same network,\n"; + msg += "open a browser at\nhttp://"; + msg += spotifyPlayer.getNodeName(); + msg += ".local"; + + ui.cDrawString(msg.c_str(), ui.getCenterWidth(), 20, 24, TFTColor::Yellow, ui.getBackground(), ""); + spotifyPlayer.requestRefreshToken(); + + // clear instructions + tft.fillRect(0, 20, tft.width(), 300, TFT_BLACK); + ui.drawLogo(); + } + + // Log into Spotify + ui.drawProgress("Logging into Spotify...", 90); + spotifyPlayer.login(); + + vault.eraseEncryptedCredentials(); + + // Update to show complete + ui.drawProgress("Startup completed!", 100); + + delay(1000); + + // Initialize Display Modes + UIViewManager& dm = UIViewManager::getInstance(); + dm.setDisplayUI(&ui); + + spotifyPlayer.startBackgroundRefreshes(); + + // Create task and queue to process the UI + + // Initialize queue + scuiQueue = xQueueCreate(10, sizeof(SCUIMessage)); + if (scuiQueue == NULL) + { + spLogE(LOGTAG_MULTITASK, "Failed to create scuiQueue!"); + } + + spLogI(LOGTAG_MULTITASK, "creating background task - pinned to core 0"); + xTaskCreatePinnedToCore( + uiHandlerTask, // Task function + "UIHandler", // Task name + 8192, // Stack size + NULL, // Task parameter + 2, // Task priority + &UITaskHandle, // Task handle + 0 // Core ID + ); + spLogI(LOGTAG_MULTITASK, "background uiHandlerTask task created"); } - - -// ---------------------------------------------------------------------------- -// Functions -// ---------------------------------------------------------------------------- - -// void drawProgress(const char *text, int8_t percentage) { -// int numberOfLinebreaks = 0; -// int numberOfChars = strlen(text); -// for (int i = 0; i < numberOfChars; i++) { -// if (text[i] == '\n') { -// numberOfLinebreaks++; -// } -// } - -// ofr.setFontSize(24); -// int pbWidth = tft.width() - 100; -// int pbX = (tft.width() - pbWidth)/2; -// int pbY = 260; -// int progressTextY = 210; - -// tft.fillRect(0, progressTextY, tft.width(), 40, TFT_BLACK); -// ofr.cdrawString(text, centerWidth, progressTextY - (numberOfLinebreaks * 26)); -// ui.drawProgressBar(pbX, pbY, pbWidth, 15, percentage, TFT_WHITE, TFT_TP_BLUE); -// } -void drawProgress(const char *text, int8_t percentage) { - ofr.setFontSize(24); - int pbWidth = tft.width() - 100; - int pbX = (tft.width() - pbWidth)/2; - int pbY = 260; - int progressTextY = 210; - - tft.fillRect(0, progressTextY, tft.width(), 40, TFT_BLACK); - ofr.cdrawString(text, centerWidth, progressTextY); - ui.drawProgressBar(pbX, pbY, pbWidth, 15, percentage, TFT_WHITE, TFT_TP_BLUE); +/* +** =================================================================== +** syncTime() - Call back routine to sync the time and keep it +** accurate. +** =================================================================== +*/ +void syncTime() { + if (initTime()) + { + lastTimeSyncMillis = millis(); + setTimezone(Vault::getInstance().getTimezone().c_str()); + spLogI(LOGTAG_GENERAL, "Current local time: %s", getCurrentTimestamp(SYSTEM_TIMESTAMP_FORMAT).c_str()); + } } -void drawSeparator(uint16_t y) { - tft.drawFastHLine(10, y, tft.width() - 2 * 15, 0x4228); +/* +** =================================================================== +** setupLogging() +** Configures the logging levels for various tags using SCLogger. +** +** =================================================================== +*/ +void setupLogging() +{ + + Serial.printf("Entering setupLogging()\n"); + // Create the logger instance + SCLogger& logger = SCLogger::getInstance(); + + // Set default logging level globally (optional) + logger.setLogLevel("*", ESP_LOG_WARN); // Suppresses all Informational messages + + // Configure specific logging levels for each tag + logger.setLogLevel(LOGTAG_INPUT, ESP_LOG_VERBOSE); + // logger.setLogLevel(LOGTAG_SONG_DATA, ESP_LOG_ERROR); + logger.setLogLevel(LOGTAG_PLAYER, ESP_LOG_INFO); + // logger.setLogLevel(LOGTAG_GUI, ESP_LOG_INFO); + logger.setLogLevel(LOGTAG_GENERAL, ESP_LOG_INFO); + // logger.setLogLevel(LOGTAG_MULTITASK, ESP_LOG_VERBOSE); + logger.setLogLevel(LOGTAG_METRICS, ESP_LOG_INFO); + logger.setLogLevel(LOGTAG_HEAP, ESP_LOG_INFO); + //logger.setLogLevel(LOGTAG_TRACE, ESP_LOG_INFO); + logger.setLogLevel(LOGTAG_FILEIO, ESP_LOG_INFO); + // logger.setLogLevel(LOGTAG_CACHE, ESP_LOG_INFO); + logger.setLogLevel(LOGTAG_VAULT, ESP_LOG_INFO); + + // Supress logs for ESP32 components + logger.setLogLevel("ssl_client", ESP_LOG_NONE); + + // Print log levels to serial for verification + Serial.printf("Log level for General: %d\n", logger.getLogLevel(LOGTAG_GENERAL)); + Serial.printf("Log level for GUI: %d\n", logger.getLogLevel(LOGTAG_GUI)); + Serial.printf("Log level for Input: %d\n", logger.getLogLevel(LOGTAG_INPUT)); + Serial.printf("Log level for Multitask: %d\n", logger.getLogLevel(LOGTAG_MULTITASK)); + Serial.printf("Log level for Player: %d\n", logger.getLogLevel(LOGTAG_PLAYER)); + Serial.printf("Log level for SongData: %d\n", logger.getLogLevel(LOGTAG_SONG_DATA)); + Serial.printf("Log level for Metrics: %d\n", logger.getLogLevel(LOGTAG_METRICS)); + Serial.printf("Log level for Heap: %d\n", logger.getLogLevel(LOGTAG_HEAP)); + Serial.printf("Log level for Trace: %d\n", logger.getLogLevel(LOGTAG_TRACE)); + Serial.printf("Log level for File IO: %d\n", logger.getLogLevel(LOGTAG_FILEIO)); + Serial.printf("Log level for Cache: %d\n", logger.getLogLevel(LOGTAG_CACHE)); + Serial.printf("Log level for Vault: %d\n", logger.getLogLevel(LOGTAG_VAULT)); + + // Example of logging initialization completion + spLogI(LOGTAG_GENERAL, "Logging levels initialized."); + + // + // ESP_LOG_NONE - 0: No log output. Suppresses all log messages for the tag. + // ESP_LOG_ERROR - 1: Errors only. Outputs critical errors that might require immediate attention. + // ESP_LOG_WARN - 2: Warnings and Errors. Includes non-critical issues and potential problems. + // ESP_LOG_INFO - 3: Informational logs. General information about application flow. + // ESP_LOG_DEBUG - 4: Debugging logs. Detailed information useful for debugging during development. + // ESP_LOG_VERBOSE - 5: All logs. Includes extremely detailed and low-level messages. + // + // Note: + // Default level for all tags can be set using: + // esp_log_level_set("*", ); + // Individual tags override the default level. } -void initJpegDecoder() { - // The JPEG image can be scaled by a factor of 1, 2, 4, or 8 (default: 0) - TJpgDec.setJpgScale(1); - // The decoder must be given the exact name of the rendering function - TJpgDec.setCallback(pushImageToTft); +/* +** =================================================================== +** Register Monitor Descriptions +** This function registers human-readable descriptions for all +** monitor IDs defined in the application. Call this function +** during application initialization. +** =================================================================== +*/ +void registerMonitorDescriptions() +{ + Monitor::registerDescription(MONITOR_ID_CALCULATE_AVG_BACKGROUND_COLOR, "Calc Color Avg"); + Monitor::registerDescription(MONITOR_ID_SPOTIFY_GET_CURRENTLY_PLAYING, "Curr Playing REST"); + Monitor::registerDescription(MONITOR_ID_SPOTIFY_IMAGE_HTTP_GET, "Art http get"); + Monitor::registerDescription(MONITOR_ID_SPOTIFY_IMAGE_FILE_SAVE, "Art file save"); + Monitor::registerDescription(MONITOR_ID_SPOTIFY_IMAGE_FILE_LOAD, "Art file load"); + Monitor::registerDescription(MONITOR_ID_SPOTIFY_IMAGE_CACHE_LOAD, "Art index load"); + Monitor::registerDescription(MONITOR_ID_SPOTIFY_IMAGE_CACHE_SAVE, "Art index save"); + Monitor::registerDescription(MONITOR_ID_FETCH_ALBUM_ART, "Total art fetch"); + Monitor::registerDescription(MONITOR_ID_SCUI_QUEUE_DELAY, "SCUI Queue Delay"); } -void initOpenFontRender() { +/* +** =================================================================== +** initOpenFontRender() +** +** initialize open font render +** =================================================================== +*/ +void initOpenFontRender() +{ ofr.loadFont(opensans, sizeof(opensans)); ofr.setDrawer(tft); ofr.setFontColor(TFT_WHITE); ofr.setBackgroundColor(TFT_BLACK); + + clockFont.loadFont(cousineBold, sizeof(cousineBold)); + clockFont.setDrawer(tft); + clockFont.setFontColor(TFT_WHITE); + clockFont.setBackgroundColor(TFT_BLACK); } -void initScheduler() { +/* +** =================================================================== +** initScheduler() +** +** initialize the scheduler to update the clock +** =================================================================== +*/ +void initScheduler() +{ // Set the options for the task so that it "catches up" if there is a delay clockTask.setSchedulingOption(TASK_SCHEDULE); @@ -207,25 +412,203 @@ void initScheduler() { clockTask.enable(); } -// Function will be called as a callback during decoding of a JPEG file to -// render each block to the TFT. -bool pushImageToTft(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t *bitmap) { - // Stop further decoding as image is running off bottom of screen - if (y >= tft.height()) { - return 0; - } +/* +** =================================================================== +** loop() +** The main application loop. This function runs continuously +** but is designed to yield execution to FreeRTOS tasks. +** +** - Delays execution to prevent excessive CPU usage. +** - Executes scheduled tasks using the TaskScheduler. +** +** Since FreeRTOS tasks handle most functionality, this loop +** primarily serves as a placeholder. +** =================================================================== +*/ + +void loop() +{ + vTaskDelay(pdMS_TO_TICKS(1000)); // Keep loop from consuming resources + schedule.execute(); +} + +/* +** =================================================================== +** UI Handler Task +** This task is responsible for managing UI updates and input +** processing in a continuous loop. It performs the following: +** +** - Retrieves the currently active UI view from UIViewManager. +** - Processes system performance metrics. +** - Handles touch input events. +** - Dispatches messages from the SCUI queue. +** - Delays execution to maintain a 60 FPS refresh rate and +** allow other tasks to execute. +** +** Since vTaskDelay() is used, the Task Watchdog does not require +** manual resets. +** +** This task runs indefinitely. +** +** Parameters: +** pvParameters - Unused parameter required by FreeRTOS. +** =================================================================== +*/ +UIView *pActiveView = nullptr; +void uiHandlerTask(void *pvParameters) +{ + spLogI(LOGTAG_MULTITASK, "uiHandlerTask task executing. about to enter loop."); + + while (true) + { + // Feed the Task Watchdog to avoid timeout (not needed with vTaskDelay below) + // esp_task_wdt_reset(); + + processPerformanceMetrics(); + + pActiveView = UIViewManager::getInstance().getActiveView(); + + handleTouchInput(); + + // Reset pActiveView in case it changed after handling input + // (if you don't do this, messages will go to the wrong + // view.) + pActiveView = UIViewManager::getInstance().getActiveView(); + + dispatchSCUIQueueMessages(); + + // Allow other tasks to execute + vTaskDelay(pdMS_TO_TICKS(16)); // 60 fps refresh rate + } +} - // Automatically clips the image block rendering at the TFT boundaries. - tft.pushImage(x, y, w, h, bitmap); +/* +** =================================================================== +** processPerformanceMetrics() +** =================================================================== +*/ +void processPerformanceMetrics() +{ + + if (millis() > requestDueTime) + { + bool heapWarning = !(Monitor::watchHeap(HEAP_LOW_THRESHOLD)); + if (heapWarning) + { + // ui.setBackground(TFTColor::SC_LowHeap, true); + // char s[50]; + // snprintf(s, sizeof(s), "Low Heap: %u", Monitor::getFreeHeap()); + // ui.drawTextToLCD(s, (ui.getCenterHeight() - 10)); + // vTaskDelay(pdMS_TO_TICKS(3000)); // 3 second delay to keep msg up + } + Monitor::dumpStats(LOGTAG_METRICS); + Monitor::logUptime(LOGTAG_METRICS); + requestDueTime = millis() + delayBetweenRequests; + + } - // Return 1 to decode next block - return 1; } -void syncTime() { - if (initTime()) { - lastTimeSyncMillis = millis(); - setTimezone(TIMEZONE); - log_i("Current local time: %s", getCurrentTimestamp(SYSTEM_TIMESTAMP_FORMAT).c_str()); - } +/* +** =================================================================== +** handleTouchInput() +** =================================================================== +*/ +void handleTouchInput() +{ + static bool isTouchInProgress = false; + if (ts.touched()) + { + // track touch has started + isTouchInProgress = true; + + // x and y are the portrait coordinates. + // not sure why... + TS_Point p = ts.getPoint(); + + // flip the cooridnates to be landscape + uint16_t touchX = p.y; + uint16_t touchY = tft.height() - p.x; + + p.x = touchX; + p.y = touchY; + + bIsInitialLift = false; + if (!bIsInitialPress) + { + + spLogI(LOGTAG_INPUT, "Initial Press Detected x=%d, y=%d", touchX, touchY); + bIsInitialPress = true; + + UBaseType_t messagesInQueue = uxQueueMessagesWaiting(scuiQueue); + + pActiveView->onTouchDown(p); + + // if there was a message in the queue when the press took place, + // there is a good chance that the UI is being updated and the onTouchDown() + // event which happens immediately will act in the middle of something. Leaving + // this for now since responding immediately can be a good thing but let's + // at least clean up afterwards if something paints on top of the resulting + // action. TODO: Consider putting user input on the queue instead of letting it + // interrupt whatever is going on; however, if this works reasonably well + // it might be worth leaving as is since it provides a bit of responsiveness. + if (messagesInQueue > 1) + { + spLogI(LOGTAG_GENERAL, "Pending messages in queue when processing touch. count: %u", messagesInQueue); + SCUIMessage msg; + msg.type = SCUIMessageType::UM_MARK_DIRTY; + msg.str = ""; + msg.num = true; + + Monitor::start(MONITOR_ID_SCUI_QUEUE_DELAY, LOGTAG_MULTITASK, "handleTouchInput()"); + if (xQueueSend(scuiQueue, &msg, pdMS_TO_TICKS(10)) != pdPASS) + { + spLogE(LOGTAG_PLAYER, "Failed to send SCUI message to queue"); + } + } + + } + } + else + { + if (isTouchInProgress) + { + bIsInitialPress = false; + if (!bIsInitialLift) + { + bIsInitialLift = true; + isTouchInProgress = false; + pActiveView->onTouchUp(); + } + } + } } + +/* +** =================================================================== +** dispatchSCUIQueueMessages() +** =================================================================== +*/ +void dispatchSCUIQueueMessages() +{ + // make static to avoid reallocations + static SCUIMessage message; + + // watch the size + Monitor::watchQueue(scuiQueue, 1); + + if (xQueueReceive(scuiQueue, &message, 0) != pdPASS) + { + // Send in IDLE if nothing in the queue + message.type = SCUIMessageType::UM_IDLE; + message.str = ""; + message.num = 0; + } + else + { + // Track how long these messages are taking + Monitor::stop(MONITOR_ID_SCUI_QUEUE_DELAY); + } + pActiveView->handleMessage(&message); +} + diff --git a/src/persistence.h b/src/persistence.h deleted file mode 100644 index 7b16308..0000000 --- a/src/persistence.h +++ /dev/null @@ -1,57 +0,0 @@ -// SPDX-FileCopyrightText: 2023 ThingPulse Ltd., https://thingpulse.com -// SPDX-License-Identifier: MIT - -#pragma once - -#include - -void listFiles(); - -void initFileSystem() { - if (LittleFS.begin()) { - log_i("Flash FS available!"); - } else { - log_e("Flash FS initialisation failed!"); - } - - listFiles(); -} - -void listFiles() { - log_i("Flash FS files found:"); - - File root = LittleFS.open("/"); - while (true) { - File entry = root.openNextFile(); - if (!entry) { - break; - } - log_i("- %s, %d bytes", entry.name(), entry.size()); - entry.close(); - } -} - -String readFsString(const char *path) { - String token = ""; - log_i("Loading string from '%s'.", path); - File f = LittleFS.open(path, "r"); - if (f) { - token = f.readString(); - log_d("Persisted string: %s", token.c_str()); - f.close(); - } else { - log_e("Failed to load string from file system, returning empty."); - } - return token; -} - -void saveFsString(const char *path, String string) { - log_i("Saving string to '%s'.", path); - File f = LittleFS.open(path, "w+"); - if (f) { - f.print(string); - f.close(); - } else { - log_e("Failed to open file."); - } -} diff --git a/src/scui.h b/src/scui.h new file mode 100644 index 0000000..08960a0 --- /dev/null +++ b/src/scui.h @@ -0,0 +1,45 @@ +/*------------------------------------------------------------------------------------------------- +** +** scui.h +** +** Defines Spotify Companion User Interface (SCUI) messaging. This includes +** message types and structures used for asynchronous, thread-safe +** communication between tasks via FreeRTOS queues. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2025-02-08 - Electric Diversions - Initial creation. +** ------------------------------------------------------------------------------------------------ +*/ + +#ifndef SCUI_H +#define SCUI_H + +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/queue.h" + +// Enum for supported message types +enum class SCUIMessageType +{ + UM_STATUS_BOX, + UM_DOWNLOAD_BOX, + UM_MARK_DIRTY, + UM_IDLE, + UM_PLAYER_REFRESH +}; + +// Structure for a UI message +struct SCUIMessage +{ + SCUIMessageType type; + String str; // Generic data payload (e.g., track name, status update) + int num; // Generic payload +}; + +#endif // SCUI_H + diff --git a/src/settings.h b/src/settings.h index 1d5b590..8ae9ba4 100644 --- a/src/settings.h +++ b/src/settings.h @@ -1,46 +1,82 @@ -// SPDX-FileCopyrightText: 2023 ThingPulse Ltd., https://thingpulse.com -// SPDX-License-Identifier: MIT +/*------------------------------------------------------------------------------------------------- +** +** settings.h +** +** Main constants used by the application, including versioning, +** compile-time info, time zone, display rotation, and system behavior +** for the ESP32 Spotify Remote. +** +** SPDX-FileCopyrightText: 2025 ThingPulse Ltd., https://thingpulse.com +** SPDX-License-Identifier: MIT +** +** ------------------------------------------------------------------------------------------------ +** Change Log: +** 2024-12-26 - Electric Diversions - Copied from ThingPulse Spotify Remote. +** 2024-12-27 - Electric Diversions - Removed blocking declarations and dead code. +** ------------------------------------------------------------------------------------------------ +*/ #pragma once +#include "compile_time.h" // **************************************************************************** // User settings +// +// Note: You can also use an optional 'user.ini' file. This will externalize +// settings outside of the code, is already added to .gitignore, +// and provides advanced privacy and encryption options. See Vault +// class for details. // **************************************************************************** -// WiFi -const char *SSID = ""; -const char *WIFI_PWD = ""; -// timezone Europe/Zurich as per https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv +// WiFi Credentials +static const char *SSID = "SSID Goes Here"; +static const char *WIFI_PWD = "WiFi Password Goes Here"; + +// Spotify Credentials +static const char *SPOTIFY_CLIENT_ID = "SPOTIFY_CLIENT_ID goes here"; +static const char *SPOTIFY_CLIENT_SECRET = "SPOTIFY_CLIENT_SECRET goes here"; + +// Timezone +// Europe/Zurich as per +// https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv #define TIMEZONE "CET-1CEST,M3.5.0,M10.5.0/3" -// Spotify settings -const char *SPOTIFY_CLIENT_ID = ""; -const char *SPOTIFY_CLIENT_SECRET = ""; -// Use http://.local/callback/ as the redirect URI for the app on Spotify. -// Hence, the default URI is http://tp-spotify.local/callback/. -// If you change the value here, you need to modify the redirect URI on Spotify as well. -#define SPOTIFY_ESPOTIFIER_NODE_NAME "tp-spotify" +// format specifiers: https://cplusplus.com/reference/ctime/strftime/ +// values below are tested. updating the formatting may affect UI layout. + // Set to true to use US date/time formatting (12 hour times and US dates) + static const bool Use_US_Date_Time_Format = false; -// **************************************************************************** -// System settings - do not modify unless you understand what you are doing! -// **************************************************************************** -typedef struct RectangleDef { - uint16_t x; - uint16_t y; - uint16_t width; - uint16_t height; -} RectangleDef; + // Formatting consistent with norms in the United States + #define UI_DATE_FORMAT_US "%A %B %d %Y" + #define UI_TIME_FORMAT_US "%l:%M %p" + + // Formatting consistent with many countries outside the United States + #define UI_DATE_FORMAT "%A %d %B %Y" + #define UI_TIME_FORMAT "%H:%M" + +/* +** =================================================================== +** Version and Name Information +** =================================================================== +*/ +constexpr const char* APP_NAME = "ESP32 Spotify Remote"; +constexpr const char* VERSION = "2.0.0"; +constexpr const char* COMPILE_TIME = SC_COMPILE_TIME; -RectangleDef timeSpritePos = {0, 0, 320, 88}; + +/* +** =================================================================== +** System Settings - Per ThingPulse, do not modify unless you +** understand what you are doing! +** =================================================================== +*/ // 2: portrait, on/off switch right side -> 0/0 top left // 3: landscape, on/off switch at the top -> 0/0 top left -#define TFT_ROTATION 2 +#define TFT_ROTATION 3 // all other TFT_xyz flags are defined in platformio.ini as PIO build flags -// 0: portrait, on/off switch right side -> 0/0 top left -// 1: landscape, on/off switch at the top -> 0/0 top left #define TOUCH_ROTATION 0 #define TOUCH_SENSITIVITY 40 #define TOUCH_SDA 23 @@ -48,33 +84,6 @@ RectangleDef timeSpritePos = {0, 0, 320, 88}; // Initial LCD Backlight brightness #define TFT_LED_BRIGHTNESS 200 -// the medium blue in the TP logo is 0x0067B0 which converts to 0x0336 in 16bit RGB565 -#define TFT_TP_BLUE 0x0336 - -// format specifiers: https://cplusplus.com/reference/ctime/strftime/ -#ifdef DATE_TIME_FORMAT_US - int timePosX = 29; - #define UI_DATE_FORMAT "%m/%d/%Y" - #define UI_TIME_FORMAT "%I:%M:%S %P" - #define UI_TIME_FORMAT_NO_SECONDS "%I:%M %P" - #define UI_TIMESTAMP_FORMAT (UI_DATE_FORMAT + " " + UI_TIME_FORMAT) -#else - int timePosX = 68; - #define UI_DATE_FORMAT "%d.%m.%Y" - #define UI_TIME_FORMAT "%H:%M:%S" - #define UI_TIME_FORMAT_NO_SECONDS "%H:%M" - #define UI_TIMESTAMP_FORMAT (UI_DATE_FORMAT + " " + UI_TIME_FORMAT) -#endif - #define SYSTEM_TIMESTAMP_FORMAT "%Y-%m-%d %H:%M:%S" #define CLOCK_TASK_INTERVAL_MILLIS 3600000 -// the spotify-api-arduino library sets this to 1 (i.e. enabled) by default -// #define SPOTIFY_DEBUG 0 - -#define SPOTIFY_REFRESH_TOKEN_FILE_NAME "/refresh-token.txt" -// the '/callback/' path is essential as spotify.h#fetchSpotifyAuthCode() registers a handler for it -#define SPOTIFY_REDIRECT_URI "http%3A%2F%2F" SPOTIFY_ESPOTIFIER_NODE_NAME ".local%2Fcallback%2F" - -#define APP_NAME "ESP32 Spotify Remote" -#define VERSION "1.0.0" diff --git a/src/spotify.h b/src/spotify.h deleted file mode 100644 index 648ee04..0000000 --- a/src/spotify.h +++ /dev/null @@ -1,113 +0,0 @@ -// SPDX-FileCopyrightText: 2023 ThingPulse Ltd., https://thingpulse.com -// SPDX-License-Identifier: MIT - -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "settings.h" - -String authCode = ""; -String scope = "user-read-playback-state%20user-modify-playback-state"; -WebServer server(80); -WiFiClientSecure client; -SpotifyArduino spotify(client, SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET); - -const char *webpageTemplate = - R"( - - - - - - - - -
- Click to load Spotify authentication code -
- - -)"; - -void initSpotifiy() { - client.setCACert(spotify_server_cert); -} - -void handleCallback() { - String code = ""; - for (uint8_t i = 0; i < server.args(); i++) { - if (server.argName(i) == "code") { - authCode = server.arg(i); - } - } - - if (authCode == "") { - server.send(404, "text/plain", "Failed to fetch Spotify authentication code, check serial monitor. Maybe go back in browser history and try again."); - } else { - server.send(200, "text/plain", "Succesfully fetched Spotify authentication code. Follow instructions on device."); - } -} - -void handleFavicon() { - server.send(200, "image/vnd.microsoft.icon", "00000100"); -} - -void handleNotFound() { - String message = "File Not Found\n\n"; - message += "URI: "; - message += server.uri(); - message += "\nMethod: "; - message += (server.method() == HTTP_GET) ? "GET" : "POST"; - message += "\nArguments: "; - message += server.args(); - message += "\n"; - - for (uint8_t i = 0; i < server.args(); i++) { - message += " " + server.argName(i) + ": " + server.arg(i) + "\n"; - } - - log_e("%s", message.c_str()); - server.send(404, "text/plain", message); -} - -void handleRoot() { - char webpage[800]; - sprintf(webpage, webpageTemplate, SPOTIFY_CLIENT_ID, SPOTIFY_REDIRECT_URI, scope.c_str()); - server.send(200, "text/html", webpage); -} - -String fetchSpotifyAuthCode() { - if (MDNS.begin(SPOTIFY_ESPOTIFIER_NODE_NAME)) { - log_i("MDNS responder started for node name '%s'.", SPOTIFY_ESPOTIFIER_NODE_NAME); - log_i("Open browser at http://%s.local", SPOTIFY_ESPOTIFIER_NODE_NAME); - } - - server.on("/", handleRoot); - server.on("/callback/", handleCallback); - server.on("/favicon.ico", handleFavicon); - server.onNotFound(handleNotFound); - server.begin(); - log_i("HTTP server started"); - - while (authCode == "") { - server.handleClient(); - yield(); - } - - log_i("Successfully loaded Spotify authentication code: '%s'.", authCode.c_str()); - - log_i("Stopping HTTP server"); - server.stop(); - log_i("Stopping MDNS responder"); - MDNS.end(); - - return authCode; -} diff --git a/src/util.h b/src/util.h deleted file mode 100644 index 976d9cf..0000000 --- a/src/util.h +++ /dev/null @@ -1,85 +0,0 @@ -// SPDX-FileCopyrightText: 2023 ThingPulse Ltd., https://thingpulse.com -// SPDX-License-Identifier: MIT - -#pragma once - -#include "time.h" -#include "settings.h" - -char timestampBuffer[26]; - - -String getCurrentTimestamp(const char* format) { - struct tm timeinfo; - if (!getLocalTime(&timeinfo)) { - log_e("Failed to obtain time."); - return ""; - } - strftime(timestampBuffer, sizeof(timestampBuffer), format, &timeinfo); - return String(timestampBuffer); -} - -boolean initTime() { - struct tm timeinfo; - - log_i("Synchronizing time."); - // Connect to NTP server with 0 TZ offset, call setTimezone() later - configTime(0, 0, "pool.ntp.org"); - // getLocalTime() uses a default timeout of 5s -> the loop takes at most 3*5s to - for (int i = 0; i < 3; i++) { - if (getLocalTime(&timeinfo)) { - log_i("UTC time: %s.", getCurrentTimestamp(SYSTEM_TIMESTAMP_FORMAT).c_str()); - return true; - } - } - - log_e("Failed to obtain time."); - return false; -} - -void logBanner() { - log_i("**********************************************"); - log_i("* ThingPulse Spotify Controller v%s *", VERSION); - log_i("**********************************************"); -} - -void logMemoryStats() { - log_i("Total heap: %d", ESP.getHeapSize()); - log_i("Free heap: %d", ESP.getFreeHeap()); - log_i("Total PSRAM: %d", ESP.getPsramSize()); - log_i("Free PSRAM: %d", ESP.getFreePsram()); -} - -void setTimezone(const char* timezone) { - log_i("Setting timezone to '%s'.", timezone); - // Clock settings are adjusted to show the new local time - setenv("TZ", timezone, 1); - tzset(); -} - -// Algorithm: http://howardhinnant.github.io/date_algorithms.html -int days_from_epoch(int y, int m, int d) { - y -= m <= 2; - int era = y / 400; - int yoe = y - era * 400; // [0, 399] - int doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; // [0, 365] - int doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; // [0, 146096] - return era * 146097 + doe - 719468; -} -// https://stackoverflow.com/a/58037981/131929 -// aka timegm() but that's already defined in the Weather Station lib but not accessible -time_t mkgmtime(struct tm const *t) { - int year = t->tm_year + 1900; - int month = t->tm_mon; // 0-11 - if (month > 11) { - year += month / 12; - month %= 12; - } else if (month < 0) { - int years_diff = (11 - month) / 12; - year -= years_diff; - month += 12 * years_diff; - } - int days_since_epoch = days_from_epoch(year, month + 1, t->tm_mday); - - return 60 * (60 * (24L * days_since_epoch + t->tm_hour) + t->tm_min) + t->tm_sec; -}