First commit
This commit is contained in:
commit
61c37de884
171 files changed
+9201
No files matched your search
+2932
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,95 @@
|
||||
# Modular screens (beta)
|
||||
|
||||
Goal: pick which carousel screens are compiled in, straight from the YAML - comment a
|
||||
line in `packages:` and the screen is gone from the firmware (and the flash it used).
|
||||
|
||||
This is a **beta** layout on the `beta` branch. It needs to be compiled on a device
|
||||
(there is no local validation here). Until it is verified, `main` keeps the single-file
|
||||
config.
|
||||
|
||||
## How you choose screens
|
||||
|
||||
You keep one thin file locally (`guition-va.yaml`, repo root) that pulls the core and the
|
||||
screens from GitHub as a remote package. Pick screens in its `files:` list:
|
||||
|
||||
```yaml
|
||||
packages:
|
||||
core:
|
||||
url: https://github.com/MichalZaniewicz/esphome-guition-jc3636k718c-va
|
||||
ref: beta
|
||||
files:
|
||||
- base/core.yaml # always on (clock + controls + settings)
|
||||
- base/screens/timer.yaml
|
||||
- base/screens/cool-cars.yaml
|
||||
- base/screens/space-wars.yaml # comment out to drop Space Wars
|
||||
refresh: 0s
|
||||
```
|
||||
|
||||
Remove/comment a `base/screens/*.yaml` line -> that screen's page, scripts, globals and
|
||||
game tick are not compiled, and its carousel slot disappears. Nothing else to edit.
|
||||
|
||||
## How it stays decoupled (the contract)
|
||||
|
||||
ESPHome merges package lists (globals/scripts/interval/lvgl.pages/esphome.on_boot all
|
||||
concatenate), so each screen file is self-contained and the core never names a screen's
|
||||
symbols. The glue is a few plain globals in the core:
|
||||
|
||||
- `g_base` (int) - id of the current carousel screen. Fixed ids: 0=clock, 1=player,
|
||||
2=timer, 3=cars, 4=space (an id never changes, even if a screen is absent).
|
||||
- `g_order[12]`, `g_order_n` - carousel order, built at boot. Core seeds clock+player;
|
||||
each screen package appends its id in an `esphome.on_boot` step (priority sets the
|
||||
position). Swipe left/right just steps through `g_order` and wraps.
|
||||
- `g_nav_req` (bool) + `g_nav_anim` (int: 0=right 1=left 2=top 3=bottom) - core sets
|
||||
these on a screen change; each screen package owns a small handler that, when
|
||||
`g_base == <my id>`, runs its own `lvgl.page.show` with the matching animation and
|
||||
clears `g_nav_req`. Core only `page.show`s its own (clock/player).
|
||||
- `g_knob_capture` (bool) + `g_knob_delta` (int) - when a screen wants the knob (game
|
||||
running, timer idle), it sets `g_knob_capture=true`; the core knob handler then just
|
||||
accumulates +/-1 into `g_knob_delta` instead of changing volume. The screen reads and
|
||||
zeroes `g_knob_delta` in its own tick. Core never names a game variable.
|
||||
- Per-screen **settings registry** (`g_wgrp_*` + `g_wopt_*`) - a screen declares its own
|
||||
Settings -> Widgets group and options in `on_boot`: append a group name to
|
||||
`g_wgrp_labels`, then append each option (label + kind 0=toggle / 1=action + a `bool*`
|
||||
to its own global) to `g_wopt_*`. The core renders them generically (modes 30 options /
|
||||
31 confirm). The option globals live in the screen package, not the core.
|
||||
- `g_factory_reset` (bool, persists a reboot) - Settings -> System -> Factory reset sets
|
||||
it; each game package clears its own score globals when it sees the flag in its tick.
|
||||
|
||||
So a screen package contributes: its `globals`, its `script`s, its game-tick `interval`,
|
||||
its `lvgl.pages` entry, an `on_boot` step to register its carousel id (and any Settings
|
||||
group/options), and one nav handler. It reads/writes only the shared core globals above.
|
||||
|
||||
## Status
|
||||
|
||||
- [x] core refactor: data-driven carousel (`g_order`), nav via `g_nav_req`, knob via
|
||||
`g_knob_capture`/`g_knob_delta`.
|
||||
- [x] extract Space Wars -> `base/screens/space-wars.yaml`
|
||||
- [x] extract Cool Cars -> `base/screens/cool-cars.yaml`
|
||||
- [x] extract Player -> `base/screens/player.yaml` (volume + overlay stay in core)
|
||||
- [x] timer carousel-screen toggle -> `base/screens/timer.yaml` (voice timers/alarm/badge stay in core)
|
||||
- [x] new Weather screen -> `base/screens/weather.yaml` (radial 7-day dial, knob highlights the day;
|
||||
forecast from the HA helper `base/screens/weather.ha-helper.yaml`)
|
||||
- [x] new Thermostat screen -> `base/screens/thermostat.yaml` (climate.* dial; knob sets the target with a
|
||||
debounced commit, tap toggles on/off, accent colour by heating/cooling/idle)
|
||||
- [x] settings ownership: home-screen widget toggles moved to a "Home" submenu (core); "Display"
|
||||
keeps only global brightness/night/screen-off
|
||||
- [x] carousel order via the `screen_order` substitution; screens register `g_present`,
|
||||
absent ones are skipped (clock kept as a safety fallback)
|
||||
- [x] self-contained screen settings: each screen declares its Widgets group + options
|
||||
(toggles/actions) and owns the backing globals; the core renders them generically
|
||||
(`g_wgrp_*` / `g_wopt_*`, modes 30/31). Game scores + reset live in the game packages.
|
||||
- [x] verified on device (compiles; screens toggle from `files:`, order from `screen_order`,
|
||||
settings list adapts)
|
||||
- [x] merged to `main` (stable); `beta` is the active dev branch
|
||||
|
||||
## Adding a new screen (recipe)
|
||||
|
||||
A screen package adds: its `globals` / `script`s / game-tick `interval` / `lvgl.pages`
|
||||
entry; an `esphome.on_boot` step that sets `id(g_present)[<id>] = true`; a 50ms nav
|
||||
handler that shows its page when `g_nav_req && g_base == <id>`. To give it a carousel
|
||||
slot, add its name to the core `name -> id` map in the order builder and to `screen_order`.
|
||||
|
||||
To add Settings, register a Widgets group + options in `on_boot` (see `demo.yaml` /
|
||||
`weather.yaml` as templates): claim `int g = id(g_wgrp_n)++;`, append the group name to
|
||||
`id(g_wgrp_labels)`, then for each option append to `g_wopt_*` (group `g`, a label, kind
|
||||
0=toggle / 1=action, and `&id(your_global)`). Declare those globals in your own file.
|
||||
@@ -0,0 +1,565 @@
|
||||
# Cool Cars - optional carousel screen (base id 3). Lane racer.
|
||||
# Remove it: drop this file from your config's package "files:" list. Nothing else to edit.
|
||||
# Scores (g_top / g_best) + the "Reset scores" settings action live here - fully self-contained.
|
||||
|
||||
esphome:
|
||||
on_boot:
|
||||
- priority: 670
|
||||
then:
|
||||
- lambda: 'id(g_present)[3] = true;' # register after clock/player/timer
|
||||
# Settings (Settings -> Widgets -> Cool Cars -> Reset scores) + factory-reset handling.
|
||||
- priority: 650
|
||||
then:
|
||||
- lambda: |-
|
||||
int g = id(g_wgrp_n)++;
|
||||
id(g_wgrp_labels) += (g ? "\n" : ""); id(g_wgrp_labels) += "Cool Cars";
|
||||
int o = id(g_wopt_n)++;
|
||||
id(g_wopt_group)[o] = g; id(g_wopt_label)[o] = "Reset scores"; id(g_wopt_kind)[o] = 1; id(g_wopt_ptr)[o] = &id(g_cars_reset);
|
||||
|
||||
globals:
|
||||
# --- scores (persistent) + reset flag, owned by this package ---
|
||||
- id: g_best
|
||||
type: int
|
||||
restore_value: yes
|
||||
initial_value: '0'
|
||||
- id: g_top
|
||||
type: int[10]
|
||||
restore_value: yes
|
||||
- id: g_cars_reset # Widgets "Reset scores" sets this; the watcher below clears the scores
|
||||
type: bool
|
||||
restore_value: no
|
||||
initial_value: 'false'
|
||||
# --- "Car" game (page_game screen, g_base==3) ---
|
||||
- id: g_game_state # 0=idle 1=playing 2=game over
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
- id: g_car_lane # car lane 0/1/2
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '1'
|
||||
- id: g_score
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
- id: g_gspeed # obstacle speed (px/tick)
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '7'
|
||||
- id: g_lives # lives (3) - game v2
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '3'
|
||||
- id: g_inv # invulnerability frames after a hit (anti double-hit)
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
- id: g_o1_type # car obstacle: 0=cop 1=muscle 2=sedan
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
- id: g_o1_lane
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
- id: g_o1_x # X position (horizontal; they move left)
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '200'
|
||||
- id: g_o2_type # obstacle: 0=cop 1=muscle 2=sedan 3=cone (all = crash)
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
- id: g_o2_lane
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '2'
|
||||
- id: g_o2_x
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '320'
|
||||
- id: g_o3_type # 3rd obstacle (more = harder)
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '1'
|
||||
- id: g_o3_lane
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '1'
|
||||
- id: g_o3_x
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '460'
|
||||
- id: g_cn_lane # coin
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '1'
|
||||
- id: g_cn_x
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '440'
|
||||
- id: g_cn_on
|
||||
type: bool
|
||||
restore_value: no
|
||||
initial_value: 'true'
|
||||
- id: g_dash_off # offset of the dashed lane lines (0..359)
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
|
||||
script:
|
||||
# --- "Car" game ---
|
||||
- id: car_render # car lane (Y) + score + lives (hearts) + message per state
|
||||
then:
|
||||
- lvgl.widget.update: { id: car, y: !lambda 'return (id(g_car_lane) - 1) * 55;' }
|
||||
- lvgl.label.update:
|
||||
id: lbl_game_score
|
||||
text: !lambda 'char b[10]; snprintf(b, sizeof(b), "%d", id(g_score)); return std::string(b);'
|
||||
- lvgl.label.update:
|
||||
id: lbl_lives
|
||||
text: !lambda |-
|
||||
std::string h; for (int i = 0; i < id(g_lives); i++) h += ""; return h;
|
||||
# Shows the correct game screen per state (0=menu 1=playing 2=game over 3=how-to 4=scores)
|
||||
- id: game_screen
|
||||
then:
|
||||
# knob steers only during play + game over -> capture it there (block volume), free it in menu/how-to/scores
|
||||
- lambda: 'id(g_knob_capture) = (id(g_game_state) == 1 || id(g_game_state) == 2);'
|
||||
# 1) hide everything
|
||||
- lvgl.widget.hide: road_bg
|
||||
- lvgl.widget.hide: logo_bg
|
||||
- lvgl.widget.hide: car
|
||||
- lvgl.widget.hide: obs1
|
||||
- lvgl.widget.hide: obs2
|
||||
- lvgl.widget.hide: obs3
|
||||
- lvgl.widget.hide: coin
|
||||
- lvgl.widget.hide: dl0
|
||||
- lvgl.widget.hide: dl1
|
||||
- lvgl.widget.hide: db0
|
||||
- lvgl.widget.hide: db1
|
||||
- lvgl.widget.hide: lbl_lives
|
||||
- lvgl.widget.hide: lbl_game_score
|
||||
- lvgl.widget.hide: hit_flash
|
||||
- lvgl.widget.hide: btn_start
|
||||
- lvgl.widget.hide: btn_howto
|
||||
- lvgl.widget.hide: btn_scores
|
||||
- lvgl.widget.hide: lbl_over
|
||||
- lvgl.widget.hide: lbl_rec
|
||||
- lvgl.widget.hide: btn_replay
|
||||
- lvgl.widget.hide: btn_exit
|
||||
- lvgl.widget.hide: lbl_howto_t
|
||||
- lvgl.widget.hide: lbl_howto1
|
||||
- lvgl.widget.hide: lbl_howto2
|
||||
- lvgl.widget.hide: lbl_howto_b
|
||||
- lvgl.widget.hide: img_howcoin
|
||||
- lvgl.widget.hide: lbl_howto_l
|
||||
- lvgl.widget.hide: lbl_howheart
|
||||
- lvgl.widget.hide: badge1
|
||||
- lvgl.widget.hide: badge2
|
||||
- lvgl.widget.hide: badge3
|
||||
- lvgl.widget.hide: badge4
|
||||
- lvgl.widget.hide: lbl_scores_t
|
||||
- lvgl.widget.hide: lbl_scores1
|
||||
- lvgl.widget.hide: lbl_scores2
|
||||
- lvgl.widget.hide: btn_back
|
||||
# 2) show per state
|
||||
- if: # MENU
|
||||
condition: { lambda: 'return id(g_game_state) == 0;' }
|
||||
then:
|
||||
- lvgl.widget.show: logo_bg
|
||||
- lvgl.widget.show: btn_start
|
||||
- lvgl.widget.show: btn_howto
|
||||
- lvgl.widget.show: btn_scores
|
||||
- if: # GAME
|
||||
condition: { lambda: 'return id(g_game_state) == 1;' }
|
||||
then:
|
||||
- lvgl.widget.show: road_bg
|
||||
- lvgl.widget.show: car
|
||||
- lvgl.widget.show: obs1
|
||||
- lvgl.widget.show: obs2
|
||||
- lvgl.widget.show: obs3
|
||||
- lvgl.widget.show: coin
|
||||
- lvgl.widget.show: dl0
|
||||
- lvgl.widget.show: dl1
|
||||
- lvgl.widget.show: db0
|
||||
- lvgl.widget.show: db1
|
||||
- lvgl.widget.show: lbl_lives
|
||||
- lvgl.widget.show: lbl_game_score
|
||||
- if: # GAME OVER
|
||||
condition: { lambda: 'return id(g_game_state) == 2;' }
|
||||
then:
|
||||
- lvgl.widget.show: logo_bg
|
||||
- lvgl.widget.show: lbl_over
|
||||
- lvgl.widget.show: lbl_rec
|
||||
- lvgl.widget.show: btn_replay
|
||||
- lvgl.widget.show: btn_exit
|
||||
- lvgl.label.update: { id: lbl_over, text: !lambda 'char b[24]; snprintf(b,sizeof(b),"Score: %d", id(g_score)); return std::string(b);' }
|
||||
- lvgl.label.update: { id: lbl_rec, text: !lambda 'char b[24]; snprintf(b,sizeof(b),"Best: %d", id(g_best)); return std::string(b);' }
|
||||
- if: # HOW TO PLAY
|
||||
condition: { lambda: 'return id(g_game_state) == 3;' }
|
||||
then:
|
||||
- lvgl.widget.show: lbl_howto_t
|
||||
- lvgl.widget.show: lbl_howto1
|
||||
- lvgl.widget.show: lbl_howto2
|
||||
- lvgl.widget.show: lbl_howto_b
|
||||
- lvgl.widget.show: img_howcoin
|
||||
- lvgl.widget.show: lbl_howto_l
|
||||
- lvgl.widget.show: lbl_howheart
|
||||
- lvgl.widget.show: badge1
|
||||
- lvgl.widget.show: badge2
|
||||
- lvgl.widget.show: badge3
|
||||
- lvgl.widget.show: badge4
|
||||
- lvgl.widget.show: btn_back
|
||||
- if: # SCORES (top 10)
|
||||
condition: { lambda: 'return id(g_game_state) == 4;' }
|
||||
then:
|
||||
- lvgl.widget.show: lbl_scores_t
|
||||
- lvgl.widget.show: lbl_scores1
|
||||
- lvgl.widget.show: lbl_scores2
|
||||
- lvgl.widget.show: btn_back
|
||||
- lvgl.label.update:
|
||||
id: lbl_scores1
|
||||
text: !lambda |-
|
||||
std::string s; char b[24];
|
||||
for (int i = 0; i < 5; i++) { if(i) s += "\n"; if(id(g_top)[i]>0){ snprintf(b,sizeof(b),"%d. %d",i+1,id(g_top)[i]); } else { snprintf(b,sizeof(b),"%d. -",i+1); } s += b; }
|
||||
return s;
|
||||
- lvgl.label.update:
|
||||
id: lbl_scores2
|
||||
text: !lambda |-
|
||||
std::string s; char b[24];
|
||||
for (int i = 5; i < 10; i++) { if(i>5) s += "\n"; if(id(g_top)[i]>0){ snprintf(b,sizeof(b),"%d. %d",i+1,id(g_top)[i]); } else { snprintf(b,sizeof(b),"%d. -",i+1); } s += b; }
|
||||
return s;
|
||||
- id: score_render # score with "$" (refreshed on change, not every tick)
|
||||
then:
|
||||
- lvgl.label.update:
|
||||
id: lbl_game_score
|
||||
text: !lambda 'char b[10]; snprintf(b, sizeof(b), "%d", id(g_score)); return std::string(b);'
|
||||
- id: obs1_spawn # obstacle: 0=cop 1=muscle 2=sedan 3=cone
|
||||
then:
|
||||
- lambda: |-
|
||||
id(g_o1_type) = esp_random() % 4;
|
||||
id(g_o1_lane) = esp_random() % 3;
|
||||
id(g_o1_x) = 200 + (int)(esp_random() % 120);
|
||||
- if: { condition: { lambda: 'return id(g_o1_type)==0;' }, then: [lvgl.image.update: { id: obs1, src: img_cop }] }
|
||||
- if: { condition: { lambda: 'return id(g_o1_type)==1;' }, then: [lvgl.image.update: { id: obs1, src: img_mus }] }
|
||||
- if: { condition: { lambda: 'return id(g_o1_type)==2;' }, then: [lvgl.image.update: { id: obs1, src: img_sed }] }
|
||||
- if: { condition: { lambda: 'return id(g_o1_type)==3;' }, then: [lvgl.image.update: { id: obs1, src: img_cone }] }
|
||||
- lvgl.widget.update: { id: obs1, x: !lambda 'return id(g_o1_x);', y: !lambda 'return (id(g_o1_lane)-1)*55;' }
|
||||
- id: obs2_spawn
|
||||
then:
|
||||
- lambda: |-
|
||||
id(g_o2_type) = esp_random() % 4;
|
||||
id(g_o2_lane) = esp_random() % 3;
|
||||
id(g_o2_x) = 260 + (int)(esp_random() % 140);
|
||||
- if: { condition: { lambda: 'return id(g_o2_type)==0;' }, then: [lvgl.image.update: { id: obs2, src: img_cop }] }
|
||||
- if: { condition: { lambda: 'return id(g_o2_type)==1;' }, then: [lvgl.image.update: { id: obs2, src: img_mus }] }
|
||||
- if: { condition: { lambda: 'return id(g_o2_type)==2;' }, then: [lvgl.image.update: { id: obs2, src: img_sed }] }
|
||||
- if: { condition: { lambda: 'return id(g_o2_type)==3;' }, then: [lvgl.image.update: { id: obs2, src: img_cone }] }
|
||||
- lvgl.widget.update: { id: obs2, x: !lambda 'return id(g_o2_x);', y: !lambda 'return (id(g_o2_lane)-1)*55;' }
|
||||
- id: obs3_spawn
|
||||
then:
|
||||
- lambda: |-
|
||||
id(g_o3_type) = esp_random() % 4;
|
||||
id(g_o3_lane) = esp_random() % 3;
|
||||
id(g_o3_x) = 340 + (int)(esp_random() % 160);
|
||||
- if: { condition: { lambda: 'return id(g_o3_type)==0;' }, then: [lvgl.image.update: { id: obs3, src: img_cop }] }
|
||||
- if: { condition: { lambda: 'return id(g_o3_type)==1;' }, then: [lvgl.image.update: { id: obs3, src: img_mus }] }
|
||||
- if: { condition: { lambda: 'return id(g_o3_type)==2;' }, then: [lvgl.image.update: { id: obs3, src: img_sed }] }
|
||||
- if: { condition: { lambda: 'return id(g_o3_type)==3;' }, then: [lvgl.image.update: { id: obs3, src: img_cone }] }
|
||||
- lvgl.widget.update: { id: obs3, x: !lambda 'return id(g_o3_x);', y: !lambda 'return (id(g_o3_lane)-1)*55;' }
|
||||
- id: coin_spawn
|
||||
then:
|
||||
- lambda: |-
|
||||
id(g_cn_lane) = esp_random() % 3;
|
||||
id(g_cn_x) = 220 + (int)(esp_random() % 200);
|
||||
id(g_cn_on) = true;
|
||||
- lvgl.widget.show: coin
|
||||
- lvgl.widget.update: { id: coin, x: !lambda 'return id(g_cn_x);', y: !lambda 'return (id(g_cn_lane)-1)*55;' }
|
||||
- id: game_start
|
||||
then:
|
||||
- lambda: |-
|
||||
id(g_game_state) = 1; id(g_score) = 0; id(g_gspeed) = 7;
|
||||
id(g_car_lane) = 1; id(g_lives) = 3; id(g_inv) = 0;
|
||||
id(g_knob_capture) = true; id(g_knob_delta) = 0;
|
||||
- lvgl.widget.update: { id: car, x: -120 }
|
||||
- script.execute: obs1_spawn
|
||||
- script.execute: obs2_spawn
|
||||
- script.execute: obs3_spawn
|
||||
- script.execute: coin_spawn
|
||||
- script.execute: score_render
|
||||
- script.execute: game_screen
|
||||
- script.execute: car_render
|
||||
- id: lose_life # losing a life: red flash + invulnerability (blinking), 0 lives -> game over
|
||||
then:
|
||||
- lambda: 'id(g_lives)--; id(g_inv) = 22;' # ~1.1s invulnerability (scaled for 50ms)
|
||||
- lvgl.widget.show: hit_flash
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_lives) <= 0;' }
|
||||
then: [ script.execute: game_crash ]
|
||||
else: [ script.execute: car_render ]
|
||||
- id: game_crash # GAME OVER (0 lives) - save score + insert into top 10
|
||||
then:
|
||||
- lambda: |-
|
||||
id(g_game_state) = 2; // game_screen keeps the knob captured on game over (no volume)
|
||||
int s = id(g_score);
|
||||
if (s > id(g_best)) id(g_best) = s;
|
||||
for (int i = 0; i < 10; i++) {
|
||||
if (s > id(g_top)[i]) {
|
||||
for (int j = 9; j > i; j--) id(g_top)[j] = id(g_top)[j-1];
|
||||
id(g_top)[i] = s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
- script.execute: game_screen
|
||||
|
||||
interval:
|
||||
# Reset scores when the Widgets action fired (g_cars_reset). Idempotent + cheap.
|
||||
- interval: 250ms
|
||||
then:
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_cars_reset) || id(g_factory_reset);' }
|
||||
then:
|
||||
- lambda: 'for (int i = 0; i < 10; i++) id(g_top)[i] = 0; id(g_best) = 0; id(g_cars_reset) = false;'
|
||||
# nav: show this screen when it is selected in the carousel
|
||||
- interval: 50ms
|
||||
then:
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_nav_req) && id(g_base)==3;' }
|
||||
then:
|
||||
- lambda: 'id(g_nav_req) = false; id(g_game_state) = 0;'
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==0;' }, then: [lvgl.page.show: { id: page_game, animation: move_left, time: 250ms }] }
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==1;' }, then: [lvgl.page.show: { id: page_game, animation: move_right, time: 250ms }] }
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==2;' }, then: [lvgl.page.show: { id: page_game, animation: move_top, time: 250ms }] }
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==3;' }, then: [lvgl.page.show: { id: page_game, animation: move_bottom, time: 250ms }] }
|
||||
- script.execute: game_screen
|
||||
# "Car" game (driving right) - tick only on the game screen during play.
|
||||
# 50ms (20fps) so motion is smooth; speeds are scaled down to keep the same px/sec pace.
|
||||
- interval: 50ms
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
lambda: 'return id(g_base) == 3 && id(g_game_state) == 1;'
|
||||
then:
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_knob_capture) && id(g_knob_delta) != 0;' }
|
||||
then:
|
||||
- lambda: 'id(g_car_lane) += id(g_knob_delta); if (id(g_car_lane) < 0) id(g_car_lane) = 0; if (id(g_car_lane) > 2) id(g_car_lane) = 2; id(g_knob_delta) = 0;'
|
||||
- script.execute: car_render
|
||||
- lambda: |-
|
||||
id(g_o1_x) -= id(g_gspeed);
|
||||
id(g_o2_x) -= id(g_gspeed);
|
||||
id(g_o3_x) -= id(g_gspeed);
|
||||
id(g_cn_x) -= id(g_gspeed);
|
||||
if (id(g_inv) > 0) id(g_inv)--;
|
||||
int sp = 7 + id(g_score) / 11; if (sp > 16) sp = 16; id(g_gspeed) = sp; // ramps up (scaled for 50ms)
|
||||
id(g_dash_off) = (id(g_dash_off) + id(g_gspeed)) % 360;
|
||||
# Recycle obstacles past the left edge (+ a point for dodging, score updated only on change)
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_o1_x) < -200;' }
|
||||
then: [ lambda: 'id(g_score)++;', script.execute: obs1_spawn, script.execute: score_render ]
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_o2_x) < -200;' }
|
||||
then: [ lambda: 'id(g_score)++;', script.execute: obs2_spawn, script.execute: score_render ]
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_o3_x) < -200;' }
|
||||
then: [ lambda: 'id(g_score)++;', script.execute: obs3_spawn, script.execute: score_render ]
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_cn_x) < -200;' }
|
||||
then: [ script.execute: coin_spawn ]
|
||||
# Collisions (cars + cone = lose a life)
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_inv) <= 0 && abs(id(g_o1_x) - (-120)) < 30 && id(g_o1_lane) == id(g_car_lane);' }
|
||||
then: [ script.execute: lose_life ]
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_inv) <= 0 && abs(id(g_o2_x) - (-120)) < 30 && id(g_o2_lane) == id(g_car_lane);' }
|
||||
then: [ script.execute: lose_life ]
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_inv) <= 0 && abs(id(g_o3_x) - (-120)) < 30 && id(g_o3_lane) == id(g_car_lane);' }
|
||||
then: [ script.execute: lose_life ]
|
||||
# Coin pickup
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_cn_on) && abs(id(g_cn_x) - (-120)) < 28 && id(g_cn_lane) == id(g_car_lane);' }
|
||||
then:
|
||||
- lambda: 'id(g_score) += 5; id(g_cn_on) = false;'
|
||||
- lvgl.widget.hide: coin
|
||||
- script.execute: coin_spawn
|
||||
- script.execute: score_render
|
||||
# Hit effect: red flash fades after a moment, car blinks during invulnerability
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_inv) > 0;' }
|
||||
then:
|
||||
- lvgl.widget.update: { id: car, hidden: !lambda 'return (id(g_inv) % 4) >= 2;' }
|
||||
- lvgl.widget.update: { id: hit_flash, hidden: !lambda 'return id(g_inv) < 16;' }
|
||||
# Move obstacles/coin/dashes in ONE lambda (lv_obj_align) instead of 8 widget.update
|
||||
# actions - far less per-tick overhead (the pattern that keeps Snake/Knobuss smooth).
|
||||
- lambda: |-
|
||||
lv_obj_align(id(obs1), LV_ALIGN_CENTER, id(g_o1_x), (id(g_o1_lane) - 1) * 55);
|
||||
lv_obj_align(id(obs2), LV_ALIGN_CENTER, id(g_o2_x), (id(g_o2_lane) - 1) * 55);
|
||||
lv_obj_align(id(obs3), LV_ALIGN_CENTER, id(g_o3_x), (id(g_o3_lane) - 1) * 55);
|
||||
lv_obj_align(id(coin), LV_ALIGN_CENTER, id(g_cn_x), (id(g_cn_lane) - 1) * 55);
|
||||
int d0 = ((0 - id(g_dash_off)) % 360 + 360) % 360 - 180;
|
||||
int d1 = ((180 - id(g_dash_off)) % 360 + 360) % 360 - 180;
|
||||
lv_obj_align(id(dl0), LV_ALIGN_CENTER, d0, -28);
|
||||
lv_obj_align(id(dl1), LV_ALIGN_CENTER, d1, -28);
|
||||
lv_obj_align(id(db0), LV_ALIGN_CENTER, d0, 27);
|
||||
lv_obj_align(id(db1), LV_ALIGN_CENTER, d1, 27);
|
||||
|
||||
image:
|
||||
- id: img_road2
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/cool-cars/road2.png"
|
||||
type: RGB565
|
||||
- id: img_p
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/cool-cars/pcar2.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_cop
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/cool-cars/ocop2.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_mus
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/cool-cars/omus2.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_sed
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/cool-cars/osed2.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_coin
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/cool-cars/coin.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_cone
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/cool-cars/cone.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_logo
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/cool-cars/logo.png"
|
||||
type: RGB565
|
||||
|
||||
lvgl:
|
||||
pages:
|
||||
# --- "Car" GAME page (after the player): 3 lanes, car at the bottom, obstacles from the top ---
|
||||
- id: page_game
|
||||
bg_color: 0x0D1323
|
||||
scrollbar_mode: "off"
|
||||
scrollable: false
|
||||
widgets:
|
||||
- image: { id: road_bg, align: CENTER, x: 0, y: 0, src: img_road2, hidden: true } # road (only in-game)
|
||||
# Dashed lane lines - moving (scrolled in the interval = sense of driving)
|
||||
- obj: { id: dl0, align: CENTER, x: 0, y: -28, width: 26, height: 4, radius: 0, border_width: 0, bg_color: 0xECD25A, hidden: true }
|
||||
- obj: { id: dl1, align: CENTER, x: 0, y: -28, width: 26, height: 4, radius: 0, border_width: 0, bg_color: 0xECD25A, hidden: true }
|
||||
- obj: { id: db0, align: CENTER, x: 0, y: 27, width: 26, height: 4, radius: 0, border_width: 0, bg_color: 0xECD25A, hidden: true }
|
||||
- obj: { id: db1, align: CENTER, x: 0, y: 27, width: 26, height: 4, radius: 0, border_width: 0, bg_color: 0xECD25A, hidden: true }
|
||||
- image: { id: coin, align: CENTER, x: 440, y: 0, src: img_coin, hidden: true }
|
||||
- image: { id: obs1, align: CENTER, x: 200, y: -55, src: img_cop, hidden: true }
|
||||
- image: { id: obs2, align: CENTER, x: 320, y: 55, src: img_mus, hidden: true }
|
||||
- image: { id: obs3, align: CENTER, x: 460, y: 0, src: img_sed, hidden: true }
|
||||
- image: { id: car, align: CENTER, x: -120, y: 0, src: img_p, hidden: true }
|
||||
- label: { id: lbl_lives, align: CENTER, x: -70, y: -108, text: "", text_font: icons18, text_color: 0xE24B4A, hidden: true }
|
||||
- label: { id: lbl_game_score, align: CENTER, x: 70, y: -108, text: "0", text_font: font_med, text_color: 0xCFEAE4, hidden: true }
|
||||
- obj: { id: hit_flash, align: CENTER, x: 0, y: 0, width: 360, height: 360, radius: 0, border_width: 0, bg_color: 0xFF2A2A, bg_opa: 45%, hidden: true }
|
||||
# --- Logo background (menu) ---
|
||||
- image: { id: logo_bg, align: CENTER, x: 0, y: 0, src: img_logo, hidden: true }
|
||||
# --- MENU (state 0): Start / How to play? / Scores (under the logo) ---
|
||||
- button:
|
||||
id: btn_start
|
||||
align: CENTER
|
||||
y: -8
|
||||
width: 150
|
||||
height: 44
|
||||
radius: 22
|
||||
bg_color: 0x2FE6CF
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
on_click: [ script.execute: game_start ]
|
||||
widgets:
|
||||
- label: { align: CENTER, text: "Start", text_font: font_med, text_color: 0x06201C }
|
||||
- button:
|
||||
id: btn_howto
|
||||
align: CENTER
|
||||
y: 42
|
||||
width: 150
|
||||
height: 44
|
||||
radius: 22
|
||||
bg_color: 0x2A2E55
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
on_click: [ lambda: 'id(g_game_state) = 3;', script.execute: game_screen ]
|
||||
widgets:
|
||||
- label: { align: CENTER, text: "How to play?", text_font: font_med, text_color: 0xFFFFFF }
|
||||
- button:
|
||||
id: btn_scores
|
||||
align: CENTER
|
||||
y: 92
|
||||
width: 150
|
||||
height: 44
|
||||
radius: 22
|
||||
bg_color: 0x2A2E55
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
on_click: [ lambda: 'id(g_game_state) = 4;', script.execute: game_screen ]
|
||||
widgets:
|
||||
- label: { align: CENTER, text: "Scores", text_font: font_med, text_color: 0xFFFFFF }
|
||||
# --- GAME OVER (state 2): score + Play + Exit ---
|
||||
- label: { id: lbl_over, align: CENTER, x: 0, y: -10, text_align: CENTER, text: "", text_font: font_sel, text_color: 0xFFFFFF, hidden: true }
|
||||
- label: { id: lbl_rec, align: CENTER, x: 0, y: 22, text_align: CENTER, text: "", text_font: font_artist, text_color: 0x9AA6B2, hidden: true }
|
||||
- button:
|
||||
id: btn_replay
|
||||
align: CENTER
|
||||
x: -66
|
||||
y: 62
|
||||
width: 112
|
||||
height: 48
|
||||
radius: 24
|
||||
bg_color: 0x2FE6CF
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
hidden: true
|
||||
on_click: [ script.execute: game_start ]
|
||||
widgets:
|
||||
- label: { align: CENTER, text: "Play", text_font: font_med, text_color: 0x06201C }
|
||||
- button:
|
||||
id: btn_exit
|
||||
align: CENTER
|
||||
x: 66
|
||||
y: 62
|
||||
width: 112
|
||||
height: 48
|
||||
radius: 24
|
||||
bg_color: 0x3A3A3D
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
hidden: true
|
||||
on_click: [ lambda: 'id(g_game_state) = 0;', script.execute: game_screen ]
|
||||
widgets:
|
||||
- label: { align: CENTER, text: "Exit", text_font: font_med, text_color: 0xFFFFFF }
|
||||
# --- HOW TO PLAY? (state 3) - navy background, logo accents ---
|
||||
- label: { id: lbl_howto_t, align: CENTER, y: -104, text: "HOW TO PLAY?", text_font: font_sel, text_color: 0xE94FB0, hidden: true }
|
||||
- obj: { id: badge1, align: CENTER, x: -90, y: -46, width: 24, height: 24, radius: 12, bg_color: 0xE94FB0, bg_opa: COVER, border_width: 0, pad_all: 0, hidden: true, widgets: [ label: { align: CENTER, text: "1", text_font: font_artist, text_color: 0xFFFFFF } ] }
|
||||
- obj: { id: badge2, align: CENTER, x: -90, y: -10, width: 24, height: 24, radius: 12, bg_color: 0xE94FB0, bg_opa: COVER, border_width: 0, pad_all: 0, hidden: true, widgets: [ label: { align: CENTER, text: "2", text_font: font_artist, text_color: 0xFFFFFF } ] }
|
||||
- obj: { id: badge3, align: CENTER, x: -90, y: 26, width: 24, height: 24, radius: 12, bg_color: 0xE94FB0, bg_opa: COVER, border_width: 0, pad_all: 0, hidden: true, widgets: [ label: { align: CENTER, text: "3", text_font: font_artist, text_color: 0xFFFFFF } ] }
|
||||
- obj: { id: badge4, align: CENTER, x: -90, y: 62, width: 24, height: 24, radius: 12, bg_color: 0xE94FB0, bg_opa: COVER, border_width: 0, pad_all: 0, hidden: true, widgets: [ label: { align: CENTER, text: "4", text_font: font_artist, text_color: 0xFFFFFF } ] }
|
||||
- label: { id: lbl_howto1, align_to: { id: badge1, align: OUT_RIGHT_MID, x: 8 }, text: "Use the knob", text_font: font_med, text_color: 0xC2CDDA, hidden: true }
|
||||
- label: { id: lbl_howto2, align_to: { id: badge2, align: OUT_RIGHT_MID, x: 8 }, text: "Dodge obstacles", text_font: font_med, text_color: 0xC2CDDA, hidden: true }
|
||||
- label: { id: lbl_howto_b, align_to: { id: badge3, align: OUT_RIGHT_MID, x: 8 }, text: "Collect bonuses", text_font: font_med, text_color: 0xC2CDDA, hidden: true }
|
||||
- image: { id: img_howcoin, align_to: { id: lbl_howto_b, align: OUT_RIGHT_MID, x: 6 }, src: img_coin, hidden: true }
|
||||
- label: { id: lbl_howto_l, align_to: { id: badge4, align: OUT_RIGHT_MID, x: 8 }, text: "You have 3 lives", text_font: font_med, text_color: 0xC2CDDA, hidden: true }
|
||||
- label: { id: lbl_howheart, align_to: { id: lbl_howto_l, align: OUT_RIGHT_MID, x: 6 }, text: "", text_font: icons18, text_color: 0xE24B4A, hidden: true }
|
||||
# --- SCORES (state 4): top 10 in two columns ---
|
||||
- label: { id: lbl_scores_t, align: CENTER, y: -116, text: "SCORES", text_font: font_sel, text_color: 0xE94FB0, hidden: true }
|
||||
- label: { id: lbl_scores1, align: CENTER, x: -52, y: 0, text: "", text_align: LEFT, text_font: font_artist, text_color: 0xC2CDDA, hidden: true }
|
||||
- label: { id: lbl_scores2, align: CENTER, x: 56, y: 0, text: "", text_align: LEFT, text_font: font_artist, text_color: 0xC2CDDA, hidden: true }
|
||||
# --- Back (state 3/4 -> menu) ---
|
||||
- button:
|
||||
id: btn_back
|
||||
align: CENTER
|
||||
y: 120
|
||||
width: 130
|
||||
height: 42
|
||||
radius: 21
|
||||
bg_color: 0x3A3A3D
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
hidden: true
|
||||
on_click: [ lambda: 'id(g_game_state) = 0;', script.execute: game_screen ]
|
||||
widgets:
|
||||
- label: { align: CENTER, text: "Back", text_font: font_med, text_color: 0xFFFFFF }
|
||||
@@ -0,0 +1,147 @@
|
||||
# ============================================================================
|
||||
# DEMO / EXAMPLE SCREEN (base id 6, carousel name "demo")
|
||||
# ----------------------------------------------------------------------------
|
||||
# A minimal, heavily-commented screen that shows how a screen package is built:
|
||||
# * it registers itself in the carousel (g_present),
|
||||
# * it shows its own LVGL page,
|
||||
# * it reacts to a tap (toggles the background black <-> white, text inverts),
|
||||
# * it declares its OWN settings toggles (Settings -> Widgets -> Demo):
|
||||
# g_demo_border -> draw a border
|
||||
# g_demo_led -> drive the LED ring (green on white bg, red on black bg)
|
||||
# The globals live in THIS file and are registered into the core's generic settings
|
||||
# registry (g_wgrp_* / g_wopt_*) in on_boot - see how below as the template.
|
||||
# * it drives the LED ring directly (a simple demo - it bypasses the ring
|
||||
# reaction-priority system in the core; that is fine for an example).
|
||||
#
|
||||
# Everything a screen needs lives in THIS file (globals/scripts/interval/lvgl page +
|
||||
# its settings registration). Remove the screen by dropping this file from the package
|
||||
# "files:" list - nothing else to edit, anywhere.
|
||||
# ============================================================================
|
||||
|
||||
esphome:
|
||||
on_boot:
|
||||
# Register this screen so the carousel knows it exists. The core builds the
|
||||
# carousel order from `screen_order`, including only screens that set g_present.
|
||||
- priority: 655
|
||||
then:
|
||||
- lambda: 'id(g_present)[6] = true;'
|
||||
# Register this screen's settings (Settings -> Widgets -> Demo) in the generic registry.
|
||||
- priority: 650
|
||||
then:
|
||||
- lambda: |-
|
||||
int g = id(g_wgrp_n)++;
|
||||
id(g_wgrp_labels) += (g ? "\n" : ""); id(g_wgrp_labels) += "Demo";
|
||||
int o;
|
||||
o = id(g_wopt_n)++; id(g_wopt_group)[o] = g; id(g_wopt_label)[o] = "Border"; id(g_wopt_kind)[o] = 0; id(g_wopt_ptr)[o] = &id(g_demo_border);
|
||||
o = id(g_wopt_n)++; id(g_wopt_group)[o] = g; id(g_wopt_label)[o] = "Demo LED"; id(g_wopt_kind)[o] = 0; id(g_wopt_ptr)[o] = &id(g_demo_led);
|
||||
|
||||
globals:
|
||||
- id: g_demo_bg # background: false = black, true = white (runtime state)
|
||||
type: bool
|
||||
restore_value: no
|
||||
initial_value: 'false'
|
||||
# Demo settings (declared here, registered above; the core menu reaches them by pointer).
|
||||
- id: g_demo_border # draw a border (Settings -> Widgets -> Demo)
|
||||
type: bool
|
||||
restore_value: yes
|
||||
initial_value: 'false'
|
||||
- id: g_demo_led # drive the LED ring (Settings -> Widgets -> Demo)
|
||||
type: bool
|
||||
restore_value: yes
|
||||
initial_value: 'false'
|
||||
# Tap de-duplication: the core bumps g_tap_seq on every normal-mode tap; we
|
||||
# remember the last value we handled so one tap = one toggle.
|
||||
- id: g_demo_lasttap
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
# Tracks whether WE currently own the LED ring, so we can release it on leave.
|
||||
# Persistent so that after a reboot (the ring component restores its last colour) the
|
||||
# 80ms loop below sees "we owned it", and since we boot onto the home screen it turns
|
||||
# the ring back off instead of leaving the demo colour stuck on.
|
||||
- id: g_demo_ring_on
|
||||
type: bool
|
||||
restore_value: yes
|
||||
initial_value: 'false'
|
||||
|
||||
script:
|
||||
# Redraws the screen from the three state flags (g_demo_bg / border / led).
|
||||
- id: demo_render
|
||||
then:
|
||||
# background + text colour (text is the inverse of the background)
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_demo_bg);' } # white background
|
||||
then:
|
||||
- lvgl.widget.update: { id: demo_bg, bg_color: 0xFFFFFF, border_color: 0x000000 }
|
||||
- lvgl.label.update: { id: demo_lbl, text_color: 0x000000 }
|
||||
else: # black background
|
||||
- lvgl.widget.update: { id: demo_bg, bg_color: 0x000000, border_color: 0xFFFFFF }
|
||||
- lvgl.label.update: { id: demo_lbl, text_color: 0xFFFFFF }
|
||||
# border on/off (width is an int, so a lambda is fine here)
|
||||
- lvgl.widget.update: { id: demo_bg, border_width: !lambda 'return id(g_demo_border) ? 6 : 0;' }
|
||||
# LED ring: green when the background is white, red when it is black
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_demo_led);' }
|
||||
then:
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_demo_bg);' }
|
||||
then: [ light.turn_on: { id: ring_light, red: 0%, green: 100%, blue: 0%, effect: "None" } ]
|
||||
else: [ light.turn_on: { id: ring_light, red: 100%, green: 0%, blue: 0%, effect: "None" } ]
|
||||
- lambda: 'id(g_demo_ring_on) = true;'
|
||||
else:
|
||||
# LED toggle is off: release the ring if we were driving it
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_demo_ring_on);' }
|
||||
then:
|
||||
- light.turn_off: ring_light
|
||||
- lambda: 'id(g_demo_ring_on) = false;'
|
||||
|
||||
interval:
|
||||
# Carousel navigation: when this screen becomes selected, show its page.
|
||||
- interval: 50ms
|
||||
then:
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_nav_req) && id(g_base)==6;' }
|
||||
then:
|
||||
- lambda: 'id(g_nav_req) = false; id(g_demo_lasttap) = id(g_tap_seq);' # sync tap counter so arriving never counts as a tap
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==0;' }, then: [lvgl.page.show: { id: page_demo, animation: move_left, time: 250ms }] }
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==1;' }, then: [lvgl.page.show: { id: page_demo, animation: move_right, time: 250ms }] }
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==2;' }, then: [lvgl.page.show: { id: page_demo, animation: move_top, time: 250ms }] }
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==3;' }, then: [lvgl.page.show: { id: page_demo, animation: move_bottom, time: 250ms }] }
|
||||
- script.execute: demo_render
|
||||
# React to a tap while this screen is on top; release the LED when we leave.
|
||||
- interval: 80ms
|
||||
then:
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_base)==6 && id(g_tap_seq) != id(g_demo_lasttap);' }
|
||||
then:
|
||||
- lambda: 'id(g_demo_lasttap) = id(g_tap_seq); id(g_demo_bg) = !id(g_demo_bg);' # one tap = flip background
|
||||
- script.execute: demo_render
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_base) != 6 && id(g_demo_ring_on);' }
|
||||
then:
|
||||
- lambda: 'id(g_demo_ring_on) = false;'
|
||||
|
||||
lvgl:
|
||||
pages:
|
||||
- id: page_demo
|
||||
bg_color: 0x000000
|
||||
widgets:
|
||||
# A full circle that fills the round screen - this is the background we recolour.
|
||||
# radius 180 on a 360x360 box makes it a circle, so the optional border is a clean ring.
|
||||
- obj:
|
||||
id: demo_bg
|
||||
align: CENTER
|
||||
width: 360
|
||||
height: 360
|
||||
radius: 180
|
||||
bg_color: 0x000000
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
border_color: 0xFFFFFF
|
||||
- label:
|
||||
id: demo_lbl
|
||||
align: CENTER
|
||||
text: "TAP ME!"
|
||||
text_font: montserrat_34
|
||||
text_color: 0xFFFFFF
|
||||
@@ -0,0 +1,711 @@
|
||||
# Knobuss - optional carousel screen (base id 10). A Gyruss-style tube shooter: your ship orbits the rim, you
|
||||
# auto-fire inward, foes spiral out from the centre. Turn the knob to aim, blast them before they
|
||||
# reach you. Remove it: drop this file from your config's package "files:" list. Nothing else to edit.
|
||||
# Scores (g_gy_top / g_gy_best) + the "Reset scores" settings action live here - fully self-contained.
|
||||
#
|
||||
# Perf: ship + bullet pool (6) + enemy pool (10) are all positioned each tick from float arrays in
|
||||
# ONE lambda (lv_obj_set_pos over a pooled lv_obj_t* array - LVGL can't index a widget by a runtime
|
||||
# var), so per-tick cost is constant and the action chain stays shallow. Sprites have a fixed src
|
||||
# (enemies alternate enemy/enemy2 by slot), so there are no per-tick image swaps.
|
||||
|
||||
esphome:
|
||||
on_boot:
|
||||
- priority: 655
|
||||
then:
|
||||
- lambda: 'id(g_present)[10] = true;'
|
||||
- priority: 650
|
||||
then:
|
||||
- lambda: |-
|
||||
int g = id(g_wgrp_n)++;
|
||||
id(g_wgrp_labels) += (g ? "\n" : ""); id(g_wgrp_labels) += "Knobuss";
|
||||
int o = id(g_wopt_n)++;
|
||||
id(g_wopt_group)[o] = g; id(g_wopt_label)[o] = "Reset scores"; id(g_wopt_kind)[o] = 1; id(g_wopt_ptr)[o] = &id(g_gy_reset);
|
||||
|
||||
globals:
|
||||
- id: g_gy_best
|
||||
type: int
|
||||
restore_value: yes
|
||||
initial_value: '0'
|
||||
- id: g_gy_top
|
||||
type: int[10]
|
||||
restore_value: yes
|
||||
- id: g_gy_reset
|
||||
type: bool
|
||||
restore_value: no
|
||||
initial_value: 'false'
|
||||
- id: g_gy_state # 0=menu 1=playing 2=game over 3=how-to 4=scores
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
- id: g_gy_score
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
- id: g_gy_lives
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '3'
|
||||
- id: g_gy_ship_ang # ship orbit angle (radians)
|
||||
type: float
|
||||
restore_value: no
|
||||
initial_value: '0.0'
|
||||
- id: g_gy_dead
|
||||
type: bool
|
||||
restore_value: no
|
||||
initial_value: 'false'
|
||||
- id: g_gy_fire_clk
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
- id: g_gy_spawn_clk
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
- id: g_gy_hud_dirty # score/lives changed this tick -> refresh the HUD labels
|
||||
type: bool
|
||||
restore_value: no
|
||||
initial_value: 'false'
|
||||
# bullet pool (fly inward along a fixed radial)
|
||||
- id: g_gy_br # radius (px from centre)
|
||||
type: float[6]
|
||||
restore_value: no
|
||||
- id: g_gy_ba # angle (radians)
|
||||
type: float[6]
|
||||
restore_value: no
|
||||
- id: g_gy_bon
|
||||
type: bool[6]
|
||||
restore_value: no
|
||||
# enemy pool (spiral outward from the centre)
|
||||
- id: g_gy_er
|
||||
type: float[10]
|
||||
restore_value: no
|
||||
- id: g_gy_ea
|
||||
type: float[10]
|
||||
restore_value: no
|
||||
- id: g_gy_eon
|
||||
type: bool[10]
|
||||
restore_value: no
|
||||
# starfield (depth illusion): dots rush outward from the centre, faster as they near the rim
|
||||
- id: g_gy_sr # radius (px from centre)
|
||||
type: float[10]
|
||||
restore_value: no
|
||||
- id: g_gy_sa # angle (radians)
|
||||
type: float[10]
|
||||
restore_value: no
|
||||
# explosion pool (expanding shockwave ring of dots) - fired when a foe reaches the rim
|
||||
- id: g_gy_exx # centre X (relative to screen centre, px)
|
||||
type: float[2]
|
||||
restore_value: no
|
||||
- id: g_gy_exy # centre Y
|
||||
type: float[2]
|
||||
restore_value: no
|
||||
- id: g_gy_exage # -1 = inactive, else explosion frame 0..7
|
||||
type: int[2]
|
||||
restore_value: no
|
||||
- id: g_gy_shipframe # current ship rotation frame (0..15), -1 forces a refresh
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '-1'
|
||||
- id: g_gy_shipdirty # ship rotation bucket changed this tick
|
||||
type: bool
|
||||
restore_value: no
|
||||
initial_value: 'false'
|
||||
- id: g_gy_exact # an explosion is active or expired this tick (refresh its sprite)
|
||||
type: bool
|
||||
restore_value: no
|
||||
initial_value: 'false'
|
||||
- id: g_gy_tick # frame counter (throttles the starfield + ship-sprite refresh)
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
|
||||
script:
|
||||
# position every active widget from the float arrays (constant cost, one shallow lambda)
|
||||
- id: gy_render
|
||||
then:
|
||||
- lambda: |-
|
||||
const float R = 148.0f;
|
||||
// starfield (background depth) - small dots rushing outward
|
||||
lv_obj_t *SF[6] = { id(gy_s0), id(gy_s1), id(gy_s2), id(gy_s3), id(gy_s4), id(gy_s5) };
|
||||
for (int k = 0; k < 6; k++) {
|
||||
float r = id(g_gy_sr)[k], a = id(g_gy_sa)[k];
|
||||
lv_obj_set_pos(SF[k], 180 + (int) lroundf(cosf(a) * r) - 1, 180 + (int) lroundf(sinf(a) * r) - 1);
|
||||
}
|
||||
lv_obj_t *BB[6] = { id(gy_b0), id(gy_b1), id(gy_b2), id(gy_b3), id(gy_b4), id(gy_b5) };
|
||||
for (int k = 0; k < 6; k++) {
|
||||
if (id(g_gy_bon)[k]) {
|
||||
float r = id(g_gy_br)[k], a = id(g_gy_ba)[k];
|
||||
lv_obj_set_pos(BB[k], 180 + (int) lroundf(cosf(a) * r) - 6, 180 + (int) lroundf(sinf(a) * r) - 6);
|
||||
} else lv_obj_set_pos(BB[k], 400, 400);
|
||||
}
|
||||
lv_obj_t *EE[10] = { id(gy_e0), id(gy_e1), id(gy_e2), id(gy_e3), id(gy_e4), id(gy_e5), id(gy_e6), id(gy_e7), id(gy_e8), id(gy_e9) };
|
||||
for (int k = 0; k < 10; k++) {
|
||||
if (id(g_gy_eon)[k]) {
|
||||
float r = id(g_gy_er)[k], a = id(g_gy_ea)[k];
|
||||
lv_obj_set_pos(EE[k], 180 + (int) lroundf(cosf(a) * r) - 13, 180 + (int) lroundf(sinf(a) * r) - 13);
|
||||
} else lv_obj_set_pos(EE[k], 400, 400);
|
||||
}
|
||||
float sa = id(g_gy_ship_ang);
|
||||
lv_obj_set_pos(id(gy_ship), 180 + (int) lroundf(cosf(sa) * R) - 18, 180 + (int) lroundf(sinf(sa) * R) - 18);
|
||||
// explosions (32px sprites) - positioned while alive, parked otherwise
|
||||
lv_obj_t *XX[2] = { id(gy_x0), id(gy_x1) };
|
||||
for (int s = 0; s < 2; s++) {
|
||||
if (id(g_gy_exage)[s] >= 0) lv_obj_set_pos(XX[s], 180 + (int) lroundf(id(g_gy_exx)[s]) - 16, 180 + (int) lroundf(id(g_gy_exy)[s]) - 16);
|
||||
else lv_obj_set_pos(XX[s], 400, 400);
|
||||
}
|
||||
# set the ship sprite to its current rotation frame (called only when the bucket changes)
|
||||
- id: gy_shipspr
|
||||
then:
|
||||
- if: { condition: { lambda: 'return id(g_gy_shipframe)==0;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship0 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_gy_shipframe)==1;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship1 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_gy_shipframe)==2;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship2 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_gy_shipframe)==3;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship3 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_gy_shipframe)==4;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship4 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_gy_shipframe)==5;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship5 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_gy_shipframe)==6;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship6 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_gy_shipframe)==7;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship7 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_gy_shipframe)==8;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship8 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_gy_shipframe)==9;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship9 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_gy_shipframe)==10;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship10 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_gy_shipframe)==11;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship11 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_gy_shipframe)==12;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship12 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_gy_shipframe)==13;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship13 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_gy_shipframe)==14;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship14 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_gy_shipframe)==15;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship15 } ] }
|
||||
# advance/hide the explosion sprites by their frame (called only while a burst is live)
|
||||
- id: gy_explspr
|
||||
then:
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_gy_exage)[0] < 0;' }
|
||||
then: [ lvgl.widget.hide: gy_x0 ]
|
||||
else:
|
||||
- lvgl.widget.show: gy_x0
|
||||
- if: { condition: { lambda: 'return id(g_gy_exage)[0]==0;' }, then: [ lvgl.image.update: { id: gy_x0, src: img_gy_expl0 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_gy_exage)[0]==1;' }, then: [ lvgl.image.update: { id: gy_x0, src: img_gy_expl1 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_gy_exage)[0]==2;' }, then: [ lvgl.image.update: { id: gy_x0, src: img_gy_expl2 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_gy_exage)[0]==3;' }, then: [ lvgl.image.update: { id: gy_x0, src: img_gy_expl3 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_gy_exage)[0]==4;' }, then: [ lvgl.image.update: { id: gy_x0, src: img_gy_expl4 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_gy_exage)[0]==5;' }, then: [ lvgl.image.update: { id: gy_x0, src: img_gy_expl5 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_gy_exage)[0]==6;' }, then: [ lvgl.image.update: { id: gy_x0, src: img_gy_expl6 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_gy_exage)[0]>=7;' }, then: [ lvgl.image.update: { id: gy_x0, src: img_gy_expl7 } ] }
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_gy_exage)[1] < 0;' }
|
||||
then: [ lvgl.widget.hide: gy_x1 ]
|
||||
else:
|
||||
- lvgl.widget.show: gy_x1
|
||||
- if: { condition: { lambda: 'return id(g_gy_exage)[1]==0;' }, then: [ lvgl.image.update: { id: gy_x1, src: img_gy_expl0 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_gy_exage)[1]==1;' }, then: [ lvgl.image.update: { id: gy_x1, src: img_gy_expl1 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_gy_exage)[1]==2;' }, then: [ lvgl.image.update: { id: gy_x1, src: img_gy_expl2 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_gy_exage)[1]==3;' }, then: [ lvgl.image.update: { id: gy_x1, src: img_gy_expl3 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_gy_exage)[1]==4;' }, then: [ lvgl.image.update: { id: gy_x1, src: img_gy_expl4 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_gy_exage)[1]==5;' }, then: [ lvgl.image.update: { id: gy_x1, src: img_gy_expl5 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_gy_exage)[1]==6;' }, then: [ lvgl.image.update: { id: gy_x1, src: img_gy_expl6 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_gy_exage)[1]>=7;' }, then: [ lvgl.image.update: { id: gy_x1, src: img_gy_expl7 } ] }
|
||||
- id: gy_hud
|
||||
then:
|
||||
- lvgl.label.update: { id: lbl_gy_score, text: !lambda 'char b[12]; snprintf(b,sizeof(b),"%d",id(g_gy_score)); return std::string(b);' }
|
||||
- lvgl.label.update: { id: lbl_gy_lives, text: !lambda 'char b[16]; snprintf(b,sizeof(b),"LIVES %d",id(g_gy_lives)<0?0:id(g_gy_lives)); return std::string(b);' }
|
||||
# park every gameplay widget off-screen
|
||||
- id: gy_clear
|
||||
then:
|
||||
- lambda: |-
|
||||
lv_obj_t *BB[6] = { id(gy_b0), id(gy_b1), id(gy_b2), id(gy_b3), id(gy_b4), id(gy_b5) };
|
||||
for (int k = 0; k < 6; k++) lv_obj_set_pos(BB[k], 400, 400);
|
||||
lv_obj_t *EE[10] = { id(gy_e0), id(gy_e1), id(gy_e2), id(gy_e3), id(gy_e4), id(gy_e5), id(gy_e6), id(gy_e7), id(gy_e8), id(gy_e9) };
|
||||
for (int k = 0; k < 10; k++) lv_obj_set_pos(EE[k], 400, 400);
|
||||
lv_obj_t *SF[6] = { id(gy_s0), id(gy_s1), id(gy_s2), id(gy_s3), id(gy_s4), id(gy_s5) };
|
||||
for (int k = 0; k < 6; k++) lv_obj_set_pos(SF[k], 400, 400);
|
||||
lv_obj_set_pos(id(gy_ship), 400, 400);
|
||||
- lvgl.widget.hide: gy_x0
|
||||
- lvgl.widget.hide: gy_x1
|
||||
- id: gy_start
|
||||
then:
|
||||
- lambda: |-
|
||||
id(g_gy_state) = 1; id(g_gy_score) = 0; id(g_gy_lives) = 3; id(g_gy_dead) = false;
|
||||
id(g_gy_ship_ang) = -1.5708f; // start at the top
|
||||
id(g_gy_fire_clk) = 0; id(g_gy_spawn_clk) = 0;
|
||||
for (int k = 0; k < 6; k++) id(g_gy_bon)[k] = false;
|
||||
for (int k = 0; k < 10; k++) id(g_gy_eon)[k] = false;
|
||||
for (int k = 0; k < 6; k++) { id(g_gy_sr)[k] = 6.0f + (esp_random() % 168); id(g_gy_sa)[k] = (esp_random() % 62832) / 10000.0f; }
|
||||
for (int s = 0; s < 2; s++) id(g_gy_exage)[s] = -1;
|
||||
id(g_gy_shipframe) = -1; // force a ship-sprite refresh on the first tick
|
||||
id(g_knob_capture) = true; id(g_knob_delta) = 0;
|
||||
- script.execute: gy_screen
|
||||
- script.execute: gy_hud
|
||||
- id: gy_over
|
||||
then:
|
||||
- lambda: |-
|
||||
id(g_gy_state) = 2;
|
||||
int s = id(g_gy_score);
|
||||
if (s > id(g_gy_best)) id(g_gy_best) = s;
|
||||
for (int i = 0; i < 10; i++) {
|
||||
if (s > id(g_gy_top)[i]) {
|
||||
for (int j = 9; j > i; j--) id(g_gy_top)[j] = id(g_gy_top)[j-1];
|
||||
id(g_gy_top)[i] = s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
- script.execute: gy_screen
|
||||
# screen state + the single knob-capture authority (captured only in play + game over)
|
||||
- id: gy_screen
|
||||
then:
|
||||
- lambda: 'id(g_knob_capture) = (id(g_gy_state) == 1 || id(g_gy_state) == 2);'
|
||||
- script.execute: gy_clear
|
||||
- lvgl.widget.hide: gy_logo
|
||||
- lvgl.widget.hide: gy_ring
|
||||
- lvgl.widget.hide: gy_ring2
|
||||
- lvgl.widget.hide: gy_ring3
|
||||
- lvgl.widget.hide: gy_core
|
||||
- lvgl.widget.hide: lbl_gy_score
|
||||
- lvgl.widget.hide: lbl_gy_lives
|
||||
- lvgl.widget.hide: lbl_gy_title
|
||||
- lvgl.widget.hide: lbl_gy_sub
|
||||
- lvgl.widget.hide: btn_gy_start
|
||||
- lvgl.widget.hide: btn_gy_howto
|
||||
- lvgl.widget.hide: btn_gy_scores
|
||||
- lvgl.widget.hide: lbl_gy_over
|
||||
- lvgl.widget.hide: lbl_gy_rec
|
||||
- lvgl.widget.hide: btn_gy_replay
|
||||
- lvgl.widget.hide: btn_gy_exit
|
||||
- lvgl.widget.hide: lbl_gy_howto_t
|
||||
- lvgl.widget.hide: gy_hb1
|
||||
- lvgl.widget.hide: gy_hb2
|
||||
- lvgl.widget.hide: gy_hb3
|
||||
- lvgl.widget.hide: gy_hb4
|
||||
- lvgl.widget.hide: lbl_gy_howto1
|
||||
- lvgl.widget.hide: lbl_gy_howto2
|
||||
- lvgl.widget.hide: lbl_gy_howto3
|
||||
- lvgl.widget.hide: lbl_gy_howto4
|
||||
- lvgl.widget.hide: gy_life_icon
|
||||
- lvgl.widget.hide: lbl_gy_scores_t
|
||||
- lvgl.widget.hide: lbl_gy_scores1
|
||||
- lvgl.widget.hide: lbl_gy_scores2
|
||||
- lvgl.widget.hide: btn_gy_back
|
||||
- if: # MENU
|
||||
condition: { lambda: 'return id(g_gy_state) == 0;' }
|
||||
then:
|
||||
- lvgl.widget.show: gy_logo
|
||||
- lvgl.widget.show: btn_gy_start
|
||||
- lvgl.widget.show: btn_gy_howto
|
||||
- lvgl.widget.show: btn_gy_scores
|
||||
- if: # PLAY
|
||||
condition: { lambda: 'return id(g_gy_state) == 1;' }
|
||||
then:
|
||||
- lvgl.widget.show: gy_ring
|
||||
- lvgl.widget.show: gy_ring2
|
||||
- lvgl.widget.show: gy_ring3
|
||||
- lvgl.widget.show: gy_core
|
||||
- lvgl.widget.show: lbl_gy_score
|
||||
- lvgl.widget.show: lbl_gy_lives
|
||||
- script.execute: gy_render
|
||||
- if: # GAME OVER
|
||||
condition: { lambda: 'return id(g_gy_state) == 2;' }
|
||||
then:
|
||||
- lvgl.widget.show: gy_logo
|
||||
- lvgl.widget.show: lbl_gy_over
|
||||
- lvgl.widget.show: lbl_gy_rec
|
||||
- lvgl.widget.show: btn_gy_replay
|
||||
- lvgl.widget.show: btn_gy_exit
|
||||
- lvgl.label.update: { id: lbl_gy_over, text: !lambda 'char b[24]; snprintf(b,sizeof(b),"Score: %d", id(g_gy_score)); return std::string(b);' }
|
||||
- lvgl.label.update: { id: lbl_gy_rec, text: !lambda 'char b[24]; snprintf(b,sizeof(b),"Best: %d", id(g_gy_best)); return std::string(b);' }
|
||||
- if: # HOW TO PLAY
|
||||
condition: { lambda: 'return id(g_gy_state) == 3;' }
|
||||
then:
|
||||
- lvgl.widget.show: lbl_gy_howto_t
|
||||
- lvgl.widget.show: gy_hb1
|
||||
- lvgl.widget.show: gy_hb2
|
||||
- lvgl.widget.show: gy_hb3
|
||||
- lvgl.widget.show: gy_hb4
|
||||
- lvgl.widget.show: lbl_gy_howto1
|
||||
- lvgl.widget.show: lbl_gy_howto2
|
||||
- lvgl.widget.show: lbl_gy_howto3
|
||||
- lvgl.widget.show: lbl_gy_howto4
|
||||
- lvgl.widget.show: gy_life_icon
|
||||
- lvgl.widget.show: btn_gy_back
|
||||
- if: # SCORES (top 10)
|
||||
condition: { lambda: 'return id(g_gy_state) == 4;' }
|
||||
then:
|
||||
- lvgl.widget.show: lbl_gy_scores_t
|
||||
- lvgl.widget.show: lbl_gy_scores1
|
||||
- lvgl.widget.show: lbl_gy_scores2
|
||||
- lvgl.widget.show: btn_gy_back
|
||||
- lvgl.label.update:
|
||||
id: lbl_gy_scores1
|
||||
text: !lambda |-
|
||||
std::string s; char b[24];
|
||||
for (int i = 0; i < 5; i++) { if(i) s += "\n"; if(id(g_gy_top)[i]>0){ snprintf(b,sizeof(b),"%d. %d",i+1,id(g_gy_top)[i]); } else { snprintf(b,sizeof(b),"%d. -",i+1); } s += b; }
|
||||
return s;
|
||||
- lvgl.label.update:
|
||||
id: lbl_gy_scores2
|
||||
text: !lambda |-
|
||||
std::string s; char b[24];
|
||||
for (int i = 5; i < 10; i++) { if(i>5) s += "\n"; if(id(g_gy_top)[i]>0){ snprintf(b,sizeof(b),"%d. %d",i+1,id(g_gy_top)[i]); } else { snprintf(b,sizeof(b),"%d. -",i+1); } s += b; }
|
||||
return s;
|
||||
|
||||
interval:
|
||||
- interval: 250ms
|
||||
then:
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_gy_reset) || id(g_factory_reset);' }
|
||||
then:
|
||||
- lambda: 'for (int i = 0; i < 10; i++) id(g_gy_top)[i] = 0; id(g_gy_best) = 0; id(g_gy_reset) = false;'
|
||||
- interval: 50ms
|
||||
then:
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_nav_req) && id(g_base)==10;' }
|
||||
then:
|
||||
- lambda: 'id(g_nav_req) = false; id(g_gy_state) = 0; id(g_knob_delta) = 0;'
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==0;' }, then: [lvgl.page.show: { id: page_gyruss, animation: move_left, time: 250ms }] }
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==1;' }, then: [lvgl.page.show: { id: page_gyruss, animation: move_right, time: 250ms }] }
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==2;' }, then: [lvgl.page.show: { id: page_gyruss, animation: move_top, time: 250ms }] }
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==3;' }, then: [lvgl.page.show: { id: page_gyruss, animation: move_bottom, time: 250ms }] }
|
||||
- script.execute: gy_screen
|
||||
# Game tick: aim (knob) + auto-fire + spiral the foes + collisions; all positioning in gy_render.
|
||||
- interval: 50ms
|
||||
then:
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_base) == 10 && id(g_gy_state) == 1;' }
|
||||
then:
|
||||
- lambda: |-
|
||||
id(g_gy_hud_dirty) = false; id(g_gy_shipdirty) = false;
|
||||
id(g_gy_tick)++;
|
||||
int kd = id(g_knob_delta); id(g_knob_delta) = 0;
|
||||
id(g_gy_ship_ang) += kd * 0.30f; // aim sensitivity: ~17 deg per knob detent
|
||||
// auto-fire a bullet down the ship's radial
|
||||
if (++id(g_gy_fire_clk) >= 5) {
|
||||
id(g_gy_fire_clk) = 0;
|
||||
for (int k = 0; k < 6; k++) if (!id(g_gy_bon)[k]) { id(g_gy_bon)[k] = true; id(g_gy_br)[k] = 138.0f; id(g_gy_ba)[k] = id(g_gy_ship_ang); break; }
|
||||
}
|
||||
for (int k = 0; k < 6; k++) if (id(g_gy_bon)[k]) { id(g_gy_br)[k] -= 9.0f; if (id(g_gy_br)[k] < 12.0f) id(g_gy_bon)[k] = false; }
|
||||
// spawn foes from the core (faster as the score climbs)
|
||||
int sw = 24 - id(g_gy_score) / 200; if (sw < 9) sw = 9;
|
||||
if (++id(g_gy_spawn_clk) >= sw) {
|
||||
id(g_gy_spawn_clk) = 0;
|
||||
for (int k = 0; k < 10; k++) if (!id(g_gy_eon)[k]) {
|
||||
float a = (esp_random() % 62832) / 10000.0f;
|
||||
id(g_gy_eon)[k] = true; id(g_gy_er)[k] = 32.0f; id(g_gy_ea)[k] = a; break; // start at the core's rim (not under it) to avoid central overdraw
|
||||
}
|
||||
}
|
||||
// age existing explosions first (8-frame sprite); refresh sprite while any is live/expiring
|
||||
id(g_gy_exact) = false;
|
||||
for (int s = 0; s < 2; s++) if (id(g_gy_exage)[s] >= 0) {
|
||||
id(g_gy_exact) = true;
|
||||
if (++id(g_gy_exage)[s] > 7) id(g_gy_exage)[s] = -1;
|
||||
}
|
||||
// foes spiral outward; ANY foe that reaches the rim costs a life and explodes there
|
||||
float esp = 1.5f + id(g_gy_score) / 1500.0f; if (esp > 3.2f) esp = 3.2f;
|
||||
for (int k = 0; k < 10; k++) if (id(g_gy_eon)[k]) {
|
||||
id(g_gy_er)[k] += esp; id(g_gy_ea)[k] += 0.05f;
|
||||
if (id(g_gy_er)[k] >= 146.0f) {
|
||||
id(g_gy_eon)[k] = false;
|
||||
id(g_gy_lives)--; id(g_gy_hud_dirty) = true; if (id(g_gy_lives) <= 0) id(g_gy_dead) = true;
|
||||
for (int s = 0; s < 2; s++) if (id(g_gy_exage)[s] < 0) { id(g_gy_exx)[s] = cosf(id(g_gy_ea)[k]) * 146.0f; id(g_gy_exy)[s] = sinf(id(g_gy_ea)[k]) * 146.0f; id(g_gy_exage)[s] = 0; id(g_gy_exact) = true; break; }
|
||||
}
|
||||
}
|
||||
// depth starfield (throttled to every 2nd tick): rush outward, respawn near the centre
|
||||
if ((id(g_gy_tick) & 1) == 0) for (int k = 0; k < 6; k++) {
|
||||
id(g_gy_sr)[k] += id(g_gy_sr)[k] * 0.10f + 2.0f;
|
||||
if (id(g_gy_sr)[k] > 176.0f) { id(g_gy_sr)[k] = 4.0f + (esp_random() % 14); id(g_gy_sa)[k] = (esp_random() % 62832) / 10000.0f; }
|
||||
}
|
||||
// ship rotation frame (every 2nd tick, on the ODD ticks so it doesn't share a tick with
|
||||
// the starfield move above - spreads the two throttled costs; position stays 50ms)
|
||||
if ((id(g_gy_tick) & 1) == 1) {
|
||||
int bk = (int) lroundf(id(g_gy_ship_ang) / 0.3926991f); bk %= 16; if (bk < 0) bk += 16;
|
||||
id(g_gy_shipdirty) = (bk != id(g_gy_shipframe));
|
||||
if (id(g_gy_shipdirty)) id(g_gy_shipframe) = bk;
|
||||
}
|
||||
// bullet vs foe (polar proximity)
|
||||
for (int b = 0; b < 6; b++) if (id(g_gy_bon)[b]) for (int e = 0; e < 10; e++) if (id(g_gy_eon)[e]) {
|
||||
float dr = id(g_gy_br)[b] - id(g_gy_er)[e]; if (dr < 0) dr = -dr;
|
||||
if (dr < 13.0f) {
|
||||
float da = id(g_gy_ba)[b] - id(g_gy_ea)[e]; da = atan2f(sinf(da), cosf(da));
|
||||
if (fabsf(da) < 0.28f) { id(g_gy_bon)[b] = false; id(g_gy_eon)[e] = false; id(g_gy_score) += 10; id(g_gy_hud_dirty) = true; break; }
|
||||
}
|
||||
}
|
||||
- if: { condition: { lambda: 'return id(g_gy_shipdirty);' }, then: [ script.execute: gy_shipspr ] }
|
||||
- if: { condition: { lambda: 'return id(g_gy_exact);' }, then: [ script.execute: gy_explspr ] }
|
||||
- script.execute: gy_render
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_gy_dead);' }
|
||||
then:
|
||||
- script.execute: gy_over
|
||||
else:
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_gy_hud_dirty);' }
|
||||
then:
|
||||
- script.execute: gy_hud
|
||||
|
||||
# Sprites are pulled from the repo /main/ raw URLs at compile time.
|
||||
# Third-party art (see assets/sprites/knobuss/CREDITS.txt).
|
||||
image:
|
||||
- id: img_gy_ship0
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship0.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_gy_ship1
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship1.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_gy_ship2
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship2.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_gy_ship3
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship3.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_gy_ship4
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship4.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_gy_ship5
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship5.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_gy_ship6
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship6.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_gy_ship7
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship7.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_gy_ship8
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship8.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_gy_ship9
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship9.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_gy_ship10
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship10.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_gy_ship11
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship11.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_gy_ship12
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship12.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_gy_ship13
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship13.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_gy_ship14
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship14.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_gy_ship15
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship15.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_gy_enemy0
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/enemy0.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_gy_enemy1
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/enemy1.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_gy_enemy2
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/enemy2.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_gy_enemy3
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/enemy3.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_gy_expl0
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/expl0.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_gy_expl1
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/expl1.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_gy_expl2
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/expl2.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_gy_expl3
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/expl3.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_gy_expl4
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/expl4.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_gy_expl5
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/expl5.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_gy_expl6
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/expl6.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_gy_expl7
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/expl7.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_gy_bullet
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/bullet.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_gy_core
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/core.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_gy_logo
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/knobuss-logo.png"
|
||||
type: RGB565
|
||||
- id: img_gy_life
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/life.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
lvgl:
|
||||
pages:
|
||||
- id: page_gyruss
|
||||
bg_color: 0x070611
|
||||
scrollbar_mode: "off"
|
||||
scrollable: false
|
||||
widgets:
|
||||
# Knobuss title screen (full-screen menu backdrop, behind the buttons) - hidden in play
|
||||
- image: { id: gy_logo, align: CENTER, x: 0, y: 0, src: img_gy_logo, hidden: true }
|
||||
# starfield (declared first = drawn behind everything) - parked until play
|
||||
- obj: { id: gy_s0, x: 400, y: 400, width: 3, height: 3, radius: 999, bg_color: 0x6076A4, bg_opa: COVER, border_width: 0, pad_all: 0 }
|
||||
- obj: { id: gy_s1, x: 400, y: 400, width: 2, height: 2, radius: 999, bg_color: 0x32466E, bg_opa: COVER, border_width: 0, pad_all: 0 }
|
||||
- obj: { id: gy_s2, x: 400, y: 400, width: 3, height: 3, radius: 999, bg_color: 0x6076A4, bg_opa: COVER, border_width: 0, pad_all: 0 }
|
||||
- obj: { id: gy_s3, x: 400, y: 400, width: 2, height: 2, radius: 999, bg_color: 0x32466E, bg_opa: COVER, border_width: 0, pad_all: 0 }
|
||||
- obj: { id: gy_s4, x: 400, y: 400, width: 3, height: 3, radius: 999, bg_color: 0x6076A4, bg_opa: COVER, border_width: 0, pad_all: 0 }
|
||||
- obj: { id: gy_s5, x: 400, y: 400, width: 2, height: 2, radius: 999, bg_color: 0x32466E, bg_opa: COVER, border_width: 0, pad_all: 0 }
|
||||
# concentric guide rings (static) suggest a tube receding to the core - free depth cue
|
||||
- obj: { id: gy_ring3, align: CENTER, x: 0, y: 0, width: 112, height: 112, radius: 56, bg_opa: TRANSP, border_width: 1, border_color: 0x1A2742, hidden: true }
|
||||
- obj: { id: gy_ring2, align: CENTER, x: 0, y: 0, width: 200, height: 200, radius: 100, bg_opa: TRANSP, border_width: 1, border_color: 0x1E2C4A, hidden: true }
|
||||
- obj: { id: gy_ring, align: CENTER, x: 0, y: 0, width: 300, height: 300, radius: 150, bg_opa: TRANSP, border_width: 2, border_color: 0x2A3C66, hidden: true }
|
||||
- image: { id: gy_core, align: CENTER, x: 0, y: 0, src: img_gy_core, hidden: true }
|
||||
# foes: 4 glowing-orb colours cycled across the 10 slots (fixed src - no runtime swap)
|
||||
- image: { id: gy_e0, x: 400, y: 400, src: img_gy_enemy0 }
|
||||
- image: { id: gy_e1, x: 400, y: 400, src: img_gy_enemy1 }
|
||||
- image: { id: gy_e2, x: 400, y: 400, src: img_gy_enemy2 }
|
||||
- image: { id: gy_e3, x: 400, y: 400, src: img_gy_enemy3 }
|
||||
- image: { id: gy_e4, x: 400, y: 400, src: img_gy_enemy0 }
|
||||
- image: { id: gy_e5, x: 400, y: 400, src: img_gy_enemy1 }
|
||||
- image: { id: gy_e6, x: 400, y: 400, src: img_gy_enemy2 }
|
||||
- image: { id: gy_e7, x: 400, y: 400, src: img_gy_enemy3 }
|
||||
- image: { id: gy_e8, x: 400, y: 400, src: img_gy_enemy0 }
|
||||
- image: { id: gy_e9, x: 400, y: 400, src: img_gy_enemy1 }
|
||||
- image: { id: gy_b0, x: 400, y: 400, src: img_gy_bullet }
|
||||
- image: { id: gy_b1, x: 400, y: 400, src: img_gy_bullet }
|
||||
- image: { id: gy_b2, x: 400, y: 400, src: img_gy_bullet }
|
||||
- image: { id: gy_b3, x: 400, y: 400, src: img_gy_bullet }
|
||||
- image: { id: gy_b4, x: 400, y: 400, src: img_gy_bullet }
|
||||
- image: { id: gy_b5, x: 400, y: 400, src: img_gy_bullet }
|
||||
- image: { id: gy_ship, x: 400, y: 400, src: img_gy_ship0 }
|
||||
# explosion sprites (8-frame), parked + hidden until a foe hits the rim
|
||||
- image: { id: gy_x0, x: 400, y: 400, src: img_gy_expl0, hidden: true }
|
||||
- image: { id: gy_x1, x: 400, y: 400, src: img_gy_expl0, hidden: true }
|
||||
- label: { id: lbl_gy_score, align: CENTER, x: 0, y: -150, text: "0", text_font: font_med, text_color: 0xCFE4FF, hidden: true }
|
||||
- label: { id: lbl_gy_lives, align: CENTER, x: 0, y: 150, text: "LIVES 3", text_font: font_artist, text_color: 0xFF7A6A, hidden: true }
|
||||
- label: { id: lbl_gy_title, align: CENTER, x: 0, y: -86, text: "KNOBUSS", text_font: font_sel, text_color: 0x46E3D0, hidden: true }
|
||||
- label: { id: lbl_gy_sub, align: CENTER, x: 0, y: -52, text: "tube shooter", text_font: font_artist, text_color: 0x6B7BA6, hidden: true }
|
||||
- button:
|
||||
id: btn_gy_start
|
||||
align: CENTER
|
||||
y: -8
|
||||
width: 150
|
||||
height: 44
|
||||
radius: 22
|
||||
bg_color: 0x39C6E6
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
on_click: [ script.execute: gy_start ]
|
||||
widgets:
|
||||
- label: { align: CENTER, text: "Start", text_font: font_med, text_color: 0x06131A }
|
||||
- button:
|
||||
id: btn_gy_howto
|
||||
align: CENTER
|
||||
y: 40
|
||||
width: 150
|
||||
height: 44
|
||||
radius: 22
|
||||
bg_color: 0x1C2A44
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
on_click: [ lambda: 'id(g_gy_state) = 3;', script.execute: gy_screen ]
|
||||
widgets:
|
||||
- label: { align: CENTER, text: "How to play?", text_font: font_med, text_color: 0xC4D2F0 }
|
||||
- button:
|
||||
id: btn_gy_scores
|
||||
align: CENTER
|
||||
y: 88
|
||||
width: 150
|
||||
height: 44
|
||||
radius: 22
|
||||
bg_color: 0x1C2A44
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
on_click: [ lambda: 'id(g_gy_state) = 4;', script.execute: gy_screen ]
|
||||
widgets:
|
||||
- label: { align: CENTER, text: "Scores", text_font: font_med, text_color: 0xC4D2F0 }
|
||||
- label: { id: lbl_gy_over, align: CENTER, x: 0, y: -10, text_align: CENTER, text: "", text_font: font_sel, text_color: 0xCFE4FF, hidden: true }
|
||||
- label: { id: lbl_gy_rec, align: CENTER, x: 0, y: 22, text_align: CENTER, text: "", text_font: font_artist, text_color: 0x6B7BA6, hidden: true }
|
||||
- button:
|
||||
id: btn_gy_replay
|
||||
align: CENTER
|
||||
x: -66
|
||||
y: 62
|
||||
width: 112
|
||||
height: 48
|
||||
radius: 24
|
||||
bg_color: 0x39C6E6
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
hidden: true
|
||||
on_click: [ script.execute: gy_start ]
|
||||
widgets:
|
||||
- label: { align: CENTER, text: "Play", text_font: font_med, text_color: 0x06131A }
|
||||
- button:
|
||||
id: btn_gy_exit
|
||||
align: CENTER
|
||||
x: 66
|
||||
y: 62
|
||||
width: 112
|
||||
height: 48
|
||||
radius: 24
|
||||
bg_color: 0x2A2E3D
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
hidden: true
|
||||
on_click: [ lambda: 'id(g_gy_state) = 0;', script.execute: gy_screen ]
|
||||
widgets:
|
||||
- label: { align: CENTER, text: "Exit", text_font: font_med, text_color: 0xFFFFFF }
|
||||
- label: { id: lbl_gy_howto_t, align: CENTER, y: -104, text: "HOW TO PLAY?", text_font: font_sel, text_color: 0x46E3D0, hidden: true }
|
||||
- obj: { id: gy_hb1, align: CENTER, x: -96, y: -52, width: 24, height: 24, radius: 12, bg_color: 0x2C6FB2, bg_opa: COVER, border_width: 0, pad_all: 0, hidden: true, widgets: [ label: { align: CENTER, text: "1", text_font: font_artist, text_color: 0xFFFFFF } ] }
|
||||
- obj: { id: gy_hb2, align: CENTER, x: -96, y: -16, width: 24, height: 24, radius: 12, bg_color: 0x2C6FB2, bg_opa: COVER, border_width: 0, pad_all: 0, hidden: true, widgets: [ label: { align: CENTER, text: "2", text_font: font_artist, text_color: 0xFFFFFF } ] }
|
||||
- obj: { id: gy_hb3, align: CENTER, x: -96, y: 20, width: 24, height: 24, radius: 12, bg_color: 0x2C6FB2, bg_opa: COVER, border_width: 0, pad_all: 0, hidden: true, widgets: [ label: { align: CENTER, text: "3", text_font: font_artist, text_color: 0xFFFFFF } ] }
|
||||
- obj: { id: gy_hb4, align: CENTER, x: -96, y: 56, width: 24, height: 24, radius: 12, bg_color: 0x2C6FB2, bg_opa: COVER, border_width: 0, pad_all: 0, hidden: true, widgets: [ label: { align: CENTER, text: "4", text_font: font_artist, text_color: 0xFFFFFF } ] }
|
||||
- label: { id: lbl_gy_howto1, align_to: { id: gy_hb1, align: OUT_RIGHT_MID, x: 8 }, text: "Turn knob to aim", text_font: font_med, text_color: 0xC4D2F0, hidden: true }
|
||||
- label: { id: lbl_gy_howto2, align_to: { id: gy_hb2, align: OUT_RIGHT_MID, x: 8 }, text: "You auto-fire inward", text_font: font_med, text_color: 0xC4D2F0, hidden: true }
|
||||
- label: { id: lbl_gy_howto3, align_to: { id: gy_hb3, align: OUT_RIGHT_MID, x: 8 }, text: "Blast foes before the rim", text_font: font_med, text_color: 0xC4D2F0, hidden: true }
|
||||
- label: { id: lbl_gy_howto4, align_to: { id: gy_hb4, align: OUT_RIGHT_MID, x: 8 }, text: "You have three lives", text_font: font_med, text_color: 0xC4D2F0, hidden: true }
|
||||
- image: { id: gy_life_icon, align_to: { id: lbl_gy_howto4, align: OUT_RIGHT_MID, x: 8 }, src: img_gy_life, hidden: true }
|
||||
- label: { id: lbl_gy_scores_t, align: CENTER, y: -116, text: "SCORES", text_font: font_sel, text_color: 0x46E3D0, hidden: true }
|
||||
- label: { id: lbl_gy_scores1, align: CENTER, x: -52, y: 0, text: "", text_align: LEFT, text_font: font_artist, text_color: 0xC4D2F0, hidden: true }
|
||||
- label: { id: lbl_gy_scores2, align: CENTER, x: 56, y: 0, text: "", text_align: LEFT, text_font: font_artist, text_color: 0xC4D2F0, hidden: true }
|
||||
- button:
|
||||
id: btn_gy_back
|
||||
align: CENTER
|
||||
y: 120
|
||||
width: 130
|
||||
height: 42
|
||||
radius: 21
|
||||
bg_color: 0x2A2E3D
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
hidden: true
|
||||
on_click: [ lambda: 'id(g_gy_state) = 0;', script.execute: gy_screen ]
|
||||
widgets:
|
||||
- label: { align: CENTER, text: "Back", text_font: font_med, text_color: 0xFFFFFF }
|
||||
@@ -0,0 +1,265 @@
|
||||
# Player - optional carousel screen (base id 1). Full-screen album art + transport.
|
||||
# Remove it: drop this file from your config's package "files:" list.
|
||||
# Volume control + the volume overlay stay in the core; this is only the now-playing SCREEN.
|
||||
|
||||
esphome:
|
||||
on_boot:
|
||||
- priority: 690
|
||||
then:
|
||||
- lambda: 'id(g_present)[1] = true;' # register after clock
|
||||
|
||||
globals:
|
||||
- id: g_last_pic
|
||||
type: std::string
|
||||
restore_value: no
|
||||
- id: g_pos # position in the track (s) - progress bar
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
- id: g_dur # track length (s)
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
|
||||
# Required by online_image (fetching album art over HTTP)
|
||||
http_request:
|
||||
timeout: 10s
|
||||
verify_ssl: false
|
||||
|
||||
# Album art fetched dynamically from HA (entity_picture is a signed URL)
|
||||
|
||||
online_image:
|
||||
- id: album_art
|
||||
url: "${ha_base_url}/"
|
||||
format: JPEG
|
||||
type: RGB565
|
||||
resize: 360x360
|
||||
# Album art FIX: after download you must EXPLICITLY refresh the LVGL widget (it won't redraw itself)
|
||||
on_download_finished:
|
||||
- lvgl.image.update:
|
||||
id: ui_album
|
||||
src: album_art
|
||||
on_error:
|
||||
- logger.log: "Failed to fetch album art"
|
||||
|
||||
sensor:
|
||||
- platform: homeassistant
|
||||
id: media_pos
|
||||
entity_id: ${player_entity}
|
||||
attribute: media_position
|
||||
on_value:
|
||||
- lambda: 'id(g_pos) = (int) x;'
|
||||
- platform: homeassistant
|
||||
id: media_dur
|
||||
entity_id: ${player_entity}
|
||||
attribute: media_duration
|
||||
on_value:
|
||||
- lambda: 'id(g_dur) = (int) x;'
|
||||
|
||||
text_sensor:
|
||||
- platform: homeassistant
|
||||
id: media_title
|
||||
entity_id: ${player_entity}
|
||||
attribute: media_title
|
||||
on_value:
|
||||
- lvgl.label.update:
|
||||
id: lbl_title
|
||||
text: !lambda 'return x.empty() ? std::string("-") : x;'
|
||||
- platform: homeassistant
|
||||
id: media_artist
|
||||
entity_id: ${player_entity}
|
||||
attribute: media_artist
|
||||
on_value:
|
||||
- lvgl.label.update:
|
||||
id: lbl_artist
|
||||
text: !lambda 'return x;'
|
||||
- platform: homeassistant
|
||||
id: media_pic
|
||||
entity_id: ${player_entity}
|
||||
attribute: entity_picture
|
||||
on_value:
|
||||
- lambda: |-
|
||||
if (x.empty()) return;
|
||||
std::string url = x.rfind("http", 0) == 0 ? x : std::string("${ha_base_url}") + x;
|
||||
if (url == id(g_last_pic)) return; // same album art -> don't reload (less flicker)
|
||||
id(g_last_pic) = url;
|
||||
id(album_art)->set_url(url.c_str());
|
||||
id(album_art)->update();
|
||||
# Player state -> only "playing" shows the player; pause/stop = player disappears (clock)
|
||||
- platform: homeassistant
|
||||
id: media_state
|
||||
entity_id: ${player_entity}
|
||||
on_value:
|
||||
- lvgl.label.update: # icon on the button: pause when playing, play when not
|
||||
id: lbl_play
|
||||
text: !lambda 'return id(media_state).state == "playing" ? std::string("") : std::string("");'
|
||||
- if:
|
||||
condition:
|
||||
lambda: 'return !id(g_va_active) && !id(g_boot) && id(g_mode) == 0 && (id(g_base) == 0 || id(g_base) == 1);' # auto-switch clock<->player only when we're on the clock/player (and not during boot splash)
|
||||
then:
|
||||
# Only switch on an ACTUAL transition: home -> player when playback starts,
|
||||
# player -> home when it stops. (Avoids re-navigating on every media-state event,
|
||||
# incl. the state HA pushes at boot, which used to flip the home to Classic.)
|
||||
- if:
|
||||
condition: { lambda: 'return x == "playing" && id(g_base) == 0;' }
|
||||
then:
|
||||
- lvgl.page.show: { id: page_player, animation: move_left, time: 250ms }
|
||||
- lambda: 'id(g_base) = 1;'
|
||||
- if:
|
||||
condition: { lambda: 'return x != "playing" && id(g_base) == 1;' }
|
||||
then:
|
||||
# back to the home base, respecting the active watchface (Classic shows
|
||||
# page_home directly; other faces self-show via g_wf_req) - don't hardcode Classic
|
||||
- lambda: 'id(g_base) = 0; id(g_nav_anim) = 1;'
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_watchface) == 0;' }
|
||||
then:
|
||||
- lvgl.page.show: { id: page_home, animation: move_right, time: 250ms }
|
||||
else:
|
||||
- lambda: 'id(g_wf_req) = true;'
|
||||
|
||||
interval:
|
||||
# nav: show this screen when it is selected in the carousel
|
||||
- interval: 50ms
|
||||
then:
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_nav_req) && id(g_base)==1;' }
|
||||
then:
|
||||
- lambda: 'id(g_nav_req) = false;'
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==0;' }, then: [lvgl.page.show: { id: page_player, animation: move_left, time: 250ms }] }
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==1;' }, then: [lvgl.page.show: { id: page_player, animation: move_right, time: 250ms }] }
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==2;' }, then: [lvgl.page.show: { id: page_player, animation: move_top, time: 250ms }] }
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==3;' }, then: [lvgl.page.show: { id: page_player, animation: move_bottom, time: 250ms }] }
|
||||
# track progress (position / time / progress bar)
|
||||
- interval: 1s
|
||||
then:
|
||||
# Track progress: tick the position while playing + update time and bar
|
||||
- if:
|
||||
condition:
|
||||
lambda: 'return id(media_state).state == "playing" && id(g_dur) > 0 && id(g_pos) < id(g_dur);'
|
||||
then:
|
||||
- lambda: 'id(g_pos)++;'
|
||||
- lvgl.label.update:
|
||||
id: lbl_time
|
||||
text: !lambda |-
|
||||
int p = id(g_pos), d = id(g_dur);
|
||||
if (d <= 0) return std::string("");
|
||||
char b[20]; snprintf(b, sizeof(b), "%d:%02d | %d:%02d", p/60, p%60, d/60, d%60);
|
||||
return std::string(b);
|
||||
- lvgl.bar.update:
|
||||
id: prog_bar
|
||||
value: !lambda 'return id(g_dur) > 0 ? (int)(1000L * id(g_pos) / id(g_dur)) : 0;'
|
||||
|
||||
lvgl:
|
||||
pages:
|
||||
# --- Player page: full-screen album art + scrim + title/artist + volume arc ---
|
||||
- id: page_player
|
||||
bg_color: 0x000000
|
||||
widgets:
|
||||
# Album art covering the whole round screen (360x360; the bezel hides the corners)
|
||||
- image:
|
||||
id: ui_album
|
||||
align: CENTER
|
||||
src: album_art
|
||||
# Dim the whole thing under text and buttons (readability)
|
||||
- obj:
|
||||
align: CENTER
|
||||
width: 360
|
||||
height: 360
|
||||
radius: 0
|
||||
border_width: 0
|
||||
bg_color: 0x000000
|
||||
bg_opa: 42%
|
||||
# Title (scroll) + artist (clip) - in the upper part
|
||||
- label:
|
||||
id: lbl_title
|
||||
align: CENTER
|
||||
y: -72
|
||||
width: 250
|
||||
long_mode: scroll_circular
|
||||
text_align: CENTER
|
||||
text_font: font_med
|
||||
text_color: 0xFFFFFF
|
||||
text: "-"
|
||||
- label:
|
||||
id: lbl_artist
|
||||
align: CENTER
|
||||
y: -46
|
||||
width: 230
|
||||
long_mode: clip
|
||||
text_align: CENTER
|
||||
text_font: font_artist
|
||||
text_color: 0x9FC7BE
|
||||
text: ""
|
||||
# Transport: prev / play-pause / next (touch buttons)
|
||||
- button:
|
||||
id: btn_prev
|
||||
align: CENTER
|
||||
x: -84
|
||||
y: 6
|
||||
width: 50
|
||||
height: 50
|
||||
radius: 25
|
||||
bg_color: 0x223230
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
on_click:
|
||||
- homeassistant.action:
|
||||
action: media_player.media_previous_track
|
||||
data: { entity_id: "${player_entity}" }
|
||||
widgets:
|
||||
- label: { align: CENTER, text: "", text_font: montserrat_22, text_color: 0xCFEAE4 }
|
||||
- button:
|
||||
id: btn_play
|
||||
align: CENTER
|
||||
x: 0
|
||||
y: 6
|
||||
width: 74
|
||||
height: 74
|
||||
radius: 37
|
||||
bg_color: 0x2FE6CF
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
on_click:
|
||||
- homeassistant.action:
|
||||
action: media_player.media_play_pause
|
||||
data: { entity_id: "${player_entity}" }
|
||||
widgets:
|
||||
- label: { id: lbl_play, align: CENTER, text: "", text_font: montserrat_28, text_color: 0x06201C }
|
||||
- button:
|
||||
id: btn_next
|
||||
align: CENTER
|
||||
x: 84
|
||||
y: 6
|
||||
width: 50
|
||||
height: 50
|
||||
radius: 25
|
||||
bg_color: 0x223230
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
on_click:
|
||||
- homeassistant.action:
|
||||
action: media_player.media_next_track
|
||||
data: { entity_id: "${player_entity}" }
|
||||
widgets:
|
||||
- label: { align: CENTER, text: "", text_font: montserrat_22, text_color: 0xCFEAE4 }
|
||||
# Time + progress bar
|
||||
- label:
|
||||
id: lbl_time
|
||||
align: CENTER
|
||||
y: 64
|
||||
text: ""
|
||||
text_font: font_artist
|
||||
text_color: 0xCFEAE4
|
||||
- bar:
|
||||
id: prog_bar
|
||||
align: CENTER
|
||||
y: 90
|
||||
width: 210
|
||||
height: 5
|
||||
min_value: 0
|
||||
max_value: 1000
|
||||
value: 0
|
||||
border_width: 0
|
||||
indicator:
|
||||
bg_color: 0x2FE6CF
|
||||
@@ -0,0 +1,244 @@
|
||||
# Sensors / glance - optional carousel screen (base id 8).
|
||||
# Shows one configurable Home Assistant sensor at a time, big and readable; turn the KNOB to
|
||||
# cycle between them. Pulls straight from HA (no helper). Works for 1 sensor (no cycling) up to
|
||||
# 6 (knob scrolls, a "n/total" indicator appears). Any entity works - the state is shown as text
|
||||
# and the unit comes from the entity's unit_of_measurement attribute.
|
||||
#
|
||||
# Configure slots with the glanceN_entity / glanceN_name / glanceN_icon substitutions. Leave a
|
||||
# slot at its default (a non-existent entity) and it's skipped automatically. Icon names use the
|
||||
# same set as the control tiles; leave empty for no icon.
|
||||
#
|
||||
# Remove it: drop this file from the package "files:" list.
|
||||
|
||||
esphome:
|
||||
on_boot:
|
||||
- priority: 655
|
||||
then:
|
||||
- lambda: |-
|
||||
id(g_present)[8] = true; // register the sensors screen
|
||||
// resolve icon names -> glyphs (same set as the control tiles); "" -> no icon
|
||||
auto ic = [](const std::string &s) -> std::string {
|
||||
if (s == "bulb" || s == "light") return "\U000f0335";
|
||||
if (s == "lamp") return "\U000f06b5";
|
||||
if (s == "led" || s == "strip") return "\U000f07d6";
|
||||
if (s == "power") return "\U000f0425";
|
||||
if (s == "plug") return "\U000f06a5";
|
||||
if (s == "switch") return "\U000f0521";
|
||||
if (s == "fan") return "\U000f0210";
|
||||
if (s == "ac" || s == "aircon") return "\U000f001b";
|
||||
if (s == "heater" || s == "radiator") return "\U000f0438";
|
||||
if (s == "thermometer" || s == "temp") return "\U000f050f";
|
||||
if (s == "lock") return "\U000f033e";
|
||||
if (s == "unlock") return "\U000f033f";
|
||||
if (s == "door") return "\U000f081a";
|
||||
if (s == "blinds") return "\U000f00ac";
|
||||
if (s == "tv") return "\U000f0502";
|
||||
if (s == "speaker") return "\U000f04c3";
|
||||
if (s == "bell") return "\U000f009a";
|
||||
if (s == "wifi") return "\U000f05a9";
|
||||
if (s == "home") return "\U000f02dc";
|
||||
if (s == "flash") return "\U000f0241";
|
||||
if (s == "air" || s == "pm" || s == "airquality") return "\U000f0d44";
|
||||
if (s == "plant" || s == "sprout" || s == "moisture") return "\U000f0e66";
|
||||
if (s == "plex") return "\U000f06ba";
|
||||
if (s == "server") return "\U000f048b";
|
||||
if (s == "shield" || s == "alarm" || s == "security") return "\U000f068a";
|
||||
return s;
|
||||
};
|
||||
id(g_gl_glyph)[0] = ic("${glance1_icon}"); id(g_gl_name)[0] = "${glance1_name}";
|
||||
id(g_gl_glyph)[1] = ic("${glance2_icon}"); id(g_gl_name)[1] = "${glance2_name}";
|
||||
id(g_gl_glyph)[2] = ic("${glance3_icon}"); id(g_gl_name)[2] = "${glance3_name}";
|
||||
id(g_gl_glyph)[3] = ic("${glance4_icon}"); id(g_gl_name)[3] = "${glance4_name}";
|
||||
id(g_gl_glyph)[4] = ic("${glance5_icon}"); id(g_gl_name)[4] = "${glance5_name}";
|
||||
id(g_gl_glyph)[5] = ic("${glance6_icon}"); id(g_gl_name)[5] = "${glance6_name}";
|
||||
|
||||
globals:
|
||||
- id: g_gl_val
|
||||
type: std::string[6]
|
||||
restore_value: no
|
||||
- id: g_gl_unit
|
||||
type: std::string[6]
|
||||
restore_value: no
|
||||
- id: g_gl_name
|
||||
type: std::string[6]
|
||||
restore_value: no
|
||||
- id: g_gl_glyph
|
||||
type: std::string[6]
|
||||
restore_value: no
|
||||
- id: g_gl_present
|
||||
type: bool[6]
|
||||
restore_value: no
|
||||
- id: g_gl_sel
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
|
||||
script:
|
||||
- id: gl_render
|
||||
then:
|
||||
- lambda: |-
|
||||
int first = -1;
|
||||
for (int i = 0; i < 6; i++) if (id(g_gl_present)[i]) { first = i; break; }
|
||||
if (first >= 0 && !id(g_gl_present)[id(g_gl_sel)]) id(g_gl_sel) = first;
|
||||
- if:
|
||||
condition: { lambda: 'int c=0; for(int i=0;i<6;i++) if(id(g_gl_present)[i]) c++; return c==0;' }
|
||||
then:
|
||||
- lvgl.widget.show: gl_empty
|
||||
- lvgl.widget.hide: gl_icon
|
||||
- lvgl.widget.hide: gl_value
|
||||
- lvgl.widget.hide: gl_name
|
||||
- lvgl.widget.hide: gl_pos
|
||||
else:
|
||||
- lvgl.widget.hide: gl_empty
|
||||
- lvgl.widget.show: gl_value
|
||||
- lvgl.widget.show: gl_name
|
||||
- lvgl.label.update: { id: gl_icon, text: !lambda 'return id(g_gl_glyph)[id(g_gl_sel)];' }
|
||||
- lvgl.widget.update: { id: gl_icon, hidden: !lambda 'return id(g_gl_glyph)[id(g_gl_sel)].empty();' }
|
||||
- lvgl.label.update:
|
||||
id: gl_value
|
||||
text: !lambda |-
|
||||
std::string v = id(g_gl_val)[id(g_gl_sel)];
|
||||
if (v.empty() || v == "unknown" || v == "unavailable") return std::string("--");
|
||||
std::string u = id(g_gl_unit)[id(g_gl_sel)];
|
||||
if (!u.empty()) v += " " + u;
|
||||
// replace symbols the value font lacks (micro sign, superscripts) so units don't tofu
|
||||
auto rep = [&](const std::string &a, const std::string &b) {
|
||||
size_t p; while ((p = v.find(a)) != std::string::npos) v.replace(p, a.size(), b);
|
||||
};
|
||||
rep("\xC2\xB5", "u"); // µ micro sign (U+00B5) -> u
|
||||
rep("\xCE\xBC", "u"); // μ Greek mu (U+03BC, what HA often uses) -> u
|
||||
rep("\xC2\xB2", "2"); // ² -> 2
|
||||
rep("\xC2\xB3", "3"); // ³ -> 3
|
||||
rep("\xC2\xB9", "1"); // ¹ -> 1
|
||||
return v;
|
||||
- lvgl.label.update: { id: gl_name, text: !lambda 'return id(g_gl_name)[id(g_gl_sel)];' }
|
||||
- lvgl.widget.update: { id: gl_pos, hidden: !lambda 'int c=0; for(int i=0;i<6;i++) if(id(g_gl_present)[i]) c++; return c<=1;' }
|
||||
- lvgl.label.update:
|
||||
id: gl_pos
|
||||
text: !lambda |-
|
||||
std::string d; // slider dots: active = bigger (circle-medium), rest = circle-small
|
||||
for (int i=0;i<6;i++) {
|
||||
if (!id(g_gl_present)[i]) continue;
|
||||
d += (i == id(g_gl_sel)) ? "\U000f09de" : "\U000f09df";
|
||||
}
|
||||
return d;
|
||||
# per-sensor accent (by slot): tints the icon + dots so each glance has its identity
|
||||
- if: { condition: { lambda: 'return id(g_gl_sel)==0;' }, then: [ lvgl.label.update: { id: gl_icon, text_color: 0x35C759 }, lvgl.label.update: { id: gl_pos, text_color: 0x35C759 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_gl_sel)==1;' }, then: [ lvgl.label.update: { id: gl_icon, text_color: 0x3FA9D6 }, lvgl.label.update: { id: gl_pos, text_color: 0x3FA9D6 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_gl_sel)==2;' }, then: [ lvgl.label.update: { id: gl_icon, text_color: 0xF5B83D }, lvgl.label.update: { id: gl_pos, text_color: 0xF5B83D } ] }
|
||||
- if: { condition: { lambda: 'return id(g_gl_sel)==3;' }, then: [ lvgl.label.update: { id: gl_icon, text_color: 0xA66BFF }, lvgl.label.update: { id: gl_pos, text_color: 0xA66BFF } ] }
|
||||
- if: { condition: { lambda: 'return id(g_gl_sel)==4;' }, then: [ lvgl.label.update: { id: gl_icon, text_color: 0xFF6B60 }, lvgl.label.update: { id: gl_pos, text_color: 0xFF6B60 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_gl_sel)==5;' }, then: [ lvgl.label.update: { id: gl_icon, text_color: 0x2FE6CF }, lvgl.label.update: { id: gl_pos, text_color: 0x2FE6CF } ] }
|
||||
|
||||
text_sensor:
|
||||
- platform: homeassistant
|
||||
id: gl1_v
|
||||
entity_id: ${glance1_entity}
|
||||
on_value: [ lambda: 'id(g_gl_val)[0]=x; id(g_gl_present)[0]=true;', script.execute: gl_render ]
|
||||
- platform: homeassistant
|
||||
id: gl1_u
|
||||
entity_id: ${glance1_entity}
|
||||
attribute: unit_of_measurement
|
||||
on_value: [ lambda: 'id(g_gl_unit)[0]=x;', script.execute: gl_render ]
|
||||
- platform: homeassistant
|
||||
id: gl2_v
|
||||
entity_id: ${glance2_entity}
|
||||
on_value: [ lambda: 'id(g_gl_val)[1]=x; id(g_gl_present)[1]=true;', script.execute: gl_render ]
|
||||
- platform: homeassistant
|
||||
id: gl2_u
|
||||
entity_id: ${glance2_entity}
|
||||
attribute: unit_of_measurement
|
||||
on_value: [ lambda: 'id(g_gl_unit)[1]=x;', script.execute: gl_render ]
|
||||
- platform: homeassistant
|
||||
id: gl3_v
|
||||
entity_id: ${glance3_entity}
|
||||
on_value: [ lambda: 'id(g_gl_val)[2]=x; id(g_gl_present)[2]=true;', script.execute: gl_render ]
|
||||
- platform: homeassistant
|
||||
id: gl3_u
|
||||
entity_id: ${glance3_entity}
|
||||
attribute: unit_of_measurement
|
||||
on_value: [ lambda: 'id(g_gl_unit)[2]=x;', script.execute: gl_render ]
|
||||
- platform: homeassistant
|
||||
id: gl4_v
|
||||
entity_id: ${glance4_entity}
|
||||
on_value: [ lambda: 'id(g_gl_val)[3]=x; id(g_gl_present)[3]=true;', script.execute: gl_render ]
|
||||
- platform: homeassistant
|
||||
id: gl4_u
|
||||
entity_id: ${glance4_entity}
|
||||
attribute: unit_of_measurement
|
||||
on_value: [ lambda: 'id(g_gl_unit)[3]=x;', script.execute: gl_render ]
|
||||
- platform: homeassistant
|
||||
id: gl5_v
|
||||
entity_id: ${glance5_entity}
|
||||
on_value: [ lambda: 'id(g_gl_val)[4]=x; id(g_gl_present)[4]=true;', script.execute: gl_render ]
|
||||
- platform: homeassistant
|
||||
id: gl5_u
|
||||
entity_id: ${glance5_entity}
|
||||
attribute: unit_of_measurement
|
||||
on_value: [ lambda: 'id(g_gl_unit)[4]=x;', script.execute: gl_render ]
|
||||
- platform: homeassistant
|
||||
id: gl6_v
|
||||
entity_id: ${glance6_entity}
|
||||
on_value: [ lambda: 'id(g_gl_val)[5]=x; id(g_gl_present)[5]=true;', script.execute: gl_render ]
|
||||
- platform: homeassistant
|
||||
id: gl6_u
|
||||
entity_id: ${glance6_entity}
|
||||
attribute: unit_of_measurement
|
||||
on_value: [ lambda: 'id(g_gl_unit)[5]=x;', script.execute: gl_render ]
|
||||
|
||||
interval:
|
||||
# Carousel nav: when selected, show the page and take the knob (only if >1 sensor).
|
||||
- interval: 50ms
|
||||
then:
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_nav_req) && id(g_base)==8;' }
|
||||
then:
|
||||
- lambda: 'id(g_nav_req) = false;'
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==0;' }, then: [lvgl.page.show: { id: page_sensors, animation: move_left, time: 250ms }] }
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==1;' }, then: [lvgl.page.show: { id: page_sensors, animation: move_right, time: 250ms }] }
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==2;' }, then: [lvgl.page.show: { id: page_sensors, animation: move_top, time: 250ms }] }
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==3;' }, then: [lvgl.page.show: { id: page_sensors, animation: move_bottom, time: 250ms }] }
|
||||
- script.execute: gl_render
|
||||
# Knob cycles between present sensors (capture only when there's more than one).
|
||||
- interval: 120ms
|
||||
then:
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_base)==8;' }
|
||||
then:
|
||||
- lambda: |-
|
||||
int c=0; for (int i=0;i<6;i++) if (id(g_gl_present)[i]) c++;
|
||||
id(g_knob_capture) = (c > 1);
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_knob_delta) != 0;' }
|
||||
then:
|
||||
- lambda: |-
|
||||
int d = id(g_knob_delta); id(g_knob_delta) = 0;
|
||||
int cnt=0; for (int i=0;i<6;i++) if (id(g_gl_present)[i]) cnt++;
|
||||
if (cnt > 0) {
|
||||
int step = (d > 0) ? 1 : -1; int n = (d > 0) ? d : -d;
|
||||
for (int k=0;k<n;k++) { do { id(g_gl_sel) = (id(g_gl_sel)+step+6)%6; } while (!id(g_gl_present)[id(g_gl_sel)]); }
|
||||
}
|
||||
- script.execute: gl_render
|
||||
|
||||
lvgl:
|
||||
pages:
|
||||
- id: page_sensors
|
||||
bg_color: 0x000000
|
||||
widgets:
|
||||
- obj: # faint outer ring for depth (matches the other screens)
|
||||
id: gl_ring_out
|
||||
align: CENTER
|
||||
width: 340
|
||||
height: 340
|
||||
radius: 170
|
||||
bg_opa: TRANSP
|
||||
border_width: 2
|
||||
border_color: 0x1A1F2A
|
||||
border_opa: 70%
|
||||
clickable: false
|
||||
scrollable: false
|
||||
- label: { id: gl_icon, align: CENTER, y: -70, text: "", text_font: icons34, text_color: 0xBFD2DA }
|
||||
- label: { id: gl_value, align: CENTER, y: -6, width: 320, text_align: CENTER, long_mode: clip, text: "--", text_font: montserrat_34, text_color: 0xFFFFFF }
|
||||
- label: { id: gl_name, align: CENTER, y: 40, width: 300, text_align: CENTER, long_mode: clip, text: "", text_font: font_med, text_color: 0x9FC7BE }
|
||||
- label: { id: gl_pos, align: CENTER, y: 88, text: "", text_font: icons18, text_color: 0x6F9A92, hidden: true }
|
||||
- label: { id: gl_empty, align: CENTER, text: "Set glance sensors", text_font: font_med, text_color: 0x466761, hidden: true }
|
||||
@@ -0,0 +1,704 @@
|
||||
# Snake - optional carousel screen (base id 9). Smooth-steering 360-degree snake steered by the knob.
|
||||
# Remove it: drop this file from your config's package "files:" list. Nothing else to edit.
|
||||
# Scores (g_sn_top / g_sn_best) + the "Reset scores" settings action live here - fully self-contained.
|
||||
#
|
||||
# GENERATED by scripts/gen_snake.py - edit the generator, not the blocks. Body = a trail of dots
|
||||
# (ring buffer) rendered from a lambda (lv_obj_set_pos), so per-tick cost is constant + shallow.
|
||||
|
||||
esphome:
|
||||
on_boot:
|
||||
- priority: 655
|
||||
then:
|
||||
- lambda: 'id(g_present)[9] = true;'
|
||||
- priority: 650
|
||||
then:
|
||||
- lambda: |-
|
||||
int g = id(g_wgrp_n)++;
|
||||
id(g_wgrp_labels) += (g ? "\n" : ""); id(g_wgrp_labels) += "Snake";
|
||||
int o = id(g_wopt_n)++;
|
||||
id(g_wopt_group)[o] = g; id(g_wopt_label)[o] = "Reset scores"; id(g_wopt_kind)[o] = 1; id(g_wopt_ptr)[o] = &id(g_sn_reset);
|
||||
|
||||
globals:
|
||||
- id: g_sn_best
|
||||
type: int
|
||||
restore_value: yes
|
||||
initial_value: '0'
|
||||
- id: g_sn_top
|
||||
type: int[10]
|
||||
restore_value: yes
|
||||
- id: g_sn_reset
|
||||
type: bool
|
||||
restore_value: no
|
||||
initial_value: 'false'
|
||||
- id: g_sn_state # 0=menu 1=playing 2=game over 3=how-to 4=scores
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
- id: g_sn_len # body dots in the snake
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '5'
|
||||
- id: g_sn_score
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
- id: g_sn_tx # body trail dots X (ring buffer; centre coords px)
|
||||
type: float[100]
|
||||
restore_value: no
|
||||
- id: g_sn_ty
|
||||
type: float[100]
|
||||
restore_value: no
|
||||
- id: g_sn_head # ring index of the newest dot
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
- id: g_sn_hx # smooth head position (leads the newest dot)
|
||||
type: float
|
||||
restore_value: no
|
||||
initial_value: '0.0'
|
||||
- id: g_sn_hy
|
||||
type: float
|
||||
restore_value: no
|
||||
initial_value: '0.0'
|
||||
- id: g_sn_ang # heading (radians)
|
||||
type: float
|
||||
restore_value: no
|
||||
initial_value: '0.0'
|
||||
- id: g_sn_grow # pending growth (appends that won't drop the tail)
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
- id: g_sn_dead
|
||||
type: bool
|
||||
restore_value: no
|
||||
initial_value: 'false'
|
||||
- id: g_sn_add # ring slot to show/position this tick (-1 none)
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '-1'
|
||||
- id: g_sn_del # ring slot to park off-screen this tick (-1 none)
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '-1'
|
||||
- id: g_sn_fx
|
||||
type: float[3]
|
||||
restore_value: no
|
||||
- id: g_sn_fy
|
||||
type: float[3]
|
||||
restore_value: no
|
||||
- id: g_sn_fi
|
||||
type: int[3]
|
||||
restore_value: no
|
||||
- id: g_sn_fon
|
||||
type: bool[3]
|
||||
restore_value: no
|
||||
- id: g_sn_fclk
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
- id: g_sn_newfruit
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '-1'
|
||||
- id: g_sn_skx
|
||||
type: float[2]
|
||||
restore_value: no
|
||||
- id: g_sn_sky
|
||||
type: float[2]
|
||||
restore_value: no
|
||||
- id: g_sn_skon
|
||||
type: bool[2]
|
||||
restore_value: no
|
||||
- id: g_sn_skage
|
||||
type: int[2]
|
||||
restore_value: no
|
||||
- id: g_sn_skclk
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
- id: g_sn_newskull
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '-1'
|
||||
|
||||
script:
|
||||
- id: sn_fspr0
|
||||
then:
|
||||
- lvgl.widget.update: { id: sn_fruit0, hidden: false, x: !lambda 'return (int) lroundf(id(g_sn_fx)[0]);', y: !lambda 'return (int) lroundf(id(g_sn_fy)[0]);' }
|
||||
- if: { condition: { lambda: 'return id(g_sn_fi)[0] == 0;' }, then: [ lvgl.image.update: { id: sn_fruit0, src: img_sn_fruit0 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_sn_fi)[0] == 1;' }, then: [ lvgl.image.update: { id: sn_fruit0, src: img_sn_fruit1 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_sn_fi)[0] == 2;' }, then: [ lvgl.image.update: { id: sn_fruit0, src: img_sn_fruit2 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_sn_fi)[0] == 3;' }, then: [ lvgl.image.update: { id: sn_fruit0, src: img_sn_fruit3 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_sn_fi)[0] == 4;' }, then: [ lvgl.image.update: { id: sn_fruit0, src: img_sn_fruit4 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_sn_fi)[0] == 5;' }, then: [ lvgl.image.update: { id: sn_fruit0, src: img_sn_fruit5 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_sn_fi)[0] == 6;' }, then: [ lvgl.image.update: { id: sn_fruit0, src: img_sn_fruit6 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_sn_fi)[0] == 7;' }, then: [ lvgl.image.update: { id: sn_fruit0, src: img_sn_fruit7 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_sn_fi)[0] == 8;' }, then: [ lvgl.image.update: { id: sn_fruit0, src: img_sn_fruit8 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_sn_fi)[0] == 9;' }, then: [ lvgl.image.update: { id: sn_fruit0, src: img_sn_fruit9 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_sn_fi)[0] == 10;' }, then: [ lvgl.image.update: { id: sn_fruit0, src: img_sn_fruit10 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_sn_fi)[0] == 11;' }, then: [ lvgl.image.update: { id: sn_fruit0, src: img_sn_fruit11 } ] }
|
||||
- id: sn_fspr1
|
||||
then:
|
||||
- lvgl.widget.update: { id: sn_fruit1, hidden: false, x: !lambda 'return (int) lroundf(id(g_sn_fx)[1]);', y: !lambda 'return (int) lroundf(id(g_sn_fy)[1]);' }
|
||||
- if: { condition: { lambda: 'return id(g_sn_fi)[1] == 0;' }, then: [ lvgl.image.update: { id: sn_fruit1, src: img_sn_fruit0 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_sn_fi)[1] == 1;' }, then: [ lvgl.image.update: { id: sn_fruit1, src: img_sn_fruit1 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_sn_fi)[1] == 2;' }, then: [ lvgl.image.update: { id: sn_fruit1, src: img_sn_fruit2 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_sn_fi)[1] == 3;' }, then: [ lvgl.image.update: { id: sn_fruit1, src: img_sn_fruit3 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_sn_fi)[1] == 4;' }, then: [ lvgl.image.update: { id: sn_fruit1, src: img_sn_fruit4 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_sn_fi)[1] == 5;' }, then: [ lvgl.image.update: { id: sn_fruit1, src: img_sn_fruit5 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_sn_fi)[1] == 6;' }, then: [ lvgl.image.update: { id: sn_fruit1, src: img_sn_fruit6 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_sn_fi)[1] == 7;' }, then: [ lvgl.image.update: { id: sn_fruit1, src: img_sn_fruit7 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_sn_fi)[1] == 8;' }, then: [ lvgl.image.update: { id: sn_fruit1, src: img_sn_fruit8 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_sn_fi)[1] == 9;' }, then: [ lvgl.image.update: { id: sn_fruit1, src: img_sn_fruit9 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_sn_fi)[1] == 10;' }, then: [ lvgl.image.update: { id: sn_fruit1, src: img_sn_fruit10 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_sn_fi)[1] == 11;' }, then: [ lvgl.image.update: { id: sn_fruit1, src: img_sn_fruit11 } ] }
|
||||
- id: sn_fspr2
|
||||
then:
|
||||
- lvgl.widget.update: { id: sn_fruit2, hidden: false, x: !lambda 'return (int) lroundf(id(g_sn_fx)[2]);', y: !lambda 'return (int) lroundf(id(g_sn_fy)[2]);' }
|
||||
- if: { condition: { lambda: 'return id(g_sn_fi)[2] == 0;' }, then: [ lvgl.image.update: { id: sn_fruit2, src: img_sn_fruit0 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_sn_fi)[2] == 1;' }, then: [ lvgl.image.update: { id: sn_fruit2, src: img_sn_fruit1 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_sn_fi)[2] == 2;' }, then: [ lvgl.image.update: { id: sn_fruit2, src: img_sn_fruit2 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_sn_fi)[2] == 3;' }, then: [ lvgl.image.update: { id: sn_fruit2, src: img_sn_fruit3 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_sn_fi)[2] == 4;' }, then: [ lvgl.image.update: { id: sn_fruit2, src: img_sn_fruit4 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_sn_fi)[2] == 5;' }, then: [ lvgl.image.update: { id: sn_fruit2, src: img_sn_fruit5 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_sn_fi)[2] == 6;' }, then: [ lvgl.image.update: { id: sn_fruit2, src: img_sn_fruit6 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_sn_fi)[2] == 7;' }, then: [ lvgl.image.update: { id: sn_fruit2, src: img_sn_fruit7 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_sn_fi)[2] == 8;' }, then: [ lvgl.image.update: { id: sn_fruit2, src: img_sn_fruit8 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_sn_fi)[2] == 9;' }, then: [ lvgl.image.update: { id: sn_fruit2, src: img_sn_fruit9 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_sn_fi)[2] == 10;' }, then: [ lvgl.image.update: { id: sn_fruit2, src: img_sn_fruit10 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_sn_fi)[2] == 11;' }, then: [ lvgl.image.update: { id: sn_fruit2, src: img_sn_fruit11 } ] }
|
||||
- id: sn_hud
|
||||
then:
|
||||
- lvgl.label.update: { id: lbl_sn_score, text: !lambda 'char b[12]; snprintf(b,sizeof(b),"%d",id(g_sn_score)); return std::string(b);' }
|
||||
- id: sn_items
|
||||
then:
|
||||
- lvgl.widget.update: { id: sn_fruit0, hidden: !lambda 'return !id(g_sn_fon)[0];' }
|
||||
- lvgl.widget.update: { id: sn_fruit1, hidden: !lambda 'return !id(g_sn_fon)[1];' }
|
||||
- lvgl.widget.update: { id: sn_fruit2, hidden: !lambda 'return !id(g_sn_fon)[2];' }
|
||||
- lvgl.widget.update: { id: sn_skull0, hidden: !lambda 'return !id(g_sn_skon)[0];' }
|
||||
- lvgl.widget.update: { id: sn_skull1, hidden: !lambda 'return !id(g_sn_skon)[1];' }
|
||||
# full body redraw - used at start / when (re)entering the play screen (one lambda, not per tick)
|
||||
- id: sn_drawfull
|
||||
then:
|
||||
- lambda: |-
|
||||
lv_obj_t *SS[100] = { id(sn_seg0), id(sn_seg1), id(sn_seg2), id(sn_seg3), id(sn_seg4), id(sn_seg5), id(sn_seg6), id(sn_seg7), id(sn_seg8), id(sn_seg9), id(sn_seg10), id(sn_seg11), id(sn_seg12), id(sn_seg13), id(sn_seg14), id(sn_seg15), id(sn_seg16), id(sn_seg17), id(sn_seg18), id(sn_seg19), id(sn_seg20), id(sn_seg21), id(sn_seg22), id(sn_seg23), id(sn_seg24), id(sn_seg25), id(sn_seg26), id(sn_seg27), id(sn_seg28), id(sn_seg29), id(sn_seg30), id(sn_seg31), id(sn_seg32), id(sn_seg33), id(sn_seg34), id(sn_seg35), id(sn_seg36), id(sn_seg37), id(sn_seg38), id(sn_seg39), id(sn_seg40), id(sn_seg41), id(sn_seg42), id(sn_seg43), id(sn_seg44), id(sn_seg45), id(sn_seg46), id(sn_seg47), id(sn_seg48), id(sn_seg49), id(sn_seg50), id(sn_seg51), id(sn_seg52), id(sn_seg53), id(sn_seg54), id(sn_seg55), id(sn_seg56), id(sn_seg57), id(sn_seg58), id(sn_seg59), id(sn_seg60), id(sn_seg61), id(sn_seg62), id(sn_seg63), id(sn_seg64), id(sn_seg65), id(sn_seg66), id(sn_seg67), id(sn_seg68), id(sn_seg69), id(sn_seg70), id(sn_seg71), id(sn_seg72), id(sn_seg73), id(sn_seg74), id(sn_seg75), id(sn_seg76), id(sn_seg77), id(sn_seg78), id(sn_seg79), id(sn_seg80), id(sn_seg81), id(sn_seg82), id(sn_seg83), id(sn_seg84), id(sn_seg85), id(sn_seg86), id(sn_seg87), id(sn_seg88), id(sn_seg89), id(sn_seg90), id(sn_seg91), id(sn_seg92), id(sn_seg93), id(sn_seg94), id(sn_seg95), id(sn_seg96), id(sn_seg97), id(sn_seg98), id(sn_seg99) };
|
||||
for (int k = 0; k < 100; k++) {
|
||||
int d = (id(g_sn_head) - k + 400) % 100;
|
||||
if (d < id(g_sn_len)) lv_obj_set_pos(SS[k], 180 + (int) lroundf(id(g_sn_tx)[k]) - 4, 180 + (int) lroundf(id(g_sn_ty)[k]) - 4);
|
||||
else lv_obj_set_pos(SS[k], 400, 400);
|
||||
}
|
||||
lv_obj_set_pos(id(sn_head), 180 + (int) lroundf(id(g_sn_hx)) - 4, 180 + (int) lroundf(id(g_sn_hy)) - 4);
|
||||
- script.execute: sn_items
|
||||
# hide every gameplay widget (park body off-screen + hide fruit/skulls)
|
||||
- id: sn_clear
|
||||
then:
|
||||
- lambda: |-
|
||||
lv_obj_t *SS[100] = { id(sn_seg0), id(sn_seg1), id(sn_seg2), id(sn_seg3), id(sn_seg4), id(sn_seg5), id(sn_seg6), id(sn_seg7), id(sn_seg8), id(sn_seg9), id(sn_seg10), id(sn_seg11), id(sn_seg12), id(sn_seg13), id(sn_seg14), id(sn_seg15), id(sn_seg16), id(sn_seg17), id(sn_seg18), id(sn_seg19), id(sn_seg20), id(sn_seg21), id(sn_seg22), id(sn_seg23), id(sn_seg24), id(sn_seg25), id(sn_seg26), id(sn_seg27), id(sn_seg28), id(sn_seg29), id(sn_seg30), id(sn_seg31), id(sn_seg32), id(sn_seg33), id(sn_seg34), id(sn_seg35), id(sn_seg36), id(sn_seg37), id(sn_seg38), id(sn_seg39), id(sn_seg40), id(sn_seg41), id(sn_seg42), id(sn_seg43), id(sn_seg44), id(sn_seg45), id(sn_seg46), id(sn_seg47), id(sn_seg48), id(sn_seg49), id(sn_seg50), id(sn_seg51), id(sn_seg52), id(sn_seg53), id(sn_seg54), id(sn_seg55), id(sn_seg56), id(sn_seg57), id(sn_seg58), id(sn_seg59), id(sn_seg60), id(sn_seg61), id(sn_seg62), id(sn_seg63), id(sn_seg64), id(sn_seg65), id(sn_seg66), id(sn_seg67), id(sn_seg68), id(sn_seg69), id(sn_seg70), id(sn_seg71), id(sn_seg72), id(sn_seg73), id(sn_seg74), id(sn_seg75), id(sn_seg76), id(sn_seg77), id(sn_seg78), id(sn_seg79), id(sn_seg80), id(sn_seg81), id(sn_seg82), id(sn_seg83), id(sn_seg84), id(sn_seg85), id(sn_seg86), id(sn_seg87), id(sn_seg88), id(sn_seg89), id(sn_seg90), id(sn_seg91), id(sn_seg92), id(sn_seg93), id(sn_seg94), id(sn_seg95), id(sn_seg96), id(sn_seg97), id(sn_seg98), id(sn_seg99) };
|
||||
for (int k = 0; k < 100; k++) lv_obj_set_pos(SS[k], 400, 400);
|
||||
lv_obj_set_pos(id(sn_head), 400, 400);
|
||||
- lvgl.widget.hide: sn_fruit0
|
||||
- lvgl.widget.hide: sn_fruit1
|
||||
- lvgl.widget.hide: sn_fruit2
|
||||
- lvgl.widget.hide: sn_skull0
|
||||
- lvgl.widget.hide: sn_skull1
|
||||
- id: sn_start
|
||||
then:
|
||||
- lambda: |-
|
||||
id(g_sn_state) = 1; id(g_sn_score) = 0; id(g_sn_dead) = false;
|
||||
id(g_sn_ang) = -1.5708f; id(g_sn_grow) = 0; // heading up
|
||||
id(g_sn_len) = 5; id(g_sn_head) = 4;
|
||||
for (int i = 0; i < 100; i++) { id(g_sn_tx)[i] = 0.0f; id(g_sn_ty)[i] = 0.0f; }
|
||||
for (int k = 0; k < 5; k++) { id(g_sn_tx)[k] = 0.0f; id(g_sn_ty)[k] = (4 - k) * 8.0f; }
|
||||
id(g_sn_hx) = 0.0f; id(g_sn_hy) = 0.0f;
|
||||
for (int s = 0; s < 3; s++) id(g_sn_fon)[s] = false;
|
||||
for (int s = 0; s < 2; s++) { id(g_sn_skon)[s] = false; id(g_sn_skage)[s] = 0; }
|
||||
id(g_sn_fclk) = 0; id(g_sn_skclk) = 0;
|
||||
id(g_sn_newfruit) = -1; id(g_sn_newskull) = -1; id(g_sn_add) = -1; id(g_sn_del) = -1;
|
||||
id(g_knob_capture) = true; id(g_knob_delta) = 0;
|
||||
float a = (esp_random() % 62832) / 10000.0f; float rad = 60.0f + (esp_random() % 80);
|
||||
id(g_sn_fx)[0] = cosf(a)*rad; id(g_sn_fy)[0] = sinf(a)*rad;
|
||||
id(g_sn_fi)[0] = esp_random() % 12; id(g_sn_fon)[0] = true;
|
||||
- script.execute: sn_fspr0
|
||||
- script.execute: sn_screen
|
||||
- script.execute: sn_drawfull
|
||||
- script.execute: sn_hud
|
||||
- id: sn_over
|
||||
then:
|
||||
- lambda: |-
|
||||
id(g_sn_state) = 2;
|
||||
int s = id(g_sn_score);
|
||||
if (s > id(g_sn_best)) id(g_sn_best) = s;
|
||||
for (int i = 0; i < 10; i++) {
|
||||
if (s > id(g_sn_top)[i]) {
|
||||
for (int j = 9; j > i; j--) id(g_sn_top)[j] = id(g_sn_top)[j-1];
|
||||
id(g_sn_top)[i] = s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
- script.execute: sn_screen
|
||||
# screen state + the single knob-capture authority (captured only in play + game over)
|
||||
- id: sn_screen
|
||||
then:
|
||||
- lambda: 'id(g_knob_capture) = (id(g_sn_state) == 1 || id(g_sn_state) == 2);'
|
||||
- lvgl.widget.hide: sn_ring
|
||||
- script.execute: sn_clear
|
||||
- lvgl.widget.hide: lbl_sn_score
|
||||
- lvgl.widget.hide: sn_logo
|
||||
- lvgl.widget.hide: btn_sn_start
|
||||
- lvgl.widget.hide: btn_sn_howto
|
||||
- lvgl.widget.hide: btn_sn_scores
|
||||
- lvgl.widget.hide: lbl_sn_over
|
||||
- lvgl.widget.hide: lbl_sn_rec
|
||||
- lvgl.widget.hide: btn_sn_replay
|
||||
- lvgl.widget.hide: btn_sn_exit
|
||||
- lvgl.widget.hide: lbl_sn_howto_t
|
||||
- lvgl.widget.hide: sn_b1
|
||||
- lvgl.widget.hide: sn_b2
|
||||
- lvgl.widget.hide: sn_b3
|
||||
- lvgl.widget.hide: sn_b4
|
||||
- lvgl.widget.hide: lbl_sn_howto1
|
||||
- lvgl.widget.hide: lbl_sn_howto2
|
||||
- lvgl.widget.hide: lbl_sn_howto3
|
||||
- lvgl.widget.hide: lbl_sn_howto4
|
||||
- lvgl.widget.hide: lbl_sn_scores_t
|
||||
- lvgl.widget.hide: lbl_sn_scores1
|
||||
- lvgl.widget.hide: lbl_sn_scores2
|
||||
- lvgl.widget.hide: btn_sn_back
|
||||
- if: # MENU
|
||||
condition: { lambda: 'return id(g_sn_state) == 0;' }
|
||||
then:
|
||||
- lvgl.widget.show: sn_logo
|
||||
- lvgl.widget.show: btn_sn_start
|
||||
- lvgl.widget.show: btn_sn_howto
|
||||
- lvgl.widget.show: btn_sn_scores
|
||||
- if: # PLAY
|
||||
condition: { lambda: 'return id(g_sn_state) == 1;' }
|
||||
then:
|
||||
- lvgl.widget.show: sn_ring
|
||||
- lvgl.widget.show: lbl_sn_score
|
||||
- script.execute: sn_drawfull
|
||||
- if: # GAME OVER
|
||||
condition: { lambda: 'return id(g_sn_state) == 2;' }
|
||||
then:
|
||||
- lvgl.widget.show: sn_logo
|
||||
- lvgl.widget.show: lbl_sn_over
|
||||
- lvgl.widget.show: lbl_sn_rec
|
||||
- lvgl.widget.show: btn_sn_replay
|
||||
- lvgl.widget.show: btn_sn_exit
|
||||
- lvgl.label.update: { id: lbl_sn_over, text: !lambda 'char b[24]; snprintf(b,sizeof(b),"Score: %d", id(g_sn_score)); return std::string(b);' }
|
||||
- lvgl.label.update: { id: lbl_sn_rec, text: !lambda 'char b[24]; snprintf(b,sizeof(b),"Best: %d", id(g_sn_best)); return std::string(b);' }
|
||||
- if: # HOW TO PLAY
|
||||
condition: { lambda: 'return id(g_sn_state) == 3;' }
|
||||
then:
|
||||
- lvgl.widget.show: lbl_sn_howto_t
|
||||
- lvgl.widget.show: sn_b1
|
||||
- lvgl.widget.show: sn_b2
|
||||
- lvgl.widget.show: sn_b3
|
||||
- lvgl.widget.show: sn_b4
|
||||
- lvgl.widget.show: lbl_sn_howto1
|
||||
- lvgl.widget.show: lbl_sn_howto2
|
||||
- lvgl.widget.show: lbl_sn_howto3
|
||||
- lvgl.widget.show: lbl_sn_howto4
|
||||
- lvgl.widget.show: btn_sn_back
|
||||
- if: # SCORES (top 10)
|
||||
condition: { lambda: 'return id(g_sn_state) == 4;' }
|
||||
then:
|
||||
- lvgl.widget.show: lbl_sn_scores_t
|
||||
- lvgl.widget.show: lbl_sn_scores1
|
||||
- lvgl.widget.show: lbl_sn_scores2
|
||||
- lvgl.widget.show: btn_sn_back
|
||||
- lvgl.label.update:
|
||||
id: lbl_sn_scores1
|
||||
text: !lambda |-
|
||||
std::string s; char b[24];
|
||||
for (int i = 0; i < 5; i++) { if(i) s += "\n"; if(id(g_sn_top)[i]>0){ snprintf(b,sizeof(b),"%d. %d",i+1,id(g_sn_top)[i]); } else { snprintf(b,sizeof(b),"%d. -",i+1); } s += b; }
|
||||
return s;
|
||||
- lvgl.label.update:
|
||||
id: lbl_sn_scores2
|
||||
text: !lambda |-
|
||||
std::string s; char b[24];
|
||||
for (int i = 5; i < 10; i++) { if(i>5) s += "\n"; if(id(g_sn_top)[i]>0){ snprintf(b,sizeof(b),"%d. %d",i+1,id(g_sn_top)[i]); } else { snprintf(b,sizeof(b),"%d. -",i+1); } s += b; }
|
||||
return s;
|
||||
|
||||
interval:
|
||||
- interval: 250ms
|
||||
then:
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_sn_reset) || id(g_factory_reset);' }
|
||||
then:
|
||||
- lambda: 'for (int i = 0; i < 10; i++) id(g_sn_top)[i] = 0; id(g_sn_best) = 0; id(g_sn_reset) = false;'
|
||||
- interval: 50ms
|
||||
then:
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_nav_req) && id(g_base)==9;' }
|
||||
then:
|
||||
- lambda: 'id(g_nav_req) = false; id(g_sn_state) = 0; id(g_knob_delta) = 0;'
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==0;' }, then: [lvgl.page.show: { id: page_snake, animation: move_left, time: 250ms }] }
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==1;' }, then: [lvgl.page.show: { id: page_snake, animation: move_right, time: 250ms }] }
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==2;' }, then: [lvgl.page.show: { id: page_snake, animation: move_top, time: 250ms }] }
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==3;' }, then: [lvgl.page.show: { id: page_snake, animation: move_bottom, time: 250ms }] }
|
||||
- script.execute: sn_screen
|
||||
# Game tick: steer + glide head; lay/drop one trail dot when the head advanced SEG_DIST.
|
||||
# All body/head positioning is done IN this lambda (lv_obj_set_pos) to keep the action chain shallow.
|
||||
- interval: 50ms
|
||||
then:
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_base) == 9 && id(g_sn_state) == 1;' }
|
||||
then:
|
||||
- lambda: |-
|
||||
id(g_sn_add) = -1; id(g_sn_del) = -1; id(g_sn_newfruit) = -1; id(g_sn_newskull) = -1;
|
||||
int kd = id(g_knob_delta); id(g_knob_delta) = 0;
|
||||
id(g_sn_ang) += kd * 0.26f;
|
||||
float spd = 3.2f + id(g_sn_score) / 400.0f; if (spd > 5.5f) spd = 5.5f;
|
||||
float hx = id(g_sn_hx) + cosf(id(g_sn_ang)) * spd;
|
||||
float hy = id(g_sn_hy) + sinf(id(g_sn_ang)) * spd;
|
||||
if (hx*hx + hy*hy > 168.0f * 168.0f) { id(g_sn_dead) = true; } // hit the ring
|
||||
else {
|
||||
id(g_sn_hx) = hx; id(g_sn_hy) = hy;
|
||||
int len = id(g_sn_len), head = id(g_sn_head);
|
||||
for (int i = 6; i < len; i++) {
|
||||
int sl = (head - i + 400) % 100;
|
||||
float dx = hx - id(g_sn_tx)[sl], dy = hy - id(g_sn_ty)[sl];
|
||||
if (dx*dx + dy*dy < 8.0f * 8.0f) { id(g_sn_dead) = true; break; }
|
||||
}
|
||||
for (int s = 0; s < 2; s++) if (id(g_sn_skon)[s]) {
|
||||
float dx = hx - id(g_sn_skx)[s], dy = hy - id(g_sn_sky)[s];
|
||||
if (dx*dx + dy*dy < 13.0f * 13.0f) { id(g_sn_dead) = true; }
|
||||
}
|
||||
for (int s = 0; s < 3 && !id(g_sn_dead); s++) if (id(g_sn_fon)[s]) {
|
||||
float dx = hx - id(g_sn_fx)[s], dy = hy - id(g_sn_fy)[s];
|
||||
if (dx*dx + dy*dy < 12.0f * 12.0f) { id(g_sn_fon)[s] = false; id(g_sn_score) += 10; id(g_sn_grow) += 3; }
|
||||
}
|
||||
if (!id(g_sn_dead)) {
|
||||
float dx = hx - id(g_sn_tx)[head], dy = hy - id(g_sn_ty)[head];
|
||||
if (dx*dx + dy*dy >= 8.0f * 8.0f) {
|
||||
int nh = (head + 1) % 100;
|
||||
id(g_sn_tx)[nh] = hx; id(g_sn_ty)[nh] = hy;
|
||||
id(g_sn_head) = nh; id(g_sn_add) = nh;
|
||||
if (id(g_sn_grow) > 0 && id(g_sn_len) < 99) { id(g_sn_grow)--; id(g_sn_len)++; id(g_sn_del) = -1; } // cap at POOL-1: the ring needs one free slot or add/drop collide
|
||||
else { if (id(g_sn_grow) > 0) id(g_sn_grow)--; id(g_sn_del) = (head - id(g_sn_len) + 1 + 400) % 100; }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!id(g_sn_dead)) {
|
||||
int fc = 0; for (int s = 0; s < 3; s++) if (id(g_sn_fon)[s]) fc++;
|
||||
id(g_sn_fclk)++;
|
||||
int wait = (fc <= 1) ? 45 : 90;
|
||||
if (fc < 3 && id(g_sn_fclk) >= wait) {
|
||||
id(g_sn_fclk) = 0;
|
||||
int sl = -1; for (int s = 0; s < 3; s++) if (!id(g_sn_fon)[s]) { sl = s; break; }
|
||||
if (sl >= 0) {
|
||||
float a = (esp_random() % 62832) / 10000.0f; float rad = 25.0f + (esp_random() % 140);
|
||||
id(g_sn_fx)[sl] = cosf(a)*rad; id(g_sn_fy)[sl] = sinf(a)*rad;
|
||||
id(g_sn_fi)[sl] = esp_random() % 12; id(g_sn_fon)[sl] = true; id(g_sn_newfruit) = sl;
|
||||
}
|
||||
}
|
||||
int sc = 0; for (int s = 0; s < 2; s++) if (id(g_sn_skon)[s]) sc++;
|
||||
id(g_sn_skclk)++;
|
||||
if (sc < 2 && id(g_sn_skclk) >= 200) {
|
||||
id(g_sn_skclk) = 0;
|
||||
int sl = -1; for (int s = 0; s < 2; s++) if (!id(g_sn_skon)[s]) { sl = s; break; }
|
||||
if (sl >= 0) {
|
||||
float a = (esp_random() % 62832) / 10000.0f; float rad = 30.0f + (esp_random() % 130);
|
||||
id(g_sn_skx)[sl] = cosf(a)*rad; id(g_sn_sky)[sl] = sinf(a)*rad;
|
||||
id(g_sn_skon)[sl] = true; id(g_sn_skage)[sl] = 0; id(g_sn_newskull) = sl;
|
||||
}
|
||||
}
|
||||
for (int s = 0; s < 2; s++) if (id(g_sn_skon)[s]) { id(g_sn_skage)[s]++; if (id(g_sn_skage)[s] >= 220) id(g_sn_skon)[s] = false; }
|
||||
// render (constant cost): head every tick + the one added/dropped dot
|
||||
lv_obj_set_pos(id(sn_head), 180 + (int) lroundf(id(g_sn_hx)) - 4, 180 + (int) lroundf(id(g_sn_hy)) - 4);
|
||||
if (id(g_sn_add) >= 0 || id(g_sn_del) >= 0) {
|
||||
lv_obj_t *SS[100] = { id(sn_seg0), id(sn_seg1), id(sn_seg2), id(sn_seg3), id(sn_seg4), id(sn_seg5), id(sn_seg6), id(sn_seg7), id(sn_seg8), id(sn_seg9), id(sn_seg10), id(sn_seg11), id(sn_seg12), id(sn_seg13), id(sn_seg14), id(sn_seg15), id(sn_seg16), id(sn_seg17), id(sn_seg18), id(sn_seg19), id(sn_seg20), id(sn_seg21), id(sn_seg22), id(sn_seg23), id(sn_seg24), id(sn_seg25), id(sn_seg26), id(sn_seg27), id(sn_seg28), id(sn_seg29), id(sn_seg30), id(sn_seg31), id(sn_seg32), id(sn_seg33), id(sn_seg34), id(sn_seg35), id(sn_seg36), id(sn_seg37), id(sn_seg38), id(sn_seg39), id(sn_seg40), id(sn_seg41), id(sn_seg42), id(sn_seg43), id(sn_seg44), id(sn_seg45), id(sn_seg46), id(sn_seg47), id(sn_seg48), id(sn_seg49), id(sn_seg50), id(sn_seg51), id(sn_seg52), id(sn_seg53), id(sn_seg54), id(sn_seg55), id(sn_seg56), id(sn_seg57), id(sn_seg58), id(sn_seg59), id(sn_seg60), id(sn_seg61), id(sn_seg62), id(sn_seg63), id(sn_seg64), id(sn_seg65), id(sn_seg66), id(sn_seg67), id(sn_seg68), id(sn_seg69), id(sn_seg70), id(sn_seg71), id(sn_seg72), id(sn_seg73), id(sn_seg74), id(sn_seg75), id(sn_seg76), id(sn_seg77), id(sn_seg78), id(sn_seg79), id(sn_seg80), id(sn_seg81), id(sn_seg82), id(sn_seg83), id(sn_seg84), id(sn_seg85), id(sn_seg86), id(sn_seg87), id(sn_seg88), id(sn_seg89), id(sn_seg90), id(sn_seg91), id(sn_seg92), id(sn_seg93), id(sn_seg94), id(sn_seg95), id(sn_seg96), id(sn_seg97), id(sn_seg98), id(sn_seg99) };
|
||||
if (id(g_sn_add) >= 0) lv_obj_set_pos(SS[id(g_sn_add)], 180 + (int) lroundf(id(g_sn_tx)[id(g_sn_add)]) - 4, 180 + (int) lroundf(id(g_sn_ty)[id(g_sn_add)]) - 4);
|
||||
if (id(g_sn_del) >= 0) lv_obj_set_pos(SS[id(g_sn_del)], 400, 400);
|
||||
}
|
||||
}
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_sn_dead);' }
|
||||
then:
|
||||
- script.execute: sn_over
|
||||
else:
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_sn_newfruit) >= 0;' }
|
||||
then:
|
||||
- if: { condition: { lambda: 'return id(g_sn_newfruit) == 0;' }, then: [ script.execute: sn_fspr0 ] }
|
||||
- if: { condition: { lambda: 'return id(g_sn_newfruit) == 1;' }, then: [ script.execute: sn_fspr1 ] }
|
||||
- if: { condition: { lambda: 'return id(g_sn_newfruit) == 2;' }, then: [ script.execute: sn_fspr2 ] }
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_sn_newskull) >= 0;' }
|
||||
then:
|
||||
- if: { condition: { lambda: 'return id(g_sn_newskull) == 0;' }, then: [ lvgl.widget.update: { id: sn_skull0, hidden: false, x: !lambda 'return (int) lroundf(id(g_sn_skx)[0]);', y: !lambda 'return (int) lroundf(id(g_sn_sky)[0]);' } ] }
|
||||
- if: { condition: { lambda: 'return id(g_sn_newskull) == 1;' }, then: [ lvgl.widget.update: { id: sn_skull1, hidden: false, x: !lambda 'return (int) lroundf(id(g_sn_skx)[1]);', y: !lambda 'return (int) lroundf(id(g_sn_sky)[1]);' } ] }
|
||||
- script.execute: sn_items
|
||||
- script.execute: sn_hud
|
||||
|
||||
image:
|
||||
- id: img_sn_logo
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/snake/snake-logo.png"
|
||||
type: RGB565
|
||||
- id: img_sn_fruit0
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/snake/fruit0.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_sn_fruit1
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/snake/fruit1.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_sn_fruit2
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/snake/fruit2.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_sn_fruit3
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/snake/fruit3.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_sn_fruit4
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/snake/fruit4.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_sn_fruit5
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/snake/fruit5.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_sn_fruit6
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/snake/fruit6.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_sn_fruit7
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/snake/fruit7.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_sn_fruit8
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/snake/fruit8.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_sn_fruit9
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/snake/fruit9.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_sn_fruit10
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/snake/fruit10.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_sn_fruit11
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/snake/fruit11.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_sn_skull
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/snake/skull.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
|
||||
lvgl:
|
||||
pages:
|
||||
- id: page_snake
|
||||
bg_color: 0x0D1E15
|
||||
scrollbar_mode: "off"
|
||||
scrollable: false
|
||||
widgets:
|
||||
- obj: { id: sn_ring, align: CENTER, x: 0, y: 0, width: 344, height: 344, radius: 172, bg_opa: TRANSP, border_width: 4, border_color: 0x3A8C4E, hidden: true }
|
||||
- image: { id: sn_fruit0, align: CENTER, x: 0, y: 0, src: img_sn_fruit0, hidden: true }
|
||||
- image: { id: sn_fruit1, align: CENTER, x: 0, y: 0, src: img_sn_fruit0, hidden: true }
|
||||
- image: { id: sn_fruit2, align: CENTER, x: 0, y: 0, src: img_sn_fruit0, hidden: true }
|
||||
- image: { id: sn_skull0, align: CENTER, x: 0, y: 0, src: img_sn_skull, hidden: true }
|
||||
- image: { id: sn_skull1, align: CENTER, x: 0, y: 0, src: img_sn_skull, hidden: true }
|
||||
- obj: { id: sn_seg0, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg1, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg2, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg3, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg4, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg5, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg6, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg7, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg8, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg9, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg10, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg11, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg12, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg13, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg14, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg15, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg16, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg17, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg18, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg19, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg20, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg21, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg22, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg23, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg24, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg25, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg26, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg27, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg28, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg29, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg30, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg31, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg32, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg33, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg34, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg35, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg36, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg37, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg38, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg39, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg40, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg41, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg42, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg43, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg44, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg45, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg46, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg47, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg48, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg49, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg50, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg51, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg52, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg53, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg54, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg55, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg56, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg57, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg58, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg59, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg60, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg61, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg62, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg63, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg64, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg65, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg66, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg67, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg68, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg69, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg70, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg71, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg72, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg73, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg74, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg75, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg76, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg77, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg78, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg79, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg80, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg81, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg82, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg83, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg84, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg85, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg86, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg87, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg88, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg89, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg90, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg91, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg92, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg93, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg94, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg95, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg96, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg97, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg98, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_seg99, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
|
||||
- obj: { id: sn_head, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0xB8E86A, bg_opa: COVER }
|
||||
- label: { id: lbl_sn_score, align: CENTER, x: 0, y: -150, text: "0", text_font: font_med, text_color: 0xCFEAE4, hidden: true }
|
||||
- image: { id: sn_logo, align: CENTER, x: 0, y: 0, src: img_sn_logo, hidden: true }
|
||||
- button:
|
||||
id: btn_sn_start
|
||||
align: CENTER
|
||||
y: 4
|
||||
width: 150
|
||||
height: 44
|
||||
radius: 22
|
||||
bg_color: 0xA6D85F
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
on_click: [ script.execute: sn_start ]
|
||||
widgets:
|
||||
- label: { align: CENTER, text: "Start", text_font: font_med, text_color: 0x12241A }
|
||||
- button:
|
||||
id: btn_sn_howto
|
||||
align: CENTER
|
||||
y: 54
|
||||
width: 150
|
||||
height: 44
|
||||
radius: 22
|
||||
bg_color: 0x244A30
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
on_click: [ lambda: 'id(g_sn_state) = 3;', script.execute: sn_screen ]
|
||||
widgets:
|
||||
- label: { align: CENTER, text: "How to play?", text_font: font_med, text_color: 0xCFE6C4 }
|
||||
- button:
|
||||
id: btn_sn_scores
|
||||
align: CENTER
|
||||
y: 104
|
||||
width: 150
|
||||
height: 44
|
||||
radius: 22
|
||||
bg_color: 0x244A30
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
on_click: [ lambda: 'id(g_sn_state) = 4;', script.execute: sn_screen ]
|
||||
widgets:
|
||||
- label: { align: CENTER, text: "Scores", text_font: font_med, text_color: 0xCFE6C4 }
|
||||
- label: { id: lbl_sn_over, align: CENTER, x: 0, y: -10, text_align: CENTER, text: "", text_font: font_sel, text_color: 0x12212D, hidden: true }
|
||||
- label: { id: lbl_sn_rec, align: CENTER, x: 0, y: 22, text_align: CENTER, text: "", text_font: font_artist, text_color: 0x12212D, hidden: true }
|
||||
- button:
|
||||
id: btn_sn_replay
|
||||
align: CENTER
|
||||
x: -66
|
||||
y: 62
|
||||
width: 112
|
||||
height: 48
|
||||
radius: 24
|
||||
bg_color: 0xA6D85F
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
hidden: true
|
||||
on_click: [ script.execute: sn_start ]
|
||||
widgets:
|
||||
- label: { align: CENTER, text: "Play", text_font: font_med, text_color: 0x12241A }
|
||||
- button:
|
||||
id: btn_sn_exit
|
||||
align: CENTER
|
||||
x: 66
|
||||
y: 62
|
||||
width: 112
|
||||
height: 48
|
||||
radius: 24
|
||||
bg_color: 0x3A3A3D
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
hidden: true
|
||||
on_click: [ lambda: 'id(g_sn_state) = 0;', script.execute: sn_screen ]
|
||||
widgets:
|
||||
- label: { align: CENTER, text: "Exit", text_font: font_med, text_color: 0xFFFFFF }
|
||||
- label: { id: lbl_sn_howto_t, align: CENTER, y: -104, text: "HOW TO PLAY?", text_font: font_sel, text_color: 0xA6D85F, hidden: true }
|
||||
- obj: { id: sn_b1, align: CENTER, x: -96, y: -52, width: 24, height: 24, radius: 12, bg_color: 0x6FB23F, bg_opa: COVER, border_width: 0, pad_all: 0, hidden: true, widgets: [ label: { align: CENTER, text: "1", text_font: font_artist, text_color: 0xFFFFFF } ] }
|
||||
- obj: { id: sn_b2, align: CENTER, x: -96, y: -16, width: 24, height: 24, radius: 12, bg_color: 0x6FB23F, bg_opa: COVER, border_width: 0, pad_all: 0, hidden: true, widgets: [ label: { align: CENTER, text: "2", text_font: font_artist, text_color: 0xFFFFFF } ] }
|
||||
- obj: { id: sn_b3, align: CENTER, x: -96, y: 20, width: 24, height: 24, radius: 12, bg_color: 0x6FB23F, bg_opa: COVER, border_width: 0, pad_all: 0, hidden: true, widgets: [ label: { align: CENTER, text: "3", text_font: font_artist, text_color: 0xFFFFFF } ] }
|
||||
- obj: { id: sn_b4, align: CENTER, x: -96, y: 56, width: 24, height: 24, radius: 12, bg_color: 0xE05A4A, bg_opa: COVER, border_width: 0, pad_all: 0, hidden: true, widgets: [ label: { align: CENTER, text: "!", text_font: font_artist, text_color: 0xFFFFFF } ] }
|
||||
- label: { id: lbl_sn_howto1, align_to: { id: sn_b1, align: OUT_RIGHT_MID, x: 8 }, text: "Turn knob to steer", text_font: font_med, text_color: 0xC6DCC0, hidden: true }
|
||||
- label: { id: lbl_sn_howto2, align_to: { id: sn_b2, align: OUT_RIGHT_MID, x: 8 }, text: "Eat fruit to grow", text_font: font_med, text_color: 0xC6DCC0, hidden: true }
|
||||
- label: { id: lbl_sn_howto3, align_to: { id: sn_b3, align: OUT_RIGHT_MID, x: 8 }, text: "Avoid the ring & tail", text_font: font_med, text_color: 0xC6DCC0, hidden: true }
|
||||
- label: { id: lbl_sn_howto4, align_to: { id: sn_b4, align: OUT_RIGHT_MID, x: 8 }, text: "Skulls kill - avoid!", text_font: font_med, text_color: 0xC6DCC0, hidden: true }
|
||||
- label: { id: lbl_sn_scores_t, align: CENTER, y: -116, text: "SCORES", text_font: font_sel, text_color: 0xA6D85F, hidden: true }
|
||||
- label: { id: lbl_sn_scores1, align: CENTER, x: -52, y: 0, text: "", text_align: LEFT, text_font: font_artist, text_color: 0xC6DCC0, hidden: true }
|
||||
- label: { id: lbl_sn_scores2, align: CENTER, x: 56, y: 0, text: "", text_align: LEFT, text_font: font_artist, text_color: 0xC6DCC0, hidden: true }
|
||||
- button:
|
||||
id: btn_sn_back
|
||||
align: CENTER
|
||||
y: 120
|
||||
width: 130
|
||||
height: 42
|
||||
radius: 21
|
||||
bg_color: 0x3A3A3D
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
hidden: true
|
||||
on_click: [ lambda: 'id(g_sn_state) = 0;', script.execute: sn_screen ]
|
||||
widgets:
|
||||
- label: { align: CENTER, text: "Back", text_font: font_med, text_color: 0xFFFFFF }
|
||||
@@ -0,0 +1,498 @@
|
||||
# Space Wars - optional carousel screen (base id 4). Vertical shooter.
|
||||
# Remove it: drop this file from your config's package "files:" list. Nothing else to edit.
|
||||
# Scores (g_sw_top / g_sw_best) + the "Reset scores" settings action live here - fully self-contained.
|
||||
|
||||
esphome:
|
||||
on_boot:
|
||||
- priority: 660
|
||||
then:
|
||||
- lambda: 'id(g_present)[4] = true;' # register after clock/player/timer/cars
|
||||
# Settings (Settings -> Widgets -> Space Wars -> Reset scores) + factory-reset handling.
|
||||
- priority: 650
|
||||
then:
|
||||
- lambda: |-
|
||||
int g = id(g_wgrp_n)++;
|
||||
id(g_wgrp_labels) += (g ? "\n" : ""); id(g_wgrp_labels) += "Space Wars";
|
||||
int o = id(g_wopt_n)++;
|
||||
id(g_wopt_group)[o] = g; id(g_wopt_label)[o] = "Reset scores"; id(g_wopt_kind)[o] = 1; id(g_wopt_ptr)[o] = &id(g_sw_reset);
|
||||
|
||||
globals:
|
||||
# --- scores (persistent) + reset flag, owned by this package ---
|
||||
- id: g_sw_best
|
||||
type: int
|
||||
restore_value: yes
|
||||
initial_value: '0'
|
||||
- id: g_sw_top
|
||||
type: int[10]
|
||||
restore_value: yes
|
||||
- id: g_sw_reset # Widgets "Reset scores" sets this; the watcher below clears the scores
|
||||
type: bool
|
||||
restore_value: no
|
||||
initial_value: 'false'
|
||||
# --- Game 2 "Space Wars" (page_game2, g_base==4) ---
|
||||
- id: g_sw_state # 0=menu 1=playing 2=game over 3=how-to 4=scores
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
- id: g_sw_px # player ship X
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
- id: g_sw_lives
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '3'
|
||||
- id: g_sw_inv
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
- id: g_sw_score
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
- id: g_fire # auto-fire cadence counter
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
- id: g_tcount # time counter (points for time survived)
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
- id: g_sw_crash
|
||||
type: bool
|
||||
restore_value: no
|
||||
initial_value: 'false'
|
||||
- id: g_b_x # player bullets (2): X
|
||||
type: int[2]
|
||||
restore_value: no
|
||||
- id: g_b_y
|
||||
type: int[2]
|
||||
restore_value: no
|
||||
- id: g_b_on
|
||||
type: bool[2]
|
||||
restore_value: no
|
||||
- id: g_en_x # enemies (3)
|
||||
type: int[3]
|
||||
restore_value: no
|
||||
- id: g_en_y
|
||||
type: int[3]
|
||||
restore_value: no
|
||||
- id: g_en_on
|
||||
type: bool[3]
|
||||
restore_value: no
|
||||
- id: g_eb_x # enemy bullets (2)
|
||||
type: int[2]
|
||||
restore_value: no
|
||||
- id: g_eb_y
|
||||
type: int[2]
|
||||
restore_value: no
|
||||
- id: g_eb_on
|
||||
type: bool[2]
|
||||
restore_value: no
|
||||
- id: g_exp_f # explosion: countdown frame counter (0=inactive), ticks in the 70ms loop
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
- id: g_exp_x # explosion: position (from the destroyed enemy)
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
- id: g_exp_y
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
- id: g_exp_new # flag: start the animation this tick
|
||||
type: bool
|
||||
restore_value: no
|
||||
initial_value: 'false'
|
||||
- id: g_exp_hide # flag: hide the explosion this tick
|
||||
type: bool
|
||||
restore_value: no
|
||||
initial_value: 'false'
|
||||
|
||||
script:
|
||||
# ===== SPACE WARS (game 2) =====
|
||||
- id: sw_render # ship X + score + lives
|
||||
then:
|
||||
- lvgl.widget.update: { id: sw_ship, x: !lambda 'return id(g_sw_px);' }
|
||||
- lvgl.label.update: { id: lbl_sw_score, text: !lambda 'char b[12]; snprintf(b,sizeof(b),"%d",id(g_sw_score)); return std::string(b);' }
|
||||
- lvgl.label.update:
|
||||
id: lbl_sw_lives
|
||||
text: !lambda |-
|
||||
std::string h; for (int i=0;i<id(g_sw_lives);i++) h += ""; return h;
|
||||
- id: sw_start
|
||||
then:
|
||||
- lambda: |-
|
||||
id(g_sw_state)=1; id(g_sw_score)=0; id(g_sw_lives)=3; id(g_sw_inv)=0;
|
||||
id(g_sw_px)=0; id(g_fire)=0; id(g_tcount)=0;
|
||||
id(g_knob_capture)=true; id(g_knob_delta)=0;
|
||||
id(g_exp_f)=0; id(g_exp_new)=false; id(g_exp_hide)=false;
|
||||
for (int i=0;i<2;i++){ id(g_b_on)[i]=false; id(g_eb_on)[i]=false; }
|
||||
for (int i=0;i<3;i++){ id(g_en_on)[i]=true; id(g_en_x)[i]=(int)(esp_random()%280)-140; id(g_en_y)[i]=-160 - i*70; }
|
||||
- script.execute: sw_screen
|
||||
- script.execute: sw_render
|
||||
- id: sw_lose
|
||||
then:
|
||||
- lambda: 'id(g_sw_lives)--; id(g_sw_inv)=16;'
|
||||
- lvgl.widget.show: sw_flash
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_sw_lives) <= 0;' }
|
||||
then: [ script.execute: sw_over ]
|
||||
else: [ script.execute: sw_render ]
|
||||
- id: sw_over
|
||||
then:
|
||||
- lambda: |-
|
||||
id(g_sw_state)=2; // sw_screen keeps the knob captured on game over (no volume)
|
||||
int s=id(g_sw_score);
|
||||
if (s>id(g_sw_best)) id(g_sw_best)=s;
|
||||
for (int i=0;i<10;i++){ if(s>id(g_sw_top)[i]){ for(int j=9;j>i;j--) id(g_sw_top)[j]=id(g_sw_top)[j-1]; id(g_sw_top)[i]=s; break; } }
|
||||
// reset game state so the end of the current tick restores nothing (red flash / enemies)
|
||||
id(g_sw_inv)=0;
|
||||
for(int i=0;i<3;i++) id(g_en_on)[i]=false;
|
||||
for(int i=0;i<2;i++){ id(g_b_on)[i]=false; id(g_eb_on)[i]=false; }
|
||||
id(g_exp_f)=0; id(g_exp_new)=false; id(g_exp_hide)=false;
|
||||
- script.execute: sw_screen
|
||||
- id: sw_screen # 0=menu 1=playing 2=game over 3=how-to 4=scores
|
||||
then:
|
||||
# knob steers only during play + game over -> capture it there (block volume), free it in menu/how-to/scores
|
||||
- lambda: 'id(g_knob_capture) = (id(g_sw_state) == 1 || id(g_sw_state) == 2);'
|
||||
- lvgl.widget.hide: sw_bg
|
||||
- lvgl.widget.hide: sw_logo
|
||||
- lvgl.widget.hide: sw_ship
|
||||
- lvgl.widget.hide: sw_exp
|
||||
- lvgl.widget.hide: b0
|
||||
- lvgl.widget.hide: b1
|
||||
- lvgl.widget.hide: eb0
|
||||
- lvgl.widget.hide: eb1
|
||||
- lvgl.widget.hide: en0
|
||||
- lvgl.widget.hide: en1
|
||||
- lvgl.widget.hide: en2
|
||||
- lvgl.widget.hide: lbl_sw_lives
|
||||
- lvgl.widget.hide: lbl_sw_score
|
||||
- lvgl.widget.hide: sw_flash
|
||||
- lvgl.widget.hide: btn_sw_start
|
||||
- lvgl.widget.hide: btn_sw_howto
|
||||
- lvgl.widget.hide: btn_sw_scores
|
||||
- lvgl.widget.hide: lbl_sw_over
|
||||
- lvgl.widget.hide: lbl_sw_rec
|
||||
- lvgl.widget.hide: btn_sw_replay
|
||||
- lvgl.widget.hide: btn_sw_exit
|
||||
- lvgl.widget.hide: lbl_sw_ht_t
|
||||
- lvgl.widget.hide: badge_h1
|
||||
- lvgl.widget.hide: badge_h2
|
||||
- lvgl.widget.hide: badge_h3
|
||||
- lvgl.widget.hide: badge_h4
|
||||
- lvgl.widget.hide: lbl_sw_ht1
|
||||
- lvgl.widget.hide: lbl_sw_ht2
|
||||
- lvgl.widget.hide: lbl_sw_ht3
|
||||
- lvgl.widget.hide: lbl_sw_ht4
|
||||
- lvgl.widget.hide: lbl_sw_hheart
|
||||
- lvgl.widget.hide: lbl_sw_st
|
||||
- lvgl.widget.hide: lbl_sw_s1
|
||||
- lvgl.widget.hide: lbl_sw_s2
|
||||
- lvgl.widget.hide: btn_sw_back
|
||||
- if: # MENU
|
||||
condition: { lambda: 'return id(g_sw_state)==0;' }
|
||||
then: [ lvgl.widget.show: sw_logo, lvgl.widget.show: btn_sw_start, lvgl.widget.show: btn_sw_howto, lvgl.widget.show: btn_sw_scores ]
|
||||
- if: # GAME
|
||||
condition: { lambda: 'return id(g_sw_state)==1;' }
|
||||
then:
|
||||
- lvgl.widget.show: sw_bg
|
||||
- lvgl.widget.show: sw_ship
|
||||
- lvgl.widget.show: en0
|
||||
- lvgl.widget.show: en1
|
||||
- lvgl.widget.show: en2
|
||||
- lvgl.widget.show: lbl_sw_lives
|
||||
- lvgl.widget.show: lbl_sw_score
|
||||
- if: # GAME OVER
|
||||
condition: { lambda: 'return id(g_sw_state)==2;' }
|
||||
then:
|
||||
- lvgl.widget.show: sw_logo
|
||||
- lvgl.widget.show: lbl_sw_over
|
||||
- lvgl.widget.show: lbl_sw_rec
|
||||
- lvgl.widget.show: btn_sw_replay
|
||||
- lvgl.widget.show: btn_sw_exit
|
||||
- lvgl.label.update: { id: lbl_sw_over, text: !lambda 'char b[24]; snprintf(b,sizeof(b),"Score: %d", id(g_sw_score)); return std::string(b);' }
|
||||
- lvgl.label.update: { id: lbl_sw_rec, text: !lambda 'char b[24]; snprintf(b,sizeof(b),"Best: %d", id(g_sw_best)); return std::string(b);' }
|
||||
- if: # HOW TO PLAY
|
||||
condition: { lambda: 'return id(g_sw_state)==3;' }
|
||||
then:
|
||||
- lvgl.widget.show: sw_bg
|
||||
- lvgl.widget.show: lbl_sw_ht_t
|
||||
- lvgl.widget.show: badge_h1
|
||||
- lvgl.widget.show: badge_h2
|
||||
- lvgl.widget.show: badge_h3
|
||||
- lvgl.widget.show: badge_h4
|
||||
- lvgl.widget.show: lbl_sw_ht1
|
||||
- lvgl.widget.show: lbl_sw_ht2
|
||||
- lvgl.widget.show: lbl_sw_ht3
|
||||
- lvgl.widget.show: lbl_sw_ht4
|
||||
- lvgl.widget.show: lbl_sw_hheart
|
||||
- lvgl.widget.show: btn_sw_back
|
||||
- if: # SCORES
|
||||
condition: { lambda: 'return id(g_sw_state)==4;' }
|
||||
then:
|
||||
- lvgl.widget.show: sw_bg
|
||||
- lvgl.widget.show: lbl_sw_st
|
||||
- lvgl.widget.show: lbl_sw_s1
|
||||
- lvgl.widget.show: lbl_sw_s2
|
||||
- lvgl.widget.show: btn_sw_back
|
||||
- lvgl.label.update: { id: lbl_sw_s1, text: !lambda 'std::string s; char b[24]; for(int i=0;i<5;i++){ if(i) s+="\n"; if(id(g_sw_top)[i]>0){ snprintf(b,sizeof(b),"%d. %d",i+1,id(g_sw_top)[i]); } else { snprintf(b,sizeof(b),"%d. -",i+1); } s+=b; } return s;' }
|
||||
- lvgl.label.update: { id: lbl_sw_s2, text: !lambda 'std::string s; char b[24]; for(int i=5;i<10;i++){ if(i>5) s+="\n"; if(id(g_sw_top)[i]>0){ snprintf(b,sizeof(b),"%d. %d",i+1,id(g_sw_top)[i]); } else { snprintf(b,sizeof(b),"%d. -",i+1); } s+=b; } return s;' }
|
||||
|
||||
interval:
|
||||
# Reset scores when the Widgets action fired (g_sw_reset). Idempotent + cheap.
|
||||
- interval: 250ms
|
||||
then:
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_sw_reset) || id(g_factory_reset);' }
|
||||
then:
|
||||
- lambda: 'for (int i = 0; i < 10; i++) id(g_sw_top)[i] = 0; id(g_sw_best) = 0; id(g_sw_reset) = false;'
|
||||
# nav: show this screen when it is selected in the carousel
|
||||
- interval: 50ms
|
||||
then:
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_nav_req) && id(g_base)==4;' }
|
||||
then:
|
||||
- lambda: 'id(g_nav_req) = false; id(g_sw_state) = 0;'
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==0;' }, then: [lvgl.page.show: { id: page_game2, animation: move_left, time: 250ms }] }
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==1;' }, then: [lvgl.page.show: { id: page_game2, animation: move_right, time: 250ms }] }
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==2;' }, then: [lvgl.page.show: { id: page_game2, animation: move_top, time: 250ms }] }
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==3;' }, then: [lvgl.page.show: { id: page_game2, animation: move_bottom, time: 250ms }] }
|
||||
- script.execute: sw_screen
|
||||
# Space Wars - tick only on the Space screen during play
|
||||
- interval: 70ms
|
||||
then:
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_base) == 4 && id(g_sw_state) == 1;' }
|
||||
then:
|
||||
- lambda: |-
|
||||
id(g_sw_px) += id(g_knob_delta) * 18; if (id(g_sw_px) < -150) id(g_sw_px) = -150; if (id(g_sw_px) > 150) id(g_sw_px) = 150; id(g_knob_delta) = 0;
|
||||
int px = id(g_sw_px);
|
||||
if (id(g_sw_inv) > 0) id(g_sw_inv)--;
|
||||
if (id(g_exp_f) > 0) { id(g_exp_f)--; if (id(g_exp_f)==0) id(g_exp_hide)=true; }
|
||||
id(g_tcount)++; if (id(g_tcount) % 10 == 0) id(g_sw_score)++;
|
||||
// auto-fire every 5 ticks into a free slot
|
||||
id(g_fire)++;
|
||||
if (id(g_fire) >= 5) { id(g_fire)=0; for(int i=0;i<2;i++){ if(!id(g_b_on)[i]){ id(g_b_on)[i]=true; id(g_b_x)[i]=px; id(g_b_y)[i]=100; break; } } }
|
||||
// player bullets go up
|
||||
for (int i=0;i<2;i++){ if(id(g_b_on)[i]){ id(g_b_y)[i]-=24; if(id(g_b_y)[i]<-185) id(g_b_on)[i]=false; } }
|
||||
// enemies go down; destroyed/escaped -> respawn at the top
|
||||
for (int e=0;e<3;e++){
|
||||
if(id(g_en_on)[e]){ id(g_en_y)[e]+=6; if(id(g_en_y)[e]>188){ id(g_en_y)[e]=-160; id(g_en_x)[e]=(int)(esp_random()%280)-140; } }
|
||||
else { id(g_en_on)[e]=true; id(g_en_y)[e]=-160-(int)(esp_random()%140); id(g_en_x)[e]=(int)(esp_random()%280)-140; }
|
||||
}
|
||||
// enemies shoot
|
||||
if ((int)(esp_random()%100) < 9) { int k=(int)(esp_random()%3); if(id(g_en_on)[k]) for(int i=0;i<2;i++){ if(!id(g_eb_on)[i]){ id(g_eb_on)[i]=true; id(g_eb_x)[i]=id(g_en_x)[k]; id(g_eb_y)[i]=id(g_en_y)[k]+26; break; } } }
|
||||
// enemy bullets go down
|
||||
for (int i=0;i<2;i++){ if(id(g_eb_on)[i]){ id(g_eb_y)[i]+=13; if(id(g_eb_y)[i]>188) id(g_eb_on)[i]=false; } }
|
||||
// player bullet x enemy
|
||||
for (int b=0;b<2;b++){ if(id(g_b_on)[b]) for(int e=0;e<3;e++){ if(id(g_en_on)[e] && abs(id(g_b_x)[b]-id(g_en_x)[e])<22 && abs(id(g_b_y)[b]-id(g_en_y)[e])<26){ id(g_en_on)[e]=false; id(g_b_on)[b]=false; id(g_sw_score)+=10; id(g_exp_x)=id(g_en_x)[e]; id(g_exp_y)=id(g_en_y)[e]; id(g_exp_f)=6; id(g_exp_new)=true; break; } } }
|
||||
// enemy / enemy bullet x player
|
||||
id(g_sw_crash)=false;
|
||||
if (id(g_sw_inv)<=0){
|
||||
for(int e=0;e<3;e++){ if(id(g_en_on)[e] && abs(id(g_en_x)[e]-px)<26 && abs(id(g_en_y)[e]-130)<32){ id(g_en_on)[e]=false; id(g_sw_crash)=true; break; } }
|
||||
for(int i=0;i<2;i++){ if(id(g_eb_on)[i] && abs(id(g_eb_x)[i]-px)<18 && abs(id(g_eb_y)[i]-130)<26){ id(g_eb_on)[i]=false; id(g_sw_crash)=true; break; } }
|
||||
}
|
||||
- lvgl.widget.update: { id: sw_ship, x: !lambda 'return id(g_sw_px);' }
|
||||
- if: { condition: { lambda: 'return id(g_sw_crash);' }, then: [ script.execute: sw_lose ] }
|
||||
# explosion after destroying an enemy: set position, show, run the animation once
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_exp_new);' }
|
||||
then:
|
||||
- lvgl.widget.update: { id: sw_exp, x: !lambda 'return id(g_exp_x);', y: !lambda 'return id(g_exp_y);', hidden: false }
|
||||
- lvgl.animimg.start: sw_exp
|
||||
- lambda: 'id(g_exp_new)=false;'
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_exp_hide);' }
|
||||
then:
|
||||
- lvgl.animimg.stop: sw_exp
|
||||
- lvgl.widget.hide: sw_exp
|
||||
- lambda: 'id(g_exp_hide)=false;'
|
||||
# positions + visibility
|
||||
- lvgl.widget.update: { id: b0, x: !lambda 'return id(g_b_x)[0];', y: !lambda 'return id(g_b_y)[0];', hidden: !lambda 'return !id(g_b_on)[0];' }
|
||||
- lvgl.widget.update: { id: b1, x: !lambda 'return id(g_b_x)[1];', y: !lambda 'return id(g_b_y)[1];', hidden: !lambda 'return !id(g_b_on)[1];' }
|
||||
- lvgl.widget.update: { id: eb0, x: !lambda 'return id(g_eb_x)[0];', y: !lambda 'return id(g_eb_y)[0];', hidden: !lambda 'return !id(g_eb_on)[0];' }
|
||||
- lvgl.widget.update: { id: eb1, x: !lambda 'return id(g_eb_x)[1];', y: !lambda 'return id(g_eb_y)[1];', hidden: !lambda 'return !id(g_eb_on)[1];' }
|
||||
- lvgl.widget.update: { id: en0, x: !lambda 'return id(g_en_x)[0];', y: !lambda 'return id(g_en_y)[0];', hidden: !lambda 'return !id(g_en_on)[0];' }
|
||||
- lvgl.widget.update: { id: en1, x: !lambda 'return id(g_en_x)[1];', y: !lambda 'return id(g_en_y)[1];', hidden: !lambda 'return !id(g_en_on)[1];' }
|
||||
- lvgl.widget.update: { id: en2, x: !lambda 'return id(g_en_x)[2];', y: !lambda 'return id(g_en_y)[2];', hidden: !lambda 'return !id(g_en_on)[2];' }
|
||||
- lvgl.label.update: { id: lbl_sw_score, text: !lambda 'char b[12]; snprintf(b,sizeof(b),"%d",id(g_sw_score)); return std::string(b);' }
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_sw_inv) > 0;' }
|
||||
then:
|
||||
- lvgl.widget.update: { id: sw_ship, hidden: !lambda 'return (id(g_sw_inv) % 4) >= 2;' }
|
||||
- lvgl.widget.update: { id: sw_flash, hidden: !lambda 'return id(g_sw_inv) < 12;' }
|
||||
|
||||
image:
|
||||
# Space Wars
|
||||
- id: img_spacebg
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/space-wars/spacebg.png"
|
||||
type: RGB565
|
||||
- id: img_logo2
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/space-wars/logo2.png"
|
||||
type: RGB565
|
||||
- id: img_sp
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/space-wars/player.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_se1
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/space-wars/enemy1.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_se2
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/space-wars/enemy2.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_exp0
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/space-wars/exp0.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_exp1
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/space-wars/exp1.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_exp2
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/space-wars/exp2.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_exp3
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/space-wars/exp3.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_exp4
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/space-wars/exp4.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_exp5
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/space-wars/exp5.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
|
||||
lvgl:
|
||||
pages:
|
||||
# --- SPACE WARS page (game 2): vertical shooter ---
|
||||
- id: page_game2
|
||||
bg_color: 0x140A20
|
||||
scrollbar_mode: "off"
|
||||
scrollable: false
|
||||
widgets:
|
||||
- image: { id: sw_bg, align: CENTER, x: 0, y: 0, src: img_spacebg, hidden: true }
|
||||
- image: { id: sw_logo, align: CENTER, x: 0, y: 0, src: img_logo2, hidden: true }
|
||||
- obj: { id: b0, align: CENTER, x: 0, y: 0, width: 4, height: 14, radius: 2, border_width: 0, bg_color: 0xFFE45A, hidden: true }
|
||||
- obj: { id: b1, align: CENTER, x: 0, y: 0, width: 4, height: 14, radius: 2, border_width: 0, bg_color: 0xFFE45A, hidden: true }
|
||||
- obj: { id: eb0, align: CENTER, x: 0, y: 0, width: 5, height: 14, radius: 2, border_width: 0, bg_color: 0xFF5050, hidden: true }
|
||||
- obj: { id: eb1, align: CENTER, x: 0, y: 0, width: 5, height: 14, radius: 2, border_width: 0, bg_color: 0xFF5050, hidden: true }
|
||||
- image: { id: en0, align: CENTER, x: -60, y: -150, src: img_se1, hidden: true }
|
||||
- image: { id: en1, align: CENTER, x: 60, y: -150, src: img_se1, hidden: true }
|
||||
- image: { id: en2, align: CENTER, x: 0, y: -150, src: img_se2, hidden: true }
|
||||
- image: { id: sw_ship, align: CENTER, x: 0, y: 130, src: img_sp, hidden: true }
|
||||
- animimg:
|
||||
id: sw_exp
|
||||
align: CENTER
|
||||
x: 0
|
||||
y: 0
|
||||
src: [img_exp0, img_exp1, img_exp2, img_exp3, img_exp4, img_exp5]
|
||||
duration: 420ms
|
||||
repeat_count: 1
|
||||
auto_start: false
|
||||
hidden: true
|
||||
- label: { id: lbl_sw_lives, align: CENTER, x: -54, y: -132, text: "", text_font: icons18, text_color: 0xE24B4A, hidden: true }
|
||||
- label: { id: lbl_sw_score, align: CENTER, x: 54, y: -132, text: "0", text_font: font_med, text_color: 0xCFEAE4, hidden: true }
|
||||
- obj: { id: sw_flash, align: CENTER, x: 0, y: 0, width: 360, height: 360, radius: 0, border_width: 0, bg_color: 0xFF2A2A, bg_opa: 45%, hidden: true }
|
||||
# MENU
|
||||
- button:
|
||||
id: btn_sw_start
|
||||
align: CENTER
|
||||
y: 4
|
||||
width: 150
|
||||
height: 44
|
||||
radius: 22
|
||||
bg_color: 0xFF7A4D
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
on_click: [ script.execute: sw_start ]
|
||||
widgets: [ label: { align: CENTER, text: "Start", text_font: font_med, text_color: 0x2A0E06 } ]
|
||||
- button:
|
||||
id: btn_sw_howto
|
||||
align: CENTER
|
||||
y: 54
|
||||
width: 150
|
||||
height: 44
|
||||
radius: 22
|
||||
bg_color: 0x3A2150
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
on_click: [ lambda: 'id(g_sw_state)=3;', script.execute: sw_screen ]
|
||||
widgets: [ label: { align: CENTER, text: "How to play?", text_font: font_med, text_color: 0xFFFFFF } ]
|
||||
- button:
|
||||
id: btn_sw_scores
|
||||
align: CENTER
|
||||
y: 104
|
||||
width: 150
|
||||
height: 44
|
||||
radius: 22
|
||||
bg_color: 0x3A2150
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
on_click: [ lambda: 'id(g_sw_state)=4;', script.execute: sw_screen ]
|
||||
widgets: [ label: { align: CENTER, text: "Scores", text_font: font_med, text_color: 0xFFFFFF } ]
|
||||
# GAME OVER
|
||||
- label: { id: lbl_sw_over, align: CENTER, x: 0, y: -10, text_align: CENTER, text: "", text_font: font_sel, text_color: 0xFFFFFF, hidden: true }
|
||||
- label: { id: lbl_sw_rec, align: CENTER, x: 0, y: 22, text_align: CENTER, text: "", text_font: font_artist, text_color: 0x9AA6B2, hidden: true }
|
||||
- button:
|
||||
id: btn_sw_replay
|
||||
align: CENTER
|
||||
x: -66
|
||||
y: 62
|
||||
width: 112
|
||||
height: 48
|
||||
radius: 24
|
||||
bg_color: 0xFF7A4D
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
hidden: true
|
||||
on_click: [ script.execute: sw_start ]
|
||||
widgets: [ label: { align: CENTER, text: "Play", text_font: font_med, text_color: 0x2A0E06 } ]
|
||||
- button:
|
||||
id: btn_sw_exit
|
||||
align: CENTER
|
||||
x: 66
|
||||
y: 62
|
||||
width: 112
|
||||
height: 48
|
||||
radius: 24
|
||||
bg_color: 0x3A3A3D
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
hidden: true
|
||||
on_click: [ lambda: 'id(g_sw_state)=0;', script.execute: sw_screen ]
|
||||
widgets: [ label: { align: CENTER, text: "Exit", text_font: font_med, text_color: 0xFFFFFF } ]
|
||||
# HOW TO PLAY
|
||||
- label: { id: lbl_sw_ht_t, align: CENTER, y: -104, text: "HOW TO PLAY?", text_font: font_sel, text_color: 0xFF7A4D, hidden: true }
|
||||
- obj: { id: badge_h1, align: CENTER, x: -90, y: -46, width: 24, height: 24, radius: 12, bg_color: 0xFF7A4D, bg_opa: COVER, border_width: 0, pad_all: 0, hidden: true, widgets: [ label: { align: CENTER, text: "1", text_font: font_artist, text_color: 0xFFFFFF } ] }
|
||||
- obj: { id: badge_h2, align: CENTER, x: -90, y: -10, width: 24, height: 24, radius: 12, bg_color: 0xFF7A4D, bg_opa: COVER, border_width: 0, pad_all: 0, hidden: true, widgets: [ label: { align: CENTER, text: "2", text_font: font_artist, text_color: 0xFFFFFF } ] }
|
||||
- obj: { id: badge_h3, align: CENTER, x: -90, y: 26, width: 24, height: 24, radius: 12, bg_color: 0xFF7A4D, bg_opa: COVER, border_width: 0, pad_all: 0, hidden: true, widgets: [ label: { align: CENTER, text: "3", text_font: font_artist, text_color: 0xFFFFFF } ] }
|
||||
- obj: { id: badge_h4, align: CENTER, x: -90, y: 62, width: 24, height: 24, radius: 12, bg_color: 0xFF7A4D, bg_opa: COVER, border_width: 0, pad_all: 0, hidden: true, widgets: [ label: { align: CENTER, text: "4", text_font: font_artist, text_color: 0xFFFFFF } ] }
|
||||
- label: { id: lbl_sw_ht1, align_to: { id: badge_h1, align: OUT_RIGHT_MID, x: 8 }, text: "Use the knob", text_font: font_med, text_color: 0xC2CDDA, hidden: true }
|
||||
- label: { id: lbl_sw_ht2, align_to: { id: badge_h2, align: OUT_RIGHT_MID, x: 8 }, text: "Dodge bullets", text_font: font_med, text_color: 0xC2CDDA, hidden: true }
|
||||
- label: { id: lbl_sw_ht3, align_to: { id: badge_h3, align: OUT_RIGHT_MID, x: 8 }, text: "Destroy enemies", text_font: font_med, text_color: 0xC2CDDA, hidden: true }
|
||||
- label: { id: lbl_sw_ht4, align_to: { id: badge_h4, align: OUT_RIGHT_MID, x: 8 }, text: "You have 3 lives", text_font: font_med, text_color: 0xC2CDDA, hidden: true }
|
||||
- label: { id: lbl_sw_hheart, align_to: { id: lbl_sw_ht4, align: OUT_RIGHT_MID, x: 6 }, text: "", text_font: icons18, text_color: 0xE24B4A, hidden: true }
|
||||
# SCORES
|
||||
- label: { id: lbl_sw_st, align: CENTER, y: -116, text: "SCORES", text_font: font_sel, text_color: 0xFF7A4D, hidden: true }
|
||||
- label: { id: lbl_sw_s1, align: CENTER, x: -50, y: -6, text: "", text_align: LEFT, text_font: font_artist, text_color: 0xC2CDDA, hidden: true }
|
||||
- label: { id: lbl_sw_s2, align: CENTER, x: 50, y: -6, text: "", text_align: LEFT, text_font: font_artist, text_color: 0xC2CDDA, hidden: true }
|
||||
- button:
|
||||
id: btn_sw_back
|
||||
align: CENTER
|
||||
y: 120
|
||||
width: 130
|
||||
height: 42
|
||||
radius: 21
|
||||
bg_color: 0x3A3A3D
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
hidden: true
|
||||
on_click: [ lambda: 'id(g_sw_state)=0;', script.execute: sw_screen ]
|
||||
widgets: [ label: { align: CENTER, text: "Back", text_font: font_med, text_color: 0xFFFFFF } ]
|
||||
@@ -0,0 +1,298 @@
|
||||
# Thermostat - optional carousel screen (base id 7).
|
||||
# A round dial for a Home Assistant climate.* entity (${climate_entity}):
|
||||
# * big centre = the target temperature; turn the KNOB to change it (by the entity's
|
||||
# target_temp_step). The new value is sent to HA ~0.6 s after you stop turning.
|
||||
# * "now ..." = the current room temperature; "RH ..%" = humidity (if the entity has it).
|
||||
# * top label + the target colour show the action: heating (orange) / cooling (blue) /
|
||||
# idle (teal) / off (grey). Bottom chip = the HVAC mode.
|
||||
# * TAP toggles the climate on/off.
|
||||
#
|
||||
# Set ${climate_entity} in your config. Remove this screen by dropping the file from the
|
||||
# package "files:" list.
|
||||
|
||||
esphome:
|
||||
on_boot:
|
||||
- priority: 655
|
||||
then:
|
||||
- lambda: 'id(g_present)[7] = true;' # register the thermostat screen
|
||||
|
||||
globals:
|
||||
- id: g_th_cur # current temperature (-127 = no reading yet)
|
||||
type: float
|
||||
restore_value: no
|
||||
initial_value: '-127.0'
|
||||
- id: g_th_target # target temperature (synced from HA; edited locally by the knob)
|
||||
type: float
|
||||
restore_value: no
|
||||
initial_value: '20.0'
|
||||
- id: g_th_action # hvac_action: heating / cooling / idle / off (may be empty)
|
||||
type: std::string
|
||||
restore_value: no
|
||||
- id: g_th_mode # hvac mode (the entity state): off / heat / cool / auto / heat_cool
|
||||
type: std::string
|
||||
restore_value: no
|
||||
- id: g_th_hum # current humidity % (-1 = none)
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '-1'
|
||||
- id: g_th_min
|
||||
type: float
|
||||
restore_value: no
|
||||
initial_value: '7.0'
|
||||
- id: g_th_max
|
||||
type: float
|
||||
restore_value: no
|
||||
initial_value: '35.0'
|
||||
- id: g_th_step
|
||||
type: float
|
||||
restore_value: no
|
||||
initial_value: '0.5'
|
||||
- id: g_th_pending # true = local edit not yet sent; blocks HA from overwriting the target
|
||||
type: bool
|
||||
restore_value: no
|
||||
initial_value: 'false'
|
||||
- id: g_th_lasttap # last g_tap_seq we handled (one tap = one on/off toggle)
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
|
||||
script:
|
||||
# Sends the edited target to HA, but only ~0.6 s after the last knob turn (mode: restart).
|
||||
- id: th_commit
|
||||
mode: restart
|
||||
then:
|
||||
- delay: 600ms
|
||||
- homeassistant.action:
|
||||
action: climate.set_temperature
|
||||
data:
|
||||
entity_id: ${climate_entity}
|
||||
temperature: !lambda 'char b[8]; snprintf(b, sizeof(b), "%.1f", id(g_th_target)); return std::string(b);'
|
||||
- lambda: 'id(g_th_pending) = false;'
|
||||
# Redraws the dial from the state globals.
|
||||
- id: th_render
|
||||
then:
|
||||
- lvgl.label.update:
|
||||
id: th_target
|
||||
text: !lambda 'char b[12]; snprintf(b, sizeof(b), "%.1f°", id(g_th_target)); return std::string(b);'
|
||||
- lvgl.label.update:
|
||||
id: th_cur
|
||||
text: !lambda |-
|
||||
if (id(g_th_cur) < -100) return std::string("now --");
|
||||
char b[16]; snprintf(b, sizeof(b), "now %.1f°", id(g_th_cur)); return std::string(b);
|
||||
- lvgl.widget.update: { id: th_hum, hidden: !lambda 'return id(g_th_hum) < 0;' }
|
||||
- lvgl.label.update:
|
||||
id: th_hum
|
||||
text: !lambda |-
|
||||
if (id(g_th_hum) < 0) return std::string("");
|
||||
char b[12]; snprintf(b, sizeof(b), "RH %d%%", id(g_th_hum)); return std::string(b);
|
||||
- lvgl.label.update:
|
||||
id: th_mode
|
||||
text: !lambda |-
|
||||
std::string m = id(g_th_mode);
|
||||
if (m == "heat_cool") return std::string("Auto");
|
||||
if (m.empty()) return std::string("-");
|
||||
if (m[0] >= 'a' && m[0] <= 'z') m[0] -= 32; return m;
|
||||
- lvgl.arc.update:
|
||||
id: th_arc
|
||||
value: !lambda |-
|
||||
float r = id(g_th_max) - id(g_th_min);
|
||||
if (r <= 0) return 0.0f;
|
||||
float p = 100.0f * (id(g_th_target) - id(g_th_min)) / r;
|
||||
if (p < 0) p = 0; if (p > 100) p = 100;
|
||||
return p;
|
||||
- lvgl.label.update:
|
||||
id: th_action
|
||||
text: !lambda |-
|
||||
std::string m = id(g_th_mode), a = id(g_th_action);
|
||||
if (m == "off" || m.empty()) return std::string("OFF");
|
||||
if (a.find("heat") != std::string::npos || (a.empty() && m == "heat")) return std::string("HEATING");
|
||||
if (a.find("cool") != std::string::npos || (a.empty() && m == "cool")) return std::string("COOLING");
|
||||
if (a == "idle") return std::string("IDLE");
|
||||
if (!a.empty()) { if (a[0] >= 'a' && a[0] <= 'z') a[0] -= 32; return a; }
|
||||
if (m[0] >= 'a' && m[0] <= 'z') m[0] -= 32; return m;
|
||||
# accent colour for the action label + target by heat/cool/off/idle
|
||||
- if:
|
||||
condition:
|
||||
lambda: 'return id(g_th_mode) != "off" && !id(g_th_mode).empty() && (id(g_th_action).find("heat") != std::string::npos || (id(g_th_action).empty() && id(g_th_mode) == "heat"));'
|
||||
then:
|
||||
- lvgl.label.update: { id: th_action, text_color: 0xFF6B30 }
|
||||
- lvgl.label.update: { id: th_target, text_color: 0xFF6B30 }
|
||||
- lvgl.arc.update: { id: th_arc, indicator: { arc_color: 0xFF6B30 } }
|
||||
else:
|
||||
- if:
|
||||
condition:
|
||||
lambda: 'return id(g_th_mode) != "off" && !id(g_th_mode).empty() && (id(g_th_action).find("cool") != std::string::npos || (id(g_th_action).empty() && id(g_th_mode) == "cool"));'
|
||||
then:
|
||||
- lvgl.label.update: { id: th_action, text_color: 0x3FA9D6 }
|
||||
- lvgl.label.update: { id: th_target, text_color: 0x3FA9D6 }
|
||||
- lvgl.arc.update: { id: th_arc, indicator: { arc_color: 0x3FA9D6 } }
|
||||
else:
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_th_mode) == "off" || id(g_th_mode).empty();' }
|
||||
then:
|
||||
- lvgl.label.update: { id: th_action, text_color: 0x6F9A92 }
|
||||
- lvgl.label.update: { id: th_target, text_color: 0x6F9A92 }
|
||||
- lvgl.arc.update: { id: th_arc, indicator: { arc_color: 0x44506A } }
|
||||
else:
|
||||
- lvgl.label.update: { id: th_action, text_color: 0x9FC7BE }
|
||||
- lvgl.label.update: { id: th_target, text_color: 0xFFFFFF }
|
||||
- lvgl.arc.update: { id: th_arc, indicator: { arc_color: 0x2FE6CF } }
|
||||
- lambda: |- # position the target head-dot on the gauge
|
||||
float r = id(g_th_max) - id(g_th_min);
|
||||
float p = (r > 0) ? 100.0f * (id(g_th_target) - id(g_th_min)) / r : 0.0f;
|
||||
if (p < 0) p = 0; if (p > 100) p = 100;
|
||||
float a = (135.0f + p / 100.0f * 270.0f) * 0.01745329f;
|
||||
int x = 180 + (int) lroundf(144.0f * cosf(a)) - 9;
|
||||
int y = 180 + (int) lroundf(144.0f * sinf(a)) - 9;
|
||||
lv_obj_set_pos(id(th_dot), x, y);
|
||||
|
||||
sensor:
|
||||
- platform: homeassistant
|
||||
id: th_cur_s
|
||||
entity_id: ${climate_entity}
|
||||
attribute: current_temperature
|
||||
on_value:
|
||||
- lambda: 'id(g_th_cur) = x;'
|
||||
- script.execute: th_render
|
||||
- platform: homeassistant
|
||||
id: th_tgt_s
|
||||
entity_id: ${climate_entity}
|
||||
attribute: temperature
|
||||
on_value:
|
||||
- lambda: 'if (!id(g_th_pending)) id(g_th_target) = x;' # don't clobber a value being edited
|
||||
- script.execute: th_render
|
||||
- platform: homeassistant
|
||||
id: th_hum_s
|
||||
entity_id: ${climate_entity}
|
||||
attribute: current_humidity
|
||||
on_value:
|
||||
- lambda: 'id(g_th_hum) = (int) roundf(x);'
|
||||
- script.execute: th_render
|
||||
- platform: homeassistant
|
||||
id: th_min_s
|
||||
entity_id: ${climate_entity}
|
||||
attribute: min_temp
|
||||
on_value: [ lambda: 'id(g_th_min) = x;' ]
|
||||
- platform: homeassistant
|
||||
id: th_max_s
|
||||
entity_id: ${climate_entity}
|
||||
attribute: max_temp
|
||||
on_value: [ lambda: 'id(g_th_max) = x;' ]
|
||||
- platform: homeassistant
|
||||
id: th_step_s
|
||||
entity_id: ${climate_entity}
|
||||
attribute: target_temp_step
|
||||
on_value: [ lambda: 'if (x > 0) id(g_th_step) = x;' ]
|
||||
|
||||
text_sensor:
|
||||
- platform: homeassistant
|
||||
id: th_mode_s
|
||||
entity_id: ${climate_entity} # the entity state IS the hvac mode
|
||||
on_value:
|
||||
- lambda: 'id(g_th_mode) = x;'
|
||||
- script.execute: th_render
|
||||
- platform: homeassistant
|
||||
id: th_action_s
|
||||
entity_id: ${climate_entity}
|
||||
attribute: hvac_action
|
||||
on_value:
|
||||
- lambda: 'id(g_th_action) = x;'
|
||||
- script.execute: th_render
|
||||
|
||||
interval:
|
||||
# Carousel nav: when selected, show the page and take the knob.
|
||||
- interval: 50ms
|
||||
then:
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_nav_req) && id(g_base)==7;' }
|
||||
then:
|
||||
- lambda: 'id(g_nav_req) = false; id(g_knob_capture) = true; id(g_th_lasttap) = id(g_tap_seq);'
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==0;' }, then: [lvgl.page.show: { id: page_thermostat, animation: move_left, time: 250ms }] }
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==1;' }, then: [lvgl.page.show: { id: page_thermostat, animation: move_right, time: 250ms }] }
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==2;' }, then: [lvgl.page.show: { id: page_thermostat, animation: move_top, time: 250ms }] }
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==3;' }, then: [lvgl.page.show: { id: page_thermostat, animation: move_bottom, time: 250ms }] }
|
||||
- script.execute: th_render
|
||||
# Knob (adjust target, debounced commit) + tap (on/off) while this screen is on top.
|
||||
- interval: 120ms
|
||||
then:
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_base)==7 && id(g_knob_delta) != 0;' }
|
||||
then:
|
||||
- lambda: |-
|
||||
id(g_th_target) += id(g_knob_delta) * id(g_th_step);
|
||||
id(g_knob_delta) = 0;
|
||||
if (id(g_th_target) < id(g_th_min)) id(g_th_target) = id(g_th_min);
|
||||
if (id(g_th_target) > id(g_th_max)) id(g_th_target) = id(g_th_max);
|
||||
id(g_th_pending) = true;
|
||||
- script.execute: th_commit
|
||||
- script.execute: th_render
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_base)==7 && id(g_tap_seq) != id(g_th_lasttap);' }
|
||||
then:
|
||||
- lambda: 'id(g_th_lasttap) = id(g_tap_seq);'
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_th_mode) == "off";' }
|
||||
then:
|
||||
- homeassistant.action:
|
||||
action: climate.turn_on
|
||||
data:
|
||||
entity_id: ${climate_entity}
|
||||
else:
|
||||
- homeassistant.action:
|
||||
action: climate.turn_off
|
||||
data:
|
||||
entity_id: ${climate_entity}
|
||||
|
||||
lvgl:
|
||||
pages:
|
||||
- id: page_thermostat
|
||||
bg_color: 0x0B1020
|
||||
widgets:
|
||||
- obj: # faint outer ring for depth
|
||||
id: th_ring_out
|
||||
align: CENTER
|
||||
width: 332
|
||||
height: 332
|
||||
radius: 166
|
||||
bg_opa: TRANSP
|
||||
border_width: 2
|
||||
border_color: 0x1B2740
|
||||
border_opa: 70%
|
||||
clickable: false
|
||||
scrollable: false
|
||||
- arc:
|
||||
id: th_arc
|
||||
align: CENTER
|
||||
width: 300
|
||||
height: 300
|
||||
arc_width: 12
|
||||
adjustable: false
|
||||
min_value: 0
|
||||
max_value: 100
|
||||
value: 50
|
||||
start_angle: 135
|
||||
end_angle: 45
|
||||
arc_color: 0x21304A
|
||||
indicator:
|
||||
arc_color: 0x2FE6CF
|
||||
arc_width: 12
|
||||
- obj: # head-dot: white bead marking the target on the gauge (positioned in th_render)
|
||||
id: th_dot
|
||||
x: 0
|
||||
y: 0
|
||||
width: 18
|
||||
height: 18
|
||||
radius: 9
|
||||
bg_color: 0xFFFFFF
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
shadow_color: 0xFFFFFF
|
||||
shadow_width: 10
|
||||
shadow_opa: 55%
|
||||
clickable: false
|
||||
scrollable: false
|
||||
- label: { id: th_action, align: CENTER, y: -86, text: "", text_font: font_artist, text_color: 0x9FC7BE }
|
||||
- label: { id: th_target, align: CENTER, y: -14, text: "--", text_font: montserrat_48, text_color: 0xFFFFFF }
|
||||
- label: { id: th_cur, align: CENTER, y: 30, text: "now --", text_font: font_med, text_color: 0x9FC7BE }
|
||||
- label: { id: th_hum, align: CENTER, y: 58, text: "", text_font: font_artist, text_color: 0x3FA9D6, hidden: true }
|
||||
- label: { id: th_mode, align: CENTER, y: 92, text: "", text_font: font_artist, text_color: 0xCFEAE4 }
|
||||
@@ -0,0 +1,17 @@
|
||||
# Timer carousel-screen toggle.
|
||||
#
|
||||
# Unlike the other screens, the timer cannot be a full standalone package: its logic is
|
||||
# wired into core components (the voice_assistant on_timer_* triggers, the shared alarm
|
||||
# overlay, timer_render, the home-screen badge, the knob). So the timer FEATURE always
|
||||
# lives in base/core.yaml. This tiny package only adds the timer SCREEN to the swipe
|
||||
# carousel (base id 2).
|
||||
#
|
||||
# Remove this file from your config's package "files:" list to hide the timer screen from
|
||||
# the carousel. Voice timers ("set a 5 minute timer"), the alarm and the home-screen
|
||||
# countdown badge keep working - you just can't swipe to the on-device timer screen.
|
||||
|
||||
esphome:
|
||||
on_boot:
|
||||
- priority: 680
|
||||
then:
|
||||
- lambda: 'id(g_present)[2] = true;' # add the timer screen to the carousel
|
||||
@@ -0,0 +1,37 @@
|
||||
# Home Assistant helper for the device's Weather screen (base/screens/weather.yaml).
|
||||
#
|
||||
# HA no longer exposes a weather entity's multi-day forecast as an attribute, so this
|
||||
# template calls weather.get_forecasts and publishes a single compact sensor the device
|
||||
# reads. State format: "condition,high,low,DayName|..." (one segment per day, up to 7).
|
||||
#
|
||||
# 1. Change weather.forecast_dom below to your weather entity (3 places).
|
||||
# 2. Put this in HA configuration.yaml (or a packages/ file), then restart HA / reload
|
||||
# template entities. It creates sensor.va_weekly_forecast.
|
||||
# 3. On the device, ${forecast_entity} defaults to sensor.va_weekly_forecast - override
|
||||
# the substitution if you name it differently.
|
||||
|
||||
template:
|
||||
- trigger:
|
||||
- platform: homeassistant
|
||||
event: start
|
||||
- platform: time_pattern
|
||||
minutes: "/30"
|
||||
action:
|
||||
- service: weather.get_forecasts
|
||||
target:
|
||||
entity_id: weather.forecast_dom
|
||||
data:
|
||||
type: daily
|
||||
response_variable: fc
|
||||
sensor:
|
||||
- name: "VA weekly forecast"
|
||||
unique_id: va_weekly_forecast
|
||||
state: >
|
||||
{% set ns = namespace(parts=[]) %}
|
||||
{% for d in fc['weather.forecast_dom'].forecast[:7] %}
|
||||
{% set hi = (d.temperature | round(0)) if d.temperature is not none else 0 %}
|
||||
{% set lo = (d.templow | round(0)) if (d.templow is defined and d.templow is not none) else hi %}
|
||||
{% set nm = as_timestamp(d.datetime) | timestamp_custom('%a', true) %}
|
||||
{% set ns.parts = ns.parts + [d.condition ~ ',' ~ hi ~ ',' ~ lo ~ ',' ~ nm] %}
|
||||
{% endfor %}
|
||||
{{ ns.parts | join('|') }}
|
||||
@@ -0,0 +1,541 @@
|
||||
# Weather - carousel screen (base id 5). GENERATED by scripts/gen_weather.py.
|
||||
# A dial of forecast days around the rim (knob scrolls), animated condition icon + colour-coded
|
||||
# temperature in the centre, a glow that slides to the selected day. Forecast comes from a Home
|
||||
# Assistant helper sensor (see base/screens/weather.ha-helper.yaml). Remove: drop from `files:`.
|
||||
|
||||
esphome:
|
||||
on_boot:
|
||||
- priority: 655
|
||||
then:
|
||||
- lambda: 'id(g_present)[5] = true;'
|
||||
- priority: 650
|
||||
then:
|
||||
- lambda: |-
|
||||
int g = id(g_wgrp_n)++;
|
||||
id(g_wgrp_labels) += (g ? "\n" : ""); id(g_wgrp_labels) += "Weather";
|
||||
int o = id(g_wopt_n)++;
|
||||
id(g_wopt_group)[o] = g; id(g_wopt_label)[o] = "Fahrenheit"; id(g_wopt_kind)[o] = 0; id(g_wopt_ptr)[o] = &id(g_wx_fahrenheit);
|
||||
|
||||
globals:
|
||||
# --- forecast data ---
|
||||
- id: g_wx_fahrenheit
|
||||
type: bool
|
||||
restore_value: yes
|
||||
initial_value: 'false'
|
||||
- id: g_wx_now
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '-127'
|
||||
- id: g_wx_state_cur
|
||||
type: std::string
|
||||
restore_value: no
|
||||
- id: g_wx_cond
|
||||
type: std::string[7]
|
||||
restore_value: no
|
||||
- id: g_wx_icon
|
||||
type: std::string[7]
|
||||
restore_value: no
|
||||
- id: g_wx_name
|
||||
type: std::string[7]
|
||||
restore_value: no
|
||||
- id: g_wx_hi
|
||||
type: int[7]
|
||||
restore_value: no
|
||||
- id: g_wx_lo
|
||||
type: int[7]
|
||||
restore_value: no
|
||||
- id: g_wx_n
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
- id: g_wx_day
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
# --- render cache ---
|
||||
- id: g_wx_sel
|
||||
type: std::string
|
||||
restore_value: no
|
||||
- id: g_wx_tc
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '-127'
|
||||
- id: g_wx_lastcond
|
||||
type: std::string
|
||||
restore_value: no
|
||||
- id: g_wx_ikey
|
||||
type: std::string
|
||||
restore_value: no
|
||||
- id: g_wx_gpos
|
||||
type: float
|
||||
restore_value: no
|
||||
initial_value: '0.0'
|
||||
|
||||
font:
|
||||
- file: "https://github.com/Templarian/MaterialDesign-Webfont/raw/v7.4.47/fonts/materialdesignicons-webfont.ttf"
|
||||
id: icons_wx
|
||||
size: 30
|
||||
bpp: 4
|
||||
glyphs: ["", "", "", "", "", "", "", "", "", ""]
|
||||
|
||||
image:
|
||||
- id: img_wx_sun0
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/sun0.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_sun1
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/sun1.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_sun2
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/sun2.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_sun3
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/sun3.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_sun4
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/sun4.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_sun5
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/sun5.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_sun6
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/sun6.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_sun7
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/sun7.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_partly0
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/partly0.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_partly1
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/partly1.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_partly2
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/partly2.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_partly3
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/partly3.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_partly4
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/partly4.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_partly5
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/partly5.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_partly6
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/partly6.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_partly7
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/partly7.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_moon0
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/moon0.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_moon1
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/moon1.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_moon2
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/moon2.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_moon3
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/moon3.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_moon4
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/moon4.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_moon5
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/moon5.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_moon6
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/moon6.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_moon7
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/moon7.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_cloud0
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/cloud0.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_cloud1
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/cloud1.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_cloud2
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/cloud2.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_cloud3
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/cloud3.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_cloud4
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/cloud4.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_cloud5
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/cloud5.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_cloud6
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/cloud6.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_cloud7
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/cloud7.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_rain0
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/rain0.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_rain1
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/rain1.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_rain2
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/rain2.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_rain3
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/rain3.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_rain4
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/rain4.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_rain5
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/rain5.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_rain6
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/rain6.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_rain7
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/rain7.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_storm0
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/storm0.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_storm1
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/storm1.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_storm2
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/storm2.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_storm3
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/storm3.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_storm4
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/storm4.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_storm5
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/storm5.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_storm6
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/storm6.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_storm7
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/storm7.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_snow0
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/snow0.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_snow1
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/snow1.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_snow2
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/snow2.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_snow3
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/snow3.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_snow4
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/snow4.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_snow5
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/snow5.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_snow6
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/snow6.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- id: img_wx_snow7
|
||||
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/snow7.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
|
||||
script:
|
||||
- id: wx_render
|
||||
then:
|
||||
- lambda: |-
|
||||
int d = id(g_wx_day);
|
||||
bool has = (id(g_wx_n) > 0 && d < id(g_wx_n));
|
||||
id(g_wx_sel) = has ? id(g_wx_cond)[d] : id(g_wx_state_cur);
|
||||
id(g_wx_tc) = has ? id(g_wx_hi)[d] : id(g_wx_now);
|
||||
- lvgl.label.update:
|
||||
id: wx_day
|
||||
text: !lambda |-
|
||||
int d = id(g_wx_day);
|
||||
if (id(g_wx_n) <= 0 || d >= id(g_wx_n)) return std::string("TODAY");
|
||||
std::string s = id(g_wx_name)[d];
|
||||
if (s == "Mon") return std::string("MONDAY");
|
||||
if (s == "Tue") return std::string("TUESDAY");
|
||||
if (s == "Wed") return std::string("WEDNESDAY");
|
||||
if (s == "Thu") return std::string("THURSDAY");
|
||||
if (s == "Fri") return std::string("FRIDAY");
|
||||
if (s == "Sat") return std::string("SATURDAY");
|
||||
if (s == "Sun") return std::string("SUNDAY");
|
||||
return s;
|
||||
- lvgl.label.update:
|
||||
id: wx_htemp
|
||||
text: !lambda |-
|
||||
int d = id(g_wx_day);
|
||||
bool f = id(g_wx_fahrenheit);
|
||||
char u = f ? 'F' : 'C';
|
||||
if (id(g_wx_n) <= 0 || d >= id(g_wx_n)) {
|
||||
if (id(g_wx_now) > -100) { int n = id(g_wx_now); if (f) n = n*9/5+32; char b[12]; snprintf(b,sizeof(b),"%d°%c",n,u); return std::string(b); }
|
||||
return std::string("--");
|
||||
}
|
||||
int hi = id(g_wx_hi)[d], lo = id(g_wx_lo)[d];
|
||||
if (f) { hi = hi*9/5+32; lo = lo*9/5+32; }
|
||||
char b[24]; snprintf(b,sizeof(b),"%d° / %d°",hi,lo); return std::string(b);
|
||||
- lvgl.label.update:
|
||||
id: wx_cond
|
||||
text: !lambda |-
|
||||
if (id(g_wx_n) <= 0) return std::string("Add the HA forecast helper");
|
||||
std::string s = id(g_wx_sel);
|
||||
if (s == "sunny") return std::string("Sunny");
|
||||
if (s == "clear-night") return std::string("Clear");
|
||||
if (s == "partlycloudy") return std::string("Partly Cloudy");
|
||||
if (s == "cloudy") return std::string("Cloudy");
|
||||
if (s == "fog") return std::string("Fog");
|
||||
if (s == "rainy") return std::string("Rainy");
|
||||
if (s == "pouring") return std::string("Heavy Rain");
|
||||
if (s == "lightning" || s == "lightning-rainy") return std::string("Storm");
|
||||
if (s == "snowy" || s == "snowy-rainy") return std::string("Snow");
|
||||
if (s == "hail") return std::string("Hail");
|
||||
if (s == "windy" || s == "windy-variant") return std::string("Windy");
|
||||
if (s == "exceptional") return std::string("Severe");
|
||||
return s;
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_wx_sel) != id(g_wx_lastcond);' }
|
||||
then:
|
||||
- lambda: |-
|
||||
id(g_wx_lastcond) = id(g_wx_sel);
|
||||
std::string s = id(g_wx_sel), k;
|
||||
if (s == "sunny") k = "sun";
|
||||
else if (s == "clear-night") k = "moon";
|
||||
else if (s == "partlycloudy") k = "partly";
|
||||
else if (s == "rainy" || s == "pouring") k = "rain";
|
||||
else if (s == "lightning" || s == "lightning-rainy") k = "storm";
|
||||
else if (s == "snowy" || s == "snowy-rainy" || s == "hail") k = "snow";
|
||||
else k = "cloud";
|
||||
id(g_wx_ikey) = k;
|
||||
- lvgl.animimg.stop: wx_a_sun
|
||||
- lvgl.animimg.stop: wx_a_partly
|
||||
- lvgl.animimg.stop: wx_a_moon
|
||||
- lvgl.animimg.stop: wx_a_cloud
|
||||
- lvgl.animimg.stop: wx_a_rain
|
||||
- lvgl.animimg.stop: wx_a_storm
|
||||
- lvgl.animimg.stop: wx_a_snow
|
||||
- lvgl.widget.hide: wx_a_sun
|
||||
- lvgl.widget.hide: wx_a_partly
|
||||
- lvgl.widget.hide: wx_a_moon
|
||||
- lvgl.widget.hide: wx_a_cloud
|
||||
- lvgl.widget.hide: wx_a_rain
|
||||
- lvgl.widget.hide: wx_a_storm
|
||||
- lvgl.widget.hide: wx_a_snow
|
||||
- if: { condition: { lambda: 'return id(g_wx_ikey) == "sun";' }, then: [ lvgl.widget.show: wx_a_sun, lvgl.animimg.start: wx_a_sun ] }
|
||||
- if: { condition: { lambda: 'return id(g_wx_ikey) == "partly";' }, then: [ lvgl.widget.show: wx_a_partly, lvgl.animimg.start: wx_a_partly ] }
|
||||
- if: { condition: { lambda: 'return id(g_wx_ikey) == "moon";' }, then: [ lvgl.widget.show: wx_a_moon, lvgl.animimg.start: wx_a_moon ] }
|
||||
- if: { condition: { lambda: 'return id(g_wx_ikey) == "cloud";' }, then: [ lvgl.widget.show: wx_a_cloud, lvgl.animimg.start: wx_a_cloud ] }
|
||||
- if: { condition: { lambda: 'return id(g_wx_ikey) == "rain";' }, then: [ lvgl.widget.show: wx_a_rain, lvgl.animimg.start: wx_a_rain ] }
|
||||
- if: { condition: { lambda: 'return id(g_wx_ikey) == "storm";' }, then: [ lvgl.widget.show: wx_a_storm, lvgl.animimg.start: wx_a_storm ] }
|
||||
- if: { condition: { lambda: 'return id(g_wx_ikey) == "snow";' }, then: [ lvgl.widget.show: wx_a_snow, lvgl.animimg.start: wx_a_snow ] }
|
||||
- lvgl.widget.update: { id: wx_glow, hidden: !lambda 'return id(g_wx_n) <= 0;' }
|
||||
- lvgl.widget.update: { id: wx_d0, hidden: !lambda 'return 0 >= id(g_wx_n);', x: !lambda 'int n=id(g_wx_n); if(n<1)n=1; return (int) lroundf(140.0f * sinf(0 * 6.2831853f / n));', y: !lambda 'int n=id(g_wx_n); if(n<1)n=1; return (int) lroundf(-140.0f * cosf(0 * 6.2831853f / n));' }
|
||||
- lvgl.label.update: { id: wx_d0, text: !lambda 'return id(g_wx_n) > 0 ? id(g_wx_icon)[0] : std::string("");' }
|
||||
- lvgl.widget.update: { id: wx_d1, hidden: !lambda 'return 1 >= id(g_wx_n);', x: !lambda 'int n=id(g_wx_n); if(n<1)n=1; return (int) lroundf(140.0f * sinf(1 * 6.2831853f / n));', y: !lambda 'int n=id(g_wx_n); if(n<1)n=1; return (int) lroundf(-140.0f * cosf(1 * 6.2831853f / n));' }
|
||||
- lvgl.label.update: { id: wx_d1, text: !lambda 'return id(g_wx_n) > 1 ? id(g_wx_icon)[1] : std::string("");' }
|
||||
- lvgl.widget.update: { id: wx_d2, hidden: !lambda 'return 2 >= id(g_wx_n);', x: !lambda 'int n=id(g_wx_n); if(n<1)n=1; return (int) lroundf(140.0f * sinf(2 * 6.2831853f / n));', y: !lambda 'int n=id(g_wx_n); if(n<1)n=1; return (int) lroundf(-140.0f * cosf(2 * 6.2831853f / n));' }
|
||||
- lvgl.label.update: { id: wx_d2, text: !lambda 'return id(g_wx_n) > 2 ? id(g_wx_icon)[2] : std::string("");' }
|
||||
- lvgl.widget.update: { id: wx_d3, hidden: !lambda 'return 3 >= id(g_wx_n);', x: !lambda 'int n=id(g_wx_n); if(n<1)n=1; return (int) lroundf(140.0f * sinf(3 * 6.2831853f / n));', y: !lambda 'int n=id(g_wx_n); if(n<1)n=1; return (int) lroundf(-140.0f * cosf(3 * 6.2831853f / n));' }
|
||||
- lvgl.label.update: { id: wx_d3, text: !lambda 'return id(g_wx_n) > 3 ? id(g_wx_icon)[3] : std::string("");' }
|
||||
- lvgl.widget.update: { id: wx_d4, hidden: !lambda 'return 4 >= id(g_wx_n);', x: !lambda 'int n=id(g_wx_n); if(n<1)n=1; return (int) lroundf(140.0f * sinf(4 * 6.2831853f / n));', y: !lambda 'int n=id(g_wx_n); if(n<1)n=1; return (int) lroundf(-140.0f * cosf(4 * 6.2831853f / n));' }
|
||||
- lvgl.label.update: { id: wx_d4, text: !lambda 'return id(g_wx_n) > 4 ? id(g_wx_icon)[4] : std::string("");' }
|
||||
- lvgl.widget.update: { id: wx_d5, hidden: !lambda 'return 5 >= id(g_wx_n);', x: !lambda 'int n=id(g_wx_n); if(n<1)n=1; return (int) lroundf(140.0f * sinf(5 * 6.2831853f / n));', y: !lambda 'int n=id(g_wx_n); if(n<1)n=1; return (int) lroundf(-140.0f * cosf(5 * 6.2831853f / n));' }
|
||||
- lvgl.label.update: { id: wx_d5, text: !lambda 'return id(g_wx_n) > 5 ? id(g_wx_icon)[5] : std::string("");' }
|
||||
- lvgl.widget.update: { id: wx_d6, hidden: !lambda 'return 6 >= id(g_wx_n);', x: !lambda 'int n=id(g_wx_n); if(n<1)n=1; return (int) lroundf(140.0f * sinf(6 * 6.2831853f / n));', y: !lambda 'int n=id(g_wx_n); if(n<1)n=1; return (int) lroundf(-140.0f * cosf(6 * 6.2831853f / n));' }
|
||||
- lvgl.label.update: { id: wx_d6, text: !lambda 'return id(g_wx_n) > 6 ? id(g_wx_icon)[6] : std::string("");' }
|
||||
- lvgl.label.update: { id: wx_htemp, text_color: 0xFFFFFF }
|
||||
- if: { condition: { lambda: 'return id(g_wx_tc) > -100 && id(g_wx_tc) <= 0;' }, then: [ lvgl.label.update: { id: wx_htemp, text_color: 0x7FB2FF } ] }
|
||||
- if: { condition: { lambda: 'return id(g_wx_tc) > 0 && id(g_wx_tc) < 10;' }, then: [ lvgl.label.update: { id: wx_htemp, text_color: 0x6FD0E8 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_wx_tc) >= 10 && id(g_wx_tc) < 18;' }, then: [ lvgl.label.update: { id: wx_htemp, text_color: 0x7FE6B0 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_wx_tc) >= 18 && id(g_wx_tc) < 25;' }, then: [ lvgl.label.update: { id: wx_htemp, text_color: 0xFFD061 } ] }
|
||||
- if: { condition: { lambda: 'return id(g_wx_tc) >= 25;' }, then: [ lvgl.label.update: { id: wx_htemp, text_color: 0xFF7A4D } ] }
|
||||
|
||||
sensor:
|
||||
- platform: homeassistant
|
||||
id: wx_now_temp
|
||||
entity_id: ${weather_entity}
|
||||
attribute: temperature
|
||||
on_value:
|
||||
- lambda: 'id(g_wx_now) = (int) roundf(x);'
|
||||
- script.execute: wx_render
|
||||
|
||||
text_sensor:
|
||||
- platform: homeassistant
|
||||
id: wx_now_state
|
||||
entity_id: ${weather_entity}
|
||||
on_value:
|
||||
- lambda: 'id(g_wx_state_cur) = x;'
|
||||
- script.execute: wx_render
|
||||
- platform: homeassistant
|
||||
id: wx_week
|
||||
entity_id: ${forecast_entity}
|
||||
on_value:
|
||||
- lambda: |-
|
||||
auto glyph = [](const std::string &s) -> std::string {
|
||||
if (s == "sunny") return std::string("");
|
||||
if (s == "clear-night") return std::string("");
|
||||
if (s == "partlycloudy") return std::string("");
|
||||
if (s == "fog") return std::string("");
|
||||
if (s == "windy" || s == "windy-variant") return std::string("");
|
||||
if (s == "lightning" || s == "lightning-rainy") return std::string("");
|
||||
if (s == "pouring") return std::string("");
|
||||
if (s == "rainy") return std::string("");
|
||||
if (s == "snowy" || s == "snowy-rainy" || s == "hail") return std::string("");
|
||||
return std::string("");
|
||||
};
|
||||
std::string s = x;
|
||||
for (int i = 0; i < 7; i++) { id(g_wx_icon)[i] = ""; id(g_wx_cond)[i] = ""; }
|
||||
int n = 0; size_t pos = 0;
|
||||
while (n < 7) {
|
||||
size_t bar = s.find('|', pos);
|
||||
std::string seg = (bar == std::string::npos) ? s.substr(pos) : s.substr(pos, bar - pos);
|
||||
size_t c1 = seg.find(',');
|
||||
if (c1 != std::string::npos) {
|
||||
size_t c2 = seg.find(',', c1 + 1);
|
||||
size_t c3 = (c2 != std::string::npos) ? seg.find(',', c2 + 1) : std::string::npos;
|
||||
if (c2 != std::string::npos && c3 != std::string::npos) {
|
||||
std::string cond = seg.substr(0, c1);
|
||||
id(g_wx_cond)[n] = cond;
|
||||
id(g_wx_icon)[n] = glyph(cond);
|
||||
id(g_wx_hi)[n] = atoi(seg.substr(c1 + 1, c2 - c1 - 1).c_str());
|
||||
id(g_wx_lo)[n] = atoi(seg.substr(c2 + 1, c3 - c2 - 1).c_str());
|
||||
id(g_wx_name)[n] = seg.substr(c3 + 1);
|
||||
n++;
|
||||
}
|
||||
}
|
||||
if (bar == std::string::npos) break;
|
||||
pos = bar + 1;
|
||||
}
|
||||
id(g_wx_n) = n;
|
||||
if (id(g_wx_day) >= n) id(g_wx_day) = 0;
|
||||
- script.execute: wx_render
|
||||
|
||||
interval:
|
||||
- interval: 50ms
|
||||
then:
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_nav_req) && id(g_base)==5;' }
|
||||
then:
|
||||
- lambda: 'id(g_nav_req) = false; id(g_knob_capture) = true; id(g_wx_day) = 0; id(g_wx_gpos) = 0.0f; id(g_wx_lastcond) = "";'
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==0;' }, then: [lvgl.page.show: { id: page_weather, animation: move_left, time: 250ms }] }
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==1;' }, then: [lvgl.page.show: { id: page_weather, animation: move_right, time: 250ms }] }
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==2;' }, then: [lvgl.page.show: { id: page_weather, animation: move_top, time: 250ms }] }
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==3;' }, then: [lvgl.page.show: { id: page_weather, animation: move_bottom, time: 250ms }] }
|
||||
- script.execute: wx_render
|
||||
- interval: 120ms
|
||||
then:
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_base)==5 && id(g_knob_delta) != 0;' }
|
||||
then:
|
||||
- lambda: |-
|
||||
id(g_wx_day) += id(g_knob_delta); id(g_knob_delta) = 0;
|
||||
int mx = (id(g_wx_n) > 0) ? id(g_wx_n) - 1 : 0;
|
||||
if (id(g_wx_day) < 0) id(g_wx_day) = 0;
|
||||
if (id(g_wx_day) > mx) id(g_wx_day) = mx;
|
||||
- script.execute: wx_render
|
||||
- interval: 30ms
|
||||
then:
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_base)==5;' }
|
||||
then:
|
||||
- lambda: |-
|
||||
float p = id(g_wx_gpos), tgt = (float) id(g_wx_day);
|
||||
if (fabsf(tgt - p) > 0.001f) { // only while actually sliding
|
||||
int n = id(g_wx_n); if (n < 1) n = 1;
|
||||
p += (tgt - p) * 0.35f;
|
||||
if (fabsf(tgt - p) < 0.02f) p = tgt;
|
||||
id(g_wx_gpos) = p;
|
||||
float rx = 140.0f * sinf(p * 6.2831853f / n);
|
||||
float ry = -140.0f * cosf(p * 6.2831853f / n);
|
||||
lv_obj_set_pos(id(wx_glow), 180 + (int) lroundf(rx) - 28, 180 + (int) lroundf(ry) - 28);
|
||||
}
|
||||
|
||||
lvgl:
|
||||
pages:
|
||||
- id: page_weather
|
||||
bg_color: 0x0B1020
|
||||
widgets:
|
||||
- obj: { id: wx_bg, align: CENTER, x: 0, y: 0, width: 360, height: 360, radius: 0, bg_color: 0x1C2838, bg_opa: COVER, border_width: 0 }
|
||||
- obj: { id: wx_ring_out, align: CENTER, width: 348, height: 348, radius: 174, bg_opa: TRANSP, border_width: 2, border_color: 0x33455C, border_opa: 55%, clickable: false, scrollable: false }
|
||||
- obj: { id: wx_glow, x: 152, y: 12, width: 56, height: 56, radius: 28, bg_color: 0xCAD6E8, bg_opa: 30%, border_width: 0, hidden: true }
|
||||
- label: { id: wx_d0, align: CENTER, x: 0, y: 0, text: "", text_font: icons_wx, text_color: 0xDCE6F2, hidden: true }
|
||||
- label: { id: wx_d1, align: CENTER, x: 0, y: 0, text: "", text_font: icons_wx, text_color: 0xDCE6F2, hidden: true }
|
||||
- label: { id: wx_d2, align: CENTER, x: 0, y: 0, text: "", text_font: icons_wx, text_color: 0xDCE6F2, hidden: true }
|
||||
- label: { id: wx_d3, align: CENTER, x: 0, y: 0, text: "", text_font: icons_wx, text_color: 0xDCE6F2, hidden: true }
|
||||
- label: { id: wx_d4, align: CENTER, x: 0, y: 0, text: "", text_font: icons_wx, text_color: 0xDCE6F2, hidden: true }
|
||||
- label: { id: wx_d5, align: CENTER, x: 0, y: 0, text: "", text_font: icons_wx, text_color: 0xDCE6F2, hidden: true }
|
||||
- label: { id: wx_d6, align: CENTER, x: 0, y: 0, text: "", text_font: icons_wx, text_color: 0xDCE6F2, hidden: true }
|
||||
- animimg: { id: wx_a_sun, align: CENTER, x: 0, y: -52, duration: 1400ms, repeat_count: 65535, auto_start: false, hidden: true, src: [ img_wx_sun0, img_wx_sun1, img_wx_sun2, img_wx_sun3, img_wx_sun4, img_wx_sun5, img_wx_sun6, img_wx_sun7 ] }
|
||||
- animimg: { id: wx_a_partly, align: CENTER, x: 0, y: -52, duration: 1400ms, repeat_count: 65535, auto_start: false, hidden: true, src: [ img_wx_partly0, img_wx_partly1, img_wx_partly2, img_wx_partly3, img_wx_partly4, img_wx_partly5, img_wx_partly6, img_wx_partly7 ] }
|
||||
- animimg: { id: wx_a_moon, align: CENTER, x: 0, y: -52, duration: 1400ms, repeat_count: 65535, auto_start: false, hidden: true, src: [ img_wx_moon0, img_wx_moon1, img_wx_moon2, img_wx_moon3, img_wx_moon4, img_wx_moon5, img_wx_moon6, img_wx_moon7 ] }
|
||||
- animimg: { id: wx_a_cloud, align: CENTER, x: 0, y: -52, duration: 1400ms, repeat_count: 65535, auto_start: false, hidden: true, src: [ img_wx_cloud0, img_wx_cloud1, img_wx_cloud2, img_wx_cloud3, img_wx_cloud4, img_wx_cloud5, img_wx_cloud6, img_wx_cloud7 ] }
|
||||
- animimg: { id: wx_a_rain, align: CENTER, x: 0, y: -52, duration: 1400ms, repeat_count: 65535, auto_start: false, hidden: true, src: [ img_wx_rain0, img_wx_rain1, img_wx_rain2, img_wx_rain3, img_wx_rain4, img_wx_rain5, img_wx_rain6, img_wx_rain7 ] }
|
||||
- animimg: { id: wx_a_storm, align: CENTER, x: 0, y: -52, duration: 1400ms, repeat_count: 65535, auto_start: false, hidden: true, src: [ img_wx_storm0, img_wx_storm1, img_wx_storm2, img_wx_storm3, img_wx_storm4, img_wx_storm5, img_wx_storm6, img_wx_storm7 ] }
|
||||
- animimg: { id: wx_a_snow, align: CENTER, x: 0, y: -52, duration: 1400ms, repeat_count: 65535, auto_start: false, hidden: true, src: [ img_wx_snow0, img_wx_snow1, img_wx_snow2, img_wx_snow3, img_wx_snow4, img_wx_snow5, img_wx_snow6, img_wx_snow7 ] }
|
||||
- label: { id: wx_day, align: CENTER, x: 0, y: -10, text: "", text_font: font_artist, text_color: 0x9AA6B2 }
|
||||
- label: { id: wx_htemp, align: CENTER, x: 0, y: 24, text: "--", text_font: montserrat_34, text_color: 0xFFFFFF }
|
||||
- label: { id: wx_cond, align: CENTER, x: 0, y: 62, text: "", text_font: font_med, text_color: 0xCFE0F2 }
|
||||
@@ -0,0 +1,118 @@
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
# DEMO WATCHFACE - a minimal, heavily-commented template. Copy this file to
|
||||
# base/watchfaces/<your-name>.yaml, change the 4 spots marked >>> EDIT <<< and
|
||||
# you have your own home-screen look.
|
||||
#
|
||||
# How watchfaces work (same idea as the carousel screens):
|
||||
# - The home base (the clock area) can show one of several "faces".
|
||||
# - "Classic" is built into core and is always the fallback (face id 0).
|
||||
# - Every OTHER face is an optional file like this one, switched on by adding it
|
||||
# to the `files:` list in your thin config, and chosen at runtime in
|
||||
# Settings > Home > Watchface (turn the knob to preview, tap to keep).
|
||||
# - Core can't reference a page that might not be compiled in, so each face
|
||||
# SHOWS ITSELF: core just raises a flag (g_wf_req) and the active face's own
|
||||
# interval calls lvgl.page.show. Same trick the screens use (g_nav_req).
|
||||
#
|
||||
# Everything you can read from core (no extra entities needed):
|
||||
# id(ha_time).now() - current time (ESPTime) -> strftime("%H"), etc.
|
||||
# id(wx_temp).state - outdoor temperature (float) + .has_state()
|
||||
# id(room_temp).state - room temperature (float)
|
||||
# id(room_hum).state - room humidity (float)
|
||||
# id(g_nf_glyph) - weather icon glyph (std::string, MDI in font icons34)
|
||||
# id(g_nf_cond) - weather condition text, UPPERCASE
|
||||
# id(g_batt) / id(g_charging) - battery % (int, -1 = unknown) / charging (bool)
|
||||
# id(g_show_weather) / id(g_show_climate) - the Display toggles (bool)
|
||||
# Fonts already provided by core: montserrat_18/48, font_med, font_artist,
|
||||
# font_sel, icons18, icons34. Define your own under `font:` if you want more.
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
# ── 1. REGISTER the face at boot ──────────────────────────────────────────────
|
||||
# Pick a UNIQUE id (0 = Classic, 1 = Neon are taken; use 2, 3, ...). The name is
|
||||
# what shows in the picker. Priority 655 runs after core but before the boot
|
||||
# finalize that validates the saved choice.
|
||||
esphome:
|
||||
on_boot:
|
||||
- priority: 655
|
||||
then:
|
||||
- lambda: 'id(g_wf_present)[2] = true; id(g_wf_name)[2] = "Demo";' # >>> EDIT <<< id + name
|
||||
|
||||
# ── 2. (optional) your own fonts ──────────────────────────────────────────────
|
||||
# This demo reuses core's fonts, so nothing here. Add a `font:` block if needed.
|
||||
|
||||
# ── 3. THE PAGE ───────────────────────────────────────────────────────────────
|
||||
# A full-screen page. Give it a unique id (page_home_<name>) and unique widget ids.
|
||||
lvgl:
|
||||
pages:
|
||||
- id: page_home_demo # >>> EDIT <<< unique page id
|
||||
bg_color: 0x000000
|
||||
widgets:
|
||||
- label: # big centered clock
|
||||
id: dwf_time
|
||||
align: CENTER
|
||||
y: -16
|
||||
text: "00:00"
|
||||
text_font: montserrat_48
|
||||
text_color: 0xFFFFFF
|
||||
- label: # small date under it
|
||||
id: dwf_date
|
||||
align: CENTER
|
||||
y: 30
|
||||
text: ""
|
||||
text_font: font_med
|
||||
text_color: 0x6f9a92
|
||||
|
||||
# ── 4. INTERVALS: self-show + refresh ─────────────────────────────────────────
|
||||
interval:
|
||||
# Self-show: core raises g_wf_req when it wants the home base shown. If we are
|
||||
# the active face, show OUR page (and render once so it isn't blank), then clear
|
||||
# the flag. Keep the id check (== 2) matching the one you registered above.
|
||||
- interval: 50ms
|
||||
then:
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_wf_req) && id(g_watchface) == 2;' } # >>> EDIT <<< id
|
||||
then:
|
||||
- lambda: 'id(g_wf_req) = false;'
|
||||
# slide in matching the carousel direction (like Classic / the screens) - no laggy fade
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==0;' }, then: [lvgl.page.show: { id: page_home_demo, animation: move_left, time: 250ms }] }
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==1;' }, then: [lvgl.page.show: { id: page_home_demo, animation: move_right, time: 250ms }] }
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==2;' }, then: [lvgl.page.show: { id: page_home_demo, animation: move_top, time: 250ms }] }
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==3;' }, then: [lvgl.page.show: { id: page_home_demo, animation: move_bottom, time: 250ms }] }
|
||||
- script.execute: dwf_render
|
||||
# Refresh once a second while we are the active home face.
|
||||
- interval: 1s
|
||||
then:
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_watchface) == 2 && id(g_base) == 0;' } # >>> EDIT <<< id
|
||||
then: [ script.execute: dwf_render ]
|
||||
|
||||
# ── 5. RENDER: fill your widgets from core's data ─────────────────────────────
|
||||
script:
|
||||
- id: dwf_render # >>> EDIT <<< unique script id
|
||||
then:
|
||||
# Time - HH:MM (24h). Use "%I:%M %p" for 12-hour.
|
||||
- lvgl.label.update:
|
||||
id: dwf_time
|
||||
text: !lambda 'return id(ha_time).now().is_valid() ? id(ha_time).now().strftime("%H:%M") : std::string("00:00");'
|
||||
# Date - e.g. "Mon 19 Jun"
|
||||
- lvgl.label.update:
|
||||
id: dwf_date
|
||||
text: !lambda |-
|
||||
auto t = id(ha_time).now();
|
||||
if (!t.is_valid()) return std::string("");
|
||||
static const char* d[8] = {"","Sun","Mon","Tue","Wed","Thu","Fri","Sat"};
|
||||
std::string day = (t.day_of_week >= 1 && t.day_of_week <= 7) ? std::string(d[t.day_of_week]) : std::string("");
|
||||
return day + t.strftime(" %d %b");
|
||||
# ── Want more? Uncomment and add widgets above, then fill them here. ──
|
||||
# Example: show room temperature (respecting the "Show climate" toggle)
|
||||
# - lvgl.label.update:
|
||||
# id: dwf_temp
|
||||
# text: !lambda |-
|
||||
# if (!id(g_show_climate) || !id(room_temp).has_state()) return std::string("");
|
||||
# char b[12]; snprintf(b, sizeof(b), "%.1f\xC2\xB0", id(room_temp).state); return std::string(b);
|
||||
# Example: weather icon (font icons34) + outdoor temp
|
||||
# - lvgl.label.update: { id: dwf_wx, text: !lambda 'return id(g_show_weather) ? id(g_nf_glyph) : std::string("");' }
|
||||
|
||||
# ── 6. TURN IT ON ─────────────────────────────────────────────────────────────
|
||||
# Add this file to the `files:` list in your thin config:
|
||||
# - base/watchfaces/demo.yaml
|
||||
# then rebuild and pick "Demo" in Settings > Home > Watchface.
|
||||
@@ -0,0 +1,191 @@
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
# MINECRAFT watchface (optional, id 3). A blocky scene with a day/night cycle:
|
||||
# sky + textured grass/dirt ground, the sun by day / moon + stars by night, pixel
|
||||
# flowers on the grass, a blocky pixel font for the time (classic drop shadow),
|
||||
# outdoor temp, and a Minecraft heart bar (HUD) for the battery.
|
||||
#
|
||||
# PERFORMANCE: the per-second tick is just a tiny lambda; the heavy redraws (the big
|
||||
# pixel digits, the day/night swap) only run when the minute actually changes or the
|
||||
# day/night state flips - so it stays light. No animated clouds.
|
||||
#
|
||||
# Assets (font + sprites) are fetched from this repo's /main/ raw GitHub URLs at compile time.
|
||||
# Watchface contract: see base/watchfaces/demo.yaml.
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
esphome:
|
||||
on_boot:
|
||||
- priority: 655
|
||||
then:
|
||||
- lambda: 'id(g_wf_present)[3] = true; id(g_wf_name)[3] = "Minecraft";'
|
||||
|
||||
globals:
|
||||
- { id: g_mc_last_min, type: int, restore_value: no, initial_value: '-1' }
|
||||
- { id: g_mc_last_night, type: int, restore_value: no, initial_value: '-1' }
|
||||
- { id: g_mc_upd_min, type: bool, restore_value: no, initial_value: 'false' }
|
||||
- { id: g_mc_upd_night, type: bool, restore_value: no, initial_value: 'false' }
|
||||
|
||||
font:
|
||||
- file: https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/fonts/pixel-font.ttf
|
||||
id: mc_font_big
|
||||
size: 78
|
||||
glyphs: "0123456789:"
|
||||
- file: https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/fonts/pixel-font.ttf
|
||||
id: mc_font_sm
|
||||
size: 26
|
||||
glyphs: " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" # date only (uppercased) - no GF_Latin_Core, avoids missing-glyph warnings
|
||||
|
||||
image:
|
||||
- { id: img_mc_ground, file: https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/minecraft/ground.png, type: RGB565 }
|
||||
- { id: img_mc_sun, file: https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/minecraft/sun.png, type: RGB565, transparency: alpha_channel }
|
||||
- { id: img_mc_moon, file: https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/minecraft/moon.png, type: RGB565, transparency: alpha_channel }
|
||||
- { id: img_mc_flower, file: https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/minecraft/flower.png, type: RGB565, transparency: alpha_channel }
|
||||
|
||||
lvgl:
|
||||
pages:
|
||||
- id: page_home_mc
|
||||
bg_color: 0x000000
|
||||
widgets:
|
||||
# sky (recoloured day/night) - full screen, behind everything
|
||||
- obj: { id: mc_sky, x: 0, y: 0, width: 360, height: 360, bg_color: 0x5A9BD4, bg_opa: COVER, radius: 0, border_width: 0, clickable: false, scrollable: false }
|
||||
# stars (night only)
|
||||
- obj: { id: mc_star1, x: 44, y: 64, width: 6, height: 6, bg_color: 0xEAECF5, bg_opa: COVER, radius: 0, border_width: 0, clickable: false, scrollable: false, hidden: true }
|
||||
- obj: { id: mc_star2, x: 120, y: 42, width: 6, height: 6, bg_color: 0xEAECF5, bg_opa: COVER, radius: 0, border_width: 0, clickable: false, scrollable: false, hidden: true }
|
||||
- obj: { id: mc_star3, x: 250, y: 48, width: 6, height: 6, bg_color: 0xEAECF5, bg_opa: COVER, radius: 0, border_width: 0, clickable: false, scrollable: false, hidden: true }
|
||||
- obj: { id: mc_star4, x: 70, y: 116, width: 5, height: 5, bg_color: 0xC9D0E4, bg_opa: COVER, radius: 0, border_width: 0, clickable: false, scrollable: false, hidden: true }
|
||||
- obj: { id: mc_star5, x: 300, y: 120, width: 5, height: 5, bg_color: 0xC9D0E4, bg_opa: COVER, radius: 0, border_width: 0, clickable: false, scrollable: false, hidden: true }
|
||||
# textured ground (flush to the bottom edge)
|
||||
- image: { id: mc_ground, src: img_mc_ground, align: BOTTOM_MID, y: 0 }
|
||||
# pixel flowers standing on the grass
|
||||
- image: { id: mc_flower1, src: img_mc_flower, x: 56, y: 200 }
|
||||
- image: { id: mc_flower2, src: img_mc_flower, x: 282, y: 206 }
|
||||
# sun by day / moon by night (top-right)
|
||||
- image: { id: mc_sun, src: img_mc_sun, align: TOP_RIGHT, x: -38, y: 38 }
|
||||
- image: { id: mc_moon, src: img_mc_moon, align: TOP_RIGHT, x: -38, y: 38, hidden: true }
|
||||
- label: { id: mc_sun_temp, align: TOP_RIGHT, x: -52, y: 100, text: "--°", text_font: font_artist, text_color: 0x16314A }
|
||||
# time: dark shadow behind + white in front (the Minecraft look)
|
||||
- label: { id: mc_time_sh, align: CENTER, x: 3, y: -41, text: "00:00", text_font: mc_font_big, text_color: 0x1A2A40 }
|
||||
- label: { id: mc_time, align: CENTER, x: 0, y: -44, text: "00:00", text_font: mc_font_big, text_color: 0xFFFFFF }
|
||||
- label: { id: mc_date, align: CENTER, x: 0, y: 14, text: "", text_font: mc_font_sm, text_color: 0xEAF2FB }
|
||||
# battery as a Minecraft heart bar (HUD), low: 5 dark "empty" slots + red fill on top
|
||||
- label: { id: mc_hearts_bg, align: BOTTOM_LEFT, x: 128, y: -16, text: "", text_font: icons18, text_color: 0x3A1614 }
|
||||
- label: { id: mc_hearts, align: BOTTOM_LEFT, x: 128, y: -16, text: "", text_font: icons18, text_color: 0xE2453F }
|
||||
# timer badge (active timer - minutes left)
|
||||
- label:
|
||||
id: mc_timer
|
||||
align: TOP_MID
|
||||
y: 6
|
||||
text: ""
|
||||
text_font: montserrat_18
|
||||
text_color: 0xFFFFFF
|
||||
bg_color: 0xFF3B30
|
||||
bg_opa: COVER
|
||||
radius: 12
|
||||
pad_left: 11
|
||||
pad_right: 11
|
||||
pad_top: 4
|
||||
pad_bottom: 4
|
||||
hidden: true
|
||||
|
||||
interval:
|
||||
- interval: 50ms # self-show when core requests the home base
|
||||
then:
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_wf_req) && id(g_watchface) == 3;' }
|
||||
then:
|
||||
- lambda: 'id(g_wf_req) = false; id(g_mc_last_min) = -1; id(g_mc_last_night) = -1;' # force a full redraw on entry
|
||||
# slide in matching the carousel direction (like Classic / the screens) - no laggy fade
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==0;' }, then: [lvgl.page.show: { id: page_home_mc, animation: move_left, time: 250ms }] }
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==1;' }, then: [lvgl.page.show: { id: page_home_mc, animation: move_right, time: 250ms }] }
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==2;' }, then: [lvgl.page.show: { id: page_home_mc, animation: move_top, time: 250ms }] }
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==3;' }, then: [lvgl.page.show: { id: page_home_mc, animation: move_bottom, time: 250ms }] }
|
||||
- script.execute: mc_render
|
||||
- interval: 1s # cheap tick - only redraws on a real change
|
||||
then:
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_watchface) == 3 && id(g_base) == 0;' }
|
||||
then: [ script.execute: mc_render ]
|
||||
|
||||
script:
|
||||
- id: mc_render
|
||||
then:
|
||||
# Decide what actually changed (so the second-by-second tick is nearly free)
|
||||
- lambda: |-
|
||||
id(g_mc_upd_min) = false; id(g_mc_upd_night) = false;
|
||||
auto t = id(ha_time).now();
|
||||
if (!t.is_valid()) return;
|
||||
int mn = t.hour * 60 + t.minute;
|
||||
if (mn != id(g_mc_last_min)) { id(g_mc_last_min) = mn; id(g_mc_upd_min) = true; }
|
||||
int night = (t.hour < 6 || t.hour >= 20) ? 1 : 0;
|
||||
if (night != id(g_mc_last_night)) { id(g_mc_last_night) = night; id(g_mc_upd_night) = true; }
|
||||
# Minute changed -> redraw time / date / temp / hearts
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_mc_upd_min);' }
|
||||
then:
|
||||
- lvgl.label.update: { id: mc_time, text: !lambda 'return id(ha_time).now().strftime("%H:%M");' }
|
||||
- lvgl.label.update: { id: mc_time_sh, text: !lambda 'return id(ha_time).now().strftime("%H:%M");' }
|
||||
- lvgl.label.update:
|
||||
id: mc_date
|
||||
text: !lambda |-
|
||||
auto t = id(ha_time).now();
|
||||
static const char* d[8] = {"","SUN","MON","TUE","WED","THU","FRI","SAT"};
|
||||
std::string day = (t.day_of_week >= 1 && t.day_of_week <= 7) ? std::string(d[t.day_of_week]) : std::string("");
|
||||
std::string s = day + t.strftime(" %d %b");
|
||||
for (auto &c : s) { if (c >= 'a' && c <= 'z') c -= 32; }
|
||||
return s;
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_show_weather);' }
|
||||
then:
|
||||
- lvgl.widget.show: mc_sun_temp
|
||||
- lvgl.label.update:
|
||||
id: mc_sun_temp
|
||||
text: !lambda |-
|
||||
if (!id(wx_temp).has_state()) return std::string("--°");
|
||||
char b[10]; snprintf(b, sizeof(b), "%d°", (int) roundf(id(wx_temp).state)); return std::string(b);
|
||||
else: [ lvgl.widget.hide: mc_sun_temp ]
|
||||
- lvgl.label.update:
|
||||
id: mc_hearts
|
||||
text: !lambda |-
|
||||
int p = id(g_batt);
|
||||
int n = (p < 0) ? 5 : (p + 10) / 20; if (n < 0) n = 0; if (n > 5) n = 5;
|
||||
std::string s; for (int i = 0; i < n; i++) s += "";
|
||||
return s;
|
||||
# Day/night flipped -> swap sky tint, sun<->moon, stars, temp colour
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_mc_upd_night);' }
|
||||
then:
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_mc_last_night) == 1;' }
|
||||
then:
|
||||
- lvgl.widget.update: { id: mc_sky, bg_color: 0x101A33 }
|
||||
- lvgl.widget.hide: mc_sun
|
||||
- lvgl.widget.show: mc_moon
|
||||
- lvgl.widget.show: mc_star1
|
||||
- lvgl.widget.show: mc_star2
|
||||
- lvgl.widget.show: mc_star3
|
||||
- lvgl.widget.show: mc_star4
|
||||
- lvgl.widget.show: mc_star5
|
||||
- lvgl.label.update: { id: mc_sun_temp, text_color: 0xC8D2E2 }
|
||||
else:
|
||||
- lvgl.widget.update: { id: mc_sky, bg_color: 0x5A9BD4 }
|
||||
- lvgl.widget.show: mc_sun
|
||||
- lvgl.widget.hide: mc_moon
|
||||
- lvgl.widget.hide: mc_star1
|
||||
- lvgl.widget.hide: mc_star2
|
||||
- lvgl.widget.hide: mc_star3
|
||||
- lvgl.widget.hide: mc_star4
|
||||
- lvgl.widget.hide: mc_star5
|
||||
- lvgl.label.update: { id: mc_sun_temp, text_color: 0x16314A }
|
||||
# Timer badge (active timer - minutes left), kept responsive (outside the minute gate)
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_timer_active);' }
|
||||
then:
|
||||
- lvgl.widget.show: mc_timer
|
||||
- lvgl.label.update:
|
||||
id: mc_timer
|
||||
text: !lambda |-
|
||||
int s = id(g_timer_secs); if (s < 0) s = 0;
|
||||
int m = (s + 59) / 60;
|
||||
char b[24]; snprintf(b, sizeof(b), " %d min", m);
|
||||
return std::string(b);
|
||||
else:
|
||||
- lvgl.widget.hide: mc_timer
|
||||
@@ -0,0 +1,245 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# NEON watchface (optional home face). Add to the thin config's files: list to
|
||||
# enable; the picker (Settings > Home > Watchface) then offers it. Watchface id 1.
|
||||
#
|
||||
# A full-screen home page with huge two-tone digits, neon rim rings, weather +
|
||||
# room/battery stats. It reuses core's clock/weather/room/battery sensors and the
|
||||
# g_show_weather / g_show_climate display toggles - no extra entities needed.
|
||||
#
|
||||
# Contract (mirrors the screen carousel): self-registers in the watchface registry
|
||||
# at boot, renders itself while active, and self-shows its page when core requests
|
||||
# the home base (core can't reference an optional page id, hence the g_wf_req flag).
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
esphome:
|
||||
on_boot:
|
||||
- priority: 655
|
||||
then:
|
||||
- lambda: 'id(g_wf_present)[1] = true; id(g_wf_name)[1] = "Neon";'
|
||||
|
||||
globals:
|
||||
- { id: g_nf_last_min, type: int, restore_value: no, initial_value: '-1' } # perf: only redraw on minute change
|
||||
- { id: g_nf_upd_min, type: bool, restore_value: no, initial_value: 'false' }
|
||||
|
||||
font:
|
||||
# Huge bold digits - only 0-9 needed, so it stays tiny in flash
|
||||
- file: { type: gfonts, family: "Roboto", weight: 900 }
|
||||
id: font_neon
|
||||
size: 108
|
||||
glyphs: "0123456789"
|
||||
|
||||
lvgl:
|
||||
pages:
|
||||
- id: page_home_neon
|
||||
bg_color: 0x000000
|
||||
widgets:
|
||||
- obj: # outer neon ring at the rim
|
||||
id: nf_ring_out
|
||||
align: CENTER
|
||||
width: 352
|
||||
height: 352
|
||||
radius: 176
|
||||
bg_opa: TRANSP
|
||||
border_width: 4
|
||||
border_color: 0xFF2E88
|
||||
border_opa: 60%
|
||||
clickable: false
|
||||
scrollable: false
|
||||
- obj: # second neon ring, also hugging the rim (interior left free for content)
|
||||
id: nf_ring_in
|
||||
align: CENTER
|
||||
width: 320
|
||||
height: 320
|
||||
radius: 160
|
||||
bg_opa: TRANSP
|
||||
border_width: 4
|
||||
border_color: 0x9A5BFF
|
||||
border_opa: 55%
|
||||
clickable: false
|
||||
scrollable: false
|
||||
- label: # weather icon
|
||||
id: nf_wx_ic
|
||||
align: CENTER
|
||||
x: -34
|
||||
y: -103
|
||||
text: ""
|
||||
text_font: icons34
|
||||
text_color: 0xBFD2DA
|
||||
- label: # outdoor temperature
|
||||
id: nf_wx_temp
|
||||
align: CENTER
|
||||
x: 26
|
||||
y: -106
|
||||
text: "--°"
|
||||
text_font: font_sel
|
||||
text_color: 0xFFFFFF
|
||||
- label: # condition text (UPPERCASE)
|
||||
id: nf_wx_cond
|
||||
align: CENTER
|
||||
y: -76
|
||||
text: ""
|
||||
text_font: font_artist
|
||||
text_color: 0x8A93A0
|
||||
- label: # HOURS
|
||||
id: nf_hh
|
||||
align: CENTER
|
||||
x: -70
|
||||
y: -4
|
||||
text: "00"
|
||||
text_font: font_neon
|
||||
text_color: 0xFF2E88
|
||||
- label: # MINUTES
|
||||
id: nf_mm
|
||||
align: CENTER
|
||||
x: 70
|
||||
y: -4
|
||||
text: "00"
|
||||
text_font: font_neon
|
||||
text_color: 0x9A5BFF
|
||||
- label: # date
|
||||
id: nf_date
|
||||
align: CENTER
|
||||
y: 70
|
||||
text: ""
|
||||
text_font: font_med
|
||||
text_color: 0x6f9a92
|
||||
- label: # room temperature icon
|
||||
id: nf_ct_ic
|
||||
align: CENTER
|
||||
x: -60
|
||||
y: 99
|
||||
text: ""
|
||||
text_font: icons18
|
||||
text_color: 0xFF8A5C
|
||||
- label:
|
||||
id: nf_ct_val
|
||||
align: CENTER
|
||||
x: -32
|
||||
y: 99
|
||||
text: "--"
|
||||
text_font: font_artist
|
||||
text_color: 0x9AA3B0
|
||||
- label: # room humidity icon
|
||||
id: nf_hum_ic
|
||||
align: CENTER
|
||||
x: 16
|
||||
y: 99
|
||||
text: ""
|
||||
text_font: icons18
|
||||
text_color: 0x3FA9D6
|
||||
- label:
|
||||
id: nf_hum_val
|
||||
align: CENTER
|
||||
x: 42
|
||||
y: 99
|
||||
text: "--"
|
||||
text_font: font_artist
|
||||
text_color: 0x9AA3B0
|
||||
- label: # battery (glyph + %), mirrored from core's lbl_batt (same LVGL symbols)
|
||||
id: nf_bt
|
||||
align: CENTER
|
||||
y: 126
|
||||
text: ""
|
||||
text_font: montserrat_18
|
||||
text_color: 0x6f9a92
|
||||
- label: # timer badge (active timer - mins left), mirrors the classic face
|
||||
id: nf_timer
|
||||
align: TOP_MID
|
||||
y: 22
|
||||
text: ""
|
||||
text_font: montserrat_18
|
||||
text_color: 0xFFFFFF
|
||||
bg_color: 0xFF3B30
|
||||
bg_opa: COVER
|
||||
radius: 14
|
||||
pad_left: 12
|
||||
pad_right: 12
|
||||
pad_top: 5
|
||||
pad_bottom: 5
|
||||
hidden: true
|
||||
|
||||
interval:
|
||||
# Self-show: core sets g_wf_req when it wants the home base shown; we own page_home_neon.
|
||||
- interval: 50ms
|
||||
then:
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_wf_req) && id(g_watchface) == 1;' }
|
||||
then:
|
||||
- lambda: 'id(g_wf_req) = false; id(g_nf_last_min) = -1;' # force a full redraw on entry
|
||||
# slide in matching the carousel direction (like Classic / the screens) - no laggy fade
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==0;' }, then: [lvgl.page.show: { id: page_home_neon, animation: move_left, time: 250ms }] }
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==1;' }, then: [lvgl.page.show: { id: page_home_neon, animation: move_right, time: 250ms }] }
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==2;' }, then: [lvgl.page.show: { id: page_home_neon, animation: move_top, time: 250ms }] }
|
||||
- if: { condition: { lambda: 'return id(g_nav_anim)==3;' }, then: [lvgl.page.show: { id: page_home_neon, animation: move_bottom, time: 250ms }] }
|
||||
- script.execute: neon_render
|
||||
# Refresh while this is the active home face
|
||||
- interval: 1s
|
||||
then:
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_watchface) == 1 && id(g_base) == 0;' }
|
||||
then: [ script.execute: neon_render ]
|
||||
|
||||
script:
|
||||
- id: neon_render
|
||||
then:
|
||||
# Cheap tick: figure out if the minute actually changed (heavy redraws only then)
|
||||
- lambda: |-
|
||||
id(g_nf_upd_min) = false;
|
||||
auto t = id(ha_time).now();
|
||||
if (!t.is_valid()) return;
|
||||
int mn = t.hour * 60 + t.minute;
|
||||
if (mn != id(g_nf_last_min)) { id(g_nf_last_min) = mn; id(g_nf_upd_min) = true; }
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_nf_upd_min);' }
|
||||
then:
|
||||
- lvgl.label.update: { id: nf_hh, text: !lambda 'return id(ha_time).now().strftime("%H");' }
|
||||
- lvgl.label.update: { id: nf_mm, text: !lambda 'return id(ha_time).now().strftime("%M");' }
|
||||
- lvgl.label.update: { id: nf_wx_ic, text: !lambda 'return id(g_nf_glyph).empty() ? std::string("") : id(g_nf_glyph);' }
|
||||
- lvgl.label.update: { id: nf_wx_cond, text: !lambda 'return id(g_nf_cond);' }
|
||||
- lvgl.label.update:
|
||||
id: nf_wx_temp
|
||||
text: !lambda |-
|
||||
if (!id(wx_temp).has_state()) return std::string("--°");
|
||||
char b[10]; snprintf(b, sizeof(b), "%d°", (int) roundf(id(wx_temp).state)); return std::string(b);
|
||||
- lvgl.label.update:
|
||||
id: nf_date
|
||||
text: !lambda |-
|
||||
auto t = id(ha_time).now();
|
||||
static const char* d[8] = {"","Sun","Mon","Tue","Wed","Thu","Fri","Sat"};
|
||||
std::string day = (t.day_of_week >= 1 && t.day_of_week <= 7) ? std::string(d[t.day_of_week]) : std::string("");
|
||||
return t.strftime("%d %b") + " " + day;
|
||||
- lvgl.label.update:
|
||||
id: nf_ct_val
|
||||
text: !lambda 'if (!id(room_temp).has_state()) return std::string("--"); char b[12]; snprintf(b, sizeof(b), "%.1f°", id(room_temp).state); return std::string(b);'
|
||||
- lvgl.label.update:
|
||||
id: nf_hum_val
|
||||
text: !lambda 'if (!id(room_hum).has_state()) return std::string("--"); char b[8]; snprintf(b, sizeof(b), "%d%%", (int) roundf(id(room_hum).state)); return std::string(b);'
|
||||
# Battery: mirror core's lbl_batt text + colour
|
||||
- lambda: 'lv_label_set_text(id(nf_bt), lv_label_get_text(id(lbl_batt)));'
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_charging);' }
|
||||
then: [ lvgl.label.update: { id: nf_bt, text_color: 0x35c759 } ]
|
||||
else: [ lvgl.label.update: { id: nf_bt, text_color: 0x6f9a92 } ]
|
||||
# Weather / climate visibility obey the Display toggles
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_show_weather);' }
|
||||
then: [ lvgl.widget.show: nf_wx_ic, lvgl.widget.show: nf_wx_temp, lvgl.widget.show: nf_wx_cond ]
|
||||
else: [ lvgl.widget.hide: nf_wx_ic, lvgl.widget.hide: nf_wx_temp, lvgl.widget.hide: nf_wx_cond ]
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_show_climate);' }
|
||||
then: [ lvgl.widget.show: nf_ct_ic, lvgl.widget.show: nf_ct_val, lvgl.widget.show: nf_hum_ic, lvgl.widget.show: nf_hum_val ]
|
||||
else: [ lvgl.widget.hide: nf_ct_ic, lvgl.widget.hide: nf_ct_val, lvgl.widget.hide: nf_hum_ic, lvgl.widget.hide: nf_hum_val ]
|
||||
# Timer badge - kept responsive (cheap) so it appears/updates promptly
|
||||
- if:
|
||||
condition: { lambda: 'return id(g_timer_active);' }
|
||||
then:
|
||||
- lvgl.widget.show: nf_timer
|
||||
- lvgl.label.update:
|
||||
id: nf_timer
|
||||
text: !lambda |-
|
||||
int s = id(g_timer_secs); if (s < 0) s = 0;
|
||||
int m = (s + 59) / 60;
|
||||
char b[24]; snprintf(b, sizeof(b), " %d min", m);
|
||||
return std::string(b);
|
||||
else:
|
||||
- lvgl.widget.hide: nf_timer
|
||||
Reference in new issue
Block a user