Skip to content

Commit 7347347

Browse files
basmeermanclaude
andcommitted
feat: extract pure C OCPP logic module with 56 native tests
Extract testable decision functions from esp32.cpp OCPP lambdas into ocpp_logic.h/c: auth path selection, connector state mapping, RFID hex formatting, LoadBl exclusivity check, and settings validation. Add missing PILOT_3V constant to evse_ctx.h and TEST_ASSERT_EQUAL_STRING to the test framework. Issue: #28 [Plan-03] Increment 1 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9b53659 commit 7347347

File tree

10 files changed

+1219
-0
lines changed

10 files changed

+1219
-0
lines changed

SmartEVSE-3/src/evse_ctx.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ extern "C" {
9393
#define PILOT_12V 12
9494
#define PILOT_9V 9
9595
#define PILOT_6V 6
96+
#define PILOT_3V 3
9697
#define PILOT_DIODE 1
9798
#define PILOT_SHORT 255
9899
#define PILOT_NOK 0

SmartEVSE-3/src/ocpp_logic.c

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
/*
2+
* ocpp_logic.c - Pure C OCPP decision logic
3+
*
4+
* Extracted from esp32.cpp OCPP lambdas. All functions are pure: they take
5+
* inputs and return decisions without side effects, making them testable
6+
* natively with gcc on any host.
7+
*/
8+
9+
#include "ocpp_logic.h"
10+
#include "evse_ctx.h"
11+
#include <string.h>
12+
#include <stdio.h>
13+
14+
/* ---- Auth path selection ---- */
15+
16+
ocpp_auth_path_t ocpp_select_auth_path(uint8_t rfid_reader) {
17+
/* RFIDReader=0 (disabled) or RFIDReader=6 (Rmt/OCPP): OCPP controls Access_bit */
18+
if (rfid_reader == 0 || rfid_reader == 6) {
19+
return OCPP_AUTH_PATH_OCPP_CONTROLLED;
20+
}
21+
/* RFIDReader=1..5: built-in RFID store controls charging, OCPP just reports */
22+
return OCPP_AUTH_PATH_BUILTIN_RFID;
23+
}
24+
25+
/* ---- Connector state ---- */
26+
27+
bool ocpp_is_connector_plugged(uint8_t cp_voltage) {
28+
/* Matches esp32.cpp: PILOT_3V..PILOT_9V means connector is plugged */
29+
return cp_voltage >= PILOT_3V && cp_voltage <= PILOT_9V;
30+
}
31+
32+
bool ocpp_is_ev_ready(uint8_t cp_voltage) {
33+
/* PILOT_3V..PILOT_6V = State C (EV requesting charge) */
34+
return cp_voltage >= PILOT_3V && cp_voltage <= PILOT_6V;
35+
}
36+
37+
/* ---- Access decision ---- */
38+
39+
bool ocpp_should_set_access(bool permits_charge, bool prev_permits_charge) {
40+
/*
41+
* Set Access_bit only on rising edge: OCPP just started permitting charge.
42+
* From esp32.cpp: if (!OcppTrackPermitsCharge && ocppPermitsCharge())
43+
* This ensures we set Access_bit only once per OCPP transaction, so other
44+
* modules can override it without OCPP re-setting it.
45+
*/
46+
return !prev_permits_charge && permits_charge;
47+
}
48+
49+
bool ocpp_should_clear_access(bool permits_charge, uint8_t access_status) {
50+
/*
51+
* Clear Access_bit when OCPP revokes permission and Access is currently ON.
52+
* From esp32.cpp: if (AccessStatus == ON && !ocppPermitsCharge())
53+
*/
54+
return access_status == 1 && !permits_charge; /* 1 = ON */
55+
}
56+
57+
/* ---- RFID hex formatting ---- */
58+
59+
void ocpp_format_rfid_hex(const uint8_t *rfid, size_t rfid_len,
60+
char *out, size_t out_size) {
61+
if (!rfid || !out || out_size == 0 || rfid_len == 0) {
62+
if (out && out_size > 0) out[0] = '\0';
63+
return;
64+
}
65+
66+
size_t start = 0;
67+
size_t count = rfid_len;
68+
69+
/*
70+
* Old reader format: rfid[0]==0x01 means the 6-byte UID starts at rfid[1].
71+
* New reader format: rfid[0]!=0x01 means the 7-byte UID starts at rfid[0].
72+
* From esp32.cpp ocppLoop():
73+
* if (RFID[0] == 0x01) snprintf(buf, ..., RFID[1]..RFID[6])
74+
* else snprintf(buf, ..., RFID[0]..RFID[6])
75+
*/
76+
if (rfid_len >= 7 && rfid[0] == 0x01) {
77+
start = 1;
78+
count = 6;
79+
}
80+
81+
out[0] = '\0';
82+
for (size_t i = start; i < start + count && i < rfid_len; i++) {
83+
size_t pos = (i - start) * 2;
84+
if (pos + 2 >= out_size) break;
85+
snprintf(out + pos, 3, "%02X", rfid[i]);
86+
}
87+
}
88+
89+
/* ---- Load balancing exclusivity ---- */
90+
91+
ocpp_lb_status_t ocpp_check_lb_exclusivity(uint8_t load_bl, bool ocpp_mode,
92+
bool was_standalone) {
93+
if (!ocpp_mode) {
94+
return OCPP_LB_OK;
95+
}
96+
97+
if (load_bl != 0) {
98+
/* LoadBl is non-zero while OCPP is active — Smart Charging is ineffective.
99+
* The state machine guards with !ctx->LoadBl, so OCPP limits are silently
100+
* ignored even though the backend thinks they're enforced. */
101+
return OCPP_LB_CONFLICT;
102+
}
103+
104+
if (!was_standalone) {
105+
/* LoadBl was non-zero when OCPP was initialized, now it's 0.
106+
* The Smart Charging callback was never registered — need OCPP reinit. */
107+
return OCPP_LB_NEEDS_REINIT;
108+
}
109+
110+
return OCPP_LB_OK;
111+
}
112+
113+
/* ---- Settings validation ---- */
114+
115+
ocpp_validate_result_t ocpp_validate_backend_url(const char *url) {
116+
if (!url || url[0] == '\0') {
117+
return OCPP_VALIDATE_EMPTY;
118+
}
119+
120+
/* Must start with ws:// or wss:// */
121+
bool has_ws = (strncmp(url, "ws://", 5) == 0);
122+
bool has_wss = (strncmp(url, "wss://", 6) == 0);
123+
124+
if (!has_ws && !has_wss) {
125+
return OCPP_VALIDATE_BAD_SCHEME;
126+
}
127+
128+
/* Must have content after the scheme */
129+
size_t scheme_len = has_wss ? 6 : 5;
130+
if (url[scheme_len] == '\0') {
131+
return OCPP_VALIDATE_BAD_SCHEME;
132+
}
133+
134+
return OCPP_VALIDATE_OK;
135+
}
136+
137+
ocpp_validate_result_t ocpp_validate_chargebox_id(const char *cb_id) {
138+
if (!cb_id || cb_id[0] == '\0') {
139+
return OCPP_VALIDATE_EMPTY;
140+
}
141+
142+
size_t len = strlen(cb_id);
143+
144+
/* OCPP 1.6 CiString20: max 20 characters */
145+
if (len > 20) {
146+
return OCPP_VALIDATE_TOO_LONG;
147+
}
148+
149+
/* Only printable ASCII allowed, no special/control chars */
150+
for (size_t i = 0; i < len; i++) {
151+
char c = cb_id[i];
152+
if (c < 0x20 || c > 0x7E) {
153+
return OCPP_VALIDATE_BAD_CHARS;
154+
}
155+
/* Reject chars that could cause issues in OCPP identifiers */
156+
if (c == '<' || c == '>' || c == '&' || c == '"' || c == '\'') {
157+
return OCPP_VALIDATE_BAD_CHARS;
158+
}
159+
}
160+
161+
return OCPP_VALIDATE_OK;
162+
}
163+
164+
ocpp_validate_result_t ocpp_validate_auth_key(const char *auth_key) {
165+
if (!auth_key || auth_key[0] == '\0') {
166+
/* Empty auth key is acceptable (no auth) */
167+
return OCPP_VALIDATE_OK;
168+
}
169+
170+
size_t len = strlen(auth_key);
171+
172+
/* OCPP 1.6: max 40 characters for AuthorizationKey */
173+
if (len > 40) {
174+
return OCPP_VALIDATE_TOO_LONG;
175+
}
176+
177+
return OCPP_VALIDATE_OK;
178+
}

SmartEVSE-3/src/ocpp_logic.h

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*
2+
* ocpp_logic.h - Pure C OCPP decision logic
3+
*
4+
* Extracted from esp32.cpp OCPP lambdas and glue code so that authorization,
5+
* connector state, RFID formatting, settings validation, and load-balancing
6+
* exclusivity logic can be tested natively without MicroOcpp or Arduino.
7+
*
8+
* All functions operate on plain C types — no MicroOcpp, Arduino, or ESP-IDF
9+
* dependencies allowed in this header.
10+
*/
11+
12+
#ifndef OCPP_LOGIC_H
13+
#define OCPP_LOGIC_H
14+
15+
#include <stdint.h>
16+
#include <stdbool.h>
17+
#include <stddef.h>
18+
19+
#ifdef __cplusplus
20+
extern "C" {
21+
#endif
22+
23+
/* ---- Auth path selection ---- */
24+
25+
typedef enum {
26+
OCPP_AUTH_PATH_OCPP_CONTROLLED = 0, /* RFIDReader=0 or 6: OCPP controls Access_bit */
27+
OCPP_AUTH_PATH_BUILTIN_RFID = 1 /* RFIDReader=1..5: built-in RFID store controls, OCPP just reports */
28+
} ocpp_auth_path_t;
29+
30+
ocpp_auth_path_t ocpp_select_auth_path(uint8_t rfid_reader);
31+
32+
/* ---- Connector state ---- */
33+
34+
bool ocpp_is_connector_plugged(uint8_t cp_voltage);
35+
bool ocpp_is_ev_ready(uint8_t cp_voltage);
36+
37+
/* ---- Access decision (OCPP-controlled auth path) ---- */
38+
39+
/*
40+
* Returns true if OCPP should set Access_bit ON.
41+
* Caller provides:
42+
* permits_charge — current ocppPermitsCharge() result
43+
* prev_permits_charge — tracked value from previous loop iteration
44+
*/
45+
bool ocpp_should_set_access(bool permits_charge, bool prev_permits_charge);
46+
47+
/*
48+
* Returns true if OCPP should clear Access_bit to OFF.
49+
* Caller provides:
50+
* permits_charge — current ocppPermitsCharge() result
51+
* access_status — current AccessStatus (0=OFF, 1=ON, 2=PAUSE)
52+
*/
53+
bool ocpp_should_clear_access(bool permits_charge, uint8_t access_status);
54+
55+
/* ---- RFID hex formatting ---- */
56+
57+
#define OCPP_RFID_HEX_MAX 15 /* 7 bytes * 2 hex chars + NUL */
58+
59+
/*
60+
* Format RFID bytes as uppercase hex string.
61+
* rfid — raw RFID bytes (7 bytes expected)
62+
* rfid_len — number of bytes in rfid array
63+
* out — output buffer (must be >= OCPP_RFID_HEX_MAX)
64+
* out_size — size of output buffer
65+
*
66+
* Old reader format: rfid[0]==0x01 means 6-byte UID starts at rfid[1].
67+
* New reader format: rfid[0]!=0x01 means 7-byte UID starts at rfid[0].
68+
*/
69+
void ocpp_format_rfid_hex(const uint8_t *rfid, size_t rfid_len,
70+
char *out, size_t out_size);
71+
72+
/* ---- Load balancing exclusivity ---- */
73+
74+
typedef enum {
75+
OCPP_LB_OK = 0, /* No conflict */
76+
OCPP_LB_CONFLICT = 1, /* LoadBl != 0 while OCPP active — Smart Charging ineffective */
77+
OCPP_LB_NEEDS_REINIT = 2 /* LoadBl changed from non-zero to 0 — OCPP needs disable/enable */
78+
} ocpp_lb_status_t;
79+
80+
/*
81+
* Check whether OCPP Smart Charging and internal load balancing conflict.
82+
* load_bl — current LoadBl value (0=standalone)
83+
* ocpp_mode — true if OCPP is enabled
84+
* was_standalone — true if LoadBl was 0 when OCPP was last initialized
85+
*/
86+
ocpp_lb_status_t ocpp_check_lb_exclusivity(uint8_t load_bl, bool ocpp_mode,
87+
bool was_standalone);
88+
89+
/* ---- Settings validation ---- */
90+
91+
typedef enum {
92+
OCPP_VALIDATE_OK = 0,
93+
OCPP_VALIDATE_EMPTY = 1,
94+
OCPP_VALIDATE_BAD_SCHEME = 2,
95+
OCPP_VALIDATE_TOO_LONG = 3,
96+
OCPP_VALIDATE_BAD_CHARS = 4
97+
} ocpp_validate_result_t;
98+
99+
/*
100+
* Validate OCPP backend URL.
101+
* Must start with "ws://" or "wss://", be non-empty, and have content after scheme.
102+
*/
103+
ocpp_validate_result_t ocpp_validate_backend_url(const char *url);
104+
105+
/*
106+
* Validate OCPP ChargeBoxId.
107+
* OCPP 1.6 CiString20: max 20 chars, printable ASCII, no special chars.
108+
*/
109+
ocpp_validate_result_t ocpp_validate_chargebox_id(const char *cb_id);
110+
111+
/*
112+
* Validate OCPP auth key.
113+
* OCPP 1.6: max 40 chars.
114+
*/
115+
ocpp_validate_result_t ocpp_validate_auth_key(const char *auth_key);
116+
117+
#ifdef __cplusplus
118+
}
119+
#endif
120+
121+
#endif /* OCPP_LOGIC_H */

SmartEVSE-3/test/native/Makefile

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ MQTT_PUBLISH_SRC := $(SRC_DIR)/mqtt_publish.c
2424
HTTP_API_SRC := $(SRC_DIR)/http_api.c
2525
SERIAL_PARSER_SRC := $(SRC_DIR)/serial_parser.c
2626
LED_COLOR_SRC := $(SRC_DIR)/led_color.c
27+
OCPP_LOGIC_SRC := $(SRC_DIR)/ocpp_logic.c
2728

2829
# Discover all test files
2930
TEST_SRCS := $(wildcard $(TEST_DIR)/test_*.c)
@@ -50,6 +51,21 @@ $(BUILD)/test_serial_parser: $(TEST_DIR)/test_serial_parser.c $(SERIAL_PARSER_SR
5051
$(BUILD)/test_led_color: $(TEST_DIR)/test_led_color.c $(LED_COLOR_SRC) $(SRC_DIR)/led_color.h include/*.h | $(BUILD)
5152
$(CC) $(CFLAGS) $(INCLUDES) -o $@ $(TEST_DIR)/test_led_color.c $(LED_COLOR_SRC)
5253

54+
$(BUILD)/test_ocpp_auth: $(TEST_DIR)/test_ocpp_auth.c $(OCPP_LOGIC_SRC) $(SRC_DIR)/ocpp_logic.h include/*.h | $(BUILD)
55+
$(CC) $(CFLAGS) $(INCLUDES) -o $@ $(TEST_DIR)/test_ocpp_auth.c $(OCPP_LOGIC_SRC)
56+
57+
$(BUILD)/test_ocpp_connector: $(TEST_DIR)/test_ocpp_connector.c $(OCPP_LOGIC_SRC) $(SRC_DIR)/ocpp_logic.h include/*.h | $(BUILD)
58+
$(CC) $(CFLAGS) $(INCLUDES) -o $@ $(TEST_DIR)/test_ocpp_connector.c $(OCPP_LOGIC_SRC)
59+
60+
$(BUILD)/test_ocpp_rfid: $(TEST_DIR)/test_ocpp_rfid.c $(OCPP_LOGIC_SRC) $(SRC_DIR)/ocpp_logic.h include/*.h | $(BUILD)
61+
$(CC) $(CFLAGS) $(INCLUDES) -o $@ $(TEST_DIR)/test_ocpp_rfid.c $(OCPP_LOGIC_SRC)
62+
63+
$(BUILD)/test_ocpp_lb: $(TEST_DIR)/test_ocpp_lb.c $(OCPP_LOGIC_SRC) $(SRC_DIR)/ocpp_logic.h include/*.h | $(BUILD)
64+
$(CC) $(CFLAGS) $(INCLUDES) -o $@ $(TEST_DIR)/test_ocpp_lb.c $(OCPP_LOGIC_SRC)
65+
66+
$(BUILD)/test_ocpp_settings: $(TEST_DIR)/test_ocpp_settings.c $(OCPP_LOGIC_SRC) $(SRC_DIR)/ocpp_logic.h include/*.h | $(BUILD)
67+
$(CC) $(CFLAGS) $(INCLUDES) -o $@ $(TEST_DIR)/test_ocpp_settings.c $(OCPP_LOGIC_SRC)
68+
5369
# Build each state machine test binary (generic rule)
5470
$(BUILD)/test_%: $(TEST_DIR)/test_%.c $(EVSE_SRC) include/*.h $(SRC_DIR)/*.h | $(BUILD)
5571
$(CC) $(CFLAGS) $(INCLUDES) -o $@ $(TEST_DIR)/test_$*.c $(EVSE_SRC)

SmartEVSE-3/test/native/include/test_framework.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,16 @@ static const char *tf_current_test = NULL;
5959
} \
6060
} while(0)
6161

62+
#define TEST_ASSERT_EQUAL_STRING(expected, actual) do { \
63+
const char *_e = (expected); const char *_a = (actual); \
64+
if ((_e == NULL && _a != NULL) || (_e != NULL && _a == NULL) || \
65+
(_e != NULL && _a != NULL && strcmp(_e, _a) != 0)) { \
66+
printf("\n FAIL %s:%d: expected \"%s\", got \"%s\"", __FILE__, __LINE__, \
67+
_e ? _e : "(null)", _a ? _a : "(null)"); \
68+
tf_current_failed = 1; \
69+
} \
70+
} while(0)
71+
6272
#define TEST_ASSERT_LESS_OR_EQUAL(threshold, actual) do { \
6373
int _t = (int)(threshold); int _a = (int)(actual); \
6474
if (_a > _t) { \

0 commit comments

Comments
 (0)