ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
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 (PC_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
10 * dependency lives here, so it compiles and unit-tests standalone; the route /
11 * SSE wiring is in dashboard_routes.cpp.
12 */
13
15#include "shared_primitives/strbuf.h" // pc_sb frame builder
16
17#if PC_ENABLE_DASHBOARD
18
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 pc_widget *widgets = nullptr;
29 uint8_t count = 0;
30 float values[PC_DASHBOARD_MAX_WIDGETS] = {};
31 pc_control_cb control_cb = nullptr;
32};
33static DashboardCtx s_dash;
34
35static const char *widget_type_name(pc_widget_type t)
36{
37 switch (t)
38 {
39 case pc_widget_type::PC_WIDGET_GAUGE:
40 return "gauge";
41 case pc_widget_type::PC_WIDGET_BAR:
42 return "bar";
43 case pc_widget_type::PC_WIDGET_SPARKLINE:
44 return "sparkline";
45 case pc_widget_type::PC_WIDGET_CHART:
46 return "chart";
47 case pc_widget_type::PC_WIDGET_BUTTON:
48 return "button";
49 case pc_widget_type::PC_WIDGET_TOGGLE:
50 return "toggle";
51 case pc_widget_type::PC_WIDGET_SLIDER:
52 return "slider";
53 default:
54 return "value";
55 }
56}
57
58void pc_dashboard_configure(const pc_widget *widgets, uint8_t count)
59{
60 s_dash.widgets = widgets;
61 s_dash.count = count > PC_DASHBOARD_MAX_WIDGETS ? PC_DASHBOARD_MAX_WIDGETS : count;
62 for (uint8_t i = 0; i < PC_DASHBOARD_MAX_WIDGETS; i++)
63 {
64 s_dash.values[i] = 0.0f;
65 }
66}
67
68bool pc_dashboard_set(const char *key, float value)
69{
70 if (!key || !s_dash.widgets)
71 {
72 return false;
73 }
74 for (uint8_t i = 0; i < s_dash.count; i++)
75 {
76 if (s_dash.widgets[i].key && strcmp(s_dash.widgets[i].key, key) == 0)
77 {
78 s_dash.values[i] = value;
79 return true;
80 }
81 }
82 return false;
83}
84
85// The layout is an array of widget objects; the values document is one flat object of key/number
86// pairs. Both open, repeat one frame per widget, and close, with the separating comma carried as
87// the repeated frame's first field.
88static const pc_field DASH_ARRAY_OPEN[] = {{PC_FK_LIT, 0, 1, "["}, PC_END};
89static const pc_field DASH_ARRAY_CLOSE[] = {{PC_FK_LIT, 0, 1, "]"}, PC_END};
90static const pc_field DASH_WIDGET[] = {
91 PC_STR, // "," from the second widget on
92 {PC_FK_LIT, 0, 8, "{\"type\":"}, //
93 PC_JSON, // type name
94 {PC_FK_LIT, 0, 9, ",\"label\":"}, //
95 PC_JSON, //
96 {PC_FK_LIT, 0, 7, ",\"key\":"}, //
97 PC_JSON, //
98 {PC_FK_LIT, 0, 7, ",\"min\":"}, //
99 {PC_FK_G, 0, 0, nullptr}, // width 0 == 6 significant digits, the %g default
100 {PC_FK_LIT, 0, 7, ",\"max\":"}, //
101 {PC_FK_G, 0, 0, nullptr}, //
102 {PC_FK_LIT, 0, 8, ",\"unit\":"}, //
103 PC_JSON, //
104 {PC_FK_LIT, 0, 1, "}"}, //
105 PC_END,
106};
107
108static const pc_field DASH_OBJECT_OPEN[] = {{PC_FK_LIT, 0, 1, "{"}, PC_END};
109static const pc_field DASH_OBJECT_CLOSE[] = {{PC_FK_LIT, 0, 1, "}"}, PC_END};
110static const pc_field DASH_VALUE[] = {
111 PC_STR, // "," from the second pair on
112 PC_JSON, // key
113 {PC_FK_LIT, 0, 1, ":"}, //
114 {PC_FK_G, 0, 0, nullptr}, // the reading
115 PC_END,
116};
117
118int32_t pc_dashboard_layout_json(char *out, uint32_t cap)
119{
120 if (!out || cap == 0)
121 {
122 return 0;
123 }
124 out[0] = '\0';
125 if (!s_dash.widgets)
126 {
127 return 0;
128 }
129 if (pc_frame_append(out, cap, DASH_ARRAY_OPEN) == 0)
130 {
131 return 0;
132 }
133 for (uint8_t i = 0; i < s_dash.count; i++)
134 {
135 const pc_widget *w = &s_dash.widgets[i];
136 if (pc_frame_append(out, cap, DASH_WIDGET, i ? "," : "", widget_type_name(w->type), w->label ? w->label : "",
137 w->key ? w->key : "", (double)w->min, (double)w->max, w->unit ? w->unit : "") == 0)
138 {
139 return 0;
140 }
141 }
142 return (int32_t)pc_frame_append(out, cap, DASH_ARRAY_CLOSE);
143}
144
145int32_t pc_dashboard_values_json(char *out, uint32_t cap)
146{
147 if (!out || cap == 0)
148 {
149 return 0;
150 }
151 out[0] = '\0';
152 if (!s_dash.widgets)
153 {
154 return 0;
155 }
156 if (pc_frame_append(out, cap, DASH_OBJECT_OPEN) == 0)
157 {
158 return 0;
159 }
160 for (uint8_t i = 0; i < s_dash.count; i++)
161 {
162 if (pc_frame_append(out, cap, DASH_VALUE, i ? "," : "", s_dash.widgets[i].key ? s_dash.widgets[i].key : "",
163 (double)s_dash.values[i]) == 0)
164 {
165 return 0;
166 }
167 }
168 return (int32_t)pc_frame_append(out, cap, DASH_OBJECT_CLOSE);
169}
170
171// ---------------------------------------------------------------------------
172// Controls (inbound WebSocket messages)
173// ---------------------------------------------------------------------------
174
175void pc_dashboard_on_control(pc_control_cb cb)
176{
177 s_dash.control_cb = cb;
178}
179
180// Locate the value of "key" in a {"k":...,"v":...} object: a pointer just past
181// the ':' (whitespace skipped), or nullptr. The quoted pattern ("k" / "v") only
182// matches the message's own keys, not a widget key that happens to contain k/v.
183static const char *control_value_ptr(const char *s, const char *key)
184{
185 char pat[8];
186 pc_sb sb_pat = {pat, sizeof(pat), 0, true};
187 pc_sb_put(&sb_pat, "\"");
188 pc_sb_put(&sb_pat, key);
189 pc_sb_put(&sb_pat, "\"");
190 if (pc_sb_finish(&sb_pat) == 0)
191 {
192 pat[0] = '\0';
193 }
194 const char *p = strstr(s, pat);
195 if (!p)
196 {
197 return nullptr;
198 }
199 p += strnlen(pat, sizeof(pat));
200 while (*p == ' ' || *p == '\t')
201 {
202 p++;
203 }
204 if (*p != ':')
205 {
206 return nullptr;
207 }
208 p++;
209 while (*p == ' ' || *p == '\t')
210 {
211 p++;
212 }
213 return p;
214}
215
216bool pc_dashboard_parse_control(const char *msg, char *key_out, size_t key_cap, float *value_out)
217{
218 if (!msg || !key_out || key_cap == 0 || !value_out)
219 {
220 return false;
221 }
222 key_out[0] = '\0';
223 const char *kp = control_value_ptr(msg, "k");
224 const char *vp = control_value_ptr(msg, "v");
225 if (!kp || !vp || *kp != '"')
226 {
227 return false;
228 }
229 kp++;
230 size_t i = 0;
231 while (*kp && *kp != '"' && i + 1 < key_cap)
232 {
233 key_out[i++] = *kp++;
234 }
235 if (*kp != '"')
236 {
237 key_out[0] = '\0';
238 return false; // unterminated or key too long
239 }
240 key_out[i] = '\0';
241 const char *end = nullptr;
242 float v = pc_strtof(vp, &end);
243 if (end == vp)
244 {
245 return false; // no numeric value
246 }
247 *value_out = v;
248 return true;
249}
250
251bool pc_dashboard_dispatch_control(const char *msg)
252{
253 char key[32];
254 float value;
255 if (!pc_dashboard_parse_control(msg, key, sizeof(key), &value))
256 {
257 return false;
258 }
259 if (s_dash.control_cb)
260 {
261 s_dash.control_cb(key, value);
262 }
263 return s_dash.control_cb != nullptr;
264}
265
266#endif // PC_ENABLE_DASHBOARD
Real-time SVG telemetry dashboard (PC_ENABLE_DASHBOARD).
size_t pc_frame_append(char *out, size_t cap, const pc_field *spec,...)
Append spec to the NUL-terminated contents already in out.
Definition frame.cpp:109
Declarative frame builder: a frame is a static table of typed fields, built by one engine.
#define PC_END
Definition frame.h:110
#define PC_JSON
Definition frame.h:108
@ PC_FK_G
double, printf %.<width>g (width 0 means 6)
Definition frame.h:64
@ PC_FK_LIT
literal text from lit; takes no argument
Definition frame.h:56
#define PC_STR
Definition frame.h:103
Tiny no-stdlib base-10 number parsers (strtol/strtoul/strtof replacements).
float pc_strtof(const char *s, const char **end)
Parse a float (integer[.frac][e[+/-]exp]); sets end (or to s if none).
Definition numparse.h:154
#define PC_DASHBOARD_MAX_WIDGETS
Maximum widgets in the dashboard table (BSS value array).
Bounded no-heap string builder that fails closed on overflow (one shared copy).
size_t pc_sb_finish(pc_sb *b)
NUL-terminate and return the built length, or 0 if the build overflowed.
Definition strbuf.h:648
void pc_sb_put(pc_sb *b, const char *s)
Append NUL-terminated s; leaves the buffer untouched and clears ok if it would not fit.
Definition strbuf.h:60
One field of a frame. Frames are static const pc_field[], so they live in rodata.
Definition frame.h:80
Bump-append target; ok latches false once an append would overflow cap.
Definition strbuf.h:30