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

stephendade / Rpanion-server / 18647213774

20 Oct 2025 08:56AM UTC coverage: 35.8% (-0.1%) from 35.921%
18647213774

push

github

stephendade
Build: Fix package versions

322 of 1165 branches covered (27.64%)

Branch coverage included in aggregate %.

1018 of 2578 relevant lines covered (39.49%)

2.87 hits per line

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

45.95
/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 = ''
6✔
15
  try {
6✔
16
    cpuInfo = fs.readFileSync('/proc/device-tree/compatible', { encoding: 'utf8' })
6✔
17
  } catch (e) {
18
    // if this fails, this is probably not a pi
19
    return false
6✔
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
42✔
36
        this.isConnected = this.settings.value('ppp.enabled', false);
42✔
37
        this.pppProcess = null;
42✔
38
        this.device = this.settings.value('ppp.uart', null);
42✔
39
        this.baudRate = this.settings.value('ppp.baud', { value: 921600, label: '921600' })
42✔
40
        this.localIP = this.settings.value('ppp.localIP', '192.168.144.14');  // default local IP
42✔
41
        this.remoteIP = this.settings.value('ppp.remoteIP', '192.168.144.15'); // default remote IP
42✔
42
        this.baudRates = [
42✔
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 = [];
42✔
48
        this.badbaudRate = false; // flag to indicate if the baud rate is not supported
42✔
49
        this.prevdata = null; // previous data for comparison
42✔
50

51
        if (this.isConnected) {
42✔
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')) {
3!
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);
×
62
                            this.isConnected = false;
×
63
                            this.setSettings();
×
64
                        }
65
                    } else {
66
                        console.log('PPP connection started successfully');
3✔
67
                    }
68
                });
69
            };
70

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

