Este projeto mostra como transformar um ESP32 em um simulador de EVSE capaz de se comunicar com um servidor OCPP 1.6 por WebSocket seguro (WSS). O simulador possui Wi-Fi, LCD 20×4 com interface I²C e dois botões para selecionar e executar as funções OCPP.

1. O objetivo do simulador
Um EVSE real normalmente possui um controlador que executa as funções locais do carregador e se comunica com um sistema de gerenciamento através de OCPP. Neste projeto o ESP32 não controla a potência de carregamento: ele simula o comportamento de um EVSE para permitir testes do servidor OCPP.
O menu implementado permite executar, individualmente, estas operações:
| Função | Comando OCPP | Finalidade |
|---|---|---|
| CONNECT | WebSocket/WSS | Conectar ao servidor |
| BOOT | BootNotification | Identificar o EVSE |
| HEARTBEAT | Heartbeat | Testar/manter a comunicação |
| STATUS | StatusNotification | Informar o estado do carregador |
| AUTHORIZE | Authorize | Solicitar autorização do cartão/tag |
| START | StartTransaction | Iniciar uma transação |
| METER | MeterValues | Enviar uma medição |
| STOP | StopTransaction | Encerrar a transação |
2. Hardware utilizado
- ESP32
- LCD 20×4 com backpack I²C
- Dois botões
- Fonte/alimentação adequada para o ESP32 e LCD
- Servidor OCPP 1.6 acessível pela rede local
| Função | GPIO ESP32 |
|---|---|
| I²C SDA | GPIO 21 |
| I²C SCL | GPIO 22 |
| SELECT | GPIO 27 |
| SEND | GPIO 14 |
| Endereço I²C do LCD | 0x27 |
0x27.3. Bibliotecas
#include <Arduino.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <WebSocketsClient.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <ESPDateTime.h>A biblioteca LiquidCrystal_I2C 2.0.0 foi utilizada no protótipo. O Arduino IDE pode emitir um warning informando que a biblioteca declara suporte a todas as arquiteturas. Durante o desenvolvimento esse aviso não impediu a execução; o LCD foi testado e funcionou.
4. Ligação do LCD
O primeiro diagnóstico utilizado foi um scanner I²C. Isso é uma boa prática antes de investigar a biblioteca do LCD.
Wire.begin(21, 22);O scanner encontrou:
I2C device encontrado em 0x27
Total de dispositivos: 1Com isso foi possível separar o diagnóstico em duas partes: primeiro confirmar o barramento I²C e depois testar a biblioteca do LCD.
5. Configuração do servidor OCPP
O simulador utiliza WebSocket seguro. No código do projeto o servidor está configurado com o endereço:
const char* SERVER_HOST = "192.168.68.114";
const uint16_t SERVER_PORT = 3001;
const char* SERVER_PATH = "/";A conexão é iniciada por beginSSL(). Para este projeto, o servidor OCPP existente foi mantido sem alterações.
6. Como funciona uma mensagem OCPP
O simulador monta mensagens OCPP CALL no formato:
[2,"MessageId","Action",Payload]Por exemplo:
[2,"b67ae272-b1d9-475b-a181-9c22d39154c3",
"StartTransaction",
{"action":"StartTransaction", ...}]O servidor responde com uma mensagem CALLRESULT, cujo primeiro campo é 3:
[3,"MessageId",Payload]7. O ponto importante: transactionId
Quando o simulador envia StartTransaction, o servidor cria o identificador da transação. No teste realizado, o servidor retornou:
[3,"...","transactionId":"17898343970563", ...]O ESP32 armazena esse valor em:
String transactionId = "";Quando o ID é enviado novamente ao servidor em MeterValues e StopTransaction, ele é colocado no JSON sem aspas:
"transactionId":17898343970563Essa abordagem reproduziu corretamente o comportamento do cliente Node.js usado como referência no projeto.
8. Funções do programa
functionName()
Converte o valor do enum selecionado no menu para o nome que será apresentado no Serial Monitor e no LCD.
lcdClearLine()
Limpa uma linha do LCD escrevendo 20 espaços.
lcdLine()
Escreve texto em uma linha, limitando-o ao tamanho do display.
updateLCD()
Reconstrói a tela principal do simulador com a função selecionada, o estado WSS e a mensagem de status.
showStatus()
Altera a mensagem de status e chama updateLCD().
generateUUID()
Gera o Message ID usado pelas mensagens OCPP.
connectWiFi()
Conecta o ESP32 à rede Wi-Fi e aguarda a obtenção de um endereço IP.
setupDateTime()
Inicializa a sincronização de data e hora através da biblioteca ESPDateTime.
sendOcpp()
É uma das funções centrais do simulador. Recebe o nome do comando OCPP e seu payload, gera o Message ID, monta o frame CALL e o envia pelo WebSocket.
connectOCPP()
Inicializa o WebSocket seguro, registra o callback de eventos e configura o intervalo de reconexão.
bootNotification()
Envia ao servidor as informações de identificação e configuração do EVSE.
heartbeat()
Envia um Heartbeat e recebe do servidor o horário atual.
statusNotification()
Informa ao servidor o estado atual do carregador.
authorize()
Envia o ID tag utilizado para iniciar a autorização da sessão.
startTransaction()
Solicita ao servidor a criação de uma nova transação. A resposta do servidor contém o transactionId que será utilizado nas mensagens seguintes.
meterValues()
Envia o valor de energia medido pelo EVSE. No protótipo foi utilizado um valor fixo de 1000 Wh para a simulação.
stopTransaction()
Encerra a transação utilizando o mesmo transactionId criado pelo servidor.
processOcppResponse()
Recebe as respostas do servidor e procura informações importantes no JSON. A rotina faz a extração do transactionId retornado pelo StartTransaction e também identifica respostas como Accepted, Rejected e currentTime.
webSocketEvent()
É chamado pela biblioteca WebSocket quando ocorre conexão, desconexão ou chegada de uma mensagem de texto.
executeFunction()
Faz a associação entre a opção selecionada no menu e a função que deve ser executada.
selectFunction()
Avança para a próxima opção do menu. Quando chega à última opção, retorna para CONNECT.
sendFunction()
Executa a função atualmente selecionada quando o botão SEND é pressionado.
setup()
Inicializa todos os periféricos e serviços: Serial, gerador aleatório, I²C, LCD, botões, Wi-Fi, relógio e tela inicial.
loop()
É o ciclo principal do ESP32. Mantém o WebSocket ativo através de webSocket.loop() e monitora os botões SELECT e SEND. O código também aplica debounce para evitar múltiplos acionamentos.
9. Sequência de operação
A sequência de teste utilizada no protótipo foi:
- CONNECT — conecta ao servidor WSS.
- BOOT — envia BootNotification.
- HEARTBEAT — verifica a comunicação.
- STATUS — envia StatusNotification.
- AUTHORIZE — solicita autorização da tag.
- START — inicia a transação.
- METER — envia a medição.
- STOP — encerra a transação.
10. Controles do protótipo
| Botão | GPIO | Função |
|---|---|---|
| SELECT | 27 | Seleciona a próxima operação |
| SEND | 14 | Executa a operação selecionada |
Os botões utilizam INPUT_PULLUP. Portanto, o acionamento ocorre quando o GPIO passa de HIGH para LOW.
11. Código-fonte completo
O código abaixo é o programa completo utilizado no protótipo. Ele inclui Wi-Fi, WSS, OCPP 1.6, LCD 20×4, menu, botões e tratamento do transactionId. Os comentários foram colocados para facilitar o estudo de cada rotina.
#include <Arduino.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <WebSocketsClient.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <ESPDateTime.h>
// ============================================================
// CONFIGURAÇÃO WIFI
// ============================================================
const char* WIFI_SSID = "seuwifi";
const char* WIFI_PASSWORD = "suasenha";
// ============================================================
// SERVIDOR OCPP
// ============================================================
const char* SERVER_HOST = "192.168.68.114";
const uint16_t SERVER_PORT = 3001;
const char* SERVER_PATH = "/";
// ============================================================
// LCD 20 x 4
// ============================================================
#define LCD_ADDRESS 0x27
#define LCD_COLUMNS 20
#define LCD_ROWS 4
LiquidCrystal_I2C lcd(
LCD_ADDRESS,
LCD_COLUMNS,
LCD_ROWS
);
// I2C
#define LCD_SDA 22
#define LCD_SCL 21
// ============================================================
// BOTÕES
// ============================================================
#define BUTTON_SELECT 27
#define BUTTON_SEND 14
// ============================================================
// WEBSOCKET
// ============================================================
WebSocketsClient webSocket;
// ============================================================
// DADOS DO EVSE
// ============================================================
const char* CHARGER_ID = "EVSE1";
const char* ID_TAG = "TAG123";
const char* CHARGE_POINT_SERIAL = "EVSE001";
const char* CHARGE_POINT_MODEL = "ModelSingle32A";
const char* CHARGE_BOX_SERIAL = "ABC123";
const char* FIRMWARE_VERSION = "1.0";
const char* ICCID = "ICCID123";
const char* IMSI = "IMSI456";
const char* METER_TYPE = "TypeA";
const char* METER_SERIAL = "789012";
const int CONNECTOR_ID = 1;
const char* VEHICLE_ID = "Placa123";
const int METER_VALUE = 1000;
// ============================================================
// TRANSACTION ID
// ============================================================
String transactionId = "";
// ============================================================
// ESTADO WSS
// ============================================================
bool wssConnected = false;
// ============================================================
// MENU
// ============================================================
enum Function {
FUNC_CONNECT,
FUNC_BOOT,
FUNC_HEARTBEAT,
FUNC_STATUS,
FUNC_AUTHORIZE,
FUNC_START,
FUNC_METER,
FUNC_STOP
};
Function selectedFunction = FUNC_CONNECT;
// ============================================================
// NOME DAS FUNÇÕES
// ============================================================
// functionName(): Retorna o nome textual da função atualmente selecionada no menu.
const char* functionName(Function function)
{
switch (function) {
case FUNC_CONNECT:
return "CONNECT";
case FUNC_BOOT:
return "BOOT";
case FUNC_HEARTBEAT:
return "HEARTBEAT";
case FUNC_STATUS:
return "STATUS";
case FUNC_AUTHORIZE:
return "AUTHORIZE";
case FUNC_START:
return "START";
case FUNC_METER:
return "METER";
case FUNC_STOP:
return "STOP";
}
return "UNKNOWN";
}
// ============================================================
// MESSAGE ID
// ============================================================
String lastMessageId = "";
// ============================================================
// MENSAGEM TEMPORÁRIA DO LCD
// ============================================================
String lcdStatus = "Press SEND";
// ============================================================
// CONTROLE DOS BOTÕES
// ============================================================
bool lastSelectState = HIGH;
bool lastSendState = HIGH;
unsigned long lastButtonTime = 0;
const unsigned long BUTTON_DEBOUNCE = 250;
// ============================================================
// LCD - LIMPA UMA LINHA
// ============================================================
void lcdClearLine(uint8_t row)
{
lcd.setCursor(0, row);
lcd.print(
" "
);
}
// ============================================================
// LCD - ESCREVE UMA LINHA
// ============================================================
void lcdLine(
uint8_t row,
const String& text
)
{
lcdClearLine(row);
lcd.setCursor(0, row);
lcd.print(
text.substring(
0,
LCD_COLUMNS
)
);
}
// ============================================================
// LCD - TELA PRINCIPAL
// ============================================================
// updateLCD(): Atualiza a tela principal mostrando o título do simulador, a função selecionada, o estado do WSS e a última mensagem de status.
void updateLCD()
{
lcdLine(
0,
String("EVSE1 OCPP 1.6")
);
lcdLine(
1,
String("> ") +
functionName(selectedFunction)
);
String wss;
if (wssConnected)
wss = "WSS: CONNECTED";
else
wss = "WSS: DISCONNECTED";
lcdLine(
2,
wss
);
lcdLine(
3,
lcdStatus
);
}
// ============================================================
// LCD - MOSTRA STATUS TEMPORÁRIO
// ============================================================
void showStatus(
const String& status
)
{
lcdStatus = status;
updateLCD();
}
// ============================================================
// GERA UUID
// ============================================================
// generateUUID(): Gera o identificador usado como Message ID nas mensagens OCPP. O código cria 36 caracteres e força o caractere da posição 14 para a versão 4.
String generateUUID()
{
const char* hex =
"0123456789abcdef";
char uuid[37];
for (int i = 0; i < 36; i++) {
if (
i == 8 ||
i == 13 ||
i == 18 ||
i == 23
) {
uuid[i] = '-';
}
else {
uuid[i] =
hex[random(0, 16)];
}
}
uuid[36] = '\0';
// UUID versão 4
uuid[14] = '4';
return String(uuid);
}
// ============================================================
// WIFI
// ============================================================
// connectWiFi(): Inicializa a conexão Wi-Fi, aguarda até WL_CONNECTED e mostra no Serial Monitor o endereço IP obtido.
void connectWiFi()
{
lcdLine(0, "EVSE1 OCPP 1.6");
lcdLine(1, "Connecting WiFi...");
lcdLine(2, "");
lcdLine(3, "");
Serial.println();
Serial.println(
"================================"
);
Serial.println(
"CONECTANDO AO WIFI"
);
Serial.println(
"================================"
);
WiFi.begin(
WIFI_SSID,
WIFI_PASSWORD
);
while (
WiFi.status() != WL_CONNECTED
) {
delay(500);
Serial.print(".");
}
Serial.println();
Serial.println(
"WiFi conectado"
);
Serial.print(
"IP ESP32: "
);
Serial.println(
WiFi.localIP()
);
lcdLine(
1,
"WiFi: CONNECTED"
);
lcdLine(
2,
WiFi.localIP().toString()
);
delay(1500);
}
// ============================================================
// DATA/HORA
// ============================================================
// setupDateTime(): Inicializa a biblioteca ESPDateTime e verifica se o horário obtido por NTP é válido.
void setupDateTime()
{
DateTime.setTimeZone(
"UTC+3"
);
DateTime.begin();
if (
!DateTime.isTimeValid()
) {
Serial.println(
"Erro ao obter hora NTP"
);
}
else {
Serial.println(
"Hora sincronizada"
);
Serial.println(
DateTime.toString()
);
}
}
// ============================================================
// ENVIA MENSAGEM OCPP
// ============================================================
String sendOcpp(
const char* action,
const String& payload
)
{
if (!wssConnected) {
Serial.println(
"WSS não conectado"
);
showStatus(
"WSS OFF"
);
return "";
}
lastMessageId =
generateUUID();
String message;
message = "[2,\"";
message += lastMessageId;
message += "\",\"";
message += action;
message += "\",";
message += payload;
message += "]";
Serial.println();
Serial.println(
"================================"
);
Serial.print(
"-> "
);
Serial.println(
action
);
Serial.println(
message
);
Serial.println(
"================================"
);
webSocket.sendTXT(
message
);
showStatus(
String("TX: ") + action
);
return lastMessageId;
}
// ============================================================
// CONNECT
// ============================================================
// connectOCPP(): Configura a conexão WebSocket seguro com o servidor OCPP usando beginSSL(), registra o callback de eventos e define reconexão automática.
void connectOCPP()
{
Serial.println();
Serial.println(
"Conectando WSS..."
);
showStatus(
"WSS connecting..."
);
/*
* Para o primeiro teste utilizamos
* beginSSL().
*
* Depois podemos instalar o CA
* utilizado pelo seu servidor.
*/
webSocket.beginSSL(
SERVER_HOST,
SERVER_PORT,
SERVER_PATH
);
webSocket.onEvent(
webSocketEvent
);
webSocket.setReconnectInterval(
5000
);
}
// ============================================================
// BOOT NOTIFICATION
// ============================================================
// bootNotification(): Monta e envia a mensagem BootNotification com os dados de identificação do EVSE, firmware, SIM e medidor.
void bootNotification()
{
String payload = "{";
payload +=
"\"action\":\"BootNotification\",";
payload +=
"\"chargerId\":\"";
payload +=
CHARGER_ID;
payload += "\",";
payload +=
"\"chargepointserianumber\":\"";
payload +=
CHARGE_POINT_SERIAL;
payload += "\",";
payload +=
"\"chargePointModel\":\"";
payload +=
CHARGE_POINT_MODEL;
payload += "\",";
payload +=
"\"chargeBoxSerialNumber\":\"";
payload +=
CHARGE_BOX_SERIAL;
payload += "\",";
payload +=
"\"firmwareVersion\":\"";
payload +=
FIRMWARE_VERSION;
payload += "\",";
payload +=
"\"iccid\":\"";
payload +=
ICCID;
payload += "\",";
payload +=
"\"imsi\":\"";
payload +=
IMSI;
payload += "\",";
payload +=
"\"meterType\":\"";
payload +=
METER_TYPE;
payload += "\",";
payload +=
"\"meterSerialNumber\":\"";
payload +=
METER_SERIAL;
payload += "\"";
payload += "}";
sendOcpp(
"BootNotification",
payload
);
}
// ============================================================
// HEARTBEAT
// ============================================================
// heartbeat(): Envia Heartbeat com payload vazio para manter a comunicação e permitir que o servidor responda com currentTime.
void heartbeat()
{
sendOcpp(
"Heartbeat",
"{}"
);
}
// ============================================================
// STATUS NOTIFICATION
// ============================================================
// statusNotification(): Envia o estado atual do carregador ao servidor usando StatusNotification.
void statusNotification()
{
String payload = "{";
payload +=
"\"chargerId\":\"";
payload +=
CHARGER_ID;
payload += "\",";
payload +=
"\"status\":\"ATIVO\"";
payload += "}";
sendOcpp(
"StatusNotification",
payload
);
}
// ============================================================
// AUTHORIZE
// ============================================================
// authorize(): Envia o ID tag TAG123 para autorização da sessão.
void authorize()
{
String payload = "{";
payload +=
"\"action\":\"Authorize\",";
payload +=
"\"chargerId\":\"";
payload +=
CHARGER_ID;
payload += "\",";
payload +=
"\"idTag\":\"";
payload +=
ID_TAG;
payload += "\"";
payload += "}";
sendOcpp(
"Authorize",
payload
);
}
// ============================================================
// START TRANSACTION
// ============================================================
// startTransaction(): Monta StartTransaction com chargerId, idTag, connectorId, horário de início e identificação do veículo.
void startTransaction()
{
String startTime =
DateTime.toString();
String payload = "{";
payload +=
"\"action\":\"StartTransaction\",";
payload +=
"\"chargerId\":\"";
payload +=
CHARGER_ID;
payload += "\",";
payload +=
"\"idTag\":\"";
payload +=
ID_TAG;
payload += "\",";
payload +=
"\"connectorId\":";
payload +=
CONNECTOR_ID;
payload += ",";
payload +=
"\"startTime\":\"";
payload +=
startTime;
payload += "\",";
payload +=
"\"vehicleId\":\"";
payload +=
VEHICLE_ID;
payload += "\"";
payload += "}";
sendOcpp(
"StartTransaction",
payload
);
}
// ============================================================
// METER VALUES
// ============================================================
// meterValues(): Envia a medição de energia associada à transação. O transactionId capturado do servidor é inserido como número JSON, sem aspas.
void meterValues()
{
if (transactionId.length() == 0) {
Serial.println(
"transactionId não disponível"
);
showStatus(
"NO Transaction"
);
return;
}
String timestamp =
DateTime.toString();
String payload = "{";
payload +=
"\"chargerId\":\"";
payload +=
CHARGER_ID;
payload += "\",";
payload +=
"\"transactionId\":\"";
payload +=
transactionId;
payload += "\",";
payload +=
"\"meterValue\":[";
payload += "{";
payload +=
"\"timestamp\":\"";
payload +=
timestamp;
payload += "\",";
payload +=
"\"sampledValue\":[";
payload += "{";
payload +=
"\"measurand\":\"Energy.Active.Import.Register\",";
payload +=
"\"value\":\"";
payload +=
METER_VALUE;
payload += "\",";
payload +=
"\"unit\":\"Wh\",";
payload +=
"\"context\":\"Sample.Periodic\",";
payload +=
"\"format\":\"Raw\"";
payload += "}";
payload += "]";
payload += "}";
payload += "]";
payload += "}";
sendOcpp(
"MeterValues",
payload
);
}
// ============================================================
// STOP TRANSACTION
// ============================================================
// stopTransaction(): Encerra a transação usando o mesmo transactionId recebido em StartTransaction e envia a medição final.
void stopTransaction()
{
if (transactionId.length() == 0) {
Serial.println(
"transactionId não disponível"
);
showStatus(
"NO Transaction"
);
return;
}
String payload = "{";
payload +=
"\"chargerId\":\"";
payload +=
CHARGER_ID;
payload += "\",";
payload +=
"\"transactionId\":\"";
payload +=
transactionId;
payload += "\",";
payload +=
"\"meterValue\":";
payload +=
METER_VALUE;
payload += "}";
sendOcpp(
"StopTransaction",
payload
);
}
// ============================================================
// PROCESSA RESPOSTA OCPP
// ============================================================
void processOcppResponse(
uint8_t* payload,
size_t length
)
{
String response;
for (
size_t i = 0;
i < length;
i++
) {
response +=
(char)payload[i];
}
Serial.println();
Serial.println(
"<- SERVIDOR"
);
Serial.println(
response
);
// --------------------------------------------------------
// START TRANSACTION
// --------------------------------------------------------
if (
response.indexOf(
"\"transactionId\""
) >= 0
) {
int p =
response.indexOf(
"\"transactionId\""
);
p =
response.indexOf(
":",
p
);
if (p >= 0) {
p++;
while (
p < response.length() &&
response[p] == ' '
) {
p++;
}
int end = p;
while (
end < response.length() &&
isDigit(
response[end]
)
) {
end++;
}
String id =
response.substring(
p,
end
);
// O servidor retorna transactionId como STRING
transactionId = id;
Serial.print(
"transactionId = "
);
Serial.println(
transactionId
);
showStatus(
String("ID: ") +
transactionId
);
return;
}
}
// --------------------------------------------------------
// ACCEPTED
// --------------------------------------------------------
if (
response.indexOf(
"\"Accepted\""
) >= 0
) {
showStatus(
"RX: Accepted"
);
return;
}
// --------------------------------------------------------
// REJECTED
// --------------------------------------------------------
if (
response.indexOf(
"\"Rejected\""
) >= 0
) {
showStatus(
"RX: Rejected"
);
return;
}
// --------------------------------------------------------
// HEARTBEAT
// --------------------------------------------------------
if (
response.indexOf(
"\"currentTime\""
) >= 0
) {
showStatus(
"RX: currentTime"
);
return;
}
// --------------------------------------------------------
// OUTROS
// --------------------------------------------------------
showStatus(
"RX: response"
);
}
// ============================================================
// WEBSOCKET EVENT
// ============================================================
void webSocketEvent(
WStype_t type,
uint8_t* payload,
size_t length
)
{
switch (type) {
case WStype_DISCONNECTED:
wssConnected = false;
Serial.println(
"WSS DESCONECTADO"
);
showStatus(
"WSS DISCONNECTED"
);
break;
case WStype_CONNECTED:
wssConnected = true;
Serial.println(
"WSS CONECTADO"
);
showStatus(
"WSS CONNECTED"
);
break;
case WStype_TEXT:
processOcppResponse(
payload,
length
);
break;
default:
break;
}
}
// ============================================================
// EXECUTA FUNÇÃO
// ============================================================
// executeFunction(): Despacha a função selecionada para a rotina OCPP correspondente.
void executeFunction()
{
switch (
selectedFunction
) {
case FUNC_CONNECT:
connectOCPP();
break;
case FUNC_BOOT:
bootNotification();
break;
case FUNC_HEARTBEAT:
heartbeat();
break;
case FUNC_STATUS:
statusNotification();
break;
case FUNC_AUTHORIZE:
authorize();
break;
case FUNC_START:
startTransaction();
break;
case FUNC_METER:
meterValues();
break;
case FUNC_STOP:
stopTransaction();
break;
}
}
// ============================================================
// SELECT
// ============================================================
// selectFunction(): Avança ciclicamente pelas oito opções do menu: CONNECT, BOOT, HEARTBEAT, STATUS, AUTHORIZE, START, METER e STOP.
void selectFunction()
{
selectedFunction =
(Function)(
(
selectedFunction + 1
) % 8
);
Serial.println();
Serial.print(
"SELECT: "
);
Serial.println(
functionName(
selectedFunction
)
);
lcdStatus =
"Press SEND";
updateLCD();
}
// ============================================================
// SEND
// ============================================================
// sendFunction(): Executa a função atualmente selecionada quando o botão SEND é pressionado.
void sendFunction()
{
Serial.println();
Serial.print(
"SEND: "
);
Serial.println(
functionName(
selectedFunction
)
);
executeFunction();
}
// ============================================================
// SETUP
// ============================================================
// setup(): Inicializa Serial, geração aleatória, I²C, LCD, botões, Wi-Fi, horário e a tela inicial.
void setup()
{
Serial.begin(
115200
);
delay(1000);
// --------------------------------------------------------
// RANDOM
// --------------------------------------------------------
randomSeed(
analogRead(0)
);
// --------------------------------------------------------
// I2C
// --------------------------------------------------------
Wire.begin(
LCD_SDA,
LCD_SCL
);
// --------------------------------------------------------
// LCD
// --------------------------------------------------------
lcd.init();
lcd.backlight();
lcd.clear();
lcdLine(
0,
"EVSE1 OCPP 1.6"
);
lcdLine(
1,
"Inicializando..."
);
delay(1000);
// --------------------------------------------------------
// BOTÕES
// --------------------------------------------------------
pinMode(
BUTTON_SELECT,
INPUT_PULLUP
);
pinMode(
BUTTON_SEND,
INPUT_PULLUP
);
// --------------------------------------------------------
// WIFI
// --------------------------------------------------------
connectWiFi();
// --------------------------------------------------------
// DATA/HORA
// --------------------------------------------------------
setupDateTime();
// --------------------------------------------------------
// TELA
// --------------------------------------------------------
lcdStatus =
"Press SEND";
updateLCD();
// --------------------------------------------------------
// SERIAL
// --------------------------------------------------------
Serial.println();
Serial.println(
"========================================"
);
Serial.println(
"SIMULADOR EVSE - OCPP 1.6"
);
Serial.println(
"========================================"
);
Serial.println(
"SELECT = GPIO 27"
);
Serial.println(
"SEND = GPIO 14"
);
Serial.println(
"LCD SDA = GPIO 22"
);
Serial.println(
"LCD SCL = GPIO 21"
);
Serial.println();
Serial.print(
"Funcao: "
);
Serial.println(
functionName(
selectedFunction
)
);
}
// ============================================================
// LOOP
// ============================================================
// loop(): Mantém o WebSocket ativo e faz a leitura dos dois botões com debounce para executar SELECT e SEND.
void loop()
{
// --------------------------------------------------------
// WEB SOCKET
// --------------------------------------------------------
webSocket.loop();
// --------------------------------------------------------
// SELECT
// --------------------------------------------------------
bool selectState =
digitalRead(
BUTTON_SELECT
);
if (
lastSelectState == HIGH &&
selectState == LOW
) {
if (
millis() - lastButtonTime >
BUTTON_DEBOUNCE
) {
lastButtonTime =
millis();
selectFunction();
}
}
lastSelectState =
selectState;
// --------------------------------------------------------
// SEND
// --------------------------------------------------------
bool sendState =
digitalRead(
BUTTON_SEND
);
if (
lastSendState == HIGH &&
sendState == LOW
) {
if (
millis() - lastButtonTime >
BUTTON_DEBOUNCE
) {
lastButtonTime =
millis();
sendFunction();
}
}
lastSendState =
sendState;
delay(10);
}12. Resultado final
O projeto demonstra que um ESP32 pode ser utilizado como uma ferramenta prática para testar um servidor OCPP 1.6. O simulador permite executar os principais comandos individualmente e observar no Serial Monitor tanto as mensagens enviadas quanto as respostas do servidor.
A arquitetura também facilita a evolução do projeto. Novos comandos OCPP podem ser adicionados ao enum, à função executeFunction() e às respectivas rotinas de montagem do payload.
Projeto de laboratório: o código apresentado é um simulador de comunicação OCPP. Ele não substitui as funções de segurança, controle de potência e supervisão exigidas por um EVSE real.

Deixe um comentário