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

stephendade / Rpanion-server / 19204169944

09 Nov 2025 05:49AM UTC coverage: 34.547% (-1.1%) from 35.678%
19204169944

Pull #342

github

web-flow
Merge 9bcb15a56 into 6694db97a
Pull Request #342: Remove react-select controls in favour of Form.Select

316 of 1186 branches covered (26.64%)

Branch coverage included in aggregate %.

20 of 61 new or added lines in 6 files covered. (32.79%)

31 existing lines in 2 files now uncovered.

993 of 2603 relevant lines covered (38.15%)

2.63 hits per line

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

35.76
/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
class PPPConnection {
34
    constructor(settings) {
35
        this.settings = settings
39✔
36
        this.isConnected = this.settings.value('ppp.enabled', false);
39✔
37
        this.pppProcess = null;
39✔
38
        this.device = this.settings.value('ppp.uart', null);
39✔
39
        this.baudRate = this.settings.value('ppp.baud', 921600)
39✔
40
        this.localIP = this.settings.value('ppp.localIP', '192.168.144.14');  // default local IP
39✔
41
        this.remoteIP = this.settings.value('ppp.remoteIP', '192.168.144.15'); // default remote IP
39✔
42
        this.baudRates = [
39✔
43
            { value: 921600, label: '921600' },
44
            { value: 1500000, label: '1.5 MBaud' },
45
            { value: 2000000, label: '2 MBaud' },
46
            { value: 12500000, label: '12.5 MBaud' }];
47
        this.serialDevices = [];
39✔
48
        this.badbaudRate = false; // flag to indicate if the baud rate is not supported
39✔
49
        this.prevdata = null; // previous data for comparison
39✔
50

51
        if (this.isConnected) {
39✔
52
            const attemptPPPStart = () => {
3✔
53
                this.startPPP(this.device, this.baudRate, this.localIP, this.remoteIP, (err, result) => {
6✔
54
                    if (err) {
6!
55
                        if (err.message.includes('already connected')) {
6✔
56
                            console.log('PPP connection is already established. Retrying in 1 second...');
3✔
57
                            this.isConnected = false;
3✔
58
                            this.setSettings();
3✔
59
                            setTimeout(attemptPPPStart, 1000); // Retry after 1 second
3✔
60
                        } else {
61
                            console.error('Error starting PPP connection:', err);
3✔
62
                            this.isConnected = false;
3✔
63
                            this.setSettings();
3✔
64
                        }
65
                    } else {
UNCOV
66
                        console.log('PPP connection started successfully');
×
67
                    }
68
                });
69
            };
70

71
            attemptPPPStart();
3✔
72
        }
73
    }
74

75
    setSettings() {
76
        this.settings.setValue('ppp.uart', this.device);
9✔
77
        this.settings.setValue('ppp.baud', this.baudRate);
9✔
78
        this.settings.setValue('ppp.localIP', this.localIP);
9✔
79
        this.settings.setValue('ppp.remoteIP', this.remoteIP);
9✔
80
        this.settings.setValue('ppp.enabled', this.isConnected);
9✔
81
    }
82

83
    quitting() {
84
        // stop the PPP connection if rpanion is quitting
85
        if (this.pppProcess) {
6!
86
            console.log('Stopping PPP connection on quit...');
×
87
            this.pppProcess.kill();
×
88
            try {
×
89
                execSync('sudo pkill -SIGTERM pppd && sleep 1');
×
90
            } catch (error) {
91
                console.error('Error stopping PPP connection on shutdown:', error);
×
92
            }
93
        }
94
        console.log('PPPConnection quitting');
6✔
95
    }
96

97
    async getDevices (callback) {
98
        // get all serial devices
99
        this.serialDevices = []
3✔
100
        let retError = null
3✔
101

102
        const Binding = autoDetect()
3✔
103
        const ports = await Binding.list()
3✔
104

105
        for (let i = 0, len = ports.length; i < len; i++) {
3✔
106
            if (ports[i].pnpId !== undefined) {
96!
107
                // usb-ArduPilot_Pixhawk1-1M_32002A000847323433353231-if00
108
                // console.log("Port: ", ports[i].pnpID);
109
                let namePorts = ''
×
110
                if (ports[i].pnpId.split('_').length > 2) {
×
111
                namePorts = ports[i].pnpId.split('_')[1] + ' (' + ports[i].path + ')'
×
112
                } else {
113
                namePorts = ports[i].manufacturer + ' (' + ports[i].path + ')'
×
114
                }
115
                // console.log("Port: ", ports[i].pnpID);
116
                this.serialDevices.push({ value: ports[i].path, label: namePorts, pnpId: ports[i].pnpId })
×
117
            } else if (ports[i].manufacturer !== undefined) {
96!
118
                // on recent RasPiOS, the pnpID is undefined :(
119
                const nameports = ports[i].manufacturer + ' (' + ports[i].path + ')'
×
120
                this.serialDevices.push({ value: ports[i].path, label: nameports, pnpId: nameports })
×
121
            }
122
        }
123

124
        // for the Ras Pi's inbuilt UART
125
        if (fs.existsSync('/dev/serial0') && isPi()) {
3!
126
        this.serialDevices.push({ value: '/dev/serial0', label: '/dev/serial0', pnpId: '/dev/serial0' })
×
127
        }
128
        if (fs.existsSync('/dev/ttyAMA0') && isPi()) {
3!
129
        //Pi5 uses a different UART name. See https://forums.raspberrypi.com/viewtopic.php?t=359132
130
        this.serialDevices.push({ value: '/dev/ttyAMA0', label: '/dev/ttyAMA0', pnpId: '/dev/ttyAMA0' })
×
131
        }
132
        if (fs.existsSync('/dev/ttyAMA1') && isPi()) {
3!
133
        this.serialDevices.push({ value: '/dev/ttyAMA1', label: '/dev/ttyAMA1', pnpId: '/dev/ttyAMA1' })
×
134
        }
135
        if (fs.existsSync('/dev/ttyAMA2') && isPi()) {
3!
136
        this.serialDevices.push({ value: '/dev/ttyAMA2', label: '/dev/ttyAMA2', pnpId: '/dev/ttyAMA2' })
×
137
        }
138
        if (fs.existsSync('/dev/ttyAMA3') && isPi()) {
3!
139
        this.serialDevices.push({ value: '/dev/ttyAMA3', label: '/dev/ttyAMA3', pnpId: '/dev/ttyAMA3' })
×
140
        }
141
        if (fs.existsSync('/dev/ttyAMA4') && isPi()) {
3!
142
        this.serialDevices.push({ value: '/dev/ttyAMA4', label: '/dev/ttyAMA4', pnpId: '/dev/ttyAMA4' })
×
143
        }
144
        // rpi uart has different name under Ubuntu
145
        const data = await si.osInfo()
3✔
146
        if (data.distro.toString().includes('Ubuntu') && fs.existsSync('/dev/ttyS0') && isPi()) {
3!
147
        // console.log("Running Ubuntu")
148
        this.serialDevices.push({ value: '/dev/ttyS0', label: '/dev/ttyS0', pnpId: '/dev/ttyS0' })
×
149
        }
150
        // jetson serial ports
151
        if (fs.existsSync('/dev/ttyTHS1')) {
3!
152
        this.serialDevices.push({ value: '/dev/ttyTHS1', label: '/dev/ttyTHS1', pnpId: '/dev/ttyTHS1' })
×
153
        }
154
        if (fs.existsSync('/dev/ttyTHS2')) {
3!
155
        this.serialDevices.push({ value: '/dev/ttyTHS2', label: '/dev/ttyTHS2', pnpId: '/dev/ttyTHS2' })
×
156
        }
157
        if (fs.existsSync('/dev/ttyTHS3')) {
3!
158
        this.serialDevices.push({ value: '/dev/ttyTHS3', label: '/dev/ttyTHS3', pnpId: '/dev/ttyTHS3' })
×
159
        }
160

161
        return callback(retError, this.serialDevices);
3✔
162
    }
163

164
    startPPP(device, baudRate, localIP, remoteIP, callback) {
165
        this.badbaudRate = false;
15✔
166
        if (this.isConnected) {
15✔
167
            return callback(new Error('PPP is already connected'), {
6✔
168
                selDevice: this.device,
169
                selBaudRate: this.baudRate,
170
                localIP: this.localIP,
171
                remoteIP: this.remoteIP,
172
                enabled: this.isConnected,
173
                baudRates: this.baudRates,
174
                serialDevices: this.serialDevices,
175
            });
176
        }
177
        if (!device) {
9✔
178
            return callback(new Error('Device is required'), {
3✔
179
                selDevice: this.device,
180
                selBaudRate: this.baudRate,
181
                localIP: this.localIP,
182
                remoteIP: this.remoteIP,
183
                enabled: this.isConnected,
184
                baudRates: this.baudRates,
185
                serialDevices: this.serialDevices,
186
            });
187
        }
188
        if (this.pppProcess) {
6!
189
            return callback(new Error('PPP still running. Please wait for it to finish.'), {
×
190
                selDevice: this.device,
191
                selBaudRate: this.baudRate,
192
                localIP: this.localIP,
193
                remoteIP: this.remoteIP,
194
                enabled: this.isConnected,
195
                baudRates: this.baudRates,
196
                serialDevices: this.serialDevices,
197
            });
198
        }
199

200
        //ensure device string is valid in the serialdevices list
201
        const validDevice = this.serialDevices.find(d => d.value === device);
6✔
202
        if (!validDevice) {
6!
203
            return callback(new Error('Invalid device selected'), {
6✔
204
                selDevice: this.device,
205
                selBaudRate: this.baudRate,
206
                localIP: this.localIP,
207
                remoteIP: this.remoteIP,
208
                enabled: this.isConnected,
209
                baudRates: this.baudRates,
210
                serialDevices: this.serialDevices,
211
            });
212
        }
213

UNCOV
214
        this.device = device;
×
UNCOV
215
        this.baudRate = baudRate;
×
UNCOV
216
        this.localIP = localIP;
×
UNCOV
217
        this.remoteIP = remoteIP;
×
218
        
UNCOV
219
        const args = [
×
220
            "pppd",
221
            this.device,
222
            this.baudRate, // baud rate
223
            //'persist',          // enables faster termination
224
            //'holdoff', '1',     // minimum delay of 1 second between connection attempts
225
            this.localIP + ':' + this.remoteIP, // local and remote IPs
226
            'local',
227
            'noauth',
228
            //'debug',
229
            'crtscts',
230
            'nodetach',
231
            'ktune'
232
        ];
233
        // if running in dev env, need to preload sudo login
UNCOV
234
        if (process.env.NODE_ENV === 'development') {
×
UNCOV
235
            execSync('sudo -v');
×
236
        }
UNCOV
237
        console.log(`Starting PPP with args: ${args.join(' ')}`);
×
UNCOV
238
        this.pppProcess = spawn('sudo', args, {
×
239
        //detached: true,
240
        stdio: ['ignore', 'pipe', 'pipe'] // or 'ignore' for all three to fully detach
241
        });
UNCOV
242
        this.pppProcess.stdout.on('data', (data) => {
×
243
            console.log("PPP Output: ", data.toString().trim());
×
244
            // Check for non support baud rates "speed <baud> not supported"
245
            if (data.toString().includes('speed') && data.toString().includes('not supported')) {
×
246
                this.pppProcess.kill();
×
247
                this.pppProcess = null; // reset the process reference
×
248
                this.isConnected = false;
×
249
                this.badbaudRate = true;
×
250
            }
251
        });
UNCOV
252
        this.pppProcess.stderr.on('data', (data) => {
×
UNCOV
253
            console.log("PPP Error: ", data.toString().trim());
×
254
        });
UNCOV
255
        this.pppProcess.on('close', (code) => {
×
UNCOV
256
            console.log("PPP process exited with code: ", code.toString().trim());
×
UNCOV
257
            this.isConnected = false;
×
UNCOV
258
            this.pppProcess = null; // reset the process reference
×
UNCOV
259
            this.setSettings();
×
260
        });
UNCOV
261
        this.isConnected = true;
×
UNCOV
262
        this.setSettings();
×
UNCOV
263
        return callback(null, {
×
264
            selDevice: this.device,
265
            selBaudRate: this.baudRate,
266
            localIP: this.localIP,
267
            remoteIP: this.remoteIP,
268
            enabled: this.isConnected,
269
            baudRates: this.baudRates,
270
            serialDevices: this.serialDevices,
271
        });
272
    }
273

274
    stopPPP(callback) {
275
        if (!this.isConnected) {
6✔
276
            return callback(new Error('PPP is not connected'), {
3✔
277
                selDevice: this.device,
278
                selBaudRate: this.baudRate,
279
                localIP: this.localIP,
280
                remoteIP: this.remoteIP,
281
                enabled: this.isConnected,
282
                baudRates: this.baudRates,
283
                serialDevices: this.serialDevices,
284
            });
285
        }
286
        if (this.pppProcess) {
3!
287
            // Gracefully kill the PPP process
288
            console.log('Stopping PPP connection...');
×
289
            this.pppProcess.kill();
×
290
            execSync('sudo pkill -SIGTERM pppd');
×
291
            this.isConnected = false;
×
292
            this.setSettings();
×
293
        }
294
        return callback(null, {
3✔
295
            selDevice: this.device,
296
            selBaudRate: this.baudRate,
297
            localIP: this.localIP,
298
            remoteIP: this.remoteIP,
299
            enabled: this.isConnected,
300
            baudRates: this.baudRates,
301
            serialDevices: this.serialDevices,
302
        });
303
    }
304

305
    getPPPdatarate(callback) {
306
        if (!this.isConnected) {
×
307
            return callback(new Error('PPP is not connected'));
×
308
        }
309
        // get current data transfer stats for connected PPP session
310
        return new Promise((resolve, reject) => {
×
311
            exec('ifconfig ppp0', (error, stdout, stderr) => {
×
312
                if (error) {
×
313
                    reject(`Error getting PPP data rate: ${stderr}`);
×
314
                } else {
315
                    // match format RX packets 110580  bytes 132651067 (132.6 MB)
316
                    const match = stdout.match(/RX packets \d+  bytes (\d+) \(\d+\.\d+ MB\).*TX packets \d+  bytes (\d+) \(\d+\.\d+ MB\)/);
×
317
                    if (match) {
×
318
                        const rxBytes = parseInt(match[1], 10);
×
319
                        const txBytes = parseInt(match[5], 10);
×
320
                        resolve({ rxBytes, txBytes });
×
321
                    } else {
322
                        reject('Could not parse PPP data rate');
×
323
                    }
324
                }
325
            });
326
        });
327
    }
328

329
    getPPPSettings(callback) {
UNCOV
330
        this.getDevices((err, devices) => {
×
UNCOV
331
            if (err) {
×
332
                console.error('Error fetching serial devices:', err);
×
333
                return callback(err, {
×
334
                    selDevice: null,
335
                    selBaudRate: this.baudRate,
336
                    localIP: this.localIP,
337
                    remoteIP: this.remoteIP,
338
                    enabled: this.isConnected,
339
                    baudRates: this.baudRates,
340
                    serialDevices: [],
341
                });
342
            }
343
            
NEW
344
            this.serialDevices = devices;
×
345
            
346
            // Set default device if not already set
NEW
347
            if (!this.device && this.serialDevices.length > 0) {
×
NEW
348
                this.device = this.serialDevices[0].value;
×
349
            }
350
            
351
            // if this.device is not in the list, set it to first available device
NEW
352
            if (this.device && !this.serialDevices.some(d => d.value === this.device.value)) {
×
NEW
353
                this.device = this.serialDevices[0].value;
×
354
            }
355
            
356
            // Always return callback
NEW
357
            return callback(null, {
×
358
                selDevice: this.device,
359
                selBaudRate: this.baudRate,
360
                localIP: this.localIP,
361
                remoteIP: this.remoteIP,
362
                enabled: this.isConnected,
363
                baudRates: this.baudRates,
364
                serialDevices: this.serialDevices,
365
            });
366
        });
367
    }
368

369
    // uses ifconfig to get the PPP connection datarate
370
    getPPPDataRate() {
371
        if (!this.isConnected) {
×
372
            return { rxRate: 0, txRate: 0 };
×
373
        }
374
        // get current data transfer stats for connected PPP session
375
        try {
×
376
            let stdout = execSync('ifconfig ppp0 | grep packets', { encoding: 'utf8' }).toString().trim();
×
377
            if (!stdout) {
×
378
                return { rxRate: 0, txRate: 0, percentusedRx: 0, percentusedTx: 0 };
×
379
            }
380
            // match format :
381
            //        RX packets 0  bytes 0 (0.0 B)
382
            //        TX packets 118  bytes 12232 (12.2 KB)
383
            const [ , matchRX, matchTX ] = stdout.match(/RX\s+packets\s+\d+\s+bytes\s+(\d+).*TX\s+packets\s+\d+\s+bytes\s+(\d+)/s);
×
384
            if (matchRX && matchTX) {
×
385
                const rxBytes = parseInt(matchRX);
×
386
                const txBytes = parseInt(matchTX);
×
387
                // calculate the data rate in bytes per second
388
                if (this.prevdata) {
×
389
                    const elapsed = Date.now() - this.prevdata.timestamp; // in milliseconds
×
390
                    const rxRate = (rxBytes - this.prevdata.rxBytes) / (elapsed / 1000); // bytes per second
×
391
                    const txRate = (txBytes - this.prevdata.txBytes) / (elapsed / 1000); // bytes per second
×
NEW
392
                    const percentusedRx = rxRate / (this.baudRate / 8); // percent of baud rate used
×
NEW
393
                    const percentusedTx = txRate / (this.baudRate / 8); // percent of baud rate used
×
394
                    this.prevdata = { rxBytes, txBytes, timestamp: Date.now() };
×
395
                    return { rxRate, txRate, percentusedRx, percentusedTx };
×
396
                }
397
                this.prevdata = { rxBytes, txBytes, timestamp: Date.now() };
×
398
                return { rxRate: 0, txRate: 0, percentusedRx: 0, percentusedTx: 0 };
×
399
            } else {
400
                return { rxRate: 0, txRate: 0, percentusedRx: 0, percentusedTx: 0};
×
401
            }
402
        } catch (error) {
403
            console.error('Error getting PPP data rate:', error.message);
×
404
            return { rxRate: 0, txRate: 0, percentusedRx: 0, percentusedTx: 0 };
×
405
        }
406
    }
407

408
    // Returns a string representation of the PPP connection status for use by socket.io
409
    conStatusStr () {
410
        //format the connection status string
411
        if (this.badbaudRate) {
×
412
            return 'Disconnected (Baud rate not supported)';
×
413
        }
414
        if (!this.isConnected) {
×
415
            return 'Disconnected';
×
416
        }
417
        if (this.pppProcess && this.pppProcess.pid) {
×
418
            //get datarate
419
            const { rxRate, txRate, percentusedRx, percentusedTx } = this.getPPPDataRate();
×
420
            let status = 'Connected';
×
421
            if (this.pppProcess.pid) {
×
422
                status += ` (PID: ${this.pppProcess.pid})`;
×
423
            }
424
            if (rxRate > 0 || txRate > 0) {
×
425
                status += `, RX: ${rxRate.toFixed(2)} B/s (${(percentusedRx * 100).toFixed(2)}%), TX: ${txRate.toFixed(2)} B/s (${(percentusedTx * 100).toFixed(2)}%)`;
×
426
            } else {
427
                status += ', No data transfer';
×
428
            }
429
            return status;
×
430
        }
431
        else {
NEW
432
            return 'Disconnected';
×
433
        }
434
    }
435
}
436

437

438
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