Add persistent OLED UI settings and recovery
This commit is contained in:
@@ -10,6 +10,18 @@ Use these commands from the UART0 `serial-tool>` administration console. Run `he
|
|||||||
| `reboot` | Drain console output briefly and restart the ESP32. |
|
| `reboot` | Drain console output briefly and restart the ESP32. |
|
||||||
| `status` | Show quick MAX3243 signal state. |
|
| `status` | Show quick MAX3243 signal state. |
|
||||||
|
|
||||||
|
## Local display
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---|---|
|
||||||
|
| `display status` | Show the runtime aging settings and OLED service state. |
|
||||||
|
| `display set dim-seconds <0..86400>` | Set the RAM inactivity delay before contrast drops to `1`; `0` disables dimming. |
|
||||||
|
| `display set off-seconds <0..86400>` | Set the RAM inactivity delay before the OLED switches off; `0` disables automatic off. |
|
||||||
|
| `display save` / `display load` | Save the working aging settings to NVS or load them. |
|
||||||
|
| `display defaults` / `display reset` | Apply 300/600-second defaults in RAM, or apply and persist them. |
|
||||||
|
|
||||||
|
When both transitions are enabled, `off-seconds` must be greater than `dim-seconds`. Applying settings counts as local UI activity. A missing OLED remains nonfatal; after reconnecting it safely, one new button press requests a bounded reprobe and is consumed without navigating.
|
||||||
|
|
||||||
## Serial service
|
## Serial service
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|
|||||||
@@ -157,6 +157,33 @@ For Wi-Fi lifecycle calls, confirm the immediate result says `Requested`, then u
|
|||||||
|
|
||||||
Run these checks with UART0 available. Repeat appropriate stop/revoke cases with USB CDC, WebSocket, and SSH clients connected; verify the intended session/service is interrupted, unrelated recovery paths remain responsive, and no action injects serial data.
|
Run these checks with UART0 available. Repeat appropriate stop/revoke cases with USB CDC, WebSocket, and SSH clients connected; verify the intended session/service is interrupted, unrelated recovery paths remain responsive, and no action injects serial data.
|
||||||
|
|
||||||
|
### 10. Phase 7E persistence and fault recovery
|
||||||
|
|
||||||
|
First shorten the delays for a bounded aging-policy test:
|
||||||
|
|
||||||
|
```text
|
||||||
|
display status
|
||||||
|
display set dim-seconds 5
|
||||||
|
display set off-seconds 10
|
||||||
|
display save
|
||||||
|
reboot
|
||||||
|
```
|
||||||
|
|
||||||
|
After reboot, `display status` must report the stored 5/10-second values. Verify dim at five seconds, off at ten seconds, and one consumed wake press. Run `display load`, then test `display defaults` without saving and confirm 300/600 seconds are restored only in RAM. Run `display reset`, reboot, and confirm the defaults persisted. Verify invalid combinations such as dim `10` with off `5` are rejected without changing the working values. Set either timeout to `0`, save/reboot, and confirm that transition is disabled; finish with `display reset`.
|
||||||
|
|
||||||
|
Exercise the following fault matrix while keeping UART0 available:
|
||||||
|
|
||||||
|
| Fault/stress | Procedure | Required result |
|
||||||
|
|---|---|---|
|
||||||
|
| OLED absent at boot | Power down, remove the OLED, then boot. | UART0, serial, USB, Wi-Fi, HTTPS/WebSocket, and SSH startup remain independent; no reset or probe loop occurs. |
|
||||||
|
| Reattach | Power down before reconnecting loose wiring, boot, and press one button. If using a connector explicitly suitable for live removal, avoid shorts and reconnect before pressing. | One bounded reprobe initializes the panel; the wake press does not navigate or execute an action. |
|
||||||
|
| NACK/timeout | With safe test wiring, interrupt the display during refresh or hold one bus line low briefly, then release it. | One bounded frame fails and marks the OLED unavailable; no watchdog reset, repeated log flood, serial loss, or broker ownership change occurs. A later new press can recover after the bus is healthy. |
|
||||||
|
| Stuck button | Hold each button continuously for at least ten seconds, then operate each of the other buttons. | The held input is quarantined, the other controls remain usable, and no action repeats. Release and debounce the held input; it must rearm without rebooting. |
|
||||||
|
| Real chord | Press two healthy buttons together before either is quarantined. | The chord is ignored and cannot confirm an action. |
|
||||||
|
| Repeated actions | Re-enter Controls and repeat start/stop, reconnect, next-profile, writer-revoke, and display-off requests during transitions. | Each confirmation hold executes once; stale state produces an error rather than the opposite action; queue saturation, if reached, reports an error and leaves the Wi-Fi alert/counter visible. |
|
||||||
|
|
||||||
|
For the final concurrency regression, connect USB CDC, one WebSocket terminal, and one SSH terminal while UART1 carries sustained bidirectional traffic. Navigate the UI, run the persistence checks, exercise service stop/start and writer revocation, and inject the display/stuck-button faults above. Confirm UART0 remains responsive, the UI never appears as a broker client or injects serial bytes, writer ownership is always the expected client or none, and serial/broker drop counters do not increase unexpectedly. Record `memory`, transport counters, `wifi counters`, and `debug display status` before and after the run.
|
||||||
|
|
||||||
## Configuration A: data and handshake pairs
|
## Configuration A: data and handshake pairs
|
||||||
|
|
||||||
Connect the following pairs:
|
Connect the following pairs:
|
||||||
|
|||||||
+11
-9
@@ -37,7 +37,7 @@ These constraints apply across all phases:
|
|||||||
| 5A | Authenticated HTTPS administration foundation | **Complete** |
|
| 5A | Authenticated HTTPS administration foundation | **Complete** |
|
||||||
| 5B | Offline xterm.js WebSocket serial terminal | **Complete** |
|
| 5B | Offline xterm.js WebSocket serial terminal | **Complete** |
|
||||||
| 6 | Authenticated SSH serial transport | **Complete** |
|
| 6 | Authenticated SSH serial transport | **Complete** |
|
||||||
| 7 | Local display and button interface | **In progress (7D)** |
|
| 7 | Local display and button interface | **In progress (7E validation)** |
|
||||||
| 8 | Security and production hardening | **Planned** |
|
| 8 | Security and production hardening | **Planned** |
|
||||||
| 9 | Authenticated, rollback-capable OTA | **Planned** |
|
| 9 | Authenticated, rollback-capable OTA | **Planned** |
|
||||||
| 10 | BLE serial transport and provisioning evaluation | **Planned** |
|
| 10 | BLE serial transport and provisioning evaluation | **Planned** |
|
||||||
@@ -184,7 +184,7 @@ The order below is the current plan. Phase 7 is in progress; later phases remain
|
|||||||
|
|
||||||
Add a standalone local status/control interface without making it a dependency of the serial core. The planning baseline uses a 128×64 dual-color monochrome I²C OLED sold with an SSD1315 controller. Phase 7A confirmed SSD1306-compatible operation, 7-bit I²C address `0x3c`, orientation, column mapping, contrast/inversion behavior, button inputs, and the physical color geometry on the selected hardware.
|
Add a standalone local status/control interface without making it a dependency of the serial core. The planning baseline uses a 128×64 dual-color monochrome I²C OLED sold with an SSD1315 controller. Phase 7A confirmed SSD1306-compatible operation, 7-bit I²C address `0x3c`, orientation, column mapping, contrast/inversion behavior, button inputs, and the physical color geometry on the selected hardware.
|
||||||
|
|
||||||
Phase 7A diagnostics and target-hardware electrical validation are complete. Phases 7B and 7C are complete; Phase 7 overall remains in progress (7D). Phase 7D is implemented and awaiting validation; Phase 7E is not complete.
|
Phase 7A diagnostics and target-hardware electrical validation are complete. Phases 7B, 7C, and 7D are complete. Phase 7E is implemented and awaiting target-hardware fault and persistence validation.
|
||||||
|
|
||||||
#### Hardware baseline
|
#### Hardware baseline
|
||||||
|
|
||||||
@@ -220,17 +220,19 @@ The persistent yellow strip uses fixed-position serial, Wi-Fi-strength, USB, Web
|
|||||||
- To reduce OLED aging, inactivity dims contrast to `1` after five minutes and switches the panel off after ten minutes. The first debounced button press restores contrast `127` and wakes without navigating.
|
- To reduce OLED aging, inactivity dims contrast to `1` after five minutes and switches the panel off after ten minutes. The first debounced button press restores contrast `127` and wakes without navigating.
|
||||||
- A missing/unresponsive display is nonfatal; the task remains read-only and never acquires serial writer ownership.
|
- A missing/unresponsive display is nonfatal; the task remains read-only and never acquires serial writer ownership.
|
||||||
- Target-hardware validation passed for the four pages, fixed active-only status icons, lowercase layout, diagnostics hold, dim/off/wake behavior, all available concurrent client interfaces, UART0 recovery, and serial traffic from 9600 through 230400 baud.
|
- Target-hardware validation passed for the four pages, fixed active-only status icons, lowercase layout, diagnostics hold, dim/off/wake behavior, all available concurrent client interfaces, UART0 recovery, and serial traffic from 9600 through 230400 baud.
|
||||||
4. **Phase 7D — Local controls — Implemented; validation pending**
|
4. **Phase 7D — Local controls — Complete**
|
||||||
- A shallow local `Controls` menu directly invokes public serial-service, Wi-Fi, HTTPS, SSH, broker, display, and restart APIs; it never parses or injects CLI text.
|
- A shallow local `Controls` menu directly invokes public serial-service, Wi-Fi, HTTPS, SSH, broker, display, and restart APIs; it never parses or injects CLI text.
|
||||||
- The menu provides serial start/stop, Wi-Fi start/stop/reconnect/next-profile, HTTPS start/stop, SSH start/stop, emergency current-writer revocation, display off, and reboot. `WiFi:next profile` queues a rotation to the enabled profile after the currently active one in priority order, wrapping safely; profile configuration and credentials remain unavailable to the UI. The menu deliberately excludes serial configuration editing, Wi-Fi profile/AP changes, host-key/certificate changes, I²C scans, and assignment of the writer to another client.
|
- The menu provides serial start/stop, Wi-Fi start/stop/reconnect/next-profile, HTTPS start/stop, SSH start/stop, emergency current-writer revocation, display off, and reboot. `WiFi:next profile` queues a rotation to the enabled profile after the currently active one in priority order, wrapping safely; profile configuration and credentials remain unavailable to the UI. The menu deliberately excludes serial configuration editing, Wi-Fi profile/AP changes, host-key/certificate changes, I²C scans, and assignment of the writer to another client.
|
||||||
- A visible confirmation screen and one continuous two-second Select hold are required for service stops, Wi-Fi reconnect, Wi-Fi next-profile, writer revocation, and reboot. The confirmation times out after 30 seconds; previous/back cancels. A wake press is consumed and cannot navigate or confirm an action.
|
- A visible confirmation screen and one continuous two-second Select hold are required for service stops, Wi-Fi reconnect, Wi-Fi next-profile, writer revocation, and reboot. The confirmation times out after 30 seconds; previous/back cancels. A wake press is consumed and cannot navigate or confirm an action.
|
||||||
- Wi-Fi lifecycle results report a queued request rather than falsely claiming an asynchronous transition has completed. HTTPS/SSH start requests require a connected station or active AP. The current writer is only ever force-released to no writer; the UI is not a broker client and cannot acquire or assign a writer lease.
|
- Wi-Fi lifecycle results report a queued request rather than falsely claiming an asynchronous transition has completed. HTTPS/SSH start requests require a connected station or active AP. The current writer is only ever force-released to no writer; the UI is not a broker client and cannot acquire or assign a writer lease.
|
||||||
- Pending target-hardware validation: menu navigation/timeouts, each enabled action and error result, confirmation cancellation/chord rejection, wake-press suppression, and preservation of UART0 plus transport recovery.
|
- Target-hardware validation passed for menu navigation and timeout behavior, controls and asynchronous results, two-second confirmations and cancellation, Wi-Fi next-profile progression/wrap, chord rejection, wake-press suppression, and preservation of UART0 plus transport recovery.
|
||||||
5. **Phase 7E — Reliability, persistence, and documentation — Planned**
|
5. **Phase 7E — Reliability, persistence, and documentation — Implemented; validation pending**
|
||||||
- Make the implemented contrast dim/blank timeouts configurable and persistent without making the display necessary for recovery.
|
- A versioned `local_ui` NVS configuration makes the dim and display-off inactivity delays independently configurable from 0 through 86400 seconds. Zero disables a transition; defaults remain 300/600 seconds. UART0 `display` commands provide status, RAM edits, save/load, defaults, and atomic reset behavior.
|
||||||
- Validate display removal, I²C NACK/timeouts, stuck buttons, queue saturation, and repeated actions.
|
- A button held for ten seconds is quarantined until its debounced release, after which it rearms. Quarantined inputs no longer keep a chord latched or block the other controls. A confirmation hold can execute at most once.
|
||||||
- Re-run concurrent USB CDC, WebSocket, and SSH traffic while the UI refreshes and confirm UART0 remains responsive.
|
- Dim/off wake and missing-display recovery now occur only on a new debounced press edge. A held or stuck input cannot continuously probe an absent OLED, flood logs, or indefinitely refresh the inactivity timer; the wake gesture remains consumed.
|
||||||
- Update wiring, electrical-test, command-reference, and recovery documentation.
|
- Dirty framebuffer commits have a 500 ms scheduling budget in addition to each bounded 250 ms I²C transaction. A failed frame marks the panel unavailable, while serial, broker, network, USB, and UART0 services remain independent. Safely reconnecting the OLED and pressing a button requests one bounded reprobe.
|
||||||
|
- Wi-Fi manager queue drops now activate the persistent yellow alert slot and remain observable through Wi-Fi counters. Local action errors continue to be reported directly without blocking queue insertion.
|
||||||
|
- Pending target-hardware validation: configuration persistence/disable semantics, absent and reattached display behavior, I²C NACK/timeout handling, each stuck button and rearm path, repeated actions/queue-error indication, and concurrent USB CDC, WebSocket, SSH, serial, and UART0 operation during faults.
|
||||||
|
|
||||||
Completion requires electrical validation on the selected module, correct operation with all current transports, bounded display/button failure behavior, safe action confirmation, no unexplained serial or broker loss, and documented UART0 recovery.
|
Completion requires electrical validation on the selected module, correct operation with all current transports, bounded display/button failure behavior, safe action confirmation, no unexplained serial or broker loss, and documented UART0 recovery.
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -127,13 +127,13 @@ GPIO13 ───── select/confirm button ──── GND
|
|||||||
GPIO14 ───── next button ────────────── GND
|
GPIO14 ───── next button ────────────── GND
|
||||||
```
|
```
|
||||||
|
|
||||||
> **OLED voltage warning:** Power OLED `VCC` from `3V3`, not 5 V. Many OLED modules connect their SDA/SCL pull-up resistors directly to `VCC`; powering such a module from 5 V could expose the ESP32-S3 GPIOs to unsafe I²C levels.
|
> **OLED voltage warning:** Power OLED `VCC` from `3V3`, not 5 V. Many OLED modules connect their SDA/SCL pull-up resistors directly to `VCC`; powering such a module from 5 V could expose the ESP32-S3 GPIOs to unsafe I²C levels. Power down before attaching or removing loose OLED wiring. Only perform live-removal fault tests with a connector designed to avoid shorts and unintended pin sequencing.
|
||||||
|
|
||||||
Before applying power, verify whether the module already includes SDA and SCL pull-ups and where they terminate. Any module-mounted or external I²C pull-ups must go to 3.3 V. If pull-ups are absent, add suitable external pull-ups from SDA and SCL to `3V3`; if they are present, account for their parallel resistance before adding more. The buttons normally need no external pull-ups because firmware enables the ESP32 internal pull-ups.
|
Before applying power, verify whether the module already includes SDA and SCL pull-ups and where they terminate. Any module-mounted or external I²C pull-ups must go to 3.3 V. If pull-ups are absent, add suitable external pull-ups from SDA and SCL to `3V3`; if they are present, account for their parallel resistance before adding more. The buttons normally need no external pull-ups because firmware enables the ESP32 internal pull-ups.
|
||||||
|
|
||||||
## Electrical verification
|
## Electrical verification
|
||||||
|
|
||||||
See [Electrical tests](electrical_tests.md) for Phase 7A OLED/button bring-up, safe loopback wiring, polarity checks, UART flow-control verification, and session-broker loopback testing.
|
See [Electrical tests](electrical_tests.md) for Phase 7 OLED/button bring-up, persistent aging settings, optional-display and stuck-button fault recovery, safe loopback wiring, polarity checks, UART flow-control verification, and session-broker/concurrent-transport testing.
|
||||||
|
|
||||||
## Future hardware profiles
|
## Future hardware profiles
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ idf_component_register(
|
|||||||
"status_led.c"
|
"status_led.c"
|
||||||
"local_display.c"
|
"local_display.c"
|
||||||
"local_status_ui.c"
|
"local_status_ui.c"
|
||||||
|
"local_ui_config.c"
|
||||||
|
"local_ui_console.c"
|
||||||
"local_ui_hw_test.c"
|
"local_ui_hw_test.c"
|
||||||
"rs232_hw_test.c"
|
"rs232_hw_test.c"
|
||||||
"rs232_port_owner.c"
|
"rs232_port_owner.c"
|
||||||
|
|||||||
@@ -53,6 +53,16 @@ static const char *const s_completion_candidates[] = {
|
|||||||
"debug buttons status",
|
"debug buttons status",
|
||||||
"debug buttons test",
|
"debug buttons test",
|
||||||
|
|
||||||
|
/* Persistent local OLED aging settings. */
|
||||||
|
"display status",
|
||||||
|
"display set",
|
||||||
|
"display set dim-seconds",
|
||||||
|
"display set off-seconds",
|
||||||
|
"display save",
|
||||||
|
"display load",
|
||||||
|
"display defaults",
|
||||||
|
"display reset",
|
||||||
|
|
||||||
/* Serial service lifecycle, persistence, counters, and settings. */
|
/* Serial service lifecycle, persistence, counters, and settings. */
|
||||||
"serial status",
|
"serial status",
|
||||||
"serial start",
|
"serial start",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
#include "board_pins.h"
|
#include "board_pins.h"
|
||||||
#include "driver/i2c_master.h"
|
#include "driver/i2c_master.h"
|
||||||
|
#include "esp_timer.h"
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/semphr.h"
|
#include "freertos/semphr.h"
|
||||||
#include "freertos/task.h"
|
#include "freertos/task.h"
|
||||||
@@ -21,6 +22,7 @@
|
|||||||
#define LOCAL_DISPLAY_DEFAULT_CONTRAST 127U
|
#define LOCAL_DISPLAY_DEFAULT_CONTRAST 127U
|
||||||
#define LOCAL_DISPLAY_COMMAND_CAPACITY 32U
|
#define LOCAL_DISPLAY_COMMAND_CAPACITY 32U
|
||||||
#define LOCAL_DISPLAY_DATA_CHUNK_SIZE 128U
|
#define LOCAL_DISPLAY_DATA_CHUNK_SIZE 128U
|
||||||
|
#define LOCAL_DISPLAY_FLUSH_BUDGET_US (500LL * 1000LL)
|
||||||
|
|
||||||
static const uint8_t s_expected_addresses[] = {0x3cU, 0x3dU};
|
static const uint8_t s_expected_addresses[] = {0x3cU, 0x3dU};
|
||||||
|
|
||||||
@@ -172,6 +174,7 @@ static esp_err_t flush_dirty_locked(void)
|
|||||||
return ESP_ERR_INVALID_STATE;
|
return ESP_ERR_INVALID_STATE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int64_t started = esp_timer_get_time();
|
||||||
uint8_t transfer[LOCAL_DISPLAY_DATA_CHUNK_SIZE + 1U];
|
uint8_t transfer[LOCAL_DISPLAY_DATA_CHUNK_SIZE + 1U];
|
||||||
transfer[0] = 0x40U;
|
transfer[0] = 0x40U;
|
||||||
for (uint8_t page = 0U; page < LOCAL_DISPLAY_PAGE_COUNT; ++page) {
|
for (uint8_t page = 0U; page < LOCAL_DISPLAY_PAGE_COUNT; ++page) {
|
||||||
@@ -179,6 +182,9 @@ static esp_err_t flush_dirty_locked(void)
|
|||||||
if ((s_dirty_pages & page_mask) == 0U) {
|
if ((s_dirty_pages & page_mask) == 0U) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if ((esp_timer_get_time() - started) >= LOCAL_DISPLAY_FLUSH_BUDGET_US) {
|
||||||
|
return ESP_ERR_TIMEOUT;
|
||||||
|
}
|
||||||
|
|
||||||
const uint8_t commands[] = {
|
const uint8_t commands[] = {
|
||||||
0x21U, 0x00U, (uint8_t)(LOCAL_DISPLAY_WIDTH - 1U),
|
0x21U, 0x00U, (uint8_t)(LOCAL_DISPLAY_WIDTH - 1U),
|
||||||
@@ -188,6 +194,9 @@ static esp_err_t flush_dirty_locked(void)
|
|||||||
if (error != ESP_OK) {
|
if (error != ESP_OK) {
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
if ((esp_timer_get_time() - started) >= LOCAL_DISPLAY_FLUSH_BUDGET_US) {
|
||||||
|
return ESP_ERR_TIMEOUT;
|
||||||
|
}
|
||||||
memcpy(&transfer[1], &s_framebuffer[(size_t)page * LOCAL_DISPLAY_WIDTH],
|
memcpy(&transfer[1], &s_framebuffer[(size_t)page * LOCAL_DISPLAY_WIDTH],
|
||||||
LOCAL_DISPLAY_WIDTH);
|
LOCAL_DISPLAY_WIDTH);
|
||||||
error = i2c_master_transmit(s_device, transfer, sizeof(transfer),
|
error = i2c_master_transmit(s_device, transfer, sizeof(transfer),
|
||||||
|
|||||||
+82
-24
@@ -35,8 +35,6 @@
|
|||||||
#define LOCAL_STATUS_UI_TEXT_CAPACITY 22U
|
#define LOCAL_STATUS_UI_TEXT_CAPACITY 22U
|
||||||
#define LOCAL_STATUS_UI_CLIENT_ROWS 4U
|
#define LOCAL_STATUS_UI_CLIENT_ROWS 4U
|
||||||
#define LOCAL_STATUS_UI_DIAGNOSTIC_HOLD_MS 30000U
|
#define LOCAL_STATUS_UI_DIAGNOSTIC_HOLD_MS 30000U
|
||||||
#define LOCAL_STATUS_UI_DIM_DELAY_MS (5U * 60U * 1000U)
|
|
||||||
#define LOCAL_STATUS_UI_OFF_DELAY_MS (10U * 60U * 1000U)
|
|
||||||
#define LOCAL_STATUS_UI_DIM_CONTRAST 1U
|
#define LOCAL_STATUS_UI_DIM_CONTRAST 1U
|
||||||
#define LOCAL_STATUS_UI_ACTIVE_CONTRAST 127U
|
#define LOCAL_STATUS_UI_ACTIVE_CONTRAST 127U
|
||||||
#define LOCAL_STATUS_UI_ICON_SIZE 8U
|
#define LOCAL_STATUS_UI_ICON_SIZE 8U
|
||||||
@@ -45,6 +43,7 @@
|
|||||||
#define LOCAL_STATUS_UI_MENU_TIMEOUT_MS 30000U
|
#define LOCAL_STATUS_UI_MENU_TIMEOUT_MS 30000U
|
||||||
#define LOCAL_STATUS_UI_RESULT_TIMEOUT_MS 2500U
|
#define LOCAL_STATUS_UI_RESULT_TIMEOUT_MS 2500U
|
||||||
#define LOCAL_STATUS_UI_CONFIRM_HOLD_MS 2000U
|
#define LOCAL_STATUS_UI_CONFIRM_HOLD_MS 2000U
|
||||||
|
#define LOCAL_STATUS_UI_STUCK_BUTTON_MS 10000U
|
||||||
|
|
||||||
static const char *TAG = "local_status_ui";
|
static const char *TAG = "local_status_ui";
|
||||||
|
|
||||||
@@ -117,6 +116,7 @@ typedef struct {
|
|||||||
TickType_t pressed_since;
|
TickType_t pressed_since;
|
||||||
bool long_emitted;
|
bool long_emitted;
|
||||||
bool suppress_gesture;
|
bool suppress_gesture;
|
||||||
|
bool quarantined;
|
||||||
} local_status_button_state_t;
|
} local_status_button_state_t;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
@@ -201,6 +201,8 @@ static portMUX_TYPE s_timing_mux = portMUX_INITIALIZER_UNLOCKED;
|
|||||||
static bool s_diagnostic_hold_active;
|
static bool s_diagnostic_hold_active;
|
||||||
static TickType_t s_diagnostic_hold_started;
|
static TickType_t s_diagnostic_hold_started;
|
||||||
static uint32_t s_external_activity_sequence;
|
static uint32_t s_external_activity_sequence;
|
||||||
|
static local_ui_config_t s_config;
|
||||||
|
static bool s_config_available;
|
||||||
|
|
||||||
static const gpio_num_t s_button_gpios[LOCAL_STATUS_BUTTON_COUNT] = {
|
static const gpio_num_t s_button_gpios[LOCAL_STATUS_BUTTON_COUNT] = {
|
||||||
LOCAL_UI_BUTTON_PREVIOUS_GPIO,
|
LOCAL_UI_BUTTON_PREVIOUS_GPIO,
|
||||||
@@ -417,7 +419,9 @@ static bool snapshot_has_alert(const local_status_snapshot_t *snapshot)
|
|||||||
return !snapshot->broker_available || !snapshot->usb_available ||
|
return !snapshot->broker_available || !snapshot->usb_available ||
|
||||||
!snapshot->wifi_available || !snapshot->web_available ||
|
!snapshot->wifi_available || !snapshot->web_available ||
|
||||||
!snapshot->web_serial_available || !snapshot->ssh_available ||
|
!snapshot->web_serial_available || !snapshot->ssh_available ||
|
||||||
(snapshot->wifi_available && snapshot->wifi.last_error != ESP_OK) ||
|
(snapshot->wifi_available &&
|
||||||
|
(snapshot->wifi.last_error != ESP_OK ||
|
||||||
|
snapshot->wifi.counters.queue_drops != 0U)) ||
|
||||||
(snapshot->web_available && snapshot->web.last_error != ESP_OK) ||
|
(snapshot->web_available && snapshot->web.last_error != ESP_OK) ||
|
||||||
(snapshot->ssh_available && snapshot->ssh.last_error != ESP_OK) ||
|
(snapshot->ssh_available && snapshot->ssh.last_error != ESP_OK) ||
|
||||||
snapshot->serial_counters.rx_dropped_bytes != 0U ||
|
snapshot->serial_counters.rx_dropped_bytes != 0U ||
|
||||||
@@ -927,9 +931,16 @@ static bool poll_button_event(local_status_button_state_t states[LOCAL_STATUS_BU
|
|||||||
}
|
}
|
||||||
if (!states[i].stable_pressed) {
|
if (!states[i].stable_pressed) {
|
||||||
states[i].suppress_gesture = false;
|
states[i].suppress_gesture = false;
|
||||||
|
states[i].quarantined = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (states[i].stable_pressed) {
|
if (states[i].stable_pressed && !states[i].quarantined &&
|
||||||
|
(now - states[i].pressed_since) >=
|
||||||
|
pdMS_TO_TICKS(LOCAL_STATUS_UI_STUCK_BUTTON_MS)) {
|
||||||
|
states[i].quarantined = true;
|
||||||
|
states[i].suppress_gesture = true;
|
||||||
|
}
|
||||||
|
if (states[i].stable_pressed && !states[i].quarantined) {
|
||||||
++pressed_count;
|
++pressed_count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -945,7 +956,7 @@ static bool poll_button_event(local_status_button_state_t states[LOCAL_STATUS_BU
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
for (size_t i = 0U; i < LOCAL_STATUS_BUTTON_COUNT; ++i) {
|
for (size_t i = 0U; i < LOCAL_STATUS_BUTTON_COUNT; ++i) {
|
||||||
if (pressed_edge[i]) {
|
if (pressed_edge[i] && !states[i].quarantined) {
|
||||||
*event = (local_status_button_event_t){
|
*event = (local_status_button_event_t){
|
||||||
.button = (local_status_button_t)i,
|
.button = (local_status_button_t)i,
|
||||||
.type = LOCAL_STATUS_BUTTON_EVENT_PRESS,
|
.type = LOCAL_STATUS_BUTTON_EVENT_PRESS,
|
||||||
@@ -954,7 +965,8 @@ static bool poll_button_event(local_status_button_state_t states[LOCAL_STATUS_BU
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (size_t i = 0U; i < LOCAL_STATUS_BUTTON_COUNT; ++i) {
|
for (size_t i = 0U; i < LOCAL_STATUS_BUTTON_COUNT; ++i) {
|
||||||
if (states[i].stable_pressed && !states[i].long_emitted &&
|
if (states[i].stable_pressed && !states[i].quarantined &&
|
||||||
|
!states[i].long_emitted &&
|
||||||
!states[i].suppress_gesture &&
|
!states[i].suppress_gesture &&
|
||||||
(now - states[i].pressed_since) >= pdMS_TO_TICKS(LOCAL_STATUS_UI_CONFIRM_HOLD_MS)) {
|
(now - states[i].pressed_since) >= pdMS_TO_TICKS(LOCAL_STATUS_UI_CONFIRM_HOLD_MS)) {
|
||||||
states[i].long_emitted = true;
|
states[i].long_emitted = true;
|
||||||
@@ -1196,13 +1208,16 @@ static void handle_button_event(local_status_ui_state_t *state,
|
|||||||
|
|
||||||
static void update_display_power(local_status_power_t *power,
|
static void update_display_power(local_status_power_t *power,
|
||||||
TickType_t now,
|
TickType_t now,
|
||||||
TickType_t last_activity)
|
TickType_t last_activity,
|
||||||
|
const local_ui_config_t *config)
|
||||||
{
|
{
|
||||||
TickType_t inactive = now - last_activity;
|
TickType_t inactive = now - last_activity;
|
||||||
bool should_stop = *power != LOCAL_STATUS_POWER_OFF &&
|
bool should_stop = config->off_timeout_seconds != 0U &&
|
||||||
inactive >= pdMS_TO_TICKS(LOCAL_STATUS_UI_OFF_DELAY_MS);
|
*power != LOCAL_STATUS_POWER_OFF &&
|
||||||
bool should_dim = *power == LOCAL_STATUS_POWER_ACTIVE &&
|
inactive >= pdMS_TO_TICKS(config->off_timeout_seconds * 1000U);
|
||||||
inactive >= pdMS_TO_TICKS(LOCAL_STATUS_UI_DIM_DELAY_MS);
|
bool should_dim = config->dim_timeout_seconds != 0U &&
|
||||||
|
*power == LOCAL_STATUS_POWER_ACTIVE &&
|
||||||
|
inactive >= pdMS_TO_TICKS(config->dim_timeout_seconds * 1000U);
|
||||||
if ((!should_stop && !should_dim) || s_diagnostic_gate == NULL ||
|
if ((!should_stop && !should_dim) || s_diagnostic_gate == NULL ||
|
||||||
xSemaphoreTake(s_diagnostic_gate, pdMS_TO_TICKS(100U)) != pdTRUE) {
|
xSemaphoreTake(s_diagnostic_gate, pdMS_TO_TICKS(100U)) != pdTRUE) {
|
||||||
return;
|
return;
|
||||||
@@ -1267,21 +1282,14 @@ static void local_status_ui_task(void *context)
|
|||||||
last_activity = now;
|
last_activity = now;
|
||||||
}
|
}
|
||||||
sync_display_power(&power);
|
sync_display_power(&power);
|
||||||
bool wake_gesture = false;
|
if (diagnostics_held) {
|
||||||
if (power != LOCAL_STATUS_POWER_ACTIVE) {
|
|
||||||
for (size_t i = 0U; i < LOCAL_STATUS_BUTTON_COUNT; ++i) {
|
for (size_t i = 0U; i < LOCAL_STATUS_BUTTON_COUNT; ++i) {
|
||||||
if (buttons[i].stable_pressed) {
|
if (buttons[i].stable_pressed) {
|
||||||
buttons[i].suppress_gesture = true;
|
buttons[i].suppress_gesture = true;
|
||||||
wake_gesture = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (wake_gesture && !diagnostics_held) {
|
|
||||||
last_activity = now;
|
|
||||||
render_requested = wake_display(&power);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (have_event) {
|
||||||
if (have_event && !wake_gesture) {
|
|
||||||
last_activity = now;
|
last_activity = now;
|
||||||
if (!diagnostics_held) {
|
if (!diagnostics_held) {
|
||||||
local_status_snapshot_t snapshot;
|
local_status_snapshot_t snapshot;
|
||||||
@@ -1309,7 +1317,11 @@ static void local_status_ui_task(void *context)
|
|||||||
render_requested = true;
|
render_requested = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
update_display_power(&power, now, last_activity);
|
local_ui_config_t config;
|
||||||
|
portENTER_CRITICAL(&s_timing_mux);
|
||||||
|
config = s_config;
|
||||||
|
portEXIT_CRITICAL(&s_timing_mux);
|
||||||
|
update_display_power(&power, now, last_activity, &config);
|
||||||
diagnostics_held = diagnostic_hold_is_active(now);
|
diagnostics_held = diagnostic_hold_is_active(now);
|
||||||
bool rendered = false;
|
bool rendered = false;
|
||||||
if (power != LOCAL_STATUS_POWER_OFF && !diagnostics_held &&
|
if (power != LOCAL_STATUS_POWER_OFF && !diagnostics_held &&
|
||||||
@@ -1344,14 +1356,55 @@ void local_status_ui_hold_for_diagnostics(void)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t local_status_ui_start(void)
|
esp_err_t local_status_ui_get_config(local_ui_config_t *config)
|
||||||
{
|
{
|
||||||
|
if (config == NULL) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
portENTER_CRITICAL(&s_timing_mux);
|
||||||
|
bool available = s_config_available;
|
||||||
|
*config = s_config;
|
||||||
|
portEXIT_CRITICAL(&s_timing_mux);
|
||||||
|
return available ? ESP_OK : ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t local_status_ui_apply_config(const local_ui_config_t *config)
|
||||||
|
{
|
||||||
|
esp_err_t error = local_ui_config_validate(config);
|
||||||
|
if (error != ESP_OK) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
portENTER_CRITICAL(&s_timing_mux);
|
||||||
|
if (!s_config_available) {
|
||||||
|
portEXIT_CRITICAL(&s_timing_mux);
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
s_config = *config;
|
||||||
|
++s_external_activity_sequence;
|
||||||
|
portEXIT_CRITICAL(&s_timing_mux);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t local_status_ui_start(const local_ui_config_t *config)
|
||||||
|
{
|
||||||
|
esp_err_t error = local_ui_config_validate(config);
|
||||||
|
if (error != ESP_OK) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
if (s_task != NULL || s_diagnostic_gate != NULL) {
|
if (s_task != NULL || s_diagnostic_gate != NULL) {
|
||||||
return ESP_ERR_INVALID_STATE;
|
return ESP_ERR_INVALID_STATE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
portENTER_CRITICAL(&s_timing_mux);
|
||||||
|
s_config = *config;
|
||||||
|
s_config_available = true;
|
||||||
|
portEXIT_CRITICAL(&s_timing_mux);
|
||||||
|
|
||||||
s_diagnostic_gate = xSemaphoreCreateMutexStatic(&s_diagnostic_gate_storage);
|
s_diagnostic_gate = xSemaphoreCreateMutexStatic(&s_diagnostic_gate_storage);
|
||||||
if (s_diagnostic_gate == NULL) {
|
if (s_diagnostic_gate == NULL) {
|
||||||
|
portENTER_CRITICAL(&s_timing_mux);
|
||||||
|
s_config_available = false;
|
||||||
|
portEXIT_CRITICAL(&s_timing_mux);
|
||||||
return ESP_ERR_NO_MEM;
|
return ESP_ERR_NO_MEM;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1362,11 +1415,16 @@ esp_err_t local_status_ui_start(void)
|
|||||||
s_task = NULL;
|
s_task = NULL;
|
||||||
vSemaphoreDelete(s_diagnostic_gate);
|
vSemaphoreDelete(s_diagnostic_gate);
|
||||||
s_diagnostic_gate = NULL;
|
s_diagnostic_gate = NULL;
|
||||||
|
portENTER_CRITICAL(&s_timing_mux);
|
||||||
|
s_config_available = false;
|
||||||
|
portEXIT_CRITICAL(&s_timing_mux);
|
||||||
return ESP_ERR_NO_MEM;
|
return ESP_ERR_NO_MEM;
|
||||||
}
|
}
|
||||||
|
|
||||||
ESP_LOGI(TAG,
|
ESP_LOGI(TAG,
|
||||||
"Read-only status UI started at %u Hz maximum; dim/off after 5/10 minutes",
|
"Local status/control UI started at %u Hz maximum; dim/off after %u/%u seconds",
|
||||||
1000U / LOCAL_STATUS_UI_REFRESH_MS);
|
1000U / LOCAL_STATUS_UI_REFRESH_MS,
|
||||||
|
(unsigned int)config->dim_timeout_seconds,
|
||||||
|
(unsigned int)config->off_timeout_seconds);
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "esp_err.h"
|
#include "esp_err.h"
|
||||||
|
#include "local_ui_config.h"
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
extern "C" {
|
extern "C" {
|
||||||
@@ -14,7 +15,11 @@ extern "C" {
|
|||||||
* task never becomes a broker client or serial writer. The OLED and buttons
|
* task never becomes a broker client or serial writer. The OLED and buttons
|
||||||
* are optional, so a missing display is not an error.
|
* are optional, so a missing display is not an error.
|
||||||
*/
|
*/
|
||||||
esp_err_t local_status_ui_start(void);
|
esp_err_t local_status_ui_start(const local_ui_config_t *config);
|
||||||
|
|
||||||
|
/* Runtime settings are copied atomically and never expose display-frame ownership. */
|
||||||
|
esp_err_t local_status_ui_get_config(local_ui_config_t *config);
|
||||||
|
esp_err_t local_status_ui_apply_config(const local_ui_config_t *config);
|
||||||
|
|
||||||
/* Preserve a manually selected display diagnostic for a bounded interval. */
|
/* Preserve a manually selected display diagnostic for a bounded interval. */
|
||||||
void local_status_ui_hold_for_diagnostics(void);
|
void local_status_ui_hold_for_diagnostics(void);
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||||
|
/* Versioned persistent configuration for the optional local OLED UI. */
|
||||||
|
|
||||||
|
#include "local_ui_config.h"
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
#include "nvs.h"
|
||||||
|
#include "nvs_flash.h"
|
||||||
|
|
||||||
|
void local_ui_config_defaults(local_ui_config_t *config)
|
||||||
|
{
|
||||||
|
if (config == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
*config = (local_ui_config_t){
|
||||||
|
.version = LOCAL_UI_CONFIG_VERSION,
|
||||||
|
.dim_timeout_seconds = LOCAL_UI_CONFIG_DEFAULT_DIM_SECONDS,
|
||||||
|
.off_timeout_seconds = LOCAL_UI_CONFIG_DEFAULT_OFF_SECONDS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t local_ui_config_validate(const local_ui_config_t *config)
|
||||||
|
{
|
||||||
|
if (config == NULL || config->version != LOCAL_UI_CONFIG_VERSION ||
|
||||||
|
config->dim_timeout_seconds > LOCAL_UI_CONFIG_MAX_TIMEOUT_SECONDS ||
|
||||||
|
config->off_timeout_seconds > LOCAL_UI_CONFIG_MAX_TIMEOUT_SECONDS) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
if (config->dim_timeout_seconds != 0U && config->off_timeout_seconds != 0U &&
|
||||||
|
config->off_timeout_seconds <= config->dim_timeout_seconds) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t local_ui_config_load(local_ui_config_t *config, bool *used_stored_config)
|
||||||
|
{
|
||||||
|
if (config == NULL || used_stored_config == NULL) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
local_ui_config_defaults(config);
|
||||||
|
*used_stored_config = false;
|
||||||
|
|
||||||
|
esp_err_t error = nvs_flash_init();
|
||||||
|
if (error != ESP_OK) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
nvs_handle_t handle;
|
||||||
|
error = nvs_open(LOCAL_UI_CONFIG_NVS_NAMESPACE, NVS_READONLY, &handle);
|
||||||
|
if (error == ESP_ERR_NVS_NOT_FOUND) {
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
if (error != ESP_OK) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t stored_size = 0U;
|
||||||
|
error = nvs_get_blob(handle, LOCAL_UI_CONFIG_NVS_BLOB_KEY, NULL, &stored_size);
|
||||||
|
if (error == ESP_ERR_NVS_NOT_FOUND || error == ESP_ERR_NVS_TYPE_MISMATCH ||
|
||||||
|
(error == ESP_OK && stored_size != sizeof(local_ui_config_t))) {
|
||||||
|
nvs_close(handle);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
if (error != ESP_OK) {
|
||||||
|
nvs_close(handle);
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
local_ui_config_t stored;
|
||||||
|
error = nvs_get_blob(handle, LOCAL_UI_CONFIG_NVS_BLOB_KEY, &stored, &stored_size);
|
||||||
|
nvs_close(handle);
|
||||||
|
if (error == ESP_ERR_NVS_INVALID_LENGTH) {
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
if (error != ESP_OK) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
if (stored_size != sizeof(stored) || local_ui_config_validate(&stored) != ESP_OK) {
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
*config = stored;
|
||||||
|
*used_stored_config = true;
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t local_ui_config_save(const local_ui_config_t *config)
|
||||||
|
{
|
||||||
|
esp_err_t error = local_ui_config_validate(config);
|
||||||
|
if (error != ESP_OK) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
error = nvs_flash_init();
|
||||||
|
if (error != ESP_OK) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
nvs_handle_t handle;
|
||||||
|
error = nvs_open(LOCAL_UI_CONFIG_NVS_NAMESPACE, NVS_READWRITE, &handle);
|
||||||
|
if (error != ESP_OK) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
error = nvs_set_blob(handle, LOCAL_UI_CONFIG_NVS_BLOB_KEY, config, sizeof(*config));
|
||||||
|
if (error == ESP_OK) {
|
||||||
|
error = nvs_commit(handle);
|
||||||
|
}
|
||||||
|
nvs_close(handle);
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t local_ui_config_reset_storage(void)
|
||||||
|
{
|
||||||
|
local_ui_config_t config;
|
||||||
|
local_ui_config_defaults(&config);
|
||||||
|
return local_ui_config_save(&config);
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||||
|
/* Versioned persistent configuration for the optional local OLED UI. */
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include "esp_err.h"
|
||||||
|
|
||||||
|
#define LOCAL_UI_CONFIG_VERSION 1U
|
||||||
|
#define LOCAL_UI_CONFIG_DEFAULT_DIM_SECONDS 300U
|
||||||
|
#define LOCAL_UI_CONFIG_DEFAULT_OFF_SECONDS 600U
|
||||||
|
#define LOCAL_UI_CONFIG_MAX_TIMEOUT_SECONDS 86400U
|
||||||
|
|
||||||
|
#define LOCAL_UI_CONFIG_NVS_NAMESPACE "local_ui"
|
||||||
|
#define LOCAL_UI_CONFIG_NVS_BLOB_KEY "config"
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint32_t version;
|
||||||
|
/* Zero disables the corresponding inactivity transition. */
|
||||||
|
uint32_t dim_timeout_seconds;
|
||||||
|
uint32_t off_timeout_seconds;
|
||||||
|
} local_ui_config_t;
|
||||||
|
|
||||||
|
void local_ui_config_defaults(local_ui_config_t *config);
|
||||||
|
esp_err_t local_ui_config_validate(const local_ui_config_t *config);
|
||||||
|
esp_err_t local_ui_config_load(local_ui_config_t *config, bool *used_stored_config);
|
||||||
|
esp_err_t local_ui_config_save(const local_ui_config_t *config);
|
||||||
|
esp_err_t local_ui_config_reset_storage(void);
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||||
|
/* UART0 administration commands for local UI aging settings. */
|
||||||
|
|
||||||
|
#include "local_ui_console.h"
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "esp_console.h"
|
||||||
|
#include "local_display.h"
|
||||||
|
#include "local_status_ui.h"
|
||||||
|
#include "local_ui_config.h"
|
||||||
|
|
||||||
|
static void print_usage(void)
|
||||||
|
{
|
||||||
|
printf("Usage:\n");
|
||||||
|
printf(" display status\n");
|
||||||
|
printf(" display set <dim-seconds|off-seconds> <0..%u>\n",
|
||||||
|
LOCAL_UI_CONFIG_MAX_TIMEOUT_SECONDS);
|
||||||
|
printf(" display save|load|defaults|reset\n");
|
||||||
|
printf("Zero disables the selected inactivity transition.\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void print_config(const local_ui_config_t *config)
|
||||||
|
{
|
||||||
|
printf("Local UI configuration v%u: dim-seconds=%u off-seconds=%u\n",
|
||||||
|
(unsigned int)config->version,
|
||||||
|
(unsigned int)config->dim_timeout_seconds,
|
||||||
|
(unsigned int)config->off_timeout_seconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool parse_timeout(const char *text, uint32_t *value)
|
||||||
|
{
|
||||||
|
if (text == NULL || *text == '\0') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (const char *character = text; *character != '\0'; ++character) {
|
||||||
|
if (*character < '0' || *character > '9') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
errno = 0;
|
||||||
|
char *end = NULL;
|
||||||
|
unsigned long parsed = strtoul(text, &end, 10);
|
||||||
|
if (errno != 0 || end == text || *end != '\0' ||
|
||||||
|
parsed > LOCAL_UI_CONFIG_MAX_TIMEOUT_SECONDS) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
*value = (uint32_t)parsed;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int show_status(void)
|
||||||
|
{
|
||||||
|
local_ui_config_t config;
|
||||||
|
esp_err_t error = local_status_ui_get_config(&config);
|
||||||
|
if (error != ESP_OK) {
|
||||||
|
printf("Could not read local UI configuration: %s\n", esp_err_to_name(error));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
print_config(&config);
|
||||||
|
|
||||||
|
local_display_snapshot_t display;
|
||||||
|
error = local_display_get_snapshot(&display);
|
||||||
|
if (error != ESP_OK) {
|
||||||
|
printf("Display service unavailable: %s\n", esp_err_to_name(error));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("OLED: bus=%s initialized=%s address=0x%02x contrast=%u last-error=%s\n",
|
||||||
|
display.bus_ready ? "ready" : "unavailable",
|
||||||
|
display.initialized ? "yes" : "no",
|
||||||
|
display.address_7bit,
|
||||||
|
(unsigned int)display.contrast,
|
||||||
|
esp_err_to_name(display.last_error));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int apply_parameter(const char *parameter, const char *text)
|
||||||
|
{
|
||||||
|
uint32_t value;
|
||||||
|
if (!parse_timeout(text, &value)) {
|
||||||
|
printf("Timeout must be 0..%u seconds.\n",
|
||||||
|
LOCAL_UI_CONFIG_MAX_TIMEOUT_SECONDS);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
local_ui_config_t config;
|
||||||
|
esp_err_t error = local_status_ui_get_config(&config);
|
||||||
|
if (error != ESP_OK) {
|
||||||
|
printf("Could not read local UI configuration: %s\n", esp_err_to_name(error));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (strcmp(parameter, "dim-seconds") == 0) {
|
||||||
|
config.dim_timeout_seconds = value;
|
||||||
|
} else if (strcmp(parameter, "off-seconds") == 0) {
|
||||||
|
config.off_timeout_seconds = value;
|
||||||
|
} else {
|
||||||
|
printf("Unknown display parameter '%s'.\n", parameter);
|
||||||
|
print_usage();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
error = local_status_ui_apply_config(&config);
|
||||||
|
if (error != ESP_OK) {
|
||||||
|
printf("Invalid display configuration: %s. When both timeouts are enabled, off must be later than dim.\n",
|
||||||
|
esp_err_to_name(error));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
print_config(&config);
|
||||||
|
printf("Applied in RAM; run 'display save' to persist it.\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int command_display(int argc, char **argv)
|
||||||
|
{
|
||||||
|
if (argc == 1 || (argc == 2 && strcmp(argv[1], "status") == 0)) {
|
||||||
|
return show_status();
|
||||||
|
}
|
||||||
|
if (argc == 4 && strcmp(argv[1], "set") == 0) {
|
||||||
|
return apply_parameter(argv[2], argv[3]);
|
||||||
|
}
|
||||||
|
if (argc == 2 && strcmp(argv[1], "save") == 0) {
|
||||||
|
local_ui_config_t config;
|
||||||
|
esp_err_t error = local_status_ui_get_config(&config);
|
||||||
|
if (error == ESP_OK) {
|
||||||
|
error = local_ui_config_save(&config);
|
||||||
|
}
|
||||||
|
if (error != ESP_OK) {
|
||||||
|
printf("Could not save display configuration: %s\n", esp_err_to_name(error));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("Display configuration saved to NVS.\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (argc == 2 && strcmp(argv[1], "load") == 0) {
|
||||||
|
local_ui_config_t config;
|
||||||
|
bool used_stored_config;
|
||||||
|
esp_err_t error = local_ui_config_load(&config, &used_stored_config);
|
||||||
|
if (error == ESP_OK) {
|
||||||
|
error = local_status_ui_apply_config(&config);
|
||||||
|
}
|
||||||
|
if (error != ESP_OK) {
|
||||||
|
printf("Could not load display configuration: %s\n", esp_err_to_name(error));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("Loaded %s display configuration.\n",
|
||||||
|
used_stored_config ? "stored" : "default");
|
||||||
|
print_config(&config);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (argc == 2 && strcmp(argv[1], "defaults") == 0) {
|
||||||
|
local_ui_config_t config;
|
||||||
|
local_ui_config_defaults(&config);
|
||||||
|
esp_err_t error = local_status_ui_apply_config(&config);
|
||||||
|
if (error != ESP_OK) {
|
||||||
|
printf("Could not apply display defaults: %s\n", esp_err_to_name(error));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("Display defaults applied in RAM; run 'display save' to persist them.\n");
|
||||||
|
print_config(&config);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (argc == 2 && strcmp(argv[1], "reset") == 0) {
|
||||||
|
local_ui_config_t previous;
|
||||||
|
local_ui_config_t defaults;
|
||||||
|
local_ui_config_defaults(&defaults);
|
||||||
|
esp_err_t error = local_status_ui_get_config(&previous);
|
||||||
|
if (error == ESP_OK) {
|
||||||
|
error = local_status_ui_apply_config(&defaults);
|
||||||
|
}
|
||||||
|
if (error == ESP_OK) {
|
||||||
|
error = local_ui_config_reset_storage();
|
||||||
|
if (error != ESP_OK) {
|
||||||
|
(void)local_status_ui_apply_config(&previous);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (error != ESP_OK) {
|
||||||
|
printf("Could not reset display configuration: %s\n", esp_err_to_name(error));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("Display defaults applied and saved to NVS.\n");
|
||||||
|
print_config(&defaults);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
print_usage();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t local_ui_console_register_commands(void)
|
||||||
|
{
|
||||||
|
const esp_console_cmd_t command = {
|
||||||
|
.command = "display",
|
||||||
|
.help = "Configure persistent local OLED aging timeouts; use 'display' for status",
|
||||||
|
.hint = NULL,
|
||||||
|
.func = &command_display,
|
||||||
|
.argtable = NULL,
|
||||||
|
};
|
||||||
|
return esp_console_cmd_register(&command);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||||
|
/* UART0 administration commands for local UI aging settings. */
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esp_err.h"
|
||||||
|
|
||||||
|
esp_err_t local_ui_console_register_commands(void);
|
||||||
+18
-2
@@ -7,6 +7,8 @@
|
|||||||
#include "network_console.h"
|
#include "network_console.h"
|
||||||
#include "local_display.h"
|
#include "local_display.h"
|
||||||
#include "local_status_ui.h"
|
#include "local_status_ui.h"
|
||||||
|
#include "local_ui_config.h"
|
||||||
|
#include "local_ui_console.h"
|
||||||
#include "local_ui_hw_test.h"
|
#include "local_ui_hw_test.h"
|
||||||
#include "rs232_hw_test.h"
|
#include "rs232_hw_test.h"
|
||||||
#include "rs232_port_owner.h"
|
#include "rs232_port_owner.h"
|
||||||
@@ -38,7 +40,7 @@ static const char *TAG = "firmware";
|
|||||||
|
|
||||||
void app_main(void)
|
void app_main(void)
|
||||||
{
|
{
|
||||||
ESP_LOGI(TAG, "ESP32-S3 Serial Swiss Army Knife Phase 7D local controls started");
|
ESP_LOGI(TAG, "ESP32-S3 Serial Swiss Army Knife Phase 7E local UI reliability started");
|
||||||
|
|
||||||
if (esp_psram_is_initialized()) {
|
if (esp_psram_is_initialized()) {
|
||||||
ESP_LOGI(TAG, "PSRAM initialized: %u bytes", (unsigned int)esp_psram_get_size());
|
ESP_LOGI(TAG, "PSRAM initialized: %u bytes", (unsigned int)esp_psram_get_size());
|
||||||
@@ -77,6 +79,17 @@ void app_main(void)
|
|||||||
esp_err_to_name(local_ui_error));
|
esp_err_to_name(local_ui_error));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local_ui_config_t local_ui_config;
|
||||||
|
bool used_stored_local_ui_config = false;
|
||||||
|
esp_err_t local_ui_config_error =
|
||||||
|
local_ui_config_load(&local_ui_config, &used_stored_local_ui_config);
|
||||||
|
if (local_ui_config_error != ESP_OK) {
|
||||||
|
local_ui_config_defaults(&local_ui_config);
|
||||||
|
ESP_LOGW(TAG,
|
||||||
|
"NVS local UI configuration unavailable (%s); using RAM defaults",
|
||||||
|
esp_err_to_name(local_ui_config_error));
|
||||||
|
}
|
||||||
|
|
||||||
serial_config_t serial_config;
|
serial_config_t serial_config;
|
||||||
bool used_stored_config = false;
|
bool used_stored_config = false;
|
||||||
esp_err_t config_error = serial_config_load(&serial_config, &used_stored_config);
|
esp_err_t config_error = serial_config_load(&serial_config, &used_stored_config);
|
||||||
@@ -203,13 +216,15 @@ void app_main(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (local_ui_error == ESP_OK) {
|
if (local_ui_error == ESP_OK) {
|
||||||
esp_err_t local_status_ui_error = local_status_ui_start();
|
esp_err_t local_status_ui_error = local_status_ui_start(&local_ui_config);
|
||||||
if (local_status_ui_error != ESP_OK) {
|
if (local_status_ui_error != ESP_OK) {
|
||||||
ESP_LOGW(TAG, "Local status UI unavailable: %s",
|
ESP_LOGW(TAG, "Local status UI unavailable: %s",
|
||||||
esp_err_to_name(local_status_ui_error));
|
esp_err_to_name(local_status_ui_error));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Using %s local UI configuration",
|
||||||
|
used_stored_local_ui_config ? "stored" : "default");
|
||||||
ESP_LOGI(
|
ESP_LOGI(
|
||||||
TAG,
|
TAG,
|
||||||
"Using %s serial configuration; UART service starts on 'serial start' or native USB open",
|
"Using %s serial configuration; UART service starts on 'serial start' or native USB open",
|
||||||
@@ -235,6 +250,7 @@ void app_main(void)
|
|||||||
|
|
||||||
/* The REPL constructor initializes esp_console and installs `help`. */
|
/* The REPL constructor initializes esp_console and installs `help`. */
|
||||||
ESP_ERROR_CHECK(rs232_hw_test_register_console_commands());
|
ESP_ERROR_CHECK(rs232_hw_test_register_console_commands());
|
||||||
|
ESP_ERROR_CHECK(local_ui_console_register_commands());
|
||||||
ESP_ERROR_CHECK(serial_console_register_commands());
|
ESP_ERROR_CHECK(serial_console_register_commands());
|
||||||
ESP_ERROR_CHECK(session_console_register_commands());
|
ESP_ERROR_CHECK(session_console_register_commands());
|
||||||
ESP_ERROR_CHECK(usb_console_register_commands());
|
ESP_ERROR_CHECK(usb_console_register_commands());
|
||||||
|
|||||||
Reference in New Issue
Block a user