diff --git a/hardware/firmware/src/main.cpp b/hardware/firmware/src/main.cpp index 3862498..fabe211 100644 --- a/hardware/firmware/src/main.cpp +++ b/hardware/firmware/src/main.cpp @@ -35,9 +35,18 @@ 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 const unsigned long IM1281C_POLL_INTERVAL_MS = 5000; +static const unsigned long OLED_REFRESH_INTERVAL_MS = 500; +static const unsigned long CARD_ID_DISPLAY_MS = 5000; +static const unsigned long WAIT_HINT_BLINK_MS = 300; +static const int CC_FILTER_MIN = 0; +static const int CC_FILTER_MAX = 100; +static const int CC_FILTER_ON_THRESHOLD = 75; +static const int CC_FILTER_OFF_THRESHOLD = 25; +static const int CC_FILTER_DELTA_UP = 6; +static const int CC_FILTER_DELTA_DOWN_IDLE = -8; +static const int CC_FILTER_DELTA_DOWN_ACTIVE = -20; static bool s_cc1_plugged = false; static bool s_cc2_plugged = false; @@ -47,15 +56,22 @@ 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_cc1_filter_inited = false; +static int s_cc1_filter_score = 0; +static bool s_cc2_filter_inited = false; +static int s_cc2_filter_score = 0; 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 String s_last_swipe_id; +static unsigned long s_last_swipe_at_ms = 0; static bool s_remote_start_accepted = false; static bool authWindowValid(); +static bool isConnectorIdle(unsigned int connectorId); static void clearAuthWait(const char *reason) { if (s_auth_ok || s_auth_id_tag.length() > 0) @@ -102,6 +118,90 @@ IM1281C im1281c; static IM1281CAData s_meter_a; static IM1281CBData s_meter_b; static bool s_meter_data_ready = false; +static bool s_oled_ready = false; + +static const char *ledStageText(LEDState state) +{ + switch (state) + { + case LED_INITIALIZING: + return "INIT"; + case LED_WIFI_CONNECTED: + return "WIFI"; + case LED_OCPP_CONNECTED: + return "OCPP"; + case LED_ERROR: + return "ERROR"; + case LED_RESET_TX: + return "RST-TX"; + case LED_FACTORY_RESET: + return "FACTORY"; + default: + return "UNKNOWN"; + } +} + +static const char *cpStatusShort(ChargePointStatus status) +{ + switch (status) + { + case ChargePointStatus_Available: + return "AVL"; + case ChargePointStatus_Preparing: + return "PRE"; + case ChargePointStatus_Charging: + return "CHG"; + case ChargePointStatus_SuspendedEVSE: + return "SEVSE"; + case ChargePointStatus_SuspendedEV: + return "SEV"; + case ChargePointStatus_Finishing: + return "FIN"; + case ChargePointStatus_Reserved: + return "RSV"; + case ChargePointStatus_Unavailable: + return "UNAV"; + case ChargePointStatus_Faulted: + return "FLT"; + default: + return "N/A"; + } +} + +static unsigned int getWaitHintConnectorId() +{ + if (!authWindowValid()) + { + return 0; + } + + const bool c1_active = isTransactionActive(1) || isTransactionRunning(1) || ocppPermitsCharge(1); + const bool c2_active = isTransactionActive(2) || isTransactionRunning(2) || ocppPermitsCharge(2); + + if (c1_active && !c2_active && isConnectorIdle(2)) + { + return 2; + } + + if (c2_active && !c1_active && isConnectorIdle(1)) + { + return 1; + } + + if (!s_cc1_plugged && !s_cc2_plugged) + { + if (isConnectorIdle(1) && isOperative(1)) + { + return 1; + } + if (isConnectorIdle(2) && isOperative(2)) + { + return 2; + } + } + + return 0; +} static int energyKwhToWh(float energyKwh) { @@ -118,6 +218,101 @@ static int energyKwhToWh(float energyKwh) return static_cast(energyWh + 0.5f); } +static void drawCenteredText(const String &text, int16_t y, uint8_t textSize) +{ + int16_t x1 = 0; + int16_t y1 = 0; + uint16_t w = 0; + uint16_t h = 0; + + display.setTextSize(textSize); + display.getTextBounds(text.c_str(), 0, y, &x1, &y1, &w, &h); + + int16_t x = (128 - static_cast(w)) / 2; + if (x < 0) + { + x = 0; + } + + display.setCursor(x, y); + display.print(text); +} + +static void refreshOled() +{ + if (!s_oled_ready) + { + return; + } + + static unsigned long s_last_refresh_ms = 0; + const unsigned long now = millis(); + if ((now - s_last_refresh_ms) < OLED_REFRESH_INTERVAL_MS) + { + return; + } + s_last_refresh_ms = now; + + const bool c1_chg = ocppPermitsCharge(1); + const bool c2_chg = ocppPermitsCharge(2); + const ChargePointStatus st1 = getChargePointStatus(1); + const ChargePointStatus st2 = getChargePointStatus(2); + const bool show_card = s_last_swipe_id.length() > 0 && (now - s_last_swipe_at_ms) <= CARD_ID_DISPLAY_MS; + + display.clearDisplay(); + display.setTextColor(SSD1306_WHITE); + + if (show_card) + { + String shownId = s_last_swipe_id; + if (shownId.length() > 12) + { + shownId = shownId.substring(shownId.length() - 12); + } + + // Card display mode: full-screen centered content + drawCenteredText(String("CARD"), 16, 1); + drawCenteredText(shownId, 34, 2); + display.display(); + return; + } + + display.setTextSize(1); + display.setCursor(0, 0); + display.printf("ST:%s AUTH:%s", ledStageText(s_led_state), authWindowValid() ? "WAIT" : "IDLE"); + + display.setCursor(0, 8); + display.printf("C1 P%d CH%d %s", s_cc1_plugged ? 1 : 0, c1_chg ? 1 : 0, cpStatusShort(st1)); + + display.setCursor(0, 16); + display.printf("C2 P%d CH%d %s", s_cc2_plugged ? 1 : 0, c2_chg ? 1 : 0, cpStatusShort(st2)); + + if (!s_meter_data_ready) + { + display.setCursor(0, 28); + display.print("Meter: waiting IM1281C"); + } + else + { + display.setCursor(0, 24); + display.printf("A U%.1f I%.2f", s_meter_a.voltage, s_meter_a.current); + + display.setCursor(0, 32); + display.printf("A P%.0f E%.3f", s_meter_a.power, s_meter_a.energy); + + display.setCursor(0, 40); + display.printf("B U%.1f I%.2f", s_meter_b.voltage, s_meter_b.current); + + display.setCursor(0, 48); + display.printf("B P%.0f E%.3f", s_meter_b.power, s_meter_b.energy); + + display.setCursor(0, 56); + display.printf("T%.1fC F%.1fHz", s_meter_a.temperature, s_meter_a.frequency); + } + + display.display(); +} + static bool isConnectorPlugged(unsigned int connectorId) { if (connectorId == 1) @@ -139,18 +334,45 @@ static bool isConnectorStartReady(unsigned int connectorId) static void updateConnectorPluggedState() { - unsigned long now = millis(); - const bool cc1_raw = (digitalRead(PIN_CC1) == HIGH); - if (cc1_raw != s_cc1_raw_last) + + if (!s_cc1_filter_inited) { - s_cc1_raw_last = cc1_raw; - s_cc1_last_change_ms = now; + s_cc1_filter_inited = true; + s_cc1_filter_score = cc1_raw ? CC_FILTER_MAX : CC_FILTER_MIN; + s_cc1_plugged = cc1_raw; } - if ((now - s_cc1_last_change_ms) >= CC_DEBOUNCE_MS) + + int cc1_delta = cc1_raw ? CC_FILTER_DELTA_UP : CC_FILTER_DELTA_DOWN_IDLE; + if ((isTransactionActive(1) || isTransactionRunning(1)) && !cc1_raw) + { + cc1_delta = CC_FILTER_DELTA_DOWN_ACTIVE; + } + + s_cc1_filter_score += cc1_delta; + if (s_cc1_filter_score > CC_FILTER_MAX) + { + s_cc1_filter_score = CC_FILTER_MAX; + } + else if (s_cc1_filter_score < CC_FILTER_MIN) + { + s_cc1_filter_score = CC_FILTER_MIN; + } + + bool cc1_candidate = s_cc1_plugged; + if (s_cc1_filter_score >= CC_FILTER_ON_THRESHOLD) + { + cc1_candidate = true; + } + else if (s_cc1_filter_score <= CC_FILTER_OFF_THRESHOLD) + { + cc1_candidate = false; + } + + if (cc1_candidate != s_cc1_plugged) { bool cc1_plugged_old = s_cc1_plugged; - s_cc1_plugged = s_cc1_raw_last; + s_cc1_plugged = cc1_candidate; // If connector1 just connected and auth window is valid, try to start if (!cc1_plugged_old && s_cc1_plugged && s_auth_ok && (millis() - s_auth_ok_at_ms) <= AUTH_WINDOW_MS && s_auth_id_tag.length() > 0 && isConnectorIdle(1)) @@ -166,15 +388,44 @@ static void updateConnectorPluggedState() } const bool cc2_raw = (digitalRead(PIN_CC2) == HIGH); - if (cc2_raw != s_cc2_raw_last) + + if (!s_cc2_filter_inited) { - s_cc2_raw_last = cc2_raw; - s_cc2_last_change_ms = now; + s_cc2_filter_inited = true; + s_cc2_filter_score = cc2_raw ? CC_FILTER_MAX : CC_FILTER_MIN; + s_cc2_plugged = cc2_raw; } - if ((now - s_cc2_last_change_ms) >= CC_DEBOUNCE_MS) + + int delta = cc2_raw ? CC_FILTER_DELTA_UP : CC_FILTER_DELTA_DOWN_IDLE; + if ((isTransactionActive(2) || isTransactionRunning(2)) && !cc2_raw) + { + delta = CC_FILTER_DELTA_DOWN_ACTIVE; + } + + s_cc2_filter_score += delta; + if (s_cc2_filter_score > CC_FILTER_MAX) + { + s_cc2_filter_score = CC_FILTER_MAX; + } + else if (s_cc2_filter_score < CC_FILTER_MIN) + { + s_cc2_filter_score = CC_FILTER_MIN; + } + + bool cc2_candidate = s_cc2_plugged; + if (s_cc2_filter_score >= CC_FILTER_ON_THRESHOLD) + { + cc2_candidate = true; + } + else if (s_cc2_filter_score <= CC_FILTER_OFF_THRESHOLD) + { + cc2_candidate = false; + } + + if (cc2_candidate != s_cc2_plugged) { bool cc2_plugged_old = s_cc2_plugged; - s_cc2_plugged = s_cc2_raw_last; + s_cc2_plugged = cc2_candidate; // If connector2 just connected and auth window is valid, try to start if (!cc2_plugged_old && s_cc2_plugged && s_auth_ok && (millis() - s_auth_ok_at_ms) <= AUTH_WINDOW_MS && s_auth_id_tag.length() > 0 && isConnectorIdle(2)) @@ -201,10 +452,27 @@ static void updateChargeActuators() { const bool chg1_on = ocppPermitsCharge(1); const bool chg2_on = ocppPermitsCharge(2); + bool led1_on = chg1_on; + bool led2_on = chg2_on; + + // During local authorization wait, blink the suggested idle connector LED. + const unsigned int hintConnector = getWaitHintConnectorId(); + if (hintConnector > 0) + { + const bool blink_on = ((millis() / WAIT_HINT_BLINK_MS) % 2) == 0; + if (hintConnector == 1 && !chg1_on) + { + led1_on = blink_on; + } + else if (hintConnector == 2 && !chg2_on) + { + led2_on = blink_on; + } + } // LEDs and relays are low-active - digitalWrite(PIN_LED1, chg1_on ? LOW : HIGH); - digitalWrite(PIN_LED2, chg2_on ? LOW : HIGH); + digitalWrite(PIN_LED1, led1_on ? LOW : HIGH); + digitalWrite(PIN_LED2, led2_on ? LOW : HIGH); digitalWrite(PIN_RELAY1, chg1_on ? LOW : HIGH); digitalWrite(PIN_RELAY2, chg2_on ? LOW : HIGH); } @@ -311,6 +579,9 @@ static void pollRfidCard() } idTag.toUpperCase(); + s_last_swipe_id = idTag; + s_last_swipe_at_ms = millis(); + rfid.PICC_HaltA(); rfid.PCD_StopCrypto1(); @@ -497,6 +768,33 @@ void setup() // Initialize I2C for OLED (from schematic pin map) Wire.begin(PIN_OLED_SDA, PIN_OLED_SCL); + // Initialize SSD1306 OLED over I2C, try 0x3C then 0x3D + if (display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) + { + s_oled_ready = true; + } + else if (display.begin(SSD1306_SWITCHCAPVCC, 0x3D)) + { + s_oled_ready = true; + } + else + { + s_oled_ready = false; + Serial.println("[OLED] SSD1306 init failed"); + } + + if (s_oled_ready) + { + display.clearDisplay(); + display.setTextSize(1); + display.setTextColor(SSD1306_WHITE); + display.setCursor(0, 0); + display.println("Helios EVCS"); + display.setCursor(0, 12); + display.println("Booting..."); + display.display(); + } + // Initialize IM1281C over UART2 (default: address 1, 4800bps, 8N1) im1281c.begin(Serial2, PIN_U2RXD, PIN_U2TXD); @@ -968,6 +1266,7 @@ void loop() } updateLED(); + refreshOled(); delay(10); } diff --git a/hardware/pcb/.history b/hardware/pcb/.history index 5df7c3b..468ec3b 160000 --- a/hardware/pcb/.history +++ b/hardware/pcb/.history @@ -1 +1 @@ -Subproject commit 5df7c3b623b8a9b7f85c60d5f3eb73b7aef91c18 +Subproject commit 468ec3b8092f7aced715b5ab17dde0fa10735f9d