feat: OCPP 2.0.1 multi-station simulator + Hasura/UI fixes
- Add ocpp-simulator-multi.js: 15 stations CP001-CP015 via WebSocket SP1 - Add ocpp-sp0-connector.js: Security Profile 0 connector - Add configure-auth.py: BasicAuthPassword setup for all stations - Add Dockerfile.simulator + Dockerfile.sp0 for containerized simulators - Fix Hasura DB password (ALTER USER citrine) - Fix UI NEXTAUTH_SECRET mismatch (C1tR1n30S2... vs Digitribe972) - Fix UI network (traefik-public) + Traefik labels - Update docker-compose-citrineos.yml with simulator services - Set isOnline=true for all 15 stations in DB
This commit is contained in:
133
scripts/ocpp-sp0-connector.js
Normal file
133
scripts/ocpp-sp0-connector.js
Normal file
@@ -0,0 +1,133 @@
|
||||
#!/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); });
|
||||
Reference in New Issue
Block a user