Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions SmartEVSE-3/src/diag_modbus.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* diag_modbus.c - Modbus frame timing event ring buffer
*
* Pure C module — no platform dependencies, testable natively.
*/

#include "diag_modbus.h"
#include <string.h>

void diag_mb_init(diag_mb_ring_t *ring)
{
if (!ring)
return;
memset(ring, 0, sizeof(*ring));
ring->enabled = false;
}

void diag_mb_record(diag_mb_ring_t *ring, uint32_t timestamp_ms,
uint8_t address, uint8_t function,
uint8_t event_type, uint8_t error_code)
{
if (!ring || !ring->enabled)
return;

diag_mb_event_t *ev = &ring->events[ring->head];
ev->timestamp_ms = timestamp_ms;
ev->address = address;
ev->function = function;
ev->event_type = event_type;
ev->error_code = error_code;

ring->head = (ring->head + 1) % DIAG_MB_RING_SIZE;
if (ring->count < DIAG_MB_RING_SIZE)
ring->count++;
}

uint8_t diag_mb_read(const diag_mb_ring_t *ring, diag_mb_event_t *out,
uint8_t max_count)
{
if (!ring || !out || ring->count == 0)
return 0;

uint8_t to_read = ring->count;
if (to_read > max_count)
to_read = max_count;

uint8_t start;
if (ring->count < DIAG_MB_RING_SIZE)
start = 0;
else
start = ring->head;

for (uint8_t i = 0; i < to_read; i++) {
uint8_t idx = (start + i) % DIAG_MB_RING_SIZE;
out[i] = ring->events[idx];
}

return to_read;
}

void diag_mb_reset(diag_mb_ring_t *ring)
{
if (!ring)
return;
ring->head = 0;
ring->count = 0;
}

void diag_mb_enable(diag_mb_ring_t *ring, bool enable)
{
if (!ring)
return;
ring->enabled = enable;
}
67 changes: 67 additions & 0 deletions SmartEVSE-3/src/diag_modbus.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* diag_modbus.h - Modbus frame timing event ring buffer
*
* Pure C module — no platform dependencies, testable natively.
* Captures Modbus request/response/error events with timestamps
* for diagnosing meter communication issues.
*/

#ifndef DIAG_MODBUS_H
#define DIAG_MODBUS_H

#ifdef __cplusplus
extern "C" {
#endif

#include <stdint.h>
#include <stdbool.h>

/* Event types */
#define DIAG_MB_EVENT_SENT 0 /* Request sent to device */
#define DIAG_MB_EVENT_RECEIVED 1 /* Response received */
#define DIAG_MB_EVENT_ERROR 2 /* Error/timeout on request */

/* Modbus event record — 8 bytes per event */
typedef struct __attribute__((packed)) {
uint32_t timestamp_ms; /* millis() when event occurred (4) */
uint8_t address; /* Modbus device address (1) */
uint8_t function; /* Modbus function code (1) */
uint8_t event_type; /* DIAG_MB_EVENT_* (1) */
uint8_t error_code; /* 0 on success, error code on fail (1) */
} diag_mb_event_t;

/* Ring buffer configuration */
#define DIAG_MB_RING_SIZE 32 /* 32 * 8 = 256 bytes */

/* Ring buffer state */
typedef struct {
diag_mb_event_t events[DIAG_MB_RING_SIZE];
uint8_t head; /* Next write position */
uint8_t count; /* Number of valid entries */
bool enabled; /* Capture enabled */
} diag_mb_ring_t;

/* Initialize the Modbus event ring buffer. */
void diag_mb_init(diag_mb_ring_t *ring);

/* Record a Modbus event. No-op if ring is NULL or disabled. */
void diag_mb_record(diag_mb_ring_t *ring, uint32_t timestamp_ms,
uint8_t address, uint8_t function,
uint8_t event_type, uint8_t error_code);

/* Read up to max_count events in chronological order.
* Returns the number of events actually copied. */
uint8_t diag_mb_read(const diag_mb_ring_t *ring, diag_mb_event_t *out,
uint8_t max_count);

/* Reset the ring buffer (clears all events). */
void diag_mb_reset(diag_mb_ring_t *ring);

/* Enable/disable event capture. */
void diag_mb_enable(diag_mb_ring_t *ring, bool enable);

#ifdef __cplusplus
}
#endif

#endif /* DIAG_MODBUS_H */
201 changes: 201 additions & 0 deletions SmartEVSE-3/src/diag_sampler.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
/*
* diag_sampler.cpp - Diagnostic telemetry firmware integration
*
* Bridges firmware globals into diag_snapshot_t and pushes them
* into the static ring buffer. Call diag_sample() from timer1s.
*/

#if defined(SMARTEVSE_VERSION) /* ESP32 firmware only */

#include "main.h"
#include "esp32.h"
#include "diag_sampler.h"
#include "diag_modbus.h"
#include "evse_bridge.h"
#include "meter.h"
#include "debug.h"

#include <WiFi.h>
#include <string.h>

/* Extern globals not exposed via headers */
extern uint8_t State;
extern uint8_t ErrorFlags;
extern uint8_t ChargeDelay;
extern uint8_t Mode;
extern uint16_t ChargeCurrent;
extern int16_t IsetBalanced;
extern uint16_t OverrideCurrent;
extern uint16_t SolarStopTimer;
extern uint16_t ImportCurrent;
extern uint16_t StartCurrent;
extern uint8_t NoCurrent;
extern uint8_t C1Timer;
extern uint8_t AccessTimer;
extern uint8_t Nr_Of_Phases_Charging;
extern uint8_t LoadBl;
extern uint16_t Balanced[];
extern uint8_t BalancedState[];
extern int8_t TempEVSE;
extern uint8_t RCmon;
extern uint8_t pilot;
extern int16_t Isum;
extern EnableC2_t EnableC2;
extern AccessStatus_t AccessStatus;
extern Switch_Phase_t Switching_Phases_C2;
extern uint32_t serialnr;

#if MQTT
extern struct MQTTclient_t MQTTclient;
extern struct MQTTclientSmartEVSE_t MQTTclientSmartEVSE;
#endif

/* Static ring buffer — 8 KB at default 128 slots */
static diag_snapshot_t diag_buffer[DIAG_RING_SIZE_DEFAULT];
static diag_ring_t diag_ring;
static uint32_t diag_uptime_seconds;

void diag_sampler_init(void)
{
diag_ring_init(&diag_ring, diag_buffer, DIAG_RING_SIZE_DEFAULT);
diag_uptime_seconds = 0;
}

diag_ring_t *diag_get_ring(void)
{
return &diag_ring;
}

extern diag_mb_ring_t g_diag_mb_ring;

void diag_start(diag_profile_t profile)
{
diag_ring_reset(&diag_ring);
diag_set_profile(&diag_ring, profile);
diag_ring.start_time = diag_uptime_seconds;

/* Enable Modbus event ring for MODBUS/FAST profiles */
bool mb_enable = (profile == DIAG_PROFILE_MODBUS || profile == DIAG_PROFILE_FAST);
diag_mb_enable(&g_diag_mb_ring, mb_enable);
if (mb_enable)
diag_mb_reset(&g_diag_mb_ring);
}

void diag_stop(void)
{
diag_ring_freeze(&diag_ring, true);
diag_mb_enable(&g_diag_mb_ring, false);
}

