next iteration on display, still some artifacting issues, and ux with buttons states and updating

This commit is contained in:
matt committed 2026-08-04 19:38:58 -07:00
1 parent 45998b6176
commit ae5d12b3a2
14 files changed
+860 -398

No files matched your search

+114
View File
@@ -305,6 +305,120 @@ const char *ha_condition_short(const char *slug)
return (c != NULL) ? c->shrt : (slug != NULL ? slug : "--");
}
/* ------------------------------------------------------------------ trends */
/*
* 20 slots, one kept per ~4 minutes, covers ~80 minutes. Feeding faster than
* that overwrites the newest slot instead of consuming a new one, so a 60s
* poll cadence still fills the ring at 4-minute spacing.
*
* Size matters here more than it looks: this array is .bss, which lives in
* internal RAM on the S3 -- the same internal RAM the RGB panel's bounce
* buffers come from, with only a few KB of slack (README, memory notes). A
* naive 72-slot-per-poll ring cost 2.6KB and was enough to tip display_init
* into "no mem for bounce buffer" on an unlucky boot. Keep this structure
* small; if it ever needs to grow, it must move to PSRAM instead.
*
* Static per channel; a reboot forgets history, and the display shows no
* arrow until enough accumulates again (see ha_data.h).
*/
#define TREND_SLOTS 20
#define TREND_BUCKET_MS (4u * 60u * 1000u)
typedef struct {
float value;
uint32_t ms;
bool used;
} trend_sample_t;
static trend_sample_t trend_ring[HA_TREND_CHANNELS][TREND_SLOTS];
static int trend_head[HA_TREND_CHANNELS]; /* slot most recently written */
void ha_trend_feed(int channel, float value, uint32_t now_ms)
{
if (channel < 0 || channel >= HA_TREND_CHANNELS) {
return;
}
trend_sample_t *newest = &trend_ring[channel][trend_head[channel]];
/* Inside the current bucket: refresh the newest sample in place, so
* compute always sees the latest reading without spending a slot. */
if (newest->used && (now_ms - newest->ms) < TREND_BUCKET_MS) {
newest->value = value;
return;
}
const int next = newest->used ? (trend_head[channel] + 1) % TREND_SLOTS
: trend_head[channel];
trend_ring[channel][next] = (trend_sample_t){ value, now_ms, true };
trend_head[channel] = next;
}
ha_trend_t ha_trend_compute(int channel, float threshold_per_hour, uint32_t now_ms)
{
if (channel < 0 || channel >= HA_TREND_CHANNELS) {
return HA_TREND_UNKNOWN;
}
/* The newest sample is the one just fed; find the usable sample whose
* age is closest to the window. Unsigned subtraction keeps this correct
* across tick wrap. */
const trend_sample_t *newest = NULL;
const trend_sample_t *anchor = NULL;
uint32_t best_dist = UINT32_MAX;
for (int i = 0; i < TREND_SLOTS; i++) {
const trend_sample_t *s = &trend_ring[channel][i];
if (!s->used) {
continue;
}
const uint32_t age = now_ms - s->ms;
if (age > HA_TREND_WINDOW_MS + HA_TREND_MIN_AGE_MS) {
continue; /* too old even for a sparse ring; ignore */
}
if (newest == NULL || age < (now_ms - newest->ms)) {
newest = s;
}
if (age >= HA_TREND_MIN_AGE_MS) {
const uint32_t dist = (age > HA_TREND_WINDOW_MS)
? age - HA_TREND_WINDOW_MS
: HA_TREND_WINDOW_MS - age;
if (dist < best_dist) {
best_dist = dist;
anchor = s;
}
}
}
if (newest == NULL || anchor == NULL || newest == anchor) {
return HA_TREND_UNKNOWN;
}
/*
* Scale the threshold to the actual span so a 20-minute-old anchor uses
* a proportionally smaller bar than a full hour would. Without this the
* first hour after boot under-reports every trend.
*/
const uint32_t span_ms = anchor->ms == newest->ms
? 1
: newest->ms - anchor->ms;
const float threshold = threshold_per_hour *
((float)span_ms / (float)HA_TREND_WINDOW_MS);
const float delta = newest->value - anchor->value;
if (delta > threshold) {
return HA_TREND_RISING;
}
if (delta < -threshold) {
return HA_TREND_FALLING;
}
return HA_TREND_FLAT;
}
const char *ha_wind_cardinal(float degrees)
{
static const char *const points[16] = {