diff --git a/.github/ISSUE_TEMPLATE/Bug Report.yml b/.github/ISSUE_TEMPLATE/Bug Report.yml index bc77e8c1b..cdf482344 100644 --- a/.github/ISSUE_TEMPLATE/Bug Report.yml +++ b/.github/ISSUE_TEMPLATE/Bug Report.yml @@ -75,11 +75,11 @@ body: - type: checkboxes id: mui attributes: - label: Is this bug report about any UI component firmware like InkHUD or Meshtatic UI (MUI)? + label: Is this bug report about any UI (https://meshtastic.org/docs/configuration/device-uis/) component firmware? options: - - label: Meshtastic UI aka MUI colorTFT - - label: InkHUD ePaper - - label: OLED slide UI on any display + - label: Meshtastic UI aka MUI + - label: InkHUD + - label: BaseUI - type: input id: version diff --git a/.github/workflows/test_native.yml b/.github/workflows/test_native.yml index 2fabf0591..1e22d74d1 100644 --- a/.github/workflows/test_native.yml +++ b/.github/workflows/test_native.yml @@ -86,7 +86,13 @@ jobs: run: sed -i 's/-DBUILD_EPOCH=$UNIX_TIME/#-DBUILD_EPOCH=$UNIX_TIME/' platformio.ini - name: PlatformIO Tests - run: platformio test -e coverage -v --junit-output-path testreport.xml + run: | + set -o pipefail + # Filter out SKIPPED summary rows for hardware variants that can't run on the + # native host. They flood the log and make it harder to spot real failures. + # The JUnit XML is written directly to testreport.xml before the pipe, so + # the test artifact is unaffected. + platformio test -e coverage -v --junit-output-path testreport.xml 2>&1 | grep -v "[[:space:]]SKIPPED$" - name: Save test results if: always() # run this step even if previous step failed diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index d0cbaa8bc..f90f4f4ac 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -8,18 +8,18 @@ plugins: uri: https://github.com/trunk-io/plugins lint: enabled: - - checkov@3.2.517 - - renovate@43.110.9 - - prettier@3.8.1 - - trufflehog@3.94.3 + - checkov@3.2.524 + - renovate@43.141.0 + - prettier@3.8.3 + - trufflehog@3.95.2 - yamllint@1.38.0 - bandit@1.9.4 - - trivy@0.69.3 + - trivy@0.70.0 - taplo@0.10.0 - - ruff@0.15.9 + - ruff@0.15.11 - isort@8.0.1 - markdownlint@0.48.0 - - oxipng@10.1.0 + - oxipng@10.1.1 - svgo@4.0.1 - actionlint@1.7.12 - flake8@7.3.0 diff --git a/bin/show-unmerged-prs.sh b/bin/show-unmerged-prs.sh new file mode 100755 index 000000000..2a76f63d6 --- /dev/null +++ b/bin/show-unmerged-prs.sh @@ -0,0 +1,118 @@ +#!/bin/bash + +# Script to show commits in develop that are not in master +# with their associated PR info and commit hashes +# +# Usage: +# ./show-unmerged-prs.sh # Show all unmerged commits +# ./show-unmerged-prs.sh --bugfix # Show only bugfix-labeled PRs + +set -e + +REPO="firmware" +OWNER="meshtastic" +BASE_BRANCH="master" +HEAD_BRANCH="develop" +LIMIT=100 +FILTER_LABEL="" + +# Parse arguments +for arg in "$@"; do + case $arg in + --bugfix) + FILTER_LABEL="bugfix" + shift + ;; + --feature) + FILTER_LABEL="feature" + shift + ;; + --help) + echo "Usage: $0 [OPTIONS]" + echo "Options:" + echo " --bugfix Show only PRs labeled with 'bugfix'" + echo " --feature Show only PRs labeled with 'feature'" + echo " --help Show this help message" + exit 0 + ;; + esac +done + +if [ -n "$FILTER_LABEL" ]; then + echo "Fetching commits in $HEAD_BRANCH that are not in $BASE_BRANCH (filtered by label: $FILTER_LABEL)..." +else + echo "Fetching commits in $HEAD_BRANCH that are not in $BASE_BRANCH..." +fi +echo "" + +# Check if gh CLI is available +if ! command -v gh &> /dev/null; then + echo "ERROR: GitHub CLI (gh) not found. Please install it first." + echo "Visit: https://cli.github.com/" + exit 1 +fi + +# Get commits in develop that are not in master +# For each commit, try to find associated PR +git fetch origin develop master 2>/dev/null || true + +# Use git to get the list of commits +commits=$(git log --pretty=format:"%H|%s" origin/master..origin/develop | head -n $LIMIT) + +count=0 +displayed=0 +echo "Commits in $HEAD_BRANCH not in $BASE_BRANCH:" +echo "==============================================" +echo "" + +while IFS='|' read -r hash subject; do + ((count++)) + + # Try to find the PR for this commit + # Extract PR number, title, description, and labels + pr_response=$(gh api -X GET "/repos/$OWNER/$REPO/commits/$hash/pulls" \ + -H "Accept: application/vnd.github.v3+json" 2>/dev/null | \ + jq -r '.[0] | "\(.number)|\(.title)|\(.body // "No description")|\(.labels | map(.name) | join(","))"' 2>/dev/null || echo "||||") + + if [ -z "$pr_response" ] || [ "$pr_response" = "||||" ]; then + # If no PR found, skip if filter is active, otherwise show the commit + if [ -z "$FILTER_LABEL" ]; then + ((displayed++)) + echo "[$displayed] Commit: $hash" + echo " Subject: $subject" + echo " PR: Not found in GitHub" + echo "" + fi + else + IFS='|' read -r pr_num pr_title pr_desc pr_labels <<< "$pr_response" + + # Check if filter matches + if [ -n "$FILTER_LABEL" ]; then + # Only show if the label is in the labels list + if ! echo "$pr_labels" | grep -q "$FILTER_LABEL"; then + continue + fi + fi + + ((displayed++)) + echo "[$displayed] PR #$pr_num - $pr_title" + echo " Commit: $hash" + if [ -n "$pr_desc" ] && [ "$pr_desc" != "No description" ]; then + # Truncate description to 200 chars + desc_short="${pr_desc:0:200}" + [ ${#pr_desc} -gt 200 ] && desc_short+="..." + echo " Description: $desc_short" + fi + if [ -n "$pr_labels" ] && [ "$pr_labels" != "" ]; then + echo " Labels: $pr_labels" + fi + echo "" + fi +done <<< "$commits" + +echo "" +if [ -n "$FILTER_LABEL" ]; then + echo "Done. Showing $displayed PRs with label '$FILTER_LABEL' from $displayed commits checked." +else + echo "Done. Showing $displayed commits from $HEAD_BRANCH not in $BASE_BRANCH." +fi diff --git a/platformio.ini b/platformio.ini index 48f534d5d..7f39cc5f0 100644 --- a/platformio.ini +++ b/platformio.ini @@ -29,6 +29,7 @@ build_flags = -Wno-missing-field-initializers -DUSE_THREAD_NAMES -DTINYGPS_OPTION_NO_CUSTOM_FIELDS -DPB_ENABLE_MALLOC=1 + -DPB_VALIDATE_UTF8=1 -DRADIOLIB_EXCLUDE_CC1101=1 -DRADIOLIB_EXCLUDE_NRF24=1 -DRADIOLIB_EXCLUDE_RF69=1 @@ -66,7 +67,7 @@ monitor_speed = 115200 monitor_filters = direct lib_deps = # renovate: datasource=git-refs depName=meshtastic-esp8266-oled-ssd1306 packageName=https://github.com/meshtastic/esp8266-oled-ssd1306 gitBranch=master - https://github.com/meshtastic/esp8266-oled-ssd1306/archive/21e484f409cde18d44012caef84c244eb5ca28f3.zip + https://github.com/meshtastic/esp8266-oled-ssd1306/archive/6bfd1f135e1ebe37afd6050bb4b9964cea3fcfda.zip # renovate: datasource=git-refs depName=meshtastic-OneButton packageName=https://github.com/meshtastic/OneButton gitBranch=master https://github.com/meshtastic/OneButton/archive/fa352d668c53f290cfa480a5f79ad422cd828c70.zip # renovate: datasource=git-refs depName=meshtastic-arduino-fsm packageName=https://github.com/meshtastic/arduino-fsm gitBranch=master diff --git a/protobufs b/protobufs index 4d5b500df..249a80855 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 4d5b500df5af68a4f57d3e19705cc3bb1136358c +Subproject commit 249a80855a2adb76fb0904dac8bf6285d45f330f diff --git a/src/Power.cpp b/src/Power.cpp index 2e3217a1c..b40b41623 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -94,16 +94,31 @@ static const adc_atten_t atten = ADC_ATTENUATION; #endif #endif // BATTERY_PIN && ARCH_ESP32 +#ifdef EXT_PWR_DETECT +#ifndef EXT_PWR_DETECT_MODE +#define EXT_PWR_DETECT_MODE INPUT +// If using internal pull resistors, we can infer EXT_PWR_DETECT_VALUE +#elif EXT_PWR_DETECT_MODE == INPUT_PULLUP +#define EXT_PWR_DETECT_VALUE LOW +#elif EXT_PWR_DETECT_MODE == INPUT_PULLDOWN +#define EXT_PWR_DETECT_VALUE HIGH +#endif +#ifndef EXT_PWR_DETECT_VALUE +#define EXT_PWR_DETECT_VALUE HIGH +#endif +#endif + #ifdef EXT_CHRG_DETECT #ifndef EXT_CHRG_DETECT_MODE -static const uint8_t ext_chrg_detect_mode = INPUT; -#else -static const uint8_t ext_chrg_detect_mode = EXT_CHRG_DETECT_MODE; +#define EXT_CHRG_DETECT_MODE INPUT +// If using internal pull resistors, we can infer EXT_CHRG_DETECT_VALUE +#elif EXT_CHRG_DETECT_MODE == INPUT_PULLUP +#define EXT_CHRG_DETECT_VALUE LOW +#elif EXT_CHRG_DETECT_MODE == INPUT_PULLDOWN +#define EXT_CHRG_DETECT_VALUE HIGH #endif #ifndef EXT_CHRG_DETECT_VALUE -static const uint8_t ext_chrg_detect_value = HIGH; -#else -static const uint8_t ext_chrg_detect_value = EXT_CHRG_DETECT_VALUE; +#define EXT_CHRG_DETECT_VALUE HIGH #endif #endif @@ -469,28 +484,14 @@ class AnalogBatteryLevel : public HasBatteryLevel virtual bool isBatteryConnect() override { return getBatteryPercent() != -1; } #endif - /// If we see a battery voltage higher than physics allows - assume charger is - /// pumping in power On some boards we don't have the power management chip - /// (like AXPxxxx) so we use EXT_PWR_DETECT GPIO pin to detect external power - /// source + // Detect if an external power source is connected if we don’t have a PMIC; + // Firstly prefer EXT_PWR_DETECT GPIO if available, + // secondly try an nRF52-specific routine on some variants, + // lastly provide a fallback to indicate external power when fully charged. virtual bool isVbusIn() override { #ifdef EXT_PWR_DETECT -#if defined(HELTEC_CAPSULE_SENSOR_V3) || defined(HELTEC_SENSOR_HUB) - // if external powered that pin will be pulled down - if (digitalRead(EXT_PWR_DETECT) == LOW) { - return true; - } - // if it's not LOW - check the battery -#else - // if external powered that pin will be pulled up - if (digitalRead(EXT_PWR_DETECT) == HIGH) { - return true; - } - // if it's not HIGH - check the battery -#endif - // If we have an EXT_PWR_DETECT pin and it indicates no external power, believe it. - return false; + return digitalRead(EXT_PWR_DETECT) == EXT_PWR_DETECT_VALUE; // technically speaking this should work for all(?) NRF52 boards // but needs testing across multiple devices. NRF52 USB would not even work if @@ -511,9 +512,9 @@ class AnalogBatteryLevel : public HasBatteryLevel } #endif #if defined(ELECROW_ThinkNode_M6) - return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value || isVbusIn(); + return digitalRead(EXT_CHRG_DETECT) == EXT_CHRG_DETECT_VALUE || isVbusIn(); #elif EXT_CHRG_DETECT - return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value; + return digitalRead(EXT_CHRG_DETECT) == EXT_CHRG_DETECT_VALUE; #elif defined(BATTERY_CHARGING_INV) return !digitalRead(BATTERY_CHARGING_INV); #else @@ -646,14 +647,10 @@ Power::Power() : OSThread("Power") bool Power::analogInit() { #ifdef EXT_PWR_DETECT -#if defined(HELTEC_CAPSULE_SENSOR_V3) || defined(HELTEC_SENSOR_HUB) - pinMode(EXT_PWR_DETECT, INPUT_PULLUP); -#else - pinMode(EXT_PWR_DETECT, INPUT); -#endif + pinMode(EXT_PWR_DETECT, EXT_PWR_DETECT_MODE); #endif #ifdef EXT_CHRG_DETECT - pinMode(EXT_CHRG_DETECT, ext_chrg_detect_mode); + pinMode(EXT_CHRG_DETECT, EXT_CHRG_DETECT_MODE); #endif #ifdef BATTERY_PIN @@ -746,37 +743,17 @@ bool Power::setup() found = true; #endif } -#ifdef EXT_PWR_DETECT - attachInterrupt( - EXT_PWR_DETECT, - []() { - power->setIntervalFromNow(0); - runASAP = true; - }, - CHANGE); -#endif -#ifdef BATTERY_CHARGING_INV - attachInterrupt( - BATTERY_CHARGING_INV, - []() { - power->setIntervalFromNow(0); - runASAP = true; - }, - CHANGE); -#endif -#ifdef EXT_CHRG_DETECT - attachInterrupt( - EXT_CHRG_DETECT, - []() { - power->setIntervalFromNow(0); - runASAP = true; - BaseType_t higherWake = 0; - }, - CHANGE); -#endif + attachPowerInterrupts(); enabled = found; low_voltage_counter = 0; +#ifdef ARCH_ESP32 + // Register callbacks for before and after lightsleep + // Used to detach and reattach interrupts + lsObserver.observe(¬ifyLightSleep); + lsEndObserver.observe(¬ifyLightSleepEnd); +#endif + return found; } @@ -1023,6 +1000,14 @@ int32_t Power::runOnce() powerFSM.trigger(EVENT_POWER_CONNECTED); } +#ifdef PMU_POWER_BUTTON_IS_CANCEL + // cancel action also turns the screen on and off. + if (PMU->isPekeyShortPressIrq()) { + LOG_INFO("Input: Corona Button Click"); + InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_CANCEL, .kbchar = 0, .touchX = 0, .touchY = 0}; + inputBroker->injectInputEvent(&event); + } +#endif /* Other things we could check if we cared... @@ -1039,13 +1024,6 @@ int32_t Power::runOnce() LOG_DEBUG("Battery removed"); } */ -#ifndef T_WATCH_S3 // FIXME - why is this triggering on the T-Watch S3? - if (PMU->isPekeyLongPressIrq()) { - LOG_DEBUG("PEK long button press"); - if (screen) - screen->setOn(false); - } -#endif PMU->clearIrqStatus(); } @@ -1055,6 +1033,97 @@ int32_t Power::runOnce() return (statusHandler && statusHandler->isInitialized()) ? (1000 * 20) : RUN_SAME; } +#ifdef ARCH_ESP32 + +// Detach our class' interrupts before lightsleep +// Allows sleep.cpp to configure its own interrupts, which wake the device on user-button press +int Power::beforeLightSleep(void *unused) +{ + LOG_WARN("Detaching power interrupts for sleep"); + detachPowerInterrupts(); + return 0; // Indicates success +} + +// Reconfigure our interrupts +// Our class' interrupts were disconnected during sleep, to allow the user button to wake the device from sleep +int Power::afterLightSleep(esp_sleep_wakeup_cause_t cause) +{ + attachPowerInterrupts(); + return 0; // Indicates success +} + +#endif + +/* + * Attach (or re-attach) hardware interrupts for power management + * Public method. Used outside class when waking from MCU sleep + */ +void Power::attachPowerInterrupts() +{ +#ifdef EXT_PWR_DETECT + attachInterrupt( + EXT_PWR_DETECT, + []() { + power->setIntervalFromNow(0); + runASAP = true; + }, + CHANGE); +#endif +#ifdef BATTERY_CHARGING_INV + attachInterrupt( + BATTERY_CHARGING_INV, + []() { + power->setIntervalFromNow(0); + runASAP = true; + }, + CHANGE); +#endif +#ifdef EXT_CHRG_DETECT + attachInterrupt( + EXT_CHRG_DETECT, + []() { + power->setIntervalFromNow(0); + runASAP = true; + BaseType_t higherWake = 0; + }, + CHANGE); +#endif +#ifdef PMU_IRQ + if (PMU) { + attachInterrupt( + PMU_IRQ, + []() { + pmu_irq = true; + power->setIntervalFromNow(0); + runASAP = true; + }, + FALLING); + } +#endif +} + +/* + * Detach the "normal" button interrupts. + * Public method. Used before attaching a "wake-on-button" interrupt for MCU sleep + */ +void Power::detachPowerInterrupts() +{ +#ifdef EXT_PWR_DETECT + detachInterrupt(EXT_PWR_DETECT); +#endif +#ifdef BATTERY_CHARGING_INV + detachInterrupt(BATTERY_CHARGING_INV); +#endif +#ifdef EXT_CHRG_DETECT + detachInterrupt(EXT_CHRG_DETECT); +#endif +#ifdef PMU_IRQ + if (PMU) { + detachInterrupt(PMU_IRQ); + } +#endif +} + /** * Init the power manager chip * @@ -1368,21 +1437,16 @@ bool Power::axpChipInit() uint64_t pmuIrqMask = 0; if (PMU->getChipModel() == XPOWERS_AXP192) { - pmuIrqMask = XPOWERS_AXP192_VBUS_INSERT_IRQ | XPOWERS_AXP192_BAT_INSERT_IRQ | XPOWERS_AXP192_PKEY_SHORT_IRQ; + pmuIrqMask = XPOWERS_AXP192_VBUS_INSERT_IRQ | XPOWERS_AXP192_VBUS_REMOVE_IRQ | XPOWERS_AXP192_PKEY_SHORT_IRQ; } else if (PMU->getChipModel() == XPOWERS_AXP2101) { - pmuIrqMask = XPOWERS_AXP2101_VBUS_INSERT_IRQ | XPOWERS_AXP2101_BAT_INSERT_IRQ | XPOWERS_AXP2101_PKEY_SHORT_IRQ; + pmuIrqMask = XPOWERS_AXP2101_VBUS_INSERT_IRQ | XPOWERS_AXP2101_VBUS_REMOVE_IRQ | XPOWERS_AXP2101_PKEY_SHORT_IRQ; } pinMode(PMU_IRQ, INPUT); - attachInterrupt( - PMU_IRQ, [] { pmu_irq = true; }, FALLING); - // we do not look for AXPXXX_CHARGING_FINISHED_IRQ & AXPXXX_CHARGING_IRQ - // because it occurs repeatedly while there is no battery also it could cause - // inadvertent waking from light sleep just because the battery filled we - // don't look for AXPXXX_BATT_REMOVED_IRQ because it occurs repeatedly while - // no battery installed we don't look at AXPXXX_VBUS_REMOVED_IRQ because we - // don't have anything hooked to vbus + // We wake on IRQ, so only enable the IRQs that we care about. + // we want USB plug and unplug to update the screen and LED status, + // and short press on the power button to trigger the "cancel" action in the UI (which also turns the screen on and off). PMU->enableIRQ(pmuIrqMask); PMU->clearIrqStatus(); @@ -1848,7 +1912,7 @@ class SerialBatteryLevel : public HasBatteryLevel { #if defined(EXT_CHRG_DETECT) - return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value; + return digitalRead(EXT_CHRG_DETECT) == EXT_CHRG_DETECT_VALUE; #endif return false; @@ -1857,7 +1921,7 @@ class SerialBatteryLevel : public HasBatteryLevel virtual bool isCharging() override { #ifdef EXT_CHRG_DETECT - return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value; + return digitalRead(EXT_CHRG_DETECT) == EXT_CHRG_DETECT_VALUE; #endif // by default, we check the battery voltage only @@ -1879,10 +1943,10 @@ SerialBatteryLevel serialBatteryLevel; bool Power::serialBatteryInit() { #ifdef EXT_PWR_DETECT - pinMode(EXT_PWR_DETECT, INPUT); + pinMode(EXT_PWR_DETECT, EXT_PWR_DETECT_MODE); #endif #ifdef EXT_CHRG_DETECT - pinMode(EXT_CHRG_DETECT, ext_chrg_detect_mode); + pinMode(EXT_CHRG_DETECT, EXT_CHRG_DETECT_MODE); #endif bool result = serialBatteryLevel.runOnce(); diff --git a/src/PowerFSM.cpp b/src/PowerFSM.cpp index b11f37cf0..a1610109c 100644 --- a/src/PowerFSM.cpp +++ b/src/PowerFSM.cpp @@ -58,6 +58,35 @@ static bool isPowered() return !isPowerSavingMode && powerStatus && (!powerStatus->getHasBattery() || powerStatus->getHasUSB()); } +#if defined(T5_S3_EPAPER_PRO) +static void t5BacklightOffForSleep() +{ + t5BacklightSetForcedBySleep(true); +} + +static void t5BacklightWakeFromSleep() +{ + t5BacklightSetForcedBySleep(false); +} + +static void t5BacklightOffForTimeout() +{ + t5BacklightSetForcedByTimeout(true); + t5TouchSetForcedByTimeout(true); +} + +static void t5BacklightOnFromUserInput() +{ + t5BacklightHandleUserInput(); + t5TouchHandleUserInput(); +} +#else +static void t5BacklightOffForSleep() {} +static void t5BacklightWakeFromSleep() {} +static void t5BacklightOffForTimeout() {} +static void t5BacklightOnFromUserInput() {} +#endif + static void sdsEnter() { LOG_POWERFSM("State: SDS"); @@ -87,6 +116,7 @@ static void lsEnter() LOG_POWERFSM("lsEnter begin, ls_secs=%u", config.power.ls_secs); if (screen) screen->setOn(false); + t5BacklightOffForSleep(); secsSlept = 0; // How long have we been sleeping this time // LOG_INFO("lsEnter end"); @@ -159,6 +189,8 @@ static void lsIdle() static void lsExit() { LOG_POWERFSM("State: lsExit"); + // Lift the light-sleep force-off gate when leaving LS. + t5BacklightWakeFromSleep(); } static void nbEnter() @@ -180,6 +212,8 @@ static void darkEnter() setBluetoothEnable(true); if (screen) screen->setOn(false); + // Screen timeout enters DARK; ensure backlight also turns off. + t5BacklightOffForTimeout(); } static void serialEnter() @@ -289,12 +323,13 @@ void PowerFSM_setup() powerFSM.add_transition(&stateNB, &stateNB, EVENT_PACKET_FOR_PHONE, NULL, "Received packet, resetting win wake"); // Handle press events - note: we ignore button presses when in API mode - powerFSM.add_transition(&stateLS, &stateON, EVENT_PRESS, NULL, "Press"); - powerFSM.add_transition(&stateNB, &stateON, EVENT_PRESS, NULL, "Press"); - powerFSM.add_transition(&stateDARK, isPowered() ? &statePOWER : &stateON, EVENT_PRESS, NULL, "Press"); - powerFSM.add_transition(&statePOWER, &statePOWER, EVENT_PRESS, NULL, "Press"); - powerFSM.add_transition(&stateON, &stateON, EVENT_PRESS, NULL, "Press"); // reenter On to restart our timers - powerFSM.add_transition(&stateSERIAL, &stateSERIAL, EVENT_PRESS, NULL, + powerFSM.add_transition(&stateLS, &stateON, EVENT_PRESS, t5BacklightOnFromUserInput, "Press"); + powerFSM.add_transition(&stateNB, &stateON, EVENT_PRESS, t5BacklightOnFromUserInput, "Press"); + powerFSM.add_transition(&stateDARK, isPowered() ? &statePOWER : &stateON, EVENT_PRESS, t5BacklightOnFromUserInput, "Press"); + powerFSM.add_transition(&statePOWER, &statePOWER, EVENT_PRESS, t5BacklightOnFromUserInput, "Press"); + powerFSM.add_transition(&stateON, &stateON, EVENT_PRESS, t5BacklightOnFromUserInput, + "Press"); // reenter On to restart our timers + powerFSM.add_transition(&stateSERIAL, &stateSERIAL, EVENT_PRESS, t5BacklightOnFromUserInput, "Press"); // Allow button to work while in serial API // Handle critically low power battery by forcing deep sleep @@ -314,11 +349,13 @@ void PowerFSM_setup() powerFSM.add_transition(&stateSERIAL, &stateSHUTDOWN, EVENT_SHUTDOWN, NULL, "Shutdown"); // Inputbroker - powerFSM.add_transition(&stateLS, &stateON, EVENT_INPUT, NULL, "Input Device"); - powerFSM.add_transition(&stateNB, &stateON, EVENT_INPUT, NULL, "Input Device"); - powerFSM.add_transition(&stateDARK, &stateON, EVENT_INPUT, NULL, "Input Device"); - powerFSM.add_transition(&stateON, &stateON, EVENT_INPUT, NULL, "Input Device"); // restarts the sleep timer - powerFSM.add_transition(&statePOWER, &statePOWER, EVENT_INPUT, NULL, "Input Device"); // restarts the sleep timer + powerFSM.add_transition(&stateLS, &stateON, EVENT_INPUT, t5BacklightOnFromUserInput, "Input Device"); + powerFSM.add_transition(&stateNB, &stateON, EVENT_INPUT, t5BacklightOnFromUserInput, "Input Device"); + powerFSM.add_transition(&stateDARK, &stateON, EVENT_INPUT, t5BacklightOnFromUserInput, "Input Device"); + powerFSM.add_transition(&stateON, &stateON, EVENT_INPUT, t5BacklightOnFromUserInput, + "Input Device"); // restarts the sleep timer + powerFSM.add_transition(&statePOWER, &statePOWER, EVENT_INPUT, t5BacklightOnFromUserInput, + "Input Device"); // restarts the sleep timer powerFSM.add_transition(&stateDARK, &stateON, EVENT_BLUETOOTH_PAIR, NULL, "Bluetooth pairing"); powerFSM.add_transition(&stateON, &stateON, EVENT_BLUETOOTH_PAIR, NULL, "Bluetooth pairing"); diff --git a/src/airtime.h b/src/airtime.h index 3ed7b6d7c..8e3e6c557 100644 --- a/src/airtime.h +++ b/src/airtime.h @@ -19,8 +19,8 @@ TX_LOG + RX_LOG = Total air time for a particular meshtastic channel. - TX_LOG + RX_LOG = Total air time for a particular meshtastic channel, including - other lora radios. + TX_LOG + RX_ALL_LOG = Total air time for a particular meshtastic channel, including + other lora radios. RX_ALL_LOG - RX_LOG = Other lora radios on our frequency channel. */ diff --git a/src/configuration.h b/src/configuration.h index 84dabee4e..efd9ddcf7 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -499,6 +499,7 @@ along with this program. If not, see . #define MESHTASTIC_EXCLUDE_PKI 1 #define MESHTASTIC_EXCLUDE_POWER_FSM 1 #define MESHTASTIC_EXCLUDE_TZ 1 +#define MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH 1 #endif // Turn off all optional modules diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index e298663a0..e3471c32a 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -415,30 +415,45 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) #if !defined(M5STACK_UNITC6L) case INA_ADDR: // Same as SHT2X case INA_ADDR_ALTERNATE: - case INA_ADDR_WAVESHARE_UPS: - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFE), 2); - LOG_DEBUG("Register MFG_UID: 0x%x", registerValue); - if (registerValue == 0x5449) { - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFF), 2); - LOG_DEBUG("Register DIE_UID: 0x%x", registerValue); + case INA_ADDR_WAVESHARE_UPS: { + uint16_t mfg = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFE), 2); - if (registerValue == 0x2260) { + LOG_DEBUG("Register MFG_UID: 0x%x", mfg); + + // Only read DIE_UID for vendors we recognize as INA-compatible to avoid + // an extra I2C transaction + delay on other devices sharing this address. + if (mfg == 0x5449 || mfg == 0x190F) { + uint16_t die = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFF), 2); + LOG_DEBUG("Register DIE_UID: 0x%x", die); + + // TI INA226 or fully compatible clones (e.g. TPA626) + if (mfg == 0x5449 && die == 0x2260) { logFoundDevice("INA226", (uint8_t)addr.address); type = INA226; - } else { + } + // Silergy SQ52201 (INA226-compatible with different IDs) + else if (mfg == 0x190F && die == 0x0000) { + logFoundDevice("INA226 (SQ52201)", (uint8_t)addr.address); + type = INA226; + } + // TI INA260 + else if (mfg == 0x5449) { logFoundDevice("INA260", (uint8_t)addr.address); type = INA260; } + } #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR - } else if (detectSHT21SerialNumber(i2cBus, (uint8_t)addr.address)) { + if (type == NONE && detectSHT21SerialNumber(i2cBus, (uint8_t)addr.address)) { logFoundDevice("SHTXX (SHT2X)", (uint8_t)addr.address); type = SHTXX; + } #endif - } else { // Assume INA219 if none of the above ones are found + else { // Assume INA219 if none of the above ones are found logFoundDevice("INA219", (uint8_t)addr.address); type = INA219; } break; + } case INA3221_ADDR: registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFE), 2); LOG_DEBUG("Register MFG_UID FE: 0x%x", registerValue); diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 3790510eb..9be88f015 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -39,6 +39,7 @@ along with this program. If not, see . #include "draw/NodeListRenderer.h" #include "draw/NotificationRenderer.h" #include "draw/UIRenderer.h" +#include "graphics/TFTColorRegions.h" #include "modules/CannedMessageModule.h" #if !MESHTASTIC_EXCLUDE_GPS @@ -54,6 +55,7 @@ along with this program. If not, see . #include "gps/RTC.h" #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" +#include "graphics/TFTPalette.h" #include "graphics/emotes.h" #include "graphics/images.h" #include "input/TouchScreenImpl1.h" @@ -69,12 +71,6 @@ along with this program. If not, see . #include "target_specific.h" extern MessageStore messageStore; -#if USE_TFTDISPLAY -extern uint16_t TFT_MESH; -#else -uint16_t TFT_MESH = COLOR565(0x67, 0xEA, 0x94); -#endif - #if HAS_WIFI && !defined(ARCH_PORTDUINO) #include "mesh/wifi/WiFiAPClient.h" #endif @@ -109,6 +105,27 @@ namespace graphics // A text message frame + debug frame + all the node infos FrameCallback *normalFrames; static uint32_t targetFramerate = IDLE_FRAMERATE; +#if GRAPHICS_TFT_COLORING_ENABLED +static inline void prepareFrameColorRegions() +{ +#if GRAPHICS_TFT_COLORING_ENABLED + clearTFTColorRegions(); + // Full-frame FrameMono inversion for themes that need it (e.g. light themes). + if (isThemeFullFrameInvert()) { + setAndRegisterTFTColorRole(TFTColorRole::FrameMono, getThemeBodyFg(), getThemeBodyBg(), 0, 0, screen->getWidth(), + screen->getHeight()); + } +#endif +} +#endif + +static inline void updateUiFrame(OLEDDisplayUi *ui) +{ +#if GRAPHICS_TFT_COLORING_ENABLED + prepareFrameColorRegions(); +#endif + ui->update(); +} // Global variables for alert banner - explicitly define with extern "C" linkage to prevent optimization uint32_t logo_timeout = 5000; // 4 seconds for EACH logo @@ -227,7 +244,7 @@ void Screen::showOverlayBanner(BannerOverlayOptions banner_overlay_options) static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); ui->setTargetFPS(60); - ui->update(); + updateUiFrame(ui); } // Called to trigger a banner with custom message and duration @@ -249,7 +266,7 @@ void Screen::showNodePicker(const char *message, uint32_t durationMs, std::funct static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); ui->setTargetFPS(60); - ui->update(); + updateUiFrame(ui); } // Called to trigger a banner with custom message and duration @@ -273,7 +290,7 @@ void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); ui->setTargetFPS(60); - ui->update(); + updateUiFrame(ui); } void Screen::showTextInput(const char *header, const char *initialText, uint32_t durationMs, @@ -296,7 +313,7 @@ void Screen::showTextInput(const char *header, const char *initialText, uint32_t static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); ui->setTargetFPS(60); - ui->update(); + updateUiFrame(ui); } static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) @@ -388,30 +405,6 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O { graphics::normalFrames = new FrameCallback[MAX_NUM_NODES + NUM_EXTRA_FRAMES]; - int32_t rawRGB = uiconfig.screen_rgb_color; - - // Only validate the combined value once - if (rawRGB > 0 && rawRGB <= 255255255) { - LOG_INFO("Setting screen RGB color to user chosen: 0x%06X", rawRGB); - // Extract each component as a normal int first - int r = (rawRGB >> 16) & 0xFF; - int g = (rawRGB >> 8) & 0xFF; - int b = rawRGB & 0xFF; - if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) { - TFT_MESH = COLOR565(static_cast(r), static_cast(g), static_cast(b)); - } -#ifdef TFT_MESH_OVERRIDE - } else if (rawRGB == 0) { - LOG_INFO("Setting screen RGB color to TFT_MESH_OVERRIDE: 0x%04X", TFT_MESH_OVERRIDE); - // Default to TFT_MESH_OVERRIDE if available - TFT_MESH = TFT_MESH_OVERRIDE; -#endif - } else { - // Default best readable yellow color - LOG_INFO("Setting screen RGB color to default: (255,255,128)"); - TFT_MESH = COLOR565(255, 255, 128); - } - #if defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SH1107_128_64) dispdev = new SH1106Wire(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); @@ -475,9 +468,13 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O #endif #if defined(USE_ST7789) - static_cast(dispdev)->setRGB(TFT_MESH); + // Keep firmware and ST7789 driver region structs layout-compatible: + // we pass `graphics::colorRegions` through a type cast below. + static_assert(sizeof(graphics::TFTColorRegion) == sizeof(::TFTColorRegion), + "graphics::TFTColorRegion layout must match ST7789 TFTColorRegion"); + static_cast(dispdev)->setRGB(TFTPalette::White, (::TFTColorRegion *)colorRegions); #elif defined(USE_ST7796) - static_cast(dispdev)->setRGB(TFT_MESH); + static_cast(dispdev)->setRGB(TFTPalette::White); #endif ui = new OLEDDisplayUi(dispdev); @@ -664,16 +661,16 @@ void Screen::setup() static_cast(dispdev)->setSubtype(7); #endif -#if defined(USE_ST7789) && defined(TFT_MESH) - // Apply custom RGB color (e.g. Heltec T114/T190) - static_cast(dispdev)->setRGB(TFT_MESH); +#if defined(USE_ST7789) + static_assert(sizeof(graphics::TFTColorRegion) == sizeof(::TFTColorRegion), + "graphics::TFTColorRegion layout must match ST7789 TFTColorRegion"); + static_cast(dispdev)->setRGB(TFTPalette::White, (::TFTColorRegion *)colorRegions); #endif #if defined(MUZI_BASE) dispdev->delayPoweron = true; #endif -#if defined(USE_ST7796) && defined(TFT_MESH) - // Custom text color, if defined in variant.h - static_cast(dispdev)->setRGB(TFT_MESH); +#if defined(USE_ST7796) + static_cast(dispdev)->setRGB(TFTPalette::White); #endif // Initialize display and UI system @@ -719,7 +716,7 @@ void Screen::setup() #endif { const char *region = myRegion ? myRegion->name : nullptr; - graphics::UIRenderer::drawIconScreen(region, display, state, x, y); + graphics::UIRenderer::drawBootIconScreen(region, display, state, x, y); } }; ui->setFrames(alertFrames, 1); @@ -759,9 +756,9 @@ void Screen::setup() // Turn on display and trigger first draw handleSetOn(true); graphics::currentResolution = graphics::determineScreenResolution(dispdev->height(), dispdev->width()); - ui->update(); + updateUiFrame(ui); #ifndef USE_EINK - ui->update(); // Some SSD1306 clones drop the first draw, so run twice + updateUiFrame(ui); // Some SSD1306 clones drop the first draw, so run twice #endif serialSinceMsec = millis(); @@ -834,7 +831,7 @@ void Screen::forceDisplay(bool forceUiUpdate) do { startUpdate = millis(); // Handle impossibly unlikely corner case of a millis() overflow.. delay(10); - ui->update(); + updateUiFrame(ui); } while (ui->getUiState()->lastUpdate < startUpdate); // Return to normal frame rate @@ -905,9 +902,9 @@ int32_t Screen::runOnce() static FrameCallback bootOEMFrames[] = {graphics::UIRenderer::drawOEMBootScreen}; static const int bootOEMFrameCount = sizeof(bootOEMFrames) / sizeof(bootOEMFrames[0]); ui->setFrames(bootOEMFrames, bootOEMFrameCount); - ui->update(); + updateUiFrame(ui); #ifndef USE_EINK - ui->update(); + updateUiFrame(ui); #endif showingOEMBootScreen = false; } @@ -998,7 +995,7 @@ int32_t Screen::runOnce() // this must be before the frameState == FIXED check, because we always // want to draw at least one FIXED frame before doing forceDisplay - ui->update(); + updateUiFrame(ui); // Switch to a low framerate (to save CPU) when we are not in transition // but we should only call setTargetFPS when framestate changes, because @@ -1060,7 +1057,7 @@ void Screen::setSSLFrames() // LOG_DEBUG("Show SSL frames"); static FrameCallback sslFrames[] = {NotificationRenderer::drawSSLScreen}; ui->setFrames(sslFrames, 1); - ui->update(); + updateUiFrame(ui); } } @@ -1096,7 +1093,7 @@ void Screen::setScreensaverFrames(FrameCallback einkScreensaver) do { startUpdate = millis(); // Handle impossibly unlikely corner case of a millis() overflow.. delay(1); - ui->update(); + updateUiFrame(ui); } while (ui->getUiState()->lastUpdate < startUpdate); #if defined(USE_EINK_PARALLELDISPLAY) @@ -1471,9 +1468,15 @@ void Screen::blink() dispdev->setBrightness(254); while (count > 0) { dispdev->fillRect(0, 0, dispdev->getWidth(), dispdev->getHeight()); +#if GRAPHICS_TFT_COLORING_ENABLED + prepareFrameColorRegions(); +#endif dispdev->display(); delay(50); dispdev->clear(); +#if GRAPHICS_TFT_COLORING_ENABLED + prepareFrameColorRegions(); +#endif dispdev->display(); delay(50); count = count - 1; @@ -1607,6 +1610,9 @@ void Screen::setFastFramerate() { #if defined(M5STACK_UNITC6L) dispdev->clear(); +#if GRAPHICS_TFT_COLORING_ENABLED + prepareFrameColorRegions(); +#endif dispdev->display(); #endif // We are about to start a transition so speed up fps @@ -1818,7 +1824,7 @@ int Screen::handleInputEvent(const InputEvent *event) static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); setFastFramerate(); // Draw ASAP - ui->update(); + updateUiFrame(ui); return 0; } @@ -1833,7 +1839,7 @@ int Screen::handleInputEvent(const InputEvent *event) static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); setFastFramerate(); // Draw ASAP - ui->update(); + updateUiFrame(ui); menuHandler::handleMenuSwitch(dispdev); return 0; diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp index ec50654ae..becd3e75d 100644 --- a/src/graphics/SharedUIDisplay.cpp +++ b/src/graphics/SharedUIDisplay.cpp @@ -1,16 +1,20 @@ #include "configuration.h" #if HAS_SCREEN #include "MeshService.h" +#include "NodeDB.h" #include "RTC.h" #include "draw/NodeListRenderer.h" #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" +#include "graphics/TFTColorRegions.h" +#include "graphics/TFTPalette.h" #include "graphics/draw/UIRenderer.h" #include "main.h" #include "meshtastic/config.pb.h" #include "modules/ExternalNotificationModule.h" #include "power.h" #include +#include #include namespace graphics @@ -65,6 +69,12 @@ uint32_t lastBlinkShared = 0; bool isMailIconVisible = true; uint32_t lastMailBlink = 0; +static inline bool useClockHeaderAccentTheme(uint32_t themeId) +{ + return themeId == ThemeID::Pink || themeId == ThemeID::Creamsicle || themeId == ThemeID::MeshtasticGreen || + themeId == ThemeID::ClassicRed || themeId == ThemeID::MonochromeWhite; +} + // ********************************* // * Rounded Header when inverted * // ********************************* @@ -85,7 +95,8 @@ void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, // ************************* // * Common Header Drawing * // ************************* -void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert, bool show_date) +void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert, bool show_date, + bool transparent_background, bool use_title_color_override, uint16_t title_color_override) { constexpr int HEADER_OFFSET_Y = 1; y += HEADER_OFFSET_Y; @@ -100,30 +111,93 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti const int screenW = display->getWidth(); const int screenH = display->getHeight(); + const int headerHeight = highlightHeight + 2; + const uint16_t headerColorForRoles = getThemeHeaderBg(); + // Color TFT headers use a fixed dark background + white glyphs. + // Keep legacy inverted bitmap behavior only for monochrome displays. + const bool useInvertedHeaderStyle = (isInverted && !force_no_invert && !isTFTColoringEnabled() && !transparent_background); +#if GRAPHICS_TFT_COLORING_ENABLED + int statusLeftEndX = 0; + int statusRightStartX = screenW; + const bool isClockHeader = transparent_background && show_date && (!titleStr || titleStr[0] == '\0'); + const auto activeThemeId = getActiveTheme().id; + const bool useClockHeaderAccent = isClockHeader && useClockHeaderAccentTheme(activeThemeId); +#endif + + { + const uint16_t headerColor = getThemeHeaderBg(); + const uint16_t headerTextColor = getThemeHeaderText(); + const uint16_t headerTitleColorForRole = use_title_color_override ? title_color_override : headerTextColor; + uint16_t headerStatusColor = getThemeHeaderStatus(); +#if GRAPHICS_TFT_COLORING_ENABLED + // Clock frame uses transparent header + date + empty title. + // For accent clock themes (Pink/Creamsicle + classic monochrome), tint + // status items (battery outline, %, date, mail icon) to the header accent. + if (useClockHeaderAccent) { + headerStatusColor = getThemeHeaderBg(); + } + + if (transparent_background) { + // Transparent clock headers should inherit whatever body off-color is + // already active under the header (important for light/inverted themes). + const uint16_t transparentBgColor = resolveTFTOffColorAt(0, headerHeight + 1, getThemeBodyBg()); + setAndRegisterTFTColorRole(TFTColorRole::HeaderBackground, transparentBgColor, transparentBgColor, 0, 0, screenW, + headerHeight); + setTFTColorRole(TFTColorRole::HeaderTitle, headerTitleColorForRole, transparentBgColor); + setTFTColorRole(TFTColorRole::HeaderStatus, headerStatusColor, transparentBgColor); + } else if (useInvertedHeaderStyle) { + setAndRegisterTFTColorRole(TFTColorRole::HeaderBackground, headerColor, TFTPalette::Black, 0, 0, screenW, + headerHeight); + setTFTColorRole(TFTColorRole::HeaderTitle, headerColor, headerTitleColorForRole); + setTFTColorRole(TFTColorRole::HeaderStatus, headerColor, headerStatusColor); + } else { + setAndRegisterTFTColorRole(TFTColorRole::HeaderBackground, TFTPalette::Black, headerColor, 0, 0, screenW, + headerHeight); + setTFTColorRole(TFTColorRole::HeaderTitle, headerTitleColorForRole, headerColor); + setTFTColorRole(TFTColorRole::HeaderStatus, headerStatusColor, headerColor); + } +#endif - if (!force_no_invert) { // === Inverted Header Background === - if (isInverted) { + if (useInvertedHeaderStyle) { display->setColor(BLACK); - display->fillRect(0, 0, screenW, highlightHeight + 2); + display->fillRect(0, 0, screenW, headerHeight); display->setColor(WHITE); drawRoundedHighlight(display, x, y, screenW, highlightHeight, 2); display->setColor(BLACK); } else { display->setColor(BLACK); - display->fillRect(0, 0, screenW, highlightHeight + 2); - display->setColor(WHITE); - if (currentResolution == ScreenResolution::High) { - display->drawLine(0, 20, screenW, 20); - } else { - display->drawLine(0, 14, screenW, 14); + display->fillRect(0, 0, screenW, headerHeight); +// Keep the legacy white separator for monochrome displays only when header background is visible. +#if !GRAPHICS_TFT_COLORING_ENABLED + if (!transparent_background) { + display->setColor(WHITE); + if (currentResolution == ScreenResolution::High) { + display->drawLine(0, 20, screenW, 20); + } else { + display->drawLine(0, 14, screenW, 14); + } } +#endif } + if (transparent_background) { + display->setColor(WHITE); + } + +#if GRAPHICS_TFT_COLORING_ENABLED + // TFT role coloring expects foreground glyph bits to be "set". + display->setColor(WHITE); +#endif + // === Screen Title === const char *headerTitle = titleStr ? titleStr : ""; const int titleWidth = UIRenderer::measureStringWithEmotes(display, headerTitle); const int titleX = (SCREEN_WIDTH - titleWidth) / 2; +#if GRAPHICS_TFT_COLORING_ENABLED + const int titleRegionWidth = titleWidth + (config.display.heading_bold ? 3 : 2); + registerTFTColorRegion(TFTColorRole::HeaderTitle, titleX - 1, y, titleRegionWidth, FONT_HEIGHT_SMALL); +#endif UIRenderer::drawStringWithEmotes(display, titleX, y, headerTitle, FONT_HEIGHT_SMALL, 1, config.display.heading_bold); } display->setTextAlignment(TEXT_ALIGN_LEFT); @@ -152,6 +226,17 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti bool useHorizontalBattery = (currentResolution == ScreenResolution::High && screenW >= screenH); const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + bool hasBatteryFillRegion = false; + int16_t batteryFillRegionX = 0; + int16_t batteryFillRegionY = 0; + int16_t batteryFillRegionW = 0; + int16_t batteryFillRegionH = 0; +#if GRAPHICS_TFT_COLORING_ENABLED + uint16_t batteryFillColor = getThemeBatteryFillColor(chargePercent); + if (useClockHeaderAccent) { + batteryFillColor = getThemeHeaderBg(); + } +#endif int batteryX = 1; int batteryY = HEADER_OFFSET_Y + 1; @@ -180,6 +265,15 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti display->drawLine(batteryX + 5, batteryY + 12, batteryX + 10, batteryY + 12); int fillWidth = 14 * chargePercent / 100; display->fillRect(batteryX + 1, batteryY + 1, fillWidth, 11); +#if GRAPHICS_TFT_COLORING_ENABLED + if (fillWidth > 0) { + hasBatteryFillRegion = true; + batteryFillRegionX = batteryX + 1; + batteryFillRegionY = batteryY + 1; + batteryFillRegionW = fillWidth; + batteryFillRegionH = 11; + } +#endif } batteryX += 18; // Icon + 2 pixels } else { @@ -194,21 +288,41 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti int fillHeight = 8 * chargePercent / 100; int fillY = batteryY - fillHeight; display->fillRect(batteryX + 1, fillY + 10, 5, fillHeight); +#if GRAPHICS_TFT_COLORING_ENABLED + if (fillHeight > 0) { + hasBatteryFillRegion = true; + batteryFillRegionX = batteryX + 1; + batteryFillRegionY = fillY + 10; + batteryFillRegionW = 5; + batteryFillRegionH = fillHeight; + } +#endif } batteryX += 9; // Icon + 2 pixels } } +#if GRAPHICS_TFT_COLORING_ENABLED + statusLeftEndX = batteryX + 2; +#endif if (chargePercent != 101) { // === Battery % Display === char chargeStr[4]; snprintf(chargeStr, sizeof(chargeStr), "%d", chargePercent); int chargeNumWidth = display->getStringWidth(chargeStr); + const int percentWidth = display->getStringWidth("%"); + const int percentX = batteryX + chargeNumWidth - 1; display->drawString(batteryX, textY, chargeStr); - display->drawString(batteryX + chargeNumWidth - 1, textY, "%"); + display->drawString(percentX, textY, "%"); +#if GRAPHICS_TFT_COLORING_ENABLED + statusLeftEndX = percentX + percentWidth + 2; +#endif if (isBold) { display->drawString(batteryX + 1, textY, chargeStr); - display->drawString(batteryX + chargeNumWidth, textY, "%"); + display->drawString(percentX + 1, textY, "%"); +#if GRAPHICS_TFT_COLORING_ENABLED + statusLeftEndX = percentX + percentWidth + 3; +#endif } } @@ -253,6 +367,9 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti timeStrWidth = display->getStringWidth(timeStr); } timeX = screenW - xOffset - timeStrWidth + 3; +#if GRAPHICS_TFT_COLORING_ENABLED + statusRightStartX = timeX - (useHorizontalBattery ? 22 : 16); +#endif // === Show Mail or Mute Icon to the Left of Time === int iconRightEdge = timeX - 2; @@ -278,7 +395,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti int iconW = 16, iconH = 12; int iconX = iconRightEdge - iconW; int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1; - if (isInverted && !force_no_invert) { + if (useInvertedHeaderStyle) { display->setColor(WHITE); display->fillRect(iconX - 1, iconY - 1, iconW + 3, iconH + 2); display->setColor(BLACK); @@ -293,7 +410,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti } else { int iconX = iconRightEdge - (mail_width - 2); int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; - if (isInverted && !force_no_invert) { + if (useInvertedHeaderStyle) { display->setColor(WHITE); display->fillRect(iconX - 1, iconY - 1, mail_width + 2, mail_height + 2); display->setColor(BLACK); @@ -309,7 +426,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti int iconX = iconRightEdge - mute_symbol_big_width; int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2; - if (isInverted && !force_no_invert) { + if (useInvertedHeaderStyle) { display->setColor(WHITE); display->fillRect(iconX - 1, iconY - 1, mute_symbol_big_width + 2, mute_symbol_big_height + 2); display->setColor(BLACK); @@ -323,7 +440,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti int iconX = iconRightEdge - mute_symbol_width; int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; - if (isInverted && !force_no_invert) { + if (useInvertedHeaderStyle) { display->setColor(WHITE); display->fillRect(iconX - 1, iconY - 1, mute_symbol_width + 2, mute_symbol_height + 2); display->setColor(BLACK); @@ -351,7 +468,9 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti } else { // === No Time Available: Mail/Mute Icon Moves to Far Right === int iconRightEdge = screenW - xOffset; - +#if GRAPHICS_TFT_COLORING_ENABLED + statusRightStartX = screenW - (useHorizontalBattery ? 22 : 12); +#endif bool showMail = false; #ifndef USE_EINK @@ -393,6 +512,16 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti } } } +#endif +#if GRAPHICS_TFT_COLORING_ENABLED + registerTFTColorRegion(TFTColorRole::HeaderStatus, 0, 0, statusLeftEndX, headerHeight); + if (statusRightStartX < screenW) { + registerTFTColorRegion(TFTColorRole::HeaderStatus, statusRightStartX, 0, screenW - statusRightStartX, headerHeight); + } + if (hasBatteryFillRegion) { + registerTFTColorRegionDirect(batteryFillRegionX, batteryFillRegionY, batteryFillRegionW, batteryFillRegionH, + batteryFillColor, headerColorForRoles); + } #endif display->setColor(WHITE); // Reset for other UI } @@ -430,14 +559,23 @@ void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y) return; const int scale = (currentResolution == ScreenResolution::High) ? 2 : 1; + const int footerY = SCREEN_HEIGHT - (1 * scale) - (connection_icon_height * scale); + const int footerH = (connection_icon_height * scale) + (2 * scale); + const int iconX = 0; + const int iconY = SCREEN_HEIGHT - (connection_icon_height * scale); + const int iconW = connection_icon_width * scale; + const int iconH = connection_icon_height * scale; + +#if GRAPHICS_TFT_COLORING_ENABLED + // Only tint the link glyph itself on TFT; keep the footer background black. + setAndRegisterTFTColorRole(TFTColorRole::ConnectionIcon, TFTPalette::Blue, TFTPalette::Black, iconX, iconY, iconW, iconH); +#endif + display->setColor(BLACK); - display->fillRect(0, SCREEN_HEIGHT - (1 * scale) - (connection_icon_height * scale), (connection_icon_width * scale), - (connection_icon_height * scale) + (2 * scale)); + display->fillRect(0, footerY, SCREEN_WIDTH, footerH); display->setColor(WHITE); if (currentResolution == ScreenResolution::High) { const int bytesPerRow = (connection_icon_width + 7) / 8; - int iconX = 0; - int iconY = SCREEN_HEIGHT - (connection_icon_height * 2); for (int yy = 0; yy < connection_icon_height; ++yy) { const uint8_t *rowPtr = connection_icon + yy * bytesPerRow; @@ -451,65 +589,127 @@ void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y) } } else { - display->drawXbm(0, SCREEN_HEIGHT - connection_icon_height, connection_icon_width, connection_icon_height, - connection_icon); + display->drawXbm(iconX, iconY, connection_icon_width, connection_icon_height, connection_icon); } } bool isAllowedPunctuation(char c) { - const std::string allowed = ".,!?;:-_()[]{}'\"@#$/\\&+=%~^ "; - return allowed.find(c) != std::string::npos; + switch (c) { + case '.': + case ',': + case '!': + case '?': + case ';': + case ':': + case '-': + case '_': + case '(': + case ')': + case '[': + case ']': + case '{': + case '}': + case '\'': + case '"': + case '@': + case '#': + case '$': + case '/': + case '\\': + case '&': + case '+': + case '=': + case '%': + case '~': + case '^': + case ' ': + return true; + default: + return false; + } } -static void replaceAll(std::string &s, const std::string &from, const std::string &to) +static inline size_t utf8CodePointLength(unsigned char lead) { - if (from.empty()) - return; - size_t pos = 0; - while ((pos = s.find(from, pos)) != std::string::npos) { - s.replace(pos, from.size(), to); - pos += to.size(); + if ((lead & 0x80) == 0x00) { + return 1; } + if ((lead & 0xE0) == 0xC0) { + return 2; + } + if ((lead & 0xF0) == 0xE0) { + return 3; + } + if ((lead & 0xF8) == 0xF0) { + return 4; + } + return 1; } std::string sanitizeString(const std::string &input) { + static constexpr char kReplacementChar = static_cast(0xBF); // Inverted question mark in ISO-8859-1. std::string output; + output.reserve(input.size()); bool inReplacement = false; - - // Make a mutable copy so we can normalize UTF-8 “smart punctuation” into ASCII first. - std::string s = input; - - // Curly single quotes: ‘ ’ - replaceAll(s, "\xE2\x80\x98", "'"); // U+2018 - replaceAll(s, "\xE2\x80\x99", "'"); // U+2019 - - // Curly double quotes: “ ” - replaceAll(s, "\xE2\x80\x9C", "\""); // U+201C - replaceAll(s, "\xE2\x80\x9D", "\""); // U+201D - - // En dash / Em dash: – — - replaceAll(s, "\xE2\x80\x93", "-"); // U+2013 - replaceAll(s, "\xE2\x80\x94", "-"); // U+2014 - - // Non-breaking space - replaceAll(s, "\xC2\xA0", " "); // U+00A0 - - // Now do your original sanitize pass over the normalized string. - for (unsigned char uc : s) { - char c = static_cast(uc); - if (std::isalnum(uc) || isAllowedPunctuation(c)) { - output += c; - inReplacement = false; - } else { + const size_t inputSize = input.size(); + size_t i = 0; + while (i < inputSize) { + const unsigned char byte0 = static_cast(input[i]); + char normalized = '\0'; + size_t consumed = 0; + if (byte0 < 0x80) { + normalized = static_cast(byte0); + consumed = 1; + } else if ((i + 2) < inputSize && byte0 == 0xE2 && static_cast(input[i + 1]) == 0x80) { + // Smart punctuation: ' ' \" \" - - + switch (static_cast(input[i + 2])) { + case 0x98: + case 0x99: + normalized = '\''; + consumed = 3; + break; + case 0x9C: + case 0x9D: + normalized = '\"'; + consumed = 3; + break; + case 0x93: + case 0x94: + normalized = '-'; + consumed = 3; + break; + default: + break; + } + } else if ((i + 1) < inputSize && byte0 == 0xC2 && static_cast(input[i + 1]) == 0xA0) { + // Non-breaking space. + normalized = ' '; + consumed = 2; + } + if (consumed == 0) { + size_t seqLen = utf8CodePointLength(byte0); + if (seqLen > (inputSize - i)) { + seqLen = 1; + } if (!inReplacement) { - output += static_cast(0xBF); // ISO-8859-1 for inverted question mark + output.push_back(kReplacementChar); inReplacement = true; } + i += seqLen; + continue; } + const unsigned char normalizedUc = static_cast(normalized); + if (std::isalnum(normalizedUc) || isAllowedPunctuation(normalized)) { + output.push_back(normalized); + inReplacement = false; + } else if (!inReplacement) { + output.push_back(kReplacementChar); + inReplacement = true; + } + i += consumed; } - return output; } diff --git a/src/graphics/SharedUIDisplay.h b/src/graphics/SharedUIDisplay.h index 35e767056..95244d099 100644 --- a/src/graphics/SharedUIDisplay.h +++ b/src/graphics/SharedUIDisplay.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include namespace graphics @@ -52,7 +53,8 @@ void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, // Shared battery/time/mail header void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr = "", bool force_no_invert = false, - bool show_date = false); + bool show_date = false, bool transparent_background = false, bool use_title_color_override = false, + uint16_t title_color_override = 0); // Shared battery/time/mail header void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y); diff --git a/src/graphics/TFTColorRegions.cpp b/src/graphics/TFTColorRegions.cpp new file mode 100644 index 000000000..877835ea6 --- /dev/null +++ b/src/graphics/TFTColorRegions.cpp @@ -0,0 +1,819 @@ +#include "TFTColorRegions.h" +#include "NodeDB.h" +#include "TFTPalette.h" + +#include + +namespace graphics +{ +TFTColorRegion colorRegions[MAX_TFT_COLOR_REGIONS]; + +namespace +{ + +struct TFTRoleColorsBe { + uint16_t onColorBe; + uint16_t offColorBe; +}; + +static uint8_t colorRegionCount = 0; +static constexpr uint32_t kFnv1aOffsetBasis = 2166136261u; +static constexpr uint32_t kFnv1aPrime = 16777619u; + +static constexpr uint16_t toBe565(uint16_t color) +{ + return static_cast((color >> 8) | (color << 8)); +} + +static constexpr bool kRoleIsBody[static_cast(TFTColorRole::Count)] = { + false, // HeaderBackground + false, // HeaderTitle + false, // HeaderStatus + true, // SignalBars + true, // ConnectionIcon + true, // UtilizationFill + true, // FavoriteNode + true, // ActionMenuBorder + true, // ActionMenuBody + true, // ActionMenuTitle + true, // FrameMono + false, // BootSplash + true, // FavoriteNodeBGHighlight + false, // NavigationBar + false // NavigationArrow +}; + +static inline bool isBodyColorRole(TFTColorRole role) +{ + return kRoleIsBody[static_cast(role)]; +} + +static inline bool isMonochromeTheme(uint32_t themeId) +{ + return themeId == ThemeID::MeshtasticGreen || themeId == ThemeID::ClassicRed || themeId == ThemeID::MonochromeWhite; +} + +static inline uint16_t getMonochromeAccent(uint32_t themeId) +{ + return (themeId == ThemeID::MeshtasticGreen) ? TFTPalette::MeshtasticGreen + : (themeId == ThemeID::ClassicRed) ? TFTPalette::ClassicRed + : TFTPalette::White; +} + +static inline void replaceColor(uint16_t &value, uint16_t from, uint16_t to) +{ + if (value == from) { + value = to; + } +} + +static inline uint32_t fnv1aAppendByte(uint32_t hash, uint8_t value) +{ + return (hash ^ value) * kFnv1aPrime; +} + +static inline uint32_t fnv1aAppendU16(uint32_t hash, uint16_t value) +{ + hash = fnv1aAppendByte(hash, static_cast(value & 0xFF)); + hash = fnv1aAppendByte(hash, static_cast((value >> 8) & 0xFF)); + return hash; +} + +// Compile-time header color overrides (backward-compatible) +#ifdef TFT_HEADER_BG_COLOR_OVERRIDE +static constexpr uint16_t kHeaderBackground = TFT_HEADER_BG_COLOR_OVERRIDE; +#else +static constexpr uint16_t kHeaderBackground = TFTPalette::DarkGray; +#endif + +#ifdef TFT_HEADER_TITLE_COLOR_OVERRIDE +static constexpr uint16_t kTitleColor = TFT_HEADER_TITLE_COLOR_OVERRIDE; +#else +static constexpr uint16_t kTitleColor = TFTPalette::White; +#endif + +#ifdef TFT_HEADER_STATUS_COLOR_OVERRIDE +static constexpr uint16_t kStatusColor = TFT_HEADER_STATUS_COLOR_OVERRIDE; +#else +static constexpr uint16_t kStatusColor = TFTPalette::White; +#endif + +// Theme definitions +// Stored in kThemes[] and looked up by matching uiconfig.screen_rgb_color +// against each entry's .uniqueIdentifier field. + +static const TFTThemeDef kThemes[] = { + + // Default Dark (ThemeID::DefaultDark = 0) + { + ThemeID::DefaultDark, // id + "Default Dark", // name + 0, // uniqueIdentifier + // roles[TFTColorRole::Count] + { + {kHeaderBackground, TFTPalette::Black}, // HeaderBackground + {kHeaderBackground, kTitleColor}, // HeaderTitle + {kHeaderBackground, kStatusColor}, // HeaderStatus + {TFTPalette::Good, TFTPalette::Black}, // SignalBars + {TFTPalette::Blue, TFTPalette::Black}, // ConnectionIcon + {TFTPalette::Good, TFTPalette::Black}, // UtilizationFill + {TFTPalette::Yellow, TFTPalette::Black}, // FavoriteNode + {TFTPalette::DarkGray, TFTPalette::Black}, // ActionMenuBorder + {TFTPalette::White, TFTPalette::Black}, // ActionMenuBody + {TFTPalette::DarkGray, TFTPalette::White}, // ActionMenuTitle + {TFTPalette::Black, TFTPalette::White}, // FrameMono + {TFTPalette::White, TFTPalette::Black}, // BootSplash + {TFTPalette::Yellow, TFTPalette::Black}, // FavoriteNodeBGHighlight + {kStatusColor, kHeaderBackground}, // NavigationBar (icon fg, bar bg) + {kTitleColor, TFTPalette::Black}, // NavigationArrow (arrow fg, body bg) + }, + TFTPalette::Good, // batteryFillGood + TFTPalette::Medium, // batteryFillMedium + TFTPalette::Bad, // batteryFillBad + false, // fullFrameInvert + true, // visible + }, + + // Default Light (ThemeID::DefaultLight = 1) + { + ThemeID::DefaultLight, // id + "Default Light", // name + 1, // uniqueIdentifier + { + {TFTPalette::LightGray, TFTPalette::Black}, // HeaderBackground + {TFTPalette::LightGray, TFTPalette::Black}, // HeaderTitle + {TFTPalette::LightGray, TFTPalette::Black}, // HeaderStatus + {TFTPalette::Good, TFTPalette::White}, // SignalBars + {TFTPalette::Blue, TFTPalette::White}, // ConnectionIcon + {TFTPalette::Good, TFTPalette::White}, // UtilizationFill + {TFTPalette::Black, TFTPalette::Yellow}, // FavoriteNode + {TFTPalette::DarkGray, TFTPalette::White}, // ActionMenuBorder + {TFTPalette::Black, TFTPalette::White}, // ActionMenuBody + {TFTPalette::DarkGray, TFTPalette::Black}, // ActionMenuTitle + {TFTPalette::Black, TFTPalette::White}, // FrameMono + {TFTPalette::White, TFTPalette::Black}, // BootSplash + {TFTPalette::Black, TFTPalette::Yellow}, // FavoriteNodeBGHighlight + {TFTPalette::Black, TFTPalette::LightGray}, // NavigationBar (icon fg, bar bg) + {TFTPalette::Black, TFTPalette::White}, // NavigationArrow (arrow fg, body bg) + }, + TFTPalette::Good, // batteryFillGood + TFTPalette::Medium, // batteryFillMedium + TFTPalette::Bad, // batteryFillBad + true, // fullFrameInvert + true, // visible + }, + + // Christmas (ThemeID::Christmas = 2) + { + ThemeID::Christmas, // id + "Christmas", // name + 2, // uniqueIdentifier + { + {TFTPalette::ChristmasRed, TFTPalette::Black}, // HeaderBackground + {TFTPalette::ChristmasRed, TFTPalette::Gold}, // HeaderTitle + {TFTPalette::ChristmasRed, TFTPalette::Gold}, // HeaderStatus + {TFTPalette::ChristmasGreen, TFTPalette::Pine}, // SignalBars + {TFTPalette::Gold, TFTPalette::Pine}, // ConnectionIcon + {TFTPalette::ChristmasGreen, TFTPalette::Pine}, // UtilizationFill + {TFTPalette::Gold, TFTPalette::Pine}, // FavoriteNode + {TFTPalette::ChristmasRed, TFTPalette::Pine}, // ActionMenuBorder + {TFTPalette::White, TFTPalette::Pine}, // ActionMenuBody + {TFTPalette::ChristmasRed, TFTPalette::White}, // ActionMenuTitle + {TFTPalette::Pine, TFTPalette::White}, // FrameMono + {TFTPalette::White, TFTPalette::ChristmasRed}, // BootSplash + {TFTPalette::Gold, TFTPalette::Pine}, // FavoriteNodeBGHighlight + {TFTPalette::Gold, TFTPalette::ChristmasRed}, // NavigationBar (icon fg, bar bg) + {TFTPalette::Gold, TFTPalette::Pine}, // NavigationArrow (arrow fg, body bg) + }, + TFTPalette::ChristmasGreen, // batteryFillGood + TFTPalette::Gold, // batteryFillMedium + TFTPalette::ChristmasRed, // batteryFillBad + true, // fullFrameInvert + false, // visible + }, + + // Pink (ThemeID::Pink = 3) light variant + { + ThemeID::Pink, // id + "Pink", // name + 3, // uniqueIdentifier + { + {TFTPalette::HotPink, TFTPalette::Black}, // HeaderBackground + {TFTPalette::HotPink, TFTPalette::White}, // HeaderTitle + {TFTPalette::HotPink, TFTPalette::White}, // HeaderStatus + {TFTPalette::DeepPink, TFTPalette::PalePink}, // SignalBars + {TFTPalette::HotPink, TFTPalette::PalePink}, // ConnectionIcon + {TFTPalette::DeepPink, TFTPalette::PalePink}, // UtilizationFill + {TFTPalette::Black, TFTPalette::HotPink}, // FavoriteNode + {TFTPalette::HotPink, TFTPalette::PalePink}, // ActionMenuBorder + {TFTPalette::Black, TFTPalette::PalePink}, // ActionMenuBody + {TFTPalette::HotPink, TFTPalette::White}, // ActionMenuTitle + {TFTPalette::Black, TFTPalette::White}, // FrameMono + {TFTPalette::White, TFTPalette::HotPink}, // BootSplash + {TFTPalette::Black, TFTPalette::HotPink}, // FavoriteNodeBGHighlight + {TFTPalette::White, TFTPalette::HotPink}, // NavigationBar (icon fg, bar bg) + {TFTPalette::HotPink, TFTPalette::PalePink}, // NavigationArrow (arrow fg, body bg) + }, + TFTPalette::DeepPink, // batteryFillGood + TFTPalette::HotPink, // batteryFillMedium + TFTPalette::Bad, // batteryFillBad + true, // fullFrameInvert + true, // visible + }, + + // Blue (ThemeID::Blue = 4) dark variant + { + ThemeID::Blue, // id + "Blue", // name + 4, // uniqueIdentifier + { + {TFTPalette::DeepBlue, TFTPalette::Black}, // HeaderBackground + {TFTPalette::DeepBlue, TFTPalette::White}, // HeaderTitle + {TFTPalette::DeepBlue, TFTPalette::SkyBlue}, // HeaderStatus + {TFTPalette::SkyBlue, TFTPalette::Navy}, // SignalBars + {TFTPalette::SkyBlue, TFTPalette::Navy}, // ConnectionIcon + {TFTPalette::SkyBlue, TFTPalette::Navy}, // UtilizationFill + {TFTPalette::SkyBlue, TFTPalette::Navy}, // FavoriteNode + {TFTPalette::DeepBlue, TFTPalette::Navy}, // ActionMenuBorder + {TFTPalette::White, TFTPalette::Navy}, // ActionMenuBody + {TFTPalette::DeepBlue, TFTPalette::White}, // ActionMenuTitle + {TFTPalette::Navy, TFTPalette::White}, // FrameMono + {TFTPalette::White, TFTPalette::DeepBlue}, // BootSplash + {TFTPalette::SkyBlue, TFTPalette::Navy}, // FavoriteNodeBGHighlight + {TFTPalette::SkyBlue, TFTPalette::DeepBlue}, // NavigationBar (icon fg, bar bg) + {TFTPalette::SkyBlue, TFTPalette::Black}, // NavigationArrow (arrow fg, body bg) + }, + TFTPalette::SkyBlue, // batteryFillGood + TFTPalette::Medium, // batteryFillMedium + TFTPalette::Bad, // batteryFillBad + true, // fullFrameInvert + true, // visible + }, + + // Creamsicle (ThemeID::Creamsicle = 5)light variant + { + ThemeID::Creamsicle, // id + "Creamsicle", // name + 5, // uniqueIdentifier + { + {TFTPalette::CreamOrange, TFTPalette::Black}, // HeaderBackground + {TFTPalette::CreamOrange, TFTPalette::White}, // HeaderTitle + {TFTPalette::CreamOrange, TFTPalette::White}, // HeaderStatus + {TFTPalette::DeepOrange, TFTPalette::Cream}, // SignalBars + {TFTPalette::CreamOrange, TFTPalette::Cream}, // ConnectionIcon + {TFTPalette::DeepOrange, TFTPalette::Cream}, // UtilizationFill + {TFTPalette::Black, TFTPalette::CreamOrange}, // FavoriteNode + {TFTPalette::CreamOrange, TFTPalette::Cream}, // ActionMenuBorder + {TFTPalette::Black, TFTPalette::Cream}, // ActionMenuBody + {TFTPalette::CreamOrange, TFTPalette::White}, // ActionMenuTitle + {TFTPalette::Black, TFTPalette::White}, // FrameMono + {TFTPalette::White, TFTPalette::CreamOrange}, // BootSplash + {TFTPalette::Black, TFTPalette::CreamOrange}, // FavoriteNodeBGHighlight + {TFTPalette::White, TFTPalette::CreamOrange}, // NavigationBar (icon fg, bar bg) + {TFTPalette::CreamOrange, TFTPalette::White}, // NavigationArrow (arrow fg, body bg) + }, + TFTPalette::DeepOrange, // batteryFillGood + TFTPalette::Gold, // batteryFillMedium + TFTPalette::Bad, // batteryFillBad + true, // fullFrameInvert + true, // visible + }, + + // Meshtastic Green (ThemeID::MeshtasticGreen = 6) classic monochrome + // Pure single-color-on-black look. Every role maps foreground pixels to + // the theme color and background pixels to Black. + { + ThemeID::MeshtasticGreen, // id + "Meshtastic Green", // name + 6, // uniqueIdentifier + { + {TFTPalette::MeshtasticGreen, TFTPalette::Black}, // HeaderBackground + {TFTPalette::MeshtasticGreen, TFTPalette::Black}, // HeaderTitle + {TFTPalette::MeshtasticGreen, TFTPalette::Black}, // HeaderStatus + {TFTPalette::MeshtasticGreen, TFTPalette::Black}, // SignalBars + {TFTPalette::MeshtasticGreen, TFTPalette::Black}, // ConnectionIcon + {TFTPalette::MeshtasticGreen, TFTPalette::Black}, // UtilizationFill + {TFTPalette::MeshtasticGreen, TFTPalette::Black}, // FavoriteNode + {TFTPalette::MeshtasticGreen, TFTPalette::Black}, // ActionMenuBorder + {TFTPalette::MeshtasticGreen, TFTPalette::Black}, // ActionMenuBody + {TFTPalette::MeshtasticGreen, TFTPalette::Black}, // ActionMenuTitle + {TFTPalette::Black, TFTPalette::MeshtasticGreen}, // FrameMono (bodyBg, bodyFg) + {TFTPalette::MeshtasticGreen, TFTPalette::Black}, // BootSplash + {TFTPalette::MeshtasticGreen, TFTPalette::Black}, // FavoriteNodeBGHighlight + {TFTPalette::MeshtasticGreen, TFTPalette::Black}, // NavigationBar + {TFTPalette::MeshtasticGreen, TFTPalette::Black}, // NavigationArrow + }, + TFTPalette::Black, // batteryFillGood + TFTPalette::Black, // batteryFillMedium + TFTPalette::Black, // batteryFillBad + true, // fullFrameInvert + true, // visible + }, + + // Classic Red (ThemeID::ClassicRed = 7) classic monochrome + { + ThemeID::ClassicRed, // id + "Classic Red", // name + 7, // uniqueIdentifier + { + {TFTPalette::ClassicRed, TFTPalette::Black}, // HeaderBackground + {TFTPalette::ClassicRed, TFTPalette::Black}, // HeaderTitle + {TFTPalette::ClassicRed, TFTPalette::Black}, // HeaderStatus + {TFTPalette::ClassicRed, TFTPalette::Black}, // SignalBars + {TFTPalette::ClassicRed, TFTPalette::Black}, // ConnectionIcon + {TFTPalette::ClassicRed, TFTPalette::Black}, // UtilizationFill + {TFTPalette::ClassicRed, TFTPalette::Black}, // FavoriteNode + {TFTPalette::ClassicRed, TFTPalette::Black}, // ActionMenuBorder + {TFTPalette::ClassicRed, TFTPalette::Black}, // ActionMenuBody + {TFTPalette::ClassicRed, TFTPalette::Black}, // ActionMenuTitle + {TFTPalette::Black, TFTPalette::ClassicRed}, // FrameMono (bodyBg, bodyFg) + {TFTPalette::ClassicRed, TFTPalette::Black}, // BootSplash + {TFTPalette::ClassicRed, TFTPalette::Black}, // FavoriteNodeBGHighlight + {TFTPalette::ClassicRed, TFTPalette::Black}, // NavigationBar + {TFTPalette::ClassicRed, TFTPalette::Black}, // NavigationArrow + }, + TFTPalette::Black, // batteryFillGood + TFTPalette::Black, // batteryFillMedium + TFTPalette::Black, // batteryFillBad + true, // fullFrameInvert + true, // visible + }, + + // Monochrome White (ThemeID::MonochromeWhite = 8) classic monochrome + { + ThemeID::MonochromeWhite, // id + "Monochrome White", // name + 8, // uniqueIdentifier + { + {TFTPalette::White, TFTPalette::Black}, // HeaderBackground + {TFTPalette::White, TFTPalette::Black}, // HeaderTitle + {TFTPalette::White, TFTPalette::Black}, // HeaderStatus + {TFTPalette::White, TFTPalette::Black}, // SignalBars + {TFTPalette::White, TFTPalette::Black}, // ConnectionIcon + {TFTPalette::White, TFTPalette::Black}, // UtilizationFill + {TFTPalette::White, TFTPalette::Black}, // FavoriteNode + {TFTPalette::White, TFTPalette::Black}, // ActionMenuBorder + {TFTPalette::White, TFTPalette::Black}, // ActionMenuBody + {TFTPalette::White, TFTPalette::Black}, // ActionMenuTitle + {TFTPalette::Black, TFTPalette::White}, // FrameMono (bodyBg, bodyFg) + {TFTPalette::White, TFTPalette::Black}, // BootSplash + {TFTPalette::White, TFTPalette::Black}, // FavoriteNodeBGHighlight + {TFTPalette::White, TFTPalette::Black}, // NavigationBar + {TFTPalette::White, TFTPalette::Black}, // NavigationArrow + }, + TFTPalette::Black, // batteryFillGood + TFTPalette::Black, // batteryFillMedium + TFTPalette::Black, // batteryFillBad + true, // fullFrameInvert + true, // visible + }, +}; + +static constexpr size_t kInternalThemeCount = sizeof(kThemes) / sizeof(kThemes[0]); + +// Resolve the kThemes[] index for the currently persisted theme. Called at +// boot (indirectly via getActiveTheme()) and whenever the active theme is +// queried, so uiconfig.screen_rgb_color remains the single source of truth. +// Matches against .uniqueIdentifier - that's the field whose value is stored +// in the user's config. Falls back to 0 (DefaultDark) if no match is found, +// which gracefully handles removed or retired themes. +static inline size_t resolveThemeIndex() +{ + const uint32_t savedIdentifier = uiconfig.screen_rgb_color & 0x1F; + for (size_t i = 0; i < kInternalThemeCount; i++) { + if (kThemes[i].uniqueIdentifier == savedIdentifier) + return i; + } + return 0; // Default Dark fallback +} + +static inline bool normalizeRegion(int16_t &x, int16_t &y, int16_t &width, int16_t &height) +{ + if (width <= 0 || height <= 0) { + return false; + } + + if (x < 0) { + width += x; + x = 0; + } + if (y < 0) { + height += y; + y = 0; + } + + return width > 0 && height > 0; +} + +static inline void appendColorRegion(int16_t x, int16_t y, int16_t width, int16_t height, uint16_t onColorBe, uint16_t offColorBe) +{ + // Keep the last slot permanently disabled as a sentinel for ST7789 scans. + // This leaves MAX_TFT_COLOR_REGIONS - 1 usable entries. + if (colorRegionCount >= MAX_TFT_COLOR_REGIONS - 1) { + memmove(&colorRegions[0], &colorRegions[1], sizeof(TFTColorRegion) * (MAX_TFT_COLOR_REGIONS - 2)); + colorRegionCount = MAX_TFT_COLOR_REGIONS - 2; + } + + TFTColorRegion ®ion = colorRegions[colorRegionCount++]; + region.x = x; + region.y = y; + region.width = width; + region.height = height; + region.onColorBe = onColorBe; + region.offColorBe = offColorBe; + region.enabled = true; + + // Keep one disabled sentinel after the active range for ST7789 countColorRegions(). + if (colorRegionCount < MAX_TFT_COLOR_REGIONS) { + colorRegions[colorRegionCount].enabled = false; + } + colorRegions[MAX_TFT_COLOR_REGIONS - 1].enabled = false; +} + +// Current working role colors (big-endian). Initialised to Dark defaults; +// call loadThemeDefaults() after boot / theme change to refresh. +static TFTRoleColorsBe roleColors[static_cast(TFTColorRole::Count)] = { + {toBe565(kHeaderBackground), toBe565(TFTPalette::Black)}, // HeaderBackground + {toBe565(kHeaderBackground), toBe565(kTitleColor)}, // HeaderTitle + {toBe565(kHeaderBackground), toBe565(kStatusColor)}, // HeaderStatus + {toBe565(TFTPalette::Good), toBe565(TFTPalette::Black)}, // SignalBars + {toBe565(TFTPalette::Blue), toBe565(TFTPalette::Black)}, // ConnectionIcon + {toBe565(TFTPalette::Good), toBe565(TFTPalette::Black)}, // UtilizationFill + {toBe565(TFTPalette::Yellow), toBe565(TFTPalette::Black)}, // FavoriteNode + {toBe565(TFTPalette::DarkGray), toBe565(TFTPalette::Black)}, // ActionMenuBorder + {toBe565(TFTPalette::White), toBe565(TFTPalette::Black)}, // ActionMenuBody + {toBe565(TFTPalette::DarkGray), toBe565(TFTPalette::White)}, // ActionMenuTitle + {toBe565(TFTPalette::Black), toBe565(TFTPalette::White)}, // FrameMono + {toBe565(TFTPalette::White), toBe565(TFTPalette::Black)}, // BootSplash + {toBe565(TFTPalette::Yellow), toBe565(TFTPalette::Black)}, // FavoriteNodeBGHighlight + {toBe565(kStatusColor), toBe565(kHeaderBackground)}, // NavigationBar + {toBe565(kTitleColor), toBe565(TFTPalette::Black)} // NavigationArrow +}; + +} // namespace + +// Theme accessors + +const TFTThemeDef &getActiveTheme() +{ + return kThemes[resolveThemeIndex()]; +} + +// Visible-theme accessors +// These iterate only themes flagged .visible = true, preserving kThemes[] +// order. Menu code should use these so hidden themes don't appear in the +// picker while still applying correctly if their ID is persisted. + +size_t getVisibleThemeCount() +{ + size_t count = 0; + for (size_t i = 0; i < kInternalThemeCount; i++) { + if (kThemes[i].visible) + count++; + } + return count; +} + +const TFTThemeDef &getVisibleThemeByIndex(size_t visibleIndex) +{ + size_t seen = 0; + for (size_t i = 0; i < kInternalThemeCount; i++) { + if (!kThemes[i].visible) + continue; + if (seen == visibleIndex) + return kThemes[i]; + seen++; + } + // Fallback: return first theme (never trust a bad index). + return kThemes[0]; +} + +size_t getActiveVisibleThemeIndex() +{ + const size_t active = resolveThemeIndex(); + if (!kThemes[active].visible) + return SIZE_MAX; + size_t visibleIdx = 0; + for (size_t i = 0; i < active; i++) { + if (kThemes[i].visible) + visibleIdx++; + } + return visibleIdx; +} + +uint16_t getThemeHeaderBg() +{ +#if GRAPHICS_TFT_COLORING_ENABLED +#ifdef TFT_HEADER_BG_COLOR_OVERRIDE + return TFT_HEADER_BG_COLOR_OVERRIDE; +#else + return kThemes[resolveThemeIndex()].roles[static_cast(TFTColorRole::HeaderBackground)].onColor; +#endif +#else + return TFTPalette::DarkGray; +#endif +} + +uint16_t getThemeHeaderText() +{ +#if GRAPHICS_TFT_COLORING_ENABLED +#ifdef TFT_HEADER_TITLE_COLOR_OVERRIDE + return TFT_HEADER_TITLE_COLOR_OVERRIDE; +#else + return kThemes[resolveThemeIndex()].roles[static_cast(TFTColorRole::HeaderTitle)].offColor; +#endif +#else + return TFTPalette::White; +#endif +} + +uint16_t getThemeHeaderStatus() +{ +#if GRAPHICS_TFT_COLORING_ENABLED +#ifdef TFT_HEADER_STATUS_COLOR_OVERRIDE + return TFT_HEADER_STATUS_COLOR_OVERRIDE; +#else + return kThemes[resolveThemeIndex()].roles[static_cast(TFTColorRole::HeaderStatus)].offColor; +#endif +#else + return TFTPalette::White; +#endif +} + +uint16_t getThemeBodyBg() +{ +#if GRAPHICS_TFT_COLORING_ENABLED + return kThemes[resolveThemeIndex()].roles[static_cast(TFTColorRole::FrameMono)].onColor; +#else + return TFTPalette::Black; +#endif +} + +uint16_t getThemeBodyFg() +{ +#if GRAPHICS_TFT_COLORING_ENABLED + return kThemes[resolveThemeIndex()].roles[static_cast(TFTColorRole::FrameMono)].offColor; +#else + return TFTPalette::White; +#endif +} + +bool isThemeFullFrameInvert() +{ +#if GRAPHICS_TFT_COLORING_ENABLED + return kThemes[resolveThemeIndex()].fullFrameInvert; +#else + return false; +#endif +} + +uint16_t getThemeBatteryFillColor(int batteryPercent) +{ + const TFTThemeDef &theme = kThemes[resolveThemeIndex()]; + if (batteryPercent <= 20) { + return theme.batteryFillBad; + } + if (batteryPercent <= 50) { + return theme.batteryFillMedium; + } + return theme.batteryFillGood; +} + +void loadThemeDefaults() +{ +#if GRAPHICS_TFT_COLORING_ENABLED + const TFTThemeDef &theme = kThemes[resolveThemeIndex()]; + for (uint8_t i = 0; i < static_cast(TFTColorRole::Count); i++) { + roleColors[i].onColorBe = toBe565(theme.roles[i].onColor); + roleColors[i].offColorBe = toBe565(theme.roles[i].offColor); + } +#endif +} + +// Role color assignment with theme-aware transforms + +void setTFTColorRole(TFTColorRole role, uint16_t onColor, uint16_t offColor) +{ +#if !GRAPHICS_TFT_COLORING_ENABLED + return; +#endif + + const uint8_t index = static_cast(role); + if (index >= static_cast(TFTColorRole::Count)) { + return; + } + + const uint32_t themeId = uiconfig.screen_rgb_color & 0x1F; + const bool isHighlightRole = (role == TFTColorRole::FavoriteNode || role == TFTColorRole::FavoriteNodeBGHighlight); + const bool isBodyRole = !isHighlightRole && isBodyColorRole(role); + + // Classic monochrome themes collapse all non-black accents into one tone. + if (isMonochromeTheme(themeId)) { + if (onColor != TFTPalette::Black) { + onColor = getMonochromeAccent(themeId); + } + } else { + switch (themeId) { + case ThemeID::DefaultLight: + if (isHighlightRole) { + // High-contrast highlight chips on light UI. + onColor = TFTPalette::Black; + offColor = TFTPalette::Yellow; + } else if (isBodyRole) { + // Invert body colors for readability on white frames. + if (offColor == TFTPalette::Black && role != TFTColorRole::ActionMenuTitle) { + offColor = TFTPalette::White; + } + replaceColor(onColor, TFTPalette::White, TFTPalette::Black); + } + break; + case ThemeID::Christmas: + if (isHighlightRole || isBodyRole) { + replaceColor(onColor, TFTPalette::Yellow, TFTPalette::Gold); + replaceColor(offColor, TFTPalette::Black, TFTPalette::Pine); + } + break; + case ThemeID::Pink: + if (isHighlightRole) { + onColor = TFTPalette::Black; + offColor = TFTPalette::HotPink; + } else if (isBodyRole) { + replaceColor(offColor, TFTPalette::Black, TFTPalette::PalePink); + replaceColor(onColor, TFTPalette::White, TFTPalette::Black); + replaceColor(onColor, TFTPalette::Yellow, TFTPalette::DeepPink); + } + break; + case ThemeID::Creamsicle: + if (isHighlightRole) { + onColor = TFTPalette::Black; + offColor = TFTPalette::CreamOrange; + } else if (isBodyRole) { + replaceColor(offColor, TFTPalette::Black, TFTPalette::Cream); + replaceColor(onColor, TFTPalette::White, TFTPalette::Black); + replaceColor(onColor, TFTPalette::Yellow, TFTPalette::DeepOrange); + } + break; + case ThemeID::Blue: + if (isHighlightRole || isBodyRole) { + replaceColor(onColor, TFTPalette::Yellow, TFTPalette::SkyBlue); + replaceColor(offColor, TFTPalette::Black, TFTPalette::Navy); + } + break; + default: + break; + } + } + + roleColors[index].onColorBe = toBe565(onColor); + roleColors[index].offColorBe = toBe565(offColor); +} + +// Region registration + +void registerTFTColorRegion(TFTColorRole role, int16_t x, int16_t y, int16_t width, int16_t height) +{ +#if !GRAPHICS_TFT_COLORING_ENABLED + return; +#endif + + const uint8_t roleIndex = static_cast(role); + if (roleIndex >= static_cast(TFTColorRole::Count)) { + return; + } + + if (!normalizeRegion(x, y, width, height)) { + return; + } + + const TFTRoleColorsBe &colors = roleColors[roleIndex]; + appendColorRegion(x, y, width, height, colors.onColorBe, colors.offColorBe); +} + +void setAndRegisterTFTColorRole(TFTColorRole role, uint16_t onColor, uint16_t offColor, int16_t x, int16_t y, int16_t width, + int16_t height) +{ +#if !GRAPHICS_TFT_COLORING_ENABLED + (void)role; + (void)onColor; + (void)offColor; + (void)x; + (void)y; + (void)width; + (void)height; + return; +#else + setTFTColorRole(role, onColor, offColor); + registerTFTColorRegion(role, x, y, width, height); +#endif +} + +void registerTFTColorRegionDirect(int16_t x, int16_t y, int16_t width, int16_t height, uint16_t onColor, uint16_t offColor) +{ +#if !GRAPHICS_TFT_COLORING_ENABLED + return; +#endif + + if (!normalizeRegion(x, y, width, height)) + return; + + appendColorRegion(x, y, width, height, toBe565(onColor), toBe565(offColor)); +} + +void registerTFTActionMenuRegions(int16_t boxLeft, int16_t boxTop, int16_t boxWidth, int16_t boxHeight) +{ +#if !GRAPHICS_TFT_COLORING_ENABLED + (void)boxLeft; + (void)boxTop; + (void)boxWidth; + (void)boxHeight; + return; +#else + // Use theme-appropriate menu colors. + const TFTThemeDef &theme = kThemes[resolveThemeIndex()]; + const TFTThemeRoleColor &menuBody = theme.roles[static_cast(TFTColorRole::ActionMenuBody)]; + const TFTThemeRoleColor &menuBorder = theme.roles[static_cast(TFTColorRole::ActionMenuBorder)]; + + // Fill role includes a 1px shadow guard so stale frame edges are overwritten uniformly. + setAndRegisterTFTColorRole(TFTColorRole::ActionMenuBody, menuBody.onColor, menuBody.offColor, boxLeft - 1, boxTop - 1, + boxWidth + 2, boxHeight + 2); + registerTFTColorRegion(TFTColorRole::ActionMenuBody, boxLeft, boxTop - 2, boxWidth, 1); + registerTFTColorRegion(TFTColorRole::ActionMenuBody, boxLeft, boxTop + boxHeight + 1, boxWidth, 1); + registerTFTColorRegion(TFTColorRole::ActionMenuBody, boxLeft - 2, boxTop, 1, boxHeight); + registerTFTColorRegion(TFTColorRole::ActionMenuBody, boxLeft + boxWidth + 1, boxTop, 1, boxHeight); + + setAndRegisterTFTColorRole(TFTColorRole::ActionMenuBorder, menuBorder.onColor, menuBorder.offColor, boxLeft, boxTop, boxWidth, + 1); + registerTFTColorRegion(TFTColorRole::ActionMenuBorder, boxLeft, boxTop + boxHeight - 1, boxWidth, 1); + registerTFTColorRegion(TFTColorRole::ActionMenuBorder, boxLeft, boxTop, 1, boxHeight); + registerTFTColorRegion(TFTColorRole::ActionMenuBorder, boxLeft + boxWidth - 1, boxTop, 1, boxHeight); +#endif +} + +// Frame signature & utilities + +uint32_t getTFTColorFrameSignature() +{ +#if !GRAPHICS_TFT_COLORING_ENABLED + return 0; +#else + uint32_t hash = kFnv1aOffsetBasis; + hash = fnv1aAppendByte(hash, colorRegionCount); + for (uint8_t i = 0; i < colorRegionCount; i++) { + const TFTColorRegion &r = colorRegions[i]; + hash = fnv1aAppendU16(hash, static_cast(r.x)); + hash = fnv1aAppendU16(hash, static_cast(r.y)); + hash = fnv1aAppendU16(hash, static_cast(r.width)); + hash = fnv1aAppendU16(hash, static_cast(r.height)); + hash = fnv1aAppendU16(hash, r.onColorBe); + hash = fnv1aAppendU16(hash, r.offColorBe); + } + + return hash; +#endif +} + +uint8_t getTFTColorRegionCount() +{ +#if !GRAPHICS_TFT_COLORING_ENABLED + return 0; +#else + return colorRegionCount; +#endif +} + +void clearTFTColorRegions() +{ + for (uint8_t i = 0; i < colorRegionCount; i++) { + colorRegions[i].enabled = false; + } + if (colorRegionCount < MAX_TFT_COLOR_REGIONS) { + colorRegions[colorRegionCount].enabled = false; + } + colorRegionCount = 0; +} + +uint16_t resolveTFTColorPixel(int16_t x, int16_t y, bool isset, uint16_t defaultOnColor, uint16_t defaultOffColor) +{ + for (int i = static_cast(colorRegionCount) - 1; i >= 0; i--) { + const TFTColorRegion &r = colorRegions[i]; + if (x >= r.x && x < r.x + r.width && y >= r.y && y < r.y + r.height) { + return isset ? r.onColorBe : r.offColorBe; + } + } + return isset ? defaultOnColor : defaultOffColor; +} + +uint16_t resolveTFTOffColorAt(int16_t x, int16_t y, uint16_t defaultOffColor) +{ +#if !GRAPHICS_TFT_COLORING_ENABLED + (void)x; + (void)y; + return defaultOffColor; +#else + const uint16_t defaultOffBe = toBe565(defaultOffColor); + const uint16_t sampledBe = resolveTFTColorPixel(x, y, false, defaultOffBe, defaultOffBe); + return static_cast((sampledBe >> 8) | (sampledBe << 8)); +#endif +} + +} // namespace graphics diff --git a/src/graphics/TFTColorRegions.h b/src/graphics/TFTColorRegions.h new file mode 100644 index 000000000..fd35bdb1a --- /dev/null +++ b/src/graphics/TFTColorRegions.h @@ -0,0 +1,163 @@ +#pragma once + +#include "configuration.h" +#include + +namespace graphics +{ + +struct TFTColorRegion { + int16_t x; + int16_t y; + int16_t width; + int16_t height; + uint16_t onColorBe; + uint16_t offColorBe; + // Required by ST7789 driver: it scans until the first disabled entry. + bool enabled = false; +}; + +static constexpr size_t MAX_TFT_COLOR_REGIONS = 48; +extern TFTColorRegion colorRegions[MAX_TFT_COLOR_REGIONS]; + +enum class TFTColorRole : uint8_t { + HeaderBackground = 0, + HeaderTitle, + HeaderStatus, + SignalBars, + ConnectionIcon, + UtilizationFill, + FavoriteNode, + ActionMenuBorder, + ActionMenuBody, + ActionMenuTitle, + FrameMono, + BootSplash, + FavoriteNodeBGHighlight, + NavigationBar, + NavigationArrow, + Count +}; + +#if HAS_TFT || defined(ST7701_CS) || defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || \ + defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) || \ + defined(USE_ST7796) || defined(HACKADAY_COMMUNICATOR) +#define GRAPHICS_TFT_COLORING_ENABLED 1 +#else +#define GRAPHICS_TFT_COLORING_ENABLED 0 +#endif + +static constexpr bool kTFTColoringEnabled = GRAPHICS_TFT_COLORING_ENABLED != 0; +constexpr bool isTFTColoringEnabled() +{ + return kTFTColoringEnabled; +} + +void setTFTColorRole(TFTColorRole role, uint16_t onColor, uint16_t offColor); +void registerTFTColorRegion(TFTColorRole role, int16_t x, int16_t y, int16_t width, int16_t height); +// Convenience helper for the common "set role then register one region" flow. +void setAndRegisterTFTColorRole(TFTColorRole role, uint16_t onColor, uint16_t offColor, int16_t x, int16_t y, int16_t width, + int16_t height); +// Register a region using explicit colors (no role lookup). Use when the +// color comes from a theme field rather than a role (e.g. battery fill). +void registerTFTColorRegionDirect(int16_t x, int16_t y, int16_t width, int16_t height, uint16_t onColor, uint16_t offColor); +void registerTFTActionMenuRegions(int16_t boxLeft, int16_t boxTop, int16_t boxWidth, int16_t boxHeight); +uint32_t getTFTColorFrameSignature(); +uint8_t getTFTColorRegionCount(); +void clearTFTColorRegions(); +uint16_t resolveTFTColorPixel(int16_t x, int16_t y, bool isset, uint16_t defaultOnColor, uint16_t defaultOffColor); +// Resolve effective region-mapped OFF color at a coordinate in native-endian RGB565. +uint16_t resolveTFTOffColorAt(int16_t x, int16_t y, uint16_t defaultOffColor); + +// -- Theme engine ------------------------------------------------------ +// Each theme has four fields that work together: +// +// id - ThemeID:: constant, used for in-code references. +// name - human-readable label shown in the theme picker. +// uniqueIdentifier - the stable numeric value persisted to +// uiconfig.screen_rgb_color and restored at boot. +// This is a CONTRACT with saved configs on disk - once +// assigned, never reuse or renumber, even if the theme is +// deleted or the kThemes[] array is reordered. +// visible - controls whether a theme appears in the picker menu. +// Hidden themes can still be restored and applied if their +// uniqueIdentifier is persisted. +// +// Display order in the menu is controlled by kThemes[] array order among +// themes where visible == true, NOT by any numeric value above. +// +// To add a new theme: +// 1. Add a unique constant in ThemeID below (next unused value). +// 2. Add a kThemes[] entry at the desired menu position, with a unique +// uniqueIdentifier that has never been used by any prior theme. +// 3. Set visible=true if it should appear in the picker. +// +// To retire a theme without breaking saved configs: +// - Preferred: keep the entry and set visible=false so existing saved +// uniqueIdentifier values still resolve to the same theme. +// - If you remove the entry, resolveThemeIndex() falls back to DefaultDark +// when the persisted uniqueIdentifier no longer matches any theme. +// - Do NOT reuse a retired uniqueIdentifier for a future theme. +namespace ThemeID +{ +constexpr uint32_t DefaultDark = 0; +constexpr uint32_t DefaultLight = 1; +constexpr uint32_t Christmas = 2; +constexpr uint32_t Pink = 3; +constexpr uint32_t Blue = 4; +constexpr uint32_t Creamsicle = 5; +constexpr uint32_t MeshtasticGreen = 6; +constexpr uint32_t ClassicRed = 7; +constexpr uint32_t MonochromeWhite = 8; +} // namespace ThemeID + +// Per-role color pair stored in native (little-endian) RGB565 format. +struct TFTThemeRoleColor { + uint16_t onColor; + uint16_t offColor; +}; + +// Complete theme definition. +struct TFTThemeDef { + uint32_t id; // ThemeID constant - in-code identifier for this theme. + const char *name; // Human-readable label shown in the theme picker. + uint32_t uniqueIdentifier; // Stable persisted value copied into uiconfig.screen_rgb_color. + // Never reuse or renumber - see file-level notes above. + TFTThemeRoleColor roles[static_cast(TFTColorRole::Count)]; + uint16_t batteryFillGood; + uint16_t batteryFillMedium; + uint16_t batteryFillBad; + bool fullFrameInvert; // Apply full-frame FrameMono inversion (ST7789 light themes) + bool visible; // Show in the theme picker menu. Hidden themes still apply + // correctly if their uniqueIdentifier is persisted (dev/legacy themes). +}; + +// Count of themes whose .visible flag is true. Use this when building menus. +size_t getVisibleThemeCount(); + +// Access the Nth visible theme (0 .. getVisibleThemeCount()-1). Hidden themes +// are skipped, preserving kThemes[] order among the visible entries. +const TFTThemeDef &getVisibleThemeByIndex(size_t visibleIndex); + +// Return the theme that matches uiconfig.screen_rgb_color (falls back to Dark). +const TFTThemeDef &getActiveTheme(); + +// Return the visible-theme index for the currently active theme, or SIZE_MAX +// if the active theme is hidden (so menus can show "no selection"). +size_t getActiveVisibleThemeIndex(); + +// Convenience accessors - safe to call even when coloring is compiled out. +uint16_t getThemeHeaderBg(); +uint16_t getThemeHeaderText(); +uint16_t getThemeHeaderStatus(); +uint16_t getThemeBodyBg(); +uint16_t getThemeBodyFg(); +bool isThemeFullFrameInvert(); +uint16_t getThemeBatteryFillColor(int batteryPercent); + +// Reinitialise default roleColors from the active theme. Call after a +// theme change so that any role registered without a prior setTFTColorRole() +// picks up theme-appropriate defaults. +void loadThemeDefaults(); + +} // namespace graphics diff --git a/src/graphics/TFTDisplay.cpp b/src/graphics/TFTDisplay.cpp index bc680dd1b..1b74fa16a 100644 --- a/src/graphics/TFTDisplay.cpp +++ b/src/graphics/TFTDisplay.cpp @@ -1220,7 +1220,9 @@ static LGFX *tft = nullptr; #endif #include "SPILock.h" +#include "TFTColorRegions.h" #include "TFTDisplay.h" +#include "TFTPalette.h" #include #ifdef UNPHONE @@ -1230,6 +1232,25 @@ extern unPhone unphone; GpioPin *TFTDisplay::backlightEnable = NULL; +namespace +{ +static constexpr uint8_t kFullRepaintChunkRows = 8; + +static inline uint16_t getThemeDefaultOnColor() +{ + return graphics::TFTPalette::White; +} + +static inline uint16_t getThemeDefaultOffColor() +{ +#if GRAPHICS_TFT_COLORING_ENABLED + return graphics::getThemeBodyBg(); +#else + return TFT_BLACK; +#endif +} +} // namespace + TFTDisplay::TFTDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY geometry, HW_I2C i2cBus) { LOG_DEBUG("TFTDisplay!"); @@ -1269,14 +1290,15 @@ TFTDisplay::~TFTDisplay() free(linePixelBuffer); linePixelBuffer = nullptr; } + if (repaintChunkBuffer != nullptr) { + free(repaintChunkBuffer); + repaintChunkBuffer = nullptr; + } } // Write the buffer to the display memory void TFTDisplay::display(bool fromBlank) { - if (fromBlank) - tft->fillScreen(TFT_BLACK); - concurrency::LockGuard g(spiLock); uint32_t x, y; @@ -1285,12 +1307,70 @@ void TFTDisplay::display(bool fromBlank) uint32_t x_FirstPixelUpdate; uint32_t x_LastPixelUpdate; bool isset, dblbuf_isset; - uint16_t colorTftMesh, colorTftBlack; + uint16_t colorTftWhite, colorTftBlack; bool somethingChanged = false; - // Store colors byte-reversed so that TFT_eSPI doesn't have to swap bytes in a separate step - colorTftMesh = __builtin_bswap16(TFT_MESH); - colorTftBlack = __builtin_bswap16(TFT_BLACK); + // Theme defaults for non-role pixels. + const uint16_t defaultOnColor = getThemeDefaultOnColor(); + const uint16_t defaultOffColor = getThemeDefaultOffColor(); + static uint16_t lastDefaultOnColor = 0; + static uint16_t lastDefaultOffColor = 0; + static bool haveLastDefaults = false; + const bool themeDefaultsChanged = + !haveLastDefaults || (defaultOnColor != lastDefaultOnColor) || (defaultOffColor != lastDefaultOffColor); + const bool forceFullRepaint = fromBlank || themeDefaultsChanged; + + // If theme defaults changed, reset panel background immediately so stale pixels don't linger. + if (forceFullRepaint) { + tft->fillScreen(defaultOffColor); + } + + colorTftWhite = (defaultOnColor >> 8) | ((defaultOnColor & 0xFF) << 8); + colorTftBlack = (defaultOffColor >> 8) | ((defaultOffColor & 0xFF) << 8); + +#if GRAPHICS_TFT_COLORING_ENABLED + static uint32_t lastColorFrameSignature = 0; + const bool hasColorRegions = graphics::getTFTColorRegionCount() > 0; + const uint32_t colorFrameSignature = graphics::getTFTColorFrameSignature(); + const bool forceFullColorRepaint = forceFullRepaint || (colorFrameSignature != lastColorFrameSignature); + + // When region roles/layout changed, color can differ even with identical monochrome glyph bits. + // Repaint full frame only for those frames, then return to diff-based updates. + if (forceFullColorRepaint) { + for (uint32_t yStart = 0; yStart < displayHeight; yStart += kFullRepaintChunkRows) { + const uint32_t rowsThisChunk = min(kFullRepaintChunkRows, displayHeight - yStart); + for (uint32_t row = 0; row < rowsThisChunk; row++) { + y = yStart + row; + y_byteIndex = (y / 8) * displayWidth; + y_byteMask = (1 << (y & 7)); + + uint16_t *chunkRow = repaintChunkBuffer + (row * displayWidth); + for (x = 0; x < displayWidth; x++) { + isset = (buffer[x + y_byteIndex] & y_byteMask) != 0; + if (hasColorRegions) { + chunkRow[x] = graphics::resolveTFTColorPixel(static_cast(x), static_cast(y), isset, + colorTftWhite, colorTftBlack); + } else { + chunkRow[x] = isset ? colorTftWhite : colorTftBlack; + } + } + } +#if defined(HACKADAY_COMMUNICATOR) + tft->draw16bitBeRGBBitmap(0, yStart, repaintChunkBuffer, displayWidth, rowsThisChunk); +#else + tft->pushImage(0, yStart, displayWidth, rowsThisChunk, repaintChunkBuffer); +#endif + } + + memcpy(buffer_back, buffer, displayBufferSize); + lastColorFrameSignature = colorFrameSignature; + haveLastDefaults = true; + lastDefaultOnColor = defaultOnColor; + lastDefaultOffColor = defaultOffColor; + graphics::clearTFTColorRegions(); + return; + } +#endif y = 0; while (y < displayHeight) { @@ -1299,7 +1379,7 @@ void TFTDisplay::display(bool fromBlank) // Step 1: Do a quick scan of 8 rows together. This allows fast-forwarding over unchanged screen areas. if (y_byteMask == 1) { - if (!fromBlank) { + if (!forceFullRepaint) { for (x = 0; x < displayWidth; x++) { if (buffer[x + y_byteIndex] != buffer_back[x + y_byteIndex]) break; @@ -1317,13 +1397,14 @@ void TFTDisplay::display(bool fromBlank) } } - // Step 2: Scan each of the 8 rows individually. Find the first pixel in each row that needs updating - for (x_FirstPixelUpdate = 0; x_FirstPixelUpdate < displayWidth; x_FirstPixelUpdate++) { - isset = buffer[x_FirstPixelUpdate + y_byteIndex] & y_byteMask; + // Step 2: Scan this row for changed span (first and last changed pixel). + uint32_t x_FirstChanged = 0; + for (x_FirstChanged = 0; x_FirstChanged < displayWidth; x_FirstChanged++) { + isset = buffer[x_FirstChanged + y_byteIndex] & y_byteMask; - if (!fromBlank) { + if (!forceFullRepaint) { // get src pixel in the page based ordering the OLED lib uses - dblbuf_isset = buffer_back[x_FirstPixelUpdate + y_byteIndex] & y_byteMask; + dblbuf_isset = buffer_back[x_FirstChanged + y_byteIndex] & y_byteMask; if (isset != dblbuf_isset) { break; } @@ -1333,37 +1414,45 @@ void TFTDisplay::display(bool fromBlank) } // Did we find a pixel that needs updating on this row? - if (x_FirstPixelUpdate < displayWidth) { - // Align the first pixel for update to an even number so the total alignment of - // the data will be at 32-bit boundary, which is required by GDMA SPI transfers. - x_FirstPixelUpdate &= ~1; - - // Step 3a: copy rest of the pixels in this row into the pixel line buffer, - // while also recording the last pixel in the row that needs updating. - // Since the first changed pixel will be looked up, the x_LastPixelUpdate will be set. - for (x = x_FirstPixelUpdate; x < displayWidth; x++) { - isset = buffer[x + y_byteIndex] & y_byteMask; - linePixelBuffer[x] = isset ? colorTftMesh : colorTftBlack; - - if (!fromBlank) { - dblbuf_isset = buffer_back[x + y_byteIndex] & y_byteMask; + if (x_FirstChanged < displayWidth) { + uint32_t x_LastChanged = displayWidth - 1; + while (x_LastChanged > x_FirstChanged) { + isset = buffer[x_LastChanged + y_byteIndex] & y_byteMask; + if (!forceFullRepaint) { + dblbuf_isset = buffer_back[x_LastChanged + y_byteIndex] & y_byteMask; if (isset != dblbuf_isset) { - x_LastPixelUpdate = x; + break; } } else if (isset) { - x_LastPixelUpdate = x; + break; } + x_LastChanged--; } - // Step 3b: Round up the last pixel to odd number to maintain 32-bit alignment for SPIs. - // Most displays will have even number of pixels in a row -- this will be in bounds - // of the displayWidth. (Hopefully odd displays will just ignore that extra pixel.) - x_LastPixelUpdate |= 1; - // Ensure the last pixel index does not exceed the display width. + + // Align the first pixel for update to an even number so the total alignment of + // the data will be at 32-bit boundary, which is required by GDMA SPI transfers. + x_FirstPixelUpdate = x_FirstChanged & ~1U; + x_LastPixelUpdate = x_LastChanged | 1U; if (x_LastPixelUpdate >= displayWidth) { x_LastPixelUpdate = displayWidth - 1; } int y_offset = 0; + // Step 3: Copy only the changed span into the pixel line buffer. + for (x = x_FirstPixelUpdate; x <= x_LastPixelUpdate; x++) { + isset = buffer[x + y_byteIndex] & y_byteMask; +#if GRAPHICS_TFT_COLORING_ENABLED + if (hasColorRegions) { + linePixelBuffer[x] = graphics::resolveTFTColorPixel(static_cast(x), static_cast(y), isset, + colorTftWhite, colorTftBlack); + } else { + linePixelBuffer[x] = isset ? colorTftWhite : colorTftBlack; + } +#else + linePixelBuffer[x] = isset ? colorTftWhite : colorTftBlack; +#endif + } + #if defined(CO5300_CS) uint8_t lines_updated = 2; if (y % 2 == 0) { @@ -1372,7 +1461,16 @@ void TFTDisplay::display(bool fromBlank) uint32_t bufferIndex = 1; for (x = x_FirstPixelUpdate; x < x_LastPixelUpdate; x++) { isset = buffer[x + y_byteIndex] & y_byteMask; - linePixelBuffer[bufferIndex++ + x_LastPixelUpdate] = isset ? colorTftMesh : colorTftBlack; +#if GRAPHICS_TFT_COLORING_ENABLED + if (hasColorRegions) { + linePixelBuffer[x] = graphics::resolveTFTColorPixel(static_cast(x), static_cast(y), + isset, colorTftWhite, colorTftBlack); + } else { + linePixelBuffer[x] = isset ? colorTftWhite : colorTftBlack; + } +#else + linePixelBuffer[x] = isset ? colorTftWhite : colorTftBlack; +#endif } } else { y_offset = -1; @@ -1383,7 +1481,16 @@ void TFTDisplay::display(bool fromBlank) uint32_t bufferIndex = 0; for (x = x_FirstPixelUpdate; x < x_LastPixelUpdate; x++) { isset = buffer[x + y_byteIndex] & y_byteMask; - linePixelBuffer[bufferIndex++ + x_FirstPixelUpdate] = isset ? colorTftMesh : colorTftBlack; +#if GRAPHICS_TFT_COLORING_ENABLED + if (hasColorRegions) { + linePixelBuffer[x] = graphics::resolveTFTColorPixel(static_cast(x), static_cast(y), + isset, colorTftWhite, colorTftBlack); + } else { + linePixelBuffer[x] = isset ? colorTftWhite : colorTftBlack; + } +#else + linePixelBuffer[x] = isset ? colorTftWhite : colorTftBlack; +#endif } } #else @@ -1396,8 +1503,8 @@ void TFTDisplay::display(bool fromBlank) #else // Step 4: Send the changed pixels on this line to the screen as a single block transfer. // This function accepts pixel data MSB first so it can dump the memory straight out the SPI port. - tft->pushRect(x_FirstPixelUpdate, y + y_offset, (x_LastPixelUpdate - x_FirstPixelUpdate + 1), lines_updated, - &linePixelBuffer[x_FirstPixelUpdate]); + tft->pushImage(x_FirstPixelUpdate, y + y_offset, (x_LastPixelUpdate - x_FirstPixelUpdate + 1), lines_updated, + &linePixelBuffer[x_FirstPixelUpdate]); #endif somethingChanged = true; } @@ -1406,6 +1513,14 @@ void TFTDisplay::display(bool fromBlank) // Copy the Buffer to the Back Buffer if (somethingChanged) memcpy(buffer_back, buffer, displayBufferSize); + +#if GRAPHICS_TFT_COLORING_ENABLED + lastColorFrameSignature = colorFrameSignature; +#endif + haveLastDefaults = true; + lastDefaultOnColor = defaultOnColor; + lastDefaultOffColor = defaultOffColor; + graphics::clearTFTColorRegions(); } void TFTDisplay::sdlLoop() @@ -1619,7 +1734,7 @@ bool TFTDisplay::connect() #else tft->setRotation(3); // Orient horizontal and wide underneath the silkscreen name label #endif - tft->fillScreen(TFT_BLACK); + tft->fillScreen(getThemeDefaultOffColor()); if (this->linePixelBuffer == NULL) { #if defined(CO5300_CS) @@ -1633,6 +1748,14 @@ bool TFTDisplay::connect() return false; } } + if (this->repaintChunkBuffer == NULL) { + this->repaintChunkBuffer = (uint16_t *)malloc(sizeof(uint16_t) * displayWidth * kFullRepaintChunkRows); + + if (!this->repaintChunkBuffer) { + LOG_ERROR("Not enough memory to create TFT repaint chunk buffer\n"); + return false; + } + } return true; } diff --git a/src/graphics/TFTDisplay.h b/src/graphics/TFTDisplay.h index a64922d23..2c86f05d2 100644 --- a/src/graphics/TFTDisplay.h +++ b/src/graphics/TFTDisplay.h @@ -63,4 +63,5 @@ class TFTDisplay : public OLEDDisplay virtual bool connect() override; uint16_t *linePixelBuffer = nullptr; -}; \ No newline at end of file + uint16_t *repaintChunkBuffer = nullptr; +}; diff --git a/src/graphics/TFTPalette.h b/src/graphics/TFTPalette.h new file mode 100644 index 000000000..516a9f057 --- /dev/null +++ b/src/graphics/TFTPalette.h @@ -0,0 +1,70 @@ +#pragma once + +#include + +namespace graphics +{ +namespace TFTPalette +{ + +constexpr uint16_t rgb565(uint8_t red, uint8_t green, uint8_t blue) +{ + return static_cast(((red & 0xF8) << 8) | ((green & 0xFC) << 3) | ((blue & 0xF8) >> 3)); +} + +constexpr uint16_t Black = 0x0000; +constexpr uint16_t White = 0xFFFF; +constexpr uint16_t DarkGray = 0x4208; +constexpr uint16_t Gray = 0x8410; +constexpr uint16_t LightGray = 0xC618; + +constexpr uint16_t Red = rgb565(255, 0, 0); +constexpr uint16_t Green = rgb565(0, 255, 0); +constexpr uint16_t Blue = rgb565(0, 130, 252); +constexpr uint16_t Yellow = rgb565(255, 255, 0); +constexpr uint16_t Orange = rgb565(255, 165, 0); +constexpr uint16_t Cyan = rgb565(0, 255, 255); +constexpr uint16_t Magenta = rgb565(255, 0, 255); + +constexpr uint16_t Good = Green; +constexpr uint16_t Medium = Yellow; +constexpr uint16_t Bad = Red; + +// Christmas / seasonal accent colors +constexpr uint16_t ChristmasRed = rgb565(178, 34, 34); +constexpr uint16_t ChristmasGreen = rgb565(0, 128, 0); +constexpr uint16_t Gold = rgb565(255, 215, 0); +constexpr uint16_t Pine = rgb565(15, 35, 10); + +// Pink theme colors (light variant) +constexpr uint16_t HotPink = rgb565(255, 105, 180); +constexpr uint16_t PalePink = rgb565(255, 228, 235); +constexpr uint16_t DeepPink = rgb565(200, 50, 120); + +// Blue theme colors (dark variant) +constexpr uint16_t SkyBlue = rgb565(100, 180, 255); +constexpr uint16_t Navy = rgb565(15, 15, 50); +constexpr uint16_t DeepBlue = rgb565(30, 60, 120); + +// Creamsicle theme colors (light variant) +constexpr uint16_t CreamOrange = rgb565(255, 140, 50); +constexpr uint16_t DeepOrange = rgb565(220, 100, 20); +constexpr uint16_t Cream = rgb565(255, 248, 235); + +// Classic monochrome theme accent colors (single-color-on-black themes) +constexpr uint16_t MeshtasticGreen = rgb565(0x67, 0xEA, 0x94); +constexpr uint16_t ClassicRed = rgb565(255, 64, 64); +// Monochrome White reuses TFTPalette::White above. + +// Fast contrast picker for monochrome glyph overlays on arbitrary RGB565 backgrounds. +// Uses channel-sum brightness approximation to keep code size small. +constexpr uint16_t pickReadableMonoFg(uint16_t backgroundColor) +{ + const uint16_t r = (backgroundColor >> 11) & 0x1F; + const uint16_t g = (backgroundColor >> 5) & 0x3F; + const uint16_t b = backgroundColor & 0x1F; + return ((r + g + b) >= 70) ? DarkGray : White; +} + +} // namespace TFTPalette +} // namespace graphics diff --git a/src/graphics/draw/ClockRenderer.cpp b/src/graphics/draw/ClockRenderer.cpp index 66bbe1bfe..5045174be 100644 --- a/src/graphics/draw/ClockRenderer.cpp +++ b/src/graphics/draw/ClockRenderer.cpp @@ -145,7 +145,7 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1 // === Set Title, Blank for Clock const char *titleStr = ""; // === Header === - graphics::drawCommonHeader(display, x, y, titleStr, true, true); + graphics::drawCommonHeader(display, x, y, titleStr, true, true, true); uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone char timeString[16]; @@ -293,11 +293,15 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1 // Draw an analog clock void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { +#if GRAPHICS_TFT_COLORING_ENABLED + // Clear previous frame pixels so moving hands don't leave stale artifacts on TFT light theme. + display->clear(); +#endif display->setTextAlignment(TEXT_ALIGN_LEFT); // === Set Title, Blank for Clock const char *titleStr = ""; // === Header === - graphics::drawCommonHeader(display, x, y, titleStr, true, true); + graphics::drawCommonHeader(display, x, y, titleStr, true, true, true); // clock face center coordinates int16_t centerX = display->getWidth() / 2; @@ -478,4 +482,4 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 } // namespace ClockRenderer } // namespace graphics -#endif \ No newline at end of file +#endif diff --git a/src/graphics/draw/CompassRenderer.cpp b/src/graphics/draw/CompassRenderer.cpp index fe54d68e7..1ee02194c 100644 --- a/src/graphics/draw/CompassRenderer.cpp +++ b/src/graphics/draw/CompassRenderer.cpp @@ -9,113 +9,61 @@ namespace graphics { namespace CompassRenderer { - -// Point helper class for compass calculations -struct Point { - float x, y; - Point(float x, float y) : x(x), y(y) {} - - void rotate(float angle) - { - float cos_a = cosf(angle); - float sin_a = sinf(angle); - float new_x = x * cos_a - y * sin_a; - float new_y = x * sin_a + y * cos_a; - x = new_x; - y = new_y; - } - - void scale(float factor) - { - x *= factor; - y *= factor; - } - - void translate(float dx, float dy) - { - x += dx; - y += dy; - } -}; - void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading, int16_t radius) { - // Show the compass heading (not implemented in original) - // This could draw a "N" indicator or north arrow - // For now, we'll draw a simple north indicator - // const float radius = 17.0f; if (currentResolution == ScreenResolution::High) { radius += 4; } - float northX = 0.0f; - float northY = -radius; - if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) { - const float c = cosf(-myHeading); - const float s = sinf(-myHeading); - const float rx = northX * c - northY * s; - const float ry = northX * s + northY * c; - northX = rx; - northY = ry; - } - northX += compassX; - northY += compassY; + + const float northAngle = (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) ? -myHeading : 0.0f; + const int16_t nX = compassX + static_cast((radius - 1) * sinf(northAngle)); + const int16_t nY = compassY - static_cast((radius - 1) * cosf(northAngle)); display->setFont(FONT_SMALL); display->setTextAlignment(TEXT_ALIGN_CENTER); +#if !GRAPHICS_TFT_COLORING_ENABLED display->setColor(BLACK); const int16_t nLabelWidth = display->getStringWidth("N"); if (currentResolution == ScreenResolution::High) { - display->fillRect(northX - 8, northY - 1, nLabelWidth + 3, FONT_HEIGHT_SMALL - 6); + display->fillRect(nX - 8, nY - 1, nLabelWidth + 3, FONT_HEIGHT_SMALL - 6); } else { - display->fillRect(northX - 4, northY - 1, nLabelWidth + 2, FONT_HEIGHT_SMALL - 6); + display->fillRect(nX - 4, nY - 1, nLabelWidth + 2, FONT_HEIGHT_SMALL - 6); } - display->setColor(WHITE); - display->drawString(northX, northY - 3, "N"); -} - -void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian) -{ - Point tip(0.0f, -0.5f), tail(0.0f, 0.35f); // pointing up initially - float arrowOffsetX = 0.14f, arrowOffsetY = 0.9f; - Point leftArrow(tip.x - arrowOffsetX, tip.y + arrowOffsetY), rightArrow(tip.x + arrowOffsetX, tip.y + arrowOffsetY); - - Point *arrowPoints[] = {&tip, &tail, &leftArrow, &rightArrow}; - - for (int i = 0; i < 4; i++) { - arrowPoints[i]->rotate(headingRadian); - arrowPoints[i]->scale(compassDiam * 0.6); - arrowPoints[i]->translate(compassX, compassY); - } - -#ifdef USE_EINK - display->drawTriangle(tip.x, tip.y, rightArrow.x, rightArrow.y, tail.x, tail.y); -#else - display->fillTriangle(tip.x, tip.y, rightArrow.x, rightArrow.y, tail.x, tail.y); #endif - display->drawTriangle(tip.x, tip.y, leftArrow.x, leftArrow.y, tail.x, tail.y); + display->setColor(WHITE); + display->drawString(nX, nY - 3, "N"); } void drawArrowToNode(OLEDDisplay *display, int16_t x, int16_t y, int16_t size, float bearing) { - float radians = bearing * DEG_TO_RAD; + const float radians = bearing * DEG_TO_RAD; + const float sinA = sinf(radians); + const float cosA = cosf(radians); + const float tipHalf = size * 0.5f; + const float lx = -(size / 6.0f); + const float ly = size / 4.0f; + const float rx = (size / 6.0f); + const float ry = size / 4.0f; + const float tx = 0.0f; + const float ty = size / 4.5f; - Point tip(0, -size / 2); - Point left(-size / 6, size / 4); - Point right(size / 6, size / 4); - Point tail(0, size / 4.5); + const int16_t tipX = static_cast(x + (tipHalf * sinA)); + const int16_t tipY = static_cast(y - (tipHalf * cosA)); + const int16_t leftX = static_cast(x + (lx * cosA) - (ly * sinA)); + const int16_t leftY = static_cast(y + (lx * sinA) + (ly * cosA)); + const int16_t rightX = static_cast(x + (rx * cosA) - (ry * sinA)); + const int16_t rightY = static_cast(y + (rx * sinA) + (ry * cosA)); + const int16_t tailX = static_cast(x + (tx * cosA) - (ty * sinA)); + const int16_t tailY = static_cast(y + (tx * sinA) + (ty * cosA)); - tip.rotate(radians); - left.rotate(radians); - right.rotate(radians); - tail.rotate(radians); + display->fillTriangle(tipX, tipY, leftX, leftY, tailX, tailY); + display->fillTriangle(tipX, tipY, rightX, rightY, tailX, tailY); +} - tip.translate(x, y); - left.translate(x, y); - right.translate(x, y); - tail.translate(x, y); - - display->fillTriangle(tip.x, tip.y, left.x, left.y, tail.x, tail.y); - display->fillTriangle(tip.x, tip.y, right.x, right.y, tail.x, tail.y); +void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian) +{ + const int16_t size = static_cast(compassDiam * 0.6f); + drawArrowToNode(display, compassX, compassY, size, headingRadian * RAD_TO_DEG); } bool getHeadingRadians(double lat, double lon, float &headingRadian) diff --git a/src/graphics/draw/CompassRenderer.h b/src/graphics/draw/CompassRenderer.h index d77623847..41adf6e64 100644 --- a/src/graphics/draw/CompassRenderer.h +++ b/src/graphics/draw/CompassRenderer.h @@ -1,6 +1,7 @@ #pragma once #include "graphics/Screen.h" +#include "mesh/generated/meshtastic/mesh.pb.h" #include #include diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index 98e907255..982049b4b 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -11,6 +11,8 @@ #include "gps/RTC.h" #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" +#include "graphics/TFTColorRegions.h" +#include "graphics/TFTPalette.h" #include "graphics/TimeFormatters.h" #include "graphics/images.h" #include "main.h" @@ -469,9 +471,11 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int chUtil_y = getTextPositions(display)[line] + 3; int chutil_bar_width = (currentResolution == ScreenResolution::High) ? 100 : 50; + int chutil_bar_max_fill = chutil_bar_width - 2; // Account for border int chutil_bar_height = (currentResolution == ScreenResolution::High) ? 12 : 7; int extraoffset = (currentResolution == ScreenResolution::High) ? 6 : 3; int chutil_percent = airTime->channelUtilizationPercent(); + const int raw_chutil_percent = chutil_percent; int centerofscreen = SCREEN_WIDTH / 2; int total_line_content_width = (chUtil_x + chutil_bar_width + display->getStringWidth(chUtilPercentage) + extraoffset) / 2; @@ -479,7 +483,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, display->drawString(starting_position, getTextPositions(display)[line], chUtil); - // Force 56% or higher to show a full 100% bar, text would still show related percent. + // Force 61% or higher to show a full 100% bar, text would still show related percent. if (chutil_percent >= 61) { chutil_percent = 100; } @@ -492,9 +496,9 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, float weight3 = 0.20; // Weight for 40–100% float totalWeight = weight1 + weight2 + weight3; - int seg1 = chutil_bar_width * (weight1 / totalWeight); - int seg2 = chutil_bar_width * (weight2 / totalWeight); - int seg3 = chutil_bar_width * (weight3 / totalWeight); + int seg1 = chutil_bar_max_fill * (weight1 / totalWeight); + int seg2 = chutil_bar_max_fill * (weight2 / totalWeight); + int seg3 = chutil_bar_max_fill - seg1 - seg2; // Remainder absorbs rounding errors int fillRight = 0; @@ -511,7 +515,17 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, // Fill progress if (fillRight > 0) { - display->fillRect(starting_position + chUtil_x, chUtil_y, fillRight, chutil_bar_height); +#if GRAPHICS_TFT_COLORING_ENABLED + uint16_t UtilizationFillColor = TFTPalette::Good; + if (raw_chutil_percent >= 60) { + UtilizationFillColor = TFTPalette::Bad; + } else if (raw_chutil_percent >= 35) { + UtilizationFillColor = TFTPalette::Medium; + } + setAndRegisterTFTColorRole(TFTColorRole::UtilizationFill, UtilizationFillColor, TFTPalette::Black, + starting_position + chUtil_x + 1, chUtil_y + 1, fillRight, chutil_bar_height - 2); +#endif + display->fillRect(starting_position + chUtil_x + 1, chUtil_y + 1, fillRight, chutil_bar_height - 2); } display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[line++], @@ -584,6 +598,17 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x display->setColor(WHITE); display->drawRect(barX, barY, adjustedBarWidth, barHeight); +#if GRAPHICS_TFT_COLORING_ENABLED + uint16_t UtilizationFillColor = TFTPalette::Good; + if (percent >= 80) { + UtilizationFillColor = TFTPalette::Bad; + } else if (percent >= 60) { + UtilizationFillColor = TFTPalette::Medium; + } + setAndRegisterTFTColorRole(TFTColorRole::UtilizationFill, UtilizationFillColor, TFTPalette::Black, barX + 1, barY + 1, + fillWidth - 1, barHeight - 2); +#endif + display->fillRect(barX, barY, fillWidth, barHeight); display->setColor(WHITE); #endif diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index e92ba4839..f31cb405b 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -11,6 +11,7 @@ #include "buzz.h" #include "graphics/Screen.h" #include "graphics/SharedUIDisplay.h" +#include "graphics/TFTColorRegions.h" #include "graphics/draw/MessageRenderer.h" #include "graphics/draw/UIRenderer.h" #include "input/RotaryEncoderInterruptImpl1.h" @@ -30,8 +31,6 @@ #include #include -extern uint16_t TFT_MESH; - namespace graphics { @@ -2028,109 +2027,6 @@ void menuHandler::switchToMUIMenu() screen->showOverlayBanner(bannerOptions); } -void menuHandler::TFTColorPickerMenu(OLEDDisplay *display) -{ - static const ScreenColorOption colorOptions[] = { - {"Back", OptionsAction::Back}, - {"Default", OptionsAction::Select, ScreenColor(0, 0, 0, true)}, - {"Meshtastic Green", OptionsAction::Select, ScreenColor(0x67, 0xEA, 0x94)}, - {"Yellow", OptionsAction::Select, ScreenColor(255, 255, 128)}, - {"Red", OptionsAction::Select, ScreenColor(255, 64, 64)}, - {"Orange", OptionsAction::Select, ScreenColor(255, 160, 20)}, - {"Purple", OptionsAction::Select, ScreenColor(204, 153, 255)}, - {"Blue", OptionsAction::Select, ScreenColor(0, 0, 255)}, - {"Teal", OptionsAction::Select, ScreenColor(16, 102, 102)}, - {"Cyan", OptionsAction::Select, ScreenColor(0, 255, 255)}, - {"Ice", OptionsAction::Select, ScreenColor(173, 216, 230)}, - {"Pink", OptionsAction::Select, ScreenColor(255, 105, 180)}, - {"White", OptionsAction::Select, ScreenColor(255, 255, 255)}, - {"Gray", OptionsAction::Select, ScreenColor(128, 128, 128)}, - }; - - constexpr size_t colorCount = sizeof(colorOptions) / sizeof(colorOptions[0]); - static std::array colorLabels{}; - - auto bannerOptions = createStaticBannerOptions( - "Select Screen Color", colorOptions, colorLabels, [display](const ScreenColorOption &option, int) -> void { - if (option.action == OptionsAction::Back) { - menuQueue = SystemBaseMenu; - screen->runNow(); - return; - } - - if (!option.hasValue) { - return; - } - -#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || \ - HAS_TFT || defined(HACKADAY_COMMUNICATOR) - const ScreenColor &color = option.value; - if (color.useVariant) { - LOG_INFO("Setting color to system default or defined variant"); - } else { - LOG_INFO("Setting color to %s", option.label); - } - - uint8_t r = color.r; - uint8_t g = color.g; - uint8_t b = color.b; - - display->setColor(BLACK); - display->fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); - display->setColor(WHITE); - - if (color.useVariant || (r == 0 && g == 0 && b == 0)) { -#ifdef TFT_MESH_OVERRIDE - TFT_MESH = TFT_MESH_OVERRIDE; -#else - TFT_MESH = COLOR565(255, 255, 128); -#endif - } else { - TFT_MESH = COLOR565(r, g, b); - } - -#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) - static_cast(screen->getDisplayDevice())->setRGB(TFT_MESH); -#endif - - screen->setFrames(graphics::Screen::FOCUS_SYSTEM); - if (color.useVariant || (r == 0 && g == 0 && b == 0)) { - uiconfig.screen_rgb_color = 0; - } else { - uiconfig.screen_rgb_color = - (static_cast(r) << 16) | (static_cast(g) << 8) | static_cast(b); - } - LOG_INFO("Storing Value of %d to uiconfig.screen_rgb_color", uiconfig.screen_rgb_color); - saveUIConfig(); -#endif - }); - - int initialSelection = 0; - if (uiconfig.screen_rgb_color == 0) { - initialSelection = 1; - } else { - uint32_t currentColor = uiconfig.screen_rgb_color; - for (size_t i = 0; i < colorCount; ++i) { - if (!colorOptions[i].hasValue) { - continue; - } - const ScreenColor &color = colorOptions[i].value; - if (color.useVariant) { - continue; - } - uint32_t encoded = - (static_cast(color.r) << 16) | (static_cast(color.g) << 8) | static_cast(color.b); - if (encoded == currentColor) { - initialSelection = static_cast(i); - break; - } - } - } - bannerOptions.InitialSelected = initialSelection; - - screen->showOverlayBanner(bannerOptions); -} - void menuHandler::rebootMenu() { static const char *optionsArray[] = {"Back", "Confirm"}; @@ -2318,9 +2214,9 @@ void menuHandler::screenOptionsMenu() bool hasSupportBrightness = false; #endif - enum optionsNumbers { Back, Brightness, ScreenColor, FrameToggles, DisplayUnits, MessageBubbles }; - static const char *optionsArray[6] = {"Back"}; - static int optionsEnumArray[6] = {Back}; + enum optionsNumbers { Back, Brightness, FrameToggles, DisplayUnits, MessageBubbles, Theme }; + static const char *optionsArray[7] = {"Back"}; + static int optionsEnumArray[7] = {Back}; int options = 1; // Only show brightness for B&W displays @@ -2329,13 +2225,6 @@ void menuHandler::screenOptionsMenu() optionsEnumArray[options++] = Brightness; } - // Only show screen color for TFT displays -#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || \ - HAS_TFT || defined(HACKADAY_COMMUNICATOR) - optionsArray[options] = "Screen Color"; - optionsEnumArray[options++] = ScreenColor; -#endif - optionsArray[options] = "Frame Visibility"; optionsEnumArray[options++] = FrameToggles; @@ -2345,6 +2234,11 @@ void menuHandler::screenOptionsMenu() optionsArray[options] = "Message Bubbles"; optionsEnumArray[options++] = MessageBubbles; +#if GRAPHICS_TFT_COLORING_ENABLED + optionsArray[options] = "Theme"; + optionsEnumArray[options++] = Theme; +#endif + BannerOverlayOptions bannerOptions; bannerOptions.message = "Display Options"; bannerOptions.optionsArrayPtr = optionsArray; @@ -2354,9 +2248,6 @@ void menuHandler::screenOptionsMenu() if (selected == Brightness) { menuHandler::menuQueue = menuHandler::BrightnessPicker; screen->runNow(); - } else if (selected == ScreenColor) { - menuHandler::menuQueue = menuHandler::TftColorMenuPicker; - screen->runNow(); } else if (selected == FrameToggles) { menuHandler::menuQueue = menuHandler::FrameToggles; screen->runNow(); @@ -2366,6 +2257,9 @@ void menuHandler::screenOptionsMenu() } else if (selected == MessageBubbles) { menuHandler::menuQueue = menuHandler::MessageBubblesMenu; screen->runNow(); + } else if (selected == Theme) { + menuHandler::menuQueue = menuHandler::ThemeMenu; + screen->runNow(); } else { menuQueue = SystemBaseMenu; screen->runNow(); @@ -2649,6 +2543,53 @@ void menuHandler::messageBubblesMenu() screen->showOverlayBanner(bannerOptions); } +void menuHandler::themeMenu() +{ + // Build menu dynamically from the theme table. + // Only visible themes appear! + // Slot budget: 1 for "Back" + up to kMaxThemesInMenu visible themes. + // Bump kMaxThemesInMenu if you add more themes than will fit here. + constexpr size_t kMaxThemesInMenu = 15; + const size_t visibleCount = getVisibleThemeCount(); + static const char *optionsArray[kMaxThemesInMenu + 1] = {"Back"}; + const size_t shownCount = (visibleCount < kMaxThemesInMenu) ? visibleCount : kMaxThemesInMenu; + const int options = static_cast(shownCount) + 1; // +1 for Back + + for (size_t i = 0; i < shownCount; i++) { + optionsArray[i + 1] = getVisibleThemeByIndex(i).name; + } + + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Theme"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = options; + + // Highlight the currently active theme (visible index + 1 for the Back + // offset). If the active theme is hidden, leave selection on "Back". + const size_t activeVisible = getActiveVisibleThemeIndex(); + bannerOptions.InitialSelected = (activeVisible == SIZE_MAX) ? 0 : static_cast(activeVisible) + 1; + + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 0) { + // Back + menuHandler::menuQueue = menuHandler::ScreenOptionsMenu; + screen->runNow(); + } else { + // Selection is an index into the VISIBLE themes (1-based, slot 0 is Back). + const size_t visibleIdx = static_cast(selected - 1); + if (visibleIdx < getVisibleThemeCount()) { + // Persist the theme's uniqueIdentifier so boot-time + // resolveThemeIndex() can restore this theme on next startup. + uiconfig.screen_rgb_color = COLOR565(255, 255, (getVisibleThemeByIndex(visibleIdx).uniqueIdentifier & 0x1F) << 3); + loadThemeDefaults(); + saveUIConfig(); + screen->runNow(); + } + } + }; + screen->showOverlayBanner(bannerOptions); +} + void menuHandler::handleMenuSwitch(OLEDDisplay *display) { if (menuQueue != MenuNone) @@ -2724,9 +2665,6 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display) case MuiPicker: switchToMUIMenu(); break; - case TftColorMenuPicker: - TFTColorPickerMenu(display); - break; case BrightnessPicker: BrightnessPickerMenu(); break; @@ -2799,6 +2737,9 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display) case MessageBubblesMenu: messageBubblesMenu(); break; + case ThemeMenu: + themeMenu(); + break; } menuQueue = MenuNone; } @@ -2810,4 +2751,4 @@ void menuHandler::saveUIConfig() } // namespace graphics -#endif \ No newline at end of file +#endif diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h index 4a0360412..3ac9e606e 100644 --- a/src/graphics/draw/MenuHandler.h +++ b/src/graphics/draw/MenuHandler.h @@ -30,7 +30,6 @@ class menuHandler ResetNodeDbMenu, BuzzerModeMenuPicker, MuiPicker, - TftColorMenuPicker, BrightnessPicker, RebootMenu, ShutdownMenu, @@ -55,7 +54,8 @@ class menuHandler NodeNameLengthMenu, FrameToggles, DisplayUnits, - MessageBubblesMenu + MessageBubblesMenu, + ThemeMenu }; static screenMenus menuQueue; static uint32_t pickedNodeNum; // node selected by NodePicker for ManageNodeMenu @@ -89,7 +89,6 @@ class menuHandler static void GPSPositionBroadcastMenu(); static void BuzzerModeMenu(); static void switchToMUIMenu(); - static void TFTColorPickerMenu(OLEDDisplay *display); static void nodeListMenu(); static void resetNodeDBMenu(); static void BrightnessPickerMenu(); @@ -110,6 +109,7 @@ class menuHandler static void frameTogglesMenu(); static void displayUnitsMenu(); static void messageBubblesMenu(); + static void themeMenu(); static void textMessageMenu(); private: @@ -136,23 +136,10 @@ template struct MenuOption { MenuOption(const char *labelIn, OptionsAction actionIn) : label(labelIn), action(actionIn), hasValue(false), value() {} }; -struct ScreenColor { - uint8_t r; - uint8_t g; - uint8_t b; - bool useVariant; - - explicit ScreenColor(uint8_t rIn = 0, uint8_t gIn = 0, uint8_t bIn = 0, bool variantIn = false) - : r(rIn), g(gIn), b(bIn), useVariant(variantIn) - { - } -}; - using RadioPresetOption = MenuOption; using LoraRegionOption = MenuOption; using TimezoneOption = MenuOption; using CompassOption = MenuOption; -using ScreenColorOption = MenuOption; using GPSToggleOption = MenuOption; using GPSFormatOption = MenuOption; using NodeNameOption = MenuOption; diff --git a/src/graphics/draw/MessageRenderer.cpp b/src/graphics/draw/MessageRenderer.cpp index 2fd9bf541..2260c57df 100644 --- a/src/graphics/draw/MessageRenderer.cpp +++ b/src/graphics/draw/MessageRenderer.cpp @@ -11,6 +11,8 @@ #include "graphics/Screen.h" #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" +#include "graphics/TFTColorRegions.h" +#include "graphics/TFTPalette.h" #include "graphics/TimeFormatters.h" #include "graphics/emotes.h" #include "main.h" @@ -254,6 +256,76 @@ struct MessageBlock { bool mine; }; +#if GRAPHICS_TFT_COLORING_ENABLED +static void setDarkModeBubbleRoleColors(uint32_t themeId, bool mine) +{ + uint16_t bubbleOnColor; + uint16_t bubbleOffColor; + + if (themeId == ThemeID::Blue) { + bubbleOnColor = mine ? TFTPalette::Navy : TFTPalette::White; + bubbleOffColor = mine ? TFTPalette::SkyBlue : TFTPalette::DeepBlue; + } else { + bubbleOnColor = mine ? TFTPalette::Black : getThemeBodyFg(); + bubbleOffColor = mine ? TFTPalette::SkyBlue : TFTPalette::DarkGray; + } + + setTFTColorRole(TFTColorRole::ActionMenuBody, bubbleOnColor, bubbleOffColor); +} + +static void registerRoundedBubbleFillRegion(int x, int y, int w, int h, int radius) +{ + if (w <= 0 || h <= 0) { + return; + } + + if (radius <= 0 || w < 3 || h < 3) { + registerTFTColorRegion(TFTColorRole::ActionMenuBody, x, y, w, h); + return; + } + + // Keep region count low so we don't churn MAX_TFT_COLOR_REGIONS while + // scrolling long message lists (which can flatten older bubble corners). + int capRows = 0; + if (radius >= 4 && h >= 5) { + capRows = 2; // 5 regions total (2 top caps + middle + 2 bottom caps) + } else if (radius >= 2 && h >= 3) { + capRows = 1; // 3 regions total + } + if (capRows <= 0) { + registerTFTColorRegion(TFTColorRole::ActionMenuBody, x, y, w, h); + return; + } + + for (int row = 0; row < capRows; ++row) { + int inset = 0; + if (radius >= 4) { + inset = (row == 0) ? 2 : 1; + } else if (radius >= 2) { + inset = 1; + } + const int stripW = w - (inset * 2); + if (stripW <= 0) { + continue; + } + + const int topY = y + row; + registerTFTColorRegion(TFTColorRole::ActionMenuBody, x + inset, topY, stripW, 1); + + const int bottomY = y + h - 1 - row; + if (bottomY != topY) { + registerTFTColorRegion(TFTColorRole::ActionMenuBody, x + inset, bottomY, stripW, 1); + } + } + + const int middleY = y + capRows; + const int middleH = h - (capRows * 2); + if (middleH > 0) { + registerTFTColorRegion(TFTColorRole::ActionMenuBody, x, middleY, w, middleH); + } +} +#endif + static int getDrawnLinePixelBottom(int lineTopY, const std::string &line, bool isHeaderLine) { if (isHeaderLine) { @@ -648,6 +720,11 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 const int contentBottom = scrollBottom; // already excludes nav line const int rightEdge = SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN; const int bubbleGapY = std::max(1, MESSAGE_BLOCK_GAP / 2); +#if GRAPHICS_TFT_COLORING_ENABLED + const uint32_t themeId = getActiveTheme().id; + // Blue is a dark variant but uses full frame inversion, Keep it on the same filled bubble style as Default Dark. + const bool useDarkModeBubbleFill = showBubbles && (!isThemeFullFrameInvert() || themeId == ThemeID::Blue); +#endif std::vector lineTop; lineTop.resize(cachedLines.size()); @@ -686,6 +763,17 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 int visualBottom = getDrawnLinePixelBottom(lineTop[b.end], cachedLines[b.end], isHeader[b.end]); int bottomY = visualBottom + BUBBLE_PAD_Y; + // On high-res screens, keep a 1px gap under the header + if (currentResolution == ScreenResolution::High) { + const int minTopY = contentTop + 1; + if (topY < minTopY) { + // Preserve bubble height when we push it down from the header. + const int shift = minTopY - topY; + topY = minTopY; + bottomY += shift; + } + } + if (bi + 1 < blocks.size()) { int nextHeaderIndex = (int)blocks[bi + 1].start; int nextTop = lineTop[nextHeaderIndex]; @@ -735,24 +823,56 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 const int by = topY; const int bw = bubbleW; const int bh = bubbleH; +#if GRAPHICS_TFT_COLORING_ENABLED + const bool drawBubbleOutline = !useDarkModeBubbleFill; +#else + const bool drawBubbleOutline = true; +#endif +#if GRAPHICS_TFT_COLORING_ENABLED + if (useDarkModeBubbleFill) { + setDarkModeBubbleRoleColors(themeId, b.mine); + registerRoundedBubbleFillRegion(bx, by, bw, bh, r); + } +#endif - // Draw the 4 corner arcs using drawCircleQuads - display->drawCircleQuads(bx + r, by + r, r, 0x2); // Top-left - display->drawCircleQuads(bx + bw - r - 1, by + r, r, 0x1); // Top-right - display->drawCircleQuads(bx + r, by + bh - r - 1, r, 0x4); // Bottom-left - display->drawCircleQuads(bx + bw - r - 1, by + bh - r - 1, r, 0x8); // Bottom-right + if (drawBubbleOutline) { + // Draw the 4 corner arcs using drawCircleQuads + display->drawCircleQuads(bx + r, by + r, r, 0x2); // Top-left + display->drawCircleQuads(bx + bw - r - 1, by + r, r, 0x1); // Top-right + display->drawCircleQuads(bx + r, by + bh - r - 1, r, 0x4); // Bottom-left + display->drawCircleQuads(bx + bw - r - 1, by + bh - r - 1, r, 0x8); // Bottom-right - // Draw the 4 edges between corners - display->drawHorizontalLine(bx + r, by, bw - 2 * r); // Top edge - display->drawHorizontalLine(bx + r, by + bh - 1, bw - 2 * r); // Bottom edge - display->drawVerticalLine(bx, by + r, bh - 2 * r); // Left edge - display->drawVerticalLine(bx + bw - 1, by + r, bh - 2 * r); // Right edge + // Draw the 4 edges between corners + display->drawHorizontalLine(bx + r, by, bw - 2 * r); // Top edge + display->drawHorizontalLine(bx + r, by + bh - 1, bw - 2 * r); // Bottom edge + display->drawVerticalLine(bx, by + r, bh - 2 * r); // Left edge + display->drawVerticalLine(bx + bw - 1, by + r, bh - 2 * r); // Right edge + } } else if (bubbleW > 1 && bubbleH > 1) { // Fallback to simple rectangle for very small bubbles - display->drawRect(bubbleX, topY, bubbleW, bubbleH); +#if GRAPHICS_TFT_COLORING_ENABLED + const bool drawBubbleOutline = !useDarkModeBubbleFill; +#else + const bool drawBubbleOutline = true; +#endif +#if GRAPHICS_TFT_COLORING_ENABLED + if (useDarkModeBubbleFill) { + setDarkModeBubbleRoleColors(themeId, b.mine); + registerTFTColorRegion(TFTColorRole::ActionMenuBody, bubbleX, topY, bubbleW, bubbleH); + } +#endif + if (drawBubbleOutline) { + display->drawRect(bubbleX, topY, bubbleW, bubbleH); + } } } } // end if (showBubbles) +#if GRAPHICS_TFT_COLORING_ENABLED + if (useDarkModeBubbleFill) { + // Restore theme role defaults so other screens keep their intended palette. + loadThemeDefaults(); + } +#endif // Render visible lines int lineY = yOffset; @@ -772,7 +892,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 headerX = x + textIndent; } graphics::UIRenderer::drawStringWithEmotes(display, headerX, lineY, cachedLines[i].c_str(), FONT_HEIGHT_SMALL, 1, - false); + true); // Draw underline just under header text int underlineY = lineY + FONT_HEIGHT_SMALL; diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp index e0c5df124..00ae74b58 100644 --- a/src/graphics/draw/NodeListRenderer.cpp +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -11,6 +11,8 @@ #include "gps/RTC.h" // for getTime() function #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" +#include "graphics/TFTColorRegions.h" +#include "graphics/TFTPalette.h" #include "graphics/images.h" #include "meshUtils.h" #include @@ -213,6 +215,33 @@ void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries, } } +static inline void applyFavoriteNodeNameColor(OLEDDisplay *display, const meshtastic_NodeInfoLite *node, const char *nodeName, + int16_t nameX, int16_t y, int nameMaxWidth) +{ + if (!display || !node || !node->is_favorite || !isTFTColoringEnabled() || !nodeName) { + return; + } + + const int textWidth = UIRenderer::measureStringWithEmotes(display, nodeName); + const int regionWidth = min(textWidth, max(0, nameMaxWidth)); + if (regionWidth <= 0) { + return; + } + + // Node list rows can begin a couple of pixels inside header space. + // Clamp favorite-name color region below the header to avoid black overlap there. + const int16_t minContentY = static_cast(FONT_HEIGHT_SMALL + 1); + const int16_t regionY = max(y, minContentY); + const int16_t yClip = regionY - y; + const int16_t regionHeight = static_cast(FONT_HEIGHT_SMALL - yClip); + if (regionHeight <= 0) { + return; + } + + setAndRegisterTFTColorRole(TFTColorRole::FavoriteNode, TFTPalette::Yellow, TFTPalette::Black, nameX, regionY, regionWidth, + regionHeight); +} + // ============================= // Entry Renderers // ============================= @@ -227,6 +256,9 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int char nodeName[96]; UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName), nameMaxWidth); +#if GRAPHICS_TFT_COLORING_ENABLED + applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth); +#endif bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; char timeStr[10]; @@ -275,14 +307,20 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int int nameMaxWidth = getNodeNameMaxWidth(columnWidth, columnWidth - 25); int barsOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19); - int hopOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 21 : 29) : (isLeftCol ? 13 : 17); + constexpr int kBarCount = 4; + constexpr int kBarWidth = 2; + constexpr int kBarGap = 1; int barsXOffset = columnWidth - barsOffset; + int barsRightEdge = x + barsXOffset + ((kBarCount - 1) * (kBarWidth + kBarGap)) + kBarWidth; const int nameX = x + ((currentResolution == ScreenResolution::High) ? 6 : 3); char nodeName[96]; UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName), nameMaxWidth); +#if GRAPHICS_TFT_COLORING_ENABLED + applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth); +#endif bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; display->setTextAlignment(TEXT_ALIGN_LEFT); @@ -304,28 +342,48 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int } } - // Draw signal strength bars - int bars = (node->snr > 5) ? 4 : (node->snr > 0) ? 3 : (node->snr > -5) ? 2 : (node->snr > -10) ? 1 : 0; - int barWidth = 2; - int barStartX = x + barsXOffset; - int barStartY = y + 1 + (FONT_HEIGHT_SMALL / 2) + 2; + const bool isZeroHop = node->has_hops_away && node->hops_away == 0; - for (int b = 0; b < 4; b++) { - if (b < bars) { - int height = (b * 2); - display->fillRect(barStartX + (b * (barWidth + 1)), barStartY - height, barWidth, height); + // Show signal only for direct neighbors (0 hops) + if (isZeroHop) { + int bars = (node->snr > 5) ? 4 : (node->snr > 0) ? 3 : (node->snr > -5) ? 2 : (node->snr > -10) ? 1 : 0; + int barStartX = x + barsXOffset; + int barStartY = y + 1 + (FONT_HEIGHT_SMALL / 2) + 2; + + if (bars > 0) { + uint16_t signalBarsColor = TFTPalette::Bad; + if (bars >= 3) { + signalBarsColor = TFTPalette::Good; + } else if (bars == 2) { + signalBarsColor = TFTPalette::Medium; + } + + // Highest bar reaches 6 px in this renderer. + setAndRegisterTFTColorRole(TFTColorRole::SignalBars, signalBarsColor, TFTPalette::Black, barStartX, barStartY - 6, + (kBarCount * kBarWidth) + ((kBarCount - 1) * kBarGap), 6); + } + + for (int b = 0; b < kBarCount; b++) { + if (b < bars) { + int height = (b * 2); + display->fillRect(barStartX + (b * (kBarWidth + kBarGap)), barStartY - height, kBarWidth, height); + } } } - // Draw hop count - char hopStr[6] = ""; - if (node->has_hops_away && node->hops_away > 0) - snprintf(hopStr, sizeof(hopStr), "[%d]", node->hops_away); + // Draw hop count + hop icon + if (node->has_hops_away && node->hops_away > 0) { + char hopCount[6]; + snprintf(hopCount, sizeof(hopCount), "%d", node->hops_away); - if (hopStr[0] != '\0') { - int rightEdge = x + columnWidth - hopOffset; - int textWidth = display->getStringWidth(hopStr); - display->drawString(rightEdge - textWidth, y, hopStr); + const int hopCountWidth = display->getStringWidth(hopCount); + const int gap = 1; + const int totalWidth = hopCountWidth + gap + hop_width; + const int hopX = barsRightEdge - totalWidth; + const int iconY = y + (FONT_HEIGHT_SMALL - hop_height) / 2; + + display->drawString(hopX, y, hopCount); + display->drawXbm(hopX + hopCountWidth + gap, iconY, hop_width, hop_height, hop); } } @@ -340,6 +398,9 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 char nodeName[96]; UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName), nameMaxWidth); +#if GRAPHICS_TFT_COLORING_ENABLED + applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth); +#endif bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; char distStr[10] = ""; @@ -445,6 +506,9 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 char nodeName[96]; UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName), nameMaxWidth); +#if GRAPHICS_TFT_COLORING_ENABLED + applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth); +#endif bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; display->setTextAlignment(TEXT_ALIGN_LEFT); @@ -700,6 +764,9 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1); display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1); display->setColor(WHITE); +#if GRAPHICS_TFT_COLORING_ENABLED + registerTFTActionMenuRegions(boxLeft, boxTop, boxWidth, boxHeight); +#endif // Text display->drawString(boxLeft + padding, boxTop + padding, buf); diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp index 31eb2c3c8..cca60d1e2 100644 --- a/src/graphics/draw/NotificationRenderer.cpp +++ b/src/graphics/draw/NotificationRenderer.cpp @@ -7,6 +7,8 @@ #include "UIRenderer.h" #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" +#include "graphics/TFTColorRegions.h" +#include "graphics/TFTPalette.h" #include "graphics/images.h" #include "input/RotaryEncoderInterruptImpl1.h" #include "input/UpDownInterruptImpl1.h" @@ -608,6 +610,9 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1); display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1); display->setColor(WHITE); +#if GRAPHICS_TFT_COLORING_ENABLED + registerTFTActionMenuRegions(boxLeft, boxTop, boxWidth, boxHeight); +#endif // Draw Content int16_t lineY = boxTop + vPadding; @@ -630,7 +635,21 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay if (strchr(lineBuffer, 'p') || strchr(lineBuffer, 'g') || strchr(lineBuffer, 'y') || strchr(lineBuffer, 'j')) { background_yOffset = -1; } - display->fillRect(boxLeft, boxTop + 1, boxWidth, effectiveLineHeight - background_yOffset); + const int16_t titleBarY = boxTop + 1; + const int16_t titleBarHeight = effectiveLineHeight - background_yOffset; + display->fillRect(boxLeft, titleBarY, boxWidth, titleBarHeight); +#if GRAPHICS_TFT_COLORING_ENABLED + if (alertBannerOptions > 0) { + const uint16_t titleTextColor = + (getActiveTheme().id == ThemeID::DefaultLight) ? TFTPalette::Black : getThemeHeaderText(); + // Keep title role away from border/corner pixels so rounded-corner masks are not remapped to the title text + // color. + if (boxWidth > 2 && titleBarHeight > 0) { + setAndRegisterTFTColorRole(TFTColorRole::ActionMenuTitle, getThemeHeaderBg(), titleTextColor, boxLeft + 1, + titleBarY, boxWidth - 2, titleBarHeight); + } + } +#endif display->setColor(BLACK); int yOffset = 3; if (current_notification_type == notificationTypeEnum::node_picker) { @@ -650,6 +669,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay const int barSpacing = 2; const int barHeightStep = 2; const int gap = 6; + const int maxBarHeight = totalBars * barHeightStep; int textWidth = display->getStringWidth(lineBuffer, strlen(lineBuffer), true); int barsWidth = totalBars * barWidth + (totalBars - 1) * barSpacing + gap; @@ -664,6 +684,20 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay int baseX = groupStartX + textWidth + gap; int baseY = lineY + effectiveLineHeight - 1; +#if GRAPHICS_TFT_COLORING_ENABLED + if (graphics::bannerSignalBars > 0) { + uint16_t signalBarsColor = TFTPalette::Medium; + if (graphics::bannerSignalBars <= 1) { + signalBarsColor = TFTPalette::Bad; + } else if (graphics::bannerSignalBars >= 4) { + signalBarsColor = TFTPalette::Good; + } + const int activeBars = min(graphics::bannerSignalBars, totalBars); + const int regionWidth = activeBars * barWidth + (activeBars - 1) * barSpacing; + setAndRegisterTFTColorRole(TFTColorRole::SignalBars, signalBarsColor, TFTPalette::Black, baseX, + baseY - maxBarHeight, regionWidth, maxBarHeight); + } +#endif for (int b = 0; b < totalBars; b++) { int barHeight = (b + 1) * barHeightStep; int x = baseX + b * (barWidth + barSpacing); diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index ef2d5e3c5..a28a4fe24 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -13,6 +13,8 @@ #include "gps/GeoCoord.h" #include "graphics/EmoteRenderer.h" #include "graphics/SharedUIDisplay.h" +#include "graphics/TFTColorRegions.h" +#include "graphics/TFTPalette.h" #include "graphics/TimeFormatters.h" #include "graphics/images.h" #include "main.h" @@ -30,6 +32,7 @@ namespace graphics { NodeNum UIRenderer::currentFavoriteNodeNum = 0; std::vector graphics::UIRenderer::favoritedNodes; +static bool gBootSplashBoldPass = false; static inline void drawSatelliteIcon(OLEDDisplay *display, int16_t x, int16_t y) { @@ -41,6 +44,347 @@ static inline void drawSatelliteIcon(OLEDDisplay *display, int16_t x, int16_t y) } } +struct StandardCompassNeedlePoints { + int16_t northTipX; + int16_t northTipY; + int16_t northLeftX; + int16_t northLeftY; + int16_t northRightX; + int16_t northRightY; + int16_t southTipX; + int16_t southTipY; + int16_t southLeftX; + int16_t southLeftY; + int16_t southRightX; + int16_t southRightY; +}; + +static inline void swapPoint(int16_t &ax, int16_t &ay, int16_t &bx, int16_t &by) +{ + const int16_t tx = ax; + const int16_t ty = ay; + ax = bx; + ay = by; + bx = tx; + by = ty; +} + +static inline void transformNeedlePoint(float localX, float localY, float sinHeading, float cosHeading, float scale, + int16_t centerX, int16_t centerY, int16_t &outX, int16_t &outY) +{ + const float x = ((localX * cosHeading) - (localY * sinHeading)) * scale + centerX; + const float y = ((localX * sinHeading) + (localY * cosHeading)) * scale + centerY; + outX = static_cast(x); + outY = static_cast(y); +} + +static float getCompassRingAngleOffset(float heading) +{ + return (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) ? -heading : 0.0f; +} + +static inline StandardCompassNeedlePoints computeStandardCompassNeedlePoints(int16_t compassX, int16_t compassY, + uint16_t compassDiam, float headingRadian, + float centerGapPx) +{ + // Standard-style symmetric needle with a narrow waist and a tiny center gap + // between north/south halves to prevent seam bleed while rotating. + const float scaledDiam = compassDiam * 0.76f; + const float gapNormHalf = (centerGapPx * 0.5f) / scaledDiam; + const float sinHeading = sinf(headingRadian); + const float cosHeading = cosf(headingRadian); + + StandardCompassNeedlePoints points{}; + transformNeedlePoint(0.0f, -0.5f, sinHeading, cosHeading, scaledDiam, compassX, compassY, points.northTipX, points.northTipY); + transformNeedlePoint(-0.09f, -gapNormHalf, sinHeading, cosHeading, scaledDiam, compassX, compassY, points.northLeftX, + points.northLeftY); + transformNeedlePoint(0.09f, -gapNormHalf, sinHeading, cosHeading, scaledDiam, compassX, compassY, points.northRightX, + points.northRightY); + transformNeedlePoint(0.0f, 0.5f, sinHeading, cosHeading, scaledDiam, compassX, compassY, points.southTipX, points.southTipY); + transformNeedlePoint(-0.09f, gapNormHalf, sinHeading, cosHeading, scaledDiam, compassX, compassY, points.southLeftX, + points.southLeftY); + transformNeedlePoint(0.09f, gapNormHalf, sinHeading, cosHeading, scaledDiam, compassX, compassY, points.southRightX, + points.southRightY); + return points; +} + +static inline void drawCompassNorthOnlyLabel(OLEDDisplay *display, int16_t compassX, int16_t compassY, int16_t compassRadius, + float heading) +{ + int16_t labelRadius = compassRadius; + // CompassRenderer::drawCompassNorth() expands radius on high-res by +4. + // Compensate so label placement stays aligned with the current UI layout. + if (currentResolution == ScreenResolution::High && labelRadius > 4) { + labelRadius -= 4; + } + graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, heading, labelRadius); +} + +static inline void drawMonoCompass(OLEDDisplay *display, int16_t compassX, int16_t compassY, int16_t compassRadius, float heading) +{ + const StandardCompassNeedlePoints points = + computeStandardCompassNeedlePoints(compassX, compassY, static_cast(compassRadius * 2), -heading, 0.0f); + +#ifdef USE_EINK + display->setColor(WHITE); + display->drawTriangle(points.northTipX, points.northTipY, points.northLeftX, points.northLeftY, points.northRightX, + points.northRightY); + display->drawTriangle(points.southTipX, points.southTipY, points.southLeftX, points.southLeftY, points.southRightX, + points.southRightY); +#else + // OLED variant: same needle geometry as TFT, but monochrome contrast. + display->setColor(WHITE); + display->fillTriangle(points.northTipX, points.northTipY, points.northLeftX, points.northLeftY, points.northRightX, + points.northRightY); + display->setColor(BLACK); + display->fillTriangle(points.southTipX, points.southTipY, points.southLeftX, points.southLeftY, points.southRightX, + points.southRightY); + // Keep a white outline so the black half remains visible on dark backgrounds. + display->setColor(WHITE); + display->drawTriangle(points.southTipX, points.southTipY, points.southLeftX, points.southLeftY, points.southRightX, + points.southRightY); +#endif + + display->drawCircle(compassX, compassY, compassRadius); + drawCompassNorthOnlyLabel(display, compassX, compassY, compassRadius, heading); +} + +#if GRAPHICS_TFT_COLORING_ENABLED +struct NeedleColorBand { + int16_t xMin; + int16_t xMax; + int16_t yMin; + int16_t yMax; + bool used; +}; + +static constexpr int kNeedleBandCount = 6; + +static inline void registerNeedleSpan(NeedleColorBand (&bands)[kNeedleBandCount], int16_t bandTop, int16_t bandHeight, int16_t y, + int16_t a, int16_t b) +{ + if (a > b) { + const int16_t t = a; + a = b; + b = t; + } + + int band = (static_cast(y - bandTop) * kNeedleBandCount) / bandHeight; + if (band < 0) { + band = 0; + } else if (band >= kNeedleBandCount) { + band = kNeedleBandCount - 1; + } + + NeedleColorBand ®ion = bands[band]; + if (!region.used) { + region.used = true; + region.xMin = a; + region.xMax = b; + region.yMin = y; + region.yMax = y; + return; + } + if (a < region.xMin) + region.xMin = a; + if (b > region.xMax) + region.xMax = b; + if (y < region.yMin) + region.yMin = y; + if (y > region.yMax) + region.yMax = y; +} + +static void drawNeedleHalfAndRegisterBands(OLEDDisplay *display, int16_t x0, int16_t y0, int16_t x1, int16_t y1, int16_t x2, + int16_t y2, uint16_t onColor, uint16_t offColor) +{ + // Important for maintainers: + // The compass needle rotates continuously, so color-region registration must + // track triangle shape (or a close approximation), not only one AABB. + // Coarse rectangles can leak south color into north at diagonal angles. + // Keep this banded approach unless a replacement preserves per-angle coverage. + // Performance note: draw the triangle once via fillTriangle(), then build + // band regions in software for accurate color-role registration. + display->fillTriangle(x0, y0, x1, y1, x2, y2); + + if (y0 > y1) + swapPoint(x0, y0, x1, y1); + if (y1 > y2) + swapPoint(x1, y1, x2, y2); + if (y0 > y1) + swapPoint(x0, y0, x1, y1); + + NeedleColorBand bands[kNeedleBandCount] = {}; + + const int16_t bandTop = y0; + const int16_t bandBottom = y2; + const int16_t bandHeight = (bandBottom >= bandTop) ? static_cast(bandBottom - bandTop + 1) : 1; + + const int32_t dx01 = x1 - x0; + const int32_t dy01 = y1 - y0; + const int32_t dx02 = x2 - x0; + const int32_t dy02 = y2 - y0; + const int32_t dx12 = x2 - x1; + const int32_t dy12 = y2 - y1; + + int32_t sa = 0; + int32_t sb = 0; + int16_t y = y0; + + const int16_t last = (y1 == y2) ? y1 : static_cast(y1 - 1); + for (; y <= last; y++) { + const int16_t a = static_cast(x0 + ((dy01 != 0) ? (sa / dy01) : 0)); + const int16_t b = static_cast(x0 + ((dy02 != 0) ? (sb / dy02) : 0)); + sa += dx01; + sb += dx02; + registerNeedleSpan(bands, bandTop, bandHeight, y, a, b); + } + + sa = dx12 * static_cast(y - y1); + sb = dx02 * static_cast(y - y0); + for (; y <= y2; y++) { + const int16_t a = static_cast(x1 + ((dy12 != 0) ? (sa / dy12) : 0)); + const int16_t b = static_cast(x0 + ((dy02 != 0) ? (sb / dy02) : 0)); + sa += dx12; + sb += dx02; + registerNeedleSpan(bands, bandTop, bandHeight, y, a, b); + } + + for (int i = 0; i < kNeedleBandCount; i++) { + if (!bands[i].used) + continue; + registerTFTColorRegionDirect(bands[i].xMin, bands[i].yMin, bands[i].xMax - bands[i].xMin + 1, + bands[i].yMax - bands[i].yMin + 1, onColor, offColor); + } +} + +static inline void drawCompassCardinalLabel(OLEDDisplay *display, int16_t x, int16_t y, const char *label, int16_t textWidth) +{ + const int16_t labelTop = y - (FONT_HEIGHT_SMALL / 2); + const int16_t padX = 1; + const int16_t padY = 1; + + // Clear any ring/tick pixels behind the label so letters remain clean. + display->setColor(BLACK); + display->fillRect(x - (textWidth / 2) - padX, labelTop - padY, textWidth + (padX * 2), FONT_HEIGHT_SMALL + (padY * 2)); + + display->setColor(WHITE); + display->drawString(x, labelTop, label); +} + +static inline void drawCompassCardinalLabels(OLEDDisplay *display, int16_t compassX, int16_t compassY, int16_t compassRadius, + float heading) +{ + const float northAngle = getCompassRingAngleOffset(heading); + const float radius = compassRadius - 1.0f; + const float sinNorth = sinf(northAngle); + const float cosNorth = cosf(northAngle); + + const int16_t nX = compassX + static_cast(radius * sinNorth); + const int16_t nY = compassY - static_cast(radius * cosNorth); + const int16_t eX = compassX + static_cast(radius * cosNorth); + const int16_t eY = compassY + static_cast(radius * sinNorth); + const int16_t sX = compassX - static_cast(radius * sinNorth); + const int16_t sY = compassY + static_cast(radius * cosNorth); + const int16_t wX = compassX - static_cast(radius * cosNorth); + const int16_t wY = compassY - static_cast(radius * sinNorth); + + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_CENTER); + const int16_t labelWidth = static_cast(display->getStringWidth("N")); + drawCompassCardinalLabel(display, nX, nY, "N", labelWidth); + drawCompassCardinalLabel(display, eX, eY, "E", labelWidth); + drawCompassCardinalLabel(display, sX, sY, "S", labelWidth); + drawCompassCardinalLabel(display, wX, wY, "W", labelWidth); +} + +static inline void drawCompassDegreeMarkers(OLEDDisplay *display, int16_t compassX, int16_t compassY, int16_t compassRadius, + float heading) +{ + const float baseAngle = getCompassRingAngleOffset(heading); + + constexpr int16_t majorLen = 5; + constexpr int16_t minorLen = 3; + + display->setColor(WHITE); + constexpr float kStepAngle = 15.0f * DEG_TO_RAD; + const float sinStep = sinf(kStepAngle); + const float cosStep = cosf(kStepAngle); + float sinAngle = sinf(baseAngle); + float cosAngle = cosf(baseAngle); + bool isMajor = true; + for (int tick = 0; tick < 24; tick++) { + const int16_t tickLen = isMajor ? majorLen : minorLen; + + const int16_t xOuter = compassX + static_cast((compassRadius - 1) * sinAngle); + const int16_t yOuter = compassY - static_cast((compassRadius - 1) * cosAngle); + const int16_t xInner = compassX + static_cast((compassRadius - tickLen) * sinAngle); + const int16_t yInner = compassY - static_cast((compassRadius - tickLen) * cosAngle); + display->drawLine(xInner, yInner, xOuter, yOuter); + + // Rotate [sin, cos] by a fixed step instead of recomputing trig 24x/frame. + const float nextSin = (sinAngle * cosStep) + (cosAngle * sinStep); + const float nextCos = (cosAngle * cosStep) - (sinAngle * sinStep); + sinAngle = nextSin; + cosAngle = nextCos; + isMajor = !isMajor; + } +} + +static inline void drawStandardCompassNeedle(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, + float headingRadian, uint16_t needleOffColor) +{ + const StandardCompassNeedlePoints points = + computeStandardCompassNeedlePoints(compassX, compassY, compassDiam, headingRadian, 9.0f); + + display->setColor(WHITE); +#ifdef USE_EINK + display->drawTriangle(points.northTipX, points.northTipY, points.northLeftX, points.northLeftY, points.northRightX, + points.northRightY); + display->drawTriangle(points.southTipX, points.southTipY, points.southLeftX, points.southLeftY, points.southRightX, + points.southRightY); +#else + // NOTE: do not collapse these to one region per half during "flash + // optimization". The needle spins, and coarse rectangles will bleed color + // across halves at diagonal angles. + drawNeedleHalfAndRegisterBands(display, points.northTipX, points.northTipY, points.northLeftX, points.northLeftY, + points.northRightX, points.northRightY, TFTPalette::Red, needleOffColor); + drawNeedleHalfAndRegisterBands(display, points.southTipX, points.southTipY, points.southLeftX, points.southLeftY, + points.southRightX, points.southRightY, TFTPalette::Blue, needleOffColor); +#endif +} + +static inline void drawTftCompass(OLEDDisplay *display, int16_t compassX, int16_t compassY, int16_t compassRadius, float heading) +{ + // Compass colors should follow whatever background role is already active at this location. + const uint16_t compassBgColor = resolveTFTOffColorAt(compassX, compassY, getThemeBodyBg()); + const uint16_t compassGlyphColor = TFTPalette::pickReadableMonoFg(compassBgColor); + const int16_t pad = 2; + const int16_t labelPadX = static_cast(display->getStringWidth("W") / 2) + 2; + const int16_t labelPadY = static_cast(FONT_HEIGHT_SMALL / 2) + 2; + const int16_t boxX = compassX - compassRadius - pad - labelPadX; + const int16_t boxY = compassY - compassRadius - pad - labelPadY; + const int16_t boxW = (compassRadius * 2) + (pad * 2) + 1 + (labelPadX * 2); + const int16_t boxH = (compassRadius * 2) + (pad * 2) + 1 + (labelPadY * 2); + // Never let compass-local tint regions override the header role regions. + const int16_t bodyTop = static_cast(getTextPositions(display)[1]); + int16_t clippedY = boxY; + int16_t clippedH = boxH; + if (clippedY < bodyTop) { + clippedH = static_cast(clippedH - (bodyTop - clippedY)); + clippedY = bodyTop; + } + if (clippedH > 0) { + registerTFTColorRegionDirect(boxX, clippedY, boxW, clippedH, compassGlyphColor, compassBgColor); + } + + drawStandardCompassNeedle(display, compassX, compassY, static_cast(compassRadius * 2), -heading, compassBgColor); + display->drawCircle(compassX, compassY, compassRadius); + drawCompassDegreeMarkers(display, compassX, compassY, compassRadius, heading); + drawCompassCardinalLabels(display, compassX, compassY, compassRadius, heading); +} +#endif // GRAPHICS_TFT_COLORING_ENABLED + static void drawCompassStatusText(OLEDDisplay *display, int16_t compassX, int16_t compassY, const char *statusLine1, const char *statusLine2) { @@ -50,6 +394,99 @@ static void drawCompassStatusText(OLEDDisplay *display, int16_t compassX, int16_ display->setTextAlignment(TEXT_ALIGN_LEFT); } +static void drawBearingCompassOrStatus(OLEDDisplay *display, int16_t compassX, int16_t compassY, int16_t compassRadius, + bool showCompass, float myHeading, float bearing, const char *statusLine1, + const char *statusLine2) +{ + // Shared "favorite node" compass renderer: draw ring, then either heading data or fallback status text. + display->drawCircle(compassX, compassY, compassRadius); + if (showCompass) { + CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius); + CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, bearing); + } else { + drawCompassStatusText(display, compassX, compassY, statusLine1, statusLine2); + } +} + +static void drawDetailedCompassOrStatus(OLEDDisplay *display, int16_t compassX, int16_t compassY, int16_t compassRadius, + bool validHeading, float heading, const char *statusLine1, const char *statusLine2) +{ + // Shared "position screen" compass renderer: use mono/TFT path only when heading is valid. + if (validHeading) { +#if GRAPHICS_TFT_COLORING_ENABLED + drawTftCompass(display, compassX, compassY, compassRadius, heading); +#else + drawMonoCompass(display, compassX, compassY, compassRadius, heading); +#endif + } else { + display->drawCircle(compassX, compassY, compassRadius); + drawCompassStatusText(display, compassX, compassY, statusLine1, statusLine2); + } +} + +static bool computeLandscapeCompassPlacement(OLEDDisplay *display, int16_t xOffset, int16_t topY, int16_t *compassX, + int16_t *compassY, int16_t *compassRadius) +{ + // Keep compass vertically centered in the body area while reserving footer/nav space. + const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); + const int16_t usableHeight = bottomY - topY - 5; + int16_t radius = usableHeight / 2; + if (radius < 8) { + radius = 8; + } + + *compassRadius = radius; + *compassX = xOffset + SCREEN_WIDTH - radius - 8; + *compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2; + return true; +} + +static bool computeBottomCompassPlacement(OLEDDisplay *display, int16_t xOffset, int16_t yBelowContent, int16_t bottomReserved, + int16_t margin, int16_t *compassX, int16_t *compassY, int16_t *compassRadius) +{ + // Return false when content leaves no room for a readable compass. + int availableHeight = SCREEN_HEIGHT - yBelowContent - bottomReserved - margin; + if (availableHeight < FONT_HEIGHT_SMALL * 2) { + return false; + } + + int16_t radius = static_cast(availableHeight / 2); + if (radius < 8) { + radius = 8; + } + if (radius * 2 > SCREEN_WIDTH - 16) { + radius = (SCREEN_WIDTH - 16) / 2; + } + + *compassRadius = radius; + *compassX = xOffset + (SCREEN_WIDTH / 2); + *compassY = static_cast(yBelowContent + (availableHeight / 2)); + return true; +} + +static void drawTruncatedStatusLine(OLEDDisplay *display, int16_t x, int16_t y, const std::string &statusText) +{ + // Fixed-buffer truncate helper replaces iterative std::string chopping to keep code size down. + char rawStatus[96]; + snprintf(rawStatus, sizeof(rawStatus), " Status: %s", statusText.c_str()); + + char clippedStatus[96]; + UIRenderer::truncateStringWithEmotes(display, rawStatus, clippedStatus, sizeof(clippedStatus), display->getWidth()); + display->drawString(x, y, clippedStatus); +} + +static int computeChannelUtilizationFill(int percent, int maxFill) +{ + // Compact linear fill mapping for the utilization bar. + if (percent <= 0 || maxFill <= 0) { + return 0; + } + if (percent >= 100) { + return maxFill; + } + return (maxFill * percent + 50) / 100; +} + void graphics::UIRenderer::rebuildFavoritedNodes() { favoritedNodes.clear(); @@ -331,7 +768,7 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat snprintf(titlestr, sizeof(titlestr), "*%s*", shortName); // === Draw battery/time/mail header (common across screens) === - graphics::drawCommonHeader(display, x, y, titlestr); + graphics::drawCommonHeader(display, x, y, titlestr, false, false, false, true, TFTPalette::Yellow); // ===== DYNAMIC ROW STACKING WITH YOUR MACROS ===== // 1. Each potential info row has a macro-defined Y position (not regular increments!). @@ -349,8 +786,13 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat username = (node->has_user && node->user.long_name[0]) ? node->user.long_name : nullptr; } + // Print node's long name (e.g. "Backpack Node") if (username) { - // Print node's long name (e.g. "Backpack Node") +#if GRAPHICS_TFT_COLORING_ENABLED + const int usernameWidth = UIRenderer::measureStringWithEmotes(display, username); + setAndRegisterTFTColorRole(TFTColorRole::FavoriteNodeBGHighlight, TFTPalette::Yellow, TFTPalette::Black, x, + getTextPositions(display)[line], usernameWidth, FONT_HEIGHT_SMALL); +#endif UIRenderer::drawStringWithEmotes(display, x, getTextPositions(display)[line++], username, FONT_HEIGHT_SMALL, 1, false); } @@ -370,45 +812,15 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat } if (found) { - std::string statusLine = std::string(" Status: ") + found->statusText; - { - const int screenW = display->getWidth(); - const int ellipseW = display->getStringWidth("..."); - int w = display->getStringWidth(statusLine.c_str()); - - // Only do work if it overflows - if (w > screenW) { - bool truncated = false; - if (ellipseW > screenW) { - statusLine.clear(); - } else { - while (!statusLine.empty()) { - // remove one char (byte) at a time - statusLine.pop_back(); - truncated = true; - - // Measure candidate with ellipsis appended - std::string candidate = statusLine + "..."; - if (display->getStringWidth(candidate.c_str()) <= screenW) { - statusLine = std::move(candidate); - break; - } - } - if (statusLine.empty() && ellipseW <= screenW) { - statusLine = "..."; - } - } - } - } - display->drawString(x, getTextPositions(display)[line++], statusLine.c_str()); + drawTruncatedStatusLine(display, x, getTextPositions(display)[line++], found->statusText); } } #endif - // === 2. Signal and Hops (combined on one line, if available) === - char signalHopsStr[32] = ""; + // === 2. Signal/Hops line (if available) === bool haveSignal = false; int bars = 0; + const char *qualityLabel = nullptr; // Helper to get SNR limit based on modem preset auto getSnrLimit = [](meshtastic_Config_LoRaConfig_ModemPreset preset) -> float { @@ -429,80 +841,51 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat } }; - // Calculate signal grade using modem preset and SNR only - float snrLimit = getSnrLimit(config.lora.modem_preset); - float snr = node->snr; - - // Determine signal quality label and bars using SNR-only grading - const char *qualityLabel = nullptr; - - if (snr > snrLimit + 10) { - qualityLabel = "Good"; - bars = 4; - } else if (snr > snrLimit + 6) { - qualityLabel = "Good"; - bars = 3; - } else if (snr > snrLimit + 2) { - qualityLabel = "Good"; - bars = 2; - } else if (snr > snrLimit - 4) { - qualityLabel = "Fair"; - bars = 1; - } else { - qualityLabel = "Bad"; - bars = 1; - } - // Add extra spacing on the left if we have an API connection to account for the common footer icons const char *leftSideSpacing = graphics::isAPIConnected(service->api_state) ? (currentResolution == ScreenResolution::High ? " " : " ") : " "; + const bool isZeroHop = node->has_hops_away && node->hops_away == 0; - // --- Build the Signal/Hops line --- - // Only show signal if we have valid SNR - if (snr > -100 && snr != 0) { - snprintf(signalHopsStr, sizeof(signalHopsStr), "%sSig:%s", leftSideSpacing, qualityLabel); - haveSignal = true; - } - - if (node->hops_away > 0) { - size_t len = strlen(signalHopsStr); - if (haveSignal) { - snprintf(signalHopsStr + len, sizeof(signalHopsStr) - len, " [#]"); - } else { - snprintf(signalHopsStr, sizeof(signalHopsStr), "[#]"); - } - } - - if (signalHopsStr[0]) { - int yPos = getTextPositions(display)[line++]; - int curX = x; - - // Split combined string into signal text and hop suffix - char sigPart[20] = ""; - const char *hopPart = nullptr; - - char *bracket = strchr(signalHopsStr, '['); - if (bracket) { - size_t n = (size_t)(bracket - signalHopsStr); - if (n >= sizeof(sigPart)) - n = sizeof(sigPart) - 1; - memcpy(sigPart, signalHopsStr, n); - sigPart[n] = '\0'; - - // Trim trailing spaces - while (strlen(sigPart) && sigPart[strlen(sigPart) - 1] == ' ') { - sigPart[strlen(sigPart) - 1] = '\0'; + // Signal text/bars are only for direct (zero-hop) nodes with valid SNR. + if (isZeroHop) { + float snr = node->snr; + if (snr > -100 && snr != 0) { + float snrLimit = getSnrLimit(config.lora.modem_preset); + // Determine signal quality label and bars using SNR-only grading. + if (snr > snrLimit + 10) { + qualityLabel = "Good"; + bars = 4; + } else if (snr > snrLimit + 6) { + qualityLabel = "Good"; + bars = 3; + } else if (snr > snrLimit + 2) { + qualityLabel = "Good"; + bars = 2; + } else if (snr > snrLimit - 4) { + qualityLabel = "Fair"; + bars = 1; + } else { + qualityLabel = "Bad"; + bars = 1; } - hopPart = bracket; // "[n Hop(s)]" - } else { - strncpy(sigPart, signalHopsStr, sizeof(sigPart) - 1); - sigPart[sizeof(sigPart) - 1] = '\0'; + haveSignal = true; } + } - // Draw signal quality text - display->drawString(curX, yPos, sigPart); - curX += display->getStringWidth(sigPart) + 4; + const bool showHops = node->has_hops_away && node->hops_away > 0; + + if (haveSignal || showHops) { + int yPos = getTextPositions(display)[line++]; + int curX = x + display->getStringWidth(leftSideSpacing); + + // Draw signal quality text for zero-hop nodes when present. + if (haveSignal && qualityLabel) { + char signalLabel[20]; + snprintf(signalLabel, sizeof(signalLabel), "Sig:%s", qualityLabel); + display->drawString(curX, yPos, signalLabel); + curX += display->getStringWidth(signalLabel) + 4; + } // Draw signal bars (skip on UltraLow, text only) if (currentResolution != ScreenResolution::UltraLow && haveSignal && bars > 0) { @@ -521,6 +904,16 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat if (!hi) maxBarHeight -= 1; int barY = yPos + (FONT_HEIGHT_SMALL - maxBarHeight) / 2; + int totalBarsWidth = (kMaxBars * barWidth) + ((kMaxBars - 1) * barGap); + + uint16_t signalBarsColor = TFTPalette::Good; + if (qualityLabel && strcmp(qualityLabel, "Fair") == 0) { + signalBarsColor = TFTPalette::Medium; + } else if (qualityLabel && strcmp(qualityLabel, "Bad") == 0) { + signalBarsColor = TFTPalette::Bad; + } + setAndRegisterTFTColorRole(TFTColorRole::SignalBars, signalBarsColor, TFTPalette::Black, barX, barY, totalBarsWidth, + maxBarHeight); for (int bi = 0; bi < kMaxBars; bi++) { int barHeight = maxBarHeight * (bi + 1) / kMaxBars; @@ -538,29 +931,23 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat } } - curX += (kMaxBars * barWidth) + ((kMaxBars - 1) * barGap) + 2; + curX += totalBarsWidth + 2; } - // Draw hops AFTER the bars as: [ number + hop icon ] - if (hopPart && node->hops_away > 0) { + // Draw hops for non-zero-hop nodes as: number + hop icon. + // This path is mutually exclusive with the zero-hop signal-bars path above. + if (showHops) { + display->drawString(curX, yPos, "Hop:"); + curX += display->getStringWidth("Hop:") + 2; - // open bracket - display->drawString(curX, yPos, "["); - curX += display->getStringWidth("[") + 1; - - // hop count char hopCount[6]; snprintf(hopCount, sizeof(hopCount), "%d", node->hops_away); display->drawString(curX, yPos, hopCount); curX += display->getStringWidth(hopCount) + 2; - // hop icon const int iconY = yPos + (FONT_HEIGHT_SMALL - hop_height) / 2; display->drawXbm(curX, iconY, hop_width, hop_height, hop); curX += hop_width + 1; - - // closing bracket - display->drawString(curX, yPos, "]"); } } @@ -599,48 +986,29 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat bool haveDistance = false; if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) { - double lat1 = ourNode->position.latitude_i * 1e-7; - double lon1 = ourNode->position.longitude_i * 1e-7; - double lat2 = node->position.latitude_i * 1e-7; - double lon2 = node->position.longitude_i * 1e-7; - double earthRadiusKm = 6371.0; - double dLat = (lat2 - lat1) * DEG_TO_RAD; - double dLon = (lon2 - lon1) * DEG_TO_RAD; - double a = - sin(dLat / 2) * sin(dLat / 2) + cos(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * sin(dLon / 2) * sin(dLon / 2); - double c = 2 * atan2(sqrt(a), sqrt(1 - a)); - double distanceKm = earthRadiusKm * c; - + // Use shared meter conversion, then format display units with lightweight integer rounding. + const float distanceMeters = + GeoCoord::latLongToMeter(DegD(node->position.latitude_i), DegD(node->position.longitude_i), + DegD(ourNode->position.latitude_i), DegD(ourNode->position.longitude_i)); if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { - double miles = distanceKm * 0.621371; - if (miles < 0.1) { - int feet = (int)(miles * 5280); - if (feet > 0 && feet < 1000) { - snprintf(distStr, sizeof(distStr), "%sDistance:%dft", leftSideSpacing, feet); - haveDistance = true; - } else if (feet >= 1000) { - snprintf(distStr, sizeof(distStr), "%sDistance:¼mi", leftSideSpacing); - haveDistance = true; - } + const int feet = static_cast((distanceMeters * METERS_TO_FEET) + 0.5f); + if (feet > 0 && feet < 1000) { + snprintf(distStr, sizeof(distStr), "%sDistance:%dft", leftSideSpacing, feet); + haveDistance = true; } else { - int roundedMiles = (int)(miles + 0.5); - if (roundedMiles > 0 && roundedMiles < 1000) { - snprintf(distStr, sizeof(distStr), "%sDistance:%dmi", leftSideSpacing, roundedMiles); + const int miles = (feet + 2640) / 5280; // rounded to nearest mile + if (miles > 0 && miles < 1000) { + snprintf(distStr, sizeof(distStr), "%sDistance:%dmi", leftSideSpacing, miles); haveDistance = true; } } } else { - if (distanceKm < 1.0) { - int meters = (int)(distanceKm * 1000); - if (meters > 0 && meters < 1000) { - snprintf(distStr, sizeof(distStr), "%sDistance:%dm", leftSideSpacing, meters); - haveDistance = true; - } else if (meters >= 1000) { - snprintf(distStr, sizeof(distStr), "%sDistance:1km", leftSideSpacing); - haveDistance = true; - } + const int meters = static_cast(distanceMeters + 0.5f); + if (meters > 0 && meters < 1000) { + snprintf(distStr, sizeof(distStr), "%sDistance:%dm", leftSideSpacing, meters); + haveDistance = true; } else { - int km = (int)(distanceKm + 0.5); + const int km = (meters + 500) / 1000; // rounded to nearest km if (km > 0 && km < 1000) { snprintf(distStr, sizeof(distStr), "%sDistance:%dkm", leftSideSpacing, km); haveDistance = true; @@ -725,64 +1093,29 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat } // --- Compass Rendering: landscape (wide) screens use the original side-aligned logic --- - if (SCREEN_WIDTH > SCREEN_HEIGHT) { - if (showCompass || statusLine1) { + if (showCompass || statusLine1) { + int16_t compassX = 0; + int16_t compassY = 0; + int16_t compassRadius = 0; + if (SCREEN_WIDTH > SCREEN_HEIGHT) { const int16_t topY = getTextPositions(display)[1]; - const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); - const int16_t usableHeight = bottomY - topY - 5; - int16_t compassRadius = usableHeight / 2; - if (compassRadius < 8) - compassRadius = 8; - const int16_t compassX = x + SCREEN_WIDTH - compassRadius - 8; - const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2; - const int16_t compassDiam = compassRadius * 2; - - display->drawCircle(compassX, compassY, compassRadius); - if (showCompass) { - CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius); - CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearing); - } else { - drawCompassStatusText(display, compassX, compassY, statusLine1, statusLine2); - } - } - // else show nothing - } else { - // Portrait or square: put compass at the bottom and centered, scaled to fit available space - if (showCompass || statusLine1) { - int yBelowContent = (line > 0 && line <= 5) ? (getTextPositions(display)[line - 1] + FONT_HEIGHT_SMALL + 2) - : getTextPositions(display)[1]; - const int margin = 4; -// --------- PATCH FOR EINK NAV BAR (ONLY CHANGE BELOW) ----------- + computeLandscapeCompassPlacement(display, x, topY, &compassX, &compassY, &compassRadius); + } else { + const int yBelowContent = (line > 0 && line <= 5) ? (getTextPositions(display)[line - 1] + FONT_HEIGHT_SMALL + 2) + : getTextPositions(display)[1]; #if defined(USE_EINK) const int iconSize = (currentResolution == ScreenResolution::High) ? 16 : 8; const int navBarHeight = iconSize + 6; #else const int navBarHeight = 0; #endif - // --------- END PATCH FOR EINK NAV BAR ----------- - int availableHeight = SCREEN_HEIGHT - yBelowContent - navBarHeight - margin; - - if (availableHeight < FONT_HEIGHT_SMALL * 2) + if (!computeBottomCompassPlacement(display, x, yBelowContent, navBarHeight, 4, &compassX, &compassY, + &compassRadius)) { return; - - int compassRadius = availableHeight / 2; - if (compassRadius < 8) - compassRadius = 8; - if (compassRadius * 2 > SCREEN_WIDTH - 16) - compassRadius = (SCREEN_WIDTH - 16) / 2; - - int compassX = x + SCREEN_WIDTH / 2; - int compassY = yBelowContent + availableHeight / 2; - - display->drawCircle(compassX, compassY, compassRadius); - if (showCompass) { - graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius); - graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, bearing); - } else { - drawCompassStatusText(display, compassX, compassY, statusLine1, statusLine2); } } - // else show nothing + drawBearingCompassOrStatus(display, compassX, compassY, compassRadius, showCompass, myHeading, bearing, statusLine1, + statusLine2); } #endif graphics::drawCommonFooter(display, x, y); @@ -808,12 +1141,6 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta // === Content below header === - // Determine if we need to show 4 or 5 rows on the screen - int rows = 4; - if (!config.bluetooth.enabled) { - rows = 5; - } - // === First Row: Region / Channel Utilization and Uptime === bool origBold = config.display.heading_bold; config.display.heading_bold = false; @@ -877,13 +1204,15 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta // === Third Row: Channel Utilization Bluetooth Off (Only If Actually Off) === const char *chUtil = "ChUtil:"; char chUtilPercentage[10]; - snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent()); + int chutil_percent = static_cast(airTime->channelUtilizationPercent() + 0.5f); + snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%d%%", chutil_percent); int chUtil_x = (currentResolution == ScreenResolution::High) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5; int chUtil_y = getTextPositions(display)[line] + 3; int chutil_bar_width = (currentResolution == ScreenResolution::High) ? 100 : 50; + int chutil_bar_max_fill = chutil_bar_width - 2; // Account for border if (!config.bluetooth.enabled) { #if defined(USE_EINK) chutil_bar_width = (currentResolution == ScreenResolution::High) ? 50 : 30; @@ -896,50 +1225,36 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta if (!config.bluetooth.enabled) { extraoffset = (currentResolution == ScreenResolution::High) ? 6 : 1; } - int chutil_percent = airTime->channelUtilizationPercent(); + const int raw_chutil_percent = chutil_percent; - int centerofscreen = SCREEN_WIDTH / 2; - int total_line_content_width = (chUtil_x + chutil_bar_width + display->getStringWidth(chUtilPercentage) + extraoffset) / 2; - int starting_position = centerofscreen - total_line_content_width; - if (!config.bluetooth.enabled) { - starting_position = 0; - } + // With BT disabled we pin this row left to make room for the extra "BT off" indicator. + const int starting_position = config.bluetooth.enabled ? x : 0; display->drawString(starting_position, getTextPositions(display)[line], chUtil); - // Force 56% or higher to show a full 100% bar, text would still show related percent. + // Force 61% or higher to show a full 100% bar, text would still show related percent. if (chutil_percent >= 61) { chutil_percent = 100; } - // Weighting for nonlinear segments - float milestone1 = 25; - float milestone2 = 40; - float weight1 = 0.45; // Weight for 0–25% - float weight2 = 0.35; // Weight for 25–40% - float weight3 = 0.20; // Weight for 40–100% - float totalWeight = weight1 + weight2 + weight3; - - int seg1 = chutil_bar_width * (weight1 / totalWeight); - int seg2 = chutil_bar_width * (weight2 / totalWeight); - int seg3 = chutil_bar_width * (weight3 / totalWeight); - - int fillRight = 0; - - if (chutil_percent <= milestone1) { - fillRight = (seg1 * (chutil_percent / milestone1)); - } else if (chutil_percent <= milestone2) { - fillRight = seg1 + (seg2 * ((chutil_percent - milestone1) / (milestone2 - milestone1))); - } else { - fillRight = seg1 + seg2 + (seg3 * ((chutil_percent - milestone2) / (100 - milestone2))); - } + int fillRight = computeChannelUtilizationFill(chutil_percent, chutil_bar_max_fill); // Draw outline display->drawRect(starting_position + chUtil_x, chUtil_y, chutil_bar_width, chutil_bar_height); // Fill progress if (fillRight > 0) { - display->fillRect(starting_position + chUtil_x, chUtil_y, fillRight, chutil_bar_height); +#if GRAPHICS_TFT_COLORING_ENABLED + uint16_t UtilizationFillColor = TFTPalette::Good; + if (raw_chutil_percent >= 60) { + UtilizationFillColor = TFTPalette::Bad; + } else if (raw_chutil_percent >= 35) { + UtilizationFillColor = TFTPalette::Medium; + } + setAndRegisterTFTColorRole(TFTColorRole::UtilizationFill, UtilizationFillColor, TFTPalette::Black, + starting_position + chUtil_x + 1, chUtil_y + 1, fillRight, chutil_bar_height - 2); +#endif + display->fillRect(starting_position + chUtil_x + 1, chUtil_y + 1, fillRight, chutil_bar_height - 2); } display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[line], @@ -970,9 +1285,8 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta if (SCREEN_WIDTH - UIRenderer::measureStringWithEmotes(display, combinedName) > 10) { textWidth = UIRenderer::measureStringWithEmotes(display, combinedName); nameX = (SCREEN_WIDTH - textWidth) / 2; - UIRenderer::drawStringWithEmotes( - display, nameX, ((rows == 4) ? getTextPositions(display)[line++] : getTextPositions(display)[line++]) + yOffset, - combinedName, FONT_HEIGHT_SMALL, 1, false); + UIRenderer::drawStringWithEmotes(display, nameX, getTextPositions(display)[line++] + yOffset, combinedName, + FONT_HEIGHT_SMALL, 1, false); } else { // === LongName Centered === textWidth = UIRenderer::measureStringWithEmotes(display, longName); @@ -1148,6 +1462,9 @@ void UIRenderer::drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLED // draw centered icon left to right and centered above the one line of app text #if defined(M5STACK_UNITC6L) display->drawXbm(x + (SCREEN_WIDTH - 50) / 2, y + (SCREEN_HEIGHT - 28) / 2, icon_width, icon_height, icon_bits); + if (gBootSplashBoldPass) { + display->drawXbm(x + (SCREEN_WIDTH - 50) / 2 + 1, y + (SCREEN_HEIGHT - 28) / 2, icon_width, icon_height, icon_bits); + } display->setFont(FONT_MEDIUM); display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); @@ -1157,6 +1474,9 @@ void UIRenderer::drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLED int msgX = x + (SCREEN_WIDTH - msgWidth) / 2; int msgY = y; display->drawString(msgX, msgY, upperMsg); + if (gBootSplashBoldPass) { + display->drawString(msgX + 1, msgY, upperMsg); + } } // Draw version and short name in bottom middle char footer[64]; @@ -1169,6 +1489,10 @@ void UIRenderer::drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLED int footerX = x + ((SCREEN_WIDTH - footerW) / 2); UIRenderer::drawStringWithEmotes(display, footerX, y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, footer, FONT_HEIGHT_SMALL, 1, false); + if (gBootSplashBoldPass) { + UIRenderer::drawStringWithEmotes(display, footerX + 1, y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, footer, FONT_HEIGHT_SMALL, + 1, false); + } screen->forceDisplay(); display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code @@ -1179,21 +1503,35 @@ void UIRenderer::drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLED display->setFont(FONT_MEDIUM); display->setTextAlignment(TEXT_ALIGN_LEFT); const char *title = "meshtastic.org"; - display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title); + display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - 5, title); + if (gBootSplashBoldPass) { + display->drawString(x + getStringCenteredX(title) + 1, y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - 5, title); + } display->setFont(FONT_SMALL); // Draw region in upper left - if (upperMsg) - display->drawString(x + 0, y + 0, upperMsg); + if (upperMsg) { + display->drawString(x + 5, y + 5, upperMsg); + if (gBootSplashBoldPass) { + display->drawString(x + 6, y + 5, upperMsg); + } + } // Draw version and short name in upper right const char *version = xstr(APP_VERSION_SHORT); - int versionX = x + SCREEN_WIDTH - display->getStringWidth(version); - display->drawString(versionX, y + 0, version); + int versionX = x + SCREEN_WIDTH - display->getStringWidth(version) - 5; + display->drawString(versionX, y + 5, version); + if (gBootSplashBoldPass) { + display->drawString(versionX + 1, y + 5, version); + } if (owner.short_name && owner.short_name[0]) { const char *shortName = owner.short_name; int shortNameW = UIRenderer::measureStringWithEmotes(display, shortName); - int shortNameX = x + SCREEN_WIDTH - shortNameW; - UIRenderer::drawStringWithEmotes(display, shortNameX, y + FONT_HEIGHT_SMALL, shortName, FONT_HEIGHT_SMALL, 1, false); + int shortNameX = x + SCREEN_WIDTH - shortNameW - 5; + UIRenderer::drawStringWithEmotes(display, shortNameX, y + 5 + FONT_HEIGHT_SMALL, shortName, FONT_HEIGHT_SMALL, 1, false); + if (gBootSplashBoldPass) { + UIRenderer::drawStringWithEmotes(display, shortNameX + 1, y + 5 + FONT_HEIGHT_SMALL, shortName, FONT_HEIGHT_SMALL, 1, + false); + } } screen->forceDisplay(); @@ -1201,6 +1539,20 @@ void UIRenderer::drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLED #endif } +void UIRenderer::drawBootIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ +#if GRAPHICS_TFT_COLORING_ENABLED + // Meshtastic brand green background with black foreground text/icon on TFT startup screen. + static constexpr uint16_t kMeshtasticGreen = TFTPalette::rgb565(103, 234, 145); + setAndRegisterTFTColorRole(TFTColorRole::BootSplash, TFTPalette::Black, kMeshtasticGreen, x, y, SCREEN_WIDTH, SCREEN_HEIGHT); + gBootSplashBoldPass = true; +#endif + drawIconScreen(upperMsg, display, state, x, y); +#if GRAPHICS_TFT_COLORING_ENABLED + gBootSplashBoldPass = false; +#endif +} + // **************************** // * My Position Screen * // **************************** @@ -1328,45 +1680,23 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU int16_t compassRadius = usableHeight / 2; if (compassRadius < 8) compassRadius = 8; - const int16_t compassDiam = compassRadius * 2; const int16_t compassX = x + SCREEN_WIDTH - compassRadius - 8; // Center vertically and nudge down slightly to keep "N" clear of header const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2; - display->drawCircle(compassX, compassY, compassRadius); - if (validHeading) { - CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, -heading); - - // "N" label - float northAngle = 0; - if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) - northAngle = -heading; - float radius = compassRadius; - int16_t nX = compassX + (radius - 1) * sin(northAngle); - int16_t nY = compassY - (radius - 1) * cos(northAngle); - int16_t nLabelWidth = display->getStringWidth("N") + 2; - int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1; - - display->setColor(BLACK); - display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox); - display->setColor(WHITE); - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N"); - } else { - drawCompassStatusText(display, compassX, compassY, statusLine1, statusLine2); - } + drawDetailedCompassOrStatus(display, compassX, compassY, compassRadius, validHeading, heading, statusLine1, + statusLine2); } else { // Portrait or square: put compass at the bottom and centered, scaled to fit available space // For E-Ink screens, account for navigation bar at the bottom! - int yBelowContent = textPos[5] + FONT_HEIGHT_SMALL + 2; - const int margin = 4; - int availableHeight = + const int yBelowContent = textPos[5] + FONT_HEIGHT_SMALL + 2; #if defined(USE_EINK) - SCREEN_HEIGHT - yBelowContent - 24; // Leave extra space for nav bar on E-Ink + const int margin = 4; + int availableHeight = SCREEN_HEIGHT - yBelowContent - 24; // Leave extra space for nav bar on E-Ink #else - SCREEN_HEIGHT - yBelowContent - margin; + const int margin = 4; + int availableHeight = SCREEN_HEIGHT - yBelowContent - margin; #endif if (availableHeight < FONT_HEIGHT_SMALL * 2) @@ -1381,29 +1711,8 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU int compassX = x + SCREEN_WIDTH / 2; int compassY = yBelowContent + availableHeight / 2; - display->drawCircle(compassX, compassY, compassRadius); - if (validHeading) { - CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, -heading); - - // "N" label - float northAngle = 0; - if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) - northAngle = -heading; - float radius = compassRadius; - int16_t nX = compassX + (radius - 1) * sin(northAngle); - int16_t nY = compassY - (radius - 1) * cos(northAngle); - int16_t nLabelWidth = display->getStringWidth("N") + 2; - int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1; - - display->setColor(BLACK); - display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox); - display->setColor(WHITE); - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N"); - } else { - drawCompassStatusText(display, compassX, compassY, statusLine1, statusLine2); - } + drawDetailedCompassOrStatus(display, compassX, compassY, compassRadius, validHeading, heading, statusLine1, + statusLine2); } } #endif @@ -1475,18 +1784,21 @@ void UIRenderer::drawOEMBootScreen(OLEDDisplay *display, OLEDDisplayUiState *sta #endif // Navigation bar overlay implementation -static int8_t lastFrameIndex = -1; +static int16_t lastFrameIndex = -1; static uint32_t lastFrameChangeTime = 0; constexpr uint32_t ICON_DISPLAY_DURATION_MS = 2000; // cppcheck-suppress constParameterPointer; signature must match OverlayCallback typedef from OLEDDisplayUi library void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state) { - int currentFrame = state->currentFrame; + uint8_t frameToHighlight = state->currentFrame; + if (state->frameState == IN_TRANSITION && state->transitionFrameTarget < screen->indicatorIcons.size()) { + frameToHighlight = state->transitionFrameTarget; + } // Detect frame change and record time - if (currentFrame != lastFrameIndex) { - lastFrameIndex = currentFrame; + if (frameToHighlight != lastFrameIndex) { + lastFrameIndex = frameToHighlight; lastFrameChangeTime = millis(); } @@ -1505,15 +1817,15 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta usableWidth = iconSize; const size_t iconsPerPage = usableWidth / (iconSize + spacing); - const size_t currentPage = currentFrame / iconsPerPage; + const size_t currentPage = frameToHighlight / iconsPerPage; const size_t pageStart = currentPage * iconsPerPage; const size_t pageEnd = min(pageStart + iconsPerPage, totalIcons); const int totalWidth = (pageEnd - pageStart) * iconSize + (pageEnd - pageStart - 1) * spacing; const int xStart = (SCREEN_WIDTH - totalWidth) / 2; - bool navBarVisible = millis() - lastFrameChangeTime <= ICON_DISPLAY_DURATION_MS; - int y = navBarVisible ? (SCREEN_HEIGHT - iconSize - 1) : SCREEN_HEIGHT; + const bool navBarVisible = millis() - lastFrameChangeTime <= ICON_DISPLAY_DURATION_MS; + const int y = navBarVisible ? (SCREEN_HEIGHT - iconSize - 1) : SCREEN_HEIGHT; #if defined(USE_EINK) // Only show bar briefly after switching frames @@ -1544,25 +1856,54 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta // Pre-calculate bounding rect const int rectX = xStart - 2 - bigOffset; + const int rectY = y - 2; const int rectWidth = totalWidth + 4 + (bigOffset * 2); const int rectHeight = iconSize + 6; // Clear background and draw border display->setColor(BLACK); - display->fillRect(rectX + 1, y - 2, rectWidth - 2, rectHeight - 2); +#if GRAPHICS_TFT_COLORING_ENABLED + // NavigationBar and NavigationArrow roles are fully defined in the theme table. + // We must call setTFTColorRole() before registerTFTColorRegion() because + // registerTFTColorRegion() snapshots colors from the roleColors[] working array, + // and loadThemeDefaults() isn't guaranteed to have run since boot. + const TFTThemeDef &theme = getActiveTheme(); + const auto &navBarRole = theme.roles[static_cast(TFTColorRole::NavigationBar)]; + const auto &navArrowRole = theme.roles[static_cast(TFTColorRole::NavigationArrow)]; + + setAndRegisterTFTColorRole(TFTColorRole::NavigationBar, navBarRole.onColor, navBarRole.offColor, rectX, rectY, rectWidth, + rectHeight); + setTFTColorRole(TFTColorRole::NavigationArrow, navArrowRole.onColor, navArrowRole.offColor); + display->fillRect(rectX, rectY, rectWidth, rectHeight); +#else + // Keep legacy OLED behavior untouched. + display->fillRect(rectX + 1, rectY, rectWidth - 2, rectHeight - 2); + display->setColor(WHITE); + display->drawRect(rectX, rectY, rectWidth, rectHeight); +#endif + + // Icons are 1-bit glyphs and must be drawn with WHITE to set pixels. display->setColor(WHITE); - display->drawRect(rectX, y - 2, rectWidth, rectHeight); // Icon drawing loop for the current page for (size_t i = pageStart; i < pageEnd; ++i) { const uint8_t *icon = screen->indicatorIcons[i]; const int x = xStart + (i - pageStart) * (iconSize + spacing); - const bool isActive = (i == static_cast(currentFrame)); + const bool isActive = (i == static_cast(frameToHighlight)); if (isActive) { +#if GRAPHICS_TFT_COLORING_ENABLED + // Active icon inverts on TFT: white chip with black glyph. + // Keep the buffer visibly different too, so dirty-rect updates include this region. + registerTFTColorRegion(TFTColorRole::NavigationBar, x - 1, y - 1, iconSize + 2, iconSize + 2); + display->setColor(WHITE); + display->fillRect(x - 1, y - 1, iconSize + 2, iconSize + 2); + display->setColor(BLACK); +#else display->setColor(WHITE); display->fillRect(x - 2, y - 2, iconSize + 4, iconSize + 4); display->setColor(BLACK); +#endif } if (currentResolution == ScreenResolution::High) { @@ -1576,22 +1917,17 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta } } - // Compact arrow drawer + display->setColor(WHITE); + + const int offset = (currentResolution == ScreenResolution::High) ? 3 : 1; + const int halfH = rectHeight / 2; + const int top = rectY + (rectHeight - halfH) / 2; + const int bottom = top + halfH - 1; + const int midY = top + (halfH / 2); + const int maxW = 4; + auto drawArrow = [&](bool rightSide) { - display->setColor(WHITE); - - const int offset = (currentResolution == ScreenResolution::High) ? 3 : 1; - const int halfH = rectHeight / 2; - - const int top = (y - 2) + (rectHeight - halfH) / 2; - const int bottom = top + halfH - 1; - const int midY = top + (halfH / 2); - - const int maxW = 4; - - // Determine left X coordinate - int baseX = rightSide ? (rectX + rectWidth + offset) : // right arrow - (rectX - offset - 1); // left arrow + int baseX = rightSide ? (rectX + rectWidth + offset) : (rectX - offset - 1); for (int yy = top; yy <= bottom; yy++) { int dist = abs(yy - midY); @@ -1606,21 +1942,43 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta } } }; + // Right arrow - if (pageEnd < totalIcons) { + if (navBarVisible && pageEnd < totalIcons) { + int baseX = rectX + rectWidth + offset; + int regionX = baseX; + +#if GRAPHICS_TFT_COLORING_ENABLED + registerTFTColorRegion(TFTColorRole::NavigationArrow, regionX, top, maxW, halfH); +#endif + drawArrow(true); } // Left arrow - if (pageStart > 0) { + if (navBarVisible && pageStart > 0) { + int baseX = rectX - offset - 1; + int regionX = baseX - maxW + 1; + +#if GRAPHICS_TFT_COLORING_ENABLED + registerTFTColorRegion(TFTColorRole::NavigationArrow, regionX, top, maxW, halfH); +#endif + drawArrow(false); } // Knock the corners off the square +#if GRAPHICS_TFT_COLORING_ENABLED + // TFT corner mask + registerTFTColorRegion(TFTColorRole::NavigationArrow, rectX, rectY, 1, 1); + registerTFTColorRegion(TFTColorRole::NavigationArrow, rectX + rectWidth - 1, rectY, 1, 1); +#else + // monochrome styling only display->setColor(BLACK); - display->drawRect(rectX, y - 2, 1, 1); - display->drawRect(rectX + rectWidth - 1, y - 2, 1, 1); + display->drawRect(rectX, rectY, 1, 1); + display->drawRect(rectX + rectWidth - 1, rectY, 1, 1); display->setColor(WHITE); +#endif } void UIRenderer::drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *message) diff --git a/src/graphics/draw/UIRenderer.h b/src/graphics/draw/UIRenderer.h index a705d944d..0aeace42e 100644 --- a/src/graphics/draw/UIRenderer.h +++ b/src/graphics/draw/UIRenderer.h @@ -54,6 +54,8 @@ class UIRenderer static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + static void drawBootIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + // Icon and screen drawing functions static void drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); diff --git a/src/graphics/niche/Drivers/EInk/ED047TC1.cpp b/src/graphics/niche/Drivers/EInk/ED047TC1.cpp new file mode 100644 index 000000000..2e283737c --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/ED047TC1.cpp @@ -0,0 +1,255 @@ +/* + + NicheGraphics parallel E-Ink driver for the LilyGo T5-S3-ePaper-Pro (ED047TC1). + + InkHUD buffer format : 1bpp, horizontal bytes, MSB = leftmost pixel, 1 = white + FastEPD buffer format: 1bpp, horizontal bytes, MSB = leftmost pixel, 1 = white + + Both formats share the same pixel layout and polarity (1 = white, 0 = black). + The InkHUD safe-area buffer (944×523) is copied into the centre of the physical + 960×540 FastEPD buffer so content clears the panel's inactive edge border. + See ED047TC1.h for the H_OFFSET_BYTES / V_OFFSET_TOP / V_OFFSET_BOTTOM constants. + +*/ + +// Ruler diagnostic — uncomment to draw calibration lines at each physical edge. +// #define EINK_EDGE_LINES + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS +#ifdef T5_S3_EPAPER_PRO + +#include "./ED047TC1.h" + +#include "FastEPD.h" +#include "configuration.h" + +using namespace NicheGraphics::Drivers; + +#if defined(T5_S3_EPAPER_PRO_V2) +// FastEPD helper symbols are defined in FastEPD.inl with C++ linkage. +extern void bbepPCA9535DigitalWrite(uint8_t pin, uint8_t value); +extern uint8_t bbepPCA9535DigitalRead(uint8_t pin); +extern int bbepI2CWrite(unsigned char iAddr, unsigned char *pData, int iLen); +extern int bbepI2CReadRegister(unsigned char iAddr, unsigned char u8Register, unsigned char *pData, int iLen); +#endif + +namespace +{ +#if defined(T5_S3_EPAPER_PRO_V2) +// FastEPD default V2 power callback blocks forever waiting for PWRGOOD. +// Replace it with a timeout-safe version so boot never deadlocks. +int safeEPDiyV7EinkPower(void *pBBEP, int bOn) +{ + static bool warnedPgood = false; + static bool warnedTpsPg = false; + static bool warnedTpsWrite = false; + + FASTEPDSTATE *pState = static_cast(pBBEP); + if (!pState) { + return BBEP_ERROR_BAD_PARAMETER; + } + + if (bOn == pState->pwr_on) { + return BBEP_SUCCESS; + } + + if (bOn) { + bbepPCA9535DigitalWrite(8, 1); // OE on + bbepPCA9535DigitalWrite(9, 1); // GMOD on + bbepPCA9535DigitalWrite(13, 1); // WAKEUP on + bbepPCA9535DigitalWrite(11, 1); // PWRUP on + bbepPCA9535DigitalWrite(12, 1); // VCOM CTRL on + delay(1); + + const uint32_t pgoodStart = millis(); + bool pgoodSeen = false; + while (!bbepPCA9535DigitalRead(14)) { // CFG_PIN_PWRGOOD + if ((millis() - pgoodStart) > 1200) { + if (!warnedPgood) { + LOG_WARN("ED047TC1: PWRGOOD timeout, continuing with fallback power-on path"); + warnedPgood = true; + } + break; + } + delay(1); + } + if (bbepPCA9535DigitalRead(14)) { + pgoodSeen = true; + } + + uint8_t ucTemp[4] = {0}; + ucTemp[0] = 0x01; // TPS_REG_ENABLE + ucTemp[1] = 0x3f; // enable rails + const int tpsEnableRc = bbepI2CWrite(0x68, ucTemp, 2); + + const int vcom = pState->iVCOM / -10; + ucTemp[0] = 3; // VCOM registers 3+4 (L + H) + ucTemp[1] = static_cast(vcom); + ucTemp[2] = static_cast(vcom >> 8); + const int tpsVcomRc = bbepI2CWrite(0x68, ucTemp, 3); + if ((tpsEnableRc == 0 || tpsVcomRc == 0) && !warnedTpsWrite) { + LOG_WARN("ED047TC1: TPS write did not ACK, continuing with fallback"); + warnedTpsWrite = true; + } + + int iTimeout = 0; + uint8_t u8Value = 0; + while (iTimeout < 220 && ((u8Value & 0xfa) != 0xfa)) { + bbepI2CReadRegister(0x68, 0x0F, &u8Value, 1); // TPS_REG_PG + iTimeout++; + delay(1); + } + if (iTimeout >= 220 && !warnedTpsPg) { + if (pgoodSeen) { + LOG_WARN("ED047TC1: TPS power-good register timeout, panel may still work"); + } else { + LOG_WARN("ED047TC1: TPS power-good register timeout after PWRGOOD fallback"); + } + warnedTpsPg = true; + } + + pState->pwr_on = 1; + } else { + bbepPCA9535DigitalWrite(8, 0); // OE off + bbepPCA9535DigitalWrite(9, 0); // GMOD off + bbepPCA9535DigitalWrite(11, 0); // PWRUP off + bbepPCA9535DigitalWrite(12, 0); // VCOM CTRL off + delay(1); + bbepPCA9535DigitalWrite(13, 0); // WAKEUP off + pState->pwr_on = 0; + } + + return BBEP_SUCCESS; +} +#endif + +class SafeFastEPD : public FASTEPD +{ + public: + void installSafePowerHandler() + { +#if defined(T5_S3_EPAPER_PRO_V2) + _state.pfnEinkPower = safeEPDiyV7EinkPower; +#endif + } +}; +} // namespace + +void ED047TC1::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst) +{ + // Parallel display — SPI parameters are not used + (void)spi; + (void)pin_dc; + (void)pin_cs; + (void)pin_busy; + (void)pin_rst; + + SafeFastEPD *safeEpaper = new SafeFastEPD; + epaper = safeEpaper; + + int initRc = BBEP_ERROR_BAD_PARAMETER; +#if defined(T5_S3_EPAPER_PRO_V1) + initRc = epaper->initPanel(BB_PANEL_LILYGO_T5PRO, 28000000); +#elif defined(T5_S3_EPAPER_PRO_V2) + initRc = epaper->initPanel(BB_PANEL_LILYGO_T5PRO_V2, 28000000); + // Initialize all PCA9535 port-0 pins as outputs / HIGH + for (int i = 0; i < 8; i++) { + epaper->ioPinMode(i, OUTPUT); + epaper->ioWrite(i, HIGH); + } + // On this board, the physical side key is labeled IO48; electrically it maps to PCA9535 IO12 (bit 2 on port-1). + // FastEPD's generic V7 init drives 8..13 as outputs; force IO12 back to input + // so variant touch-control polling can read the key reliably. + epaper->ioPinMode(10, INPUT); +#else +#error "ED047TC1 driver: unsupported variant — define T5_S3_EPAPER_PRO_V1 or T5_S3_EPAPER_PRO_V2" +#endif + + if (initRc != BBEP_SUCCESS) { + LOG_ERROR("ED047TC1 initPanel failed rc=%d", initRc); + return; + } + + safeEpaper->installSafePowerHandler(); + + const int modeRc = epaper->setMode(BB_MODE_1BPP); + if (modeRc != BBEP_SUCCESS) { + LOG_WARN("ED047TC1 setMode failed rc=%d", modeRc); + } + + const int clearRc = epaper->clearWhite(); + if (clearRc != BBEP_SUCCESS) { + LOG_WARN("ED047TC1 clearWhite failed rc=%d", clearRc); + } + + const int fullRc = epaper->fullUpdate(true); // Blocking initial clear + if (fullRc != BBEP_SUCCESS) { + LOG_WARN("ED047TC1 initial fullUpdate failed rc=%d", fullRc); + } +} + +void ED047TC1::update(uint8_t *imageData, UpdateTypes type) +{ + if (!epaper) + return; + + // InkHUD renders into a DISPLAY_WIDTH × DISPLAY_HEIGHT safe-area buffer. + // We need to place that into the centre of the physical 960×540 FastEPD buffer, + // leaving blank margins at every edge to avoid the panel's inactive border. + const uint32_t srcRowBytes = (DISPLAY_WIDTH + 7) / 8; // bytes per row in InkHUD buffer (118) + const uint32_t dstRowBytes = (960 + 7) / 8; // bytes per row in physical buffer (120) + const uint32_t dstTotalRows = 540; + + uint8_t *cur = epaper->currentBuffer(); + + // Fill physical buffer with white (0xFF = white in FastEPD 1bpp) + memset(cur, 0xFF, dstRowBytes * dstTotalRows); + + // Copy each InkHUD row into the physical buffer with horizontal + vertical offsets + for (uint32_t row = 0; row < DISPLAY_HEIGHT; row++) { + const uint8_t *srcRow = imageData + row * srcRowBytes; + uint8_t *dstRow = cur + (row + V_OFFSET_TOP) * dstRowBytes + H_OFFSET_BYTES; + memcpy(dstRow, srcRow, srcRowBytes); + } + +#ifdef EINK_EDGE_LINES + // Draw a 1px black box at the exact boundary of the safe area within the + // physical buffer. If the margins are correct, all 4 lines should be + // fully visible and right at the edge of the usable display area. + + auto setPixelBlack = [&](uint32_t col, uint32_t row) { cur[row * dstRowBytes + col / 8] &= ~(0x80 >> (col % 8)); }; + + const uint32_t safeX = H_OFFSET_BYTES * 8; + const uint32_t safeY = V_OFFSET_TOP; + const uint32_t safeW = DISPLAY_WIDTH; + const uint32_t safeH = DISPLAY_HEIGHT; + + // Top edge: horizontal line at safeY + for (uint32_t col = safeX; col < safeX + safeW; col++) + setPixelBlack(col, safeY); + + // Bottom edge: horizontal line at safeY + safeH - 1 + for (uint32_t col = safeX; col < safeX + safeW; col++) + setPixelBlack(col, safeY + safeH - 1); + + // Left edge: vertical line at safeX + for (uint32_t row = safeY; row < safeY + safeH; row++) + setPixelBlack(safeX, row); + + // Right edge: vertical line at safeX + safeW - 1 + for (uint32_t row = safeY; row < safeY + safeH; row++) + setPixelBlack(safeX + safeW - 1, row); +#endif + + if (type == FULL) { + epaper->fullUpdate(CLEAR_SLOW, false); + epaper->backupPlane(); // Sync pPrevious so next partialUpdate has a correct baseline + } else { + // FAST: true partial update - compares pCurrent vs pPrevious and only applies + // update waveform to rows that changed. partialUpdate() updates pPrevious. + epaper->partialUpdate(false, 0, dstTotalRows - 1); + } +} + +#endif // T5_S3_EPAPER_PRO +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS diff --git a/src/graphics/niche/Drivers/EInk/ED047TC1.h b/src/graphics/niche/Drivers/EInk/ED047TC1.h new file mode 100644 index 000000000..3540481e7 --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/ED047TC1.h @@ -0,0 +1,90 @@ +/* + + E-Ink display driver adapter + - ED047TC1 (via FastEPD library) + - Manufacturer: E Ink / used in LilyGo T5-E-Paper-S3-Pro + - Size: 4.7 inch + - Physical resolution: 960px x 540px + - Interface: 8-bit parallel (NOT SPI) + + Unlike the other NicheGraphics EInk drivers, this one drives a parallel e-paper + panel via the FastEPD library. SPI parameters passed to begin() are ignored. + + The ED047TC1 panel has an inactive pixel border on all four edges (~4–8 physical + pixels). DISPLAY_WIDTH / DISPLAY_HEIGHT expose a reduced "safe area" to InkHUD so + that content is never drawn into this dead zone. The update() method copies the + InkHUD frame buffer into the centre of the larger physical 960×540 buffer, using + H_OFFSET_BYTES (horizontal, whole bytes = 8 pixels per byte), + V_OFFSET_TOP and V_OFFSET_BOTTOM (vertical, pixel rows) to position it. + + Changing these constants shifts content inward from each physical edge: + H_OFFSET_BYTES = 1 → 8px left margin, 8px right margin (960 – 8 – 8 = 944) + V_OFFSET_TOP = 9 → 9px top margin (asymmetric: top ≠ bottom) + V_OFFSET_BOTTOM = 8 → 8px bottom margin (540 – 9 – 8 = 523) + +*/ + +#pragma once + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +#include "configuration.h" + +#include "./EInk.h" + +// Forward declare to avoid pulling FastEPD into all translation units +class FASTEPD; + +namespace NicheGraphics::Drivers +{ + +class ED047TC1 : public EInk +{ + // Safe-area dimensions exposed to InkHUD (physical panel is 960×540). + // + // The ED047TC1 has an inactive pixel border on all physical edges. + // The physical buffer coordinates do NOT directly match the visual orientation + // due to FastEPD's portrait scan direction and InkHUD's rotation=3 (270° CW): + // + // Physical buffer Visual on device (rotation=3) + // ───────────────── ────────────────────────────── + // Physical LEFT cols → Visual TOP edge + // Physical RIGHT cols → Visual BOTTOM edge + // Physical TOP rows → Visual RIGHT edge + // Physical BOTTOM rows → Visual LEFT edge + // + // Offset constants shift the InkHUD safe-area away from each physical dead zone: + // H_OFFSET_BYTES : whole bytes from physical left (8px per byte, affects visual TOP) + // Physical right margin = 960 − H_OFFSET_BYTES×8 − DISPLAY_WIDTH (affects visual BOTTOM) + // V_OFFSET_TOP : pixel rows from physical top (affects visual RIGHT) + // V_OFFSET_BOTTOM: pixel rows from physical bottom (affects visual LEFT) + // + // Calibrated by flashing a 1px border box and adjusting until all 4 sides are visible. + + static constexpr uint16_t DISPLAY_WIDTH = 944; // 960 − H_OFFSET_BYTES×8 − right_margin (8+8 = 16px) + static constexpr uint16_t DISPLAY_HEIGHT = 523; // 540 − V_OFFSET_TOP − V_OFFSET_BOTTOM (9+8 = 17px) + + static constexpr uint8_t H_OFFSET_BYTES = 1; // visual TOP : 8px physical left margin + // visual BOTTOM: 960−8−944=8px physical right margin + static constexpr uint8_t V_OFFSET_TOP = 9; // visual RIGHT : CONFIRMED OK + static constexpr uint8_t V_OFFSET_BOTTOM = 8; // visual LEFT : 8px physical bottom margin + + static constexpr UpdateTypes supported = static_cast(FULL | FAST); + + public: + ED047TC1() : EInk(DISPLAY_WIDTH, DISPLAY_HEIGHT, supported) {} + + // EInk interface — SPI params are not used for this parallel display + void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst = 0xFF) override; + void update(uint8_t *imageData, UpdateTypes type) override; + + protected: + bool isUpdateDone() override { return true; } // FastEPD updates are blocking + + private: + FASTEPD *epaper = nullptr; +}; + +} // namespace NicheGraphics::Drivers + +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS diff --git a/src/graphics/niche/Fonts/FreeSans18pt_Win1253.h b/src/graphics/niche/Fonts/FreeSans18pt_Win1253.h new file mode 100644 index 000000000..9b29f32b5 --- /dev/null +++ b/src/graphics/niche/Fonts/FreeSans18pt_Win1253.h @@ -0,0 +1,1467 @@ +// trunk-ignore-all(clang-format) +#pragma once +/* PROPERTIES + +FONT_NAME FreeSans18pt_Win1253 +*/ +const uint8_t FreeSans18pt_Win1253Bitmaps[] PROGMEM = { + 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x6C, 0x00, 0x00, 0x00, 0x23, 0x00, + 0x00, 0x00, 0x10, 0x80, 0x00, 0x00, 0x08, 0x40, 0x00, 0x00, 0x04, 0x30, + 0x00, 0x00, 0x02, 0x18, 0x00, 0x00, 0x01, 0x08, 0x00, 0x00, 0x01, 0x84, + 0x00, 0x00, 0x01, 0x82, 0x00, 0x00, 0x01, 0x83, 0x00, 0x00, 0x01, 0x81, + 0x80, 0x00, 0x03, 0x80, 0xBF, 0xC0, 0x03, 0x00, 0x7C, 0x30, 0x03, 0x00, + 0x60, 0x18, 0x01, 0x00, 0x20, 0x04, 0x01, 0x80, 0x10, 0x06, 0x7F, 0xC0, + 0x0C, 0x06, 0x30, 0x00, 0x07, 0xFF, 0xD8, 0x00, 0x03, 0xE0, 0x2C, 0x00, + 0x01, 0x80, 0x1E, 0x00, 0x01, 0xC0, 0x0F, 0x00, 0x01, 0xA0, 0x05, 0x80, + 0x03, 0x9C, 0x06, 0xC0, 0x03, 0x07, 0xFE, 0x60, 0x00, 0x06, 0x01, 0xB0, + 0x00, 0x03, 0x00, 0x58, 0x00, 0x01, 0x80, 0x2C, 0x00, 0x00, 0xC0, 0x16, + 0x00, 0x00, 0x3E, 0x1B, 0xF8, 0x00, 0x3F, 0xF9, 0xFF, 0x80, 0x10, 0x10, + 0x00, 0x60, 0x08, 0x08, 0x00, 0x1E, 0x06, 0x04, 0x00, 0x03, 0xFF, 0xFE, + 0x00, 0x00, 0x06, 0x1E, 0x00, 0x00, 0x00, 0x79, 0xF0, 0x00, 0x07, 0xFF, + 0xEC, 0x00, 0x0E, 0x03, 0x02, 0x00, 0x0C, 0x01, 0x01, 0x0F, 0xFC, 0x00, + 0x80, 0x87, 0xE0, 0x00, 0x7F, 0xF3, 0x00, 0x00, 0x1E, 0x0D, 0x80, 0x00, + 0x18, 0x02, 0xC0, 0x00, 0x0C, 0x01, 0x60, 0x00, 0x06, 0x00, 0xB0, 0x00, + 0x03, 0x80, 0xD8, 0x00, 0x60, 0xFF, 0xCC, 0x00, 0x1C, 0xC0, 0x36, 0x00, + 0x03, 0x40, 0x0B, 0x00, 0x00, 0xE0, 0x07, 0x80, 0x00, 0x38, 0x03, 0xC0, + 0x00, 0x1F, 0x81, 0x60, 0x00, 0x07, 0xFF, 0xBF, 0xE0, 0x06, 0x01, 0x00, + 0x30, 0x02, 0x00, 0xC0, 0x08, 0x01, 0x00, 0x20, 0x06, 0x00, 0xC0, 0x30, + 0x01, 0x80, 0x3F, 0x18, 0x00, 0x70, 0x13, 0xF8, 0x00, 0x0C, 0x0C, 0x00, + 0x00, 0x03, 0x06, 0x00, 0x00, 0x00, 0xC1, 0x00, 0x00, 0x00, 0x30, 0x80, + 0x00, 0x00, 0x08, 0x40, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0x02, 0x18, + 0x00, 0x00, 0x01, 0x08, 0x00, 0x00, 0x00, 0x84, 0x00, 0x00, 0x00, 0x46, + 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x07, + 0xFC, 0x00, 0x00, 0x07, 0x80, 0xE0, 0x00, 0x01, 0x80, 0x03, 0x00, 0x00, + 0xC0, 0x00, 0x18, 0x00, 0x20, 0x00, 0x01, 0x80, 0x08, 0x00, 0x00, 0x08, + 0x02, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x18, 0x10, 0x00, 0x00, + 0x01, 0x04, 0x00, 0x00, 0x00, 0x10, 0x80, 0x00, 0x00, 0x02, 0x20, 0x00, + 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x04, 0x80, 0x70, 0x07, 0x00, 0xA0, + 0x31, 0x01, 0x18, 0x0C, 0x04, 0x30, 0x61, 0x01, 0x80, 0x00, 0x00, 0x00, + 0x30, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xA8, 0x00, + 0x54, 0x18, 0x2A, 0x00, 0x12, 0x83, 0x05, 0x40, 0x02, 0xA0, 0x50, 0x00, + 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x02, 0x40, 0x10, 0x01, 0x00, 0x44, + 0x03, 0x80, 0xE0, 0x10, 0x80, 0x0F, 0xE0, 0x02, 0x08, 0x00, 0x00, 0x00, + 0x81, 0x00, 0x00, 0x00, 0x30, 0x10, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, + 0x01, 0x00, 0x10, 0x00, 0x00, 0x40, 0x01, 0x80, 0x00, 0x30, 0x00, 0x0C, + 0x00, 0x18, 0x00, 0x00, 0xF0, 0x1E, 0x00, 0x00, 0x03, 0xFE, 0x00, 0x00, + 0x00, 0x00, 0x7F, 0xC0, 0x00, 0x00, 0x00, 0x70, 0x1C, 0x00, 0x00, 0x00, + 0x60, 0x00, 0xC0, 0x00, 0x00, 0x60, 0x00, 0x0C, 0x00, 0x00, 0x30, 0x00, + 0x01, 0x80, 0x00, 0x10, 0x00, 0x00, 0x10, 0x00, 0x08, 0x00, 0x00, 0x02, + 0x00, 0x06, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x0C, 0x00, 0x06, 0x10, 0x00, + 0x86, 0x00, 0x00, 0xC2, 0x00, 0x22, 0x00, 0x00, 0x08, 0x80, 0x11, 0x00, + 0x00, 0x01, 0x10, 0x04, 0x40, 0x00, 0x00, 0x44, 0x01, 0x03, 0xE0, 0x0F, + 0x81, 0x00, 0x81, 0x8C, 0x06, 0x30, 0x20, 0x20, 0x41, 0x01, 0x04, 0x08, + 0x08, 0x00, 0x00, 0x00, 0x02, 0x03, 0xE0, 0x00, 0x00, 0x0F, 0x83, 0x98, + 0x00, 0x00, 0x02, 0x31, 0x86, 0x00, 0x00, 0x00, 0xC2, 0xC1, 0x00, 0x00, + 0x00, 0x10, 0xE0, 0x4F, 0x80, 0x03, 0xE4, 0x18, 0x32, 0x1F, 0xFF, 0x09, + 0x06, 0x08, 0xE0, 0x00, 0x0E, 0x61, 0x4E, 0x1F, 0xFF, 0xFF, 0x0C, 0x8F, + 0x87, 0xFF, 0xFF, 0xC3, 0xC0, 0x20, 0xFF, 0xFF, 0xE0, 0x80, 0x0C, 0x0F, + 0x83, 0xE0, 0x60, 0x01, 0x81, 0xC0, 0x70, 0x30, 0x00, 0x20, 0x0F, 0xE0, + 0x18, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0xC0, 0x00, 0x06, 0x00, + 0x00, 0x18, 0x00, 0x03, 0x00, 0x00, 0x03, 0x80, 0x03, 0x00, 0x00, 0x00, + 0x3C, 0x07, 0x80, 0x00, 0x00, 0x01, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x0C, + 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x08, 0x40, 0x00, 0x00, 0x71, 0xC0, 0x00, 0x03, 0x9F, 0x0C, 0x00, 0x00, + 0xFA, 0x30, 0x07, 0x00, 0x11, 0xC3, 0x01, 0xB0, 0x02, 0x18, 0x30, 0x62, + 0x00, 0x61, 0x83, 0x08, 0xC0, 0x06, 0x18, 0x31, 0x18, 0x01, 0xE1, 0x83, + 0x23, 0x00, 0x66, 0x18, 0x34, 0x20, 0x08, 0x61, 0x83, 0x84, 0x01, 0x86, + 0x18, 0x38, 0xC0, 0x18, 0x61, 0x83, 0x08, 0x03, 0x86, 0x10, 0x41, 0x00, + 0xF8, 0x60, 0x18, 0x30, 0x31, 0x86, 0x02, 0x02, 0x06, 0x18, 0x40, 0x40, + 0x60, 0xC1, 0x80, 0x08, 0x04, 0x0C, 0x18, 0x01, 0x00, 0x80, 0xC1, 0x00, + 0x20, 0x18, 0x0C, 0x00, 0x06, 0x01, 0x10, 0xC0, 0x00, 0xC0, 0x23, 0x8C, + 0x00, 0x0C, 0x06, 0x10, 0xC0, 0x00, 0x01, 0xE0, 0x0C, 0x00, 0x00, 0x37, + 0x00, 0xC0, 0x00, 0x04, 0x60, 0x0C, 0x00, 0x01, 0x80, 0x00, 0xC0, 0x00, + 0x20, 0x00, 0x0C, 0x00, 0x0C, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x07, + 0x00, 0xC0, 0x00, 0x00, 0x7F, 0xF0, 0x00, 0x00, 0x01, 0xF0, 0x00, 0x00, + 0x00, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x80, 0x00, 0x00, 0x00, 0xD0, 0x00, + 0x00, 0xE0, 0x12, 0x00, 0x00, 0x16, 0x04, 0x60, 0x00, 0x02, 0x71, 0x8C, + 0x00, 0x00, 0x63, 0x20, 0x80, 0x00, 0x0C, 0x1F, 0xD0, 0x78, 0x00, 0x8E, + 0x0F, 0xFD, 0x00, 0x12, 0x00, 0x30, 0x60, 0x02, 0x80, 0x02, 0x08, 0x00, + 0x60, 0x00, 0x22, 0x00, 0x7C, 0x00, 0x06, 0xC0, 0xFD, 0x00, 0x00, 0x50, + 0x30, 0x20, 0x00, 0x0C, 0x06, 0x08, 0x00, 0x00, 0x80, 0x71, 0x00, 0x00, + 0x18, 0x03, 0x20, 0x00, 0x02, 0xC0, 0x3C, 0x00, 0x00, 0x4C, 0x01, 0x80, + 0x00, 0x08, 0x60, 0x10, 0x00, 0x01, 0x06, 0x07, 0x00, 0x00, 0x40, 0xC0, + 0xA0, 0x00, 0x0B, 0xF0, 0x36, 0x00, 0x03, 0xE0, 0x04, 0x40, 0x00, 0x60, + 0x01, 0x04, 0x00, 0x14, 0x00, 0x60, 0xC0, 0x04, 0x80, 0x0B, 0xFF, 0x07, + 0x10, 0x01, 0xF0, 0xBF, 0xC3, 0x00, 0x00, 0x10, 0x4C, 0x60, 0x00, 0x03, + 0x18, 0xE4, 0x00, 0x00, 0x62, 0x06, 0x80, 0x00, 0x0C, 0xC0, 0x70, 0x00, + 0x00, 0xB0, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x03, 0x80, 0x00, + 0x00, 0x00, 0x00, 0x1F, 0xC0, 0x00, 0x00, 0x03, 0x83, 0x80, 0x00, 0x00, + 0x30, 0x06, 0x00, 0x00, 0x03, 0x00, 0x18, 0x00, 0x00, 0x30, 0x00, 0x60, + 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x18, 0x00, 0x0C, 0x00, 0x07, 0xC0, + 0x00, 0x60, 0x00, 0xC3, 0x80, 0x01, 0x00, 0x0C, 0x04, 0x00, 0x08, 0x00, + 0xC0, 0x30, 0x00, 0x40, 0x04, 0x00, 0x80, 0x02, 0x00, 0x20, 0x04, 0x00, + 0x18, 0x0F, 0x00, 0x00, 0x01, 0xF1, 0xC0, 0x00, 0x00, 0x00, 0xC8, 0x00, + 0x00, 0x00, 0x03, 0x80, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, + 0x60, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, + 0x00, 0x03, 0x0F, 0xFF, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x30, 0x03, 0x00, 0x38, 0x03, 0x80, + 0x38, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x1E, 0x01, 0xE0, 0x1E, 0x00, 0xE0, + 0x0E, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x80, 0x30, 0x00, 0x00, 0x1C, 0x03, 0x80, 0x00, 0x01, 0xE0, + 0x3C, 0x00, 0x00, 0x0F, 0x01, 0xC0, 0x00, 0x00, 0x70, 0x0E, 0x00, 0x00, + 0x00, 0x00, 0x07, 0xF0, 0x00, 0x00, 0x00, 0x1C, 0x0C, 0x00, 0x00, 0x00, + 0x70, 0x02, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x01, 0x80, 0x00, + 0x80, 0x00, 0x01, 0x80, 0x00, 0x40, 0x00, 0x03, 0x00, 0x00, 0x40, 0x00, + 0x3F, 0x00, 0x00, 0x20, 0x00, 0xE1, 0xC0, 0x00, 0x20, 0x01, 0x80, 0x60, + 0x00, 0x20, 0x01, 0x00, 0x20, 0x00, 0x20, 0x02, 0x00, 0x10, 0x00, 0x20, + 0x02, 0x00, 0x10, 0x00, 0x20, 0x06, 0x00, 0x00, 0x00, 0x38, 0x1E, 0x00, + 0x00, 0x00, 0x0C, 0x70, 0x00, 0x00, 0x00, 0x02, 0x40, 0x00, 0x00, 0x00, + 0x03, 0x80, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00, 0x01, 0x80, + 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00, 0x01, 0x40, 0x00, 0x00, + 0x00, 0x02, 0x60, 0x00, 0x00, 0x00, 0x02, 0x18, 0x20, 0x00, 0x00, 0x0C, + 0x0F, 0xF0, 0x00, 0x00, 0x70, 0x00, 0x0F, 0xFC, 0x00, 0x80, 0x00, 0x03, + 0xE7, 0x03, 0x00, 0x00, 0x00, 0x01, 0xFC, 0x00, 0x01, 0xF0, 0x00, 0x1F, + 0x00, 0x0F, 0xFC, 0x01, 0xFF, 0x80, 0x39, 0xDC, 0x07, 0x3B, 0x80, 0xF2, + 0xDC, 0x1E, 0x5B, 0x83, 0xB4, 0xEC, 0x6E, 0x9D, 0x8D, 0x38, 0xCD, 0x93, + 0x19, 0x9E, 0x30, 0xCB, 0xA3, 0x19, 0x64, 0x31, 0x69, 0xC7, 0x3B, 0x8C, + 0x52, 0x71, 0x8B, 0x4F, 0x96, 0x94, 0x61, 0x93, 0x8F, 0xA7, 0x18, 0x63, + 0xA3, 0x0D, 0xC6, 0x18, 0xE4, 0xC3, 0x19, 0x86, 0x39, 0x68, 0x87, 0x31, + 0x8E, 0x5A, 0x71, 0xCB, 0x53, 0x96, 0x9C, 0x62, 0xD1, 0x25, 0xA7, 0x18, + 0x64, 0xE2, 0x69, 0xC6, 0x18, 0xE8, 0xC4, 0x70, 0x86, 0x39, 0x70, 0xD0, + 0xE1, 0x8E, 0x5A, 0x21, 0xE0, 0xE2, 0xD2, 0x9C, 0x62, 0x81, 0xA4, 0xE3, + 0x08, 0xB7, 0x01, 0x38, 0xC3, 0x19, 0x34, 0x01, 0x30, 0xC7, 0x2E, 0x30, + 0x01, 0x31, 0xCB, 0x4C, 0x40, 0x01, 0x72, 0xD3, 0x8D, 0x00, 0x03, 0xB4, + 0xE3, 0x1C, 0x00, 0x03, 0x38, 0xC3, 0x38, 0x00, 0x03, 0x30, 0xC7, 0x60, + 0x00, 0x01, 0x31, 0xCB, 0x00, 0x00, 0x01, 0x72, 0x54, 0x00, 0x00, 0x01, + 0xB4, 0x70, 0x00, 0x00, 0x01, 0x18, 0x40, 0x00, 0x00, 0x01, 0x93, 0x00, + 0x00, 0x00, 0x01, 0xBC, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, + 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x38, 0x00, + 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x07, 0xE0, 0x00, 0x00, 0x01, 0xFE, + 0x00, 0x00, 0x00, 0x3F, 0xE0, 0x00, 0x00, 0x0F, 0xFF, 0xC0, 0x00, 0x01, + 0xFF, 0xFE, 0x00, 0x00, 0x3F, 0xFF, 0xC0, 0x00, 0x07, 0xFF, 0xFC, 0x00, + 0x03, 0xFF, 0xFF, 0x80, 0x00, 0xFF, 0xFF, 0xF0, 0x00, 0x3F, 0xFF, 0xFE, + 0x00, 0x07, 0xFF, 0xFF, 0xE0, 0x01, 0xFF, 0xFF, 0xFE, 0x00, 0x3F, 0xFF, + 0xFF, 0xE0, 0x07, 0xE1, 0xF8, 0xFC, 0x00, 0xF8, 0x1E, 0x0F, 0x80, 0x3E, + 0x71, 0x98, 0xF8, 0x1F, 0xCF, 0x37, 0x9F, 0xC7, 0xF9, 0xC6, 0x63, 0xFC, + 0xFF, 0x01, 0xE0, 0x7F, 0xBF, 0xF0, 0x7C, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, + 0x03, 0xFF, 0xBF, 0xF0, 0x00, 0x7F, 0xE7, 0xFE, 0x00, 0x1F, 0xFC, 0x7F, + 0xE0, 0x03, 0xFF, 0x0F, 0xFE, 0x00, 0xFF, 0xC0, 0xFF, 0xF0, 0x7F, 0xF0, + 0x07, 0xFF, 0xFF, 0xFC, 0x00, 0x7F, 0xFF, 0xFF, 0x00, 0x03, 0xFF, 0xFF, + 0x00, 0x00, 0x07, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, + 0x22, 0x00, 0x00, 0x00, 0x0C, 0x20, 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, + 0x00, 0x20, 0x80, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x00, 0x7C, 0x00, + 0x00, 0x00, 0x3F, 0xE0, 0x00, 0x00, 0x1C, 0x07, 0x00, 0x00, 0x06, 0x00, + 0x30, 0x00, 0x01, 0x00, 0x03, 0x00, 0x00, 0x43, 0x00, 0x30, 0x00, 0x18, + 0xC0, 0x03, 0x00, 0x02, 0x30, 0x00, 0x20, 0x00, 0x44, 0x00, 0x04, 0x00, + 0x11, 0x80, 0x00, 0xC0, 0x02, 0x20, 0x00, 0x08, 0x00, 0x40, 0x00, 0x01, + 0x00, 0x08, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x04, 0x00, 0x20, 0x00, + 0x00, 0x80, 0x0C, 0x00, 0x00, 0x18, 0x01, 0x00, 0x00, 0x03, 0x00, 0x20, + 0x00, 0x00, 0x20, 0x0D, 0xFF, 0xFE, 0x04, 0x01, 0xE0, 0x00, 0x00, 0x40, + 0x40, 0x00, 0x00, 0x04, 0x10, 0x00, 0x00, 0x00, 0x44, 0x07, 0xFF, 0xFC, + 0x04, 0xFF, 0xF8, 0x0F, 0xFE, 0xBF, 0xFF, 0x01, 0xFF, 0xFF, 0xFF, 0xE0, + 0x3F, 0xFF, 0xFF, 0xFE, 0x0F, 0xFF, 0xC7, 0xFF, 0xC1, 0xFF, 0xF0, 0x3F, + 0xFC, 0x7F, 0xF8, 0x00, 0x3F, 0xFF, 0xE0, 0x00, 0x00, 0x07, 0xFC, 0x00, + 0x00, 0x07, 0x00, 0xF0, 0x00, 0x03, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x00, + 0x18, 0x00, 0x20, 0x00, 0x00, 0x80, 0x08, 0x00, 0x00, 0x08, 0x02, 0x00, + 0x00, 0x00, 0x80, 0x82, 0x00, 0x02, 0x08, 0x11, 0xC0, 0x00, 0x71, 0x04, + 0xE0, 0x00, 0x03, 0x11, 0x90, 0x00, 0x00, 0x33, 0x26, 0x00, 0x00, 0x03, + 0x24, 0x0E, 0x00, 0x0F, 0x05, 0x87, 0xF0, 0x07, 0xF0, 0x61, 0xCF, 0x01, + 0xE7, 0x0C, 0x09, 0x80, 0x0C, 0x81, 0x81, 0x30, 0x01, 0x90, 0x30, 0x26, + 0x00, 0x32, 0x06, 0x04, 0xC0, 0x06, 0x40, 0xC0, 0x98, 0xF8, 0xC8, 0x18, + 0x13, 0xFF, 0xF9, 0x03, 0x02, 0x70, 0x07, 0x20, 0xD0, 0x4C, 0x00, 0x64, + 0x12, 0x09, 0x80, 0x0C, 0x82, 0x61, 0x3F, 0xFF, 0x90, 0xC4, 0x27, 0xFF, + 0xF2, 0x10, 0xC4, 0xFF, 0xFE, 0x46, 0x08, 0x9F, 0xDF, 0xC8, 0x80, 0x93, + 0x00, 0x19, 0x30, 0x1A, 0x60, 0x03, 0x2C, 0x01, 0xCC, 0x00, 0x67, 0x80, + 0xC1, 0x80, 0x1C, 0x18, 0x10, 0x18, 0x03, 0x01, 0x03, 0x03, 0xC1, 0xE0, + 0x60, 0x20, 0x18, 0x30, 0x08, 0x06, 0x03, 0xFF, 0x02, 0x00, 0x3F, 0x80, + 0x3F, 0x80, 0x00, 0x01, 0xF0, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, + 0x24, 0x80, 0x00, 0x00, 0x1F, 0xE0, 0x00, 0x00, 0x06, 0xC8, 0x00, 0x00, + 0x01, 0x93, 0x00, 0x00, 0x00, 0x55, 0xC0, 0x00, 0x00, 0x1F, 0xF0, 0x00, + 0x00, 0x04, 0x44, 0x00, 0x00, 0x01, 0x11, 0x00, 0x00, 0x00, 0xC4, 0x40, + 0x00, 0x00, 0x31, 0x10, 0x00, 0x00, 0x0C, 0x46, 0x00, 0x00, 0x02, 0x11, + 0x80, 0x00, 0x00, 0x84, 0x20, 0x00, 0x00, 0x61, 0x08, 0x00, 0x00, 0x18, + 0x43, 0x00, 0x00, 0x04, 0x10, 0xC0, 0x00, 0x01, 0x04, 0x10, 0x00, 0x00, + 0x41, 0x04, 0x00, 0x00, 0x30, 0x41, 0x00, 0x00, 0x3C, 0x10, 0x78, 0x00, + 0x7E, 0x04, 0x0F, 0x80, 0x7A, 0x01, 0x01, 0xBC, 0x70, 0xC0, 0x40, 0x43, + 0xD0, 0x10, 0x10, 0x30, 0x10, 0x06, 0x0C, 0x0C, 0x00, 0x00, 0xC7, 0xC6, + 0x00, 0x00, 0x13, 0x1B, 0x00, 0x00, 0x07, 0x83, 0x80, 0x00, 0x00, 0xF1, + 0xE0, 0x00, 0x00, 0x1C, 0x70, 0x00, 0x00, 0x03, 0x18, 0x00, 0x00, 0x03, + 0x83, 0x80, 0x00, 0x01, 0x80, 0x30, 0x00, 0x00, 0x40, 0x04, 0x00, 0x00, + 0x07, 0xFC, 0x00, 0x00, 0x07, 0x80, 0xF0, 0x00, 0x01, 0x80, 0x03, 0x80, + 0x00, 0xC0, 0x00, 0x18, 0x00, 0x20, 0x00, 0x00, 0x80, 0x08, 0x00, 0x00, + 0x08, 0x02, 0x00, 0x00, 0x00, 0x80, 0x83, 0x00, 0x00, 0x08, 0x10, 0xE0, + 0x00, 0x01, 0x84, 0x30, 0x00, 0x00, 0x10, 0x8C, 0x00, 0x00, 0x03, 0x21, + 0x00, 0x00, 0x10, 0x24, 0x03, 0x00, 0x0E, 0x04, 0x80, 0xF0, 0x03, 0x00, + 0xE0, 0x1E, 0x01, 0xC0, 0x0C, 0x03, 0xC0, 0x3E, 0x01, 0x80, 0x30, 0x00, + 0xF0, 0x30, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, + 0x00, 0x1E, 0x18, 0x00, 0x07, 0x07, 0xE1, 0x00, 0x00, 0x30, 0xFF, 0x90, + 0x00, 0x02, 0x1F, 0xFA, 0x00, 0x00, 0x43, 0xFF, 0x40, 0x00, 0x30, 0x7F, + 0xE4, 0x00, 0x01, 0x0F, 0xFC, 0x80, 0x00, 0x20, 0xFF, 0x88, 0x00, 0x0C, + 0x3F, 0xE1, 0x80, 0x06, 0x07, 0xF0, 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0xE0, 0x01, 0x80, 0x00, 0x30, 0x00, + 0x0C, 0x00, 0x1C, 0x00, 0x00, 0x70, 0x1E, 0x00, 0x00, 0x01, 0xFE, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0xC0, 0x30, 0x00, 0xC0, 0xC0, + 0x00, 0x30, 0x30, 0x40, 0x30, 0x04, 0x04, 0x30, 0x04, 0x01, 0x00, 0x08, + 0x00, 0x00, 0x24, 0x02, 0x3C, 0x00, 0x09, 0x80, 0x99, 0x80, 0x02, 0x00, + 0x2C, 0x20, 0x00, 0x80, 0x06, 0x08, 0xF0, 0x20, 0x01, 0x86, 0x47, 0x18, + 0x1C, 0x3F, 0x10, 0x7C, 0x01, 0x0C, 0x04, 0x00, 0x00, 0x0F, 0x80, 0x80, + 0x00, 0x0C, 0x78, 0x21, 0x80, 0x02, 0x13, 0x08, 0x20, 0x01, 0x04, 0x7E, + 0x00, 0x00, 0x41, 0x1E, 0x00, 0x00, 0x30, 0x8D, 0x00, 0x0C, 0x0C, 0x66, + 0x23, 0x04, 0x07, 0x11, 0x08, 0x42, 0x01, 0x40, 0x41, 0x01, 0x80, 0x48, + 0x00, 0x40, 0x60, 0x32, 0x00, 0x7C, 0x10, 0x0C, 0x43, 0xE7, 0x8C, 0x07, + 0x88, 0x01, 0x3F, 0x01, 0x21, 0x00, 0x47, 0x80, 0xCC, 0x30, 0x20, 0x00, + 0x31, 0x83, 0xF0, 0x00, 0xCC, 0x38, 0xF0, 0x00, 0x05, 0x83, 0xF0, 0x04, + 0x01, 0x30, 0xE0, 0x01, 0x80, 0xC7, 0xE0, 0x00, 0x60, 0xA1, 0xE0, 0x00, + 0x00, 0x69, 0xC0, 0x00, 0x00, 0x0F, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x07, + 0xFC, 0x00, 0x00, 0x07, 0x00, 0x70, 0x00, 0x03, 0x80, 0x03, 0x80, 0x00, + 0xC0, 0x00, 0x18, 0x00, 0x20, 0x00, 0x00, 0x80, 0x08, 0x00, 0x00, 0x08, + 0x02, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x08, 0x10, 0x00, 0x00, + 0x01, 0x04, 0x00, 0x00, 0x00, 0x11, 0x80, 0x00, 0x00, 0x02, 0x20, 0x00, + 0x00, 0x00, 0x24, 0x01, 0x80, 0x18, 0x05, 0x80, 0x78, 0x07, 0x80, 0xA0, + 0x0F, 0x00, 0xF0, 0x0C, 0x01, 0xE0, 0x1E, 0x01, 0x80, 0x18, 0x01, 0x80, + 0x30, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, + 0x00, 0x18, 0x00, 0x00, 0x00, 0x03, 0x07, 0xC0, 0x01, 0xF0, 0x70, 0x87, + 0xFF, 0xC2, 0x12, 0x1C, 0x00, 0x01, 0xC2, 0x41, 0xFF, 0xFF, 0xF0, 0x4C, + 0x3F, 0xFF, 0xFE, 0x10, 0x83, 0xFF, 0xFF, 0x82, 0x08, 0x1F, 0x03, 0xC0, + 0x81, 0x01, 0xC0, 0x30, 0x10, 0x10, 0x07, 0xF8, 0x04, 0x01, 0x00, 0x00, + 0x01, 0x00, 0x10, 0x00, 0x00, 0x40, 0x01, 0x80, 0x00, 0x30, 0x00, 0x1C, + 0x00, 0x1C, 0x00, 0x00, 0xE0, 0x0E, 0x00, 0x00, 0x03, 0xFE, 0x00, 0x00, + 0x00, 0x07, 0xFC, 0x00, 0x00, 0x07, 0x80, 0xE0, 0x00, 0x01, 0x80, 0x03, + 0x00, 0x00, 0xC0, 0x00, 0x18, 0x00, 0x20, 0x00, 0x01, 0x80, 0x08, 0x00, + 0x00, 0x08, 0x02, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x18, 0x10, + 0x10, 0x01, 0x81, 0x04, 0x1C, 0x00, 0x1E, 0x10, 0x8E, 0x00, 0x00, 0xF2, + 0x20, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x04, 0x80, 0xF0, 0x07, + 0xC0, 0xA0, 0x67, 0x81, 0x9C, 0x0C, 0x08, 0x70, 0x61, 0xC1, 0x83, 0x0F, + 0x18, 0x3C, 0x30, 0x63, 0xE3, 0x0F, 0x86, 0x0C, 0xFC, 0x73, 0xF0, 0xC1, + 0xFB, 0x8F, 0xEE, 0x18, 0x1F, 0xE0, 0xFF, 0x83, 0x03, 0xFC, 0x0F, 0xE0, + 0x50, 0x1E, 0x00, 0xF8, 0x12, 0x00, 0x00, 0x00, 0x02, 0x40, 0x00, 0x00, + 0x00, 0x44, 0x00, 0x00, 0x00, 0x10, 0x80, 0x00, 0x00, 0x02, 0x08, 0x00, + 0x00, 0x00, 0x81, 0x00, 0x3F, 0x80, 0x30, 0x10, 0x04, 0x10, 0x04, 0x01, + 0x00, 0x00, 0x01, 0x00, 0x10, 0x00, 0x00, 0x40, 0x01, 0x80, 0x00, 0x30, + 0x00, 0x0C, 0x00, 0x18, 0x00, 0x00, 0xF0, 0x1E, 0x00, 0x00, 0x03, 0xFE, + 0x00, 0x00, 0x00, 0x03, 0xFC, 0x00, 0x00, 0x00, 0x70, 0x0E, 0x02, 0x00, + 0x06, 0x00, 0x0E, 0x0C, 0x00, 0x60, 0x00, 0x0E, 0x58, 0x02, 0x00, 0x00, + 0x0F, 0x20, 0x10, 0x00, 0x00, 0x18, 0x40, 0x80, 0x00, 0x00, 0x41, 0x86, + 0x00, 0x00, 0x01, 0x02, 0x10, 0x00, 0x00, 0x08, 0x04, 0x80, 0x00, 0x00, + 0x20, 0x12, 0x00, 0x00, 0x00, 0x80, 0x50, 0x00, 0x00, 0x02, 0x01, 0x40, + 0x00, 0x00, 0x0C, 0x0D, 0x01, 0xF0, 0x1F, 0x18, 0x68, 0x0C, 0x60, 0xC6, + 0x3E, 0x20, 0x00, 0x82, 0x08, 0x08, 0x80, 0x00, 0x00, 0x00, 0x22, 0x00, + 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0x02, 0x20, 0x00, 0x00, 0x00, + 0x08, 0x80, 0x00, 0x00, 0x00, 0x22, 0x0F, 0xC0, 0x03, 0xE0, 0x84, 0x21, + 0xFF, 0xF0, 0x84, 0x10, 0xE0, 0x00, 0x0E, 0x10, 0x41, 0xFF, 0xFF, 0xF8, + 0x40, 0x87, 0xFF, 0xFF, 0xC2, 0x02, 0x0F, 0xFF, 0xFE, 0x08, 0x04, 0x0F, + 0x83, 0xF0, 0x40, 0x10, 0x1C, 0x07, 0x03, 0x00, 0x20, 0x0F, 0xE0, 0x08, + 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x80, 0x00, 0x06, 0x00, 0x01, 0x80, + 0x00, 0x30, 0x00, 0x01, 0x80, 0x03, 0x00, 0x00, 0x03, 0xC0, 0x70, 0x00, + 0x00, 0x01, 0xFF, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x01, 0xF8, 0x00, + 0x00, 0x0F, 0xC0, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x0F, 0xE0, 0x00, 0x00, + 0xFF, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x0F, 0xF0, + 0x00, 0x01, 0xFF, 0x80, 0x00, 0x3F, 0xF8, 0x00, 0x87, 0xFF, 0x80, 0x18, + 0x7F, 0x78, 0x03, 0x8F, 0xCF, 0x00, 0x38, 0xF8, 0xF0, 0x07, 0x9F, 0x0F, + 0x00, 0x79, 0xE0, 0xF0, 0x0F, 0xDE, 0x0F, 0x04, 0xFF, 0xC0, 0xF9, 0xCF, + 0xFC, 0x0F, 0xFE, 0xFF, 0xC0, 0x7F, 0xEF, 0xFC, 0x07, 0xFF, 0xFF, 0xC0, + 0x3F, 0xFF, 0xF6, 0x01, 0xFF, 0xFF, 0x60, 0x0F, 0xFF, 0xE0, 0x00, 0xFF, + 0x7E, 0x00, 0x07, 0xE7, 0xE0, 0x00, 0x7E, 0x7E, 0x00, 0x07, 0xE3, 0xE0, + 0x00, 0x7C, 0x1E, 0x00, 0x0F, 0xC1, 0xF0, 0x00, 0xF8, 0x0F, 0x80, 0x1F, + 0x00, 0x3E, 0x07, 0xC0, 0x01, 0xFF, 0xF8, 0x00, 0x03, 0xFC, 0x00, 0x00, + 0x0F, 0xFC, 0x00, 0x00, 0x01, 0xC0, 0x38, 0x00, 0x00, 0x38, 0x00, 0x60, + 0x00, 0x03, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x02, 0x00, 0x01, 0x00, + 0x00, 0x18, 0x00, 0x18, 0x1F, 0x80, 0x60, 0x01, 0x81, 0x86, 0x01, 0x00, + 0x08, 0x08, 0x10, 0x0C, 0x00, 0xC0, 0xC0, 0xF8, 0x30, 0x04, 0x16, 0x03, + 0x60, 0x80, 0x60, 0xF0, 0x13, 0x06, 0x02, 0x01, 0x80, 0x10, 0x18, 0x30, + 0x04, 0x00, 0x80, 0x61, 0x00, 0x30, 0x0C, 0x01, 0x18, 0x00, 0xC0, 0x20, + 0x0C, 0xC0, 0x03, 0x01, 0x00, 0x34, 0x01, 0xF8, 0x04, 0x00, 0xA0, 0x00, + 0x60, 0x30, 0x05, 0x00, 0x01, 0x00, 0xC0, 0x38, 0x00, 0x0C, 0x01, 0x80, + 0xC0, 0x00, 0x20, 0x04, 0x06, 0x00, 0x01, 0x80, 0x00, 0x38, 0x00, 0x06, + 0x00, 0x01, 0xC0, 0x00, 0x18, 0x00, 0x1B, 0x00, 0x00, 0x60, 0x00, 0xCC, + 0x00, 0x01, 0x80, 0x0C, 0x38, 0x00, 0x06, 0x00, 0x40, 0x7C, 0x00, 0x7E, + 0x0E, 0x00, 0xFF, 0xFF, 0x3F, 0xC0, 0x00, 0x00, 0x7F, 0xC0, 0x00, 0x00, + 0x00, 0x70, 0x1C, 0x00, 0x00, 0x00, 0x30, 0x01, 0x80, 0x00, 0x00, 0x10, + 0x00, 0x30, 0x00, 0x00, 0x08, 0x00, 0x06, 0x00, 0x00, 0x06, 0x00, 0x00, + 0xC0, 0x00, 0x03, 0x00, 0x00, 0x18, 0x01, 0xC0, 0x80, 0x00, 0x02, 0x0F, + 0xC8, 0x40, 0x00, 0x00, 0xC6, 0x71, 0x30, 0x70, 0x18, 0x19, 0x1C, 0x78, + 0x1C, 0x0E, 0x03, 0x85, 0x03, 0x03, 0x01, 0x81, 0x83, 0x60, 0x40, 0x00, + 0x00, 0xC0, 0x8C, 0x18, 0x00, 0x00, 0x20, 0x61, 0x83, 0x00, 0xE0, 0x18, + 0x30, 0x20, 0x40, 0x38, 0x04, 0x18, 0x0C, 0x18, 0x00, 0x03, 0x04, 0x03, + 0x82, 0x00, 0x00, 0x83, 0x80, 0xA0, 0x80, 0x00, 0x60, 0xA0, 0x6C, 0x30, + 0x00, 0x18, 0x68, 0x13, 0x0C, 0x00, 0x04, 0x13, 0x04, 0x41, 0x00, 0x01, + 0x04, 0x41, 0x10, 0x40, 0x00, 0x41, 0x10, 0x40, 0x10, 0x00, 0x10, 0x04, + 0x10, 0x04, 0x00, 0x04, 0x01, 0x04, 0x01, 0x00, 0x01, 0x00, 0x41, 0x80, + 0xC0, 0x00, 0x40, 0x30, 0x20, 0x3F, 0xFF, 0xF8, 0x08, 0x0E, 0x18, 0xFF, + 0xC3, 0x0C, 0x00, 0xFC, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x07, 0xFC, 0x00, + 0x00, 0x07, 0x80, 0xE0, 0x00, 0x01, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x00, + 0x18, 0x00, 0x20, 0x00, 0x01, 0x80, 0x08, 0x00, 0x00, 0x08, 0x02, 0x00, + 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x18, 0x10, 0x00, 0x00, 0x01, 0x04, + 0x00, 0x00, 0x00, 0x10, 0x80, 0x00, 0x00, 0x02, 0x20, 0x7E, 0x01, 0xF8, + 0x24, 0x1F, 0xE0, 0x7F, 0x84, 0x82, 0xF4, 0x0B, 0xD0, 0xA0, 0x9E, 0x42, + 0x79, 0x0C, 0x10, 0x88, 0x42, 0x21, 0x82, 0x01, 0x08, 0x04, 0x30, 0x40, + 0x21, 0x00, 0x86, 0x04, 0x08, 0x10, 0x20, 0xC0, 0xC3, 0x03, 0x0C, 0x18, + 0x07, 0x80, 0x1E, 0x03, 0x00, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, + 0x12, 0x00, 0x00, 0x00, 0x02, 0x40, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, + 0x00, 0x10, 0x80, 0x00, 0x00, 0x02, 0x08, 0x00, 0x00, 0x00, 0x81, 0x00, + 0x7F, 0xE0, 0x30, 0x10, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x01, 0x00, + 0x10, 0x00, 0x00, 0x40, 0x01, 0x80, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x18, + 0x00, 0x00, 0xF0, 0x1E, 0x00, 0x00, 0x03, 0xFE, 0x00, 0x00, 0x00, 0x00, + 0x7F, 0x80, 0x00, 0x00, 0x00, 0xE0, 0x1C, 0x00, 0x00, 0x00, 0xC0, 0x00, + 0xC0, 0x00, 0x00, 0xC0, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x01, 0x80, + 0x00, 0x30, 0x00, 0x00, 0x30, 0x00, 0x18, 0x00, 0x00, 0x06, 0x00, 0x0C, + 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xF0, 0x30, 0x01, 0x80, 0x00, + 0x66, 0x06, 0x00, 0x60, 0x78, 0x10, 0x81, 0x80, 0x30, 0x13, 0x04, 0x00, + 0x30, 0x0C, 0x08, 0x00, 0x00, 0x0C, 0x07, 0x02, 0x00, 0x00, 0x43, 0x01, + 0x80, 0x00, 0x00, 0x10, 0x60, 0x60, 0x00, 0x00, 0x08, 0x18, 0x18, 0x00, + 0x00, 0x04, 0x06, 0x06, 0x00, 0x00, 0x06, 0x01, 0x81, 0x81, 0xC0, 0x07, + 0x00, 0x60, 0x60, 0x3E, 0x0F, 0x00, 0x18, 0x18, 0x01, 0xFF, 0x00, 0x06, + 0x07, 0xE0, 0x00, 0x00, 0x0F, 0x87, 0xCD, 0xC0, 0x00, 0x66, 0x79, 0x31, + 0x50, 0x00, 0x27, 0x12, 0x46, 0x32, 0x00, 0x19, 0x88, 0x98, 0x8C, 0x80, + 0x04, 0x46, 0x7B, 0x11, 0x20, 0x01, 0x11, 0x36, 0x22, 0x08, 0x00, 0x40, + 0x99, 0xC4, 0x01, 0x00, 0x10, 0x0C, 0xF8, 0x80, 0x40, 0x08, 0x06, 0x79, + 0x80, 0x10, 0x02, 0x00, 0x26, 0x30, 0x04, 0x00, 0x80, 0x11, 0x40, 0x01, + 0x00, 0x20, 0x00, 0xD8, 0x00, 0xC0, 0x0C, 0x00, 0x61, 0x80, 0x38, 0x07, + 0x00, 0x60, 0x38, 0x37, 0xFF, 0xB0, 0x70, 0x01, 0xF8, 0x00, 0x07, 0xE0, + 0x00, 0x00, 0x07, 0xFC, 0x00, 0x00, 0x07, 0x80, 0xE0, 0x00, 0x01, 0x80, + 0x03, 0x00, 0x00, 0xC0, 0x00, 0x18, 0x00, 0x20, 0x00, 0x01, 0x80, 0x08, + 0x00, 0x00, 0x08, 0x02, 0x00, 0x00, 0x00, 0x80, 0x81, 0xC0, 0x00, 0x18, + 0x10, 0x70, 0x00, 0x01, 0x04, 0x18, 0x00, 0x00, 0x10, 0x86, 0x00, 0x00, + 0x02, 0x20, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x02, 0x04, 0x80, 0x30, + 0x01, 0xC0, 0xA0, 0x0F, 0x00, 0x60, 0x0C, 0x01, 0xE0, 0x18, 0x01, 0x80, + 0x3C, 0x07, 0xC0, 0x30, 0x03, 0x00, 0x1E, 0x06, 0x00, 0x00, 0x00, 0x00, + 0xC0, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x00, 0x50, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x04, 0x02, 0x40, 0x38, + 0x01, 0x80, 0x44, 0x01, 0xC0, 0xE0, 0x10, 0x80, 0x07, 0xE0, 0x02, 0x08, + 0x00, 0x00, 0x00, 0x81, 0x00, 0x00, 0x00, 0x30, 0x10, 0x00, 0x00, 0x04, + 0x01, 0x00, 0x00, 0x01, 0x00, 0x10, 0x00, 0x00, 0x40, 0x01, 0x80, 0x00, + 0x30, 0x00, 0x0C, 0x00, 0x18, 0x00, 0x00, 0xF0, 0x1E, 0x00, 0x00, 0x03, + 0xFE, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0x00, 0x00, 0x03, 0xB8, 0x1C, 0x00, + 0x00, 0x60, 0xC0, 0x30, 0x00, 0x18, 0x00, 0x00, 0x80, 0x03, 0x00, 0x07, + 0xE4, 0x00, 0x61, 0xC0, 0x03, 0x20, 0x0C, 0x1E, 0x00, 0x01, 0x00, 0x81, + 0xE0, 0x00, 0x08, 0x10, 0x1E, 0x01, 0x80, 0xC3, 0x00, 0xC0, 0x3C, 0x04, + 0x20, 0x00, 0x03, 0xC0, 0x26, 0x00, 0x00, 0x3C, 0x02, 0x60, 0x00, 0x01, + 0x80, 0x24, 0x00, 0x00, 0x00, 0x01, 0x40, 0x00, 0x00, 0x00, 0x14, 0x00, + 0x7F, 0x00, 0x01, 0x40, 0x1C, 0x1C, 0x00, 0x14, 0x00, 0x00, 0x60, 0x01, + 0x40, 0x00, 0x02, 0x00, 0x14, 0x00, 0x00, 0x00, 0x01, 0xF8, 0x00, 0x00, + 0x00, 0x18, 0x80, 0xFC, 0x00, 0x03, 0x8C, 0xF0, 0x20, 0x00, 0x28, 0x78, + 0x06, 0x00, 0x02, 0x40, 0x07, 0x80, 0x00, 0x64, 0x01, 0xC0, 0x00, 0x04, + 0x40, 0x3C, 0x00, 0x00, 0xC4, 0x00, 0x40, 0x00, 0x18, 0x40, 0x04, 0x00, + 0x01, 0x04, 0x01, 0xC0, 0x00, 0x20, 0x40, 0x04, 0x00, 0x0C, 0x04, 0x00, + 0xC0, 0x01, 0x80, 0x60, 0x04, 0x00, 0x70, 0x03, 0x00, 0x60, 0x3C, 0x00, + 0x18, 0x1F, 0xFF, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x00, 0x1D, 0xFC, + 0x00, 0x00, 0x0C, 0xC4, 0x80, 0x00, 0x06, 0x19, 0x9C, 0x00, 0x07, 0x06, + 0x66, 0xE0, 0x01, 0x03, 0x33, 0x43, 0x00, 0xC0, 0x8C, 0xD8, 0x30, 0x10, + 0x06, 0x66, 0x01, 0x04, 0x00, 0x31, 0x00, 0x10, 0x80, 0x00, 0xC0, 0x01, + 0x10, 0x00, 0x68, 0x00, 0x32, 0x07, 0xF3, 0x00, 0x02, 0x41, 0x84, 0xC0, + 0x00, 0x64, 0x00, 0x70, 0x00, 0x04, 0xC0, 0x18, 0x03, 0x80, 0x8C, 0x1F, + 0x00, 0x78, 0x1B, 0xFF, 0xE0, 0x0F, 0x01, 0x60, 0x3C, 0x01, 0xE0, 0x2C, + 0x03, 0x00, 0x38, 0x05, 0x80, 0x00, 0x00, 0x00, 0xB0, 0x00, 0x00, 0x00, + 0x16, 0x00, 0x00, 0x00, 0x02, 0xC0, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, + 0x00, 0x09, 0x00, 0x00, 0x00, 0x02, 0x20, 0x18, 0x00, 0x80, 0x46, 0x03, + 0xFF, 0xF0, 0x08, 0x40, 0x00, 0x00, 0x02, 0x0C, 0x00, 0x00, 0x00, 0x40, + 0x80, 0x00, 0x00, 0x10, 0x08, 0x00, 0x00, 0x06, 0x01, 0x80, 0x00, 0x01, + 0x80, 0x18, 0x00, 0x00, 0x60, 0x00, 0xC0, 0x00, 0x10, 0x00, 0x0C, 0x00, + 0x0C, 0x00, 0x00, 0x70, 0x0E, 0x00, 0x00, 0x01, 0xFE, 0x00, 0x00, 0x01, + 0xC0, 0x00, 0x01, 0xF8, 0x00, 0x00, 0x86, 0x00, 0x00, 0x41, 0xC0, 0x00, + 0x30, 0x30, 0x00, 0x1C, 0x0C, 0x00, 0x1B, 0x83, 0x80, 0x08, 0x60, 0x60, + 0x04, 0x18, 0x18, 0x03, 0x07, 0x04, 0x00, 0xC1, 0x83, 0x00, 0x30, 0x60, + 0x80, 0x1F, 0xF0, 0x60, 0x3C, 0x3E, 0x10, 0x30, 0x03, 0x8C, 0x70, 0x00, + 0x62, 0x60, 0x10, 0x11, 0x20, 0x7E, 0x0C, 0xF0, 0x61, 0x82, 0x6C, 0xE0, + 0x61, 0xB3, 0xC0, 0x10, 0x0B, 0x80, 0x08, 0x07, 0x60, 0x04, 0x03, 0x18, + 0x02, 0x03, 0x86, 0x03, 0x01, 0xC1, 0x01, 0x80, 0xE0, 0x61, 0x80, 0x58, + 0x1F, 0x80, 0x24, 0x00, 0x00, 0x33, 0x00, 0x00, 0x10, 0xC0, 0x00, 0x18, + 0x30, 0x00, 0x18, 0x0C, 0x00, 0x18, 0x03, 0x80, 0x18, 0x00, 0xFF, 0xF8, + 0x00, 0x0F, 0xE0, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, + 0x00, 0x3F, 0xFC, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xE1, 0xF8, 0x7E, 0x1F, + 0x87, 0xE1, 0xC0, 0x00, 0x38, 0x30, 0x00, 0x30, 0x70, 0x00, 0x70, 0x70, + 0x00, 0x70, 0x70, 0x00, 0x70, 0xE0, 0x00, 0x60, 0xE0, 0x00, 0xE0, 0xE0, + 0x3F, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0x01, 0xC1, 0xC0, + 0x01, 0xC1, 0xC0, 0x01, 0xC1, 0x80, 0x03, 0x83, 0x80, 0x03, 0x83, 0x80, + 0xFF, 0xFF, 0xFC, 0xFF, 0xFF, 0xFC, 0xFF, 0xFF, 0xFC, 0x07, 0x07, 0x00, + 0x07, 0x07, 0x00, 0x07, 0x06, 0x00, 0x0E, 0x0E, 0x00, 0x0E, 0x0E, 0x00, + 0x0E, 0x0E, 0x00, 0x0E, 0x0C, 0x00, 0x01, 0x80, 0x00, 0xC0, 0x00, 0x60, + 0x00, 0x30, 0x00, 0xFF, 0x81, 0xFF, 0xF1, 0xFF, 0xF8, 0xF3, 0x0C, 0xF1, + 0x80, 0x70, 0xC0, 0x38, 0x60, 0x1C, 0x30, 0x0F, 0x18, 0x03, 0xFC, 0x00, + 0xFF, 0xC0, 0x1F, 0xF8, 0x01, 0xFF, 0x00, 0xC7, 0x80, 0x61, 0xE0, 0x30, + 0x70, 0x18, 0x38, 0x0C, 0x1E, 0x06, 0x1F, 0xC3, 0x3E, 0xFF, 0xFE, 0x3F, + 0xFE, 0x03, 0xFC, 0x00, 0x30, 0x00, 0x18, 0x00, 0x0C, 0x00, 0x06, 0x00, + 0x03, 0x00, 0x1F, 0x80, 0x06, 0x01, 0xFE, 0x00, 0x70, 0x1E, 0x78, 0x03, + 0x00, 0xE1, 0xC0, 0x30, 0x0E, 0x07, 0x03, 0x80, 0x70, 0x38, 0x18, 0x03, + 0x81, 0xC1, 0xC0, 0x1C, 0x0E, 0x0C, 0x00, 0xE0, 0x70, 0xC0, 0x07, 0x03, + 0x8E, 0x00, 0x1C, 0x38, 0x60, 0x00, 0xF3, 0xC7, 0x00, 0x03, 0xFC, 0x30, + 0xFC, 0x0F, 0xC3, 0x0F, 0xF0, 0x00, 0x38, 0xF3, 0xC0, 0x01, 0x87, 0x0E, + 0x00, 0x1C, 0x70, 0x38, 0x00, 0xC3, 0x81, 0xC0, 0x0C, 0x1C, 0x0E, 0x00, + 0xE0, 0xE0, 0x70, 0x06, 0x07, 0x03, 0x80, 0x70, 0x38, 0x1C, 0x03, 0x00, + 0xE1, 0xC0, 0x30, 0x07, 0x9E, 0x03, 0x80, 0x1F, 0xE0, 0x18, 0x00, 0x7E, + 0x00, 0x01, 0xFC, 0x00, 0x07, 0xFF, 0x00, 0x0F, 0xFF, 0x00, 0x1F, 0x03, + 0x00, 0x1E, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x1C, 0x00, + 0x00, 0x0E, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x0F, 0x80, 0x00, 0x1F, 0xC0, + 0x00, 0x3D, 0xE0, 0x0E, 0x78, 0xF0, 0x0E, 0x70, 0x78, 0x1E, 0xF0, 0x3C, + 0x1C, 0xE0, 0x1F, 0x1C, 0xE0, 0x0F, 0xB8, 0xE0, 0x07, 0xF8, 0xE0, 0x03, + 0xF0, 0xF0, 0x01, 0xF0, 0x78, 0x03, 0xF0, 0x3E, 0x0F, 0xF8, 0x3F, 0xFF, + 0x7C, 0x0F, 0xFE, 0x3E, 0x03, 0xF8, 0x1F, 0xFF, 0xFF, 0xFF, 0xE0, 0x07, + 0x0E, 0x1E, 0x1C, 0x18, 0x38, 0x38, 0x70, 0x70, 0x70, 0x70, 0xE0, 0xE0, + 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0x60, 0x70, 0x70, 0x70, 0x38, + 0x38, 0x1C, 0x1C, 0x1C, 0x0E, 0x07, 0xE0, 0x70, 0x70, 0x38, 0x38, 0x1C, + 0x1C, 0x0E, 0x0E, 0x0E, 0x0E, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, + 0x07, 0x07, 0x0E, 0x0E, 0x0E, 0x0E, 0x1C, 0x1C, 0x38, 0x38, 0x78, 0x70, + 0xE0, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x41, 0x82, 0xF1, 0x8F, 0x39, + 0x9C, 0x1F, 0xF8, 0x07, 0xE0, 0x07, 0xE0, 0x1F, 0xF0, 0x39, 0x9C, 0xF1, + 0x8F, 0x41, 0x82, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x00, 0x38, 0x00, + 0x00, 0x70, 0x00, 0x00, 0xE0, 0x00, 0x01, 0xC0, 0x00, 0x03, 0x80, 0x00, + 0x07, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x38, 0x00, 0x00, + 0x70, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x07, + 0x00, 0x00, 0x0E, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x38, 0x00, 0x00, 0x70, + 0x00, 0x00, 0xE0, 0x00, 0x01, 0xC0, 0x00, 0x03, 0x80, 0x00, 0x07, 0x00, + 0x00, 0x0E, 0x00, 0x00, 0x77, 0x77, 0x6E, 0xEC, 0xFF, 0xFF, 0xFF, 0xE0, + 0xFF, 0xF0, 0x00, 0x70, 0x0F, 0x00, 0xE0, 0x0E, 0x01, 0xE0, 0x1C, 0x01, + 0xC0, 0x1C, 0x03, 0x80, 0x38, 0x03, 0x80, 0x78, 0x07, 0x00, 0x70, 0x0F, + 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x1C, 0x01, 0xC0, 0x1C, 0x03, 0x80, 0x38, + 0x03, 0x80, 0x78, 0x07, 0x00, 0x70, 0x0F, 0x00, 0xE0, 0x00, 0x03, 0xF0, + 0x03, 0xFF, 0x01, 0xFF, 0xE0, 0xF8, 0x7C, 0x38, 0x07, 0x1E, 0x01, 0xE7, + 0x00, 0x39, 0xC0, 0x0E, 0xE0, 0x01, 0xF8, 0x00, 0x7E, 0x00, 0x1F, 0x80, + 0x07, 0xE0, 0x01, 0xF8, 0x00, 0x7E, 0x00, 0x1F, 0x80, 0x07, 0xE0, 0x01, + 0xF8, 0x00, 0x77, 0x00, 0x39, 0xC0, 0x0E, 0x78, 0x07, 0x8E, 0x01, 0xC3, + 0xE1, 0xF0, 0x7F, 0xF8, 0x0F, 0xFC, 0x00, 0xFC, 0x00, 0x1F, 0x81, 0xFF, + 0x03, 0xFE, 0x07, 0x1C, 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x01, 0xC0, + 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x38, 0x00, 0x70, 0x00, + 0xE0, 0x01, 0xC0, 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x38, + 0x00, 0x70, 0x00, 0xE0, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x3F, 0xE0, + 0xFF, 0xF8, 0xFF, 0xFC, 0xF0, 0x3E, 0x80, 0x0E, 0x00, 0x0F, 0x00, 0x07, + 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, 0x0E, 0x00, 0x1E, 0x00, 0x1C, + 0x00, 0x38, 0x00, 0x78, 0x00, 0xF0, 0x01, 0xE0, 0x03, 0xC0, 0x07, 0x80, + 0x0F, 0x00, 0x1E, 0x00, 0x3C, 0x00, 0xF8, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x1F, 0xE0, 0x3F, 0xFC, 0x1F, 0xFF, 0x0C, 0x07, 0xC0, 0x00, + 0xF0, 0x00, 0x38, 0x00, 0x1C, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x07, 0x00, + 0x07, 0x81, 0xFF, 0x80, 0xFF, 0x00, 0x7F, 0xE0, 0x00, 0x7C, 0x00, 0x0E, + 0x00, 0x07, 0x80, 0x01, 0xC0, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x78, 0x00, + 0x3B, 0x00, 0x7D, 0xFF, 0xFC, 0xFF, 0xFC, 0x1F, 0xF0, 0x00, 0x00, 0x3E, + 0x00, 0x07, 0xC0, 0x01, 0xF8, 0x00, 0x77, 0x00, 0x0E, 0xE0, 0x03, 0x9C, + 0x00, 0xE3, 0x80, 0x1C, 0x70, 0x07, 0x0E, 0x00, 0xE1, 0xC0, 0x38, 0x38, + 0x0E, 0x07, 0x01, 0xC0, 0xE0, 0x70, 0x1C, 0x1C, 0x03, 0x83, 0x80, 0x70, + 0xE0, 0x0E, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0xE0, + 0x00, 0x1C, 0x00, 0x03, 0x80, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x01, 0xC0, + 0x7F, 0xFE, 0x3F, 0xFF, 0x1F, 0xFF, 0x8E, 0x00, 0x07, 0x00, 0x03, 0x80, + 0x01, 0xC0, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x3F, 0xF0, 0x1F, 0xFE, 0x0F, + 0xFF, 0xC6, 0x03, 0xE0, 0x00, 0x78, 0x00, 0x1E, 0x00, 0x07, 0x00, 0x03, + 0x80, 0x01, 0xC0, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x78, 0x00, 0x7B, 0x00, + 0xFD, 0xFF, 0xFC, 0xFF, 0xF8, 0x1F, 0xF0, 0x00, 0x01, 0xFC, 0x01, 0xFF, + 0xC0, 0xFF, 0xF0, 0x7C, 0x0C, 0x3C, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x01, + 0xC0, 0x00, 0xF0, 0x00, 0x38, 0xFE, 0x0E, 0x7F, 0xE3, 0xBF, 0xFC, 0xFE, + 0x0F, 0xBE, 0x00, 0xEF, 0x80, 0x3F, 0xC0, 0x07, 0xF0, 0x01, 0xFC, 0x00, + 0x77, 0x00, 0x1D, 0xC0, 0x07, 0x78, 0x03, 0xCE, 0x00, 0xE3, 0xE0, 0xF8, + 0x7F, 0xFC, 0x0F, 0xFE, 0x00, 0xFE, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x1E, 0x00, 0x1C, 0x00, 0x1C, 0x00, + 0x38, 0x00, 0x38, 0x00, 0x38, 0x00, 0x70, 0x00, 0x70, 0x00, 0xF0, 0x00, + 0xE0, 0x00, 0xE0, 0x01, 0xE0, 0x01, 0xC0, 0x01, 0xC0, 0x03, 0xC0, 0x03, + 0x80, 0x03, 0x80, 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, 0x0E, 0x00, 0x03, + 0xF8, 0x03, 0xFF, 0x03, 0xFF, 0xF0, 0xF0, 0x3C, 0x78, 0x07, 0x9C, 0x00, + 0xE7, 0x00, 0x39, 0xC0, 0x0E, 0x70, 0x03, 0x8E, 0x01, 0xC3, 0xC0, 0xF0, + 0x7F, 0xF8, 0x07, 0xF8, 0x07, 0xFF, 0x87, 0xC0, 0xF9, 0xC0, 0x0E, 0xE0, + 0x01, 0xF8, 0x00, 0x7E, 0x00, 0x1F, 0x80, 0x07, 0xE0, 0x01, 0xFC, 0x00, + 0xF7, 0xC0, 0xF8, 0xFF, 0xFC, 0x1F, 0xFE, 0x01, 0xFE, 0x00, 0x07, 0xF0, + 0x07, 0xFF, 0x03, 0xFF, 0xE1, 0xF0, 0x7C, 0x70, 0x07, 0x3C, 0x01, 0xEE, + 0x00, 0x3B, 0x80, 0x0E, 0xE0, 0x03, 0xF8, 0x00, 0xFE, 0x00, 0x3F, 0xC0, + 0x1F, 0x70, 0x07, 0xDF, 0x07, 0xF3, 0xFF, 0xDC, 0x7F, 0xE7, 0x07, 0xF1, + 0xC0, 0x00, 0xF0, 0x00, 0x38, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x03, 0xC3, + 0x03, 0xE0, 0xFF, 0xF0, 0x3F, 0xF8, 0x03, 0xF8, 0x00, 0xFF, 0xF0, 0x00, + 0x00, 0x00, 0x3F, 0xFC, 0x77, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x77, + 0x77, 0x6E, 0xEC, 0x00, 0x00, 0x04, 0x00, 0x00, 0xF0, 0x00, 0x1F, 0xC0, + 0x03, 0xFE, 0x00, 0x3F, 0xC0, 0x07, 0xFC, 0x00, 0xFF, 0x80, 0x0F, 0xF0, + 0x00, 0xFF, 0x00, 0x03, 0xE0, 0x00, 0x0F, 0xF0, 0x00, 0x0F, 0xF0, 0x00, + 0x0F, 0xF8, 0x00, 0x07, 0xFC, 0x00, 0x03, 0xFC, 0x00, 0x03, 0xFE, 0x00, + 0x01, 0xFC, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, + 0x80, 0x00, 0x03, 0xC0, 0x00, 0x0F, 0xE0, 0x00, 0x1F, 0xF0, 0x00, 0x0F, + 0xF0, 0x00, 0x0F, 0xF8, 0x00, 0x07, 0xFC, 0x00, 0x03, 0xFC, 0x00, 0x03, + 0xFC, 0x00, 0x01, 0xF0, 0x00, 0x3F, 0xC0, 0x03, 0xFC, 0x00, 0x7F, 0xC0, + 0x0F, 0xF8, 0x00, 0xFF, 0x00, 0x1F, 0xF0, 0x00, 0xFE, 0x00, 0x03, 0xC0, + 0x00, 0x08, 0x00, 0x00, 0x00, 0x1F, 0xC1, 0xFF, 0xCF, 0xFF, 0xBC, 0x1E, + 0x80, 0x3C, 0x00, 0x70, 0x01, 0xC0, 0x07, 0x00, 0x78, 0x03, 0xE0, 0x1F, + 0x00, 0xF8, 0x07, 0xC0, 0x3C, 0x00, 0xE0, 0x03, 0x80, 0x0E, 0x00, 0x38, + 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x80, 0x0E, + 0x00, 0x38, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0xFF, 0xFC, 0x00, 0x07, + 0xFF, 0xFE, 0x00, 0x1F, 0x80, 0x7E, 0x00, 0x7C, 0x00, 0x3E, 0x01, 0xE0, + 0x00, 0x1E, 0x07, 0x80, 0x00, 0x1E, 0x1E, 0x00, 0x00, 0x1E, 0x38, 0x0F, + 0x8E, 0x1C, 0xF0, 0x7F, 0xDC, 0x39, 0xC1, 0xFF, 0xF8, 0x3F, 0x83, 0xC1, + 0xF0, 0x7E, 0x0F, 0x01, 0xE0, 0xFC, 0x1C, 0x01, 0xC1, 0xF8, 0x38, 0x03, + 0x83, 0xF0, 0x70, 0x07, 0x07, 0xE0, 0xE0, 0x0E, 0x1F, 0xC1, 0xC0, 0x1C, + 0x3B, 0x83, 0xC0, 0x78, 0xF7, 0x83, 0xC1, 0xF3, 0xC7, 0x07, 0xFF, 0xFF, + 0x0E, 0x07, 0xFD, 0xF8, 0x0E, 0x03, 0xE3, 0xC0, 0x1E, 0x00, 0x00, 0x00, + 0x1E, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x02, 0x00, 0x3F, 0x00, 0x0E, 0x00, + 0x1F, 0x80, 0xFC, 0x00, 0x1F, 0xFF, 0xF0, 0x00, 0x0F, 0xFF, 0x80, 0x00, + 0x07, 0xF8, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0xF8, 0x00, 0x01, 0xF0, + 0x00, 0x07, 0x70, 0x00, 0x0E, 0xE0, 0x00, 0x1D, 0xC0, 0x00, 0x71, 0xC0, + 0x00, 0xE3, 0x80, 0x03, 0xC7, 0x80, 0x07, 0x07, 0x00, 0x0E, 0x0E, 0x00, + 0x3C, 0x1E, 0x00, 0x70, 0x1C, 0x00, 0xE0, 0x38, 0x03, 0x80, 0x38, 0x07, + 0x00, 0x70, 0x1E, 0x00, 0xF0, 0x3F, 0xFF, 0xE0, 0x7F, 0xFF, 0xC1, 0xFF, + 0xFF, 0xC3, 0x80, 0x03, 0x87, 0x00, 0x07, 0x1C, 0x00, 0x07, 0x38, 0x00, + 0x0E, 0x70, 0x00, 0x1D, 0xC0, 0x00, 0x1C, 0xFF, 0xF8, 0x3F, 0xFF, 0x8F, + 0xFF, 0xF3, 0x80, 0x3C, 0xE0, 0x07, 0xB8, 0x00, 0xEE, 0x00, 0x3B, 0x80, + 0x0E, 0xE0, 0x03, 0xB8, 0x01, 0xEE, 0x00, 0xF3, 0xFF, 0xF8, 0xFF, 0xFC, + 0x3F, 0xFF, 0xCE, 0x00, 0xFB, 0x80, 0x0E, 0xE0, 0x01, 0xF8, 0x00, 0x7E, + 0x00, 0x1F, 0x80, 0x07, 0xE0, 0x01, 0xF8, 0x00, 0xFE, 0x00, 0xFB, 0xFF, + 0xFC, 0xFF, 0xFE, 0x3F, 0xFE, 0x00, 0x00, 0x7F, 0x80, 0x1F, 0xFF, 0x83, + 0xFF, 0xFE, 0x3F, 0x01, 0xF3, 0xE0, 0x01, 0x9E, 0x00, 0x05, 0xE0, 0x00, + 0x0E, 0x00, 0x00, 0x70, 0x00, 0x07, 0x00, 0x00, 0x38, 0x00, 0x01, 0xC0, + 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x03, 0x80, 0x00, 0x1C, 0x00, 0x00, + 0xE0, 0x00, 0x03, 0x80, 0x00, 0x1C, 0x00, 0x00, 0xF0, 0x00, 0x03, 0xC0, + 0x00, 0x9F, 0x00, 0x0C, 0x7E, 0x03, 0xE1, 0xFF, 0xFF, 0x03, 0xFF, 0xF0, + 0x07, 0xFC, 0x00, 0xFF, 0xF0, 0x07, 0xFF, 0xF0, 0x3F, 0xFF, 0xE1, 0xC0, + 0x1F, 0x8E, 0x00, 0x3E, 0x70, 0x00, 0x73, 0x80, 0x03, 0xDC, 0x00, 0x0E, + 0xE0, 0x00, 0x7F, 0x00, 0x01, 0xF8, 0x00, 0x0F, 0xC0, 0x00, 0x7E, 0x00, + 0x03, 0xF0, 0x00, 0x1F, 0x80, 0x00, 0xFC, 0x00, 0x07, 0xE0, 0x00, 0x3F, + 0x00, 0x03, 0xF8, 0x00, 0x1D, 0xC0, 0x01, 0xEE, 0x00, 0x0E, 0x70, 0x01, + 0xF3, 0x80, 0x3F, 0x1F, 0xFF, 0xF0, 0xFF, 0xFE, 0x07, 0xFF, 0x80, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, + 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xFF, 0xFE, + 0xFF, 0xFE, 0xFF, 0xFE, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, + 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x0E, + 0x00, 0x1C, 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x03, 0x80, + 0x07, 0xFF, 0xEF, 0xFF, 0xDF, 0xFF, 0xB8, 0x00, 0x70, 0x00, 0xE0, 0x01, + 0xC0, 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x38, 0x00, 0x70, + 0x00, 0xE0, 0x01, 0xC0, 0x00, 0x00, 0xFF, 0x80, 0x0F, 0xFF, 0xC0, 0xFF, + 0xFF, 0x87, 0xE0, 0x3E, 0x3E, 0x00, 0x18, 0xE0, 0x00, 0x27, 0x80, 0x00, + 0x1C, 0x00, 0x00, 0x70, 0x00, 0x03, 0x80, 0x00, 0x0E, 0x00, 0x00, 0x38, + 0x00, 0x00, 0xE0, 0x07, 0xFF, 0x80, 0x1F, 0xFE, 0x00, 0x7F, 0xF8, 0x00, + 0x07, 0xE0, 0x00, 0x1D, 0xC0, 0x00, 0x77, 0x00, 0x01, 0xDE, 0x00, 0x07, + 0x38, 0x00, 0x1C, 0xF8, 0x00, 0x71, 0xF8, 0x07, 0xC3, 0xFF, 0xFE, 0x03, + 0xFF, 0xE0, 0x03, 0xFE, 0x00, 0xE0, 0x00, 0xFC, 0x00, 0x1F, 0x80, 0x03, + 0xF0, 0x00, 0x7E, 0x00, 0x0F, 0xC0, 0x01, 0xF8, 0x00, 0x3F, 0x00, 0x07, + 0xE0, 0x00, 0xFC, 0x00, 0x1F, 0x80, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xF8, 0x00, 0x3F, 0x00, 0x07, 0xE0, 0x00, 0xFC, 0x00, 0x1F, + 0x80, 0x03, 0xF0, 0x00, 0x7E, 0x00, 0x0F, 0xC0, 0x01, 0xF8, 0x00, 0x3F, + 0x00, 0x07, 0xE0, 0x00, 0xFC, 0x00, 0x1C, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, + 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, + 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x0E, 0x1E, 0xFE, + 0xFC, 0xF0, 0xE0, 0x03, 0xCE, 0x00, 0x78, 0xE0, 0x0F, 0x0E, 0x01, 0xE0, + 0xE0, 0x3C, 0x0E, 0x07, 0x80, 0xE0, 0xF0, 0x0E, 0x1E, 0x00, 0xE3, 0xC0, + 0x0E, 0x78, 0x00, 0xEF, 0x00, 0x0F, 0xE0, 0x00, 0xFE, 0x00, 0x0F, 0xF0, + 0x00, 0xEF, 0x80, 0x0E, 0x7C, 0x00, 0xE3, 0xE0, 0x0E, 0x1F, 0x00, 0xE0, + 0xF8, 0x0E, 0x07, 0xC0, 0xE0, 0x3E, 0x0E, 0x01, 0xF0, 0xE0, 0x0F, 0x8E, + 0x00, 0x7C, 0xE0, 0x03, 0xEE, 0x00, 0x1F, 0xE0, 0x00, 0xE0, 0x00, 0xE0, + 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, + 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, + 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, + 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, + 0x00, 0x3F, 0xF8, 0x00, 0xFF, 0xF0, 0x01, 0xFF, 0xE0, 0x03, 0xFF, 0xE0, + 0x0F, 0xFD, 0xC0, 0x1D, 0xFB, 0x80, 0x3B, 0xF3, 0x80, 0xE7, 0xE7, 0x01, + 0xCF, 0xCF, 0x07, 0x9F, 0x8E, 0x0E, 0x3F, 0x1C, 0x1C, 0x7E, 0x1C, 0x70, + 0xFC, 0x38, 0xE1, 0xF8, 0x71, 0xC3, 0xF0, 0x77, 0x07, 0xE0, 0xEE, 0x0F, + 0xC1, 0xFC, 0x1F, 0x81, 0xF0, 0x3F, 0x03, 0xE0, 0x7E, 0x03, 0x80, 0xFC, + 0x00, 0x01, 0xF8, 0x00, 0x03, 0xF0, 0x00, 0x07, 0xE0, 0x00, 0x0F, 0xC0, + 0x00, 0x1C, 0xF8, 0x00, 0xFF, 0x00, 0x1F, 0xF0, 0x03, 0xFE, 0x00, 0x7F, + 0xE0, 0x0F, 0xDC, 0x01, 0xFB, 0xC0, 0x3F, 0x38, 0x07, 0xE7, 0x80, 0xFC, + 0x70, 0x1F, 0x8F, 0x03, 0xF0, 0xE0, 0x7E, 0x0E, 0x0F, 0xC1, 0xC1, 0xF8, + 0x1C, 0x3F, 0x03, 0xC7, 0xE0, 0x38, 0xFC, 0x07, 0x9F, 0x80, 0x73, 0xF0, + 0x0F, 0x7E, 0x00, 0xEF, 0xC0, 0x1F, 0xF8, 0x01, 0xFF, 0x00, 0x3F, 0xE0, + 0x03, 0xFC, 0x00, 0x7C, 0x00, 0xFF, 0x00, 0x03, 0xFF, 0xC0, 0x0F, 0xFF, + 0xF0, 0x1F, 0x81, 0xF8, 0x3E, 0x00, 0x7C, 0x3C, 0x00, 0x3C, 0x78, 0x00, + 0x1E, 0x70, 0x00, 0x0E, 0x70, 0x00, 0x0E, 0xE0, 0x00, 0x07, 0xE0, 0x00, + 0x07, 0xE0, 0x00, 0x07, 0xE0, 0x00, 0x07, 0xE0, 0x00, 0x07, 0xE0, 0x00, + 0x07, 0xE0, 0x00, 0x07, 0xE0, 0x00, 0x07, 0x70, 0x00, 0x0E, 0x70, 0x00, + 0x0E, 0x78, 0x00, 0x1E, 0x3C, 0x00, 0x3C, 0x3E, 0x00, 0x7C, 0x1F, 0x81, + 0xF8, 0x0F, 0xFF, 0xF0, 0x03, 0xFF, 0xC0, 0x00, 0xFF, 0x00, 0xFF, 0xE0, + 0xFF, 0xF8, 0xFF, 0xFC, 0xE0, 0x3E, 0xE0, 0x0F, 0xE0, 0x07, 0xE0, 0x07, + 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x0F, 0xE0, 0x3E, 0xFF, 0xFC, + 0xFF, 0xF8, 0xFF, 0xE0, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, + 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, + 0xE0, 0x00, 0x00, 0xFF, 0x00, 0x03, 0xFF, 0xC0, 0x0F, 0xFF, 0xF0, 0x1F, + 0x81, 0xF8, 0x3E, 0x00, 0x7C, 0x3C, 0x00, 0x3C, 0x78, 0x00, 0x1E, 0x70, + 0x00, 0x0E, 0x70, 0x00, 0x0E, 0xE0, 0x00, 0x07, 0xE0, 0x00, 0x07, 0xE0, + 0x00, 0x07, 0xE0, 0x00, 0x07, 0xE0, 0x00, 0x07, 0xE0, 0x00, 0x07, 0xE0, + 0x00, 0x07, 0xE0, 0x00, 0x07, 0x70, 0x00, 0x0E, 0x70, 0x00, 0x0E, 0x78, + 0x00, 0x1E, 0x3C, 0x00, 0x3C, 0x3E, 0x00, 0x7C, 0x1F, 0x81, 0xF8, 0x0F, + 0xFF, 0xF0, 0x03, 0xFF, 0xC0, 0x00, 0xFF, 0xC0, 0x00, 0x01, 0xE0, 0x00, + 0x01, 0xE0, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x78, 0x00, 0x00, 0x3C, 0xFF, + 0xE0, 0x1F, 0xFF, 0x03, 0xFF, 0xF8, 0x70, 0x1F, 0x0E, 0x00, 0xF1, 0xC0, + 0x0E, 0x38, 0x01, 0xC7, 0x00, 0x38, 0xE0, 0x07, 0x1C, 0x00, 0xE3, 0x80, + 0x3C, 0x70, 0x0F, 0x0F, 0xFF, 0xC1, 0xFF, 0xF0, 0x3F, 0xFE, 0x07, 0x01, + 0xE0, 0xE0, 0x1E, 0x1C, 0x01, 0xC3, 0x80, 0x3C, 0x70, 0x03, 0x8E, 0x00, + 0x79, 0xC0, 0x07, 0x38, 0x00, 0xE7, 0x00, 0x0E, 0xE0, 0x01, 0xDC, 0x00, + 0x1C, 0x07, 0xFC, 0x07, 0xFF, 0xC3, 0xFF, 0xF1, 0xF0, 0x0C, 0xF0, 0x00, + 0x38, 0x00, 0x0E, 0x00, 0x03, 0x80, 0x00, 0xE0, 0x00, 0x3C, 0x00, 0x07, + 0xC0, 0x00, 0xFF, 0x80, 0x1F, 0xFC, 0x00, 0xFF, 0xC0, 0x01, 0xF8, 0x00, + 0x1E, 0x00, 0x03, 0xC0, 0x00, 0x70, 0x00, 0x1C, 0x00, 0x07, 0x00, 0x01, + 0xE0, 0x00, 0xEF, 0x00, 0xFB, 0xFF, 0xFC, 0xFF, 0xFE, 0x0F, 0xFE, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0xE0, 0x00, 0x07, + 0x00, 0x00, 0x38, 0x00, 0x01, 0xC0, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, + 0x03, 0x80, 0x00, 0x1C, 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0x38, + 0x00, 0x01, 0xC0, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x03, 0x80, 0x00, + 0x1C, 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0x38, 0x00, 0x01, 0xC0, + 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x03, 0x80, 0x00, 0xE0, 0x00, 0xFC, + 0x00, 0x1F, 0x80, 0x03, 0xF0, 0x00, 0x7E, 0x00, 0x0F, 0xC0, 0x01, 0xF8, + 0x00, 0x3F, 0x00, 0x07, 0xE0, 0x00, 0xFC, 0x00, 0x1F, 0x80, 0x03, 0xF0, + 0x00, 0x7E, 0x00, 0x0F, 0xC0, 0x01, 0xF8, 0x00, 0x3F, 0x00, 0x07, 0xE0, + 0x00, 0xFC, 0x00, 0x1F, 0x80, 0x03, 0xF8, 0x00, 0xF7, 0x00, 0x1C, 0xF0, + 0x07, 0x8F, 0x01, 0xE1, 0xFF, 0xFC, 0x0F, 0xFE, 0x00, 0x7F, 0x00, 0xE0, + 0x00, 0x0E, 0xE0, 0x00, 0x39, 0xC0, 0x00, 0x73, 0x80, 0x01, 0xE3, 0x80, + 0x03, 0x87, 0x00, 0x07, 0x0F, 0x00, 0x1E, 0x0E, 0x00, 0x38, 0x1C, 0x00, + 0x70, 0x3C, 0x01, 0xE0, 0x38, 0x03, 0x80, 0x70, 0x07, 0x00, 0x70, 0x1C, + 0x00, 0xE0, 0x38, 0x01, 0xE0, 0xF0, 0x01, 0xC1, 0xC0, 0x03, 0x83, 0x80, + 0x07, 0x8F, 0x00, 0x07, 0x1C, 0x00, 0x0E, 0x38, 0x00, 0x0E, 0xE0, 0x00, + 0x1D, 0xC0, 0x00, 0x3F, 0x80, 0x00, 0x3E, 0x00, 0x00, 0x7C, 0x00, 0x00, + 0xF8, 0x00, 0xE0, 0x03, 0xC0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0x70, 0x07, + 0xE0, 0x0E, 0x70, 0x07, 0xE0, 0x0E, 0x70, 0x07, 0xE0, 0x0E, 0x70, 0x0F, + 0xF0, 0x0E, 0x38, 0x0E, 0x70, 0x1C, 0x38, 0x0E, 0x70, 0x1C, 0x38, 0x0E, + 0x70, 0x1C, 0x38, 0x1E, 0x78, 0x1C, 0x1C, 0x1C, 0x38, 0x38, 0x1C, 0x1C, + 0x38, 0x38, 0x1C, 0x1C, 0x38, 0x38, 0x1C, 0x1C, 0x38, 0x38, 0x0E, 0x38, + 0x1C, 0x70, 0x0E, 0x38, 0x1C, 0x70, 0x0E, 0x38, 0x1C, 0x70, 0x0E, 0x38, + 0x1C, 0x70, 0x0F, 0x70, 0x0E, 0xE0, 0x07, 0x70, 0x0E, 0xE0, 0x07, 0x70, + 0x0E, 0xE0, 0x07, 0x70, 0x0E, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x03, 0xE0, + 0x07, 0xC0, 0x03, 0xE0, 0x07, 0xC0, 0x03, 0xE0, 0x07, 0xC0, 0x78, 0x00, + 0x78, 0xF0, 0x03, 0xC1, 0xC0, 0x0E, 0x07, 0x80, 0x78, 0x0F, 0x03, 0xC0, + 0x1C, 0x0E, 0x00, 0x78, 0x78, 0x00, 0xF3, 0xC0, 0x01, 0xCE, 0x00, 0x07, + 0xF8, 0x00, 0x0F, 0xC0, 0x00, 0x1E, 0x00, 0x00, 0x78, 0x00, 0x03, 0xF0, + 0x00, 0x0F, 0xC0, 0x00, 0x7F, 0x80, 0x03, 0xCF, 0x00, 0x0E, 0x1C, 0x00, + 0x78, 0x78, 0x03, 0xC0, 0xF0, 0x0E, 0x01, 0xC0, 0x78, 0x07, 0x83, 0xC0, + 0x0F, 0x0E, 0x00, 0x1C, 0x78, 0x00, 0x7B, 0xC0, 0x00, 0xF0, 0xF0, 0x00, + 0x7B, 0xC0, 0x07, 0x8E, 0x00, 0x38, 0x78, 0x03, 0xC1, 0xE0, 0x3C, 0x07, + 0x01, 0xC0, 0x3C, 0x1E, 0x00, 0xF1, 0xE0, 0x03, 0x8E, 0x00, 0x1E, 0xF0, + 0x00, 0x7F, 0x00, 0x01, 0xF0, 0x00, 0x0F, 0x80, 0x00, 0x38, 0x00, 0x01, + 0xC0, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x03, 0x80, 0x00, 0x1C, 0x00, + 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0x38, 0x00, 0x01, 0xC0, 0x00, 0x0E, + 0x00, 0x00, 0x70, 0x00, 0x03, 0x80, 0x00, 0xFF, 0xFF, 0xF7, 0xFF, 0xFF, + 0xBF, 0xFF, 0xFC, 0x00, 0x01, 0xC0, 0x00, 0x1E, 0x00, 0x01, 0xE0, 0x00, + 0x1E, 0x00, 0x01, 0xE0, 0x00, 0x0E, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, + 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0x78, 0x00, 0x07, 0x80, 0x00, 0x78, + 0x00, 0x07, 0x80, 0x00, 0x38, 0x00, 0x03, 0xC0, 0x00, 0x3C, 0x00, 0x03, + 0xC0, 0x00, 0x3C, 0x00, 0x01, 0xC0, 0x00, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0x0E, 0x1C, 0x38, 0x70, 0xE1, + 0xC3, 0x87, 0x0E, 0x1C, 0x38, 0x70, 0xE1, 0xC3, 0x87, 0x0E, 0x1C, 0x38, + 0x70, 0xE1, 0xC3, 0x87, 0x0F, 0xFF, 0xFF, 0x80, 0xE0, 0x0F, 0x00, 0x70, + 0x07, 0x00, 0x78, 0x03, 0x80, 0x38, 0x03, 0x80, 0x1C, 0x01, 0xC0, 0x1C, + 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0F, 0x00, 0x70, 0x07, 0x00, 0x70, 0x03, + 0x80, 0x38, 0x03, 0x80, 0x1C, 0x01, 0xC0, 0x1C, 0x01, 0xE0, 0x0E, 0x00, + 0xE0, 0x0F, 0x00, 0x70, 0xFF, 0xFF, 0xF8, 0x70, 0xE1, 0xC3, 0x87, 0x0E, + 0x1C, 0x38, 0x70, 0xE1, 0xC3, 0x87, 0x0E, 0x1C, 0x38, 0x70, 0xE1, 0xC3, + 0x87, 0x0E, 0x1C, 0x38, 0x7F, 0xFF, 0xFF, 0x80, 0x00, 0x78, 0x00, 0x03, + 0xF0, 0x00, 0x1F, 0xE0, 0x00, 0xF3, 0xC0, 0x07, 0x87, 0x80, 0x3C, 0x0F, + 0x01, 0xE0, 0x1E, 0x0F, 0x00, 0x3C, 0x78, 0x00, 0x7B, 0xC0, 0x00, 0xF0, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0xF0, 0x70, 0x38, 0x1C, 0x0E, + 0x07, 0x0F, 0xE0, 0x3F, 0xF8, 0x7F, 0xFC, 0x70, 0x1E, 0x40, 0x0E, 0x00, + 0x07, 0x00, 0x07, 0x0F, 0xFF, 0x3F, 0xFF, 0x7F, 0xFF, 0x78, 0x07, 0xE0, + 0x07, 0xE0, 0x07, 0xE0, 0x0F, 0xE0, 0x1F, 0xF8, 0x3F, 0x7F, 0xFF, 0x3F, + 0xF7, 0x0F, 0xC7, 0xE0, 0x00, 0x70, 0x00, 0x38, 0x00, 0x1C, 0x00, 0x0E, + 0x00, 0x07, 0x00, 0x03, 0x80, 0x01, 0xC0, 0x00, 0xE3, 0xF0, 0x77, 0xFE, + 0x3F, 0xFF, 0x9F, 0x83, 0xCF, 0x80, 0xF7, 0x80, 0x3B, 0x80, 0x0F, 0xC0, + 0x07, 0xE0, 0x03, 0xF0, 0x01, 0xF8, 0x00, 0xFC, 0x00, 0x7E, 0x00, 0x3F, + 0x80, 0x3B, 0xE0, 0x3D, 0xF8, 0x3C, 0xFF, 0xFE, 0x77, 0xFE, 0x38, 0xFC, + 0x00, 0x03, 0xF8, 0x1F, 0xFC, 0x7F, 0xF9, 0xF0, 0x37, 0x80, 0x0E, 0x00, + 0x3C, 0x00, 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x03, 0x80, 0x07, 0x00, 0x0F, + 0x00, 0x0E, 0x00, 0x1E, 0x00, 0x1F, 0x03, 0x1F, 0xFE, 0x1F, 0xFC, 0x0F, + 0xE0, 0x00, 0x03, 0x80, 0x01, 0xC0, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x38, + 0x00, 0x1C, 0x00, 0x0E, 0x00, 0x07, 0x07, 0xE3, 0x8F, 0xFD, 0xCF, 0xFF, + 0xE7, 0x83, 0xF7, 0x80, 0xFB, 0x80, 0x3F, 0x80, 0x0F, 0xC0, 0x07, 0xE0, + 0x03, 0xF0, 0x01, 0xF8, 0x00, 0xFC, 0x00, 0x7E, 0x00, 0x3B, 0x80, 0x3D, + 0xE0, 0x3E, 0x78, 0x3F, 0x3F, 0xFF, 0x8F, 0xFD, 0xC1, 0xF8, 0xE0, 0x03, + 0xF8, 0x03, 0xFF, 0x81, 0xFF, 0xF0, 0xF8, 0x3E, 0x78, 0x03, 0x9C, 0x00, + 0xEE, 0x00, 0x1F, 0x80, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x80, 0x00, 0xE0, 0x00, 0x1C, 0x00, 0x07, 0x80, 0x08, 0xF8, 0x0E, 0x1F, + 0xFF, 0x83, 0xFF, 0xC0, 0x3F, 0xC0, 0x03, 0xF0, 0x7F, 0x0F, 0xF1, 0xE0, + 0x1C, 0x01, 0xC0, 0x1C, 0x01, 0xC0, 0xFF, 0xEF, 0xFE, 0xFF, 0xE1, 0xC0, + 0x1C, 0x01, 0xC0, 0x1C, 0x01, 0xC0, 0x1C, 0x01, 0xC0, 0x1C, 0x01, 0xC0, + 0x1C, 0x01, 0xC0, 0x1C, 0x01, 0xC0, 0x1C, 0x01, 0xC0, 0x1C, 0x00, 0x07, + 0xE3, 0x8F, 0xFD, 0xCF, 0xFF, 0xEF, 0x83, 0xF7, 0x80, 0xFB, 0x80, 0x3F, + 0x80, 0x0F, 0xC0, 0x07, 0xE0, 0x03, 0xF0, 0x01, 0xF8, 0x00, 0xFC, 0x00, + 0x7E, 0x00, 0x3B, 0x80, 0x3D, 0xE0, 0x3E, 0xF8, 0x3F, 0x3F, 0xFF, 0x8F, + 0xFD, 0xC1, 0xF8, 0xE0, 0x00, 0x70, 0x00, 0x70, 0x00, 0x78, 0xC0, 0x7C, + 0x7F, 0xFC, 0x3F, 0xFC, 0x07, 0xF8, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, + 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE3, + 0xF0, 0xEF, 0xFC, 0xFF, 0xFE, 0xFC, 0x1E, 0xF0, 0x0F, 0xF0, 0x07, 0xE0, + 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, + 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, + 0x07, 0xFF, 0xF0, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, + 0x0E, 0x1C, 0x38, 0x70, 0x00, 0x00, 0x00, 0x0E, 0x1C, 0x38, 0x70, 0xE1, + 0xC3, 0x87, 0x0E, 0x1C, 0x38, 0x70, 0xE1, 0xC3, 0x87, 0x0E, 0x1C, 0x38, + 0x70, 0xE1, 0xC7, 0xFE, 0xF9, 0xE0, 0xE0, 0x00, 0x70, 0x00, 0x38, 0x00, + 0x1C, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x03, 0x80, 0x01, 0xC0, 0x00, 0xE0, + 0x0F, 0x70, 0x0F, 0x38, 0x1F, 0x1C, 0x1F, 0x0E, 0x1F, 0x07, 0x1E, 0x03, + 0x9E, 0x01, 0xFE, 0x00, 0xFE, 0x00, 0x7F, 0x00, 0x3B, 0xC0, 0x1C, 0xF0, + 0x0E, 0x3C, 0x07, 0x0F, 0x03, 0x83, 0xC1, 0xC0, 0xF0, 0xE0, 0x3C, 0x70, + 0x0F, 0x38, 0x03, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x80, 0xE3, 0xF0, 0x1F, 0x87, 0x7F, 0xE3, 0xFF, 0x3F, 0xFF, + 0xBF, 0xFD, 0xF8, 0x3D, 0xC1, 0xEF, 0x00, 0xF8, 0x07, 0xF8, 0x03, 0xC0, + 0x1F, 0x80, 0x1C, 0x00, 0xFC, 0x00, 0xE0, 0x07, 0xE0, 0x07, 0x00, 0x3F, + 0x00, 0x38, 0x01, 0xF8, 0x01, 0xC0, 0x0F, 0xC0, 0x0E, 0x00, 0x7E, 0x00, + 0x70, 0x03, 0xF0, 0x03, 0x80, 0x1F, 0x80, 0x1C, 0x00, 0xFC, 0x00, 0xE0, + 0x07, 0xE0, 0x07, 0x00, 0x3F, 0x00, 0x38, 0x01, 0xF8, 0x01, 0xC0, 0x0E, + 0xE3, 0xF0, 0xEF, 0xFC, 0xFF, 0xFE, 0xFC, 0x1E, 0xF0, 0x0F, 0xF0, 0x07, + 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, + 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, + 0xE0, 0x07, 0x03, 0xF0, 0x03, 0xFF, 0x03, 0xFF, 0xF0, 0xF0, 0x3C, 0x78, + 0x07, 0x9C, 0x00, 0xEE, 0x00, 0x3F, 0x80, 0x07, 0xE0, 0x01, 0xF8, 0x00, + 0x7E, 0x00, 0x1F, 0x80, 0x07, 0xF0, 0x03, 0xDC, 0x00, 0xE7, 0x80, 0x78, + 0xF0, 0x3C, 0x3F, 0xFF, 0x03, 0xFF, 0x00, 0x3F, 0x00, 0xE3, 0xF0, 0x77, + 0xFE, 0x3F, 0xFF, 0x9F, 0x83, 0xCF, 0x80, 0xF7, 0x80, 0x3B, 0x80, 0x0F, + 0xC0, 0x07, 0xE0, 0x03, 0xF0, 0x01, 0xF8, 0x00, 0xFC, 0x00, 0x7E, 0x00, + 0x3F, 0x80, 0x3B, 0xE0, 0x3D, 0xF8, 0x3C, 0xFF, 0xFE, 0x77, 0xFE, 0x38, + 0xFC, 0x1C, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x03, 0x80, 0x01, 0xC0, 0x00, + 0xE0, 0x00, 0x70, 0x00, 0x00, 0x07, 0xE3, 0x8F, 0xFD, 0xCF, 0xFF, 0xE7, + 0x83, 0xF7, 0x80, 0xFB, 0x80, 0x3F, 0x80, 0x0F, 0xC0, 0x07, 0xE0, 0x03, + 0xF0, 0x01, 0xF8, 0x00, 0xFC, 0x00, 0x7E, 0x00, 0x3B, 0x80, 0x3D, 0xE0, + 0x3E, 0x78, 0x3F, 0x3F, 0xFF, 0x8F, 0xFD, 0xC1, 0xF8, 0xE0, 0x00, 0x70, + 0x00, 0x38, 0x00, 0x1C, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x03, 0x80, 0x01, + 0xC0, 0xE3, 0xFD, 0xFF, 0xFF, 0xFE, 0x0F, 0x01, 0xE0, 0x38, 0x07, 0x00, + 0xE0, 0x1C, 0x03, 0x80, 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0x00, 0xE0, + 0x1C, 0x03, 0x80, 0x00, 0x0F, 0xF0, 0x7F, 0xF9, 0xFF, 0xF7, 0xC0, 0x6E, + 0x00, 0x1C, 0x00, 0x3C, 0x00, 0x3F, 0xC0, 0x3F, 0xF0, 0x1F, 0xF0, 0x03, + 0xF0, 0x00, 0xF0, 0x00, 0xE0, 0x01, 0xE0, 0x03, 0xF8, 0x1F, 0xFF, 0xFC, + 0xFF, 0xF0, 0x3F, 0x80, 0x38, 0x07, 0x00, 0xE0, 0x1C, 0x03, 0x81, 0xFF, + 0xFF, 0xFF, 0xFF, 0x38, 0x07, 0x00, 0xE0, 0x1C, 0x03, 0x80, 0x70, 0x0E, + 0x01, 0xC0, 0x38, 0x07, 0x00, 0xE0, 0x1C, 0x03, 0xC0, 0x3F, 0xC7, 0xF8, + 0x3F, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, + 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, + 0x07, 0xE0, 0x07, 0xE0, 0x0F, 0xF0, 0x0F, 0x78, 0x3F, 0x7F, 0xFF, 0x3F, + 0xF7, 0x0F, 0xC7, 0xE0, 0x00, 0xEE, 0x00, 0x39, 0xC0, 0x07, 0x3C, 0x01, + 0xE3, 0x80, 0x38, 0x70, 0x07, 0x0F, 0x01, 0xE0, 0xE0, 0x38, 0x1C, 0x0F, + 0x01, 0xC1, 0xC0, 0x38, 0x38, 0x07, 0x8F, 0x00, 0x71, 0xC0, 0x0E, 0x38, + 0x00, 0xEE, 0x00, 0x1D, 0xC0, 0x03, 0xF8, 0x00, 0x3E, 0x00, 0x07, 0xC0, + 0x00, 0xE0, 0x1E, 0x01, 0xFC, 0x0F, 0xC0, 0xF7, 0x03, 0xF0, 0x39, 0xC0, + 0xFC, 0x0E, 0x70, 0x3F, 0x03, 0x9E, 0x1C, 0xE1, 0xE3, 0x87, 0x38, 0x70, + 0xE1, 0xCE, 0x1C, 0x38, 0x73, 0x87, 0x07, 0x38, 0x73, 0x81, 0xCE, 0x1C, + 0xE0, 0x73, 0x87, 0x38, 0x1D, 0xE1, 0xEE, 0x03, 0xF0, 0x3F, 0x00, 0xFC, + 0x0F, 0xC0, 0x3F, 0x03, 0xF0, 0x0F, 0xC0, 0xFC, 0x01, 0xE0, 0x1E, 0x00, + 0x78, 0x07, 0x80, 0x78, 0x01, 0xE7, 0x80, 0x78, 0x78, 0x1E, 0x07, 0x03, + 0x80, 0xF0, 0xF0, 0x0F, 0x3C, 0x00, 0xFF, 0x00, 0x0F, 0xC0, 0x00, 0xF0, + 0x00, 0x1E, 0x00, 0x07, 0xE0, 0x01, 0xFE, 0x00, 0x79, 0xC0, 0x1E, 0x3C, + 0x03, 0x83, 0xC0, 0xF0, 0x38, 0x3C, 0x07, 0x8F, 0x00, 0x7B, 0xC0, 0x07, + 0x80, 0xE0, 0x00, 0xEE, 0x00, 0x39, 0xC0, 0x07, 0x1C, 0x01, 0xE3, 0x80, + 0x38, 0x78, 0x0F, 0x07, 0x01, 0xC0, 0xF0, 0x38, 0x0E, 0x0E, 0x01, 0xC1, + 0xC0, 0x1C, 0x78, 0x03, 0x8E, 0x00, 0x79, 0xC0, 0x07, 0x70, 0x00, 0xFE, + 0x00, 0x0F, 0xC0, 0x01, 0xF0, 0x00, 0x1E, 0x00, 0x03, 0x80, 0x00, 0x70, + 0x00, 0x1C, 0x00, 0x03, 0x80, 0x00, 0xF0, 0x01, 0xFC, 0x00, 0x3F, 0x00, + 0x07, 0xC0, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x01, 0xE0, 0x03, + 0xC0, 0x0F, 0x00, 0x3C, 0x00, 0xF0, 0x03, 0xC0, 0x07, 0x80, 0x1E, 0x00, + 0x78, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x3C, 0x00, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xF8, 0x00, 0xF8, 0x1F, 0xC0, 0xFE, 0x0F, 0x00, 0x70, 0x03, + 0x80, 0x1C, 0x00, 0xE0, 0x07, 0x00, 0x38, 0x01, 0xC0, 0x0E, 0x00, 0x70, + 0x07, 0x03, 0xF8, 0x1F, 0x00, 0xFE, 0x00, 0xF0, 0x03, 0xC0, 0x0E, 0x00, + 0x70, 0x03, 0x80, 0x1C, 0x00, 0xE0, 0x07, 0x00, 0x38, 0x01, 0xC0, 0x0E, + 0x00, 0x78, 0x01, 0xFC, 0x0F, 0xE0, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0xF8, 0x07, 0xF0, + 0x3F, 0x80, 0x1E, 0x00, 0x70, 0x03, 0x80, 0x1C, 0x00, 0xE0, 0x07, 0x00, + 0x38, 0x01, 0xC0, 0x0E, 0x00, 0x70, 0x03, 0xC0, 0x0F, 0xE0, 0x1F, 0x03, + 0xF8, 0x1E, 0x01, 0xE0, 0x0E, 0x00, 0x70, 0x03, 0x80, 0x1C, 0x00, 0xE0, + 0x07, 0x00, 0x38, 0x01, 0xC0, 0x0E, 0x00, 0xF0, 0x7F, 0x03, 0xF8, 0x1F, + 0x00, 0x0F, 0xC0, 0x05, 0xFF, 0xE0, 0x7F, 0xFF, 0xFF, 0xFC, 0x1F, 0xFE, + 0xC0, 0x0F, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x80, 0x0F, 0xFE, 0x01, + 0xFF, 0xF0, 0x3E, 0x0F, 0x07, 0x80, 0x30, 0xF0, 0x01, 0x0E, 0x00, 0x00, + 0xE0, 0x00, 0x1C, 0x00, 0x07, 0xFF, 0xF0, 0xFF, 0xFE, 0x01, 0xC0, 0x00, + 0x1C, 0x00, 0x01, 0xC0, 0x00, 0x1C, 0x00, 0x07, 0xFF, 0xC0, 0xFF, 0xF8, + 0x01, 0xC0, 0x00, 0x0E, 0x00, 0x00, 0xE0, 0x00, 0x0F, 0x00, 0x10, 0x78, + 0x03, 0x03, 0xE0, 0xF0, 0x1F, 0xFF, 0x00, 0xFF, 0xE0, 0x03, 0xF8, 0x77, + 0x77, 0x6E, 0xEC, 0x00, 0x7E, 0x01, 0xFC, 0x07, 0xF8, 0x1E, 0x00, 0x38, + 0x00, 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x1F, 0xFC, 0x3F, 0xF8, 0x7F, 0xF0, + 0x1C, 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x03, 0x80, 0x07, + 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x01, 0xC0, + 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x38, 0x00, 0x70, 0x01, + 0xE0, 0x7F, 0x80, 0xFE, 0x01, 0xF8, 0x00, 0x71, 0xDC, 0x77, 0x1D, 0xC7, + 0x61, 0xB8, 0xEE, 0x3B, 0x0C, 0xE0, 0x0E, 0x00, 0xFC, 0x01, 0xC0, 0x1F, + 0x80, 0x38, 0x03, 0xF0, 0x07, 0x00, 0x70, 0x03, 0x80, 0x07, 0x00, 0x0E, + 0x00, 0x1C, 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x7F, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFC, 0x0E, 0x00, 0x1C, 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x01, + 0xC0, 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x38, 0x00, 0x70, + 0x00, 0xE0, 0x01, 0xC0, 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x00, + 0x38, 0x00, 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x38, 0x00, + 0x70, 0x00, 0xE0, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x0E, 0x00, 0x1C, + 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x03, 0x80, 0x07, 0x00, + 0x0E, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0xE0, 0x01, 0xC0, 0x03, + 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x38, 0x00, 0x1F, 0x80, 0x06, + 0x00, 0x00, 0x07, 0xF8, 0x01, 0xC0, 0x00, 0x01, 0xE7, 0x80, 0x30, 0x00, + 0x00, 0x38, 0x70, 0x0C, 0x00, 0x00, 0x0E, 0x07, 0x03, 0x80, 0x00, 0x01, + 0xC0, 0xE0, 0x60, 0x00, 0x00, 0x38, 0x1C, 0x1C, 0x00, 0x00, 0x07, 0x03, + 0x83, 0x00, 0x00, 0x00, 0xE0, 0x70, 0xC0, 0x00, 0x00, 0x1C, 0x0E, 0x38, + 0x00, 0x00, 0x01, 0xC3, 0x86, 0x00, 0x00, 0x00, 0x3C, 0xF1, 0xC0, 0x00, + 0x00, 0x03, 0xFC, 0x30, 0xFC, 0x03, 0xF0, 0x3F, 0x0C, 0x3F, 0xC0, 0xFF, + 0x00, 0x03, 0x8F, 0x3C, 0x3C, 0xF0, 0x00, 0x61, 0xC3, 0x87, 0x0E, 0x00, + 0x1C, 0x70, 0x39, 0xC0, 0xE0, 0x03, 0x0E, 0x07, 0x38, 0x1C, 0x00, 0xC1, + 0xC0, 0xE7, 0x03, 0x80, 0x38, 0x38, 0x1C, 0xE0, 0x70, 0x06, 0x07, 0x03, + 0x9C, 0x0E, 0x01, 0xC0, 0xE0, 0x73, 0x81, 0xC0, 0x30, 0x0E, 0x1C, 0x38, + 0x70, 0x0C, 0x01, 0xE7, 0x87, 0x9E, 0x03, 0x80, 0x1F, 0xE0, 0x7F, 0x80, + 0x60, 0x01, 0xF8, 0x07, 0xE0, 0x01, 0x03, 0x07, 0x0E, 0x1C, 0x38, 0x70, + 0xE0, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03, 0x01, 0x37, 0x76, 0xEE, + 0xEE, 0x77, 0x77, 0x6E, 0xEC, 0x30, 0xDC, 0x77, 0x1D, 0x86, 0xE3, 0xB8, + 0xEE, 0x3B, 0x8E, 0x71, 0xDC, 0x77, 0x1D, 0xC7, 0x61, 0xB8, 0xEE, 0x3B, + 0x0C, 0x1E, 0x1F, 0xE7, 0xFB, 0xFF, 0xFF, 0xFF, 0xFF, 0xFD, 0xFE, 0x7F, + 0x87, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x38, 0x1F, 0xFC, + 0xF0, 0xF1, 0x83, 0xE7, 0xC6, 0x0D, 0x9B, 0x18, 0x33, 0xCC, 0x60, 0xCF, + 0x31, 0x83, 0x18, 0xC6, 0x0C, 0x03, 0x18, 0x30, 0x0C, 0x60, 0xC0, 0x30, + 0x80, 0xC0, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x07, 0x0E, 0x1C, 0x38, + 0x70, 0xE0, 0xC0, 0x80, 0x00, 0x01, 0xE0, 0x78, 0x0E, 0x03, 0x80, 0xE0, + 0x38, 0x00, 0x07, 0x8F, 0xF1, 0xFE, 0x3F, 0xC7, 0x80, 0x03, 0xC0, 0x00, + 0x0F, 0x00, 0x00, 0x1D, 0xF0, 0x00, 0x73, 0xE0, 0x01, 0xC7, 0xC0, 0x07, + 0x1D, 0xC0, 0x00, 0x3B, 0x80, 0x00, 0x77, 0x00, 0x01, 0xC7, 0x00, 0x03, + 0x8E, 0x00, 0x0F, 0x1E, 0x00, 0x1C, 0x1C, 0x00, 0x38, 0x38, 0x00, 0xF0, + 0x78, 0x01, 0xC0, 0x70, 0x03, 0x80, 0xE0, 0x0E, 0x00, 0xE0, 0x1C, 0x01, + 0xC0, 0x78, 0x03, 0xC0, 0xFF, 0xFF, 0x81, 0xFF, 0xFF, 0x07, 0xFF, 0xFF, + 0x0E, 0x00, 0x0E, 0x1C, 0x00, 0x1C, 0x70, 0x00, 0x1C, 0xE0, 0x00, 0x39, + 0xC0, 0x00, 0x77, 0x00, 0x00, 0x70, 0x00, 0xFE, 0x00, 0xFF, 0xC0, 0xFF, + 0xE0, 0xF0, 0x30, 0x70, 0x00, 0x70, 0x00, 0x38, 0x00, 0x1C, 0x00, 0x0E, + 0x00, 0x07, 0x00, 0x03, 0x80, 0x01, 0xC0, 0x00, 0xE0, 0x03, 0xFF, 0xE1, + 0xFF, 0xF0, 0xFF, 0xF8, 0x0E, 0x00, 0x07, 0x00, 0x03, 0x80, 0x01, 0xC0, + 0x00, 0xE0, 0x00, 0x70, 0x00, 0x38, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xC0, 0x40, 0x00, 0x5C, 0x00, 0x1D, 0xE7, 0xCF, 0x1F, 0xFF, 0xC1, + 0xFF, 0xF0, 0x3C, 0x1E, 0x07, 0x01, 0xC1, 0xC0, 0x1C, 0x38, 0x03, 0x87, + 0x00, 0x70, 0xE0, 0x0E, 0x1C, 0x01, 0xC1, 0xC0, 0x70, 0x3C, 0x1E, 0x0F, + 0xFF, 0xC1, 0xFF, 0xFC, 0x79, 0xF1, 0xDC, 0x00, 0x1D, 0x00, 0x01, 0x00, + 0xF0, 0x01, 0xEE, 0x00, 0x39, 0xE0, 0x0F, 0x1C, 0x01, 0xC3, 0xC0, 0x78, + 0x38, 0x0E, 0x07, 0x83, 0xC0, 0x70, 0x70, 0x0F, 0x1E, 0x00, 0xE3, 0x80, + 0x1E, 0xF0, 0x3F, 0xDF, 0xE7, 0xFF, 0xFC, 0x03, 0xE0, 0x00, 0x7C, 0x00, + 0x07, 0x00, 0x00, 0xE0, 0x0F, 0xFF, 0xF9, 0xFF, 0xFF, 0x00, 0x70, 0x00, + 0x0E, 0x00, 0x01, 0xC0, 0x00, 0x38, 0x00, 0x07, 0x00, 0x00, 0xE0, 0x00, + 0x1C, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x03, 0xFF, 0xFF, 0xFF, + 0xFF, 0xF8, 0x0F, 0xE0, 0xFF, 0xC3, 0x87, 0x1C, 0x04, 0x70, 0x01, 0xC0, + 0x07, 0x80, 0x0F, 0x00, 0x1F, 0x00, 0xFE, 0x07, 0x3E, 0x38, 0x7C, 0xE0, + 0x7B, 0x80, 0xFE, 0x01, 0xFC, 0x07, 0x7C, 0x1C, 0xF8, 0xE0, 0xFB, 0x81, + 0xF8, 0x01, 0xF0, 0x03, 0xC0, 0x07, 0x80, 0x0E, 0x00, 0x39, 0x00, 0xE7, + 0x07, 0x1F, 0xF8, 0x1F, 0xC0, 0xF1, 0xFE, 0x3F, 0xC7, 0xF8, 0xF0, 0x00, + 0x7F, 0x00, 0x00, 0xFF, 0xE0, 0x01, 0xE0, 0x3C, 0x01, 0xC0, 0x07, 0x01, + 0xC0, 0x01, 0xC1, 0xC3, 0xF8, 0x71, 0xC3, 0xFE, 0x18, 0xC3, 0xC1, 0x06, + 0x63, 0x80, 0x03, 0x63, 0xC0, 0x00, 0xF1, 0xC0, 0x00, 0x78, 0xE0, 0x00, + 0x3C, 0x70, 0x00, 0x1E, 0x38, 0x00, 0x0F, 0x1C, 0x00, 0x07, 0x8F, 0x00, + 0x03, 0x63, 0x80, 0x03, 0x30, 0xF0, 0x41, 0x9C, 0x3F, 0xE1, 0xC7, 0x0F, + 0xE1, 0xC1, 0xC0, 0x01, 0xC0, 0x70, 0x01, 0xC0, 0x1E, 0x03, 0xC0, 0x03, + 0xFF, 0x80, 0x00, 0x7F, 0x00, 0x00, 0x01, 0x02, 0x06, 0x0C, 0x1C, 0x38, + 0x70, 0xE1, 0xC3, 0x87, 0x0E, 0x1C, 0x38, 0x70, 0xE0, 0xE1, 0xC0, 0xE1, + 0xC0, 0xE1, 0xC0, 0xE1, 0xC0, 0xE1, 0xC0, 0xE1, 0xC0, 0xC1, 0x80, 0x81, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x07, 0x00, + 0x00, 0x1C, 0x00, 0x00, 0x70, 0x00, 0x01, 0xC0, 0x00, 0x07, 0x00, 0x00, + 0x1C, 0x00, 0x00, 0x70, 0xFF, 0xFF, 0xFF, 0xE0, 0x00, 0x7F, 0x00, 0x00, + 0xFF, 0xE0, 0x01, 0xE0, 0x3C, 0x01, 0xC0, 0x07, 0x01, 0xC0, 0x01, 0xC1, + 0xC7, 0xF8, 0x71, 0xC3, 0xFE, 0x18, 0xC1, 0xC7, 0x86, 0x60, 0xE1, 0xC3, + 0x60, 0x70, 0xE0, 0xF0, 0x38, 0xF0, 0x78, 0x1F, 0xF0, 0x3C, 0x0F, 0xE0, + 0x1E, 0x07, 0x38, 0x0F, 0x03, 0x8C, 0x07, 0x81, 0xC7, 0x03, 0x60, 0xE3, + 0x83, 0x30, 0x70, 0xE1, 0x9C, 0x38, 0x71, 0xC7, 0x1C, 0x1D, 0xC1, 0xC0, + 0x01, 0xC0, 0x70, 0x01, 0xC0, 0x1E, 0x03, 0xC0, 0x03, 0xFF, 0x80, 0x00, + 0x7F, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x1F, 0x07, 0xF1, 0xC7, 0x70, 0x7C, 0x07, + 0x80, 0xF0, 0x1F, 0x07, 0x71, 0xC7, 0xF0, 0x7C, 0x00, 0x00, 0x38, 0x00, + 0x00, 0x70, 0x00, 0x00, 0xE0, 0x00, 0x01, 0xC0, 0x00, 0x03, 0x80, 0x00, + 0x07, 0x00, 0x00, 0x0E, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFC, 0x00, 0xE0, 0x00, 0x01, 0xC0, 0x00, 0x03, 0x80, 0x00, 0x07, + 0x00, 0x00, 0x0E, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xC0, 0x7E, 0x3F, 0xEC, 0x1C, 0x03, 0x00, 0xC0, 0x70, 0x18, 0x0C, 0x0E, + 0x07, 0x03, 0x81, 0xC0, 0xFF, 0xFF, 0xF0, 0x1F, 0x8F, 0xF9, 0x83, 0x80, + 0x30, 0x0E, 0x3F, 0x87, 0xF0, 0x07, 0x00, 0x60, 0x0C, 0x01, 0xC0, 0xFF, + 0xFC, 0xFE, 0x00, 0x0F, 0x1E, 0x1C, 0x38, 0x70, 0xE0, 0x01, 0xE0, 0x78, + 0x0E, 0x03, 0x80, 0xE0, 0x38, 0x00, 0x07, 0x8F, 0xF1, 0xFE, 0x3F, 0xC7, + 0x80, 0x03, 0xC0, 0x00, 0x0F, 0x00, 0x00, 0x1D, 0xF0, 0x00, 0x73, 0xE0, + 0x01, 0xC7, 0xC0, 0x07, 0x1D, 0xC0, 0x00, 0x3B, 0x80, 0x00, 0x77, 0x00, + 0x01, 0xC7, 0x00, 0x03, 0x8E, 0x00, 0x0F, 0x1E, 0x00, 0x1C, 0x1C, 0x00, + 0x38, 0x38, 0x00, 0xF0, 0x78, 0x01, 0xC0, 0x70, 0x03, 0x80, 0xE0, 0x0E, + 0x00, 0xE0, 0x1C, 0x01, 0xC0, 0x78, 0x03, 0xC0, 0xFF, 0xFF, 0x81, 0xFF, + 0xFF, 0x07, 0xFF, 0xFF, 0x0E, 0x00, 0x0E, 0x1C, 0x00, 0x1C, 0x70, 0x00, + 0x1C, 0xE0, 0x00, 0x39, 0xC0, 0x00, 0x77, 0x00, 0x00, 0x70, 0xFF, 0xF0, + 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x77, 0xFF, 0xF9, 0xCF, 0xFF, 0xF7, + 0x1F, 0xFF, 0xEC, 0x38, 0x00, 0x00, 0x70, 0x00, 0x00, 0xE0, 0x00, 0x01, + 0xC0, 0x00, 0x03, 0x80, 0x00, 0x07, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x1C, + 0x00, 0x00, 0x3F, 0xFF, 0x80, 0x7F, 0xFF, 0x00, 0xFF, 0xFE, 0x01, 0xC0, + 0x00, 0x03, 0x80, 0x00, 0x07, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x1C, 0x00, + 0x00, 0x38, 0x00, 0x00, 0x70, 0x00, 0x00, 0xE0, 0x00, 0x01, 0xC0, 0x00, + 0x03, 0xFF, 0xFC, 0x07, 0xFF, 0xF8, 0x0F, 0xFF, 0xF0, 0x07, 0x00, 0x00, + 0x03, 0x80, 0x00, 0x01, 0xDC, 0x00, 0x1C, 0xE7, 0x00, 0x07, 0x31, 0xC0, + 0x01, 0xD8, 0x70, 0x00, 0x70, 0x1C, 0x00, 0x1C, 0x07, 0x00, 0x07, 0x01, + 0xC0, 0x01, 0xC0, 0x70, 0x00, 0x70, 0x1C, 0x00, 0x1C, 0x07, 0x00, 0x07, + 0x01, 0xC0, 0x01, 0xC0, 0x7F, 0xFF, 0xF0, 0x1F, 0xFF, 0xFC, 0x07, 0xFF, + 0xFF, 0x01, 0xC0, 0x01, 0xC0, 0x70, 0x00, 0x70, 0x1C, 0x00, 0x1C, 0x07, + 0x00, 0x07, 0x01, 0xC0, 0x01, 0xC0, 0x70, 0x00, 0x70, 0x1C, 0x00, 0x1C, + 0x07, 0x00, 0x07, 0x01, 0xC0, 0x01, 0xC0, 0x70, 0x00, 0x70, 0x1C, 0x00, + 0x1C, 0x07, 0x00, 0x07, 0x07, 0x03, 0x81, 0xDC, 0xE7, 0x71, 0xD8, 0x70, + 0x1C, 0x07, 0x01, 0xC0, 0x70, 0x1C, 0x07, 0x01, 0xC0, 0x70, 0x1C, 0x07, + 0x01, 0xC0, 0x70, 0x1C, 0x07, 0x01, 0xC0, 0x70, 0x1C, 0x07, 0x01, 0xC0, + 0x70, 0x1C, 0x07, 0x81, 0x01, 0x83, 0x03, 0x87, 0x03, 0x87, 0x03, 0x87, + 0x03, 0x87, 0x03, 0x87, 0x03, 0x87, 0x07, 0x0E, 0x1C, 0x38, 0x70, 0xE1, + 0xC3, 0x87, 0x0E, 0x1C, 0x38, 0x30, 0x60, 0x40, 0x80, 0x07, 0x00, 0x00, + 0x01, 0xC0, 0x00, 0x00, 0x70, 0x7F, 0x80, 0x1C, 0x3F, 0xFC, 0x03, 0x1F, + 0xFF, 0xC0, 0xC7, 0xE0, 0x7C, 0x01, 0xE0, 0x03, 0xC0, 0x78, 0x00, 0x3C, + 0x0E, 0x00, 0x03, 0x81, 0xC0, 0x00, 0x78, 0x70, 0x00, 0x07, 0x0E, 0x00, + 0x00, 0xE1, 0xC0, 0x00, 0x1E, 0x38, 0x00, 0x01, 0xC7, 0x00, 0x00, 0x38, + 0xE0, 0x00, 0x07, 0x1C, 0x00, 0x00, 0xE3, 0x80, 0x00, 0x3C, 0x70, 0x00, + 0x07, 0x0E, 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x3C, 0x1C, 0x00, 0x07, 0x03, + 0xC0, 0x01, 0xE0, 0x3C, 0x00, 0x78, 0x03, 0xF0, 0x3E, 0x00, 0x3F, 0xFF, + 0x80, 0x03, 0xFF, 0xE0, 0x00, 0x0F, 0xE0, 0x00, 0x3C, 0x00, 0x07, 0x07, + 0xE0, 0x00, 0x30, 0x33, 0x00, 0x03, 0x00, 0x18, 0x00, 0x38, 0x00, 0xC0, + 0x01, 0x80, 0x06, 0x00, 0x1C, 0x00, 0x30, 0x01, 0xC0, 0x01, 0x80, 0x0C, + 0x00, 0x0C, 0x00, 0xE0, 0x00, 0x60, 0x06, 0x00, 0x03, 0x00, 0x60, 0x00, + 0x18, 0x07, 0x1F, 0x8F, 0xFC, 0x31, 0xFF, 0x7F, 0xE3, 0x08, 0x1C, 0x00, + 0x38, 0x00, 0x60, 0x01, 0x80, 0x03, 0x00, 0x18, 0x00, 0x30, 0x01, 0xC0, + 0x03, 0x80, 0x0C, 0x00, 0x38, 0x00, 0xE0, 0x03, 0x80, 0x06, 0x00, 0x38, + 0x00, 0x60, 0x03, 0x80, 0x07, 0x00, 0x38, 0x00, 0x30, 0x03, 0xFF, 0x03, + 0x00, 0x1F, 0xF8, 0x38, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0xE0, + 0x00, 0x00, 0x1C, 0xE0, 0x00, 0xF3, 0x8F, 0x00, 0x0E, 0x70, 0x78, 0x01, + 0xE6, 0x03, 0x80, 0x3C, 0x00, 0x3C, 0x03, 0x80, 0x01, 0xE0, 0x78, 0x00, + 0x0E, 0x0F, 0x00, 0x00, 0xF0, 0xE0, 0x00, 0x07, 0x1E, 0x00, 0x00, 0x3B, + 0xC0, 0x00, 0x03, 0xF8, 0x00, 0x00, 0x1F, 0x80, 0x00, 0x00, 0xF0, 0x00, + 0x00, 0x0E, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, + 0xE0, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x0E, 0x00, + 0x00, 0x00, 0xE0, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, + 0x0E, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x0E, 0x00, 0x0F, 0x00, 0x00, + 0x01, 0xC0, 0x00, 0x00, 0x70, 0x7F, 0x80, 0x1C, 0x3F, 0xFC, 0x07, 0x1F, + 0xFF, 0xE1, 0xC7, 0xE0, 0x7E, 0x01, 0xF0, 0x03, 0xE0, 0x38, 0x00, 0x3C, + 0x0F, 0x00, 0x03, 0xC1, 0xC0, 0x00, 0x38, 0x78, 0x00, 0x07, 0x0E, 0x00, + 0x00, 0x71, 0xC0, 0x00, 0x0E, 0x38, 0x00, 0x01, 0xC7, 0x00, 0x00, 0x38, + 0xE0, 0x00, 0x07, 0x1C, 0x00, 0x00, 0xE3, 0x80, 0x00, 0x3C, 0x38, 0x00, + 0x07, 0x07, 0x00, 0x00, 0xE0, 0xF0, 0x00, 0x38, 0x0E, 0x00, 0x07, 0x00, + 0xE0, 0x01, 0xC0, 0x0E, 0x00, 0x70, 0x00, 0xE0, 0x1C, 0x03, 0xFE, 0x07, + 0xFC, 0x7F, 0xC0, 0xFF, 0x8F, 0xF8, 0x1F, 0xF0, 0x00, 0xE0, 0x38, 0x0E, + 0x03, 0x80, 0x60, 0x18, 0x00, 0x07, 0x8F, 0xF1, 0xFE, 0x3F, 0xC7, 0x80, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x1C, 0x03, 0x80, 0x70, 0x0E, 0x01, + 0xC0, 0x38, 0x07, 0x00, 0xE0, 0x1C, 0x03, 0x80, 0x70, 0x0E, 0x01, 0xC0, + 0x3C, 0x03, 0x80, 0x7F, 0x07, 0xE0, 0x7C, 0x00, 0x7C, 0x00, 0x00, 0xF8, + 0x00, 0x01, 0xF0, 0x00, 0x07, 0x70, 0x00, 0x0E, 0xE0, 0x00, 0x1D, 0xC0, + 0x00, 0x71, 0xC0, 0x00, 0xE3, 0x80, 0x03, 0xC7, 0x80, 0x07, 0x07, 0x00, + 0x0E, 0x0E, 0x00, 0x3C, 0x1E, 0x00, 0x70, 0x1C, 0x00, 0xE0, 0x38, 0x03, + 0x80, 0x38, 0x07, 0x00, 0x70, 0x1E, 0x00, 0xF0, 0x3F, 0xFF, 0xE0, 0x7F, + 0xFF, 0xC1, 0xFF, 0xFF, 0xC3, 0x80, 0x03, 0x87, 0x00, 0x07, 0x1C, 0x00, + 0x07, 0x38, 0x00, 0x0E, 0x70, 0x00, 0x1D, 0xC0, 0x00, 0x1C, 0xFF, 0xF8, + 0x3F, 0xFF, 0x8F, 0xFF, 0xF3, 0x80, 0x3C, 0xE0, 0x07, 0xB8, 0x00, 0xEE, + 0x00, 0x3B, 0x80, 0x0E, 0xE0, 0x03, 0xB8, 0x01, 0xEE, 0x00, 0xF3, 0xFF, + 0xF8, 0xFF, 0xFC, 0x3F, 0xFF, 0xCE, 0x00, 0xFB, 0x80, 0x0E, 0xE0, 0x01, + 0xF8, 0x00, 0x7E, 0x00, 0x1F, 0x80, 0x07, 0xE0, 0x01, 0xF8, 0x00, 0xFE, + 0x00, 0xFB, 0xFF, 0xFC, 0xFF, 0xFE, 0x3F, 0xFE, 0x00, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x38, 0x00, 0x70, 0x00, + 0xE0, 0x01, 0xC0, 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x38, + 0x00, 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, + 0x1C, 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x00, 0x00, 0x7C, + 0x00, 0x00, 0xF8, 0x00, 0x01, 0xF0, 0x00, 0x07, 0x70, 0x00, 0x0E, 0xE0, + 0x00, 0x1D, 0xC0, 0x00, 0x71, 0xC0, 0x00, 0xE3, 0x80, 0x03, 0xC7, 0x80, + 0x07, 0x07, 0x00, 0x0E, 0x0E, 0x00, 0x3C, 0x1E, 0x00, 0x70, 0x1C, 0x00, + 0xE0, 0x38, 0x03, 0x80, 0x38, 0x07, 0x00, 0x70, 0x1E, 0x00, 0xF0, 0x38, + 0x00, 0xE0, 0x70, 0x01, 0xC1, 0xE0, 0x03, 0xC3, 0x80, 0x03, 0x87, 0x00, + 0x07, 0x1C, 0x00, 0x07, 0x3F, 0xFF, 0xFE, 0x7F, 0xFF, 0xFD, 0xFF, 0xFF, + 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x00, 0xE0, 0x00, 0xE0, + 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xFF, + 0xFE, 0xFF, 0xFE, 0xFF, 0xFE, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, + 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0xFF, 0xFF, 0xBF, 0xFF, + 0xFC, 0x00, 0x01, 0xC0, 0x00, 0x1E, 0x00, 0x01, 0xE0, 0x00, 0x1E, 0x00, + 0x01, 0xE0, 0x00, 0x0E, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, + 0x00, 0x0F, 0x00, 0x00, 0x78, 0x00, 0x07, 0x80, 0x00, 0x78, 0x00, 0x07, + 0x80, 0x00, 0x38, 0x00, 0x03, 0xC0, 0x00, 0x3C, 0x00, 0x03, 0xC0, 0x00, + 0x3C, 0x00, 0x01, 0xC0, 0x00, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xC0, 0xE0, 0x00, 0xFC, 0x00, 0x1F, 0x80, 0x03, 0xF0, 0x00, 0x7E, + 0x00, 0x0F, 0xC0, 0x01, 0xF8, 0x00, 0x3F, 0x00, 0x07, 0xE0, 0x00, 0xFC, + 0x00, 0x1F, 0x80, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, + 0x00, 0x3F, 0x00, 0x07, 0xE0, 0x00, 0xFC, 0x00, 0x1F, 0x80, 0x03, 0xF0, + 0x00, 0x7E, 0x00, 0x0F, 0xC0, 0x01, 0xF8, 0x00, 0x3F, 0x00, 0x07, 0xE0, + 0x00, 0xFC, 0x00, 0x1C, 0x00, 0xFF, 0x00, 0x03, 0xFF, 0xC0, 0x0F, 0xFF, + 0xF0, 0x1F, 0x81, 0xF8, 0x3E, 0x00, 0x7C, 0x3C, 0x00, 0x3C, 0x78, 0x00, + 0x1E, 0x70, 0x00, 0x0E, 0x70, 0x00, 0x0E, 0xE0, 0x00, 0x07, 0xE0, 0x00, + 0x07, 0xE3, 0xFF, 0xC7, 0xE3, 0xFF, 0xC7, 0xE3, 0xFF, 0xC7, 0xE0, 0x00, + 0x07, 0xE0, 0x00, 0x07, 0xE0, 0x00, 0x07, 0x70, 0x00, 0x0E, 0x70, 0x00, + 0x0E, 0x78, 0x00, 0x1E, 0x3C, 0x00, 0x3C, 0x3E, 0x00, 0x7C, 0x1F, 0x81, + 0xF8, 0x0F, 0xFF, 0xF0, 0x03, 0xFF, 0xC0, 0x00, 0xFF, 0x00, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0xE0, 0x03, 0xCE, 0x00, + 0x78, 0xE0, 0x0F, 0x0E, 0x01, 0xE0, 0xE0, 0x3C, 0x0E, 0x07, 0x80, 0xE0, + 0xF0, 0x0E, 0x1E, 0x00, 0xE3, 0xC0, 0x0E, 0x78, 0x00, 0xEF, 0x00, 0x0F, + 0xE0, 0x00, 0xFE, 0x00, 0x0F, 0xF0, 0x00, 0xEF, 0x80, 0x0E, 0x7C, 0x00, + 0xE3, 0xE0, 0x0E, 0x1F, 0x00, 0xE0, 0xF8, 0x0E, 0x07, 0xC0, 0xE0, 0x3E, + 0x0E, 0x01, 0xF0, 0xE0, 0x0F, 0x8E, 0x00, 0x7C, 0xE0, 0x03, 0xEE, 0x00, + 0x1F, 0x00, 0x7C, 0x00, 0x00, 0xF8, 0x00, 0x01, 0xF0, 0x00, 0x07, 0x70, + 0x00, 0x0E, 0xE0, 0x00, 0x1D, 0xC0, 0x00, 0x71, 0xC0, 0x00, 0xE3, 0x80, + 0x03, 0xC7, 0x80, 0x07, 0x07, 0x00, 0x0E, 0x0E, 0x00, 0x3C, 0x1E, 0x00, + 0x70, 0x1C, 0x00, 0xE0, 0x38, 0x03, 0x80, 0x38, 0x07, 0x00, 0x70, 0x1E, + 0x00, 0xF0, 0x38, 0x00, 0xE0, 0x70, 0x01, 0xC1, 0xE0, 0x03, 0xC3, 0x80, + 0x03, 0x87, 0x00, 0x07, 0x1C, 0x00, 0x07, 0x38, 0x00, 0x0E, 0x70, 0x00, + 0x1D, 0xC0, 0x00, 0x1C, 0xF8, 0x00, 0x3F, 0xF8, 0x00, 0xFF, 0xF0, 0x01, + 0xFF, 0xE0, 0x03, 0xFF, 0xE0, 0x0F, 0xFD, 0xC0, 0x1D, 0xFB, 0x80, 0x3B, + 0xF3, 0x80, 0xE7, 0xE7, 0x01, 0xCF, 0xCF, 0x07, 0x9F, 0x8E, 0x0E, 0x3F, + 0x1C, 0x1C, 0x7E, 0x1C, 0x70, 0xFC, 0x38, 0xE1, 0xF8, 0x71, 0xC3, 0xF0, + 0x77, 0x07, 0xE0, 0xEE, 0x0F, 0xC1, 0xFC, 0x1F, 0x81, 0xF0, 0x3F, 0x03, + 0xE0, 0x7E, 0x03, 0x80, 0xFC, 0x00, 0x01, 0xF8, 0x00, 0x03, 0xF0, 0x00, + 0x07, 0xE0, 0x00, 0x0F, 0xC0, 0x00, 0x1C, 0xF8, 0x00, 0xFF, 0x00, 0x1F, + 0xF0, 0x03, 0xFE, 0x00, 0x7F, 0xE0, 0x0F, 0xDC, 0x01, 0xFB, 0xC0, 0x3F, + 0x38, 0x07, 0xE7, 0x80, 0xFC, 0x70, 0x1F, 0x8F, 0x03, 0xF0, 0xE0, 0x7E, + 0x0E, 0x0F, 0xC1, 0xC1, 0xF8, 0x1C, 0x3F, 0x03, 0xC7, 0xE0, 0x38, 0xFC, + 0x07, 0x9F, 0x80, 0x73, 0xF0, 0x0F, 0x7E, 0x00, 0xEF, 0xC0, 0x1F, 0xF8, + 0x01, 0xFF, 0x00, 0x3F, 0xE0, 0x03, 0xFC, 0x00, 0x7C, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFC, 0x3F, 0xFC, 0x3F, + 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x00, 0xFF, 0x00, 0x03, 0xFF, 0xC0, 0x0F, 0xFF, 0xF0, 0x1F, 0x81, + 0xF8, 0x3E, 0x00, 0x7C, 0x3C, 0x00, 0x3C, 0x78, 0x00, 0x1E, 0x70, 0x00, + 0x0E, 0x70, 0x00, 0x0E, 0xE0, 0x00, 0x07, 0xE0, 0x00, 0x07, 0xE0, 0x00, + 0x07, 0xE0, 0x00, 0x07, 0xE0, 0x00, 0x07, 0xE0, 0x00, 0x07, 0xE0, 0x00, + 0x07, 0xE0, 0x00, 0x07, 0x70, 0x00, 0x0E, 0x70, 0x00, 0x0E, 0x78, 0x00, + 0x1E, 0x3C, 0x00, 0x3C, 0x3E, 0x00, 0x7C, 0x1F, 0x81, 0xF8, 0x0F, 0xFF, + 0xF0, 0x03, 0xFF, 0xC0, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xF0, 0x00, 0x7E, 0x00, 0x0F, 0xC0, 0x01, 0xF8, 0x00, 0x3F, + 0x00, 0x07, 0xE0, 0x00, 0xFC, 0x00, 0x1F, 0x80, 0x03, 0xF0, 0x00, 0x7E, + 0x00, 0x0F, 0xC0, 0x01, 0xF8, 0x00, 0x3F, 0x00, 0x07, 0xE0, 0x00, 0xFC, + 0x00, 0x1F, 0x80, 0x03, 0xF0, 0x00, 0x7E, 0x00, 0x0F, 0xC0, 0x01, 0xF8, + 0x00, 0x3F, 0x00, 0x07, 0xE0, 0x00, 0xFC, 0x00, 0x1C, 0xFF, 0xE0, 0xFF, + 0xF8, 0xFF, 0xFC, 0xE0, 0x3E, 0xE0, 0x0F, 0xE0, 0x07, 0xE0, 0x07, 0xE0, + 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x0F, 0xE0, 0x3E, 0xFF, 0xFC, 0xFF, + 0xF8, 0xFF, 0xE0, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, + 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, + 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x78, 0x00, 0x3C, + 0x00, 0x1E, 0x00, 0x0F, 0x00, 0x07, 0x80, 0x03, 0xC0, 0x01, 0xE0, 0x00, + 0xF0, 0x00, 0xF0, 0x01, 0xE0, 0x03, 0xC0, 0x03, 0x80, 0x07, 0x80, 0x0F, + 0x00, 0x1E, 0x00, 0x1C, 0x00, 0x3C, 0x00, 0x78, 0x00, 0xF0, 0x00, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFE, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0x38, 0x00, 0x01, 0xC0, 0x00, + 0x0E, 0x00, 0x00, 0x70, 0x00, 0x03, 0x80, 0x00, 0x1C, 0x00, 0x00, 0xE0, + 0x00, 0x07, 0x00, 0x00, 0x38, 0x00, 0x01, 0xC0, 0x00, 0x0E, 0x00, 0x00, + 0x70, 0x00, 0x03, 0x80, 0x00, 0x1C, 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, + 0x00, 0x38, 0x00, 0x01, 0xC0, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x03, + 0x80, 0x00, 0xF0, 0x00, 0x7B, 0xC0, 0x07, 0x8E, 0x00, 0x38, 0x78, 0x03, + 0xC1, 0xE0, 0x3C, 0x07, 0x01, 0xC0, 0x3C, 0x1E, 0x00, 0xF1, 0xE0, 0x03, + 0x8E, 0x00, 0x1E, 0xF0, 0x00, 0x7F, 0x00, 0x01, 0xF0, 0x00, 0x0F, 0x80, + 0x00, 0x38, 0x00, 0x01, 0xC0, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x03, + 0x80, 0x00, 0x1C, 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0x38, 0x00, + 0x01, 0xC0, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x03, 0x80, 0x00, 0x00, + 0x1C, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x07, 0x00, 0x00, 0x1F, 0xF0, 0x00, + 0x7F, 0xFF, 0x00, 0x7F, 0xFF, 0xC0, 0x7E, 0x73, 0xF0, 0x78, 0x38, 0x3C, + 0x78, 0x1C, 0x0F, 0x38, 0x0E, 0x03, 0xB8, 0x07, 0x00, 0xFC, 0x03, 0x80, + 0x7E, 0x01, 0xC0, 0x3F, 0x00, 0xE0, 0x1F, 0x80, 0x70, 0x0F, 0xC0, 0x38, + 0x07, 0x70, 0x1C, 0x07, 0x3C, 0x0E, 0x07, 0x8F, 0x07, 0x07, 0x83, 0xF3, + 0x9F, 0x80, 0xFF, 0xFF, 0x80, 0x3F, 0xFF, 0x80, 0x03, 0xFE, 0x00, 0x00, + 0x38, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x78, 0x00, 0x78, + 0xF0, 0x03, 0xC1, 0xC0, 0x0E, 0x07, 0x80, 0x78, 0x0F, 0x03, 0xC0, 0x1C, + 0x0E, 0x00, 0x78, 0x78, 0x00, 0xF3, 0xC0, 0x01, 0xCE, 0x00, 0x07, 0xF8, + 0x00, 0x0F, 0xC0, 0x00, 0x1E, 0x00, 0x00, 0x78, 0x00, 0x03, 0xF0, 0x00, + 0x0F, 0xC0, 0x00, 0x7F, 0x80, 0x03, 0xCF, 0x00, 0x0E, 0x1C, 0x00, 0x78, + 0x78, 0x03, 0xC0, 0xF0, 0x0E, 0x01, 0xC0, 0x78, 0x07, 0x83, 0xC0, 0x0F, + 0x0E, 0x00, 0x1C, 0x78, 0x00, 0x7B, 0xC0, 0x00, 0xF0, 0xE0, 0x1C, 0x03, + 0xF0, 0x0E, 0x01, 0xF8, 0x07, 0x00, 0xFC, 0x03, 0x80, 0x7E, 0x01, 0xC0, + 0x3F, 0x00, 0xE0, 0x1F, 0x80, 0x70, 0x0F, 0xC0, 0x38, 0x07, 0xE0, 0x1C, + 0x03, 0xF0, 0x0E, 0x01, 0xF8, 0x07, 0x00, 0xFE, 0x03, 0x80, 0xE7, 0x01, + 0xC0, 0x73, 0x80, 0xE0, 0x38, 0xE0, 0x70, 0x38, 0x78, 0x38, 0x3C, 0x1E, + 0x1C, 0x3C, 0x07, 0xCE, 0x7C, 0x01, 0xFF, 0xFC, 0x00, 0x7F, 0xFC, 0x00, + 0x0F, 0xF8, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x70, 0x00, 0x00, 0x38, 0x00, + 0x00, 0x1C, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x03, 0xFF, + 0xC0, 0x0F, 0xFF, 0xF0, 0x1F, 0x81, 0xF8, 0x3E, 0x00, 0x7C, 0x3C, 0x00, + 0x1C, 0x78, 0x00, 0x1E, 0x70, 0x00, 0x0E, 0xF0, 0x00, 0x0F, 0xE0, 0x00, + 0x07, 0xE0, 0x00, 0x07, 0xE0, 0x00, 0x07, 0xE0, 0x00, 0x07, 0xE0, 0x00, + 0x07, 0xE0, 0x00, 0x07, 0xE0, 0x00, 0x07, 0x70, 0x00, 0x0E, 0x70, 0x00, + 0x0E, 0x78, 0x00, 0x1E, 0x38, 0x00, 0x1C, 0x1C, 0x00, 0x38, 0x0E, 0x00, + 0x70, 0x07, 0x00, 0xE0, 0xFF, 0x81, 0xFF, 0xFF, 0x81, 0xFF, 0xFF, 0x81, + 0xFF, 0xF1, 0xFE, 0x3F, 0xC7, 0xF8, 0xF0, 0x00, 0x00, 0x03, 0x80, 0x70, + 0x0E, 0x01, 0xC0, 0x38, 0x07, 0x00, 0xE0, 0x1C, 0x03, 0x80, 0x70, 0x0E, + 0x01, 0xC0, 0x38, 0x07, 0x00, 0xE0, 0x1C, 0x03, 0x80, 0x70, 0x0E, 0x01, + 0xC0, 0x38, 0x07, 0x00, 0xE0, 0x1C, 0x03, 0x80, 0x70, 0x07, 0x8F, 0x00, + 0x3C, 0x78, 0x01, 0xE3, 0xC0, 0x0F, 0x1E, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x03, 0xC0, 0x01, 0xEF, 0x00, 0x1E, 0x38, 0x00, 0xE1, 0xE0, 0x0F, 0x07, + 0x80, 0xF0, 0x1C, 0x07, 0x00, 0xF0, 0x78, 0x03, 0xC7, 0x80, 0x0E, 0x38, + 0x00, 0x7B, 0xC0, 0x01, 0xFC, 0x00, 0x07, 0xC0, 0x00, 0x3E, 0x00, 0x00, + 0xE0, 0x00, 0x07, 0x00, 0x00, 0x38, 0x00, 0x01, 0xC0, 0x00, 0x0E, 0x00, + 0x00, 0x70, 0x00, 0x03, 0x80, 0x00, 0x1C, 0x00, 0x00, 0xE0, 0x00, 0x07, + 0x00, 0x00, 0x38, 0x00, 0x01, 0xC0, 0x00, 0x0E, 0x00, 0x00, 0x1C, 0x00, + 0x07, 0x80, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x0E, 0x00, 0x03, 0x80, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x38, 0x7F, 0xCF, 0x1F, + 0xFD, 0xC7, 0x87, 0xB8, 0xE0, 0x7F, 0x3C, 0x07, 0xC7, 0x00, 0xF8, 0xE0, + 0x1F, 0x1C, 0x03, 0xC3, 0x80, 0x38, 0x70, 0x07, 0x0E, 0x01, 0xE1, 0xC0, + 0x3C, 0x3C, 0x0F, 0xC3, 0x81, 0xF8, 0x78, 0x7F, 0x87, 0xFF, 0xFC, 0x7F, + 0xCF, 0x83, 0xF0, 0xF0, 0x00, 0x1C, 0x00, 0x70, 0x01, 0xC0, 0x07, 0x00, + 0x0C, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xE0, 0x7F, + 0xF1, 0xFF, 0xE7, 0x80, 0xCE, 0x00, 0x1C, 0x00, 0x3C, 0x00, 0x3F, 0xE0, + 0x1F, 0xC0, 0x7F, 0x83, 0xC0, 0x0F, 0x00, 0x1C, 0x00, 0x38, 0x00, 0x78, + 0x00, 0xF8, 0x06, 0xFF, 0xFC, 0xFF, 0xF8, 0x3F, 0xC0, 0x00, 0x0E, 0x00, + 0x1C, 0x00, 0x38, 0x00, 0x70, 0x00, 0x60, 0x00, 0xC0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xE3, 0xF0, 0xEF, 0xFC, 0xFF, 0xFE, 0xFC, 0x1E, 0xF8, + 0x0F, 0xF0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, + 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, + 0x07, 0xE0, 0x07, 0xE0, 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, + 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, 0x07, 0x07, 0x03, 0x83, 0x83, + 0x83, 0x80, 0x00, 0x00, 0x00, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81, + 0xC0, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0xF0, 0x3F, + 0x8F, 0xC3, 0xE0, 0x00, 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x03, 0x80, 0x03, + 0x00, 0x06, 0x00, 0x00, 0x00, 0x78, 0xF0, 0x78, 0xF0, 0x78, 0xF0, 0x78, + 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, + 0x1C, 0xE0, 0x1C, 0xE0, 0x0E, 0xE0, 0x0E, 0xE0, 0x07, 0xE0, 0x07, 0xE0, + 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x0F, 0xE0, 0x0F, 0xE0, + 0x0E, 0xE0, 0x1E, 0xF0, 0x3C, 0x78, 0x7C, 0x7F, 0xF8, 0x3F, 0xF0, 0x0F, + 0xC0, 0x07, 0xE1, 0xC3, 0xFE, 0x78, 0xFF, 0xEE, 0x3C, 0x3D, 0xC7, 0x03, + 0xF9, 0xE0, 0x3E, 0x38, 0x07, 0xC7, 0x00, 0xF8, 0xE0, 0x1E, 0x1C, 0x01, + 0xC3, 0x80, 0x38, 0x70, 0x0F, 0x0E, 0x01, 0xE1, 0xE0, 0x7E, 0x1C, 0x0F, + 0xC3, 0xC3, 0xFC, 0x3F, 0xFF, 0xE3, 0xFE, 0x7C, 0x1F, 0x87, 0x80, 0x0F, + 0xE0, 0x1F, 0xFC, 0x1F, 0xFF, 0x0F, 0x07, 0x8F, 0x01, 0xE7, 0x00, 0x73, + 0x80, 0x39, 0xC0, 0x1C, 0xE0, 0x0E, 0x70, 0x0F, 0x38, 0x0F, 0x1C, 0x0F, + 0x8E, 0x7F, 0x87, 0x3F, 0xC3, 0x9F, 0xF9, 0xC0, 0x3E, 0xE0, 0x07, 0x70, + 0x01, 0xF8, 0x00, 0xFC, 0x00, 0x7E, 0x00, 0x3F, 0x00, 0x1F, 0xC0, 0x1D, + 0xF8, 0x3E, 0xFF, 0xFE, 0x7F, 0xFE, 0x39, 0xFC, 0x1C, 0x00, 0x0E, 0x00, + 0x07, 0x00, 0x03, 0x80, 0x01, 0xC0, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x00, + 0xE0, 0x00, 0xFF, 0x00, 0x3B, 0xE0, 0x07, 0x1E, 0x01, 0xE1, 0xC0, 0x38, + 0x3C, 0x0F, 0x03, 0x81, 0xC0, 0x70, 0x38, 0x0F, 0x0E, 0x00, 0xE1, 0xC0, + 0x1C, 0x78, 0x03, 0xCE, 0x00, 0x3B, 0xC0, 0x07, 0x70, 0x00, 0xFE, 0x00, + 0x0F, 0x80, 0x01, 0xF0, 0x00, 0x3E, 0x00, 0x03, 0x80, 0x00, 0x70, 0x00, + 0x0E, 0x00, 0x01, 0xC0, 0x00, 0x38, 0x00, 0x07, 0x00, 0x00, 0xE0, 0x00, + 0x1C, 0x00, 0x03, 0xFE, 0x03, 0xFF, 0xC1, 0xFF, 0xF0, 0xF0, 0x04, 0x38, + 0x00, 0x0E, 0x00, 0x03, 0xE0, 0x00, 0x7F, 0xC0, 0x0F, 0xFC, 0x07, 0xFF, + 0x83, 0x81, 0xF1, 0xC0, 0x1E, 0x70, 0x03, 0xB8, 0x00, 0xFE, 0x00, 0x1F, + 0x80, 0x07, 0xE0, 0x01, 0xF8, 0x00, 0x7E, 0x00, 0x1F, 0xC0, 0x0F, 0x70, + 0x03, 0x9E, 0x01, 0xE3, 0xC0, 0xF0, 0xFF, 0xFC, 0x0F, 0xFC, 0x00, 0xFC, + 0x00, 0x07, 0xF0, 0x3F, 0xF8, 0xFF, 0xF3, 0xC0, 0x67, 0x00, 0x0E, 0x00, + 0x1E, 0x00, 0x1F, 0xF0, 0x0F, 0xE0, 0x3F, 0xC1, 0xE0, 0x07, 0x80, 0x0E, + 0x00, 0x1C, 0x00, 0x3C, 0x00, 0x7C, 0x03, 0x7F, 0xFE, 0x7F, 0xFC, 0x1F, + 0xE0, 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0x00, 0x7E, 0x00, 0xF8, 0x03, + 0xE0, 0x07, 0xC0, 0x0F, 0x80, 0x1E, 0x00, 0x1C, 0x00, 0x38, 0x00, 0x78, + 0x00, 0x70, 0x00, 0x70, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, + 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xF0, 0x00, 0x70, 0x00, 0x78, 0x00, 0x3E, + 0x00, 0x1F, 0xF8, 0x0F, 0xFE, 0x01, 0xFE, 0x00, 0x0F, 0x00, 0x07, 0x00, + 0x07, 0x00, 0x0F, 0x00, 0x1E, 0x00, 0x1E, 0x00, 0x1C, 0xE3, 0xF0, 0xEF, + 0xFC, 0xFF, 0xFE, 0xFC, 0x1E, 0xF8, 0x0F, 0xF0, 0x07, 0xE0, 0x07, 0xE0, + 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, + 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0x00, + 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, + 0x07, 0x03, 0xF0, 0x03, 0xFF, 0x01, 0xFF, 0xE0, 0xF8, 0x7C, 0x38, 0x07, + 0x1E, 0x01, 0xE7, 0x00, 0x39, 0xC0, 0x0E, 0xF0, 0x03, 0xF8, 0x00, 0x7E, + 0x00, 0x1F, 0x80, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, + 0x07, 0xE0, 0x01, 0xF8, 0x00, 0x7E, 0x00, 0x1D, 0xC0, 0x0E, 0x70, 0x03, + 0x9E, 0x01, 0xE3, 0x80, 0x70, 0xF8, 0x7C, 0x1F, 0xFE, 0x03, 0xFF, 0x00, + 0x3F, 0x00, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, + 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xF0, 0x7F, 0x3F, 0x1F, 0xE0, 0x3C, 0x70, + 0x3C, 0x38, 0x3C, 0x1C, 0x3C, 0x0E, 0x3C, 0x07, 0x3C, 0x03, 0xBC, 0x01, + 0xFE, 0x00, 0xFF, 0x80, 0x7F, 0xC0, 0x3C, 0xF0, 0x1C, 0x3C, 0x0E, 0x0F, + 0x07, 0x03, 0xC3, 0x80, 0xE1, 0xC0, 0x78, 0xE0, 0x1E, 0x70, 0x07, 0xB8, + 0x01, 0xE0, 0x1F, 0x00, 0x03, 0xF0, 0x00, 0x7F, 0x00, 0x01, 0xE0, 0x00, + 0x1E, 0x00, 0x01, 0xC0, 0x00, 0x38, 0x00, 0x07, 0x80, 0x00, 0x70, 0x00, + 0x1E, 0x00, 0x03, 0xE0, 0x00, 0xFC, 0x00, 0x1F, 0xC0, 0x07, 0xB8, 0x00, + 0xE7, 0x00, 0x3C, 0xF0, 0x07, 0x0E, 0x00, 0xE1, 0xC0, 0x38, 0x1C, 0x07, + 0x03, 0x81, 0xC0, 0x78, 0x38, 0x07, 0x0E, 0x00, 0xE1, 0xC0, 0x1E, 0x78, + 0x01, 0xCE, 0x00, 0x3B, 0xC0, 0x03, 0x80, 0xE0, 0x07, 0x38, 0x01, 0xCE, + 0x00, 0x73, 0x80, 0x1C, 0xE0, 0x07, 0x38, 0x01, 0xCE, 0x00, 0x73, 0x80, + 0x1C, 0xE0, 0x07, 0x38, 0x01, 0xCE, 0x00, 0x73, 0x80, 0x1C, 0xE0, 0x07, + 0x38, 0x01, 0xCF, 0x00, 0xF3, 0xE0, 0xFC, 0xFF, 0xFF, 0xFB, 0xFC, 0xFE, + 0x7E, 0x3B, 0x80, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x0E, 0x00, 0x03, 0x80, + 0x00, 0xE0, 0x00, 0x38, 0x00, 0x00, 0xE0, 0x0E, 0x78, 0x07, 0x1C, 0x01, + 0xCE, 0x00, 0xE7, 0x80, 0x39, 0xC0, 0x1C, 0xE0, 0x0E, 0x78, 0x07, 0x1C, + 0x03, 0x8E, 0x03, 0xC7, 0x01, 0xC1, 0xC1, 0xE0, 0xE0, 0xE0, 0x70, 0xE0, + 0x1C, 0xF0, 0x0E, 0xF0, 0x07, 0xF0, 0x01, 0xF0, 0x00, 0xF0, 0x00, 0x7F, + 0xFC, 0xFF, 0xF9, 0xFF, 0xF0, 0xFC, 0x03, 0xC0, 0x0F, 0x00, 0x1C, 0x00, + 0x38, 0x00, 0x70, 0x00, 0xF0, 0x00, 0xF8, 0x00, 0xFF, 0x80, 0x3F, 0x03, + 0xFE, 0x0F, 0x80, 0x3C, 0x00, 0xF0, 0x01, 0xC0, 0x03, 0x80, 0x07, 0x00, + 0x0E, 0x00, 0x1E, 0x00, 0x1E, 0x00, 0x3F, 0x00, 0x3F, 0xF0, 0x3F, 0xF8, + 0x1F, 0xF0, 0x00, 0xF0, 0x00, 0xE0, 0x01, 0xC0, 0x07, 0x80, 0x1E, 0x00, + 0x3C, 0x00, 0x70, 0x03, 0xF0, 0x03, 0xFF, 0x03, 0xFF, 0xF0, 0xF0, 0x3C, + 0x78, 0x07, 0x9C, 0x00, 0xEE, 0x00, 0x3F, 0x80, 0x07, 0xE0, 0x01, 0xF8, + 0x00, 0x7E, 0x00, 0x1F, 0x80, 0x07, 0xF0, 0x03, 0xDC, 0x00, 0xE7, 0x80, + 0x78, 0xF0, 0x3C, 0x3F, 0xFF, 0x03, 0xFF, 0x00, 0x3F, 0x00, 0xFF, 0xFF, + 0xBF, 0xFF, 0xEF, 0xFF, 0xF8, 0xE0, 0x38, 0x38, 0x0E, 0x0E, 0x03, 0x83, + 0x80, 0xE0, 0xE0, 0x38, 0x38, 0x0E, 0x0E, 0x03, 0x83, 0x80, 0xE0, 0xE0, + 0x38, 0x38, 0x0E, 0x0E, 0x03, 0x83, 0x80, 0xE0, 0xE0, 0x38, 0x38, 0x0F, + 0xCE, 0x01, 0xF3, 0x80, 0x7C, 0x03, 0xF8, 0x03, 0xFF, 0x81, 0xFF, 0xF0, + 0xF0, 0x3C, 0x78, 0x07, 0x9C, 0x00, 0xEE, 0x00, 0x3F, 0x80, 0x07, 0xE0, + 0x01, 0xF8, 0x00, 0x7E, 0x00, 0x1F, 0x80, 0x07, 0xE0, 0x03, 0xFC, 0x00, + 0xEF, 0x80, 0x7B, 0xF0, 0x3C, 0xEF, 0xFF, 0x39, 0xFF, 0x0E, 0x3F, 0x03, + 0x80, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x0E, 0x00, 0x03, 0x80, 0x00, 0xE0, + 0x00, 0x38, 0x00, 0x00, 0x03, 0xF8, 0x1F, 0xFC, 0x7F, 0xF9, 0xF0, 0x37, + 0x80, 0x0E, 0x00, 0x3C, 0x00, 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x03, 0x80, + 0x07, 0x00, 0x0F, 0x00, 0x0E, 0x00, 0x1E, 0x00, 0x1F, 0x00, 0x1F, 0xF0, + 0x1F, 0xF8, 0x0F, 0xF0, 0x00, 0xF0, 0x00, 0xE0, 0x01, 0xC0, 0x07, 0x80, + 0x1E, 0x00, 0x3C, 0x00, 0x70, 0x07, 0xFF, 0xF1, 0xFF, 0xFF, 0x3F, 0xFF, + 0xF7, 0xC0, 0xF0, 0x78, 0x07, 0x87, 0x00, 0x38, 0xE0, 0x03, 0xCE, 0x00, + 0x1C, 0xE0, 0x01, 0xCE, 0x00, 0x1C, 0xE0, 0x01, 0xCE, 0x00, 0x1C, 0xF0, + 0x03, 0xC7, 0x00, 0x38, 0x78, 0x07, 0x83, 0xC0, 0xF0, 0x3F, 0xFE, 0x00, + 0xFF, 0xC0, 0x03, 0xF0, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, + 0x38, 0x00, 0x1C, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x03, 0x80, 0x01, 0xC0, + 0x00, 0xE0, 0x00, 0x70, 0x00, 0x38, 0x00, 0x1C, 0x00, 0x0E, 0x00, 0x07, + 0x00, 0x03, 0xC0, 0x00, 0xFE, 0x00, 0x3F, 0x00, 0x0F, 0x80, 0xE0, 0x1C, + 0xE0, 0x1C, 0xE0, 0x0E, 0xE0, 0x0E, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, + 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x0F, 0xE0, 0x0F, 0xE0, 0x0E, + 0xE0, 0x1E, 0xF0, 0x3C, 0x78, 0x7C, 0x7F, 0xF8, 0x3F, 0xF0, 0x0F, 0xC0, + 0x06, 0x3C, 0x03, 0xCF, 0xE0, 0xFB, 0xFE, 0x3E, 0x71, 0xE7, 0x8E, 0x1D, + 0xE1, 0xC3, 0xFC, 0x38, 0x3F, 0x07, 0x07, 0xE0, 0xE0, 0xFC, 0x1C, 0x1F, + 0x83, 0x83, 0xF0, 0x70, 0x7E, 0x0E, 0x0E, 0xE1, 0xC3, 0x9E, 0x38, 0xF1, + 0xE7, 0x3C, 0x3F, 0xFF, 0x83, 0xFF, 0xE0, 0x1F, 0xF0, 0x00, 0x70, 0x00, + 0x0E, 0x00, 0x01, 0xC0, 0x00, 0x38, 0x00, 0x07, 0x00, 0x00, 0xE0, 0x00, + 0x1C, 0x00, 0xF0, 0x03, 0xFF, 0x00, 0xEF, 0xC0, 0x78, 0x78, 0x1C, 0x0E, + 0x0E, 0x03, 0xC7, 0x80, 0x71, 0xC0, 0x1C, 0xF0, 0x03, 0xB8, 0x00, 0xFE, + 0x00, 0x3F, 0x00, 0x07, 0xC0, 0x01, 0xE0, 0x00, 0x78, 0x00, 0x3E, 0x00, + 0x0F, 0xC0, 0x07, 0xF0, 0x01, 0xDC, 0x00, 0xF3, 0x80, 0x38, 0xE0, 0x1E, + 0x3C, 0x07, 0x07, 0x03, 0x81, 0xE1, 0xE0, 0x3F, 0x70, 0x0F, 0xFC, 0x00, + 0xF0, 0xE0, 0xE0, 0xFC, 0x1C, 0x1F, 0x83, 0x83, 0xF0, 0x70, 0x7E, 0x0E, + 0x0F, 0xC1, 0xC1, 0xF8, 0x38, 0x3F, 0x07, 0x07, 0xE0, 0xE0, 0xFC, 0x1C, + 0x1F, 0x83, 0x83, 0xF0, 0x70, 0x7E, 0x0E, 0x0F, 0xE1, 0xC3, 0xDC, 0x38, + 0x73, 0xE7, 0x3E, 0x3F, 0xFF, 0x83, 0xFF, 0xE0, 0x0F, 0xE0, 0x00, 0x70, + 0x00, 0x0E, 0x00, 0x01, 0xC0, 0x00, 0x38, 0x00, 0x07, 0x00, 0x00, 0xE0, + 0x00, 0x1C, 0x00, 0x1C, 0x00, 0x1C, 0x1C, 0x00, 0x07, 0x0E, 0x00, 0x03, + 0x8E, 0x00, 0x00, 0xE7, 0x00, 0x00, 0x73, 0x80, 0x00, 0x3B, 0x80, 0x00, + 0x0F, 0xC0, 0x00, 0x07, 0xE0, 0x1C, 0x03, 0xF0, 0x0E, 0x01, 0xF8, 0x07, + 0x00, 0xFC, 0x03, 0x80, 0x7E, 0x01, 0xC0, 0x3F, 0x81, 0xF0, 0x3D, 0xC0, + 0xD8, 0x1C, 0xF0, 0xEE, 0x1E, 0x3F, 0xF7, 0xFE, 0x0F, 0xF1, 0xFE, 0x03, + 0xF0, 0x7E, 0x00, 0xF1, 0xFE, 0x3F, 0xC7, 0xF8, 0xF0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x1C, 0x03, 0x80, 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0x00, + 0xE0, 0x1C, 0x03, 0x80, 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0x80, 0x70, + 0x0F, 0xE0, 0xFC, 0x0F, 0x80, 0x7C, 0xF8, 0x7C, 0xF8, 0x7C, 0xF8, 0x7C, + 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, + 0x1C, 0xE0, 0x1C, 0xE0, 0x0E, 0xE0, 0x0E, 0xE0, 0x07, 0xE0, 0x07, 0xE0, + 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x0F, 0xE0, 0x0F, 0xE0, + 0x0E, 0xE0, 0x1E, 0xF0, 0x3C, 0x78, 0x7C, 0x7F, 0xF8, 0x3F, 0xF0, 0x0F, + 0xC0, 0x00, 0x1E, 0x00, 0x07, 0x00, 0x03, 0x80, 0x01, 0xC0, 0x00, 0xE0, + 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, + 0xFF, 0xC0, 0xFF, 0xFC, 0x3C, 0x0F, 0x1E, 0x01, 0xE7, 0x00, 0x3B, 0x80, + 0x0F, 0xE0, 0x01, 0xF8, 0x00, 0x7E, 0x00, 0x1F, 0x80, 0x07, 0xE0, 0x01, + 0xFC, 0x00, 0xF7, 0x00, 0x39, 0xE0, 0x1E, 0x3C, 0x0F, 0x0F, 0xFF, 0xC0, + 0xFF, 0xC0, 0x0F, 0xC0, 0x00, 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x01, 0x80, + 0x03, 0x80, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x0E, 0xE0, 0x0E, 0xE0, 0x07, 0xE0, 0x07, + 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x0F, 0xE0, 0x0F, + 0xE0, 0x0E, 0xE0, 0x1E, 0xF0, 0x3C, 0x78, 0x7C, 0x7F, 0xF8, 0x3F, 0xF0, + 0x0F, 0xC0, 0x00, 0x01, 0xC0, 0x00, 0x01, 0xE0, 0x00, 0x00, 0xE0, 0x00, + 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x07, + 0x07, 0x00, 0x01, 0xC3, 0x80, 0x00, 0xE3, 0x80, 0x00, 0x39, 0xC0, 0x00, + 0x1C, 0xE0, 0x00, 0x0E, 0xE0, 0x00, 0x03, 0xF0, 0x00, 0x01, 0xF8, 0x07, + 0x00, 0xFC, 0x03, 0x80, 0x7E, 0x01, 0xC0, 0x3F, 0x00, 0xE0, 0x1F, 0x80, + 0x70, 0x0F, 0xE0, 0x7C, 0x0F, 0x70, 0x36, 0x07, 0x3C, 0x3B, 0x87, 0x8F, + 0xFD, 0xFF, 0x83, 0xFC, 0x7F, 0x80, 0xFC, 0x1F, 0x80, +}; + +const GFXglyph FreeSans18pt_Win1253Glyphs[] PROGMEM = { +/* 0x01 */ { 0, 33, 36, 45, 6, -29 }, +/* 0x02 */ { 149, 33, 36, 45, 6, -29 }, +/* 0x03 */ { 298, 35, 36, 45, 5, -29 }, +/* 0x04 */ { 456, 42, 36, 45, 1, -29 }, +/* 0x05 */ { 645, 35, 36, 45, 5, -29 }, +/* 0x06 */ { 803, 35, 36, 45, 5, -29 }, +/* 0x07 */ { 961, 0, 0, 0, 0, 0 }, +/* 0x08 */ { 961, 37, 36, 45, 4, -29 }, +/* 0x09 */ { 1128, 40, 28, 45, 2, -25 }, +/* 0x0A */ { 1268, 0, 0, 0, 0, 0 }, +/* 0x0B */ { 1268, 39, 36, 45, 3, -29 }, +/* 0x0C */ { 1444, 35, 36, 45, 5, -29 }, +/* 0x0D */ { 1602, 0, 0, 0, 0, 0 }, +/* 0x0E */ { 1602, 35, 36, 45, 5, -29 }, +/* 0x0F */ { 1760, 35, 37, 45, 5, -29 }, +/* 0x10 */ { 1922, 34, 36, 45, 5, -29 }, +/* 0x11 */ { 2075, 35, 36, 45, 5, -29 }, +/* 0x12 */ { 2233, 34, 36, 45, 5, -29 }, +/* 0x13 */ { 2386, 35, 36, 45, 5, -29 }, +/* 0x14 */ { 2544, 35, 36, 45, 5, -29 }, +/* 0x15 */ { 2702, 38, 36, 45, 4, -29 }, +/* 0x16 */ { 2873, 28, 36, 45, 8, -29 }, +/* 0x17 */ { 2999, 37, 30, 45, 4, -26 }, +/* 0x18 */ { 3138, 42, 30, 45, 1, -26 }, +/* 0x19 */ { 3296, 35, 36, 45, 5, -29 }, +/* 0x1A */ { 3454, 0, 0, 0, 0, 0 }, +/* 0x1B */ { 3454, 42, 37, 45, 1, -29 }, +/* 0x1C */ { 3649, 35, 36, 45, 5, -29 }, +/* 0x1D */ { 3807, 36, 36, 45, 4, -28 }, +/* 0x1E */ { 3969, 35, 36, 45, 4, -29 }, +/* 0x1F */ { 4127, 25, 36, 45, 10, -29 }, +/* 0x20 */ { 4240, 1, 1, 11, 0, 0 }, +/* 0x21 */ { 4241, 3, 26, 14, 5, -25 }, +/* 0x22 */ { 4251, 10, 9, 16, 3, -25 }, +/* 0x23 */ { 4263, 24, 25, 29, 3, -24 }, +/* 0x24 */ { 4338, 17, 32, 22, 3, -26 }, +/* 0x25 */ { 4406, 29, 26, 33, 2, -25 }, +/* 0x26 */ { 4501, 24, 26, 27, 2, -25 }, +/* 0x27 */ { 4579, 3, 9, 10, 3, -25 }, +/* 0x28 */ { 4583, 8, 31, 14, 3, -26 }, +/* 0x29 */ { 4614, 8, 31, 14, 3, -26 }, +/* 0x2A */ { 4645, 16, 16, 18, 1, -25 }, +/* 0x2B */ { 4677, 23, 23, 29, 4, -22 }, +/* 0x2C */ { 4744, 4, 8, 11, 3, -3 }, +/* 0x2D */ { 4748, 9, 3, 13, 2, -10 }, +/* 0x2E */ { 4752, 3, 4, 11, 4, -3 }, +/* 0x2F */ { 4754, 12, 29, 12, 0, -25 }, +/* 0x30 */ { 4798, 18, 26, 22, 2, -25 }, +/* 0x31 */ { 4857, 15, 26, 22, 4, -25 }, +/* 0x32 */ { 4906, 16, 26, 22, 3, -25 }, +/* 0x33 */ { 4958, 17, 26, 22, 3, -25 }, +/* 0x34 */ { 5014, 19, 26, 22, 2, -25 }, +/* 0x35 */ { 5076, 17, 26, 22, 3, -25 }, +/* 0x36 */ { 5132, 18, 26, 22, 2, -25 }, +/* 0x37 */ { 5191, 16, 26, 22, 3, -25 }, +/* 0x38 */ { 5243, 18, 26, 22, 2, -25 }, +/* 0x39 */ { 5302, 18, 26, 22, 2, -25 }, +/* 0x3A */ { 5361, 3, 18, 12, 4, -17 }, +/* 0x3B */ { 5368, 4, 22, 12, 3, -17 }, +/* 0x3C */ { 5379, 22, 19, 29, 4, -19 }, +/* 0x3D */ { 5432, 22, 10, 29, 4, -15 }, +/* 0x3E */ { 5460, 22, 19, 29, 4, -19 }, +/* 0x3F */ { 5513, 14, 26, 19, 3, -25 }, +/* 0x40 */ { 5559, 31, 31, 35, 2, -24 }, +/* 0x41 */ { 5680, 23, 26, 24, 0, -25 }, +/* 0x42 */ { 5755, 18, 26, 24, 3, -25 }, +/* 0x43 */ { 5814, 21, 26, 24, 2, -25 }, +/* 0x44 */ { 5883, 21, 26, 27, 3, -25 }, +/* 0x45 */ { 5952, 16, 26, 22, 3, -25 }, +/* 0x46 */ { 6004, 15, 26, 20, 3, -25 }, +/* 0x47 */ { 6053, 22, 26, 27, 2, -25 }, +/* 0x48 */ { 6125, 19, 26, 26, 3, -25 }, +/* 0x49 */ { 6187, 3, 26, 10, 3, -25 }, +/* 0x4A */ { 6197, 8, 33, 10, -2, -25 }, +/* 0x4B */ { 6230, 20, 26, 23, 3, -25 }, +/* 0x4C */ { 6295, 16, 26, 20, 3, -25 }, +/* 0x4D */ { 6347, 23, 26, 30, 3, -25 }, +/* 0x4E */ { 6422, 19, 26, 26, 3, -25 }, +/* 0x4F */ { 6484, 24, 26, 28, 2, -25 }, +/* 0x50 */ { 6562, 16, 26, 21, 3, -25 }, +/* 0x51 */ { 6614, 24, 31, 28, 2, -25 }, +/* 0x52 */ { 6707, 19, 26, 24, 3, -25 }, +/* 0x53 */ { 6769, 18, 26, 22, 2, -25 }, +/* 0x54 */ { 6828, 21, 26, 21, 0, -25 }, +/* 0x55 */ { 6897, 19, 26, 26, 3, -25 }, +/* 0x56 */ { 6959, 23, 26, 24, 0, -25 }, +/* 0x57 */ { 7034, 32, 26, 35, 1, -25 }, +/* 0x58 */ { 7138, 22, 26, 24, 1, -25 }, +/* 0x59 */ { 7210, 21, 26, 21, 0, -25 }, +/* 0x5A */ { 7279, 21, 26, 24, 2, -25 }, +/* 0x5B */ { 7348, 7, 31, 14, 3, -26 }, +/* 0x5C */ { 7376, 12, 29, 12, 0, -25 }, +/* 0x5D */ { 7420, 7, 31, 14, 3, -26 }, +/* 0x5E */ { 7448, 22, 10, 29, 4, -25 }, +/* 0x5F */ { 7476, 18, 3, 18, 0, 6 }, +/* 0x60 */ { 7483, 8, 6, 18, 3, -27 }, +/* 0x61 */ { 7489, 16, 19, 21, 2, -18 }, +/* 0x62 */ { 7527, 17, 27, 22, 3, -26 }, +/* 0x63 */ { 7585, 15, 19, 19, 2, -18 }, +/* 0x64 */ { 7621, 17, 27, 22, 2, -26 }, +/* 0x65 */ { 7679, 18, 19, 22, 2, -18 }, +/* 0x66 */ { 7722, 12, 27, 12, 1, -26 }, +/* 0x67 */ { 7763, 17, 26, 22, 2, -18 }, +/* 0x68 */ { 7819, 16, 27, 22, 3, -26 }, +/* 0x69 */ { 7873, 3, 27, 10, 3, -26 }, +/* 0x6A */ { 7884, 7, 34, 10, -1, -26 }, +/* 0x6B */ { 7914, 17, 27, 20, 3, -26 }, +/* 0x6C */ { 7972, 3, 27, 10, 3, -26 }, +/* 0x6D */ { 7983, 29, 19, 34, 3, -18 }, +/* 0x6E */ { 8052, 16, 19, 22, 3, -18 }, +/* 0x6F */ { 8090, 18, 19, 21, 2, -18 }, +/* 0x70 */ { 8133, 17, 26, 22, 3, -18 }, +/* 0x71 */ { 8189, 17, 26, 22, 2, -18 }, +/* 0x72 */ { 8245, 11, 19, 14, 3, -18 }, +/* 0x73 */ { 8272, 15, 19, 18, 2, -18 }, +/* 0x74 */ { 8308, 11, 24, 14, 1, -23 }, +/* 0x75 */ { 8341, 16, 19, 22, 3, -18 }, +/* 0x76 */ { 8379, 19, 19, 21, 1, -18 }, +/* 0x77 */ { 8425, 26, 19, 29, 1, -18 }, +/* 0x78 */ { 8487, 19, 19, 21, 1, -18 }, +/* 0x79 */ { 8533, 19, 26, 21, 1, -18 }, +/* 0x7A */ { 8595, 15, 19, 18, 2, -18 }, +/* 0x7B */ { 8631, 13, 32, 22, 5, -26 }, +/* 0x7C */ { 8683, 3, 35, 12, 4, -26 }, +/* 0x7D */ { 8697, 13, 32, 22, 4, -26 }, +/* 0x7E */ { 8749, 22, 6, 29, 4, -13 }, +/* 0x7F */ { 8766, 0, 0, 0, 0, 0 }, +/* 0x80 */ { 8766, 20, 26, 22, 0, -25 }, +/* 0x81 */ { 8831, 0, 0, 0, 0, 0 }, +/* 0x82 */ { 8831, 4, 8, 11, 3, -3 }, +/* 0x83 */ { 8835, 15, 34, 12, -2, -26 }, +/* 0x84 */ { 8899, 10, 8, 18, 3, -3 }, +/* 0x85 */ { 8909, 27, 4, 35, 4, -3 }, +/* 0x86 */ { 8923, 15, 29, 18, 1, -25 }, +/* 0x87 */ { 8978, 15, 29, 18, 1, -25 }, +/* 0x88 */ { 9033, 0, 0, 0, 0, 0 }, +/* 0x89 */ { 9033, 43, 26, 47, 2, -25 }, +/* 0x8A */ { 9173, 0, 0, 0, 0, 0 }, +/* 0x8B */ { 9173, 8, 16, 14, 3, -17 }, +/* 0x8C */ { 9189, 0, 0, 0, 0, 0 }, +/* 0x8D */ { 9189, 0, 0, 0, 0, 0 }, +/* 0x8E */ { 9189, 0, 0, 0, 0, 0 }, +/* 0x8F */ { 9189, 0, 0, 0, 0, 0 }, +/* 0x90 */ { 9189, 0, 0, 0, 0, 0 }, +/* 0x91 */ { 9189, 4, 8, 11, 3, -25 }, +/* 0x92 */ { 9193, 4, 8, 11, 3, -25 }, +/* 0x93 */ { 9197, 10, 8, 18, 3, -25 }, +/* 0x94 */ { 9207, 10, 8, 18, 3, -25 }, +/* 0x95 */ { 9217, 10, 10, 21, 5, -17 }, +/* 0x96 */ { 9230, 14, 3, 18, 2, -10 }, +/* 0x97 */ { 9236, 32, 3, 35, 2, -10 }, +/* 0x98 */ { 9248, 0, 0, 0, 0, 0 }, +/* 0x99 */ { 9248, 22, 10, 35, 5, -25 }, +/* 0x9A */ { 9276, 0, 0, 0, 0, 0 }, +/* 0x9B */ { 9276, 8, 16, 14, 3, -17 }, +/* 0x9C */ { 9292, 0, 0, 0, 0, 0 }, +/* 0x9D */ { 9292, 0, 0, 0, 0, 0 }, +/* 0x9E */ { 9292, 0, 0, 0, 0, 0 }, +/* 0x9F */ { 9292, 0, 0, 0, 0, 0 }, +/* 0xA0 */ { 9292, 1, 1, 11, 0, 0 }, +/* 0xA1 */ { 9293, 11, 11, 18, 4, -33 }, +/* 0xA2 */ { 9309, 23, 28, 24, 0, -27 }, +/* 0xA3 */ { 9390, 17, 26, 22, 2, -25 }, +/* 0xA4 */ { 9446, 19, 19, 22, 2, -19 }, +/* 0xA5 */ { 9492, 19, 26, 22, 1, -25 }, +/* 0xA6 */ { 9554, 3, 31, 12, 4, -24 }, +/* 0xA7 */ { 9566, 14, 29, 18, 2, -25 }, +/* 0xA8 */ { 9617, 11, 4, 18, 4, -26 }, +/* 0xA9 */ { 9623, 25, 25, 35, 5, -24 }, +/* 0xAA */ { 9702, 0, 0, 0, 0, 0 }, +/* 0xAB */ { 9702, 15, 16, 21, 3, -17 }, +/* 0xAC */ { 9732, 22, 10, 29, 4, -14 }, +/* 0xAD */ { 9760, 9, 3, 13, 2, -10 }, +/* 0xAE */ { 9764, 25, 25, 35, 5, -24 }, +/* 0xAF */ { 9843, 35, 3, 35, 0, -10 }, +/* 0xB0 */ { 9857, 11, 11, 18, 3, -25 }, +/* 0xB1 */ { 9873, 23, 22, 29, 3, -21 }, +/* 0xB2 */ { 9937, 10, 14, 14, 2, -25 }, +/* 0xB3 */ { 9955, 11, 14, 14, 2, -25 }, +/* 0xB4 */ { 9975, 8, 6, 18, 7, -27 }, +/* 0xB5 */ { 9981, 11, 11, 18, 4, -33 }, +/* 0xB6 */ { 9997, 23, 28, 24, 0, -27 }, +/* 0xB7 */ { 10078, 3, 4, 11, 4, -13 }, +/* 0xB8 */ { 10080, 23, 28, 26, 0, -27 }, +/* 0xB9 */ { 10161, 26, 28, 30, 0, -27 }, +/* 0xBA */ { 10252, 10, 28, 14, 0, -27 }, +/* 0xBB */ { 10287, 15, 16, 21, 3, -17 }, +/* 0xBC */ { 10317, 27, 28, 28, 0, -27 }, +/* 0xBD */ { 10412, 29, 26, 34, 3, -25 }, +/* 0xBE */ { 10507, 28, 28, 29, 0, -27 }, +/* 0xBF */ { 10605, 27, 28, 29, 0, -27 }, +/* 0xC0 */ { 10700, 11, 34, 12, 0, -33 }, +/* 0xC1 */ { 10747, 23, 26, 24, 0, -25 }, +/* 0xC2 */ { 10822, 18, 26, 24, 3, -25 }, +/* 0xC3 */ { 10881, 15, 26, 19, 3, -25 }, +/* 0xC4 */ { 10930, 23, 26, 24, 0, -25 }, +/* 0xC5 */ { 11005, 16, 26, 22, 3, -25 }, +/* 0xC6 */ { 11057, 21, 26, 24, 2, -25 }, +/* 0xC7 */ { 11126, 19, 26, 26, 3, -25 }, +/* 0xC8 */ { 11188, 24, 26, 28, 2, -25 }, +/* 0xC9 */ { 11266, 3, 26, 10, 3, -25 }, +/* 0xCA */ { 11276, 20, 26, 23, 3, -25 }, +/* 0xCB */ { 11341, 23, 26, 24, 0, -25 }, +/* 0xCC */ { 11416, 23, 26, 30, 3, -25 }, +/* 0xCD */ { 11491, 19, 26, 26, 3, -25 }, +/* 0xCE */ { 11553, 16, 26, 22, 3, -25 }, +/* 0xCF */ { 11605, 24, 26, 28, 2, -25 }, +/* 0xD0 */ { 11683, 19, 26, 25, 3, -25 }, +/* 0xD1 */ { 11745, 16, 26, 21, 3, -25 }, +/* 0xD2 */ { 11797, 0, 0, 0, 0, 0 }, +/* 0xD3 */ { 11797, 16, 26, 22, 3, -25 }, +/* 0xD4 */ { 11849, 21, 26, 21, 0, -25 }, +/* 0xD5 */ { 11918, 21, 26, 21, 0, -25 }, +/* 0xD6 */ { 11987, 25, 26, 29, 2, -25 }, +/* 0xD7 */ { 12069, 22, 26, 24, 1, -25 }, +/* 0xD8 */ { 12141, 25, 26, 29, 2, -25 }, +/* 0xD9 */ { 12223, 24, 26, 28, 2, -25 }, +/* 0xDA */ { 12301, 11, 32, 10, -1, -31 }, +/* 0xDB */ { 12345, 21, 32, 21, 0, -31 }, +/* 0xDC */ { 12429, 19, 28, 23, 2, -27 }, +/* 0xDD */ { 12496, 15, 28, 19, 2, -27 }, +/* 0xDE */ { 12549, 16, 35, 22, 3, -27 }, +/* 0xDF */ { 12619, 9, 28, 12, 3, -27 }, +/* 0xE0 */ { 12651, 16, 35, 21, 3, -33 }, +/* 0xE1 */ { 12721, 19, 19, 23, 2, -18 }, +/* 0xE2 */ { 12767, 17, 34, 22, 3, -26 }, +/* 0xE3 */ { 12840, 19, 26, 21, 1, -18 }, +/* 0xE4 */ { 12902, 18, 26, 22, 2, -25 }, +/* 0xE5 */ { 12961, 15, 19, 19, 2, -18 }, +/* 0xE6 */ { 12997, 16, 34, 20, 2, -26 }, +/* 0xE7 */ { 13065, 16, 26, 22, 3, -18 }, +/* 0xE8 */ { 13117, 18, 27, 22, 2, -26 }, +/* 0xE9 */ { 13178, 8, 19, 12, 3, -18 }, +/* 0xEA */ { 13197, 17, 19, 21, 3, -18 }, +/* 0xEB */ { 13238, 19, 27, 21, 1, -26 }, +/* 0xEC */ { 13303, 18, 26, 22, 3, -18 }, +/* 0xED */ { 13362, 17, 19, 20, 1, -18 }, +/* 0xEE */ { 13403, 15, 34, 19, 2, -26 }, +/* 0xEF */ { 13467, 18, 19, 21, 2, -18 }, +/* 0xF0 */ { 13510, 18, 19, 21, 2, -18 }, +/* 0xF1 */ { 13553, 18, 26, 23, 3, -18 }, +/* 0xF2 */ { 13612, 15, 26, 20, 2, -18 }, +/* 0xF3 */ { 13661, 20, 19, 23, 2, -18 }, +/* 0xF4 */ { 13709, 17, 19, 21, 2, -18 }, +/* 0xF5 */ { 13750, 16, 19, 21, 3, -17 }, +/* 0xF6 */ { 13788, 19, 26, 23, 2, -18 }, +/* 0xF7 */ { 13850, 18, 26, 20, 1, -18 }, +/* 0xF8 */ { 13909, 19, 26, 23, 2, -18 }, +/* 0xF9 */ { 13971, 25, 19, 29, 2, -17 }, +/* 0xFA */ { 14031, 11, 27, 12, 0, -26 }, +/* 0xFB */ { 14069, 16, 28, 21, 3, -26 }, +/* 0xFC */ { 14125, 18, 28, 21, 2, -27 }, +/* 0xFD */ { 14188, 16, 29, 21, 3, -27 }, +/* 0xFE */ { 14246, 25, 29, 29, 2, -27 }, +/* 0xFF */ { 14337, 0, 0, 0, 0, 0 }, +}; + +const GFXfont FreeSans18pt_Win1253 PROGMEM = { +(uint8_t*)FreeSans18pt_Win1253Bitmaps, +(GFXglyph*)FreeSans18pt_Win1253Glyphs, +0x01, 0xFF, 41 +}; diff --git a/src/graphics/niche/Fonts/FreeSans24pt_Win1253.h b/src/graphics/niche/Fonts/FreeSans24pt_Win1253.h new file mode 100644 index 000000000..7efefd443 --- /dev/null +++ b/src/graphics/niche/Fonts/FreeSans24pt_Win1253.h @@ -0,0 +1,2429 @@ +// trunk-ignore-all(clang-format) +#pragma once +/* PROPERTIES + +FONT_NAME FreeSans24pt_Win1253 +*/ +const uint8_t FreeSans24pt_Win1253Bitmaps[] PROGMEM = { + 0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, + 0x00, 0x0C, 0xE0, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0x00, 0x00, + 0x60, 0xC0, 0x00, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x00, 0x01, 0x83, + 0x00, 0x00, 0x00, 0x00, 0x30, 0x60, 0x00, 0x00, 0x00, 0x06, 0x0C, 0x00, + 0x00, 0x00, 0x00, 0xC1, 0x80, 0x00, 0x00, 0x00, 0x18, 0x30, 0x00, 0x00, + 0x00, 0x06, 0x06, 0x00, 0x00, 0x00, 0x01, 0xC0, 0xC0, 0x00, 0x00, 0x00, + 0x70, 0x30, 0x00, 0x00, 0x00, 0x1C, 0x06, 0x00, 0x00, 0x00, 0x07, 0x00, + 0xC0, 0x00, 0x00, 0x03, 0x80, 0x13, 0xFF, 0x00, 0x00, 0xE0, 0x07, 0xFF, + 0xF0, 0x00, 0x70, 0x00, 0xF0, 0x06, 0x00, 0x0C, 0x00, 0x38, 0x00, 0xC0, + 0x03, 0x00, 0x06, 0x00, 0x18, 0x00, 0x60, 0x00, 0xC0, 0x03, 0x01, 0xFC, + 0x00, 0x18, 0x00, 0x67, 0xFF, 0x00, 0x03, 0x80, 0x38, 0xC0, 0x00, 0x00, + 0x3F, 0xFF, 0xD8, 0x00, 0x00, 0x07, 0xFE, 0x1F, 0x00, 0x00, 0x01, 0xE0, + 0x01, 0xE0, 0x00, 0x00, 0x3C, 0x00, 0x1C, 0x00, 0x00, 0x0F, 0x00, 0x03, + 0x80, 0x00, 0x03, 0xA0, 0x00, 0xF0, 0x00, 0x00, 0xE6, 0x00, 0x1E, 0x00, + 0x00, 0x78, 0xF0, 0x1F, 0xC0, 0x00, 0x1C, 0x0F, 0xFF, 0xD8, 0x00, 0x02, + 0x01, 0xC0, 0x7B, 0x00, 0x00, 0x00, 0x60, 0x03, 0xE0, 0x00, 0x00, 0x0C, + 0x00, 0x3C, 0x00, 0x00, 0x01, 0x80, 0x07, 0x80, 0x00, 0x00, 0x30, 0x00, + 0xF0, 0x00, 0x00, 0x07, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x7F, 0x86, 0xFF, + 0x80, 0x00, 0x1F, 0xFF, 0x9F, 0xFE, 0x00, 0x03, 0x00, 0xC0, 0x01, 0xF0, + 0x00, 0xC0, 0x18, 0x00, 0x07, 0x00, 0x18, 0x03, 0x00, 0x00, 0x3C, 0x01, + 0x80, 0x60, 0x00, 0x03, 0xFC, 0x7E, 0x1C, 0x00, 0x00, 0x0F, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x01, 0x80, 0xF8, 0x00, + 0x00, 0x0F, 0xFF, 0xFF, 0x80, 0x00, 0x07, 0xE0, 0x78, 0x38, 0x00, 0x03, + 0xC0, 0x0C, 0x03, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x60, 0x00, 0xF8, 0x00, + 0x60, 0x0C, 0x3F, 0xFC, 0x00, 0x06, 0x03, 0xC7, 0xF8, 0x00, 0x00, 0xFF, + 0xFC, 0xC0, 0x00, 0x00, 0x0F, 0xE0, 0xD8, 0x00, 0x00, 0x03, 0x80, 0x0F, + 0x00, 0x00, 0x00, 0x60, 0x01, 0xE0, 0x00, 0x00, 0x0C, 0x00, 0x3C, 0x00, + 0x00, 0x01, 0x80, 0x07, 0x80, 0x00, 0x00, 0x30, 0x01, 0xB0, 0x00, 0x04, + 0x03, 0xFF, 0xF6, 0x00, 0x00, 0xE0, 0x7F, 0xFE, 0xC0, 0x00, 0x0F, 0x1C, + 0x01, 0xF8, 0x00, 0x00, 0x73, 0x00, 0x0F, 0x00, 0x00, 0x07, 0xC0, 0x01, + 0xE0, 0x00, 0x00, 0x78, 0x00, 0x1C, 0x00, 0x00, 0x07, 0x80, 0x07, 0x80, + 0x00, 0x00, 0xF8, 0x00, 0xF0, 0x00, 0x00, 0x0F, 0xFF, 0x3F, 0xC0, 0x00, + 0x01, 0xFF, 0xFE, 0xFF, 0xE0, 0x00, 0x70, 0x03, 0x80, 0x7E, 0x00, 0x0C, + 0x00, 0x30, 0x00, 0xC0, 0x01, 0x80, 0x06, 0x00, 0x18, 0x00, 0x30, 0x00, + 0xC0, 0x01, 0x80, 0x07, 0x00, 0x18, 0x00, 0x18, 0x00, 0x78, 0x03, 0x00, + 0x01, 0xC0, 0x0F, 0xFF, 0xC0, 0x00, 0x1E, 0x00, 0x8F, 0xF0, 0x00, 0x00, + 0xE0, 0x18, 0x00, 0x00, 0x00, 0x0E, 0x03, 0x00, 0x00, 0x00, 0x00, 0x60, + 0x60, 0x00, 0x00, 0x00, 0x0E, 0x06, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xC0, + 0x00, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x00, 0x01, 0x83, 0x00, 0x00, + 0x00, 0x00, 0x30, 0x60, 0x00, 0x00, 0x00, 0x06, 0x0C, 0x00, 0x00, 0x00, + 0x00, 0xC1, 0x80, 0x00, 0x00, 0x00, 0x18, 0x30, 0x00, 0x00, 0x00, 0x03, + 0x06, 0x00, 0x00, 0x00, 0x00, 0x61, 0x80, 0x00, 0x00, 0x00, 0x06, 0x70, + 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, + 0x00, 0x00, 0x1F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, + 0x00, 0x07, 0xE0, 0x07, 0xE0, 0x00, 0x00, 0x1F, 0x00, 0x00, 0xF8, 0x00, + 0x00, 0x3C, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x70, 0x00, 0x00, 0x0E, 0x00, + 0x00, 0xE0, 0x00, 0x00, 0x07, 0x00, 0x01, 0x80, 0x00, 0x00, 0x01, 0x80, + 0x03, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, 0x00, 0x00, 0x60, + 0x0E, 0x00, 0x00, 0x00, 0x00, 0x70, 0x1C, 0x00, 0x00, 0x00, 0x00, 0x30, + 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x38, 0x00, 0x00, 0x00, 0x00, 0x1C, + 0x30, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x70, 0x00, 0x00, 0x00, 0x00, 0x0C, + 0x60, 0x00, 0x00, 0x00, 0x00, 0x06, 0x60, 0x03, 0xC0, 0x03, 0xC0, 0x06, + 0x60, 0x07, 0xE0, 0x07, 0xE0, 0x06, 0xC0, 0x0C, 0x30, 0x0C, 0x30, 0x03, + 0xC0, 0x18, 0x30, 0x0C, 0x38, 0x03, 0xC0, 0x18, 0x18, 0x18, 0x18, 0x03, + 0xC0, 0x00, 0x00, 0x00, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x03, + 0xC0, 0x00, 0x00, 0x00, 0x00, 0x03, 0xC0, 0x0D, 0x00, 0x00, 0x4A, 0x03, + 0xC0, 0x69, 0x00, 0x00, 0xD2, 0x03, 0xC0, 0x4B, 0x00, 0x00, 0x96, 0x03, + 0xC0, 0xD2, 0x00, 0x01, 0xA4, 0x03, 0x60, 0x10, 0x00, 0x00, 0x20, 0x06, + 0x60, 0x00, 0x00, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x00, 0x00, 0x06, + 0x70, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, + 0x38, 0x01, 0xF0, 0x0F, 0x80, 0x1C, 0x18, 0x00, 0x7F, 0xFE, 0x00, 0x18, + 0x1C, 0x00, 0x0F, 0xF0, 0x00, 0x30, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x70, + 0x06, 0x00, 0x00, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0x00, 0x00, 0xC0, + 0x03, 0x80, 0x00, 0x00, 0x01, 0x80, 0x01, 0xE0, 0x00, 0x00, 0x07, 0x00, + 0x00, 0x70, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x3C, 0x00, + 0x00, 0x1F, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x07, 0xE0, 0x07, 0xE0, 0x00, + 0x00, 0x01, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x1F, 0xF8, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFC, + 0x00, 0x00, 0x00, 0x00, 0x0F, 0xC0, 0x0F, 0xC0, 0x00, 0x00, 0x00, 0x1F, + 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x1E, 0x00, 0x00, + 0x00, 0x1C, 0x00, 0x00, 0x03, 0x80, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, + 0xF0, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x18, 0x00, + 0x00, 0x00, 0x06, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, + 0x1C, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x38, + 0x38, 0x00, 0x0C, 0x3C, 0x00, 0x00, 0x0F, 0x0C, 0x00, 0x0E, 0x38, 0x00, + 0x00, 0x01, 0xC7, 0x00, 0x06, 0x38, 0x00, 0x00, 0x00, 0x71, 0x80, 0x07, + 0x18, 0x00, 0x00, 0x00, 0x18, 0xE0, 0x03, 0x08, 0x00, 0x00, 0x00, 0x04, + 0x30, 0x01, 0x80, 0x3E, 0x00, 0x07, 0xC0, 0x18, 0x00, 0xC0, 0x71, 0xC0, + 0x0C, 0x38, 0x0C, 0x00, 0xC0, 0x60, 0x30, 0x0C, 0x06, 0x03, 0x00, 0x60, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xC0, 0x1F, 0xC0, 0x00, 0x00, 0x00, 0x0F, 0xE0, 0x1F, 0x60, 0x00, 0x00, + 0x00, 0x06, 0xF8, 0x3C, 0x30, 0x00, 0x00, 0x00, 0x03, 0x0E, 0x38, 0x10, + 0x00, 0x00, 0x00, 0x00, 0x83, 0x98, 0x18, 0x00, 0x00, 0x00, 0x00, 0x40, + 0xD8, 0x0C, 0x60, 0x00, 0x00, 0x06, 0x30, 0x3C, 0x04, 0x6F, 0xC0, 0x00, + 0xFF, 0x98, 0x1E, 0x02, 0x30, 0x1F, 0xFF, 0xE0, 0xC4, 0x0F, 0x03, 0x1C, + 0x00, 0x00, 0x00, 0xE2, 0x07, 0x81, 0x07, 0xE0, 0x00, 0x07, 0xE1, 0x83, + 0x61, 0x83, 0xFF, 0xFF, 0xFF, 0xF0, 0x63, 0x1F, 0xC0, 0xFF, 0xFF, 0xFF, + 0xF0, 0x3F, 0x86, 0x30, 0x3F, 0xFF, 0xFF, 0xF0, 0x32, 0x00, 0x18, 0x0F, + 0xFF, 0xFF, 0xF0, 0x18, 0x00, 0x06, 0x03, 0xFC, 0x0F, 0xF0, 0x18, 0x00, + 0x03, 0x80, 0x78, 0x01, 0xE0, 0x1C, 0x00, 0x00, 0xE0, 0x0E, 0x01, 0xC0, + 0x1C, 0x00, 0x00, 0x30, 0x00, 0xFF, 0x00, 0x0C, 0x00, 0x00, 0x0E, 0x00, + 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x80, 0x00, 0x00, 0x1C, 0x00, 0x00, + 0x00, 0xE0, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x3C, + 0x00, 0x00, 0x00, 0x07, 0x80, 0x00, 0x78, 0x00, 0x00, 0x00, 0x01, 0xF8, + 0x01, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xE0, 0x00, 0x00, 0x00, + 0x00, 0x01, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x02, 0x0C, 0x00, 0x00, + 0x00, 0x00, 0x07, 0x80, 0x00, 0x00, 0x00, 0x1F, 0x03, 0x80, 0x00, 0x00, + 0x1F, 0x3F, 0x03, 0x00, 0x00, 0x00, 0x7F, 0xC3, 0x00, 0x07, 0x80, 0x00, + 0xC7, 0x83, 0x00, 0x1F, 0x80, 0x03, 0x07, 0x03, 0x00, 0x63, 0x00, 0x06, + 0x07, 0x03, 0x01, 0xC6, 0x00, 0x06, 0x07, 0x03, 0x03, 0x0C, 0x00, 0x06, + 0x07, 0x03, 0x06, 0x18, 0x00, 0x0E, 0x07, 0x03, 0x0C, 0x30, 0x00, 0x7E, + 0x07, 0x03, 0x18, 0x60, 0x00, 0xC6, 0x07, 0x03, 0x30, 0xC0, 0x03, 0x06, + 0x07, 0x03, 0x61, 0x80, 0x06, 0x06, 0x07, 0x03, 0xC1, 0x80, 0x0E, 0x06, + 0x07, 0x03, 0x83, 0x00, 0x0E, 0x06, 0x07, 0x03, 0x06, 0x00, 0x0E, 0x06, + 0x06, 0x06, 0x06, 0x00, 0x3E, 0x06, 0x00, 0x18, 0x0C, 0x00, 0xFE, 0x06, + 0x00, 0x30, 0x18, 0x03, 0x8E, 0x06, 0x00, 0x40, 0x18, 0x06, 0x0E, 0x06, + 0x01, 0x80, 0x30, 0x0C, 0x0E, 0x00, 0x03, 0x00, 0x30, 0x1C, 0x0E, 0x00, + 0x06, 0x00, 0x60, 0x1C, 0x0E, 0x00, 0x0C, 0x00, 0x60, 0x1C, 0x0E, 0x00, + 0x18, 0x00, 0xC0, 0x1C, 0x08, 0x00, 0x30, 0x01, 0x80, 0x1C, 0x00, 0x00, + 0x30, 0x03, 0x0C, 0x1C, 0x00, 0x00, 0x60, 0x02, 0x0C, 0x1C, 0x00, 0x00, + 0x60, 0x0F, 0x0E, 0x1C, 0x00, 0x00, 0xC0, 0x1B, 0x0C, 0x1C, 0x00, 0x00, + 0x00, 0x36, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x67, 0x00, 0x1C, 0x00, 0x00, + 0x00, 0xC7, 0x80, 0x1C, 0x00, 0x00, 0x03, 0x07, 0x00, 0x1C, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x18, 0x00, 0x00, 0x1C, 0x00, 0x00, + 0x30, 0x00, 0x00, 0x0C, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x0E, 0x00, 0x03, + 0x00, 0x00, 0x00, 0x0F, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x07, 0x80, 0xF0, + 0x00, 0x00, 0x00, 0x03, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, + 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, + 0x00, 0x00, 0x01, 0xB0, 0x00, 0x00, 0x01, 0x80, 0x06, 0x60, 0x00, 0x00, + 0x07, 0x80, 0x1C, 0xC0, 0x00, 0x00, 0x0F, 0xC0, 0x31, 0x80, 0x00, 0x00, + 0x19, 0xC0, 0xC3, 0x00, 0x00, 0x00, 0x11, 0xE1, 0x82, 0x00, 0x00, 0x00, + 0x30, 0xE6, 0x06, 0x00, 0x00, 0x00, 0x60, 0xFF, 0xCC, 0x00, 0x00, 0x00, + 0xC1, 0xFF, 0xF8, 0x3F, 0x80, 0x01, 0x8F, 0x80, 0xFF, 0xFF, 0x00, 0x01, + 0xBC, 0x00, 0x7E, 0x06, 0x00, 0x03, 0xE0, 0x00, 0x38, 0x18, 0x00, 0x07, + 0x80, 0x00, 0x38, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x39, 0xC0, 0x01, 0xF8, + 0x00, 0x00, 0x33, 0x00, 0xFF, 0xF0, 0x00, 0x00, 0x7C, 0x03, 0xF0, 0xC0, + 0x00, 0x00, 0x78, 0x06, 0x03, 0x80, 0x00, 0x00, 0xE0, 0x0E, 0x06, 0x00, + 0x00, 0x00, 0xC0, 0x0F, 0x0C, 0x00, 0x00, 0x01, 0xE0, 0x07, 0x18, 0x00, + 0x00, 0x03, 0xE0, 0x07, 0xB0, 0x00, 0x00, 0x06, 0xF0, 0x03, 0xE0, 0x00, + 0x00, 0x0C, 0x70, 0x01, 0xC0, 0x00, 0x00, 0x18, 0x78, 0x01, 0x80, 0x00, + 0x00, 0x30, 0x38, 0x03, 0x80, 0x00, 0x00, 0xC0, 0x30, 0x0F, 0x00, 0x00, + 0x01, 0x8F, 0xE0, 0x3F, 0x00, 0x00, 0x07, 0xFF, 0x00, 0x66, 0x00, 0x00, + 0x0F, 0xC0, 0x01, 0x8E, 0x00, 0x00, 0x38, 0x00, 0x07, 0x0E, 0x00, 0x00, + 0xF0, 0x00, 0x0C, 0x0E, 0x00, 0x03, 0xE0, 0x00, 0x30, 0x3F, 0x00, 0x1E, + 0xC0, 0x00, 0x6F, 0xFF, 0x80, 0xF8, 0x80, 0x00, 0xFE, 0x0F, 0xFF, 0xC1, + 0x80, 0x00, 0xC0, 0x19, 0xFF, 0x83, 0x00, 0x00, 0x00, 0x30, 0x33, 0x86, + 0x00, 0x00, 0x00, 0x60, 0xE3, 0xCC, 0x00, 0x00, 0x00, 0x41, 0x81, 0xCC, + 0x00, 0x00, 0x00, 0xC6, 0x01, 0xF8, 0x00, 0x00, 0x01, 0x9C, 0x00, 0xF0, + 0x00, 0x00, 0x03, 0x30, 0x00, 0x00, 0x00, 0x00, 0x06, 0xC0, 0x00, 0x00, + 0x00, 0x00, 0x0F, 0x80, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xC0, 0x00, + 0x00, 0x00, 0x00, 0x7F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, + 0x00, 0x00, 0x00, 0xE0, 0x01, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x70, + 0x00, 0x00, 0x00, 0xE0, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x60, 0x00, 0x06, + 0x00, 0x00, 0x00, 0x60, 0x00, 0x01, 0x80, 0x00, 0x00, 0x30, 0x00, 0x00, + 0xC0, 0x00, 0x03, 0xF0, 0x00, 0x00, 0x30, 0x00, 0x07, 0xFE, 0x00, 0x00, + 0x18, 0x00, 0x07, 0x83, 0xC0, 0x00, 0x0C, 0x00, 0x07, 0x00, 0x60, 0x00, + 0x02, 0x00, 0x03, 0x00, 0x18, 0x00, 0x01, 0x00, 0x03, 0x80, 0x06, 0x00, + 0x01, 0x80, 0x01, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x00, 0xC0, 0x01, 0x80, + 0x00, 0x70, 0x07, 0xE0, 0x00, 0x00, 0x00, 0x3E, 0x0F, 0xF0, 0x00, 0x00, + 0x00, 0x01, 0xCF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x66, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x07, 0x80, 0x00, 0x00, 0x00, 0x00, 0x03, 0xC0, 0x00, 0x00, + 0x00, 0x00, 0x01, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x01, 0x9E, 0x00, 0x00, + 0x00, 0x00, 0x03, 0xC7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, + 0x00, 0x60, 0x00, 0x60, 0x00, 0x70, 0x00, 0x70, 0x00, 0x70, 0x00, 0x78, + 0x00, 0x78, 0x00, 0x78, 0x00, 0x7C, 0x00, 0x7C, 0x00, 0x7C, 0x00, 0x3E, + 0x00, 0x3E, 0x00, 0x3E, 0x00, 0x1E, 0x00, 0x1E, 0x00, 0x1E, 0x00, 0x07, + 0x00, 0x07, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0xC0, 0x00, 0x00, + 0x00, 0x70, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x78, 0x00, 0xF0, 0x00, 0x00, + 0x00, 0x7C, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x7C, 0x00, 0x00, + 0x00, 0x1E, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x07, 0x00, 0x0E, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x07, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xF8, + 0x00, 0x00, 0x00, 0x00, 0x0F, 0x80, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x78, + 0x00, 0x70, 0x00, 0x00, 0x00, 0x03, 0x80, 0x00, 0xE0, 0x00, 0x00, 0x00, + 0x1C, 0x00, 0x01, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x03, 0x80, 0x00, + 0x00, 0x07, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x0C, + 0x00, 0x00, 0x0F, 0xE0, 0x00, 0x00, 0x30, 0x00, 0x00, 0xFF, 0xE0, 0x00, + 0x00, 0xE0, 0x00, 0x07, 0x83, 0xC0, 0x00, 0x01, 0x80, 0x00, 0x38, 0x03, + 0x80, 0x00, 0x06, 0x00, 0x01, 0xC0, 0x07, 0x00, 0x00, 0x18, 0x00, 0x06, + 0x00, 0x0C, 0x00, 0x00, 0x60, 0x00, 0x38, 0x00, 0x38, 0x00, 0x01, 0x80, + 0x00, 0xC0, 0x00, 0x60, 0x00, 0x06, 0x00, 0x03, 0x00, 0x01, 0x80, 0x00, + 0x1C, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x03, 0xF0, 0x00, 0x00, + 0x00, 0x00, 0x38, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x31, 0xC0, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xB8, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x60, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x19, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x63, 0x80, + 0x00, 0x00, 0x00, 0x00, 0x03, 0x07, 0x81, 0x80, 0x00, 0x00, 0x00, 0x38, + 0x0F, 0xFF, 0x00, 0x00, 0x00, 0x0F, 0xC0, 0x07, 0xF7, 0x00, 0x60, 0x00, + 0x7C, 0x00, 0x00, 0x0F, 0xFF, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x07, 0xF3, + 0xC0, 0x78, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xC0, 0x00, 0x00, 0x00, + 0x00, 0x03, 0xF8, 0x00, 0x00, 0x00, 0x7F, 0x80, 0x00, 0x1F, 0xE0, 0x00, + 0x1F, 0xFE, 0x00, 0x07, 0xFF, 0x80, 0x07, 0xCF, 0xF8, 0x01, 0xF3, 0xBE, + 0x00, 0xFC, 0xD9, 0xC0, 0x38, 0x3C, 0xF0, 0x1F, 0xEC, 0xFE, 0x07, 0xE3, + 0x6F, 0x83, 0xB7, 0xC7, 0xB0, 0xDF, 0x33, 0xD8, 0x33, 0x1C, 0x39, 0x99, + 0xBB, 0x1C, 0xC7, 0xF0, 0xC1, 0xD9, 0xD9, 0xF0, 0xCE, 0x6F, 0x0E, 0x1E, + 0xFF, 0x8F, 0x0E, 0x66, 0x70, 0xF1, 0xB6, 0x78, 0x30, 0xF6, 0xC3, 0x8D, + 0x99, 0xE1, 0x83, 0x8D, 0xBC, 0x3C, 0xCD, 0x8E, 0x1C, 0x3C, 0xCF, 0xE3, + 0x6C, 0x78, 0x61, 0xE3, 0x6C, 0x7F, 0x33, 0xC3, 0x87, 0x1B, 0x33, 0xC3, + 0xFB, 0x1C, 0x1C, 0x79, 0x9B, 0x1C, 0x3D, 0xF0, 0xC1, 0xE6, 0xD8, 0xF0, + 0xE3, 0xC7, 0x0E, 0x1F, 0x67, 0x87, 0x0F, 0x3C, 0x30, 0xF1, 0xBE, 0x38, + 0x30, 0xFB, 0x63, 0x8D, 0x98, 0xE1, 0xC3, 0x8D, 0xE6, 0x3C, 0xCF, 0x86, + 0x1E, 0x3E, 0xC6, 0x63, 0x6C, 0x78, 0x71, 0xF3, 0x7C, 0x63, 0x33, 0xC3, + 0x87, 0x99, 0xB3, 0xCC, 0x3B, 0x1C, 0x1C, 0x6D, 0x8F, 0x1C, 0xC1, 0xF0, + 0xE1, 0xE6, 0x78, 0x70, 0xF8, 0x1F, 0x0F, 0x1B, 0x63, 0x83, 0x0F, 0x80, + 0xF8, 0xF9, 0x9E, 0x1C, 0x38, 0xF0, 0x0F, 0xCD, 0xD8, 0xE1, 0xE3, 0xCF, + 0x00, 0x7E, 0xCF, 0x86, 0x1F, 0x36, 0xE0, 0x03, 0x3C, 0x38, 0x71, 0xBB, + 0x3C, 0x00, 0x39, 0xC1, 0x87, 0x99, 0xF1, 0xC0, 0x01, 0xCC, 0x1C, 0x6D, + 0x87, 0x38, 0x00, 0x0C, 0xE1, 0xE6, 0x78, 0x33, 0x00, 0x00, 0x6F, 0x1B, + 0x63, 0x83, 0xE0, 0x00, 0x03, 0xD9, 0x9E, 0x1C, 0x3C, 0x00, 0x00, 0x1C, + 0xF8, 0xE1, 0xE3, 0x80, 0x00, 0x00, 0xC7, 0x87, 0x1F, 0x30, 0x00, 0x00, + 0x06, 0x38, 0x79, 0x9E, 0x00, 0x00, 0x00, 0x31, 0xC7, 0xD8, 0xC0, 0x00, + 0x00, 0x01, 0x9E, 0x6F, 0x98, 0x00, 0x00, 0x00, 0x0D, 0xB6, 0x3B, 0x00, + 0x00, 0x00, 0x00, 0x79, 0xE1, 0xE0, 0x00, 0x00, 0x00, 0x03, 0x8E, 0x1C, + 0x00, 0x00, 0x00, 0x00, 0x0C, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x67, + 0x60, 0x00, 0x00, 0x00, 0x00, 0x03, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x1F, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x00, + 0x03, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x00, 0x00, + 0x3F, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xE0, 0x00, 0x00, 0x00, 0x01, + 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x07, + 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x1F, + 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0x80, 0x00, 0x01, 0xFF, + 0xFF, 0xFF, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xFE, 0x00, 0x00, 0x3F, 0xFF, + 0xFF, 0xFC, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xF8, 0x00, 0x01, 0xFF, 0xFF, + 0xFF, 0xF8, 0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xF8, 0x00, 0x0F, 0xFF, 0xFF, + 0xFF, 0xF8, 0x00, 0x1F, 0xFF, 0xFF, 0xFF, 0xF8, 0x00, 0x3F, 0xE3, 0xFF, + 0x1F, 0xF0, 0x00, 0x7F, 0x03, 0xF8, 0x0F, 0xE0, 0x00, 0xFC, 0x03, 0xE0, + 0x0F, 0xE0, 0x07, 0xF0, 0xC3, 0x87, 0x1F, 0xC0, 0x1F, 0xE3, 0xC7, 0x1F, + 0x1F, 0xE0, 0xFF, 0xC7, 0x8E, 0x3E, 0x3F, 0xE1, 0xFF, 0x8F, 0x1C, 0x7C, + 0x7F, 0xE7, 0xFF, 0x0C, 0x38, 0x71, 0xFF, 0xDF, 0xFF, 0x00, 0xF8, 0x03, + 0xFF, 0xFF, 0xFF, 0x03, 0xF8, 0x0F, 0xFF, 0xFF, 0xFF, 0x9F, 0xFC, 0x7F, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFB, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x03, 0xFF, + 0xF7, 0xFF, 0xE0, 0x00, 0x07, 0xFF, 0xEF, 0xFF, 0xC0, 0x00, 0x1F, 0xFF, + 0x8F, 0xFF, 0xC0, 0x00, 0x3F, 0xFF, 0x1F, 0xFF, 0xC0, 0x00, 0xFF, 0xFC, + 0x1F, 0xFF, 0xE0, 0x07, 0xFF, 0xF0, 0x1F, 0xFF, 0xF0, 0x3F, 0xFF, 0xE0, + 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x1F, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, + 0x0F, 0xFF, 0xFF, 0xFF, 0xE0, 0x00, 0x07, 0xFF, 0xFF, 0xFF, 0x00, 0x00, + 0x01, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x3F, 0xFE, 0x00, 0x00, 0x00, + 0x00, 0x03, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x18, 0xE0, 0x00, 0x00, 0x00, + 0x00, 0x20, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0x00, + 0x01, 0x81, 0x80, 0x00, 0x00, 0x00, 0x03, 0x03, 0x00, 0x00, 0x00, 0x00, + 0x06, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x06, 0x38, 0x00, 0x00, 0x00, 0x00, + 0x07, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xF8, 0x00, 0x00, 0x00, 0x03, + 0xC0, 0x3C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x60, + 0x00, 0x0E, 0x00, 0x00, 0x01, 0x80, 0x00, 0x0E, 0x00, 0x00, 0x06, 0x04, + 0x00, 0x0C, 0x00, 0x00, 0x08, 0x38, 0x00, 0x0C, 0x00, 0x00, 0x30, 0xE0, + 0x00, 0x08, 0x00, 0x00, 0x43, 0x00, 0x00, 0x18, 0x00, 0x01, 0x86, 0x00, + 0x00, 0x10, 0x00, 0x03, 0x18, 0x00, 0x00, 0x30, 0x00, 0x04, 0x30, 0x00, + 0x00, 0x60, 0x00, 0x18, 0x40, 0x00, 0x00, 0xC0, 0x00, 0x30, 0x00, 0x00, + 0x00, 0x80, 0x00, 0x60, 0x00, 0x00, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, + 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x0C, 0x00, 0x04, 0x00, 0x00, 0x00, 0x18, 0x00, 0x18, 0x00, 0x00, 0x00, + 0x30, 0x00, 0x30, 0x00, 0x00, 0x00, 0x20, 0x00, 0x60, 0x00, 0x00, 0x00, + 0x60, 0x00, 0x80, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x3F, 0xFF, 0xFE, 0x00, + 0x80, 0x07, 0xE0, 0x00, 0x00, 0x01, 0x80, 0x18, 0x00, 0x00, 0x00, 0x01, + 0x80, 0x60, 0x00, 0x00, 0x00, 0x03, 0x81, 0x80, 0x00, 0x00, 0x00, 0x03, + 0x86, 0x00, 0x00, 0x00, 0x00, 0x03, 0x18, 0x00, 0xFF, 0xFF, 0xF0, 0x03, + 0x30, 0xFF, 0xFF, 0xFF, 0xFF, 0xC3, 0xFF, 0xFF, 0xE0, 0x07, 0xFF, 0xFB, + 0xFF, 0xFF, 0xC0, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x1F, 0xFF, 0xFB, + 0xFF, 0xFF, 0x80, 0x7F, 0xFF, 0xF3, 0xFF, 0xFF, 0x01, 0xFF, 0xFF, 0x81, + 0xFF, 0xFF, 0x87, 0xFF, 0xFC, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, + 0x03, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x1F, 0xF8, 0x00, 0x00, 0x00, + 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x07, 0xE0, 0x07, 0xE0, 0x00, 0x00, + 0x1F, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x3C, 0x00, 0x00, + 0x70, 0x00, 0x00, 0x0E, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x07, 0x00, 0x03, + 0x80, 0x00, 0x00, 0x01, 0x80, 0x03, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x06, + 0x00, 0x00, 0x00, 0x00, 0x60, 0x0E, 0x08, 0x00, 0x00, 0x30, 0x70, 0x1C, + 0x3C, 0x00, 0x00, 0x3C, 0x38, 0x18, 0xE0, 0x00, 0x00, 0x0F, 0x18, 0x39, + 0xC0, 0x00, 0x00, 0x03, 0x9C, 0x33, 0x80, 0x00, 0x00, 0x01, 0xCC, 0x73, + 0x00, 0x00, 0x00, 0x00, 0xCC, 0x60, 0x1F, 0x00, 0x00, 0xF8, 0x06, 0x60, + 0x7F, 0xC0, 0x03, 0xFE, 0x06, 0x60, 0xF3, 0xE0, 0x07, 0x8F, 0x06, 0xC0, + 0xF3, 0xA0, 0x07, 0x8F, 0x03, 0xC0, 0x33, 0x80, 0x03, 0x8C, 0x03, 0xC0, + 0x33, 0x80, 0x03, 0x8C, 0x03, 0xC0, 0x33, 0x80, 0x03, 0x8C, 0x03, 0xC0, + 0x33, 0x80, 0x03, 0x8C, 0x03, 0xC0, 0x33, 0x80, 0x03, 0x8C, 0x03, 0xC0, + 0x33, 0x80, 0x03, 0x8C, 0x03, 0xC0, 0x33, 0xBF, 0xFB, 0x8C, 0x03, 0xC0, + 0x33, 0xFC, 0x7F, 0x8C, 0x03, 0xC0, 0x33, 0xC0, 0x07, 0x8C, 0x03, 0x60, + 0x33, 0x80, 0x03, 0x8C, 0x06, 0x60, 0x33, 0x80, 0x03, 0x8C, 0x06, 0x60, + 0x33, 0x80, 0x03, 0x8C, 0x06, 0x30, 0x33, 0xFF, 0xFF, 0x8C, 0x0C, 0x30, + 0x33, 0xFF, 0xFF, 0x8C, 0x0C, 0x38, 0x33, 0xFF, 0xFF, 0x8C, 0x18, 0x18, + 0x33, 0xFF, 0xFF, 0x8C, 0x18, 0x0C, 0x33, 0xFF, 0xFF, 0x8C, 0x30, 0x0E, + 0x33, 0xF0, 0x1F, 0x8C, 0x70, 0x06, 0x33, 0x80, 0x03, 0x8C, 0x60, 0x03, + 0x33, 0x80, 0x03, 0x8C, 0xC0, 0x01, 0xB3, 0x80, 0x03, 0x8D, 0x80, 0x07, + 0xF3, 0x80, 0x03, 0x8F, 0xC0, 0x0E, 0x03, 0x80, 0x03, 0x80, 0xF0, 0x0C, + 0x01, 0xE0, 0x0F, 0x00, 0x30, 0x07, 0x00, 0x78, 0x1C, 0x00, 0xE0, 0x03, + 0x00, 0x18, 0x18, 0x00, 0xC0, 0x03, 0x80, 0x3F, 0xFC, 0x03, 0xC0, 0x01, + 0xFF, 0xF7, 0xEF, 0xFF, 0x80, 0x00, 0x7F, 0xC0, 0x03, 0xFE, 0x00, 0x00, + 0x00, 0x1D, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x80, 0x00, 0x00, 0x00, + 0x03, 0x33, 0x00, 0x00, 0x00, 0x00, 0x0C, 0xCC, 0x00, 0x00, 0x00, 0x00, + 0x3F, 0xF0, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x06, + 0x33, 0x00, 0x00, 0x00, 0x00, 0x18, 0xC4, 0x00, 0x00, 0x00, 0x00, 0x63, + 0x18, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x07, 0x7B, + 0x80, 0x00, 0x00, 0x00, 0x18, 0xC6, 0x00, 0x00, 0x00, 0x00, 0x43, 0x18, + 0x00, 0x00, 0x00, 0x03, 0x0C, 0x60, 0x00, 0x00, 0x00, 0x0C, 0x30, 0x80, + 0x00, 0x00, 0x00, 0x30, 0xC3, 0x00, 0x00, 0x00, 0x00, 0xC3, 0x0C, 0x00, + 0x00, 0x00, 0x03, 0x0C, 0x30, 0x00, 0x00, 0x00, 0x0C, 0x30, 0xC0, 0x00, + 0x00, 0x00, 0x60, 0xC3, 0x00, 0x00, 0x00, 0x01, 0x83, 0x06, 0x00, 0x00, + 0x00, 0x06, 0x0C, 0x18, 0x00, 0x00, 0x00, 0x30, 0x30, 0x60, 0x00, 0x00, + 0x00, 0xC0, 0xC0, 0x80, 0x00, 0x00, 0x03, 0x03, 0x03, 0x00, 0x00, 0x00, + 0x0C, 0x0C, 0x0C, 0x00, 0x00, 0x00, 0x30, 0x30, 0x30, 0x00, 0x00, 0x01, + 0x80, 0xC0, 0xC0, 0x00, 0x00, 0x36, 0x03, 0x01, 0xA0, 0x00, 0x03, 0xF0, + 0x0C, 0x07, 0xF0, 0x00, 0x3F, 0x80, 0x30, 0x07, 0xF0, 0x03, 0xCC, 0x00, + 0xC0, 0x18, 0xF0, 0x7C, 0x18, 0x03, 0x00, 0x60, 0xF3, 0xC0, 0x60, 0x0C, + 0x01, 0x80, 0xE0, 0x01, 0xC0, 0x30, 0x0C, 0x00, 0x80, 0x03, 0x01, 0xE0, + 0x70, 0x00, 0x00, 0x06, 0x1F, 0xC1, 0x80, 0x00, 0x00, 0x1C, 0x63, 0x8C, + 0x00, 0x00, 0x00, 0x3B, 0x03, 0x70, 0x00, 0x00, 0x00, 0x7C, 0x0F, 0x80, + 0x00, 0x00, 0x00, 0xF0, 0x7C, 0x00, 0x00, 0x00, 0x01, 0xE1, 0xE0, 0x00, + 0x00, 0x00, 0x03, 0x8F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x1C, 0x00, 0x00, + 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0x00, 0x07, 0x00, 0x38, 0x00, 0x00, + 0x00, 0x38, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, + 0x00, 0x1F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, + 0x07, 0xE0, 0x07, 0xE0, 0x00, 0x00, 0x1F, 0x00, 0x00, 0xF8, 0x00, 0x00, + 0x3C, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x70, 0x00, 0x00, 0x0E, 0x00, 0x00, + 0xE0, 0x00, 0x00, 0x07, 0x00, 0x01, 0x80, 0x00, 0x00, 0x01, 0x80, 0x03, + 0x00, 0x00, 0x00, 0x00, 0xC0, 0x06, 0x03, 0x00, 0x00, 0x00, 0x60, 0x0E, + 0x0F, 0x00, 0x00, 0x00, 0x70, 0x1C, 0x1C, 0x00, 0x00, 0x00, 0x30, 0x18, + 0x30, 0x00, 0x00, 0x00, 0x18, 0x38, 0x60, 0x00, 0x00, 0x00, 0x1C, 0x30, + 0x40, 0x00, 0x00, 0x18, 0x0C, 0x70, 0x03, 0xC0, 0x00, 0x78, 0x0C, 0x60, + 0x03, 0xC0, 0x00, 0xE0, 0x06, 0x60, 0x07, 0xE0, 0x03, 0xC0, 0x06, 0x60, + 0x07, 0xE0, 0x07, 0x00, 0x06, 0xC0, 0x07, 0xE0, 0x0F, 0xC0, 0x03, 0xC0, + 0x03, 0xC0, 0x07, 0xF8, 0x03, 0xC0, 0x03, 0xC0, 0x00, 0x38, 0x03, 0xC0, + 0x00, 0x00, 0x00, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x03, 0xC0, + 0x00, 0x00, 0x00, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x00, 0x0F, 0x03, 0xC0, + 0x00, 0x00, 0x00, 0x1F, 0x83, 0xC0, 0x00, 0x00, 0xE0, 0x3F, 0xC3, 0xC0, + 0x00, 0x00, 0xF0, 0x3F, 0xC0, 0x60, 0x00, 0x00, 0x38, 0x3F, 0xFC, 0x60, + 0x00, 0x00, 0x18, 0x3F, 0xFE, 0x60, 0x00, 0x00, 0x38, 0x3F, 0xFF, 0x70, + 0x00, 0x00, 0x70, 0x3F, 0xFF, 0x30, 0x00, 0x00, 0x70, 0x1F, 0xFF, 0x38, + 0x00, 0x00, 0x18, 0x1F, 0xFF, 0x18, 0x00, 0x00, 0x18, 0x1F, 0xFE, 0x1C, + 0x00, 0x00, 0x38, 0x3F, 0xFC, 0x0E, 0x00, 0x00, 0xF0, 0x3F, 0xF8, 0x07, + 0x00, 0x00, 0xE0, 0x0F, 0xE0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x07, 0x80, 0x00, + 0x70, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x3C, 0x00, 0x00, + 0x1F, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x07, 0xE0, 0x07, 0xE0, 0x00, 0x00, + 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xF8, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x38, 0x01, 0x80, 0x00, 0x00, + 0x07, 0xE0, 0x06, 0x00, 0x00, 0xC0, 0x3C, 0x00, 0x00, 0x38, 0x03, 0x01, + 0xC0, 0x18, 0x00, 0x60, 0x1C, 0x06, 0x00, 0x60, 0x00, 0xC0, 0x00, 0x38, + 0x00, 0xC0, 0x03, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x0E, 0x00, 0x03, 0x0E, + 0x00, 0x00, 0x19, 0x80, 0x0C, 0xFE, 0x00, 0x00, 0x67, 0x00, 0x37, 0x18, + 0x00, 0x01, 0x84, 0x00, 0xF8, 0x30, 0xF0, 0x06, 0x00, 0x01, 0xC0, 0xC7, + 0xF0, 0x18, 0x00, 0x03, 0x02, 0x38, 0xF0, 0xE0, 0x1C, 0x0F, 0xF8, 0xC0, + 0xF7, 0x00, 0x78, 0x3F, 0xC3, 0x00, 0xF8, 0x00, 0x40, 0x40, 0x0C, 0x00, + 0x00, 0x00, 0x1F, 0x80, 0x18, 0x00, 0x00, 0x01, 0xFF, 0x80, 0x60, 0x60, + 0x00, 0x0F, 0x1F, 0x80, 0xC1, 0x80, 0x00, 0x30, 0x67, 0x03, 0x06, 0x00, + 0x01, 0xC1, 0x8E, 0x38, 0x00, 0x00, 0x06, 0x06, 0x0F, 0xE0, 0x00, 0x00, + 0x18, 0x18, 0x3E, 0x00, 0x07, 0x00, 0xE0, 0xE3, 0xF0, 0x00, 0x3C, 0x03, + 0x83, 0x0C, 0xC1, 0x81, 0xC0, 0x1E, 0x18, 0x71, 0x87, 0x0C, 0x00, 0x78, + 0x61, 0x86, 0x08, 0x30, 0x01, 0xF0, 0x06, 0x18, 0x00, 0x80, 0x0C, 0xC0, + 0x00, 0x30, 0x06, 0x00, 0x33, 0x00, 0x00, 0xC0, 0x18, 0x01, 0xC6, 0x00, + 0x1F, 0xC0, 0x60, 0x07, 0x1C, 0x0F, 0xEF, 0x81, 0x00, 0x1C, 0x38, 0x3E, + 0x37, 0x8C, 0x00, 0xD8, 0x70, 0x00, 0xCF, 0xE0, 0x03, 0x30, 0xE0, 0x06, + 0x0F, 0x00, 0x18, 0xE1, 0xF0, 0x38, 0x00, 0x00, 0x61, 0xC1, 0xFF, 0xC0, + 0x00, 0x73, 0xC3, 0x81, 0xFC, 0x00, 0x01, 0xCF, 0x07, 0x87, 0xC0, 0x00, + 0x00, 0x36, 0x07, 0xFC, 0x00, 0x20, 0x01, 0x9C, 0x0F, 0x80, 0x00, 0xC0, + 0x06, 0x3C, 0xF8, 0x00, 0x03, 0x00, 0x30, 0x3F, 0x00, 0x00, 0x04, 0x0C, + 0xC1, 0xF0, 0x00, 0x00, 0x00, 0x3B, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x6F, + 0xE0, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x1F, 0xF8, 0x00, 0x00, 0x00, 0x01, 0xE0, 0x07, 0x80, 0x00, 0x00, + 0x07, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x38, 0x00, 0x00, + 0x30, 0x00, 0x00, 0x0C, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x03, 0x00, 0x01, + 0x80, 0x00, 0x00, 0x01, 0x80, 0x03, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x06, + 0x00, 0x00, 0x00, 0x00, 0x60, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x30, 0x0C, + 0x00, 0x00, 0x00, 0x00, 0x30, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, + 0x00, 0x00, 0x00, 0x00, 0x18, 0x30, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x30, + 0x03, 0xC0, 0x03, 0xC0, 0x0C, 0x60, 0x03, 0xC0, 0x03, 0xC0, 0x06, 0x60, + 0x07, 0xE0, 0x07, 0xE0, 0x06, 0x60, 0x07, 0xE0, 0x07, 0xE0, 0x06, 0xC0, + 0x07, 0xE0, 0x07, 0xE0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, + 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x03, 0xC0, + 0x00, 0x00, 0x00, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x03, 0xC0, + 0x00, 0x00, 0x00, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x03, 0xC0, + 0x60, 0x00, 0x00, 0x06, 0x03, 0xC0, 0xDF, 0x80, 0x01, 0xFF, 0x03, 0x60, + 0xC0, 0x7F, 0xFF, 0x83, 0x06, 0x60, 0xE0, 0x00, 0x00, 0x07, 0x06, 0x60, + 0xFE, 0x00, 0x00, 0x7F, 0x06, 0x30, 0x7F, 0xFF, 0xFF, 0xFE, 0x0C, 0x30, + 0x3F, 0xFF, 0xFF, 0xFC, 0x0C, 0x38, 0x1F, 0xFF, 0xFF, 0xF8, 0x18, 0x18, + 0x0F, 0xFF, 0xFF, 0xF0, 0x18, 0x0C, 0x07, 0xF8, 0x1F, 0xE0, 0x30, 0x0C, + 0x01, 0xE0, 0x07, 0x80, 0x30, 0x06, 0x00, 0x70, 0x0E, 0x00, 0x60, 0x03, + 0x00, 0x0F, 0xF0, 0x00, 0xC0, 0x01, 0x80, 0x00, 0x00, 0x01, 0x80, 0x00, + 0xC0, 0x00, 0x00, 0x03, 0x00, 0x00, 0x70, 0x00, 0x00, 0x0E, 0x00, 0x00, + 0x1C, 0x00, 0x00, 0x38, 0x00, 0x00, 0x07, 0x00, 0x00, 0xE0, 0x00, 0x00, + 0x01, 0xE0, 0x07, 0x80, 0x00, 0x00, 0x00, 0x1F, 0xF8, 0x00, 0x00, 0x00, + 0x00, 0x1F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, + 0x07, 0xE0, 0x07, 0xE0, 0x00, 0x00, 0x1F, 0x00, 0x00, 0xF8, 0x00, 0x00, + 0x3C, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x70, 0x00, 0x00, 0x0E, 0x00, 0x00, + 0xE0, 0x00, 0x00, 0x07, 0x00, 0x01, 0x80, 0x00, 0x00, 0x01, 0x80, 0x03, + 0x00, 0x00, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, 0x00, 0x00, 0x60, 0x0E, + 0x00, 0x00, 0x00, 0x00, 0x70, 0x1C, 0x03, 0x00, 0x01, 0x80, 0x30, 0x18, + 0x07, 0x00, 0x00, 0xE0, 0x18, 0x38, 0x3C, 0x00, 0x00, 0x7C, 0x1C, 0x30, + 0xF8, 0x00, 0x00, 0x1F, 0x0C, 0x70, 0x40, 0x00, 0x00, 0x02, 0x0C, 0x60, + 0x00, 0x00, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x00, 0x00, 0x06, 0x60, + 0x0F, 0xC0, 0x03, 0xF0, 0x06, 0xC0, 0x11, 0xE0, 0x06, 0x38, 0x03, 0xC0, + 0x60, 0xF8, 0x18, 0x1E, 0x03, 0xC0, 0x40, 0xF8, 0x10, 0x1E, 0x03, 0xC0, + 0xC0, 0xFC, 0x30, 0x1F, 0x03, 0xC0, 0xC1, 0xFC, 0x30, 0x3F, 0x03, 0xC0, + 0xE3, 0xFC, 0x38, 0xFF, 0x03, 0xC0, 0xFF, 0x3C, 0x3F, 0xCF, 0x03, 0xC0, + 0xFF, 0x3C, 0x3F, 0xCF, 0x03, 0xC0, 0x7F, 0xF8, 0x1F, 0xFE, 0x03, 0xC0, + 0x7F, 0xF8, 0x1F, 0xFE, 0x03, 0x60, 0x3F, 0xF0, 0x0F, 0xFC, 0x06, 0x60, + 0x0F, 0xC0, 0x03, 0xF0, 0x06, 0x60, 0x00, 0x00, 0x00, 0x00, 0x06, 0x70, + 0x00, 0x00, 0x00, 0x00, 0x0C, 0x30, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x38, + 0x00, 0x00, 0x00, 0x00, 0x1C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x1C, + 0x00, 0x00, 0x00, 0x00, 0x30, 0x0E, 0x00, 0x07, 0xE0, 0x00, 0x70, 0x06, + 0x00, 0x1F, 0xF0, 0x00, 0x60, 0x03, 0x00, 0x08, 0x10, 0x00, 0xC0, 0x03, + 0x80, 0x00, 0x00, 0x01, 0x80, 0x01, 0xE0, 0x00, 0x00, 0x07, 0x00, 0x00, + 0x70, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x3C, 0x00, 0x00, + 0x1F, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x07, 0xE0, 0x07, 0xE0, 0x00, 0x00, + 0x01, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x1F, 0xF8, 0x00, 0x00, 0x00, + 0x00, 0x1F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xE0, 0x00, 0x00, + 0x00, 0x1F, 0x80, 0x1F, 0x00, 0x60, 0x00, 0x0F, 0x80, 0x00, 0x78, 0x0E, + 0x00, 0x03, 0xC0, 0x00, 0x03, 0xC3, 0xC0, 0x00, 0xE0, 0x00, 0x00, 0x1C, + 0xCC, 0x00, 0x38, 0x00, 0x00, 0x01, 0xF8, 0xC0, 0x1C, 0x00, 0x00, 0x00, + 0x1E, 0x1C, 0x03, 0x00, 0x00, 0x00, 0x01, 0x81, 0x80, 0xC0, 0x00, 0x00, + 0x00, 0x70, 0x38, 0x38, 0x00, 0x00, 0x00, 0x0C, 0x03, 0x0E, 0x00, 0x00, + 0x00, 0x03, 0x80, 0x71, 0x80, 0x00, 0x00, 0x00, 0x60, 0x06, 0x70, 0x00, + 0x00, 0x00, 0x0C, 0x00, 0xCC, 0x00, 0x00, 0x00, 0x01, 0x80, 0x1B, 0x80, + 0x00, 0x00, 0x00, 0x30, 0x03, 0x60, 0x00, 0x00, 0x00, 0x06, 0x00, 0x6C, + 0x00, 0x00, 0x00, 0x20, 0x60, 0x19, 0x80, 0x1F, 0xC0, 0x3F, 0x86, 0x06, + 0x60, 0x06, 0x1C, 0x0E, 0x38, 0x3F, 0x8C, 0x00, 0x81, 0x81, 0x01, 0x00, + 0x31, 0x80, 0x00, 0x00, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xC6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x00, + 0x00, 0x03, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x63, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x0C, 0x60, 0x30, 0x00, 0x00, 0x07, 0x01, 0x8C, 0x0D, 0xF8, + 0x00, 0x1F, 0xE0, 0x70, 0xC1, 0x80, 0xFF, 0xFE, 0x06, 0x0C, 0x18, 0x38, + 0x00, 0x00, 0x01, 0x81, 0x83, 0x07, 0xF0, 0x00, 0x03, 0xF0, 0x30, 0x70, + 0x7F, 0xFF, 0xFF, 0xFC, 0x0C, 0x06, 0x07, 0xFF, 0xFF, 0xFF, 0x81, 0x80, + 0xE0, 0x7F, 0xFF, 0xFF, 0xE0, 0x70, 0x0C, 0x07, 0xFF, 0xFF, 0xF8, 0x0C, + 0x01, 0xC0, 0x7F, 0x81, 0xFE, 0x03, 0x00, 0x1C, 0x07, 0xC0, 0x0F, 0x00, + 0xE0, 0x01, 0x80, 0x1C, 0x03, 0x80, 0x38, 0x00, 0x18, 0x00, 0xFF, 0x80, + 0x0E, 0x00, 0x01, 0x80, 0x00, 0x00, 0x03, 0x80, 0x00, 0x1C, 0x00, 0x00, + 0x00, 0xE0, 0x00, 0x01, 0xC0, 0x00, 0x00, 0x38, 0x00, 0x00, 0x1E, 0x00, + 0x00, 0x1E, 0x00, 0x00, 0x01, 0xF0, 0x00, 0x0F, 0x80, 0x00, 0x00, 0x0F, + 0xC0, 0x0F, 0xC0, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xC0, 0x00, 0x00, 0x00, + 0x00, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x03, 0xF0, 0x00, 0x00, 0x00, 0x0F, + 0xE0, 0x00, 0x00, 0x00, 0x7F, 0x80, 0x00, 0x00, 0x01, 0xFE, 0x00, 0x00, + 0x00, 0x0F, 0xF8, 0x00, 0x00, 0x00, 0x7F, 0xE0, 0x00, 0x00, 0x03, 0xFF, + 0x00, 0x00, 0x00, 0x1F, 0xFC, 0x00, 0x00, 0x00, 0xFF, 0xE0, 0x00, 0x00, + 0x07, 0xFF, 0x00, 0x00, 0x00, 0x7F, 0xF8, 0x00, 0x00, 0x03, 0xFF, 0xE0, + 0x00, 0x00, 0x3F, 0xFF, 0x00, 0x00, 0x01, 0xFF, 0xF8, 0x00, 0x08, 0x1F, + 0xFF, 0xC0, 0x00, 0xC1, 0xFF, 0xBE, 0x00, 0x0C, 0x1F, 0xF3, 0xF0, 0x00, + 0xE0, 0xFF, 0x1F, 0x80, 0x0F, 0x0F, 0xF1, 0xF8, 0x00, 0x78, 0x7F, 0x0F, + 0xC0, 0x07, 0xE7, 0xF0, 0x7E, 0x00, 0x3F, 0x3F, 0x03, 0xF0, 0x01, 0xF9, + 0xF0, 0x1F, 0x81, 0x1F, 0xEF, 0x80, 0xFE, 0x08, 0xFF, 0xF8, 0x07, 0xFD, + 0xE7, 0xFF, 0xC0, 0x3F, 0xFF, 0xBF, 0xFE, 0x00, 0xFF, 0xFD, 0xFF, 0xF0, + 0x07, 0xFF, 0xEF, 0xFF, 0x80, 0x1F, 0xFF, 0x7F, 0xFC, 0x00, 0x7F, 0xFF, + 0xFF, 0xF0, 0x01, 0xFF, 0xFF, 0xFB, 0x80, 0x07, 0xFF, 0xFF, 0xCE, 0x00, + 0x1F, 0xFB, 0xFC, 0x20, 0x00, 0xFF, 0x9F, 0xE0, 0x00, 0x03, 0xFC, 0xFF, + 0x00, 0x00, 0x1F, 0xE3, 0xF8, 0x00, 0x00, 0xFF, 0x1F, 0xC0, 0x00, 0x07, + 0xF0, 0x7E, 0x00, 0x00, 0x3F, 0x83, 0xF0, 0x00, 0x01, 0xF8, 0x0F, 0xC0, + 0x00, 0x1F, 0x80, 0x3E, 0x00, 0x00, 0xF8, 0x00, 0xF8, 0x00, 0x0F, 0x80, + 0x03, 0xF0, 0x01, 0xF8, 0x00, 0x07, 0xE0, 0x3F, 0x00, 0x00, 0x0F, 0xFF, + 0xE0, 0x00, 0x00, 0x0F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xF8, 0x00, + 0x00, 0x00, 0x00, 0xFC, 0x1F, 0xC0, 0x00, 0x00, 0x00, 0x70, 0x00, 0x38, + 0x00, 0x00, 0x00, 0x70, 0x00, 0x07, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, + 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x06, 0x00, 0x00, + 0x01, 0x80, 0x00, 0x03, 0x00, 0x10, 0x00, 0x30, 0x00, 0x01, 0x80, 0x3F, + 0x80, 0x0E, 0x00, 0x00, 0x60, 0x1C, 0x70, 0x01, 0x80, 0x00, 0x30, 0x0E, + 0x06, 0x00, 0x30, 0x00, 0x18, 0x03, 0x00, 0xC0, 0x0E, 0x00, 0x06, 0x00, + 0x80, 0x37, 0x81, 0x80, 0x03, 0x00, 0x60, 0x07, 0x60, 0x30, 0x00, 0xC1, + 0xF8, 0x01, 0x98, 0x0C, 0x00, 0x60, 0x1E, 0x00, 0x46, 0x01, 0x80, 0x18, + 0x00, 0x80, 0x01, 0x80, 0x30, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x0E, 0x03, + 0x00, 0x0C, 0x00, 0x30, 0x01, 0x81, 0x80, 0x01, 0x80, 0x0C, 0x00, 0x30, + 0x60, 0x00, 0x70, 0x03, 0x00, 0x06, 0x18, 0x00, 0x0E, 0x00, 0x60, 0x01, + 0x8C, 0x00, 0x0F, 0xC0, 0x18, 0x00, 0x33, 0x00, 0x0F, 0xF0, 0x03, 0x00, + 0x0C, 0xC0, 0x01, 0x06, 0x00, 0x60, 0x01, 0xB0, 0x00, 0x01, 0x80, 0x0C, + 0x00, 0x6C, 0x00, 0x00, 0x20, 0x01, 0xC0, 0x0B, 0x00, 0x00, 0x0C, 0x00, + 0x38, 0x03, 0xC0, 0x00, 0x01, 0x80, 0x06, 0x00, 0xF0, 0x00, 0x00, 0x60, + 0x00, 0x00, 0x3C, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x0D, 0x80, 0x00, 0x03, + 0x80, 0x00, 0x03, 0x60, 0x00, 0x00, 0x70, 0x00, 0x00, 0x9C, 0x00, 0x00, + 0x0E, 0x00, 0x00, 0x63, 0x80, 0x00, 0x01, 0xC0, 0x00, 0x18, 0x70, 0x00, + 0x00, 0x38, 0x00, 0x0C, 0x0F, 0x00, 0x00, 0x07, 0x00, 0x07, 0x00, 0xF8, + 0x00, 0x00, 0x70, 0x03, 0x80, 0x0F, 0xFF, 0x7F, 0xFF, 0xFF, 0xC0, 0x00, + 0x7F, 0xFF, 0xF0, 0xFF, 0xC0, 0x00, 0x00, 0x03, 0xFF, 0xC0, 0x00, 0x00, + 0x00, 0x00, 0x0F, 0xC3, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x38, + 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0xE0, + 0x00, 0x06, 0x00, 0x00, 0x00, 0x01, 0xC0, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x01, 0x80, 0x00, 0x01, 0x80, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0xC0, + 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x70, 0x0C, 0x00, 0x00, + 0x00, 0x60, 0x1C, 0xFC, 0x0C, 0x00, 0x00, 0x00, 0x30, 0x3E, 0xC6, 0x18, + 0x04, 0x00, 0x60, 0x18, 0x63, 0xC6, 0x38, 0x0E, 0x00, 0xF0, 0x18, 0xC3, + 0xC3, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x83, 0xC1, 0xF0, 0x0F, 0x00, 0xF0, + 0x0F, 0x83, 0x60, 0x30, 0x0E, 0x00, 0x60, 0x18, 0x06, 0x60, 0x18, 0x00, + 0x00, 0x00, 0x18, 0x0E, 0x30, 0x0C, 0x00, 0x00, 0x00, 0x30, 0x0C, 0x38, + 0x0C, 0x00, 0x38, 0x00, 0x60, 0x18, 0x1C, 0x06, 0x00, 0x3C, 0x00, 0x60, + 0x30, 0x0C, 0x06, 0x00, 0x7C, 0x00, 0xC0, 0x70, 0x06, 0x03, 0x00, 0x38, + 0x00, 0xC0, 0x60, 0x07, 0x03, 0x00, 0x00, 0x01, 0x80, 0xE0, 0x07, 0x01, + 0x80, 0x00, 0x01, 0x81, 0xE0, 0x0D, 0x81, 0x80, 0x00, 0x01, 0x81, 0xA0, + 0x0D, 0x81, 0x80, 0x00, 0x03, 0x03, 0x30, 0x08, 0xC0, 0xC0, 0x00, 0x03, + 0x03, 0x30, 0x18, 0xC0, 0xC0, 0x00, 0x03, 0x03, 0x10, 0x18, 0xC0, 0xC0, + 0x00, 0x03, 0x02, 0x10, 0x18, 0xC0, 0xC0, 0x00, 0x02, 0x02, 0x18, 0x18, + 0x00, 0xC0, 0x00, 0x02, 0x00, 0x18, 0x18, 0x00, 0xC0, 0x00, 0x06, 0x00, + 0x18, 0x18, 0x00, 0xC0, 0x00, 0x02, 0x00, 0x18, 0x18, 0x00, 0xC0, 0x00, + 0x02, 0x00, 0x18, 0x18, 0x00, 0xC0, 0x00, 0x03, 0x00, 0x10, 0x08, 0x00, + 0xC0, 0x00, 0x03, 0x00, 0x30, 0x0C, 0x01, 0xFF, 0xFF, 0xFF, 0x00, 0x70, + 0x06, 0x03, 0xFF, 0xFF, 0xFF, 0x80, 0xE0, 0x03, 0xFF, 0x00, 0x00, 0x00, + 0xFF, 0xC0, 0x01, 0xFE, 0x00, 0x00, 0x00, 0x7F, 0x80, 0x00, 0x00, 0x1F, + 0xF8, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x07, 0xE0, + 0x07, 0xE0, 0x00, 0x00, 0x1F, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x3C, 0x00, + 0x00, 0x3C, 0x00, 0x00, 0x70, 0x00, 0x00, 0x0E, 0x00, 0x00, 0xE0, 0x00, + 0x00, 0x07, 0x00, 0x01, 0x80, 0x00, 0x00, 0x01, 0x80, 0x03, 0x00, 0x00, + 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, 0x00, 0x00, 0x60, 0x0E, 0x00, 0x00, + 0x00, 0x00, 0x70, 0x1C, 0x00, 0x00, 0x00, 0x00, 0x30, 0x18, 0x00, 0x00, + 0x00, 0x00, 0x18, 0x38, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x30, 0x00, 0x00, + 0x00, 0x00, 0x0C, 0x70, 0x0F, 0xC0, 0x03, 0xF0, 0x0C, 0x60, 0x3F, 0xF0, + 0x0F, 0xFC, 0x06, 0x60, 0x6F, 0xD8, 0x1B, 0xF6, 0x06, 0x60, 0x6F, 0xD8, + 0x1B, 0xF6, 0x06, 0xC0, 0xC7, 0x8C, 0x31, 0xE3, 0x03, 0xC0, 0xC0, 0x0C, + 0x30, 0x03, 0x03, 0xC0, 0xC0, 0x0C, 0x30, 0x03, 0x03, 0xC0, 0xC0, 0x0C, + 0x30, 0x03, 0x03, 0xC0, 0xE0, 0x1C, 0x38, 0x07, 0x03, 0xC0, 0x60, 0x18, + 0x18, 0x06, 0x03, 0xC0, 0x38, 0x70, 0x0E, 0x1C, 0x03, 0xC0, 0x1F, 0xE0, + 0x07, 0xF8, 0x03, 0xC0, 0x0F, 0xC0, 0x03, 0xF0, 0x03, 0xC0, 0x00, 0x00, + 0x00, 0x00, 0x03, 0x60, 0x00, 0x00, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, + 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x00, 0x00, 0x06, 0x70, 0x00, 0x00, + 0x00, 0x00, 0x0C, 0x30, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x38, 0x00, 0x00, + 0x00, 0x00, 0x1C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x1C, 0x00, 0x00, + 0x00, 0x00, 0x30, 0x0E, 0x00, 0x3F, 0xFC, 0x00, 0x70, 0x06, 0x00, 0x3F, + 0xFC, 0x00, 0x60, 0x03, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x80, 0x00, + 0x00, 0x01, 0x80, 0x01, 0xE0, 0x00, 0x00, 0x07, 0x00, 0x00, 0x70, 0x00, + 0x00, 0x0E, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x1F, 0x00, + 0x00, 0xF0, 0x00, 0x00, 0x07, 0xE0, 0x07, 0xE0, 0x00, 0x00, 0x01, 0xFF, + 0xFF, 0x80, 0x00, 0x00, 0x00, 0x1F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x01, + 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xF8, 0x00, 0x00, 0x00, + 0x00, 0x7E, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x01, 0xF0, 0x00, 0x0F, 0x80, + 0x00, 0x00, 0x03, 0xC0, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x07, 0x00, 0x00, + 0x00, 0xE0, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x38, + 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x0C, 0x00, + 0x00, 0x60, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x01, 0xC0, 0x00, 0x00, 0x78, 0x03, 0x80, 0x01, 0x80, 0x00, + 0x01, 0x8C, 0x01, 0x80, 0x03, 0x80, 0x00, 0x01, 0x06, 0x01, 0xC0, 0x03, + 0x00, 0x7C, 0x03, 0x00, 0x00, 0xC0, 0x03, 0x00, 0xC7, 0x03, 0x00, 0x00, + 0xC0, 0x06, 0x01, 0x83, 0x00, 0x00, 0x00, 0x60, 0x06, 0x01, 0x80, 0x00, + 0x00, 0x00, 0x60, 0x06, 0x01, 0x80, 0x00, 0x00, 0x60, 0x60, 0x0C, 0x00, + 0x00, 0x00, 0x00, 0x60, 0x70, 0x0C, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x30, + 0x0C, 0x00, 0x00, 0x00, 0x01, 0x80, 0x30, 0x0C, 0x00, 0x00, 0x00, 0x03, + 0x00, 0x30, 0x0C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x30, 0x0C, 0x02, 0x00, + 0x00, 0x1C, 0x00, 0x30, 0x0C, 0x03, 0xC0, 0x00, 0x70, 0x00, 0x30, 0x0C, + 0x00, 0xF8, 0x03, 0xC0, 0x00, 0x30, 0x0C, 0x00, 0x1F, 0xFF, 0x00, 0x00, + 0x30, 0x0D, 0xC0, 0x00, 0x00, 0x00, 0x03, 0xB0, 0x1E, 0x60, 0x00, 0x00, + 0x00, 0x04, 0xF8, 0x3E, 0x31, 0xC0, 0x00, 0x03, 0x88, 0x7C, 0x22, 0x1A, + 0x60, 0x00, 0x06, 0x58, 0xC4, 0x23, 0x0E, 0x30, 0x00, 0x0C, 0x70, 0xC4, + 0x31, 0x8C, 0x30, 0x00, 0x0C, 0x61, 0x8C, 0x70, 0x84, 0x30, 0x00, 0x0C, + 0x63, 0x0E, 0xC8, 0xC2, 0x30, 0x00, 0x0C, 0x42, 0x1B, 0xC4, 0x60, 0x18, + 0x00, 0x18, 0x04, 0x23, 0xE6, 0x20, 0x18, 0x00, 0x18, 0x04, 0x47, 0x63, + 0x00, 0x18, 0x00, 0x18, 0x00, 0x86, 0x71, 0x80, 0x0C, 0x00, 0x30, 0x01, + 0x0E, 0xCC, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x3B, 0xC6, 0x00, 0x0C, 0x00, + 0x30, 0x00, 0x63, 0xC2, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x43, 0x60, 0x00, + 0x0C, 0x00, 0x30, 0x00, 0x06, 0x30, 0x00, 0x18, 0x00, 0x18, 0x00, 0x0C, + 0x1C, 0x00, 0x18, 0x00, 0x18, 0x00, 0x38, 0x07, 0x00, 0x3F, 0xC3, 0xFC, + 0x00, 0xE0, 0x01, 0xC0, 0xE7, 0xFF, 0xE7, 0x03, 0x80, 0x00, 0x3F, 0x80, + 0x00, 0x01, 0xFC, 0x00, 0x00, 0x00, 0x1F, 0xF8, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x07, 0xE0, 0x07, 0xE0, 0x00, 0x00, 0x1F, + 0x00, 0x00, 0xF8, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x70, + 0x00, 0x00, 0x0E, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x07, 0x00, 0x01, 0x80, + 0x00, 0x00, 0x01, 0x80, 0x03, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x06, 0x00, + 0x00, 0x00, 0x00, 0x60, 0x0E, 0x03, 0x80, 0x00, 0x00, 0x70, 0x1C, 0x0F, + 0x80, 0x00, 0x00, 0x30, 0x18, 0x1C, 0x00, 0x00, 0x00, 0x18, 0x38, 0x38, + 0x00, 0x00, 0x00, 0x1C, 0x30, 0x70, 0x00, 0x00, 0x00, 0x0C, 0x70, 0x60, + 0x00, 0x00, 0x00, 0x0C, 0x60, 0x00, 0x00, 0x00, 0x00, 0x06, 0x60, 0x00, + 0x00, 0x00, 0x18, 0x06, 0x60, 0x03, 0xC0, 0x00, 0x78, 0x06, 0xC0, 0x03, + 0xC0, 0x00, 0xE0, 0x03, 0xC0, 0x07, 0xE0, 0x03, 0xC0, 0x03, 0xC0, 0x07, + 0xE0, 0x07, 0x00, 0x03, 0xC0, 0x07, 0xE0, 0x0F, 0xC0, 0x03, 0xC0, 0x03, + 0xC0, 0x07, 0xF8, 0x03, 0xC0, 0x03, 0x80, 0x00, 0x38, 0x03, 0xC0, 0x00, + 0x00, 0x00, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x03, 0xC0, 0x00, + 0x00, 0x00, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x03, 0x60, 0x00, + 0x00, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x00, 0x00, 0x06, 0x60, 0x03, + 0x00, 0x00, 0x40, 0x06, 0x70, 0x03, 0xC0, 0x01, 0xC0, 0x0C, 0x30, 0x01, + 0xF0, 0x0F, 0x80, 0x0C, 0x38, 0x00, 0x7F, 0xFE, 0x00, 0x1C, 0x18, 0x00, + 0x0F, 0xF0, 0x00, 0x18, 0x1C, 0x00, 0x00, 0x00, 0x00, 0x30, 0x0E, 0x00, + 0x00, 0x00, 0x00, 0x70, 0x06, 0x00, 0x00, 0x00, 0x00, 0x60, 0x03, 0x00, + 0x00, 0x00, 0x00, 0xC0, 0x03, 0x80, 0x00, 0x00, 0x01, 0x80, 0x01, 0xE0, + 0x00, 0x00, 0x07, 0x00, 0x00, 0x70, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x3C, + 0x00, 0x00, 0x3C, 0x00, 0x00, 0x1F, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x07, + 0xE0, 0x07, 0xE0, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, + 0x1F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x7F, 0xEF, 0xF8, 0x00, 0x00, 0x00, + 0xFB, 0xC0, 0x1F, 0x00, 0x00, 0x00, 0xF0, 0x70, 0x03, 0xC0, 0x00, 0x01, + 0xE0, 0x08, 0x00, 0x78, 0x00, 0x01, 0xC0, 0x00, 0x3F, 0x8E, 0x00, 0x01, + 0xC1, 0xE0, 0x1F, 0xF3, 0x80, 0x01, 0xC0, 0xF0, 0x00, 0x3C, 0xE0, 0x01, + 0xC0, 0xFC, 0x00, 0x00, 0x38, 0x01, 0xC0, 0x7E, 0x00, 0x00, 0x0E, 0x00, + 0xC0, 0x3F, 0x00, 0x78, 0x03, 0x00, 0xE0, 0x1F, 0x00, 0x3C, 0x01, 0xC0, + 0x60, 0x07, 0x80, 0x3F, 0x00, 0x60, 0x60, 0x00, 0x00, 0x1F, 0x80, 0x18, + 0x30, 0x00, 0x00, 0x0F, 0xC0, 0x0C, 0x38, 0x00, 0x00, 0x03, 0xC0, 0x03, + 0x18, 0x00, 0x00, 0x01, 0xE0, 0x01, 0x8C, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xCE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x76, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x1B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0x80, 0x00, 0x7F, 0x00, 0x00, + 0x06, 0xC0, 0x01, 0xFF, 0xE0, 0x00, 0x03, 0x60, 0x00, 0xE0, 0x3C, 0x00, + 0x01, 0xB0, 0x00, 0x00, 0x07, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x01, 0x80, + 0x00, 0x6C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x37, 0xC0, 0x00, 0x00, 0x00, + 0x00, 0x1E, 0x30, 0x01, 0xFC, 0x00, 0x00, 0x1B, 0x0C, 0x07, 0x03, 0x00, + 0x00, 0x0D, 0x86, 0x0C, 0x01, 0x80, 0x00, 0x06, 0x61, 0x98, 0x03, 0xC0, + 0x00, 0x03, 0x30, 0x70, 0x0F, 0xC0, 0x00, 0x03, 0x08, 0x00, 0x1F, 0x80, + 0x00, 0x01, 0x86, 0x00, 0x1E, 0x00, 0x00, 0x01, 0x83, 0x00, 0x3E, 0x00, + 0x00, 0x01, 0xC1, 0x80, 0x1B, 0x00, 0x00, 0x00, 0xC0, 0xC0, 0x08, 0x80, + 0x00, 0x00, 0xC0, 0x60, 0x00, 0x40, 0x00, 0x00, 0xE0, 0x30, 0x00, 0xF0, + 0x00, 0x00, 0xE0, 0x18, 0x00, 0xD8, 0x00, 0x00, 0xE0, 0x0C, 0x00, 0x0C, + 0x00, 0x00, 0xE0, 0x06, 0x00, 0x0C, 0x00, 0x01, 0xE0, 0x01, 0x00, 0x0F, + 0x00, 0x03, 0xC0, 0x00, 0xC0, 0x01, 0x80, 0x07, 0x80, 0x00, 0x30, 0x01, + 0xFE, 0xFF, 0x00, 0x00, 0x0E, 0x03, 0xBF, 0xFC, 0x00, 0x00, 0x01, 0xFE, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF7, 0xF0, 0x00, 0x00, 0x00, + 0x03, 0x9C, 0xC8, 0x00, 0x00, 0x00, 0x0E, 0x19, 0x8E, 0x00, 0x00, 0x00, + 0x78, 0x33, 0x9F, 0xC0, 0x00, 0x03, 0xC0, 0x67, 0x3D, 0xF0, 0x00, 0x0E, + 0x01, 0xCC, 0x6C, 0x3C, 0x00, 0x18, 0x07, 0x38, 0xC8, 0x0E, 0x00, 0x30, + 0x04, 0x63, 0x10, 0x03, 0x80, 0x60, 0x00, 0xC6, 0x60, 0x01, 0xC0, 0x60, + 0x00, 0x18, 0xC0, 0x00, 0xE0, 0xC0, 0x00, 0x13, 0x00, 0x00, 0x60, 0xC0, + 0x00, 0x0F, 0x00, 0x00, 0x30, 0xC0, 0x00, 0x1B, 0x00, 0x00, 0x38, 0xC0, + 0x3F, 0x63, 0x00, 0x00, 0x18, 0xC0, 0x60, 0x8E, 0x00, 0x00, 0x0C, 0xC0, + 0x00, 0x1C, 0x00, 0x00, 0x0C, 0x60, 0x00, 0x78, 0x00, 0x00, 0x06, 0x70, + 0x00, 0xE0, 0x01, 0xC0, 0x06, 0x38, 0x03, 0xE0, 0x03, 0xE0, 0x06, 0x1C, + 0x1F, 0xF0, 0x03, 0xF0, 0x07, 0x1F, 0xFB, 0xF0, 0x03, 0xF0, 0x03, 0x30, + 0x83, 0xF0, 0x03, 0xF0, 0x03, 0x30, 0x01, 0xE0, 0x03, 0xE0, 0x03, 0x30, + 0x01, 0xE0, 0x01, 0xC0, 0x03, 0x30, 0x00, 0x00, 0x00, 0x00, 0x03, 0x30, + 0x00, 0x00, 0x00, 0x00, 0x03, 0x30, 0x00, 0x00, 0x00, 0x00, 0x03, 0x30, + 0x00, 0x00, 0x00, 0x00, 0x03, 0x10, 0x00, 0x00, 0x00, 0x00, 0x03, 0x18, + 0x00, 0x00, 0x00, 0x00, 0x06, 0x18, 0x00, 0x00, 0x00, 0x00, 0x06, 0x18, + 0x00, 0x00, 0x00, 0x00, 0x06, 0x0C, 0x00, 0xE0, 0x01, 0x80, 0x0E, 0x0C, + 0x00, 0xFF, 0xFF, 0xC0, 0x0C, 0x0C, 0x00, 0x1F, 0xFE, 0x00, 0x1C, 0x06, + 0x00, 0x00, 0x00, 0x00, 0x18, 0x03, 0x00, 0x00, 0x00, 0x00, 0x38, 0x03, + 0x00, 0x00, 0x00, 0x00, 0x70, 0x01, 0x80, 0x00, 0x00, 0x00, 0x60, 0x00, + 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x60, 0x00, 0x00, 0x03, 0x80, 0x00, + 0x38, 0x00, 0x00, 0x07, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x1E, 0x00, 0x00, + 0x0F, 0x80, 0x00, 0x7C, 0x00, 0x00, 0x03, 0xF0, 0x03, 0xF0, 0x00, 0x00, + 0x00, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x0F, 0xFC, 0x00, 0x00, 0x00, + 0x3C, 0x00, 0x00, 0x00, 0x1F, 0xC0, 0x00, 0x00, 0x06, 0x38, 0x00, 0x00, + 0x03, 0x07, 0x00, 0x00, 0x00, 0xC0, 0xF0, 0x00, 0x00, 0x18, 0x0E, 0x00, + 0x00, 0x07, 0x01, 0xC0, 0x00, 0x03, 0xE0, 0x3C, 0x00, 0x01, 0xDC, 0x03, + 0x80, 0x00, 0x61, 0xC0, 0x70, 0x00, 0x18, 0x38, 0x06, 0x00, 0x06, 0x07, + 0x00, 0xC0, 0x00, 0xC0, 0xF0, 0x30, 0x00, 0x38, 0x0C, 0x06, 0x00, 0x07, + 0x01, 0x81, 0xC0, 0x00, 0xE0, 0x60, 0x30, 0x00, 0x1F, 0xEC, 0x06, 0x00, + 0x3F, 0x7F, 0x01, 0x80, 0x1C, 0x01, 0xF8, 0x30, 0x1E, 0x00, 0x0F, 0x0C, + 0x0E, 0x00, 0x00, 0xE3, 0x0E, 0x00, 0x00, 0x18, 0x63, 0x00, 0x7C, 0x03, + 0x19, 0x80, 0x7F, 0xC0, 0xC6, 0x60, 0x78, 0x38, 0x19, 0x98, 0x38, 0x06, + 0x06, 0x23, 0xFC, 0x00, 0xC0, 0xCC, 0x7C, 0x00, 0x30, 0x03, 0x3C, 0x00, + 0x0C, 0x00, 0xDF, 0x80, 0x03, 0x00, 0x3E, 0x30, 0x00, 0xC0, 0x0F, 0x0E, + 0x00, 0x30, 0x03, 0xC1, 0x80, 0x0C, 0x00, 0xB0, 0x30, 0x06, 0x00, 0x6C, + 0x0E, 0x03, 0x80, 0x1B, 0x01, 0xC1, 0xC0, 0x06, 0x60, 0x3F, 0xE0, 0x03, + 0x18, 0x03, 0xE0, 0x00, 0xC7, 0x00, 0x00, 0x00, 0x30, 0xC0, 0x00, 0x00, + 0x18, 0x18, 0x00, 0x00, 0x06, 0x07, 0x00, 0x00, 0x03, 0x00, 0xE0, 0x00, + 0x01, 0xC0, 0x1C, 0x00, 0x00, 0xE0, 0x03, 0xC0, 0x00, 0xF0, 0x00, 0x3E, + 0x01, 0xF0, 0x00, 0x03, 0xFF, 0xF0, 0x00, 0x00, 0x1F, 0xE0, 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xC0, 0xF0, 0x7F, + 0x83, 0xFC, 0x1F, 0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xC1, 0xFE, 0x0F, 0xF0, + 0x7F, 0x83, 0xFC, 0x1F, 0xE0, 0xFF, 0x07, 0x80, 0x00, 0x07, 0x81, 0xE0, + 0x00, 0x07, 0x81, 0xE0, 0x00, 0x07, 0x01, 0xE0, 0x00, 0x0F, 0x01, 0xC0, + 0x00, 0x0F, 0x03, 0xC0, 0x00, 0x0F, 0x03, 0xC0, 0x00, 0x0E, 0x03, 0xC0, + 0x00, 0x1E, 0x03, 0x80, 0x00, 0x1E, 0x03, 0x80, 0x00, 0x1E, 0x07, 0x80, + 0x1F, 0xFF, 0xFF, 0xFF, 0x1F, 0xFF, 0xFF, 0xFF, 0x1F, 0xFF, 0xFF, 0xFF, + 0x1F, 0xFF, 0xFF, 0xFF, 0x00, 0x3C, 0x0F, 0x00, 0x00, 0x38, 0x0F, 0x00, + 0x00, 0x78, 0x0E, 0x00, 0x00, 0x78, 0x1E, 0x00, 0x00, 0x70, 0x1E, 0x00, + 0x00, 0xF0, 0x1E, 0x00, 0x00, 0xF0, 0x1C, 0x00, 0xFF, 0xFF, 0xFF, 0xF8, + 0xFF, 0xFF, 0xFF, 0xF8, 0xFF, 0xFF, 0xFF, 0xF8, 0xFF, 0xFF, 0xFF, 0xF8, + 0x01, 0xE0, 0x78, 0x00, 0x01, 0xC0, 0x78, 0x00, 0x01, 0xC0, 0x78, 0x00, + 0x03, 0xC0, 0x70, 0x00, 0x03, 0xC0, 0xF0, 0x00, 0x03, 0xC0, 0xF0, 0x00, + 0x03, 0x80, 0xF0, 0x00, 0x07, 0x80, 0xE0, 0x00, 0x07, 0x80, 0xE0, 0x00, + 0x07, 0x81, 0xE0, 0x00, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x03, 0x00, + 0x00, 0x0C, 0x00, 0x00, 0x30, 0x00, 0x07, 0xFF, 0x00, 0xFF, 0xFF, 0x07, + 0xFF, 0xFC, 0x3F, 0xFF, 0xF1, 0xF8, 0xC1, 0xCF, 0x83, 0x00, 0x3C, 0x0C, + 0x00, 0xF0, 0x30, 0x03, 0xC0, 0xC0, 0x0F, 0x03, 0x00, 0x3E, 0x0C, 0x00, + 0x7E, 0x30, 0x01, 0xFF, 0xC0, 0x03, 0xFF, 0xE0, 0x03, 0xFF, 0xF0, 0x03, + 0xFF, 0xE0, 0x00, 0xFF, 0xC0, 0x03, 0x1F, 0x80, 0x0C, 0x1F, 0x00, 0x30, + 0x7C, 0x00, 0xC0, 0xF0, 0x03, 0x03, 0xC0, 0x0C, 0x0F, 0x00, 0x30, 0x7E, + 0x00, 0xC3, 0xEF, 0x83, 0x3F, 0xBF, 0xFF, 0xFC, 0xFF, 0xFF, 0xE1, 0xFF, + 0xFF, 0x00, 0x7F, 0xE0, 0x00, 0x0C, 0x00, 0x00, 0x30, 0x00, 0x00, 0xC0, + 0x00, 0x03, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, + 0x07, 0xE0, 0x00, 0x0E, 0x00, 0x3F, 0xF0, 0x00, 0x3C, 0x00, 0xFF, 0xF0, + 0x00, 0x70, 0x03, 0xE1, 0xE0, 0x01, 0xE0, 0x07, 0x81, 0xE0, 0x03, 0x80, + 0x0F, 0x03, 0xC0, 0x0F, 0x00, 0x3C, 0x03, 0xC0, 0x3C, 0x00, 0x78, 0x07, + 0x80, 0x70, 0x00, 0xF0, 0x0F, 0x01, 0xE0, 0x01, 0xE0, 0x1E, 0x03, 0x80, + 0x03, 0xC0, 0x3C, 0x0F, 0x00, 0x07, 0x80, 0x78, 0x1C, 0x00, 0x0F, 0x00, + 0xF0, 0x70, 0x00, 0x0F, 0x03, 0xC1, 0xE0, 0x00, 0x1E, 0x07, 0x83, 0x80, + 0x00, 0x3E, 0x1F, 0x0F, 0x00, 0x00, 0x3F, 0xFC, 0x1C, 0x00, 0x00, 0x3F, + 0xF0, 0x78, 0x1F, 0x80, 0x1F, 0x81, 0xE0, 0xFF, 0xC0, 0x00, 0x03, 0x83, + 0xFF, 0xC0, 0x00, 0x0F, 0x0F, 0x87, 0xC0, 0x00, 0x1C, 0x1E, 0x07, 0x80, + 0x00, 0x78, 0x3C, 0x0F, 0x00, 0x00, 0xE0, 0xF0, 0x0F, 0x00, 0x03, 0x81, + 0xE0, 0x1E, 0x00, 0x0F, 0x03, 0xC0, 0x3C, 0x00, 0x1C, 0x07, 0x80, 0x78, + 0x00, 0x78, 0x0F, 0x00, 0xF0, 0x00, 0xE0, 0x1E, 0x01, 0xE0, 0x03, 0xC0, + 0x3C, 0x03, 0xC0, 0x07, 0x00, 0x3C, 0x0F, 0x00, 0x1C, 0x00, 0x78, 0x1E, + 0x00, 0x78, 0x00, 0xF8, 0x78, 0x00, 0xE0, 0x00, 0xFF, 0xF0, 0x03, 0xC0, + 0x00, 0xFF, 0xC0, 0x07, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x3F, 0x80, 0x00, + 0x00, 0xFF, 0xF0, 0x00, 0x03, 0xFF, 0xF8, 0x00, 0x07, 0xFF, 0xF8, 0x00, + 0x07, 0xE0, 0x78, 0x00, 0x0F, 0x80, 0x08, 0x00, 0x0F, 0x80, 0x00, 0x00, + 0x0F, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, + 0x0F, 0x00, 0x00, 0x00, 0x0F, 0x80, 0x00, 0x00, 0x07, 0xC0, 0x00, 0x00, + 0x07, 0xE0, 0x00, 0x00, 0x03, 0xF0, 0x00, 0x00, 0x07, 0xF8, 0x00, 0x00, + 0x1F, 0xFC, 0x00, 0x00, 0x1E, 0x7E, 0x00, 0x3C, 0x3E, 0x3F, 0x00, 0x3C, + 0x7C, 0x1F, 0x80, 0x7C, 0x78, 0x0F, 0xC0, 0x78, 0xF8, 0x07, 0xE0, 0x78, + 0xF0, 0x03, 0xF0, 0x78, 0xF0, 0x01, 0xF8, 0xF0, 0xF0, 0x00, 0xFC, 0xF0, + 0xF0, 0x00, 0x7E, 0xE0, 0xF0, 0x00, 0x3F, 0xE0, 0xF8, 0x00, 0x1F, 0xC0, + 0x78, 0x00, 0x0F, 0xC0, 0x7C, 0x00, 0x0F, 0xC0, 0x3E, 0x00, 0x3F, 0xE0, + 0x3F, 0xC0, 0xFF, 0xF0, 0x1F, 0xFF, 0xFC, 0xF8, 0x0F, 0xFF, 0xF8, 0x7E, + 0x03, 0xFF, 0xE0, 0x3F, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xF0, 0x03, 0xE0, 0x78, 0x1E, 0x03, 0xC0, 0xF0, 0x1E, 0x07, + 0x80, 0xF0, 0x3C, 0x07, 0x80, 0xF0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x07, + 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, + 0xF0, 0x1E, 0x03, 0xC0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x01, 0xE0, 0x3C, + 0x07, 0x80, 0x78, 0x0F, 0x00, 0xF0, 0x1E, 0x01, 0xE0, 0x3C, 0x03, 0xC0, + 0x7C, 0xF8, 0x0F, 0x00, 0xF0, 0x1E, 0x01, 0xE0, 0x3C, 0x03, 0xC0, 0x78, + 0x07, 0x80, 0xF0, 0x1E, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x0F, 0x01, + 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, + 0x3C, 0x07, 0x81, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x3C, 0x07, 0x80, 0xF0, + 0x3C, 0x07, 0x81, 0xE0, 0x3C, 0x0F, 0x01, 0xE0, 0x78, 0x1F, 0x00, 0x00, + 0x70, 0x00, 0x03, 0x80, 0x00, 0x1C, 0x00, 0x00, 0xE0, 0x04, 0x07, 0x01, + 0x78, 0x38, 0x3F, 0xE1, 0xC3, 0xE7, 0xCE, 0x7C, 0x0F, 0x77, 0x80, 0x1F, + 0xF0, 0x00, 0x7F, 0x00, 0x03, 0xF8, 0x00, 0x3F, 0xE0, 0x07, 0xBB, 0xC0, + 0xF9, 0xCF, 0x9F, 0x0E, 0x1F, 0xF0, 0x70, 0x7A, 0x03, 0x80, 0x80, 0x1C, + 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0x38, 0x00, 0x00, 0x07, 0x80, + 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x01, 0xE0, 0x00, + 0x00, 0x07, 0x80, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, + 0x01, 0xE0, 0x00, 0x00, 0x07, 0x80, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, + 0x78, 0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x07, 0x80, 0x03, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFC, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x01, 0xE0, 0x00, + 0x00, 0x07, 0x80, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, + 0x01, 0xE0, 0x00, 0x00, 0x07, 0x80, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, + 0x78, 0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x07, 0x80, 0x00, 0x00, 0x1E, + 0x00, 0x00, 0x3C, 0xF3, 0xCF, 0x3C, 0xE7, 0x9C, 0x73, 0xCE, 0x00, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x0F, 0x00, 0x1F, + 0x00, 0x1E, 0x00, 0x1E, 0x00, 0x3E, 0x00, 0x3C, 0x00, 0x3C, 0x00, 0x3C, + 0x00, 0x78, 0x00, 0x78, 0x00, 0x78, 0x00, 0xF8, 0x00, 0xF0, 0x00, 0xF0, + 0x01, 0xF0, 0x01, 0xE0, 0x01, 0xE0, 0x01, 0xE0, 0x03, 0xC0, 0x03, 0xC0, + 0x03, 0xC0, 0x07, 0x80, 0x07, 0x80, 0x07, 0x80, 0x0F, 0x80, 0x0F, 0x00, + 0x0F, 0x00, 0x1F, 0x00, 0x1E, 0x00, 0x1E, 0x00, 0x1E, 0x00, 0x3C, 0x00, + 0x3C, 0x00, 0x3C, 0x00, 0x7C, 0x00, 0x78, 0x00, 0x78, 0x00, 0xF8, 0x00, + 0xF0, 0x00, 0x00, 0x7F, 0x00, 0x03, 0xFF, 0xC0, 0x07, 0xFF, 0xE0, 0x0F, + 0xFF, 0xF0, 0x1F, 0x81, 0xF8, 0x1F, 0x00, 0xF8, 0x3E, 0x00, 0x7C, 0x3C, + 0x00, 0x3C, 0x7C, 0x00, 0x3E, 0x78, 0x00, 0x1E, 0x78, 0x00, 0x1E, 0xF8, + 0x00, 0x1E, 0xF0, 0x00, 0x0F, 0xF0, 0x00, 0x0F, 0xF0, 0x00, 0x0F, 0xF0, + 0x00, 0x0F, 0xF0, 0x00, 0x0F, 0xF0, 0x00, 0x0F, 0xF0, 0x00, 0x0F, 0xF0, + 0x00, 0x0F, 0xF0, 0x00, 0x0F, 0xF0, 0x00, 0x0F, 0xF0, 0x00, 0x0F, 0xF0, + 0x00, 0x0F, 0xF8, 0x00, 0x1E, 0x78, 0x00, 0x1E, 0x78, 0x00, 0x1E, 0x7C, + 0x00, 0x3E, 0x3C, 0x00, 0x3C, 0x3E, 0x00, 0x7C, 0x1F, 0x00, 0xF8, 0x1F, + 0x81, 0xF8, 0x0F, 0xFF, 0xF0, 0x07, 0xFF, 0xE0, 0x03, 0xFF, 0xC0, 0x00, + 0xFE, 0x00, 0x03, 0xF0, 0x03, 0xFF, 0x00, 0xFF, 0xF0, 0x0F, 0xFF, 0x00, + 0xFC, 0xF0, 0x0C, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, + 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, + 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, + 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, + 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, + 0x00, 0xF0, 0x00, 0x0F, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x07, 0xFC, 0x00, 0xFF, 0xFE, 0x0F, 0xFF, 0xFE, 0x3F, + 0xFF, 0xFC, 0xFE, 0x03, 0xF3, 0x80, 0x03, 0xE8, 0x00, 0x07, 0x80, 0x00, + 0x1F, 0x00, 0x00, 0x3C, 0x00, 0x00, 0xF0, 0x00, 0x03, 0xC0, 0x00, 0x0F, + 0x00, 0x00, 0x3C, 0x00, 0x01, 0xE0, 0x00, 0x0F, 0x80, 0x00, 0x3C, 0x00, + 0x01, 0xF0, 0x00, 0x0F, 0x80, 0x00, 0x7C, 0x00, 0x03, 0xE0, 0x00, 0x1F, + 0x00, 0x00, 0xF8, 0x00, 0x07, 0xC0, 0x00, 0x3E, 0x00, 0x01, 0xF0, 0x00, + 0x0F, 0x80, 0x00, 0x7C, 0x00, 0x03, 0xE0, 0x00, 0x1F, 0x00, 0x00, 0xF8, + 0x00, 0x07, 0xC0, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xC0, 0x0F, 0xFE, 0x00, 0xFF, 0xFF, 0x01, 0xFF, 0xFF, + 0x83, 0xFF, 0xFF, 0x87, 0x00, 0x3F, 0x80, 0x00, 0x1F, 0x00, 0x00, 0x1F, + 0x00, 0x00, 0x1E, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x78, 0x00, 0x00, 0xF0, + 0x00, 0x01, 0xE0, 0x00, 0x07, 0x80, 0x00, 0x1F, 0x00, 0x00, 0xFC, 0x01, + 0xFF, 0xF0, 0x03, 0xFF, 0x80, 0x07, 0xFF, 0x80, 0x0F, 0xFF, 0xC0, 0x00, + 0x1F, 0xC0, 0x00, 0x0F, 0xC0, 0x00, 0x07, 0x80, 0x00, 0x0F, 0x80, 0x00, + 0x0F, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x78, 0x00, 0x00, + 0xF0, 0x00, 0x03, 0xE0, 0x00, 0x0F, 0xA0, 0x00, 0x3F, 0x7C, 0x01, 0xFC, + 0xFF, 0xFF, 0xF1, 0xFF, 0xFF, 0xC1, 0xFF, 0xFE, 0x00, 0x3F, 0xE0, 0x00, + 0x00, 0x03, 0xF0, 0x00, 0x03, 0xF8, 0x00, 0x01, 0xFC, 0x00, 0x01, 0xFE, + 0x00, 0x01, 0xEF, 0x00, 0x00, 0xE7, 0x80, 0x00, 0xF3, 0xC0, 0x00, 0xF1, + 0xE0, 0x00, 0x78, 0xF0, 0x00, 0x78, 0x78, 0x00, 0x78, 0x3C, 0x00, 0x3C, + 0x1E, 0x00, 0x3C, 0x0F, 0x00, 0x3C, 0x07, 0x80, 0x1E, 0x03, 0xC0, 0x1E, + 0x01, 0xE0, 0x1E, 0x00, 0xF0, 0x0F, 0x00, 0x78, 0x0F, 0x00, 0x3C, 0x0F, + 0x80, 0x1E, 0x07, 0x80, 0x0F, 0x07, 0x80, 0x07, 0x83, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x3C, + 0x00, 0x00, 0x1E, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x07, 0x80, 0x00, 0x03, + 0xC0, 0x00, 0x01, 0xE0, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x78, 0x00, 0x7F, + 0xFF, 0xE1, 0xFF, 0xFF, 0x87, 0xFF, 0xFE, 0x1F, 0xFF, 0xF8, 0x78, 0x00, + 0x01, 0xE0, 0x00, 0x07, 0x80, 0x00, 0x1E, 0x00, 0x00, 0x78, 0x00, 0x01, + 0xE0, 0x00, 0x07, 0x80, 0x00, 0x1E, 0xFE, 0x00, 0x7F, 0xFF, 0x01, 0xFF, + 0xFF, 0x07, 0xFF, 0xFE, 0x1E, 0x03, 0xFC, 0x40, 0x01, 0xF0, 0x00, 0x03, + 0xE0, 0x00, 0x07, 0x80, 0x00, 0x1F, 0x00, 0x00, 0x3C, 0x00, 0x00, 0xF0, + 0x00, 0x03, 0xC0, 0x00, 0x0F, 0x00, 0x00, 0x3C, 0x00, 0x00, 0xF0, 0x00, + 0x07, 0xC0, 0x00, 0x1E, 0x00, 0x00, 0xFA, 0x00, 0x07, 0xCF, 0x00, 0x7F, + 0x3F, 0xFF, 0xF8, 0xFF, 0xFF, 0xC3, 0xFF, 0xFC, 0x01, 0xFF, 0xC0, 0x00, + 0x00, 0x1F, 0xF0, 0x00, 0xFF, 0xFC, 0x03, 0xFF, 0xFC, 0x07, 0xFF, 0xFC, + 0x0F, 0xE0, 0x0C, 0x1F, 0x80, 0x00, 0x1E, 0x00, 0x00, 0x3E, 0x00, 0x00, + 0x3C, 0x00, 0x00, 0x78, 0x00, 0x00, 0x78, 0x00, 0x00, 0x78, 0x00, 0x00, + 0xF8, 0x7F, 0x00, 0xF1, 0xFF, 0xE0, 0xF3, 0xFF, 0xF0, 0xF7, 0xFF, 0xF8, + 0xFF, 0xC1, 0xFC, 0xFF, 0x00, 0x7E, 0xFE, 0x00, 0x3E, 0xFC, 0x00, 0x1E, + 0xFC, 0x00, 0x1F, 0xF8, 0x00, 0x0F, 0xF8, 0x00, 0x0F, 0xF8, 0x00, 0x0F, + 0x78, 0x00, 0x0F, 0x78, 0x00, 0x0F, 0x78, 0x00, 0x0F, 0x7C, 0x00, 0x1F, + 0x3C, 0x00, 0x1E, 0x3E, 0x00, 0x3E, 0x1F, 0x00, 0x7C, 0x1F, 0xC1, 0xFC, + 0x0F, 0xFF, 0xF8, 0x07, 0xFF, 0xF0, 0x03, 0xFF, 0xE0, 0x00, 0x7F, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, + 0x00, 0x78, 0x00, 0x03, 0xE0, 0x00, 0x0F, 0x00, 0x00, 0x7C, 0x00, 0x01, + 0xE0, 0x00, 0x07, 0x80, 0x00, 0x3E, 0x00, 0x00, 0xF0, 0x00, 0x03, 0xC0, + 0x00, 0x1E, 0x00, 0x00, 0x78, 0x00, 0x03, 0xE0, 0x00, 0x0F, 0x00, 0x00, + 0x3C, 0x00, 0x01, 0xE0, 0x00, 0x07, 0x80, 0x00, 0x3E, 0x00, 0x00, 0xF0, + 0x00, 0x03, 0xC0, 0x00, 0x1E, 0x00, 0x00, 0x78, 0x00, 0x03, 0xE0, 0x00, + 0x0F, 0x00, 0x00, 0x3C, 0x00, 0x01, 0xE0, 0x00, 0x07, 0x80, 0x00, 0x3E, + 0x00, 0x00, 0xF0, 0x00, 0x03, 0xC0, 0x00, 0x1E, 0x00, 0x00, 0x00, 0xFF, + 0x00, 0x07, 0xFF, 0xE0, 0x0F, 0xFF, 0xF0, 0x1F, 0xFF, 0xF8, 0x3F, 0x81, + 0xFC, 0x3E, 0x00, 0x7C, 0x7C, 0x00, 0x3E, 0x78, 0x00, 0x1E, 0x78, 0x00, + 0x1E, 0x78, 0x00, 0x1E, 0x78, 0x00, 0x1E, 0x78, 0x00, 0x1E, 0x3C, 0x00, + 0x3C, 0x3E, 0x00, 0x7C, 0x1F, 0x81, 0xF8, 0x0F, 0xFF, 0xF0, 0x03, 0xFF, + 0xC0, 0x07, 0xFF, 0xE0, 0x1F, 0xFF, 0xF8, 0x3F, 0x81, 0xFC, 0x7C, 0x00, + 0x3E, 0x78, 0x00, 0x1E, 0xF8, 0x00, 0x1F, 0xF0, 0x00, 0x0F, 0xF0, 0x00, + 0x0F, 0xF0, 0x00, 0x0F, 0xF0, 0x00, 0x0F, 0xF0, 0x00, 0x0F, 0xF8, 0x00, + 0x1F, 0x78, 0x00, 0x1E, 0x7C, 0x00, 0x3E, 0x3F, 0x00, 0xFC, 0x3F, 0xFF, + 0xFC, 0x1F, 0xFF, 0xF8, 0x07, 0xFF, 0xE0, 0x00, 0xFF, 0x00, 0x00, 0xFE, + 0x00, 0x07, 0xFF, 0xC0, 0x0F, 0xFF, 0xE0, 0x1F, 0xFF, 0xF0, 0x3F, 0x83, + 0xF8, 0x7E, 0x00, 0xF8, 0x7C, 0x00, 0x7C, 0x78, 0x00, 0x3C, 0xF8, 0x00, + 0x3E, 0xF0, 0x00, 0x1E, 0xF0, 0x00, 0x1E, 0xF0, 0x00, 0x1E, 0xF0, 0x00, + 0x1F, 0xF0, 0x00, 0x1F, 0xF0, 0x00, 0x1F, 0xF8, 0x00, 0x3F, 0x78, 0x00, + 0x3F, 0x7C, 0x00, 0x7F, 0x7E, 0x00, 0xFF, 0x3F, 0x83, 0xFF, 0x1F, 0xFF, + 0xEF, 0x0F, 0xFF, 0xCF, 0x07, 0xFF, 0x8F, 0x00, 0xFE, 0x1E, 0x00, 0x00, + 0x1E, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x3C, 0x00, 0x00, + 0x7C, 0x00, 0x00, 0xF8, 0x00, 0x01, 0xF8, 0x30, 0x07, 0xF0, 0x3F, 0xFF, + 0xE0, 0x3F, 0xFF, 0xC0, 0x3F, 0xFF, 0x00, 0x0F, 0xF8, 0x00, 0xFF, 0xFF, + 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFF, + 0xFF, 0x3C, 0xF3, 0xCF, 0x3C, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x03, 0xCF, 0x3C, 0xF3, 0xCE, 0x79, 0xC7, 0x3C, 0xE0, 0x00, + 0x00, 0x00, 0x08, 0x00, 0x00, 0x03, 0xC0, 0x00, 0x00, 0xFE, 0x00, 0x00, + 0x3F, 0xF0, 0x00, 0x07, 0xFF, 0x00, 0x01, 0xFF, 0xE0, 0x00, 0x7F, 0xF8, + 0x00, 0x1F, 0xFE, 0x00, 0x03, 0xFF, 0x80, 0x00, 0xFF, 0xF0, 0x00, 0x3F, + 0xFC, 0x00, 0x01, 0xFF, 0x00, 0x00, 0x0F, 0xE0, 0x00, 0x00, 0x7F, 0xC0, + 0x00, 0x03, 0xFF, 0xC0, 0x00, 0x03, 0xFF, 0xC0, 0x00, 0x03, 0xFF, 0x80, + 0x00, 0x07, 0xFF, 0x80, 0x00, 0x07, 0xFF, 0x80, 0x00, 0x07, 0xFF, 0x80, + 0x00, 0x07, 0xFF, 0x00, 0x00, 0x0F, 0xFC, 0x00, 0x00, 0x0F, 0xE0, 0x00, + 0x00, 0x0F, 0x00, 0x00, 0x00, 0x08, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x80, 0x00, 0x00, + 0x07, 0x80, 0x00, 0x00, 0x3F, 0x80, 0x00, 0x01, 0xFF, 0x80, 0x00, 0x07, + 0xFF, 0x00, 0x00, 0x0F, 0xFF, 0x00, 0x00, 0x0F, 0xFF, 0x00, 0x00, 0x0F, + 0xFF, 0x00, 0x00, 0x0F, 0xFE, 0x00, 0x00, 0x1F, 0xFE, 0x00, 0x00, 0x1F, + 0xFE, 0x00, 0x00, 0x1F, 0xF0, 0x00, 0x00, 0x1F, 0x80, 0x00, 0x07, 0xFC, + 0x00, 0x01, 0xFF, 0xE0, 0x00, 0x7F, 0xF8, 0x00, 0x0F, 0xFE, 0x00, 0x03, + 0xFF, 0xC0, 0x00, 0xFF, 0xF0, 0x00, 0x3F, 0xFC, 0x00, 0x07, 0xFF, 0x00, + 0x00, 0x7F, 0xE0, 0x00, 0x03, 0xF8, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, + 0x80, 0x00, 0x00, 0x00, 0x07, 0xF0, 0x0F, 0xFF, 0x07, 0xFF, 0xF3, 0xFF, + 0xFE, 0xF8, 0x1F, 0xB8, 0x01, 0xF8, 0x00, 0x7C, 0x00, 0x0F, 0x00, 0x03, + 0xC0, 0x00, 0xF0, 0x00, 0x3C, 0x00, 0x1E, 0x00, 0x0F, 0x80, 0x07, 0xC0, + 0x03, 0xE0, 0x01, 0xF8, 0x00, 0xFC, 0x00, 0x7E, 0x00, 0x1F, 0x00, 0x0F, + 0x80, 0x03, 0xC0, 0x00, 0xF0, 0x00, 0x3C, 0x00, 0x0F, 0x00, 0x03, 0xC0, + 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x00, + 0x3E, 0x00, 0x0F, 0x80, 0x03, 0xE0, 0x00, 0xF8, 0x00, 0x3E, 0x00, 0x00, + 0x00, 0x7F, 0xC0, 0x00, 0x00, 0x03, 0xFF, 0xFC, 0x00, 0x00, 0x07, 0xFF, + 0xFF, 0xC0, 0x00, 0x0F, 0xFF, 0xFF, 0xF0, 0x00, 0x0F, 0xF8, 0x03, 0xFE, + 0x00, 0x0F, 0xE0, 0x00, 0x3F, 0x80, 0x0F, 0xC0, 0x00, 0x07, 0xE0, 0x0F, + 0x80, 0x00, 0x01, 0xF8, 0x0F, 0x80, 0x00, 0x00, 0x7C, 0x0F, 0x80, 0x00, + 0x00, 0x1F, 0x07, 0x80, 0x00, 0x00, 0x07, 0x87, 0xC0, 0x1F, 0x87, 0x81, + 0xE3, 0xC0, 0x1F, 0xF3, 0xC0, 0xF3, 0xE0, 0x3F, 0xFD, 0xE0, 0x7D, 0xE0, + 0x1F, 0xFF, 0xF0, 0x1E, 0xF0, 0x1F, 0x83, 0xF8, 0x0F, 0xF0, 0x0F, 0x00, + 0x7C, 0x07, 0xF8, 0x0F, 0x80, 0x3E, 0x03, 0xFC, 0x07, 0x80, 0x0F, 0x01, + 0xFE, 0x03, 0xC0, 0x07, 0x80, 0xFF, 0x01, 0xE0, 0x03, 0xC0, 0x7F, 0x80, + 0xF0, 0x01, 0xE0, 0x7B, 0xC0, 0x78, 0x00, 0xF0, 0x3D, 0xE0, 0x3E, 0x00, + 0xF8, 0x3E, 0xF0, 0x0F, 0x00, 0x7C, 0x3E, 0x3C, 0x07, 0xE0, 0xFE, 0x7E, + 0x1E, 0x01, 0xFF, 0xFF, 0xFE, 0x0F, 0x00, 0xFF, 0xF7, 0xFE, 0x07, 0xC0, + 0x3F, 0xF3, 0xFC, 0x01, 0xF0, 0x07, 0xE1, 0xF0, 0x00, 0xF8, 0x00, 0x00, + 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x80, 0x00, 0x00, 0x00, + 0x07, 0xE0, 0x00, 0x00, 0x80, 0x01, 0xFC, 0x00, 0x01, 0xE0, 0x00, 0x7F, + 0x80, 0x01, 0xF0, 0x00, 0x1F, 0xF8, 0x07, 0xF8, 0x00, 0x03, 0xFF, 0xFF, + 0xF8, 0x00, 0x00, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x0F, 0xFF, 0xE0, 0x00, + 0x00, 0x00, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x07, 0xC0, 0x00, 0x00, 0x1F, + 0xC0, 0x00, 0x00, 0x3F, 0x80, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x01, 0xFF, + 0x00, 0x00, 0x03, 0xDE, 0x00, 0x00, 0x0F, 0xBE, 0x00, 0x00, 0x1E, 0x3C, + 0x00, 0x00, 0x3C, 0x78, 0x00, 0x00, 0xF8, 0xF8, 0x00, 0x01, 0xE0, 0xF0, + 0x00, 0x03, 0xC1, 0xE0, 0x00, 0x0F, 0x01, 0xE0, 0x00, 0x1E, 0x03, 0xC0, + 0x00, 0x7C, 0x07, 0xC0, 0x00, 0xF0, 0x07, 0x80, 0x01, 0xE0, 0x0F, 0x00, + 0x07, 0xC0, 0x1F, 0x00, 0x0F, 0x00, 0x1E, 0x00, 0x3E, 0x00, 0x3E, 0x00, + 0x78, 0x00, 0x3C, 0x00, 0xFF, 0xFF, 0xF8, 0x03, 0xFF, 0xFF, 0xF8, 0x07, + 0xFF, 0xFF, 0xF0, 0x0F, 0xFF, 0xFF, 0xE0, 0x3E, 0x00, 0x03, 0xE0, 0x78, + 0x00, 0x03, 0xC1, 0xF0, 0x00, 0x07, 0xC3, 0xC0, 0x00, 0x07, 0x87, 0x80, + 0x00, 0x0F, 0x1F, 0x00, 0x00, 0x1F, 0x3C, 0x00, 0x00, 0x1E, 0x78, 0x00, + 0x00, 0x3D, 0xE0, 0x00, 0x00, 0x3C, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xE0, + 0xFF, 0xFF, 0xF8, 0xFF, 0xFF, 0xF8, 0xF0, 0x01, 0xFC, 0xF0, 0x00, 0x7E, + 0xF0, 0x00, 0x3E, 0xF0, 0x00, 0x1E, 0xF0, 0x00, 0x1E, 0xF0, 0x00, 0x1E, + 0xF0, 0x00, 0x1E, 0xF0, 0x00, 0x3E, 0xF0, 0x00, 0x7C, 0xF0, 0x01, 0xFC, + 0xFF, 0xFF, 0xF8, 0xFF, 0xFF, 0xE0, 0xFF, 0xFF, 0xF0, 0xFF, 0xFF, 0xF8, + 0xF0, 0x00, 0xFC, 0xF0, 0x00, 0x3E, 0xF0, 0x00, 0x1E, 0xF0, 0x00, 0x0F, + 0xF0, 0x00, 0x0F, 0xF0, 0x00, 0x0F, 0xF0, 0x00, 0x0F, 0xF0, 0x00, 0x0F, + 0xF0, 0x00, 0x0F, 0xF0, 0x00, 0x1F, 0xF0, 0x00, 0x3E, 0xF0, 0x00, 0xFE, + 0xFF, 0xFF, 0xFC, 0xFF, 0xFF, 0xF8, 0xFF, 0xFF, 0xF0, 0xFF, 0xFF, 0x80, + 0x00, 0x0F, 0xFC, 0x00, 0x07, 0xFF, 0xF8, 0x01, 0xFF, 0xFF, 0xE0, 0x3F, + 0xFF, 0xFF, 0x07, 0xF0, 0x07, 0xF0, 0xFC, 0x00, 0x0F, 0x1F, 0x00, 0x00, + 0x31, 0xE0, 0x00, 0x01, 0x3E, 0x00, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x78, + 0x00, 0x00, 0x07, 0x80, 0x00, 0x00, 0x78, 0x00, 0x00, 0x0F, 0x00, 0x00, + 0x00, 0xF0, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x0F, + 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x00, + 0x00, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x07, 0x80, 0x00, 0x00, + 0x78, 0x00, 0x00, 0x07, 0x80, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x03, 0xE0, + 0x00, 0x00, 0x1E, 0x00, 0x00, 0x11, 0xF0, 0x00, 0x03, 0x0F, 0xC0, 0x00, + 0xF0, 0x7F, 0x80, 0x7F, 0x03, 0xFF, 0xFF, 0xF0, 0x1F, 0xFF, 0xFE, 0x00, + 0x7F, 0xFF, 0x80, 0x00, 0xFF, 0xC0, 0xFF, 0xFF, 0x00, 0x07, 0xFF, 0xFF, + 0x80, 0x3F, 0xFF, 0xFF, 0x01, 0xFF, 0xFF, 0xFC, 0x0F, 0x00, 0x1F, 0xF8, + 0x78, 0x00, 0x0F, 0xE3, 0xC0, 0x00, 0x1F, 0x1E, 0x00, 0x00, 0x7C, 0xF0, + 0x00, 0x01, 0xE7, 0x80, 0x00, 0x0F, 0xBC, 0x00, 0x00, 0x3D, 0xE0, 0x00, + 0x01, 0xEF, 0x00, 0x00, 0x0F, 0xF8, 0x00, 0x00, 0x3F, 0xC0, 0x00, 0x01, + 0xFE, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x7F, 0x80, 0x00, 0x03, 0xFC, + 0x00, 0x00, 0x1F, 0xE0, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x07, 0xF8, 0x00, + 0x00, 0x7F, 0xC0, 0x00, 0x03, 0xDE, 0x00, 0x00, 0x1E, 0xF0, 0x00, 0x01, + 0xF7, 0x80, 0x00, 0x0F, 0x3C, 0x00, 0x00, 0xF9, 0xE0, 0x00, 0x0F, 0x8F, + 0x00, 0x01, 0xFC, 0x78, 0x00, 0x7F, 0xC3, 0xFF, 0xFF, 0xF8, 0x1F, 0xFF, + 0xFF, 0x80, 0xFF, 0xFF, 0xF0, 0x07, 0xFF, 0xF8, 0x00, 0x00, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x03, + 0xC0, 0x00, 0x0F, 0x00, 0x00, 0x3C, 0x00, 0x00, 0xF0, 0x00, 0x03, 0xC0, + 0x00, 0x0F, 0x00, 0x00, 0x3C, 0x00, 0x00, 0xF0, 0x00, 0x03, 0xC0, 0x00, + 0x0F, 0xFF, 0xFF, 0xBF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFB, 0xFF, 0xFF, 0xEF, + 0x00, 0x00, 0x3C, 0x00, 0x00, 0xF0, 0x00, 0x03, 0xC0, 0x00, 0x0F, 0x00, + 0x00, 0x3C, 0x00, 0x00, 0xF0, 0x00, 0x03, 0xC0, 0x00, 0x0F, 0x00, 0x00, + 0x3C, 0x00, 0x00, 0xF0, 0x00, 0x03, 0xC0, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, + 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, + 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xFF, 0xFF, 0xEF, 0xFF, 0xFE, + 0xFF, 0xFF, 0xEF, 0xFF, 0xFE, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, + 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, + 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, + 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x0F, 0xFC, + 0x00, 0x01, 0xFF, 0xFF, 0x00, 0x1F, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFE, + 0x07, 0xF8, 0x07, 0xF8, 0x3F, 0x00, 0x01, 0xE1, 0xF0, 0x00, 0x01, 0x8F, + 0x80, 0x00, 0x02, 0x3E, 0x00, 0x00, 0x01, 0xF0, 0x00, 0x00, 0x07, 0x80, + 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x03, 0xC0, 0x00, + 0x00, 0x0F, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, + 0x03, 0xC0, 0x00, 0xFF, 0xFF, 0x00, 0x03, 0xFF, 0xFC, 0x00, 0x0F, 0xFF, + 0xF0, 0x00, 0x3F, 0xFF, 0xC0, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x03, 0xDE, + 0x00, 0x00, 0x0F, 0x78, 0x00, 0x00, 0x3D, 0xE0, 0x00, 0x00, 0xF7, 0xC0, + 0x00, 0x03, 0xCF, 0x80, 0x00, 0x0F, 0x3E, 0x00, 0x00, 0x3C, 0x7E, 0x00, + 0x00, 0xF0, 0xFC, 0x00, 0x07, 0xC1, 0xFE, 0x00, 0x7F, 0x03, 0xFF, 0xFF, + 0xF8, 0x07, 0xFF, 0xFF, 0xC0, 0x07, 0xFF, 0xF8, 0x00, 0x03, 0xFF, 0x00, + 0xF0, 0x00, 0x03, 0xFC, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x3F, 0xC0, 0x00, + 0x0F, 0xF0, 0x00, 0x03, 0xFC, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x3F, 0xC0, + 0x00, 0x0F, 0xF0, 0x00, 0x03, 0xFC, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x3F, + 0xC0, 0x00, 0x0F, 0xF0, 0x00, 0x03, 0xFC, 0x00, 0x00, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, + 0x00, 0x3F, 0xC0, 0x00, 0x0F, 0xF0, 0x00, 0x03, 0xFC, 0x00, 0x00, 0xFF, + 0x00, 0x00, 0x3F, 0xC0, 0x00, 0x0F, 0xF0, 0x00, 0x03, 0xFC, 0x00, 0x00, + 0xFF, 0x00, 0x00, 0x3F, 0xC0, 0x00, 0x0F, 0xF0, 0x00, 0x03, 0xFC, 0x00, + 0x00, 0xFF, 0x00, 0x00, 0x3F, 0xC0, 0x00, 0x0F, 0xF0, 0x00, 0x03, 0xFC, + 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0xE0, 0x3C, 0x07, + 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, + 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, + 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, + 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x3C, 0x07, + 0x83, 0xF7, 0xFC, 0xFF, 0x9F, 0xC3, 0xE0, 0x00, 0xF0, 0x00, 0x1F, 0x9E, + 0x00, 0x07, 0xE3, 0xC0, 0x01, 0xF8, 0x78, 0x00, 0x7E, 0x0F, 0x00, 0x1F, + 0x81, 0xE0, 0x07, 0xE0, 0x3C, 0x01, 0xF8, 0x07, 0x80, 0x7E, 0x00, 0xF0, + 0x1F, 0x80, 0x1E, 0x07, 0xE0, 0x03, 0xC3, 0xF0, 0x00, 0x78, 0xFC, 0x00, + 0x0F, 0x3F, 0x00, 0x01, 0xEF, 0xC0, 0x00, 0x3F, 0xF0, 0x00, 0x07, 0xFC, + 0x00, 0x00, 0xFF, 0x80, 0x00, 0x1F, 0xF8, 0x00, 0x03, 0xDF, 0x80, 0x00, + 0x79, 0xF8, 0x00, 0x0F, 0x1F, 0x80, 0x01, 0xE0, 0xF8, 0x00, 0x3C, 0x0F, + 0x80, 0x07, 0x80, 0xF8, 0x00, 0xF0, 0x0F, 0x80, 0x1E, 0x00, 0xF8, 0x03, + 0xC0, 0x0F, 0x80, 0x78, 0x00, 0xF8, 0x0F, 0x00, 0x0F, 0x81, 0xE0, 0x00, + 0xF8, 0x3C, 0x00, 0x0F, 0x87, 0x80, 0x00, 0xF8, 0xF0, 0x00, 0x0F, 0x9E, + 0x00, 0x00, 0xFC, 0xF0, 0x00, 0x07, 0x80, 0x00, 0x3C, 0x00, 0x01, 0xE0, + 0x00, 0x0F, 0x00, 0x00, 0x78, 0x00, 0x03, 0xC0, 0x00, 0x1E, 0x00, 0x00, + 0xF0, 0x00, 0x07, 0x80, 0x00, 0x3C, 0x00, 0x01, 0xE0, 0x00, 0x0F, 0x00, + 0x00, 0x78, 0x00, 0x03, 0xC0, 0x00, 0x1E, 0x00, 0x00, 0xF0, 0x00, 0x07, + 0x80, 0x00, 0x3C, 0x00, 0x01, 0xE0, 0x00, 0x0F, 0x00, 0x00, 0x78, 0x00, + 0x03, 0xC0, 0x00, 0x1E, 0x00, 0x00, 0xF0, 0x00, 0x07, 0x80, 0x00, 0x3C, + 0x00, 0x01, 0xE0, 0x00, 0x0F, 0x00, 0x00, 0x78, 0x00, 0x03, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0xFE, 0x00, 0x00, + 0xFF, 0xFC, 0x00, 0x01, 0xFF, 0xFC, 0x00, 0x07, 0xFF, 0xF8, 0x00, 0x0F, + 0xFF, 0xF0, 0x00, 0x1F, 0xFE, 0xF0, 0x00, 0x7B, 0xFD, 0xE0, 0x00, 0xF7, + 0xFB, 0xE0, 0x03, 0xEF, 0xF3, 0xC0, 0x07, 0x9F, 0xE7, 0x80, 0x0F, 0x3F, + 0xC7, 0x80, 0x3C, 0x7F, 0x8F, 0x00, 0x78, 0xFF, 0x1F, 0x01, 0xF1, 0xFE, + 0x1E, 0x03, 0xC3, 0xFC, 0x3C, 0x07, 0x87, 0xF8, 0x3C, 0x1E, 0x0F, 0xF0, + 0x78, 0x3C, 0x1F, 0xE0, 0xF8, 0xF8, 0x3F, 0xC0, 0xF1, 0xE0, 0x7F, 0x81, + 0xE3, 0xC0, 0xFF, 0x01, 0xEF, 0x01, 0xFE, 0x03, 0xDE, 0x03, 0xFC, 0x07, + 0xFC, 0x07, 0xF8, 0x07, 0xF0, 0x0F, 0xF0, 0x0F, 0xE0, 0x1F, 0xE0, 0x1F, + 0xC0, 0x3F, 0xC0, 0x1F, 0x00, 0x7F, 0x80, 0x00, 0x00, 0xFF, 0x00, 0x00, + 0x01, 0xFE, 0x00, 0x00, 0x03, 0xFC, 0x00, 0x00, 0x07, 0xF8, 0x00, 0x00, + 0x0F, 0xF0, 0x00, 0x00, 0x1F, 0xE0, 0x00, 0x00, 0x3C, 0xFC, 0x00, 0x03, + 0xFF, 0x80, 0x00, 0xFF, 0xE0, 0x00, 0x3F, 0xFC, 0x00, 0x0F, 0xFF, 0x00, + 0x03, 0xFF, 0xE0, 0x00, 0xFF, 0x78, 0x00, 0x3F, 0xDF, 0x00, 0x0F, 0xF3, + 0xE0, 0x03, 0xFC, 0x78, 0x00, 0xFF, 0x1F, 0x00, 0x3F, 0xC3, 0xC0, 0x0F, + 0xF0, 0xF8, 0x03, 0xFC, 0x1E, 0x00, 0xFF, 0x07, 0xC0, 0x3F, 0xC0, 0xF0, + 0x0F, 0xF0, 0x3E, 0x03, 0xFC, 0x07, 0xC0, 0xFF, 0x00, 0xF0, 0x3F, 0xC0, + 0x3E, 0x0F, 0xF0, 0x07, 0x83, 0xFC, 0x01, 0xF0, 0xFF, 0x00, 0x3C, 0x3F, + 0xC0, 0x0F, 0x8F, 0xF0, 0x01, 0xE3, 0xFC, 0x00, 0x7C, 0xFF, 0x00, 0x0F, + 0xBF, 0xC0, 0x01, 0xEF, 0xF0, 0x00, 0x7F, 0xFC, 0x00, 0x0F, 0xFF, 0x00, + 0x03, 0xFF, 0xC0, 0x00, 0x7F, 0xF0, 0x00, 0x1F, 0xFC, 0x00, 0x03, 0xF0, + 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x7F, 0xFE, 0x00, 0x01, 0xFF, 0xFF, 0x80, + 0x03, 0xFF, 0xFF, 0xC0, 0x07, 0xF0, 0x0F, 0xE0, 0x0F, 0xC0, 0x03, 0xF0, + 0x1F, 0x80, 0x01, 0xF8, 0x3E, 0x00, 0x00, 0x7C, 0x3E, 0x00, 0x00, 0x7C, + 0x7C, 0x00, 0x00, 0x3C, 0x78, 0x00, 0x00, 0x3E, 0x78, 0x00, 0x00, 0x1E, + 0x78, 0x00, 0x00, 0x1E, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, + 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, + 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, + 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0x78, 0x00, 0x00, 0x1E, + 0x78, 0x00, 0x00, 0x1E, 0x78, 0x00, 0x00, 0x1E, 0x7C, 0x00, 0x00, 0x3C, + 0x3E, 0x00, 0x00, 0x7C, 0x3E, 0x00, 0x00, 0x7C, 0x1F, 0x80, 0x01, 0xF8, + 0x0F, 0xC0, 0x03, 0xF0, 0x07, 0xF0, 0x0F, 0xE0, 0x03, 0xFF, 0xFF, 0xC0, + 0x01, 0xFF, 0xFF, 0x80, 0x00, 0x7F, 0xFE, 0x00, 0x00, 0x0F, 0xF0, 0x00, + 0xFF, 0xFE, 0x03, 0xFF, 0xFE, 0x0F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0xF0, + 0x03, 0xFB, 0xC0, 0x03, 0xEF, 0x00, 0x07, 0xBC, 0x00, 0x1F, 0xF0, 0x00, + 0x3F, 0xC0, 0x00, 0xFF, 0x00, 0x03, 0xFC, 0x00, 0x0F, 0xF0, 0x00, 0x3F, + 0xC0, 0x01, 0xFF, 0x00, 0x07, 0xBC, 0x00, 0x3E, 0xF0, 0x03, 0xFB, 0xFF, + 0xFF, 0xCF, 0xFF, 0xFE, 0x3F, 0xFF, 0xE0, 0xFF, 0xFE, 0x03, 0xC0, 0x00, + 0x0F, 0x00, 0x00, 0x3C, 0x00, 0x00, 0xF0, 0x00, 0x03, 0xC0, 0x00, 0x0F, + 0x00, 0x00, 0x3C, 0x00, 0x00, 0xF0, 0x00, 0x03, 0xC0, 0x00, 0x0F, 0x00, + 0x00, 0x3C, 0x00, 0x00, 0xF0, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x00, 0x0F, + 0xF0, 0x00, 0x00, 0x7F, 0xFE, 0x00, 0x01, 0xFF, 0xFF, 0x80, 0x03, 0xFF, + 0xFF, 0xC0, 0x07, 0xF0, 0x0F, 0xE0, 0x0F, 0xC0, 0x03, 0xF0, 0x1F, 0x80, + 0x01, 0xF8, 0x3E, 0x00, 0x00, 0x7C, 0x3E, 0x00, 0x00, 0x7C, 0x7C, 0x00, + 0x00, 0x3C, 0x78, 0x00, 0x00, 0x3E, 0x78, 0x00, 0x00, 0x1E, 0x78, 0x00, + 0x00, 0x1E, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, + 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, + 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, + 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0x78, 0x00, 0x00, 0x1E, 0x78, 0x00, + 0x00, 0x1E, 0x78, 0x00, 0x00, 0x1E, 0x7C, 0x00, 0x00, 0x3E, 0x3E, 0x00, + 0x00, 0x7C, 0x3E, 0x00, 0x00, 0x7C, 0x1F, 0x80, 0x01, 0xF8, 0x0F, 0xC0, + 0x03, 0xF0, 0x07, 0xF0, 0x0F, 0xE0, 0x03, 0xFF, 0xFF, 0xC0, 0x01, 0xFF, + 0xFF, 0x80, 0x00, 0x7F, 0xFE, 0x00, 0x00, 0x0F, 0xFF, 0x00, 0x00, 0x00, + 0x1F, 0x80, 0x00, 0x00, 0x0F, 0xC0, 0x00, 0x00, 0x07, 0xE0, 0x00, 0x00, + 0x03, 0xF0, 0x00, 0x00, 0x01, 0xF8, 0xFF, 0xFE, 0x00, 0x1F, 0xFF, 0xF8, + 0x03, 0xFF, 0xFF, 0x80, 0x7F, 0xFF, 0xF8, 0x0F, 0x00, 0x3F, 0x81, 0xE0, + 0x01, 0xF0, 0x3C, 0x00, 0x1F, 0x07, 0x80, 0x01, 0xE0, 0xF0, 0x00, 0x3C, + 0x1E, 0x00, 0x07, 0x83, 0xC0, 0x00, 0xF0, 0x78, 0x00, 0x1E, 0x0F, 0x00, + 0x03, 0xC1, 0xE0, 0x00, 0xF8, 0x3C, 0x00, 0x3E, 0x07, 0x80, 0x1F, 0xC0, + 0xFF, 0xFF, 0xF0, 0x1F, 0xFF, 0xFC, 0x03, 0xFF, 0xFE, 0x00, 0x7F, 0xFF, + 0xF0, 0x0F, 0x00, 0x7E, 0x01, 0xE0, 0x03, 0xE0, 0x3C, 0x00, 0x3E, 0x07, + 0x80, 0x07, 0xC0, 0xF0, 0x00, 0x7C, 0x1E, 0x00, 0x07, 0x83, 0xC0, 0x00, + 0xF8, 0x78, 0x00, 0x0F, 0x0F, 0x00, 0x01, 0xF1, 0xE0, 0x00, 0x1E, 0x3C, + 0x00, 0x03, 0xE7, 0x80, 0x00, 0x3E, 0xF0, 0x00, 0x03, 0xDE, 0x00, 0x00, + 0x7C, 0x00, 0xFF, 0x80, 0x07, 0xFF, 0xF8, 0x1F, 0xFF, 0xFC, 0x3F, 0xFF, + 0xFC, 0x3F, 0x00, 0xFC, 0x7C, 0x00, 0x1C, 0x78, 0x00, 0x04, 0xF0, 0x00, + 0x00, 0xF0, 0x00, 0x00, 0xF0, 0x00, 0x00, 0xF0, 0x00, 0x00, 0xF0, 0x00, + 0x00, 0xF8, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x7F, 0xC0, 0x00, 0x3F, 0xFE, + 0x00, 0x3F, 0xFF, 0xC0, 0x0F, 0xFF, 0xF0, 0x03, 0xFF, 0xF8, 0x00, 0x7F, + 0xFC, 0x00, 0x03, 0xFE, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x3F, 0x00, 0x00, + 0x1F, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x0F, 0x00, 0x00, + 0x0F, 0x00, 0x00, 0x1F, 0x80, 0x00, 0x1E, 0xE0, 0x00, 0x3E, 0xFE, 0x01, + 0xFC, 0xFF, 0xFF, 0xFC, 0xFF, 0xFF, 0xF8, 0x3F, 0xFF, 0xE0, 0x03, 0xFF, + 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x00, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x0F, + 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x00, + 0x00, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, + 0xF0, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x0F, 0x00, + 0x00, 0x00, 0xF0, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, + 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0xF0, + 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x0F, 0x00, 0x00, + 0x00, 0xF0, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x0F, + 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x00, + 0xF0, 0x00, 0x03, 0xFC, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x3F, 0xC0, 0x00, + 0x0F, 0xF0, 0x00, 0x03, 0xFC, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x3F, 0xC0, + 0x00, 0x0F, 0xF0, 0x00, 0x03, 0xFC, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x3F, + 0xC0, 0x00, 0x0F, 0xF0, 0x00, 0x03, 0xFC, 0x00, 0x00, 0xFF, 0x00, 0x00, + 0x3F, 0xC0, 0x00, 0x0F, 0xF0, 0x00, 0x03, 0xFC, 0x00, 0x00, 0xFF, 0x00, + 0x00, 0x3F, 0xC0, 0x00, 0x0F, 0xF0, 0x00, 0x03, 0xFC, 0x00, 0x00, 0xFF, + 0x00, 0x00, 0x3F, 0xC0, 0x00, 0x0F, 0xF0, 0x00, 0x03, 0xFE, 0x00, 0x01, + 0xF7, 0x80, 0x00, 0x79, 0xE0, 0x00, 0x1E, 0x7C, 0x00, 0x0F, 0x8F, 0x80, + 0x07, 0xC3, 0xF8, 0x07, 0xF0, 0x7F, 0xFF, 0xF8, 0x0F, 0xFF, 0xFC, 0x00, + 0xFF, 0xFC, 0x00, 0x0F, 0xFC, 0x00, 0xF0, 0x00, 0x00, 0x1E, 0xF0, 0x00, + 0x00, 0x79, 0xE0, 0x00, 0x00, 0xF3, 0xE0, 0x00, 0x03, 0xE3, 0xC0, 0x00, + 0x07, 0x87, 0x80, 0x00, 0x0F, 0x0F, 0x80, 0x00, 0x3E, 0x0F, 0x00, 0x00, + 0x78, 0x1F, 0x00, 0x01, 0xF0, 0x1E, 0x00, 0x03, 0xC0, 0x3C, 0x00, 0x07, + 0x80, 0x7C, 0x00, 0x1F, 0x00, 0x78, 0x00, 0x3C, 0x00, 0xF0, 0x00, 0x78, + 0x00, 0xF0, 0x01, 0xF0, 0x01, 0xE0, 0x03, 0xC0, 0x03, 0xE0, 0x0F, 0x80, + 0x03, 0xC0, 0x1E, 0x00, 0x07, 0x80, 0x3C, 0x00, 0x0F, 0x80, 0xF8, 0x00, + 0x0F, 0x01, 0xE0, 0x00, 0x1E, 0x03, 0xC0, 0x00, 0x1E, 0x0F, 0x00, 0x00, + 0x3C, 0x1E, 0x00, 0x00, 0x7C, 0x7C, 0x00, 0x00, 0x78, 0xF0, 0x00, 0x00, + 0xF1, 0xE0, 0x00, 0x01, 0xF7, 0xC0, 0x00, 0x01, 0xEF, 0x00, 0x00, 0x03, + 0xFE, 0x00, 0x00, 0x03, 0xF8, 0x00, 0x00, 0x07, 0xF0, 0x00, 0x00, 0x0F, + 0xE0, 0x00, 0x00, 0x0F, 0x80, 0x00, 0xF0, 0x00, 0x3F, 0x80, 0x01, 0xFF, + 0x00, 0x07, 0xF0, 0x00, 0x7D, 0xE0, 0x00, 0xFE, 0x00, 0x0F, 0x3C, 0x00, + 0x1F, 0xC0, 0x01, 0xE7, 0x80, 0x07, 0xFC, 0x00, 0x3C, 0xF8, 0x00, 0xF7, + 0x80, 0x0F, 0x8F, 0x00, 0x1E, 0xF0, 0x01, 0xE1, 0xE0, 0x03, 0xDE, 0x00, + 0x3C, 0x3C, 0x00, 0xFB, 0xE0, 0x07, 0x87, 0xC0, 0x1E, 0x3C, 0x01, 0xF0, + 0x78, 0x03, 0xC7, 0x80, 0x3C, 0x0F, 0x00, 0x78, 0xF0, 0x07, 0x81, 0xE0, + 0x1F, 0x1F, 0x00, 0xF0, 0x3E, 0x03, 0xC1, 0xE0, 0x3E, 0x03, 0xC0, 0x78, + 0x3C, 0x07, 0x80, 0x78, 0x0F, 0x07, 0x80, 0xF0, 0x0F, 0x03, 0xE0, 0xF8, + 0x1E, 0x01, 0xF0, 0x78, 0x0F, 0x07, 0xC0, 0x1E, 0x0F, 0x01, 0xE0, 0xF0, + 0x03, 0xC1, 0xE0, 0x3C, 0x1E, 0x00, 0x78, 0x7C, 0x07, 0xC3, 0xC0, 0x0F, + 0x8F, 0x00, 0x78, 0xF8, 0x00, 0xF1, 0xE0, 0x0F, 0x1E, 0x00, 0x1E, 0x3C, + 0x01, 0xE3, 0xC0, 0x03, 0xCF, 0x80, 0x3E, 0x78, 0x00, 0x7D, 0xE0, 0x03, + 0xDF, 0x00, 0x07, 0xBC, 0x00, 0x7B, 0xC0, 0x00, 0xF7, 0x80, 0x0F, 0x78, + 0x00, 0x1E, 0xF0, 0x01, 0xEF, 0x00, 0x03, 0xFC, 0x00, 0x1F, 0xE0, 0x00, + 0x3F, 0x80, 0x03, 0xF8, 0x00, 0x07, 0xF0, 0x00, 0x7F, 0x00, 0x00, 0xFE, + 0x00, 0x0F, 0xE0, 0x00, 0x1F, 0x80, 0x00, 0xFC, 0x00, 0x3E, 0x00, 0x01, + 0xF0, 0xF8, 0x00, 0x1F, 0x03, 0xC0, 0x01, 0xF0, 0x1F, 0x00, 0x0F, 0x80, + 0x7C, 0x00, 0xF8, 0x01, 0xE0, 0x0F, 0x80, 0x0F, 0x80, 0x7C, 0x00, 0x3E, + 0x07, 0xC0, 0x00, 0xF0, 0x7C, 0x00, 0x07, 0xC3, 0xE0, 0x00, 0x1F, 0x3E, + 0x00, 0x00, 0xFB, 0xE0, 0x00, 0x03, 0xFF, 0x00, 0x00, 0x0F, 0xF0, 0x00, + 0x00, 0x7F, 0x00, 0x00, 0x01, 0xF0, 0x00, 0x00, 0x0F, 0xC0, 0x00, 0x00, + 0xFE, 0x00, 0x00, 0x07, 0xF8, 0x00, 0x00, 0x7F, 0xE0, 0x00, 0x07, 0xDF, + 0x00, 0x00, 0x3C, 0x7C, 0x00, 0x03, 0xE1, 0xF0, 0x00, 0x3E, 0x0F, 0x80, + 0x03, 0xE0, 0x3E, 0x00, 0x1F, 0x00, 0xF0, 0x01, 0xF0, 0x07, 0xC0, 0x1F, + 0x00, 0x1F, 0x00, 0xF8, 0x00, 0x78, 0x0F, 0x80, 0x03, 0xE0, 0xF8, 0x00, + 0x0F, 0x87, 0xC0, 0x00, 0x3C, 0x7C, 0x00, 0x01, 0xF7, 0xC0, 0x00, 0x07, + 0xC0, 0xF8, 0x00, 0x01, 0xF7, 0xC0, 0x00, 0x3E, 0x3E, 0x00, 0x07, 0xC3, + 0xE0, 0x00, 0x7C, 0x1F, 0x00, 0x0F, 0x80, 0xF8, 0x01, 0xF0, 0x0F, 0x80, + 0x1F, 0x00, 0x7C, 0x03, 0xE0, 0x03, 0xE0, 0x7C, 0x00, 0x3E, 0x07, 0xC0, + 0x01, 0xF0, 0xF8, 0x00, 0x0F, 0x9F, 0x00, 0x00, 0xF9, 0xF0, 0x00, 0x07, + 0xFE, 0x00, 0x00, 0x3F, 0xC0, 0x00, 0x03, 0xFC, 0x00, 0x00, 0x1F, 0x80, + 0x00, 0x00, 0xF0, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, + 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0xF0, + 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x0F, 0x00, 0x00, + 0x00, 0xF0, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x0F, + 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x00, + 0x7F, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0xFE, 0x7F, 0xFF, 0xFF, 0xE7, 0xFF, + 0xFF, 0xFE, 0x00, 0x00, 0x07, 0xC0, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x1F, + 0x80, 0x00, 0x01, 0xF0, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x07, 0xC0, 0x00, + 0x00, 0xF8, 0x00, 0x00, 0x1F, 0x80, 0x00, 0x01, 0xF0, 0x00, 0x00, 0x3E, + 0x00, 0x00, 0x07, 0xC0, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x1F, 0x00, 0x00, + 0x01, 0xF0, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x07, 0xC0, 0x00, 0x00, 0xF8, + 0x00, 0x00, 0x1F, 0x00, 0x00, 0x03, 0xF0, 0x00, 0x00, 0x3E, 0x00, 0x00, + 0x07, 0xC0, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x03, 0xF0, + 0x00, 0x00, 0x3E, 0x00, 0x00, 0x07, 0xC0, 0x00, 0x00, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, + 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, + 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, + 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0xFF, + 0xFF, 0xFF, 0xFF, 0xF0, 0xF0, 0x00, 0xF8, 0x00, 0x78, 0x00, 0x78, 0x00, + 0x7C, 0x00, 0x3C, 0x00, 0x3C, 0x00, 0x3C, 0x00, 0x1E, 0x00, 0x1E, 0x00, + 0x1E, 0x00, 0x1F, 0x00, 0x0F, 0x00, 0x0F, 0x00, 0x0F, 0x80, 0x07, 0x80, + 0x07, 0x80, 0x07, 0x80, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x01, 0xE0, + 0x01, 0xE0, 0x01, 0xE0, 0x01, 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xF8, + 0x00, 0x78, 0x00, 0x78, 0x00, 0x78, 0x00, 0x3C, 0x00, 0x3C, 0x00, 0x3C, + 0x00, 0x3E, 0x00, 0x1E, 0x00, 0x1E, 0x00, 0x1F, 0x00, 0x0F, 0xFF, 0xFF, + 0xFF, 0xFF, 0xF0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, + 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x78, 0x3C, 0x1E, + 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x83, + 0xC1, 0xE0, 0xF0, 0x78, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x0F, + 0x80, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x0F, 0xF8, 0x00, 0x00, 0xFF, 0xE0, + 0x00, 0x0F, 0xDF, 0x80, 0x00, 0xFC, 0x7E, 0x00, 0x0F, 0xC1, 0xF8, 0x00, + 0xF8, 0x07, 0xE0, 0x0F, 0x80, 0x0F, 0x80, 0xF8, 0x00, 0x3E, 0x0F, 0x80, + 0x00, 0xF8, 0xF8, 0x00, 0x03, 0xEF, 0x80, 0x00, 0x0F, 0x80, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x0F, + 0x80, 0xF0, 0x0F, 0x00, 0xF0, 0x0E, 0x01, 0xE0, 0x1E, 0x01, 0xE0, 0x01, + 0xFE, 0x00, 0x7F, 0xFE, 0x03, 0xFF, 0xFE, 0x0F, 0xFF, 0xF8, 0x3C, 0x03, + 0xF0, 0x80, 0x03, 0xE0, 0x00, 0x07, 0x80, 0x00, 0x1E, 0x00, 0x00, 0x3C, + 0x00, 0x00, 0xF0, 0x00, 0x03, 0xC0, 0x7F, 0xFF, 0x0F, 0xFF, 0xFC, 0x7F, + 0xFF, 0xF3, 0xFF, 0xFF, 0xDF, 0xC0, 0x0F, 0x78, 0x00, 0x3F, 0xE0, 0x00, + 0xFF, 0x00, 0x03, 0xFC, 0x00, 0x1F, 0xF0, 0x00, 0x7F, 0xC0, 0x03, 0xFF, + 0x80, 0x1F, 0xDF, 0x81, 0xFF, 0x7F, 0xFF, 0xBC, 0xFF, 0xFC, 0xF1, 0xFF, + 0xE3, 0xC1, 0xFC, 0x00, 0xF0, 0x00, 0x01, 0xE0, 0x00, 0x03, 0xC0, 0x00, + 0x07, 0x80, 0x00, 0x0F, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x3C, 0x00, 0x00, + 0x78, 0x00, 0x00, 0xF0, 0x00, 0x01, 0xE0, 0xFE, 0x03, 0xC7, 0xFF, 0x07, + 0x9F, 0xFF, 0x0F, 0x7F, 0xFF, 0x1F, 0xF0, 0x7E, 0x3F, 0x80, 0x3E, 0x7E, + 0x00, 0x3E, 0xF8, 0x00, 0x3D, 0xF0, 0x00, 0x7B, 0xE0, 0x00, 0xFF, 0x80, + 0x00, 0xFF, 0x00, 0x01, 0xFE, 0x00, 0x03, 0xFC, 0x00, 0x07, 0xF8, 0x00, + 0x0F, 0xF0, 0x00, 0x1F, 0xE0, 0x00, 0x3F, 0xC0, 0x00, 0x7F, 0xC0, 0x01, + 0xFF, 0x80, 0x03, 0xDF, 0x00, 0x07, 0xBF, 0x00, 0x1F, 0x7F, 0x00, 0x7C, + 0xFF, 0x83, 0xF1, 0xEF, 0xFF, 0xE3, 0xCF, 0xFF, 0x87, 0x8F, 0xFE, 0x00, + 0x07, 0xE0, 0x00, 0x00, 0x7F, 0x80, 0x3F, 0xFE, 0x07, 0xFF, 0xF0, 0xFF, + 0xFF, 0x1F, 0xC0, 0xF3, 0xF0, 0x01, 0x7C, 0x00, 0x07, 0xC0, 0x00, 0x78, + 0x00, 0x0F, 0x80, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, + 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, + 0xF8, 0x00, 0x07, 0x80, 0x00, 0x7C, 0x00, 0x03, 0xC0, 0x00, 0x3F, 0x00, + 0x11, 0xFC, 0x0F, 0x0F, 0xFF, 0xF0, 0x7F, 0xFF, 0x03, 0xFF, 0xE0, 0x07, + 0xF0, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x78, 0x00, 0x00, + 0xF0, 0x00, 0x01, 0xE0, 0x00, 0x03, 0xC0, 0x00, 0x07, 0x80, 0x00, 0x0F, + 0x00, 0x00, 0x1E, 0x01, 0xF8, 0x3C, 0x1F, 0xFC, 0x78, 0x7F, 0xFC, 0xF1, + 0xFF, 0xFD, 0xE7, 0xF0, 0x7F, 0xCF, 0x80, 0x3F, 0xBE, 0x00, 0x3F, 0x78, + 0x00, 0x3E, 0xF0, 0x00, 0x7F, 0xE0, 0x00, 0xFF, 0x80, 0x00, 0xFF, 0x00, + 0x01, 0xFE, 0x00, 0x03, 0xFC, 0x00, 0x07, 0xF8, 0x00, 0x0F, 0xF0, 0x00, + 0x1F, 0xE0, 0x00, 0x3F, 0xC0, 0x00, 0x7F, 0xC0, 0x01, 0xF7, 0x80, 0x03, + 0xEF, 0x00, 0x07, 0xDF, 0x00, 0x1F, 0x9F, 0x00, 0x7F, 0x3F, 0x83, 0xFE, + 0x3F, 0xFF, 0xBC, 0x3F, 0xFE, 0x78, 0x3F, 0xF8, 0xF0, 0x1F, 0xC0, 0x00, + 0x00, 0x7F, 0x80, 0x03, 0xFF, 0xE0, 0x07, 0xFF, 0xF0, 0x0F, 0xFF, 0xF8, + 0x1F, 0x80, 0xFC, 0x3E, 0x00, 0x3E, 0x3C, 0x00, 0x1E, 0x78, 0x00, 0x1E, + 0x78, 0x00, 0x0F, 0x70, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0xF0, 0x00, 0x00, + 0xF0, 0x00, 0x00, 0xF8, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x78, 0x00, 0x00, + 0x7C, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x3F, 0x00, 0x02, 0x1F, 0xC0, 0x3E, + 0x0F, 0xFF, 0xFE, 0x07, 0xFF, 0xFE, 0x01, 0xFF, 0xFC, 0x00, 0x7F, 0xC0, + 0x00, 0xFF, 0x03, 0xFF, 0x07, 0xFF, 0x07, 0xFF, 0x0F, 0x80, 0x0F, 0x00, + 0x0F, 0x00, 0x0F, 0x00, 0x0F, 0x00, 0x0F, 0x00, 0xFF, 0xFE, 0xFF, 0xFE, + 0xFF, 0xFE, 0xFF, 0xFE, 0x0F, 0x00, 0x0F, 0x00, 0x0F, 0x00, 0x0F, 0x00, + 0x0F, 0x00, 0x0F, 0x00, 0x0F, 0x00, 0x0F, 0x00, 0x0F, 0x00, 0x0F, 0x00, + 0x0F, 0x00, 0x0F, 0x00, 0x0F, 0x00, 0x0F, 0x00, 0x0F, 0x00, 0x0F, 0x00, + 0x0F, 0x00, 0x0F, 0x00, 0x0F, 0x00, 0x0F, 0x00, 0x0F, 0x00, 0x0F, 0x00, + 0x01, 0xFC, 0x00, 0x0F, 0xFE, 0x3C, 0x3F, 0xFE, 0x78, 0xFF, 0xFE, 0xF3, + 0xF8, 0x3F, 0xE7, 0xC0, 0x1F, 0xDF, 0x00, 0x1F, 0xBC, 0x00, 0x1F, 0x78, + 0x00, 0x3F, 0xF0, 0x00, 0x7F, 0xC0, 0x00, 0x7F, 0x80, 0x00, 0xFF, 0x00, + 0x01, 0xFE, 0x00, 0x03, 0xFC, 0x00, 0x07, 0xF8, 0x00, 0x0F, 0xF0, 0x00, + 0x1F, 0xE0, 0x00, 0x7D, 0xE0, 0x00, 0xFB, 0xC0, 0x01, 0xF7, 0xC0, 0x07, + 0xE7, 0xC0, 0x1F, 0xCF, 0xE0, 0xFF, 0x8F, 0xFF, 0xEF, 0x0F, 0xFF, 0x9E, + 0x0F, 0xFE, 0x3C, 0x07, 0xF0, 0x78, 0x00, 0x00, 0xF0, 0x00, 0x03, 0xC0, + 0x00, 0x07, 0x80, 0x00, 0x1F, 0x08, 0x00, 0x7C, 0x1E, 0x03, 0xF8, 0x3F, + 0xFF, 0xE0, 0x7F, 0xFF, 0x80, 0x7F, 0xFE, 0x00, 0x1F, 0xE0, 0x00, 0xF0, + 0x00, 0x03, 0xC0, 0x00, 0x0F, 0x00, 0x00, 0x3C, 0x00, 0x00, 0xF0, 0x00, + 0x03, 0xC0, 0x00, 0x0F, 0x00, 0x00, 0x3C, 0x00, 0x00, 0xF0, 0x00, 0x03, + 0xC1, 0xFC, 0x0F, 0x1F, 0xFC, 0x3C, 0xFF, 0xF8, 0xF7, 0xFF, 0xF3, 0xFE, + 0x07, 0xEF, 0xE0, 0x0F, 0xBF, 0x00, 0x1E, 0xF8, 0x00, 0x7F, 0xE0, 0x00, + 0xFF, 0x00, 0x03, 0xFC, 0x00, 0x0F, 0xF0, 0x00, 0x3F, 0xC0, 0x00, 0xFF, + 0x00, 0x03, 0xFC, 0x00, 0x0F, 0xF0, 0x00, 0x3F, 0xC0, 0x00, 0xFF, 0x00, + 0x03, 0xFC, 0x00, 0x0F, 0xF0, 0x00, 0x3F, 0xC0, 0x00, 0xFF, 0x00, 0x03, + 0xFC, 0x00, 0x0F, 0xF0, 0x00, 0x3F, 0xC0, 0x00, 0xFF, 0x00, 0x03, 0xFC, + 0x00, 0x0F, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x83, 0xC1, 0xE0, + 0xF0, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xE0, 0xF0, 0x78, 0x3C, + 0x1E, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, + 0x83, 0xC1, 0xE0, 0xF0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x83, 0xC1, 0xE0, + 0xF0, 0x78, 0x3C, 0x1E, 0x0F, 0x0F, 0x8F, 0xBF, 0xDF, 0xCF, 0xE7, 0xC0, + 0xF0, 0x00, 0x01, 0xE0, 0x00, 0x03, 0xC0, 0x00, 0x07, 0x80, 0x00, 0x0F, + 0x00, 0x00, 0x1E, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x78, 0x00, 0x00, 0xF0, + 0x00, 0x01, 0xE0, 0x00, 0x03, 0xC0, 0x07, 0xE7, 0x80, 0x1F, 0x8F, 0x00, + 0x7E, 0x1E, 0x01, 0xF8, 0x3C, 0x07, 0xE0, 0x78, 0x1F, 0x80, 0xF0, 0x7E, + 0x01, 0xE1, 0xF8, 0x03, 0xCF, 0xC0, 0x07, 0xBF, 0x00, 0x0F, 0xFC, 0x00, + 0x1F, 0xF0, 0x00, 0x3F, 0xE0, 0x00, 0x7F, 0xE0, 0x00, 0xF7, 0xE0, 0x01, + 0xE7, 0xE0, 0x03, 0xC7, 0xE0, 0x07, 0x87, 0xE0, 0x0F, 0x07, 0xE0, 0x1E, + 0x07, 0xE0, 0x3C, 0x07, 0xE0, 0x78, 0x07, 0xE0, 0xF0, 0x07, 0xE1, 0xE0, + 0x03, 0xE3, 0xC0, 0x03, 0xE7, 0x80, 0x03, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x00, 0x7E, 0x00, 0x3F, 0x83, 0xC7, 0xFE, 0x03, 0xFF, 0x0F, + 0x3F, 0xFC, 0x1F, 0xFE, 0x3D, 0xFF, 0xF8, 0xFF, 0xFC, 0xFF, 0x83, 0xF7, + 0xC1, 0xFB, 0xF8, 0x07, 0xDC, 0x03, 0xEF, 0xC0, 0x0F, 0xE0, 0x07, 0xBE, + 0x00, 0x3F, 0x00, 0x1F, 0xF8, 0x00, 0x7C, 0x00, 0x3F, 0xC0, 0x01, 0xE0, + 0x00, 0xFF, 0x00, 0x07, 0x80, 0x03, 0xFC, 0x00, 0x1E, 0x00, 0x0F, 0xF0, + 0x00, 0x78, 0x00, 0x3F, 0xC0, 0x01, 0xE0, 0x00, 0xFF, 0x00, 0x07, 0x80, + 0x03, 0xFC, 0x00, 0x1E, 0x00, 0x0F, 0xF0, 0x00, 0x78, 0x00, 0x3F, 0xC0, + 0x01, 0xE0, 0x00, 0xFF, 0x00, 0x07, 0x80, 0x03, 0xFC, 0x00, 0x1E, 0x00, + 0x0F, 0xF0, 0x00, 0x78, 0x00, 0x3F, 0xC0, 0x01, 0xE0, 0x00, 0xFF, 0x00, + 0x07, 0x80, 0x03, 0xFC, 0x00, 0x1E, 0x00, 0x0F, 0xF0, 0x00, 0x78, 0x00, + 0x3F, 0xC0, 0x01, 0xE0, 0x00, 0xFF, 0x00, 0x07, 0x80, 0x03, 0xC0, 0x00, + 0x7F, 0x03, 0xC7, 0xFF, 0x0F, 0x3F, 0xFE, 0x3D, 0xFF, 0xFC, 0xFF, 0x81, + 0xFB, 0xF8, 0x03, 0xEF, 0xC0, 0x07, 0xBE, 0x00, 0x1F, 0xF8, 0x00, 0x3F, + 0xC0, 0x00, 0xFF, 0x00, 0x03, 0xFC, 0x00, 0x0F, 0xF0, 0x00, 0x3F, 0xC0, + 0x00, 0xFF, 0x00, 0x03, 0xFC, 0x00, 0x0F, 0xF0, 0x00, 0x3F, 0xC0, 0x00, + 0xFF, 0x00, 0x03, 0xFC, 0x00, 0x0F, 0xF0, 0x00, 0x3F, 0xC0, 0x00, 0xFF, + 0x00, 0x03, 0xFC, 0x00, 0x0F, 0xF0, 0x00, 0x3F, 0xC0, 0x00, 0xFF, 0x00, + 0x03, 0xC0, 0x00, 0xFF, 0x00, 0x03, 0xFF, 0xC0, 0x0F, 0xFF, 0xF0, 0x1F, + 0xFF, 0xF8, 0x1F, 0x81, 0xF8, 0x3E, 0x00, 0x7C, 0x7C, 0x00, 0x3E, 0x7C, + 0x00, 0x3E, 0x78, 0x00, 0x1E, 0xF8, 0x00, 0x1F, 0xF0, 0x00, 0x0F, 0xF0, + 0x00, 0x0F, 0xF0, 0x00, 0x0F, 0xF0, 0x00, 0x0F, 0xF0, 0x00, 0x0F, 0xF0, + 0x00, 0x0F, 0xF0, 0x00, 0x0F, 0xF0, 0x00, 0x0F, 0xF8, 0x00, 0x1F, 0x78, + 0x00, 0x1E, 0x7C, 0x00, 0x3E, 0x7C, 0x00, 0x3E, 0x3E, 0x00, 0x7C, 0x1F, + 0x81, 0xF8, 0x1F, 0xFF, 0xF8, 0x0F, 0xFF, 0xF0, 0x03, 0xFF, 0xC0, 0x00, + 0xFF, 0x00, 0x00, 0x7F, 0x01, 0xE3, 0xFF, 0x83, 0xCF, 0xFF, 0x87, 0xBF, + 0xFF, 0x8F, 0xF8, 0x3F, 0x1F, 0xC0, 0x1F, 0x3F, 0x00, 0x1F, 0x7C, 0x00, + 0x1E, 0xF8, 0x00, 0x3D, 0xF0, 0x00, 0x7F, 0xC0, 0x00, 0x7F, 0x80, 0x00, + 0xFF, 0x00, 0x01, 0xFE, 0x00, 0x03, 0xFC, 0x00, 0x07, 0xF8, 0x00, 0x0F, + 0xF0, 0x00, 0x1F, 0xE0, 0x00, 0x3F, 0xE0, 0x00, 0xFF, 0xC0, 0x01, 0xEF, + 0x80, 0x03, 0xDF, 0x80, 0x0F, 0xBF, 0x80, 0x3E, 0x7F, 0xC1, 0xF8, 0xF7, + 0xFF, 0xF1, 0xE7, 0xFF, 0xC3, 0xC7, 0xFF, 0x07, 0x83, 0xF0, 0x0F, 0x00, + 0x00, 0x1E, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x78, 0x00, 0x00, 0xF0, 0x00, + 0x01, 0xE0, 0x00, 0x03, 0xC0, 0x00, 0x07, 0x80, 0x00, 0x0F, 0x00, 0x00, + 0x00, 0x00, 0xFC, 0x00, 0x0F, 0xFE, 0x3C, 0x3F, 0xFE, 0x78, 0xFF, 0xFE, + 0xF3, 0xF8, 0x3F, 0xE7, 0xC0, 0x1F, 0xDF, 0x00, 0x1F, 0xBC, 0x00, 0x1F, + 0x78, 0x00, 0x3F, 0xF0, 0x00, 0x7F, 0xC0, 0x00, 0x7F, 0x80, 0x00, 0xFF, + 0x00, 0x01, 0xFE, 0x00, 0x03, 0xFC, 0x00, 0x07, 0xF8, 0x00, 0x0F, 0xF0, + 0x00, 0x1F, 0xE0, 0x00, 0x3F, 0xE0, 0x00, 0xFB, 0xC0, 0x01, 0xF7, 0x80, + 0x03, 0xEF, 0x80, 0x0F, 0xCF, 0x80, 0x3F, 0x9F, 0xC1, 0xFF, 0x1F, 0xFF, + 0xDE, 0x1F, 0xFF, 0x3C, 0x1F, 0xFC, 0x78, 0x0F, 0xE0, 0xF0, 0x00, 0x01, + 0xE0, 0x00, 0x03, 0xC0, 0x00, 0x07, 0x80, 0x00, 0x0F, 0x00, 0x00, 0x1E, + 0x00, 0x00, 0x3C, 0x00, 0x00, 0x78, 0x00, 0x00, 0xF0, 0x00, 0x01, 0xE0, + 0x00, 0x7F, 0xE3, 0xFF, 0xCF, 0xFF, 0xBF, 0xFF, 0xF8, 0x1F, 0xC0, 0x3F, + 0x00, 0x7C, 0x00, 0xF8, 0x01, 0xE0, 0x03, 0xC0, 0x07, 0x80, 0x0F, 0x00, + 0x1E, 0x00, 0x3C, 0x00, 0x78, 0x00, 0xF0, 0x01, 0xE0, 0x03, 0xC0, 0x07, + 0x80, 0x0F, 0x00, 0x1E, 0x00, 0x3C, 0x00, 0x78, 0x00, 0xF0, 0x01, 0xE0, + 0x03, 0xC0, 0x00, 0x03, 0xFC, 0x03, 0xFF, 0xF0, 0xFF, 0xFF, 0x3F, 0xFF, + 0xE7, 0xE0, 0x3D, 0xF0, 0x00, 0xBC, 0x00, 0x07, 0x80, 0x00, 0xF0, 0x00, + 0x1E, 0x00, 0x03, 0xE0, 0x00, 0x3F, 0x00, 0x07, 0xFE, 0x00, 0x7F, 0xFC, + 0x03, 0xFF, 0xC0, 0x0F, 0xFE, 0x00, 0x1F, 0xC0, 0x00, 0xFC, 0x00, 0x07, + 0x80, 0x00, 0xF0, 0x00, 0x1E, 0x00, 0x03, 0xE0, 0x00, 0xFF, 0xC0, 0x7E, + 0xFF, 0xFF, 0xDF, 0xFF, 0xF1, 0xFF, 0xF8, 0x07, 0xFC, 0x00, 0x1E, 0x00, + 0x1E, 0x00, 0x1E, 0x00, 0x1E, 0x00, 0x1E, 0x00, 0x1E, 0x00, 0x1E, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1E, 0x00, 0x1E, 0x00, + 0x1E, 0x00, 0x1E, 0x00, 0x1E, 0x00, 0x1E, 0x00, 0x1E, 0x00, 0x1E, 0x00, + 0x1E, 0x00, 0x1E, 0x00, 0x1E, 0x00, 0x1E, 0x00, 0x1E, 0x00, 0x1E, 0x00, + 0x1E, 0x00, 0x1E, 0x00, 0x1E, 0x00, 0x1F, 0x00, 0x0F, 0xFF, 0x0F, 0xFF, + 0x07, 0xFF, 0x01, 0xFF, 0x00, 0x00, 0x03, 0xC0, 0x00, 0xFF, 0x00, 0x03, + 0xFC, 0x00, 0x0F, 0xF0, 0x00, 0x3F, 0xC0, 0x00, 0xFF, 0x00, 0x03, 0xFC, + 0x00, 0x0F, 0xF0, 0x00, 0x3F, 0xC0, 0x00, 0xFF, 0x00, 0x03, 0xFC, 0x00, + 0x0F, 0xF0, 0x00, 0x3F, 0xC0, 0x00, 0xFF, 0x00, 0x03, 0xFC, 0x00, 0x0F, + 0xF0, 0x00, 0x3F, 0xC0, 0x00, 0xFF, 0x00, 0x03, 0xFC, 0x00, 0x1F, 0xF8, + 0x00, 0x7D, 0xE0, 0x03, 0xF7, 0xC0, 0x1F, 0xDF, 0x81, 0xFF, 0x3F, 0xFF, + 0xBC, 0x7F, 0xFC, 0xF0, 0xFF, 0xE3, 0xC0, 0xFE, 0x00, 0xF0, 0x00, 0x07, + 0xBC, 0x00, 0x07, 0x9E, 0x00, 0x03, 0xCF, 0x80, 0x03, 0xE3, 0xC0, 0x01, + 0xE1, 0xE0, 0x00, 0xF0, 0xF8, 0x00, 0xF8, 0x3C, 0x00, 0x78, 0x1E, 0x00, + 0x3C, 0x07, 0x80, 0x3C, 0x03, 0xC0, 0x1E, 0x01, 0xF0, 0x1F, 0x00, 0x78, + 0x0F, 0x00, 0x3C, 0x07, 0x80, 0x1F, 0x07, 0xC0, 0x07, 0x83, 0xC0, 0x03, + 0xC1, 0xE0, 0x01, 0xF1, 0xF0, 0x00, 0x78, 0xF0, 0x00, 0x3E, 0xF8, 0x00, + 0x0F, 0x78, 0x00, 0x07, 0xBC, 0x00, 0x03, 0xFE, 0x00, 0x00, 0xFE, 0x00, + 0x00, 0x7F, 0x00, 0x00, 0x3F, 0x80, 0x00, 0xF0, 0x03, 0xF8, 0x01, 0xFF, + 0x00, 0x7F, 0x00, 0x7D, 0xE0, 0x0F, 0xE0, 0x0F, 0x3C, 0x01, 0xFC, 0x01, + 0xE7, 0x80, 0x7F, 0xC0, 0x3C, 0xF8, 0x0F, 0x78, 0x0F, 0x8F, 0x01, 0xEF, + 0x01, 0xE1, 0xE0, 0x3D, 0xE0, 0x3C, 0x3C, 0x0F, 0x9E, 0x07, 0x83, 0xC1, + 0xE3, 0xC1, 0xF0, 0x78, 0x3C, 0x78, 0x3C, 0x0F, 0x07, 0x8F, 0x07, 0x81, + 0xE1, 0xE0, 0xF0, 0xF0, 0x1E, 0x3C, 0x1E, 0x3C, 0x03, 0xC7, 0x83, 0xC7, + 0x80, 0x78, 0xF0, 0x78, 0xF0, 0x0F, 0x3C, 0x07, 0x9E, 0x00, 0xF7, 0x80, + 0xF7, 0x80, 0x1E, 0xF0, 0x1E, 0xF0, 0x03, 0xFE, 0x03, 0xFE, 0x00, 0x7F, + 0x80, 0x3F, 0xC0, 0x07, 0xF0, 0x07, 0xF0, 0x00, 0xFE, 0x00, 0xFE, 0x00, + 0x1F, 0xC0, 0x1F, 0xC0, 0x03, 0xF0, 0x01, 0xF8, 0x00, 0x3E, 0x00, 0x3E, + 0x00, 0x7C, 0x00, 0x0F, 0x9F, 0x00, 0x0F, 0x87, 0xC0, 0x0F, 0x81, 0xF0, + 0x0F, 0x80, 0xF8, 0x07, 0xC0, 0x3E, 0x07, 0xC0, 0x0F, 0x87, 0xC0, 0x03, + 0xE7, 0xC0, 0x01, 0xF3, 0xE0, 0x00, 0x7F, 0xE0, 0x00, 0x1F, 0xE0, 0x00, + 0x07, 0xE0, 0x00, 0x03, 0xF0, 0x00, 0x03, 0xF8, 0x00, 0x01, 0xFE, 0x00, + 0x01, 0xFF, 0x80, 0x01, 0xF7, 0xC0, 0x01, 0xF1, 0xF0, 0x00, 0xF8, 0x7C, + 0x00, 0xF8, 0x3E, 0x00, 0xF8, 0x0F, 0x80, 0xF8, 0x03, 0xE0, 0x7C, 0x00, + 0xF8, 0x7C, 0x00, 0x7C, 0x7C, 0x00, 0x1F, 0x7C, 0x00, 0x07, 0xC0, 0xF8, + 0x00, 0x0F, 0xBC, 0x00, 0x07, 0x9E, 0x00, 0x03, 0xCF, 0x80, 0x03, 0xE3, + 0xC0, 0x01, 0xE1, 0xF0, 0x01, 0xF0, 0x78, 0x00, 0xF0, 0x3C, 0x00, 0x78, + 0x1F, 0x00, 0x7C, 0x07, 0x80, 0x3C, 0x03, 0xE0, 0x3E, 0x00, 0xF0, 0x1E, + 0x00, 0x78, 0x0F, 0x00, 0x3E, 0x0F, 0x80, 0x0F, 0x07, 0x80, 0x07, 0xC7, + 0xC0, 0x01, 0xE3, 0xC0, 0x00, 0xF1, 0xE0, 0x00, 0x7D, 0xE0, 0x00, 0x1E, + 0xF0, 0x00, 0x0F, 0xF8, 0x00, 0x03, 0xF8, 0x00, 0x01, 0xFC, 0x00, 0x00, + 0xFC, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x0F, 0x00, 0x00, + 0x0F, 0x80, 0x00, 0x07, 0x80, 0x00, 0x07, 0xC0, 0x00, 0x07, 0xE0, 0x00, + 0x07, 0xE0, 0x00, 0x3F, 0xF0, 0x00, 0x1F, 0xF0, 0x00, 0x0F, 0xF0, 0x00, + 0x07, 0xE0, 0x00, 0x00, 0x7F, 0xFF, 0xFB, 0xFF, 0xFF, 0xDF, 0xFF, 0xFE, + 0xFF, 0xFF, 0xF0, 0x00, 0x1F, 0x00, 0x01, 0xF8, 0x00, 0x1F, 0x80, 0x00, + 0xF8, 0x00, 0x0F, 0x80, 0x00, 0xF8, 0x00, 0x0F, 0x80, 0x00, 0xF8, 0x00, + 0x0F, 0xC0, 0x00, 0xFC, 0x00, 0x07, 0xC0, 0x00, 0x7C, 0x00, 0x07, 0xC0, + 0x00, 0x7C, 0x00, 0x07, 0xE0, 0x00, 0x7E, 0x00, 0x07, 0xE0, 0x00, 0x3E, + 0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xC0, 0x00, 0x0F, 0xC0, 0x1F, 0xF0, 0x0F, 0xFC, 0x03, 0xFF, 0x01, 0xF8, + 0x00, 0x7C, 0x00, 0x1E, 0x00, 0x07, 0x80, 0x01, 0xE0, 0x00, 0x78, 0x00, + 0x1E, 0x00, 0x07, 0x80, 0x01, 0xE0, 0x00, 0x78, 0x00, 0x1E, 0x00, 0x07, + 0x80, 0x01, 0xE0, 0x00, 0xF8, 0x00, 0x7C, 0x03, 0xFF, 0x00, 0xFF, 0x00, + 0x3F, 0xC0, 0x0F, 0xF8, 0x00, 0x3F, 0x00, 0x03, 0xE0, 0x00, 0xF8, 0x00, + 0x1E, 0x00, 0x07, 0x80, 0x01, 0xE0, 0x00, 0x78, 0x00, 0x1E, 0x00, 0x07, + 0x80, 0x01, 0xE0, 0x00, 0x78, 0x00, 0x1E, 0x00, 0x07, 0x80, 0x01, 0xE0, + 0x00, 0x7C, 0x00, 0x1F, 0x80, 0x03, 0xFF, 0x00, 0xFF, 0xC0, 0x1F, 0xF0, + 0x01, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xF0, 0xFE, 0x00, 0x3F, 0xE0, 0x0F, 0xFC, 0x03, 0xFF, 0x00, 0x07, + 0xE0, 0x00, 0xF8, 0x00, 0x1E, 0x00, 0x07, 0x80, 0x01, 0xE0, 0x00, 0x78, + 0x00, 0x1E, 0x00, 0x07, 0x80, 0x01, 0xE0, 0x00, 0x78, 0x00, 0x1E, 0x00, + 0x07, 0x80, 0x01, 0xE0, 0x00, 0x7C, 0x00, 0x0F, 0x80, 0x03, 0xFF, 0x00, + 0x3F, 0xC0, 0x0F, 0xF0, 0x07, 0xFC, 0x03, 0xF0, 0x01, 0xF0, 0x00, 0x7C, + 0x00, 0x1E, 0x00, 0x07, 0x80, 0x01, 0xE0, 0x00, 0x78, 0x00, 0x1E, 0x00, + 0x07, 0x80, 0x01, 0xE0, 0x00, 0x78, 0x00, 0x1E, 0x00, 0x07, 0x80, 0x01, + 0xE0, 0x00, 0xF8, 0x00, 0x7E, 0x03, 0xFF, 0x00, 0xFF, 0xC0, 0x3F, 0xE0, + 0x0F, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x80, 0x00, 0x47, 0xFF, + 0x80, 0x0E, 0xFF, 0xFF, 0x81, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x0F, 0xFF, + 0xFB, 0x80, 0x0F, 0xFF, 0x18, 0x00, 0x0F, 0xE0, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x03, 0xF8, 0x00, 0x07, 0xFF, 0xC0, 0x03, 0xFF, 0xF8, 0x03, 0xFF, + 0xFF, 0x00, 0xFC, 0x07, 0xC0, 0x7C, 0x00, 0x70, 0x3E, 0x00, 0x0C, 0x0F, + 0x00, 0x01, 0x07, 0xC0, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x78, 0x00, 0x00, + 0x3E, 0x00, 0x00, 0x7F, 0xFF, 0xFC, 0x1F, 0xFF, 0xFE, 0x0F, 0xFF, 0xFF, + 0x80, 0x3C, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x03, 0xC0, 0x00, 0x00, 0xF0, + 0x00, 0x00, 0x3C, 0x00, 0x00, 0x7F, 0xFF, 0xC0, 0x1F, 0xFF, 0xE0, 0x0F, + 0xFF, 0xF8, 0x00, 0x3E, 0x00, 0x00, 0x07, 0x80, 0x00, 0x01, 0xE0, 0x00, + 0x00, 0x78, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x03, 0xE0, 0x00, 0x40, 0xF8, + 0x00, 0x30, 0x1F, 0x80, 0x1C, 0x03, 0xF8, 0x1F, 0x00, 0x7F, 0xFF, 0xC0, + 0x0F, 0xFF, 0xE0, 0x01, 0xFF, 0xF0, 0x00, 0x0F, 0xE0, 0x3C, 0xF3, 0xCF, + 0x3C, 0xE7, 0x9C, 0x73, 0xCE, 0x00, 0x00, 0x0F, 0xF0, 0x03, 0xFF, 0x00, + 0x7F, 0xF0, 0x07, 0xFF, 0x00, 0xF8, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, + 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x0F, 0xFF, 0xE0, 0xFF, 0xFE, + 0x0F, 0xFF, 0xE0, 0xFF, 0xFE, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, + 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, + 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, + 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, + 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, + 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x01, 0xF0, + 0x00, 0x3E, 0x00, 0xFF, 0xE0, 0x0F, 0xFC, 0x00, 0xFF, 0x80, 0x0F, 0xF0, + 0x00, 0x3C, 0x1E, 0x78, 0x3C, 0xF0, 0x79, 0xE0, 0xF3, 0xC1, 0xE7, 0x03, + 0x9E, 0x0F, 0x38, 0x1C, 0x70, 0x39, 0xE0, 0xF3, 0x81, 0xC0, 0xF0, 0x00, + 0xF0, 0x00, 0xFF, 0x00, 0x0F, 0x00, 0x0F, 0xF0, 0x00, 0xF0, 0x00, 0xFF, + 0x00, 0x0F, 0x00, 0x0F, 0xF0, 0x00, 0xF0, 0x00, 0xFF, 0x00, 0x0F, 0x00, + 0x0F, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, + 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, + 0x0F, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, + 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, + 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, + 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, + 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, + 0x00, 0xF0, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, + 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, + 0xF0, 0x00, 0x0F, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, + 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, + 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xF0, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, + 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, + 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x07, 0xE0, 0x00, 0x0E, 0x00, 0x00, 0x00, + 0x07, 0xFE, 0x00, 0x07, 0x80, 0x00, 0x00, 0x03, 0xFF, 0xC0, 0x01, 0xC0, + 0x00, 0x00, 0x01, 0xF0, 0xF0, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x78, 0x1E, + 0x00, 0x38, 0x00, 0x00, 0x00, 0x1E, 0x07, 0x80, 0x1E, 0x00, 0x00, 0x00, + 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x03, 0xC0, 0x3C, 0x03, 0x80, + 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x01, 0xE0, 0x00, 0x00, 0x00, 0x3C, 0x03, + 0xC0, 0x70, 0x00, 0x00, 0x00, 0x0F, 0x00, 0xF0, 0x3C, 0x00, 0x00, 0x00, + 0x03, 0xC0, 0x3C, 0x0E, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x07, 0x00, + 0x00, 0x00, 0x00, 0x1E, 0x07, 0x83, 0xC0, 0x00, 0x00, 0x00, 0x07, 0x81, + 0xE0, 0xE0, 0x00, 0x00, 0x00, 0x01, 0xF0, 0xF8, 0x78, 0x00, 0x00, 0x00, + 0x00, 0x3F, 0xFC, 0x1C, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFE, 0x0F, 0x03, + 0xF0, 0x00, 0x7E, 0x00, 0x7E, 0x07, 0x83, 0xFF, 0x00, 0x7F, 0xE0, 0x00, + 0x01, 0xC1, 0xFF, 0xE0, 0x3F, 0xFC, 0x00, 0x00, 0xF0, 0xF8, 0x7C, 0x1F, + 0x0F, 0x80, 0x00, 0x38, 0x3C, 0x0F, 0x07, 0x81, 0xE0, 0x00, 0x1E, 0x0F, + 0x03, 0xC1, 0xE0, 0x78, 0x00, 0x07, 0x07, 0x80, 0x78, 0xF0, 0x0F, 0x00, + 0x03, 0x81, 0xE0, 0x1E, 0x3C, 0x03, 0xC0, 0x01, 0xE0, 0x78, 0x07, 0x8F, + 0x00, 0xF0, 0x00, 0x70, 0x1E, 0x01, 0xE3, 0xC0, 0x3C, 0x00, 0x3C, 0x07, + 0x80, 0x78, 0xF0, 0x0F, 0x00, 0x0E, 0x01, 0xE0, 0x1E, 0x3C, 0x03, 0xC0, + 0x07, 0x80, 0x78, 0x07, 0x8F, 0x00, 0xF0, 0x01, 0xC0, 0x0F, 0x03, 0xC1, + 0xE0, 0x78, 0x00, 0xE0, 0x03, 0xC0, 0xF0, 0x78, 0x1E, 0x00, 0x78, 0x00, + 0xF8, 0x78, 0x1F, 0x0F, 0x00, 0x1C, 0x00, 0x1F, 0xFE, 0x03, 0xFF, 0xC0, + 0x0F, 0x00, 0x03, 0xFF, 0x00, 0x7F, 0xE0, 0x03, 0x80, 0x00, 0x3F, 0x00, + 0x07, 0xE0, 0x00, 0x20, 0x0C, 0x03, 0x80, 0xF0, 0x3C, 0x0F, 0x03, 0xC1, + 0xF0, 0x7C, 0x1F, 0x03, 0xC0, 0x7C, 0x07, 0xC0, 0x7C, 0x03, 0xC0, 0x3C, + 0x03, 0xC0, 0x3C, 0x03, 0x80, 0x30, 0x02, 0x1C, 0xF3, 0x8E, 0x79, 0xCF, + 0x3C, 0xF3, 0xCF, 0x00, 0x3C, 0xF3, 0xCF, 0x3D, 0xE7, 0x9C, 0x73, 0xCE, + 0x00, 0x1C, 0x0E, 0x78, 0x3C, 0xE0, 0x71, 0xC0, 0xE7, 0x83, 0xCE, 0x07, + 0x3C, 0x1E, 0x78, 0x3C, 0xF0, 0x79, 0xE0, 0xF3, 0xC1, 0xE0, 0x3C, 0x1E, + 0x78, 0x3C, 0xF0, 0x79, 0xE0, 0xF3, 0xC1, 0xE7, 0x03, 0x9E, 0x0F, 0x38, + 0x1C, 0x70, 0x39, 0xE0, 0xF3, 0x81, 0xC0, 0x0F, 0xC0, 0x7F, 0x83, 0xFF, + 0x1F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xF7, 0xFF, 0x8F, 0xFC, 0x1F, 0xE0, 0x3F, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xF9, 0xE0, 0x1F, 0xFF, 0xF3, 0xE0, 0x7C, 0x1C, + 0x07, 0xE1, 0xF8, 0x38, 0x0E, 0xE3, 0x70, 0x70, 0x1C, 0xCC, 0xE0, 0xE0, + 0x39, 0xF9, 0xC1, 0xC0, 0x71, 0xE3, 0x83, 0x80, 0xE1, 0xC7, 0x07, 0x01, + 0xC3, 0x0E, 0x0E, 0x03, 0x80, 0x1C, 0x1C, 0x07, 0x00, 0x38, 0x38, 0x0E, + 0x00, 0x70, 0x70, 0x1C, 0x00, 0xE0, 0x80, 0x18, 0x03, 0x80, 0x78, 0x07, + 0x80, 0x78, 0x07, 0x80, 0x7C, 0x07, 0xC0, 0x7C, 0x07, 0x81, 0xF0, 0x7C, + 0x1F, 0x07, 0x81, 0xE0, 0x78, 0x1E, 0x03, 0x80, 0x60, 0x08, 0x00, 0x00, + 0x00, 0x3E, 0x00, 0xF8, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x00, + 0xF0, 0x03, 0xC0, 0x0F, 0x00, 0x00, 0x03, 0xE1, 0xF7, 0xC3, 0xEF, 0x87, + 0xDF, 0x0F, 0xBE, 0x1F, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x01, 0xE0, 0x00, + 0x00, 0x07, 0x80, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x7C, 0x7C, 0x00, + 0x00, 0xF1, 0xFC, 0x00, 0x03, 0xC3, 0xF8, 0x00, 0x0F, 0x07, 0xF0, 0x00, + 0x1C, 0x1F, 0xF0, 0x00, 0x00, 0x3D, 0xE0, 0x00, 0x00, 0xFB, 0xE0, 0x00, + 0x01, 0xE3, 0xC0, 0x00, 0x03, 0xC7, 0x80, 0x00, 0x0F, 0x8F, 0x80, 0x00, + 0x1E, 0x0F, 0x00, 0x00, 0x3C, 0x1E, 0x00, 0x00, 0xF0, 0x1E, 0x00, 0x01, + 0xE0, 0x3C, 0x00, 0x07, 0xC0, 0x7C, 0x00, 0x0F, 0x00, 0x78, 0x00, 0x1E, + 0x00, 0xF0, 0x00, 0x7C, 0x01, 0xF0, 0x00, 0xF0, 0x01, 0xE0, 0x03, 0xE0, + 0x03, 0xE0, 0x07, 0x80, 0x03, 0xC0, 0x0F, 0xFF, 0xFF, 0x80, 0x3F, 0xFF, + 0xFF, 0x80, 0x7F, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFE, 0x03, 0xE0, 0x00, + 0x3E, 0x07, 0x80, 0x00, 0x3C, 0x1F, 0x00, 0x00, 0x7C, 0x3C, 0x00, 0x00, + 0x78, 0x78, 0x00, 0x00, 0xF1, 0xF0, 0x00, 0x01, 0xF3, 0xC0, 0x00, 0x01, + 0xE7, 0x80, 0x00, 0x03, 0xDE, 0x00, 0x00, 0x03, 0xC0, 0x00, 0x1F, 0xF0, + 0x01, 0xFF, 0xF0, 0x0F, 0xFF, 0xC0, 0x7F, 0xFF, 0x03, 0xF0, 0x0C, 0x0F, + 0x80, 0x00, 0x3C, 0x00, 0x01, 0xE0, 0x00, 0x07, 0x80, 0x00, 0x1E, 0x00, + 0x00, 0x78, 0x00, 0x01, 0xE0, 0x00, 0x07, 0x80, 0x00, 0x1E, 0x00, 0x00, + 0x78, 0x00, 0x01, 0xE0, 0x00, 0x07, 0x80, 0x03, 0xFF, 0xFF, 0x0F, 0xFF, + 0xFC, 0x3F, 0xFF, 0xF0, 0xFF, 0xFF, 0xC0, 0x1E, 0x00, 0x00, 0x78, 0x00, + 0x01, 0xE0, 0x00, 0x07, 0x80, 0x00, 0x1E, 0x00, 0x00, 0x78, 0x00, 0x01, + 0xE0, 0x00, 0x07, 0x80, 0x00, 0x1E, 0x00, 0x00, 0x78, 0x00, 0x3F, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x20, 0x00, + 0x01, 0x1C, 0x00, 0x00, 0xEF, 0x80, 0x00, 0x7D, 0xF0, 0xFC, 0x3E, 0x3E, + 0xFF, 0xDF, 0x07, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0xC0, 0x1F, 0x87, 0xE0, + 0x0F, 0x80, 0x7C, 0x03, 0xC0, 0x0F, 0x01, 0xF0, 0x03, 0xE0, 0x78, 0x00, + 0x78, 0x1E, 0x00, 0x1E, 0x07, 0x80, 0x07, 0x81, 0xE0, 0x01, 0xE0, 0x7C, + 0x00, 0xF8, 0x0F, 0x00, 0x3C, 0x03, 0xE0, 0x1F, 0x00, 0x7E, 0x1F, 0x80, + 0x3F, 0xFF, 0xF0, 0x1F, 0xFF, 0xFE, 0x0F, 0xBF, 0xF7, 0xC7, 0xC3, 0xF0, + 0xFB, 0xE0, 0x00, 0x1F, 0x70, 0x00, 0x03, 0x88, 0x00, 0x00, 0x40, 0xF8, + 0x00, 0x07, 0xDE, 0x00, 0x01, 0xE7, 0xC0, 0x00, 0xF8, 0xF8, 0x00, 0x7C, + 0x1E, 0x00, 0x1E, 0x07, 0xC0, 0x0F, 0x80, 0xF0, 0x03, 0xC0, 0x3E, 0x01, + 0xF0, 0x07, 0x80, 0x78, 0x01, 0xF0, 0x3E, 0x00, 0x3C, 0x0F, 0x00, 0x0F, + 0x87, 0x80, 0x01, 0xF3, 0xE0, 0x1F, 0xFC, 0xFF, 0xE7, 0xFF, 0xFF, 0xF9, + 0xFF, 0xFF, 0xFE, 0x00, 0x7F, 0x80, 0x00, 0x0F, 0xC0, 0x00, 0x03, 0xF0, + 0x00, 0x00, 0x78, 0x00, 0x00, 0x1E, 0x00, 0x1F, 0xFF, 0xFF, 0xE7, 0xFF, + 0xFF, 0xF9, 0xFF, 0xFF, 0xFE, 0x00, 0x1E, 0x00, 0x00, 0x07, 0x80, 0x00, + 0x01, 0xE0, 0x00, 0x00, 0x78, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x07, 0x80, + 0x00, 0x01, 0xE0, 0x00, 0x00, 0x78, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x07, + 0x80, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, + 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0xFE, + 0x00, 0xFF, 0xF0, 0x7F, 0xFE, 0x0F, 0x83, 0xC3, 0xE0, 0x08, 0x78, 0x00, + 0x0F, 0x00, 0x01, 0xE0, 0x00, 0x3E, 0x00, 0x03, 0xE0, 0x00, 0x7E, 0x00, + 0x07, 0xF0, 0x01, 0xFF, 0x00, 0x7B, 0xF8, 0x1E, 0x1F, 0x83, 0xC1, 0xFC, + 0xF0, 0x0F, 0xDE, 0x00, 0xFB, 0xC0, 0x0F, 0xFC, 0x00, 0xFF, 0x80, 0x1E, + 0xF8, 0x03, 0xCF, 0xC0, 0x78, 0xFC, 0x1E, 0x0F, 0xE7, 0xC0, 0x7F, 0xF0, + 0x03, 0xF8, 0x00, 0x3F, 0x00, 0x03, 0xF0, 0x00, 0x1E, 0x00, 0x03, 0xE0, + 0x00, 0x3C, 0x00, 0x07, 0x80, 0x00, 0xF0, 0x80, 0x3E, 0x1E, 0x0F, 0x83, + 0xFF, 0xE0, 0x7F, 0xF8, 0x03, 0xFC, 0x00, 0xF8, 0x7F, 0xE1, 0xFF, 0x87, + 0xFE, 0x1F, 0xF8, 0x7C, 0x00, 0x07, 0xF8, 0x00, 0x00, 0x0F, 0xFF, 0xC0, + 0x00, 0x0F, 0x80, 0x7C, 0x00, 0x07, 0x00, 0x03, 0x80, 0x03, 0x80, 0x00, + 0x70, 0x01, 0x80, 0x00, 0x06, 0x00, 0xC0, 0x3F, 0xC0, 0xC0, 0x60, 0x3F, + 0xFC, 0x18, 0x38, 0x3F, 0xFF, 0x07, 0x0C, 0x1F, 0x80, 0xC0, 0xC6, 0x07, + 0xC0, 0x00, 0x19, 0x83, 0xE0, 0x00, 0x06, 0x60, 0xF0, 0x00, 0x01, 0xB0, + 0x7C, 0x00, 0x00, 0x3C, 0x1E, 0x00, 0x00, 0x0F, 0x07, 0x80, 0x00, 0x03, + 0xC1, 0xE0, 0x00, 0x00, 0xF0, 0x78, 0x00, 0x00, 0x3C, 0x1E, 0x00, 0x00, + 0x0F, 0x07, 0x80, 0x00, 0x03, 0xC1, 0xF0, 0x00, 0x00, 0xD8, 0x3C, 0x00, + 0x00, 0x66, 0x0F, 0x80, 0x00, 0x19, 0x81, 0xF0, 0x00, 0x06, 0x30, 0x7E, + 0x03, 0x03, 0x0E, 0x0F, 0xFF, 0xC1, 0xC1, 0x80, 0xFF, 0xF0, 0x60, 0x30, + 0x0F, 0xF0, 0x30, 0x06, 0x00, 0x00, 0x18, 0x00, 0xE0, 0x00, 0x1C, 0x00, + 0x1C, 0x00, 0x0E, 0x00, 0x03, 0xE0, 0x1F, 0x00, 0x00, 0x3F, 0xFF, 0x00, + 0x00, 0x01, 0xFE, 0x00, 0x00, 0x00, 0x20, 0x08, 0x03, 0x00, 0xC0, 0x38, + 0x0E, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x07, + 0xC1, 0xF0, 0x7C, 0x1F, 0x07, 0xC1, 0xF0, 0x3C, 0x0F, 0x01, 0xF0, 0x7C, + 0x07, 0xC1, 0xF0, 0x1F, 0x07, 0xC0, 0x3C, 0x0F, 0x00, 0xF0, 0x3C, 0x03, + 0xC0, 0xF0, 0x0F, 0x03, 0xC0, 0x38, 0x0E, 0x00, 0xC0, 0x30, 0x02, 0x00, + 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x07, 0x80, 0x00, 0x00, 0x3C, 0x00, + 0x00, 0x01, 0xE0, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, + 0x03, 0xC0, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x07, + 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x07, 0xF8, 0x00, 0x00, + 0x0F, 0xFF, 0xC0, 0x00, 0x0F, 0x80, 0x7C, 0x00, 0x07, 0x00, 0x03, 0x80, + 0x03, 0x80, 0x00, 0x70, 0x01, 0x80, 0x00, 0x06, 0x00, 0xC3, 0xFF, 0x80, + 0xC0, 0x60, 0xFF, 0xF8, 0x18, 0x38, 0x3C, 0x0F, 0x07, 0x0C, 0x0F, 0x01, + 0xE0, 0xC6, 0x03, 0xC0, 0x78, 0x19, 0x80, 0xF0, 0x1E, 0x06, 0x60, 0x3C, + 0x07, 0x81, 0xB0, 0x0F, 0x01, 0xE0, 0x3C, 0x03, 0xC0, 0xF0, 0x0F, 0x00, + 0xFF, 0xF8, 0x03, 0xC0, 0x3F, 0xF0, 0x00, 0xF0, 0x0F, 0x1E, 0x00, 0x3C, + 0x03, 0xC3, 0xC0, 0x0F, 0x00, 0xF0, 0x78, 0x03, 0xC0, 0x3C, 0x1E, 0x00, + 0xD8, 0x0F, 0x07, 0xC0, 0x66, 0x03, 0xC0, 0xF0, 0x19, 0x80, 0xF0, 0x3E, + 0x06, 0x30, 0x3C, 0x07, 0x83, 0x0E, 0x0F, 0x01, 0xF1, 0xC1, 0x83, 0xC0, + 0x3C, 0x60, 0x30, 0xF0, 0x0F, 0xB0, 0x06, 0x00, 0x00, 0x18, 0x00, 0xE0, + 0x00, 0x1C, 0x00, 0x1C, 0x00, 0x0E, 0x00, 0x03, 0xE0, 0x1F, 0x00, 0x00, + 0x3F, 0xFF, 0x00, 0x00, 0x01, 0xFE, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x07, 0xC0, 0x3F, 0xE0, + 0xFF, 0xE3, 0xC1, 0xE7, 0x01, 0xDC, 0x01, 0xF8, 0x03, 0xF0, 0x07, 0xE0, + 0x0F, 0xC0, 0x1D, 0xC0, 0x73, 0xC1, 0xE3, 0xFF, 0x83, 0xFE, 0x01, 0xF0, + 0x00, 0x00, 0x07, 0x80, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x78, 0x00, + 0x00, 0x01, 0xE0, 0x00, 0x00, 0x07, 0x80, 0x00, 0x00, 0x1E, 0x00, 0x00, + 0x00, 0x78, 0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x07, 0x80, 0x03, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFC, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x01, 0xE0, + 0x00, 0x00, 0x07, 0x80, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x78, 0x00, + 0x00, 0x01, 0xE0, 0x00, 0x00, 0x07, 0x80, 0x00, 0x00, 0x1E, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x3F, 0xC3, 0xFF, 0xCF, 0xFF, 0xB0, + 0x1F, 0x00, 0x3C, 0x00, 0x70, 0x01, 0xC0, 0x0E, 0x00, 0x78, 0x03, 0xC0, + 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x03, 0xE0, 0x1E, 0x00, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xC0, 0x1F, 0xC1, 0xFF, 0xC7, 0xFF, 0x98, 0x0F, 0x00, + 0x1C, 0x00, 0x70, 0x03, 0x83, 0xFE, 0x0F, 0xE0, 0x3F, 0xE0, 0x07, 0x80, + 0x07, 0x00, 0x1C, 0x00, 0x70, 0x03, 0xF0, 0x1F, 0xFF, 0xFB, 0xFF, 0xC3, + 0xFC, 0x00, 0x03, 0xE0, 0xF8, 0x1E, 0x07, 0x81, 0xE0, 0x78, 0x0F, 0x03, + 0xC0, 0xF0, 0x00, 0x00, 0x3E, 0x00, 0xF8, 0x01, 0xE0, 0x07, 0x80, 0x1E, + 0x00, 0x78, 0x00, 0xF0, 0x03, 0xC0, 0x0F, 0x00, 0x00, 0x03, 0xE1, 0xF7, + 0xC3, 0xEF, 0x87, 0xDF, 0x0F, 0xBE, 0x1F, 0x00, 0x00, 0xF8, 0x00, 0x00, + 0x01, 0xE0, 0x00, 0x00, 0x07, 0x80, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, + 0x7C, 0x7C, 0x00, 0x00, 0xF1, 0xFC, 0x00, 0x03, 0xC3, 0xF8, 0x00, 0x0F, + 0x07, 0xF0, 0x00, 0x1C, 0x1F, 0xF0, 0x00, 0x00, 0x3D, 0xE0, 0x00, 0x00, + 0xFB, 0xE0, 0x00, 0x01, 0xE3, 0xC0, 0x00, 0x03, 0xC7, 0x80, 0x00, 0x0F, + 0x8F, 0x80, 0x00, 0x1E, 0x0F, 0x00, 0x00, 0x3C, 0x1E, 0x00, 0x00, 0xF0, + 0x1E, 0x00, 0x01, 0xE0, 0x3C, 0x00, 0x07, 0xC0, 0x7C, 0x00, 0x0F, 0x00, + 0x78, 0x00, 0x1E, 0x00, 0xF0, 0x00, 0x7C, 0x01, 0xF0, 0x00, 0xF0, 0x01, + 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x07, 0x80, 0x03, 0xC0, 0x0F, 0xFF, 0xFF, + 0x80, 0x3F, 0xFF, 0xFF, 0x80, 0x7F, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFE, + 0x03, 0xE0, 0x00, 0x3E, 0x07, 0x80, 0x00, 0x3C, 0x1F, 0x00, 0x00, 0x7C, + 0x3C, 0x00, 0x00, 0x78, 0x78, 0x00, 0x00, 0xF1, 0xF0, 0x00, 0x01, 0xF3, + 0xC0, 0x00, 0x01, 0xE7, 0x80, 0x00, 0x03, 0xDE, 0x00, 0x00, 0x03, 0xC0, + 0xFF, 0xFF, 0xFF, 0x03, 0xC0, 0x00, 0x00, 0x0F, 0x80, 0x00, 0x00, 0x1E, + 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x01, 0xE7, 0xFF, 0xFF, 0xE7, 0x8F, + 0xFF, 0xFF, 0xCF, 0x1F, 0xFF, 0xFF, 0xBC, 0x3F, 0xFF, 0xFF, 0xF0, 0x78, + 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x03, 0xC0, + 0x00, 0x00, 0x07, 0x80, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x1E, 0x00, + 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xF0, 0x00, + 0x00, 0x01, 0xFF, 0xFF, 0xF0, 0x03, 0xFF, 0xFF, 0xE0, 0x07, 0xFF, 0xFF, + 0xC0, 0x0F, 0xFF, 0xFF, 0x80, 0x1E, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, + 0x00, 0x78, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, + 0x03, 0xC0, 0x00, 0x00, 0x07, 0x80, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, + 0x1E, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, + 0xF0, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0xF8, 0x03, 0xFF, 0xFF, 0xF0, 0x07, + 0xFF, 0xFF, 0xE0, 0x0F, 0xFF, 0xFF, 0xC0, 0x03, 0xE0, 0x00, 0x00, 0x00, + 0x78, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x07, 0x80, 0x00, 0x00, + 0x01, 0xE7, 0x80, 0x00, 0x1E, 0x3C, 0xF0, 0x00, 0x03, 0xCF, 0x1E, 0x00, + 0x00, 0x7B, 0xC3, 0xC0, 0x00, 0x0F, 0x70, 0x78, 0x00, 0x01, 0xE0, 0x0F, + 0x00, 0x00, 0x3C, 0x01, 0xE0, 0x00, 0x07, 0x80, 0x3C, 0x00, 0x00, 0xF0, + 0x07, 0x80, 0x00, 0x1E, 0x00, 0xF0, 0x00, 0x03, 0xC0, 0x1E, 0x00, 0x00, + 0x78, 0x03, 0xC0, 0x00, 0x0F, 0x00, 0x78, 0x00, 0x01, 0xE0, 0x0F, 0x00, + 0x00, 0x3C, 0x01, 0xFF, 0xFF, 0xFF, 0x80, 0x3F, 0xFF, 0xFF, 0xF0, 0x07, + 0xFF, 0xFF, 0xFE, 0x00, 0xFF, 0xFF, 0xFF, 0xC0, 0x1E, 0x00, 0x00, 0x78, + 0x03, 0xC0, 0x00, 0x0F, 0x00, 0x78, 0x00, 0x01, 0xE0, 0x0F, 0x00, 0x00, + 0x3C, 0x01, 0xE0, 0x00, 0x07, 0x80, 0x3C, 0x00, 0x00, 0xF0, 0x07, 0x80, + 0x00, 0x1E, 0x00, 0xF0, 0x00, 0x03, 0xC0, 0x1E, 0x00, 0x00, 0x78, 0x03, + 0xC0, 0x00, 0x0F, 0x00, 0x78, 0x00, 0x01, 0xE0, 0x0F, 0x00, 0x00, 0x3C, + 0x01, 0xE0, 0x00, 0x07, 0x80, 0x3C, 0x00, 0x00, 0xF0, 0x07, 0x80, 0x00, + 0x1E, 0x00, 0xF0, 0x00, 0x03, 0xC0, 0x03, 0xE0, 0x3E, 0x01, 0xE0, 0x1E, + 0x01, 0xE7, 0x8E, 0x3C, 0xF1, 0xEF, 0x0F, 0xF0, 0x78, 0x03, 0xC0, 0x1E, + 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, + 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, + 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, + 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x80, 0x20, 0x06, 0x01, + 0x80, 0x38, 0x0E, 0x01, 0xE0, 0x78, 0x07, 0x81, 0xE0, 0x1E, 0x07, 0x80, + 0x78, 0x1E, 0x01, 0xF0, 0x7C, 0x07, 0xC1, 0xF0, 0x1F, 0x07, 0xC0, 0x78, + 0x1E, 0x07, 0xC1, 0xF0, 0x7C, 0x1F, 0x07, 0xC1, 0xF0, 0x78, 0x1E, 0x07, + 0x81, 0xE0, 0x78, 0x1E, 0x07, 0x81, 0xE0, 0x38, 0x0E, 0x01, 0x80, 0x60, + 0x08, 0x02, 0x00, 0x00, 0x03, 0xE0, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, + 0x00, 0x07, 0x80, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0xF0, 0x00, 0x1E, 0x07, + 0xFF, 0xE0, 0x01, 0xE1, 0xFF, 0xFF, 0x80, 0x3C, 0x3F, 0xFF, 0xFC, 0x07, + 0x87, 0xF0, 0x0F, 0xE0, 0x70, 0xFC, 0x00, 0x3F, 0x00, 0x1F, 0x00, 0x01, + 0xF8, 0x03, 0xE0, 0x00, 0x0F, 0x80, 0x3E, 0x00, 0x00, 0x7C, 0x07, 0xC0, + 0x00, 0x03, 0xC0, 0x78, 0x00, 0x00, 0x3E, 0x07, 0x80, 0x00, 0x01, 0xE0, + 0xF8, 0x00, 0x00, 0x1E, 0x0F, 0x00, 0x00, 0x01, 0xF0, 0xF0, 0x00, 0x00, + 0x0F, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0xF0, 0x00, 0x00, 0x0F, 0x0F, 0x00, + 0x00, 0x00, 0xF0, 0xF0, 0x00, 0x00, 0x0F, 0x0F, 0x00, 0x00, 0x00, 0xF0, + 0xF0, 0x00, 0x00, 0x0F, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0xF0, 0x00, 0x00, + 0x1F, 0x0F, 0x80, 0x00, 0x01, 0xE0, 0x78, 0x00, 0x00, 0x1E, 0x07, 0x80, + 0x00, 0x03, 0xE0, 0x7C, 0x00, 0x00, 0x3C, 0x03, 0xE0, 0x00, 0x07, 0xC0, + 0x3E, 0x00, 0x00, 0xF8, 0x01, 0xF0, 0x00, 0x1F, 0x80, 0x0F, 0xC0, 0x03, + 0xF0, 0x00, 0x7F, 0x00, 0xFE, 0x00, 0x03, 0xFF, 0xFF, 0xC0, 0x00, 0x1F, + 0xFF, 0xF8, 0x00, 0x00, 0x7F, 0xFE, 0x00, 0x00, 0x01, 0xFF, 0x00, 0x00, + 0x1F, 0x00, 0x00, 0x1E, 0x01, 0xFE, 0x00, 0x00, 0x78, 0x03, 0xFC, 0x00, + 0x00, 0xE0, 0x07, 0x38, 0x00, 0x03, 0xC0, 0x00, 0x70, 0x00, 0x07, 0x00, + 0x00, 0xE0, 0x00, 0x1E, 0x00, 0x01, 0xC0, 0x00, 0x78, 0x00, 0x03, 0x80, + 0x00, 0xE0, 0x00, 0x07, 0x00, 0x03, 0xC0, 0x00, 0x0E, 0x00, 0x07, 0x00, + 0x00, 0x1C, 0x00, 0x1E, 0x00, 0x00, 0x38, 0x00, 0x78, 0x00, 0x00, 0x70, + 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x03, 0xC0, 0x00, 0x01, 0xC0, 0x07, 0x00, + 0x00, 0x03, 0x80, 0x1E, 0x0F, 0xF0, 0xFF, 0xF8, 0x78, 0x7F, 0xF9, 0xFF, + 0xF0, 0xE0, 0xFF, 0xFB, 0xFF, 0xE3, 0xC1, 0x80, 0xF0, 0x00, 0x07, 0x00, + 0x00, 0xF0, 0x00, 0x1E, 0x00, 0x00, 0xE0, 0x00, 0x78, 0x00, 0x01, 0x80, + 0x00, 0xE0, 0x00, 0x07, 0x00, 0x03, 0xC0, 0x00, 0x1C, 0x00, 0x07, 0x00, + 0x00, 0x78, 0x00, 0x1E, 0x00, 0x01, 0xE0, 0x00, 0x38, 0x00, 0x07, 0x80, + 0x00, 0xE0, 0x00, 0x1E, 0x00, 0x03, 0xC0, 0x00, 0xF8, 0x00, 0x07, 0x00, + 0x03, 0xE0, 0x00, 0x1E, 0x00, 0x1F, 0x00, 0x00, 0x38, 0x00, 0x3F, 0xFF, + 0x00, 0xE0, 0x00, 0x7F, 0xFE, 0x03, 0xC0, 0x00, 0xFF, 0xFC, 0x07, 0x00, + 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x00, 0x03, 0xC0, 0x00, 0x00, + 0x00, 0x1F, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x03, 0xC0, + 0x00, 0x00, 0x00, 0x1E, 0x3E, 0x00, 0x00, 0x78, 0xF0, 0x7C, 0x00, 0x03, + 0xE3, 0x81, 0xF0, 0x00, 0x1F, 0x1E, 0x03, 0xE0, 0x00, 0x78, 0xF0, 0x07, + 0xC0, 0x03, 0xE0, 0x00, 0x1F, 0x00, 0x1F, 0x00, 0x00, 0x3E, 0x00, 0x78, + 0x00, 0x00, 0x7C, 0x03, 0xE0, 0x00, 0x00, 0xF0, 0x1F, 0x00, 0x00, 0x03, + 0xE0, 0x78, 0x00, 0x00, 0x07, 0xC3, 0xE0, 0x00, 0x00, 0x0F, 0x1F, 0x00, + 0x00, 0x00, 0x3E, 0x78, 0x00, 0x00, 0x00, 0x7F, 0xE0, 0x00, 0x00, 0x00, + 0xFF, 0x00, 0x00, 0x00, 0x03, 0xF8, 0x00, 0x00, 0x00, 0x07, 0xE0, 0x00, + 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x00, + 0xF0, 0x00, 0x00, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, + 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x03, + 0xC0, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, + 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x00, 0x0F, + 0x00, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, + 0x00, 0x03, 0xC0, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x03, 0xC0, 0x00, + 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x0F, 0x80, 0x00, 0x00, 0x00, 0xF0, + 0x0F, 0xF8, 0x00, 0x1E, 0x07, 0xFF, 0xE0, 0x03, 0xC1, 0xFF, 0xFF, 0x80, + 0x78, 0x3F, 0xFF, 0xFE, 0x07, 0x07, 0xF8, 0x0F, 0xF0, 0xF0, 0xFC, 0x00, + 0x3F, 0x80, 0x1F, 0x80, 0x00, 0xF8, 0x03, 0xE0, 0x00, 0x07, 0xC0, 0x3E, + 0x00, 0x00, 0x3E, 0x07, 0xC0, 0x00, 0x01, 0xE0, 0x78, 0x00, 0x00, 0x1E, + 0x07, 0x80, 0x00, 0x01, 0xF0, 0x78, 0x00, 0x00, 0x0F, 0x0F, 0x00, 0x00, + 0x00, 0xF0, 0xF0, 0x00, 0x00, 0x0F, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0xF0, + 0x00, 0x00, 0x0F, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0xF0, 0x00, 0x00, 0x0F, + 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x78, 0x00, 0x00, 0x0F, 0x07, 0x80, 0x00, + 0x01, 0xF0, 0x78, 0x00, 0x00, 0x1E, 0x03, 0xC0, 0x00, 0x01, 0xE0, 0x3C, + 0x00, 0x00, 0x3E, 0x01, 0xE0, 0x00, 0x07, 0xC0, 0x1F, 0x00, 0x00, 0x78, + 0x00, 0xF8, 0x00, 0x0F, 0x80, 0x07, 0xC0, 0x01, 0xF0, 0x00, 0x3E, 0x00, + 0x3E, 0x00, 0x01, 0xF0, 0x0F, 0xC0, 0x0F, 0xFF, 0x81, 0xFF, 0xF0, 0xFF, + 0xF8, 0x1F, 0xFF, 0x0F, 0xFF, 0x81, 0xFF, 0xF0, 0xFF, 0xF8, 0x1F, 0xFF, + 0x00, 0x3E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x3C, 0x00, + 0xF0, 0x03, 0xC0, 0x07, 0x00, 0x00, 0x03, 0xE1, 0xF7, 0xC3, 0xEF, 0x87, + 0xDF, 0x0F, 0xBE, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xF0, 0x01, 0xE0, 0x03, 0xC0, 0x07, 0x80, 0x0F, 0x00, 0x1E, + 0x00, 0x3C, 0x00, 0x78, 0x00, 0xF0, 0x01, 0xE0, 0x03, 0xC0, 0x07, 0x80, + 0x0F, 0x00, 0x1E, 0x00, 0x3C, 0x00, 0x78, 0x00, 0xF0, 0x01, 0xE0, 0x03, + 0xC0, 0x03, 0xC0, 0x07, 0x80, 0x0F, 0x80, 0x1F, 0xF0, 0x1F, 0xE0, 0x1F, + 0xC0, 0x0F, 0x80, 0x00, 0x07, 0xC0, 0x00, 0x00, 0x1F, 0xC0, 0x00, 0x00, + 0x3F, 0x80, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x01, 0xFF, 0x00, 0x00, 0x03, + 0xDE, 0x00, 0x00, 0x0F, 0xBE, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x3C, + 0x78, 0x00, 0x00, 0xF8, 0xF8, 0x00, 0x01, 0xE0, 0xF0, 0x00, 0x03, 0xC1, + 0xE0, 0x00, 0x0F, 0x01, 0xE0, 0x00, 0x1E, 0x03, 0xC0, 0x00, 0x7C, 0x07, + 0xC0, 0x00, 0xF0, 0x07, 0x80, 0x01, 0xE0, 0x0F, 0x00, 0x07, 0xC0, 0x1F, + 0x00, 0x0F, 0x00, 0x1E, 0x00, 0x3E, 0x00, 0x3E, 0x00, 0x78, 0x00, 0x3C, + 0x00, 0xFF, 0xFF, 0xF8, 0x03, 0xFF, 0xFF, 0xF8, 0x07, 0xFF, 0xFF, 0xF0, + 0x0F, 0xFF, 0xFF, 0xE0, 0x3E, 0x00, 0x03, 0xE0, 0x78, 0x00, 0x03, 0xC1, + 0xF0, 0x00, 0x07, 0xC3, 0xC0, 0x00, 0x07, 0x87, 0x80, 0x00, 0x0F, 0x1F, + 0x00, 0x00, 0x1F, 0x3C, 0x00, 0x00, 0x1E, 0x78, 0x00, 0x00, 0x3D, 0xE0, + 0x00, 0x00, 0x3C, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xE0, 0xFF, 0xFF, 0xF8, + 0xFF, 0xFF, 0xF8, 0xF0, 0x01, 0xFC, 0xF0, 0x00, 0x7E, 0xF0, 0x00, 0x3E, + 0xF0, 0x00, 0x1E, 0xF0, 0x00, 0x1E, 0xF0, 0x00, 0x1E, 0xF0, 0x00, 0x1E, + 0xF0, 0x00, 0x3E, 0xF0, 0x00, 0x7C, 0xF0, 0x01, 0xFC, 0xFF, 0xFF, 0xF8, + 0xFF, 0xFF, 0xE0, 0xFF, 0xFF, 0xF0, 0xFF, 0xFF, 0xF8, 0xF0, 0x00, 0xFC, + 0xF0, 0x00, 0x3E, 0xF0, 0x00, 0x1E, 0xF0, 0x00, 0x0F, 0xF0, 0x00, 0x0F, + 0xF0, 0x00, 0x0F, 0xF0, 0x00, 0x0F, 0xF0, 0x00, 0x0F, 0xF0, 0x00, 0x0F, + 0xF0, 0x00, 0x1F, 0xF0, 0x00, 0x3E, 0xF0, 0x00, 0xFE, 0xFF, 0xFF, 0xFC, + 0xFF, 0xFF, 0xF8, 0xFF, 0xFF, 0xF0, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x78, 0x00, + 0x03, 0xC0, 0x00, 0x1E, 0x00, 0x00, 0xF0, 0x00, 0x07, 0x80, 0x00, 0x3C, + 0x00, 0x01, 0xE0, 0x00, 0x0F, 0x00, 0x00, 0x78, 0x00, 0x03, 0xC0, 0x00, + 0x1E, 0x00, 0x00, 0xF0, 0x00, 0x07, 0x80, 0x00, 0x3C, 0x00, 0x01, 0xE0, + 0x00, 0x0F, 0x00, 0x00, 0x78, 0x00, 0x03, 0xC0, 0x00, 0x1E, 0x00, 0x00, + 0xF0, 0x00, 0x07, 0x80, 0x00, 0x3C, 0x00, 0x01, 0xE0, 0x00, 0x0F, 0x00, + 0x00, 0x78, 0x00, 0x03, 0xC0, 0x00, 0x1E, 0x00, 0x00, 0xF0, 0x00, 0x07, + 0x80, 0x00, 0x00, 0x00, 0x07, 0xC0, 0x00, 0x00, 0x1F, 0xC0, 0x00, 0x00, + 0x3F, 0x80, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x01, 0xEF, 0x00, 0x00, 0x03, + 0xDE, 0x00, 0x00, 0x0F, 0xBE, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x3C, + 0x78, 0x00, 0x00, 0xF8, 0xF8, 0x00, 0x01, 0xE0, 0xF0, 0x00, 0x03, 0xC1, + 0xE0, 0x00, 0x0F, 0x01, 0xE0, 0x00, 0x1E, 0x03, 0xC0, 0x00, 0x7C, 0x07, + 0xC0, 0x00, 0xF0, 0x07, 0x80, 0x01, 0xE0, 0x0F, 0x00, 0x07, 0xC0, 0x1F, + 0x00, 0x0F, 0x00, 0x1E, 0x00, 0x3E, 0x00, 0x3E, 0x00, 0x78, 0x00, 0x3C, + 0x00, 0xF0, 0x00, 0x78, 0x03, 0xE0, 0x00, 0xF8, 0x07, 0x80, 0x00, 0xF0, + 0x0F, 0x00, 0x01, 0xE0, 0x3E, 0x00, 0x01, 0xE0, 0x78, 0x00, 0x03, 0xC1, + 0xF0, 0x00, 0x07, 0xC3, 0xC0, 0x00, 0x07, 0x87, 0x80, 0x00, 0x0F, 0x1F, + 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFE, 0x7F, 0xFF, 0xFF, 0xFD, 0xFF, + 0xFF, 0xFF, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xF0, 0x00, 0x03, 0xC0, 0x00, 0x0F, 0x00, 0x00, 0x3C, 0x00, + 0x00, 0xF0, 0x00, 0x03, 0xC0, 0x00, 0x0F, 0x00, 0x00, 0x3C, 0x00, 0x00, + 0xF0, 0x00, 0x03, 0xC0, 0x00, 0x0F, 0xFF, 0xFF, 0xBF, 0xFF, 0xFE, 0xFF, + 0xFF, 0xFB, 0xFF, 0xFF, 0xEF, 0x00, 0x00, 0x3C, 0x00, 0x00, 0xF0, 0x00, + 0x03, 0xC0, 0x00, 0x0F, 0x00, 0x00, 0x3C, 0x00, 0x00, 0xF0, 0x00, 0x03, + 0xC0, 0x00, 0x0F, 0x00, 0x00, 0x3C, 0x00, 0x00, 0xF0, 0x00, 0x03, 0xC0, + 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xF0, 0x7F, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0xFE, 0x7F, 0xFF, 0xFF, 0xE7, + 0xFF, 0xFF, 0xFE, 0x00, 0x00, 0x07, 0xC0, 0x00, 0x00, 0xF8, 0x00, 0x00, + 0x1F, 0x80, 0x00, 0x01, 0xF0, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x07, 0xC0, + 0x00, 0x00, 0xF8, 0x00, 0x00, 0x1F, 0x80, 0x00, 0x01, 0xF0, 0x00, 0x00, + 0x3E, 0x00, 0x00, 0x07, 0xC0, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x1F, 0x00, + 0x00, 0x01, 0xF0, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x07, 0xC0, 0x00, 0x00, + 0xF8, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x03, 0xF0, 0x00, 0x00, 0x3E, 0x00, + 0x00, 0x07, 0xC0, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x03, + 0xF0, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x07, 0xC0, 0x00, 0x00, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xF0, 0x00, 0x03, 0xFC, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x3F, 0xC0, 0x00, + 0x0F, 0xF0, 0x00, 0x03, 0xFC, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x3F, 0xC0, + 0x00, 0x0F, 0xF0, 0x00, 0x03, 0xFC, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x3F, + 0xC0, 0x00, 0x0F, 0xF0, 0x00, 0x03, 0xFC, 0x00, 0x00, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, + 0x00, 0x3F, 0xC0, 0x00, 0x0F, 0xF0, 0x00, 0x03, 0xFC, 0x00, 0x00, 0xFF, + 0x00, 0x00, 0x3F, 0xC0, 0x00, 0x0F, 0xF0, 0x00, 0x03, 0xFC, 0x00, 0x00, + 0xFF, 0x00, 0x00, 0x3F, 0xC0, 0x00, 0x0F, 0xF0, 0x00, 0x03, 0xFC, 0x00, + 0x00, 0xFF, 0x00, 0x00, 0x3F, 0xC0, 0x00, 0x0F, 0xF0, 0x00, 0x03, 0xFC, + 0x00, 0x00, 0xF0, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x7F, 0xFE, 0x00, 0x01, + 0xFF, 0xFF, 0x80, 0x03, 0xFF, 0xFF, 0xC0, 0x07, 0xF0, 0x0F, 0xE0, 0x0F, + 0xC0, 0x03, 0xF0, 0x1F, 0x80, 0x01, 0xF8, 0x3F, 0x00, 0x00, 0x7C, 0x3E, + 0x00, 0x00, 0x7C, 0x7C, 0x00, 0x00, 0x3C, 0x78, 0x00, 0x00, 0x1E, 0x78, + 0x00, 0x00, 0x1E, 0x78, 0x00, 0x00, 0x1E, 0xF0, 0x00, 0x00, 0x0F, 0xF0, + 0x00, 0x00, 0x0F, 0xF0, 0xFF, 0xFF, 0x0F, 0xF0, 0xFF, 0xFF, 0x0F, 0xF0, + 0xFF, 0xFF, 0x0F, 0xF0, 0xFF, 0xFF, 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0xF0, + 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0x78, + 0x00, 0x00, 0x1E, 0x78, 0x00, 0x00, 0x1E, 0x78, 0x00, 0x00, 0x1E, 0x7C, + 0x00, 0x00, 0x3C, 0x3E, 0x00, 0x00, 0x7C, 0x3E, 0x00, 0x00, 0x7C, 0x1F, + 0x80, 0x01, 0xF8, 0x0F, 0xC0, 0x03, 0xF0, 0x07, 0xF0, 0x0F, 0xE0, 0x03, + 0xFF, 0xFF, 0xC0, 0x01, 0xFF, 0xFF, 0x80, 0x00, 0x7F, 0xFE, 0x00, 0x00, + 0x0F, 0xF0, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x1F, 0x9E, + 0x00, 0x07, 0xE3, 0xC0, 0x01, 0xF8, 0x78, 0x00, 0x7E, 0x0F, 0x00, 0x1F, + 0x81, 0xE0, 0x07, 0xE0, 0x3C, 0x01, 0xF8, 0x07, 0x80, 0x7E, 0x00, 0xF0, + 0x1F, 0x80, 0x1E, 0x07, 0xE0, 0x03, 0xC3, 0xF0, 0x00, 0x78, 0xFC, 0x00, + 0x0F, 0x3F, 0x00, 0x01, 0xEF, 0xC0, 0x00, 0x3F, 0xF0, 0x00, 0x07, 0xFC, + 0x00, 0x00, 0xFF, 0x80, 0x00, 0x1F, 0xF8, 0x00, 0x03, 0xDF, 0x80, 0x00, + 0x79, 0xF8, 0x00, 0x0F, 0x1F, 0x80, 0x01, 0xE0, 0xF8, 0x00, 0x3C, 0x0F, + 0x80, 0x07, 0x80, 0xF8, 0x00, 0xF0, 0x0F, 0x80, 0x1E, 0x00, 0xF8, 0x03, + 0xC0, 0x0F, 0x80, 0x78, 0x00, 0xF8, 0x0F, 0x00, 0x0F, 0x81, 0xE0, 0x00, + 0xF8, 0x3C, 0x00, 0x0F, 0x87, 0x80, 0x00, 0xF8, 0xF0, 0x00, 0x0F, 0x9E, + 0x00, 0x00, 0xFC, 0x00, 0x07, 0xC0, 0x00, 0x00, 0x1F, 0xC0, 0x00, 0x00, + 0x3F, 0x80, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x01, 0xEF, 0x00, 0x00, 0x03, + 0xDE, 0x00, 0x00, 0x0F, 0xBE, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x3C, + 0x78, 0x00, 0x00, 0xF8, 0xF8, 0x00, 0x01, 0xE0, 0xF0, 0x00, 0x03, 0xC1, + 0xE0, 0x00, 0x0F, 0x01, 0xE0, 0x00, 0x1E, 0x03, 0xC0, 0x00, 0x7C, 0x07, + 0xC0, 0x00, 0xF0, 0x07, 0x80, 0x01, 0xE0, 0x0F, 0x00, 0x07, 0xC0, 0x1F, + 0x00, 0x0F, 0x00, 0x1E, 0x00, 0x3E, 0x00, 0x3E, 0x00, 0x78, 0x00, 0x3C, + 0x00, 0xF0, 0x00, 0x78, 0x03, 0xE0, 0x00, 0xF8, 0x07, 0x80, 0x00, 0xF0, + 0x0F, 0x00, 0x01, 0xE0, 0x3E, 0x00, 0x03, 0xE0, 0x78, 0x00, 0x03, 0xC1, + 0xF0, 0x00, 0x07, 0xC3, 0xC0, 0x00, 0x07, 0x87, 0x80, 0x00, 0x0F, 0x1F, + 0x00, 0x00, 0x1F, 0x3C, 0x00, 0x00, 0x1E, 0x78, 0x00, 0x00, 0x3D, 0xE0, + 0x00, 0x00, 0x3C, 0xFE, 0x00, 0x00, 0xFF, 0xFC, 0x00, 0x01, 0xFF, 0xFC, + 0x00, 0x07, 0xFF, 0xF8, 0x00, 0x0F, 0xFF, 0xF0, 0x00, 0x1F, 0xFE, 0xF0, + 0x00, 0x7B, 0xFD, 0xE0, 0x00, 0xF7, 0xFB, 0xE0, 0x03, 0xEF, 0xF3, 0xC0, + 0x07, 0x9F, 0xE7, 0x80, 0x0F, 0x3F, 0xC7, 0x80, 0x3C, 0x7F, 0x8F, 0x00, + 0x78, 0xFF, 0x1F, 0x01, 0xF1, 0xFE, 0x1E, 0x03, 0xC3, 0xFC, 0x3C, 0x07, + 0x87, 0xF8, 0x3C, 0x1E, 0x0F, 0xF0, 0x78, 0x3C, 0x1F, 0xE0, 0xF8, 0xF8, + 0x3F, 0xC0, 0xF1, 0xE0, 0x7F, 0x81, 0xE3, 0xC0, 0xFF, 0x01, 0xEF, 0x01, + 0xFE, 0x03, 0xDE, 0x03, 0xFC, 0x07, 0xFC, 0x07, 0xF8, 0x07, 0xF0, 0x0F, + 0xF0, 0x0F, 0xE0, 0x1F, 0xE0, 0x1F, 0xC0, 0x3F, 0xC0, 0x1F, 0x00, 0x7F, + 0x80, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x01, 0xFE, 0x00, 0x00, 0x03, 0xFC, + 0x00, 0x00, 0x07, 0xF8, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x1F, 0xE0, + 0x00, 0x00, 0x3C, 0xFC, 0x00, 0x03, 0xFF, 0x80, 0x00, 0xFF, 0xE0, 0x00, + 0x3F, 0xFC, 0x00, 0x0F, 0xFF, 0x00, 0x03, 0xFF, 0xE0, 0x00, 0xFF, 0x78, + 0x00, 0x3F, 0xDF, 0x00, 0x0F, 0xF3, 0xE0, 0x03, 0xFC, 0x78, 0x00, 0xFF, + 0x1F, 0x00, 0x3F, 0xC3, 0xC0, 0x0F, 0xF0, 0xF8, 0x03, 0xFC, 0x1E, 0x00, + 0xFF, 0x07, 0xC0, 0x3F, 0xC0, 0xF0, 0x0F, 0xF0, 0x3E, 0x03, 0xFC, 0x07, + 0xC0, 0xFF, 0x00, 0xF0, 0x3F, 0xC0, 0x3E, 0x0F, 0xF0, 0x07, 0x83, 0xFC, + 0x01, 0xF0, 0xFF, 0x00, 0x3C, 0x3F, 0xC0, 0x0F, 0x8F, 0xF0, 0x01, 0xE3, + 0xFC, 0x00, 0x7C, 0xFF, 0x00, 0x0F, 0xBF, 0xC0, 0x01, 0xEF, 0xF0, 0x00, + 0x7F, 0xFC, 0x00, 0x0F, 0xFF, 0x00, 0x03, 0xFF, 0xC0, 0x00, 0x7F, 0xF0, + 0x00, 0x1F, 0xFC, 0x00, 0x03, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x87, 0xFF, 0xFC, + 0x3F, 0xFF, 0xE1, 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, + 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, + 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x7F, 0xFE, 0x00, 0x01, 0xFF, 0xFF, 0x80, + 0x03, 0xFF, 0xFF, 0xC0, 0x07, 0xF0, 0x0F, 0xE0, 0x0F, 0xC0, 0x03, 0xF0, + 0x1F, 0x80, 0x01, 0xF8, 0x3E, 0x00, 0x00, 0x7C, 0x3E, 0x00, 0x00, 0x7C, + 0x7C, 0x00, 0x00, 0x3C, 0x78, 0x00, 0x00, 0x3E, 0x78, 0x00, 0x00, 0x1E, + 0x78, 0x00, 0x00, 0x1E, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, + 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, + 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, + 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0x78, 0x00, 0x00, 0x1E, + 0x78, 0x00, 0x00, 0x1E, 0x78, 0x00, 0x00, 0x1E, 0x7C, 0x00, 0x00, 0x3C, + 0x3E, 0x00, 0x00, 0x7C, 0x3E, 0x00, 0x00, 0x7C, 0x1F, 0x80, 0x01, 0xF8, + 0x0F, 0xC0, 0x03, 0xF0, 0x07, 0xF0, 0x0F, 0xE0, 0x03, 0xFF, 0xFF, 0xC0, + 0x01, 0xFF, 0xFF, 0x80, 0x00, 0x7F, 0xFE, 0x00, 0x00, 0x0F, 0xF0, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xF0, 0x00, 0x03, 0xFC, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x3F, 0xC0, + 0x00, 0x0F, 0xF0, 0x00, 0x03, 0xFC, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x3F, + 0xC0, 0x00, 0x0F, 0xF0, 0x00, 0x03, 0xFC, 0x00, 0x00, 0xFF, 0x00, 0x00, + 0x3F, 0xC0, 0x00, 0x0F, 0xF0, 0x00, 0x03, 0xFC, 0x00, 0x00, 0xFF, 0x00, + 0x00, 0x3F, 0xC0, 0x00, 0x0F, 0xF0, 0x00, 0x03, 0xFC, 0x00, 0x00, 0xFF, + 0x00, 0x00, 0x3F, 0xC0, 0x00, 0x0F, 0xF0, 0x00, 0x03, 0xFC, 0x00, 0x00, + 0xFF, 0x00, 0x00, 0x3F, 0xC0, 0x00, 0x0F, 0xF0, 0x00, 0x03, 0xFC, 0x00, + 0x00, 0xFF, 0x00, 0x00, 0x3F, 0xC0, 0x00, 0x0F, 0xF0, 0x00, 0x03, 0xFC, + 0x00, 0x00, 0xF0, 0xFF, 0xFE, 0x03, 0xFF, 0xFE, 0x0F, 0xFF, 0xFE, 0x3F, + 0xFF, 0xFC, 0xF0, 0x03, 0xFB, 0xC0, 0x03, 0xEF, 0x00, 0x07, 0xBC, 0x00, + 0x1F, 0xF0, 0x00, 0x3F, 0xC0, 0x00, 0xFF, 0x00, 0x03, 0xFC, 0x00, 0x0F, + 0xF0, 0x00, 0x3F, 0xC0, 0x01, 0xFF, 0x00, 0x07, 0xBC, 0x00, 0x3E, 0xF0, + 0x03, 0xFB, 0xFF, 0xFF, 0xCF, 0xFF, 0xFE, 0x3F, 0xFF, 0xE0, 0xFF, 0xFE, + 0x03, 0xC0, 0x00, 0x0F, 0x00, 0x00, 0x3C, 0x00, 0x00, 0xF0, 0x00, 0x03, + 0xC0, 0x00, 0x0F, 0x00, 0x00, 0x3C, 0x00, 0x00, 0xF0, 0x00, 0x03, 0xC0, + 0x00, 0x0F, 0x00, 0x00, 0x3C, 0x00, 0x00, 0xF0, 0x00, 0x03, 0xC0, 0x00, + 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFC, 0x00, 0x01, 0xF8, 0x00, 0x03, 0xF0, 0x00, 0x07, 0xE0, 0x00, 0x0F, + 0xC0, 0x00, 0x1F, 0x80, 0x00, 0x3E, 0x00, 0x00, 0x7C, 0x00, 0x00, 0xF8, + 0x00, 0x01, 0xF0, 0x00, 0x03, 0xE0, 0x00, 0x07, 0xC0, 0x00, 0x1F, 0x00, + 0x00, 0xF8, 0x00, 0x07, 0xC0, 0x00, 0x3E, 0x00, 0x01, 0xF8, 0x00, 0x07, + 0xC0, 0x00, 0x3E, 0x00, 0x01, 0xF0, 0x00, 0x0F, 0x80, 0x00, 0x3E, 0x00, + 0x01, 0xF0, 0x00, 0x0F, 0x80, 0x00, 0x7C, 0x00, 0x03, 0xE0, 0x00, 0x0F, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x00, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x0F, 0x00, 0x00, + 0x00, 0xF0, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x0F, + 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x00, + 0x00, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, + 0xF0, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x0F, 0x00, + 0x00, 0x00, 0xF0, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, + 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0xF0, + 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x0F, 0x00, 0x00, + 0x00, 0xF0, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x00, 0xF8, 0x00, + 0x01, 0xF7, 0xC0, 0x00, 0x3E, 0x3E, 0x00, 0x07, 0xC3, 0xE0, 0x00, 0x7C, + 0x1F, 0x00, 0x0F, 0x80, 0xF8, 0x01, 0xF0, 0x0F, 0x80, 0x1F, 0x00, 0x7C, + 0x03, 0xE0, 0x03, 0xE0, 0x7C, 0x00, 0x3E, 0x07, 0xC0, 0x01, 0xF0, 0xF8, + 0x00, 0x0F, 0x9F, 0x00, 0x00, 0xF9, 0xF0, 0x00, 0x07, 0xFE, 0x00, 0x00, + 0x3F, 0xC0, 0x00, 0x03, 0xFC, 0x00, 0x00, 0x1F, 0x80, 0x00, 0x00, 0xF0, + 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x0F, 0x00, 0x00, + 0x00, 0xF0, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x0F, + 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x00, + 0x00, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, + 0xF0, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x03, 0xC0, + 0x00, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x03, 0xC0, + 0x00, 0x00, 0x1F, 0xF8, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x03, 0xFF, 0xFF, + 0xC0, 0x0F, 0xFF, 0xFF, 0xF0, 0x1F, 0xE3, 0xC7, 0xF8, 0x3F, 0x83, 0xC1, + 0xFC, 0x3E, 0x03, 0xC0, 0x7C, 0x7C, 0x03, 0xC0, 0x3E, 0x78, 0x03, 0xC0, + 0x1E, 0xF8, 0x03, 0xC0, 0x1F, 0xF0, 0x03, 0xC0, 0x0F, 0xF0, 0x03, 0xC0, + 0x0F, 0xF0, 0x03, 0xC0, 0x0F, 0xF0, 0x03, 0xC0, 0x0F, 0xF0, 0x03, 0xC0, + 0x0F, 0xF0, 0x03, 0xC0, 0x0F, 0xF8, 0x03, 0xC0, 0x1F, 0x78, 0x03, 0xC0, + 0x1E, 0x7C, 0x03, 0xC0, 0x3E, 0x3E, 0x03, 0xC0, 0x7C, 0x3F, 0x83, 0xC1, + 0xFC, 0x1F, 0xE3, 0xC7, 0xF8, 0x0F, 0xFF, 0xFF, 0xF0, 0x03, 0xFF, 0xFF, + 0xC0, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x1F, 0xF8, 0x00, 0x00, 0x03, 0xC0, + 0x00, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x03, 0xC0, + 0x00, 0x3E, 0x00, 0x01, 0xF0, 0xF8, 0x00, 0x1F, 0x03, 0xC0, 0x01, 0xF0, + 0x1F, 0x00, 0x0F, 0x80, 0x7C, 0x00, 0xF8, 0x01, 0xE0, 0x0F, 0x80, 0x0F, + 0x80, 0x7C, 0x00, 0x3E, 0x07, 0xC0, 0x00, 0xF0, 0x7C, 0x00, 0x07, 0xC3, + 0xE0, 0x00, 0x1F, 0x3E, 0x00, 0x00, 0xFB, 0xE0, 0x00, 0x03, 0xFF, 0x00, + 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x01, 0xF0, 0x00, 0x00, + 0x0F, 0xC0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x07, 0xF8, 0x00, 0x00, 0x7F, + 0xE0, 0x00, 0x07, 0xDF, 0x00, 0x00, 0x3C, 0x7C, 0x00, 0x03, 0xE1, 0xF0, + 0x00, 0x3E, 0x0F, 0x80, 0x03, 0xE0, 0x3E, 0x00, 0x1F, 0x00, 0xF0, 0x01, + 0xF0, 0x07, 0xC0, 0x1F, 0x00, 0x1F, 0x00, 0xF8, 0x00, 0x78, 0x0F, 0x80, + 0x03, 0xE0, 0xF8, 0x00, 0x0F, 0x87, 0xC0, 0x00, 0x3C, 0x7C, 0x00, 0x01, + 0xF7, 0xC0, 0x00, 0x07, 0xC0, 0xF0, 0x03, 0xC0, 0x0F, 0xF0, 0x03, 0xC0, + 0x0F, 0xF0, 0x03, 0xC0, 0x0F, 0xF0, 0x03, 0xC0, 0x0F, 0xF0, 0x03, 0xC0, + 0x0F, 0xF0, 0x03, 0xC0, 0x0F, 0xF0, 0x03, 0xC0, 0x0F, 0xF0, 0x03, 0xC0, + 0x0F, 0xF0, 0x03, 0xC0, 0x0F, 0xF0, 0x03, 0xC0, 0x0F, 0xF0, 0x03, 0xC0, + 0x0F, 0xF0, 0x03, 0xC0, 0x0F, 0xF0, 0x03, 0xC0, 0x0F, 0xF0, 0x03, 0xC0, + 0x0F, 0x78, 0x03, 0xC0, 0x1E, 0x78, 0x03, 0xC0, 0x1E, 0x78, 0x03, 0xC0, + 0x1E, 0x7C, 0x03, 0xC0, 0x3C, 0x3E, 0x03, 0xC0, 0x7C, 0x3E, 0x03, 0xC0, + 0x7C, 0x1F, 0x03, 0xC0, 0xF8, 0x0F, 0xC3, 0xC3, 0xF0, 0x07, 0xF3, 0xCF, + 0xE0, 0x03, 0xFF, 0xFF, 0xC0, 0x01, 0xFF, 0xFF, 0x80, 0x00, 0xFF, 0xFF, + 0x00, 0x00, 0x1F, 0xF8, 0x00, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x03, 0xC0, + 0x00, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x03, 0xC0, + 0x00, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x0F, 0xF0, + 0x00, 0x00, 0x7F, 0xFE, 0x00, 0x01, 0xFF, 0xFF, 0x80, 0x03, 0xFF, 0xFF, + 0xC0, 0x07, 0xF0, 0x0F, 0xE0, 0x0F, 0xC0, 0x03, 0xF0, 0x1F, 0x00, 0x00, + 0xF8, 0x3E, 0x00, 0x00, 0x7C, 0x3C, 0x00, 0x00, 0x3C, 0x7C, 0x00, 0x00, + 0x3E, 0x78, 0x00, 0x00, 0x1E, 0x78, 0x00, 0x00, 0x1E, 0xF0, 0x00, 0x00, + 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, + 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, + 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x1F, 0x78, 0x00, 0x00, + 0x1E, 0x78, 0x00, 0x00, 0x1E, 0x7C, 0x00, 0x00, 0x3E, 0x3C, 0x00, 0x00, + 0x3C, 0x3E, 0x00, 0x00, 0x7C, 0x1E, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, + 0xF0, 0x0F, 0x80, 0x01, 0xF0, 0x07, 0xE0, 0x07, 0xE0, 0x03, 0xF0, 0x0F, + 0xC0, 0xFF, 0xF8, 0x1F, 0xFF, 0xFF, 0xF8, 0x1F, 0xFF, 0xFF, 0xF8, 0x1F, + 0xFF, 0xFF, 0xF8, 0x1F, 0xFF, 0xF8, 0x7F, 0xE1, 0xFF, 0x87, 0xFE, 0x1F, + 0xF8, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, + 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x1E, + 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, + 0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x01, + 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, + 0x78, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x01, 0xF0, + 0xF8, 0x00, 0x1F, 0x0F, 0x80, 0x01, 0xF0, 0xF8, 0x00, 0x1F, 0x0F, 0x80, + 0x01, 0xF0, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x01, + 0xF7, 0xC0, 0x00, 0x3E, 0x3E, 0x00, 0x07, 0xC3, 0xE0, 0x00, 0x7C, 0x1F, + 0x00, 0x0F, 0x80, 0xF8, 0x01, 0xF0, 0x0F, 0x80, 0x1F, 0x00, 0x7C, 0x03, + 0xE0, 0x03, 0xE0, 0x7C, 0x00, 0x3E, 0x07, 0xC0, 0x01, 0xF0, 0xF8, 0x00, + 0x0F, 0x9F, 0x00, 0x00, 0xF9, 0xF0, 0x00, 0x07, 0xFE, 0x00, 0x00, 0x3F, + 0xC0, 0x00, 0x03, 0xFC, 0x00, 0x00, 0x1F, 0x80, 0x00, 0x00, 0xF0, 0x00, + 0x00, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, + 0xF0, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x0F, 0x00, + 0x00, 0x00, 0xF0, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, + 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0xF0, + 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x01, 0xE0, 0x00, + 0x00, 0xF0, 0x00, 0x00, 0x78, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x0F, 0x00, + 0x00, 0x07, 0x80, 0x00, 0x03, 0xC0, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x70, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xF0, 0x00, 0x07, + 0xFF, 0x07, 0x83, 0xFF, 0xE3, 0xE1, 0xFF, 0xFC, 0xF0, 0xFC, 0x3F, 0x3C, + 0x3C, 0x03, 0xDF, 0x1F, 0x00, 0xFF, 0x87, 0x80, 0x1F, 0xE1, 0xE0, 0x07, + 0xF8, 0xF8, 0x01, 0xFC, 0x3C, 0x00, 0x7F, 0x0F, 0x00, 0x0F, 0x83, 0xC0, + 0x03, 0xE0, 0xF0, 0x00, 0xF8, 0x3C, 0x00, 0x3C, 0x0F, 0x00, 0x0F, 0x03, + 0xC0, 0x03, 0xE0, 0xF0, 0x01, 0xF8, 0x3C, 0x00, 0x7E, 0x07, 0x80, 0x1F, + 0x81, 0xE0, 0x0F, 0xE0, 0x7C, 0x03, 0xFC, 0x0F, 0x81, 0xFF, 0x83, 0xF0, + 0xFB, 0xFC, 0x7F, 0xFE, 0xFF, 0x0F, 0xFF, 0x1F, 0xC1, 0xFF, 0x81, 0xF0, + 0x1F, 0xC0, 0x00, 0x00, 0x03, 0xC0, 0x00, 0xF8, 0x00, 0x3E, 0x00, 0x07, + 0x80, 0x01, 0xE0, 0x00, 0x78, 0x00, 0x0E, 0x00, 0x03, 0xC0, 0x00, 0xF0, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0x80, 0xFF, 0xFC, 0x3F, 0xFF, + 0x8F, 0xFF, 0xF3, 0xF8, 0x06, 0x7C, 0x00, 0x0F, 0x00, 0x01, 0xE0, 0x00, + 0x3C, 0x00, 0x07, 0x80, 0x00, 0x78, 0x00, 0x07, 0xC0, 0x00, 0x3F, 0xE0, + 0x0F, 0xFC, 0x03, 0xFF, 0x81, 0xFF, 0xF0, 0x3F, 0x00, 0x0F, 0x80, 0x01, + 0xE0, 0x00, 0x3C, 0x00, 0x07, 0x80, 0x00, 0xF0, 0x00, 0x1F, 0x00, 0x01, + 0xF8, 0x01, 0x9F, 0xFF, 0xF3, 0xFF, 0xFE, 0x1F, 0xFF, 0xC0, 0x7F, 0xE0, + 0x00, 0x00, 0xF0, 0x00, 0x07, 0x80, 0x00, 0x3C, 0x00, 0x01, 0xF0, 0x00, + 0x07, 0x80, 0x00, 0x3C, 0x00, 0x01, 0xE0, 0x00, 0x07, 0x00, 0x00, 0x38, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xC0, 0xF1, 0xFF, 0xC3, + 0xCF, 0xFF, 0x8F, 0x7F, 0xFF, 0x3F, 0xE0, 0x7E, 0xFE, 0x00, 0xFB, 0xF0, + 0x01, 0xEF, 0x80, 0x07, 0xFE, 0x00, 0x0F, 0xF0, 0x00, 0x3F, 0xC0, 0x00, + 0xFF, 0x00, 0x03, 0xFC, 0x00, 0x0F, 0xF0, 0x00, 0x3F, 0xC0, 0x00, 0xFF, + 0x00, 0x03, 0xFC, 0x00, 0x0F, 0xF0, 0x00, 0x3F, 0xC0, 0x00, 0xFF, 0x00, + 0x03, 0xFC, 0x00, 0x0F, 0xF0, 0x00, 0x3F, 0xC0, 0x00, 0xFF, 0x00, 0x03, + 0xFC, 0x00, 0x0F, 0xF0, 0x00, 0x3F, 0xC0, 0x00, 0xF0, 0x00, 0x03, 0xC0, + 0x00, 0x0F, 0x00, 0x00, 0x3C, 0x00, 0x00, 0xF0, 0x00, 0x03, 0xC0, 0x00, + 0x0F, 0x00, 0x00, 0x3C, 0x00, 0x00, 0xF0, 0x00, 0x03, 0xC0, 0x00, 0x0F, + 0x01, 0xE0, 0x3C, 0x07, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x1C, 0x03, 0xC0, + 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, + 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, + 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, + 0xF8, 0x07, 0xC0, 0x7F, 0xC3, 0xFC, 0x1F, 0xC0, 0xFC, 0x00, 0x0F, 0x00, + 0x00, 0xF0, 0x00, 0x0F, 0x80, 0x00, 0x78, 0x00, 0x07, 0x80, 0x00, 0x78, + 0x00, 0x07, 0x80, 0x00, 0x38, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x00, 0x0F, + 0x87, 0xC0, 0x7C, 0x3E, 0x03, 0xE1, 0xF0, 0x1F, 0x0F, 0x80, 0xF8, 0x7C, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x78, 0x01, 0xE3, 0xC0, 0x07, 0x9E, 0x00, 0x3C, + 0xF0, 0x00, 0xF7, 0x80, 0x07, 0xBC, 0x00, 0x3D, 0xE0, 0x00, 0xFF, 0x00, + 0x07, 0xF8, 0x00, 0x3F, 0xC0, 0x01, 0xFE, 0x00, 0x0F, 0xF0, 0x00, 0x7F, + 0x80, 0x03, 0xFC, 0x00, 0x1F, 0xE0, 0x01, 0xFF, 0x00, 0x0F, 0x78, 0x00, + 0x7B, 0xC0, 0x07, 0xDE, 0x00, 0x3C, 0xF8, 0x03, 0xE3, 0xC0, 0x3E, 0x1F, + 0x87, 0xF0, 0x7F, 0xFF, 0x03, 0xFF, 0xF0, 0x07, 0xFE, 0x00, 0x1F, 0xC0, + 0x00, 0x01, 0xFC, 0x00, 0x01, 0xFF, 0xC1, 0xE0, 0xFF, 0xF8, 0xF8, 0x7F, + 0xFF, 0x3C, 0x3F, 0x0F, 0xCF, 0x0F, 0x00, 0xF7, 0xC7, 0xC0, 0x3F, 0xE1, + 0xE0, 0x07, 0xF8, 0x78, 0x01, 0xFE, 0x3E, 0x00, 0x7F, 0x0F, 0x00, 0x1F, + 0xC3, 0xC0, 0x03, 0xE0, 0xF0, 0x00, 0xF8, 0x3C, 0x00, 0x3E, 0x0F, 0x00, + 0x0F, 0x03, 0xC0, 0x03, 0xC0, 0xF0, 0x00, 0xF8, 0x3C, 0x00, 0x7E, 0x0F, + 0x00, 0x1F, 0x81, 0xE0, 0x07, 0xE0, 0x78, 0x03, 0xF8, 0x1F, 0x00, 0xFF, + 0x03, 0xE0, 0x7F, 0xE0, 0xFC, 0x3E, 0xFF, 0x1F, 0xFF, 0xBF, 0xC3, 0xFF, + 0xC7, 0xF0, 0x7F, 0xE0, 0x7C, 0x07, 0xF0, 0x00, 0x03, 0xFC, 0x00, 0x3F, + 0xFC, 0x01, 0xFF, 0xFC, 0x0F, 0xFF, 0xF8, 0x7E, 0x07, 0xE1, 0xF0, 0x07, + 0xCF, 0x80, 0x0F, 0x3E, 0x00, 0x3C, 0xF0, 0x00, 0xF3, 0xC0, 0x03, 0xCF, + 0x00, 0x0F, 0x3C, 0x00, 0x3C, 0xF0, 0x01, 0xF3, 0xC0, 0x0F, 0x8F, 0x00, + 0x7E, 0x3C, 0x07, 0xF0, 0xF1, 0xFF, 0xC3, 0xC7, 0xFC, 0x0F, 0x1F, 0xFC, + 0x3C, 0x7F, 0xF8, 0xF0, 0x07, 0xF3, 0xC0, 0x03, 0xEF, 0x00, 0x07, 0xBC, + 0x00, 0x1F, 0xF0, 0x00, 0x3F, 0xC0, 0x00, 0xFF, 0x00, 0x03, 0xFC, 0x00, + 0x0F, 0xF0, 0x00, 0x3F, 0xC0, 0x01, 0xFF, 0x80, 0x07, 0xBF, 0x00, 0x3E, + 0xFF, 0x03, 0xFB, 0xFF, 0xFF, 0xCF, 0xFF, 0xFE, 0x3D, 0xFF, 0xF0, 0xF1, + 0xFE, 0x03, 0xC0, 0x00, 0x0F, 0x00, 0x00, 0x3C, 0x00, 0x00, 0xF0, 0x00, + 0x03, 0xC0, 0x00, 0x0F, 0x00, 0x00, 0x3C, 0x00, 0x00, 0xF0, 0x00, 0x03, + 0xC0, 0x00, 0x00, 0xF0, 0x00, 0x07, 0xFF, 0x00, 0x01, 0xEF, 0xE0, 0x00, + 0x7B, 0xF8, 0x00, 0x3E, 0x1F, 0x00, 0x0F, 0x03, 0xC0, 0x07, 0xC0, 0xF0, + 0x01, 0xE0, 0x1E, 0x00, 0xF8, 0x07, 0x80, 0x3C, 0x01, 0xF0, 0x0F, 0x00, + 0x3C, 0x07, 0xC0, 0x0F, 0x01, 0xE0, 0x03, 0xE0, 0xF8, 0x00, 0x78, 0x3C, + 0x00, 0x1E, 0x0F, 0x00, 0x07, 0xC7, 0x80, 0x00, 0xF1, 0xE0, 0x00, 0x3C, + 0xF8, 0x00, 0x0F, 0xBC, 0x00, 0x01, 0xFF, 0x00, 0x00, 0x7F, 0x80, 0x00, + 0x1F, 0xE0, 0x00, 0x03, 0xF0, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x3F, 0x00, + 0x00, 0x07, 0x80, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x78, 0x00, 0x00, 0x1E, + 0x00, 0x00, 0x07, 0x80, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x78, 0x00, 0x00, + 0x1E, 0x00, 0x00, 0x07, 0x80, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x78, 0x00, + 0x00, 0xFF, 0xC0, 0x03, 0xFF, 0xF0, 0x0F, 0xFF, 0xF8, 0x0F, 0xFF, 0xF8, + 0x1F, 0x80, 0x38, 0x1E, 0x00, 0x08, 0x1E, 0x00, 0x00, 0x1E, 0x00, 0x00, + 0x1F, 0x80, 0x00, 0x0F, 0xF0, 0x00, 0x0F, 0xFF, 0x00, 0x07, 0xFF, 0xC0, + 0x07, 0xFF, 0xF0, 0x1F, 0x1F, 0xF8, 0x1E, 0x01, 0xFC, 0x3C, 0x00, 0x7C, + 0x78, 0x00, 0x3E, 0x78, 0x00, 0x1E, 0x70, 0x00, 0x1F, 0xF0, 0x00, 0x0F, + 0xF0, 0x00, 0x0F, 0xF0, 0x00, 0x0F, 0xF0, 0x00, 0x0F, 0xF0, 0x00, 0x0F, + 0xF0, 0x00, 0x0F, 0xF0, 0x00, 0x0F, 0xF8, 0x00, 0x1F, 0x78, 0x00, 0x1E, + 0x78, 0x00, 0x3E, 0x7C, 0x00, 0x3E, 0x3E, 0x00, 0x7C, 0x3F, 0x81, 0xFC, + 0x1F, 0xFF, 0xF8, 0x0F, 0xFF, 0xF0, 0x03, 0xFF, 0xC0, 0x00, 0xFF, 0x00, + 0x03, 0xFF, 0x01, 0xFF, 0xF8, 0x7F, 0xFF, 0x1F, 0xFF, 0xE7, 0xF0, 0x0C, + 0xF8, 0x00, 0x1E, 0x00, 0x03, 0xC0, 0x00, 0x78, 0x00, 0x0F, 0x00, 0x00, + 0xF0, 0x00, 0x0F, 0x80, 0x00, 0x7F, 0xC0, 0x1F, 0xF8, 0x07, 0xFF, 0x03, + 0xFF, 0xE0, 0x7E, 0x00, 0x1F, 0x00, 0x03, 0xC0, 0x00, 0x78, 0x00, 0x0F, + 0x00, 0x01, 0xE0, 0x00, 0x3E, 0x00, 0x03, 0xF0, 0x03, 0x3F, 0xFF, 0xE7, + 0xFF, 0xFC, 0x3F, 0xFF, 0x80, 0xFF, 0xC0, 0x7F, 0xFF, 0xFB, 0xFF, 0xFF, + 0xDF, 0xFF, 0xFE, 0xFF, 0xFF, 0xF0, 0x00, 0xFF, 0x00, 0x0F, 0xE0, 0x00, + 0xFC, 0x00, 0x1F, 0xC0, 0x01, 0xF8, 0x00, 0x1F, 0x80, 0x01, 0xF8, 0x00, + 0x1F, 0x80, 0x00, 0xF8, 0x00, 0x0F, 0x80, 0x00, 0xF8, 0x00, 0x07, 0x80, + 0x00, 0x7C, 0x00, 0x03, 0xC0, 0x00, 0x1E, 0x00, 0x01, 0xF0, 0x00, 0x0F, + 0x00, 0x00, 0x78, 0x00, 0x03, 0xC0, 0x00, 0x1E, 0x00, 0x00, 0xF0, 0x00, + 0x07, 0x80, 0x00, 0x3C, 0x00, 0x01, 0xF0, 0x00, 0x07, 0x80, 0x00, 0x3E, + 0x00, 0x01, 0xF0, 0x00, 0x07, 0xE0, 0x00, 0x3F, 0xC0, 0x00, 0xFF, 0xFC, + 0x01, 0xFF, 0xF8, 0x07, 0xFF, 0xE0, 0x07, 0xFF, 0x00, 0x00, 0x7C, 0x00, + 0x01, 0xE0, 0x00, 0x0F, 0x00, 0x00, 0x78, 0x00, 0x03, 0xC0, 0x00, 0x3E, + 0x00, 0x07, 0xE0, 0x00, 0x3F, 0x00, 0x01, 0xF0, 0x00, 0x0E, 0x00, 0x00, + 0x7F, 0x03, 0xC7, 0xFF, 0x0F, 0x3F, 0xFE, 0x3D, 0xFF, 0xFC, 0xFF, 0x81, + 0xFB, 0xF8, 0x03, 0xEF, 0xC0, 0x07, 0xBE, 0x00, 0x1F, 0xF8, 0x00, 0x3F, + 0xC0, 0x00, 0xFF, 0x00, 0x03, 0xFC, 0x00, 0x0F, 0xF0, 0x00, 0x3F, 0xC0, + 0x00, 0xFF, 0x00, 0x03, 0xFC, 0x00, 0x0F, 0xF0, 0x00, 0x3F, 0xC0, 0x00, + 0xFF, 0x00, 0x03, 0xFC, 0x00, 0x0F, 0xF0, 0x00, 0x3F, 0xC0, 0x00, 0xFF, + 0x00, 0x03, 0xFC, 0x00, 0x0F, 0xF0, 0x00, 0x3F, 0xC0, 0x00, 0xFF, 0x00, + 0x03, 0xC0, 0x00, 0x0F, 0x00, 0x00, 0x3C, 0x00, 0x00, 0xF0, 0x00, 0x03, + 0xC0, 0x00, 0x0F, 0x00, 0x00, 0x3C, 0x00, 0x00, 0xF0, 0x00, 0x03, 0xC0, + 0x00, 0x0F, 0x00, 0x00, 0x3C, 0x00, 0x7E, 0x00, 0x01, 0xFF, 0x80, 0x07, + 0xFF, 0xE0, 0x0F, 0xFF, 0xF0, 0x1F, 0x81, 0xF8, 0x1F, 0x00, 0xF8, 0x3E, + 0x00, 0x7C, 0x3C, 0x00, 0x3C, 0x7C, 0x00, 0x3E, 0x78, 0x00, 0x1E, 0x78, + 0x00, 0x1E, 0x78, 0x00, 0x1E, 0xF0, 0x00, 0x1F, 0xF0, 0x00, 0x0F, 0xF0, + 0x00, 0x0F, 0xF0, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x0F, 0xF0, 0x00, 0x0F, 0xF0, + 0x00, 0x0F, 0xF0, 0x00, 0x0F, 0xF0, 0x00, 0x0F, 0x78, 0x00, 0x1E, 0x78, + 0x00, 0x1E, 0x78, 0x00, 0x1E, 0x7C, 0x00, 0x3E, 0x3C, 0x00, 0x3C, 0x3E, + 0x00, 0x7C, 0x1F, 0x00, 0xF8, 0x1F, 0x81, 0xF0, 0x0F, 0xFF, 0xF0, 0x07, + 0xFF, 0xE0, 0x01, 0xFF, 0x80, 0x00, 0x7E, 0x00, 0xF0, 0x3C, 0x0F, 0x03, + 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, + 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF8, 0x1F, 0x07, + 0xFC, 0xFF, 0x1F, 0xC3, 0xF0, 0xF0, 0x01, 0xF3, 0xC0, 0x1F, 0x8F, 0x00, + 0xFC, 0x3C, 0x07, 0xE0, 0xF0, 0x3F, 0x03, 0xC1, 0xF8, 0x0F, 0x0F, 0xC0, + 0x3C, 0x7E, 0x00, 0xF3, 0xF0, 0x03, 0xDF, 0x80, 0x0F, 0xFE, 0x00, 0x3F, + 0xFC, 0x00, 0xFE, 0xF8, 0x03, 0xF1, 0xE0, 0x0F, 0x87, 0xC0, 0x3C, 0x0F, + 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x7C, 0x0F, 0x00, 0xF8, 0x3C, 0x01, 0xF0, + 0xF0, 0x07, 0xC3, 0xC0, 0x0F, 0x8F, 0x00, 0x1F, 0x3C, 0x00, 0x7C, 0xF0, + 0x00, 0xFB, 0xC0, 0x01, 0xF0, 0x0F, 0xC0, 0x00, 0x07, 0xF0, 0x00, 0x03, + 0xFC, 0x00, 0x01, 0xFF, 0x00, 0x00, 0x0F, 0x80, 0x00, 0x03, 0xE0, 0x00, + 0x00, 0xF0, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x0F, 0x00, + 0x00, 0x07, 0xC0, 0x00, 0x01, 0xE0, 0x00, 0x01, 0xF0, 0x00, 0x00, 0xFC, + 0x00, 0x00, 0xFE, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x7F, 0xC0, 0x00, 0x3D, + 0xE0, 0x00, 0x3E, 0xF8, 0x00, 0x1E, 0x3C, 0x00, 0x1F, 0x1E, 0x00, 0x0F, + 0x0F, 0x80, 0x0F, 0x83, 0xC0, 0x07, 0x81, 0xE0, 0x07, 0xC0, 0xF8, 0x03, + 0xC0, 0x3C, 0x03, 0xE0, 0x1F, 0x01, 0xE0, 0x07, 0x80, 0xF0, 0x03, 0xC0, + 0xF8, 0x01, 0xF0, 0x78, 0x00, 0x78, 0x7C, 0x00, 0x3C, 0x3C, 0x00, 0x1F, + 0x3E, 0x00, 0x07, 0x9E, 0x00, 0x03, 0xDF, 0x00, 0x00, 0xF0, 0xF0, 0x00, + 0x3C, 0x78, 0x00, 0x1E, 0x3C, 0x00, 0x0F, 0x1E, 0x00, 0x07, 0x8F, 0x00, + 0x03, 0xC7, 0x80, 0x01, 0xE3, 0xC0, 0x00, 0xF1, 0xE0, 0x00, 0x78, 0xF0, + 0x00, 0x3C, 0x78, 0x00, 0x1E, 0x3C, 0x00, 0x0F, 0x1E, 0x00, 0x07, 0x8F, + 0x00, 0x03, 0xC7, 0x80, 0x01, 0xE3, 0xC0, 0x00, 0xF1, 0xE0, 0x00, 0x78, + 0xF0, 0x00, 0x3C, 0x78, 0x00, 0x1E, 0x3C, 0x00, 0x0F, 0x1F, 0x00, 0x0F, + 0x8F, 0x80, 0x07, 0xC7, 0xE0, 0x07, 0xE3, 0xFC, 0x0F, 0xFB, 0xFF, 0xFF, + 0xFF, 0xF7, 0xFF, 0x9F, 0xF9, 0xFF, 0x8F, 0xFC, 0x3F, 0x03, 0xDE, 0x00, + 0x00, 0x0F, 0x00, 0x00, 0x07, 0x80, 0x00, 0x03, 0xC0, 0x00, 0x01, 0xE0, + 0x00, 0x00, 0xF0, 0x00, 0x00, 0x78, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x1E, + 0x00, 0x00, 0x00, 0xF0, 0x01, 0xE3, 0xE0, 0x03, 0xC7, 0x80, 0x0F, 0x1E, + 0x00, 0x1E, 0x7C, 0x00, 0x78, 0xF0, 0x01, 0xE3, 0xC0, 0x03, 0xCF, 0x80, + 0x0F, 0x1E, 0x00, 0x3C, 0x78, 0x00, 0xF1, 0xE0, 0x03, 0xC3, 0xC0, 0x0F, + 0x0F, 0x00, 0x7C, 0x3C, 0x01, 0xE0, 0xF8, 0x0F, 0x81, 0xE0, 0x3E, 0x07, + 0x81, 0xF0, 0x1F, 0x0F, 0x80, 0x3C, 0x3E, 0x00, 0xF1, 0xF0, 0x03, 0xEF, + 0x80, 0x07, 0xFC, 0x00, 0x1F, 0xE0, 0x00, 0x7F, 0x80, 0x00, 0xFC, 0x00, + 0x03, 0xE0, 0x00, 0x7F, 0xFF, 0xE7, 0xFF, 0xFE, 0x7F, 0xFF, 0xE7, 0xFF, + 0xFE, 0x07, 0xF0, 0x01, 0xF8, 0x00, 0x3E, 0x00, 0x03, 0xC0, 0x00, 0x78, + 0x00, 0x07, 0x80, 0x00, 0x78, 0x00, 0x07, 0x80, 0x00, 0x7C, 0x00, 0x03, + 0xE0, 0x00, 0x3F, 0xE0, 0x01, 0xFF, 0xF8, 0x07, 0xFF, 0x80, 0x3F, 0xF8, + 0x0F, 0xFF, 0x81, 0xFC, 0x00, 0x3E, 0x00, 0x07, 0xC0, 0x00, 0x78, 0x00, + 0x0F, 0x80, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, + 0x00, 0xF0, 0x00, 0x0F, 0x80, 0x00, 0x7C, 0x00, 0x07, 0xE0, 0x00, 0x3F, + 0x80, 0x03, 0xFF, 0xF0, 0x0F, 0xFF, 0xC0, 0x7F, 0xFE, 0x00, 0xFF, 0xE0, + 0x00, 0x1F, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, + 0x00, 0x01, 0xF0, 0x00, 0x7E, 0x00, 0x07, 0xE0, 0x00, 0x7C, 0x00, 0x07, + 0x00, 0x00, 0xFF, 0x00, 0x03, 0xFF, 0xC0, 0x0F, 0xFF, 0xF0, 0x1F, 0xFF, + 0xF8, 0x1F, 0x81, 0xF8, 0x3E, 0x00, 0x7C, 0x7C, 0x00, 0x3E, 0x7C, 0x00, + 0x3E, 0x78, 0x00, 0x1E, 0xF8, 0x00, 0x1F, 0xF0, 0x00, 0x0F, 0xF0, 0x00, + 0x0F, 0xF0, 0x00, 0x0F, 0xF0, 0x00, 0x0F, 0xF0, 0x00, 0x0F, 0xF0, 0x00, + 0x0F, 0xF0, 0x00, 0x0F, 0xF0, 0x00, 0x0F, 0xF8, 0x00, 0x1F, 0x78, 0x00, + 0x1E, 0x7C, 0x00, 0x3E, 0x7C, 0x00, 0x3E, 0x3E, 0x00, 0x7C, 0x1F, 0x81, + 0xF8, 0x1F, 0xFF, 0xF8, 0x0F, 0xFF, 0xF0, 0x03, 0xFF, 0xC0, 0x00, 0xFF, + 0x00, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0xFF, 0xBF, 0xFF, 0xFF, 0xDF, 0xFF, + 0xFF, 0xE1, 0xE0, 0x07, 0x80, 0xF0, 0x03, 0xC0, 0x78, 0x01, 0xE0, 0x3C, + 0x00, 0xF0, 0x1E, 0x00, 0x78, 0x0F, 0x00, 0x3C, 0x07, 0x80, 0x1E, 0x03, + 0xC0, 0x0F, 0x01, 0xE0, 0x07, 0x80, 0xF0, 0x03, 0xC0, 0x78, 0x01, 0xE0, + 0x3C, 0x00, 0xF0, 0x1E, 0x00, 0x78, 0x0F, 0x00, 0x3C, 0x07, 0x80, 0x1E, + 0x03, 0xC0, 0x0F, 0x01, 0xE0, 0x07, 0x80, 0xF0, 0x03, 0xC0, 0x78, 0x01, + 0xF0, 0x3C, 0x00, 0xFF, 0x1E, 0x00, 0x3F, 0x8F, 0x00, 0x1F, 0xC0, 0x00, + 0x07, 0xE0, 0x00, 0xFF, 0x00, 0x03, 0xFF, 0xC0, 0x07, 0xFF, 0xF0, 0x0F, + 0xFF, 0xF8, 0x1F, 0x81, 0xF8, 0x3E, 0x00, 0x7C, 0x7C, 0x00, 0x3E, 0x7C, + 0x00, 0x3E, 0x78, 0x00, 0x1E, 0xF8, 0x00, 0x1F, 0xF0, 0x00, 0x0F, 0xF0, + 0x00, 0x0F, 0xF0, 0x00, 0x0F, 0xF0, 0x00, 0x0F, 0xF0, 0x00, 0x0F, 0xF0, + 0x00, 0x0F, 0xF0, 0x00, 0x0F, 0xF0, 0x00, 0x0F, 0xF8, 0x00, 0x1F, 0xF8, + 0x00, 0x1E, 0xFC, 0x00, 0x3E, 0xFC, 0x00, 0x3E, 0xFE, 0x00, 0x7C, 0xFF, + 0x81, 0xF8, 0xF7, 0xFF, 0xF8, 0xF3, 0xFF, 0xF0, 0xF1, 0xFF, 0xC0, 0xF0, + 0x7F, 0x00, 0xF0, 0x00, 0x00, 0xF0, 0x00, 0x00, 0xF0, 0x00, 0x00, 0xF0, + 0x00, 0x00, 0xF0, 0x00, 0x00, 0xF0, 0x00, 0x00, 0xF0, 0x00, 0x00, 0xF0, + 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x7F, 0x80, 0x3F, 0xFE, 0x07, 0xFF, + 0xF0, 0xFF, 0xFF, 0x1F, 0xC0, 0xF3, 0xF0, 0x01, 0x7C, 0x00, 0x07, 0xC0, + 0x00, 0x78, 0x00, 0x0F, 0x80, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, + 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, + 0x00, 0x00, 0xF8, 0x00, 0x07, 0x80, 0x00, 0x7C, 0x00, 0x07, 0xC0, 0x00, + 0x3E, 0x00, 0x01, 0xF8, 0x00, 0x1F, 0xFE, 0x00, 0x7F, 0xF8, 0x03, 0xFF, + 0xC0, 0x0F, 0xFC, 0x00, 0x03, 0xE0, 0x00, 0x1E, 0x00, 0x01, 0xE0, 0x00, + 0x1E, 0x00, 0x01, 0xE0, 0x00, 0x3E, 0x00, 0x0F, 0xC0, 0x00, 0xFC, 0x00, + 0x0F, 0x80, 0x00, 0xE0, 0x00, 0xFF, 0xFF, 0xC1, 0xFF, 0xFF, 0xF0, 0xFF, + 0xFF, 0xFC, 0x7F, 0xFF, 0xFF, 0x3F, 0x81, 0xFC, 0x0F, 0x80, 0x1F, 0x07, + 0xC0, 0x03, 0xE1, 0xE0, 0x00, 0x78, 0xF8, 0x00, 0x1E, 0x3E, 0x00, 0x07, + 0xCF, 0x00, 0x00, 0xF3, 0xC0, 0x00, 0x3C, 0xF0, 0x00, 0x0F, 0x3C, 0x00, + 0x03, 0xCF, 0x00, 0x00, 0xF3, 0xC0, 0x00, 0x3C, 0xF0, 0x00, 0x0F, 0x3E, + 0x00, 0x07, 0xC7, 0x80, 0x01, 0xE1, 0xE0, 0x00, 0xF8, 0x7C, 0x00, 0x3E, + 0x0F, 0x80, 0x1F, 0x01, 0xF8, 0x1F, 0x80, 0x7F, 0xFF, 0xE0, 0x0F, 0xFF, + 0xF0, 0x00, 0xFF, 0xF0, 0x00, 0x0F, 0xF0, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x3C, 0x00, 0x00, + 0x3C, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x3C, 0x00, 0x00, + 0x3C, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x3C, 0x00, 0x00, + 0x3C, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x3C, 0x00, 0x00, + 0x3C, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x3E, 0x00, 0x00, + 0x1F, 0x00, 0x00, 0x1F, 0xF0, 0x00, 0x0F, 0xF0, 0x00, 0x07, 0xF0, 0x00, + 0x03, 0xF0, 0xF0, 0x03, 0xC7, 0x80, 0x0F, 0x3C, 0x00, 0x79, 0xE0, 0x01, + 0xEF, 0x00, 0x0F, 0x78, 0x00, 0x7B, 0xC0, 0x01, 0xFE, 0x00, 0x0F, 0xF0, + 0x00, 0x7F, 0x80, 0x03, 0xFC, 0x00, 0x1F, 0xE0, 0x00, 0xFF, 0x00, 0x07, + 0xF8, 0x00, 0x3F, 0xC0, 0x03, 0xFE, 0x00, 0x1E, 0xF0, 0x00, 0xF7, 0x80, + 0x0F, 0xBC, 0x00, 0x79, 0xF0, 0x07, 0xC7, 0x80, 0x7C, 0x3F, 0x0F, 0xE0, + 0xFF, 0xFE, 0x07, 0xFF, 0xE0, 0x0F, 0xFC, 0x00, 0x3F, 0x80, 0x00, 0x00, + 0xC3, 0xE0, 0x00, 0xF1, 0xFE, 0x00, 0xFC, 0xFF, 0xC0, 0x7F, 0x3F, 0xF8, + 0x3F, 0x9F, 0x3F, 0x0F, 0x87, 0x87, 0xC7, 0xC1, 0xE0, 0xF9, 0xF0, 0x78, + 0x1E, 0x78, 0x1E, 0x07, 0xBE, 0x07, 0x81, 0xFF, 0x01, 0xE0, 0x3F, 0xC0, + 0x78, 0x0F, 0xF0, 0x1E, 0x03, 0xFC, 0x07, 0x80, 0xFF, 0x01, 0xE0, 0x3F, + 0xC0, 0x78, 0x0F, 0xF0, 0x1E, 0x03, 0xFC, 0x07, 0x80, 0xFF, 0x81, 0xE0, + 0x7D, 0xE0, 0x78, 0x1E, 0x7C, 0x1E, 0x07, 0x9F, 0x07, 0x83, 0xE3, 0xE1, + 0xE1, 0xF0, 0x7E, 0x79, 0xF8, 0x1F, 0xFF, 0xFE, 0x03, 0xFF, 0xFF, 0x00, + 0x3F, 0xFF, 0x00, 0x03, 0xFF, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x07, 0x80, + 0x00, 0x01, 0xE0, 0x00, 0x00, 0x78, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x07, + 0x80, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x78, 0x00, 0x00, 0x1E, 0x00, 0x00, + 0xF8, 0x00, 0x1F, 0xFE, 0x00, 0x3E, 0xFF, 0x00, 0x3E, 0xFF, 0x80, 0x7C, + 0x0F, 0x80, 0x7C, 0x07, 0x80, 0xF8, 0x07, 0xC0, 0xF8, 0x03, 0xC1, 0xF0, + 0x03, 0xE1, 0xF0, 0x01, 0xE3, 0xE0, 0x01, 0xE3, 0xC0, 0x01, 0xF7, 0xC0, + 0x00, 0xFF, 0x80, 0x00, 0xFF, 0x80, 0x00, 0x7F, 0x00, 0x00, 0x7F, 0x00, + 0x00, 0x7E, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x7E, 0x00, + 0x00, 0xFE, 0x00, 0x00, 0xFE, 0x00, 0x01, 0xFF, 0x00, 0x01, 0xFF, 0x00, + 0x03, 0xEF, 0x80, 0x03, 0xC7, 0x80, 0x07, 0xC7, 0x80, 0x07, 0x83, 0xC0, + 0x0F, 0x83, 0xC0, 0x1F, 0x03, 0xC0, 0x1F, 0x01, 0xE0, 0x3E, 0x01, 0xF0, + 0x3E, 0x01, 0xFF, 0x7C, 0x00, 0xFF, 0x7C, 0x00, 0x7F, 0xF8, 0x00, 0x1F, + 0xF0, 0x1E, 0x03, 0xFC, 0x07, 0x80, 0xFF, 0x01, 0xE0, 0x3F, 0xC0, 0x78, + 0x0F, 0xF0, 0x1E, 0x03, 0xFC, 0x07, 0x80, 0xFF, 0x01, 0xE0, 0x3F, 0xC0, + 0x78, 0x0F, 0xF0, 0x1E, 0x03, 0xFC, 0x07, 0x80, 0xFF, 0x01, 0xE0, 0x3F, + 0xC0, 0x78, 0x0F, 0xF0, 0x1E, 0x03, 0xFC, 0x07, 0x80, 0xFF, 0x01, 0xE0, + 0x3F, 0xC0, 0x78, 0x0F, 0xF0, 0x1E, 0x03, 0xFC, 0x07, 0x80, 0xFF, 0x01, + 0xE0, 0x7F, 0xE0, 0x78, 0x1E, 0x7C, 0x1E, 0x0F, 0x9F, 0x87, 0x87, 0xE3, + 0xF9, 0xE7, 0xF0, 0x7F, 0xFF, 0xF8, 0x0F, 0xFF, 0xFC, 0x00, 0xFF, 0xFC, + 0x00, 0x0F, 0xF8, 0x00, 0x00, 0x78, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x07, + 0x80, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x78, 0x00, 0x00, 0x1E, 0x00, 0x00, + 0x07, 0x80, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x78, 0x00, 0x0F, 0x00, 0x00, + 0xF0, 0x1E, 0x00, 0x00, 0x78, 0x1E, 0x00, 0x00, 0x78, 0x3C, 0x00, 0x00, + 0x3C, 0x3C, 0x00, 0x00, 0x3C, 0x78, 0x00, 0x00, 0x1E, 0x78, 0x00, 0x00, + 0x1E, 0x78, 0x00, 0x00, 0x1E, 0x70, 0x00, 0x00, 0x0E, 0xF0, 0x00, 0x00, + 0x0F, 0xF0, 0x03, 0xC0, 0x0F, 0xF0, 0x03, 0xC0, 0x0F, 0xF0, 0x03, 0xC0, + 0x0F, 0xF0, 0x03, 0xC0, 0x0F, 0xF0, 0x03, 0xC0, 0x0F, 0xF0, 0x03, 0xC0, + 0x0F, 0xF0, 0x07, 0xC0, 0x0F, 0xF8, 0x07, 0xE0, 0x1F, 0x78, 0x07, 0xE0, + 0x1E, 0x78, 0x07, 0xE0, 0x1E, 0x7C, 0x0F, 0xF0, 0x3E, 0x3E, 0x1E, 0xF8, + 0x7C, 0x3F, 0xFE, 0x7F, 0xFC, 0x1F, 0xFC, 0x7F, 0xF8, 0x0F, 0xFC, 0x3F, + 0xF0, 0x03, 0xF0, 0x0F, 0xC0, 0xF8, 0x7F, 0xE1, 0xFF, 0x87, 0xFE, 0x1F, + 0xF8, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, + 0x03, 0xC0, 0x0F, 0x00, 0x3C, 0x00, 0xF0, 0x03, 0xC0, 0x0F, 0x00, 0x3C, + 0x00, 0xF0, 0x03, 0xC0, 0x0F, 0x00, 0x3C, 0x00, 0xF0, 0x03, 0xC0, 0x0F, + 0x00, 0x3C, 0x00, 0xF0, 0x03, 0xC0, 0x0F, 0x00, 0x1E, 0x00, 0x78, 0x01, + 0xF0, 0x07, 0xFC, 0x0F, 0xF0, 0x1F, 0xC0, 0x1F, 0x3E, 0x1F, 0x01, 0xF0, + 0xF8, 0x0F, 0x87, 0xC0, 0x7C, 0x3E, 0x03, 0xE1, 0xF0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0xE0, 0x07, 0x8F, 0x00, 0x1E, 0x78, 0x00, 0xF3, 0xC0, 0x03, 0xDE, + 0x00, 0x1E, 0xF0, 0x00, 0xF7, 0x80, 0x03, 0xFC, 0x00, 0x1F, 0xE0, 0x00, + 0xFF, 0x00, 0x07, 0xF8, 0x00, 0x3F, 0xC0, 0x01, 0xFE, 0x00, 0x0F, 0xF0, + 0x00, 0x7F, 0x80, 0x07, 0xFC, 0x00, 0x3D, 0xE0, 0x01, 0xEF, 0x00, 0x1F, + 0x78, 0x00, 0xF3, 0xE0, 0x0F, 0x8F, 0x00, 0xF8, 0x7E, 0x1F, 0xC1, 0xFF, + 0xFC, 0x0F, 0xFF, 0xC0, 0x1F, 0xF8, 0x00, 0x7F, 0x00, 0x00, 0x00, 0x01, + 0xE0, 0x00, 0x03, 0xE0, 0x00, 0x03, 0xC0, 0x00, 0x07, 0x80, 0x00, 0x0F, + 0x00, 0x00, 0x1E, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x78, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x03, 0xFF, + 0xC0, 0x0F, 0xFF, 0xF0, 0x1F, 0xFF, 0xF8, 0x1F, 0x81, 0xF8, 0x3E, 0x00, + 0x7C, 0x7C, 0x00, 0x3E, 0x7C, 0x00, 0x3E, 0x78, 0x00, 0x1E, 0xF8, 0x00, + 0x1F, 0xF0, 0x00, 0x0F, 0xF0, 0x00, 0x0F, 0xF0, 0x00, 0x0F, 0xF0, 0x00, + 0x0F, 0xF0, 0x00, 0x0F, 0xF0, 0x00, 0x0F, 0xF0, 0x00, 0x0F, 0xF0, 0x00, + 0x0F, 0xF8, 0x00, 0x1F, 0x78, 0x00, 0x1E, 0x7C, 0x00, 0x3E, 0x7C, 0x00, + 0x3E, 0x3E, 0x00, 0x7C, 0x1F, 0x81, 0xF8, 0x1F, 0xFF, 0xF8, 0x0F, 0xFF, + 0xF0, 0x03, 0xFF, 0xC0, 0x00, 0xFF, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xF8, + 0x00, 0x0F, 0x80, 0x00, 0x78, 0x00, 0x07, 0x80, 0x00, 0x78, 0x00, 0x03, + 0x80, 0x00, 0x3C, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x01, 0xE3, 0xC0, 0x07, 0x9E, 0x00, + 0x3C, 0xF0, 0x00, 0xF7, 0x80, 0x07, 0xBC, 0x00, 0x3D, 0xE0, 0x00, 0xFF, + 0x00, 0x07, 0xF8, 0x00, 0x3F, 0xC0, 0x01, 0xFE, 0x00, 0x0F, 0xF0, 0x00, + 0x7F, 0x80, 0x03, 0xFC, 0x00, 0x1F, 0xE0, 0x01, 0xFF, 0x00, 0x0F, 0x78, + 0x00, 0x7B, 0xC0, 0x07, 0xDE, 0x00, 0x3C, 0xF8, 0x03, 0xE3, 0xC0, 0x3E, + 0x1F, 0x87, 0xF0, 0x7F, 0xFF, 0x03, 0xFF, 0xF0, 0x07, 0xFE, 0x00, 0x1F, + 0xC0, 0x00, 0x00, 0x00, 0x0F, 0x80, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, + 0x1E, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, + 0xF0, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x03, + 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x1E, 0x00, + 0x00, 0x78, 0x1E, 0x00, 0x00, 0x78, 0x3C, 0x00, 0x00, 0x3C, 0x3C, 0x00, + 0x00, 0x3C, 0x78, 0x00, 0x00, 0x1E, 0x78, 0x00, 0x00, 0x1E, 0x78, 0x00, + 0x00, 0x1E, 0x70, 0x00, 0x00, 0x0E, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x03, + 0xC0, 0x0F, 0xF0, 0x03, 0xC0, 0x0F, 0xF0, 0x03, 0xC0, 0x0F, 0xF0, 0x03, + 0xC0, 0x0F, 0xF0, 0x03, 0xC0, 0x0F, 0xF0, 0x03, 0xC0, 0x0F, 0xF0, 0x07, + 0xC0, 0x0F, 0xF8, 0x07, 0xE0, 0x1F, 0x78, 0x07, 0xE0, 0x1E, 0x78, 0x07, + 0xE0, 0x1E, 0x7C, 0x0F, 0xF0, 0x3E, 0x3E, 0x1E, 0xF8, 0x7C, 0x3F, 0xFE, + 0x7F, 0xFC, 0x1F, 0xFC, 0x7F, 0xF8, 0x0F, 0xFC, 0x3F, 0xF0, 0x03, 0xF0, + 0x0F, 0xC0, +}; + +const GFXglyph FreeSans24pt_Win1253Glyphs[] PROGMEM = { +/* 0x01 */ { 0, 43, 48, 60, 8, -39 }, +/* 0x02 */ { 258, 43, 48, 60, 8, -39 }, +/* 0x03 */ { 516, 48, 48, 60, 6, -39 }, +/* 0x04 */ { 804, 57, 48, 60, 1, -39 }, +/* 0x05 */ { 1146, 47, 48, 60, 6, -39 }, +/* 0x06 */ { 1428, 47, 48, 60, 6, -39 }, +/* 0x07 */ { 1710, 0, 0, 0, 0, 0 }, +/* 0x08 */ { 1710, 49, 48, 60, 5, -39 }, +/* 0x09 */ { 2004, 54, 38, 60, 3, -34 }, +/* 0x0A */ { 2261, 0, 0, 0, 0, 0 }, +/* 0x0B */ { 2261, 52, 48, 60, 4, -39 }, +/* 0x0C */ { 2573, 47, 48, 60, 6, -39 }, +/* 0x0D */ { 2855, 0, 0, 0, 0, 0 }, +/* 0x0E */ { 2855, 47, 48, 60, 6, -39 }, +/* 0x0F */ { 3137, 48, 49, 60, 6, -39 }, +/* 0x10 */ { 3431, 46, 48, 60, 7, -39 }, +/* 0x11 */ { 3707, 48, 48, 60, 6, -39 }, +/* 0x12 */ { 3995, 46, 48, 60, 7, -39 }, +/* 0x13 */ { 4271, 48, 46, 60, 6, -38 }, +/* 0x14 */ { 4547, 48, 48, 60, 6, -39 }, +/* 0x15 */ { 4835, 51, 48, 60, 4, -39 }, +/* 0x16 */ { 5141, 37, 47, 60, 11, -38 }, +/* 0x17 */ { 5359, 50, 40, 60, 5, -35 }, +/* 0x18 */ { 5609, 56, 40, 60, 2, -35 }, +/* 0x19 */ { 5889, 48, 48, 60, 6, -39 }, +/* 0x1A */ { 6177, 0, 0, 0, 0, 0 }, +/* 0x1B */ { 6177, 56, 49, 60, 2, -39 }, +/* 0x1C */ { 6520, 48, 48, 60, 6, -39 }, +/* 0x1D */ { 6808, 49, 49, 60, 5, -39 }, +/* 0x1E */ { 7109, 48, 47, 60, 5, -38 }, +/* 0x1F */ { 7391, 34, 48, 60, 13, -39 }, +/* 0x20 */ { 7595, 1, 1, 15, 0, 0 }, +/* 0x21 */ { 7596, 5, 34, 19, 7, -33 }, +/* 0x22 */ { 7618, 13, 13, 22, 4, -33 }, +/* 0x23 */ { 7640, 32, 35, 39, 4, -34 }, +/* 0x24 */ { 7780, 22, 42, 30, 4, -34 }, +/* 0x25 */ { 7896, 39, 36, 45, 3, -34 }, +/* 0x26 */ { 8072, 32, 36, 37, 3, -34 }, +/* 0x27 */ { 8216, 4, 13, 13, 4, -33 }, +/* 0x28 */ { 8223, 11, 42, 18, 4, -35 }, +/* 0x29 */ { 8281, 11, 42, 18, 4, -35 }, +/* 0x2A */ { 8339, 21, 22, 24, 1, -34 }, +/* 0x2B */ { 8397, 30, 30, 39, 5, -29 }, +/* 0x2C */ { 8510, 6, 11, 15, 4, -5 }, +/* 0x2D */ { 8519, 12, 4, 17, 2, -14 }, +/* 0x2E */ { 8525, 4, 6, 15, 6, -5 }, +/* 0x2F */ { 8528, 16, 39, 16, 0, -33 }, +/* 0x30 */ { 8606, 24, 36, 30, 3, -34 }, +/* 0x31 */ { 8714, 20, 34, 30, 5, -33 }, +/* 0x32 */ { 8799, 22, 35, 30, 4, -34 }, +/* 0x33 */ { 8896, 23, 36, 30, 4, -34 }, +/* 0x34 */ { 9000, 25, 34, 30, 2, -33 }, +/* 0x35 */ { 9107, 22, 35, 30, 4, -33 }, +/* 0x36 */ { 9204, 24, 36, 30, 3, -34 }, +/* 0x37 */ { 9312, 22, 34, 30, 4, -33 }, +/* 0x38 */ { 9406, 24, 36, 30, 3, -34 }, +/* 0x39 */ { 9514, 24, 36, 30, 3, -34 }, +/* 0x3A */ { 9622, 5, 24, 16, 6, -23 }, +/* 0x3B */ { 9637, 6, 29, 16, 4, -23 }, +/* 0x3C */ { 9659, 29, 25, 39, 5, -26 }, +/* 0x3D */ { 9750, 29, 14, 39, 5, -21 }, +/* 0x3E */ { 9801, 29, 25, 39, 5, -26 }, +/* 0x3F */ { 9892, 18, 35, 25, 3, -34 }, +/* 0x40 */ { 9971, 41, 41, 47, 3, -32 }, +/* 0x41 */ { 10182, 31, 34, 32, 0, -33 }, +/* 0x42 */ { 10314, 24, 34, 32, 4, -33 }, +/* 0x43 */ { 10416, 28, 36, 33, 3, -34 }, +/* 0x44 */ { 10542, 29, 34, 36, 4, -33 }, +/* 0x45 */ { 10666, 22, 34, 30, 4, -33 }, +/* 0x46 */ { 10760, 20, 34, 27, 4, -33 }, +/* 0x47 */ { 10845, 30, 36, 36, 3, -34 }, +/* 0x48 */ { 10980, 26, 34, 35, 4, -33 }, +/* 0x49 */ { 11091, 4, 34, 14, 4, -33 }, +/* 0x4A */ { 11108, 11, 43, 14, -3, -33 }, +/* 0x4B */ { 11168, 27, 34, 31, 4, -33 }, +/* 0x4C */ { 11283, 21, 34, 26, 4, -33 }, +/* 0x4D */ { 11373, 31, 34, 41, 4, -33 }, +/* 0x4E */ { 11505, 26, 34, 35, 4, -33 }, +/* 0x4F */ { 11616, 32, 36, 37, 3, -34 }, +/* 0x50 */ { 11760, 22, 34, 28, 4, -33 }, +/* 0x51 */ { 11854, 32, 41, 37, 3, -34 }, +/* 0x52 */ { 12018, 27, 34, 33, 4, -33 }, +/* 0x53 */ { 12133, 24, 36, 30, 3, -34 }, +/* 0x54 */ { 12241, 28, 34, 29, 0, -33 }, +/* 0x55 */ { 12360, 26, 35, 34, 5, -33 }, +/* 0x56 */ { 12474, 31, 34, 32, 0, -33 }, +/* 0x57 */ { 12606, 43, 34, 46, 2, -33 }, +/* 0x58 */ { 12789, 29, 34, 31, 1, -33 }, +/* 0x59 */ { 12913, 28, 34, 29, 0, -33 }, +/* 0x5A */ { 13032, 28, 34, 32, 2, -33 }, +/* 0x5B */ { 13151, 10, 42, 18, 4, -35 }, +/* 0x5C */ { 13204, 16, 39, 16, 0, -33 }, +/* 0x5D */ { 13282, 9, 42, 18, 4, -35 }, +/* 0x5E */ { 13330, 29, 13, 39, 5, -33 }, +/* 0x5F */ { 13378, 24, 4, 24, 0, 8 }, +/* 0x60 */ { 13390, 11, 9, 24, 4, -37 }, +/* 0x61 */ { 13403, 22, 28, 29, 3, -26 }, +/* 0x62 */ { 13480, 23, 37, 30, 4, -35 }, +/* 0x63 */ { 13587, 20, 28, 26, 3, -26 }, +/* 0x64 */ { 13657, 23, 37, 30, 3, -35 }, +/* 0x65 */ { 13764, 24, 28, 29, 3, -26 }, +/* 0x66 */ { 13848, 16, 36, 17, 1, -35 }, +/* 0x67 */ { 13920, 23, 37, 30, 3, -26 }, +/* 0x68 */ { 14027, 22, 36, 30, 4, -35 }, +/* 0x69 */ { 14126, 4, 36, 13, 4, -35 }, +/* 0x6A */ { 14144, 9, 46, 13, -1, -35 }, +/* 0x6B */ { 14196, 23, 36, 27, 4, -35 }, +/* 0x6C */ { 14300, 4, 36, 13, 4, -35 }, +/* 0x6D */ { 14318, 38, 27, 46, 4, -26 }, +/* 0x6E */ { 14447, 22, 27, 30, 4, -26 }, +/* 0x6F */ { 14522, 24, 28, 29, 3, -26 }, +/* 0x70 */ { 14606, 23, 37, 30, 4, -26 }, +/* 0x71 */ { 14713, 23, 37, 30, 3, -26 }, +/* 0x72 */ { 14820, 15, 27, 19, 4, -26 }, +/* 0x73 */ { 14871, 19, 28, 24, 3, -26 }, +/* 0x74 */ { 14938, 16, 33, 18, 1, -32 }, +/* 0x75 */ { 15004, 22, 28, 30, 4, -26 }, +/* 0x76 */ { 15081, 25, 26, 28, 1, -25 }, +/* 0x77 */ { 15163, 35, 26, 38, 2, -25 }, +/* 0x78 */ { 15277, 25, 26, 28, 1, -25 }, +/* 0x79 */ { 15359, 25, 36, 28, 1, -25 }, +/* 0x7A */ { 15472, 21, 26, 25, 2, -25 }, +/* 0x7B */ { 15541, 18, 43, 30, 6, -35 }, +/* 0x7C */ { 15638, 4, 47, 16, 6, -35 }, +/* 0x7D */ { 15662, 18, 43, 30, 6, -35 }, +/* 0x7E */ { 15759, 29, 9, 39, 5, -18 }, +/* 0x7F */ { 15792, 0, 0, 0, 0, 0 }, +/* 0x80 */ { 15792, 26, 36, 30, 0, -34 }, +/* 0x81 */ { 15909, 0, 0, 0, 0, 0 }, +/* 0x82 */ { 15909, 6, 11, 15, 4, -5 }, +/* 0x83 */ { 15918, 20, 46, 17, -3, -35 }, +/* 0x84 */ { 16033, 15, 11, 24, 4, -5 }, +/* 0x85 */ { 16054, 36, 6, 47, 5, -5 }, +/* 0x86 */ { 16081, 20, 39, 24, 2, -33 }, +/* 0x87 */ { 16179, 20, 39, 24, 2, -33 }, +/* 0x88 */ { 16277, 0, 0, 0, 0, 0 }, +/* 0x89 */ { 16277, 58, 36, 63, 3, -34 }, +/* 0x8A */ { 16538, 0, 0, 0, 0, 0 }, +/* 0x8B */ { 16538, 11, 21, 19, 4, -23 }, +/* 0x8C */ { 16567, 0, 0, 0, 0, 0 }, +/* 0x8D */ { 16567, 0, 0, 0, 0, 0 }, +/* 0x8E */ { 16567, 0, 0, 0, 0, 0 }, +/* 0x8F */ { 16567, 0, 0, 0, 0, 0 }, +/* 0x90 */ { 16567, 0, 0, 0, 0, 0 }, +/* 0x91 */ { 16567, 6, 11, 15, 4, -33 }, +/* 0x92 */ { 16576, 6, 11, 15, 4, -33 }, +/* 0x93 */ { 16585, 15, 11, 24, 4, -33 }, +/* 0x94 */ { 16606, 15, 11, 24, 4, -33 }, +/* 0x95 */ { 16627, 14, 14, 28, 7, -23 }, +/* 0x96 */ { 16652, 19, 4, 24, 2, -14 }, +/* 0x97 */ { 16662, 42, 4, 47, 2, -14 }, +/* 0x98 */ { 16683, 0, 0, 0, 0, 0 }, +/* 0x99 */ { 16683, 31, 13, 47, 6, -33 }, +/* 0x9A */ { 16734, 0, 0, 0, 0, 0 }, +/* 0x9B */ { 16734, 11, 21, 19, 4, -23 }, +/* 0x9C */ { 16763, 0, 0, 0, 0, 0 }, +/* 0x9D */ { 16763, 0, 0, 0, 0, 0 }, +/* 0x9E */ { 16763, 0, 0, 0, 0, 0 }, +/* 0x9F */ { 16763, 0, 0, 0, 0, 0 }, +/* 0xA0 */ { 16763, 1, 1, 15, 0, 0 }, +/* 0xA1 */ { 16764, 15, 15, 24, 5, -45 }, +/* 0xA2 */ { 16793, 31, 38, 33, 0, -37 }, +/* 0xA3 */ { 16941, 22, 35, 30, 3, -34 }, +/* 0xA4 */ { 17038, 26, 26, 30, 2, -26 }, +/* 0xA5 */ { 17123, 26, 34, 30, 2, -33 }, +/* 0xA6 */ { 17234, 4, 40, 16, 6, -31 }, +/* 0xA7 */ { 17254, 19, 39, 24, 2, -34 }, +/* 0xA8 */ { 17347, 14, 5, 24, 5, -35 }, +/* 0xA9 */ { 17356, 34, 34, 47, 7, -33 }, +/* 0xAA */ { 17501, 0, 0, 0, 0, 0 }, +/* 0xAB */ { 17501, 21, 21, 29, 4, -23 }, +/* 0xAC */ { 17557, 29, 13, 39, 5, -19 }, +/* 0xAD */ { 17605, 12, 4, 17, 2, -14 }, +/* 0xAE */ { 17611, 34, 34, 47, 7, -33 }, +/* 0xAF */ { 17756, 47, 4, 47, 0, -14 }, +/* 0xB0 */ { 17780, 15, 15, 24, 4, -34 }, +/* 0xB1 */ { 17809, 30, 30, 39, 5, -29 }, +/* 0xB2 */ { 17922, 14, 19, 19, 2, -34 }, +/* 0xB3 */ { 17956, 14, 19, 19, 2, -34 }, +/* 0xB4 */ { 17990, 11, 9, 24, 9, -37 }, +/* 0xB5 */ { 18003, 15, 15, 24, 5, -45 }, +/* 0xB6 */ { 18032, 31, 38, 33, 0, -37 }, +/* 0xB7 */ { 18180, 4, 6, 15, 5, -18 }, +/* 0xB8 */ { 18183, 31, 38, 35, 0, -37 }, +/* 0xB9 */ { 18331, 35, 38, 41, 0, -37 }, +/* 0xBA */ { 18498, 13, 38, 19, 0, -37 }, +/* 0xBB */ { 18560, 21, 21, 29, 4, -23 }, +/* 0xBC */ { 18616, 36, 39, 38, 0, -37 }, +/* 0xBD */ { 18792, 39, 36, 46, 4, -34 }, +/* 0xBE */ { 18968, 38, 38, 39, 0, -37 }, +/* 0xBF */ { 19149, 36, 38, 39, 0, -37 }, +/* 0xC0 */ { 19320, 15, 46, 16, 0, -45 }, +/* 0xC1 */ { 19407, 31, 34, 32, 0, -33 }, +/* 0xC2 */ { 19539, 24, 34, 32, 4, -33 }, +/* 0xC3 */ { 19641, 21, 34, 25, 4, -33 }, +/* 0xC4 */ { 19731, 31, 34, 32, 0, -33 }, +/* 0xC5 */ { 19863, 22, 34, 30, 4, -33 }, +/* 0xC6 */ { 19957, 28, 34, 32, 2, -33 }, +/* 0xC7 */ { 20076, 26, 34, 35, 4, -33 }, +/* 0xC8 */ { 20187, 32, 36, 38, 3, -34 }, +/* 0xC9 */ { 20331, 4, 34, 14, 4, -33 }, +/* 0xCA */ { 20348, 27, 34, 31, 4, -33 }, +/* 0xCB */ { 20463, 31, 34, 32, 0, -33 }, +/* 0xCC */ { 20595, 31, 34, 41, 4, -33 }, +/* 0xCD */ { 20727, 26, 34, 35, 4, -33 }, +/* 0xCE */ { 20838, 21, 34, 29, 4, -33 }, +/* 0xCF */ { 20928, 32, 36, 37, 3, -34 }, +/* 0xD0 */ { 21072, 26, 34, 34, 4, -33 }, +/* 0xD1 */ { 21183, 22, 34, 28, 4, -33 }, +/* 0xD2 */ { 21277, 0, 0, 0, 0, 0 }, +/* 0xD3 */ { 21277, 22, 34, 29, 4, -33 }, +/* 0xD4 */ { 21371, 28, 34, 29, 0, -33 }, +/* 0xD5 */ { 21490, 28, 34, 29, 0, -33 }, +/* 0xD6 */ { 21609, 32, 34, 38, 3, -33 }, +/* 0xD7 */ { 21745, 29, 34, 31, 1, -33 }, +/* 0xD8 */ { 21869, 32, 34, 38, 3, -33 }, +/* 0xD9 */ { 22005, 32, 35, 38, 3, -34 }, +/* 0xDA */ { 22145, 14, 44, 14, -1, -43 }, +/* 0xDB */ { 22222, 28, 44, 29, 0, -43 }, +/* 0xDC */ { 22376, 26, 39, 31, 3, -37 }, +/* 0xDD */ { 22503, 19, 39, 25, 3, -37 }, +/* 0xDE */ { 22596, 22, 48, 30, 4, -37 }, +/* 0xDF */ { 22728, 12, 38, 16, 4, -37 }, +/* 0xE0 */ { 22785, 21, 47, 28, 4, -45 }, +/* 0xE1 */ { 22909, 26, 28, 31, 3, -26 }, +/* 0xE2 */ { 23000, 22, 46, 29, 4, -35 }, +/* 0xE3 */ { 23127, 26, 36, 28, 1, -25 }, +/* 0xE4 */ { 23244, 24, 36, 30, 3, -34 }, +/* 0xE5 */ { 23352, 19, 28, 25, 3, -26 }, +/* 0xE6 */ { 23419, 21, 47, 25, 2, -35 }, +/* 0xE7 */ { 23543, 22, 37, 30, 4, -26 }, +/* 0xE8 */ { 23645, 24, 37, 30, 3, -35 }, +/* 0xE9 */ { 23756, 10, 26, 16, 4, -25 }, +/* 0xEA */ { 23789, 22, 26, 27, 4, -25 }, +/* 0xEB */ { 23861, 25, 36, 27, 1, -35 }, +/* 0xEC */ { 23974, 25, 36, 30, 4, -25 }, +/* 0xED */ { 24087, 22, 26, 26, 2, -25 }, +/* 0xEE */ { 24159, 20, 47, 25, 2, -35 }, +/* 0xEF */ { 24277, 24, 28, 29, 3, -26 }, +/* 0xF0 */ { 24361, 25, 27, 28, 2, -25 }, +/* 0xF1 */ { 24446, 24, 37, 31, 4, -26 }, +/* 0xF2 */ { 24557, 20, 38, 28, 3, -26 }, +/* 0xF3 */ { 24652, 26, 27, 30, 3, -25 }, +/* 0xF4 */ { 24740, 24, 26, 28, 2, -25 }, +/* 0xF5 */ { 24818, 21, 26, 28, 4, -24 }, +/* 0xF6 */ { 24887, 26, 37, 32, 3, -26 }, +/* 0xF7 */ { 25008, 24, 36, 26, 1, -25 }, +/* 0xF8 */ { 25116, 26, 36, 32, 3, -25 }, +/* 0xF9 */ { 25233, 32, 26, 38, 3, -24 }, +/* 0xFA */ { 25337, 14, 36, 16, 0, -35 }, +/* 0xFB */ { 25400, 21, 37, 28, 4, -35 }, +/* 0xFC */ { 25498, 24, 39, 29, 3, -37 }, +/* 0xFD */ { 25615, 21, 39, 28, 4, -37 }, +/* 0xFE */ { 25718, 32, 39, 38, 3, -37 }, +/* 0xFF */ { 25874, 0, 0, 0, 0, 0 }, +}; + +const GFXfont FreeSans24pt_Win1253 PROGMEM = { +(uint8_t*)FreeSans24pt_Win1253Bitmaps, +(GFXglyph*)FreeSans24pt_Win1253Glyphs, +0x01, 0xFF, 55 +}; diff --git a/src/graphics/niche/InkHUD/Applet.h b/src/graphics/niche/InkHUD/Applet.h index 3c14c2607..6b727f273 100644 --- a/src/graphics/niche/InkHUD/Applet.h +++ b/src/graphics/niche/InkHUD/Applet.h @@ -104,6 +104,15 @@ class Applet : public GFX virtual void onFreeText(char c) {} virtual void onFreeTextDone() {} virtual void onFreeTextCancel() {} + // Absolute display-space touch point, for touch-friendly UI interactions. + // Return true if consumed. + virtual bool onTouchPoint(uint16_t x, uint16_t y, bool longPress) + { + (void)x; + (void)y; + (void)longPress; + return false; + } // List of inputs which can be subscribed to enum InputMask { // | No Joystick | With Joystick | BUTTON_SHORT = 1, // | Button Click | Joystick Center Click | diff --git a/src/graphics/niche/InkHUD/AppletFont.h b/src/graphics/niche/InkHUD/AppletFont.h index 8374c7f61..c97c12c36 100644 --- a/src/graphics/niche/InkHUD/AppletFont.h +++ b/src/graphics/niche/InkHUD/AppletFont.h @@ -88,8 +88,12 @@ class AppletFont // Greek #include "graphics/niche/Fonts/FreeSans12pt_Win1253.h" +#include "graphics/niche/Fonts/FreeSans18pt_Win1253.h" +#include "graphics/niche/Fonts/FreeSans24pt_Win1253.h" #include "graphics/niche/Fonts/FreeSans6pt_Win1253.h" #include "graphics/niche/Fonts/FreeSans9pt_Win1253.h" +#define FREESANS_24PT_WIN1253 InkHUD::AppletFont(FreeSans24pt_Win1253, InkHUD::AppletFont::WINDOWS_1253, -5, 3) +#define FREESANS_18PT_WIN1253 InkHUD::AppletFont(FreeSans18pt_Win1253, InkHUD::AppletFont::WINDOWS_1253, -4, 2) #define FREESANS_12PT_WIN1253 InkHUD::AppletFont(FreeSans12pt_Win1253, InkHUD::AppletFont::WINDOWS_1253, -3, 1) #define FREESANS_9PT_WIN1253 InkHUD::AppletFont(FreeSans9pt_Win1253, InkHUD::AppletFont::WINDOWS_1253, -2, -1) #define FREESANS_6PT_WIN1253 InkHUD::AppletFont(FreeSans6pt_Win1253, InkHUD::AppletFont::WINDOWS_1253, -1, -2) diff --git a/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp b/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp index 06ddd5bb0..63ccaa216 100644 --- a/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp @@ -43,8 +43,8 @@ void InkHUD::MapApplet::onRender(bool full) // Add white halo outline first constexpr int outlinePad = 1; - int boxSize = 11; - int radius = 2; // rounded corner radius + int boxSize = fontSmall.lineHeight() + 2; // scale with font so digit fits + int radius = max(2, boxSize / 6); // White halo background fillRoundedRect(x, y, boxSize + (outlinePad * 2), boxSize + (outlinePad * 2), radius + 1, WHITE); @@ -143,17 +143,19 @@ void InkHUD::MapApplet::onRender(bool full) int16_t centerX = X(0.5) + (self.eastMeters * metersToPx); int16_t centerY = Y(0.5) - (self.northMeters * metersToPx); + int16_t r = fontSmall.lineHeight() / 2; // scale marker with font + // White fill background + halo - fillCircle(centerX, centerY, 8, WHITE); // big white base - drawCircle(centerX, centerY, 8, WHITE); // crisp edge + fillCircle(centerX, centerY, r + 2, WHITE); + drawCircle(centerX, centerY, r + 2, WHITE); // Black bullseye on top - drawCircle(centerX, centerY, 6, BLACK); - fillCircle(centerX, centerY, 2, BLACK); + drawCircle(centerX, centerY, r, BLACK); + fillCircle(centerX, centerY, max(2, r / 4), BLACK); // Crosshairs - drawLine(centerX - 8, centerY, centerX + 8, centerY, BLACK); - drawLine(centerX, centerY - 8, centerX, centerY + 8, BLACK); + drawLine(centerX - r - 2, centerY, centerX + r + 2, centerY, BLACK); + drawLine(centerX, centerY - r - 2, centerX, centerY + r + 2, BLACK); } } @@ -382,9 +384,9 @@ void InkHUD::MapApplet::drawLabeledMarker(meshtastic_NodeInfoLite *node) constexpr uint16_t paddingH = 2; constexpr uint16_t paddingW = 4; - uint16_t paddingInnerW = 2; // Zero'd out if no text - constexpr uint16_t markerSizeMax = 12; // Size of cross (if marker uses a cross) - constexpr uint16_t markerSizeMin = 5; + uint16_t paddingInnerW = 2; // Zero'd out if no text + uint16_t markerSizeMax = fontSmall.lineHeight(); // Scale cross with font + uint16_t markerSizeMin = max(5, fontSmall.lineHeight() / 3); int16_t textX; int16_t textY; diff --git a/src/graphics/niche/InkHUD/Applets/System/AppSwitcher/AppSwitcherApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/AppSwitcher/AppSwitcherApplet.cpp new file mode 100644 index 000000000..005327bae --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/System/AppSwitcher/AppSwitcherApplet.cpp @@ -0,0 +1,545 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +#include "./AppSwitcherApplet.h" + +#include "graphics/niche/InkHUD/InkHUD.h" +#include "graphics/niche/InkHUD/Tile.h" + +#include +#include + +using namespace NicheGraphics; + +namespace +{ +static constexpr uint16_t BODY_MARGIN_X = 8; +static constexpr uint16_t BODY_MARGIN_Y = 6; +static constexpr uint16_t SLOT_GAP_X = 8; +static constexpr uint16_t SLOT_GAP_Y = 8; +static constexpr uint8_t ICON_RADIUS = 8; +static constexpr uint16_t FOOTER_PAD = 4; +static constexpr uint16_t LABEL_BOTTOM_PAD = 1; +static constexpr uint16_t LABEL_GAP_Y = 1; +static constexpr uint16_t TITLE_H_PAD = 8; + +static constexpr uint8_t GRID_COLS = 3; +static constexpr uint8_t GRID_ROWS = 4; +static constexpr uint8_t ICON_NATIVE_SIZE = 48; +static constexpr uint8_t ICON_OUTLINE_STROKE = 1; + +enum class IconKind : uint8_t { GENERIC, ALL_MESSAGES, DMS, CHANNEL, POSITIONS, RECENTS, HEARD, FAVORITES }; + +struct GridLayout { + uint16_t footerH = 0; + uint16_t bodyTop = 0; + uint16_t bodyBottom = 0; + uint16_t slotW = 0; + uint16_t slotH = 0; + uint16_t iconBox = 0; +}; + +/* + * Icons sourced from Material Design Icons PNG set (Apache 2.0): + * https://github.com/material-icons/material-icons-png + * + * Families used: outline-2x (48x48) + * apps, markunread, chat, forum, place, history, hearing, star_border + */ +static constexpr uint64_t icon_generic_apps[48] = { + 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, + 0x000000000000ULL, 0x000000000000ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, + 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x000000000000ULL, 0x000000000000ULL, + 0x000000000000ULL, 0x000000000000ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, + 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x000000000000ULL, 0x000000000000ULL, + 0x000000000000ULL, 0x000000000000ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, + 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x000000000000ULL, 0x000000000000ULL, + 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, +}; + +static constexpr uint64_t icon_all_messages[48] = { + 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, + 0x000000000000ULL, 0x000000000000ULL, 0x07FFFFFFFFE0ULL, 0x0FFFFFFFFFF0ULL, 0x0FFFFFFFFFF0ULL, 0x0FFFFFFFFFF0ULL, + 0x0FC0000003F0ULL, 0x0FE0000007F0ULL, 0x0FF800001FF0ULL, 0x0FFE00007FF0ULL, 0x0FFF0000FFF0ULL, 0x0F7FC003FEF0ULL, + 0x0F1FE007F8F0ULL, 0x0F07F81FE0F0ULL, 0x0F03FE7FC0F0ULL, 0x0F00FFFF00F0ULL, 0x0F007FFE00F0ULL, 0x0F001FF800F0ULL, + 0x0F0007E000F0ULL, 0x0F0003C000F0ULL, 0x0F00000000F0ULL, 0x0F00000000F0ULL, 0x0F00000000F0ULL, 0x0F00000000F0ULL, + 0x0F00000000F0ULL, 0x0F00000000F0ULL, 0x0F00000000F0ULL, 0x0F00000000F0ULL, 0x0F00000000F0ULL, 0x0F00000000F0ULL, + 0x0FFFFFFFFFF0ULL, 0x0FFFFFFFFFF0ULL, 0x0FFFFFFFFFF0ULL, 0x07FFFFFFFFE0ULL, 0x000000000000ULL, 0x000000000000ULL, + 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, +}; + +static constexpr uint64_t icon_dms[48] = { + 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x07FFFFFFFFE0ULL, 0x0FFFFFFFFFF0ULL, + 0x0FFFFFFFFFF0ULL, 0x0FFFFFFFFFF0ULL, 0x0F00000000F0ULL, 0x0F00000000F0ULL, 0x0F00000000F0ULL, 0x0F00000000F0ULL, + 0x0F0FFFFFF0F0ULL, 0x0F0FFFFFF0F0ULL, 0x0F0FFFFFF0F0ULL, 0x0F0FFFFFF0F0ULL, 0x0F00000000F0ULL, 0x0F00000000F0ULL, + 0x0F0FFFFFF0F0ULL, 0x0F0FFFFFF0F0ULL, 0x0F0FFFFFF0F0ULL, 0x0F0FFFFFF0F0ULL, 0x0F00000000F0ULL, 0x0F00000000F0ULL, + 0x0F0FFFF000F0ULL, 0x0F0FFFF000F0ULL, 0x0F0FFFF000F0ULL, 0x0F0FFFF000F0ULL, 0x0F00000000F0ULL, 0x0F00000000F0ULL, + 0x0F00000000F0ULL, 0x0F00000000F0ULL, 0x0F7FFFFFFFF0ULL, 0x0FFFFFFFFFF0ULL, 0x0FFFFFFFFFF0ULL, 0x0FFFFFFFFFE0ULL, + 0x0FF000000000ULL, 0x0FE000000000ULL, 0x0FC000000000ULL, 0x0F8000000000ULL, 0x0F0000000000ULL, 0x0E0000000000ULL, + 0x0C0000000000ULL, 0x080000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, +}; + +static constexpr uint64_t icon_channel[48] = { + 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x0FFFFFFFC000ULL, 0x0FFFFFFFC000ULL, + 0x0FFFFFFFC000ULL, 0x0FFFFFFFC000ULL, 0x0F000003C000ULL, 0x0F000003C000ULL, 0x0F000003C000ULL, 0x0F000003C000ULL, + 0x0F000003C3F0ULL, 0x0F000003C3F0ULL, 0x0F000003C3F0ULL, 0x0F000003C3F0ULL, 0x0F000003C3F0ULL, 0x0F000003C3F0ULL, + 0x0F000003C3F0ULL, 0x0F000003C3F0ULL, 0x0F000003C3F0ULL, 0x0F000003C3F0ULL, 0x0F7FFFFFC3F0ULL, 0x0FFFFFFFC3F0ULL, + 0x0FFFFFFFC3F0ULL, 0x0FFFFFFFC3F0ULL, 0x0FF0000003F0ULL, 0x0FE0000003F0ULL, 0x0FC0000003F0ULL, 0x0F80000003F0ULL, + 0x0F0FFFFFFFF0ULL, 0x0E0FFFFFFFF0ULL, 0x0C0FFFFFFFF0ULL, 0x080FFFFFFFF0ULL, 0x000FFFFFFFF0ULL, 0x000FFFFFFFF0ULL, + 0x000000000FF0ULL, 0x0000000007F0ULL, 0x0000000003F0ULL, 0x0000000001F0ULL, 0x0000000000F0ULL, 0x000000000070ULL, + 0x000000000030ULL, 0x000000000010ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, +}; + +static constexpr uint64_t icon_positions[48] = { + 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x00001FF80000ULL, 0x00007FFE0000ULL, + 0x0001FFFF8000ULL, 0x0003FFFFC000ULL, 0x0007FFFFE000ULL, 0x000FF00FF000ULL, 0x000FC003F000ULL, 0x001F8001F800ULL, + 0x001F0000F800ULL, 0x003F07E0FC00ULL, 0x003E0FF07C00ULL, 0x003E1FF87C00ULL, 0x003E1FF87C00ULL, 0x003E1FF87C00ULL, + 0x003E1FF87C00ULL, 0x003E1FF87C00ULL, 0x003E1FF87C00ULL, 0x003E0FF07C00ULL, 0x003E07E07C00ULL, 0x001F0000F800ULL, + 0x001F0000F800ULL, 0x001F8001F800ULL, 0x000F8001F000ULL, 0x000FC003F000ULL, 0x0007C003E000ULL, 0x0007E007E000ULL, + 0x0003F00FC000ULL, 0x0003F00FC000ULL, 0x0001F81F8000ULL, 0x0001FC3F8000ULL, 0x0000FC3F0000ULL, 0x00007E7E0000ULL, + 0x00003FFC0000ULL, 0x00003FFC0000ULL, 0x00001FF80000ULL, 0x00000FF00000ULL, 0x00000FF00000ULL, 0x000007E00000ULL, + 0x000003C00000ULL, 0x000001800000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, +}; + +static constexpr uint64_t icon_recents[48] = { + 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, + 0x00000FFF0000ULL, 0x00003FFFC000ULL, 0x0000FFFFF000ULL, 0x0001FFFFF800ULL, 0x0007FFFFFE00ULL, 0x000FF801FF00ULL, + 0x000FE0007F00ULL, 0x001FC0003F80ULL, 0x003F00000FC0ULL, 0x003F00000FC0ULL, 0x007E00E007E0ULL, 0x007C00E003E0ULL, + 0x00FC00E003F0ULL, 0x00F800E001F0ULL, 0x00F800E001F0ULL, 0x00F800E001F0ULL, 0x00F800E001F0ULL, 0x00F800E001F0ULL, + 0x3FFFC0F001F0ULL, 0x1FFF80FC01F0ULL, 0x0FFF00FF01F0ULL, 0x07FE003F81F0ULL, 0x03FC001FC1F0ULL, 0x01F80007C3F0ULL, + 0x00F0000183E0ULL, 0x0060000007E0ULL, 0x000000000FC0ULL, 0x000000000FC0ULL, 0x0001C0003F80ULL, 0x0003E0007F00ULL, + 0x0007F801FF00ULL, 0x0007FFFFFE00ULL, 0x0001FFFFF800ULL, 0x0000FFFFF000ULL, 0x00003FFFC000ULL, 0x00000FFF0000ULL, + 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, +}; + +static constexpr uint64_t icon_heard[48] = { + 0x000000000000ULL, 0x000000000000ULL, 0x000800000000ULL, 0x001C00000000ULL, 0x003E01FF8000ULL, 0x007F07FFE000ULL, + 0x007E1FFFF800ULL, 0x00FC3FFFFC00ULL, 0x00F87FFFFE00ULL, 0x01F8FF00FF00ULL, 0x01F0FC003F00ULL, 0x01F1F8001F80ULL, + 0x03E1F0000F80ULL, 0x03E3F07E0FC0ULL, 0x03E3E0FF07C0ULL, 0x03E3E1FF87C0ULL, 0x03E3E1FF87C0ULL, 0x03E3E1FF87C0ULL, + 0x03E3E1FF8000ULL, 0x03E3E1FF8000ULL, 0x03E3E1FF8000ULL, 0x03E3E0FF0000ULL, 0x03E3F07E0000ULL, 0x03E1F0000000ULL, + 0x01F1F8000000ULL, 0x01F1F8000000ULL, 0x01F8FC000000ULL, 0x00F87E000000ULL, 0x00FC7F800000ULL, 0x007E3FC00000ULL, + 0x007F1FE00000ULL, 0x003E0FF00000ULL, 0x001C03F00000ULL, 0x000801F80000ULL, 0x000000F80000ULL, 0x000000FC0000ULL, + 0x0000007C07C0ULL, 0x0000007E07C0ULL, 0x0000003F0FC0ULL, 0x0000003FFFC0ULL, 0x0000001FFF80ULL, 0x0000000FFF00ULL, + 0x00000007FE00ULL, 0x00000003FC00ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, +}; + +static constexpr uint64_t icon_favorites[48] = { + 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000001800000ULL, 0x000001800000ULL, + 0x000003C00000ULL, 0x000003C00000ULL, 0x000003C00000ULL, 0x000007E00000ULL, 0x000007E00000ULL, 0x00000FF00000ULL, + 0x00000FF00000ULL, 0x00001FF80000ULL, 0x00001FF80000ULL, 0x00001E780000ULL, 0x00003E7C0000ULL, 0x003FFC3FFC00ULL, + 0x0FFFFC3FFFF0ULL, 0x0FFFF81FFFF0ULL, 0x03FFF81FFFC0ULL, 0x01F800001F80ULL, 0x00FC00003F00ULL, 0x007E00007E00ULL, + 0x003F8001FC00ULL, 0x001FC003F800ULL, 0x000FE007F000ULL, 0x0003E007C000ULL, 0x0003E007C000ULL, 0x0003C003C000ULL, + 0x0003C003C000ULL, 0x0003C3C3C000ULL, 0x0007CFF3E000ULL, 0x00079FF9E000ULL, 0x0007FFFFE000ULL, 0x0007FE7FE000ULL, + 0x000FFC3FF000ULL, 0x000FF00FF000ULL, 0x000FC003F000ULL, 0x000F8001F000ULL, 0x001E00007000ULL, 0x001800001800ULL, + 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, +}; + +using IconBitmap = const uint64_t *; + +IconBitmap iconBitmapForKind(IconKind kind) +{ + switch (kind) { + case IconKind::ALL_MESSAGES: + return icon_all_messages; + case IconKind::DMS: + return icon_dms; + case IconKind::CHANNEL: + return icon_channel; + case IconKind::POSITIONS: + return icon_positions; + case IconKind::RECENTS: + return icon_recents; + case IconKind::HEARD: + return icon_heard; + case IconKind::FAVORITES: + return icon_favorites; + case IconKind::GENERIC: + default: + return icon_generic_apps; + } +} + +GridLayout computeLayout(const InkHUD::Applet *applet) +{ + GridLayout layout; + + const uint16_t w = applet->width(); + const uint16_t h = applet->height(); + + layout.footerH = InkHUD::Applet::fontSmall.lineHeight() + (FOOTER_PAD * 2); + layout.bodyTop = BODY_MARGIN_Y; + layout.bodyBottom = (h > (layout.footerH + BODY_MARGIN_Y)) ? (h - layout.footerH - BODY_MARGIN_Y) : layout.bodyTop; + + const uint16_t bodyW = (w > (BODY_MARGIN_X * 2)) ? (w - (BODY_MARGIN_X * 2)) : 1; + const uint16_t bodyH = (layout.bodyBottom > layout.bodyTop) ? (layout.bodyBottom - layout.bodyTop) : 1; + const uint16_t gapsX = SLOT_GAP_X * (GRID_COLS - 1); + const uint16_t gapsY = SLOT_GAP_Y * (GRID_ROWS - 1); + + layout.slotW = (bodyW > gapsX) ? ((bodyW - gapsX) / GRID_COLS) : 1; + layout.slotH = (bodyH > gapsY) ? ((bodyH - gapsY) / GRID_ROWS) : 1; + + const uint16_t maxIconW = (layout.slotW > 6) ? (layout.slotW - 6) : layout.slotW; + const uint16_t maxIconH = (layout.slotH > (InkHUD::Applet::fontSmall.lineHeight() + LABEL_GAP_Y + LABEL_BOTTOM_PAD + 6)) + ? (layout.slotH - InkHUD::Applet::fontSmall.lineHeight() - LABEL_GAP_Y - LABEL_BOTTOM_PAD - 6) + : layout.slotH / 2; + layout.iconBox = std::max(20, std::min(maxIconW, maxIconH)); + return layout; +} + +std::string lowercase(const char *name) +{ + if (!name) + return ""; + std::string out(name); + std::transform(out.begin(), out.end(), out.begin(), [](unsigned char c) { return (char)std::tolower(c); }); + return out; +} + +IconKind iconKindForAppletName(const char *name) +{ + const std::string lower = lowercase(name); + if (lower.find("all message") != std::string::npos || lower.find("messages") != std::string::npos) + return IconKind::ALL_MESSAGES; + if (lower.find("dm") != std::string::npos) + return IconKind::DMS; + if (lower.find("channel") != std::string::npos) + return IconKind::CHANNEL; + if (lower.find("position") != std::string::npos) + return IconKind::POSITIONS; + if (lower.find("recent") != std::string::npos) + return IconKind::RECENTS; + if (lower.find("heard") != std::string::npos) + return IconKind::HEARD; + if (lower.find("favorite") != std::string::npos) + return IconKind::FAVORITES; + return IconKind::GENERIC; +} + +void drawIconBitmapScaled(InkHUD::Applet *applet, IconBitmap bmp48, int16_t left, int16_t top, uint16_t boxSize, uint16_t color) +{ + if (!bmp48 || boxSize == 0) + return; + + auto srcOn = [bmp48](int16_t sx, int16_t sy) -> bool { + if (sx < 0 || sy < 0 || sx >= ICON_NATIVE_SIZE || sy >= ICON_NATIVE_SIZE) + return false; + const uint64_t rowBits = bmp48[sy]; + return (rowBits & (1ULL << (47 - sx))) != 0; + }; + + for (uint16_t y = 0; y < boxSize; y++) { + const uint8_t srcY = (uint8_t)((y * ICON_NATIVE_SIZE) / boxSize); + for (uint16_t x = 0; x < boxSize; x++) { + const uint8_t srcX = (uint8_t)((x * ICON_NATIVE_SIZE) / boxSize); + if (!srcOn(srcX, srcY)) + continue; + + const uint16_t w = std::min(ICON_OUTLINE_STROKE, boxSize - x); + const uint16_t h = std::min(ICON_OUTLINE_STROKE, boxSize - y); + applet->fillRect(left + x, top + y, w, h, color); + } + } +} +} // namespace + +InkHUD::AppSwitcherApplet::AppSwitcherApplet() +{ + alwaysRender = true; +} + +void InkHUD::AppSwitcherApplet::rebuildActiveAppletList() +{ + activeAppletIndices.clear(); + + const auto &settings = inkhud->persistence->settings; + const uint8_t tileCount = std::min(settings.userTiles.count, Persistence::MAX_TILES_GLOBAL); + const uint8_t focusedTile = (tileCount > 0) ? std::min(settings.userTiles.focused, tileCount - 1) : 0; + + // Applets displayed on other tiles should not be selectable here. + std::vector occupiedOnOtherTiles(inkhud->userApplets.size(), false); + for (uint8_t tile = 0; tile < tileCount; tile++) { + if (tile == focusedTile) + continue; + + const uint8_t appletIndex = settings.userTiles.displayedUserApplet[tile]; + if (appletIndex < occupiedOnOtherTiles.size()) + occupiedOnOtherTiles[appletIndex] = true; + } + + for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { + Applet *a = inkhud->userApplets.at(i); + if (a && a->isActive() && !occupiedOnOtherTiles[i]) + activeAppletIndices.push_back(i); + } +} + +uint8_t InkHUD::AppSwitcherApplet::cardsPerPage() const +{ + return GRID_COLS * GRID_ROWS; +} + +uint8_t InkHUD::AppSwitcherApplet::currentPage() const +{ + const uint8_t cpp = cardsPerPage(); + if (cpp == 0) + return 0; + return selectedIndex / cpp; +} + +void InkHUD::AppSwitcherApplet::stepPage(int8_t delta) +{ + if (activeAppletIndices.empty()) + return; + + const uint8_t cpp = cardsPerPage(); + const uint8_t pageCount = std::max(1, (activeAppletIndices.size() + cpp - 1) / cpp); + int16_t nextPage = (int16_t)currentPage() + delta; + while (nextPage < 0) + nextPage += pageCount; + while (nextPage >= pageCount) + nextPage -= pageCount; + + selectedIndex = std::min((uint8_t)(nextPage * cpp), activeAppletIndices.size() - 1); + requestUpdate(Drivers::EInk::UpdateTypes::FAST); +} + +void InkHUD::AppSwitcherApplet::clampSelection() +{ + if (activeAppletIndices.empty()) { + selectedIndex = 0; + return; + } + + if (selectedIndex >= activeAppletIndices.size()) + selectedIndex = activeAppletIndices.size() - 1; +} + +void InkHUD::AppSwitcherApplet::activateSelectedApplet() +{ + if (activeAppletIndices.empty()) { + sendToBackground(); + requestUpdate(Drivers::EInk::UpdateTypes::FAST); + return; + } + + const uint8_t appletIndex = activeAppletIndices.at(selectedIndex); + + sendToBackground(); + inkhud->showApplet(appletIndex); +} + +void InkHUD::AppSwitcherApplet::onForeground() +{ + rebuildActiveAppletList(); + clampSelection(); + handleInput = true; + lockRequests = true; + requestUpdate(Drivers::EInk::UpdateTypes::FAST); +} + +void InkHUD::AppSwitcherApplet::onBackground() +{ + handleInput = false; + lockRequests = false; + + if (borrowedTileOwner) + borrowedTileOwner->bringToForeground(); + + Tile *t = getTile(); + if (t) + t->assignApplet(borrowedTileOwner); + borrowedTileOwner = nullptr; +} + +void InkHUD::AppSwitcherApplet::show(Tile *t) +{ + if (!t) + return; + + borrowedTileOwner = t->getAssignedApplet(); + if (borrowedTileOwner) + borrowedTileOwner->sendToBackground(); + + t->assignApplet(this); + bringToForeground(); +} + +void InkHUD::AppSwitcherApplet::onRender(bool full) +{ + (void)full; + + const GridLayout layout = computeLayout(this); + const uint8_t cpp = cardsPerPage(); + const uint8_t page = currentPage(); + const uint8_t pageStart = page * cpp; + + setFont(fontMedium); + setTextColor(BLACK); + + fillRect(0, 0, width(), height(), WHITE); + drawRect(0, 0, width(), height(), BLACK); + + if (activeAppletIndices.empty()) { + setFont(fontSmall); + printAt(width() / 2, height() / 2, "No Available Applets", CENTER, MIDDLE); + return; + } + + for (uint8_t i = 0; i < cpp; i++) { + const uint8_t idx = pageStart + i; + if (idx >= activeAppletIndices.size()) + break; + + const uint8_t row = i / GRID_COLS; + const uint8_t col = i % GRID_COLS; + const int16_t slotL = BODY_MARGIN_X + (col * (layout.slotW + SLOT_GAP_X)); + const int16_t slotT = layout.bodyTop + (row * (layout.slotH + SLOT_GAP_Y)); + const bool selected = (idx == selectedIndex); + + const uint8_t appletIndex = activeAppletIndices.at(idx); + Applet *a = inkhud->userApplets.at(appletIndex); + if (!a) + continue; + + const int16_t iconLeft = slotL + ((layout.slotW - layout.iconBox) / 2); + const int16_t iconTop = slotT + 1; + + // Requested style: icon in outlined rounded square only (no filled box, no outer app card). + drawRoundRect(iconLeft, iconTop, layout.iconBox, layout.iconBox, ICON_RADIUS, BLACK); + if (selected) + drawRoundRect(iconLeft + 2, iconTop + 2, layout.iconBox - 4, layout.iconBox - 4, ICON_RADIUS, BLACK); + + const IconBitmap bmp = iconBitmapForKind(iconKindForAppletName(a->name)); + drawIconBitmapScaled(this, bmp, iconLeft + 3, iconTop + 3, layout.iconBox - 6, BLACK); + + setFont(fontSmall); + std::string label = a->name ? a->name : "Applet"; + const uint16_t maxLabelW = layout.slotW > 4 ? (layout.slotW - 4) : layout.slotW; + if (getTextWidth(label) > maxLabelW) { + while (!label.empty() && getTextWidth(label + "...") > maxLabelW) + label.pop_back(); + label = label.empty() ? "..." : label + "..."; + } + const int16_t labelY = iconTop + layout.iconBox + LABEL_GAP_Y; + setTextColor(BLACK); + printAt(slotL + (layout.slotW / 2), labelY, label.c_str(), CENTER, TOP); + + if (a->isForeground()) + fillCircle(iconLeft + layout.iconBox - 4, iconTop + 4, 2, BLACK); + } + + const uint8_t pageCount = std::max(1, (activeAppletIndices.size() + cpp - 1) / cpp); + if (pageCount > 1) { + setFont(fontSmall); + setTextColor(BLACK); + const int16_t footerY = height() - layout.footerH + FOOTER_PAD; + printAt(TITLE_H_PAD, footerY, "<", LEFT, TOP); + printAt(width() - TITLE_H_PAD, footerY, ">", RIGHT, TOP); + const std::string pageText = std::to_string(page + 1) + "/" + std::to_string(pageCount); + printAt(width() / 2, footerY, pageText.c_str(), CENTER, TOP); + } +} + +bool InkHUD::AppSwitcherApplet::onTouchPoint(uint16_t x, uint16_t y, bool longPress) +{ + (void)longPress; + + Tile *t = getTile(); + if (!t || activeAppletIndices.empty()) + return true; + + const uint16_t tileL = t->getLeft(); + const uint16_t tileT = t->getTop(); + const uint16_t tileR = tileL + t->getWidth(); + const uint16_t tileB = tileT + t->getHeight(); + if (x < tileL || x >= tileR || y < tileT || y >= tileB) + return false; + + const GridLayout layout = computeLayout(this); + const uint8_t cpp = cardsPerPage(); + const uint8_t page = currentPage(); + const uint8_t pageStart = page * cpp; + const int16_t localX = (int16_t)x - (int16_t)tileL; + const int16_t localY = (int16_t)y - (int16_t)tileT; + + for (uint8_t i = 0; i < cpp; i++) { + const uint8_t idx = pageStart + i; + if (idx >= activeAppletIndices.size()) + break; + + const uint8_t row = i / GRID_COLS; + const uint8_t col = i % GRID_COLS; + const int16_t slotL = BODY_MARGIN_X + (col * (layout.slotW + SLOT_GAP_X)); + const int16_t slotT = layout.bodyTop + (row * (layout.slotH + SLOT_GAP_Y)); + + if (localX < slotL || localX >= (slotL + (int16_t)layout.slotW)) + continue; + if (localY < slotT || localY >= (slotT + (int16_t)layout.slotH)) + continue; + + selectedIndex = idx; + clampSelection(); + activateSelectedApplet(); + return true; + } + + const uint8_t pageCount = std::max(1, (activeAppletIndices.size() + cpp - 1) / cpp); + if (pageCount <= 1) + return true; + + const int16_t footerTop = height() - layout.footerH; + if (localY >= footerTop) { + if (localX < (int16_t)(width() / 3)) + stepPage(-1); + else if (localX >= (int16_t)((width() * 2) / 3)) + stepPage(1); + } + + return true; +} + +void InkHUD::AppSwitcherApplet::onButtonShortPress() +{ + if (activeAppletIndices.empty()) + return; + + selectedIndex = (selectedIndex + 1) % activeAppletIndices.size(); + clampSelection(); + requestUpdate(Drivers::EInk::UpdateTypes::FAST); +} + +void InkHUD::AppSwitcherApplet::onButtonLongPress() +{ + activateSelectedApplet(); +} + +void InkHUD::AppSwitcherApplet::onExitShort() +{ + sendToBackground(); + requestUpdate(Drivers::EInk::UpdateTypes::FAST); +} + +void InkHUD::AppSwitcherApplet::onNavUp() +{ + if (activeAppletIndices.empty()) + return; + + if (selectedIndex == 0) + selectedIndex = activeAppletIndices.size() - 1; + else + selectedIndex--; + + clampSelection(); + requestUpdate(Drivers::EInk::UpdateTypes::FAST); +} + +void InkHUD::AppSwitcherApplet::onNavDown() +{ + if (activeAppletIndices.empty()) + return; + + selectedIndex = (selectedIndex + 1) % activeAppletIndices.size(); + clampSelection(); + requestUpdate(Drivers::EInk::UpdateTypes::FAST); +} + +#endif diff --git a/src/graphics/niche/InkHUD/Applets/System/AppSwitcher/AppSwitcherApplet.h b/src/graphics/niche/InkHUD/Applets/System/AppSwitcher/AppSwitcherApplet.h new file mode 100644 index 000000000..01ea561ae --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/System/AppSwitcher/AppSwitcherApplet.h @@ -0,0 +1,51 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +#pragma once + +#include "configuration.h" + +#include "graphics/niche/InkHUD/SystemApplet.h" + +#include + +namespace NicheGraphics::InkHUD +{ + +class Tile; + +class AppSwitcherApplet : public SystemApplet +{ + public: + AppSwitcherApplet(); + + void onForeground() override; + void onBackground() override; + void onRender(bool full) override; + + void onButtonShortPress() override; + void onButtonLongPress() override; + void onExitShort() override; + void onNavUp() override; + void onNavDown() override; + bool onTouchPoint(uint16_t x, uint16_t y, bool longPress) override; + + // Open the app switcher on a user tile and temporarily replace the tile's owner. + void show(Tile *t); + + private: + void rebuildActiveAppletList(); + void clampSelection(); + uint8_t cardsPerPage() const; + uint8_t currentPage() const; + void stepPage(int8_t delta); + void activateSelectedApplet(); + + std::vector activeAppletIndices; + uint8_t selectedIndex = 0; + + Applet *borrowedTileOwner = nullptr; +}; + +} // namespace NicheGraphics::InkHUD + +#endif diff --git a/src/graphics/niche/InkHUD/Applets/System/Keyboard/KeyboardApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Keyboard/KeyboardApplet.cpp index 57581d56b..cb2b59953 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Keyboard/KeyboardApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Keyboard/KeyboardApplet.cpp @@ -1,155 +1,100 @@ #ifdef MESHTASTIC_INCLUDE_INKHUD #include "./KeyboardApplet.h" +#include + using namespace NicheGraphics; +namespace +{ +bool usePortraitKeyboardSizing() +{ + InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); + return inkhud && inkhud->height() > inkhud->width(); +} +} // namespace + InkHUD::KeyboardApplet::KeyboardApplet() { - // Calculate row widths - for (uint8_t row = 0; row < KBD_ROWS; row++) { - rowWidths[row] = 0; - for (uint8_t col = 0; col < KBD_COLS; col++) - rowWidths[row] += keyWidths[row * KBD_COLS + col]; - } + mode = MODE_TEXT; + lastTypingMode = MODE_TEXT; + emotePage = 0; + selectedKey = 0; + prevSelectedKey = 0; + normalizeSelection(); } void InkHUD::KeyboardApplet::onRender(bool full) { - uint16_t em = fontSmall.lineHeight(); // 16 pt - uint16_t keyH = Y(1.0) / KBD_ROWS; - int16_t keyTopPadding = (keyH - fontSmall.lineHeight()) / 2; + const bool showSelection = showSelectionHighlight(); - if (full) { // Draw full keyboard - for (uint8_t row = 0; row < KBD_ROWS; row++) { - - // Calculate the remaining space to be used as padding - int16_t keyXPadding = X(1.0) - ((rowWidths[row] * em) >> 4); - - // Draw keys - uint16_t xPos = 0; - for (uint8_t col = 0; col < KBD_COLS; col++) { - Color fgcolor = BLACK; - uint8_t index = row * KBD_COLS + col; - uint16_t keyX = ((xPos * em) >> 4) + ((col * keyXPadding) / (KBD_COLS - 1)); - uint16_t keyY = row * keyH; - uint16_t keyW = (keyWidths[index] * em) >> 4; - if (index == selectedKey) { - fgcolor = WHITE; - fillRect(keyX, keyY, keyW, keyH, BLACK); - } - drawKeyLabel(keyX, keyY + keyTopPadding, keyW, keys[index], fgcolor); - xPos += keyWidths[index]; - } - } - } else { // Only draw the difference - if (selectedKey != prevSelectedKey) { - // Draw previously selected key - uint8_t row = prevSelectedKey / KBD_COLS; - int16_t keyXPadding = X(1.0) - ((rowWidths[row] * em) >> 4); - uint16_t xPos = 0; - for (uint8_t i = prevSelectedKey - (prevSelectedKey % KBD_COLS); i < prevSelectedKey; i++) - xPos += keyWidths[i]; - uint16_t keyX = ((xPos * em) >> 4) + (((prevSelectedKey % KBD_COLS) * keyXPadding) / (KBD_COLS - 1)); - uint16_t keyY = row * keyH; - uint16_t keyW = (keyWidths[prevSelectedKey] * em) >> 4; - fillRect(keyX, keyY, keyW, keyH, WHITE); - drawKeyLabel(keyX, keyY + keyTopPadding, keyW, keys[prevSelectedKey], BLACK); - - // Draw newly selected key - row = selectedKey / KBD_COLS; - keyXPadding = X(1.0) - ((rowWidths[row] * em) >> 4); - xPos = 0; - for (uint8_t i = selectedKey - (selectedKey % KBD_COLS); i < selectedKey; i++) - xPos += keyWidths[i]; - keyX = ((xPos * em) >> 4) + (((selectedKey % KBD_COLS) * keyXPadding) / (KBD_COLS - 1)); - keyY = row * keyH; - keyW = (keyWidths[selectedKey] * em) >> 4; - fillRect(keyX, keyY, keyW, keyH, BLACK); - drawKeyLabel(keyX, keyY + keyTopPadding, keyW, keys[selectedKey], WHITE); - } + if (full) { + for (uint8_t i = 0; i < KBD_KEY_COUNT; i++) + drawKey(i, showSelection && i == selectedKey); + } else if (showSelection && selectedKey != prevSelectedKey) { + drawKey(prevSelectedKey, false); + drawKey(selectedKey, true); } prevSelectedKey = selectedKey; } -// Draw the key label corresponding to the char -// for most keys it draws the character itself -// for ['\b', '\n', ' ', '\x1b'] it draws special glyphs -void InkHUD::KeyboardApplet::drawKeyLabel(uint16_t left, uint16_t top, uint16_t width, char key, Color color) +void InkHUD::KeyboardApplet::drawKey(uint8_t index, bool selected) { - if (key == '\b') { - // Draw backspace glyph: 13 x 9 px - /** - * [][][][][][][][][] - * [][] [] - * [][] [] [] [] - * [][] [] [] [] - * [][] [] [] - * [][] [] [] [] - * [][] [] [] [] - * [][] [] - * [][][][][][][][][] - */ - const uint8_t bsBitmap[] = {0x0f, 0xf8, 0x18, 0x08, 0x32, 0x28, 0x61, 0x48, 0xc0, - 0x88, 0x61, 0x48, 0x32, 0x28, 0x18, 0x08, 0x0f, 0xf8}; - uint16_t leftPadding = (width - 13) >> 1; - drawBitmap(left + leftPadding, top + 1, bsBitmap, 13, 9, color); - } else if (key == '\n') { - // Draw done glyph: 12 x 9 px - /** - * [][] - * [][] - * [][] - * [][] - * [][] - * [][] [][] - * [][] [][] - * [][][] - * [] - */ - const uint8_t doneBitmap[] = {0x00, 0x30, 0x00, 0x60, 0x00, 0xc0, 0x01, 0x80, 0x03, - 0x00, 0xc6, 0x00, 0x6c, 0x00, 0x38, 0x00, 0x10, 0x00}; - uint16_t leftPadding = (width - 12) >> 1; - drawBitmap(left + leftPadding, top + 1, doneBitmap, 12, 9, color); - } else if (key == ' ') { - // Draw space glyph: 13 x 9 px - /** - * - * - * - * - * [] [] - * [] [] - * [][][][][][][][][][][][][] - * - * - */ - const uint8_t spaceBitmap[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, - 0x08, 0x80, 0x08, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00}; - uint16_t leftPadding = (width - 13) >> 1; - drawBitmap(left + leftPadding, top + 1, spaceBitmap, 13, 9, color); - } else if (key == '\x1b') { - setTextColor(color); - std::string keyText = "ESC"; - uint16_t leftPadding = (width - getTextWidth(keyText)) >> 1; - printAt(left + leftPadding, top, keyText); - } else { - setTextColor(color); - if (key >= 0x61) - key -= 32; // capitalize - std::string keyText = std::string(1, key); - uint16_t leftPadding = (width - getTextWidth(keyText)) >> 1; - printAt(left + leftPadding, top, keyText); + uint16_t keyX = 0; + uint16_t keyY = 0; + uint16_t keyW = 0; + uint16_t keyH = 0; + if (!getKeyBounds(index, keyX, keyY, keyW, keyH)) + return; + if (keyW == 0 || keyH == 0) + return; + + // Translate absolute tile coordinates into applet-local coordinates. + const int16_t localX = keyX - getTile()->getLeft(); + const int16_t localY = keyY - getTile()->getTop(); + const bool enabled = isKeyEnabledAt(index); + + // Clean background first so hidden keys never leave stale pixels when mode changes. + fillRect(localX, localY, keyW, keyH, WHITE); + + if (!enabled) + return; + + fillRoundRect(localX, localY, keyW, keyH, KEY_RADIUS, selected ? BLACK : WHITE); + drawRoundRect(localX, localY, keyW, keyH, KEY_RADIUS, BLACK); + + const int16_t labelTop = localY + ((keyH - fontSmall.lineHeight()) / 2); + drawKeyLabel(localX, labelTop, keyW, getKeyLabelAt(index), selected ? WHITE : BLACK); +} + +void InkHUD::KeyboardApplet::drawKeyLabel(uint16_t left, uint16_t top, uint16_t width, const std::string &label, Color color) +{ + if (label.empty()) + return; + + setTextColor(color); + uint16_t textW = getTextWidth(label); + if (textW > width) { + // Keep labels readable in narrow keys. + textW = getTextWidth(".."); + printAt(left + ((width - textW) >> 1), top, ".."); + return; } + + uint16_t leftPadding = (width - textW) >> 1; + printAt(left + leftPadding, top, label); } void InkHUD::KeyboardApplet::onForeground() { - handleInput = true; // Intercept the button input for our applet - - // Select the first key + handleInput = true; + mode = MODE_TEXT; + lastTypingMode = MODE_TEXT; + emotePage = 0; selectedKey = 0; prevSelectedKey = 0; + normalizeSelection(); } void InkHUD::KeyboardApplet::onBackground() @@ -159,32 +104,12 @@ void InkHUD::KeyboardApplet::onBackground() void InkHUD::KeyboardApplet::onButtonShortPress() { - char key = keys[selectedKey]; - if (key == '\n') { - inkhud->freeTextDone(); - inkhud->closeKeyboard(); - } else if (key == '\x1b') { - inkhud->freeTextCancel(); - inkhud->closeKeyboard(); - } else { - inkhud->freeText(key); - } + inputSelectedKey(false); } void InkHUD::KeyboardApplet::onButtonLongPress() { - char key = keys[selectedKey]; - if (key == '\n') { - inkhud->freeTextDone(); - inkhud->closeKeyboard(); - } else if (key == '\x1b') { - inkhud->freeTextCancel(); - inkhud->closeKeyboard(); - } else { - if (key >= 0x61) - key -= 32; // capitalize - inkhud->freeText(key); - } + inputSelectedKey(true); } void InkHUD::KeyboardApplet::onExitShort() @@ -201,57 +126,377 @@ void InkHUD::KeyboardApplet::onExitLong() void InkHUD::KeyboardApplet::onNavUp() { - if (selectedKey < KBD_COLS) // wrap + if (selectedKey < KBD_COLS) selectedKey += KBD_COLS * (KBD_ROWS - 1); - else // move 1 row back + else selectedKey -= KBD_COLS; - // Request rendering over the previously drawn render - requestUpdate(EInk::UpdateTypes::FAST, false); - // Force an update to bypass lockRequests - inkhud->forceUpdate(EInk::UpdateTypes::FAST); + normalizeSelection(); + requestFastKeyboardRefresh(); } void InkHUD::KeyboardApplet::onNavDown() { selectedKey += KBD_COLS; - selectedKey %= (KBD_COLS * KBD_ROWS); - - // Request rendering over the previously drawn render - requestUpdate(EInk::UpdateTypes::FAST, false); - // Force an update to bypass lockRequests - inkhud->forceUpdate(EInk::UpdateTypes::FAST); + selectedKey %= KBD_KEY_COUNT; + normalizeSelection(); + requestFastKeyboardRefresh(); } void InkHUD::KeyboardApplet::onNavLeft() { - if (selectedKey % KBD_COLS == 0) // wrap + if (selectedKey % KBD_COLS == 0) selectedKey += KBD_COLS - 1; - else // move 1 column back + else selectedKey--; - // Request rendering over the previously drawn render - requestUpdate(EInk::UpdateTypes::FAST, false); - // Force an update to bypass lockRequests - inkhud->forceUpdate(EInk::UpdateTypes::FAST); + normalizeSelection(); + requestFastKeyboardRefresh(); } void InkHUD::KeyboardApplet::onNavRight() { - if (selectedKey % KBD_COLS == KBD_COLS - 1) // wrap + if (selectedKey % KBD_COLS == KBD_COLS - 1) selectedKey -= KBD_COLS - 1; - else // move 1 column forward + else selectedKey++; - // Request rendering over the previously drawn render - requestUpdate(EInk::UpdateTypes::FAST, false); - // Force an update to bypass lockRequests - inkhud->forceUpdate(EInk::UpdateTypes::FAST); + normalizeSelection(); + requestFastKeyboardRefresh(); +} + +bool InkHUD::KeyboardApplet::onTouchPoint(uint16_t x, uint16_t y, bool longPress) +{ + // If touch is outside our tile, let other handlers process it. + if (!getTile()) + return false; + const uint16_t tileL = getTile()->getLeft(); + const uint16_t tileT = getTile()->getTop(); + const uint16_t tileR = tileL + getTile()->getWidth(); + const uint16_t tileB = tileT + getTile()->getHeight(); + if (x < tileL || x >= tileR || y < tileT || y >= tileB) + return false; + + const int16_t hitIndex = getKeyIndexAt(x, y); + // Consume touches that land in keyboard whitespace/disabled cells so we don't + // fall back to generic short-press behavior (which would type the old selection). + if (hitIndex < 0) + return true; + + const uint8_t newSelected = (uint8_t)hitIndex; + if (selectedKey != newSelected) { + selectedKey = newSelected; + normalizeSelection(); + if (showSelectionHighlight()) + requestFastKeyboardRefresh(); + } + + if (!isKeyEnabledAt(selectedKey)) + return true; + + inputSelectedKey(longPress); + return true; +} + +bool InkHUD::KeyboardApplet::getKeyBounds(uint8_t index, uint16_t &left, uint16_t &top, uint16_t &width, uint16_t &height) +{ + if (index >= KBD_KEY_COUNT || !getTile()) + return false; + + const uint16_t tileW = getTile()->getWidth(); + const uint16_t tileH = getTile()->getHeight(); + const uint16_t tileL = getTile()->getLeft(); + const uint16_t tileT = getTile()->getTop(); + const uint8_t row = index / KBD_COLS; + const uint8_t col = index % KBD_COLS; + + const uint16_t totalGapY = KEY_GAP_Y * (KBD_ROWS + 1); + const uint16_t keyH = (tileH > totalGapY) ? ((tileH - totalGapY) / KBD_ROWS) : (tileH / KBD_ROWS); + top = tileT + KEY_GAP_Y + row * (keyH + KEY_GAP_Y); + height = keyH; + + const uint16_t totalGapX = KEY_GAP_X * (KBD_COLS + 1); + const uint16_t rowSpace = (tileW > totalGapX) ? (tileW - totalGapX) : tileW; + uint32_t rowUnits = 0; + const uint8_t rowStart = row * KBD_COLS; + for (uint8_t i = 0; i < KBD_COLS; i++) { + rowUnits += getKeyWidthAt(rowStart + i); + } + if (rowUnits == 0) + return false; + + uint32_t cursorX = tileL + KEY_GAP_X; + for (uint8_t i = 0; i < col; i++) { + const uint8_t rowIndex = rowStart + i; + const uint32_t keyW = ((uint32_t)rowSpace * getKeyWidthAt(rowIndex)) / rowUnits; + cursorX += keyW + KEY_GAP_X; + } + + left = (uint16_t)cursorX; + + if (col == (KBD_COLS - 1)) { + const uint32_t rightEdge = tileL + tileW - KEY_GAP_X; + width = (rightEdge > cursorX) ? (uint16_t)(rightEdge - cursorX) : 0; + } else { + width = (uint16_t)(((uint32_t)rowSpace * getKeyWidthAt(index)) / rowUnits); + } + + return true; +} + +int16_t InkHUD::KeyboardApplet::getKeyIndexAt(uint16_t x, uint16_t y) +{ + for (uint8_t i = 0; i < KBD_KEY_COUNT; i++) { + uint16_t keyL = 0; + uint16_t keyT = 0; + uint16_t keyW = 0; + uint16_t keyH = 0; + if (!getKeyBounds(i, keyL, keyT, keyW, keyH)) + return -1; + + if (keyW == 0 || keyH == 0) + continue; + + if (x >= keyL && x < (keyL + keyW) && y >= keyT && y < (keyT + keyH)) + return i; + } + + return -1; +} + +void InkHUD::KeyboardApplet::inputSelectedKey(bool longPress) +{ + inputKeyCode(getKeyCodeAt(selectedKey), longPress); +} + +void InkHUD::KeyboardApplet::inputKeyCode(int16_t keyCode, bool longPress) +{ + if (keyCode == KEY_NONE) + return; + + if (keyCode >= KEY_EMOTE_SLOT_BASE) { + const uint8_t slot = (uint8_t)(keyCode - KEY_EMOTE_SLOT_BASE); + const uint16_t emoteIndex = emotePage * EMOTE_SLOT_COUNT + slot; + if (emoteIndex < fontEmoteCount) + inkhud->freeText((char)fontEmotes[emoteIndex]); + return; + } + + switch (keyCode) { + case KEY_BACKSPACE: + inkhud->freeText('\b'); + return; + case KEY_SEND: + inkhud->freeTextDone(); + inkhud->closeKeyboard(); + return; + case KEY_EMOTE_TOGGLE: + toggleEmoteMode(); + return; + case KEY_PUNCT_TOGGLE: + case KEY_ALPHA_TOGGLE: + togglePunctuationMode(); + return; + case KEY_EMOTE_UP: + pageEmotes(false); + return; + case KEY_EMOTE_DOWN: + pageEmotes(true); + return; + default: + break; + } + + if (keyCode >= 0 && keyCode <= 0xFF) { + char key = (char)keyCode; + if (longPress && key >= 'a' && key <= 'z') + key = (char)std::toupper((unsigned char)key); + inkhud->freeText(key); + } +} + +int16_t InkHUD::KeyboardApplet::getKeyCodeAt(uint8_t index) const +{ + if (index >= KBD_KEY_COUNT) + return KEY_NONE; + + if (mode == MODE_TEXT) + return textKeys[index]; + if (mode == MODE_PUNCT) + return punctKeys[index]; + + // Emote mode + if (index < EMOTE_SLOT_COUNT) { + const uint16_t emoteIndex = emotePage * EMOTE_SLOT_COUNT + index; + if (emoteIndex < fontEmoteCount) + return KEY_EMOTE_SLOT_BASE + index; + return KEY_NONE; + } + + // Emote controls on the bottom row + switch (index - EMOTE_SLOT_COUNT) { + case 0: + return KEY_EMOTE_UP; + case 1: + return KEY_EMOTE_DOWN; + case 2: + return KEY_ALPHA_TOGGLE; + case 3: + return ','; + case 4: + return ' '; + case 5: + return '.'; + case 6: + return KEY_SEND; + case 7: + return KEY_BACKSPACE; + default: + return KEY_NONE; + } +} + +uint16_t InkHUD::KeyboardApplet::getKeyWidthAt(uint8_t index) const +{ + if (index >= KBD_KEY_COUNT) + return 0; + + if (mode == MODE_EMOTE) + return emoteKeyWidths[index]; + return typingKeyWidths[index]; +} + +std::string InkHUD::KeyboardApplet::getKeyLabelAt(uint8_t index) const +{ + const int16_t keyCode = getKeyCodeAt(index); + if (keyCode == KEY_NONE) + return ""; + + if (keyCode >= KEY_EMOTE_SLOT_BASE) { + const uint8_t slot = (uint8_t)(keyCode - KEY_EMOTE_SLOT_BASE); + const uint16_t emoteIndex = emotePage * EMOTE_SLOT_COUNT + slot; + if (emoteIndex < fontEmoteCount) + return std::string(1, (char)fontEmotes[emoteIndex]); + return ""; + } + + switch (keyCode) { + case KEY_BACKSPACE: + return "DEL"; + case KEY_SEND: + return "SEND"; + case KEY_EMOTE_TOGGLE: + return std::string(1, (char)0x03); // Smiling face icon from InkHUD emote font map + case KEY_PUNCT_TOGGLE: + return "!#1"; + case KEY_ALPHA_TOGGLE: + return "ABC"; + case KEY_EMOTE_UP: + return "UP"; + case KEY_EMOTE_DOWN: + return "DN"; + default: + break; + } + + if (keyCode >= 0 && keyCode <= 0xFF) { + const char c = (char)keyCode; + if (c == ' ') + return "SPACE"; + if (c >= 'a' && c <= 'z') + return std::string(1, (char)std::toupper((unsigned char)c)); + return std::string(1, c); + } + + return ""; +} + +bool InkHUD::KeyboardApplet::isKeyEnabledAt(uint8_t index) const +{ + return getKeyCodeAt(index) != KEY_NONE; +} + +void InkHUD::KeyboardApplet::normalizeSelection() +{ + if (selectedKey >= KBD_KEY_COUNT) + selectedKey = 0; + + if (isKeyEnabledAt(selectedKey)) + return; + + for (uint8_t i = 0; i < KBD_KEY_COUNT; i++) { + if (isKeyEnabledAt(i)) { + selectedKey = i; + return; + } + } +} + +void InkHUD::KeyboardApplet::togglePunctuationMode() +{ + if (mode == MODE_EMOTE) { + mode = lastTypingMode; + } else { + mode = (mode == MODE_TEXT) ? MODE_PUNCT : MODE_TEXT; + lastTypingMode = mode; + } + + normalizeSelection(); + requestFastKeyboardRefresh(true); +} + +void InkHUD::KeyboardApplet::toggleEmoteMode() +{ + if (mode == MODE_EMOTE) { + mode = lastTypingMode; + } else { + lastTypingMode = mode; + mode = MODE_EMOTE; + } + + emotePage = 0; + normalizeSelection(); + requestFastKeyboardRefresh(true); +} + +void InkHUD::KeyboardApplet::pageEmotes(bool down) +{ + if (mode != MODE_EMOTE) + return; + + const uint8_t maxPage = (fontEmoteCount == 0) ? 0 : (uint8_t)((fontEmoteCount - 1) / EMOTE_SLOT_COUNT); + + if (down) { + if (emotePage < maxPage) + emotePage++; + } else { + if (emotePage > 0) + emotePage--; + } + + normalizeSelection(); + requestFastKeyboardRefresh(true); +} + +void InkHUD::KeyboardApplet::requestFastKeyboardRefresh(bool full) +{ + requestUpdate(EInk::UpdateTypes::FAST, full); +} + +bool InkHUD::KeyboardApplet::showSelectionHighlight() const +{ + // On touch-capable devices, prioritize input throughput over per-key highlight updates. + // E-ink refresh can lag rapid taps; skipping highlight avoids update-induced input latency. + return !inkhud->hasTouchEnabledProvider(); } uint16_t InkHUD::KeyboardApplet::getKeyboardHeight() { - const uint16_t keyH = fontSmall.lineHeight() * 1.2; - return keyH * KBD_ROWS; + // Keep touch keys tall and roomy for finger input. + // In portrait orientation we increase row height for larger touch targets. + const uint16_t rowUnit = fontSmall.lineHeight() + 8; + const uint8_t rowScale = usePortraitKeyboardSizing() ? 3 : 2; + const uint16_t keyH = rowUnit * rowScale; + return (keyH * KBD_ROWS) + (KEY_GAP_Y * (KBD_ROWS + 1)); } #endif diff --git a/src/graphics/niche/InkHUD/Applets/System/Keyboard/KeyboardApplet.h b/src/graphics/niche/InkHUD/Applets/System/Keyboard/KeyboardApplet.h index 0ae181a2c..b71376459 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Keyboard/KeyboardApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Keyboard/KeyboardApplet.h @@ -12,6 +12,7 @@ System Applet to render an on-screen keyboard #include "graphics/niche/InkHUD/InkHUD.h" #include "graphics/niche/InkHUD/SystemApplet.h" #include + namespace NicheGraphics::InkHUD { @@ -31,34 +32,111 @@ class KeyboardApplet : public SystemApplet void onNavDown() override; void onNavLeft() override; void onNavRight() override; + bool onTouchPoint(uint16_t x, uint16_t y, bool longPress) override; static uint16_t getKeyboardHeight(); // used to set the keyboard tile height private: - void drawKeyLabel(uint16_t left, uint16_t top, uint16_t width, char key, Color color); + enum KeyCode : int16_t { + KEY_NONE = -1, + KEY_BACKSPACE = 256, + KEY_SEND, + KEY_EMOTE_TOGGLE, + KEY_PUNCT_TOGGLE, + KEY_ALPHA_TOGGLE, + KEY_EMOTE_UP, + KEY_EMOTE_DOWN, + KEY_EMOTE_SLOT_BASE = 512 + }; + + enum KeyboardMode : uint8_t { MODE_TEXT = 0, MODE_PUNCT = 1, MODE_EMOTE = 2 }; + + void drawKey(uint8_t index, bool selected); + void drawKeyLabel(uint16_t left, uint16_t top, uint16_t width, const std::string &label, Color color); + bool getKeyBounds(uint8_t index, uint16_t &left, uint16_t &top, uint16_t &width, uint16_t &height); + int16_t getKeyIndexAt(uint16_t x, uint16_t y); + void inputSelectedKey(bool longPress); + void inputKeyCode(int16_t keyCode, bool longPress); + int16_t getKeyCodeAt(uint8_t index) const; + uint16_t getKeyWidthAt(uint8_t index) const; + std::string getKeyLabelAt(uint8_t index) const; + bool isKeyEnabledAt(uint8_t index) const; + void normalizeSelection(); + void togglePunctuationMode(); + void toggleEmoteMode(); + void pageEmotes(bool down); + void requestFastKeyboardRefresh(bool full = false); + bool showSelectionHighlight() const; static const uint8_t KBD_COLS = 11; - static const uint8_t KBD_ROWS = 4; + static const uint8_t KBD_ROWS = 5; + static const uint8_t KBD_KEY_COUNT = KBD_COLS * KBD_ROWS; + static const uint8_t EMOTE_SLOT_COUNT = KBD_COLS * (KBD_ROWS - 1); // top 4 rows + static constexpr uint8_t fontEmotes[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x08, 0x09, 0x0B, 0x0C, 0x0E, 0x0F, 0x10, 0x11, + 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F}; + static constexpr uint8_t fontEmoteCount = sizeof(fontEmotes) / sizeof(fontEmotes[0]); - const char keys[KBD_COLS * KBD_ROWS] = { - '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '\b', // row 0 - 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '\n', // row 1 - 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', '!', ' ', // row 2 - 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '?', '\x1b' // row 3 - }; + // Text keyboard (requested layout): + // row 0: 1..0 + // row 1: q..p + // row 2: a..l + // row 3: EMO, z..m, DEL + // row 4: !#1, comma, space, period, SEND + const int16_t textKeys[KBD_KEY_COUNT] = { + // row 0 + '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', KEY_NONE, + // row 1 + 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', KEY_NONE, + // row 2 + 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', KEY_NONE, KEY_NONE, + // row 3 + KEY_EMOTE_TOGGLE, 'z', 'x', 'c', 'v', 'b', 'n', 'm', KEY_BACKSPACE, KEY_NONE, KEY_NONE, + // row 4 + KEY_PUNCT_TOGGLE, ',', ' ', '.', KEY_SEND, KEY_NONE, KEY_NONE, KEY_NONE, KEY_NONE, KEY_NONE, KEY_NONE}; - // This array represents the widths of each key in points - // 16 pt = line height of the text - const uint16_t keyWidths[KBD_COLS * KBD_ROWS] = { - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 24, // row 0 - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 24, // row 1 - 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 24, // row 2 - 16, 16, 16, 16, 16, 16, 16, 10, 10, 12, 40 // row 3 - }; + // Punctuation keyboard (toggle via !#1/ABC) + const int16_t punctKeys[KBD_KEY_COUNT] = { + // row 0 + '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', KEY_NONE, + // row 1 + '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', KEY_NONE, + // row 2 + '-', '_', '=', '+', '[', ']', '{', '}', '/', '?', KEY_NONE, + // row 3 + KEY_EMOTE_TOGGLE, ';', ':', '\'', '"', '<', '>', '\\', KEY_BACKSPACE, KEY_NONE, KEY_NONE, + // row 4 + KEY_ALPHA_TOGGLE, ',', ' ', '.', KEY_SEND, KEY_NONE, KEY_NONE, KEY_NONE, KEY_NONE, KEY_NONE, KEY_NONE}; - uint16_t rowWidths[KBD_ROWS]; - uint8_t selectedKey = 0; // selected key index + const uint16_t typingKeyWidths[KBD_KEY_COUNT] = {// row 0 + 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 0, + // row 1 + 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 0, + // row 2 + 12, 12, 12, 12, 12, 12, 12, 12, 12, 0, 0, + // row 3 + 18, 12, 12, 12, 12, 12, 12, 12, 20, 0, 0, + // row 4 + 20, 12, 56, 12, 24, 0, 0, 0, 0, 0, 0}; + + const uint16_t emoteKeyWidths[KBD_KEY_COUNT] = {// row 0 + 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, + // row 1 + 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, + // row 2 + 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, + // row 3 + 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, + // row 4 controls + 14, 14, 18, 12, 40, 12, 20, 18, 0, 0, 0}; + + uint8_t selectedKey = 0; uint8_t prevSelectedKey = 0; + uint8_t emotePage = 0; + KeyboardMode mode = MODE_TEXT; + KeyboardMode lastTypingMode = MODE_TEXT; + static constexpr uint8_t KEY_GAP_X = 3; + static constexpr uint8_t KEY_GAP_Y = 4; + static constexpr uint8_t KEY_RADIUS = 4; }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h index 7ec76292b..e1f004d38 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h @@ -109,6 +109,7 @@ enum MenuAction { TOGGLE_CHANNEL_POSITION, SET_CHANNEL_PRECISION, // Display + SET_DISPLAY_TIMEOUT, TOGGLE_DISPLAY_UNITS, // Network TOGGLE_WIFI, @@ -119,4 +120,4 @@ enum MenuAction { } // namespace NicheGraphics::InkHUD -#endif \ No newline at end of file +#endif diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp index d489d21ee..dfef4d085 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp @@ -8,6 +8,7 @@ #include "RTC.h" #include "Router.h" #include "airtime.h" +#include "graphics/niche/Utils/FlashData.h" #include "main.h" #include "mesh/generated/meshtastic/deviceonly.pb.h" #include "power.h" @@ -27,6 +28,16 @@ static constexpr uint8_t MENU_TIMEOUT_SEC = 60; // How many seconds before menu // These are offered to users as possible values for settings.recentlyActiveSeconds static constexpr uint8_t RECENTS_OPTIONS_MINUTES[] = {2, 5, 10, 30, 60, 120}; +struct DisplayTimeoutOption { + uint32_t seconds; + const char *label; +}; + +static constexpr DisplayTimeoutOption DISPLAY_TIMEOUT_OPTIONS[] = { + {0, "Forever"}, {30, "30 secs"}, {60, "1 min"}, {5 * 60, "5 min"}, + {15 * 60, "15 min"}, {30 * 60, "30 min"}, {60 * 60, "1 hr"}, +}; + struct PositionPrecisionOption { uint8_t value; // proto value const char *metric; @@ -39,6 +50,77 @@ static constexpr PositionPrecisionOption POSITION_PRECISION_OPTIONS[] = { {12, "5.8 km", "3.6 mi"}, {11, "12 km", "7.3 mi"}, {10, "23 km", "15 mi"}, }; +static const char *getDisplayTimeoutLabel(uint32_t timeoutSeconds) +{ + constexpr uint8_t optionCount = sizeof(DISPLAY_TIMEOUT_OPTIONS) / sizeof(DISPLAY_TIMEOUT_OPTIONS[0]); + for (uint8_t i = 0; i < optionCount; i++) { + if (DISPLAY_TIMEOUT_OPTIONS[i].seconds == timeoutSeconds) { + return DISPLAY_TIMEOUT_OPTIONS[i].label; + } + } + + return "Custom"; +} + +static bool supportsFreeTextKeyboard(const InkHUD::InkHUD *inkhud, const InkHUD::Persistence::Settings *settings) +{ + return !inkhud->twoWayRocker && (settings->joystick.enabled || inkhud->hasTouchEnabledProvider()); +} + +static bool useTouchFriendlyMenuLayout(const InkHUD::InkHUD *inkhud) +{ + return inkhud != nullptr && inkhud->hasTouchEnabledProvider(); +} + +static uint16_t getMenuItemHeightPx(const InkHUD::InkHUD *inkhud) +{ + const bool touchFriendly = useTouchFriendlyMenuLayout(inkhud); + const uint16_t lineH = touchFriendly ? InkHUD::Applet::fontMedium.lineHeight() : InkHUD::Applet::fontSmall.lineHeight(); + const float rowScale = touchFriendly ? 1.9f : 1.6f; + uint16_t itemH = (uint16_t)(lineH * rowScale); + if (itemH == 0) { + itemH = 1; + } + return itemH; +} + +#if defined(T5_S3_EPAPER_PRO) +namespace +{ +static constexpr uint32_t T5_BACKLIGHT_PREFS_VERSION = 1; + +struct T5BacklightPrefs { + uint32_t version = T5_BACKLIGHT_PREFS_VERSION; + bool keepOn = true; +}; + +T5BacklightPrefs t5BacklightPrefs; +bool t5BacklightPrefsLoaded = false; + +bool loadT5BacklightKeepOn() +{ + if (!t5BacklightPrefsLoaded) { + T5BacklightPrefs loaded; + const bool ok = FlashData::load(&loaded, "t5_backlight"); + if (ok && loaded.version == T5_BACKLIGHT_PREFS_VERSION) { + t5BacklightPrefs = loaded; + } + t5BacklightPrefsLoaded = true; + } + + return t5BacklightPrefs.keepOn; +} + +void saveT5BacklightKeepOn(bool keepOn) +{ + loadT5BacklightKeepOn(); + t5BacklightPrefs.version = T5_BACKLIGHT_PREFS_VERSION; + t5BacklightPrefs.keepOn = keepOn; + FlashData::save(&t5BacklightPrefs, "t5_backlight"); +} +} // namespace +#endif + InkHUD::MenuApplet::MenuApplet() : concurrency::OSThread("MenuApplet") { // No timer tasks at boot @@ -47,7 +129,11 @@ InkHUD::MenuApplet::MenuApplet() : concurrency::OSThread("MenuApplet") // Note: don't get instance if we're not actually using the backlight, // or else you will unintentionally instantiate it if (settings->optionalMenuItems.backlight) { +#if defined(T5_S3_EPAPER_PRO) + t5BacklightSetUserEnabled(loadT5BacklightKeepOn()); +#else backlight = Drivers::LatchingBacklight::getInstance(); +#endif } // Initialize the Canned Message store @@ -76,9 +162,11 @@ void InkHUD::MenuApplet::onForeground() // backlight on always when menu opens. // Courtesy to T-Echo users who removed the capacitive touch button if (settings->optionalMenuItems.backlight) { +#if !defined(T5_S3_EPAPER_PRO) assert(backlight); if (!backlight->isOn()) backlight->peek(); +#endif } // Prevent user applets requesting update while menu is open @@ -106,9 +194,11 @@ void InkHUD::MenuApplet::onBackground() // Item in options submenu allows keeping backlight on after menu is closed // If this item is deselected we will turn backlight off again, now that menu is closing if (settings->optionalMenuItems.backlight) { +#if !defined(T5_S3_EPAPER_PRO) assert(backlight); if (!backlight->isLatched()) backlight->off(); +#endif } // Stop the auto-timeout @@ -333,17 +423,14 @@ void InkHUD::MenuApplet::execute(MenuItem item) handleFreeText = true; cm.freeTextItem.rawText.erase(); // clear the previous freetext message freeTextMode = true; // render input field instead of normal menu - // Open the on-screen keyboard only for full joystick devices - if (settings->joystick.enabled && !inkhud->twoWayRocker) + if (supportsFreeTextKeyboard(inkhud, settings)) inkhud->openKeyboard(); break; - case STORE_CANNEDMESSAGE_SELECTION: - if (!settings->joystick.enabled || inkhud->twoWayRocker) - cm.selectedMessageItem = &cm.messageItems.at(cursor - 1); // Minus one: offset for the initial "Send Ping" entry - else - cm.selectedMessageItem = &cm.messageItems.at(cursor - 2); // Minus two: offset for the "Send Ping" and free text entry - break; + case STORE_CANNEDMESSAGE_SELECTION: { + const uint8_t prefixItems = supportsFreeTextKeyboard(inkhud, settings) ? 2 : 1; + cm.selectedMessageItem = &cm.messageItems.at(cursor - prefixItems); + } break; case SEND_CANNEDMESSAGE: cm.selectedRecipientItem = &cm.recipientItems.at(cursor); @@ -422,14 +509,27 @@ void InkHUD::MenuApplet::execute(MenuItem item) break; case TOGGLE_BACKLIGHT: - // Note: backlight is already on in this situation - // We're marking that it should *remain* on once menu closes - assert(backlight); + // Note: backlight is already on in this situation. + // This toggle controls whether it should remain on when menu closes. +#if defined(T5_S3_EPAPER_PRO) + { + const bool keepOn = !t5BacklightIsUserEnabled(); + t5BacklightSetUserEnabled(keepOn); + saveT5BacklightKeepOn(keepOn); + if (item.checkState) + *(item.checkState) = keepOn; + } +#else + if (!backlight) + backlight = Drivers::LatchingBacklight::getInstance(); if (backlight->isLatched()) backlight->off(); else backlight->latch(); - break; + if (item.checkState) + *(item.checkState) = backlight->isLatched(); +#endif + break; case TOGGLE_12H_CLOCK: config.display.use_12h_clock = !config.display.use_12h_clock; @@ -527,6 +627,17 @@ void InkHUD::MenuApplet::execute(MenuItem item) } // Display + case SET_DISPLAY_TIMEOUT: { + // cursor - 1 because index 0 is "Back" + const uint8_t index = cursor - 1; + constexpr uint8_t optionCount = sizeof(DISPLAY_TIMEOUT_OPTIONS) / sizeof(DISPLAY_TIMEOUT_OPTIONS[0]); + if (index < optionCount) { + config.display.screen_on_secs = DISPLAY_TIMEOUT_OPTIONS[index].seconds; + nodeDB->saveToDisk(SEGMENT_CONFIG); + } + break; + } + case TOGGLE_DISPLAY_UNITS: if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) config.display.units = meshtastic_Config_DisplayConfig_DisplayUnits_METRIC; @@ -893,11 +1004,16 @@ void InkHUD::MenuApplet::showPage(MenuPage page) previousPage = MenuPage::ROOT; items.push_back(MenuItem("Back", previousPage)); // Optional: backlight - if (settings->optionalMenuItems.backlight) - items.push_back(MenuItem(backlight->isLatched() ? "Backlight Off" : "Keep Backlight On", // Label - MenuAction::TOGGLE_BACKLIGHT, // Action - MenuPage::EXIT // Exit once complete - )); + if (settings->optionalMenuItems.backlight) { +#if defined(T5_S3_EPAPER_PRO) + keepBacklightOn = t5BacklightIsUserEnabled(); +#else + if (!backlight) + backlight = Drivers::LatchingBacklight::getInstance(); + keepBacklightOn = backlight->isLatched(); +#endif + items.push_back(MenuItem("Keep Backlight On", MenuAction::TOGGLE_BACKLIGHT, MenuPage::OPTIONS, &keepBacklightOn)); + } // Options Toggles items.push_back(MenuItem("Applets", MenuPage::APPLETS)); @@ -1109,6 +1225,9 @@ void InkHUD::MenuApplet::showPage(MenuPage page) items.push_back(MenuItem("12-Hour Clock", MenuAction::TOGGLE_12H_CLOCK, MenuPage::NODE_CONFIG_DISPLAY, &config.display.use_12h_clock)); + nodeConfigLabels.emplace_back("Screen Timeout: " + std::string(getDisplayTimeoutLabel(config.display.screen_on_secs))); + items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_DISPLAY_TIMEOUT)); + const char *unitsLabel = (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) ? "Units: Imperial" : "Units: Metric"; @@ -1118,6 +1237,13 @@ void InkHUD::MenuApplet::showPage(MenuPage page) break; } + case NODE_CONFIG_DISPLAY_TIMEOUT: + previousPage = MenuPage::NODE_CONFIG_DISPLAY; + items.push_back(MenuItem("Back", previousPage)); + populateDisplayTimeoutPage(); + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + break; + case NODE_CONFIG_BLUETOOTH: { previousPage = MenuPage::NODE_CONFIG; items.push_back(MenuItem("Back", previousPage)); @@ -1386,10 +1512,14 @@ void InkHUD::MenuApplet::onRender(bool full) if (items.size() == 0) LOG_ERROR("Empty Menu"); + const bool touchFriendlyLayout = useTouchFriendlyMenuLayout(inkhud); + AppletFont menuItemFont = touchFriendlyLayout ? fontMedium : fontSmall; + setFont(menuItemFont); + // Dimensions for the slots where we will draw menuItems const float padding = 0.05; - const uint16_t itemH = fontSmall.lineHeight() * 1.6; - const int16_t selectInsetY = 2; + const uint16_t itemH = getMenuItemHeightPx(inkhud); + const int16_t selectInsetY = touchFriendlyLayout ? 3 : 2; const int16_t itemW = width() - X(padding) - X(padding); const int16_t itemL = X(padding); const int16_t itemR = X(1 - padding); @@ -1422,6 +1552,11 @@ void InkHUD::MenuApplet::onRender(bool full) itemT = max(siT + siH, 0); // Offset the first menu entry, so menu starts below the system info panel } + // drawSystemInfoPanel() changes font state (clock/info text). + // Restore the active menu font so ROOT page item text matches other menu pages, + // including touch-friendly layouts. + setFont(menuItemFont); + // Draw menu items // =================== @@ -1449,8 +1584,6 @@ void InkHUD::MenuApplet::onRender(bool full) // Header (non-selectable section label) if (item.isHeader) { - setFont(fontSmall); - // Header text (flush left) printAt(itemL + X(padding), center, item.label, LEFT, MIDDLE); @@ -1459,8 +1592,17 @@ void InkHUD::MenuApplet::onRender(bool full) drawLine(itemL + X(padding), underlineY, itemR - X(padding), underlineY, BLACK); } else { // Box, if currently selected - if (cursorShown && i == cursor) - drawRect(itemL, itemT + selectInsetY, itemW, itemH - (selectInsetY * 2), BLACK); + if (cursorShown && i == cursor && (!touchFriendlyLayout || !hideTouchSelectionHighlight)) { + const int16_t selTop = itemT + selectInsetY; + const int16_t selH = itemH - (selectInsetY * 2); + drawRect(itemL, selTop, itemW, selH, BLACK); + // Touch layouts need a stronger visual cue than a thin outline. + if (touchFriendlyLayout) { + const int16_t markerInset = 3; + const int16_t markerW = 4; + fillRect(itemL + markerInset, selTop + markerInset, markerW, max(1, selH - (markerInset * 2)), BLACK); + } + } // Indented normal item text printAt(itemL + X(padding * 2), center, item.label, LEFT, MIDDLE); @@ -1468,9 +1610,9 @@ void InkHUD::MenuApplet::onRender(bool full) // Checkbox, if relevant if (item.checkState) { - const uint16_t cbWH = fontSmall.lineHeight(); // Checkbox: width / height - const int16_t cbL = itemR - X(padding) - cbWH; // Checkbox: left - const int16_t cbT = center - (cbWH / 2); // Checkbox : top + const uint16_t cbWH = menuItemFont.lineHeight(); // Checkbox: width / height + const int16_t cbL = itemR - X(padding) - cbWH; // Checkbox: left + const int16_t cbT = center - (cbWH / 2); // Checkbox : top // Checkbox ticked if (*(item.checkState)) { drawRect(cbL, cbT, cbWH, cbWH, BLACK); @@ -1499,13 +1641,102 @@ void InkHUD::MenuApplet::onRender(bool full) } } +bool InkHUD::MenuApplet::onTouchPoint(uint16_t x, uint16_t y, bool longPress) +{ + (void)longPress; + + if (freeTextMode || !getTile()) { + return false; + } + + const uint16_t tileL = getTile()->getLeft(); + const uint16_t tileT = getTile()->getTop(); + const uint16_t tileR = tileL + getTile()->getWidth(); + const uint16_t tileB = tileT + getTile()->getHeight(); + if (x < tileL || x >= tileR || y < tileT || y >= tileB) { + return false; + } + + if (items.empty()) { + return true; + } + + // Direct touch controls should act as activity and keep the menu open. + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + + // If button-driven selection is active on touch-first layouts, clear it as soon as + // touch interaction resumes so touch behavior remains direct/tap-first. + if (useTouchFriendlyMenuLayout(inkhud)) { + cursorShown = false; + hideTouchSelectionHighlight = true; + } + + const int16_t localY = (int16_t)y - (int16_t)tileT; + + // Keep geometry in sync with onRender() so touch hit-testing matches what users see. + const uint16_t itemH = getMenuItemHeightPx(inkhud); + int16_t itemT = 0; + uint8_t slotCount = (height() - itemT) / itemH; + if (slotCount == 0) { + slotCount = 1; + } + const uint16_t &siH = systemInfoPanelHeight; + const uint8_t slotsObscured = ceilf(siH / (float)itemH); + + if (currentPage == ROOT) { + int16_t siT = 0; + const int16_t scrollThreshold = (int16_t)slotCount - (int16_t)slotsObscured - 1; + if (scrollThreshold >= 0 && (int16_t)cursor >= scrollThreshold) { + siT = 0 - ((cursor - scrollThreshold) * itemH); + } + itemT = max((int16_t)(siT + siH), (int16_t)0); + } + + const uint8_t firstItem = (cursor < slotCount) ? 0 : (cursor - (slotCount - 1)); + uint16_t visibleEnd = (uint16_t)firstItem + (uint16_t)slotCount; + const uint8_t maxIndex = (uint8_t)items.size() - 1; + if (visibleEnd > maxIndex) { + visibleEnd = maxIndex; + } + const uint8_t lastItem = (uint8_t)visibleEnd; + + for (uint8_t i = firstItem; i <= lastItem; i++) { + const int16_t rowTop = itemT; + const int16_t rowBottom = itemT + itemH; + + if (localY >= rowTop && localY < rowBottom) { + if (items.at(i).isHeader) { + // Consume taps on headers so they don't fall back to button semantics. + return true; + } + + cursor = i; + cursorShown = true; + execute(items.at(cursor)); + + if (!wantsToRender()) { + requestUpdate(Drivers::EInk::UpdateTypes::FAST); + } + return true; + } + + itemT += itemH; + } + + // Consume taps on menu whitespace so we don't trigger button-like fallback behavior. + return true; +} + void InkHUD::MenuApplet::onButtonShortPress() { if (!freeTextMode) { // Push the auto-close timer back OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); - if (!settings->joystick.enabled) { + // Touch-first nodes keep user-button short-press as "advance selection" in menus. + // Any button-driven navigation should restore visible highlight. + hideTouchSelectionHighlight = false; + if (!settings->joystick.enabled || useTouchFriendlyMenuLayout(inkhud)) { if (!cursorShown) { cursorShown = true; // Select the first item that isn't a header @@ -1566,6 +1797,32 @@ void InkHUD::MenuApplet::onNavUp() if (!freeTextMode) { OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + // Touch-first menus: swipe up/down should scroll only. + // Keep cursor movement for scroll math, but selection box is hidden in onRender(). + if (useTouchFriendlyMenuLayout(inkhud)) { + hideTouchSelectionHighlight = true; + if (!cursorShown) { + cursorShown = true; + cursor = items.size() - 1; + while (items.at(cursor).isHeader) { + if (cursor == 0) { + cursorShown = false; + break; + } + cursor--; + } + } else { + do { + if (cursor == 0) + cursor = items.size() - 1; + else + cursor--; + } while (items.at(cursor).isHeader); + } + requestUpdate(Drivers::EInk::UpdateTypes::FAST); + return; + } + if (!cursorShown) { cursorShown = true; // Select the last item that isn't a header @@ -1595,6 +1852,29 @@ void InkHUD::MenuApplet::onNavDown() if (!freeTextMode) { OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + // Touch-first menus: swipe up/down should scroll only. + // Keep cursor movement for scroll math, but selection box is hidden in onRender(). + if (useTouchFriendlyMenuLayout(inkhud)) { + hideTouchSelectionHighlight = true; + if (!cursorShown) { + cursorShown = true; + cursor = 0; + while (cursor < items.size() && items.at(cursor).isHeader) { + cursor++; + } + if (cursor >= items.size()) { + cursorShown = false; + cursor = 0; + } + } else { + do { + cursor = (cursor + 1) % items.size(); + } while (items.at(cursor).isHeader); + } + requestUpdate(Drivers::EInk::UpdateTypes::FAST); + return; + } + if (!cursorShown) { cursorShown = true; // Select the first item that isn't a header @@ -1727,6 +2007,17 @@ void InkHUD::MenuApplet::populateRecentsPage() } } +void InkHUD::MenuApplet::populateDisplayTimeoutPage() +{ + constexpr uint8_t optionCount = sizeof(DISPLAY_TIMEOUT_OPTIONS) / sizeof(DISPLAY_TIMEOUT_OPTIONS[0]); + for (uint8_t i = 0; i < optionCount; i++) { + displayTimeoutSelected[i] = (config.display.screen_on_secs == DISPLAY_TIMEOUT_OPTIONS[i].seconds); + nodeConfigLabels.emplace_back(DISPLAY_TIMEOUT_OPTIONS[i].label); + items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::SET_DISPLAY_TIMEOUT, MenuPage::NODE_CONFIG_DISPLAY, + &displayTimeoutSelected[i])); + } +} + // MenuItem entries for the "send" page // Dynamically creates menu items based on available canned messages void InkHUD::MenuApplet::populateSendPage() @@ -1734,8 +2025,8 @@ void InkHUD::MenuApplet::populateSendPage() // Position / NodeInfo packet items.push_back(MenuItem("Ping", MenuAction::SEND_PING, MenuPage::EXIT)); - // If joystick is available, include the Free Text option - if (settings->joystick.enabled && !inkhud->twoWayRocker) + // Show the Free Text option on any node that supports the on-screen keyboard. + if (supportsFreeTextKeyboard(inkhud, settings)) items.push_back(MenuItem("Free Text", MenuAction::FREE_TEXT, MenuPage::SEND)); // One menu item for each canned message diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h index b5c1c86e4..2d7fbd398 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h @@ -35,6 +35,7 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread void onFreeText(char c) override; void onFreeTextDone() override; void onFreeTextCancel() override; + bool onTouchPoint(uint16_t x, uint16_t y, bool longPress) override; void onRender(bool full) override; void show(Tile *t); // Open the menu, onto a user tile @@ -48,11 +49,12 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread void execute(MenuItem item); // Perform the MenuAction associated with a MenuItem, if any void showPage(MenuPage page); // Load and display a MenuPage - void populateSendPage(); // Dynamically create MenuItems including canned messages - void populateRecipientPage(); // Dynamically create a page of possible destinations for a canned message - void populateAppletPage(); // Dynamically create MenuItems for toggling loaded applets - void populateAutoshowPage(); // Dynamically create MenuItems for selecting which applets can autoshow - void populateRecentsPage(); // Create menu items: a choice of values for settings.recentlyActiveSeconds + void populateSendPage(); // Dynamically create MenuItems including canned messages + void populateRecipientPage(); // Dynamically create a page of possible destinations for a canned message + void populateAppletPage(); // Dynamically create MenuItems for toggling loaded applets + void populateAutoshowPage(); // Dynamically create MenuItems for selecting which applets can autoshow + void populateRecentsPage(); // Create menu items: a choice of values for settings.recentlyActiveSeconds + void populateDisplayTimeoutPage(); // Create menu items for config.display.screen_on_secs void drawInputField(uint16_t left, uint16_t top, uint16_t width, uint16_t height, const std::string &text); // Draw input field for free text @@ -65,8 +67,9 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread MenuPage startPageOverride = MenuPage::ROOT; MenuPage currentPage = MenuPage::ROOT; MenuPage previousPage = MenuPage::EXIT; - uint8_t cursor = 0; // Which menu item is currently highlighted - bool cursorShown = false; // Is *any* item highlighted? (Root menu: no initial selection) + uint8_t cursor = 0; // Which menu item is currently highlighted + bool cursorShown = false; // Is *any* item highlighted? (Root menu: no initial selection) + bool hideTouchSelectionHighlight = false; // Touch scrolling keeps cursor for paging math, but can hide highlight bool freeTextMode = false; uint16_t systemInfoPanelHeight = 0; // Need to know before we render uint16_t menuTextLimit = 200; @@ -80,6 +83,8 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread // Recents menu checkbox state (derived from settings.recentlyActiveSeconds) static constexpr uint8_t RECENTS_COUNT = 6; bool recentsSelected[RECENTS_COUNT] = {}; + static constexpr uint8_t DISPLAY_TIMEOUT_COUNT = 7; + bool displayTimeoutSelected[DISPLAY_TIMEOUT_COUNT] = {}; // Data for selecting and sending canned messages via the menu // Placed into a sub-class for organization only @@ -116,7 +121,8 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread Applet *borrowedTileOwner = nullptr; // Which applet we have temporarily replaced while displaying menu - bool invertedColors = false; // Helper to display current state of config.display.displaymode in InkHUD options + bool invertedColors = false; // Helper to display current state of config.display.displaymode in InkHUD options + bool keepBacklightOn = false; // Helper to display current backlight latch state in InkHUD options }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuPage.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuPage.h index 138c389be..9bc6bb0cc 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuPage.h +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuPage.h @@ -32,6 +32,7 @@ enum MenuPage : uint8_t { NODE_CONFIG_POWER_ADC_CAL, NODE_CONFIG_NETWORK, NODE_CONFIG_DISPLAY, + NODE_CONFIG_DISPLAY_TIMEOUT, NODE_CONFIG_BLUETOOTH, NODE_CONFIG_POSITION, NODE_CONFIG_ADMIN_RESET, @@ -45,4 +46,4 @@ enum MenuPage : uint8_t { } // namespace NicheGraphics::InkHUD -#endif \ No newline at end of file +#endif diff --git a/src/graphics/niche/InkHUD/Applets/System/Notification/TouchStatusApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Notification/TouchStatusApplet.cpp new file mode 100644 index 000000000..04475c803 --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/System/Notification/TouchStatusApplet.cpp @@ -0,0 +1,30 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +#include "./TouchStatusApplet.h" + +using namespace NicheGraphics; + +InkHUD::TouchStatusApplet::TouchStatusApplet() +{ + alwaysRender = true; +} + +void InkHUD::TouchStatusApplet::onRender(bool full) +{ + (void)full; + + if (inkhud->isTouchEnabled()) { + return; + } + + setFont(fontSmall); + + const uint16_t barH = fontSmall.lineHeight() + 4; + const int16_t top = height() - barH; + + fillRect(0, top, width(), barH, WHITE); + drawLine(0, top, width() - 1, top, BLACK); + printAt(width() / 2, top + (barH / 2), "TOUCH OFF", CENTER, MIDDLE); +} + +#endif diff --git a/src/graphics/niche/InkHUD/Applets/System/Notification/TouchStatusApplet.h b/src/graphics/niche/InkHUD/Applets/System/Notification/TouchStatusApplet.h new file mode 100644 index 000000000..1d3ddc816 --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/System/Notification/TouchStatusApplet.h @@ -0,0 +1,29 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +/* + + Low-profile bottom-edge touch status indicator. + Shown only while touch input is disabled. + +*/ + +#pragma once + +#include "configuration.h" + +#include "graphics/niche/InkHUD/SystemApplet.h" + +namespace NicheGraphics::InkHUD +{ + +class TouchStatusApplet : public SystemApplet +{ + public: + TouchStatusApplet(); + + void onRender(bool full) override; +}; + +} // namespace NicheGraphics::InkHUD + +#endif diff --git a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp index a45e8d9b5..ffc8f6f99 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp @@ -47,6 +47,9 @@ InkHUD::TipsApplet::TipsApplet() void InkHUD::TipsApplet::onRender(bool full) { + const char *continuePrompt = + (inkhud && inkhud->hasTouchEnabledProvider()) ? "Tap screen to continue" : "Press button to continue"; + switch (tipQueue.front()) { case Tip::WELCOME: renderWelcome(); @@ -79,7 +82,7 @@ void InkHUD::TipsApplet::onRender(bool full) cursorY += fontSmall.lineHeight() / 2; drawBullet("More info at meshtastic.org"); - printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); + printAt(0, Y(1.0), continuePrompt, LEFT, BOTTOM); } break; case Tip::PICK_REGION: { @@ -109,7 +112,7 @@ void InkHUD::TipsApplet::onRender(bool full) printWrapped(0, cursorY, width(), body); cursorY += bodyH + (fontSmall.lineHeight() / 2); - printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); + printAt(0, Y(1.0), continuePrompt, LEFT, BOTTOM); } break; case Tip::CUSTOMIZATION: { @@ -129,10 +132,13 @@ void InkHUD::TipsApplet::onRender(bool full) printWrapped(0, cursorY, width(), body); cursorY += bodyH + (fontSmall.lineHeight() / 2); - printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); + printAt(0, Y(1.0), continuePrompt, LEFT, BOTTOM); } break; case Tip::BUTTONS: { +#if defined(T5_S3_EPAPER_PRO) + renderT5S3ButtonsTip(); +#else setFont(fontMedium); const char *title = "Tip: Buttons"; @@ -164,7 +170,8 @@ void InkHUD::TipsApplet::onRender(bool full) drawBullet("- press: switch tile or close menu"); } - printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); + printAt(0, Y(1.0), continuePrompt, LEFT, BOTTOM); +#endif } break; case Tip::ROTATION: { @@ -189,7 +196,7 @@ void InkHUD::TipsApplet::onRender(bool full) "To rotate the display, use the InkHUD menu. Press the user button > Options > Rotate."); } - printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); + printAt(0, Y(1.0), continuePrompt, LEFT, BOTTOM); // Revert the "flip screen" setting, preventing this message showing again config.display.flip_screen = false; @@ -198,6 +205,42 @@ void InkHUD::TipsApplet::onRender(bool full) } } +#if defined(T5_S3_EPAPER_PRO) +void InkHUD::TipsApplet::renderT5S3ButtonsTip() +{ + setFont(fontMedium); + + const char *title = "Tip: T5-S3 Buttons"; + uint16_t h = getWrappedTextHeight(0, width(), title); + printWrapped(0, 0, width(), title); + + setFont(fontSmall); + int16_t cursorY = h + fontSmall.lineHeight(); + + auto drawBullet = [&](const char *text) { + uint16_t bh = getWrappedTextHeight(0, width(), text); + printWrapped(0, cursorY, width(), text); + cursorY += bh + (fontSmall.lineHeight() / 3); + }; + + drawBullet("BOOT button"); + drawBullet("- short press: next"); + drawBullet("- long press: open menu or select"); + + drawBullet("IO48 button"); + drawBullet("- short press: toggle touch on/off"); + drawBullet("- long press: toggle backlight on/off"); + + drawBullet("PWR button"); + drawBullet("- Hold Press to wake after Shutdown"); + + drawBullet("HOME button"); + drawBullet("- press: back/exit in InkHUD and open App switcher"); + + printAt(0, Y(1.0), "Tap screen to continue", LEFT, BOTTOM); +} +#endif + // This tip has its own render method, only because it's a big block of code // Didn't want to clutter up the switch in onRender too much void InkHUD::TipsApplet::renderWelcome() @@ -244,7 +287,9 @@ void InkHUD::TipsApplet::renderWelcome() // Block 3 - press to continue // ============================ - printAt(X(0.5), Y(1), "Press button to continue", CENTER, BOTTOM); + const char *continuePrompt = + (inkhud && inkhud->hasTouchEnabledProvider()) ? "Tap screen to continue" : "Press button to continue"; + printAt(X(0.5), Y(1), continuePrompt, CENTER, BOTTOM); } void InkHUD::TipsApplet::onForeground() diff --git a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h index 2e81d678b..ea2db228c 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h @@ -41,6 +41,9 @@ class TipsApplet : public SystemApplet protected: void renderWelcome(); // Very first screen of tutorial +#if defined(T5_S3_EPAPER_PRO) + void renderT5S3ButtonsTip(); +#endif std::deque tipQueue; // List of tips to show, one after another @@ -49,4 +52,4 @@ class TipsApplet : public SystemApplet } // namespace NicheGraphics::InkHUD -#endif \ No newline at end of file +#endif diff --git a/src/graphics/niche/InkHUD/Events.cpp b/src/graphics/niche/InkHUD/Events.cpp index 577a773bb..ddcc6781b 100644 --- a/src/graphics/niche/InkHUD/Events.cpp +++ b/src/graphics/niche/InkHUD/Events.cpp @@ -2,6 +2,7 @@ #include "./Events.h" +#include "PowerFSM.h" #include "RTC.h" #include "buzz.h" #include "modules/ExternalNotificationModule.h" @@ -14,6 +15,19 @@ using namespace NicheGraphics; +namespace +{ +// When touch long-press opens menu, some panels report a delayed release/tap +// if the finger stays down briefly. Keep this long enough to cover that release. +constexpr uint32_t TOUCH_MENU_OPEN_TAP_SUPPRESS_MS = 1200; + +inline void noteInkHUDUserInteraction() +{ + // Keep power state and screen-timeout behavior in sync with InkHUD input activity. + powerFSM.trigger(EVENT_INPUT); +} +} // namespace + InkHUD::Events::Events() { // Get convenient references @@ -38,6 +52,8 @@ void InkHUD::Events::begin() void InkHUD::Events::onButtonShort() { + noteInkHUDUserInteraction(); + // Audio feedback (via buzzer) // Short tone playChirp(); @@ -74,6 +90,8 @@ void InkHUD::Events::onButtonShort() void InkHUD::Events::onButtonLong() { + noteInkHUDUserInteraction(); + // Audio feedback (via buzzer) // Slightly longer than playChirp playBoop(); @@ -102,193 +120,295 @@ void InkHUD::Events::onButtonLong() void InkHUD::Events::onExitShort() { - if (settings->joystick.enabled) { - // Audio feedback (via buzzer) - // Short tone - playChirp(); - // Cancel any beeping, buzzing, blinking - // Some button handling suppressed if we are dismissing an external notification (see below) - bool dismissedExt = dismissExternalNotification(); + // Preserve legacy behavior on non-touch builds: + // EXIT input is only active when joystick mode is enabled. + // Touch-capable builds intentionally bypass this joystick gate. + if (!settings->joystick.enabled && !inkhud->hasTouchEnabledProvider()) { + return; + } - // Check which system applet wants to handle the button press (if any) - SystemApplet *consumer = nullptr; - for (SystemApplet *sa : inkhud->systemApplets) { - if (sa->handleInput) { - consumer = sa; - break; - } + noteInkHUDUserInteraction(); + + // Audio feedback (via buzzer) + // Short tone + playChirp(); + // Cancel any beeping, buzzing, blinking + // Some button handling suppressed if we are dismissing an external notification module (see below) + bool dismissedExt = dismissExternalNotification(); + + // Check which system applet wants to handle the button press (if any) + SystemApplet *consumer = nullptr; + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleInput) { + consumer = sa; + break; } + } - // If no system applet is handling input, default behavior instead is change tiles - if (consumer) - consumer->onExitShort(); - else if (!dismissedExt) { // Don't change tile if this button press silenced the external notification module - Applet *userConsumer = inkhud->getActiveApplet(); + // Always let active system applets consume EXIT/HOME input (menu close, keyboard cancel, etc), + // including on touch-first nodes where joystick mode is disabled. + if (consumer) { + consumer->onExitShort(); + return; + } - if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::EXIT_SHORT)) - userConsumer->onExitShort(); - else - inkhud->nextTile(); + // Touch-capable InkHUD nodes use EXIT/HOME as a quick app switcher launcher. + if (!dismissedExt && inkhud->hasTouchEnabledProvider()) { + inkhud->openAppSwitcher(); + return; + } + + if (!dismissedExt) { + Applet *userConsumer = inkhud->getActiveApplet(); + + if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::EXIT_SHORT)) { + userConsumer->onExitShort(); + } else if (settings->joystick.enabled) { + // Preserve existing joystick behavior when no applet handles EXIT. + inkhud->nextTile(); } } } void InkHUD::Events::onExitLong() { - if (settings->joystick.enabled) { - // Audio feedback (via buzzer) - // Slightly longer than playChirp - playBoop(); + // Preserve legacy behavior on non-touch builds: + // EXIT input is only active when joystick mode is enabled. + // Touch-capable builds intentionally bypass this joystick gate. + if (!settings->joystick.enabled && !inkhud->hasTouchEnabledProvider()) { + return; + } - // Check which system applet wants to handle the button press (if any) - SystemApplet *consumer = nullptr; - for (SystemApplet *sa : inkhud->systemApplets) { - if (sa->handleInput) { - consumer = sa; - break; - } + noteInkHUDUserInteraction(); + + // Audio feedback (via buzzer) + // Slightly longer than playChirp + playBoop(); + + // Check which system applet wants to handle the button press (if any) + SystemApplet *consumer = nullptr; + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleInput) { + consumer = sa; + break; } + } - if (consumer) - consumer->onExitLong(); - else { - Applet *userConsumer = inkhud->getActiveApplet(); + // Always allow system applets to consume EXIT/HOME long-press. + if (consumer) { + consumer->onExitLong(); + } else { + Applet *userConsumer = inkhud->getActiveApplet(); - if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::EXIT_LONG)) - userConsumer->onExitLong(); - // Nothing uses exit long yet - } + if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::EXIT_LONG)) + userConsumer->onExitLong(); + // Nothing uses exit long yet } } void InkHUD::Events::onNavUp() { - if (settings->joystick.enabled) { - // Audio feedback (via buzzer) - // Short tone - playChirp(); - // Cancel any beeping, buzzing, blinking - // Some button handling suppressed if we are dismissing an external notification (see below) - bool dismissedExt = dismissExternalNotification(); - - // Check which system applet wants to handle the button press (if any) - SystemApplet *consumer = nullptr; - for (SystemApplet *sa : inkhud->systemApplets) { - if (sa->handleInput) { - consumer = sa; - break; - } - } - - if (consumer) - consumer->onNavUp(); - else if (!dismissedExt) { - Applet *userConsumer = inkhud->getActiveApplet(); - - if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_UP)) - userConsumer->onNavUp(); - } - } + if (settings->joystick.enabled) + onTouchNavUp(); } void InkHUD::Events::onNavDown() { - if (settings->joystick.enabled) { - // Audio feedback (via buzzer) - // Short tone - playChirp(); - // Cancel any beeping, buzzing, blinking - // Some button handling suppressed if we are dismissing an external notification (see below) - bool dismissedExt = dismissExternalNotification(); - - // Check which system applet wants to handle the button press (if any) - SystemApplet *consumer = nullptr; - for (SystemApplet *sa : inkhud->systemApplets) { - if (sa->handleInput) { - consumer = sa; - break; - } - } - - if (consumer) - consumer->onNavDown(); - else if (!dismissedExt) { - Applet *userConsumer = inkhud->getActiveApplet(); - - if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_DOWN)) - userConsumer->onNavDown(); - } - } + if (settings->joystick.enabled) + onTouchNavDown(); } void InkHUD::Events::onNavLeft() { - if (settings->joystick.enabled) { - // Audio feedback (via buzzer) - // Short tone - playChirp(); - // Cancel any beeping, buzzing, blinking - // Some button handling suppressed if we are dismissing an external notification (see below) - bool dismissedExt = dismissExternalNotification(); - - // Check which system applet wants to handle the button press (if any) - SystemApplet *consumer = nullptr; - for (SystemApplet *sa : inkhud->systemApplets) { - if (sa->handleInput) { - consumer = sa; - break; - } - } - - // If no system applet is handling input, default behavior instead is to cycle applets - if (consumer) - consumer->onNavLeft(); - else if (!dismissedExt) { // Don't change applet if this button press silenced the external notification module - Applet *userConsumer = inkhud->getActiveApplet(); - - if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_LEFT)) - userConsumer->onNavLeft(); - else - inkhud->prevApplet(); - } - } + if (settings->joystick.enabled) + onTouchNavLeft(); } void InkHUD::Events::onNavRight() { - if (settings->joystick.enabled) { - // Audio feedback (via buzzer) - // Short tone - playChirp(); - // Cancel any beeping, buzzing, blinking - // Some button handling suppressed if we are dismissing an external notification (see below) - bool dismissedExt = dismissExternalNotification(); + if (settings->joystick.enabled) + onTouchNavRight(); +} - // Check which system applet wants to handle the button press (if any) - SystemApplet *consumer = nullptr; - for (SystemApplet *sa : inkhud->systemApplets) { - if (sa->handleInput) { - consumer = sa; - break; - } - } +void InkHUD::Events::onTouchNavUp() +{ + noteInkHUDUserInteraction(); - // If no system applet is handling input, default behavior instead is to cycle applets - if (consumer) - consumer->onNavRight(); - else if (!dismissedExt) { // Don't change applet if this button press silenced the external notification module - Applet *userConsumer = inkhud->getActiveApplet(); + // Audio feedback (via buzzer) + // Short tone + playChirp(); + // Cancel any beeping, buzzing, blinking + // Some button handling suppressed if we are dismissing an external notification (see below) + bool dismissedExt = dismissExternalNotification(); - if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_RIGHT)) - userConsumer->onNavRight(); - else - inkhud->nextApplet(); + // Check which system applet wants to handle the button press (if any) + SystemApplet *consumer = nullptr; + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleInput) { + consumer = sa; + break; } } + + if (consumer) + consumer->onNavUp(); + else if (!dismissedExt) { + Applet *userConsumer = inkhud->getActiveApplet(); + + if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_UP)) + userConsumer->onNavUp(); + } +} + +void InkHUD::Events::onTouchNavDown() +{ + noteInkHUDUserInteraction(); + + // Audio feedback (via buzzer) + // Short tone + playChirp(); + // Cancel any beeping, buzzing, blinking + // Some button handling suppressed if we are dismissing an external notification (see below) + bool dismissedExt = dismissExternalNotification(); + + // Check which system applet wants to handle the button press (if any) + SystemApplet *consumer = nullptr; + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleInput) { + consumer = sa; + break; + } + } + + if (consumer) + consumer->onNavDown(); + else if (!dismissedExt) { + Applet *userConsumer = inkhud->getActiveApplet(); + + if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_DOWN)) + userConsumer->onNavDown(); + } +} + +void InkHUD::Events::onTouchNavLeft() +{ + noteInkHUDUserInteraction(); + + // Audio feedback (via buzzer) + // Short tone + playChirp(); + // Cancel any beeping, buzzing, blinking + // Some button handling suppressed if we are dismissing an external notification (see below) + bool dismissedExt = dismissExternalNotification(); + + // Check which system applet wants to handle the button press (if any) + SystemApplet *consumer = nullptr; + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleInput) { + consumer = sa; + break; + } + } + + // If no system applet is handling input, default behavior instead is to cycle applets + if (consumer) + consumer->onNavLeft(); + else if (!dismissedExt) { // Don't change applet if this button press silenced the external notification module + Applet *userConsumer = inkhud->getActiveApplet(); + + if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_LEFT)) + userConsumer->onNavLeft(); + else + inkhud->prevApplet(); + } +} + +void InkHUD::Events::onTouchNavRight() +{ + noteInkHUDUserInteraction(); + + // Audio feedback (via buzzer) + // Short tone + playChirp(); + // Cancel any beeping, buzzing, blinking + // Some button handling suppressed if we are dismissing an external notification (see below) + bool dismissedExt = dismissExternalNotification(); + + // Check which system applet wants to handle the button press (if any) + SystemApplet *consumer = nullptr; + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleInput) { + consumer = sa; + break; + } + } + + // If no system applet is handling input, default behavior instead is to cycle applets + if (consumer) + consumer->onNavRight(); + else if (!dismissedExt) { // Don't change applet if this button press silenced the external notification module + Applet *userConsumer = inkhud->getActiveApplet(); + + if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_RIGHT)) + userConsumer->onNavRight(); + else + inkhud->nextApplet(); + } +} + +void InkHUD::Events::onTouchTap(uint16_t x, uint16_t y, bool longPress) +{ + const bool touchEnabledBuild = inkhud->hasTouchEnabledProvider(); + + // A long-press used to open the menu can be followed by a synthetic/queued tap at release. + // Ignore that brief follow-up window so touch-opened menus do not auto-select an item. + if (touchEnabledBuild && !longPress && suppressTouchTapUntilMs != 0) { + if ((int32_t)(millis() - suppressTouchTapUntilMs) < 0) { + noteInkHUDUserInteraction(); + return; + } + suppressTouchTapUntilMs = 0; + } + + // Give system applets (menu, keyboard, etc) first chance to consume direct touch input. + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleInput && sa->onTouchPoint(x, y, longPress)) { + noteInkHUDUserInteraction(); + return; + } + } + + // In split layouts, tapping a different tile changes focus. + // Consume this tap so selection does not also trigger button fallback behavior. + if (inkhud->selectTileAt(x, y)) { + noteInkHUDUserInteraction(); + return; + } + + // Allow foreground user applet to consume direct touch input if it wants. + Applet *userConsumer = inkhud->getActiveApplet(); + if (userConsumer != nullptr && userConsumer->onTouchPoint(x, y, longPress)) { + noteInkHUDUserInteraction(); + return; + } + + // Fallback to existing button semantics so non-touch-aware applets keep working unchanged. + if (longPress) { + onButtonLong(); + + // Only arm suppression if the long-press actually opened menu foreground. + SystemApplet *menu = inkhud->getSystemApplet("Menu"); + if (touchEnabledBuild && menu && menu->isForeground()) { + suppressTouchTapUntilMs = millis() + TOUCH_MENU_OPEN_TAP_SUPPRESS_MS; + } + } else + onButtonShort(); } void InkHUD::Events::onFreeText(char c) { + noteInkHUDUserInteraction(); + // Trigger the first system applet that wants to handle the new character for (SystemApplet *sa : inkhud->systemApplets) { if (sa->handleFreeText) { @@ -300,6 +420,8 @@ void InkHUD::Events::onFreeText(char c) void InkHUD::Events::onFreeTextDone() { + noteInkHUDUserInteraction(); + // Trigger the first system applet that wants to handle it for (SystemApplet *sa : inkhud->systemApplets) { if (sa->handleFreeText) { @@ -311,6 +433,8 @@ void InkHUD::Events::onFreeTextDone() void InkHUD::Events::onFreeTextCancel() { + noteInkHUDUserInteraction(); + // Trigger the first system applet that wants to handle it for (SystemApplet *sa : inkhud->systemApplets) { if (sa->handleFreeText) { @@ -487,4 +611,4 @@ bool InkHUD::Events::dismissExternalNotification() return true; } -#endif \ No newline at end of file +#endif diff --git a/src/graphics/niche/InkHUD/Events.h b/src/graphics/niche/InkHUD/Events.h index 873f53fd5..ceb298b3b 100644 --- a/src/graphics/niche/InkHUD/Events.h +++ b/src/graphics/niche/InkHUD/Events.h @@ -17,6 +17,7 @@ however this class handles general events which concern InkHUD as a whole, e.g. #include "./InkHUD.h" #include "./Persistence.h" +#include namespace NicheGraphics::InkHUD { @@ -30,12 +31,17 @@ class Events void onButtonShort(); // User button: short press void onButtonLong(); // User button: long press void applyingChanges(); - void onExitShort(); // Exit button: short press - void onExitLong(); // Exit button: long press - void onNavUp(); // Navigate up - void onNavDown(); // Navigate down - void onNavLeft(); // Navigate left - void onNavRight(); // Navigate right + void onExitShort(); // Exit button: short press + void onExitLong(); // Exit button: long press + void onNavUp(); // Navigate up + void onNavDown(); // Navigate down + void onNavLeft(); // Navigate left + void onNavRight(); // Navigate right + void onTouchNavUp(); // Navigate up from touch input + void onTouchNavDown(); // Navigate down from touch input + void onTouchNavLeft(); // Navigate left from touch input + void onTouchNavRight(); // Navigate right from touch input + void onTouchTap(uint16_t x, uint16_t y, bool longPress); // Touch tap/long-press with coordinates // Free text typing events void onFreeText(char c); // New freetext character input @@ -79,8 +85,11 @@ class Events // If set, InkHUD's data will be erased during onReboot bool eraseOnReboot = false; + + // Suppress follow-up tap generated immediately after a touch long-press opens menu. + uint32_t suppressTouchTapUntilMs = 0; }; } // namespace NicheGraphics::InkHUD -#endif \ No newline at end of file +#endif diff --git a/src/graphics/niche/InkHUD/InkHUD.cpp b/src/graphics/niche/InkHUD/InkHUD.cpp index edffda6b7..9ed88af5f 100644 --- a/src/graphics/niche/InkHUD/InkHUD.cpp +++ b/src/graphics/niche/InkHUD/InkHUD.cpp @@ -73,6 +73,24 @@ void InkHUD::InkHUD::begin() // LogoApplet shows boot screen here } +void InkHUD::InkHUD::setTouchEnabledProvider(TouchEnabledProvider provider) +{ + touchEnabledProvider = provider; +} + +bool InkHUD::InkHUD::hasTouchEnabledProvider() const +{ + return touchEnabledProvider != nullptr; +} + +bool InkHUD::InkHUD::isTouchEnabled() const +{ + if (!touchEnabledProvider) + return true; + + return touchEnabledProvider(); +} + // Call this when your user button gets a short press // Should be connected to an input source in nicheGraphics.h (NicheGraphics::Inputs::TwoButton?) void InkHUD::InkHUD::shortpress() @@ -175,6 +193,92 @@ void InkHUD::InkHUD::navRight() } } +// Call this when touch input needs joystick-like up navigation independent of joystick-enabled mode +void InkHUD::InkHUD::touchNavUp() +{ + switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) { + case 1: // 90 deg + events->onTouchNavLeft(); + break; + case 2: // 180 deg + events->onTouchNavDown(); + break; + case 3: // 270 deg + events->onTouchNavRight(); + break; + default: // 0 deg + events->onTouchNavUp(); + break; + } +} + +// Call this when touch input needs joystick-like down navigation independent of joystick-enabled mode +void InkHUD::InkHUD::touchNavDown() +{ + switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) { + case 1: // 90 deg + events->onTouchNavRight(); + break; + case 2: // 180 deg + events->onTouchNavUp(); + break; + case 3: // 270 deg + events->onTouchNavLeft(); + break; + default: // 0 deg + events->onTouchNavDown(); + break; + } +} + +// Call this when touch input needs joystick-like left navigation independent of joystick-enabled mode +void InkHUD::InkHUD::touchNavLeft() +{ + switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) { + case 1: // 90 deg + events->onTouchNavDown(); + break; + case 2: // 180 deg + events->onTouchNavRight(); + break; + case 3: // 270 deg + events->onTouchNavUp(); + break; + default: // 0 deg + events->onTouchNavLeft(); + break; + } +} + +// Call this when touch input needs joystick-like right navigation independent of joystick-enabled mode +void InkHUD::InkHUD::touchNavRight() +{ + switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) { + case 1: // 90 deg + events->onTouchNavUp(); + break; + case 2: // 180 deg + events->onTouchNavLeft(); + break; + case 3: // 270 deg + events->onTouchNavDown(); + break; + default: // 0 deg + events->onTouchNavRight(); + break; + } +} + +void InkHUD::InkHUD::touchTap(uint16_t x, uint16_t y) +{ + events->onTouchTap(x, y, false); +} + +void InkHUD::InkHUD::touchLongPress(uint16_t x, uint16_t y) +{ + events->onTouchTap(x, y, true); +} + // Call this for keyboard input // The Keyboard Applet also calls this void InkHUD::InkHUD::freeText(char c) @@ -223,6 +327,12 @@ void InkHUD::InkHUD::openMenu() windowManager->openMenu(); } +// Show touch-friendly app switcher (on the focused tile) +void InkHUD::InkHUD::openAppSwitcher() +{ + windowManager->openAppSwitcher(); +} + // Bring AlignStick applet to the foreground void InkHUD::InkHUD::openAlignStick() { @@ -255,6 +365,16 @@ void InkHUD::InkHUD::prevTile() windowManager->prevTile(); } +bool InkHUD::InkHUD::showApplet(uint8_t appletIndex) +{ + return windowManager->showApplet(appletIndex); +} + +bool InkHUD::InkHUD::selectTileAt(uint16_t x, uint16_t y) +{ + return windowManager->selectTileAt(x, y); +} + // Rotate the display image by 90 degrees void InkHUD::InkHUD::rotate() { @@ -376,4 +496,4 @@ void InkHUD::InkHUD::drawPixel(int16_t x, int16_t y, Color c) renderer->handlePixel(x, y, c); } -#endif \ No newline at end of file +#endif diff --git a/src/graphics/niche/InkHUD/InkHUD.h b/src/graphics/niche/InkHUD/InkHUD.h index abd53951a..0c1682637 100644 --- a/src/graphics/niche/InkHUD/InkHUD.h +++ b/src/graphics/niche/InkHUD/InkHUD.h @@ -39,6 +39,8 @@ class WindowManager; class InkHUD { public: + using TouchEnabledProvider = bool (*)(); + static InkHUD *getInstance(); // Access to this singleton class // Configuration @@ -51,6 +53,11 @@ class InkHUD void begin(); + // Optional touch-state provider for reusable touch status indicators. + void setTouchEnabledProvider(TouchEnabledProvider provider); + bool hasTouchEnabledProvider() const; + bool isTouchEnabled() const; + // Handle user-button press // - connected to an input source, in variant nicheGraphics.h @@ -62,6 +69,12 @@ class InkHUD void navDown(); void navLeft(); void navRight(); + void touchNavUp(); + void touchNavDown(); + void touchNavLeft(); + void touchNavRight(); + void touchTap(uint16_t x, uint16_t y); + void touchLongPress(uint16_t x, uint16_t y); // Freetext handlers void freeText(char c); @@ -76,11 +89,14 @@ class InkHUD void prevApplet(); NicheGraphics::InkHUD::Applet *getActiveApplet(); void openMenu(); + void openAppSwitcher(); void openAlignStick(); void openKeyboard(); void closeKeyboard(); void nextTile(); void prevTile(); + bool showApplet(uint8_t appletIndex); + bool selectTileAt(uint16_t x, uint16_t y); void rotate(); void rotateJoystick(uint8_t angle = 1); // rotate 90 deg by default void toggleBatteryIcon(); @@ -129,6 +145,7 @@ class InkHUD Events *events = nullptr; // Handle non-specific firmware events Renderer *renderer = nullptr; // Co-ordinate display updates WindowManager *windowManager = nullptr; // Multiplexing of applets + TouchEnabledProvider touchEnabledProvider = nullptr; }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Persistence.h b/src/graphics/niche/InkHUD/Persistence.h index 5054b7234..187af2129 100644 --- a/src/graphics/niche/InkHUD/Persistence.h +++ b/src/graphics/niche/InkHUD/Persistence.h @@ -142,4 +142,4 @@ class Persistence } // namespace NicheGraphics::InkHUD -#endif \ No newline at end of file +#endif diff --git a/src/graphics/niche/InkHUD/WindowManager.cpp b/src/graphics/niche/InkHUD/WindowManager.cpp index c4a0813d8..804ae8729 100644 --- a/src/graphics/niche/InkHUD/WindowManager.cpp +++ b/src/graphics/niche/InkHUD/WindowManager.cpp @@ -3,11 +3,13 @@ #include "./WindowManager.h" #include "./Applets/System/AlignStick/AlignStickApplet.h" +#include "./Applets/System/AppSwitcher/AppSwitcherApplet.h" #include "./Applets/System/BatteryIcon/BatteryIconApplet.h" #include "./Applets/System/Keyboard/KeyboardApplet.h" #include "./Applets/System/Logo/LogoApplet.h" #include "./Applets/System/Menu/MenuApplet.h" #include "./Applets/System/Notification/NotificationApplet.h" +#include "./Applets/System/Notification/TouchStatusApplet.h" #include "./Applets/System/Pairing/PairingApplet.h" #include "./Applets/System/Placeholder/PlaceholderApplet.h" #include "./Applets/System/Tips/TipsApplet.h" @@ -15,6 +17,14 @@ using namespace NicheGraphics; +namespace +{ +bool supportsOnScreenKeyboard(const InkHUD::InkHUD *inkhud, const InkHUD::Persistence::Settings *settings) +{ + return !inkhud->twoWayRocker && (settings->joystick.enabled || inkhud->hasTouchEnabledProvider()); +} +} // namespace + InkHUD::WindowManager::WindowManager() { // Convenient references @@ -132,6 +142,38 @@ void InkHUD::WindowManager::prevTile() userTiles.at(settings->userTiles.focused)->requestHighlight(); } +// Focus the user tile containing a touch coordinate. +// Returns true only when the focused tile changes. +bool InkHUD::WindowManager::selectTileAt(uint16_t x, uint16_t y) +{ + if (userTiles.size() < 2) + return false; + + const int32_t tx = x; + const int32_t ty = y; + + for (uint8_t i = 0; i < userTiles.size(); i++) { + Tile *tile = userTiles.at(i); + + const int32_t left = tile->getLeft(); + const int32_t top = tile->getTop(); + const int32_t right = left + tile->getWidth(); + const int32_t bottom = top + tile->getHeight(); + + if (tx < left || tx >= right || ty < top || ty >= bottom) + continue; + + if (settings->userTiles.focused == i) + return false; + + settings->userTiles.focused = i; + refocusTile(); + return true; + } + + return false; +} + // Show the menu (on the the focused tile) // The applet previously displayed there will be restored once the menu closes void InkHUD::WindowManager::openMenu() @@ -140,6 +182,18 @@ void InkHUD::WindowManager::openMenu() menu->show(userTiles.at(settings->userTiles.focused)); } +// Show touch-only app switcher on the focused tile +void InkHUD::WindowManager::openAppSwitcher() +{ + if (!inkhud->hasTouchEnabledProvider()) + return; + + AppSwitcherApplet *switcher = static_cast(inkhud->getSystemApplet("AppSwitcher")); + if (switcher) { + switcher->show(userTiles.at(settings->userTiles.focused)); + } +} + // Bring the AlignStick applet to the foreground void InkHUD::WindowManager::openAlignStick() { @@ -151,7 +205,7 @@ void InkHUD::WindowManager::openAlignStick() void InkHUD::WindowManager::openKeyboard() { - if (!settings->joystick.enabled || inkhud->twoWayRocker) + if (!supportsOnScreenKeyboard(inkhud, settings)) return; KeyboardApplet *keyboard = (KeyboardApplet *)inkhud->getSystemApplet("Keyboard"); @@ -165,7 +219,7 @@ void InkHUD::WindowManager::openKeyboard() void InkHUD::WindowManager::closeKeyboard() { - if (!settings->joystick.enabled || inkhud->twoWayRocker) + if (!supportsOnScreenKeyboard(inkhud, settings)) return; KeyboardApplet *keyboard = (KeyboardApplet *)inkhud->getSystemApplet("Keyboard"); @@ -279,6 +333,41 @@ void InkHUD::WindowManager::prevApplet() inkhud->forceUpdate(EInk::UpdateTypes::FAST); // bringToForeground already requested, but we're manually forcing FAST } +// Show a specific applet on the focused tile, or focus the tile where it is already shown. +bool InkHUD::WindowManager::showApplet(uint8_t appletIndex) +{ + if (appletIndex >= inkhud->userApplets.size()) + return false; + + Applet *target = inkhud->userApplets.at(appletIndex); + if (!target || !target->isActive()) + return false; + + // If target is already visible on another tile, just focus that tile. + for (uint8_t i = 0; i < userTiles.size(); i++) { + if (userTiles.at(i)->getAssignedApplet() == target) { + settings->userTiles.focused = i; + refocusTile(); + if (!settings->optionalMenuItems.nextTile) + userTiles.at(settings->userTiles.focused)->requestHighlight(); + inkhud->forceUpdate(EInk::UpdateTypes::FAST); + return true; + } + } + + // Otherwise replace the focused tile's applet. + Tile *focused = userTiles.at(settings->userTiles.focused); + Applet *current = focused->getAssignedApplet(); + if (current && current != target) + current->sendToBackground(); + + focused->assignApplet(target); + target->bringToForeground(); + settings->userTiles.displayedUserApplet[settings->userTiles.focused] = appletIndex; + inkhud->forceUpdate(EInk::UpdateTypes::FAST); + return true; +} + // Returns active applet NicheGraphics::InkHUD::Applet *InkHUD::WindowManager::getActiveApplet() { @@ -485,14 +574,21 @@ void InkHUD::WindowManager::createSystemApplets() addSystemApplet("Tips", new TipsApplet, new Tile); if (settings->joystick.enabled && !inkhud->twoWayRocker) { addSystemApplet("AlignStick", new AlignStickApplet, new Tile); + } + if (supportsOnScreenKeyboard(inkhud, settings)) { addSystemApplet("Keyboard", new KeyboardApplet, new Tile); } + if (inkhud->hasTouchEnabledProvider()) { + addSystemApplet("AppSwitcher", new AppSwitcherApplet, nullptr); + } addSystemApplet("Menu", new MenuApplet, nullptr); // Battery and notifications *behind* the menu addSystemApplet("Notification", new NotificationApplet, new Tile); addSystemApplet("BatteryIcon", new BatteryIconApplet, new Tile); + if (inkhud->hasTouchEnabledProvider()) + addSystemApplet("TouchStatus", new TouchStatusApplet, new Tile); // Special handling only, via Rendering::renderPlaceholders addSystemApplet("Placeholder", new PlaceholderApplet, nullptr); @@ -511,6 +607,8 @@ void InkHUD::WindowManager::placeSystemTiles() inkhud->getSystemApplet("Tips")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); if (settings->joystick.enabled && !inkhud->twoWayRocker) { inkhud->getSystemApplet("AlignStick")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); + } + if (supportsOnScreenKeyboard(inkhud, settings)) { const uint16_t keyboardHeight = KeyboardApplet::getKeyboardHeight(); inkhud->getSystemApplet("Keyboard") ->getTile() @@ -527,6 +625,17 @@ void InkHUD::WindowManager::placeSystemTiles() batteryIconWidth + 1, // width batteryIconHeight + 2); // height + if (inkhud->hasTouchEnabledProvider()) { + const uint16_t touchStatusH = Applet::fontSmall.lineHeight() + 4; + inkhud->getSystemApplet("TouchStatus") + ->getTile() + ->setRegion(0, inkhud->height() - touchStatusH, inkhud->width(), touchStatusH); + if (inkhud->isTouchEnabled()) + inkhud->getSystemApplet("TouchStatus")->sendToBackground(); + else + inkhud->getSystemApplet("TouchStatus")->bringToForeground(); + } + // Note: the tiles of placeholder and menu applets are manipulated specially // - menuApplet borrows user tiles // - placeholder applet is temporarily assigned to each user tile of WindowManager::getEmptyTiles diff --git a/src/graphics/niche/InkHUD/WindowManager.h b/src/graphics/niche/InkHUD/WindowManager.h index a11688cf5..1ab33c085 100644 --- a/src/graphics/niche/InkHUD/WindowManager.h +++ b/src/graphics/niche/InkHUD/WindowManager.h @@ -29,13 +29,16 @@ class WindowManager void nextTile(); void prevTile(); + bool selectTileAt(uint16_t x, uint16_t y); Applet *getActiveApplet(); void openMenu(); void openAlignStick(); + void openAppSwitcher(); void openKeyboard(); void closeKeyboard(); void nextApplet(); void prevApplet(); + bool showApplet(uint8_t appletIndex); void rotate(); void toggleBatteryIcon(); @@ -76,4 +79,4 @@ class WindowManager } // namespace NicheGraphics::InkHUD -#endif \ No newline at end of file +#endif diff --git a/src/input/ButtonThread.cpp b/src/input/ButtonThread.cpp index df8de4905..3b0a46e88 100644 --- a/src/input/ButtonThread.cpp +++ b/src/input/ButtonThread.cpp @@ -143,6 +143,10 @@ int32_t ButtonThread::runOnce() leadUpSequenceActive = false; resetLeadUpSequence(); } +#ifdef INPUT_DEBUG + if (buttonCurrentlyPressed) + LOG_WARN("Button held for %u ms", millis() - buttonPressStartTime); +#endif // Progressive lead-up sound system if (!_suppressLeadUp && buttonCurrentlyPressed && (millis() - buttonPressStartTime) >= BUTTON_LEADUP_MS) { @@ -311,7 +315,8 @@ int32_t ButtonThread::runOnce() void ButtonThread::attachButtonInterrupts() { // Interrupt for user button, during normal use. Improves responsiveness. - attachInterrupt(_pinNum, _intRoutine, CHANGE); + if (_intRoutine != nullptr) + attachInterrupt(_pinNum, _intRoutine, CHANGE); } /* @@ -320,7 +325,8 @@ void ButtonThread::attachButtonInterrupts() */ void ButtonThread::detachButtonInterrupts() { - detachInterrupt(_pinNum); + if (_intRoutine != nullptr) + detachInterrupt(_pinNum); } #ifdef ARCH_ESP32 diff --git a/src/input/TouchScreenBase.cpp b/src/input/TouchScreenBase.cpp index 4fd275a40..0afb13faf 100644 --- a/src/input/TouchScreenBase.cpp +++ b/src/input/TouchScreenBase.cpp @@ -9,6 +9,35 @@ #define TIME_LONG_PRESS 400 #endif +// Touch sampling cadence (milliseconds). +// Can be overridden by board variants for faster touch panels. +#ifndef TOUCH_POLL_INTERVAL_IDLE +#define TOUCH_POLL_INTERVAL_IDLE 100 +#endif + +#ifndef TOUCH_POLL_INTERVAL_ACTIVE +#define TOUCH_POLL_INTERVAL_ACTIVE 20 +#endif + +#ifndef TOUCH_POLL_INTERVAL_RELEASE +#define TOUCH_POLL_INTERVAL_RELEASE 50 +#endif + +// Faster cadence used for keyboard-like tap-heavy UIs. +#ifndef TOUCH_POLL_INTERVAL_ACTIVE_FAST +#define TOUCH_POLL_INTERVAL_ACTIVE_FAST TOUCH_POLL_INTERVAL_ACTIVE +#endif + +#ifndef TOUCH_POLL_INTERVAL_RELEASE_FAST +#define TOUCH_POLL_INTERVAL_RELEASE_FAST TOUCH_POLL_INTERVAL_RELEASE +#endif + +// Ignore very short "finger lifted" glitches from noisy touch controllers. +// A release is only accepted once we've seen no-touch for at least this duration. +#ifndef TOUCH_RELEASE_GRACE_MS +#define TOUCH_RELEASE_GRACE_MS 35 +#endif + // move a minimum distance over the screen to detect a "swipe" #ifndef TOUCH_THRESHOLD_X #define TOUCH_THRESHOLD_X 30 @@ -20,7 +49,7 @@ TouchScreenBase::TouchScreenBase(const char *name, uint16_t width, uint16_t height) : concurrency::OSThread(name), _display_width(width), _display_height(height), _first_x(0), _last_x(0), _first_y(0), - _last_y(0), _start(0), _tapped(false), _originName(name) + _last_y(0), _start(0), _lastTouchSeenMs(0), _tapped(false), _originName(name) { } @@ -28,7 +57,7 @@ void TouchScreenBase::init(bool hasTouch) { if (hasTouch) { LOG_INFO("TouchScreen initialized %d %d", TOUCH_THRESHOLD_X, TOUCH_THRESHOLD_Y); - this->setInterval(100); + this->setInterval(TOUCH_POLL_INTERVAL_IDLE); } else { disable(); this->setInterval(UINT_MAX); @@ -39,6 +68,8 @@ int32_t TouchScreenBase::runOnce() { TouchEvent e; e.touchEvent = static_cast(TOUCH_ACTION_NONE); + const bool fastTapMode = fastTapModeEnabled(); + const bool allowLongPress = longPressEnabled(); // process touch events int16_t x, y; @@ -46,9 +77,13 @@ int32_t TouchScreenBase::runOnce() if (x < 0 || y < 0) // T-deck can emit phantom touch events with a negative value when turning off the screen touched = false; if (touched) { - this->setInterval(20); + _lastTouchSeenMs = millis(); + this->setInterval(fastTapMode ? TOUCH_POLL_INTERVAL_ACTIVE_FAST : TOUCH_POLL_INTERVAL_ACTIVE); _last_x = x; _last_y = y; + } else if (_touchedOld && ((uint32_t)millis() - _lastTouchSeenMs) < TOUCH_RELEASE_GRACE_MS) { + // Treat brief no-touch samples as continuous touch to preserve long-press detection. + touched = true; } if (touched != _touchedOld) { if (touched) { @@ -62,7 +97,7 @@ int32_t TouchScreenBase::runOnce() time_t duration = millis() - _start; x = _last_x; y = _last_y; - this->setInterval(50); + this->setInterval(fastTapMode ? TOUCH_POLL_INTERVAL_RELEASE_FAST : TOUCH_POLL_INTERVAL_RELEASE); // compute distance int16_t dx = x - _first_x; @@ -92,7 +127,7 @@ int32_t TouchScreenBase::runOnce() } // tap else { - if (duration > 0 && duration < TIME_LONG_PRESS) { + if (duration > 0 && (duration < TIME_LONG_PRESS || !allowLongPress)) { if (_tapped) { _tapped = false; } else { @@ -132,7 +167,7 @@ int32_t TouchScreenBase::runOnce() #endif // fire LONG_PRESS event without the need for release - if (touched && (time_t(millis()) - _start) > TIME_LONG_PRESS) { + if (allowLongPress && touched && (time_t(millis()) - _start) > TIME_LONG_PRESS) { // tricky: prevent reoccurring events and another touch event when releasing _start = millis() + 30000; e.touchEvent = static_cast(TOUCH_ACTION_LONG_PRESS); @@ -157,3 +192,13 @@ void TouchScreenBase::hapticFeedback() drv.go(); #endif } + +bool TouchScreenBase::fastTapModeEnabled() const +{ + return false; +} + +bool TouchScreenBase::longPressEnabled() const +{ + return true; +} diff --git a/src/input/TouchScreenBase.h b/src/input/TouchScreenBase.h index 90314cf02..e08c4832b 100644 --- a/src/input/TouchScreenBase.h +++ b/src/input/TouchScreenBase.h @@ -35,6 +35,8 @@ class TouchScreenBase : public Observable, public concurrenc virtual bool getTouch(int16_t &x, int16_t &y) = 0; virtual void onEvent(const TouchEvent &event) = 0; + virtual bool fastTapModeEnabled() const; + virtual bool longPressEnabled() const; volatile TouchScreenBaseStateType _state = TOUCH_EVENT_CLEARED; volatile TouchScreenBaseEventType _action = TOUCH_ACTION_NONE; @@ -49,6 +51,7 @@ class TouchScreenBase : public Observable, public concurrenc int16_t _first_x, _last_x; // horizontal swipe direction int16_t _first_y, _last_y; // vertical swipe direction time_t _start; // for LONG_PRESS + uint32_t _lastTouchSeenMs; // helps suppress brief touch-controller dropouts bool _tapped; // for DOUBLE_TAP const char *_originName; diff --git a/src/input/TouchScreenImpl1.cpp b/src/input/TouchScreenImpl1.cpp index 69dcab04e..0f1c9d023 100644 --- a/src/input/TouchScreenImpl1.cpp +++ b/src/input/TouchScreenImpl1.cpp @@ -3,6 +3,12 @@ #include "PowerFSM.h" #include "configuration.h" #include "modules/ExternalNotificationModule.h" +#include + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS +#include "graphics/niche/InkHUD/InkHUD.h" +#include "graphics/niche/InkHUD/SystemApplet.h" +#endif #if ARCH_PORTDUINO #include "platform/portduino/PortduinoGlue.h" @@ -29,7 +35,8 @@ void TouchScreenImpl1::init() return; #else TouchScreenBase::init(true); - inputBroker->registerSource(this); + if (inputBroker) + inputBroker->registerSource(this); #endif } @@ -38,6 +45,31 @@ bool TouchScreenImpl1::getTouch(int16_t &x, int16_t &y) return _getTouch(&x, &y); } +bool TouchScreenImpl1::fastTapModeEnabled() const +{ +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + const auto *inkhud = NicheGraphics::InkHUD::InkHUD::getInstance(); + if (!inkhud) { + return false; + } + + for (auto *sa : inkhud->systemApplets) { + if (!sa || !sa->name) { + continue; + } + if (strcmp(sa->name, "Keyboard") == 0) { + return sa->isForeground(); + } + } +#endif + return false; +} + +bool TouchScreenImpl1::longPressEnabled() const +{ + return !fastTapModeEnabled(); +} + /** * @brief forward touchscreen event * @@ -82,4 +114,4 @@ void TouchScreenImpl1::onEvent(const TouchEvent &event) return; } this->notifyObservers(&e); -} \ No newline at end of file +} diff --git a/src/input/TouchScreenImpl1.h b/src/input/TouchScreenImpl1.h index 0c5338459..3629982ef 100644 --- a/src/input/TouchScreenImpl1.h +++ b/src/input/TouchScreenImpl1.h @@ -10,6 +10,8 @@ class TouchScreenImpl1 : public TouchScreenBase protected: virtual bool getTouch(int16_t &x, int16_t &y); virtual void onEvent(const TouchEvent &event); + bool fastTapModeEnabled() const override; + bool longPressEnabled() const override; bool (*_getTouch)(int16_t *, int16_t *); }; diff --git a/src/main.cpp b/src/main.cpp index bf0f52626..9b83602d5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -340,138 +340,9 @@ void setup() #ifdef BLE_LED pinMode(BLE_LED, OUTPUT); -#ifdef BLE_LED_INVERTED - digitalWrite(BLE_LED, HIGH); -#else - digitalWrite(BLE_LED, LOW); -#endif -#endif - -#if defined(T_DECK) - // GPIO10 manages all peripheral power supplies - // Turn on peripheral power immediately after MUC starts. - // If some boards are turned on late, ESP32 will reset due to low voltage. - // ESP32-C3(Keyboard) , MAX98357A(Audio Power Amplifier) , - // TF Card , Display backlight(AW9364DNR) , AN48841B(Trackball) , ES7210(Decoder) - pinMode(KB_POWERON, OUTPUT); - digitalWrite(KB_POWERON, HIGH); - // T-Deck has all three SPI peripherals (TFT, SD, LoRa) attached to the same SPI bus - // We need to initialize all CS pins in advance otherwise there will be SPI communication issues - // e.g. when detecting the SD card - pinMode(LORA_CS, OUTPUT); - digitalWrite(LORA_CS, HIGH); - pinMode(SDCARD_CS, OUTPUT); - digitalWrite(SDCARD_CS, HIGH); - pinMode(TFT_CS, OUTPUT); - digitalWrite(TFT_CS, HIGH); - delay(100); -#elif defined(T_DECK_PRO) - pinMode(LORA_EN, OUTPUT); - digitalWrite(LORA_EN, HIGH); - pinMode(LORA_CS, OUTPUT); - digitalWrite(LORA_CS, HIGH); - pinMode(SDCARD_CS, OUTPUT); - digitalWrite(SDCARD_CS, HIGH); - pinMode(PIN_EINK_CS, OUTPUT); - digitalWrite(PIN_EINK_CS, HIGH); -#if PIN_EINK_RES >= 0 - pinMode(PIN_EINK_RES, OUTPUT); - digitalWrite(PIN_EINK_RES, HIGH); -#endif - pinMode(CST328_PIN_RST, OUTPUT); - digitalWrite(CST328_PIN_RST, HIGH); -#elif defined(T_LORA_PAGER) - pinMode(LORA_CS, OUTPUT); - digitalWrite(LORA_CS, HIGH); - pinMode(SDCARD_CS, OUTPUT); - digitalWrite(SDCARD_CS, HIGH); - pinMode(TFT_CS, OUTPUT); - digitalWrite(TFT_CS, HIGH); - pinMode(KB_INT, INPUT_PULLUP); - // io expander - io.begin(Wire, XL9555_SLAVE_ADDRESS0, SDA, SCL); - io.pinMode(EXPANDS_DRV_EN, OUTPUT); - io.digitalWrite(EXPANDS_DRV_EN, HIGH); - io.pinMode(EXPANDS_AMP_EN, OUTPUT); - io.digitalWrite(EXPANDS_AMP_EN, LOW); - io.pinMode(EXPANDS_LORA_EN, OUTPUT); - io.digitalWrite(EXPANDS_LORA_EN, HIGH); - io.pinMode(EXPANDS_GPS_EN, OUTPUT); - io.digitalWrite(EXPANDS_GPS_EN, HIGH); - io.pinMode(EXPANDS_KB_EN, OUTPUT); - io.digitalWrite(EXPANDS_KB_EN, HIGH); - io.pinMode(EXPANDS_SD_EN, OUTPUT); - io.digitalWrite(EXPANDS_SD_EN, HIGH); - io.pinMode(EXPANDS_GPIO_EN, OUTPUT); - io.digitalWrite(EXPANDS_GPIO_EN, HIGH); - io.pinMode(EXPANDS_SD_PULLEN, INPUT); -#elif defined(HACKADAY_COMMUNICATOR) - pinMode(KB_INT, INPUT); digitalWrite(BLE_LED, LED_STATE_OFF); #endif -#if defined(T_DECK) - // GPIO10 manages all peripheral power supplies - // Turn on peripheral power immediately after MUC starts. - // If some boards are turned on late, ESP32 will reset due to low voltage. - // ESP32-C3(Keyboard) , MAX98357A(Audio Power Amplifier) , - // TF Card , Display backlight(AW9364DNR) , AN48841B(Trackball) , ES7210(Decoder) - pinMode(KB_POWERON, OUTPUT); - digitalWrite(KB_POWERON, HIGH); - // T-Deck has all three SPI peripherals (TFT, SD, LoRa) attached to the same SPI bus - // We need to initialize all CS pins in advance otherwise there will be SPI communication issues - // e.g. when detecting the SD card - pinMode(LORA_CS, OUTPUT); - digitalWrite(LORA_CS, HIGH); - pinMode(SDCARD_CS, OUTPUT); - digitalWrite(SDCARD_CS, HIGH); - pinMode(TFT_CS, OUTPUT); - digitalWrite(TFT_CS, HIGH); - delay(100); -#elif defined(T_DECK_PRO) - pinMode(LORA_EN, OUTPUT); - digitalWrite(LORA_EN, HIGH); - pinMode(LORA_CS, OUTPUT); - digitalWrite(LORA_CS, HIGH); - pinMode(SDCARD_CS, OUTPUT); - digitalWrite(SDCARD_CS, HIGH); - pinMode(PIN_EINK_CS, OUTPUT); - digitalWrite(PIN_EINK_CS, HIGH); -#if PIN_EINK_RES >= 0 - pinMode(PIN_EINK_RES, OUTPUT); - digitalWrite(PIN_EINK_RES, HIGH); -#endif - pinMode(CST328_PIN_RST, OUTPUT); - digitalWrite(CST328_PIN_RST, HIGH); -#elif defined(T_LORA_PAGER) - pinMode(LORA_CS, OUTPUT); - digitalWrite(LORA_CS, HIGH); - pinMode(SDCARD_CS, OUTPUT); - digitalWrite(SDCARD_CS, HIGH); - pinMode(TFT_CS, OUTPUT); - digitalWrite(TFT_CS, HIGH); - pinMode(KB_INT, INPUT_PULLUP); - // io expander - io.begin(Wire, XL9555_SLAVE_ADDRESS0, SDA, SCL); - io.pinMode(EXPANDS_DRV_EN, OUTPUT); - io.digitalWrite(EXPANDS_DRV_EN, HIGH); - io.pinMode(EXPANDS_AMP_EN, OUTPUT); - io.digitalWrite(EXPANDS_AMP_EN, LOW); - io.pinMode(EXPANDS_LORA_EN, OUTPUT); - io.digitalWrite(EXPANDS_LORA_EN, HIGH); - io.pinMode(EXPANDS_GPS_EN, OUTPUT); - io.digitalWrite(EXPANDS_GPS_EN, HIGH); - io.pinMode(EXPANDS_KB_EN, OUTPUT); - io.digitalWrite(EXPANDS_KB_EN, HIGH); - io.pinMode(EXPANDS_SD_EN, OUTPUT); - io.digitalWrite(EXPANDS_SD_EN, HIGH); - io.pinMode(EXPANDS_GPIO_EN, OUTPUT); - io.digitalWrite(EXPANDS_GPIO_EN, HIGH); - io.pinMode(EXPANDS_SD_PULLEN, INPUT); -#elif defined(HACKADAY_COMMUNICATOR) - pinMode(KB_INT, INPUT); -#endif - concurrency::hasBeenSetup = true; #if HAS_SCREEN meshtastic_Config_DisplayConfig_OledType screen_model = diff --git a/src/mesh/HardwareRNG.cpp b/src/mesh/HardwareRNG.cpp index f5a805487..b79b0d012 100644 --- a/src/mesh/HardwareRNG.cpp +++ b/src/mesh/HardwareRNG.cpp @@ -48,8 +48,11 @@ bool mixWithLoRaEntropy(uint8_t *buffer, size_t length) // and return false so callers know no extra mixing occurred. RadioLibInterface *radio = RadioLibInterface::instance; if (!radio) { - // Intentionally silent: this path runs during portduinoSetup() before the - // console/SerialConsole is initialized, so LOG_* here would dereference a null pointer. + // This path can run during portduinoSetup() before the console is initialized, + // both for unit-test binaries and the simulator's meshtasticd; LOG_* dereferences `console`. + if (console) { + LOG_ERROR("No radio instance available to provide entropy"); + } return false; } diff --git a/src/mesh/NextHopRouter.cpp b/src/mesh/NextHopRouter.cpp index 13f948a7b..e8613d457 100644 --- a/src/mesh/NextHopRouter.cpp +++ b/src/mesh/NextHopRouter.cpp @@ -101,9 +101,12 @@ void NextHopRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtast if (origTx) { // Either relayer of ACK was also a relayer of the packet, or we were the *only* relayer and the ACK came // directly from the destination - bool wasAlreadyRelayer = wasRelayer(p->relay_node, p->decoded.request_id, p->to); + // Single lookup for both relayer checks on the same (request_id, to) pair + bool wasAlreadyRelayer = false; bool weWereSoleRelayer = false; - bool weWereRelayer = wasRelayer(ourRelayID, p->decoded.request_id, p->to, &weWereSoleRelayer); + bool weWereRelayer = false; + checkRelayers(p->relay_node, ourRelayID, p->decoded.request_id, p->to, &wasAlreadyRelayer, &weWereRelayer, + &weWereSoleRelayer); if ((weWereRelayer && wasAlreadyRelayer) || (getHopsAway(*p) == 0 && weWereSoleRelayer)) { if (origTx->next_hop != p->relay_node) { // Not already set LOG_INFO("Update next hop of 0x%x to 0x%x based on ACK/reply (was relayer %d we were sole %d)", p->from, diff --git a/src/mesh/PacketHistory.cpp b/src/mesh/PacketHistory.cpp index 845a936d4..8289f0078 100644 --- a/src/mesh/PacketHistory.cpp +++ b/src/mesh/PacketHistory.cpp @@ -1,6 +1,7 @@ #include "PacketHistory.h" #include "configuration.h" #include "mesh-pb-constants.h" +#include "meshUtils.h" #ifdef ARCH_PORTDUINO #include "platform/portduino/PortduinoGlue.h" @@ -23,6 +24,14 @@ PacketHistory::PacketHistory(uint32_t size) : recentPacketsCapacity(0), recentPa size = PACKETHISTORY_MAX; // Use default size if invalid } +#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH + // Ensure capacity fits in uint16_t hash index (HASH_EMPTY = 0xFFFF is the sentinel) + if (size >= HASH_EMPTY) { + LOG_WARN("Packet History - Clamping size %d to %d (hash index limit)", size, HASH_EMPTY - 1); + size = HASH_EMPTY - 1; + } +#endif + // Allocate memory for the recent packets array recentPacketsCapacity = size; recentPackets = new PacketRecord[recentPacketsCapacity]; @@ -35,6 +44,20 @@ PacketHistory::PacketHistory(uint32_t size) : recentPacketsCapacity(0), recentPa // Initialize the recent packets array to zero memset(recentPackets, 0, sizeof(PacketRecord) * recentPacketsCapacity); + +#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH + // Allocate hash index with load factor <= 0.5 for short probe chains + hashCapacity = nextPowerOf2(recentPacketsCapacity * 2); + hashMask = hashCapacity - 1; + hashIndex = new uint16_t[hashCapacity]; + if (!hashIndex) { + LOG_ERROR("Packet History - Hash index allocation failed for %d entries", hashCapacity); + hashCapacity = 0; + hashMask = 0; + return; + } + memset(hashIndex, 0xFF, sizeof(uint16_t) * hashCapacity); // Fill with HASH_EMPTY (0xFFFF) +#endif } PacketHistory::~PacketHistory() @@ -42,6 +65,12 @@ PacketHistory::~PacketHistory() recentPacketsCapacity = 0; delete[] recentPackets; recentPackets = NULL; +#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH + delete[] hashIndex; + hashIndex = NULL; + hashCapacity = 0; + hashMask = 0; +#endif } /** Update recentPackets and return true if we have already seen this packet */ @@ -194,7 +223,78 @@ bool PacketHistory::wasSeenRecently(const meshtastic_MeshPacket *p, bool withUpd return seenRecently; } -/** Find a packet record in history. +#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH +// Hash function for (sender, id) pairs. Uses xor-shift mixing for good distribution. +uint32_t PacketHistory::hashSlot(NodeNum sender, PacketId id) const +{ + uint32_t h = sender ^ (id * 0x9E3779B9); // Fibonacci hashing constant + h ^= h >> 16; + h *= 0x45d9f3b; + h ^= h >> 16; + return h & hashMask; +} + +void PacketHistory::hashInsert(NodeNum sender, PacketId id, uint16_t slotIdx) +{ + if (!hashIndex) + return; + uint32_t bucket = hashSlot(sender, id); + // Guard against infinite loop if hash table is corrupted (no HASH_EMPTY slots) + for (uint32_t i = 0; i < hashCapacity; i++) { + if (hashIndex[bucket] == HASH_EMPTY) { + hashIndex[bucket] = slotIdx; + return; + } + bucket = (bucket + 1) & hashMask; + } + LOG_ERROR("Packet History - hashInsert: table full or corrupted, rebuilding"); + hashRebuild(); +} + +void PacketHistory::hashRemove(NodeNum sender, PacketId id) +{ + if (!hashIndex) + return; + uint32_t bucket = hashSlot(sender, id); + for (uint32_t i = 0; i < hashCapacity; i++) { + if (hashIndex[bucket] == HASH_EMPTY) + return; + uint16_t idx = hashIndex[bucket]; + if (idx < recentPacketsCapacity && recentPackets[idx].sender == sender && recentPackets[idx].id == id) { + // Found it — delete and re-insert subsequent entries to maintain probe chain integrity + hashIndex[bucket] = HASH_EMPTY; + uint32_t next = (bucket + 1) & hashMask; + for (uint32_t j = 0; j < hashCapacity; j++) { + if (hashIndex[next] == HASH_EMPTY) + break; + uint16_t displaced = hashIndex[next]; + hashIndex[next] = HASH_EMPTY; + if (displaced < recentPacketsCapacity) { + const auto &rec = recentPackets[displaced]; + hashInsert(rec.sender, rec.id, displaced); + } + next = (next + 1) & hashMask; + } + return; + } + bucket = (bucket + 1) & hashMask; + } +} + +void PacketHistory::hashRebuild() +{ + if (!hashIndex) + return; + memset(hashIndex, 0xFF, sizeof(uint16_t) * hashCapacity); + for (uint32_t i = 0; i < recentPacketsCapacity; i++) { + if (recentPackets[i].rxTimeMsec != 0) + hashInsert(recentPackets[i].sender, recentPackets[i].id, (uint16_t)i); + } +} +#endif + +/** Find a packet record in history using the hash index for O(1) average lookup. + * Falls back to linear scan if hash index is unavailable. * @return pointer to PacketRecord if found, NULL if not found */ PacketHistory::PacketRecord *PacketHistory::find(NodeNum sender, PacketId id) { @@ -205,23 +305,40 @@ PacketHistory::PacketRecord *PacketHistory::find(NodeNum sender, PacketId id) return NULL; } - PacketRecord *it = NULL; - for (it = recentPackets; it < (recentPackets + recentPacketsCapacity); ++it) { - if (it->id == id && it->sender == sender) { +#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH + // Use hash index for O(1) lookup when available + if (hashIndex) { + uint32_t bucket = hashSlot(sender, id); + for (uint32_t i = 0; i < hashCapacity; i++) { + if (hashIndex[bucket] == HASH_EMPTY) + break; + uint16_t idx = hashIndex[bucket]; + if (idx < recentPacketsCapacity && recentPackets[idx].id == id && recentPackets[idx].sender == sender) { #if VERBOSE_PACKET_HISTORY - LOG_DEBUG("Packet History - find: s=%08x id=%08x FOUND nh=%02x rby=%02x %02x %02x age=%d slot=%d/%d", it->sender, - it->id, it->next_hop, it->relayed_by[0], it->relayed_by[1], it->relayed_by[2], millis() - (it->rxTimeMsec), - it - recentPackets, recentPacketsCapacity); + LOG_DEBUG("Packet History - find: s=%08x id=%08x FOUND nh=%02x rby=%02x %02x %02x age=%d slot=%d/%d", + recentPackets[idx].sender, recentPackets[idx].id, recentPackets[idx].next_hop, + recentPackets[idx].relayed_by[0], recentPackets[idx].relayed_by[1], recentPackets[idx].relayed_by[2], + millis() - (recentPackets[idx].rxTimeMsec), idx, recentPacketsCapacity); #endif - // only the first match is returned, so be careful not to create duplicate entries - return it; // Return pointer to the found record + return &recentPackets[idx]; + } + bucket = (bucket + 1) & hashMask; + } +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - find: s=%08x id=%08x NOT FOUND", sender, id); +#endif + return NULL; + } +#endif + + // Linear scan (sole path when hash excluded, fallback when hash allocation failed) + for (PacketRecord *it = recentPackets; it < (recentPackets + recentPacketsCapacity); ++it) { + if (it->id == id && it->sender == sender) { + return it; } } -#if VERBOSE_PACKET_HISTORY - LOG_DEBUG("Packet History - find: s=%08x id=%08x NOT FOUND", sender, id); -#endif - return NULL; // Not found + return NULL; } /** Insert/Replace oldest PacketRecord in recentPackets. */ @@ -327,8 +444,22 @@ void PacketHistory::insert(const PacketRecord &r) return; // Return early if we can't update the history } +#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH + // Maintain hash index: remove old entry if evicting a different packet, then insert new entry + bool isMatchingSlot = (tu->id == r.id && tu->sender == r.sender); + if (!isMatchingSlot && tu->rxTimeMsec != 0) { + hashRemove(tu->sender, tu->id); + } + *tu = r; // store the packet + if (!isMatchingSlot) { + hashInsert(r.sender, r.id, (uint16_t)(tu - recentPackets)); + } +#else + *tu = r; // store the packet +#endif + #if VERBOSE_PACKET_HISTORY LOG_DEBUG("Packet History - insert: Store slot@ %d/%d s=%08x id=%08x nh=%02x rby=%02x %02x %02x rxT=%d AFTER", tu - recentPackets, recentPacketsCapacity, tu->sender, tu->id, tu->next_hop, tu->relayed_by[0], tu->relayed_by[1], @@ -396,6 +527,31 @@ bool PacketHistory::wasRelayer(const uint8_t relayer, const PacketRecord &r, boo return found; } +// Check two relayers against the same packet record with a single find() call, +// avoiding redundant O(N) lookups when both are checked for the same (id, sender) pair. +void PacketHistory::checkRelayers(uint8_t relayer1, uint8_t relayer2, uint32_t id, NodeNum sender, bool *r1Result, bool *r2Result, + bool *r2WasSole) +{ + *r1Result = false; + *r2Result = false; + if (r2WasSole) + *r2WasSole = false; + + if (!initOk()) { + LOG_ERROR("PacketHistory - checkRelayers: NOT INITIALIZED!"); + return; + } + + const PacketRecord *found = find(sender, id); + if (!found) + return; + + if (relayer1 != 0) + *r1Result = wasRelayer(relayer1, *found); + if (relayer2 != 0) + *r2Result = wasRelayer(relayer2, *found, r2WasSole); +} + // Remove a relayer from the list of relayers of a packet in the history given an ID and sender void PacketHistory::removeRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender) { diff --git a/src/mesh/PacketHistory.h b/src/mesh/PacketHistory.h index 9b6a93280..a11e2d038 100644 --- a/src/mesh/PacketHistory.h +++ b/src/mesh/PacketHistory.h @@ -28,6 +28,22 @@ class PacketHistory 0; // Can be set in constructor, no need to recompile. Used to allocate memory for mx_recentPackets. PacketRecord *recentPackets = NULL; // Simple and fixed in size. Debloat. +#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH + // Open-addressing hash table for O(1) lookup in find(), replacing the O(N) linear scan. + // Maps (sender, id) -> index into recentPackets[]. Uses linear probing with a load factor <= 0.5. + // The load factor invariant holds permanently: hashCapacity = 2 * nextPowerOf2(recentPacketsCapacity), + // and at most recentPacketsCapacity entries can ever be live (one per recentPackets[] slot). + static constexpr uint16_t HASH_EMPTY = 0xFFFF; + uint16_t *hashIndex = NULL; + uint32_t hashCapacity = 0; // Always a power of 2 + uint32_t hashMask = 0; // hashCapacity - 1, for fast modular indexing + + uint32_t hashSlot(NodeNum sender, PacketId id) const; + void hashInsert(NodeNum sender, PacketId id, uint16_t slotIdx); + void hashRemove(NodeNum sender, PacketId id); + void hashRebuild(); +#endif + /** Find a packet record in history. * @param sender NodeNum * @param id PacketId @@ -70,6 +86,16 @@ class PacketHistory * @return true if node was indeed a relayer, false if not */ bool wasRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender, bool *wasSole = nullptr); + /** + * Check two relayers against the same packet record with a single lookup. + * Avoids redundant find() calls when checking multiple relayers for the same (id, sender) pair. + * @param r1Result set to true if relayer1 was a relayer + * @param r2Result set to true if relayer2 was a relayer + * @param r2WasSole if not nullptr, set to true if relayer2 was the sole relayer + */ + void checkRelayers(uint8_t relayer1, uint8_t relayer2, uint32_t id, NodeNum sender, bool *r1Result, bool *r2Result, + bool *r2WasSole = nullptr); + // Remove a relayer from the list of relayers of a packet in the history given an ID and sender void removeRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender); diff --git a/src/mesh/PhoneAPI.cpp b/src/mesh/PhoneAPI.cpp index 714e61108..cb25efb77 100644 --- a/src/mesh/PhoneAPI.cpp +++ b/src/mesh/PhoneAPI.cpp @@ -470,8 +470,13 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf) fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_traffic_management_tag; fromRadioScratch.moduleConfig.payload_variant.traffic_management = moduleConfig.traffic_management; break; + case meshtastic_ModuleConfig_tak_tag: + LOG_DEBUG("Send module config: tak"); + fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_tak_tag; + fromRadioScratch.moduleConfig.payload_variant.tak = moduleConfig.tak; + break; default: - LOG_ERROR("Unknown module config type %d", config_state); + LOG_DEBUG("Unhandled module config type %d", config_state); } config_state++; diff --git a/src/mesh/RadioLibInterface.cpp b/src/mesh/RadioLibInterface.cpp index 7ef707e0d..6024d06b6 100644 --- a/src/mesh/RadioLibInterface.cpp +++ b/src/mesh/RadioLibInterface.cpp @@ -46,6 +46,16 @@ RadioLibInterface::RadioLibInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE c #endif } +RadioLibInterface::~RadioLibInterface() +{ + // If the static `instance` pointer still references us, clear it. + // A later successful init() may have replaced `instance` with a newer + // interface — don't clobber that case. + if (instance == this) { + instance = nullptr; + } +} + #ifdef ARCH_ESP32 // ESP32 doesn't use that flag #define YIELD_FROM_ISR(x) portYIELD_FROM_ISR() diff --git a/src/mesh/RadioLibInterface.h b/src/mesh/RadioLibInterface.h index 310ca76bb..2859558ed 100644 --- a/src/mesh/RadioLibInterface.h +++ b/src/mesh/RadioLibInterface.h @@ -136,6 +136,13 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified RadioLibInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy, PhysicalLayer *iface = NULL); + /** + * Clear the static `instance` pointer if it still points at us, so callers + * that check `RadioLibInterface::instance != nullptr` don't dereference a + * freed object after a failed init() + unique_ptr reset. + */ + virtual ~RadioLibInterface(); + virtual ErrorCode send(meshtastic_MeshPacket *p) override; /** diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index 836cd1a22..ffeb7c539 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -499,9 +499,9 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p) meshtastic_Data decodedtmp; memset(&decodedtmp, 0, sizeof(decodedtmp)); if (!pb_decode_from_bytes(bytes, rawSize, &meshtastic_Data_msg, &decodedtmp)) { - LOG_ERROR("Invalid protobufs in received mesh packet id=0x%08x (bad psk?)!", p->id); + LOG_DEBUG("Invalid protobufs in received mesh packet id=0x%08x (bad psk?)", p->id); } else if (decodedtmp.portnum == meshtastic_PortNum_UNKNOWN_APP) { - LOG_ERROR("Invalid portnum (bad psk?)!"); + LOG_DEBUG("Invalid portnum (bad psk?)"); #if !(MESHTASTIC_EXCLUDE_PKI) } else if (!owner.is_licensed && isToUs(p) && decodedtmp.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP) { LOG_WARN("Rejecting legacy DM"); @@ -736,9 +736,13 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src) // Also, we should set the time from the ISR and it should have msec level resolution p->rx_time = getValidTime(RTCQualityFromNet); // store the arrival timestamp for the phone - // Store a copy of encrypted packet for MQTT + // Store a copy of the encrypted packet for MQTT. + // Local, not a class member: handleReceived re-enters itself when a module + // reply broadcast goes through MeshService::sendToMesh -> Router::sendLocal, + // and a member would be silently overwritten without release on the inner + // call. Each invocation now owns its own copy (issue #9632, #10101, #8729). DEBUG_HEAP_BEFORE; - p_encrypted = packetPool.allocCopy(*p); + meshtastic_MeshPacket *p_encrypted = packetPool.allocCopy(*p); DEBUG_HEAP_AFTER("Router::handleReceived", p_encrypted); // Take those raw bytes and convert them back into a well structured protobuf we can understand @@ -832,8 +836,7 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src) #endif } - packetPool.release(p_encrypted); // Release the encrypted packet - p_encrypted = nullptr; + packetPool.release(p_encrypted); // Release the encrypted packet (release() handles nullptr) } void Router::perhapsHandleReceived(meshtastic_MeshPacket *p) diff --git a/src/mesh/Router.h b/src/mesh/Router.h index 0f342d57b..bd4188693 100644 --- a/src/mesh/Router.h +++ b/src/mesh/Router.h @@ -92,9 +92,6 @@ class Router : protected concurrency::OSThread, protected PacketHistory before us */ uint32_t rxDupe = 0, txRelayCanceled = 0; - // pointer to the encrypted packet - meshtastic_MeshPacket *p_encrypted = nullptr; - protected: friend class RoutingModule; diff --git a/src/mesh/SX126xInterface.cpp b/src/mesh/SX126xInterface.cpp index bcb08f2c5..44c4a805a 100644 --- a/src/mesh/SX126xInterface.cpp +++ b/src/mesh/SX126xInterface.cpp @@ -455,6 +455,15 @@ template void SX126xInterface::resetAGC() // RX boosted gain mode lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain); + // Re-apply the undocumented 0x8B5 RX sensitivity patch that was set in init(). + // The CALIBRATE_ALL (0x7F) command above clears bit 0 of register 0x8B5, which + // silently removes the RX sensitivity improvement introduced in #9571 / #9777. + // Without this re-apply, every SX1262 node loses its RX boost ~60s after boot + // and never recovers until reboot. See empirical evidence in the PR description. + if (module.SPIsetRegValue(0x8B5, 0x01, 0, 0) != RADIOLIB_ERR_NONE) { + LOG_WARN("SX126x resetAGC: failed to re-apply 0x8B5 RX sensitivity patch"); + } + // 6. Resume receiving startReceive(); } diff --git a/src/mesh/TypeConversions.cpp b/src/mesh/TypeConversions.cpp index 75195bd42..3798daf28 100644 --- a/src/mesh/TypeConversions.cpp +++ b/src/mesh/TypeConversions.cpp @@ -1,6 +1,7 @@ #include "TypeConversions.h" #include "mesh/generated/meshtastic/deviceonly.pb.h" #include "mesh/generated/meshtastic/mesh.pb.h" +#include "meshUtils.h" meshtastic_NodeInfo TypeConversions::ConvertToNodeInfo(const meshtastic_NodeInfoLite *lite) { @@ -81,7 +82,11 @@ meshtastic_UserLite TypeConversions::ConvertToUserLite(meshtastic_User user) meshtastic_UserLite lite = meshtastic_UserLite_init_default; strncpy(lite.long_name, user.long_name, sizeof(lite.long_name)); + lite.long_name[sizeof(lite.long_name) - 1] = '\0'; + sanitizeUtf8(lite.long_name, sizeof(lite.long_name)); strncpy(lite.short_name, user.short_name, sizeof(lite.short_name)); + lite.short_name[sizeof(lite.short_name) - 1] = '\0'; + sanitizeUtf8(lite.short_name, sizeof(lite.short_name)); lite.hw_model = user.hw_model; lite.role = user.role; lite.is_licensed = user.is_licensed; @@ -99,7 +104,11 @@ meshtastic_User TypeConversions::ConvertToUser(uint32_t nodeNum, meshtastic_User snprintf(user.id, sizeof(user.id), "!%08x", nodeNum); strncpy(user.long_name, lite.long_name, sizeof(user.long_name)); + user.long_name[sizeof(user.long_name) - 1] = '\0'; + sanitizeUtf8(user.long_name, sizeof(user.long_name)); strncpy(user.short_name, lite.short_name, sizeof(user.short_name)); + user.short_name[sizeof(user.short_name) - 1] = '\0'; + sanitizeUtf8(user.short_name, sizeof(user.short_name)); user.hw_model = lite.hw_model; user.role = lite.role; user.is_licensed = lite.is_licensed; diff --git a/src/mesh/generated/meshtastic/config.pb.h b/src/mesh/generated/meshtastic/config.pb.h index c82dd5ff5..0e14334d5 100644 --- a/src/mesh/generated/meshtastic/config.pb.h +++ b/src/mesh/generated/meshtastic/config.pb.h @@ -285,7 +285,18 @@ typedef enum _meshtastic_Config_LoRaConfig_RegionCode { /* Nepal 865MHz */ meshtastic_Config_LoRaConfig_RegionCode_NP_865 = 25, /* Brazil 902MHz */ - meshtastic_Config_LoRaConfig_RegionCode_BR_902 = 26 + meshtastic_Config_LoRaConfig_RegionCode_BR_902 = 26, + /* ITU Region 1 Amateur Radio 2m band (144-146 MHz) */ + meshtastic_Config_LoRaConfig_RegionCode_ITU1_2M = 27, + /* ITU Region 2 / 3 Amateur Radio 2m band (144-148 MHz) */ + meshtastic_Config_LoRaConfig_RegionCode_ITU23_2M = 28, + /* EU 866MHz band (Band no. 47b of 2006/771/EC and subsequent amendments) for Non-specific short-range devices (SRD) */ + meshtastic_Config_LoRaConfig_RegionCode_EU_866 = 29, + /* EU 874MHz and 917MHz bands (Band no. 1 and 4 of 2022/172/EC and subsequent amendments) for Non-specific short-range devices (SRD) */ + meshtastic_Config_LoRaConfig_RegionCode_EU_874 = 30, + meshtastic_Config_LoRaConfig_RegionCode_EU_917 = 31, + /* EU 868MHz band, with narrow presets */ + meshtastic_Config_LoRaConfig_RegionCode_EU_N_868 = 32 } meshtastic_Config_LoRaConfig_RegionCode; /* Standard predefined channel settings @@ -315,7 +326,24 @@ typedef enum _meshtastic_Config_LoRaConfig_ModemPreset { meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO = 8, /* Long Range - Turbo This preset performs similarly to LongFast, but with 500Khz bandwidth. */ - meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO = 9 + meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO = 9, + /* Lite Fast + Medium range preset optimized for EU 866MHz SRD band with 125kHz bandwidth. + Comparable link budget to MEDIUM_FAST but compliant with Band no. 47b of 2006/771/EC. */ + meshtastic_Config_LoRaConfig_ModemPreset_LITE_FAST = 10, + /* Lite Slow + Medium-to-moderate range preset optimized for EU 866MHz SRD band with 125kHz bandwidth. + Comparable link budget to LONG_FAST but compliant with Band no. 47b of 2006/771/EC. */ + meshtastic_Config_LoRaConfig_ModemPreset_LITE_SLOW = 11, + /* Narrow Fast + Medium-to-moderate range preset optimized for EU 868MHz band with 62.5kHz bandwidth. + Comparable link budget to SHORT_SLOW, but with half the data rate. + Intended to avoid interference with other devices. */ + meshtastic_Config_LoRaConfig_ModemPreset_NARROW_FAST = 12, + /* Narrow Slow + Moderate range preset optimized for EU 868MHz band with 62.5kHz bandwidth. + Comparable link budget and data rate to LONG_FAST. */ + meshtastic_Config_LoRaConfig_ModemPreset_NARROW_SLOW = 13 } meshtastic_Config_LoRaConfig_ModemPreset; typedef enum _meshtastic_Config_LoRaConfig_FEM_LNA_Mode { @@ -702,12 +730,12 @@ extern "C" { #define _meshtastic_Config_DisplayConfig_CompassOrientation_ARRAYSIZE ((meshtastic_Config_DisplayConfig_CompassOrientation)(meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED+1)) #define _meshtastic_Config_LoRaConfig_RegionCode_MIN meshtastic_Config_LoRaConfig_RegionCode_UNSET -#define _meshtastic_Config_LoRaConfig_RegionCode_MAX meshtastic_Config_LoRaConfig_RegionCode_BR_902 -#define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_BR_902+1)) +#define _meshtastic_Config_LoRaConfig_RegionCode_MAX meshtastic_Config_LoRaConfig_RegionCode_EU_N_868 +#define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_EU_N_868+1)) #define _meshtastic_Config_LoRaConfig_ModemPreset_MIN meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST -#define _meshtastic_Config_LoRaConfig_ModemPreset_MAX meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO -#define _meshtastic_Config_LoRaConfig_ModemPreset_ARRAYSIZE ((meshtastic_Config_LoRaConfig_ModemPreset)(meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO+1)) +#define _meshtastic_Config_LoRaConfig_ModemPreset_MAX meshtastic_Config_LoRaConfig_ModemPreset_NARROW_SLOW +#define _meshtastic_Config_LoRaConfig_ModemPreset_ARRAYSIZE ((meshtastic_Config_LoRaConfig_ModemPreset)(meshtastic_Config_LoRaConfig_ModemPreset_NARROW_SLOW+1)) #define _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MIN meshtastic_Config_LoRaConfig_FEM_LNA_Mode_DISABLED #define _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MAX meshtastic_Config_LoRaConfig_FEM_LNA_Mode_NOT_PRESENT diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h index d7ff32cb4..f22825030 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.h +++ b/src/mesh/generated/meshtastic/mesh.pb.h @@ -315,6 +315,8 @@ typedef enum _meshtastic_HardwareModel { meshtastic_HardwareModel_THINKNODE_M7 = 129, meshtastic_HardwareModel_THINKNODE_M8 = 130, meshtastic_HardwareModel_THINKNODE_M9 = 131, + /* The Heltec-V4-R8 uses an ESP32S3R8 chip, plus an SX1262. */ + meshtastic_HardwareModel_HELTEC_V4_R8 = 132, /* ------------------------------------------------------------------------------------------------------------------------------------------ Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits. ------------------------------------------------------------------------------------------------------------------------------------------ */ diff --git a/src/meshUtils.cpp b/src/meshUtils.cpp index 1a4497101..f2ee20589 100644 --- a/src/meshUtils.cpp +++ b/src/meshUtils.cpp @@ -117,4 +117,93 @@ size_t pb_string_length(const char *str, size_t max_len) } } return len; +} + +bool sanitizeUtf8(char *buf, size_t bufSize) +{ + if (!buf || bufSize == 0) + return false; + + // Ensure null-terminated within buffer; report if we had to enforce it + bool replaced = (buf[bufSize - 1] != '\0'); + buf[bufSize - 1] = '\0'; + + size_t i = 0; + size_t len = strlen(buf); + + while (i < len) { + uint8_t b = (uint8_t)buf[i]; + + // Determine expected sequence length from lead byte + size_t seqLen; + uint32_t minCodepoint; + if (b <= 0x7F) { + // ASCII — valid single byte + i++; + continue; + } else if ((b & 0xE0) == 0xC0) { + seqLen = 2; + minCodepoint = 0x80; // Reject overlong + } else if ((b & 0xF0) == 0xE0) { + seqLen = 3; + minCodepoint = 0x800; + } else if ((b & 0xF8) == 0xF0) { + seqLen = 4; + minCodepoint = 0x10000; + } else { + // Invalid lead byte (0x80-0xBF or 0xF8+) + buf[i] = '?'; + replaced = true; + i++; + continue; + } + + // Check that we have enough bytes remaining + if (i + seqLen > len) { + // Truncated sequence at end of string — replace remaining bytes + for (size_t j = i; j < len; j++) { + buf[j] = '?'; + } + replaced = true; + break; + } + + // Validate continuation bytes (must be 10xxxxxx) + bool valid = true; + for (size_t j = 1; j < seqLen; j++) { + if (((uint8_t)buf[i + j] & 0xC0) != 0x80) { + valid = false; + break; + } + } + + if (valid) { + // Decode codepoint to check for overlong encodings and surrogates + uint32_t cp = 0; + if (seqLen == 2) + cp = b & 0x1F; + else if (seqLen == 3) + cp = b & 0x0F; + else + cp = b & 0x07; + for (size_t j = 1; j < seqLen; j++) + cp = (cp << 6) | ((uint8_t)buf[i + j] & 0x3F); + + if (cp < minCodepoint || cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF)) { + // Overlong encoding, out of Unicode range, or surrogate half + valid = false; + } + } + + if (valid) { + i += seqLen; + } else { + // Replace only the lead byte; continuation bytes will be caught on next iteration + buf[i] = '?'; + replaced = true; + i++; + } + } + + return replaced; } \ No newline at end of file diff --git a/src/meshUtils.h b/src/meshUtils.h index da3a4593b..4c450b3c4 100644 --- a/src/meshUtils.h +++ b/src/meshUtils.h @@ -11,6 +11,24 @@ template constexpr const T &clamp(const T &v, const T &lo, const T &hi return (v < lo) ? lo : (hi < v) ? hi : v; } +/// Return the smallest power of 2 >= n (undefined for n > 2^31) +static inline uint32_t nextPowerOf2(uint32_t n) +{ + if (n <= 1) + return 1; +#if defined(__GNUC__) + return 1U << (32 - __builtin_clz(n - 1)); +#else + n--; + n |= n >> 1; + n |= n >> 2; + n |= n >> 4; + n |= n >> 8; + n |= n >> 16; + return n + 1; +#endif +} + #if HAS_SCREEN #define IF_SCREEN(X) \ if (screen) { \ @@ -38,6 +56,10 @@ const std::string vformat(const char *const zcFormat, ...); // Get actual string length for nanopb char array fields. size_t pb_string_length(const char *str, size_t max_len); +// Sanitize a fixed-size char buffer in-place by replacing invalid UTF-8 sequences with '?'. +// Ensures the result is null-terminated within bufSize. Returns true if any bytes were replaced. +bool sanitizeUtf8(char *buf, size_t bufSize); + /// Calculate 2^n without calling pow() - used for spreading factor and other calculations inline uint32_t pow_of_2(uint32_t n) { diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 852a257e5..865ac38f5 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -626,10 +626,14 @@ void AdminModule::handleSetOwner(const meshtastic_User &o) if (*o.long_name) { changed |= strcmp(owner.long_name, o.long_name); strncpy(owner.long_name, o.long_name, sizeof(owner.long_name)); + owner.long_name[sizeof(owner.long_name) - 1] = '\0'; + sanitizeUtf8(owner.long_name, sizeof(owner.long_name)); } if (*o.short_name) { changed |= strcmp(owner.short_name, o.short_name); strncpy(owner.short_name, o.short_name, sizeof(owner.short_name)); + owner.short_name[sizeof(owner.short_name) - 1] = '\0'; + sanitizeUtf8(owner.short_name, sizeof(owner.short_name)); } snprintf(owner.id, sizeof(owner.id), "!%08x", nodeDB->getNodeNum()); @@ -1430,7 +1434,11 @@ void AdminModule::handleSetHamMode(const meshtastic_HamParameters &p) // Set call sign and override lora limitations for licensed use strncpy(owner.long_name, p.call_sign, sizeof(owner.long_name)); + owner.long_name[sizeof(owner.long_name) - 1] = '\0'; + sanitizeUtf8(owner.long_name, sizeof(owner.long_name)); strncpy(owner.short_name, p.short_name, sizeof(owner.short_name)); + owner.short_name[sizeof(owner.short_name) - 1] = '\0'; + sanitizeUtf8(owner.short_name, sizeof(owner.short_name)); owner.is_licensed = true; config.lora.override_duty_cycle = true; config.lora.tx_power = p.tx_power; diff --git a/src/modules/PositionModule.cpp b/src/modules/PositionModule.cpp index c435beb67..d9bbda7b9 100644 --- a/src/modules/PositionModule.cpp +++ b/src/modules/PositionModule.cpp @@ -492,15 +492,24 @@ void PositionModule::sendLostAndFoundText() { meshtastic_MeshPacket *p = allocDataPacket(); p->to = NODENUM_BROADCAST; - char *message = new char[60]; - sprintf(message, "🚨I'm lost! Lat / Lon: %f, %f\a", (lastGpsLatitude * 1e-7), (lastGpsLongitude * 1e-7)); + char message[128]; + int written = snprintf(message, sizeof(message), "🚨I'm lost! Lat / Lon: %f, %f\a", (lastGpsLatitude * 1e-7), + (lastGpsLongitude * 1e-7)); p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP; p->want_ack = false; - p->decoded.payload.size = strlen(message); - memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size); + if (written < 0) { + // snprintf encoding error — send an empty payload rather than uninitialized bytes. + p->decoded.payload.size = 0; + } else { + // Clamp to buffer capacity (snprintf returns "would-have-written" which can exceed the buffer). + const size_t msg_len = std::min(static_cast(written), sizeof(message) - 1); + p->decoded.payload.size = msg_len; + if (msg_len > 0) { + memcpy(p->decoded.payload.bytes, message, msg_len); + } + } service->sendToMesh(p, RX_SRC_LOCAL, true); - delete[] message; } // Helper: return imprecise (truncated + centered) lat/lon as int32 using current precision @@ -580,4 +589,4 @@ void PositionModule::handleNewPosition() } } -#endif \ No newline at end of file +#endif diff --git a/src/modules/StoreForwardModule.cpp b/src/modules/StoreForwardModule.cpp index 6df0e18f0..6c2efe83f 100644 --- a/src/modules/StoreForwardModule.cpp +++ b/src/modules/StoreForwardModule.cpp @@ -206,7 +206,7 @@ void StoreForwardModule::historyAdd(const meshtastic_MeshPacket &mp) this->packetHistory[this->packetHistoryTotalCount].hop_limit = mp.hop_limit; this->packetHistory[this->packetHistoryTotalCount].via_mqtt = mp.via_mqtt; this->packetHistory[this->packetHistoryTotalCount].transport_mechanism = mp.transport_mechanism; - memcpy(this->packetHistory[this->packetHistoryTotalCount].payload, p.payload.bytes, meshtastic_Constants_DATA_PAYLOAD_LEN); + memcpy(this->packetHistory[this->packetHistoryTotalCount].payload, p.payload.bytes, p.payload.size); this->packetHistoryTotalCount++; } diff --git a/src/motion/AccelerometerThread.h b/src/motion/AccelerometerThread.h old mode 100644 new mode 100755 diff --git a/src/motion/BMA423Sensor.cpp b/src/motion/BMA423Sensor.cpp old mode 100644 new mode 100755 diff --git a/src/motion/BMA423Sensor.h b/src/motion/BMA423Sensor.h old mode 100644 new mode 100755 diff --git a/src/motion/BMX160Sensor.cpp b/src/motion/BMX160Sensor.cpp old mode 100644 new mode 100755 diff --git a/src/motion/BMX160Sensor.h b/src/motion/BMX160Sensor.h old mode 100644 new mode 100755 diff --git a/src/motion/ICM20948Sensor.cpp b/src/motion/ICM20948Sensor.cpp old mode 100644 new mode 100755 diff --git a/src/motion/ICM20948Sensor.h b/src/motion/ICM20948Sensor.h old mode 100644 new mode 100755 diff --git a/src/motion/LIS3DHSensor.cpp b/src/motion/LIS3DHSensor.cpp old mode 100644 new mode 100755 diff --git a/src/motion/LIS3DHSensor.h b/src/motion/LIS3DHSensor.h old mode 100644 new mode 100755 diff --git a/src/motion/LSM6DS3Sensor.cpp b/src/motion/LSM6DS3Sensor.cpp old mode 100644 new mode 100755 diff --git a/src/motion/LSM6DS3Sensor.h b/src/motion/LSM6DS3Sensor.h old mode 100644 new mode 100755 diff --git a/src/motion/MPU6050Sensor.cpp b/src/motion/MPU6050Sensor.cpp old mode 100644 new mode 100755 diff --git a/src/motion/MPU6050Sensor.h b/src/motion/MPU6050Sensor.h old mode 100644 new mode 100755 diff --git a/src/motion/MotionSensor.cpp b/src/motion/MotionSensor.cpp old mode 100644 new mode 100755 diff --git a/src/motion/MotionSensor.h b/src/motion/MotionSensor.h old mode 100644 new mode 100755 diff --git a/src/motion/STK8XXXSensor.cpp b/src/motion/STK8XXXSensor.cpp old mode 100644 new mode 100755 diff --git a/src/motion/STK8XXXSensor.h b/src/motion/STK8XXXSensor.h old mode 100644 new mode 100755 diff --git a/src/nimble/NimbleBluetooth.cpp b/src/nimble/NimbleBluetooth.cpp index 3bb4ce817..5a4d150ae 100644 --- a/src/nimble/NimbleBluetooth.cpp +++ b/src/nimble/NimbleBluetooth.cpp @@ -323,7 +323,7 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread /** * Subclasses can use this as a hook to provide custom notifications for their transport (i.e. bluetooth notifies) */ - virtual void onNowHasData(uint32_t fromRadioNum) + virtual void onNowHasData(uint32_t fromRadioNum) override { PhoneAPI::onNowHasData(fromRadioNum); @@ -350,7 +350,7 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread } /// Check the current underlying physical link to see if the client is currently connected - virtual bool checkIsConnected() { return bleServer && bleServer->getConnectedCount() > 0; } + virtual bool checkIsConnected() override { return bleServer && bleServer->getConnectedCount() > 0; } void requestHighThroughputConnection(uint16_t conn_handle) { @@ -412,9 +412,9 @@ static uint8_t lastToRadio[MAX_TO_FROM_RADIO_SIZE]; class NimbleBluetoothToRadioCallback : public NimBLECharacteristicCallbacks { #ifdef NIMBLE_TWO - virtual void onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) + virtual void onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) override #else - virtual void onWrite(NimBLECharacteristic *pCharacteristic) + virtual void onWrite(NimBLECharacteristic *pCharacteristic) override #endif { @@ -464,9 +464,9 @@ class NimbleBluetoothToRadioCallback : public NimBLECharacteristicCallbacks class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks { #ifdef NIMBLE_TWO - virtual void onRead(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) + virtual void onRead(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) override #else - virtual void onRead(NimBLECharacteristic *pCharacteristic) + virtual void onRead(NimBLECharacteristic *pCharacteristic) override #endif { // CAUTION: This callback runs in the NimBLE task!!! Don't do anything except communicate with the main task's runOnce. @@ -582,9 +582,9 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks private: NimbleBluetooth *ble; - virtual uint32_t onPassKeyDisplay() + virtual uint32_t onPassKeyDisplay() override #else - virtual uint32_t onPassKeyRequest() + virtual uint32_t onPassKeyRequest() override #endif { uint32_t passkey = config.bluetooth.fixed_pin; @@ -635,9 +635,9 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks } #ifdef NIMBLE_TWO - virtual void onAuthenticationComplete(NimBLEConnInfo &connInfo) + virtual void onAuthenticationComplete(NimBLEConnInfo &connInfo) override #else - virtual void onAuthenticationComplete(ble_gap_conn_desc *desc) + virtual void onAuthenticationComplete(ble_gap_conn_desc *desc) override #endif { LOG_INFO("BLE authentication complete"); @@ -655,7 +655,7 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks } #ifdef NIMBLE_TWO - virtual void onConnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo) + virtual void onConnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo) override { LOG_INFO("BLE incoming connection %s", connInfo.getAddress().toString().c_str()); @@ -683,11 +683,11 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks #endif #ifdef NIMBLE_TWO - virtual void onDisconnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo, int reason) + virtual void onDisconnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo, int reason) override { LOG_INFO("BLE disconnect reason: %d", reason); #else - virtual void onDisconnect(NimBLEServer *pServer, ble_gap_conn_desc *desc) + virtual void onDisconnect(NimBLEServer *pServer, ble_gap_conn_desc *desc) override { LOG_INFO("BLE disconnect"); #endif @@ -989,11 +989,4 @@ void NimbleBluetooth::sendLog(const uint8_t *logMessage, size_t length) #endif } -void clearNVS() -{ - NimBLEDevice::deleteAllBonds(); -#ifdef ARCH_ESP32 - ESP.restart(); -#endif -} #endif diff --git a/src/nimble/NimbleBluetooth.h b/src/nimble/NimbleBluetooth.h index 458fa4a67..a7b14ff73 100644 --- a/src/nimble/NimbleBluetooth.h +++ b/src/nimble/NimbleBluetooth.h @@ -24,5 +24,4 @@ class NimbleBluetooth : BluetoothApi #endif }; -void setBluetoothEnable(bool enable); -void clearNVS(); \ No newline at end of file +void setBluetoothEnable(bool enable); \ No newline at end of file diff --git a/variants/esp32s3/m5stack_cardputer_adv/variant.cpp b/src/platform/extra_variants/m5stack_cardputer_adv/variant.cpp similarity index 97% rename from variants/esp32s3/m5stack_cardputer_adv/variant.cpp rename to src/platform/extra_variants/m5stack_cardputer_adv/variant.cpp index 2bbe8e2e3..7ec9dca80 100644 --- a/variants/esp32s3/m5stack_cardputer_adv/variant.cpp +++ b/src/platform/extra_variants/m5stack_cardputer_adv/variant.cpp @@ -1,6 +1,9 @@ -#include "AudioBoard.h" #include "configuration.h" +#ifdef M5STACK_CARDPUTER_ADV + +#include "AudioBoard.h" + DriverPins PinsAudioBoardES8311; AudioBoard board(AudioDriverES8311, PinsAudioBoardES8311); @@ -38,3 +41,5 @@ void lateInitVariant() es8311_write_reg(0x32, 0xBF); // DAC volume (0dB) es8311_write_reg(0x37, 0x08); // EQ bypass } + +#endif diff --git a/src/platform/extra_variants/t5s3_epaper/variant.cpp b/src/platform/extra_variants/t5s3_epaper/variant.cpp new file mode 100644 index 000000000..a829bb980 --- /dev/null +++ b/src/platform/extra_variants/t5s3_epaper/variant.cpp @@ -0,0 +1,709 @@ +#include "configuration.h" + +#ifdef T5_S3_EPAPER_PRO + +#include "Observer.h" +#include "TouchDrvGT911.hpp" +#include "Wire.h" +#include "buzz.h" +#include "concurrency/OSThread.h" +#include "input/InputBroker.h" +#include "input/TouchScreenImpl1.h" +#include "main.h" +#include "sleep.h" +#include + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS +#include "graphics/niche/InkHUD/InkHUD.h" +#include "graphics/niche/InkHUD/Persistence.h" +#include "graphics/niche/InkHUD/SystemApplet.h" + +// Bridges touch events from TouchScreenImpl1 directly into InkHUD, +// bypassing the InputBroker (which is excluded in InkHUD builds). +// Routing mirrors the mini-epaper-s3 two-way rocker pattern: +// - Nav left/right: prevApplet/nextApplet when idle, navUp/Down when a system applet has focus (e.g. menu) +// - Nav up/down: navUp/navDown always (menu scroll) +// - Tap/long-press: direct touch point dispatch (with fallback to short/long button semantics) +class TouchInkHUDBridge : public Observer +{ + int onNotify(const InputEvent *e) override + { + auto *inkhud = NicheGraphics::InkHUD::InkHUD::getInstance(); + + // Keep alignment in sync with the current rotation so that visual-frame gestures + // always pass through nav functions without remapping: (rotation + alignment) % 4 == 0. + inkhud->persistence->settings.joystick.alignment = (4 - inkhud->persistence->settings.rotation) % 4; + + // Check whether a system applet (e.g. menu) is currently handling input + bool systemHandlingInput = false; + for (NicheGraphics::InkHUD::SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleInput) { + systemHandlingInput = true; + break; + } + } + + switch (e->inputEvent) { + case INPUT_BROKER_USER_PRESS: + inkhud->touchTap(e->touchX, e->touchY); + break; + case INPUT_BROKER_SELECT: + inkhud->touchLongPress(e->touchX, e->touchY); + break; + case INPUT_BROKER_LEFT: + if (systemHandlingInput) + inkhud->touchNavUp(); + else + inkhud->prevApplet(); + break; + case INPUT_BROKER_RIGHT: + if (systemHandlingInput) + inkhud->touchNavDown(); + else + inkhud->nextApplet(); + break; + case INPUT_BROKER_UP: + inkhud->touchNavUp(); + break; + case INPUT_BROKER_DOWN: + inkhud->touchNavDown(); + break; + default: + break; + } + return 0; + } +}; + +static TouchInkHUDBridge touchBridge; +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +TouchDrvGT911 touch; + +namespace +{ +constexpr uint8_t BACKLIGHT_ON_LEVEL = HIGH; +constexpr uint8_t BACKLIGHT_OFF_LEVEL = LOW; +volatile bool backlightUserEnabled = true; +volatile bool backlightForcedByTimeout = false; +volatile bool backlightForcedBySleep = false; + +void applyBacklightState() +{ + const bool shouldOn = backlightUserEnabled && !backlightForcedByTimeout && !backlightForcedBySleep; + digitalWrite(BOARD_BL_EN, shouldOn ? BACKLIGHT_ON_LEVEL : BACKLIGHT_OFF_LEVEL); +} + +volatile bool touchInputEnabled = true; +volatile bool touchForcedByTimeout = false; +volatile bool touchControllerReady = false; +volatile bool touchLightSleepActive = false; +volatile bool touchNeedsWake = false; +volatile bool touchIndicatorRefreshPending = false; +volatile uint32_t touchResumeBlockUntilMs = 0; +volatile uint32_t touchStateEpoch = 1; +volatile bool homeCapButtonEventsEnabled = false; +#if HAS_SCREEN +uint32_t lastTouchIndicatorMs = 0; +#endif + +void showTouchIndicator(const char *text) +{ +#if HAS_SCREEN +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + // InkHUD builds render a dedicated bottom-edge "TOUCH OFF" overlay instead of popup banners. + (void)text; + return; +#else + // Keep repeated notifications low profile and non-spammy. + if ((millis() - lastTouchIndicatorMs) < 500) { + return; + } + lastTouchIndicatorMs = millis(); + if (screen) { + screen->showSimpleBanner(text, 1400); + } +#endif +#else + (void)text; +#endif +} + +#if defined(BOARD_PCA9535_ADDR) && defined(BOARD_PCA9535_BUTTON_MASK) +bool readPca9535Port1(uint8_t *value) +{ + if (!value) { + return false; + } + + Wire.beginTransmission(BOARD_PCA9535_ADDR); + Wire.write((uint8_t)0x01); // input port 1 + if (Wire.endTransmission(false) != 0) { + return false; + } + if (Wire.requestFrom((uint8_t)BOARD_PCA9535_ADDR, (uint8_t)1) != 1) { + return false; + } + + *value = Wire.read(); + return true; +} + +bool isPca9535SideKeyPressed() +{ + uint8_t port1 = 0xFF; + if (!readPca9535Port1(&port1)) { + return false; + } + + return (port1 & BOARD_PCA9535_BUTTON_MASK) == 0; +} + +class SideKeyInterruptThread : public concurrency::OSThread +{ + public: + SideKeyInterruptThread() : concurrency::OSThread("t5s3SideKeyInt", SAMPLE_MS) + { + // Do not run unless an edge arrives. + OSThread::disable(); + instance = this; +#ifdef ARCH_ESP32 + lsObserver.observe(¬ifyLightSleep); + lsEndObserver.observe(¬ifyLightSleepEnd); +#endif + } + + void begin() + { + pinMode(BOARD_PCA9535_INT, INPUT_PULLUP); + attachInterrupt(BOARD_PCA9535_INT, SideKeyInterruptThread::isr, FALLING); + } + + protected: + int32_t runOnce() override + { + const uint32_t now = millis(); + + if (now < touchResumeBlockUntilMs) { + resetStateAndStop(); + return OSThread::disable(); + } + + if (touchLightSleepActive) { + resetStateAndStop(); + return OSThread::disable(); + } + + // Ignore side-key handling while BOOT/user button is held. + if (digitalRead(BUTTON_PIN) == LOW) { + resetStateAndStop(); + return OSThread::disable(); + } + + switch (state) { + case State::IRQ_PENDING: + // Initial debounce after expander interrupt edge. + if ((uint32_t)(now - irqAtMs) < DEBOUNCE_MS) { + return SAMPLE_MS; + } + + if (isPca9535SideKeyPressed()) { + state = State::PRESSED; + pressStartMs = now; + return SAMPLE_MS; + } + + // Spurious/cleared edge. + resetStateAndStop(); + return OSThread::disable(); + + case State::PRESSED: { + if (isPca9535SideKeyPressed()) { + // Fire long-press action as soon as threshold is reached, without waiting for release. + if (!longPressFired && (uint32_t)(now - pressStartMs) >= LONG_PRESS_MIN_MS && + (uint32_t)(now - lastActionMs) >= ACTION_COOLDOWN_MS) { + t5BacklightToggleUser(); + longPressFired = true; + lastActionMs = now; + } + return SAMPLE_MS; + } + + // Released: if long-press already fired, do nothing. Otherwise classify short press. + const uint32_t heldMs = now - pressStartMs; + if (!longPressFired && heldMs >= SHORT_PRESS_MIN_MS && (uint32_t)(now - lastActionMs) >= ACTION_COOLDOWN_MS) { + // If timeout forced touch/backlight off, short-press acts as a wake action first. + if (t5TouchIsForcedByTimeout()) { + t5TouchHandleUserInput(); + t5BacklightHandleUserInput(); + } else { + toggleTouchInputEnabled(); + } + lastActionMs = now; + } + + resetStateAndStop(); + return OSThread::disable(); + } + + case State::REST: + default: + return OSThread::disable(); + } + } + + private: + enum class State : uint8_t { + REST, + IRQ_PENDING, + PRESSED, + }; + + static constexpr uint32_t SAMPLE_MS = 15; + static constexpr uint32_t DEBOUNCE_MS = 25; + static constexpr uint32_t SHORT_PRESS_MIN_MS = 30; + static constexpr uint32_t LONG_PRESS_MIN_MS = 450; + static constexpr uint32_t ACTION_COOLDOWN_MS = 180; + + static SideKeyInterruptThread *instance; + + static void isr() + { + if (instance) { + instance->onInterruptEdge(); + } + } + + void onInterruptEdge() + { + if (touchLightSleepActive) { + return; + } + const uint32_t now = millis(); + if (now < touchResumeBlockUntilMs) { + return; + } + if (state != State::REST) { + return; + } + + state = State::IRQ_PENDING; + irqAtMs = millis(); + startThread(); + } + + void startThread() + { + if (!OSThread::enabled) { + OSThread::setIntervalFromNow(0); + OSThread::enabled = true; + runASAP = true; + } + } + + void resetStateAndStop() + { + state = State::REST; + longPressFired = false; + if (OSThread::enabled) { + OSThread::disable(); + } + } + +#ifdef ARCH_ESP32 + int onLightSleep(void *) + { + detachInterrupt(BOARD_PCA9535_INT); + // Clear any latched PCA9535 interrupt before enabling GPIO wake. + // If INT is left asserted low, light sleep exits immediately. + uint8_t ignored = 0xFF; + (void)readPca9535Port1(&ignored); + resetStateAndStop(); + return 0; + } + + int onLightSleepEnd(esp_sleep_wakeup_cause_t cause) + { + (void)cause; + // Consume any pending interrupt source before reattaching ISR. + uint8_t ignored = 0xFF; + (void)readPca9535Port1(&ignored); + pinMode(BOARD_PCA9535_INT, INPUT_PULLUP); + attachInterrupt(BOARD_PCA9535_INT, SideKeyInterruptThread::isr, FALLING); + + return 0; + } + + CallbackObserver lsObserver{this, &SideKeyInterruptThread::onLightSleep}; + CallbackObserver lsEndObserver{this, + &SideKeyInterruptThread::onLightSleepEnd}; +#endif + + volatile State state = State::REST; + volatile uint32_t irqAtMs = 0; + uint32_t pressStartMs = 0; + bool longPressFired = false; + uint32_t lastActionMs = 0; +}; + +SideKeyInterruptThread *SideKeyInterruptThread::instance = nullptr; +SideKeyInterruptThread *sideKeyThread = nullptr; +#endif + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS +void refreshTouchIndicatorInInkHUD(bool async = true) +{ + auto *inkhud = NicheGraphics::InkHUD::InkHUD::getInstance(); + NicheGraphics::InkHUD::SystemApplet *touchStatus = nullptr; + for (auto *sa : inkhud->systemApplets) { + if (sa && sa->name && strcmp(sa->name, "TouchStatus") == 0) { + touchStatus = sa; + break; + } + } + + if (touchStatus) { + if (inkhud->isTouchEnabled()) + touchStatus->sendToBackground(); + else + touchStatus->bringToForeground(); + } + + // Re-render all applets so touch-status visibility changes are immediately reflected. + inkhud->forceUpdate(NicheGraphics::Drivers::EInk::UpdateTypes::FAST, true, async); +} +#endif + +} // namespace + +void t5BacklightSetUserEnabled(bool enabled) +{ + backlightUserEnabled = enabled; + if (enabled) { + // Manual ON should release auto-off gates. + backlightForcedByTimeout = false; + backlightForcedBySleep = false; + } + applyBacklightState(); +} + +bool t5BacklightIsUserEnabled() +{ + return backlightUserEnabled; +} + +void t5BacklightToggleUser() +{ + t5BacklightSetUserEnabled(!backlightUserEnabled); +} + +void t5BacklightSetForcedByTimeout(bool forced) +{ + backlightForcedByTimeout = forced; + applyBacklightState(); +} + +void t5BacklightSetForcedBySleep(bool forced) +{ + backlightForcedBySleep = forced; + applyBacklightState(); +} + +void t5BacklightHandleUserInput() +{ + // Screen-timeout should be lifted by direct user interaction. + backlightForcedByTimeout = false; + applyBacklightState(); +} + +void t5TouchSetForcedByTimeout(bool forced) +{ + touchForcedByTimeout = forced; + touchStateEpoch++; + touchIndicatorRefreshPending = true; + + if (forced) { + // While timeout-forced, keep controller asleep to avoid stale IRQ chatter. + touchNeedsWake = false; + if (touchControllerReady && !touchLightSleepActive) { + touch.sleep(); + } + } else if (touchInputEnabled && touchControllerReady && !touchLightSleepActive) { + // Defer wake until readTouch() so I2C settles post-state transition. + touchNeedsWake = true; + } + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + if (!touchLightSleepActive) { + refreshTouchIndicatorInInkHUD(); + touchIndicatorRefreshPending = false; + } +#endif +} + +bool t5TouchIsForcedByTimeout() +{ + return touchForcedByTimeout; +} + +void t5TouchHandleUserInput() +{ + t5TouchSetForcedByTimeout(false); +} + +void t5SetHomeCapButtonEventsEnabled(bool enabled) +{ + homeCapButtonEventsEnabled = enabled; +} + +bool isTouchInputEnabled() +{ + return touchInputEnabled && !touchForcedByTimeout && !touchLightSleepActive; +} + +void setTouchInputEnabled(bool enabled, bool showIndicator) +{ + if (touchInputEnabled == enabled) { + LOG_DEBUG("touchscreen1: setTouchInputEnabled no-op en=%d", enabled); + return; + } + + LOG_DEBUG("touchscreen1: setTouchInputEnabled %d -> %d (showIndicator=%d)", touchInputEnabled, enabled, showIndicator); + touchInputEnabled = enabled; + touchStateEpoch++; + + if (enabled) { + touchNeedsWake = touchControllerReady; + if (touchControllerReady && !touchLightSleepActive) { + LOG_DEBUG("touchscreen1: wakeup() on enable"); + touch.wakeup(); + touchNeedsWake = false; + } + } else { + touchNeedsWake = false; + if (touchControllerReady && !touchLightSleepActive) { + LOG_DEBUG("touchscreen1: sleep() on disable"); + touch.sleep(); + } + if (showIndicator) { + showTouchIndicator("Touch OFF"); + touchIndicatorRefreshPending = true; + } + } + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + if (showIndicator && !touchLightSleepActive) { + refreshTouchIndicatorInInkHUD(); + touchIndicatorRefreshPending = false; + } +#endif +} + +void toggleTouchInputEnabled() +{ + setTouchInputEnabled(!touchInputEnabled, true); +} + +// Commands the GT911 into standby before the Wire bus is torn down. +// notifyDeepSleep fires before Wire.end() in doDeepSleep(), so I2C is still available here. +struct TouchDeepSleepObserver { + int onDeepSleep(void *) + { + touch.sleep(); + return 0; + } + CallbackObserver observer{this, &TouchDeepSleepObserver::onDeepSleep}; +} static touchDeepSleepObserver; + +#ifdef ARCH_ESP32 +struct TouchLightSleepObserver { + int onLightSleep(void *) + { + touchLightSleepActive = true; +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + // Render touch-off overlay before sleeping so user sees touch is unavailable. + touchIndicatorRefreshPending = true; + refreshTouchIndicatorInInkHUD(false); + touchIndicatorRefreshPending = false; +#endif + return 0; + } + + CallbackObserver observer{this, &TouchLightSleepObserver::onLightSleep}; +} static touchLightSleepObserver; + +struct TouchLightSleepEndObserver { + int onLightSleepEnd(esp_sleep_wakeup_cause_t cause) + { + (void)cause; + touchLightSleepActive = false; + + if (!touchControllerReady) { + return 0; + } + + if (touchInputEnabled && !touchForcedByTimeout) { + touchNeedsWake = true; + } else { + touchNeedsWake = false; + } + + touchStateEpoch++; + touchResumeBlockUntilMs = millis() + 150; + touchIndicatorRefreshPending = !isTouchInputEnabled(); +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + // Clear sleep-time touch overlay after wake. + touchIndicatorRefreshPending = true; + refreshTouchIndicatorInInkHUD(); + touchIndicatorRefreshPending = false; +#endif + return 0; + } + + CallbackObserver observer{this, + &TouchLightSleepEndObserver::onLightSleepEnd}; +} static touchLightSleepEndObserver; +#endif + +bool readTouch(int16_t *x, int16_t *y) +{ +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + static uint32_t suppressUntilMs = 0; + static uint32_t seenTouchStateEpoch = 0; + + // Reset transient gesture helpers whenever touch mode changes. + if (seenTouchStateEpoch != touchStateEpoch) { + seenTouchStateEpoch = touchStateEpoch; + suppressUntilMs = 0; + } + + // Let buses and peripherals settle briefly after light-sleep wake. + if (millis() < touchResumeBlockUntilMs) { + return false; + } + + if (touchIndicatorRefreshPending) { + refreshTouchIndicatorInInkHUD(); + touchIndicatorRefreshPending = false; + } + + if (!isTouchInputEnabled()) { + return false; + } + + if (touchNeedsWake && touchControllerReady) { + LOG_DEBUG("touchscreen1: wakeup() on deferred resume"); + touch.wakeup(); + touchNeedsWake = false; + suppressUntilMs = millis() + 60; + return false; + } + + // After a recovery pulse, emit a brief "released" window so gesture state can reset. + if (suppressUntilMs != 0 && millis() < suppressUntilMs) { + return false; + } +#endif + + if (!digitalRead(GT911_PIN_INT)) { + int16_t raw_x; + int16_t raw_y; + if (touch.getPoint(&raw_x, &raw_y)) { +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + // Transform raw GT911 axes to visual-frame coordinates for the current display rotation. + // rotation=3 is the physical identity (device's default orientation). + switch (NicheGraphics::InkHUD::InkHUD::getInstance()->persistence->settings.rotation) { + default: + case 3: + *x = raw_x; + *y = raw_y; + break; // identity + case 2: + *x = (EPD_WIDTH - 1) - raw_y; + *y = raw_x; + break; // 90° CW tilt + case 1: + *x = (EPD_HEIGHT - 1) - raw_x; + *y = (EPD_WIDTH - 1) - raw_y; + break; // 180° flip + case 0: + *x = raw_y; + *y = (EPD_HEIGHT - 1) - raw_x; + break; // 90° CCW tilt + } +#else + *x = raw_x; + *y = raw_y; +#endif + LOG_DEBUG("touched(%d/%d)", *x, *y); + return true; + } + } + + return false; +} + +void variant_shutdown() +{ + // Ensure backlight is off during deep sleep. + t5BacklightSetForcedBySleep(true); +} + +void lateInitVariant() +{ + touch.setPins(GT911_PIN_RST, GT911_PIN_INT); + if (touch.begin(Wire, GT911_SLAVE_ADDRESS_H, GT911_PIN_SDA, GT911_PIN_SCL)) { + // Match LilyGO sample behavior: GT911 center/home capacitive key callback. + touch.setHomeButtonCallback( + [](void *user_data) { +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + if (!homeCapButtonEventsEnabled) { + return; + } + + static uint32_t lastHomeMs = 0; + const uint32_t now = millis(); + if ((uint32_t)(now - lastHomeMs) < 220) { + return; // debounce repeated key reports while still touched + } + lastHomeMs = now; + + auto *inkhud = NicheGraphics::InkHUD::InkHUD::getInstance(); + if (inkhud) { + // Route through InkHUD EXIT/HOME path (menu close, etc). + inkhud->exitShort(); + } +#else + (void)user_data; +#endif + }, + nullptr); + touchControllerReady = true; + touchInputEnabled = true; + touchForcedByTimeout = false; + touchLightSleepActive = false; + touchStateEpoch++; + touchDeepSleepObserver.observer.observe(¬ifyDeepSleep); +#ifdef ARCH_ESP32 + touchLightSleepObserver.observer.observe(¬ifyLightSleep); + touchLightSleepEndObserver.observer.observe(¬ifyLightSleepEnd); +#endif + touchScreenImpl1 = new TouchScreenImpl1(EPD_WIDTH, EPD_HEIGHT, readTouch); + touchScreenImpl1->init(); +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + touchBridge.observe(touchScreenImpl1); +#endif + } else { + touchControllerReady = false; + LOG_ERROR("Failed to find touch controller!"); + } + +#if defined(BOARD_PCA9535_ADDR) && defined(BOARD_PCA9535_BUTTON_MASK) + // Start side-key interrupt handling after touch init is complete. + if (!sideKeyThread) { + sideKeyThread = new SideKeyInterruptThread(); + sideKeyThread->begin(); + } +#endif +} +#endif diff --git a/src/platform/nrf52/NRF52Bluetooth.cpp b/src/platform/nrf52/NRF52Bluetooth.cpp index 307e35b0c..52e45cccc 100644 --- a/src/platform/nrf52/NRF52Bluetooth.cpp +++ b/src/platform/nrf52/NRF52Bluetooth.cpp @@ -1,6 +1,7 @@ #include "NRF52Bluetooth.h" #include "BLEDfuSecure.h" #include "BluetoothCommon.h" +#include "HardwareRNG.h" #include "PowerFSM.h" #include "configuration.h" #include "main.h" @@ -272,9 +273,13 @@ void NRF52Bluetooth::setup() Bluefruit.setTxPower(NRF52_BLE_TX_POWER); #endif if (config.bluetooth.mode != meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN) { - configuredPasskey = config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_FIXED_PIN - ? config.bluetooth.fixed_pin - : random(100000, 999999); + if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_FIXED_PIN) { + configuredPasskey = config.bluetooth.fixed_pin; + } else { + uint32_t hwrand = 0; + HardwareRNG::fill(reinterpret_cast(&hwrand), sizeof(hwrand)); + configuredPasskey = hwrand % 900000u + 100000u; + } auto pinString = std::to_string(configuredPasskey); LOG_INFO("Bluetooth pin set to '%i'", configuredPasskey); Bluefruit.Security.setPIN(pinString.c_str()); diff --git a/src/power.h b/src/power.h index d46eaadd2..4b5ef609d 100644 --- a/src/power.h +++ b/src/power.h @@ -86,7 +86,7 @@ extern RAK9154Sensor rak9154Sensor; extern XPowersLibInterface *PMU; #endif -class Power : private concurrency::OSThread +class Power : public concurrency::OSThread { public: @@ -100,6 +100,15 @@ class Power : private concurrency::OSThread virtual int32_t runOnce() override; void setStatusHandler(meshtastic::PowerStatus *handler) { statusHandler = handler; } const uint16_t OCV[11] = {OCV_ARRAY}; + bool isLowBattery() { return low_voltage_counter >= 10; }; + +#ifdef ARCH_ESP32 + int beforeLightSleep(void *unused); + int afterLightSleep(esp_sleep_wakeup_cause_t cause); +#endif + + void attachPowerInterrupts(); + void detachPowerInterrupts(); protected: meshtastic::PowerStatus *statusHandler; @@ -125,6 +134,14 @@ class Power : private concurrency::OSThread // open circuit voltage lookup table uint8_t low_voltage_counter; uint32_t lastLogTime = 0; + +#ifdef ARCH_ESP32 + // Get notified when lightsleep begins and ends + CallbackObserver lsObserver = CallbackObserver(this, &Power::beforeLightSleep); + CallbackObserver lsEndObserver = + CallbackObserver(this, &Power::afterLightSleep); +#endif + #ifdef DEBUG_HEAP uint32_t lastheap; #endif diff --git a/src/sleep.cpp b/src/sleep.cpp index 2af6222f4..00533f5fa 100644 --- a/src/sleep.cpp +++ b/src/sleep.cpp @@ -79,7 +79,7 @@ RTC_DATA_ATTR int bootCount = 0; */ void setCPUFast(bool on) { -#if defined(ARCH_ESP32) && HAS_WIFI && !HAS_TFT +#if defined(ARCH_ESP32) && HAS_WIFI && !HAS_TFT && !defined(T_LORA_PAGER) && !defined(T_DECK) if (isWifiAvailable()) { /* @@ -316,9 +316,14 @@ void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveN #ifdef HAS_PPM if (PPM) { - LOG_INFO("PMM shutdown"); - console->flush(); - PPM->shutdown(); + // BQ25896 PMIC shutdown is a hard power-off state. + // Only use it for "sleep forever" / explicit shutdown, because timed deep sleep + // must remain wakeable by RTC timer. + if (msecToWake == portMAX_DELAY) { + LOG_INFO("PPM shutdown"); + console->flush(); + PPM->shutdown(); + } } #endif @@ -425,6 +430,10 @@ esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more r #ifdef KB_INT gpio_wakeup_enable((gpio_num_t)KB_INT, GPIO_INTR_LOW_LEVEL); #endif +#ifdef BOARD_PCA9535_INT + // Side-key interrupt line from PCA9535 expander (active low). + gpio_wakeup_enable((gpio_num_t)BOARD_PCA9535_INT, GPIO_INTR_LOW_LEVEL); +#endif #ifdef BUTTON_PIN gpio_num_t pin = (gpio_num_t)(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN); gpio_wakeup_enable(pin, GPIO_INTR_LOW_LEVEL); @@ -472,6 +481,9 @@ esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more r #ifdef KB_INT gpio_wakeup_disable((gpio_num_t)KB_INT); #endif +#ifdef BOARD_PCA9535_INT + gpio_wakeup_disable((gpio_num_t)BOARD_PCA9535_INT); +#endif #ifdef BUTTON_PIN // Disable wake-on-button interrupt. Re-attach normal button-interrupts gpio_wakeup_disable(pin); @@ -497,13 +509,12 @@ esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more r esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause(); notifyLightSleepEnd.notifyObservers(cause); // Button interrupts are reattached here -#ifdef BUTTON_PIN if (cause == ESP_SLEEP_WAKEUP_GPIO) { - LOG_INFO("Exit light sleep gpio: btn=%d", - !digitalRead(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN)); - } else -#endif - { + LOG_INFO("Exit light sleep gpio"); + // If we woke because of a GPIO, it's possible power needs to run to handle. + power->setIntervalFromNow(0); + runASAP = true; + } else { LOG_INFO("Exit light sleep cause: %d", cause); } diff --git a/test/test_packet_history/test_main.cpp b/test/test_packet_history/test_main.cpp new file mode 100644 index 000000000..2453956c5 --- /dev/null +++ b/test/test_packet_history/test_main.cpp @@ -0,0 +1,834 @@ +/* + * Unit tests for PacketHistory — the packet deduplication engine + * used by the mesh routing stack. + * + * PacketHistory maintains a fixed-size array of PacketRecords with an + * optional hash table for O(1) lookup. It tracks which nodes relayed + * each packet, supports LRU-style eviction, and detects fallback-to- + * flooding and hop-limit upgrades. + */ + +#include "PacketHistory.h" + +#include "TestUtil.h" +#include + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- +static constexpr uint32_t OUR_NODE_NUM = 0xDEAD1234; +static constexpr uint8_t OUR_RELAY_ID = 0x34; // getLastByteOfNodeNum(OUR_NODE_NUM) +static constexpr uint32_t SMALL_CAPACITY = 8; + +// --------------------------------------------------------------------------- +// Per-test state +// --------------------------------------------------------------------------- +static PacketHistory *ph = nullptr; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- +static meshtastic_MeshPacket makePacket(uint32_t from, uint32_t id, uint8_t hop_limit = 3, + uint8_t next_hop = NO_NEXT_HOP_PREFERENCE, uint8_t relay_node = 0) +{ + meshtastic_MeshPacket p = meshtastic_MeshPacket_init_zero; + p.from = from; + p.id = id; + p.hop_limit = hop_limit; + p.next_hop = next_hop; + p.relay_node = relay_node; + return p; +} + +// --------------------------------------------------------------------------- +// setUp / tearDown — called before and after every test +// --------------------------------------------------------------------------- +void setUp(void) +{ + myNodeInfo.my_node_num = OUR_NODE_NUM; + ph = new PacketHistory(SMALL_CAPACITY); +} + +void tearDown(void) +{ + delete ph; + ph = nullptr; +} + +// =========================================================================== +// Group 1 — Initialization +// =========================================================================== + +void test_init_valid_size(void) +{ + PacketHistory h(8); + TEST_ASSERT_TRUE(h.initOk()); +} + +void test_init_minimum_size(void) +{ + PacketHistory h(4); + TEST_ASSERT_TRUE(h.initOk()); +} + +void test_init_too_small_falls_back(void) +{ + // Sizes < 4 or > PACKETHISTORY_MAX are clamped to PACKETHISTORY_MAX inside the constructor + PacketHistory h(2); + TEST_ASSERT_TRUE(h.initOk()); +} + +// =========================================================================== +// Group 2 — Basic Deduplication +// =========================================================================== + +void test_first_packet_not_seen(void) +{ + auto p = makePacket(0x1111, 100); + TEST_ASSERT_FALSE(ph->wasSeenRecently(&p)); +} + +void test_same_packet_seen_twice(void) +{ + auto p = makePacket(0x1111, 100); + TEST_ASSERT_FALSE(ph->wasSeenRecently(&p)); // first time + TEST_ASSERT_TRUE(ph->wasSeenRecently(&p)); // duplicate +} + +void test_different_id_not_confused(void) +{ + auto p1 = makePacket(0x1111, 100); + auto p2 = makePacket(0x1111, 200); + ph->wasSeenRecently(&p1); + TEST_ASSERT_FALSE(ph->wasSeenRecently(&p2)); +} + +void test_different_sender_not_confused(void) +{ + auto p1 = makePacket(0x1111, 100); + auto p2 = makePacket(0x2222, 100); + ph->wasSeenRecently(&p1); + TEST_ASSERT_FALSE(ph->wasSeenRecently(&p2)); +} + +void test_withUpdate_false_no_insert(void) +{ + auto p = makePacket(0x1111, 100); + // First call with withUpdate=false: should not store + TEST_ASSERT_FALSE(ph->wasSeenRecently(&p, /*withUpdate=*/false)); + // Second call with withUpdate=true: still not found because first didn't store + TEST_ASSERT_FALSE(ph->wasSeenRecently(&p, /*withUpdate=*/true)); +} + +void test_withUpdate_true_inserts(void) +{ + auto p = makePacket(0x1111, 100); + TEST_ASSERT_FALSE(ph->wasSeenRecently(&p, /*withUpdate=*/true)); + TEST_ASSERT_TRUE(ph->wasSeenRecently(&p, /*withUpdate=*/false)); // found without inserting again +} + +// =========================================================================== +// Group 3 — LRU Eviction +// =========================================================================== + +void test_fill_capacity_all_found(void) +{ + for (uint32_t i = 1; i <= SMALL_CAPACITY; i++) { + auto p = makePacket(0xAAAA, i); + ph->wasSeenRecently(&p); + } + // All 8 should be found + for (uint32_t i = 1; i <= SMALL_CAPACITY; i++) { + auto p = makePacket(0xAAAA, i); + TEST_ASSERT_TRUE(ph->wasSeenRecently(&p, false)); + } +} + +void test_eviction_oldest_replaced(void) +{ + // Fill all 8 slots + for (uint32_t i = 1; i <= SMALL_CAPACITY; i++) { + auto p = makePacket(0xAAAA, i); + ph->wasSeenRecently(&p); + } + + // Advance time so the eviction logic can distinguish "oldest" from "newest". + // insert() uses (now_millis - rxTimeMsec) > OldtrxTimeMsec with strict >, so + // entries with identical timestamps all have age 0 and none gets selected. + delay(1); + + // Insert a 9th packet — should evict the oldest + auto p9 = makePacket(0xAAAA, 9); + ph->wasSeenRecently(&p9); + + // The 9th should be found + TEST_ASSERT_TRUE(ph->wasSeenRecently(&p9, false)); + + // At least one of the originals should have been evicted + int evicted = 0; + for (uint32_t i = 1; i <= SMALL_CAPACITY; i++) { + auto p = makePacket(0xAAAA, i); + if (!ph->wasSeenRecently(&p, false)) + evicted++; + } + TEST_ASSERT_TRUE(evicted > 0); +} + +void test_matching_slot_reused(void) +{ + // Insert packet, then re-insert same (sender, id) — should reuse slot, not evict others + auto p1 = makePacket(0xAAAA, 1); + auto p2 = makePacket(0xBBBB, 2); + ph->wasSeenRecently(&p1); + ph->wasSeenRecently(&p2); + + // Re-observe p1 (triggers merge path) + ph->wasSeenRecently(&p1); + + // Both should still be present + TEST_ASSERT_TRUE(ph->wasSeenRecently(&p1, false)); + TEST_ASSERT_TRUE(ph->wasSeenRecently(&p2, false)); +} + +void test_free_slot_preferred(void) +{ + // Insert 4 packets into capacity-8 history — next insert should use a free slot, not evict + for (uint32_t i = 1; i <= 4; i++) { + auto p = makePacket(0xAAAA, i); + ph->wasSeenRecently(&p); + } + auto p5 = makePacket(0xAAAA, 5); + ph->wasSeenRecently(&p5); + + // All 5 should be present (no eviction needed) + for (uint32_t i = 1; i <= 5; i++) { + auto p = makePacket(0xAAAA, i); + TEST_ASSERT_TRUE(ph->wasSeenRecently(&p, false)); + } +} + +void test_evict_all_old_packets(void) +{ + // Fill with packets 1..8 + for (uint32_t i = 1; i <= SMALL_CAPACITY; i++) { + auto p = makePacket(0xAAAA, i); + ph->wasSeenRecently(&p); + } + + // Advance time so the replacement batch can evict the originals + delay(1); + + // Replace all with packets 101..108 + for (uint32_t i = 101; i <= 100 + SMALL_CAPACITY; i++) { + auto p = makePacket(0xBBBB, i); + ph->wasSeenRecently(&p); + } + // None of the originals should be found + for (uint32_t i = 1; i <= SMALL_CAPACITY; i++) { + auto p = makePacket(0xAAAA, i); + TEST_ASSERT_FALSE(ph->wasSeenRecently(&p, false)); + } + // All new ones should be found + for (uint32_t i = 101; i <= 100 + SMALL_CAPACITY; i++) { + auto p = makePacket(0xBBBB, i); + TEST_ASSERT_TRUE(ph->wasSeenRecently(&p, false)); + } +} + +// =========================================================================== +// Group 4 — Relayer Tracking +// =========================================================================== + +void test_wasRelayer_true(void) +{ + // Non-us relay_nodes only enter relayed_by[] through the "heard-back" merge path: + // we must have relayed first, then observe the packet return at hop_limit-1. + auto p1 = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID); + ph->wasSeenRecently(&p1); + + // Heard-back from 0xCC at hop_limit=2 (ourTxHopLimit-1) triggers the merge + auto p2 = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, 0xCC); + ph->wasSeenRecently(&p2); + + TEST_ASSERT_TRUE(ph->wasRelayer(0xCC, 100, 0x1111)); +} + +void test_wasRelayer_false(void) +{ + auto p = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, 0xAA); + ph->wasSeenRecently(&p); + // 0xCC was never a relayer + TEST_ASSERT_FALSE(ph->wasRelayer(0xCC, 100, 0x1111)); +} + +void test_wasRelayer_zero_returns_false(void) +{ + auto p = makePacket(0x1111, 100); + ph->wasSeenRecently(&p); + TEST_ASSERT_FALSE(ph->wasRelayer(0, 100, 0x1111)); +} + +void test_wasRelayer_not_found(void) +{ + // Packet not in history at all + TEST_ASSERT_FALSE(ph->wasRelayer(0xAA, 999, 0x9999)); +} + +void test_wasRelayer_wasSole_true(void) +{ + // relay_node = ourRelayID → relayed_by[0] = ourRelayID + auto p = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID); + ph->wasSeenRecently(&p); + + bool wasSole = false; + bool result = ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111, &wasSole); + TEST_ASSERT_TRUE(result); + TEST_ASSERT_TRUE(wasSole); +} + +void test_wasRelayer_wasSole_false(void) +{ + // First observation: we relay + auto p1 = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID); + ph->wasSeenRecently(&p1); + + // Second observation: different relayer adds to record + auto p2 = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, 0xBB); + ph->wasSeenRecently(&p2); + + bool wasSole = true; + bool result = ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111, &wasSole); + TEST_ASSERT_TRUE(result); + TEST_ASSERT_FALSE(wasSole); +} + +void test_wasRelayer_all_six_slots(void) +{ + // First observation: we relay with hop_limit=3 (fills slot 0, ourTxHopLimit=3) + auto p = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID); + ph->wasSeenRecently(&p); + + // Each heard-back must satisfy: hop_limit == ourTxHopLimit OR ourTxHopLimit-1. + // Using hop_limit=2 (ourTxHopLimit-1) for all, which triggers the heard-back + // merge path each time. Each new relay_node pushes to slot 0 and shifts existing + // relayers right, eventually filling all NUM_RELAYERS(6) slots. + uint8_t relayers[] = {0x11, 0x22, 0x33, 0x44, 0x55}; + for (int i = 0; i < 5; i++) { + auto pn = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, relayers[i]); + ph->wasSeenRecently(&pn); + } + + // All 6 should be detected + TEST_ASSERT_TRUE(ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111)); + for (int i = 0; i < 5; i++) { + TEST_ASSERT_TRUE(ph->wasRelayer(relayers[i], 100, 0x1111)); + } +} + +// =========================================================================== +// Group 5 — removeRelayer +// =========================================================================== + +void test_removeRelayer_removes(void) +{ + auto p1 = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID); + ph->wasSeenRecently(&p1); + TEST_ASSERT_TRUE(ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111)); + + ph->removeRelayer(OUR_RELAY_ID, 100, 0x1111); + TEST_ASSERT_FALSE(ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111)); +} + +void test_removeRelayer_compacts(void) +{ + // We relay first + auto p1 = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID); + ph->wasSeenRecently(&p1); + // Second relayer + auto p2 = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, 0xBB); + ph->wasSeenRecently(&p2); + + // Remove us, 0xBB should still be found + ph->removeRelayer(OUR_RELAY_ID, 100, 0x1111); + TEST_ASSERT_FALSE(ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111)); + TEST_ASSERT_TRUE(ph->wasRelayer(0xBB, 100, 0x1111)); +} + +void test_removeRelayer_nonexistent_safe(void) +{ + auto p = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID); + ph->wasSeenRecently(&p); + // Removing a relayer that doesn't exist should not crash + ph->removeRelayer(0xFF, 100, 0x1111); + // Original should still be there + TEST_ASSERT_TRUE(ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111)); +} + +void test_removeRelayer_packet_not_found_safe(void) +{ + // Packet not in history — should not crash + ph->removeRelayer(0xAA, 999, 0x9999); +} + +// =========================================================================== +// Group 6 — checkRelayers +// =========================================================================== + +void test_checkRelayers_both_found(void) +{ + // We relay first + auto p1 = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID); + ph->wasSeenRecently(&p1); + // Second relayer + auto p2 = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, 0xBB); + ph->wasSeenRecently(&p2); + + bool r1 = false, r2 = false; + ph->checkRelayers(OUR_RELAY_ID, 0xBB, 100, 0x1111, &r1, &r2); + TEST_ASSERT_TRUE(r1); + TEST_ASSERT_TRUE(r2); +} + +void test_checkRelayers_one_found(void) +{ + auto p = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID); + ph->wasSeenRecently(&p); + + bool r1 = false, r2 = false; + ph->checkRelayers(OUR_RELAY_ID, 0xCC, 100, 0x1111, &r1, &r2); + TEST_ASSERT_TRUE(r1); + TEST_ASSERT_FALSE(r2); +} + +void test_checkRelayers_r2WasSole(void) +{ + auto p = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID); + ph->wasSeenRecently(&p); + + bool r1 = false, r2 = false, r2Sole = false; + // relayer1=0xCC (not found), relayer2=OUR_RELAY_ID (sole relayer) + ph->checkRelayers(0xCC, OUR_RELAY_ID, 100, 0x1111, &r1, &r2, &r2Sole); + TEST_ASSERT_FALSE(r1); + TEST_ASSERT_TRUE(r2); + TEST_ASSERT_TRUE(r2Sole); +} + +// =========================================================================== +// Group 7 — wasSeenRecently Merge Logic +// =========================================================================== + +void test_merge_preserves_original_next_hop(void) +{ + // First observation with next_hop=0x55 + auto p1 = makePacket(0x1111, 100, 3, 0x55, 0xAA); + ph->wasSeenRecently(&p1); + + // Re-observation with different next_hop + auto p2 = makePacket(0x1111, 100, 2, 0x77, 0xBB); + ph->wasSeenRecently(&p2); + + // The stored next_hop should still be 0x55 (the original) + // We verify via weWereNextHop: if we set original next_hop to ourRelayID, it should detect it + auto p3 = makePacket(0x1111, 200, 3, OUR_RELAY_ID, 0xAA); + ph->wasSeenRecently(&p3); + auto p4 = makePacket(0x1111, 200, 2, 0x99, 0xBB); + bool weWereNextHop = false; + ph->wasSeenRecently(&p4, true, nullptr, &weWereNextHop); + TEST_ASSERT_TRUE(weWereNextHop); +} + +void test_merge_preserves_highest_hop_limit(void) +{ + // First observation with hop_limit=5 + auto p1 = makePacket(0x1111, 100, 5); + ph->wasSeenRecently(&p1); + + // Re-observation with hop_limit=2 (lower) + auto p2 = makePacket(0x1111, 100, 2); + ph->wasSeenRecently(&p2); + + // Third observation with hop_limit=3 should not trigger upgrade (highest was 5) + bool wasUpgraded = true; + auto p3 = makePacket(0x1111, 100, 3); + ph->wasSeenRecently(&p3, true, nullptr, nullptr, &wasUpgraded); + TEST_ASSERT_FALSE(wasUpgraded); +} + +void test_merge_no_duplicate_relayers(void) +{ + // Observe with relayer 0xAA (stored via relay_node, but only slot 0 for ourRelayID) + // We need to use ourRelayID for the first observation to get it into slot 0 + auto p1 = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID); + ph->wasSeenRecently(&p1); + + // Re-observe with same relay_node=ourRelayID — should not create duplicates + auto p2 = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID); + ph->wasSeenRecently(&p2); + + // ourRelayID should appear exactly once — wasSole should still be true + bool wasSole = false; + TEST_ASSERT_TRUE(ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111, &wasSole)); + TEST_ASSERT_TRUE(wasSole); +} + +void test_merge_adds_new_relayer(void) +{ + auto p1 = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID); + ph->wasSeenRecently(&p1); + + auto p2 = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, 0xBB); + ph->wasSeenRecently(&p2); + + TEST_ASSERT_TRUE(ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111)); + TEST_ASSERT_TRUE(ph->wasRelayer(0xBB, 100, 0x1111)); +} + +void test_merge_we_relay_sets_slot_zero(void) +{ + // When relay_node == ourRelayID, relayed_by[0] should be set to ourRelayID + auto p = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID); + ph->wasSeenRecently(&p); + + TEST_ASSERT_TRUE(ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111)); +} + +void test_merge_heard_back_stores_relay_node(void) +{ + // First: we relay (hop_limit=3) + auto p1 = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID); + ph->wasSeenRecently(&p1); + + // Second: we hear the packet back with hop_limit=2 (one less), from relay_node=0xCC + // This triggers the "heard back" logic: weWereRelayer && hop_limit == ourTxHopLimit-1 + auto p2 = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, 0xCC); + ph->wasSeenRecently(&p2); + + TEST_ASSERT_TRUE(ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111)); + TEST_ASSERT_TRUE(ph->wasRelayer(0xCC, 100, 0x1111)); +} + +// =========================================================================== +// Group 8 — Fallback-to-Flooding Detection +// =========================================================================== + +void test_fallback_detected(void) +{ + // The fallback condition requires wasRelayer(relay_node) && !wasRelayer(ourRelayID). + // Non-us relayers only enter relayed_by[] via the heard-back merge path, which + // also stores ourRelayID. So we must removeRelayer(ourRelayID) to satisfy both. + // + // Scenario: we relay a directed packet, hear it back from 0xAA, then the router + // removes us from the relayer list. Later the sender falls back to flooding. + + // Step 1: We relay (directed to next_hop=0x55) + auto p1 = makePacket(0x1111, 100, 3, 0x55, OUR_RELAY_ID); + ph->wasSeenRecently(&p1); + + // Step 2: Heard-back from 0xAA at hop_limit-1 → stores 0xAA in relayed_by + auto p2 = makePacket(0x1111, 100, 2, 0x55, 0xAA); + ph->wasSeenRecently(&p2); + + // Step 3: Router removes us from the relayer list + ph->removeRelayer(OUR_RELAY_ID, 100, 0x1111); + + // Step 4: Sender falls back to flooding — same packet, NO_NEXT_HOP_PREFERENCE, from 0xAA + auto p3 = makePacket(0x1111, 100, 1, NO_NEXT_HOP_PREFERENCE, 0xAA); + bool wasFallback = false; + ph->wasSeenRecently(&p3, true, &wasFallback); + TEST_ASSERT_TRUE(wasFallback); +} + +void test_fallback_not_when_we_relayed(void) +{ + // First observation: directed, we relayed it + auto p1 = makePacket(0x1111, 100, 3, 0x55, OUR_RELAY_ID); + ph->wasSeenRecently(&p1); + + // Second observation: fallback to flooding from same relayer (us) + // But since we already relayed, wasFallback should be false + auto p2 = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID); + bool wasFallback = false; + ph->wasSeenRecently(&p2, true, &wasFallback); + TEST_ASSERT_FALSE(wasFallback); +} + +void test_fallback_not_on_first_observation(void) +{ + // First time seen — can't be a fallback + auto p = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, 0xAA); + bool wasFallback = false; + ph->wasSeenRecently(&p, true, &wasFallback); + TEST_ASSERT_FALSE(wasFallback); +} + +// =========================================================================== +// Group 9 — Next-Hop and Upgrade Detection +// =========================================================================== + +void test_weWereNextHop_true(void) +{ + // Packet directed to us (next_hop = ourRelayID) + auto p1 = makePacket(0x1111, 100, 3, OUR_RELAY_ID, 0xAA); + ph->wasSeenRecently(&p1); + + // Re-observe: check if we were the original next_hop + auto p2 = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, 0xBB); + bool weWereNextHop = false; + ph->wasSeenRecently(&p2, true, nullptr, &weWereNextHop); + TEST_ASSERT_TRUE(weWereNextHop); +} + +void test_weWereNextHop_false(void) +{ + // Packet directed to someone else + auto p1 = makePacket(0x1111, 100, 3, 0x99, 0xAA); + ph->wasSeenRecently(&p1); + + auto p2 = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, 0xBB); + bool weWereNextHop = false; + ph->wasSeenRecently(&p2, true, nullptr, &weWereNextHop); + TEST_ASSERT_FALSE(weWereNextHop); +} + +void test_wasUpgraded_true(void) +{ + // First observation with hop_limit=3 → stored as highestHopLimit bits 0-2 = 3 + auto p1 = makePacket(0x1111, 100, 3); + ph->wasSeenRecently(&p1); + + // Re-observation with hop_limit=5 + // The upgrade check on line 122 compares the raw packed byte found->hop_limit against p->hop_limit. + // found->hop_limit has highestHopLimit=3 in bits 0-2 (and possibly ourTxHopLimit in bits 3-5). + // So the packed byte value is 3 (or more if ourTxHopLimit was set), and p->hop_limit is 5. + // Since 3 < 5 (with no ourTxHopLimit set), this should detect an upgrade. + auto p2 = makePacket(0x1111, 100, 5); + bool wasUpgraded = false; + ph->wasSeenRecently(&p2, true, nullptr, nullptr, &wasUpgraded); + TEST_ASSERT_TRUE(wasUpgraded); +} + +void test_wasUpgraded_false(void) +{ + auto p1 = makePacket(0x1111, 100, 5); + ph->wasSeenRecently(&p1); + + // Same or lower hop_limit + auto p2 = makePacket(0x1111, 100, 3); + bool wasUpgraded = false; + ph->wasSeenRecently(&p2, true, nullptr, nullptr, &wasUpgraded); + TEST_ASSERT_FALSE(wasUpgraded); +} + +// =========================================================================== +// Group 10 — Edge Cases +// =========================================================================== + +void test_packet_id_zero_not_stored(void) +{ + auto p = makePacket(0x1111, 0); + TEST_ASSERT_FALSE(ph->wasSeenRecently(&p)); + TEST_ASSERT_FALSE(ph->wasSeenRecently(&p)); // still not found +} + +void test_sender_zero_substituted(void) +{ + // from=0 means "from us" — getFrom() substitutes nodeDB->getNodeNum() + auto p = makePacket(0, 100); + ph->wasSeenRecently(&p); + + // Should be stored under our node num, not 0 + auto p2 = makePacket(OUR_NODE_NUM, 100); + TEST_ASSERT_TRUE(ph->wasSeenRecently(&p2, false)); +} + +void test_uninitialized_wasSeenRecently(void) +{ + // Simulate uninitialized state — create a PacketHistory that looks uninitialized + // We can't easily make allocation fail, but we can test the initOk guard with a destructed one + PacketHistory h(4); + TEST_ASSERT_TRUE(h.initOk()); // sanity check + h.~PacketHistory(); + + auto p = makePacket(0x1111, 100); + TEST_ASSERT_FALSE(h.wasSeenRecently(&p)); + + // Reconstruct in place to allow proper destruction + new (&h) PacketHistory(4); +} + +void test_uninitialized_wasRelayer(void) +{ + PacketHistory h(4); + h.~PacketHistory(); + + TEST_ASSERT_FALSE(h.wasRelayer(0xAA, 100, 0x1111)); + + new (&h) PacketHistory(4); +} + +void test_multiple_instances_independent(void) +{ + PacketHistory h2(SMALL_CAPACITY); + + auto p = makePacket(0x1111, 100); + ph->wasSeenRecently(&p); + + // h2 should NOT find it + TEST_ASSERT_FALSE(h2.wasSeenRecently(&p, false)); + + // ph should still find it + TEST_ASSERT_TRUE(ph->wasSeenRecently(&p, false)); +} + +// =========================================================================== +// Group 11 — Hash Table Stress +// =========================================================================== + +void test_many_packets_no_false_negatives(void) +{ + PacketHistory big(64); + for (uint32_t i = 1; i <= 64; i++) { + auto p = makePacket(0xAAAA, i); + big.wasSeenRecently(&p); + } + for (uint32_t i = 1; i <= 64; i++) { + auto p = makePacket(0xAAAA, i); + TEST_ASSERT_TRUE_MESSAGE(big.wasSeenRecently(&p, false), "False negative in hash table"); + } +} + +void test_many_packets_no_false_positives(void) +{ + PacketHistory big(64); + for (uint32_t i = 1; i <= 64; i++) { + auto p = makePacket(0xAAAA, i); + big.wasSeenRecently(&p); + } + // IDs 65..128 were never inserted + for (uint32_t i = 65; i <= 128; i++) { + auto p = makePacket(0xAAAA, i); + TEST_ASSERT_FALSE_MESSAGE(big.wasSeenRecently(&p, false), "False positive in hash table"); + } +} + +void test_churn_correctness(void) +{ + // Insert 3x capacity to force heavy eviction. + // Advance time between each generation so eviction can distinguish old from new. + PacketHistory big(32); + uint32_t capacity = 32; + uint32_t generations = 3; + + for (uint32_t gen = 0; gen < generations; gen++) { + if (gen > 0) + delay(1); // Ensure new generation has a newer timestamp than the old + for (uint32_t i = 1; i <= capacity; i++) { + auto p = makePacket(0xAAAA, gen * capacity + i); + big.wasSeenRecently(&p); + } + } + + uint32_t total = capacity * generations; + + // Only the most recent 32 should be present (due to LRU eviction) + for (uint32_t i = total - 31; i <= total; i++) { + auto p = makePacket(0xAAAA, i); + TEST_ASSERT_TRUE_MESSAGE(big.wasSeenRecently(&p, false), "Recent packet lost after churn"); + } + // Older packets should be gone + int found = 0; + for (uint32_t i = 1; i <= total - capacity; i++) { + auto p = makePacket(0xAAAA, i); + if (big.wasSeenRecently(&p, false)) + found++; + } + TEST_ASSERT_EQUAL_INT_MESSAGE(0, found, "Evicted packets should not be found"); +} + +// =========================================================================== +// Test runner +// =========================================================================== + +void setup() +{ + delay(10); + delay(2000); + + initializeTestEnvironment(); + UNITY_BEGIN(); + + // Group 1 — Initialization + RUN_TEST(test_init_valid_size); + RUN_TEST(test_init_minimum_size); + RUN_TEST(test_init_too_small_falls_back); + + // Group 2 — Basic Deduplication + RUN_TEST(test_first_packet_not_seen); + RUN_TEST(test_same_packet_seen_twice); + RUN_TEST(test_different_id_not_confused); + RUN_TEST(test_different_sender_not_confused); + RUN_TEST(test_withUpdate_false_no_insert); + RUN_TEST(test_withUpdate_true_inserts); + + // Group 3 — LRU Eviction + RUN_TEST(test_fill_capacity_all_found); + RUN_TEST(test_eviction_oldest_replaced); + RUN_TEST(test_matching_slot_reused); + RUN_TEST(test_free_slot_preferred); + RUN_TEST(test_evict_all_old_packets); + + // Group 4 — Relayer Tracking + RUN_TEST(test_wasRelayer_true); + RUN_TEST(test_wasRelayer_false); + RUN_TEST(test_wasRelayer_zero_returns_false); + RUN_TEST(test_wasRelayer_not_found); + RUN_TEST(test_wasRelayer_wasSole_true); + RUN_TEST(test_wasRelayer_wasSole_false); + RUN_TEST(test_wasRelayer_all_six_slots); + + // Group 5 — removeRelayer + RUN_TEST(test_removeRelayer_removes); + RUN_TEST(test_removeRelayer_compacts); + RUN_TEST(test_removeRelayer_nonexistent_safe); + RUN_TEST(test_removeRelayer_packet_not_found_safe); + + // Group 6 — checkRelayers + RUN_TEST(test_checkRelayers_both_found); + RUN_TEST(test_checkRelayers_one_found); + RUN_TEST(test_checkRelayers_r2WasSole); + + // Group 7 — Merge Logic + RUN_TEST(test_merge_preserves_original_next_hop); + RUN_TEST(test_merge_preserves_highest_hop_limit); + RUN_TEST(test_merge_no_duplicate_relayers); + RUN_TEST(test_merge_adds_new_relayer); + RUN_TEST(test_merge_we_relay_sets_slot_zero); + RUN_TEST(test_merge_heard_back_stores_relay_node); + + // Group 8 — Fallback-to-Flooding Detection + RUN_TEST(test_fallback_detected); + RUN_TEST(test_fallback_not_when_we_relayed); + RUN_TEST(test_fallback_not_on_first_observation); + + // Group 9 — Next-Hop and Upgrade Detection + RUN_TEST(test_weWereNextHop_true); + RUN_TEST(test_weWereNextHop_false); + RUN_TEST(test_wasUpgraded_true); + RUN_TEST(test_wasUpgraded_false); + + // Group 10 — Edge Cases + RUN_TEST(test_packet_id_zero_not_stored); + RUN_TEST(test_sender_zero_substituted); + RUN_TEST(test_uninitialized_wasSeenRecently); + RUN_TEST(test_uninitialized_wasRelayer); + RUN_TEST(test_multiple_instances_independent); + + // Group 11 — Hash Table Stress + RUN_TEST(test_many_packets_no_false_negatives); + RUN_TEST(test_many_packets_no_false_positives); + RUN_TEST(test_churn_correctness); + + exit(UNITY_END()); +} + +void loop() {} diff --git a/test/test_transmit_history/test_main.cpp b/test/test_transmit_history/test_main.cpp index 3bd84b55c..c242aa646 100644 --- a/test/test_transmit_history/test_main.cpp +++ b/test/test_transmit_history/test_main.cpp @@ -303,6 +303,10 @@ void setup() { initializeTestEnvironment(); + // Wait for portduino's millis() clock to start ticking before tests run + testDelay(10); + testDelay(2000); + UNITY_BEGIN(); RUN_TEST(test_setLastSentToMesh_stores_millis); diff --git a/test/test_utf8/test_main.cpp b/test/test_utf8/test_main.cpp new file mode 100644 index 000000000..5a074e96e --- /dev/null +++ b/test/test_utf8/test_main.cpp @@ -0,0 +1,197 @@ +#include "meshUtils.h" +#include +#include + +void setUp(void) {} +void tearDown(void) {} + +// --- Valid UTF-8 should pass through unchanged --- + +void test_ascii_unchanged() +{ + char buf[32] = "Hello World"; + TEST_ASSERT_FALSE(sanitizeUtf8(buf, sizeof(buf))); + TEST_ASSERT_EQUAL_STRING("Hello World", buf); +} + +void test_valid_2byte_unchanged() +{ + // "café" — é is C3 A9 + char buf[16] = "caf\xC3\xA9"; + TEST_ASSERT_FALSE(sanitizeUtf8(buf, sizeof(buf))); + TEST_ASSERT_EQUAL_STRING("caf\xC3\xA9", buf); +} + +void test_valid_3byte_unchanged() +{ + // "€" is E2 82 AC + char buf[16] = "\xE2\x82\xAC"; + TEST_ASSERT_FALSE(sanitizeUtf8(buf, sizeof(buf))); + TEST_ASSERT_EQUAL_STRING("\xE2\x82\xAC", buf); +} + +void test_valid_4byte_emoji_unchanged() +{ + // 🌙 is F0 9F 8C 99 + char buf[16] = "\xF0\x9F\x8C\x99"; + TEST_ASSERT_FALSE(sanitizeUtf8(buf, sizeof(buf))); + TEST_ASSERT_EQUAL_STRING("\xF0\x9F\x8C\x99", buf); +} + +void test_valid_mixed_unchanged() +{ + // "Hi 🌙!" — mix of ASCII and 4-byte + char buf[16] = "Hi \xF0\x9F\x8C\x99!"; + TEST_ASSERT_FALSE(sanitizeUtf8(buf, sizeof(buf))); + TEST_ASSERT_EQUAL_STRING("Hi \xF0\x9F\x8C\x99!", buf); +} + +void test_empty_string() +{ + char buf[8] = ""; + TEST_ASSERT_FALSE(sanitizeUtf8(buf, sizeof(buf))); + TEST_ASSERT_EQUAL_STRING("", buf); +} + +// --- Invalid sequences observed in the wild --- + +void test_truncated_4byte_at_end() +{ + // Name with valid emoji 🌙 followed by a truncated 4-byte sequence + ASCII + char buf[32] = "Lunar Tower \xF0\x9F\x8C\x99\xF0\x9F\x97" + "4"; + TEST_ASSERT_TRUE(sanitizeUtf8(buf, sizeof(buf))); + // The 🌙 should be preserved; F0 9F 97 is an incomplete 4-byte sequence, + // '4' (0x34) is not a valid continuation byte + TEST_ASSERT_EQUAL_STRING("Lunar Tower \xF0\x9F\x8C\x99???4", buf); +} + +void test_lone_lead_bytes_without_continuations() +{ + // Mixed ASCII with stray multibyte lead bytes (E1, F3) lacking proper continuations + char buf[32] = "Mesht\xE1\xF3tic 37e2"; + TEST_ASSERT_TRUE(sanitizeUtf8(buf, sizeof(buf))); + // E1 expects 2 continuation bytes, but F3 is not a continuation → E1 replaced + // F3 expects 3 continuation bytes, 't','i','c' are not continuations → F3 replaced + TEST_ASSERT_EQUAL_STRING("Mesht??tic 37e2", buf); +} + +// --- Edge cases --- + +void test_bare_continuation_byte() +{ + // 0x80 alone is invalid (continuation byte with no lead) + char buf[8] = "\x80"; + TEST_ASSERT_TRUE(sanitizeUtf8(buf, sizeof(buf))); + TEST_ASSERT_EQUAL_STRING("?", buf); +} + +void test_overlong_2byte() +{ + // C0 AF is an overlong encoding of U+002F '/' + char buf[8] = "\xC0\xAF"; + TEST_ASSERT_TRUE(sanitizeUtf8(buf, sizeof(buf))); + // C0 is a 2-byte lead, AF is valid continuation, but codepoint 0x2F < 0x80 → overlong + // C0 replaced, AF (now bare continuation) also replaced + TEST_ASSERT_EQUAL_STRING("??", buf); +} + +void test_surrogate_half() +{ + // ED A0 80 encodes U+D800 (surrogate half — invalid in UTF-8) + char buf[8] = "\xED\xA0\x80"; + TEST_ASSERT_TRUE(sanitizeUtf8(buf, sizeof(buf))); + TEST_ASSERT_EQUAL_STRING("???", buf); +} + +void test_5byte_sequence_rejected() +{ + // F8 80 80 80 80 — 5-byte sequence, not valid UTF-8 + char buf[8] = "\xF8\x80\x80\x80\x80"; + TEST_ASSERT_TRUE(sanitizeUtf8(buf, sizeof(buf))); + // F8 is invalid lead (>= 0xF8), each 0x80 is bare continuation + TEST_ASSERT_EQUAL_STRING("?????", buf); +} + +void test_truncated_3byte_at_buffer_end() +{ + // Buffer is exactly 4 bytes: E2 82 then forced null at [3] + char buf[4]; + buf[0] = '\xE2'; + buf[1] = '\x82'; + buf[2] = '\0'; // String ends before the 3-byte sequence completes + buf[3] = '\0'; + TEST_ASSERT_TRUE(sanitizeUtf8(buf, sizeof(buf))); + TEST_ASSERT_EQUAL_STRING("??", buf); +} + +void test_null_termination_enforced() +{ + // Fill buffer completely with no null terminator + char buf[5]; + memset(buf, 'A', sizeof(buf)); + TEST_ASSERT_TRUE(sanitizeUtf8(buf, sizeof(buf))); + // Should be null-terminated and content preserved (all ASCII) + TEST_ASSERT_EQUAL_STRING("AAAA", buf); +} + +void test_null_buffer() +{ + TEST_ASSERT_FALSE(sanitizeUtf8(nullptr, 10)); +} + +void test_zero_size() +{ + char buf[4] = "Hi"; + TEST_ASSERT_FALSE(sanitizeUtf8(buf, 0)); + // Buffer should be untouched + TEST_ASSERT_EQUAL_STRING("Hi", buf); +} + +void test_valid_max_codepoint() +{ + // U+10FFFF = F4 8F BF BF (maximum valid Unicode codepoint) + char buf[8] = "\xF4\x8F\xBF\xBF"; + TEST_ASSERT_FALSE(sanitizeUtf8(buf, sizeof(buf))); + TEST_ASSERT_EQUAL_STRING("\xF4\x8F\xBF\xBF", buf); +} + +void test_above_max_codepoint() +{ + // U+110000 = F4 90 80 80 (just above maximum valid Unicode) + char buf[8] = "\xF4\x90\x80\x80"; + TEST_ASSERT_TRUE(sanitizeUtf8(buf, sizeof(buf))); +} + +void setup() +{ + UNITY_BEGIN(); + + // Valid UTF-8 passthrough + RUN_TEST(test_ascii_unchanged); + RUN_TEST(test_valid_2byte_unchanged); + RUN_TEST(test_valid_3byte_unchanged); + RUN_TEST(test_valid_4byte_emoji_unchanged); + RUN_TEST(test_valid_mixed_unchanged); + RUN_TEST(test_empty_string); + + // Invalid sequences observed in the wild + RUN_TEST(test_truncated_4byte_at_end); + RUN_TEST(test_lone_lead_bytes_without_continuations); + + // Edge cases + RUN_TEST(test_bare_continuation_byte); + RUN_TEST(test_overlong_2byte); + RUN_TEST(test_surrogate_half); + RUN_TEST(test_5byte_sequence_rejected); + RUN_TEST(test_truncated_3byte_at_buffer_end); + RUN_TEST(test_null_termination_enforced); + RUN_TEST(test_null_buffer); + RUN_TEST(test_zero_size); + RUN_TEST(test_valid_max_codepoint); + RUN_TEST(test_above_max_codepoint); + + exit(UNITY_END()); +} + +void loop() {} diff --git a/variants/esp32/m5stack_coreink/platformio.ini b/variants/esp32/m5stack_coreink/platformio.ini index e107bd893..70ada7bf3 100644 --- a/variants/esp32/m5stack_coreink/platformio.ini +++ b/variants/esp32/m5stack_coreink/platformio.ini @@ -19,7 +19,7 @@ build_flags = lib_deps = ${esp32_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.8 + zinggjm/GxEPD2@1.6.9 # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib lewisxhe/SensorLib@0.3.4 lib_ignore = diff --git a/variants/esp32/tbeam/variant.h b/variants/esp32/tbeam/variant.h index cca52cb9a..e51855b1a 100644 --- a/variants/esp32/tbeam/variant.h +++ b/variants/esp32/tbeam/variant.h @@ -35,9 +35,10 @@ // code) #endif -// Leave undefined to disable our PMU IRQ handler. DO NOT ENABLE THIS because the pmuirq can cause sperious interrupts -// and waking from light sleep -// #define PMU_IRQ 35 +// Voiding more warranties. +#define PMU_IRQ 35 +#define PMU_POWER_BUTTON_IS_CANCEL // maps a short click of the power button to a cancel action (turning off the screen) + #define HAS_AXP192 #define GPS_UBLOX #define GPS_RX_PIN 34 diff --git a/variants/esp32s3/diy/my_esp32s3_diy_eink/platformio.ini b/variants/esp32s3/diy/my_esp32s3_diy_eink/platformio.ini index 90e4910f4..71116279c 100644 --- a/variants/esp32s3/diy/my_esp32s3_diy_eink/platformio.ini +++ b/variants/esp32s3/diy/my_esp32s3_diy_eink/platformio.ini @@ -11,7 +11,7 @@ upload_speed = 921600 lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.8 + zinggjm/GxEPD2@1.6.9 build_unflags = ${esp32s3_base.build_unflags} -DARDUINO_USB_MODE=1 diff --git a/variants/esp32s3/esp32-s3-pico/platformio.ini b/variants/esp32s3/esp32-s3-pico/platformio.ini index 64f50f80e..b5ff66b85 100644 --- a/variants/esp32s3/esp32-s3-pico/platformio.ini +++ b/variants/esp32s3/esp32-s3-pico/platformio.ini @@ -23,4 +23,4 @@ build_flags = ${esp32s3_base.build_flags} lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.8 + zinggjm/GxEPD2@1.6.9 diff --git a/variants/esp32s3/heltec_capsule_sensor_v3/variant.h b/variants/esp32s3/heltec_capsule_sensor_v3/variant.h index 3ee5545a8..f689b20a8 100644 --- a/variants/esp32s3/heltec_capsule_sensor_v3/variant.h +++ b/variants/esp32s3/heltec_capsule_sensor_v3/variant.h @@ -1,6 +1,7 @@ #define LED_POWER 33 #define LED_POWER2 34 #define EXT_PWR_DETECT 35 +#define EXT_PWR_DETECT_MODE INPUT_PULLUP #define BUTTON_PIN 18 #define BUTTON_ACTIVE_LOW false diff --git a/variants/esp32s3/heltec_sensor_hub/variant.h b/variants/esp32s3/heltec_sensor_hub/variant.h index 8c5d31c9a..64255c038 100644 --- a/variants/esp32s3/heltec_sensor_hub/variant.h +++ b/variants/esp32s3/heltec_sensor_hub/variant.h @@ -1,4 +1,5 @@ #define EXT_PWR_DETECT 20 +#define EXT_PWR_DETECT_MODE INPUT_PULLUP #define BUTTON_PIN 17 #define BUTTON_ACTIVE_LOW false diff --git a/variants/esp32s3/heltec_v4/platformio.ini b/variants/esp32s3/heltec_v4/platformio.ini index 5a5004a45..d6634aa74 100644 --- a/variants/esp32s3/heltec_v4/platformio.ini +++ b/variants/esp32s3/heltec_v4/platformio.ini @@ -120,7 +120,7 @@ build_flags = -D TFT_OFFSET_Y=0 -D TFT_OFFSET_ROTATION=0 -D SCREEN_ROTATE - -D SCREEN_TRANSITION_FRAMERATE=5 + -D SCREEN_TRANSITION_FRAMERATE=30 -D BRIGHTNESS_DEFAULT=130 ; Medium Low Brightness -D HAS_TOUCHSCREEN=1 -D TOUCH_I2C_PORT=0 @@ -133,4 +133,4 @@ lib_deps = ${heltec_v4_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX lovyan03/LovyanGFX@1.2.19 # renovate: datasource=git-refs depName=Quency-D_chsc6x packageName=https://github.com/Quency-D/chsc6x gitBranch=master - https://github.com/Quency-D/chsc6x/archive/5cbead829d6b432a8d621ed1aafd4eb474fd4f27.zip \ No newline at end of file + https://github.com/Quency-D/chsc6x/archive/5cbead829d6b432a8d621ed1aafd4eb474fd4f27.zip diff --git a/variants/esp32s3/heltec_vision_master_t190/platformio.ini b/variants/esp32s3/heltec_vision_master_t190/platformio.ini index 3dab9f93c..c0c3b2f0e 100644 --- a/variants/esp32s3/heltec_vision_master_t190/platformio.ini +++ b/variants/esp32s3/heltec_vision_master_t190/platformio.ini @@ -20,5 +20,5 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-st7789 packageName=https://github.com/meshtastic/st7789 gitBranch=main - https://github.com/meshtastic/st7789/archive/92bae2e4a307afb430c3b0bc3d661c55ee1565f0.zip + https://github.com/meshtastic/st7789/archive/5180423ae2dbf5885168a8bfb308c7fb7eff6930.zip upload_speed = 921600 diff --git a/variants/esp32s3/m5stack_cardputer_adv/platformio.ini b/variants/esp32s3/m5stack_cardputer_adv/platformio.ini index 3b378ed94..1144994a0 100644 --- a/variants/esp32s3/m5stack_cardputer_adv/platformio.ini +++ b/variants/esp32s3/m5stack_cardputer_adv/platformio.ini @@ -10,13 +10,10 @@ build_flags = -D M5STACK_CARDPUTER_ADV -D ARDUINO_USB_CDC_ON_BOOT=1 -I variants/esp32s3/m5stack_cardputer_adv -build_src_filter = - ${esp32s3_base.build_src_filter} - +<../variants/esp32s3/m5stack_cardputer_adv> lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-st7789 packageName=https://github.com/meshtastic/st7789 gitBranch=main - https://github.com/meshtastic/st7789/archive/92bae2e4a307afb430c3b0bc3d661c55ee1565f0.zip + https://github.com/meshtastic/st7789/archive/5180423ae2dbf5885168a8bfb308c7fb7eff6930.zip # renovate: datasource=github-tags depName=pschatzmann_arduino-audio-driver packageName=pschatzmann/arduino-audio-driver https://github.com/pschatzmann/arduino-audio-driver/archive/v0.2.1.zip # renovate: datasource=git-refs depName=ESP8266Audio packageName=https://github.com/meshtastic/ESP8266Audio gitBranch=meshtastic-2.0.0-dacfix diff --git a/variants/esp32s3/picomputer-s3/variant.h b/variants/esp32s3/picomputer-s3/variant.h index 7b6218f87..60afac002 100644 --- a/variants/esp32s3/picomputer-s3/variant.h +++ b/variants/esp32s3/picomputer-s3/variant.h @@ -47,10 +47,9 @@ #define TFT_OFFSET_Y 0 #define TFT_OFFSET_ROTATION 0 #define SCREEN_ROTATE -#define SCREEN_TRANSITION_FRAMERATE 5 +#define SCREEN_TRANSITION_FRAMERATE 30 // Picomputer gets a white on black display -#define TFT_MESH_OVERRIDE COLOR565(255, 255, 255) #define INPUTBROKER_MATRIX_TYPE 1 diff --git a/variants/esp32s3/seeed-sensecap-indicator/variant.h b/variants/esp32s3/seeed-sensecap-indicator/variant.h index f946528ae..8fa9e2393 100644 --- a/variants/esp32s3/seeed-sensecap-indicator/variant.h +++ b/variants/esp32s3/seeed-sensecap-indicator/variant.h @@ -36,7 +36,7 @@ #define TFT_OFFSET_ROTATION 0 #define TFT_BL 45 #define SCREEN_ROTATE -#define SCREEN_TRANSITION_FRAMERATE 5 // fps +#define SCREEN_TRANSITION_FRAMERATE 30 // fps #define USE_TFTDISPLAY 1 #define HAS_TOUCHSCREEN 1 diff --git a/variants/esp32s3/station-g2/pins_arduino.h b/variants/esp32s3/station-g2/pins_arduino.h old mode 100644 new mode 100755 diff --git a/variants/esp32s3/station-g2/platformio.ini b/variants/esp32s3/station-g2/platformio.ini old mode 100644 new mode 100755 diff --git a/variants/esp32s3/station-g2/variant.h b/variants/esp32s3/station-g2/variant.h old mode 100644 new mode 100755 diff --git a/variants/esp32s3/t-deck-pro-v1_1/platformio.ini b/variants/esp32s3/t-deck-pro-v1_1/platformio.ini index 1a9b20f76..22432d769 100644 --- a/variants/esp32s3/t-deck-pro-v1_1/platformio.ini +++ b/variants/esp32s3/t-deck-pro-v1_1/platformio.ini @@ -30,7 +30,7 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.8 + zinggjm/GxEPD2@1.6.9 # renovate: datasource=git-refs depName=CSE_Touch packageName=https://github.com/CIRCUITSTATE/CSE_Touch gitBranch=main https://github.com/CIRCUITSTATE/CSE_Touch/archive/b44f23b6f870b848f1fbe453c190879bc6cfaafa.zip # renovate: datasource=github-tags depName=CSE_CST328 packageName=CIRCUITSTATE/CSE_CST328 diff --git a/variants/esp32s3/t-deck-pro/platformio.ini b/variants/esp32s3/t-deck-pro/platformio.ini index 93ef8babf..d1a2398a4 100644 --- a/variants/esp32s3/t-deck-pro/platformio.ini +++ b/variants/esp32s3/t-deck-pro/platformio.ini @@ -34,7 +34,7 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.8 + zinggjm/GxEPD2@1.6.9 # renovate: datasource=git-refs depName=CSE_Touch packageName=https://github.com/CIRCUITSTATE/CSE_Touch gitBranch=main https://github.com/CIRCUITSTATE/CSE_Touch/archive/b44f23b6f870b848f1fbe453c190879bc6cfaafa.zip # renovate: datasource=github-tags depName=CSE_CST328 packageName=CIRCUITSTATE/CSE_CST328 diff --git a/variants/esp32s3/t-deck/variant.h b/variants/esp32s3/t-deck/variant.h index 5d885579a..eb1bbdfef 100644 --- a/variants/esp32s3/t-deck/variant.h +++ b/variants/esp32s3/t-deck/variant.h @@ -20,7 +20,7 @@ #define TFT_OFFSET_Y 0 #define TFT_OFFSET_ROTATION 0 #define SCREEN_ROTATE -#define SCREEN_TRANSITION_FRAMERATE 5 +#define SCREEN_TRANSITION_FRAMERATE 30 #define BRIGHTNESS_DEFAULT 130 // Medium Low Brightness #define USE_TFTDISPLAY 1 #define HAS_PHYSICAL_KEYBOARD 1 diff --git a/variants/esp32s3/t-watch-s3/variant.h b/variants/esp32s3/t-watch-s3/variant.h index df275c31d..fddd98304 100644 --- a/variants/esp32s3/t-watch-s3/variant.h +++ b/variants/esp32s3/t-watch-s3/variant.h @@ -39,9 +39,10 @@ #define DAC_I2S_BCK 48 #define DAC_I2S_WS 15 #define DAC_I2S_DOUT 46 -#define DAC_I2S_MCLK 0 +#define DAC_I2S_MCLK -1 #define HAS_AXP2101 +#define PMU_POWER_BUTTON_IS_CANCEL // maps a short click of the power button to a cancel action (turning off the screen) // PCF8563 RTC Module #define PCF8563_RTC 0x51 @@ -60,6 +61,8 @@ #define BUTTON_PIN 0 // only for Plus version +#define PMU_IRQ 21 // Interrupt pin for the PMU + #define USE_SX1262 #define USE_SX1268 diff --git a/variants/esp32s3/t5s3_epaper/nicheGraphics.h b/variants/esp32s3/t5s3_epaper/nicheGraphics.h index 699a82de0..6b5f434ba 100644 --- a/variants/esp32s3/t5s3_epaper/nicheGraphics.h +++ b/variants/esp32s3/t5s3_epaper/nicheGraphics.h @@ -6,26 +6,27 @@ NicheGraphics attempts a different approach: Per-device config takes place in this setupNicheGraphics() method (And a small amount in platformio.ini) -This file sets up InkHUD for Heltec VM-E290. -Different NicheGraphics UIs and different hardware variants will each have their own setup procedure. +This file sets up InkHUD for the LilyGo T5-E-Paper-S3-Pro. + +The board uses a 4.7" ED047TC1 parallel e-paper display (960×540, 8-bit parallel interface). +This is driven via the FastEPD library through the NicheGraphics ED047TC1 driver adapter. */ #pragma once #include "configuration.h" -#include "mesh/MeshModule.h" #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS // InkHUD-specific components // --------------------------- -// #include "graphics/niche/InkHUD/InkHUD.h" -#include "graphics/niche/InkHUD/WindowManager.h" +#include "graphics/niche/InkHUD/InkHUD.h" // Applets #include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" #include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" +#include "graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h" #include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" #include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" #include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" @@ -33,27 +34,20 @@ Different NicheGraphics UIs and different hardware variants will each have their // Shared NicheGraphics components // -------------------------------- -#include "graphics/niche/Drivers/Backlight/LatchingBacklight.h" -#include "graphics/niche/Drivers/EInk/DEPG0290BNS800.h" +#include "graphics/niche/Drivers/EInk/ED047TC1.h" #include "graphics/niche/Inputs/TwoButton.h" void setupNicheGraphics() { using namespace NicheGraphics; - // SPI - // ----------------------------- - - // Display is connected to HSPI - SPIClass *hspi = new SPIClass(HSPI); - hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); - // E-Ink Driver // ----------------------------- + // The ED047TC1 is a parallel display — no SPI bus setup needed. + // begin() args are part of the EInk interface but are ignored for parallel displays. - // Use E-Ink driver - Drivers::EInk *driver = new Drivers::DEPG0290BNS800; - driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY); + Drivers::EInk *driver = new Drivers::ED047TC1; + driver->begin(nullptr, 0, 0, 0); // InkHUD // ---------------------------- @@ -67,57 +61,59 @@ void setupNicheGraphics() // Set how unhealthy additional FAST updates beyond this number are inkhud->setDisplayResilience(7, 1.5); - // Prepare fonts - InkHUD::Applet::fontLarge = FREESANS_9PT_WIN1252; - InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; + // Prepare fonts — use larger sizes to suit the 4.7" screen at ~234 DPI + InkHUD::Applet::fontLarge = FREESANS_24PT_WIN1253; + InkHUD::Applet::fontMedium = FREESANS_18PT_WIN1253; + InkHUD::Applet::fontSmall = FREESANS_12PT_WIN1253; - // Init settings, and customize defaults + // Customize default settings inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle? - inkhud->persistence->settings.rotation = 1; // 90 degrees clockwise + inkhud->persistence->settings.rotation = 3; // 270 degrees clockwise inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users - inkhud->persistence->settings.optionalMenuItems.nextTile = false; // Behavior handled by aux button instead - inkhud->persistence->settings.optionalFeatures.batteryIcon = true; // Device definitely has a battery + inkhud->persistence->settings.optionalFeatures.batteryIcon = true; + inkhud->persistence->settings.optionalMenuItems.backlight = false; - // Setup backlight - // Note: AUX button behavior configured further down - Drivers::LatchingBacklight *backlight = Drivers::LatchingBacklight::getInstance(); - backlight->setPin(PIN_EINK_EN); + // Alignment must cancel rotation for visual-frame touch input: (rotation + alignment) % 4 == 0. + inkhud->persistence->settings.joystick.alignment = (4 - inkhud->persistence->settings.rotation) % 4; // Pick applets // Note: order of applets determines priority of "auto-show" feature - // Optional arguments for defaults: - // - is activated? - // - is autoshown? - // - is foreground on a specific tile (index)? - inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown - inkhud->addApplet("DMs", new InkHUD::DMApplet); - inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); + inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, false, false); // Not Active, not autoshown + inkhud->addApplet("DMs", new InkHUD::DMApplet, true, true); // Activated, Autoshown + inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0), true, true); // Activated, Autoshown + inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1), false, false); // Not Active, not autoshown + inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true, false); // Activated, not autoshown + inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet, true, false); // Activated, not autoshown inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0 - // inkhud->addApplet("Basic", new InkHUD::BasicExampleApplet); - // inkhud->addApplet("NewMsg", new InkHUD::NewMsgExampleApplet); + inkhud->addApplet("Favorites Map", new InkHUD::FavoritesMapApplet, false, false); // Not Active, not autoshown + + // Enable reusable InkHUD touch status indicator for this touch-capable board. + inkhud->setTouchEnabledProvider(isTouchInputEnabled); // Start running InkHUD inkhud->begin(); + // Arm GT911 capacitive-home callback only after InkHUD startup is complete. + t5SetHomeCapButtonEventsEnabled(true); + + // Keep Wireless Paper single-button semantics regardless of persisted settings: + // short press advances, long press opens menu/selects. + inkhud->persistence->settings.joystick.enabled = false; // Buttons // -------------------------- Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // A shared NicheGraphics component - // Setup the main user button (0) + // #0: BOOT button (primary user input for InkHUD navigation on T5-S3) +#if defined(T5_S3_EPAPER_PRO_V1) + buttons->setWiring(0, PIN_BUTTON2); +#else buttons->setWiring(0, BUTTON_PIN); - buttons->setHandlerShortPress(0, []() { InkHUD::InkHUD::getInstance()->shortpress(); }); - buttons->setHandlerLongPress(0, []() { InkHUD::InkHUD::getInstance()->longpress(); }); - - // Setup the aux button (1) - // Bonus feature of VME290 - buttons->setWiring(1, BUTTON_PIN_SECONDARY); - buttons->setHandlerShortPress(1, []() { InkHUD::InkHUD::getInstance()->nextTile(); }); +#endif + buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); + buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); buttons->start(); } -#endif \ No newline at end of file +#endif diff --git a/variants/esp32s3/t5s3_epaper/platformio.ini b/variants/esp32s3/t5s3_epaper/platformio.ini index bad36706c..2001133c7 100644 --- a/variants/esp32s3/t5s3_epaper/platformio.ini +++ b/variants/esp32s3/t5s3_epaper/platformio.ini @@ -5,7 +5,7 @@ board_build.partition = default_16MB.csv board_check = true upload_protocol = esptool build_flags = -fno-strict-aliasing - ${esp32_base.build_flags} + ${esp32s3_base.build_flags} -I variants/esp32s3/t5s3_epaper -D T5_S3_EPAPER_PRO -D USE_EINK diff --git a/variants/esp32s3/t5s3_epaper/variant.cpp b/variants/esp32s3/t5s3_epaper/variant.cpp index e10d7c347..6599ced23 100644 --- a/variants/esp32s3/t5s3_epaper/variant.cpp +++ b/variants/esp32s3/t5s3_epaper/variant.cpp @@ -1,28 +1,9 @@ -#include "configuration.h" - -#ifdef T5_S3_EPAPER_PRO - -#include "TouchDrvGT911.hpp" -#include "Wire.h" -#include "input/TouchScreenImpl1.h" - -TouchDrvGT911 touch; - -bool readTouch(int16_t *x, int16_t *y) -{ - if (!digitalRead(GT911_PIN_INT)) { - int16_t raw_x; - int16_t raw_y; - if (touch.getPoint(&raw_x, &raw_y)) { - // rotate 90° for landscape - *x = raw_y; - *y = EPD_WIDTH - 1 - raw_x; - LOG_DEBUG("touched(%d/%d)", *x, *y); - return true; - } - } - return false; -} +// Pin-level early init only. All touch, backlight, and InkHUD code lives in +// src/platform/extra_variants/t5s3_epaper/variant.cpp where PlatformIO's +// library dependency finder can resolve headers like TouchDrvGT911.hpp. +#include "variant.h" +#include "Arduino.h" +#include "pins_arduino.h" void earlyInitVariant() { @@ -31,17 +12,28 @@ void earlyInitVariant() pinMode(SDCARD_CS, OUTPUT); digitalWrite(SDCARD_CS, HIGH); pinMode(BOARD_BL_EN, OUTPUT); -} + // Backlight ON at boot (active-HIGH). Full backlight state management + // lives in src/platform/extra_variants/t5s3_epaper/variant.cpp. + digitalWrite(BOARD_BL_EN, HIGH); -// T5-S3-ePaper Pro specific (late-) init -void lateInitVariant(void) -{ - touch.setPins(GT911_PIN_RST, GT911_PIN_INT); - if (touch.begin(Wire, GT911_SLAVE_ADDRESS_L, GT911_PIN_SDA, GT911_PIN_SCL)) { - touchScreenImpl1 = new TouchScreenImpl1(EPD_WIDTH, EPD_HEIGHT, readTouch); - touchScreenImpl1->init(); - } else { - LOG_ERROR("Failed to find touch controller!"); - } + // Program GT911 touch controller to I2C address 0x14 (GT911_SLAVE_ADDRESS_H) before + // the I2C bus scan runs. GPIO3 (INT) defaults LOW on ESP32-S3 cold boot, which would + // leave the GT911 at 0x5D (GT911_SLAVE_ADDRESS_L) — the same address as the SFA30 + // air quality sensor — causing a false-positive SFA30 detection during the I2C scan. + // + // GT911 datasheet §4.3 "Address Selection": + // Pull INT HIGH before releasing RST → device latches address 0x14 (SLAVE_ADDRESS_H) + // Pull INT LOW before releasing RST → device latches address 0x5D (SLAVE_ADDRESS_L) + // Minimum RST assert time: 100 µs; minimum startup time after RST deassert: 5 ms. + // + // lateInitVariant() calls touch.begin() which repeats this sequence internally while + // also performing full I2C initialisation; the double-reset is harmless. + pinMode(GT911_PIN_RST, OUTPUT); + digitalWrite(GT911_PIN_RST, LOW); + pinMode(GT911_PIN_INT, OUTPUT); + digitalWrite(GT911_PIN_INT, HIGH); // HIGH → latch address 0x14 + delay(1); // > 100 µs + digitalWrite(GT911_PIN_RST, HIGH); + delay(10); // > 5 ms startup + pinMode(GT911_PIN_INT, INPUT); // release INT for interrupt use } -#endif \ No newline at end of file diff --git a/variants/esp32s3/t5s3_epaper/variant.h b/variants/esp32s3/t5s3_epaper/variant.h index c2c001373..49579a39c 100644 --- a/variants/esp32s3/t5s3_epaper/variant.h +++ b/variants/esp32s3/t5s3_epaper/variant.h @@ -16,6 +16,11 @@ #define I2C_SCL SCL #define HAS_TOUCHSCREEN 1 +#define TOUCH_POLL_INTERVAL_IDLE 25 +#define TOUCH_POLL_INTERVAL_ACTIVE 15 +#define TOUCH_POLL_INTERVAL_RELEASE 20 +#define TOUCH_POLL_INTERVAL_ACTIVE_FAST 8 +#define TOUCH_POLL_INTERVAL_RELEASE_FAST 8 #define GT911_PIN_SDA SDA #define GT911_PIN_SCL SCL #if defined(T5_S3_EPAPER_PRO_V1) @@ -25,10 +30,34 @@ #define GT911_PIN_INT 3 #define GT911_PIN_RST 9 #endif +// Do not use touch as a light-sleep wake source on T5-S3. +// Wake should come from physical buttons/radio/timer only. +#define SCREEN_TOUCH_INT GT911_PIN_INT -#define PCF85063_RTC 0x51 +// Touch control helpers for this variant +bool isTouchInputEnabled(); +void setTouchInputEnabled(bool enabled, bool showIndicator); +void toggleTouchInputEnabled(); + +// Backlight control helpers for this variant (non-latching behavior) +void t5BacklightSetUserEnabled(bool enabled); +bool t5BacklightIsUserEnabled(); +void t5BacklightToggleUser(); +void t5BacklightSetForcedByTimeout(bool forced); +void t5BacklightSetForcedBySleep(bool forced); +void t5BacklightHandleUserInput(); + +// Touch timeout/wake helpers for this variant +void t5TouchSetForcedByTimeout(bool forced); +bool t5TouchIsForcedByTimeout(); +void t5TouchHandleUserInput(); + +// Gate GT911 capacitive-home callback delivery until InkHUD startup is complete. +void t5SetHomeCapButtonEventsEnabled(bool enabled); + +#define PCF8563_RTC 0x51 #define HAS_RTC 1 -#define PCF85063_INT 2 +#define PCF8563_INT 2 #define USE_POWERSAVE #define SLEEP_TIME 120 @@ -45,8 +74,10 @@ #define ALT_BUTTON_PIN PIN_BUTTON2 #else #define BUTTON_PIN 0 +#define BOARD_PCA9535_ADDR 0x20 +#define BOARD_PCA9535_INT 38 +#define BOARD_PCA9535_BUTTON_MASK 0x04 #endif - // SD card #define HAS_SDCARD #define SDCARD_CS SPI_CS diff --git a/variants/esp32s3/tbeam-s3-core/variant.h b/variants/esp32s3/tbeam-s3-core/variant.h index 9ce4aade9..11e463364 100644 --- a/variants/esp32s3/tbeam-s3-core/variant.h +++ b/variants/esp32s3/tbeam-s3-core/variant.h @@ -9,8 +9,6 @@ #define BUTTON_PIN 0 // The middle button GPIO on the T-Beam S3 // #define EXT_NOTIFY_OUT 13 // Default pin to use for Ext Notify Module. -#define LED_STATE_ON 0 // State when LED is lit - // TTGO uses a common pinout for their SX1262 vs RF95 modules - both can be enabled and we will probe at runtime for RF95 and if // not found then probe for SX1262 #define USE_SX1262 @@ -48,9 +46,9 @@ #define LR11X0_DIO_AS_RF_SWITCH #endif -// Leave undefined to disable our PMU IRQ handler. DO NOT ENABLE THIS because the pmuirq can cause sperious interrupts -// and waking from light sleep -// #define PMU_IRQ 40 +// Voiding warrenties, we're gonna try the IRQ +#define PMU_IRQ 40 +#define PMU_POWER_BUTTON_IS_CANCEL // maps a short click of the power button to a cancel action (turning off the screen) #define HAS_AXP2101 // PCF8563 RTC Module @@ -76,4 +74,4 @@ // has 32768 Hz crystal #define HAS_32768HZ 1 -#define USE_SH1106 \ No newline at end of file +#define USE_SH1106 diff --git a/variants/esp32s3/tlora-pager/variant.h b/variants/esp32s3/tlora-pager/variant.h index 28359c786..f2512fb7c 100644 --- a/variants/esp32s3/tlora-pager/variant.h +++ b/variants/esp32s3/tlora-pager/variant.h @@ -18,7 +18,7 @@ #define TFT_OFFSET_Y 0 #define TFT_OFFSET_ROTATION 3 #define SCREEN_ROTATE -#define SCREEN_TRANSITION_FRAMERATE 5 +#define SCREEN_TRANSITION_FRAMERATE 30 #define BRIGHTNESS_DEFAULT 130 // Medium Low Brightness #define USE_TFTDISPLAY 1 #define HAS_PHYSICAL_KEYBOARD 1 diff --git a/variants/esp32s3/tracksenger/internal/variant.h b/variants/esp32s3/tracksenger/internal/variant.h index f9a20c901..b2822c24b 100644 --- a/variants/esp32s3/tracksenger/internal/variant.h +++ b/variants/esp32s3/tracksenger/internal/variant.h @@ -73,7 +73,6 @@ #define SX126X_DIO3_TCXO_VOLTAGE 1.8 // Picomputer gets a white on black display -#define TFT_MESH_OVERRIDE COLOR565(255, 255, 255) // keyboard changes @@ -89,4 +88,4 @@ { \ 26, 37, 17, 16, 15, 7 \ } -// #end keyboard \ No newline at end of file +// #end keyboard diff --git a/variants/esp32s3/tracksenger/lcd/variant.h b/variants/esp32s3/tracksenger/lcd/variant.h index 029f7753b..6c32ff279 100644 --- a/variants/esp32s3/tracksenger/lcd/variant.h +++ b/variants/esp32s3/tracksenger/lcd/variant.h @@ -97,7 +97,6 @@ #define SX126X_DIO3_TCXO_VOLTAGE 1.8 // Picomputer gets a white on black display -#define TFT_MESH_OVERRIDE COLOR565(255, 255, 255) // keyboard changes @@ -113,4 +112,4 @@ { \ 26, 37, 17, 16, 15, 7 \ } -// #end keyboard \ No newline at end of file +// #end keyboard diff --git a/variants/esp32s3/tracksenger/oled/variant.h b/variants/esp32s3/tracksenger/oled/variant.h index 1f1fbbaa1..72762c7af 100644 --- a/variants/esp32s3/tracksenger/oled/variant.h +++ b/variants/esp32s3/tracksenger/oled/variant.h @@ -74,7 +74,6 @@ #define SX126X_DIO3_TCXO_VOLTAGE 1.8 // Picomputer gets a white on black display -#define TFT_MESH_OVERRIDE COLOR565(255, 255, 255) // keyboard changes @@ -90,4 +89,4 @@ { \ 26, 37, 17, 16, 15, 7 \ } -// #end keyboard \ No newline at end of file +// #end keyboard diff --git a/variants/esp32s3/unphone/variant.h b/variants/esp32s3/unphone/variant.h index 268eedea5..76c66ca64 100644 --- a/variants/esp32s3/unphone/variant.h +++ b/variants/esp32s3/unphone/variant.h @@ -35,7 +35,7 @@ #define TFT_OFFSET_ROTATION 6 // unPhone's screen wired unusually, 0 typical #define TFT_INVERT false #define SCREEN_ROTATE true -#define SCREEN_TRANSITION_FRAMERATE 5 +#define SCREEN_TRANSITION_FRAMERATE 30 #define USE_TFTDISPLAY 1 #define HAS_TOUCHSCREEN 1 @@ -74,4 +74,4 @@ // #define BATTERY_PIN 13 // battery V measurement pin; vbat divider is here // #define ADC_CHANNEL ADC2_GPIO13_CHANNEL -// #define BAT_MEASURE_ADC_UNIT 2 \ No newline at end of file +// #define BAT_MEASURE_ADC_UNIT 2 diff --git a/variants/native/portduino.ini b/variants/native/portduino.ini index 87d8431a3..b276d2779 100644 --- a/variants/native/portduino.ini +++ b/variants/native/portduino.ini @@ -2,7 +2,7 @@ [portduino_base] platform = # renovate: datasource=git-refs depName=platform-native packageName=https://github.com/meshtastic/platform-native gitBranch=develop - https://github.com/meshtastic/platform-native/archive/71ed55bb95feb3c43ebde1ec1e2e17643a424c04.zip + https://github.com/meshtastic/platform-native/archive/135b91e953db0b5f44d278f8ebd5b8d985fc03d8.zip framework = arduino build_src_filter = diff --git a/variants/nrf52840/Dongle_nRF52840-pca10059-v1/platformio.ini b/variants/nrf52840/Dongle_nRF52840-pca10059-v1/platformio.ini index fd159a6d2..e0cdd73c5 100644 --- a/variants/nrf52840/Dongle_nRF52840-pca10059-v1/platformio.ini +++ b/variants/nrf52840/Dongle_nRF52840-pca10059-v1/platformio.ini @@ -12,5 +12,5 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/Dongle_ lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.8 + zinggjm/GxEPD2@1.6.9 debug_tool = jlink diff --git a/variants/nrf52840/ELECROW-ThinkNode-M4/variant.h b/variants/nrf52840/ELECROW-ThinkNode-M4/variant.h index 2cfe948e3..2164bcedc 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M4/variant.h +++ b/variants/nrf52840/ELECROW-ThinkNode-M4/variant.h @@ -85,7 +85,6 @@ static const uint8_t A0 = PIN_A0; // charger status #define EXT_CHRG_DETECT (32 + 6) -#define EXT_CHRG_DETECT_VALUE HIGH // SPI #define SPI_INTERFACES_COUNT 1 diff --git a/variants/nrf52840/ELECROW-ThinkNode-M6/variant.cpp b/variants/nrf52840/ELECROW-ThinkNode-M6/variant.cpp index a43755c06..f15a03f4d 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M6/variant.cpp +++ b/variants/nrf52840/ELECROW-ThinkNode-M6/variant.cpp @@ -20,6 +20,7 @@ #include "variant.h" #include "nrf.h" +#include "power.h" #include "wiring_constants.h" #include "wiring_digital.h" @@ -65,7 +66,11 @@ void variant_shutdown() nrf_gpio_pin_sense_t sense1 = NRF_GPIO_PIN_SENSE_LOW; nrf_gpio_cfg_sense_set(PIN_BUTTON1, sense1); - nrf_gpio_cfg_input(EXT_CHRG_DETECT, NRF_GPIO_PIN_PULLUP); // Configure the pin to be woken up as an input - nrf_gpio_pin_sense_t sense2 = NRF_GPIO_PIN_SENSE_LOW; - nrf_gpio_cfg_sense_set(EXT_CHRG_DETECT, sense2); + // If we are sleeping because of low battery, wake up when the solar charger detects power. + // But if the user intentionally put us to sleep with the button, don't wake up just because the lights are on + if (power->isLowBattery()) { + nrf_gpio_cfg_input(EXT_CHRG_DETECT, NRF_GPIO_PIN_PULLUP); // Configure the pin to be woken up as an input + nrf_gpio_pin_sense_t sense2 = NRF_GPIO_PIN_SENSE_LOW; + nrf_gpio_cfg_sense_set(EXT_CHRG_DETECT, sense2); + } } diff --git a/variants/nrf52840/ELECROW-ThinkNode-M6/variant.h b/variants/nrf52840/ELECROW-ThinkNode-M6/variant.h index 2ebb79031..48b27c669 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M6/variant.h +++ b/variants/nrf52840/ELECROW-ThinkNode-M6/variant.h @@ -138,7 +138,7 @@ static const uint8_t A0 = PIN_A0; #define HAS_SOLAR -#define OCV_ARRAY 4080, 3990, 3935, 3880, 3825, 3770, 3715, 3660, 3605, 3550, 3450 +#define OCV_ARRAY 4080, 3990, 3935, 3880, 3825, 3770, 3715, 3660, 3605, 3550, 3490 #ifdef __cplusplus } diff --git a/variants/nrf52840/ME25LS01-4Y10TD_e-ink/platformio.ini b/variants/nrf52840/ME25LS01-4Y10TD_e-ink/platformio.ini index 39b5dfbd4..217e0dd3a 100644 --- a/variants/nrf52840/ME25LS01-4Y10TD_e-ink/platformio.ini +++ b/variants/nrf52840/ME25LS01-4Y10TD_e-ink/platformio.ini @@ -16,7 +16,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ME25LS0 lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.8 + zinggjm/GxEPD2@1.6.9 ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) upload_protocol = nrfutil ;upload_port = /dev/ttyACM1 diff --git a/variants/nrf52840/MakePython_nRF52840_eink/platformio.ini b/variants/nrf52840/MakePython_nRF52840_eink/platformio.ini index ebea1ce97..d89ef348d 100644 --- a/variants/nrf52840/MakePython_nRF52840_eink/platformio.ini +++ b/variants/nrf52840/MakePython_nRF52840_eink/platformio.ini @@ -15,6 +15,6 @@ lib_deps = # renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.8 + zinggjm/GxEPD2@1.6.9 debug_tool = jlink ;upload_port = /dev/ttyACM4 \ No newline at end of file diff --git a/variants/nrf52840/TWC_mesh_v4/platformio.ini b/variants/nrf52840/TWC_mesh_v4/platformio.ini index c529caa0b..8f7479f74 100644 --- a/variants/nrf52840/TWC_mesh_v4/platformio.ini +++ b/variants/nrf52840/TWC_mesh_v4/platformio.ini @@ -9,5 +9,5 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/TWC_mes lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.8 + zinggjm/GxEPD2@1.6.9 debug_tool = jlink diff --git a/variants/nrf52840/heltec_mesh_node_t114/platformio.ini b/variants/nrf52840/heltec_mesh_node_t114/platformio.ini index c9f998240..77beb4d33 100644 --- a/variants/nrf52840/heltec_mesh_node_t114/platformio.ini +++ b/variants/nrf52840/heltec_mesh_node_t114/platformio.ini @@ -23,4 +23,4 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_ lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-st7789 packageName=https://github.com/meshtastic/st7789 gitBranch=main - https://github.com/meshtastic/st7789/archive/92bae2e4a307afb430c3b0bc3d661c55ee1565f0.zip + https://github.com/meshtastic/st7789/archive/5180423ae2dbf5885168a8bfb308c7fb7eff6930.zip diff --git a/variants/nrf52840/heltec_mesh_node_t114/variant.h b/variants/nrf52840/heltec_mesh_node_t114/variant.h index e7385c4bb..509f749a8 100644 --- a/variants/nrf52840/heltec_mesh_node_t114/variant.h +++ b/variants/nrf52840/heltec_mesh_node_t114/variant.h @@ -57,9 +57,6 @@ extern "C" { #define TFT_OFFSET_X 0 #define TFT_OFFSET_Y 0 -// T114 gets a muted yellow on black display -#define TFT_MESH_OVERRIDE COLOR565(255, 255, 128) - // #define TFT_OFFSET_ROTATION 0 // #define SCREEN_ROTATE // #define SCREEN_TRANSITION_FRAMERATE 5 diff --git a/variants/nrf52840/heltec_mesh_solar/platformio.ini b/variants/nrf52840/heltec_mesh_solar/platformio.ini index 1b6f59a68..ae68455dc 100644 --- a/variants/nrf52840/heltec_mesh_solar/platformio.ini +++ b/variants/nrf52840/heltec_mesh_solar/platformio.ini @@ -132,4 +132,4 @@ build_flags = ${heltec_mesh_solar_base.build_flags} lib_deps = ${heltec_mesh_solar_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-st7789 packageName=https://github.com/meshtastic/st7789 gitBranch=main - https://github.com/meshtastic/st7789/archive/92bae2e4a307afb430c3b0bc3d661c55ee1565f0.zip + https://github.com/meshtastic/st7789/archive/5180423ae2dbf5885168a8bfb308c7fb7eff6930.zip diff --git a/variants/nrf52840/nrf52.ini b/variants/nrf52840/nrf52.ini index f42c29308..d11f4fc56 100644 --- a/variants/nrf52840/nrf52.ini +++ b/variants/nrf52840/nrf52.ini @@ -7,7 +7,7 @@ extends = arduino_base platform_packages = ; our custom Git version with C++17 support in platform.txt # TODO renovate - platformio/framework-arduinoadafruitnrf52 @ https://github.com/meshtastic/Adafruit_nRF52_Arduino#cpp17-platform + platformio/framework-arduinoadafruitnrf52 @ https://github.com/meshtastic/Adafruit_nRF52_Arduino#master ; Don't renovate toolchain-gccarmnoneeabi platformio/toolchain-gccarmnoneeabi@~1.90301.0 diff --git a/variants/nrf52840/rak4631_epaper/platformio.ini b/variants/nrf52840/rak4631_epaper/platformio.ini index f71fb6301..c970baddd 100644 --- a/variants/nrf52840/rak4631_epaper/platformio.ini +++ b/variants/nrf52840/rak4631_epaper/platformio.ini @@ -16,7 +16,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak4631 lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.8 + zinggjm/GxEPD2@1.6.9 # renovate: datasource=custom.pio depName=Melopero RV3028 packageName=melopero/library/Melopero RV3028 melopero/Melopero RV3028@1.2.0 # renovate: datasource=custom.pio depName=RAK NCP5623 RGB LED packageName=rakwireless/library/RAKwireless NCP5623 RGB LED library diff --git a/variants/nrf52840/rak4631_epaper_onrxtx/platformio.ini b/variants/nrf52840/rak4631_epaper_onrxtx/platformio.ini index 670b2c415..af57ea3cd 100644 --- a/variants/nrf52840/rak4631_epaper_onrxtx/platformio.ini +++ b/variants/nrf52840/rak4631_epaper_onrxtx/platformio.ini @@ -18,7 +18,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak4631 lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.8 + zinggjm/GxEPD2@1.6.9 # renovate: datasource=custom.pio depName=Melopero RV3028 packageName=melopero/library/Melopero RV3028 melopero/Melopero RV3028@1.2.0 # renovate: datasource=custom.pio depName=RAK NCP5623 RGB LED packageName=rakwireless/library/RAKwireless NCP5623 RGB LED library