mikuso / ocpp-rpc

A Node.js client & server implementation of the WAMP-like RPC-over-websocket system defined in the OCPP-J protocols.
MIT License
97 stars 29 forks source link

connection dropping afer one minute #80

Open saimirg opened 3 months ago

saimirg commented 3 months ago

Hi @mikuso, i need some help. I have a specific charger that is dropping connection after one minute. One the following simple code, the charger disconnects after one minute. However, if I connect the charger using the WS library the charger stays connected and i can communicate two ways.

This is the code used where the charger disconnects.


const server = new RPCServer({
    protocols: ['ocpp1.6'], // server accepts ocpp1.6 subprotocol
    strictMode: true,       // enable strict validation of requests & responses
});

server.auth((accept, reject, handshake) => {
    // accept the incoming client
    accept({
        // anything passed to accept() will be attached as a 'session' property of the client.
        sessionId: 'XYZ123'
    });
});

server.on('client', async (client) => {
    console.log(`${client.session.sessionId} connected!`); // `XYZ123 connected!`

    // create a specific handler for handling BootNotification requests
    client.handle('BootNotification', ({params}) => {
        console.log(`Server got BootNotification from ${client.identity}:`, params);

        // respond to accept the client
        return {
            status: "Accepted",
            interval: 300,
            currentTime: new Date().toISOString()
        };
    });

    // create a specific handler for handling Heartbeat requests
    client.handle('Heartbeat', ({params}) => {
        console.log(`Server got Heartbeat from ${client.identity}:`, params);

        // respond with the server's current time.
        return {
            currentTime: new Date().toISOString()
        };
    });

    // create a specific handler for handling StatusNotification requests
    client.handle('StatusNotification', ({params}) => {
        console.log(`Server got StatusNotification from ${client.identity}:`, params);
        return {};
    });

    // create a wildcard handler to handle any RPC method
    client.handle(({method, params}) => {
        // This handler will be called if the incoming method cannot be handled elsewhere.
        console.log(`Server got ${method} from ${client.identity}:`, params);

        // throw an RPC error to inform the server that we don't understand the request.
        throw createRPCError("NotImplemented");
    });

    // when clients disconnect, remove them from our Map
    client.once('close', () => {
        // check that we're about to delete the correct client
        console.log(`${client.session.sessionId} disconnected!`);
        console.log(`${client.identity} disconnected!`);

        // if (clients.get(client.identity) === client) {
        //     clients.delete(client.identity);
        // }
    });
});

await server.listen(3000);

Charger does not disconnect when using this code:


const express = require('express');
const http = require('http');
const WebSocket = require('ws');
const bodyParser = require('body-parser');

const app = express();
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });

// Middleware
app.use(bodyParser.json());

// WebSocket handling for OCPP 1.6+
wss.on('connection', (ws) => {
  console.log('New WebSocket connection');

  ws.on('message', (message) => {
    const parsedMessage = JSON.parse(message);
    handleOCPPMessage(parsedMessage, (response) => {
      ws.send(JSON.stringify(response));
    });
  });
});

function handleOCPPMessage(message, callback) {
  const [messageType, messageId, action, payload] = message;

  switch (action) {
    case 'BootNotification':
      handleBootNotification(messageId, payload, callback);
      break;
    case 'StatusNotification':
      handleStatusNotification(messageId, payload, callback);
      break;
    default:
      callback([3, messageId, { error: 'NotImplemented' }]);
  }
}

function handleBootNotification(messageId, payload, callback) {
  console.log('Received BootNotification:', payload);
  const response = {
    status: 'Accepted',
    currentTime: new Date().toISOString(),
    interval: 300
  };
  callback([3, messageId, response]);
}

function handleStatusNotification(messageId, payload, callback) {
  console.log('Received StatusNotification:', payload);
  const response = {};
  callback([3, messageId, response]);
}

const PORT = process.env.PORT || 80;
server.listen(PORT, () => {
  console.log(`OCPP server listening on port ${PORT}`);
});```
saimirg commented 2 months ago

Hi @mikuso, we found out the problem is with Pong response. For some reasons Pong response from the charger is considered malformed and servers throws and error which drops the connection. If we disable ping requests from the server everything works fine but this has an effect on other chargers. We tried asking manufacturer to check the pong response but didnt succeed.

Anything else we can do to fix it ?

mikuso commented 2 months ago

Hi @saimirg

Were you able to recreate this issue using the WS library? What happens when the WS library receives a malformed pong?

One possible solution might be to set the pingIntervalMs option to Infinity when constructing the RPCServer, like so:

const server = new RPCServer({
    protocols: ['ocpp1.6'], // server accepts ocpp1.6 subprotocol
    strictMode: true,       // enable strict validation of requests & responses
    pingIntervalMs: Infinity, // disable pings
});

This should stop the server from sending pings, but disables the keepAlive functionality - so you may end up with lingering connections which get "stuck" if they don't cleanly disconnect.

saimirg commented 2 months ago

Yes, the same behavior happens with WS library when initiating Ping from server side. An error will be thrown and connection would drop when pong is received.

Setting pingIntervalMs: Infinity (we tried null) will work on this particular charger but has an effect on other chargers. It would be great if we could set it on charger level rather then on server.

mikuso commented 2 months ago

Understood. Have you tried using client.reconfigure() to do this on an individual basis?

Something like...

server.on('client', client => {
    if (client.identity === 'badclient') {
        client.reconfigure({ pingIntervalMs: Infinity });
    }
});