Updated icons, buttons, and display; issues still with Main Room button, and sliders.
This commit is contained in:
1 parent
b5d6b4a897
commit
45998b6176
15 files changed
+972
-76
No files matched your search
@@ -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()`.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
+12
-7
@@ -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.
|
||||
@@ -66,11 +71,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)
|
||||
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
|
||||
+3
-2
@@ -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)
|
||||
};
|
||||
|
||||
+1
-1
@@ -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))
|
||||
|
||||
/*
|
||||
|
||||
@@ -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",
|
||||
|
||||
+16
@@ -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) {
|
||||
|
||||
@@ -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 <stddef.h>
|
||||
|
||||
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,
|
||||
};
|
||||
@@ -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 <stddef.h>
|
||||
|
||||
/* 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
|
||||
+311
-52
@@ -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");
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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-<name>::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 <stddef.h>\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 <stddef.h>\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())
|
||||
Reference in new issue
Block a user