Skip to content

Commit 06dc654

Browse files
authored
Merge pull request #84 from basmeerman/work/plan-06
feat: diagnostic telemetry system (Plan 06)
2 parents aef3769 + c746e2d commit 06dc654

24 files changed

+3338
-1
lines changed

SmartEVSE-3/src/diag_modbus.c

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* diag_modbus.c - Modbus frame timing event ring buffer
3+
*
4+
* Pure C module — no platform dependencies, testable natively.
5+
*/
6+
7+
#include "diag_modbus.h"
8+
#include <string.h>
9+
10+
void diag_mb_init(diag_mb_ring_t *ring)
11+
{
12+
if (!ring)
13+
return;
14+
memset(ring, 0, sizeof(*ring));
15+
ring->enabled = false;
16+
}
17+
18+
void diag_mb_record(diag_mb_ring_t *ring, uint32_t timestamp_ms,
19+
uint8_t address, uint8_t function,
20+
uint8_t event_type, uint8_t error_code)
21+
{
22+
if (!ring || !ring->enabled)
23+
return;
24+
25+
diag_mb_event_t *ev = &ring->events[ring->head];
26+
ev->timestamp_ms = timestamp_ms;
27+
ev->address = address;
28+
ev->function = function;
29+
ev->event_type = event_type;
30+
ev->error_code = error_code;
31+
32+
ring->head = (ring->head + 1) % DIAG_MB_RING_SIZE;
33+
if (ring->count < DIAG_MB_RING_SIZE)
34+
ring->count++;
35+
}
36+
37+
uint8_t diag_mb_read(const diag_mb_ring_t *ring, diag_mb_event_t *out,
38+
uint8_t max_count)
39+
{
40+
if (!ring || !out || ring->count == 0)
41+
return 0;
42+
43+
uint8_t to_read = ring->count;
44+
if (to_read > max_count)
45+
to_read = max_count;
46+
47+
uint8_t start;
48+
if (ring->count < DIAG_MB_RING_SIZE)
49+
start = 0;
50+
else
51+
start = ring->head;
52+
53+
for (uint8_t i = 0; i < to_read; i++) {
54+
uint8_t idx = (start + i) % DIAG_MB_RING_SIZE;
55+
out[i] = ring->events[idx];
56+
}
57+
58+
return to_read;
59+
}
60+
61+
void diag_mb_reset(diag_mb_ring_t *ring)
62+
{
63+
if (!ring)
64+
return;
65+
ring->head = 0;
66+
ring->count = 0;
67+
}
68+
69+
void diag_mb_enable(diag_mb_ring_t *ring, bool enable)
70+
{
71+
if (!ring)
72+
return;
73+
ring->enabled = enable;
74+
}

