fix: VariableAttributes mapping + locations detail parseInt + .gitignore pnpm-store
This commit is contained in:
281
snapshots/20260615_163350/scripts/ocpp-simulator.js
Normal file
281
snapshots/20260615_163350/scripts/ocpp-simulator.js
Normal file
@@ -0,0 +1,281 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* OCPP 2.0.1 Charging Station Simulator for Cariflex/CitrineOS
|
||||
* Uses raw TCP WebSocket (bypassing ws module subprotocol issues)
|
||||
*/
|
||||
|
||||
const net = require('net');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const CONFIG = {
|
||||
host: process.env.OCPP_HOST || 'localhost',
|
||||
port: parseInt(process.env.OCPP_PORT || '8082'),
|
||||
path: '/1/CP001',
|
||||
stationId: 'CP001',
|
||||
password: 'DEADBEEFDEADBEEF',
|
||||
vendorName: 'Cariflex',
|
||||
firmwareVersion: '1.0.0',
|
||||
};
|
||||
|
||||
let msgId = 0;
|
||||
const pending = new Map();
|
||||
let socket = null;
|
||||
let buffer = Buffer.alloc(0);
|
||||
let wsReady = false;
|
||||
|
||||
function encodeFrame(payload) {
|
||||
const maskKey = crypto.randomBytes(4);
|
||||
const payloadBuf = Buffer.from(payload, 'utf8');
|
||||
const len = payloadBuf.length;
|
||||
|
||||
let frameLen;
|
||||
if (len < 126) {
|
||||
frameLen = 6 + len; // 2 header + 4 mask + payload
|
||||
} else if (len < 65536) {
|
||||
frameLen = 8 + len; // 2 header + 2 ext len + 4 mask + payload
|
||||
} else {
|
||||
frameLen = 16 + len; // 2 header + 8 ext len + 4 mask + payload
|
||||
}
|
||||
|
||||
const frame = Buffer.alloc(frameLen);
|
||||
frame[0] = 0x81; // FIN + text
|
||||
|
||||
if (len < 126) {
|
||||
frame[1] = 0x80 | len;
|
||||
} else if (len < 65536) {
|
||||
frame[1] = 0x80 | 126;
|
||||
frame.writeUInt16BE(len, 2);
|
||||
} else {
|
||||
frame[1] = 0x80 | 127;
|
||||
frame.writeBigUInt64BE(BigInt(len), 2);
|
||||
}
|
||||
|
||||
maskKey.copy(frame, frameLen - len - 4);
|
||||
for (let i = 0; i < len; i++) {
|
||||
frame[frameLen - len + i] = payloadBuf[i] ^ maskKey[i % 4];
|
||||
}
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
function parseFrames() {
|
||||
const messages = [];
|
||||
|
||||
while (buffer.length >= 2) {
|
||||
const firstByte = buffer[0];
|
||||
const secondByte = buffer[1];
|
||||
const opcode = firstByte & 0x0F;
|
||||
const masked = (secondByte & 0x80) !== 0;
|
||||
let payloadLen = secondByte & 0x7F;
|
||||
let headerLen = 2;
|
||||
|
||||
if (payloadLen === 126) {
|
||||
if (buffer.length < 4) return messages;
|
||||
payloadLen = buffer.readUInt16BE(2);
|
||||
headerLen = 4;
|
||||
} else if (payloadLen === 127) {
|
||||
if (buffer.length < 10) return messages;
|
||||
payloadLen = Number(buffer.readBigUInt64BE(2));
|
||||
headerLen = 10;
|
||||
}
|
||||
|
||||
if (masked) headerLen += 4;
|
||||
|
||||
const totalLen = headerLen + payloadLen;
|
||||
if (buffer.length < totalLen) return messages;
|
||||
|
||||
let payload = buffer.slice(headerLen, headerLen + payloadLen);
|
||||
|
||||
if (masked) {
|
||||
const maskKey = buffer.slice(headerLen - 4, headerLen);
|
||||
const unmasked = Buffer.alloc(payloadLen);
|
||||
for (let i = 0; i < payloadLen; i++) {
|
||||
unmasked[i] = payload[i] ^ maskKey[i % 4];
|
||||
}
|
||||
payload = unmasked;
|
||||
}
|
||||
|
||||
messages.push({ opcode, payload: payload.toString('utf8') });
|
||||
buffer = buffer.slice(totalLen);
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
function sendCall(action, payload) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const id = String(++msgId);
|
||||
const msg = JSON.stringify([2, id, action, payload]);
|
||||
pending.set(id, { resolve, reject });
|
||||
console.log(`[SEND] ${action} (${id})`);
|
||||
socket.write(encodeFrame(msg));
|
||||
setTimeout(() => {
|
||||
if (pending.has(id)) {
|
||||
pending.delete(id);
|
||||
reject(new Error(`Timeout: ${action}`));
|
||||
}
|
||||
}, 15000);
|
||||
});
|
||||
}
|
||||
|
||||
function handleMessage(msg) {
|
||||
try {
|
||||
const data = JSON.parse(msg);
|
||||
if (data[0] === 3) { // CallResult
|
||||
const id = data[1];
|
||||
if (pending.has(id)) {
|
||||
pending.get(id).resolve(data[2]);
|
||||
pending.delete(id);
|
||||
}
|
||||
} else if (data[0] === 2) { // Call from server
|
||||
const id = data[1];
|
||||
const action = data[2];
|
||||
console.log(`[CALL] ${action} from server`);
|
||||
const response = JSON.stringify([3, id, {}]);
|
||||
socket.write(encodeFrame(response));
|
||||
} else if (data[0] === 4) { // Error
|
||||
const id = data[1];
|
||||
console.log(`[ERROR] ${data[2]}: ${data[3]}`);
|
||||
if (pending.has(id)) {
|
||||
pending.get(id).reject(new Error(`${data[2]}: ${data[3]}`));
|
||||
pending.delete(id);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[PARSE ERROR]', e.message, '- raw:', JSON.stringify(msg.substring(0, 100)));
|
||||
}
|
||||
}
|
||||
|
||||
function connect() {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log(`Connecting to ${CONFIG.host}:${CONFIG.port}${CONFIG.path}...`);
|
||||
|
||||
socket = net.createConnection(CONFIG.port, CONFIG.host, () => {
|
||||
const key = crypto.randomBytes(16).toString('base64');
|
||||
const encoded = Buffer.from(CONFIG.stationId + ':' + CONFIG.password).toString('base64');
|
||||
|
||||
const handshake = `GET ${CONFIG.path} HTTP/1.1\r\n` +
|
||||
`Host: ${CONFIG.host}:${CONFIG.port}\r\n` +
|
||||
`Upgrade: websocket\r\n` +
|
||||
`Connection: Upgrade\r\n` +
|
||||
`Sec-WebSocket-Key: ${key}\r\n` +
|
||||
`Sec-WebSocket-Version: 13\r\n` +
|
||||
`Sec-WebSocket-Protocol: ocpp2.0.1\r\n` +
|
||||
`Authorization: Basic ${encoded}\r\n` +
|
||||
`\r\n`;
|
||||
|
||||
socket.write(handshake);
|
||||
});
|
||||
|
||||
socket.on('data', (data) => {
|
||||
buffer = Buffer.concat([buffer, data]);
|
||||
|
||||
if (!wsReady) {
|
||||
const str = buffer.toString('utf8');
|
||||
const headerEnd = str.indexOf('\r\n\r\n');
|
||||
if (headerEnd === -1) return; // Wait for full HTTP response
|
||||
|
||||
const statusLine = str.split('\r\n')[0];
|
||||
console.log('Handshake:', statusLine);
|
||||
|
||||
if (statusLine.includes('101')) {
|
||||
wsReady = true;
|
||||
buffer = buffer.slice(headerEnd + 4); // Skip past HTTP headers
|
||||
console.log('✅ WebSocket connected!');
|
||||
resolve();
|
||||
} else {
|
||||
console.log('❌ Handshake failed');
|
||||
console.log(str.substring(0, 500));
|
||||
socket.end();
|
||||
reject(new Error('Handshake failed'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse WebSocket frames
|
||||
const messages = parseFrames();
|
||||
for (const msg of messages) {
|
||||
if (msg.opcode === 0x1) { // Text frame
|
||||
handleMessage(msg.payload);
|
||||
} else if (msg.opcode === 0x8) { // Close frame
|
||||
console.log('[CLOSE] Server sent close frame');
|
||||
socket.end();
|
||||
} else if (msg.opcode === 0x9) { // Ping
|
||||
// Send pong
|
||||
socket.write(encodeFrame(''));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('error', (e) => {
|
||||
console.error('[SOCKET ERROR]', e.message);
|
||||
reject(e);
|
||||
});
|
||||
|
||||
socket.on('close', () => {
|
||||
console.log('[DISCONNECTED]');
|
||||
});
|
||||
|
||||
setTimeout(() => reject(new Error('Connection timeout')), 10000);
|
||||
});
|
||||
}
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
await connect();
|
||||
|
||||
// BootNotification
|
||||
console.log('Sending BootNotification...');
|
||||
const bootResult = await sendCall('BootNotification', {
|
||||
chargingStation: {
|
||||
model: CONFIG.stationId,
|
||||
vendorName: CONFIG.vendorName,
|
||||
firmwareVersion: CONFIG.firmwareVersion,
|
||||
serialNumber: CONFIG.stationId,
|
||||
},
|
||||
reason: 'PowerUp',
|
||||
});
|
||||
console.log('✅ BootNotification result:', bootResult);
|
||||
|
||||
// StatusNotification - Available
|
||||
console.log('Sending StatusNotification (Available)...');
|
||||
await sendCall('StatusNotification', {
|
||||
evseId: 1,
|
||||
connectorId: 1,
|
||||
connectorStatus: 'Available',
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
console.log('✅ Connector 1 is Available');
|
||||
|
||||
// Heartbeat
|
||||
console.log('Sending Heartbeat...');
|
||||
const hbResult = await sendCall('Heartbeat', {});
|
||||
console.log('✅ Heartbeat result:', hbResult);
|
||||
|
||||
console.log('');
|
||||
console.log('=== ✅ Station CP001 is ONLINE and READY ===');
|
||||
console.log('=== Press Ctrl+C to stop ===');
|
||||
|
||||
// Keep alive with heartbeats every 60s
|
||||
setInterval(async () => {
|
||||
try {
|
||||
await sendCall('Heartbeat', {});
|
||||
console.log('[HEARTBEAT] ✅');
|
||||
} catch (e) {
|
||||
console.error('[HEARTBEAT] ❌', e.message);
|
||||
}
|
||||
}, 60000);
|
||||
|
||||
} catch (e) {
|
||||
console.error('❌ Failed:', e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
console.log('\nShutting down...');
|
||||
if (socket) socket.end();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
run();
|
||||
Reference in New Issue
Block a user