Como construir um simulador EVSE OCPP 1.6 com ESP32

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.

 

Protótipo do simulador EVSE OCPP 1.6: ESP32, LCD 20×4 e painel de comandos.

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çãoComando OCPPFinalidade
CONNECTWebSocket/WSSConectar ao servidor
BOOTBootNotificationIdentificar o EVSE
HEARTBEATHeartbeatTestar/manter a comunicação
STATUSStatusNotificationInformar o estado do carregador
AUTHORIZEAuthorizeSolicitar autorização do cartão/tag
STARTStartTransactionIniciar uma transação
METERMeterValuesEnviar uma medição
STOPStopTransactionEncerrar 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çãoGPIO ESP32
I²C SDAGPIO 21
I²C SCLGPIO 22
SELECTGPIO 27
SENDGPIO 14
Endereço I²C do LCD0x27
Importante: neste protótipo a ligação física encontrada foi SDA = GPIO 21 e SCL = GPIO 22. Um scanner I²C confirmou o dispositivo no endereço 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: 1

Com 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 = "";
Por que String? O identificador retornado possui muitos dígitos. Armazená-lo em um inteiro de 32 bits poderia provocar overflow. Internamente, portanto, ele permanece como String.

Quando o ID é enviado novamente ao servidor em MeterValues e StopTransaction, ele é colocado no JSON sem aspas:

"transactionId":17898343970563

Essa 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:

  1. CONNECT — conecta ao servidor WSS.
  2. BOOT — envia BootNotification.
  3. HEARTBEAT — verifica a comunicação.
  4. STATUS — envia StatusNotification.
  5. AUTHORIZE — solicita autorização da tag.
  6. START — inicia a transação.
  7. METER — envia a medição.
  8. STOP — encerra a transação.
Teste realizado: todas as etapas foram aceitas pelo servidor, incluindo StartTransaction, MeterValues e StopTransaction.

10. Controles do protótipo

BotãoGPIOFunção
SELECT27Seleciona a próxima operação
SEND14Executa 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.

Próximos passos possíveis: simular estados reais do EVSE, implementar evolução automática da energia medida, acrescentar controle do piloto CP, simular conexão/desconexão do veículo e integrar o comportamento elétrico do EVSE ao simulador OCPP.

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

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *