Implementando um Cliente Node.js para Servidor OCPP 1.6 com Interface Web

Introdução

O OCPP (Open Charge Point Protocol) é um protocolo de comunicação aberto e padronizado que conecta estações de carregamento de veículos elétricos (EVSE) a sistemas de gerenciamento central (CSMS). Ele foi criado para permitir que diferentes fabricantes e provedores trabalhem de forma interoperável, sem depender de soluções proprietárias.

Na versão OCPP 1.6, amplamente utilizada no mercado, a comunicação entre o carregador e o servidor é feita através de WebSocket, um canal bidirecional que permite a troca contínua de mensagens em tempo real. Isso é essencial para funções como:

  • Autenticação de usuários (ex: cartão RFID ou app).

  • Início e encerramento de sessões de carregamento.

  • Monitoramento de consumo de energia.

  • Atualizações remotas de firmware.

  • Envio de notificações e diagnósticos.

Para ambientes de produção, recomenda-se fortemente o uso de WebSocket seguro (WSS), que é o WebSocket rodando sobre TLS/SSL. O WSS garante que os dados trafeguem criptografados, evitando problemas como:

  • Interceptação de dados (ex: IDs de usuários ou dados de cobrança).

  • Modificação maliciosa de mensagens durante a transmissão.

  • Ataques do tipo man-in-the-middle.

Em resumo: o OCPP 1.6 com WSS combina padronização, interoperabilidade e segurança, tornando-se a escolha ideal para projetos que envolvem mobilidade elétrica em cenários reais de operação.

Como funciona

  1. Conexão WSS: a interface web envia comando para client.js, que conecta ao servidor OCPP.

  2. Envio de comandos OCPP: BootNotification, Authorize, StartTransaction, StopTransaction e MeterValues.

  3. Logs em tempo real: mensagens do servidor OCPP são retransmitidas via Socket.IO para o navegador.

  4. Configuração dinâmica: Charger ID pode ser alterado sem reiniciar o Node.js.

 

Fluxo de comunicação:

  • usuário → interface HTML → Socket.IO → client.js → WebSocket (servidor OCPP)

  • mensagens de resposta do servidor OCPP → WebSocket → client.js → Socket.IO → interface HTML

public/index.html:

  • interface com campos de entrada, botões, logs

  • funções JS que emitem os eventos corretos para o servidor

Tipos de Mensagem no protocolo OCPP:

  • Tipo 2: Request / Call

  • Tipo 3: CallResult

  • Tipo 4: CallError

  • Possivelmente Tipo 1 (Heartbeat etc).

Segurança e Certificado SSL no OCPP 1.6

Quando falamos de integração OCPP usando WSS (WebSocket seguro), a comunicação entre a estação de carregamento (EVSE) e o servidor precisa ser criptografada. Isso garante que os dados — como autorizações de usuários, início/fim de transações e medições de energia — não possam ser interceptados ou alterados por terceiros.

No projeto, isso é feito configurando um certificado SSL no cliente, carregado pelo client.js:

const caCert = fs.readFileSync(CERT_PATH);

ws = new WebSocket(url, [], {
ca: caCert,
rejectUnauthorized: true
});

Pontos importantes:

  1. Uso de certificados válidos
    Sempre que possível, utilize certificados emitidos por uma autoridade certificadora confiável (ex: Let’s Encrypt). Assim, seu cliente pode confiar no servidor sem ajustes manuais.

  2. Certificados autoassinados
    Para testes locais, você pode usar um certificado autoassinado (cert.pem). Nesse caso, será necessário fornecer explicitamente o arquivo .pem no cliente. Em produção, essa prática não é recomendada, pois expõe a comunicação a riscos de ataques “man-in-the-middle”.

  3. rejectUnauthorized: true
    Essa opção garante que o cliente só aceite conexões com certificados válidos. Definir como false pode facilitar testes, mas abre brechas graves de segurança.

  4. Proteção de dados sensíveis
    Todas as mensagens OCPP carregam informações importantes, como IDs de usuários (tags RFID) e registros de cobrança. O uso de WSS com SSL garante que esses dados estejam protegidos.

  5. Rotação e validade
    Certificados têm prazo de validade. Certifique-se de renovar antes do vencimento e, se possível, automatizar a rotação (ex: usando o Certbot para Let’s Encrypt).

 

 

Estrutura do Projeto

project/

                    ├─ cert.pem # Certificado CA para conexão WSS
                    ├─ client.js # Cliente Node.js + servidor web
└─
public/
└─ index.html # Interface web para interação
  • cert.pem: necessário para validar a conexão WSS.

  • client.js: gerencia a conexão OCPP e integra o Socket.IO.

  • index.html: interface web para enviar comandos e exibir logs em tempo real.

 

 

client.js


const fs = require("fs");
const path = require("path");
const express = require("express");
const http = require("http");
const { Server } = require("socket.io");
const WebSocket = require("ws");

// ==========================
// Configurações OCPP Client
// ==========================
const SERVER_URL = "wss://0.0.0.0:3000"; // seu servidor OCPP no teste rodei no IP add 192.168.68.114:3000
let CHARGE_BOX_ID = "EVSE0200"; // pode ser alterado pela UI
const CERT_PATH = path.join(__dirname, "cert.pem");

let ws; // conexão WebSocket com CSMS
let messageCounter = 1;

// ==========================
// Funções auxiliares
// ==========================
function generateUniqueId() {
  return `${Date.now()}-${messageCounter++}`;
}

// ==========================
// Funções OCPP
// ==========================
function connectOcpp(io) {
  if (ws && ws.readyState === WebSocket.OPEN) {
    ws.close();
  }

  const caCert = fs.readFileSync(CERT_PATH);
  const ocppUrl = `${SERVER_URL}/${CHARGE_BOX_ID}`;

  ws = new WebSocket(ocppUrl, ["ocpp1.6"], {
    ca: caCert,
    rejectUnauthorized: true
  });

  ws.on("open", () => {
    io.emit("log", `✅ Conectado ao CSMS via WSS: ${ocppUrl}`);
  });

  ws.on("message", (data) => {
    try {
      const msg = JSON.parse(data.toString());
      const msgType = msg[0];

      if (msgType === 3) {
        // CallResult
        io.emit("log", `⬅️ CallResult recebido: ${JSON.stringify(msg)}`);
      } else if (msgType === 4) {
        // CallError
        io.emit("log", `❌ CallError recebido: ${JSON.stringify(msg)}`);
      } else {
        io.emit("log", `⬅️ Mensagem inesperada: ${JSON.stringify(msg)}`);
      }
    } catch (err) {
      io.emit("log", `❌ Erro ao parsear mensagem: ${err.message}`);
    }
  });

  ws.on("close", () => {
    io.emit("log", "🔌 Conexão OCPP encerrada");
  });

  ws.on("error", (err) => {
    io.emit("log", `❌ Erro OCPP: ${err.message}`);
  });
}

function sendOcppMessage(action, payload, io) {
  if (!ws || ws.readyState !== WebSocket.OPEN) {
    io.emit("log", "⚠️ Não conectado ao servidor OCPP");
    return;
  }
  const msgId = generateUniqueId();
  const msg = [2, msgId, action, payload];
  ws.send(JSON.stringify(msg));
  io.emit("log", `➡️ Enviado: ${JSON.stringify(msg)}`);
}

// ==========================
// Express + Socket.IO (UI)
// ==========================
const app = express();
const server = http.createServer(app);
const io = new Server(server);

app.use(express.static(path.join(__dirname, "public")));

io.on("connection", (socket) => {
  socket.emit("log", "🌐 Cliente conectado à interface web");

  socket.on("setChargerId", (id) => {
    CHARGE_BOX_ID = id;
    socket.emit("log", `🔧 ChargerId configurado: ${CHARGE_BOX_ID}`);
  });

  socket.on("connectOcpp", () => connectOcpp(io));

  socket.on("bootNotification", (params) => {
    sendOcppMessage("BootNotification", {
      chargerId: params.model || CHARGE_BOX_ID,
      chargePointModel: params.model || "modelX",
      chargePointVendor: params.vendor || "vendorX",
      chargeBoxSerialNumber: CHARGE_BOX_ID,
      firmwareVersion: params.firmware || "1.0"
    }, io);
  });

  socket.on("authorize", (params) => {
    sendOcppMessage("Authorize", { idTag: params.idTag || "TAG123" }, io);
  });

  socket.on("startTransaction", (params) => {
    sendOcppMessage("StartTransaction", {
      connectorId: parseInt(params.connectorId) || 1,
      idTag: params.idTag || "TAG123",
      meterStart: parseInt(params.meterStart) || 0,
      timestamp: new Date().toISOString()
    }, io);
  });

  socket.on("stopTransaction", (params) => {
    sendOcppMessage("StopTransaction", {
      transactionId: parseInt(params.transactionId) || 1,
      meterStop: parseInt(params.meterStop) || 100,
      timestamp: new Date().toISOString()
    }, io);
  });

  socket.on("heartbeat", () => {
    sendOcppMessage("Heartbeat", {}, io);
  });
});

// ==========================
// Start servidor web
// ==========================
const PORT = 8080;
server.listen(PORT, () => {
  console.log(`🚀 Interface disponível em http://localhost:${PORT}`);
});

Interface Web (index.html)

Permite enviar comandos OCPP e visualizar logs em tempo real:

