Skip to content

Commit c0eb374

Browse files
skialpineclaude
andcommitted
feat: WiFi scale support — WebSocket control + events
Extend the /snapshot WebSocket beyond a one-way weight stream into a full control + event protocol so web/app clients reach feature parity with BLE: * Event broadcasts: button presses (sendWebsocketButton) and power-off reasons (sendWebsocketPowerOff) wired into the same call sites as their BLE/USB equivalents. * Status frames: sendWebsocketStatus / sendWebsocketStatusAll publish battery %, voltage, charging, timer, display, soft-sleep, LED, and rate state. Periodic broadcast every WEBSOCKET_STATUS_NOTIFY_INTERVAL_MS. * Commands accepted as text or JSON: status, events on/off, tare, timer start/stop/zero, led <r,g,b>|off, display on/off, low_power on/off, sleep on/off, power off. * Selectable weight rate: 2 Hz / 5 Hz / 10 Hz, via "rate <hz>", "interval <ms>", or JSON {rate_hz|hz|rate|interval_ms|...}. Falls back to default on client disconnect. * README: document the protocol (events, status, commands, rates). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 42da6ed commit c0eb374

5 files changed

Lines changed: 718 additions & 11 deletions

File tree

README.md

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,164 @@ When WiFi mode is enabled on HDS, there is also a new WebSocket endpoint availab
4949

5050
you can also send a simple `tare` String over the websocket and the scale will tare itself.
5151

52+
For backwards compatibility, snapshot frames do not include a `type` field.
53+
Clients should treat the absence of `type` as a weight snapshot. Event and
54+
response frames always include `type`.
55+
56+
By default, WiFi clients receive weight snapshots at 2 Hz. A connected client
57+
can negotiate one of the supported WiFi stream rates by sending one of these
58+
WebSocket messages:
59+
60+
```text
61+
rate 2k
62+
rate 5k
63+
rate 10k
64+
```
65+
66+
or JSON:
67+
68+
```json
69+
{ "command": "rate", "value": "10k" }
70+
{ "rate": "10k" }
71+
```
72+
73+
The supported rates are 2 Hz, 5 Hz, and 10 Hz. The firmware sends back a
74+
`type: "rate"` acknowledgement with the active interval and rate. The firmware
75+
does not automatically downgrade a requested rate on weak WiFi; if the socket is
76+
not writable for a tick, that frame is skipped rather than queued.
77+
78+
WiFi snapshots are intentionally kept small, especially at 10 Hz:
79+
80+
```json
81+
{
82+
"grams": 25.66,
83+
"ms": 12345
84+
}
85+
```
86+
87+
Battery, charging, timer, and power/display state are sent in typed `status`
88+
frames instead of every snapshot. `status`, `battery`, and `info` return a
89+
status frame immediately. After `events on`, the firmware also sends a periodic
90+
`type: "status"` frame every 5 seconds.
91+
92+
WiFi clients can send these text commands over the same WebSocket:
93+
94+
```text
95+
tare
96+
events on
97+
events off
98+
timer start
99+
timer stop
100+
timer reset
101+
display on
102+
display off
103+
led 255 128 0
104+
led off
105+
low_power on
106+
low_power off
107+
soft_sleep on
108+
soft_sleep off
109+
power off
110+
status
111+
battery
112+
info
113+
```
114+
115+
The legacy text `tare` command is intentionally silent for backwards
116+
compatibility. JSON `{ "command": "tare" }` returns a `type: "status"` ack.
117+
118+
The same commands can be sent as JSON:
119+
120+
```json
121+
{ "command": "tare" }
122+
{ "command": "timer", "action": "start" }
123+
{ "command": "led", "r": 255, "g": 128, "b": 0 }
124+
```
125+
126+
Status frame shape:
127+
128+
```json
129+
{
130+
"type": "status",
131+
"status": "ok",
132+
"protocol_version": 1,
133+
"firmware_version": "FW: 3.0.9",
134+
"grams": 25.66,
135+
"ms": 12345,
136+
"battery_percent": 82,
137+
"battery_voltage": 3.95,
138+
"charging": false,
139+
"timer_running": true,
140+
"timer_seconds": 12,
141+
"display_on": true,
142+
"low_power": false,
143+
"soft_sleep": false,
144+
"events_enabled": true,
145+
"rate_hz": 10,
146+
"interval_ms": 100,
147+
"led": {
148+
"enabled": true,
149+
"r": 255,
150+
"g": 128,
151+
"b": 0
152+
}
153+
}
154+
```
155+
156+
For backwards compatibility, WiFi only sends weight snapshots by default. A
157+
client must send `events on` before periodic status, local scale button presses,
158+
or power-off notifications are emitted. The event stream resets to off when the
159+
WebSocket disconnects.
160+
161+
Button event fields:
162+
163+
```json
164+
{
165+
"type": "button",
166+
"button": "circle",
167+
"button_number": 1,
168+
"press": "short",
169+
"press_code": 1,
170+
"ms": 12345
171+
}
172+
```
173+
174+
Button numbers are `1 = circle` and `2 = square`. Press codes currently emitted
175+
over WiFi are `1 = short` and `2 = long`; the current finger-detection path only
176+
emits short-press events.
177+
178+
Power event fields:
179+
180+
```json
181+
{
182+
"type": "power",
183+
"event": "power_off",
184+
"reason": "low_battery",
185+
"reason_code": 3,
186+
"ms": 12345
187+
}
188+
```
189+
190+
Power reason codes are `0 = disabled/failed`, `1 = circle double-click`,
191+
`2 = square double-click`, `3 = low battery`, and `4 = gyro` when gyro support
192+
is compiled in.
193+
194+
Power and display command semantics:
195+
196+
- `display on` / `display off`: OLED power save on/off. The scale and WiFi stay
197+
awake.
198+
- `led <r> <g> <b>`: sets rich LED RGB state with each channel clamped to
199+
`0..255`. `led off` is equivalent to `led 0 0 0`.
200+
- `low_power on` / `low_power off`: sets OLED contrast to minimum/maximum. It
201+
does not disable the WiFi modem or drop the WebSocket link.
202+
- `soft_sleep on`: turns off the OLED and sensor power rails and pauses normal
203+
scale-loop work. WiFi remains configured so a later WebSocket command can wake
204+
it, but weight snapshots stop while soft sleep is active.
205+
- `soft_sleep off` / `soft_sleep wake`: restores sensor power, OLED power, and
206+
normal scale-loop work.
207+
- `power off`: full scale power-off. The WebSocket link will drop and cannot
208+
wake the scale.
209+
52210

