2323#endif
2424
2525#include " firmware_manager.h"
26+ #include " meter.h"
2627
2728#if SMARTEVSE_VERSION >=30
2829#include " OneWire.h"
@@ -86,6 +87,225 @@ static bool isTrackedLcdWsConnection(const mg_connection *connection) {
8687 return false ;
8788}
8889
90+ // BEGIN PLAN-07: WebSocket data channel
91+ #define WS_DATA_MAX_CONNECTIONS 4
92+ #define WS_DATA_INTERVAL_MS 500
93+ #define WS_DATA_SYNC_TICKS 60 // 60 * 500ms = 30 seconds
94+
95+ // Externs for globals accessed by the data channel
96+ extern uint8_t pilot;
97+ extern uint32_t CurrentPWM;
98+ extern uint16_t OverrideCurrent;
99+ extern int8_t TempEVSE;
100+ extern uint16_t maxTemp;
101+ extern int16_t homeBatteryCurrent;
102+ extern time_t homeBatteryLastUpdate;
103+ extern int phasesLastUpdate;
104+ extern uint8_t ErrorFlags;
105+ extern uint16_t MinCurrent;
106+ extern uint16_t MaxCurrent;
107+
108+ std::vector<mg_connection*> wsDataConnections;
109+ static mg_timer *wsDataTimer = nullptr ;
110+ static int wsDataSyncCounter = 0 ;
111+
112+ // Previous state for differential updates
113+ struct WsDataPrev {
114+ uint8_t mode_id;
115+ uint8_t state_id;
116+ uint8_t error_flags;
117+ uint16_t charge_current;
118+ int8_t temp;
119+ uint32_t pwm;
120+ uint16_t solar_stop_timer;
121+ bool car_connected;
122+ uint8_t loadbl;
123+ int32_t phase[3 ];
124+ int32_t evmeter_irms[3 ];
125+ int32_t evmeter_power;
126+ int32_t evmeter_charged_wh;
127+ int16_t battery_current;
128+ uint16_t override_current;
129+ bool initialized;
130+ };
131+ static WsDataPrev wsPrev = {};
132+
133+ static uint8_t wsGetModeId () {
134+ if (AccessStatus == OFF) return 0 ;
135+ if (AccessStatus == PAUSE) return 4 ;
136+ switch (Mode) {
137+ case MODE_NORMAL: return 1 ;
138+ case MODE_SOLAR: return 2 ;
139+ case MODE_SMART: return 3 ;
140+ default : return 255 ;
141+ }
142+ }
143+
144+ static void stopWsDataTimer (struct mg_mgr *manager) {
145+ if (wsDataTimer != nullptr && manager != nullptr ) {
146+ mg_timer_free (&manager->timers , wsDataTimer);
147+ wsDataTimer = nullptr ;
148+ _LOG_V (" Stopped WS data timer\n " );
149+ }
150+ }
151+
152+ static bool isTrackedDataWsConnection (const mg_connection *connection) {
153+ for (const auto *tracked : wsDataConnections) {
154+ if (tracked == connection) return true ;
155+ }
156+ return false ;
157+ }
158+
159+ // Build full state JSON for sync messages
160+ static void wsBuildFullState (DynamicJsonDocument &doc) {
161+ doc[" type" ] = " sync" ;
162+ JsonObject d = doc.createNestedObject (" d" );
163+ uint8_t mid = wsGetModeId ();
164+ d[" mode_id" ] = mid;
165+ d[" state_id" ] = State;
166+ d[" error_flags" ] = ErrorFlags;
167+ d[" charge_current" ] = Balanced[0 ];
168+ d[" temp" ] = TempEVSE;
169+ d[" temp_max" ] = maxTemp;
170+ d[" pwm" ] = CurrentPWM;
171+ d[" solar_stop_timer" ] = SolarStopTimer;
172+ d[" car_connected" ] = (pilot != PILOT_12V);
173+ d[" loadbl" ] = LoadBl;
174+ d[" override_current" ] = OverrideCurrent;
175+ d[" current_min" ] = MinCurrent;
176+ d[" current_max" ] = MaxCurrent;
177+ d[" phase_L1" ] = MainsMeter.Irms [0 ];
178+ d[" phase_L2" ] = MainsMeter.Irms [1 ];
179+ d[" phase_L3" ] = MainsMeter.Irms [2 ];
180+ d[" evmeter_L1" ] = EVMeter.Irms [0 ];
181+ d[" evmeter_L2" ] = EVMeter.Irms [1 ];
182+ d[" evmeter_L3" ] = EVMeter.Irms [2 ];
183+ d[" evmeter_power" ] = EVMeter.PowerMeasured ;
184+ d[" evmeter_charged_wh" ] = EVMeter.EnergyCharged ;
185+ d[" battery_current" ] = homeBatteryCurrent;
186+ d[" battery_last_update" ] = homeBatteryLastUpdate;
187+ d[" phases_last_update" ] = phasesLastUpdate;
188+ }
189+
190+ // Timer function - sends state updates to all connected websocket clients
191+ static void ws_data_timer_fn (void *arg) {
192+ struct mg_mgr *mgr_ptr = (struct mg_mgr *) arg;
193+
194+ if (wsDataConnections.empty ()) {
195+ stopWsDataTimer (mgr_ptr);
196+ return ;
197+ }
198+
199+ // Remove stale connections
200+ for (size_t i = wsDataConnections.size (); i > 0 ; --i) {
201+ const size_t idx = i - 1 ;
202+ mg_connection *c = wsDataConnections[idx];
203+ if (c == nullptr || c->is_closing ) {
204+ wsDataConnections.erase (wsDataConnections.begin () + idx);
205+ }
206+ }
207+ if (wsDataConnections.empty ()) {
208+ stopWsDataTimer (mgr_ptr);
209+ return ;
210+ }
211+
212+ wsDataSyncCounter++;
213+ bool fullSync = (wsDataSyncCounter >= WS_DATA_SYNC_TICKS);
214+ if (fullSync) wsDataSyncCounter = 0 ;
215+
216+ // Build JSON
217+ DynamicJsonDocument doc (fullSync ? 640 : 384 );
218+
219+ if (fullSync) {
220+ wsBuildFullState (doc);
221+ } else {
222+ // Differential update - only send changed fields
223+ uint8_t mid = wsGetModeId ();
224+ bool changed = false ;
225+ JsonObject d = doc.createNestedObject (" d" );
226+
227+ #define WS_DIFF (field, val ) do { \
228+ if (!wsPrev.initialized || wsPrev.field != (val)) { \
229+ d[#field] = (val); wsPrev.field = (val); changed = true ; \
230+ } \
231+ } while (0 )
232+
233+ WS_DIFF (mode_id, mid);
234+ WS_DIFF (state_id, State);
235+ WS_DIFF (error_flags, ErrorFlags);
236+ WS_DIFF (charge_current, Balanced[0 ]);
237+ WS_DIFF (temp, TempEVSE);
238+ WS_DIFF (pwm, CurrentPWM);
239+ WS_DIFF (solar_stop_timer, SolarStopTimer);
240+
241+ bool connected = (pilot != PILOT_12V);
242+ if (!wsPrev.initialized || wsPrev.car_connected != connected) {
243+ d[" car_connected" ] = connected; wsPrev.car_connected = connected; changed = true ;
244+ }
245+
246+ WS_DIFF (loadbl, LoadBl);
247+ WS_DIFF (override_current, OverrideCurrent);
248+
249+ // Phase currents
250+ for (int i = 0 ; i < 3 ; i++) {
251+ if (!wsPrev.initialized || wsPrev.phase [i] != MainsMeter.Irms [i]) {
252+ const char *keys[] = {" phase_L1" , " phase_L2" , " phase_L3" };
253+ d[keys[i]] = MainsMeter.Irms [i];
254+ wsPrev.phase [i] = MainsMeter.Irms [i];
255+ changed = true ;
256+ }
257+ }
258+ for (int i = 0 ; i < 3 ; i++) {
259+ if (!wsPrev.initialized || wsPrev.evmeter_irms [i] != EVMeter.Irms [i]) {
260+ const char *keys[] = {" evmeter_L1" , " evmeter_L2" , " evmeter_L3" };
261+ d[keys[i]] = EVMeter.Irms [i];
262+ wsPrev.evmeter_irms [i] = EVMeter.Irms [i];
263+ changed = true ;
264+ }
265+ }
266+
267+ WS_DIFF (evmeter_power, EVMeter.PowerMeasured );
268+ WS_DIFF (evmeter_charged_wh, EVMeter.EnergyCharged );
269+ WS_DIFF (battery_current, homeBatteryCurrent);
270+
271+ #undef WS_DIFF
272+
273+ wsPrev.initialized = true ;
274+
275+ if (!changed) return ; // Nothing changed, skip sending
276+ doc[" type" ] = " state" ;
277+ }
278+
279+ String json;
280+ serializeJson (doc, json);
281+ for (auto *c : wsDataConnections) {
282+ mg_ws_send (c, json.c_str (), json.length (), WEBSOCKET_OP_TEXT);
283+ }
284+
285+ // After full sync, update prev state
286+ if (fullSync) {
287+ wsPrev.mode_id = wsGetModeId ();
288+ wsPrev.state_id = State;
289+ wsPrev.error_flags = ErrorFlags;
290+ wsPrev.charge_current = Balanced[0 ];
291+ wsPrev.temp = TempEVSE;
292+ wsPrev.pwm = CurrentPWM;
293+ wsPrev.solar_stop_timer = SolarStopTimer;
294+ wsPrev.car_connected = (pilot != PILOT_12V);
295+ wsPrev.loadbl = LoadBl;
296+ wsPrev.override_current = OverrideCurrent;
297+ for (int i = 0 ; i < 3 ; i++) {
298+ wsPrev.phase [i] = MainsMeter.Irms [i];
299+ wsPrev.evmeter_irms [i] = EVMeter.Irms [i];
300+ }
301+ wsPrev.evmeter_power = EVMeter.PowerMeasured ;
302+ wsPrev.evmeter_charged_wh = EVMeter.EnergyCharged ;
303+ wsPrev.battery_current = homeBatteryCurrent;
304+ wsPrev.initialized = true ;
305+ }
306+ }
307+ // END PLAN-07: WebSocket data channel
308+
89309static void sendWsError (struct mg_connection *c, const char *reason) {
90310 DynamicJsonDocument response (96 );
91311 response[" error" ] = reason;
@@ -1071,6 +1291,16 @@ static void fn_http_server(struct mg_connection *c, int ev, void *ev_data) {
10711291 }
10721292 }
10731293 // END PLAN-06
1294+ // BEGIN PLAN-07: Clean up data WS connections on close
1295+ for (auto it = wsDataConnections.begin (); it != wsDataConnections.end (); ++it) {
1296+ if (*it == c) {
1297+ wsDataConnections.erase (it);
1298+ _LOG_V (" Removed websocket data connection, remaining: %d\n " , wsDataConnections.size ());
1299+ if (wsDataConnections.empty ()) stopWsDataTimer (c->mgr );
1300+ break ;
1301+ }
1302+ }
1303+ // END PLAN-07
10741304 } else if (ev == MG_EV_WS_OPEN) {
10751305 // Websocket connection opened - check if it's for /ws/lcd endpoint
10761306 struct mg_http_message *hm = (struct mg_http_message *) ev_data;
@@ -1090,16 +1320,44 @@ static void fn_http_server(struct mg_connection *c, int ev, void *ev_data) {
10901320 _LOG_V (" New diag WS connection, total: %d\n " , wsDiagConnections.size ());
10911321 }
10921322 // END PLAN-06
1323+ // BEGIN PLAN-07: Track data WS connections
1324+ if (mg_match (hm->uri , mg_str (" /ws/data" ), NULL )) {
1325+ if ((int )wsDataConnections.size () >= WS_DATA_MAX_CONNECTIONS) {
1326+ _LOG_W (" Max data WS connections reached (%d), rejecting\n " , WS_DATA_MAX_CONNECTIONS);
1327+ c->is_closing = 1 ;
1328+ } else {
1329+ wsDataConnections.push_back (c);
1330+ _LOG_V (" New websocket data connection, total: %d\n " , wsDataConnections.size ());
1331+
1332+ // Start timer if this is the first connection
1333+ if (wsDataConnections.size () == 1 && wsDataTimer == nullptr ) {
1334+ wsDataSyncCounter = 0 ;
1335+ wsPrev.initialized = false ;
1336+ wsDataTimer = mg_timer_add (&mgr, WS_DATA_INTERVAL_MS, MG_TIMER_REPEAT | MG_TIMER_RUN_NOW, ws_data_timer_fn, &mgr);
1337+ _LOG_V (" Started WS data timer\n " );
1338+ }
1339+
1340+ // Send immediate full sync to newly connected client
1341+ DynamicJsonDocument doc (640 );
1342+ wsBuildFullState (doc);
1343+ String json;
1344+ serializeJson (doc, json);
1345+ mg_ws_send (c, json.c_str (), json.length (), WEBSOCKET_OP_TEXT);
1346+ }
1347+ }
1348+ // END PLAN-07
10931349 } else if (ev == MG_EV_WS_MSG) {
10941350 // Websocket message received - handle button commands
10951351 struct mg_ws_message *wm = (struct mg_ws_message *) ev_data;
1096- if (!isTrackedLcdWsConnection (c)) return ;
1097-
1098- // Check if this is a text message (button commands are JSON text)
1099- if ((wm->flags & 0x0f ) == WEBSOCKET_OP_TEXT) {
1100- handleButtonCommand (c, (const char *)wm->data .buf , wm->data .len );
1352+ if (isTrackedLcdWsConnection (c)) {
1353+ // Check if this is a text message (button commands are JSON text)
1354+ if ((wm->flags & 0x0f ) == WEBSOCKET_OP_TEXT) {
1355+ handleButtonCommand (c, (const char *)wm->data .buf , wm->data .len );
1356+ }
1357+ // Binary messages are ignored (only server sends binary BMP images)
11011358 }
1102- // Binary messages are ignored (only server sends binary BMP images)
1359+ // BEGIN PLAN-07: Data WS messages (subscribe, etc.) - acknowledged silently
1360+ // END PLAN-07
11031361 } else if (ev == MG_EV_HTTP_MSG) { // New HTTP request received
11041362 struct mg_http_message *hm = (struct mg_http_message *) ev_data; // Parsed HTTP request
11051363
@@ -1115,6 +1373,13 @@ static void fn_http_server(struct mg_connection *c, int ev, void *ev_data) {
11151373 }
11161374 // END PLAN-06
11171375
1376+ // BEGIN PLAN-07: WebSocket upgrade for data channel
1377+ if (mg_match (hm->uri , mg_str (" /ws/data" ), NULL )) {
1378+ mg_ws_upgrade (c, hm, NULL );
1379+ return ;
1380+ }
1381+ // END PLAN-07
1382+
11181383 static webServerRequest requestObj; // Static to avoid heap allocation on every request
11191384 webServerRequest* request = &requestObj;
11201385 request->setMessage (hm);
0 commit comments