Add LVGL watch GUI with tileview navigation and serial screenshot

LVGL 9.5.0 integration replacing direct framebuffer rendering.
Watch face with time, date, status bar (mode/battery), and three
complications (LoRa/GPS/Steps). Tileview swipe navigation between
five screens: watch face, radio status, GPS, messages, settings.
Haptic feedback on screen transitions via DRV2605.

Radio status screen shows frequency, LoRa params, RSSI bar gauge,
channel utilization, BLE state, and packet counts. GPS screen shows
coordinates, fix quality, altitude/speed, and beacon status.

Serial screenshot tool (scripts/screenshot.py) captures display
contents over USB CDC by sending trigger bytes and receiving the
shadow framebuffer as raw RGB565.

Build changes:
- FlashSize=16M in FQBN (bootloader embeds flash size, defaults 4MB)
- PSRAM=enabled for LVGL draw buffers and screenshot shadow buffer
- Custom 8MB app partition (partition_twatch.csv) for 16MB flash
- flash-twatch_ultra-full make target for full bootloader+partition+app flash

Architecture notes in code:
- display_init() must run BEFORE xl9555_init() (display power gate
  defaults high at power-on, reordering causes black screen)
- LVGL draw buffers use separate swap buffer for RGB565 byte-order
  conversion to avoid corrupting LVGL's internal buffer state
- Touch input registered via function pointer to decouple Gui.h from
  touch library include order
This commit is contained in:
GlassOnTin 2026-03-28 11:41:45 +00:00
commit 2c0c9f3d5a
8 changed files with 3820 additions and 64 deletions

View file

@ -14,9 +14,8 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#if BOARD_MODEL == BOARD_TWATCH_ULT
// T-Watch Ultra uses CO5300 QSPI AMOLED — separate display path
// T-Watch Ultra — LVGL GUI on CO5300 QSPI AMOLED (410x502)
#include <Adafruit_GFX.h>
#include <Fonts/FreeSansBold24pt7b.h>
#include "Fonts/Org_01.h"
#include "XL9555.h"
#include "CO5300.h"
@ -27,16 +26,14 @@
bool recondition_display = false;
void ext_fb_enable() { }
void ext_fb_disable() { }
uint8_t fb[0]; // empty framebuffer stub
uint8_t fb[0];
bool display_blanked = false;
uint32_t last_unblank_event = 0;
uint32_t display_blanking_timeout = 0; // Disabled until button wake is implemented
uint32_t display_blanking_timeout = 0;
// Partial framebuffer for clock region (410 x 60 = ~49KB, fits in DMA memory)
#define CLOCK_FB_W CO5300_WIDTH
#define CLOCK_FB_H 60
static uint16_t *clock_fb = NULL;
// LVGL GUI (must come after variable declarations above)
#include "Gui.h"
void display_unblank() {
if (display_blanked) {
@ -44,6 +41,7 @@
xl9555_set(EXPANDS_DISP_EN, true);
co5300_set_brightness(128);
display_blanked = false;
if (gui_screen) lv_obj_invalidate(gui_screen);
}
last_unblank_event = millis();
}
@ -52,21 +50,16 @@
if (!co5300_init()) return false;
co5300_set_brightness(128);
// Enable blanking — touch input now available to unblank
display_blanking_enabled = true;
display_blanking_timeout = 10000; // 10 seconds
// Allocate partial framebuffer for clock region (PSRAM preferred for large buffers)
clock_fb = (uint16_t *)heap_caps_malloc(CLOCK_FB_W * CLOCK_FB_H * 2, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (!clock_fb) clock_fb = (uint16_t *)malloc(CLOCK_FB_W * CLOCK_FB_H * 2);
display_blanking_timeout = 10000;
if (!gui_init()) return false;
return true;
}
void update_display(bool force = false) {
if (!co5300_ready) return;
// Handle display blanking
if (display_blanking_enabled && !display_blanked) {
if (millis() - last_unblank_event > display_blanking_timeout) {
co5300_set_brightness(0);
@ -77,45 +70,9 @@
}
}
if (display_blanked && !force) return;
if (!clock_fb) return;
display_updating = true;
// Render clock in partial framebuffer
memset(clock_fb, 0, CLOCK_FB_W * CLOCK_FB_H * 2);
char time_str[6];
snprintf(time_str, sizeof(time_str), "%02d:%02d", rtc_hour, rtc_minute);
// Draw time centered, large font
const GFXfont *font = &FreeSansBold24pt7b;
uint16_t tw = co5300_draw_string(clock_fb, CLOCK_FB_W, CLOCK_FB_H,
0, 45, time_str, CO5300_WHITE, font);
// Re-render centered
if (tw > 0 && tw < CLOCK_FB_W) {
memset(clock_fb, 0, CLOCK_FB_W * CLOCK_FB_H * 2);
co5300_draw_string(clock_fb, CLOCK_FB_W, CLOCK_FB_H,
(CLOCK_FB_W - tw) / 2, 45, time_str, CO5300_WHITE, font);
}
// Push clock region to display (upper area)
co5300_push_pixels(0, 80, CLOCK_FB_W, CLOCK_FB_H, clock_fb);
// Render status line below clock
memset(clock_fb, 0, CLOCK_FB_W * CLOCK_FB_H * 2);
char status[64];
snprintf(status, sizeof(status), "%s %d%% %dsats %lus",
radio_online ? "RADIO" : "idle",
(int)battery_percent,
gps_sats,
millis() / 1000);
co5300_draw_string(clock_fb, CLOCK_FB_W, CLOCK_FB_H,
10, 15, status, CO5300_GREY, &Org_01);
co5300_push_pixels(0, 150, CLOCK_FB_W, CLOCK_FB_H, clock_fb);
gui_update();
display_updating = false;
}

1904
Fonts/montserrat_bold_28.c Normal file

File diff suppressed because it is too large Load diff

984
Fonts/montserrat_bold_96.c Normal file
View file

