#include "../include.h" #ifdef CHADEMO_BATTERY #include "../datalayer/datalayer.h" #include "../devboard/utils/events.h" #include "CHADEMO-BATTERY-INTERNAL.h" #include "CHADEMO-BATTERY.h" #include "CHADEMO-SHUNTS.h" /* CHADEMO handling runs at 6.25 times the rate of most other code, so, rather than the * default value of 12 (for 12 iterations of the 5s value update loop) * 5 for a 60s timeout, * instead use 75 for 75*0.8s = 60s */ #undef CAN_STILL_ALIVE #define CAN_STILL_ALIVE 75 //#define CH_CAN_DEBUG static unsigned long setupMillis = 0; static unsigned long handlerBeforeMillis = 0; static unsigned long handlerAfterMillis = 0; /* Do not change code below unless you are sure what you are doing */ static unsigned long previousMillis100 = 0; // will store last time a 100ms CAN Message was send static unsigned long previousMillis5000 = 0; // will store last time a 5s threshold was reached for display during debug bool plug_inserted = false; bool vehicle_can_initialized = false; bool vehicle_can_received = false; bool vehicle_permission = false; bool evse_permission = false; bool precharge_low = false; bool positive_high = false; bool contactors_ready = false; uint8_t framecount = 0; uint8_t max_discharge_current = 0; //TODO not sure on this one, but really influenced by inverter capability bool high_current_control_enabled = false; // set to true when high current control is operating // if true, values from 110.1 and 110.2 should be used instead of 102.3 // and 118 should be used for evse responses // permissible rate of change is -20A/s to 20A/s relative to 102.3 Mode EVSE_mode = CHADEMO_DISCHARGE; CHADEMO_STATE CHADEMO_Status = CHADEMO_IDLE; /* Charge/discharge sequence, indicating applicable V2H guideline * If sequence number is not agreed upon via H201/H209 between EVSE and Vehicle, * V2H 1.1 is assumed * Use CHADEMO_seq to decide whether emitting 209 is necessary * 0x0 1.0 and earlier * 0x1 2.0 appendix A * 0x2 2.0 appendix B * Unused for now. uint8_t CHADEMO_seq = 0x0; */ bool x201_received = false; bool x209_sent = false; struct x100_Vehicle_Charging_Limits x100_chg_lim = {}; struct x101_Vehicle_Charging_Estimate x101_chg_est = {}; struct x102_Vehicle_Charging_Session x102_chg_session = {}; struct x110_Vehicle_Dynamic_Control x110_vehicle_dyn = {}; struct x200_Vehicle_Discharge_Limits x200_discharge_limits = {}; struct x201_Vehicle_Discharge_Estimate x201_discharge_estimate = {}; struct x700_Vehicle_Vendor_ID x700_vendor_id = {}; struct x209_EVSE_Discharge_Estimate x209_evse_dischg_est; struct x108_EVSE_Capabilities x108_evse_cap; struct x109_EVSE_Status x109_evse_state; struct x118_EVSE_Dynamic_Control x118_evse_dyn; struct x208_EVSE_Discharge_Capability x208_evse_dischg_cap; CAN_frame CHADEMO_108 = {.FD = false, .ext_ID = false, .DLC = 8, .ID = 0x108, .data = {0x01, 0xF4, 0x01, 0x0F, 0xB3, 0x01, 0x00, 0x00}}; CAN_frame CHADEMO_109 = {.FD = false, .ext_ID = false, .DLC = 8, .ID = 0x109, .data = {0x02, 0x00, 0x00, 0x00, 0x01, 0x20, 0xFF, 0xFF}}; //For chademo v2.0 only CAN_frame CHADEMO_118 = {.FD = false, .ext_ID = false, .DLC = 8, .ID = 0x118, .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; // OLD value from skeleton implementation, indicates dynamic control is possible. // Hardcode above as being incompatible for simplicity in current incarnation. // .data = {0x10, 0x64, 0x00, 0xB0, 0x00, 0x1E, 0x00, 0x8F}}; // 0x200 : From vehicle-side. A V2X-ready vehicle will send this message to broadcast its “Maximum discharger current”. (It is a similar logic to the limits set in 0x100 or 0x102 during a DC charging session) // 0x208 : From EVSE-side. A V2X EVSE will use this to send the “present discharger current” during the session, and the “available input current”. (uses similar logic to 0x108 and 0x109 during a DC charging session) CAN_frame CHADEMO_208 = {.FD = false, .ext_ID = false, .DLC = 8, .ID = 0x208, .data = {0xFF, 0xF4, 0x01, 0xF0, 0x00, 0x00, 0xFA, 0x00}}; CAN_frame CHADEMO_209 = {.FD = false, .ext_ID = false, .DLC = 8, .ID = 0x209, .data = {0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; //This function maps all the values fetched via CAN to the correct parameters used for the inverter void update_values_battery() { datalayer.battery.status.real_soc = x102_chg_session.StateOfCharge; datalayer.battery.status.max_discharge_power_W = (x200_discharge_limits.MaximumDischargeCurrent * x100_chg_lim.MaximumBatteryVoltage); //In Watts, Convert A to P datalayer.battery.status.voltage_dV = get_measured_voltage() * 10; datalayer.battery.info.total_capacity_Wh = ((x101_chg_est.RatedBatteryCapacity / 0.1) * 1000); //(Added in CHAdeMO v1.0.1), maybe handle hardcoded on lower protocol version? /* TODO max charging rate = * x200_discharge_limits.MaxRemainingCapacityForCharging / * x101_chg_est.RatedBatteryCapacity * 100; */ datalayer.battery.status.remaining_capacity_Wh = static_cast( (static_cast(datalayer.battery.status.real_soc) / 10000) * datalayer.battery.info.total_capacity_Wh); /* To simulate or NOT to simulate battery cell voltages, that is .. A question. * Answer for now: Not, because they are not available in any direct manner. * This will impact Solax inverter support, which uses cell min/max mV to populate * CAN frames. */ if (vehicle_can_received) { uint8_t chargingrate = 0; if (x100_chg_lim.ConstantOfChargingRateIndication > 0) { chargingrate = x102_chg_session.StateOfCharge / x100_chg_lim.ConstantOfChargingRateIndication * 100; } } } //TODO simplified start/stop helper functions //see IEEE Table A.26—Charge control termination command pattern on pg58 //for stop conditions inline void process_vehicle_charging_minimums(CAN_frame rx_frame) { x100_chg_lim.MinimumChargeCurrent = rx_frame.data.u8[0]; x100_chg_lim.MinimumBatteryVoltage = ((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[2]); x100_chg_lim.MaximumBatteryVoltage = ((rx_frame.data.u8[5] << 8) | rx_frame.data.u8[4]); x100_chg_lim.ConstantOfChargingRateIndication = rx_frame.data.u8[6]; } inline void process_vehicle_charging_maximums(CAN_frame rx_frame) { x101_chg_est.MaxChargingTime10sBit = rx_frame.data.u8[1]; x101_chg_est.MaxChargingTime1minBit = rx_frame.data.u8[2]; x101_chg_est.EstimatedChargingTime = rx_frame.data.u8[3]; x101_chg_est.RatedBatteryCapacity = ((rx_frame.data.u8[6] << 8) | rx_frame.data.u8[5]); } inline void process_vehicle_charging_session(CAN_frame rx_frame) { uint16_t newTargetBatteryVoltage = ((rx_frame.data.u8[2] << 8) | rx_frame.data.u8[1]); uint16_t priorTargetBatteryVoltage = x102_chg_session.TargetBatteryVoltage; uint8_t newChargingCurrentRequest = rx_frame.data.u8[3]; uint8_t priorChargingCurrentRequest = x102_chg_session.ChargingCurrentRequest; vehicle_can_initialized = true; vehicle_permission = digitalRead(CHADEMO_PIN_4); x102_chg_session.ControlProtocolNumberEV = rx_frame.data.u8[0]; x102_chg_session.f.fault.FaultBatteryOverVoltage = bitRead(rx_frame.data.u8[4], 0); x102_chg_session.f.fault.FaultBatteryUnderVoltage = bitRead(rx_frame.data.u8[4], 1); x102_chg_session.f.fault.FaultBatteryCurrentDeviation = bitRead(rx_frame.data.u8[4], 2); x102_chg_session.f.fault.FaultHighBatteryTemperature = bitRead(rx_frame.data.u8[4], 3); x102_chg_session.f.fault.FaultBatteryVoltageDeviation = bitRead(rx_frame.data.u8[4], 4); x102_chg_session.s.status.StatusVehicleChargingEnabled = bitRead(rx_frame.data.u8[5], 0); x102_chg_session.s.status.StatusVehicleShifterPosition = bitRead(rx_frame.data.u8[5], 1); x102_chg_session.s.status.StatusChargingError = bitRead(rx_frame.data.u8[5], 2); x102_chg_session.s.status.StatusVehicle = bitRead(rx_frame.data.u8[5], 3); x102_chg_session.s.status.StatusNormalStopRequest = bitRead(rx_frame.data.u8[5], 4); x102_chg_session.s.status.StatusVehicleDischargeCompatible = bitRead(rx_frame.data.u8[5], 7); x102_chg_session.StateOfCharge = rx_frame.data.u8[6]; //NOTE: behavior differs in the case of high current control (x110 support TBD) // In that mode, ChargingCurrentRequest is set to 0xFF when >= 1 A is specified in // in “request charging current (for extended) (x110.1, x110.2),” and then afterward in x102, // it will not be updated x102_chg_session.ChargingCurrentRequest = newChargingCurrentRequest; x102_chg_session.TargetBatteryVoltage = newTargetBatteryVoltage; #ifdef DEBUG_LOG //Note on p131 uint8_t chargingrate = 0; if (x100_chg_lim.ConstantOfChargingRateIndication > 0) { chargingrate = x102_chg_session.StateOfCharge / x100_chg_lim.ConstantOfChargingRateIndication * 100; logging.print("Charge Rate (kW): "); logging.println(chargingrate); } #endif //Table A.26—Charge control termination command patterns -- should echo x108 handling /* charge/discharge permission signal from vehicle on pin 4 should NOT be sensed before first CAN received from vehicle. * Also, vehicle CAN should not simultaneously indicate enabled while permissions signal is absent * * Either is a logical inconsistency per spec (vehicle has lost state, some wire/pin is broken, etc) * this should trigger stop and teardown */ if ((CHADEMO_Status == CHADEMO_INIT && vehicle_permission) || (x102_chg_session.s.status.StatusVehicleChargingEnabled && !vehicle_permission)) { #ifdef DEBUG_LOG logging.println("Inconsistent charge/discharge state."); #endif CHADEMO_Status = CHADEMO_FAULT; return; } if (x102_chg_session.f.fault.FaultBatteryOverVoltage) { #ifdef DEBUG_LOG logging.println("Vehicle indicates fault, battery over voltage."); #endif CHADEMO_Status = CHADEMO_STOP; return; } if (x102_chg_session.f.fault.FaultBatteryUnderVoltage) { #ifdef DEBUG_LOG logging.println("Vehicle indicates fault, battery under voltage."); #endif CHADEMO_Status = CHADEMO_STOP; return; } if (x102_chg_session.f.fault.FaultBatteryCurrentDeviation) { #ifdef DEBUG_LOG logging.println("Vehicle indicates fault, battery current deviation. Possible EVSE issue?"); #endif CHADEMO_Status = CHADEMO_STOP; return; } if (x102_chg_session.f.fault.FaultBatteryVoltageDeviation) { #ifdef DEBUG_LOG logging.println("Vehicle indicates fault, battery voltage deviation. Possible EVSE issue?"); #endif CHADEMO_Status = CHADEMO_STOP; return; } // end if (priorTargetBatteryVoltage > 0 && newTargetBatteryVoltage == 0) { CHADEMO_Status = CHADEMO_STOP; return; } //FIXME condition nesting or more stanzas needed here for clear determination of cessation reason if (CHADEMO_Status == CHADEMO_POWERFLOW && EVSE_mode == CHADEMO_CHARGE && !vehicle_permission) { #ifdef DEBUG_LOG logging.println("State of charge ceiling reached or charging interrupted, stop charging"); #endif CHADEMO_Status = CHADEMO_STOP; return; } if (vehicle_permission && CHADEMO_Status == CHADEMO_NEGOTIATE) { CHADEMO_Status = CHADEMO_EV_ALLOWED; #ifdef DEBUG_LOG logging.println("STATE shift to CHADEMO_EV_ALLOWED in process_vehicle_charging_session()"); #endif return; } // TODO this and the next stanza influence state/control // and probably don't belong in this function // consider relocating if (vehicle_permission && CHADEMO_Status == CHADEMO_EVSE_PREPARE && priorTargetBatteryVoltage == 0 && newTargetBatteryVoltage > 0 && x102_chg_session.s.status.StatusVehicleChargingEnabled) { #ifdef DEBUG_LOG logging.println("STATE SHIFT to EVSE_START reached in process_vehicle_charging_session()"); #endif CHADEMO_Status = CHADEMO_EVSE_START; return; } if (vehicle_permission && evse_permission && CHADEMO_Status == CHADEMO_POWERFLOW) { #ifdef DEBUG_LOG logging.println("updating vehicle request in process_vehicle_charging_session()"); #endif return; } #ifdef DEBUG_LOG logging.println("UNHANDLED STATE IN process_vehicle_charging_session()"); #endif return; } /* x200 Vehicle, peer to x208 EVSE */ inline void process_vehicle_charging_limits(CAN_frame rx_frame) { x200_discharge_limits.MaximumDischargeCurrent = rx_frame.data.u8[0]; x200_discharge_limits.MinimumDischargeVoltage = ((rx_frame.data.u8[5] << 8) | rx_frame.data.u8[4]); x200_discharge_limits.MinimumBatteryDischargeLevel = rx_frame.data.u8[6]; x200_discharge_limits.MaxRemainingCapacityForCharging = rx_frame.data.u8[7]; #ifdef DEBUG_LOG /* unsigned long currentMillis = millis(); if (currentMillis - previousMillis5000 >= INTERVAL_5_S) { previousMillis5000 = currentMillis; logging.println("x200 Max remaining capacity for charging/discharging:"); // initially this is set to 0, which is represented as 0xFF logging.println(0xFF - x200_discharge_limits.MaxRemainingCapacityForCharging); } */ #endif if (get_measured_voltage() <= x200_discharge_limits.MinimumDischargeVoltage && CHADEMO_Status > CHADEMO_NEGOTIATE) { #ifdef DEBUG_LOG logging.println("x200 minimum discharge voltage met or exceeded, stopping."); #endif CHADEMO_Status = CHADEMO_STOP; } } /* Vehicle 0x201, peer to EVSE 0x209 * HOWEVER, 201 isn't even emitted in any of the v2x canlogs available */ inline void process_vehicle_discharge_estimate(CAN_frame rx_frame) { unsigned long currentMillis = millis(); x201_discharge_estimate.V2HchargeDischargeSequenceNum = rx_frame.data.u8[0]; x201_discharge_estimate.ApproxDischargeCompletionTime = ((rx_frame.data.u8[2] << 8) | rx_frame.data.u8[1]); x201_discharge_estimate.AvailableVehicleEnergy = ((rx_frame.data.u8[4] << 8) | rx_frame.data.u8[3]); #ifdef DEBUG_LOG if (currentMillis - previousMillis5000 >= INTERVAL_5_S) { previousMillis5000 = currentMillis; logging.print("x201 availabile vehicle energy, completion time: "); logging.println(x201_discharge_estimate.AvailableVehicleEnergy); logging.print("x201 approx vehicle completion time: "); logging.println(x201_discharge_estimate.ApproxDischargeCompletionTime); } #endif } inline void process_vehicle_dynamic_control(CAN_frame rx_frame) { //SM Dynamic Control = Charging station can increase of decrease "available output current" during charging. //If you set 0x110 byte 0, bit 0 to 1 you say you can do dynamic control. //Charging station communicates this in 0x118 byte 0, bit 0 x110_vehicle_dyn.u.status.HighVoltageControlStatus = bitRead(rx_frame.data.u8[0], 2); x110_vehicle_dyn.u.status.HighCurrentControlStatus = bitRead(rx_frame.data.u8[0], 1); x110_vehicle_dyn.u.status.DynamicControlStatus = bitRead(rx_frame.data.u8[0], 0); } inline void process_vehicle_vendor_ID(CAN_frame rx_frame) { x700_vendor_id.AutomakerCode = rx_frame.data.u8[0]; x700_vendor_id.OptionalContent = ((rx_frame.data.u8[2] << 8) | rx_frame.data.u8[1]); //Actually more bytes, but not needed for our purpose } void handle_incoming_can_frame_battery(CAN_frame rx_frame) { #ifdef CH_CAN_DEBUG logging.print(millis()); // Example printout, time, ID, length, data: 7553 1DB 8 FF C0 B9 EA 0 0 2 5D logging.print(" "); logging.print(rx_frame.ID, HEX); logging.print(" "); logging.print(rx_frame.DLC); logging.print(" "); for (int i = 0; i < rx_frame.DLC; ++i) { logging.print(rx_frame.data.u8[i], HEX); logging.print(" "); } logging.println(""); #endif // CHADEMO coexists with a CAN-based shunt. Only process CHADEMO-specific IDs // 202 is unknown if (!((rx_frame.ID >= 0x100 && rx_frame.ID <= 0x202) || rx_frame.ID == 0x700)) { return; } // used for testing vehicle sanity vehicle_can_received = true; /* CHADEMO_INIT state is a transient, used to indicate when CAN * has not yet been receied from a vehicle */ datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE; //We are getting CAN messages from the vehicle, inform the watchdog switch (rx_frame.ID) { case 0x100: process_vehicle_charging_minimums(rx_frame); break; case 0x101: process_vehicle_charging_maximums(rx_frame); break; case 0x102: framecount++; //the first few frames start as 0x03, then like 20 of 0x01 if (vehicle_can_initialized && framecount < 20) { return; } process_vehicle_charging_session(rx_frame); /* counter to help discard inital frames with bad SOC data */ break; case 0x200: //For V2X process_vehicle_charging_limits(rx_frame); break; case 0x201: //For V2X x201_received = true; process_vehicle_discharge_estimate(rx_frame); break; case 0x110: //Only present on Chademo v2.0 process_vehicle_dynamic_control(rx_frame); case 0x700: process_vehicle_vendor_ID(rx_frame); break; case 0x202: // unknown. LEAF specific? default: break; } if (CHADEMO_Status == CHADEMO_INIT) { // First CAN messages received, entering into negotiation // TODO consider tracking delta since transition time for expiry CHADEMO_Status = CHADEMO_NEGOTIATE; } handle_chademo_sequence(); } /* (re)initialize evse structures to pre-charge/discharge states */ void evse_init() { // Held at 1 until start of charge when set to 0 // returns to 1 when ceasing power flow // mutually exclusive values x109_evse_state.s.status.ChgDischStopControl = 1; x109_evse_state.s.status.EVSE_status = 0; x109_evse_state.s.status.connector_locked = 0; x109_evse_state.s.status.battery_incompatible = 0; x109_evse_state.s.status.ChgDischError = 0; /* values set during object initialization x109_evse_state.CHADEMO_protocol_number x109_evse_state.remaining_time_10s x109_evse_state.remaining_time_1m */ } /* updates for x108 */ void update_evse_capabilities(CAN_frame& f) { /* TODO use charger defines/runtime config? * for now..leave as a future tweak. * use vehicle requests as a ceiling */ /* interpret this as mostly a timing concern, so indicate yes we support EV contactor weld detection * expectations: <1s after vehicle contactors open, charger output will drop to <=25% of prior voltage * <2s after vehicle contactors open, charter output voltage will drop to <=10V * * see A.10.2.1 Example of welding detection logic on the vehicle */ x108_evse_cap.contactor_weld_detection = 0x1; /* should this be set to MAX_EVSE_OUTPUT_VOLTAGE or x102_chg_session.TargetBatteryVoltage ? */ x108_evse_cap.available_output_voltage = MAX_EVSE_OUTPUT_VOLTAGE; /* calculate max threshold to protect battery - using vehicle-provided max minus 2% */ x108_evse_cap.threshold_voltage = x100_chg_lim.MaximumBatteryVoltage - (int)(x100_chg_lim.MaximumBatteryVoltage / 100 * 2); // Power and voltage may be best derived from config/defines not from the x108 settings elsewhere, ideally // only set current when voltage > 0, as it is set by x102 TargetBatteryVoltage if (x108_evse_cap.available_output_voltage) { x108_evse_cap.available_output_current = MAX_EVSE_POWER_CHARGING / x108_evse_cap.available_output_voltage; } /* update Frame - byte 6 and 7 are unused */ CHADEMO_108.data.u8[0] = x108_evse_cap.contactor_weld_detection; CHADEMO_108.data.u8[1] = lowByte(x108_evse_cap.available_output_voltage); CHADEMO_108.data.u8[2] = highByte(x108_evse_cap.available_output_voltage); CHADEMO_108.data.u8[3] = x108_evse_cap.available_output_current; CHADEMO_108.data.u8[4] = lowByte(x108_evse_cap.threshold_voltage); CHADEMO_108.data.u8[5] = highByte(x108_evse_cap.threshold_voltage); } /* updates for x109 */ void update_evse_status(CAN_frame& f) { x109_evse_state.s.status.EVSE_status = 1; x109_evse_state.s.status.EVSE_error = 0; x109_evse_state.s.status.ChgDischError = 0; x109_evse_state.s.status.ChgDischStopControl = 0; /* updated only in state handler * TODO..why? x109_evse_state.s.connector_locked = 0; */ if (EVSE_mode == CHADEMO_DISCHARGE) { /* Occasionally oberved as set to 0 when discharging * this may be true for all V2H versions * unless it was a logging discrepancy x109_evse_state.setpoint_HV_VDC = 0; x109_evse_state.setpoint_HV_IDC = 0; */ x109_evse_state.setpoint_HV_VDC = min(x102_chg_session.TargetBatteryVoltage, x108_evse_cap.available_output_voltage); x109_evse_state.setpoint_HV_IDC = min(x102_chg_session.ChargingCurrentRequest, x108_evse_cap.available_output_current); /* TODO calculate remaining discharge time : for now == 60m */ x109_evse_state.remaining_time_1m = 60; } else if (EVSE_mode == CHADEMO_CHARGE) { x109_evse_state.setpoint_HV_VDC = get_measured_voltage(); x109_evse_state.setpoint_HV_IDC = get_measured_current(); /*For posterity if anyone is forced to simulate a shunt NOTE: these are supposed to be measured values, e.g., from a shunt If a sensor is not used, we are literally asserting that the measured value is exactly equivalent to the request or max charger capability this is pretty likely to fail on most vehicles x109_evse_state.setpoint_HV_VDC = min(x102_chg_session.TargetBatteryVoltage, x108_evse_cap.available_output_voltage); x109_evse_state.setpoint_HV_IDC = min(x102_chg_session.ChargingCurrentRequest, x108_evse_cap.available_output_current); */ /* The spec suggests throwing a 109.5.4 = 1 if vehicle curr request 102.3 > evse curr available 108.3, * but realistically many chargers seem to act tolerant here and stay under limits and supply whatever they are able */ /* if power overcommitted, back down to just below while maintaining voltage target */ if (x109_evse_state.setpoint_HV_VDC > 0 && x109_evse_state.setpoint_HV_IDC * x109_evse_state.setpoint_HV_VDC > MAX_EVSE_POWER_CHARGING) { x109_evse_state.setpoint_HV_IDC = floor(MAX_EVSE_POWER_CHARGING / x109_evse_state.setpoint_HV_VDC); } /* TODO calculate remaining charge time : for now == 60m */ x109_evse_state.remaining_time_1m = 60; } /* Table A.26 - See also Charge control termination command patterns * This handling must be mirrored in handling for vehicle x102 * */ if ((x102_chg_session.TargetBatteryVoltage > x108_evse_cap.available_output_voltage) || (x100_chg_lim.MaximumBatteryVoltage < x108_evse_cap.threshold_voltage)) { //Toggle battery incompatibility flag 109.5.3 x109_evse_state.s.status.EVSE_error = 1; x109_evse_state.s.status.battery_incompatible = 1; x109_evse_state.s.status.ChgDischStopControl = 1; CHADEMO_Status = CHADEMO_FAULT; } //CHADEMO_109.data.u8[0] hardcoded to 0x2 for CHAdeMO v1, 1.0.1, 1.1, 1.2 // in initialization CHADEMO_109.data.u8[0] = x109_evse_state.CHADEMO_protocol_number; CHADEMO_109.data.u8[1] = lowByte(x109_evse_state.setpoint_HV_VDC); CHADEMO_109.data.u8[2] = highByte(x109_evse_state.setpoint_HV_VDC); CHADEMO_109.data.u8[3] = x109_evse_state.setpoint_HV_IDC; CHADEMO_109.data.u8[4] = x109_evse_state.discharge_compatible; /* clear statuses/faults, then set explicitly */ CHADEMO_109.data.u8[5] = 0; CHADEMO_109.data.u8[5] = x109_evse_state.s.status.EVSE_status | x109_evse_state.s.status.EVSE_error << 1 | x109_evse_state.s.status.connector_locked << 2 | x109_evse_state.s.status.battery_incompatible << 3 | x109_evse_state.s.status.ChgDischError << 4 | x109_evse_state.s.status.ChgDischStopControl << 5; CHADEMO_109.data.u8[6] = x109_evse_state.remaining_time_10s; CHADEMO_109.data.u8[7] = x109_evse_state.remaining_time_1m; } /* Discharge estimates: x209 EVSE = peer to x201 Vehicle * NOTE: x209 is emitted in CAN logs when x201 isn't even present * it may not be understood by leaf (or ignored unless >= a certain protocol version or v2h sequence number */ void update_evse_discharge_estimate(CAN_frame& f) { //x209_evse_dischg_est.remaining_discharge_time_1m = x201_discharge_estimate.ApproxDischargeCompletionTime; //x201_discharge_estimate.AvailableVehicleEnergy = 0; //do nothing to alter the default initialized values..this may be unneeded /* TODO if (x200_discharge_limits.MaxRemainingCapacityForCharging = max charge voltage){ if (x200_discharge_limits.MinimumBatteryDischargeLevel = kwH for v2h<1.0){ if (x200_discharge_limits.MaxRemainingCapacityForCharging = kwH for v2h<1.0){ */ CHADEMO_209.data.u8[0] = x209_evse_dischg_est.sequence_control_number; CHADEMO_209.data.u8[1] = lowByte(x209_evse_dischg_est.remaining_discharge_time); CHADEMO_209.data.u8[2] = highByte(x209_evse_dischg_est.remaining_discharge_time); } /* x208 EVSE, peer to 0x200 Vehicle */ void update_evse_discharge_capabilities(CAN_frame& f) { //present discharge current is a measured value x208_evse_dischg_cap.present_discharge_current = 0xFF - get_measured_current(); /* Present discharge current is a measured value. In the absence of a shunt, the evse here is quite literally lying to the vehicle. The spec seems to suggest this is tolerated unless the current measured on the EV side continualy exceeds the maximum discharge current by 10amps x208_evse_dischg_cap.present_discharge_current = 0xFF - 6; */ //EVSE maximum current input is partly an inverter-influenced value i.e., min(inverter, vehicle_max_discharge) //use max_discharge_current variable if nonzero, otherwise tell the vehicle the EVSE will take everything it can give if (max_discharge_current) { x208_evse_dischg_cap.available_input_current = 0xFF - max_discharge_current; } else { x208_evse_dischg_cap.available_input_current = 0xFF - x200_discharge_limits.MaximumDischargeCurrent; } x208_evse_dischg_cap.available_input_voltage = x200_discharge_limits.MinimumDischargeVoltage; /* calculate min threshold to protect battery - using vehicle-provided minimum plus 2% * *As this is partly an inverter-influenced value, should this be a configurable variable backed by a defined default? * It seems sensible to be MAX(lowest usable voltage of the inverter input, lowest tolerable voltage of the vehicle battery) * NOT the vehicle minimumDischargeVoltage. * Thus, why here we are adding a few percent of cushion atop the minimum * This is the reverse treatment of the lower_threshold_voltage of charging mode */ x208_evse_dischg_cap.lower_threshold_voltage = x200_discharge_limits.MinimumDischargeVoltage + (int)(x200_discharge_limits.MinimumDischargeVoltage / 100 * 2); /* 0x00 == unused if (x200_discharge_limits.MinimumBatteryDischargeLevel > 0 && x208_evse_dischg_cap.minimum_input_voltage < x200_discharge_limits.MinimumBatteryDischargeLevel){ // stop discharging, but permit charging if mode = bidirectional } */ //TODO might be ideal to do the 0xFF subtraction HERE during serialization, rather than above? CHADEMO_208.data.u8[0] = x208_evse_dischg_cap.present_discharge_current; CHADEMO_208.data.u8[1] = lowByte(x208_evse_dischg_cap.available_input_voltage); CHADEMO_208.data.u8[2] = highByte(x208_evse_dischg_cap.available_input_voltage); CHADEMO_208.data.u8[3] = x208_evse_dischg_cap.available_input_current; CHADEMO_208.data.u8[6] = lowByte(x208_evse_dischg_cap.lower_threshold_voltage); CHADEMO_208.data.u8[7] = highByte(x208_evse_dischg_cap.lower_threshold_voltage); } void transmit_can_battery(unsigned long currentMillis) { handlerBeforeMillis = currentMillis; handle_chademo_sequence(); handlerAfterMillis = millis(); // Send 100ms CAN Message if (currentMillis - previousMillis100 >= INTERVAL_100_MS) { previousMillis100 = currentMillis; /* no EVSE messages should be sent until the vehicle has * initiated */ // if (CHADEMO_Status <= CHADEMO_INIT || !vehicle_can_received) { if (CHADEMO_Status <= CHADEMO_INIT) { return; } update_evse_capabilities(CHADEMO_108); update_evse_status(CHADEMO_109); update_evse_discharge_capabilities(CHADEMO_208); update_evse_discharge_estimate(CHADEMO_209); /* most updates to these EVSE frames are made * upon receipt of a Vehicle message, as * that is the limiting factor. Therefore, we * can generally send as is without tweaks here. */ transmit_can_frame(&CHADEMO_108, can_config.battery); transmit_can_frame(&CHADEMO_109, can_config.battery); /* TODO for dynamic control: can send x118 with byte 6 bit 0 set to 0 for 1s (before flipping back to 1) as a way of giving vehicle a chance to update 101.1 and 101.2 * within 6 seconds of x118 toggle. * Then 109.6 and 109.7 reset remaining charging time * see A.11.5.3.1.3 Remaining charging time (H’109.6, H’109.7) for a better description */ if (EVSE_mode == CHADEMO_DISCHARGE || EVSE_mode == CHADEMO_BIDIRECTIONAL) { transmit_can_frame(&CHADEMO_208, can_config.battery); if (x201_received) { transmit_can_frame(&CHADEMO_209, can_config.battery); x209_sent = true; } } // TODO need an update_evse_dynamic_control(..) function above before we send 118 // 110.0.0 if (x102_chg_session.ControlProtocolNumberEV >= 0x03) { //Only send the following on Chademo 2.0 vehicles? #ifdef DEBUG_LOG //FIXME REMOVE logging.println("REMOVE: proto 2.0"); #endif transmit_can_frame(&CHADEMO_118, can_config.battery); } } } /* A lot of the heavy lifting happens here. This is essentially the state hander. SOME * state transitions happen in functions before/after this is called. * * stages according to IEEE SPEC, with our states mapped into each. * 1) Standby stage * CHADEMO_IDLE * 2) Preparation stage * CHADEMO_CONNECTED * CHADEMO_INIT * CHADEMO_NEGOTIATE * CHADEMO_EV_ALLOWED * CHADEMO_EVSE_PREPARE * CHADEMO_EVSE_START * CHADEMO_EVSE_CONTACTORS_ENABLED * 3) Charging/Discharging stage * CHADEMO_POWERFLOW * 4) Termination stage * CHADEMO_STOP * 5) Emergency stop stage * CHADEMO_FAULT */ void handle_chademo_sequence() { precharge_low = digitalRead(PRECHARGE_PIN) == LOW; positive_high = digitalRead(POSITIVE_CONTACTOR_PIN) == HIGH; contactors_ready = precharge_low && positive_high; vehicle_permission = digitalRead(CHADEMO_PIN_4); /* ------------------- State override conditions checks ------------------- */ /* ------------------------------------------------------------------------------ */ if (CHADEMO_Status >= CHADEMO_EV_ALLOWED && x102_chg_session.s.status.StatusVehicleShifterPosition) { #ifdef DEBUG_LOG logging.println("Vehicle is not parked, abort."); #endif CHADEMO_Status = CHADEMO_STOP; } if (CHADEMO_Status >= CHADEMO_EV_ALLOWED && !vehicle_permission) { #ifdef DEBUG_LOG logging.println("Vehicle charge/discharge permission ended, stop."); #endif CHADEMO_Status = CHADEMO_STOP; } /* ------------------- STATE HANDLER ------------------- */ /* ------------------------------------------------------------------------------ */ switch (CHADEMO_Status) { case CHADEMO_IDLE: /* this is where we can unlock connector */ digitalWrite(CHADEMO_LOCK, LOW); plug_inserted = digitalRead(CHADEMO_PIN_7); if (!plug_inserted) { #ifdef DEBUG_LOG // Commented unless needed for debug // logging.println("CHADEMO plug is not inserted."); #endif return; } CHADEMO_Status = CHADEMO_CONNECTED; #ifdef DEBUG_LOG logging.println("CHADEMO plug is inserted. Provide EVSE power to vehicle to trigger initialization."); #endif break; case CHADEMO_CONNECTED: #ifdef DEBUG_LOG // Commented unless needed for debug //logging.println("CHADEMO_CONNECTED State"); #endif /* plug_inserted is .. essentially a volatile of sorts, so verify */ if (plug_inserted) { /* If connection is detectable, jumpstart handshake by * indicate that the EVSE is ready to begin */ digitalWrite(CHADEMO_PIN_2, HIGH); /* State change to initializing. We will re-enter the handler upon receipt of CAN */ CHADEMO_Status = CHADEMO_INIT; } else { /* this potentially-viewed-as-redundant condition checking is candidly * an expression racy-relaties of the real world. Depending upon * testing/performance, it may be better to pepper this state handler * with timers to have higher confidence of certain conditions hitting * a steady state */ #ifdef DEBUG_LOG logging.println("CHADEMO plug is not inserted, cannot connect d2 relay to begin initialization."); #endif CHADEMO_Status = CHADEMO_IDLE; } break; case CHADEMO_INIT: /* Transient state while awaiting CAN from Vehicle. * Used for triggers/error handling elsewhere; * State change to CHADEMO_NEGOTIATE occurs in handle_incoming_can_frame_battery(..) */ #ifdef DEBUG_LOG // logging.println("Awaiting initial vehicle CAN to trigger negotiation"); #endif evse_init(); break; case CHADEMO_NEGOTIATE: /* Vehicle and EVSE dance */ //TODO if pin 4 / j goes high, #ifdef DEBUG_LOG // Commented unless needed for debug // logging.println("CHADEMO_NEGOTIATE State"); #endif x109_evse_state.s.status.ChgDischStopControl = 1; break; case CHADEMO_EV_ALLOWED: #ifdef DEBUG_LOG // Commented unless needed for debug logging.println("CHADEMO_EV_ALLOWED State"); #endif // If we are in this state, vehicle_permission was already set to true...but re-verify // that pin 4 (j) reads high if (vehicle_permission) { //lock connector here digitalWrite(CHADEMO_LOCK, HIGH); //TODO spec requires test to validate solenoid has indeed engaged. // example uses a comparator/current consumption check around solenoid x109_evse_state.s.status.connector_locked = true; CHADEMO_Status = CHADEMO_EVSE_PREPARE; } break; case CHADEMO_EVSE_PREPARE: #ifdef DEBUG_LOG // Commented unless needed for debug logging.println("CHADEMO_EVSE_PREPARE State"); #endif /* TODO voltage check of output < 20v * insulation test hypothetically happens here before triggering PIN 10 high * see Table A.28—Requirements for the insulation test for output DC circuit Note: required that if 102.5.0 == 0, do not perform evse insulation test we should not be here in this state unless 102.5.0 was == 1 previously, but check again in case it has changed simulate via? if evse_present _voltage + 10 <= vehicle voltage_target { evse_present_voltage += 10; } else { evse_present_voltage = vehicle voltage_target; } */ if (x102_chg_session.s.status.StatusVehicleChargingEnabled) { if (get_measured_voltage() < 20) { digitalWrite(CHADEMO_PIN_10, HIGH); evse_permission = true; } else { logging.println("Insulation check measures > 20v "); } // likely unnecessary but just to be sure. consider removal x109_evse_state.s.status.ChgDischStopControl = 1; x109_evse_state.s.status.EVSE_status = 0; } else { CHADEMO_Status = CHADEMO_STOP; } //state changes to CHADEMO_EVSE_START only upon receipt of charging session request break; case CHADEMO_EVSE_START: #ifdef DEBUG_LOG // Commented unless needed for debug logging.println("CHADEMO_EVSE_START State"); #endif datalayer.system.status.battery_allows_contactor_closing = true; x109_evse_state.s.status.ChgDischStopControl = 1; x109_evse_state.s.status.EVSE_status = 0; CHADEMO_Status = CHADEMO_EVSE_CONTACTORS_ENABLED; #ifdef DEBUG_LOG logging.println("Initiating contactors"); #endif /* break rather than fall through because contactors are not instantaneous; * worth giving it a cycle to finish */ break; case CHADEMO_EVSE_CONTACTORS_ENABLED: #ifdef DEBUG_LOG // Commented unless needed for debug logging.println("CHADEMO_EVSE_CONTACTORS State"); #endif /* check whether contactors ready, because externally dependent upon inverter allow during discharge */ if (contactors_ready) { #ifdef DEBUG_LOG logging.println("Contactors ready"); logging.print("Voltage: "); logging.println(get_measured_voltage()); #endif /* transition to POWERFLOW state if discharge compatible on both sides */ if (x109_evse_state.discharge_compatible && x102_chg_session.s.status.StatusVehicleDischargeCompatible && (EVSE_mode == CHADEMO_DISCHARGE || EVSE_mode == CHADEMO_BIDIRECTIONAL)) { CHADEMO_Status = CHADEMO_POWERFLOW; x109_evse_state.s.status.ChgDischStopControl = 0; x109_evse_state.s.status.EVSE_status = 1; } if (EVSE_mode == CHADEMO_CHARGE) { //TODO not supported currently //CHADEMO_Status = CHADEMO_POWERFLOW; //x109_evse_state.s.status.ChgDischStopControl = 0; } } /* break or fall through ? TODO */ break; case CHADEMO_POWERFLOW: #ifdef DEBUG_LOG // Commented unless needed for debug logging.println("CHADEMO_POWERFLOW State"); #endif /* POWERFLOW for charging, discharging, and bidirectional */ /* Interpretation */ if (x102_chg_session.s.status.StatusVehicleShifterPosition) { /* Vehicle is no longer in park */ // vehicle will switch k off, EVSE MAY see j (pin 4) go low // EVEN IF evse reads read pin 4 to see high, if this condition is true then trigger EVSE charge/discharge stop // per spec // SEPARATE check for pin 4/j condition does not depend on this flag. it's an OR condition } if (x102_chg_session.TargetBatteryVoltage == 0x00) { //TODO flag error and do not calculate power in EVSE response? // probably unnecessary as other flags will be set causing this to be caught } if (get_measured_voltage() <= x200_discharge_limits.MinimumDischargeVoltage) { #ifdef DEBUG_LOG logging.println("x200 minimum discharge voltage met or exceeded, stopping."); #endif CHADEMO_Status = CHADEMO_STOP; } // Potentially unnecessary (set in CHADEMO_EVSE_CONTACTORS_ENABLED stanza), but just in case x109_evse_state.s.status.ChgDischStopControl = 0; x109_evse_state.s.status.EVSE_status = 1; break; case CHADEMO_STOP: #ifdef DEBUG_LOG // Commented unless needed for debug logging.println("CHADEMO_STOP State"); #endif /* back to CHADEMO_IDLE after teardown */ x109_evse_state.s.status.ChgDischStopControl = 1; x109_evse_state.s.status.EVSE_status = 0; x109_evse_state.s.status.battery_incompatible = 0; evse_permission = false; vehicle_permission = false; x209_sent = false; x201_received = false; /* protection of EV contactors - IEEE A.7.2.9 Protection of EV contactor * see also Table A.29—Charging stage and check item * * We will re-enter the handler until the amperage drops sufficiently * and then transition to CHADEMO_IDLE */ if (get_measured_current() <= 5 && get_measured_voltage() <= 10) { /* welding detection ideally here */ digitalWrite(CHADEMO_PIN_10, LOW); digitalWrite(CHADEMO_PIN_2, LOW); CHADEMO_Status = CHADEMO_IDLE; } break; case CHADEMO_FAULT: #ifdef DEBUG_LOG // Commented unless needed for debug logging.println("CHADEMO_FAULT State"); #endif /* Once faulted, never departs CHADEMO_FAULT state unless device is power cycled as a safety measure */ x109_evse_state.s.status.EVSE_error = 1; x109_evse_state.s.status.ChgDischError = 1; x109_evse_state.s.status.ChgDischStopControl = 1; #ifdef DEBUG_LOG logging.println("CHADEMO fault encountered, tearing down to make safe"); #endif digitalWrite(CHADEMO_PIN_10, LOW); digitalWrite(CHADEMO_PIN_2, LOW); evse_permission = false; vehicle_permission = false; x209_sent = false; x201_received = false; break; default: #ifdef DEBUG_LOG logging.println("UNHANDLED CHADEMO_STATE, setting FAULT"); #endif CHADEMO_Status = CHADEMO_FAULT; break; } return; } void setup_battery(void) { // Performs one time setup at startup pinMode(CHADEMO_PIN_2, OUTPUT); digitalWrite(CHADEMO_PIN_2, LOW); pinMode(CHADEMO_PIN_10, OUTPUT); digitalWrite(CHADEMO_PIN_10, LOW); pinMode(CHADEMO_LOCK, OUTPUT); digitalWrite(CHADEMO_LOCK, LOW); pinMode(CHADEMO_PIN_4, INPUT); pinMode(CHADEMO_PIN_7, INPUT); strncpy(datalayer.system.info.battery_protocol, "Chademo V2X mode", 63); datalayer.system.info.battery_protocol[63] = '\0'; CHADEMO_Status = CHADEMO_IDLE; /* disallow contactors until permissions is granted by vehicle */ datalayer.system.status.battery_allows_contactor_closing = false; /* Pretend that we know the SOH, assert that it is 99% */ datalayer.battery.status.soh_pptt = 9900; /* Briefly assert that we're starting at a modest SOC of 30% */ datalayer.battery.status.real_soc = 300; //TODO Must be user configured, most likely. Artificially capped for the time being datalayer.battery.status.max_discharge_power_W = 1000; datalayer.battery.status.max_charge_power_W = 1000; datalayer.battery.status.current_dA = 0; datalayer.battery.status.remaining_capacity_Wh = 12000; //TODO this is probably fine for a baseline, though CHADEMO can go as low as 150v and as high as 1500v in the latest revision //the below is relative to a 96 cell NMC. lower end is possibly too low datalayer.battery.info.max_design_voltage_dV = 4040; // 404.4V, over this, charging is not possible (goes into forced discharge) datalayer.battery.info.min_design_voltage_dV = 2600; // 260.0V under this, discharging further is disabled /* initialize EVSE data, state, and CAN frame representations */ switch (EVSE_mode) { case CHADEMO_DISCHARGE: case CHADEMO_BIDIRECTIONAL: x109_evse_state.discharge_compatible = true; break; case CHADEMO_CHARGE: x109_evse_state.discharge_compatible = false; break; default: break; } x109_evse_state.s.status.ChgDischStopControl = 1; handle_chademo_sequence(); // ISA_deFAULT(); // ISA Setup - it is sufficient to set it once, because it is saved in SUNT // ISA_initialize(); // ISA Setup - it is sufficient to set it once, because it is saved in SUNT // ISA_RESTART(); setupMillis = millis(); } #endif