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

stephendade / Rpanion-server / 18556818853

16 Oct 2025 09:31AM UTC coverage: 35.93% (+0.009%) from 35.921%
18556818853

Pull #332

github

web-flow
Merge 71353902b into 6e6c2fa47
Pull Request #332: CI: Restore package regen for arm64

322 of 1161 branches covered (27.73%)

Branch coverage included in aggregate %.

0 of 3 new or added lines in 1 file covered. (0.0%)

240 existing lines in 2 files now uncovered.

1020 of 2574 relevant lines covered (39.63%)

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
            'proxyarp',
218
            'ktune'
219
        ];
220
        // if running in dev env, need to preload sudo login
221
        if (process.env.NODE_ENV === 'development') {
6!
222
            execSync('sudo -v');
6✔
223
        }
224
        console.log(`Starting PPP with args: ${args.join(' ')}`);
6✔
225
        this.pppProcess = spawn('sudo', args, {
6✔
226
        //detached: true,
227
        stdio: ['ignore', 'pipe', 'pipe'] // or 'ignore' for all three to fully detach
228
        });
229
        this.pppProcess.stdout.on('data', (data) => {
6✔
UNCOV
230
            console.log("PPP Output: ", data.toString().trim());
×
231
            // Check for non support baud rates "speed <baud> not supported"
232
            if (data.toString().includes('speed') && data.toString().includes('not supported')) {
×
233
                this.pppProcess.kill();
×
234
                this.pppProcess = null; // reset the process reference
×
235
                this.isConnected = false;
×
UNCOV
236
                this.badbaudRate = true;
×
237
            }
238
        });
239
        this.pppProcess.stderr.on('data', (data) => {
6✔
240
            console.log("PPP Error: ", data.toString().trim());
7✔
241
        });
242
        this.pppProcess.on('close', (code) => {
6✔
243
            console.log("PPP process exited with code: ", code.toString().trim());
6✔
244
            this.isConnected = false;
6✔
245
            this.pppProcess = null; // reset the process reference
6✔
246
            this.setSettings();
6✔
247
        });
248
        this.isConnected = true;
6✔
249
        this.setSettings();
6✔
250
        return callback(null, {
6✔
251
            selDevice: this.device,
252
            selBaudRate: this.baudRate,
253
            localIP: this.localIP,
254
            remoteIP: this.remoteIP,
255
            enabled: this.isConnected,
256
            baudRates: this.baudRates,
257
            serialDevices: this.serialDevices,
258
        });
259
    }
260

261
    stopPPP(callback) {
262
        if (!this.isConnected) {
6✔
263
            return callback(new Error('PPP is not connected'), {
3✔
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
        if (this.pppProcess) {
3!
274
            // Gracefully kill the PPP process
275
            console.log('Stopping PPP connection...');
×
276
            this.pppProcess.kill();
×
277
            execSync('sudo pkill -SIGTERM pppd');
×
278
            this.isConnected = false;
×
UNCOV
279
            this.setSettings();
×
280
        }
281
        return callback(null, {
3✔
282
            selDevice: this.device,
283
            selBaudRate: this.baudRate,
284
            localIP: this.localIP,
285
            remoteIP: this.remoteIP,
286
            enabled: this.isConnected,
287
            baudRates: this.baudRates,
288
            serialDevices: this.serialDevices,
289
        });
290
    }
291

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

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

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

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

417

418
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