GCC Code Coverage Report


Directory: ./
File: channeloutput/PixelString.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 383 568 67.4%
Functions: 25 30 83.3%
Branches: 321 818 39.2%

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 "fpp-json.h"
16
17 // not in fpp-pch.h; libc++ pulls them in transitively but libstdc++ does not
18 #include <cmath>
19 #include <memory>
20 #include <tuple>
21
22 #include "../Sequence.h"
23 #include "../log.h"
24
25 #include "PixelString.h"
26 #include "ChannelOutputSetup.h"
27 #include "../OutputMonitor.h"
28 #include "../Warnings.h"
29 #include "overlays/PixelOverlay.h"
30
31 /////////////////////////////////////////////////////////////////////////////
32
33 #define MAX_PIXEL_STRING_LENGTH 1600
34
35 constexpr uint32_t SMART_RECEIVER_LEADIN = 6 * 3;
36 constexpr uint32_t SMART_RECEIVER_LEN = 6 * 3;
37 constexpr uint32_t SMART_RECEIVER_LEADOUT = 6 * 3;
38
39 constexpr uint32_t SMART_RECEIVER_V2_GAP = 10;
40
41 #define CHECKPS_SETTING(SETTING) \
42 if (SETTING) { \
43 LogErr(VB_CHANNELOUT, "Invalid PixelString Config %s\n", #SETTING); \
44 AddOutputConfigWarning(26, "Invalid PixelString Config: " + std::string(#SETTING)); \
45 return 0; \
46 }
47
48 // one config key per receiver; the hasChannels array in Init() is sized to
49 // match, so nothing may index either of them past this
50 constexpr int MAX_SMART_RECEIVERS = 6;
51
52 const char* SMART_RECEIVER_LABELS[MAX_SMART_RECEIVERS] = {
53 "virtualStrings",
54 "virtualStringsB",
55 "virtualStringsC",
56 "virtualStringsD",
57 "virtualStringsE",
58 "virtualStringsF",
59 };
60
61 578 VirtualString::VirtualString() :
62 578 receiverNum(-1),
63 578 startChannel(0),
64 578 pixelCount(0),
65 578 groupCount(0),
66 578 reverse(0),
67 578 colorOrder(FPPColorOrder::kColorOrderRGB),
68 578 startNulls(0),
69 578 endNulls(0),
70 578 zigZag(0),
71 578 brightness(100),
72 578 gamma(1.0),
73 578 leadInCount(0),
74 578 toggleCount(0),
75 578 leadOutCount(0) {
76 // never leave this dangling - ReadVirtualString normally replaces it, but
77 // a default constructed string is used directly for empty ports
78
1/2
✓ Branch 0 taken 578 times.
✗ Branch 1 not taken.
578 brightnessMap = BrightnessLUTCache::get(100, 1.0f, false);
79 578 }
80
81 ✗ VirtualString::VirtualString(int rt, int rn) :
82 ✗ receiverNum(rn),
83 ✗ startChannel(0),
84 ✗ pixelCount(0),
85 ✗ groupCount(0),
86 ✗ reverse(0),
87 ✗ colorOrder(FPPColorOrder::kColorOrderRGB),
88 ✗ startNulls(0),
89 ✗ endNulls(0),
90 ✗ zigZag(0),
91 ✗ brightness(100),
92 ✗ gamma(1.0),
93 ✗ leadInCount(0),
94 ✗ toggleCount(0),
95 ✗ leadOutCount(0) {
96 ✗ switch (rt) {
97 ✗ case 0:
98 ✗ leadInCount = 0;
99 ✗ toggleCount = SMART_RECEIVER_LEN;
100 ✗ leadOutCount = SMART_RECEIVER_LEADOUT;
101 ✗ break;
102 ✗ case 1:
103 ✗ leadInCount = SMART_RECEIVER_LEADIN;
104 ✗ toggleCount = SMART_RECEIVER_LEN * 2;
105 ✗ leadOutCount = SMART_RECEIVER_LEADOUT;
106 ✗ break;
107 ✗ case 2:
108 ✗ leadInCount = SMART_RECEIVER_LEADIN;
109 ✗ toggleCount = SMART_RECEIVER_LEN * 3;
110 ✗ leadOutCount = SMART_RECEIVER_LEADOUT;
111 ✗ break;
112 ✗ case 3:
113 ✗ leadInCount = SMART_RECEIVER_V2_GAP;
114 ✗ toggleCount = 0;
115 ✗ leadOutCount = 0;
116 ✗ break;
117 ✗ case 4: // Falcon v5
118 ✗ leadInCount = 0;
119 ✗ toggleCount = 0;
120 ✗ leadOutCount = 0;
121 ✗ break;
122 }
123 ✗ pixelCount = leadInCount + toggleCount + leadOutCount;
124 // identity: full brightness, no gamma
125 ✗ brightnessMap = BrightnessLUTCache::get(100, 1.0f, false);
126 ✗ }
127 namespace {
128 struct LUTKey {
129 int brightness;
130 int gammaScaled;
131 bool inverted;
132 2120 bool operator<(const LUTKey& o) const {
133
1/2
✓ Branch 0 taken 2120 times.
✗ Branch 1 not taken.
4010 return std::tie(brightness, gammaScaled, inverted) <
134 4240 std::tie(o.brightness, o.gammaScaled, o.inverted);
135 }
136 };
137 std::map<LUTKey, std::unique_ptr<std::array<uint8_t, 256>>> g_luts;
138 std::map<LUTKey, std::unique_ptr<std::array<uint16_t, 256>>> g_wideLuts;
139 std::mutex g_lutLock;
140
141 // gamma is quantised so it can key the cache; build from the quantised value
142 // so two strings sharing a key get byte-identical tables whichever is built
143 // first. 1/10000 is past any configurable gamma - 2.2f round trips exactly.
144 1149 int scaleGamma(float gamma) {
145 1149 return (int)std::lround(gamma * 10000.0f);
146 }
147 } // namespace
148
149 1149 const uint8_t* BrightnessLUTCache::get(int brightness, float gamma, bool inverted) {
150 // Quantise gamma so it can key the cache, then build from the quantised
151 // value rather than the caller's: two strings that share a key must get
152 // byte-identical tables whichever of them is built first. 1/10000 is fine
153 // past any gamma anyone configures - 2.2f round trips exactly.
154 1149 int gammaScaled = scaleGamma(gamma);
155 1149 LUTKey key{ brightness, gammaScaled, inverted };
156
157
1/2
✓ Branch 0 taken 1149 times.
✗ Branch 1 not taken.
1149 std::lock_guard<std::mutex> lock(g_lutLock);
158
1/2
✓ Branch 0 taken 1149 times.
✗ Branch 1 not taken.
1149 auto it = g_luts.find(key);
159
2/2
✓ Branch 0 taken 1056 times.
✓ Branch 1 taken 93 times.
1149 if (it != g_luts.end()) {
160 2112 return it->second->data();
161 }
162
163
1/2
✓ Branch 0 taken 93 times.
✗ Branch 1 not taken.
93 auto table = std::make_unique<std::array<uint8_t, 256>>();
164 // identical to the original per-string computation, so the tables are
165 // byte for byte what they always were (at 100/1.0 that is the identity
166 // map, which is what the smart receiver markers want)
167 93 float maxB = brightness * 2.55f;
168 93 float g = gammaScaled / 10000.0f;
169
2/2
✓ Branch 0 taken 23808 times.
✓ Branch 1 taken 93 times.
23901 for (int x = 0; x < 256; x++) {
170 23808 float f = maxB * pow((float)x / 255.0f, g);
171
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 23808 times.
23808 if (f > 255.0) {
172 ✗ f = 255.0;
173 }
174
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 23808 times.
23808 if (f < 0.0) {
175 ✗ f = 0.0;
176 }
177 23808 uint8_t v = (uint8_t)round(f);
178
2/2
✓ Branch 0 taken 512 times.
✓ Branch 1 taken 23296 times.
23808 (*table)[x] = inverted ? (uint8_t)~v : v;
179 }
180 93 const uint8_t* p = table->data();
181
1/2
✓ Branch 0 taken 93 times.
✗ Branch 1 not taken.
93 g_luts[key] = std::move(table);
182 93 return p;
183 1149 }
184
185 ✗ const uint16_t* BrightnessLUTCache::getWide(int brightness, float gamma, bool inverted) {
186 ✗ int gammaScaled = scaleGamma(gamma);
187 ✗ LUTKey key{ brightness, gammaScaled, inverted };
188
189 ✗ std::lock_guard<std::mutex> lock(g_lutLock);
190 ✗ auto it = g_wideLuts.find(key);
191 ✗ if (it != g_wideLuts.end()) {
192 ✗ return it->second->data();
193 }
194
195 ✗ auto table = std::make_unique<std::array<uint16_t, 256>>();
196 // the same curve as the 8 bit table, evaluated to full 16 bit range
197 ✗ float maxB = brightness * 655.35f;
198 ✗ float g = gammaScaled / 10000.0f;
199 ✗ for (int x = 0; x < 256; x++) {
200 ✗ float f = maxB * pow((float)x / 255.0f, g);
201 ✗ if (f > 65535.0) {
202 ✗ f = 65535.0;
203 }
204 ✗ if (f < 0.0) {
205 ✗ f = 0.0;
206 }
207 ✗ uint16_t v = (uint16_t)llround(f);
208 ✗ (*table)[x] = inverted ? (uint16_t)~v : v;
209 }
210 ✗ const uint16_t* p = table->data();
211 ✗ g_wideLuts[key] = std::move(table);
212 ✗ return p;
213 ✗ }
214
215 ✗ size_t BrightnessLUTCache::tableCount() {
216 ✗ std::lock_guard<std::mutex> lock(g_lutLock);
217 ✗ return g_luts.size() + g_wideLuts.size();
218 ✗ }
219
220 4 bool VirtualString::isSmartReceiver() const {
221 4 return receiverNum >= 0;
222 }
223 109 bool VirtualString::isFalconV5SmartReceiver() const {
224
1/8
✗ Branch 0 not taken.
✓ Branch 1 taken 109 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
109 return receiverNum >= 0 && leadInCount == 0 && toggleCount == 0 && leadOutCount == 0;
225 }
226
227 159195 int VirtualString::channelsPerNode() const {
228
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 159195 times.
159195 if (receiverNum >= 0) {
229 ✗ return 1;
230 }
231 159195 return colorOrder.channelCount();
232 }
233
234 /*
235 *
236 */
237 570 PixelString::PixelString(bool supportSmart) :
238 570 m_portNumber(0),
239 570 m_channelOffset(0),
240 570 m_outputBytes(0),
241 570 m_isSmartReceiver(supportSmart),
242 570 m_brightnessMaps(nullptr),
243
1/2
✓ Branch 0 taken 570 times.
✗ Branch 1 not taken.
570 m_outputBuffer(nullptr) {
244 570 }
245
246 /*
247 *
248 */
249 570 PixelString::~PixelString() {
250
2/2
✓ Branch 0 taken 506 times.
✓ Branch 1 taken 64 times.
570 if (!m_pinConfig.empty()) {
251 506 OutputMonitor::INSTANCE.RemovePortConfiguration(m_portNumber, m_pinConfig);
252 }
253
2/2
✓ Branch 0 taken 568 times.
✓ Branch 1 taken 2 times.
570 if (m_brightnessMaps) {
254 568 free(m_brightnessMaps);
255 }
256
2/2
✓ Branch 0 taken 568 times.
✓ Branch 1 taken 2 times.
570 if (m_outputBuffer) {
257 568 free(m_outputBuffer);
258 }
259 570 }
260
261 /*
262 *
263 */
264 570 int PixelString::Init(Json::Value config, Json::Value* pinConfig) {
265
2/4
✓ Branch 0 taken 570 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 570 times.
✗ Branch 3 not taken.
570 m_portNumber = config["portNumber"].asInt();
266 // Drivers index per-port arrays (smart receiver packets, GPIO commands,
267 // current monitoring) by this, unchecked.
268
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 569 times.
570 if (m_portNumber < 0) {
269
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogErr(VB_CHANNELOUT, "Pixel string has an invalid port number %d\n", m_portNumber);
270 1 return 0;
271 }
272 569 m_outputBytes = 0;
273 // needed before the virtual strings are read so their lookup tables are
274 // built at the right width
275
3/6
✓ Branch 0 taken 569 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 569 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 569 times.
✗ Branch 5 not taken.
569 m_bytesPerChannel = protocolBytesPerChannel(config["protocol"].asString());
276
277 569 int receiverCount = 1;
278
7/8
✓ Branch 0 taken 510 times.
✓ Branch 1 taken 59 times.
✓ Branch 2 taken 510 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 472 times.
✓ Branch 5 taken 38 times.
✓ Branch 6 taken 472 times.
✓ Branch 7 taken 97 times.
569 if (m_isSmartReceiver && config.isMember("differentialType")) {
279 472 smartReceiverType = ReceiverType::Standard;
280
2/4
✓ Branch 0 taken 472 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 472 times.
✗ Branch 3 not taken.
472 int rt = config["differentialType"].asInt();
281
2/2
✓ Branch 0 taken 34 times.
✓ Branch 1 taken 438 times.
472 if (rt) {
282
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 34 times.
34 if (rt <= 3) {
283 // v1 smart receivers
284 ✗ receiverCount = rt;
285 ✗ smartReceiverType = ReceiverType::v1;
286
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 26 times.
34 } else if (rt > 15) {
287 // Falcon V4 smart receivers (send-only config packet)
288 8 receiverCount = rt - 15;
289 8 smartReceiverType = ReceiverType::FalconV4;
290
2/2
✓ Branch 0 taken 22 times.
✓ Branch 1 taken 4 times.
26 } else if (rt > 9) {
291 // Falcon V5 smart receivers (bidirectional)
292 22 receiverCount = rt - 9;
293 22 smartReceiverType = ReceiverType::FalconV5;
294 } else {
295 // v2 smart receivers
296 4 receiverCount = rt - 3;
297 4 smartReceiverType = ReceiverType::v2;
298 }
299
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 34 times.
34 if (receiverCount > MAX_SMART_RECEIVERS) {
300 // only the Falcon v4 range is open ended (rt - 15), so a hand
301 // edited differentialType is the only way to get here
302 ✗ LogWarn(VB_CHANNELOUT, "Invalid smart receiver count %d on port %d; using %d\n",
303 receiverCount, m_portNumber + 1, MAX_SMART_RECEIVERS);
304 ✗ AddOutputConfigWarning(UNKNOWN_WARNING_ID, "PixelString: invalid smart receiver count on port " +
305 ✗ std::to_string(m_portNumber + 1) + ", using " +
306 ✗ std::to_string(MAX_SMART_RECEIVERS));
307 ✗ receiverCount = MAX_SMART_RECEIVERS;
308 }
309 }
310 472 m_isSmartReceiver = smartReceiverType != ReceiverType::Standard;
311
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 472 times.
472 if (m_bytesPerChannel != 1 &&
312 ✗ (smartReceiverType == ReceiverType::v1 || smartReceiverType == ReceiverType::v2)) {
313 // v1/v2 receivers interleave fixed width marker patterns with the
314 // pixel data, so a wide part would need two widths in one stream.
315 // Falcon v4/v5 markers are zero length, so those are fine.
316 ✗ LogWarn(VB_CHANNELOUT, "16 bit protocols are not supported with v1/v2 smart receivers on port %d; using 8 bit\n", m_portNumber + 1);
317 ✗ AddOutputConfigWarning(UNKNOWN_WARNING_ID, "PixelString: 16 bit protocol not supported with v1/v2 smart receivers on port " + std::to_string(m_portNumber + 1));
318 ✗ m_bytesPerChannel = 1;
319 }
320 } else {
321 97 smartReceiverType = ReceiverType::None;
322 97 m_isSmartReceiver = false;
323 }
324
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 569 times.
569 if (smartReceiverType == ReceiverType::v1) {
325 // v1 needs a lead in
326 ✗ AddVirtualString(VirtualString(0, 0));
327 }
328 569 int startMaxChan = m_outputBytes;
329
4/6
✓ Branch 0 taken 1138 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1138 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 570 times.
✓ Branch 5 taken 568 times.
1138 for (int i = 0; i < config["virtualStrings"].size(); i++) {
330
3/6
✓ Branch 0 taken 570 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 570 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 570 times.
✗ Branch 5 not taken.
570 Json::Value vsc = config["virtualStrings"][i];
331
1/2
✓ Branch 0 taken 570 times.
✗ Branch 1 not taken.
570 VirtualString vs;
332
3/4
✓ Branch 0 taken 570 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 569 times.
570 if (!ReadVirtualString(vsc, vs)) {
333 1 return 0;
334 }
335
1/2
✓ Branch 0 taken 569 times.
✗ Branch 1 not taken.
569 AddVirtualString(vs);
336
4/4
✓ Branch 0 taken 569 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 569 times.
✓ Branch 3 taken 1 times.
571 }
337 std::array<bool, MAX_SMART_RECEIVERS> hasChannels;
338 568 hasChannels[0] = startMaxChan != m_outputBytes;
339
5/8
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 560 times.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 8 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 568 times.
568 if (!hasChannels[0] && (smartReceiverType == ReceiverType::v1 || smartReceiverType == ReceiverType::v2)) {
340 // we need to output at least 1 pixel
341 ✗ AddNullPixelString();
342 }
343
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 568 times.
568 for (int p = 1; p < receiverCount; p++) {
344 ✗ int sz = m_virtualStrings.size();
345 ✗ int mc = m_outputBytes;
346
347 ✗ if (smartReceiverType == ReceiverType::v1) {
348 // v1 smart receiver
349 ✗ AddVirtualString(VirtualString(p, p));
350 ✗ } else if (smartReceiverType == ReceiverType::v2) {
351 // v2 smart receiver, .15ms gap
352 ✗ AddVirtualString(VirtualString(3, p));
353 ✗ } else if (smartReceiverType == ReceiverType::FalconV5 || smartReceiverType == ReceiverType::FalconV4) {
354 // Falcon v4/v5, just marker to determine config packets later
355 ✗ AddVirtualString(VirtualString(4, p));
356 }
357 ✗ startMaxChan = m_outputBytes;
358 ✗ for (int i = 0; i < config[SMART_RECEIVER_LABELS[p]].size(); i++) {
359 ✗ Json::Value vsc = config[SMART_RECEIVER_LABELS[p]][i];
360 ✗ VirtualString vs;
361 ✗ if (!ReadVirtualString(vsc, vs)) {
362 ✗ return 0;
363 }
364 ✗ AddVirtualString(vs);
365 ✗ }
366 ✗ hasChannels[p] = startMaxChan != m_outputBytes;
367 ✗ hasChannels[0] |= hasChannels[p];
368 ✗ if (!hasChannels[p]) {
369 ✗ if (p != (receiverCount - 1)) {
370 // we need to output at least 1 pixel
371 ✗ AddNullPixelString();
372 } else {
373 // in this case, we can revert and just send one less receivers of data
374 ✗ m_outputBytes = mc;
375 ✗ while (sz != m_virtualStrings.size()) {
376 ✗ m_virtualStrings.pop_back();
377 }
378 }
379 }
380 }
381
382
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 560 times.
568 if (!hasChannels[0]) {
383 // empty, no strings
384 8 m_outputBytes = 0;
385 8 m_virtualStrings.clear();
386
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
8 AddVirtualString(VirtualString());
387 }
388
2/2
✓ Branch 0 taken 508 times.
✓ Branch 1 taken 60 times.
568 if (pinConfig) {
389
1/2
✓ Branch 0 taken 508 times.
✗ Branch 1 not taken.
508 m_pinConfig = *pinConfig;
390
1/2
✓ Branch 0 taken 508 times.
✗ Branch 1 not taken.
508 OutputMonitor::INSTANCE.AddPortConfiguration(m_portNumber, m_pinConfig, m_outputBytes > 0);
391 }
392
393
1/2
✓ Branch 0 taken 568 times.
✗ Branch 1 not taken.
568 m_outputMap.resize(m_outputBytes);
394
395 // Initialize all maps to an unused location which should be zero.
396 // We need this so that null nodes in the middle of a string are sent
397 // all zeroes to keep them dark.
398
2/2
✓ Branch 0 taken 44336 times.
✓ Branch 1 taken 568 times.
44904 for (int i = 0; i < m_outputBytes; i++) {
399 44336 m_outputMap[i] = FPPD_OFF_CHANNEL;
400 }
401
402 568 int offset = 0;
403 568 int mapIndex = 0;
404
405 568 m_brightnessMaps = (const uint8_t**)calloc(1, sizeof(const uint8_t*) * m_outputBytes);
406
407
2/2
✓ Branch 0 taken 569 times.
✓ Branch 1 taken 568 times.
1137 for (int i = 0; i < m_virtualStrings.size(); i++) {
408 569 m_virtualStrings[i].chMap = &m_outputMap[offset];
409 569 int start = offset;
410
1/2
✓ Branch 0 taken 569 times.
✗ Branch 1 not taken.
569 offset += m_virtualStrings[i].startNulls * m_virtualStrings[i].channelsPerNode();
411
1/2
✓ Branch 0 taken 569 times.
✗ Branch 1 not taken.
569 SetupMap(offset, m_virtualStrings[i]);
412
1/2
✓ Branch 0 taken 569 times.
✗ Branch 1 not taken.
569 offset += m_virtualStrings[i].pixelCount * m_virtualStrings[i].channelsPerNode();
413
1/2
✓ Branch 0 taken 569 times.
✗ Branch 1 not taken.
569 offset += m_virtualStrings[i].endNulls * m_virtualStrings[i].channelsPerNode();
414 569 m_virtualStrings[i].chMapCount = offset - start;
415
5/8
✓ Branch 0 taken 44905 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 44905 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 44905 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 44336 times.
✓ Branch 7 taken 569 times.
44905 for (int j = 0; j < ((m_virtualStrings[i].startNulls * m_virtualStrings[i].channelsPerNode()) + (m_virtualStrings[i].pixelCount * m_virtualStrings[i].channelsPerNode()) + (m_virtualStrings[i].endNulls * m_virtualStrings[i].channelsPerNode())); j++)
416 44336 m_brightnessMaps[mapIndex++] = m_virtualStrings[i].brightnessMap;
417 }
418 // A 16 bit part clocks two bytes per colour channel. The channel maps
419 // above are per channel and stay as they are - only the wire length grows.
420 // Marker strings from a smart receiver are raw wire bytes and do not
421 // widen, but the only receivers allowed here (Falcon v4/v5) have zero
422 // length markers, so this is a straight doubling.
423
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 568 times.
568 if (m_bytesPerChannel != 1) {
424 ✗ m_outputBytes *= m_bytesPerChannel;
425 }
426
427 // Protocol preamble, if any. It has to land after the channel maps are
428 // built (those are sized for the pixel bytes only) and before the gpio off
429 // command below, which needs the full wire length.
430
11/16
✓ Branch 0 taken 560 times.
✓ Branch 1 taken 8 times.
✓ Branch 2 taken 560 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 560 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 560 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2 times.
✓ Branch 9 taken 558 times.
✓ Branch 10 taken 560 times.
✓ Branch 11 taken 8 times.
✓ Branch 12 taken 2 times.
✓ Branch 13 taken 566 times.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
568 if (m_outputBytes && protocolNeedsTM1814Preamble(config["protocol"].asString())) {
431 // per channel drive current; the part powers up at its 6.5mA floor, so
432 // without this a TM1814 string just looks dim
433
2/8
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
2 float mA = config.isMember("tm1814MilliAmps") ? config["tm1814MilliAmps"].asFloat() : 38.0f;
434
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 m_preamble = buildTM1814Preamble(mA);
435 2 m_outputBytes += m_preamble.size();
436 }
437
438 // turn gpio off after all channels on this port are done
439
2/2
✓ Branch 0 taken 560 times.
✓ Branch 1 taken 8 times.
568 if (m_outputBytes) {
440
1/2
✓ Branch 0 taken 560 times.
✗ Branch 1 not taken.
560 m_gpioCommands.push_back(GPIOCommand(m_portNumber, m_outputBytes));
441 }
442
443 // certain testing capabilities (like pixel counting) may require a
444 // buffer larger than the number of pixels configured
445 568 int obs = std::max(2400, m_outputBytes);
446 568 m_outputBuffer = (uint8_t*)calloc(obs, 1);
447
448 // Cape declared inversion corrects a wiring defect; protocol inversion is
449 // the pixel type asking for it. Both flip the same line, so they compose
450 // by xor and cancel when a TM18xx string lands on a backwards-wired port.
451
3/6
✓ Branch 0 taken 568 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 568 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 568 times.
✗ Branch 5 not taken.
568 m_protocolInverted = isInvertedProtocol(config["protocol"].asString());
452 // isMember first: pinConfig points into the cape config and a bare
453 // operator[] would insert a null member into it
454
4/12
✓ Branch 0 taken 508 times.
✓ Branch 1 taken 60 times.
✓ Branch 2 taken 508 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 508 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
568 bool wireInverted = pinConfig && pinConfig->isMember("inverted") && (*pinConfig)["inverted"].asBool();
455
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 566 times.
568 if (m_protocolInverted != wireInverted) {
456
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 invertOutput();
457 }
458
459 // after any inversion, so the bytes copied in are the wire form
460
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 566 times.
568 if (!m_preamble.empty()) {
461 2 memcpy(m_outputBuffer, m_preamble.data(), m_preamble.size());
462 }
463
464 568 return 1;
465 }
466
467 // TM1814 frames are "C1 C2 D1..Dn Reset". C1 carries four 6 bit constant
468 // current levels in the chip's physical W,R,G,B pin order with the top two
469 // bits of each byte fixed at 0; C2 is the bitwise complement of C1 and the
470 // chip will not decode the frame without it. Every chip in the chain reads
471 // and forwards both, so this is once per frame, not once per pixel.
472 2 std::vector<uint8_t> PixelString::buildTM1814Preamble(float milliAmps) {
473 // 64 levels spanning 6.5mA (all zero) to 38mA (all ones)
474 2 constexpr float MIN_MA = 6.5f;
475 2 constexpr float MAX_MA = 38.0f;
476
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 milliAmps = std::clamp(milliAmps, MIN_MA, MAX_MA);
477 2 uint8_t level = (uint8_t)std::lround((milliAmps - MIN_MA) * 63.0f / (MAX_MA - MIN_MA)) & 0x3F;
478
479
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 std::vector<uint8_t> p(8);
480 // C1: W, R, G, B - all four channels get the same level. This is the
481 // chip's pin order and is independent of the string's color order, which
482 // only decides how channel data lands in the W/R/G/B slots of Dn.
483
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 2 times.
10 for (int i = 0; i < 4; i++) {
484 8 p[i] = level;
485 8 p[i + 4] = ~level;
486 }
487 2 return p;
488 }
489
490 // Per protocol bit cell. Anything not listed is the 800kHz ws281x family,
491 // which is the overwhelming majority and what the timing has always been.
492 //
493 // The listed ones are genuinely different parts, not aliases: driving them
494 // with the ws281x cell does not merely shift an edge, it puts the pulse
495 // outside the window the part decodes. That is why they were kept out of the
496 // protocol dropdown until the firmware could take its timing at runtime.
497 304 PixelString::Timing PixelString::protocolTiming(const std::string& protocol) {
498 // the ws281x family - see the alias list in co-pixelStrings.php
499 static const Timing ws281x{ 320, 750, 1120 };
500
501 // 400kHz parts: a 2.5us cell, twice the ws281x period
502
6/8
✓ Branch 0 taken 286 times.
✓ Branch 1 taken 18 times.
✓ Branch 2 taken 286 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 286 times.
✓ Branch 6 taken 18 times.
✓ Branch 7 taken 286 times.
304 if (protocol == "ucs1903" || protocol == "ws2811slow" || protocol == "ws2811 slow") {
503 18 return { 500, 2000, 2500 };
504 }
505
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 286 times.
286 if (protocol == "tm1803") {
506 ✗ return { 750, 1875, 2625 };
507 }
508
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 286 times.
286 if (protocol == "gw6205") {
509 ✗ return { 750, 1625, 2375 };
510 }
511 // longer cell parts, between the two
512
3/6
✓ Branch 0 taken 286 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 286 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 286 times.
286 if (protocol == "tm1804" || protocol == "tm1809slow") {
513 ✗ return { 500, 1000, 1500 };
514 }
515
3/6
✓ Branch 0 taken 286 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 286 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 286 times.
286 if (protocol == "sk6822" || protocol == "pl9823") {
516 ✗ return { 375, 1375, 1750 };
517 }
518
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 286 times.
286 if (protocol == "ucs1912") {
519 ✗ return { 250, 1250, 1625 };
520 }
521
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 286 times.
286 if (protocol == "ge8822") {
522 ✗ return { 375, 1000, 1375 };
523 }
524 286 return ws281x;
525 }
526
527 569 int PixelString::protocolBytesPerChannel(const std::string& protocol) {
528 // single wire 16 bit parts; bit depths follow the pixel table in xLights
529 // (src-core/models/Pixels.cpp). The bare name is the 16 bit part itself,
530 // and the explicit "16 bit"/"(16)" spellings some controllers use mean the
531 // same thing here.
532
2/4
✓ Branch 0 taken 569 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 569 times.
✗ Branch 3 not taken.
1138 if (protocol == "ucs8903" || protocol == "ucs8904" || protocol == "ucs7604" ||
533
4/8
✓ Branch 0 taken 569 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 569 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 569 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 569 times.
1707 protocol == "ucs8903 16 bit" || protocol == "ucs8904 16 bit" ||
534
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 569 times.
569 protocol == "ucs7604 16 bit") {
535 ✗ return 2;
536 }
537 569 return 1;
538 }
539
540 560 bool PixelString::protocolNeedsTM1814Preamble(const std::string& protocol) {
541
3/4
✓ Branch 0 taken 558 times.
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 558 times.
560 return protocol == "tm1814" || protocol == "tm1814a";
542 }
543
544 426 void PixelString::setPixelDataChannels(int channels) {
545 // a port clamped to nothing sends nothing, preamble included - the same
546 // rule Init uses when deciding to build one at all
547
2/2
✓ Branch 0 taken 396 times.
✓ Branch 1 taken 30 times.
426 m_outputBytes = channels ? channels * m_bytesPerChannel + (int)m_preamble.size() : 0;
548 426 }
549
550 ✗ void PixelString::dropPreamble() {
551 ✗ if (m_preamble.empty()) {
552 ✗ return;
553 }
554 ✗ int n = (int)m_preamble.size();
555 ✗ m_outputBytes -= n;
556 // the preamble shifted the whole stream, so every command past it moves back
557 ✗ for (auto& c : m_gpioCommands) {
558 ✗ if (c.channelOffset >= n) {
559 ✗ c.channelOffset -= n;
560 }
561 }
562 ✗ m_preamble.clear();
563 ✗ memset(m_outputBuffer, 0, n);
564 }
565
566 568 bool PixelString::isInvertedProtocol(const std::string& protocol) {
567 // Only the protocols FPP can actually drive today. The TM18xx bit cell
568 // is close enough to ws2811 that the shared timing covers it; what makes
569 // them different is the idle level.
570
3/4
✓ Branch 0 taken 566 times.
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 566 times.
568 return protocol == "tm1814" || protocol == "tm1814a";
571 }
572
573 577 void PixelString::AddVirtualString(const VirtualString& vs) {
574 577 m_outputBytes += vs.startNulls * vs.channelsPerNode();
575 577 m_outputBytes += vs.pixelCount * vs.channelsPerNode();
576 577 m_outputBytes += vs.endNulls * vs.channelsPerNode();
577
578 577 m_virtualStrings.push_back(vs);
579 577 }
580
581 570 int PixelString::ReadVirtualString(Json::Value& vsc, VirtualString& vs) const {
582 570 vs.startChannel = vsc["startChannel"].asInt();
583 570 vs.pixelCount = vsc["pixelCount"].asInt();
584 570 vs.groupCount = vsc["groupCount"].asInt();
585 570 vs.reverse = vsc["reverse"].asInt();
586 570 vs.startNulls = vsc["nullNodes"].asInt();
587
1/2
✓ Branch 0 taken 570 times.
✗ Branch 1 not taken.
570 if (vsc.isMember("description")) {
588
2/4
✓ Branch 0 taken 570 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 570 times.
✗ Branch 3 not taken.
570 vs.description = vsc["description"].asString();
589 }
590
1/2
✓ Branch 0 taken 570 times.
✗ Branch 1 not taken.
570 if (vsc.isMember("endNulls")) {
591 570 vs.endNulls = vsc["endNulls"].asInt();
592 } else {
593 ✗ vs.endNulls = 0;
594 }
595 570 vs.zigZag = vsc["zigZag"].asInt();
596 570 vs.brightness = vsc["brightness"].asInt();
597
2/4
✓ Branch 0 taken 570 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 570 times.
✗ Branch 3 not taken.
570 vs.gamma = atof(vsc["gamma"].asString().c_str());
598
599
2/4
✓ Branch 0 taken 570 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 570 times.
570 if (vs.brightness > 100 || vs.brightness < 0) {
600 ✗ vs.brightness = 100;
601 }
602
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 568 times.
570 if (!std::isfinite(vs.gamma)) {
603 // "nan" or "inf" in a hand-edited config: not a gamma anyone chose,
604 // so use the default rather than clamping it to an end of the range
605 // (NaN fails both clamps below and landed on 0.01, a curve that
606 // drives every lit channel near full brightness)
607 2 vs.gamma = 1.0f;
608 }
609
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 570 times.
570 if (vs.gamma < 0.01f) {
610 ✗ vs.gamma = 0.01f;
611 }
612
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 570 times.
570 if (vs.gamma > 50.0f) {
613 ✗ vs.gamma = 50.0f;
614 }
615
616
1/8
✗ Branch 0 not taken.
✓ Branch 1 taken 570 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
570 CHECKPS_SETTING(vs.pixelCount < 0);
617
1/8
✗ Branch 0 not taken.
✓ Branch 1 taken 570 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
570 CHECKPS_SETTING(vs.pixelCount > MAX_PIXEL_STRING_LENGTH);
618
2/2
✓ Branch 0 taken 562 times.
✓ Branch 1 taken 8 times.
570 if (vs.pixelCount) {
619 // only validate settings and such if there are pixels on this string
620
1/10
✗ Branch 0 not taken.
✓ Branch 1 taken 562 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
563 CHECKPS_SETTING(vs.startChannel < 0);
621
1/10
✗ Branch 0 not taken.
✓ Branch 1 taken 562 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
562 CHECKPS_SETTING(vs.startChannel > FPPD_MAX_CHANNELS);
622
1/10
✗ Branch 0 not taken.
✓ Branch 1 taken 562 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
562 CHECKPS_SETTING(vs.startNulls < 0);
623
1/10
✗ Branch 0 not taken.
✓ Branch 1 taken 562 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
562 CHECKPS_SETTING(vs.startNulls > MAX_PIXEL_STRING_LENGTH);
624
1/10
✗ Branch 0 not taken.
✓ Branch 1 taken 562 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
562 CHECKPS_SETTING(vs.endNulls < 0);
625
1/10
✗ Branch 0 not taken.
✓ Branch 1 taken 562 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
562 CHECKPS_SETTING(vs.endNulls > MAX_PIXEL_STRING_LENGTH);
626
1/10
✗ Branch 0 not taken.
✓ Branch 1 taken 562 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
562 CHECKPS_SETTING((vs.startNulls + vs.pixelCount + vs.endNulls) > MAX_PIXEL_STRING_LENGTH);
627
1/10
✗ Branch 0 not taken.
✓ Branch 1 taken 562 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
562 CHECKPS_SETTING(vs.reverse < 0);
628
1/10
✗ Branch 0 not taken.
✓ Branch 1 taken 562 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
562 CHECKPS_SETTING(vs.reverse > 1);
629
1/10
✗ Branch 0 not taken.
✓ Branch 1 taken 562 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
562 CHECKPS_SETTING(vs.groupCount < 0);
630
1/10
✗ Branch 0 not taken.
✓ Branch 1 taken 562 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
562 CHECKPS_SETTING(vs.groupCount > vs.pixelCount);
631
1/10
✗ Branch 0 not taken.
✓ Branch 1 taken 562 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
562 CHECKPS_SETTING(vs.zigZag > vs.pixelCount);
632
1/10
✗ Branch 0 not taken.
✓ Branch 1 taken 562 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
562 CHECKPS_SETTING(vs.zigZag < 0);
633
634
2/4
✓ Branch 0 taken 562 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 562 times.
✗ Branch 3 not taken.
562 std::string colorOrder = vsc["colorOrder"].asString();
635
1/2
✓ Branch 0 taken 562 times.
✗ Branch 1 not taken.
562 vs.colorOrder = ColorOrderFromString(colorOrder);
636
637
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 562 times.
562 if (vs.groupCount == 1)
638 ✗ vs.groupCount = 0;
639
640 // The start is checked above, but not where the string ends:
641 // prepareOutput() reads up to the last node's channels, and the drivers
642 // drop channels at or past FPPD_MAX_CHANNELS from the range they
643 // report, so the loader's range check never saw the overrun.
644
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 562 times.
562 int64_t vsNodes = vs.groupCount ? (vs.pixelCount + vs.groupCount - 1) / vs.groupCount : vs.pixelCount;
645
1/2
✓ Branch 0 taken 562 times.
✗ Branch 1 not taken.
562 int64_t vsEnd = (int64_t)vs.startChannel + vsNodes * vs.channelsPerNode();
646
6/10
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 561 times.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1 times.
✗ Branch 9 not taken.
564 CHECKPS_SETTING(vsEnd > FPPD_MAX_CHANNELS);
647
648
2/4
✓ Branch 0 taken 561 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 561 times.
561 if ((vs.zigZag == vs.pixelCount) || (vs.zigZag == 1))
649 ✗ vs.zigZag = 0;
650
651
1/2
✓ Branch 0 taken 561 times.
✗ Branch 1 not taken.
561 vs.brightnessMap = BrightnessLUTCache::get(vs.brightness, vs.gamma, false);
652
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 561 times.
561 if (m_bytesPerChannel == 2) {
653 ✗ vs.brightnessMap16 = BrightnessLUTCache::getWide(vs.brightness, vs.gamma, false);
654 }
655
2/2
✓ Branch 0 taken 561 times.
✓ Branch 1 taken 1 times.
562 } else {
656
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
8 vs.colorOrder = ColorOrderFromString("RGB");
657 8 vs.groupCount = 1;
658 8 vs.zigZag = 0;
659 8 vs.reverse = 0;
660 8 vs.startNulls = 0;
661 8 vs.endNulls = 0;
662 8 vs.startChannel = 0;
663 8 vs.brightnessMap = BrightnessLUTCache::get(100, 1.0f, false);
664 }
665
666 569 return 1;
667 }
668 ✗ void PixelString::AddNullPixelString() {
669 ✗ VirtualString vs;
670 ✗ vs.pixelCount = 1;
671 ✗ vs.startChannel = FPPD_OFF_CHANNEL;
672 ✗ AddVirtualString(vs);
673 ✗ }
674
675 569 void PixelString::SetupMap(int vsOffset, const VirtualString& vs) {
676
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 569 times.
569 if (vs.receiverNum >= 0) {
677 // smart receiever, add codes to outputs
678 // drop low to trigger receiver to start trying to figure out which data is next
679 ✗ if (vs.leadInCount > 0) {
680 ✗ m_gpioCommands.push_back(GPIOCommand(m_portNumber, vsOffset));
681 ✗ for (int x = 0; x < vs.channelsPerNode() * vs.leadInCount; x++) {
682 ✗ m_outputMap[vsOffset++] = FPPD_OFF_CHANNEL;
683 }
684 // Turn back on
685 ✗ m_gpioCommands.push_back(GPIOCommand(m_portNumber, vsOffset, 1));
686 }
687 ✗ if (vs.toggleCount) {
688 // Toggle a bunch back and forth
689 ✗ for (int x = 0; x < vs.channelsPerNode() * vs.toggleCount; x++) {
690 // using white allows an even up/down on the GPIO
691 ✗ m_outputMap[vsOffset++] = FPPD_WHITE_CHANNEL;
692 }
693 }
694 ✗ if (vs.leadOutCount) {
695 // drop low again and turn off so receiver can enable/disable whichever outputs are needed
696 ✗ m_gpioCommands.push_back(GPIOCommand(m_portNumber, vsOffset));
697 ✗ for (int x = 0; x < vs.channelsPerNode() * vs.leadOutCount; x++) {
698 ✗ m_outputMap[vsOffset++] = FPPD_OFF_CHANNEL;
699 }
700 // turn gpio back on
701 ✗ m_gpioCommands.push_back(GPIOCommand(m_portNumber, vsOffset, 1));
702 }
703 ✗ return;
704 }
705
706 569 int offset = vsOffset;
707 569 int group = 0;
708 569 int maxGroups = 0;
709 569 int itemsInGroup = 0;
710 569 int ch = 0;
711 569 int pStart = 0;
712 569 int ch1 = 0;
713 569 int ch2 = 0;
714 569 int ch3 = 0;
715
716
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 569 times.
569 if (vs.groupCount) {
717 ✗ maxGroups = vs.pixelCount / vs.groupCount;
718 }
719
720 569 int redOffset = vs.colorOrder.redOffset();
721 569 int greenOffset = vs.colorOrder.greenOffset();
722 569 int blueOffset = vs.colorOrder.blueOffset();
723 569 int whiteOffset = vs.colorOrder.whiteOffset();
724
725
2/2
✓ Branch 0 taken 14771 times.
✓ Branch 1 taken 569 times.
15340 for (int p = pStart; p < vs.pixelCount; p++) {
726
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 14771 times.
14771 if (vs.groupCount) {
727 ✗ if (itemsInGroup >= vs.groupCount) {
728 ✗ group++;
729 ✗ itemsInGroup = 0;
730 }
731
732 ✗ ch = vs.startChannel + (group * vs.channelsPerNode());
733
734 ✗ itemsInGroup++;
735 } else {
736 14771 ch = vs.startChannel + (p * vs.channelsPerNode());
737 }
738
739
2/2
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 14761 times.
14771 if (vs.colorOrder == FPPColorOrder::kColorOrderONE) {
740 10 m_outputMap[offset++] = ch;
741 } else {
742 // incoming data is always RGB or RGBW, map to whatever order is
743 // needed. The offsets are positions within the output node with
744 // W counted, so for WRGB red is at 1, not 0.
745 14761 m_outputMap[offset + redOffset] = ch; // red is always data[0]
746 14761 m_outputMap[offset + greenOffset] = ch + 1; // green is always data[1]
747 14761 m_outputMap[offset + blueOffset] = ch + 2; // blue is always data[2]
748
2/2
✓ Branch 0 taken 19 times.
✓ Branch 1 taken 14742 times.
14761 if (whiteOffset >= 0) {
749 19 m_outputMap[offset + whiteOffset] = ch + 3; // W is always data[3]
750 19 offset += 4;
751 } else {
752 14742 offset += 3;
753 }
754 }
755 }
756
757 569 DumpMap("BEFORE ZIGZAG");
758
759
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 567 times.
569 if (vs.zigZag) {
760 2 int chanCount = vs.channelsPerNode();
761 2 int zigChannelCount = vs.zigZag * chanCount;
762 // the folds belong to this virtual string, so they are counted from
763 // its first pixel and stop at its last. Anchoring at the port instead
764 // re-flips segments owned by the other strings sharing the port, and
765 // any lead in or null nodes ahead of this one shift every boundary.
766 2 int vsEnd = vsOffset + (vs.pixelCount * chanCount);
767
768
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 2 times.
8 for (int i = vsOffset; i < vsEnd; i += zigChannelCount) {
769 6 int segment = (i - vsOffset) / zigChannelCount;
770
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 4 times.
6 if (segment % 2) {
771 2 int offset1 = i;
772 2 int offset2 = i + zigChannelCount - chanCount;
773
774 // the whole of the segment's last node has to be there - it is
775 // chanCount channels wide, which is 4 for RGBW, not 3
776
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if ((offset2 + chanCount) <= vsEnd)
777 2 FlipPixels(offset1, offset2, chanCount);
778 }
779 }
780
781 2 DumpMap("AFTER ZIGZAG");
782 }
783
784
3/4
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 567 times.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
569 if (vs.reverse && (vs.pixelCount > 1)) {
785 2 FlipPixels(vsOffset, vsOffset + (vs.pixelCount * vs.channelsPerNode()) - vs.channelsPerNode(), vs.channelsPerNode());
786
787 2 DumpMap("AFTER REVERSE");
788 }
789 }
790
791 /*
792 *
793 */
794 573 void PixelString::DumpMap(const char* msg) {
795
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 573 times.
573 if (WillLog(LOG_EXCESSIVE, VB_CHANNELOUT)) {
796 ✗ LogExcess(VB_CHANNELOUT, "OutputMap: %s\n", msg);
797 ✗ for (int i = 0; i < m_outputBytes; i++) {
798 ✗ LogExcess(VB_CHANNELOUT, "map[%d] = %d\n", i, m_outputMap[i]);
799 }
800 }
801 573 }
802
803 /*
804 *
805 */
806 4 void PixelString::FlipPixels(int offset1, int offset2, int chanCount) {
807 4 int flipPixels = (offset2 - offset1 + chanCount) / chanCount / 2;
808
809
2/2
✓ Branch 0 taken 11 times.
✓ Branch 1 taken 4 times.
15 for (int i = 0; i < flipPixels; i++) {
810 // a node is exactly chanCount channels wide - 1 for a single colour
811 // string, 3 for RGB, 4 for RGBW - so swapping a fixed three either
812 // steps outside the node or leaves part of it behind
813
2/2
✓ Branch 0 taken 23 times.
✓ Branch 1 taken 11 times.
34 for (int c = 0; c < chanCount; c++) {
814 23 int ch = m_outputMap[offset1 + c];
815 23 m_outputMap[offset1 + c] = m_outputMap[offset2 + c];
816 23 m_outputMap[offset2 + c] = ch;
817 }
818
819 11 offset1 += chanCount;
820 11 offset2 -= chanCount;
821 }
822 4 }
823
824 /*
825 *
826 */
827 603 void PixelString::DumpConfig(void) {
828 603 LogDebug(VB_CHANNELOUT, " port number : %d\n", m_portNumber);
829
830 603 int count = 0;
831
2/2
✓ Branch 0 taken 605 times.
✓ Branch 1 taken 603 times.
1208 for (auto& vs : m_virtualStrings) {
832
1/2
✓ Branch 0 taken 605 times.
✗ Branch 1 not taken.
605 if (vs.receiverNum == -1) {
833
1/2
✓ Branch 0 taken 605 times.
✗ Branch 1 not taken.
605 LogDebug(VB_CHANNELOUT, " --- Virtual String #%d ---\n", ++count);
834
1/2
✓ Branch 0 taken 605 times.
✗ Branch 1 not taken.
605 LogDebug(VB_CHANNELOUT, " description : %s\n", vs.description.c_str());
835
1/2
✓ Branch 0 taken 605 times.
✗ Branch 1 not taken.
605 LogDebug(VB_CHANNELOUT, " start channel : %d\n", vs.startChannel + m_channelOffset);
836
1/2
✓ Branch 0 taken 605 times.
✗ Branch 1 not taken.
605 LogDebug(VB_CHANNELOUT, " pixel count : %d\n", vs.pixelCount);
837
1/2
✓ Branch 0 taken 605 times.
✗ Branch 1 not taken.
605 LogDebug(VB_CHANNELOUT, " group count : %d\n", vs.groupCount);
838
1/2
✓ Branch 0 taken 605 times.
✗ Branch 1 not taken.
605 LogDebug(VB_CHANNELOUT, " reverse : %d\n", vs.reverse);
839
2/4
✓ Branch 0 taken 605 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 605 times.
✗ Branch 3 not taken.
605 LogDebug(VB_CHANNELOUT, " color order : %s\n", ColorOrderToString(vs.colorOrder).c_str());
840
1/2
✓ Branch 0 taken 605 times.
✗ Branch 1 not taken.
605 LogDebug(VB_CHANNELOUT, " start nulls : %d\n", vs.startNulls);
841
1/2
✓ Branch 0 taken 605 times.
✗ Branch 1 not taken.
605 LogDebug(VB_CHANNELOUT, " end nulls : %d\n", vs.endNulls);
842
1/2
✓ Branch 0 taken 605 times.
✗ Branch 1 not taken.
605 LogDebug(VB_CHANNELOUT, " zig zag : %d\n", vs.zigZag);
843
1/2
✓ Branch 0 taken 605 times.
✗ Branch 1 not taken.
605 LogDebug(VB_CHANNELOUT, " brightness : %d\n", vs.brightness);
844
1/2
✓ Branch 0 taken 605 times.
✗ Branch 1 not taken.
605 LogDebug(VB_CHANNELOUT, " gamma : %.3f\n", vs.gamma);
845 }
846 }
847 603 }
848
849 // Everything downstream of here holds wire form, not logical form: on an
850 // inverted port the line idles high and the data phase drives high for a
851 // logical zero, so every byte that goes out has to be complemented. The
852 // brightness maps carry that for pixel data; the preamble needs it too.
853 2 void PixelString::invertOutput() {
854 2 m_isInverted = !m_isInverted;
855
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
4 for (auto& vs : m_virtualStrings) {
856 // the tables are shared, so swap to the complemented one rather than
857 // flipping this string's in place and corrupting every other user
858
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
2 int b = vs.isSmartReceiver() ? 100 : vs.brightness;
859
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
2 float g = vs.isSmartReceiver() ? 1.0f : vs.gamma;
860
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 vs.brightnessMap = BrightnessLUTCache::get(b, g, m_isInverted);
861
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (vs.brightnessMap16) {
862 ✗ vs.brightnessMap16 = BrightnessLUTCache::getWide(b, g, m_isInverted);
863 }
864 }
865
2/2
✓ Branch 0 taken 16 times.
✓ Branch 1 taken 2 times.
18 for (auto& b : m_preamble) {
866 16 b = ~b;
867 }
868 2 }
869
870 84 void PixelString::AutoCreateOverlayModels(const std::vector<PixelString*>& strings, std::list<std::string>& autoModelNames) {
871
2/2
✓ Branch 0 taken 83 times.
✓ Branch 1 taken 1 times.
84 if (PixelOverlayManager::INSTANCE.isAutoCreatePixelOverlayModels()) {
872 83 std::map<std::string, std::vector<VirtualString*>> vstrings;
873
2/2
✓ Branch 0 taken 393 times.
✓ Branch 1 taken 83 times.
476 for (int s = 0; s < strings.size(); s++) {
874 393 char vsnum = 'A';
875 393 int vsidx = 0;
876
2/2
✓ Branch 0 taken 393 times.
✓ Branch 1 taken 393 times.
786 for (int vs = 0; vs < strings[s]->m_virtualStrings.size(); vs++) {
877
1/2
✓ Branch 0 taken 393 times.
✗ Branch 1 not taken.
393 std::string desc = strings[s]->m_virtualStrings[vs].description;
878
3/4
✓ Branch 0 taken 393 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✓ Branch 3 taken 387 times.
393 if (desc == "") {
879
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 desc = "String-" + std::to_string(s + 1);
880
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (strings[s]->m_virtualStrings.size() > 1) {
881 ✗ desc.append(1, vsnum);
882 ✗ desc += std::string("-");
883 ✗ desc += std::to_string(vsidx + 1);
884 ✗ if (strings[s]->m_virtualStrings[vs].receiverNum == -1) {
885 ✗ vsidx++;
886 ✗ } else if (vs != 0) {
887 ✗ vsidx = 0;
888 ✗ vsnum++;
889 }
890 }
891 }
892 // xLights will name the individual strings of a matrix/prop/model with a -str-# postfix
893 // so we will try and detect this and recreate the original model.
894 // Anything that is not a modest non-negative number (free-form
895 // user names, corrupt descriptions) is treated as a plain name
896 // below instead of throwing (stoi) or resizing gigabytes (huge
897 // index) and terminating fppd at model creation.
898 393 size_t found = desc.find("-str-");
899
2/2
✓ Branch 0 taken 391 times.
✓ Branch 1 taken 2 times.
393 if (found == std::string::npos) {
900
2/4
✓ Branch 0 taken 391 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 391 times.
✗ Branch 3 not taken.
391 vstrings[desc].push_back(&strings[s]->m_virtualStrings[vs]);
901 } else {
902 2 int idx = -1;
903 try {
904 2 size_t len = 0;
905
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 long long v = std::stoll(desc.substr(found + 5), &len);
906 // Entire tail must be the number (no "12x" partial
907 // parses) and it must fit a realistic string count.
908
4/8
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
✗ Branch 7 not taken.
2 if (len == desc.size() - (found + 5) && v >= 0 && v <= 100000) {
909 2 idx = (int)v;
910 }
911 ✗ } catch (...) {
912 ✗ }
913
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (idx < 0) {
914 ✗ vstrings[desc].push_back(&strings[s]->m_virtualStrings[vs]);
915 } else {
916
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (idx > 0)
917 2 --idx;
918
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 desc = desc.substr(0, found);
919
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 if (vstrings[desc].size() <= (size_t)idx) {
920
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 vstrings[desc].resize(idx + 1);
921 }
922
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 vstrings[desc][idx] = &strings[s]->m_virtualStrings[vs];
923 }
924 }
925 393 }
926 }
927
2/2
✓ Branch 0 taken 392 times.
✓ Branch 1 taken 83 times.
475 for (auto& m : vstrings) {
928
2/4
✓ Branch 0 taken 392 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 392 times.
392 if (PixelOverlayManager::INSTANCE.getModel(m.first) != nullptr) {
929 ✗ continue;
930 }
931
1/2
✓ Branch 0 taken 392 times.
✗ Branch 1 not taken.
392 std::string name = m.first;
932 392 auto& vs = m.second;
933
934 392 uint32_t startChannel = FPPD_MAX_CHANNELS;
935 392 uint32_t channelsPerNode = 0;
936
1/2
✓ Branch 0 taken 392 times.
✗ Branch 1 not taken.
784 std::string orientation = "H";
937
1/2
✓ Branch 0 taken 392 times.
✗ Branch 1 not taken.
392 std::string startLocation = "BL";
938 392 uint32_t strings = vs.size();
939 392 uint32_t strands = 1;
940 392 uint32_t maxChan = 0;
941 392 uint32_t minChannelsPerNode = UINT32_MAX;
942 392 int8_t rn = -1;
943
2/2
✓ Branch 0 taken 393 times.
✓ Branch 1 taken 392 times.
785 for (auto& a : vs) {
944
1/2
✓ Branch 0 taken 393 times.
✗ Branch 1 not taken.
393 if (a) {
945 393 startChannel = std::min(startChannel, (uint32_t)a->startChannel);
946
2/4
✓ Branch 0 taken 393 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 393 times.
393 maxChan = std::max(maxChan, (uint32_t)a->startChannel + (a->pixelCount * a->channelsPerNode() / (a->groupCount ? a->groupCount : 1)));
947
1/2
✓ Branch 0 taken 393 times.
✗ Branch 1 not taken.
393 channelsPerNode = std::max(channelsPerNode, (uint32_t)a->channelsPerNode());
948
1/2
✓ Branch 0 taken 393 times.
✗ Branch 1 not taken.
393 minChannelsPerNode = std::min(minChannelsPerNode, (uint32_t)a->channelsPerNode());
949 393 rn = std::max(rn, a->receiverNum);
950 } else {
951 ✗ --strings;
952 }
953 }
954
2/4
✓ Branch 0 taken 392 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 392 times.
392 if (strings && minChannelsPerNode != channelsPerNode) {
955 // One model cannot describe two node widths. Taking the wider of
956 // them mis-addresses every pixel on the narrower strings, and the
957 // channel order reported for the model is wrong for them too, so
958 // say so rather than creating a silently broken model.
959 ✗ LogWarn(VB_CHANNELOUT, "Overlay model %s spans strings of %u and %u channels per node; using %u - pixels on the narrower strings will not line up\n",
960 name.c_str(), minChannelsPerNode, channelsPerNode, channelsPerNode);
961 }
962 // The model's channel order is derived from channelsPerNode by
963 // PixelOverlayModel - the virtual string's own colour order describes
964 // the wire, which is applied later by prepareOutput(), and must not be
965 // copied onto the model or it would be applied twice.
966 392 int32_t channelCount = maxChan - startChannel;
967
968
6/12
✓ Branch 0 taken 392 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 392 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 392 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 392 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 392 times.
✗ Branch 10 not taken.
✓ Branch 11 taken 392 times.
392 if (name.find("Tree") != std::string::npos || name.find("TREE") != std::string::npos || name.find("tree") != std::string::npos || name.find("Vert") != std::string::npos || name.find("vert") != std::string::npos) {
969 // some common names that are usually vertical oriented
970 ✗ orientation = std::string("V");
971 }
972
973
3/4
✓ Branch 0 taken 386 times.
✓ Branch 1 taken 6 times.
✓ Branch 2 taken 386 times.
✗ Branch 3 not taken.
392 if ((channelCount > 0) && (rn == -1)) {
974
1/2
✓ Branch 0 taken 386 times.
✗ Branch 1 not taken.
386 autoModelNames.push_back(name);
975
2/4
✓ Branch 0 taken 386 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 386 times.
✗ Branch 3 not taken.
1158 PixelOverlayManager::INSTANCE.addAutoOverlayModel(name, startChannel, channelCount, channelsPerNode, orientation,
976 startLocation, strings, strands);
977 }
978 392 }
979 83 }
980 84 }
981
982 1651 uint8_t* PixelString::prepareOutput(uint8_t* channelData) {
983 // any protocol preamble is already sitting at the head of the buffer
984 1651 int idx = m_preamble.size();
985
2/2
✓ Branch 0 taken 1651 times.
✓ Branch 1 taken 1651 times.
3302 for (auto& vs : m_virtualStrings) {
986 1651 int* map = vs.chMap;
987
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1651 times.
1651 if (vs.brightnessMap16) {
988 // 16 bit parts take each channel most significant byte first
989 ✗ const uint16_t* brightness = vs.brightnessMap16;
990 ✗ for (int ch = 0; ch < vs.chMapCount; ch++) {
991 ✗ uint16_t v = brightness[channelData[map[ch]]];
992 ✗ m_outputBuffer[idx++] = (uint8_t)(v >> 8);
993 ✗ m_outputBuffer[idx++] = (uint8_t)v;
994 }
995 } else {
996 1651 const uint8_t* brightness = vs.brightnessMap;
997
2/2
✓ Branch 0 taken 116373 times.
✓ Branch 1 taken 1651 times.
118024 for (int ch = 0; ch < vs.chMapCount; ch++) {
998 116373 m_outputBuffer[idx++] = brightness[channelData[map[ch]]];
999 }
1000 }
1001 }
1002 1651 return m_outputBuffer;
1003 }
1004