GCC Code Coverage Report


Directory: ./
File: channeloutput/LORSerial.h
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 38 39 97.4%
Functions: 7 7 100.0%
Branches: 22 36 61.1%

Line Branch Exec Source
1 #pragma once
2 /*
3 * This file is part of the Falcon Player (FPP) and is Copyright (C)
4 * 2013-2026 by the Falcon Player Developers.
5 *
6 * The Falcon Player (FPP) is free software, and is covered under
7 * multiple Open Source licenses. Please see the included 'LICENSES'
8 * file for descriptions of what files are covered by each license.
9 *
10 * This source file is covered under the GPL v2 as described in the
11 * included LICENSE.GPL file.
12 */
13
14 // Serial plumbing shared by the LOR and LOR Enhanced outputs. Each is its
15 // own plugin and compiles this in; it is not part of the plugin API.
16
17 #include <sys/ioctl.h>
18 #include <algorithm>
19 #include <chrono>
20 #include <condition_variable>
21 #include <mutex>
22 #include <thread>
23 #include <unistd.h>
24
25 #include "../common.h"
26
27 #include "SerialChannelOutput.h"
28 #include "serialutil.h"
29
30 #define LOR_HEARTBEAT_SIZE 5
31
32 // Units go inactive after 2 seconds without a heartbeat, blank their
33 // outputs, and can need several heartbeats before they act on commands
34 // again, so the heartbeat runs for as long as the port is open, not only
35 // while frames are being sent.
36 #define LOR_HEARTBEAT_INTERVAL 300000
37
38 // Marks a channel whose state on the unit is not known: never sent since
39 // the output opened, or its command was cut off by a short write.
40 #define LOR_UNKNOWN 0x100
41
42 // A frame's commands go out in one write(), capped below what the tty can
43 // queue so the non-blocking fd does not drop the tail of a batch.
44 #define LOR_MAX_BATCH_SIZE 4096
45
46 // Frame interval assumed for the first frame, and the range a measured
47 // interval is clamped to when sizing a frame's byte budget.
48 #define LOR_DEFAULT_FRAME_TIME 50000
49 #define LOR_MIN_FRAME_TIME 10000
50 #define LOR_MAX_FRAME_TIME 100000
51
52 // Shortest time the background refresh takes to resend every channel.
53 #define LOR_REFRESH_PERIOD 1000000
54
55 class LORSerialOutput : public SerialChannelOutput {
56 public:
57 32 virtual ~LORSerialOutput() {
58 32 StopHeartbeat();
59 32 }
60
61 14 void StartHeartbeat() {
62 14 stopHeartbeat = false;
63
1/2
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
28 heartbeatThread = std::thread([this]() { HeartbeatLoop(); });
64 14 }
65 30 void StopHeartbeat() {
66
2/2
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 16 times.
30 if (heartbeatThread.joinable()) {
67 {
68
1/2
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
14 std::unique_lock<std::mutex> lk(lock);
69 14 stopHeartbeat = true;
70 14 }
71 14 heartbeatCond.notify_all();
72 14 heartbeatThread.join();
73 }
74 30 }
75
76 // Bytes the line can carry before the next frame, less whatever is
77 // still queued in the tty. Writing more only builds a backlog that
78 // delays the lights behind the audio, and once the tty is full the
79 // non-blocking write drops bytes. Call once per frame; it also records
80 // the frame interval in frameTime.
81 329 int FrameBudget() {
82
1/2
✓ Branch 0 taken 329 times.
✗ Branch 1 not taken.
329 long long now = GetMonotonicTimeMicros();
83
2/2
✓ Branch 0 taken 315 times.
✓ Branch 1 taken 14 times.
329 long long ft = lastFrame ? now - lastFrame : LOR_DEFAULT_FRAME_TIME;
84 329 lastFrame = now;
85
1/2
✓ Branch 0 taken 329 times.
✗ Branch 1 not taken.
329 frameTime = std::clamp(ft, (long long)LOR_MIN_FRAME_TIME, (long long)LOR_MAX_FRAME_TIME);
86 329 long long budget = (long long)speed / 10 * frameTime / 1000000;
87 329 int queued = 0;
88
3/6
✓ Branch 0 taken 329 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 329 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 329 times.
329 if (ioctl(getFD(), TIOCOUTQ, &queued) == 0 && queued > 0) {
89 ✗ budget -= queued;
90 }
91
1/2
✓ Branch 0 taken 329 times.
✗ Branch 1 not taken.
329 return (int)std::clamp(budget, 0LL, (long long)LOR_MAX_BATCH_SIZE);
92 }
93
94 // Writes a frame's batch, serialised against the heartbeat so the
95 // heartbeat always lands between frames. Returns the bytes written,
96 // never negative; anything short of len did not go out.
97 276 int WriteBatch(const unsigned char* buf, int len) {
98
1/2
✓ Branch 0 taken 276 times.
✗ Branch 1 not taken.
276 std::unique_lock<std::mutex> lk(lock);
99
1/2
✓ Branch 0 taken 276 times.
✗ Branch 1 not taken.
276 int written = SerialWrite(getFD(), buf, len);
100 276 return written < 0 ? 0 : written;
101 276 }
102
103 int speed = 19200;
104 long long frameTime = LOR_DEFAULT_FRAME_TIME;
105
106 private:
107 14 void HeartbeatLoop() {
108 static const unsigned char heartbeat[LOR_HEARTBEAT_SIZE] = { 0x00, 0xFF, 0x81, 0x56, 0x00 };
109
1/2
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
14 std::unique_lock<std::mutex> lk(lock);
110
2/2
✓ Branch 0 taken 565 times.
✓ Branch 1 taken 14 times.
579 while (!stopHeartbeat) {
111 // monotonic: on the wall clock a step back stopped the heartbeat
112 // for the size of the step, and the controllers dropped out
113
1/2
✓ Branch 0 taken 565 times.
✗ Branch 1 not taken.
565 long long now = GetMonotonicTimeMicros();
114
2/2
✓ Branch 0 taken 99 times.
✓ Branch 1 taken 466 times.
565 if (now - lastHeartbeat >= LOR_HEARTBEAT_INTERVAL) {
115
1/2
✓ Branch 0 taken 99 times.
✗ Branch 1 not taken.
99 SerialWrite(getFD(), heartbeat, LOR_HEARTBEAT_SIZE);
116 99 lastHeartbeat = now;
117 }
118
1/2
✓ Branch 0 taken 565 times.
✗ Branch 1 not taken.
565 heartbeatCond.wait_for(lk, std::chrono::milliseconds(50));
119 }
120 14 }
121
122 long long lastFrame = 0;
123 long long lastHeartbeat = 0;
124 std::mutex lock;
125 std::condition_variable heartbeatCond;
126 std::thread heartbeatThread;
127 bool stopHeartbeat = false;
128 };
129