void diag_sample(void)
{
diag_uptime_seconds++;

if (!diag_ring_tick(&diag_ring))
return;

diag_snapshot_t snap;
memset(&snap, 0, sizeof(snap));

snap.timestamp = diag_uptime_seconds;

/* State machine — read from globals (called within timer1s context,
* after evse_sync_ctx_to_globals so globals are up-to-date) */
snap.state = State;
snap.error_flags = (uint8_t)(ErrorFlags & 0xFF);
snap.charge_delay = ChargeDelay;
snap.access_status = (uint8_t)AccessStatus;
snap.mode = Mode;

/* Currents */
snap.mains_irms[0] = MainsMeter.Irms[0];
snap.mains_irms[1] = MainsMeter.Irms[1];
snap.mains_irms[2] = MainsMeter.Irms[2];
snap.ev_irms[0] = EVMeter.Irms[0];
snap.ev_irms[1] = EVMeter.Irms[1];
snap.ev_irms[2] = EVMeter.Irms[2];
snap.isum = Isum;

/* Power allocation */
snap.charge_current = ChargeCurrent;
snap.iset_balanced = (int16_t)IsetBalanced;
snap.override_current = OverrideCurrent;

/* Solar */
snap.solar_stop_timer = SolarStopTimer;
snap.import_current = ImportCurrent;
snap.start_current = StartCurrent;

/* Timers — StateTimer is internal to evse_ctx_t, read via bridge */
evse_bridge_lock();
snap.state_timer = g_evse_ctx.StateTimer;
evse_bridge_unlock();
snap.c1_timer = C1Timer;
snap.access_timer = AccessTimer;
snap.no_current = NoCurrent;

/* Phase switching */
snap.nr_phases_charging = Nr_Of_Phases_Charging;
snap.switching_c2 = (uint8_t)Switching_Phases_C2;
snap.enable_c2 = (uint8_t)EnableC2;

/* Load balancing */
snap.load_bl = LoadBl;
snap.balanced_state_0 = BalancedState[0];
snap.balanced_0 = Balanced[0];

/* Temperature & safety */
snap.temp_evse = TempEVSE;
snap.rc_mon = RCmon;
snap.pilot_reading = pilot;

/* Modbus health */
#if !defined(SMARTEVSE_VERSION) || SMARTEVSE_VERSION >=30 && SMARTEVSE_VERSION < 40
snap.mains_meter_timeout = MainsMeter.Timeout;
snap.ev_meter_timeout = EVMeter.Timeout;
#endif
snap.mains_meter_type = MainsMeter.Type;
snap.ev_meter_type = EVMeter.Type;

/* Network */
snap.wifi_rssi = WiFi.isConnected() ? (int8_t)WiFi.RSSI() : 0;
#if MQTT
snap.mqtt_connected = (MQTTclient.connected ? 1 : 0)
| (MQTTclientSmartEVSE.connected ? 2 : 0);
#endif

diag_ring_push(&diag_ring, &snap);

/* Push to WebSocket clients if any are connected */
diag_ws_push_snapshot(&snap);
}

int diag_status_json(char *buf, size_t bufsz)
{
if (!buf || bufsz == 0)
return -1;

const char *profile_names[] = {
"off", "general", "solar", "loadbal", "modbus", "fast"
};
const char *pname = "off";
if (diag_ring.profile >= DIAG_PROFILE_OFF &&
diag_ring.profile <= DIAG_PROFILE_FAST)
pname = profile_names[diag_ring.profile];

int n = snprintf(buf, bufsz,
"{\"profile\":\"%s\",\"count\":%u,\"capacity\":%u,"
"\"frozen\":%s,\"uptime\":%u}",
pname,
(unsigned)diag_ring.count,
(unsigned)diag_ring.capacity,
diag_ring.frozen ? "true" : "false",
(unsigned)diag_uptime_seconds);

if (n < 0 || (size_t)n >= bufsz)
return -1;

return n;
}

#endif /* SMARTEVSE_VERSION */
47 changes: 47 additions & 0 deletions SmartEVSE-3/src/diag_sampler.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* diag_sampler.h - Diagnostic telemetry firmware integration
*
* Provides the sampling bridge between firmware globals and the
* pure C ring buffer (diag_telemetry.h).
*
* Call diag_sampler_init() once at startup.
* Call diag_sample() from Timer1S (1 Hz) or Timer10ms (10 Hz for FAST/MODBUS).
*/

#ifndef DIAG_SAMPLER_H
#define DIAG_SAMPLER_H

#ifdef __cplusplus
extern "C" {
#endif

#include "diag_telemetry.h"

/* Initialize the diagnostic sampler (allocates static ring buffer). */
void diag_sampler_init(void);

/* Take a sample if the active profile says it's time.
* Call from timer1s (1 Hz context) for GENERAL/SOLAR/LOADBAL profiles. */
void diag_sample(void);

/* Get a pointer to the global ring buffer (for REST endpoints). */
diag_ring_t *diag_get_ring(void);

/* Start capture with a given profile. Resets the ring buffer. */
void diag_start(diag_profile_t profile);

/* Stop capture (freezes the ring buffer for download). */
void diag_stop(void);

/* Format a JSON status response into buf. Returns bytes written or -1. */
int diag_status_json(char *buf, size_t bufsz);

/* Push latest snapshot to connected WebSocket clients.
* Implemented in network_common.cpp. No-op if no clients connected. */
void diag_ws_push_snapshot(const diag_snapshot_t *snap);

#ifdef __cplusplus
}
#endif

#endif /* DIAG_SAMPLER_H */
Loading
Loading