SmartEVSE-3/src/diag_modbus.h

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* diag_modbus.h - Modbus frame timing event ring buffer
3+
*
4+
* Pure C module — no platform dependencies, testable natively.
5+
* Captures Modbus request/response/error events with timestamps
6+
* for diagnosing meter communication issues.
7+
*/
8+
9+
#ifndef DIAG_MODBUS_H
10+
#define DIAG_MODBUS_H
11+
12+
#ifdef __cplusplus
13+
extern "C" {
14+
#endif
15+
16+
#include <stdint.h>
17+
#include <stdbool.h>
18+
19+
/* Event types */
20+
#define DIAG_MB_EVENT_SENT 0 /* Request sent to device */
21+
#define DIAG_MB_EVENT_RECEIVED 1 /* Response received */
22+
#define DIAG_MB_EVENT_ERROR 2 /* Error/timeout on request */
23+
24+
/* Modbus event record — 8 bytes per event */
25+
typedef struct __attribute__((packed)) {
26+
uint32_t timestamp_ms; /* millis() when event occurred (4) */
27+
uint8_t address; /* Modbus device address (1) */
28+
uint8_t function; /* Modbus function code (1) */
29+
uint8_t event_type; /* DIAG_MB_EVENT_* (1) */
30+
uint8_t error_code; /* 0 on success, error code on fail (1) */
31+
} diag_mb_event_t;
32+
33+
/* Ring buffer configuration */
34+
#define DIAG_MB_RING_SIZE 32 /* 32 * 8 = 256 bytes */
35+
36+
/* Ring buffer state */
37+
typedef struct {
38+
diag_mb_event_t events[DIAG_MB_RING_SIZE];
39+
uint8_t head; /* Next write position */
40+
uint8_t count; /* Number of valid entries */
41+
bool enabled; /* Capture enabled */
42+
} diag_mb_ring_t;
43+
44+
/* Initialize the Modbus event ring buffer. */
45+
void diag_mb_init(diag_mb_ring_t *ring);
46+
47+
/* Record a Modbus event. No-op if ring is NULL or disabled. */
48+
void diag_mb_record(diag_mb_ring_t *ring, uint32_t timestamp_ms,
49+
uint8_t address, uint8_t function,
50+
uint8_t event_type, uint8_t error_code);
51+
52+
/* Read up to max_count events in chronological order.
53+
* Returns the number of events actually copied. */
54+
uint8_t diag_mb_read(const diag_mb_ring_t *ring, diag_mb_event_t *out,
55+
uint8_t max_count);
56+
57+
/* Reset the ring buffer (clears all events). */
58+
void diag_mb_reset(diag_mb_ring_t *ring);
59+
60+
/* Enable/disable event capture. */
61+
void diag_mb_enable(diag_mb_ring_t *ring, bool enable);
62+
63+
#ifdef __cplusplus
64+
}
65+
#endif
66+
67+
#endif /* DIAG_MODBUS_H */

