Implement Phase 7C local status dashboard

Add fixed status icons, OLED dimming and wake behavior, lowercase
glyphs, and access-point metadata tracking. Update validation guidance,
roadmap status, and provide an icon layout mockup with licensing.
This commit is contained in:
2026-08-29 19:05:06 +02:00
parent c10e7d24a8
commit e291e29357
11 changed files with 652 additions and 126 deletions
+11 -2
View File
@@ -127,9 +127,18 @@ After boot, the OLED starts on the **OVERVIEW** page. A short previous/back pres
3. **BROKER** — connected clients, current writer marker, pending output, and drop/event counters. 3. **BROKER** — connected clients, current writer marker, pending output, and drop/event counters.
4. **NETWORK SERVICES** — Wi-Fi state/RSSI/IP/AP state and HTTPS, WebSocket, and SSH service state. 4. **NETWORK SERVICES** — Wi-Fi state/RSSI/IP/AP state and HTTPS, WebSocket, and SSH service state.
Confirm that each page remains entirely within the blue content panel and that the yellow strip continuously shows compact serial/Wi-Fi/client/service state. The UI refreshes at 4 Hz maximum and is read-only: select (GPIO13) must not change a service, acquire/release a writer, inject serial data, or expose passwords, credentials, tickets, or key material. Confirm that each page remains entirely within the blue content panel. The yellow panel must retain the same fixed icon positions while pages change: serial, segmented Wi-Fi strength, USB, WebSocket, SSH, broker clients, and alert. Active icons are solid with a value below; inactive icon slots remain completely blank and omit their value. Confirm that page text uses readable upper/lowercase, separates items and values with colons, and displays the active station SSID or AP SSID with its channel on the Overview and Network pages.
With UART0, USB CDC, WebSocket, or SSH active, navigate pages and confirm that the display continues to update without disrupting serial traffic or UART0 recovery. Record a missing, stale, clipped, or implausible status value before Phase 7C is marked complete. The UI refreshes at 4 Hz maximum and is read-only: select (GPIO13) must not change a service, acquire/release a writer, inject serial data, or expose passwords, credentials, tickets, or key material. With UART0, USB CDC, WebSocket, and SSH active, navigate pages and confirm that the display continues to update without disrupting serial traffic or UART0 recovery.
For the OLED-aging policy, leave all three buttons untouched and verify:
1. At five minutes, display contrast drops to `1` while the contents remain present.
2. At ten minutes, the OLED switches off.
3. The first debounced press of any button wakes the OLED at contrast `127` without changing the current page.
4. A subsequent previous/next press navigates normally and restarts the inactivity timers.
`debug display status` may verify an individual transition in a separate timing run. Every display diagnostic counts as activity and holds normal UI rendering for 30 seconds, so do not invoke it between the five- and ten-minute observations of one continuous run. Record a missing, stale, clipped, or implausible status value or incorrect dim/off/wake transition before Phase 7C is marked complete.
## Configuration A: data and handshake pairs ## Configuration A: data and handshake pairs
+36
View File
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="1080" height="720" viewBox="0 0 1080 720" role="img" aria-labelledby="title description">
<title id="title">Phase 7C compact icon status UI mockup</title>
<desc id="description">Four 128 by 64 OLED page mockups with the same fixed-position yellow status icons and changing blue page content. Active icons are solid yellow with values beneath; inactive SSH and alert slots are blank.</desc>
<defs>
<style>
.heading{fill:#d8e4ec;font:600 20px sans-serif}.note{fill:#93a8b6;font:14px sans-serif}.screen{fill:#010609;stroke:#668092;stroke-width:2}.yellowbg{fill:#191600}.divider{stroke:#77858d;stroke-opacity:.5;stroke-width:2}.active{fill:#ffd52e;stroke:#ffd52e}.blue{fill:#27c9ff;stroke:#27c9ff}.content{fill:#27c9ff;font:600 18px 'DejaVu Sans Mono',monospace}.value{fill:#ffd52e;font:600 14px 'DejaVu Sans Mono',monospace}.page{fill:#d8e4ec;font:600 17px sans-serif}
</style>
<!-- Official Material Design Icons usb path, @mdi/svg 7.4.47. -->
<symbol id="usb" viewBox="0 0 24 24"><path d="M15,7V11H16V13H13V5H15L12,1L9,5H11V13H8V10.93C8.7,10.56 9.2,9.85 9.2,9C9.2,7.78 8.21,6.8 7,6.8C5.78,6.8 4.8,7.78 4.8,9C4.8,9.85 5.3,10.56 6,10.93V13A2,2 0 0,0 8,15H11V18.05C10.29,18.41 9.8,19.15 9.8,20A2.2,2.2 0 0,0 12,22.2A2.2,2.2 0 0,0 14.2,20C14.2,19.15 13.71,18.41 13,18.05V15H16A2,2 0 0,0 18,13V11H19V7H15Z"/></symbol>
<symbol id="serial" viewBox="0 0 24 24"><path d="M5 3h14v5h3v13H2V8h3zm2 2v3h10V5zM5 11v7h14v-7zm2 2h2v2H7zm4 0h2v2h-2zm4 0h2v2h-2z"/></symbol>
<symbol id="wifi" viewBox="0 0 24 24"><path d="M1 8q11-9 22 0l-2 3q-9-7-18 0zm5 6q6-5 12 0l-2 3q-4-3-8 0zm4 6a2 2 0 1 1 4 0z"/></symbol>
<symbol id="web" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20M4.3 9h3.1A16 16 0 0 1 9 4.7 8.1 8.1 0 0 0 4.3 9m-.2 6h3.3a16 16 0 0 1 0-6H4.3a8 8 0 0 0-.2 6m4.9 4.3A16 16 0 0 1 7.4 15H4.3A8.1 8.1 0 0 0 9 19.3M10 15h4a14 14 0 0 0 0-6h-4a14 14 0 0 0 0 6m1 4.9v-3h2v3a8 8 0 0 1-2 0m0-15.8v3h2v-3a8 8 0 0 0-2 0m4 15.2a16 16 0 0 0 1.6-4.3h3.1a8.1 8.1 0 0 1-4.7 4.3m1.6-10.3h3.1A8.1 8.1 0 0 0 15 4.7 16 16 0 0 1 16.6 9m.1 2a14 14 0 0 1 0 4h3.2a8 8 0 0 0 0-4z"/></symbol>
<symbol id="ssh" viewBox="0 0 24 24"><path d="M2 3h20v18H2zm3 4 4 4-4 4 2 2 6-6-6-6zm8 8v2h6v-2z"/></symbol>
<symbol id="clients" viewBox="0 0 24 24"><path d="M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8m7-1a3 3 0 1 0 0-6 3 3 0 0 0 0 6M2 21v-3c0-3 3-5 7-5s7 2 7 5v3zm14-8c4 0 6 2 6 5v3h-4v-3c0-2-1-4-3-5z"/></symbol>
<symbol id="alert" viewBox="0 0 24 24"><path d="M12 2 1 22h22zm-1 7h2v7h-2zm0 9h2v2h-2z"/></symbol>
<g id="status">
<g class="active"><use href="#serial" x="12" y="4" width="30" height="30"/><use href="#wifi" x="80" y="4" width="30" height="30"/><use href="#usb" x="150" y="4" width="30" height="30"/><use href="#web" x="220" y="4" width="30" height="30"/><use href="#clients" x="360" y="4" width="30" height="30"/></g>
<text x="17" y="57" class="value">On</text><text x="91" y="57" class="value">6</text><text x="154" y="57" class="value">Wr</text><text x="231" y="57" class="value">1</text><text x="371" y="57" class="value">3</text>
</g>
</defs>
<rect width="1080" height="720" fill="#0b1118"/><text x="540" y="34" text-anchor="middle" class="heading">Phase 7C — fixed icon dashboard and compact status pages</text><text x="540" y="57" text-anchor="middle" class="note">Yellow strip is identical on every page · inactive icon slots and their supplementary values remain blank</text>
<g transform="translate(62 98)"><text x="256" y="-16" text-anchor="middle" class="page">Overview</text><rect width="512" height="256" class="screen"/><rect width="512" height="64" class="yellowbg"/><line x1="0" y1="64" x2="512" y2="64" class="divider"/><use href="#status"/>
<g class="blue"><use href="#serial" x="8" y="72" width="30" height="30"/><use href="#clients" x="8" y="104" width="30" height="30"/><use href="#wifi" x="8" y="136" width="30" height="30"/><use href="#web" x="8" y="200" width="30" height="30"/></g><text x="45" y="96" class="content">Serial:230400 8N1</text><text x="45" y="128" class="content">Clients:3 Wr:USB</text><text x="45" y="160" class="content">WiFi:LabNet Ch:6</text><text x="45" y="192" class="content">IP:192.168.1.42</text><text x="45" y="224" class="content">Web:1 SSH:0 USB:1</text></g>
<g transform="translate(592 98)"><text x="256" y="-16" text-anchor="middle" class="page">RS-232 / modem</text><rect width="512" height="256" class="screen"/><rect width="512" height="64" class="yellowbg"/><line x1="0" y1="64" x2="512" y2="64" class="divider"/><use href="#status"/>
<g class="blue"><use href="#serial" x="8" y="72" width="30" height="30"/></g><text x="45" y="96" class="content">Mode:230400 8N1</text><text x="45" y="128" class="content">Flow:rts-cts</text><text x="45" y="160" class="content">DCD:0 DSR:1 CTS:1</text><text x="45" y="192" class="content">Valid:Yes RX:1.2M</text><text x="45" y="224" class="content">TX:633 Err:0</text></g>
<g transform="translate(62 438)"><text x="256" y="-16" text-anchor="middle" class="page">Broker clients</text><rect width="512" height="256" class="screen"/><rect width="512" height="64" class="yellowbg"/><line x1="0" y1="64" x2="512" y2="64" class="divider"/><use href="#status"/>
<g class="blue"><use href="#usb" x="8" y="72" width="30" height="30"/><use href="#web" x="8" y="104" width="30" height="30"/><use href="#ssh" x="8" y="136" width="30" height="30"/></g><text x="45" y="96" class="content">Wr:USB Q:0</text><text x="45" y="128" class="content">Ob:web Q:0</text><text x="45" y="160" class="content">Ob:SSH Q:0</text><text x="45" y="224" class="content">Drop:0 Events:0</text></g>
<g transform="translate(592 438)"><text x="256" y="-16" text-anchor="middle" class="page">Network / services</text><rect width="512" height="256" class="screen"/><rect width="512" height="64" class="yellowbg"/><line x1="0" y1="64" x2="512" y2="64" class="divider"/><use href="#status"/>
<g class="blue"><use href="#wifi" x="8" y="72" width="30" height="30"/><use href="#web" x="8" y="168" width="30" height="30"/><use href="#ssh" x="8" y="200" width="30" height="30"/></g><text x="45" y="96" class="content">WiFi:LabNet Ch:6</text><text x="45" y="128" class="content">IP:192.168.1.42</text><text x="45" y="160" class="content">AP:Off Clients:0</text><text x="45" y="192" class="content">HTTPS:On Web:1</text><text x="45" y="224" class="content">SSH:On Sessions:0</text></g>
</svg>

After

Width:  |  Height:  |  Size: 6.4 KiB

+10 -8
View File
@@ -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 (7C)** | | 7 | Local display and button interface | **In progress (7D)** |
| 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. Phase 7B display-driver validation is complete; Phase 7 overall remains in progress (7C). Phases 7C through 7E are not complete. Phase 7A diagnostics and target-hardware electrical validation are complete. Phases 7B and 7C are complete; Phase 7 overall remains in progress (7D). Phases 7D and 7E are not complete.
#### Hardware baseline #### Hardware baseline
@@ -196,9 +196,9 @@ Phase 7A diagnostics and target-hardware electrical validation are complete. Pha
- Hardware verification established yellow rows 015 and blue rows 1663. A narrow physical black divider separates the two regions even though row 15 is the final yellow addressable row and row 16 the first blue addressable row. - Hardware verification established yellow rows 015 and blue rows 1663. A narrow physical black divider separates the two regions even though row 15 is the final yellow addressable row and row 16 the first blue addressable row.
- Keep assignments centralized in the board profile rather than scattering display or button GPIO assumptions through UI code. - Keep assignments centralized in the board profile rather than scattering display or button GPIO assumptions through UI code.
![Proposed Phase 7 overview display](phase7_display_mockup.svg) ![Phase 7C icon status-page mockup](phase7c_icon_mockup.svg)
The persistent yellow strip is reserved for serial-service state, Wi-Fi strength, active USB/Web/SSH counts, current writer, total clients, and an alert indicator. Phase 7B must render it as a separate 128×16 status panel. The blue 128×48 content panel begins at row 16 and rotates through overview, RS-232, broker-client, and network/service pages; the physical black divider must remain visually clear. No password, Wi-Fi secret, private-key material, or routine credential data may appear on the display. The persistent yellow strip uses fixed-position serial, Wi-Fi-strength, USB, WebSocket, SSH, broker-client, and alert icons. Active icons are solid with a compact value below; inactive icon slots remain blank and show no value. The blue 128×48 content panel begins at row 16 and rotates through overview, RS-232, broker-client, and network/service pages; the physical black divider remains visually clear. No password, Wi-Fi secret, private-key material, or routine credential data may appear on the display.
#### Implementation sequence #### Implementation sequence
@@ -207,23 +207,25 @@ The persistent yellow strip is reserved for serial-service state, Wi-Fi strength
- The selected module acknowledged at 7-bit `0x3c` (8-bit `0x78` write / `0x79` read). A guarded full scan is retained for the dedicated local-UI bus; an absent display remains nonfatal and does not make the serial core dependent on the OLED. - The selected module acknowledged at 7-bit `0x3c` (8-bit `0x78` write / `0x79` read). A guarded full scan is retained for the dedicated local-UI bus; an absent display remains nonfatal and does not make the serial core dependent on the OLED.
- Hardware validation passed for geometry, orientation, row/column addressing, contrast, inversion, button pull-ups/debounce/short-press/long-press/stuck behavior, and the color geometry: yellow rows 015, blue rows 1663, with a physical black separator between the regions. - Hardware validation passed for geometry, orientation, row/column addressing, contrast, inversion, button pull-ups/debounce/short-press/long-press/stuck behavior, and the color geometry: yellow rows 015, blue rows 1663, with a physical black separator between the regions.
2. **Phase 7B — Display driver — Complete** 2. **Phase 7B — Display driver — Complete**
- The `local_display` service owns the local I²C bus, SSD1315-compatible panel, and framebuffer. It uses a static 1 KiB 128×64 framebuffer, a compact 5×7 uppercase/digit/punctuation renderer, and an internal mutex; do not add LVGL for this fixed monochrome UI. - The `local_display` service owns the local I²C bus, SSD1315-compatible panel, and framebuffer. It uses a static 1 KiB 128×64 framebuffer, a compact 5×7 upper/lowercase, digit, and punctuation renderer, and an internal mutex; do not add LVGL for this fixed monochrome UI.
- Rendering treats the yellow 128×16 status panel (rows 015) and blue 128×48 content panel (rows 1663) as separate panels so the physical divider remains clear. - Rendering treats the yellow 128×16 status panel (rows 015) and blue 128×48 content panel (rows 1663) as separate panels so the physical divider remains clear.
- Frame commits refresh only dirty 8-pixel pages. I²C runs at 100 kHz with bounded transactions. - Frame commits refresh only dirty 8-pixel pages. I²C runs at 100 kHz with bounded transactions.
- The display bus initializes at boot; a known display is automatically initialized and cleared. A missing or unresponsive display is nonfatal and does not make the serial core dependent on it. - The display bus initializes at boot; a known display is automatically initialized and cleared. A missing or unresponsive display is nonfatal and does not make the serial core dependent on it.
- Target-hardware validation passed for initialization, all display diagnostic patterns, the rendered panel layout and physical divider, row 15/16 boundary, contrast, inversion, display-off/reinitialization, and button diagnostics. Observed contrast control has a useful brightness range of 1255. - Target-hardware validation passed for initialization, all display diagnostic patterns, the rendered panel layout and physical divider, row 15/16 boundary, contrast, inversion, display-off/reinitialization, and button diagnostics. Observed contrast control has a useful brightness range of 1255.
3. **Phase 7C — Read-only status UI — Implemented; validation pending** 3. **Phase 7C — Read-only status UI — Complete**
- The low-priority `local_status_ui` owner task builds display state from copied public serial, Wi-Fi, broker, USB, WebSocket, HTTPS, and SSH snapshots. It does not parse CLI output, reach into transport internals, or become a broker client. - The low-priority `local_status_ui` owner task builds display state from copied public serial, Wi-Fi, broker, USB, WebSocket, HTTPS, and SSH snapshots. It does not parse CLI output, reach into transport internals, or become a broker client.
- Previous/back and next short presses navigate overview, RS-232/modem, broker-client/writer, and network/service pages. Select has no state-changing action in this read-only phase. - Previous/back and next short presses navigate overview, RS-232/modem, broker-client/writer, and network/service pages. Select has no state-changing action in this read-only phase.
- The task polls and debounces the existing active-low button inputs and refreshes at 4 Hz maximum. It collects service snapshots before beginning a display frame, so no service or broker lock is held across an I²C transaction. - The task polls and debounces the existing active-low button inputs and refreshes at 4 Hz maximum. It collects service snapshots before beginning a display frame, so no service or broker lock is held across an I²C transaction.
- The yellow panel is page-independent and uses fixed icon slots. Wi-Fi strength is shown by segmented Wi-Fi bars; the USB glyph is derived from Pictogrammers Material Design Icons. Page text uses lowercase where appropriate, separates labels and values with colons, and shows the active station or AP SSID plus channel.
- 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.
- Pending target-hardware validation: page navigation, each page's live snapshot values, concurrent USB/WebSocket/SSH operation while refreshing, and UART0 responsiveness. - 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 — Planned** 4. **Phase 7D — Local controls — Planned**
- Add a shallow menu for safe serial, Wi-Fi, HTTPS, SSH, writer-release, display, and reboot actions through direct service APIs. - Add a shallow menu for safe serial, Wi-Fi, HTTPS, SSH, writer-release, display, and reboot actions through direct service APIs.
- Require a visible confirmation screen and a timed select hold before stopping active services, revoking a writer, rebooting, or performing another disruptive action. - Require a visible confirmation screen and a timed select hold before stopping active services, revoking a writer, rebooting, or performing another disruptive action.
- A local display is not a serial broker client and cannot silently acquire the writer lease. - A local display is not a serial broker client and cannot silently acquire the writer lease.
5. **Phase 7E — Reliability, persistence, and documentation — Planned** 5. **Phase 7E — Reliability, persistence, and documentation — Planned**
- Add contrast and optional dim/blank timeout settings to limit OLED burn-in without making the display necessary for recovery. - Make the implemented contrast dim/blank timeouts configurable and persistent without making the display necessary for recovery.
- Validate display removal, I²C NACK/timeouts, stuck buttons, queue saturation, and repeated actions. - Validate display removal, I²C NACK/timeouts, stuck buttons, queue saturation, and repeated actions.
- Re-run concurrent USB CDC, WebSocket, and SSH traffic while the UI refreshes and confirm UART0 remains responsive. - Re-run concurrent USB CDC, WebSocket, and SSH traffic while the UI refreshes and confirm UART0 remains responsive.
- Update wiring, electrical-test, command-reference, and recovery documentation. - Update wiring, electrical-test, command-reference, and recovery documentation.
+27 -4
View File
@@ -29,7 +29,7 @@ typedef struct {
uint8_t columns[5]; uint8_t columns[5];
} glyph_t; } glyph_t;
/* Compact 5x7 ASCII subset: lower-case input maps to upper-case glyphs. */ /* Compact project-owned 5x7 ASCII subset with distinct upper/lower-case glyphs. */
static const glyph_t s_glyphs[] = { static const glyph_t s_glyphs[] = {
{' ', {0x00, 0x00, 0x00, 0x00, 0x00}}, {' ', {0x00, 0x00, 0x00, 0x00, 0x00}},
{'!', {0x00, 0x00, 0x5f, 0x00, 0x00}}, {'!', {0x00, 0x00, 0x5f, 0x00, 0x00}},
@@ -75,6 +75,32 @@ static const glyph_t s_glyphs[] = {
{'X', {0x63, 0x14, 0x08, 0x14, 0x63}}, {'X', {0x63, 0x14, 0x08, 0x14, 0x63}},
{'Y', {0x03, 0x04, 0x78, 0x04, 0x03}}, {'Y', {0x03, 0x04, 0x78, 0x04, 0x03}},
{'Z', {0x61, 0x51, 0x49, 0x45, 0x43}}, {'Z', {0x61, 0x51, 0x49, 0x45, 0x43}},
{'a', {0x20, 0x54, 0x54, 0x54, 0x78}},
{'b', {0x7f, 0x48, 0x44, 0x44, 0x38}},
{'c', {0x38, 0x44, 0x44, 0x44, 0x20}},
{'d', {0x38, 0x44, 0x44, 0x48, 0x7f}},
{'e', {0x38, 0x54, 0x54, 0x54, 0x18}},
{'f', {0x08, 0x7e, 0x09, 0x01, 0x02}},
{'g', {0x0c, 0x52, 0x52, 0x52, 0x3e}},
{'h', {0x7f, 0x08, 0x04, 0x04, 0x78}},
{'i', {0x00, 0x44, 0x7d, 0x40, 0x00}},
{'j', {0x20, 0x40, 0x44, 0x3d, 0x00}},
{'k', {0x7f, 0x10, 0x28, 0x44, 0x00}},
{'l', {0x00, 0x41, 0x7f, 0x40, 0x00}},
{'m', {0x7c, 0x04, 0x18, 0x04, 0x78}},
{'n', {0x7c, 0x08, 0x04, 0x04, 0x78}},
{'o', {0x38, 0x44, 0x44, 0x44, 0x38}},
{'p', {0x7c, 0x14, 0x14, 0x14, 0x08}},
{'q', {0x08, 0x14, 0x14, 0x18, 0x7c}},
{'r', {0x7c, 0x08, 0x04, 0x04, 0x08}},
{'s', {0x48, 0x54, 0x54, 0x54, 0x20}},
{'t', {0x04, 0x3f, 0x44, 0x40, 0x20}},
{'u', {0x3c, 0x40, 0x40, 0x20, 0x7c}},
{'v', {0x1c, 0x20, 0x40, 0x20, 0x1c}},
{'w', {0x3c, 0x40, 0x30, 0x40, 0x3c}},
{'x', {0x44, 0x28, 0x10, 0x28, 0x44}},
{'y', {0x0c, 0x50, 0x50, 0x50, 0x3c}},
{'z', {0x44, 0x64, 0x54, 0x4c, 0x44}},
}; };
static i2c_master_bus_handle_t s_bus; static i2c_master_bus_handle_t s_bus;
@@ -212,9 +238,6 @@ static bool panel_geometry(local_display_panel_t panel, uint8_t *origin_y, uint8
static const glyph_t *find_glyph(char character) static const glyph_t *find_glyph(char character)
{ {
if (character >= 'a' && character <= 'z') {
character = (char)(character - ('a' - 'A'));
}
for (size_t index = 0U; index < sizeof(s_glyphs) / sizeof(s_glyphs[0]); ++index) { for (size_t index = 0U; index < sizeof(s_glyphs) / sizeof(s_glyphs[0]); ++index) {
if (s_glyphs[index].character == character) { if (s_glyphs[index].character == character) {
return &s_glyphs[index]; return &s_glyphs[index];
+529 -104
View File
@@ -13,6 +13,8 @@
#include "esp_log.h" #include "esp_log.h"
#include "esp_netif_ip_addr.h" #include "esp_netif_ip_addr.h"
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/portmacro.h"
#include "freertos/semphr.h"
#include "freertos/task.h" #include "freertos/task.h"
#include "local_display.h" #include "local_display.h"
#include "serial_config.h" #include "serial_config.h"
@@ -32,6 +34,12 @@
#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_ACTIVE_CONTRAST 127U
#define LOCAL_STATUS_UI_ICON_SIZE 8U
#define LOCAL_STATUS_UI_STATUS_SLOT_WIDTH 18U
static const char *TAG = "local_status_ui"; static const char *TAG = "local_status_ui";
@@ -50,6 +58,22 @@ typedef enum {
LOCAL_STATUS_BUTTON_COUNT, LOCAL_STATUS_BUTTON_COUNT,
} local_status_button_t; } local_status_button_t;
typedef enum {
LOCAL_STATUS_POWER_ACTIVE = 0,
LOCAL_STATUS_POWER_DIMMED,
LOCAL_STATUS_POWER_OFF,
} local_status_power_t;
typedef enum {
LOCAL_STATUS_ICON_SERIAL = 0,
LOCAL_STATUS_ICON_WIFI,
LOCAL_STATUS_ICON_USB,
LOCAL_STATUS_ICON_WEB,
LOCAL_STATUS_ICON_SSH,
LOCAL_STATUS_ICON_CLIENTS,
LOCAL_STATUS_ICON_ALERT,
} local_status_icon_t;
typedef struct { typedef struct {
bool stable_pressed; bool stable_pressed;
bool candidate_pressed; bool candidate_pressed;
@@ -78,8 +102,53 @@ typedef struct {
ssh_transport_snapshot_t ssh; ssh_transport_snapshot_t ssh;
} local_status_snapshot_t; } local_status_snapshot_t;
/*
* The USB bitmap is a hand-rasterized 8x8 derivative of Pictogrammers
* Material Design Icons "usb". The Wi-Fi strength masks are compact segmented
* adaptations of "wifi-strength-4". Upstream SVGs and license are retained in
* third_party/material-design-icons/ (version 7.4.47, Apache-2.0 icons).
*/
static const uint8_t s_icon_serial[LOCAL_STATUS_UI_ICON_SIZE] = {
0x3cU, 0x42U, 0x7eU, 0x42U, 0x7eU, 0x42U, 0x3cU, 0x00U,
};
static const uint8_t s_icon_usb[LOCAL_STATUS_UI_ICON_SIZE] = {
0x18U, 0x3cU, 0x18U, 0x5aU, 0x3eU, 0x18U, 0x3cU, 0x18U,
};
static const uint8_t s_icon_web[LOCAL_STATUS_UI_ICON_SIZE] = {
0x3cU, 0x42U, 0xa5U, 0x99U, 0x99U, 0xa5U, 0x42U, 0x3cU,
};
static const uint8_t s_icon_ssh[LOCAL_STATUS_UI_ICON_SIZE] = {
0x7eU, 0x42U, 0x52U, 0x4aU, 0x52U, 0x42U, 0x7eU, 0x00U,
};
static const uint8_t s_icon_clients[LOCAL_STATUS_UI_ICON_SIZE] = {
0x24U, 0x5aU, 0x5aU, 0x24U, 0x00U, 0x7eU, 0xffU, 0x00U,
};
static const uint8_t s_icon_alert[LOCAL_STATUS_UI_ICON_SIZE] = {
0x18U, 0x18U, 0x3cU, 0x3cU, 0x7eU, 0x66U, 0xffU, 0x00U,
};
static const uint8_t s_icon_wifi_full[LOCAL_STATUS_UI_ICON_SIZE] = {
0x7eU, 0x81U, 0x3cU, 0x42U, 0x18U, 0x24U, 0x00U, 0x18U,
};
static const uint8_t s_icon_wifi_three[LOCAL_STATUS_UI_ICON_SIZE] = {
0x00U, 0x00U, 0x3cU, 0x42U, 0x18U, 0x24U, 0x00U, 0x18U,
};
static const uint8_t s_icon_wifi_two[LOCAL_STATUS_UI_ICON_SIZE] = {
0x00U, 0x00U, 0x00U, 0x00U, 0x18U, 0x24U, 0x00U, 0x18U,
};
static const uint8_t s_icon_wifi_one[LOCAL_STATUS_UI_ICON_SIZE] = {
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x18U,
};
static const uint8_t s_icon_access_point[LOCAL_STATUS_UI_ICON_SIZE] = {
0x18U, 0x18U, 0x3cU, 0x5aU, 0x99U, 0x18U, 0x18U, 0x18U,
};
static TaskHandle_t s_task; static TaskHandle_t s_task;
static volatile TickType_t s_diagnostic_hold_until; static StaticSemaphore_t s_diagnostic_gate_storage;
static SemaphoreHandle_t s_diagnostic_gate;
static portMUX_TYPE s_timing_mux = portMUX_INITIALIZER_UNLOCKED;
static bool s_diagnostic_hold_active;
static TickType_t s_diagnostic_hold_started;
static uint32_t s_external_activity_sequence;
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,
@@ -99,20 +168,49 @@ static const char *broker_type_to_string(session_broker_client_type_t type)
{ {
switch (type) { switch (type) {
case SESSION_BROKER_CLIENT_CONSOLE: case SESSION_BROKER_CLIENT_CONSOLE:
return "CON"; return "con";
case SESSION_BROKER_CLIENT_USB: case SESSION_BROKER_CLIENT_USB:
return "USB"; return "USB";
case SESSION_BROKER_CLIENT_WEB: case SESSION_BROKER_CLIENT_WEB:
return "WEB"; return "web";
case SESSION_BROKER_CLIENT_SSH: case SESSION_BROKER_CLIENT_SSH:
return "SSH"; return "SSH";
case SESSION_BROKER_CLIENT_INTERNAL: case SESSION_BROKER_CLIENT_INTERNAL:
return "INT"; return "int";
default: default:
return "?"; return "?";
} }
} }
static const char *writer_type_to_string(const local_status_snapshot_t *snapshot)
{
for (size_t i = 0U; i < snapshot->client_count; ++i) {
if (snapshot->clients[i].is_writer) {
return broker_type_to_string(snapshot->clients[i].type);
}
}
return "none";
}
static const char *on_off(bool value)
{
return value ? "On" : "Off";
}
static char parity_letter(serial_config_parity_t parity)
{
switch (parity) {
case SERIAL_CONFIG_PARITY_NONE:
return 'N';
case SERIAL_CONFIG_PARITY_EVEN:
return 'E';
case SERIAL_CONFIG_PARITY_ODD:
return 'O';
default:
return '?';
}
}
static void format_ipv4(uint32_t address, char output[16]) static void format_ipv4(uint32_t address, char output[16])
{ {
if (address == 0U) { if (address == 0U) {
@@ -127,6 +225,116 @@ static void format_ipv4(uint32_t address, char output[16])
} }
} }
static const uint8_t *icon_bitmap(local_status_icon_t icon)
{
switch (icon) {
case LOCAL_STATUS_ICON_SERIAL:
return s_icon_serial;
case LOCAL_STATUS_ICON_USB:
return s_icon_usb;
case LOCAL_STATUS_ICON_WEB:
return s_icon_web;
case LOCAL_STATUS_ICON_SSH:
return s_icon_ssh;
case LOCAL_STATUS_ICON_CLIENTS:
return s_icon_clients;
case LOCAL_STATUS_ICON_ALERT:
return s_icon_alert;
case LOCAL_STATUS_ICON_WIFI:
default:
return s_icon_wifi_full;
}
}
static const uint8_t *wifi_bitmap(int8_t rssi)
{
if (rssi >= -55) {
return s_icon_wifi_full;
}
if (rssi >= -67) {
return s_icon_wifi_three;
}
if (rssi >= -75) {
return s_icon_wifi_two;
}
return s_icon_wifi_one;
}
static void format_compact_u64(uint64_t value, char output[8])
{
if (value < 1000U) {
(void)snprintf(output, 8U, "%u", (unsigned int)value);
} else if (value < 1000000U) {
(void)snprintf(output, 8U, "%uk", (unsigned int)(value / 1000U));
} else if (value < 1000000000U) {
(void)snprintf(output, 8U, "%uM", (unsigned int)(value / 1000000U));
} else if (value < 1000000000000ULL) {
(void)snprintf(output, 8U, "%uG", (unsigned int)(value / 1000000000U));
} else {
memcpy(output, "999G+", sizeof("999G+"));
}
}
static void format_ssid(const char *ssid, char output[9])
{
size_t length = strlen(ssid);
if (length <= 8U) {
(void)snprintf(output, 9U, "%s", ssid);
return;
}
memcpy(output, ssid, 7U);
output[7] = '~';
output[8] = '\0';
}
static bool diagnostic_hold_is_active(TickType_t now)
{
bool active;
portENTER_CRITICAL(&s_timing_mux);
if (s_diagnostic_hold_active &&
(now - s_diagnostic_hold_started) >=
pdMS_TO_TICKS(LOCAL_STATUS_UI_DIAGNOSTIC_HOLD_MS)) {
s_diagnostic_hold_active = false;
}
active = s_diagnostic_hold_active;
portEXIT_CRITICAL(&s_timing_mux);
return active;
}
static bool consume_external_activity(uint32_t *observed_sequence)
{
uint32_t sequence;
portENTER_CRITICAL(&s_timing_mux);
sequence = s_external_activity_sequence;
portEXIT_CRITICAL(&s_timing_mux);
if (sequence == *observed_sequence) {
return false;
}
*observed_sequence = sequence;
return true;
}
static void draw_bitmap(local_display_panel_t panel,
uint8_t x,
uint8_t y,
const uint8_t bitmap[LOCAL_STATUS_UI_ICON_SIZE])
{
for (uint8_t row = 0U; row < LOCAL_STATUS_UI_ICON_SIZE; ++row) {
for (uint8_t column = 0U; column < LOCAL_STATUS_UI_ICON_SIZE; ++column) {
if ((bitmap[row] & (uint8_t)(0x80U >> column)) != 0U) {
local_display_frame_set_pixel(panel, (uint8_t)(x + column),
(uint8_t)(y + row), true);
}
}
}
}
static void draw_content_item(local_status_icon_t icon, uint8_t y, const char *text)
{
draw_bitmap(LOCAL_DISPLAY_PANEL_CONTENT, 0U, y, icon_bitmap(icon));
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 10U, y, text);
}
static void collect_snapshot(local_status_snapshot_t *snapshot) static void collect_snapshot(local_status_snapshot_t *snapshot)
{ {
memset(snapshot, 0, sizeof(*snapshot)); memset(snapshot, 0, sizeof(*snapshot));
@@ -166,119 +374,226 @@ static bool snapshot_has_alert(const local_status_snapshot_t *snapshot)
snapshot->broker.counters.output_dropped_bytes != 0U); snapshot->broker.counters.output_dropped_bytes != 0U);
} }
static bool wifi_is_active(const local_status_snapshot_t *snapshot)
{
if (!snapshot->wifi_available || !snapshot->wifi.started) {
return false;
}
return snapshot->wifi.state == WIFI_MANAGER_STATE_ONLINE ||
snapshot->wifi.state == WIFI_MANAGER_STATE_AP_ONLY ||
snapshot->wifi.ap_running;
}
static const uint8_t *wifi_snapshot_bitmap(const local_status_snapshot_t *snapshot)
{
if (snapshot->wifi_available && snapshot->wifi.state == WIFI_MANAGER_STATE_ONLINE) {
return wifi_bitmap(snapshot->wifi.sta_rssi);
}
if (snapshot->wifi_available && snapshot->wifi.ap_running) {
return s_icon_access_point;
}
return s_icon_wifi_full;
}
static void wifi_identity(const local_status_snapshot_t *snapshot,
const char **ssid,
uint8_t *channel)
{
if (snapshot->wifi_available && snapshot->wifi.sta_ssid_len > 0U &&
snapshot->wifi.state == WIFI_MANAGER_STATE_ONLINE) {
*ssid = snapshot->wifi.sta_ssid;
*channel = snapshot->wifi.sta_channel;
return;
}
if (snapshot->wifi_available && snapshot->wifi.ap_ssid_len > 0U &&
snapshot->wifi.ap_running) {
*ssid = snapshot->wifi.ap_ssid;
*channel = snapshot->wifi.ap_channel;
return;
}
*ssid = "none";
*channel = 0U;
}
static void render_status_slot(uint8_t slot,
const uint8_t bitmap[LOCAL_STATUS_UI_ICON_SIZE],
bool active,
const char *value)
{
if (!active) {
return;
}
uint8_t x = (uint8_t)(slot * LOCAL_STATUS_UI_STATUS_SLOT_WIDTH + 5U);
draw_bitmap(LOCAL_DISPLAY_PANEL_STATUS, x, 0U, bitmap);
if (value != NULL) {
size_t length = strlen(value);
uint8_t text_x = (uint8_t)(slot * LOCAL_STATUS_UI_STATUS_SLOT_WIDTH);
if (length < 3U) {
text_x = (uint8_t)(text_x + (3U - length) * 3U);
}
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_STATUS, text_x, 8U, value);
}
}
static void render_status_panel(const local_status_snapshot_t *snapshot) static void render_status_panel(const local_status_snapshot_t *snapshot)
{ {
char line[LOCAL_STATUS_UI_TEXT_CAPACITY]; char value[4];
int rssi = snapshot->wifi_available ? snapshot->wifi.sta_rssi : 0; bool wifi_active = wifi_is_active(snapshot);
unsigned int clients = snapshot->broker_available ? snapshot->broker.connected_clients : 0U; bool usb_active = snapshot->usb_available && snapshot->usb.attached && snapshot->usb.dtr;
unsigned int web_sessions = bool web_active = snapshot->web_serial_available && snapshot->web_serial.active_sessions > 0U;
snapshot->web_serial_available ? snapshot->web_serial.active_sessions : 0U; bool ssh_active = snapshot->ssh_available && snapshot->ssh.active_sessions > 0U;
unsigned int ssh_sessions = snapshot->ssh_available ? snapshot->ssh.active_sessions : 0U; bool clients_active = snapshot->broker_available && snapshot->broker.connected_clients > 0U;
bool alert_active = snapshot_has_alert(snapshot);
format_text(line, "SER %s WF %d%s", snapshot->serial_running ? "ON" : "OFF", rssi, render_status_slot(0U, s_icon_serial, snapshot->serial_running,
snapshot_has_alert(snapshot) ? "!" : ""); snapshot->serial_running ? "On" : NULL);
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_STATUS, 0U, 0U, line); (void)snprintf(value, sizeof(value), "%u",
format_text(line, "C%u U%s W%u S%u", clients, (unsigned int)(snapshot->wifi.sta_channel != 0U
snapshot->usb_available && snapshot->usb.dtr ? "1" : "0", web_sessions, ? snapshot->wifi.sta_channel
ssh_sessions); : snapshot->wifi.ap_channel));
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_STATUS, 0U, 8U, line); bool station_online = snapshot->wifi_available &&
snapshot->wifi.state == WIFI_MANAGER_STATE_ONLINE;
const uint8_t *wifi_icon = station_online
? wifi_bitmap(snapshot->wifi.sta_rssi)
: (snapshot->wifi.ap_running
? s_icon_access_point
: s_icon_wifi_full);
render_status_slot(1U, wifi_icon, wifi_active, wifi_active ? value : NULL);
render_status_slot(2U, s_icon_usb, usb_active,
usb_active ? (snapshot->usb.writer ? "Wr" : "Ob") : NULL);
(void)snprintf(value, sizeof(value), "%u",
(unsigned int)snapshot->web_serial.active_sessions);
render_status_slot(3U, s_icon_web, web_active, web_active ? value : NULL);
(void)snprintf(value, sizeof(value), "%u",
(unsigned int)snapshot->ssh.active_sessions);
render_status_slot(4U, s_icon_ssh, ssh_active, ssh_active ? value : NULL);
(void)snprintf(value, sizeof(value), "%u",
(unsigned int)snapshot->broker.connected_clients);
render_status_slot(5U, s_icon_clients, clients_active, clients_active ? value : NULL);
render_status_slot(6U, s_icon_alert, alert_active, alert_active ? "!" : NULL);
} }
static void render_overview_page(const local_status_snapshot_t *snapshot) static void render_overview_page(const local_status_snapshot_t *snapshot)
{ {
char line[LOCAL_STATUS_UI_TEXT_CAPACITY]; char line[LOCAL_STATUS_UI_TEXT_CAPACITY];
char address[16];
char ssid_display[9];
const char *ssid;
uint8_t channel;
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 0U, "OVERVIEW"); local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 0U, "Overview");
if (snapshot->serial_config_available) { if (snapshot->serial_config_available) {
format_text(line, "SER %s %" PRIu32 " %s%s%s", snapshot->serial_running ? "ON" : "OFF", format_text(line, "Serial:%" PRIu32 " %s%c%s", snapshot->serial_config.baud_rate,
snapshot->serial_config.baud_rate,
serial_config_data_bits_to_string(snapshot->serial_config.data_bits), serial_config_data_bits_to_string(snapshot->serial_config.data_bits),
serial_config_parity_to_string(snapshot->serial_config.parity), parity_letter(snapshot->serial_config.parity),
serial_config_stop_bits_to_string(snapshot->serial_config.stop_bits)); serial_config_stop_bits_to_string(snapshot->serial_config.stop_bits));
} else { } else {
format_text(line, "SER %s CONFIG N/A", snapshot->serial_running ? "ON" : "OFF"); format_text(line, "Serial:Unavailable");
} }
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 8U, line); draw_content_item(LOCAL_STATUS_ICON_SERIAL, 8U, line);
format_text(line, "BROKER %u WR %s", snapshot->broker_available ? snapshot->broker.connected_clients : 0U, format_text(line, "Clients:%u Wr:%s",
snapshot->broker_available && snapshot->broker.writer_id != SESSION_BROKER_NO_CLIENT snapshot->broker_available ? snapshot->broker.connected_clients : 0U,
? "SET" writer_type_to_string(snapshot));
: "NONE"); draw_content_item(LOCAL_STATUS_ICON_CLIENTS, 16U, line);
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 16U, line);
format_text(line, "USB %s WEB %u", snapshot->usb_available && snapshot->usb.dtr ? "OPEN" : "OFF", wifi_identity(snapshot, &ssid, &channel);
snapshot->web_serial_available ? snapshot->web_serial.active_sessions : 0U); format_ssid(ssid, ssid_display);
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 24U, line); format_text(line, "WiFi:%s Ch:%u", ssid_display, channel);
format_text(line, "HTTPS %s SSH %u", snapshot->web_available && snapshot->web.running ? "ON" : "OFF", draw_bitmap(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 24U,
snapshot->ssh_available ? snapshot->ssh.active_sessions : 0U); wifi_snapshot_bitmap(snapshot));
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 32U, line); local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 10U, 24U, line);
format_text(line, "WIFI %s", snapshot->wifi_available
? wifi_manager_state_to_string(snapshot->wifi.state) format_ipv4(snapshot->wifi.ip, address);
: "N/A"); format_text(line, "IP:%s", address);
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 40U, line); local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 10U, 32U, line);
format_text(line, "Web:%u SSH:%u USB:%u",
snapshot->web_serial_available ? snapshot->web_serial.active_sessions : 0U,
snapshot->ssh_available ? snapshot->ssh.active_sessions : 0U,
snapshot->usb_available && snapshot->usb.dtr ? 1U : 0U);
draw_content_item(LOCAL_STATUS_ICON_WEB, 40U, line);
} }
static void render_rs232_page(const local_status_snapshot_t *snapshot) static void render_rs232_page(const local_status_snapshot_t *snapshot)
{ {
char line[LOCAL_STATUS_UI_TEXT_CAPACITY]; char line[LOCAL_STATUS_UI_TEXT_CAPACITY];
char rx[8];
char tx[8];
char errors[8];
uint64_t faults = snapshot->serial_counters.frame_errors + uint64_t faults = snapshot->serial_counters.frame_errors +
snapshot->serial_counters.parity_errors + snapshot->serial_counters.parity_errors +
snapshot->serial_counters.fifo_overflows + snapshot->serial_counters.fifo_overflows +
snapshot->serial_counters.buffer_full_events + snapshot->serial_counters.buffer_full_events +
snapshot->serial_counters.breaks; snapshot->serial_counters.breaks;
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 0U, "RS232 MODEM"); local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 0U, "RS-232 / modem");
if (snapshot->serial_config_available) { if (snapshot->serial_config_available) {
format_text(line, "%" PRIu32 " %s%s%s %s", snapshot->serial_config.baud_rate, format_text(line, "Mode:%" PRIu32 " %s%c%s", snapshot->serial_config.baud_rate,
serial_config_data_bits_to_string(snapshot->serial_config.data_bits), serial_config_data_bits_to_string(snapshot->serial_config.data_bits),
serial_config_parity_to_string(snapshot->serial_config.parity), parity_letter(snapshot->serial_config.parity),
serial_config_stop_bits_to_string(snapshot->serial_config.stop_bits), serial_config_stop_bits_to_string(snapshot->serial_config.stop_bits));
serial_config_flow_control_to_string(snapshot->serial_config.flow_control));
} else { } else {
format_text(line, "CONFIG UNAVAILABLE"); format_text(line, "Mode:Unavailable");
}
draw_content_item(LOCAL_STATUS_ICON_SERIAL, 8U, line);
format_text(line, "Flow:%s", snapshot->serial_config_available
? serial_config_flow_control_to_string(
snapshot->serial_config.flow_control)
: "Unavailable");
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 10U, 16U, line);
format_text(line, "DCD:%d DSR:%d CTS:%d", snapshot->modem.dcd, snapshot->modem.dsr,
snapshot->modem.cts);
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 10U, 24U, line);
format_compact_u64(snapshot->serial_counters.rx_bytes, rx);
format_text(line, "Valid:%s RX:%s", snapshot->modem.valid ? "Yes" : "No", rx);
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 10U, 32U, line);
format_compact_u64(snapshot->serial_counters.tx_sent_to_uart_bytes, tx);
format_compact_u64(faults + snapshot->serial_counters.rx_dropped_bytes +
snapshot->serial_counters.tx_dropped_bytes,
errors);
format_text(line, "TX:%s Err:%s", tx, errors);
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 10U, 40U, line);
}
static local_status_icon_t client_icon(session_broker_client_type_t type)
{
switch (type) {
case SESSION_BROKER_CLIENT_USB:
return LOCAL_STATUS_ICON_USB;
case SESSION_BROKER_CLIENT_WEB:
return LOCAL_STATUS_ICON_WEB;
case SESSION_BROKER_CLIENT_SSH:
return LOCAL_STATUS_ICON_SSH;
default:
return LOCAL_STATUS_ICON_CLIENTS;
} }
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 8U, line);
format_text(line, "DCD%d DSR%d CTS%d RI%d", snapshot->modem.dcd, snapshot->modem.dsr,
snapshot->modem.cts, snapshot->modem.ri);
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 16U, line);
format_text(line, "VALID %s RX %" PRIu64, snapshot->modem.valid ? "YES" : "NO",
snapshot->serial_counters.rx_bytes);
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 24U, line);
format_text(line, "TX %" PRIu64 " Q%u", snapshot->serial_counters.tx_sent_to_uart_bytes,
(unsigned int)serial_service_tx_pending());
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 32U, line);
format_text(line, "DROP %" PRIu64 " ERR %" PRIu64,
snapshot->serial_counters.rx_dropped_bytes + snapshot->serial_counters.tx_dropped_bytes,
faults);
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 40U, line);
} }
static void render_broker_page(const local_status_snapshot_t *snapshot) static void render_broker_page(const local_status_snapshot_t *snapshot)
{ {
char line[LOCAL_STATUS_UI_TEXT_CAPACITY]; char line[LOCAL_STATUS_UI_TEXT_CAPACITY];
format_text(line, "BROKER C%u WR %s", snapshot->broker_available ? snapshot->broker.connected_clients : 0U, format_text(line, "Broker clients:%u",
snapshot->broker_available && snapshot->broker.writer_id != SESSION_BROKER_NO_CLIENT snapshot->broker_available ? snapshot->broker.connected_clients : 0U);
? "SET"
: "NONE");
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 0U, line); local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 0U, line);
for (size_t i = 0U; i < LOCAL_STATUS_UI_CLIENT_ROWS; ++i) { for (size_t i = 0U; i < LOCAL_STATUS_UI_CLIENT_ROWS && i < snapshot->client_count; ++i) {
if (i >= snapshot->client_count) {
break;
}
const session_broker_client_snapshot_t *client = &snapshot->clients[i]; const session_broker_client_snapshot_t *client = &snapshot->clients[i];
format_text(line, "%c%s %s Q%u", client->is_writer ? 'W' : 'O', format_text(line, "%s:%s Q:%u", client->is_writer ? "Wr" : "Ob",
broker_type_to_string(client->type), client->name, broker_type_to_string(client->type),
(unsigned int)client->output_bytes_pending); (unsigned int)client->output_bytes_pending);
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, draw_content_item(client_icon(client->type), (uint8_t)((i + 1U) * 8U), line);
(uint8_t)((i + 1U) * 8U), line);
} }
if (snapshot->broker_available) { if (snapshot->broker_available) {
format_text(line, "DROP %" PRIu64 " EVT %" PRIu64, char drops[8];
snapshot->broker.counters.output_dropped_bytes, char events[8];
snapshot->broker.counters.event_drops); format_compact_u64(snapshot->broker.counters.output_dropped_bytes, drops);
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 40U, line); format_compact_u64(snapshot->broker.counters.event_drops, events);
format_text(line, "Drop:%s Events:%s", drops, events);
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 10U, 40U, line);
} }
} }
@@ -286,29 +601,31 @@ static void render_network_page(const local_status_snapshot_t *snapshot)
{ {
char line[LOCAL_STATUS_UI_TEXT_CAPACITY]; char line[LOCAL_STATUS_UI_TEXT_CAPACITY];
char address[16]; char address[16];
char ssid_display[9];
const char *ssid;
uint8_t channel;
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 0U, "NETWORK SERVICES"); local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 0U, "Network / services");
if (snapshot->wifi_available) { wifi_identity(snapshot, &ssid, &channel);
format_text(line, "WF %s %dDB", wifi_manager_state_to_string(snapshot->wifi.state), format_ssid(ssid, ssid_display);
snapshot->wifi.sta_rssi); format_text(line, "WiFi:%s Ch:%u", ssid_display, channel);
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 8U, line); draw_bitmap(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 8U,
format_ipv4(snapshot->wifi.ip, address); wifi_snapshot_bitmap(snapshot));
format_text(line, "IP %s", address); local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 10U, 8U, line);
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 16U, line); format_ipv4(snapshot->wifi.ip, address);
format_text(line, "AP %s C%u", snapshot->wifi.ap_running ? "ON" : "OFF", format_text(line, "IP:%s", address);
snapshot->wifi.ap_client_count); local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 10U, 16U, line);
} else { format_text(line, "AP:%s Clients:%u", on_off(snapshot->wifi.ap_running),
format_text(line, "WIFI UNAVAILABLE"); snapshot->wifi.ap_client_count);
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 8U, line); local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 10U, 24U, line);
format_text(line, "AP N/A"); format_text(line, "HTTPS:%s Web:%u",
} on_off(snapshot->web_available && snapshot->web.running),
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 24U, line);
format_text(line, "HTTPS %s WEB %u", snapshot->web_available && snapshot->web.running ? "ON" : "OFF",
snapshot->web_serial_available ? snapshot->web_serial.active_sessions : 0U); snapshot->web_serial_available ? snapshot->web_serial.active_sessions : 0U);
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 32U, line); draw_content_item(LOCAL_STATUS_ICON_WEB, 32U, line);
format_text(line, "SSH %s S%u", snapshot->ssh_available && snapshot->ssh.running ? "ON" : "OFF", format_text(line, "SSH:%s Sessions:%u",
on_off(snapshot->ssh_available && snapshot->ssh.running),
snapshot->ssh_available ? snapshot->ssh.active_sessions : 0U); snapshot->ssh_available ? snapshot->ssh.active_sessions : 0U);
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 40U, line); draw_content_item(LOCAL_STATUS_ICON_SSH, 40U, line);
} }
static void render_page(local_status_page_t page, const local_status_snapshot_t *snapshot) static void render_page(local_status_page_t page, const local_status_snapshot_t *snapshot)
@@ -316,6 +633,10 @@ static void render_page(local_status_page_t page, const local_status_snapshot_t
if (local_display_frame_begin() != ESP_OK) { if (local_display_frame_begin() != ESP_OK) {
return; return;
} }
if (diagnostic_hold_is_active(xTaskGetTickCount())) {
local_display_frame_cancel();
return;
}
local_display_frame_clear_all(); local_display_frame_clear_all();
render_status_panel(snapshot); render_status_panel(snapshot);
@@ -367,37 +688,124 @@ static bool poll_buttons(local_status_button_state_t states[LOCAL_STATUS_BUTTON_
return event; return event;
} }
static void sync_display_power(local_status_power_t *power)
{
local_display_snapshot_t snapshot;
if (local_display_get_snapshot(&snapshot) != ESP_OK) {
return;
}
if (!snapshot.initialized) {
*power = LOCAL_STATUS_POWER_OFF;
} else if (snapshot.contrast <= LOCAL_STATUS_UI_DIM_CONTRAST) {
*power = LOCAL_STATUS_POWER_DIMMED;
} else {
*power = LOCAL_STATUS_POWER_ACTIVE;
}
}
static bool wake_display(local_status_power_t *power)
{
local_display_snapshot_t snapshot;
bool initialized = local_display_get_snapshot(&snapshot) == ESP_OK && snapshot.initialized;
esp_err_t error = ESP_OK;
if (*power == LOCAL_STATUS_POWER_OFF || !initialized) {
error = local_display_start();
} else if (*power == LOCAL_STATUS_POWER_DIMMED || snapshot.contrast != LOCAL_STATUS_UI_ACTIVE_CONTRAST) {
error = local_display_set_contrast(LOCAL_STATUS_UI_ACTIVE_CONTRAST);
}
if (error != ESP_OK) {
ESP_LOGW(TAG, "Display wake failed: %s", esp_err_to_name(error));
return false;
}
*power = LOCAL_STATUS_POWER_ACTIVE;
return true;
}
static void update_display_power(local_status_power_t *power,
TickType_t now,
TickType_t last_activity)
{
TickType_t inactive = now - last_activity;
bool should_stop = *power != LOCAL_STATUS_POWER_OFF &&
inactive >= pdMS_TO_TICKS(LOCAL_STATUS_UI_OFF_DELAY_MS);
bool should_dim = *power == LOCAL_STATUS_POWER_ACTIVE &&
inactive >= pdMS_TO_TICKS(LOCAL_STATUS_UI_DIM_DELAY_MS);
if ((!should_stop && !should_dim) || s_diagnostic_gate == NULL ||
xSemaphoreTake(s_diagnostic_gate, pdMS_TO_TICKS(100U)) != pdTRUE) {
return;
}
/* A diagnostic publishes its hold before waiting on this gate. */
if (diagnostic_hold_is_active(xTaskGetTickCount())) {
(void)xSemaphoreGive(s_diagnostic_gate);
return;
}
if (should_stop) {
esp_err_t error = local_display_stop();
if (error == ESP_OK) {
*power = LOCAL_STATUS_POWER_OFF;
}
} else {
esp_err_t error = local_display_set_contrast(LOCAL_STATUS_UI_DIM_CONTRAST);
if (error == ESP_OK) {
*power = LOCAL_STATUS_POWER_DIMMED;
}
}
(void)xSemaphoreGive(s_diagnostic_gate);
}
static void local_status_ui_task(void *context) static void local_status_ui_task(void *context)
{ {
(void)context; (void)context;
local_status_page_t page = LOCAL_STATUS_PAGE_OVERVIEW; local_status_page_t page = LOCAL_STATUS_PAGE_OVERVIEW;
local_status_power_t power = LOCAL_STATUS_POWER_ACTIVE;
local_status_button_state_t buttons[LOCAL_STATUS_BUTTON_COUNT] = {0}; local_status_button_state_t buttons[LOCAL_STATUS_BUTTON_COUNT] = {0};
TickType_t last_render = 0U; TickType_t last_render = 0U;
TickType_t last_activity = xTaskGetTickCount();
uint32_t observed_activity_sequence = 0U;
for (size_t i = 0U; i < LOCAL_STATUS_BUTTON_COUNT; ++i) { for (size_t i = 0U; i < LOCAL_STATUS_BUTTON_COUNT; ++i) {
bool pressed = gpio_get_level(s_button_gpios[i]) == 0; bool pressed = gpio_get_level(s_button_gpios[i]) == 0;
buttons[i].stable_pressed = pressed; buttons[i].stable_pressed = pressed;
buttons[i].candidate_pressed = pressed; buttons[i].candidate_pressed = pressed;
buttons[i].candidate_since = xTaskGetTickCount(); buttons[i].candidate_since = last_activity;
} }
for (;;) { for (;;) {
local_status_button_t button; local_status_button_t button;
bool button_event = poll_buttons(buttons, &button); bool button_event = poll_buttons(buttons, &button);
bool render_requested = false;
TickType_t now = xTaskGetTickCount();
if (consume_external_activity(&observed_activity_sequence)) {
last_activity = now;
}
sync_display_power(&power);
if (button_event) { if (button_event) {
if (button == LOCAL_STATUS_BUTTON_PREVIOUS) { last_activity = now;
if (power != LOCAL_STATUS_POWER_ACTIVE) {
/* The first press wakes only, preventing accidental navigation. */
render_requested = wake_display(&power);
} else if (button == LOCAL_STATUS_BUTTON_PREVIOUS) {
page = page == LOCAL_STATUS_PAGE_OVERVIEW page = page == LOCAL_STATUS_PAGE_OVERVIEW
? LOCAL_STATUS_PAGE_COUNT - 1U ? LOCAL_STATUS_PAGE_COUNT - 1U
: (local_status_page_t)(page - 1U); : (local_status_page_t)(page - 1U);
render_requested = true;
} else if (button == LOCAL_STATUS_BUTTON_NEXT) { } else if (button == LOCAL_STATUS_BUTTON_NEXT) {
page = (local_status_page_t)((page + 1U) % LOCAL_STATUS_PAGE_COUNT); page = (local_status_page_t)((page + 1U) % LOCAL_STATUS_PAGE_COUNT);
render_requested = true;
} else {
render_requested = true;
} }
} }
TickType_t now = xTaskGetTickCount(); update_display_power(&power, now, last_activity);
bool diagnostics_held = (int32_t)(now - s_diagnostic_hold_until) < 0; bool diagnostics_held = diagnostic_hold_is_active(now);
if (!diagnostics_held && if (power != LOCAL_STATUS_POWER_OFF && !diagnostics_held &&
(button_event || (now - last_render) >= pdMS_TO_TICKS(LOCAL_STATUS_UI_REFRESH_MS))) { (render_requested ||
(now - last_render) >= pdMS_TO_TICKS(LOCAL_STATUS_UI_REFRESH_MS))) {
local_status_snapshot_t snapshot; local_status_snapshot_t snapshot;
collect_snapshot(&snapshot); collect_snapshot(&snapshot);
render_page(page, &snapshot); render_page(page, &snapshot);
@@ -409,25 +817,42 @@ static void local_status_ui_task(void *context)
void local_status_ui_hold_for_diagnostics(void) void local_status_ui_hold_for_diagnostics(void)
{ {
s_diagnostic_hold_until = portENTER_CRITICAL(&s_timing_mux);
xTaskGetTickCount() + pdMS_TO_TICKS(LOCAL_STATUS_UI_DIAGNOSTIC_HOLD_MS); s_diagnostic_hold_started = xTaskGetTickCount();
s_diagnostic_hold_active = true;
++s_external_activity_sequence;
portEXIT_CRITICAL(&s_timing_mux);
/* Wait for any dim/off operation that began before the hold was published. */
if (s_diagnostic_gate != NULL &&
xSemaphoreTake(s_diagnostic_gate, pdMS_TO_TICKS(500U)) == pdTRUE) {
(void)xSemaphoreGive(s_diagnostic_gate);
}
} }
esp_err_t local_status_ui_start(void) esp_err_t local_status_ui_start(void)
{ {
if (s_task != NULL) { if (s_task != NULL || s_diagnostic_gate != NULL) {
return ESP_ERR_INVALID_STATE; return ESP_ERR_INVALID_STATE;
} }
s_diagnostic_gate = xSemaphoreCreateMutexStatic(&s_diagnostic_gate_storage);
if (s_diagnostic_gate == NULL) {
return ESP_ERR_NO_MEM;
}
BaseType_t created = xTaskCreate(local_status_ui_task, "local_status_ui", BaseType_t created = xTaskCreate(local_status_ui_task, "local_status_ui",
LOCAL_STATUS_UI_TASK_STACK_SIZE, NULL, LOCAL_STATUS_UI_TASK_STACK_SIZE, NULL,
LOCAL_STATUS_UI_TASK_PRIORITY, &s_task); LOCAL_STATUS_UI_TASK_PRIORITY, &s_task);
if (created != pdPASS) { if (created != pdPASS) {
s_task = NULL; s_task = NULL;
vSemaphoreDelete(s_diagnostic_gate);
s_diagnostic_gate = NULL;
return ESP_ERR_NO_MEM; return ESP_ERR_NO_MEM;
} }
ESP_LOGI(TAG, "Read-only status UI started at %u Hz maximum", ESP_LOGI(TAG,
"Read-only status UI started at %u Hz maximum; dim/off after 5/10 minutes",
1000U / LOCAL_STATUS_UI_REFRESH_MS); 1000U / LOCAL_STATUS_UI_REFRESH_MS);
return ESP_OK; return ESP_OK;
} }
+1 -1
View File
@@ -38,7 +38,7 @@ static const char *TAG = "firmware";
void app_main(void) void app_main(void)
{ {
ESP_LOGI(TAG, "ESP32-S3 Serial Swiss Army Knife Phase 7B display driver started"); ESP_LOGI(TAG, "ESP32-S3 Serial Swiss Army Knife Phase 7C local status UI 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());
+14 -7
View File
@@ -233,12 +233,17 @@ static uint8_t build_profile_order(uint8_t *order, const wifi_app_config_t *conf
return count; return count;
} }
static void note_ap_running(bool running, uint8_t channel) static void note_ap_running(bool running, const wifi_app_config_t *config)
{ {
lock_shared(); lock_shared();
bool changed = s_shared.snapshot.ap_running != running; bool changed = s_shared.snapshot.ap_running != running;
s_shared.snapshot.ap_running = running; s_shared.snapshot.ap_running = running;
s_shared.snapshot.ap_channel = channel; s_shared.snapshot.ap_channel = config->ap_channel;
if (running) {
s_shared.snapshot.ap_ssid_len = config->ap_ssid_len;
memset(s_shared.snapshot.ap_ssid, 0, sizeof(s_shared.snapshot.ap_ssid));
memcpy(s_shared.snapshot.ap_ssid, config->ap_ssid, config->ap_ssid_len);
}
if (!running) { if (!running) {
s_shared.snapshot.ap_client_count = 0U; s_shared.snapshot.ap_client_count = 0U;
} }
@@ -325,7 +330,7 @@ static esp_err_t set_runtime_ap_enabled(manager_runtime_t *runtime, bool enabled
} }
if (error == ESP_OK) { if (error == ESP_OK) {
runtime->ap_enabled = true; runtime->ap_enabled = true;
note_ap_running(true, config.ap_channel); note_ap_running(true, &config);
} else { } else {
/* Do not leave an untracked default/stale AP active after failure. */ /* Do not leave an untracked default/stale AP active after failure. */
esp_err_t rollback_error = esp_wifi_set_mode(WIFI_MODE_STA); esp_err_t rollback_error = esp_wifi_set_mode(WIFI_MODE_STA);
@@ -338,7 +343,7 @@ static esp_err_t set_runtime_ap_enabled(manager_runtime_t *runtime, bool enabled
error = esp_wifi_set_mode(WIFI_MODE_STA); error = esp_wifi_set_mode(WIFI_MODE_STA);
if (error == ESP_OK) { if (error == ESP_OK) {
runtime->ap_enabled = false; runtime->ap_enabled = false;
note_ap_running(false, config.ap_channel); note_ap_running(false, &config);
} }
} }
@@ -379,7 +384,7 @@ static void stop_radio(manager_runtime_t *runtime)
wifi_app_config_t config; wifi_app_config_t config;
copy_working_config(&config); copy_working_config(&config);
if (runtime->ap_enabled) { if (runtime->ap_enabled) {
note_ap_running(false, config.ap_channel); note_ap_running(false, &config);
} }
wifi_config_secure_wipe(&config, sizeof(config)); wifi_config_secure_wipe(&config, sizeof(config));
@@ -551,7 +556,7 @@ static void start_radio_and_policy(manager_runtime_t *runtime)
runtime->radio_started = true; runtime->radio_started = true;
runtime->ap_enabled = want_ap; runtime->ap_enabled = want_ap;
set_last_error(ESP_OK); set_last_error(ESP_OK);
note_ap_running(want_ap, config.ap_channel); note_ap_running(want_ap, &config);
if (runtime->profile_count == 0U) { if (runtime->profile_count == 0U) {
set_active_profile(-1, NULL); set_active_profile(-1, NULL);
@@ -1178,6 +1183,9 @@ esp_err_t wifi_manager_init(const wifi_app_config_t *config)
s_shared.snapshot.state = WIFI_MANAGER_STATE_STOPPED; s_shared.snapshot.state = WIFI_MANAGER_STATE_STOPPED;
s_shared.snapshot.active_profile = -1; s_shared.snapshot.active_profile = -1;
s_shared.snapshot.ap_policy = config->ap_policy; s_shared.snapshot.ap_policy = config->ap_policy;
s_shared.snapshot.ap_ssid_len = config->ap_ssid_len;
memcpy(s_shared.snapshot.ap_ssid, config->ap_ssid, config->ap_ssid_len);
s_shared.snapshot.ap_ssid[config->ap_ssid_len] = '\0';
s_shared.snapshot.ap_channel = config->ap_channel; s_shared.snapshot.ap_channel = config->ap_channel;
s_shared.snapshot.sta_auth = WIFI_AUTH_OPEN; s_shared.snapshot.sta_auth = WIFI_AUTH_OPEN;
s_shared.snapshot.last_error = ESP_OK; s_shared.snapshot.last_error = ESP_OK;
@@ -1302,7 +1310,6 @@ esp_err_t wifi_manager_apply_working_config(const wifi_app_config_t *config)
s_shared.snapshot.config_generation = 1U; s_shared.snapshot.config_generation = 1U;
} }
s_shared.snapshot.ap_policy = config->ap_policy; s_shared.snapshot.ap_policy = config->ap_policy;
s_shared.snapshot.ap_channel = config->ap_channel;
++s_shared.snapshot.counters.applies; ++s_shared.snapshot.counters.applies;
unlock_shared(); unlock_shared();
return ESP_OK; return ESP_OK;
+2
View File
@@ -60,6 +60,8 @@ typedef struct {
wifi_config_ap_policy_t ap_policy; wifi_config_ap_policy_t ap_policy;
bool ap_running; bool ap_running;
uint8_t ap_ssid_len;
char ap_ssid[WIFI_CONFIG_SSID_MAX_LEN + 1U];
uint8_t ap_client_count; uint8_t ap_client_count;
uint8_t ap_channel; uint8_t ap_channel;
+20
View File
@@ -0,0 +1,20 @@
Pictogrammers Free License
--------------------------
This icon collection is released as free, open source, and GPL friendly by
the [Pictogrammers](http://pictogrammers.com/) icon group. You may use it
for commercial projects, open source projects, or anything really.
# Icons: Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0)
Some of the icons are redistributed under the Apache 2.0 license. All other
icons are either redistributed under their respective licenses or are
distributed under the Apache 2.0 license.
# Fonts: Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0)
All web and desktop fonts are distributed under the Apache 2.0 license. Web
and desktop fonts contain some icons that are redistributed under the Apache
2.0 license. All other icons are either redistributed under their respective
licenses or are distributed under the Apache 2.0 license.
# Code: MIT (https://opensource.org/licenses/MIT)
The MIT license applies to all non-font and non-icon files.
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" id="mdi-usb" viewBox="0 0 24 24"><path d="M15,7V11H16V13H13V5H15L12,1L9,5H11V13H8V10.93C8.7,10.56 9.2,9.85 9.2,9C9.2,7.78 8.21,6.8 7,6.8C5.78,6.8 4.8,7.78 4.8,9C4.8,9.85 5.3,10.56 6,10.93V13A2,2 0 0,0 8,15H11V18.05C10.29,18.41 9.8,19.15 9.8,20A2.2,2.2 0 0,0 12,22.2A2.2,2.2 0 0,0 14.2,20C14.2,19.15 13.71,18.41 13,18.05V15H16A2,2 0 0,0 18,13V11H19V7H15Z" /></svg>

After

Width:  |  Height:  |  Size: 403 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" id="mdi-wifi-strength-4" viewBox="0 0 24 24"><path d="M12,3C7.79,3 3.7,4.41 0.38,7C4.41,12.06 7.89,16.37 12,21.5C16.08,16.42 20.24,11.24 23.65,7C20.32,4.41 16.22,3 12,3Z" /></svg>

After

Width:  |  Height:  |  Size: 219 B