Skip to content

Commit 7f7b735

Browse files
authored
Merge pull request #121 from basmeerman/fix/menu-mode-setmode-consistency
fix: MENU_MODE uses setMode() for consistent slave mode sync
2 parents c63ee1e + 40dddbb commit 7f7b735

File tree

2 files changed

+311
-1
lines changed

2 files changed

+311
-1
lines changed

SmartEVSE-3/src/main.cpp

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2689,7 +2689,15 @@ uint8_t setItemValue(uint8_t nav, uint16_t val) {
26892689
break;
26902690
SETITEM(MENU_MAX_TEMP, maxTemp)
26912691
SETITEM(MENU_CONFIG, Config)
2692-
SETITEM(MENU_MODE, Mode)
2692+
// MENU_MODE must call setMode() for full side effects (phase switching,
2693+
// error clearing, ChargeDelay reset) — not just raw assignment.
2694+
// Mirrors the STATUS_MODE handler below. Guards with Mode != val to
2695+
// prevent redundant calls when both BroadcastSettings and per-node
2696+
// writes deliver the same mode. Fixes #120.
2697+
case MENU_MODE:
2698+
if (Mode != val)
2699+
setMode(val);
2700+
break;
26932701
SETITEM(MENU_START, StartCurrent)
26942702
SETITEM(MENU_STOP, StopTime)
26952703
SETITEM(MENU_IMPORT, ImportCurrent)
Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
/*
2+
* test_mode_sync.c - Mode synchronization behavioral expectations
3+
*
4+
* The firmware's setMode() function applies side effects when switching
5+
* between Normal/Smart/Solar modes: phase switching, error clearing,
6+
* timer resets, and charge delay clearing. These tests verify the state
7+
* machine's expected state after a mode switch, documenting the behavior
8+
* that the MENU_MODE fix (#120) enables for slaves receiving mode via
9+
* BroadcastSettings.
10+
*
11+
* Note: setMode() itself is in main.cpp (firmware glue, not testable
12+
* natively). These tests exercise the pure C functions it calls:
13+
* evse_check_switching_phases(), evse_clear_error_flags(), evse_set_state().
14+
*/
15+
16+
#include "test_framework.h"
17+
#include "evse_ctx.h"
18+
#include "evse_state_machine.h"
19+
20+
static evse_ctx_t ctx;
21+
22+
static void setup_charging_3p(void) {
23+
evse_init(&ctx, NULL);
24+
ctx.AccessStatus = ON;
25+
ctx.Mode = MODE_SMART;
26+
ctx.LoadBl = 0;
27+
ctx.State = STATE_C;
28+
ctx.BalancedState[0] = STATE_C;
29+
ctx.BalancedMax[0] = 160;
30+
ctx.Balanced[0] = 100;
31+
ctx.ChargeCurrent = 160;
32+
ctx.MinCurrent = 6;
33+
ctx.MaxCurrent = 16;
34+
ctx.MaxCapacity = 16;
35+
ctx.Nr_Of_Phases_Charging = 3;
36+
ctx.EnableC2 = AUTO;
37+
ctx.contactor1_state = true;
38+
ctx.contactor2_state = true;
39+
ctx.MainsMeterType = 1;
40+
ctx.MaxMains = 25;
41+
ctx.MaxCircuit = 32;
42+
ctx.phasesLastUpdateFlag = true;
43+
ctx.Node[0].IntTimer = 100;
44+
}
45+
46+
/* ================================================================
47+
* GROUP 1: Phase switching on mode change (EnableC2 = SOLAR_OFF)
48+
*
49+
* When EnableC2 = SOLAR_OFF and mode switches between Solar and
50+
* other modes, the CP must disconnect (C→C1 or B→B1) to allow
51+
* safe contactor switching. Without setMode(), this is skipped.
52+
* ================================================================ */
53+
54+
/*
55+
* @feature Mode Synchronization
56+
* @req REQ-MODE-SYNC-001
57+
* @scenario SOLAR_OFF: switching to Solar requires single-phase (evse_force_single_phase)
58+
* @given EVSE in Smart mode charging on 3 phases, EnableC2=SOLAR_OFF
59+
* @when Mode is set to Solar and evse_check_switching_phases is called
60+
* @then evse_force_single_phase returns true (C2 must be off in Solar mode)
61+
*/
62+
void test_solar_off_forces_single_phase_in_solar(void) {
63+
setup_charging_3p();
64+
ctx.EnableC2 = SOLAR_OFF;
65+
ctx.Mode = MODE_SOLAR;
66+
67+
/* evse_force_single_phase checks EnableC2 and Mode */
68+
int force_1p = evse_force_single_phase(&ctx);
69+
TEST_ASSERT_TRUE(force_1p);
70+
}
71+
72+
/*
73+
* @feature Mode Synchronization
74+
* @req REQ-MODE-SYNC-002
75+
* @scenario SOLAR_OFF: Smart mode allows three-phase
76+
* @given EVSE with EnableC2=SOLAR_OFF in Smart mode
77+
* @when evse_force_single_phase is checked
78+
* @then Returns false (C2 allowed in non-Solar modes with SOLAR_OFF)
79+
*/
80+
void test_solar_off_allows_3p_in_smart(void) {
81+
setup_charging_3p();
82+
ctx.EnableC2 = SOLAR_OFF;
83+
ctx.Mode = MODE_SMART;
84+
85+
int force_1p = evse_force_single_phase(&ctx);
86+
TEST_ASSERT_FALSE(force_1p);
87+
}
88+
89+
/*
90+
* @feature Mode Synchronization
91+
* @req REQ-MODE-SYNC-003
92+
* @scenario State C entry with SOLAR_OFF in Solar mode opens C2 contactor
93+
* @given EVSE with EnableC2=SOLAR_OFF, Mode=Solar, entering STATE_C
94+
* @when evse_set_state(ctx, STATE_C) is called
95+
* @then contactor2 is off (single-phase charging)
96+
*/
97+
void test_state_c_entry_solar_off_opens_c2(void) {
98+
setup_charging_3p();
99+
ctx.EnableC2 = SOLAR_OFF;
100+
ctx.Mode = MODE_SOLAR;
101+
ctx.State = STATE_B;
102+
103+
evse_set_state(&ctx, STATE_C);
104+
105+
/* In Solar mode with SOLAR_OFF, C2 should be off */
106+
TEST_ASSERT_FALSE(ctx.contactor2_state);
107+
TEST_ASSERT_EQUAL_INT(1, ctx.Nr_Of_Phases_Charging);
108+
}
109+
110+
/* ================================================================
111+
* GROUP 2: Error and timer clearing on mode switch
112+
*
113+
* setMode() clears LESS_6A and SolarStopTimer when switching to
114+
* Smart mode. Without setMode(), a slave could retain stale errors
115+
* from a previous Solar mode session.
116+
* ================================================================ */
117+
118+
/*
119+
* @feature Mode Synchronization
120+
* @req REQ-MODE-SYNC-004
121+
* @scenario Clearing LESS_6A on switch to Smart (via evse_clear_error_flags)
122+
* @given EVSE with LESS_6A error set from solar shortage
123+
* @when evse_clear_error_flags clears LESS_6A (as setMode does for Smart)
124+
* @then ErrorFlags no longer has LESS_6A set
125+
*/
126+
void test_clear_less6a_on_mode_switch(void) {
127+
setup_charging_3p();
128+
ctx.ErrorFlags = LESS_6A;
129+
130+
/* setMode(Smart) calls clearErrorFlags(LESS_6A) */
131+
evse_clear_error_flags(&ctx, LESS_6A);
132+
133+
TEST_ASSERT_FALSE(ctx.ErrorFlags & LESS_6A);
134+
}
135+
136+
/*
137+
* @feature Mode Synchronization
138+
* @req REQ-MODE-SYNC-005
139+
* @scenario SolarStopTimer persists if mode switch misses setMode
140+
* @given EVSE with SolarStopTimer=300, mode changes to Smart
141+
* @when Only Mode variable is assigned (simulating SETITEM bug)
142+
* @then SolarStopTimer remains at 300 (stale — not cleared)
143+
* @note This documents the bug: without setMode(), timers are not reset
144+
*/
145+
void test_raw_mode_assign_leaves_timer_stale(void) {
146+
setup_charging_3p();
147+
ctx.Mode = MODE_SOLAR;
148+
ctx.SolarStopTimer = 300;
149+
150+
/* Simulate SETITEM(MENU_MODE, Mode): raw assignment */
151+
ctx.Mode = MODE_SMART;
152+
153+
/* Timer NOT cleared — this is the bug */
154+
TEST_ASSERT_EQUAL_INT(300, ctx.SolarStopTimer);
155+
}
156+
157+
/*
158+
* @feature Mode Synchronization
159+
* @req REQ-MODE-SYNC-006
160+
* @scenario SolarStopTimer cleared when setMode side effects applied
161+
* @given EVSE with SolarStopTimer=300, mode changes to Smart
162+
* @when setMode side effects are applied (timer reset to 0)
163+
* @then SolarStopTimer is 0
164+
*/
165+
void test_setmode_clears_timer(void) {
166+
setup_charging_3p();
167+
ctx.Mode = MODE_SOLAR;
168+
ctx.SolarStopTimer = 300;
169+
170+
/* Simulate what setMode(Smart) does: */
171+
ctx.Mode = MODE_SMART;
172+
ctx.SolarStopTimer = 0; /* setSolarStopTimer(0) */
173+
evse_clear_error_flags(&ctx, LESS_6A);
174+
175+
TEST_ASSERT_EQUAL_INT(0, ctx.SolarStopTimer);
176+
TEST_ASSERT_FALSE(ctx.ErrorFlags & LESS_6A);
177+
}
178+
179+
/* ================================================================
180+
* GROUP 3: Mode-dependent regulation behavior
181+
*
182+
* After a mode switch, the state machine must use the new mode for
183+
* current regulation. These tests verify that evse_calc_balanced_current
184+
* respects the Mode field consistently.
185+
* ================================================================ */
186+
187+
/*
188+
* @feature Mode Synchronization
189+
* @req REQ-MODE-SYNC-007
190+
* @scenario Smart→Solar mid-charge: regulation switches to solar algorithm
191+
* @given EVSE charging in Smart mode with mains headroom available
192+
* @when Mode is changed to Solar and evse_calc_balanced_current is called
193+
* @then Solar fine regulation is applied (IsetBalanced changes differently)
194+
*/
195+
void test_mid_charge_smart_to_solar(void) {
196+
setup_charging_3p();
197+
ctx.Mode = MODE_SMART;
198+
ctx.MainsMeterImeasured = 100;
199+
ctx.Isum = 100;
200+
ctx.IsetBalanced = 150;
201+
ctx.IsetBalanced_ema = 150;
202+
ctx.StartCurrent = 4;
203+
ctx.StopTime = 10;
204+
ctx.ImportCurrent = 0;
205+
ctx.SolarFineDeadBand = SOLAR_FINE_DEADBAND_DEFAULT;
206+
207+
/* Run one cycle in Smart mode */
208+
evse_calc_balanced_current(&ctx, 0);
209+
int32_t smart_iset = ctx.IsetBalanced;
210+
211+
/* Switch to Solar mode (simulating setMode side effects) */
212+
ctx.Mode = MODE_SOLAR;
213+
ctx.IsetBalanced = 150;
214+
ctx.IsetBalanced_ema = 150;
215+
216+
/* Run one cycle in Solar mode with same grid conditions */
217+
evse_calc_balanced_current(&ctx, 0);
218+
int32_t solar_iset = ctx.IsetBalanced;
219+
220+
/* Solar regulation should produce different result than Smart
221+
* (solar decreases more aggressively when importing) */
222+
TEST_ASSERT_TRUE(smart_iset != solar_iset);
223+
}
224+
225+
/*
226+
* @feature Mode Synchronization
227+
* @req REQ-MODE-SYNC-008
228+
* @scenario Solar→Normal mid-charge: all EVSEs get full current
229+
* @given Master with 2 EVSEs in Solar mode with shortage
230+
* @when Mode is changed to Normal
231+
* @then Both EVSEs get full current (Normal ignores solar/mains constraints)
232+
*/
233+
void test_mid_charge_solar_to_normal(void) {
234+
evse_init(&ctx, NULL);
235+
ctx.AccessStatus = ON;
236+
ctx.Mode = MODE_SOLAR;
237+
ctx.LoadBl = 1;
238+
ctx.MaxCurrent = 16;
239+
ctx.MaxCapacity = 16;
240+
ctx.MinCurrent = 6;
241+
ctx.MaxCircuit = 32;
242+
ctx.MaxMains = 25;
243+
ctx.ChargeCurrent = 160;
244+
ctx.MainsMeterType = 1;
245+
ctx.phasesLastUpdateFlag = true;
246+
ctx.Nr_Of_Phases_Charging = 3;
247+
ctx.StartCurrent = 4;
248+
ctx.StopTime = 10;
249+
ctx.ImportCurrent = 0;
250+
251+
for (int i = 0; i < 2; i++) {
252+
ctx.BalancedState[i] = STATE_C;
253+
ctx.BalancedMax[i] = 160;
254+
ctx.Balanced[i] = 60;
255+
ctx.Node[i].Online = 1;
256+
ctx.Node[i].IntTimer = 100;
257+
}
258+
ctx.State = STATE_C;
259+
ctx.MainsMeterImeasured = 200; /* Importing = no solar */
260+
ctx.Isum = 200;
261+
ctx.IsetBalanced = 60;
262+
ctx.IsetBalanced_ema = 60;
263+
264+
/* Solar mode: shortage, low distribution */
265+
evse_calc_balanced_current(&ctx, 0);
266+
int32_t solar_total = ctx.Balanced[0] + ctx.Balanced[1];
267+
268+
/* Switch to Normal (simulating setMode side effects) */
269+
ctx.Mode = MODE_NORMAL;
270+
ctx.SolarStopTimer = 0;
271+
ctx.phasesLastUpdateFlag = true;
272+
evse_clear_error_flags(&ctx, LESS_6A);
273+
274+
evse_calc_balanced_current(&ctx, 0);
275+
int32_t normal_total = ctx.Balanced[0] + ctx.Balanced[1];
276+
277+
/* Normal mode should give substantially more current */
278+
TEST_ASSERT_GREATER_THAN(solar_total, normal_total);
279+
TEST_ASSERT_GREATER_OR_EQUAL(60, ctx.Balanced[0]);
280+
TEST_ASSERT_GREATER_OR_EQUAL(60, ctx.Balanced[1]);
281+
}
282+
283+
/* ---- Main ---- */
284+
int main(void) {
285+
TEST_SUITE_BEGIN("Mode Synchronization");
286+
287+
/* Group 1: Phase switching */
288+
RUN_TEST(test_solar_off_forces_single_phase_in_solar);
289+
RUN_TEST(test_solar_off_allows_3p_in_smart);
290+
RUN_TEST(test_state_c_entry_solar_off_opens_c2);
291+
292+
/* Group 2: Error/timer clearing */
293+
RUN_TEST(test_clear_less6a_on_mode_switch);
294+
RUN_TEST(test_raw_mode_assign_leaves_timer_stale);
295+
RUN_TEST(test_setmode_clears_timer);
296+
297+
/* Group 3: Mode-dependent regulation */
298+
RUN_TEST(test_mid_charge_smart_to_solar);
299+
RUN_TEST(test_mid_charge_solar_to_normal);
300+
301+
TEST_SUITE_RESULTS();
302+
}

0 commit comments

Comments
 (0)