75
    setSettings() {
76
        this.settings.setValue('ppp.uart', this.device);
18✔
77
        this.settings.setValue('ppp.baud', this.baudRate);
18✔
78
        this.settings.setValue('ppp.localIP', this.localIP);
18✔
79
        this.settings.setValue('ppp.remoteIP', this.remoteIP);
18✔
80
        this.settings.setValue('ppp.enabled', this.isConnected);
18✔
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 = []
6✔
100
        let retError = null
6✔
101

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

105
        for (let i = 0, len = ports.length; i < len; i++) {
6✔
106
            if (ports[i].pnpId !== undefined) {
192!
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) {
192!
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()) {
6!
126
        this.serialDevices.push({ value: '/dev/serial0', label: '/dev/serial0', pnpId: '/dev/serial0' })
×
127
        }
128
        if (fs.existsSync('/dev/ttyAMA0') && isPi()) {
6!
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()) {
6!
133
        this.serialDevices.push({ value: '/dev/ttyAMA1', label: '/dev/ttyAMA1', pnpId: '/dev/ttyAMA1' })
×
134
        }
135
        if (fs.existsSync('/dev/ttyAMA2') && isPi()) {
6!
136
        this.serialDevices.push({ value: '/dev/ttyAMA2', label: '/dev/ttyAMA2', pnpId: '/dev/ttyAMA2' })
×
137
        }
138
        if (fs.existsSync('/dev/ttyAMA3') && isPi()) {
6!
139
        this.serialDevices.push({ value: '/dev/ttyAMA3', label: '/dev/ttyAMA3', pnpId: '/dev/ttyAMA3' })
×
140
        }
141
        if (fs.existsSync('/dev/ttyAMA4') && isPi()) {
6!
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()
6✔
146
        if (data.distro.toString().includes('Ubuntu') && fs.existsSync('/dev/ttyS0') && isPi()) {
6!
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')) {
6!
152
        this.serialDevices.push({ value: '/dev/ttyTHS1', label: '/dev/ttyTHS1', pnpId: '/dev/ttyTHS1' })
×
153
        }
154
        if (fs.existsSync('/dev/ttyTHS2')) {
6!
155
        this.serialDevices.push({ value: '/dev/ttyTHS2', label: '/dev/ttyTHS2', pnpId: '/dev/ttyTHS2' })
×
156
        }
157
        if (fs.existsSync('/dev/ttyTHS3')) {
6!
158
        this.serialDevices.push({ value: '/dev/ttyTHS3', label: '/dev/ttyTHS3', pnpId: '/dev/ttyTHS3' })
×
159
        }
160

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

260
    stopPPP(callback) {
261
        if (!this.isConnected) {
6✔
262
            return callback(new Error('PPP is not connected'), {
3✔
263
                selDevice: this.device,
264
                selBaudRate: this.baudRate,
265
                localIP: this.localIP,
266
                remoteIP: this.remoteIP,
267
                enabled: this.isConnected,
268
                baudRates: this.baudRates,
269
                serialDevices: this.serialDevices,
270
            });
271
        }
272
        if (this.pppProcess) {
3!
273
            // Gracefully kill the PPP process
274
            console.log('Stopping PPP connection...');
×
275
            this.pppProcess.kill();
×
276
            execSync('sudo pkill -SIGTERM pppd');
×
277
            this.isConnected = false;
×
278
            this.setSettings();
×
279
        }
280
        return callback(null, {
3✔
281
            selDevice: this.device,
282
            selBaudRate: this.baudRate,
283
            localIP: this.localIP,
284
            remoteIP: this.remoteIP,
285
            enabled: this.isConnected,
286
            baudRates: this.baudRates,
287
            serialDevices: this.serialDevices,
288
        });
289
    }
290

291
    getPPPdatarate(callback) {
292
        if (!this.isConnected) {
×
293
            return callback(new Error('PPP is not connected'));
×
294
        }
295
        // get current data transfer stats for connected PPP session
296
        return new Promise((resolve, reject) => {
×
297
            exec('ifconfig ppp0', (error, stdout, stderr) => {
×
298
                if (error) {
×
299
                    reject(`Error getting PPP data rate: ${stderr}`);
×
300
                } else {
301
                    // match format RX packets 110580  bytes 132651067 (132.6 MB)
302
                    const match = stdout.match(/RX packets \d+  bytes (\d+) \(\d+\.\d+ MB\).*TX packets \d+  bytes (\d+) \(\d+\.\d+ MB\)/);
×
303
                    if (match) {
×
304
                        const rxBytes = parseInt(match[1], 10);
×
305
                        const txBytes = parseInt(match[5], 10);
×
306
                        resolve({ rxBytes, txBytes });
×
307
                    } else {
308
                        reject('Could not parse PPP data rate');
×
309
                    }
310
                }
311
            });
312
        });
313
    }
314

315
    getPPPSettings(callback) {
316
        this.getDevices((err, devices) => {
3✔
317
            if (err) {
3!
318
                console.error('Error fetching serial devices:', err);
×
319
                return callback(err, {
×
320
                    selDevice: null,
321
                    selBaudRate: this.baudRate,
322
                    localIP: this.localIP,
323
                    remoteIP: this.remoteIP,
324
                    enabled: this.isConnected,
325
                    baudRates: this.baudRates,
326
                    serialDevices: [],
327
                });
328
            } else {
329
                this.serialDevices = devices;
3✔
330
                // Set default device if not already set
331
                if (!this.device && this.serialDevices.length > 0) {
3!
332
                    this.device = this.serialDevices[0]; // Set first available device as default
×
333
                }
334
                // if this.device is not in the list, set it to first available device
335
                if (this.device && !this.serialDevices.some(d => d.value === this.device.value)) {
3!
336
                    this.device = this.serialDevices[0];
3✔
337
                }
338
                return callback(null, {
3✔
339
                    selDevice: this.device,
340
                    selBaudRate: this.baudRate,
341
                    localIP: this.localIP,
342
                    remoteIP: this.remoteIP,
343
                    enabled: this.isConnected,
344
                    baudRates: this.baudRates,
345
                    serialDevices: this.serialDevices,
346
                });
347
            }
348
        });
349
    }
350

351
    // uses ifconfig to get the PPP connection datarate
352
    getPPPDataRate() {
353
        if (!this.isConnected) {
×
354
            return { rxRate: 0, txRate: 0 };
×
355
        }
356
        // get current data transfer stats for connected PPP session
357
        try {
×
358
            let stdout = execSync('ifconfig ppp0 | grep packets', { encoding: 'utf8' }).toString().trim();
×
359
            if (!stdout) {
×
360
                return { rxRate: 0, txRate: 0, percentusedRx: 0, percentusedTx: 0 };
×
361
            }
362
            // match format :
363
            //        RX packets 0  bytes 0 (0.0 B)
364
            //        TX packets 118  bytes 12232 (12.2 KB)
365
            const [ , matchRX, matchTX ] = stdout.match(/RX\s+packets\s+\d+\s+bytes\s+(\d+).*TX\s+packets\s+\d+\s+bytes\s+(\d+)/s);
×
366
            if (matchRX && matchTX) {
×
367
                const rxBytes = parseInt(matchRX);
×
368
                const txBytes = parseInt(matchTX);
×
369
                // calculate the data rate in bytes per second
370
                if (this.prevdata) {
×
371
                    const elapsed = Date.now() - this.prevdata.timestamp; // in milliseconds
×
372
                    const rxRate = (rxBytes - this.prevdata.rxBytes) / (elapsed / 1000); // bytes per second
×
373
                    const txRate = (txBytes - this.prevdata.txBytes) / (elapsed / 1000); // bytes per second
×
374
                    const percentusedRx = rxRate / (this.baudRate.value / 8); // percent of baud rate used
×
375
                    const percentusedTx = txRate / (this.baudRate.value / 8); // percent of baud rate used
×
376
                    this.prevdata = { rxBytes, txBytes, timestamp: Date.now() };
×
377
                    return { rxRate, txRate, percentusedRx, percentusedTx };
×
378
                }
379
                this.prevdata = { rxBytes, txBytes, timestamp: Date.now() };
×
380
                return { rxRate: 0, txRate: 0, percentusedRx: 0, percentusedTx: 0 };
×
381
            } else {
382
                return { rxRate: 0, txRate: 0, percentusedRx: 0, percentusedTx: 0};
×
383
            }
384
        } catch (error) {
385
            console.error('Error getting PPP data rate:', error.message);
×
386
            return { rxRate: 0, txRate: 0, percentusedRx: 0, percentusedTx: 0 };
×
387
        }
388
    }
389

390
    // Returns a string representation of the PPP connection status for use by socket.io
391
    conStatusStr () {
392
        //format the connection status string
393
        if (this.badbaudRate) {
×
394
            return 'Disconnected (Baud rate not supported)';
×
395
        }
396
        if (!this.isConnected) {
×
397
            return 'Disconnected';
×
398
        }
399
        if (this.pppProcess && this.pppProcess.pid) {
×
400
            //get datarate
401
            const { rxRate, txRate, percentusedRx, percentusedTx } = this.getPPPDataRate();
×
402
            let status = 'Connected';
×
403
            if (this.pppProcess.pid) {
×
404
                status += ` (PID: ${this.pppProcess.pid})`;
×
405
            }
406
            if (rxRate > 0 || txRate > 0) {
×
407
                status += `, RX: ${rxRate.toFixed(2)} B/s (${(percentusedRx * 100).toFixed(2)}%), TX: ${txRate.toFixed(2)} B/s (${(percentusedTx * 100).toFixed(2)}%)`;
×
408
            } else {
409
                status += ', No data transfer';
×
410
            }
411
            return status;
×
412
        }
413
    }
414
}
415

416

417
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