From c11c7f1a4a550f86f78e24b9b7c8c4ae1022e6df Mon Sep 17 00:00:00 2001 From: Timothy Yin Date: Sun, 19 Apr 2026 20:20:03 +0800 Subject: [PATCH] =?UTF-8?q?feat(firmware):=20=E7=A1=AC=E4=BB=B6=E8=81=94?= =?UTF-8?q?=E8=B0=83=E5=BC=95=E8=84=9A=E5=AE=9A=E4=B9=89&=E8=BF=9C?= =?UTF-8?q?=E7=A8=8B=E5=90=AF=E5=8A=A8=E5=85=85=E7=94=B5=E5=92=8C=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E5=A4=96=E8=AE=BE=E8=81=94=E5=8A=A8=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hardware/firmware/include/pins.h | 36 ++++ hardware/firmware/src/main.cpp | 345 ++++++++++++++++++++++++++++++- 2 files changed, 376 insertions(+), 5 deletions(-) create mode 100644 hardware/firmware/include/pins.h diff --git a/hardware/firmware/include/pins.h b/hardware/firmware/include/pins.h new file mode 100644 index 0000000..0787b9d --- /dev/null +++ b/hardware/firmware/include/pins.h @@ -0,0 +1,36 @@ +#ifndef HELIOS_PINS_H +#define HELIOS_PINS_H + +#include + +// Panel switches and LEDs +static const uint8_t PIN_CC1 = 39; // High-active switch +static const uint8_t PIN_CC2 = 36; // High-active switch +static const uint8_t PIN_LED1 = 32; // Low-active LED +static const uint8_t PIN_LED2 = 33; // Low-active LED + +// Key inputs +static const uint8_t PIN_KEY1 = 34; +static const uint8_t PIN_KEY2 = 35; + +// Relay outputs +static const uint8_t PIN_RELAY1 = 27; +static const uint8_t PIN_RELAY2 = 14; + +// I2C (OLED) +static const uint8_t PIN_OLED_SCL = 22; +static const uint8_t PIN_OLED_SDA = 21; + +// SPI (RC522) +static const uint8_t PIN_RC_MOSI = 23; +static const uint8_t PIN_RC_MISO = 19; +static const uint8_t PIN_RC_SCK = 18; +static const uint8_t PIN_RC_CS = 5; +static const uint8_t PIN_RC_RST = 4; + +// UART2 <-> IM1281C (U2) +static const uint8_t PIN_U2TXD = 26; +static const uint8_t PIN_U2RXD = 25; +static const uint32_t BAUD_IM1281C = 9600; + +#endif // HELIOS_PINS_H diff --git a/hardware/firmware/src/main.cpp b/hardware/firmware/src/main.cpp index 6770d0c..54b4273 100644 --- a/hardware/firmware/src/main.cpp +++ b/hardware/firmware/src/main.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -14,6 +15,7 @@ #include "esp_system.h" #include "config.h" +#include "pins.h" /* LED State Enum */ enum LEDState @@ -32,6 +34,27 @@ 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 +static const unsigned long CC_DEBOUNCE_MS = 30; +static const unsigned long AUTH_WINDOW_MS = 30000; + +static bool s_cc1_plugged = false; +static bool s_cc2_plugged = false; +static bool s_cc1_raw_last = false; +static bool s_cc2_raw_last = false; +static unsigned long s_cc1_last_change_ms = 0; +static unsigned long s_cc2_last_change_ms = 0; +static bool s_cc1_prev_plugged = false; +static bool s_cc2_prev_plugged = false; + +static bool s_key1_prev = false; +static bool s_key2_prev = false; + +static bool s_auth_in_progress = false; +static bool s_auth_ok = false; +static unsigned long s_auth_ok_at_ms = 0; +static String s_auth_id_tag; + +static bool s_remote_start_accepted = false; uint8_t mac[6]; char cpSerial[13]; @@ -62,6 +85,205 @@ Adafruit_SSD1306 display(128, 64, &Wire, -1); #define LED_COUNT 1 SmartLed leds(LED_WS2812B, LED_COUNT, LED_PIN, 0, DoubleBuffer); +MFRC522 rfid(PIN_RC_CS, PIN_RC_RST); + +static bool isConnectorPlugged(unsigned int connectorId) +{ + if (connectorId == 1) + return s_cc1_plugged; + if (connectorId == 2) + return s_cc2_plugged; + return false; +} + +static bool isConnectorIdle(unsigned int connectorId) +{ + return !isTransactionActive(connectorId) && !isTransactionRunning(connectorId) && !getTransaction(connectorId); +} + +static bool isConnectorStartReady(unsigned int connectorId) +{ + return connectorId >= 1 && connectorId <= 2 && isOperative(connectorId) && isConnectorIdle(connectorId) && isConnectorPlugged(connectorId); +} + +static void updateConnectorPluggedState() +{ + unsigned long now = millis(); + + const bool cc1_raw = (digitalRead(PIN_CC1) == HIGH); + if (cc1_raw != s_cc1_raw_last) + { + s_cc1_raw_last = cc1_raw; + s_cc1_last_change_ms = now; + } + if ((now - s_cc1_last_change_ms) >= CC_DEBOUNCE_MS) + { + s_cc1_plugged = s_cc1_raw_last; + } + + const bool cc2_raw = (digitalRead(PIN_CC2) == HIGH); + if (cc2_raw != s_cc2_raw_last) + { + s_cc2_raw_last = cc2_raw; + s_cc2_last_change_ms = now; + } + if ((now - s_cc2_last_change_ms) >= CC_DEBOUNCE_MS) + { + s_cc2_plugged = s_cc2_raw_last; + } +} + +static void updatePanelLedsFromPlugState() +{ + // Reserved for startup sync; runtime LED behavior is tied to charging permission. + digitalWrite(PIN_LED1, HIGH); + digitalWrite(PIN_LED2, HIGH); +} + +static void updateChargeActuators() +{ + const bool chg1_on = ocppPermitsCharge(1); + const bool chg2_on = ocppPermitsCharge(2); + + // LEDs and relays are low-active + digitalWrite(PIN_LED1, chg1_on ? LOW : HIGH); + digitalWrite(PIN_LED2, chg2_on ? LOW : HIGH); + digitalWrite(PIN_RELAY1, chg1_on ? LOW : HIGH); + digitalWrite(PIN_RELAY2, chg2_on ? LOW : HIGH); +} + +static void stopIfUnplugged() +{ + if (s_cc1_prev_plugged && !s_cc1_plugged && (isTransactionActive(1) || isTransactionRunning(1))) + { + Serial.println("[main] Connector 1 unplugged. Stop transaction immediately."); + endTransaction(nullptr, "EVDisconnected", 1); + } + + if (s_cc2_prev_plugged && !s_cc2_plugged && (isTransactionActive(2) || isTransactionRunning(2))) + { + Serial.println("[main] Connector 2 unplugged. Stop transaction immediately."); + endTransaction(nullptr, "EVDisconnected", 2); + } + + s_cc1_prev_plugged = s_cc1_plugged; + s_cc2_prev_plugged = s_cc2_plugged; +} + +static bool authWindowValid() +{ + return s_auth_ok && (millis() - s_auth_ok_at_ms) <= AUTH_WINDOW_MS && s_auth_id_tag.length() > 0; +} + +static void requestAuthorizeByCard(const String &idTag) +{ + if (s_auth_in_progress) + { + return; + } + + s_auth_in_progress = true; + s_auth_ok = false; + s_auth_id_tag = ""; + + Serial.printf("[main] Authorize idTag: %s\n", idTag.c_str()); + authorize( + idTag.c_str(), + [idTag](JsonObject payload) + { + s_auth_in_progress = false; + const char *status = payload["idTagInfo"]["status"] | ""; + if (!strcmp(status, "Accepted")) + { + s_auth_ok = true; + s_auth_ok_at_ms = millis(); + s_auth_id_tag = idTag; + Serial.printf("[main] Authorize accepted for idTag %s\n", idTag.c_str()); + } + else + { + s_auth_ok = false; + s_auth_id_tag = ""; + Serial.printf("[main] Authorize rejected, status=%s\n", status); + } + }, + []() + { + s_auth_in_progress = false; + s_auth_ok = false; + s_auth_id_tag = ""; + Serial.println("[main] Authorize aborted"); + }); +} + +static void pollRfidCard() +{ + if (!rfid.PICC_IsNewCardPresent() || !rfid.PICC_ReadCardSerial()) + { + return; + } + + String idTag; + for (byte i = 0; i < rfid.uid.size; i++) + { + if (rfid.uid.uidByte[i] < 0x10) + { + idTag += '0'; + } + idTag += String(rfid.uid.uidByte[i], HEX); + } + idTag.toUpperCase(); + + rfid.PICC_HaltA(); + rfid.PCD_StopCrypto1(); + + requestAuthorizeByCard(idTag); +} + +static void tryStartByKey(unsigned int connectorId) +{ + if (!authWindowValid()) + { + Serial.println("[main] No valid authorization window. Swipe card first."); + return; + } + + if (!isConnectorStartReady(connectorId)) + { + Serial.printf("[main] Connector %u not ready for start (needs idle + operative + plugged).\n", connectorId); + return; + } + + auto tx = beginTransaction_authorized(s_auth_id_tag.c_str(), nullptr, connectorId); + if (tx) + { + Serial.printf("[main] Local start accepted on connector %u for idTag %s\n", connectorId, s_auth_id_tag.c_str()); + s_auth_ok = false; + s_auth_id_tag = ""; + } + else + { + Serial.printf("[main] Local start failed on connector %u\n", connectorId); + } +} + +static void pollStartKeys() +{ + const bool key1 = (digitalRead(PIN_KEY1) == HIGH); + const bool key2 = (digitalRead(PIN_KEY2) == HIGH); + + if (key1 && !s_key1_prev) + { + tryStartByKey(1); + } + if (key2 && !s_key2_prev) + { + tryStartByKey(2); + } + + s_key1_prev = key1; + s_key2_prev = key2; +} /* LED Control Functions */ void updateLED() @@ -175,11 +397,38 @@ void setup() s_blink_last_time = 0; s_blink_on = false; + // Initialize CC switches (input) and panel LEDs (low-active output) + pinMode(PIN_CC1, INPUT); + pinMode(PIN_CC2, INPUT); + pinMode(PIN_KEY1, INPUT); + pinMode(PIN_KEY2, INPUT); + pinMode(PIN_LED1, OUTPUT); + pinMode(PIN_LED2, OUTPUT); + pinMode(PIN_RELAY1, OUTPUT); + pinMode(PIN_RELAY2, OUTPUT); + digitalWrite(PIN_LED1, HIGH); // Off by default (low-active) + digitalWrite(PIN_LED2, HIGH); // Off by default (low-active) + digitalWrite(PIN_RELAY1, HIGH); // Off by default (low-active) + digitalWrite(PIN_RELAY2, HIGH); // Off by default (low-active) + updateConnectorPluggedState(); + updatePanelLedsFromPlugState(); + leds[0] = Rgb{255, 255, 0}; leds.show(); - // Initialize IIC OLED - Wire.begin(4, 15); + // Initialize I2C for OLED (from schematic pin map) + Wire.begin(PIN_OLED_SDA, PIN_OLED_SCL); + + // Initialize UART2 for IM1281C (U2) + Serial2.begin(BAUD_IM1281C, SERIAL_8N1, PIN_U2RXD, PIN_U2TXD); + + // Initialize SPI bus for RC522 + SPI.begin(PIN_RC_SCK, PIN_RC_MISO, PIN_RC_MOSI, PIN_RC_CS); + pinMode(PIN_RC_CS, OUTPUT); + digitalWrite(PIN_RC_CS, HIGH); + pinMode(PIN_RC_RST, OUTPUT); + digitalWrite(PIN_RC_RST, HIGH); + rfid.PCD_Init(); // Load configuration from Preferences Preferences preferences; @@ -410,6 +659,80 @@ void setup() 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)); + // Expose both physical connectors to CSMS and feed live plug-state from CC switches. + // connectorId 1 <-> CC1, connectorId 2 <-> CC2 + setConnectorPluggedInput([]() + { return s_cc1_plugged; }, + 1); + setConnectorPluggedInput([]() + { return s_cc2_plugged; }, + 2); + + // Occupied state drives StatusNotification (Available <-> Preparing/Finishing) + // to report plug-in / unplug events even without an active transaction. + setOccupiedInput([]() + { return s_cc1_plugged; }, + 1); + setOccupiedInput([]() + { return s_cc2_plugged; }, + 2); + + // Custom RemoteStartTransaction policy: + // accept only when target connector is idle + operative + plugged. + setRequestHandler( + "RemoteStartTransaction", + [](JsonObject payload) + { + s_remote_start_accepted = false; + + const char *idTag = payload["idTag"] | ""; + if (!idTag || !*idTag) + { + return; + } + + int reqConnectorId = payload["connectorId"] | -1; + unsigned int targetConnector = 0; + + if (reqConnectorId >= 1 && reqConnectorId <= 2) + { + if (isConnectorStartReady((unsigned int)reqConnectorId)) + { + targetConnector = (unsigned int)reqConnectorId; + } + } + else + { + for (unsigned int cid = 1; cid <= 2; cid++) + { + if (isConnectorStartReady(cid)) + { + targetConnector = cid; + break; + } + } + } + + if (targetConnector == 0) + { + return; + } + + auto tx = beginTransaction_authorized(idTag, nullptr, targetConnector); + s_remote_start_accepted = (tx != nullptr); + if (s_remote_start_accepted) + { + Serial.printf("[main] Remote start accepted on connector %u\n", targetConnector); + } + }, + []() -> std::unique_ptr + { + auto doc = std::unique_ptr(new MicroOcpp::JsonDoc(JSON_OBJECT_SIZE(1))); + JsonObject payload = doc->to(); + payload["status"] = s_remote_start_accepted ? "Accepted" : "Rejected"; + return doc; + }); + // For development/recovery: Set up BOOT button (GPIO 0) pinMode(0, INPUT_PULLUP); @@ -417,16 +740,27 @@ void setup() setOnSendConf("RemoteStopTransaction", [](JsonObject payload) { if (!strcmp(payload["status"], "Rejected")) { - Serial.println("[main] MicroOcpp rejected RemoteStopTransaction! Force overriding and stopping charging..."); - endTransaction(nullptr, "Remote", 1); + unsigned int connectorId = payload["connectorId"] | 1; + if (connectorId < 1 || connectorId > 2) + { + connectorId = 1; + } + Serial.printf("[main] MicroOcpp rejected RemoteStopTransaction on connector %u. Force overriding and stopping charging...\n", connectorId); + endTransaction(nullptr, "Remote", connectorId); } }); } } void loop() { + updateConnectorPluggedState(); + stopIfUnplugged(); + pollRfidCard(); + pollStartKeys(); + mg_mgr_poll(&mgr, 10); mocpp_loop(); + updateChargeActuators(); // Handle BOOT button (GPIO 0) interactions for recovery bool is_btn_pressed = (digitalRead(0) == LOW); @@ -485,8 +819,9 @@ void loop() } else if (held_time >= 3000) { - Serial.println("BOOT button held for > 3s! Forcefully ending dangling transaction..."); + Serial.println("BOOT button held for > 3s! Forcefully ending dangling transactions on connector 1 and 2..."); endTransaction(nullptr, "PowerLoss", 1); + endTransaction(nullptr, "PowerLoss", 2); } boot_was_pressed = false; // Temporarily set to init so the logic below restores the actual network state accurately