Merge dev into feature/eink-ui-scale

This commit is contained in:
pmow
2026-10-10 16:19:17 -10:00
26 changed files with 2836 additions and 116 deletions
+123 -6
View File
@@ -8,6 +8,20 @@
#include "../Features.h"
#include "../GeoUtils.h"
#include "target.h"
// The CardKB background poll task (see UITask::cardkbTask) shares its I2C bus
// with the environment sensors, and some screens query those sensors while
// rendering -- exactly when that task is active. Holding this lock around a
// sensor query makes the two take turns. No-op on builds without the task.
#if defined(CARDKB_I2C) && defined(NRF52_PLATFORM)
static void* g_cardkb_bus_mutex = nullptr; // SemaphoreHandle_t, set by startCardKBCapture()
struct CardKBBusLock {
CardKBBusLock() { if (g_cardkb_bus_mutex) xSemaphoreTake((SemaphoreHandle_t)g_cardkb_bus_mutex, portMAX_DELAY); }
~CardKBBusLock() { if (g_cardkb_bus_mutex) xSemaphoreGive((SemaphoreHandle_t)g_cardkb_bus_mutex); }
};
#else
struct CardKBBusLock { };
#endif
#ifdef WIFI_SSID
#include <WiFi.h>
#endif
@@ -173,6 +187,7 @@ static const telemetry::Style L1_INFO = { "\xc2\xb0", true, false, 5 };
#include "SensorPlaceholders.h"
#include "../ui-core/UiCore.h" // shared UI Core: history rings + unread models (MessagesScreen views them)
#include "../ui-core/SettingsSchema.h" // the settings both frontends share (SettingsScreen.h)
#include "../ui-core/SettingsCli.h" // and their companion CLI keys
#include "SettingsScreen.h"
#include "MessagesScreen.h"
@@ -828,7 +843,7 @@ class HomeScreen : public UIScreen {
sensors_lpp.reset();
sensors_nb = 0;
sensors_lpp.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
sensors.querySensors(0xFF, sensors_lpp);
{ CardKBBusLock bus_lock; sensors.querySensors(0xFF, sensors_lpp); }
LPPReader reader (sensors_lpp.getBuffer(), sensors_lpp.getSize());
uint8_t channel, type;
while(reader.readHeader(channel, type)) {
@@ -2044,6 +2059,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
#if defined(PIN_GPIO1)
gpio_screen = new GpioScreen(this, node_prefs);
#endif
startCardKBCapture(); // after every screen: an optional task must not starve one
applyBrightness();
applyRotation();
applyFullRefreshInterval();
@@ -2726,7 +2742,11 @@ static void formatDashVal(uint8_t field, char* val, int val_len, uint16_t batt_m
{ NodePrefs* np = the_mesh.getNodePrefs(); in.gps_on = np && np->gps_enabled; } // off: no stale satellite count
#endif
if (telemetry::isSensor(field)) {
if (!lpp) { static CayenneLPP s_lpp(200); s_lpp.reset(); sensors.querySensors(0xFF, s_lpp); lpp = &s_lpp; }
if (!lpp) {
static CayenneLPP s_lpp(200); s_lpp.reset();
{ CardKBBusLock bus_lock; sensors.querySensors(0xFF, s_lpp); }
lpp = &s_lpp;
}
in.lpp = lpp->getBuffer();
in.lpp_len = lpp->getSize();
}
@@ -2893,12 +2913,100 @@ void UITask::pollCardKB() {
// Polling every loop() iteration (same as a digital button's check(), which
// has no throttle either) just shrinks that miss window down to exactly the
// render() duration instead of render()+30ms.
// First, keys the background task captured while the display blocked, in order.
while (_cardkb_q_tail != _cardkb_q_head) {
uint8_t q = _cardkb_q[_cardkb_q_tail];
_cardkb_q_tail = (uint8_t)((_cardkb_q_tail + 1) % CARDKB_Q_LEN);
handleCardKBByte(q);
}
uint8_t raw;
if (!readCardKBEdge(raw)) return;
handleCardKBByte(raw);
#endif
}
#if defined(CARDKB_I2C)
bool UITask::readCardKBEdge(uint8_t& raw) {
CARDKB_I2C.requestFrom(0x5F, 1);
if (!CARDKB_I2C.available()) return;
uint8_t raw = CARDKB_I2C.read();
if (raw == _cardkb_last_raw) return; // still held (or still released) -- no new edge
if (!CARDKB_I2C.available()) return false;
raw = CARDKB_I2C.read();
if (raw == _cardkb_last_raw) return false; // still held (or still released) -- no new edge
_cardkb_last_raw = raw;
if (raw == 0) return; // key just released, nothing to enqueue
return raw != 0; // 0 = key just released, nothing to enqueue
}
// Single producer (the poll task) / single consumer (pollCardKB() in the
// loop) ring -- byte-sized indices, so no locking needed.
void UITask::cardkbQueuePush(uint8_t raw) {
uint8_t next = (uint8_t)((_cardkb_q_head + 1) % CARDKB_Q_LEN);
if (next == _cardkb_q_tail) return; // full: drop (16 keys within one refresh)
_cardkb_q[_cardkb_q_head] = raw;
_cardkb_q_head = next;
}
#if defined(NRF52_PLATFORM)
void UITask::cardkbTask(void* self) {
UITask* t = (UITask*)self;
SemaphoreHandle_t m = (SemaphoreHandle_t)t->_cardkb_mutex;
for (;;) {
if (t->_cardkb_bg_active && xSemaphoreTake(m, portMAX_DELAY) == pdTRUE) {
if (t->_cardkb_bg_active) { // re-check: loop may have just finished
uint8_t raw;
if (t->readCardKBEdge(raw)) t->cardkbQueuePush(raw);
}
xSemaphoreGive(m);
}
vTaskDelay(ms2tick(10)); // a keypress lasts ~50+ ms, so 10 ms never misses one
}
}
#endif
#endif
// Keep sampling the CardKB while the display blocks the loop, so keys typed
// during a (long, on big panels) refresh aren't lost. Called LAST in begin():
// the heap is tight on large-panel builds, and every screen is allocated with
// `new` -- a failed allocation leaves that screen silently unopenable. Creating
// the optional poll task afterwards means a short heap costs the task (the
// CardKB is then only read between frames, as without it), never a screen.
void UITask::startCardKBCapture() {
#if defined(CARDKB_I2C) && defined(NRF52_PLATFORM)
if (!_has_cardkb) return;
SemaphoreHandle_t m = xSemaphoreCreateMutex();
if (!m) return;
// Publish the mutex BEFORE creating the task: at higher priority it starts
// running inside xTaskCreate() and reads _cardkb_mutex straight away.
_cardkb_mutex = (void*)m;
// TASK_PRIO_NORMAL (2) preempts the loop task (TASK_PRIO_LOW, 1); BLE runs higher.
if (xTaskCreate(&UITask::cardkbTask, "cardkb", 512, this, TASK_PRIO_NORMAL, NULL) == pdPASS) {
g_cardkb_bus_mutex = m; // shared with CardKBBusLock (sensor queries)
return;
}
_cardkb_mutex = nullptr; // no task -> setCardKBBackground() stays a no-op
vSemaphoreDelete(m);
#endif
}
// Hand CardKB sampling to the background task (on = loop is about to block on
// the display) or take it back (off). Taking the mutex on the way out waits for
// any in-flight background I2C read to finish before the loop touches the bus.
void UITask::setCardKBBackground(bool on) {
#if defined(CARDKB_I2C) && defined(NRF52_PLATFORM)
if (!_cardkb_mutex) return;
if (on) {
_cardkb_bg_active = true;
} else {
SemaphoreHandle_t m = (SemaphoreHandle_t)_cardkb_mutex;
xSemaphoreTake(m, portMAX_DELAY);
_cardkb_bg_active = false;
xSemaphoreGive(m);
}
#else
(void)on;
#endif
}
void UITask::handleCardKBByte(uint8_t raw) {
#if defined(CARDKB_I2C)
// Compact mode (Settings > Keyboard's "Ext. KB" row) hides the letter grid
// entirely, and is meant to guarantee joystick-free operation: while it's
@@ -3466,6 +3574,7 @@ void UITask::loop() {
if (now != ext) { ext = now; _next_refresh = 0; } }
if (_display != NULL && _display->isOn()) {
setCardKBBackground(true); // keep sampling the keyboard while the frame blocks
// Lock-screen password prompt
if (_locked && _unlock_kb && (int32_t)(millis() - _lock_wake_until) >= 0) {
cancelUnlockPrompt(); // Cancel unlock attempt on idle
@@ -3534,6 +3643,7 @@ void UITask::loop() {
_display->endFrame();
PERF_T2();
}
setCardKBBackground(false);
#if AUTO_OFF_MILLIS > 0
#ifdef KEEP_DISPLAY_ON_USB
// Opt-in: refresh the auto-off deadline while externally powered, so the
@@ -4112,6 +4222,13 @@ void UITask::applySoundPrefs() {
#endif
}
// The companion CLI's settings keys (ui-core/SettingsCli.h).
bool UITask::cliCommand(const char* command, char* reply, int n) {
if (!settingscli::handle(*_core, command, reply, n)) return false;
_next_refresh = 0; // a screen showing the value draws it anew
return true;
}
void UITask::setBuzzerVolumeLevel(uint8_t level) {
#ifdef PIN_BUZZER
if (_node_prefs == NULL) return;