<!DOCTYPE html>
<html lang=”pt-br”>
<head>
<meta charset=”UTF-8″ />
<title>Cliente OCPP 1.6</title>
<script src=”/socket.io/socket.io.js”></script>
<style>
body { font-family: Arial, sans-serif; margin: 20px; background: #f0f0f0; }
h1 { color: #333; }
#logs { background: black; color: lime; padding: 10px; height: 300px; overflow-y: scroll; }
button { margin: 5px; padding: 10px; }
input { padding: 5px; margin: 5px; width: 250px; }
fieldset { margin: 10px 0; padding: 10px; }
label { display: inline-block; width: 180px; }
</style>
</head>
<body>
<h1>Cliente OCPP 1.6</h1>

<fieldset>
<legend>Configuração</legend>
<label>chargerId: </label>
<input type=”text” id=”chargerId” value=”EVSE0200″ />
<button onclick=”setChargerId()”>Definir</button>
<button onclick=”connectOcpp()”>Conectar</button>
</fieldset>

<fieldset>
<legend>BootNotification</legend>
<label>chargerId: </label><input type=”text” id=”chargerId” value=”EVSE0200″ /><br>
<label>Vendor: </label><input type=”text” id=”vendor” value=”BTech” /><br>
<label>Model: </label><input type=”text” id=”model” value=”BTech ModelY” /><br>
<label>ChargeBox Serial Number: </label><input type=”text” id=”chargeBoxSerialNumber” value=”BTech0001″ /><br>
<label>ChargePoint Serial Number: </label><input type=”text” id=”chargePointSerialNumber” value=”CP123456″ /><br>
<label>Firmware Version: </label><input type=”text” id=”firmware” value=”1.0″ /><br>
<button onclick=”sendBoot()”>Enviar BootNotification</button>
</fieldset>

<fieldset>
<legend>Authorize</legend>
<label>idTag: </label><input type=”text” id=”authIdTag” value=”TAG123″ />
<button onclick=”sendAuthorize()”>Enviar Authorize</button>
</fieldset>

<fieldset>
<legend>StartTransaction</legend>
<label>ConnectorId: </label><input type=”number” id=”connectorId” value=”1″ /><br>
<label>idTag: </label><input type=”text” id=”startIdTag” value=”TAG123″ /><br>
<label>MeterStart: </label><input type=”number” id=”meterStart” value=”0″ /><br>
<button onclick=”sendStart()”>Enviar StartTransaction</button>
</fieldset>

<fieldset>
<legend>StopTransaction</legend>
<label>TransactionId: </label><input type=”number” id=”transactionId” value=”1″ /><br>
<label>MeterStop: </label><input type=”number” id=”meterStop” value=”100″ /><br>
<button onclick=”sendStop()”>Enviar StopTransaction</button>
</fieldset>

<fieldset>
<legend>Outros</legend>
<button onclick=”sendHeartbeat()”>Enviar Heartbeat</button>
</fieldset>

<h3>Logs</h3>
<div id=”logs”></div>

<script>
const socket = io();

// Logs
socket.on(“log”, (msg) => {
const logs = document.getElementById(“logs”);
logs.innerHTML += msg + “<br>”;
logs.scrollTop = logs.scrollHeight;
});

// Definir ChargerId localmente
function setChargerId() {
const id = document.getElementById(“chargerId”).value;
socket.emit(“setChargerId”, id);
log(`ChargerId definido: ${id}`);
}

function connectOcpp() {
socket.emit(“connectOcpp”);
log(“Tentando conectar ao servidor OCPP…”);
}

// BootNotification com chargerId
function sendBoot() {
const messageId = Date.now() + “-1”; // gera ID único
const payload = {
chargerId: document.getElementById(“chargerId”).value,
chargePointVendor: document.getElementById(“vendor”).value,
chargePointModel: document.getElementById(“model”).value,
chargeBoxSerialNumber: document.getElementById(“chargeBoxSerialNumber”).value,
chargePointSerialNumber: document.getElementById(“chargePointSerialNumber”).value,
firmwareVersion: document.getElementById(“firmware”).value

};

const message = [2, messageId, “BootNotification”, payload];

socket.emit(“bootNotification”, message); // envia no formato array OCPP 1.6
}

function sendAuthorize() {
const payload = {
action: “Authorize”,
chargerId: document.getElementById(“chargerId”).value,
idTag: document.getElementById(“authIdTag”).value
};
socket.emit(“authorize”, payload);
log(“Authorize enviado: ” + JSON.stringify(payload));
}

function sendStart() {
const payload = {
action: “StartTransaction”,
chargerId: document.getElementById(“chargerId”).value,
connectorId: document.getElementById(“connectorId”).value,
idTag: document.getElementById(“startIdTag”).value,
meterStart: document.getElementById(“meterStart”).value
};
socket.emit(“startTransaction”, payload);
log(“StartTransaction enviado: ” + JSON.stringify(payload));
}

function sendStop() {
const payload = {
action: “StopTransaction”,
chargerId: document.getElementById(“chargerId”).value,
transactionId: document.getElementById(“transactionId”).value,
meterStop: document.getElementById(“meterStop”).value
};
socket.emit(“stopTransaction”, payload);
log(“StopTransaction enviado: ” + JSON.stringify(payload));
}

function sendHeartbeat() {
const payload = {
action: “Heartbeat”,
chargerId: document.getElementById(“chargerId”).value
};
socket.emit(“heartbeat”, payload);
log(“Heartbeat enviado”);
}

function log(msg) {
const logs = document.getElementById(“logs”);
logs.innerHTML += `[${new Date().toLocaleTimeString()}] ${msg}<br>`;
logs.scrollTop = logs.scrollHeight;
}
</script>
</body>
</html>


 


Testando localmente

  1. Coloque cert.pem na raiz do projeto.

  2. Execute o servidor Node.js:

node client.js
  1. Acesse http://localhost:8080.

  2. Defina o Charger ID, clique em “Conectar WSS” e teste os comandos OCPP.

Deixe um comentário

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