PixelStatus NX is an ESP32-based ambient status-monitoring and visualization system. It is conceived as an intellectual successor to the abandoned 2015 eins78/pixelstatus project, while substantially generalizing its monitoring, state, rendering, and hardware architecture.
The name NX means NeXt.
The core idea is simple:
Collect simple status information from LAN, Internet, and peer systems, normalize that information into named states, and render those states as persistent ambient visual indicators.
PixelStatus NX is deliberately not intended to become a general-purpose monitoring platform such as Nagios, Zabbix, Prometheus, or Home Assistant. It handles a useful set of simple monitoring primitives itself. Anything requiring sophisticated computation, scripting, authentication workflows, aggregation, or domain-specific logic should be monitored externally and expose its resulting state to PixelStatus NX through a push or query interface.
The current target hardware includes an ESP32 and a 16×16 RGB Bluetooth-connected display, but neither the monitoring system nor rendering architecture should be coupled to that particular display.
Development begins with a portable C++20 core, a native Win32 display simulator,
and a headless Linux browser runtime. This permits configuration, state, timing,
appearance, layout, framebuffer, monitoring, and output-driver behavior to be
developed without ESP32 tools or hardware. See
Host-First Development for the Windows workflow and
Linux Host for a clean-machine Linux bootstrap, browser
runtime, BlueZ/Bleak bridge, and persistent user-service setup.
The authoritative feature matrix, live-validation boundary, and consolidated
backlog are in Implementation Status and Loose Ends.
In the numbered design sections below, should, may, possible, and
eventually describe intended architecture unless an implementation note or the
status ledger says the feature is complete.
The current host implementation also accepts authenticated status pushes over a
loopback HTTP endpoint; its transport-independent handler is intended to be reused
behind the eventual ESP32 HTTP server. The supported JSON appearance syntax is
documented in Configuration V1.
The host monitor-engine layer—normalized monitor results, ordered evaluation rules,
and deterministic interval scheduling—is described in
Host Monitor Engine.
Declarative HTTP monitoring with bounded methods, headers, request/response bodies,
timeouts, status/body/JSON-Pointer observation, JSON array counts, numeric ratios,
and a loopback-tested desktop adapter now uses those contracts. The authenticated
appliance-monitor.example.json fixture
demonstrates TrueNAS/UniFi-shaped health, alert, storage, and WAN values. A
bounded desktop worker pool prevents one slow check from stalling unrelated
monitors or either display backend.
TCP-connect monitoring uses the same scheduler and evaluator while exposing
successful connection latency as a numeric observation.
Bounded TCP-exchange monitoring adds optional text transmission, delimiter-based
response capture, and body or total-latency observation for simple service checks.
Standalone DNS monitoring adds address-family filtering plus address-list, count,
and lookup-latency observations without changing those contracts.
Bounded ICMP echo monitoring adds a latency-valued reachability check for hosts
that expose no suitable application service.
The same rendered framebuffer is also available through a responsive
Browser Display Backend, including a read-only frame API and
headless host mode.
The renderer can cycle bitmap, two-line local/UTC clock, live indicator, and
composite cards through instant, fade, or four-direction slide transitions.
Composite layouts support explicit AABBs, deterministic row/column splits, and
back-to-front stacks plus status indicators, aggregate-status backgrounds, clocks,
numeric utilization bars, status grids, and bounded bitmaps. See
Card Decks and the runnable
card-deck.example.json.
Complete split and layered examples are
split-layout.example.json and
layered-layout.example.json.
Site-specific monitor inventories belong in the ignored
examples/operations.local.json profile so LAN addresses, private hostnames, and
operational notes cannot be staged accidentally.
The implemented WinHTTP/Schannel desktop TLS path, planned ESP32 TLS path, named
secret handling, and integration requirements for TrueNAS CORE, UniFi, Netgear
cable modems, and Starlink are described in
Appliance Monitoring and TLS.
The Windows host can already query the official local UniFi Network API through a
loopback-only, certificate-pinned collector documented in
UniFi Network Monitoring; its API key remains a named
Windows Credential Manager secret.
The same host-first boundary now includes a certificate-pinned, read-only OpenWrt
collector for Wi-Fi bridge and Starlink router-path status, documented in
OpenWrt Starlink Bridge Monitoring.
For unattended use during development, a tested snapshot can be published outside
the working tree and run at interactive user logon as described in
Windows Published Runtime and Task Scheduler.
Linux can run the source-tree release build and optional BLE bridge as supervised
systemd user services described in Linux Host.
The staged host-layout, live-data, Windows Bluetooth, and eventual ESP32 work is
tracked in the Development Roadmap.
PixelStatus NX should be:
- Small enough to run comfortably on an ESP32.
- Reliable enough to operate continuously as an appliance.
- Configurable without recompiling firmware.
- Capable of both pull-based monitoring and push-based status updates.
- Independent of any particular physical display.
- Capable of driving both remotely connected and directly connected displays.
- Able to express more information than simple red/green status lights.
- Able to define arbitrary named statuses.
- Able to associate each status with a time-varying visual appearance.
- Extensible through well-defined monitor, evaluator, renderer, and output-driver interfaces.
- Understandable enough that configuration remains declarative rather than becoming a programming language.
It should explicitly avoid:
- arbitrary shell execution;
- embedded Unix-like scripting;
- attempting to duplicate full monitoring systems;
- direct coupling between monitoring code and LED hardware;
- hard-coding the current 16×16 physical geometry into the monitoring model;
- forcing all statuses into
OK/FAIL.
The original PixelStatus project implemented approximately:
Task
↓
Runner
↓
Expectation
↓
Pass / Fail
↓
Reaction
↓
Solid color applied to LED section
Its useful architectural idea was the separation between:
- obtaining information;
- evaluating that information;
- deciding what status it represented;
- displaying the result.
PixelStatus NX retains that conceptual separation while replacing the original linear LED-section model with a substantially more general architecture:
┌────────────────────┐
│ Push Sources │
│ │
│ HTTP / MQTT / etc. │
└─────────┬──────────┘
│
│
┌───────────────────┐ │
│ Pull Scheduler │ │
│ │ │
│ periodic / cron │ │
└─────────┬─────────┘ │
│ │
v │
┌───────────────────┐ │
│ Monitor Runner │ │
│ │ │
│ HTTP / Ping / │ │
│ DNS / SNMP / TCP │ │
└─────────┬─────────┘ │
│ │
v │
┌───────────────────┐ │
│ Evaluator │ │
│ │ │
│ raw result → │ │
│ named status │ │
└─────────┬─────────┘ │
│ │
└────────────┬───────────┘
v
┌───────────────┐
│ State Store │
└───────┬───────┘
│
v
┌───────────────┐
│ Renderer │
└───────┬───────┘
│
v
┌───────────────┐
│ Frame / Scene │
└───────┬───────┘
│
┌─────────┴─────────┐
v v
Bluetooth display Direct LED driver
No source, monitor, or evaluator should need to know how the resulting state is physically displayed.
Initial development should target an ESP32, preferably an ESP32-S3 with sufficient flash and PSRAM.
The ESP32 provides:
- Wi-Fi;
- Bluetooth LE;
- TCP/IP networking;
- TLS;
- FreeRTOS;
- persistent flash storage;
- timers;
- OTA firmware updating;
- sufficient CPU and memory for the intended workload.
The current development display is:
- 16×16 pixels;
- RGB;
- 256 total pixels;
- externally controlled;
- connected over Bluetooth Low Energy using GATT writes.
The device-validated single-pixel and full-frame block protocols are documented in MI LED Display Python Development Handoff. The live framebuffer bridge now drives the user's display through Windows/Bleak in both modes. See the runnable Windows MI Bluetooth Bridge. Upper-left row-major RGB mapping, top-to-bottom two-row blocks, and browser/physical parity are confirmed. A forced interrupted transfer and longer soak still require device validation.
The current device is an important target, but must not define the core architecture.
Rendering and physical output must be separate layers.
A renderer should produce an abstract frame or scene. An output driver translates that representation into whatever protocol the attached display requires.
Conceptually:
Monitor states
↓
Layout / Renderer
↓
Logical framebuffer
↓
OutputDriver
↓
Physical device
The first output drivers should include:
For the existing 16×16 display.
Responsibilities include:
- BLE discovery or configured device binding;
- connection/reconnection;
- protocol encoding;
- frame updates;
- brightness if supported;
- connection-state reporting.
The production firmware implementation should be a native ESP-IDF C/C++ component
using the NimBLE central/GATT-client APIs. The desktop Python/bleak implementation
is a protocol reference, diagnostic tool, and test oracle; it is not a runtime
dependency of the ESP32 firmware.
Support directly attached hardware such as:
- WS2812 / NeoPixel;
- SK6812;
- possibly APA102 or similar devices.
The direct driver should allow users to build PixelStatus NX hardware without the Bluetooth display.
A host-side or development-mode output that displays the logical framebuffer without physical hardware.
Useful for:
- unit testing;
- layout development;
- appearance-rule development;
- CI;
- screenshots;
- debugging.
Additional output drivers should be possible without altering monitor or renderer code.
The Windows host build includes both a native Win32 driver and an HTTP browser driver. Linux builds the same HTTP driver as a headless browser-only runtime. Both paths consume the same logical frames used by the BLE and future firmware drivers.
Example interface:
enum class FrameSubmitResult {
accepted,
coalesced,
unavailable,
};
class OutputDriver {
public:
virtual bool begin() = 0;
virtual FrameSubmitResult submitFrame(const Frame& frame) = 0;
virtual DriverState state() const = 0;
};Frame submission should not require the renderer to wait for physical transport.
A slow driver may retain or copy the newest frame, replace an older pending frame,
and report that coalescing occurred. Acceptance means that the driver took
responsibility for the frame; it does not guarantee that the physical display has
already received it. Transport errors and reconnect progress are exposed through
DriverState.
Output capabilities may differ, so drivers may eventually expose capabilities such as:
dimensions
color depth
maximum frame rate
brightness support
per-pixel support
animation support
orientation
The architecture should not assume every output is necessarily 16×16.
The renderer should operate on a logical display surface rather than raw physical LED indexes.
For the current hardware:
width = 16
height = 16
pixels = 256
But these values should come from display configuration or driver capabilities.
At minimum:
struct RGB {
uint8_t r;
uint8_t g;
uint8_t b;
};
struct Frame {
unsigned width;
unsigned height;
RGB pixels[];
};A more sophisticated scene representation may sit above the framebuffer.
A monitor obtains or receives state information.
An indicator determines where and how that information appears.
For example:
Monitor:
id: web-prod
type: http
Indicator:
source: web-prod
x: 0
y: 0
width: 4
height: 4
This separation permits:
- one monitor to drive multiple visual elements;
- one indicator to change layout without changing monitoring;
- multiple indicators to derive from a single state;
- hardware geometry to change independently of monitoring configuration.
PixelStatus NX should implement simple checks directly.
A check should be something that can reasonably be described declaratively:
query this thing
obtain this result
compare it against this rule
produce this status
It should not support arbitrary command execution or general scripting.
For example, this Unix-style monitor:
curl -sf https://server/api/status |
jq -e '.database.replication_lag < 30'should either become a declarative HTTP/JSON monitor:
type: http_json
url: https://server/api/status
expect:
path: database.replication_lag
less_than: 30or, if determining health requires substantially more logic, an external monitoring system should perform that computation and publish:
{
"id": "database",
"status": "ok"
}to PixelStatus NX.
Pull monitors execute according to schedules maintained by PixelStatus NX.
Example:
Scheduler
↓
Monitor becomes due
↓
Work queue
↓
Runner
↓
Result
↓
Evaluator
↓
State Store
A bounded number of monitors should execute concurrently.
There is little reason to launch dozens of simultaneous TLS sessions from an ESP32.
A small worker pool, perhaps 2–4 simultaneous network operations, is sufficient.
Support two scheduling styles.
Implementation status: deterministic interval scheduling and a bounded one-to-eight worker desktop executor are complete on Windows and Linux. Jitter and cron-like schedules are planned, not accepted by Configuration V1.
Preferred for normal health checks:
interval: 30sExamples:
5s
30s
2m
15m
1h
Optional jitter should prevent synchronized bursts when many monitors use identical intervals.
Example:
interval: 60s
jitter: 5sUseful for time-dependent checks:
schedule: "0 8 * * MON-FRI"Cron syntax need not initially implement every obscure cron feature.
System time should be synchronized using SNTP/NTP.
Implementation status: Windows runner types are icmp_ping, dns, tcp_connect,
tcp_exchange, and http (including HTTPS). Linux implements all of those except
icmp_ping; its HTTPS path uses OpenSSL and system trust. Their exact JSON contract
is in Configuration V1. Examples in this architectural
section may describe richer future observations than the V1 schema.
Check host reachability.
Possible observations:
- successful replies;
- packet loss;
- average RTT;
- maximum RTT;
- timeout.
Example:
type: ping
host: router.local
count: 3
timeout: 1s
expect:
received:
greater_or_equal: 2
avg_rtt:
less_than: 100msResolve a hostname.
Checks might include:
- successfully resolves;
- resolves to expected IPv4 address;
- resolves to expected IPv6 address;
- response latency.
Example:
type: dns
host: internal.example.com
expect:
ipv4: 192.168.10.20Attempt a connection to a specified port.
Example:
type: tcp_connect
host: server.local
port: 22
timeout: 2sUseful for determining whether:
- SSH;
- SMTP;
- database;
- web;
- custom TCP service
is accepting connections.
Connect, optionally transmit a defined payload, read a limited response, and evaluate it.
Example:
type: tcp_exchange
host: mail.example.com
port: 25
expect:
contains: "220"This covers many simple service checks without implementing full application protocols.
The implemented V1 grammar uses a bounded optional send string, required
read_until delimiter, and either body or total-latency observation; see
Configuration V1 for the runnable contract.
This should be one of the most capable built-in monitors.
Configuration may include:
URL
method
headers
authentication
request body
timeout
maximum response size
Evaluation may include:
HTTP status
header value
body contains
body equals
response latency
Example:
type: http
url: https://example.com/health
expect:
status:
between: [200, 299]
body:
contains: "healthy"
latency:
less_than: 2sJSON APIs deserve first-class handling.
Implementation status: JSON is part of the implemented http monitor rather than
a separate http_json type. V1 supports an RFC 6901 scalar pointer, selected-array
length, or scaled ratio of two numeric pointers. The dotted/indexed notation below
is illustrative design history and is not accepted by the current parser.
Example:
type: http_json
url: https://server.local/api/status
expect:
path: database.health
equals: healthyNumeric example:
expect:
path: storage.percent_used
less_than: 90A simple JSON path grammar is preferable to embedding a full query language.
For example:
database.health
services.web.state
disks[0].percent_used
TLS checking can be useful independently of HTTP.
Implementation status: HTTPS transport with Schannel system trust and hostname validation is complete on Windows. A standalone TLS/certificate-expiry monitor is not implemented.
Example:
type: tls
host: example.com
port: 443
expect:
valid: true
expires_in:
greater_than: 14dPossible checks:
- connection succeeds;
- TLS handshake succeeds;
- certificate chain validates;
- hostname matches;
- certificate expiration threshold.
SNMP is practical on ESP32 and useful enough to warrant support.
Implementation status: SNMP remains planned; neither the host schema nor a runner currently accepts it.
Initial scope should be deliberately narrow:
- SNMP v1;
- SNMP v2c;
- GET requests;
- common scalar values.
Possible types:
INTEGER
OCTET STRING
Counter32
Counter64
Gauge32
TimeTicks
Example:
type: snmp
host: switch.local
version: 2c
community: public
oid: 1.3.6.1.2.1.2.2.1.8.5
expect:
equals: 1Potential uses:
- router state;
- switch interfaces;
- UPS battery;
- temperature;
- printer state;
- toner level;
- storage equipment;
- network appliances.
Initially exclude:
- SET;
- WALK;
- extensive MIB interpretation;
- SNMPv3.
Those can be added if justified.
Candidates for later versions include:
UDP request/response
NTP
mDNS service discovery
MQTT query/subscription
Modbus/TCP
These should only be added when they provide substantial utility without turning the firmware into a protocol collection.
Push sources allow another system to tell PixelStatus NX what state should be displayed.
This is important because it provides the escape hatch for complex monitoring.
Example:
External monitoring software
↓
evaluates
↓
determines service state
↓
PixelStatus NX API
PixelStatus NX need not understand how the state was derived.
PixelStatus NX should expose an HTTP endpoint on the LAN.
Implementation status: the bearer-authenticated V1 status API is implemented and host-tested on loopback, including TTL/stale behavior. It has not yet been ported to an ESP32 HTTP server. See Configuration V1.
For example:
POST /api/v1/status/build
Body:
{
"status": "ok",
"message": "main branch passed",
"ttl": 1800
}Or a generic endpoint:
POST /api/v1/status
{
"id": "build",
"status": "ok",
"value": 42,
"message": "main branch passed",
"ttl": 1800
}Authentication should be available.
At minimum:
API token
The ESP32 should generally not be exposed directly to the public Internet.
MQTT is a strong candidate for remote push monitoring.
Implementation status: MQTT remains planned and is not a current input type.
The ESP32 makes an outbound connection to a broker and subscribes to configured topics.
Example:
pixelstatus/status/build
pixelstatus/status/backup
pixelstatus/status/router
Message:
{
"status": "warn",
"message": "backup overdue",
"ttl": 3600
}This works across NAT without exposing the ESP32 as a public server.
It also integrates naturally with:
- Home Assistant;
- monitoring servers;
- CI systems;
- custom daemons;
- embedded devices.
Freshness must be a first-class concept.
A pushed state should not remain healthy forever merely because the reporting system disappeared.
For example:
{
"status": "ok",
"ttl": 900
}means:
current time - last update < 900 seconds
→ OK
current time - last update >= 900 seconds
→ STALE
Pull monitors similarly become stale if the scheduler or runner cannot obtain sufficiently recent data.
STALE should be distinct from an explicit negative result.
PixelStatus NX must not hard-code only a fixed list of statuses.
There should be a useful default set, such as:
ok
info
active
warn
fail
communication_failure
stale
unknown
disabled
but users should be able to define additional statuses.
Examples:
deploying
charging
offline
maintenance
backing_up
degraded
busy
idle
critical
A monitor produces a status identifier.
The status definition determines its appearance.
This is an important separation:
Monitoring:
"backup" is OVERDUE
Appearance definition:
OVERDUE means slowly pulsing amber
The monitoring subsystem must not directly encode:
OVERDUE = RGB(255,128,0)
Each status may define a time-varying LED appearance.
The grammar should be intentionally small, declarative, and deterministic.
It should be capable of expressing:
- solid colors;
- blinking;
- flashing;
- toggling between colors;
- fades;
- pulses;
- repeating sequences;
- potentially finite introductory sequences followed by steady state.
For example:
statuses:
ok:
appearance:
solid: "#00FF00"
fail:
appearance:
blink:
color: "#FF0000"
on: 500ms
off: 500ms
stale:
appearance:
pulse:
color: "#FF8000"
period: 2s
communication_failure:
appearance:
sequence:
repeat: true
steps:
- color: "#FF00FF"
duration: 200ms
- color: "#000000"
duration: 200ms
- color: "#FF00FF"
duration: 200ms
- color: "#000000"
duration: 1400msThe grammar should ultimately reduce to a simple function:
appearance(status, elapsed_time) → color
or perhaps:
appearance(status, elapsed_time, pixel_context) → visual value
if more advanced rendering is later required.
A particularly clean internal representation would treat an appearance as a timeline of color keyframes.
Conceptually:
time color
0 ms red
200 ms red
201 ms black
400 ms black
401 ms red
...
Interpolation determines whether a transition is:
step
linear
This single mechanism can represent most desired effects.
For example, solid green:
timeline:
repeat: true
keyframes:
- at: 0ms
color: "#00FF00"Blink:
timeline:
repeat: true
duration: 1000ms
keyframes:
- at: 0ms
color: "#FF0000"
transition: step
- at: 500ms
color: "#000000"
transition: stepFade:
timeline:
repeat: true
duration: 2000ms
keyframes:
- at: 0ms
color: "#000000"
- at: 1000ms
color: "#FF8000"
transition: linear
- at: 2000ms
color: "#000000"
transition: linearHigher-level syntax such as:
solid
blink
pulse
flash
toggle
could simply compile into this timeline representation.
This keeps the rendering engine simple while providing a convenient configuration language.
appearance:
solid: "#00FF00"appearance:
blink:
color: "#FF0000"
period: 1s
duty: 50%appearance:
toggle:
colors:
- "#FF0000"
- "#0000FF"
period: 500msappearance:
pulse:
color: "#FF8000"
period: 2s
minimum: 10%
maximum: 100%appearance:
sequence:
repeat: true
steps:
- color: "#FFFFFF"
duration: 100ms
- color: "#000000"
duration: 100ms
- color: "#FFFFFF"
duration: 100ms
- color: "#000000"
duration: 1700msappearance:
cycle:
transition: linear
steps:
- color: "#FF0000"
duration: 1s
- color: "#FFFF00"
duration: 1sEach indicator should have a defined animation epoch.
Normally, animation timing should begin when the indicator enters a status.
For example:
12:00:00 status changes OK → FAIL
12:00:00 fail animation begins at t=0
This allows an appearance to communicate transitions intentionally.
There may eventually be multiple timing modes:
on_status_entry
globally_synchronized
wall_clock
Globally synchronized blinking can be useful when many indicators share the same status.
Example:
all failed indicators blink together
rather than each blinking at unrelated phases.
The renderer consumes:
- current monitor states;
- status appearance definitions;
- indicator layout;
- current time.
It produces the logical display frame.
Conceptually:
StateStore
+
StatusDefinitions
+
Layout
+
Clock
↓
Renderer
↓
Frame
The renderer should not perform network operations.
A 16×16 display provides enough pixels for more than simple one-pixel indicators.
Indicators should eventually support regions.
Example:
indicators:
internet:
source: internet
x: 0
y: 0
width: 4
height: 4
backup:
source: backup
x: 4
y: 0
width: 4
height: 4A basic indicator may simply fill its assigned area with its status appearance.
Later indicator/render types could include:
solid region
icon
bar
meter
sparkline
text glyph
border
background
These should be considered rendering features rather than monitor features.
Layout stack containers now provide deterministic back-to-front layers:
background
↓
status regions
↓
icons
↓
alerts
↓
global overlays
Every child of a stack receives the same AABB. Its first child paints the background
and each later child paints over the earlier result. The aggregate_status widget
can supply an across-the-room card background selected from the worst effective
status among multiple sources, while later grids, clocks, icons, or alerts preserve
their individual colors. This is integer framebuffer composition without alpha
blending, so behavior is identical on the desktop and ESP32 targets.
Every pull runner should normalize its output.
Conceptually:
struct MonitorResult {
bool transport_success;
Value value;
uint32_t latency_ms;
ErrorCode error;
std::string detail;
Timestamp timestamp;
};Different runners may populate different fields.
The evaluator maps a MonitorResult into a named status.
Generic comparison operations should include:
exists
not_exists
equals
not_equals
contains
not_contains
greater_than
greater_or_equal
less_than
less_or_equal
between
Example:
expect:
value:
less_than: 90A monitor may need multiple thresholds.
Example:
evaluate:
- when:
value:
greater_or_equal: 95
status: critical
- when:
value:
greater_or_equal: 85
status: warn
- otherwise:
status: okThis enables custom statuses without hard-coding application-specific logic.
At minimum, distinguish:
The remote system answered and reported something unhealthy.
Example:
HTTP 200
JSON health = degraded
Result:
warn
PixelStatus NX could not determine the state.
Examples:
DNS failure
TCP timeout
TLS failure
HTTP timeout
SNMP timeout
Result:
communication_failure
The last known state is too old.
Result:
stale
Those three cases should be independently stylable.
The central State Store should contain current state independent of source type.
Conceptually:
struct MonitorState {
std::string id;
std::string status;
Value value;
std::string message;
Timestamp observed_at;
Timestamp updated_at;
optional<Duration> ttl;
};Push and pull sources both ultimately update this same structure.
Implementation status: the host loads one validated JSON configuration file. The NVS/LittleFS split described here is planned for ESP32 and has not been implemented.
Configuration should be runtime editable.
Suggested storage split:
NVS
├── Wi-Fi credentials
├── hostname
├── device identity
├── secrets
└── system settings
LittleFS
├── monitors.json
├── statuses.json
├── layout.json
└── display.json
JSON is preferable to YAML on the ESP32 itself.
Desktop tools or import utilities can translate YAML into JSON if desirable.
PixelStatus NX should eventually expose a local management interface, for example:
Implementation status: the responsive read-only browser display and framebuffer API are complete. The configuration/dashboard management interface described in this section is not implemented.
http://pixelstatus-nx.local/
Possible sections:
Dashboard
Monitors
Push Sources
Statuses
Appearance Editor
Layout
Display
Network
Logs
System
Firmware
An appearance editor could preview:
solid
blink
pulse
fade
sequence
directly in the browser.
A useful API should expose at least:
GET /api/v1/status
GET /api/v1/status/{id}
POST /api/v1/status
POST /api/v1/status/{id}
GET /api/v1/monitors
GET /api/v1/display
Configuration mutation endpoints can be added later.
The API and internal state model should use the same status names.
Implementation status: the four status GET/POST routes and read-only display route
exist on the Windows and Linux hosts. GET /api/v1/monitors and configuration mutation routes
do not.
The current display is controlled as a BLE peripheral through GATT writes. Protocol knowledge has different evidence levels and should not be treated uniformly.
- the upstream
draw_pixels.pypath controls the display; - graffiti-mode initialization uses
BC 00 01 01 55followed byBC 00 0D 0D 55; - the working single-pixel packet rule uses the trailing-byte behavior documented in the Python handoff rather than the conflicting upstream protocol note;
- rewriting all 256 pixels through individual writes takes roughly five seconds on the current Windows host.
- the Windows bridge discovered and connected to the expected FFD1 characteristic;
- the characteristic advertised write-without-response with a reported 511-byte maximum, and accepted one complete live PixelStatus frame through 256 pixel writes.
- observed corner and block patterns confirmed upper-left row-major RGB mapping, correct top-to-bottom block order, and browser/physical-frame parity.
- advertised name:
MI Matrix Display; - service UUID:
0000ffd0-0000-1000-8000-00805f9b34fb; - write characteristic UUID:
0000ffd1-0000-1000-8000-00805f9b34fb; - block-mode initialization:
BC 0F F1 08 08 55; - eight 100-byte block packets are expected to transfer a complete frame.
These upstream-derived details are suitable starting points but should be recorded as device-validated only after the corresponding calibration and block tests pass.
- forced disconnect during a block transfer and newest-frame restoration;
- longer unattended write/reconnect/task-restart soak behavior;
- write-without-response stability; the observed characteristic did not advertise write-with-response;
- stable block delay and maximum practical rate beyond the observed roughly 4 Hz visible panel update rate;
- whether graffiti and block modes can be switched or interleaved safely;
- brightness commands, if supported;
- an evidence-based automatic sparse/block crossover, if mode switching proves safe.
The firmware driver should contain three focused parts:
MiBleOutputDriver
├── frame diffing, coalescing, mode choice, and driver state
├── MI packet encoder with pure byte-building functions
└── ESP-IDF NimBLE GATT transport
Sparse/full-frame selection belongs inside this driver. Automatic mode selection must not be enabled until mode-switching behavior and the crossover threshold have been measured. The renderer remains unaware of all MI-specific details.
Directly connected LEDs should be supported as an alternative output backend.
Potential initial targets:
WS2812B
SK6812
Potential later target:
APA102
Configuration may include:
display:
driver: ws2812
width: 16
height: 16
pin: 18
layout:
serpentine: true
origin: top_left
color_order: GRBMapping (x,y) to physical LED index belongs inside the output driver or its geometry adapter.
Output failure should itself be observable internally.
Examples:
BLE disconnected
BLE reconnecting
direct LED driver initialization failed
The system should retain monitoring state even while the display is unavailable.
When the display reconnects, the current frame should be restored immediately.
A reasonable FreeRTOS decomposition:
Wi-Fi / TCP-IP subsystem
Scheduler task
↓
Monitor work queue
↓
2–4 monitor workers
↓
State Store
HTTP server task ───────────┐
│
MQTT task ──────────────────┤
v
State Store
│
v
Renderer
│
v
Output Driver
The renderer may run continuously at a modest frame rate such as:
20–60 FPS
when animations are active.
Network monitors operate on much slower schedules.
Network operations should have explicit limits:
timeout
maximum body size
maximum JSON size
maximum simultaneous connections
maximum number of monitors
maximum response buffer
A monitoring endpoint returning 20 MB of JSON should not exhaust the ESP32.
HTTP/JSON monitors only need enough response data to perform configured checks.
At minimum:
- TLS certificate validation should be enabled by default.
- Secrets should not be exposed through normal API responses.
- Push API should support authentication.
- Management interface should support authentication.
- Internet-facing inbound access should not be assumed.
- MQTT should support TLS and authentication.
- Configuration should distinguish secret values from ordinary values.
The device should normally initiate outbound Internet connections rather than require public inbound access.
OTA should be considered a core appliance feature.
Firmware updates should not require physical USB access.
Configuration must survive firmware updates.
A dual-partition OTA strategy with rollback support is preferable.
Desired startup sequence:
boot
↓
load persistent configuration
↓
initialize state store
↓
initialize display
↓
show startup state
↓
connect Wi-Fi
↓
synchronize time
↓
start push interfaces
↓
start scheduler
↓
begin monitoring
Previously persisted states could optionally be displayed immediately but should clearly become stale until refreshed.
This section describes the intended V1 release envelope, not the first implementation increment. V1 should be built through independently testable vertical slices.
- ESP32-S3
- ESP-IDF
- FreeRTOS
- existing Bluetooth 16×16 RGB display;
- simulator output;
- direct WS2812/SK6812 support.
- Ping;
- DNS;
- TCP connect;
- TCP exchange;
- HTTP/HTTPS;
- HTTP JSON;
- TLS certificate;
- SNMP v1/v2c GET.
- HTTP status API;
- MQTT subscriptions.
- intervals;
- simple cron-like scheduling;
- bounded worker pool.
- built-in defaults;
- user-defined custom statuses.
- solid;
- blink;
- flash;
- toggle;
- fade;
- pulse;
- repeating sequence.
- logical framebuffer;
- layout independent of physical output;
- animated rendering.
- NVS;
- LittleFS.
- basic local HTTP API;
- eventually web UI.
- OTA firmware updates.
The first implementation milestone should establish one complete path:
versioned JSON configuration
↓
in-memory state update
↓
state store
↓
solid and blink appearances
↓
rectangular indicator layout
↓
logical framebuffer
↓
simulator output
This milestone should also define the shared value type, timestamp and TTL semantics, configuration validation behavior, output-driver ownership/backpressure contract, and byte-level MI protocol test vectors. It does not require network monitors or physical display access.
Status: complete on the Windows, Linux-headless, and portable-core paths and substantially exceeded. The repository also has real pull monitors, browser output, composite card decks, vendor adapters, and a physically validated Windows MI bridge. Linux MI support uses the same Bleak bridge through BlueZ and awaits physical Linux acceptance. ESP32 work remains separate; see the current status ledger.
After host rendering and layout are verified, validate the MI display from Windows
with the Python/bleak reference transport. The later ESP32 stage replaces that
diagnostic transport with native NimBLE while keeping the same framebuffer and
renderer contracts.
Do not initially implement:
- arbitrary shell commands;
- Python execution in production firmware;
- Lua;
- JavaScript execution;
- SSH command execution;
- full SNMP MIB browser;
- SNMPv3;
- Prometheus server;
- historical time-series database;
- complex alert escalation;
- email sending;
- SMS sending;
- general monitoring-agent functionality;
- sophisticated dashboard graphics.
These belong either to external systems or later extensions.
A possible repository organization:
pixelstatus-nx/
├── firmware/
│ ├── main/
│ │
│ └── components/
│ ├── core/
│ │ ├── state_store/
│ │ ├── scheduler/
│ │ ├── evaluator/
│ │ └── config/
│ │
│ ├── monitors/
│ │ ├── ping/
│ │ ├── dns/
│ │ ├── tcp/
│ │ ├── http/
│ │ ├── http_json/
│ │ ├── tls/
│ │ └── snmp/
│ │
│ ├── push/
│ │ ├── http_api/
│ │ └── mqtt/
│ │
│ ├── rendering/
│ │ ├── framebuffer/
│ │ ├── appearance/
│ │ ├── layout/
│ │ └── renderer/
│ │
│ └── output/
│ ├── ble_display/
│ ├── ws2812/
│ └── simulator/
│
├── simulator/
├── tools/
├── docs/
├── examples/
└── README.md
Exact structure can evolve, but subsystem boundaries should remain clear.
A likely conceptual interface set:
class MonitorRunner {
public:
virtual MonitorResult run(const MonitorConfig&) = 0;
};class Evaluator {
public:
virtual Status evaluate(
const MonitorResult& result,
const EvaluationConfig& config
) = 0;
};class Appearance {
public:
virtual RGB sample(Duration elapsed) const = 0;
};class Renderer {
public:
virtual void render(
const StateStore& states,
Timestamp now,
Frame& output
) = 0;
};class OutputDriver {
public:
virtual bool begin() = 0;
virtual FrameSubmitResult submitFrame(const Frame&) = 0;
virtual DriverState state() const = 0;
};The most important constraint should remain:
Monitoring produces state.
State has meaning.
Appearance represents state.
Layout places appearance.
Rendering generates pixels.
Output drivers transport pixels.
No layer should collapse those concepts unnecessarily.
In particular:
HTTP monitor
should never contain logic equivalent to:
if (http_status != 200)
pixels[37] = RED;Instead:
HTTP result
↓
FAIL
↓
status definition
↓
flashing red
↓
indicator layout
↓
pixels
↓
output driver
This separation is what allows PixelStatus NX to evolve beyond both its current Bluetooth 16×16 display and the much more constrained architecture of the original PixelStatus.
This section is the original approval sequence. Gates 1–4 were adapted to honor the later host-first decision: no ESP-IDF toolchain is required until the Windows behavior is mature. Current stage status and remaining exit gates are maintained in the Development Roadmap, with the exhaustive backlog in Implementation Status and Loose Ends.
- create the ESP-IDF project and component directories;
- select and pin the initial ESP-IDF toolchain version;
- define the frame, value, monitor-state, status, time, and output-driver types;
- define versioned minimal JSON schemas and validation rules;
- add host-runnable tests for pure core logic and MI packet vectors.
Deliverable: a building skeleton with tests, but no networking or hardware access.
Status: complete for the portable C++20/Windows skeleton, schema, core contracts, tests, and MI packet vectors. Creating and pinning the ESP-IDF project was deliberately moved to the production-port stage.
- implement state transitions and TTL/stale behavior;
- compile solid and blink appearances to the timeline representation;
- render rectangular indicators into a logical framebuffer;
- display or export frames through a simulator driver.
Deliverable: configuration-to-pixels behavior that can be tested without hardware.
Status: complete on the native Win32 target. The timeline compiler now covers the full initial appearance grammar, not only solid and blink.
- run the Python calibration, block, MTU, response-mode, delay, and reconnect tests;
- record results in the MI handoff;
- connect the Windows framebuffer output to the display through
bleak; - add frame coalescing and conservative reconnect behavior;
- enable sparse/full-frame selection only if the measurements justify it.
Deliverable: the same simulator-rendered frames displayed on the physical MI matrix from Windows. A native C++/WinRT transport is optional; the production ESP32 NimBLE transport remains a later stage.
Status: the conservative pixel-mode deliverable, orientation/color calibration, and repeated visually correct block-mode operation are physically validated. The bridge now coalesces frames, reconnects with bounded backoff, performs full restoration, and emits an atomic write heartbeat used by the Scheduled Task status check. A forced mid-transfer disconnect, longer soak, and same-connection mode switching remain before automatic mode selection or closure of the reliability gate.
- add the authenticated local HTTP status endpoint;
- apply TTL and validation limits;
- confirm that pushed states update both simulator and MI outputs identically.
Deliverable: an end-to-end ambient status appliance with one push interface.
Status: the hardware-independent portion is complete and host-tested. The portable authenticated handler updates the simulator through localhost HTTP, and the shared resulting framebuffer has been mirrored to the physical MI output.
Add scheduling and pull monitors incrementally, followed by MQTT, direct LEDs, persistence hardening, management UI, and OTA. Each monitor and output backend should enter through the established state, rendering, and driver contracts.
Status: the portable interval scheduler, runner boundary, normalized result, generic evaluator, strict monitor JSON grammar, and desktop HTTP/JSON (including request methods, headers, and bodies), TCP-connect, ICMP-ping, TCP-exchange, and DNS runners are host-tested. The Win32 host also has a bounded multi-worker executor with per-monitor in-flight exclusion. WinHTTP/Schannel HTTPS and named desktop secrets are host-tested. Per-monitor custom TLS trust, ESP32 secret storage, additional network runners, jitter/cron support, and in-flight cancellation remain incremental follow-on work.