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

stephendade / Rpanion-server / 19360612817

14 Nov 2025 09:43AM UTC coverage: 34.683% (+0.1%) from 34.547%
19360612817

Pull #343

github

web-flow
Merge ae5d17ea3 into 2164b81a9
Pull Request #343: Add Orange Pi Zero 3 support

328 of 1210 branches covered (27.11%)

Branch coverage included in aggregate %.

23 of 51 new or added lines in 5 files covered. (45.1%)

168 existing lines in 3 files now uncovered.

1008 of 2642 relevant lines covered (38.15%)

2.61 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

NEW
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: 921600, label: '921600' },
56
            { value: 1500000, label: '1.5 MBaud' },
57
            { value: 2000000, label: '2 MBaud' },
58
            { value: 12500000, label: '12.5 MBaud' }];
59
        this.serialDevices = [];
39✔
60
        this.badbaudRate = false; // flag to indicate if the baud rate is not supported
39✔
61
        this.prevdata = null; // previous data for comparison
39✔
62

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

83
            attemptPPPStart();
3✔
84
        }
85
    }
86

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

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

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

114
        const Binding = autoDetect()
3✔
115
        const ports = await Binding.list()
3✔
116

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

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

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

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

UNCOV
232
        this.device = device;
×
UNCOV
233
        this.baudRate = baudRate;
×
UNCOV
234
        this.localIP = localIP;
×
235
        this.remoteIP = remoteIP;
×
236
        
237
        const args = [
×
238
            "pppd",
239
            this.device,
240
            this.baudRate, // baud rate
241
            //'persist',          // enables faster termination
242
            //'holdoff', '1',     // minimum delay of 1 second between connection attempts
243
            this.localIP + ':' + this.remoteIP, // local and remote IPs
244
            'local',
245
            'noauth',
246
            //'debug',
247
            'crtscts',
248
            'nodetach',
249
            'ktune'
250
        ];
251
        // if running in dev env, need to preload sudo login
UNCOV
252
        if (process.env.NODE_ENV === 'development') {
×
UNCOV
253
            execSync('sudo -v');
×
254
        }
255
        console.log(`Starting PPP with args: ${args.join(' ')}`);
×
256
        this.pppProcess = spawn('sudo', args, {
×
257
        //detached: true,
258
        stdio: ['ignore', 'pipe', 'pipe'] // or 'ignore' for all three to fully detach
259
        });
UNCOV
260
        this.pppProcess.stdout.on('data', (data) => {
×
UNCOV
261
            console.log("PPP Output: ", data.toString().trim());
×
262
            // Check for non support baud rates "speed <baud> not supported"
263
            if (data.toString().includes('speed') && data.toString().includes('not supported')) {
×
264
                this.pppProcess.kill();
×
UNCOV
265
                this.pppProcess = null; // reset the process reference
×
266
                this.isConnected = false;
×
267
                this.badbaudRate = true;
×
268
            }
269
        });
270
        this.pppProcess.stderr.on('data', (data) => {
×
UNCOV
271
            console.log("PPP Error: ", data.toString().trim());
×
272
        });
273
        this.pppProcess.on('close', (code) => {
×
274
            console.log("PPP process exited with code: ", code.toString().trim());
×
UNCOV
275
            this.isConnected = false;
×
276
            this.pppProcess = null; // reset the process reference
×
277
            this.setSettings();
×
278
        });
279
        this.isConnected = true;
×
280
        this.setSettings();
×
UNCOV
281
        return callback(null, {
×
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
    stopPPP(callback) {
293
        if (!this.isConnected) {
6✔
294
            return callback(new Error('PPP is not connected'), {
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
        if (this.pppProcess) {
3!
305
            // Gracefully kill the PPP process
UNCOV
306
            console.log('Stopping PPP connection...');
×
UNCOV
307
            this.pppProcess.kill();
×
UNCOV
308
            execSync('sudo pkill -SIGTERM pppd');
×
309
            this.isConnected = false;
×
310
            this.setSettings();
×
311
        }
312
        return callback(null, {
3✔
313
            selDevice: this.device,
314
            selBaudRate: this.baudRate,
315
            localIP: this.localIP,
316
            remoteIP: this.remoteIP,
317
            enabled: this.isConnected,
318
            baudRates: this.baudRates,
319
            serialDevices: this.serialDevices,
320
        });
321
    }
322

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

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

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

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

455

456
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