Compare commits

..

27 Commits

Author SHA1 Message Date
pelgraine 4ba130ccfa update readme for v1.8; added fix: robust SPIFFS recovery when auto-format fails
SPIFFS.begin(true) auto-formats on mount failure, but the auto-format
itself can fail if the partition contains residual data from a previous
firmware (e.g. stock LilyGo, Meshtastic, or MeshCore with a different
partition layout). When that happens the firmware previously printed
"SPIFFS format FAILED!" and continued in a broken state with no
persistence.

Now on auto-format failure:
1. Find the SPIFFS partition via esp_partition_find_first()
2. Erase it completely with esp_partition_erase_range()
3. Call SPIFFS.format() + SPIFFS.begin(false) with up to 3 retries

Added #include <esp_partition.h> under ESP32 guard.

The existing first-boot display feedback ("Formatting storage...
First boot - please wait") is unchanged -- it fires on the initial
mount failure. The new partition erase code only triggers if the
auto-format also fails.
2026-05-03 21:22:07 +10:00
pelgraine c2bfc3c985 Add true power-off via BQ25896 BATFET disconnect
Hibernate (deep sleep) leaves the BQ25896 charger IC powered, drawing
~30-60uA quiescent from BAT. This adds a second option on the shutdown
page -- "power off" -- that writes the BATFET_DIS bit in BQ25896 REG09
to fully disconnect the battery from VSYS. Leakage drops to ~12-23uA
(IC internal only). Wake requires USB-C plug-in.

Shutdown page now shows two options with a cursor (up/down to toggle):
  > hibernate: long press/Enter   (T-Deck Pro)
    power off: long press/Enter
  > hibernate: long press          (T5S3 / other)
    power off: long press

Selecting "hibernate" triggers immediately (unchanged behaviour).
Selecting "power off" shows a confirmation prompt:
  power off device?
  usb-c to wake
  Enter:yes  q:no

Power-off display suppresses the header (node name, clock, battery)
and shows only "powering off..." and "plug in USB-C to turn on".

The 's' key shortcut to settings is gated on the shutdown page so it
passes through to the hibernate/power-off selection toggle instead.
Both the TCA8418 handler (loop) and the broader handler
(handleKeyboardInput) are gated via isHomeOnShutdownPage().

PRESS_LABEL macro: removed dead UI_HAS_JOYSTICK branch (no Meck
device has a joystick), collapsed to a single #define "long press".
Joystick input polling block in loop() also removed (dead code behind
#if UI_HAS_JOYSTICK, never compiled for any Meck build).

BQ25896 I2C sequence follows TI recommendation (E2E forum, Jeff/TI):
  1. Read REG09
  2. Write BATFET_DLY=1 (bit 3) -- delays disconnect so I2C completes
  3. Write BATFET_DIS=1 | BATFET_DLY=1 (bits 5+3) -- last I2C write
The write happens after display turnOff but before board powerOff, so
I2C pull-ups on VDD3V3 are still alive. Board enters deep sleep, then
BATFET opens after tSM_DLY (~10-15s). Skipping the delay risks leaving
the BQ25896 I2C engine in an undefined state that can prevent wake on
USB-C plug-in (device soft-brick requiring battery disconnect).

REG09 bit map (confirmed from Linux kernel bq25890_charger.c):
  Bit 7: FORCE_ICO
  Bit 6: TMR2X_EN
  Bit 5: BATFET_DIS  (0x20) -- disconnect battery
  Bit 4: JEITA_VSET
  Bit 3: BATFET_DLY  (0x08) -- delay before disconnect
  Bit 2: BATFET_RST_EN (0x04) -- QON wake (not wired on T-Deck Pro)
  Bit 1: PUMPX_UP
  Bit 0: PUMPX_DN

Schematic confirms QON (pin 12) has R4 10K pull-up to REGN with no
user-accessible button -- USB-C is the only wake path from ship mode.

Guarded by #ifdef I2C_ADDR_BQ25896 so it compiles on all platforms
but only activates on boards with the charger (T-Deck Pro, T5S3).

Files changed:
  UITask.h   -- _full_poweroff, setFullPowerOff(), isHomeOnShutdownPage()
  UITask.cpp -- shutdown page UI, input handling, BATFET write,
                PRESS_LABEL cleanup, joystick removal
  main.cpp   -- 's' key gated on shutdown page (both handlers)
2026-05-03 20:24:20 +10:00
pelgraine 00b3f66bc4 Merge branch 'dev' 2026-05-03 18:55:34 +10:00
pelgraine fb93b4e4ec update readme links 2026-05-03 18:55:03 +10:00
pelgraine a7b44de613 Went back to the previous method of markAllChannelsRead() for WIfi and BLE envs ie non-standalone devices so msgs marked read on device when app is connected 2026-05-03 18:40:28 +10:00
pelgraine 7ebafc39ee update firmware build date 2026-05-03 18:20:48 +10:00
pelgraine 97498e131d updated chunked save method so it doesn't occur when device is actively being used. The flow: user navigates (keypresses every ~200ms) → _lastUserInput stays fresh → userActive is true → save deferred. User stops navigating → 3 seconds pass → userActive goes false → chunked save starts/resumes, 20 contacts per loop iteration until done. 2026-05-02 10:44:17 +10:00
pelgraine 95c992d966 Hibernate now enters true deep sleep on T-Deck Pro.
UITask shutdown (already occurs/unchanged):
1. BLE disabled (_serial->disable()).
2. WiFi disconnected + WIFI_OFF.
3. 4G modem shutdown().
4. GPS power cut (PIN_GPS_EN LOW).
5. LoRa radio powerOff() (standby mode).
6. Display turnOff().

TDeckBoard::powerOff() (new):
7. btStop() -- BLE controller stop.
8. Peripheral power OFF (PIN_PERF_POWERON LOW) -- keyboard, BQ27220, sensors.
9. LoRa module power OFF (P_LORA_EN LOW) -- cuts power entirely.
10. Hold LoRa NSS high (prevents SX1262 drawing current from floating CS).
11. esp_deep_sleep_start() -- CPU halts, ~10-40uA.

Wake (reset button or USB power-on):
12. ESP32-S3 cold boots.
13. TDeckBoard::begin() runs: peripheral power ON, I2C init, LoRa power ON, NSS hold released.
14. App starts fresh -- prefs/contacts/messages load from flash.

No LoRa wake during hibernate -- the device is truly off. Only a hardware
reset (reset button) or USB power-on wakes the device.
2026-05-02 10:19:37 +10:00
pelgraine 9c4c374db8 added has meck fonts build flag to draft max platformio; new fast scroll implementation update with next page scroll function with shift+w/s; updated home page hint text to make it clearer you can press enter to toggle on/off/send adverts etc 2026-05-02 10:07:49 +10:00
pelgraine ca16af54cc renamed WIP variant project files to reflect WIP status appropriately 2026-05-02 09:29:26 +10:00
pelgraine f51d8be290 Fix to beer emoji mug handle; font changes that support diacritics meaning updates to display drivers, fonts, highlighting, sprites; build flag added to both tdpro and t5s3 pro to enable diacritics rendering changes 2026-05-02 09:26:30 +10:00
pelgraine 6c0c77d788 improved tdpro and t5s3 ble sync speed with large contact lists; updated firmware build date 2026-05-02 06:07:12 +10:00
pelgraine 715202849c initial successful t-echo card build 2026-04-30 18:13:01 +10:00
pelgraine 46ff5229e1 attempted to fix emojisprites override; added beer stein sprite; renamed t-echo card folder 2026-04-30 16:49:25 +10:00
pelgraine 64afdb2829 updated max frame size in base serial interface to match upstream meshcore; fix getbatterypercent in abstractuitask; update renderbatteryindicator et al in uitask; change lock screen refresh to 1 min for td pro and 2 min to t5s3; fix issue in t5s3 where it was still showing lock screen in hibernating mode'; fixed cardkb bug caused by t-echo lite wip work 2026-04-30 00:20:00 +10:00
pelgraine 26d54074c8 update firmware version and build date 2026-04-27 08:30:49 +10:00
pelgraine b5b11a2036 T-Echo Card: apply hardware findings from Meshtastic PR #10267.
Incorporates hardware-specific learnings from caveman99's Meshtastic
T-Echo Card support PR (meshtastic/firmware#10267), cross-referenced
against LilyGo's official t_echo_card_config.h pinmap.

Battery (critical):
- Add BATTERY_MEASUREMENT_CONTROL pin P0.31 — gates the resistive
  voltage divider feeding AIN0. Without toggling this pin, battery
  ADC reads are invalid (floating divider input).
- TechoCardBoard::getBatteryVoltage() now drives P0.31 HIGH before
  SAADC read and LOW after to avoid parasitic drain.

Display (critical):
- Add OLED_DISPLAY_OFFSET = 24 for the SSD1315 72×40 panel. The
  physical display is mapped at GDDRAM pages 3–7 (rows 24–63);
  SETDISPLAYOFFSET and SETMULTIPLEX commands are sent after
  display.begin() in target.cpp to shift the visible window.

Power rail (high priority):
- RT9080 3V3 LDO now gets a clean HIGH→LOW→HIGH reset cycle with
  100ms dwell in board.begin(), preventing brown-out when LoRa TX
  fires at +22 dBm after a soft reset.
2026-04-25 19:35:44 +10:00
pelgraine 964606a018 Update README.md
Updated discord channel link
2026-04-23 18:29:00 +10:00
pelgraine a8d65aa3ba Update README.md
Fixed links for discord and Meshcore flasher
2026-04-23 18:23:34 +10:00
pelgraine e74aa3b214 t-echo lite - attempt to fix debounce and emoji gibberish 2026-04-22 15:12:20 +10:00
pelgraine 36976e0029 t-echo lite - stripped emoji in contacts and last heard, changed recent adverts list to default 4 2026-04-22 12:54:16 +10:00
pelgraine f461777214 t-echo lite screen: removed diag diagnostic prints, sorted compose mode with cardkb, fixed enter & esc handlers; increased e-ink offset for home screen centering; condensed footer text for all screens; datastore chunked saved guarded for esp32 ; still encountering memory problems with ble build even w 250 contacts and 10 chanel message history so trying standalone 2026-04-21 22:43:29 +10:00
pelgraine 7e1009f31c various t-echo light screen improvements including refresh time to 15s 2026-04-21 17:40:50 +10:00
pelgraine db0ecd3c58 initial screen based t-echo lite with card kb support build 2026-04-21 14:07:06 +10:00
pelgraine 291c42a40e fix ota for remote wifi repeaters 2026-04-19 22:32:31 +10:00
pelgraine 88a56d3ff5 update meck remote repeater builds to ensure consistency with region defaults and loop detect 2026-04-19 22:11:15 +10:00
pelgraine 4ec5d17402 update remote repeater firmware files to enable loop detection and regions 2026-04-19 22:04:31 +10:00
76 changed files with 7485 additions and 1605 deletions
+23 -7
View File
@@ -2,7 +2,7 @@
A fork created specifically to focus on enabling BLE & WiFi companion firmware for the LilyGo T-Deck Pro & LilyGo T5 E-Paper S3 Pro. Created wholly with Claude AI using Meshcore v1.11 code. 100% vibecoded.
[Check out the Meck discussion channel on the MeshCore Discord](https://discord.com/channels/1343693475589263471/1460136499390447670)
[Check out the Meck discussion channel on the MeshCore Discord](https://discord.com/channels/1495203904898728149/1496789639556501614)
<img src="https://github.com/user-attachments/assets/b30ce6bd-79af-44d3-93c4-f5e7e21e5621" alt="IMG_1453" width="300" height="650">
@@ -39,6 +39,7 @@ A fork created specifically to focus on enabling BLE & WiFi companion firmware f
- [Alarm Clock (Audio only)](#alarm-clock-audio-only)
- [Voice Notes Over LoRa (Audio only)](#voice-notes-over-lora-audio-only)
- [Lock Screen (T-Deck Pro)](#lock-screen-t-deck-pro)
- [Shutdown (T-Deck Pro)](#shutdown-t-deck-pro)
- [Remote Repeater (T-Deck Pro 4G)](#remote-repeater-t-deck-pro-4g)
- [WiFi Repeater](#wifi-repeater)
- [T5S3 E-Paper Pro](#t5s3-e-paper-pro)
@@ -120,7 +121,7 @@ On macOS the port is typically `/dev/cu.usbmodem*`. On Windows it will be a COM
**Using the MeshCore Flasher (web-based, T-Deck Pro only):**
1. Go to https://flasher.meshcore.co.uk
1. Go to https://flasher.meshcore.io
2. Select **Custom Firmware**
3. Select the **merged** `.bin` file you downloaded
4. Click **Flash**, select your device in the popup, and click **Connect**
@@ -140,6 +141,8 @@ esptool.py --chip esp32s3 --port /dev/ttyACM0 --baud 921600 \
> **Tip:** If you're unsure whether the device already has a bootloader, it's always safe to use the merged file and flash at `0x0` — it will overwrite everything cleanly.
> **First boot:** On a fresh flash, the device will format its internal storage partition. The display shows "Formatting storage... First boot - please wait" — this takes 1-2 minutes and only happens once. If the device was previously running different firmware (e.g. stock LilyGo or Meshtastic), the partition is automatically erased and reformatted to ensure a clean start.
### Launcher
If you're loading firmware from an SD card via the LilyGo Launcher firmware, use the **non-merged** `.bin` file. The Launcher provides its own bootloader and only needs the application image.
@@ -343,6 +346,7 @@ Flood-based hop estimates (`~D`, `~N`) are drawn from a cache of up to 1,000 rec
| Key | Action |
|-----|--------|
| W / S | Scroll up / down through contacts |
| Shift+W / Shift+S | Page up / page down |
| A / D | Cycle filter: All → Chat → Rptr → Room → Sens → Fav |
| Enter | Enter select mode (highlights current contact, enables batch operations) |
| P | Open path editor for the highlighted contact |
@@ -433,6 +437,7 @@ Press **S** from the home screen to open settings. On first boot (when the devic
| Key | Action |
|-----|--------|
| W / S | Navigate up / down through settings |
| Shift+W / Shift+S | Page up / page down |
| Enter | Edit selected setting, or enter a sub-screen |
| Q | Back one level (sub-screen → top level → home screen) |
@@ -491,6 +496,8 @@ Change the font in **Settings → Font** — use A/D to cycle with a live previe
Font styles are available in both Tiny and Larger text size modes. Custom fonts at Tiny size use 7pt glyphs; at Larger size, 9pt — matching the existing FreeSans layout. The font preference is saved and persists across reboots.
**Accented character support (v1.8+):** All three font styles now display accented and diacritical characters (Czech, Polish, French, German, etc.) instead of dropping them. **Noto Sans at Larger text size** renders diacritical marks natively (carons, accents, cedillas). All other font and size combinations fold accented characters to their ASCII base letter (ě→e, ž→z, ñ→n) — the letter is always visible, just without the diacritic mark.
### Compose Mode
| Key | Action |
@@ -527,7 +534,7 @@ Press the **Sym** key then the letter key to enter numbers and symbols:
### Emoji Picker
While in compose mode, press the **$** key to open the emoji picker. A scrollable grid of 76 emoji is displayed in a 5-column layout, with faces and emotions grouped first. Scrolling wraps around — pressing W on the first row goes to the last row and vice versa.
While in compose mode, press the **$** key to open the emoji picker. A scrollable grid of 77 emoji is displayed in a 5-column layout, with faces and emotions grouped first. Scrolling wraps around — pressing W on the first row goes to the last row and vice versa.
| Key | Action |
|-----|--------|
@@ -641,6 +648,10 @@ Double-click the Boot button again to unlock and return to whatever screen you w
An auto-lock timer can be configured in **Settings → Auto Lock** (None / 2 / 5 / 10 / 15 / 30 minutes of idle time).
### Shutdown (T-Deck Pro)
The home screen includes a **Shutdown** page. Selecting it powers the device off completely — the ESP32-S3 enters deep sleep with no wake sources, peripheral power is cut, and the LoRa module is powered down. Only a hardware reset (reset button) or USB power-on will wake the device. This is distinct from the auto-lock hibernate, which maintains wake-on-LoRa capability.
---
## Remote Repeater (T-Deck Pro 4G)
@@ -762,7 +773,7 @@ The virtual keyboard supports:
- QWERTY letter layout with a symbol/number layer (tap the **123** key to switch)
- Shift toggle for uppercase
- Backspace (UTF-8 aware — correctly deletes multi-byte emoji) and Enter keys
- **Emoji picker** — tap the **$** key to open a scrollable 8-column grid of 76 emoji sprites with page indicators. Tap an emoji to insert it inline in your message. Tap **Back** to return to the keyboard. Faces and emotions are grouped first for quick access.
- **Emoji picker** — tap the **$** key to open a scrollable 8-column grid of 77 emoji sprites with page indicators. Tap an emoji to insert it inline in your message. Tap **Back** to return to the keyboard. Faces and emotions are grouped first for quick access.
- Inline emoji rendering — emoji appear as pixel sprites in the text field as you type
- Phantom keystroke prevention (a brief cooldown after the keyboard opens prevents accidental taps)
@@ -1021,7 +1032,7 @@ The companion firmware can be connected to via BLE (T-Deck Pro and T5S3 BLE vari
## 🛠 Hardware Compatibility
MeshCore is designed for devices listed in the [MeshCore Flasher](https://flasher.meshcore.co.uk). Meck specifically targets the LilyGo T-Deck Pro, LilyGo T5S3 E-Paper Pro, Heltec V3 (remote repeater only), and Heltec V4 (remote repeater only).
MeshCore is designed for devices listed in the [MeshCore Flasher](https://flasher.meshcore.io). Meck specifically targets the LilyGo T-Deck Pro, LilyGo T5S3 E-Paper Pro, Heltec V3 (remote repeater only), and Heltec V4 (remote repeater only).
## Contributing
@@ -1068,8 +1079,12 @@ There are a number of fairly major features in the pipeline, with no particular
- [X] Channel picker screen with unread badges
- [X] Region scope (MeshCore v1.15+ compatibility)
- [X] Selectable font styles (Classic, Noto Sans, Montserrat)
- [X] Expanded emoji picker (76 emoji, reordered, wrap scrolling)
- [X] Expanded emoji picker (77 emoji, reordered, wrap scrolling)
- [X] 1,000-entry advert path cache (PSRAM)
- [X] Accented character / diacritics support (Czech, Polish, French, German, Latin Extended)
- [X] Page scroll (Shift+W/S) on all list screens
- [X] True power off (deep sleep, no wake sources)
- [X] BLE 2M PHY, DLE, and faster write interval
- [ ] Fix M4B rendering to enable chaptered audiobook playback
- [ ] Better JPEG and PNG decoding
- [ ] Improve EPUB rendering and EPUB format handling
@@ -1103,6 +1118,7 @@ There are a number of fairly major features in the pipeline, with no particular
- [X] Region scope (MeshCore v1.15+ compatibility)
- [X] Selectable font styles (Classic, Noto Sans, Montserrat)
- [X] Virtual keyboard emoji grid with scrollable pages
- [X] Accented character / diacritics support (Czech, Polish, French, German, Latin Extended)
- [ ] Improve EPUB rendering and EPUB format handling
**Heltec V4:**
@@ -1118,7 +1134,7 @@ There are a number of fairly major features in the pipeline, with no particular
## 📞 Get Support
- Join [MeshCore Discord](https://discord.gg/BMwCtwHj5V) to chat with the developers and get help from the community.
- Join [MeshCore Discord](https://discord.gg/KWFeY45sN) to chat with the developers and get help from the community.
## 📜 License
+23 -2
View File
@@ -8,7 +8,28 @@
"extra_flags": [
"-DARDUINO_NRF52840_TECHO_CARD",
"-DNRF52840_XXAA",
"-DNRF52_SERIES"
"-DNRF52_SERIES",
"-DUSE_LFXO",
"-DLED_BUILTIN=39",
"-DLED_BLUE=39",
"-DLED_RED=39",
"-DLED_STATE_ON=1",
"-DPIN_WIRE_SDA=36",
"-DPIN_WIRE_SCL=34",
"-DWIRE_INTERFACES_COUNT=1",
"-DPIN_SPI_MISO=17",
"-DPIN_SPI_SCK=13",
"-DPIN_SPI_MOSI=15",
"-DSPI_INTERFACES_COUNT=1",
"-DPIN_SERIAL1_RX=21",
"-DPIN_SERIAL1_TX=19",
"-DPIN_A0=2"
],
"f_cpu": "64000000L",
"hwids": [["0x239A", "0x8029"]],
@@ -45,4 +66,4 @@
},
"url": "https://github.com/Xinyuan-LilyGO/T-Echo-Card",
"vendor": "LILYGO"
}
}
@@ -36,6 +36,7 @@ public:
void setHasConnection(bool connected) { _connected = connected; }
bool hasConnection() const { return _connected; }
uint16_t getBattMilliVolts() const { return _board->getBattMilliVolts(); }
uint8_t getBatteryPercent() const { return _board->getBatteryPercent(); }
bool isSerialEnabled() const { return _serial->isEnabled(); }
void enableSerial() { _serial->enable(); }
void disableSerial() { _serial->disable(); }
@@ -50,6 +51,7 @@ public:
// Mark a channel as read when BLE companion app syncs a message
virtual void markChannelReadFromBLE(uint8_t channel_idx) {}
virtual void markAllChannelsRead() {} // Companion builds: zero all unread on app connect
// Repeater admin callbacks (from MyMesh)
virtual void onAdminLoginResult(bool success, uint8_t permissions, uint32_t server_time) {}
+54 -98
View File
@@ -274,9 +274,6 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
if (file.read((uint8_t *)&_prefs.large_font, sizeof(_prefs.large_font)) != sizeof(_prefs.large_font)) {
_prefs.large_font = 0; // default: tiny font
}
if (file.read((uint8_t *)&_prefs.ui_font_style, sizeof(_prefs.ui_font_style)) != sizeof(_prefs.ui_font_style)) {
_prefs.ui_font_style = 0; // default: Classic (FreeSans)
}
if (file.read((uint8_t *)&_prefs.tx_fail_reset_threshold, sizeof(_prefs.tx_fail_reset_threshold)) != sizeof(_prefs.tx_fail_reset_threshold)) {
_prefs.tx_fail_reset_threshold = 3; // default: 3
}
@@ -284,20 +281,11 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
_prefs.rx_fail_reboot_threshold = 3; // default: 3
}
// v1.7+ Meck region scope fields — may not exist in older prefs files
if (file.read((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)) != sizeof(_prefs.default_scope_name)) {
memset(_prefs.default_scope_name, 0, sizeof(_prefs.default_scope_name)); // default: unscoped
}
if (file.read((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)) != sizeof(_prefs.default_scope_key)) {
memset(_prefs.default_scope_key, 0, sizeof(_prefs.default_scope_key)); // default: null key
}
// Clamp to valid ranges
if (_prefs.dark_mode > 1) _prefs.dark_mode = 0;
if (_prefs.portrait_mode > 1) _prefs.portrait_mode = 0;
if (_prefs.hint_shown > 1) _prefs.hint_shown = 0;
if (_prefs.large_font > 1) _prefs.large_font = 0;
if (_prefs.ui_font_style >= 3) _prefs.ui_font_style = 0;
if (_prefs.tx_fail_reset_threshold > 10) _prefs.tx_fail_reset_threshold = 3;
if (_prefs.rx_fail_reboot_threshold > 10) _prefs.rx_fail_reboot_threshold = 3;
// auto_lock_minutes: only accept known options (0, 2, 5, 10, 15, 30)
@@ -354,11 +342,8 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
file.write((uint8_t *)&_prefs.auto_lock_minutes, sizeof(_prefs.auto_lock_minutes)); // 100
file.write((uint8_t *)&_prefs.hint_shown, sizeof(_prefs.hint_shown)); // 101
file.write((uint8_t *)&_prefs.large_font, sizeof(_prefs.large_font)); // 102
file.write((uint8_t *)&_prefs.ui_font_style, sizeof(_prefs.ui_font_style)); // 103
file.write((uint8_t *)&_prefs.tx_fail_reset_threshold, sizeof(_prefs.tx_fail_reset_threshold)); // 104
file.write((uint8_t *)&_prefs.rx_fail_reboot_threshold, sizeof(_prefs.rx_fail_reboot_threshold)); // 105
file.write((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)); // 106
file.write((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); // 137
file.write((uint8_t *)&_prefs.tx_fail_reset_threshold, sizeof(_prefs.tx_fail_reset_threshold)); // 103
file.write((uint8_t *)&_prefs.rx_fail_reboot_threshold, sizeof(_prefs.rx_fail_reboot_threshold)); // 104
file.close();
}
@@ -445,10 +430,42 @@ void DataStore::loadContacts(DataStoreHost* host) {
void DataStore::saveContacts(DataStoreHost* host) {
FILESYSTEM* fs = _getContactsChannelsFS();
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
// nRF52/STM32: direct write (no tmp+rename — InternalFS doesn't need atomic pattern)
File file = openWrite(fs, "/contacts3");
if (file) {
uint32_t idx = 0;
ContactInfo c;
uint8_t unused = 0;
uint32_t recordsWritten = 0;
while (host->getContactForSave(idx, c)) {
bool success = (file.write(c.id.pub_key, 32) == 32);
success = success && (file.write((uint8_t *)&c.name, 32) == 32);
success = success && (file.write(&c.type, 1) == 1);
success = success && (file.write(&c.flags, 1) == 1);
success = success && (file.write(&unused, 1) == 1);
success = success && (file.write((uint8_t *)&c.sync_since, 4) == 4);
success = success && (file.write((uint8_t *)&c.out_path_len, 1) == 1);
success = success && (file.write((uint8_t *)&c.last_advert_timestamp, 4) == 4);
success = success && (file.write(c.out_path, 64) == 64);
success = success && (file.write((uint8_t *)&c.lastmod, 4) == 4);
success = success && (file.write((uint8_t *)&c.gps_lat, 4) == 4);
success = success && (file.write((uint8_t *)&c.gps_lon, 4) == 4);
if (!success) break;
recordsWritten++;
idx++;
}
file.close();
Serial.printf("DataStore: saved %d contacts\n", recordsWritten);
}
#else
// ESP32: atomic tmp+rename pattern (protects against SD card corruption on power loss)
const char* finalPath = "/contacts3";
const char* tmpPath = "/contacts3.tmp";
// --- Step 1: Write all contacts to a temporary file ---
File file = openWrite(fs, tmpPath);
if (!file) {
Serial.println("DataStore: saveContacts FAILED — cannot open tmp file");
@@ -487,9 +504,8 @@ void DataStore::saveContacts(DataStoreHost* host) {
file.close();
// --- Step 2: Verify the write completed ---
// Reopen read-only to get true on-disk size (SPIFFS file.size() is unreliable before close)
size_t expectedBytes = recordsWritten * 152; // 152 bytes per contact record
// Verify the write completed
size_t expectedBytes = recordsWritten * 152;
File verify = openRead(fs, tmpPath);
size_t bytesWritten = verify ? verify.size() : 0;
if (verify) verify.close();
@@ -497,23 +513,25 @@ void DataStore::saveContacts(DataStoreHost* host) {
if (!writeOk || bytesWritten != expectedBytes) {
Serial.printf("DataStore: saveContacts ABORTED — wrote %d bytes, expected %d (%d records)\n",
(int)bytesWritten, (int)expectedBytes, recordsWritten);
fs->remove(tmpPath); // Clean up failed tmp file
return; // Original /contacts3 is untouched
fs->remove(tmpPath);
return;
}
// --- Step 3: Replace original with verified temp file ---
// Replace original with verified temp file
fs->remove(finalPath);
if (fs->rename(tmpPath, finalPath)) {
Serial.printf("DataStore: saved %d contacts (%d bytes)\n", recordsWritten, (int)bytesWritten);
} else {
// Rename failed — tmp file still has the good data
Serial.println("DataStore: rename failed, tmp file preserved");
}
#endif
}
// =========================================================================
// Chunked contact save — non-blocking across multiple loop iterations
// Only for ESP32 with SD card — nRF52 uses blocking saveContacts() above
// =========================================================================
#if !defined(NRF52_PLATFORM) && !defined(STM32_PLATFORM)
bool DataStore::beginSaveContacts(DataStoreHost* host) {
if (_saveInProgress) return false; // Already saving
@@ -605,62 +623,14 @@ void DataStore::finishSaveContacts() {
Serial.println("DataStore: rename failed, tmp file preserved");
}
}
#endif // !NRF52_PLATFORM && !STM32_PLATFORM
void DataStore::loadChannels(DataStoreHost* host) {
FILESYSTEM* fs = _getContactsChannelsFS();
// Crash recovery (same pattern as contacts)
if (!fs->exists("/channels3") && fs->exists("/channels3.tmp")) {
Serial.println("DataStore: recovering channels3 from .tmp file");
fs->rename("/channels3.tmp", "/channels3");
}
if (fs->exists("/channels3.tmp")) {
fs->remove("/channels3.tmp");
}
// Try channels3 (new format with scope_name) first
if (fs->exists("/channels3")) {
File file = openRead(fs, "/channels3");
if (file) {
bool full = false;
uint8_t channel_idx = 0;
while (!full) {
ChannelDetails ch;
memset(ch.scope_name, 0, sizeof(ch.scope_name));
uint8_t unused[4];
bool success = (file.read(unused, 4) == 4);
success = success && (file.read((uint8_t *)ch.name, 32) == 32);
success = success && (file.read((uint8_t *)ch.channel.secret, 32) == 32);
success = success && (file.read((uint8_t *)ch.scope_name, 31) == 31);
if (!success) break; // EOF
// Sanitize scope_name — reject if it contains non-region characters
// (catches garbage from uninitialised memory in early channels3 files)
ch.scope_name[30] = '\0'; // force null-terminate
for (int s = 0; ch.scope_name[s]; s++) {
char sc = ch.scope_name[s];
if (!((sc >= 'a' && sc <= 'z') || (sc >= '0' && sc <= '9') || sc == '-')) {
memset(ch.scope_name, 0, sizeof(ch.scope_name)); // invalid — clear
break;
}
}
if (host->onChannelLoaded(channel_idx, ch)) {
channel_idx++;
} else {
full = true;
}
}
file.close();
return; // channels3 loaded successfully
}
}
// Fall back to channels2 (legacy format without scope_name)
if (!fs->exists("/channels2") && fs->exists("/channels2.tmp")) {
Serial.println("DataStore: recovering channels2 from .tmp file");
Serial.println("DataStore: recovering channels from .tmp file");
fs->rename("/channels2.tmp", "/channels2");
}
if (fs->exists("/channels2.tmp")) {
@@ -673,7 +643,6 @@ void DataStore::loadChannels(DataStoreHost* host) {
uint8_t channel_idx = 0;
while (!full) {
ChannelDetails ch;
memset(ch.scope_name, 0, sizeof(ch.scope_name)); // default: no scope
uint8_t unused[4];
bool success = (file.read(unused, 4) == 4);
@@ -689,18 +658,13 @@ void DataStore::loadChannels(DataStoreHost* host) {
}
}
file.close();
// Migrate: save as channels3 and remove channels2
Serial.println("DataStore: migrating channels2 → channels3");
saveChannels(host);
fs->remove("/channels2");
}
}
void DataStore::saveChannels(DataStoreHost* host) {
FILESYSTEM* fs = _getContactsChannelsFS();
const char* finalPath = "/channels3";
const char* tmpPath = "/channels3.tmp";
const char* finalPath = "/channels2";
const char* tmpPath = "/channels2.tmp";
File file = openWrite(fs, tmpPath);
if (!file) {
@@ -718,7 +682,6 @@ void DataStore::saveChannels(DataStoreHost* host) {
bool success = (file.write(unused, 4) == 4);
success = success && (file.write((uint8_t *)ch.name, 32) == 32);
success = success && (file.write((uint8_t *)ch.channel.secret, 32) == 32);
success = success && (file.write((uint8_t *)ch.scope_name, 31) == 31);
if (!success) {
writeOk = false;
@@ -731,7 +694,7 @@ void DataStore::saveChannels(DataStoreHost* host) {
file.close();
// Reopen read-only to get true on-disk size (SPIFFS file.size() is unreliable before close)
size_t expectedBytes = channel_idx * 99; // 4 + 32 + 32 + 31 = 99 bytes per channel
size_t expectedBytes = channel_idx * 68; // 4 + 32 + 32 = 68 bytes per channel
File verify = openRead(fs, tmpPath);
size_t bytesWritten = verify ? verify.size() : 0;
if (verify) verify.close();
@@ -776,7 +739,7 @@ void DataStore::checkAdvBlobFile() {
}
void DataStore::migrateToSecondaryFS() {
// migrate old adv_blobs, contacts3 and channels3/channels2 files to secondary FS if they don't already exist
// migrate old adv_blobs, contacts3 and channels2 files to secondary FS if they don't already exist
if (!_fsExtra->exists("/adv_blobs")) {
if (_fs->exists("/adv_blobs")) {
File oldAdvBlobs = openRead(_fs, "/adv_blobs");
@@ -815,14 +778,10 @@ void DataStore::migrateToSecondaryFS() {
_fs->remove("/contacts3");
}
}
if (!_fsExtra->exists("/channels3") && !_fsExtra->exists("/channels2")) {
// Migrate channels3 (preferred) or channels2 (legacy) to secondary FS
const char* srcName = _fs->exists("/channels3") ? "/channels3"
: _fs->exists("/channels2") ? "/channels2"
: nullptr;
if (srcName) {
File oldFile = openRead(_fs, srcName);
File newFile = openWrite(_fsExtra, srcName);
if (!_fsExtra->exists("/channels2")) {
if (_fs->exists("/channels2")) {
File oldFile = openRead(_fs, "/channels2");
File newFile = openWrite(_fsExtra, "/channels2");
if (oldFile && newFile) {
uint8_t buf[64];
@@ -833,7 +792,7 @@ void DataStore::migrateToSecondaryFS() {
}
if (oldFile) oldFile.close();
if (newFile) newFile.close();
_fs->remove(srcName);
_fs->remove("/channels2");
}
}
// cleanup nodes which have been testing the extra fs, copy _main.id and new_prefs back to primary
@@ -876,9 +835,6 @@ void DataStore::migrateToSecondaryFS() {
if (_fs->exists("/contacts3")) {
_fs->remove("/contacts3");
}
if (_fs->exists("/channels3")) {
_fs->remove("/channels3");
}
if (_fs->exists("/channels2")) {
_fs->remove("/channels2");
}
+6 -5
View File
@@ -24,13 +24,15 @@ class DataStore {
void checkAdvBlobFile();
#endif
// Chunked save state
#if !defined(NRF52_PLATFORM) && !defined(STM32_PLATFORM)
// Chunked save state (ESP32 with SD card only)
File _saveFile;
DataStoreHost* _saveHost = nullptr;
uint32_t _saveIdx = 0;
uint32_t _saveRecordsWritten = 0;
bool _saveInProgress = false;
bool _saveWriteOk = true;
#endif
public:
DataStore(FILESYSTEM& fs, mesh::RTCClock& clock);
@@ -45,14 +47,13 @@ public:
void savePrefs(const NodePrefs& prefs, double node_lat, double node_lon);
void loadContacts(DataStoreHost* host);
void saveContacts(DataStoreHost* host);
#if !defined(NRF52_PLATFORM) && !defined(STM32_PLATFORM)
// Chunked save — splits contact write across multiple loop iterations
// to prevent blocking the main loop for 500ms+ on large contact lists.
// Call beginSaveContacts(), then saveContactsChunk() each loop until it
// returns false (done), then finishSaveContacts() to verify and commit.
bool beginSaveContacts(DataStoreHost* host);
bool saveContactsChunk(int batchSize = 20); // returns true if more to write
bool saveContactsChunk(int batchSize = 20);
void finishSaveContacts();
bool isSaveInProgress() const { return _saveInProgress; }
#endif
void loadChannels(DataStoreHost* host);
void saveChannels(DataStoreHost* host);
void migrateToSecondaryFS();
+127 -5
View File
@@ -114,6 +114,7 @@
#define DIRECT_SEND_PERHOP_FACTOR 6.0f
#define DIRECT_SEND_PERHOP_EXTRA_MILLIS 250
#define LAZY_CONTACTS_WRITE_DELAY 5000
#define USER_IDLE_SAVE_THRESHOLD 15000 // Defer saves until 15s after last keypress
#define PUBLIC_GROUP_PSK "izOH6cXN6mrJ5e26oRXNcg=="
@@ -154,6 +155,10 @@
#define AUTO_ADD_ROOM_SERVER (1 << 3) // 0x08 - auto-add Room Server (ADV_TYPE_ROOM)
#define AUTO_ADD_SENSOR (1 << 4) // 0x10 - auto-add Sensor (ADV_TYPE_SENSOR)
// All type bits combined (excludes overwrite flag)
#define AUTO_ADD_ALL_TYPES (AUTO_ADD_CHAT | AUTO_ADD_REPEATER | \
AUTO_ADD_ROOM_SERVER | AUTO_ADD_SENSOR)
void MyMesh::writeOKFrame() {
uint8_t buf[1];
buf[0] = RESP_CODE_OK;
@@ -1472,6 +1477,11 @@ void MyMesh::handleCmdFrame(size_t len) {
MESH_DEBUG_PRINTLN("App %s connected", app_name);
_iter_started = false; // stop any left-over ContactsIterator
#if defined(BLE_PIN_CODE) || defined(MECK_WIFI_COMPANION)
// Companion builds: mark all channels/DMs as read on app connect
if (_ui) _ui->markAllChannelsRead();
#endif
int i = 0;
out_frame[i++] = RESP_CODE_SELF_INFO;
out_frame[i++] = ADV_TYPE_CHAT; // what this node Advert identifies as (maybe node's pronouns too?? :-)
@@ -2550,6 +2560,20 @@ void MyMesh::checkCLIRescueCmd() {
Serial.printf(" apn: %s\n", modemManager.getAPN());
Serial.printf(" imei: %s\n", modemManager.getIMEI());
#endif
// Contact auto-add
{
const char* mode = (_prefs.manual_add_contacts & 1) == 0 ? "auto" :
(_prefs.autoadd_config & AUTO_ADD_ALL_TYPES) == 0 ? "manual" : "custom";
Serial.printf(" contacts: %s", mode);
if ((_prefs.manual_add_contacts & 1) != 0 && (_prefs.autoadd_config & AUTO_ADD_ALL_TYPES) != 0) {
Serial.printf(" [%s%s%s%s]",
(_prefs.autoadd_config & AUTO_ADD_CHAT) ? "C" : "",
(_prefs.autoadd_config & AUTO_ADD_REPEATER) ? "R" : "",
(_prefs.autoadd_config & AUTO_ADD_ROOM_SERVER) ? "S" : "",
(_prefs.autoadd_config & AUTO_ADD_SENSOR) ? "N" : "");
}
Serial.printf(" maxhops:%d\n", _prefs.autoadd_max_hops);
}
// Detect current preset
bool presetFound = false;
for (int i = 0; i < (int)NUM_RADIO_PRESETS; i++) {
@@ -2590,6 +2614,30 @@ void MyMesh::checkCLIRescueCmd() {
}
}
if (!chFound) Serial.println(" (none)");
// --- Contact auto-add settings ---
} else if (strcmp(key, "contact.mode") == 0) {
if ((_prefs.manual_add_contacts & 1) == 0) {
Serial.println(" > auto");
} else if ((_prefs.autoadd_config & AUTO_ADD_ALL_TYPES) == 0) {
Serial.println(" > manual");
} else {
Serial.println(" > custom");
}
} else if (strcmp(key, "contact.autoadd") == 0) {
Serial.printf(" > chat:%s rptr:%s room:%s sensor:%s overwrite:%s\n",
(_prefs.autoadd_config & AUTO_ADD_CHAT) ? "on" : "off",
(_prefs.autoadd_config & AUTO_ADD_REPEATER) ? "on" : "off",
(_prefs.autoadd_config & AUTO_ADD_ROOM_SERVER) ? "on" : "off",
(_prefs.autoadd_config & AUTO_ADD_SENSOR) ? "on" : "off",
(_prefs.autoadd_config & AUTO_ADD_OVERWRITE_OLDEST) ? "on" : "off");
} else if (strcmp(key, "contact.maxhops") == 0) {
if (_prefs.autoadd_max_hops == 0) {
Serial.println(" > 0 (no limit)");
} else {
Serial.printf(" > %d\n", _prefs.autoadd_max_hops);
}
} else {
Serial.printf(" Error: unknown key '%s' (try 'help')\n", key);
}
@@ -3049,6 +3097,60 @@ void MyMesh::checkCLIRescueCmd() {
Serial.println(" Error: backlight not available on this device");
#endif
// --- Contact auto-add settings ---
} else if (memcmp(config, "contact.mode ", 13) == 0) {
const char* val = &config[13];
if (strcmp(val, "auto") == 0) {
_prefs.manual_add_contacts &= ~1;
savePrefs();
Serial.println(" > auto-add all");
} else if (strcmp(val, "custom") == 0) {
_prefs.manual_add_contacts |= 1;
if ((_prefs.autoadd_config & AUTO_ADD_ALL_TYPES) == 0) {
_prefs.autoadd_config |= AUTO_ADD_ALL_TYPES;
}
savePrefs();
Serial.println(" > custom (use contact.autoadd to configure)");
} else if (strcmp(val, "manual") == 0) {
_prefs.manual_add_contacts |= 1;
_prefs.autoadd_config &= ~AUTO_ADD_ALL_TYPES;
savePrefs();
Serial.println(" > manual only (no auto-add)");
} else if (strcmp(val, "repeater-only") == 0) {
_prefs.manual_add_contacts |= 1;
_prefs.autoadd_config = (_prefs.autoadd_config & AUTO_ADD_OVERWRITE_OLDEST) | AUTO_ADD_REPEATER;
savePrefs();
Serial.println(" > auto-add repeaters only");
} else {
Serial.println(" Error: auto|custom|manual|repeater-only");
}
} else if (memcmp(config, "contact.autoadd ", 16) == 0) {
const char* rest = &config[16];
uint8_t bit = 0;
const char* val = NULL;
if (memcmp(rest, "chat ", 5) == 0) { bit = AUTO_ADD_CHAT; val = &rest[5]; }
else if (memcmp(rest, "repeater ", 9) == 0) { bit = AUTO_ADD_REPEATER; val = &rest[9]; }
else if (memcmp(rest, "room ", 5) == 0) { bit = AUTO_ADD_ROOM_SERVER; val = &rest[5]; }
else if (memcmp(rest, "sensor ", 7) == 0) { bit = AUTO_ADD_SENSOR; val = &rest[7]; }
else if (memcmp(rest, "overwrite ", 10) == 0) { bit = AUTO_ADD_OVERWRITE_OLDEST; val = &rest[10]; }
if (bit && val) {
if (strcmp(val, "on") == 0) { _prefs.autoadd_config |= bit; savePrefs(); Serial.println(" > OK"); }
else if (strcmp(val, "off") == 0) { _prefs.autoadd_config &= ~bit; savePrefs(); Serial.println(" > OK"); }
else { Serial.println(" Error: on|off"); }
} else {
Serial.println(" Error: chat|repeater|room|sensor|overwrite on|off");
}
} else if (memcmp(config, "contact.maxhops ", 16) == 0) {
int h = atoi(&config[16]);
if (h >= 0 && h <= 64) {
_prefs.autoadd_max_hops = (uint8_t)h;
savePrefs();
Serial.printf(" > maxhops = %d%s\n", h, h == 0 ? " (no limit)" : "");
} else {
Serial.println(" Error: 0-64 (0=no limit)");
}
} else {
Serial.printf(" Error: unknown setting '%s' (try 'help')\n", config);
}
@@ -3146,6 +3248,12 @@ void MyMesh::checkCLIRescueCmd() {
Serial.println(" set region none Clear default region (unscoped)");
Serial.println(" get channel.scope <i> Show scope for channel i");
Serial.println(" set channel.scope <i> <name|none>");
Serial.println("");
Serial.println(" Contact auto-add:");
Serial.println(" contact.mode auto|custom|manual|repeater-only");
Serial.println(" contact.autoadd Show type toggles");
Serial.println(" contact.autoadd <type> on|off chat|repeater|room|sensor|overwrite");
Serial.println(" contact.maxhops <0-64> Max hops for auto-add (0=no limit)");
#ifdef HAS_4G_MODEM
Serial.println("");
Serial.println(" 4G modem:");
@@ -3360,22 +3468,36 @@ void MyMesh::loop() {
}
// is there are pending dirty contacts write needed?
bool userActive = _lastUserInput && (millis() - _lastUserInput) < USER_IDLE_SAVE_THRESHOLD;
if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) {
if (_deferSaves) {
// Voice session receiving — push save forward to avoid SPI contention
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
// nRF52/STM32: blocking save (fast on internal flash, no chunking needed)
if (!_deferSaves && !userActive) {
_store->saveContacts(this);
dirty_contacts_expiry = 0;
} else {
dirty_contacts_expiry = futureMillis(2000);
}
#else
if (_deferSaves || userActive) {
// Voice session or active keyboard use -- push save forward
dirty_contacts_expiry = futureMillis(2000);
} else if (!_store->isSaveInProgress()) {
_store->beginSaveContacts(this);
dirty_contacts_expiry = 0;
}
#endif
}
// Drive chunked contact save — write a batch each loop iteration
if (_store->isSaveInProgress() && !_deferSaves) {
#if !defined(NRF52_PLATFORM) && !defined(STM32_PLATFORM)
// Drive chunked contact save -- write a batch each loop iteration
// Paused while user is actively pressing keys or voice session is receiving
if (_store->isSaveInProgress() && !_deferSaves && !userActive) {
if (!_store->saveContactsChunk(20)) { // 20 contacts per chunk (~3KB, ~30ms)
_store->finishSaveContacts(); // Done or error verify and commit
_store->finishSaveContacts(); // Done or error -- verify and commit
}
}
#endif
// Discovery scan timeout
if (_discoveryActive && millisHasNowPassed(_discoveryTimeout)) {
+7 -2
View File
@@ -8,11 +8,11 @@
#define FIRMWARE_VER_CODE 11
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "19 April 2026"
#define FIRMWARE_BUILD_DATE "3 May 2026"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "Meck v1.7"
#define FIRMWARE_VERSION "Meck v1.8"
#endif
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
@@ -160,6 +160,10 @@ public:
void setDeferSaves(bool defer) { _deferSaves = defer; }
bool isDeferSaves() const { return _deferSaves; }
// Notify that the user pressed a key — defers contact saves until idle.
// Call from main.cpp keyboard handler on every keypress.
void notifyUserInput() { _lastUserInput = millis(); }
// Repeater admin - UI-initiated operations
bool uiLoginToRepeater(uint32_t contact_idx, const char* password, uint32_t& est_timeout_ms);
bool uiSendCliCommand(uint32_t contact_idx, const char* command);
@@ -274,6 +278,7 @@ private:
VoiceEnvelopeHandler _voiceEnvHandler = nullptr;
mutable bool _forceNextImport = false;
bool _deferSaves = false;
unsigned long _lastUserInput = 0; // millis() of last keypress -- defer saves until idle
uint32_t pending_login;
uint32_t pending_status;
uint32_t pending_telemetry, pending_discovery; // pending _TELEMETRY_REQ
+4 -2
View File
@@ -86,8 +86,10 @@ struct NodePrefs { // persisted to file
#if defined(LilyGo_T5S3_EPaper_Pro)
return 0;
#else
// Custom 7pt fonts at textSize 0 use GFXfont (baseline rendering), not built-in
if (ui_font_style > 0 && !large_font) return -2;
// Custom 7pt fonts at textSize 0 use GFXfont (baseline rendering), not built-in.
// Offset 0: highlight starts at row top, covers 7pt ascenders without
// bleeding into the row above (-2 was calibrated for taller 9pt ascenders).
if (ui_font_style > 0 && !large_font) return 0;
return large_font ? -2 : 5;
#endif
}
+398 -19
View File
@@ -1,10 +1,13 @@
#include <Arduino.h> // needed for PlatformIO
#ifdef BLE_PIN_CODE
#if defined(ESP32) && defined(BLE_PIN_CODE)
#include <esp_bt.h> // for esp_bt_controller_mem_release (web reader WiFi)
#endif
#ifdef MECK_OTA_UPDATE
#if defined(ESP32) && defined(MECK_OTA_UPDATE)
#include <esp_ota_ops.h>
#endif
#ifdef ESP32
#include <esp_partition.h>
#endif
#include <Mesh.h>
#include "MyMesh.h"
#include "variant.h" // Board-specific defines (HAS_GPS, etc.)
@@ -890,6 +893,42 @@
}
#endif
// --- T-Echo Lite: CardKB keyboard, GxEPD2 e-ink, no touch ---
#if defined(LILYGO_TECHO_LITE)
#include "ContactsScreen.h"
#include "ChannelScreen.h"
#include "ChannelPickerScreen.h"
#include "SettingsScreen.h"
#include "RepeaterAdminScreen.h"
#include "DiscoveryScreen.h"
#include "LastHeardScreen.h"
#include "PathEditorScreen.h"
#ifdef MECK_CARDKB
#include "CardKBKeyboard.h"
static CardKBKeyboard cardkb;
static unsigned long lastCardKBProbe = 0;
#define CARDKB_PROBE_INTERVAL_MS 5000
#endif
#endif
// CardKB compose mode state — standalone so ANY variant with MECK_CARDKB gets these
#ifdef MECK_CARDKB
static bool ckbComposeMode = false;
static char ckbComposeBuf[138]; // 137 bytes max + null
static int ckbComposePos = 0;
static uint8_t ckbComposeChIdx = 0;
static bool ckbComposeDM = false;
static int ckbComposeDMIdx = -1;
static char ckbComposeDMName[32];
static unsigned long ckbLastKeystroke = 0;
static bool ckbComposeRefresh = false;
#define CKB_COMPOSE_DEBOUNCE 600
void drawCardKBCompose();
void sendCardKBMessage();
#endif
// Board-agnostic: CPU frequency scaling and AGC reset
CPUPowerManager cpuPower;
#define AGC_RESET_INTERVAL_MS 500
@@ -1834,7 +1873,7 @@ void setup() {
#elif defined(ESP32)
MESH_DEBUG_PRINTLN("setup() - ESP32 filesystem init - calling SPIFFS.begin()");
if (!SPIFFS.begin(false)) {
// First boot or corrupted partition format required (can take 1-2 minutes)
// First boot or corrupted partition -- format required (can take 1-2 minutes)
Serial.println("SPIFFS mount failed - formatting (this may take 1-2 minutes)...");
if (disp) {
disp->startFrame();
@@ -1846,7 +1885,27 @@ void setup() {
disp->endFrame();
}
if (!SPIFFS.begin(true)) {
Serial.println("SPIFFS format FAILED!");
// Auto-format failed -- partition likely contains non-SPIFFS data from
// a previous firmware. Erase the entire partition and retry.
Serial.println("SPIFFS auto-format failed -- erasing partition and retrying...");
const esp_partition_t* spiffsPart = esp_partition_find_first(
ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, NULL);
if (spiffsPart) {
Serial.printf("SPIFFS partition: offset=0x%x size=0x%x -- erasing...\n",
spiffsPart->address, spiffsPart->size);
esp_partition_erase_range(spiffsPart, 0, spiffsPart->size);
}
bool spiffsMounted = false;
for (int attempt = 0; attempt < 3 && !spiffsMounted; attempt++) {
if (attempt > 0) { Serial.printf("SPIFFS retry %d/3...\n", attempt + 1); delay(500); }
SPIFFS.format();
spiffsMounted = SPIFFS.begin(false);
}
if (spiffsMounted) {
Serial.println("SPIFFS mounted after explicit format");
} else {
Serial.println("ERROR: SPIFFS mount failed after all retries");
}
} else {
Serial.println("SPIFFS format complete");
}
@@ -2020,7 +2079,8 @@ void setup() {
// IMPORTANT: sensors.begin() calls initBasicGPS() which steals the GPS pins for Serial1.
// We must end Serial1 first, then reclaim the pins for Serial2 (which feeds gpsStream).
#if HAS_GPS
// This is ESP32-specific — on nRF52, GPS Serial1 is initialised in radio_init().
#if HAS_GPS && defined(ESP32)
Serial1.end(); // Release GPS pins from Serial1's UART + ISR
Serial2.end(); // Close any existing Serial2
{
@@ -2089,9 +2149,11 @@ void setup() {
#endif
// Initialize CardKB external keyboard (if connected via QWIIC)
#if defined(LilyGo_T5S3_EPaper_Pro) && defined(MECK_CARDKB)
#if defined(MECK_CARDKB)
if (cardkb.begin()) {
#if defined(LilyGo_T5S3_EPaper_Pro)
ui_task.setCardKBDetected(true);
#endif
Serial.println("setup() - CardKB detected at 0x5F");
} else {
Serial.println("setup() - CardKB not detected (will re-probe)");
@@ -2317,8 +2379,10 @@ void setup() {
the_mesh.setVoiceEnvelopeHandler(voiceEnvelopeCallback);
#endif
Serial.printf("setup() complete — free heap: %d, largest block: %d\n",
#ifdef ESP32
Serial.printf("setup() complete - free heap: %d, largest block: %d\n",
ESP.getFreeHeap(), ESP.getMaxAllocHeap());
#endif
MESH_DEBUG_PRINTLN("=== setup() - COMPLETE ===");
}
@@ -2388,6 +2452,7 @@ void loop() {
// #endif
// Map screen: periodically update own GPS position and contact markers
#ifdef DISPLAY_CLASS
#if HAS_GPS
if (ui_task.isOnMapScreen()) {
static unsigned long lastMapUpdate = 0;
@@ -2413,6 +2478,7 @@ void loop() {
}
}
#endif
#endif
// CPU frequency auto-timeout back to idle
cpuPower.loop();
@@ -2846,6 +2912,16 @@ void loop() {
#ifdef HAS_4G_MODEM
smsMode = ui_task.isOnSMSScreen();
#endif
#elif defined(MECK_CARDKB)
if (!ckbComposeMode) {
ui_task.loop();
} else {
// Compose mode: debounced rendering
if (ckbComposeRefresh && (millis() - ckbLastKeystroke) >= CKB_COMPOSE_DEBOUNCE) {
drawCardKBCompose();
ckbComposeRefresh = false;
}
}
#else
ui_task.loop();
#endif
@@ -3130,12 +3206,12 @@ void loop() {
#endif // MECK_TOUCH_ENABLED
// ---------------------------------------------------------------------------
// CardKB external keyboard polling (T5S3 only, via QWIIC)
// CardKB external keyboard polling (via QWIIC)
// When VKB is active: typed characters feed into the VKB text buffer.
// When VKB is not active: navigation keys route through injectKey().
// ESC key maps to 'q' (back) when no VKB is active.
// ---------------------------------------------------------------------------
#if defined(LilyGo_T5S3_EPaper_Pro) && defined(MECK_CARDKB)
#if defined(MECK_CARDKB)
{
// Hot-plug detection: re-probe periodically
if (millis() - lastCardKBProbe >= CARDKB_PROBE_INTERVAL_MS) {
@@ -3143,7 +3219,9 @@ void loop() {
bool wasDetected = cardkb.isDetected();
bool nowDetected = cardkb.probe();
if (nowDetected != wasDetected) {
#if defined(LilyGo_T5S3_EPaper_Pro)
ui_task.setCardKBDetected(nowDetected);
#endif
Serial.printf("[CardKB] %s\n", nowDetected ? "Connected" : "Disconnected");
}
}
@@ -3151,15 +3229,58 @@ void loop() {
// Poll for keypress
char ckb = cardkb.readKey();
if (ckb != 0) {
// Block input while locked (same as touch)
Serial.printf("[CardKB] key=0x%02X '%c'\n", (uint8_t)ckb, (ckb >= 32 && ckb < 127) ? ckb : '?');
// --- CardKB compose mode: intercept ALL keys ---
if (ckbComposeMode) {
cpuPower.setBoost();
ui_task.keepAlive();
if (ckb == 0x1B) {
// ESC: cancel compose
ckbComposeMode = false;
ui_task.forceRefresh();
} else if (ckb == '\r') {
// Enter: send message
if (ckbComposePos > 0) {
sendCardKBMessage();
} else {
ckbComposeMode = false;
ui_task.forceRefresh();
}
} else if (ckb == '\b') {
// Backspace: delete last character
if (ckbComposePos > 0) {
ckbComposeBuf[--ckbComposePos] = '\0';
ckbComposeRefresh = true;
ckbLastKeystroke = millis();
}
} else if (ckb >= 32 && ckb < 127) {
// Printable character
if (ckbComposePos < 137) {
ckbComposeBuf[ckbComposePos++] = ckb;
ckbComposeBuf[ckbComposePos] = '\0';
ckbComposeRefresh = true;
ckbLastKeystroke = millis();
}
}
// All keys consumed in compose mode — skip normal routing
} else {
// --- Normal (non-compose) key routing ---
#if defined(LilyGo_T5S3_EPaper_Pro)
if (!ui_task.isLocked()) {
#else
{
#endif
cpuPower.setBoost();
ui_task.keepAlive();
#if defined(LilyGo_T5S3_EPaper_Pro)
if (ui_task.isVKBActive()) {
// VKB is open — feed character into VKB text buffer
ui_task.feedCardKBChar(ckb);
} else if (ui_task.isOnHomeScreen()) {
} else
#endif
if (ui_task.isOnHomeScreen()) {
// Home screen: ESC does nothing special, letter shortcuts open tiles
if (ckb == 0x1B) {
// ESC on home — no-op (already home)
@@ -3167,11 +3288,21 @@ void loop() {
switch (ckb) {
case 'm': ui_task.gotoChannelPickerScreen(); break;
case 'c': ui_task.gotoContactsScreen(); break;
#if !defined(LILYGO_TECHO_LITE)
case 'e': ui_task.gotoTextReader(); break;
case 'n': ui_task.gotoNotesScreen(); break;
case 's': ui_task.gotoSettingsScreen(); break;
#endif
case 's':
if (ui_task.isHomeOnShutdownPage()) {
ui_task.injectKey(ckb);
} else {
ui_task.gotoSettingsScreen();
}
break;
case 'f': ui_task.gotoDiscoveryScreen(); break;
case 'h': ui_task.gotoLastHeardScreen(); break;
case (char)0xF3: ui_task.injectKey(KEY_LEFT); break; // Left arrow → prev page
case (char)0xF4: ui_task.injectKey(KEY_RIGHT); break; // Right arrow → next page
#ifdef MECK_WEB_READER
case 'b': ui_task.gotoWebReader(); break;
#endif
@@ -3187,6 +3318,7 @@ void loop() {
// Notes editing/renaming: route ALL keys directly (no VKB).
// This gives: Enter=newline, arrows=cursor, printable=insert, ESC=save&exit
#if !defined(LILYGO_TECHO_LITE)
if (ui_task.isOnNotesScreen()) {
NotesScreen* notesScr = (NotesScreen*)ui_task.getNotesScreen();
if (notesScr && (notesScr->isEditing() || notesScr->isRenaming())) {
@@ -3214,25 +3346,24 @@ void loop() {
ui_task.forceRefresh();
}
}
#endif
if (!handled) {
// ESC → back (same as 'q' on T-Deck Pro) for all non-notes screens
if (ckb == 0x1B) {
// Channel picker: ESC goes home
// ESC or Q → back navigation
if (ckb == 0x1B || ckb == 'q') {
if (ui_task.isOnChannelPickerScreen()) {
ui_task.gotoHomeScreen();
// Channel screen: ESC goes to picker
} else if (ui_task.isOnChannelScreen()) {
ChannelScreen* chScr = (ChannelScreen*)ui_task.getChannelScreen();
if (chScr && (chScr->isReplySelectMode() || chScr->isShowingPathOverlay())) {
ui_task.injectKey('q'); // dismiss overlay/reply first
} else if (chScr && chScr->isDMConversation()) {
ui_task.injectKey('q'); // DM conversation → inbox (handled internally)
ui_task.injectKey('q'); // DM conversation → inbox
} else {
ui_task.gotoChannelPickerScreen();
}
} else {
ui_task.injectKey('q');
ui_task.gotoHomeScreen(); // All other screens → home
}
} else if (ckb == '\r') {
// Enter key — screen-specific compose or select
@@ -3253,7 +3384,20 @@ void loop() {
if (the_mesh.getContactByIdx(j, ci) && strcmp(ci.name, dmName) == 0) {
char label[40];
snprintf(label, sizeof(label), "DM: %s", dmName);
#if defined(LilyGo_T5S3_EPaper_Pro)
ui_task.showVirtualKeyboard(VKB_DM, label, "", 137, j);
#elif defined(MECK_CARDKB)
ckbComposeMode = true;
ckbComposeBuf[0] = '\0';
ckbComposePos = 0;
ckbComposeDM = true;
ckbComposeDMIdx = (int)j;
strncpy(ckbComposeDMName, dmName, sizeof(ckbComposeDMName) - 1);
ckbComposeRefresh = true;
ckbLastKeystroke = millis();
#else
ui_task.injectKey('\r');
#endif
ui_task.clearDMUnread(j);
break;
}
@@ -3266,7 +3410,19 @@ void loop() {
if (the_mesh.getChannel(chIdx, ch)) {
char label[40];
snprintf(label, sizeof(label), "To: %s", ch.name);
#if defined(LilyGo_T5S3_EPaper_Pro)
ui_task.showVirtualKeyboard(VKB_CHANNEL_MSG, label, "", 137, chIdx);
#elif defined(MECK_CARDKB)
ckbComposeMode = true;
ckbComposeBuf[0] = '\0';
ckbComposePos = 0;
ckbComposeDM = false;
ckbComposeChIdx = chIdx;
ckbComposeRefresh = true;
ckbLastKeystroke = millis();
#else
ui_task.injectKey('\r');
#endif
}
}
} else if (ui_task.isOnContactsScreen()) {
@@ -3290,7 +3446,20 @@ void loop() {
cs->getSelectedContactName(dname, sizeof(dname));
char label[40];
snprintf(label, sizeof(label), "DM: %s", dname);
#if defined(LilyGo_T5S3_EPaper_Pro)
ui_task.showVirtualKeyboard(VKB_DM, label, "", 137, idx);
#elif defined(MECK_CARDKB)
ckbComposeMode = true;
ckbComposeBuf[0] = '\0';
ckbComposePos = 0;
ckbComposeDM = true;
ckbComposeDMIdx = idx;
strncpy(ckbComposeDMName, dname, sizeof(ckbComposeDMName) - 1);
ckbComposeRefresh = true;
ckbLastKeystroke = millis();
#else
ui_task.injectKey('\r');
#endif
}
} else if (idx >= 0 && ctype == ADV_TYPE_REPEATER) {
ui_task.gotoRepeaterAdmin(idx);
@@ -3310,9 +3479,17 @@ void loop() {
if (admin) {
RepeaterAdminScreen::AdminState astate = admin->getState();
if (astate == RepeaterAdminScreen::STATE_PASSWORD_ENTRY) {
#if defined(LilyGo_T5S3_EPaper_Pro)
ui_task.showVirtualKeyboard(VKB_ADMIN_PASSWORD, "Admin Password", "", 32);
#else
ui_task.injectKey('\r');
#endif
} else {
#if defined(LilyGo_T5S3_EPaper_Pro)
ui_task.showVirtualKeyboard(VKB_ADMIN_CLI, "Admin Command", "", 137);
#else
ui_task.injectKey('\r');
#endif
}
}
} else if (ui_task.isOnPathEditor()) {
@@ -3377,6 +3554,7 @@ void loop() {
}
}
}
} // end compose mode else
}
}
#endif
@@ -3470,6 +3648,9 @@ void handleKeyboardInput() {
Serial.printf("handleKeyboardInput: key='%c' (0x%02X) composeMode=%d\n",
key >= 32 ? key : '?', key, composeMode);
// Defer contact saves while user is actively pressing keys
the_mesh.notifyUserInput();
// Alarm ringing: ANY key dismisses (highest priority after lock screen)
#ifdef MECK_AUDIO_VARIANT
{
@@ -4418,6 +4599,23 @@ void handleKeyboardInput() {
ui_task.gotoNotesScreen();
break;
case 'S':
// Shift+S: page scroll down on list screens
if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen() || ui_task.isOnRepeaterAdmin()
|| ui_task.isOnDiscoveryScreen() || ui_task.isOnLastHeardScreen()
|| ui_task.isOnPathEditor() || ui_task.isOnChannelPickerScreen()
#ifdef MECK_WEB_READER
|| ui_task.isOnWebReader()
#endif
|| ui_task.isOnMapScreen()
#ifdef MECK_AUDIO_VARIANT
|| ui_task.isOnAlarmScreen()
#endif
) {
ui_task.injectKey('S');
}
break;
case 's':
// Open settings (from home), or navigate down on channel/contacts/admin/web/map/discovery/lastheard
if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen() || ui_task.isOnRepeaterAdmin()
@@ -4430,6 +4628,7 @@ void handleKeyboardInput() {
#ifdef MECK_AUDIO_VARIANT
|| ui_task.isOnAlarmScreen()
#endif
|| ui_task.isHomeOnShutdownPage()
) {
ui_task.injectKey('s'); // Pass directly for scrolling
} else {
@@ -4438,6 +4637,23 @@ void handleKeyboardInput() {
}
break;
case 'W':
// Shift+W: page scroll up on list screens
if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen() || ui_task.isOnRepeaterAdmin()
|| ui_task.isOnDiscoveryScreen() || ui_task.isOnLastHeardScreen()
|| ui_task.isOnPathEditor() || ui_task.isOnChannelPickerScreen()
#ifdef MECK_WEB_READER
|| ui_task.isOnWebReader()
#endif
|| ui_task.isOnMapScreen()
#ifdef MECK_AUDIO_VARIANT
|| ui_task.isOnAlarmScreen()
#endif
) {
ui_task.injectKey('W');
}
break;
case 'w':
// Navigate up/previous (scroll on channel screen)
if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen() || ui_task.isOnRepeaterAdmin()
@@ -5053,4 +5269,167 @@ void audio_eof_mp3(const char *info) {
}
#endif // !HAS_4G_MODEM
#endif // LilyGo_TDeck_Pro
#endif // LilyGo_TDeck_Pro
// ============================================================================
// CARDKB COMPOSE FUNCTIONS (T-Echo Lite)
// ============================================================================
#if defined(MECK_CARDKB)
void drawCardKBCompose() {
#ifdef DISPLAY_CLASS
display.startFrame();
display.setTextSize(1);
display.setColor(DisplayDriver::GREEN);
display.setCursor(0, 0);
// Header: "To: channel" or "DM: contact"
char headerBuf[40];
if (ckbComposeDM) {
snprintf(headerBuf, sizeof(headerBuf), "DM: %s", ckbComposeDMName);
} else {
ChannelDetails channel;
if (the_mesh.getChannel(ckbComposeChIdx, channel)) {
snprintf(headerBuf, sizeof(headerBuf), "To: %s", channel.name);
} else {
snprintf(headerBuf, sizeof(headerBuf), "To: Channel %d", ckbComposeChIdx);
}
}
display.print(headerBuf);
display.setColor(DisplayDriver::LIGHT);
display.drawRect(0, 11, display.width(), 1);
// Body: word-wrapped compose buffer
int y = 14;
int px = 0;
int lineW = display.width();
char charStr[2] = {0, 0};
char dblStr[3] = {0, 0, 0};
bool atWordBoundary = true;
display.setCursor(0, y);
display.setColor(DisplayDriver::LIGHT);
for (int i = 0; i < ckbComposePos; i++) {
uint8_t b = (uint8_t)ckbComposeBuf[i];
// Word wrap: check if next word fits on this line
if (atWordBoundary && b != ' ' && px > 0) {
int wordW = 0;
for (int j = i; j < ckbComposePos; j++) {
uint8_t wb = (uint8_t)ckbComposeBuf[j];
if (wb == ' ') break;
dblStr[0] = dblStr[1] = (char)wb;
charStr[0] = (char)wb;
wordW += display.getTextWidth(dblStr) - display.getTextWidth(charStr);
}
if (px + wordW > lineW) {
px = 0;
y += 12;
}
}
if (b == ' ') {
charStr[0] = ' ';
dblStr[0] = dblStr[1] = ' ';
int adv = display.getTextWidth(dblStr) - display.getTextWidth(charStr);
if (px + adv > lineW) {
px = 0;
y += 12;
} else {
display.setCursor(px, y);
display.print(charStr);
px += adv;
}
atWordBoundary = true;
} else {
charStr[0] = (char)b;
dblStr[0] = dblStr[1] = (char)b;
int adv = display.getTextWidth(dblStr) - display.getTextWidth(charStr);
if (px + adv > lineW) {
px = 0;
y += 12;
}
display.setCursor(px, y);
display.print(charStr);
px += adv;
atWordBoundary = false;
}
}
// Cursor
display.setCursor(px, y);
display.print("_");
// Footer status bar
int statusY = display.height() - 12;
display.setColor(DisplayDriver::LIGHT);
display.drawRect(0, statusY - 2, display.width(), 1);
display.setCursor(0, statusY);
display.setColor(DisplayDriver::YELLOW);
char status[32];
if (ckbComposePos == 0) {
display.print("Esc:Cancel");
} else {
snprintf(status, sizeof(status), "Esc:X %d/137", ckbComposePos);
display.print(status);
}
const char* rt = "Ent:Send";
display.setCursor(display.width() - display.getTextWidth(rt) - 2, statusY);
display.print(rt);
display.endFrame();
#endif
}
void sendCardKBMessage() {
if (ckbComposePos == 0) return;
cpuPower.setBoost();
if (ckbComposeDM) {
// Direct message
if (ckbComposeDMIdx >= 0) {
if (the_mesh.uiSendDirectMessage((uint32_t)ckbComposeDMIdx, ckbComposeBuf)) {
ui_task.addSentDM(ckbComposeDMName, the_mesh.getNodePrefs()->node_name, ckbComposeBuf);
ui_task.showAlert("DM sent!", 1500);
} else {
ui_task.showAlert("DM failed!", 1500);
}
} else {
ui_task.showAlert("No contact!", 1500);
}
} else {
// Channel message
ChannelDetails channel;
if (the_mesh.getChannel(ckbComposeChIdx, channel)) {
uint32_t timestamp = rtc_clock.getCurrentTime();
int len = strlen(ckbComposeBuf);
if (the_mesh.sendGroupMessage(timestamp, channel.channel,
the_mesh.getNodePrefs()->node_name,
ckbComposeBuf, len)) {
ui_task.addSentChannelMessage(ckbComposeChIdx,
the_mesh.getNodePrefs()->node_name,
ckbComposeBuf);
the_mesh.queueSentChannelMessage(ckbComposeChIdx, timestamp,
the_mesh.getNodePrefs()->node_name,
ckbComposeBuf);
ui_task.showAlert("Sent!", 1500);
} else {
ui_task.showAlert("Send failed!", 1500);
}
} else {
ui_task.showAlert("No channel!", 1500);
}
}
ckbComposeMode = false;
ckbComposeBuf[0] = '\0';
ckbComposePos = 0;
ui_task.forceRefresh();
}
#endif // MECK_CARDKB
@@ -5,19 +5,22 @@
// Polls 0x5F on the shared I2C bus via QWIIC connector.
// Maps CardKB special key codes to Meck key constants.
//
// Platform support:
// - ESP32/ESP32-S3 (T5S3, T-Deck Pro): Wire.begin(SDA, SCL)
// - nRF52840 (T-Echo Lite): Wire.begin() uses variant.h pins
//
// Usage:
// CardKBKeyboard cardkb;
// if (cardkb.begin()) { /* detected */ }
// char key = cardkb.readKey(); // returns 0 if no key
// =============================================================================
#if defined(LilyGo_T5S3_EPaper_Pro) && defined(MECK_CARDKB)
#if defined(MECK_CARDKB)
#ifndef CARDKB_KEYBOARD_H
#define CARDKB_KEYBOARD_H
#include <Arduino.h>
#include <Wire.h>
#include "variant.h" // For I2C_SDA, I2C_SCL (bus recovery)
// I2C address (defined in variant.h, fallback here)
#ifndef CARDKB_I2C_ADDR
@@ -75,7 +78,13 @@ public:
_errorCount++;
if (_errorCount >= 3) {
// I2C bus may be stuck — re-init to recover
#if defined(ESP32)
// ESP32: Wire.begin() accepts explicit SDA/SCL pins
Wire.begin(I2C_SDA, I2C_SCL);
#else
// nRF52: Wire.begin() uses PIN_WIRE_SDA/SCL from variant.h
Wire.begin();
#endif
Wire.setClock(100000);
_pollInterval = 500; // Back off for 500ms
_errorCount = 0;
@@ -119,4 +128,4 @@ private:
};
#endif // CARDKB_KEYBOARD_H
#endif // LilyGo_T5S3_EPaper_Pro && MECK_CARDKB
#endif // MECK_CARDKB
+162 -23
View File
@@ -13,8 +13,12 @@
#endif
// Maximum messages to store in history
#ifndef CHANNEL_MSG_HISTORY_SIZE
#define CHANNEL_MSG_HISTORY_SIZE 300
#endif
#ifndef CHANNEL_MSG_TEXT_LEN
#define CHANNEL_MSG_TEXT_LEN 160
#endif
#define MSG_PATH_MAX 20 // Max repeater hops stored per message
#ifndef MAX_GROUP_CHANNELS
@@ -332,6 +336,11 @@ public:
}
}
// Mark all channels + DMs as read (companion app connected)
void markAllRead() {
memset(_unread, 0, sizeof(_unread));
}
// Get unread count for a specific channel
int getUnreadForChannel(uint8_t channel_idx) const {
int slot = (channel_idx == 0xFF) ? MAX_GROUP_CHANNELS : channel_idx;
@@ -728,6 +737,12 @@ public:
const char* rtInbox = "Hold:Open";
display.setCursor(display.width() - display.getTextWidth(rtInbox) - 2, footerY);
display.print(rtInbox);
#elif defined(LILYGO_TECHO_LITE)
display.setCursor(0, footerY);
display.print("Q:Bk");
const char* rtInbox = "Ent:Open";
display.setCursor(display.width() - display.getTextWidth(rtInbox) - 2, footerY);
display.print(rtInbox);
#else
display.setCursor(0, footerY);
display.print("Q:Bck A/D:Ch");
@@ -964,6 +979,10 @@ public:
display.print("Swipe: Switch channel");
display.setCursor(0, 40);
display.print("Long press: Compose");
#elif defined(LILYGO_TECHO_LITE)
display.print("Arrows: Switch channel");
display.setCursor(0, 40);
display.print("Ent: Compose message");
#else
display.print("A/D: Switch channel");
display.setCursor(0, 40);
@@ -1216,9 +1235,16 @@ public:
uint8_t b = (uint8_t)msg->text[pos];
if (b == EMOJI_PAD_BYTE) { pos++; continue; }
// --- UTF-8 lead byte detection ---
// Must check BEFORE isEmojiEscape() because lead bytes 0xC2-0xCC
// overlap the emoji escape range (0x80-0xCC). A byte >= 0xC2
// followed by a continuation byte (0x80-0xBF) is always UTF-8.
bool isUtf8 = (b >= 0xC2 && pos + 1 < textLen &&
((uint8_t)msg->text[pos + 1] & 0xC0) == 0x80);
// Word wrap: when starting a new text word, check if it fits
if (b != ' ' && !isEmojiEscape(b) && px > 0) {
if (b != ' ' && (isUtf8 || !isEmojiEscape(b)) && px > 0) {
bool boundary = (pos == 0);
if (!boundary) {
for (int bp = pos - 1; bp >= 0; bp--) {
@@ -1230,15 +1256,31 @@ public:
}
if (boundary) {
int wordW = 0;
for (int j = pos; j < textLen; j++) {
for (int j = pos; j < textLen; ) {
uint8_t wb = (uint8_t)msg->text[j];
if (wb == EMOJI_PAD_BYTE) continue;
if (wb == ' ' || isEmojiEscape(wb)) break;
charStr[0] = (char)wb;
dblStr[0] = dblStr[1] = (char)wb;
int charAdv = display.getTextWidth(dblStr) - display.getTextWidth(charStr);
if (charAdv < 1) charAdv = 1;
wordW += charAdv;
if (wb == EMOJI_PAD_BYTE) { j++; continue; }
if (wb == ' ') break;
// Check for UTF-8 lead byte in word scan
bool wbUtf8 = (wb >= 0xC2 && j + 1 < textLen &&
((uint8_t)msg->text[j + 1] & 0xC0) == 0x80);
if (!wbUtf8 && isEmojiEscape(wb)) break;
if (wbUtf8) {
int clen = (wb < 0xE0) ? 2 : (wb < 0xF0) ? 3 : 4;
int actual = (j + clen <= textLen) ? clen : textLen - j;
char mbuf[5] = {0};
memcpy(mbuf, &msg->text[j], actual);
int charAdv = display.getTextWidth(mbuf);
if (charAdv < 1) charAdv = 1;
wordW += charAdv;
j += actual;
} else {
charStr[0] = (char)wb;
dblStr[0] = dblStr[1] = (char)wb;
int charAdv = display.getTextWidth(dblStr) - display.getTextWidth(charStr);
if (charAdv < 1) charAdv = 1;
wordW += charAdv;
j++;
}
}
if (px + wordW > lineW) {
px = 0;
@@ -1249,7 +1291,25 @@ public:
}
}
if (isEmojiEscape(b)) {
// --- Render: UTF-8 multi-byte character ---
if (isUtf8) {
int clen = (b < 0xE0) ? 2 : (b < 0xF0) ? 3 : 4;
int actual = (pos + clen <= textLen) ? clen : textLen - pos;
char mbuf[5] = {0};
memcpy(mbuf, &msg->text[pos], actual);
int adv = display.getTextWidth(mbuf);
if (adv < 1) adv = 1;
if (px + adv > lineW) {
px = 0;
linesForThisMsg++;
y += lineHeight;
if (linesForThisMsg >= maxLinesPerMsg || y + lineHeight > maxY) break;
}
display.setCursor(px, y);
display.print(mbuf);
px += adv;
pos += actual;
} else if (isEmojiEscape(b)) {
if (px + EMOJI_SM_W > lineW) {
px = 0;
linesForThisMsg++;
@@ -1388,7 +1448,7 @@ public:
display.setCursor(display.width() - display.getTextWidth(rtCh) - 2, footerY);
display.print(rtCh);
} else {
display.print("Swipe:List/Scroll");
display.print("Swipe:Ch/Scroll");
const char* midCh = "Tap:Path";
display.setCursor((display.width() - display.getTextWidth(midCh)) / 2, footerY);
display.print(midCh);
@@ -1396,6 +1456,19 @@ public:
display.setCursor(display.width() - display.getTextWidth(rtCh) - 2, footerY);
display.print(rtCh);
}
#elif defined(LILYGO_TECHO_LITE)
// T-Echo Lite: minimal footer for narrow display
if (_viewChannelIdx == 0xFF) {
display.print("Q:Bk");
const char* rightText = "Ent:Reply";
display.setCursor(display.width() - display.getTextWidth(rightText) - 2, footerY);
display.print(rightText);
} else {
display.print("Q:Bk");
const char* rightText = "Ent:New";
display.setCursor(display.width() - display.getTextWidth(rightText) - 2, footerY);
display.print(rightText);
}
#else
// Left side: abbreviated controls
if (_replySelectMode) {
@@ -1413,14 +1486,10 @@ public:
display.setCursor(display.width() - display.getTextWidth(rightText) - 2, footerY);
display.print(rightText);
} else {
display.print("Q:Bk A/D:List R:Rply");
// "Ent:New" only fits with Classic+TINY — wider fonts cause overlap
NodePrefs* _fp = the_mesh.getNodePrefs();
if (_fp->ui_font_style == 0 && !_fp->large_font) {
const char* rightText = " Ent:New";
display.setCursor(display.width() - display.getTextWidth(rightText) - 2, footerY);
display.print(rightText);
}
display.print("Q:Bck A/D:Ch R:Rply");
const char* rightText = "Ent:New";
display.setCursor(display.width() - display.getTextWidth(rightText) - 2, footerY);
display.print(rightText);
}
#endif
@@ -1636,11 +1705,81 @@ public:
}
}
// A/D - open channel picker (handled by main.cpp — return false to pass through)
if (c == 'a' || c == 'A' || c == 'd' || c == 'D') {
// A - previous channel (includes DM tab at 0xFF)
if (c == 'a' || c == 'A') {
_replySelectMode = false;
_replySelectPos = -1;
return false; // Let main.cpp open the channel picker
if (_viewChannelIdx == 0xFF) {
// DM tab → go to last valid group channel
for (uint8_t i = MAX_GROUP_CHANNELS - 1; i > 0; i--) {
ChannelDetails ch;
if (the_mesh.getChannel(i, ch) && ch.name[0] != '\0') {
_viewChannelIdx = i;
break;
}
}
} else if (_viewChannelIdx > 0) {
// Skip backwards over any empty/gap slots
uint8_t prev = _viewChannelIdx - 1;
bool found = false;
while (true) {
ChannelDetails ch;
if (the_mesh.getChannel(prev, ch) && ch.name[0] != '\0') {
_viewChannelIdx = prev;
found = true;
break;
}
if (prev == 0) break;
prev--;
}
if (!found) {
// No valid channel below → wrap to DM tab
_viewChannelIdx = 0xFF;
_dmInboxMode = true;
_dmInboxScroll = 0;
_dmFilterName[0] = '\0';
}
} else {
// Channel 0 → wrap to DM tab
_viewChannelIdx = 0xFF;
_dmInboxMode = true;
_dmInboxScroll = 0;
_dmFilterName[0] = '\0';
}
_scrollPos = 0;
markChannelRead(_viewChannelIdx);
return true;
}
// D - next channel (includes DM tab at 0xFF)
if (c == 'd' || c == 'D') {
_replySelectMode = false;
_replySelectPos = -1;
if (_viewChannelIdx == 0xFF) {
// DM tab → wrap to channel 0
_viewChannelIdx = 0;
} else {
// Skip forward over any empty/gap slots
bool found = false;
for (uint8_t next = _viewChannelIdx + 1; next < MAX_GROUP_CHANNELS; next++) {
ChannelDetails ch;
if (the_mesh.getChannel(next, ch) && ch.name[0] != '\0') {
_viewChannelIdx = next;
found = true;
break;
}
}
if (!found) {
// Past last channel → go to DM tab
_viewChannelIdx = 0xFF;
_dmInboxMode = true;
_dmInboxScroll = 0;
_dmFilterName[0] = '\0';
}
}
_scrollPos = 0;
markChannelRead(_viewChannelIdx);
return true;
}
return false;
@@ -341,6 +341,11 @@ public:
const char* rt = "Boot:Back";
display.setCursor(display.width() - display.getTextWidth(rt) - 2, footerY);
display.print(rt);
#elif defined(LILYGO_TECHO_LITE)
display.print("Q:Bk");
const char* rt = "Ent:Open";
display.setCursor(display.width() - display.getTextWidth(rt) - 2, footerY);
display.print(rt);
#else
display.print("W/S:Nav Q:Back");
const char* rt = "Ent:Open";
@@ -466,6 +466,16 @@ public:
display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY);
display.print(right);
}
#elif defined(LILYGO_TECHO_LITE)
display.setCursor(0, footerY);
if (_selectMode) {
display.print("Q:Done");
} else {
display.print("Q:Bk");
const char* right = "Ent:Sel";
display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY);
display.print(right);
}
#else
display.setCursor(0, footerY);
if (_selectMode) {
@@ -485,16 +495,30 @@ public:
}
bool handleInput(char c) override {
// Shift+W: page up
if (c == 'W') {
int pageSize = (128 - 14 - 14) / the_mesh.getNodePrefs()->smallLineH();
if (pageSize < 3) pageSize = 3;
_scrollPos = max(0, _scrollPos - pageSize);
return true;
}
// W - scroll up (previous contact)
if (c == 'w' || c == 'W' || c == 0xF2) {
if (c == 'w' || c == 0xF2) {
if (_scrollPos > 0) {
_scrollPos--;
return true;
}
}
// Shift+S: page down
if (c == 'S') {
int pageSize = (128 - 14 - 14) / the_mesh.getNodePrefs()->smallLineH();
if (pageSize < 3) pageSize = 3;
_scrollPos = min(_filteredCount - 1, _scrollPos + pageSize);
return true;
}
// S - scroll down (next contact)
if (c == 's' || c == 'S' || c == 0xF1) {
if (c == 's' || c == 0xF1) {
if (_scrollPos < _filteredCount - 1) {
_scrollPos++;
return true;
@@ -217,16 +217,30 @@ public:
bool handleInput(char c) override {
int count = the_mesh.getDiscoveredCount();
// Shift+W: page up
if (c == 'W') {
int pageSize = (128 - 14 - 14) / the_mesh.getNodePrefs()->smallLineH();
if (pageSize < 3) pageSize = 3;
_scrollPos = max(0, _scrollPos - pageSize);
return true;
}
// W - scroll up
if (c == 'w' || c == 'W' || c == 0xF2) {
if (c == 'w' || c == 0xF2) {
if (_scrollPos > 0) {
_scrollPos--;
return true;
}
}
// Shift+S: page down
if (c == 'S') {
int pageSize = (128 - 14 - 14) / the_mesh.getNodePrefs()->smallLineH();
if (pageSize < 3) pageSize = 3;
_scrollPos = min(count - 1, _scrollPos + pageSize);
return true;
}
// S - scroll down
if (c == 's' || c == 'S' || c == 0xF1) {
if (c == 's' || c == 0xF1) {
if (_scrollPos < count - 1) {
_scrollPos++;
return true;
+29 -4
View File
@@ -3,7 +3,7 @@
// Emoji sprites for e-ink display - dual size
// Large (12x12) for compose/picker, Small (10x10) for channel view
// MSB-first, 2 bytes per row
// 76 total emoji: joy/thumbsup/frown first, then 43 original, then 19 new, then 11 newest
// 77 total emoji: joy/thumbsup/frown first, then 43 original, then 19 new, then 11 newest, then 1 latest
#include <stdint.h>
#ifdef ESP32
@@ -15,11 +15,11 @@
#define EMOJI_SM_W 10
#define EMOJI_SM_H 10
#define EMOJI_COUNT 76
#define EMOJI_COUNT 77
// Escape codes in 0x80+ range - safe from keyboard ASCII (32-126)
#define EMOJI_ESCAPE_START 0x80
#define EMOJI_ESCAPE_END 0xCB // 0x80 + 75
#define EMOJI_ESCAPE_END 0xCC // 0x80 + 76
#define EMOJI_PAD_BYTE 0x7F // DEL, not typeable (key < 127 guard)
// ======== LARGE 12x12 SPRITES ========
@@ -328,6 +328,10 @@ static const uint8_t emoji_lg_eight_spoked_asterisk[] PROGMEM = {
static const uint8_t emoji_lg_signal_strength[] PROGMEM = {
0x00,0x20, 0x00,0x20, 0x00,0xA0, 0x00,0xA0, 0x02,0xA0, 0x02,0xA0, 0x0A,0xA0, 0x0A,0xA0, 0x2A,0xA0, 0x2A,0xA0, 0xAA,0xA0, 0xAA,0xA0,
};
// [76] beer 🍺
static const uint8_t emoji_lg_beer[] PROGMEM = {
0x24,0x80, 0x77,0x60, 0xFF,0x80, 0x7F,0xC0, 0x40,0x70, 0x5F,0x50, 0x5F,0x50, 0x5F,0x50, 0x40,0x70, 0x7F,0xC0, 0xFF,0xE0, 0x00,0x00,
};
static const uint8_t* const EMOJI_SPRITES_LG[] PROGMEM = {
@@ -354,6 +358,7 @@ static const uint8_t* const EMOJI_SPRITES_LG[] PROGMEM = {
emoji_lg_diamond_suit, emoji_lg_spade_suit, emoji_lg_pizza, emoji_lg_four_leaf_clover,
emoji_lg_cloud, emoji_lg_rocket, emoji_lg_passport_control,
emoji_lg_eight_spoked_asterisk, emoji_lg_signal_strength,
emoji_lg_beer,
};
// ======== SMALL 10x10 SPRITES ========
@@ -616,6 +621,10 @@ static const uint8_t emoji_sm_eight_spoked_asterisk[] PROGMEM = {
static const uint8_t emoji_sm_signal_strength[] PROGMEM = {
0x00,0x80, 0x00,0x80, 0x02,0x80, 0x02,0x80, 0x0A,0x80, 0x0A,0x80, 0x2A,0x80, 0x2A,0x80, 0xAA,0x80, 0xAA,0x80,
};
// [76] beer 🍺
static const uint8_t emoji_sm_beer[] PROGMEM = {
0x54,0x00, 0x6E,0xC0, 0xFF,0x80, 0x80,0xC0, 0xBE,0x40, 0xBE,0x40, 0x80,0xC0, 0xFF,0x80, 0x00,0x00, 0xFF,0x80,
};
static const uint8_t* const EMOJI_SPRITES_SM[] PROGMEM = {
// Faces/emotion first
@@ -641,6 +650,7 @@ static const uint8_t* const EMOJI_SPRITES_SM[] PROGMEM = {
emoji_sm_diamond_suit, emoji_sm_spade_suit, emoji_sm_pizza, emoji_sm_four_leaf_clover,
emoji_sm_cloud, emoji_sm_rocket, emoji_sm_passport_control,
emoji_sm_eight_spoked_asterisk, emoji_sm_signal_strength,
emoji_sm_beer,
};
// ---- Codepoint lookup for UTF-8 conversion ----
@@ -726,6 +736,7 @@ static const EmojiCodepoint EMOJI_CODEPOINTS[EMOJI_COUNT] = {
{ 0x1F6C2, 0x0000, 0xC9 }, // passport_control
{ 0x2733, 0x0000, 0xCA }, // eight_spoked_asterisk
{ 0x1F4F6, 0x0000, 0xCB }, // signal_strength
{ 0x1F37A, 0x0000, 0xCC }, // beer
};
// ---- Helper functions ----
@@ -769,6 +780,7 @@ static void emojiSanitize(const char* src, char* dst, int dstLen) {
int consumed;
uint32_t cp = emojiDecodeUtf8(s + si, srcLen - si, &consumed);
if (cp == 0xFE0F) { si += consumed; continue; }
if (cp == 0xFFFD) { si += consumed; continue; } // Invalid UTF-8 — skip stray bytes safely
bool found = false;
for (int e = 0; e < EMOJI_COUNT; e++) {
if (EMOJI_CODEPOINTS[e].cp == cp) {
@@ -803,7 +815,20 @@ static void emojiSanitize(const char* src, char* dst, int dstLen) {
}
}
}
if (!found) si += consumed; // Skip unknown multi-byte chars
if (!found) {
// Preserve non-emoji UTF-8 (accented letters, Cyrillic, Greek, etc.) by
// copying original bytes through to dst. UTF-8-aware render paths handle
// them downstream. Emoji-escape disambiguation remains unambiguous because
// emoji escapes (0x80-0xCC) only appear standalone — never as a continuation
// byte following a UTF-8 lead byte (0xC2-0xF7), so the decoder can tell them
// apart by tracking lead-byte state.
if (di + consumed < dstLen) {
for (int k = 0; k < consumed; k++) dst[di++] = (char)s[si + k];
si += consumed;
} else {
break; // Not enough room for full UTF-8 sequence — stop cleanly
}
}
} else {
dst[di++] = (char)b;
si++;
@@ -223,14 +223,28 @@ public:
}
bool handleInput(char c) override {
// Shift+W: page up
if (c == 'W') {
int pageSize = (128 - 14 - 14) / the_mesh.getNodePrefs()->smallLineH();
if (pageSize < 3) pageSize = 3;
_scrollPos = max(0, _scrollPos - pageSize);
return true;
}
// Scroll up
if (c == 'w' || c == 'W' || c == 0xF2) {
if (c == 'w' || c == 0xF2) {
if (_scrollPos > 0) { _scrollPos--; return true; }
return false;
}
// Shift+S: page down
if (c == 'S') {
int pageSize = (128 - 14 - 14) / the_mesh.getNodePrefs()->smallLineH();
if (pageSize < 3) pageSize = 3;
_scrollPos = min(_count - 1, _scrollPos + pageSize);
return true;
}
// Scroll down
if (c == 's' || c == 'S' || c == 0xF1) {
if (c == 's' || c == 0xF1) {
if (_scrollPos < _count - 1) { _scrollPos++; return true; }
return false;
}
+15 -15
View File
@@ -37,8 +37,8 @@ static inline const char* meckFontStyleName(uint8_t style) {
// Font includes — Noto Sans family
// ---------------------------------------------------------------------------
#include "fonts/NotoSans7pt7b.h"
#include "fonts/NotoSans9pt7b.h"
#include "fonts/NotoSans12pt7b.h"
#include "fonts/NotoSans9pt8b.h"
#include "fonts/NotoSans12pt8b.h"
#include "fonts/NotoSansBold7pt7b.h"
#include "fonts/NotoSansBold9pt7b.h"
#include "fonts/NotoSansBold12pt7b.h"
@@ -52,8 +52,8 @@ static inline const char* meckFontStyleName(uint8_t style) {
#include "fonts/Montserrat9pt7b.h"
#include "fonts/Montserrat12pt7b.h"
#include "fonts/MontserratBold7pt7b.h"
#include "fonts/MontserratBold9pt7b.h"
#include "fonts/MontserratBold12pt7b.h"
#include "fonts/MontserratBold9pt8b.h"
#include "fonts/MontserratBold12pt8b.h"
#include "fonts/MontserratBold18pt7b.h"
#include "fonts/MontserratBold24pt7b.h"
@@ -83,11 +83,11 @@ static inline const GFXfont* meckGetFont_TDeckPro(uint8_t style, int textSize) {
if (style == MECK_FONT_NOTO) {
switch (textSize) {
case 0: return &NotoSans_Regular7pt7b;
case 1: return &NotoSans_Regular9pt7b;
case 2: return &NotoSans_Regular9pt7b;
case 1: return &NotoSans9pt8b;
case 2: return &NotoSans9pt8b;
case 3: return &NotoSans_Bold12pt7b;
case 5: return &NotoSans_Bold12pt7b; // caller applies ×2 scale
default: return &NotoSans_Regular9pt7b;
case 5: return &NotoSans_Bold12pt7b; // caller applies x2 scale
default: return &NotoSans9pt8b;
}
}
@@ -96,8 +96,8 @@ static inline const GFXfont* meckGetFont_TDeckPro(uint8_t style, int textSize) {
case 0: return &Montserrat_Regular7pt7b;
case 1: return &Montserrat_Regular9pt7b;
case 2: return &Montserrat_Regular9pt7b;
case 3: return &Montserrat_Bold12pt7b;
case 5: return &Montserrat_Bold12pt7b; // caller applies ×2 scale
case 3: return &MontserratBold12pt8b;
case 5: return &MontserratBold12pt8b; // caller applies x2 scale
default: return &Montserrat_Regular9pt7b;
}
}
@@ -130,22 +130,22 @@ static inline const GFXfont* meckGetFont_T5S3(uint8_t style, int textSize) {
if (style == MECK_FONT_NOTO) {
switch (textSize) {
case 0: return &NotoSans_Regular12pt7b;
case 0: return &NotoSans12pt8b;
case 1: return &NotoSans_Bold12pt7b;
case 2: return &NotoSans_Bold18pt7b;
case 3: return &NotoSans_Bold24pt7b;
case 5: return &NotoSans_Bold24pt7b; // caller applies ×5 scale
default: return &NotoSans_Regular12pt7b;
case 5: return &NotoSans_Bold24pt7b; // caller applies x5 scale
default: return &NotoSans12pt8b;
}
}
// MECK_FONT_MONTSERRAT
switch (textSize) {
case 0: return &Montserrat_Regular12pt7b;
case 1: return &Montserrat_Bold12pt7b;
case 1: return &MontserratBold12pt8b;
case 2: return &Montserrat_Bold18pt7b;
case 3: return &Montserrat_Bold24pt7b;
case 5: return &Montserrat_Bold24pt7b; // caller applies ×5 scale
case 5: return &Montserrat_Bold24pt7b; // caller applies x5 scale
default: return &Montserrat_Regular12pt7b;
}
}
+16 -2
View File
@@ -909,12 +909,26 @@ private:
bool handleFileListInput(char c) {
int totalItems = 1 + (int)_fileList.size();
if (c == 'w' || c == 'W' || c == 0xF2) {
// Shift+W: page up
if (c == 'W') {
int pageSize = (128 - 14 - 14) / _prefs->smallLineH();
if (pageSize < 3) pageSize = 3;
_selectedFile = max(0, _selectedFile - pageSize);
return true;
}
if (c == 'w' || c == 0xF2) {
if (_selectedFile > 0) { _selectedFile--; return true; }
return false;
}
if (c == 's' || c == 'S' || c == 0xF1) {
// Shift+S: page down
if (c == 'S') {
int pageSize = (128 - 14 - 14) / _prefs->smallLineH();
if (pageSize < 3) pageSize = 3;
_selectedFile = min(totalItems - 1, _selectedFile + pageSize);
return true;
}
if (c == 's' || c == 0xF1) {
if (_selectedFile < totalItems - 1) { _selectedFile++; return true; }
return false;
}
@@ -402,12 +402,16 @@ private:
addRow(ROW_TX_POWER);
addRow(ROW_UTC_OFFSET);
addRow(ROW_MSG_NOTIFY);
#if HAS_GPS
addRow(ROW_GPS_BAUD);
#endif
addRow(ROW_PATH_HASH_SIZE);
addRow(ROW_DEFAULT_SCOPE);
addRow(ROW_DARK_MODE);
#if !defined(LILYGO_TECHO_LITE)
addRow(ROW_LARGE_FONT);
addRow(ROW_FONT_STYLE);
#endif
#if defined(LilyGo_T5S3_EPaper_Pro)
addRow(ROW_PORTRAIT_MODE);
#endif
@@ -2404,6 +2408,21 @@ public:
} else {
display.print("Editing...");
}
#elif defined(LILYGO_TECHO_LITE)
if (_editMode == EDIT_TEXT) {
display.print("Ent:Ok Q:Cancel");
} else if (_editMode == EDIT_PICKER) {
display.print("A/D:Pick Ent:Ok");
} else if (_editMode == EDIT_NUMBER) {
display.print("W/S:Adj Ent:Ok");
} else if (_editMode == EDIT_CONFIRM) {
// overlay handles it
} else {
display.print("Q:Bk");
const char* r = "Ent:Edit";
display.setCursor(display.width() - display.getTextWidth(r) - 2, footerY);
display.print(r);
}
#else
if (_editMode == EDIT_TEXT) {
display.print("Type, Enter:Ok Q:Cancel");
@@ -2977,8 +2996,17 @@ public:
// --- Normal browsing mode ---
// W/S: navigate
if (c == 'w' || c == 'W') {
// W/S: navigate, Shift+W/S: page scroll
if (c == 'W') {
// Shift+W: page up
int pageSize = (128 - 14 - 14) / _prefs->smallLineH();
if (pageSize < 3) pageSize = 3;
_cursor = max(0, _cursor - pageSize);
skipNonSelectable(-1);
Serial.printf("Settings: page up cursor=%d/%d\n", _cursor, _numRows);
return true;
}
if (c == 'w') {
if (_cursor > 0) {
_cursor--;
skipNonSelectable(-1);
@@ -2986,7 +3014,16 @@ public:
Serial.printf("Settings: cursor=%d/%d row=%d\n", _cursor, _numRows, _rows[_cursor].type);
return true;
}
if (c == 's' || c == 'S') {
if (c == 'S') {
// Shift+S: page down
int pageSize = (128 - 14 - 14) / _prefs->smallLineH();
if (pageSize < 3) pageSize = 3;
_cursor = min(_numRows - 1, _cursor + pageSize);
skipNonSelectable(1);
Serial.printf("Settings: page down cursor=%d/%d\n", _cursor, _numRows);
return true;
}
if (c == 's') {
if (_cursor < _numRows - 1) {
_cursor++;
skipNonSelectable(1);
@@ -1123,10 +1123,9 @@ private:
display.fillRect(0, y, display.width(), listLineH);
#else
// setCursor adds +5 to y internally, but fillRect does not.
// Built-in font: offset by +5 to align with top-left positioned text.
// GFX fonts (large_font or custom style): offset by -2 to cover ascenders above baseline.
int hlOff = (!_prefs->large_font && display.getFontStyle() > 0) ? -2 : _prefs->smallHighlightOff();
display.fillRect(0, y + hlOff, display.width(), listLineH);
// NodePrefs::smallHighlightOff() returns the correct offset for all
// font combinations (built-in, large_font, custom 7pt styles).
display.fillRect(0, y + _prefs->smallHighlightOff(), display.width(), listLineH);
#endif
display.setColor(DisplayDriver::DARK);
} else {
@@ -1759,7 +1758,7 @@ public:
#if defined(LilyGo_T5S3_EPaper_Pro)
const int bodyTop = startY;
#else
const int bodyTop = startY + ((_prefs && !_prefs->large_font && _prefs->ui_font_style > 0) ? -2 : (_prefs ? _prefs->smallHighlightOff() : 5));
const int bodyTop = startY + (_prefs ? _prefs->smallHighlightOff() : 5);
#endif
if (vy < bodyTop || vy >= 128 - footerH) return 0;
@@ -1812,8 +1811,15 @@ public:
bool handleFileListInput(char c) {
int total = totalListItems();
// Shift+W: page up
if (c == 'W') {
int pageSize = (128 - 14 - 14) / _prefs->smallLineH();
if (pageSize < 3) pageSize = 3;
_selectedFile = max(0, _selectedFile - pageSize);
return true;
}
// W - scroll up
if (c == 'w' || c == 'W' || c == 0xF2) {
if (c == 'w' || c == 0xF2) {
if (_selectedFile > 0) {
_selectedFile--;
return true;
@@ -1821,8 +1827,15 @@ public:
return false;
}
// Shift+S: page down
if (c == 'S') {
int pageSize = (128 - 14 - 14) / _prefs->smallLineH();
if (pageSize < 3) pageSize = 3;
_selectedFile = min(total - 1, _selectedFile + pageSize);
return true;
}
// S - scroll down
if (c == 's' || c == 'S' || c == 0xF1) {
if (c == 's' || c == 0xF1) {
if (_selectedFile < total - 1) {
_selectedFile++;
return true;
+293 -169
View File
@@ -1,7 +1,9 @@
#include "UITask.h"
#include <helpers/TxtDataHelpers.h>
#include "../MyMesh.h"
#if !defined(LILYGO_TECHO_LITE)
#include "NotesScreen.h"
#endif
#include "RepeaterAdminScreen.h"
#include "PathEditorScreen.h"
#include "DiscoveryScreen.h"
@@ -44,19 +46,15 @@
#endif
#endif
#if UI_HAS_JOYSTICK
#define PRESS_LABEL "press Enter"
#elif defined(LilyGo_T5S3_EPaper_Pro)
#define PRESS_LABEL "long press"
#else
#define PRESS_LABEL "long press"
#endif
#define PRESS_LABEL "long press"
#include "icons.h"
#include "ChannelScreen.h"
#include "ChannelPickerScreen.h"
#include "ContactsScreen.h"
#if !defined(LILYGO_TECHO_LITE)
#include "TextReaderScreen.h"
#endif
#include "SettingsScreen.h"
#ifdef MECK_AUDIO_VARIANT
#include "AudiobookPlayerScreen.h"
@@ -143,6 +141,9 @@ class HomeScreen : public UIScreen {
uint8_t _page;
bool _shutdown_init;
unsigned long _shutdown_at; // earliest time to proceed with shutdown (after e-ink refresh)
bool _poweroff_selected; // true = "power off" highlighted, false = "hibernate"
bool _poweroff_confirm; // true = showing confirmation prompt for power off
bool _poweroff_msg_shown; // true = "powering off..." already displayed once
bool _editing_utc;
int8_t _saved_utc_offset; // for cancel/undo
@@ -150,8 +151,16 @@ class HomeScreen : public UIScreen {
void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts, int* outIconX = nullptr) {
// Use voltage-based estimation to match BLE app readings
// Use BQ27220 fuel gauge State of Charge when available — it tracks
// the real LiPo discharge curve, impedance, and temperature. The old
// linear voltage mapping (3.04.2V) over-reports while charging
// (voltage inflated by charger current) and is non-linear across the
// flat middle of the discharge curve.
uint8_t batteryPercentage = 0;
#if HAS_BQ27220
batteryPercentage = _task->getBatteryPercent();
#else
// Fallback for boards without a fuel gauge
if (batteryMilliVolts > 0) {
const int minMilliVolts = 3000;
const int maxMilliVolts = 4200;
@@ -160,6 +169,7 @@ void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts,
if (pct > 100) pct = 100;
batteryPercentage = (uint8_t)pct;
}
#endif
display.setColor(DisplayDriver::GREEN);
display.setTextSize(_node_prefs->smallTextSize());
@@ -175,6 +185,16 @@ void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts,
display.setCursor(textX, 0);
display.print(battStr);
display.setTextSize(1); // restore default text size
#elif defined(LILYGO_TECHO_LITE)
// T-Echo Lite: text-only battery (icon misaligns due to fillRect/setCursor offset mismatch at 2× scale)
char battStr[8];
snprintf(battStr, sizeof(battStr), "%d%%", batteryPercentage);
uint16_t textWidth = display.getTextWidth(battStr);
int textX = display.width() - textWidth - 2;
if (outIconX) *outIconX = textX;
display.setCursor(textX, 0); // Same baseline as node name (HOME_HDR_Y)
display.print(battStr);
display.setTextSize(1);
#else
// T-Deck Pro: icon + percentage text (icon hidden in large font)
int iconWidth = 16;
@@ -287,10 +307,12 @@ void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts,
public:
HomeScreen(UITask* task, mesh::RTCClock* rtc, SensorManager* sensors, NodePrefs* node_prefs)
: _task(task), _rtc(rtc), _sensors(sensors), _node_prefs(node_prefs), _page(0),
_shutdown_init(false), _shutdown_at(0), _editing_utc(false), _saved_utc_offset(0), sensors_lpp(200) { }
_shutdown_init(false), _shutdown_at(0), _poweroff_selected(false), _poweroff_confirm(false),
_poweroff_msg_shown(false), _editing_utc(false), _saved_utc_offset(0), sensors_lpp(200) { }
bool isEditingUTC() const { return _editing_utc; }
bool isOnRecentPage() const { return _page == HomePage::RECENT; }
bool isOnShutdownPage() const { return _page == HomePage::SHUTDOWN; }
void cancelEditing() {
if (_editing_utc) {
_node_prefs->utc_offset_hours = _saved_utc_offset;
@@ -300,6 +322,7 @@ public:
void poll() override {
if (_shutdown_init && millis() >= _shutdown_at && !_task->isButtonPressed()) {
if (_poweroff_selected) _task->setFullPowerOff(true);
_task->shutdown();
}
}
@@ -309,6 +332,27 @@ public:
#if defined(LilyGo_T5S3_EPaper_Pro)
_task->setHomeShowingTiles(false); // Reset — only set true on FIRST page
#endif
// Power off: full-screen message, no header
// First render: "powering off..." + wake instruction
// Second render onward: wake instruction only (persists on e-ink)
if (_shutdown_init && _poweroff_selected) {
#if defined(LilyGo_T5S3_EPaper_Pro)
board.setBacklight(false);
#endif
display.setColor(DisplayDriver::GREEN);
display.setTextSize(1);
if (!_poweroff_msg_shown) {
_poweroff_msg_shown = true;
display.drawTextCentered(display.width() / 2, 30, "powering off...");
display.drawTextCentered(display.width() / 2, 46, "plug in USB-C to turn on");
return 1500;
} else {
display.drawTextCentered(display.width() / 2, 38, "plug in USB-C to turn on");
return 5000;
}
}
// node name (tinyfont to avoid overlapping clock)
display.setTextSize(_node_prefs->smallTextSize());
display.setColor(DisplayDriver::GREEN);
@@ -318,6 +362,8 @@ public:
// T5S3: FreeSans12pt ascenders need more room than built-in font.
// Shift header elements down by 4 virtual units (~17px physical).
#define HOME_HDR_Y 1
#elif defined(LILYGO_TECHO_LITE)
#define HOME_HDR_Y 0
#else
#define HOME_HDR_Y -3
#endif
@@ -365,7 +411,9 @@ public:
}
}
// curr page indicator
#if defined(LilyGo_T5S3_EPaper_Pro)
#if defined(LILYGO_TECHO_LITE)
int y = 13; // Below header
#elif defined(LilyGo_T5S3_EPaper_Pro)
int y = 14; // Closer to header
#else
int y = 14;
@@ -389,6 +437,8 @@ public:
#else
int y = 26; // Standalone: extra line below dots (no IP/Connected row)
#endif
#elif defined(LILYGO_TECHO_LITE)
int y = 18; // Below page dots
#else
int y = 20;
#endif
@@ -396,7 +446,11 @@ public:
display.setTextSize(2);
sprintf(tmp, "MSG: %d", _task->getUnreadMsgCount());
display.drawTextCentered(display.width() / 2, y, tmp);
#if defined(LILYGO_TECHO_LITE)
y += 12; // Compact
#else
y += 14; // Reduced from 18
#endif
#if defined(WIFI_SSID) || defined(MECK_WIFI_COMPANION)
IPAddress ip = WiFi.localIP();
@@ -419,7 +473,11 @@ public:
display.setTextSize(2);
sprintf(tmp, "Pin:%d", the_mesh.getBLEPin());
display.drawTextCentered(display.width() / 2, y, tmp);
#if defined(LILYGO_TECHO_LITE)
y += 14; // Compact
#else
y += 18;
#endif
#endif
}
#endif
@@ -476,6 +534,24 @@ public:
}
display.setTextSize(1);
#else
// Non-T5S3: keyboard shortcut menu
#if defined(LILYGO_TECHO_LITE)
// T-Echo Lite: compact centered menu (tiny font fits 117px virtual width)
display.setColor(DisplayDriver::LIGHT);
display.setTextSize(0); // 6×8 built-in font
y += 2;
display.drawTextCentered(display.width() / 2, y, "M:Msgs C:Contacts");
y += 8;
display.drawTextCentered(display.width() / 2, y, "S:Set F:Discover");
y += 8;
display.drawTextCentered(display.width() / 2, y, "H:Last Heard");
y += 9;
if (y < display.height() - 14) {
display.setColor(DisplayDriver::GREEN);
display.drawTextCentered(display.width() / 2, y, "Arrows: cycle views");
}
display.setTextSize(1); // restore
#else
// ----- T-Deck Pro: Keyboard shortcut text menu -----
display.setColor(DisplayDriver::LIGHT);
@@ -487,12 +563,10 @@ public:
y += 2;
int col1, col2;
if (_node_prefs->large_font) {
// 9pt font: measure widest left entry and place col2 just past it
col1 = 2;
int leftW = display.getTextWidth("[M] Messages");
col2 = col1 + leftW + 3;
} else {
// Custom tiny (7pt): centered layout
col1 = display.width() / 10;
col2 = display.width() * 11 / 20;
}
@@ -574,6 +648,7 @@ public:
(_node_prefs->large_font || display.getFontStyle() > 0) ? "A/D: cycle views" : "Press A/D to cycle home views");
}
display.setTextSize(1); // restore
#endif // LILYGO_TECHO_LITE
#endif
} else if (_page == HomePage::RECENT) {
the_mesh.getRecentlyHeard(recent, UI_RECENT_LIST_SIZE);
@@ -662,7 +737,8 @@ public:
#if defined(LilyGo_T5S3_EPaper_Pro)
display.drawTextCentered(display.width() / 2, 80, "toggle: " PRESS_LABEL);
#else
display.drawTextCentered(display.width() / 2, 72, "toggle: " PRESS_LABEL);
display.drawTextCentered(display.width() / 2, 68, "toggle: " PRESS_LABEL);
display.drawTextCentered(display.width() / 2, 78, "or press Enter key");
#endif
#endif
#ifdef MECK_WIFI_COMPANION
@@ -714,7 +790,8 @@ public:
#if defined(LilyGo_T5S3_EPaper_Pro)
display.drawTextCentered(display.width() / 2, 64, "advert: " PRESS_LABEL);
#else
display.drawTextCentered(display.width() / 2, 64 - 11, "advert: " PRESS_LABEL);
display.drawTextCentered(display.width() / 2, 57, "advert: " PRESS_LABEL);
display.drawTextCentered(display.width() / 2, 67, "or press Enter key");
#endif
#if ENV_INCLUDE_GPS == 1
} else if (_page == HomePage::GPS) {
@@ -944,20 +1021,43 @@ public:
display.setTextSize(1);
if (_shutdown_init) {
#if defined(LilyGo_T5S3_EPaper_Pro)
board.setBacklight(false); // Turn off backlight on hibernate
board.setBacklight(false);
#endif
display.drawTextCentered(display.width() / 2, 34, "hibernating...");
} else {
} else if (_poweroff_confirm) {
// Confirmation prompt for power off
#if defined(LilyGo_T5S3_EPaper_Pro)
display.drawXbm((display.width() - 32) / 2, 28, power_icon, 32, 32);
#else
display.drawXbm((display.width() - 32) / 2, 18, power_icon, 32, 32);
display.drawXbm((display.width() - 32) / 2, 10, power_icon, 32, 32);
#endif
#if defined(LilyGo_T5S3_EPaper_Pro)
display.drawTextCentered(display.width() / 2, 64, "hibernate:" PRESS_LABEL);
display.drawTextCentered(display.width() / 2, 64, "power off device?");
display.drawTextCentered(display.width() / 2, 76, "usb-c to wake");
#else
display.drawTextCentered(display.width() / 2, 64 - 11, "hibernate:" PRESS_LABEL);
display.drawTextCentered(display.width() / 2, 50, "power off device?");
display.drawTextCentered(display.width() / 2, 60, "usb-c to wake");
display.drawTextCentered(display.width() / 2, 76, "Enter:yes q:no");
#endif
} else {
// Menu: hibernate / power off
#if defined(LilyGo_T5S3_EPaper_Pro)
display.drawXbm((display.width() - 32) / 2, 20, power_icon, 32, 32);
const int y1 = 58, y2 = 70;
#else
display.drawXbm((display.width() - 32) / 2, 10, power_icon, 32, 32);
const int y1 = 50, y2 = 62;
#endif
char line1[48], line2[48];
#if defined(LilyGo_TDeck_Pro)
snprintf(line1, sizeof(line1), "%shibernate: long press/Enter", _poweroff_selected ? " " : ">");
snprintf(line2, sizeof(line2), "%spower off: long press/Enter", _poweroff_selected ? ">" : " ");
#else
snprintf(line1, sizeof(line1), "%shibernate: " PRESS_LABEL, _poweroff_selected ? " " : ">");
snprintf(line2, sizeof(line2), "%spower off: " PRESS_LABEL, _poweroff_selected ? ">" : " ");
#endif
display.drawTextCentered(display.width() / 2, y1, line1);
display.drawTextCentered(display.width() / 2, y2, line2);
}
}
return _editing_utc ? 700 : 5000;
@@ -998,6 +1098,39 @@ public:
return true; // Consume all other keys while editing
}
// SHUTDOWN page -- intercept up/down and Enter before page cycling
if (_page == HomePage::SHUTDOWN) {
if (_poweroff_confirm) {
// Confirmation mode for power off
if (c == KEY_ENTER) {
_shutdown_init = true;
_shutdown_at = millis() + 2500; // extra time for two-phase e-ink update
return true;
}
// Cancel: q, left, prev
if (c == 'q' || c == KEY_LEFT || c == KEY_PREV) {
_poweroff_confirm = false;
return true;
}
return true; // eat all other keys while confirming
}
// Up/down toggles between hibernate and power off
if (c == KEY_NEXT || c == 's' || c == KEY_PREV || c == 'w') {
_poweroff_selected = !_poweroff_selected;
return true;
}
if (c == KEY_ENTER) {
if (_poweroff_selected) {
_poweroff_confirm = true;
} else {
_shutdown_init = true;
_shutdown_at = millis() + 900;
}
return true;
}
// Left/right fall through to page cycling below
}
if (c == KEY_LEFT || c == KEY_PREV) {
_page = (_page + HomePage::Count - 1) % HomePage::Count;
return true;
@@ -1046,107 +1179,11 @@ public:
return true;
}
#endif
if (c == KEY_ENTER && _page == HomePage::SHUTDOWN) {
_shutdown_init = true;
_shutdown_at = millis() + 900; // allow e-ink refresh (644ms) before shutdown
return true;
}
return false;
}
};
class MsgPreviewScreen : public UIScreen {
UITask* _task;
mesh::RTCClock* _rtc;
struct MsgEntry {
uint32_t timestamp;
char origin[62];
char msg[78];
};
#define MAX_UNREAD_MSGS 32
int num_unread;
MsgEntry unread[MAX_UNREAD_MSGS];
public:
MsgPreviewScreen(UITask* task, mesh::RTCClock* rtc) : _task(task), _rtc(rtc) { num_unread = 0; }
void addPreview(uint8_t path_len, const char* from_name, const char* msg) {
if (num_unread >= MAX_UNREAD_MSGS) return; // full
auto p = &unread[num_unread++];
p->timestamp = _rtc->getCurrentTime();
if (path_len == 0xFF) {
sprintf(p->origin, "(D) %s:", from_name);
} else {
sprintf(p->origin, "(%d) %s:", (uint32_t) path_len, from_name);
}
StrHelper::strncpy(p->msg, msg, sizeof(p->msg));
}
int render(DisplayDriver& display) override {
char tmp[16];
display.setCursor(0, 0);
display.setTextSize(1);
display.setColor(DisplayDriver::GREEN);
sprintf(tmp, "Unread: %d", num_unread);
display.print(tmp);
auto p = &unread[0];
int secs = _rtc->getCurrentTime() - p->timestamp;
if (secs < 60) {
sprintf(tmp, "%ds", secs);
} else if (secs < 60*60) {
sprintf(tmp, "%dm", secs / 60);
} else {
sprintf(tmp, "%dh", secs / (60*60));
}
display.setCursor(display.width() - display.getTextWidth(tmp) - 2, 0);
display.print(tmp);
display.drawRect(0, 11, display.width(), 1); // horiz line
display.setCursor(0, 14);
display.setColor(DisplayDriver::YELLOW);
char filtered_origin[sizeof(p->origin)];
display.translateUTF8ToBlocks(filtered_origin, p->origin, sizeof(filtered_origin));
display.print(filtered_origin);
display.setCursor(0, 25);
display.setColor(DisplayDriver::LIGHT);
char filtered_msg[sizeof(p->msg)];
display.translateUTF8ToBlocks(filtered_msg, p->msg, sizeof(filtered_msg));
display.printWordWrap(filtered_msg, display.width());
#if AUTO_OFF_MILLIS==0 // probably e-ink
return 10000; // 10 s
#else
return 1000; // next render after 1000 ms
#endif
}
bool handleInput(char c) override {
if (c == KEY_NEXT || c == KEY_RIGHT) {
num_unread--;
if (num_unread == 0) {
_task->gotoHomeScreen();
} else {
// delete first/curr item from unread queue
for (int i = 0; i < num_unread; i++) {
unread[i] = unread[i + 1];
}
}
return true;
}
if (c == KEY_ENTER) {
num_unread = 0; // clear unread queue
_task->gotoHomeScreen();
return true;
}
return false;
}
};
// MsgPreviewScreen removed — all platforms now use toast alerts for new messages
// ==========================================================================
// Lock Screen — T5S3 and T-Deck Pro
@@ -1189,13 +1226,17 @@ public:
// ---- Battery + unread on one line ----
display.setTextSize(1);
{
uint16_t mv = _task->getBattMilliVolts();
int pct = 0;
#if HAS_BQ27220
pct = _task->getBatteryPercent();
#else
uint16_t mv = _task->getBattMilliVolts();
if (mv > 0) {
pct = ((mv - 3000) * 100) / (4200 - 3000);
if (pct < 0) pct = 0;
if (pct > 100) pct = 100;
}
#endif
int unread = _task->getUnreadMsgCount();
char infoBuf[32];
@@ -1293,15 +1334,19 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
splash = new SplashScreen(this);
home = new HomeScreen(this, &rtc_clock, sensors, node_prefs);
msg_preview = new MsgPreviewScreen(this, &rtc_clock);
channel_screen = new ChannelScreen(this, &rtc_clock);
((ChannelScreen*)channel_screen)->setDMUnreadPtr(_dmUnread);
channel_picker_screen = new ChannelPickerScreen(this);
((ChannelPickerScreen*)channel_picker_screen)->setChannelScreen((ChannelScreen*)channel_screen);
contacts_screen = new ContactsScreen(this, &rtc_clock);
((ContactsScreen*)contacts_screen)->setDMUnreadPtr(_dmUnread);
#if !defined(LILYGO_TECHO_LITE)
text_reader = new TextReaderScreen(this, node_prefs);
notes_screen = new NotesScreen(this, node_prefs);
#else
text_reader = nullptr; // T-Echo Lite: excluded to save RAM (256KB nRF52)
notes_screen = nullptr;
#endif
settings_screen = new SettingsScreen(this, &rtc_clock, node_prefs);
repeater_admin = nullptr; // Lazy-initialized on first use to preserve heap for audio
path_editor = nullptr; // Lazy-initialized on first use from contacts screen
@@ -1410,9 +1455,6 @@ switch(t){
void UITask::msgRead(int msgcount) {
_msgcount = msgcount;
if (msgcount == 0 && curr == msg_preview) {
gotoHomeScreen();
}
}
void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount,
@@ -1437,9 +1479,6 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i
_dedup[_dedupIdx].millis = now;
_dedupIdx = (_dedupIdx + 1) % MSG_DEDUP_SIZE;
// Add to preview screen (for notifications on non-keyboard devices)
((MsgPreviewScreen *) msg_preview)->addPreview(path_len, from_name, text);
// Determine channel index by looking up the channel name
// For channel messages, from_name is the channel name
// For contact messages, from_name is the contact name (channel_idx = 0xFF)
@@ -1483,15 +1522,17 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i
((ChannelScreen *) channel_screen)->addMessage(channel_idx, path_len, from_name, text, path, snr);
}
// If user is currently viewing this channel, mark it as read immediately
// (they can see the message arrive in real-time)
if (isOnChannelScreen() &&
((ChannelScreen *) channel_screen)->getViewChannelIdx() == channel_idx) {
// If user is currently viewing this channel on the device, or companion
// app is connected (they'll see it there), mark as read immediately
if ((isOnChannelScreen() &&
((ChannelScreen *) channel_screen)->getViewChannelIdx() == channel_idx) ||
hasConnection()) {
((ChannelScreen *) channel_screen)->markChannelRead(channel_idx);
}
// Per-contact DM unread tracking: find contact index by name
if (channel_idx == 0xFF && _dmUnread) {
// Skip increment when companion app is connected (user sees DMs there)
if (channel_idx == 0xFF && _dmUnread && !hasConnection()) {
uint32_t numContacts = the_mesh.getNumContacts();
ContactInfo contact;
for (uint32_t ci = 0; ci < numContacts; ci++) {
@@ -1502,7 +1543,6 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i
}
}
#if defined(LilyGo_TDeck_Pro) || defined(LilyGo_T5S3_EPaper_Pro)
// Don't interrupt user with popup - just show brief notification
// Messages are stored in channel history, accessible via tile/key
// Suppress toasts for room server messages (bulk sync would spam toasts)
@@ -1515,10 +1555,6 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i
if (isOnChannelPickerScreen()) {
forceRefresh();
}
#else
// Other devices: Show full preview screen (legacy behavior, skip room sync)
if (!isRoomMsg) setCurrScreen(msg_preview);
#endif
if (_display != NULL) {
if (!_display->isOn() && !hasConnection()) {
@@ -1626,6 +1662,36 @@ void UITask::shutdown(bool restart){
// Power off LoRa radio, display, and board
radio_driver.powerOff();
_display->turnOff();
// BQ25896 ship mode: disconnect battery from VSYS entirely.
// Must happen BEFORE _board->powerOff() cuts PIN_PERF_POWERON
// (I2C pull-ups need VDD3V3 to complete the transaction).
// TI recommends: set BATFET_DLY=1 first, then BATFET_DIS=1 as
// the last I2C write to avoid bricking the I2C state machine.
// After tSM_DLY (~10-15s) the BATFET opens during deep sleep.
// Wake: USB-C plug-in only (no reset button -- no power to ESP32).
#ifdef I2C_ADDR_BQ25896
if (_full_poweroff) {
Wire.beginTransmission(I2C_ADDR_BQ25896);
Wire.write(0x09);
Wire.endTransmission(false);
Wire.requestFrom((uint8_t)I2C_ADDR_BQ25896, (uint8_t)1);
uint8_t reg09 = Wire.read();
// Step 1: set BATFET_DLY=1 (bit 3) for safe I2C completion
Wire.beginTransmission(I2C_ADDR_BQ25896);
Wire.write(0x09);
Wire.write(reg09 | 0x08); // BATFET_DLY = bit 3
Wire.endTransmission();
// Step 2: set BATFET_DIS=1 (bit 5) -- MUST be the last I2C write
Wire.beginTransmission(I2C_ADDR_BQ25896);
Wire.write(0x09);
Wire.write(reg09 | 0x28); // BATFET_DIS (0x20) | BATFET_DLY (0x08)
Wire.endTransmission();
}
#endif
_board->powerOff();
}
}
@@ -1640,30 +1706,7 @@ bool UITask::isButtonPressed() const {
void UITask::loop() {
char c = 0;
#if UI_HAS_JOYSTICK
int ev = user_btn.check();
if (ev == BUTTON_EVENT_CLICK) {
c = checkDisplayOn(KEY_ENTER);
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
c = handleLongPress(KEY_ENTER); // REVISIT: could be mapped to different key code
}
ev = joystick_left.check();
if (ev == BUTTON_EVENT_CLICK) {
c = checkDisplayOn(KEY_LEFT);
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
c = handleLongPress(KEY_LEFT);
}
ev = joystick_right.check();
if (ev == BUTTON_EVENT_CLICK) {
c = checkDisplayOn(KEY_RIGHT);
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
c = handleLongPress(KEY_RIGHT);
}
ev = back_btn.check();
if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
c = handleTripleClick(KEY_SELECT);
}
#elif defined(PIN_USER_BTN)
#if defined(PIN_USER_BTN)
int ev = user_btn.check();
if (ev == BUTTON_EVENT_CLICK) {
#if defined(LilyGo_T5S3_EPaper_Pro)
@@ -1678,6 +1721,7 @@ void UITask::loop() {
c = checkDisplayOn(KEY_NEXT);
} else {
// Navigate back: reader reading→file list, file list→home, others→home
#if !defined(LILYGO_TECHO_LITE)
if (isOnTextReader()) {
TextReaderScreen* reader = (TextReaderScreen*)text_reader;
if (reader && reader->isReading()) {
@@ -1695,7 +1739,9 @@ void UITask::loop() {
gotoHomeScreen();
}
c = 0;
} else if (isOnChannelPickerScreen()) {
} else
#endif
if (isOnChannelPickerScreen()) {
gotoHomeScreen(); // picker → home
c = 0;
} else if (isOnChannelScreen()) {
@@ -1835,6 +1881,12 @@ if (curr) curr->poll();
if (_display != NULL && _display->isOn()) {
if (millis() >= _next_refresh && curr) {
// Defer display refresh while BLE is actively transferring contacts.
// E-ink partial update blocks for ~820ms, stalling the BLE send queue
// and adding ~1.6s of dead time to a full contact sync.
if (_serial != NULL && _serial->hasPendingData()) {
_next_refresh = millis() + 500; // Re-check in 500ms
} else {
// Sync dark mode with prefs (settings toggle takes effect here)
if (_node_prefs && display.isDarkMode() != (_node_prefs->dark_mode != 0)) {
display.setDarkMode(_node_prefs->dark_mode != 0);
@@ -1991,12 +2043,17 @@ if (curr) curr->poll();
#endif
_display->endFrame();
// E-ink render throttle: enforce minimum 800ms between renders.
// Each partial update blocks for ~644ms. Without this floor, incoming
// mesh notifications can trigger back-to-back renders that starve the
// keyboard polling loop, causing TCA8418 FIFO overflow and lost keys.
unsigned long minNext = millis() + 800;
// E-ink render throttle: enforce minimum interval between renders.
// Partial update blocks for ~644ms; full refresh blocks for ~3000ms.
// Without this floor, changing readings (battery, uptime) trigger
// back-to-back renders that cause continuous flashing.
#ifdef EINK_FULL_REFRESH_ONLY
unsigned long minNext = millis() + 300000; // Full refresh: 5 min idle
#else
unsigned long minNext = millis() + 800; // Partial refresh: 800ms floor
#endif
if (_next_refresh < minNext) _next_refresh = minNext;
} // end else (not bulk syncing)
}
#if AUTO_OFF_MILLIS > 0
if (millis() > _auto_off) {
@@ -2018,11 +2075,27 @@ if (curr) curr->poll();
}
}
// Lock screen clock refresh — update time display every 15 minutes.
// Runs outside the _display->isOn() gate so it works even after auto-off.
// Wakes the display briefly to render, then lets auto-off turn it back off.
// Lock screen clock refresh — keeps the displayed time current.
// T-Deck Pro: every 1 minute. T5S3: every 2 minutes.
// Wakes the display driver briefly to render, then auto-off handles it.
// T5S3 standalone: no refreshes once powersaving begins — the device
// shows "hibernating..." and enters light sleep instead.
#if defined(LilyGo_T5S3_EPaper_Pro) && !defined(BLE_PIN_CODE) && !defined(MECK_WIFI_COMPANION)
// T5S3 standalone: only refresh while still active (before powersaving kicks in)
if (_locked && _display != NULL && _display->isOn()) {
const unsigned long LOCK_REFRESH_INTERVAL = 2UL * 60UL * 1000UL; // 2 minutes
#elif defined(LilyGo_T5S3_EPaper_Pro)
// T5S3 BLE/WiFi: refresh every 2 minutes
if (_locked && _display != NULL) {
const unsigned long LOCK_REFRESH_INTERVAL = 15UL * 60UL * 1000UL; // 15 minutes
const unsigned long LOCK_REFRESH_INTERVAL = 2UL * 60UL * 1000UL; // 2 minutes
#elif defined(LilyGo_TDeck_Pro)
// T-Deck Pro: refresh every 1 minute
if (_locked && _display != NULL) {
const unsigned long LOCK_REFRESH_INTERVAL = 1UL * 60UL * 1000UL; // 1 minute
#else
if (_locked && _display != NULL) {
const unsigned long LOCK_REFRESH_INTERVAL = 2UL * 60UL * 1000UL; // 2 minutes
#endif
if (millis() - _lastLockRefresh >= LOCK_REFRESH_INTERVAL) {
_lastLockRefresh = millis();
if (!_display->isOn()) {
@@ -2044,6 +2117,20 @@ if (curr) curr->poll();
if (_locked && _display != NULL && !_display->isOn()) {
unsigned long now = millis();
if (now - _psLastActive >= _psNextSleepSecs * 1000UL) {
// First sleep entry: render a static "hibernating..." frame on the
// e-ink. Since e-ink retains its image indefinitely without power,
// this tells the user the device is in low-power mode until they
// wake it with the boot button.
if (_psNextSleepSecs == 60) {
_display->turnOn();
_display->startFrame();
_display->setTextSize(1);
_display->setColor(DisplayDriver::GREEN);
_display->drawTextCentered(_display->width() / 2, 34, "hibernating...");
_display->endFrame();
delay(700); // Allow e-ink refresh to complete
_display->turnOff();
}
Serial.println("[POWERSAVE] Entering light sleep (locked+idle)");
board.sleep(1800); // Light sleep up to 30 min
// ── CPU resumes here on wake ──
@@ -2195,7 +2282,7 @@ void UITask::lockScreen() {
#endif
_next_refresh = 0; // Draw lock screen immediately
_auto_off = millis() + 60000; // 60s before display off while locked
_lastLockRefresh = millis(); // Start 15-min clock refresh cycle
_lastLockRefresh = millis(); // Start lock screen clock refresh cycle
#if defined(LilyGo_T5S3_EPaper_Pro) && !defined(BLE_PIN_CODE) && !defined(MECK_WIFI_COMPANION)
_psLastActive = millis(); // Start powersaving countdown (60s to first sleep)
_psNextSleepSecs = 60;
@@ -2336,12 +2423,14 @@ void UITask::onVKBSubmit() {
break;
}
case VKB_NOTES: {
#if !defined(LILYGO_TECHO_LITE)
NotesScreen* notes = (NotesScreen*)getNotesScreen();
if (notes && strlen(text) > 0) {
for (int i = 0; text[i]; i++) {
notes->handleInput(text[i]);
}
}
#endif
if (_screenBeforeVKB) setCurrScreen(_screenBeforeVKB);
break;
}
@@ -2403,6 +2492,7 @@ void UITask::onVKBSubmit() {
}
#endif
case VKB_TEXT_PAGE: {
#if !defined(LILYGO_TECHO_LITE)
if (strlen(text) > 0) {
int pageNum = atoi(text);
TextReaderScreen* reader = (TextReaderScreen*)getTextReaderScreen();
@@ -2410,6 +2500,7 @@ void UITask::onVKBSubmit() {
reader->gotoPage(pageNum);
}
}
#endif
if (_screenBeforeVKB) setCurrScreen(_screenBeforeVKB);
break;
}
@@ -2524,9 +2615,24 @@ void UITask::injectKey(char c) {
if (_next_refresh < earliest) {
_next_refresh = earliest;
}
}
#ifdef EINK_FULL_REFRESH_ONLY
// Full-refresh displays (SSD1681): debounce printable character input.
// Compose typing (0x20-0x7E) pushes the render 2.5s into the future so
// the user can type a whole word before a ~2.2s full refresh fires.
// Navigation/special keys (arrows, enter, escape, etc.) refresh
// immediately so scrolling and screen changes remain responsive.
else if ((unsigned char)c >= 0x20 && (unsigned char)c <= 0x7E) {
unsigned long earliest = millis() + 2500;
if (_next_refresh < earliest) _next_refresh = earliest;
} else {
_next_refresh = 100; // navigation key — refresh now
}
#else
else {
_next_refresh = 100; // trigger refresh
}
#endif
}
}
@@ -2558,6 +2664,10 @@ bool UITask::isHomeOnRecentPage() const {
return curr == home && ((HomeScreen *) home)->isOnRecentPage();
}
bool UITask::isHomeOnShutdownPage() const {
return curr == home && ((HomeScreen *) home)->isOnShutdownPage();
}
void UITask::gotoChannelScreen(bool resetDmView) {
ChannelScreen* cs = (ChannelScreen*)channel_screen;
// If currently showing DM view, reset to channel 0 (unless caller opts out)
@@ -2622,6 +2732,8 @@ void UITask::gotoContactsScreen() {
}
void UITask::gotoTextReader() {
if (!text_reader) return; // Not available on this platform
#if !defined(LILYGO_TECHO_LITE)
TextReaderScreen* reader = (TextReaderScreen*)text_reader;
if (_display != NULL) {
reader->enter(*_display);
@@ -2632,9 +2744,12 @@ void UITask::gotoTextReader() {
}
_auto_off = millis() + AUTO_OFF_MILLIS;
_next_refresh = 100;
#endif
}
void UITask::gotoNotesScreen() {
if (!notes_screen) return; // Not available on this platform
#if !defined(LILYGO_TECHO_LITE)
NotesScreen* notes = (NotesScreen*)notes_screen;
if (_display != NULL) {
notes->enter(*_display);
@@ -2649,6 +2764,7 @@ void UITask::gotoNotesScreen() {
}
_auto_off = millis() + AUTO_OFF_MILLIS;
_next_refresh = 100;
#endif
}
void UITask::gotoSettingsScreen() {
@@ -2765,6 +2881,14 @@ void UITask::markChannelReadFromBLE(uint8_t channel_idx) {
_next_refresh = millis() + 200;
}
void UITask::markAllChannelsRead() {
((ChannelScreen *) channel_screen)->markAllRead();
if (_dmUnread) {
memset(_dmUnread, 0, MAX_CONTACTS * sizeof(uint8_t));
}
_next_refresh = millis() + 200;
}
bool UITask::hasDMUnread(int contactIdx) const {
if (!_dmUnread || contactIdx < 0 || contactIdx >= MAX_CONTACTS) return false;
return _dmUnread[contactIdx] > 0;
+6
View File
@@ -66,6 +66,7 @@ class UITask : public AbstractUITask {
int _msgcount;
unsigned long ui_started_at, next_batt_chck;
uint8_t _low_batt_count = 0; // Consecutive low-voltage readings for debounce
bool _full_poweroff = false; // True = BQ25896 BATFET disconnect (USB-C wake only)
int next_backlight_btn_check = 0;
#ifdef PIN_STATUS_LED
int led_state = 0;
@@ -215,6 +216,8 @@ public:
void showBootHint(bool immediate = false); // Show navigation hint overlay on first boot
void dismissBootHint(); // Dismiss hint and save preference
bool isHintActive() const { return _hintActive; }
// BQ25896 BATFET disconnect -- true power off, USB-C required to wake
void setFullPowerOff(bool v) { _full_poweroff = v; }
// Wake display and extend auto-off timer. Call this when handling keys
// outside of injectKey() to prevent display auto-off during direct input.
void keepAlive() {
@@ -291,6 +294,7 @@ public:
bool isEditingHomeScreen() const;
// Check if home screen is showing the Recent Adverts page
bool isHomeOnRecentPage() const;
bool isHomeOnShutdownPage() const;
// Inject a key press from external source (e.g., keyboard)
void injectKey(char c);
@@ -301,6 +305,8 @@ public:
// Mark channel as read when BLE companion app syncs messages
void markChannelReadFromBLE(uint8_t channel_idx) override;
// Mark all channels + DMs as read (companion app connected)
void markAllChannelsRead() override;
// Repeater admin callbacks
void onAdminLoginResult(bool success, uint8_t permissions, uint32_t server_time) override;
@@ -91,6 +91,7 @@ static const char* EMOJI_LABELS[EMOJI_COUNT] = {
"HFC", // 73 passport_control
"Star", // 74 eight_spoked_asterisk
"Sig", // 75 signal_strength
"Beer", // 76 beer
};
struct EmojiPicker {
@@ -1,310 +0,0 @@
#ifndef MONTSERRATBOLD12PT7B_H
#define MONTSERRATBOLD12PT7B_H
const uint8_t Montserrat_Bold12pt7bBitmaps[] PROGMEM = {
0x00, 0xFF, 0xFF, 0xF7, 0x39, 0xCE, 0x73, 0x9C, 0x00, 0x3B, 0xFF, 0x70,
0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0x07, 0x1C, 0x03, 0x8E, 0x01,
0xC7, 0x00, 0xE3, 0x87, 0xFF, 0xFB, 0xFF, 0xFD, 0xFF, 0xFE, 0x1C, 0x30,
0x0E, 0x38, 0x07, 0x1C, 0x03, 0x8E, 0x1F, 0xFF, 0xEF, 0xFF, 0xF0, 0x61,
0xC0, 0x70, 0xC0, 0x38, 0x60, 0x1C, 0x70, 0x00, 0x03, 0x00, 0x0C, 0x00,
0x30, 0x07, 0xF8, 0x3F, 0xF9, 0xFF, 0xCF, 0xB3, 0x3C, 0xC0, 0xF3, 0x03,
0xEC, 0x07, 0xF8, 0x0F, 0xFC, 0x1F, 0xF8, 0x0F, 0xE0, 0x37, 0xC0, 0xCF,
0xE3, 0x7B, 0xFF, 0xEF, 0xFF, 0x0F, 0xF8, 0x03, 0x00, 0x0C, 0x00, 0x30,
0x00, 0x3C, 0x03, 0x0F, 0xC0, 0xE3, 0x9C, 0x38, 0x63, 0x86, 0x0C, 0x31,
0xC1, 0x8E, 0x70, 0x31, 0xCC, 0x07, 0xF3, 0x80, 0x7C, 0xE7, 0x80, 0x39,
0xF8, 0x07, 0x73, 0x81, 0xCE, 0x30, 0x71, 0x86, 0x0E, 0x38, 0xC3, 0x87,
0x38, 0xE0, 0x7F, 0x1C, 0x07, 0xC0, 0x0F, 0xC0, 0x1F, 0xE0, 0x3F, 0xF0,
0x38, 0x70, 0x38, 0x70, 0x3C, 0xF0, 0x1F, 0xE0, 0x1F, 0xC0, 0x3F, 0x88,
0x7F, 0xCE, 0xF1, 0xEE, 0xF0, 0xFE, 0xF0, 0x7C, 0xF0, 0x7C, 0xFF, 0xFE,
0x7F, 0xFE, 0x1F, 0xC6, 0xFF, 0xFF, 0xF8, 0x1C, 0xF7, 0x9E, 0x73, 0xCF,
0x3C, 0xF3, 0x8E, 0x38, 0xE3, 0x8F, 0x3C, 0xF3, 0xC7, 0x1E, 0x38, 0xF1,
0xC0, 0xF0, 0xF1, 0xE1, 0xE3, 0xC7, 0x87, 0x0F, 0x1E, 0x3C, 0x78, 0xF1,
0xE3, 0xC7, 0x8F, 0x1C, 0x78, 0xF1, 0xC7, 0x8F, 0x3C, 0x00, 0x0C, 0x03,
0x06, 0xDF, 0xFF, 0x3F, 0x0F, 0xEF, 0xFD, 0xB2, 0x0C, 0x03, 0x00, 0x07,
0x00, 0x70, 0x07, 0x00, 0x70, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x70, 0x07,
0x00, 0x70, 0x07, 0x00, 0xFF, 0xFF, 0x76, 0xEE, 0xFF, 0xFF, 0xF8, 0xFF,
0xFF, 0x00, 0xE0, 0x1C, 0x07, 0x80, 0xE0, 0x3C, 0x07, 0x00, 0xE0, 0x3C,
0x07, 0x00, 0xE0, 0x3C, 0x07, 0x00, 0xE0, 0x3C, 0x07, 0x00, 0xE0, 0x3C,
0x07, 0x00, 0xE0, 0x3C, 0x07, 0x00, 0xE0, 0x00, 0x0F, 0xC0, 0x7F, 0xC3,
0xFF, 0x9F, 0x3E, 0x78, 0x3F, 0xC0, 0xFF, 0x03, 0xFC, 0x0F, 0xF0, 0x3F,
0xC0, 0xFF, 0x03, 0xFC, 0x0F, 0x78, 0x3D, 0xF1, 0xE3, 0xFF, 0x8F, 0xFC,
0x0F, 0xC0, 0xFF, 0xFF, 0xF8, 0x70, 0xE1, 0xC3, 0x87, 0x0E, 0x1C, 0x38,
0x70, 0xE1, 0xC3, 0x87, 0x0E, 0x1F, 0xC3, 0xFF, 0x3F, 0xFC, 0xF1, 0xF2,
0x07, 0x80, 0x3C, 0x01, 0xE0, 0x1E, 0x01, 0xF0, 0x1F, 0x01, 0xF0, 0x1F,
0x01, 0xF0, 0x1F, 0x01, 0xFF, 0xEF, 0xFF, 0x7F, 0xF8, 0x7F, 0xFB, 0xFF,
0xDF, 0xFE, 0x01, 0xE0, 0x1E, 0x01, 0xE0, 0x1E, 0x00, 0xF8, 0x07, 0xF0,
0x3F, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0xB0, 0x7F, 0xFF, 0xFF, 0xFE, 0x3F,
0xC0, 0x01, 0xF0, 0x03, 0xC0, 0x0F, 0x00, 0x3C, 0x00, 0xF8, 0x01, 0xE0,
0x07, 0x80, 0x1E, 0x3C, 0x7C, 0x78, 0xF0, 0xF3, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xE0, 0x0F, 0x00, 0x1E, 0x00, 0x3C, 0x00, 0x78, 0x3F, 0xF8, 0xFF,
0xE3, 0xFF, 0x8F, 0x00, 0x3C, 0x00, 0xE0, 0x03, 0x80, 0x0F, 0xF8, 0x3F,
0xF9, 0xFF, 0xE0, 0x07, 0x80, 0x0F, 0x00, 0x3D, 0xC1, 0xE7, 0xFF, 0xBF,
0xFC, 0x3F, 0xE0, 0x07, 0xF0, 0x7F, 0xE3, 0xFF, 0x1F, 0x04, 0x78, 0x03,
0xC0, 0x0F, 0x10, 0x3F, 0xFC, 0xFF, 0xFB, 0xFB, 0xEF, 0x83, 0xFC, 0x0F,
0xF0, 0x3D, 0xE1, 0xF7, 0xFF, 0x8F, 0xFC, 0x0F, 0xE0, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xC0, 0xFE, 0x0F, 0x70, 0x78, 0x07, 0x80, 0x3C, 0x03, 0xE0,
0x1E, 0x01, 0xF0, 0x0F, 0x00, 0x78, 0x07, 0x80, 0x3C, 0x03, 0xE0, 0x1E,
0x00, 0x0F, 0xC0, 0xFF, 0xC7, 0xFF, 0x9E, 0x1E, 0xF0, 0x7B, 0xC1, 0xE7,
0x87, 0x8F, 0xFC, 0x3F, 0xF1, 0xFF, 0xEF, 0x07, 0xFC, 0x0F, 0xF0, 0x3F,
0xC1, 0xFF, 0xFF, 0x9F, 0xFC, 0x1F, 0xE0, 0x0F, 0xC0, 0xFF, 0xC7, 0xFF,
0x9E, 0x1E, 0x70, 0x3F, 0xC0, 0xF7, 0x83, 0xDF, 0x1F, 0x7F, 0xFC, 0xFF,
0xF0, 0xF3, 0xC0, 0x0F, 0x00, 0x7C, 0x83, 0xE3, 0xFF, 0x1F, 0xF8, 0x3F,
0xC0, 0xFF, 0xFF, 0x00, 0x00, 0x0F, 0xFF, 0xF0, 0xFF, 0xFF, 0x00, 0x00,
0x0F, 0xFF, 0xF7, 0x6E, 0xE0, 0x00, 0x30, 0x1F, 0x07, 0xF3, 0xFC, 0xFE,
0x0F, 0x00, 0xFC, 0x07, 0xF8, 0x0F, 0xF0, 0x1F, 0x00, 0x70, 0x00, 0xFF,
0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF,
0xF0, 0xE0, 0x0F, 0x80, 0xFF, 0x01, 0xFE, 0x07, 0xF0, 0x0F, 0x03, 0xF1,
0xFE, 0xFF, 0x0F, 0xC0, 0xE0, 0x00, 0x00, 0x1F, 0xC3, 0xFF, 0xBF, 0xFC,
0xE1, 0xF2, 0x07, 0x80, 0x3C, 0x03, 0xC0, 0x3E, 0x03, 0xE0, 0x1E, 0x01,
0xE0, 0x00, 0x00, 0x00, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x00,
0xFE, 0x00, 0x0F, 0xFF, 0x80, 0x3E, 0x0F, 0x80, 0xF0, 0x03, 0x83, 0x80,
0x03, 0x8E, 0x1F, 0x7B, 0x9C, 0x7F, 0xF3, 0x71, 0xFF, 0xE6, 0xE7, 0x87,
0xCF, 0xCF, 0x07, 0x9F, 0x9C, 0x0F, 0x3F, 0x38, 0x1E, 0x7E, 0x78, 0x3C,
0xFC, 0xF0, 0xF9, 0xB8, 0xFF, 0xFF, 0x38, 0xFE, 0xFC, 0x70, 0xF8, 0xF0,
0x70, 0x00, 0x00, 0x78, 0x00, 0x00, 0x7E, 0x3C, 0x00, 0x7F, 0xF8, 0x00,
0x1F, 0xC0, 0x00, 0x01, 0xE0, 0x00, 0x3E, 0x00, 0x0F, 0xC0, 0x01, 0xFC,
0x00, 0x7F, 0x80, 0x0F, 0x70, 0x03, 0xCF, 0x00, 0x78, 0xE0, 0x0E, 0x1E,
0x03, 0xC3, 0xC0, 0x7F, 0xFC, 0x1F, 0xFF, 0x83, 0xFF, 0xF8, 0xF0, 0x0F,
0x1E, 0x00, 0xF7, 0x80, 0x1E, 0xF0, 0x03, 0xC0, 0xFF, 0xE1, 0xFF, 0xF3,
0xFF, 0xF7, 0x81, 0xEF, 0x01, 0xFE, 0x03, 0xBC, 0x0F, 0x7F, 0xFC, 0xFF,
0xF9, 0xFF, 0xFB, 0xC0, 0x7F, 0x80, 0xFF, 0x01, 0xFE, 0x03, 0xFF, 0xFF,
0xFF, 0xFE, 0xFF, 0xF0, 0x03, 0xF8, 0x0F, 0xFC, 0x3F, 0xFF, 0x3F, 0x1E,
0x7C, 0x04, 0xF8, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00,
0xF0, 0x00, 0xF8, 0x00, 0x78, 0x04, 0x7E, 0x1E, 0x3F, 0xFF, 0x1F, 0xFE,
0x07, 0xF8, 0xFF, 0xC0, 0x7F, 0xFC, 0x3F, 0xFF, 0x1E, 0x1F, 0xCF, 0x01,
0xF7, 0x80, 0x7B, 0xC0, 0x1F, 0xE0, 0x0F, 0xF0, 0x07, 0xF8, 0x03, 0xFC,
0x01, 0xFE, 0x01, 0xEF, 0x01, 0xF7, 0x83, 0xF3, 0xFF, 0xF9, 0xFF, 0xF0,
0xFF, 0xE0, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x0F, 0x00, 0x78,
0x03, 0xC0, 0x1F, 0xFE, 0xFF, 0xF7, 0xFF, 0xBC, 0x01, 0xE0, 0x0F, 0x00,
0x78, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xFF, 0xF7, 0xFF, 0xBF,
0xFD, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x00, 0x03,
0xF8, 0x0F, 0xFE, 0x3F, 0xFF, 0x3F, 0x1E, 0x7C, 0x04, 0xF8, 0x00, 0xF0,
0x00, 0xF0, 0x00, 0xF0, 0x0F, 0xF0, 0x0F, 0xF0, 0x0F, 0xF8, 0x0F, 0x78,
0x0F, 0x7E, 0x0F, 0x3F, 0xFF, 0x1F, 0xFE, 0x07, 0xF8, 0xF0, 0x0F, 0xE0,
0x1F, 0xC0, 0x3F, 0x80, 0x7F, 0x00, 0xFE, 0x01, 0xFC, 0x03, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x3F, 0x80, 0x7F, 0x00, 0xFE, 0x01, 0xFC,
0x03, 0xF8, 0x07, 0xF0, 0x0E, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xF0, 0x7F, 0xEF, 0xFD, 0xFF, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78,
0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0xC3, 0xFF, 0xF7, 0xFE,
0x3F, 0x80, 0xF0, 0x1E, 0xF0, 0x3C, 0xF0, 0x78, 0xF0, 0xF8, 0xF1, 0xF0,
0xF3, 0xE0, 0xF7, 0xC0, 0xFF, 0x80, 0xFF, 0x80, 0xFF, 0xC0, 0xFF, 0xE0,
0xF9, 0xF0, 0xF0, 0xF0, 0xF0, 0x78, 0xF0, 0x7C, 0xF0, 0x3E, 0xF0, 0x1F,
0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00,
0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xFF, 0xFF, 0xFF,
0xFF, 0xF0, 0xE0, 0x01, 0xFE, 0x00, 0x3F, 0xE0, 0x0F, 0xFC, 0x01, 0xFF,
0xC0, 0x7F, 0xF8, 0x0F, 0xFF, 0x83, 0xFF, 0xF8, 0xFF, 0xFF, 0x1D, 0xFE,
0xF7, 0xBF, 0xCE, 0xE7, 0xF9, 0xFC, 0xFF, 0x1F, 0x1F, 0xE1, 0xC3, 0xFC,
0x38, 0x7F, 0x80, 0x0F, 0xF0, 0x01, 0xE0, 0xE0, 0x0F, 0xE0, 0x1F, 0xE0,
0x3F, 0xE0, 0x7F, 0xE0, 0xFF, 0xE1, 0xFF, 0xC3, 0xFF, 0xC7, 0xF7, 0xCF,
0xE7, 0xDF, 0xC7, 0xFF, 0x87, 0xFF, 0x0F, 0xFE, 0x0F, 0xFC, 0x0F, 0xF8,
0x0F, 0xF0, 0x0E, 0x03, 0xF8, 0x03, 0xFF, 0x83, 0xFF, 0xF0, 0xFC, 0x7E,
0x7C, 0x07, 0xBE, 0x01, 0xFF, 0x00, 0x3F, 0xC0, 0x0F, 0xF0, 0x03, 0xFC,
0x00, 0xFF, 0x00, 0x3F, 0xE0, 0x0F, 0x78, 0x07, 0x9F, 0x87, 0xE3, 0xFF,
0xF0, 0x7F, 0xF8, 0x07, 0xF8, 0x00, 0xFF, 0xC1, 0xFF, 0xE3, 0xFF, 0xE7,
0x87, 0xEF, 0x03, 0xDE, 0x03, 0xFC, 0x07, 0xF8, 0x1E, 0xF0, 0x3D, 0xFF,
0xFB, 0xFF, 0xE7, 0xFF, 0x0F, 0x00, 0x1E, 0x00, 0x3C, 0x00, 0x78, 0x00,
0xF0, 0x00, 0x03, 0xF8, 0x01, 0xFF, 0xC0, 0xFF, 0xFC, 0x1F, 0x8F, 0xC7,
0xC0, 0x79, 0xF0, 0x0F, 0xBC, 0x00, 0xF7, 0x80, 0x1E, 0xF0, 0x03, 0xDE,
0x00, 0x7B, 0xC0, 0x0F, 0x7C, 0x01, 0xE7, 0x80, 0x78, 0xFC, 0x3F, 0x0F,
0xFF, 0xC0, 0xFF, 0xF0, 0x07, 0xF8, 0x00, 0x1F, 0x18, 0x01, 0xFF, 0x80,
0x1F, 0xE0, 0x00, 0xF8, 0xFF, 0x81, 0xFF, 0xE3, 0xFF, 0xE7, 0x87, 0xEF,
0x03, 0xDE, 0x03, 0xFC, 0x07, 0xF8, 0x1E, 0xF0, 0x3D, 0xFF, 0xFB, 0xFF,
0xE7, 0xFF, 0x0F, 0x0F, 0x1E, 0x1E, 0x3C, 0x1E, 0x78, 0x1E, 0xF0, 0x3E,
0x1F, 0xE0, 0xFF, 0xE7, 0xFF, 0x3E, 0x0C, 0xF0, 0x03, 0xC0, 0x0F, 0x80,
0x1F, 0xE0, 0x3F, 0xF0, 0x7F, 0xE0, 0x1F, 0x80, 0x1F, 0x00, 0x3F, 0x81,
0xEF, 0xFF, 0xBF, 0xFC, 0x3F, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8,
0x3C, 0x00, 0x78, 0x00, 0xF0, 0x01, 0xE0, 0x03, 0xC0, 0x07, 0x80, 0x0F,
0x00, 0x1E, 0x00, 0x3C, 0x00, 0x78, 0x00, 0xF0, 0x01, 0xE0, 0x03, 0xC0,
0x07, 0x80, 0xF0, 0x1F, 0xE0, 0x3F, 0xC0, 0x7F, 0x80, 0xFF, 0x01, 0xFE,
0x03, 0xFC, 0x07, 0xF8, 0x0F, 0xF0, 0x1F, 0xE0, 0x3F, 0xC0, 0x7F, 0x80,
0xFF, 0x01, 0xEF, 0x0F, 0x9F, 0xFF, 0x1F, 0xFC, 0x0F, 0xE0, 0xF0, 0x03,
0xFE, 0x00, 0xF7, 0x80, 0x79, 0xE0, 0x1E, 0x3C, 0x0F, 0x0F, 0x03, 0xC3,
0xE0, 0xE0, 0x78, 0x78, 0x1F, 0x1E, 0x03, 0xCF, 0x00, 0xF3, 0xC0, 0x1F,
0xE0, 0x07, 0xF8, 0x00, 0xFC, 0x00, 0x3F, 0x00, 0x0F, 0xC0, 0x01, 0xE0,
0x00, 0xF0, 0x1E, 0x01, 0xFC, 0x07, 0x80, 0xFF, 0x01, 0xF0, 0x3D, 0xE0,
0xFC, 0x0E, 0x78, 0x3F, 0x07, 0x9E, 0x0F, 0xE1, 0xE3, 0xC7, 0xF8, 0x70,
0xF1, 0xCE, 0x3C, 0x3C, 0x73, 0xCF, 0x07, 0xBC, 0xF3, 0x81, 0xEE, 0x1D,
0xE0, 0x7F, 0x87, 0xF8, 0x0F, 0xE1, 0xFC, 0x03, 0xF0, 0x3F, 0x00, 0xFC,
0x0F, 0xC0, 0x1F, 0x03, 0xE0, 0x07, 0x80, 0x78, 0x00, 0x78, 0x0F, 0x3E,
0x07, 0x8F, 0x07, 0x83, 0xC7, 0x81, 0xF7, 0xC0, 0x7F, 0xC0, 0x1F, 0xC0,
0x07, 0xC0, 0x03, 0xE0, 0x03, 0xF8, 0x01, 0xFC, 0x01, 0xEF, 0x01, 0xF7,
0xC0, 0xF1, 0xF0, 0xF0, 0x78, 0xF8, 0x1E, 0xF8, 0x0F, 0x80, 0xF0, 0x07,
0x7C, 0x07, 0x9E, 0x07, 0x87, 0x83, 0xC3, 0xC3, 0xC0, 0xF1, 0xC0, 0x7D,
0xE0, 0x1F, 0xE0, 0x07, 0xF0, 0x03, 0xF0, 0x00, 0xF0, 0x00, 0x78, 0x00,
0x3C, 0x00, 0x1E, 0x00, 0x0F, 0x00, 0x07, 0x80, 0x03, 0xC0, 0x00, 0xFF,
0xFD, 0xFF, 0xFB, 0xFF, 0xF0, 0x03, 0xE0, 0x0F, 0x80, 0x3E, 0x00, 0x78,
0x01, 0xE0, 0x07, 0x80, 0x1F, 0x00, 0x7C, 0x01, 0xF0, 0x03, 0xC0, 0x0F,
0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0x8F, 0x1E,
0x3C, 0x78, 0xF1, 0xE3, 0xC7, 0x8F, 0x1E, 0x3C, 0x78, 0xF1, 0xE3, 0xC7,
0x8F, 0xFF, 0xFF, 0x80, 0xF0, 0x0E, 0x01, 0xC0, 0x3C, 0x03, 0x80, 0x70,
0x0F, 0x00, 0xE0, 0x1C, 0x03, 0xC0, 0x38, 0x07, 0x00, 0xF0, 0x0E, 0x01,
0xC0, 0x3C, 0x03, 0x80, 0x78, 0x07, 0x00, 0xE0, 0x1E, 0x01, 0xC0, 0xFF,
0xFF, 0xF8, 0xF1, 0xE3, 0xC7, 0x8F, 0x1E, 0x3C, 0x78, 0xF1, 0xE3, 0xC7,
0x8F, 0x1E, 0x3C, 0x78, 0xFF, 0xFF, 0xFF, 0x80, 0x0F, 0x00, 0xF8, 0x1F,
0x81, 0xD8, 0x19, 0xC3, 0x9C, 0x38, 0xE7, 0x0E, 0x70, 0x7E, 0x07, 0xFF,
0xFF, 0xFF, 0x78, 0x78, 0x78, 0x3F, 0x8F, 0xFE, 0x7F, 0xE6, 0x0F, 0x00,
0xF0, 0xFF, 0x7F, 0xFF, 0xFF, 0xF0, 0xFF, 0x0F, 0xFB, 0xF7, 0xFF, 0x3F,
0x70, 0xE0, 0x03, 0x80, 0x0E, 0x00, 0x38, 0x00, 0xE0, 0x03, 0x9F, 0x0F,
0xFF, 0x3F, 0xFE, 0xF8, 0xFB, 0xC1, 0xFF, 0x03, 0xF8, 0x0F, 0xF0, 0x3F,
0xC0, 0xFF, 0x87, 0xBF, 0xFE, 0xFF, 0xF3, 0xBF, 0x00, 0x0F, 0xC1, 0xFF,
0x9F, 0xFD, 0xF0, 0xEF, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07,
0xC3, 0x9F, 0xFC, 0x7F, 0xE0, 0xFC, 0x00, 0x00, 0x3C, 0x00, 0xF0, 0x03,
0xC0, 0x0F, 0x00, 0x3C, 0x7E, 0xF3, 0xFF, 0xDF, 0xFF, 0xF8, 0x7F, 0xC0,
0xFF, 0x03, 0xFC, 0x0F, 0xF0, 0x3F, 0xC0, 0xFF, 0x87, 0xDF, 0xFF, 0x3F,
0xFC, 0x7E, 0xF0, 0x0F, 0xC1, 0xFF, 0x1F, 0xFD, 0xF0, 0xFF, 0x03, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x07, 0xC1, 0x1F, 0xFC, 0x7F, 0xE0, 0xFE,
0x00, 0x07, 0xC7, 0xF1, 0xF8, 0xF0, 0x3C, 0x3F, 0xEF, 0xFB, 0xFE, 0x3C,
0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x00,
0x1F, 0xBC, 0xFF, 0xF7, 0xFF, 0xFE, 0x1F, 0xF0, 0x3F, 0xC0, 0xFF, 0x03,
0xFC, 0x0F, 0xF8, 0x7D, 0xFF, 0xF7, 0xFF, 0xC7, 0xEF, 0x00, 0x3C, 0x00,
0xF7, 0x07, 0x9F, 0xFE, 0x7F, 0xF0, 0x3E, 0x00, 0xE0, 0x07, 0x00, 0x38,
0x01, 0xC0, 0x0E, 0x00, 0x73, 0xE3, 0xFF, 0xDF, 0xFE, 0xF8, 0xFF, 0x83,
0xFC, 0x1F, 0xC0, 0xFE, 0x07, 0xF0, 0x3F, 0x81, 0xFC, 0x0F, 0xE0, 0x7F,
0x03, 0xC0, 0x77, 0xBF, 0xE0, 0x01, 0xCE, 0x73, 0x9C, 0xE7, 0x39, 0xCE,
0x73, 0x9C, 0x07, 0x07, 0x83, 0xE1, 0xE0, 0x00, 0x00, 0x3C, 0x1E, 0x0F,
0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x83, 0xC1,
0xE1, 0xE7, 0xF3, 0xF0, 0xF0, 0xE0, 0x03, 0x80, 0x0E, 0x00, 0x38, 0x00,
0xE0, 0x03, 0x83, 0xEE, 0x1F, 0x38, 0xF8, 0xE7, 0xC3, 0xBE, 0x0F, 0xF0,
0x3F, 0xE0, 0xFF, 0xC3, 0xEF, 0x0F, 0x1E, 0x38, 0x7C, 0xE0, 0xFB, 0x81,
0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0xE7, 0xC3, 0xE3, 0xFF,
0xBF, 0xCF, 0xFF, 0xFF, 0xBE, 0x3F, 0x1E, 0xF0, 0xF8, 0x7B, 0xC1, 0xE0,
0xFE, 0x07, 0x83, 0xF8, 0x1E, 0x0F, 0xE0, 0x78, 0x3F, 0x81, 0xE0, 0xFE,
0x07, 0x83, 0xF8, 0x1E, 0x0F, 0xE0, 0x78, 0x3C, 0xE7, 0xC7, 0xFF, 0xBF,
0xFD, 0xF1, 0xFF, 0x07, 0xF8, 0x3F, 0x81, 0xFC, 0x0F, 0xE0, 0x7F, 0x03,
0xF8, 0x1F, 0xC0, 0xFE, 0x07, 0x80, 0x0F, 0xC0, 0xFF, 0xC7, 0xFF, 0xBE,
0x1E, 0xF0, 0x3F, 0xC0, 0xFF, 0x03, 0xFC, 0x0F, 0xF0, 0x3F, 0xE1, 0xE7,
0xFF, 0x8F, 0xFC, 0x0F, 0xC0, 0xE7, 0xC3, 0xFF, 0xCF, 0xFF, 0xBE, 0x3E,
0xF0, 0x7F, 0xC0, 0xFE, 0x03, 0xFC, 0x0F, 0xF0, 0x3F, 0xE1, 0xEF, 0xFF,
0xBF, 0xFC, 0xE7, 0xC3, 0x80, 0x0E, 0x00, 0x38, 0x00, 0xE0, 0x03, 0x80,
0x00, 0x1F, 0xBC, 0xFF, 0xF7, 0xFF, 0xFE, 0x1F, 0xF0, 0x3F, 0xC0, 0xFF,
0x03, 0xFC, 0x0F, 0xF0, 0x3F, 0xE1, 0xF7, 0xFF, 0xCF, 0xFF, 0x1F, 0xBC,
0x00, 0xF0, 0x03, 0xC0, 0x0F, 0x00, 0x3C, 0x00, 0xF0, 0xE7, 0xEF, 0xFF,
0xFC, 0xF0, 0xF0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0x1F, 0xE3,
0xFF, 0x7F, 0xE7, 0x82, 0x78, 0x07, 0xF8, 0x3F, 0xE1, 0xFF, 0x00, 0xF0,
0x07, 0x7F, 0xF7, 0xFE, 0x3F, 0xC0, 0x3C, 0x0F, 0x03, 0xC3, 0xFE, 0xFF,
0xBF, 0xE3, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0xE1,
0xFC, 0x3F, 0xE0, 0x7F, 0x03, 0xF8, 0x1F, 0xC0, 0xFE, 0x07, 0xF0, 0x3F,
0x81, 0xFC, 0x0F, 0xF0, 0x7F, 0x87, 0xFF, 0xFE, 0xFF, 0xF3, 0xF7, 0x80,
0xF0, 0x1D, 0xE0, 0x79, 0xE0, 0xF3, 0xC3, 0xC3, 0x87, 0x87, 0x8E, 0x0F,
0x3C, 0x0F, 0x70, 0x1F, 0xE0, 0x1F, 0xC0, 0x3F, 0x00, 0x3E, 0x00, 0x78,
0x00, 0xF0, 0x38, 0x1D, 0xC1, 0xE0, 0x77, 0x07, 0xC3, 0xDE, 0x1F, 0x0E,
0x38, 0xFC, 0x38, 0xE3, 0xF9, 0xE3, 0xDE, 0xE7, 0x07, 0x73, 0xFC, 0x1D,
0xC7, 0xE0, 0x7F, 0x1F, 0x80, 0xF8, 0x7E, 0x03, 0xE0, 0xF0, 0x0F, 0x03,
0xC0, 0x78, 0x3D, 0xE1, 0xE3, 0xCF, 0x07, 0xF8, 0x0F, 0xE0, 0x3F, 0x00,
0x78, 0x03, 0xF0, 0x0F, 0xE0, 0x7F, 0xC3, 0xCF, 0x1E, 0x1E, 0xF8, 0x3C,
0xF0, 0x1D, 0xE0, 0x79, 0xE0, 0xF3, 0xC1, 0xC3, 0xC7, 0x87, 0x8E, 0x07,
0x3C, 0x0F, 0x70, 0x1F, 0xE0, 0x1F, 0xC0, 0x3F, 0x00, 0x3E, 0x00, 0x78,
0x00, 0xF0, 0x13, 0xC0, 0x7F, 0x80, 0xFE, 0x00, 0xF0, 0x00, 0xFF, 0xFF,
0xFF, 0xFF, 0x81, 0xE0, 0x78, 0x1E, 0x07, 0xC0, 0xF0, 0x3C, 0x0F, 0x03,
0xFF, 0xFF, 0xFF, 0xFE, 0x07, 0x1F, 0x3F, 0x3E, 0x3C, 0x3C, 0x3C, 0x3C,
0x3C, 0x3C, 0xF8, 0xF0, 0xFC, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3E,
0x3F, 0x1F, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8,
0xF0, 0xF8, 0xFC, 0x3C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1F, 0x0F,
0x1F, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x3C, 0x7C, 0xFC, 0xF8, 0xE0, 0x38,
0x37, 0xE3, 0x7F, 0xFE, 0x7E, 0xE1, 0xC0 };
const GFXglyph Montserrat_Bold12pt7bGlyphs[] PROGMEM = {
{ 0, 1, 1, 7, 0, 0 }, // 0x20 ' '
{ 1, 5, 17, 7, 1, -16 }, // 0x21 '!'
{ 12, 8, 7, 10, 1, -16 }, // 0x22 '"'
{ 19, 17, 17, 17, 0, -16 }, // 0x23 '#'
{ 56, 14, 23, 15, 1, -19 }, // 0x24 '$'
{ 97, 19, 17, 21, 1, -16 }, // 0x25 '%'
{ 138, 16, 17, 17, 1, -16 }, // 0x26 '&'
{ 172, 3, 7, 6, 1, -16 }, // 0x27 '''
{ 175, 6, 23, 9, 2, -17 }, // 0x28 '('
{ 193, 7, 23, 9, 0, -17 }, // 0x29 ')'
{ 214, 10, 10, 10, 0, -17 }, // 0x2A '*'
{ 227, 12, 11, 14, 1, -13 }, // 0x2B '+'
{ 244, 4, 8, 6, 1, -3 }, // 0x2C ','
{ 248, 7, 3, 9, 1, -7 }, // 0x2D '-'
{ 251, 4, 4, 6, 1, -3 }, // 0x2E '.'
{ 253, 11, 22, 9, -1, -19 }, // 0x2F '/'
{ 284, 14, 17, 16, 1, -16 }, // 0x30 '0'
{ 314, 7, 17, 9, 0, -16 }, // 0x31 '1'
{ 329, 13, 17, 14, 0, -16 }, // 0x32 '2'
{ 357, 13, 17, 14, 0, -16 }, // 0x33 '3'
{ 385, 15, 17, 17, 1, -16 }, // 0x34 '4'
{ 417, 14, 17, 14, 0, -16 }, // 0x35 '5'
{ 447, 14, 17, 15, 1, -16 }, // 0x36 '6'
{ 477, 13, 17, 15, 1, -16 }, // 0x37 '7'
{ 505, 14, 17, 16, 1, -16 }, // 0x38 '8'
{ 535, 14, 17, 15, 0, -16 }, // 0x39 '9'
{ 565, 4, 13, 6, 1, -12 }, // 0x3A ':'
{ 572, 4, 17, 6, 1, -12 }, // 0x3B ';'
{ 581, 12, 12, 14, 1, -13 }, // 0x3C '<'
{ 599, 12, 9, 14, 1, -12 }, // 0x3D '='
{ 613, 12, 12, 14, 1, -13 }, // 0x3E '>'
{ 631, 13, 17, 14, 0, -16 }, // 0x3F '?'
{ 659, 23, 22, 25, 1, -16 }, // 0x40 '@'
{ 723, 19, 17, 18, 0, -16 }, // 0x41 'A'
{ 764, 15, 17, 18, 2, -16 }, // 0x42 'B'
{ 796, 16, 17, 17, 1, -16 }, // 0x43 'C'
{ 830, 17, 17, 20, 2, -16 }, // 0x44 'D'
{ 867, 13, 17, 16, 2, -16 }, // 0x45 'E'
{ 895, 13, 17, 15, 2, -16 }, // 0x46 'F'
{ 923, 16, 17, 19, 1, -16 }, // 0x47 'G'
{ 957, 15, 17, 19, 2, -16 }, // 0x48 'H'
{ 989, 4, 17, 8, 2, -16 }, // 0x49 'I'
{ 998, 11, 17, 13, 0, -16 }, // 0x4A 'J'
{ 1022, 16, 17, 18, 2, -16 }, // 0x4B 'K'
{ 1056, 12, 17, 15, 2, -16 }, // 0x4C 'L'
{ 1082, 19, 17, 23, 2, -16 }, // 0x4D 'M'
{ 1123, 15, 17, 19, 2, -16 }, // 0x4E 'N'
{ 1155, 18, 17, 20, 1, -16 }, // 0x4F 'O'
{ 1194, 15, 17, 18, 2, -16 }, // 0x50 'P'
{ 1226, 19, 21, 20, 1, -16 }, // 0x51 'Q'
{ 1276, 15, 17, 18, 2, -16 }, // 0x52 'R'
{ 1308, 14, 17, 15, 1, -16 }, // 0x53 'S'
{ 1338, 15, 17, 15, 0, -16 }, // 0x54 'T'
{ 1370, 15, 17, 19, 2, -16 }, // 0x55 'U'
{ 1402, 18, 17, 18, 0, -16 }, // 0x56 'V'
{ 1441, 26, 17, 28, 1, -16 }, // 0x57 'W'
{ 1497, 17, 17, 17, 0, -16 }, // 0x58 'X'
{ 1534, 17, 17, 16, 0, -16 }, // 0x59 'Y'
{ 1571, 15, 17, 16, 1, -16 }, // 0x5A 'Z'
{ 1603, 7, 23, 9, 2, -17 }, // 0x5B '['
{ 1624, 11, 22, 9, -1, -19 }, // 0x5C '\'
{ 1655, 7, 23, 9, 0, -17 }, // 0x5D ']'
{ 1676, 12, 10, 14, 1, -12 }, // 0x5E '^'
{ 1691, 12, 2, 12, 0, 1 }, // 0x5F '_'
{ 1694, 7, 3, 14, 2, -17 }, // 0x60 '`'
{ 1697, 12, 13, 15, 1, -12 }, // 0x61 'a'
{ 1717, 14, 18, 17, 2, -17 }, // 0x62 'b'
{ 1749, 13, 13, 14, 1, -12 }, // 0x63 'c'
{ 1771, 14, 18, 17, 1, -17 }, // 0x64 'd'
{ 1803, 13, 13, 15, 1, -12 }, // 0x65 'e'
{ 1825, 10, 18, 9, 0, -17 }, // 0x66 'f'
{ 1848, 14, 18, 17, 1, -12 }, // 0x67 'g'
{ 1880, 13, 18, 17, 2, -17 }, // 0x68 'h'
{ 1910, 5, 19, 7, 1, -18 }, // 0x69 'i'
{ 1922, 9, 24, 7, -2, -18 }, // 0x6A 'j'
{ 1949, 14, 18, 16, 2, -17 }, // 0x6B 'k'
{ 1981, 3, 18, 7, 2, -17 }, // 0x6C 'l'
{ 1988, 22, 13, 25, 2, -12 }, // 0x6D 'm'
{ 2024, 13, 13, 17, 2, -12 }, // 0x6E 'n'
{ 2046, 14, 13, 16, 1, -12 }, // 0x6F 'o'
{ 2069, 14, 18, 17, 2, -12 }, // 0x70 'p'
{ 2101, 14, 18, 17, 1, -12 }, // 0x71 'q'
{ 2133, 8, 13, 10, 2, -12 }, // 0x72 'r'
{ 2146, 12, 13, 13, 0, -12 }, // 0x73 's'
{ 2166, 10, 16, 10, 0, -15 }, // 0x74 't'
{ 2186, 13, 13, 16, 2, -12 }, // 0x75 'u'
{ 2208, 15, 13, 14, 0, -12 }, // 0x76 'v'
{ 2233, 22, 13, 22, 0, -12 }, // 0x77 'w'
{ 2269, 14, 13, 14, 0, -12 }, // 0x78 'x'
{ 2292, 15, 18, 14, 0, -12 }, // 0x79 'y'
{ 2326, 11, 13, 13, 1, -12 }, // 0x7A 'z'
{ 2344, 8, 23, 9, 1, -17 }, // 0x7B '{'
{ 2367, 3, 23, 7, 2, -17 }, // 0x7C '|'
{ 2376, 8, 23, 9, 0, -17 }, // 0x7D '}'
{ 2399, 12, 5, 14, 1, -10 } }; // 0x7E '~'
const GFXfont Montserrat_Bold12pt7b PROGMEM = {
(uint8_t *)Montserrat_Bold12pt7bBitmaps,
(GFXglyph *)Montserrat_Bold12pt7bGlyphs,
0x20, 0x7E, 29 };
// Approx. 3079 bytes
#endif // MONTSERRATBOLD12PT7B_H
File diff suppressed because it is too large Load Diff
@@ -1,227 +0,0 @@
#ifndef MONTSERRATBOLD9PT7B_H
#define MONTSERRATBOLD9PT7B_H
const uint8_t Montserrat_Bold9pt7bBitmaps[] PROGMEM = {
0x00, 0xFF, 0xFF, 0xFF, 0xE3, 0xFE, 0xCF, 0x3C, 0xF3, 0xCF, 0x30, 0x0C,
0x60, 0x66, 0x03, 0x30, 0xFF, 0xF7, 0xFF, 0x8C, 0x60, 0x63, 0x03, 0x18,
0xFF, 0xF7, 0xFF, 0x86, 0x60, 0x33, 0x03, 0x18, 0x00, 0x06, 0x00, 0xC0,
0x3E, 0x1F, 0xE7, 0xFC, 0xEC, 0x1D, 0x83, 0xF0, 0x3F, 0x81, 0xF8, 0x1F,
0x83, 0x76, 0x6E, 0xFF, 0x8F, 0xE0, 0x30, 0x06, 0x00, 0x38, 0x18, 0xF8,
0x71, 0x98, 0xC6, 0x33, 0x04, 0x6C, 0x0D, 0x98, 0x0F, 0x67, 0x01, 0x9F,
0x03, 0x66, 0x0C, 0xCC, 0x31, 0x98, 0x63, 0xB1, 0x83, 0xC0, 0x1E, 0x03,
0xF0, 0x73, 0x87, 0x38, 0x77, 0x03, 0xE0, 0x3C, 0x0F, 0xE6, 0xE7, 0xEC,
0x3E, 0xE1, 0xEF, 0xFF, 0x7F, 0x60, 0xFF, 0xF0, 0x39, 0x9C, 0xE6, 0x73,
0x9C, 0xE7, 0x38, 0xC7, 0x38, 0xC7, 0x63, 0x9C, 0x73, 0x9C, 0xE7, 0x39,
0xCE, 0x73, 0x99, 0xCC, 0x18, 0x5E, 0x7E, 0x3C, 0xFE, 0x5A, 0x18, 0x1C,
0x0E, 0x07, 0x1F, 0xFF, 0xF8, 0xE0, 0x70, 0x38, 0xFF, 0xFD, 0x80, 0xFF,
0xC0, 0xFF, 0x80, 0x03, 0x81, 0x81, 0xC0, 0xE0, 0x60, 0x70, 0x38, 0x18,
0x1C, 0x0C, 0x06, 0x07, 0x03, 0x01, 0x81, 0xC0, 0xC0, 0x60, 0x00, 0x1E,
0x07, 0xF1, 0xFE, 0x70, 0xEE, 0x1D, 0xC3, 0xB8, 0x37, 0x06, 0xE1, 0xDC,
0x39, 0xCF, 0x3F, 0xC3, 0xF0, 0xFF, 0xFF, 0xC7, 0x1C, 0x71, 0xC7, 0x1C,
0x71, 0xC7, 0x1C, 0x1E, 0x1F, 0xEF, 0xF8, 0x07, 0x01, 0xC0, 0xE0, 0x78,
0x3C, 0x1E, 0x0F, 0x07, 0x81, 0xFF, 0x7F, 0xC0, 0x7F, 0x9F, 0xE7, 0xF8,
0x1C, 0x0E, 0x03, 0x81, 0xF8, 0x7F, 0x01, 0xC0, 0x76, 0x3F, 0xFE, 0x7F,
0x00, 0x07, 0x00, 0xE0, 0x38, 0x0E, 0x03, 0xC0, 0x70, 0x1C, 0xE7, 0x1C,
0xFF, 0xFF, 0xFC, 0x0E, 0x01, 0xC0, 0x38, 0x3F, 0x8F, 0xE3, 0xF9, 0xC0,
0x70, 0x1F, 0xC7, 0xF8, 0x1F, 0x01, 0xC0, 0x76, 0x3F, 0xFE, 0x7F, 0x00,
0x0F, 0x0F, 0xE7, 0xFB, 0xC0, 0xE0, 0x3B, 0xCF, 0xFB, 0xCF, 0xE1, 0xF8,
0x7F, 0x1D, 0xFE, 0x3F, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x70, 0xEE, 0x3C,
0x07, 0x00, 0xE0, 0x38, 0x07, 0x01, 0xC0, 0x38, 0x0F, 0x01, 0xC0, 0x1E,
0x1F, 0xEF, 0x3F, 0x87, 0xE1, 0xDF, 0xE7, 0xFB, 0xCF, 0xE1, 0xF8, 0x7E,
0x1F, 0xFE, 0x3F, 0x00, 0x1E, 0x07, 0xF1, 0xCF, 0x70, 0xEE, 0x1E, 0xE3,
0xDF, 0xF9, 0xF7, 0x00, 0xE0, 0x38, 0x0F, 0x3F, 0xC7, 0xF0, 0xFF, 0x80,
0x3F, 0xE0, 0xFF, 0x80, 0x3F, 0xFF, 0x60, 0x00, 0x83, 0xC7, 0xFF, 0x8E,
0x07, 0xC0, 0xFC, 0x1F, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0xE0, 0x00, 0x07,
0xFF, 0xFE, 0x80, 0x70, 0x3F, 0x03, 0xF0, 0x38, 0x7D, 0xF9, 0xE0, 0xC0,
0x00, 0x1E, 0x1F, 0xEF, 0xF8, 0x07, 0x01, 0xC0, 0xE0, 0x70, 0x38, 0x0E,
0x00, 0x00, 0xE0, 0x38, 0x0E, 0x00, 0x03, 0xE0, 0x0F, 0xFC, 0x0E, 0x07,
0x8E, 0x00, 0xC6, 0x3F, 0xB6, 0x3F, 0xCF, 0x38, 0xE7, 0x98, 0x73, 0xCC,
0x39, 0xE6, 0x1C, 0xF3, 0x8E, 0x79, 0xFF, 0xE6, 0x3D, 0xE3, 0x80, 0x00,
0xF0, 0x40, 0x1F, 0xE0, 0x03, 0xC0, 0x00, 0x07, 0x00, 0x1E, 0x00, 0x78,
0x03, 0xF0, 0x0D, 0xC0, 0x73, 0x81, 0xCE, 0x0E, 0x1C, 0x3F, 0xF1, 0xFF,
0xC7, 0x03, 0x98, 0x0E, 0xE0, 0x1C, 0xFF, 0x0F, 0xFC, 0xFF, 0xEE, 0x0E,
0xE0, 0xEE, 0x1E, 0xFF, 0xCF, 0xFE, 0xE0, 0x7E, 0x07, 0xE0, 0xFF, 0xFE,
0xFF, 0xC0, 0x0F, 0x83, 0xFE, 0x7F, 0xF7, 0x06, 0xE0, 0x0E, 0x00, 0xE0,
0x0E, 0x00, 0xE0, 0x0F, 0x00, 0x78, 0xE3, 0xFE, 0x1F, 0xC0, 0xFF, 0x07,
0xFE, 0x3F, 0xFD, 0xC0, 0xEE, 0x03, 0xF0, 0x1F, 0x80, 0xFC, 0x07, 0xE0,
0x3F, 0x03, 0xF8, 0x7D, 0xFF, 0xCF, 0xF8, 0x00, 0xFF, 0xFF, 0xFF, 0xFF,
0x80, 0xE0, 0x3F, 0xEF, 0xFB, 0xFE, 0xE0, 0x38, 0x0E, 0x03, 0xFF, 0xFF,
0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0xE0, 0x38, 0x0F, 0xFB, 0xFE, 0xE0,
0x38, 0x0E, 0x03, 0x80, 0xE0, 0x00, 0x0F, 0x83, 0xFE, 0x7F, 0xF7, 0x06,
0xE0, 0x0E, 0x00, 0xE0, 0x7E, 0x07, 0xE0, 0x7F, 0x07, 0x78, 0xF3, 0xFF,
0x1F, 0xC0, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7F, 0xFF, 0xFF,
0xFF, 0xFF, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x70, 0xFF, 0xFF,
0xFF, 0xFF, 0xFE, 0x7F, 0x7F, 0x7F, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
0x07, 0xC7, 0xFF, 0x7E, 0xE0, 0x77, 0x07, 0x38, 0x71, 0xC7, 0x0E, 0x70,
0x77, 0x03, 0xF8, 0x1F, 0xE0, 0xFF, 0x87, 0x9E, 0x38, 0x79, 0xC1, 0xCE,
0x07, 0x00, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE0, 0x38, 0x0E, 0x03, 0x80,
0xE0, 0x38, 0x0E, 0x03, 0xFF, 0xFF, 0xC0, 0xE0, 0x0F, 0xE0, 0x1F, 0xC0,
0x7F, 0xC1, 0xFF, 0x83, 0xFF, 0x8F, 0xFB, 0x9B, 0xF7, 0x77, 0xE7, 0xCF,
0xC7, 0x9F, 0x8E, 0x3F, 0x08, 0x7E, 0x00, 0xE0, 0xE0, 0x7F, 0x07, 0xF8,
0x7F, 0x87, 0xFC, 0x7F, 0xE7, 0xEF, 0x7E, 0x7F, 0xE3, 0xFE, 0x1F, 0xE0,
0xFE, 0x0F, 0xE0, 0x70, 0x0F, 0x81, 0xFF, 0x1F, 0xFC, 0xE0, 0xFE, 0x03,
0xF0, 0x1F, 0x80, 0x7C, 0x07, 0xE0, 0x3F, 0x81, 0xDE, 0x3C, 0x7F, 0xC1,
0xFC, 0x00, 0xFE, 0x1F, 0xFB, 0xFF, 0xF0, 0x7E, 0x0F, 0xC1, 0xF8, 0x3F,
0xFF, 0xFF, 0xDF, 0xC3, 0x80, 0x70, 0x0E, 0x00, 0x0F, 0x80, 0xFF, 0x87,
0xFF, 0x1C, 0x1E, 0xE0, 0x3B, 0x80, 0xEE, 0x01, 0xB8, 0x0E, 0xE0, 0x3B,
0xC0, 0xE7, 0x8F, 0x0F, 0xF8, 0x1F, 0xC0, 0x0F, 0x20, 0x1F, 0xC0, 0x1E,
0xFE, 0x0F, 0xFC, 0xFF, 0xEE, 0x0E, 0xE0, 0xEE, 0x0E, 0xE0, 0xEF, 0xFE,
0xFF, 0xCE, 0x38, 0xE1, 0xCE, 0x1E, 0xE0, 0xE0, 0x0F, 0x87, 0xF9, 0xFF,
0x38, 0x07, 0x00, 0xF8, 0x0F, 0xE0, 0x7E, 0x01, 0xE0, 0x1D, 0x83, 0xBF,
0xE3, 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0x87, 0x00, 0xE0, 0x1C, 0x03, 0x80,
0x70, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0x00, 0xE0, 0xE0, 0x7E, 0x07, 0xE0,
0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x77, 0x0F, 0x79,
0xE3, 0xFE, 0x1F, 0x80, 0xE0, 0x1F, 0x80, 0xE7, 0x03, 0x9C, 0x1C, 0x38,
0x70, 0xE1, 0x83, 0xCE, 0x07, 0x38, 0x1D, 0xC0, 0x3F, 0x00, 0xF8, 0x01,
0xE0, 0x07, 0x00, 0xE0, 0x70, 0x3B, 0x83, 0x83, 0x9C, 0x1E, 0x1C, 0xE1,
0xF0, 0xE3, 0x8D, 0x8E, 0x1C, 0x6E, 0x70, 0xE7, 0x73, 0x83, 0xB3, 0xB8,
0x1D, 0x8F, 0xC0, 0xFC, 0x7E, 0x03, 0xC3, 0xE0, 0x1E, 0x0F, 0x00, 0xF0,
0x78, 0x00, 0xF0, 0x73, 0x83, 0x8E, 0x38, 0x7B, 0x81, 0xFC, 0x07, 0xC0,
0x1C, 0x01, 0xF0, 0x1F, 0xC0, 0xEE, 0x0E, 0x38, 0xE1, 0xEF, 0x07, 0x00,
0xE0, 0x3F, 0x07, 0x70, 0xE3, 0x8E, 0x39, 0xC1, 0xD8, 0x1F, 0x80, 0xF0,
0x07, 0x00, 0x70, 0x07, 0x00, 0x70, 0x07, 0x00, 0xFF, 0xDF, 0xFB, 0xFF,
0x01, 0xC0, 0x78, 0x1E, 0x03, 0x80, 0xE0, 0x38, 0x0E, 0x03, 0xC0, 0x7F,
0xFF, 0xFE, 0xFF, 0xF9, 0xCE, 0x73, 0x9C, 0xE7, 0x39, 0xCE, 0x73, 0xFF,
0xE0, 0x30, 0x1C, 0x06, 0x03, 0x01, 0xC0, 0x60, 0x30, 0x1C, 0x06, 0x03,
0x01, 0xC0, 0x60, 0x38, 0x1C, 0x06, 0x03, 0x80, 0xFF, 0xCE, 0x73, 0x9C,
0xE7, 0x39, 0xCE, 0x73, 0x9F, 0xFF, 0x1C, 0x0E, 0x0D, 0x86, 0xC6, 0x73,
0x1B, 0x8C, 0xFF, 0xFF, 0xC0, 0xE3, 0x8E, 0x3C, 0x7F, 0x99, 0xE0, 0x73,
0xFF, 0xFF, 0x8F, 0x87, 0xFF, 0xBF, 0xC0, 0xE0, 0x1C, 0x03, 0x80, 0x77,
0x8F, 0xF9, 0xF7, 0xBC, 0x3F, 0x07, 0xE0, 0xFC, 0x1F, 0xC7, 0x7F, 0xEE,
0xF8, 0x1E, 0x3F, 0xFF, 0xFC, 0x2C, 0x06, 0x03, 0x81, 0xE7, 0x7F, 0x9F,
0x80, 0x01, 0xC0, 0x70, 0x1C, 0x77, 0x7F, 0xFF, 0xFE, 0x1F, 0x87, 0xC1,
0xF8, 0x7F, 0x3D, 0xFF, 0x3F, 0xC0, 0x1E, 0x1F, 0xCF, 0x3B, 0x87, 0xFF,
0xFF, 0xFE, 0x03, 0xC4, 0x7F, 0x8F, 0xC0, 0x00, 0x1E, 0x3E, 0x30, 0x30,
0xFE, 0xFE, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x1C, 0xDF, 0xFF,
0xFF, 0x87, 0xC1, 0xF8, 0x7E, 0x1F, 0xFF, 0x7F, 0xC2, 0x74, 0x1D, 0xFF,
0xFF, 0x87, 0x00, 0xE0, 0x38, 0x0E, 0x03, 0xBC, 0xFF, 0xBF, 0xFF, 0x1F,
0x87, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xE1, 0xC0, 0xFF, 0x8F, 0xFF, 0xFF,
0xFF, 0xC0, 0x1C, 0x71, 0xC0, 0x1C, 0x71, 0xC7, 0x1C, 0x71, 0xC7, 0x1C,
0x71, 0xDF, 0x79, 0xC0, 0xE0, 0x1C, 0x03, 0x80, 0x70, 0xEE, 0x39, 0xCE,
0x3B, 0x87, 0xF0, 0xFE, 0x1E, 0xE3, 0x9E, 0x71, 0xEE, 0x1E, 0xFF, 0xFF,
0xFF, 0xFF, 0xFE, 0xEF, 0x1C, 0x7F, 0xFF, 0xBF, 0xFF, 0xDC, 0x38, 0x7E,
0x1C, 0x3F, 0x0E, 0x1F, 0x87, 0x0F, 0xC3, 0x87, 0xE1, 0xC3, 0xF0, 0xE1,
0xC0, 0xEF, 0x3F, 0xEF, 0xFF, 0xC7, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xE1,
0xF8, 0x70, 0x1E, 0x1F, 0xEF, 0xFB, 0x87, 0xC1, 0xF0, 0x7E, 0x1F, 0xCF,
0x7F, 0x8F, 0xC0, 0xEF, 0x1F, 0xF3, 0xEF, 0x78, 0x7E, 0x0F, 0xC1, 0xF8,
0x3F, 0x8E, 0xFF, 0xDD, 0xF3, 0x80, 0x70, 0x0E, 0x00, 0x1D, 0xDF, 0xFF,
0xFF, 0x87, 0xC1, 0xF0, 0x7E, 0x1F, 0xCF, 0x7F, 0xCF, 0xF0, 0x1C, 0x07,
0x01, 0xC0, 0xEF, 0xFF, 0xFC, 0xE3, 0x8E, 0x38, 0xE3, 0x80, 0x1F, 0x3F,
0xDC, 0x4C, 0x07, 0xE1, 0xFC, 0x0E, 0x87, 0x7F, 0xBF, 0x80, 0x30, 0x30,
0xFE, 0xFE, 0x30, 0x30, 0x30, 0x30, 0x30, 0x38, 0x3E, 0x1F, 0xE1, 0xF8,
0x7E, 0x1F, 0x87, 0xE1, 0xF8, 0x7E, 0x1F, 0xCF, 0x7F, 0xCF, 0xF0, 0xE0,
0xFC, 0x19, 0xC7, 0x38, 0xE3, 0x38, 0x77, 0x0E, 0xC0, 0xF8, 0x1E, 0x01,
0xC0, 0xE1, 0xC3, 0xF0, 0xE1, 0x98, 0x70, 0xCE, 0x7C, 0xE3, 0x36, 0x61,
0xBB, 0xB0, 0xFD, 0xF8, 0x3C, 0x78, 0x1E, 0x3C, 0x0E, 0x1E, 0x00, 0xE1,
0xCE, 0x38, 0xEE, 0x0F, 0x81, 0xE0, 0x1C, 0x07, 0xC1, 0xDC, 0x73, 0xDC,
0x38, 0xE0, 0xFC, 0x19, 0xC7, 0x38, 0xE3, 0x38, 0x77, 0x06, 0xC0, 0xF8,
0x1E, 0x01, 0xC0, 0x38, 0x7E, 0x0F, 0x80, 0xE0, 0x00, 0xFF, 0xFF, 0x07,
0x0E, 0x1C, 0x38, 0x70, 0xE0, 0xFF, 0xFF, 0x3C, 0xF7, 0x1C, 0x71, 0xC7,
0x3C, 0xF1, 0xC7, 0x1C, 0x71, 0xC7, 0xCF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xF3, 0xE3, 0x8E, 0x38, 0xE3, 0x8F, 0x3C, 0xE3, 0x8E, 0x38, 0xEF,
0xBC, 0x71, 0xFF, 0xF3, 0xC0 };
const GFXglyph Montserrat_Bold9pt7bGlyphs[] PROGMEM = {
{ 0, 1, 1, 5, 0, 0 }, // 0x20 ' '
{ 1, 3, 13, 5, 1, -12 }, // 0x21 '!'
{ 6, 6, 6, 8, 1, -12 }, // 0x22 '"'
{ 11, 13, 13, 13, 0, -12 }, // 0x23 '#'
{ 33, 11, 17, 11, 0, -14 }, // 0x24 '$'
{ 57, 15, 13, 16, 0, -12 }, // 0x25 '%'
{ 82, 12, 13, 13, 1, -12 }, // 0x26 '&'
{ 102, 2, 6, 4, 1, -12 }, // 0x27 '''
{ 104, 5, 16, 6, 1, -12 }, // 0x28 '('
{ 114, 5, 16, 6, 0, -12 }, // 0x29 ')'
{ 124, 8, 7, 8, 0, -12 }, // 0x2A '*'
{ 131, 9, 8, 11, 1, -9 }, // 0x2B '+'
{ 140, 3, 6, 5, 1, -2 }, // 0x2C ','
{ 143, 5, 2, 7, 1, -5 }, // 0x2D '-'
{ 145, 3, 3, 5, 1, -2 }, // 0x2E '.'
{ 147, 9, 17, 7, -1, -14 }, // 0x2F '/'
{ 167, 11, 13, 12, 1, -12 }, // 0x30 '0'
{ 185, 6, 13, 7, 0, -12 }, // 0x31 '1'
{ 195, 10, 13, 11, 0, -12 }, // 0x32 '2'
{ 212, 10, 13, 11, 0, -12 }, // 0x33 '3'
{ 229, 11, 13, 12, 1, -12 }, // 0x34 '4'
{ 247, 10, 13, 11, 0, -12 }, // 0x35 '5'
{ 264, 10, 13, 11, 1, -12 }, // 0x36 '6'
{ 281, 11, 13, 11, 0, -12 }, // 0x37 '7'
{ 299, 10, 13, 12, 1, -12 }, // 0x38 '8'
{ 316, 11, 13, 11, 0, -12 }, // 0x39 '9'
{ 334, 3, 9, 5, 1, -8 }, // 0x3A ':'
{ 338, 3, 12, 5, 1, -8 }, // 0x3B ';'
{ 343, 9, 9, 11, 1, -10 }, // 0x3C '<'
{ 354, 9, 7, 11, 1, -9 }, // 0x3D '='
{ 362, 9, 9, 11, 1, -10 }, // 0x3E '>'
{ 373, 10, 13, 11, 0, -12 }, // 0x3F '?'
{ 390, 17, 17, 19, 1, -12 }, // 0x40 '@'
{ 427, 14, 13, 14, 0, -12 }, // 0x41 'A'
{ 450, 12, 13, 14, 1, -12 }, // 0x42 'B'
{ 470, 12, 13, 13, 1, -12 }, // 0x43 'C'
{ 490, 13, 13, 15, 1, -12 }, // 0x44 'D'
{ 512, 10, 13, 12, 1, -12 }, // 0x45 'E'
{ 529, 10, 13, 12, 1, -12 }, // 0x46 'F'
{ 546, 12, 13, 14, 1, -12 }, // 0x47 'G'
{ 566, 12, 13, 15, 1, -12 }, // 0x48 'H'
{ 586, 3, 13, 6, 1, -12 }, // 0x49 'I'
{ 591, 8, 13, 10, 0, -12 }, // 0x4A 'J'
{ 604, 13, 13, 13, 1, -12 }, // 0x4B 'K'
{ 626, 10, 13, 11, 1, -12 }, // 0x4C 'L'
{ 643, 15, 13, 17, 1, -12 }, // 0x4D 'M'
{ 668, 12, 13, 15, 1, -12 }, // 0x4E 'N'
{ 688, 13, 13, 15, 1, -12 }, // 0x4F 'O'
{ 710, 11, 13, 13, 1, -12 }, // 0x50 'P'
{ 728, 14, 16, 15, 1, -12 }, // 0x51 'Q'
{ 756, 12, 13, 13, 1, -12 }, // 0x52 'R'
{ 776, 11, 13, 11, 0, -12 }, // 0x53 'S'
{ 794, 11, 13, 11, 0, -12 }, // 0x54 'T'
{ 812, 12, 13, 14, 1, -12 }, // 0x55 'U'
{ 832, 14, 13, 13, 0, -12 }, // 0x56 'V'
{ 855, 21, 13, 21, 0, -12 }, // 0x57 'W'
{ 890, 13, 13, 13, 0, -12 }, // 0x58 'X'
{ 912, 12, 13, 12, 0, -12 }, // 0x59 'Y'
{ 932, 11, 13, 12, 1, -12 }, // 0x5A 'Z'
{ 950, 5, 16, 7, 1, -12 }, // 0x5B '['
{ 960, 9, 17, 7, -1, -14 }, // 0x5C '\'
{ 980, 5, 16, 7, 0, -12 }, // 0x5D ']'
{ 990, 9, 7, 11, 1, -9 }, // 0x5E '^'
{ 998, 9, 2, 9, 0, 1 }, // 0x5F '_'
{ 1001, 5, 3, 11, 2, -13 }, // 0x60 '`'
{ 1003, 9, 10, 11, 1, -9 }, // 0x61 'a'
{ 1015, 11, 13, 12, 1, -12 }, // 0x62 'b'
{ 1033, 9, 10, 11, 1, -9 }, // 0x63 'c'
{ 1045, 10, 13, 12, 1, -12 }, // 0x64 'd'
{ 1062, 10, 10, 11, 1, -9 }, // 0x65 'e'
{ 1075, 8, 14, 7, 0, -13 }, // 0x66 'f'
{ 1089, 10, 14, 13, 1, -9 }, // 0x67 'g'
{ 1107, 10, 13, 12, 1, -12 }, // 0x68 'h'
{ 1124, 3, 14, 5, 1, -13 }, // 0x69 'i'
{ 1130, 6, 18, 6, -2, -13 }, // 0x6A 'j'
{ 1144, 11, 13, 12, 1, -12 }, // 0x6B 'k'
{ 1162, 3, 13, 5, 1, -12 }, // 0x6C 'l'
{ 1167, 17, 10, 19, 1, -9 }, // 0x6D 'm'
{ 1189, 10, 10, 12, 1, -9 }, // 0x6E 'n'
{ 1202, 10, 10, 12, 1, -9 }, // 0x6F 'o'
{ 1215, 11, 13, 12, 1, -9 }, // 0x70 'p'
{ 1233, 10, 13, 12, 1, -9 }, // 0x71 'q'
{ 1250, 6, 10, 8, 1, -9 }, // 0x72 'r'
{ 1258, 9, 10, 10, 0, -9 }, // 0x73 's'
{ 1270, 8, 12, 8, 0, -11 }, // 0x74 't'
{ 1282, 10, 10, 12, 1, -9 }, // 0x75 'u'
{ 1295, 11, 10, 11, 0, -9 }, // 0x76 'v'
{ 1309, 17, 10, 17, 0, -9 }, // 0x77 'w'
{ 1331, 11, 10, 11, 0, -9 }, // 0x78 'x'
{ 1345, 11, 14, 11, 0, -9 }, // 0x79 'y'
{ 1365, 8, 10, 10, 1, -9 }, // 0x7A 'z'
{ 1375, 6, 16, 7, 1, -12 }, // 0x7B '{'
{ 1387, 3, 16, 6, 1, -12 }, // 0x7C '|'
{ 1393, 6, 16, 7, 0, -12 }, // 0x7D '}'
{ 1405, 9, 3, 11, 1, -7 } }; // 0x7E '~'
const GFXfont Montserrat_Bold9pt7b PROGMEM = {
(uint8_t *)Montserrat_Bold9pt7bBitmaps,
(GFXglyph *)Montserrat_Bold9pt7bGlyphs,
0x20, 0x7E, 21 };
// Approx. 2081 bytes
#endif // MONTSERRATBOLD9PT7B_H
@@ -0,0 +1,899 @@
// MontserratBold9pt8b.h
//
// Adafruit GFX font header — Latin Extended-A coverage (U+0020 to U+017F).
// Source: Montserrat Bold at 9pt.
// 352 glyphs covering ASCII, Latin-1 Supplement, and Latin Extended-A.
// Supports Czech, Polish, German, French, Spanish, Italian, Croatian,
// Hungarian, Slovak, Romanian, and other European languages.
//
// Codepoints 0x7F-0x9F (C0/C1 control codes) are present as empty glyphs.
// They are never rendered in normal use because UTF-8 decoding does not
// produce them as standalone codepoints.
//
// To render glyphs above 0x7F, use a drawCodepoint(font, x, y, cp) helper
// that walks the glyph table directly. Adafruit GFX's print() / write() /
// drawChar() are byte-oriented (uint8_t) and cannot address indices > 255.
#ifndef MONTSERRATBOLD9PT8B_H
#define MONTSERRATBOLD9PT8B_H
const uint8_t MontserratBold9pt8bBitmaps[] PROGMEM = {
0x00, 0xFF, 0xFF, 0xFF, 0xE3, 0xFE, 0xCF, 0x3C, 0xF3, 0xCF, 0x30, 0x0C,
0x60, 0x66, 0x03, 0x30, 0xFF, 0xF7, 0xFF, 0x8C, 0x60, 0x63, 0x03, 0x18,
0xFF, 0xF7, 0xFF, 0x86, 0x60, 0x33, 0x03, 0x18, 0x00, 0x06, 0x00, 0xC0,
0x3E, 0x1F, 0xE7, 0xFC, 0xEC, 0x1D, 0x83, 0xF0, 0x3F, 0x81, 0xF8, 0x1F,
0x83, 0x76, 0x6E, 0xFF, 0x8F, 0xE0, 0x30, 0x06, 0x00, 0x38, 0x18, 0xF8,
0x71, 0x98, 0xC6, 0x33, 0x04, 0x6C, 0x0D, 0x98, 0x0F, 0x67, 0x01, 0x9F,
0x03, 0x66, 0x0C, 0xCC, 0x31, 0x98, 0x63, 0xB1, 0x83, 0xC0, 0x1E, 0x03,
0xF0, 0x73, 0x87, 0x38, 0x77, 0x03, 0xE0, 0x3C, 0x0F, 0xE6, 0xE7, 0xEC,
0x3E, 0xE1, 0xEF, 0xFF, 0x7F, 0x60, 0xFF, 0xF0, 0x39, 0x9C, 0xE6, 0x73,
0x9C, 0xE7, 0x38, 0xC7, 0x38, 0xC7, 0x63, 0x9C, 0x73, 0x9C, 0xE7, 0x39,
0xCE, 0x73, 0x99, 0xCC, 0x18, 0x5E, 0x7E, 0x3C, 0xFE, 0x5A, 0x18, 0x1C,
0x0E, 0x07, 0x1F, 0xFF, 0xF8, 0xE0, 0x70, 0x38, 0xFF, 0xFD, 0x80, 0xFF,
0xC0, 0xFF, 0x80, 0x03, 0x81, 0x81, 0xC0, 0xE0, 0x60, 0x70, 0x38, 0x18,
0x1C, 0x0C, 0x06, 0x07, 0x03, 0x01, 0x81, 0xC0, 0xC0, 0x60, 0x00, 0x1E,
0x07, 0xF1, 0xFE, 0x70, 0xEE, 0x1D, 0xC3, 0xB8, 0x37, 0x06, 0xE1, 0xDC,
0x39, 0xCF, 0x3F, 0xC3, 0xF0, 0xFF, 0xFF, 0xC7, 0x1C, 0x71, 0xC7, 0x1C,
0x71, 0xC7, 0x1C, 0x1E, 0x1F, 0xEF, 0xF8, 0x07, 0x01, 0xC0, 0xE0, 0x78,
0x3C, 0x1E, 0x0F, 0x07, 0x81, 0xFF, 0x7F, 0xC0, 0x7F, 0x9F, 0xE7, 0xF8,
0x1C, 0x0E, 0x03, 0x81, 0xF8, 0x7F, 0x01, 0xC0, 0x76, 0x3F, 0xFE, 0x7F,
0x00, 0x07, 0x00, 0xE0, 0x38, 0x0E, 0x03, 0xC0, 0x70, 0x1C, 0xE7, 0x1C,
0xFF, 0xFF, 0xFC, 0x0E, 0x01, 0xC0, 0x38, 0x3F, 0x8F, 0xE3, 0xF9, 0xC0,
0x70, 0x1F, 0xC7, 0xF8, 0x1F, 0x01, 0xC0, 0x76, 0x3F, 0xFE, 0x7F, 0x00,
0x0F, 0x0F, 0xE7, 0xFB, 0xC0, 0xE0, 0x3B, 0xCF, 0xFB, 0xCF, 0xE1, 0xF8,
0x7F, 0x1D, 0xFE, 0x3F, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x70, 0xEE, 0x3C,
0x07, 0x00, 0xE0, 0x38, 0x07, 0x01, 0xC0, 0x38, 0x0F, 0x01, 0xC0, 0x1E,
0x1F, 0xEF, 0x3F, 0x87, 0xE1, 0xDF, 0xE7, 0xFB, 0xCF, 0xE1, 0xF8, 0x7E,
0x1F, 0xFE, 0x3F, 0x00, 0x1E, 0x07, 0xF1, 0xCF, 0x70, 0xEE, 0x1E, 0xE3,
0xDF, 0xF9, 0xF7, 0x00, 0xE0, 0x38, 0x0F, 0x3F, 0xC7, 0xF0, 0xFF, 0x80,
0x3F, 0xE0, 0xFF, 0x80, 0x3F, 0xFF, 0x60, 0x00, 0x83, 0xC7, 0xFF, 0x8E,
0x07, 0xC0, 0xFC, 0x1F, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0xE0, 0x00, 0x07,
0xFF, 0xFE, 0x80, 0x70, 0x3F, 0x03, 0xF0, 0x38, 0x7D, 0xF9, 0xE0, 0xC0,
0x00, 0x1E, 0x1F, 0xEF, 0xF8, 0x07, 0x01, 0xC0, 0xE0, 0x70, 0x38, 0x0E,
0x00, 0x00, 0xE0, 0x38, 0x0E, 0x00, 0x03, 0xE0, 0x0F, 0xFC, 0x0E, 0x07,
0x8E, 0x00, 0xC6, 0x3F, 0xB6, 0x3F, 0xCF, 0x38, 0xE7, 0x98, 0x73, 0xCC,
0x39, 0xE6, 0x1C, 0xF3, 0x8E, 0x79, 0xFF, 0xE6, 0x3D, 0xE3, 0x80, 0x00,
0xF0, 0x40, 0x1F, 0xE0, 0x03, 0xC0, 0x00, 0x07, 0x00, 0x1E, 0x00, 0x78,
0x03, 0xF0, 0x0D, 0xC0, 0x73, 0x81, 0xCE, 0x0E, 0x1C, 0x3F, 0xF1, 0xFF,
0xC7, 0x03, 0x98, 0x0E, 0xE0, 0x1C, 0xFF, 0x0F, 0xFC, 0xFF, 0xEE, 0x0E,
0xE0, 0xEE, 0x1E, 0xFF, 0xCF, 0xFE, 0xE0, 0x7E, 0x07, 0xE0, 0xFF, 0xFE,
0xFF, 0xC0, 0x0F, 0x83, 0xFE, 0x7F, 0xF7, 0x06, 0xE0, 0x0E, 0x00, 0xE0,
0x0E, 0x00, 0xE0, 0x0F, 0x00, 0x78, 0xE3, 0xFE, 0x1F, 0xC0, 0xFF, 0x07,
0xFE, 0x3F, 0xFD, 0xC0, 0xEE, 0x03, 0xF0, 0x1F, 0x80, 0xFC, 0x07, 0xE0,
0x3F, 0x03, 0xF8, 0x7D, 0xFF, 0xCF, 0xF8, 0x00, 0xFF, 0xFF, 0xFF, 0xFF,
0x80, 0xE0, 0x3F, 0xEF, 0xFB, 0xFE, 0xE0, 0x38, 0x0E, 0x03, 0xFF, 0xFF,
0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0xE0, 0x38, 0x0F, 0xFB, 0xFE, 0xE0,
0x38, 0x0E, 0x03, 0x80, 0xE0, 0x00, 0x0F, 0x83, 0xFE, 0x7F, 0xF7, 0x06,
0xE0, 0x0E, 0x00, 0xE0, 0x7E, 0x07, 0xE0, 0x7F, 0x07, 0x78, 0xF3, 0xFF,
0x1F, 0xC0, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7F, 0xFF, 0xFF,
0xFF, 0xFF, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x70, 0xFF, 0xFF,
0xFF, 0xFF, 0xFE, 0x7F, 0x7F, 0x7F, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
0x07, 0xC7, 0xFF, 0x7E, 0xE0, 0x77, 0x07, 0x38, 0x71, 0xC7, 0x0E, 0x70,
0x77, 0x03, 0xF8, 0x1F, 0xE0, 0xFF, 0x87, 0x9E, 0x38, 0x79, 0xC1, 0xCE,
0x07, 0x00, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE0, 0x38, 0x0E, 0x03, 0x80,
0xE0, 0x38, 0x0E, 0x03, 0xFF, 0xFF, 0xC0, 0xE0, 0x0F, 0xE0, 0x1F, 0xC0,
0x7F, 0xC1, 0xFF, 0x83, 0xFF, 0x8F, 0xFB, 0x9B, 0xF7, 0x77, 0xE7, 0xCF,
0xC7, 0x9F, 0x8E, 0x3F, 0x08, 0x7E, 0x00, 0xE0, 0xE0, 0x7F, 0x07, 0xF8,
0x7F, 0x87, 0xFC, 0x7F, 0xE7, 0xEF, 0x7E, 0x7F, 0xE3, 0xFE, 0x1F, 0xE0,
0xFE, 0x0F, 0xE0, 0x70, 0x0F, 0x81, 0xFF, 0x1F, 0xFC, 0xE0, 0xFE, 0x03,
0xF0, 0x1F, 0x80, 0x7C, 0x07, 0xE0, 0x3F, 0x81, 0xDE, 0x3C, 0x7F, 0xC1,
0xFC, 0x00, 0xFE, 0x1F, 0xFB, 0xFF, 0xF0, 0x7E, 0x0F, 0xC1, 0xF8, 0x3F,
0xFF, 0xFF, 0xDF, 0xC3, 0x80, 0x70, 0x0E, 0x00, 0x0F, 0x80, 0xFF, 0x87,
0xFF, 0x1C, 0x1E, 0xE0, 0x3B, 0x80, 0xEE, 0x01, 0xB8, 0x0E, 0xE0, 0x3B,
0xC0, 0xE7, 0x8F, 0x0F, 0xF8, 0x1F, 0xC0, 0x0F, 0x20, 0x1F, 0xC0, 0x1E,
0xFE, 0x0F, 0xFC, 0xFF, 0xEE, 0x0E, 0xE0, 0xEE, 0x0E, 0xE0, 0xEF, 0xFE,
0xFF, 0xCE, 0x38, 0xE1, 0xCE, 0x1E, 0xE0, 0xE0, 0x0F, 0x87, 0xF9, 0xFF,
0x38, 0x07, 0x00, 0xF8, 0x0F, 0xE0, 0x7E, 0x01, 0xE0, 0x1D, 0x83, 0xBF,
0xE3, 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0x87, 0x00, 0xE0, 0x1C, 0x03, 0x80,
0x70, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0x00, 0xE0, 0xE0, 0x7E, 0x07, 0xE0,
0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x77, 0x0F, 0x79,
0xE3, 0xFE, 0x1F, 0x80, 0xE0, 0x1F, 0x80, 0xE7, 0x03, 0x9C, 0x1C, 0x38,
0x70, 0xE1, 0x83, 0xCE, 0x07, 0x38, 0x1D, 0xC0, 0x3F, 0x00, 0xF8, 0x01,
0xE0, 0x07, 0x00, 0xE0, 0x70, 0x3B, 0x83, 0x83, 0x9C, 0x1E, 0x1C, 0xE1,
0xF0, 0xE3, 0x8D, 0x8E, 0x1C, 0x6E, 0x70, 0xE7, 0x73, 0x83, 0xB3, 0xB8,
0x1D, 0x8F, 0xC0, 0xFC, 0x7E, 0x03, 0xC3, 0xE0, 0x1E, 0x0F, 0x00, 0xF0,
0x78, 0x00, 0xF0, 0x73, 0x83, 0x8E, 0x38, 0x7B, 0x81, 0xFC, 0x07, 0xC0,
0x1C, 0x01, 0xF0, 0x1F, 0xC0, 0xEE, 0x0E, 0x38, 0xE1, 0xEF, 0x07, 0x00,
0xE0, 0x3F, 0x07, 0x70, 0xE3, 0x8E, 0x39, 0xC1, 0xD8, 0x1F, 0x80, 0xF0,
0x07, 0x00, 0x70, 0x07, 0x00, 0x70, 0x07, 0x00, 0xFF, 0xDF, 0xFB, 0xFF,
0x01, 0xC0, 0x78, 0x1E, 0x03, 0x80, 0xE0, 0x38, 0x0E, 0x03, 0xC0, 0x7F,
0xFF, 0xFE, 0xFF, 0xF9, 0xCE, 0x73, 0x9C, 0xE7, 0x39, 0xCE, 0x73, 0xFF,
0xE0, 0x30, 0x1C, 0x06, 0x03, 0x01, 0xC0, 0x60, 0x30, 0x1C, 0x06, 0x03,
0x01, 0xC0, 0x60, 0x38, 0x1C, 0x06, 0x03, 0x80, 0xFF, 0xCE, 0x73, 0x9C,
0xE7, 0x39, 0xCE, 0x73, 0x9F, 0xFF, 0x1C, 0x0E, 0x0D, 0x86, 0xC6, 0x73,
0x1B, 0x8C, 0xFF, 0xFF, 0xC0, 0xE3, 0x8E, 0x3C, 0x7F, 0x99, 0xE0, 0x73,
0xFF, 0xFF, 0x8F, 0x87, 0xFF, 0xBF, 0xC0, 0xE0, 0x1C, 0x03, 0x80, 0x77,
0x8F, 0xF9, 0xF7, 0xBC, 0x3F, 0x07, 0xE0, 0xFC, 0x1F, 0xC7, 0x7F, 0xEE,
0xF8, 0x1E, 0x3F, 0xFF, 0xFC, 0x2C, 0x06, 0x03, 0x81, 0xE7, 0x7F, 0x9F,
0x80, 0x01, 0xC0, 0x70, 0x1C, 0x77, 0x7F, 0xFF, 0xFE, 0x1F, 0x87, 0xC1,
0xF8, 0x7F, 0x3D, 0xFF, 0x3F, 0xC0, 0x1E, 0x1F, 0xCF, 0x3B, 0x87, 0xFF,
0xFF, 0xFE, 0x03, 0xC4, 0x7F, 0x8F, 0xC0, 0x00, 0x1E, 0x3E, 0x30, 0x30,
0xFE, 0xFE, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x1C, 0xDF, 0xFF,
0xFF, 0x87, 0xC1, 0xF8, 0x7E, 0x1F, 0xFF, 0x7F, 0xC2, 0x74, 0x1D, 0xFF,
0xFF, 0x87, 0x00, 0xE0, 0x38, 0x0E, 0x03, 0xBC, 0xFF, 0xBF, 0xFF, 0x1F,
0x87, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xE1, 0xC0, 0xFF, 0x8F, 0xFF, 0xFF,
0xFF, 0xC0, 0x1C, 0x71, 0xC0, 0x1C, 0x71, 0xC7, 0x1C, 0x71, 0xC7, 0x1C,
0x71, 0xDF, 0x79, 0xC0, 0xE0, 0x1C, 0x03, 0x80, 0x70, 0xEE, 0x39, 0xCE,
0x3B, 0x87, 0xF0, 0xFE, 0x1E, 0xE3, 0x9E, 0x71, 0xEE, 0x1E, 0xFF, 0xFF,
0xFF, 0xFF, 0xFE, 0xEF, 0x1C, 0x7F, 0xFF, 0xBF, 0xFF, 0xDC, 0x38, 0x7E,
0x1C, 0x3F, 0x0E, 0x1F, 0x87, 0x0F, 0xC3, 0x87, 0xE1, 0xC3, 0xF0, 0xE1,
0xC0, 0xEF, 0x3F, 0xEF, 0xFF, 0xC7, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xE1,
0xF8, 0x70, 0x1E, 0x1F, 0xEF, 0xFB, 0x87, 0xC1, 0xF0, 0x7E, 0x1F, 0xCF,
0x7F, 0x8F, 0xC0, 0xEF, 0x1F, 0xF3, 0xEF, 0x78, 0x7E, 0x0F, 0xC1, 0xF8,
0x3F, 0x8E, 0xFF, 0xDD, 0xF3, 0x80, 0x70, 0x0E, 0x00, 0x1D, 0xDF, 0xFF,
0xFF, 0x87, 0xC1, 0xF0, 0x7E, 0x1F, 0xCF, 0x7F, 0xCF, 0xF0, 0x1C, 0x07,
0x01, 0xC0, 0xEF, 0xFF, 0xFC, 0xE3, 0x8E, 0x38, 0xE3, 0x80, 0x1F, 0x3F,
0xDC, 0x4C, 0x07, 0xE1, 0xFC, 0x0E, 0x87, 0x7F, 0xBF, 0x80, 0x30, 0x30,
0xFE, 0xFE, 0x30, 0x30, 0x30, 0x30, 0x30, 0x38, 0x3E, 0x1F, 0xE1, 0xF8,
0x7E, 0x1F, 0x87, 0xE1, 0xF8, 0x7E, 0x1F, 0xCF, 0x7F, 0xCF, 0xF0, 0xE0,
0xFC, 0x19, 0xC7, 0x38, 0xE3, 0x38, 0x77, 0x0E, 0xC0, 0xF8, 0x1E, 0x01,
0xC0, 0xE1, 0xC3, 0xF0, 0xE1, 0x98, 0x70, 0xCE, 0x7C, 0xE3, 0x36, 0x61,
0xBB, 0xB0, 0xFD, 0xF8, 0x3C, 0x78, 0x1E, 0x3C, 0x0E, 0x1E, 0x00, 0xE1,
0xCE, 0x38, 0xEE, 0x0F, 0x81, 0xE0, 0x1C, 0x07, 0xC1, 0xDC, 0x73, 0xDC,
0x38, 0xE0, 0xFC, 0x19, 0xC7, 0x38, 0xE3, 0x38, 0x77, 0x06, 0xC0, 0xF8,
0x1E, 0x01, 0xC0, 0x38, 0x7E, 0x0F, 0x80, 0xE0, 0x00, 0xFF, 0xFF, 0x07,
0x0E, 0x1C, 0x38, 0x70, 0xE0, 0xFF, 0xFF, 0x3C, 0xF7, 0x1C, 0x71, 0xC7,
0x3C, 0xF1, 0xC7, 0x1C, 0x71, 0xC7, 0xCF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xF3, 0xE3, 0x8E, 0x38, 0xE3, 0x8F, 0x3C, 0xE3, 0x8E, 0x38, 0xEF,
0xBC, 0x71, 0xFF, 0xF3, 0xC0, 0xFF, 0xFF, 0xE0, 0x70, 0x38, 0x1C, 0x0E,
0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70, 0x3F, 0xF8, 0xFF, 0xFF, 0xE0, 0x70,
0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70, 0x3F, 0xF8, 0xFF,
0xFF, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70,
0x3F, 0xF8, 0xFF, 0xFF, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81,
0xC0, 0xE0, 0x70, 0x3F, 0xF8, 0xFF, 0xFF, 0xE0, 0x70, 0x38, 0x1C, 0x0E,
0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70, 0x3F, 0xF8, 0xFF, 0xFF, 0xE0, 0x70,
0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70, 0x3F, 0xF8, 0xFF,
0xFF, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70,
0x3F, 0xF8, 0xFF, 0xFF, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81,
0xC0, 0xE0, 0x70, 0x3F, 0xF8, 0xFF, 0xFF, 0xE0, 0x70, 0x38, 0x1C, 0x0E,
0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70, 0x3F, 0xF8, 0xFF, 0xFF, 0xE0, 0x70,
0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70, 0x3F, 0xF8, 0xFF,
0xFF, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70,
0x3F, 0xF8, 0xFF, 0xFF, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81,
0xC0, 0xE0, 0x70, 0x3F, 0xF8, 0xFF, 0xFF, 0xE0, 0x70, 0x38, 0x1C, 0x0E,
0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70, 0x3F, 0xF8, 0xFF, 0xFF, 0xE0, 0x70,
0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70, 0x3F, 0xF8, 0xFF,
0xFF, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70,
0x3F, 0xF8, 0xFF, 0xFF, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81,
0xC0, 0xE0, 0x70, 0x3F, 0xF8, 0xFF, 0xFF, 0xE0, 0x70, 0x38, 0x1C, 0x0E,
0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70, 0x3F, 0xF8, 0xFF, 0xFF, 0xE0, 0x70,
0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70, 0x3F, 0xF8, 0xFF,
0xFF, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70,
0x3F, 0xF8, 0xFF, 0xFF, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81,
0xC0, 0xE0, 0x70, 0x3F, 0xF8, 0xFF, 0xFF, 0xE0, 0x70, 0x38, 0x1C, 0x0E,
0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70, 0x3F, 0xF8, 0xFF, 0xFF, 0xE0, 0x70,
0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70, 0x3F, 0xF8, 0xFF,
0xFF, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70,
0x3F, 0xF8, 0xFF, 0xFF, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81,
0xC0, 0xE0, 0x70, 0x3F, 0xF8, 0xFF, 0xFF, 0xE0, 0x70, 0x38, 0x1C, 0x0E,
0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70, 0x3F, 0xF8, 0xFF, 0xFF, 0xE0, 0x70,
0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70, 0x3F, 0xF8, 0xFF,
0xFF, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70,
0x3F, 0xF8, 0xFF, 0xFF, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81,
0xC0, 0xE0, 0x70, 0x3F, 0xF8, 0xFF, 0xFF, 0xE0, 0x70, 0x38, 0x1C, 0x0E,
0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70, 0x3F, 0xF8, 0xFF, 0xFF, 0xE0, 0x70,
0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70, 0x3F, 0xF8, 0xFF,
0xFF, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70,
0x3F, 0xF8, 0xFF, 0xFF, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81,
0xC0, 0xE0, 0x70, 0x3F, 0xF8, 0xFF, 0xFF, 0xE0, 0x70, 0x38, 0x1C, 0x0E,
0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70, 0x3F, 0xF8, 0x00, 0x7F, 0x81, 0xFF,
0xFF, 0xFE, 0x0C, 0x06, 0x07, 0x8F, 0xFF, 0xFF, 0x6B, 0x31, 0x98, 0xEC,
0x7F, 0xDF, 0xE7, 0xE0, 0xC0, 0x60, 0x07, 0xC3, 0xFC, 0xFF, 0x1C, 0x07,
0x00, 0xE0, 0x3F, 0xE7, 0xFC, 0x70, 0x0E, 0x01, 0xC0, 0x7F, 0xEF, 0xFC,
0x00, 0x26, 0x07, 0x7F, 0xF3, 0xFE, 0x38, 0xE3, 0x06, 0x30, 0x63, 0x0E,
0x39, 0xC7, 0xFE, 0xFF, 0x76, 0x02, 0xE0, 0x1B, 0xC0, 0xE7, 0x07, 0x0E,
0x38, 0x1C, 0xE0, 0x77, 0x00, 0xF8, 0x0F, 0xF8, 0x3F, 0xE0, 0x1C, 0x03,
0xFE, 0x01, 0xC0, 0x07, 0x00, 0xFF, 0xFF, 0xC0, 0x03, 0xFF, 0xFF, 0x1E,
0x1F, 0xDC, 0x4C, 0x07, 0x83, 0xF9, 0x9E, 0xC7, 0x73, 0xBF, 0x87, 0xC0,
0x74, 0x33, 0xF8, 0xF8, 0x03, 0xB6, 0xC0, 0x07, 0x03, 0xFC, 0x60, 0x64,
0xF3, 0x9F, 0x99, 0x81, 0xB8, 0x19, 0x81, 0x9F, 0x9C, 0xFB, 0x40, 0x23,
0x0C, 0x1F, 0x80, 0x00, 0xF8, 0x33, 0xF4, 0x6F, 0xC0, 0x33, 0x33, 0x3B,
0x99, 0x86, 0x63, 0xB8, 0xEE, 0xFF, 0xFF, 0xC0, 0xE0, 0x70, 0x38, 0xFF,
0xFF, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70,
0x3F, 0xF8, 0x07, 0x03, 0xFC, 0x60, 0x65, 0xF3, 0x9F, 0x99, 0x89, 0x99,
0x99, 0xF9, 0x99, 0x9D, 0x9B, 0x40, 0x23, 0x0C, 0x1F, 0x80, 0xFF, 0xFC,
0x73, 0xE8, 0xE3, 0xCF, 0xE0, 0x1C, 0x0E, 0x07, 0x1F, 0xFF, 0xF8, 0xE0,
0x70, 0x38, 0x00, 0x00, 0x3F, 0xFF, 0xF0, 0x38, 0xF9, 0x18, 0x71, 0xC7,
0x1F, 0xBF, 0x7E, 0x18, 0x60, 0xF0, 0x6D, 0xDF, 0x00, 0x3B, 0xB8, 0xE1,
0xF8, 0x7E, 0x1F, 0x87, 0xE1, 0xF8, 0x7E, 0x1F, 0xCF, 0xFF, 0xFF, 0xBE,
0x03, 0x80, 0xE0, 0x00, 0x3F, 0xEF, 0xFF, 0xF1, 0xFE, 0x37, 0xC6, 0xF8,
0xC3, 0x18, 0x63, 0x0C, 0x61, 0x8C, 0x31, 0x86, 0x30, 0xC6, 0x18, 0xC3,
0x18, 0x7F, 0xB0, 0x67, 0x1F, 0xF0, 0xC3, 0x0C, 0x33, 0xFF, 0xC0, 0x00,
0xF9, 0x9E, 0x36, 0x6F, 0x80, 0x77, 0x0C, 0xC3, 0xB8, 0x66, 0x3B, 0x9D,
0xC6, 0x60, 0x00, 0x0C, 0x78, 0x0C, 0x0C, 0x0E, 0x06, 0x06, 0x03, 0x06,
0x01, 0x87, 0x03, 0xF3, 0x19, 0xFB, 0x18, 0x03, 0x9C, 0x01, 0x8D, 0x81,
0x8F, 0xE1, 0x80, 0x60, 0xC0, 0x30, 0x00, 0x0C, 0x78, 0x0C, 0x0C, 0x0E,
0x06, 0x06, 0x03, 0x06, 0x01, 0x87, 0x73, 0xF3, 0x7D, 0xFB, 0x23, 0x03,
0x83, 0x81, 0x83, 0x81, 0x83, 0x81, 0x83, 0xF0, 0xC1, 0xF8, 0x00, 0x06,
0x1F, 0x83, 0x00, 0xC1, 0xC0, 0x60, 0x60, 0x1E, 0x30, 0x01, 0x9C, 0x06,
0xE6, 0x31, 0xF3, 0x18, 0x01, 0xCE, 0x00, 0x63, 0x60, 0x31, 0xFC, 0x18,
0x06, 0x06, 0x01, 0x80, 0x18, 0x07, 0x01, 0xC0, 0x00, 0x00, 0x07, 0x03,
0x81, 0xC0, 0xE0, 0x38, 0x4F, 0x3D, 0xFE, 0x3E, 0x00, 0x0E, 0x00, 0x1C,
0x00, 0x38, 0x00, 0x00, 0x07, 0x00, 0x1E, 0x00, 0x78, 0x03, 0xF0, 0x0D,
0xC0, 0x73, 0x81, 0xCE, 0x0E, 0x1C, 0x3F, 0xF1, 0xFF, 0xC7, 0x03, 0x98,
0x0E, 0xE0, 0x1C, 0x00, 0xE0, 0x07, 0x00, 0x38, 0x00, 0x00, 0x07, 0x00,
0x1E, 0x00, 0x78, 0x03, 0xF0, 0x0D, 0xC0, 0x73, 0x81, 0xCE, 0x0E, 0x1C,
0x3F, 0xF1, 0xFF, 0xC7, 0x03, 0x98, 0x0E, 0xE0, 0x1C, 0x03, 0x80, 0x1F,
0x00, 0xCE, 0x00, 0x00, 0x07, 0x00, 0x1E, 0x00, 0x78, 0x03, 0xF0, 0x0D,
0xC0, 0x73, 0x81, 0xCE, 0x0E, 0x1C, 0x3F, 0xF1, 0xFF, 0xC7, 0x03, 0x98,
0x0E, 0xE0, 0x1C, 0x0F, 0x60, 0x37, 0x80, 0x00, 0x01, 0xC0, 0x07, 0x80,
0x1E, 0x00, 0xFC, 0x03, 0x70, 0x1C, 0xE0, 0x73, 0x83, 0x87, 0x0F, 0xFC,
0x7F, 0xF1, 0xC0, 0xE6, 0x03, 0xB8, 0x07, 0x00, 0x00, 0x3B, 0x00, 0x6C,
0x00, 0x00, 0x07, 0x00, 0x1E, 0x00, 0x78, 0x03, 0xF0, 0x0D, 0xC0, 0x73,
0x81, 0xCE, 0x0E, 0x1C, 0x3F, 0xF1, 0xFF, 0xC7, 0x03, 0x98, 0x0E, 0xE0,
0x1C, 0x03, 0x80, 0x1B, 0x00, 0x4C, 0x00, 0xE0, 0x00, 0x00, 0x1C, 0x00,
0x78, 0x01, 0xE0, 0x0F, 0xC0, 0x37, 0x01, 0xCE, 0x07, 0x38, 0x38, 0x70,
0xFF, 0xC7, 0xFF, 0x1C, 0x0E, 0x60, 0x3B, 0x80, 0x70, 0x01, 0xFF, 0xC0,
0x7F, 0xF8, 0x0F, 0xFF, 0x03, 0xB8, 0x00, 0x67, 0x00, 0x1C, 0xFF, 0x83,
0x1F, 0xF0, 0xE3, 0xFE, 0x3F, 0xF0, 0x07, 0xFE, 0x01, 0xC1, 0xC0, 0x38,
0x3F, 0xFE, 0x07, 0xFE, 0x0F, 0x83, 0xFE, 0x7F, 0xF7, 0x06, 0xE0, 0x0E,
0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0F, 0x00, 0x78, 0xE3, 0xFE, 0x1F, 0xC0,
0x60, 0x07, 0x00, 0x10, 0x0F, 0x00, 0x38, 0x07, 0x00, 0xE0, 0x00, 0xFF,
0xFF, 0xFF, 0xFF, 0x80, 0xE0, 0x3F, 0xEF, 0xFB, 0xFE, 0xE0, 0x38, 0x0E,
0x03, 0xFF, 0xFF, 0xC0, 0x03, 0x81, 0xC0, 0xE0, 0x00, 0xFF, 0xFF, 0xFF,
0xFF, 0x80, 0xE0, 0x3F, 0xEF, 0xFB, 0xFE, 0xE0, 0x38, 0x0E, 0x03, 0xFF,
0xFF, 0xC0, 0x0E, 0x07, 0xC3, 0x38, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x80,
0xE0, 0x3F, 0xEF, 0xFB, 0xFE, 0xE0, 0x38, 0x0E, 0x03, 0xFF, 0xFF, 0xC0,
0x00, 0x0E, 0xC1, 0xB0, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0xE0, 0x3F,
0xEF, 0xFB, 0xFE, 0xE0, 0x38, 0x0E, 0x03, 0xFF, 0xFF, 0xC0, 0xE3, 0x8E,
0x07, 0x39, 0xCE, 0x73, 0x9C, 0xE7, 0x39, 0xCE, 0x70, 0x1C, 0xE7, 0x00,
0xE3, 0x8E, 0x38, 0xE3, 0x8E, 0x38, 0xE3, 0x8E, 0x38, 0xE0, 0x38, 0xFB,
0x38, 0x07, 0x0E, 0x1C, 0x38, 0x70, 0xE1, 0xC3, 0x87, 0x0E, 0x1C, 0x38,
0x70, 0x03, 0xB6, 0xC0, 0x71, 0xC7, 0x1C, 0x71, 0xC7, 0x1C, 0x71, 0xC7,
0x1C, 0x70, 0xFF, 0x07, 0xFE, 0x3F, 0xFD, 0xC0, 0xEE, 0x03, 0xF0, 0x1F,
0xF8, 0xFF, 0xC7, 0xE0, 0x3F, 0x03, 0xF8, 0x7D, 0xFF, 0xCF, 0xF8, 0x00,
0x1E, 0xC1, 0xBC, 0x00, 0x0E, 0x07, 0xF0, 0x7F, 0x87, 0xF8, 0x7F, 0xC7,
0xFE, 0x7E, 0xF7, 0xE7, 0xFE, 0x3F, 0xE1, 0xFE, 0x0F, 0xE0, 0xFE, 0x07,
0x1C, 0x00, 0x70, 0x01, 0xC0, 0x00, 0x00, 0xF8, 0x1F, 0xF1, 0xFF, 0xCE,
0x0F, 0xE0, 0x3F, 0x01, 0xF8, 0x07, 0xC0, 0x7E, 0x03, 0xF8, 0x1D, 0xE3,
0xC7, 0xFC, 0x1F, 0xC0, 0x01, 0xC0, 0x1C, 0x01, 0xC0, 0x00, 0x00, 0xF8,
0x1F, 0xF1, 0xFF, 0xCE, 0x0F, 0xE0, 0x3F, 0x01, 0xF8, 0x07, 0xC0, 0x7E,
0x03, 0xF8, 0x1D, 0xE3, 0xC7, 0xFC, 0x1F, 0xC0, 0x07, 0x00, 0x7C, 0x06,
0x70, 0x00, 0x00, 0xF8, 0x1F, 0xF1, 0xFF, 0xCE, 0x0F, 0xE0, 0x3F, 0x01,
0xF8, 0x07, 0xC0, 0x7E, 0x03, 0xF8, 0x1D, 0xE3, 0xC7, 0xFC, 0x1F, 0xC0,
0x1E, 0xC0, 0xDE, 0x00, 0x00, 0x1F, 0x03, 0xFE, 0x3F, 0xF9, 0xC1, 0xFC,
0x07, 0xE0, 0x3F, 0x00, 0xF8, 0x0F, 0xC0, 0x7F, 0x03, 0xBC, 0x78, 0xFF,
0x83, 0xF8, 0x00, 0x00, 0xEC, 0x03, 0x60, 0x00, 0x00, 0xF8, 0x1F, 0xF1,
0xFF, 0xCE, 0x0F, 0xE0, 0x3F, 0x01, 0xF8, 0x07, 0xC0, 0x7E, 0x03, 0xF8,
0x1D, 0xE3, 0xC7, 0xFC, 0x1F, 0xC0, 0x45, 0xDD, 0xF1, 0xC7, 0xDD, 0xF1,
0x80, 0x00, 0x60, 0x7F, 0x0F, 0xF8, 0xFF, 0xE7, 0x1F, 0xF1, 0x9F, 0x8C,
0xFC, 0xC3, 0xE6, 0x3F, 0x61, 0xFF, 0x0E, 0xF1, 0xE3, 0xFE, 0x1F, 0xE0,
0x80, 0x00, 0x1C, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x7E, 0x07, 0xE0,
0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x77, 0x0F, 0x79,
0xE3, 0xFE, 0x1F, 0x80, 0x01, 0xC0, 0x38, 0x07, 0x00, 0x00, 0xE0, 0x7E,
0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x77,
0x0F, 0x79, 0xE3, 0xFE, 0x1F, 0x80, 0x07, 0x00, 0xF8, 0x19, 0xC0, 0x00,
0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07,
0xE0, 0x77, 0x0F, 0x79, 0xE3, 0xFE, 0x1F, 0x80, 0x00, 0x01, 0xD8, 0x0D,
0x80, 0x00, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0,
0x7E, 0x07, 0xE0, 0x77, 0x0F, 0x79, 0xE3, 0xFE, 0x1F, 0x80, 0x01, 0xC0,
0x38, 0x07, 0x00, 0x00, 0xE0, 0x3F, 0x07, 0x70, 0xE3, 0x8E, 0x39, 0xC1,
0xD8, 0x1F, 0x80, 0xF0, 0x07, 0x00, 0x70, 0x07, 0x00, 0x70, 0x07, 0x00,
0xE0, 0x1C, 0x03, 0xFE, 0x7F, 0xEE, 0x1F, 0xC1, 0xF8, 0x3F, 0x07, 0xE3,
0xFF, 0xFB, 0xFE, 0x70, 0x0E, 0x00, 0x00, 0x07, 0xE1, 0xFE, 0x38, 0xEE,
0x1D, 0xC3, 0xB9, 0xE7, 0x3E, 0xE7, 0xDC, 0x1F, 0x83, 0xF0, 0xFE, 0xFD,
0xDF, 0x00, 0x70, 0x1C, 0x07, 0x00, 0x03, 0xC7, 0xF9, 0x9E, 0x07, 0x3F,
0xFF, 0xF8, 0xF8, 0x7F, 0xFB, 0xFC, 0x07, 0x07, 0x07, 0x00, 0x03, 0xC7,
0xF9, 0x9E, 0x07, 0x3F, 0xFF, 0xF8, 0xF8, 0x7F, 0xFB, 0xFC, 0x1C, 0x1F,
0x19, 0xC0, 0x03, 0xC7, 0xF9, 0x9E, 0x07, 0x3F, 0xFF, 0xF8, 0xF8, 0x7F,
0xFB, 0xFC, 0x30, 0x3D, 0x93, 0x80, 0x03, 0xC7, 0xF9, 0x9E, 0x07, 0x3F,
0xFF, 0xF8, 0xF8, 0x7F, 0xFB, 0xFC, 0x00, 0x3B, 0x0D, 0x80, 0x03, 0xC7,
0xF9, 0x9E, 0x07, 0x3F, 0xFF, 0xF8, 0xF8, 0x7F, 0xFB, 0xFC, 0x1C, 0x1B,
0x09, 0x83, 0x80, 0x01, 0xE3, 0xFC, 0xCF, 0x03, 0x9F, 0xFF, 0xFC, 0x7C,
0x3F, 0xFD, 0xFE, 0x3C, 0x78, 0xFF, 0xFE, 0x67, 0xCF, 0x03, 0x87, 0x3F,
0xFF, 0xFF, 0xFF, 0xE3, 0x80, 0xC3, 0xC6, 0xFF, 0xFF, 0x7C, 0x7C, 0x1E,
0x3F, 0xFF, 0xFC, 0x2C, 0x06, 0x03, 0x81, 0xE7, 0x7F, 0x9F, 0x86, 0x03,
0x80, 0x41, 0xE0, 0x70, 0x0E, 0x01, 0xC0, 0x00, 0x1E, 0x1F, 0xCF, 0x3B,
0x87, 0xFF, 0xFF, 0xFE, 0x03, 0xC4, 0x7F, 0x8F, 0xC0, 0x07, 0x03, 0x81,
0xC0, 0x00, 0x1E, 0x1F, 0xCF, 0x3B, 0x87, 0xFF, 0xFF, 0xFE, 0x03, 0xC4,
0x7F, 0x8F, 0xC0, 0x1C, 0x0F, 0x86, 0x70, 0x00, 0x1E, 0x1F, 0xCF, 0x3B,
0x87, 0xFF, 0xFF, 0xFE, 0x03, 0xC4, 0x7F, 0x8F, 0xC0, 0x00, 0x1D, 0x83,
0x60, 0x00, 0x1E, 0x1F, 0xCF, 0x3B, 0x87, 0xFF, 0xFF, 0xFE, 0x03, 0xC4,
0x7F, 0x8F, 0xC0, 0xE3, 0x8E, 0x03, 0x9C, 0xE7, 0x39, 0xCE, 0x73, 0x9C,
0x3B, 0xB8, 0x0E, 0x73, 0x9C, 0xE7, 0x39, 0xCE, 0x70, 0x38, 0xF1, 0xB0,
0x03, 0x87, 0x0E, 0x1C, 0x38, 0x70, 0xE1, 0xC3, 0x87, 0x00, 0xDE, 0xC0,
0xE7, 0x39, 0xCE, 0x73, 0x9C, 0xE7, 0x00, 0x3D, 0x83, 0xC1, 0xF0, 0xCE,
0x01, 0x8F, 0xF7, 0xFD, 0xC7, 0xE1, 0xF8, 0x77, 0x1D, 0xFE, 0x3F, 0x00,
0x18, 0x0F, 0x62, 0x70, 0x00, 0xEF, 0x3F, 0xEF, 0xFF, 0xC7, 0xE1, 0xF8,
0x7E, 0x1F, 0x87, 0xE1, 0xF8, 0x70, 0x70, 0x0E, 0x01, 0xC0, 0x00, 0x1E,
0x1F, 0xEF, 0xFB, 0x87, 0xC1, 0xF0, 0x7E, 0x1F, 0xCF, 0x7F, 0x8F, 0xC0,
0x07, 0x03, 0x81, 0xC0, 0x00, 0x1E, 0x1F, 0xEF, 0xFB, 0x87, 0xC1, 0xF0,
0x7E, 0x1F, 0xCF, 0x7F, 0x8F, 0xC0, 0x1C, 0x0F, 0x86, 0x70, 0x00, 0x1E,
0x1F, 0xEF, 0xFB, 0x87, 0xC1, 0xF0, 0x7E, 0x1F, 0xCF, 0x7F, 0x8F, 0xC0,
0x30, 0x1E, 0xC4, 0xE0, 0x00, 0x1E, 0x1F, 0xEF, 0xFB, 0x87, 0xC1, 0xF0,
0x7E, 0x1F, 0xCF, 0x7F, 0x8F, 0xC0, 0x00, 0x1D, 0x83, 0x60, 0x00, 0x1E,
0x1F, 0xEF, 0xFB, 0x87, 0xC1, 0xF0, 0x7E, 0x1F, 0xCF, 0x7F, 0x8F, 0xC0,
0x08, 0x0E, 0x07, 0x00, 0x00, 0x07, 0xFF, 0xFE, 0x00, 0x1C, 0x0E, 0x06,
0x00, 0x01, 0x07, 0xC7, 0xFB, 0xFE, 0xE5, 0xF3, 0x7C, 0x9F, 0xE7, 0xF3,
0xDF, 0xE3, 0xF1, 0x80, 0x38, 0x07, 0x00, 0xE0, 0x00, 0xE1, 0xF8, 0x7E,
0x1F, 0x87, 0xE1, 0xF8, 0x7E, 0x1F, 0xCF, 0x7F, 0xCF, 0xF0, 0x03, 0x81,
0xC0, 0xE0, 0x00, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xE1, 0xF8, 0x7E, 0x1F,
0xCF, 0x7F, 0xCF, 0xF0, 0x0E, 0x07, 0xC3, 0x38, 0x00, 0xE1, 0xF8, 0x7E,
0x1F, 0x87, 0xE1, 0xF8, 0x7E, 0x1F, 0xCF, 0x7F, 0xCF, 0xF0, 0x00, 0x0E,
0xC1, 0xB0, 0x00, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xE1, 0xF8, 0x7E, 0x1F,
0xCF, 0x7F, 0xCF, 0xF0, 0x03, 0x80, 0xE0, 0x38, 0x00, 0x0E, 0x0F, 0xC1,
0x9C, 0x73, 0x8E, 0x33, 0x87, 0x70, 0x6C, 0x0F, 0x81, 0xE0, 0x1C, 0x03,
0x87, 0xE0, 0xF8, 0x0E, 0x00, 0xE0, 0x1C, 0x03, 0x80, 0x77, 0x8F, 0xF9,
0xF7, 0xBC, 0x3F, 0x07, 0xE0, 0xFC, 0x1F, 0xC7, 0x7F, 0xEE, 0xF9, 0xC0,
0x38, 0x07, 0x00, 0x00, 0x07, 0x60, 0x6C, 0x00, 0x0E, 0x0F, 0xC1, 0x9C,
0x73, 0x8E, 0x33, 0x87, 0x70, 0x6C, 0x0F, 0x81, 0xE0, 0x1C, 0x03, 0x87,
0xE0, 0xF8, 0x0E, 0x00, 0x0F, 0xE0, 0x3F, 0x80, 0x00, 0x01, 0xC0, 0x07,
0x80, 0x1E, 0x00, 0xFC, 0x03, 0x70, 0x1C, 0xE0, 0x73, 0x83, 0x87, 0x0F,
0xFC, 0x7F, 0xF1, 0xC0, 0xE6, 0x03, 0xB8, 0x07, 0x7F, 0x3F, 0x80, 0x07,
0x8F, 0xF3, 0x3C, 0x0E, 0x7F, 0xFF, 0xF1, 0xF0, 0xFF, 0xF7, 0xF8, 0x0C,
0x60, 0x33, 0x00, 0x7C, 0x00, 0x00, 0x07, 0x00, 0x1E, 0x00, 0x78, 0x03,
0xF0, 0x0D, 0xC0, 0x73, 0x81, 0xCE, 0x0E, 0x1C, 0x3F, 0xF1, 0xFF, 0xC7,
0x03, 0x98, 0x0E, 0xE0, 0x1C, 0x63, 0x33, 0x0F, 0x80, 0x03, 0xC7, 0xF9,
0x9E, 0x07, 0x3F, 0xFF, 0xF8, 0xF8, 0x7F, 0xFB, 0xFC, 0x07, 0x00, 0x1E,
0x00, 0x78, 0x03, 0xF0, 0x0D, 0xC0, 0x73, 0x81, 0xCE, 0x0E, 0x1C, 0x3F,
0xF1, 0xFF, 0xC7, 0x03, 0x98, 0x0E, 0xE0, 0x1C, 0x00, 0xE0, 0x03, 0x00,
0x0C, 0x00, 0x3C, 0x3C, 0x7F, 0x99, 0xE0, 0x73, 0xFF, 0xFF, 0x8F, 0x87,
0xFF, 0xBF, 0xC1, 0xC0, 0xC0, 0x60, 0x3C, 0x01, 0xC0, 0x38, 0x07, 0x00,
0x00, 0x0F, 0x83, 0xFE, 0x7F, 0xF7, 0x06, 0xE0, 0x0E, 0x00, 0xE0, 0x0E,
0x00, 0xE0, 0x0F, 0x00, 0x78, 0xE3, 0xFE, 0x1F, 0xC0, 0x07, 0x07, 0x07,
0x00, 0x01, 0xE3, 0xFF, 0xFF, 0xC2, 0xC0, 0x60, 0x38, 0x1E, 0x77, 0xF9,
0xF8, 0x07, 0x00, 0xF8, 0x19, 0xC0, 0x00, 0x0F, 0x83, 0xFE, 0x7F, 0xF7,
0x06, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0F, 0x00, 0x78, 0xE3,
0xFE, 0x1F, 0xC0, 0x1C, 0x1F, 0x19, 0xC0, 0x01, 0xE3, 0xFF, 0xFF, 0xC2,
0xC0, 0x60, 0x38, 0x1E, 0x77, 0xF9, 0xF8, 0x07, 0x00, 0x70, 0x07, 0x00,
0x00, 0x0F, 0x83, 0xFE, 0x7F, 0xF7, 0x06, 0xE0, 0x0E, 0x00, 0xE0, 0x0E,
0x00, 0xE0, 0x0F, 0x00, 0x78, 0xE3, 0xFE, 0x1F, 0xC0, 0x1C, 0x0E, 0x07,
0x00, 0x01, 0xE3, 0xFF, 0xFF, 0xC2, 0xC0, 0x60, 0x38, 0x1E, 0x77, 0xF9,
0xF8, 0x18, 0xC0, 0xD8, 0x07, 0x00, 0x00, 0x0F, 0x83, 0xFE, 0x7F, 0xF7,
0x06, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0F, 0x00, 0x78, 0xE3,
0xFE, 0x1F, 0xC0, 0x63, 0x1B, 0x07, 0x00, 0x01, 0xE3, 0xFF, 0xFF, 0xC2,
0xC0, 0x60, 0x38, 0x1E, 0x77, 0xF9, 0xF8, 0x31, 0x80, 0xD8, 0x03, 0x80,
0x00, 0x0F, 0xF0, 0x7F, 0xE3, 0xFF, 0xDC, 0x0E, 0xE0, 0x3F, 0x01, 0xF8,
0x0F, 0xC0, 0x7E, 0x03, 0xF0, 0x3F, 0x87, 0xDF, 0xFC, 0xFF, 0x80, 0x00,
0x18, 0x07, 0x60, 0x1D, 0x80, 0x70, 0x1D, 0xC1, 0xFF, 0x0F, 0xFC, 0x38,
0x70, 0xE1, 0xC3, 0x07, 0x0E, 0x1C, 0x3C, 0xF0, 0x7F, 0xC0, 0xFF, 0x00,
0xFF, 0x07, 0xFE, 0x3F, 0xFD, 0xC0, 0xEE, 0x03, 0xF0, 0x1F, 0xF8, 0xFF,
0xC7, 0xE0, 0x3F, 0x03, 0xF8, 0x7D, 0xFF, 0xCF, 0xF8, 0x00, 0x0F, 0xE1,
0xFC, 0x07, 0x0E, 0xE7, 0xFD, 0xFF, 0xB8, 0x77, 0x0E, 0xC1, 0xDC, 0x3B,
0xCF, 0x3F, 0xE3, 0xFC, 0x3F, 0x8F, 0xE0, 0x03, 0xFF, 0xFF, 0xFF, 0xFE,
0x03, 0x80, 0xFF, 0xBF, 0xEF, 0xFB, 0x80, 0xE0, 0x38, 0x0F, 0xFF, 0xFF,
0x7F, 0x1F, 0xC0, 0x00, 0x78, 0x7F, 0x3C, 0xEE, 0x1F, 0xFF, 0xFF, 0xF8,
0x0F, 0x11, 0xFE, 0x3F, 0x00, 0x31, 0x8C, 0xC1, 0xF0, 0x00, 0xFF, 0xFF,
0xFF, 0xFF, 0x80, 0xE0, 0x3F, 0xEF, 0xFB, 0xFE, 0xE0, 0x38, 0x0E, 0x03,
0xFF, 0xFF, 0xC0, 0x63, 0x19, 0x83, 0xE0, 0x00, 0x1E, 0x1F, 0xCF, 0x3B,
0x87, 0xFF, 0xFF, 0xFE, 0x03, 0xC4, 0x7F, 0x8F, 0xC0, 0x0E, 0x03, 0x80,
0xE0, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0xE0, 0x3F, 0xEF, 0xFB, 0xFE,
0xE0, 0x38, 0x0E, 0x03, 0xFF, 0xFF, 0xC0, 0x1C, 0x07, 0x01, 0xC0, 0x00,
0x1E, 0x1F, 0xCF, 0x3B, 0x87, 0xFF, 0xFF, 0xFE, 0x03, 0xC4, 0x7F, 0x8F,
0xC0, 0xFF, 0xDF, 0xFB, 0xFF, 0x70, 0x0E, 0x01, 0xFF, 0x3F, 0xE7, 0xFC,
0xE0, 0x1C, 0x03, 0x80, 0x7F, 0xEF, 0xFC, 0x03, 0x80, 0x60, 0x0C, 0x01,
0xE0, 0x1E, 0x1F, 0xCF, 0x3B, 0x87, 0xFF, 0xFF, 0xFE, 0x03, 0xC4, 0x7F,
0x8F, 0xC0, 0x60, 0x10, 0x06, 0x01, 0xE0, 0x31, 0x86, 0xC0, 0xE0, 0x00,
0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0xE0, 0x3F, 0xEF, 0xFB, 0xFE, 0xE0, 0x38,
0x0E, 0x03, 0xFF, 0xFF, 0xC0, 0x63, 0x0D, 0x81, 0xC0, 0x00, 0x1E, 0x1F,
0xCF, 0x3B, 0x87, 0xFF, 0xFF, 0xFE, 0x03, 0xC4, 0x7F, 0x8F, 0xC0, 0x07,
0x00, 0xF8, 0x19, 0xC0, 0x00, 0x0F, 0x83, 0xFE, 0x7F, 0xF7, 0x06, 0xE0,
0x0E, 0x00, 0xE0, 0x7E, 0x07, 0xE0, 0x7F, 0x07, 0x78, 0xF3, 0xFF, 0x1F,
0xC0, 0x0E, 0x07, 0xC3, 0x38, 0x00, 0x1C, 0xDF, 0xFF, 0xFF, 0x87, 0xC1,
0xF8, 0x7E, 0x1F, 0xFF, 0x7F, 0xC2, 0x74, 0x1D, 0xFF, 0xFF, 0x87, 0x00,
0x18, 0xC1, 0x98, 0x0F, 0x80, 0x00, 0x0F, 0x83, 0xFE, 0x7F, 0xF7, 0x06,
0xE0, 0x0E, 0x00, 0xE0, 0x7E, 0x07, 0xE0, 0x7F, 0x07, 0x78, 0xF3, 0xFF,
0x1F, 0xC0, 0x31, 0x8C, 0xC1, 0xF0, 0x00, 0x1C, 0xDF, 0xFF, 0xFF, 0x87,
0xC1, 0xF8, 0x7E, 0x1F, 0xFF, 0x7F, 0xC2, 0x74, 0x1D, 0xFF, 0xFF, 0x87,
0x00, 0x07, 0x00, 0x70, 0x07, 0x00, 0x00, 0x0F, 0x83, 0xFE, 0x7F, 0xF7,
0x06, 0xE0, 0x0E, 0x00, 0xE0, 0x7E, 0x07, 0xE0, 0x7F, 0x07, 0x78, 0xF3,
0xFF, 0x1F, 0xC0, 0x0E, 0x03, 0x80, 0xE0, 0x00, 0x1C, 0xDF, 0xFF, 0xFF,
0x87, 0xC1, 0xF8, 0x7E, 0x1F, 0xFF, 0x7F, 0xC2, 0x74, 0x1D, 0xFF, 0xFF,
0x87, 0x00, 0x0F, 0x83, 0xFE, 0x7F, 0xF7, 0x06, 0xE0, 0x0E, 0x00, 0xE0,
0x7E, 0x07, 0xE0, 0x7F, 0x07, 0x78, 0xF3, 0xFF, 0x1F, 0xC0, 0x00, 0x06,
0x00, 0x70, 0x02, 0x00, 0x60, 0x06, 0x01, 0x00, 0xE0, 0x38, 0x00, 0x07,
0x37, 0xFF, 0xFF, 0xE1, 0xF0, 0x7E, 0x1F, 0x87, 0xFF, 0xDF, 0xF0, 0x9D,
0x07, 0x7F, 0xFF, 0xE1, 0xC0, 0x07, 0x00, 0xF8, 0x19, 0xC0, 0x00, 0xE0,
0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0,
0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x70, 0x38, 0x07, 0xC0, 0xCE, 0x00,
0x00, 0x38, 0x03, 0x80, 0x38, 0x03, 0xBC, 0x3F, 0xE3, 0xFF, 0x3C, 0x73,
0x87, 0x38, 0x73, 0x87, 0x38, 0x73, 0x87, 0x38, 0x70, 0x38, 0x38, 0x70,
0x73, 0xFF, 0xFF, 0xFF, 0xF3, 0x83, 0x87, 0x07, 0x0F, 0xFE, 0x1F, 0xFC,
0x38, 0x38, 0x70, 0x70, 0xE0, 0xE1, 0xC1, 0xC3, 0x83, 0x80, 0x30, 0x1F,
0xC0, 0xC0, 0x19, 0xC3, 0xFE, 0x7F, 0xCE, 0x39, 0x83, 0x30, 0x66, 0x0C,
0xC1, 0x98, 0x33, 0x06, 0x75, 0xB8, 0x03, 0x87, 0x0E, 0x1C, 0x38, 0x70,
0xE1, 0xC3, 0x87, 0x0E, 0x1C, 0x38, 0x21, 0xDD, 0xC0, 0x38, 0xE3, 0x8E,
0x38, 0xE3, 0x8E, 0x38, 0xE0, 0xFF, 0xC1, 0xCE, 0x73, 0x9C, 0xE7, 0x39,
0xCE, 0x73, 0x9C, 0xFF, 0xC0, 0xE7, 0x39, 0xCE, 0x73, 0x9C, 0xE7, 0x00,
0xC7, 0x99, 0xF0, 0x07, 0x0E, 0x1C, 0x38, 0x70, 0xE1, 0xC3, 0x87, 0x0E,
0x1C, 0x38, 0x70, 0x8E, 0xFC, 0x07, 0x39, 0xCE, 0x73, 0x9C, 0xE7, 0x38,
0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xE6, 0x66, 0x70, 0xFF, 0x8F, 0xFF,
0xFF, 0xFF, 0xF6, 0xDC, 0x77, 0x70, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE,
0xE0, 0xFF, 0xFF, 0xFF, 0xFC, 0xE0, 0xFC, 0x1F, 0x83, 0xF0, 0x7E, 0x0F,
0xC1, 0xF8, 0x38, 0x07, 0x00, 0xE0, 0x39, 0x8F, 0x7F, 0xC7, 0xF0, 0xE3,
0xF1, 0xF8, 0xE0, 0x0E, 0x77, 0x3B, 0x9D, 0xCE, 0xE7, 0x73, 0xB9, 0xDC,
0xEE, 0x77, 0x38, 0x1C, 0x3E, 0x1E, 0x0E, 0x00, 0x1C, 0x3E, 0x67, 0x00,
0x7F, 0x7F, 0x7F, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0xC7, 0xFF,
0x7E, 0x1C, 0x3C, 0x36, 0x00, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C,
0x1C, 0x1C, 0x1C, 0x1C, 0x7C, 0x78, 0x70, 0xE0, 0x77, 0x07, 0x38, 0x71,
0xC7, 0x0E, 0x70, 0x77, 0x03, 0xF8, 0x1F, 0xE0, 0xFF, 0x87, 0x9E, 0x38,
0x79, 0xC1, 0xCE, 0x07, 0x00, 0x00, 0x30, 0x01, 0xC0, 0x04, 0x00, 0x60,
0x00, 0xE0, 0x1C, 0x03, 0x80, 0x70, 0xEE, 0x39, 0xCE, 0x3B, 0x87, 0xF0,
0xFE, 0x1E, 0xE3, 0x9E, 0x71, 0xEE, 0x1E, 0x00, 0x06, 0x00, 0xE0, 0x08,
0x03, 0x00, 0xE1, 0xDC, 0x73, 0x9C, 0x77, 0x0F, 0xE1, 0xFC, 0x3D, 0xC7,
0x3C, 0xE3, 0xDC, 0x3C, 0x1C, 0x0E, 0x07, 0x00, 0x00, 0xE0, 0x38, 0x0E,
0x03, 0x80, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE0, 0x38, 0x0E, 0x03, 0xFF,
0xFF, 0xC0, 0x3B, 0xB8, 0x0E, 0x73, 0x9C, 0xE7, 0x39, 0xCE, 0x73, 0x9C,
0xE0, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE0,
0x38, 0x0E, 0x03, 0xFF, 0xFF, 0xC0, 0x00, 0xC0, 0x38, 0x04, 0x03, 0x00,
0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x37, 0x58, 0xE3, 0x38, 0xCE, 0x33, 0x80,
0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE0, 0x38, 0x0E, 0x03, 0xFF, 0xFF, 0xC0,
0x0D, 0xDB, 0xB7, 0x0E, 0x1C, 0x38, 0x70, 0xE1, 0xC3, 0x87, 0x0E, 0x1C,
0x00, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE3, 0xB8, 0xEE, 0x3B, 0x80, 0xE0,
0x38, 0x0E, 0x03, 0xFF, 0xFF, 0xC0, 0xE1, 0xC3, 0x87, 0x0E, 0x1D, 0xBB,
0xF6, 0xE1, 0xC3, 0x87, 0x0E, 0x00, 0x70, 0x0E, 0x01, 0xC0, 0x3B, 0x87,
0xE0, 0xF8, 0x1C, 0x07, 0x80, 0x70, 0x0E, 0x01, 0xC0, 0x3F, 0xF7, 0xFE,
0x38, 0x70, 0xE1, 0xC3, 0xC7, 0xCF, 0x3C, 0xF8, 0xF0, 0xE1, 0xC3, 0x80,
0x01, 0xC0, 0x38, 0x07, 0x00, 0x00, 0xE0, 0x7F, 0x07, 0xF8, 0x7F, 0x87,
0xFC, 0x7F, 0xE7, 0xEF, 0x7E, 0x7F, 0xE3, 0xFE, 0x1F, 0xE0, 0xFE, 0x0F,
0xE0, 0x70, 0x03, 0x81, 0xC0, 0xE0, 0x00, 0xEF, 0x3F, 0xEF, 0xFF, 0xC7,
0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xE1, 0xF8, 0x70, 0xE0, 0x7F, 0x07, 0xF8,
0x7F, 0x87, 0xFC, 0x7F, 0xE7, 0xEF, 0x7E, 0x7F, 0xE3, 0xFE, 0x1F, 0xE0,
0xFE, 0x0F, 0xE0, 0x70, 0x00, 0x06, 0x00, 0x70, 0x02, 0x00, 0x60, 0xEF,
0x3F, 0xEF, 0xFF, 0xC7, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xE1, 0xF8, 0x70,
0x00, 0x30, 0x0E, 0x01, 0x00, 0xC0, 0x18, 0xC0, 0xD8, 0x07, 0x00, 0x00,
0xE0, 0x7F, 0x07, 0xF8, 0x7F, 0x87, 0xFC, 0x7F, 0xE7, 0xEF, 0x7E, 0x7F,
0xE3, 0xFE, 0x1F, 0xE0, 0xFE, 0x0F, 0xE0, 0x70, 0x31, 0x86, 0xC0, 0xE0,
0x00, 0xEF, 0x3F, 0xEF, 0xFF, 0xC7, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xE1,
0xF8, 0x70, 0x00, 0x07, 0x00, 0x38, 0x01, 0xC0, 0x0D, 0xDE, 0x6F, 0xFB,
0x7F, 0xE3, 0xC7, 0x1C, 0x38, 0xE1, 0xC7, 0x0E, 0x38, 0x71, 0xC3, 0x8E,
0x1C, 0xE0, 0x7F, 0x07, 0xF8, 0x7F, 0x87, 0xFC, 0x7F, 0xE7, 0xEF, 0x7E,
0x7F, 0xE3, 0xFE, 0x1F, 0xE0, 0xFE, 0x0F, 0xE0, 0x70, 0x07, 0x07, 0xF0,
0xFE, 0x03, 0x80, 0xEF, 0x3F, 0xEF, 0xFF, 0xC7, 0xE1, 0xF8, 0x7E, 0x1F,
0x87, 0xE1, 0xF8, 0x70, 0x1C, 0x1F, 0x07, 0x81, 0xC0, 0x1F, 0xC0, 0xFE,
0x00, 0x00, 0x1F, 0x03, 0xFE, 0x3F, 0xF9, 0xC1, 0xFC, 0x07, 0xE0, 0x3F,
0x00, 0xF8, 0x0F, 0xC0, 0x7F, 0x03, 0xBC, 0x78, 0xFF, 0x83, 0xF8, 0x7F,
0x1F, 0xC0, 0x00, 0x78, 0x7F, 0xBF, 0xEE, 0x1F, 0x07, 0xC1, 0xF8, 0x7F,
0x3D, 0xFE, 0x3F, 0x00, 0x18, 0xC0, 0xCC, 0x03, 0xE0, 0x00, 0x00, 0xF8,
0x1F, 0xF1, 0xFF, 0xCE, 0x0F, 0xE0, 0x3F, 0x01, 0xF8, 0x07, 0xC0, 0x7E,
0x03, 0xF8, 0x1D, 0xE3, 0xC7, 0xFC, 0x1F, 0xC0, 0x63, 0x19, 0x83, 0xE0,
0x00, 0x1E, 0x1F, 0xEF, 0xFB, 0x87, 0xC1, 0xF0, 0x7E, 0x1F, 0xCF, 0x7F,
0x8F, 0xC0, 0x06, 0xE0, 0x76, 0x03, 0x70, 0x00, 0x00, 0xF8, 0x1F, 0xF1,
0xFF, 0xCE, 0x0F, 0xE0, 0x3F, 0x01, 0xF8, 0x07, 0xC0, 0x7E, 0x03, 0xF8,
0x1D, 0xE3, 0xC7, 0xFC, 0x1F, 0xC0, 0x1B, 0x8E, 0xC3, 0x70, 0x00, 0x1E,
0x1F, 0xEF, 0xFB, 0x87, 0xC1, 0xF0, 0x7E, 0x1F, 0xCF, 0x7F, 0x8F, 0xC0,
0x07, 0xFF, 0xC7, 0xFF, 0xF9, 0xFF, 0xFF, 0x38, 0x38, 0x0E, 0x07, 0x01,
0xC0, 0xFF, 0xB8, 0x1F, 0xF7, 0x03, 0xFE, 0xE0, 0x70, 0x1E, 0x0E, 0x01,
0xF1, 0xC0, 0x1F, 0xFF, 0xF0, 0xFF, 0xFE, 0x1E, 0x1C, 0x1F, 0xDF, 0xCF,
0xFE, 0x3B, 0x87, 0x06, 0xC1, 0xFF, 0xF0, 0x7F, 0xFE, 0x1C, 0x03, 0xCF,
0x84, 0x7F, 0xFF, 0x8F, 0x8F, 0xC0, 0x03, 0x80, 0x70, 0x0E, 0x00, 0x00,
0xFE, 0x0F, 0xFC, 0xFF, 0xEE, 0x0E, 0xE0, 0xEE, 0x0E, 0xE0, 0xEF, 0xFE,
0xFF, 0xCE, 0x38, 0xE1, 0xCE, 0x1E, 0xE0, 0xE0, 0x0E, 0x38, 0xE0, 0x0E,
0xDF, 0xBF, 0x78, 0xE1, 0xC3, 0x87, 0x0E, 0x1C, 0x00, 0xFE, 0x0F, 0xFC,
0xFF, 0xEE, 0x0E, 0xE0, 0xEE, 0x0E, 0xE0, 0xEF, 0xFE, 0xFF, 0xCE, 0x38,
0xE1, 0xCE, 0x1E, 0xE0, 0xE0, 0x00, 0x0C, 0x00, 0xE0, 0x04, 0x00, 0xC0,
0xEF, 0xFF, 0xFC, 0xE3, 0x8E, 0x38, 0xE3, 0x80, 0x30, 0xE1, 0x0C, 0x00,
0x31, 0x81, 0xB0, 0x0E, 0x00, 0x00, 0xFE, 0x0F, 0xFC, 0xFF, 0xEE, 0x0E,
0xE0, 0xEE, 0x0E, 0xE0, 0xEF, 0xFE, 0xFF, 0xCE, 0x38, 0xE1, 0xCE, 0x1E,
0xE0, 0xE0, 0xC6, 0xD8, 0xE0, 0x0E, 0xDF, 0xBF, 0x78, 0xE1, 0xC3, 0x87,
0x0E, 0x1C, 0x00, 0x03, 0x80, 0xE0, 0x38, 0x00, 0x00, 0xF8, 0x7F, 0x9F,
0xF3, 0x80, 0x70, 0x0F, 0x80, 0xFE, 0x07, 0xE0, 0x1E, 0x01, 0xD8, 0x3B,
0xFE, 0x3F, 0x80, 0x07, 0x07, 0x07, 0x00, 0x01, 0xF3, 0xFD, 0xC4, 0xC0,
0x7E, 0x1F, 0xC0, 0xE8, 0x77, 0xFB, 0xF8, 0x0E, 0x03, 0xE0, 0xCE, 0x00,
0x00, 0xF8, 0x7F, 0x9F, 0xF3, 0x80, 0x70, 0x0F, 0x80, 0xFE, 0x07, 0xE0,
0x1E, 0x01, 0xD8, 0x3B, 0xFE, 0x3F, 0x80, 0x1C, 0x1F, 0x19, 0xC0, 0x01,
0xF3, 0xFD, 0xC4, 0xC0, 0x7E, 0x1F, 0xC0, 0xE8, 0x77, 0xFB, 0xF8, 0x0F,
0x87, 0xF9, 0xFF, 0x38, 0x07, 0x00, 0xF8, 0x0F, 0xE0, 0x7E, 0x01, 0xE0,
0x1D, 0x83, 0xBF, 0xE3, 0xF8, 0x18, 0x03, 0x80, 0x10, 0x1E, 0x00, 0x1F,
0x3F, 0xDC, 0x4C, 0x07, 0xE1, 0xFC, 0x0E, 0x87, 0x7F, 0xBF, 0x86, 0x03,
0x80, 0x41, 0xE0, 0x31, 0x83, 0x60, 0x38, 0x00, 0x00, 0xF8, 0x7F, 0x9F,
0xF3, 0x80, 0x70, 0x0F, 0x80, 0xFE, 0x07, 0xE0, 0x1E, 0x01, 0xD8, 0x3B,
0xFE, 0x3F, 0x80, 0x63, 0x1B, 0x07, 0x00, 0x01, 0xF3, 0xFD, 0xC4, 0xC0,
0x7E, 0x1F, 0xC0, 0xE8, 0x77, 0xFB, 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0x87,
0x00, 0xE0, 0x1C, 0x03, 0x80, 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0x00,
0xE0, 0x18, 0x03, 0x80, 0x10, 0x1E, 0x00, 0x30, 0x30, 0xFE, 0xFE, 0x30,
0x30, 0x30, 0x30, 0x30, 0x38, 0x3E, 0x1F, 0x18, 0x1C, 0x04, 0x3C, 0x31,
0x83, 0x60, 0x38, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xF8, 0x70, 0x0E, 0x01,
0xC0, 0x38, 0x07, 0x00, 0xE0, 0x1C, 0x03, 0x80, 0x70, 0x0E, 0x00, 0x06,
0x06, 0x36, 0x30, 0xFE, 0xFE, 0x30, 0x30, 0x30, 0x30, 0x30, 0x38, 0x3E,
0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0x87, 0x00, 0xE0, 0x1C, 0x0F, 0xE1, 0xFC,
0x0E, 0x01, 0xC0, 0x38, 0x07, 0x00, 0xE0, 0x38, 0x38, 0x38, 0xFE, 0xFE,
0x38, 0xFC, 0xFC, 0x38, 0x38, 0x3E, 0x1F, 0x1E, 0xC1, 0xBC, 0x00, 0x0E,
0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E,
0x07, 0x70, 0xF7, 0x9E, 0x3F, 0xE1, 0xF8, 0x18, 0x0F, 0x62, 0x70, 0x00,
0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xE1, 0xF8, 0x7E, 0x1F, 0xCF, 0x7F, 0xCF,
0xF0, 0x1F, 0xC1, 0xFC, 0x00, 0x0E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E,
0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0x70, 0xF7, 0x9E, 0x3F, 0xE1,
0xF8, 0x3F, 0x8F, 0xE0, 0x03, 0x87, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xE1,
0xF8, 0x7F, 0x3D, 0xFF, 0x3F, 0xC0, 0x18, 0xC1, 0x98, 0x0F, 0x80, 0x00,
0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07,
0xE0, 0x77, 0x0F, 0x79, 0xE3, 0xFE, 0x1F, 0x80, 0x31, 0x8C, 0xC1, 0xF0,
0x00, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xE1, 0xF8, 0x7E, 0x1F, 0xCF, 0x7F,
0xCF, 0xF0, 0x07, 0x00, 0xD8, 0x09, 0x80, 0x70, 0x00, 0x0E, 0x07, 0xE0,
0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0x70,
0xF7, 0x9E, 0x3F, 0xE1, 0xF8, 0x0E, 0x06, 0xC1, 0x30, 0x38, 0x00, 0x38,
0x7E, 0x1F, 0x87, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xF3, 0xDF, 0xF3, 0xFC,
0x06, 0xE0, 0xEC, 0x0D, 0xC0, 0x00, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07,
0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x77, 0x0F, 0x79, 0xE3, 0xFE,
0x1F, 0x80, 0x0D, 0xC7, 0x61, 0xB8, 0x00, 0xE1, 0xF8, 0x7E, 0x1F, 0x87,
0xE1, 0xF8, 0x7E, 0x1F, 0xCF, 0x7F, 0xCF, 0xF0, 0xE0, 0x7E, 0x07, 0xE0,
0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x77, 0x0F, 0x79,
0xE3, 0xFC, 0x1F, 0x80, 0x60, 0x0C, 0x00, 0xC0, 0x07, 0x00, 0xE1, 0xF8,
0x7E, 0x1F, 0x87, 0xE1, 0xF8, 0x7E, 0x1F, 0xCF, 0x7F, 0xCF, 0xF0, 0x38,
0x0C, 0x03, 0x00, 0xF0, 0x00, 0x70, 0x00, 0x07, 0xC0, 0x00, 0x67, 0x00,
0x00, 0x00, 0x0E, 0x07, 0x03, 0xB8, 0x38, 0x39, 0xC1, 0xE1, 0xCE, 0x1F,
0x0E, 0x38, 0xD8, 0xE1, 0xC6, 0xE7, 0x0E, 0x77, 0x38, 0x3B, 0x3B, 0x81,
0xD8, 0xFC, 0x0F, 0xC7, 0xE0, 0x3C, 0x3E, 0x01, 0xE0, 0xF0, 0x0F, 0x07,
0x80, 0x01, 0xC0, 0x01, 0xF0, 0x01, 0x9C, 0x00, 0x00, 0x0E, 0x1C, 0x3F,
0x0E, 0x19, 0x87, 0x0C, 0xE7, 0xCE, 0x33, 0x66, 0x1B, 0xBB, 0x0F, 0xDF,
0x83, 0xC7, 0x81, 0xE3, 0xC0, 0xE1, 0xE0, 0x07, 0x00, 0xF8, 0x19, 0xC0,
0x00, 0xE0, 0x3F, 0x07, 0x70, 0xE3, 0x8E, 0x39, 0xC1, 0xD8, 0x1F, 0x80,
0xF0, 0x07, 0x00, 0x70, 0x07, 0x00, 0x70, 0x07, 0x00, 0x0E, 0x03, 0xE0,
0xCE, 0x00, 0x0E, 0x0F, 0xC1, 0x9C, 0x73, 0x8E, 0x33, 0x87, 0x70, 0x6C,
0x0F, 0x81, 0xE0, 0x1C, 0x03, 0x87, 0xE0, 0xF8, 0x0E, 0x00, 0x00, 0x01,
0xD8, 0x0D, 0x80, 0x00, 0xE0, 0x3F, 0x07, 0x70, 0xE3, 0x8E, 0x39, 0xC1,
0xD8, 0x1F, 0x80, 0xF0, 0x07, 0x00, 0x70, 0x07, 0x00, 0x70, 0x07, 0x00,
0x03, 0x80, 0xE0, 0x38, 0x00, 0x0F, 0xFD, 0xFF, 0xBF, 0xF0, 0x1C, 0x07,
0x81, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE0, 0x3C, 0x07, 0xFF, 0xFF, 0xE0,
0x07, 0x0E, 0x1C, 0x00, 0xFF, 0xFF, 0x07, 0x0E, 0x1C, 0x38, 0x70, 0xE0,
0xFF, 0xFF, 0x0E, 0x01, 0xC0, 0x38, 0x00, 0x0F, 0xFD, 0xFF, 0xBF, 0xF0,
0x1C, 0x07, 0x81, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE0, 0x3C, 0x07, 0xFF,
0xFF, 0xE0, 0x1C, 0x1C, 0x1C, 0x00, 0xFF, 0xFF, 0x07, 0x0E, 0x1C, 0x38,
0x70, 0xE0, 0xFF, 0xFF, 0x31, 0x83, 0x60, 0x38, 0x00, 0x0F, 0xFD, 0xFF,
0xBF, 0xF0, 0x1C, 0x07, 0x81, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE0, 0x3C,
0x07, 0xFF, 0xFF, 0xE0, 0x63, 0x36, 0x1C, 0x00, 0xFF, 0xFF, 0x07, 0x0E,
0x1C, 0x38, 0x70, 0xE0, 0xFF, 0xFF, 0x00, 0x1F, 0x3E, 0x38, 0x38, 0xF8,
0xF8, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38 };
const GFXglyph MontserratBold9pt8bGlyphs[] PROGMEM = {
{ 0, 1, 1, 5, 0, 0 }, // 0x20 ' '
{ 1, 3, 13, 5, 1, -12 }, // 0x21 '!'
{ 6, 6, 6, 8, 1, -12 }, // 0x22 '"'
{ 11, 13, 13, 13, 0, -12 }, // 0x23 '#'
{ 33, 11, 17, 11, 0, -14 }, // 0x24 '$'
{ 57, 15, 13, 16, 0, -12 }, // 0x25 '%'
{ 82, 12, 13, 13, 1, -12 }, // 0x26 '&'
{ 102, 2, 6, 4, 1, -12 }, // 0x27 '''
{ 104, 5, 16, 6, 1, -12 }, // 0x28 '('
{ 114, 5, 16, 6, 0, -12 }, // 0x29 ')'
{ 124, 8, 7, 8, 0, -12 }, // 0x2A '*'
{ 131, 9, 8, 11, 1, -9 }, // 0x2B '+'
{ 140, 3, 6, 5, 1, -2 }, // 0x2C ','
{ 143, 5, 2, 7, 1, -5 }, // 0x2D '-'
{ 145, 3, 3, 5, 1, -2 }, // 0x2E '.'
{ 147, 9, 17, 7, -1, -14 }, // 0x2F '/'
{ 167, 11, 13, 12, 1, -12 }, // 0x30 '0'
{ 185, 6, 13, 7, 0, -12 }, // 0x31 '1'
{ 195, 10, 13, 11, 0, -12 }, // 0x32 '2'
{ 212, 10, 13, 11, 0, -12 }, // 0x33 '3'
{ 229, 11, 13, 12, 1, -12 }, // 0x34 '4'
{ 247, 10, 13, 11, 0, -12 }, // 0x35 '5'
{ 264, 10, 13, 11, 1, -12 }, // 0x36 '6'
{ 281, 11, 13, 11, 0, -12 }, // 0x37 '7'
{ 299, 10, 13, 12, 1, -12 }, // 0x38 '8'
{ 316, 11, 13, 11, 0, -12 }, // 0x39 '9'
{ 334, 3, 9, 5, 1, -8 }, // 0x3A ':'
{ 338, 3, 12, 5, 1, -8 }, // 0x3B ';'
{ 343, 9, 9, 11, 1, -10 }, // 0x3C '<'
{ 354, 9, 7, 11, 1, -9 }, // 0x3D '='
{ 362, 9, 9, 11, 1, -10 }, // 0x3E '>'
{ 373, 10, 13, 11, 0, -12 }, // 0x3F '?'
{ 390, 17, 17, 19, 1, -12 }, // 0x40 '@'
{ 427, 14, 13, 14, 0, -12 }, // 0x41 'A'
{ 450, 12, 13, 14, 1, -12 }, // 0x42 'B'
{ 470, 12, 13, 13, 1, -12 }, // 0x43 'C'
{ 490, 13, 13, 15, 1, -12 }, // 0x44 'D'
{ 512, 10, 13, 12, 1, -12 }, // 0x45 'E'
{ 529, 10, 13, 12, 1, -12 }, // 0x46 'F'
{ 546, 12, 13, 14, 1, -12 }, // 0x47 'G'
{ 566, 12, 13, 15, 1, -12 }, // 0x48 'H'
{ 586, 3, 13, 6, 1, -12 }, // 0x49 'I'
{ 591, 8, 13, 10, 0, -12 }, // 0x4A 'J'
{ 604, 13, 13, 13, 1, -12 }, // 0x4B 'K'
{ 626, 10, 13, 11, 1, -12 }, // 0x4C 'L'
{ 643, 15, 13, 17, 1, -12 }, // 0x4D 'M'
{ 668, 12, 13, 15, 1, -12 }, // 0x4E 'N'
{ 688, 13, 13, 15, 1, -12 }, // 0x4F 'O'
{ 710, 11, 13, 13, 1, -12 }, // 0x50 'P'
{ 728, 14, 16, 15, 1, -12 }, // 0x51 'Q'
{ 756, 12, 13, 13, 1, -12 }, // 0x52 'R'
{ 776, 11, 13, 11, 0, -12 }, // 0x53 'S'
{ 794, 11, 13, 11, 0, -12 }, // 0x54 'T'
{ 812, 12, 13, 14, 1, -12 }, // 0x55 'U'
{ 832, 14, 13, 13, 0, -12 }, // 0x56 'V'
{ 855, 21, 13, 21, 0, -12 }, // 0x57 'W'
{ 890, 13, 13, 13, 0, -12 }, // 0x58 'X'
{ 912, 12, 13, 12, 0, -12 }, // 0x59 'Y'
{ 932, 11, 13, 12, 1, -12 }, // 0x5A 'Z'
{ 950, 5, 16, 7, 1, -12 }, // 0x5B '['
{ 960, 9, 17, 7, -1, -14 }, // 0x5C '\'
{ 980, 5, 16, 7, 0, -12 }, // 0x5D ']'
{ 990, 9, 7, 11, 1, -9 }, // 0x5E '^'
{ 998, 9, 2, 9, 0, 1 }, // 0x5F '_'
{ 1001, 5, 3, 11, 2, -13 }, // 0x60 '`'
{ 1003, 9, 10, 11, 1, -9 }, // 0x61 'a'
{ 1015, 11, 13, 12, 1, -12 }, // 0x62 'b'
{ 1033, 9, 10, 11, 1, -9 }, // 0x63 'c'
{ 1045, 10, 13, 12, 1, -12 }, // 0x64 'd'
{ 1062, 10, 10, 11, 1, -9 }, // 0x65 'e'
{ 1075, 8, 14, 7, 0, -13 }, // 0x66 'f'
{ 1089, 10, 14, 13, 1, -9 }, // 0x67 'g'
{ 1107, 10, 13, 12, 1, -12 }, // 0x68 'h'
{ 1124, 3, 14, 5, 1, -13 }, // 0x69 'i'
{ 1130, 6, 18, 6, -2, -13 }, // 0x6A 'j'
{ 1144, 11, 13, 12, 1, -12 }, // 0x6B 'k'
{ 1162, 3, 13, 5, 1, -12 }, // 0x6C 'l'
{ 1167, 17, 10, 19, 1, -9 }, // 0x6D 'm'
{ 1189, 10, 10, 12, 1, -9 }, // 0x6E 'n'
{ 1202, 10, 10, 12, 1, -9 }, // 0x6F 'o'
{ 1215, 11, 13, 12, 1, -9 }, // 0x70 'p'
{ 1233, 10, 13, 12, 1, -9 }, // 0x71 'q'
{ 1250, 6, 10, 8, 1, -9 }, // 0x72 'r'
{ 1258, 9, 10, 10, 0, -9 }, // 0x73 's'
{ 1270, 8, 12, 8, 0, -11 }, // 0x74 't'
{ 1282, 10, 10, 12, 1, -9 }, // 0x75 'u'
{ 1295, 11, 10, 11, 0, -9 }, // 0x76 'v'
{ 1309, 17, 10, 17, 0, -9 }, // 0x77 'w'
{ 1331, 11, 10, 11, 0, -9 }, // 0x78 'x'
{ 1345, 11, 14, 11, 0, -9 }, // 0x79 'y'
{ 1365, 8, 10, 10, 1, -9 }, // 0x7A 'z'
{ 1375, 6, 16, 7, 1, -12 }, // 0x7B '{'
{ 1387, 3, 16, 6, 1, -12 }, // 0x7C '|'
{ 1393, 6, 16, 7, 0, -12 }, // 0x7D '}'
{ 1405, 9, 3, 11, 1, -7 }, // 0x7E '~'
{ 1409, 9, 13, 11, 1, -12 }, // 0x7F
{ 1424, 9, 13, 11, 1, -12 }, // 0x80
{ 1439, 9, 13, 11, 1, -12 }, // 0x81
{ 1454, 9, 13, 11, 1, -12 }, // 0x82
{ 1469, 9, 13, 11, 1, -12 }, // 0x83
{ 1484, 9, 13, 11, 1, -12 }, // 0x84
{ 1499, 9, 13, 11, 1, -12 }, // 0x85
{ 1514, 9, 13, 11, 1, -12 }, // 0x86
{ 1529, 9, 13, 11, 1, -12 }, // 0x87
{ 1544, 9, 13, 11, 1, -12 }, // 0x88
{ 1559, 9, 13, 11, 1, -12 }, // 0x89
{ 1574, 9, 13, 11, 1, -12 }, // 0x8A
{ 1589, 9, 13, 11, 1, -12 }, // 0x8B
{ 1604, 9, 13, 11, 1, -12 }, // 0x8C
{ 1619, 9, 13, 11, 1, -12 }, // 0x8D
{ 1634, 9, 13, 11, 1, -12 }, // 0x8E
{ 1649, 9, 13, 11, 1, -12 }, // 0x8F
{ 1664, 9, 13, 11, 1, -12 }, // 0x90
{ 1679, 9, 13, 11, 1, -12 }, // 0x91
{ 1694, 9, 13, 11, 1, -12 }, // 0x92
{ 1709, 9, 13, 11, 1, -12 }, // 0x93
{ 1724, 9, 13, 11, 1, -12 }, // 0x94
{ 1739, 9, 13, 11, 1, -12 }, // 0x95
{ 1754, 9, 13, 11, 1, -12 }, // 0x96
{ 1769, 9, 13, 11, 1, -12 }, // 0x97
{ 1784, 9, 13, 11, 1, -12 }, // 0x98
{ 1799, 9, 13, 11, 1, -12 }, // 0x99
{ 1814, 9, 13, 11, 1, -12 }, // 0x9A
{ 1829, 9, 13, 11, 1, -12 }, // 0x9B
{ 1844, 9, 13, 11, 1, -12 }, // 0x9C
{ 1859, 9, 13, 11, 1, -12 }, // 0x9D
{ 1874, 9, 13, 11, 1, -12 }, // 0x9E
{ 1889, 9, 13, 11, 1, -12 }, // 0x9F
{ 1904, 1, 1, 5, 0, 0 }, // 0xA0
{ 1905, 3, 13, 5, 1, -9 }, // 0xA1
{ 1910, 9, 14, 11, 1, -11 }, // 0xA2
{ 1926, 11, 13, 12, 1, -12 }, // 0xA3
{ 1944, 12, 12, 13, 0, -11 }, // 0xA4
{ 1962, 14, 13, 13, 0, -12 }, // 0xA5
{ 1985, 3, 16, 6, 1, -12 }, // 0xA6
{ 1991, 9, 15, 9, 0, -12 }, // 0xA7
{ 2008, 6, 3, 11, 2, -13 }, // 0xA8
{ 2011, 12, 13, 14, 1, -12 }, // 0xA9
{ 2031, 7, 6, 7, 0, -13 }, // 0xAA
{ 2037, 9, 7, 10, 1, -7 }, // 0xAB
{ 2045, 9, 5, 11, 1, -6 }, // 0xAC
{ 2051, 9, 13, 11, 1, -12 }, // 0xAD
{ 2066, 12, 13, 14, 1, -12 }, // 0xAE
{ 2086, 7, 2, 11, 2, -12 }, // 0xAF
{ 2088, 6, 6, 8, 1, -12 }, // 0xB0
{ 2093, 9, 12, 11, 1, -11 }, // 0xB1
{ 2107, 7, 8, 8, 0, -13 }, // 0xB2
{ 2114, 7, 7, 8, 0, -12 }, // 0xB3
{ 2121, 5, 3, 11, 4, -13 }, // 0xB4
{ 2123, 10, 13, 12, 1, -9 }, // 0xB5
{ 2140, 11, 15, 12, 0, -12 }, // 0xB6
{ 2161, 3, 4, 5, 1, -6 }, // 0xB7
{ 2163, 4, 4, 11, 3, 1 }, // 0xB8
{ 2165, 6, 7, 8, 1, -12 }, // 0xB9
{ 2171, 7, 6, 8, 0, -13 }, // 0xBA
{ 2177, 10, 7, 10, 0, -7 }, // 0xBB
{ 2186, 17, 13, 19, 1, -12 }, // 0xBC
{ 2214, 17, 13, 19, 1, -12 }, // 0xBD
{ 2242, 18, 13, 19, 0, -12 }, // 0xBE
{ 2272, 10, 13, 11, 1, -9 }, // 0xBF
{ 2289, 14, 17, 14, 0, -16 }, // 0xC0
{ 2319, 14, 17, 14, 0, -16 }, // 0xC1
{ 2349, 14, 17, 14, 0, -16 }, // 0xC2
{ 2379, 14, 16, 14, 0, -15 }, // 0xC3
{ 2407, 14, 17, 14, 0, -16 }, // 0xC4
{ 2437, 14, 18, 14, 0, -17 }, // 0xC5
{ 2469, 19, 13, 19, 0, -12 }, // 0xC6
{ 2500, 12, 17, 13, 1, -12 }, // 0xC7
{ 2526, 10, 17, 12, 1, -16 }, // 0xC8
{ 2548, 10, 17, 12, 1, -16 }, // 0xC9
{ 2570, 10, 17, 12, 1, -16 }, // 0xCA
{ 2592, 10, 17, 12, 1, -16 }, // 0xCB
{ 2614, 5, 17, 6, 0, -16 }, // 0xCC
{ 2625, 6, 17, 6, 1, -16 }, // 0xCD
{ 2638, 7, 17, 6, 0, -16 }, // 0xCE
{ 2653, 6, 17, 6, 0, -16 }, // 0xCF
{ 2666, 13, 13, 15, 1, -12 }, // 0xD0
{ 2688, 12, 16, 15, 1, -15 }, // 0xD1
{ 2712, 13, 17, 15, 1, -16 }, // 0xD2
{ 2740, 13, 17, 15, 1, -16 }, // 0xD3
{ 2768, 13, 17, 15, 1, -16 }, // 0xD4
{ 2796, 13, 16, 15, 1, -15 }, // 0xD5
{ 2822, 13, 17, 15, 1, -16 }, // 0xD6
{ 2850, 7, 8, 11, 2, -9 }, // 0xD7
{ 2857, 13, 15, 15, 1, -13 }, // 0xD8
{ 2882, 12, 17, 14, 1, -16 }, // 0xD9
{ 2908, 12, 17, 14, 1, -16 }, // 0xDA
{ 2934, 12, 17, 14, 1, -16 }, // 0xDB
{ 2960, 12, 17, 14, 1, -16 }, // 0xDC
{ 2986, 12, 17, 12, 0, -16 }, // 0xDD
{ 3012, 11, 13, 13, 1, -12 }, // 0xDE
{ 3030, 11, 14, 12, 1, -13 }, // 0xDF
{ 3050, 9, 14, 11, 1, -13 }, // 0xE0
{ 3066, 9, 14, 11, 1, -13 }, // 0xE1
{ 3082, 9, 14, 11, 1, -13 }, // 0xE2
{ 3098, 9, 14, 11, 1, -13 }, // 0xE3
{ 3114, 9, 14, 11, 1, -13 }, // 0xE4
{ 3130, 9, 15, 11, 1, -14 }, // 0xE5
{ 3147, 16, 10, 18, 1, -9 }, // 0xE6
{ 3167, 9, 14, 11, 1, -9 }, // 0xE7
{ 3183, 10, 14, 11, 1, -13 }, // 0xE8
{ 3201, 10, 14, 11, 1, -13 }, // 0xE9
{ 3219, 10, 14, 11, 1, -13 }, // 0xEA
{ 3237, 10, 14, 11, 1, -13 }, // 0xEB
{ 3255, 5, 14, 5, -1, -13 }, // 0xEC
{ 3264, 5, 14, 5, 1, -13 }, // 0xED
{ 3273, 7, 14, 5, -1, -13 }, // 0xEE
{ 3286, 5, 13, 5, 0, -12 }, // 0xEF
{ 3295, 10, 13, 11, 0, -12 }, // 0xF0
{ 3312, 10, 14, 12, 1, -13 }, // 0xF1
{ 3330, 10, 14, 12, 1, -13 }, // 0xF2
{ 3348, 10, 14, 12, 1, -13 }, // 0xF3
{ 3366, 10, 14, 12, 1, -13 }, // 0xF4
{ 3384, 10, 14, 12, 1, -13 }, // 0xF5
{ 3402, 10, 14, 12, 1, -13 }, // 0xF6
{ 3420, 9, 11, 11, 1, -11 }, // 0xF7
{ 3433, 10, 12, 12, 1, -10 }, // 0xF8
{ 3448, 10, 14, 12, 1, -13 }, // 0xF9
{ 3466, 10, 14, 12, 1, -13 }, // 0xFA
{ 3484, 10, 14, 12, 1, -13 }, // 0xFB
{ 3502, 10, 14, 12, 1, -13 }, // 0xFC
{ 3520, 11, 18, 11, 0, -13 }, // 0xFD
{ 3545, 11, 16, 12, 1, -12 }, // 0xFE
{ 3567, 11, 18, 11, 0, -13 }, // 0xFF
{ 3592, 14, 16, 14, 0, -15 }, // 0x100
{ 3620, 9, 13, 11, 1, -12 }, // 0x101
{ 3635, 14, 17, 14, 0, -16 }, // 0x102
{ 3665, 9, 14, 11, 1, -13 }, // 0x103
{ 3681, 14, 17, 14, 0, -12 }, // 0x104
{ 3711, 9, 14, 11, 1, -9 }, // 0x105
{ 3727, 12, 17, 13, 1, -16 }, // 0x106
{ 3753, 9, 14, 11, 1, -13 }, // 0x107
{ 3769, 12, 17, 13, 1, -16 }, // 0x108
{ 3795, 9, 14, 11, 1, -13 }, // 0x109
{ 3811, 12, 17, 13, 1, -16 }, // 0x10A
{ 3837, 9, 14, 11, 1, -13 }, // 0x10B
{ 3853, 12, 17, 13, 1, -16 }, // 0x10C
{ 3879, 9, 14, 11, 1, -13 }, // 0x10D
{ 3895, 13, 17, 15, 1, -16 }, // 0x10E
{ 3923, 14, 14, 12, 1, -13 }, // 0x10F
{ 3948, 13, 13, 15, 1, -12 }, // 0x110
{ 3970, 11, 13, 12, 1, -12 }, // 0x111
{ 3988, 10, 16, 12, 1, -15 }, // 0x112
{ 4008, 10, 13, 11, 1, -12 }, // 0x113
{ 4025, 10, 17, 12, 1, -16 }, // 0x114
{ 4047, 10, 14, 11, 1, -13 }, // 0x115
{ 4065, 10, 17, 12, 1, -16 }, // 0x116
{ 4087, 10, 14, 11, 1, -13 }, // 0x117
{ 4105, 11, 17, 12, 1, -12 }, // 0x118
{ 4129, 10, 14, 11, 1, -9 }, // 0x119
{ 4147, 10, 17, 12, 1, -16 }, // 0x11A
{ 4169, 10, 14, 11, 1, -13 }, // 0x11B
{ 4187, 12, 17, 14, 1, -16 }, // 0x11C
{ 4213, 10, 18, 13, 1, -13 }, // 0x11D
{ 4236, 12, 17, 14, 1, -16 }, // 0x11E
{ 4262, 10, 18, 13, 1, -13 }, // 0x11F
{ 4285, 12, 17, 14, 1, -16 }, // 0x120
{ 4311, 10, 18, 13, 1, -13 }, // 0x121
{ 4334, 12, 18, 14, 1, -12 }, // 0x122
{ 4361, 10, 19, 13, 1, -14 }, // 0x123
{ 4385, 12, 17, 15, 1, -16 }, // 0x124
{ 4411, 12, 17, 12, -1, -16 }, // 0x125
{ 4437, 15, 13, 15, 0, -12 }, // 0x126
{ 4462, 11, 13, 13, 0, -12 }, // 0x127
{ 4480, 7, 16, 6, 0, -15 }, // 0x128
{ 4494, 6, 14, 5, -1, -13 }, // 0x129
{ 4505, 5, 16, 6, 1, -15 }, // 0x12A
{ 4515, 5, 13, 5, 0, -12 }, // 0x12B
{ 4524, 7, 17, 6, 0, -16 }, // 0x12C
{ 4539, 5, 14, 5, 0, -13 }, // 0x12D
{ 4548, 4, 17, 6, 1, -12 }, // 0x12E
{ 4557, 3, 18, 5, 1, -13 }, // 0x12F
{ 4564, 4, 17, 6, 1, -16 }, // 0x130
{ 4573, 3, 10, 5, 1, -9 }, // 0x131
{ 4577, 11, 13, 13, 1, -12 }, // 0x132
{ 4595, 9, 18, 11, 1, -13 }, // 0x133
{ 4616, 8, 17, 10, 0, -16 }, // 0x134
{ 4633, 8, 18, 6, -2, -13 }, // 0x135
{ 4651, 13, 18, 13, 1, -12 }, // 0x136
{ 4681, 11, 18, 12, 1, -12 }, // 0x137
{ 4706, 11, 10, 12, 1, -9 }, // 0x138
{ 4720, 10, 17, 11, 1, -16 }, // 0x139
{ 4742, 5, 17, 5, 1, -16 }, // 0x13A
{ 4753, 10, 18, 11, 1, -12 }, // 0x13B
{ 4776, 3, 18, 5, 1, -12 }, // 0x13C
{ 4783, 10, 13, 11, 1, -12 }, // 0x13D
{ 4800, 7, 14, 5, 1, -13 }, // 0x13E
{ 4813, 10, 13, 11, 1, -12 }, // 0x13F
{ 4830, 7, 13, 8, 1, -12 }, // 0x140
{ 4842, 11, 13, 11, 0, -12 }, // 0x141
{ 4860, 7, 13, 5, -1, -12 }, // 0x142
{ 4872, 12, 17, 15, 1, -16 }, // 0x143
{ 4898, 10, 14, 12, 1, -13 }, // 0x144
{ 4916, 12, 18, 15, 1, -12 }, // 0x145
{ 4943, 10, 15, 12, 1, -9 }, // 0x146
{ 4962, 12, 17, 15, 1, -16 }, // 0x147
{ 4988, 10, 14, 12, 1, -13 }, // 0x148
{ 5006, 13, 14, 14, 0, -13 }, // 0x149
{ 5029, 12, 17, 15, 1, -12 }, // 0x14A
{ 5055, 10, 14, 12, 1, -9 }, // 0x14B
{ 5073, 13, 16, 15, 1, -15 }, // 0x14C
{ 5099, 10, 13, 12, 1, -12 }, // 0x14D
{ 5116, 13, 17, 15, 1, -16 }, // 0x14E
{ 5144, 10, 14, 12, 1, -13 }, // 0x14F
{ 5162, 13, 17, 15, 1, -16 }, // 0x150
{ 5190, 10, 14, 12, 1, -13 }, // 0x151
{ 5208, 19, 13, 20, 1, -12 }, // 0x152
{ 5239, 18, 10, 19, 1, -9 }, // 0x153
{ 5262, 12, 17, 13, 1, -16 }, // 0x154
{ 5288, 7, 14, 8, 1, -13 }, // 0x155
{ 5301, 12, 18, 13, 1, -12 }, // 0x156
{ 5328, 6, 15, 8, 1, -9 }, // 0x157
{ 5340, 12, 17, 13, 1, -16 }, // 0x158
{ 5366, 7, 14, 8, 1, -13 }, // 0x159
{ 5379, 11, 17, 11, 0, -16 }, // 0x15A
{ 5403, 9, 14, 10, 0, -13 }, // 0x15B
{ 5419, 11, 17, 11, 0, -16 }, // 0x15C
{ 5443, 9, 14, 10, 0, -13 }, // 0x15D
{ 5459, 11, 17, 11, 0, -12 }, // 0x15E
{ 5483, 9, 14, 10, 0, -9 }, // 0x15F
{ 5499, 11, 17, 11, 0, -16 }, // 0x160
{ 5523, 9, 14, 10, 0, -13 }, // 0x161
{ 5539, 11, 17, 11, 0, -12 }, // 0x162
{ 5563, 8, 16, 8, 0, -11 }, // 0x163
{ 5579, 11, 17, 11, 0, -16 }, // 0x164
{ 5603, 8, 14, 8, 0, -13 }, // 0x165
{ 5617, 11, 13, 11, 0, -12 }, // 0x166
{ 5635, 8, 12, 8, 0, -11 }, // 0x167
{ 5647, 12, 16, 14, 1, -15 }, // 0x168
{ 5671, 10, 14, 12, 1, -13 }, // 0x169
{ 5689, 12, 16, 14, 1, -15 }, // 0x16A
{ 5713, 10, 13, 12, 1, -12 }, // 0x16B
{ 5730, 12, 17, 14, 1, -16 }, // 0x16C
{ 5756, 10, 14, 12, 1, -13 }, // 0x16D
{ 5774, 12, 18, 14, 1, -17 }, // 0x16E
{ 5801, 10, 15, 12, 1, -14 }, // 0x16F
{ 5820, 12, 17, 14, 1, -16 }, // 0x170
{ 5846, 10, 14, 12, 1, -13 }, // 0x171
{ 5864, 12, 17, 14, 1, -12 }, // 0x172
{ 5890, 10, 14, 12, 1, -9 }, // 0x173
{ 5908, 21, 17, 21, 0, -16 }, // 0x174
{ 5953, 17, 14, 17, 0, -13 }, // 0x175
{ 5983, 12, 17, 12, 0, -16 }, // 0x176
{ 6009, 11, 18, 11, 0, -13 }, // 0x177
{ 6034, 12, 17, 12, 0, -16 }, // 0x178
{ 6060, 11, 17, 12, 1, -16 }, // 0x179
{ 6084, 8, 14, 10, 1, -13 }, // 0x17A
{ 6098, 11, 17, 12, 1, -16 }, // 0x17B
{ 6122, 8, 14, 10, 1, -13 }, // 0x17C
{ 6136, 11, 17, 12, 1, -16 }, // 0x17D
{ 6160, 8, 14, 10, 1, -13 }, // 0x17E
{ 6174, 8, 14, 6, 0, -13 } }; // 0x17F
const GFXfont MontserratBold9pt8b PROGMEM = {
(uint8_t *)MontserratBold9pt8bBitmaps,
(GFXglyph *)MontserratBold9pt8bGlyphs,
0x20, 0x17F, 21 };
// Approx. 8659 bytes
#endif // MONTSERRATBOLD9PT8B_H
@@ -1,272 +0,0 @@
#ifndef NOTOSANS12PT7B_H
#define NOTOSANS12PT7B_H
const uint8_t NotoSans_Regular12pt7bBitmaps[] PROGMEM = {
0x00, 0xDB, 0x6D, 0xB6, 0xDB, 0x60, 0x37, 0xC0, 0xCF, 0x3C, 0xF3, 0x8E,
0x30, 0x06, 0x30, 0x10, 0x80, 0xC6, 0x03, 0x18, 0x0C, 0x63, 0xFF, 0xFF,
0xFF, 0xC6, 0x30, 0x18, 0xC0, 0x63, 0x01, 0x8C, 0x3F, 0xFE, 0x31, 0x80,
0xC6, 0x03, 0x18, 0x0C, 0x60, 0x21, 0x00, 0x04, 0x00, 0x80, 0x7E, 0x1F,
0xF6, 0x45, 0xC8, 0x39, 0x03, 0x20, 0x7C, 0x03, 0xF0, 0x1F, 0x02, 0x70,
0x46, 0x08, 0xF1, 0x3F, 0xFE, 0x3F, 0x00, 0x80, 0x10, 0x00, 0x38, 0x0C,
0x36, 0x06, 0x31, 0x86, 0x18, 0xC3, 0x0C, 0x63, 0x06, 0x31, 0x03, 0x19,
0x81, 0x8D, 0x9E, 0x66, 0xD9, 0xBE, 0xC8, 0xC4, 0x6C, 0x20, 0x66, 0x10,
0x33, 0x08, 0x30, 0x84, 0x10, 0x46, 0x18, 0x33, 0x18, 0x0F, 0x00, 0x0F,
0x00, 0x3F, 0xC0, 0x30, 0xC0, 0x30, 0xC0, 0x30, 0xC0, 0x31, 0xC0, 0x1B,
0x80, 0x0F, 0x00, 0x1E, 0x00, 0x73, 0x06, 0x61, 0x8E, 0xE0, 0xEC, 0xC0,
0x7C, 0xE0, 0x38, 0x60, 0x7C, 0x7F, 0xEE, 0x3F, 0x87, 0xFF, 0xA0, 0x19,
0x8C, 0xC6, 0x33, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x8C, 0x63, 0x0C, 0x71,
0x80, 0xC3, 0x18, 0x63, 0x18, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x66, 0x31,
0x98, 0xCC, 0x00, 0x0E, 0x01, 0x80, 0x10, 0x42, 0x1F, 0x5F, 0xFF, 0xC3,
0x80, 0xD8, 0x3B, 0x8E, 0x30, 0x44, 0x00, 0x06, 0x00, 0xC0, 0x18, 0x03,
0x00, 0x61, 0xFF, 0xFF, 0xF8, 0x30, 0x06, 0x00, 0xC0, 0x18, 0x00, 0x76,
0x66, 0xCC, 0xFF, 0xF0, 0xDF, 0x00, 0x03, 0x81, 0x80, 0xC0, 0xC0, 0x60,
0x30, 0x30, 0x18, 0x18, 0x0C, 0x06, 0x06, 0x03, 0x01, 0x81, 0x80, 0xC0,
0xC0, 0x00, 0x1F, 0x07, 0xF1, 0xC3, 0x30, 0x76, 0x07, 0x80, 0xF0, 0x1E,
0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x36, 0x06, 0xC1, 0xD8, 0x31, 0xFE,
0x1F, 0x00, 0x0C, 0x73, 0xFB, 0x4C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C,
0x30, 0xC3, 0x0C, 0x1F, 0x0F, 0xF3, 0x83, 0x00, 0x60, 0x0E, 0x01, 0x80,
0x30, 0x0E, 0x01, 0x80, 0x60, 0x18, 0x06, 0x01, 0x80, 0x60, 0x18, 0x07,
0xFF, 0xFF, 0xE0, 0x3F, 0x1F, 0xF9, 0x03, 0x00, 0x70, 0x0E, 0x01, 0x80,
0x70, 0xFC, 0x1F, 0x00, 0x38, 0x03, 0x80, 0x30, 0x06, 0x00, 0xE0, 0x3F,
0xFE, 0x7F, 0x80, 0x00, 0xE0, 0x0F, 0x00, 0x58, 0x06, 0xC0, 0x66, 0x03,
0x30, 0x31, 0x83, 0x0C, 0x18, 0x61, 0x83, 0x18, 0x19, 0xFF, 0xFF, 0xFF,
0x80, 0x30, 0x01, 0x80, 0x0C, 0x00, 0x60, 0x7F, 0xCF, 0xF9, 0x80, 0x30,
0x06, 0x00, 0xC0, 0x18, 0x03, 0xFC, 0x77, 0xC0, 0x1C, 0x01, 0x80, 0x30,
0x06, 0x00, 0xE0, 0x37, 0xFE, 0x7F, 0x00, 0x07, 0xC3, 0xF8, 0xE0, 0x18,
0x06, 0x00, 0xC0, 0x18, 0x86, 0xFE, 0xF0, 0xFC, 0x0F, 0x81, 0xE0, 0x36,
0x06, 0xC0, 0xDC, 0x39, 0xFE, 0x1F, 0x80, 0xFF, 0xFF, 0xFC, 0x01, 0x80,
0x70, 0x0C, 0x03, 0x80, 0x60, 0x0C, 0x03, 0x00, 0x60, 0x18, 0x03, 0x00,
0xE0, 0x18, 0x07, 0x00, 0xC0, 0x38, 0x00, 0x1F, 0x07, 0xF9, 0x83, 0x30,
0x36, 0x06, 0xC1, 0x8C, 0x70, 0xFC, 0x1F, 0x07, 0x79, 0x83, 0xE0, 0x3C,
0x07, 0x80, 0xF8, 0x1B, 0xDE, 0x3F, 0x80, 0x1F, 0x07, 0xF1, 0x83, 0x70,
0x7C, 0x07, 0x80, 0xF0, 0x1F, 0x03, 0x60, 0xE7, 0xEC, 0x79, 0x80, 0x30,
0x0C, 0x01, 0x80, 0x63, 0x7C, 0x7E, 0x00, 0xDF, 0x00, 0x00, 0x03, 0x7C,
0x67, 0x60, 0x00, 0x00, 0x00, 0x66, 0x6C, 0xCC, 0x00, 0x20, 0x1C, 0x0E,
0x07, 0x03, 0x81, 0xC0, 0x3C, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0x80,
0xFF, 0xE0, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xC0, 0xC0, 0x1E, 0x00,
0xF0, 0x07, 0x80, 0x3C, 0x01, 0xC0, 0x78, 0x3C, 0x3E, 0x1E, 0x03, 0x00,
0x00, 0x3E, 0x3F, 0xE4, 0x18, 0x03, 0x00, 0xC0, 0x60, 0x18, 0x1C, 0x0E,
0x03, 0x01, 0x80, 0x60, 0x00, 0x00, 0x01, 0x80, 0x70, 0x1C, 0x00, 0x00,
0xF8, 0x00, 0xFF, 0xC0, 0x38, 0x0C, 0x0C, 0x00, 0xC3, 0x00, 0x0C, 0xC3,
0xF8, 0x98, 0xE3, 0x1A, 0x18, 0x63, 0xC6, 0x0C, 0x78, 0xC1, 0x0F, 0x18,
0x21, 0xE3, 0x0C, 0x24, 0x31, 0xCC, 0xC7, 0xCF, 0x18, 0x00, 0x01, 0x80,
0x00, 0x38, 0x00, 0x03, 0xF7, 0x80, 0x1F, 0xE0, 0x00, 0x03, 0x80, 0x07,
0x00, 0x0A, 0x00, 0x36, 0x00, 0x6C, 0x01, 0xCC, 0x03, 0x18, 0x06, 0x30,
0x18, 0x30, 0x30, 0x60, 0x7F, 0xC1, 0xFF, 0xC3, 0x01, 0x8E, 0x03, 0x98,
0x03, 0x30, 0x06, 0xC0, 0x06, 0xFF, 0x0F, 0xFC, 0xC0, 0xEC, 0x06, 0xC0,
0x6C, 0x06, 0xC0, 0x6F, 0xFC, 0xFF, 0x8C, 0x0E, 0xC0, 0x7C, 0x03, 0xC0,
0x3C, 0x07, 0xC0, 0x6F, 0xFE, 0xFF, 0x80, 0x07, 0xF0, 0xFF, 0xCF, 0x02,
0x60, 0x07, 0x00, 0x30, 0x01, 0x80, 0x1C, 0x00, 0xE0, 0x07, 0x00, 0x18,
0x00, 0xC0, 0x06, 0x00, 0x38, 0x00, 0xE0, 0x03, 0xFE, 0x0F, 0xF0, 0xFF,
0x03, 0xFF, 0x0C, 0x0F, 0x30, 0x0C, 0xC0, 0x1B, 0x00, 0x6C, 0x01, 0xF0,
0x07, 0xC0, 0x1F, 0x00, 0x7C, 0x01, 0xF0, 0x06, 0xC0, 0x1B, 0x00, 0xCC,
0x0F, 0x3F, 0xF8, 0xFF, 0x00, 0xFF, 0xFF, 0xFC, 0x03, 0x00, 0xC0, 0x30,
0x0C, 0x03, 0xFE, 0xFF, 0xB0, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03,
0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFC, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03,
0x00, 0xFF, 0xBF, 0xEC, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0,
0x00, 0x03, 0xF8, 0x3F, 0xF1, 0xC0, 0x4C, 0x00, 0x70, 0x01, 0x80, 0x06,
0x00, 0x38, 0x00, 0xE0, 0x7F, 0x81, 0xF6, 0x00, 0xD8, 0x03, 0x60, 0x0C,
0xC0, 0x33, 0x80, 0xC7, 0xFF, 0x07, 0xFC, 0xC0, 0x1E, 0x00, 0xF0, 0x07,
0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x7F, 0xFF, 0xFF, 0xFE, 0x00, 0xF0,
0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x18, 0xFF,
0xF3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x1E, 0xFC,
0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3,
0x0C, 0x30, 0xEF, 0xF8, 0xC0, 0x36, 0x03, 0x30, 0x31, 0x83, 0x0C, 0x30,
0x63, 0x83, 0x38, 0x1B, 0x80, 0xFC, 0x07, 0xF0, 0x31, 0xC1, 0x87, 0x0C,
0x18, 0x60, 0xE3, 0x03, 0x98, 0x0C, 0xC0, 0x38, 0xC0, 0x30, 0x0C, 0x03,
0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0,
0x30, 0x0C, 0x03, 0xFF, 0xFF, 0xC0, 0xF0, 0x03, 0xF8, 0x03, 0xFC, 0x01,
0xFB, 0x00, 0xFD, 0x80, 0xDE, 0xC0, 0x6F, 0x30, 0x27, 0x98, 0x33, 0xCE,
0x19, 0xE3, 0x18, 0xF1, 0x8C, 0x78, 0x66, 0x3C, 0x36, 0x1E, 0x1B, 0x0F,
0x07, 0x07, 0x83, 0x83, 0xC1, 0xC1, 0x80, 0xE0, 0x0F, 0xC0, 0x3F, 0x00,
0xFE, 0x03, 0xD8, 0x0F, 0x30, 0x3C, 0xE0, 0xF1, 0x83, 0xC3, 0x0F, 0x0E,
0x3C, 0x18, 0xF0, 0x33, 0xC0, 0xEF, 0x01, 0xBC, 0x07, 0xF0, 0x0F, 0xC0,
0x1C, 0x07, 0xE0, 0x1F, 0xF8, 0x38, 0x1C, 0x30, 0x0E, 0x60, 0x06, 0x60,
0x07, 0x60, 0x07, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0x60, 0x03, 0x60,
0x07, 0x60, 0x06, 0x70, 0x0E, 0x38, 0x1C, 0x1F, 0xF8, 0x0F, 0xF0, 0xFE,
0x1F, 0xFB, 0x07, 0x60, 0x7C, 0x07, 0x80, 0xF0, 0x1E, 0x07, 0xC7, 0xDF,
0xF3, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x00, 0x07,
0xE0, 0x1F, 0xF8, 0x38, 0x1C, 0x30, 0x0E, 0x60, 0x06, 0x60, 0x07, 0x60,
0x07, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0x60, 0x03, 0x60, 0x07, 0x60,
0x06, 0x70, 0x0E, 0x38, 0x1C, 0x1F, 0xF8, 0x0F, 0xF0, 0x00, 0x70, 0x00,
0x38, 0x00, 0x1C, 0x00, 0x0E, 0xFF, 0x0F, 0xFC, 0xC1, 0xCC, 0x0E, 0xC0,
0x6C, 0x06, 0xC0, 0x6C, 0x0C, 0xFF, 0x8F, 0xF0, 0xC3, 0x0C, 0x18, 0xC1,
0xCC, 0x0C, 0xC0, 0xEC, 0x06, 0xC0, 0x70, 0x1F, 0x8F, 0xF9, 0x81, 0x70,
0x0E, 0x00, 0xC0, 0x1C, 0x01, 0xE0, 0x1F, 0x00, 0xF8, 0x07, 0x00, 0x70,
0x06, 0x00, 0xE0, 0x37, 0xFE, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xC1, 0xC0,
0x0E, 0x00, 0x70, 0x03, 0x80, 0x1C, 0x00, 0xE0, 0x07, 0x00, 0x38, 0x01,
0xC0, 0x0E, 0x00, 0x70, 0x03, 0x80, 0x1C, 0x00, 0xE0, 0x07, 0x00, 0xC0,
0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03,
0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xF0, 0x1D, 0xC0, 0xC7,
0xFC, 0x1F, 0xC0, 0xC0, 0x0F, 0x80, 0x76, 0x01, 0x98, 0x06, 0x30, 0x38,
0xC0, 0xC3, 0x03, 0x06, 0x1C, 0x18, 0x60, 0x61, 0x80, 0xCC, 0x03, 0x30,
0x0C, 0xC0, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x03, 0x00, 0xE0, 0x30, 0x1D,
0x81, 0xC0, 0x66, 0x07, 0x81, 0x98, 0x1E, 0x0E, 0x70, 0xD8, 0x30, 0xC3,
0x30, 0xC3, 0x0C, 0xC3, 0x0C, 0x33, 0x1C, 0x39, 0x8C, 0x60, 0x66, 0x19,
0x81, 0x98, 0x66, 0x06, 0xC1, 0xB8, 0x0F, 0x03, 0xC0, 0x3C, 0x0F, 0x00,
0xF0, 0x3C, 0x03, 0x80, 0xE0, 0x06, 0x01, 0x80, 0xE0, 0x19, 0xC0, 0xC3,
0x03, 0x06, 0x18, 0x18, 0xE0, 0x33, 0x00, 0xF8, 0x01, 0xE0, 0x07, 0x00,
0x1E, 0x00, 0xDC, 0x03, 0x30, 0x18, 0x60, 0xE1, 0x83, 0x03, 0x18, 0x0E,
0xE0, 0x1C, 0xC0, 0x1B, 0x01, 0xDC, 0x0C, 0x60, 0xE3, 0x86, 0x0C, 0x60,
0x73, 0x01, 0xB0, 0x0F, 0x80, 0x38, 0x00, 0xC0, 0x06, 0x00, 0x30, 0x01,
0x80, 0x0C, 0x00, 0x60, 0x03, 0x00, 0xFF, 0xEF, 0xFE, 0x00, 0xE0, 0x0C,
0x01, 0xC0, 0x38, 0x03, 0x00, 0x70, 0x0E, 0x00, 0xC0, 0x1C, 0x03, 0x80,
0x30, 0x07, 0x00, 0xE0, 0x0F, 0xFF, 0xFF, 0xF0, 0xFF, 0xF1, 0x8C, 0x63,
0x18, 0xC6, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x31, 0xFF, 0x80, 0xC0, 0x30,
0x18, 0x0C, 0x03, 0x01, 0x80, 0x60, 0x30, 0x18, 0x06, 0x03, 0x01, 0x80,
0x60, 0x30, 0x0C, 0x06, 0x03, 0x80, 0xFF, 0xC6, 0x31, 0x8C, 0x63, 0x18,
0xC6, 0x31, 0x8C, 0x63, 0x18, 0xC7, 0xFF, 0x80, 0x04, 0x00, 0xE0, 0x0A,
0x01, 0xB0, 0x19, 0x03, 0x18, 0x30, 0x86, 0x0C, 0x60, 0x4C, 0x06, 0xC0,
0x30, 0xFF, 0xFF, 0xF0, 0xE3, 0x0C, 0x30, 0x1F, 0x1F, 0xE2, 0x0C, 0x03,
0x00, 0xC0, 0xF3, 0xFD, 0x83, 0xC0, 0xF0, 0x3C, 0x1D, 0xDF, 0x7E, 0x40,
0xC0, 0x18, 0x03, 0x00, 0x60, 0x0C, 0x01, 0x9E, 0x37, 0xE7, 0x06, 0xE0,
0xF8, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1F, 0x06, 0xFF, 0xDB,
0xF0, 0x0F, 0x4F, 0xE7, 0x09, 0x80, 0x60, 0x38, 0x0C, 0x03, 0x00, 0xE0,
0x18, 0x07, 0x00, 0xFE, 0x1F, 0x80, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30,
0x06, 0x1C, 0xCF, 0xFB, 0x87, 0x60, 0x7C, 0x0F, 0x81, 0xE0, 0x3C, 0x07,
0xC0, 0xD8, 0x1B, 0x03, 0x3F, 0xA3, 0xE4, 0x0F, 0x07, 0xF1, 0xC3, 0x30,
0x36, 0x07, 0xFF, 0xFF, 0xFF, 0x00, 0xE0, 0x0C, 0x01, 0xC0, 0x1F, 0xE1,
0xFC, 0x0F, 0x8F, 0xC6, 0x03, 0x01, 0x81, 0xFB, 0xFC, 0x30, 0x18, 0x0C,
0x06, 0x03, 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x00, 0x0E, 0x27,
0xF5, 0xC3, 0xB0, 0x36, 0x07, 0xC0, 0xF0, 0x1E, 0x03, 0xE0, 0x6C, 0x0D,
0x81, 0x9F, 0xF1, 0xF6, 0x00, 0xC0, 0x18, 0x03, 0x60, 0xEF, 0xF8, 0x38,
0x00, 0xC0, 0x18, 0x03, 0x00, 0x60, 0x0C, 0x01, 0x9E, 0x37, 0xF7, 0x06,
0xE0, 0xF8, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0,
0x78, 0x0C, 0xF0, 0xFF, 0xFF, 0xFF, 0xC0, 0x18, 0xC0, 0x01, 0x8C, 0x63,
0x18, 0xC6, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x33, 0xFB, 0x00, 0xC0, 0x30,
0x0C, 0x03, 0x00, 0xC0, 0x30, 0x7C, 0x1B, 0x0C, 0xC6, 0x33, 0x0D, 0x83,
0xE0, 0xEC, 0x33, 0x8C, 0x73, 0x0C, 0xC1, 0xB0, 0x70, 0xFF, 0xFF, 0xFF,
0xFF, 0xF0, 0xCF, 0x0F, 0x37, 0xEF, 0xEE, 0x1E, 0x1F, 0x83, 0x83, 0xC0,
0xC0, 0xF0, 0x30, 0x3C, 0x0C, 0x0F, 0x03, 0x03, 0xC0, 0xC0, 0xF0, 0x30,
0x3C, 0x0C, 0x0F, 0x03, 0x03, 0xC0, 0xC0, 0xC0, 0xCF, 0x1B, 0xFB, 0x83,
0x70, 0x7C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0,
0x3C, 0x06, 0x0F, 0x03, 0xFC, 0x70, 0xE6, 0x06, 0x60, 0x3E, 0x03, 0xC0,
0x3C, 0x03, 0xE0, 0x36, 0x07, 0x70, 0x63, 0xFC, 0x1F, 0x80, 0xCF, 0x1B,
0xF3, 0x83, 0x70, 0x7C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F,
0x83, 0x7F, 0xED, 0xF9, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x18, 0x03, 0x00,
0x00, 0x0E, 0x27, 0xF5, 0xC3, 0xB0, 0x3E, 0x07, 0xC0, 0xF0, 0x1E, 0x03,
0xE0, 0x6C, 0x0D, 0x81, 0x9F, 0xF1, 0xF6, 0x00, 0xC0, 0x18, 0x03, 0x00,
0x60, 0x0C, 0x01, 0x80, 0xCF, 0xBF, 0xC7, 0x0C, 0x18, 0x30, 0x60, 0xC1,
0x83, 0x06, 0x0C, 0x00, 0x1E, 0x3F, 0xF8, 0x38, 0x0E, 0x03, 0xC0, 0xF8,
0x0E, 0x01, 0x80, 0xC0, 0x7E, 0xFF, 0xE0, 0x10, 0x30, 0x7F, 0xFF, 0x30,
0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x1D, 0x1F, 0xC0, 0xF8,
0x1F, 0x03, 0xE0, 0x7C, 0x0F, 0x81, 0xF0, 0x3E, 0x07, 0xC0, 0xF8, 0x1F,
0x03, 0xBF, 0xB7, 0xE6, 0xC0, 0x3E, 0x07, 0x60, 0x66, 0x06, 0x30, 0xC3,
0x0C, 0x30, 0xC1, 0x98, 0x19, 0x81, 0xB0, 0x0F, 0x00, 0xF0, 0x06, 0x00,
0xC0, 0xC0, 0xD8, 0x38, 0x36, 0x1E, 0x0D, 0x87, 0x86, 0x61, 0xB1, 0x8C,
0x4C, 0x63, 0x33, 0x38, 0xCC, 0x6C, 0x33, 0x1B, 0x07, 0x86, 0xC1, 0xE0,
0xA0, 0x78, 0x38, 0x0C, 0x0E, 0x00, 0x60, 0x77, 0x06, 0x30, 0xC1, 0x9C,
0x0D, 0x80, 0xF0, 0x06, 0x00, 0xF0, 0x0D, 0x81, 0x9C, 0x30, 0xC7, 0x06,
0xE0, 0x70, 0xC0, 0x3E, 0x07, 0x60, 0x66, 0x06, 0x30, 0xC3, 0x0C, 0x38,
0xC1, 0x98, 0x19, 0x80, 0xD8, 0x0F, 0x00, 0xF0, 0x06, 0x00, 0x60, 0x06,
0x00, 0xC0, 0x1C, 0x0F, 0x80, 0xE0, 0x00, 0xFF, 0xFF, 0xC0, 0xE0, 0x60,
0x60, 0x60, 0x30, 0x30, 0x30, 0x38, 0x18, 0x1F, 0xFF, 0xF8, 0x02, 0x3C,
0x60, 0xC1, 0x83, 0x06, 0x0C, 0x31, 0xE3, 0x03, 0x83, 0x03, 0x06, 0x0C,
0x18, 0x30, 0x60, 0xF0, 0x20, 0xFF, 0xFF, 0xFF, 0x81, 0xE0, 0xC1, 0x83,
0x06, 0x0C, 0x18, 0x38, 0x3C, 0x18, 0xE3, 0x86, 0x0C, 0x18, 0x30, 0x60,
0xC7, 0x08, 0x00, 0x78, 0x3F, 0xEE, 0x1F, 0x80 };
const GFXglyph NotoSans_Regular12pt7bGlyphs[] PROGMEM = {
{ 0, 1, 1, 6, 0, 0 }, // 0x20 ' '
{ 1, 3, 17, 6, 2, -16 }, // 0x21 '!'
{ 8, 6, 6, 10, 2, -16 }, // 0x22 '"'
{ 13, 14, 17, 15, 1, -16 }, // 0x23 '#'
{ 43, 11, 19, 13, 1, -17 }, // 0x24 '$'
{ 70, 17, 17, 20, 1, -16 }, // 0x25 '%'
{ 107, 16, 17, 17, 1, -16 }, // 0x26 '&'
{ 141, 2, 6, 5, 2, -16 }, // 0x27 '''
{ 143, 5, 21, 7, 1, -16 }, // 0x28 '('
{ 157, 5, 21, 7, 1, -16 }, // 0x29 ')'
{ 171, 11, 11, 13, 1, -17 }, // 0x2A '*'
{ 187, 11, 11, 13, 1, -13 }, // 0x2B '+'
{ 203, 4, 6, 6, 1, -2 }, // 0x2C ','
{ 206, 6, 2, 8, 1, -6 }, // 0x2D '-'
{ 208, 3, 3, 6, 2, -2 }, // 0x2E '.'
{ 210, 9, 17, 9, 0, -16 }, // 0x2F '/'
{ 230, 11, 17, 13, 1, -16 }, // 0x30 '0'
{ 254, 6, 17, 13, 2, -16 }, // 0x31 '1'
{ 267, 11, 17, 13, 1, -16 }, // 0x32 '2'
{ 291, 11, 17, 13, 1, -16 }, // 0x33 '3'
{ 315, 13, 17, 13, 0, -16 }, // 0x34 '4'
{ 343, 11, 17, 13, 1, -16 }, // 0x35 '5'
{ 367, 11, 17, 13, 1, -16 }, // 0x36 '6'
{ 391, 11, 17, 13, 1, -16 }, // 0x37 '7'
{ 415, 11, 17, 13, 1, -16 }, // 0x38 '8'
{ 439, 11, 17, 13, 1, -16 }, // 0x39 '9'
{ 463, 3, 13, 6, 2, -12 }, // 0x3A ':'
{ 468, 4, 16, 6, 1, -12 }, // 0x3B ';'
{ 476, 11, 11, 13, 1, -13 }, // 0x3C '<'
{ 492, 11, 6, 13, 1, -10 }, // 0x3D '='
{ 501, 11, 11, 13, 1, -13 }, // 0x3E '>'
{ 517, 10, 17, 10, 0, -16 }, // 0x3F '?'
{ 539, 19, 19, 21, 1, -16 }, // 0x40 '@'
{ 585, 15, 17, 15, 0, -16 }, // 0x41 'A'
{ 617, 12, 17, 15, 2, -16 }, // 0x42 'B'
{ 643, 13, 17, 15, 1, -16 }, // 0x43 'C'
{ 671, 14, 17, 17, 2, -16 }, // 0x44 'D'
{ 701, 10, 17, 13, 2, -16 }, // 0x45 'E'
{ 723, 10, 17, 12, 2, -16 }, // 0x46 'F'
{ 745, 14, 17, 17, 1, -16 }, // 0x47 'G'
{ 775, 13, 17, 17, 2, -16 }, // 0x48 'H'
{ 803, 6, 17, 8, 1, -16 }, // 0x49 'I'
{ 816, 6, 21, 6, -2, -16 }, // 0x4A 'J'
{ 832, 13, 17, 15, 2, -16 }, // 0x4B 'K'
{ 860, 10, 17, 12, 2, -16 }, // 0x4C 'L'
{ 882, 17, 17, 21, 2, -16 }, // 0x4D 'M'
{ 919, 14, 17, 18, 2, -16 }, // 0x4E 'N'
{ 949, 16, 17, 18, 1, -16 }, // 0x4F 'O'
{ 983, 11, 17, 14, 2, -16 }, // 0x50 'P'
{ 1007, 16, 21, 18, 1, -16 }, // 0x51 'Q'
{ 1049, 12, 17, 15, 2, -16 }, // 0x52 'R'
{ 1075, 11, 17, 13, 1, -16 }, // 0x53 'S'
{ 1099, 13, 17, 13, 0, -16 }, // 0x54 'T'
{ 1127, 13, 17, 17, 2, -16 }, // 0x55 'U'
{ 1155, 14, 17, 14, 0, -16 }, // 0x56 'V'
{ 1185, 22, 17, 22, 0, -16 }, // 0x57 'W'
{ 1232, 14, 17, 14, 0, -16 }, // 0x58 'X'
{ 1262, 13, 17, 13, 0, -16 }, // 0x59 'Y'
{ 1290, 12, 17, 13, 1, -16 }, // 0x5A 'Z'
{ 1316, 5, 21, 8, 2, -16 }, // 0x5B '['
{ 1330, 9, 17, 9, 0, -16 }, // 0x5C '\'
{ 1350, 5, 21, 8, 1, -16 }, // 0x5D ']'
{ 1364, 12, 11, 13, 1, -16 }, // 0x5E '^'
{ 1381, 10, 2, 10, 0, 3 }, // 0x5F '_'
{ 1384, 5, 4, 7, 1, -17 }, // 0x60 '`'
{ 1387, 10, 13, 13, 1, -12 }, // 0x61 'a'
{ 1404, 11, 18, 14, 2, -17 }, // 0x62 'b'
{ 1429, 10, 13, 11, 1, -12 }, // 0x63 'c'
{ 1446, 11, 18, 14, 1, -17 }, // 0x64 'd'
{ 1471, 11, 13, 13, 1, -12 }, // 0x65 'e'
{ 1489, 9, 18, 8, 0, -17 }, // 0x66 'f'
{ 1510, 11, 19, 14, 1, -12 }, // 0x67 'g'
{ 1537, 11, 18, 15, 2, -17 }, // 0x68 'h'
{ 1562, 2, 17, 6, 2, -16 }, // 0x69 'i'
{ 1567, 5, 23, 6, -1, -16 }, // 0x6A 'j'
{ 1582, 10, 18, 13, 2, -17 }, // 0x6B 'k'
{ 1605, 2, 18, 6, 2, -17 }, // 0x6C 'l'
{ 1610, 18, 13, 22, 2, -12 }, // 0x6D 'm'
{ 1640, 11, 13, 15, 2, -12 }, // 0x6E 'n'
{ 1658, 12, 13, 14, 1, -12 }, // 0x6F 'o'
{ 1678, 11, 19, 14, 2, -12 }, // 0x70 'p'
{ 1705, 11, 19, 14, 1, -12 }, // 0x71 'q'
{ 1732, 7, 13, 10, 2, -12 }, // 0x72 'r'
{ 1744, 9, 13, 11, 1, -12 }, // 0x73 's'
{ 1759, 8, 15, 8, 0, -14 }, // 0x74 't'
{ 1774, 11, 13, 15, 2, -12 }, // 0x75 'u'
{ 1792, 12, 13, 12, 0, -12 }, // 0x76 'v'
{ 1812, 18, 13, 18, 0, -12 }, // 0x77 'w'
{ 1842, 12, 13, 12, 0, -12 }, // 0x78 'x'
{ 1862, 12, 19, 12, 0, -12 }, // 0x79 'y'
{ 1891, 9, 13, 11, 1, -12 }, // 0x7A 'z'
{ 1906, 7, 21, 9, 1, -16 }, // 0x7B '{'
{ 1925, 1, 24, 13, 6, -17 }, // 0x7C '|'
{ 1928, 7, 21, 9, 1, -16 }, // 0x7D '}'
{ 1947, 11, 3, 13, 1, -9 } }; // 0x7E '~'
const GFXfont NotoSans_Regular12pt7b PROGMEM = {
(uint8_t *)NotoSans_Regular12pt7bBitmaps,
(GFXglyph *)NotoSans_Regular12pt7bGlyphs,
0x20, 0x7E, 32 };
// Approx. 2624 bytes
#endif // NOTOSANS12PT7B_H
File diff suppressed because it is too large Load Diff
@@ -1,201 +0,0 @@
#ifndef NOTOSANS9PT7B_H
#define NOTOSANS9PT7B_H
const uint8_t NotoSans_Regular9pt7bBitmaps[] PROGMEM = {
0x00, 0xF5, 0x55, 0x43, 0xC0, 0xDE, 0xF3, 0x9C, 0x80, 0x0C, 0x81, 0x10,
0x22, 0x04, 0x47, 0xFE, 0x32, 0x04, 0x40, 0x88, 0xFF, 0xC2, 0x60, 0xC8,
0x11, 0x02, 0x20, 0x18, 0x3E, 0x7F, 0xD8, 0xD8, 0xF8, 0x3C, 0x1F, 0x1B,
0x19, 0x9B, 0xFE, 0x18, 0x18, 0x70, 0x46, 0xC2, 0x22, 0x21, 0x13, 0x08,
0x90, 0x45, 0xB3, 0x6B, 0x4E, 0xD3, 0x04, 0x98, 0x44, 0xC2, 0x26, 0x21,
0x23, 0x07, 0x00, 0x1C, 0x07, 0x60, 0x62, 0x06, 0x20, 0x26, 0x03, 0xC0,
0x38, 0x06, 0xC6, 0xC6, 0x6C, 0x3C, 0xC1, 0x8E, 0x3C, 0x7E, 0x70, 0xFF,
0xC0, 0x32, 0x64, 0xCC, 0x88, 0x88, 0x8C, 0xC4, 0x63, 0x8C, 0x46, 0x22,
0x23, 0x33, 0x22, 0x64, 0xC8, 0x18, 0x10, 0xD3, 0xFF, 0x18, 0x2C, 0x66,
0x24, 0x18, 0x18, 0x18, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x7E, 0xF0, 0xF0,
0x04, 0x30, 0xC2, 0x18, 0x41, 0x0C, 0x21, 0x86, 0x10, 0xC0, 0x3C, 0x7E,
0x42, 0xC3, 0xC1, 0x81, 0x81, 0x81, 0xC1, 0xC3, 0xC3, 0x66, 0x3C, 0x13,
0xD9, 0x11, 0x11, 0x11, 0x11, 0x10, 0x3C, 0xFE, 0x83, 0x03, 0x03, 0x02,
0x06, 0x0C, 0x18, 0x30, 0x60, 0xC0, 0xFF, 0x3C, 0xFE, 0x83, 0x03, 0x03,
0x06, 0x38, 0x06, 0x03, 0x03, 0x03, 0x86, 0xFC, 0x03, 0x01, 0xC0, 0x70,
0x3C, 0x1B, 0x04, 0xC2, 0x31, 0x8C, 0x43, 0x3F, 0xF0, 0x30, 0x0C, 0x03,
0x00, 0x7E, 0x7E, 0x40, 0x40, 0x40, 0xFC, 0x7E, 0x03, 0x03, 0x03, 0x03,
0x86, 0xFC, 0x0E, 0x3E, 0x60, 0x40, 0xC0, 0xDC, 0xE7, 0xC3, 0xC1, 0xC1,
0xC3, 0x63, 0x3E, 0xFF, 0xFF, 0x03, 0x02, 0x06, 0x04, 0x0C, 0x0C, 0x18,
0x18, 0x10, 0x30, 0x60, 0x3C, 0x7E, 0xC3, 0xC3, 0x43, 0x76, 0x3C, 0x66,
0xC3, 0x81, 0x81, 0xC3, 0x7E, 0x38, 0x7E, 0xC3, 0xC3, 0x81, 0xC3, 0xC3,
0x7D, 0x03, 0x03, 0x02, 0x0E, 0x78, 0x74, 0x00, 0xF0, 0x74, 0x00, 0xFA,
0x01, 0x03, 0x0C, 0x30, 0xC0, 0xE0, 0x1C, 0x07, 0x01, 0xFF, 0xFF, 0x00,
0x00, 0xFF, 0x80, 0xC0, 0x30, 0x0C, 0x03, 0x07, 0x38, 0xE0, 0x80, 0x39,
0xFC, 0x08, 0x10, 0x60, 0x86, 0x08, 0x30, 0x00, 0x01, 0x83, 0x00, 0x03,
0x80, 0x7F, 0x83, 0x01, 0x18, 0x02, 0x47, 0xCF, 0x31, 0x38, 0x84, 0x62,
0x31, 0x88, 0xCE, 0x23, 0x2C, 0x77, 0x10, 0x00, 0x60, 0x00, 0x7F, 0x00,
0x30, 0x00, 0x04, 0x01, 0xC0, 0x28, 0x05, 0x81, 0xB0, 0x22, 0x04, 0x61,
0x84, 0x3F, 0xCC, 0x19, 0x81, 0x20, 0x3C, 0x06, 0xF8, 0x7F, 0x20, 0xD0,
0x68, 0x34, 0x33, 0xF1, 0x06, 0x81, 0x40, 0xE0, 0x50, 0xEF, 0xE0, 0x0F,
0x0F, 0xF6, 0x01, 0x80, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x06,
0x00, 0xC2, 0x1F, 0x80, 0xF8, 0x3F, 0xC8, 0x3A, 0x06, 0x80, 0xE0, 0x38,
0x0E, 0x03, 0x80, 0xE0, 0x68, 0x1A, 0x1C, 0xFC, 0x00, 0xFF, 0xFE, 0x04,
0x08, 0x10, 0x3F, 0x40, 0x81, 0x02, 0x04, 0x0F, 0xE0, 0xFF, 0xFE, 0x04,
0x08, 0x10, 0x3F, 0x7E, 0x81, 0x02, 0x04, 0x08, 0x00, 0x0F, 0x87, 0xF9,
0xC0, 0x30, 0x0C, 0x01, 0x80, 0x30, 0xFE, 0x1F, 0xC0, 0x68, 0x0D, 0x81,
0x98, 0x31, 0xFE, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x07, 0xFF, 0x01,
0x80, 0xC0, 0x60, 0x30, 0x18, 0x08, 0xFF, 0x66, 0x66, 0x66, 0x66, 0x66,
0xF0, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x3E, 0x81, 0xC1, 0xA1,
0x91, 0x89, 0x85, 0x83, 0xC1, 0xB0, 0x8C, 0x46, 0x21, 0x90, 0x68, 0x18,
0x81, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x81, 0x02, 0x04, 0x0F, 0xE0,
0xC0, 0x3C, 0x03, 0xE0, 0x7A, 0x05, 0xB0, 0xDB, 0x09, 0x90, 0x99, 0x99,
0x89, 0x18, 0xB1, 0x8F, 0x18, 0x61, 0x86, 0x10, 0xC0, 0xF0, 0x3E, 0x0E,
0xC3, 0xB0, 0xE6, 0x38, 0x8E, 0x33, 0x86, 0xE0, 0xB8, 0x3E, 0x07, 0x80,
0xC0, 0x0F, 0x03, 0xFC, 0x60, 0x64, 0x06, 0xC0, 0x2C, 0x03, 0xC0, 0x3C,
0x03, 0xC0, 0x3C, 0x02, 0x60, 0x63, 0x0C, 0x1F, 0x80, 0xF0, 0xFE, 0x82,
0x83, 0x83, 0x83, 0x86, 0xFC, 0x80, 0x80, 0x80, 0x80, 0x80, 0x0F, 0x03,
0xFC, 0x60, 0x64, 0x06, 0xC0, 0x2C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C,
0x02, 0x60, 0x63, 0x0C, 0x1F, 0x80, 0x18, 0x01, 0xC0, 0x0E, 0xF0, 0x7F,
0x20, 0xD0, 0x68, 0x34, 0x13, 0xF9, 0xF0, 0x8C, 0x43, 0x21, 0x90, 0x68,
0x18, 0x3D, 0x7E, 0xC0, 0xC0, 0xC0, 0x60, 0x3C, 0x0E, 0x03, 0x03, 0x03,
0x86, 0xFC, 0xFF, 0xFF, 0xF0, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30,
0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x00, 0x80, 0xC0, 0x60, 0x30, 0x18,
0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, 0x70, 0x7C, 0x63, 0xE0, 0xC0, 0x78,
0x09, 0x03, 0x30, 0x46, 0x18, 0x43, 0x0C, 0x40, 0x98, 0x1B, 0x03, 0x40,
0x38, 0x07, 0x00, 0xC0, 0xC1, 0x83, 0x41, 0x83, 0x41, 0xC3, 0x63, 0xC2,
0x63, 0x46, 0x22, 0x46, 0x22, 0x66, 0x36, 0x24, 0x34, 0x2C, 0x14, 0x3C,
0x14, 0x38, 0x1C, 0x18, 0x18, 0x18, 0xC0, 0xD8, 0x22, 0x18, 0xCC, 0x1A,
0x03, 0x80, 0xC0, 0x38, 0x1A, 0x0C, 0xC2, 0x19, 0x82, 0xC0, 0xC0, 0xC0,
0xD0, 0x26, 0x18, 0x84, 0x33, 0x04, 0x81, 0xE0, 0x30, 0x0C, 0x03, 0x00,
0xC0, 0x30, 0x0C, 0x00, 0xFF, 0xFF, 0x03, 0x06, 0x04, 0x0C, 0x18, 0x10,
0x30, 0x60, 0x40, 0xC0, 0xFF, 0xFF, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
0xCF, 0xC1, 0x04, 0x18, 0x20, 0xC3, 0x04, 0x18, 0x20, 0x83, 0x04, 0xFF,
0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x1F, 0x10, 0x18, 0x38, 0x24, 0x64,
0x42, 0x42, 0x81, 0xFF, 0xD9, 0x80, 0x18, 0xFC, 0x08, 0x11, 0xEE, 0x70,
0xC1, 0xC6, 0xF4, 0xC0, 0x60, 0x30, 0x19, 0x8F, 0xF7, 0x0B, 0x07, 0x83,
0xC1, 0xE0, 0xF0, 0x7C, 0x6D, 0xE0, 0x1C, 0xFD, 0x06, 0x0C, 0x18, 0x30,
0x60, 0x62, 0x7C, 0x01, 0x01, 0x01, 0x19, 0x7F, 0xC3, 0xC1, 0xC1, 0xC1,
0xC1, 0xC1, 0x63, 0x3D, 0x18, 0x7E, 0x43, 0xC3, 0xFF, 0xFF, 0xC0, 0xC0,
0x61, 0x3F, 0x1E, 0x60, 0x81, 0x0F, 0xC4, 0x08, 0x10, 0x20, 0x40, 0x81,
0x02, 0x00, 0x18, 0x7F, 0x43, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0x63, 0x3D,
0x01, 0x01, 0x43, 0x7E, 0xC0, 0xC0, 0xC0, 0xCC, 0xFF, 0xE1, 0xC1, 0xC1,
0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xF0, 0xFF, 0xFF, 0xC0, 0x33, 0x00, 0x33,
0x33, 0x33, 0x33, 0x33, 0x33, 0xE0, 0xC0, 0xC0, 0xC0, 0xC1, 0xC6, 0xCC,
0xD8, 0xD0, 0xF8, 0xCC, 0xC4, 0xC6, 0xC3, 0xFF, 0xFF, 0xFF, 0xC0, 0x0C,
0x33, 0xFB, 0xFE, 0x38, 0xF0, 0xC3, 0xC1, 0x07, 0x04, 0x1C, 0x10, 0x70,
0x41, 0xC1, 0x07, 0x04, 0x10, 0x0C, 0xFF, 0xE1, 0xC1, 0xC1, 0xC1, 0xC1,
0xC1, 0xC1, 0xC1, 0x18, 0x3F, 0x10, 0xD8, 0x3C, 0x1E, 0x0F, 0x07, 0x82,
0x63, 0x1F, 0x00, 0x0C, 0x7F, 0xB8, 0x58, 0x3C, 0x1E, 0x0F, 0x07, 0x83,
0xE3, 0x6F, 0x30, 0x18, 0x0C, 0x06, 0x00, 0x18, 0x7F, 0xC3, 0xC1, 0xC1,
0xC1, 0xC1, 0xC1, 0x63, 0x3D, 0x01, 0x01, 0x01, 0x01, 0x0F, 0x7E, 0x30,
0xC3, 0x0C, 0x30, 0xC3, 0x00, 0x39, 0xFA, 0x06, 0x07, 0x07, 0x81, 0x83,
0x85, 0xF8, 0x20, 0x86, 0x3F, 0x20, 0x82, 0x08, 0x20, 0x83, 0x0F, 0xC1,
0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0x63, 0x7D, 0xC1, 0xA0, 0x98, 0xCC,
0x42, 0x21, 0xB0, 0x50, 0x28, 0x1C, 0x00, 0xC3, 0x0D, 0x1C, 0x26, 0x79,
0x99, 0x26, 0x24, 0x90, 0xB3, 0x43, 0x8F, 0x0E, 0x18, 0x18, 0x60, 0xE3,
0x99, 0x05, 0x83, 0x80, 0xC0, 0xB0, 0xC8, 0xC6, 0xC1, 0x80, 0xC1, 0xA0,
0x98, 0xC4, 0x43, 0x21, 0xB0, 0x50, 0x38, 0x0C, 0x04, 0x06, 0x02, 0x0F,
0x00, 0xFC, 0x18, 0x20, 0xC3, 0x0C, 0x10, 0x60, 0xFE, 0x04, 0x71, 0x04,
0x10, 0x41, 0x3C, 0xE0, 0xC1, 0x04, 0x10, 0x41, 0xC3, 0xFF, 0xFF, 0xFF,
0xFF, 0xC0, 0x87, 0x18, 0x42, 0x10, 0x87, 0x39, 0x08, 0x42, 0x33, 0x90,
0xF9, 0x8F };
const GFXglyph NotoSans_Regular9pt7bGlyphs[] PROGMEM = {
{ 0, 1, 1, 5, 0, 0 }, // 0x20 ' '
{ 1, 2, 13, 5, 1, -12 }, // 0x21 '!'
{ 5, 5, 5, 7, 1, -12 }, // 0x22 '"'
{ 9, 11, 13, 11, 0, -12 }, // 0x23 '#'
{ 27, 8, 14, 10, 1, -12 }, // 0x24 '$'
{ 41, 13, 13, 15, 1, -12 }, // 0x25 '%'
{ 63, 12, 13, 13, 1, -12 }, // 0x26 '&'
{ 83, 2, 5, 4, 1, -12 }, // 0x27 '''
{ 85, 4, 16, 5, 1, -12 }, // 0x28 '('
{ 93, 4, 16, 5, 1, -12 }, // 0x29 ')'
{ 101, 8, 8, 10, 1, -12 }, // 0x2A '*'
{ 109, 8, 8, 10, 1, -9 }, // 0x2B '+'
{ 117, 2, 4, 5, 1, -1 }, // 0x2C ','
{ 118, 4, 1, 6, 1, -4 }, // 0x2D '-'
{ 119, 2, 2, 5, 1, -1 }, // 0x2E '.'
{ 120, 6, 13, 7, 0, -12 }, // 0x2F '/'
{ 130, 8, 13, 10, 1, -12 }, // 0x30 '0'
{ 143, 4, 13, 10, 2, -12 }, // 0x31 '1'
{ 150, 8, 13, 10, 1, -12 }, // 0x32 '2'
{ 163, 8, 13, 10, 1, -12 }, // 0x33 '3'
{ 176, 10, 13, 10, 0, -12 }, // 0x34 '4'
{ 193, 8, 13, 10, 1, -12 }, // 0x35 '5'
{ 206, 8, 13, 10, 1, -12 }, // 0x36 '6'
{ 219, 8, 13, 10, 1, -12 }, // 0x37 '7'
{ 232, 8, 13, 10, 1, -12 }, // 0x38 '8'
{ 245, 8, 13, 10, 1, -12 }, // 0x39 '9'
{ 258, 2, 10, 5, 1, -9 }, // 0x3A ':'
{ 261, 2, 12, 5, 1, -9 }, // 0x3B ';'
{ 264, 8, 9, 10, 1, -10 }, // 0x3C '<'
{ 273, 8, 5, 10, 1, -8 }, // 0x3D '='
{ 278, 8, 9, 10, 1, -10 }, // 0x3E '>'
{ 287, 7, 13, 8, 0, -12 }, // 0x3F '?'
{ 299, 14, 15, 16, 1, -12 }, // 0x40 '@'
{ 326, 11, 13, 11, 0, -12 }, // 0x41 'A'
{ 344, 9, 13, 11, 2, -12 }, // 0x42 'B'
{ 359, 10, 13, 11, 1, -12 }, // 0x43 'C'
{ 376, 10, 13, 13, 2, -12 }, // 0x44 'D'
{ 393, 7, 13, 10, 2, -12 }, // 0x45 'E'
{ 405, 7, 13, 9, 2, -12 }, // 0x46 'F'
{ 417, 11, 13, 13, 1, -12 }, // 0x47 'G'
{ 435, 9, 13, 13, 2, -12 }, // 0x48 'H'
{ 450, 4, 13, 6, 1, -12 }, // 0x49 'I'
{ 457, 4, 16, 5, -1, -12 }, // 0x4A 'J'
{ 465, 9, 13, 11, 2, -12 }, // 0x4B 'K'
{ 480, 7, 13, 9, 2, -12 }, // 0x4C 'L'
{ 492, 12, 13, 16, 2, -12 }, // 0x4D 'M'
{ 512, 10, 13, 13, 2, -12 }, // 0x4E 'N'
{ 529, 12, 13, 14, 1, -12 }, // 0x4F 'O'
{ 549, 8, 13, 11, 2, -12 }, // 0x50 'P'
{ 562, 12, 16, 14, 1, -12 }, // 0x51 'Q'
{ 586, 9, 13, 11, 2, -12 }, // 0x52 'R'
{ 601, 8, 13, 10, 1, -12 }, // 0x53 'S'
{ 614, 10, 13, 10, 0, -12 }, // 0x54 'T'
{ 631, 9, 13, 13, 2, -12 }, // 0x55 'U'
{ 646, 11, 13, 11, 0, -12 }, // 0x56 'V'
{ 664, 16, 13, 16, 0, -12 }, // 0x57 'W'
{ 690, 10, 13, 10, 0, -12 }, // 0x58 'X'
{ 707, 10, 13, 10, 0, -12 }, // 0x59 'Y'
{ 724, 8, 13, 10, 1, -12 }, // 0x5A 'Z'
{ 737, 4, 16, 6, 1, -12 }, // 0x5B '['
{ 745, 6, 13, 7, 0, -12 }, // 0x5C '\'
{ 755, 4, 16, 6, 0, -12 }, // 0x5D ']'
{ 763, 8, 8, 10, 1, -12 }, // 0x5E '^'
{ 771, 8, 1, 8, 0, 3 }, // 0x5F '_'
{ 772, 3, 3, 5, 1, -13 }, // 0x60 '`'
{ 774, 7, 10, 10, 1, -9 }, // 0x61 'a'
{ 783, 9, 13, 11, 1, -12 }, // 0x62 'b'
{ 798, 7, 10, 8, 1, -9 }, // 0x63 'c'
{ 807, 8, 13, 11, 1, -12 }, // 0x64 'd'
{ 820, 8, 10, 10, 1, -9 }, // 0x65 'e'
{ 830, 7, 13, 6, 0, -12 }, // 0x66 'f'
{ 842, 8, 14, 11, 1, -9 }, // 0x67 'g'
{ 856, 8, 13, 11, 1, -12 }, // 0x68 'h'
{ 869, 2, 13, 5, 1, -12 }, // 0x69 'i'
{ 873, 4, 17, 5, -1, -12 }, // 0x6A 'j'
{ 882, 8, 13, 9, 1, -12 }, // 0x6B 'k'
{ 895, 2, 13, 5, 1, -12 }, // 0x6C 'l'
{ 899, 14, 10, 16, 1, -9 }, // 0x6D 'm'
{ 917, 8, 10, 11, 1, -9 }, // 0x6E 'n'
{ 927, 9, 10, 11, 1, -9 }, // 0x6F 'o'
{ 939, 9, 14, 11, 1, -9 }, // 0x70 'p'
{ 955, 8, 14, 11, 1, -9 }, // 0x71 'q'
{ 969, 6, 10, 7, 1, -9 }, // 0x72 'r'
{ 977, 7, 10, 8, 1, -9 }, // 0x73 's'
{ 986, 6, 12, 6, 0, -11 }, // 0x74 't'
{ 995, 8, 9, 11, 1, -8 }, // 0x75 'u'
{ 1004, 9, 9, 9, 0, -8 }, // 0x76 'v'
{ 1015, 14, 9, 14, 0, -8 }, // 0x77 'w'
{ 1031, 9, 9, 9, 0, -8 }, // 0x78 'x'
{ 1042, 9, 13, 9, 0, -8 }, // 0x79 'y'
{ 1057, 7, 9, 8, 1, -8 }, // 0x7A 'z'
{ 1065, 6, 16, 7, 0, -12 }, // 0x7B '{'
{ 1077, 2, 17, 10, 4, -12 }, // 0x7C '|'
{ 1082, 5, 16, 7, 1, -12 }, // 0x7D '}'
{ 1092, 8, 2, 10, 1, -6 } }; // 0x7E '~'
const GFXfont NotoSans_Regular9pt7b PROGMEM = {
(uint8_t *)NotoSans_Regular9pt7bBitmaps,
(GFXglyph *)NotoSans_Regular9pt7bGlyphs,
0x20, 0x7E, 24 };
// Approx. 1766 bytes
#endif // NOTOSANS9PT7B_H
@@ -0,0 +1,789 @@
// NotoSans9pt8b.h
//
// Adafruit GFX font header — Latin Extended-A coverage (U+0020 to U+017F).
// Source: Noto Sans at 9pt.
// 352 glyphs covering ASCII, Latin-1 Supplement, and Latin Extended-A.
// Supports Czech, Polish, German, French, Spanish, Italian, Croatian,
// Hungarian, Slovak, Romanian, and other European languages.
//
// Codepoints 0x7F-0x9F (C0/C1 control codes) are present as empty glyphs.
// They are never rendered in normal use because UTF-8 decoding does not
// produce them as standalone codepoints.
//
// To render glyphs above 0x7F, use a drawCodepoint(font, x, y, cp) helper
// that walks the glyph table directly. Adafruit GFX's print() / write() /
// drawChar() are byte-oriented (uint8_t) and cannot address indices > 255.
#ifndef NOTOSANS9PT8B_H
#define NOTOSANS9PT8B_H
const uint8_t NotoSans9pt8bBitmaps[] PROGMEM = {
0x00, 0xF5, 0x55, 0x43, 0xC0, 0xDE, 0xF3, 0x9C, 0x80, 0x0C, 0x81, 0x10,
0x22, 0x04, 0x47, 0xFE, 0x32, 0x04, 0x40, 0x88, 0xFF, 0xC2, 0x60, 0xC8,
0x11, 0x02, 0x20, 0x18, 0x3E, 0x7F, 0xD8, 0xD8, 0xF8, 0x3C, 0x1F, 0x1B,
0x19, 0x9B, 0xFE, 0x18, 0x18, 0x70, 0x46, 0xC2, 0x22, 0x21, 0x13, 0x08,
0x90, 0x45, 0xB3, 0x6B, 0x4E, 0xD3, 0x04, 0x98, 0x44, 0xC2, 0x26, 0x21,
0x23, 0x07, 0x00, 0x1C, 0x07, 0x60, 0x62, 0x06, 0x20, 0x26, 0x03, 0xC0,
0x38, 0x06, 0xC6, 0xC6, 0x6C, 0x3C, 0xC1, 0x8E, 0x3C, 0x7E, 0x70, 0xFF,
0xC0, 0x32, 0x64, 0xCC, 0x88, 0x88, 0x8C, 0xC4, 0x63, 0x8C, 0x46, 0x22,
0x23, 0x33, 0x22, 0x64, 0xC8, 0x18, 0x10, 0xD3, 0xFF, 0x18, 0x2C, 0x66,
0x24, 0x18, 0x18, 0x18, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x7E, 0xF0, 0xF0,
0x04, 0x30, 0xC2, 0x18, 0x41, 0x0C, 0x21, 0x86, 0x10, 0xC0, 0x3C, 0x7E,
0x42, 0xC3, 0xC1, 0x81, 0x81, 0x81, 0xC1, 0xC3, 0xC3, 0x66, 0x3C, 0x13,
0xD9, 0x11, 0x11, 0x11, 0x11, 0x10, 0x3C, 0xFE, 0x83, 0x03, 0x03, 0x02,
0x06, 0x0C, 0x18, 0x30, 0x60, 0xC0, 0xFF, 0x3C, 0xFE, 0x83, 0x03, 0x03,
0x06, 0x38, 0x06, 0x03, 0x03, 0x03, 0x86, 0xFC, 0x03, 0x01, 0xC0, 0x70,
0x3C, 0x1B, 0x04, 0xC2, 0x31, 0x8C, 0x43, 0x3F, 0xF0, 0x30, 0x0C, 0x03,
0x00, 0x7E, 0x7E, 0x40, 0x40, 0x40, 0xFC, 0x7E, 0x03, 0x03, 0x03, 0x03,
0x86, 0xFC, 0x0E, 0x3E, 0x60, 0x40, 0xC0, 0xDC, 0xE7, 0xC3, 0xC1, 0xC1,
0xC3, 0x63, 0x3E, 0xFF, 0xFF, 0x03, 0x02, 0x06, 0x04, 0x0C, 0x0C, 0x18,
0x18, 0x10, 0x30, 0x60, 0x3C, 0x7E, 0xC3, 0xC3, 0x43, 0x76, 0x3C, 0x66,
0xC3, 0x81, 0x81, 0xC3, 0x7E, 0x38, 0x7E, 0xC3, 0xC3, 0x81, 0xC3, 0xC3,
0x7D, 0x03, 0x03, 0x02, 0x0E, 0x78, 0x74, 0x00, 0xF0, 0x74, 0x00, 0xFA,
0x01, 0x03, 0x0C, 0x30, 0xC0, 0xE0, 0x1C, 0x07, 0x01, 0xFF, 0xFF, 0x00,
0x00, 0xFF, 0x80, 0xC0, 0x30, 0x0C, 0x03, 0x07, 0x38, 0xE0, 0x80, 0x39,
0xFC, 0x08, 0x10, 0x60, 0x86, 0x08, 0x30, 0x00, 0x01, 0x83, 0x00, 0x03,
0x80, 0x7F, 0x83, 0x01, 0x18, 0x02, 0x47, 0xCF, 0x31, 0x38, 0x84, 0x62,
0x31, 0x88, 0xCE, 0x23, 0x2C, 0x77, 0x10, 0x00, 0x60, 0x00, 0x7F, 0x00,
0x30, 0x00, 0x04, 0x01, 0xC0, 0x28, 0x05, 0x81, 0xB0, 0x22, 0x04, 0x61,
0x84, 0x3F, 0xCC, 0x19, 0x81, 0x20, 0x3C, 0x06, 0xF8, 0x7F, 0x20, 0xD0,
0x68, 0x34, 0x33, 0xF1, 0x06, 0x81, 0x40, 0xE0, 0x50, 0xEF, 0xE0, 0x0F,
0x0F, 0xF6, 0x01, 0x80, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x06,
0x00, 0xC2, 0x1F, 0x80, 0xF8, 0x3F, 0xC8, 0x3A, 0x06, 0x80, 0xE0, 0x38,
0x0E, 0x03, 0x80, 0xE0, 0x68, 0x1A, 0x1C, 0xFC, 0x00, 0xFF, 0xFE, 0x04,
0x08, 0x10, 0x3F, 0x40, 0x81, 0x02, 0x04, 0x0F, 0xE0, 0xFF, 0xFE, 0x04,
0x08, 0x10, 0x3F, 0x7E, 0x81, 0x02, 0x04, 0x08, 0x00, 0x0F, 0x87, 0xF9,
0xC0, 0x30, 0x0C, 0x01, 0x80, 0x30, 0xFE, 0x1F, 0xC0, 0x68, 0x0D, 0x81,
0x98, 0x31, 0xFE, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x07, 0xFF, 0x01,
0x80, 0xC0, 0x60, 0x30, 0x18, 0x08, 0xFF, 0x66, 0x66, 0x66, 0x66, 0x66,
0xF0, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x3E, 0x81, 0xC1, 0xA1,
0x91, 0x89, 0x85, 0x83, 0xC1, 0xB0, 0x8C, 0x46, 0x21, 0x90, 0x68, 0x18,
0x81, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x81, 0x02, 0x04, 0x0F, 0xE0,
0xC0, 0x3C, 0x03, 0xE0, 0x7A, 0x05, 0xB0, 0xDB, 0x09, 0x90, 0x99, 0x99,
0x89, 0x18, 0xB1, 0x8F, 0x18, 0x61, 0x86, 0x10, 0xC0, 0xF0, 0x3E, 0x0E,
0xC3, 0xB0, 0xE6, 0x38, 0x8E, 0x33, 0x86, 0xE0, 0xB8, 0x3E, 0x07, 0x80,
0xC0, 0x0F, 0x03, 0xFC, 0x60, 0x64, 0x06, 0xC0, 0x2C, 0x03, 0xC0, 0x3C,
0x03, 0xC0, 0x3C, 0x02, 0x60, 0x63, 0x0C, 0x1F, 0x80, 0xF0, 0xFE, 0x82,
0x83, 0x83, 0x83, 0x86, 0xFC, 0x80, 0x80, 0x80, 0x80, 0x80, 0x0F, 0x03,
0xFC, 0x60, 0x64, 0x06, 0xC0, 0x2C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C,
0x02, 0x60, 0x63, 0x0C, 0x1F, 0x80, 0x18, 0x01, 0xC0, 0x0E, 0xF0, 0x7F,
0x20, 0xD0, 0x68, 0x34, 0x13, 0xF9, 0xF0, 0x8C, 0x43, 0x21, 0x90, 0x68,
0x18, 0x3D, 0x7E, 0xC0, 0xC0, 0xC0, 0x60, 0x3C, 0x0E, 0x03, 0x03, 0x03,
0x86, 0xFC, 0xFF, 0xFF, 0xF0, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30,
0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x00, 0x80, 0xC0, 0x60, 0x30, 0x18,
0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, 0x70, 0x7C, 0x63, 0xE0, 0xC0, 0x78,
0x09, 0x03, 0x30, 0x46, 0x18, 0x43, 0x0C, 0x40, 0x98, 0x1B, 0x03, 0x40,
0x38, 0x07, 0x00, 0xC0, 0xC1, 0x83, 0x41, 0x83, 0x41, 0xC3, 0x63, 0xC2,
0x63, 0x46, 0x22, 0x46, 0x22, 0x66, 0x36, 0x24, 0x34, 0x2C, 0x14, 0x3C,
0x14, 0x38, 0x1C, 0x18, 0x18, 0x18, 0xC0, 0xD8, 0x22, 0x18, 0xCC, 0x1A,
0x03, 0x80, 0xC0, 0x38, 0x1A, 0x0C, 0xC2, 0x19, 0x82, 0xC0, 0xC0, 0xC0,
0xD0, 0x26, 0x18, 0x84, 0x33, 0x04, 0x81, 0xE0, 0x30, 0x0C, 0x03, 0x00,
0xC0, 0x30, 0x0C, 0x00, 0xFF, 0xFF, 0x03, 0x06, 0x04, 0x0C, 0x18, 0x10,
0x30, 0x60, 0x40, 0xC0, 0xFF, 0xFF, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
0xCF, 0xC1, 0x04, 0x18, 0x20, 0xC3, 0x04, 0x18, 0x20, 0x83, 0x04, 0xFF,
0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x1F, 0x10, 0x18, 0x38, 0x24, 0x64,
0x42, 0x42, 0x81, 0xFF, 0xD9, 0x80, 0x18, 0xFC, 0x08, 0x11, 0xEE, 0x70,
0xC1, 0xC6, 0xF4, 0xC0, 0x60, 0x30, 0x19, 0x8F, 0xF7, 0x0B, 0x07, 0x83,
0xC1, 0xE0, 0xF0, 0x7C, 0x6D, 0xE0, 0x1C, 0xFD, 0x06, 0x0C, 0x18, 0x30,
0x60, 0x62, 0x7C, 0x01, 0x01, 0x01, 0x19, 0x7F, 0xC3, 0xC1, 0xC1, 0xC1,
0xC1, 0xC1, 0x63, 0x3D, 0x18, 0x7E, 0x43, 0xC3, 0xFF, 0xFF, 0xC0, 0xC0,
0x61, 0x3F, 0x1E, 0x60, 0x81, 0x0F, 0xC4, 0x08, 0x10, 0x20, 0x40, 0x81,
0x02, 0x00, 0x18, 0x7F, 0x43, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0x63, 0x3D,
0x01, 0x01, 0x43, 0x7E, 0xC0, 0xC0, 0xC0, 0xCC, 0xFF, 0xE1, 0xC1, 0xC1,
0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xF0, 0xFF, 0xFF, 0xC0, 0x33, 0x00, 0x33,
0x33, 0x33, 0x33, 0x33, 0x33, 0xE0, 0xC0, 0xC0, 0xC0, 0xC1, 0xC6, 0xCC,
0xD8, 0xD0, 0xF8, 0xCC, 0xC4, 0xC6, 0xC3, 0xFF, 0xFF, 0xFF, 0xC0, 0x0C,
0x33, 0xFB, 0xFE, 0x38, 0xF0, 0xC3, 0xC1, 0x07, 0x04, 0x1C, 0x10, 0x70,
0x41, 0xC1, 0x07, 0x04, 0x10, 0x0C, 0xFF, 0xE1, 0xC1, 0xC1, 0xC1, 0xC1,
0xC1, 0xC1, 0xC1, 0x18, 0x3F, 0x10, 0xD8, 0x3C, 0x1E, 0x0F, 0x07, 0x82,
0x63, 0x1F, 0x00, 0x0C, 0x7F, 0xB8, 0x58, 0x3C, 0x1E, 0x0F, 0x07, 0x83,
0xE3, 0x6F, 0x30, 0x18, 0x0C, 0x06, 0x00, 0x18, 0x7F, 0xC3, 0xC1, 0xC1,
0xC1, 0xC1, 0xC1, 0x63, 0x3D, 0x01, 0x01, 0x01, 0x01, 0x0F, 0x7E, 0x30,
0xC3, 0x0C, 0x30, 0xC3, 0x00, 0x39, 0xFA, 0x06, 0x07, 0x07, 0x81, 0x83,
0x85, 0xF8, 0x20, 0x86, 0x3F, 0x20, 0x82, 0x08, 0x20, 0x83, 0x0F, 0xC1,
0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0x63, 0x7D, 0xC1, 0xA0, 0x98, 0xCC,
0x42, 0x21, 0xB0, 0x50, 0x28, 0x1C, 0x00, 0xC3, 0x0D, 0x1C, 0x26, 0x79,
0x99, 0x26, 0x24, 0x90, 0xB3, 0x43, 0x8F, 0x0E, 0x18, 0x18, 0x60, 0xE3,
0x99, 0x05, 0x83, 0x80, 0xC0, 0xB0, 0xC8, 0xC6, 0xC1, 0x80, 0xC1, 0xA0,
0x98, 0xC4, 0x43, 0x21, 0xB0, 0x50, 0x38, 0x0C, 0x04, 0x06, 0x02, 0x0F,
0x00, 0xFC, 0x18, 0x20, 0xC3, 0x0C, 0x10, 0x60, 0xFE, 0x04, 0x71, 0x04,
0x10, 0x41, 0x3C, 0xE0, 0xC1, 0x04, 0x10, 0x41, 0xC3, 0xFF, 0xFF, 0xFF,
0xFF, 0xC0, 0x87, 0x18, 0x42, 0x10, 0x87, 0x39, 0x08, 0x42, 0x33, 0x90,
0xF9, 0x8F, 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C,
0x1F, 0xE0, 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C,
0x1F, 0xE0, 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C,
0x1F, 0xE0, 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C,
0x1F, 0xE0, 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C,
0x1F, 0xE0, 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C,
0x1F, 0xE0, 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C,
0x1F, 0xE0, 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C,
0x1F, 0xE0, 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C,
0x1F, 0xE0, 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C,
0x1F, 0xE0, 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C,
0x1F, 0xE0, 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C,
0x1F, 0xE0, 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C,
0x1F, 0xE0, 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C,
0x1F, 0xE0, 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C,
0x1F, 0xE0, 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C,
0x1F, 0xE0, 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C,
0x1F, 0xE0, 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C,
0x1F, 0xE0, 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C,
0x1F, 0xE0, 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C,
0x1F, 0xE0, 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C,
0x1F, 0xE0, 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C,
0x1F, 0xE0, 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C,
0x1F, 0xE0, 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C,
0x1F, 0xE0, 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C,
0x1F, 0xE0, 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C,
0x1F, 0xE0, 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C,
0x1F, 0xE0, 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C,
0x1F, 0xE0, 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C,
0x1F, 0xE0, 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C,
0x1F, 0xE0, 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C,
0x1F, 0xE0, 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C,
0x1F, 0xE0, 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C,
0x1F, 0xE0, 0x00, 0x74, 0x55, 0x57, 0xC0, 0x10, 0x21, 0xFE, 0x0C, 0x10,
0x20, 0x40, 0x81, 0x81, 0xF0, 0x81, 0x00, 0x1E, 0x3F, 0x60, 0x60, 0x60,
0x60, 0xFC, 0x60, 0x60, 0x60, 0x60, 0xC0, 0xFF, 0xDB, 0x7E, 0x42, 0x43,
0x42, 0x62, 0xFF, 0x81, 0xC0, 0xD0, 0x26, 0x18, 0x84, 0x33, 0x04, 0x81,
0xE0, 0xFC, 0x0C, 0x03, 0x03, 0xF0, 0x30, 0x0C, 0x00, 0xFF, 0xF0, 0x03,
0xFF, 0xC0, 0x7F, 0x83, 0x03, 0x85, 0xD8, 0xF0, 0xB3, 0x3C, 0x1C, 0x1C,
0x3F, 0xC0, 0x9C, 0x80, 0x0F, 0x01, 0x86, 0x18, 0x08, 0x9F, 0x28, 0x81,
0x4C, 0x06, 0x60, 0x33, 0x01, 0x98, 0x0E, 0x6C, 0x91, 0xCC, 0x60, 0xC1,
0xF8, 0x00, 0x69, 0x1F, 0x9F, 0x32, 0xCB, 0x34, 0xC4, 0xCC, 0xCC, 0x80,
0xFF, 0x01, 0x01, 0x01, 0x01, 0xF0, 0x0F, 0x01, 0x86, 0x18, 0x08, 0x9E,
0x28, 0x99, 0x44, 0x46, 0x3C, 0x31, 0xE1, 0x89, 0x0E, 0x4C, 0x92, 0x3C,
0x60, 0xC1, 0xF8, 0x00, 0xFF, 0xFF, 0xC0, 0x73, 0x28, 0xE3, 0xC8, 0xC0,
0x18, 0x18, 0x18, 0x18, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x00, 0xFF, 0x7A,
0x42, 0x11, 0x11, 0x1F, 0xF9, 0x20, 0x9E, 0x08, 0x38, 0x9C, 0x6F, 0x00,
0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xE3, 0xFD, 0xC0, 0xC0, 0xC0,
0xC0, 0x7F, 0xFE, 0x7F, 0x3F, 0x9F, 0xCF, 0xE7, 0xF2, 0xF9, 0x04, 0x82,
0x41, 0x20, 0x90, 0x48, 0x24, 0x12, 0xF0, 0x66, 0x3E, 0x7E, 0x92, 0x49,
0x74, 0xE3, 0x18, 0xF8, 0x98, 0x99, 0x99, 0x96, 0x59, 0xA6, 0x00, 0x20,
0x4E, 0x0C, 0xA0, 0x82, 0x10, 0x23, 0x02, 0x22, 0x26, 0x62, 0x4A, 0x09,
0xA1, 0x92, 0x13, 0xF2, 0x02, 0x60, 0x20, 0x10, 0x63, 0x82, 0x34, 0x20,
0x23, 0x01, 0x10, 0x09, 0xB8, 0x4A, 0x62, 0x81, 0x0C, 0x18, 0x41, 0x86,
0x18, 0x21, 0x82, 0x0F, 0x80, 0x70, 0x13, 0x20, 0x80, 0x86, 0x06, 0x10,
0x78, 0xC0, 0x32, 0x68, 0xD3, 0xBE, 0xCE, 0x02, 0x58, 0x13, 0x60, 0x4F,
0xC2, 0x06, 0x18, 0x18, 0x08, 0x38, 0x20, 0x00, 0x81, 0x06, 0x18, 0x61,
0x83, 0x02, 0x17, 0xE0, 0x0C, 0x01, 0x80, 0x18, 0x00, 0x00, 0x40, 0x1C,
0x02, 0x80, 0x58, 0x1B, 0x02, 0x20, 0x46, 0x18, 0x43, 0xFC, 0xC1, 0x98,
0x12, 0x03, 0xC0, 0x60, 0x03, 0x00, 0x60, 0x18, 0x00, 0x00, 0x40, 0x1C,
0x02, 0x80, 0x58, 0x1B, 0x02, 0x20, 0x46, 0x18, 0x43, 0xFC, 0xC1, 0x98,
0x12, 0x03, 0xC0, 0x60, 0x04, 0x01, 0xC0, 0x6C, 0x00, 0x40, 0x40, 0x1C,
0x02, 0x80, 0x58, 0x1B, 0x02, 0x20, 0x46, 0x18, 0x43, 0xFC, 0xC1, 0x98,
0x12, 0x03, 0xC0, 0x60, 0x1C, 0x82, 0xF0, 0x00, 0x02, 0x00, 0xE0, 0x14,
0x02, 0xC0, 0xD8, 0x11, 0x02, 0x30, 0xC2, 0x1F, 0xE6, 0x0C, 0xC0, 0x90,
0x1E, 0x03, 0x09, 0x81, 0x20, 0x00, 0x02, 0x00, 0xE0, 0x14, 0x02, 0xC0,
0xD8, 0x11, 0x02, 0x30, 0xC2, 0x1F, 0xE6, 0x0C, 0xC0, 0x90, 0x1E, 0x03,
0x0E, 0x01, 0x20, 0x38, 0x07, 0x00, 0xA0, 0x16, 0x06, 0xC0, 0x88, 0x11,
0x86, 0x10, 0xFF, 0x30, 0x66, 0x04, 0x80, 0xF0, 0x18, 0x03, 0xFC, 0x1F,
0xF0, 0x6C, 0x03, 0x30, 0x0C, 0xC0, 0x23, 0x01, 0x8F, 0xC4, 0x30, 0x3F,
0xC0, 0x83, 0x06, 0x0C, 0x10, 0x30, 0xC0, 0xFC, 0x0F, 0x0F, 0xF6, 0x01,
0x80, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x06, 0x00, 0xC2, 0x1F,
0x81, 0x00, 0x60, 0x0C, 0x0E, 0x00, 0x60, 0xC0, 0xC0, 0x0F, 0xFF, 0xE0,
0x40, 0x81, 0x03, 0xF4, 0x08, 0x10, 0x20, 0x40, 0xFE, 0x0C, 0x18, 0x60,
0x0F, 0xFF, 0xE0, 0x40, 0x81, 0x03, 0xF4, 0x08, 0x10, 0x20, 0x40, 0xFE,
0x10, 0x71, 0xB0, 0x1F, 0xFF, 0xE0, 0x40, 0x81, 0x03, 0xF4, 0x08, 0x10,
0x20, 0x40, 0xFE, 0x4C, 0x90, 0x07, 0xFF, 0xF0, 0x20, 0x40, 0x81, 0xFA,
0x04, 0x08, 0x10, 0x20, 0x7F, 0xCC, 0x60, 0xFF, 0x66, 0x66, 0x66, 0x66,
0x66, 0xF0, 0x33, 0x60, 0xFF, 0x66, 0x66, 0x66, 0x66, 0x66, 0xF0, 0x21,
0xCD, 0x81, 0x79, 0xE3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x78,
0x9C, 0x81, 0xEF, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x31, 0x9E, 0x7C, 0x0F,
0xE1, 0x07, 0x20, 0x64, 0x06, 0x80, 0xFF, 0x1A, 0x03, 0x40, 0x68, 0x19,
0x03, 0x21, 0xC7, 0xE0, 0x39, 0x0B, 0xC0, 0x03, 0x03, 0xC0, 0xF8, 0x3B,
0x0E, 0xC3, 0x98, 0xE2, 0x38, 0xCE, 0x1B, 0x82, 0xE0, 0xF8, 0x1E, 0x03,
0x0C, 0x00, 0xC0, 0x06, 0x00, 0x00, 0x0F, 0x03, 0xFC, 0x60, 0x64, 0x06,
0xC0, 0x2C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x02, 0x60, 0x63, 0x0C,
0x1F, 0x80, 0x03, 0x00, 0x30, 0x06, 0x00, 0x00, 0x0F, 0x03, 0xFC, 0x60,
0x64, 0x06, 0xC0, 0x2C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x02, 0x60,
0x63, 0x0C, 0x1F, 0x80, 0x04, 0x00, 0xE0, 0x1B, 0x00, 0x08, 0x0F, 0x03,
0xFC, 0x60, 0x64, 0x06, 0xC0, 0x2C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C,
0x02, 0x60, 0x63, 0x0C, 0x1F, 0x80, 0x1C, 0x81, 0x78, 0x00, 0x00, 0xF0,
0x3F, 0xC6, 0x06, 0x40, 0x6C, 0x02, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03,
0xC0, 0x26, 0x06, 0x30, 0xC1, 0xF8, 0x09, 0x80, 0x90, 0x00, 0x00, 0xF0,
0x3F, 0xC6, 0x06, 0x40, 0x6C, 0x02, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03,
0xC0, 0x26, 0x06, 0x30, 0xC1, 0xF8, 0xC3, 0x66, 0x3C, 0x18, 0x3C, 0x66,
0xC3, 0x41, 0x0F, 0x63, 0xFC, 0x60, 0xE4, 0x1E, 0xC1, 0x2C, 0x23, 0xC6,
0x3C, 0xC3, 0xC8, 0x3D, 0x82, 0x70, 0x63, 0x0C, 0x7F, 0x84, 0x00, 0x30,
0x18, 0x06, 0x00, 0x08, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, 0x60, 0x30,
0x18, 0x0C, 0x07, 0x07, 0xC6, 0x3E, 0x00, 0x06, 0x03, 0x03, 0x00, 0x08,
0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x07, 0x07,
0xC6, 0x3E, 0x00, 0x08, 0x0E, 0x0D, 0x80, 0x28, 0x0C, 0x06, 0x03, 0x01,
0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x07, 0x07, 0xC6, 0x3E, 0x00, 0x26,
0x12, 0x00, 0x10, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, 0x60, 0x30,
0x18, 0x0E, 0x0F, 0x8C, 0x7C, 0x06, 0x01, 0x80, 0xC0, 0x00, 0xC0, 0xD0,
0x26, 0x18, 0x84, 0x33, 0x04, 0x81, 0xE0, 0x30, 0x0C, 0x03, 0x00, 0xC0,
0x30, 0x0C, 0x00, 0x80, 0x80, 0x80, 0xFE, 0x86, 0x83, 0x83, 0x83, 0x86,
0xFC, 0x80, 0x80, 0x80, 0x7F, 0x21, 0x90, 0x58, 0x6C, 0x66, 0x63, 0x31,
0x8E, 0xC3, 0xE0, 0x70, 0x3A, 0x3D, 0xF0, 0x30, 0x60, 0x60, 0x01, 0x8F,
0xC0, 0x81, 0x1E, 0xE7, 0x0C, 0x1C, 0x6F, 0x40, 0x0C, 0x18, 0x60, 0x01,
0x8F, 0xC0, 0x81, 0x1E, 0xE7, 0x0C, 0x1C, 0x6F, 0x40, 0x10, 0x71, 0xB0,
0x11, 0x8F, 0xC0, 0x81, 0x1E, 0xE7, 0x0C, 0x1C, 0x6F, 0x40, 0x72, 0xBC,
0x00, 0xC7, 0xE0, 0x40, 0x8F, 0x73, 0x86, 0x0E, 0x37, 0xA0, 0x26, 0x48,
0x00, 0xC7, 0xE0, 0x40, 0x8F, 0x73, 0x86, 0x0E, 0x37, 0xA0, 0x38, 0x48,
0xE0, 0x01, 0x8F, 0xC0, 0x81, 0x1E, 0xE7, 0x0C, 0x1C, 0x6F, 0x40, 0x18,
0x63, 0xFF, 0x81, 0xC6, 0x04, 0x11, 0xFF, 0xB9, 0xFF, 0x08, 0x10, 0xE0,
0xC5, 0x8B, 0xC7, 0xC0, 0x1C, 0xFD, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x62,
0x7C, 0x40, 0xC0, 0xC7, 0x00, 0x30, 0x30, 0x18, 0x00, 0x18, 0x7E, 0x43,
0xC3, 0xFF, 0xFF, 0xC0, 0xC0, 0x61, 0x3F, 0x0C, 0x0C, 0x18, 0x00, 0x18,
0x7E, 0x43, 0xC3, 0xFF, 0xFF, 0xC0, 0xC0, 0x61, 0x3F, 0x10, 0x38, 0x6C,
0x02, 0x18, 0x7E, 0x43, 0xC3, 0xFF, 0xFF, 0xC0, 0xC0, 0x61, 0x3F, 0x26,
0x24, 0x00, 0x18, 0x7E, 0x43, 0xC3, 0xFF, 0xFF, 0xC0, 0xC0, 0x61, 0x3F,
0xD9, 0x80, 0xDB, 0x6D, 0xB6, 0xC0, 0x33, 0x60, 0x0C, 0xCC, 0xCC, 0xCC,
0xCC, 0x21, 0xCD, 0x81, 0x01, 0x86, 0x18, 0x61, 0x86, 0x18, 0x61, 0x80,
0x9C, 0x80, 0x06, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x00, 0x3B, 0x06, 0x0D,
0x84, 0x60, 0x33, 0xF9, 0x0F, 0x83, 0xC1, 0xE0, 0xF0, 0x4C, 0x63, 0xE0,
0x39, 0x2F, 0x00, 0x0C, 0xFF, 0xE1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1,
0xC1, 0x30, 0x18, 0x06, 0x00, 0x01, 0x83, 0xF1, 0x0D, 0x83, 0xC1, 0xE0,
0xF0, 0x78, 0x26, 0x31, 0xF0, 0x06, 0x03, 0x03, 0x00, 0x01, 0x83, 0xF1,
0x0D, 0x83, 0xC1, 0xE0, 0xF0, 0x78, 0x26, 0x31, 0xF0, 0x08, 0x0E, 0x0D,
0x80, 0x21, 0x83, 0xF1, 0x0D, 0x83, 0xC1, 0xE0, 0xF0, 0x78, 0x26, 0x31,
0xF0, 0x72, 0x2F, 0x00, 0x03, 0x07, 0xE2, 0x1B, 0x07, 0x83, 0xC1, 0xE0,
0xF0, 0x4C, 0x63, 0xE0, 0x26, 0x12, 0x00, 0x03, 0x07, 0xE2, 0x1B, 0x07,
0x83, 0xC1, 0xE0, 0xF0, 0x4C, 0x63, 0xE0, 0x18, 0x18, 0x00, 0xFF, 0x00,
0x00, 0x18, 0x18, 0x19, 0x3F, 0x90, 0xD8, 0xAC, 0xDE, 0xCF, 0x47, 0xE2,
0x63, 0x7F, 0x10, 0x00, 0x30, 0x30, 0x18, 0x00, 0x00, 0xC1, 0xC1, 0xC1,
0xC1, 0xC1, 0xC1, 0xC1, 0x63, 0x7D, 0x06, 0x06, 0x0C, 0x00, 0x00, 0xC1,
0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0x63, 0x7D, 0x08, 0x1C, 0x36, 0x01,
0x00, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0x63, 0x7D, 0x26, 0x24,
0x00, 0x00, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0x63, 0x7D, 0x06,
0x03, 0x03, 0x00, 0x00, 0x06, 0x0D, 0x04, 0xC6, 0x22, 0x19, 0x0D, 0x82,
0x81, 0xC0, 0x60, 0x20, 0x30, 0x10, 0x78, 0x00, 0xC0, 0x60, 0x30, 0x19,
0x8F, 0xF7, 0x0B, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x7C, 0x6D, 0xE6, 0x03,
0x01, 0x80, 0xC0, 0x00, 0x26, 0x12, 0x00, 0x00, 0x0C, 0x1A, 0x09, 0x8C,
0x44, 0x32, 0x1B, 0x05, 0x03, 0x80, 0xC0, 0x40, 0x60, 0x20, 0xF0, 0x00,
0x1F, 0x00, 0x00, 0x10, 0x07, 0x00, 0xA0, 0x16, 0x06, 0xC0, 0x88, 0x11,
0x86, 0x10, 0xFF, 0x30, 0x66, 0x04, 0x80, 0xF0, 0x18, 0x3E, 0x00, 0x63,
0xF0, 0x20, 0x47, 0xB9, 0xC3, 0x07, 0x1B, 0xD0, 0x11, 0x03, 0xE0, 0x00,
0x02, 0x00, 0xE0, 0x14, 0x02, 0xC0, 0xD8, 0x11, 0x02, 0x30, 0xC2, 0x1F,
0xE6, 0x0C, 0xC0, 0x90, 0x1E, 0x03, 0x22, 0x7C, 0x00, 0xC7, 0xE0, 0x40,
0x8F, 0x73, 0x86, 0x0E, 0x37, 0xA0, 0x04, 0x00, 0xE0, 0x0A, 0x00, 0xB0,
0x1B, 0x01, 0x10, 0x11, 0x83, 0x08, 0x3F, 0xC6, 0x0C, 0x60, 0x44, 0x06,
0xC0, 0x60, 0x06, 0x00, 0x40, 0x04, 0x00, 0x70, 0x18, 0x7E, 0x02, 0x02,
0x1E, 0x72, 0xC2, 0x82, 0xC6, 0x7A, 0x06, 0x04, 0x04, 0x07, 0x03, 0x00,
0xC0, 0x60, 0x00, 0x0F, 0x0F, 0xF6, 0x01, 0x80, 0xC0, 0x30, 0x0C, 0x03,
0x00, 0xC0, 0x30, 0x06, 0x00, 0xC2, 0x1F, 0x80, 0x0C, 0x18, 0x60, 0x01,
0xCF, 0xD0, 0x60, 0xC1, 0x83, 0x06, 0x06, 0x27, 0xC0, 0x04, 0x03, 0x81,
0xB0, 0x02, 0x0F, 0x0F, 0xF6, 0x01, 0x80, 0xC0, 0x30, 0x0C, 0x03, 0x00,
0xC0, 0x30, 0x06, 0x00, 0xC2, 0x1F, 0x80, 0x10, 0x71, 0xB0, 0x11, 0xCF,
0xD0, 0x60, 0xC1, 0x83, 0x06, 0x06, 0x27, 0xC0, 0x04, 0x01, 0x80, 0x00,
0x3C, 0x3F, 0xD8, 0x06, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0,
0x18, 0x03, 0x08, 0x7E, 0x10, 0x30, 0x00, 0xE7, 0xE8, 0x30, 0x60, 0xC1,
0x83, 0x03, 0x13, 0xE0, 0x10, 0x86, 0xC0, 0xE0, 0x00, 0x0F, 0x0F, 0xF6,
0x01, 0x80, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x06, 0x00, 0xC2,
0x1F, 0x80, 0x42, 0xD8, 0xE0, 0x01, 0xCF, 0xD0, 0x60, 0xC1, 0x83, 0x06,
0x06, 0x27, 0xC0, 0x21, 0x0D, 0x81, 0xC0, 0x00, 0xF8, 0x3F, 0xC8, 0x3A,
0x06, 0x80, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE0, 0x68, 0x1A, 0x1C, 0xFC,
0x00, 0x01, 0x60, 0x28, 0x04, 0x0C, 0x87, 0xF1, 0x86, 0x30, 0x46, 0x08,
0xC1, 0x18, 0x23, 0x04, 0x31, 0x83, 0xD0, 0x7C, 0x0F, 0xE1, 0x07, 0x20,
0x64, 0x06, 0x80, 0xFF, 0x1A, 0x03, 0x40, 0x68, 0x19, 0x03, 0x21, 0xC7,
0xE0, 0x01, 0x07, 0xF0, 0x10, 0x64, 0x7F, 0x30, 0xCC, 0x13, 0x04, 0xC1,
0x30, 0x4C, 0x11, 0x8C, 0x3D, 0x00, 0x7C, 0x03, 0xFF, 0xF8, 0x10, 0x20,
0x40, 0xFD, 0x02, 0x04, 0x08, 0x10, 0x3F, 0x80, 0x3E, 0x00, 0x18, 0x7E,
0x43, 0xC3, 0xFF, 0xFF, 0xC0, 0xC0, 0x61, 0x3F, 0x44, 0xF8, 0x07, 0xFF,
0xF0, 0x20, 0x40, 0x81, 0xFA, 0x04, 0x08, 0x10, 0x20, 0x7F, 0x22, 0x3E,
0x00, 0x18, 0x7E, 0x43, 0xC3, 0xFF, 0xFF, 0xC0, 0xC0, 0x61, 0x3F, 0x10,
0x30, 0x07, 0xFF, 0xF0, 0x20, 0x40, 0x81, 0xFA, 0x04, 0x08, 0x10, 0x20,
0x7F, 0x10, 0x18, 0x00, 0x18, 0x7E, 0x43, 0xC3, 0xFF, 0xFF, 0xC0, 0xC0,
0x61, 0x3F, 0xFF, 0xFE, 0x04, 0x08, 0x10, 0x3F, 0x40, 0x81, 0x02, 0x04,
0x0F, 0xE1, 0x82, 0x04, 0x0E, 0x18, 0x7E, 0x43, 0xC3, 0xFF, 0xFF, 0xC0,
0xC0, 0x61, 0x3F, 0x06, 0x04, 0x04, 0x07, 0x42, 0xD8, 0xE0, 0x0F, 0xFF,
0xE0, 0x40, 0x81, 0x03, 0xF4, 0x08, 0x10, 0x20, 0x40, 0xFE, 0x42, 0x6C,
0x38, 0x00, 0x18, 0x7E, 0x43, 0xC3, 0xFF, 0xFF, 0xC0, 0xC0, 0x61, 0x3F,
0x04, 0x01, 0xC0, 0x6C, 0x00, 0x40, 0xF8, 0x7F, 0x9C, 0x03, 0x00, 0xC0,
0x18, 0x03, 0x0F, 0xE1, 0xFC, 0x06, 0x80, 0xD8, 0x19, 0x83, 0x1F, 0xE0,
0x08, 0x1C, 0x36, 0x01, 0x18, 0x7F, 0x43, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1,
0x63, 0x3D, 0x01, 0x01, 0x43, 0x7E, 0x08, 0x81, 0xF0, 0x00, 0x07, 0xC3,
0xFC, 0xE0, 0x18, 0x06, 0x00, 0xC0, 0x18, 0x7F, 0x0F, 0xE0, 0x34, 0x06,
0xC0, 0xCC, 0x18, 0xFF, 0x22, 0x3E, 0x00, 0x18, 0x7F, 0x43, 0xC1, 0xC1,
0xC1, 0xC1, 0xC1, 0x63, 0x3D, 0x01, 0x01, 0x43, 0x7E, 0x02, 0x00, 0x60,
0x00, 0x07, 0xC3, 0xFC, 0xE0, 0x18, 0x06, 0x00, 0xC0, 0x18, 0x7F, 0x0F,
0xE0, 0x34, 0x06, 0xC0, 0xCC, 0x18, 0xFF, 0x08, 0x0C, 0x00, 0x18, 0x7F,
0x43, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0x63, 0x3D, 0x01, 0x01, 0x43, 0x7E,
0x0F, 0x87, 0xF9, 0xC0, 0x30, 0x0C, 0x01, 0x80, 0x30, 0xFE, 0x1F, 0xC0,
0x68, 0x0D, 0x81, 0x98, 0x31, 0xFE, 0x00, 0x00, 0x80, 0x30, 0x04, 0x00,
0x08, 0x08, 0x18, 0x00, 0x18, 0x7F, 0x43, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1,
0x63, 0x3D, 0x01, 0x01, 0x43, 0x7E, 0x08, 0x0E, 0x0D, 0x80, 0x28, 0x0C,
0x06, 0x03, 0x01, 0x80, 0xC0, 0x7F, 0xF0, 0x18, 0x0C, 0x06, 0x03, 0x01,
0x80, 0x80, 0x20, 0x38, 0x36, 0x00, 0x80, 0x03, 0x01, 0x80, 0xC0, 0x66,
0x3F, 0xDC, 0x2C, 0x16, 0x0B, 0x05, 0x82, 0xC1, 0x60, 0xB0, 0x40, 0x20,
0x21, 0x01, 0x08, 0x09, 0xFF, 0xF2, 0x02, 0x10, 0x10, 0xFF, 0x84, 0x04,
0x20, 0x21, 0x01, 0x08, 0x08, 0x40, 0x42, 0x02, 0x00, 0x60, 0x7F, 0x18,
0x0C, 0x06, 0xFB, 0x8D, 0x82, 0xC1, 0x60, 0xB0, 0x58, 0x2C, 0x16, 0x08,
0xE6, 0xF0, 0x1E, 0x78, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x1E,
0xE6, 0xF0, 0x00, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xF8, 0x3D,
0xE6, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x33, 0xC0, 0xF8, 0x00, 0xC6, 0x31,
0x8C, 0x63, 0x18, 0xC0, 0x8F, 0xC1, 0xEF, 0x31, 0x8C, 0x63, 0x18, 0xC6,
0x31, 0x9E, 0x8F, 0xC0, 0x06, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x00, 0xF7,
0x98, 0xC6, 0x31, 0x8C, 0x63, 0x18, 0xCF, 0x18, 0x84, 0x38, 0xD8, 0x0D,
0xB6, 0xDB, 0x6D, 0xA4, 0xE0, 0x46, 0x0F, 0xF6, 0x66, 0x66, 0x66, 0x66,
0x6F, 0xFF, 0xFF, 0xC0, 0xF1, 0xF1, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61,
0x61, 0x61, 0x61, 0x61, 0xF1, 0x01, 0x03, 0x0E, 0xC7, 0x8C, 0x00, 0x0C,
0x78, 0xF1, 0xE3, 0xC7, 0x8F, 0x1E, 0x3C, 0x60, 0xC1, 0x83, 0x1C, 0x10,
0x71, 0xB0, 0x11, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x81, 0x02, 0x04,
0x08, 0x10, 0x20, 0xC7, 0x00, 0x10, 0x71, 0xB0, 0x10, 0x06, 0x0C, 0x18,
0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C, 0x18, 0x31, 0xC0, 0x81, 0xC1, 0xA1,
0x91, 0x89, 0x85, 0x83, 0xC1, 0xB0, 0x8C, 0x46, 0x21, 0x90, 0x68, 0x18,
0x00, 0x20, 0x30, 0x10, 0x00, 0xC0, 0xC0, 0xC0, 0xC1, 0xC6, 0xCC, 0xD8,
0xD0, 0xF8, 0xCC, 0xC4, 0xC6, 0xC3, 0x00, 0x08, 0x18, 0x10, 0xC7, 0xCC,
0xC8, 0xD8, 0xF8, 0xCC, 0xC4, 0xC6, 0xC3, 0x60, 0xC3, 0x00, 0x08, 0x10,
0x20, 0x40, 0x81, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0xFE, 0x33, 0x60,
0x0C, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0x81, 0x02, 0x04, 0x08, 0x10,
0x20, 0x40, 0x81, 0x02, 0x04, 0x0F, 0xE0, 0x04, 0x18, 0x20, 0xFF, 0xFF,
0xFF, 0xC7, 0x80, 0x87, 0x0A, 0x14, 0x08, 0x10, 0x20, 0x40, 0x81, 0x02,
0x04, 0x0F, 0xE0, 0xDE, 0xB1, 0x8C, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x00,
0x81, 0x02, 0x04, 0x08, 0x10, 0x22, 0x46, 0x81, 0x02, 0x04, 0x0F, 0xE0,
0xC6, 0x31, 0x8C, 0x6B, 0x78, 0xC6, 0x31, 0x8C, 0x00, 0x20, 0x10, 0x08,
0x04, 0x02, 0x01, 0x20, 0xE0, 0xE0, 0xE0, 0x10, 0x08, 0x04, 0x03, 0xF8,
0x63, 0x18, 0xC6, 0x3D, 0xDC, 0xE3, 0x18, 0xC6, 0x00, 0x06, 0x01, 0x80,
0xC0, 0x00, 0xC0, 0xF0, 0x3E, 0x0E, 0xC3, 0xB0, 0xE6, 0x38, 0x8E, 0x33,
0x86, 0xE0, 0xB8, 0x3E, 0x07, 0x80, 0xC0, 0x06, 0x06, 0x0C, 0x00, 0x0C,
0xFF, 0xE1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC0, 0xF0, 0x3E,
0x0E, 0xC3, 0xB0, 0xE6, 0x38, 0x8E, 0x33, 0x86, 0xE0, 0xB8, 0x3E, 0x07,
0x80, 0xC0, 0x00, 0x40, 0x30, 0x08, 0x00, 0x0C, 0xFF, 0xE1, 0xC1, 0xC1,
0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0x00, 0x08, 0x18, 0x10, 0x21, 0x0D, 0x81,
0xC0, 0x00, 0xC0, 0xF0, 0x3E, 0x0E, 0xC3, 0xB0, 0xE6, 0x38, 0x8E, 0x33,
0x86, 0xE0, 0xB8, 0x3E, 0x07, 0x80, 0xC0, 0x21, 0x36, 0x1C, 0x00, 0x0C,
0xFF, 0xE1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0x60, 0x18, 0x04,
0x01, 0x0C, 0xBF, 0xCE, 0x13, 0x04, 0xC1, 0x30, 0x4C, 0x13, 0x04, 0xC1,
0x30, 0x40, 0xC0, 0xF0, 0x3E, 0x0E, 0xC3, 0xB0, 0xE6, 0x38, 0xCE, 0x33,
0x86, 0xE0, 0xF8, 0x1E, 0x07, 0x80, 0xC0, 0x30, 0x08, 0x1E, 0x0C, 0xFF,
0xE1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0x01, 0x01, 0x01, 0x07,
0x0F, 0x80, 0x00, 0x0F, 0x03, 0xFC, 0x60, 0x64, 0x06, 0xC0, 0x2C, 0x03,
0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x02, 0x60, 0x63, 0x0C, 0x1F, 0x80, 0x3E,
0x00, 0x06, 0x0F, 0xC4, 0x36, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0x98, 0xC7,
0xC0, 0x11, 0x01, 0xF0, 0x00, 0x00, 0xF0, 0x3F, 0xC6, 0x06, 0x40, 0x6C,
0x02, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x26, 0x06, 0x30, 0xC1,
0xF8, 0x22, 0x1F, 0x00, 0x03, 0x07, 0xE2, 0x1B, 0x07, 0x83, 0xC1, 0xE0,
0xF0, 0x4C, 0x63, 0xE0, 0x0D, 0x80, 0x90, 0x13, 0x00, 0x00, 0x0F, 0x03,
0xFC, 0x60, 0x64, 0x06, 0xC0, 0x2C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C,
0x02, 0x60, 0x63, 0x0C, 0x1F, 0x80, 0x36, 0x12, 0x13, 0x00, 0x01, 0x83,
0xF1, 0x0D, 0x83, 0xC1, 0xE0, 0xF0, 0x78, 0x26, 0x31, 0xF0, 0x0F, 0xFC,
0xFF, 0xF6, 0x08, 0x10, 0x20, 0xC0, 0x83, 0x02, 0x0C, 0x0F, 0xF0, 0x20,
0xC0, 0x83, 0x02, 0x06, 0x08, 0x0C, 0x20, 0x1F, 0xFC, 0x18, 0x30, 0xFD,
0xF9, 0x0E, 0x16, 0x0C, 0x3C, 0x1F, 0xF8, 0x3F, 0xF0, 0x60, 0x60, 0xC0,
0x63, 0xC4, 0x7C, 0xF8, 0x0C, 0x06, 0x06, 0x00, 0x0F, 0x07, 0xF2, 0x0D,
0x06, 0x83, 0x41, 0x3F, 0x9F, 0x08, 0xC4, 0x32, 0x19, 0x06, 0x81, 0x80,
0x0C, 0x31, 0x80, 0x0F, 0x7E, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x00, 0xF0,
0x7F, 0x20, 0xD0, 0x68, 0x34, 0x13, 0xF9, 0xF0, 0x8C, 0x43, 0x21, 0x90,
0x68, 0x18, 0x00, 0x20, 0x30, 0x10, 0x00, 0x0F, 0x7E, 0x30, 0xC3, 0x0C,
0x30, 0xC3, 0x00, 0x10, 0xC2, 0x00, 0x42, 0x36, 0x0E, 0x00, 0x0F, 0x07,
0xF2, 0x0D, 0x06, 0x83, 0x41, 0x3F, 0x9F, 0x08, 0xC4, 0x32, 0x19, 0x06,
0x81, 0x80, 0x42, 0xD8, 0xE0, 0x00, 0xDB, 0xB8, 0x60, 0xC1, 0x83, 0x06,
0x0C, 0x18, 0x00, 0x0C, 0x0C, 0x18, 0x00, 0x3D, 0x7E, 0xC0, 0xC0, 0xC0,
0x60, 0x3C, 0x0E, 0x03, 0x03, 0x03, 0x86, 0xFC, 0x0C, 0x18, 0x60, 0x03,
0x9F, 0xA0, 0x60, 0x70, 0x78, 0x18, 0x38, 0x5F, 0x80, 0x10, 0x38, 0x6C,
0x02, 0x3D, 0x7E, 0xC0, 0xC0, 0xC0, 0x60, 0x3C, 0x0E, 0x03, 0x03, 0x03,
0x86, 0xFC, 0x10, 0x71, 0xB0, 0x13, 0x9F, 0xA0, 0x60, 0x70, 0x78, 0x18,
0x38, 0x5F, 0x80, 0x3D, 0x7E, 0xC0, 0xC0, 0xC0, 0x60, 0x3C, 0x0E, 0x03,
0x03, 0x03, 0x86, 0xFC, 0x10, 0x18, 0x0C, 0x38, 0x39, 0xFA, 0x06, 0x07,
0x07, 0x81, 0x83, 0x85, 0xF8, 0x81, 0x81, 0x8E, 0x00, 0x42, 0x6C, 0x38,
0x00, 0x3D, 0x7E, 0xC0, 0xC0, 0xC0, 0x60, 0x3C, 0x0E, 0x03, 0x03, 0x03,
0x86, 0xFC, 0x42, 0xD8, 0xE0, 0x03, 0x9F, 0xA0, 0x60, 0x70, 0x78, 0x18,
0x38, 0x5F, 0x80, 0xFF, 0xFF, 0xF0, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0,
0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x02, 0x00, 0xC0, 0x18, 0x1C,
0x00, 0x20, 0x86, 0x3F, 0x20, 0x82, 0x08, 0x20, 0x83, 0x0F, 0x10, 0x60,
0xCE, 0x21, 0x0D, 0x81, 0xC0, 0x00, 0xFF, 0xFF, 0xF0, 0xC0, 0x30, 0x0C,
0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x00, 0x01,
0x23, 0x20, 0x60, 0xFC, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x30, 0x3C,
0xFF, 0xFF, 0xF0, 0xC0, 0x30, 0x0C, 0x03, 0x07, 0xF0, 0x30, 0x0C, 0x03,
0x00, 0xC0, 0x30, 0x0C, 0x00, 0x20, 0x86, 0x3F, 0x20, 0x8F, 0xC8, 0x20,
0x83, 0x0F, 0x39, 0x17, 0x80, 0x10, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x80,
0xC0, 0x60, 0x30, 0x18, 0x0E, 0x0F, 0x8C, 0x7C, 0x39, 0x2F, 0x00, 0x00,
0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0x63, 0x7D, 0x3E, 0x00, 0x20,
0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, 0x60, 0x30, 0x1C, 0x1F,
0x18, 0xF8, 0x3E, 0x00, 0x00, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1,
0x63, 0x7D, 0x22, 0x1F, 0x00, 0x10, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x80,
0xC0, 0x60, 0x30, 0x18, 0x0E, 0x0F, 0x8C, 0x7C, 0x22, 0x3E, 0x00, 0x00,
0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0x63, 0x7D, 0x1C, 0x09, 0x07,
0x00, 0x08, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C,
0x07, 0x07, 0xC6, 0x3E, 0x00, 0x1C, 0x12, 0x1C, 0x00, 0x00, 0xC1, 0xC1,
0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0x63, 0x7D, 0x1B, 0x09, 0x09, 0x80, 0x08,
0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x07, 0x07,
0xC6, 0x3E, 0x00, 0x1B, 0x12, 0x26, 0x00, 0x00, 0xC1, 0xC1, 0xC1, 0xC1,
0xC1, 0xC1, 0xC1, 0x63, 0x7D, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06,
0x03, 0x01, 0x80, 0xC0, 0x70, 0x7C, 0x63, 0xE0, 0x10, 0x10, 0x08, 0x07,
0x00, 0xC1, 0x60, 0xB0, 0x58, 0x2C, 0x16, 0x0B, 0x04, 0xC6, 0x7D, 0x01,
0x80, 0x80, 0x40, 0x38, 0x00, 0x80, 0x01, 0xC0, 0x03, 0x60, 0x00, 0x10,
0xC1, 0x83, 0x41, 0x83, 0x41, 0xC3, 0x63, 0xC2, 0x63, 0x46, 0x22, 0x46,
0x22, 0x66, 0x36, 0x24, 0x34, 0x2C, 0x14, 0x3C, 0x14, 0x38, 0x1C, 0x18,
0x18, 0x18, 0x02, 0x00, 0x1C, 0x00, 0xD8, 0x00, 0x10, 0x00, 0x07, 0x0C,
0x24, 0x70, 0x99, 0xE6, 0x64, 0x98, 0x92, 0x42, 0xCD, 0x0E, 0x3C, 0x38,
0x60, 0x61, 0x80, 0x08, 0x07, 0x03, 0x60, 0x04, 0xC0, 0xD0, 0x26, 0x18,
0x84, 0x33, 0x04, 0x81, 0xE0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C,
0x00, 0x08, 0x0E, 0x0D, 0x80, 0x20, 0x06, 0x0D, 0x04, 0xC6, 0x22, 0x19,
0x0D, 0x82, 0x81, 0xC0, 0x60, 0x20, 0x30, 0x10, 0x78, 0x00, 0x13, 0x04,
0x80, 0x03, 0x03, 0x40, 0x98, 0x62, 0x10, 0xCC, 0x12, 0x07, 0x80, 0xC0,
0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x0C, 0x18, 0x00, 0xFF, 0xFF,
0x03, 0x06, 0x04, 0x0C, 0x18, 0x10, 0x30, 0x60, 0x40, 0xC0, 0xFF, 0x0C,
0x18, 0x60, 0x00, 0x1F, 0x83, 0x04, 0x18, 0x61, 0x82, 0x0C, 0x1F, 0xC0,
0x08, 0x0C, 0x00, 0xFF, 0xFF, 0x03, 0x06, 0x04, 0x0C, 0x18, 0x10, 0x30,
0x60, 0x40, 0xC0, 0xFF, 0x10, 0x30, 0x00, 0x0F, 0xC1, 0x82, 0x0C, 0x30,
0xC1, 0x06, 0x0F, 0xE0, 0x42, 0x6C, 0x38, 0x00, 0xFF, 0xFF, 0x03, 0x06,
0x04, 0x0C, 0x18, 0x10, 0x30, 0x60, 0x40, 0xC0, 0xFF, 0x85, 0xB1, 0xC0,
0x00, 0x1F, 0x83, 0x04, 0x18, 0x61, 0x82, 0x0C, 0x1F, 0xC0, 0x7A, 0x11,
0x8C, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x00 };
const GFXglyph NotoSans9pt8bGlyphs[] PROGMEM = {
{ 0, 1, 1, 5, 0, 0 }, // 0x20 ' '
{ 1, 2, 13, 5, 1, -12 }, // 0x21 '!'
{ 5, 5, 5, 7, 1, -12 }, // 0x22 '"'
{ 9, 11, 13, 11, 0, -12 }, // 0x23 '#'
{ 27, 8, 14, 10, 1, -12 }, // 0x24 '$'
{ 41, 13, 13, 15, 1, -12 }, // 0x25 '%'
{ 63, 12, 13, 13, 1, -12 }, // 0x26 '&'
{ 83, 2, 5, 4, 1, -12 }, // 0x27 '''
{ 85, 4, 16, 5, 1, -12 }, // 0x28 '('
{ 93, 4, 16, 5, 1, -12 }, // 0x29 ')'
{ 101, 8, 8, 10, 1, -12 }, // 0x2A '*'
{ 109, 8, 8, 10, 1, -9 }, // 0x2B '+'
{ 117, 2, 4, 5, 1, -1 }, // 0x2C ','
{ 118, 4, 1, 6, 1, -4 }, // 0x2D '-'
{ 119, 2, 2, 5, 1, -1 }, // 0x2E '.'
{ 120, 6, 13, 7, 0, -12 }, // 0x2F '/'
{ 130, 8, 13, 10, 1, -12 }, // 0x30 '0'
{ 143, 4, 13, 10, 2, -12 }, // 0x31 '1'
{ 150, 8, 13, 10, 1, -12 }, // 0x32 '2'
{ 163, 8, 13, 10, 1, -12 }, // 0x33 '3'
{ 176, 10, 13, 10, 0, -12 }, // 0x34 '4'
{ 193, 8, 13, 10, 1, -12 }, // 0x35 '5'
{ 206, 8, 13, 10, 1, -12 }, // 0x36 '6'
{ 219, 8, 13, 10, 1, -12 }, // 0x37 '7'
{ 232, 8, 13, 10, 1, -12 }, // 0x38 '8'
{ 245, 8, 13, 10, 1, -12 }, // 0x39 '9'
{ 258, 2, 10, 5, 1, -9 }, // 0x3A ':'
{ 261, 2, 12, 5, 1, -9 }, // 0x3B ';'
{ 264, 8, 9, 10, 1, -10 }, // 0x3C '<'
{ 273, 8, 5, 10, 1, -8 }, // 0x3D '='
{ 278, 8, 9, 10, 1, -10 }, // 0x3E '>'
{ 287, 7, 13, 8, 0, -12 }, // 0x3F '?'
{ 299, 14, 15, 16, 1, -12 }, // 0x40 '@'
{ 326, 11, 13, 11, 0, -12 }, // 0x41 'A'
{ 344, 9, 13, 11, 2, -12 }, // 0x42 'B'
{ 359, 10, 13, 11, 1, -12 }, // 0x43 'C'
{ 376, 10, 13, 13, 2, -12 }, // 0x44 'D'
{ 393, 7, 13, 10, 2, -12 }, // 0x45 'E'
{ 405, 7, 13, 9, 2, -12 }, // 0x46 'F'
{ 417, 11, 13, 13, 1, -12 }, // 0x47 'G'
{ 435, 9, 13, 13, 2, -12 }, // 0x48 'H'
{ 450, 4, 13, 6, 1, -12 }, // 0x49 'I'
{ 457, 4, 16, 5, -1, -12 }, // 0x4A 'J'
{ 465, 9, 13, 11, 2, -12 }, // 0x4B 'K'
{ 480, 7, 13, 9, 2, -12 }, // 0x4C 'L'
{ 492, 12, 13, 16, 2, -12 }, // 0x4D 'M'
{ 512, 10, 13, 13, 2, -12 }, // 0x4E 'N'
{ 529, 12, 13, 14, 1, -12 }, // 0x4F 'O'
{ 549, 8, 13, 11, 2, -12 }, // 0x50 'P'
{ 562, 12, 16, 14, 1, -12 }, // 0x51 'Q'
{ 586, 9, 13, 11, 2, -12 }, // 0x52 'R'
{ 601, 8, 13, 10, 1, -12 }, // 0x53 'S'
{ 614, 10, 13, 10, 0, -12 }, // 0x54 'T'
{ 631, 9, 13, 13, 2, -12 }, // 0x55 'U'
{ 646, 11, 13, 11, 0, -12 }, // 0x56 'V'
{ 664, 16, 13, 16, 0, -12 }, // 0x57 'W'
{ 690, 10, 13, 10, 0, -12 }, // 0x58 'X'
{ 707, 10, 13, 10, 0, -12 }, // 0x59 'Y'
{ 724, 8, 13, 10, 1, -12 }, // 0x5A 'Z'
{ 737, 4, 16, 6, 1, -12 }, // 0x5B '['
{ 745, 6, 13, 7, 0, -12 }, // 0x5C '\'
{ 755, 4, 16, 6, 0, -12 }, // 0x5D ']'
{ 763, 8, 8, 10, 1, -12 }, // 0x5E '^'
{ 771, 8, 1, 8, 0, 3 }, // 0x5F '_'
{ 772, 3, 3, 5, 1, -13 }, // 0x60 '`'
{ 774, 7, 10, 10, 1, -9 }, // 0x61 'a'
{ 783, 9, 13, 11, 1, -12 }, // 0x62 'b'
{ 798, 7, 10, 8, 1, -9 }, // 0x63 'c'
{ 807, 8, 13, 11, 1, -12 }, // 0x64 'd'
{ 820, 8, 10, 10, 1, -9 }, // 0x65 'e'
{ 830, 7, 13, 6, 0, -12 }, // 0x66 'f'
{ 842, 8, 14, 11, 1, -9 }, // 0x67 'g'
{ 856, 8, 13, 11, 1, -12 }, // 0x68 'h'
{ 869, 2, 13, 5, 1, -12 }, // 0x69 'i'
{ 873, 4, 17, 5, -1, -12 }, // 0x6A 'j'
{ 882, 8, 13, 9, 1, -12 }, // 0x6B 'k'
{ 895, 2, 13, 5, 1, -12 }, // 0x6C 'l'
{ 899, 14, 10, 16, 1, -9 }, // 0x6D 'm'
{ 917, 8, 10, 11, 1, -9 }, // 0x6E 'n'
{ 927, 9, 10, 11, 1, -9 }, // 0x6F 'o'
{ 939, 9, 14, 11, 1, -9 }, // 0x70 'p'
{ 955, 8, 14, 11, 1, -9 }, // 0x71 'q'
{ 969, 6, 10, 7, 1, -9 }, // 0x72 'r'
{ 977, 7, 10, 8, 1, -9 }, // 0x73 's'
{ 986, 6, 12, 6, 0, -11 }, // 0x74 't'
{ 995, 8, 9, 11, 1, -8 }, // 0x75 'u'
{ 1004, 9, 9, 9, 0, -8 }, // 0x76 'v'
{ 1015, 14, 9, 14, 0, -8 }, // 0x77 'w'
{ 1031, 9, 9, 9, 0, -8 }, // 0x78 'x'
{ 1042, 9, 13, 9, 0, -8 }, // 0x79 'y'
{ 1057, 7, 9, 8, 1, -8 }, // 0x7A 'z'
{ 1065, 6, 16, 7, 0, -12 }, // 0x7B '{'
{ 1077, 2, 17, 10, 4, -12 }, // 0x7C '|'
{ 1082, 5, 16, 7, 1, -12 }, // 0x7D '}'
{ 1092, 8, 2, 10, 1, -6 }, // 0x7E '~'
{ 1094, 7, 13, 11, 2, -12 }, // 0x7F
{ 1106, 7, 13, 11, 2, -12 }, // 0x80
{ 1118, 7, 13, 11, 2, -12 }, // 0x81
{ 1130, 7, 13, 11, 2, -12 }, // 0x82
{ 1142, 7, 13, 11, 2, -12 }, // 0x83
{ 1154, 7, 13, 11, 2, -12 }, // 0x84
{ 1166, 7, 13, 11, 2, -12 }, // 0x85
{ 1178, 7, 13, 11, 2, -12 }, // 0x86
{ 1190, 7, 13, 11, 2, -12 }, // 0x87
{ 1202, 7, 13, 11, 2, -12 }, // 0x88
{ 1214, 7, 13, 11, 2, -12 }, // 0x89
{ 1226, 7, 13, 11, 2, -12 }, // 0x8A
{ 1238, 7, 13, 11, 2, -12 }, // 0x8B
{ 1250, 7, 13, 11, 2, -12 }, // 0x8C
{ 1262, 7, 13, 11, 2, -12 }, // 0x8D
{ 1274, 7, 13, 11, 2, -12 }, // 0x8E
{ 1286, 7, 13, 11, 2, -12 }, // 0x8F
{ 1298, 7, 13, 11, 2, -12 }, // 0x90
{ 1310, 7, 13, 11, 2, -12 }, // 0x91
{ 1322, 7, 13, 11, 2, -12 }, // 0x92
{ 1334, 7, 13, 11, 2, -12 }, // 0x93
{ 1346, 7, 13, 11, 2, -12 }, // 0x94
{ 1358, 7, 13, 11, 2, -12 }, // 0x95
{ 1370, 7, 13, 11, 2, -12 }, // 0x96
{ 1382, 7, 13, 11, 2, -12 }, // 0x97
{ 1394, 7, 13, 11, 2, -12 }, // 0x98
{ 1406, 7, 13, 11, 2, -12 }, // 0x99
{ 1418, 7, 13, 11, 2, -12 }, // 0x9A
{ 1430, 7, 13, 11, 2, -12 }, // 0x9B
{ 1442, 7, 13, 11, 2, -12 }, // 0x9C
{ 1454, 7, 13, 11, 2, -12 }, // 0x9D
{ 1466, 7, 13, 11, 2, -12 }, // 0x9E
{ 1478, 7, 13, 11, 2, -12 }, // 0x9F
{ 1490, 1, 1, 5, 0, 0 }, // 0xA0
{ 1491, 2, 13, 5, 1, -9 }, // 0xA1
{ 1495, 7, 13, 10, 2, -12 }, // 0xA2
{ 1507, 8, 13, 10, 1, -12 }, // 0xA3
{ 1520, 8, 8, 10, 1, -9 }, // 0xA4
{ 1528, 10, 13, 10, 0, -12 }, // 0xA5
{ 1545, 2, 17, 10, 4, -12 }, // 0xA6
{ 1550, 7, 13, 9, 1, -12 }, // 0xA7
{ 1562, 5, 2, 10, 3, -12 }, // 0xA8
{ 1564, 13, 13, 15, 1, -12 }, // 0xA9
{ 1586, 4, 6, 6, 1, -12 }, // 0xAA
{ 1589, 7, 7, 9, 1, -7 }, // 0xAB
{ 1596, 8, 5, 10, 1, -6 }, // 0xAC
{ 1601, 4, 1, 6, 1, -4 }, // 0xAD
{ 1602, 13, 13, 15, 1, -12 }, // 0xAE
{ 1624, 9, 2, 9, 0, -14 }, // 0xAF
{ 1627, 6, 6, 8, 1, -12 }, // 0xB0
{ 1632, 8, 11, 10, 1, -10 }, // 0xB1
{ 1643, 5, 8, 6, 0, -14 }, // 0xB2
{ 1648, 6, 8, 6, 0, -14 }, // 0xB3
{ 1654, 3, 3, 5, 1, -13 }, // 0xB4
{ 1656, 8, 13, 11, 1, -8 }, // 0xB5
{ 1669, 9, 15, 12, 1, -12 }, // 0xB6
{ 1686, 2, 2, 5, 1, -6 }, // 0xB7
{ 1687, 4, 4, 4, 0, 1 }, // 0xB8
{ 1689, 3, 8, 6, 1, -14 }, // 0xB9
{ 1692, 5, 6, 7, 1, -12 }, // 0xBA
{ 1696, 7, 7, 9, 1, -7 }, // 0xBB
{ 1703, 12, 13, 13, 1, -12 }, // 0xBC
{ 1723, 13, 13, 14, 0, -12 }, // 0xBD
{ 1745, 14, 13, 14, 0, -12 }, // 0xBE
{ 1768, 7, 13, 8, 0, -9 }, // 0xBF
{ 1780, 11, 17, 11, 0, -16 }, // 0xC0
{ 1804, 11, 17, 11, 0, -16 }, // 0xC1
{ 1828, 11, 17, 11, 0, -16 }, // 0xC2
{ 1852, 11, 16, 11, 0, -15 }, // 0xC3
{ 1874, 11, 16, 11, 0, -15 }, // 0xC4
{ 1896, 11, 15, 11, 0, -14 }, // 0xC5
{ 1917, 14, 13, 16, 0, -12 }, // 0xC6
{ 1940, 10, 17, 11, 1, -12 }, // 0xC7
{ 1962, 7, 17, 10, 2, -16 }, // 0xC8
{ 1977, 7, 17, 10, 2, -16 }, // 0xC9
{ 1992, 7, 17, 10, 2, -16 }, // 0xCA
{ 2007, 7, 16, 10, 2, -15 }, // 0xCB
{ 2021, 4, 17, 6, 1, -16 }, // 0xCC
{ 2030, 4, 17, 6, 1, -16 }, // 0xCD
{ 2039, 6, 17, 6, 0, -16 }, // 0xCE
{ 2052, 5, 16, 6, 1, -15 }, // 0xCF
{ 2062, 11, 13, 13, 1, -12 }, // 0xD0
{ 2080, 10, 16, 13, 2, -15 }, // 0xD1
{ 2100, 12, 17, 14, 1, -16 }, // 0xD2
{ 2126, 12, 17, 14, 1, -16 }, // 0xD3
{ 2152, 12, 17, 14, 1, -16 }, // 0xD4
{ 2178, 12, 16, 14, 1, -15 }, // 0xD5
{ 2202, 12, 16, 14, 1, -15 }, // 0xD6
{ 2226, 8, 8, 10, 1, -9 }, // 0xD7
{ 2234, 12, 14, 14, 1, -12 }, // 0xD8
{ 2255, 9, 17, 13, 2, -16 }, // 0xD9
{ 2275, 9, 17, 13, 2, -16 }, // 0xDA
{ 2295, 9, 17, 13, 2, -16 }, // 0xDB
{ 2315, 9, 16, 13, 2, -15 }, // 0xDC
{ 2333, 10, 17, 10, 0, -16 }, // 0xDD
{ 2355, 8, 13, 11, 2, -12 }, // 0xDE
{ 2368, 9, 13, 11, 1, -12 }, // 0xDF
{ 2383, 7, 14, 10, 1, -13 }, // 0xE0
{ 2396, 7, 14, 10, 1, -13 }, // 0xE1
{ 2409, 7, 14, 10, 1, -13 }, // 0xE2
{ 2422, 7, 13, 10, 1, -12 }, // 0xE3
{ 2434, 7, 13, 10, 1, -12 }, // 0xE4
{ 2446, 7, 14, 10, 1, -13 }, // 0xE5
{ 2459, 13, 10, 15, 1, -9 }, // 0xE6
{ 2476, 7, 14, 8, 1, -9 }, // 0xE7
{ 2489, 8, 14, 10, 1, -13 }, // 0xE8
{ 2503, 8, 14, 10, 1, -13 }, // 0xE9
{ 2517, 8, 14, 10, 1, -13 }, // 0xEA
{ 2531, 8, 13, 10, 1, -12 }, // 0xEB
{ 2544, 3, 14, 5, 0, -13 }, // 0xEC
{ 2550, 4, 14, 5, 1, -13 }, // 0xED
{ 2557, 6, 14, 5, 0, -13 }, // 0xEE
{ 2568, 5, 13, 5, 0, -12 }, // 0xEF
{ 2577, 9, 13, 11, 1, -12 }, // 0xF0
{ 2592, 8, 13, 11, 1, -12 }, // 0xF1
{ 2605, 9, 14, 11, 1, -13 }, // 0xF2
{ 2621, 9, 14, 11, 1, -13 }, // 0xF3
{ 2637, 9, 14, 11, 1, -13 }, // 0xF4
{ 2653, 9, 13, 11, 1, -12 }, // 0xF5
{ 2668, 9, 13, 11, 1, -12 }, // 0xF6
{ 2683, 8, 8, 10, 1, -9 }, // 0xF7
{ 2691, 9, 11, 11, 1, -9 }, // 0xF8
{ 2704, 8, 14, 11, 1, -13 }, // 0xF9
{ 2718, 8, 14, 11, 1, -13 }, // 0xFA
{ 2732, 8, 14, 11, 1, -13 }, // 0xFB
{ 2746, 8, 13, 11, 1, -12 }, // 0xFC
{ 2759, 9, 18, 9, 0, -13 }, // 0xFD
{ 2780, 9, 17, 11, 1, -12 }, // 0xFE
{ 2800, 9, 17, 9, 0, -12 }, // 0xFF
{ 2820, 11, 15, 11, 0, -14 }, // 0x100
{ 2841, 7, 12, 10, 1, -11 }, // 0x101
{ 2852, 11, 16, 11, 0, -15 }, // 0x102
{ 2874, 7, 13, 10, 1, -12 }, // 0x103
{ 2886, 12, 17, 11, 0, -12 }, // 0x104
{ 2912, 8, 14, 10, 1, -9 }, // 0x105
{ 2926, 10, 17, 11, 1, -16 }, // 0x106
{ 2948, 7, 14, 8, 1, -13 }, // 0x107
{ 2961, 10, 17, 11, 1, -16 }, // 0x108
{ 2983, 7, 14, 8, 1, -13 }, // 0x109
{ 2996, 10, 16, 11, 1, -15 }, // 0x10A
{ 3016, 7, 13, 8, 1, -12 }, // 0x10B
{ 3028, 10, 17, 11, 1, -16 }, // 0x10C
{ 3050, 7, 14, 8, 1, -13 }, // 0x10D
{ 3063, 10, 17, 13, 2, -16 }, // 0x10E
{ 3085, 11, 13, 11, 1, -12 }, // 0x10F
{ 3103, 11, 13, 13, 1, -12 }, // 0x110
{ 3121, 10, 13, 11, 1, -12 }, // 0x111
{ 3138, 7, 15, 10, 2, -14 }, // 0x112
{ 3152, 8, 12, 10, 1, -11 }, // 0x113
{ 3164, 7, 16, 10, 2, -15 }, // 0x114
{ 3178, 8, 13, 10, 1, -12 }, // 0x115
{ 3191, 7, 16, 10, 2, -15 }, // 0x116
{ 3205, 8, 13, 10, 1, -12 }, // 0x117
{ 3218, 7, 17, 10, 2, -12 }, // 0x118
{ 3233, 8, 14, 10, 1, -9 }, // 0x119
{ 3247, 7, 17, 10, 2, -16 }, // 0x11A
{ 3262, 8, 14, 10, 1, -13 }, // 0x11B
{ 3276, 11, 17, 13, 1, -16 }, // 0x11C
{ 3300, 8, 18, 11, 1, -13 }, // 0x11D
{ 3318, 11, 16, 13, 1, -15 }, // 0x11E
{ 3340, 8, 17, 11, 1, -12 }, // 0x11F
{ 3357, 11, 16, 13, 1, -15 }, // 0x120
{ 3379, 8, 17, 11, 1, -12 }, // 0x121
{ 3396, 11, 17, 13, 1, -12 }, // 0x122
{ 3420, 8, 18, 11, 1, -13 }, // 0x123
{ 3438, 9, 17, 13, 2, -16 }, // 0x124
{ 3458, 9, 18, 11, 0, -17 }, // 0x125
{ 3479, 13, 13, 13, 0, -12 }, // 0x126
{ 3501, 9, 13, 11, 0, -12 }, // 0x127
{ 3516, 6, 16, 6, 0, -15 }, // 0x128
{ 3528, 6, 13, 5, -1, -12 }, // 0x129
{ 3538, 5, 15, 6, 1, -14 }, // 0x12A
{ 3548, 5, 12, 5, 0, -11 }, // 0x12B
{ 3556, 5, 16, 6, 1, -15 }, // 0x12C
{ 3566, 5, 13, 5, 0, -12 }, // 0x12D
{ 3575, 5, 17, 6, 1, -12 }, // 0x12E
{ 3586, 3, 17, 5, 1, -12 }, // 0x12F
{ 3593, 4, 16, 6, 1, -15 }, // 0x130
{ 3601, 2, 9, 5, 1, -8 }, // 0x131
{ 3604, 8, 16, 11, 1, -12 }, // 0x132
{ 3620, 7, 17, 9, 1, -12 }, // 0x133
{ 3635, 7, 20, 5, -1, -16 }, // 0x134
{ 3653, 7, 18, 5, -1, -13 }, // 0x135
{ 3669, 9, 17, 11, 2, -12 }, // 0x136
{ 3689, 8, 17, 9, 1, -12 }, // 0x137
{ 3706, 8, 9, 9, 1, -8 }, // 0x138
{ 3715, 7, 17, 9, 2, -16 }, // 0x139
{ 3730, 4, 18, 5, 1, -17 }, // 0x13A
{ 3739, 7, 17, 9, 2, -12 }, // 0x13B
{ 3754, 2, 17, 5, 1, -12 }, // 0x13C
{ 3759, 7, 13, 9, 2, -12 }, // 0x13D
{ 3771, 5, 13, 5, 1, -12 }, // 0x13E
{ 3780, 7, 13, 9, 2, -12 }, // 0x13F
{ 3792, 5, 13, 5, 1, -12 }, // 0x140
{ 3801, 9, 13, 9, 0, -12 }, // 0x141
{ 3816, 5, 13, 5, 0, -12 }, // 0x142
{ 3825, 10, 17, 13, 2, -16 }, // 0x143
{ 3847, 8, 14, 11, 1, -13 }, // 0x144
{ 3861, 10, 17, 13, 2, -12 }, // 0x145
{ 3883, 8, 14, 11, 1, -9 }, // 0x146
{ 3897, 10, 17, 13, 2, -16 }, // 0x147
{ 3919, 8, 14, 11, 1, -13 }, // 0x148
{ 3933, 10, 13, 12, 0, -12 }, // 0x149
{ 3950, 10, 16, 13, 2, -12 }, // 0x14A
{ 3970, 8, 14, 11, 1, -9 }, // 0x14B
{ 3984, 12, 15, 14, 1, -14 }, // 0x14C
{ 4007, 9, 12, 11, 1, -11 }, // 0x14D
{ 4021, 12, 16, 14, 1, -15 }, // 0x14E
{ 4045, 9, 13, 11, 1, -12 }, // 0x14F
{ 4060, 12, 17, 14, 1, -16 }, // 0x150
{ 4086, 9, 14, 11, 1, -13 }, // 0x151
{ 4102, 14, 13, 16, 1, -12 }, // 0x152
{ 4125, 15, 10, 17, 1, -9 }, // 0x153
{ 4144, 9, 17, 11, 2, -16 }, // 0x154
{ 4164, 6, 14, 7, 1, -13 }, // 0x155
{ 4175, 9, 17, 11, 2, -12 }, // 0x156
{ 4195, 6, 14, 7, 1, -9 }, // 0x157
{ 4206, 9, 17, 11, 2, -16 }, // 0x158
{ 4226, 7, 14, 7, 1, -13 }, // 0x159
{ 4239, 8, 17, 10, 1, -16 }, // 0x15A
{ 4256, 7, 14, 8, 1, -13 }, // 0x15B
{ 4269, 8, 17, 10, 1, -16 }, // 0x15C
{ 4286, 7, 14, 8, 1, -13 }, // 0x15D
{ 4299, 8, 17, 10, 1, -12 }, // 0x15E
{ 4316, 7, 14, 8, 1, -9 }, // 0x15F
{ 4329, 8, 17, 10, 1, -16 }, // 0x160
{ 4346, 7, 14, 8, 1, -13 }, // 0x161
{ 4359, 10, 17, 10, 0, -12 }, // 0x162
{ 4381, 6, 16, 6, 0, -11 }, // 0x163
{ 4393, 10, 17, 10, 0, -16 }, // 0x164
{ 4415, 8, 13, 6, 0, -12 }, // 0x165
{ 4428, 10, 13, 10, 0, -12 }, // 0x166
{ 4445, 6, 12, 6, 0, -11 }, // 0x167
{ 4454, 9, 16, 13, 2, -15 }, // 0x168
{ 4472, 8, 13, 11, 1, -12 }, // 0x169
{ 4485, 9, 15, 13, 2, -14 }, // 0x16A
{ 4502, 8, 12, 11, 1, -11 }, // 0x16B
{ 4514, 9, 16, 13, 2, -15 }, // 0x16C
{ 4532, 8, 13, 11, 1, -12 }, // 0x16D
{ 4545, 9, 17, 13, 2, -16 }, // 0x16E
{ 4565, 8, 14, 11, 1, -13 }, // 0x16F
{ 4579, 9, 17, 13, 2, -16 }, // 0x170
{ 4599, 8, 14, 11, 1, -13 }, // 0x171
{ 4613, 9, 17, 13, 2, -12 }, // 0x172
{ 4633, 9, 13, 11, 1, -8 }, // 0x173
{ 4648, 16, 17, 16, 0, -16 }, // 0x174
{ 4682, 14, 14, 14, 0, -13 }, // 0x175
{ 4707, 10, 17, 10, 0, -16 }, // 0x176
{ 4729, 9, 18, 9, 0, -13 }, // 0x177
{ 4750, 10, 16, 10, 0, -15 }, // 0x178
{ 4770, 8, 17, 10, 1, -16 }, // 0x179
{ 4787, 7, 14, 8, 1, -13 }, // 0x17A
{ 4800, 8, 16, 10, 1, -15 }, // 0x17B
{ 4816, 7, 13, 8, 1, -12 }, // 0x17C
{ 4828, 8, 17, 10, 1, -16 }, // 0x17D
{ 4845, 7, 14, 8, 1, -13 }, // 0x17E
{ 4858, 5, 13, 6, 1, -12 } }; // 0x17F
const GFXfont NotoSans9pt8b PROGMEM = {
(uint8_t *)NotoSans9pt8bBitmaps,
(GFXglyph *)NotoSans9pt8bGlyphs,
0x20, 0x17F, 24 };
// Approx. 7338 bytes
#endif // NOTOSANS9PT8B_H
+4
View File
@@ -100,6 +100,10 @@ struct TelemetryData {
char apn[40];
char oper[24];
bool mqtt_connected;
// Routing settings (v1.8+)
uint8_t loop_detect; // 0=off, 1=minimal, 2=moderate, 3=strict
uint8_t path_hash_mode; // 0=1-byte, 1=2-byte, 2=3-byte
uint8_t flood_max; // max flood hop count (0-64)
};
// ---------------------------------------------------------------------------
+86
View File
@@ -501,6 +501,51 @@ bool MyMesh::filterRecvFloodPacket(mesh::Packet* pkt) {
} else {
recv_pkt_region = NULL;
}
// --- Loop detection (MeshCore v1.14+) ---
// Walk the packet's path and count how many times our own hash appears.
// If it exceeds the threshold for the configured detection level, drop
// the packet to break routing loops caused by misbehaving nodes.
if (_prefs.loop_detect != LOOP_DETECT_OFF) {
uint8_t hops = pkt->path_len & 0x3F;
uint8_t bph = (pkt->path_len >> 6) + 1; // bytes per hop (1, 2, or 3)
if (hops > 0 && hops * bph <= MAX_PATH_SIZE) {
// Count self-hash appearances in the path
int selfCount = 0;
for (uint8_t i = 0; i < hops; i++) {
if (self_id.isHashMatch(&pkt->path[i * bph], bph)) {
selfCount++;
}
}
// Threshold depends on detection level and path hash size
// 1-byte 2-byte 3-byte
// minimal: 4 2 1
// moderate: 2 1 1
// strict: 1 1 1
int threshold;
switch (_prefs.loop_detect) {
case LOOP_DETECT_MINIMAL:
threshold = (bph == 1) ? 4 : (bph == 2) ? 2 : 1;
break;
case LOOP_DETECT_MODERATE:
threshold = (bph == 1) ? 2 : 1;
break;
case LOOP_DETECT_STRICT:
default:
threshold = 1;
break;
}
if (selfCount >= threshold) {
MESH_DEBUG_PRINTLN("Loop detected: self-hash appears %d times (threshold %d, bph=%d, mode=%d) — dropping",
selfCount, threshold, (int)bph, (int)_prefs.loop_detect);
return true; // Drop the packet
}
}
}
// do normal processing
return false;
}
@@ -802,6 +847,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_prefs.adc_multiplier = 0.0f; // 0.0f means use default board multiplier
_prefs.path_hash_mode = 0; // 1-byte path hashes (legacy default)
_prefs.loop_detect = LOOP_DETECT_OFF; // no loop detection by default
}
void MyMesh::begin(FILESYSTEM *fs) {
@@ -1120,6 +1166,26 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
} else if (n == 2 && strcmp(parts[1], "home") == 0) {
auto home = region_map.getHomeRegion();
sprintf(reply, " home is %s", home ? home->name : "*");
} else if (n >= 3 && strcmp(parts[1], "default") == 0) {
// "region default <name>" — set default scope
auto def = region_map.findByNamePrefix(parts[2]);
if (def) {
region_map.setDefaultRegion(def);
sprintf(reply, " default is now %s", def->name);
} else {
// empty or unrecognised name → clear default scope
region_map.setDefaultRegion(NULL);
strcpy(reply, " default cleared");
}
} else if (n == 2 && strcmp(parts[1], "default") == 0) {
auto def = region_map.getDefaultRegion();
sprintf(reply, " default is %s", def ? def->name : "(none)");
} else if (n >= 3 && strcmp(parts[1], "list") == 0 && sender_timestamp == 0) {
// "region list allowed" / "region list denied" — serial only
bool denied = (strcmp(parts[2], "denied") == 0);
char buf[256];
region_map.exportNamesTo(buf, sizeof(buf), REGION_DENY_FLOOD, denied);
Serial.printf("Regions (%s): %s\n", parts[2], buf);
} else if (n >= 3 && strcmp(parts[1], "put") == 0) {
auto parent = n >= 4 ? region_map.findByNamePrefix(parts[3]) : &region_map.getWildcard();
if (parent == NULL) {
@@ -1157,6 +1223,26 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
}
} else if (strcmp(command, "get path.hash.mode") == 0) {
sprintf(reply, "> %d (%d-byte path hashes)", _prefs.path_hash_mode, _prefs.path_hash_mode + 1);
} else if (memcmp(command, "set loop.detect ", 16) == 0) {
const char* val = &command[16];
if (strcmp(val, "off") == 0) {
_prefs.loop_detect = LOOP_DETECT_OFF;
} else if (strcmp(val, "minimal") == 0) {
_prefs.loop_detect = LOOP_DETECT_MINIMAL;
} else if (strcmp(val, "moderate") == 0) {
_prefs.loop_detect = LOOP_DETECT_MODERATE;
} else if (strcmp(val, "strict") == 0) {
_prefs.loop_detect = LOOP_DETECT_STRICT;
} else {
strcpy(reply, "ERR: use off, minimal, moderate, or strict");
return;
}
savePrefs();
sprintf(reply, "OK - loop.detect = %s", val);
} else if (strcmp(command, "get loop.detect") == 0) {
const char* labels[] = { "off", "minimal", "moderate", "strict" };
uint8_t mode = _prefs.loop_detect <= 3 ? _prefs.loop_detect : 0;
sprintf(reply, "> %s", labels[mode]);
} else{
_cli.handleCommand(sender_timestamp, command, reply); // common CLI commands
}
+8 -3
View File
@@ -1124,19 +1124,24 @@ restart:
xSemaphoreGive(_telemetryMutex);
}
char json[400];
static const char* loopLabels[] = { "off", "minimal", "moderate", "strict" };
const char* loopStr = td.loop_detect <= 3 ? loopLabels[td.loop_detect] : "off";
char json[512];
snprintf(json, sizeof(json),
"{\"uptime\":%lu,\"batt_mv\":%d,\"batt_pct\":%d,\"temp\":%.1f,"
"\"csq\":%d,\"bars\":%d,\"neighbors\":%d,"
"\"freq\":%.3f,\"bw\":%.1f,\"sf\":%d,\"cr\":%d,\"tx\":%d,"
"\"name\":\"%s\",\"ip\":\"%s\",\"oper\":\"%s\",\"apn\":\"%s\","
"\"heap\":%d}",
"\"heap\":%d,"
"\"loop_detect\":\"%s\",\"path_hash_mode\":%d,\"flood_max\":%d}",
td.uptime_secs, td.battery_mv, td.battery_pct,
td.temperature / 10.0f,
_csq, getSignalBars(), td.neighbor_count,
td.freq, td.bw, td.sf, td.cr, td.tx_power,
td.node_name, _ipAddr, _operator, _apn,
ESP.getFreeHeap());
ESP.getFreeHeap(),
loopStr, td.path_hash_mode, td.flood_max);
mqttPublish(_topicTelem, json);
lastTelem = millis();
+6
View File
@@ -246,6 +246,9 @@ void loop() {
strncpy(td.oper, cellularMQTT.getOperator(), sizeof(td.oper) - 1);
td.mqtt_connected = cellularMQTT.isConnected();
td.neighbor_count = 0; // TODO: expose from MyMesh
td.loop_detect = p->loop_detect;
td.path_hash_mode = p->path_hash_mode;
td.flood_max = p->flood_max;
cellularMQTT.updateTelemetry(td);
lastTelemUpdate = millis();
@@ -299,6 +302,9 @@ void loop() {
strncpy(td.node_name, p->node_name, sizeof(td.node_name) - 1);
td.mqtt_connected = wifiMQTT.isConnected();
td.neighbor_count = 0;
td.loop_detect = p->loop_detect;
td.path_hash_mode = p->path_hash_mode;
td.flood_max = p->flood_max;
wifiMQTT.updateTelemetry(td);
lastTelemUpdate = millis();
+26 -25
View File
@@ -1,10 +1,10 @@
#ifdef MECK_WIFI_REMOTE
#include "target.h"
#include "WiFiMQTT.h"
#include <esp_mac.h>
#include <Update.h>
#include <HTTPClient.h>
#include "target.h"
WiFiMQTT wifiMQTT;
@@ -373,19 +373,24 @@ void WiFiMQTT::publishQueuedResponses() {
void WiFiMQTT::publishTelemetry() {
_rssi = WiFi.RSSI();
char json[400];
static const char* loopLabels[] = { "off", "minimal", "moderate", "strict" };
const char* loopStr = _telemetry.loop_detect <= 3 ? loopLabels[_telemetry.loop_detect] : "off";
char json[512];
snprintf(json, sizeof(json),
"{\"uptime\":%lu,\"batt_mv\":%d,\"batt_pct\":%d,\"temp\":%.1f,"
"\"rssi\":%d,\"bars\":%d,\"neighbors\":%d,"
"\"freq\":%.3f,\"bw\":%.1f,\"sf\":%d,\"cr\":%d,\"tx\":%d,"
"\"name\":\"%s\",\"ip\":\"%s\",\"ssid\":\"%s\","
"\"heap\":%d}",
"\"heap\":%d,"
"\"loop_detect\":\"%s\",\"path_hash_mode\":%d,\"flood_max\":%d}",
_telemetry.uptime_secs, _telemetry.battery_mv, _telemetry.battery_pct,
_telemetry.temperature / 10.0f,
_rssi, getSignalBars(), _telemetry.neighbor_count,
_telemetry.freq, _telemetry.bw, _telemetry.sf, _telemetry.cr, _telemetry.tx_power,
_telemetry.node_name, _ipAddr, _config.networks[_activeNetwork].ssid,
ESP.getFreeHeap());
ESP.getFreeHeap(),
loopStr, _telemetry.path_hash_mode, _telemetry.flood_max);
_mqttClient.publish(_topicTelem, json);
}
@@ -403,34 +408,39 @@ void WiFiMQTT::performOTA() {
_mqttClient.publish(_topicRsp, "OTA: Starting download...");
_mqttClient.loop();
// Disconnect MQTT cleanly before starting HTTP download —
// we can't reuse _wifiClient (PubSubClient holds the socket)
_mqttClient.disconnect();
delay(100);
// Use a dedicated TLS client for the firmware download
WiFiClientSecure otaClient;
otaClient.setInsecure(); // skip cert verification (same as MQTT client)
otaClient.setTimeout(30); // 30 second socket timeout
HTTPClient http;
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
http.setTimeout(180000);
if (!http.begin(_wifiClient, _otaUrl)) {
if (!http.begin(otaClient, _otaUrl)) {
Serial.println("[OTA] HTTP begin failed");
_mqttClient.publish(_topicRsp, "OTA: HTTP begin failed");
_state = WiFiMQTTState::CONNECTED;
_state = WiFiMQTTState::MQTT_CONNECTING; // trigger MQTT reconnect
return;
}
int httpCode = http.GET();
if (httpCode != HTTP_CODE_OK) {
Serial.printf("[OTA] HTTP error: %d\n", httpCode);
char msg[60];
snprintf(msg, sizeof(msg), "OTA: HTTP error %d", httpCode);
_mqttClient.publish(_topicRsp, msg);
http.end();
_state = WiFiMQTTState::CONNECTED;
_state = WiFiMQTTState::MQTT_CONNECTING;
return;
}
int fileSize = http.getSize();
if (fileSize <= 0) {
Serial.println("[OTA] Unknown content length");
_mqttClient.publish(_topicRsp, "OTA: Unknown file size");
http.end();
_state = WiFiMQTTState::CONNECTED;
_state = WiFiMQTTState::MQTT_CONNECTING;
return;
}
@@ -438,9 +448,8 @@ void WiFiMQTT::performOTA() {
if (!Update.begin(fileSize)) {
Serial.printf("[OTA] Update.begin failed: %s\n", Update.errorString());
_mqttClient.publish(_topicRsp, "OTA: Flash init failed");
http.end();
_state = WiFiMQTTState::CONNECTED;
_state = WiFiMQTTState::MQTT_CONNECTING;
return;
}
@@ -472,10 +481,6 @@ void WiFiMQTT::performOTA() {
int pct = (offset * 100) / fileSize;
if (pct / 10 != lastPct / 10) {
Serial.printf("[OTA] Progress: %d%% (%d/%d)\n", pct, offset, fileSize);
char msg[60];
snprintf(msg, sizeof(msg), "OTA: Flashing %d%%", pct);
_mqttClient.publish(_topicRsp, msg);
_mqttClient.loop();
lastPct = pct;
}
@@ -487,21 +492,17 @@ void WiFiMQTT::performOTA() {
if (offset < fileSize) {
Serial.printf("[OTA] Incomplete: %d of %d\n", offset, fileSize);
Update.abort();
_mqttClient.publish(_topicRsp, "OTA: Download incomplete");
_state = WiFiMQTTState::CONNECTED;
_state = WiFiMQTTState::MQTT_CONNECTING;
return;
}
if (!Update.end(true)) {
Serial.printf("[OTA] Update.end failed: %s\n", Update.errorString());
_mqttClient.publish(_topicRsp, "OTA: Verification failed");
_state = WiFiMQTTState::CONNECTED;
_state = WiFiMQTTState::MQTT_CONNECTING;
return;
}
Serial.println("[OTA] SUCCESS — rebooting in 3 seconds");
_mqttClient.publish(_topicRsp, "OTA: Success! Rebooting...");
_mqttClient.loop();
delay(3000);
ESP.restart();
}
+4
View File
@@ -103,6 +103,10 @@ struct TelemetryData {
uint8_t tx_power;
char node_name[32];
bool mqtt_connected;
// Routing settings (v1.8+)
uint8_t loop_detect; // 0=off, 1=minimal, 2=moderate, 3=strict
uint8_t path_hash_mode; // 0=1-byte, 1=2-byte, 2=3-byte
uint8_t flood_max; // max flood hop count (0-64)
};
// ---------------------------------------------------------------------------
+66
View File
@@ -0,0 +1,66 @@
"""
patch_nrf52_bsp.py Pre-build BSP patches for nRF52 Meck builds
Patches the Adafruit nRF52 BSP's LittleFS File class to add a default
constructor. BSP 1.7.0 removed the default File() constructor, but the
Meck screen headers (NotesScreen, TextReaderScreen, EpubZipReader) have
File member variables that need default construction.
Runs automatically before each build via extra_scripts in platformio.ini.
Idempotent safe to run repeatedly.
"""
Import("env")
import os
framework_dir = env.PioPlatform().get_package_dir("framework-arduinoadafruitnrf52")
lfs_src = os.path.join(framework_dir, "libraries", "Adafruit_LittleFS", "src")
# -------------------------------------------------------------------------
# 1. Patch header: add File() default constructor declaration
# -------------------------------------------------------------------------
header_path = os.path.join(lfs_src, "Adafruit_LittleFS_File.h")
with open(header_path, "r") as f:
h = f.read()
if "File ();" not in h and "File();" not in h:
h = h.replace(
"File (Adafruit_LittleFS &fs);",
"File (Adafruit_LittleFS &fs);\n File (); // Meck nRF52 compat"
)
with open(header_path, "w") as f:
f.write(h)
print("LittleFS File patch: Added default constructor declaration")
else:
print("LittleFS File patch: OK - header already patched")
# -------------------------------------------------------------------------
# 2. Patch source: add File() default constructor implementation
# Uses C++11 delegating constructor → File(InternalFS)
# -------------------------------------------------------------------------
source_path = os.path.join(lfs_src, "Adafruit_LittleFS_File.cpp")
with open(source_path, "r") as f:
s = f.read()
if "File::File()" not in s:
# Locate InternalFileSystem header (Adafruit BSP has a known typo: "Sytem")
ifs_include = None
for dirname in ["InternalFileSytem", "InternalFileSystem"]:
candidate = os.path.join(framework_dir, "libraries", dirname, "src")
if os.path.isdir(candidate):
rel = os.path.relpath(candidate, lfs_src).replace("\\", "/")
ifs_include = rel + "/InternalFileSystem.h"
break
if ifs_include:
s += "\n// Meck nRF52 compat: default File() constructor\n"
s += '#include "' + ifs_include + '"\n'
s += "File::File() : File(InternalFS) {}\n"
with open(source_path, "w") as f:
f.write(s)
print("LittleFS File patch: Added default constructor implementation")
else:
print("LittleFS File patch: WARNING - could not find InternalFileSystem")
else:
print("LittleFS File patch: OK - source already patched")
+1
View File
@@ -43,6 +43,7 @@ namespace mesh {
class MainBoard {
public:
virtual uint16_t getBattMilliVolts() = 0;
virtual uint8_t getBatteryPercent() { return 0; }
virtual float getMCUTemperature() { return NAN; }
virtual bool setAdcMultiplier(float multiplier) { return false; };
virtual float getAdcMultiplier() const { return 0.0f; }
+3 -2
View File
@@ -2,7 +2,7 @@
#include <Arduino.h>
#define MAX_FRAME_SIZE 172
#define MAX_FRAME_SIZE 255
class BaseSerialInterface {
protected:
@@ -16,6 +16,7 @@ public:
virtual bool isConnected() const = 0;
virtual bool isWriteBusy() const = 0;
virtual bool hasPendingData() const { return false; }
virtual size_t writeFrame(const uint8_t src[], size_t len) = 0;
virtual size_t checkRecvFrame(uint8_t dest[]) = 0;
};
};
+7 -1
View File
@@ -83,6 +83,10 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
file.read((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170
file.read((uint8_t *)&_prefs->path_hash_mode, sizeof(_prefs->path_hash_mode)); // 290
// 291
if (file.read((uint8_t *)&_prefs->loop_detect, sizeof(_prefs->loop_detect)) != sizeof(_prefs->loop_detect)) {
_prefs->loop_detect = LOOP_DETECT_OFF; // default for older prefs files
}
// 292
// sanitise bad pref values
_prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f);
@@ -109,6 +113,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
_prefs->gps_enabled = constrain(_prefs->gps_enabled, 0, 1);
_prefs->advert_loc_policy = constrain(_prefs->advert_loc_policy, 0, 2);
_prefs->path_hash_mode = constrain(_prefs->path_hash_mode, 0, 2);
_prefs->loop_detect = constrain(_prefs->loop_detect, 0, 3);
file.close();
}
@@ -168,7 +173,8 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
file.write((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166
file.write((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170
file.write((uint8_t *)&_prefs->path_hash_mode, sizeof(_prefs->path_hash_mode)); // 290
// 291
file.write((uint8_t *)&_prefs->loop_detect, sizeof(_prefs->loop_detect)); // 291
// 292
file.close();
}
+6
View File
@@ -13,6 +13,11 @@
#define ADVERT_LOC_SHARE 1
#define ADVERT_LOC_PREFS 2
#define LOOP_DETECT_OFF 0
#define LOOP_DETECT_MINIMAL 1
#define LOOP_DETECT_MODERATE 2
#define LOOP_DETECT_STRICT 3
struct NodePrefs { // persisted to file
float airtime_factor;
char node_name[32];
@@ -54,6 +59,7 @@ struct NodePrefs { // persisted to file
char owner_info[120];
// Multi-byte path hash support (added for Meck remote repeater)
uint8_t path_hash_mode; // 0=1-byte (legacy), 1=2-byte, 2=3-byte path hashes
uint8_t loop_detect; // 0=off, 1=minimal, 2=moderate, 3=strict (MeshCore v1.14+)
};
class CommonCLICallbacks {
+95 -24
View File
@@ -2,8 +2,48 @@
#include <helpers/TxtDataHelpers.h>
#include <SHA256.h>
// helper class for region map exporter, we emulate Stream with a safe buffer writer.
class BufStream : public Stream {
public:
BufStream(char *buf, size_t max_len)
: _buf(buf), _max_len(max_len), _pos(0) {
if (_max_len > 0) _buf[0] = 0;
}
size_t write(uint8_t c) override {
if (_pos + 1 >= _max_len) return 0;
_buf[_pos++] = c;
_buf[_pos] = 0;
return 1;
}
size_t write(const uint8_t *buffer, size_t size) override {
size_t written = 0;
while (written < size) {
if (!write(buffer[written])) break;
written++;
}
return written;
}
int available() override { return 0; }
int read() override { return -1; }
int peek() override { return -1; }
void flush() override {}
size_t length() const { return _pos; }
private:
char *_buf;
size_t _max_len;
size_t _pos;
};
RegionMap::RegionMap(TransportKeyStore& store) : _store(&store) {
next_id = 1; num_regions = 0; home_id = 0;
next_id = 1; num_regions = 0;
default_id = home_id = 0;
wildcard.id = wildcard.parent = 0;
wildcard.flags = 0; // default behaviour, allow flood and direct
strcpy(wildcard.name, "*");
@@ -40,9 +80,11 @@ bool RegionMap::load(FILESYSTEM* _fs, const char* path) {
if (file) {
uint8_t pad[128];
num_regions = 0; next_id = 1; home_id = 0;
num_regions = 0; next_id = 1;
default_id = home_id = 0;
bool success = file.read(pad, 5) == 5; // reserved header
bool success = file.read(pad, 3) == 3; // reserved header
success = success && file.read((uint8_t *) &default_id, sizeof(default_id)) == sizeof(default_id);
success = success && file.read((uint8_t *) &home_id, sizeof(home_id)) == sizeof(home_id);
success = success && file.read((uint8_t *) &wildcard.flags, sizeof(wildcard.flags)) == sizeof(wildcard.flags);
success = success && file.read((uint8_t *) &next_id, sizeof(next_id)) == sizeof(next_id);
@@ -78,7 +120,8 @@ bool RegionMap::save(FILESYSTEM* _fs, const char* path) {
uint8_t pad[128];
memset(pad, 0, sizeof(pad));
bool success = file.write(pad, 5) == 5; // reserved header
bool success = file.write(pad, 3) == 3; // reserved header
success = success && file.write((uint8_t *) &default_id, sizeof(default_id)) == sizeof(default_id);
success = success && file.write((uint8_t *) &home_id, sizeof(home_id)) == sizeof(home_id);
success = success && file.write((uint8_t *) &wildcard.flags, sizeof(wildcard.flags)) == sizeof(wildcard.flags);
success = success && file.write((uint8_t *) &next_id, sizeof(next_id)) == sizeof(next_id);
@@ -125,24 +168,29 @@ RegionEntry* RegionMap::putRegion(const char* name, uint16_t parent_id, uint16_t
return region;
}
int RegionMap::getTransportKeysFor(const RegionEntry& src, TransportKey dest[], int max_num) {
int num;
if (src.name[0] == '$') { // private region
num = _store->loadKeysFor(src.id, dest, max_num);
} else if (src.name[0] == '#') { // auto hashtag region
_store->getAutoKeyFor(src.id, src.name, dest[0]);
num = 1;
} else { // new: implicit auto hashtag region
char tmp[sizeof(src.name)+1];
tmp[0] = '#';
strcpy(&tmp[1], src.name);
_store->getAutoKeyFor(src.id, tmp, dest[0]);
num = 1;
}
return num;
}
RegionEntry* RegionMap::findMatch(mesh::Packet* packet, uint8_t mask) {
for (int i = 0; i < num_regions; i++) {
auto region = &regions[i];
if ((region->flags & mask) == 0) { // does region allow this? (per 'mask' param)
TransportKey keys[4];
int num;
if (region->name[0] == '$') { // private region
num = _store->loadKeysFor(region->id, keys, 4);
} else if (region->name[0] == '#') { // auto hashtag region
_store->getAutoKeyFor(region->id, region->name, keys[0]);
num = 1;
} else { // new: implicit auto hashtag region
char tmp[sizeof(region->name)];
tmp[0] = '#';
strcpy(&tmp[1], region->name);
_store->getAutoKeyFor(region->id, tmp, keys[0]);
num = 1;
}
int num = getTransportKeysFor(*region, keys, 4);
for (int j = 0; j < num; j++) {
uint16_t code = keys[j].calcTransportCode(packet);
if (packet->transport_codes[0] == code) { // a match!!
@@ -198,6 +246,14 @@ void RegionMap::setHomeRegion(const RegionEntry* home) {
home_id = home ? home->id : 0;
}
RegionEntry* RegionMap::getDefaultRegion() {
return default_id == 0 ? NULL : findById(default_id);
}
void RegionMap::setDefaultRegion(const RegionEntry* def) {
default_id = def ? def->id : 0;
}
bool RegionMap::removeRegion(const RegionEntry& region) {
if (region.id == 0) return false; // failed (cannot remove the wildcard Region)
@@ -249,27 +305,42 @@ void RegionMap::exportTo(Stream& out) const {
printChildRegions(0, &wildcard, out); // recursive
}
int RegionMap::exportNamesTo(char *dest, int max_len, uint8_t mask) {
size_t RegionMap::exportTo(char *dest, size_t max_len) const {
if (!dest || max_len == 0) return 0;
BufStream bs(dest, max_len);
exportTo(bs); // reuse existing logic
return bs.length();
}
int RegionMap::exportNamesTo(char *dest, int max_len, uint8_t mask, bool invert) {
char *dp = dest;
if ((wildcard.flags & mask) == 0) {
// Check wildcard region
bool wildcard_matches = invert ? (wildcard.flags & mask) : !(wildcard.flags & mask);
if (wildcard_matches) {
*dp++ = '*';
*dp++ = ',';
}
for (int i = 0; i < num_regions; i++) {
auto region = &regions[i];
if ((region->flags & mask) == 0) { // region allowed? (per 'mask' param)
const char* name = skip_hash(region->name);
int len = strlen(name);
// Check if region matches the filter criteria
bool region_matches = invert ? (region->flags & mask) : !(region->flags & mask);
if (region_matches) {
int len = strlen(skip_hash(region->name));
if ((dp - dest) + len + 2 < max_len) { // only append if name will fit
memcpy(dp, name, len);
memcpy(dp, skip_hash(region->name), len);
dp += len;
*dp++ = ',';
}
}
}
if (dp > dest) { dp--; } // don't include trailing comma
*dp = 0; // set null terminator
return dp - dest; // return length
}
}
+11 -4
View File
@@ -16,11 +16,13 @@ struct RegionEntry {
uint16_t parent;
uint8_t flags;
char name[31];
bool isWildcard() const { return id == 0; }
};
class RegionMap {
TransportKeyStore* _store;
uint16_t next_id, home_id;
uint16_t next_id, home_id, default_id;
uint16_t num_regions;
RegionEntry regions[MAX_REGION_ENTRIES];
RegionEntry wildcard;
@@ -43,13 +45,18 @@ public:
RegionEntry* findById(uint16_t id);
RegionEntry* getHomeRegion(); // NOTE: can be NULL
void setHomeRegion(const RegionEntry* home);
RegionEntry* getDefaultRegion(); // NOTE: can be NULL
void setDefaultRegion(const RegionEntry* def);
bool removeRegion(const RegionEntry& region);
bool clear();
void resetFrom(const RegionMap& src) { num_regions = 0; next_id = src.next_id; }
int getCount() const { return num_regions; }
const RegionEntry* getByIdx(int i) const { return &regions[i]; }
const RegionEntry* getRoot() const { return &wildcard; }
int exportNamesTo(char *dest, int max_len, uint8_t mask);
int exportNamesTo(char *dest, int max_len, uint8_t mask, bool invert = false);
int getTransportKeysFor(const RegionEntry& src, TransportKey dest[], int max_num);
void exportTo(Stream& out) const;
};
void exportTo(Stream& out) const;
size_t exportTo(char *dest, size_t max_len) const;
};
+15 -5
View File
@@ -96,6 +96,16 @@ void SerialBLEInterface::onAuthenticationComplete(esp_ble_auth_cmpl_t cmpl) {
conn_params.timeout = 400; // 4 seconds supervision timeout
esp_ble_gap_update_conn_params(&conn_params);
BLE_DEBUG_PRINTLN(" - Requested fast connection interval (15-20ms)");
// Request 2M PHY for doubled air data rate (BLE 5.0, supported on ESP32-S3)
// Note: ESP-IDF misspells "preferred" as "prefered" in their API
esp_ble_gap_set_prefered_phy(_remote_bda,
0, // all_phys: no preference flags, use tx/rx masks
ESP_BLE_GAP_PHY_2M_PREF_MASK,
ESP_BLE_GAP_PHY_2M_PREF_MASK,
ESP_BLE_GAP_PHY_OPTIONS_NO_PREF);
esp_ble_gap_set_pkt_data_len(_remote_bda, 251); // Request DLE (max link-layer PDU)
BLE_DEBUG_PRINTLN(" - Requested 2M PHY and DLE (251 bytes)");
} else {
BLE_DEBUG_PRINTLN(" - SecurityCallback - Authentication Failure*");
@@ -205,7 +215,7 @@ size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) {
return 0;
}
#define BLE_WRITE_MIN_INTERVAL 15
#define BLE_WRITE_MIN_INTERVAL 7
bool SerialBLEInterface::isWriteBusy() const {
return millis() < _last_write + BLE_WRITE_MIN_INTERVAL; // still too soon to start another write?
@@ -224,8 +234,8 @@ size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) {
BLE_DEBUG_PRINTLN("writeBytes: sz=%d, hdr=%d", (uint32_t)send_queue[0].len, (uint32_t) send_queue[0].buf[0]);
send_queue_len--;
for (int i = 0; i < send_queue_len; i++) { // delete top item from queue
send_queue[i] = send_queue[i + 1];
if (send_queue_len > 0) {
memmove(&send_queue[0], &send_queue[1], send_queue_len * sizeof(Frame));
}
}
@@ -236,8 +246,8 @@ size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) {
BLE_DEBUG_PRINTLN("readBytes: sz=%d, hdr=%d", len, (uint32_t) dest[0]);
recv_queue_len--;
for (int i = 0; i < recv_queue_len; i++) { // delete top item from queue
recv_queue[i] = recv_queue[i + 1];
if (recv_queue_len > 0) {
memmove(&recv_queue[0], &recv_queue[1], recv_queue_len * sizeof(Frame));
}
return len;
}
+1
View File
@@ -79,6 +79,7 @@ public:
bool isConnected() const override;
bool isWriteBusy() const override;
bool hasPendingData() const override { return deviceConnected && send_queue_len > 0; }
size_t writeFrame(const uint8_t src[], size_t len) override;
size_t checkRecvFrame(uint8_t dest[]) override;
};
+1 -1
View File
@@ -130,7 +130,7 @@ void SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code
snprintf(charpin, sizeof(charpin), "%lu", (unsigned long)pin_code);
// If we want to control BLE LED ourselves, uncomment this:
// Bluefruit.autoConnLed(false);
Bluefruit.autoConnLed(false);
Bluefruit.configPrphBandwidth(BANDWIDTH_MAX);
Bluefruit.begin();
+18 -8
View File
@@ -47,17 +47,27 @@ public:
print(str);
}
// convert UTF-8 characters to displayable block characters for compatibility
// Strip non-ASCII characters (emoji, etc.) from a UTF-8 string.
// Uses the same lead-byte-pattern approach as emojiDecodeUtf8() in
// EmojiSprites.h to determine sequence length, so both the channel
// message path (emojiSanitize) and the contact/advert name path
// consume identical byte spans for any given UTF-8 or malformed input.
virtual void translateUTF8ToBlocks(char* dest, const char* src, size_t dest_size) {
size_t j = 0;
for (size_t i = 0; src[i] != 0 && j < dest_size - 1; i++) {
size_t len = strlen(src);
for (size_t i = 0; i < len && j < dest_size - 1; ) {
unsigned char c = (unsigned char)src[i];
if (c >= 32 && c <= 126) {
dest[j++] = c; // ASCII printable
} else if (c >= 0x80) {
dest[j++] = '\xDB'; // CP437 full block █
while (src[i+1] && (src[i+1] & 0xC0) == 0x80)
i++; // skip UTF-8 continuation bytes
if (c < 0x80) {
if (c >= 32) dest[j++] = c; // ASCII printable
i++;
} else {
// Determine sequence length from lead byte (matches emojiDecodeUtf8)
int skip;
if ((c & 0xE0) == 0xC0) skip = 2;
else if ((c & 0xF0) == 0xE0) skip = 3;
else if ((c & 0xF8) == 0xF0) skip = 4;
else skip = 1; // stray continuation or invalid
i += skip;
}
}
dest[j] = 0;
+252 -11
View File
@@ -22,7 +22,90 @@
// CLEAR_FAST, CLEAR_SLOW — full refresh modes
// Periodic slow (deep) refresh to clear ghosting
#define FULL_SLOW_PERIOD 1 // every frame eliminates ghosting (increase to 2+ for less flashing)
#define FULL_SLOW_PERIOD 1 // every frame -- eliminates ghosting (increase to 2+ for less flashing)
// ---------------------------------------------------------------------------
// UTF-8 helpers for diacritic / extended Latin rendering
// ---------------------------------------------------------------------------
static uint32_t utf8Decode(const uint8_t* s, int len, int* consumed) {
if (len <= 0) { *consumed = 0; return 0; }
uint8_t b = s[0];
if (b < 0x80) { *consumed = 1; return b; }
if ((b & 0xE0) == 0xC0 && len >= 2 && (s[1] & 0xC0) == 0x80) {
*consumed = 2;
return ((uint32_t)(b & 0x1F) << 6) | (s[1] & 0x3F);
}
if ((b & 0xF0) == 0xE0 && len >= 3 && (s[1] & 0xC0) == 0x80 && (s[2] & 0xC0) == 0x80) {
*consumed = 3;
return ((uint32_t)(b & 0x0F) << 12) | ((uint32_t)(s[1] & 0x3F) << 6) | (s[2] & 0x3F);
}
if ((b & 0xF8) == 0xF0 && len >= 4 && (s[1] & 0xC0) == 0x80 && (s[2] & 0xC0) == 0x80 && (s[3] & 0xC0) == 0x80) {
*consumed = 4;
return ((uint32_t)(b & 0x07) << 18) | ((uint32_t)(s[1] & 0x3F) << 12) | ((uint32_t)(s[2] & 0x3F) << 6) | (s[3] & 0x3F);
}
*consumed = 1;
return 0xFFFD;
}
static char foldToAscii(uint32_t cp) {
if (cp < 0x80) return (char)cp;
if (cp >= 0xC0 && cp <= 0xFF) {
static const char f[64] = {
'A','A','A','A','A','A','A','C','E','E','E','E','I','I','I','I',
'D','N','O','O','O','O','O', 0 ,'O','U','U','U','U','Y', 0 ,'s',
'a','a','a','a','a','a','a','c','e','e','e','e','i','i','i','i',
'd','n','o','o','o','o','o', 0 ,'o','u','u','u','u','y', 0 ,'y'
};
return f[cp - 0xC0];
}
if (cp >= 0x100 && cp <= 0x17F) {
switch (cp) {
case 0x100: case 0x102: case 0x104: return 'A';
case 0x101: case 0x103: case 0x105: return 'a';
case 0x106: case 0x108: case 0x10A: case 0x10C: return 'C';
case 0x107: case 0x109: case 0x10B: case 0x10D: return 'c';
case 0x10E: case 0x110: return 'D';
case 0x10F: case 0x111: return 'd';
case 0x112: case 0x114: case 0x116: case 0x118: case 0x11A: return 'E';
case 0x113: case 0x115: case 0x117: case 0x119: case 0x11B: return 'e';
case 0x11C: case 0x11E: case 0x120: case 0x122: return 'G';
case 0x11D: case 0x11F: case 0x121: case 0x123: return 'g';
case 0x124: case 0x126: return 'H'; case 0x125: case 0x127: return 'h';
case 0x128: case 0x12A: case 0x12C: case 0x12E: case 0x130: return 'I';
case 0x129: case 0x12B: case 0x12D: case 0x12F: case 0x131: return 'i';
case 0x134: return 'J'; case 0x135: return 'j';
case 0x136: return 'K'; case 0x137: case 0x138: return 'k';
case 0x139: case 0x13B: case 0x13D: case 0x13F: case 0x141: return 'L';
case 0x13A: case 0x13C: case 0x13E: case 0x140: case 0x142: return 'l';
case 0x143: case 0x145: case 0x147: return 'N';
case 0x144: case 0x146: case 0x148: case 0x149: return 'n';
case 0x14C: case 0x14E: case 0x150: return 'O';
case 0x14D: case 0x14F: case 0x151: return 'o';
case 0x152: return 'O'; case 0x153: return 'o';
case 0x154: case 0x156: case 0x158: return 'R';
case 0x155: case 0x157: case 0x159: return 'r';
case 0x15A: case 0x15C: case 0x15E: case 0x160: return 'S';
case 0x15B: case 0x15D: case 0x15F: case 0x161: return 's';
case 0x162: case 0x164: case 0x166: return 'T';
case 0x163: case 0x165: case 0x167: return 't';
case 0x168: case 0x16A: case 0x16C: case 0x16E: case 0x170: case 0x172: return 'U';
case 0x169: case 0x16B: case 0x16D: case 0x16F: case 0x171: case 0x173: return 'u';
case 0x174: return 'W'; case 0x175: return 'w';
case 0x176: case 0x178: return 'Y'; case 0x177: return 'y';
case 0x179: case 0x17B: case 0x17D: return 'Z';
case 0x17A: case 0x17C: case 0x17E: return 'z';
}
}
return 0;
}
static bool hasNonAscii(const char* str) {
for (int i = 0; str[i]; i++) {
if ((uint8_t)str[i] >= 0x80) return true;
}
return false;
}
FastEPDDisplay::~FastEPDDisplay() {
delete _canvas;
@@ -128,43 +211,55 @@ void FastEPDDisplay::setTextSize(int sz) {
const GFXfont* customFont = meckGetFont(_fontStyle, sz);
if (customFont) {
_canvas->setFont(customFont);
// textSize 5 (clock face) uses ×5 scaling even with custom fonts
_canvas->setTextSize(sz == 5 ? 5 : 1);
_currentFont = customFont;
// textSize 5 (clock face) uses x5 scaling even with custom fonts
_currentTextScale = (sz == 5) ? 5 : 1;
_canvas->setTextSize(_currentTextScale);
return;
}
// Classic style (or fallback) original FreeSans/FreeSerif fonts
// Classic style (or fallback) -- original FreeSans/FreeSerif fonts
// Toggle between font families via -D MECK_SERIF_FONT build flag
_currentTextScale = 1;
switch(sz) {
case 0: // Body text reader content, settings rows, messages, footers
case 0: // Body text -- reader content, settings rows, messages, footers
#ifdef MECK_SERIF_FONT
_canvas->setFont(&FreeSerif12pt7b);
_currentFont = &FreeSerif12pt7b;
#else
_canvas->setFont(&FreeSans12pt7b);
_currentFont = &FreeSans12pt7b;
#endif
_canvas->setTextSize(1);
break;
case 1: // Headings screen titles, channel names (bold, same height as body)
case 1: // Headings -- screen titles, channel names (bold, same height as body)
_canvas->setFont(&FreeSansBold12pt7b);
_currentFont = &FreeSansBold12pt7b;
_canvas->setTextSize(1);
break;
case 2: // Large bold MSG count, tile letters
case 2: // Large bold -- MSG count, tile letters
_canvas->setFont(&FreeSansBold18pt7b);
_currentFont = &FreeSansBold18pt7b;
_canvas->setTextSize(1);
break;
case 3: // Extra large splash screen title
case 3: // Extra large -- splash screen title
_canvas->setFont(&FreeSansBold24pt7b);
_currentFont = &FreeSansBold24pt7b;
_canvas->setTextSize(1);
break;
case 5: // Clock face lock screen (FreeSansBold24pt scaled 5×)
case 5: // Clock face -- lock screen (FreeSansBold24pt scaled 5x)
_canvas->setFont(&FreeSansBold24pt7b);
_currentFont = &FreeSansBold24pt7b;
_currentTextScale = 5;
_canvas->setTextSize(5);
break;
default:
#ifdef MECK_SERIF_FONT
_canvas->setFont(&FreeSerif12pt7b);
_currentFont = &FreeSerif12pt7b;
#else
_canvas->setFont(&FreeSans12pt7b);
_currentFont = &FreeSans12pt7b;
#endif
_canvas->setTextSize(1);
break;
@@ -203,7 +298,79 @@ void FastEPDDisplay::setCursor(int x, int y) {
void FastEPDDisplay::print(const char* str) {
if (!_canvas || !str) return;
_frameCRC.update<char>(str, strlen(str));
_canvas->print(str);
if (!hasNonAscii(str)) {
// Pure ASCII fast path
_canvas->print(str);
return;
}
const uint8_t* s = (const uint8_t*)str;
int len = strlen(str);
int pos = 0;
bool has8bFont = _currentFont && _currentFont->last > 0xFF;
while (pos < len) {
uint8_t b = s[pos];
if (b < 0x80) {
_canvas->write(b);
pos++;
} else {
int consumed;
uint32_t cp = utf8Decode(s + pos, len - pos, &consumed);
if (has8bFont && cp >= _currentFont->first && cp <= _currentFont->last) {
drawGlyphAtCursor((uint16_t)cp);
} else if (!has8bFont) {
char folded = foldToAscii(cp);
if (folded) _canvas->write((uint8_t)folded);
}
pos += consumed;
}
}
}
void FastEPDDisplay::drawGlyphAtCursor(uint16_t cp) {
if (!_canvas || !_currentFont || cp < _currentFont->first || cp > _currentFont->last) return;
uint16_t idx = cp - _currentFont->first;
GFXglyph* glyph = &_currentFont->glyph[idx];
uint8_t gw = glyph->width;
uint8_t gh = glyph->height;
int8_t xo = glyph->xOffset;
int8_t yo = glyph->yOffset;
uint16_t bo = glyph->bitmapOffset;
uint8_t xa = glyph->xAdvance;
int16_t cx = _canvas->getCursorX();
int16_t cy = _canvas->getCursorY();
// Canvas color: 0 = black, 1 = white
uint16_t canvasColor = (_curr_color == GxEPD_BLACK) ? 0 : 1;
if (gw > 0 && gh > 0) {
const uint8_t* bitmap = _currentFont->bitmap;
uint8_t bit = 0, bits = 0;
for (uint8_t yy = 0; yy < gh; yy++) {
for (uint8_t xx = 0; xx < gw; xx++) {
if (!(bit++ & 7)) bits = bitmap[bo++];
if (bits & 0x80) {
if (_currentTextScale == 1) {
_canvas->drawPixel(cx + xo + xx, cy + yo + yy, canvasColor);
} else {
_canvas->fillRect(
cx + (xo + xx) * _currentTextScale,
cy + (yo + yy) * _currentTextScale,
_currentTextScale, _currentTextScale, canvasColor);
}
}
bits <<= 1;
}
}
}
_canvas->setCursor(cx + xa * _currentTextScale, cy);
}
void FastEPDDisplay::fillRect(int x, int y, int w, int h) {
@@ -277,9 +444,54 @@ void FastEPDDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) {
uint16_t FastEPDDisplay::getTextWidth(const char* str) {
if (!_canvas || !str) return 0;
if (!hasNonAscii(str)) {
int16_t x1, y1;
uint16_t w, h;
_canvas->getTextBounds(str, 0, 0, &x1, &y1, &w, &h);
return (uint16_t)ceil((w + 1) / scale_x);
}
bool has8bFont = _currentFont && _currentFont->last > 0xFF;
if (has8bFont) {
const uint8_t* s = (const uint8_t*)str;
int len = strlen(str);
int pos = 0;
int totalAdv = 0;
while (pos < len) {
int consumed;
uint32_t cp = utf8Decode(s + pos, len - pos, &consumed);
if (cp >= _currentFont->first && cp <= _currentFont->last) {
totalAdv += _currentFont->glyph[cp - _currentFont->first].xAdvance * _currentTextScale;
}
pos += consumed;
}
return (uint16_t)ceil((totalAdv + 1) / scale_x);
}
// Classic/7b: fold to ASCII, then measure
char folded[256];
const uint8_t* s = (const uint8_t*)str;
int len = strlen(str);
int pos = 0, fi = 0;
while (pos < len && fi < 254) {
uint8_t b = s[pos];
if (b < 0x80) {
folded[fi++] = (char)b;
pos++;
} else {
int consumed;
uint32_t cp = utf8Decode(s + pos, len - pos, &consumed);
char fc = foldToAscii(cp);
if (fc) folded[fi++] = fc;
pos += consumed;
}
}
folded[fi] = 0;
int16_t x1, y1;
uint16_t w, h;
_canvas->getTextBounds(str, 0, 0, &x1, &y1, &w, &h);
_canvas->getTextBounds(folded, 0, 0, &x1, &y1, &w, &h);
return (uint16_t)ceil((w + 1) / scale_x);
}
@@ -328,6 +540,35 @@ void FastEPDDisplay::endFrame() {
_epd->backupPlane();
}
void FastEPDDisplay::translateUTF8ToBlocks(char* dest, const char* src, size_t dest_size) {
if (_currentFont && _currentFont->last > 0xFF) {
strncpy(dest, src, dest_size - 1);
dest[dest_size - 1] = '\0';
return;
}
// Classic/7b: fold accented chars to ASCII
size_t j = 0;
const uint8_t* s = (const uint8_t*)src;
int len = strlen(src);
int pos = 0;
while (pos < len && j < dest_size - 1) {
uint8_t b = s[pos];
if (b >= 32 && b <= 126) {
dest[j++] = (char)b;
pos++;
} else if (b >= 0x80) {
int consumed;
uint32_t cp = utf8Decode(s + pos, len - pos, &consumed);
char folded = foldToAscii(cp);
if (folded) dest[j++] = folded;
pos += consumed;
} else {
pos++;
}
}
dest[j] = 0;
}
void FastEPDDisplay::setDarkMode(bool dark) {
_darkMode = dark;
_lastCRC = 0; // Force redraw
+7 -1
View File
@@ -78,7 +78,12 @@ class FastEPDDisplay : public DisplayDriver {
uint32_t _lastUpdateMs = 0; // Rate limiting — minimum interval between refreshes
bool _forcePartial = false; // When true, use partial updates (VKB typing)
bool _darkMode = false; // Invert all pixels (black bg, white text)
bool _portraitMode = false; // Rotated 90° (540×960 logical)
bool _portraitMode = false; // Rotated 90 (540x960 logical)
const GFXfont* _currentFont = nullptr; // Track for UTF-8 rendering
uint8_t _currentTextScale = 1; // Track glyph scale factor
// Render one glyph from the current 8b font at the canvas cursor position
void drawGlyphAtCursor(uint16_t cp);
// Virtual 128×128 → physical canvas mapping (runtime, changes with portrait)
float scale_x = 7.5f; // 960 / 128 (landscape default)
@@ -106,6 +111,7 @@ public:
void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override;
uint16_t getTextWidth(const char* str) override;
void endFrame() override;
void translateUTF8ToBlocks(char* dest, const char* src, size_t dest_size) override;
// --- Raw pixel access for MapScreen (bypasses scaling) ---
void drawPixelRaw(int16_t x, int16_t y, uint16_t color) {
+287 -8
View File
@@ -9,6 +9,101 @@
#define DISPLAY_ROTATION 0
#endif
// ---------------------------------------------------------------------------
// UTF-8 helpers for diacritic / extended Latin rendering
// ---------------------------------------------------------------------------
// Decode one UTF-8 character. Returns codepoint, sets *consumed to byte count.
static uint32_t utf8Decode(const uint8_t* s, int len, int* consumed) {
if (len <= 0) { *consumed = 0; return 0; }
uint8_t b = s[0];
if (b < 0x80) { *consumed = 1; return b; }
if ((b & 0xE0) == 0xC0 && len >= 2 && (s[1] & 0xC0) == 0x80) {
*consumed = 2;
return ((uint32_t)(b & 0x1F) << 6) | (s[1] & 0x3F);
}
if ((b & 0xF0) == 0xE0 && len >= 3 && (s[1] & 0xC0) == 0x80 && (s[2] & 0xC0) == 0x80) {
*consumed = 3;
return ((uint32_t)(b & 0x0F) << 12) | ((uint32_t)(s[1] & 0x3F) << 6) | (s[2] & 0x3F);
}
if ((b & 0xF8) == 0xF0 && len >= 4 && (s[1] & 0xC0) == 0x80 && (s[2] & 0xC0) == 0x80 && (s[3] & 0xC0) == 0x80) {
*consumed = 4;
return ((uint32_t)(b & 0x07) << 18) | ((uint32_t)(s[1] & 0x3F) << 12) | ((uint32_t)(s[2] & 0x3F) << 6) | (s[3] & 0x3F);
}
// Invalid lead byte or truncated sequence -- skip one byte
*consumed = 1;
return 0xFFFD; // replacement character
}
// Fold a Unicode codepoint to its ASCII base letter.
// Returns the ASCII char, or 0 if no folding exists.
static char foldToAscii(uint32_t cp) {
if (cp < 0x80) return (char)cp;
// Latin-1 Supplement (U+00C0 -- U+00FF)
if (cp >= 0xC0 && cp <= 0xFF) {
// C0 CF
static const char f[64] = {
'A','A','A','A','A','A','A','C','E','E','E','E','I','I','I','I',
'D','N','O','O','O','O','O', 0 ,'O','U','U','U','U','Y', 0 ,'s',
'a','a','a','a','a','a','a','c','e','e','e','e','i','i','i','i',
'd','n','o','o','o','o','o', 0 ,'o','u','u','u','u','y', 0 ,'y'
};
return f[cp - 0xC0];
}
// Latin Extended-A (U+0100 -- U+017F) -- Czech carons, Polish, etc.
if (cp >= 0x100 && cp <= 0x17F) {
switch (cp) {
case 0x100: case 0x102: case 0x104: return 'A';
case 0x101: case 0x103: case 0x105: return 'a';
case 0x106: case 0x108: case 0x10A: case 0x10C: return 'C';
case 0x107: case 0x109: case 0x10B: case 0x10D: return 'c';
case 0x10E: case 0x110: return 'D';
case 0x10F: case 0x111: return 'd';
case 0x112: case 0x114: case 0x116: case 0x118: case 0x11A: return 'E';
case 0x113: case 0x115: case 0x117: case 0x119: case 0x11B: return 'e';
case 0x11C: case 0x11E: case 0x120: case 0x122: return 'G';
case 0x11D: case 0x11F: case 0x121: case 0x123: return 'g';
case 0x124: case 0x126: return 'H';
case 0x125: case 0x127: return 'h';
case 0x128: case 0x12A: case 0x12C: case 0x12E: case 0x130: return 'I';
case 0x129: case 0x12B: case 0x12D: case 0x12F: case 0x131: return 'i';
case 0x134: return 'J'; case 0x135: return 'j';
case 0x136: return 'K'; case 0x137: case 0x138: return 'k';
case 0x139: case 0x13B: case 0x13D: case 0x13F: case 0x141: return 'L';
case 0x13A: case 0x13C: case 0x13E: case 0x140: case 0x142: return 'l';
case 0x143: case 0x145: case 0x147: return 'N';
case 0x144: case 0x146: case 0x148: case 0x149: return 'n';
case 0x14C: case 0x14E: case 0x150: return 'O';
case 0x14D: case 0x14F: case 0x151: return 'o';
case 0x152: return 'O'; case 0x153: return 'o'; // OE ligature
case 0x154: case 0x156: case 0x158: return 'R';
case 0x155: case 0x157: case 0x159: return 'r';
case 0x15A: case 0x15C: case 0x15E: case 0x160: return 'S';
case 0x15B: case 0x15D: case 0x15F: case 0x161: return 's';
case 0x162: case 0x164: case 0x166: return 'T';
case 0x163: case 0x165: case 0x167: return 't';
case 0x168: case 0x16A: case 0x16C: case 0x16E: case 0x170: case 0x172: return 'U';
case 0x169: case 0x16B: case 0x16D: case 0x16F: case 0x171: case 0x173: return 'u';
case 0x174: return 'W'; case 0x175: return 'w';
case 0x176: case 0x178: return 'Y'; case 0x177: return 'y';
case 0x179: case 0x17B: case 0x17D: return 'Z';
case 0x17A: case 0x17C: case 0x17E: return 'z';
}
}
return 0; // not foldable
}
// Check if a string contains any non-ASCII bytes
static bool hasNonAscii(const char* str) {
for (int i = 0; str[i]; i++) {
if ((uint8_t)str[i] >= 0x80) return true;
}
return false;
}
#ifdef ESP32
// E-ink and LoRa SHARE the same SPI bus (SCK=36, MOSI=33)
// They MUST use the same SPI peripheral (HSPI) to avoid GPIO conflicts
@@ -25,6 +120,10 @@ bool GxEPDDisplay::begin() {
// Tell GxEPD2 to use our SPI instance
// Using slower speed (4MHz) for reliable e-ink communication
display.epd2.selectSPI(displaySpi, SPISettings(4000000, MSBFIRST, SPI_MODE0));
#elif defined(LILYGO_TECHO)
// T-Echo Lite: display on SPI1 (pins 19/20), LoRa on SPI (pins 13/15/17)
SPI1.begin();
display.epd2.selectSPI(SPI1, SPISettings(4000000, MSBFIRST, SPI_MODE0));
#endif
// Initialize with:
@@ -35,10 +134,15 @@ bool GxEPDDisplay::begin() {
display.init(115200, true, 2, false);
display.setRotation(DISPLAY_ROTATION);
setTextSize(1); // Default to size 1
#ifdef EINK_FULL_REFRESH_ONLY
display.setFullWindow();
display.fillScreen(GxEPD_WHITE);
display.display(false); // Full refresh (SSD1681 doesn't support partial)
#else
display.setPartialWindow(0, 0, display.width(), display.height());
display.fillScreen(GxEPD_WHITE);
display.display(true);
#endif
#if DISP_BACKLIGHT
digitalWrite(DISP_BACKLIGHT, LOW);
@@ -101,38 +205,50 @@ void GxEPDDisplay::setTextSize(int sz) {
display_crc.update<uint8_t>(_fontStyle);
// Check for custom font style first (Noto Sans, Montserrat)
#ifdef HAS_MECK_FONTS
const GFXfont* customFont = meckGetFont(_fontStyle, sz);
if (customFont) {
display.setFont(customFont);
// textSize 5 (clock face) uses ×2 scaling even with custom fonts
display.setTextSize(sz == 5 ? 2 : 1);
_currentFont = customFont;
// textSize 5 (clock face) uses x2 scaling even with custom fonts
_currentTextScale = (sz == 5) ? 2 : 1;
display.setTextSize(_currentTextScale);
return;
}
#endif
// Classic style (or fallback) original FreeSans fonts
// Classic style (or fallback) -- original FreeSans fonts
_currentTextScale = 1;
switch(sz) {
case 0: // Tiny - built-in 6x8 pixel font
display.setFont(NULL);
_currentFont = nullptr;
display.setTextSize(1);
break;
case 1: // Small - use 9pt (was 9pt)
display.setFont(&FreeSans9pt7b);
_currentFont = &FreeSans9pt7b;
display.setTextSize(1);
break;
case 2: // Medium Bold - use 9pt bold instead of 12pt
display.setFont(&FreeSans9pt7b);
_currentFont = &FreeSans9pt7b;
display.setTextSize(1);
break;
case 3: // Large - use 12pt instead of 18pt
display.setFont(&FreeSansBold12pt7b);
_currentFont = &FreeSansBold12pt7b;
display.setTextSize(1);
break;
case 5: // Extra Large - lock screen clock face
display.setFont(&FreeSansBold12pt7b);
display.setTextSize(2); // GxEPD2 native 2× scaling on 12pt bold
_currentFont = &FreeSansBold12pt7b;
_currentTextScale = 2;
display.setTextSize(2); // GxEPD2 native 2x scaling on 12pt bold
break;
default:
display.setFont(&FreeSans9pt7b);
_currentFont = &FreeSans9pt7b;
display.setTextSize(1);
break;
}
@@ -166,7 +282,89 @@ void GxEPDDisplay::setCursor(int x, int y) {
void GxEPDDisplay::print(const char* str) {
display_crc.update<char>(str, strlen(str));
display.print(str);
if (!hasNonAscii(str)) {
// Pure ASCII fast path -- no decoding needed
display.print(str);
return;
}
const uint8_t* s = (const uint8_t*)str;
int len = strlen(str);
int pos = 0;
// Check if current font is an 8b font (covers codepoints > 0xFF)
bool has8bFont = _currentFont && _currentFont->last > 0xFF;
while (pos < len) {
uint8_t b = s[pos];
if (b < 0x80) {
// ASCII: use normal Adafruit GFX path (handles newlines etc.)
display.write(b);
pos++;
} else {
// Multi-byte UTF-8 sequence
int consumed;
uint32_t cp = utf8Decode(s + pos, len - pos, &consumed);
if (has8bFont && cp >= _currentFont->first && cp <= _currentFont->last) {
// Render directly from 8b font glyph table
drawGlyphAtCursor((uint16_t)cp);
} else if (!has8bFont) {
// Classic/7b font -- fold to ASCII
char folded = foldToAscii(cp);
if (folded) display.write((uint8_t)folded);
}
// else: codepoint outside font range, skip
pos += consumed;
}
}
}
// Render one glyph from the current 8b font at the display cursor position.
// Mimics Adafruit_GFX::drawChar() but supports 16-bit codepoints.
void GxEPDDisplay::drawGlyphAtCursor(uint16_t cp) {
if (!_currentFont || cp < _currentFont->first || cp > _currentFont->last) return;
uint16_t idx = cp - _currentFont->first;
GFXglyph* glyph = &_currentFont->glyph[idx];
uint8_t gw = glyph->width;
uint8_t gh = glyph->height;
int8_t xo = glyph->xOffset;
int8_t yo = glyph->yOffset;
uint16_t bo = glyph->bitmapOffset;
uint8_t xa = glyph->xAdvance;
int16_t cx = display.getCursorX();
int16_t cy = display.getCursorY();
if (gw > 0 && gh > 0) {
const uint8_t* bitmap = _currentFont->bitmap;
uint8_t bit = 0, bits = 0;
for (uint8_t yy = 0; yy < gh; yy++) {
for (uint8_t xx = 0; xx < gw; xx++) {
if (!(bit++ & 7)) bits = bitmap[bo++];
if (bits & 0x80) {
if (_currentTextScale == 1) {
display.drawPixel(cx + xo + xx, cy + yo + yy, _curr_color);
} else {
// Scaled rendering (clock face etc.)
display.fillRect(
cx + (xo + xx) * _currentTextScale,
cy + (yo + yy) * _currentTextScale,
_currentTextScale, _currentTextScale, _curr_color);
}
}
bits <<= 1;
}
}
}
// Advance cursor by xAdvance (scaled)
display.setCursor(cx + xa * _currentTextScale, cy);
}
void GxEPDDisplay::fillRect(int x, int y, int w, int h) {
@@ -227,16 +425,97 @@ void GxEPDDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) {
}
uint16_t GxEPDDisplay::getTextWidth(const char* str) {
if (!hasNonAscii(str)) {
// Pure ASCII fast path
int16_t x1, y1;
uint16_t w, h;
display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h);
return ceil((w + 1) / scale_x);
}
bool has8bFont = _currentFont && _currentFont->last > 0xFF;
if (has8bFont) {
// 8b font: sum xAdvance for each decoded codepoint
const uint8_t* s = (const uint8_t*)str;
int len = strlen(str);
int pos = 0;
int totalAdv = 0;
while (pos < len) {
int consumed;
uint32_t cp = utf8Decode(s + pos, len - pos, &consumed);
if (cp >= _currentFont->first && cp <= _currentFont->last) {
totalAdv += _currentFont->glyph[cp - _currentFont->first].xAdvance * _currentTextScale;
}
pos += consumed;
}
return ceil((totalAdv + 1) / scale_x);
}
// Classic/7b: fold to ASCII, then measure the folded string
char folded[256];
const uint8_t* s = (const uint8_t*)str;
int len = strlen(str);
int pos = 0, fi = 0;
while (pos < len && fi < 254) {
uint8_t b = s[pos];
if (b < 0x80) {
folded[fi++] = (char)b;
pos++;
} else {
int consumed;
uint32_t cp = utf8Decode(s + pos, len - pos, &consumed);
char fc = foldToAscii(cp);
if (fc) folded[fi++] = fc;
pos += consumed;
}
}
folded[fi] = 0;
int16_t x1, y1;
uint16_t w, h;
display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h);
display.getTextBounds(folded, 0, 0, &x1, &y1, &w, &h);
return ceil((w + 1) / scale_x);
}
void GxEPDDisplay::endFrame() {
uint32_t crc = display_crc.finalize();
if (crc != last_display_crc_value) {
display.display(true); // Partial refresh
#ifdef EINK_FULL_REFRESH_ONLY
display.display(false); // Full refresh (SSD1681 doesn't support partial)
#else
display.display(true); // Partial refresh
#endif
last_display_crc_value = crc;
}
}
void GxEPDDisplay::translateUTF8ToBlocks(char* dest, const char* src, size_t dest_size) {
// If an 8b font is active, pass through UTF-8 bytes (font can render them)
if (_currentFont && _currentFont->last > 0xFF) {
strncpy(dest, src, dest_size - 1);
dest[dest_size - 1] = '\0';
return;
}
// Classic/7b font: fold accented chars to ASCII base letters
size_t j = 0;
const uint8_t* s = (const uint8_t*)src;
int len = strlen(src);
int pos = 0;
while (pos < len && j < dest_size - 1) {
uint8_t b = s[pos];
if (b >= 32 && b <= 126) {
dest[j++] = (char)b;
pos++;
} else if (b >= 0x80) {
int consumed;
uint32_t cp = utf8Decode(s + pos, len - pos, &consumed);
char folded = foldToAscii(cp);
if (folded) dest[j++] = folded;
pos += consumed;
} else {
pos++; // skip control chars
}
}
dest[j] = 0;
}
+20 -2
View File
@@ -20,7 +20,11 @@
#include <Fonts/FreeSans18pt7b.h>
// Meck custom font styles (Noto Sans, Montserrat)
// Define HAS_MECK_FONTS in platformio.ini build_flags for platforms with
// enough flash to carry the extra font data (~50KB for 8b Latin Extended).
#ifdef HAS_MECK_FONTS
#include "MeckFonts.h"
#endif
// Inline CRC32 for frame change detection (replaces bakercp/CRC32
// to avoid naming collision with PNGdec's bundled CRC32.h)
@@ -70,12 +74,25 @@ class GxEPDDisplay : public DisplayDriver {
uint16_t _curr_color;
FrameCRC32 display_crc;
int last_display_crc_value = 0;
const GFXfont* _currentFont = nullptr; // Track for UTF-8 rendering
uint8_t _currentTextScale = 1; // Track glyph scale factor
// Render one glyph from the current 8b font at the display's cursor position
void drawGlyphAtCursor(uint16_t cp);
public:
// Virtual canvas dimensions — default 128×128 (MeshCore standard).
// Override for displays where physical resolution / scale < 128.
#ifndef EINK_VIRTUAL_W
#define EINK_VIRTUAL_W 128
#endif
#ifndef EINK_VIRTUAL_H
#define EINK_VIRTUAL_H 128
#endif
#if defined(EINK_DISPLAY_MODEL)
GxEPDDisplay() : DisplayDriver(128, 128), display(EINK_DISPLAY_MODEL(PIN_DISPLAY_CS, PIN_DISPLAY_DC, PIN_DISPLAY_RST, PIN_DISPLAY_BUSY)) {}
GxEPDDisplay() : DisplayDriver(EINK_VIRTUAL_W, EINK_VIRTUAL_H), display(EINK_DISPLAY_MODEL(PIN_DISPLAY_CS, PIN_DISPLAY_DC, PIN_DISPLAY_RST, PIN_DISPLAY_BUSY)) {}
#else
GxEPDDisplay() : DisplayDriver(128, 128), display(GxEPD2_150_BN(DISP_CS, DISP_DC, DISP_RST, DISP_BUSY)) {}
GxEPDDisplay() : DisplayDriver(EINK_VIRTUAL_W, EINK_VIRTUAL_H), display(GxEPD2_150_BN(DISP_CS, DISP_DC, DISP_RST, DISP_BUSY)) {}
#endif
bool begin();
@@ -99,6 +116,7 @@ public:
void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override;
uint16_t getTextWidth(const char* str) override;
void endFrame() override;
void translateUTF8ToBlocks(char* dest, const char* src, size_t dest_size) override;
// --- Raw pixel access for MapScreen (bypasses scaling) ---
void drawPixelRaw(int16_t x, int16_t y, uint16_t color) {
@@ -59,6 +59,7 @@ build_flags =
-D PIN_USER_BTN=0
-D SDCARD_USE_SPI1
-D ARDUINO_LOOP_STACK_SIZE=32768
-D HAS_MECK_FONTS
build_src_filter = ${esp32_base.build_src_filter}
+<../variants/LilyGo_T5S3_EPaper_Pro>
lib_deps =
+22 -4
View File
@@ -30,10 +30,28 @@ public:
void begin();
void powerOff() override {
// Stop Bluetooth before power off
btStop();
// Don't call parent or enterDeepSleep - let normal shutdown continue
// Display will show "hibernating..." text
// True hibernate: deep sleep with no software wake sources.
// Only a hardware reset (reset button) or USB power-on wakes the device.
// BLE, WiFi, 4G, GPS, and LoRa are already shut down by UITask
// before this method is called.
btStop(); // Belt and suspenders -- BLE controller stop
// Cut power to peripherals (keyboard, BQ27220, sensors)
pinMode(PIN_PERF_POWERON, OUTPUT);
digitalWrite(PIN_PERF_POWERON, LOW);
// Cut power to LoRa module (radio already in standby from radio_driver.powerOff)
#ifdef P_LORA_EN
digitalWrite(P_LORA_EN, LOW);
#endif
// Hold LoRa NSS high to prevent SX1262 drawing current from floating CS
rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS);
// Enter deep sleep with no wake sources -- only hardware reset wakes
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_OFF);
esp_deep_sleep_start();
}
void enterDeepSleep(uint32_t secs, int pin_wake_btn) {
+1
View File
@@ -87,6 +87,7 @@ build_flags =
-D CST328_PIN_INT=12
-D CST328_PIN_RST=38
-D ARDUINO_LOOP_STACK_SIZE=32768
-D HAS_MECK_FONTS
build_src_filter = ${esp32_base.build_src_filter}
+<../variants/LilyGo_TDeck_Pro>
+<helpers/sensors/*.cpp>
@@ -132,6 +132,7 @@ build_flags =
-D CST328_PIN_INT=12
-D CST328_PIN_RST=-1
-D ARDUINO_LOOP_STACK_SIZE=32768
-D HAS_MECK_FONTS
build_src_filter = ${esp32_base.build_src_filter}
; Include TDeckBoard.cpp from V1.1 (parent class with BQ27220 code)
+<../variants/LilyGo_TDeck_Pro/TDeckBoard.cpp>
@@ -0,0 +1,16 @@
#pragma once
// =============================================================================
// CPUPowerManager — no-op stub for nRF52840
//
// The nRF52840 does not support runtime CPU frequency scaling.
// This stub satisfies the #include in main.cpp without any effect.
// =============================================================================
class CPUPowerManager {
public:
void begin() {}
void setHighPerformance() {}
void setLowPower() {}
void loop() {}
};
@@ -0,0 +1,34 @@
#pragma once
// ---------------------------------------------------------------------------
// Radio presets — shared between SettingsScreen (UI) and MyMesh (Serial CLI)
// ---------------------------------------------------------------------------
struct RadioPreset {
const char* name;
float freq;
float bw;
uint8_t sf;
uint8_t cr;
uint8_t tx_power;
};
static const RadioPreset RADIO_PRESETS[] = {
{ "Australia", 915.800f, 250.0f, 10, 5, 22 },
{ "Australia (Narrow)", 916.575f, 62.5f, 7, 8, 22 },
{ "Australia: SA, WA", 923.125f, 62.5f, 8, 8, 22 },
{ "Australia: QLD", 923.125f, 62.5f, 8, 5, 22 },
{ "EU/UK (Narrow)", 869.618f, 62.5f, 8, 8, 14 },
{ "EU/UK (Long Range)", 869.525f, 250.0f, 11, 5, 14 },
{ "EU/UK (Medium Range)", 869.525f, 250.0f, 10, 5, 14 },
{ "Czech Republic (Narrow)",869.432f, 62.5f, 7, 5, 14 },
{ "EU 433 (Long Range)", 433.650f, 250.0f, 11, 5, 14 },
{ "New Zealand", 917.375f, 250.0f, 11, 5, 22 },
{ "New Zealand (Narrow)", 917.375f, 62.5f, 7, 5, 22 },
{ "Portugal 433", 433.375f, 62.5f, 9, 6, 14 },
{ "Portugal 868", 869.618f, 62.5f, 7, 6, 14 },
{ "Switzerland", 869.618f, 62.5f, 8, 8, 14 },
{ "USA/Canada (Recommended)",910.525f, 62.5f, 7, 5, 22 },
{ "Vietnam", 920.250f, 250.0f, 11, 5, 22 },
};
#define NUM_RADIO_PRESETS (sizeof(RADIO_PRESETS) / sizeof(RADIO_PRESETS[0]))
+120 -28
View File
@@ -1,5 +1,10 @@
// =============================================================================
// TechoCardBoard — Implementation for LilyGo T-Echo Card
//
// Patches applied from Meshtastic PR #10267 (caveman99):
// 1. RT9080 power rail reset cycle in begin() — prevents LoRa TX brown-out
// 2. Battery measurement control pin (P0.31) — enables voltage divider
// 3. WS2812 NeoPixel implementation via Adafruit_NeoPixel
// =============================================================================
#include "TechoCardBoard.h"
@@ -11,6 +16,49 @@
void TechoCardBoard::begin() {
NRF52BoardDCDC::begin();
// -------------------------------------------------------------------------
// RT9080 3V3 rail: clean reset cycle
//
// From Meshtastic PR #10267 (earlyInitVariant): if the nRF52840 was in a
// half-enabled state from a previous soft reset, the RT9080 LDO can be in
// an indeterminate state. Toggling EN HIGH→LOW→HIGH with 100ms dwell
// forces a clean power-on. Without this, the 3V3 rail can brown-out when
// LoRa TX fires at full power (+22 dBm).
// -------------------------------------------------------------------------
#if PIN_OLED_EN >= 0
pinMode(PIN_OLED_EN, OUTPUT);
digitalWrite(PIN_OLED_EN, HIGH);
delay(100);
digitalWrite(PIN_OLED_EN, LOW);
delay(100);
digitalWrite(PIN_OLED_EN, HIGH);
delay(100);
#endif
// -------------------------------------------------------------------------
// Park peripheral enable pins LOW before the rest of setup runs.
// Prevents peripherals from sinking current while the 3V3 rail is ramping.
// (Adapted from Meshtastic PR #10267 earlyInitVariant)
// -------------------------------------------------------------------------
#if defined(HAS_GPS) && PIN_GPS_EN >= 0
pinMode(PIN_GPS_EN, OUTPUT);
digitalWrite(PIN_GPS_EN, LOW);
#endif
#if defined(HAS_GPS) && PIN_GPS_RF_EN >= 0
pinMode(PIN_GPS_RF_EN, OUTPUT);
digitalWrite(PIN_GPS_RF_EN, LOW);
#endif
#if defined(HAS_BUZZER) && PIN_BUZZER >= 0
pinMode(PIN_BUZZER, OUTPUT);
digitalWrite(PIN_BUZZER, LOW);
#endif
// Configure battery measurement control pin
#if defined(BATTERY_MEASUREMENT_CONTROL)
pinMode(BATTERY_MEASUREMENT_CONTROL, OUTPUT);
digitalWrite(BATTERY_MEASUREMENT_CONTROL, !BATTERY_MEASUREMENT_ACTIVE);
#endif
// Configure battery ADC pin as analog input
pinMode(PIN_VBAT_READ, INPUT);
@@ -18,31 +66,33 @@ void TechoCardBoard::begin() {
pinMode(PIN_BUTTON_A, INPUT_PULLUP);
pinMode(PIN_BUTTON_BOOT, INPUT_PULLUP);
// Buzzer off
#if defined(HAS_BUZZER) && PIN_BUZZER >= 0
pinMode(PIN_BUZZER, OUTPUT);
digitalWrite(PIN_BUZZER, LOW);
#endif
// RGB LED off at boot
// Initialise WS2812 NeoPixels (3 independent LEDs, all off at boot)
#if defined(HAS_RGB_LED)
ledOff();
_pixel_power.begin();
_pixel_power.clear();
_pixel_power.show();
_pixel_notify.begin();
_pixel_notify.clear();
_pixel_notify.show();
_pixel_pairing.begin();
_pixel_pairing.clear();
_pixel_pairing.show();
#endif
}
// -----------------------------------------------------------------------------
// Battery voltage reading via nRF52840 SAADC
//
// The T-Echo Card has a voltage divider on AIN0 (P0.02).
// nRF52840 SAADC: 12-bit, internal 0.6V reference, configurable gain.
// With 1/6 gain: input range 03.6V. Multiply by divider ratio (ADC_MULTIPLIER).
// The T-Echo Card has a gated voltage divider on AIN0 (P0.02).
// BATTERY_MEASUREMENT_CONTROL (P0.31) must be driven HIGH to enable the
// divider before reading, and LOW after to avoid parasitic drain.
//
// NOTE: The T-Echo Lite has a known issue reading 100% / 6.00V constantly.
// This is likely caused by incorrect SAADC configuration (wrong gain, wrong
// reference, or the pin being pulled high by the charging circuit).
// We use explicit SAADC register programming to avoid that issue.
// nRF52840 SAADC: 12-bit, internal 0.6V reference, 1/6 gain.
// With 1/6 gain: input range 03.6V. Multiply by divider ratio (ADC_MULTIPLIER).
// -----------------------------------------------------------------------------
float TechoCardBoard::getBatteryVoltage() {
uint16_t TechoCardBoard::getBattMilliVolts() {
uint32_t now = millis();
// Cache battery reading — only read every 10 seconds
@@ -50,6 +100,12 @@ float TechoCardBoard::getBatteryVoltage() {
return _cached_battery_mv;
}
// Enable battery voltage divider
#if defined(BATTERY_MEASUREMENT_CONTROL)
digitalWrite(BATTERY_MEASUREMENT_CONTROL, BATTERY_MEASUREMENT_ACTIVE);
delay(5); // Allow divider to settle
#endif
// Configure SAADC for single-shot reading
NRF_SAADC->RESOLUTION = SAADC_RESOLUTION_VAL_12bit;
@@ -91,6 +147,11 @@ float TechoCardBoard::getBatteryVoltage() {
// Disable SAADC to save power
NRF_SAADC->ENABLE = SAADC_ENABLE_ENABLE_Disabled;
// Disable battery voltage divider to save power
#if defined(BATTERY_MEASUREMENT_CONTROL)
digitalWrite(BATTERY_MEASUREMENT_CONTROL, !BATTERY_MEASUREMENT_ACTIVE);
#endif
// Convert: voltage = (result / 4096) * 3.6V * ADC_MULTIPLIER * 1000 (mV)
if (result < 0) result = 0;
float voltage_mv = ((float)result / 4096.0f) * 3600.0f * _adc_multiplier;
@@ -98,18 +159,18 @@ float TechoCardBoard::getBatteryVoltage() {
_cached_battery_mv = voltage_mv;
_last_battery_read = now;
return voltage_mv;
return (uint16_t)voltage_mv;
}
uint8_t TechoCardBoard::getBatteryPercent() {
float mv = getBatteryVoltage();
if (mv <= 0) return 0;
uint16_t mv = getBattMilliVolts();
if (mv == 0) return 0;
// Simple linear approximation for single-cell LiPo
// 3200 mV = 0%, 4200 mV = 100%
if (mv >= 4200.0f) return 100;
if (mv <= 3200.0f) return 0;
return (uint8_t)(((mv - 3200.0f) / 1000.0f) * 100.0f);
if (mv >= 4200) return 100;
if (mv <= 3200) return 0;
return (uint8_t)(((uint32_t)(mv - 3200) * 100) / 1000);
}
// -----------------------------------------------------------------------------
@@ -137,14 +198,24 @@ void TechoCardBoard::enableSpeaker(bool enable) {
}
// -----------------------------------------------------------------------------
// RGB LED — WS2812 via NeoPixel protocol
// Simple bit-bang implementation for nRF52840 at 64MHz
// RGB LEDs — WS2812 via Adafruit_NeoPixel
//
// Three independent WS2812s on separate data pins (not a chain).
// Each is a 1-pixel NeoPixel strand.
//
// setLED() drives all three to the same colour (legacy interface).
// For per-LED control, use setStatusLED() with a role index.
// -----------------------------------------------------------------------------
void TechoCardBoard::setLED(uint8_t r, uint8_t g, uint8_t b) {
#if defined(HAS_RGB_LED)
// TODO: Implement WS2812 bit-bang or use Adafruit_NeoPixel library
// For initial bringup, just use the Adafruit_NeoPixel library
// which is available in the Adafruit nRF52 Arduino core
uint32_t color = Adafruit_NeoPixel::Color(r, g, b);
_pixel_power.setPixelColor(0, color);
_pixel_power.show();
_pixel_notify.setPixelColor(0, color);
_pixel_notify.show();
_pixel_pairing.setPixelColor(0, color);
_pixel_pairing.show();
#else
(void)r; (void)g; (void)b;
#endif
}
@@ -153,6 +224,27 @@ void TechoCardBoard::ledOff() {
setLED(0, 0, 0);
}
void TechoCardBoard::setStatusLED(uint8_t led_index, uint32_t color) {
#if defined(HAS_RGB_LED)
switch (led_index) {
case 0: // Power / charge
_pixel_power.setPixelColor(0, color);
_pixel_power.show();
break;
case 1: // Notification / mesh activity
_pixel_notify.setPixelColor(0, color);
_pixel_notify.show();
break;
case 2: // BLE pairing
_pixel_pairing.setPixelColor(0, color);
_pixel_pairing.show();
break;
}
#else
(void)led_index; (void)color;
#endif
}
// -----------------------------------------------------------------------------
// Buzzer — PWM tone generation
// -----------------------------------------------------------------------------
@@ -166,4 +258,4 @@ void TechoCardBoard::buzz(uint16_t freq_hz, uint16_t duration_ms) {
#else
(void)freq_hz; (void)duration_ms;
#endif
}
}
@@ -4,10 +4,11 @@
// TechoCardBoard — Board class for LilyGo T-Echo Card
//
// Extends NRF52BoardDCDC with:
// - Battery ADC (AIN0, P0.02) with solar charging via BQ25896
// - Battery ADC (AIN0, P0.02) with gated voltage divider (P0.31)
// - Solar charging via BQ25896
// - GPS power control (L76K)
// - Speaker/mic enable
// - RGB LED control
// - WS2812 RGB LEDs (3 independent, via Adafruit_NeoPixel)
// - Buzzer
// - NFC NDEF contact sharing
// =============================================================================
@@ -16,6 +17,11 @@
#include <helpers/NRF52Board.h>
#include "variant.h"
// WS2812 NeoPixel support — 3 independent LEDs on separate GPIOs
#if defined(HAS_RGB_LED)
#include <Adafruit_NeoPixel.h>
#endif
#ifdef NRF52_POWER_MANAGEMENT
// Power management config for T-Echo Card
// AIN0 (P0.02) for battery voltage sensing
@@ -33,6 +39,13 @@ private:
uint32_t _last_battery_read;
float _cached_battery_mv;
// Three independent WS2812 NeoPixels (1 LED per strand)
#if defined(HAS_RGB_LED)
Adafruit_NeoPixel _pixel_power = Adafruit_NeoPixel(1, PIN_RGB_LED_1, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel _pixel_notify = Adafruit_NeoPixel(1, PIN_RGB_LED_2, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel _pixel_pairing = Adafruit_NeoPixel(1, PIN_RGB_LED_3, NEO_GRB + NEO_KHZ800);
#endif
public:
TechoCardBoard()
: _adc_multiplier(ADC_MULTIPLIER),
@@ -42,10 +55,13 @@ public:
void begin() override;
// Battery
float getBatteryVoltage() override;
uint8_t getBatteryPercent() override;
float getAdcMultiplier() override { return _adc_multiplier; }
void setAdcMultiplier(float mult) { _adc_multiplier = mult; }
uint16_t getBattMilliVolts() override;
uint8_t getBatteryPercent();
float getAdcMultiplier() const override { return _adc_multiplier; }
bool setAdcMultiplier(float mult) override { _adc_multiplier = mult; return true; }
// Board identity
const char* getManufacturerName() const override { return "LilyGo T-Echo Card"; }
// GPS power control
void enableGPS(bool enable);
@@ -53,16 +69,20 @@ public:
// Speaker power control
void enableSpeaker(bool enable);
// RGB LED
// RGB LEDs — all three to same colour (legacy interface)
void setLED(uint8_t r, uint8_t g, uint8_t b);
void ledOff();
// Per-LED status control (0=power, 1=notify, 2=pairing)
// colour is packed 0xRRGGBB — use NEOPIXEL_COLOR_* defines from variant.h
void setStatusLED(uint8_t led_index, uint32_t color);
// Buzzer
void buzz(uint16_t freq_hz, uint16_t duration_ms);
#ifdef NRF52_POWER_MANAGEMENT
const PowerMgtConfig* getPowerConfig() const override {
const PowerMgtConfig* getPowerConfig() const {
return &TECHO_CARD_POWER_CONFIG;
}
#endif
};
};
+15 -7
View File
@@ -7,14 +7,18 @@
// Only needed if creating a custom board variant inside the Adafruit nRF52
// Arduino framework package. If using build flag overrides in platformio.ini,
// this file is optional.
//
// Pin mapping cross-referenced against:
// - LilyGo official: T-Echo-Card/libraries/private_library/t_echo_card_config.h
// - Meshtastic PR #10267 (caveman99 T-Echo-Card support)
// =============================================================================
// On nRF52840, Arduino digital pin numbers map 1:1 to nRF GPIO numbers
// (047 for port 0 and port 1).
// LED
// LED — WS2812 addressable (no plain GPIO LED on this board)
#define LED_BUILTIN PIN_LED1
#define PIN_LED1 39 // WS2812 RGB LED data (1, 7)
#define PIN_LED1 39 // WS2812 RGB LED data 1 (1, 7)
#define LED_STATE_ON 1
// Buttons
@@ -39,11 +43,15 @@
// Analog
#define PIN_A0 2 // (0, 2) — Battery ADC / AIN0
// QSPI Flash (ZD25WQ32CEIGR 4MB)
// nRF52840 QSPI uses fixed pins — framework handles these
// #define PIN_QSPI_SCK 19 // Conflict with GPS TX — check if QSPI is on different pins
// NOTE: QSPI pin mapping needs verification on actual hardware.
// The T-Echo Card may use SPI (not QSPI) for external flash.
// QSPI Flash ZD25WQ32CEIGR 4MB
// Confirmed from LilyGo t_echo_card_config.h and Meshtastic PR #10267.
// These are on a dedicated SPI bus, separate from LoRa SPI.
#define PIN_QSPI_SCK 4 // (0, 4)
#define PIN_QSPI_CS 12 // (0, 12)
#define PIN_QSPI_IO0 6 // (0, 6) — MOSI / D0
#define PIN_QSPI_IO1 8 // (0, 8) — MISO / D1
#define PIN_QSPI_IO2 41 // (1, 9) — WP / D2
#define PIN_QSPI_IO3 26 // (0, 26) — HOLD / D3
// NFC (dedicated nRF52840 NFC pins — not GPIO-assignable)
// NFC1 = P0.09, NFC2 = P0.10
+38 -4
View File
@@ -17,11 +17,29 @@ extra_scripts =
create-uf2.py
build_flags = ${nrf52_base.build_flags}
-I variants/lilygo_techo_card
-I src/helpers/ui
-D LILYGO_TECHO_CARD
-D NRF52_POWER_MANAGEMENT
-D WIRE_INTERFACES_COUNT=1
-D SPI_INTERFACES_COUNT=1
-D ps_calloc=calloc
; I2C
-D PIN_BOARD_SDA=36
-D PIN_BOARD_SCL=34
-D PIN_WIRE_SDA=36
-D PIN_WIRE_SCL=34
-D PIN_SPI_MISO=17
-D PIN_SPI_SCK=13
-D PIN_SPI_MOSI=15
-D USE_LFXO
-D LED_BLUE=39
-D LED_BUILTIN=39
-D LED_STATE_ON=1
-D PINS_COUNT=48
-D NUM_DIGITAL_PINS=48
-D NUM_ANALOG_INPUTS=6
-D PIN_SERIAL1_RX=21
-D PIN_SERIAL1_TX=19
; LoRa SX1262 (HPB16B3 module)
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
@@ -36,14 +54,15 @@ build_flags = ${nrf52_base.build_flags}
-D SX126X_CURRENT_LIMIT=140
-D SX126X_RX_BOOSTED_GAIN=1
-D SX126X_DIO2_AS_RF_SWITCH=1
; Display — SSD1315 OLED (72×40, I2C, SSD1306-compatible)
-D DISPLAY_CLASS=SSD1306Display
; Display — not defined in base; added per-env when needed
; (companion BLE skips display — phone app is the UI)
-D PIN_OLED_RESET=-1
; GPS — L76K
-D HAS_GPS=1
-D PIN_GPS_TX=19
-D PIN_GPS_RX=21
-D PIN_GPS_EN=47
-D GPS_EN_ACTIVE=HIGH
-D GPS_BAUDRATE=9600
-D ENV_INCLUDE_GPS=1
; Battery ADC
@@ -54,9 +73,12 @@ build_flags = ${nrf52_base.build_flags}
-D BOARD_CLASS=TechoCardBoard
build_src_filter = ${nrf52_base.build_src_filter}
+<../variants/lilygo_techo_card/*.cpp>
+<helpers/sensors>
lib_deps = ${nrf52_base.lib_deps}
olikraus/U8g2 @ ^2.35.19
stevemarple/MicroNMEA @ ^2.0.6
adafruit/Adafruit NeoPixel @ ^1.12.3
end2endzone/NonBlockingRtttl @ ^1.3.0
; =============================================================================
; Build Environments
@@ -64,16 +86,28 @@ lib_deps = ${nrf52_base.lib_deps}
; --- BLE Companion Radio ---
; Pairs with MeshCore companion app (Android/iOS/Web) over Bluetooth.
; Includes GPS for location and time sync.
; No DISPLAY_CLASS — phone app is the primary interface.
; OLED status screen will be added as a follow-up.
[env:meck_techo_card_companion_radio_ble]
extends = lilygo_techo_card
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
board_upload.maximum_size = 712704
build_flags = ${lilygo_techo_card.build_flags}
-I examples/companion_radio
-D FIRMWARE_NAME='"Meck T-Echo Card BLE"'
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D BLE_PIN_CODE=123456
-D OFFLINE_QUEUE_SIZE=256
; Debug (disable for release)
; -D MESH_DEBUG
; -D BLE_DEBUG_LOGGING
build_src_filter = ${lilygo_techo_card.build_src_filter}
+<helpers/ui/buzzer.cpp>
+<helpers/nrf52/SerialBLEInterface.cpp>
+<../examples/companion_radio/*.cpp>
lib_deps = ${lilygo_techo_card.lib_deps}
densaugeo/base64 @ ~1.4.0
; --- Repeater ---
; Standalone LoRa repeater node. GPS for position adverts.
@@ -103,4 +137,4 @@ extends = lilygo_techo_card
build_flags = ${lilygo_techo_card.build_flags}
-D FIRMWARE_NAME='"Meck T-Echo Card Sensor"'
build_src_filter = ${lilygo_techo_card.build_src_filter}
+<../examples/simple_sensor/*.cpp>
+<../examples/simple_sensor/*.cpp>
+73 -41
View File
@@ -1,72 +1,71 @@
// =============================================================================
// MeshCore target implementation for LilyGo T-Echo Card
//
// nRF52840 + SX1262 (HPB16B3 module) + SSD1315 OLED + L76K GPS
// nRF52840 + SX1262 (HPB16B3 / S62F module) + SSD1315 OLED + L76K GPS
// =============================================================================
#include <Arduino.h>
#include "target.h"
#include "variant.h"
// --- SPI for LoRa radio (software SPI on nRF52) ---
// The HPB16B3 SX1262 module uses dedicated SPI pins, not shared with anything else.
// RadioLib Module handles the SPI internally when given pin numbers.
static Module radio_module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY);
// --- Radio driver ---
RADIO_CLASS radio_driver(&radio_module);
WRAPPER_CLASS radio_wrapper(radio_driver);
// --- Board ---
TechoCardBoard board;
// --- Display (SSD1306-compatible, SSD1315 at 0x3C, 72x40) ---
#ifdef DISPLAY_CLASS
DISPLAY_CLASS display(OLED_WIDTH, OLED_HEIGHT);
// --- Clock ---
// No hardware RTC on T-Echo Card — VolatileRTCClock tracks time via millis().
// Time gets set from GPS lock or BLE companion app sync.
// AutoDiscoverRTCClock probes I2C for hardware RTCs; if none found, uses fallback.
VolatileRTCClock fallback_clock;
AutoDiscoverRTCClock rtc_clock(fallback_clock);
// --- Radio ---
// nRF52 Adafruit BSP SPIClass requires (peripheral, MISO, SCK, MOSI)
#if defined(P_LORA_SCLK)
static SPIClass spi(NRF_SPIM3, P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI);
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi);
#else
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY);
#endif
// --- MeshCore stores ---
mesh::IdentityStore identity_store;
mesh::NodePrefs node_prefs;
mesh::DataStore data_store;
WRAPPER_CLASS radio_driver(radio, board);
// --- Display ---
#ifdef DISPLAY_CLASS
DISPLAY_CLASS display;
#endif
// --- Sensor manager ---
#if defined(ENV_INCLUDE_GPS) && ENV_INCLUDE_GPS
#if ENV_INCLUDE_GPS
#include <helpers/sensors/MicroNMEALocationProvider.h>
static MicroNMEALocationProvider gps_provider(Serial1);
EnvironmentSensorManager sensor_manager(gps_provider);
EnvironmentSensorManager sensors(gps_provider);
#else
EnvironmentSensorManager sensor_manager;
EnvironmentSensorManager sensors;
#endif
// --- Target initialization ---
void target_setup() {
// Enable OLED power
#if PIN_OLED_EN >= 0
pinMode(PIN_OLED_EN, OUTPUT);
digitalWrite(PIN_OLED_EN, HIGH);
delay(10);
#endif
bool radio_init() {
// Board-level init — cycles RT9080 3V3 rail, parks peripheral pins LOW
board.begin();
// Enable GPS power
// Enable GPS power (was parked LOW in board.begin())
#if defined(HAS_GPS) && PIN_GPS_EN >= 0
pinMode(PIN_GPS_EN, OUTPUT);
digitalWrite(PIN_GPS_EN, HIGH);
delay(10);
#endif
// GPS RF/LNA enable
#if defined(HAS_GPS) && PIN_GPS_RF_EN >= 0
pinMode(PIN_GPS_RF_EN, OUTPUT);
digitalWrite(PIN_GPS_RF_EN, HIGH);
#endif
// Initialize GPS UART
// Initialise GPS UART
#if defined(HAS_GPS)
Serial1.setPins(PIN_GPS_RX, PIN_GPS_TX);
Serial1.begin(GPS_BAUDRATE);
#endif
// Speaker off by default (save power)
// Speaker off by default
#if defined(HAS_SPEAKER)
pinMode(PIN_SPK_EN, OUTPUT);
digitalWrite(PIN_SPK_EN, LOW);
@@ -76,20 +75,53 @@ void target_setup() {
#endif
#endif
// Initialize I2C
Wire.setPins(I2C_SDA, I2C_SCL);
// Initialise I2C
Wire.begin();
Wire.setClock(400000);
// Initialize display
// Initialise clocks — probe I2C for hardware RTCs, fall back to VolatileRTCClock
rtc_clock.begin(Wire);
// Initialise display
#ifdef DISPLAY_CLASS
display.begin(OLED_I2C_ADDR);
display.begin();
#endif
// Board-level init
board.begin();
// SX1262 DIO2 as RF switch control
radio.setDio2AsRfSwitch(true);
// Initialize LoRa radio
// SX1262 DIO2 as RF switch control (common for HPB16B3 modules)
radio_driver.setDio2AsRfSwitch(true);
// SX1262 TCXO via DIO3 (1.8V, from Meshtastic PR #10267)
// TODO: Verify on hardware — if module uses crystal, remove this call
radio.setTCXO(1.8f);
// Initialise radio
#if defined(P_LORA_SCLK)
return radio.std_init(&spi);
#else
return radio.std_init();
#endif
}
uint32_t radio_get_rng_seed() {
return radio.random(0x7FFFFFFF);
}
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
radio.setFrequency(freq);
radio.setSpreadingFactor(sf);
radio.setBandwidth(bw);
radio.setCodingRate(cr);
}
void radio_set_tx_power(int8_t dbm) {
radio.setOutputPower(dbm);
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng);
}
void radio_reset_agc() {
radio.setRxBoostedGainMode(true);
}
+19 -15
View File
@@ -5,35 +5,39 @@
// =============================================================================
#include <Arduino.h>
#include <Wire.h>
#include <SPI.h>
#include <RadioLib.h>
#include <helpers/ArduinoHelpers.h>
#include <helpers/RadioLibWrappers.h>
#include <helpers/radiolib/RadioLibWrappers.h>
#include <helpers/NRF52Board.h>
#include <helpers/CustomSX1262Wrapper.h>
#include <helpers/IdentityStore.h>
#include <helpers/NodePrefs.h>
#include <helpers/DataStore.h>
#include <helpers/SensorManager.h>
#include <helpers/radiolib/CustomSX1262Wrapper.h>
#include <helpers/AutoDiscoverRTCClock.h>
#ifdef DISPLAY_CLASS
#include <helpers/ui/DisplaySSD1306.h>
#include <helpers/ui/SSD1306Display.h>
#endif
#include <helpers/sensors/EnvironmentSensorManager.h>
#include "TechoCardBoard.h"
// Hardware object declarations (instantiated in target.cpp)
extern RADIO_CLASS radio_driver;
extern WRAPPER_CLASS radio_wrapper;
extern RADIO_CLASS radio;
extern WRAPPER_CLASS radio_driver;
extern TechoCardBoard board;
extern AutoDiscoverRTCClock rtc_clock;
#ifdef DISPLAY_CLASS
extern DISPLAY_CLASS display;
#endif
extern mesh::IdentityStore identity_store;
extern mesh::NodePrefs node_prefs;
extern mesh::DataStore data_store;
extern EnvironmentSensorManager sensor_manager;
extern EnvironmentSensorManager sensors;
// Target initialization
void target_setup();
// Target functions — called from main.cpp and MyMesh.cpp
bool radio_init();
uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(int8_t dbm);
mesh::LocalIdentity radio_new_identity();
void radio_reset_agc();
@@ -0,0 +1,18 @@
// =============================================================================
// g_ADigitalPinMap — nRF52840 Arduino pin to GPIO mapping
//
// Required by the Adafruit nRF52 BSP (SPI.cpp, Wire, NeoPixel, etc.).
// On nRF52840, Arduino pin numbers map 1:1 to nRF GPIO numbers.
// 48 entries: P0.00P0.31 (031) + P1.00P1.15 (3247)
// =============================================================================
#include <Arduino.h>
const uint32_t g_ADigitalPinMap[] = {
0, 1, 2, 3, 4, 5, 6, 7, // P0.00 P0.07
8, 9, 10, 11, 12, 13, 14, 15, // P0.08 P0.15
16, 17, 18, 19, 20, 21, 22, 23, // P0.16 P0.23
24, 25, 26, 27, 28, 29, 30, 31, // P0.24 P0.31
32, 33, 34, 35, 36, 37, 38, 39, // P1.00 P1.07
40, 41, 42, 43, 44, 45, 46, 47, // P1.08 P1.15
};
+84 -20
View File
@@ -7,6 +7,10 @@
// + MP34DT05 PDM Microphone + ICM20948 IMU + BQ25896 Charger + Solar
//
// Pin notation from LilyGo pinmap: (port, pin) → nRF GPIO = port*32 + pin
//
// Cross-referenced against:
// - LilyGo official: T-Echo-Card/libraries/private_library/t_echo_card_config.h
// - Meshtastic PR #10267 (caveman99 T-Echo-Card support)
// =============================================================================
#define LILYGO_TECHO_CARD
@@ -30,14 +34,24 @@
#define P_LORA_BUSY 14 // (0, 14) — BUSY
#define P_LORA_DIO_1 40 // (1, 8) — DIO1 / interrupt
// RF switch control (HPB16B3 module)
#define LORA_DIO2 5 // (0, 5) — DIO2 (TXCO / RF switch)
#define LORA_RF_VC1 27 // (0, 27) — RF_VC1
#define LORA_RF_VC2 33 // (1, 1) — RF_VC2
// RF switch control (HPB16B3 / S62F module)
// DIO2 is used internally by the SX1262 for RF switch control.
// RF_VC1 / RF_VC2 are external PA/LNA select lines.
// Meshtastic PR #10267 maps these as TXEN/RXEN — verify on hardware
// whether DIO2-as-RF-switch alone is sufficient, or if VC1/VC2 are also
// needed for full TX/RX performance.
#define LORA_DIO2 5 // (0, 5) — DIO2 (RF switch / TXCO)
#define LORA_RF_VC1 27 // (0, 27) — RF_VC1 (potential TXEN)
#define LORA_RF_VC2 33 // (1, 1) — RF_VC2 (potential RXEN)
// LoRa radio power enable (from schematic: LORA_EN drives RT9080 for LORA_VDD)
// NOTE: Confirm actual GPIO — pinmap shows dedicated enable pin
// For now use -1 if always powered
// SX1262 TCXO voltage via DIO3
// Meshtastic PR #10267 sets this to 1.8V. Without it, the TCXO may not
// start and the radio will have frequency drift or fail to init.
// Confirm on hardware — if the module has a TCXO fed by DIO3, this is needed.
#define SX126X_DIO3_TCXO_VOLTAGE 1.8f
// LoRa radio power: RT9080 controls the 3V3 rail for all peripherals
// including LoRa. No dedicated LoRa power enable pin.
#define PIN_LORA_EN -1
// Default radio settings (Australia)
@@ -47,6 +61,21 @@
// -----------------------------------------------------------------------------
// 0.42" OLED Display — SSD1315 (SSD1306-compatible), 72×40, I2C
//
// The SSD1315 has a 128×64 GDDRAM, but the physical panel is only 72×40.
// The visible window is mapped at columns 2899, pages 37 (rows 2463).
// This means:
// - Horizontal: auto-centred by driver ((128 - 72) / 2 = 28)
// - Vertical: need SETDISPLAYOFFSET = 24 (3 pages × 8 rows) so that
// data written to pages 04 appears on the physical display.
//
// If the MeshCore OLEDDisplay driver's display() method sends PAGEADDR
// starting at page 0, the SETDISPLAYOFFSET command should handle the
// mapping. If content appears shifted or blank, the alternative is to
// modify the PAGEADDR commands to write to pages 37 directly.
//
// Ref: Meshtastic PR #10267 uses setYOffset(3) to shift every PAGEADDR
// write, plus GEOMETRY_72_40 which sets SETMULTIPLEX to 39.
// -----------------------------------------------------------------------------
#define HAS_OLED 1
#define OLED_I2C_ADDR 0x3C
@@ -55,7 +84,12 @@
#define OLED_SDA I2C_SDA
#define OLED_SCL I2C_SCL
// OLED power control via RT9080 enable pin
// SSD1315 display offset: 3 pages = 24 rows
// Applied via SETDISPLAYOFFSET (0xD3) after display.begin() in target.cpp
#define OLED_DISPLAY_OFFSET 24
// OLED / peripheral power control via RT9080 enable pin
// This controls the 3V3 rail for OLED, GPS, LoRa, and sensors.
#define PIN_OLED_EN 30 // (0, 30) — RT9080_EN
// No hardware reset pin for OLED on T-Echo Card
@@ -80,11 +114,18 @@
#define BATTERY_ADC_AIN 0 // nRF SAADC AIN channel number
#define BATTERY_CAPACITY_MAH 800
// Battery voltage divider enable gate
// P0.31 controls a FET/switch that enables the resistive divider feeding
// AIN0. Must be driven HIGH before ADC read and LOW after to avoid
// parasitic drain through the divider.
// Source: LilyGo t_echo_card_config.h → BATTERY_MEASUREMENT_CONTROL
// Confirmed: Meshtastic PR #10267 → ADC_CTRL (0 + 31), ADC_CTRL_ENABLED HIGH
#define BATTERY_MEASUREMENT_CONTROL 31 // (0, 31)
#define BATTERY_MEASUREMENT_ACTIVE HIGH
// Battery voltage divider calibration
// The T-Echo Lite has issues reading battery correctly — we'll need to
// calibrate this with actual hardware. Starting with a reasonable default.
// nRF52840 SAADC: 0.6V internal ref, 1/6 gain → 03.6V range
// If there's a voltage divider (e.g. 2:1), multiply by 2.
// With 2:1 resistive divider, multiply by 2 to get actual cell voltage.
// Adjust ADC_MULTIPLIER after measuring real voltage vs ADC reading.
#ifndef ADC_MULTIPLIER
#define ADC_MULTIPLIER 2.0f
@@ -132,13 +173,28 @@
#define PIN_BUZZER 38 // (1, 6) — piezo buzzer data / PWM
// -----------------------------------------------------------------------------
// WS2812 RGB LED (3 LEDs in series)
// WS2812 RGB LEDs — 3 independent LEDs on separate data lines (NOT a chain)
//
// Confirmed by Meshtastic PR #10267: each WS2812 is on its own GPIO,
// driven as a 1-pixel NeoPixel strand. The bare-die WS2812s are very
// bright at full intensity — scale to ~25% (0x40 max per channel).
//
// Role assignments (matching Meshtastic PR #10267):
// DATA_1 (P1.7) → power/charge status (red)
// DATA_2 (P1.12) → notification / mesh activity (green)
// DATA_3 (P0.28) → BLE pairing status (blue)
// -----------------------------------------------------------------------------
#define HAS_RGB_LED 1
#define PIN_RGB_LED_1 39 // (1, 7) — WS2812 data 1
#define PIN_RGB_LED_2 44 // (1, 12) — WS2812 data 2
#define PIN_RGB_LED_3 28 // (0, 28) — WS2812 data 3
// Typically only one data pin drives the chain; confirm wiring on hardware
#define PIN_RGB_LED_1 39 // (1, 7) — WS2812 data 1 (power/charge)
#define PIN_RGB_LED_2 44 // (1, 12) — WS2812 data 2 (notification)
#define PIN_RGB_LED_3 28 // (0, 28) — WS2812 data 3 (BLE pairing)
// Default NeoPixel colours at 25% brightness (0x40 max per channel)
#define NEOPIXEL_COLOR_POWER 0x400000 // red
#define NEOPIXEL_COLOR_NOTIFY 0x004000 // green
#define NEOPIXEL_COLOR_PAIRING 0x000040 // blue
// Legacy aliases (kept for any code referencing these)
#define PIN_NEOPIXEL PIN_RGB_LED_1
#define NUM_NEOPIXELS 3
@@ -159,14 +215,22 @@
#define HAS_NFC 1
// -----------------------------------------------------------------------------
// External Flash — ZD25WQ32CEIGR (4MB SPI Flash)
// Uses QSPI interface on nRF52840
// Pin mapping from nRF52840 QSPI peripheral (typically fixed)
// External Flash — ZD25WQ32CEIGR (4MB QSPI Flash)
//
// Pin mapping confirmed from LilyGo t_echo_card_config.h and Meshtastic
// PR #10267. These are on a separate SPI bus from LoRa.
// The Adafruit nRF52 core supports QSPI via Adafruit_SPIFlash + LittleFS.
// -----------------------------------------------------------------------------
#define HAS_EXT_FLASH 1
#define PIN_QSPI_SCK 4 // (0, 4)
#define PIN_QSPI_CS 12 // (0, 12)
#define PIN_QSPI_IO0 6 // (0, 6) — MOSI / D0
#define PIN_QSPI_IO1 8 // (0, 8) — MISO / D1
#define PIN_QSPI_IO2 41 // (1, 9) — WP / D2
#define PIN_QSPI_IO3 26 // (0, 26) — HOLD / D3
// -----------------------------------------------------------------------------
// No SD Card on T-Echo Card (unlike T-Echo which has e-paper + no SD either)
// No SD Card on T-Echo Card
// Settings stored in LittleFS on internal/external flash
// -----------------------------------------------------------------------------
// #define HAS_SDCARD
@@ -0,0 +1,15 @@
#pragma once
// CPUPowerManager.h — nRF52 no-op stub
// nRF52840 runs at fixed 64 MHz; no frequency scaling available.
// All methods are empty so main.cpp compiles without #ifdef guards.
class CPUPowerManager {
public:
void begin() {}
void loop() {}
void setBoost() {}
void setIdle() {}
void setLowPower() {}
void clearLowPower() {}
int getFrequencyMHz() { return 64; }
};
+19
View File
@@ -0,0 +1,19 @@
#pragma once
// FS.h — nRF52 compatibility stub
// ESP32 Arduino core provides this as the base filesystem abstraction.
// On nRF52, File and filesystem types come from Adafruit_LittleFS.
// This stub exists solely to satisfy #include <FS.h> in shared headers.
#include <Arduino.h>
#include <time.h> // struct tm, gmtime — implicit on ESP32, needs explicit on nRF52
// ESP32 FS.h defines these mode strings; some shared code references them
#ifndef FILE_READ
#define FILE_READ "r"
#endif
#ifndef FILE_WRITE
#define FILE_WRITE "w"
#endif
#ifndef FILE_APPEND
#define FILE_APPEND "a"
#endif
+43
View File
@@ -0,0 +1,43 @@
#pragma once
// SD.h — nRF52 compatibility stub for Meck
// Maps Arduino SD API to Adafruit InternalFS (LittleFS on QSPI flash).
// T-Echo Lite has no SD card slot; file operations use internal flash.
#include <Adafruit_LittleFS.h>
#include <InternalFileSystem.h>
#include <time.h> // struct tm, gmtime — implicit on ESP32, explicit on nRF52
// ESP32 SD uses string file modes; define them here for compile compatibility
#ifndef FILE_READ
#define FILE_READ "r"
#endif
#ifndef FILE_WRITE
#define FILE_WRITE "w"
#endif
class SDClass {
public:
// InternalFS is already initialised by main — begin() is a no-op
bool begin(uint8_t cs = 0) { return true; }
// Accept any extra args (cs, SPI, freq) without complaint
template<typename... Args>
bool begin(Args...) { return true; }
bool exists(const char* path) { return InternalFS.exists(path); }
bool remove(const char* path) { return InternalFS.remove(path); }
bool mkdir(const char* path) { return InternalFS.mkdir(path); }
// String mode overload — matches ESP32 SD API (FILE_READ="r", FILE_WRITE="w", "r+")
Adafruit_LittleFS_Namespace::File open(const char* path, const char* mode = "r") {
uint8_t m = FILE_O_READ;
if (mode) {
if (mode[0] == 'w') m = FILE_O_WRITE;
else if (mode[0] == 'r' && mode[1] == '+') m = FILE_O_WRITE;
}
return InternalFS.open(path, m);
}
};
// Static instance per translation unit — no state (just forwards to InternalFS singleton)
static SDClass SD;
@@ -0,0 +1,54 @@
#include <Arduino.h>
#include <Wire.h>
#include "TechoBoard.h"
#ifdef LILYGO_TECHO
void TechoBoard::begin() {
NRF52Board::begin();
// Configure battery measurement control BEFORE Wire.begin()
// to ensure P0.02 is not claimed by another peripheral
pinMode(PIN_VBAT_MEAS_EN, OUTPUT);
digitalWrite(PIN_VBAT_MEAS_EN, LOW);
pinMode(PIN_VBAT_READ, INPUT);
Wire.begin();
pinMode(SX126X_POWER_EN, OUTPUT);
digitalWrite(SX126X_POWER_EN, HIGH);
delay(10);
}
uint16_t TechoBoard::getBattMilliVolts() {
// Use LilyGo's exact ADC configuration
analogReference(AR_INTERNAL_3_0);
analogReadResolution(12);
// Enable battery voltage divider (MOSFET gate on P0.31)
pinMode(PIN_VBAT_MEAS_EN, OUTPUT);
digitalWrite(PIN_VBAT_MEAS_EN, HIGH);
// Reclaim P0.02 for analog input (in case another peripheral touched it)
pinMode(PIN_VBAT_READ, INPUT);
delay(10); // let divider + ADC settle
// Read and average (matching LilyGo's approach)
uint32_t sum = 0;
for (int i = 0; i < 8; i++) {
sum += analogRead(PIN_VBAT_READ);
delayMicroseconds(100);
}
uint16_t adc = sum / 8;
// Disable divider to save power
digitalWrite(PIN_VBAT_MEAS_EN, LOW);
// LilyGo's exact formula: adc * (3000.0 / 4096.0) * 2.0
// = adc * 0.73242188 * 2.0 = adc * 1.46484375
uint16_t millivolts = (uint16_t)((float)adc * (3000.0f / 4096.0f) * 2.0f);
return millivolts;
}
#endif
@@ -0,0 +1,43 @@
#pragma once
#include <MeshCore.h>
#include <Arduino.h>
#include <helpers/NRF52Board.h>
// ============================================================
// T-Echo Lite battery pins — hardcoded from LilyGo t_echo_lite_config.h
// NOT using any defines from variant.h for battery measurement
// ============================================================
#define PIN_VBAT_READ _PINNUM(0, 2) // BATTERY_ADC_DATA
#define PIN_VBAT_MEAS_EN _PINNUM(0, 31) // BATTERY_MEASUREMENT_CONTROL
class TechoBoard : public NRF52BoardDCDC {
public:
TechoBoard() {}
void begin();
uint16_t getBattMilliVolts() override;
const char* getManufacturerName() const override {
return "LilyGo T-Echo Lite";
}
void powerOff() override {
digitalWrite(PIN_VBAT_MEAS_EN, LOW);
#ifdef LED_RED
digitalWrite(LED_RED, LOW);
#endif
#ifdef LED_GREEN
digitalWrite(LED_GREEN, LOW);
#endif
#ifdef LED_BLUE
digitalWrite(LED_BLUE, LOW);
#endif
#ifdef DISP_BACKLIGHT
digitalWrite(DISP_BACKLIGHT, LOW);
#endif
#ifdef PIN_PWR_EN
digitalWrite(PIN_PWR_EN, LOW);
#endif
sd_power_system_off();
}
};
@@ -0,0 +1,268 @@
; =============================================================================
; LilyGo T-Echo Lite — Meck variant configuration
;
; nRF52840 + SX1262 + GxEPD2 1.22" e-ink (176×192, GDEM0122T61/SSD1681)
; + CardKB via QWIIC (0x5F) + optional L76K GPS
;
; Display: GxEPD2_122_T61 — full refresh only (~2s), no fast/partial refresh.
; UI must minimise unnecessary redraws.
; Scale factors: 1.5×/2.0× give ~117×96 virtual coordinate space.
;
; Platform: nRF52 (Adafruit nRF52 Arduino)
; Board JSON: boards/t-echo.json (nRF52840 PCA10056 compatible)
; =============================================================================
; --- Base configuration for all T-Echo Lite Meck builds (with display) ---
[lilygo_techo_lite_meck]
extends = nrf52_base
board = t-echo
board_build.ldscript = boards/nrf52840_s140_v6.ld
extra_scripts = pre:patch_nrf52_bsp.py
build_flags = ${nrf52_base.build_flags}
-I variants/lilygo_techo_lite
-I src/helpers/nrf52
-I lib/nrf52/s140_nrf52_6.1.1_API/include
-I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52
-D LILYGO_TECHO
-D LILYGO_TECHO_LITE
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
-D LORA_TX_POWER=22
-D SX126X_POWER_EN=30
-D SX126X_CURRENT_LIMIT=140
-D SX126X_RX_BOOSTED_GAIN=1
; nRF52 compatibility — no PSRAM, no SD card, fallback GPS baud for CLI code paths
-D ps_calloc=calloc
-D ps_malloc=malloc
-D GPS_BAUDRATE=9600
-D SDCARD_CS=-1
-D ROW_AUTO_LOCK=255
; Display — GxEPD2 1.22" e-ink (176×192, SSD1681)
-D DISPLAY_CLASS=GxEPDDisplay
-D EINK_DISPLAY_MODEL=GxEPD2_122_T61
-D EINK_SCALE_X=1.5f
-D EINK_SCALE_Y=2.0f
-D EINK_X_OFFSET=6
-D EINK_Y_OFFSET=1
-D DISPLAY_ROTATION=4
-D EINK_FULL_REFRESH_ONLY=1
-D EINK_VIRTUAL_W=117
-D EINK_VIRTUAL_H=88
-D AUTO_OFF_MILLIS=0
build_src_filter = ${nrf52_base.build_src_filter}
+<helpers/*.cpp>
+<TechoBoard.cpp>
+<helpers/sensors/EnvironmentSensorManager.cpp>
+<helpers/ui/GxEPDDisplay.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<../variants/lilygo_techo_lite>
lib_deps =
${nrf52_base.lib_deps}
stevemarple/MicroNMEA @ ^2.0.6
adafruit/Adafruit BME280 Library @ ^2.3.0
https://github.com/SoulOfNoob/GxEPD2.git
bakercp/CRC32 @ ^2.0.0
debug_tool = jlink
upload_protocol = nrfutil
; =============================================================================
; Build Environments
; =============================================================================
; --- BLE Companion Radio (no GPS) ---
; Pairs with MeshCore companion app over Bluetooth.
; CardKB provides on-device text input for standalone messaging.
; No GPS — time synced via BLE companion or serial CLI.
[env:meck_techo_lite_ble]
extends = lilygo_techo_lite_meck
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
board_upload.maximum_size = 712704
build_flags =
${lilygo_techo_lite_meck.build_flags}
-I src/helpers/ui
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=250
-D MAX_GROUP_CHANNELS=8
-D CHANNEL_MSG_HISTORY_SIZE=20
-D BLE_PIN_CODE=123456
; -D BLE_DEBUG_LOGGING=1
-D OFFLINE_QUEUE_SIZE=64
-D MECK_CARDKB
-D UI_SENSORS_PAGE=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
-D AUTO_SHUTDOWN_MILLIVOLTS=2800
build_src_filter = ${lilygo_techo_lite_meck.build_src_filter}
+<helpers/nrf52/SerialBLEInterface.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${lilygo_techo_lite_meck.lib_deps}
densaugeo/base64 @ ~1.4.0
; --- Standalone Radio (no BLE, CardKB + display only) ---
; No companion app — device IS the terminal.
; USB serial for CLI configuration.
; Frees ~20-30KB BLE RAM → room for 500 contacts + larger message history.
[env:meck_techo_lite_standalone]
extends = lilygo_techo_lite_meck
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
board_upload.maximum_size = 712704
build_flags =
${lilygo_techo_lite_meck.build_flags}
-I src/helpers/ui
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=500
-D MAX_GROUP_CHANNELS=8
-D CHANNEL_MSG_HISTORY_SIZE=150
-D OFFLINE_QUEUE_SIZE=1
-D MECK_CARDKB
-D UI_SENSORS_PAGE=1
-D AUTO_SHUTDOWN_MILLIVOLTS=2800
build_src_filter = ${lilygo_techo_lite_meck.build_src_filter}
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${lilygo_techo_lite_meck.lib_deps}
densaugeo/base64 @ ~1.4.0
; --- BLE Companion Radio (with GPS) ---
; Same as above + L76K GPS for location and time sync.
; Requires external GPS module connected to UART1 (TX=P0.29, RX=P1.10).
[env:meck_techo_lite_gps_ble]
extends = lilygo_techo_lite_meck
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
board_upload.maximum_size = 712704
build_flags =
${lilygo_techo_lite_meck.build_flags}
-I src/helpers/ui
-I examples/companion_radio/ui-new
-D ENV_INCLUDE_GPS=1
-D GPS_BAUD_RATE=9600
-D PIN_GPS_EN=GPS_EN
-D MAX_CONTACTS=250
-D MAX_GROUP_CHANNELS=8
-D CHANNEL_MSG_HISTORY_SIZE=20
-D BLE_PIN_CODE=123456
-D OFFLINE_QUEUE_SIZE=64
-D MECK_CARDKB
-D UI_RECENT_LIST_SIZE=9
-D UI_SENSORS_PAGE=1
-D AUTO_SHUTDOWN_MILLIVOLTS=2800
build_src_filter = ${lilygo_techo_lite_meck.build_src_filter}
+<helpers/nrf52/SerialBLEInterface.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${lilygo_techo_lite_meck.lib_deps}
stevemarple/MicroNMEA @ ^2.0.6
densaugeo/base64 @ ~1.4.0
; --- Repeater ---
; Standalone LoRa repeater node. E-ink shows status.
; CardKB not useful here but included by base for consistency.
[env:meck_techo_lite_repeater]
extends = lilygo_techo_lite_meck
build_src_filter = ${lilygo_techo_lite_meck.build_src_filter}
+<../examples/simple_repeater>
build_flags =
${lilygo_techo_lite_meck.build_flags}
-D ADVERT_NAME='"Meck T-Echo Lite Repeater"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=50
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
; --- Room Server ---
; BBS-style message board node.
[env:meck_techo_lite_room_server]
extends = lilygo_techo_lite_meck
build_src_filter = ${lilygo_techo_lite_meck.build_src_filter}
+<../examples/simple_room_server>
build_flags =
${lilygo_techo_lite_meck.build_flags}
-D ADVERT_NAME='"Meck T-Echo Lite Room"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
; =============================================================================
; Headless (no display) variants
; =============================================================================
; --- Headless base (no display, no GxEPD2) ---
[lilygo_techo_lite_meck_core]
extends = nrf52_base
board = t-echo
board_build.ldscript = boards/nrf52840_s140_v6.ld
build_flags = ${nrf52_base.build_flags}
-I variants/lilygo_techo_lite
-I src/helpers/nrf52
-I lib/nrf52/s140_nrf52_6.1.1_API/include
-I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52
-D LILYGO_TECHO
-D LILYGO_TECHO_LITE
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
-D LORA_TX_POWER=22
-D SX126X_POWER_EN=30
-D SX126X_CURRENT_LIMIT=140
-D SX126X_RX_BOOSTED_GAIN=1
; nRF52 compatibility
-D ps_calloc=calloc
-D ps_malloc=malloc
-D GPS_BAUDRATE=9600
-D SDCARD_CS=-1
-D ROW_AUTO_LOCK=255
-D DISABLE_DIAGNOSTIC_OUTPUT
-D AUTO_OFF_MILLIS=0
build_src_filter = ${nrf52_base.build_src_filter}
+<helpers/*.cpp>
+<TechoBoard.cpp>
+<helpers/sensors/EnvironmentSensorManager.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<../variants/lilygo_techo_lite>
lib_deps =
${nrf52_base.lib_deps}
stevemarple/MicroNMEA @ ^2.0.6
adafruit/Adafruit BME280 Library @ ^2.3.0
bakercp/CRC32 @ ^2.0.0
debug_tool = jlink
upload_protocol = nrfutil
; --- Headless Repeater (no display — lowest power, outdoor deployment) ---
[env:meck_techo_lite_core_repeater]
extends = lilygo_techo_lite_meck_core
build_src_filter = ${lilygo_techo_lite_meck_core.build_src_filter}
+<../examples/simple_repeater>
build_flags =
${lilygo_techo_lite_meck_core.build_flags}
-D ADVERT_NAME='"Meck T-Echo Lite Core Repeater"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=50
; --- Headless BLE Companion (no display — phone-only UI) ---
[env:meck_techo_lite_core_ble]
extends = lilygo_techo_lite_meck_core
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
board_upload.maximum_size = 712704
build_flags =
${lilygo_techo_lite_meck_core.build_flags}
-D MAX_CONTACTS=250
-D MAX_GROUP_CHANNELS=8
-D CHANNEL_MSG_HISTORY_SIZE=20
-D BLE_PIN_CODE=234567
-D OFFLINE_QUEUE_SIZE=64
-D AUTO_SHUTDOWN_MILLIVOLTS=2800
build_src_filter = ${lilygo_techo_lite_meck_core.build_src_filter}
+<helpers/nrf52/SerialBLEInterface.cpp>
+<../examples/companion_radio/*.cpp>
lib_deps =
${lilygo_techo_lite_meck_core.lib_deps}
densaugeo/base64 @ ~1.4.0
+55
View File
@@ -0,0 +1,55 @@
#include <Arduino.h>
#include "target.h"
#include <helpers/ArduinoHelpers.h>
#include <helpers/sensors/MicroNMEALocationProvider.h>
TechoBoard board;
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI);
WRAPPER_CLASS radio_driver(radio, board);
VolatileRTCClock fallback_clock;
AutoDiscoverRTCClock rtc_clock(fallback_clock);
#ifdef ENV_INCLUDE_GPS
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock);
EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea);
#else
EnvironmentSensorManager sensors = EnvironmentSensorManager();
#endif
#ifdef DISPLAY_CLASS
DISPLAY_CLASS display;
MomentaryButton user_btn(PIN_USER_BTN, 1000, true);
#endif
bool radio_init() {
rtc_clock.begin(Wire);
return radio.std_init(&SPI);
}
uint32_t radio_get_rng_seed() {
return radio.random(0x7FFFFFFF);
}
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
radio.setFrequency(freq);
radio.setSpreadingFactor(sf);
radio.setBandwidth(bw);
radio.setCodingRate(cr);
}
void radio_set_tx_power(int8_t dbm) {
radio.setOutputPower(dbm);
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // create new random identity
}
void radio_reset_agc() {
radio.setRxBoostedGainMode(true);
}
+32
View File
@@ -0,0 +1,32 @@
#pragma once
#define RADIOLIB_STATIC_ONLY 1
#include <RadioLib.h>
#include <helpers/radiolib/RadioLibWrappers.h>
#include <TechoBoard.h>
#include <helpers/radiolib/CustomSX1262Wrapper.h>
#include <helpers/AutoDiscoverRTCClock.h>
#include <helpers/SensorManager.h>
#include <helpers/sensors/EnvironmentSensorManager.h>
#include <helpers/sensors/LocationProvider.h>
#ifdef DISPLAY_CLASS
#include <helpers/ui/GxEPDDisplay.h>
#include <helpers/ui/MomentaryButton.h>
#endif
extern TechoBoard board;
extern WRAPPER_CLASS radio_driver;
extern AutoDiscoverRTCClock rtc_clock;
extern EnvironmentSensorManager sensors;
#ifdef DISPLAY_CLASS
extern DISPLAY_CLASS display;
extern MomentaryButton user_btn;
#endif
bool radio_init();
uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(int8_t dbm);
mesh::LocalIdentity radio_new_identity();
void radio_reset_agc();
@@ -0,0 +1,39 @@
#include "variant.h"
#include "wiring_constants.h"
#include "wiring_digital.h"
const int MISO = PIN_SPI1_MISO;
const int MOSI = PIN_SPI1_MOSI;
const int SCK = PIN_SPI1_SCK;
const uint32_t g_ADigitalPinMap[] = {
0xff, 0xff, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,
14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26,
27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39,
40, 41, 42, 43, 44, 45, 46, 47
};
void initVariant() {
pinMode(PIN_PWR_EN, OUTPUT);
digitalWrite(PIN_PWR_EN, HIGH);
pinMode(PIN_BUTTON1, INPUT_PULLUP);
pinMode(PIN_BUTTON2, INPUT_PULLUP);
pinMode(LED_RED, OUTPUT);
pinMode(LED_GREEN, OUTPUT);
pinMode(LED_BLUE, OUTPUT);
digitalWrite(LED_BLUE, HIGH);
digitalWrite(LED_GREEN, HIGH);
digitalWrite(LED_RED, HIGH);
// pinMode(PIN_TXCO, OUTPUT);
// digitalWrite(PIN_TXCO, HIGH);
pinMode(DISP_POWER, OUTPUT);
digitalWrite(DISP_POWER, LOW);
// shutdown gps
pinMode(GPS_EN, OUTPUT);
digitalWrite(GPS_EN, LOW);
}
+159
View File
@@ -0,0 +1,159 @@
/*
* variant.h
* Copyright (C) 2023 Seeed K.K.
* MIT License
*/
#pragma once
#define _PINNUM(port, pin) ((port) * 32 + (pin))
#include "WVariant.h"
////////////////////////////////////////////////////////////////////////////////
// Low frequency clock source
#define USE_LFXO // 32.768 kHz crystal oscillator
#define VARIANT_MCK (64000000ul)
#define WIRE_INTERFACES_COUNT (1)
////////////////////////////////////////////////////////////////////////////////
// Power
#define PIN_PWR_EN _PINNUM(0, 30) // RT9080_EN
#define BATTERY_PIN _PINNUM(0, 2)
#define ADC_MULTIPLIER (2.0F)
#define ADC_RESOLUTION (14)
#define BATTERY_SENSE_RES (12)
#define AREF_VOLTAGE (3.0)
////////////////////////////////////////////////////////////////////////////////
// Number of pins
#define PINS_COUNT (48)
#define NUM_DIGITAL_PINS (48)
#define NUM_ANALOG_INPUTS (1)
#define NUM_ANALOG_OUTPUTS (0)
////////////////////////////////////////////////////////////////////////////////
// UART pin definition
#define PIN_SERIAL1_RX PIN_GPS_TX
#define PIN_SERIAL1_TX PIN_GPS_RX
////////////////////////////////////////////////////////////////////////////////
// I2C pin definition
#define PIN_WIRE_SDA _PINNUM(1, 4) // (SDA) - per LilyGo IIC_1_SDA
#define PIN_WIRE_SCL _PINNUM(1, 2) // (SCL) - per LilyGo IIC_1_SCL
////////////////////////////////////////////////////////////////////////////////
// SPI pin definition
#define SPI_INTERFACES_COUNT (2)
#define PIN_SPI_MISO _PINNUM(0, 17) // (MISO)
#define PIN_SPI_MOSI _PINNUM(0, 15) // (MOSI)
#define PIN_SPI_SCK _PINNUM(0, 13) // (SCK)
#define PIN_SPI_NSS (-1)
////////////////////////////////////////////////////////////////////////////////
// QSPI FLASH
#define PIN_QSPI_SCK _PINNUM(0, 4)
#define PIN_QSPI_CS _PINNUM(0, 12)
#define PIN_QSPI_IO0 _PINNUM(0, 6)
#define PIN_QSPI_IO1 _PINNUM(0, 8)
#define PIN_QSPI_IO2 _PINNUM(1, 9)
#define PIN_QSPI_IO3 _PINNUM(0, 26)
#define EXTERNAL_FLASH_DEVICES ZD25WQ32CEIGR
#define EXTERNAL_FLASH_USE_QSPI
////////////////////////////////////////////////////////////////////////////////
// Builtin LEDs
#define LED_RED _PINNUM(1, 14) // LED_3
#define LED_BLUE _PINNUM(1, 5) // LED_2
#define LED_GREEN _PINNUM(1, 7) // LED_1
//#define PIN_STATUS_LED LED_BLUE
#define LED_BUILTIN (-1)
#define LED_PIN LED_BUILTIN
#define LED_STATE_ON LOW
////////////////////////////////////////////////////////////////////////////////
// Builtin buttons
#define PIN_BUTTON1 _PINNUM(0, 24) // BOOT
#define BUTTON_PIN PIN_BUTTON1
#define PIN_USER_BTN BUTTON_PIN
#define PIN_BUTTON2 _PINNUM(0, 18)
#define BUTTON_PIN2 PIN_BUTTON2
#define EXTERNAL_FLASH_DEVICES MX25R1635F
#define EXTERNAL_FLASH_USE_QSPI
////////////////////////////////////////////////////////////////////////////////
// Lora
#define USE_SX1262
#define LORA_CS _PINNUM(0, 11)
#define SX126X_POWER_EN _PINNUM(0, 30)
#define SX126X_DIO1 _PINNUM(1, 8)
#define SX126X_BUSY _PINNUM(0, 14)
#define SX126X_RESET _PINNUM(0, 7)
#define SX126X_RF_VC1 _PINNUM(0, 27)
#define SX126X_RF_VC2 _PINNUM(0, 33)
#define P_LORA_DIO_1 SX126X_DIO1
#define P_LORA_NSS LORA_CS
#define P_LORA_RESET SX126X_RESET
#define P_LORA_BUSY SX126X_BUSY
#define P_LORA_SCLK PIN_SPI_SCK
#define P_LORA_MISO PIN_SPI_MISO
#define P_LORA_MOSI PIN_SPI_MOSI
////////////////////////////////////////////////////////////////////////////////
// SPI1
#define PIN_SPI1_MISO (-1) // Not used for Display
#define PIN_SPI1_MOSI _PINNUM(0, 20)
#define PIN_SPI1_SCK _PINNUM(0, 19)
// GxEPD2 needs that for a panel that is not even used !
extern const int MISO;
extern const int MOSI;
extern const int SCK;
////////////////////////////////////////////////////////////////////////////////
// Display
// #define DISP_MISO (-1) // Not used for Display
#define DISP_MOSI _PINNUM(0, 20)
#define DISP_SCLK _PINNUM(0, 19)
#define DISP_CS _PINNUM(0, 22)
#define DISP_DC _PINNUM(0, 21)
#define DISP_RST _PINNUM(0, 28)
#define DISP_BUSY _PINNUM(0, 3)
#define DISP_POWER _PINNUM(1, 12)
// #define DISP_BACKLIGHT (-1) // Display has no backlight
#define PIN_DISPLAY_CS DISP_CS
#define PIN_DISPLAY_DC DISP_DC
#define PIN_DISPLAY_RST DISP_RST
#define PIN_DISPLAY_BUSY DISP_BUSY
////////////////////////////////////////////////////////////////////////////////
// GPS — per LilyGo t_echo_lite_config.h
// PIN_GPS_TX/RX named from GPS module's perspective
#define PIN_GPS_TX _PINNUM(0, 29) // GPS UART TX → MCU RX
#define PIN_GPS_RX _PINNUM(1, 10) // GPS UART RX ← MCU TX
#define GPS_EN _PINNUM(1, 11) // GPS RT9080 power enable
#define PIN_GPS_STANDBY _PINNUM(1, 13) // GPS wake-up
#define PIN_GPS_PPS _PINNUM(1, 15) // GPS 1PPS