EVSE charger miev, leaf, tesla... 6-16A ESP32

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> &deg;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:




1 Like

It looks like a lot of work but you did a great job!

Thanks, the charger that came with the car burned up and the dealer did not have any chargers in stock, so I had to whip up a charger with things I had + could order in less than 2 days. will probably be without original charger for at least a month (guaranty thing, so slow service).
Took 3 days to get a working code. I did use a defect SSR at first and I did not found out until late on the second day, my car was not happy and gave me fault code and denied charging. After I fixed the SSR's then things started to look like the workflow for SAE J1772, IEC 62196, standard was being implemented by my code after all....
My car still thew me a few suprises, it tested the state machine 2-3 times sometimes and denied charging if the ESP32 did not follow 12v-9v-3v-9v-3v with SSR powering up and down 2 times. I guess the car was testing stuck relay before charging, my first implementation had a debounce delay from state to state but my car was not happy with such foolery and threw me an error and denied charging... (this is not part of the J1772 standard).

There are a few bugs still, PWM to pilot does not update while charging, and I need a better web page update routine, page updates every 4 secs, and if you change slider, you risk having the slider reset when you are sliding it. I have fixed the first bug on my charger and I will see if I can fix the latter during the weekend, either way I will upload new code on sunday probably.
In my revised code I added a small bug/quirk as my car (i-miev) does not like the PWM and does not start to charge until at 9Amps requested and then only charge at 6A, and at the16A mark it will only charge at 12-13A, but if I offset by 3A so it pwms from 9A and up. Then by offsetting, my car charges up to 16A measured on the current clamp. My car can only take 16A max in, so if I PWM at 32 max, my car still takes only 15-16A while charging. I will test my charger later this month on a Tesla model S on 32A outlet.

1 Like

Thanks for the update!