Files
bfe-core1106-sdk/drivers/freshness/freshness.c
T
BFE EngineeringandClaude Opus 4.8 4ee4dfcf88 review: fix review findings (correctness, hardening, doc-accuracy)
First recursive code-review-harness pass over the authored SDK code (sim/,
config-lint, flowgen, drivers/{relays,freshness}, build/, ci.yml). Four parallel
dimension reviewers; all findings at every severity corrected per workspace rule.

Correctness / reliability:
- freshness.c min_budget_ms: use a `seen` flag, not `best==0`, as the empty
  sentinel — a zero-tolerance (max_stale_ms==0) binding was silently widened to a
  looser neighbour's budget. Regression test added; still 66/66 MC/DC.
- config-lint parse_reserved_ranges: match `reg` as a whole property token (ident
  boundary before, `=` after) so `reg-names` / a `region-*` label no longer
  mis-parses into a bogus reserved range.
- config-lint loader check: fail closed — flag any loader with a LOAD_ADDR that is
  not a known-safe boot component, instead of only known MCU names, so a future
  coprocessor ("Rtos"/"Bl32") can't slip past the 0x40000-brick gate.
- build-kernel.sh: sha256 verification is now mandatory (refuse to build if the pin
  is missing) and the mktemp scratch tree is removed on exit (trap), while a
  caller-provided WORK is left intact for CI artifact upload.

Test quality:
- freshness: added the age==max_stale boundary case and a clock-wraparound
  (now < last_ok) fail-safe-to-UNKNOWN test.
- relays: unsetenv(WARDEN_GPIO_ROOT) at main() so the NULL-env arm is hermetic.

Security / CI:
- ci.yml: top-level `permissions: contents: read` (badges overrides to write);
  pin taiki-e/install-action to commit SHA (v2.86.7).

Maintainability / docs:
- drivers/enforce-mcdc.sh: one shared, name-derived gate replaces the two
  copy-pasted per-driver scripts; Makefiles call ../../enforce-mcdc.sh.
- docs/architecture.md: §3/§4/§6/§7 rewritten to match reality — NPU/RGA models,
  config-lint, and the relays+freshness MC/DC harnesses are done; kernel §6 now
  reflects the 5.10->6.18.46 forward-port (ADR-0001), not the superseded plan44/6.6.
- README: status blurb + layout table corrected (kernel/, .github/; stale ci/ and
  patches/ descriptions fixed). hpmcu "8 tests" -> 7 in docs.
- freshness.{c,h}: ADR reference points at flare-edge ADR-0004 (warden-sdk's
  ADR-0004 is the CI runner — number collision).
- normalize rustfmt drift across sim/ + config-lint.

All green: sim 37 tests, config-lint 8 tests, both drivers 100% MC/DC (relays
40/40, freshness 66/66), clippy clean under -D warnings, gitleaks clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017wB8KB3MMQztRDXCMCkPrf
2026-08-25 15:48:52 -06:00

192 lines
6.6 KiB
C

/*
* The UI Freshness Contract engine (flare-edge ADR-0004) — core, LVGL-free.
* See freshness.h for the contract. LVGL binding lives in freshness_lv.c.
*/
#include "freshness.h"
#include <stdio.h>
#include <string.h>
/* The unknown mark the engine renders when a source cannot be evaluated (from
* freshness.h so the LVGL layer and tests share the exact literal). */
#define FRESH_UNKNOWN_MARK WARDEN_FRESH_UNKNOWN_MARK
/* Max simultaneous live bindings. Bindings belong to visible pages; the whole
* navigable set of a screen is small, so this is generous. A full table drops
* the binding (returns NULL) rather than silently overflowing — the LVGL layer
* turns that into a visible fault, never a stale value. */
#ifndef FRESH_MAX
#define FRESH_MAX 96
#endif
#define FRESH_BUFSZ 64
struct warden_fresh {
void * page;
warden_fresh_produce_cb produce;
void * ud;
uint32_t max_stale_ms;
const char * source;
warden_fresh_render_cb render;
void * widget;
uint32_t last_ok_ms; /* time of last OK/SAME produce */
char last[FRESH_BUFSZ]; /* last good value string */
bool ever_ok;
bool showing_unknown;
bool visible;
bool used;
};
/* A fixed table scanned in full: bindings are torn down all at once by
* warden_fresh_reset (like the screen timers), never individually, so a running
* high-water bound would only hide the free-slot arms from tests without saving
* real work — the visible set per screen is a handful. */
static struct warden_fresh s_vals[FRESH_MAX];
warden_fresh_render_t warden_fresh_decide(warden_fresh_result_t produced,
bool ever_ok, bool showing_unknown,
uint32_t age_ms, uint32_t max_stale_ms)
{
switch(produced) {
case FRESH_OK:
return FRESH_RENDER_VALUE;
case FRESH_SAME:
/* Unchanged and fresh: normally nothing to do. But if the widget is
* currently showing UNKNOWN (it went stale), an unchanged value
* still has to be repainted to clear the mark. */
return showing_unknown ? FRESH_RENDER_VALUE : FRESH_RENDER_NOCHANGE;
case FRESH_UNKNOWN:
default:
/* Never had a value, or the last good value is now older than its
* budget: stop asserting a confident number. Otherwise tolerate a
* brief blip and hold the last value until the budget expires. */
if(!ever_ok) return FRESH_RENDER_UNKNOWN;
if(age_ms > max_stale_ms) return FRESH_RENDER_UNKNOWN;
return FRESH_RENDER_NOCHANGE;
}
}
static void refresh_one(struct warden_fresh *v, uint32_t now)
{
char buf[FRESH_BUFSZ];
buf[0] = '\0';
warden_fresh_result_t r = v->produce(buf, sizeof buf, v->ud);
warden_fresh_render_t what = warden_fresh_decide(
r, v->ever_ok, v->showing_unknown, now - v->last_ok_ms, v->max_stale_ms);
if(r == FRESH_OK) {
/* Keep the last good string so a later SAME-recovery can repaint it.
* snprintf truncates and null-terminates; buf and last are both
* FRESH_BUFSZ, so this cannot overflow. */
snprintf(v->last, sizeof v->last, "%s", buf);
}
if(r == FRESH_OK || r == FRESH_SAME) {
v->last_ok_ms = now;
v->ever_ok = true;
}
switch(what) {
case FRESH_RENDER_VALUE:
v->render(v->widget, FRESH_RENDER_VALUE,
(r == FRESH_OK) ? buf : v->last, v->ud);
v->showing_unknown = false;
break;
case FRESH_RENDER_UNKNOWN:
v->render(v->widget, FRESH_RENDER_UNKNOWN, FRESH_UNKNOWN_MARK, v->ud);
v->showing_unknown = true;
break;
case FRESH_RENDER_NOCHANGE:
break;
}
}
warden_fresh_t *warden_fresh_bind(void *page, warden_fresh_produce_cb produce,
void *ud, uint32_t max_stale_ms,
const char *source,
warden_fresh_render_cb render, void *widget)
{
if(!produce || !render) return NULL;
for(uint32_t i = 0; i < FRESH_MAX; i++) {
if(s_vals[i].used) continue;
struct warden_fresh *v = &s_vals[i];
memset(v, 0, sizeof *v);
v->page = page;
v->produce = produce;
v->ud = ud;
v->max_stale_ms = max_stale_ms;
v->source = source;
v->render = render;
v->widget = widget;
v->visible = true; /* bound while building the page that's about to show */
v->used = true;
return v;
}
return NULL; /* table full — caller surfaces a fault, never a stale value */
}
void warden_fresh_set_visible(void *page, bool visible)
{
for(uint32_t i = 0; i < FRESH_MAX; i++) {
if(s_vals[i].used && s_vals[i].page == page) s_vals[i].visible = visible;
}
}
void warden_fresh_page_show(void *page, uint32_t now_ms)
{
for(uint32_t i = 0; i < FRESH_MAX; i++) {
struct warden_fresh *v = &s_vals[i];
if(v->used && v->page == page) {
v->visible = true;
refresh_one(v, now_ms);
}
}
}
void warden_fresh_tick(uint32_t now_ms)
{
for(uint32_t i = 0; i < FRESH_MAX; i++) {
if(s_vals[i].used && s_vals[i].visible) refresh_one(&s_vals[i], now_ms);
}
}
void warden_fresh_invalidate(const char *source, uint32_t now_ms)
{
if(!source) return;
for(uint32_t i = 0; i < FRESH_MAX; i++) {
struct warden_fresh *v = &s_vals[i];
if(v->used && v->visible && v->source && strcmp(v->source, source) == 0) {
refresh_one(v, now_ms);
}
}
}
void warden_fresh_reset(void)
{
memset(s_vals, 0, sizeof s_vals);
}
uint32_t warden_fresh_count(void)
{
uint32_t n = 0;
for(uint32_t i = 0; i < FRESH_MAX; i++) if(s_vals[i].used) n++;
return n;
}
uint32_t warden_fresh_min_budget_ms(void)
{
/* `seen` — not `best == 0` — marks "nothing scanned yet", so a legitimate
* zero-tolerance binding (max_stale_ms == 0, "must be fresh every tick") wins
* the minimum instead of being mistaken for the empty sentinel and widened. */
uint32_t best = 0;
bool seen = false;
for(uint32_t i = 0; i < FRESH_MAX; i++) {
struct warden_fresh *v = &s_vals[i];
if(v->used && v->visible && (!seen || v->max_stale_ms < best)) {
best = v->max_stale_ms;
seen = true;
}
}
return best;
}