SmartEVSE-3/src/diag_sampler.cpp

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
/*
2+
* diag_sampler.cpp - Diagnostic telemetry firmware integration
3+
*
4+
* Bridges firmware globals into diag_snapshot_t and pushes them
5+
* into the static ring buffer. Call diag_sample() from timer1s.
6+
*/
7+
8+
#if defined(SMARTEVSE_VERSION) /* ESP32 firmware only */
9+
10+
#include "main.h"
11+
#include "esp32.h"
12+
#include "diag_sampler.h"
13+
#include "diag_modbus.h"
14+
#include "evse_bridge.h"
15+
#include "meter.h"
16+
#include "debug.h"
17+
18+
#include <WiFi.h>
19+
#include <string.h>
20+
21+
/* Extern globals not exposed via headers */
22+
extern uint8_t State;
23+
extern uint8_t ErrorFlags;
24+
extern uint8_t ChargeDelay;
25+
extern uint8_t Mode;
26+
extern uint16_t ChargeCurrent;
27+
extern int16_t IsetBalanced;
28+
extern uint16_t OverrideCurrent;
29+
extern uint16_t SolarStopTimer;
30+
extern uint16_t ImportCurrent;
31+
extern uint16_t StartCurrent;
32+
extern uint8_t NoCurrent;
33+
extern uint8_t C1Timer;
34+
extern uint8_t AccessTimer;
35+
extern uint8_t Nr_Of_Phases_Charging;
36+
extern uint8_t LoadBl;
37+
extern uint16_t Balanced[];
38+
extern uint8_t BalancedState[];
39+
extern int8_t TempEVSE;
40+
extern uint8_t RCmon;
41+
extern uint8_t pilot;
42+
extern int16_t Isum;
43+
extern EnableC2_t EnableC2;
44+
extern AccessStatus_t AccessStatus;
45+
extern Switch_Phase_t Switching_Phases_C2;
46+
extern uint32_t serialnr;
47+
48+
#if MQTT
49+
extern struct MQTTclient_t MQTTclient;
50+
extern struct MQTTclientSmartEVSE_t MQTTclientSmartEVSE;
51+
#endif
52+
53+
/* Static ring buffer — 8 KB at default 128 slots */
54+
static diag_snapshot_t diag_buffer[DIAG_RING_SIZE_DEFAULT];
55+
static diag_ring_t diag_ring;
56+
static uint32_t diag_uptime_seconds;
57+
58+
void diag_sampler_init(void)
59+
{
60+
diag_ring_init(&diag_ring, diag_buffer, DIAG_RING_SIZE_DEFAULT);
61+
diag_uptime_seconds = 0;
62+
}
63+
64+
diag_ring_t *diag_get_ring(void)
65+
{
66+
return &diag_ring;
67+
}
68+
69+
extern diag_mb_ring_t g_diag_mb_ring;
70+
71+
void diag_start(diag_profile_t profile)
72+
{
73+
diag_ring_reset(&diag_ring);
74+
diag_set_profile(&diag_ring, profile);
75+
diag_ring.start_time = diag_uptime_seconds;
76+
77+
/* Enable Modbus event ring for MODBUS/FAST profiles */
78+
bool mb_enable = (profile == DIAG_PROFILE_MODBUS || profile == DIAG_PROFILE_FAST);
79+
diag_mb_enable(&g_diag_mb_ring, mb_enable);
80+
if (mb_enable)
81+
diag_mb_reset(&g_diag_mb_ring);
82+
}
83+
84+
void diag_stop(void)
85+
{
86+
diag_ring_freeze(&diag_ring, true);
87+
diag_mb_enable(&g_diag_mb_ring, false);
88+
}
89+
90+
void diag_sample(void)
91+
{
92+
diag_uptime_seconds++;
93+
94+
if (!diag_ring_tick(&diag_ring))
95+
return;
96+
97+
diag_snapshot_t snap;
98+
memset(&snap, 0, sizeof(snap));
99+
100+
snap.timestamp = diag_uptime_seconds;
101+
102+
/* State machine — read from globals (called within timer1s context,
103+
* after evse_sync_ctx_to_globals so globals are up-to-date) */
104+
snap.state = State;
105+
snap.error_flags = (uint8_t)(ErrorFlags & 0xFF);
106+
snap.charge_delay = ChargeDelay;
107+
snap.access_status = (uint8_t)AccessStatus;
108+
snap.mode = Mode;
109+
110+
/* Currents */
111+
snap.mains_irms[0] = MainsMeter.Irms[0];
112+
snap.mains_irms[1] = MainsMeter.Irms[1];
113+
snap.mains_irms[2] = MainsMeter.Irms[2];
114+
snap.ev_irms[0] = EVMeter.Irms[0];
115+
snap.ev_irms[1] = EVMeter.Irms[1];
116+
snap.ev_irms[2] = EVMeter.Irms[2];
117+
snap.isum = Isum;
118+
119+
/* Power allocation */
120+
snap.charge_current = ChargeCurrent;
121+
snap.iset_balanced = (int16_t)IsetBalanced;
122+
snap.override_current = OverrideCurrent;
123+
124+
/* Solar */
125+
snap.solar_stop_timer = SolarStopTimer;
126+
snap.import_current = ImportCurrent;
127+
snap.start_current = StartCurrent;
128+
129+
/* Timers — StateTimer is internal to evse_ctx_t, read via bridge */
130+
evse_bridge_lock();
131+
snap.state_timer = g_evse_ctx.StateTimer;
132+
evse_bridge_unlock();
133+
snap.c1_timer = C1Timer;
134+
snap.access_timer = AccessTimer;
135+
snap.no_current = NoCurrent;
136+
137+
/* Phase switching */
138+
snap.nr_phases_charging = Nr_Of_Phases_Charging;
139+
snap.switching_c2 = (uint8_t)Switching_Phases_C2;
140+
snap.enable_c2 = (uint8_t)EnableC2;
141+
142+
/* Load balancing */
143+
snap.load_bl = LoadBl;
144+
snap.balanced_state_0 = BalancedState[0];
145+
snap.balanced_0 = Balanced[0];
146+
147+
/* Temperature & safety */
148+
snap.temp_evse = TempEVSE;
149+
snap.rc_mon = RCmon;
150+
snap.pilot_reading = pilot;
151+
152+
/* Modbus health */
153+
#if !defined(SMARTEVSE_VERSION) || SMARTEVSE_VERSION >=30 && SMARTEVSE_VERSION < 40
154+
snap.mains_meter_timeout = MainsMeter.Timeout;
155+
snap.ev_meter_timeout = EVMeter.Timeout;
156+
#endif
157+
snap.mains_meter_type = MainsMeter.Type;
158+
snap.ev_meter_type = EVMeter.Type;
159+
160+
/* Network */
161+
snap.wifi_rssi = WiFi.isConnected() ? (int8_t)WiFi.RSSI() : 0;
162+
#if MQTT
163+
snap.mqtt_connected = (MQTTclient.connected ? 1 : 0)
164+
| (MQTTclientSmartEVSE.connected ? 2 : 0);
165+
#endif
166+
167+
diag_ring_push(&diag_ring, &snap);
168+
169+
/* Push to WebSocket clients if any are connected */
170+
diag_ws_push_snapshot(&snap);
171+
}
172+
173+
int diag_status_json(char *buf, size_t bufsz)
174+
{
175+
if (!buf || bufsz == 0)
176+
return -1;
177+
178+
const char *profile_names[] = {
179+
"off", "general", "solar", "loadbal", "modbus", "fast"
180+
};
181+
const char *pname = "off";
182+
if (diag_ring.profile >= DIAG_PROFILE_OFF &&
183+
diag_ring.profile <= DIAG_PROFILE_FAST)
184+
pname = profile_names[diag_ring.profile];
185+
186+
int n = snprintf(buf, bufsz,
187+
"{\"profile\":\"%s\",\"count\":%u,\"capacity\":%u,"
188+
"\"frozen\":%s,\"uptime\":%u}",
189+
pname,
190+
(unsigned)diag_ring.count,
191+
(unsigned)diag_ring.capacity,
192+
diag_ring.frozen ? "true" : "false",
193+
(unsigned)diag_uptime_seconds);
194+
195+
if (n < 0 || (size_t)n >= bufsz)
196+
return -1;
197+
198+
return n;
199+
}
200+
201+
#endif /* SMARTEVSE_VERSION */

