I have been making a EVSE charger for my EV with an ESP32
Charger works great and I live in Europe with 230VAC outlets, so my charger is limited to 16A in this code (on the webpage), the code works great up to 32A if you want to charge Tesla or any other car that accepts 7.6kW charging, the protocol is the same.
My outlet is 3.8kW so I have limited it at 16A in this version of the program.
The limit is on the webpage slider with range from 6 to 16.
Feel free to skip all the wifi stuff and hard-code your preferences to one value or 2-3 buttons with different charging values.
controlling the charging from websockets has the advantage to limit charging to timed schedule or with the solar/wind output of the day if using off-grid solutions.
charging only if the price of kwH is between x and y.
to charge at schedule, set the charger in STATE_CUSTOM_OFF to cut off power to the pilot and all relays off, then at designated time let the loop run normally and allow it to go between states A-D at desired max current
All the open-evse projects have been based on Arduinos that do not have any wifi or are 5V only.
Also there is next to none showing how to program one for your self without using hardcore bare metal programming that is not portable between arduino flavours of MCU's.
I have made my charger with ESP32 Wroom module and the only differance from that and a vanilla Arduino is the pwm generation/resolution and the ADC resolution.
I use 2x 40A SSR for the AC power and an old SSD1306 OLED display on the box for visual confirmation. the code requires you to have a network to connect to, but can be easily changed to Acces Point instead, but I use websockets to connect it to my to be home automation system...
I also monitor the temperature with a DS18b20 temp sensor epoxied to the heatsink of the SSR's
(the SSR's will be HOT, can go to 80C at 14A charging and heatsink is not optional using SSR)
I have ordered split core current sensor to attach to the system but it has not arrived so there is no code for that yet
there are no safety checks either, so if your electric installation has no RCD protection, then do not try to replicate. My protection is DIN-rail mounted and I use DIN-rail PSU for the ESP32 system.
edit: changed code to better reflect scheduled charging on home automation + screenshots
edit: missing code for STATE_CUSTOM_ON message from websocket fixed
edit: added current sensor readings to the code to monitor how much the car draws from the socket... a new screenshot added too
edit: PWM bug fix (no realtime update from slider until button->charger off and button->on again. Fixed slider will not update from server until user changes things. few typos and comments. Charging is from 6-32 amps, careful to not burn down your house!!. Bug in line 503 added due to a quirk of my car, the number 3 should perhaps not be there for you...
The Code so far:
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <ESPmDNS.h>
#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include <Arduino_JSON.h>
//#include <ArduinoJson.h>
#include <OneWire.h>
#include <DallasTemperature.h>
// for 1kHz pwm generation
#define PILOT_GPIO 32
#define CURRENT_SENS_GPIO 35
#define PWM1_Ch 0
#define PWM1_Res 10 // 10 bits = 1024 levels = 0,05859375A per step
#define PWM1_Freq 980 // 980 - 1020 range, 1000 nominal
int PWM1_DutyCycle = 0;
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
#define RELAY_PIN 25 // charging relay on/off
typedef enum ChargingState {
STATE_A, // Pilot Voltage 12V, Not connected, ADC: 3950
STATE_B, // Pilot Voltage 9V, Connected, ADC: 3408
STATE_C, // Pilot Voltage 6V, Charging, ADC: 2847
STATE_D, // Pilot Voltage 3V, Verntilation r ADC: 2390
STATE_E, // Pilot Voltage 0V, No Power ADC: 1941
STATE_F, // Pilot Voltage -12V, EVSE Error ADC: <1500
STATE_CUSTOM_OFF, // custom state for Home automation, no charging until STATE_CUSTOM_ON or STATE_A
STATE_CUSTOM_ON
};
ChargingState pilotState = STATE_F; // setting default to error, it will change as no power is detected at startup
// GPIO where the DS18B20 is connected to
const int oneWireBus = 15;
// Setup a oneWire instance to communicate with any OneWire devices
OneWire oneWire(oneWireBus);
// Pass our oneWire reference to Dallas Temperature sensor
DallasTemperature sensors(&oneWire);
float temperatureC = 0.00;
int chargingCurrent = 14; // current AMPS for charging
// PilotPin circuit is connected to GPIO 34 (Analog ADC1_CH6)
int pilotPin = 34;
//Current sensor values
float ampValue = 0.00;
String message = "";
String amperes1 = "14";
int minvaluePilot = 0;
// variable for storing the pilot value
int pilotValue = 0;
float voltage = 0.00;
String stateStr = "";
// ***************** WIFI *************
const char *ssidArray[] = {"Your_SSID_HERE", "Your_2nd_SSID_HERE" , "Your_Telephone_Hot_Spot_SSID" }; //just add to this list or remove as you like...
const char *passwordArray[] = {"YOUR_PASSWORD_HERE", "YOUR_2nd_PASSWORD_HERE" , "Your_Telephone_Hot_Spot_Password" };
// to be filled at runtime to found network
const char* ssid = "";
const char* password = "";
// ********* end of known networks ******
// Create AsyncWebServer object on port 80
AsyncWebServer server(80);
AsyncWebSocket ws("/ws"); // access at ws://[esp ip]/ws
AsyncWebSocketClient * globalClient = NULL;
// ************************************
//Json Variable to Hold Slider Values
JSONVar sliderValues, internValues;
//Get Slider Values
String getSliderValues(){
sliderValues["amperes1"] = String(amperes1);
sliderValues["temperatureValue1"] = String(temperatureC);
sliderValues["chargingState"] = String(stateStr);
sliderValues["chargingAmps"] = String(ampValue);
String jsonString = JSON.stringify(sliderValues);
return jsonString;
}
String sendSocketUpdates(){
// We do not want to update slider values periodically
// The slider is updated by user on the webpage
// and other users are notified by getSliderValues() function
// only update generated values like amps that the car is taking
// and internal temperature of the charger
// and the state of the charger
//sliderValues["amperes1"] = String(amperes1);
internValues["temperatureValue1"] = String(temperatureC);
internValues["chargingState"] = String(stateStr);
internValues["chargingAmps"] = String(ampValue);
String jsonString = JSON.stringify(internValues);
return jsonString;
}
// Generally, you should use "unsigned long" for variables that hold time
// The value will quickly become too large for an int to store
unsigned long previousMillis = 0; // will store last time request was updated
// constants won't change:
const long interval = 4000; // interval at which to send websocket updates to client (milliseconds)
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<title>Charger Web Server</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
html {font-family: Arial; display: inline-block; text-align: center;}
h2 {font-size: 2.3rem;}
p {font-size: 1.9rem;}
body {max-width: 500px; margin:0px auto; padding-bottom: 20px;}
.slider { -webkit-appearance: none; margin: 14px; width: 380px; height: 25px; background: #FFD65C;
outline: none; -webkit-transition: .2s; transition: opacity .2s;}
.slider::-webkit-slider-thumb {-webkit-appearance: none; appearance: none; width: 35px; height: 35px; background: #003249; cursor: pointer;}
.slider::-moz-range-thumb { width: 35px; height: 35px; background: #003249; cursor: pointer; }
</style>
</head>
<body>
<h1>Choose Max Charging Current</h1>
<div class="slidecontainer">
<h2 style="float: left;" >6 </h2> <h2 style="float: right;" > 32</h2>
<input type="range" oninput="updateSlider(this)" onchange="updateSliderAMP(this)" min="6" max="32" value="14" class="slider" id="ampRange1">
<p>Max charging at: <span id="amperes1"></span> Amp.</p>
<p></p>
<p>Temperature at: <span id="tempValue"></span> °C</p>
<p>Charging State: </p>
<p><span id="chargingText"></span></p>
<p>Car Charges At <span id="chargingAtAmpsText"></span> A Now</p>
<input type="button" id="btnChargeOn" value="Charger OFF" style="float: left;" onClick="sendOff();">
<input type="button" id="btnChargeOn" value="Charger ON" style="float: right;" onClick="sendOn();">
</div>
<script>
var gateway = `ws://${window.location.hostname}/ws`;
var websocket;
window.addEventListener('load', onload);
websocket.send("getValues");
function onload(event) {
initWebSocket();
}
function getValues(){
websocket.send("getValues");
}
function sendOff(){
websocket.send("setStateCustomOff");
}
function sendOn(){
websocket.send("setStateCustomOn");
}
function initWebSocket() {
console.log('Trying to open a WebSocket connection…');
websocket = new WebSocket(gateway);
websocket.onopen = onOpen;
websocket.onclose = onClose;
websocket.onmessage = onMessage;
}
function onOpen(event) {
console.log('Connection opened');
getValues();
}
function onClose(event) {
console.log('Connection closed');
setTimeout(initWebSocket, 2000);
}
function updateSlider(element) {
var sliderNumber = element.id.charAt(element.id.length-1);
var sliderValue = document.getElementById(element.id).value;
document.getElementById("amperes"+sliderNumber).innerHTML = sliderValue;
//console.log(sliderValue);
//websocket.send(sliderNumber+"s"+sliderValue.toString());
}
function updateSliderAMP(element) {
var sliderNumber = element.id.charAt(element.id.length-1);
var sliderValue = document.getElementById(element.id).value;
document.getElementById("amperes"+sliderNumber).innerHTML = sliderValue;
console.log(sliderValue);
websocket.send(sliderNumber+"s"+sliderValue.toString());
}
function onMessage(event) {
console.log(event.data);
var myObj = JSON.parse(event.data);
var keys = Object.keys(myObj);
for (var i = 0; i <= (keys.length -1); i++){
var key = keys[i];
if (key == "temperatureValue1") {
document.getElementById(("tempValue").toString()).innerHTML = myObj[key];
}
else if (key == "chargingState") {
document.getElementById(("chargingText").toString()).innerHTML = myObj[key];
}
else if (key == "chargingAmps") {
document.getElementById(("chargingAtAmpsText").toString()).innerHTML = myObj[key];
}
else {
document.getElementById(key).innerHTML = myObj[key];
document.getElementById("ampRange"+ (i+1).toString()).value = myObj[key];
}
}
}
</script>
</body>
</html>
)rawliteral";
// Replaces placeholder with button section in your web page
// this code does nothing in charger program, needs to bee cleaned up
String processor(const String& var){
//Serial.println(var);
if(var == "BUTTONPLACEHOLDER"){
String buttons = "";
// buttons += "<h4>Output - GPIO 2</h4><label class=\"switch\"><input type=\"checkbox\" onchange=\"toggleCheckbox(this)\" id=\"2\" " + outputState(2) + "><span class=\"slider\"></span></label>";
// buttons += "<h4>Output - GPIO 4</h4><label class=\"switch\"><input type=\"checkbox\" onchange=\"toggleCheckbox(this)\" id=\"4\" " + outputState(4) + "><span class=\"slider\"></span></label>";
// buttons += "<h4>Output - GPIO 33</h4><label class=\"switch\"><input type=\"checkbox\" onchange=\"toggleCheckbox(this)\" id=\"33\" " + outputState(33) + "><span class=\"slider\"></span></label>";
return buttons;
}
return String();
}
// Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
#define OLED_RESET 27 // Reset pin # (or -1 if sharing Arduino reset pin)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
/*
ESP32-WROOM-32 Pinout
3v3 to opamp _____________________
3.3V--|* *| -- GND
Jtag Reset--|* *| -- GPIO_23
GPIO_36--|* OLED*| -- GPIO_22 <---> OLED SCL
GPIO_39--|* *| -- GPIO__1
GPIO_34--|*Analog IN *| -- GPIO__3
GPIO_35--|*Current sens OLED*| -- GPIO_21 <---> OLED SDA
GPIO_32--|*PWM 1KhZ *| -- NC / GND
GPIO_33--|* *| -- GPIO_19
GPIO_25--|*230V Relay *| -- GPIO_18
GPIO_26--|* *| -- GPIO__5
GPIO_27--|*OLED_RESET *| -- GPIO_17
GPIO_14--|* *| -- GPIO_16
GPIO_12--|* *| -- GPIO__4
GND---|* *| -- GPIO__0
GPIO_13--|* *| -- GPIO__2
GPIO__9--|* Temp Sensor*| -- GPIO_15 <---> Dallas DS18b20 Temperature sensor
GPIO_10--|* *| -- GPIO__8
GPIO_11--|* *| -- GPIO__7
5.0V--|* EN USB BOOT *| -- GPIO__6
|____**__| |__**______|
Do not use ADC2_ pins to measure when using WiFi
WiFi causes interrupts on those analog_in pins
Use GPIO 32, 33, 34, 35, 36, 39 only for analog measurements
Charging States:
State: Pilot Voltage: EV Resistance: Description: Analog theoretic: (if pwm is on 1khz)
State A 12V N/A Not Connected 3.177 V = 3943 of 4096 on ADC
State B 9V 2.7K Connected 2.811 V = 3489 of 4096 on ADC
State C 6V 882 Ohm Charging 2.445 V = 3034 of 4096 on ADC
State D 3V 246 Ohm Ventilation Required 2.079 V = 2580 of 4096 on ADC
State E 0V N/A No Power 1.713 V = 2126 of 4096 on ADC
State F -12V N/A EVSE Error 249.198 mV = 309 of 4096 on ADC
To test pikot voltage: take resistor from EV resistance table and put between pilot-pin and ground
the pilot voltage will drop to 9V with 2.7K resistor etc, then you can measure the ADC values
It is best to test with 100% pwm = DC 12V from Op-Amp (no square wave), then simple analogRead();
on PilotPin input will give you the desired ADC value.
Second choice is driving the different voltages from a powersupply through the PP pin and ground
(bias voltage must be persent on circuit or the ESP pins can be damaged)
measured Analog:
State A: 3.19V and ADC: 3950
State B: 2.75V and ADC: 3408
State C: 2.30V and ADC: 2847
State D: 1.92V and ADC: 2390
State E: 1.57V and ADC: 1941
State F:
Calculation for PWM signal to charge @ x AMPS: (valid for up to 51A)
AMPS = Duty cycle X 0.6 (duty cycle is in %)
Duty Cycle = AMPS / 0.6
example: 6A / 0.6 = 10% PWM
16A / 0.6 = 26.666% PWM
10% PWM * 0.6 = 6A
20% PWM * 0.6 = 12A
51-80A, AMPS = (Duty Cycle - 64)2.5
---------------------------------------------------------------------------------------------------------------------------------
(similar to https://ev-olution.yolasite.com/DIY-Arduino-EVSE.php?fbclid=IwAR23UIyWYvearjFswAKCXX8PWCxq1a4q0NeUoIQT9HDftjr3ByCuptJ1D0w)
(but adapted to 3.3V instead of 5V) => R4=47K, R3=68K, and 5V voltage source is changed to 3.3V
EV-Ground and ESP32 ground (and psu) must be connected together
https://www.ebay.com/itm/323540774103?hash=item4b54886cd7:g:vd4AAOSwIr9bkKIv at 150mA is sufficient to generate +-12V rails
Op-AMP circuit:
3.3V Pilot to car (PP pin on EV charging plug)
| |
R47K |--|<-| TVS diode-P6KE16A->GND
| |
GND---R68K--|--R200K---|
|
| ____________________
Pilot -- R1(1KOhm) Output1 Pin_1 |* *| Pin_8 +VCC (+12V)_____________________
10K-10K voltage divider from 3.3V->inverting input(1.75V) Pin_2 |* LF353 *| Pin_7 Output2 (not used) |
From ESP32 GPIO_32 Pin_3 |* OP-AMP *| Pin_6 Inverting input2 (not used) |
-VCC ------- -12V Pin_4 |* *| Pin_5 Non inverting input2 (not used) |
| |____________________| |
| |
GND---->|0.1uF|---|0.1uF|->OP_AMP_pin_8(+12V)__________________________________________________|
Safety-checks are omitted from this version of circuit, proof of concept only if you are not connected to RCD protected mains
Diode check is not implemented either
---------------------------------------------------------------------------------------------------------------------------------
CT- for 32A current sensing on a 2000 turn current clamp, 68 Ohm resistor gives measuring range to 34A
https://tyler.anairo.com/projects/open-energy-monitor-calculator
circuit is at:
https://learn.openenergymonitor.org/electricity-monitoring/ct-sensors/interface-with-arduino
Formula for calculating the load in Ampers can be derived from measuring the ADC-values when using heaters of different wattage.
measure the voltage in the AC with a multimeter
measure the resistance of your loads
calculate the amperage of each load
measure heat-gun or 2000W, 1500W oven then 1000W oven, or even 100-200W load and plot in excel and get the formula for the fitted line
plug the formula back in a function, you can do both for Amps and kwH
Voltage can be measured like this also for more correct calculation:
https://learn.openenergymonitor.org/electricity-monitoring/voltage-sensing/measuring-voltage-with-an-acac-power-adapter
--------------------------------------------------------------------------------------------------------------------------------
*/
// use tool @ http://javl.github.io/image2cpp/ to get 128x64 bw image formated for progmem
// 'FluidSensorTechnology', 128x64px
const unsigned char FluidSensorFluidSensorTechnology [] PROGMEM = {
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xf9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f,
0xf9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f,
0xf9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f,
0xf9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f,
0xf9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f,
0xf9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f,
0xf9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f,
0xf9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xdf, 0xff, 0xff, 0xdf, 0xdf, 0xff, 0xff, 0x9f,
0xf9, 0xff, 0xfe, 0x0c, 0xf9, 0xce, 0x70, 0x7f, 0x06, 0x04, 0xe7, 0x07, 0x07, 0x81, 0xff, 0x9f,
0xf9, 0xff, 0xfe, 0x7c, 0xfd, 0xce, 0x73, 0xbf, 0x76, 0x7c, 0x67, 0x76, 0x73, 0xbd, 0xff, 0x9f,
0xf9, 0xff, 0xfe, 0x0c, 0xfd, 0xce, 0x73, 0x9f, 0x8e, 0x04, 0x27, 0x0e, 0xfb, 0x89, 0xff, 0x9f,
0xf9, 0xff, 0xfe, 0x7c, 0xfd, 0xce, 0x73, 0x9f, 0xf2, 0x7c, 0x87, 0xe2, 0xfb, 0x83, 0xff, 0x9f,
0xf9, 0xff, 0xfe, 0x7c, 0xfd, 0xce, 0x73, 0xbf, 0x72, 0x7c, 0xc7, 0x72, 0x73, 0xbb, 0xff, 0x9f,
0xf9, 0xff, 0xfe, 0x7c, 0x0c, 0x1e, 0x70, 0x7f, 0x06, 0x04, 0xe7, 0x07, 0x07, 0xb9, 0xff, 0x9f,
0xf9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f,
0xf9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f,
0xf9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f,
0xf9, 0xff, 0xfe, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f,
0xf9, 0xff, 0xe0, 0x00, 0x0f, 0xff, 0xff, 0xff, 0x83, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xfb, 0x9f,
0xf9, 0xff, 0x07, 0xff, 0xc1, 0xff, 0xff, 0xfe, 0x1f, 0xfe, 0x3f, 0xff, 0xff, 0xff, 0xf1, 0x9f,
0xf9, 0xfc, 0x3f, 0xff, 0xf8, 0x7f, 0xff, 0xfc, 0x7f, 0xff, 0x8f, 0xff, 0xff, 0xff, 0xc7, 0x9f,
0xf9, 0xf8, 0xff, 0xff, 0xfe, 0x3f, 0xff, 0xf1, 0xff, 0xff, 0xc3, 0xff, 0xff, 0xff, 0x8f, 0x9f,
0xf9, 0xe3, 0xff, 0xff, 0xff, 0x8f, 0xff, 0xe3, 0xff, 0xff, 0xf0, 0xff, 0xff, 0xfe, 0x1f, 0x9f,
0xf9, 0xc7, 0xff, 0xff, 0xff, 0xc7, 0xff, 0x8f, 0xff, 0xff, 0xfc, 0x3f, 0xff, 0xf0, 0x7f, 0x9f,
0xf9, 0x8f, 0xff, 0xff, 0xff, 0xf1, 0xfe, 0x1f, 0xff, 0xff, 0xff, 0x01, 0xff, 0x03, 0xff, 0x9f,
0xf9, 0x9f, 0xfe, 0x00, 0xff, 0xfc, 0x00, 0x7f, 0xf0, 0x03, 0xff, 0xe0, 0x00, 0x1f, 0xff, 0x9f,
0xf9, 0xff, 0xe0, 0x00, 0x0f, 0xff, 0x03, 0xff, 0x81, 0xe0, 0xff, 0xff, 0x87, 0xff, 0xfb, 0x9f,
0xf9, 0xff, 0x07, 0xff, 0xc1, 0xff, 0xff, 0xfe, 0x1f, 0xfc, 0x3f, 0xff, 0xff, 0xff, 0xf1, 0x9f,
0xf9, 0xfc, 0x3f, 0xff, 0xf8, 0x7f, 0xff, 0xfc, 0x7f, 0xff, 0x0f, 0xff, 0xff, 0xff, 0xe3, 0x9f,
0xf9, 0xf8, 0xff, 0xff, 0xfe, 0x3f, 0xff, 0xf1, 0xff, 0xff, 0xc3, 0xff, 0xff, 0xff, 0x8f, 0x9f,
0xf9, 0xe3, 0xff, 0xff, 0xff, 0x8f, 0xff, 0xe3, 0xff, 0xff, 0xf0, 0xff, 0xff, 0xfe, 0x1f, 0x9f,
0xf9, 0xc7, 0xff, 0xff, 0xff, 0xc7, 0xff, 0x8f, 0xff, 0xff, 0xfc, 0x3f, 0xff, 0xf0, 0x7f, 0x9f,
0xf9, 0x8f, 0xff, 0xff, 0xff, 0xf1, 0xfe, 0x1f, 0xff, 0xff, 0xff, 0x03, 0xff, 0x81, 0xff, 0x9f,
0xf9, 0x9f, 0xfe, 0x00, 0xff, 0xf8, 0x00, 0x7f, 0xf0, 0x03, 0xff, 0xe0, 0x00, 0x0f, 0xff, 0x9f,
0xf9, 0xff, 0xe0, 0x00, 0x0f, 0xff, 0x03, 0xff, 0x81, 0xe0, 0xff, 0xff, 0x83, 0xff, 0xff, 0x9f,
0xf9, 0xff, 0x83, 0xff, 0x81, 0xff, 0xff, 0xff, 0x1f, 0xfc, 0x3f, 0xff, 0xff, 0xff, 0xf1, 0x9f,
0xf9, 0xfe, 0x1f, 0xff, 0xf8, 0x7f, 0xff, 0xfc, 0x7f, 0xff, 0x0f, 0xff, 0xff, 0xff, 0xe3, 0x9f,
0xf9, 0xf8, 0xff, 0xff, 0xfe, 0x3f, 0xff, 0xf1, 0xff, 0xff, 0xc3, 0xff, 0xff, 0xff, 0x8f, 0x9f,
0xf9, 0xf1, 0xff, 0xff, 0xff, 0x8f, 0xff, 0xe3, 0xff, 0xff, 0xf0, 0xff, 0xff, 0xfe, 0x1f, 0x9f,
0xf9, 0xc7, 0xff, 0xff, 0xff, 0xc7, 0xff, 0x8f, 0xff, 0xff, 0xfc, 0x3f, 0xff, 0xf8, 0x7f, 0x9f,
0xf9, 0x8f, 0xff, 0xff, 0xff, 0xf1, 0xfe, 0x1f, 0xff, 0xff, 0xff, 0x03, 0xff, 0x81, 0xff, 0x9f,
0xf9, 0x9f, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x0f, 0xff, 0x9f,
0xf9, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01, 0xff, 0xff, 0x9f,
0xf9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f,
0xf9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f,
0xf9, 0xff, 0xff, 0x81, 0x81, 0x83, 0xdc, 0xe7, 0x7c, 0x3c, 0xfe, 0x1f, 0x07, 0x7b, 0xff, 0x9f,
0xf9, 0xff, 0xff, 0xe7, 0xbf, 0x39, 0x9c, 0xe3, 0x79, 0x9c, 0xfd, 0xce, 0x73, 0x37, 0xff, 0x9f,
0xf9, 0xff, 0xff, 0xe7, 0x9f, 0x7f, 0x9c, 0xe3, 0x73, 0xcc, 0xf9, 0xe6, 0xff, 0x87, 0xff, 0x9f,
0xf9, 0xff, 0xff, 0xe7, 0x83, 0x7f, 0x80, 0xed, 0x73, 0xcc, 0xf9, 0xe6, 0xe3, 0xcf, 0xff, 0x9f,
0xf9, 0xff, 0xff, 0xe7, 0xbf, 0x39, 0x9c, 0xe4, 0x7b, 0xcc, 0xf9, 0xee, 0x7b, 0xcf, 0xff, 0x9f,
0xf9, 0xff, 0xff, 0xe7, 0x9f, 0x93, 0x9c, 0xe6, 0x79, 0x9c, 0xfc, 0xcf, 0x33, 0xcf, 0xff, 0x9f,
0xf9, 0xff, 0xff, 0xff, 0x83, 0xc7, 0xdf, 0xef, 0x7e, 0x7e, 0x1f, 0x3f, 0x8f, 0xff, 0xff, 0x9f,
0xf9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f,
0xf9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f,
0xfb, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xdf,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
};
float getChargingCurrent(int pin, int noSamples){
int analog = checkAnalog(pin, noSamples);
//float amps = (analog-1900.00)/76.70; // formula from experimental values on 243VAC tested line test on resistive heaters 600-1500W
// using current sensor from sparkfun 30A: https://www.sparkfun.com/products/11005
// and 68 Ohm burden resistor
double amps = (0.0138 * analog) - 26;
return amps;
}
void scanNet(){
Serial.println("scan start");
// WiFi.scanNetworks will return the number of networks found
int n = WiFi.scanNetworks();
int y = sizeof(ssidArray) / sizeof(ssidArray[0]);
Serial.println("scan done");
if (n == 0) {
Serial.println("no networks found");
} else {
Serial.print(n);
Serial.println(" networks found");
for (int i = 0; i < n; ++i) {
// Print SSID and RSSI for each network found
Serial.print(i + 1);
Serial.print(": ");
Serial.print(WiFi.SSID(i));
Serial.print(" (");
Serial.print(WiFi.RSSI(i));
Serial.print(")");
Serial.println((WiFi.encryptionType(i) == WIFI_AUTH_OPEN)?" ":"*");
for (int x = 0; x < y; x++){
if (WiFi.SSID(i) == ssidArray[x]){ // we found a network corresponding to a network in our known list
ssid = ssidArray[x];
password = passwordArray[x];
Serial.print("connect to network: ");
Serial.println(ssidArray[x]);
i = n; // the loop work is done, we connect to the first network and exit...
}
}
delay(10);
}
}
Serial.println("");
}
void initWebSocket() {
ws.onEvent(onEvent);
server.addHandler(&ws);
}
int checkAnalog(int analogPinToTest, int noSamples){ // the op-amp outputs a square wave for the most part so we find the peak in 500 tries ;)
int maximum = 0;
int minimum = 5000;
int value;
for (int i = 0; i <= noSamples; i++) {
value = analogRead(analogPinToTest); // pilotPin or currentSensPin
if (value <= minimum){
minimum = value;
minvaluePilot = minimum;
}
if (value >= maximum){
maximum = value;
}
}
return maximum;
}
int chargingPWM(int ampsToConvert){
//float pwmsignal = ampsToConvert/0.05859375; // 0.05859375 is 1/1024 of 1A when using 10bit resolution
float pwmsignal = (ampsToConvert + 3)*17.06667; // is 1/1024 of 1A when using 10bit resolution, My car is for some reason 3A steps wrong on pwm pilot signal and only starts charging when I ask for 9A and then charges at 6A
return (round(pwmsignal)-1);
}
void onEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) {
switch (type) {
case WS_EVT_CONNECT:
Serial.printf("WebSocket client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str());
globalClient = client;
break;
case WS_EVT_DISCONNECT:
Serial.printf("WebSocket client #%u disconnected\n", client->id());
globalClient = NULL;
break;
case WS_EVT_DATA:
handleWebSocketMessage(arg, data, len);
break;
case WS_EVT_PONG:
case WS_EVT_ERROR:
break;
}
}
void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) {
AwsFrameInfo *info = (AwsFrameInfo*)arg;
if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
data[len] = 0;
message = (char*)data;
if (message.indexOf("1s") >= 0) {
amperes1 = message.substring(2);
chargingCurrent = amperes1.toInt();
ledcWrite(PWM1_Ch, chargingPWM(chargingCurrent));
Serial.println(chargingCurrent);
Serial.print(getSliderValues());
notifyClients(getSliderValues());
}
/*
if (message.indexOf("2s") >= 0) {
sliderValue2 = message.substring(2);
dutyCycle2 = map(sliderValue2.toInt(), 0, 100, 0, 255);
Serial.println(dutyCycle2);
Serial.print(getSliderValues());
notifyClients(getSliderValues());
}
if (message.indexOf("3s") >= 0) {
sliderValue3 = message.substring(2);
dutyCycle3 = map(sliderValue3.toInt(), 0, 100, 0, 255);
Serial.println(dutyCycle3);
Serial.print(getSliderValues());
notifyClients(getSliderValues());
}
*/
if (strcmp((char*)data, "getValues") == 0) {
notifyClients(getSliderValues());
}
if (strcmp((char*)data, "setStateCustomOff") == 0) { // usage in webpage javascript: websocket.send("setStateCustomOff");
pilotState=STATE_CUSTOM_OFF;
notifyClients(getSliderValues());
Serial.println("State: STATE_CUSTOM_OFF");
}
if (strcmp((char*)data, "setStateCustomOn") == 0) { // usage in webpage javascript: websocket.send("setStateCustomOn");
// if car is charging, then we do not want to change the state to anything else than off
// setting pwm to 1023 in charging mode will result in car to abort charging and try to start again, and then go to charging error mode
if (!(pilotState == STATE_C)){ // this code will run if car is not charging
pilotState=STATE_CUSTOM_ON;
digitalWrite(RELAY_PIN, LOW); // start with relay off
PWM1_DutyCycle = 1023; // turn pwm to constant on, +12v on pilot so we do not get EVSE ERROR code (STATE_F)
ledcWrite(PWM1_Ch, PWM1_DutyCycle);
notifyClients(getSliderValues());
Serial.println("State: STATE_CUSTOM_ON");
}
}
}
}
void notifyClients(String sliderValues) {
ws.textAll(sliderValues);
}
void printDisplayData(){
display.clearDisplay();
display.setTextSize(1); // Normal 1:1 pixel scale
display.setTextColor(WHITE); // Draw white text
display.setCursor(0,2);
display.print("SSID: ");
display.println(ssid);
display.println("Http://charger.local");
display.print("charging at ");
display.println(ampValue);
display.print("SSR temp is: ");
display.println(temperatureC);
//display.print("Voltage is: ");
//voltage = (3.3 / 4096) * pilotValue;
//display.println(voltage);
display.print("Setpoint at ");
display.print(chargingCurrent);
display.println(" Amps");
display.println(stateStr); // print status on screen
display.println(WiFi.localIP());
display.display();
//delay(2);
}
void CheckState(ChargingState oldChargingState, int adc_value){
ChargingState newState;
int ampsPWM = chargingPWM(chargingCurrent);
Serial.print("pwm for charging = ");
Serial.println(ampsPWM);
if (adc_value >= 3779 && adc_value < 4096){
newState = STATE_A;
}
else if (adc_value >= 3150 && adc_value < 3779){
newState = STATE_B;
}
else if (adc_value >= 2618 && adc_value < 3150){
newState = STATE_C;
}
else if (adc_value >= 2166 && adc_value < 2618){
newState = STATE_D;
}
else if (adc_value >= 1700 && adc_value < 2166){
newState = STATE_E;
}
else if (adc_value >= 0 && adc_value < 1700){
newState = STATE_F;
}
if (!(oldChargingState == newState)){
pilotState = newState;
switch (pilotState){
case STATE_A:
stateStr = "Not Connected";
digitalWrite(RELAY_PIN, LOW);
PWM1_DutyCycle = 1023; // turn off pwm, constant on
ledcWrite(PWM1_Ch, PWM1_DutyCycle);
//digitalWrite(PILOT_GPIO, HIGH); // set +12V DC on pilot
break;
case STATE_B:
stateStr = "Connected";
//digitalWrite(PILOT_GPIO, LOW); // turn off DC voltage
digitalWrite(RELAY_PIN, LOW);
// Advertize 1kHz square wave and wait until EV goes to charging mode
PWM1_DutyCycle = ampsPWM; // % of 1023 max = Square wave
ledcWrite(PWM1_Ch, PWM1_DutyCycle);
Serial.println(ampsPWM);
delay(20);
break;
case STATE_C:
//PWM1_DutyCycle = 0; // turn off pwm
//ledcWrite(PWM1_Ch, PWM1_DutyCycle);
//delay(100);
// Advertize charging capacity
PWM1_DutyCycle = ampsPWM; // % of 1023 max =14A
ledcWrite(PWM1_Ch, PWM1_DutyCycle);
delay(10); // simulate relay closing time
digitalWrite(RELAY_PIN, HIGH);
stateStr = "Charging";
break;
case STATE_D:
stateStr = "Ventilation Requred";
digitalWrite(RELAY_PIN, LOW); // no charging
// Advertize charging capacity
PWM1_DutyCycle = ampsPWM; // turn off pwm
ledcWrite(PWM1_Ch, PWM1_DutyCycle);
break;
case STATE_E:
stateStr = "No POWER,";
stateStr += '\n';
stateStr += " Ready to connect";
digitalWrite(RELAY_PIN, LOW); // no charging
PWM1_DutyCycle = 1023; // // set +12V DC on pilot
ledcWrite(PWM1_Ch, PWM1_DutyCycle);
break;
case STATE_F:
stateStr = "--- EVSE ERROR ---";
digitalWrite(RELAY_PIN, LOW); // no charging
// Advertize charging capacity
PWM1_DutyCycle = 0; // turn off pwm
ledcWrite(PWM1_Ch, PWM1_DutyCycle);
break;
}
}
printDisplayData();
}
void setup() {
Serial.begin(115200);
// SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3D)) { // Address 0x3D for 128x64
Serial.println(F("SSD1306 allocation failed"));
for(;;); // Don't proceed, loop forever
}
// OLED reset screen:
/*
pinMode(27, OUTPUT);
digitalWrite(27, LOW);
delay(10);
digitalWrite(27, HIGH);
*/
// Start the DS18B20 sensor
sensors.begin();
// Clear the buffer
display.clearDisplay();
display.println("Bil-lader V_1");
display.display();
delay(500);
display.clearDisplay();
// Display bitmap splash for 2 sec
display.drawBitmap(0, 0, FluidSensorFluidSensorTechnology, 128, 64, WHITE);
display.display();
//delay(2000); // wifi searching takes time
display.clearDisplay();
// Set WiFi to station mode and disconnect from an AP if it was previously connected
WiFi.mode(WIFI_STA);
WiFi.disconnect();
delay(100);
Serial.println("finding network to connect to...");
scanNet();
WiFi.mode(WIFI_AP);
// *********** WIFI **************
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.println("Connecting to WiFi..");
}
if(!MDNS.begin("Charger")) {
Serial.println("Error starting mDNS");
return;
}
Serial.println(WiFi.localIP());
initWebSocket();
// Add service to MDNS-SD
MDNS.addService("http", "tcp", 80);
// Route for root / web page
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send_P(200, "text/html", index_html, processor);
});
server.on("/hello", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(200, "text/plain", "Hello World");
});
// Start TCP (HTTP) server
server.begin();
Serial.println("TCP server started");
//********************************
pinMode(34, INPUT);
pinMode(RELAY_PIN, OUTPUT);
digitalWrite(RELAY_PIN, LOW);
// initialize pwm pin at defined freq and res
ledcAttachPin(PILOT_GPIO, PWM1_Ch);
ledcSetup(PWM1_Ch, PWM1_Freq, PWM1_Res);
}
void loop() {
// Reading pilot voltage value
pilotValue = checkAnalog(pilotPin, 400); // analog is on square wave, we must get the maximum (and minimum)
// Only display charging amp value if we are charging, else display 0.00A
if (pilotState == STATE_C){
ampValue = getChargingCurrent(CURRENT_SENS_GPIO, 1000);
} else{
ampValue =0.00;
}
//Serial.println(pilotValue);
//Serial.println(minvaluePilot);
if (pilotState == STATE_CUSTOM_OFF){ // if Home Automation sets charger to off then we will not change states
stateStr = "Charging on Schedule";
digitalWrite(RELAY_PIN, LOW); // no charging
// Advertize charging capacity
PWM1_DutyCycle = 0; // turn off pwm
ledcWrite(PWM1_Ch, PWM1_DutyCycle);
} else { // we have either not got a custom_off state yet or it has been released from STATE_CUSTOM_OFF
CheckState(pilotState, pilotValue); // check connection state of charger and do what the car asks...
}
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= interval) {
// save the last time you sent message to the webbrowser
previousMillis = currentMillis;
printDisplayData(); // Print status on OLED
sensors.requestTemperatures();
temperatureC = sensors.getTempCByIndex(0);
if(globalClient != NULL && globalClient->status() == WS_CONNECTED){
globalClient->text(sendSocketUpdates()); // send json data
}
}
}
Bare-bone code without display calls or wifi can be posted if there is someone intressed...
then the only output is on the Serial console. Charging fixed to a rate or set by serial command.
The output of the web page to control the charger so far: