134 lines
5.4 KiB
JavaScript
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); });
|