53211
# How to upload Web apps?
54212

include/finger_detection.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include "parameter.h"
77
#include "ble.h"
88
#include "usbcomm.h"
9+
void sendWebsocketButton(int buttonNumber, int buttonShortPress);
910
// ============================================
1011
// Finger Press Recognition Algorithm for HDS
1112
// ============================================
@@ -210,6 +211,7 @@ bool isFingerPress(int button) {
210211
if (button == BUTTON_CIRCLE) {
211212
// Circle Button:Tare
212213
sendUsbButton(1, 1);
214+
sendWebsocketButton(1, 1);
213215
if (deviceConnected) {
214216
sendBleButton(1, 1);
215217
}
@@ -222,6 +224,7 @@ bool isFingerPress(int button) {
222224
} else if (button == BUTTON_SQUARE) {
223225
// Square Button:Timer control
224226
sendUsbButton(2, 1);
227+
sendWebsocketButton(2, 1);
225228
if (deviceConnected) {
226229
sendBleButton(2, 1);
227230
}
@@ -487,4 +490,4 @@ void updatePressSampling() {
487490
}
488491

489492
//End of finger detection
490-
#endif
493+
#endif

include/parameter.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,19 @@ unsigned long lastWeightTextNotifyTime = 0; // Stores the last time the weight
1212
unsigned long weightBleNotifyInterval = 100; // Interval at which to send weight notifications (milliseconds)
1313
unsigned long weightUsbNotifyInterval = 100; // Interval at which to send weight notifications (milliseconds)
1414
unsigned long weightTextNotifyInterval = 1000;
15+
const unsigned long WEBSOCKET_2HZ_NOTIFY_INTERVAL_MS = 500;
16+
const unsigned long WEBSOCKET_5HZ_NOTIFY_INTERVAL_MS = 200;
17+
const unsigned long WEBSOCKET_10HZ_NOTIFY_INTERVAL_MS = 100;
18+
const unsigned long WEBSOCKET_DEFAULT_NOTIFY_INTERVAL_MS = WEBSOCKET_2HZ_NOTIFY_INTERVAL_MS;
19+
const unsigned long WEBSOCKET_STATUS_NOTIFY_INTERVAL_MS = 5000;
20+
unsigned long weightWebsocketNotifyInterval = WEBSOCKET_DEFAULT_NOTIFY_INTERVAL_MS;
21+
bool b_websocketEventsEnabled = false;
22+
bool b_websocketLowPowerEnabled = false;
23+
bool b_websocketLedEnabled = false;
24+
uint8_t i_websocketLedR = 0;
25+
uint8_t i_websocketLedG = 0;
26+
uint8_t i_websocketLedB = 0;
27+
unsigned long t_lastWebsocketStatusUpdate = 0;
1528
int i_onWrite_counter = 0;
1629
unsigned long t_heartBeat = 0;
1730
unsigned long t_firstConnect = 0;

include/power.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Adafruit_ADS1115 ads; // Create an ADS1115 object
2020

2121
//prototype
2222
void sendBlePowerOff(int i_reason);
23+
void sendWebsocketPowerOff(int i_reason);
2324
void bleShutdown(); // defined in ble.h
2425
void stopWifi(); // defined in wifi_setup.cpp
2526
void stopWebServer(); // defined in webserver.h
@@ -233,6 +234,7 @@ void shut_down_low_battery(float voltage) {
233234
}
234235
sendBlePowerOff(3);
235236
#endif
237+
sendWebsocketPowerOff(3);
236238
#ifdef BUZZER
237239
buzzer.off();
238240
#endif
@@ -347,6 +349,7 @@ void power_off_gyro(int sec) {
347349
//Serial.println(" seconds to power off by gyro");
348350
if (d_timeleft <= 0) {
349351
sendBlePowerOff(4);
352+
sendWebsocketPowerOff(4);
350353
shut_down_now();
351354
}
352355
}

0 commit comments

Comments
 (0)