DeterministicESPAsyncWebServer v6.28.0
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
dashboard.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file dashboard.cpp
6 * @brief Dashboard widget table + JSON serializers (DETWS_ENABLE_DASHBOARD).
7 *
8 * The host-testable core: it owns the widget table and value array and turns them
9 * into the layout / values JSON the page consumes. No server or web_assets
10 * dependency lives here, so it compiles and unit-tests standalone; the route /
11 * SSE wiring is in dashboard_routes.cpp.
12 */
13
15
16#if DETWS_ENABLE_DASHBOARD
17
19#include <stdarg.h>
20#include <stdio.h>
21#include <string.h>
22
23// All dashboard state, owned by one instance (internal linkage): the widget table, the
24// per-widget value array, and the inbound-control callback, grouped so it is one named owner,
25// unreachable from any other translation unit.
26struct DashboardCtx
27{
28 const DetwsWidget *widgets = nullptr;
29 uint8_t count = 0;
30 float values[DETWS_DASHBOARD_MAX_WIDGETS] = {};
31 DetwsControlCb control_cb = nullptr;
32};
33static DashboardCtx s_dash;
34
35static const char *widget_type_name(DetwsWidgetType t)
36{
37 switch (t)
38 {
39 case DetwsWidgetType::DETWS_WIDGET_GAUGE:
40 return "gauge";
41 case DetwsWidgetType::DETWS_WIDGET_BAR:
42 return "bar";
43 case DetwsWidgetType::DETWS_WIDGET_SPARKLINE:
44 return "sparkline";
45 case DetwsWidgetType::DETWS_WIDGET_CHART:
46 return "chart";
47 case DetwsWidgetType::DETWS_WIDGET_BUTTON:
48 return "button";
49 case DetwsWidgetType::DETWS_WIDGET_TOGGLE:
50 return "toggle";
51 case DetwsWidgetType::DETWS_WIDGET_SLIDER:
52 return "slider";
53 default:
54 return "value";
55 }
56}
57
58// Append a formatted fragment at *pos; return -1 (leaving the buffer truncated) if
59// it would not fit, so callers fail closed rather than overflow.
60static int json_append(char *out, size_t cap, size_t *pos, const char *fmt, ...)
61{
62 if (*pos >= cap)
63 return -1;
64 va_list ap;
65 va_start(ap, fmt);
66 int w = vsnprintf(out + *pos, cap - *pos, fmt, ap);
67 va_end(ap);
68 if (w < 0 || (size_t)w >= cap - *pos)
69 return -1;
70 *pos += (size_t)w;
71 return 0;
72}
73
74void detws_dashboard_configure(const DetwsWidget *widgets, uint8_t count)
75{
76 s_dash.widgets = widgets;
77 s_dash.count = count > DETWS_DASHBOARD_MAX_WIDGETS ? DETWS_DASHBOARD_MAX_WIDGETS : count;
78 for (uint8_t i = 0; i < DETWS_DASHBOARD_MAX_WIDGETS; i++)
79 s_dash.values[i] = 0.0f;
80}
81
82bool detws_dashboard_set(const char *key, float value)
83{
84 if (!key || !s_dash.widgets)
85 return false;
86 for (uint8_t i = 0; i < s_dash.count; i++)
87 {
88 if (s_dash.widgets[i].key && strcmp(s_dash.widgets[i].key, key) == 0)
89 {
90 s_dash.values[i] = value;
91 return true;
92 }
93 }
94 return false;
95}
96
97int detws_dashboard_layout_json(char *out, size_t cap)
98{
99 if (!out || cap == 0)
100 return 0;
101 out[0] = '\0';
102 if (!s_dash.widgets)
103 return 0;
104 size_t pos = 0;
105 if (json_append(out, cap, &pos, "[") != 0)
106 return 0;
107 for (uint8_t i = 0; i < s_dash.count; i++)
108 {
109 const DetwsWidget *w = &s_dash.widgets[i];
110 if (json_append(out, cap, &pos,
111 "%s{\"type\":\"%s\",\"label\":\"%s\",\"key\":\"%s\",\"min\":%g,\"max\":%g,\"unit\":\"%s\"}",
112 i ? "," : "", widget_type_name(w->type), w->label ? w->label : "", w->key ? w->key : "",
113 (double)w->min, (double)w->max, w->unit ? w->unit : "") != 0)
114 return 0;
115 }
116 if (json_append(out, cap, &pos, "]") != 0)
117 return 0;
118 return (int)pos;
119}
120
121int detws_dashboard_values_json(char *out, size_t cap)
122{
123 if (!out || cap == 0)
124 return 0;
125 out[0] = '\0';
126 if (!s_dash.widgets)
127 return 0;
128 size_t pos = 0;
129 if (json_append(out, cap, &pos, "{") != 0)
130 return 0;
131 for (uint8_t i = 0; i < s_dash.count; i++)
132 {
133 if (json_append(out, cap, &pos, "%s\"%s\":%g", i ? "," : "", s_dash.widgets[i].key ? s_dash.widgets[i].key : "",
134 (double)s_dash.values[i]) != 0)
135 return 0;
136 }
137 if (json_append(out, cap, &pos, "}") != 0)
138 return 0;
139 return (int)pos;
140}
141
142// ---------------------------------------------------------------------------
143// Controls (inbound WebSocket messages)
144// ---------------------------------------------------------------------------
145
146void detws_dashboard_on_control(DetwsControlCb cb)
147{
148 s_dash.control_cb = cb;
149}
150
151// Locate the value of "key" in a {"k":...,"v":...} object: a pointer just past
152// the ':' (whitespace skipped), or nullptr. The quoted pattern ("k" / "v") only
153// matches the message's own keys, not a widget key that happens to contain k/v.
154static const char *control_value_ptr(const char *s, const char *key)
155{
156 char pat[8];
157 snprintf(pat, sizeof(pat), "\"%s\"", key);
158 const char *p = strstr(s, pat);
159 if (!p)
160 return nullptr;
161 p += strnlen(pat, sizeof(pat));
162 while (*p == ' ' || *p == '\t')
163 p++;
164 if (*p != ':')
165 return nullptr;
166 p++;
167 while (*p == ' ' || *p == '\t')
168 p++;
169 return p;
170}
171
172bool detws_dashboard_parse_control(const char *msg, char *key_out, size_t key_cap, float *value_out)
173{
174 if (!msg || !key_out || key_cap == 0 || !value_out)
175 return false;
176 key_out[0] = '\0';
177 const char *kp = control_value_ptr(msg, "k");
178 const char *vp = control_value_ptr(msg, "v");
179 if (!kp || !vp || *kp != '"')
180 return false;
181 kp++;
182 size_t i = 0;
183 while (*kp && *kp != '"' && i + 1 < key_cap)
184 key_out[i++] = *kp++;
185 if (*kp != '"')
186 {
187 key_out[0] = '\0';
188 return false; // unterminated or key too long
189 }
190 key_out[i] = '\0';
191 const char *end = nullptr;
192 float v = det_strtof(vp, &end);
193 if (end == vp)
194 return false; // no numeric value
195 *value_out = v;
196 return true;
197}
198
199bool detws_dashboard_dispatch_control(const char *msg)
200{
201 char key[32];
202 float value;
203 if (!detws_dashboard_parse_control(msg, key, sizeof(key), &value))
204 return false;
205 if (s_dash.control_cb)
206 s_dash.control_cb(key, value);
207 return s_dash.control_cb != nullptr;
208}
209
210#endif // DETWS_ENABLE_DASHBOARD
#define DETWS_DASHBOARD_MAX_WIDGETS
Maximum widgets in the dashboard table (BSS value array).
Real-time SVG telemetry dashboard (DETWS_ENABLE_DASHBOARD).
Tiny no-stdlib base-10 number parsers (strtol/strtoul/strtof replacements).
float det_strtof(const char *s, const char **end)
Parse a float (integer[.frac][e[+/-]exp]); sets end (or to s if none).
Definition numparse.h:124