• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

stephendade / Rpanion-server / 19425374606

17 Nov 2025 09:51AM UTC coverage: 34.54% (-0.007%) from 34.547%
19425374606

push

github

stephendade
Video server: RTSP server has seperate pipline for each client

330 of 1234 branches covered (26.74%)

Branch coverage included in aggregate %.

1006 of 2634 relevant lines covered (38.19%)

2.62 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

37.3
/server/pppConnection.js
1
/*
2
    * PPPConnection.js
3
    * This module manages a PPP connection using pppd.
4
    * It allows setting device, baud rate, local and remote IPs,
5
    * starting and stopping the PPP connection, and retrieving data transfer stats.
6
    * Used for the PPP feature in ArduPilot
7
*/
8
const { autoDetect } = require('@serialport/bindings-cpp')
3✔
9
const si = require('systeminformation')
3✔
10
const fs = require('fs');
3✔
11
const { spawn, execSync } = require('child_process');
3✔
12

13
function isPi () {
14
  let cpuInfo = ''
3✔
15
  try {
3✔
16
    cpuInfo = fs.readFileSync('/proc/device-tree/compatible', { encoding: 'utf8' })
3✔
17
  } catch (e) {
18
    // if this fails, this is probably not a pi
19
    return false
3✔
20
  }
21

22
  const model = cpuInfo
×
23
    .split(',')
24
    .filter(line => line.length > 0)
×
25

26
  if (!model || model.length === 0) {
×
27
    return false
×
28
  }
29

30
  return model[0] === 'raspberrypi'
×
31
}
32

33
function isOrangePi () {
34
  let cpuInfo = ''
3✔
35
  try {
3✔
36
    cpuInfo = fs.readFileSync('/proc/device-tree/compatible', { encoding: 'utf8' })
3✔
37
  } catch (e) {
38
    // if this fails, this is probably not an Orange Pi
39
    return false
3✔
40
  }
41

42
  return cpuInfo.toLowerCase().includes('orangepi')
×
43
}
44

