Files
cariflex/snapshots/20260615_163350/scripts/ocpp-simulator-multi.js

134 lines
5.4 KiB
JavaScript

#!/usr/bin/env node
const net = require('net');
const crypto = require('crypto');
const PASSWORD='DEADBEEFDEADBEEF';
const STATIONS = Array.from({length: 15}, (_, i) => ({
id: `CP${String(i+1).padStart(3, '0')}`,
path: `/1/CP${String(i+1).padStart(3, '0')}`,
}));
const WS_HOST = process.env.OCPP_HOST || 'cariflex-citrineos-server';
const WS_PORT = parseInt(process.env.OCPP_PORT || '8082');
let msgId = 0;
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;
}
function connect(station) {
return new Promise((resolve, reject) => {
const sock = net.createConnection(WS_PORT, WS_HOST, () => {
const key = crypto.randomBytes(16).toString('base64');
const enc = Buffer.from(station.id + ':' + PASSWORD).toString('base64');
sock.write('GET ' + station.path + ' 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\nAuthorization: Basic ' + enc + '\r\n\r\n');
});
let buf = Buffer.alloc(0), ready = false;
const sp = 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); resolve({ sock, station, sendCall: mkSend(sp, sock), sp }); }
else { console.log('[' + station.id + '] Handshake: ' + sl); sock.end(); reject(new Error('fail')); }
return;
}
const msgs = parseFrames(buf);
for (const m of msgs) { if (m.op === 0x1) handleMsg(m.payload, sp, sock); else if (m.op === 0x8) sock.end(); }
});
sock.on('error', (e) => { console.error('[' + station.id + '] ' + e.message); reject(e); });
sock.on('close', () => console.log('[' + station.id + '] DC'));
setTimeout(() => reject(new Error('timeout')), 10000);
});
}
function mkSend(sp, sock) {
return function(action, payload) {
return new Promise((res, rej) => {
const id = String(++msgId);
sp.set(id, { res, rej });
sock.write(encodeFrame(JSON.stringify([2, id, action, payload])));
setTimeout(() => { if (sp.has(id)) { sp.delete(id); rej(new Error('timeout:' + action)); } }, 15000);
});
};
}
function handleMsg(msg, sp, sock) {
try {
const d = JSON.parse(msg);
if (d[0] === 3 && sp.has(d[1])) { sp.get(d[1]).res(d[2]); sp.delete(d[1]); }
else if (d[0] === 2) { sock.write(encodeFrame(JSON.stringify([3, d[1], {}]))); }
} catch(e) {}
}
async function bootStation(station) {
// Retry connection up to 10 times with 3s delay
for (let attempt = 1; attempt <= 10; attempt++) {
try {
const { sock, sendCall } = await connect(station);
console.log('[' + station.id + '] Connected (attempt ' + attempt + ')');
const boot = await sendCall('BootNotification', { chargingStation: { model: station.id, vendorName: 'Cariflex', firmwareVersion: '1.0.0', serialNumber: station.id }, reason: 'PowerUp' });
console.log('[' + station.id + '] Boot: ' + boot.status);
await sendCall('StatusNotification', { evseId: 1, connectorId: 1, connectorStatus: 'Available', timestamp: new Date().toISOString() });
console.log('[' + station.id + '] Available');
return { sock, sendCall };
} catch(e) {
console.log('[' + station.id + '] Attempt ' + attempt + ' failed: ' + e.message);
if (attempt < 10) await new Promise(r => setTimeout(r, 3000));
}
}
throw new Error('Failed after 10 attempts');
}
async function main() {
const tenantId = '1';
console.log('Simulating ' + STATIONS.length + ' stations...');
const online = [];
for (const st of STATIONS) {
try { const s = await bootStation(st); online.push(Object.assign({}, st, s)); await new Promise(r => setTimeout(r, 300)); }
catch(e) { console.error('[' + st.id + '] ' + e.message); }
}
console.log('\n=== ' + online.length + '/' + STATIONS.length + ' ONLINE ===');
setInterval(async () => { for (const s of online) { try { await s.sendCall('Heartbeat', {}); } catch(e) {} } console.log('[HB] ' + online.length); }, 60000);
}
process.on('SIGINT', () => { console.log('exit'); process.exit(0); });
main().catch(e => { console.error(e); process.exit(1); });