🚗🔌 Cliente OCPP 1.6 com ESP32
O Open Charge Point Protocol (OCPP) é o padrão mais utilizado no mundo para comunicação entre estações de recarga (EVSE) e servidores de gerenciamento. No artigo de hoje, mostro como montar um cliente OCPP 1.6 rodando em um ESP32, capaz de se conectar via WebSocket seguro (WSS) a um servidor OCPP.
🛠️ Tecnologias utilizadas
ESP32 (Wi-Fi + poder de processamento para rodar OCPP simplificado)
ArduinoWebsockets (para comunicação WSS)
ArduinoJson (parse/serialização das mensagens OCPP)
LiquidCrystal_I2C (feedback no display LCD 16×2)
Servidor OCPP (no exemplo, rodando em Node.js com certificado SSL/TLS)
📡 Estrutura do cliente
Conexão Wi-Fi → O ESP32 acessa a rede local.
Handshake TLS/WSS → Conexão segura ao servidor OCPP na porta 3000.
Mensagens OCPP → O cliente envia/recebe mensagens no formato JSON-RPC:
BootNotificationAuthorizeStartTransactionStopTransactionMeterValuesHeartbeat
Interface com botões → Dois botões permitem:
Selecionar ação OCPP (GPIO27)
Enviar ação (GPIO14)
Feedback no LCD → Mostra a ação atual, EVSE configurado e
transactionIdrecebido.
🔐 Conexão segura com certificado
O ESP32 utiliza o mesmo certificado PEM do servidor Node.js, configurado assim:
Isso garante que a troca de mensagens siga o padrão OCPP 1.6 JSON sobre WebSocket seguro.
⚡ Ciclo de mensagens OCPP
BootNotification: estação avisa o servidor que está online.
Authorize: validação do
idTagdo usuário.StartTransaction: início de uma sessão de carga (com timestamp ISO8601).
MeterValues: envio periódico dos valores medidos (energia consumida).
StopTransaction: término da sessão de carga.
Heartbeat: mantêm a conexão viva a cada 30s.
Exemplo de envio no formato JSON-RPC:
🖥️ Visualização no LCD
O display mostra a ação selecionada e o EVSE ativo, como:
Ao receber um transactionId, ele é exibido no LCD automaticamente.
🔐 Código do Cliente OCPP 1.6 no ESP32
A seguir, o código completo comentado:
🖥️ Inicialização do LCD e Wi-Fi
📡 Conexão ao servidor OCPP
⚡ Envio de mensagens OCPP
🔄 Loop principal com Heartbeat
⚡ Fluxo de mensagens suportadas
O cliente suporta as ações principais do OCPP 1.6:
BootNotification→ registro inicialAuthorize→ autenticação de usuárioStartTransaction→ início da sessão de cargaMeterValues→ envio periódico de energiaStopTransaction→ finalização da sessãoHeartbeat→ ping a cada 30s
🔧 Fluxo de uso
Pressione GPIO27 para selecionar uma ação OCPP.
Pressione GPIO14 para enviar a ação ao servidor.
O LCD exibe o status da ação e a resposta do servidor.
Novos EVSEs podem ser criados via opção
AddEVSE.DisconnectDBencerra a sessão com o servidor.
🚀 Conclusão
Este projeto demonstra um cliente OCPP 1.6 minimalista rodando em hardware embarcado (ESP32).
Com ele, é possível:
Testar servidores OCPP em laboratório.
Simular estações de recarga reais.
Criar protótipos de carregadores inteligentes.
👉 O próximo passo pode ser armazenar logs em SPIFFS/LittleFS ou integrar medição real de energia via sensores.
🔐 Código Completo do Cliente OCPP 1.6 no ESP32
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <ArduinoWebsockets.h>
#include <LiquidCrystal_I2C.h>
#include <ArduinoJson.h>
//String ssidStr;
//String passwordStr;
// Credenciais Wi-Fi
const char* ssid = “sulwifi”;
const char* password = “#Abracadabra#”;
using namespace websockets;
// Endereço I2C do display (mais comum: 0x27 ou 0x3F)
// Se não aparecer nada, teste os dois.
LiquidCrystal_I2C lcd(0x27, 16, 2);
// Endereço do servidor OCPP (seu PC ou servidor)
const char* websocket_server = “192.168.68.114”; // IP do servidor
const int websocket_port = 3000;
// Certificado do servidor (mesmo usado no Node, mas em PEM)
const char* server_cert = R”EOF(
—–BEGIN CERTIFICATE—–
MIIDfDCCAmSgAwIBAgIUWmhASu9gF7rEp/qW+bznUue1XUcwDQYJKoZIhvcNAQEL
BQAwXjELMAkGA1UEBhMCQlIxCzAJBgNVBAgMAlNQMRIwEAYDVQQHDAlTYW8gUGF1
bG8xFTATBgNVBAoMDE1pbmhhRW1wcmVzYTEXMBUGA1UEAwwOMTkyLjE2OC42OC4x
MTQwHhcNMjUwOTAxMjE1MjM2WhcNMjYwOTAxMjE1MjM2WjBeMQswCQYDVQQGEwJC
UjELMAkGA1UECAwCU1AxEjAQBgNVBAcMCVNhbyBQYXVsbzEVMBMGA1UECgwMTWlu
aGFFbXByZXNhMRcwFQYDVQQDDA4xOTIuMTY4LjY4LjExNDCCASIwDQYJKoZIhvcN
AQEBBQADggEPADCCAQoCggEBAKK/ZON6fraSVFdsYfaUht2hbBQwZDKm/khqSpMo
8CQBDVOjPfGxKOaWEIxiIXD+Vuf02ghQyxYQ2Ft1s2PuQDd+exgEf58Abrnvyq90
/0vPO5TYfnvj5WA6Mmfl3RCQ0/d2P1prcXK4cx6N/CxiRWShpuogtb9wYU1tiZM8
ovIFh9dHcm/uQxDqrXMYT7ilUor572TU4PSKRdtGqd2e2tu0KWoeD0kRodNIop4r
1CNereu0CHlZbMXmgyRDrBGKX/ba2Exnw0OlVTzTF6M0sxZJXcaFbJu5VhNxYXMb
m3xSAWuxeSj+gNWs6KIXr60Imf0o5CEjSbG/wZrKDa05ljECAwEAAaMyMDAwDwYD
VR0RBAgwBocEwKhEcjAdBgNVHQ4EFgQUT2Mmi8409cnsIzzuFPVeSQMVDlQwDQYJ
KoZIhvcNAQELBQADggEBAE7D4dPJPskVYqXypUIY4xuug7BWM6n2lHTLCxwDstmH
uAhZ2x3nQn+CEkdoi5OY+3tFfDy9ey7vZiULy+JteSTeBuRQFB7IAYZHsBq2rq/P
BweQdwOHEb9m0MTpi6fAQnKwJnan+x/vUzdVKJR5odFMMKP5nZF76Y9xdhfOhjG3
zSRb97lKBMRyEI9ykJbCzpHd71nMAHVGV9Q8PYeozjh+tTrddJiD5iKEx9CJaOqn
32XI1Jy9dTuhZHjKnmQuSRwzztSnmLFGAMsvmUx/RoJRkRqEXbd84cRBh2jY99Pg
n1pUO1SVVz4hb7eodL4ct1jetZ+fE/RZZ2C9rzjX2us=
—–END CERTIFICATE—–
)EOF”;
WebsocketsClient webSocket;
// variaveis mensagem OCPP
String transactionId;
// ———- PINOS ———-
const int pinSelect = 27; // Seleção
const int pinSend = 14; // Enviar
// ———- CONTROLE DE ESTADO ———-
unsigned long msgCounter = 1;
unsigned long previousMillis = 0;
const long interval = 30000; // 30 segundos heartbeat
// Controle de múltiplos EVSE
int evseCounter = 201; // começa no 201 → EVSE0201
String currentChargerId = “EVSE0201”; // EVSE ativo
// Lista de ações (AddEVSE incluído antes de DisconnectDB)
const char* actions[] = {
“ConnectDB”,
“BootNotification”,
“Authorize”,
“StartTransaction”,
“StopTransaction”,
“MeterValues”,
“AddEVSE”,
“DisconnectDB”
};
int actionIndex = 0; // começa no ConnectDB
const int totalActions = sizeof(actions) / sizeof(actions[0]);
void sendOCPPMessage(String json) {
Serial.println(“➡️ Enviando: ” + json);
webSocket.send(json);
}
void iniciarLCD() {
Wire.begin(21, 22);
lcd.init();
lcd.backlight();
lcd.setCursor(0, 0);
lcd.print(actions[actionIndex]); // mostra a ação inicial
}
void atualizarLCD() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(actions[actionIndex]); // linha 0: ação
lcd.setCursor(0, 1);
lcd.print(“EVSE=” + currentChargerId); // linha 1: chargerId
}
// Servidores NTP
const char* ntpServer = “pool.ntp.org”;
const long gmtOffset_sec = -3 * 3600; // Fuso horário de São Paulo (UTC-3)
const int daylightOffset_sec = 0; // Sem horário de verão no Brasil
String getUniqueId() {
return String(“UID”) + String(msgCounter++);
}
// Função para enviar mensagem OCPP
void sendOCPPMessage(const char* action, String payload) {
String uid = getUniqueId();
String msg = “[2,\”” + uid + “\”,\”” + action + “\”,” + payload + “]”;
Serial.println(“➡️ Enviando OCPP: ” + msg);
webSocket.send(msg);
}
void printLocalTime() {
struct tm timeinfo;
if (!getLocalTime(&timeinfo)) {
Serial.println(“❌ Falha ao obter hora”);
return;
}
Serial.println(&timeinfo, “📅 %d/%m/%Y ⏰ %H:%M:%S”);
}
String getTimestampISO8601() {
struct tm timeinfo;
if (!getLocalTime(&timeinfo)) {
Serial.println(“❌ Falha ao obter hora”);
return “”;
}
char buffer[25]; // espaço suficiente para “YYYY-MM-DDTHH:MM:SSZ”
strftime(buffer, sizeof(buffer), “%Y-%m-%dT%H:%M:%SZ”, &timeinfo);
return String(buffer);
}
// Função para imprimir no LCD
void imprimirLCD(String texto, int col = 0, int row = 0) {
lcd.setCursor(col, row); // Define posição
lcd.print(texto); // Escreve texto
}
bool connectedToServer = false; // controle de estado do WebSocket
// ———- FUNÇÃO DE CONEXÃO ———-
void conectarServidor() {
if (!connectedToServer) {
webSocket.setCACert(server_cert);
if (webSocket.connect(“wss://192.168.68.114:3000”)) {
Serial.println(“✅ Conectado ao servidor OCPP via WSS”);
connectedToServer = true;
// enviar um bootNotification
String payload = “{\”chargerId\”:\”EVSE0001\”,\”chargePointVendor\”:\”btech\”,\”chargePointModel\”:\”btechmodel1\”,\”chargePointSerialNumber\”:\”btech000017\”,\”firmwareVersion\”:\”1.0\”}”;
sendOCPPMessage(“BootNotification”, payload);
} else {
Serial.println(“❌ Falha ao conectar ao servidor OCPP”);
}
// webSocket.onMessage([&](WebsocketsMessage msg) {
// Serial.println(“⬅️ Resposta: ” + msg.data());
// });
// nova versão onMessage
// …
webSocket.onMessage([&](WebsocketsMessage msg) {
Serial.println(“⬅️ Resposta: ” + msg.data());
// Cria um buffer JSON (tamanho suficiente para a resposta)
StaticJsonDocument<256> doc;
DeserializationError error = deserializeJson(doc, msg.data());
if (error) {
Serial.print(“❌ Erro ao parsear JSON: “);
Serial.println(error.c_str());
return;
}
// doc é um JsonArray no formato [3, “UID12”, { … }]
JsonArray arr = doc.as<JsonArray>();
// Verifica se o payload existe
if (arr.size() >= 3) {
JsonObject payload = arr[2];
if (payload.containsKey(“transactionId”)) {
transactionId = payload[“transactionId”].as<String>();
Serial.print(“✅ transactionId recebido: “);
Serial.println(transactionId);
lcd.setCursor(0, 0);
lcd.print(“TransactionId”);
lcd.setCursor(0, 1);
lcd.print(transactionId);
}
}
});
webSocket.onEvent([&](WebsocketsEvent event, String data) {
if(event == WebsocketsEvent::ConnectionOpened) {
Serial.println(“✅ Conexão estabelecida!”);
} else if(event == WebsocketsEvent::ConnectionClosed) {
Serial.println(“❌ Conexão encerrada!”);
connectedToServer = false;
}
});
}
}
void desconectarServidor() {
if (connectedToServer) {
webSocket.close();
connectedToServer = false;
Serial.println(“🔌 Desconectado do servidor”);
}
}
// ———- SETUP ———-
void setup() {
iniciarLCD();
Serial.begin(115200);
// // Inicializa SPIFFS
// if (!LittleFS.begin(true)) {
// Serial.println(“Erro ao montar SPIFFS”);
// return;
// }
// // Lê arquivo wifi.txt
// File f = LittleFS.open(“/wifi.txt”, “r”);
// if (!f) {
// Serial.println(“Erro ao abrir wifi.txt”);
// return;
// }
// while (f.available()) {
// String line = f.readStringUntil(‘\n’);
// line.trim();
// if (line.startsWith(“SSID=”)) ssidStr = line.substring(5);
// if (line.startsWith(“PASSWORD=”)) passwordStr = line.substring(9);
// }
// f.close();
// Conecta ao Wi-Fi (usar c_str() aqui!)
WiFi.begin(ssid,password );
Serial.print(“Conectando ao Wi-Fi”);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(“.”);
}
// WiFi.begin(ssid, password);
// Serial.print(“Conectando ao WiFi…”);
// while (WiFi.status() != WL_CONNECTED) {
// delay(500);
// Serial.print(“.”);
// }
Serial.println(“\n✅ WiFi conectado”);
pinMode(pinSelect, INPUT_PULLUP);
pinMode(pinSend, INPUT_PULLUP);
configTime(-3 * 3600, 0, “pool.ntp.org”);
Serial.println(“Sistema iniciado. Use GPIO14 para selecionar e GPIO27 para enviar.”);
}
// ———- LOOP ———-
void loop() {
if (connectedToServer) {
webSocket.poll();
unsigned long currentMillis = millis();
if (currentMillis – previousMillis >= interval) {
previousMillis = currentMillis;
// exemplo de mensagem [2, “19223206”, “Heartbeat”, {}]
String payload = “{}”;
sendOCPPMessage(“Heartbeat”, payload);
Serial.println(“💓 Heartbeat enviado”);
}
}
// Botão de seleção (GPIO27)
if (digitalRead(pinSelect) == LOW) {
delay(200); // debounce
actionIndex = (actionIndex + 1) % totalActions;
atualizarLCD();
Serial.println(“🔘 Selecionado: ” + String(actions[actionIndex]));
while (digitalRead(pinSelect) == LOW); // espera soltar
}
// Botão de envio (GPIO14)
if (digitalRead(pinSend) == LOW) {
delay(200); // debounce
String msg;
if (actionIndex == 0) { // ConnectDB
conectarServidor();
lcd.setCursor(0, 1);
lcd.print(“Conectando… “);
}
else if (actionIndex == 1 && connectedToServer) { // BootNotification
String payload = “{\”chargerId\”:\”EVSE0001\”,\”chargePointVendor\”:\”btech\”,\”chargePointModel\”:\”btechmodel1\”,\”chargePointSerialNumber\”:\”btech000017\”,\”firmwareVersion\”:\”1.0\”}”;
sendOCPPMessage(“BootNotification”, payload);
}
else if (actionIndex == 2 && connectedToServer) { // Authorize
String payload = “{\”idTag\”:\”ABC123\”}”;
sendOCPPMessage(“Authorize”, payload);
}
else if (actionIndex == 3 && connectedToServer) { // StartTransaction
String timestamp = getTimestampISO8601();
Serial.println(“🕒 Timestamp gerado: ” + timestamp);
String payload = “{\”connectorId\”:1,\”idTag\”:\”ABC123\”,\”meterStart\”:12000,\”timestamp\”:\”+timestamp+\”}”;
sendOCPPMessage(“StartTransaction”, payload);
}
else if (actionIndex == 4 && connectedToServer) { // StopTransaction
String timestamp = getTimestampISO8601();
Serial.println(“🕒 Timestamp gerado: ” + timestamp);
String payload =”{\”transactionId\”:” + String(transactionId) +
“,\”idTag\”:\”ABC123\”,\”meterStop\”:12500,” “\”timestamp\”:\”” + timestamp + “\”}”;
sendOCPPMessage(“StopTransaction”, payload);
}
else if (actionIndex == 5 && connectedToServer) { // MeterValues
String timestamp = getTimestampISO8601();
Serial.println(“🕒 Timestamp gerado: ” + timestamp);
String payload =”{\”connectorId\”:1,\”transactionId\”:” + String(transactionId) +
“,\”meterValue\”:[{\”timestamp\”:\”” + timestamp +”\”,\”sampledValue\”:[{\”value\”:\”12345\”,\”measurand\”:\”Energy.Active.Import.Register\”,\”unit\”:\”Wh\”}]}]}”;
sendOCPPMessage(“MeterValues”, payload);
}
else if (actionIndex == 6) { // AddEVSE
evseCounter++;
currentChargerId = “EVSE” + String(evseCounter);
Serial.println(“➕ Novo EVSE configurado: ” + currentChargerId);
lcd.setCursor(0, 1);
lcd.print(“new =” + currentChargerId);
}
else if (actionIndex == 7) { // DisconnectDB
desconectarServidor();
lcd.setCursor(0, 1);
lcd.print(“Desconectado “);
}
if (msg.length() > 0) {
sendOCPPMessage(msg);
Serial.println(“📤 Ação enviada: ” + String(actions[actionIndex]));
lcd.setCursor(0, 1);
lcd.print(“Enviado: “);
lcd.print(actions[actionIndex]);
}
while (digitalRead(pinSend) == LOW); // espera soltar
}
}

Deixe um comentário