#include #include #include #include #include #include #include #include #include #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( )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(const_cast(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(""), 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); }