Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion .github/workflows/host-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ on:
push:
branches: [main]

permissions:
contents: read

concurrency:
group: host-tests-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
Expand All @@ -16,8 +19,20 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Host C++ (date_fmt, day mode)
- name: Host C++ (date_fmt, day mode, dirty, settings)
env:
CXX: g++
run: make -C tests/host run

- name: Tools gen_* smoke
run: python3 -m unittest discover -s tools/tests -v

- name: Codegen artifacts in sync
run: |
set -euo pipefail
python3 tools/gen_i18n_catalog.py
git diff --exit-code -- components/flint/i18n/i18n_catalog.inc
python3 tools/gen_asset_pack.py
git diff --exit-code -- assets/fs_root/core.flintpack
# Board outputs are gitignored; regen must succeed for the default board.
python3 tools/gen_board_catalog.py --quiet
5 changes: 3 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,15 @@ Interactive picker: `./flint_tools`. Full flags and examples: [`tools/README.md`
Pure contracts (no ESP-IDF / device). From repo root:

```bash
# date_fmt + dayModeFromHour / WAITING profile (asserts + g++)
# date_fmt / day-mode / dirty rules / settings clamp (explicit fail, -UNDEBUG)
make -C tests/host run

# tools/gen_* schema / id smoke (stdlib unittest; pytest also works if installed)
python3 -m unittest discover -s tools/tests -v
```

CI runs the same commands on every PR via `.github/workflows/host-tests.yml`.
CI runs the same commands on every PR via `.github/workflows/host-tests.yml`, plus
regen of i18n/pack with `git diff --exit-code` and a quiet board catalog codegen.

Do not add on-target Unity or heavy CI matrices unless agreed separately.

Expand Down
2 changes: 1 addition & 1 deletion components/flint/app/app.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ static void debugHeartbeat(const struct tm* tiOpt) {
const char* modeName = "-";
if (ti) {
snprintf(tbuf, sizeof(tbuf), "%02d:%02d:%02d", ti->tm_hour, ti->tm_min, ti->tm_sec);
modeName = dayModeNameRu(dayModeFromHour(ti->tm_hour));
modeName = dayModeName(dayModeFromHour(ti->tm_hour));
}

LOGI("hb", "wifi=%d time=%s%s mode=%s heap=%u dirty=0x%x",
Expand Down
2 changes: 1 addition & 1 deletion components/flint/config/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,5 @@ static const uint32_t NTP_RESYNC_MS = 24UL * 60UL * 60UL * 1000UL;
static const uint32_t WIFI_RETRY_MS = 15000UL;
static const uint32_t WIFI_CONNECT_TIMEOUT_MS = 20000UL;

// Factory default when NVS has no active_face yet (picker / settings override).
// Factory default when ui.cfg has no active_face yet (picker / settings override).
inline constexpr const char* ACTIVE_FACE_ID = "digital";
4 changes: 0 additions & 4 deletions components/flint/day_mode/day_mode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@ const char* dayModeName(DayMode mode) {
return flintTrDayMode(static_cast<uint8_t>(mode));
}

const char* dayModeNameRu(DayMode mode) {
return dayModeName(mode);
}

static void fillChromeRoles(UiPalette& p) {
p.divider = colorLerp(p.bgTop, p.accent, 70);
p.selectionBg = colorLerp(p.bgTop, p.panel, 200);
Expand Down
3 changes: 1 addition & 2 deletions components/flint/day_mode/day_mode.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@

#include <stdint.h>

const char* dayModeNameRu(DayMode mode); // legacy alias → flintTrDayMode
const char* dayModeName(DayMode mode); // localized
const char* dayModeName(DayMode mode); // localized

UiPalette dayModeBuildPalette(DayMode mode);

Expand Down
2 changes: 1 addition & 1 deletion components/flint/faces/FaceContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ struct FaceContext {
bool wifiConnected;
uint8_t wifiBars; // 0 = off, 1–3 = RSSI strength while connected

// Resolved prefs (shell/settings) — faces must not read NVS themselves.
// Resolved prefs (shell/settings → /cfg/ui.cfg) — faces must not read cfg/NVS.
bool showSeconds;
bool hour12; // false = 24h, true = 12h + AM/PM
uint8_t dateFormat; // SettingsDateFormat
Expand Down
18 changes: 11 additions & 7 deletions components/flint/settings/settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "config/debug.h"
#include "i18n/i18n.h"
#include "settings/cfg_fs.h"
#include "settings/settings_clamp.h"
#include "time/tz_db.h"

#include <stdio.h>
Expand All @@ -28,6 +29,11 @@ static void settingsApplyDefaults() {
}

static void settingsApplyAfterLoad() {
gSettings.showSeconds = settingsClampShowSeconds(gSettings.showSeconds);
gSettings.lang = settingsClampLang(gSettings.lang);
gSettings.hourFormat = settingsClampHourFormat(gSettings.hourFormat);
gSettings.dateFormat = settingsClampDateFormat(gSettings.dateFormat);
gSettings.dateSep = settingsClampDateSep(gSettings.dateSep);
flintI18nSetLang(gSettings.lang);
clockTimeApplyTz(gSettings.tzId);
}
Expand Down Expand Up @@ -253,30 +259,28 @@ bool settingsSetActiveFace(const char* id) {
}

void settingsSetShowSeconds(uint8_t mode) {
gSettings.showSeconds = mode;
gSettings.showSeconds = settingsClampShowSeconds(mode);
settingsSave();
}

void settingsSetLang(uint8_t lang) {
gSettings.lang = (lang == SETTINGS_LANG_EN) ? SETTINGS_LANG_EN : SETTINGS_LANG_RU;
gSettings.lang = settingsClampLang(lang);
flintI18nSetLang(gSettings.lang);
settingsSave();
}

void settingsSetHourFormat(uint8_t fmt) {
gSettings.hourFormat = (fmt == SETTINGS_HOUR_12) ? SETTINGS_HOUR_12 : SETTINGS_HOUR_24;
gSettings.hourFormat = settingsClampHourFormat(fmt);
settingsSave();
}

void settingsSetDateFormat(uint8_t fmt) {
gSettings.dateFormat =
(fmt < SETTINGS_DATE_FORMAT_COUNT) ? fmt : static_cast<uint8_t>(SETTINGS_DATE_DMY4);
gSettings.dateFormat = settingsClampDateFormat(fmt);
settingsSave();
}

void settingsSetDateSep(uint8_t sep) {
gSettings.dateSep =
(sep < SETTINGS_DATE_SEP_COUNT) ? sep : static_cast<uint8_t>(SETTINGS_DATE_SEP_DOT);
gSettings.dateSep = settingsClampDateSep(sep);
settingsSave();
}

Expand Down
32 changes: 32 additions & 0 deletions components/flint/settings/settings_clamp.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#pragma once

#include <stdint.h>

#include "settings/settings.h"

// Pure clamp helpers for prefs enums (host-testable; used by settings setters).

inline uint8_t settingsClampHourFormat(uint8_t fmt) {
return (fmt == SETTINGS_HOUR_12) ? SETTINGS_HOUR_12 : SETTINGS_HOUR_24;
}

inline uint8_t settingsClampDateFormat(uint8_t fmt) {
return (fmt < SETTINGS_DATE_FORMAT_COUNT) ? fmt
: static_cast<uint8_t>(SETTINGS_DATE_DMY4);
}

inline uint8_t settingsClampDateSep(uint8_t sep) {
return (sep < SETTINGS_DATE_SEP_COUNT) ? sep
: static_cast<uint8_t>(SETTINGS_DATE_SEP_DOT);
}

inline uint8_t settingsClampLang(uint8_t lang) {
return (lang == SETTINGS_LANG_EN) ? SETTINGS_LANG_EN : SETTINGS_LANG_RU;
}

inline uint8_t settingsClampShowSeconds(uint8_t mode) {
if (mode == SETTINGS_SEC_ON || mode == SETTINGS_SEC_OFF || mode == SETTINGS_SEC_AUTO) {
return mode;
}
return SETTINGS_SEC_AUTO;
}
4 changes: 2 additions & 2 deletions components/flint/shell/shell_settings.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
#include <stdint.h>

// Settings overlay — owns row selection, paint, and value adjusters.
// Keep this file focused: new setting rows/adjusters → helpers beside it
// (formatters, cycles), not a return to a monolithic shell god-file.
// Do not grow this file with more rows/cycles inline: extract formatters /
// cycle helpers (or paint) beside it. New settings → helper first, not god-file.

void shellSettingsEnter();
void shellSettingsNavPrev();
Expand Down
20 changes: 9 additions & 11 deletions components/flint/ui/ui.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "ui.h"
#include "ui/ui_dirty.h"
#include "ui/ui_dirty_rules.h"
#include "config/config.h"
#include "config/debug.h"
#include "display/display.h"
Expand Down Expand Up @@ -99,28 +100,25 @@ void uiFaceTick() {
}

// Colon pulse only: reuse last context (skip palette / clock / format rebuild).
if ((dirty & ~UI_DIRTY_MOTION) == 0 && gHaveLastCtx) {
if (uiDirtyIsMotionOnly(dirty) && gHaveLastCtx) {
gLastCtx.dirty = dirty;
face->onTick(gLastCtx);
return;
}

FaceContext ctx = uiBuildContext();
ctx.dirty = dirty;
if (!ctx.showSeconds) {
dirty &= ~UI_DIRTY_SECOND;
ctx.dirty = dirty;
if (!dirty) {
gLastCtx = ctx;
gHaveLastCtx = true;
return;
}
dirty = uiDirtyFilterSeconds(dirty, ctx.showSeconds);
ctx.dirty = dirty;
if (!dirty) {
gLastCtx = ctx;
gHaveLastCtx = true;
return;
}

// FORCE / DAY_MODE / TIME_VALID → onForceRedraw.
// SETTINGS stays soft → onTick (faces rebuild layout if prefs changed).
const bool force = (dirty & (UI_DIRTY_FORCE | UI_DIRTY_DAY_MODE | UI_DIRTY_TIME_VALID)) != 0
|| ctx.dayMode != gLastMode;
const bool force = uiDirtyWantsForceRedraw(dirty) || ctx.dayMode != gLastMode;

gLastCtx = ctx;
gHaveLastCtx = true;
Expand Down
22 changes: 22 additions & 0 deletions components/flint/ui/ui_dirty_rules.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#pragma once

#include <stdint.h>

#include "ui/ui_dirty.h"

// Pure dirty-mask rules used by uiFaceTick (host-testable).

inline bool uiDirtyIsMotionOnly(uint32_t dirty) {
return dirty != 0 && (dirty & ~UI_DIRTY_MOTION) == 0;
}

inline uint32_t uiDirtyFilterSeconds(uint32_t dirty, bool showSeconds) {
if (!showSeconds) {
dirty &= ~UI_DIRTY_SECOND;
}
return dirty;
}

inline bool uiDirtyWantsForceRedraw(uint32_t dirty) {
return (dirty & (UI_DIRTY_FORCE | UI_DIRTY_DAY_MODE | UI_DIRTY_TIME_VALID)) != 0;
}
24 changes: 20 additions & 4 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
# FlinT OS architecture

Firmware platform for **FlinT** devices (base: **FlinT Spark**), by **DeeTech Labs**.
This note covers the Face paint contract and dirty/event wiring. Module ownership: see [`MODULES.md`](MODULES.md) when present; Face SDK: `components/flint/sdk/flint_face_sdk.h`.
Face paint contract and dirty/event wiring. Module map: [`MODULES.md`](MODULES.md). Face SDK: `components/flint/sdk/flint_face_sdk.h`.

## Layers

```text
hardware / board JSON
→ services (time, net, settings, day_mode)
→ services (time, net, settings/cfg, day_mode)
→ FaceContext (uiBuildContext)
→ Face::onTick / onForceRedraw
→ display (partial or full present)
```

Faces read `FaceContext` and draw. They must not call Wi‑Fi, HTTP, NVS, or settings APIs.
Faces read `FaceContext` and draw. They must not call Wi‑Fi, HTTP, NVS, or settings/cfg APIs.

## Day mode and waiting

Time-of-day modes (`MORNING` / `DAY` / `EVENING` / `NIGHT`) come only from a **valid** local hour via `dayModeFromHour` (`day_mode_hour.*`).

When `clockTimeGetLocal` fails (`!hasTime`), `uiBuildContext` sets **`DAY_MODE_WAITING`** — not a synthetic noon hour. Waiting uses a neutral profile (`showSeconds` / `showModeLabel` off for AUTO; date line already carries “Waiting for time”). `dayModeFromHour` never returns `WAITING`.

## Events → dirty → paint

Expand All @@ -32,9 +38,19 @@ Faces read `FaceContext` and draw. They must not call Wi‑Fi, HTTP, NVS, or set

### Prefs vs FORCE

- **Prefs are soft.** After a settings/NVS change, emit **`FLINT_EVT_SETTINGS` only**. Do **not** also emit `FLINT_EVT_FORCE_REDRAW`.
- **Prefs are soft.** After a prefs change on **`/cfg/ui.cfg`**, emit **`FLINT_EVT_SETTINGS` only**. Do **not** also emit `FLINT_EVT_FORCE_REDRAW`.
- `uiFaceTick` routes `UI_DIRTY_SETTINGS` to **`onTick`**, not `onForceRedraw`.
- Faces must treat `UI_DIRTY_SETTINGS` (and/or changed fields in `FaceContext`) as layout-sensitive: rebuild time layout, date line, mode label, i18n strings as needed — often a full paint **from `onTick`**.
- Use **`FLINT_EVT_FORCE_REDRAW`** for shell transitions (enter face), explicit user force, and other “wipe the canvas” cases — not as a stand-in for prefs.

Clock edits from the settings overlay emit `MINUTE` / `TIME_VALID` as appropriate; returning to the face still uses shell `FORCE` on enter.

## Persistence

| Store | Role |
|-------|------|
| `storage` LittleFS | Asset pack (`core.flintpack`) |
| `cfg` LittleFS | UI prefs (`ui.cfg`) + soft clock (`time.cfg`) |
| ESP-IDF NVS | System / Wi‑Fi — **not** FlinT UI prefs |

One-shot migrators still copy old `"flint"` NVS UI / epoch keys into `cfg` when the file is missing (`settings.cpp`, `clock_time.cpp`). **Sunset plan:** keep until field devices are known migrated; then delete migrate paths and NVS UI key reads in the same change (no long-lived dual-write).
19 changes: 12 additions & 7 deletions docs/MODULES.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,21 @@ What exists under `components/flint/` today. Paths are from that component root.
| Directory | Role |
|-----------|------|
| `app/` | Boot orchestration: bring up services, drive shell loop |
| `shell/` | State machine: face, face picker, settings overlays |
| `settings/` | Load/save prefs and soft clock on `cfg` LittleFS (`ui.cfg`) |
| `shell/` | State machine (`shell.cpp`) + overlays: `shell_picker.*`, `shell_settings.*` |
| `settings/` | Load/save prefs and soft clock on `cfg` LittleFS (`ui.cfg` / `time.cfg`) |
| `config/` | Compile-time defaults, version macros, local `secrets.h` |
| `board/` | Board JSON profiles → generated `board_config.inc` |
| `display/` | LovyanGFX HAL, shared fonts, icon draw, UI tokens |
| `assets/` | Mount `storage` LittleFS; blit from `core.flintpack` |
| `sdk/` | Public Face SDK headers (`flint_face_sdk.h`, resources) |
| `faces/` | `Face` API, registry, glances, built-in faces |
| `day_mode/` | Day segments + palettes consumed via `FaceContext` |
| `faces/` | `Face` API, registry, glances, built-in faces (+ `face_digital_layout.*`) |
| `day_mode/` | Hour→mode (`day_mode_hour.*`), profiles (`day_mode_profile.*`), palettes/wash |
| `i18n/` | Language packs / `flintTr` |
| `net/` | Wi‑Fi STA |
| `time/` | Soft clock, SNTP, date formatting helpers |
| `ui/` | Active-face paint path + dirty bitfield |
| `ui/` | Active-face paint path + dirty bitfield + chrome/splash |
| `event/` | Lightweight event bus |
| `anim/` | Short motion helpers (e.g. colon pulse) |
| `anim/` | Short motion helpers (e.g. overlay fade) |
| `util/` | `millis` / delay / heap helpers |

Related outside `flint/`:
Expand All @@ -31,10 +31,15 @@ Related outside `flint/`:
| `config/` | `sdkconfig.defaults`, `partitions.csv` |
| `assets/` | PNG sources for the asset pack |
| `tools/` | Codegen (board catalog, i18n, asset pack) |
| `tests/host/` | Host C++ (date_fmt, day-mode, dirty rules, settings clamp); CI via `host-tests.yml` |
| `tools/tests/` | Unittest smoke for `gen_*` schema / ids (+ all boards validate) |
| `community/` | Face templates (not linked into the build) |
| `.github/workflows/` | CI (`host-tests.yml`) |

**Shell growth:** keep `shell_settings.cpp` focused — new setting rows / formatters / cycles go into helpers beside it, not back into a monolithic shell file.

Persistence split:

- **`storage`** — read-mostly asset pack (rewritten on full flash)
- **`cfg`** — mutable prefs + soft clock (survives asset reflash)
- **`nvs`** — ESP-IDF / Wi‑Fi system NVS (not FlinT UI prefs)
- **`nvs`** — ESP-IDF / Wi‑Fi system NVS (not FlinT UI prefs; one-shot migrate into `cfg` only)
16 changes: 13 additions & 3 deletions tests/host/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@

FLINT := ../../components/flint
BUILD := build
CXX ?= c++
CXXFLAGS := -std=c++17 -Wall -Wextra -Werror -I$(FLINT)
CXX ?= g++
# Keep asserts armed if any sneak in; prefer hostFail / hostRequire.
CXXFLAGS := -std=c++17 -Wall -Wextra -Werror -UNDEBUG -I$(FLINT) -I.

.PHONY: all run clean

all: $(BUILD)/test_day_mode $(BUILD)/test_date_fmt
all: $(BUILD)/test_day_mode $(BUILD)/test_date_fmt $(BUILD)/test_ui_dirty \
$(BUILD)/test_settings_clamp

$(BUILD):
mkdir -p $(BUILD)
Expand All @@ -22,9 +24,17 @@ $(BUILD)/test_day_mode: test_day_mode.cpp $(FLINT)/day_mode/day_mode_hour.cpp \
$(BUILD)/test_date_fmt: test_date_fmt.cpp stubs/i18n_stub.cpp $(FLINT)/time/date_fmt.cpp | $(BUILD)
$(CXX) $(CXXFLAGS) -o $@ test_date_fmt.cpp stubs/i18n_stub.cpp $(FLINT)/time/date_fmt.cpp

$(BUILD)/test_ui_dirty: test_ui_dirty.cpp | $(BUILD)
$(CXX) $(CXXFLAGS) -o $@ test_ui_dirty.cpp

$(BUILD)/test_settings_clamp: test_settings_clamp.cpp | $(BUILD)
$(CXX) $(CXXFLAGS) -o $@ test_settings_clamp.cpp

run: all
./$(BUILD)/test_day_mode
./$(BUILD)/test_date_fmt
./$(BUILD)/test_ui_dirty
./$(BUILD)/test_settings_clamp

clean:
rm -rf $(BUILD)
Loading
Loading