SmartEVSE-3/src/diag_sampler.h

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* diag_sampler.h - Diagnostic telemetry firmware integration
3+
*
4+
* Provides the sampling bridge between firmware globals and the
5+
* pure C ring buffer (diag_telemetry.h).
6+
*
7+
* Call diag_sampler_init() once at startup.
8+
* Call diag_sample() from Timer1S (1 Hz) or Timer10ms (10 Hz for FAST/MODBUS).
9+
*/
10+
11+
#ifndef DIAG_SAMPLER_H
12+
#define DIAG_SAMPLER_H
13+
14+
#ifdef __cplusplus
15+
extern "C" {
16+
#endif
17+
18+
#include "diag_telemetry.h"
19+
20+
/* Initialize the diagnostic sampler (allocates static ring buffer). */
21+
void diag_sampler_init(void);
22+
23+
/* Take a sample if the active profile says it's time.
24+
* Call from timer1s (1 Hz context) for GENERAL/SOLAR/LOADBAL profiles. */
25+
void diag_sample(void);
26+
27+
/* Get a pointer to the global ring buffer (for REST endpoints). */
28+
diag_ring_t *diag_get_ring(void);
29+
30+
/* Start capture with a given profile. Resets the ring buffer. */
31+
void diag_start(diag_profile_t profile);
32+
33+
/* Stop capture (freezes the ring buffer for download). */
34+
void diag_stop(void);
35+
36+
/* Format a JSON status response into buf. Returns bytes written or -1. */
37+
int diag_status_json(char *buf, size_t bufsz);
38+
39+
/* Push latest snapshot to connected WebSocket clients.
40+
* Implemented in network_common.cpp. No-op if no clients connected. */
41+
void diag_ws_push_snapshot(const diag_snapshot_t *snap);
42+
43+
#ifdef __cplusplus
44+
}
45+
#endif
46+
47+
#endif /* DIAG_SAMPLER_H */

0 commit comments

Comments
 (0)