GCC Code Coverage Report


Directory: ./
File: channeloutput/serialutil.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 163 213 76.5%
Functions: 12 12 100.0%
Branches: 129 281 45.9%

Line Branch Exec Source
1 /*
2 * This file is part of the Falcon Player (FPP) and is Copyright (C)
3 * 2013-2022 by the Falcon Player Developers.
4 *
5 * The Falcon Player (FPP) is free software, and is covered under
6 * multiple Open Source licenses. Please see the included 'LICENSES'
7 * file for descriptions of what files are covered by each license.
8 *
9 * This source file is covered under the LGPL v2.1 as described in the
10 * included LICENSE.LGPL file.
11 */
12
13 #include "fpp-pch.h"
14
15 #include "../common_mini.h" // FPPstrerror() -- needed directly for NOPCH builds
16
17 #include <sys/ioctl.h>
18 #include <algorithm>
19 #include <map>
20 #include <memory>
21 #include <mutex>
22 #include <errno.h>
23 #include <fcntl.h>
24 #include <limits.h>
25 #include <poll.h>
26 #include <stdlib.h>
27 #include <string.h>
28 #include <termios.h>
29 #include <unistd.h>
30
31 #include "../log.h"
32 #include "../util/GPIOUtils.h"
33 #include "serialutil.h"
34 #include "ChannelOutputSetup.h"
35
36 #ifdef PLATFORM_OSX
37 #include <IOKit/serial/ioss.h>
38 #else
39 // The following is in asm-generic/termios.h, but including that in a C++
40 // program causes errors. The Qt developers worked around this by including
41 // the following struct definition and #define's manually:
42 // https://codereview.qt-project.org/#/c/125161/6/src/serialport/qserialport_unix.cpp,unified
43
44 struct termios2 {
45 tcflag_t c_iflag; /* input mode flags */
46 tcflag_t c_oflag; /* output mode flags */
47 tcflag_t c_cflag; /* control mode flags */
48 tcflag_t c_lflag; /* local mode flags */
49 cc_t c_line; /* line discipline */
50 cc_t c_cc[19]; /* control characters */
51 speed_t c_ispeed; /* input speed */
52 speed_t c_ospeed; /* output speed */
53 };
54
55 #ifndef TCGETS2
56 #define TCGETS2 _IOR('T', 0x2A, struct termios2)
57 #endif
58
59 #ifndef TCSETS2
60 #define TCSETS2 _IOW('T', 0x2B, struct termios2)
61 #endif
62
63 #ifndef BOTHER
64 #define BOTHER 0010000
65 #endif
66 #endif
67
68 /*
69 * Portions of code based on work found in xLights/serial_posix.cpp
70 * by Matt Brown and Joachim Buermann.
71 */
72
73 39 speed_t SerialGetBaudRate(int baud) {
74
6/7
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 9 times.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 16 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 6 times.
✓ Branch 6 taken 6 times.
39 switch (baud) {
75 1 case 9600:
76 1 return B9600;
77 9 case 19200:
78 9 return B19200;
79 1 case 57600:
80 1 return B57600;
81 16 case 115200:
82 16 return B115200;
83 ✗ case 230400:
84 ✗ return B230400;
85 #ifndef PLATFORM_OSX
86 6 case 1000000:
87 6 return B1000000;
88 #endif
89 6 default:
90 6 return B38400;
91 }
92 }
93
94 // Shared, so a write still running on an output's thread when the output
95 // closes its port (USBDMX closes before stopping the thread) keeps the backend
96 // alive until it returns.
97 static std::mutex backendsLock;
98 static std::map<int, std::shared_ptr<SerialPortBackend>> backends;
99
100 788 static std::shared_ptr<SerialPortBackend> getBackend(int fd) {
101
1/2
✓ Branch 0 taken 788 times.
✗ Branch 1 not taken.
788 std::lock_guard<std::mutex> l(backendsLock);
102
1/2
✓ Branch 0 taken 788 times.
✗ Branch 1 not taken.
788 auto it = backends.find(fd);
103
2/2
✓ Branch 0 taken 710 times.
✓ Branch 1 taken 78 times.
1576 return it == backends.end() ? nullptr : it->second;
104 788 }
105
106 1 int SerialAttachBackend(SerialPortBackend* backend) {
107 // a real descriptor, so the fd >= 0 checks callers make hold and nothing
108 // else can be handed the same number while the backend lives; a stray
109 // write() to it is discarded
110
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 int fd = open("/dev/null", O_WRONLY | O_CLOEXEC);
111
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (fd < 0) {
112 ✗ delete backend;
113 ✗ return -1;
114 }
115
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 std::lock_guard<std::mutex> l(backendsLock);
116
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 backends[fd].reset(backend);
117 1 return fd;
118 1 }
119
120 // The kernel console carries boot messages and a login prompt. On a
121 // PocketBeagle 2 it is ttyS2, the name a PocketBeagle cape used for DMX, so a
122 // config carried over from one would otherwise mix lighting data into it.
123 77 bool SerialIsConsole(const char* device) {
124 #ifdef PLATFORM_OSX
125 return false;
126 #else
127 char resolved[PATH_MAX];
128
2/2
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 68 times.
77 if (!realpath(device, resolved)) {
129 9 return false;
130 }
131 68 const char* name = strrchr(resolved, '/');
132
1/2
✓ Branch 0 taken 68 times.
✗ Branch 1 not taken.
68 name = name ? name + 1 : resolved;
133
1/2
✓ Branch 0 taken 68 times.
✗ Branch 1 not taken.
68 FILE* f = fopen("/sys/class/tty/console/active", "r");
134
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 68 times.
68 if (!f) {
135 ✗ return false;
136 }
137 char con[64];
138 68 bool found = false;
139
7/8
✓ Branch 0 taken 196 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 196 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 129 times.
✓ Branch 5 taken 67 times.
✓ Branch 6 taken 129 times.
✓ Branch 7 taken 68 times.
197 while (!found && fscanf(f, "%63s", con) == 1) {
140 129 found = strcmp(con, name) == 0;
141 }
142
1/2
✓ Branch 0 taken 68 times.
✗ Branch 1 not taken.
68 fclose(f);
143 68 return found;
144 #endif
145 }
146
147 /*
148 *
149 * Example: SerialOpen("/dev/ttyUSB0", 115200, "N81");
150 */
151 39 int SerialOpen(const char* device, int baud, const char* mode, bool output, bool configurePin, const char* desc) {
152
2/4
✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 39 times.
39 if (SerialIsConsole(device)) {
153 // the console is set on the kernel command line, so the config has
154 // to change; like the other open failures here it comes down when
155 // the output's config is next loaded (see AddOutputConfigWarning)
156 ✗ LogErr(VB_CHANNELOUT, "%s: is the system console, not opening it\n", device);
157 ✗ AddOutputConfigWarning(38, std::string(device) + ": is the system console and cannot be used for channel data");
158 ✗ return -1;
159 }
160 // some devices may multiplex pins and need to
161 // specifically configure the output pin as a uart instead of gpio
162 char buf[256];
163
2/2
✓ Branch 0 taken 37 times.
✓ Branch 1 taken 2 times.
39 if (output) {
164 37 snprintf(buf, sizeof(buf), "%s-tx", &device[5]); // "ttyS1-tx" or "ttyUSB0-tx"
165 } else {
166 2 snprintf(buf, sizeof(buf), "%s-rx", &device[5]); // "ttyS1-rx" or "ttyUSB0-rx"
167 }
168
4/4
✓ Branch 0 taken 36 times.
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 33 times.
✓ Branch 3 taken 3 times.
39 if (configurePin && strncmp(device, "/dev/ttyUSB", 11) != 0) {
169 // don't attempt to configure USB uarts as they wouldn't have a pin.
170 // Nor anything else with no UART pin: a USB adapter named by its
171 // stable /dev/serial/by-id path, or a ttyACM, finds no pin, and
172 // configuring the empty placeholder raises a warning banner.
173
2/4
✓ Branch 0 taken 33 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 33 times.
✗ Branch 3 not taken.
33 const PinCapabilities& pin = PinCapabilities::getPinByUART(buf);
174
2/4
✓ Branch 0 taken 33 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 33 times.
33 if (pin.ptr()) {
175 ✗ pin.configPin("uart", output, desc);
176 }
177 }
178
179 39 int fd = 0;
180 struct termios tty;
181
1/2
✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
39 speed_t adjustedBaud = SerialGetBaudRate(baud);
182
183
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 39 times.
39 if (strlen(mode) != 3) {
184 ✗ LogErr(VB_CHANNELOUT, "%s: Invalid Serial Port Mode: %s\n", device, mode);
185 ✗ AddOutputConfigWarning(38, std::string(device) + ": invalid serial port mode '" + mode + "'");
186 ✗ return -1;
187 }
188
189
3/4
✓ Branch 0 taken 37 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 39 times.
✗ Branch 3 not taken.
39 fd = open(device, (output ? O_RDWR : O_RDONLY) | O_NOCTTY | O_NONBLOCK);
190
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 34 times.
39 if (fd < 0) {
191 // errno has to be captured before LogErr, which can clobber it
192 5 int err = errno;
193
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 LogErr(VB_CHANNELOUT, "%s: Error opening serial port: %s\n", device, FPPstrerror(err));
194
6/12
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 5 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 5 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 5 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 5 times.
✗ Branch 11 not taken.
10 AddOutputConfigWarning(38, std::string(device) + ": could not open serial port (" + FPPstrerror(err) + ")");
195 5 return -1;
196 }
197
198
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 33 times.
34 if (ioctl(fd, TIOCEXCL) == -1) {
199
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogErr(VB_CHANNELOUT, "%s: Error setting port to exclusive mode\n", device);
200
3/6
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
1 AddOutputConfigWarning(38, std::string(device) + ": could not set serial port to exclusive mode (in use by another process?)");
201
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 close(fd);
202 1 return -1;
203 }
204
205
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 33 times.
33 if (tcgetattr(fd, &tty) == -1) {
206 ✗ LogErr(VB_CHANNELOUT, "%s: Error getting port attributes\n", device);
207 ✗ AddOutputConfigWarning(38, std::string(device) + ": could not read serial port attributes");
208 ✗ close(fd);
209 ✗ return -1;
210 }
211
212
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 33 times.
33 if (cfsetspeed(&tty, adjustedBaud) == -1) {
213 ✗ LogErr(VB_CHANNELOUT, "%s: Error setting port speed\n", device);
214 ✗ AddOutputConfigWarning(38, std::string(device) + ": could not set serial port speed");
215 ✗ close(fd);
216 ✗ return -1;
217 }
218
219 // Raw mode first: cfmakeraw() resets the character size to 8 bits and
220 // clears parity, so applied after the mode below it turned "7E1" or "8E1"
221 // into 8N1.
222 33 cfmakeraw(&tty);
223
224 33 tty.c_cflag &= ~CSIZE;
225
2/4
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 32 times.
33 switch (mode[0]) {
226 ✗ case '5':
227 ✗ tty.c_cflag |= CS5;
228 ✗ break;
229 ✗ case '6':
230 ✗ tty.c_cflag |= CS6;
231 ✗ break;
232 1 case '7':
233 1 tty.c_cflag |= CS7;
234 1 break;
235 32 default:
236 32 tty.c_cflag |= CS8;
237 32 break;
238 }
239
240
3/4
✓ Branch 0 taken 31 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 1 times.
33 switch (mode[1]) {
241 31 case 'N':
242 31 tty.c_cflag &= ~PARENB;
243 31 break;
244 ✗ case 'O':
245 ✗ tty.c_cflag |= PARENB;
246 ✗ tty.c_cflag |= PARODD;
247 ✗ break;
248 1 case 'E':
249 1 tty.c_cflag |= PARENB;
250 1 tty.c_cflag &= ~PARODD;
251 1 break;
252 }
253
254
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 29 times.
33 switch (mode[2]) {
255 4 case '2':
256 4 tty.c_cflag |= CSTOPB;
257 4 break;
258 29 default:
259 29 tty.c_cflag &= ~CSTOPB;
260 29 break;
261 }
262
263 33 tty.c_cc[VMIN] = 0;
264 33 tty.c_cc[VTIME] = 0;
265
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 32 times.
33 if (!output) {
266 1 tty.c_iflag |= BRKINT;
267 }
268
269
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 33 times.
33 if (tcsetattr(fd, TCSANOW, &tty) == -1) {
270 ✗ LogErr(VB_CHANNELOUT, "%s: Error setting port attributes\n", device);
271 ✗ AddOutputConfigWarning(38, std::string(device) + ": could not set serial port attributes");
272 ✗ close(fd);
273 ✗ return -1;
274 }
275
276
3/4
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 28 times.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
33 if ((adjustedBaud == B38400) &&
277 (baud != 38400)) {
278
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 LogInfo(VB_CHANNELOUT, "%s: Using custom baud rate of %d\n", device, baud);
279
280 #ifdef PLATFORM_OSX
281 if (ioctl(fd, IOSSIOSPEED, &adjustedBaud) == -1) {
282 LogErr(VB_CHANNELOUT, "Error %d calling ioctl( ..., IOSSIOSPEED, ... )\n", errno);
283 close(fd);
284 return -1;
285 }
286 #else
287 struct termios2 tio;
288
289
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
5 if (ioctl(fd, TCGETS2, &tio) < 0) {
290 ✗ LogErr(VB_CHANNELOUT, "Error getting termios2 settings\n");
291 ✗ close(fd);
292 ✗ return -1;
293 }
294
295 5 tio.c_cflag &= ~CBAUD;
296 5 tio.c_cflag |= BOTHER;
297 5 tio.c_ispeed = baud;
298 5 tio.c_ospeed = baud;
299
300
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
5 if (ioctl(fd, TCSETS2, &tio) < 0) {
301 ✗ LogErr(VB_CHANNELOUT, "%s: Error setting custom baud rate\n", device);
302 ✗ AddOutputConfigWarning(38, std::string(device) + ": could not set custom baud rate");
303 ✗ close(fd);
304 ✗ return -1;
305 }
306 #endif
307 }
308
309 33 return fd;
310 }
311 2 int SerialOpen(const char* device, int baud, const char* mode, bool output) {
312 2 return SerialOpen(device, baud, mode, output, true, "");
313 }
314 34 int SerialClose(int fd) {
315
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 34 times.
34 if (fd < 0)
316 ✗ return -1;
317
318 34 bool isBackend = false;
319 {
320
1/2
✓ Branch 0 taken 34 times.
✗ Branch 1 not taken.
34 std::lock_guard<std::mutex> l(backendsLock);
321
1/2
✓ Branch 0 taken 34 times.
✗ Branch 1 not taken.
34 isBackend = backends.erase(fd) != 0;
322 34 }
323
2/2
✓ Branch 0 taken 33 times.
✓ Branch 1 taken 1 times.
34 if (!isBackend) {
324 33 tcflush(fd, TCOFLUSH);
325 }
326
327 34 return close(fd);
328 }
329
330 7 int SerialSendBreak(int fd, int duration) {
331
2/4
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 7 times.
7 if (auto b = getBackend(fd)) {
332 ✗ b->sendBreak(duration);
333 ✗ return 0;
334
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 }
335 7 ioctl(fd, TIOCSBRK);
336 7 usleep(duration);
337 7 ioctl(fd, TIOCCBRK);
338
339 7 return 0;
340 }
341
342 2 int SerialResetRTS(int fd) {
343
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
2 if (getBackend(fd)) {
344 ✗ return 0;
345 }
346 2 int CtrlFlag = TIOCM_RTS;
347
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
2 if (ioctl(fd, TIOCMBIC, &CtrlFlag) < 0) {
348
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogErr(VB_CHANNELOUT, "Error clearing RTS \n");
349 1 return -1;
350 }
351
352 1 return 0;
353 }
354
355 380 int SerialWriteAll(int fd, const void* data, int len, int timeoutMs) {
356
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 380 times.
380 if (fd < 0) {
357 ✗ return -1;
358 }
359
3/4
✓ Branch 0 taken 380 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 78 times.
✓ Branch 3 taken 302 times.
380 if (auto b = getBackend(fd)) {
360
1/2
✓ Branch 0 taken 78 times.
✗ Branch 1 not taken.
78 return b->write((const uint8_t*)data, len, timeoutMs);
361
2/2
✓ Branch 0 taken 302 times.
✓ Branch 1 taken 78 times.
380 }
362 302 const char* buf = (const char*)data;
363 302 uint64_t deadline = GetMonotonicTimeMS() + timeoutMs;
364 302 int sent = 0;
365
2/2
✓ Branch 0 taken 454 times.
✓ Branch 1 taken 288 times.
742 while (sent < len) {
366
1/2
✓ Branch 0 taken 454 times.
✗ Branch 1 not taken.
454 ssize_t w = write(fd, buf + sent, len - sent);
367
2/2
✓ Branch 0 taken 332 times.
✓ Branch 1 taken 122 times.
454 if (w > 0) {
368 332 sent += w;
369 332 continue;
370 }
371
2/4
✓ Branch 0 taken 122 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 122 times.
122 if (w < 0 && errno == EINTR) {
372 ✗ continue;
373 }
374
4/6
✓ Branch 0 taken 122 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 120 times.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
122 if (w < 0 && errno != EAGAIN && errno != EWOULDBLOCK) {
375 14 break;
376 }
377
1/2
✓ Branch 0 taken 120 times.
✗ Branch 1 not taken.
120 long long left = (long long)deadline - (long long)GetMonotonicTimeMS();
378
2/2
✓ Branch 0 taken 12 times.
✓ Branch 1 taken 108 times.
120 if (left <= 0) {
379 12 break;
380 }
381 struct pollfd pfd;
382 108 pfd.fd = fd;
383 108 pfd.events = POLLOUT;
384 108 pfd.revents = 0;
385
1/2
✓ Branch 0 taken 108 times.
✗ Branch 1 not taken.
108 poll(&pfd, 1, (int)std::min<long long>(left, 100));
386 }
387 302 return sent;
388 }
389
390 392 int SerialWrite(int fd, const void* data, int len) {
391
2/4
✓ Branch 0 taken 392 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 392 times.
392 if (auto b = getBackend(fd)) {
392 ✗ return b->write((const uint8_t*)data, len, 0);
393
1/2
✓ Branch 0 taken 392 times.
✗ Branch 1 not taken.
392 }
394 392 return write(fd, data, len);
395 }
396
397 7 bool SerialWaitDrained(int fd, int timeoutMs) {
398
2/4
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 7 times.
7 if (auto b = getBackend(fd)) {
399 ✗ return b->waitDrained(timeoutMs);
400
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 }
401 7 uint64_t deadline = GetMonotonicTimeMS() + timeoutMs;
402 while (true) {
403 30 int queued = 0;
404
5/6
✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✓ Branch 3 taken 23 times.
✓ Branch 4 taken 7 times.
✓ Branch 5 taken 23 times.
30 if (ioctl(fd, TIOCOUTQ, &queued) < 0 || queued <= 0) {
405 7 return true;
406 }
407
2/4
✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 23 times.
23 if (GetMonotonicTimeMS() >= deadline) {
408 ✗ return false;
409 }
410
1/2
✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
23 usleep(500);
411 23 }
412 }
413