Files
helios-evcs/hardware/firmware/src/main.cpp

514 lines
13 KiB
C++

#include <Arduino.h>
#include <WiFiManager.h>
#include <Preferences.h>
#include <string.h>
#include <MicroOcpp.h>
#include <MicroOcppMongooseClient.h>
#include <MicroOcpp/Core/Context.h>
#include <SmartLeds.h>
#include <MFRC522.h>
#include "esp_system.h"
#include "config.h"
/* LED State Enum */
enum LEDState
{
LED_INITIALIZING, // Blue blinking - Initialization and WiFi connecting
LED_WIFI_CONNECTED, // Blue solid - WiFi connected, connecting to OCPP server
LED_OCPP_CONNECTED, // Green solid - Successfully connected to OCPP server
LED_ERROR, // Red solid - Error state
LED_RESET_TX, // Yellow solid - 3s BOOT button hold (Ready to clear transaction)
LED_FACTORY_RESET // Magenta fast blink - 7s BOOT button hold (Ready to factory reset)
};
static int s_retry_num = 0;
static volatile bool s_ocpp_connected = false;
static volatile LEDState s_led_state = LED_INITIALIZING;
static volatile unsigned long s_blink_last_time = 0;
static volatile bool s_blink_on = false;
static const unsigned long BLINK_INTERVAL = 200; // 200ms blink interval
uint8_t mac[6];
char cpSerial[13];
// OCPP Configuration Variables
char ocpp_backend[128];
char cp_identifier[64];
char ocpp_password[64];
bool shouldSaveConfig = false;
// callback notifying us of the need to save config
void saveConfigCallback()
{
Serial.println("Should save config");
shouldSaveConfig = true;
}
struct mg_mgr mgr;
/**
* WS2812B LED Pin
* - GPIO 17 - RYMCU ESP32-DevKitC
* - GPIO 16 - YD-ESP32-A
*/
#define LED_PIN 17
#define LED_COUNT 1
SmartLed leds(LED_WS2812B, LED_COUNT, LED_PIN, 0, DoubleBuffer);
/* LED Control Functions */
void updateLED()
{
unsigned long current_time = millis();
switch (s_led_state)
{
case LED_INITIALIZING:
// Blue blinking during initialization
if (current_time - s_blink_last_time >= BLINK_INTERVAL)
{
s_blink_last_time = current_time;
s_blink_on = !s_blink_on;
if (s_blink_on)
{
leds[0] = Rgb{0, 0, 255}; // Blue on
}
else
{
leds[0] = Rgb{0, 0, 0}; // Off
}
leds.show();
}
break;
case LED_WIFI_CONNECTED:
// Blue solid - WiFi connected, OCPP connecting
leds[0] = Rgb{0, 0, 255}; // Blue solid
leds.show();
break;
case LED_OCPP_CONNECTED:
// Green solid - OCPP connected
leds[0] = Rgb{0, 255, 0}; // Green solid
leds.show();
break;
case LED_ERROR:
// Red solid - Error state
leds[0] = Rgb{255, 0, 0}; // Red solid
leds.show();
break;
case LED_RESET_TX:
// Yellow fast blink - Ready to clear transaction
if (current_time - s_blink_last_time >= 100)
{
s_blink_last_time = current_time;
s_blink_on = !s_blink_on;
if (s_blink_on)
leds[0] = Rgb{150, 150, 0}; // Yellow
else
leds[0] = Rgb{0, 0, 0};
leds.show();
}
break;
case LED_FACTORY_RESET:
// Magenta fast blink - Ready to factory reset
if (current_time - s_blink_last_time >= 100)
{
s_blink_last_time = current_time;
s_blink_on = !s_blink_on;
if (s_blink_on)
leds[0] = Rgb{255, 0, 255}; // Magenta
else
leds[0] = Rgb{0, 0, 0};
leds.show();
}
break;
}
}
void setup()
{
// Get MAC address and set as Charge Point Serial Number
esp_efuse_mac_get_default(mac);
snprintf(cpSerial, sizeof(cpSerial),
"%02X%02X%02X%02X%02X%02X",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
if (strlen(CFG_CP_IDENTIFIER) > 0)
{
strncpy(cp_identifier, CFG_CP_IDENTIFIER, sizeof(cp_identifier) - 1);
cp_identifier[sizeof(cp_identifier) - 1] = '\0';
}
else
{
// Auto-generate Charge Point Identifier based on MAC (e.g. HLCP_A1B2C3)
snprintf(cp_identifier, sizeof(cp_identifier), "HLCP_%s", cpSerial + 6);
}
// reset LED
leds[0] = Rgb{0, 0, 0};
leds.show();
// initialize Serial
Serial.begin(115200);
delay(1000);
Serial.printf("\n\n%s(%s) made by %s\n", CFG_CP_MODAL, cpSerial, CFG_CP_VENDOR);
Serial.printf("Charge Point Identifier: %s\n", cp_identifier);
Serial.println("Initializing firmware...\n");
// Initialize LED
s_led_state = LED_INITIALIZING;
s_blink_last_time = 0;
s_blink_on = false;
leds[0] = Rgb{255, 255, 0};
leds.show();
// Load configuration from Preferences
Preferences preferences;
preferences.begin("ocpp-config", false);
String b = preferences.getString("backend", CFG_OCPP_BACKEND);
String p = preferences.getString("ocpp_password", CFG_OCPP_PASSWORD ? CFG_OCPP_PASSWORD : "");
Serial.printf("\n[OCPP] Loaded Backend URL: %s\n", b.c_str());
Serial.printf("[OCPP] Loaded Password length: %d\n", p.length());
strncpy(ocpp_backend, b.c_str(), sizeof(ocpp_backend) - 1);
ocpp_backend[sizeof(ocpp_backend) - 1] = '\0';
strncpy(ocpp_password, p.c_str(), sizeof(ocpp_password) - 1);
ocpp_password[sizeof(ocpp_password) - 1] = '\0';
WiFiManager wm;
wm.setSaveConfigCallback(saveConfigCallback);
wm.setSaveParamsCallback(saveConfigCallback);
wm.setParamsPage(true);
// Use autocomplete=off to prevent browsers from autofilling old URLs after a reset
WiFiManagerParameter custom_ocpp_backend("backend", "OCPP Backend URL", ocpp_backend, 128, "autocomplete=\"off\"");
WiFiManagerParameter custom_ocpp_password("ocpp_password", "OCPP Basic AuthKey", ocpp_password, 64, "autocomplete=\"off\" type=\"password\"");
wm.addParameter(&custom_ocpp_backend);
wm.addParameter(&custom_ocpp_password);
const char *customHeadElement = R"rawliteral(
<style>
:root {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: #1f2a37;
--primarycolor: #2563eb;
}
body,
body.invert {
margin: 0;
background: #f4f6fb;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 16px;
color: #1f2a37;
}
.wrap {
width: min(360px, 100%);
background: #fff;
border-radius: 16px;
padding: 24px;
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);
text-align: left;
}
h1,
h2,
h3,
h4 {
color: #111827;
margin-top: 0;
text-align: center;
}
label {
display: block;
margin-bottom: 6px;
font-weight: 600;
}
input,
select,
button {
border-radius: 10px;
width: 100%;
font-size: 1rem;
box-sizing: border-box;
}
input[type="text"],
input[type="password"],
input[type="number"],
input[type="email"],
select {
padding: 12px;
margin: 0 0 16px 0;
border: 1px solid #d0d7e2;
background: #f9fafc;
transition: border 0.2s, box-shadow 0.2s, background 0.2s;
}
input:focus,
select:focus {
outline: none;
border-color: #2563eb;
background: #fff;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
}
button,
input[type="submit"],
button.D {
padding: 12px;
border: none;
background: #2563eb;
color: #fff;
font-weight: 600;
cursor: pointer;
transition: background 0.2s, transform 0.1s;
}
button.D {
background: #dc2626;
}
button:hover,
input[type="submit"]:hover {
background: #1d4ed8;
}
button:active,
input[type="submit"]:active {
transform: scale(0.99);
}
.msg {
border-radius: 12px;
border: 1px solid #e5e7eb;
background: #f9fafb;
padding: 16px;
color: #374151;
}
.msg.P {
border-color: #2563eb;
background: rgba(37, 99, 235, 0.08);
color: #1f2a37;
}
.msg.S {
border-color: #16a34a;
background: rgba(22, 163, 74, 0.08);
}
.msg.D {
border-color: #dc2626;
background: rgba(220, 38, 38, 0.08);
}
.q {
display: none;
}
a {
color: #2563eb;
font-weight: 600;
}
hr {
border: none;
height: 1px;
background: #e5e7eb;
margin: 24px 0;
}
</style>
)rawliteral";
wm.setCustomHeadElement(customHeadElement);
bool autoConnectRet = wm.autoConnect(cp_identifier, cpSerial);
if (shouldSaveConfig)
{
strncpy(ocpp_backend, custom_ocpp_backend.getValue(), sizeof(ocpp_backend) - 1);
ocpp_backend[sizeof(ocpp_backend) - 1] = '\0';
strncpy(ocpp_password, custom_ocpp_password.getValue(), sizeof(ocpp_password) - 1);
ocpp_password[sizeof(ocpp_password) - 1] = '\0';
preferences.putString("backend", ocpp_backend);
preferences.putString("ocpp_password", ocpp_password);
Serial.println("Saved new OCPP config to Preferences");
}
preferences.end();
if (!autoConnectRet)
{
Serial.println("Failed to connect and hit timeout");
s_led_state = LED_ERROR;
}
else
{
Serial.println("WiFi connected successfully");
s_led_state = LED_WIFI_CONNECTED;
mg_mgr_init(&mgr);
const char *basic_auth_password = (strlen(ocpp_password) > 0) ? ocpp_password : nullptr;
unsigned char *basic_auth_password_bytes = nullptr;
size_t basic_auth_password_len = 0;
if (basic_auth_password)
{
basic_auth_password_bytes = reinterpret_cast<unsigned char *>(const_cast<char *>(basic_auth_password));
basic_auth_password_len = strlen(basic_auth_password);
}
MicroOcpp::MOcppMongooseClient *client = new MicroOcpp::MOcppMongooseClient(
&mgr,
ocpp_backend,
cp_identifier,
nullptr,
0,
"",
MicroOcpp::makeDefaultFilesystemAdapter(MicroOcpp::FilesystemOpt::Use_Mount_FormatOnFail),
MicroOcpp::ProtocolVersion(1, 6));
// Preferences and firmware config are the source of truth. Override any stale
// values cached in MicroOcpp's ws-conn storage before the first reconnect cycle.
client->setBackendUrl(ocpp_backend);
client->setChargeBoxId(cp_identifier);
if (basic_auth_password_bytes)
{
client->setAuthKey(basic_auth_password_bytes, basic_auth_password_len);
}
else
{
client->setAuthKey(reinterpret_cast<const unsigned char *>(""), 0);
}
client->reloadConfigs();
mocpp_initialize(*client, ChargerCredentials(CFG_CP_MODAL, CFG_CP_VENDOR, CFG_CP_FW_VERSION, cpSerial, nullptr, nullptr, CFG_CB_SERIAL, nullptr, nullptr), MicroOcpp::makeDefaultFilesystemAdapter(MicroOcpp::FilesystemOpt::Use_Mount_FormatOnFail));
// For development/recovery: Set up BOOT button (GPIO 0)
pinMode(0, INPUT_PULLUP);
// Forcefully accept rejected RemoteStopTransaction (if hardware goes out of sync with CSMS)
setOnSendConf("RemoteStopTransaction", [](JsonObject payload)
{
if (!strcmp(payload["status"], "Rejected")) {
Serial.println("[main] MicroOcpp rejected RemoteStopTransaction! Force overriding and stopping charging...");
endTransaction(nullptr, "Remote", 1);
} });
}
}
void loop()
{
mg_mgr_poll(&mgr, 10);
mocpp_loop();
// Handle BOOT button (GPIO 0) interactions for recovery
bool is_btn_pressed = (digitalRead(0) == LOW);
static unsigned long boot_press_time = 0;
static bool boot_was_pressed = false;
if (is_btn_pressed)
{
if (!boot_was_pressed)
{
boot_was_pressed = true;
boot_press_time = millis();
}
unsigned long held_time = millis() - boot_press_time;
if (held_time >= 7000)
{
s_led_state = LED_FACTORY_RESET;
}
else if (held_time >= 3000)
{
s_led_state = LED_RESET_TX;
}
}
else
{
if (boot_was_pressed)
{
unsigned long held_time = millis() - boot_press_time;
if (held_time >= 7000)
{
Serial.println("BOOT button held for > 7s! Clearing WiFi and OCPP settings, then restarting...");
// Clear WiFi completely
WiFi.disconnect(true, true);
WiFiManager wm;
wm.resetSettings();
// Clear Preferences explicitely
Preferences preferences;
preferences.begin("ocpp-config", false);
preferences.remove("backend");
preferences.remove("ocpp_password");
preferences.clear();
preferences.end();
Serial.println("NVS ocpp-config cleared.");
// Clear MicroOcpp FS configs (this removes MO's cached URL)
auto fs = MicroOcpp::makeDefaultFilesystemAdapter(MicroOcpp::FilesystemOpt::Use_Mount_FormatOnFail);
fs->remove(MO_WSCONN_FN);
Serial.println("MicroOcpp config cache cleared.");
// Give time for serial to print and NVS to sync
delay(1000);
ESP.restart();
}
else if (held_time >= 3000)
{
Serial.println("BOOT button held for > 3s! Forcefully ending dangling transaction...");
endTransaction(nullptr, "PowerLoss", 1);
}
boot_was_pressed = false;
// Temporarily set to init so the logic below restores the actual network state accurately
s_led_state = LED_INITIALIZING;
}
}
// Only update default LED states if button is not overriding them
if (!is_btn_pressed)
{
auto ctx = getOcppContext();
if (ctx && ctx->getConnection().isConnected())
{
if (s_led_state != LED_OCPP_CONNECTED)
{
s_led_state = LED_OCPP_CONNECTED;
}
}
else
{
if (s_led_state != LED_WIFI_CONNECTED)
{
s_led_state = LED_WIFI_CONNECTED;
}
}
}
updateLED();
delay(10);
}