45
class PPPConnection {
46
    constructor(settings) {
47
        this.settings = settings
39✔
48
        this.isConnected = this.settings.value('ppp.enabled', false);
39✔
49
        this.pppProcess = null;
39✔
50
        this.device = this.settings.value('ppp.uart', null);
39✔
51
        this.baudRate = this.settings.value('ppp.baud', 921600)
39✔
52
        this.localIP = this.settings.value('ppp.localIP', '192.168.144.14');  // default local IP
39✔
53
        this.remoteIP = this.settings.value('ppp.remoteIP', '192.168.144.15'); // default remote IP
39✔
54
        this.baudRates = [
39✔
55
            { value: 115200, label: '115200' },
56
            { value: 230400, label: '230400' },
57
            { value: 460800, label: '460800' },
58
            { value: 921600, label: '921600' },
59
            { value: 1500000, label: '1.5 MBaud' },
60
            { value: 2000000, label: '2 MBaud' },
61
            { value: 12500000, label: '12.5 MBaud' }];
62
        this.serialDevices = [];
39✔
63
        this.badbaudRate = false; // flag to indicate if the baud rate is not supported
39✔
64
        this.prevdata = null; // previous data for comparison
39✔
65

66
        if (this.isConnected) {
39✔
67
            const attemptPPPStart = () => {
3✔
68
                this.startPPP(this.device, this.baudRate, this.localIP, this.remoteIP, (err, result) => {
6✔
69
                    if (err) {
6!
70
                        if (err.message.includes('already connected')) {
6✔
71
                            console.log('PPP connection is already established. Retrying in 1 second...');
3✔
72
                            this.isConnected = false;
3✔
73
                            this.setSettings();
3✔
74
                            setTimeout(attemptPPPStart, 1000); // Retry after 1 second
3✔
75
                        } else {
76
                            console.error('Error starting PPP connection:', err);
3✔
77
                            this.isConnected = false;
3✔
78
                            this.setSettings();
3✔
79
                        }
80
                    } else {
81
                        console.log('PPP connection started successfully');
×
82
                    }
83
                });
84
            };
85

86
            attemptPPPStart();
3✔
87
        }
88
    }
89

90
    setSettings() {
91
        this.settings.setValue('ppp.uart', this.device);
9✔
92
        this.settings.setValue('ppp.baud', this.baudRate);
9✔
93
        this.settings.setValue('ppp.localIP', this.localIP);
9✔
94
        this.settings.setValue('ppp.remoteIP', this.remoteIP);
9✔
95
        this.settings.setValue('ppp.enabled', this.isConnected);
9✔
96
    }
97

98
    quitting() {
99
        // stop the PPP connection if rpanion is quitting
100
        if (this.pppProcess) {
6!
101
            console.log('Stopping PPP connection on quit...');
×
102
            this.pppProcess.kill();
×
103
            try {
×
104
                execSync('sudo pkill -SIGTERM pppd && sleep 1');
×
105
            } catch (error) {
106
                console.error('Error stopping PPP connection on shutdown:', error);
×
107
            }
108
        }
109
        console.log('PPPConnection quitting');
6✔
110
    }
111

112
    async getDevices (callback) {
113
        // get all serial devices
114
        this.serialDevices = []
3✔
115
        let retError = null
3✔
116

117
        const Binding = autoDetect()
3✔
118
        const ports = await Binding.list()
3✔
119

120
        for (let i = 0, len = ports.length; i < len; i++) {
3✔
121
            if (ports[i].pnpId !== undefined) {
96!
122
                // usb-ArduPilot_Pixhawk1-1M_32002A000847323433353231-if00
123
                // console.log("Port: ", ports[i].pnpID);
124
                let namePorts = ''
×
125
                if (ports[i].pnpId.split('_').length > 2) {
×
126
                namePorts = ports[i].pnpId.split('_')[1] + ' (' + ports[i].path + ')'
×
127
                } else {
128
                namePorts = ports[i].manufacturer + ' (' + ports[i].path + ')'
×
129
                }
130
                // console.log("Port: ", ports[i].pnpID);
131
                this.serialDevices.push({ value: ports[i].path, label: namePorts, pnpId: ports[i].pnpId })
×
132
            } else if (ports[i].manufacturer !== undefined) {
96!
133
                // on recent RasPiOS, the pnpID is undefined :(
134
                const nameports = ports[i].manufacturer + ' (' + ports[i].path + ')'
×
135
                this.serialDevices.push({ value: ports[i].path, label: nameports, pnpId: nameports })
×
136
            }
137
        }
138

139
        // for the Ras Pi's inbuilt UART
140
        if (fs.existsSync('/dev/serial0') && isPi()) {
3!
141
        this.serialDevices.push({ value: '/dev/serial0', label: '/dev/serial0', pnpId: '/dev/serial0' })
×
142
        }
143
        if (fs.existsSync('/dev/ttyAMA0') && isPi()) {
3!
144
        //Pi5 uses a different UART name. See https://forums.raspberrypi.com/viewtopic.php?t=359132
145
        this.serialDevices.push({ value: '/dev/ttyAMA0', label: '/dev/ttyAMA0', pnpId: '/dev/ttyAMA0' })
×
146
        }
147
        if (fs.existsSync('/dev/ttyAMA1') && isPi()) {
3!
148
        this.serialDevices.push({ value: '/dev/ttyAMA1', label: '/dev/ttyAMA1', pnpId: '/dev/ttyAMA1' })
×
149
        }
150
        if (fs.existsSync('/dev/ttyAMA2') && isPi()) {
3!
151
        this.serialDevices.push({ value: '/dev/ttyAMA2', label: '/dev/ttyAMA2', pnpId: '/dev/ttyAMA2' })
×
152
        }
153
        if (fs.existsSync('/dev/ttyAMA3') && isPi()) {
3!
154
        this.serialDevices.push({ value: '/dev/ttyAMA3', label: '/dev/ttyAMA3', pnpId: '/dev/ttyAMA3' })
×
155
        }
156
        if (fs.existsSync('/dev/ttyAMA4') && isPi()) {
3!
157
        this.serialDevices.push({ value: '/dev/ttyAMA4', label: '/dev/ttyAMA4', pnpId: '/dev/ttyAMA4' })
×
158
        }
159
        // rpi uart has different name under Ubuntu
160
        const data = await si.osInfo()
3✔
161
        if (data.distro.toString().includes('Ubuntu') && fs.existsSync('/dev/ttyS0') && isPi()) {
3!
162
        // console.log("Running Ubuntu")
163
        this.serialDevices.push({ value: '/dev/ttyS0', label: '/dev/ttyS0', pnpId: '/dev/ttyS0' })
×
164
        }
165
        // jetson serial ports
166
        if (fs.existsSync('/dev/ttyTHS1')) {
3!
167
        this.serialDevices.push({ value: '/dev/ttyTHS1', label: '/dev/ttyTHS1', pnpId: '/dev/ttyTHS1' })
×
168
        }
169
        if (fs.existsSync('/dev/ttyTHS2')) {
3!
170
        this.serialDevices.push({ value: '/dev/ttyTHS2', label: '/dev/ttyTHS2', pnpId: '/dev/ttyTHS2' })
×
171
        }
172
        if (fs.existsSync('/dev/ttyTHS3')) {
3!
173
        this.serialDevices.push({ value: '/dev/ttyTHS3', label: '/dev/ttyTHS3', pnpId: '/dev/ttyTHS3' })
×
174
        }
175
        // Orange Pi Zero3 serial ports
176
        if (fs.existsSync('/dev/ttyS5') && isOrangePi()) {
3!
177
        this.serialDevices.push({ value: '/dev/ttyS5', label: '/dev/ttyS5', pnpId: '/dev/ttyS5' })
×
178
        }
179
        if (fs.existsSync('/dev/ttyAS5') && isOrangePi()) {
3!
180
        this.serialDevices.push({ value: '/dev/ttyAS5', label: '/dev/ttyAS5', pnpId: '/dev/ttyAS5' })
×
181
        }
182
        return callback(retError, this.serialDevices);
3✔
183
    }
184

185
    startPPP(device, baudRate, localIP, remoteIP, callback) {
186
        this.badbaudRate = false;
15✔
187
        if (this.isConnected) {
15✔
188
            return callback(new Error('PPP is already connected'), {
6✔
189
                selDevice: this.device,
190
                selBaudRate: this.baudRate,
191
                localIP: this.localIP,
192
                remoteIP: this.remoteIP,
193
                enabled: this.isConnected,
194
                baudRates: this.baudRates,
195
                serialDevices: this.serialDevices,
196
            });
197
        }
198
        if (!device) {
9✔
199
            return callback(new Error('Device is required'), {
3✔
200
                selDevice: this.device,
201
                selBaudRate: this.baudRate,
202
                localIP: this.localIP,
203
                remoteIP: this.remoteIP,
204
                enabled: this.isConnected,
205
                baudRates: this.baudRates,
206
                serialDevices: this.serialDevices,
207
            });
208
        }
209
        if (this.pppProcess) {
6!
210
            return callback(new Error('PPP still running. Please wait for it to finish.'), {
×
211
                selDevice: this.device,
212
                selBaudRate: this.baudRate,
213
                localIP: this.localIP,
214
                remoteIP: this.remoteIP,
215
                enabled: this.isConnected,
216
                baudRates: this.baudRates,
217
                serialDevices: this.serialDevices,
218
            });
219
        }
220

221
        //ensure device string is valid in the serialdevices list
222
        const validDevice = this.serialDevices.find(d => d.value === device);
6✔
223
        if (!validDevice) {
6!
224
            return callback(new Error('Invalid device selected'), {
6✔
225
                selDevice: this.device,
226
                selBaudRate: this.baudRate,
227
                localIP: this.localIP,
228
                remoteIP: this.remoteIP,
229
                enabled: this.isConnected,
230
                baudRates: this.baudRates,
231
                serialDevices: this.serialDevices,
232
            });
233
        }
234

235
        this.device = device;
×
236
        this.baudRate = baudRate;
×
237
        this.localIP = localIP;
×
238
        this.remoteIP = remoteIP;
×
239
        
240
        const args = [
×
241
            "pppd",
242
            this.device,
243
            this.baudRate, // baud rate
244
            //'persist',          // enables faster termination
245
            //'holdoff', '1',     // minimum delay of 1 second between connection attempts
246
            this.localIP + ':' + this.remoteIP, // local and remote IPs
247
            'local',
248
            'noauth',
249
            //'debug',
250
            'crtscts',
251
            'nodetach',
252
            'ktune'
253
        ];
254
        // if running in dev env, need to preload sudo login
255
        if (process.env.NODE_ENV === 'development') {
×
256
            execSync('sudo -v');
×
257
        }
258
        console.log(`Starting PPP with args: ${args.join(' ')}`);
×
259
        this.pppProcess = spawn('sudo', args, {
×
260
        //detached: true,
261
        stdio: ['ignore', 'pipe', 'pipe'] // or 'ignore' for all three to fully detach
262
        });
263
        this.pppProcess.stdout.on('data', (data) => {
×
264
            console.log("PPP Output: ", data.toString().trim());
×
265
            // Check for non support baud rates "speed <baud> not supported"
266
            if (data.toString().includes('speed') && data.toString().includes('not supported')) {
×
267
                this.pppProcess.kill();
×
268
                this.pppProcess = null; // reset the process reference
×
269
                this.isConnected = false;
×
270
                this.badbaudRate = true;
×
271
            }
272
        });
273
        this.pppProcess.stderr.on('data', (data) => {
×
274
            console.log("PPP Error: ", data.toString().trim());
×
275
        });
276
        this.pppProcess.on('close', (code) => {
×
277
            console.log("PPP process exited with code: ", code.toString().trim());
×
278
            this.isConnected = false;
×
279
            this.pppProcess = null; // reset the process reference
×
280
            this.setSettings();
×
281
        });
282
        this.isConnected = true;
×
283
        this.setSettings();
×
284
        return callback(null, {
×
285
            selDevice: this.device,
286
            selBaudRate: this.baudRate,
287
            localIP: this.localIP,
288
            remoteIP: this.remoteIP,
289
            enabled: this.isConnected,
290
            baudRates: this.baudRates,
291
            serialDevices: this.serialDevices,
292
        });
293
    }
294

295
    stopPPP(callback) {
296
        if (!this.isConnected) {
6✔
297
            return callback(new Error('PPP is not connected'), {
3✔
298
                selDevice: this.device,
299
                selBaudRate: this.baudRate,
300
                localIP: this.localIP,
301
                remoteIP: this.remoteIP,
302
                enabled: this.isConnected,
303
                baudRates: this.baudRates,
304
                serialDevices: this.serialDevices,
305
            });
306
        }
307
        if (this.pppProcess) {
3!
308
            // Gracefully kill the PPP process
309
            console.log('Stopping PPP connection...');
×
310
            this.pppProcess.kill();
×
311
            execSync('sudo pkill -SIGTERM pppd');
×
312
            this.isConnected = false;
×
313
            this.setSettings();
×
314
        }
315
        return callback(null, {
3✔
316
            selDevice: this.device,
317
            selBaudRate: this.baudRate,
318
            localIP: this.localIP,
319
            remoteIP: this.remoteIP,
320
            enabled: this.isConnected,
321
            baudRates: this.baudRates,
322
            serialDevices: this.serialDevices,
323
        });
324
    }
325

326
    getPPPdatarate(callback) {
327
        if (!this.isConnected) {
×
328
            return callback(new Error('PPP is not connected'));
×
329
        }
330
        // get current data transfer stats for connected PPP session
331
        return new Promise((resolve, reject) => {
×
332
            exec('ifconfig ppp0', (error, stdout, stderr) => {
×
333
                if (error) {
×
334
                    reject(`Error getting PPP data rate: ${stderr}`);
×
335
                } else {
336
                    // match format RX packets 110580  bytes 132651067 (132.6 MB)
337
                    const match = stdout.match(/RX packets \d+  bytes (\d+) \(\d+\.\d+ MB\).*TX packets \d+  bytes (\d+) \(\d+\.\d+ MB\)/);
×
338
                    if (match) {
×
339
                        const rxBytes = parseInt(match[1], 10);
×
340
                        const txBytes = parseInt(match[5], 10);
×
341
                        resolve({ rxBytes, txBytes });
×
342
                    } else {
343
                        reject('Could not parse PPP data rate');
×
344
                    }
345
                }
346
            });
347
        });
348
    }
349

350
    getPPPSettings(callback) {
351
        this.getDevices((err, devices) => {
×
352
            if (err) {
×
353
                console.error('Error fetching serial devices:', err);
×
354
                return callback(err, {
×
355
                    selDevice: null,
356
                    selBaudRate: this.baudRate,
357
                    localIP: this.localIP,
358
                    remoteIP: this.remoteIP,
359
                    enabled: this.isConnected,
360
                    baudRates: this.baudRates,
361
                    serialDevices: [],
362
                });
363
            }
364
            
365
            this.serialDevices = devices;
×
366
            
367
            // Set default device if not already set
368
            if (!this.device && this.serialDevices.length > 0) {
×
369
                this.device = this.serialDevices[0].value;
×
370
            }
371
            
372
            // if this.device is not in the list, set it to first available device
373
            if (this.device && !this.serialDevices.some(d => d.value === this.device.value)) {
×
374
                this.device = this.serialDevices[0].value;
×
375
            }
376
            
377
            // Always return callback
378
            return callback(null, {
×
379
                selDevice: this.device,
380
                selBaudRate: this.baudRate,
381
                localIP: this.localIP,
382
                remoteIP: this.remoteIP,
383
                enabled: this.isConnected,
384
                baudRates: this.baudRates,
385
                serialDevices: this.serialDevices,
386
            });
387
        });
388
    }
389

390
    // uses ifconfig to get the PPP connection datarate
391
    getPPPDataRate() {
392
        if (!this.isConnected) {
×
393
            return { rxRate: 0, txRate: 0 };
×
394
        }
395
        // get current data transfer stats for connected PPP session
396
        try {
×
397
            let stdout = execSync('ifconfig ppp0 | grep packets', { encoding: 'utf8' }).toString().trim();
×
398
            if (!stdout) {
×
399
                return { rxRate: 0, txRate: 0, percentusedRx: 0, percentusedTx: 0 };
×
400
            }
401
            // match format :
402
            //        RX packets 0  bytes 0 (0.0 B)
403
            //        TX packets 118  bytes 12232 (12.2 KB)
404
            const [ , matchRX, matchTX ] = stdout.match(/RX\s+packets\s+\d+\s+bytes\s+(\d+).*TX\s+packets\s+\d+\s+bytes\s+(\d+)/s);
×
405
            if (matchRX && matchTX) {
×
406
                const rxBytes = parseInt(matchRX);
×
407
                const txBytes = parseInt(matchTX);
×
408
                // calculate the data rate in bytes per second
409
                if (this.prevdata) {
×
410
                    const elapsed = Date.now() - this.prevdata.timestamp; // in milliseconds
×
411
                    const rxRate = (rxBytes - this.prevdata.rxBytes) / (elapsed / 1000); // bytes per second
×
412
                    const txRate = (txBytes - this.prevdata.txBytes) / (elapsed / 1000); // bytes per second
×
413
                    const percentusedRx = rxRate / (this.baudRate / 8); // percent of baud rate used
×
414
                    const percentusedTx = txRate / (this.baudRate / 8); // percent of baud rate used
×
415
                    this.prevdata = { rxBytes, txBytes, timestamp: Date.now() };
×
416
                    return { rxRate, txRate, percentusedRx, percentusedTx };
×
417
                }
418
                this.prevdata = { rxBytes, txBytes, timestamp: Date.now() };
×
419
                return { rxRate: 0, txRate: 0, percentusedRx: 0, percentusedTx: 0 };
×
420
            } else {
421
                return { rxRate: 0, txRate: 0, percentusedRx: 0, percentusedTx: 0};
×
422
            }
423
        } catch (error) {
424
            console.error('Error getting PPP data rate:', error.message);
×
425
            return { rxRate: 0, txRate: 0, percentusedRx: 0, percentusedTx: 0 };
×
426
        }
427
    }
428

429
    // Returns a string representation of the PPP connection status for use by socket.io
430
    conStatusStr () {
431
        //format the connection status string
432
        if (this.badbaudRate) {
×
433
            return 'Disconnected (Baud rate not supported)';
×
434
        }
435
        if (!this.isConnected) {
×
436
            return 'Disconnected';
×
437
        }
438
        if (this.pppProcess && this.pppProcess.pid) {
×
439
            //get datarate
440
            const { rxRate, txRate, percentusedRx, percentusedTx } = this.getPPPDataRate();
×
441
            let status = 'Connected';
×
442
            if (this.pppProcess.pid) {
×
443
                status += ` (PID: ${this.pppProcess.pid})`;
×
444
            }
445
            if (rxRate > 0 || txRate > 0) {
×
446
                status += `, RX: ${rxRate.toFixed(2)} B/s (${(percentusedRx * 100).toFixed(2)}%), TX: ${txRate.toFixed(2)} B/s (${(percentusedTx * 100).toFixed(2)}%)`;
×
447
            } else {
448
                status += ', No data transfer';
×
449
            }
450
            return status;
×
451
        }
452
        else {
453
            return 'Disconnected';
×
454
        }
455
    }
456
}
457

458

459
module.exports = PPPConnection;
3✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc