GCC Code Coverage Report


Directory: ./
File: channeloutput/LOR.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 102 107 95.3%
Functions: 13 13 100.0%
Branches: 52 88 59.1%

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 GPL v2 as described in the
10 * included LICENSE.GPL file.
11 */
12
13 #include "fpp-pch.h"
14
15 #include "fpp-json.h"
16
17 #include <cstdint>
18 #include <fcntl.h>
19 #include <termios.h>
20 #include <unistd.h>
21
22 #include "../Warnings.h"
23 #include "../common.h"
24 #include "../log.h"
25
26 #include "LORSerial.h"
27
28 #include "LOR.h"
29
30 #define LOR_INTENSITY_SIZE 6
31 #define LOR_MAX_CHANNELS 3840
32 #define LOR_MAX_UNIT_ID 0xF0
33 #define LOR_MAX_BATCH_CMDS (LOR_MAX_BATCH_SIZE / LOR_INTENSITY_SIZE)
34
35 /////////////////////////////////////////////////////////////////////////////
36
37 class LOROutputData : public LORSerialOutput {
38 public:
39 10 LOROutputData() {
40
2/2
✓ Branch 0 taken 38400 times.
✓ Branch 1 taken 10 times.
38410 for (int i = 0; i < LOR_MAX_CHANNELS; i++) {
41 38400 lastValue[i] = LOR_UNKNOWN;
42 }
43 10 }
44
45 int controllerOffset = 0;
46 unsigned char intensityMap[256];
47
48 // What each unit was last sent, or LOR_UNKNOWN.
49 uint16_t lastValue[LOR_MAX_CHANNELS];
50 // Frame number a channel was last queued in, so the refresh pass does not
51 // send a channel twice in one frame.
52 uint32_t queuedFrame[LOR_MAX_CHANNELS] = {};
53 uint32_t frameNumber = 0;
54
55 // Where the next frame starts looking for changes, so a frame with more
56 // changes than fit the budget does not starve the higher channels.
57 int scanStart = 0;
58 // Next channel the background refresh resends.
59 int refreshAt = 0;
60
61 unsigned char batchData[LOR_MAX_BATCH_SIZE];
62 int batchChannel[LOR_MAX_BATCH_CMDS];
63 int batchCount = 0;
64 };
65
66 /////////////////////////////////////////////////////////////////////////////
67
68 #include "Plugin.h"
69 class LORPlugin : public FPPPlugins::Plugin, public FPPPlugins::ChannelOutputPlugin {
70 public:
71 9 LORPlugin() :
72
2/4
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✗ Branch 3 not taken.
27 FPPPlugins::Plugin("LOR") {
73 9 }
74 10 virtual ChannelOutput* createChannelOutput(unsigned int startChannel, unsigned int channelCount) override {
75
2/6
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 10 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
20 return new LOROutput(startChannel, channelCount);
76 }
77 };
78
79 extern "C" {
80 9 FPPPlugins::Plugin* createPlugin() {
81
2/6
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 9 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
18 return new LORPlugin();
82 }
83 }
84
85 10 LOROutput::LOROutput(unsigned int startChannel, unsigned int channelCount) :
86 ChannelOutput(startChannel, channelCount),
87 10 data(nullptr) {}
88 40 LOROutput::~LOROutput() {
89
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
20 if (data) {
90
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
20 delete data;
91 }
92 40 }
93
94 16 void LOROutput::GetRequiredChannelRanges(const std::function<void(int, int)>& addRange) {
95 16 addRange(m_startChannel, m_startChannel + m_channelCount - 1);
96 16 }
97
98 // LOR intensity is a whole percent: 0xF0 is off, 0x01 is full, and 1-99%
99 // are 228 - 2 * percent. The Pixie manual's intensity table is per percent
100 // (101 levels), and this is the table LOR's own software was captured using.
101 // A linear map over 0xF0..0x01 lands low values at or past the off end (so
102 // the bottom of a fade goes dark) and reads 50% as about 54%.
103 8 static void LOR_SetupIntensityMap(LOROutputData* privData) {
104
2/2
✓ Branch 0 taken 2048 times.
✓ Branch 1 taken 8 times.
2056 for (int i = 0; i < 256; i++) {
105 2048 int percent = (i * 100 + 127) / 255;
106
2/2
✓ Branch 0 taken 16 times.
✓ Branch 1 taken 2032 times.
2048 if (percent == 0) {
107 16 privData->intensityMap[i] = 0xF0;
108
2/2
✓ Branch 0 taken 16 times.
✓ Branch 1 taken 2016 times.
2032 } else if (percent == 100) {
109 16 privData->intensityMap[i] = 0x01;
110 } else {
111 2016 privData->intensityMap[i] = 228 - percent * 2;
112 }
113 }
114 8 }
115
116 8 int LOROutput::Close(void) {
117 8 LogDebug(VB_CHANNELOUT, "LOROutput::Close()\n");
118 8 data->StopHeartbeat();
119 8 data->closeSerialPort();
120 8 return ChannelOutput::Close();
121 }
122
123 8 void LOROutput::DumpConfig(void) {
124 8 ChannelOutput::DumpConfig();
125
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 if (data) {
126 8 data->dumpSerialConfig();
127 8 LogDebug(VB_CHANNELOUT, " port speed : %d\n", data->speed);
128 8 LogDebug(VB_CHANNELOUT, " controllerOffset: %d\n", data->controllerOffset);
129 }
130 8 }
131 10 int LOROutput::Init(Json::Value config) {
132 10 LogDebug(VB_CHANNELOUT, "LOROutput::Init()\n");
133
2/6
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 10 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
10 data = new LOROutputData();
134
135
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 if (config.isMember("speed")) {
136 10 data->speed = config["speed"].asInt();
137 }
138
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 if (config.isMember("firstControllerId")) {
139 10 data->controllerOffset = config["firstControllerId"].asInt();
140 // Fix for box off by one error reported in issue 820
141
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 if (data->controllerOffset > 0) {
142 10 data->controllerOffset -= 1;
143 }
144 }
145 // SendData() refuses (and logged) every frame of an output larger than
146 // LOR can address: say so once, here, and don't start it.
147
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 9 times.
10 if (m_channelCount > LOR_MAX_CHANNELS) {
148 1 LogErr(VB_CHANNELOUT, "LOR carries at most %d channels, %u configured\n",
149 LOR_MAX_CHANNELS, m_channelCount);
150 1 return 0;
151 }
152
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 8 times.
9 if (!data->setupSerialPort(config, data->speed, "8N1")) {
153 1 return 0;
154 }
155 8 int lastUnit = data->controllerOffset + ((std::min(m_channelCount, (unsigned int)LOR_MAX_CHANNELS) + 15) >> 4);
156
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 7 times.
8 if (lastUnit > LOR_MAX_UNIT_ID) {
157 1 LogWarn(VB_CHANNELOUT, "LOR: %d channels starting at unit %d run past unit ID %d; channels beyond it are not sent\n",
158 m_channelCount, data->controllerOffset + 1, LOR_MAX_UNIT_ID);
159 }
160
161 8 LOR_SetupIntensityMap(data);
162 8 data->StartHeartbeat();
163
164
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
8 return ChannelOutput::Init(config);
165 }
166
167 1828 static void LOR_QueueChannel(LOROutputData* privData, int channel, unsigned char value) {
168 1828 unsigned char* cmd = &privData->batchData[privData->batchCount * LOR_INTENSITY_SIZE];
169 1828 cmd[0] = 0x00;
170 1828 cmd[1] = privData->controllerOffset + (channel >> 4) + 1;
171 1828 cmd[2] = 0x03;
172 1828 cmd[3] = privData->intensityMap[value];
173 1828 cmd[4] = 0x80 | (channel & 0x0F);
174 1828 cmd[5] = 0x00;
175 1828 privData->batchChannel[privData->batchCount++] = channel;
176 1828 privData->lastValue[channel] = value;
177 1828 privData->queuedFrame[channel] = privData->frameNumber;
178 1828 }
179
180 179 int LOROutput::SendData(unsigned char* channelData) {
181
1/2
✓ Branch 0 taken 179 times.
✗ Branch 1 not taken.
179 LogDebug(VB_CHANNELDATA, "LOROutput::SendData()\n");
182
183
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 179 times.
179 if (m_channelCount > LOR_MAX_CHANNELS) {
184 ✗ LogErr(VB_CHANNELOUT,
185 "LOR_SendData() tried to send %d bytes when max is %d at %d baud\n",
186 m_channelCount, LOR_MAX_CHANNELS, data->speed);
187 ✗ return 0;
188 }
189
190 // Channels past unit 0xF0 cannot be addressed (higher IDs are reserved,
191 // 0xFF is broadcast), so they are left out rather than wrapped.
192 179 int count = std::min((int)m_channelCount, (LOR_MAX_UNIT_ID - data->controllerOffset) * 16);
193
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 179 times.
179 if (count <= 0) {
194 ✗ return m_channelCount;
195 }
196
197 // Changes that do not fit the frame's budget stay pending (lastValue is
198 // untouched) and go out on a later frame.
199
1/2
✓ Branch 0 taken 179 times.
✗ Branch 1 not taken.
179 int maxCmds = data->FrameBudget() / LOR_INTENSITY_SIZE;
200
201 179 data->frameNumber++;
202 179 data->batchCount = 0;
203
204 179 int i = data->scanStart;
205
4/4
✓ Branch 0 taken 3806 times.
✓ Branch 1 taken 71 times.
✓ Branch 2 taken 3698 times.
✓ Branch 3 taken 108 times.
3877 for (int checked = 0; checked < count && data->batchCount < maxCmds; checked++) {
206
2/2
✓ Branch 0 taken 1757 times.
✓ Branch 1 taken 1941 times.
3698 if (data->lastValue[i] != channelData[i]) {
207 1757 LOR_QueueChannel(data, i, channelData[i]);
208 }
209
2/2
✓ Branch 0 taken 75 times.
✓ Branch 1 taken 3623 times.
3698 if (++i == count) {
210 75 i = 0;
211 }
212 }
213 179 data->scanStart = i;
214
215 // With room left over, resend a few channels that have not changed. A
216 // unit that missed a command (line noise, waking up, power cycled) would
217 // otherwise hold the wrong value until the channel next changed, which
218 // for a static channel is never. Capped at a quarter of the frame so
219 // refresh never crowds out real changes, and to one pass over the
220 // channels per LOR_REFRESH_PERIOD so a small setup is not resent
221 // constantly.
222 179 int refreshCmds = std::min(maxCmds - data->batchCount, std::max(1, maxCmds / 4));
223 179 refreshCmds = std::min(refreshCmds, (int)((count * data->frameTime + LOR_REFRESH_PERIOD - 1) / LOR_REFRESH_PERIOD));
224
3/4
✓ Branch 0 taken 250 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 71 times.
✓ Branch 3 taken 179 times.
250 for (int checked = 0; checked < count && refreshCmds > 0; checked++) {
225 71 int ch = data->refreshAt;
226
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 69 times.
71 if (++data->refreshAt >= count) {
227 2 data->refreshAt = 0;
228 }
229
2/4
✓ Branch 0 taken 71 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 71 times.
✗ Branch 3 not taken.
71 if (data->queuedFrame[ch] != data->frameNumber && data->lastValue[ch] != LOR_UNKNOWN) {
230 71 LOR_QueueChannel(data, ch, channelData[ch]);
231 71 refreshCmds--;
232 }
233 }
234
235
1/2
✓ Branch 0 taken 179 times.
✗ Branch 1 not taken.
179 if (data->batchCount) {
236 179 int len = data->batchCount * LOR_INTENSITY_SIZE;
237
1/2
✓ Branch 0 taken 179 times.
✗ Branch 1 not taken.
179 int written = data->WriteBatch(data->batchData, len);
238
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 179 times.
179 if (written < len) {
239 // Every command starts with 0x00, which flushes the unit's
240 // buffer, so a cut-off command is discarded. Its channel and
241 // every one after it has to be resent.
242 ✗ for (int c = written / LOR_INTENSITY_SIZE; c < data->batchCount; c++) {
243 ✗ data->lastValue[data->batchChannel[c]] = LOR_UNKNOWN;
244 }
245 }
246 }
247 179 return m_channelCount;
248 }
249