#!/usr/bin/env node /** * OCPP 2.0.1 Security Profile 0 connector for Cariflex/CitrineOS * Connects via WebSocket to CSMS on port 8081 (no auth) to update ocppConnectionName * The main simulator handles port 8082 (Security Profile 1, Basic Auth) for actual OCPP messages */ const net = require('net'); const crypto = require('crypto'); const WS_HOST = process.env.OCPP_HOST || 'cariflex-citrineos-server'; const WS_PORT = parseInt(process.env.OCPP_PORT || '8081'); function encodeFrame(payload) { const maskKey = crypto.randomBytes(4); const pb = Buffer.from(payload, 'utf8'); const len = pb.length; const fl = len < 126 ? 6 + len : 8 + len; const f = Buffer.alloc(fl); f[0] = 0x81; if (len < 126) { f[1] = 0x80 | len; } else { f[1] = 0x80 | 126; f.writeUInt16BE(len, 2); } maskKey.copy(f, fl - len - 4); for (let i = 0; i < len; i++) f[fl - len + i] = pb[i] ^ maskKey[i % 4]; return f; } function parseFrames(buf) { const msgs = []; while (buf.length >= 2) { const op = buf[0] & 0x0F; const masked = (buf[1] & 0x80) !== 0; let pl = buf[1] & 0x7F, hl = 2; if (pl === 126) { if (buf.length < 4) return msgs; pl = buf.readUInt16BE(2); hl = 4; } else if (pl === 127) { if (buf.length < 10) return msgs; pl = Number(buf.readBigUInt64BE(2)); hl = 10; } if (masked) hl += 4; const tl = hl + pl; if (buf.length < tl) return msgs; let payload = buf.slice(hl, hl + pl); if (masked) { const mk = buf.slice(hl - 4, hl); const u = Buffer.alloc(pl); for (let i = 0; i < pl; i++) u[i] = payload[i] ^ mk[i % 4]; payload = u; } msgs.push({ op, payload: payload.toString('utf8') }); buf = buf.slice(tl); } return msgs; } async function connectStation(stationId) { return new Promise((resolve, reject) => { const sock = net.createConnection(WS_PORT, WS_HOST, () => { const key = crypto.randomBytes(16).toString('base64'); const handshake = `GET /${stationId} HTTP/1.1\r\nHost: ${WS_HOST}:${WS_PORT}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: ${key}\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Protocol: ocpp2.0.1\r\n\r\n`; sock.write(handshake); }); let buf = Buffer.alloc(0), ready = false, msgId = 0; const pending = new Map(); sock.on('data', (data) => { buf = Buffer.concat([buf, data]); if (!ready) { const s = buf.toString('utf8'); const he = s.indexOf('\r\n\r\n'); if (he === -1) return; const sl = s.split('\r\n')[0]; if (sl.includes('101')) { ready = true; buf = buf.slice(he + 4); // Send BootNotification const id = String(++msgId); const payload = JSON.stringify([2, id, 'BootNotification', JSON.stringify({ chargingStation: { model: stationId, vendorName: 'Cariflex', firmwareVersion: '1.0.0', serialNumber: stationId }, reason: 'PowerUp' })]); sock.write(encodeFrame(payload)); pending.set(id, { resolve: () => {}, reject: () => {} }); } else { sock.end(); reject(new Error('Handshake failed: ' + sl)); } return; } const msgs = parseFrames(buf); for (const m of msgs) { if (m.op === 0x1) { try { const d = JSON.parse(m.payload); if (d[0] === 3) { console.log('[' + stationId + '] Boot: ' + d[2].status); // Now send StatusNotification const id2 = String(++msgId); const sn = JSON.stringify([2, id2, 'StatusNotification', JSON.stringify({ evseId: 1, connectorId: 1, connectorStatus: 'Available', timestamp: new Date().toISOString() })]); sock.write(encodeFrame(sn)); console.log('[' + stationId + '] StatusNotification sent'); } // Respond to server calls if (d[0] === 2) { sock.write(encodeFrame(JSON.stringify([3, d[1], {}]))); } } catch(e) {} if (d[0] === 3 && d[2] && d[2].status === 'Accepted') { resolve({ sock, pending }); } } else if (m.op === 0x8) { sock.end(); } } }); sock.on('error', (e) => { console.error('[' + stationId + '] ' + e.message); reject(e); }); sock.on('close', () => console.log('[' + stationId + '] DC')); setTimeout(() => reject(new Error('timeout')), 10000); }); } async function main() { const stations = Array.from({length: 15}, (_, i) => 'CP' + String(i+1).padStart(3, '0')); console.log('Connecting ' + stations.length + ' stations to port ' + WS_PORT + ' (Security Profile 0)...'); const connections = []; for (const st of stations) { try { const conn = await connectStation(st); connections.push({ id: st, ...conn }); console.log('[' + st + '] Connected!'); await new Promise(r => setTimeout(r, 200)); } catch(e) { console.error('[' + st + '] Failed: ' + e.message); } } console.log('\n=== ' + connections.length + '/' + stations.length + ' connected on SP0 ==='); // Keep alive setInterval(() => {}, 60000); } process.on('SIGINT', () => { console.log('exit'); process.exit(0); }); main().catch(e => { console.error(e); process.exit(1); });