@ -0,0 +1,984 @@
/*******************************************************************************
* Size: 96 px
* Bpp: 4
* Opts: --bpp 4 --size 96 --font /home/ian/Arduino/libraries/lvgl/tests/src/test_files/fonts/Montserrat-Bold.ttf -r 0x30-0x3A,0x20 --format lvgl -o /home/ian/Code/RNode_Firmware/Fonts/montserrat_bold_96.c --lv-include lvgl.h --lv-font-name montserrat_bold_96
******************************************************************************/
#ifdef LV_LVGL_H_INCLUDE_SIMPLE
#include "lvgl.h"
#else
#include "lvgl.h"
#endif
#ifndef MONTSERRAT_BOLD_96
#define MONTSERRAT_BOLD_96 1
#endif
#if MONTSERRAT_BOLD_96
/*-----------------
* BITMAPS
*----------------*/
/*Store the image of the glyphs*/
static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
/* U+0020 " " */
/* U+0030 "0" */
0x0, 0xff, 0xe7, 0x11, 0xa2, 0x46, 0x40, 0x1f,
0xfe, 0x33, 0x7a, 0xdf, 0xdc, 0xbb, 0xb3, 0x7f,
0xad, 0xcc, 0x3, 0xff, 0xb6, 0x51, 0xb9, 0xa,
0x40, 0x1f, 0xc2, 0x91, 0x9d, 0x24, 0x1, 0xff,
0xd6, 0x2a, 0xd7, 0x20, 0xf, 0xfe, 0x28, 0xb6,
0xd9, 0x80, 0x7f, 0xf4, 0x86, 0x75, 0x40, 0x3f,
0xf9, 0x69, 0x94, 0x20, 0x1f, 0xfc, 0xf4, 0xf6,
0x0, 0xff, 0xe7, 0xaf, 0x28, 0x7, 0xff, 0x35,
0xec, 0x40, 0x3f, 0xfa, 0x55, 0x20, 0x1f, 0xfc,
0xba, 0x80, 0xf, 0xfe, 0xab, 0x60, 0x7, 0xff,
0x26, 0x94, 0x3, 0xff, 0xae, 0x78, 0x1, 0xff,
0xc7, 0xa5, 0x0, 0xff, 0xec, 0x9d, 0x80, 0x7f,
0xf1, 0x61, 0x40, 0x3f, 0xfb, 0x69, 0x20, 0x1f,
0xfc, 0x34, 0x70, 0xf, 0xfe, 0xeb, 0x28, 0x7,
0xff, 0x4, 0x68, 0x3, 0xff, 0xbf, 0x44, 0x1,
0xff, 0xc0, 0xb1, 0x0, 0xff, 0xf0, 0x70, 0x7,
0xfc, 0x6a, 0x1, 0xff, 0xc6, 0x49, 0xce, 0xfe,
0xd9, 0x50, 0xf, 0xfe, 0x31, 0xa0, 0x7, 0xfb,
0xc0, 0x3f, 0xf8, 0xa7, 0xb6, 0xc6, 0x20, 0x24,
0xd5, 0xca, 0x1, 0xff, 0xc5, 0x90, 0xf, 0xe5,
0x30, 0xf, 0xfe, 0x24, 0x61, 0x0, 0x7f, 0xd,
0x50, 0x3, 0xff, 0x88, 0x4c, 0x1, 0xfa, 0x40,
0x3f, 0xf8, 0x92, 0xe0, 0x1f, 0xfc, 0x15, 0xb0,
0xf, 0xfe, 0x25, 0x80, 0x7c, 0x46, 0x1, 0xff,
0xc3, 0x56, 0x0, 0xff, 0xe1, 0xa4, 0x0, 0x7f,
0xf0, 0xc9, 0x0, 0x3d, 0x60, 0x1f, 0xfc, 0x31,
0xa0, 0xf, 0xfe, 0x2b, 0x98, 0x7, 0xff, 0xf,
0x80, 0x3c, 0xe0, 0x1f, 0xfc, 0x38, 0x0, 0xff,
0xe3, 0xf8, 0x7, 0xff, 0xd, 0x44, 0x3, 0x20,
0x80, 0x7f, 0xf0, 0x85, 0x80, 0x3f, 0xf8, 0xe6,
0x80, 0x1f, 0xfc, 0x34, 0x0, 0xd8, 0x1, 0xff,
0xc3, 0x90, 0xf, 0xfe, 0x4f, 0x0, 0x7f, 0xf0,
0xfc, 0x3, 0x20, 0x7, 0xff, 0xd, 0x80, 0x3f,
0xf9, 0x2a, 0x20, 0x1f, 0xfc, 0x24, 0x0, 0x84,
0xc0, 0x3f, 0xf8, 0x44, 0x20, 0x1f, 0xfc, 0xa4,
0x0, 0xff, 0xe1, 0x9, 0x0, 0x14, 0x3, 0xff,
0x86, 0xa0, 0x1f, 0xfc, 0xbf, 0x0, 0xff, 0xe1,
0xa0, 0x0, 0xc0, 0x3f, 0xf8, 0x7a, 0x1, 0xff,
0xcb, 0x70, 0xf, 0xfe, 0x1f, 0x0, 0x38, 0x3,
0xff, 0x86, 0xe0, 0x1f, 0xfc, 0xb2, 0x0, 0xff,
0xe1, 0x90, 0x0, 0x80, 0x3f, 0xf8, 0x64, 0x1,
0xff, 0xcc, 0x30, 0xf, 0xfe, 0x13, 0x80, 0x14,
0x3, 0xff, 0x84, 0x40, 0x1f, 0xfc, 0xd6, 0x0,
0xff, 0xe1, 0x10, 0x0, 0x40, 0x3f, 0xf8, 0x4c,
0x1, 0xff, 0xcd, 0x30, 0xf, 0xfe, 0x18, 0x88,
0x3, 0xff, 0x86, 0x20, 0x1f, 0xfc, 0xde, 0x0,
0xff, 0xe1, 0x98, 0x7, 0xff, 0x10, 0xc0, 0x3f,
0xf9, 0xa2, 0x1, 0xff, 0xc3, 0x13, 0x0, 0xff,
0xe1, 0x88, 0x7, 0xff, 0x34, 0xc0, 0x3f, 0xf8,
0x82, 0x1, 0xff, 0xed, 0x70, 0xf, 0xfe, 0x27,
0x80, 0x7f, 0xf3, 0x44, 0x3, 0xff, 0x86, 0x20,
0x1f, 0xfc, 0x4f, 0x0, 0xff, 0xe6, 0x88, 0x7,
0xff, 0x10, 0x40, 0x3f, 0xfd, 0xa2, 0x1, 0xff,
0xde, 0x30, 0xf, 0xfe, 0x1b, 0x98, 0x7, 0xff,
0xc, 0x40, 0x3f, 0xfb, 0xa2, 0x20, 0xf, 0xfe,
0x19, 0x80, 0x7f, 0xf3, 0x44, 0x3, 0xff, 0xa8,
0xc0, 0x1f, 0xfc, 0xde, 0x0, 0xff, 0xe1, 0x98,
0x10, 0x7, 0xff, 0x8, 0x40, 0x3f, 0xf9, 0xa4,
0x1, 0xff, 0xc2, 0x11, 0x3, 0x0, 0x7f, 0xf0,
0x88, 0x3, 0xff, 0x9a, 0xc0, 0x1f, 0xfc, 0x22,
0x0, 0x18, 0x7, 0xff, 0xc, 0xc0, 0x3f, 0xf9,
0x84, 0x1, 0xff, 0xc2, 0x60, 0x7, 0x0, 0x7f,
0xf0, 0xd8, 0x3, 0xff, 0x96, 0x60, 0x1f, 0xfc,
0x32, 0x0, 0x28, 0x7, 0xff, 0xf, 0x0, 0x3f,
0xf9, 0x68, 0x1, 0xff, 0xc3, 0xd0, 0x1, 0x80,
0x7f, 0xf0, 0xdc, 0x3, 0xff, 0x97, 0x80, 0x1f,
0xfc, 0x34, 0x0, 0x90, 0x3, 0xff, 0x84, 0x26,
0x1, 0xff, 0xc9, 0x14, 0x0, 0xff, 0xe1, 0x18,
0x80, 0x58, 0x1, 0xff, 0xc3, 0xa0, 0xf, 0xfe,
0x4b, 0x0, 0x7f, 0xf0, 0xd0, 0x3, 0x20, 0x7,
0xff, 0xd, 0x84, 0x3, 0xff, 0x91, 0x40, 0x1f,
0xfc, 0x3c, 0x0, 0xc6, 0x40, 0x1f, 0xfc, 0x39,
0x0, 0xff, 0xe3, 0xb1, 0x0, 0x7f, 0xf0, 0x85,
0x0, 0x3a, 0x80, 0x3f, 0xf8, 0x6e, 0x40, 0x1f,
0xfc, 0x68, 0x0, 0xff, 0xe1, 0xb0, 0x7, 0x98,
0x3, 0xff, 0x89, 0xe0, 0x1f, 0xfc, 0x5a, 0x10,
0xf, 0xfe, 0x1d, 0x0, 0x78, 0x54, 0x3, 0xff,
0x86, 0x56, 0x1, 0xff, 0xc3, 0x85, 0x0, 0xff,
0xe1, 0xa1, 0x0, 0x7d, 0x20, 0x1f, 0xfc, 0x44,
0xc1, 0x0, 0xff, 0xe0, 0x53, 0x80, 0x7f, 0xf1,
0x3c, 0x3, 0xf1, 0xa8, 0x7, 0xff, 0x10, 0xf9,
0xc4, 0x3, 0xf3, 0x72, 0x80, 0x7f, 0xf1, 0x11,
0x0, 0x1f, 0xd6, 0x1, 0xff, 0xc6, 0x8e, 0xb7,
0x55, 0x9e, 0xba, 0x44, 0x3, 0xff, 0x8b, 0x20,
0x1f, 0xe1, 0x70, 0xf, 0xfe, 0x3a, 0x45, 0x55,
0xa, 0x20, 0x1f, 0xfc, 0x66, 0x20, 0xf, 0xfa,
0xc, 0x3, 0xff, 0xbe, 0x52, 0x1, 0xff, 0xc1,
0xd1, 0x0, 0xff, 0xef, 0x78, 0x7, 0xff, 0x8,
0x68, 0x3, 0xff, 0xbb, 0x24, 0x1, 0xff, 0xc3,
0x49, 0x0, 0xff, 0xed, 0xc3, 0x0, 0x7f, 0xf1,
0x5a, 0x40, 0x3f, 0xfb, 0x2e, 0xe0, 0xf, 0xfe,
0x3b, 0x48, 0x7, 0xff, 0x5d, 0xe0, 0x3, 0xff,
0x92, 0xd8, 0x20, 0x1f, 0xfd, 0x31, 0xb8, 0x0,
0xff, 0xe5, 0x9f, 0xa8, 0x7, 0xff, 0x44, 0xfd,
0x0, 0x3f, 0xf9, 0xa3, 0x54, 0x20, 0xf, 0xfe,
0x68, 0xce, 0x8, 0x7, 0xff, 0x41, 0x76, 0x48,
0x3, 0xff, 0x92, 0x31, 0xcc, 0x1, 0xff, 0xd4,
0x6d, 0xc6, 0x10, 0xf, 0xfe, 0x18, 0xad, 0xf3,
0x80, 0x7f, 0xf6, 0xe, 0x7b, 0x65, 0x90, 0x84,
0x2, 0x12, 0x46, 0x9c, 0xea, 0x40, 0xf, 0xfe,
0x38,
/* U+0031 "1" */
0x39, 0x9f, 0xfe, 0x83, 0x37, 0xff, 0x4b, 0x0,
0x3f, 0xff, 0xe0, 0x1f, 0xff, 0xf0, 0xf, 0xff,
0xf8, 0x7, 0xff, 0xfc, 0x3, 0xff, 0xa4, 0xbf,
0xff, 0xfc, 0x11, 0x0, 0xff, 0xff, 0x80, 0x7f,
0xff, 0xc0, 0x3f, 0xff, 0xe0, 0x1f, 0xff, 0xf0,
0xf, 0xff, 0xf8, 0x7, 0xff, 0xfc, 0x3, 0xff,
0xfe, 0x1, 0xff, 0xff, 0x0, 0xff, 0xff, 0x80,
0x7f, 0xff, 0xc0, 0x3f, 0xff, 0xe0, 0x1f, 0xff,
0xf0, 0xf, 0xff, 0xf8, 0x7, 0xff, 0xfc, 0x3,
0xff, 0xfe, 0x1, 0xff, 0xff, 0x0, 0xff, 0xff,
0x80, 0x7f, 0xff, 0xc0, 0x3f, 0xff, 0xe0, 0x1f,
0xff, 0xf0, 0xf, 0xff, 0xf8, 0x7, 0xff, 0xfa,
0x5d, 0xff, 0xf8, 0x5e,
/* U+0032 "2" */
0x0, 0xff, 0xe6, 0x9, 0x16, 0x10, 0xf, 0xfe,
0xe9, 0x34, 0x5e, 0xff, 0xbb, 0x77, 0x77, 0xf6,
0x4b, 0x10, 0x7, 0xff, 0x51, 0x27, 0x75, 0x2e,
0x84, 0x1, 0xfc, 0x26, 0xd3, 0xbd, 0x26, 0x1,
0xff, 0xce, 0x17, 0xeb, 0x62, 0x0, 0xff, 0xe3,
0x8b, 0x66, 0xb0, 0x7, 0xff, 0x28, 0x67, 0xa0,
0x40, 0x3f, 0xf9, 0xa5, 0x3a, 0xa0, 0x1f, 0xfc,
0x77, 0xe6, 0x0, 0xff, 0xe9, 0x15, 0x50, 0x40,
0x3f, 0xf8, 0x65, 0xb0, 0x1, 0xff, 0xd6, 0x5f,
0x30, 0xf, 0xfe, 0xb, 0x69, 0x0, 0x7f, 0xf6,
0x7, 0x10, 0x3, 0xfe, 0x99, 0x0, 0x7f, 0xf7,
0x2c, 0xc0, 0x3f, 0xa5, 0x80, 0x3f, 0xfb, 0xda,
0x20, 0x1f, 0x4b, 0x0, 0x7f, 0xf7, 0xc7, 0x40,
0x3c, 0xec, 0x1, 0xff, 0xe1, 0x35, 0x0, 0xc7,
0x0, 0x1f, 0xfe, 0x3b, 0x0, 0xd4, 0x1, 0xff,
0xe4, 0x15, 0x0, 0xa6, 0x84, 0x3, 0xff, 0x88,
0x2b, 0x39, 0xdf, 0xee, 0xdb, 0x84, 0x0, 0xff,
0xe3, 0xf0, 0x6, 0x5f, 0x50, 0xf, 0xfe, 0xa,
0xdf, 0x53, 0x18, 0x80, 0x4, 0x91, 0xef, 0x54,
0x3, 0xff, 0x8a, 0x80, 0x1c, 0x35, 0x62, 0x1,
0xfc, 0x79, 0x48, 0x1, 0xff, 0xc0, 0x2a, 0xa0,
0x7, 0xff, 0x15, 0x0, 0x3c, 0x9c, 0xc0, 0x1f,
0x3e, 0x18, 0x7, 0xff, 0x11, 0x64, 0x3, 0xff,
0x88, 0x60, 0x1f, 0xa6, 0xc8, 0x3, 0x44, 0x0,
0x3f, 0xf8, 0xec, 0x60, 0x1f, 0xfc, 0x3e, 0x0,
0xfe, 0x4d, 0x70, 0x4, 0x38, 0x7, 0xff, 0x26,
0x80, 0x3f, 0xf8, 0x66, 0x1, 0xff, 0x46, 0x3b,
0x80, 0x3f, 0xf9, 0x4e, 0x1, 0xff, 0xc3, 0x10,
0xf, 0xfe, 0x1, 0xc0, 0x7, 0xff, 0x2c, 0x40,
0x3f, 0xfd, 0x42, 0x1, 0xff, 0xc2, 0x10, 0xf,
0xfe, 0xd0, 0x80, 0x7f, 0xf0, 0x8c, 0x3, 0xff,
0xb2, 0x40, 0x1f, 0xfc, 0x3e, 0x0, 0xff, 0xec,
0xb8, 0x7, 0xff, 0xc, 0xc0, 0x3f, 0xfb, 0x3e,
0x1, 0xff, 0xc3, 0x50, 0xf, 0xfe, 0xc0, 0xa8,
0x7, 0xff, 0x8, 0x44, 0x1, 0xff, 0xd8, 0x80,
0xf, 0xfe, 0x1b, 0x80, 0x7f, 0xf6, 0x9, 0x80,
0x3f, 0xf8, 0x7a, 0x1, 0xff, 0xd8, 0xe0, 0xf,
0xfe, 0x19, 0x20, 0x7, 0xff, 0x5e, 0xc, 0x3,
0xff, 0x87, 0x0, 0x1f, 0xfd, 0x76, 0x70, 0xf,
0xfe, 0x19, 0xa8, 0x7, 0xff, 0x59, 0x64, 0x3,
0xff, 0x89, 0xe0, 0x1f, 0xfd, 0x66, 0xa0, 0xf,
0xfe, 0x23, 0x98, 0x7, 0xff, 0x55, 0xa4, 0x3,
0xff, 0x88, 0x70, 0x1, 0xff, 0xd5, 0x79, 0x0,
0xff, 0xe2, 0xe, 0x80, 0x7f, 0xf5, 0x62, 0x0,
0x1f, 0xfc, 0x5d, 0x10, 0xf, 0xfe, 0xa4, 0x38,
0x7, 0xff, 0x16, 0x8c, 0x3, 0xff, 0xa9, 0x2e,
0x1, 0xff, 0xc5, 0x95, 0x0, 0xff, 0xea, 0x53,
0x0, 0x7f, 0xf1, 0x61, 0x80, 0x3f, 0xfa, 0x94,
0xa0, 0x1f, 0xfc, 0x59, 0x70, 0xf, 0xfe, 0xa5,
0xa8, 0x7, 0xff, 0x16, 0x58, 0x3, 0xff, 0xa9,
0x68, 0x1, 0xff, 0xc5, 0x96, 0x0, 0xff, 0xea,
0x62, 0x0, 0x7f, 0xf1, 0x69, 0x80, 0x3f, 0xfa,
0x63, 0x86, 0x1, 0xff, 0xc5, 0xb5, 0x0, 0xff,
0xe9, 0x8e, 0x18, 0x7, 0xff, 0x16, 0xd0, 0x3,
0xff, 0xa6, 0x38, 0x40, 0x1f, 0xfc, 0x5c, 0x40,
0xf, 0xfe, 0x99, 0x61, 0x0, 0x7f, 0xf1, 0x7,
0xc, 0x3, 0xff, 0xa6, 0x58, 0x40, 0x1f, 0xfc,
0x41, 0xc3, 0x0, 0xff, 0xe9, 0x96, 0x8, 0x7,
0xff, 0x10, 0x70, 0x80, 0x3f, 0xfa, 0x67, 0x82,
0x1, 0xff, 0xc4, 0x2f, 0x20, 0xf, 0xfe, 0x99,
0xe0, 0x80, 0x7f, 0xf1, 0xb, 0x4, 0x3, 0xff,
0xa6, 0x98, 0x1, 0xff, 0xc5, 0x3c, 0x10, 0xf,
0xfe, 0x9a, 0x58, 0x7, 0xff, 0x14, 0xf4, 0x40,
0x3f, 0xfa, 0x6b, 0x60, 0x1f, 0xfc, 0x54, 0xc1,
0x0, 0xff, 0xe9, 0xb5, 0x0, 0x7f, 0xf1, 0x52,
0xc0, 0x3f, 0xfa, 0x8d, 0x20, 0x1f, 0xfc, 0x66,
0xdd, 0xff, 0xe7, 0xe0, 0x6, 0x79, 0x0, 0xff,
0xe3, 0x91, 0x7f, 0xf4, 0xc, 0x2, 0x78, 0x0,
0xff, 0xf4, 0x50, 0x7, 0xff, 0xfc, 0x3, 0xff,
0xfe, 0x1, 0xff, 0xff, 0x0, 0xff, 0xff, 0x80,
0x7f, 0xff, 0xc0, 0x3f, 0xff, 0xe0, 0x1f, 0xff,
0xf0, 0xf, 0xf8,
/* U+0033 "3" */
0x0, 0xd7, 0xff, 0xff, 0xe2, 0x40, 0xf, 0xff,
0xf8, 0x7, 0xff, 0xfc, 0x3, 0xff, 0xfe, 0x1,
0xff, 0xff, 0x0, 0xff, 0xff, 0x80, 0x7f, 0xff,
0xc0, 0x3f, 0xff, 0xe0, 0x1f, 0xfc, 0xb1, 0x50,
0xf, 0xff, 0x4e, 0x88, 0x7, 0xd8, 0xcd, 0xff,
0xcf, 0x30, 0xf, 0xfe, 0x25, 0x98, 0x7, 0xe7,
0x99, 0xff, 0xe7, 0x40, 0x80, 0x7f, 0xf0, 0xe5,
0x0, 0x3f, 0xfb, 0x1a, 0x40, 0x1f, 0xfc, 0x27,
0x60, 0xf, 0xfe, 0xc5, 0x18, 0x7, 0xff, 0x9,
0x60, 0x3, 0xff, 0xb1, 0x2a, 0x1, 0xff, 0xc2,
0x3a, 0x0, 0xff, 0xec, 0x3b, 0x0, 0x7f, 0xf0,
0x8b, 0x40, 0x3f, 0xfb, 0xb, 0x0, 0x1f, 0xfc,
0x21, 0xf1, 0x0, 0xff, 0xeb, 0x9d, 0x0, 0x7f,
0xf0, 0xf4, 0x80, 0x3f, 0xfa, 0xe5, 0xa0, 0x1f,
0xfc, 0x3a, 0x30, 0xf, 0xfe, 0xb8, 0xf8, 0x80,
0x7f, 0xf0, 0xa1, 0x40, 0x3f, 0xfb, 0x1a, 0x40,
0x1f, 0xfc, 0x26, 0x70, 0xf, 0xfe, 0xc5, 0x18,
0x7, 0xff, 0x9, 0x24, 0x3, 0xff, 0xb1, 0x2a,
0x1, 0xff, 0xc2, 0x3b, 0x0, 0xff, 0xec, 0x3b,
0x0, 0x7f, 0xf0, 0x8b, 0x40, 0x3f, 0xfb, 0xb,
0x0, 0x1f, 0xfc, 0x21, 0xf1, 0x0, 0xff, 0xeb,
0x9d, 0x0, 0x7f, 0xf0, 0xd0, 0x40, 0x3f, 0xfb,
0x1a, 0x1, 0xff, 0xc4, 0x5c, 0xfc, 0x72, 0x0,
0xff, 0xea, 0x8, 0x7, 0xff, 0x1c, 0xe3, 0x71,
0x0, 0x3f, 0xfd, 0x27, 0x78, 0x60, 0x1f, 0xfe,
0x93, 0xc9, 0x0, 0xff, 0xf5, 0x36, 0x8, 0x7,
0xff, 0xa4, 0xf0, 0x40, 0x3f, 0xfd, 0x25, 0x80,
0x1f, 0xfe, 0xa2, 0xa0, 0xf, 0xff, 0x52, 0xa8,
0x3, 0xff, 0xd5, 0x62, 0x1, 0xff, 0xc9, 0x44,
0xf8, 0xcc, 0x42, 0x1, 0xff, 0xc9, 0x19, 0x0,
0xff, 0xe4, 0xdd, 0xfd, 0x98, 0xde, 0xe6, 0x41,
0x0, 0x7f, 0xf1, 0x98, 0x3, 0xff, 0xa8, 0x26,
0xfb, 0x64, 0x1, 0xff, 0xc4, 0x15, 0x0, 0xff,
0xeb, 0xa6, 0xa8, 0x7, 0xff, 0x13, 0xc0, 0x3f,
0xfb, 0x34, 0x80, 0x1f, 0xfc, 0x34, 0x0, 0xff,
0xed, 0x48, 0x7, 0xff, 0xc, 0x44, 0x1, 0xff,
0xd9, 0x24, 0x0, 0xff, 0xe1, 0x90, 0x7, 0xff,
0x6b, 0x40, 0x3f, 0xf8, 0x6e, 0x1, 0xff, 0xda,
0x30, 0xf, 0xfe, 0x18, 0x80, 0x7f, 0xff, 0xc0,
0x3f, 0xf8, 0xc6, 0x1, 0xff, 0xc3, 0x10, 0xf,
0xfe, 0xd6, 0x0, 0x7f, 0xf0, 0xdc, 0x3, 0xcf,
0xc8, 0x1, 0xff, 0xd0, 0x15, 0x0, 0xff, 0xe1,
0x90, 0x7, 0xa4, 0x6e, 0x88, 0x3, 0xff, 0x9d,
0x60, 0x1f, 0xfc, 0x32, 0x10, 0xe, 0x71, 0x0,
0x2e, 0xc9, 0x0, 0x7f, 0xf2, 0xdd, 0x40, 0x3f,
0xf8, 0x6e, 0x1, 0xe9, 0x0, 0xe6, 0xdb, 0x50,
0xf, 0xfe, 0x38, 0xd4, 0x0, 0x7f, 0xf1, 0x3c,
0x3, 0x9c, 0x40, 0x3e, 0x4a, 0xfa, 0x61, 0x0,
0xff, 0xe0, 0x94, 0x72, 0x80, 0x7f, 0xf1, 0x5,
0x40, 0x3a, 0x40, 0x3f, 0xe5, 0x9e, 0xe5, 0xc3,
0x2a, 0x19, 0x91, 0x5e, 0x77, 0x4e, 0x1, 0xff,
0xc6, 0x70, 0xe, 0x71, 0x0, 0xff, 0xe1, 0xa,
0x3c, 0xd5, 0xe6, 0x2e, 0xa1, 0x88, 0x3, 0xff,
0x91, 0x0, 0x1d, 0x20, 0x1f, 0xfe, 0x37, 0x10,
0xc, 0xe2, 0x1, 0xff, 0xe2, 0x28, 0x0, 0xe9,
0x0, 0xff, 0xf1, 0xf0, 0x7, 0x38, 0x80, 0x7f,
0xf8, 0xa4, 0xc0, 0x3a, 0x40, 0x3f, 0xfc, 0x52,
0xc0, 0x1e, 0xc3, 0x0, 0xff, 0xf0, 0x4b, 0x0,
0x7c, 0x59, 0x22, 0x1, 0xff, 0xdc, 0x1c, 0x60,
0xf, 0xf3, 0x74, 0x88, 0x7, 0xff, 0x65, 0xbc,
0xc0, 0x3f, 0xf8, 0x2d, 0xd4, 0x80, 0x1f, 0xfd,
0x53, 0xc9, 0x10, 0xf, 0xfe, 0x22, 0xde, 0xc1,
0x80, 0x7f, 0xf4, 0x17, 0x30, 0x60, 0x1f, 0xfc,
0x82, 0x7c, 0xea, 0x63, 0x0, 0xff, 0xe4, 0xa,
0xcf, 0x51, 0x80, 0x7f, 0xf3, 0x45, 0x67, 0x3f,
0x69, 0xd5, 0x8, 0x44, 0x1, 0x8, 0x88, 0xda,
0x2b, 0x7a, 0x98, 0x40, 0x3f, 0xf8, 0x60,
/* U+0034 "4" */
0x0, 0xff, 0xea, 0x24, 0x47, 0xff, 0xd, 0x80,
0x3f, 0xfc, 0x45, 0x6e, 0xff, 0xfc, 0x32, 0x0,
0xff, 0xf1, 0x70, 0x7, 0xff, 0xd, 0xd0, 0x3,
0xff, 0xc3, 0x6, 0x1, 0xff, 0xc2, 0x38, 0x0,
0xff, 0xf0, 0xa3, 0x80, 0x7f, 0xf0, 0xf8, 0x3,
0xff, 0xc2, 0x34, 0x1, 0xff, 0xc3, 0xa2, 0x0,
0xff, 0xf0, 0xe8, 0x80, 0x7f, 0xf0, 0x95, 0x40,
0x1f, 0xfe, 0x18, 0x30, 0xf, 0xfe, 0x11, 0x50,
0x7, 0xff, 0x85, 0x1c, 0x3, 0xff, 0x87, 0xc0,
0x1f, 0xfe, 0x11, 0xa0, 0xf, 0xfe, 0x1c, 0x18,
0x7, 0xff, 0x86, 0x84, 0x3, 0xff, 0x84, 0x8e,
0x1, 0xff, 0xe1, 0x74, 0x0, 0xff, 0xe1, 0xd,
0x0, 0x7f, 0xf8, 0x4e, 0x0, 0x3f, 0xf8, 0x74,
0x20, 0x1f, 0xfe, 0x1, 0xd0, 0xf, 0xfe, 0x1b,
0x20, 0x7, 0xff, 0x86, 0xc4, 0x3, 0xff, 0x84,
0x52, 0x1, 0xff, 0xe1, 0x65, 0x0, 0xff, 0xe1,
0xf8, 0x7, 0xff, 0x84, 0xa4, 0x3, 0xff, 0x87,
0x24, 0x1, 0xff, 0xe1, 0xf0, 0xf, 0xfe, 0x1a,
0x30, 0x7, 0xff, 0x86, 0x88, 0x3, 0xff, 0x84,
0x34, 0x1, 0xff, 0xe1, 0x55, 0x0, 0x7f, 0xf0,
0xe8, 0x40, 0x3f, 0xfc, 0x5, 0x40, 0x1f, 0xfc,
0x37, 0x40, 0xf, 0xff, 0xf, 0x80, 0x7f, 0xf0,
0xce, 0x0, 0x3f, 0xfc, 0x32, 0x40, 0x1f, 0xfc,
0x3e, 0x0, 0xff, 0xf0, 0xab, 0x0, 0x7f, 0xf0,
0xe8, 0x80, 0x3f, 0xfc, 0x5, 0x60, 0x1f, 0xfc,
0x35, 0x50, 0x7, 0xff, 0x87, 0x84, 0x3, 0xff,
0x84, 0x54, 0x1, 0xff, 0xe1, 0x83, 0x0, 0xff,
0xe1, 0xf0, 0x7, 0xff, 0x85, 0x1c, 0x3, 0xff,
0x87, 0x6, 0x1, 0xff, 0xe0, 0x1a, 0x0, 0xff,
0xe1, 0xa3, 0x80, 0x7c, 0x51, 0x1f, 0xfc, 0x22,
0x0, 0xff, 0xe5, 0x50, 0x80, 0x7f, 0xf0, 0x86,
0x80, 0x3f, 0x3b, 0xff, 0xf1, 0x0, 0x3f, 0xf9,
0x2e, 0x80, 0x1f, 0xfc, 0x3a, 0x10, 0xf, 0xff,
0x1, 0xc0, 0x7, 0xff, 0xd, 0x90, 0x3, 0xff,
0xc0, 0x3a, 0x1, 0xff, 0xc3, 0x29, 0x0, 0xff,
0xf0, 0xd0, 0x80, 0x7f, 0xf0, 0xfc, 0x3, 0xff,
0xc2, 0xe8, 0x1, 0xff, 0xc3, 0x92, 0x0, 0xff,
0xf0, 0x1c, 0x0, 0x7f, 0xf0, 0xd1, 0x80, 0x3f,
0xfc, 0x3c, 0x1, 0xff, 0xc3, 0x1a, 0x0, 0xff,
0xf0, 0xd1, 0x0, 0x7f, 0xf0, 0xe8, 0x40, 0x3f,
0xfc, 0xc, 0xa0, 0x1f, 0xfc, 0x37, 0x40, 0xf,
0xff, 0x1, 0x48, 0x7, 0xff, 0xc, 0xe0, 0x3,
0xff, 0xc3, 0xe0, 0x1f, 0xfc, 0x4e, 0x0, 0xff,
0xf0, 0xc9, 0x0, 0x7f, 0xf1, 0x3f, 0x77, 0xff,
0x89, 0x40, 0x1f, 0xfc, 0x2a, 0xdd, 0xff, 0x18,
0x2b, 0x0, 0x7f, 0xf1, 0x48, 0xbf, 0xf8, 0xa0,
0x1f, 0xfc, 0x42, 0x2f, 0xf3, 0x85, 0x0, 0x7f,
0xff, 0xc0, 0x3f, 0xff, 0xe0, 0x1f, 0xff, 0xf0,
0xf, 0xff, 0xf8, 0x7, 0xff, 0xfc, 0x3, 0xff,
0xfe, 0x1, 0xff, 0xff, 0x0, 0xff, 0xff, 0x80,
0x7f, 0xff, 0xc0, 0x3f, 0xfa, 0x1f, 0xff, 0xfd,
0x82, 0x0, 0xff, 0xe1, 0x57, 0xff, 0xf9, 0x0,
0x3f, 0xff, 0xe0, 0x1f, 0xff, 0xf0, 0xf, 0xff,
0xf8, 0x7, 0xff, 0xfc, 0x3, 0xff, 0xfe, 0x1,
0xff, 0xff, 0x0, 0xff, 0xff, 0x80, 0x7f, 0xff,
0xc0, 0x3f, 0xff, 0xe0, 0x1f, 0xff, 0xf0, 0xf,
0xff, 0xf8, 0x7, 0xff, 0x82, 0xdd, 0xff, 0xf8,
0x60, 0x1f, 0xf0,
/* U+0035 "5" */
0x0, 0xfe, 0xaf, 0xff, 0xfe, 0xe3, 0x0, 0x7f,
0xf0, 0x44, 0x3, 0xff, 0xd4, 0xc0, 0x1f, 0xfe,
0xa3, 0x0, 0xff, 0xf5, 0x8, 0x7, 0xff, 0xa4,
0x40, 0x3f, 0xfd, 0x46, 0x1, 0xff, 0xea, 0x60,
0xf, 0xff, 0x50, 0x80, 0x7f, 0xfa, 0x88, 0x3,
0xff, 0xd5, 0xe0, 0x1f, 0xfe, 0xa2, 0x0, 0xff,
0xf5, 0x8, 0x7, 0xff, 0x9, 0x9b, 0xff, 0x9e,
0xa0, 0x1f, 0xfc, 0x6, 0x0, 0xff, 0xe0, 0xd4,
0xcf, 0xff, 0x3c, 0xc0, 0x3f, 0xf8, 0x6, 0x1,
0xff, 0xc1, 0x60, 0xf, 0xfe, 0xe0, 0x80, 0x7f,
0xf0, 0x44, 0x3, 0xff, 0xb6, 0x20, 0x1f, 0xfc,
0x22, 0x0, 0xff, 0xed, 0x98, 0x7, 0xff, 0xa9,
0x80, 0x3f, 0xf8, 0x22, 0x1, 0xff, 0xdc, 0x10,
0xf, 0xfe, 0x9, 0x0, 0x7f, 0xf7, 0x8, 0x3,
0xff, 0x82, 0xe0, 0x1f, 0xfd, 0xcf, 0x0, 0xff,
0xe0, 0x90, 0x7, 0xff, 0x70, 0x80, 0x3f, 0xf8,
0x22, 0x1, 0xff, 0xdc, 0x10, 0xf, 0xfe, 0xf,
0x0, 0x7f, 0xf7, 0x18, 0x3, 0xff, 0x82, 0x60,
0x1f, 0xfd, 0xc3, 0x0, 0xff, 0xe0, 0xd5, 0xdf,
0xaa, 0x65, 0xc, 0x84, 0x1, 0xff, 0xcf, 0x10,
0xf, 0xfe, 0x12, 0x27, 0x95, 0x98, 0xf3, 0x7b,
0xfb, 0x4c, 0x40, 0x1f, 0xfc, 0x91, 0x0, 0xff,
0xe8, 0x92, 0xce, 0xe3, 0x88, 0x7, 0xff, 0x18,
0xc0, 0x3f, 0xfa, 0xa7, 0x1d, 0x22, 0x1, 0xff,
0xc4, 0x60, 0xf, 0xfe, 0xc3, 0x74, 0x0, 0x7f,
0xf0, 0xc4, 0x3, 0xff, 0xb4, 0xf8, 0x40, 0x1f,
0xfc, 0x12, 0x0, 0xff, 0xed, 0x9e, 0xa0, 0x7,
0xff, 0x3, 0xc0, 0x3f, 0xfb, 0xb6, 0x80, 0x1f,
0xf1, 0x0, 0x7f, 0xf7, 0xac, 0xc0, 0x3f, 0xc2,
0x1, 0xff, 0xdf, 0xd1, 0x0, 0xfe, 0x60, 0xf,
0xfe, 0xf8, 0xd8, 0x7, 0xf1, 0x80, 0x7f, 0xf8,
0x14, 0x80, 0x3f, 0x8, 0x7, 0xff, 0x86, 0x0,
0x3f, 0x7f, 0xff, 0xf2, 0x3b, 0x2e, 0x58, 0xc0,
0x3f, 0xf9, 0xa, 0x20, 0x1f, 0xfc, 0xf1, 0x34,
0x69, 0xcf, 0x93, 0x0, 0xff, 0xe3, 0x38, 0x7,
0xff, 0x59, 0xb2, 0x40, 0x3f, 0xf8, 0xb4, 0x1,
0xff, 0xd8, 0x6c, 0x0, 0xff, 0xe2, 0x18, 0x7,
0xff, 0x64, 0xe4, 0x3, 0xff, 0x88, 0x40, 0x1f,
0xfd, 0x96, 0x10, 0xf, 0xfe, 0x1b, 0x80, 0x7f,
0xf6, 0x98, 0x3, 0xff, 0x86, 0x60, 0x1f, 0xfd,
0xad, 0x0, 0xff, 0xe1, 0x88, 0x7, 0xff, 0x69,
0x80, 0x3f, 0xf8, 0x7e, 0x1, 0xff, 0xda, 0x70,
0xf, 0xfe, 0x1f, 0x80, 0x7f, 0xf6, 0x88, 0x3,
0xff, 0x86, 0x20, 0x1f, 0x8, 0x7, 0xff, 0x4b,
0x0, 0x3f, 0xf8, 0x64, 0x1, 0xe5, 0xe6, 0x0,
0xff, 0xe8, 0xa, 0x80, 0x7f, 0xf0, 0xd4, 0x3,
0xd0, 0x13, 0x86, 0x1, 0xff, 0xce, 0xb0, 0xf,
0xfe, 0x18, 0x88, 0x3, 0x94, 0x80, 0x7, 0x96,
0x60, 0x1f, 0xfc, 0xb7, 0x50, 0xf, 0xfe, 0x1a,
0x0, 0x7a, 0x0, 0x39, 0x33, 0xc, 0x20, 0x1f,
0xfc, 0x61, 0xa8, 0x0, 0xff, 0xe2, 0x78, 0x7,
0x29, 0x0, 0x7c, 0x73, 0xd6, 0xc4, 0x1, 0xff,
0xc1, 0x28, 0xe5, 0x0, 0xff, 0xe2, 0xa8, 0x7,
0x40, 0x7, 0xfc, 0x93, 0xbd, 0x72, 0xea, 0x86,
0x64, 0x57, 0x9d, 0xd3, 0x80, 0x7f, 0xf1, 0x98,
0x3, 0x98, 0x80, 0x3f, 0xf8, 0x42, 0x8d, 0x15,
0x79, 0x8b, 0xa8, 0x62, 0x0, 0xff, 0xe4, 0x40,
0x7, 0x40, 0x7, 0xff, 0x8d, 0xc4, 0x3, 0x30,
0x80, 0x7f, 0xf8, 0x8a, 0x0, 0x3a, 0x0, 0x3f,
0xfc, 0x7c, 0x1, 0xcc, 0x20, 0x1f, 0xfe, 0x29,
0x30, 0xe, 0xb0, 0xf, 0xff, 0x14, 0xb0, 0x7,
0xb1, 0x0, 0x3f, 0xfc, 0x12, 0xc0, 0x1f, 0xd,
0xd1, 0x0, 0x7f, 0xf7, 0x7, 0x18, 0x3, 0xfc,
0xbb, 0x24, 0x1, 0xff, 0xd9, 0x6f, 0x30, 0xf,
0xfe, 0xb, 0x6d, 0xa0, 0x7, 0xff, 0x54, 0xf2,
0x44, 0x3, 0xff, 0x88, 0x97, 0xd2, 0x80, 0x1f,
0xfd, 0x5, 0xcc, 0x18, 0x7, 0xff, 0x20, 0x5a,
0xfe, 0xdc, 0xc0, 0x3f, 0xf9, 0x2, 0xb3, 0xd4,
0x60, 0x1f, 0xfc, 0xe4, 0x8c, 0xfd, 0xa7, 0x64,
0x21, 0x10, 0x4, 0x22, 0x23, 0x68, 0xad, 0xea,
0x61, 0x0, 0xff, 0xe1, 0x80,
/* U+0036 "6" */
0x0, 0xff, 0xe7, 0x12, 0xbd, 0x66, 0xf7, 0xfe,
0xed, 0xca, 0x86, 0x30, 0xf, 0xfe, 0xb8, 0xad,
0x76, 0xd4, 0x29, 0x90, 0x80, 0x61, 0x23, 0x57,
0x9c, 0xfc, 0x73, 0x0, 0xff, 0xe8, 0xa5, 0xf5,
0x28, 0x80, 0x7f, 0xf1, 0xce, 0x33, 0x60, 0x40,
0x3f, 0xf9, 0x8d, 0xb6, 0x80, 0x1f, 0xfc, 0xe2,
0x7e, 0x91, 0x0, 0xff, 0xe4, 0x26, 0xc9, 0x0,
0x7f, 0xf4, 0xdb, 0x98, 0x3, 0xff, 0x8a, 0x35,
0x64, 0x1, 0xff, 0xd7, 0x20, 0xf, 0xfe, 0x22,
0x7a, 0x80, 0x7f, 0xf6, 0xd, 0x0, 0x3f, 0xf8,
0x6f, 0x62, 0x1, 0xff, 0xd9, 0x80, 0xf, 0xfe,
0x1c, 0x40, 0x3, 0xff, 0xb4, 0x68, 0x1, 0xff,
0xc2, 0xa7, 0x0, 0xff, 0xed, 0xc0, 0x7, 0xff,
0xa, 0x14, 0x3, 0xff, 0xb6, 0x68, 0x1, 0xff,
0xc1, 0x67, 0x0, 0xff, 0xee, 0x40, 0x7, 0xff,
0x4, 0xe4, 0x3, 0xff, 0x98, 0x24, 0x50, 0x80,
0x7f, 0xc6, 0x80, 0x1f, 0xfc, 0x1d, 0x0, 0xff,
0xe4, 0x13, 0xd7, 0x7f, 0x6e, 0xdd, 0xfd, 0x70,
0xa2, 0x1, 0xd0, 0x1, 0xff, 0xc1, 0x81, 0x0,
0xff, 0xe3, 0x36, 0xea, 0x14, 0x40, 0x3e, 0x14,
0x7a, 0xea, 0x40, 0x1, 0xa0, 0x7, 0xff, 0x0,
0xdc, 0x3, 0xff, 0x8a, 0x9b, 0x24, 0x1, 0xff,
0xc4, 0x5b, 0xb1, 0x40, 0x7, 0xff, 0x7, 0xc0,
0x3f, 0xf8, 0xb3, 0x64, 0x1, 0xff, 0xc8, 0x4d,
0x40, 0xf, 0xfe, 0x2, 0x98, 0x7, 0xff, 0x12,
0xd8, 0x3, 0xff, 0xb3, 0x20, 0x1f, 0xfc, 0x4b,
0x40, 0xf, 0xfe, 0xc9, 0x98, 0x3, 0xff, 0x87,
0x28, 0x1, 0xff, 0xda, 0x90, 0xf, 0xfe, 0x1a,
0x30, 0x7, 0xff, 0x6d, 0x40, 0x3f, 0xf8, 0x72,
0x1, 0xff, 0xdb, 0x50, 0xf, 0xfe, 0x1b, 0x10,
0x7, 0xff, 0x6f, 0x0, 0x3f, 0xf8, 0x74, 0x1,
0xff, 0xdc, 0x40, 0xf, 0xfe, 0x11, 0x18, 0x7,
0xff, 0x6c, 0x48, 0x3, 0xff, 0x84, 0xa0, 0x1f,
0xfd, 0xc4, 0x0, 0xff, 0xe1, 0xe0, 0x7, 0xf1,
0xb4, 0x55, 0xdd, 0x53, 0xc, 0x60, 0x1f, 0xfc,
0x7d, 0x0, 0xff, 0xe1, 0xa0, 0x7, 0x13, 0xe7,
0xe4, 0xba, 0xa2, 0x4a, 0xcf, 0x39, 0xf6, 0xe2,
0x1, 0xff, 0xc3, 0x20, 0xf, 0xfe, 0x11, 0x0,
0x64, 0xbd, 0x83, 0x0, 0xff, 0xe1, 0x24, 0x75,
0x18, 0x7, 0xff, 0x5, 0x80, 0x3f, 0xf8, 0x4c,
0x0, 0x2b, 0xb2, 0x0, 0x7f, 0xf2, 0x17, 0x2c,
0x80, 0x3f, 0xe3, 0x0, 0xff, 0xe1, 0x8, 0x36,
0xa0, 0x7, 0xff, 0x31, 0x35, 0xc0, 0x3f, 0xf9,
0xa7, 0x32, 0x0, 0xff, 0xe8, 0x45, 0x88, 0x7,
0xc2, 0x1, 0xff, 0xc3, 0x66, 0x0, 0x7f, 0xf4,
0x93, 0x4, 0x3, 0xc6, 0x1, 0xff, 0xe4, 0x2c,
0x10, 0xe, 0x10, 0xf, 0xff, 0x29, 0x68, 0x7,
0xff, 0xb0, 0xe0, 0x3, 0xff, 0xd8, 0xe8, 0x1,
0xff, 0xec, 0x80, 0x8, 0x40, 0x3f, 0xf9, 0x62,
0xd5, 0xbf, 0xf6, 0xdc, 0x18, 0x7, 0xff, 0x18,
0xd4, 0x0, 0x60, 0x1f, 0xfc, 0x91, 0x9e, 0x95,
0x20, 0x8, 0x91, 0xf3, 0x54, 0x3, 0xff, 0x8b,
0x20, 0x1f, 0xfc, 0xb4, 0xf6, 0x0, 0xff, 0x8a,
0xa4, 0x3, 0xff, 0x88, 0x64, 0x2, 0x1, 0xff,
0xc7, 0x4b, 0x10, 0xf, 0xfe, 0x13, 0x50, 0x7,
0xff, 0x11, 0x40, 0x2, 0x1, 0xff, 0xc5, 0x2b,
0x0, 0xff, 0xe2, 0xac, 0x0, 0x7f, 0xf0, 0xf4,
0x0, 0x40, 0x1f, 0xfc, 0x59, 0x0, 0xff, 0xe3,
0xb9, 0x0, 0x7f, 0xf0, 0x90, 0x0, 0xc0, 0x1f,
0xfc, 0x43, 0x40, 0xf, 0xfe, 0x45, 0x0, 0x7f,
0xf0, 0x84, 0x1, 0xa0, 0x1f, 0xfc, 0x4b, 0x0,
0xff, 0xe4, 0xa8, 0x7, 0xff, 0xc, 0x40, 0x80,
0x3f, 0xf8, 0x86, 0x1, 0xff, 0xc9, 0x20, 0xf,
0xfe, 0x19, 0x82, 0x0, 0x7f, 0xf1, 0x18, 0x3,
0xff, 0x94, 0x20, 0x1f, 0xfc, 0x21, 0x1, 0x20,
0xf, 0xfe, 0xd1, 0x80, 0x7f, 0xf0, 0x84, 0x0,
0x80, 0x1f, 0xfc, 0x31, 0x0, 0xff, 0xe5, 0x18,
0x7, 0xff, 0x8, 0xc0, 0x1a, 0x1, 0xff, 0xc3,
0x50, 0xf, 0xfe, 0x48, 0x88, 0x3, 0xff, 0x84,
0x20, 0x4, 0x10, 0xf, 0xfe, 0x16, 0x80, 0x7f,
0xf2, 0x5c, 0x3, 0xff, 0x84, 0x40, 0x19, 0x80,
0x3f, 0xf8, 0x48, 0x40, 0x1f, 0xfc, 0x8a, 0x0,
0xff, 0xe1, 0x38, 0x6, 0xa0, 0xf, 0xfe, 0x1c,
0x0, 0x7f, 0xf1, 0xd0, 0xc0, 0x3f, 0xf8, 0x5a,
0x1, 0x89, 0x0, 0x3f, 0xf8, 0x4a, 0xe0, 0x1f,
0xfc, 0x51, 0xa0, 0xf, 0xfe, 0x1a, 0x80, 0x74,
0x0, 0x7f, 0xf0, 0xe1, 0xc0, 0x3f, 0xf8, 0x65,
0x82, 0x1, 0xff, 0xc2, 0x32, 0x0, 0xe3, 0x50,
0xf, 0xfe, 0x1c, 0x59, 0x0, 0x7f, 0xf0, 0x1b,
0x8, 0x3, 0xff, 0x87, 0x40, 0x1f, 0x58, 0x7,
0xff, 0x11, 0x36, 0x94, 0x40, 0x3c, 0x4f, 0xb2,
0x20, 0x1f, 0xfc, 0x32, 0x60, 0xf, 0x86, 0x0,
0x3f, 0xf8, 0xab, 0x5d, 0xcd, 0xcc, 0x6f, 0xec,
0x10, 0x7, 0xff, 0x16, 0x0, 0x3f, 0x9d, 0x40,
0x3f, 0xf8, 0xe2, 0x46, 0x62, 0x0, 0xff, 0xe4,
0x22, 0x80, 0x7f, 0xa8, 0x80, 0x3f, 0xfb, 0xc3,
0x40, 0x1f, 0xfc, 0xf, 0x10, 0xf, 0xfe, 0xee,
0x88, 0x7, 0xff, 0x0, 0xbc, 0x80, 0x3f, 0xfb,
0x76, 0x60, 0x1f, 0xfc, 0x21, 0xc3, 0x0, 0xff,
0xec, 0xda, 0x0, 0x7f, 0xf1, 0x7, 0x18, 0x3,
0xff, 0xac, 0x5a, 0x80, 0x1f, 0xfc, 0x79, 0xb1,
0x0, 0xff, 0xe9, 0xae, 0x90, 0x7, 0xff, 0x25,
0x3a, 0x4, 0x3, 0xff, 0x9e, 0x79, 0x40, 0x1f,
0xfc, 0xd7, 0xe9, 0x20, 0xf, 0xfe, 0x5a, 0x5e,
0x18, 0x7, 0xff, 0x41, 0xb7, 0x1c, 0x80, 0x3f,
0xf8, 0xa6, 0xfb, 0x68, 0x1, 0xff, 0xd4, 0x38,
0xde, 0xb8, 0x63, 0x21, 0x10, 0x0, 0x48, 0xd6,
0x2b, 0xb2, 0x8, 0x3, 0xff, 0x86,
/* U+0037 "7" */
0x6d, 0xdf, 0xff, 0x4a, 0x71, 0x17, 0xff, 0xa4,
0x40, 0x3f, 0xff, 0xe0, 0x1f, 0xff, 0xf0, 0xf,
0xff, 0xf8, 0x7, 0xff, 0xfc, 0x3, 0xff, 0xfe,
0x1, 0xff, 0xff, 0x0, 0xff, 0xf0, 0x8, 0x7,
0xff, 0xa4, 0x90, 0x3, 0xff, 0xd3, 0x60, 0x1f,
0xfc, 0x3a, 0xff, 0xff, 0xe4, 0xa8, 0x7, 0xff,
0x11, 0x80, 0x3f, 0xfb, 0x2, 0xa0, 0x1f, 0xfc,
0x36, 0x0, 0xff, 0xec, 0xc8, 0x7, 0xff, 0x12,
0xc0, 0x3f, 0xfb, 0x2c, 0x1, 0xff, 0xc3, 0x42,
0x0, 0xff, 0xec, 0x30, 0x80, 0x7f, 0xf0, 0xfc,
0x3, 0xff, 0xb3, 0x60, 0x1f, 0xfc, 0x32, 0x40,
0xf, 0xfe, 0xc2, 0x10, 0x7, 0xff, 0xe, 0xc0,
0x3f, 0xfb, 0x3e, 0x1, 0xff, 0xc3, 0x16, 0x0,
0xff, 0xec, 0x12, 0x0, 0x7f, 0xf0, 0xd8, 0x3,
0xff, 0xb3, 0x60, 0x1f, 0xfc, 0x49, 0x0, 0xff,
0xec, 0xb0, 0x7, 0xff, 0xd, 0x44, 0x3, 0xa3,
0xff, 0xff, 0x82, 0xa0, 0x1f, 0xfc, 0x47, 0x0,
0xff, 0xe2, 0x48, 0x7, 0xff, 0x66, 0x40, 0x3f,
0xf8, 0x66, 0x60, 0xf, 0xfe, 0xc2, 0x88, 0x7,
0xff, 0xe, 0x40, 0x3f, 0xfb, 0x32, 0x1, 0xff,
0xc3, 0x15, 0x0, 0xff, 0xec, 0x19, 0x80, 0x3f,
0xf8, 0x6c, 0x1, 0xff, 0xd9, 0x90, 0xf, 0xfe,
0x24, 0x80, 0x7f, 0xf6, 0x5, 0x40, 0x3f, 0xf8,
0x6a, 0x20, 0x1f, 0xfd, 0x89, 0x0, 0xff, 0xe2,
0x48, 0x7, 0xff, 0x65, 0x80, 0x3f, 0xf8, 0x66,
0x60, 0xf, 0xfe, 0xc3, 0x8, 0x7, 0xff, 0xe,
0x40, 0x3f, 0xfb, 0x36, 0x1, 0xff, 0xc3, 0x15,
0x0, 0xff, 0xec, 0x21, 0x0, 0x7f, 0xf0, 0xe4,
0x3, 0xff, 0xb3, 0xe0, 0x1f, 0xfc, 0x46, 0x0,
0xff, 0xec, 0x12, 0x0, 0x7f, 0xf0, 0xd8, 0x40,
0x3f, 0xfb, 0x16, 0x1, 0xff, 0xc4, 0xb0, 0xf,
0xfe, 0xc0, 0xb0, 0x7, 0xff, 0xd, 0x8, 0x3,
0xff, 0xb0, 0xc0, 0x1f, 0xfc, 0x4f, 0x0, 0xff,
0xec, 0xc8, 0x7, 0xff, 0xc, 0x90, 0x3, 0xff,
0xb0, 0xa2, 0x1, 0xff, 0xc3, 0xa0, 0xf, 0xfe,
0xcc, 0x80, 0x7f, 0xf1, 0x1c, 0x3, 0xff, 0xb0,
0x66, 0x0, 0xff, 0xe1, 0xb0, 0x7, 0xff, 0x66,
0x40, 0x3f, 0xf8, 0x96, 0x1, 0xff, 0xd8, 0x15,
0x0, 0xff, 0xe1, 0xa1, 0x0, 0x7f, 0xf6, 0x24,
0x3, 0xff, 0x89, 0xe0, 0x1f, 0xfd, 0x96, 0x0,
0xff, 0xe1, 0x92, 0x0, 0x7f, 0xf6, 0x18, 0x40,
0x3f, 0xf8, 0x76, 0x1, 0xff, 0xd9, 0xb0, 0xf,
0xfe, 0x18, 0xb0, 0x7, 0xff, 0x61, 0x8, 0x3,
0xff, 0x86, 0xc0, 0x1f, 0xfd, 0x9f, 0x0, 0xff,
0xe2, 0x48, 0x7, 0xff, 0x60, 0x90, 0x3, 0xff,
0x86, 0xa2, 0x1, 0xff, 0xd8, 0xb0, 0xf, 0xfe,
0x24, 0x80, 0x7f, 0xf6, 0x5, 0x80, 0x3f, 0xf8,
0x66, 0x60, 0xf, 0xfe, 0xc3, 0x0, 0x7f, 0xf1,
0x24, 0x3, 0xff, 0xb3, 0x20, 0x1f, 0xfc, 0x31,
0x50, 0xf, 0xfe, 0xc2, 0x88, 0x7, 0xff, 0xd,
0x80, 0x3f, 0xfb, 0x32, 0x1, 0xff, 0xc4, 0x90,
0xf, 0xfe, 0xc1, 0x98, 0x3, 0xff, 0x86, 0xa2,
0x1, 0xff, 0xd8, 0x90, 0xf, 0xfe, 0x24, 0x80,
0x7f, 0xf6, 0x5, 0x40, 0x3f, 0xf8, 0x66, 0x60,
0xf, 0xfe, 0xc4, 0x80, 0x7f, 0xf1, 0x24, 0x3,
0xff, 0xb2, 0xc0, 0x1f, 0xfc, 0x31, 0x50, 0xf,
0xfe, 0xc3, 0x8, 0x7, 0xff, 0xe, 0x40, 0x3f,
0xfb, 0x36, 0x1, 0xff, 0xc4, 0x60, 0xf, 0xfe,
0xc2, 0x10, 0x7, 0xff, 0xd, 0x84, 0x3, 0xff,
0x98,
/* U+0038 "8" */
0x0, 0xff, 0xe5, 0x9, 0xab, 0x3c, 0x44, 0xec,
0xa8, 0x20, 0x1f, 0xfd, 0xc4, 0x8b, 0xfe, 0xca,
0x98, 0x77, 0xa2, 0x6a, 0xfb, 0xf2, 0x54, 0x40,
0x3f, 0xfa, 0x89, 0x5f, 0x6e, 0x80, 0x1f, 0xfc,
0x33, 0x6a, 0xeb, 0x60, 0xf, 0xfe, 0x83, 0x6d,
0xa8, 0x7, 0xff, 0x2d, 0x27, 0xe0, 0x40, 0x3f,
0xf9, 0x69, 0xd2, 0x40, 0x1f, 0xfd, 0x7, 0xe7,
0x0, 0xff, 0xe4, 0xd, 0x58, 0x80, 0x7f, 0xf5,
0x23, 0x4c, 0x3, 0xff, 0x8a, 0x58, 0xa0, 0x1f,
0xfd, 0x72, 0xc5, 0x0, 0xff, 0xe1, 0x9e, 0x90,
0x7, 0xff, 0x6a, 0x9c, 0x3, 0xff, 0x82, 0x5a,
0x1, 0xff, 0xdd, 0x86, 0x0, 0xff, 0x87, 0xc4,
0x3, 0xff, 0xbd, 0x28, 0x1, 0xfe, 0x82, 0x0,
0xff, 0xf0, 0x48, 0x7, 0xf1, 0xb0, 0x7, 0xff,
0x84, 0xa0, 0x3, 0xf4, 0x0, 0x7f, 0xf1, 0x85,
0xeb, 0x7b, 0xfd, 0xd7, 0x4, 0x1, 0xff, 0xc6,
0x71, 0x0, 0xf1, 0x20, 0x7, 0xff, 0x10, 0x6b,
0xa1, 0x48, 0x40, 0x2, 0x8f, 0xb8, 0x60, 0x1f,
0xfc, 0x56, 0x0, 0xf2, 0x80, 0x7f, 0xf1, 0xf,
0xd4, 0x3, 0xfe, 0x3c, 0x70, 0xf, 0xfe, 0x25,
0x0, 0x7b, 0x0, 0x3f, 0xf8, 0x67, 0x82, 0x1,
0xff, 0xc2, 0x87, 0x0, 0xff, 0xe1, 0x90, 0x80,
0x72, 0x80, 0x7f, 0xf0, 0xfc, 0x3, 0xff, 0x8b,
0x4, 0x1, 0xff, 0xc3, 0x50, 0xc, 0x22, 0x0,
0xff, 0xe1, 0x21, 0x80, 0x7f, 0xf1, 0xac, 0x3,
0xff, 0x86, 0x40, 0x18, 0xc0, 0x3f, 0xf8, 0x78,
0x1, 0xff, 0xc7, 0x70, 0xf, 0xfe, 0x18, 0x80,
0x61, 0x0, 0xff, 0xe1, 0x98, 0x7, 0xff, 0x1c,
0x40, 0x3f, 0xf8, 0x7c, 0x1, 0xff, 0xc8, 0x70,
0xf, 0xfe, 0x40, 0x80, 0x7f, 0xf0, 0x84, 0x3,
0x10, 0x7, 0xff, 0xd, 0x80, 0x3f, 0xf9, 0x2,
0x1, 0xff, 0xc2, 0xf0, 0xc, 0x20, 0x1f, 0xfc,
0x32, 0x0, 0xff, 0xe3, 0x88, 0x7, 0xff, 0xc,
0x40, 0x38, 0x80, 0x3f, 0xf8, 0x56, 0x1, 0xff,
0xc7, 0x70, 0xf, 0xfe, 0x1a, 0x0, 0x72, 0x0,
0x7f, 0xf0, 0x8d, 0x0, 0x3f, 0xf8, 0xd6, 0x1,
0xff, 0xc2, 0x12, 0x0, 0xed, 0x0, 0xff, 0xe1,
0xc8, 0x80, 0x7f, 0xf1, 0x24, 0x80, 0x3f, 0xf8,
0x4e, 0x1, 0xe4, 0x20, 0xf, 0xfe, 0x11, 0x61,
0x0, 0x7f, 0xf0, 0xa5, 0x80, 0x3f, 0xf8, 0x74,
0x1, 0xf5, 0x80, 0x7f, 0xf0, 0xcb, 0x5c, 0x40,
0x3f, 0xcb, 0xac, 0x1, 0xff, 0xc3, 0x43, 0x0,
0xf9, 0x8c, 0x3, 0xff, 0x89, 0x1d, 0x70, 0xa8,
0x68, 0xaf, 0x5f, 0x44, 0x1, 0xff, 0xc4, 0x90,
0xf, 0xee, 0x0, 0xff, 0xe3, 0x23, 0xd5, 0xe5,
0xd4, 0x28, 0x7, 0xff, 0x1a, 0x8, 0x3, 0xf8,
0xac, 0x3, 0xff, 0xbe, 0xce, 0x1, 0xff, 0x25,
0x0, 0x7f, 0xf7, 0x56, 0x40, 0x3f, 0xf8, 0x2b,
0x82, 0x1, 0xff, 0xda, 0x8a, 0x0, 0xff, 0xe1,
0x9f, 0x88, 0x7, 0xff, 0x62, 0xdc, 0x3, 0xff,
0x88, 0x36, 0x20, 0x1f, 0xfd, 0x89, 0x30, 0xf,
0xfe, 0x1a, 0xf2, 0x80, 0x7f, 0xf6, 0x4b, 0x24,
0x3, 0xff, 0x83, 0x14, 0x1, 0xff, 0xdd, 0x6c,
0x10, 0xf, 0xf5, 0x38, 0x7, 0xff, 0x7c, 0xfc,
0x40, 0x3f, 0x4a, 0x80, 0x7f, 0xf8, 0x47, 0x4,
0x3, 0xcc, 0xc0, 0xf, 0xfe, 0x41, 0x22, 0xb2,
0xa1, 0x80, 0x7f, 0xf2, 0xa, 0x80, 0x38, 0x64,
0x3, 0xff, 0x8c, 0xd7, 0xfb, 0x75, 0x35, 0x79,
0xf8, 0xe2, 0x1, 0xff, 0xc5, 0x46, 0x0, 0xd6,
0x1, 0xff, 0xc5, 0x6f, 0x94, 0x0, 0xfe, 0x38,
0xe9, 0x10, 0xf, 0xfe, 0x24, 0x88, 0x0, 0x94,
0x3, 0xff, 0x89, 0x92, 0x1, 0xff, 0xc3, 0x6f,
0x30, 0xf, 0xfe, 0x23, 0x0, 0x28, 0x3, 0xff,
0x89, 0x86, 0x1, 0xff, 0xc5, 0x1c, 0x30, 0xf,
0xfe, 0x1c, 0x80, 0x14, 0x3, 0xff, 0x87, 0x6,
0x1, 0xff, 0xc8, 0xe0, 0xf, 0xfe, 0x18, 0x98,
0x90, 0x7, 0xff, 0x8, 0x5c, 0x3, 0xff, 0x92,
0x4c, 0x1, 0xff, 0xc3, 0x44, 0x0, 0x7f, 0xf0,
0xdc, 0x3, 0xff, 0x97, 0x40, 0x1f, 0xfc, 0x3c,
0x20, 0xf, 0xfe, 0x1e, 0x80, 0x7f, 0xf2, 0xcc,
0x3, 0xff, 0x86, 0xde, 0x1, 0xff, 0xc3, 0x70,
0xf, 0xfe, 0x61, 0x0, 0x7f, 0xf0, 0x8c, 0x40,
0x3f, 0xf8, 0x62, 0x1, 0xff, 0xcc, 0x10, 0xf,
0xfe, 0x10, 0x80, 0x7f, 0xf1, 0x4, 0x3, 0xff,
0x98, 0x20, 0x1f, 0xfc, 0x31, 0x0, 0xff, 0xe1,
0xb8, 0x7, 0xff, 0x30, 0x80, 0x3f, 0xf8, 0x43,
0xe0, 0x1f, 0xfc, 0x3d, 0x0, 0xff, 0xe5, 0x98,
0x7, 0xff, 0xc, 0xcc, 0x1, 0xff, 0xc3, 0x70,
0xf, 0xfe, 0x5d, 0x0, 0x7f, 0xf0, 0xdd, 0x40,
0x3f, 0xf8, 0x62, 0xe0, 0x1f, 0xfc, 0x92, 0x60,
0xf, 0xfe, 0x1e, 0x9, 0x0, 0x7f, 0xf0, 0xe0,
0xc0, 0x3f, 0xf9, 0x1c, 0x1, 0xff, 0xc4, 0x50,
0x50, 0xf, 0xfe, 0x26, 0x18, 0x7, 0xff, 0x14,
0x70, 0xc0, 0x3f, 0xf8, 0x64, 0x61, 0x20, 0x1f,
0xfc, 0x5c, 0x80, 0xf, 0xfe, 0x1b, 0x79, 0x80,
0x7f, 0xf1, 0x28, 0x0, 0x2a, 0x1, 0xff, 0xc5,
0x7e, 0x93, 0x0, 0xfe, 0x27, 0xd9, 0x10, 0xf,
0xfe, 0x2b, 0x0, 0x56, 0x1, 0xff, 0xc6, 0x16,
0xcf, 0xca, 0xa4, 0xca, 0xb3, 0xf6, 0x8, 0x3,
0xff, 0x8d, 0x22, 0x1, 0xc, 0x0, 0x7f, 0xf2,
0x4d, 0x54, 0xcc, 0x53, 0x0, 0xff, 0xe4, 0x9b,
0x0, 0x73, 0xa0, 0x7, 0xff, 0x8c, 0x78, 0x3,
0xeb, 0x30, 0xf, 0xff, 0x16, 0x90, 0x7, 0xed,
0x30, 0xf, 0xff, 0x5, 0x98, 0x7, 0xf0, 0xe2,
0x0, 0x7f, 0xf7, 0x47, 0x50, 0x3, 0xff, 0x81,
0x70, 0x1, 0xff, 0xdb, 0x4f, 0x20, 0xf, 0xfe,
0x13, 0xe9, 0x80, 0x7f, 0xf5, 0xc6, 0xac, 0x40,
0x3f, 0xf8, 0x85, 0x96, 0x60, 0x1f, 0xfd, 0x31,
0x8e, 0x50, 0xf, 0xfe, 0x42, 0x65, 0xa8, 0x7,
0xff, 0x3c, 0xe7, 0x9c, 0x3, 0xff, 0x98, 0x95,
0xf4, 0xa4, 0x1, 0xff, 0xc7, 0x14, 0x9e, 0xc6,
0x0, 0xff, 0xe8, 0xad, 0x6f, 0x5c, 0x32, 0x10,
0x88, 0x2, 0x11, 0x12, 0x34, 0x56, 0xf5, 0xb0,
0x80, 0x7f, 0xf0, 0xc0,
/* U+0039 "9" */
0x0, 0xff, 0xe3, 0xa3, 0xce, 0x6f, 0x7f, 0xdd,
0xb9, 0x4e, 0x82, 0x1, 0xff, 0xd9, 0x16, 0xbf,
0xb8, 0x63, 0x21, 0x0, 0x84, 0x8d, 0x62, 0xfb,
0x92, 0x60, 0x1f, 0xfd, 0x43, 0x9e, 0x94, 0x0,
0xff, 0xe2, 0x8b, 0x67, 0x40, 0x80, 0x7f, 0xf3,
0xca, 0xb1, 0x80, 0x3f, 0xf9, 0x62, 0xfd, 0x2,
0x1, 0xff, 0xcc, 0x7d, 0x50, 0xf, 0xfe, 0x83,
0xf2, 0x80, 0x7f, 0xf2, 0x46, 0xe0, 0x3, 0xff,
0xa9, 0x56, 0x20, 0x1f, 0xfc, 0x62, 0xf4, 0x0,
0xff, 0xeb, 0x27, 0x90, 0x7, 0xff, 0x10, 0xb0,
0x40, 0x3f, 0xfb, 0x3, 0x86, 0x1, 0xff, 0xc2,
0x2c, 0x10, 0xf, 0xfe, 0xd0, 0xe1, 0x80, 0x7f,
0xf0, 0x78, 0x40, 0x3f, 0xfb, 0xba, 0x20, 0x1f,
0xf4, 0x98, 0x7, 0xff, 0x78, 0x74, 0x3, 0xfc,
0x6c, 0x1, 0xff, 0xc8, 0x23, 0x38, 0x40, 0x3f,
0xf8, 0xe7, 0x20, 0x1f, 0xde, 0x1, 0xff, 0xc5,
0x17, 0xcf, 0xdc, 0xcb, 0xbe, 0xd8, 0x3, 0xff,
0x8a, 0xc6, 0x1, 0xf2, 0x18, 0x7, 0xff, 0x11,
0xfa, 0xc, 0x3, 0xe4, 0x9f, 0x70, 0xf, 0xfe,
0x27, 0x80, 0x7d, 0xc0, 0x1f, 0xfc, 0x4b, 0x80,
0xf, 0xfe, 0xc, 0x58, 0x7, 0xff, 0xc, 0xd8,
0x3, 0xca, 0x1, 0xff, 0xc3, 0xb4, 0x0, 0xff,
0xe1, 0xa6, 0x0, 0x7f, 0xf0, 0xe0, 0x3, 0x90,
0x3, 0xff, 0x86, 0xe8, 0x1, 0xff, 0xc5, 0x38,
0x0, 0xff, 0xe1, 0xa, 0x0, 0x6c, 0x0, 0xff,
0xe1, 0xc, 0x0, 0x7f, 0xf1, 0xdc, 0x40, 0x3f,
0xf8, 0x5e, 0x1, 0x8c, 0x3, 0xff, 0x84, 0xe0,
0x1f, 0xfc, 0x96, 0x0, 0xff, 0xe1, 0x21, 0x0,
0x4a, 0x1, 0xff, 0xc2, 0xc0, 0xf, 0xfe, 0x4d,
0x80, 0x7f, 0xf0, 0xd4, 0x2, 0x10, 0xf, 0xfe,
0x13, 0x80, 0x7f, 0xf2, 0x48, 0x3, 0xff, 0x87,
0x80, 0x1f, 0xfc, 0x63, 0x0, 0xff, 0xe4, 0x88,
0x7, 0xff, 0xd, 0x0, 0x2, 0x1, 0xff, 0xc3,
0x30, 0xf, 0xfe, 0xd1, 0x80, 0x80, 0x7f, 0xf0,
0xc4, 0x3, 0xff, 0x92, 0x20, 0x1f, 0xfc, 0x44,
0x0, 0xff, 0xe2, 0xa8, 0x7, 0xff, 0x25, 0x0,
0x3f, 0xf8, 0x9a, 0x0, 0x10, 0xf, 0xfe, 0x16,
0x80, 0x7f, 0xf2, 0x7c, 0x3, 0xff, 0x88, 0xc0,
0x5, 0x0, 0xff, 0xe1, 0x21, 0x0, 0x7f, 0xf1,
0xc9, 0x40, 0x3f, 0xf8, 0x84, 0x0, 0x30, 0xf,
0xfe, 0x1c, 0x80, 0x7f, 0xf1, 0xe4, 0x3, 0xff,
0x8a, 0x22, 0xc, 0x0, 0xff, 0xe1, 0xa4, 0x0,
0x7f, 0xf1, 0x61, 0x0, 0x3f, 0xf8, 0xc6, 0x8,
0x1, 0xff, 0xc4, 0x78, 0x0, 0xff, 0xe1, 0xcb,
0x80, 0x7f, 0xf1, 0xc4, 0x0, 0xc0, 0x1f, 0xfc,
0x47, 0xc4, 0x0, 0xff, 0x97, 0x58, 0x3, 0xff,
0x90, 0xe0, 0xa, 0x0, 0xff, 0xe2, 0x9d, 0xec,
0x21, 0x8, 0x0, 0x92, 0x3a, 0x88, 0x3, 0xff,
0x92, 0x20, 0x3, 0x50, 0xf, 0xfe, 0x31, 0x3d,
0xef, 0x7f, 0xb6, 0xdc, 0x40, 0x3f, 0xf9, 0xf0,
0x1, 0xff, 0xe8, 0x30, 0x8, 0xa4, 0x3, 0xff,
0xd8, 0xca, 0x1, 0xff, 0xe6, 0x30, 0xe, 0xa4,
0x0, 0xff, 0xf6, 0x5a, 0x0, 0x7f, 0xf9, 0x4,
0x3, 0xeb, 0x60, 0xf, 0xfe, 0x91, 0xc8, 0x7,
0xff, 0xd, 0x80, 0x3f, 0x4d, 0x0, 0x7f, 0xf4,
0x1f, 0x0, 0x3f, 0xf8, 0x86, 0x1, 0xfc, 0xbc,
0xc0, 0x1f, 0xfc, 0xc2, 0xc8, 0xe, 0x0, 0xff,
0xe1, 0x88, 0x7, 0xf8, 0x67, 0x58, 0x3, 0xff,
0x90, 0x55, 0xa6, 0x0, 0x20, 0xf, 0xfe, 0x11,
0x0, 0x7f, 0xf0, 0x4a, 0x7a, 0x54, 0x3, 0xff,
0x84, 0x4f, 0x9a, 0xa0, 0x19, 0xc0, 0x3f, 0xf8,
0x4e, 0x1, 0xff, 0xc3, 0x16, 0xaf, 0xca, 0x76,
0x54, 0x49, 0x5e, 0x2f, 0xb6, 0xc, 0x3, 0xc4,
0x1, 0xff, 0xc2, 0xd0, 0xf, 0xfe, 0x39, 0xac,
0x4d, 0x5d, 0xd5, 0xe, 0x82, 0x1, 0xf9, 0x0,
0x3f, 0xf8, 0x66, 0x1, 0xff, 0xdc, 0xd0, 0xf,
0xfe, 0x1a, 0x0, 0x7f, 0xf7, 0x18, 0x3, 0xff,
0x84, 0x60, 0x1f, 0xfd, 0xc6, 0x0, 0xff, 0xe1,
0xd8, 0x7, 0xff, 0x72, 0x0, 0x3f, 0xf8, 0x6a,
0x1, 0xff, 0xdb, 0x81, 0x0, 0xff, 0xe1, 0x19,
0x0, 0x7f, 0xf6, 0x8d, 0xc0, 0x3f, 0xf8, 0x72,
0x1, 0xff, 0xda, 0x2d, 0x0, 0xff, 0xe1, 0x8a,
0x80, 0x7f, 0xf6, 0x4b, 0x4, 0x3, 0xff, 0x87,
0x20, 0x1f, 0xfd, 0x94, 0xc1, 0x0, 0xff, 0xe1,
0x8b, 0x80, 0x7f, 0xf0, 0x7, 0x98, 0x3, 0xff,
0x90, 0x33, 0x62, 0x1, 0xff, 0xc4, 0xb0, 0xf,
0xfe, 0xc, 0x8c, 0xeb, 0x0, 0x7f, 0xf1, 0x6,
0x3d, 0x80, 0x3f, 0xf8, 0xa8, 0xa0, 0x1f, 0xfc,
0x1, 0x70, 0x1, 0x4f, 0xdb, 0xa9, 0x0, 0x7c,
0x28, 0xf9, 0xce, 0x20, 0x1f, 0xfc, 0x6a, 0x0,
0xff, 0xe0, 0xc8, 0x7, 0x92, 0x2b, 0x7f, 0xb9,
0xba, 0xef, 0xeb, 0x83, 0x0, 0xff, 0xe4, 0x50,
0x80, 0x7f, 0xf0, 0x5, 0xc0, 0x3f, 0xe1, 0x11,
0x10, 0x40, 0x3f, 0xf9, 0x8e, 0xa0, 0x1f, 0xfc,
0x19, 0x0, 0xff, 0xee, 0x24, 0x0, 0x7f, 0xf0,
0x45, 0xc0, 0x3f, 0xfb, 0x67, 0x60, 0x1f, 0xfc,
0x28, 0x0, 0xff, 0xed, 0xa6, 0x0, 0x7f, 0xf0,
0x85, 0x80, 0x3f, 0xfb, 0x4d, 0x60, 0x1f, 0xfc,
0x38, 0x0, 0xff, 0xed, 0x44, 0x80, 0x7f, 0xf0,
0xc5, 0x80, 0x3f, 0xfb, 0x7, 0xae, 0x1, 0xff,
0xc4, 0x21, 0x0, 0xff, 0xeb, 0xc, 0xe1, 0x0,
0x7f, 0xf1, 0x4f, 0xa0, 0x40, 0x3f, 0xfa, 0x42,
0xfc, 0xc0, 0x1f, 0xfc, 0x97, 0xe9, 0x30, 0xf,
0xfe, 0x71, 0xcf, 0x40, 0x7, 0xff, 0x35, 0xb3,
0x61, 0x0, 0x3f, 0xf9, 0x9, 0x1b, 0x8c, 0x1,
0xff, 0xd1, 0x27, 0xbf, 0xda, 0x75, 0x32, 0x10,
0xc, 0x24, 0x6a, 0xf3, 0xbf, 0x6e, 0x40, 0x1f,
0xfc, 0xa0,
/* U+003A ":" */
0x0, 0xf2, 0xcd, 0xe5, 0xd3, 0x8, 0x7, 0xf3,
0x75, 0x32, 0x1a, 0x2c, 0xf4, 0x0, 0x7d, 0x52,
0x20, 0x1f, 0x9f, 0x4, 0x3, 0x52, 0x80, 0x7f,
0xc7, 0xa0, 0x13, 0x28, 0x7, 0xff, 0x4, 0xe4,
0x1, 0x0, 0x1f, 0xfc, 0x36, 0x14, 0x10, 0xf,
0xfe, 0x23, 0x60, 0x7, 0xff, 0x17, 0x8, 0x3,
0xff, 0x8a, 0xe2, 0x1, 0xff, 0xc5, 0x11, 0x0,
0x7f, 0xf1, 0x44, 0x80, 0x3f, 0xf8, 0xaf, 0x80,
0x1f, 0xfc, 0x5d, 0x41, 0x0, 0xff, 0xe2, 0x38,
0x40, 0x7, 0xff, 0xd, 0x84, 0x19, 0x0, 0x3f,
0xf8, 0x25, 0x20, 0x15, 0xa0, 0x7, 0xfc, 0x5e,
0x1, 0xd6, 0xe0, 0x1f, 0xcb, 0xa4, 0x1, 0xe8,
0xe8, 0x31, 0x1, 0x37, 0xda, 0x0, 0xfe, 0x17,
0xce, 0xfe, 0xc8, 0x20, 0xf, 0xff, 0xf8, 0x7,
0xff, 0xfc, 0x3, 0xff, 0xfe, 0x1, 0xff, 0xfb,
0x59, 0xbc, 0xba, 0x61, 0x0, 0xfe, 0x6e, 0xa6,
0x43, 0x45, 0x9e, 0x80, 0xf, 0xaa, 0x44, 0x3,
0xf3, 0xe0, 0x80, 0x6a, 0x50, 0xf, 0xf8, 0xf4,
0x2, 0x65, 0x0, 0xff, 0xe0, 0x9c, 0x80, 0x20,
0x3, 0xff, 0x86, 0xc2, 0x82, 0x1, 0xff, 0xc4,
0x6c, 0x0, 0xff, 0xe2, 0xe1, 0x0, 0x7f, 0xf1,
0x5c, 0x40, 0x3f, 0xf8, 0xa2, 0x20, 0xf, 0xfe,
0x28, 0x90, 0x7, 0xff, 0x15, 0xf0, 0x3, 0xff,
0x8b, 0xa8, 0x20, 0x1f, 0xfc, 0x47, 0x8, 0x0,
0xff, 0xe1, 0xb0, 0x83, 0x20, 0x7, 0xff, 0x4,
0xa4, 0x2, 0xb4, 0x0, 0xff, 0x8b, 0xc0, 0x3a,
0xdc, 0x3, 0xf9, 0x74, 0x80, 0x3d, 0x1d, 0x6,
0x20, 0x26, 0xfb, 0x40, 0x18
};
/*---------------------
* GLYPH DESCRIPTION
*--------------------*/
static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = {
{.bitmap_index = 0, .adv_w = 0, .box_w = 0, .box_h = 0, .ofs_x = 0, .ofs_y = 0} /* id = 0 reserved */,
{.bitmap_index = 0, .adv_w = 435, .box_w = 0, .box_h = 0, .ofs_x = 0, .ofs_y = 0},
{.bitmap_index = 0, .adv_w = 1043, .box_w = 59, .box_h = 71, .ofs_x = 3, .ofs_y = -1},
{.bitmap_index = 801, .adv_w = 602, .box_w = 30, .box_h = 69, .ofs_x = 0, .ofs_y = -1},
{.bitmap_index = 901, .adv_w = 906, .box_w = 54, .box_h = 70, .ofs_x = 0, .ofs_y = 0},
{.bitmap_index = 1472, .adv_w = 909, .box_w = 55, .box_h = 69, .ofs_x = -1, .ofs_y = -1},
{.bitmap_index = 2007, .adv_w = 1058, .box_w = 64, .box_h = 69, .ofs_x = 2, .ofs_y = -1},
{.bitmap_index = 2466, .adv_w = 914, .box_w = 55, .box_h = 69, .ofs_x = 0, .ofs_y = -1},
{.bitmap_index = 3031, .adv_w = 978, .box_w = 57, .box_h = 70, .ofs_x = 3, .ofs_y = -1},
{.bitmap_index = 3813, .adv_w = 952, .box_w = 55, .box_h = 68, .ofs_x = 2, .ofs_y = 0},
{.bitmap_index = 4286, .adv_w = 1014, .box_w = 57, .box_h = 71, .ofs_x = 3, .ofs_y = -1},
{.bitmap_index = 5114, .adv_w = 978, .box_w = 57, .box_h = 70, .ofs_x = 1, .ofs_y = -1},
{.bitmap_index = 5892, .adv_w = 402, .box_w = 19, .box_h = 54, .ofs_x = 3, .ofs_y = -1}
};
/*---------------------
* CHARACTER MAPPING
*--------------------*/
/*Collect the unicode lists and glyph_id offsets*/
static const lv_font_fmt_txt_cmap_t cmaps[] =
{
{
.range_start = 32, .range_length = 1, .glyph_id_start = 1,
.unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY
},
{
.range_start = 48, .range_length = 11, .glyph_id_start = 2,
.unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY
}
};
/*-----------------
* KERNING
*----------------*/
/*Pair left and right glyphs for kerning*/
static const uint8_t kern_pair_glyph_ids[] =
{
2, 3,
2, 4,
2, 5,
2, 9,
4, 6,
5, 4,
5, 5,
5, 7,
5, 9,
6, 3,
6, 5,
6, 9,
6, 10,
6, 11,
6, 12,
7, 4,
7, 5,
7, 7,
7, 9,
8, 9,
9, 2,
9, 3,
9, 5,
9, 6,
9, 7,
9, 8,
9, 10,
9, 12,
11, 3,
11, 4,
11, 5,
11, 9,
12, 9
};
/* Kerning between the respective left and right glyphs
* 4.4 format which needs to scaled with `kern_scale`*/
static const int8_t kern_pair_values[] =
{
-6, -6, -15, -6, -15, -8, -8, -15,
-14, -38, -11, -65, 15, -6, 12, -8,
-8, -8, -18, -6, -31, 15, -11, -100,
-26, -31, -26, -31, -6, -6, -15, -6,
6
};
/*Collect the kern pair's data in one place*/
static const lv_font_fmt_txt_kern_pair_t kern_pairs =
{
.glyph_ids = kern_pair_glyph_ids,
.values = kern_pair_values,
.pair_cnt = 33,
.glyph_ids_size = 0
};
/*--------------------
* ALL CUSTOM DATA
*--------------------*/
#if LVGL_VERSION_MAJOR == 8
/*Store all the custom data of the font*/
static lv_font_fmt_txt_glyph_cache_t cache;
#endif
#if LVGL_VERSION_MAJOR >= 8
static const lv_font_fmt_txt_dsc_t font_dsc = {
#else
static lv_font_fmt_txt_dsc_t font_dsc = {
#endif
.glyph_bitmap = glyph_bitmap,
.glyph_dsc = glyph_dsc,
.cmaps = cmaps,
.kern_dsc = &kern_pairs,
.kern_scale = 16,
.cmap_num = 2,
.bpp = 4,
.kern_classes = 0,
.bitmap_format = 1,
#if LVGL_VERSION_MAJOR == 8
.cache = &cache
#endif
};
/*-----------------
* PUBLIC FONT
*----------------*/
/*Initialize a public general font descriptor*/
#if LVGL_VERSION_MAJOR >= 8
const lv_font_t montserrat_bold_96 = {
#else
lv_font_t montserrat_bold_96 = {
#endif
.get_glyph_dsc = lv_font_get_glyph_dsc_fmt_txt, /*Function pointer to get glyph's data*/
.get_glyph_bitmap = lv_font_get_bitmap_fmt_txt, /*Function pointer to get glyph's bitmap*/
.line_height = 71, /*The maximum line height required by the font*/
.base_line = 1, /*Baseline measured from the bottom of the line*/
#if !(LVGL_VERSION_MAJOR == 6 && LVGL_VERSION_MINOR == 0)
.subpx = LV_FONT_SUBPX_NONE,
#endif
#if LV_VERSION_CHECK(7, 4, 0) || LVGL_VERSION_MAJOR >= 8
.underline_position = -10,
.underline_thickness = 5,
#endif
.dsc = &font_dsc, /*The custom font data. Will be accessed by `get_glyph_bitmap/dsc` */
#if LV_VERSION_CHECK(8, 2, 0) || LVGL_VERSION_MAJOR >= 9
.fallback = NULL,
#endif
.user_data = NULL,
};
#endif /*#if MONTSERRAT_BOLD_96*/

749
Gui.h Normal file
View file

@ -0,0 +1,749 @@
// R-Watch GUI — LVGL integration for T-Watch Ultimate
// Tileview navigation with watch face, radio status, GPS, messages, settings
// Requires: CO5300.h (display), XL9555.h (power control), DRV2605.h (haptic)
#ifndef GUI_H
#define GUI_H
#if BOARD_MODEL == BOARD_TWATCH_ULT
#include <lvgl.h>
// Custom font: 96px Montserrat Bold for time display (digits + colon only)
#include "Fonts/montserrat_bold_96.c"
// ---------------------------------------------------------------------------
// Color palette (24-bit hex for lv_color_hex)
// ---------------------------------------------------------------------------
#define GUI_COL_BLACK 0x000000
#define GUI_COL_WHITE 0xFFFFEF // Bone white
#define GUI_COL_DIM 0x404040 // Dividers, inactive labels
#define GUI_COL_MID 0x808080 // Secondary text
#define GUI_COL_AMBER 0xFFA500 // LoRa / radio accent
#define GUI_COL_TEAL 0x00FFC0 // GPS accent
#define GUI_COL_BLUE 0x4080FF // BLE accent
#define GUI_COL_RED 0xFF0000 // Warnings
#define GUI_COL_GREEN 0x00FF00 // Confirmations
// ---------------------------------------------------------------------------
// Layout constants (410x502 display)
// ---------------------------------------------------------------------------
#define GUI_W CO5300_WIDTH
#define GUI_H CO5300_HEIGHT
#define GUI_PAD 24
// Watch face zones (adjusted for 96px time font)
#define GUI_STATUS_Y 10
#define GUI_TIME_Y 40
#define GUI_DATE_Y 150
#define GUI_RULE1_Y 190
#define GUI_COMP_Y 200
#define GUI_COMP_H 90
#define GUI_RULE2_Y 295
// ---------------------------------------------------------------------------
// LVGL core objects
// ---------------------------------------------------------------------------
static lv_display_t *gui_display = NULL;
static lv_indev_t *gui_indev = NULL;
#define GUI_BUF_LINES 120 // Must be >= tallest glyph (96px time font)
static uint8_t *gui_buf1 = NULL;
static uint8_t *gui_buf2 = NULL;
// Tileview and tiles
static lv_obj_t *gui_tileview = NULL;
static lv_obj_t *gui_tile_watch = NULL;
static lv_obj_t *gui_tile_radio = NULL;
static lv_obj_t *gui_tile_gps = NULL;
static lv_obj_t *gui_tile_msg = NULL;
static lv_obj_t *gui_tile_set = NULL;
// Convenience alias for display_unblank invalidation
static lv_obj_t *gui_screen = NULL;
// Watch face widgets
static lv_obj_t *gui_mode_label = NULL;
static lv_obj_t *gui_batt_label = NULL;
static lv_obj_t *gui_time_label = NULL;
static lv_obj_t *gui_date_label = NULL;
static lv_obj_t *gui_lora_value = NULL;
static lv_obj_t *gui_lora_label = NULL;
static lv_obj_t *gui_gps_value = NULL;
static lv_obj_t *gui_gps_label = NULL;
static lv_obj_t *gui_steps_value = NULL;
static lv_obj_t *gui_steps_label = NULL;
// Radio status widgets
static lv_obj_t *gui_radio_freq = NULL;
static lv_obj_t *gui_radio_params = NULL;
static lv_obj_t *gui_radio_rssi_bar = NULL;
static lv_obj_t *gui_radio_rssi_lbl = NULL;
static lv_obj_t *gui_radio_util = NULL;
static lv_obj_t *gui_radio_ble = NULL;
static lv_obj_t *gui_radio_pkts = NULL;
// GPS screen widgets
static lv_obj_t *gui_gps_coords = NULL;
static lv_obj_t *gui_gps_fix = NULL;
static lv_obj_t *gui_gps_alt = NULL;
static lv_obj_t *gui_gps_beacon = NULL;
// Touch input via function pointer (set by .ino after touch init)
typedef bool (*gui_touch_fn_t)(int16_t *x, int16_t *y);
static gui_touch_fn_t gui_touch_fn = NULL;
// Data update throttle
static uint32_t gui_last_data_update = 0;
#define GUI_DATA_UPDATE_MS 500
// Track current tile for haptic feedback
static uint8_t gui_last_tile_col = 1;
static uint8_t gui_last_tile_row = 1;
// ---------------------------------------------------------------------------
// LVGL display flush callback
// ---------------------------------------------------------------------------
// Swap buffer for byte-order conversion (avoids corrupting LVGL's draw buffer)
static uint16_t *gui_swap_buf = NULL;
#define GUI_SWAP_BUF_PX (GUI_W * GUI_BUF_LINES)
// Shadow framebuffer for screenshots (native RGB565 LE, readable via JTAG)
// Use: openocd -c "dump_image /tmp/screen.bin [gui_screenshot_buf address] 411640"
// Then convert with scripts/screenshot.py
uint16_t *gui_screenshot_buf = NULL; // non-static: visible to JTAG/nm for screenshots
static void gui_flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map) {
uint16_t x1 = area->x1;
uint16_t y1 = area->y1;
uint16_t w = area->x2 - area->x1 + 1;
uint16_t h = area->y2 - area->y1 + 1;
uint16_t *src = (uint16_t *)px_map;
uint32_t count = (uint32_t)w * h;
// Copy pre-swap pixels to shadow framebuffer for JTAG screenshot
if (gui_screenshot_buf) {
for (uint16_t row = 0; row < h; row++) {
memcpy(&gui_screenshot_buf[(y1 + row) * GUI_W + x1],
&src[row * w], w * sizeof(uint16_t));
}
}
// Byte-swap RGB565 into separate buffer for CO5300 SPI (big-endian)
if (gui_swap_buf && count <= GUI_SWAP_BUF_PX) {
for (uint32_t i = 0; i < count; i++) {
gui_swap_buf[i] = (src[i] >> 8) | (src[i] << 8);
}
co5300_push_pixels(x1, y1, w, h, gui_swap_buf);
} else {
for (uint32_t i = 0; i < count; i++) {
src[i] = (src[i] >> 8) | (src[i] << 8);
}
co5300_push_pixels(x1, y1, w, h, src);
}
lv_display_flush_ready(disp);
}
// ---------------------------------------------------------------------------
// LVGL touch input read callback
// ---------------------------------------------------------------------------
static void gui_touch_read_cb(lv_indev_t *indev, lv_indev_data_t *data) {
int16_t tx, ty;
if (gui_touch_fn && gui_touch_fn(&tx, &ty)) {
data->point.x = tx;
data->point.y = ty;
data->state = LV_INDEV_STATE_PRESSED;
last_unblank_event = millis();
} else {
data->state = LV_INDEV_STATE_RELEASED;
}
}
void gui_set_touch_handler(gui_touch_fn_t fn) {
gui_touch_fn = fn;
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
static void gui_style_black_container(lv_obj_t *obj) {
lv_obj_set_style_bg_color(obj, lv_color_hex(GUI_COL_BLACK), 0);
lv_obj_set_style_bg_opa(obj, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(obj, 0, 0);
lv_obj_set_style_pad_all(obj, 0, 0);
lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE);
}
static lv_obj_t *gui_create_rule(lv_obj_t *parent, lv_coord_t y) {
static lv_point_precise_t rule_pts[] = {
{GUI_PAD, 0}, {GUI_W - GUI_PAD, 0}
};
lv_obj_t *line = lv_line_create(parent);
lv_line_set_points(line, rule_pts, 2);
lv_obj_set_style_line_color(line, lv_color_hex(GUI_COL_DIM), 0);
lv_obj_set_style_line_width(line, 1, 0);
lv_obj_set_pos(line, 0, y);
return line;
}
static lv_obj_t *gui_label(lv_obj_t *parent, const lv_font_t *font,
uint32_t color, const char *text) {
lv_obj_t *lbl = lv_label_create(parent);
lv_obj_set_style_text_font(lbl, font, 0);
lv_obj_set_style_text_color(lbl, lv_color_hex(color), 0);
lv_label_set_text(lbl, text);
return lbl;
}
static lv_obj_t *gui_label_at(lv_obj_t *parent, const lv_font_t *font,
uint32_t color, const char *text,
lv_coord_t x, lv_coord_t y) {
lv_obj_t *lbl = gui_label(parent, font, color, text);
lv_obj_set_pos(lbl, x, y);
return lbl;
}
static void gui_create_complication(lv_obj_t *parent, lv_coord_t x, lv_coord_t w,
uint32_t color, const char *label_text,
lv_obj_t **value_out, lv_obj_t **label_out) {
lv_obj_t *cell = lv_obj_create(parent);
lv_obj_remove_style_all(cell);
lv_obj_set_size(cell, w, GUI_COMP_H);
lv_obj_set_pos(cell, x, 0);
lv_obj_t *val = gui_label(cell, &lv_font_montserrat_20, color, "--");
lv_obj_set_width(val, w);
lv_obj_set_style_text_align(val, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_align(val, LV_ALIGN_TOP_MID, 0, 4);
lv_obj_t *lbl = gui_label(cell, &lv_font_montserrat_14, GUI_COL_DIM, label_text);
lv_obj_set_width(lbl, w);
lv_obj_set_style_text_align(lbl, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_align(lbl, LV_ALIGN_TOP_MID, 0, 46);
if (value_out) *value_out = val;
if (label_out) *label_out = lbl;
}
// ---------------------------------------------------------------------------
// Screen: Watch Face (center tile)
// ---------------------------------------------------------------------------
static void gui_create_watchface(lv_obj_t *parent) {
gui_style_black_container(parent);
// Status bar
gui_mode_label = gui_label_at(parent, &lv_font_montserrat_14, GUI_COL_MID, "IDLE", GUI_PAD, GUI_STATUS_Y);
gui_batt_label = gui_label(parent, &lv_font_montserrat_14, GUI_COL_MID, "--%");
lv_obj_align(gui_batt_label, LV_ALIGN_TOP_RIGHT, -GUI_PAD, GUI_STATUS_Y);
// Time (96px custom font — digits and colon only)
gui_time_label = gui_label(parent, &lv_font_montserrat_48, GUI_COL_WHITE, "00:00");
// TODO: custom 96px font not rendering — using built-in 48 until debugged
lv_obj_set_style_text_letter_space(gui_time_label, 2, 0);
lv_obj_set_width(gui_time_label, GUI_W);
lv_obj_set_style_text_align(gui_time_label, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_set_pos(gui_time_label, 0, GUI_TIME_Y);
// Date
gui_date_label = gui_label(parent, &lv_font_montserrat_20, GUI_COL_MID, "--- -- ---");
lv_obj_set_width(gui_date_label, GUI_W);
lv_obj_set_style_text_align(gui_date_label, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_set_pos(gui_date_label, 0, GUI_DATE_Y);
// Rule 1
gui_create_rule(parent, GUI_RULE1_Y);
// Complications
lv_obj_t *comp = lv_obj_create(parent);
lv_obj_remove_style_all(comp);
lv_obj_set_size(comp, GUI_W, GUI_COMP_H);
lv_obj_set_pos(comp, 0, GUI_COMP_Y);
lv_obj_clear_flag(comp, LV_OBJ_FLAG_SCROLLABLE);
int cw = (GUI_W - GUI_PAD * 2) / 3;
gui_create_complication(comp, GUI_PAD, cw, GUI_COL_AMBER, "LoRa", &gui_lora_value, &gui_lora_label);
gui_create_complication(comp, GUI_PAD + cw, cw, GUI_COL_TEAL, "GPS", &gui_gps_value, &gui_gps_label);
gui_create_complication(comp, GUI_PAD + cw * 2, cw, GUI_COL_WHITE, "Steps", &gui_steps_value, &gui_steps_label);
// Rule 2
gui_create_rule(parent, GUI_RULE2_Y);
}
// ---------------------------------------------------------------------------
// Screen: Radio Status (top tile — swipe down from watch face)
// ---------------------------------------------------------------------------
static void gui_create_radio_screen(lv_obj_t *parent) {
gui_style_black_container(parent);
gui_label_at(parent, &lv_font_montserrat_14, GUI_COL_DIM, "RADIO STATUS", GUI_PAD, 12);
// Frequency
gui_radio_freq = gui_label_at(parent, &lv_font_montserrat_20, GUI_COL_AMBER, "--- MHz", GUI_PAD, 40);
// LoRa parameters
gui_radio_params = gui_label_at(parent, &lv_font_montserrat_20, GUI_COL_MID, "SF- BW- CR-", GUI_PAD, 80);
// RSSI
gui_create_rule(parent, 115);
gui_label_at(parent, &lv_font_montserrat_14, GUI_COL_DIM, "RSSI", GUI_PAD, 125);
gui_radio_rssi_lbl = gui_label(parent, &lv_font_montserrat_20, GUI_COL_AMBER, "---");
lv_obj_align(gui_radio_rssi_lbl, LV_ALIGN_TOP_RIGHT, -GUI_PAD, 122);
lv_obj_t *bar = lv_bar_create(parent);
lv_obj_set_size(bar, GUI_W - GUI_PAD * 2, 20);
lv_obj_set_pos(bar, GUI_PAD, 150);
lv_bar_set_range(bar, -140, -40);
lv_bar_set_value(bar, -140, LV_ANIM_OFF);
lv_obj_set_style_bg_color(bar, lv_color_hex(GUI_COL_DIM), 0);
lv_obj_set_style_bg_color(bar, lv_color_hex(GUI_COL_AMBER), LV_PART_INDICATOR);
lv_obj_set_style_bg_opa(bar, LV_OPA_COVER, 0);
lv_obj_set_style_bg_opa(bar, LV_OPA_COVER, LV_PART_INDICATOR);
gui_radio_rssi_bar = bar;
// Channel utilization
gui_create_rule(parent, 185);
gui_label_at(parent, &lv_font_montserrat_14, GUI_COL_DIM, "CHANNEL", GUI_PAD, 198);
gui_radio_util = gui_label(parent, &lv_font_montserrat_20, GUI_COL_MID, "-- %");
lv_obj_align(gui_radio_util, LV_ALIGN_TOP_RIGHT, -GUI_PAD, 195);
// BLE status
gui_create_rule(parent, 230);
gui_label_at(parent, &lv_font_montserrat_14, GUI_COL_DIM, "BLE", GUI_PAD, 243);
gui_radio_ble = gui_label(parent, &lv_font_montserrat_20, GUI_COL_BLUE, "---");
lv_obj_align(gui_radio_ble, LV_ALIGN_TOP_RIGHT, -GUI_PAD, 240);
// Packet counts
gui_create_rule(parent, 275);
gui_radio_pkts = gui_label_at(parent, &lv_font_montserrat_20, GUI_COL_MID,
"RX: 0 TX: 0", GUI_PAD, 290);
}
// ---------------------------------------------------------------------------
// Screen: GPS (right tile — swipe right from watch face)
// ---------------------------------------------------------------------------
static void gui_create_gps_screen(lv_obj_t *parent) {
gui_style_black_container(parent);
gui_label_at(parent, &lv_font_montserrat_14, GUI_COL_DIM, "GPS", GUI_PAD, 12);
// Coordinates
gui_gps_coords = gui_label_at(parent, &lv_font_montserrat_20, GUI_COL_TEAL,
"-- --", GUI_PAD, 40);
// Fix quality
gui_create_rule(parent, 110);
gui_gps_fix = gui_label_at(parent, &lv_font_montserrat_20, GUI_COL_MID,
"Sats: -- HDOP: --", GUI_PAD, 125);
// Altitude and speed
gui_create_rule(parent, 160);
gui_gps_alt = gui_label_at(parent, &lv_font_montserrat_20, GUI_COL_MID,
"Alt: -- Spd: --", GUI_PAD, 175);
// Beacon status
gui_create_rule(parent, 215);
gui_gps_beacon = gui_label_at(parent, &lv_font_montserrat_20, GUI_COL_AMBER,
"Beacon: --", GUI_PAD, 230);
}
// ---------------------------------------------------------------------------
// Screen: Messages (left tile — swipe left from watch face)
// ---------------------------------------------------------------------------
static void gui_create_msg_screen(lv_obj_t *parent) {
gui_style_black_container(parent);
gui_label_at(parent, &lv_font_montserrat_14, GUI_COL_DIM, "MESSAGES", GUI_PAD, 12);
lv_obj_t *lbl = gui_label(parent, &lv_font_montserrat_20, GUI_COL_DIM, "No messages");
lv_obj_align(lbl, LV_ALIGN_CENTER, 0, 0);
}
// ---------------------------------------------------------------------------
// Screen: Settings (bottom tile — swipe up from watch face)
// ---------------------------------------------------------------------------
static void gui_create_settings_screen(lv_obj_t *parent) {
gui_style_black_container(parent);
gui_label_at(parent, &lv_font_montserrat_14, GUI_COL_DIM, "SETTINGS", GUI_PAD, 12);
lv_obj_t *lbl = gui_label(parent, &lv_font_montserrat_20, GUI_COL_MID, "Coming soon");
lv_obj_align(lbl, LV_ALIGN_CENTER, 0, 0);
}
// ---------------------------------------------------------------------------
// Tileview change event — haptic feedback
// ---------------------------------------------------------------------------
static void gui_tile_change_cb(lv_event_t *e) {
lv_obj_t *tv = (lv_obj_t *)lv_event_get_target(e);
lv_obj_t *tile = (lv_obj_t *)lv_tileview_get_tile_active(tv);
if (!tile) return;
// Get tile position
lv_coord_t col = lv_obj_get_x(tile) / GUI_W;
lv_coord_t row = lv_obj_get_y(tile) / GUI_H;
if (col != gui_last_tile_col || row != gui_last_tile_row) {
gui_last_tile_col = col;
gui_last_tile_row = row;
#if defined(DRV2605_H)
if (drv2605_ready) drv2605_play(HAPTIC_TRANSITION);
#endif
}
}
// ---------------------------------------------------------------------------
// Update all screen data from firmware globals
// ---------------------------------------------------------------------------
static const char *gui_month_names[] = {"JAN", "FEB", "MAR", "APR", "MAY", "JUN",
"JUL", "AUG", "SEP", "OCT", "NOV", "DEC"};
static void gui_update_data() {
if (!gui_time_label) return;
uint32_t now = millis();
if (now - gui_last_data_update < GUI_DATA_UPDATE_MS) return;
gui_last_data_update = now;
// ---- Watch face ----
lv_label_set_text_fmt(gui_time_label, "%02d:%02d", rtc_hour, rtc_minute);
#if HAS_RTC == true
if (rtc_year > 0) {
const char *mon = (rtc_month >= 1 && rtc_month <= 12) ? gui_month_names[rtc_month - 1] : "---";
lv_label_set_text_fmt(gui_date_label, "%d %s %d", rtc_day, mon, rtc_year);
}
#endif
// Mode
if (bt_state == BT_STATE_CONNECTED) {
lv_label_set_text(gui_mode_label, "MODEM");
lv_obj_set_style_text_color(gui_mode_label, lv_color_hex(GUI_COL_BLUE), 0);
}
#if HAS_GPS == true
else if (beacon_mode_active) {
lv_label_set_text(gui_mode_label, "BEACON");
lv_obj_set_style_text_color(gui_mode_label, lv_color_hex(GUI_COL_AMBER), 0);
}
#endif
else if (radio_online) {
lv_label_set_text(gui_mode_label, "RADIO");
lv_obj_set_style_text_color(gui_mode_label, lv_color_hex(GUI_COL_MID), 0);
} else {
lv_label_set_text(gui_mode_label, "IDLE");
lv_obj_set_style_text_color(gui_mode_label, lv_color_hex(GUI_COL_DIM), 0);
}
// Battery
if (battery_state == BATTERY_STATE_CHARGING) {
lv_label_set_text_fmt(gui_batt_label, "%d%% +", (int)battery_percent);
} else {
lv_label_set_text_fmt(gui_batt_label, "%d%%", (int)battery_percent);
}
lv_obj_align(gui_batt_label, LV_ALIGN_TOP_RIGHT, -GUI_PAD, GUI_STATUS_Y);
// LoRa complication
if (radio_online) {
if (last_rssi > -292) {
lv_label_set_text_fmt(gui_lora_value, "%d", last_rssi);
} else {
lv_label_set_text(gui_lora_value, "---");
}
lv_obj_set_style_text_color(gui_lora_value, lv_color_hex(GUI_COL_AMBER), 0);
} else {
lv_label_set_text(gui_lora_value, "OFF");
lv_obj_set_style_text_color(gui_lora_value, lv_color_hex(GUI_COL_DIM), 0);
}
// GPS complication
#if HAS_GPS == true
if (gps_sats > 0) {
lv_label_set_text_fmt(gui_gps_value, "%d sats", gps_sats);
lv_obj_set_style_text_color(gui_gps_value, lv_color_hex(GUI_COL_TEAL), 0);
} else {
lv_label_set_text(gui_gps_value, "no fix");
lv_obj_set_style_text_color(gui_gps_value, lv_color_hex(GUI_COL_DIM), 0);
}
#endif
// Steps placeholder
lv_label_set_text(gui_steps_value, "--");
// ---- Radio status screen ----
if (gui_radio_freq) {
if (lora_freq > 0) {
lv_label_set_text_fmt(gui_radio_freq, "%.3f MHz", (float)lora_freq / 1000000.0);
} else {
lv_label_set_text(gui_radio_freq, "--- MHz");
}
lv_label_set_text_fmt(gui_radio_params, "SF%d BW%lu CR4/%d",
lora_sf, lora_bw / 1000, lora_cr);
if (radio_online && last_rssi > -292) {
lv_bar_set_value(gui_radio_rssi_bar, last_rssi, LV_ANIM_ON);
lv_label_set_text_fmt(gui_radio_rssi_lbl, "%d dBm", last_rssi);
} else {
lv_bar_set_value(gui_radio_rssi_bar, -140, LV_ANIM_OFF);
lv_label_set_text(gui_radio_rssi_lbl, "---");
}
lv_label_set_text_fmt(gui_radio_util, "%.1f%%",
(float)local_channel_util / 100.0);
if (bt_state == BT_STATE_CONNECTED) {
lv_label_set_text(gui_radio_ble, "Connected");
lv_obj_set_style_text_color(gui_radio_ble, lv_color_hex(GUI_COL_BLUE), 0);
} else if (bt_state == BT_STATE_ON) {
lv_label_set_text(gui_radio_ble, "Advertising");
lv_obj_set_style_text_color(gui_radio_ble, lv_color_hex(GUI_COL_MID), 0);
} else {
lv_label_set_text(gui_radio_ble, "Off");
lv_obj_set_style_text_color(gui_radio_ble, lv_color_hex(GUI_COL_DIM), 0);
}
lv_label_set_text_fmt(gui_radio_pkts, "RX: %lu TX: %lu", stat_rx, stat_tx);
}
// ---- GPS screen ----
#if HAS_GPS == true
if (gui_gps_coords) {
if (gps_sats > 0 && gps_lat != 0.0) {
lv_label_set_text_fmt(gui_gps_coords, "%.6f\n%.6f", gps_lat, gps_lon);
lv_obj_set_style_text_color(gui_gps_coords, lv_color_hex(GUI_COL_TEAL), 0);
} else {
lv_label_set_text(gui_gps_coords, "No fix");
lv_obj_set_style_text_color(gui_gps_coords, lv_color_hex(GUI_COL_DIM), 0);
}
lv_label_set_text_fmt(gui_gps_fix, "Sats: %d HDOP: %.1f", gps_sats, gps_hdop);
lv_label_set_text_fmt(gui_gps_alt, "Alt: %.0fm Spd: %.1fkm/h", gps_alt, gps_speed);
if (beacon_mode_active) {
lv_label_set_text(gui_gps_beacon, "Beacon: active");
lv_obj_set_style_text_color(gui_gps_beacon, lv_color_hex(GUI_COL_AMBER), 0);
} else {
lv_label_set_text(gui_gps_beacon, "Beacon: off");
lv_obj_set_style_text_color(gui_gps_beacon, lv_color_hex(GUI_COL_DIM), 0);
}
}
#endif
}
// ---------------------------------------------------------------------------
// Initialize LVGL and create all screens
// ---------------------------------------------------------------------------
bool gui_init() {
lv_init();
// --- Display driver ---
gui_display = lv_display_create(GUI_W, GUI_H);
if (!gui_display) return false;
lv_display_set_flush_cb(gui_display, gui_flush_cb);
uint32_t buf_size = GUI_W * GUI_BUF_LINES * sizeof(uint16_t);
gui_buf1 = (uint8_t *)heap_caps_malloc(buf_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
gui_buf2 = (uint8_t *)heap_caps_malloc(buf_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (!gui_buf1 || !gui_buf2) {
if (gui_buf1) free(gui_buf1);
if (gui_buf2) free(gui_buf2);
gui_buf2 = NULL;
gui_buf1 = (uint8_t *)malloc(buf_size);
if (!gui_buf1) return false;
}
lv_display_set_buffers(gui_display, gui_buf1, gui_buf2, buf_size,
LV_DISPLAY_RENDER_MODE_PARTIAL);
// Swap buffer for byte-order conversion (same size as draw buffer)
gui_swap_buf = (uint16_t *)heap_caps_malloc(GUI_W * GUI_BUF_LINES * sizeof(uint16_t),
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (!gui_swap_buf) gui_swap_buf = (uint16_t *)malloc(GUI_W * GUI_BUF_LINES * sizeof(uint16_t));
// Shadow framebuffer for JTAG screenshots (410*502*2 = 411,640 bytes)
gui_screenshot_buf = (uint16_t *)heap_caps_malloc(GUI_W * GUI_H * sizeof(uint16_t),
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (gui_screenshot_buf) {
memset(gui_screenshot_buf, 0, GUI_W * GUI_H * sizeof(uint16_t));
Serial.printf("[gui] screenshot buf @ %p (%u bytes)\n",
gui_screenshot_buf, GUI_W * GUI_H * 2);
}
// --- Input driver ---
gui_indev = lv_indev_create();
if (gui_indev) {
lv_indev_set_type(gui_indev, LV_INDEV_TYPE_POINTER);
lv_indev_set_read_cb(gui_indev, gui_touch_read_cb);
}
// --- Screen setup ---
gui_screen = lv_screen_active();
gui_style_black_container(gui_screen);
// --- Tileview: 3x3 grid, 5 populated tiles ---
// (1,0) Radio
// (0,1) GPS (1,1) Watch (2,1) Messages
// (1,2) Settings
gui_tileview = lv_tileview_create(gui_screen);
lv_obj_set_style_bg_color(gui_tileview, lv_color_hex(GUI_COL_BLACK), 0);
lv_obj_set_style_bg_opa(gui_tileview, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(gui_tileview, 0, 0);
lv_obj_set_style_pad_all(gui_tileview, 0, 0);
// Note: do NOT clear LV_OBJ_FLAG_SCROLLABLE — tileview needs it for swipe
lv_obj_set_size(gui_tileview, GUI_W, GUI_H);
gui_tile_watch = lv_tileview_add_tile(gui_tileview, 1, 1, LV_DIR_ALL);
gui_tile_radio = lv_tileview_add_tile(gui_tileview, 1, 0, LV_DIR_BOTTOM);
gui_tile_gps = lv_tileview_add_tile(gui_tileview, 0, 1, LV_DIR_RIGHT);
gui_tile_msg = lv_tileview_add_tile(gui_tileview, 2, 1, LV_DIR_LEFT);
gui_tile_set = lv_tileview_add_tile(gui_tileview, 1, 2, LV_DIR_TOP);
// Start on watch face
lv_tileview_set_tile(gui_tileview, gui_tile_watch, LV_ANIM_OFF);
// Haptic feedback on tile change
lv_obj_add_event_cb(gui_tileview, gui_tile_change_cb, LV_EVENT_VALUE_CHANGED, NULL);
// --- Create screen content ---
gui_create_watchface(gui_tile_watch);
gui_create_radio_screen(gui_tile_radio);
gui_create_gps_screen(gui_tile_gps);
gui_create_msg_screen(gui_tile_msg);
gui_create_settings_screen(gui_tile_set);
return true;
}
// ---------------------------------------------------------------------------
// Screenshot: dump framebuffer as raw RGB565 to a file on SPIFFS,
// or output dimensions to serial for external tools.
// Call gui_screenshot() to write /screenshot.raw to SPIFFS (if mounted),
// or read gui_screenshot_buf directly via debugger.
// ---------------------------------------------------------------------------
// Serial screenshot protocol:
// Trigger: host sends 4 bytes [0x52, 0x57, 0x53, 0x53] ("RWSS" = R-Watch Screen Shot)
// Response: "RWSS" + uint16_t(width) + uint16_t(height) + raw RGB565 LE pixels
#define GUI_SS_MAGIC_0 0x52
#define GUI_SS_MAGIC_1 0x57
#define GUI_SS_MAGIC_2 0x53
#define GUI_SS_MAGIC_3 0x53
static uint8_t gui_ss_state = 0;
void gui_check_screenshot_trigger(uint8_t byte_in) {
const uint8_t magic[] = {GUI_SS_MAGIC_0, GUI_SS_MAGIC_1, GUI_SS_MAGIC_2, GUI_SS_MAGIC_3};
if (byte_in == magic[gui_ss_state]) {
gui_ss_state++;
if (gui_ss_state == 4) {
gui_ss_state = 0;
if (gui_screenshot_buf) {
Serial.write(magic, 4);
uint16_t w = GUI_W, h = GUI_H;
Serial.write((uint8_t *)&w, 2);
Serial.write((uint8_t *)&h, 2);
Serial.write((uint8_t *)gui_screenshot_buf, GUI_W * GUI_H * 2);
Serial.flush();
} else {
// Buffer not allocated — send error marker
Serial.write(magic, 4);
uint16_t w = 0, h = 0;
Serial.write((uint8_t *)&w, 2);
Serial.write((uint8_t *)&h, 2);
Serial.flush();
}
}
} else {
gui_ss_state = (byte_in == magic[0]) ? 1 : 0;
}
}
void gui_screenshot_info() {
if (gui_screenshot_buf) {
Serial.printf("[screenshot] addr=%p size=%u w=%d h=%d\n",
gui_screenshot_buf, GUI_W * GUI_H * 2, GUI_W, GUI_H);
} else {
Serial.println("[screenshot] buffer not allocated");
}
}
// Write screenshot to SD card as raw RGB565 + BMP header
#if HAS_SD
#include <SD.h>
bool gui_screenshot_sd(const char *path = "/screenshot.bmp") {
if (!gui_screenshot_buf) return false;
// Init SD on shared SPI bus
SPI.begin(SD_CLK, SD_MISO, SD_MOSI, SD_CS);
if (!SD.begin(SD_CS, SPI)) {
Serial.println("[screenshot] SD init failed");
return false;
}
File f = SD.open(path, FILE_WRITE);
if (!f) {
Serial.println("[screenshot] file open failed");
SD.end();
return false;
}
// Write BMP header (RGB565 LE, top-down)
uint32_t img_size = GUI_W * GUI_H * 2;
uint32_t file_size = 14 + 40 + 12 + img_size; // file + info + masks + pixels
uint32_t data_offset = 14 + 40 + 12;
// BMP file header (14 bytes)
uint8_t bmp_hdr[14] = {'B', 'M'};
memcpy(&bmp_hdr[2], &file_size, 4);
memset(&bmp_hdr[6], 0, 4); // reserved
memcpy(&bmp_hdr[10], &data_offset, 4);
f.write(bmp_hdr, 14);
// DIB header (BITMAPINFOHEADER, 40 bytes)
uint8_t dib[40] = {};
uint32_t dib_size = 40;
int32_t bmp_w = GUI_W;
int32_t bmp_h = -GUI_H; // negative = top-down
uint16_t planes = 1;
uint16_t bpp = 16;
uint32_t compression = 3; // BI_BITFIELDS
memcpy(&dib[0], &dib_size, 4);
memcpy(&dib[4], &bmp_w, 4);
memcpy(&dib[8], &bmp_h, 4);
memcpy(&dib[12], &planes, 2);
memcpy(&dib[14], &bpp, 2);
memcpy(&dib[16], &compression, 4);
memcpy(&dib[20], &img_size, 4);
f.write(dib, 40);
// RGB565 bitmasks (R, G, B)
uint32_t mask_r = 0xF800, mask_g = 0x07E0, mask_b = 0x001F;
f.write((uint8_t *)&mask_r, 4);
f.write((uint8_t *)&mask_g, 4);
f.write((uint8_t *)&mask_b, 4);
// Pixel data (RGB565 LE, row by row)
// BMP rows must be 4-byte aligned; 410*2=820 bytes per row, 820%4=0, no padding needed
f.write((uint8_t *)gui_screenshot_buf, img_size);
f.close();
SD.end();
// Restart LoRa SPI after SD use (shared bus)
SPI.end();
Serial.printf("[screenshot] saved %s (%u bytes)\n", path, file_size);
return true;
}
#endif
// ---------------------------------------------------------------------------
// Main GUI update — called from update_display()
// ---------------------------------------------------------------------------
void gui_update() {
static uint32_t last_tick = 0;
uint32_t now = millis();
lv_tick_inc(now - last_tick);
last_tick = now;
gui_update_data();
lv_timer_handler();
}
#endif // BOARD_MODEL == BOARD_TWATCH_ULT
#endif // GUI_H

View file

@ -55,6 +55,7 @@ prep-esp32:
arduino-cli lib install "XPowersLib"
arduino-cli lib install "Crypto"
arduino-cli lib install "TinyGPSPlus"
arduino-cli lib install "lvgl@9.5.0"
prep-samd:
arduino-cli core update-index --config-file arduino-cli.yaml
@ -117,11 +118,16 @@ deploy-tbeam_supreme: firmware-tbeam_supreme free-port
@sleep 3
rnodeconf $(PORT) --firmware-hash $$(./partition_hashes ./build/esp32.esp32.esp32s3/RNode_Firmware.ino.bin)
# T-Watch Ultra: 16MB flash, 8MB app partition, LVGL GUI
# FlashSize=16M is critical — the bootloader embeds flash size and defaults to 4MB.
# Without it, flash mapping fails silently beyond 0x400000 (black screen, no crash).
# partition_twatch.csv must be copied to the Arduino ESP32 tools/partitions/ directory.
# When changing partition scheme, flash ALL THREE binaries (bootloader + partition + app).
firmware-twatch_ultra:
arduino-cli compile --log --fqbn "esp32:esp32:esp32s3:CDCOnBoot=cdc" -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=-DBOARD_MODEL=0x45"
arduino-cli compile --log --fqbn "esp32:esp32:esp32s3:CDCOnBoot=cdc,FlashSize=16M,PSRAM=enabled" -e --build-property "build.partitions=partition_twatch" --build-property "upload.maximum_size=8388608" --build-property "compiler.cpp.extra_flags=-DBOARD_MODEL=0x45"
upload-twatch_ultra: firmware-twatch_ultra
@echo "Flashing T-Watch Ultra app via JTAG (no BOOT+RST needed)..."
@echo "Flashing T-Watch Ultra via JTAG..."
$(HOME)/.arduino15/packages/esp32/tools/openocd-esp32/v0.12.0-esp32-20230921/bin/openocd \
-s $(HOME)/.arduino15/packages/esp32/tools/openocd-esp32/v0.12.0-esp32-20230921/share/openocd/scripts \
-f board/esp32s3-builtin.cfg \
@ -129,6 +135,13 @@ upload-twatch_ultra: firmware-twatch_ultra
@sleep 5
rnodeconf /dev/ttyACM4 --firmware-hash $$(./partition_hashes ./build/esp32.esp32.esp32s3/RNode_Firmware.ino.bin)
# Full flash including bootloader and partition table (needed after partition changes)
flash-twatch_ultra-full: firmware-twatch_ultra
esptool.py --chip esp32s3 --port /dev/ttyACM4 --baud 921600 write_flash \
0x0000 build/esp32.esp32.esp32s3/RNode_Firmware.ino.bootloader.bin \
0x8000 build/esp32.esp32.esp32s3/RNode_Firmware.ino.partitions.bin \
0x10000 build/esp32.esp32.esp32s3/RNode_Firmware.ino.bin
firmware-lora32_v10: check_bt_buffers
arduino-cli compile --log --fqbn esp32:esp32:ttgo-lora32 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x39\""

View file

@ -280,6 +280,12 @@ void setup() {
display_add_callback(work_while_waiting);
#endif
// T-Watch init order: display_init() MUST run BEFORE xl9555_init().
// The XL9555 GPIO expander controls the display power gate (EXPANDS_DISP_EN),
// but its outputs default HIGH at power-on, so the display is powered before
// the expander is explicitly configured. Moving display_init() after xl9555_init()
// causes a black screen because the power gate cycling disrupts the CO5300 QSPI
// init sequence. Do not reorder.
display_unblank();
disp_ready = display_init();
update_display();
@ -295,7 +301,7 @@ void setup() {
xl9555_init();
xl9555_enable_lora_antenna();
xl9555_set(EXPANDS_DRV_EN, true); // Enable haptic motor driver
xl9555_set(EXPANDS_DISP_EN, true); // Enable display power gate
xl9555_set(EXPANDS_DISP_EN, true); // Confirm display power gate on
xl9555_set(EXPANDS_TOUCH_RST, true); // Release touch reset
delay(100);
drv2605_init();
@ -306,6 +312,14 @@ void setup() {
if (touch.begin(Wire, 0x1A, I2C_SDA, I2C_SCL)) {
touch_ready = true;
attachInterrupt(TP_INT, touch_isr, FALLING);
// Register touch with LVGL GUI
#if HAS_DISPLAY == true
gui_set_touch_handler([](int16_t *x, int16_t *y) -> bool {
if (!touch_ready) return false;
return touch.getPoint(x, y, 1) > 0;
});
#endif
}
// Init speaker (BLDO2 already enabled by PMU init) and microphone
@ -2021,19 +2035,33 @@ void loop() {
input_read();
#endif
// Touch panel event handling
// Touch panel — IRQ-driven display wake (LVGL handles touch input via polling)
#if BOARD_MODEL == BOARD_TWATCH_ULT
if (touch_ready && touch_irq) {
touch_irq = false;
int16_t tx, ty;
if (touch.getPoint(&tx, &ty, 1) > 0) {
// Touch detected — unblank display and update activity
#if HAS_DISPLAY
display_unblank();
#endif
last_unblank_event = millis();
#if HAS_DISPLAY
if (display_blanked) display_unblank();
#endif
}
// Screenshot: long-press BOOT button (GPIO 0) for 2 seconds
#if HAS_SD && HAS_DISPLAY
{
static uint32_t btn_down_since = 0;
static bool btn_screenshot_taken = false;
if (digitalRead(0) == LOW) {
if (btn_down_since == 0) btn_down_since = millis();
if (!btn_screenshot_taken && millis() - btn_down_since > 2000) {
btn_screenshot_taken = true;
if (drv2605_ready) drv2605_play(HAPTIC_DOUBLE_CLICK);
gui_screenshot_sd();
}
} else {
btn_down_since = 0;
btn_screenshot_taken = false;
}
}
#endif
#endif
// Deferred BHI260AP init — runs once after boot is complete
@ -2304,7 +2332,11 @@ void buffer_serial() {
#else
while (c < MAX_CYCLES && Serial.available()) {
c++;
if (!fifo_isfull(&channelFIFO[CHANNEL_USB])) { fifo_push(&channelFIFO[CHANNEL_USB], Serial.read()); }
uint8_t sb = Serial.read();
#if BOARD_MODEL == BOARD_TWATCH_ULT && HAS_DISPLAY
gui_check_screenshot_trigger(sb);
#endif
if (!fifo_isfull(&channelFIFO[CHANNEL_USB])) { fifo_push(&channelFIFO[CHANNEL_USB], sb); }
}
#endif

9
partition_twatch.csv Normal file
View file

@ -0,0 +1,9 @@
# T-Watch Ultimate partition table — 16MB flash, no OTA, large app
# Requires FlashSize=16M in the Arduino FQBN or the bootloader can't map beyond 4MB.
# Copy this file to ~/.arduino15/packages/esp32/hardware/esp32/<ver>/tools/partitions/
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x800000,
spiffs, data, spiffs, 0x810000, 0x7E0000,
coredump, data, coredump,0xFF0000, 0x10000,
1 # T-Watch Ultimate partition table — 16MB flash, no OTA, large app
2 # Requires FlashSize=16M in the Arduino FQBN or the bootloader can't map beyond 4MB.
3 # Copy this file to ~/.arduino15/packages/esp32/hardware/esp32/<ver>/tools/partitions/
4 # Name, Type, SubType, Offset, Size, Flags
5 nvs, data, nvs, 0x9000, 0x5000,
6 otadata, data, ota, 0xe000, 0x2000,
7 app0, app, ota_0, 0x10000, 0x800000,
8 spiffs, data, spiffs, 0x810000, 0x7E0000,
9 coredump, data, coredump,0xFF0000, 0x10000,

108
scripts/screenshot.py Executable file
View file

@ -0,0 +1,108 @@
#!/usr/bin/env python3
"""
R-Watch Screenshot capture display via USB serial
Sends trigger bytes, firmware dumps shadow framebuffer as raw RGB565.
Handles KISS protocol data interleaved in the stream.
Usage:
./scripts/screenshot.py # default port + output
./scripts/screenshot.py -p /dev/ttyACM4 # specify port
./scripts/screenshot.py -o /tmp/watch.png # specify output
"""
import argparse
import struct
import sys
import time
WIDTH = 410
HEIGHT = 502
PIXEL_BYTES = WIDTH * HEIGHT * 2 # 411,640 bytes
MAGIC = b"RWSS"
HEADER_SIZE = 8 # RWSS + uint16 width + uint16 height
def capture(port, output_path):
try:
import serial
except ImportError:
sys.exit("pip install pyserial")
try:
from PIL import Image
except ImportError:
sys.exit("pip install Pillow")
s = serial.Serial(port, 115200, timeout=1)
# Drain any pending data
time.sleep(0.2)
s.reset_input_buffer()
# Send trigger
s.write(MAGIC)
s.flush()
# Scan stream for the magic response header
# The firmware may send KISS frames before/after the screenshot data
buf = b""
deadline = time.time() + 10
magic_idx = -1
while time.time() < deadline:
chunk = s.read(max(1, s.in_waiting))
if not chunk:
continue
buf += chunk
magic_idx = buf.find(MAGIC)
if magic_idx >= 0 and len(buf) >= magic_idx + HEADER_SIZE:
break
if magic_idx < 0:
s.close()
sys.exit(f"Magic not found in {len(buf)} bytes of response")
# Parse header
hdr = buf[magic_idx:magic_idx + HEADER_SIZE]
w, h = struct.unpack("<HH", hdr[4:8])
expected = w * h * 2
# Collect pixel data (may already have some in buf)
data = buf[magic_idx + HEADER_SIZE:]
while len(data) < expected and time.time() < deadline:
chunk = s.read(min(expected - len(data), 32768))
if chunk:
data += chunk
s.close()
received = len(data)
if received < expected:
print(f"Warning: got {received}/{expected} bytes ({received*100//expected}%)")
# Convert RGB565 LE to PNG
img = Image.new("RGB", (w, h))
pixels = img.load()
npx = min(w * h, received // 2)
for i in range(npx):
pixel = struct.unpack_from("<H", data, i * 2)[0]
r = ((pixel >> 11) & 0x1F) * 255 // 31
g = ((pixel >> 5) & 0x3F) * 255 // 63
b = (pixel & 0x1F) * 255 // 31
pixels[i % w, i // w] = (r, g, b)
img.save(output_path)
print(f"Saved: {output_path} ({w}x{h}, {npx} pixels, {received} bytes)")
def main():
parser = argparse.ArgumentParser(description="R-Watch screenshot")
parser.add_argument("-p", "--port", default="/dev/ttyACM4")
parser.add_argument("-o", "--output", default="/tmp/watch_screenshot.png")
args = parser.parse_args()
capture(args.port, args.output)
if __name__ == "__main__":
main()