forked from matth-x/MicroOcpp
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathBoot.cpp
More file actions
477 lines (352 loc) · 17.3 KB
/
Boot.cpp
File metadata and controls
477 lines (352 loc) · 17.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
// matth-x/MicroOcpp
// Copyright Matthias Akstaller 2019 - 2024
// MIT License
#include <MicroOcpp.h>
#include <MicroOcpp/Core/Connection.h>
#include <MicroOcpp/Core/Context.h>
#include <MicroOcpp/Model/Model.h>
#include <MicroOcpp/Core/Configuration.h>
#include <MicroOcpp/Core/Request.h>
#include <MicroOcpp/Core/FilesystemUtils.h>
#include <MicroOcpp/Operations/BootNotification.h>
#include <MicroOcpp/Operations/StatusNotification.h>
#include <MicroOcpp/Operations/CustomOperation.h>
#include <MicroOcpp/Model/Boot/BootService.h>
#include <MicroOcpp/Model/Transactions/TransactionStore.h>
#include <MicroOcpp/Debug.h>
#include <catch2/catch.hpp>
#include "./helpers/testHelper.h"
#define CHARGEPOINTMODEL "Test model"
#define CHARGEPOINTVENDOR "Test vendor"
#define BASE_TIME "2023-01-01T00:00:00.000Z"
#define GET_CONFIGURATION "[2,\"msgId01\",\"GetConfiguration\",{\"key\":[]}]"
#define TRIGGER_MESSAGE "[2,\"msgId02\",\"TriggerMessage\",{\"requestedMessage\":\"TriggeredOperation\"}]"
using namespace MicroOcpp;
//dummy operation type to test TriggerMessage
class TriggeredOperation : public Operation {
private:
bool& checkExecuted;
public:
TriggeredOperation(bool& checkExecuted) : checkExecuted(checkExecuted) { }
const char* getOperationType() override {return "TriggeredOperation";}
std::unique_ptr<JsonDoc> createReq() override {
checkExecuted = true;
return createEmptyDocument();
}
void processConf(JsonObject) override {}
void processReq(JsonObject) override {}
std::unique_ptr<JsonDoc> createConf() override {return createEmptyDocument();}
};
TEST_CASE( "Boot Behavior" ) {
printf("\nRun %s\n", "Boot Behavior");
//clean state
auto filesystem = makeDefaultFilesystemAdapter(FilesystemOpt::Use_Mount_FormatOnFail);
FilesystemUtils::remove_if(filesystem, [] (const char*) {return true;});
//initialize Context with dummy socket
LoopbackConnection loopback;
mocpp_initialize(loopback, ChargerCredentials(CHARGEPOINTMODEL, CHARGEPOINTVENDOR), filesystem);
mocpp_set_timer(custom_timer_cb);
SECTION("BootNotification - Accepted") {
bool checkProcessed = false;
getOcppContext()->getOperationRegistry().registerOperation("BootNotification",
[&checkProcessed] () {
return new Ocpp16::CustomOperation("BootNotification",
[ &checkProcessed] (JsonObject payload) {
//process req
checkProcessed = true;
REQUIRE( !strcmp(payload["chargePointModel"] | "_Undefined", CHARGEPOINTMODEL) );
REQUIRE( !strcmp(payload["chargePointVendor"] | "_Undefined", CHARGEPOINTVENDOR) );
},
[] () {
//create conf
auto conf = makeJsonDoc(UNIT_MEM_TAG, 1024);
(*conf)["currentTime"] = BASE_TIME;
(*conf)["interval"] = 3600;
(*conf)["status"] = "Accepted";
return conf;
});
});
loop();
REQUIRE(checkProcessed);
REQUIRE(getOcppContext()->getModel().getClock().now() >= MIN_TIME);
}
SECTION("BootNotification - Pending") {
MO_DBG_INFO("Queue messages before BootNotification to see if they come through");
loop(); //normal BootNotification run
REQUIRE( isOperative() ); //normal BN succeeded
loopback.setConnected( false );
beginTransaction_authorized("mIdTag");
loop();
endTransaction();
mocpp_deinitialize();
loopback.setConnected( true );
MO_DBG_INFO("Start charger again with queued transaction messages, also init non-tx-related msg, but now delay BN procedure");
mocpp_initialize(loopback, ChargerCredentials());
getOcppContext()->getOperationRegistry().registerOperation("BootNotification",
[] () {
return new Ocpp16::CustomOperation("BootNotification",
[] (JsonObject payload) {
//ignore req
},
[] () {
//create conf
auto conf = makeJsonDoc(UNIT_MEM_TAG, 1024);
(*conf)["currentTime"] = BASE_TIME;
(*conf)["interval"] = 3600;
(*conf)["status"] = "Pending";
return conf;
});
});
bool sentTxMsg = false;
getOcppContext()->getOperationRegistry().setOnRequest("StartTransaction",
[&sentTxMsg] (JsonObject) {
sentTxMsg = true;
});
getOcppContext()->getOperationRegistry().setOnRequest("StopTransaction",
[&sentTxMsg] (JsonObject) {
sentTxMsg = true;
});
bool checkProcessedHeartbeat = false;
auto heartbeat = makeRequest(new Ocpp16::CustomOperation(
"Heartbeat",
[] () {
//create req
return createEmptyDocument();},
[&checkProcessedHeartbeat] (JsonObject) {
//process conf
checkProcessedHeartbeat = true;
}));
heartbeat->setTimeout(0); //disable timeout and check if message will be sent later
getOcppContext()->initiateRequest(std::move(heartbeat));
bool sentNonTxMsg = false;
getOcppContext()->getOperationRegistry().setOnRequest("Heartbeat",
[&sentNonTxMsg] (JsonObject) {
sentNonTxMsg = true;
});
loop();
REQUIRE( !sentTxMsg );
REQUIRE( !sentNonTxMsg );
REQUIRE( !checkProcessedHeartbeat );
MO_DBG_INFO("Check if charger still responds to server-side messages and executes TriggerMessages");
bool reactedToServerMsg = false;
getOcppContext()->getOperationRegistry().setOnRequest("GetConfiguration",
[&reactedToServerMsg] (JsonObject) {
reactedToServerMsg = true;
});
loopback.sendTXT(GET_CONFIGURATION, sizeof(GET_CONFIGURATION) - 1);
loop();
REQUIRE( reactedToServerMsg );
bool executedTriggerMessage = false;
getOcppContext()->getOperationRegistry().registerOperation("TriggeredOperation",
[&executedTriggerMessage] () {return new TriggeredOperation(executedTriggerMessage);});
loopback.sendTXT(TRIGGER_MESSAGE, sizeof(TRIGGER_MESSAGE) - 1);
loop();
REQUIRE( executedTriggerMessage );
//other messages still didn't get through?
REQUIRE( !sentTxMsg );
REQUIRE( !sentNonTxMsg );
REQUIRE( !checkProcessedHeartbeat );
MO_DBG_INFO("Now, accept BN and check if all queued messages finally arrive");
getOcppContext()->getOperationRegistry().registerOperation("BootNotification",
[] () {
return new Ocpp16::CustomOperation("BootNotification",
[] (JsonObject payload) {
//ignore req
},
[] () {
//create conf
auto conf = makeJsonDoc(UNIT_MEM_TAG, 1024);
(*conf)["currentTime"] = BASE_TIME;
(*conf)["interval"] = 3600;
(*conf)["status"] = "Accepted";
return conf;
});
});
mtime += 3600 * 1000;
loop();
REQUIRE( sentTxMsg );
REQUIRE( sentNonTxMsg );
REQUIRE( checkProcessedHeartbeat );
}
SECTION("PreBoot transactions") {
declareConfiguration<bool>(MO_CONFIG_EXT_PREFIX "PreBootTransactions", true)->setBool(true);
declareConfiguration<bool>("AllowOfflineTxForUnknownId", true)->setBool(true);
unsigned int startTxCount = 0;
getOcppContext()->getOperationRegistry().setOnRequest("StartTransaction",
[&startTxCount] (JsonObject) {
startTxCount++;
});
//start one transaction in full offline mode
loopback.setConnected( false );
loop();
REQUIRE( getChargePointStatus() == ChargePointStatus_Available );
beginTransaction("mIdTag");
loop();
REQUIRE( getChargePointStatus() == ChargePointStatus_Charging );
endTransaction("mIdTag");
loop();
REQUIRE( getChargePointStatus() == ChargePointStatus_Available );
//start another transaction while BN is pending
getOcppContext()->getOperationRegistry().registerOperation("BootNotification",
[] () {
return new Ocpp16::CustomOperation("BootNotification",
[] (JsonObject payload) {
//ignore req
},
[] () {
//create conf
auto conf = makeJsonDoc(UNIT_MEM_TAG, 1024);
(*conf)["currentTime"] = BASE_TIME;
(*conf)["interval"] = 3600;
(*conf)["status"] = "Pending";
return conf;
});
});
loopback.setConnected( true );
loop();
REQUIRE( startTxCount == 0 );
beginTransaction("mIdTag2");
mtime += 20 * 1000;
loop();
REQUIRE( getChargePointStatus() == ChargePointStatus_Charging );
endTransaction();
loop();
REQUIRE( getChargePointStatus() == ChargePointStatus_Available );
REQUIRE( startTxCount == 0 );
//Now, accept BN and check again
getOcppContext()->getOperationRegistry().registerOperation("BootNotification",
[] () {
return new Ocpp16::CustomOperation("BootNotification",
[] (JsonObject payload) {
//ignore req
},
[] () {
//create conf
auto conf = makeJsonDoc(UNIT_MEM_TAG, 1024);
(*conf)["currentTime"] = BASE_TIME;
(*conf)["interval"] = 3600;
(*conf)["status"] = "Accepted";
return conf;
});
});
mtime += 3600 * 1000;
loop();
REQUIRE( startTxCount == 2 );
}
SECTION("Auto recovery") {
//start transaction which will persist a few boot cycles, but then will be wiped by auto recovery
loop();
beginTransaction("mIdTag");
loop();
REQUIRE( getChargePointStatus() == ChargePointStatus_Charging );
declareConfiguration<const char*>("keepConfigOverRecovery", "originalVal");
configuration_save();
mocpp_deinitialize();
//MO has 2 unexpected power cycles. Probably just back luck - keep the local state and configuration
//Increase the power cycle counter manually because it's not possible to interrupt the MO lifecycle during unit tests
BootStats bootstats;
BootService::loadBootStats(filesystem, bootstats);
bootstats.bootNr += 2;
BootService::storeBootStats(filesystem, bootstats);
mocpp_initialize(loopback, ChargerCredentials(), filesystem, /*enable auto recovery*/ true);
BootService::loadBootStats(filesystem, bootstats);
REQUIRE( bootstats.getBootFailureCount() == 2 + 1 ); //two boot failures have been measured, +1 because each power cycle is counted as potentially failing until reaching the long runtime barrier
loop();
REQUIRE( getChargePointStatus() == ChargePointStatus_Charging );
REQUIRE( !strcmp(declareConfiguration<const char*>("keepConfigOverRecovery", "otherVal")->getString(), "originalVal") );
//check that the power cycle counter has been updated properly after the controller has been running stable over a long time
mtime += MO_BOOTSTATS_LONGTIME_MS;
loop();
BootService::loadBootStats(filesystem, bootstats);
REQUIRE( bootstats.getBootFailureCount() == 0 );
mocpp_deinitialize();
//MO has 10 power cycles without running for at least 3 minutes and wipes the local state, but keeps the configuration
BootStats bootstats2;
BootService::loadBootStats(filesystem, bootstats2);
bootstats2.bootNr += 10;
BootService::storeBootStats(filesystem, bootstats2);
mocpp_initialize(loopback, ChargerCredentials(), filesystem, /*enable auto recovery*/ true);
REQUIRE( !strcmp(declareConfiguration<const char*>("keepConfigOverRecovery", "otherVal")->getString(), "originalVal") );
BootStats bootstats3;
BootService::loadBootStats(filesystem, bootstats3);
REQUIRE( bootstats3.getBootFailureCount() == 0 + 1 ); //failure count is reset, but +1 because each power cycle is counted as potentially failing until reaching the long runtime barrier
loop();
REQUIRE( getChargePointStatus() == ChargePointStatus_Available );
}
SECTION("Migration") {
//migration removes files from previous MO versions which were running on the controller. This includes the
//transaction cache, but configs are preserved
auto old_opstore = filesystem->open(MO_FILENAME_PREFIX "opstore.jsn", "w"); //the opstore has been removed in MO v1.2.0
old_opstore->write("example content", sizeof("example content") - 1);
old_opstore.reset(); //flushes the file
loop();
beginTransaction("mIdTag"); //tx store will also be removed
auto tx = getTransaction();
auto txNr = tx->getTxNr(); //remember this for later usage
tx.reset(); //reset this smart pointer
loop();
REQUIRE( getChargePointStatus() == ChargePointStatus_Charging );
endTransaction();
loop();
REQUIRE( getOcppContext()->getModel().getTransactionStore()->getTransaction(1, txNr) != nullptr ); //tx exists on flash
declareConfiguration<const char*>("keepConfigOverMigration", "originalVal"); //migration keeps configs
configuration_save();
mocpp_deinitialize();
//After a FW update, the tracked version number has changed
BootStats bootstats;
BootService::loadBootStats(filesystem, bootstats);
snprintf(bootstats.microOcppVersion, sizeof(bootstats.microOcppVersion), "oldFwVers");
BootService::storeBootStats(filesystem, bootstats);
mocpp_initialize(loopback, ChargerCredentials(), filesystem); //MO migrates here
size_t msize = 0;
REQUIRE( filesystem->stat(MO_FILENAME_PREFIX "opstore.jsn", &msize) != 0 ); //opstore has been removed
REQUIRE( getOcppContext()->getModel().getTransactionStore()->getTransaction(1, txNr) == nullptr ); //tx history entry has been removed
REQUIRE( !strcmp(declareConfiguration<const char*>("keepConfigOverMigration", "otherVal")->getString(), "originalVal") ); //config has been preserved
}
SECTION("Clean unused configs") {
declareConfiguration<const char*>("neverDeclaredInsideMO", "originalVal"); //unused configs will be cleared automatically after the controller has been running for a long time
configuration_save();
mocpp_deinitialize();
mocpp_initialize(loopback, ChargerCredentials(), filesystem); //all configs are loaded here, including the test config of this section
loop();
//unused configs will be cleared automatically after long time
mtime += MO_BOOTSTATS_LONGTIME_MS;
loop();
REQUIRE( !strcmp(declareConfiguration<const char*>("neverDeclaredInsideMO", "newVal")->getString(), "newVal") ); //config has been removed
}
SECTION("Boot with v201") {
mocpp_deinitialize();
mocpp_initialize(loopback, ChargerCredentials::v201(CHARGEPOINTMODEL, CHARGEPOINTVENDOR), filesystem, false, ProtocolVersion(2,0,1));
bool checkProcessed = false;
getOcppContext()->getOperationRegistry().registerOperation("BootNotification",
[&checkProcessed] () {
return new Ocpp16::CustomOperation("BootNotification",
[ &checkProcessed] (JsonObject payload) {
//process req
checkProcessed = true;
REQUIRE( !strcmp(payload["reason"] | "_Undefined", "PowerUp") );
REQUIRE( !strcmp(payload["chargingStation"]["model"] | "_Undefined", CHARGEPOINTMODEL) );
REQUIRE( !strcmp(payload["chargingStation"]["vendorName"] | "_Undefined", CHARGEPOINTVENDOR) );
},
[] () {
//create conf
auto conf = makeJsonDoc(UNIT_MEM_TAG, JSON_OBJECT_SIZE(3));
(*conf)["currentTime"] = BASE_TIME;
(*conf)["interval"] = 3600;
(*conf)["status"] = "Accepted";
return conf;
});
});
MO_MEM_RESET();
loop();
REQUIRE(checkProcessed);
REQUIRE(getOcppContext()->getModel().getClock().now() >= MIN_TIME);
MO_MEM_PRINT_STATS();
MO_MEM_RESET();
mtime += 3600 * 1000;
loop();
MO_DBG_INFO("Memory requirements UC G02:");
MO_MEM_PRINT_STATS();
}
mocpp_deinitialize();
}