GCC Code Coverage Report


Directory: ./
File: channeloutput/ChannelOutputSetup.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 384 404 95.0%
Functions: 29 31 93.5%
Branches: 436 762 57.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 <shared_mutex>
16
17 #include "fpp-json.h"
18
19 #include <dlfcn.h>
20 #include <errno.h>
21 #include <pthread.h>
22 #include <stdio.h>
23 #include <stdlib.h>
24 #include <string.h>
25
26 #include <algorithm>
27 #include <sstream>
28 #include <string>
29
30 #include "ChannelOutput.h"
31 #include "ChannelOutputSetup.h"
32 #include "Sequence.h"
33 #include "channeloutputthread.h"
34 #include "ThreadedChannelOutput.h"
35 #include "Warnings.h"
36 #include "common.h"
37 #include "log.h"
38 #include "settings.h"
39 #include "../AtomicSharedPtr.h"
40 #include "../FileMonitor.h"
41 #include "../OutputMonitor.h"
42 #include "../Plugin.h"
43 #include "../Plugins.h"
44 #include "../config.h"
45 #include "../MultiSync.h"
46 #include "../e131bridge.h"
47
48 #include "processors/OutputProcessor.h"
49
50 /////////////////////////////////////////////////////////////////////////////
51
52 class FPPChannelOutputInstance {
53 public:
54 1279 FPPChannelOutputInstance() {}
55 1279 ~FPPChannelOutputInstance() {}
56
57 unsigned int startChannel = 0;
58 unsigned int channelCount = 0;
59 ChannelOutput* output = nullptr;
60 std::string sourceFile;
61
62 std::atomic<FPPChannelOutputInstance*> prev;
63 std::atomic<FPPChannelOutputInstance*> next;
64 };
65
66 std::atomic<unsigned long> channelOutputFrame{ 0 };
67 std::atomic<float> mediaElapsedSeconds{ 0.0f };
68
69 std::atomic<FPPChannelOutputInstance*> channelOutputs;
70 std::atomic<FPPChannelOutputInstance*> lastChannelOutput;
71 1202 inline void addChannelOutput(FPPChannelOutputInstance* inst) {
72 // the output thread walks this list under outputThreadLock; linking a node
73 // in takes several stores, so publish them all before it can see any
74
1/2
✓ Branch 0 taken 1202 times.
✗ Branch 1 not taken.
1202 std::unique_lock<std::mutex> lock = LockOutputThread();
75 // An output added while the output thread is already up -- a config reload,
76 // which is what an xLights auto-upload does -- missed the StartingOutput()
77 // that thread made when it came up, and would otherwise run in the stopped
78 // state for as long as it lives: BBShiftPanel leaves its PRUs parked, so
79 // the panels stay dark, and PCA9685 never leaves sleep. The thread sets
80 // ThreadIsRunning and makes its own StartingOutput() call under this same
81 // lock, so checking here cannot land in between the two and either miss
82 // the call or make it twice.
83
6/8
✓ Branch 0 taken 1202 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1202 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 148 times.
✓ Branch 5 taken 1054 times.
✓ Branch 6 taken 148 times.
✓ Branch 7 taken 1054 times.
1202 if (inst->output && ChannelOutputThreadIsRunning()) {
84
2/4
✓ Branch 0 taken 148 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 148 times.
✗ Branch 3 not taken.
148 LogDebug(VB_CHANNELOUT, "Output thread already running, starting output on new %s\n",
85 inst->output->GetOutputType().c_str());
86
1/2
✓ Branch 0 taken 148 times.
✗ Branch 1 not taken.
148 inst->output->StartingOutput();
87 }
88 1202 inst->prev = lastChannelOutput.load();
89
2/2
✓ Branch 0 taken 167 times.
✓ Branch 1 taken 1035 times.
1202 if (lastChannelOutput) {
90 167 lastChannelOutput.load()->next = inst;
91 } else {
92 1035 channelOutputs = inst;
93 }
94 1202 lastChannelOutput = inst;
95 1202 }
96
97 OutputProcessors outputProcessors;
98
99 ✗ bool HasChannelOutputs() {
100 ✗ return channelOutputs.load() != nullptr;
101 }
102
103 // This and GetOutputTypes() below walk the list from HTTP/MultiSync threads.
104 // They can't take outputThreadLock: the output thread holds it across
105 // Sequence::SendSequenceData(), which runs FSEQ-triggered commands, and a
106 // command that reached back into either of these would deadlock the output
107 // thread. They read under outputListFreeLock instead, which the reload path
108 // takes last, only while it unlinks instances, and which these walks hold
109 // alone -- a leaf, so nothing can wait on it in a cycle. Without it they
110 // could read an instance the reload was freeing. The reload frees what it
111 // unlinked only after letting go: an output's Close() takes other locks
112 // (UDPOutput's removes its NetworkMonitor callback, whose dispatch takes
113 // MultiSync's lock, which is held around these walks), and the walk that
114 // could still see an unlinked instance is one the exclusive lock waited out.
115 static std::shared_mutex outputListFreeLock;
116
117 173632 bool HasUniverseOutputs() {
118
3/6
✓ Branch 0 taken 173632 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 173632 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 173632 times.
✗ Branch 5 not taken.
173632 std::string universeFile = FPP_DIR_CONFIG("/co-universes.json");
119
1/2
✓ Branch 0 taken 173632 times.
✗ Branch 1 not taken.
173632 std::shared_lock<std::shared_mutex> l(outputListFreeLock);
120
2/2
✓ Branch 0 taken 76191 times.
✓ Branch 1 taken 98441 times.
174632 for (auto inst = channelOutputs.load(); inst != nullptr; inst = inst->next) {
121
2/2
✓ Branch 0 taken 75191 times.
✓ Branch 1 taken 1000 times.
76191 if (inst->sourceFile == universeFile) {
122 75191 return true;
123 }
124 }
125 98441 return false;
126 173632 }
127
128 static std::map<std::string, std::set<std::string>> outputLoadWarnings;
129 // Warnings the outputs themselves raise while a config file's outputs are
130 // being initialised (AddOutputConfigWarning), retracted with that file's other
131 // load warnings when it is next loaded. Main loop only, like the loader.
132 static std::map<std::string, std::set<std::pair<int, std::string>>> outputConfigWarnings;
133 static std::string outputConfigWarningFile;
134
135 78 void AddOutputConfigWarning(int id, const std::string& warning) {
136
1/2
✓ Branch 0 taken 78 times.
✗ Branch 1 not taken.
78 WarningHolder::AddWarning(id, warning);
137
2/2
✓ Branch 0 taken 76 times.
✓ Branch 1 taken 2 times.
78 if (!outputConfigWarningFile.empty()) {
138
3/6
✓ Branch 0 taken 76 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 76 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
76 outputConfigWarnings[outputConfigWarningFile].insert({ id, warning });
139 }
140 78 }
141
142 using OutputRangeVec = std::vector<std::pair<uint32_t, uint32_t>>;
143 // Snapshots published by ComputeOutputRanges() and handed out by
144 // GetOutputRangesSnapshot(). Both are seeded here with the {0,8} default so a
145 // snapshot is never null and never empty -- readers never mutate these, so no
146 // lock is needed to see a consistent list while ComputeOutputRanges()
147 // publishes a new one from another thread.
148 static AtomicSharedPtr<const OutputRangeVec> outputRanges(std::make_shared<const OutputRangeVec>(OutputRangeVec{ { 0, 8 } }));
149 static AtomicSharedPtr<const OutputRangeVec> preciseOutputRanges(std::make_shared<const OutputRangeVec>(OutputRangeVec{ { 0, 8 } }));
150
151 // Returns a snapshot of the current ranges; the caller should hold the
152 // returned shared_ptr for the duration of any iteration over it rather than
153 // calling this repeatedly. Never null, never empty.
154 118458 std::shared_ptr<const OutputRangeVec> GetOutputRangesSnapshot(bool precise) {
155
2/2
✓ Branch 0 taken 118374 times.
✓ Branch 1 taken 84 times.
118458 return precise ? preciseOutputRanges.load() : outputRanges.load();
156 }
157
158 // Plugin-compatibility API (fpp-brightness and fpp-Capture call this; the
159 // return type is not part of the C++ name mangling, so changing it would let
160 // an already-compiled plugin misread the return value rather than fail to
161 // link). Each calling thread gets its own copy of the snapshot, so the
162 // reference stays valid and immutable for that thread until IT calls again --
163 // a recompute on another thread can no longer mutate a list a plugin is
164 // iterating. In-tree code should use GetOutputRangesSnapshot() instead.
165 ✗ const OutputRangeVec& GetOutputRanges(bool precise) {
166 // One copy per flavor, so holding the precise and non-precise references
167 // at the same time works, as it did when these were two distinct vectors.
168 ✗ static thread_local OutputRangeVec copies[2];
169 ✗ OutputRangeVec& copy = copies[precise ? 1 : 0];
170 ✗ copy = *GetOutputRangesSnapshot(precise);
171 ✗ return copy;
172 }
173
174 84 std::string GetOutputRangesAsString(bool precise, bool oneBased) {
175 84 std::string ret;
176 84 bool first = true;
177
1/2
✓ Branch 0 taken 84 times.
✗ Branch 1 not taken.
84 int offset = oneBased ? 1 : 0;
178
1/2
✓ Branch 0 taken 84 times.
✗ Branch 1 not taken.
84 auto ranges = GetOutputRangesSnapshot(precise);
179
2/2
✓ Branch 0 taken 84 times.
✓ Branch 1 taken 84 times.
168 for (auto& a : *ranges) {
180
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 84 times.
84 if (!first) {
181 ✗ ret += ",";
182 } else {
183 84 first = false;
184 }
185
3/6
✓ Branch 0 taken 84 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 84 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 84 times.
✗ Branch 5 not taken.
84 ret += std::to_string(a.first + offset) + "-" + std::to_string(a.first + a.second - 1 + offset);
186 }
187
2/4
✓ Branch 0 taken 84 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 84 times.
84 if (ret == "") {
188 ✗ ret = "1-8";
189 }
190 84 return ret;
191 84 }
192
193 // we'll sort the ranges that the outputs have registered and combine any overlaps
194 // or close ranges to keep the range list smaller
195 4294 static void sortRanges(std::vector<std::pair<uint32_t, uint32_t>>& rngs, bool gaps) {
196 4294 std::map<uint32_t, uint32_t> ranges;
197 // sort
198
2/2
✓ Branch 0 taken 3223 times.
✓ Branch 1 taken 4294 times.
7517 for (auto& a : rngs) {
199
1/2
✓ Branch 0 taken 3223 times.
✗ Branch 1 not taken.
3223 uint32_t cur = ranges[a.first];
200
2/2
✓ Branch 0 taken 106 times.
✓ Branch 1 taken 3117 times.
3223 if (cur) {
201
1/2
✓ Branch 0 taken 106 times.
✗ Branch 1 not taken.
106 ranges[a.first] = std::max(a.second, cur);
202 } else {
203
1/2
✓ Branch 0 taken 3117 times.
✗ Branch 1 not taken.
3117 ranges[a.first] = a.second;
204 }
205 }
206 4294 rngs.clear();
207 4294 std::pair<uint32_t, uint32_t> cur(FPPD_MAX_CHANNELS, FPPD_MAX_CHANNELS);
208
2/2
✓ Branch 0 taken 2147 times.
✓ Branch 1 taken 2147 times.
4294 uint32_t gapEliminate = gaps ? 1025 : 0;
209
210
2/2
✓ Branch 0 taken 3117 times.
✓ Branch 1 taken 4294 times.
7411 for (auto& a : ranges) {
211
2/2
✓ Branch 0 taken 2288 times.
✓ Branch 1 taken 829 times.
3117 if (cur.first == FPPD_MAX_CHANNELS) {
212 2288 cur.first = a.first;
213 2288 cur.second = a.second;
214 } else {
215
2/2
✓ Branch 0 taken 791 times.
✓ Branch 1 taken 38 times.
829 if (a.first <= (cur.first + cur.second + gapEliminate)) {
216 // overlap or within 1025 channels of an overlap, need to combine
217 // if the two ranges are "close" (wthin 1025 channels) we'll combine
218 // as the overhead of doing ranges wouldn't benefit with a small gap
219 791 uint32_t max = cur.first + cur.second - 1;
220 791 uint32_t amax = a.first + a.second - 1;
221 791 max = std::max(max, amax);
222 791 cur.second = max - cur.first + 1;
223 } else {
224
1/2
✓ Branch 0 taken 38 times.
✗ Branch 1 not taken.
38 rngs.push_back(cur);
225 38 cur.first = a.first;
226 38 cur.second = a.second;
227 }
228 }
229 }
230
2/2
✓ Branch 0 taken 2288 times.
✓ Branch 1 taken 2006 times.
4294 if (cur.first != FPPD_MAX_CHANNELS) {
231
1/2
✓ Branch 0 taken 2288 times.
✗ Branch 1 not taken.
2288 rngs.push_back(cur);
232 }
233
2/2
✓ Branch 0 taken 2006 times.
✓ Branch 1 taken 2288 times.
4294 if (rngs.empty()) {
234 2006 cur.first = 0;
235 2006 cur.second = 8;
236
1/2
✓ Branch 0 taken 2006 times.
✗ Branch 1 not taken.
2006 rngs.push_back(cur);
237 }
238 4294 }
239 1886 static void addRange(uint32_t min, uint32_t max, std::vector<std::pair<uint32_t, uint32_t>>& prec, std::vector<std::pair<uint32_t, uint32_t>>& normal) {
240 // Range lengths below are max - min + 1 in unsigned arithmetic, so an
241 // inverted range underflows to a ~4GB length that reaches the memset in
242 // BlankSequenceData(). The callers pass ints, so a negative max arrives
243 // here as a near-UINT32_MAX value that is not inverted -- bound it against
244 // the sequence buffer as well. Both checks are on the incoming values:
245 // max is aligned upward further down, which would hide an inversion.
246
4/4
✓ Branch 0 taken 1885 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 1883 times.
1886 if (max < min || max >= FPPD_MAX_CHANNELS) {
247 3 LogErr(VB_CHANNELOUT, "Ignoring invalid channel range %u - %u\n", min, max);
248 3 return;
249 }
250
2/2
✓ Branch 0 taken 2910 times.
✓ Branch 1 taken 1634 times.
4544 for (auto& r : prec) {
251 2910 int rm = r.first + r.second - 1;
252
4/4
✓ Branch 0 taken 2891 times.
✓ Branch 1 taken 19 times.
✓ Branch 2 taken 249 times.
✓ Branch 3 taken 2642 times.
2910 if (min >= r.first && max <= rm) {
253 // within the range, don't add it
254 249 return;
255 }
256 }
257
1/2
✓ Branch 0 taken 1634 times.
✗ Branch 1 not taken.
1634 prec.emplace_back(min, max - min + 1);
258
259 // having the reads be aligned to intervals of 8 can help performance so
260 // we'll expand the range a bit to align things better
261 // round minimum down to interval of 8
262 1634 min &= 0xFFFFFFF8;
263 1634 max += 8;
264 1634 max &= 0xFFFFFFF8;
265 1634 max -= 1;
266
2/2
✓ Branch 0 taken 2420 times.
✓ Branch 1 taken 1589 times.
4009 for (auto& r : normal) {
267 2420 int rm = r.first + r.second - 1;
268
4/4
✓ Branch 0 taken 2401 times.
✓ Branch 1 taken 19 times.
✓ Branch 2 taken 45 times.
✓ Branch 3 taken 2356 times.
2420 if (min >= r.first && max <= rm) {
269 // within the range, don't add it
270 45 return;
271 }
272 }
273
1/2
✓ Branch 0 taken 1589 times.
✗ Branch 1 not taken.
1589 normal.emplace_back(min, max - min + 1);
274 }
275
276 2147 static void ComputeOutputRanges() {
277 2147 std::vector<std::pair<uint32_t, uint32_t>> normal;
278 2147 std::vector<std::pair<uint32_t, uint32_t>> precise;
279 2147 FPPChannelOutputInstance* co = channelOutputs;
280
2/2
✓ Branch 0 taken 1334 times.
✓ Branch 1 taken 2147 times.
3481 while (co != nullptr) {
281
1/2
✓ Branch 0 taken 1334 times.
✗ Branch 1 not taken.
1334 if (co->output) {
282
2/4
✓ Branch 0 taken 1334 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1334 times.
✗ Branch 3 not taken.
1334 co->output->GetRequiredChannelRanges([&precise, &normal, &co](int m1, int m2) {
283
2/4
✓ Branch 0 taken 1783 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1783 times.
✗ Branch 3 not taken.
1783 LogInfo(VB_CHANNELOUT, "%s: Determined range needed %d - %d\n", co->output->GetOutputType().c_str(), m1, m2);
284 1783 addRange(m1, m2, precise, normal);
285 1783 });
286 }
287 1334 co = co->next;
288 }
289
1/2
✓ Branch 0 taken 2147 times.
✗ Branch 1 not taken.
2147 outputProcessors.GetRequiredChannelRanges([&precise, &normal](int m1, int m2) {
290 103 LogInfo(VB_CHANNELOUT, "OutputProcessor: Determined range needed %d - %d\n", m1, m2);
291 103 addRange(m1, m2, precise, normal);
292 103 });
293
1/2
✓ Branch 0 taken 2147 times.
✗ Branch 1 not taken.
2147 sortRanges(normal, false);
294
1/2
✓ Branch 0 taken 2147 times.
✗ Branch 1 not taken.
2147 sortRanges(precise, true);
295
2/2
✓ Branch 0 taken 2161 times.
✓ Branch 1 taken 2147 times.
4308 for (auto& r : precise) {
296
1/2
✓ Branch 0 taken 2161 times.
✗ Branch 1 not taken.
2161 LogInfo(VB_CHANNELOUT, "Determined range needed %d - %d\n", r.first, r.first + r.second - 1);
297 }
298
2/4
✓ Branch 0 taken 2147 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2147 times.
✗ Branch 3 not taken.
2147 preciseOutputRanges.store(std::make_shared<const OutputRangeVec>(std::move(precise)));
299
2/4
✓ Branch 0 taken 2147 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2147 times.
✗ Branch 3 not taken.
2147 outputRanges.store(std::make_shared<const OutputRangeVec>(std::move(normal)));
300
1/2
✓ Branch 0 taken 2147 times.
✗ Branch 1 not taken.
2147 Bridge_OutputRangesChanged();
301
1/2
✓ Branch 0 taken 2147 times.
✗ Branch 1 not taken.
2147 MultiSync::INSTANCE.Ping();
302
1/2
✓ Branch 0 taken 2147 times.
✗ Branch 1 not taken.
2147 MultiSync::INSTANCE.WriteRuntimeInfoFile();
303 2147 }
304
305 /////////////////////////////////////////////////////////////////////////////
306
307 // in some of these cases, we could symlink the shlib
308 static std::map<std::string, std::string> OUTPUT_REMAPS = {
309 { "VirtualMatrix", "FBMatrix" },
310 { "DMX-Pro", "USBDMX" },
311 { "DMX-Open", "USBDMX" },
312 { "Pixelnet-Lynx", "USBPixelnet" },
313 { "Pixelnet-Open", "USBPixelnet" },
314 { "universes", "UDPOutput" },
315 // The RPIWS281X driver has been removed. DPIPixels drives the same pins and
316 // maps the old RPIWS281X string configs itself, so old configs load unchanged.
317 { "RPIWS281X", "DPIPixels" }
318 };
319
320 bool skipOutputRangeCompute = false;
321
322 // Every output is closed through here. A ThreadedChannelOutput subclass's
323 // Close() frees its own resources (serial port, libusb handle, buffers, X
324 // display) and only then reaches ThreadedChannelOutput::Close(), which stops
325 // the worker - and the worker flushes any pending frame on its way out, into
326 // what was just freed. Stopping it first covers every subclass, plugins
327 // included, without relying on each one to order its Close() correctly.
328 1203 static void CloseChannelOutput(ChannelOutput* output) {
329
3/4
✓ Branch 0 taken 1203 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 60 times.
✓ Branch 3 taken 1143 times.
1203 if (auto* threaded = dynamic_cast<ThreadedChannelOutput*>(output)) {
330 60 threaded->StopOutputThread();
331 }
332 1203 output->Close();
333 1203 }
334
335 9657 static bool ReloadChannelOutputsForFile(const std::string& cfgFile) {
336
1/2
✓ Branch 0 taken 9657 times.
✗ Branch 1 not taken.
9657 Json::Value root;
337
3/4
✓ Branch 0 taken 9657 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1282 times.
✓ Branch 3 taken 8375 times.
9657 if (FileExists(cfgFile)) {
338
3/4
✓ Branch 0 taken 1282 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 1280 times.
1282 if (!LoadJsonFromFile(cfgFile, root, JsonRoot::Object)) {
339
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (!skipOutputRangeCompute) {
340 // this is a reload, it could still be writing the file so we'll wait a bit
341 // and try again
342 ✗ std::this_thread::sleep_for(std::chrono::milliseconds(5));
343 }
344
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 if (!LoadJsonFromFile(cfgFile, root, JsonRoot::Object)) {
345
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 std::string warning = "Could not parse " + cfgFile + ". Some outputs may not work.";
346
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 WarningHolder::AddWarning(26, warning);
347
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 outputLoadWarnings[cfgFile].insert(warning);
348
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 LogErr(VB_CHANNELOUT, "Error parsing %s\n", cfgFile.c_str());
349 2 return false;
350 2 }
351 }
352 }
353
354 9655 bool changed = false;
355 {
356 // Unlink and destroy under outputThreadLock. PrepareChannelData() and
357 // SendChannelData() walk this list from the channel output thread,
358 // which holds that lock for the whole of its cycle; without it the
359 // walk can be sitting inside inst->output->PrepData() when the
360 // instance is freed here, and inst = inst->next then reads a freed
361 // node as well. This used to be a 100ms sleep, which is not a
362 // barrier at all -- a slow output cycle (the reports that led here had
363 // Send: 73ms against a 50ms loop) simply runs past it.
364 //
365 // Only the teardown is covered. Init() of the replacement outputs
366 // below can take a long time (compileMatrix.sh, PRU firmware loads)
367 // and holding the lock across it would stall output for that whole
368 // time; a half-populated list is harmless, an unlinked-but-live one is
369 // not.
370 9655 std::list<FPPChannelOutputInstance*> toDelete;
371
1/2
✓ Branch 0 taken 9655 times.
✗ Branch 1 not taken.
9655 std::unique_lock<std::mutex> lock = LockOutputThread();
372 // And against a blank being sent inline from another thread (see
373 // inlineOutputLock); that send doesn't hold outputThreadLock.
374
1/2
✓ Branch 0 taken 9655 times.
✗ Branch 1 not taken.
9655 std::unique_lock<std::recursive_mutex> inlineLock(inlineOutputLock);
375 // and against HasUniverseOutputs()/GetOutputTypes(); see there
376
1/2
✓ Branch 0 taken 9655 times.
✗ Branch 1 not taken.
9655 std::unique_lock<std::shared_mutex> freeLock(outputListFreeLock);
377
2/2
✓ Branch 0 taken 3723 times.
✓ Branch 1 taken 9655 times.
13378 for (auto inst = channelOutputs.load(); inst != nullptr; inst = inst->next) {
378 // remove everything in the list that was loaded from the cfgFile
379
2/2
✓ Branch 0 taken 204 times.
✓ Branch 1 taken 3519 times.
3723 if (inst->sourceFile == cfgFile) {
380 204 changed = true;
381
1/2
✓ Branch 0 taken 204 times.
✗ Branch 1 not taken.
204 toDelete.push_back(inst);
382
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 196 times.
204 if (inst->next) {
383 8 inst->next.load()->prev = inst->prev.load();
384 }
385
2/2
✓ Branch 0 taken 75 times.
✓ Branch 1 taken 129 times.
204 if (inst->prev) {
386 75 inst->prev.load()->next = inst->next.load();
387 }
388
2/2
✓ Branch 0 taken 129 times.
✓ Branch 1 taken 75 times.
204 if (inst == channelOutputs.load()) {
389 129 channelOutputs = inst->next.load();
390 }
391
2/2
✓ Branch 0 taken 196 times.
✓ Branch 1 taken 8 times.
204 if (inst == lastChannelOutput.load()) {
392 196 lastChannelOutput = inst->prev.load();
393 }
394 }
395 }
396 // Unlinked: no walk under it can reach these any more. Let go before
397 // closing them -- see HasUniverseOutputs().
398
1/2
✓ Branch 0 taken 9655 times.
✗ Branch 1 not taken.
9655 freeLock.unlock();
399
3/4
✓ Branch 0 taken 9655 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 218 times.
✓ Branch 3 taken 9437 times.
9655 if (ChannelOutputThreadIsRunning()) {
400 // StoppingOutput() is the pair of the StartingOutput() the output
401 // thread makes when it comes up, so it is only owed to the outputs
402 // if that thread actually ran.
403
2/2
✓ Branch 0 taken 152 times.
✓ Branch 1 taken 218 times.
370 for (auto inst : toDelete) {
404
1/2
✓ Branch 0 taken 152 times.
✗ Branch 1 not taken.
152 if (inst->output) {
405
1/2
✓ Branch 0 taken 152 times.
✗ Branch 1 not taken.
152 inst->output->StoppingOutput();
406 }
407 }
408 }
409
2/2
✓ Branch 0 taken 204 times.
✓ Branch 1 taken 9655 times.
9859 for (auto inst : toDelete) {
410
1/2
✓ Branch 0 taken 204 times.
✗ Branch 1 not taken.
204 if (inst->output) {
411
1/2
✓ Branch 0 taken 204 times.
✗ Branch 1 not taken.
204 CloseChannelOutput(inst->output);
412
1/2
✓ Branch 0 taken 204 times.
✗ Branch 1 not taken.
204 delete inst->output;
413 }
414
1/2
✓ Branch 0 taken 204 times.
✗ Branch 1 not taken.
204 delete inst;
415 }
416 9655 }
417
3/4
✓ Branch 0 taken 9655 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✓ Branch 3 taken 9655 times.
9663 for (auto& warning : outputLoadWarnings[cfgFile]) {
418
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
16 WarningHolder::RemoveWarning(26, warning);
419 }
420
1/2
✓ Branch 0 taken 9655 times.
✗ Branch 1 not taken.
9655 outputLoadWarnings.erase(cfgFile);
421
3/4
✓ Branch 0 taken 9655 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 21 times.
✓ Branch 3 taken 9655 times.
9676 for (auto& [id, warning] : outputConfigWarnings[cfgFile]) {
422
2/4
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 21 times.
✗ Branch 3 not taken.
45 WarningHolder::RemoveWarning(id, warning);
423 }
424
1/2
✓ Branch 0 taken 9655 times.
✗ Branch 1 not taken.
9655 outputConfigWarnings.erase(cfgFile);
425
3/4
✓ Branch 0 taken 9655 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1280 times.
✓ Branch 3 taken 8375 times.
9655 if (FileExists(cfgFile)) {
426 // indexing a non-array by position throws, out of this callback
427
5/12
✓ Branch 0 taken 1280 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1280 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1280 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1280 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1280 times.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
1280 const Json::Value outputs = root["channelOutputs"].isArray() ? root["channelOutputs"] : Json::Value(Json::arrayValue);
428 1280 std::string type;
429 1280 int start = 0;
430 1280 int count = 0;
431
432
3/4
✓ Branch 0 taken 2574 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1294 times.
✓ Branch 3 taken 1280 times.
2574 for (int c = 0; c < outputs.size(); c++) {
433 1294 bool enabled = false;
434 try {
435 // asInt()/asString() throw on a value of the wrong type
436 // (hand-edited or third-party configs), and this runs in the
437 // FileMonitor callback: an escaping exception ends fppd
438
3/6
✓ Branch 0 taken 1294 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1294 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1294 times.
✗ Branch 5 not taken.
1294 type = outputs[c]["type"].asString();
439
4/6
✓ Branch 0 taken 1294 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1294 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1293 times.
✓ Branch 5 taken 1 times.
1294 enabled = outputs[c]["enabled"].asInt();
440
3/6
✓ Branch 0 taken 1293 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1293 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1293 times.
✗ Branch 5 not taken.
1293 start = outputs[c]["startChannel"].asInt();
441
3/6
✓ Branch 0 taken 1293 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1293 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1293 times.
✗ Branch 5 not taken.
1293 count = outputs[c]["channelCount"].asInt();
442
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 } catch (const std::exception& ex) {
443
6/12
✓ 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.
✓ Branch 6 taken 1 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1 times.
✗ Branch 11 not taken.
1 std::string warning = "Could not read output " + std::to_string(c + 1) + " in " + cfgFile + " (" + ex.what() + ")";
444
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 WarningHolder::AddWarning(26, warning);
445
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 outputLoadWarnings[cfgFile].insert(warning);
446
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogErr(VB_CHANNELOUT, "%s\n", warning.c_str());
447 1 continue;
448 1 }
449
450
2/2
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 1279 times.
1293 if (!enabled) {
451
1/2
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
14 LogDebug(VB_CHANNELOUT, "Skipping Disabled Channel Output: %s\n", type.c_str());
452 14 continue;
453 }
454
455 // internally we start channel counts at zero
456 1279 start -= 1;
457
3/4
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1278 times.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1279 if (start < 0 && count <= 0) {
458 // nothing to send from it, but the start still becomes the
459 // offset SendData() is handed; -1 would wrap to UINT_MAX
460 1 start = 0;
461 }
462
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 1279 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
1279 if (start < 0 && count > 0) {
463 // we have a negative channel number, but actually are supposed to be outputting data
464 // Skip this output as that's not valid
465 ✗ std::string warning = "Could not initialize output type " + type + ". Invalid start channel.";
466 ✗ WarningHolder::AddWarning(26, warning);
467 ✗ outputLoadWarnings[cfgFile].insert(warning);
468 ✗ LogInfo(VB_CHANNELOUT, "%s\n", warning.c_str());
469 ✗ continue;
470 ✗ }
471
472
2/4
✓ Branch 0 taken 1279 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1279 times.
1279 FPPChannelOutputInstance* channelOutput = new FPPChannelOutputInstance();
473
474 1279 channelOutput->startChannel = start;
475 1279 channelOutput->channelCount = count;
476
1/2
✓ Branch 0 taken 1279 times.
✗ Branch 1 not taken.
1279 channelOutput->sourceFile = cfgFile;
477
1/2
✓ Branch 0 taken 1279 times.
✗ Branch 1 not taken.
1279 std::string libnamePfx = "";
478
479 // First some Channel Outputs enabled everythwere
480
3/4
✓ Branch 0 taken 1279 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 147 times.
✓ Branch 3 taken 1132 times.
1279 if (type == "LEDPanelMatrix") {
481 // for LED matrices, the driver is determined by the subType
482
1/2
✓ Branch 0 taken 147 times.
✗ Branch 1 not taken.
147 libnamePfx = "matrix-";
483 // outside the try below: a subType of the wrong JSON type
484 // threw from asString() to std::terminate at startup. A
485 // non-string finds no driver, which is reported as usual.
486
2/4
✓ Branch 0 taken 147 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 147 times.
✗ Branch 3 not taken.
147 const Json::Value& subType = outputs[c]["subType"];
487
7/12
✓ Branch 0 taken 147 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 146 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 146 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1 times.
✓ Branch 9 taken 146 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
149 type = subType.isString() ? subType.asString() : std::string("(invalid subType)");
488
3/4
✓ Branch 0 taken 1132 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 814 times.
✓ Branch 3 taken 318 times.
1132 } else if (OUTPUT_REMAPS.find(type) != OUTPUT_REMAPS.end()) {
489
2/4
✓ Branch 0 taken 814 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 814 times.
✗ Branch 3 not taken.
814 type = OUTPUT_REMAPS[type];
490 }
491
492 try {
493 // the output's constructor raises warnings too, not just Init()
494 struct ConfigWarningScope {
495 1279 ConfigWarningScope(const std::string& f) { outputConfigWarningFile = f; }
496 1279 ~ConfigWarningScope() { outputConfigWarningFile.clear(); }
497
1/2
✓ Branch 0 taken 1279 times.
✗ Branch 1 not taken.
1279 } warningScope(cfgFile);
498
3/6
✓ Branch 0 taken 1279 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1279 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1279 times.
✗ Branch 5 not taken.
1279 LogInfo(VB_CHANNELOUT, "Loading output plugin: type=%s, libname=%s\n", type.c_str(), ("fpp-co-" + libnamePfx + type).c_str());
499
6/10
✓ Branch 0 taken 1279 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1279 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1279 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1279 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1276 times.
✓ Branch 9 taken 3 times.
1279 FPPPlugins::ChannelOutputPlugin* p = dynamic_cast<FPPPlugins::ChannelOutputPlugin*>(PluginManager::INSTANCE.findPlugin(libnamePfx + type, "fpp-co-" + libnamePfx + type));
500
2/2
✓ Branch 0 taken 1276 times.
✓ Branch 1 taken 3 times.
1279 if (p) {
501
1/2
✓ Branch 0 taken 1276 times.
✗ Branch 1 not taken.
1276 LogInfo(VB_CHANNELOUT, "Plugin found, creating output: start=%d, count=%d\n", start, count);
502
1/2
✓ Branch 0 taken 1276 times.
✗ Branch 1 not taken.
1276 channelOutput->output = p->createChannelOutput(start, count);
503
1/2
✓ Branch 0 taken 1276 times.
✗ Branch 1 not taken.
1276 if (channelOutput->output) {
504 // Pins the plugin: the output outlives it and is owned
505 // elsewhere, so the plugin cannot be unloaded.
506
1/2
✓ Branch 0 taken 1276 times.
✗ Branch 1 not taken.
1276 PluginManager::INSTANCE.noteChannelOutputCreated(p);
507 }
508 } else {
509
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 LogWarn(VB_CHANNELOUT, "Plugin NOT found for type: %s\n", type.c_str());
510 }
511
2/2
✓ Branch 0 taken 1276 times.
✓ Branch 1 taken 3 times.
1279 if (channelOutput->output) {
512
1/2
✓ Branch 0 taken 1276 times.
✗ Branch 1 not taken.
1276 LogInfo(VB_CHANNELOUT, "Calling Init() on %s output\n", type.c_str());
513
4/6
✓ Branch 0 taken 1276 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1276 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1272 times.
✓ Branch 5 taken 4 times.
1280 bool initOK = channelOutput->output->Init(outputs[c]);
514
2/2
✓ Branch 0 taken 1203 times.
✓ Branch 1 taken 69 times.
1272 if (initOK) {
515 // Every output reads (PrepData) or sends from exactly the
516 // ranges it reports here, straight out of the sequence
517 // buffer, which is FPPD_MAX_CHANNELS long. addRange()
518 // drops an out-of-bounds range from the read set, but the
519 // output itself would still walk off the end of the
520 // buffer every frame, so it must not be started at all.
521 1203 bool bad = false;
522 1203 int badMin = 0;
523 1203 int badMax = -1;
524
2/4
✓ Branch 0 taken 1203 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1203 times.
✗ Branch 3 not taken.
1203 channelOutput->output->GetRequiredChannelRanges([&bad, &badMin, &badMax](int m1, int m2) {
525 // Only a non-empty range outside the buffer: an
526 // inverted one is how an output with nothing to
527 // send reports. Before channel 1 is as bad as past
528 // the end: an output reads channelData[m1] either way.
529
6/8
✓ Branch 0 taken 1607 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1605 times.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 1605 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✓ Branch 7 taken 1604 times.
1607 if (!bad && m2 >= m1 && (m1 < 0 || m2 >= (int)FPPD_MAX_CHANNELS)) {
530 1 bad = true;
531 1 badMin = m1;
532 1 badMax = m2;
533 }
534 1607 });
535
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1202 times.
1203 if (bad) {
536
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 std::string warning = "Output type " + type + " needs channels " +
537
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.
4 std::to_string(badMin + 1) + "-" + std::to_string(badMax + 1) +
538
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
4 ", outside 1-" + std::to_string(FPPD_MAX_CHANNELS) +
539
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 ". Output disabled.";
540
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 WarningHolder::AddWarning(26, warning);
541
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogErr(VB_CHANNELOUT, "%s\n", warning.c_str());
542
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 outputLoadWarnings[cfgFile].insert(warning);
543
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 CloseChannelOutput(channelOutput->output);
544
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 delete channelOutput->output;
545 1 channelOutput->output = nullptr;
546 1 initOK = false;
547 1 }
548 }
549
2/2
✓ Branch 0 taken 1202 times.
✓ Branch 1 taken 70 times.
1272 if (initOK) {
550
1/2
✓ Branch 0 taken 1202 times.
✗ Branch 1 not taken.
1202 LogInfo(VB_CHANNELOUT, "Init succeeded, adding to channel outputs\n");
551
1/2
✓ Branch 0 taken 1202 times.
✗ Branch 1 not taken.
1202 addChannelOutput(channelOutput);
552
1/2
✓ Branch 0 taken 1202 times.
✗ Branch 1 not taken.
1202 LogDebug(VB_CHANNELOUT, "Configured %s Channel Output\n", type.c_str());
553 1202 channelOutput = nullptr;
554 1202 changed = true;
555
2/2
✓ Branch 0 taken 69 times.
✓ Branch 1 taken 1 times.
70 } else if (channelOutput->output) {
556
2/4
✓ Branch 0 taken 69 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 69 times.
✗ Branch 3 not taken.
69 std::string warning = "Could not initialize output type " + type + ". Check logs for details.";
557
1/2
✓ Branch 0 taken 69 times.
✗ Branch 1 not taken.
69 WarningHolder::AddWarning(26, warning);
558
1/2
✓ Branch 0 taken 69 times.
✗ Branch 1 not taken.
69 LogErr(VB_CHANNELOUT, warning);
559
2/4
✓ Branch 0 taken 69 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 69 times.
✗ Branch 3 not taken.
69 outputLoadWarnings[cfgFile].insert(warning);
560
1/2
✓ Branch 0 taken 69 times.
✗ Branch 1 not taken.
69 delete channelOutput->output;
561 69 channelOutput->output = nullptr;
562 69 }
563 } else {
564
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 std::string warning = "Could not create output type " + type + ". Check logs for details.";
565
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 WarningHolder::AddWarning(26, warning);
566
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 outputLoadWarnings[cfgFile].insert(warning);
567
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 LogErr(VB_CHANNELOUT, warning);
568 3 }
569
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
1283 } catch (const std::exception& ex) {
570
4/8
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 4 times.
✗ Branch 7 not taken.
4 std::string warning = "Could not initialize output type " + type + ". (" + ex.what() + ")";
571
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 WarningHolder::AddWarning(26, warning);
572
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 LogErr(VB_CHANNELOUT, warning);
573
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 outputLoadWarnings[cfgFile].insert(warning);
574
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 if (channelOutput && channelOutput->output) {
575
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 delete channelOutput->output;
576 4 channelOutput->output = nullptr;
577 }
578 4 }
579
2/2
✓ Branch 0 taken 77 times.
✓ Branch 1 taken 1202 times.
1279 if (channelOutput) {
580 // if we didn't add the channel output, delete it
581
1/2
✓ Branch 0 taken 77 times.
✗ Branch 1 not taken.
77 delete channelOutput;
582 }
583 1279 }
584 1280 }
585 9655 return changed;
586 9657 }
587
588 1887 int InitializeChannelOutputs(void) {
589
1/2
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
1887 Json::Value root;
590
591 1887 channelOutputFrame = 0;
592
593 // FIXME, build this list dynamically
594 1887 const char* configFiles[] = {
595 "/co-universes.json",
596 "/co-other.json",
597 "/co-pwm.json",
598 #if defined(PLATFORM_BBB) || defined(PLATFORM_BB64)
599 "/co-bbbStrings.json",
600 #else
601 "/co-pixelStrings.json",
602 #endif
603 "/channeloutputs.json",
604 NULL
605 };
606 1887 skipOutputRangeCompute = true;
607
2/2
✓ Branch 0 taken 9435 times.
✓ Branch 1 taken 1887 times.
11322 for (int f = 0; configFiles[f]; f++) {
608
3/6
✓ Branch 0 taken 9435 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9435 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 9435 times.
✗ Branch 5 not taken.
18870 std::string configFile = FPP_DIR_CONFIG(configFiles[f]);
609
4/8
✓ Branch 0 taken 9435 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9435 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 9435 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 9435 times.
✗ Branch 7 not taken.
27505 FileMonitor::INSTANCE.AddFile("ChannelOutputSetup", configFile, [configFile]() {
610 9655 LogDebug(VB_CHANNELOUT, "Reloading Channel Outputs for %s\n", configFile.c_str());
611 9655 bool changed = ReloadChannelOutputsForFile(configFile);
612 #if defined(PLATFORM_BBB) || defined(PLATFORM_BB64)
613 // On BBB platforms, if co-bbbStrings.json changes and we have BBBMatrix outputs,
614 // we need to reload channeloutputs.json too so compileMatrix.sh gets called
615
6/6
✓ Branch 0 taken 107 times.
✓ Branch 1 taken 463 times.
✓ Branch 2 taken 84 times.
✓ Branch 3 taken 23 times.
✓ Branch 4 taken 84 times.
✓ Branch 5 taken 486 times.
570 if (changed && configFile.find("co-bbbStrings.json") != std::string::npos) {
616
3/6
✓ Branch 0 taken 84 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 84 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 84 times.
✗ Branch 5 not taken.
84 std::string channelOutputsFile = FPP_DIR_CONFIG("/channeloutputs.json");
617
3/4
✓ Branch 0 taken 84 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 82 times.
84 if (FileExists(channelOutputsFile)) {
618
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 Json::Value root;
619
5/10
✓ 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.
✓ Branch 8 taken 2 times.
✗ Branch 9 not taken.
2 if (LoadJsonFromFile(channelOutputsFile, root, JsonRoot::Object) && root.isMember("channelOutputs")) {
620
3/6
✓ 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.
2 for (int i = 0; i < root["channelOutputs"].size(); i++) {
621 // the BBBMatrix driver is the "LEDscapeMatrix"
622 // subtype, which is what the UI saves
623
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 const std::string subType = root["channelOutputs"][i]["subType"].asString();
624
4/10
✓ 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.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
4 if (root["channelOutputs"][i]["type"].asString() == "LEDPanelMatrix" &&
625
6/16
✓ 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.
✗ Branch 8 not taken.
✓ Branch 9 taken 2 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
✓ Branch 14 taken 2 times.
✗ Branch 15 not taken.
6 (subType == "LEDscapeMatrix" || subType == "BBBMatrix") &&
626
5/10
✓ 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.
✓ Branch 8 taken 2 times.
✗ Branch 9 not taken.
2 root["channelOutputs"][i]["enabled"].asInt() == 1) {
627
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 LogDebug(VB_CHANNELOUT, "BBBMatrix output detected, reloading %s to ensure PRU code is compiled\n", channelOutputsFile.c_str());
628
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 ReloadChannelOutputsForFile(channelOutputsFile);
629 2 break;
630 }
631
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 }
632 }
633 2 }
634 84 }
635 #endif
636
4/4
✓ Branch 0 taken 1208 times.
✓ Branch 1 taken 8447 times.
✓ Branch 2 taken 217 times.
✓ Branch 3 taken 991 times.
9655 if (changed && !skipOutputRangeCompute) {
637 217 ComputeOutputRanges();
638 }
639 9655 })
640
2/4
✓ Branch 0 taken 9435 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9435 times.
✗ Branch 3 not taken.
18870 .TriggerFileChanged("ChannelOutputSetup", configFile);
641 9435 }
642 1887 skipOutputRangeCompute = false;
643
644 1887 int count = 0;
645 1887 FPPChannelOutputInstance* co = channelOutputs;
646
2/2
✓ Branch 0 taken 997 times.
✓ Branch 1 taken 1887 times.
2884 while (co != nullptr) {
647 997 count++;
648 997 co = co->next;
649 }
650
1/2
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
1887 LogDebug(VB_CHANNELOUT, "%d Channel Outputs configured\n", count);
651
3/6
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1887 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1887 times.
✗ Branch 5 not taken.
2047 std::string opfilename = FPP_DIR_CONFIG("/outputprocessors.json");
652
4/8
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1887 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1887 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1887 times.
✗ Branch 7 not taken.
5501 FileMonitor::INSTANCE.AddFile("outputprocessors.json", opfilename, [opfilename]() {
653
1/2
✓ Branch 0 taken 1970 times.
✗ Branch 1 not taken.
1970 Json::Value newRoot;
654
8/10
✓ Branch 0 taken 1970 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 133 times.
✓ Branch 3 taken 1837 times.
✓ Branch 4 taken 133 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 40 times.
✓ Branch 7 taken 93 times.
✓ Branch 8 taken 40 times.
✓ Branch 9 taken 1930 times.
1970 if (FileExists(opfilename) && !LoadJsonFromFile(opfilename, newRoot, JsonRoot::Object)) {
655 // probably caught part way through being
656 // written; one retry, as for the outputs, and
657 // otherwise keep the processors running now
658 // rather than dropping every one of them
659
1/2
✓ Branch 0 taken 40 times.
✗ Branch 1 not taken.
40 std::this_thread::sleep_for(std::chrono::milliseconds(5));
660
2/4
✓ Branch 0 taken 40 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 40 times.
✗ Branch 3 not taken.
40 if (!LoadJsonFromFile(opfilename, newRoot, JsonRoot::Object)) {
661
1/2
✓ Branch 0 taken 40 times.
✗ Branch 1 not taken.
40 LogErr(VB_CHANNELOUT, "Could not parse %s, keeping the current output processors\n", opfilename.c_str());
662 40 return;
663 }
664 }
665
1/2
✓ Branch 0 taken 1930 times.
✗ Branch 1 not taken.
1930 outputProcessors.loadFromJSON(newRoot);
666
1/2
✓ Branch 0 taken 1930 times.
✗ Branch 1 not taken.
1930 ComputeOutputRanges();
667
2/2
✓ Branch 0 taken 1930 times.
✓ Branch 1 taken 40 times.
1970 })
668
2/4
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1887 times.
✗ Branch 3 not taken.
3774 .TriggerFileChanged("outputprocessors.json", opfilename);
669 1887 return 1;
670 1887 }
671
672 /*
673 * Set the channel output frame counter to a specific value
674 */
675 67265 void SetChannelOutputFrameNumber(int frameNumber) {
676 // Every frame; a value nothing else is published through.
677 67265 channelOutputFrame.store(frameNumber, std::memory_order_relaxed);
678 67265 }
679
680 /*
681 * Reset the output frame count
682 */
683 1965 void ResetChannelOutputFrameNumber(void) {
684 1965 channelOutputFrame = 0;
685 1965 mediaElapsedSeconds = 0.0;
686 1965 }
687
688 3651 void OverlayOutputTestData(std::set<std::string> types, unsigned char* channelData, int cycleCnt, float percentOfCycle, int testType, const Json::Value& extraConfig) {
689
2/2
✓ Branch 0 taken 3651 times.
✓ Branch 1 taken 3651 times.
7302 for (auto inst = channelOutputs.load(); inst != nullptr; inst = inst->next) {
690 3651 auto output = inst->output;
691
8/20
✓ Branch 0 taken 3651 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3651 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3648 times.
✓ Branch 5 taken 3 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 3648 times.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✓ Branch 15 taken 3651 times.
✓ Branch 16 taken 3648 times.
✓ Branch 17 taken 3 times.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
3651 if (output && output->SupportsTesting() && (types.empty() || types.find(output->GetOutputType()) != types.end())) {
692 3648 output->OverlayTestData(channelData, cycleCnt, percentOfCycle, testType, extraConfig);
693 }
694 }
695 3651 }
696 // see the note on HasUniverseOutputs() for the lock this walk takes
697 1 std::set<std::string> GetOutputTypes() {
698 1 std::set<std::string> ret;
699
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 std::shared_lock<std::shared_mutex> l(outputListFreeLock);
700
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
2 for (auto inst = channelOutputs.load(); inst != nullptr; inst = inst->next) {
701 1 auto output = inst->output;
702
4/8
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 1 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 1 times.
1 if (output && output->SupportsTesting()) {
703 ✗ ret.insert(output->GetOutputType());
704 }
705 }
706 1 return ret;
707 1 }
708 // The two per-frame walks load each link with acquire rather than seq_cst:
709 // one barrier instead of two per node on an AM335x, and still enough to see
710 // the fields of a node addChannelOutput() linked in.
711 138483 int PrepareChannelData(char* channelData) {
712 138483 outputProcessors.ProcessData((unsigned char*)channelData);
713
2/2
✓ Branch 0 taken 140264 times.
✓ Branch 1 taken 138483 times.
278747 for (auto inst = channelOutputs.load(std::memory_order_acquire); inst != nullptr; inst = inst->next.load(std::memory_order_acquire)) {
714 140264 auto output = inst->output;
715
1/2
✓ Branch 0 taken 140264 times.
✗ Branch 1 not taken.
140264 if (output) {
716 140264 output->PrepData((unsigned char*)channelData);
717 }
718 }
719 138483 return 0;
720 }
721
722 /*
723 *
724 */
725 131548 int SendChannelData(const char* channelData) {
726 131548 int i = 0;
727 FPPChannelOutputInstance* inst;
728
729
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 131548 times.
131548 if (WillLog(LOG_DEBUG, VB_CHANNELDATA)) {
730 ✗ uint32_t minimumNeededChannel = GetOutputRangesSnapshot(false)->at(0).first;
731 char buf[128];
732 ✗ snprintf(buf, sizeof(buf), "Channel Data starting at channel %d", minimumNeededChannel);
733 ✗ HexDump(buf, &channelData[minimumNeededChannel], 16, VB_CHANNELDATA);
734 }
735
736
2/2
✓ Branch 0 taken 135459 times.
✓ Branch 1 taken 131548 times.
267007 for (auto inst = channelOutputs.load(std::memory_order_acquire); inst != nullptr; inst = inst->next.load(std::memory_order_acquire)) {
737 135459 auto output = inst->output;
738
1/2
✓ Branch 0 taken 135459 times.
✗ Branch 1 not taken.
135459 if (output) {
739 135459 output->SendData((unsigned char*)(channelData + inst->startChannel));
740 }
741 }
742
743 131548 return 0;
744 }
745
746 /*
747 *
748 */
749 1956 void StartingOutput(void) {
750 1956 OutputMonitor::INSTANCE.AutoEnableOutputs();
751
752
2/2
✓ Branch 0 taken 1079 times.
✓ Branch 1 taken 1956 times.
3035 for (auto inst = channelOutputs.load(); inst != nullptr; inst = inst->next) {
753
1/2
✓ Branch 0 taken 1079 times.
✗ Branch 1 not taken.
1079 if (inst->output) {
754 1079 inst->output->StartingOutput();
755 }
756 }
757 1956 }
758
759 /*
760 *
761 */
762 1947 void StoppingOutput(void) {
763
2/2
✓ Branch 0 taken 1075 times.
✓ Branch 1 taken 1947 times.
3022 for (auto inst = channelOutputs.load(); inst != nullptr; inst = inst->next) {
764
1/2
✓ Branch 0 taken 1075 times.
✗ Branch 1 not taken.
1075 if (inst->output) {
765 1075 inst->output->StoppingOutput();
766 }
767 }
768 1947 OutputMonitor::INSTANCE.AutoDisableOutputs();
769 1947 }
770
771 /*
772 *
773 */
774 1867 void CloseChannelOutputs(void) {
775 // Deliberately not taking outputThreadLock, unlike the reload path: this
776 // runs at shutdown, after StopChannelOutputThread(), so there is nothing
777 // left to race with. If that thread did wedge and is still holding the
778 // lock, blocking here would trade a crash-exit for a hung daemon.
779
2/2
✓ Branch 0 taken 998 times.
✓ Branch 1 taken 1867 times.
2865 for (auto inst = channelOutputs.load(); inst != nullptr; inst = inst->next) {
780
1/2
✓ Branch 0 taken 998 times.
✗ Branch 1 not taken.
998 if (inst->output) {
781 998 CloseChannelOutput(inst->output);
782 }
783 }
784
2/2
✓ Branch 0 taken 998 times.
✓ Branch 1 taken 1867 times.
2865 for (auto inst = channelOutputs.load(); inst != nullptr; inst = inst->next) {
785 998 auto output = inst->output;
786
1/2
✓ Branch 0 taken 998 times.
✗ Branch 1 not taken.
998 if (output) {
787 998 inst->output = nullptr;
788
1/2
✓ Branch 0 taken 998 times.
✗ Branch 1 not taken.
998 delete output;
789 }
790 }
791
2/2
✓ Branch 0 taken 998 times.
✓ Branch 1 taken 1867 times.
2865 while (channelOutputs.load() != nullptr) {
792 998 FPPChannelOutputInstance* inst = channelOutputs.load();
793 998 channelOutputs = inst->next.load();
794
1/2
✓ Branch 0 taken 998 times.
✗ Branch 1 not taken.
998 delete inst;
795 }
796 1867 }
797