feat(firmware): CC 模拟开关添加积分防抖&优化 OLED 信息展示和刷卡空枪闪烁提示

This commit is contained in:
2026-04-20 00:25:18 +08:00
parent 81b28b4461
commit afb0c88910
2 changed files with 315 additions and 16 deletions

View File

@@ -35,9 +35,18 @@ static volatile LEDState s_led_state = LED_INITIALIZING;
static volatile unsigned long s_blink_last_time = 0; static volatile unsigned long s_blink_last_time = 0;
static volatile bool s_blink_on = false; static volatile bool s_blink_on = false;
static const unsigned long BLINK_INTERVAL = 200; // 200ms blink interval 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 AUTH_WINDOW_MS = 30000;
static const unsigned long IM1281C_POLL_INTERVAL_MS = 5000; 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_cc1_plugged = false;
static bool s_cc2_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 unsigned long s_cc2_last_change_ms = 0;
static bool s_cc1_prev_plugged = false; static bool s_cc1_prev_plugged = false;
static bool s_cc2_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_in_progress = false;
static bool s_auth_ok = false; static bool s_auth_ok = false;
static unsigned long s_auth_ok_at_ms = 0; static unsigned long s_auth_ok_at_ms = 0;
static String s_auth_id_tag; 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 s_remote_start_accepted = false;
static bool authWindowValid(); static bool authWindowValid();
static bool isConnectorIdle(unsigned int connectorId);
static void clearAuthWait(const char *reason) static void clearAuthWait(const char *reason)
{ {
if (s_auth_ok || s_auth_id_tag.length() > 0) if (s_auth_ok || s_auth_id_tag.length() > 0)
@@ -102,6 +118,90 @@ IM1281C im1281c;
static IM1281CAData s_meter_a; static IM1281CAData s_meter_a;
static IM1281CBData s_meter_b; static IM1281CBData s_meter_b;
static bool s_meter_data_ready = false; 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) static int energyKwhToWh(float energyKwh)
{ {
@@ -118,6 +218,101 @@ static int energyKwhToWh(float energyKwh)
return static_cast<int>(energyWh + 0.5f); return static_cast<int>(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<int16_t>(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) static bool isConnectorPlugged(unsigned int connectorId)
{ {
if (connectorId == 1) if (connectorId == 1)
@@ -139,18 +334,45 @@ static bool isConnectorStartReady(unsigned int connectorId)
static void updateConnectorPluggedState() static void updateConnectorPluggedState()
{ {
unsigned long now = millis();
const bool cc1_raw = (digitalRead(PIN_CC1) == HIGH); 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_filter_inited = true;
s_cc1_last_change_ms = now; 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; 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 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)) 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); 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_filter_inited = true;
s_cc2_last_change_ms = now; 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; 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 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)) 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 chg1_on = ocppPermitsCharge(1);
const bool chg2_on = ocppPermitsCharge(2); 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 // LEDs and relays are low-active
digitalWrite(PIN_LED1, chg1_on ? LOW : HIGH); digitalWrite(PIN_LED1, led1_on ? LOW : HIGH);
digitalWrite(PIN_LED2, chg2_on ? LOW : HIGH); digitalWrite(PIN_LED2, led2_on ? LOW : HIGH);
digitalWrite(PIN_RELAY1, chg1_on ? LOW : HIGH); digitalWrite(PIN_RELAY1, chg1_on ? LOW : HIGH);
digitalWrite(PIN_RELAY2, chg2_on ? LOW : HIGH); digitalWrite(PIN_RELAY2, chg2_on ? LOW : HIGH);
} }
@@ -311,6 +579,9 @@ static void pollRfidCard()
} }
idTag.toUpperCase(); idTag.toUpperCase();
s_last_swipe_id = idTag;
s_last_swipe_at_ms = millis();
rfid.PICC_HaltA(); rfid.PICC_HaltA();
rfid.PCD_StopCrypto1(); rfid.PCD_StopCrypto1();
@@ -497,6 +768,33 @@ void setup()
// Initialize I2C for OLED (from schematic pin map) // Initialize I2C for OLED (from schematic pin map)
Wire.begin(PIN_OLED_SDA, PIN_OLED_SCL); 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) // Initialize IM1281C over UART2 (default: address 1, 4800bps, 8N1)
im1281c.begin(Serial2, PIN_U2RXD, PIN_U2TXD); im1281c.begin(Serial2, PIN_U2RXD, PIN_U2TXD);
@@ -968,6 +1266,7 @@ void loop()
} }
updateLED(); updateLED();
refreshOled();
delay(10); delay(10);
} }

Submodule hardware/pcb/.history updated: 5df7c3b623...468ec3b809