From 45998b6176fc261d7c9a1dd04267d5cfb771619e Mon Sep 17 00:00:00 2001 From: matt_ed Date: Tue, 4 Aug 2026 18:40:46 -0700 Subject: [PATCH] Updated icons, buttons, and display; issues still with Main Room button, and sliders. --- README.md | 11 ++ TODO.md | 24 +-- main/CMakeLists.txt | 1 + main/api/ha_config.h | 21 ++- main/api/ha_data.c | 5 +- main/api/ha_data.h | 2 +- main/api/ha_template.h | 4 +- main/main.c | 16 ++ main/ui/icon_font.c | 386 ++++++++++++++++++++++++++++++++++++++++ main/ui/ui_icons.h | 45 +++++ main/ui/ui_manager.c | 363 +++++++++++++++++++++++++++++++------ sdkconfig.defaults | 2 + sim/CMakeLists.txt | 1 + sim/lv_conf.h | 2 +- tools/make_icon_font.py | 167 +++++++++++++++++ 15 files changed, 973 insertions(+), 77 deletions(-) create mode 100644 main/ui/icon_font.c create mode 100644 main/ui/ui_icons.h create mode 100644 tools/make_icon_font.py diff --git a/README.md b/README.md index f9149e9..535a2be 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ forecast, and on/off plus brightness control for six lights. Thermostat control | `main/drivers/touch` | GT911 capacitive touch over I2C, registered as an LVGL input device | | `main/ui/lvgl_port.c` | LVGL tick source, service task, and locking | | `main/ui/ui_manager.c` | Screen layout and update entry points | +| `main/ui/icon_font.c` | Generated ~6KB MDI icon subset — see `tools/make_icon_font.py` | | `main/wifi` | WiFi station bring-up | | `main/api/ha_client.c` | Home Assistant REST transport (device only) | | `main/api/ha_data.c` | Response parsing and presentation helpers (**shared with the simulator**) | @@ -211,6 +212,16 @@ about PSRAM bandwidth or panel timing. - **`LV_LABEL_LONG_DOT` needs an explicit height** to truncate. With a content-sized height the label wraps to a second line first and only then adds dots — landing on top of whatever is below it. +- **Icons are label text**, not images: single glyphs from a subset MDI TTF + rasterised by tiny_ttf (`tools/make_icon_font.py` regenerates + `main/ui/icon_font.c` / `ui_icons.h`; needs network + fonttools, so the + generated files are committed). Codepoints are resolved from the MDI CSS at + generation time — never hardcode one, MDI reassigns them between majors. +- **Light tiles are optimistic.** A tap flips the tile immediately and holds + against poll data for up to 5s until HA confirms (`light_hold_t`); commands + are sent on finger-lift only, and poll updates never touch a pressed + slider. Breaking any of those three rules brings back the "button waits a + second, then the knob jumps out from under your finger" feel. - **`lv_label_set_text()` invalidates unconditionally**, even when the text is identical — so setting an unchanged value still forces a repaint. All label updates go through `label_set_if_changed()`. diff --git a/TODO.md b/TODO.md index 6f81a3b..a3f4306 100644 --- a/TODO.md +++ b/TODO.md @@ -2,18 +2,20 @@ ## Not yet verified on hardware -The Home Assistant rewrite, the new dashboard and the desktop simulator all -build clean, and the HA request/parse/render path was verified end to end -against the live instance **in the simulator**. None of it has run on the -panel yet. Specifically unconfirmed on device: +Flashed and booting clean (icons on, first poll ~8s, no errors), but eyes and +fingers haven't confirmed: -1. Touch actually toggling a light (the service calls were verified against HA - directly, and idempotently — `turn_off` on already-off entities). -2. Whether the shorter pause window (~85ms LAN HTTP, no TLS) makes the - left-edge artefacts rarer, as item 1 below predicts. -3. That the six light tiles are comfortable finger targets at 125px. This is - the thing most likely to need another pass, and the simulator's mouse - cannot answer it. +1. Touch actually toggling a light end-to-end. Tiles are now **optimistic**: + they flip on tap immediately and hold that state up to 5s until HA + confirms (see `light_hold_t` in ui_manager.c). If a tile flips then snaps + back 5s later, the command failed — check `ha_client` on serial. +2. Whether the full-screen invalidate after each poll actually bounds the + left-edge artefacts (it repaints them away; it cannot prevent them). If + lines still persist for longer than one poll interval, that mechanism is + not working and the next lever is pausing only around LVGL's flush. +3. That the six light tiles are comfortable finger targets at ~125px, and + that sliders track the finger without jumping (poll updates skip pressed + sliders by design). ## Done diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 43547ac..f0f791b 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -8,6 +8,7 @@ idf_component_register( "drivers/touch/touch_gt911.c" "ui/lvgl_port.c" "ui/ui_manager.c" + "ui/icon_font.c" "api/ambient_weather.c" "api/ha_client.c" "api/ha_data.c" diff --git a/main/api/ha_config.h b/main/api/ha_config.h index 334bb03..c492163 100644 --- a/main/api/ha_config.h +++ b/main/api/ha_config.h @@ -52,12 +52,17 @@ * template's entity list and the C table from one source makes that * impossible. * - * X(entity_id, label, dimmable) + * X(entity_id, label, dimmable, icon) * * `dimmable` is false for the patio, which is a TP-Link smart plug in the * switch domain -- on/off only, and it needs switch.turn_on rather than * light.turn_on. See ha_service_domain(). * + * `icon` is an ICON_* macro from main/ui/ui_icons.h. It rides along in this + * list (rather than a parallel array in the UI) so a reorder here can never + * put the sofa icon on the patio. Only ui_manager.c expands it; the api-layer + * consumers ignore the argument, so this header does not include ui_icons.h. + * * Note light.main_room is a GROUP containing blue_lamp, brown_lamp and * landing_chandelier. Toggling it changes three of the other tiles; that is * expected, and the UI reflects it on the next poll. @@ -65,12 +70,12 @@ /* Labels must fit one line of Montserrat 16 in a ~125px tile -- roughly 11 * characters. Longer ones are truncated with an ellipsis, which reads as a * mistake. "Dining Room" is one character too wide, hence "Dining". */ -#define HA_LIGHT_LIST(X) \ - X("light.main_room", "Main Room", true) \ - X("light.dining_room", "Dining", true) \ - X("light.blue_lamp", "Blue Lamp", true) \ - X("light.brown_lamp", "Brown Lamp", true) \ - X("light.landing_chandelier", "Chandelier", true) \ - X("switch.tp_link_smart_plug_f048_outdoor_hanging_lights", "Patio", false) +#define HA_LIGHT_LIST(X) \ + X("light.main_room", "Main Room", true, ICON_SOFA) \ + X("light.dining_room", "Dining", true, ICON_DINING) \ + X("light.blue_lamp", "Blue Lamp", true, ICON_FLOOR_LAMP) \ + X("light.brown_lamp", "Brown Lamp", true, ICON_DESK_LAMP) \ + X("light.landing_chandelier", "Chandelier", true, ICON_CHANDELIER) \ + X("switch.tp_link_smart_plug_f048_outdoor_hanging_lights", "Patio", false, ICON_STRING_LIGHTS) #endif diff --git a/main/api/ha_data.c b/main/api/ha_data.c index 3d6d4ea..d584cdc 100644 --- a/main/api/ha_data.c +++ b/main/api/ha_data.c @@ -16,8 +16,9 @@ #include "ha_data.h" -/* Build the config table from the same list the JSON template is built from. */ -#define HA_LIGHT_ENTRY_X(id, label, dimmable) { id, label, dimmable }, +/* Build the config table from the same list the JSON template is built from. + * The icon argument belongs to the UI layer and is ignored here. */ +#define HA_LIGHT_ENTRY_X(id, label, dimmable, icon) { id, label, dimmable }, const ha_light_cfg_t HA_LIGHTS[HA_LIGHT_COUNT] = { HA_LIGHT_LIST(HA_LIGHT_ENTRY_X) }; diff --git a/main/api/ha_data.h b/main/api/ha_data.h index 5c9c6ec..14678ab 100644 --- a/main/api/ha_data.h +++ b/main/api/ha_data.h @@ -21,7 +21,7 @@ #define HA_FORECAST_DAYS 5 /* Derived from HA_LIGHT_LIST so the two can never disagree. */ -#define HA_LIGHT_COUNT_X(a, b, c) +1 +#define HA_LIGHT_COUNT_X(a, b, c, d) +1 #define HA_LIGHT_COUNT (0 HA_LIGHT_LIST(HA_LIGHT_COUNT_X)) /* diff --git a/main/api/ha_template.h b/main/api/ha_template.h index 02e3bc4..caf65cd 100644 --- a/main/api/ha_template.h +++ b/main/api/ha_template.h @@ -24,8 +24,8 @@ /* Emits the entity list for the lights loop, in HA_LIGHT_LIST order. The * trailing comma is deliberate -- Jinja accepts it, and it avoids a - * last-element special case. */ -#define HA_TMPL_LIGHT_X(id, label, dimmable) "'" id "'," + * last-element special case. The icon argument is the UI layer's. */ +#define HA_TMPL_LIGHT_X(id, label, dimmable, icon) "'" id "'," /* * num() -- a state as a number, or null if it is not one ("unavailable", diff --git a/main/main.c b/main/main.c index 1c658be..943a7da 100644 --- a/main/main.c +++ b/main/main.c @@ -167,6 +167,22 @@ static void ha_task(void *arg) lvgl_port_resume(); + /* + * Repaint everything after every fetch window. + * + * The known left-edge artefacts (see README) happen when WiFi + * interrupt load starves the bounce-refill ISR, and once bad pixels + * are on screen LVGL has no idea they are wrong -- it never + * reinvalidates them, so they sit there indefinitely. A full + * invalidate after each poll bounds their lifetime at one poll + * interval. Mitigation, not cure: it cannot prevent the glitch, + * only clean up after it. + */ + if (lvgl_port_lock(100)) { + lv_obj_invalidate(lv_screen_active()); + lvgl_port_unlock(); + } + poll_count++; if (err == ESP_OK || on_fallback) { diff --git a/main/ui/icon_font.c b/main/ui/icon_font.c new file mode 100644 index 0000000..45be838 --- /dev/null +++ b/main/ui/icon_font.c @@ -0,0 +1,386 @@ +/* + * GENERATED by tools/make_icon_font.py -- do not edit by hand. + * Subset of Material Design Icons (Apache-2.0), rasterised at runtime + * by LVGL tiny_ttf. Regeneration instructions are in the script. + */ + +#include + +const size_t icon_font_data_size = 5972; + +const unsigned char icon_font_data[] = { + 0x00, 0x01, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x80, 0x00, 0x03, 0x00, 0x30, 0x47, 0x53, 0x55, 0x42, + 0xb8, 0xfa, 0xb8, 0xf4, 0x00, 0x00, 0x17, 0x28, 0x00, 0x00, 0x00, 0x2a, 0x4f, 0x53, 0x2f, 0x32, + 0x4f, 0xb3, 0x5b, 0xd3, 0x00, 0x00, 0x01, 0x38, 0x00, 0x00, 0x00, 0x56, 0x63, 0x6d, 0x61, 0x70, + 0x01, 0xb4, 0x33, 0xcc, 0x00, 0x00, 0x01, 0xcc, 0x00, 0x00, 0x00, 0xcc, 0x67, 0x6c, 0x79, 0x66, + 0x07, 0x29, 0x77, 0x5e, 0x00, 0x00, 0x02, 0xd0, 0x00, 0x00, 0x13, 0x74, 0x68, 0x65, 0x61, 0x64, + 0x59, 0x6d, 0x7c, 0x00, 0x00, 0x00, 0x00, 0xbc, 0x00, 0x00, 0x00, 0x36, 0x68, 0x68, 0x65, 0x61, + 0x03, 0xc6, 0x01, 0xbf, 0x00, 0x00, 0x00, 0xf4, 0x00, 0x00, 0x00, 0x24, 0x68, 0x6d, 0x74, 0x78, + 0x02, 0x00, 0xff, 0xff, 0x00, 0x00, 0x01, 0x90, 0x00, 0x00, 0x00, 0x3a, 0x6c, 0x6f, 0x63, 0x61, + 0x3f, 0xd5, 0x44, 0xe9, 0x00, 0x00, 0x02, 0x98, 0x00, 0x00, 0x00, 0x38, 0x6d, 0x61, 0x78, 0x70, + 0x01, 0x52, 0x01, 0x85, 0x00, 0x00, 0x01, 0x18, 0x00, 0x00, 0x00, 0x20, 0x6e, 0x61, 0x6d, 0x65, + 0x0b, 0xcc, 0x24, 0x14, 0x00, 0x00, 0x16, 0x44, 0x00, 0x00, 0x00, 0xc2, 0x70, 0x6f, 0x73, 0x74, + 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x17, 0x08, 0x00, 0x00, 0x00, 0x20, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0xe2, 0xcc, 0x9f, 0xb1, 0x5f, 0x0f, 0x3c, 0xf5, 0x00, 0x0b, 0x02, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x7c, 0x25, 0x9d, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x7c, 0x25, 0x9d, 0xc0, + 0xff, 0xff, 0xff, 0xbe, 0x02, 0x04, 0x01, 0xc5, 0x00, 0x00, 0x00, 0x08, 0x00, 0x02, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0xc0, 0xff, 0xc0, 0x00, 0x00, 0x02, 0x00, + 0xff, 0xff, 0xff, 0xfc, 0x02, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x00, 0x1b, 0x01, 0x79, + 0x00, 0x2d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x0a, 0x00, 0x00, + 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x00, 0x01, 0x90, 0x00, 0x05, + 0x00, 0x00, 0x01, 0x44, 0x01, 0x66, 0x00, 0x00, 0x00, 0x47, 0x01, 0x44, 0x01, 0x66, 0x00, 0x00, + 0x00, 0xf5, 0x00, 0x19, 0x00, 0x84, 0x00, 0x00, 0x02, 0x00, 0x05, 0x03, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x50, 0x66, 0x45, 0x64, 0x00, 0x40, 0xff, 0xff, 0xff, 0xff, 0x01, 0xc0, 0xff, 0xc0, + 0x00, 0x2e, 0x01, 0xc5, 0x00, 0x42, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x14, 0x00, 0x03, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x14, + 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, + 0x00, 0x0f, 0x02, 0x9a, 0x00, 0x0f, 0x02, 0x9a, 0x00, 0x00, 0x00, 0x01, 0x00, 0x0f, 0x05, 0x0f, + 0x00, 0x0f, 0x05, 0x0f, 0x00, 0x00, 0x00, 0x02, 0x00, 0x0f, 0x05, 0x90, 0x00, 0x0f, 0x05, 0x99, + 0x00, 0x00, 0x00, 0x03, 0x00, 0x0f, 0x05, 0x9b, 0x00, 0x0f, 0x05, 0x9d, 0x00, 0x00, 0x00, 0x0d, + 0x00, 0x0f, 0x05, 0xd6, 0x00, 0x0f, 0x05, 0xd6, 0x00, 0x00, 0x00, 0x10, 0x00, 0x0f, 0x06, 0x7e, + 0x00, 0x0f, 0x06, 0x7f, 0x00, 0x00, 0x00, 0x11, 0x00, 0x0f, 0x08, 0xdd, 0x00, 0x0f, 0x08, 0xdd, + 0x00, 0x00, 0x00, 0x13, 0x00, 0x0f, 0x09, 0x5f, 0x00, 0x0f, 0x09, 0x5f, 0x00, 0x00, 0x00, 0x14, + 0x00, 0x0f, 0x0a, 0x70, 0x00, 0x0f, 0x0a, 0x70, 0x00, 0x00, 0x00, 0x15, 0x00, 0x0f, 0x0e, 0x0a, + 0x00, 0x0f, 0x0e, 0x0a, 0x00, 0x00, 0x00, 0x16, 0x00, 0x0f, 0x0f, 0x55, 0x00, 0x0f, 0x0f, 0x55, + 0x00, 0x00, 0x00, 0x17, 0x00, 0x0f, 0x12, 0xba, 0x00, 0x0f, 0x12, 0xba, 0x00, 0x00, 0x00, 0x18, + 0x00, 0x0f, 0x15, 0x6d, 0x00, 0x0f, 0x15, 0x6d, 0x00, 0x00, 0x00, 0x19, 0x00, 0x0f, 0x17, 0x93, + 0x00, 0x0f, 0x17, 0x93, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7d, 0x00, 0xa5, + 0x00, 0xe3, 0x01, 0x58, 0x01, 0xc6, 0x02, 0x1b, 0x02, 0x78, 0x02, 0xfb, 0x03, 0x88, 0x03, 0xfd, + 0x04, 0x84, 0x04, 0xdd, 0x05, 0x38, 0x05, 0x92, 0x05, 0xf4, 0x06, 0x2e, 0x06, 0x96, 0x07, 0x30, + 0x07, 0x47, 0x07, 0x80, 0x07, 0xac, 0x07, 0xf1, 0x08, 0x29, 0x08, 0xe1, 0x09, 0x4d, 0x09, 0xba, + 0x00, 0x06, 0x00, 0x00, 0xff, 0xea, 0x01, 0xd6, 0x01, 0x96, 0x00, 0x10, 0x00, 0x23, 0x00, 0x38, + 0x00, 0x41, 0x00, 0x4a, 0x00, 0x53, 0x00, 0x00, 0x01, 0x22, 0x0e, 0x02, 0x14, 0x1e, 0x02, 0x32, + 0x3e, 0x02, 0x34, 0x2e, 0x02, 0x07, 0x32, 0x1e, 0x01, 0x15, 0x14, 0x06, 0x07, 0x2e, 0x01, 0x23, + 0x22, 0x07, 0x2e, 0x01, 0x35, 0x34, 0x3e, 0x01, 0x17, 0x22, 0x06, 0x0f, 0x01, 0x0e, 0x01, 0x07, + 0x06, 0x1e, 0x01, 0x36, 0x37, 0x36, 0x26, 0x27, 0x37, 0x36, 0x26, 0x27, 0x26, 0x07, 0x22, 0x06, + 0x14, 0x16, 0x32, 0x36, 0x34, 0x26, 0x07, 0x22, 0x06, 0x14, 0x16, 0x32, 0x36, 0x34, 0x26, 0x33, + 0x22, 0x06, 0x14, 0x16, 0x32, 0x36, 0x34, 0x26, 0x01, 0x00, 0x2a, 0x4f, 0x3c, 0x20, 0x20, 0x3c, + 0x4f, 0x54, 0x4f, 0x3c, 0x20, 0x20, 0x3c, 0x4f, 0x2a, 0x2e, 0x4f, 0x2e, 0x1e, 0x1c, 0x16, 0x3b, + 0x20, 0x40, 0x31, 0x1c, 0x1e, 0x2e, 0x4f, 0x59, 0x06, 0x0b, 0x03, 0x1d, 0x0c, 0x11, 0x05, 0x06, + 0x0e, 0x21, 0x20, 0x07, 0x04, 0x04, 0x07, 0x1e, 0x04, 0x07, 0x09, 0x03, 0x5b, 0x09, 0x0c, 0x0c, + 0x12, 0x0d, 0x0d, 0x49, 0x09, 0x0c, 0x0c, 0x12, 0x0d, 0x0d, 0xcd, 0x09, 0x0d, 0x0d, 0x12, 0x0c, + 0x0c, 0x01, 0x95, 0x20, 0x3c, 0x4f, 0x54, 0x4f, 0x3c, 0x20, 0x20, 0x3c, 0x4f, 0x54, 0x4f, 0x3c, + 0x20, 0x2a, 0x2e, 0x4f, 0x2e, 0x26, 0x42, 0x18, 0x15, 0x16, 0x2b, 0x18, 0x42, 0x26, 0x2e, 0x4f, + 0x2e, 0x29, 0x07, 0x07, 0x49, 0x02, 0x0f, 0x0a, 0x11, 0x20, 0x0d, 0x0e, 0x10, 0x0b, 0x16, 0x09, + 0x4b, 0x08, 0x10, 0x04, 0x01, 0x02, 0x0c, 0x12, 0x0d, 0x0d, 0x12, 0x0c, 0x40, 0x0c, 0x12, 0x0d, + 0x0d, 0x12, 0x0c, 0x0c, 0x12, 0x0d, 0x0d, 0x12, 0x0c, 0x00, 0x00, 0x02, 0x00, 0x00, 0xff, 0xea, + 0x01, 0x71, 0x01, 0x96, 0x00, 0x0f, 0x00, 0x18, 0x00, 0x00, 0x25, 0x35, 0x34, 0x26, 0x22, 0x06, + 0x1d, 0x01, 0x0e, 0x01, 0x1e, 0x01, 0x32, 0x3e, 0x01, 0x26, 0x27, 0x32, 0x16, 0x1d, 0x01, 0x23, + 0x35, 0x34, 0x36, 0x01, 0x40, 0x25, 0x36, 0x25, 0x1c, 0x14, 0x16, 0x38, 0x44, 0x38, 0x16, 0x14, + 0x5c, 0x09, 0x0c, 0x2a, 0x0c, 0xab, 0xaa, 0x1b, 0x25, 0x25, 0x1b, 0xaa, 0x15, 0x42, 0x41, 0x28, + 0x28, 0x41, 0x42, 0xd5, 0x0d, 0x09, 0x40, 0x40, 0x09, 0x0d, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x01, 0xeb, 0x01, 0x56, 0x00, 0x14, 0x00, 0x2a, 0x00, 0x00, 0x37, 0x22, 0x2e, 0x01, 0x34, 0x3e, + 0x01, 0x33, 0x3e, 0x01, 0x33, 0x32, 0x1e, 0x01, 0x17, 0x33, 0x32, 0x16, 0x14, 0x06, 0x23, 0x35, + 0x23, 0x35, 0x34, 0x2e, 0x01, 0x23, 0x22, 0x06, 0x07, 0x26, 0x23, 0x22, 0x06, 0x14, 0x16, 0x33, + 0x21, 0x32, 0x36, 0x34, 0x26, 0x80, 0x1d, 0x31, 0x1d, 0x1d, 0x31, 0x1d, 0x10, 0x45, 0x2b, 0x24, + 0x3e, 0x26, 0x03, 0x0a, 0x24, 0x32, 0x32, 0x24, 0x2a, 0x1d, 0x31, 0x1d, 0x28, 0x3b, 0x06, 0x0c, + 0x0b, 0x1b, 0x25, 0x25, 0x1b, 0x01, 0x15, 0x12, 0x19, 0x19, 0x2b, 0x1c, 0x31, 0x3a, 0x31, 0x1d, + 0x26, 0x2f, 0x22, 0x3b, 0x23, 0x32, 0x46, 0x32, 0x80, 0x15, 0x1d, 0x31, 0x1d, 0x33, 0x27, 0x04, + 0x25, 0x35, 0x26, 0x19, 0x24, 0x19, 0x00, 0x05, 0x00, 0x00, 0xff, 0xff, 0x01, 0xeb, 0x01, 0x81, + 0x00, 0x0b, 0x00, 0x17, 0x00, 0x3c, 0x00, 0x48, 0x00, 0x54, 0x00, 0x00, 0x37, 0x33, 0x32, 0x16, + 0x14, 0x06, 0x2b, 0x01, 0x22, 0x26, 0x34, 0x36, 0x21, 0x33, 0x32, 0x16, 0x14, 0x06, 0x2b, 0x01, + 0x22, 0x26, 0x34, 0x36, 0x25, 0x34, 0x3e, 0x01, 0x33, 0x3e, 0x01, 0x33, 0x32, 0x1e, 0x01, 0x17, + 0x37, 0x32, 0x16, 0x17, 0x23, 0x34, 0x26, 0x2b, 0x01, 0x35, 0x34, 0x2e, 0x01, 0x23, 0x22, 0x06, + 0x07, 0x26, 0x23, 0x22, 0x06, 0x15, 0x14, 0x17, 0x23, 0x17, 0x33, 0x32, 0x16, 0x14, 0x06, 0x2b, + 0x01, 0x22, 0x26, 0x34, 0x36, 0x33, 0x21, 0x32, 0x16, 0x14, 0x06, 0x23, 0x21, 0x22, 0x26, 0x34, + 0x36, 0x40, 0xd5, 0x09, 0x0d, 0x0d, 0x09, 0xd5, 0x09, 0x0c, 0x0c, 0x01, 0x1e, 0x6b, 0x09, 0x0c, + 0x0c, 0x09, 0x6b, 0x09, 0x0c, 0x0c, 0xfe, 0xc9, 0x1d, 0x31, 0x1d, 0x10, 0x45, 0x2b, 0x24, 0x3e, + 0x26, 0x03, 0x0a, 0x23, 0x32, 0x01, 0x2b, 0x19, 0x12, 0x2a, 0x1d, 0x31, 0x1d, 0x28, 0x3b, 0x06, + 0x0c, 0x0b, 0x1b, 0x25, 0x04, 0x2d, 0x29, 0x2b, 0x09, 0x0c, 0x0c, 0x09, 0x2b, 0x09, 0x0c, 0x0c, + 0x74, 0x01, 0x15, 0x09, 0x0c, 0x0c, 0x09, 0xfe, 0xeb, 0x09, 0x0d, 0x0d, 0x80, 0x0c, 0x12, 0x0d, + 0x0d, 0x12, 0x0c, 0x0c, 0x12, 0x0d, 0x0d, 0x12, 0x0c, 0x40, 0x1d, 0x31, 0x1d, 0x26, 0x2f, 0x22, + 0x3b, 0x24, 0x01, 0x32, 0x23, 0x11, 0x19, 0x16, 0x1d, 0x31, 0x1c, 0x33, 0x26, 0x04, 0x25, 0x1b, + 0x0b, 0x0a, 0x80, 0x0d, 0x12, 0x0c, 0x0c, 0x12, 0x0d, 0x0d, 0x12, 0x0c, 0x0c, 0x12, 0x0d, 0x00, + 0x00, 0x04, 0x00, 0x00, 0xff, 0xea, 0x01, 0xeb, 0x01, 0x96, 0x00, 0x34, 0x00, 0x3d, 0x00, 0x46, + 0x00, 0x4f, 0x00, 0x00, 0x37, 0x32, 0x16, 0x14, 0x06, 0x23, 0x22, 0x2e, 0x01, 0x34, 0x3e, 0x01, + 0x33, 0x3e, 0x01, 0x33, 0x32, 0x1e, 0x01, 0x17, 0x33, 0x32, 0x16, 0x14, 0x06, 0x2b, 0x01, 0x22, + 0x26, 0x34, 0x36, 0x3b, 0x01, 0x32, 0x36, 0x34, 0x26, 0x2b, 0x01, 0x35, 0x34, 0x2e, 0x01, 0x23, + 0x22, 0x06, 0x07, 0x26, 0x23, 0x22, 0x06, 0x14, 0x16, 0x17, 0x32, 0x16, 0x14, 0x06, 0x22, 0x26, + 0x34, 0x36, 0x37, 0x32, 0x16, 0x14, 0x06, 0x22, 0x26, 0x34, 0x36, 0x27, 0x32, 0x16, 0x14, 0x06, + 0x22, 0x26, 0x34, 0x36, 0x80, 0x09, 0x0c, 0x0c, 0x09, 0x1d, 0x31, 0x1d, 0x1d, 0x31, 0x1d, 0x10, + 0x45, 0x2b, 0x24, 0x3e, 0x26, 0x03, 0x0a, 0x24, 0x32, 0x32, 0x24, 0x15, 0x09, 0x0c, 0x0c, 0x09, + 0x15, 0x12, 0x19, 0x19, 0x12, 0x2a, 0x1d, 0x31, 0x1d, 0x28, 0x3b, 0x06, 0x0c, 0x0b, 0x1b, 0x25, + 0x25, 0x70, 0x12, 0x19, 0x19, 0x23, 0x19, 0x19, 0x71, 0x0e, 0x12, 0x12, 0x1b, 0x13, 0x13, 0x48, + 0x0d, 0x13, 0x13, 0x1a, 0x13, 0x13, 0x95, 0x0c, 0x12, 0x0c, 0x1c, 0x31, 0x3a, 0x31, 0x1d, 0x26, + 0x2f, 0x22, 0x3b, 0x23, 0x32, 0x46, 0x32, 0x0c, 0x12, 0x0c, 0x19, 0x24, 0x19, 0x15, 0x1d, 0x31, + 0x1d, 0x33, 0x27, 0x04, 0x25, 0x35, 0x26, 0x55, 0x19, 0x23, 0x19, 0x19, 0x23, 0x19, 0x2b, 0x13, + 0x1b, 0x12, 0x12, 0x1b, 0x13, 0x55, 0x13, 0x1a, 0x13, 0x13, 0x1a, 0x13, 0x00, 0x02, 0x00, 0x00, + 0xff, 0xea, 0x01, 0xeb, 0x01, 0x96, 0x00, 0x36, 0x00, 0x3d, 0x00, 0x00, 0x37, 0x22, 0x2e, 0x01, + 0x34, 0x3e, 0x01, 0x33, 0x3e, 0x01, 0x33, 0x32, 0x1e, 0x01, 0x17, 0x33, 0x32, 0x16, 0x14, 0x06, + 0x2b, 0x01, 0x22, 0x26, 0x34, 0x36, 0x3b, 0x01, 0x32, 0x36, 0x34, 0x26, 0x2b, 0x01, 0x35, 0x34, + 0x2e, 0x01, 0x23, 0x22, 0x06, 0x07, 0x26, 0x23, 0x22, 0x06, 0x14, 0x16, 0x3b, 0x01, 0x32, 0x16, + 0x14, 0x06, 0x23, 0x37, 0x33, 0x07, 0x33, 0x07, 0x37, 0x23, 0x80, 0x1d, 0x31, 0x1d, 0x1d, 0x31, + 0x1d, 0x10, 0x45, 0x2b, 0x24, 0x3e, 0x26, 0x03, 0x0a, 0x24, 0x32, 0x32, 0x24, 0x15, 0x09, 0x0c, + 0x0c, 0x09, 0x15, 0x12, 0x19, 0x19, 0x12, 0x2a, 0x1d, 0x31, 0x1d, 0x28, 0x3b, 0x06, 0x0c, 0x0b, + 0x1b, 0x25, 0x25, 0x1b, 0x15, 0x09, 0x0d, 0x0d, 0x09, 0x6b, 0x40, 0x2b, 0x2b, 0x50, 0x10, 0x35, + 0x6b, 0x1c, 0x31, 0x3a, 0x31, 0x1d, 0x26, 0x2f, 0x22, 0x3b, 0x23, 0x32, 0x46, 0x32, 0x0c, 0x12, + 0x0c, 0x19, 0x24, 0x19, 0x15, 0x1d, 0x31, 0x1d, 0x33, 0x27, 0x04, 0x25, 0x35, 0x26, 0x0c, 0x12, + 0x0c, 0x6a, 0x55, 0x95, 0x6a, 0x00, 0x00, 0x04, 0x00, 0x00, 0xff, 0xe3, 0x01, 0xc6, 0x01, 0xab, + 0x00, 0x09, 0x00, 0x13, 0x00, 0x2b, 0x00, 0x37, 0x00, 0x00, 0x01, 0x07, 0x17, 0x27, 0x07, 0x37, + 0x27, 0x3f, 0x01, 0x1f, 0x01, 0x07, 0x17, 0x27, 0x07, 0x37, 0x27, 0x3f, 0x01, 0x17, 0x07, 0x36, + 0x1e, 0x01, 0x07, 0x06, 0x07, 0x0e, 0x01, 0x2e, 0x02, 0x36, 0x37, 0x36, 0x37, 0x36, 0x1e, 0x01, + 0x07, 0x06, 0x1e, 0x02, 0x17, 0x2e, 0x03, 0x27, 0x0e, 0x01, 0x1e, 0x02, 0x36, 0x01, 0x7b, 0x36, + 0x13, 0x38, 0x38, 0x13, 0x36, 0x44, 0x17, 0x17, 0x8e, 0x23, 0x0d, 0x24, 0x25, 0x0d, 0x23, 0x2c, + 0x0f, 0x0e, 0x04, 0x08, 0x11, 0x05, 0x05, 0x0b, 0x0c, 0x29, 0x6e, 0x6e, 0x52, 0x1c, 0x1c, 0x29, + 0x0d, 0x0e, 0x08, 0x14, 0x0d, 0x01, 0x04, 0x1f, 0x45, 0x5a, 0x0c, 0x2e, 0x57, 0x44, 0x26, 0x02, + 0x1e, 0x13, 0x18, 0x3f, 0x54, 0x56, 0x01, 0x69, 0x2a, 0x41, 0x27, 0x27, 0x41, 0x2a, 0x02, 0x40, + 0x40, 0x96, 0x1a, 0x2b, 0x19, 0x19, 0x2b, 0x1a, 0x01, 0x2a, 0x2a, 0x6a, 0x01, 0x0e, 0x13, 0x08, + 0x10, 0x0b, 0x29, 0x1d, 0x1d, 0x52, 0x6e, 0x6e, 0x29, 0x0d, 0x0a, 0x05, 0x06, 0x10, 0x09, 0x2e, + 0x5a, 0x45, 0x20, 0x26, 0x02, 0x26, 0x44, 0x56, 0x2e, 0x21, 0x55, 0x54, 0x3f, 0x18, 0x13, 0x00, + 0x00, 0x07, 0x00, 0x00, 0xff, 0xea, 0x01, 0xd6, 0x01, 0x9a, 0x00, 0x1b, 0x00, 0x29, 0x00, 0x2f, + 0x00, 0x35, 0x00, 0x3b, 0x00, 0x41, 0x00, 0x54, 0x00, 0x00, 0x01, 0x1e, 0x01, 0x07, 0x1e, 0x01, + 0x1d, 0x01, 0x36, 0x33, 0x32, 0x16, 0x14, 0x06, 0x23, 0x21, 0x22, 0x26, 0x34, 0x36, 0x3b, 0x01, + 0x2e, 0x01, 0x37, 0x3e, 0x02, 0x17, 0x26, 0x06, 0x07, 0x06, 0x16, 0x17, 0x3e, 0x01, 0x33, 0x32, + 0x17, 0x2e, 0x01, 0x37, 0x26, 0x27, 0x37, 0x17, 0x26, 0x07, 0x06, 0x07, 0x37, 0x17, 0x06, 0x17, + 0x26, 0x27, 0x17, 0x07, 0x36, 0x05, 0x16, 0x17, 0x27, 0x37, 0x06, 0x05, 0x23, 0x35, 0x34, 0x26, + 0x22, 0x06, 0x15, 0x23, 0x22, 0x06, 0x14, 0x16, 0x33, 0x21, 0x32, 0x36, 0x34, 0x26, 0x01, 0x10, + 0x25, 0x26, 0x07, 0x14, 0x18, 0x0a, 0x0b, 0x1b, 0x25, 0x25, 0x1b, 0xfe, 0xeb, 0x23, 0x32, 0x32, + 0x23, 0x06, 0x15, 0x0a, 0x0e, 0x0d, 0x33, 0x3e, 0x0c, 0x1d, 0x3a, 0x0c, 0x07, 0x01, 0x08, 0x12, + 0x33, 0x1d, 0x16, 0x15, 0x01, 0x18, 0x0f, 0x11, 0x13, 0x36, 0x13, 0x11, 0xb3, 0x10, 0x0c, 0x03, + 0x3f, 0x14, 0xec, 0x03, 0x06, 0x33, 0x2c, 0x04, 0xfe, 0xbf, 0x02, 0x06, 0x32, 0x2b, 0x03, 0x01, + 0x56, 0x40, 0x32, 0x46, 0x32, 0x2b, 0x12, 0x19, 0x19, 0x12, 0x01, 0x15, 0x09, 0x0d, 0x0d, 0x01, + 0x4b, 0x10, 0x48, 0x27, 0x12, 0x33, 0x1c, 0x04, 0x04, 0x26, 0x35, 0x25, 0x32, 0x46, 0x32, 0x1a, + 0x42, 0x1f, 0x1d, 0x24, 0x07, 0x34, 0x0d, 0x16, 0x1c, 0x10, 0x23, 0x0f, 0x15, 0x19, 0x08, 0x15, + 0x23, 0x57, 0x08, 0x03, 0x1c, 0x3d, 0x0d, 0x08, 0x0b, 0x0e, 0x3c, 0x0f, 0x06, 0x7e, 0x14, 0x10, + 0x20, 0x2f, 0x14, 0x0b, 0x13, 0x11, 0x20, 0x2f, 0x16, 0xa4, 0x2b, 0x23, 0x32, 0x32, 0x23, 0x19, + 0x24, 0x19, 0x0d, 0x12, 0x0c, 0x00, 0x00, 0x04, 0x00, 0x00, 0xff, 0xef, 0x01, 0xeb, 0x01, 0x96, + 0x00, 0x0c, 0x00, 0x19, 0x00, 0x26, 0x00, 0x5f, 0x00, 0x00, 0x37, 0x1e, 0x01, 0x0f, 0x01, 0x0e, + 0x01, 0x2e, 0x01, 0x3f, 0x01, 0x3e, 0x01, 0x17, 0x1e, 0x01, 0x0f, 0x01, 0x0e, 0x01, 0x2e, 0x01, + 0x3f, 0x01, 0x3e, 0x01, 0x17, 0x1e, 0x01, 0x0f, 0x01, 0x0e, 0x01, 0x2e, 0x01, 0x3f, 0x01, 0x3e, + 0x01, 0x37, 0x35, 0x34, 0x2e, 0x01, 0x23, 0x22, 0x06, 0x07, 0x26, 0x23, 0x22, 0x06, 0x15, 0x14, + 0x16, 0x17, 0x31, 0x1e, 0x01, 0x0e, 0x01, 0x27, 0x31, 0x2e, 0x01, 0x34, 0x3e, 0x01, 0x33, 0x3e, + 0x01, 0x33, 0x32, 0x1e, 0x01, 0x17, 0x33, 0x32, 0x16, 0x15, 0x14, 0x06, 0x07, 0x31, 0x06, 0x2e, + 0x01, 0x36, 0x37, 0x31, 0x3e, 0x01, 0x35, 0x34, 0x26, 0x23, 0xc0, 0x08, 0x09, 0x02, 0x1c, 0x02, + 0x0f, 0x11, 0x09, 0x03, 0x1b, 0x02, 0x0f, 0x5e, 0x09, 0x09, 0x03, 0x2c, 0x02, 0x0f, 0x11, 0x09, + 0x02, 0x2c, 0x03, 0x0f, 0x5e, 0x08, 0x09, 0x02, 0x1c, 0x02, 0x0f, 0x12, 0x08, 0x02, 0x1c, 0x02, + 0x0f, 0x09, 0x1d, 0x31, 0x1d, 0x28, 0x3b, 0x06, 0x0c, 0x0b, 0x1b, 0x25, 0x11, 0x0f, 0x08, 0x04, + 0x09, 0x10, 0x08, 0x19, 0x1d, 0x1d, 0x31, 0x1d, 0x10, 0x45, 0x2b, 0x24, 0x3e, 0x26, 0x03, 0x0a, + 0x24, 0x32, 0x17, 0x14, 0x08, 0x11, 0x09, 0x05, 0x08, 0x09, 0x0c, 0x19, 0x12, 0xc0, 0x02, 0x10, + 0x08, 0x67, 0x09, 0x08, 0x04, 0x0f, 0x09, 0x67, 0x08, 0x09, 0x02, 0x02, 0x10, 0x08, 0xa5, 0x09, + 0x08, 0x04, 0x10, 0x08, 0xa5, 0x08, 0x09, 0x02, 0x02, 0x10, 0x08, 0x67, 0x09, 0x08, 0x04, 0x0f, + 0x09, 0x67, 0x08, 0x09, 0x29, 0x15, 0x1d, 0x31, 0x1d, 0x33, 0x27, 0x04, 0x25, 0x1b, 0x11, 0x1e, + 0x08, 0x04, 0x12, 0x0f, 0x04, 0x04, 0x0e, 0x31, 0x3a, 0x31, 0x1d, 0x26, 0x2f, 0x22, 0x3b, 0x23, + 0x32, 0x23, 0x18, 0x27, 0x0b, 0x04, 0x04, 0x10, 0x11, 0x04, 0x06, 0x13, 0x0c, 0x12, 0x19, 0x00, + 0x00, 0x03, 0x00, 0x00, 0xff, 0xea, 0x01, 0xeb, 0x01, 0x95, 0x00, 0x34, 0x00, 0x40, 0x00, 0x54, + 0x00, 0x00, 0x37, 0x32, 0x16, 0x14, 0x06, 0x23, 0x22, 0x2e, 0x01, 0x34, 0x3e, 0x01, 0x33, 0x3e, + 0x01, 0x33, 0x32, 0x1e, 0x01, 0x17, 0x37, 0x32, 0x16, 0x14, 0x06, 0x2b, 0x01, 0x22, 0x26, 0x34, + 0x36, 0x3b, 0x01, 0x32, 0x36, 0x34, 0x26, 0x2b, 0x01, 0x35, 0x34, 0x2e, 0x01, 0x23, 0x22, 0x06, + 0x07, 0x26, 0x23, 0x22, 0x06, 0x14, 0x16, 0x17, 0x16, 0x17, 0x16, 0x15, 0x14, 0x06, 0x22, 0x26, + 0x35, 0x34, 0x3f, 0x01, 0x0f, 0x01, 0x06, 0x07, 0x06, 0x07, 0x06, 0x15, 0x14, 0x16, 0x32, 0x36, + 0x35, 0x34, 0x27, 0x2e, 0x01, 0x27, 0x31, 0x80, 0x09, 0x0c, 0x0c, 0x09, 0x1d, 0x31, 0x1d, 0x1d, + 0x31, 0x1d, 0x10, 0x45, 0x2b, 0x24, 0x3e, 0x26, 0x03, 0x0a, 0x24, 0x32, 0x32, 0x24, 0x15, 0x09, + 0x0c, 0x0c, 0x09, 0x15, 0x12, 0x19, 0x19, 0x12, 0x2a, 0x1d, 0x31, 0x1d, 0x28, 0x3b, 0x06, 0x0b, + 0x0c, 0x1b, 0x25, 0x25, 0x9b, 0x05, 0x07, 0x1f, 0x19, 0x24, 0x19, 0x1f, 0x0c, 0x0b, 0x0b, 0x0e, + 0x0b, 0x12, 0x08, 0x0c, 0x32, 0x46, 0x32, 0x0c, 0x08, 0x24, 0x12, 0x95, 0x0d, 0x11, 0x0d, 0x1d, + 0x31, 0x3a, 0x31, 0x1c, 0x27, 0x2f, 0x23, 0x3a, 0x24, 0x01, 0x32, 0x47, 0x32, 0x0d, 0x11, 0x0d, + 0x19, 0x23, 0x19, 0x15, 0x1d, 0x31, 0x1d, 0x33, 0x26, 0x04, 0x26, 0x35, 0x25, 0x03, 0x07, 0x0a, + 0x2b, 0x16, 0x12, 0x19, 0x19, 0x12, 0x16, 0x2b, 0x54, 0x0c, 0x0e, 0x11, 0x11, 0x19, 0x12, 0x19, + 0x15, 0x23, 0x32, 0x32, 0x23, 0x15, 0x19, 0x12, 0x34, 0x15, 0x00, 0x02, 0x00, 0x00, 0xff, 0xd6, + 0x01, 0xeb, 0x01, 0x96, 0x00, 0x34, 0x00, 0x5e, 0x00, 0x00, 0x37, 0x32, 0x16, 0x14, 0x06, 0x23, + 0x22, 0x2e, 0x01, 0x34, 0x3e, 0x01, 0x33, 0x3e, 0x01, 0x33, 0x32, 0x1e, 0x01, 0x17, 0x33, 0x32, + 0x16, 0x14, 0x06, 0x2b, 0x01, 0x22, 0x26, 0x34, 0x36, 0x3b, 0x01, 0x32, 0x36, 0x34, 0x26, 0x2b, + 0x01, 0x35, 0x34, 0x2e, 0x01, 0x23, 0x22, 0x06, 0x07, 0x26, 0x23, 0x22, 0x06, 0x14, 0x16, 0x17, + 0x37, 0x27, 0x26, 0x34, 0x36, 0x32, 0x1f, 0x01, 0x37, 0x3e, 0x01, 0x1e, 0x01, 0x0f, 0x01, 0x37, + 0x36, 0x1e, 0x01, 0x06, 0x0f, 0x01, 0x17, 0x16, 0x14, 0x06, 0x22, 0x2f, 0x01, 0x07, 0x0e, 0x01, + 0x2e, 0x01, 0x3f, 0x01, 0x07, 0x06, 0x2e, 0x01, 0x36, 0x80, 0x09, 0x0c, 0x0c, 0x09, 0x1d, 0x31, + 0x1d, 0x1d, 0x31, 0x1d, 0x10, 0x45, 0x2b, 0x24, 0x3e, 0x26, 0x03, 0x0a, 0x24, 0x32, 0x32, 0x24, + 0x15, 0x09, 0x0c, 0x0c, 0x09, 0x15, 0x12, 0x19, 0x19, 0x12, 0x2a, 0x1d, 0x31, 0x1d, 0x28, 0x3b, + 0x06, 0x0c, 0x0b, 0x1b, 0x25, 0x25, 0x43, 0x2f, 0x23, 0x06, 0x0d, 0x12, 0x06, 0x22, 0x0c, 0x03, + 0x0f, 0x11, 0x09, 0x02, 0x0d, 0x2f, 0x08, 0x10, 0x04, 0x09, 0x08, 0x2f, 0x23, 0x06, 0x0d, 0x12, + 0x06, 0x22, 0x0c, 0x03, 0x0f, 0x11, 0x09, 0x02, 0x0d, 0x2f, 0x08, 0x10, 0x04, 0x09, 0x95, 0x0c, + 0x12, 0x0c, 0x1c, 0x31, 0x3a, 0x31, 0x1d, 0x26, 0x2f, 0x22, 0x3b, 0x23, 0x32, 0x46, 0x32, 0x0c, + 0x12, 0x0c, 0x19, 0x24, 0x19, 0x15, 0x1d, 0x31, 0x1d, 0x33, 0x27, 0x04, 0x25, 0x35, 0x26, 0x56, + 0x0c, 0x22, 0x06, 0x12, 0x0d, 0x06, 0x23, 0x2f, 0x08, 0x09, 0x04, 0x10, 0x08, 0x2f, 0x0d, 0x02, + 0x09, 0x11, 0x0f, 0x03, 0x0c, 0x22, 0x06, 0x12, 0x0d, 0x06, 0x23, 0x2f, 0x08, 0x09, 0x04, 0x10, + 0x08, 0x2f, 0x0d, 0x02, 0x09, 0x11, 0x0f, 0x00, 0x00, 0x08, 0x00, 0x00, 0xff, 0xea, 0x01, 0xb9, + 0x01, 0x96, 0x00, 0x0c, 0x00, 0x15, 0x00, 0x1a, 0x00, 0x1f, 0x00, 0x24, 0x00, 0x29, 0x00, 0x2e, + 0x00, 0x33, 0x00, 0x00, 0x01, 0x32, 0x1e, 0x01, 0x14, 0x0e, 0x01, 0x22, 0x2e, 0x01, 0x34, 0x3e, + 0x01, 0x17, 0x22, 0x06, 0x14, 0x16, 0x32, 0x36, 0x34, 0x26, 0x27, 0x17, 0x26, 0x22, 0x0f, 0x01, + 0x37, 0x0e, 0x01, 0x0f, 0x01, 0x37, 0x1e, 0x01, 0x17, 0x25, 0x07, 0x2e, 0x01, 0x27, 0x17, 0x07, + 0x3e, 0x01, 0x37, 0x07, 0x27, 0x16, 0x32, 0x37, 0x01, 0x00, 0x1d, 0x31, 0x1d, 0x1d, 0x31, 0x3a, + 0x31, 0x1d, 0x1d, 0x31, 0x1d, 0x1b, 0x25, 0x25, 0x36, 0x25, 0x25, 0x1b, 0x33, 0x19, 0x34, 0x19, + 0x86, 0x59, 0x14, 0x1a, 0x05, 0x25, 0x25, 0x05, 0x1a, 0x14, 0x01, 0x19, 0x26, 0x05, 0x1a, 0x14, + 0x58, 0x58, 0x14, 0x1a, 0x05, 0x93, 0x33, 0x19, 0x33, 0x1a, 0x01, 0x2b, 0x1d, 0x31, 0x3a, 0x31, + 0x1d, 0x1d, 0x31, 0x3a, 0x31, 0x1d, 0x2b, 0x25, 0x36, 0x25, 0x25, 0x36, 0x25, 0x95, 0x49, 0x09, + 0x09, 0x21, 0x07, 0x11, 0x2c, 0x1b, 0x85, 0x51, 0x1a, 0x2d, 0x12, 0xde, 0x51, 0x1a, 0x2e, 0x10, + 0xdd, 0x07, 0x11, 0x2d, 0x1a, 0xbb, 0x49, 0x09, 0x09, 0x00, 0x00, 0x06, 0x00, 0x00, 0xff, 0xff, + 0x01, 0xd6, 0x01, 0x96, 0x00, 0x13, 0x00, 0x19, 0x00, 0x1e, 0x00, 0x23, 0x00, 0x28, 0x00, 0x39, + 0x00, 0x00, 0x37, 0x33, 0x34, 0x3e, 0x01, 0x32, 0x1e, 0x01, 0x15, 0x33, 0x32, 0x16, 0x14, 0x06, + 0x23, 0x21, 0x22, 0x26, 0x34, 0x36, 0x21, 0x34, 0x26, 0x22, 0x06, 0x15, 0x37, 0x17, 0x26, 0x22, + 0x0f, 0x01, 0x37, 0x0e, 0x01, 0x07, 0x25, 0x07, 0x2e, 0x01, 0x27, 0x03, 0x37, 0x36, 0x34, 0x26, + 0x22, 0x0f, 0x01, 0x27, 0x26, 0x22, 0x06, 0x14, 0x1f, 0x01, 0x16, 0x32, 0x40, 0x55, 0x1d, 0x31, + 0x3a, 0x31, 0x1d, 0x55, 0x09, 0x0c, 0x0c, 0x09, 0xfe, 0x80, 0x09, 0x0c, 0x0c, 0x01, 0x09, 0x25, + 0x36, 0x25, 0x40, 0x33, 0x19, 0x34, 0x19, 0x86, 0x59, 0x14, 0x1a, 0x05, 0x01, 0x4c, 0x26, 0x05, + 0x1a, 0x14, 0x51, 0x42, 0x07, 0x0d, 0x11, 0x07, 0x33, 0x33, 0x07, 0x11, 0x0d, 0x07, 0x42, 0x07, + 0x10, 0xc0, 0x1d, 0x31, 0x1d, 0x1d, 0x31, 0x1d, 0x0c, 0x12, 0x0d, 0x0d, 0x12, 0x0c, 0x1b, 0x25, + 0x25, 0x1b, 0xd5, 0x49, 0x09, 0x09, 0x21, 0x07, 0x11, 0x2c, 0x1b, 0x51, 0x51, 0x1a, 0x2e, 0x10, + 0xfe, 0xd4, 0x43, 0x06, 0x12, 0x0c, 0x06, 0x34, 0x34, 0x06, 0x0c, 0x12, 0x06, 0x43, 0x06, 0x00, + 0x00, 0x06, 0x00, 0x00, 0xff, 0xfd, 0x01, 0xd6, 0x01, 0x96, 0x00, 0x13, 0x00, 0x19, 0x00, 0x1e, + 0x00, 0x23, 0x00, 0x28, 0x00, 0x39, 0x00, 0x00, 0x37, 0x33, 0x34, 0x3e, 0x01, 0x32, 0x1e, 0x01, + 0x15, 0x33, 0x32, 0x16, 0x14, 0x06, 0x23, 0x21, 0x22, 0x26, 0x34, 0x36, 0x21, 0x34, 0x26, 0x22, + 0x06, 0x15, 0x37, 0x17, 0x26, 0x22, 0x0f, 0x01, 0x37, 0x0e, 0x01, 0x07, 0x25, 0x07, 0x2e, 0x01, + 0x27, 0x07, 0x17, 0x16, 0x14, 0x06, 0x22, 0x2f, 0x01, 0x07, 0x06, 0x22, 0x26, 0x34, 0x3f, 0x01, + 0x36, 0x32, 0x40, 0x55, 0x1d, 0x31, 0x3a, 0x31, 0x1d, 0x55, 0x09, 0x0c, 0x0c, 0x09, 0xfe, 0x80, + 0x09, 0x0c, 0x0c, 0x01, 0x09, 0x25, 0x36, 0x25, 0x40, 0x33, 0x19, 0x34, 0x19, 0x86, 0x59, 0x14, + 0x1a, 0x05, 0x01, 0x4c, 0x26, 0x05, 0x1a, 0x14, 0x51, 0x42, 0x07, 0x0d, 0x11, 0x07, 0x33, 0x33, + 0x07, 0x11, 0x0d, 0x07, 0x42, 0x07, 0x10, 0xc0, 0x1d, 0x31, 0x1d, 0x1d, 0x31, 0x1d, 0x0c, 0x12, + 0x0d, 0x0d, 0x12, 0x0c, 0x1b, 0x25, 0x25, 0x1b, 0xd5, 0x49, 0x09, 0x09, 0x21, 0x07, 0x11, 0x2c, + 0x1b, 0x51, 0x51, 0x1a, 0x2e, 0x10, 0xce, 0x42, 0x06, 0x12, 0x0c, 0x06, 0x33, 0x33, 0x06, 0x0c, + 0x12, 0x06, 0x42, 0x07, 0x00, 0x03, 0x00, 0x00, 0xff, 0xea, 0x01, 0xd6, 0x01, 0x96, 0x00, 0x17, + 0x00, 0x2e, 0x00, 0x45, 0x00, 0x00, 0x37, 0x22, 0x26, 0x34, 0x36, 0x3b, 0x01, 0x32, 0x36, 0x34, + 0x26, 0x22, 0x07, 0x06, 0x22, 0x26, 0x34, 0x37, 0x36, 0x32, 0x16, 0x14, 0x06, 0x23, 0x17, 0x32, + 0x36, 0x34, 0x26, 0x22, 0x06, 0x22, 0x26, 0x34, 0x37, 0x36, 0x32, 0x16, 0x14, 0x06, 0x23, 0x21, + 0x22, 0x26, 0x34, 0x36, 0x33, 0x05, 0x21, 0x22, 0x26, 0x34, 0x36, 0x33, 0x21, 0x32, 0x16, 0x14, + 0x06, 0x22, 0x27, 0x26, 0x34, 0x36, 0x32, 0x16, 0x32, 0x36, 0x34, 0x26, 0x55, 0x09, 0x0c, 0x0c, + 0x09, 0xab, 0x12, 0x19, 0x19, 0x24, 0x0c, 0x06, 0x12, 0x0d, 0x07, 0x19, 0x46, 0x32, 0x32, 0x23, + 0x95, 0x09, 0x0d, 0x0d, 0x12, 0x0c, 0x12, 0x0c, 0x06, 0x13, 0x35, 0x25, 0x25, 0x1b, 0xfe, 0xd6, + 0x09, 0x0d, 0x0d, 0x09, 0x01, 0x15, 0xfe, 0xd5, 0x09, 0x0c, 0x0c, 0x09, 0x01, 0x2b, 0x1b, 0x25, + 0x25, 0x35, 0x13, 0x06, 0x0c, 0x12, 0x0c, 0x12, 0x0c, 0x0c, 0xeb, 0x0c, 0x12, 0x0c, 0x19, 0x24, + 0x19, 0x0d, 0x06, 0x0c, 0x12, 0x06, 0x19, 0x32, 0x46, 0x32, 0x2b, 0x0c, 0x12, 0x0d, 0x0d, 0x0d, + 0x11, 0x07, 0x12, 0x25, 0x35, 0x26, 0x0d, 0x12, 0x0c, 0x80, 0x0c, 0x12, 0x0d, 0x26, 0x35, 0x25, + 0x12, 0x07, 0x11, 0x0d, 0x0d, 0x0d, 0x12, 0x0c, 0x00, 0x04, 0x00, 0x00, 0xff, 0xea, 0x01, 0xd6, + 0x01, 0x96, 0x00, 0x03, 0x00, 0x07, 0x00, 0x18, 0x00, 0x25, 0x00, 0x00, 0x37, 0x33, 0x15, 0x23, + 0x35, 0x33, 0x15, 0x23, 0x37, 0x22, 0x0e, 0x01, 0x15, 0x14, 0x1e, 0x02, 0x32, 0x3e, 0x02, 0x34, + 0x2e, 0x02, 0x03, 0x22, 0x2e, 0x01, 0x34, 0x3e, 0x01, 0x32, 0x1e, 0x01, 0x14, 0x0e, 0x01, 0xeb, + 0x2a, 0x2a, 0x2a, 0x2a, 0x15, 0x3a, 0x62, 0x39, 0x20, 0x3c, 0x4f, 0x54, 0x4f, 0x3c, 0x20, 0x20, + 0x3c, 0x4f, 0x2a, 0x2e, 0x4f, 0x2e, 0x2e, 0x4f, 0x5c, 0x4f, 0x2e, 0x2e, 0x4f, 0x80, 0x2b, 0xd6, + 0x80, 0xea, 0x39, 0x62, 0x3a, 0x2a, 0x4f, 0x3c, 0x20, 0x20, 0x3c, 0x4f, 0x54, 0x4f, 0x3c, 0x20, + 0xfe, 0x80, 0x2e, 0x4f, 0x5c, 0x4f, 0x2e, 0x2e, 0x4f, 0x5c, 0x4f, 0x2e, 0x00, 0x03, 0x00, 0x00, + 0xff, 0xea, 0x01, 0xeb, 0x01, 0x96, 0x00, 0x34, 0x00, 0x3b, 0x00, 0x49, 0x00, 0x00, 0x37, 0x1e, + 0x01, 0x0e, 0x01, 0x27, 0x31, 0x2e, 0x01, 0x34, 0x3e, 0x01, 0x33, 0x3e, 0x01, 0x33, 0x32, 0x1e, + 0x01, 0x17, 0x33, 0x32, 0x16, 0x14, 0x06, 0x23, 0x22, 0x26, 0x34, 0x36, 0x33, 0x32, 0x36, 0x34, + 0x26, 0x2b, 0x01, 0x35, 0x34, 0x2e, 0x01, 0x23, 0x22, 0x06, 0x07, 0x26, 0x23, 0x22, 0x06, 0x15, + 0x14, 0x16, 0x17, 0x37, 0x33, 0x07, 0x33, 0x07, 0x37, 0x23, 0x17, 0x14, 0x06, 0x22, 0x26, 0x34, + 0x37, 0x36, 0x37, 0x31, 0x17, 0x16, 0x17, 0x16, 0x60, 0x08, 0x04, 0x09, 0x10, 0x08, 0x19, 0x1d, + 0x1d, 0x31, 0x1d, 0x10, 0x45, 0x2b, 0x24, 0x3e, 0x26, 0x03, 0x0a, 0x24, 0x32, 0x32, 0x24, 0x09, + 0x0c, 0x0c, 0x09, 0x12, 0x19, 0x19, 0x12, 0x2a, 0x1d, 0x31, 0x1d, 0x28, 0x3b, 0x06, 0x0c, 0x0b, + 0x1b, 0x25, 0x11, 0x0f, 0x6b, 0x40, 0x2b, 0x2b, 0x50, 0x10, 0x36, 0xe0, 0x1c, 0x28, 0x1c, 0x18, + 0x0c, 0x0c, 0x0d, 0x0e, 0x09, 0x0c, 0x9e, 0x04, 0x12, 0x0f, 0x04, 0x04, 0x0e, 0x31, 0x3a, 0x31, + 0x1d, 0x26, 0x2f, 0x22, 0x3b, 0x23, 0x32, 0x46, 0x32, 0x0c, 0x12, 0x0c, 0x19, 0x24, 0x19, 0x15, + 0x1d, 0x31, 0x1d, 0x33, 0x27, 0x04, 0x25, 0x1b, 0x11, 0x1e, 0x08, 0x37, 0x55, 0x95, 0x6a, 0x23, + 0x15, 0x1d, 0x1d, 0x29, 0x25, 0x12, 0x0e, 0x10, 0x13, 0x11, 0x17, 0x00, 0x00, 0x03, 0x00, 0x00, + 0xff, 0xfd, 0x01, 0xeb, 0x01, 0x96, 0x00, 0x0d, 0x00, 0x38, 0x00, 0x6a, 0x00, 0x00, 0x25, 0x14, + 0x06, 0x22, 0x26, 0x34, 0x37, 0x36, 0x37, 0x31, 0x17, 0x16, 0x17, 0x16, 0x25, 0x26, 0x36, 0x3f, + 0x01, 0x27, 0x26, 0x34, 0x36, 0x32, 0x1f, 0x01, 0x37, 0x3e, 0x01, 0x1e, 0x01, 0x0f, 0x01, 0x37, + 0x36, 0x1e, 0x01, 0x06, 0x0f, 0x01, 0x17, 0x16, 0x14, 0x06, 0x22, 0x2f, 0x01, 0x07, 0x0e, 0x01, + 0x2e, 0x01, 0x3f, 0x01, 0x07, 0x06, 0x26, 0x27, 0x34, 0x3e, 0x01, 0x33, 0x3e, 0x01, 0x33, 0x32, + 0x1e, 0x01, 0x17, 0x33, 0x32, 0x16, 0x14, 0x06, 0x23, 0x22, 0x26, 0x34, 0x36, 0x33, 0x32, 0x36, + 0x34, 0x26, 0x2b, 0x01, 0x35, 0x34, 0x2e, 0x01, 0x23, 0x22, 0x06, 0x07, 0x26, 0x23, 0x22, 0x06, + 0x14, 0x17, 0x16, 0x14, 0x06, 0x22, 0x27, 0x2e, 0x01, 0x01, 0x8b, 0x1c, 0x28, 0x1c, 0x18, 0x0c, + 0x0c, 0x0c, 0x0e, 0x09, 0x0d, 0xfe, 0xca, 0x02, 0x09, 0x09, 0x30, 0x23, 0x07, 0x0d, 0x13, 0x06, + 0x23, 0x0d, 0x02, 0x10, 0x11, 0x09, 0x02, 0x0d, 0x30, 0x09, 0x10, 0x05, 0x09, 0x09, 0x30, 0x23, + 0x06, 0x0d, 0x12, 0x06, 0x23, 0x0d, 0x03, 0x0f, 0x12, 0x09, 0x02, 0x0d, 0x30, 0x09, 0x0f, 0x43, + 0x1d, 0x31, 0x1d, 0x10, 0x45, 0x2b, 0x24, 0x3e, 0x26, 0x03, 0x0a, 0x24, 0x32, 0x32, 0x24, 0x09, + 0x0c, 0x0c, 0x09, 0x12, 0x19, 0x19, 0x12, 0x2a, 0x1d, 0x31, 0x1d, 0x28, 0x3b, 0x06, 0x0c, 0x0b, + 0x1b, 0x25, 0x13, 0x06, 0x0c, 0x12, 0x06, 0x0f, 0x11, 0x32, 0x15, 0x1d, 0x1d, 0x29, 0x25, 0x12, + 0x0e, 0x10, 0x13, 0x11, 0x17, 0x0e, 0x08, 0x10, 0x02, 0x0d, 0x23, 0x07, 0x12, 0x0d, 0x07, 0x23, + 0x30, 0x09, 0x09, 0x04, 0x10, 0x09, 0x30, 0x0c, 0x03, 0x09, 0x12, 0x0f, 0x02, 0x0d, 0x23, 0x07, + 0x12, 0x0d, 0x06, 0x24, 0x30, 0x09, 0x09, 0x04, 0x10, 0x09, 0x30, 0x0d, 0x02, 0x09, 0x90, 0x1d, + 0x31, 0x1d, 0x26, 0x2f, 0x22, 0x3b, 0x23, 0x32, 0x46, 0x32, 0x0c, 0x12, 0x0c, 0x19, 0x24, 0x19, + 0x15, 0x1d, 0x31, 0x1d, 0x33, 0x27, 0x04, 0x25, 0x36, 0x13, 0x06, 0x11, 0x0c, 0x06, 0x0f, 0x27, + 0x00, 0x02, 0x00, 0x00, 0xff, 0xea, 0x01, 0x6b, 0x01, 0x96, 0x00, 0x03, 0x00, 0x0b, 0x00, 0x00, + 0x01, 0x17, 0x23, 0x37, 0x17, 0x33, 0x15, 0x33, 0x15, 0x23, 0x35, 0x33, 0x01, 0x40, 0x2b, 0xd6, + 0x2b, 0x2b, 0x2a, 0x40, 0xaa, 0x40, 0x01, 0x95, 0x95, 0x95, 0xaa, 0xd6, 0x2a, 0x2a, 0x00, 0x02, + 0x00, 0x00, 0xff, 0xea, 0x01, 0x98, 0x01, 0x96, 0x00, 0x1d, 0x00, 0x23, 0x00, 0x00, 0x13, 0x07, + 0x17, 0x07, 0x23, 0x22, 0x06, 0x15, 0x14, 0x16, 0x1f, 0x01, 0x23, 0x15, 0x33, 0x35, 0x23, 0x27, + 0x36, 0x35, 0x34, 0x27, 0x37, 0x06, 0x16, 0x17, 0x37, 0x2e, 0x01, 0x07, 0x17, 0x07, 0x16, 0x3e, + 0x01, 0x26, 0xe7, 0x23, 0x18, 0x44, 0x03, 0x11, 0x19, 0x13, 0x0e, 0x57, 0x4e, 0xd6, 0x56, 0x5f, + 0x0a, 0x04, 0x44, 0x04, 0x1d, 0x1b, 0x5f, 0x1b, 0x3d, 0x18, 0x64, 0x23, 0x0b, 0x1a, 0x0e, 0x05, + 0x01, 0x95, 0x35, 0x10, 0x65, 0x19, 0x12, 0x0f, 0x17, 0x04, 0x81, 0x2a, 0x2a, 0x90, 0x0c, 0x0f, + 0x0a, 0x09, 0x65, 0x1b, 0x36, 0x12, 0x8e, 0x12, 0x06, 0x0e, 0x42, 0x36, 0x07, 0x05, 0x16, 0x1a, + 0x00, 0x02, 0x00, 0x00, 0xff, 0xea, 0x01, 0xc0, 0x01, 0x96, 0x00, 0x13, 0x00, 0x1d, 0x00, 0x00, + 0x13, 0x23, 0x35, 0x23, 0x15, 0x23, 0x35, 0x23, 0x15, 0x14, 0x16, 0x17, 0x15, 0x33, 0x35, 0x3e, + 0x01, 0x3d, 0x01, 0x23, 0x17, 0x15, 0x33, 0x15, 0x33, 0x11, 0x22, 0x07, 0x0e, 0x01, 0xeb, 0x2b, + 0x2b, 0x2a, 0x2b, 0x2e, 0x22, 0x35, 0x22, 0x2e, 0x2a, 0x6a, 0x36, 0x35, 0x1c, 0x1a, 0x17, 0x1e, + 0x01, 0x00, 0x95, 0x95, 0x95, 0x95, 0x22, 0x31, 0x02, 0xc0, 0xc0, 0x02, 0x31, 0x22, 0x95, 0x55, + 0xab, 0xaa, 0x01, 0xaa, 0x0d, 0x0d, 0x28, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x01, 0x81, + 0x01, 0x70, 0x00, 0x16, 0x00, 0x2a, 0x00, 0x00, 0x01, 0x0f, 0x01, 0x06, 0x07, 0x06, 0x07, 0x06, + 0x15, 0x14, 0x1e, 0x01, 0x32, 0x3e, 0x01, 0x35, 0x34, 0x27, 0x26, 0x27, 0x26, 0x2f, 0x01, 0x07, + 0x16, 0x17, 0x16, 0x17, 0x16, 0x17, 0x16, 0x15, 0x14, 0x06, 0x22, 0x26, 0x35, 0x34, 0x37, 0x36, + 0x37, 0x36, 0x37, 0x01, 0x00, 0x10, 0x11, 0x15, 0x11, 0x1a, 0x0d, 0x12, 0x22, 0x3b, 0x46, 0x3b, + 0x22, 0x12, 0x0d, 0x1a, 0x11, 0x15, 0x11, 0x10, 0x01, 0x03, 0x10, 0x10, 0x14, 0x0e, 0x0f, 0x31, + 0x48, 0x31, 0x0f, 0x0e, 0x14, 0x10, 0x10, 0x01, 0x70, 0x12, 0x15, 0x19, 0x19, 0x27, 0x1a, 0x27, + 0x1f, 0x22, 0x3b, 0x23, 0x23, 0x3b, 0x22, 0x1f, 0x27, 0x1a, 0x27, 0x19, 0x19, 0x15, 0x31, 0x02, + 0x03, 0x13, 0x16, 0x1e, 0x1e, 0x21, 0x12, 0x23, 0x32, 0x32, 0x23, 0x12, 0x21, 0x1e, 0x1e, 0x16, + 0x13, 0x00, 0x00, 0x03, 0x00, 0x00, 0xff, 0xea, 0x01, 0xd6, 0x01, 0x80, 0x00, 0x0e, 0x00, 0x17, + 0x00, 0x24, 0x00, 0x00, 0x01, 0x32, 0x16, 0x1d, 0x01, 0x16, 0x14, 0x06, 0x22, 0x26, 0x34, 0x37, + 0x35, 0x34, 0x36, 0x17, 0x22, 0x06, 0x1d, 0x01, 0x33, 0x35, 0x34, 0x26, 0x27, 0x07, 0x15, 0x33, + 0x07, 0x17, 0x23, 0x35, 0x23, 0x37, 0x17, 0x06, 0x07, 0x01, 0x95, 0x12, 0x19, 0x15, 0x25, 0x35, + 0x26, 0x16, 0x19, 0x11, 0x08, 0x0d, 0x2b, 0x0d, 0x9e, 0x6b, 0x98, 0x02, 0x02, 0xc2, 0x40, 0xd5, + 0x5e, 0x10, 0x09, 0x01, 0x15, 0x19, 0x11, 0x91, 0x13, 0x37, 0x25, 0x25, 0x37, 0x13, 0x91, 0x11, + 0x19, 0x15, 0x0d, 0x08, 0x16, 0x16, 0x08, 0x0d, 0x47, 0x60, 0xa7, 0x15, 0x16, 0xab, 0xc0, 0x54, + 0x0f, 0x15, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x01, 0xee, 0x01, 0x56, 0x00, 0x3e, 0x00, 0x52, + 0x00, 0x65, 0x00, 0x77, 0x00, 0x00, 0x25, 0x2e, 0x01, 0x2f, 0x01, 0x36, 0x37, 0x35, 0x06, 0x07, + 0x06, 0x22, 0x27, 0x26, 0x27, 0x15, 0x16, 0x17, 0x07, 0x0e, 0x01, 0x07, 0x06, 0x17, 0x16, 0x17, + 0x16, 0x37, 0x36, 0x37, 0x36, 0x27, 0x37, 0x16, 0x17, 0x15, 0x0e, 0x01, 0x15, 0x14, 0x17, 0x16, + 0x33, 0x32, 0x37, 0x36, 0x35, 0x34, 0x26, 0x27, 0x35, 0x36, 0x37, 0x17, 0x06, 0x17, 0x16, 0x17, + 0x16, 0x37, 0x36, 0x37, 0x36, 0x25, 0x06, 0x07, 0x06, 0x07, 0x06, 0x27, 0x26, 0x37, 0x36, 0x3f, + 0x01, 0x36, 0x37, 0x36, 0x17, 0x16, 0x17, 0x16, 0x07, 0x17, 0x22, 0x27, 0x26, 0x2f, 0x01, 0x37, + 0x36, 0x37, 0x36, 0x32, 0x17, 0x16, 0x1f, 0x01, 0x07, 0x06, 0x07, 0x06, 0x37, 0x06, 0x27, 0x26, + 0x2f, 0x01, 0x34, 0x37, 0x36, 0x37, 0x36, 0x17, 0x16, 0x1f, 0x01, 0x16, 0x07, 0x06, 0x01, 0xe1, + 0x03, 0x11, 0x0c, 0x09, 0x10, 0x0d, 0x23, 0x33, 0x3b, 0x88, 0x3b, 0x33, 0x23, 0x0d, 0x10, 0x09, + 0x0c, 0x11, 0x03, 0x0c, 0x03, 0x04, 0x24, 0x18, 0x15, 0x1e, 0x12, 0x07, 0x10, 0x09, 0x1a, 0x20, + 0x0a, 0x0b, 0x0b, 0x10, 0x25, 0x19, 0x10, 0x17, 0x0b, 0x0a, 0x20, 0x1a, 0x09, 0x10, 0x07, 0x12, + 0x1e, 0x15, 0x18, 0x23, 0x05, 0x03, 0xfe, 0x8f, 0x04, 0x03, 0x09, 0x0a, 0x0e, 0x0e, 0x13, 0x03, + 0x02, 0x06, 0x03, 0x05, 0x07, 0x0a, 0x0d, 0x14, 0x04, 0x02, 0x02, 0x84, 0x0e, 0x09, 0x05, 0x03, + 0x01, 0x01, 0x03, 0x05, 0x09, 0x1c, 0x09, 0x05, 0x03, 0x01, 0x01, 0x03, 0x05, 0x09, 0xac, 0x13, + 0x14, 0x09, 0x06, 0x01, 0x05, 0x06, 0x0e, 0x13, 0x0c, 0x05, 0x02, 0x05, 0x04, 0x01, 0x02, 0xcd, + 0x0d, 0x16, 0x07, 0x24, 0x05, 0x06, 0x2f, 0x13, 0x0b, 0x0d, 0x0d, 0x0b, 0x13, 0x2f, 0x06, 0x05, + 0x24, 0x07, 0x16, 0x0d, 0x31, 0x21, 0x31, 0x09, 0x06, 0x12, 0x1a, 0x48, 0x1c, 0x17, 0x25, 0x04, + 0x02, 0x26, 0x09, 0x1a, 0x0d, 0x32, 0x20, 0x2e, 0x16, 0x20, 0x4a, 0x0d, 0x1a, 0x09, 0x26, 0x02, + 0x04, 0x25, 0x17, 0x1c, 0x48, 0x1a, 0x12, 0x06, 0x09, 0x31, 0x21, 0x1a, 0x0c, 0x0c, 0x1b, 0x10, + 0x16, 0x03, 0x05, 0x31, 0x18, 0x17, 0x08, 0x09, 0x04, 0x06, 0x03, 0x05, 0x11, 0x09, 0x08, 0x6b, + 0x19, 0x11, 0x1d, 0x19, 0x08, 0x09, 0x06, 0x09, 0x09, 0x06, 0x09, 0x08, 0x19, 0x1d, 0x11, 0x19, + 0x15, 0x05, 0x2d, 0x17, 0x17, 0x08, 0x0a, 0x07, 0x0a, 0x04, 0x04, 0x0d, 0x07, 0x08, 0x18, 0x1c, + 0x13, 0x1a, 0x00, 0x04, 0xff, 0xff, 0xff, 0xea, 0x02, 0x00, 0x01, 0x6b, 0x00, 0x22, 0x00, 0x2f, + 0x00, 0x3c, 0x00, 0x52, 0x00, 0x00, 0x01, 0x35, 0x34, 0x26, 0x2b, 0x01, 0x22, 0x07, 0x26, 0x2b, + 0x01, 0x22, 0x06, 0x1d, 0x01, 0x22, 0x06, 0x1d, 0x01, 0x14, 0x16, 0x33, 0x15, 0x33, 0x35, 0x21, + 0x15, 0x33, 0x35, 0x32, 0x36, 0x3d, 0x01, 0x34, 0x26, 0x27, 0x33, 0x32, 0x16, 0x1d, 0x01, 0x06, + 0x1d, 0x01, 0x23, 0x35, 0x34, 0x36, 0x07, 0x34, 0x36, 0x3b, 0x01, 0x32, 0x16, 0x1d, 0x01, 0x23, + 0x35, 0x34, 0x27, 0x05, 0x14, 0x06, 0x23, 0x21, 0x22, 0x26, 0x3d, 0x01, 0x34, 0x36, 0x32, 0x16, + 0x1d, 0x01, 0x21, 0x35, 0x34, 0x36, 0x32, 0x16, 0x15, 0x01, 0xc0, 0x26, 0x1a, 0x55, 0x19, 0x12, + 0x12, 0x19, 0x55, 0x1a, 0x26, 0x1a, 0x26, 0x26, 0x1a, 0x2b, 0x01, 0x2a, 0x2b, 0x1a, 0x26, 0x26, + 0xaf, 0x55, 0x09, 0x0c, 0x15, 0x6b, 0x0d, 0xb7, 0x0c, 0x09, 0x55, 0x09, 0x0d, 0x6b, 0x15, 0x01, + 0x6a, 0x0c, 0x09, 0xfe, 0x80, 0x09, 0x0c, 0x0c, 0x12, 0x0c, 0x01, 0x56, 0x0c, 0x12, 0x0c, 0x01, + 0x00, 0x2b, 0x1a, 0x26, 0x11, 0x11, 0x26, 0x1a, 0x2b, 0x26, 0x1a, 0x6b, 0x1a, 0x26, 0x2a, 0x2a, + 0x2a, 0x2a, 0x26, 0x1a, 0x6b, 0x1a, 0x26, 0x40, 0x0d, 0x08, 0x3c, 0x13, 0x1c, 0x2b, 0x96, 0x08, + 0x0d, 0x15, 0x08, 0x0d, 0x0d, 0x08, 0x96, 0x2b, 0x1c, 0x13, 0x9a, 0x08, 0x0d, 0x0d, 0x08, 0x6b, + 0x09, 0x0c, 0x0c, 0x09, 0x55, 0x55, 0x09, 0x0c, 0x0c, 0x09, 0x00, 0x05, 0x00, 0x00, 0xff, 0xe8, + 0x01, 0xeb, 0x01, 0x96, 0x00, 0x08, 0x00, 0x16, 0x00, 0x1f, 0x00, 0x28, 0x00, 0x4c, 0x00, 0x00, + 0x25, 0x14, 0x06, 0x22, 0x26, 0x34, 0x36, 0x32, 0x16, 0x27, 0x15, 0x14, 0x16, 0x33, 0x15, 0x36, + 0x32, 0x17, 0x35, 0x32, 0x36, 0x3d, 0x01, 0x07, 0x22, 0x06, 0x14, 0x16, 0x32, 0x36, 0x34, 0x26, + 0x21, 0x22, 0x06, 0x14, 0x16, 0x32, 0x36, 0x34, 0x26, 0x07, 0x22, 0x27, 0x06, 0x22, 0x26, 0x27, + 0x06, 0x22, 0x27, 0x0e, 0x01, 0x22, 0x27, 0x06, 0x23, 0x22, 0x27, 0x1e, 0x01, 0x33, 0x32, 0x37, + 0x15, 0x14, 0x16, 0x32, 0x36, 0x3d, 0x01, 0x16, 0x33, 0x32, 0x36, 0x37, 0x06, 0x01, 0x40, 0x25, + 0x36, 0x25, 0x25, 0x36, 0x25, 0x80, 0x19, 0x12, 0x0b, 0x14, 0x0b, 0x12, 0x19, 0xeb, 0x1a, 0x26, + 0x26, 0x35, 0x25, 0x25, 0x01, 0x3b, 0x1b, 0x25, 0x25, 0x35, 0x26, 0x26, 0x1a, 0x16, 0x13, 0x11, + 0x22, 0x1f, 0x0a, 0x11, 0x2a, 0x11, 0x0a, 0x1f, 0x22, 0x11, 0x13, 0x16, 0x07, 0x0a, 0x11, 0x30, + 0x1b, 0x2a, 0x21, 0x0c, 0x12, 0x0c, 0x21, 0x2a, 0x1b, 0x30, 0x11, 0x0a, 0xa9, 0x1b, 0x25, 0x25, + 0x35, 0x26, 0x26, 0xd2, 0x15, 0x12, 0x19, 0x57, 0x02, 0x02, 0x57, 0x19, 0x12, 0x15, 0xc2, 0x25, + 0x35, 0x26, 0x26, 0x35, 0x25, 0x25, 0x35, 0x26, 0x26, 0x35, 0x25, 0x95, 0x08, 0x08, 0x0f, 0x0d, + 0x07, 0x07, 0x0d, 0x0f, 0x08, 0x08, 0x02, 0x15, 0x18, 0x1b, 0x30, 0x09, 0x0c, 0x0c, 0x09, 0x30, + 0x1b, 0x18, 0x15, 0x02, 0x00, 0x00, 0x00, 0x06, 0x00, 0x4e, 0x00, 0x03, 0x00, 0x01, 0x04, 0x09, + 0x00, 0x01, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x03, 0x00, 0x01, 0x04, 0x09, 0x00, 0x02, 0x00, 0x0e, + 0x00, 0x2a, 0x00, 0x03, 0x00, 0x01, 0x04, 0x09, 0x00, 0x03, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x03, + 0x00, 0x01, 0x04, 0x09, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x03, 0x00, 0x01, 0x04, 0x09, + 0x00, 0x05, 0x00, 0x16, 0x00, 0x38, 0x00, 0x03, 0x00, 0x01, 0x04, 0x09, 0x00, 0x06, 0x00, 0x26, + 0x00, 0x4e, 0x00, 0x4d, 0x00, 0x61, 0x00, 0x74, 0x00, 0x65, 0x00, 0x72, 0x00, 0x69, 0x00, 0x61, + 0x00, 0x6c, 0x00, 0x20, 0x00, 0x44, 0x00, 0x65, 0x00, 0x73, 0x00, 0x69, 0x00, 0x67, 0x00, 0x6e, + 0x00, 0x20, 0x00, 0x49, 0x00, 0x63, 0x00, 0x6f, 0x00, 0x6e, 0x00, 0x73, 0x00, 0x52, 0x00, 0x65, + 0x00, 0x67, 0x00, 0x75, 0x00, 0x6c, 0x00, 0x61, 0x00, 0x72, 0x00, 0x56, 0x00, 0x65, 0x00, 0x72, + 0x00, 0x73, 0x00, 0x69, 0x00, 0x6f, 0x00, 0x6e, 0x00, 0x20, 0x00, 0x31, 0x00, 0x2e, 0x00, 0x30, + 0x00, 0x4d, 0x00, 0x61, 0x00, 0x74, 0x00, 0x65, 0x00, 0x72, 0x00, 0x69, 0x00, 0x61, 0x00, 0x6c, + 0x00, 0x44, 0x00, 0x65, 0x00, 0x73, 0x00, 0x69, 0x00, 0x67, 0x00, 0x6e, 0x00, 0x49, 0x00, 0x63, + 0x00, 0x6f, 0x00, 0x6e, 0x00, 0x73, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x26, + 0x00, 0x28, 0x00, 0x02, 0x44, 0x46, 0x4c, 0x54, 0x00, 0x0e, 0x6c, 0x61, 0x74, 0x6e, 0x00, 0x18, + 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, +}; diff --git a/main/ui/ui_icons.h b/main/ui/ui_icons.h new file mode 100644 index 0000000..893a38d --- /dev/null +++ b/main/ui/ui_icons.h @@ -0,0 +1,45 @@ +/* + * GENERATED by tools/make_icon_font.py -- do not edit by hand. + * Subset of Material Design Icons (Apache-2.0), rasterised at runtime + * by LVGL tiny_ttf. Regeneration instructions are in the script. + */ + +#ifndef UI_ICONS_H +#define UI_ICONS_H + +#include + +/* The subset TTF; feed to lv_tiny_ttf_create_data(). */ +extern const unsigned char icon_font_data[]; +extern const size_t icon_font_data_size; + +/* Each icon is a single glyph as a UTF-8 string -- usable as label text + * in any font created from icon_font_data. */ +#define ICON_W_SUNNY "\xf3\xb0\x96\x99" /* U+F0599 */ +#define ICON_W_NIGHT "\xf3\xb0\x96\x94" /* U+F0594 */ +#define ICON_W_PARTLY "\xf3\xb0\x96\x95" /* U+F0595 */ +#define ICON_W_CLOUDY "\xf3\xb0\x96\x90" /* U+F0590 */ +#define ICON_W_RAINY "\xf3\xb0\x96\x97" /* U+F0597 */ +#define ICON_W_POURING "\xf3\xb0\x96\x96" /* U+F0596 */ +#define ICON_W_LIGHTNING "\xf3\xb0\x96\x93" /* U+F0593 */ +#define ICON_W_LIGHTNING_RAIN "\xf3\xb0\x99\xbe" /* U+F067E */ +#define ICON_W_SNOWY "\xf3\xb0\x96\x98" /* U+F0598 */ +#define ICON_W_SNOWY_RAIN "\xf3\xb0\x99\xbf" /* U+F067F */ +#define ICON_W_FOG "\xf3\xb0\x96\x91" /* U+F0591 */ +#define ICON_W_WINDY "\xf3\xb0\x96\x9d" /* U+F059D */ +#define ICON_W_HAIL "\xf3\xb0\x96\x92" /* U+F0592 */ +#define ICON_W_ALERT "\xf3\xb0\x97\x96" /* U+F05D6 */ +#define ICON_THERMOMETER "\xf3\xb0\x94\x8f" /* U+F050F */ +#define ICON_HOME_THERMO "\xf3\xb0\xbd\x95" /* U+F0F55 */ +#define ICON_WATER "\xf3\xb0\xb8\x8a" /* U+F0E0A */ +#define ICON_SUNRISE "\xf3\xb0\x96\x9c" /* U+F059C */ +#define ICON_SUNSET "\xf3\xb0\x96\x9b" /* U+F059B */ +#define ICON_GAUGE "\xf3\xb0\x8a\x9a" /* U+F029A */ +#define ICON_SOFA "\xf3\xb1\x95\xad" /* U+F156D */ +#define ICON_DINING "\xf3\xb0\xa9\xb0" /* U+F0A70 */ +#define ICON_FLOOR_LAMP "\xf3\xb0\xa3\x9d" /* U+F08DD */ +#define ICON_DESK_LAMP "\xf3\xb0\xa5\x9f" /* U+F095F */ +#define ICON_CHANDELIER "\xf3\xb1\x9e\x93" /* U+F1793 */ +#define ICON_STRING_LIGHTS "\xf3\xb1\x8a\xba" /* U+F12BA */ + +#endif diff --git a/main/ui/ui_manager.c b/main/ui/ui_manager.c index 5c29a32..d0f9be3 100644 --- a/main/ui/ui_manager.c +++ b/main/ui/ui_manager.c @@ -3,15 +3,22 @@ * * Five horizontal bands, sized to fill exactly one screen with no scrolling: * - * header 42px date, clock, current condition - * body 182px outdoor temperature card + a 3x2 grid of readings + * header 42px date, clock, current condition (icon + text) + * body 182px INDOOR temperature card + a 3x2 grid of readings * lights 116px one tile per HA_LIGHTS entry - * forecast 86px five day columns + * forecast 86px five day columns with condition icons * status 14px WiFi / poll failures * + * Indoor gets the big card because this is a thermostat: the number you act + * on is the one inside the house. Outdoor lives in the grid. + * * Nothing is hidden behind navigation: this is a wall panel that has to be * readable from across the room at a glance. * + * Icons are single glyphs from a subset MDI font rasterised by tiny_ttf -- + * see tools/make_icon_font.py. They are ordinary label text: colourable, + * resizable, and cheap. + * * Compiled into the desktop simulator as well as the firmware, so no driver, * socket or ESP-IDF calls belong here. See sim/README.md. */ @@ -24,12 +31,21 @@ #include "lvgl.h" #include "lvgl_port.h" +#include "ui_icons.h" #include "ui_manager.h" static const char *TAG = "ui_manager"; #define UI_LOCK_TIMEOUT_MS 1000 +/* + * How long a tile shows the state the user asked for before Home Assistant's + * word is taken over the user's. Long enough to cover command + confirm poll + * (typically <1s, but group entities settle slower); short enough that a + * genuinely failed command corrects itself rather than lying all day. + */ +#define OPTIMISTIC_HOLD_MS 5000 + /* * Band heights. * @@ -78,17 +94,25 @@ static const char *TAG = "ui_manager"; */ #define NO_VALUE "--" +/* Icon fonts, one per size in use. Created in ui_init from the embedded + * subset TTF; NULL if tiny_ttf fails, in which case icons render empty + * rather than as tofu. */ +static lv_font_t *icon_font_16; +static lv_font_t *icon_font_22; +static lv_font_t *icon_font_26; + static lv_obj_t *clock_label; static lv_obj_t *date_label; static lv_obj_t *cond_label; +static lv_obj_t *cond_icon; static lv_obj_t *temp_label; static lv_obj_t *temp_unit_label; -static lv_obj_t *outdoor_detail_label; +static lv_obj_t *indoor_detail_label; /* 3x2 reading grid: value and caption per cell. */ enum { - CELL_INDOOR = 0, + CELL_OUTDOOR = 0, CELL_WIND, CELL_RAIN, CELL_SUNRISE, @@ -100,18 +124,26 @@ static lv_obj_t *cell_value[CELL_COUNT]; static lv_obj_t *cell_sub[CELL_COUNT]; static lv_obj_t *light_tile[HA_LIGHT_COUNT]; +static lv_obj_t *light_icon[HA_LIGHT_COUNT]; static lv_obj_t *light_name[HA_LIGHT_COUNT]; static lv_obj_t *light_state[HA_LIGHT_COUNT]; static lv_obj_t *light_slider[HA_LIGHT_COUNT]; static lv_obj_t *fc_day[HA_FORECAST_DAYS]; -static lv_obj_t *fc_cond[HA_FORECAST_DAYS]; +static lv_obj_t *fc_icon[HA_FORECAST_DAYS]; static lv_obj_t *fc_temp[HA_FORECAST_DAYS]; static lv_obj_t *status_label; static ui_light_cmd_cb_t light_cb; +/* Per-room icons, riding in HA_LIGHT_LIST so order can never drift from the + * entity table. */ +#define LIGHT_ICON_X(id, label, dimmable, icon) icon, +static const char *const LIGHT_ICONS[HA_LIGHT_COUNT] = { + HA_LIGHT_LIST(LIGHT_ICON_X) +}; + /* * Last state pushed to the widgets, so ui_update() can skip a light tile whose * state and brightness are unchanged. Without this, every poll would restyle @@ -120,6 +152,68 @@ static ui_light_cmd_cb_t light_cb; static ha_light_status_t last_light[HA_LIGHT_COUNT]; static bool last_light_valid; +/* + * Optimistic state: what the user just asked for, applied to the tile + * immediately so a tap responds at touch speed instead of at poll speed. + * + * While a hold is active, poll data for that tile is ignored unless it agrees + * -- HA often reports the OLD state for a beat after a command (group + * entities especially), and applying that would visibly snap the tile back + * before flipping it again a second later. After OPTIMISTIC_HOLD_MS, HA wins + * unconditionally: if it still says off, the command genuinely failed and the + * tile should say so. + */ +typedef struct { + bool active; + uint32_t since; /* lv_tick timestamp */ + ha_light_state_t expect; +} light_hold_t; +static light_hold_t light_hold[HA_LIGHT_COUNT]; + +/* --------------------------------------------------------- condition icons */ + +typedef struct { + const char *slug; + const char *icon; + uint32_t colour; +} cond_icon_t; + +/* met.no slugs -> glyph + colour. Warm for sun, blue for water, dim for the + * grey stuff -- the forecast strip reads at a glance without the words. */ +static const cond_icon_t COND_ICONS[] = { + { "clear-night", ICON_W_NIGHT, COL_TEXT_DIM }, + { "cloudy", ICON_W_CLOUDY, COL_TEXT_DIM }, + { "exceptional", ICON_W_ALERT, 0xE74C3C }, + { "fog", ICON_W_FOG, COL_TEXT_DIM }, + { "hail", ICON_W_HAIL, COL_ACCENT }, + { "lightning", ICON_W_LIGHTNING, COL_ON }, + { "lightning-rainy", ICON_W_LIGHTNING_RAIN, COL_ON }, + { "partlycloudy", ICON_W_PARTLY, COL_ON }, + { "pouring", ICON_W_POURING, COL_ACCENT }, + { "rainy", ICON_W_RAINY, COL_ACCENT }, + { "snowy", ICON_W_SNOWY, COL_TEXT }, + { "snowy-rainy", ICON_W_SNOWY_RAIN, COL_ACCENT }, + { "sunny", ICON_W_SUNNY, COL_ON }, + { "windy", ICON_W_WINDY, COL_TEXT_DIM }, + { "windy-variant", ICON_W_WINDY, COL_TEXT_DIM }, +}; + +/* Unknown slug -> alert glyph in warning red: visibly "add me to the table", + * matching how ha_condition_label() passes unknown slugs through as text. */ +static const cond_icon_t COND_ICON_FALLBACK = { NULL, ICON_W_ALERT, 0xE74C3C }; + +static const cond_icon_t *cond_icon_lookup(const char *slug) +{ + if (slug != NULL) { + for (size_t i = 0; i < sizeof(COND_ICONS) / sizeof(COND_ICONS[0]); i++) { + if (strcmp(COND_ICONS[i].slug, slug) == 0) { + return &COND_ICONS[i]; + } + } + } + return &COND_ICON_FALLBACK; +} + /* ------------------------------------------------------------- primitives */ /* @@ -173,11 +267,51 @@ static lv_obj_t *label_create(lv_obj_t *parent, const char *text, return l; } +/* An icon is a one-glyph label in an icon font. With no font (tiny_ttf + * failed) the label is created empty, so layout stays put and nothing + * renders as a missing-glyph box. */ +static lv_obj_t *icon_create(lv_obj_t *parent, const char *glyph, + lv_font_t *font, uint32_t colour) +{ + lv_obj_t *l = lv_label_create(parent); + if (font != NULL) { + lv_obj_set_style_text_font(l, font, LV_PART_MAIN); + lv_label_set_text(l, glyph); + } else { + lv_label_set_text(l, ""); + } + lv_obj_set_style_text_color(l, lv_color_hex(colour), LV_PART_MAIN); + return l; +} + /* ------------------------------------------------------------ light tiles */ +static void light_tile_update(int idx, const ha_light_status_t *st); + +/* + * Apply the state the user just requested, before Home Assistant has + * confirmed it. The tile flips at touch speed; the hold keeps the next + * poll's possibly-stale data from snapping it back. See light_hold_t. + */ +static void light_apply_optimistic(int idx, ha_light_status_t st) +{ + light_hold[idx].active = true; + light_hold[idx].since = lv_tick_get(); + light_hold[idx].expect = st.state; + + last_light[idx] = st; + last_light_valid = true; + + light_tile_update(idx, &st); +} + /* * Both handlers run on the LVGL task with the lock held. They only forward to * light_cb, which must not block -- see ui_manager.h. + * + * LV_EVENT_CLICKED fires on RELEASE (press + release inside the object), so a + * tap commits when the finger lifts, and a finger that slides off the tile + * commits nothing. */ static void light_tile_clicked(lv_event_t *e) { @@ -195,13 +329,26 @@ static void light_tile_clicked(lv_event_t *e) const bool turn_on = !(last_light_valid && last_light[idx].state == HA_LIGHT_ON); + /* + * Optimistic brightness: keep the last known level if we have one (HA + * restores the previous level on a plain turn_on), else -1 and the tile + * says "On" without a percentage until the poll fills it in. + */ + ha_light_status_t expect = { + .state = turn_on ? HA_LIGHT_ON : HA_LIGHT_OFF, + .brightness = (turn_on && last_light[idx].brightness > 0) + ? last_light[idx].brightness + : -1, + }; + light_apply_optimistic(idx, expect); + /* -1: a tap is on/off only. Brightness is the slider's job, and sending * the current brightness here would fight a user who dimmed the light * elsewhere. */ light_cb(idx, turn_on, -1); } -static void light_slider_changed(lv_event_t *e) +static void light_slider_released(lv_event_t *e) { const int idx = (int)(intptr_t)lv_event_get_user_data(e); if (light_cb == NULL || idx < 0 || idx >= HA_LIGHT_COUNT) { @@ -214,11 +361,14 @@ static void light_slider_changed(lv_event_t *e) /* Dragging to zero means off, not "on at zero brightness" -- HA accepts * brightness 0 but leaves the light on, which looks like a bug. */ if (pct <= 0) { + light_apply_optimistic(idx, (ha_light_status_t){ HA_LIGHT_OFF, -1 }); light_cb(idx, false, -1); return; } - light_cb(idx, true, (int16_t)((pct * 255) / 100)); + const int16_t brightness = (int16_t)((pct * 255) / 100); + light_apply_optimistic(idx, (ha_light_status_t){ HA_LIGHT_ON, brightness }); + light_cb(idx, true, brightness); } static void light_tile_build(lv_obj_t *parent, int idx, int32_t w, int32_t h) @@ -233,24 +383,29 @@ static void light_tile_build(lv_obj_t *parent, int idx, int32_t w, int32_t h) light_tile[idx] = tile; /* - * Explicit alignment rather than flex. A flex column packs the three - * children against the top and leaves the rest of a 116px tile empty; the - * slider wants to sit at the bottom where a thumb reaches it. + * Explicit alignment rather than flex. A flex column packs the children + * against the top and leaves the rest of a 116px tile empty; the slider + * wants to sit at the bottom where a thumb reaches it. + * + * The icon gets its own row rather than sharing the name's. In a ~125px + * tile an icon beside the name leaves ~79px of text -- every room name + * truncated to "Main R...". Vertically there is space to burn; + * horizontally there is none. */ + light_icon[idx] = icon_create(tile, LIGHT_ICONS[idx], icon_font_22, COL_TEXT_DIM); + lv_obj_align(light_icon[idx], LV_ALIGN_TOP_LEFT, 0, -2); + light_name[idx] = label_create(tile, cfg->label, &lv_font_montserrat_16, COL_TEXT); lv_label_set_long_mode(light_name[idx], LV_LABEL_LONG_DOT); - lv_obj_set_width(light_name[idx], LV_PCT(100)); - /* - * An explicit one-line height is what actually forces truncation. With a - * content-sized height LV_LABEL_LONG_DOT has room to grow, so it wraps to - * a second line first and only then adds dots -- and that second line - * lands on top of the state label below. - */ - lv_obj_set_height(light_name[idx], lv_font_get_line_height(&lv_font_montserrat_16)); - lv_obj_align(light_name[idx], LV_ALIGN_TOP_LEFT, 0, 0); + /* The explicit one-line height is what actually forces truncation: with + * a content-sized height LV_LABEL_LONG_DOT wraps to a second line before + * it adds dots -- onto the state label below. */ + lv_obj_set_size(light_name[idx], w - 20, + lv_font_get_line_height(&lv_font_montserrat_16)); + lv_obj_align(light_name[idx], LV_ALIGN_TOP_LEFT, 0, 24); light_state[idx] = label_create(tile, "--", &lv_font_montserrat_14, COL_TEXT_DIM); - lv_obj_align(light_state[idx], LV_ALIGN_TOP_LEFT, 0, 24); + lv_obj_align(light_state[idx], LV_ALIGN_TOP_LEFT, 0, 46); if (cfg->dimmable) { lv_obj_t *s = lv_slider_create(tile); @@ -274,9 +429,13 @@ static void light_tile_build(lv_obj_t *parent, int idx, int32_t w, int32_t h) lv_obj_set_style_bg_color(s, lv_color_hex(COL_TEXT_DIM), LV_PART_KNOB); lv_obj_set_style_pad_all(s, 2, LV_PART_KNOB); - /* RELEASED, not VALUE_CHANGED: the latter fires continuously while a - * finger drags, which would queue a command per pixel. */ - lv_obj_add_event_cb(s, light_slider_changed, LV_EVENT_RELEASED, + /* + * RELEASED only: the command is sent when the finger lifts, once. + * VALUE_CHANGED fires per pixel of drag and would queue a command + * storm; it is also why ui_update never writes to a pressed slider + * -- nothing may move the knob out from under the finger. + */ + lv_obj_add_event_cb(s, light_slider_released, LV_EVENT_RELEASED, (void *)(intptr_t)idx); light_slider[idx] = s; } else { @@ -305,6 +464,13 @@ static void light_tile_update(int idx, const ha_light_status_t *st) lv_color_hex(dead ? COL_TEXT_FAINT : COL_TEXT), LV_PART_MAIN); + /* The room icon is the strongest "which lights are on" signal in the + * band: amber when lit, dim when off, faint when unreachable. */ + lv_obj_set_style_text_color(light_icon[idx], + lv_color_hex(on ? COL_ON + : (dead ? COL_TEXT_FAINT : COL_TEXT_DIM)), + LV_PART_MAIN); + char buf[24]; if (dead) { snprintf(buf, sizeof(buf), "unavailable"); @@ -342,6 +508,43 @@ static void light_tile_update(int idx, const ha_light_status_t *st) } } +/* + * True while this tile must not be touched by poll data: a finger is on it, + * or an optimistic hold is active and unresolved. + */ +static bool light_tile_busy(int idx, const ha_light_status_t *incoming) +{ + /* Never move anything under a finger. The slider check is the one that + * keeps the knob pinned where the user is holding it. */ + if (lv_obj_has_state(light_tile[idx], LV_STATE_PRESSED)) { + return true; + } + if (light_slider[idx] != NULL && + lv_obj_has_state(light_slider[idx], LV_STATE_PRESSED)) { + return true; + } + + light_hold_t *hold = &light_hold[idx]; + if (!hold->active) { + return false; + } + + /* HA agrees with what the user asked for: the hold has done its job. + * Fall through so the poll's real brightness lands on the tile. */ + if (incoming->state == hold->expect) { + hold->active = false; + return false; + } + + /* Still disagreeing after the window: the command failed, HA wins. */ + if (lv_tick_elaps(hold->since) >= OPTIMISTIC_HOLD_MS) { + hold->active = false; + return false; + } + + return true; /* disagreement inside the window: keep the user's state */ +} + /* ------------------------------------------------------------------ build */ static void build_header(lv_obj_t *scr) @@ -356,8 +559,13 @@ static void build_header(lv_obj_t *scr) clock_label = label_create(bar, "--:--", &lv_font_montserrat_24, COL_TEXT); lv_obj_align(clock_label, LV_ALIGN_CENTER, 0, 0); + /* Icon right-most; the text hangs off its left edge and is re-anchored + * on every update because its width changes with the words. */ + cond_icon = icon_create(bar, ICON_W_CLOUDY, icon_font_26, COL_TEXT_DIM); + lv_obj_align(cond_icon, LV_ALIGN_RIGHT_MID, 0, 0); + cond_label = label_create(bar, "--", &lv_font_montserrat_20, COL_ACCENT); - lv_obj_align(cond_label, LV_ALIGN_RIGHT_MID, 0, 0); + lv_obj_align_to(cond_label, cond_icon, LV_ALIGN_OUT_LEFT_MID, -8, 0); } static void build_body(lv_obj_t *scr) @@ -371,14 +579,17 @@ static void build_body(lv_obj_t *scr) lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW); lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE); - /* --- outdoor card --- */ + /* --- indoor card: the number a thermostat exists to show --- */ lv_obj_t *card = panel_create(row, 296, LV_PCT(100), COL_PANEL); lv_obj_set_style_pad_all(card, 14, LV_PART_MAIN); - lv_obj_t *heading = label_create(card, "OUTDOOR", &lv_font_montserrat_14, COL_TEXT_DIM); + lv_obj_t *heading = label_create(card, "INDOOR", &lv_font_montserrat_14, COL_TEXT_DIM); lv_obj_set_style_text_letter_space(heading, 2, LV_PART_MAIN); lv_obj_align(heading, LV_ALIGN_TOP_LEFT, 0, 0); + lv_obj_t *card_icon = icon_create(card, ICON_HOME_THERMO, icon_font_26, COL_TEXT_FAINT); + lv_obj_align(card_icon, LV_ALIGN_TOP_RIGHT, 0, -2); + /* Centred vertically rather than pinned under the heading: the card is * 182px and a 48px number at the top leaves an obvious hole in the middle * of the most important element on the screen. */ @@ -390,8 +601,8 @@ static void build_body(lv_obj_t *scr) temp_unit_label = label_create(card, "°F", &lv_font_montserrat_20, COL_TEXT_DIM); lv_obj_align_to(temp_unit_label, temp_label, LV_ALIGN_OUT_RIGHT_TOP, 4, 8); - outdoor_detail_label = label_create(card, "--", &lv_font_montserrat_16, COL_TEXT_DIM); - lv_obj_align(outdoor_detail_label, LV_ALIGN_BOTTOM_LEFT, 0, 0); + indoor_detail_label = label_create(card, "--", &lv_font_montserrat_16, COL_TEXT_DIM); + lv_obj_align(indoor_detail_label, LV_ALIGN_BOTTOM_LEFT, 0, 0); /* --- 3x2 reading grid --- */ lv_obj_t *grid = lv_obj_create(row); @@ -405,7 +616,11 @@ static void build_body(lv_obj_t *scr) lv_obj_remove_flag(grid, LV_OBJ_FLAG_SCROLLABLE); static const char *const captions[CELL_COUNT] = { - "INDOOR", "WIND", "RAIN TODAY", "SUNRISE", "SUNSET", "PRESSURE", + "OUTDOOR", "WIND", "RAIN TODAY", "SUNRISE", "SUNSET", "PRESSURE", + }; + static const char *const cell_icons[CELL_COUNT] = { + ICON_THERMOMETER, ICON_W_WINDY, ICON_WATER, + ICON_SUNRISE, ICON_SUNSET, ICON_GAUGE, }; /* Two rows of cells plus one gap must equal BAND_BODY_H exactly, or the @@ -420,6 +635,9 @@ static void build_body(lv_obj_t *scr) lv_obj_set_style_text_letter_space(cap, 1, LV_PART_MAIN); lv_obj_align(cap, LV_ALIGN_TOP_LEFT, 0, 0); + lv_obj_t *ic = icon_create(cell, cell_icons[i], icon_font_16, COL_TEXT_FAINT); + lv_obj_align(ic, LV_ALIGN_TOP_RIGHT, 0, -1); + cell_value[i] = label_create(cell, NO_VALUE, &lv_font_montserrat_24, COL_TEXT); lv_obj_align(cell_value[i], LV_ALIGN_LEFT_MID, 0, 4); @@ -463,13 +681,15 @@ static void build_forecast(lv_obj_t *scr) for (int i = 0; i < HA_FORECAST_DAYS; i++) { lv_obj_t *col = panel_create(row, col_w, LV_PCT(100), COL_PANEL); + lv_obj_set_style_pad_all(col, 6, LV_PART_MAIN); lv_obj_set_flex_flow(col, LV_FLEX_FLOW_COLUMN); - lv_obj_set_flex_align(col, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, + lv_obj_set_flex_align(col, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - lv_obj_set_style_pad_row(col, 2, LV_PART_MAIN); fc_day[i] = label_create(col, "--", &lv_font_montserrat_16, COL_TEXT); - fc_cond[i] = label_create(col, "--", &lv_font_montserrat_12, COL_TEXT_DIM); + /* The icon replaces the abbreviated condition word: it says the same + * thing faster, in a column too narrow for real words. */ + fc_icon[i] = icon_create(col, ICON_W_CLOUDY, icon_font_26, COL_TEXT_DIM); fc_temp[i] = label_create(col, NO_VALUE, &lv_font_montserrat_16, COL_ACCENT); } } @@ -481,35 +701,40 @@ static void update_body(const ha_data_t *d) char buf[48]; char sub[48]; - /* Outdoor card. Rounded with lroundf, not truncation: below 0 degF a cast + /* Indoor card. Rounded with lroundf, not truncation: below 0 degF a cast * rounds toward zero and turns -10.6 into -10. */ - if (d->outdoor_temp_f.valid) { - snprintf(buf, sizeof(buf), "%d", (int)lroundf(d->outdoor_temp_f.value)); + if (d->indoor_temp_f.valid) { + snprintf(buf, sizeof(buf), "%.1f", d->indoor_temp_f.value); } else { snprintf(buf, sizeof(buf), NO_VALUE); } label_set_if_changed(temp_label, buf); lv_obj_align_to(temp_unit_label, temp_label, LV_ALIGN_OUT_RIGHT_TOP, 4, 8); - if (d->outdoor_humidity.valid && d->dew_point_f.valid) { - snprintf(buf, sizeof(buf), "%.0f%% humidity dew %.0f°", - d->outdoor_humidity.value, d->dew_point_f.value); - } else if (d->outdoor_humidity.valid) { - snprintf(buf, sizeof(buf), "%.0f%% humidity", d->outdoor_humidity.value); + if (d->indoor_humidity.valid) { + snprintf(buf, sizeof(buf), "%.0f%% humidity", d->indoor_humidity.value); } else { snprintf(buf, sizeof(buf), NO_VALUE); } - label_set_if_changed(outdoor_detail_label, buf); + label_set_if_changed(indoor_detail_label, buf); - /* Indoor */ - format_value(&d->indoor_temp_f, "%.1f°", buf, sizeof(buf)); - label_set_if_changed(cell_value[CELL_INDOOR], buf); - if (d->indoor_humidity.valid) { - snprintf(sub, sizeof(sub), "%.0f%% humidity", d->indoor_humidity.value); + /* Outdoor cell */ + if (d->outdoor_temp_f.valid) { + snprintf(buf, sizeof(buf), "%d°", (int)lroundf(d->outdoor_temp_f.value)); + } else { + snprintf(buf, sizeof(buf), NO_VALUE); + } + label_set_if_changed(cell_value[CELL_OUTDOOR], buf); + + if (d->outdoor_humidity.valid && d->dew_point_f.valid) { + snprintf(sub, sizeof(sub), "%.0f%% dew %.0f°", + d->outdoor_humidity.value, d->dew_point_f.value); + } else if (d->outdoor_humidity.valid) { + snprintf(sub, sizeof(sub), "%.0f%% humidity", d->outdoor_humidity.value); } else { sub[0] = '\0'; } - label_set_if_changed(cell_sub[CELL_INDOOR], sub); + label_set_if_changed(cell_sub[CELL_OUTDOOR], sub); /* * Wind. The station anemometer has been offline since 2026-07-27, so this @@ -568,14 +793,20 @@ static void update_forecast(const ha_data_t *d) for (int i = 0; i < HA_FORECAST_DAYS; i++) { if (i >= d->forecast_count) { label_set_if_changed(fc_day[i], "--"); - label_set_if_changed(fc_cond[i], ""); + label_set_if_changed(fc_icon[i], ""); label_set_if_changed(fc_temp[i], NO_VALUE); continue; } const ha_forecast_day_t *f = &d->forecast[i]; label_set_if_changed(fc_day[i], f->day); - label_set_if_changed(fc_cond[i], ha_condition_short(f->condition)); + + if (icon_font_26 != NULL) { + const cond_icon_t *ci = cond_icon_lookup(f->condition); + label_set_if_changed(fc_icon[i], ci->icon); + lv_obj_set_style_text_color(fc_icon[i], lv_color_hex(ci->colour), + LV_PART_MAIN); + } if (f->high_f.valid && f->low_f.valid) { snprintf(buf, sizeof(buf), "%d / %d", @@ -602,12 +833,27 @@ void ui_update(const ha_data_t *data) label_set_if_changed(clock_label, data->clock[0] ? data->clock : "--:--"); label_set_if_changed(date_label, data->date[0] ? data->date : "--"); + label_set_if_changed(cond_label, ha_condition_label(data->condition)); + if (icon_font_26 != NULL) { + const cond_icon_t *ci = cond_icon_lookup( + data->condition[0] ? data->condition : NULL); + label_set_if_changed(cond_icon, ci->icon); + lv_obj_set_style_text_color(cond_icon, lv_color_hex(ci->colour), LV_PART_MAIN); + } + /* The label's width changed with its words; re-hang it off the icon. */ + lv_obj_align_to(cond_label, cond_icon, LV_ALIGN_OUT_LEFT_MID, -8, 0); update_body(data); update_forecast(data); for (int i = 0; i < HA_LIGHT_COUNT; i++) { + /* A finger on the tile, or an optimistic hold still waiting for HA + * to catch up: poll data keeps its hands off. */ + if (light_tile_busy(i, &data->lights[i])) { + continue; + } + /* Restyling a tile is many style writes; skip the ones that did not * move. */ if (last_light_valid && @@ -671,6 +917,18 @@ void ui_init(void) return; } + /* + * One tiny_ttf instance per size in use. The data is the ~6KB subset MDI + * font from tools/make_icon_font.py; a failure here degrades to iconless + * labels rather than stopping the UI. + */ + icon_font_16 = lv_tiny_ttf_create_data(icon_font_data, icon_font_data_size, 16); + icon_font_22 = lv_tiny_ttf_create_data(icon_font_data, icon_font_data_size, 22); + icon_font_26 = lv_tiny_ttf_create_data(icon_font_data, icon_font_data_size, 26); + if (icon_font_16 == NULL || icon_font_22 == NULL || icon_font_26 == NULL) { + ESP_LOGW(TAG, "tiny_ttf icon font failed; continuing without icons"); + } + lv_obj_t *scr = lv_screen_active(); lv_obj_set_style_bg_color(scr, lv_color_hex(COL_BG), LV_PART_MAIN); lv_obj_set_style_pad_all(scr, SCREEN_PAD, LV_PART_MAIN); @@ -693,6 +951,7 @@ void ui_init(void) lvgl_port_unlock(); - ESP_LOGI(TAG, "UI initialized (%d lights, %d forecast days)", - HA_LIGHT_COUNT, HA_FORECAST_DAYS); + ESP_LOGI(TAG, "UI initialized (%d lights, %d forecast days, icons %s)", + HA_LIGHT_COUNT, HA_FORECAST_DAYS, + icon_font_22 != NULL ? "on" : "OFF"); } diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 0202c60..4e646f5 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -92,3 +92,5 @@ CONFIG_LV_FONT_MONTSERRAT_48=y # --- Main task needs headroom for driver init --- CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192 + +CONFIG_LV_USE_TINY_TTF=y diff --git a/sim/CMakeLists.txt b/sim/CMakeLists.txt index 7879b95..0f094fb 100644 --- a/sim/CMakeLists.txt +++ b/sim/CMakeLists.txt @@ -66,6 +66,7 @@ add_executable(thermostat_sim # Anything added here must stay free of hardware access -- drivers and # HTTP transport stay on the device side. ${FW_ROOT}/main/ui/ui_manager.c + ${FW_ROOT}/main/ui/icon_font.c ${FW_ROOT}/main/api/ha_data.c ${CJSON_DIR}/cJSON.c diff --git a/sim/lv_conf.h b/sim/lv_conf.h index 35f4f7a..76cc9e1 100644 --- a/sim/lv_conf.h +++ b/sim/lv_conf.h @@ -1036,7 +1036,7 @@ #endif /** Built-in TTF decoder */ -#define LV_USE_TINY_TTF 0 +#define LV_USE_TINY_TTF 1 #if LV_USE_TINY_TTF /* Enable loading TTF data from files */ #define LV_TINY_TTF_FILE_SUPPORT 0 diff --git a/tools/make_icon_font.py b/tools/make_icon_font.py new file mode 100644 index 0000000..0693dac --- /dev/null +++ b/tools/make_icon_font.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 +""" +Generate the icon font the UI uses: main/ui/icon_font.c + main/ui/ui_icons.h. + +The icons are a ~30-glyph subset of Material Design Icons (the same icon set +Home Assistant uses, so the panel and the HA dashboard agree on what "partly +cloudy" looks like). The subset TTF is embedded as a C array and rasterised at +runtime by LVGL's tiny_ttf, which means: + + - icons are just label text in a special font -- colourable, sizeable, + no per-icon bitmaps to regenerate when a size changes; + - the whole set costs a few KB of flash, not a 1.3MB font. + +Inputs (cached copies are fine; only regeneration needs them): + + curl -sfL -o /tmp/mdi.css https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css + curl -sfL -o /tmp/mdi.ttf https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/fonts/materialdesignicons-webfont.ttf + python3 tools/make_icon_font.py /tmp/mdi.css /tmp/mdi.ttf + +Codepoints are resolved from the CSS at generation time, never hardcoded -- +MDI has re-assigned codepoints between major versions, and a stale codepoint +renders as the wrong icon with no error anywhere. + +The generated files are committed, so a normal build needs neither network +nor fonttools. +""" + +import io +import re +import sys +from pathlib import Path + +try: + from fontTools import subset +except ImportError: + sys.exit("fonttools is required: python3 -m venv /tmp/fontenv && " + "/tmp/fontenv/bin/pip install fonttools (then run with that python)") + +REPO = Path(__file__).resolve().parent.parent +OUT_C = REPO / "main" / "ui" / "icon_font.c" +OUT_H = REPO / "main" / "ui" / "ui_icons.h" + +# macro name -> candidate MDI icon names, first match wins. Candidates exist +# because MDI occasionally renames icons between versions. +ICONS = { + # met.no condition slugs (see ha_condition_* in ha_data.c) + "ICON_W_SUNNY": ["weather-sunny"], + "ICON_W_NIGHT": ["weather-night"], + "ICON_W_PARTLY": ["weather-partly-cloudy"], + "ICON_W_CLOUDY": ["weather-cloudy"], + "ICON_W_RAINY": ["weather-rainy"], + "ICON_W_POURING": ["weather-pouring"], + "ICON_W_LIGHTNING": ["weather-lightning"], + "ICON_W_LIGHTNING_RAIN":["weather-lightning-rainy"], + "ICON_W_SNOWY": ["weather-snowy"], + "ICON_W_SNOWY_RAIN": ["weather-snowy-rainy"], + "ICON_W_FOG": ["weather-fog"], + "ICON_W_WINDY": ["weather-windy"], + "ICON_W_HAIL": ["weather-hail"], + "ICON_W_ALERT": ["alert-circle-outline"], + + # grid cells + "ICON_THERMOMETER": ["thermometer"], + "ICON_HOME_THERMO": ["home-thermometer-outline", "home-thermometer"], + "ICON_WATER": ["water-outline", "water"], + "ICON_SUNRISE": ["weather-sunset-up"], + "ICON_SUNSET": ["weather-sunset-down"], + "ICON_GAUGE": ["gauge"], + + # light tiles (order-independent; ha_config.h references these by name) + "ICON_SOFA": ["sofa-outline", "sofa"], + "ICON_DINING": ["silverware-fork-knife", "silverware"], + "ICON_FLOOR_LAMP": ["floor-lamp"], + "ICON_DESK_LAMP": ["desk-lamp"], + "ICON_CHANDELIER": ["chandelier", "ceiling-light"], + "ICON_STRING_LIGHTS": ["string-lights", "outdoor-lamp"], +} + + +def resolve_codepoints(css: str) -> dict[str, int]: + """macro -> unicode codepoint, via the .mdi-::before rules.""" + cp_by_name: dict[str, int] = {} + for m in re.finditer(r'\.mdi-([a-z0-9-]+)::?before\{content:"\\([0-9A-Fa-f]+)"\}', css): + cp_by_name[m.group(1)] = int(m.group(2), 16) + + if not cp_by_name: + sys.exit("No codepoint rules found in the CSS -- wrong file?") + + resolved: dict[str, int] = {} + for macro, candidates in ICONS.items(): + for name in candidates: + if name in cp_by_name: + resolved[macro] = cp_by_name[name] + print(f" {macro:<24} mdi-{name:<24} U+{cp_by_name[name]:05X}") + break + else: + sys.exit(f"None of {candidates} exist in this MDI release " + f"(wanted for {macro}) -- pick another candidate") + return resolved + + +def subset_font(ttf_path: Path, codepoints: list[int]) -> bytes: + opts = subset.Options() + opts.flavor = None # plain TTF out + opts.desubroutinize = True + font = subset.load_font(str(ttf_path), opts) + ss = subset.Subsetter(opts) + ss.populate(unicodes=codepoints) + ss.subset(font) + buf = io.BytesIO() + font.save(buf) + return buf.getvalue() + + +def utf8_escape(cp: int) -> str: + return "".join(f"\\x{b:02x}" for b in chr(cp).encode("utf-8")) + + +def main() -> int: + if len(sys.argv) != 3: + sys.exit(__doc__) + css_path, ttf_path = Path(sys.argv[1]), Path(sys.argv[2]) + + print("resolving codepoints from CSS:") + resolved = resolve_codepoints(css_path.read_text()) + + data = subset_font(ttf_path, sorted(resolved.values())) + print(f"\nsubset TTF: {len(data)} bytes ({len(resolved)} glyphs)") + + guard_note = ( + "/*\n" + " * GENERATED by tools/make_icon_font.py -- do not edit by hand.\n" + " * Subset of Material Design Icons (Apache-2.0), rasterised at runtime\n" + " * by LVGL tiny_ttf. Regeneration instructions are in the script.\n" + " */\n" + ) + + lines = [guard_note, "#include \n"] + lines.append(f"const size_t icon_font_data_size = {len(data)};\n") + lines.append("const unsigned char icon_font_data[] = {") + for i in range(0, len(data), 16): + chunk = ", ".join(f"0x{b:02x}" for b in data[i:i + 16]) + lines.append(f" {chunk},") + lines.append("};\n") + OUT_C.write_text("\n".join(lines)) + + h = [guard_note] + h.append("#ifndef UI_ICONS_H") + h.append("#define UI_ICONS_H\n") + h.append("#include \n") + h.append("/* The subset TTF; feed to lv_tiny_ttf_create_data(). */") + h.append("extern const unsigned char icon_font_data[];") + h.append("extern const size_t icon_font_data_size;\n") + h.append("/* Each icon is a single glyph as a UTF-8 string -- usable as label text") + h.append(" * in any font created from icon_font_data. */") + for macro, cp in resolved.items(): + h.append(f'#define {macro:<24} "{utf8_escape(cp)}" /* U+{cp:05X} */') + h.append("\n#endif") + OUT_H.write_text("\n".join(h) + "\n") + + print(f"wrote {OUT_C.relative_to(REPO)}") + print(f"wrote {OUT_H.relative_to(REPO)}") + return 0 + + +if __name__ == "__main__": + sys.exit(main())