GCC Code Coverage Report


Directory: ./
File: overlays/PixelOverlay.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 1143 1374 83.2%
Functions: 74 80 92.5%
Branches: 1858 3632 51.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 #include "fpphttp.h" // drogon/HTTP helpers used here; no longer pulled transitively (see fpphttp_types.h)
17 #include "fpphttp_clientip.h" // getEffectiveClientIP() - FPP-internal, not in the plugin API
18
19 #include <sys/stat.h>
20 #include <dirent.h>
21
22 #include <algorithm> // std::sort/std::transform in the image file listing, preview ordering
23 #include <cctype> // tolower/isspace in parseColor
24 #include <cerrno> // strtoull range check in parseColor
25 #include <climits> // INT_MAX in the preview node numbering
26 #include <fcntl.h>
27 #include <filesystem> // image file listing for the Image effect's picker
28 #include <set>
29 #include <fstream> // virtualdisplaymap parse for model preview endpoint
30 #include <unistd.h> // write -- needed directly for NOPCH builds
31 #include <vector>
32
33 #include <Magick++.h>
34
35 #include <magick/type.h>
36
37 #include "../channeloutput/channeloutputthread.h"
38 #include "../common.h"
39 #include "../effects.h"
40 #include "../Sequence.h" // FPPD_MAX_CHANNELS bounds an overlay range
41 #include "../log.h"
42 #include "../settings.h"
43 #include "../Warnings.h" // InvalidConfigEntry -- needed directly for NOPCH builds
44
45 #include "PixelOverlay.h"
46 #include "PixelOverlayEffects.h"
47 #include "PixelOverlayModel.h"
48 #include "PixelOverlayModelFB.h"
49 #include "PixelOverlayModelSub.h"
50
51 PixelOverlayManager PixelOverlayManager::INSTANCE;
52
53 class OverlayRange {
54 public:
55 1494 OverlayRange(int s, int e, int v) :
56 1494 start(s),
57 1494 end(e),
58 1494 value(v) {}
59
60 int start = 0;
61 int end = 0;
62 int value = 0;
63 };
64
65 class PixelOverlayModelHolder {
66 public:
67 PixelOverlayModel* model;
68 std::map<std::string, std::function<void(PixelOverlayModel*)>> listeners;
69
70 void toJson(Json::Value& json) const {
71 if (model) {
72 model->toJson(json);
73 }
74 }
75 };
76
77 39338 bool PixelOverlayManager::parseColor(const std::string& c, uint32_t& color) {
78 static const std::map<std::string, uint32_t> NAMES = {
79 ✗ { "red", 0xFF0000 }, { "green", 0x00FF00 }, { "blue", 0x0000FF },
80 ✗ { "yellow", 0xFFFF00 }, { "black", 0x000000 }, { "white", 0xFFFFFF },
81 ✗ { "cyan", 0x00FFFF }, { "magenta", 0xFF00FF }, { "gray", 0x808080 },
82 ✗ { "grey", 0x808080 }
83
6/12
✓ Branch 0 taken 103 times.
✓ Branch 1 taken 39235 times.
✓ Branch 2 taken 103 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 103 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1030 times.
✓ Branch 7 taken 103 times.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
40574 };
84 39338 color = 0;
85
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 39336 times.
39338 if (c.empty()) {
86 2 return true;
87 }
88
1/2
✓ Branch 0 taken 39336 times.
✗ Branch 1 not taken.
39336 std::string lc = c;
89 314691 std::transform(lc.begin(), lc.end(), lc.begin(), [](unsigned char ch) { return std::tolower(ch); });
90
1/2
✓ Branch 0 taken 39336 times.
✗ Branch 1 not taken.
39336 auto it = NAMES.find(lc);
91
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 39335 times.
39336 if (it != NAMES.end()) {
92 1 color = it->second;
93 1 return true;
94 }
95 // "#RRGGBB" (or "#RRGGBBWW") is hex; anything else is a number in C
96 // notation ("0xFF0000", "16711680"). strtoull, not std::stoul: the latter
97 // throws on garbage, and these strings come off MQTT topics.
98 39335 const char* digits = c.c_str();
99 39335 int base = 0;
100
2/2
✓ Branch 0 taken 39334 times.
✓ Branch 1 taken 1 times.
39335 if (c[0] == '#') {
101 39334 digits++;
102 39334 base = 16;
103 }
104
4/8
✓ Branch 0 taken 39335 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 39335 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 39335 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 39335 times.
39335 if (*digits == '\0' || *digits == '-' || *digits == '+' || std::isspace((unsigned char)*digits)) {
105 ✗ return false;
106 }
107 39335 char* end = nullptr;
108 39335 errno = 0;
109 39335 unsigned long long v = strtoull(digits, &end, base);
110
4/6
✓ Branch 0 taken 39335 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 39334 times.
✓ Branch 3 taken 1 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 39334 times.
39335 if (errno != 0 || *end != '\0' || v > 0xFFFFFFFFull) {
111 1 return false;
112 }
113 39334 color = (uint32_t)v;
114 39334 return true;
115
10/24
✓ Branch 0 taken 103 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 103 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 103 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 103 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 103 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 103 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 103 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 103 times.
✗ Branch 15 not taken.
✓ Branch 16 taken 103 times.
✗ Branch 17 not taken.
✓ Branch 18 taken 103 times.
✗ Branch 19 not taken.
✗ Branch 20 not taken.
✗ Branch 21 not taken.
✗ Branch 22 not taken.
✗ Branch 23 not taken.
39439 }
116
117 39303 uint32_t PixelOverlayManager::mapColor(const std::string& c) {
118 39303 uint32_t color = 0;
119
2/4
✓ Branch 0 taken 39303 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 39303 times.
39303 if (!parseColor(c, color)) {
120 ✗ LogWarn(VB_CHANNELOUT, "Unrecognized color \"%s\", using black\n", c.c_str());
121 ✗ return 0x000000;
122 }
123 39303 return color;
124 }
125 2861 void PixelOverlayManager::PixelOverlayModelHolder::toJson(Json::Value& v) const {
126
1/2
✓ Branch 0 taken 2861 times.
✗ Branch 1 not taken.
2861 if (model) {
127 2861 model->toJson(v);
128 }
129 2861 }
130 // Models removed from the map whose memory is still alive because something
131 // holds a reference (getModelShared()). A retired model must not be put back
132 // on the effect schedule or the active list: both hold raw pointers that
133 // outlive the last reference. Entries leave the set when the model is freed,
134 // so a new model at the same address is not mistaken for it. Leaf lock.
135 static std::mutex s_retiredLock;
136 static std::set<const PixelOverlayModel*> s_retired;
137 190720 static bool isRetired(const PixelOverlayModel* m) {
138
1/2
✓ Branch 0 taken 190720 times.
✗ Branch 1 not taken.
190720 std::lock_guard<std::mutex> l(s_retiredLock);
139
1/2
✓ Branch 0 taken 190720 times.
✗ Branch 1 not taken.
381440 return s_retired.count(m) != 0;
140 190720 }
141 1428 static std::shared_ptr<PixelOverlayModel> makeModelOwner(PixelOverlayModel* m) {
142 527 return std::shared_ptr<PixelOverlayModel>(m, [](PixelOverlayModel* p) {
143 {
144
1/2
✓ Branch 0 taken 527 times.
✗ Branch 1 not taken.
527 std::lock_guard<std::mutex> l(s_retiredLock);
145
1/2
✓ Branch 0 taken 527 times.
✗ Branch 1 not taken.
527 s_retired.erase(p);
146 527 }
147
1/2
✓ Branch 0 taken 527 times.
✗ Branch 1 not taken.
527 delete p;
148 1955 });
149 }
150
151 2346 PixelOverlayManager::PixelOverlayManager() :
152 2346 numActive(0) {
153 2346 }
154 458 PixelOverlayManager::~PixelOverlayManager() {
155
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 458 times.
458 if (updateThread != nullptr) {
156 ✗ std::unique_lock<std::mutex> l(threadLock);
157 ✗ threadKeepRunning = false;
158 ✗ l.unlock();
159 ✗ threadCV.notify_all();
160 ✗ updateThread->join();
161 ✗ delete updateThread;
162 ✗ updateThread = nullptr;
163 ✗ }
164
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 458 times.
458 for (auto& a : models) {
165 ✗ a.second.model = nullptr;
166 ✗ a.second.owner.reset();
167 }
168 458 models.clear();
169 458 modelNames.clear();
170 458 }
171 1887 void PixelOverlayManager::Initialize() {
172 1887 loadModelMap();
173 1887 loadFonts();
174 1887 RegisterCommands();
175 1887 }
176
177 1435 void PixelOverlayManager::addModel(Json::Value config) {
178 1435 PixelOverlayModel* pmodel = nullptr;
179
180 // The definition comes from an unchecked file; isMember() and asString()
181 // throw on the wrong JSON types, and nothing above here catches.
182
8/16
✓ Branch 0 taken 1435 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1435 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1435 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1435 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1435 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1435 times.
✗ Branch 11 not taken.
✗ Branch 12 not taken.
✓ Branch 13 taken 1435 times.
✗ Branch 14 not taken.
✓ Branch 15 taken 1435 times.
1435 if (!config.isObject() || (config.isMember("Type") && !config["Type"].isString())) {
183 ✗ LogErr(VB_CHANNELOUT, "PixelOverlayManager::addModel() - skipping a model definition that is not an object with a text Type\n");
184 ✗ return;
185 }
186
2/4
✓ Branch 0 taken 1435 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1435 times.
1435 if (!config.isMember("Type"))
187 ✗ config["Type"] = "Channel";
188
189
2/4
✓ Branch 0 taken 1435 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1435 times.
✗ Branch 3 not taken.
1435 std::string type = config["Type"].asString();
190
191
3/4
✓ Branch 0 taken 1435 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1351 times.
✓ Branch 3 taken 84 times.
1435 if (type == "Channel") {
192
7/14
✓ Branch 0 taken 1351 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1351 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1351 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1351 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1351 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1351 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 1351 times.
✗ Branch 13 not taken.
1351 if (config.isMember("StartChannel") && config.isMember("ChannelCount") && config.isMember("StringCount")) {
193
3/8
✓ Branch 0 taken 1351 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1351 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 1351 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
1351 pmodel = new PixelOverlayModel(config);
194 } else {
195 ✗ std::string name;
196 ✗ if (config["Name"].isString()) {
197 ✗ name = config["Name"].asString();
198 }
199 ✗ LogWarn(VB_CHANNELOUT, "PixelOverlayManager::loadModelMap() - invalid configuration for model %s\n", name.c_str());
200 ✗ }
201
3/4
✓ Branch 0 taken 84 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 60 times.
✓ Branch 3 taken 24 times.
84 } else if (type == "FB") {
202
3/8
✓ Branch 0 taken 60 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 60 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 60 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
60 pmodel = new PixelOverlayModelFB(config);
203
2/4
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 24 times.
✗ Branch 3 not taken.
24 } else if (type == "Sub") {
204
3/8
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 24 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 24 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
24 pmodel = new PixelOverlayModelSub(config);
205 }
206
207
5/6
✓ Branch 0 taken 1435 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✓ Branch 3 taken 1428 times.
✓ Branch 4 taken 7 times.
✓ Branch 5 taken 1428 times.
1435 if (pmodel && !pmodel->isValid()) {
208 // Its definition was rejected, or its channel data could not be
209 // allocated, so every accessor on it would index a null pointer. Drop
210 // it; the constructor has already logged why.
211
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 LogErr(VB_CHANNELOUT, "PixelOverlayManager::addModel() - model %s was not created\n",
212 pmodel->getName().c_str());
213
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 delete pmodel;
214 7 pmodel = nullptr;
215 }
216
217
1/2
✓ Branch 0 taken 1435 times.
✗ Branch 1 not taken.
1435 std::unique_lock<std::recursive_mutex> lock(modelsLock);
218
8/12
✓ Branch 0 taken 1428 times.
✓ Branch 1 taken 7 times.
✓ Branch 2 taken 1428 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 246 times.
✓ Branch 5 taken 1182 times.
✓ Branch 6 taken 246 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 246 times.
✗ Branch 10 not taken.
✓ Branch 11 taken 1435 times.
1435 if (pmodel && models.count(pmodel->getName()) && models[pmodel->getName()].model) {
219 // Replacing the live model in place used to leak it, and freeing it
220 // here instead would pull it out from under the active list and the
221 // effect thread. The first definition of a name wins; anything that
222 // genuinely replaces a model removes the old one first.
223 ✗ LogWarn(VB_CHANNELOUT, "A pixel overlay model named \"%s\" already exists; ignoring the second definition\n",
224 pmodel->getName().c_str());
225 ✗ delete pmodel;
226 ✗ pmodel = nullptr;
227 }
228
2/2
✓ Branch 0 taken 1428 times.
✓ Branch 1 taken 7 times.
1435 if (pmodel) {
229
2/4
✓ Branch 0 taken 1428 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1428 times.
✗ Branch 3 not taken.
1428 models[pmodel->getName()].owner = makeModelOwner(pmodel);
230
1/2
✓ Branch 0 taken 1428 times.
✗ Branch 1 not taken.
1428 models[pmodel->getName()].model = pmodel;
231
1/2
✓ Branch 0 taken 1428 times.
✗ Branch 1 not taken.
1428 modelNames.push_back(pmodel->getName());
232 1428 ++modelsGeneration;
233
3/4
✓ Branch 0 taken 1428 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✓ Branch 3 taken 1428 times.
1442 for (auto& l : models[pmodel->getName()].listeners) {
234
1/2
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
14 l.second(pmodel);
235 }
236 }
237 1435 }
238
239 1887 void PixelOverlayManager::loadModelMap() {
240
1/2
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
1887 LogDebug(VB_CHANNELOUT, "PixelOverlayManager::loadModelMap()\n");
241
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1887 times.
1887 if (updateThread != nullptr) {
242 ✗ std::unique_lock<std::mutex> l(threadLock);
243 ✗ threadKeepRunning = false;
244 ✗ updates.clear();
245 ✗ l.unlock();
246 ✗ threadCV.notify_all();
247 ✗ updateThread->join();
248 ✗ delete updateThread;
249 ✗ updateThread = nullptr;
250 ✗ }
251
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1887 times.
1887 for (auto& a : models) {
252 ✗ for (auto& l : a.second.listeners) {
253 ✗ l.second(a.second.model);
254 }
255 ✗ a.second.model = nullptr;
256 ✗ a.second.owner.reset();
257 }
258 1887 models.clear();
259 1887 modelNames.clear();
260 1887 ++modelsGeneration;
261 // Drop the lazy submodel index so a re-uploaded xlights-submodels.json is
262 // picked up on the next reference.
263 1887 subModelConfigs.clear();
264 1887 subModelIndexLoaded = false;
265
1/2
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
1887 ConvertCMMFileToJSON();
266
267
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.
1887 std::string filename(FPP_DIR_CONFIG("/model-overlays.json"));
268
3/4
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 376 times.
✓ Branch 3 taken 1511 times.
1887 if (FileExists(filename)) {
269
1/2
✓ Branch 0 taken 376 times.
✗ Branch 1 not taken.
376 Json::Value root;
270
1/2
✓ Branch 0 taken 376 times.
✗ Branch 1 not taken.
376 bool result = LoadJsonFromFile(filename, root, JsonRoot::Object);
271
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 375 times.
376 if (!result) {
272
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogErr(VB_CHANNELOUT, "Error parsing model-overlays.json.");
273 1 return;
274 }
275
276
4/6
✓ Branch 0 taken 375 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 375 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 372 times.
✓ Branch 5 taken 3 times.
375 if (root["autoCreate"].isBool()) {
277
2/4
✓ Branch 0 taken 372 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 372 times.
✗ Branch 3 not taken.
372 autoCreate = root["autoCreate"].asBool();
278 }
279 // Runs at startup with nothing above it to catch: a throw here was a
280 // crash on every boot until someone fixed the file by hand.
281
1/2
✓ Branch 0 taken 375 times.
✗ Branch 1 not taken.
375 const Json::Value& models = root["models"];
282
5/10
✓ Branch 0 taken 375 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 375 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 375 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 375 times.
✗ Branch 8 not taken.
✓ Branch 9 taken 375 times.
375 if (!models.isNull() && !models.isArray()) {
283 ✗ LogErr(VB_CHANNELOUT, "model-overlays.json: \"models\" is not an array - no models loaded from it\n");
284 ✗ return;
285 }
286
3/4
✓ Branch 0 taken 1277 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 902 times.
✓ Branch 3 taken 375 times.
1277 for (Json::ArrayIndex c = 0; c < models.size(); c++) {
287 try {
288
3/6
✓ Branch 0 taken 902 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 902 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 902 times.
✗ Branch 5 not taken.
902 addModel(models[c]);
289 ✗ } catch (const std::exception& e) {
290 ✗ LogErr(VB_CHANNELOUT, "model-overlays.json: skipping model %d: %s\n", (int)c, e.what());
291 ✗ }
292 }
293
2/2
✓ Branch 0 taken 375 times.
✓ Branch 1 taken 1 times.
376 }
294
2/2
✓ Branch 0 taken 1886 times.
✓ Branch 1 taken 1 times.
1887 }
295 1887 void PixelOverlayManager::ConvertCMMFileToJSON() {
296 FILE* fp;
297 char buf[64];
298 char* s;
299 int startChannel;
300 int channelCount;
301
2/4
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1887 times.
✗ Branch 3 not taken.
1887 std::string filename(FPP_DIR_MEDIA("/channelmemorymaps"));
302
303
2/4
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1887 times.
✗ Branch 3 not taken.
1887 if (!FileExists(filename)) {
304 1887 return;
305 }
306 ✗ Json::Value result;
307
308 ✗ LogDebug(VB_CHANNELOUT, "Loading Channel Memory Map data.\n");
309 ✗ fp = fopen(filename.c_str(), "r");
310 ✗ if (fp == NULL) {
311 ✗ LogErr(VB_CHANNELOUT, "Could not open Channel Memory Map config file %s\n", filename.c_str());
312 ✗ return;
313 }
314
315 ✗ while (fgets(buf, 64, fp) != NULL) {
316 ✗ if (buf[0] == '#') {
317 // Allow # comments for testing
318 ✗ continue;
319 }
320 ✗ Json::Value model;
321 ✗ char* saveptr = nullptr; // for thread-safe strtok_r below
322
323 // Name
324 ✗ s = strtok_r(buf, ",", &saveptr);
325 ✗ if (s) {
326 ✗ std::string modelName = s;
327 ✗ model["Name"] = modelName;
328 ✗ } else {
329 ✗ continue;
330 }
331
332 // Start Channel
333 ✗ s = strtok_r(NULL, ",", &saveptr);
334 ✗ if (!s)
335 ✗ continue;
336 ✗ startChannel = strtol(s, NULL, 10);
337 ✗ if (startChannel <= 0) {
338 ✗ continue;
339 }
340 ✗ model["StartChannel"] = startChannel;
341
342 // Channel Count
343 ✗ s = strtok_r(NULL, ",", &saveptr);
344 ✗ if (!s)
345 ✗ continue;
346 ✗ channelCount = strtol(s, NULL, 10);
347 ✗ if (channelCount <= 0) {
348 ✗ continue;
349 }
350 ✗ model["ChannelCount"] = channelCount;
351
352 // Orientation
353 ✗ s = strtok_r(NULL, ",", &saveptr);
354 ✗ if (!s)
355 ✗ continue;
356 ✗ model["Orientation"] = !strcmp(s, "vertical") ? "vertical" : "horizontal";
357
358 // Start Corner
359 ✗ s = strtok_r(NULL, ",", &saveptr);
360 ✗ if (!s)
361 ✗ continue;
362 ✗ model["StartCorner"] = s;
363
364 // String Count
365 ✗ s = strtok_r(NULL, ",", &saveptr);
366 ✗ if (!s)
367 ✗ continue;
368 ✗ model["StringCount"] = (int)strtol(s, NULL, 10);
369
370 // Strands Per String
371 ✗ s = strtok_r(NULL, ",", &saveptr);
372 ✗ if (!s)
373 ✗ continue;
374 ✗ model["StrandsPerString"] = (int)strtol(s, NULL, 10);
375 ✗ result["models"].append(model);
376 ✗ }
377 ✗ fclose(fp);
378 ✗ remove(filename.c_str());
379
380 ✗ filename = FPP_DIR_CONFIG("/model-overlays.json");
381 ✗ SaveJsonToFile(result, filename);
382
1/4
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1887 times.
1887 }
383
384 // Holds output running for a moment after the last overlay goes away, so the
385 // frame that clears its channels is actually sent. Kept apart from numActive,
386 // which is recomputed from the active lists and would erase a bump made to it.
387 static std::atomic_int s_holdOutput{ 0 };
388 24 static void holdOutputBriefly() {
389 24 s_holdOutput++;
390
1/2
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
24 std::this_thread::sleep_for(std::chrono::milliseconds(125));
391 24 s_holdOutput--;
392 24 }
393
394 423690 bool PixelOverlayManager::hasActiveOverlays() {
395 // Asked a few times a frame and every main-loop pass. A hint only:
396 // doOverlays() takes activeModelsLock for the models themselves.
397
4/4
✓ Branch 0 taken 372669 times.
✓ Branch 1 taken 51021 times.
✓ Branch 2 taken 75 times.
✓ Branch 3 taken 372594 times.
1220049 return numActive.load(std::memory_order_relaxed) > 0 || s_holdOutput.load(std::memory_order_relaxed) > 0;
398 }
399 // Caller holds activeModelsLock.
400 239250 void PixelOverlayManager::updateActiveCountLocked() {
401 239250 numActive = (int)(activeModels.size() + activeRanges.size());
402 239250 }
403 ✗ void PixelOverlayManager::resetChildParent(const std::string& name) {
404 ✗ std::unique_lock<std::recursive_mutex> lock(modelsLock);
405 ✗ PixelOverlayModel* model = getModelLocked(name);
406 ✗ if (model) {
407 ✗ PixelOverlayModelSub* subModel = dynamic_cast<PixelOverlayModelSub*>(model);
408 ✗ if (subModel) {
409 ✗ subModel->resetParent();
410 }
411 }
412 ✗ }
413
414 // Brings m's membership of activeModels in line with m->needsOutput(). The
415 // old/new states are deliberately ignored: a model is active for two reasons
416 // (its own state, and enabled submodels within it) that change on different
417 // threads, and acting on each transition separately pushed a parent twice
418 // (children first, then its own enable) and later dropped it while children
419 // still needed it - and the counter drifted with every duplicate, so output
420 // never idled again. Recomputing under the lock converges whatever order the
421 // calls arrive in.
422 236038 void PixelOverlayManager::modelStateChanged(PixelOverlayModel* m, const PixelOverlayState& old, const PixelOverlayState& state) {
423
1/2
✓ Branch 0 taken 236038 times.
✗ Branch 1 not taken.
236038 std::unique_lock<std::recursive_mutex> lock(activeModelsLock);
424 // A removed model still referenced elsewhere can have its state set, but
425 // must never reach the output thread again.
426
5/8
✓ Branch 0 taken 236038 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 118692 times.
✓ Branch 3 taken 117346 times.
✓ Branch 4 taken 118692 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 118692 times.
✗ Branch 7 not taken.
236038 bool want = m->needsOutput() && !isRetired(m);
427
1/2
✓ Branch 0 taken 236038 times.
✗ Branch 1 not taken.
236038 bool present = std::find(activeModels.begin(), activeModels.end(), m) != activeModels.end();
428
4/4
✓ Branch 0 taken 118692 times.
✓ Branch 1 taken 117346 times.
✓ Branch 2 taken 117566 times.
✓ Branch 3 taken 1126 times.
236038 if (want && !present) {
429
1/2
✓ Branch 0 taken 117566 times.
✗ Branch 1 not taken.
117566 activeModels.push_back(m);
430
4/4
✓ Branch 0 taken 117346 times.
✓ Branch 1 taken 1126 times.
✓ Branch 2 taken 117333 times.
✓ Branch 3 taken 13 times.
118472 } else if (!want && present) {
431 117333 activeModels.remove(m);
432 }
433
1/2
✓ Branch 0 taken 236038 times.
✗ Branch 1 not taken.
236038 updateActiveCountLocked();
434
1/2
✓ Branch 0 taken 236038 times.
✗ Branch 1 not taken.
236038 lock.unlock();
435
2/2
✓ Branch 0 taken 222040 times.
✓ Branch 1 taken 13998 times.
236038 if (numActive > 0) {
436
1/2
✓ Branch 0 taken 222040 times.
✗ Branch 1 not taken.
222040 StartChannelOutputThread();
437 }
438 236038 }
439
440 // Runs on the channel output thread, once per frame.
441 //
442 // LOCK ORDER: nothing reached from here may take modelsLock. This function
443 // holds activeModelsLock for the whole pass, while every HTTP handler takes
444 // modelsLock first and then activeModelsLock (via setState() ->
445 // modelStateChanged()). Acquiring them in this order deadlocks the two threads
446 // against each other, and the symptom is nasty: fppd keeps running and
447 // /api/fppd/status keeps answering 200, but every /api/overlays and /api/models
448 // request hangs forever and overlays stop updating, so a health check will not
449 // notice. PixelOverlayModelSub::doOverlay() used to resolve its parent through
450 // getModel() from here; see the note on PixelOverlayModelSub::foundParent().
451 //
452 // removeAutoOverlayModel() and getActiveOverlayEffects() both drop modelsLock
453 // (or snapshot) before touching activeModelsLock for the same reason.
454 25608 void PixelOverlayManager::doOverlays(uint8_t* channels) {
455
2/2
✓ Branch 0 taken 38 times.
✓ Branch 1 taken 25570 times.
25608 if (numActive == 0) {
456 38 return;
457 }
458
1/2
✓ Branch 0 taken 25570 times.
✗ Branch 1 not taken.
25570 std::unique_lock<std::recursive_mutex> lock(activeModelsLock);
459 // First, flush any buffers
460
2/2
✓ Branch 0 taken 27604 times.
✓ Branch 1 taken 25570 times.
53174 for (auto m : activeModels) {
461
3/4
✓ Branch 0 taken 27604 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 27602 times.
27604 if (m->overlayBufferIsDirty()) {
462
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 m->flushOverlayBuffer();
463 }
464 }
465 // Second, do any sub-models
466
2/2
✓ Branch 0 taken 27604 times.
✓ Branch 1 taken 25570 times.
53174 for (auto m : activeModels) {
467
3/4
✓ Branch 0 taken 27604 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4832 times.
✓ Branch 3 taken 22772 times.
27604 if (m->getType() == "Sub") {
468
1/2
✓ Branch 0 taken 4832 times.
✗ Branch 1 not taken.
4832 m->doOverlay(channels);
469 }
470 }
471 // Then do any non-subs
472
2/2
✓ Branch 0 taken 27604 times.
✓ Branch 1 taken 25570 times.
53174 for (auto m : activeModels) {
473
3/4
✓ Branch 0 taken 27604 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 22772 times.
✓ Branch 3 taken 4832 times.
27604 if (m->getType() != "Sub") {
474
1/2
✓ Branch 0 taken 22772 times.
✗ Branch 1 not taken.
22772 m->doOverlay(channels);
475 }
476 }
477
478
2/2
✓ Branch 0 taken 7971 times.
✓ Branch 1 taken 25570 times.
33541 for (auto& m : activeRanges) {
479
2/2
✓ Branch 0 taken 155889041 times.
✓ Branch 1 taken 7971 times.
155897012 for (int s = m.start; s <= m.end; s++) {
480 155889041 channels[s] = m.value;
481 }
482 }
483
1/2
✓ Branch 0 taken 25570 times.
✗ Branch 1 not taken.
25570 lock.unlock();
484
1/2
✓ Branch 0 taken 25570 times.
✗ Branch 1 not taken.
25570 std::unique_lock<std::mutex> l(threadLock);
485
2/2
✓ Branch 0 taken 373 times.
✓ Branch 1 taken 25570 times.
25943 while (!afterOverlayModels.empty()) {
486 373 PixelOverlayModel* m = afterOverlayModels.front();
487 373 afterOverlayModels.pop_front();
488 // Pop and mark in flight under one hold of threadLock: dropping the
489 // lock with the model owned by neither the list nor the in-flight set
490 // is the window removeAutoOverlayModel() used to delete it in.
491
1/2
✓ Branch 0 taken 373 times.
✗ Branch 1 not taken.
373 runEffectUpdateLocked(m, l, nullptr);
492 }
493 25570 }
494
495 639 PixelOverlayModel* PixelOverlayManager::getModel(const std::string& name) {
496
1/2
✓ Branch 0 taken 639 times.
✗ Branch 1 not taken.
639 std::unique_lock<std::recursive_mutex> lock(modelsLock);
497
1/2
✓ Branch 0 taken 639 times.
✗ Branch 1 not taken.
1278 return getModelLocked(name);
498 639 }
499 390514 std::shared_ptr<PixelOverlayModel> PixelOverlayManager::getModelShared(const std::string& name) {
500
1/2
✓ Branch 0 taken 390514 times.
✗ Branch 1 not taken.
390514 std::unique_lock<std::recursive_mutex> lock(modelsLock);
501
1/2
✓ Branch 0 taken 390514 times.
✗ Branch 1 not taken.
390514 PixelOverlayModel* m = getModelLocked(name); // materializes a lazy submodel
502
2/2
✓ Branch 0 taken 206 times.
✓ Branch 1 taken 390308 times.
390514 if (!m) {
503 206 return nullptr;
504 }
505
1/2
✓ Branch 0 taken 390308 times.
✗ Branch 1 not taken.
390308 return models[m->getName()].owner;
506 390514 }
507 ✗ bool PixelOverlayManager::tryGetModel(const std::string& name, PixelOverlayModel*& model) {
508 ✗ std::unique_lock<std::recursive_mutex> lock(modelsLock, std::try_to_lock);
509 ✗ if (!lock.owns_lock()) {
510 ✗ return false;
511 }
512 ✗ model = getModelLocked(name);
513 ✗ return true;
514 ✗ }
515 803767 PixelOverlayModel* PixelOverlayManager::getModelLocked(const std::string& name) {
516
1/2
✓ Branch 0 taken 803767 times.
✗ Branch 1 not taken.
803767 auto a = models.find(name);
517
2/2
✓ Branch 0 taken 800615 times.
✓ Branch 1 taken 3152 times.
803767 if (a != models.end()) {
518 800615 return a->second.model;
519 }
520 // Not a loaded model -- it may be a lazily-loaded xLights submodel.
521
1/2
✓ Branch 0 taken 3152 times.
✗ Branch 1 not taken.
3152 return materializeSubModelLocked(name);
522 }
523
524 // Parse config/xlights-submodels.json once into a lightweight name -> compact
525 // config index. Never called at boot; only on the first submodel reference or
526 // discovery request, so systems that never use submodels pay nothing.
527 243 void PixelOverlayManager::loadSubModelIndex() {
528
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 241 times.
243 if (subModelIndexLoaded) {
529 236 return;
530 }
531 241 subModelIndexLoaded = true; // set regardless so a missing file isn't re-stat'd repeatedly
532
533
3/6
✓ Branch 0 taken 241 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 241 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 241 times.
✗ Branch 5 not taken.
241 std::string filename(FPP_DIR_CONFIG("/xlights-submodels.json"));
534
3/4
✓ Branch 0 taken 241 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 234 times.
✓ Branch 3 taken 7 times.
241 if (!FileExists(filename)) {
535 234 return;
536 }
537
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 Json::Value root;
538
2/4
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 7 times.
7 if (!LoadJsonFromFile(filename, root, JsonRoot::Object)) {
539 ✗ LogErr(VB_CHANNELOUT, "Error parsing xlights-submodels.json.\n");
540 ✗ return;
541 }
542 // Each entry is checked before it is read: the index is marked loaded
543 // above, so a throw here would leave every submodel missing until fppd
544 // restarts. An entry without a string Name is skipped.
545 10 auto addEntries = [this](const Json::Value& list, const char* file) {
546
5/6
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✓ Branch 3 taken 10 times.
✓ Branch 4 taken 14 times.
✓ Branch 5 taken 10 times.
24 for (Json::ArrayIndex c = 0; list.isArray() && c < list.size(); c++) {
547
11/16
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 9 times.
✓ Branch 5 taken 5 times.
✓ Branch 6 taken 9 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 9 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 9 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 2 times.
✓ Branch 13 taken 7 times.
✓ Branch 14 taken 7 times.
✓ Branch 15 taken 7 times.
14 if (!list[c].isObject() || !list[c]["Name"].isString()) {
548
4/8
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 7 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 7 times.
✗ Branch 7 not taken.
21 WarningHolder::InvalidConfigEntry(file, "entry " + std::to_string(c + 1) + " has no Name");
549 7 continue;
550 }
551
3/6
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 7 times.
✗ Branch 5 not taken.
7 std::string nm = list[c]["Name"].asString();
552
3/6
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 7 times.
✗ Branch 5 not taken.
28 replaceAll(nm, "/", "_"); // match PixelOverlayModel name sanitization
553
4/8
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 7 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 7 times.
✗ Branch 7 not taken.
7 subModelConfigs[nm] = SaveJsonToString(list[c], "");
554 7 }
555 10 };
556
2/4
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
7 addEntries(root["submodels"], "xlights-submodels.json");
557
558 // Model groups share the same lazy index and the same "Sub" materialization
559 // path (they are channelgrid Subs spanning multiple members).
560
3/6
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 7 times.
✗ Branch 5 not taken.
7 std::string groupFile(FPP_DIR_CONFIG("/xlights-modelgroups.json"));
561
3/4
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✓ Branch 3 taken 4 times.
7 if (FileExists(groupFile)) {
562
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 Json::Value groot;
563
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 if (LoadJsonFromFile(groupFile, groot, JsonRoot::Object)) {
564
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 addEntries(groot["modelgroups"], "xlights-modelgroups.json");
565 } else {
566 ✗ LogErr(VB_CHANNELOUT, "Error parsing xlights-modelgroups.json.\n");
567 }
568 3 }
569
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 LogDebug(VB_CHANNELOUT, "Loaded xLights submodel/group index: %d entries\n", (int)subModelConfigs.size());
570
3/4
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✓ Branch 3 taken 234 times.
241 }
571
572 // Materialize a single submodel (build the live PixelOverlayModelSub) the first
573 // time it is referenced. Returns nullptr if the name is not a known submodel.
574 3152 PixelOverlayModel* PixelOverlayManager::materializeSubModelLocked(const std::string& name) {
575
2/2
✓ Branch 0 taken 239 times.
✓ Branch 1 taken 2913 times.
3152 if (!subModelIndexLoaded) {
576
1/2
✓ Branch 0 taken 239 times.
✗ Branch 1 not taken.
239 loadSubModelIndex();
577 }
578
1/2
✓ Branch 0 taken 3152 times.
✗ Branch 1 not taken.
3152 auto it = subModelConfigs.find(name);
579
2/2
✓ Branch 0 taken 3147 times.
✓ Branch 1 taken 5 times.
3152 if (it == subModelConfigs.end()) {
580 3147 return nullptr;
581 }
582
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 Json::Value config;
583
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 5 times.
5 if (!LoadJsonFromString(it->second, config)) {
584 ✗ LogErr(VB_CHANNELOUT, "Error parsing cached submodel config for %s\n", name.c_str());
585 ✗ return nullptr;
586 }
587 try {
588
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 addModel(config);
589 ✗ } catch (const std::exception& e) {
590 // Reached from effect threads and MQTT commands, where nothing
591 // catches. addModel() is not expected to throw any more; this keeps
592 // a lazy lookup from ever being the thing that does.
593 ✗ LogErr(VB_CHANNELOUT, "Could not create submodel %s: %s\n", name.c_str(), e.what());
594 ✗ return nullptr;
595 ✗ }
596
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 auto a = models.find(name);
597
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 return (a != models.end()) ? a->second.model : nullptr;
598 5 }
599 87 void PixelOverlayManager::addModelListener(const std::string& name, const std::string& id, std::function<void(PixelOverlayModel*)> listener) {
600
1/2
✓ Branch 0 taken 87 times.
✗ Branch 1 not taken.
87 std::unique_lock<std::recursive_mutex> lock(modelsLock);
601
3/6
✓ Branch 0 taken 87 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 87 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 87 times.
✗ Branch 5 not taken.
87 models[name].listeners[id] = listener;
602 87 }
603 178 void PixelOverlayManager::removeModelListener(const std::string& name, const std::string& id) {
604
1/2
✓ Branch 0 taken 178 times.
✗ Branch 1 not taken.
178 std::unique_lock<std::recursive_mutex> lock(modelsLock);
605
1/2
✓ Branch 0 taken 178 times.
✗ Branch 1 not taken.
178 auto a = models.find(name);
606
2/2
✓ Branch 0 taken 85 times.
✓ Branch 1 taken 93 times.
178 if (a != models.end()) {
607
1/2
✓ Branch 0 taken 85 times.
✗ Branch 1 not taken.
85 a->second.listeners.erase(id);
608 }
609 178 }
610
611 24127 static bool isTTF(const std::string& mainStr) {
612
3/4
✓ Branch 0 taken 24127 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17887 times.
✓ Branch 3 taken 6240 times.
24127 return (mainStr.size() >= 4) && (mainStr.compare(mainStr.size() - 4, 4, ".ttf") == 0);
613 }
614 6240 static bool isPFB(const std::string& mainStr) {
615
3/4
✓ Branch 0 taken 6240 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5600 times.
✓ Branch 3 taken 640 times.
6240 return (mainStr.size() >= 4) && (mainStr.compare(mainStr.size() - 4, 4, ".pfb") == 0);
616 }
617 // The newest thing asked of a model wins. A fill or clear redraws the whole
618 // model, so it ends whatever effect is running there -- the effect would
619 // otherwise overwrite it on its next update -- as the bulk pixel PUT already
620 // did. A state change or a single pixel doesn't redraw the model, so a
621 // running effect carries on under it; but a pending Stop Effects, which blanks
622 // and disables the model a frame later, is cancelled by either: a fill, a
623 // state or a pixel sent after the stop is the newer instruction.
624 256601 static void retireEffectFor(PixelOverlayModel* m, bool onlyAStop) {
625
1/2
✓ Branch 0 taken 256601 times.
✗ Branch 1 not taken.
256601 std::unique_lock<std::recursive_mutex> l(m->getRunningEffectMutex());
626 256601 RunningEffect* e = m->getRunningEffect();
627
10/12
✓ Branch 0 taken 34568 times.
✓ Branch 1 taken 222033 times.
✓ Branch 2 taken 398 times.
✓ Branch 3 taken 34170 times.
✓ Branch 4 taken 398 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 398 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 158 times.
✓ Branch 9 taken 240 times.
✓ Branch 10 taken 34328 times.
✓ Branch 11 taken 222273 times.
256601 if (e && (!onlyAStop || e->name() == "Stop Effects")) {
628
1/2
✓ Branch 0 taken 34328 times.
✗ Branch 1 not taken.
34328 m->setRunningEffect(nullptr, 25); // one no-op tick, then dropped
629 }
630 256601 }
631
632 7760 static void findFonts(const std::string& dir, std::map<std::string, std::string>& fonts) {
633 DIR* dp;
634 struct dirent* ep;
635
636 7760 dp = opendir(dir.c_str());
637
2/2
✓ Branch 0 taken 6033 times.
✓ Branch 1 taken 1727 times.
7760 if (dp != NULL) {
638
2/2
✓ Branch 0 taken 38292 times.
✓ Branch 1 taken 6033 times.
44325 while ((ep = readdir(dp))) {
639 // ".", ".." and hidden entries all start with a dot.
640 //
641 // This used to be done by requiring a dot ANYWHERE in the name and
642 // rejecting one at position 0, which quietly made the recursion
643 // below dead code: every font on the system lives in a directory
644 // named for its family -- truetype/liberation, truetype/noto,
645 // truetype/lato, truetype/dejavu -- and not one of those names
646 // contains a dot, so each was skipped before it could be recursed
647 // into. The only fonts that ever reached the map were the .pfb
648 // files sitting flat in /usr/share/fonts/X11/Type1, which is why
649 // the overlay Font list offered 35 URW faces and none of the
650 // several hundred TrueType fonts actually installed.
651
2/2
✓ Branch 0 taken 12066 times.
✓ Branch 1 taken 26226 times.
38292 if (ep->d_name[0] == '.') {
652 12066 continue;
653 }
654
655 struct stat statbuf;
656
1/2
✓ Branch 0 taken 26226 times.
✗ Branch 1 not taken.
26226 std::string dname = dir;
657
1/2
✓ Branch 0 taken 26226 times.
✗ Branch 1 not taken.
26226 dname += ep->d_name;
658
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 26226 times.
26226 if (lstat(dname.c_str(), &statbuf) != 0) {
659 // Raced with a delete, or we cannot stat it; either way there is
660 // nothing to classify. Checked because the fields were
661 // previously read whether or not the call succeeded.
662 ✗ continue;
663 }
664
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 26226 times.
26226 if (S_ISLNK(statbuf.st_mode)) {
665 // symlink, skip -- fontconfig trees are full of them, and
666 // following them is how a scan ends up in a loop
667 ✗ continue;
668
2/2
✓ Branch 0 taken 2099 times.
✓ Branch 1 taken 24127 times.
26226 } else if (S_ISDIR(statbuf.st_mode)) {
669
2/4
✓ Branch 0 taken 2099 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2099 times.
✗ Branch 3 not taken.
2099 findFonts(dname + "/", fonts);
670
16/30
✓ Branch 0 taken 24127 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 24127 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 6240 times.
✓ Branch 5 taken 17887 times.
✓ Branch 6 taken 6240 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 6240 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 5600 times.
✓ Branch 11 taken 640 times.
✓ Branch 12 taken 6240 times.
✓ Branch 13 taken 17887 times.
✓ Branch 14 taken 6240 times.
✓ Branch 15 taken 17887 times.
✓ Branch 16 taken 24127 times.
✗ Branch 17 not taken.
✓ Branch 18 taken 24127 times.
✗ Branch 19 not taken.
✓ Branch 20 taken 23487 times.
✓ Branch 21 taken 640 times.
✗ Branch 22 not taken.
✗ Branch 23 not taken.
✗ Branch 24 not taken.
✗ Branch 25 not taken.
✗ Branch 26 not taken.
✗ Branch 27 not taken.
✗ Branch 28 not taken.
✗ Branch 29 not taken.
84861 } else if (isTTF(ep->d_name) || isPFB(ep->d_name)) {
671
1/2
✓ Branch 0 taken 23487 times.
✗ Branch 1 not taken.
31431 std::string fname = ep->d_name;
672
1/2
✓ Branch 0 taken 23487 times.
✗ Branch 1 not taken.
23487 fname.resize(fname.size() - 4);
673
2/4
✓ Branch 0 taken 23487 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 23487 times.
✗ Branch 3 not taken.
23487 fonts[fname] = dname;
674 23487 }
675
1/2
✓ Branch 0 taken 26226 times.
✗ Branch 1 not taken.
26226 }
676 6033 closedir(dp);
677 }
678 7760 }
679
680 1887 void PixelOverlayManager::loadFonts() {
681
1/2
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
1887 if (!fontsLoaded) {
682 #ifndef PLATFORM_OSX
683
2/4
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1887 times.
✗ Branch 3 not taken.
3774 findFonts("/usr/share/fonts/truetype/", fonts);
684
2/4
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1887 times.
✗ Branch 3 not taken.
3774 findFonts("/usr/share/fonts/X11/Type1/", fonts);
685
2/4
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1887 times.
✗ Branch 3 not taken.
3614 findFonts("/usr/local/share/fonts/", fonts);
686 #else
687 findFonts("/System/Library/fonts/", fonts);
688 #endif
689 1887 fontsLoaded = true;
690 }
691 1887 }
692
693 // find(), not operator[]: every unknown name used to be inserted into
694 // `fonts`, which GET /api/overlays/fonts iterates from other HTTP threads, and
695 // stayed in the font list for good.
696 243 const std::string& PixelOverlayManager::mapFont(const std::string& f) {
697
3/4
✓ Branch 0 taken 19 times.
✓ Branch 1 taken 224 times.
✓ Branch 2 taken 19 times.
✗ Branch 3 not taken.
243 static std::string EMPTY_STR;
698
1/2
✓ Branch 0 taken 243 times.
✗ Branch 1 not taken.
243 auto it = fonts.find(f);
699
2/2
✓ Branch 0 taken 240 times.
✓ Branch 1 taken 3 times.
243 if (it != fonts.end()) {
700 240 return it->second;
701 }
702 3 return EMPTY_STR;
703 }
704
705 596271 std::vector<std::string> PixelOverlayManager::getModelNamesSnapshot() {
706
1/2
✓ Branch 0 taken 596271 times.
✗ Branch 1 not taken.
596271 std::unique_lock<std::recursive_mutex> lock(modelsLock);
707
1/2
✓ Branch 0 taken 596271 times.
✗ Branch 1 not taken.
1192542 return std::vector<std::string>(modelNames.begin(), modelNames.end());
708 596271 }
709
710 ✗ Json::Value PixelOverlayManager::getModelsAsJson() {
711 ✗ Json::Value ret;
712 ✗ std::unique_lock<std::recursive_mutex> lock(modelsLock);
713 ✗ for (auto& mn : modelNames) {
714 ✗ Json::Value model;
715 ✗ models[mn].toJson(model);
716 ✗ ret.append(model);
717 ✗ }
718 ✗ return ret;
719 ✗ }
720 ✗ HttpResponsePtr PixelOverlayManager::render_HEAD(const HttpRequestPtr& req) {
721 ✗ return render_GET(req);
722 }
723
724 // --------------------------------------------------------------------------
725 // OpenAPI docs for the /models and /overlays/* endpoints handled below.
726 // --------------------------------------------------------------------------
727
728 /**
729 * List all configured pixel-overlay models.
730 *
731 * @route GET /api/models
732 * @param boolean simple Return just the model names instead of full definitions.
733 * @param boolean all Prepend the "--All Models--" entry to the result.
734 * @response 200 Array of models (or model names when `simple=true`).
735 */
736
737 /**
738 * Get a single pixel-overlay model definition by name.
739 *
740 * @route GET /api/models/{model}
741 * @response 200 The model definition.
742 * @response 404 No model with that name exists.
743 */
744
745 /**
746 * List the fonts available for overlay text effects.
747 *
748 * @route GET /api/overlays/fonts
749 * @response 200 Array of font names.
750 */
751
752 /**
753 * Get the pixel-overlay manager settings.
754 *
755 * @route GET /api/overlays/settings
756 * @response 200 Object with the `autoCreate` flag.
757 */
758
759 /**
760 * List all overlay models with their current runtime state (active state,
761 * running effect, dimensions).
762 *
763 * @route GET /api/overlays/models
764 * @response 200 Array of models with runtime state.
765 */
766
767 /**
768 * Get a single overlay model with its current runtime state.
769 *
770 * @route GET /api/overlays/model/{model}
771 * @response 200 Model definition plus runtime state.
772 */
773
774 /**
775 * Get the current pixel buffer of an overlay model. Append /rle for
776 * run-length-encoded data, or /raw for the binary form.
777 *
778 * @route GET /api/overlays/model/{model}/data
779 * @response 200 Object with a `data` array (and `rle` flag).
780 */
781
782 /**
783 * Get the current pixel buffer of an overlay model as raw binary:
784 * width * height * bytesPerPixel bytes, row-major from the top-left, which is
785 * the exact layout `PUT /api/overlays/model/{model}/data` accepts. The model
786 * geometry is repeated in the `X-FPP-Model-Width`, `X-FPP-Model-Height` and
787 * `X-FPP-Model-BytesPerPixel` response headers.
788 *
789 * The JSON form spends 4-6 wire bytes per payload byte; this one spends one.
790 *
791 * @route GET /api/overlays/model/{model}/data/raw
792 * @response 200 Binary pixel data (application/octet-stream).
793 */
794
795 /**
796 * Get the per-pixel virtual-display coordinates for a model, for a lightweight
797 * layout preview. Sourced from config/virtualdisplaymap (the xLights-exported
798 * layout) and returned on demand so the UI never has to inline this data (which
799 * can be hundreds of thousands of points per model) for every model at once.
800 *
801 * @route GET /api/overlays/model/{model}/preview
802 * @response 200 Object with a `pixels` array of [x, y, z, node] entries in node
803 * order (node is the 1-based node number, derived from the pixel's channel).
804 */
805
806 /**
807 * Clear (blank) an overlay model's pixel buffer.
808 *
809 * @route GET /api/overlays/model/{model}/clear
810 * @response 200 Model cleared.
811 */
812
813 /**
814 * List the available overlay effects. Add ?full=true for full descriptions.
815 *
816 * @route GET /api/overlays/effects
817 * @param boolean full Return full effect descriptions instead of just names.
818 * @response 200 Array of effect names (or descriptions when `full=true`).
819 */
820
821 /**
822 * Get the description of a single overlay effect.
823 *
824 * @route GET /api/overlays/effects/{effect}
825 * @response 200 The effect description.
826 */
827
828 /**
829 * List the overlay effects that are currently running.
830 *
831 * @route GET /api/overlays/running
832 * @response 200 Active overlay effects.
833 */
834 // Normalize an overlay model name for matching (mirrors the UI's
835 // normalizeModelName()): lowercase, strip everything but [a-z0-9]. Overlay
836 // model names use underscores (e.g. "Arch_1_-_Bottom") while the
837 // virtualdisplaymap uses the original xLights names (e.g. "Arch 1 - Bottom"),
838 // so preview lookups must compare on the normalized form.
839 11 static std::string normalizeOverlayModelName(const std::string& name) {
840 11 std::string out;
841
1/2
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
11 out.reserve(name.size());
842
2/2
✓ Branch 0 taken 43 times.
✓ Branch 1 taken 11 times.
54 for (char c : name) {
843
4/4
✓ Branch 0 taken 36 times.
✓ Branch 1 taken 7 times.
✓ Branch 2 taken 13 times.
✓ Branch 3 taken 23 times.
43 if (c >= 'A' && c <= 'Z') {
844 13 c = c - 'A' + 'a';
845 }
846
7/8
✓ Branch 0 taken 33 times.
✓ Branch 1 taken 10 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 33 times.
✓ Branch 4 taken 5 times.
✓ Branch 5 taken 5 times.
✓ Branch 6 taken 2 times.
✓ Branch 7 taken 3 times.
43 if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) {
847
1/2
✓ Branch 0 taken 35 times.
✗ Branch 1 not taken.
35 out.push_back(c);
848 }
849 }
850 11 return out;
851 ✗ }
852
853 // Collect the names of usable image files under the media images directory,
854 // relative to it, sorted, for the Image overlay effect's file picker.
855 ✗ static void collectOverlayImageNames(Json::Value& result) {
856 ✗ std::string dir = FPP_DIR_IMAGE("");
857 ✗ std::vector<std::string> names;
858
859 try {
860 ✗ if (!std::filesystem::exists(dir)) {
861 ✗ result.resize(0);
862 ✗ return;
863 }
864 ✗ for (const auto& entry : std::filesystem::recursive_directory_iterator(dir)) {
865 ✗ if (!IsOverlayImageFile(entry.path().string())) {
866 ✗ continue;
867 }
868 ✗ names.push_back(std::filesystem::relative(entry.path(), dir).string());
869 ✗ }
870 ✗ } catch (const std::filesystem::filesystem_error& e) {
871 ✗ LogErr(VB_CHANNELOUT, "Error listing overlay images: %s\n", e.what());
872 ✗ }
873
874 ✗ std::sort(names.begin(), names.end());
875 ✗ for (auto& n : names) {
876 ✗ result.append(n);
877 }
878 ✗ if (names.empty()) {
879 ✗ result.resize(0); // an empty Json::Value would serialize as null, not []
880 }
881 ✗ }
882
883 // Collect the per-pixel [x, y, z, node] preview coordinates for one model from
884 // config/virtualdisplaymap, appending them to pixels. Matches the model section
885 // on exact or normalized name. Parsing on demand (only when a preview is
886 // requested) keeps this potentially very large data out of the page.
887 9 static void collectModelPreviewPixels(const std::string& modelName, Json::Value& pixels) {
888
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 const std::string want = modelName;
889
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 const std::string wantNorm = normalizeOverlayModelName(modelName);
890
4/8
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 9 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 9 times.
✗ Branch 7 not taken.
9 std::ifstream in(FPP_DIR_CONFIG("/virtualdisplaymap"));
891
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 4 times.
9 if (!in.is_open()) {
892 5 return;
893 }
894 // Line format: x,y,z,channel(0-based),channelsPerNode,colorOrder,size
895 struct PreviewPoint {
896 int x = 0, y = 0, z = 0;
897 int channel = -1;
898 int channelsPerNode = 0;
899 };
900 4 std::vector<PreviewPoint> points;
901 4 std::string line;
902 4 bool inModel = false;
903
4/6
✓ Branch 0 taken 600021 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 600021 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 600017 times.
✓ Branch 5 taken 4 times.
600021 while (std::getline(in, line)) {
904
5/6
✓ Branch 0 taken 600017 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✓ Branch 3 taken 600012 times.
✓ Branch 4 taken 5 times.
✓ Branch 5 taken 600012 times.
600017 if (!line.empty() && line[0] == '#') {
905 // Model header line looks like: # Model: 'Some Name'
906
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 if (line.find("Model:") != std::string::npos) {
907 5 std::size_t q1 = line.find('\'');
908
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 std::size_t q2 = (q1 == std::string::npos) ? std::string::npos : line.find('\'', q1 + 1);
909
3/6
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 5 times.
✗ Branch 5 not taken.
5 if (q1 != std::string::npos && q2 != std::string::npos && q2 > q1) {
910
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 std::string name = line.substr(q1 + 1, q2 - q1 - 1);
911
7/10
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 1 times.
✓ Branch 6 taken 2 times.
✓ Branch 7 taken 3 times.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
5 inModel = (name == want) || (normalizeOverlayModelName(name) == wantNorm);
912 5 }
913 }
914 10 continue;
915 5 }
916
5/6
✓ Branch 0 taken 600007 times.
✓ Branch 1 taken 5 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 600007 times.
✓ Branch 4 taken 5 times.
✓ Branch 5 taken 600007 times.
600012 if (!inModel || line.empty()) {
917 5 continue;
918 }
919 600007 PreviewPoint pt;
920
1/2
✓ Branch 0 taken 600007 times.
✗ Branch 1 not taken.
600007 if (sscanf(line.c_str(), "%d,%d,%d,%d,%d", &pt.x, &pt.y, &pt.z, &pt.channel, &pt.channelsPerNode) >= 3) {
921
1/2
✓ Branch 0 taken 600007 times.
✗ Branch 1 not taken.
600007 points.push_back(pt);
922 }
923 }
924
925 // xLights writes each model's points in spatial order, not node order, so
926 // derive each point's node number from its channel or the UI would attach
927 // submodel node numbers to the wrong pixels.
928 4 int firstChannel = INT_MAX;
929
2/2
✓ Branch 0 taken 600007 times.
✓ Branch 1 taken 4 times.
600011 for (auto& pt : points) {
930 600007 firstChannel = std::min(firstChannel, pt.channel);
931 }
932
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 std::stable_sort(points.begin(), points.end(), [](const PreviewPoint& a, const PreviewPoint& b) {
933 8856850 return a.channel < b.channel;
934 });
935
2/2
✓ Branch 0 taken 600007 times.
✓ Branch 1 taken 4 times.
600011 for (std::size_t i = 0; i < points.size(); i++) {
936 600007 const PreviewPoint& pt = points[i];
937
1/2
✓ Branch 0 taken 600007 times.
✗ Branch 1 not taken.
600007 int node = (pt.channel >= 0 && pt.channelsPerNode > 0)
938
1/2
✓ Branch 0 taken 600007 times.
✗ Branch 1 not taken.
1200014 ? (pt.channel - firstChannel) / pt.channelsPerNode + 1
939 ✗ : (int)i + 1;
940
1/2
✓ Branch 0 taken 600007 times.
✗ Branch 1 not taken.
600007 Json::Value px(Json::arrayValue);
941
2/4
✓ Branch 0 taken 600007 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 600007 times.
✗ Branch 3 not taken.
600007 px.append(pt.x);
942
2/4
✓ Branch 0 taken 600007 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 600007 times.
✗ Branch 3 not taken.
600007 px.append(pt.y);
943
2/4
✓ Branch 0 taken 600007 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 600007 times.
✗ Branch 3 not taken.
600007 px.append(pt.z);
944
2/4
✓ Branch 0 taken 600007 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 600007 times.
✗ Branch 3 not taken.
600007 px.append(node);
945
1/2
✓ Branch 0 taken 600007 times.
✗ Branch 1 not taken.
600007 pixels.append(px);
946 600007 }
947
6/6
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 5 times.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 5 times.
✓ Branch 4 taken 4 times.
✓ Branch 5 taken 5 times.
19 }
948
949 // ---- Bulk pixel data ------------------------------------------------------
950 //
951 // Backing code for PUT /api/overlays/model/{model}/data. The per-pixel endpoint
952 // costs one HTTP round trip per pixel, and the only bulk alternative was the
953 // mmapped shm buffer -- which is local-only and awkward from anything that is
954 // not C. This moves a whole frame, or a rectangle of one, per request.
955
956 // Query args are parsed with "absent" and "malformed" kept apart so a typo
957 // reports as an error instead of silently taking the default.
958 enum class ArgState {
959 Missing,
960 Bad,
961 Ok
962 };
963 177596 static ArgState getIntArg(const HttpRequestPtr& req, const char* name, int& out) {
964
2/4
✓ Branch 0 taken 177596 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 177596 times.
✗ Branch 3 not taken.
177596 std::string v = getRequestArg(req, name);
965
2/2
✓ Branch 0 taken 177590 times.
✓ Branch 1 taken 6 times.
177596 if (v.empty()) {
966 177590 return ArgState::Missing;
967 }
968 try {
969 6 std::size_t pos = 0;
970
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 int i = std::stoi(v, &pos, 10);
971
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (pos != v.size()) {
972 ✗ return ArgState::Bad;
973 }
974 6 out = i;
975 6 return ArgState::Ok;
976 ✗ } catch (const std::exception&) {
977 ✗ return ArgState::Bad;
978 ✗ }
979 177596 }
980
981 // Wire layouts accepted for a raw pixel body. 0 means "not a raw format".
982 44339 static int bytesForPixelFormat(const std::string& fmt) {
983
6/6
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 44335 times.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 44337 times.
✓ Branch 5 taken 2 times.
44339 if (fmt.empty() || fmt == "rgb") {
984 44337 return 3;
985 }
986
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
2 if (fmt == "rgbw") {
987 1 return 4;
988 }
989
2/8
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✗ Branch 7 not taken.
1 if (fmt == "mono" || fmt == "grey" || fmt == "gray") {
990 1 return 1;
991 }
992 ✗ return 0;
993 }
994
995 // ?blend= selects how this one write combines with what is already in the
996 // model, reusing the overlay state blend rules. Deliberately NOT named "state":
997 // PUT .../state sets the model's *enable* state, and one arg name meaning both
998 // things would be a trap.
999 44399 static bool parseBlend(const std::string& v, PixelOverlayState& out) {
1000
2/8
✗ Branch 0 not taken.
✓ Branch 1 taken 44399 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✓ Branch 6 taken 44399 times.
✗ Branch 7 not taken.
44399 if (v.empty() || v == "opaque" || v == "Enabled") {
1001 44399 out = PixelOverlayState(PixelOverlayState::Enabled);
1002 ✗ } else if (v == "transparent" || v == "Transparent") {
1003 ✗ out = PixelOverlayState(PixelOverlayState::Transparent);
1004 ✗ } else if (v == "transparentrgb" || v == "TransparentRGB") {
1005 ✗ out = PixelOverlayState(PixelOverlayState::TransparentRGB);
1006 } else {
1007 ✗ return false;
1008 }
1009 44399 return true;
1010 }
1011
1012 // ?scale= for encoded-image bodies, onto ScaleOverlayImage()'s vocabulary.
1013 52 static bool parseImageScaling(const std::string& v, std::string& out) {
1014
5/6
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 51 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 51 times.
✓ Branch 5 taken 1 times.
52 if (v.empty() || v == "fit") {
1015 51 out = "Scale to Fit";
1016
3/6
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 1 times.
1 } else if (v == "fill" || v == "crop") {
1017 ✗ out = "Crop to Fill";
1018
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 } else if (v == "stretch") {
1019 ✗ out = "Stretch";
1020
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 } else if (v == "none") {
1021 1 out = "None";
1022 } else {
1023 ✗ return false;
1024 }
1025 52 return true;
1026 }
1027
1028 // Does this body look like an encoded image?
1029 //
1030 // Only ever consulted AFTER the body has failed to match the raw pixel geometry
1031 // the request describes. Raw pixel data can legitimately begin with any byte
1032 // sequence at all -- 0xFF 0xD8 0xFF is a perfectly ordinary red-ish pixel, not
1033 // necessarily a JPEG -- so sniffing before the length check would misread real
1034 // pixel data as an image. Length first, magic second, is unambiguous.
1035 8 static bool looksLikeEncodedImage(std::string_view b) {
1036 48 auto starts = [&](const char* magic, std::size_t n) {
1037
3/4
✓ Branch 0 taken 35 times.
✓ Branch 1 taken 13 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 35 times.
48 return b.size() >= n && memcmp(b.data(), magic, n) == 0;
1038 8 };
1039
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
16 return starts("\x89PNG\r\n\x1a\n", 8) || // PNG
1040
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
16 starts("\xff\xd8\xff", 3) || // JPEG
1041
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
16 starts("GIF8", 4) || // GIF
1042
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
16 starts("BM", 2) || // BMP
1043
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
16 starts("II*\0", 4) || // TIFF little-endian
1044
3/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 6 times.
26 starts("MM\0*", 4) || // TIFF big-endian
1045
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
10 (b.size() >= 12 && memcmp(b.data(), "RIFF", 4) == 0 && // WebP
1046
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
8 memcmp(b.data() + 8, "WEBP", 4) == 0);
1047 }
1048
1049 // `lock` is render_PUT()'s hold on modelsLock. It is released before the
1050 // expensive part of the request (image decode, the blit itself) for two
1051 // reasons, and the caller must not use it afterwards:
1052 //
1053 // * The Text branch below already unlocks before dispatching, because
1054 // touching a model's running effect under modelsLock inverts the order the
1055 // effect thread uses (effectLock, then modelsLock via a submodel's parent
1056 // lookup). Evicting an effect here has to follow the same rule.
1057 // * Holding modelsLock across a GraphicsMagick decode -- milliseconds -- while
1058 // this endpoint is designed to be called at frame rates widens an existing
1059 // window in which the channel output thread sits blocked in doOverlays()
1060 // holding activeModelsLock and waiting on modelsLock. See
1061 // docs/PixelOverlayBulkData.md.
1062 44399 static HttpResponsePtr bulkPixelDataPut(const HttpRequestPtr& req, PixelOverlayModel* m,
1063 std::unique_lock<std::recursive_mutex>& lock) {
1064 18 auto errorResp = [](const std::string& msg) {
1065
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 Json::Value r;
1066
2/4
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 18 times.
✗ Branch 3 not taken.
18 r["Status"] = "ERROR";
1067
2/4
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 18 times.
✗ Branch 3 not taken.
18 r["Message"] = msg;
1068
4/8
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 18 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 18 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 18 times.
✗ Branch 7 not taken.
72 return makeStringResponse(SaveJsonToString(r, ""), 400, "application/json");
1069 18 };
1070
1071 44399 const int modelW = m->getWidth();
1072 44399 const int modelH = m->getHeight();
1073
1074 44399 int x = 0, y = 0, w = 0, h = 0;
1075 44399 bool xExplicit = false, yExplicit = false, wExplicit = false, hExplicit = false;
1076 struct {
1077 const char* name;
1078 int* val;
1079 bool* explicitly;
1080 44399 } intArgs[] = {
1081 { "x", &x, &xExplicit },
1082 { "y", &y, &yExplicit },
1083 { "w", &w, &wExplicit },
1084 { "h", &h, &hExplicit },
1085 44399 };
1086
2/2
✓ Branch 0 taken 177596 times.
✓ Branch 1 taken 44399 times.
221995 for (auto& a : intArgs) {
1087
1/2
✓ Branch 0 taken 177596 times.
✗ Branch 1 not taken.
177596 ArgState st = getIntArg(req, a.name, *a.val);
1088
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 177596 times.
177596 if (st == ArgState::Bad) {
1089 ✗ return errorResp(std::string("Argument \"") + a.name + "\" is not an integer");
1090 }
1091 177596 *a.explicitly = (st == ArgState::Ok);
1092 }
1093
1094
2/4
✓ Branch 0 taken 44399 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 44399 times.
✗ Branch 3 not taken.
88798 std::string fmtArg = getRequestArg(req, "fmt");
1095
2/4
✓ Branch 0 taken 44399 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 44399 times.
✗ Branch 3 not taken.
88798 std::string scaleArg = getRequestArg(req, "scale");
1096
2/4
✓ Branch 0 taken 44399 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 44399 times.
✗ Branch 3 not taken.
88798 std::string targetArg = getRequestArg(req, "target");
1097
2/4
✓ Branch 0 taken 44399 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 44399 times.
✗ Branch 3 not taken.
88798 std::string autoEnableArg = getRequestArg(req, "autoEnable");
1098
2/4
✓ Branch 0 taken 44399 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 44399 times.
✗ Branch 3 not taken.
44399 std::string stopEffectArg = getRequestArg(req, "stopEffect");
1099
1100 44399 PixelOverlayState blend;
1101
4/8
✓ Branch 0 taken 44399 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 44399 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 44399 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 44399 times.
88798 if (!parseBlend(getRequestArg(req, "blend"), blend)) {
1102 ✗ return errorResp("Unknown blend \"" + getRequestArg(req, "blend") +
1103 ✗ "\"; expected opaque, transparent, or transparentrgb");
1104 }
1105
1106 44399 bool toOverlay = false;
1107
3/4
✓ Branch 0 taken 44399 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✓ Branch 3 taken 44393 times.
44399 if (targetArg == "overlay") {
1108 6 toOverlay = true;
1109
2/8
✗ Branch 0 not taken.
✓ Branch 1 taken 44393 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 44393 times.
44393 } else if (!targetArg.empty() && targetArg != "channel") {
1110 ✗ return errorResp("Unknown target \"" + targetArg + "\"; expected channel or overlay");
1111 }
1112
1113 // string_view, not getRequestContent(): the body is a whole frame and there
1114 // is no reason to copy it before we know what it is.
1115
1/2
✓ Branch 0 taken 44399 times.
✗ Branch 1 not taken.
44399 std::string_view body = req->body();
1116
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 44397 times.
44399 if (body.empty()) {
1117
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
4 return errorResp("Empty body");
1118 }
1119
1120
3/6
✓ Branch 0 taken 44397 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 44397 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 44397 times.
✗ Branch 5 not taken.
88794 std::string ctype = req->getHeader("content-type");
1121 44397 std::transform(ctype.begin(), ctype.end(), ctype.begin(), ::tolower);
1122
1123 enum class BodyKind {
1124 Raw,
1125 Json,
1126 Image
1127 };
1128 44397 BodyKind kind = BodyKind::Raw;
1129
12/20
✓ Branch 0 taken 44397 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 44345 times.
✓ Branch 3 taken 52 times.
✓ Branch 4 taken 44345 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 44345 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 44345 times.
✓ Branch 10 taken 44345 times.
✓ Branch 11 taken 52 times.
✓ Branch 12 taken 44345 times.
✓ Branch 13 taken 52 times.
✓ Branch 14 taken 52 times.
✓ Branch 15 taken 44345 times.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
133087 if (fmtArg == "image" || startsWith(ctype, "image/")) {
1130 52 kind = BodyKind::Image;
1131
4/6
✓ Branch 0 taken 44345 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 44345 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 44344 times.
88690 } else if (startsWith(ctype, "application/json")) {
1132 1 kind = BodyKind::Json;
1133 } else {
1134 44344 std::size_t i = 0;
1135
6/6
✓ Branch 0 taken 44945 times.
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 605 times.
✓ Branch 3 taken 44340 times.
✓ Branch 4 taken 605 times.
✓ Branch 5 taken 44344 times.
44949 while (i < body.size() && isspace((unsigned char)body[i])) {
1136 605 i++;
1137 }
1138
6/6
✓ Branch 0 taken 44340 times.
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 6 times.
✓ Branch 3 taken 44334 times.
✓ Branch 4 taken 6 times.
✓ Branch 5 taken 44338 times.
44344 if (i < body.size() && body[i] == '{') {
1139 6 kind = BodyKind::Json;
1140 }
1141 }
1142
1143 // Where the pixel (or encoded image) bytes actually live. The JSON form
1144 // decodes into `decoded`; the other two point straight at the body.
1145 44397 std::vector<uint8_t> decoded;
1146 44397 const uint8_t* data = (const uint8_t*)body.data();
1147 44397 std::size_t dataLen = body.size();
1148
1149
2/2
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 44390 times.
44397 if (kind == BodyKind::Json) {
1150
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 Json::Value root;
1151
4/6
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 5 times.
14 if (!LoadJsonFromString(std::string(body), root)) {
1152
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
4 return errorResp("Could not parse the JSON body");
1153 }
1154
3/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 1 times.
5 if (!root.isMember("Data")) {
1155
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
8 return errorResp("A JSON body needs a base64 \"Data\" member");
1156 }
1157 // JSON members win over query args -- one request, one source of truth.
1158 struct {
1159 const char* key;
1160 int* val;
1161 bool* explicitly;
1162 1 } jsonInts[] = {
1163 { "X", &x, &xExplicit },
1164 { "Y", &y, &yExplicit },
1165 { "W", &w, &wExplicit },
1166 { "H", &h, &hExplicit },
1167 1 };
1168
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 1 times.
5 for (auto& a : jsonInts) {
1169
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 if (root.isMember(a.key)) {
1170
3/6
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 4 times.
4 if (!root[a.key].isIntegral()) {
1171 ✗ return errorResp(std::string("JSON member \"") + a.key + "\" is not an integer");
1172 }
1173
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 *a.val = root[a.key].asInt();
1174 4 *a.explicitly = true;
1175 }
1176 }
1177
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 if (root.isMember("Format")) {
1178
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 fmtArg = root["Format"].asString();
1179 }
1180
3/6
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
1 decoded = base64Decode(root["Data"].asString());
1181
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (decoded.empty()) {
1182 ✗ return errorResp("The \"Data\" member did not decode to any bytes");
1183 }
1184 1 data = decoded.data();
1185 1 dataLen = decoded.size();
1186
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
1 if (fmtArg == "image") {
1187 ✗ kind = BodyKind::Image;
1188 }
1189
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 6 times.
7 }
1190
1191 // An omitted w/h means "from the offset to the far edge", which makes a
1192 // plain full-frame post need no args at all.
1193
2/2
✓ Branch 0 taken 44388 times.
✓ Branch 1 taken 3 times.
44391 if (!wExplicit) {
1194 44388 w = modelW - std::max(0, x);
1195 }
1196
2/2
✓ Branch 0 taken 44388 times.
✓ Branch 1 taken 3 times.
44391 if (!hExplicit) {
1197 44388 h = modelH - std::max(0, y);
1198 }
1199
2/4
✓ Branch 0 taken 44391 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 44391 times.
44391 if (w <= 0 || h <= 0) {
1200 ✗ return errorResp("Region " + std::to_string(w) + "x" + std::to_string(h) +
1201 ✗ " has no area (model is " + std::to_string(modelW) + "x" +
1202 ✗ std::to_string(modelH) + ")");
1203 }
1204
1205 44391 int srcBpp = 3;
1206
2/2
✓ Branch 0 taken 44339 times.
✓ Branch 1 taken 52 times.
44391 if (kind != BodyKind::Image) {
1207
1/2
✓ Branch 0 taken 44339 times.
✗ Branch 1 not taken.
44339 srcBpp = bytesForPixelFormat(fmtArg);
1208
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 44339 times.
44339 if (!srcBpp) {
1209 ✗ return errorResp("Unknown fmt \"" + fmtArg + "\"; expected rgb, rgbw, mono, or image");
1210 }
1211 // 64-bit on every platform: size_t is 32 bits on armhf, where
1212 // w=1073741825 h=4 mono wrapped to 4 and a 4-byte body passed, sending
1213 // blitData() striding a gigabyte per row through the request buffer.
1214 // With an exact match against the body, w*h*bpp can be no larger than
1215 // the bytes actually received, which bounds every index blitData uses.
1216 44339 uint64_t expect = (uint64_t)w * (uint64_t)h * (uint64_t)srcBpp;
1217
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 44331 times.
44339 if ((uint64_t)dataLen != expect) {
1218 // Wrong length for the geometry given. If the bytes are an encoded
1219 // image, the caller almost certainly meant to post one, so take it
1220 // rather than making them add ?fmt=image.
1221
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (looksLikeEncodedImage(std::string_view((const char*)data, dataLen))) {
1222 ✗ kind = BodyKind::Image;
1223 } else {
1224
3/6
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 8 times.
✗ Branch 5 not taken.
16 return errorResp("Body is " + std::to_string(dataLen) + " bytes; a " +
1225
4/8
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 8 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 8 times.
✗ Branch 7 not taken.
32 std::to_string(w) + "x" + std::to_string(h) + " " +
1226
8/14
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 8 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 8 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 7 times.
✓ Branch 11 taken 1 times.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
39 (fmtArg.empty() ? "rgb" : fmtArg) + " region needs " +
1227
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
32 std::to_string(expect) +
1228
4/8
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 8 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 8 times.
✗ Branch 7 not taken.
24 " (w * h * " + std::to_string(srcBpp) + ")");
1229 }
1230 }
1231 }
1232
1233 44383 std::string scaling;
1234
2/2
✓ Branch 0 taken 52 times.
✓ Branch 1 taken 44331 times.
44383 if (kind == BodyKind::Image) {
1235
2/4
✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 52 times.
52 if (!parseImageScaling(scaleArg, scaling)) {
1236 ✗ return errorResp("Unknown scale \"" + scaleArg + "\"; expected fit, fill, stretch, or none");
1237 }
1238 // For an image, w/h are the box it gets scaled into, and unlike the raw
1239 // path there is no body length to cross-check them against. Clamp to the
1240 // model: a bigger box cannot draw anything more (the excess is clipped
1241 // away), but it would have GraphicsMagick allocate for it first --
1242 // ?w=9999&h=9999 is a ~300MB resize, which is fatal on a BeagleBone.
1243 52 w = std::min(w, modelW);
1244 52 h = std::min(h, modelH);
1245 }
1246
1247 // autoEnable takes the same vocabulary as the Text and Image effects and,
1248 // like them, only ever promotes a Disabled model. Done while modelsLock is
1249 // still held, because that is the order every other state change uses.
1250
2/2
✓ Branch 0 taken 44320 times.
✓ Branch 1 taken 63 times.
44383 if (!autoEnableArg.empty()) {
1251
1/2
✓ Branch 0 taken 44320 times.
✗ Branch 1 not taken.
44320 PixelOverlayState st(autoEnableArg);
1252
3/4
✓ Branch 0 taken 44320 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 55 times.
✓ Branch 3 taken 44265 times.
88640 if (st.getState() != PixelOverlayState::Disabled &&
1253
3/4
✓ Branch 0 taken 44320 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 55 times.
✓ Branch 3 taken 44265 times.
88640 m->getState().getState() == PixelOverlayState::Disabled) {
1254
1/2
✓ Branch 0 taken 55 times.
✗ Branch 1 not taken.
55 m->setState(st);
1255 }
1256 }
1257
1258 // Everything below is either slow or touches the effect lock, so drop
1259 // modelsLock first. See the note on this function.
1260
1/2
✓ Branch 0 taken 44383 times.
✗ Branch 1 not taken.
44383 lock.unlock();
1261
1262 // Take the model over. An effect still ticking on it (scrolling text, a
1263 // moving image) would overwrite whatever we draw on its very next update;
1264 // the Image effect's static path evicts the running effect for exactly this
1265 // reason. ?stopEffect=false opts out for a caller deliberately drawing over
1266 // one. The .../fill endpoint does the same, and .../pixel cancels only a
1267 // pending Stop Effects (see retireEffectFor()).
1268
7/10
✓ Branch 0 taken 44383 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✓ Branch 3 taken 44375 times.
✓ Branch 4 taken 8 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 8 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 8 times.
✓ Branch 9 taken 44375 times.
44383 if (stopEffectArg != "false" && stopEffectArg != "0") {
1269
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 std::unique_lock<std::recursive_mutex> l(m->getRunningEffectMutex());
1270
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (m->getRunningEffect()) {
1271 ✗ m->setRunningEffect(nullptr, 25); // one no-op tick, then dropped
1272 }
1273 8 }
1274
1275 44383 bool ok = false;
1276 44383 std::string err;
1277
2/2
✓ Branch 0 taken 52 times.
✓ Branch 1 taken 44331 times.
44383 if (kind == BodyKind::Image) {
1278 // With no x/y given, centre the image in the box the way the Image
1279 // effect does; with either given, honour it as the top-left corner.
1280
2/4
✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 52 times.
✗ Branch 3 not taken.
52 bool center = !xExplicit && !yExplicit;
1281
1/2
✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
52 ok = DrawEncodedImageOnModel(m, data, dataLen, scaling, w, h,
1282 x, y, center, blend, toOverlay, err);
1283 } else {
1284
3/4
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 44325 times.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
44331 ok = toOverlay ? m->blitOverlayBuffer(data, w, h, srcBpp, x, y)
1285
1/2
✓ Branch 0 taken 44325 times.
✗ Branch 1 not taken.
44325 : m->blitData(data, w, h, srcBpp, x, y, blend);
1286
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 44331 times.
44331 if (!ok) {
1287 ✗ err = "the region landed entirely outside the model";
1288 }
1289 }
1290
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 44381 times.
44383 if (!ok) {
1291
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 return errorResp(err);
1292 }
1293
1294
8/10
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 44375 times.
✓ Branch 2 taken 44331 times.
✓ Branch 3 taken 50 times.
✓ Branch 4 taken 44328 times.
✓ Branch 5 taken 3 times.
✓ Branch 6 taken 44381 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 44381 times.
✗ Branch 9 not taken.
44381 LogDebug(VB_CHANNELOUT, "PUT overlay data from %s: model \"%s\", %zu bytes, %s, %dx%d @ %d,%d -> %s\n",
1295 getEffectiveClientIP(req).c_str(), m->getName().c_str(), dataLen,
1296 kind == BodyKind::Image ? "image" : (fmtArg.empty() ? "rgb" : fmtArg.c_str()),
1297 w, h, x, y, toOverlay ? "overlay buffer" : "channel data");
1298
1299
1/2
✓ Branch 0 taken 44381 times.
✗ Branch 1 not taken.
44381 Json::Value r;
1300
2/4
✓ Branch 0 taken 44381 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 44381 times.
✗ Branch 3 not taken.
44381 r["Status"] = "OK";
1301
2/4
✓ Branch 0 taken 44381 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 44381 times.
✗ Branch 3 not taken.
44381 r["Message"] = "";
1302
2/4
✓ Branch 0 taken 44381 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 44381 times.
✗ Branch 3 not taken.
44381 r["Model"] = m->getName();
1303
2/4
✓ Branch 0 taken 44381 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 44381 times.
✗ Branch 3 not taken.
44381 r["X"] = x;
1304
2/4
✓ Branch 0 taken 44381 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 44381 times.
✗ Branch 3 not taken.
44381 r["Y"] = y;
1305
2/4
✓ Branch 0 taken 44381 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 44381 times.
✗ Branch 3 not taken.
44381 r["W"] = w;
1306
2/4
✓ Branch 0 taken 44381 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 44381 times.
✗ Branch 3 not taken.
44381 r["H"] = h;
1307
4/6
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 44375 times.
✓ Branch 2 taken 44381 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 44381 times.
✗ Branch 5 not taken.
44381 r["Target"] = toOverlay ? "overlay" : "channel";
1308
2/4
✓ Branch 0 taken 44381 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 44381 times.
✗ Branch 3 not taken.
44381 r["BytesPerPixel"] = m->getBytesPerPixel();
1309 // True when part of the region fell outside the model and was clipped away
1310 // rather than rejected -- that is what lets a caller slide something off an
1311 // edge by reposting it at moving offsets.
1312
7/12
✓ Branch 0 taken 44381 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 44381 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 44380 times.
✓ Branch 5 taken 1 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 44380 times.
✓ Branch 8 taken 44381 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 44381 times.
✗ Branch 11 not taken.
44381 r["Clipped"] = (x < 0) || (y < 0) || ((x + w) > modelW) || ((y + h) > modelH);
1313
4/8
✓ Branch 0 taken 44381 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 44381 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 44381 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 44381 times.
✗ Branch 7 not taken.
133143 return makeStringResponse(SaveJsonToString(r, ""), 200, "application/json");
1314 44399 }
1315
1316 301728 HttpResponsePtr PixelOverlayManager::render_GET(const HttpRequestPtr& req) {
1317
2/4
✓ Branch 0 taken 301728 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 301728 times.
✗ Branch 3 not taken.
301728 auto parts = getPathPieces(req->path());
1318
1/2
✓ Branch 0 taken 301728 times.
✗ Branch 1 not taken.
301728 std::string p1 = parts[0];
1319 301728 int plen = parts.size();
1320
1/2
✓ Branch 0 taken 301728 times.
✗ Branch 1 not taken.
301728 LogDebug(VB_CHANNELOUT, "PixelOverlayManager::render_GET(): %s\n", p1.c_str());
1321
3/4
✓ Branch 0 taken 301728 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1790 times.
✓ Branch 3 taken 299938 times.
301728 if (p1 == "models") {
1322
1/2
✓ Branch 0 taken 1790 times.
✗ Branch 1 not taken.
1790 Json::Value result;
1323 1790 bool empty = true;
1324 1790 std::string modelsVer;
1325
2/2
✓ Branch 0 taken 1705 times.
✓ Branch 1 taken 85 times.
1790 if (plen == 1) {
1326
1/2
✓ Branch 0 taken 1705 times.
✗ Branch 1 not taken.
1705 std::unique_lock<std::recursive_mutex> lock(modelsLock);
1327 // Read the generation under the same lock the build runs under, so
1328 // the tag can never describe a model set newer than the body it
1329 // goes out with. (The counter is monotonic, so even a torn read
1330 // could only tag new content with an old version -- costing one
1331 // extra 200 -- never the reverse, which is the direction that
1332 // would serve stale content.)
1333
1/2
✓ Branch 0 taken 1705 times.
✗ Branch 1 not taken.
3410 modelsVer = std::to_string(modelsGeneration.load());
1334
3/4
✓ Branch 0 taken 1705 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 1703 times.
1705 if (etagMatches(req, modelsVer)) {
1335
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 return makeNotModifiedResponse(makeETagToken(req, modelsVer));
1336 }
1337 1703 bool simple = false;
1338
5/8
✓ Branch 0 taken 1703 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1703 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1703 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 8 times.
✓ Branch 7 taken 1695 times.
3406 if (getRequestArg(req, "simple") == "true") {
1339 8 simple = true;
1340 }
1341
4/8
✓ Branch 0 taken 1703 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1703 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1703 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 1703 times.
3406 if (getRequestArg(req, "all") == "true") {
1342 ✗ result.append("--All Models--");
1343 }
1344
3/6
✓ Branch 0 taken 1703 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1703 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1703 times.
✗ Branch 5 not taken.
1703 bool includeSubs = getRequestArg(req, "submodels") == "true";
1345
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 1699 times.
1703 if (includeSubs) {
1346 // Listing submodels for discovery: load the index (without
1347 // materializing every submodel) so their names are selectable.
1348
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 loadSubModelIndex();
1349 }
1350
2/2
✓ Branch 0 taken 2865 times.
✓ Branch 1 taken 1703 times.
4568 for (auto& mn : modelNames) {
1351
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 2861 times.
2865 if (simple) {
1352
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 result.append(mn);
1353 4 empty = false;
1354 } else {
1355
1/2
✓ Branch 0 taken 2861 times.
✗ Branch 1 not taken.
2861 Json::Value model;
1356
2/4
✓ Branch 0 taken 2861 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2861 times.
✗ Branch 3 not taken.
2861 models[mn].toJson(model);
1357
1/2
✓ Branch 0 taken 2861 times.
✗ Branch 1 not taken.
2861 result.append(model);
1358 2861 empty = false;
1359 2861 }
1360 }
1361
3/4
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 1699 times.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
1703 if (includeSubs && simple) {
1362 // Append submodel names that are not already materialized. Only
1363 // names are listed here to avoid materializing every submodel.
1364
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 4 times.
8 for (auto& sc : subModelConfigs) {
1365
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 if (models.find(sc.first) == models.end()) {
1366
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 result.append(sc.first);
1367 4 empty = false;
1368 }
1369 }
1370 }
1371
2/2
✓ Branch 0 taken 1703 times.
✓ Branch 1 taken 2 times.
1705 } else {
1372
1/2
✓ Branch 0 taken 85 times.
✗ Branch 1 not taken.
85 std::string model = parts[1];
1373 85 std::string type;
1374
1/2
✓ Branch 0 taken 85 times.
✗ Branch 1 not taken.
85 std::unique_lock<std::recursive_mutex> lock(modelsLock);
1375
1/2
✓ Branch 0 taken 85 times.
✗ Branch 1 not taken.
85 PixelOverlayModel* m = getModelLocked(model); // resolves lazy submodels too
1376
2/2
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 75 times.
85 if (m) {
1377
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 m->toJson(result);
1378 } else {
1379
3/6
✓ Branch 0 taken 75 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 75 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 75 times.
✗ Branch 5 not taken.
150 return makeStringResponse("Model not found: " + parts[1], 404);
1380 }
1381
6/6
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 75 times.
✓ Branch 2 taken 10 times.
✓ Branch 3 taken 75 times.
✓ Branch 4 taken 10 times.
✓ Branch 5 taken 75 times.
235 }
1382
4/4
✓ Branch 0 taken 334 times.
✓ Branch 1 taken 1379 times.
✓ Branch 2 taken 324 times.
✓ Branch 3 taken 10 times.
1713 if (empty && plen == 1) {
1383
3/6
✓ Branch 0 taken 324 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 324 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 324 times.
✗ Branch 5 not taken.
1296 return makeVersionedETagResponse(req, modelsVer, std::string("[]"));
1384
2/2
✓ Branch 0 taken 1379 times.
✓ Branch 1 taken 10 times.
1389 } else if (plen == 1) {
1385 // ~580 bytes per model, so this is the response that scales worst
1386 // with a large show, and the model pickers on several pages fetch
1387 // it more than once per load. It carries no live state -- that is
1388 // /api/overlays/models -- so the model-set generation is a complete
1389 // version for it.
1390
4/8
✓ Branch 0 taken 1379 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1379 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1379 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1379 times.
✗ Branch 7 not taken.
5516 return makeVersionedETagResponse(req, modelsVer, SaveJsonToString(result, ""));
1391 } else {
1392
2/4
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
✗ Branch 3 not taken.
20 std::string resultStr = SaveJsonToString(result, "");
1393
2/4
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
✗ Branch 3 not taken.
10 return makeStringResponse(resultStr, 200, "application/json");
1394 10 }
1395
2/4
✓ Branch 0 taken 299938 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 299938 times.
✗ Branch 3 not taken.
301728 } else if (p1 == "overlays") {
1396
3/10
✓ Branch 0 taken 299938 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 299938 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 299938 times.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
299938 std::string p2 = plen > 1 ? parts[1] : "";
1397
6/10
✓ Branch 0 taken 4094 times.
✓ Branch 1 taken 295844 times.
✓ Branch 2 taken 4094 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 295844 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 295844 times.
✓ Branch 7 taken 4094 times.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
595782 std::string p3 = plen > 2 ? parts[2] : "";
1398
6/10
✓ Branch 0 taken 687 times.
✓ Branch 1 taken 299251 times.
✓ Branch 2 taken 687 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 299251 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 299251 times.
✓ Branch 7 taken 687 times.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
599189 std::string p4 = plen > 3 ? parts[3] : "";
1399
6/10
✓ Branch 0 taken 283 times.
✓ Branch 1 taken 299655 times.
✓ Branch 2 taken 283 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 299655 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 299655 times.
✓ Branch 7 taken 283 times.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
599593 std::string p5 = plen > 4 ? parts[4] : "";
1400
1/2
✓ Branch 0 taken 299938 times.
✗ Branch 1 not taken.
299938 Json::Value result;
1401 299938 bool etagable = false;
1402 299938 std::string versionedTag;
1403
3/4
✓ Branch 0 taken 299938 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 23 times.
✓ Branch 3 taken 299915 times.
299938 if (p2 == "fonts") {
1404
2/2
✓ Branch 0 taken 184 times.
✓ Branch 1 taken 23 times.
207 for (auto& a : fonts) {
1405
2/4
✓ Branch 0 taken 184 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 184 times.
✗ Branch 3 not taken.
184 result.append(a.first);
1406 }
1407
2/4
✓ Branch 0 taken 299915 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 299915 times.
299915 } else if (p2 == "images") {
1408 // Flat list of image files under the media images directory, as
1409 // paths relative to it, for the Image effect's file picker. The
1410 // api/files/Images endpoint returns per-file metadata objects; the
1411 // command-argument dropdowns want a plain array of strings.
1412 ✗ collectOverlayImageNames(result);
1413
3/4
✓ Branch 0 taken 299915 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✓ Branch 3 taken 299910 times.
299915 } else if (p2 == "settings") {
1414
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 result["autoCreate"] = autoCreate;
1415
3/4
✓ Branch 0 taken 299910 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5510 times.
✓ Branch 3 taken 294400 times.
299910 } else if (p2 == "models") {
1416 // Deliberately a content hash rather than the model generation:
1417 // this response carries isActive and effectRunning, which the
1418 // generation does not track. Versioning it on the model set would
1419 // report a stopped effect as still running. The hash still saves
1420 // the transfer, which is what scales with the model count.
1421 5510 etagable = true;
1422
1/2
✓ Branch 0 taken 5510 times.
✗ Branch 1 not taken.
5510 std::unique_lock<std::recursive_mutex> lock(modelsLock);
1423 5510 bool hasModels = false;
1424
2/2
✓ Branch 0 taken 815874 times.
✓ Branch 1 taken 5510 times.
821384 for (auto& mn : modelNames) {
1425
1/2
✓ Branch 0 taken 815874 times.
✗ Branch 1 not taken.
815874 Json::Value model;
1426
1/2
✓ Branch 0 taken 815874 times.
✗ Branch 1 not taken.
815874 PixelOverlayModel* m = models[mn].model;
1427
1/2
✓ Branch 0 taken 815874 times.
✗ Branch 1 not taken.
815874 m->toJson(model);
1428
3/6
✓ Branch 0 taken 815874 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 815874 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 815874 times.
✗ Branch 5 not taken.
815874 model["isActive"] = (int)m->getState().getState();
1429
1/2
✓ Branch 0 taken 815874 times.
✗ Branch 1 not taken.
815874 std::unique_lock<std::recursive_mutex> lock(m->getRunningEffectMutex());
1430
2/2
✓ Branch 0 taken 2977 times.
✓ Branch 1 taken 812897 times.
815874 if (m->getRunningEffect()) {
1431
3/6
✓ Branch 0 taken 2977 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2977 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2977 times.
✗ Branch 5 not taken.
2977 model["effectName"] = m->getRunningEffect()->name();
1432
2/4
✓ Branch 0 taken 2977 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2977 times.
✗ Branch 3 not taken.
2977 model["isLocked"] = true;
1433
2/4
✓ Branch 0 taken 2977 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2977 times.
✗ Branch 3 not taken.
2977 model["effectRunning"] = true;
1434 } else {
1435
2/4
✓ Branch 0 taken 812897 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 812897 times.
✗ Branch 3 not taken.
812897 model["effectRunning"] = false;
1436 }
1437 815874 hasModels = true;
1438
2/4
✓ Branch 0 taken 815874 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 815874 times.
✗ Branch 3 not taken.
815874 model["width"] = m->getWidth();
1439
2/4
✓ Branch 0 taken 815874 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 815874 times.
✗ Branch 3 not taken.
815874 model["height"] = m->getHeight();
1440
1/2
✓ Branch 0 taken 815874 times.
✗ Branch 1 not taken.
815874 result.append(model);
1441 815874 }
1442
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5510 times.
5510 if (!hasModels) {
1443 ✗ result.resize(0);
1444 }
1445
3/4
✓ Branch 0 taken 294400 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2029 times.
✓ Branch 3 taken 292371 times.
299910 } else if (p2 == "model") {
1446
1/2
✓ Branch 0 taken 2029 times.
✗ Branch 1 not taken.
2029 std::unique_lock<std::recursive_mutex> lock(modelsLock);
1447
1/2
✓ Branch 0 taken 2029 times.
✗ Branch 1 not taken.
2029 auto m = getModelLocked(p3); // resolves lazy submodels too
1448
10/12
✓ Branch 0 taken 401 times.
✓ Branch 1 taken 1628 times.
✓ Branch 2 taken 401 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 140 times.
✓ Branch 5 taken 261 times.
✓ Branch 6 taken 140 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 70 times.
✓ Branch 9 taken 70 times.
✓ Branch 10 taken 70 times.
✓ Branch 11 taken 1959 times.
2029 if (!m && p4 == "data" && p5 == "raw") {
1449 // The legacy JSON paths fall through and answer 200 with a bare
1450 // "null" for an unknown model. That is left alone for
1451 // compatibility, but a binary endpoint must not hand back a
1452 // 4-byte JSON literal that a caller would read as pixel data.
1453
3/6
✓ Branch 0 taken 70 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 70 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 70 times.
✗ Branch 5 not taken.
140 return makeStringResponse("Model not found: " + p3, 404);
1454 }
1455
7/8
✓ Branch 0 taken 1628 times.
✓ Branch 1 taken 331 times.
✓ Branch 2 taken 1628 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 9 times.
✓ Branch 5 taken 1619 times.
✓ Branch 6 taken 9 times.
✓ Branch 7 taken 1950 times.
1959 if (m && p4 == "preview") {
1456 // Needs only the name. The virtualdisplaymap parse is the
1457 // slowest thing this route does, and under modelsLock and the
1458 // model's effectLock it stalled the effect thread - and the
1459 // channel output thread too, when the effect updates after
1460 // output - for as long as it took.
1461
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 std::string modelName = m->getName();
1462
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 lock.unlock();
1463
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 Json::Value pixels(Json::arrayValue);
1464
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 collectModelPreviewPixels(modelName, pixels);
1465
2/4
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✗ Branch 3 not taken.
9 result["pixels"] = pixels;
1466
2/2
✓ Branch 0 taken 1619 times.
✓ Branch 1 taken 331 times.
1959 } else if (m) {
1467
1/2
✓ Branch 0 taken 1619 times.
✗ Branch 1 not taken.
1619 std::unique_lock<std::recursive_mutex> lock(m->getRunningEffectMutex());
1468
3/4
✓ Branch 0 taken 1619 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 325 times.
✓ Branch 3 taken 1294 times.
1619 if (p4 == "data") {
1469
3/4
✓ Branch 0 taken 325 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 163 times.
✓ Branch 3 taken 162 times.
325 if (p5 == "raw") {
1470 // Binary read-back: width*height*bytesPerPixel bytes,
1471 // the exact layout PUT .../data takes. The JSON form
1472 // below spends 4-6 wire bytes per payload byte.
1473 163 std::vector<uint8_t> buf;
1474
1/2
✓ Branch 0 taken 163 times.
✗ Branch 1 not taken.
163 m->getData(buf);
1475
2/4
✓ Branch 0 taken 163 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 163 times.
✗ Branch 3 not taken.
652 auto resp = makeStringResponse(std::string((const char*)buf.data(), buf.size()),
1476
1/2
✓ Branch 0 taken 163 times.
✗ Branch 1 not taken.
163 200, "application/octet-stream");
1477
2/4
✓ Branch 0 taken 163 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 163 times.
✗ Branch 3 not taken.
489 resp->addHeader("X-FPP-Model-Width", std::to_string(m->getWidth()));
1478
2/4
✓ Branch 0 taken 163 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 163 times.
✗ Branch 3 not taken.
489 resp->addHeader("X-FPP-Model-Height", std::to_string(m->getHeight()));
1479
2/4
✓ Branch 0 taken 163 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 163 times.
✗ Branch 3 not taken.
489 resp->addHeader("X-FPP-Model-BytesPerPixel", std::to_string(m->getBytesPerPixel()));
1480 163 return resp;
1481 163 }
1482
1/2
✓ Branch 0 taken 162 times.
✗ Branch 1 not taken.
162 Json::Value data;
1483
2/4
✓ Branch 0 taken 162 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 162 times.
✗ Branch 3 not taken.
162 m->getDataJson(data, p5 == "rle");
1484
2/4
✓ Branch 0 taken 162 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 162 times.
✗ Branch 3 not taken.
162 result["data"] = data;
1485
3/6
✓ Branch 0 taken 162 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 162 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 162 times.
✗ Branch 5 not taken.
162 result["rle"] = p5 == "rle";
1486
2/4
✓ Branch 0 taken 162 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 162 times.
✗ Branch 3 not taken.
162 result["isLocked"] = m->getRunningEffect() != nullptr; // compatibility
1487
2/4
✓ Branch 0 taken 162 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 162 times.
✗ Branch 3 not taken.
162 result["effectRunning"] = m->getRunningEffect() != nullptr;
1488
3/4
✓ Branch 0 taken 1294 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✓ Branch 3 taken 1286 times.
1456 } else if (p4 == "clear") {
1489
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 retireEffectFor(m, false);
1490
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 m->clear();
1491
3/6
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 8 times.
✗ Branch 5 not taken.
32 return makeStringResponse("{ \"Status\": \"OK\", \"Message\": \"\"}", 200);
1492 } else {
1493
1/2
✓ Branch 0 taken 1286 times.
✗ Branch 1 not taken.
1286 m->toJson(result);
1494
3/6
✓ Branch 0 taken 1286 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1286 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1286 times.
✗ Branch 5 not taken.
1286 result["isActive"] = (int)m->getState().getState();
1495
2/2
✓ Branch 0 taken 820 times.
✓ Branch 1 taken 466 times.
1286 if (m->getRunningEffect()) {
1496
3/6
✓ Branch 0 taken 820 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 820 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 820 times.
✗ Branch 5 not taken.
820 result["effectName"] = m->getRunningEffect()->name();
1497
2/4
✓ Branch 0 taken 820 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 820 times.
✗ Branch 3 not taken.
820 result["isLocked"] = true;
1498
2/4
✓ Branch 0 taken 820 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 820 times.
✗ Branch 3 not taken.
820 result["effectRunning"] = true;
1499 } else {
1500
2/4
✓ Branch 0 taken 466 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 466 times.
✗ Branch 3 not taken.
466 result["isLocked"] = false; // compatibility
1501
2/4
✓ Branch 0 taken 466 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 466 times.
✗ Branch 3 not taken.
466 result["effectRunning"] = false;
1502 }
1503
2/4
✓ Branch 0 taken 1286 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1286 times.
✗ Branch 3 not taken.
1286 result["width"] = m->getWidth();
1504
2/4
✓ Branch 0 taken 1286 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1286 times.
✗ Branch 3 not taken.
1286 result["height"] = m->getHeight();
1505 }
1506
2/2
✓ Branch 0 taken 1448 times.
✓ Branch 1 taken 171 times.
1619 }
1507
5/6
✓ Branch 0 taken 1788 times.
✓ Branch 1 taken 241 times.
✓ Branch 2 taken 292371 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2103 times.
✓ Branch 5 taken 290268 times.
294400 } else if (p2 == "effects") {
1508 // Registration is the only thing that can change this: the font
1509 // and image lists an effect references are declared as
1510 // contentListUrls the client fetches separately, not inlined here.
1511 // So the effect generation is a complete version, and unlike the
1512 // content hash it was using this skips the build -- ?full=true is
1513 // ~350KB of JSON, which is 0.9-3.4s of a single-core AM62x to
1514 // serialize but only ~10KB to send once gzipped.
1515
2/4
✓ Branch 0 taken 2103 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2103 times.
✗ Branch 3 not taken.
2103 std::string effectsVer = std::to_string(PixelOverlayEffect::GetPixelOverlayEffectsGeneration());
1516
3/4
✓ Branch 0 taken 2103 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 2099 times.
2103 if (etagMatches(req, effectsVer)) {
1517
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 return makeNotModifiedResponse(makeETagToken(req, effectsVer));
1518 }
1519
1/2
✓ Branch 0 taken 2099 times.
✗ Branch 1 not taken.
2099 versionedTag = effectsVer;
1520
3/4
✓ Branch 0 taken 2099 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 29 times.
✓ Branch 3 taken 2070 times.
2099 if (p3 == "") {
1521
3/6
✓ Branch 0 taken 29 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 29 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 29 times.
✗ Branch 5 not taken.
29 bool fullResult = getRequestArg(req, "full") == "true";
1522
3/4
✓ Branch 0 taken 29 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6322 times.
✓ Branch 3 taken 29 times.
6351 for (auto& a : PixelOverlayEffect::GetPixelOverlayEffects()) {
1523
2/2
✓ Branch 0 taken 654 times.
✓ Branch 1 taken 5668 times.
6322 if (fullResult) {
1524
3/6
✓ Branch 0 taken 654 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 654 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 654 times.
✗ Branch 5 not taken.
654 result.append(PixelOverlayEffect::GetPixelOverlayEffect(a)->getDescription());
1525 } else {
1526
2/4
✓ Branch 0 taken 5668 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5668 times.
✗ Branch 3 not taken.
5668 result.append(a);
1527 }
1528 }
1529 } else {
1530
1/2
✓ Branch 0 taken 2070 times.
✗ Branch 1 not taken.
2070 PixelOverlayEffect* e = PixelOverlayEffect::GetPixelOverlayEffect(p3);
1531
2/2
✓ Branch 0 taken 1984 times.
✓ Branch 1 taken 86 times.
2070 if (e) {
1532
1/2
✓ Branch 0 taken 1984 times.
✗ Branch 1 not taken.
1984 result = e->getDescription();
1533 }
1534 }
1535
4/6
✓ Branch 0 taken 2099 times.
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 290268 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 290268 times.
✗ Branch 5 not taken.
292371 } else if (p2 == "running") {
1536
1/2
✓ Branch 0 taken 290268 times.
✗ Branch 1 not taken.
290268 result = getActiveOverlayEffects();
1537 }
1538
2/4
✓ Branch 0 taken 299693 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 299693 times.
✗ Branch 3 not taken.
299693 std::string resultStr = SaveJsonToString(result, "");
1539
2/2
✓ Branch 0 taken 2099 times.
✓ Branch 1 taken 297594 times.
299693 if (!versionedTag.empty()) {
1540
2/4
✓ Branch 0 taken 2099 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2099 times.
✗ Branch 3 not taken.
4198 return makeVersionedETagResponse(req, versionedTag, resultStr);
1541 }
1542
2/2
✓ Branch 0 taken 5510 times.
✓ Branch 1 taken 292084 times.
297594 if (etagable) {
1543
2/4
✓ Branch 0 taken 5510 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5510 times.
✗ Branch 3 not taken.
11020 return makeETagResponse(req, resultStr);
1544 }
1545
2/4
✓ Branch 0 taken 292084 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 292084 times.
✗ Branch 3 not taken.
584168 return makeStringResponse(resultStr, 200, "application/json");
1546 299938 }
1547 ✗ return makeStringResponse("Not found: " + p1, 404);
1548 301728 }
1549 /**
1550 * Replace the overlay model definitions (writes config/model-overlays.json) and
1551 * flag fppd for restart.
1552 *
1553 * @route POST /api/models
1554 * @response 200 Model overlay configuration saved.
1555 */
1556
1557 /**
1558 * Upload a raw channel-memory-map file (writes media/channelmemorymaps) and
1559 * flag fppd for restart.
1560 *
1561 * @route POST /api/models/raw
1562 * @response 200 Raw channel memory map saved.
1563 */
1564 32 HttpResponsePtr PixelOverlayManager::render_POST(const HttpRequestPtr& req) {
1565
2/4
✓ Branch 0 taken 32 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 32 times.
✗ Branch 3 not taken.
32 auto parts = getPathPieces(req->path());
1566
1/2
✓ Branch 0 taken 32 times.
✗ Branch 1 not taken.
32 std::string p1 = parts[0];
1567
2/4
✓ Branch 0 taken 32 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 32 times.
✗ Branch 3 not taken.
32 if (p1 == "models") {
1568
6/10
✓ Branch 0 taken 15 times.
✓ Branch 1 taken 17 times.
✓ Branch 2 taken 15 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 17 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 17 times.
✓ Branch 7 taken 15 times.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
49 std::string p2 = parts.size() > 1 ? parts[1] : "";
1569
3/4
✓ Branch 0 taken 32 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✓ Branch 3 taken 17 times.
32 if (p2 == "raw") {
1570 // upload of raw file
1571 char filename[2048];
1572
2/4
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✗ Branch 3 not taken.
15 strcpy(filename, FPP_DIR_MEDIA("/channelmemorymaps").c_str());
1573
1574
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 int fp = open(filename, O_CREAT | O_TRUNC | O_RDWR, 0666);
1575
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 15 times.
15 if (fp == -1) {
1576 ✗ LogErr(VB_CHANNELOUT, "Could not open Channel Memory Map config file %s\n", filename);
1577 ✗ return makeStringResponse("Could not open Channel Memory Map config file", 500);
1578 }
1579
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 std::string content(getRequestContent(req));
1580
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 write(fp, content.c_str(), content.length());
1581
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 close(fp);
1582
2/4
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✗ Branch 3 not taken.
15 LogDebug(VB_COMMAND, "POST /api/models/raw from %s: saved %zu bytes to %s\n",
1583 getEffectiveClientIP(req).c_str(), content.length(), filename);
1584 15 std::string nvresults;
1585
3/6
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 15 times.
✗ Branch 5 not taken.
60 urlPut("http://127.0.0.1/api/settings/restartFlag", "1", nvresults);
1586
3/6
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 15 times.
✗ Branch 5 not taken.
45 return makeStringResponse("{ \"Status\": \"OK\", \"Message\": \"\"}", 200);
1587
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
32 } else if (parts.size() == 1) {
1588 // Check the shape before replacing the file: whatever lands there
1589 // is loaded at the next start, and a file fppd cannot read leaves
1590 // the box with no models (it used to crash fppd on every boot).
1591
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
17 Json::Value posted;
1592
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
17 std::string body(getRequestContent(req));
1593
9/10
✓ Branch 0 taken 16 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 11 times.
✓ Branch 3 taken 5 times.
✓ Branch 4 taken 11 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 6 times.
✓ Branch 7 taken 5 times.
✓ Branch 8 taken 11 times.
✓ Branch 9 taken 5 times.
23 if (!LoadJsonFromString(body, posted) || !posted.isObject() ||
1594
7/10
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 4 times.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1 times.
✓ Branch 9 taken 1 times.
6 (posted.isMember("models") && !posted["models"].isArray())) {
1595
3/6
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 11 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 11 times.
✗ Branch 5 not taken.
44 return makeStringResponse("{ \"Status\": \"Error\", \"Message\": \"Body must be a JSON object whose \\\"models\\\" member is an array\"}", 400);
1596 }
1597
8/14
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 5 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 300 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 300 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 305 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 300 times.
✓ Branch 13 taken 5 times.
305 for (const auto& m : posted["models"]) {
1598
2/4
✓ Branch 0 taken 300 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 300 times.
300 if (!m.isObject()) {
1599 ✗ return makeStringResponse("{ \"Status\": \"Error\", \"Message\": \"Every entry in \\\"models\\\" must be an object\"}", 400);
1600 }
1601 }
1602 char filename[2048];
1603
3/6
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 5 times.
✗ Branch 5 not taken.
5 strcpy(filename, FPP_DIR_CONFIG("/model-overlays.json").c_str());
1604
1605
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 int fp = open(filename, O_CREAT | O_TRUNC | O_RDWR, 0666);
1606
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
5 if (fp == -1) {
1607 ✗ LogErr(VB_CHANNELOUT, "Could not open Channel Memory Map config file %s\n", filename);
1608 ✗ return makeStringResponse("Could not open Channel Memory Map config file", 500);
1609 }
1610
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 std::string content(getRequestContent(req));
1611
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 write(fp, content.c_str(), content.length());
1612
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 close(fp);
1613
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 LogDebug(VB_COMMAND, "POST /api/models from %s: saved %zu bytes to %s\n",
1614 getEffectiveClientIP(req).c_str(), content.length(), filename);
1615 5 std::string nvresults;
1616
3/6
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 5 times.
✗ Branch 5 not taken.
20 urlPut("http://127.0.0.1/api/settings/restartFlag", "1", nvresults);
1617
3/6
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 5 times.
✗ Branch 5 not taken.
15 return makeStringResponse("{ \"Status\": \"OK\", \"Message\": \"\"}", 200);
1618 18 }
1619
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 31 times.
32 }
1620 ✗ return makeStringResponse("POST Not found " + req->path(), 404);
1621 33 }
1622 /**
1623 * Set an overlay model's active state. Body: `{"State": <int>}`.
1624 * A running effect carries on; a pending Stop Effects is cancelled.
1625 *
1626 * @route PUT /api/overlays/model/{model}/state
1627 * @response 200 State updated.
1628 */
1629
1630 /**
1631 * Fill an overlay model with a solid color. Body: `{"RGB":[r,g,b]}` or `{"Value":v}`.
1632 * Ends any effect running on the model, which would otherwise draw over the fill.
1633 *
1634 * @route PUT /api/overlays/model/{model}/fill
1635 * @response 200 Model filled.
1636 */
1637
1638 /**
1639 * Set a single pixel in an overlay model. Body: `{"X":x,"Y":y,"RGB":[r,g,b]}`.
1640 * A running effect carries on (and draws over it); a pending Stop Effects is cancelled.
1641 *
1642 * @route PUT /api/overlays/model/{model}/pixel
1643 * @response 200 Pixel set.
1644 */
1645
1646 /**
1647 * Write bulk pixel data to an overlay model in a single request, as an
1648 * alternative to one round trip per pixel via `.../pixel`.
1649 *
1650 * The body may be any of three things, and FPP works out which:
1651 *
1652 * - **Raw pixel bytes** (`application/octet-stream`) laid out row-major from
1653 * the top-left of the region, `w * h * fmt` bytes exactly. A wrong length is
1654 * an error, never a partial write.
1655 * - **An encoded image** (PNG, JPEG, GIF, BMP, TIFF, WebP), by `Content-Type:
1656 * image/*` or `fmt=image`. Decoded and scaled server-side, so a 32x32 PNG
1657 * costs a few hundred bytes instead of 3KB and no file has to exist on the
1658 * player first.
1659 * - **JSON** with the payload base64 in a `Data` member:
1660 * `{"X":0,"Y":0,"W":32,"H":32,"Format":"rgb","Data":"..."}`. Members given
1661 * here override the equivalent query arguments.
1662 *
1663 * A region that hangs off an edge is clipped to what is visible rather than
1664 * rejected, so a caller can slide an image across a model (and off it) by
1665 * reposting at moving offsets.
1666 *
1667 * @route PUT /api/overlays/model/{model}/data
1668 * @param integer x Left edge of the region. Default 0; may be negative.
1669 * @param integer y Top edge of the region. Default 0; may be negative.
1670 * @param integer w Region width. Defaults to the model width less `x`. For an image, the box it is scaled into.
1671 * @param integer h Region height. Defaults to the model height less `y`. For an image, the box it is scaled into.
1672 * @param string fmt Raw pixel layout: `rgb` (default), `rgbw`, `mono`, or `image` to force image decoding.
1673 * @param string blend How this write combines with what is there: `opaque` (default), `transparent`, `transparentrgb`.
1674 * @param string scale Image scaling: `fit` (default), `fill`, `stretch`, `none`.
1675 * @param string target `channel` (default) writes the output channel data; `overlay` composites into the mmapped overlay buffer shared with external clients.
1676 * @param string autoEnable Enable the model if it is currently Disabled: `true`, `Enabled`, `Transparent`, or `TransparentRGB`.
1677 * @param boolean stopEffect Evict a running effect on the model first. Default true.
1678 * @response 200 Pixel data written; body reports the region and whether it was clipped.
1679 * @response 400 Malformed arguments, wrong body length, undecodable image, or a region entirely off the model.
1680 * @response 404 No model with that name exists.
1681 */
1682
1683 /**
1684 * Save an overlay model's current buffer to an image file. Body: `{"File":"name"}`.
1685 *
1686 * @route PUT /api/overlays/model/{model}/save
1687 * @response 200 Overlay saved as image.
1688 */
1689
1690 /**
1691 * Render text onto an overlay model. Body includes Message, Color, Font,
1692 * FontSize, Position, PixelsPerSecond, AntiAlias and optional AutoEnable.
1693 *
1694 * @route PUT /api/overlays/model/{model}/text
1695 * @response 200 Text effect started.
1696 */
1697
1698 /**
1699 * Force the overlay buffer to be memory-mapped so external programs can access it.
1700 *
1701 * @route PUT /api/overlays/model/{model}/mmap
1702 * @response 200 Overlay buffer mmapped.
1703 */
1704
1705 /**
1706 * Set, update, or delete active overlay channel ranges. Body: `{"Value":v}`,
1707 * `{"delete":true}`, or `{"deleteAll":true}`.
1708 *
1709 * @route PUT /api/overlays/range/{ranges}
1710 * @response 200 Ranges updated.
1711 */
1712 325273 HttpResponsePtr PixelOverlayManager::render_PUT(const HttpRequestPtr& req) {
1713
2/4
✓ Branch 0 taken 325273 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 325273 times.
✗ Branch 3 not taken.
325273 auto parts = getPathPieces(req->path());
1714
1/2
✓ Branch 0 taken 325273 times.
✗ Branch 1 not taken.
325273 std::string p1 = parts[0];
1715
3/10
✓ Branch 0 taken 325273 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 325273 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 325273 times.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
325273 std::string p2 = parts.size() > 1 ? parts[1] : "";
1716
6/10
✓ Branch 0 taken 325258 times.
✓ Branch 1 taken 15 times.
✓ Branch 2 taken 325258 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 15 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 15 times.
✓ Branch 7 taken 325258 times.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
325288 std::string p3 = parts.size() > 2 ? parts[2] : "";
1717
6/10
✓ Branch 0 taken 322240 times.
✓ Branch 1 taken 3033 times.
✓ Branch 2 taken 322240 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3033 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 3033 times.
✓ Branch 7 taken 322240 times.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
328306 std::string p4 = parts.size() > 3 ? parts[3] : "";
1718
2/4
✓ Branch 0 taken 325273 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 325273 times.
✗ Branch 3 not taken.
325273 if (p1 == "overlays") {
1719
3/4
✓ Branch 0 taken 325273 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 322313 times.
✓ Branch 3 taken 2960 times.
325273 if (p2 == "model") {
1720
1/2
✓ Branch 0 taken 322313 times.
✗ Branch 1 not taken.
322313 std::unique_lock<std::recursive_mutex> lock(modelsLock);
1721 // getModelLocked(), not models.find(): this resolves lazy xLights
1722 // submodels the same way render_GET() does. The raw map lookup used
1723 // here before meant every PUT (state, fill, pixel, ...) 404'd on a
1724 // submodel that GET could see perfectly well.
1725
1/2
✓ Branch 0 taken 322313 times.
✗ Branch 1 not taken.
322313 auto m = getModelLocked(p3);
1726
2/2
✓ Branch 0 taken 266355 times.
✓ Branch 1 taken 55958 times.
322313 if (m) {
1727 // One line per overlay control action so the crash-time log
1728 // ring can show what last changed a model. "pixel" and "data"
1729 // are deliberately excluded: those are the frame-rate paths
1730 // (see docs/PixelOverlayBulkData.md), and a line each would
1731 // fill all 256 ring slots in well under a second. "data"
1732 // logs its own line on VB_CHANNELOUT, which the ring skips
1733 // for exactly this reason.
1734
8/10
✓ Branch 0 taken 266355 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 266340 times.
✓ Branch 3 taken 15 times.
✓ Branch 4 taken 266340 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 221941 times.
✓ Branch 7 taken 44399 times.
✓ Branch 8 taken 221941 times.
✓ Branch 9 taken 44414 times.
266355 if (p4 != "pixel" && p4 != "data") {
1735
2/4
✓ Branch 0 taken 221941 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 221941 times.
✗ Branch 3 not taken.
221941 LogDebug(VB_COMMAND, "PUT /api/overlays/model/%s/%s from %s\n",
1736 m->getName().c_str(), p4.c_str(),
1737 getEffectiveClientIP(req).c_str());
1738 }
1739
3/4
✓ Branch 0 taken 266355 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 220539 times.
✓ Branch 3 taken 45816 times.
266355 if (p4 == "state") {
1740
1/2
✓ Branch 0 taken 220539 times.
✗ Branch 1 not taken.
220539 Json::Value root;
1741
5/6
✓ Branch 0 taken 220539 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 220538 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 220533 times.
✓ Branch 5 taken 5 times.
220540 if (LoadJsonFromString(std::string(getRequestContent(req)), root)) {
1742
4/4
✓ Branch 0 taken 220529 times.
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 220524 times.
✓ Branch 3 taken 5 times.
220533 if (root.isMember("State")) {
1743
1/2
✓ Branch 0 taken 220524 times.
✗ Branch 1 not taken.
220524 retireEffectFor(m, true);
1744
3/6
✓ Branch 0 taken 220524 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 220524 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 220524 times.
✗ Branch 5 not taken.
220524 m->setState(PixelOverlayState(root["State"].asInt()));
1745
3/6
✓ Branch 0 taken 220524 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 220524 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 220524 times.
✗ Branch 5 not taken.
882096 return makeStringResponse("{ \"Status\": \"OK\", \"Message\": \"\"}", 200);
1746 } else {
1747
4/8
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 5 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 5 times.
✗ Branch 7 not taken.
10 return makeStringResponse("Invalid request " + std::string(getRequestContent(req)), 500);
1748 }
1749 } else {
1750
4/8
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 5 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 5 times.
✗ Branch 7 not taken.
10 return makeStringResponse("Could not parse request " + std::string(getRequestContent(req)), 500);
1751 }
1752
3/4
✓ Branch 0 taken 45816 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1249 times.
✓ Branch 3 taken 44567 times.
266355 } else if (p4 == "fill") {
1753
1/2
✓ Branch 0 taken 1249 times.
✗ Branch 1 not taken.
1249 Json::Value root;
1754
5/6
✓ Branch 0 taken 1249 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1248 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 1243 times.
✓ Branch 5 taken 5 times.
1250 if (LoadJsonFromString(std::string(getRequestContent(req)), root)) {
1755
4/4
✓ Branch 0 taken 1239 times.
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 1233 times.
✓ Branch 3 taken 6 times.
1243 if (root.isMember("RGB")) {
1756
1/2
✓ Branch 0 taken 1233 times.
✗ Branch 1 not taken.
1233 retireEffectFor(m, false);
1757
4/8
✓ Branch 0 taken 1233 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1233 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1233 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1233 times.
✗ Branch 7 not taken.
1233 m->fill(root["RGB"][0].asInt(),
1758
3/6
✓ Branch 0 taken 1233 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1233 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1233 times.
✗ Branch 5 not taken.
1233 root["RGB"][1].asInt(),
1759
3/6
✓ Branch 0 taken 1233 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1233 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1233 times.
✗ Branch 5 not taken.
1233 root["RGB"][2].asInt());
1760
1/2
✓ Branch 0 taken 1233 times.
✗ Branch 1 not taken.
1233 m->flushOverlayBuffer();
1761
3/6
✓ Branch 0 taken 1233 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1233 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1233 times.
✗ Branch 5 not taken.
4932 return makeStringResponse("{ \"Status\": \"OK\", \"Message\": \"\"}", 200);
1762
3/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 5 times.
6 } else if (root.isMember("Value")) {
1763
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 retireEffectFor(m, false);
1764
3/6
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
1 m->fill(root["Value"].asInt(),
1765
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 root["Value"].asInt(),
1766
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 root["Value"].asInt());
1767
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 return makeStringResponse("{ \"Status\": \"OK\", \"Message\": \"\"}", 200);
1768 }
1769 }
1770
5/6
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 1234 times.
✓ Branch 2 taken 44567 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 15 times.
✓ Branch 5 taken 44552 times.
45816 } else if (p4 == "pixel") {
1771
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 Json::Value root;
1772
5/6
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 9 times.
✓ Branch 5 taken 5 times.
16 if (LoadJsonFromString(std::string(getRequestContent(req)), root)) {
1773
3/4
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 4 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 5 times.
9 if (root.isMember("RGB")) {
1774 ✗ retireEffectFor(m, true);
1775 ✗ m->setPixelValue(root["X"].asInt(),
1776 ✗ root["Y"].asInt(),
1777 ✗ root["RGB"][0].asInt(),
1778 ✗ root["RGB"][1].asInt(),
1779 ✗ root["RGB"][2].asInt());
1780 ✗ return makeStringResponse("{ \"Status\": \"OK\", \"Message\": \"\"}", 200);
1781 }
1782 }
1783
4/6
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 44552 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 44399 times.
✓ Branch 5 taken 153 times.
44567 } else if (p4 == "data") {
1784 // Takes over `lock` and releases it before the slow work,
1785 // so the model has to be pinned for the rest of the call:
1786 // an output reload can remove it meanwhile.
1787
1/2
✓ Branch 0 taken 44399 times.
✗ Branch 1 not taken.
44399 std::shared_ptr<PixelOverlayModel> pin = models[m->getName()].owner;
1788
1/2
✓ Branch 0 taken 44399 times.
✗ Branch 1 not taken.
44399 return bulkPixelDataPut(req, m, lock);
1789
3/4
✓ Branch 0 taken 153 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✓ Branch 3 taken 136 times.
44552 } else if (p4 == "save") {
1790
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
17 Json::Value root;
1791
5/6
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 16 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 11 times.
✓ Branch 5 taken 5 times.
18 if (LoadJsonFromString(std::string(getRequestContent(req)), root)) {
1792
4/4
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 5 times.
11 if (root.isMember("File")) {
1793
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 std::string file = root["File"].asString();
1794
7/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 1 times.
✓ Branch 7 taken 1 times.
✓ Branch 8 taken 1 times.
✓ Branch 9 taken 1 times.
2 if (file != "" && !PixelOverlayModel::isPlainImageFileName(file)) {
1795
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 return makeStringResponse("{ \"Status\": \"Error\", \"Message\": \"File must be a plain file name\"}", 400);
1796 }
1797
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 m->saveOverlayAsImage(file);
1798
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 return makeStringResponse("{ \"Status\": \"OK\", \"Message\": \"\"}", 200);
1799 2 }
1800 }
1801
5/6
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 136 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 15 times.
✓ Branch 5 taken 121 times.
153 } else if (p4 == "text") {
1802
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 Json::Value root;
1803
5/6
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 9 times.
✓ Branch 5 taken 5 times.
16 if (LoadJsonFromString(std::string(getRequestContent(req)), root)) {
1804
4/4
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 4 times.
9 if (root.isMember("Message")) {
1805
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 std::string color = root["Color"].asString();
1806
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 unsigned int x = mapColor(color);
1807
1808
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 std::string msg = root["Message"].asString();
1809
1810
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 std::string font = root["Font"].asString();
1811
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 std::string position = root["Position"].asString();
1812
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 int fontSize = root["FontSize"].asInt();
1813
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 bool aa = root["AntiAlias"].asBool();
1814
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 int pps = root["PixelsPerSecond"].asInt();
1815 1 bool autoEnable = false;
1816
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
1 if (root.isMember("AutoEnable")) {
1817 ✗ autoEnable = root["AutoEnable"].asBool();
1818 }
1819
1820 1 std::vector<std::string> args;
1821
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 args.push_back(p3);
1822
3/6
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
2 args.push_back(autoEnable ? "true" : "false");
1823
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 args.push_back("Text");
1824
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 args.push_back(color);
1825
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 args.push_back(font);
1826
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 args.push_back(std::to_string(fontSize));
1827
3/6
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
1 args.push_back(aa ? "true" : "false");
1828
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 args.push_back(position);
1829
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 args.push_back(std::to_string(pps));
1830
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 args.push_back("0");
1831
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 args.push_back(msg);
1832
1833 // Optional colour-treatment arguments, in the order
1834 // the Text effect declares them after Text. They
1835 // are positional, so a body that sets a later one
1836 // without an earlier one still has to push the
1837 // earlier one's default -- which is what the loop
1838 // does, stopping as soon as nothing further is
1839 // named so a body that mentions none of them sends
1840 // the same ten arguments it always has.
1841 static const struct {
1842 const char* key;
1843 const char* dflt;
1844 } TEXT_COLOR_ARGS[] = {
1845 { "ColorMode", "Single" },
1846 { "Palette", "Custom" },
1847 { "NumColors", "2" },
1848 { "Color2", "#0000FF" },
1849 { "Color3", "#00FF00" },
1850 { "Color4", "#FFFF00" },
1851 { "Color5", "#FF00FF" },
1852 { "ColorSpeed", "0" }
1853 };
1854 1 int lastNamed = -1;
1855
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 1 times.
9 for (int ci = 0; ci < (int)(sizeof(TEXT_COLOR_ARGS) / sizeof(TEXT_COLOR_ARGS[0])); ci++) {
1856
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 8 times.
8 if (root.isMember(TEXT_COLOR_ARGS[ci].key)) {
1857 ✗ lastNamed = ci;
1858 }
1859 }
1860
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 for (int ci = 0; ci <= lastNamed; ci++) {
1861 ✗ const char* key = TEXT_COLOR_ARGS[ci].key;
1862 ✗ args.push_back(root.isMember(key) ? root[key].asString()
1863 ✗ : TEXT_COLOR_ARGS[ci].dflt);
1864 }
1865
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 lock.unlock();
1866
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 LogDebug(VB_COMMAND, "PixelOverlay HTTP API from %s running \"Overlay Model Effect\" on model \"%s\"\n", getEffectiveClientIP(req).c_str(), p3.c_str());
1867
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 CommandManager::INSTANCE.run("Overlay Model Effect", args);
1868
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.
3 return makeStringResponse("{ \"Status\": \"OK\", \"Message\": \"\"}", 200);
1869 1 }
1870 }
1871
5/6
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 121 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 16 times.
✓ Branch 5 taken 105 times.
136 } else if (p4 == "mmap") {
1872 // Force mmap the overlay buffer so external programs can have access to it
1873
2/4
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 16 times.
16 if (!m->getOverlayBuffer()) {
1874 ✗ return makeStringResponse("{ \"Status\": \"ERROR\", \"Message\": \"Could not allocate the overlay buffer\"}", 500);
1875 }
1876
3/6
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 16 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 16 times.
✗ Branch 5 not taken.
64 return makeStringResponse("{ \"Status\": \"OK\", \"Message\": \"\"}", 200);
1877 } else {
1878
3/6
✓ Branch 0 taken 105 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 105 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 105 times.
✗ Branch 5 not taken.
210 return makeStringResponse("Model Command Not found " + p4, 404);
1879 }
1880 } else {
1881
3/6
✓ Branch 0 taken 55958 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 55958 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 55958 times.
✗ Branch 5 not taken.
111916 return makeStringResponse("Model Not found " + p3, 404);
1882 }
1883
4/6
✓ Branch 0 taken 39 times.
✓ Branch 1 taken 322249 times.
✓ Branch 2 taken 2960 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2960 times.
✗ Branch 5 not taken.
325273 } else if (p2 == "range") {
1884 // Same reasoning as the model branch above; a range PUT is a
1885 // one-shot control action, not a per-frame one.
1886
2/4
✓ Branch 0 taken 2960 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2960 times.
✗ Branch 3 not taken.
2960 LogDebug(VB_COMMAND, "PUT /api/overlays/range/%s/%s from %s\n",
1887 p3.c_str(), p4.c_str(), getEffectiveClientIP(req).c_str());
1888 2960 int val = -1;
1889 2960 bool deleteAll = false;
1890
3/4
✓ Branch 0 taken 2960 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2928 times.
✓ Branch 3 taken 32 times.
2960 if (p4 == "") {
1891 // The body has to be a JSON object. Anything else used to fall
1892 // through as "no Value" -- a delete -- so a garbled request
1893 // removed the range and answered OK; an array threw from
1894 // isMember() and answered an empty 500.
1895
1/2
✓ Branch 0 taken 2928 times.
✗ Branch 1 not taken.
2928 Json::Value root;
1896
5/6
✓ Branch 0 taken 2928 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2912 times.
✓ Branch 3 taken 16 times.
✓ Branch 4 taken 146 times.
✓ Branch 5 taken 2766 times.
2944 if (!LoadJsonFromString(std::string(getRequestContent(req)), root, JsonRoot::Object)) {
1897
3/6
✓ Branch 0 taken 146 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 146 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 146 times.
✗ Branch 5 not taken.
584 return makeStringResponse("{ \"Status\": \"Error\", \"Message\": \"Request body is not a JSON object\"}", 400);
1898 }
1899
8/12
✓ Branch 0 taken 2766 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 2765 times.
✓ 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 taken 2765 times.
2766 if (root.isMember("delete") && root["delete"].asBool()) {
1900 1 val = -1;
1901
8/12
✓ Branch 0 taken 2765 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1183 times.
✓ Branch 3 taken 1582 times.
✓ Branch 4 taken 1183 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1183 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1183 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1183 times.
✓ Branch 11 taken 1582 times.
2765 } else if (root.isMember("deleteAll") && root["deleteAll"].asBool()) {
1902
1/2
✓ Branch 0 taken 1183 times.
✗ Branch 1 not taken.
1183 std::unique_lock<std::recursive_mutex> lock(activeModelsLock);
1903 1183 activeRanges.clear();
1904
1/2
✓ Branch 0 taken 1183 times.
✗ Branch 1 not taken.
1183 updateActiveCountLocked();
1905
1/2
✓ Branch 0 taken 1183 times.
✗ Branch 1 not taken.
1183 lock.unlock();
1906
2/2
✓ Branch 0 taken 23 times.
✓ Branch 1 taken 1160 times.
1183 if (numActive == 0) {
1907
1/2
✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
23 holdOutputBriefly();
1908 }
1909
3/6
✓ Branch 0 taken 1183 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1183 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1183 times.
✗ Branch 5 not taken.
3549 return makeStringResponse("{ \"Status\": \"OK\", \"Message\": \"\"}", 200);
1910
3/4
✓ Branch 0 taken 1582 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1502 times.
✓ Branch 3 taken 80 times.
2765 } else if (root.isMember("Value")) {
1911
4/6
✓ Branch 0 taken 1502 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1502 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 1501 times.
1502 if (!root["Value"].isNumeric()) {
1912
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 return makeStringResponse("{ \"Status\": \"Error\", \"Message\": \"Value must be a number\"}", 400);
1913 }
1914
2/4
✓ Branch 0 taken 1501 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1501 times.
✗ Branch 3 not taken.
1501 val = root["Value"].asInt();
1915 }
1916
5/6
✓ Branch 0 taken 1582 times.
✓ Branch 1 taken 1330 times.
✓ Branch 2 taken 32 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 31 times.
✓ Branch 5 taken 1 times.
2960 } else if (p4 != "delete") {
1917 // a value in the path: a number, or the request is refused
1918 // (std::stoi threw on anything else, answering a 500)
1919 31 char* endp = nullptr;
1920 31 long v = strtol(p4.c_str(), &endp, 10);
1921
3/10
✓ Branch 0 taken 31 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 31 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✓ Branch 8 taken 31 times.
✗ Branch 9 not taken.
31 if (p4.empty() || endp != p4.c_str() + p4.size() || v < 0 || v > 255) {
1922
3/6
✓ Branch 0 taken 31 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 31 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 31 times.
✗ Branch 5 not taken.
124 return makeStringResponse("{ \"Status\": \"Error\", \"Message\": \"Value must be 0-255\"}", 400);
1923 }
1924 ✗ val = (int)v;
1925 }
1926
1927 // Parse every range before applying any: a syntax error part way
1928 // along the list threw from std::stoi after the earlier ranges had
1929 // already been changed, and answered a 500.
1930 1583 std::vector<std::pair<int, int>> ranges;
1931 {
1932 1583 const char* c = p3.c_str();
1933
2/2
✓ Branch 0 taken 1574 times.
✓ Branch 1 taken 1542 times.
3116 while (*c) {
1934 1574 char* endp = nullptr;
1935 1574 long start = strtol(c, &endp, 10);
1936
2/2
✓ Branch 0 taken 35 times.
✓ Branch 1 taken 1539 times.
1574 if (endp == c) {
1937
3/6
✓ Branch 0 taken 35 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 35 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 35 times.
✗ Branch 5 not taken.
140 return makeStringResponse("{ \"Status\": \"Error\", \"Message\": \"Could not parse the channel ranges\"}", 400);
1938 }
1939 1539 long end = start;
1940 1539 c = endp;
1941
2/2
✓ Branch 0 taken 1523 times.
✓ Branch 1 taken 16 times.
1539 if (*c == '-') {
1942 1523 end = strtol(c + 1, &endp, 10);
1943
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1523 times.
1523 if (endp == c + 1) {
1944 ✗ return makeStringResponse("{ \"Status\": \"Error\", \"Message\": \"Could not parse the channel ranges\"}", 400);
1945 }
1946 1523 c = endp;
1947 }
1948
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1538 times.
1539 if (*c == ',') {
1949 1 c++;
1950
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 1532 times.
1538 } else if (*c) {
1951
3/6
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 6 times.
✗ Branch 5 not taken.
24 return makeStringResponse("{ \"Status\": \"Error\", \"Message\": \"Could not parse the channel ranges\"}", 400);
1952 }
1953
1/2
✓ Branch 0 taken 1533 times.
✗ Branch 1 not taken.
1533 ranges.emplace_back((int)std::clamp(start, (long)INT_MIN, (long)INT_MAX),
1954
2/4
✓ Branch 0 taken 1533 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1533 times.
✗ Branch 3 not taken.
3065 (int)std::clamp(end, (long)INT_MIN, (long)INT_MAX));
1955 }
1956
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 1537 times.
1542 if (c != p3.c_str() + p3.size()) {
1957 // an embedded NUL ended the walk early
1958
3/6
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 5 times.
✗ Branch 5 not taken.
20 return makeStringResponse("{ \"Status\": \"Error\", \"Message\": \"Could not parse the channel ranges\"}", 400);
1959 }
1960 }
1961
1962 1537 bool badRange = false;
1963
2/2
✓ Branch 0 taken 1532 times.
✓ Branch 1 taken 1537 times.
3069 for (auto [start, end] : ranges) {
1964 1532 int startActive = numActive;
1965 // doOverlays() writes every channel of the range into the
1966 // sequence buffer on each frame, so a range past its end was
1967 // a heap overwrite on the output thread.
1968
6/6
✓ Branch 0 taken 1517 times.
✓ Branch 1 taken 15 times.
✓ Branch 2 taken 1512 times.
✓ Branch 3 taken 5 times.
✓ Branch 4 taken 11 times.
✓ Branch 5 taken 1501 times.
1532 if (start <= 0 || end < start || end > FPPD_MAX_CHANNELS) {
1969
1/2
✓ Branch 0 taken 31 times.
✗ Branch 1 not taken.
31 LogWarn(VB_CHANNELOUT, "Ignoring overlay range %d-%d: channels must be 1-%d\n",
1970 start, end, FPPD_MAX_CHANNELS);
1971 31 badRange = true;
1972 } else {
1973 1501 end--;
1974 1501 start--;
1975
1/2
✓ Branch 0 taken 1501 times.
✗ Branch 1 not taken.
1501 std::unique_lock<std::recursive_mutex> lock(activeModelsLock);
1976 1501 auto it = activeRanges.begin();
1977 1501 bool found = false;
1978
2/2
✓ Branch 0 taken 8913 times.
✓ Branch 1 taken 1501 times.
10414 while (it != activeRanges.end()) {
1979
6/6
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 8899 times.
✓ Branch 2 taken 6 times.
✓ Branch 3 taken 8 times.
✓ Branch 4 taken 6 times.
✓ Branch 5 taken 8907 times.
8913 if (it->start == start && it->end == end) {
1980 6 found = true;
1981
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 1 times.
6 if (val >= 0) {
1982 5 it->value = val;
1983 5 it++;
1984 } else {
1985 1 activeRanges.erase(it);
1986 1 it = activeRanges.begin();
1987 }
1988 } else {
1989 8907 it++;
1990 }
1991 }
1992 // val < 0 is a delete (or a PUT with no usable Value); a
1993 // range that was not there stays absent rather than being
1994 // added with -1, which wrote 0xFF - every channel full on.
1995
4/4
✓ Branch 0 taken 1495 times.
✓ Branch 1 taken 6 times.
✓ Branch 2 taken 1494 times.
✓ Branch 3 taken 1 times.
1501 if (!found && val >= 0) {
1996
1/2
✓ Branch 0 taken 1494 times.
✗ Branch 1 not taken.
1494 activeRanges.push_back(OverlayRange(start, end, val));
1997 }
1998
1/2
✓ Branch 0 taken 1501 times.
✗ Branch 1 not taken.
1501 updateActiveCountLocked();
1999 1501 }
2000
6/6
✓ Branch 0 taken 32 times.
✓ Branch 1 taken 1500 times.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 31 times.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 1531 times.
1532 if (numActive == 0 && startActive) {
2001 // removed some, may need to wait to make sure the new values are output
2002
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 holdOutputBriefly();
2003 }
2004 }
2005
2/2
✓ Branch 0 taken 1500 times.
✓ Branch 1 taken 37 times.
1537 if (numActive > 0) {
2006 // As modelStateChanged() does for a model. The output thread
2007 // idles only past a start request, so a range set while it
2008 // waited out its 2.5 s idle wait (as it does just after fppd
2009 // starts) went unsent until the wait timed out, the thread
2010 // stopped outputs, and the main loop started it again.
2011
1/2
✓ Branch 0 taken 1500 times.
✗ Branch 1 not taken.
1500 StartChannelOutputThread();
2012 }
2013
2/2
✓ Branch 0 taken 31 times.
✓ Branch 1 taken 1506 times.
1537 if (badRange) {
2014
3/6
✓ Branch 0 taken 31 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 31 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 31 times.
✗ Branch 5 not taken.
124 return makeStringResponse("{ \"Status\": \"Error\", \"Message\": \"Channel range out of bounds\"}", 400);
2015 }
2016
3/6
✓ Branch 0 taken 1506 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1506 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1506 times.
✗ Branch 5 not taken.
6024 return makeStringResponse("{ \"Status\": \"OK\", \"Message\": \"\"}", 200);
2017 1583 }
2018 }
2019
4/8
✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 39 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 39 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 39 times.
✗ Branch 7 not taken.
78 return makeStringResponse("PUT Not found " + req->path(), 404);
2020 325437 }
2021
2022 class OverlayCommand : public Command {
2023 public:
2024 9435 OverlayCommand(const std::string& s, PixelOverlayManager* m) :
2025 Command(s),
2026 9435 manager(m) {}
2027 18670 virtual ~OverlayCommand() {}
2028
2029 72692 std::recursive_mutex& getLock() { return manager->modelsLock; }
2030 72692 PixelOverlayModel* getModelLocked(const std::string& name) {
2031 72692 return manager->getModelLocked(name);
2032 }
2033
2034 PixelOverlayManager* manager;
2035 };
2036
2037 class EnableOverlayCommand : public OverlayCommand {
2038 public:
2039 1887 EnableOverlayCommand(PixelOverlayManager* m) :
2040
2/4
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1887 times.
✗ Branch 3 not taken.
3774 OverlayCommand("Overlay Model State", m) {
2041
7/14
✓ 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.
✓ Branch 8 taken 1887 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1887 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 1887 times.
✗ Branch 13 not taken.
15096 args.push_back(CommandArg("Model", "multistring", "Model").setContentListUrl("api/models?simple=true", false));
2042
6/12
✓ 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.
✓ Branch 8 taken 1887 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1887 times.
✗ Branch 11 not taken.
15096 args.push_back(CommandArg("State", "string", "State").setContentList({ "Disabled", "Enabled", "Transparent", "TransparentRGB" }));
2043 1887 }
2044
2045 41 virtual std::unique_ptr<Command::Result> run(const std::vector<std::string>& args) override {
2046
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 41 times.
41 if (args.size() != 2) {
2047 ✗ return std::make_unique<Command::ErrorResult>("Command needs 2 arguments, found " + std::to_string(args.size()));
2048 }
2049
1/2
✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
41 std::unique_lock<std::recursive_mutex> lock(getLock());
2050 41 std::list<PixelOverlayModel*> models;
2051
3/4
✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 41 times.
✓ Branch 3 taken 41 times.
82 for (auto& ms : split(args[0], ',')) {
2052
1/2
✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
41 auto m = getModelLocked(ms);
2053
1/2
✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
41 if (m) {
2054
1/2
✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
41 models.push_back(m);
2055 } else {
2056 ✗ return std::make_unique<Command::ErrorResult>("No model found: " + ms);
2057 }
2058
1/2
✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
41 }
2059
2/2
✓ Branch 0 taken 41 times.
✓ Branch 1 taken 41 times.
82 for (auto m : models) {
2060
1/2
✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
41 retireEffectFor(m, true);
2061
2/4
✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 41 times.
✗ Branch 3 not taken.
41 m->setState(PixelOverlayState(args[1]));
2062 }
2063
1/2
✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
41 return std::make_unique<Command::Result>("Model State Set");
2064 41 }
2065 };
2066 class ClearOverlayCommand : public OverlayCommand {
2067 public:
2068 1887 ClearOverlayCommand(PixelOverlayManager* m) :
2069
2/4
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1887 times.
✗ Branch 3 not taken.
3774 OverlayCommand("Overlay Model Clear", m) {
2070
7/14
✓ 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.
✓ Branch 8 taken 1887 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1887 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 1887 times.
✗ Branch 13 not taken.
15096 args.push_back(CommandArg("Model", "multistring", "Model").setContentListUrl("api/models?simple=true", false));
2071 1887 }
2072
2073 7 virtual std::unique_ptr<Command::Result> run(const std::vector<std::string>& args) override {
2074
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 7 times.
7 if (args.size() != 1) {
2075 ✗ return std::make_unique<Command::ErrorResult>("Command needs 1 argument, found " + std::to_string(args.size()));
2076 }
2077
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 std::unique_lock<std::recursive_mutex> lock(getLock());
2078 7 std::list<PixelOverlayModel*> models;
2079
3/4
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✓ Branch 3 taken 7 times.
14 for (auto& ms : split(args[0], ',')) {
2080
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 auto m = getModelLocked(ms);
2081
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 if (m) {
2082
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 models.push_back(m);
2083 } else {
2084 ✗ return std::make_unique<Command::ErrorResult>("No model found: " + ms);
2085 }
2086
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 }
2087
2/2
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 7 times.
14 for (auto m : models) {
2088
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 retireEffectFor(m, false);
2089
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 m->clear();
2090 }
2091
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 return std::make_unique<Command::Result>("Models Cleared");
2092 7 }
2093 };
2094 class FillOverlayCommand : public OverlayCommand {
2095 public:
2096 1887 FillOverlayCommand(PixelOverlayManager* m) :
2097
2/4
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1887 times.
✗ Branch 3 not taken.
3774 OverlayCommand("Overlay Model Fill", m) {
2098
7/14
✓ 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.
✓ Branch 8 taken 1887 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1887 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 1887 times.
✗ Branch 13 not taken.
15096 args.push_back(CommandArg("Model", "multistring", "Model").setContentListUrl("api/models?simple=true", false));
2099
6/12
✓ 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.
✓ Branch 8 taken 1887 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1887 times.
✗ Branch 11 not taken.
15096 args.push_back(CommandArg("State", "string", "State").setContentList({ "Don't Set", "Enabled", "Transparent", "TransparentRGB" }));
2100
6/12
✓ 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.
✓ Branch 8 taken 1887 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1887 times.
✗ Branch 11 not taken.
11322 args.push_back(CommandArg("Color", "color", "Color").setDefaultValue("#FF0000"));
2101 1887 }
2102
2103 34792 virtual std::unique_ptr<Command::Result> run(const std::vector<std::string>& args) override {
2104
3/6
✓ Branch 0 taken 34792 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 34792 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 34792 times.
34792 if (args.size() < 2 || args.size() > 3) {
2105 ✗ return std::make_unique<Command::ErrorResult>("Command needs 2 or 3 arguments, found " + std::to_string(args.size()));
2106 }
2107
1/2
✓ Branch 0 taken 34792 times.
✗ Branch 1 not taken.
34792 std::unique_lock<std::recursive_mutex> lock(getLock());
2108 34792 std::list<PixelOverlayModel*> models;
2109
3/4
✓ Branch 0 taken 34792 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 34792 times.
✓ Branch 3 taken 34787 times.
69579 for (auto& ms : split(args[0], ',')) {
2110
1/2
✓ Branch 0 taken 34792 times.
✗ Branch 1 not taken.
34792 auto m = getModelLocked(ms);
2111
2/2
✓ Branch 0 taken 34787 times.
✓ Branch 1 taken 5 times.
34792 if (m) {
2112
1/2
✓ Branch 0 taken 34787 times.
✗ Branch 1 not taken.
34787 models.push_back(m);
2113 } else {
2114
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 return std::make_unique<Command::ErrorResult>("No model found: " + ms);
2115 }
2116
2/2
✓ Branch 0 taken 34787 times.
✓ Branch 1 taken 5 times.
34792 }
2117
2/2
✓ Branch 0 taken 34787 times.
✓ Branch 1 taken 34787 times.
69574 for (auto m : models) {
2118 34787 std::string color;
2119
1/2
✓ Branch 0 taken 34787 times.
✗ Branch 1 not taken.
34787 std::string state = "Don't Set";
2120
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 34787 times.
34787 if (args.size() == 2) {
2121 ✗ color = args[1];
2122 } else {
2123
1/2
✓ Branch 0 taken 34787 times.
✗ Branch 1 not taken.
34787 state = args[1];
2124
1/2
✓ Branch 0 taken 34787 times.
✗ Branch 1 not taken.
34787 color = args[2];
2125 }
2126
1/2
✓ Branch 0 taken 34787 times.
✗ Branch 1 not taken.
34787 retireEffectFor(m, false);
2127
2/4
✓ Branch 0 taken 34787 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 34787 times.
✗ Branch 3 not taken.
34787 if (state != "Don't Set") {
2128
2/4
✓ Branch 0 taken 34787 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 34787 times.
✗ Branch 3 not taken.
34787 m->setState(PixelOverlayState(state));
2129 }
2130
5/6
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 34786 times.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 34786 times.
34787 if (color.size() == 9 && color[0] == '#') {
2131 // #RRGGBBWW - explicit white channel for RGBW models
2132
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 uint32_t x = PixelOverlayManager::mapColor(color);
2133 1 m->fillOverlayBuffer((x >> 24) & 0xFF,
2134 1 (x >> 16) & 0xFF,
2135 1 (x >> 8) & 0xFF,
2136
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 x & 0xFF);
2137 } else {
2138
1/2
✓ Branch 0 taken 34786 times.
✗ Branch 1 not taken.
34786 unsigned int x = PixelOverlayManager::mapColor(color);
2139 34786 m->fillOverlayBuffer((x >> 16) & 0xFF,
2140 34786 (x >> 8) & 0xFF,
2141
1/2
✓ Branch 0 taken 34786 times.
✗ Branch 1 not taken.
34786 x & 0xFF);
2142 }
2143
1/2
✓ Branch 0 taken 34787 times.
✗ Branch 1 not taken.
34787 m->flushOverlayBuffer();
2144 34787 }
2145
1/2
✓ Branch 0 taken 34787 times.
✗ Branch 1 not taken.
34787 return std::make_unique<Command::Result>("Models Filled");
2146 34792 }
2147 };
2148 class TextOverlayCommand : public OverlayCommand {
2149 public:
2150 1887 TextOverlayCommand(PixelOverlayManager* m) :
2151
2/4
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1887 times.
✗ Branch 3 not taken.
3774 OverlayCommand("Overlay Model Text", m) {
2152
7/14
✓ 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.
✓ Branch 8 taken 1887 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1887 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 1887 times.
✗ Branch 13 not taken.
15096 args.push_back(CommandArg("Model", "string", "Model").setContentListUrl("api/models?simple=true", false));
2153
6/12
✓ 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.
✓ Branch 8 taken 1887 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1887 times.
✗ Branch 11 not taken.
11322 args.push_back(CommandArg("Color", "color", "Color").setDefaultValue("#FF0000"));
2154
7/14
✓ 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.
✓ Branch 8 taken 1887 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1887 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 1887 times.
✗ Branch 13 not taken.
15096 args.push_back(CommandArg("Font", "string", "Font").setContentListUrl("api/overlays/fonts", false));
2155
6/12
✓ 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.
✓ Branch 8 taken 1887 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1887 times.
✗ Branch 11 not taken.
11322 args.push_back(CommandArg("FontSize", "int", "FontSize").setRange(4, 100).setDefaultValue("18"));
2156
6/12
✓ 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.
✓ Branch 8 taken 1887 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1887 times.
✗ Branch 11 not taken.
11322 args.push_back(CommandArg("FontAntiAlias", "bool", "Anti-Aliased").setDefaultValue("false"));
2157
6/12
✓ 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.
✓ Branch 8 taken 1887 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1887 times.
✗ Branch 11 not taken.
15096 args.push_back(CommandArg("Position", "string", "Position").setContentList({ "Center", "Right to Left", "Left to Right", "Bottom to Top", "Top to Bottom" }));
2158
6/12
✓ 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.
✓ Branch 8 taken 1887 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1887 times.
✗ Branch 11 not taken.
11322 args.push_back(CommandArg("Speed", "int", "Scroll Speed").setRange(0, 200).setDefaultValue("10"));
2159
6/12
✓ 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.
✓ Branch 8 taken 1887 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1887 times.
✗ Branch 11 not taken.
11322 args.push_back(CommandArg("AutoEnable", "bool", "Auto Enable/Disable Model").setDefaultValue("false"));
2160
2161 // keep text as last argument if possible as the MQTT commands will, by default, use the payload of the mqtt
2162 // msg as the last argument. Thus, this allows all of the above to be topic paths, but the text to be
2163 // sent in the payload
2164
5/10
✓ 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.
✓ Branch 8 taken 1887 times.
✗ Branch 9 not taken.
11322 args.push_back(CommandArg("Text", "string", "Text").setAdjustable());
2165 1887 }
2166 60 virtual bool hidden() const override { return true; }
2167
2168 1 virtual std::unique_ptr<Command::Result> run(const std::vector<std::string>& args) override {
2169
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (args.size() < 8) {
2170 ✗ return std::make_unique<Command::ErrorResult>("Command needs 9 arguments, found " + std::to_string(args.size()));
2171 }
2172 1 std::vector<std::string> newArgs;
2173
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 newArgs.push_back(args[0]); // model
2174
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (args.size() == 9) {
2175
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 newArgs.push_back(args[7]);
2176 } else {
2177 ✗ newArgs.push_back("true");
2178 }
2179
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 newArgs.push_back("Text");
2180
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 1 times.
7 for (int x = 1; x < 7; x++) {
2181
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 newArgs.push_back(args[x]);
2182 }
2183
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 newArgs.push_back("0"); // duration
2184
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (args.size() == 9) {
2185
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 newArgs.push_back(args[8]);
2186 } else {
2187 ✗ newArgs.push_back(args[7]);
2188 }
2189
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogDebug(VB_COMMAND, "Legacy command \"%s\" translating to \"Overlay Model Effect\"\n", name.c_str());
2190
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 return CommandManager::INSTANCE.run("Overlay Model Effect", newArgs);
2191 1 }
2192 };
2193
2194 class ApplyEffectOverlayCommand : public OverlayCommand {
2195 public:
2196 1887 ApplyEffectOverlayCommand(PixelOverlayManager* m) :
2197
2/4
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1887 times.
✗ Branch 3 not taken.
3774 OverlayCommand("Overlay Model Effect", m) {
2198
7/14
✓ 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.
✓ Branch 8 taken 1887 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1887 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 1887 times.
✗ Branch 13 not taken.
15096 args.push_back(CommandArg("Models", "multistring", "Models").setContentListUrl("api/models?simple=true&all=true", false));
2199
7/14
✓ 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.
✓ Branch 8 taken 1887 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1887 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 1887 times.
✗ Branch 13 not taken.
15096 args.push_back(CommandArg("AutoEnable", "string", "Auto Enable/Disable").setContentList({ "False", "Enabled", "Transparent", "Transparent RGB" }).setDefaultValue("Enabled"));
2200
7/14
✓ 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.
✓ Branch 8 taken 1887 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1887 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 1887 times.
✗ Branch 13 not taken.
15096 args.push_back(CommandArg("Effect", "subcommand", "Effect").setContentListUrl("api/overlays/effects/", false));
2201 1887 }
2202
2203 37852 virtual std::unique_ptr<Command::Result> run(const std::vector<std::string>& args) override {
2204
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 37852 times.
37852 if (args.size() < 3) {
2205 ✗ return std::make_unique<Command::ErrorResult>("Command needs at least 3 arguments, found " + std::to_string(args.size()));
2206 }
2207
1/2
✓ Branch 0 taken 37852 times.
✗ Branch 1 not taken.
37852 std::unique_lock<std::recursive_mutex> lock(getLock());
2208 37852 std::list<PixelOverlayModel*> models;
2209
2210
3/4
✓ Branch 0 taken 37852 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 37852 times.
✓ Branch 3 taken 37852 times.
75704 for (auto& ms : split(args[0], ',')) {
2211
2/4
✓ Branch 0 taken 37852 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 37852 times.
37852 if (ms == "--All Models--") {
2212 ✗ for (auto& mn : manager->getModelNames()) {
2213 ✗ auto m = getModelLocked(mn);
2214 ✗ models.push_back(m);
2215 }
2216 ✗ break;
2217 }
2218
1/2
✓ Branch 0 taken 37852 times.
✗ Branch 1 not taken.
37852 auto m = getModelLocked(ms);
2219
1/2
✓ Branch 0 taken 37852 times.
✗ Branch 1 not taken.
37852 if (m) {
2220
1/2
✓ Branch 0 taken 37852 times.
✗ Branch 1 not taken.
37852 models.push_back(m);
2221 } else {
2222 ✗ return std::make_unique<Command::ErrorResult>("No model found: " + ms);
2223 }
2224
1/2
✓ Branch 0 taken 37852 times.
✗ Branch 1 not taken.
37852 }
2225
2/2
✓ Branch 0 taken 37852 times.
✓ Branch 1 taken 37844 times.
75696 for (auto m : models) {
2226
1/2
✓ Branch 0 taken 37852 times.
✗ Branch 1 not taken.
37852 std::string effect = args[2];
2227
1/2
✓ Branch 0 taken 37852 times.
✗ Branch 1 not taken.
37852 std::vector<std::string> newArgs(args.begin() + 3, args.end());
2228
3/4
✓ Branch 0 taken 37852 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✓ Branch 3 taken 37844 times.
37852 if (!m->applyEffect(args[1], effect, newArgs)) {
2229
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
8 return std::make_unique<Command::ErrorResult>("Could not start effect: " + effect);
2230 }
2231
4/4
✓ Branch 0 taken 37844 times.
✓ Branch 1 taken 8 times.
✓ Branch 2 taken 37844 times.
✓ Branch 3 taken 8 times.
37860 }
2232
1/2
✓ Branch 0 taken 37844 times.
✗ Branch 1 not taken.
37844 return std::make_unique<Command::Result>("Model Effect Started");
2233 37852 }
2234 };
2235
2236 // Once, from Initialize(). This used to run when the first model was added,
2237 // which missed the commands entirely when a model listener had already put an
2238 // empty holder in the map, and registered them a second time (leaking the
2239 // first set) whenever the map emptied and refilled.
2240 1887 void PixelOverlayManager::RegisterCommands() {
2241
6/12
✓ 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 1727 times.
✓ Branch 7 taken 160 times.
✗ Branch 8 not taken.
✓ Branch 9 taken 1727 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3934 CommandManager::INSTANCE.addCategorizedCommand(new EnableOverlayCommand(this), "Pixel Overlay", 1);
2242
6/12
✓ 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 1727 times.
✓ Branch 7 taken 160 times.
✗ Branch 8 not taken.
✓ Branch 9 taken 1727 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3934 CommandManager::INSTANCE.addCategorizedCommand(new FillOverlayCommand(this), "Pixel Overlay", 1);
2243
6/12
✓ 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 1727 times.
✓ Branch 7 taken 160 times.
✗ Branch 8 not taken.
✓ Branch 9 taken 1727 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3934 CommandManager::INSTANCE.addCategorizedCommand(new TextOverlayCommand(this), "Pixel Overlay", 1);
2244
6/12
✓ 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 1727 times.
✓ Branch 7 taken 160 times.
✗ Branch 8 not taken.
✓ Branch 9 taken 1727 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3934 CommandManager::INSTANCE.addCategorizedCommand(new ClearOverlayCommand(this), "Pixel Overlay", 1);
2245
6/12
✓ 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 1727 times.
✓ Branch 7 taken 160 times.
✗ Branch 8 not taken.
✓ Branch 9 taken 1727 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
2207 CommandManager::INSTANCE.addCategorizedCommand(new ApplyEffectOverlayCommand(this), "Pixel Overlay", 1);
2246 1887 }
2247
2248 // colorOrder is accepted for source compatibility with existing channel output
2249 // plugins but is no longer used: a model addresses fppd's channel data, which is
2250 // always R,G,B[,W], so PixelOverlayModel derives the order from the node width.
2251 508 void PixelOverlayManager::addAutoOverlayModel(const std::string& name,
2252 uint32_t startChannel, uint32_t channelCount, uint32_t channelPerNode,
2253 const std::string& orientation, const std::string& startLocation,
2254 uint32_t strings, uint32_t strands, const std::string& colorOrder) {
2255
1/2
✓ Branch 0 taken 508 times.
✗ Branch 1 not taken.
508 Json::Value val;
2256
2/4
✓ Branch 0 taken 508 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 508 times.
✗ Branch 3 not taken.
508 val["Name"] = name;
2257
2/4
✓ Branch 0 taken 508 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 508 times.
✗ Branch 3 not taken.
508 val["Type"] = "Channel";
2258
2/4
✓ Branch 0 taken 508 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 508 times.
✗ Branch 3 not taken.
508 val["StartChannel"] = startChannel + 1;
2259
2/4
✓ Branch 0 taken 508 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 508 times.
✗ Branch 3 not taken.
508 val["ChannelCount"] = channelCount;
2260
2/4
✓ Branch 0 taken 508 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 508 times.
✗ Branch 3 not taken.
508 val["StringCount"] = strings;
2261
2/4
✓ Branch 0 taken 508 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 508 times.
✗ Branch 3 not taken.
508 val["StrandsPerString"] = strands;
2262
2/4
✓ Branch 0 taken 508 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 508 times.
✗ Branch 3 not taken.
508 val["Orientation"] = orientation;
2263
2/4
✓ Branch 0 taken 508 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 508 times.
✗ Branch 3 not taken.
508 val["StartCorner"] = startLocation;
2264
2/4
✓ Branch 0 taken 508 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 508 times.
✗ Branch 3 not taken.
508 val["ChannelCountPerNode"] = channelPerNode;
2265
2/4
✓ Branch 0 taken 508 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 508 times.
✗ Branch 3 not taken.
508 val["autoCreated"] = true;
2266
2267
2/4
✓ Branch 0 taken 508 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 508 times.
✗ Branch 3 not taken.
508 addModel(val);
2268 508 }
2269 541 void PixelOverlayManager::removeAutoOverlayModel(const std::string& name) {
2270
1/2
✓ Branch 0 taken 541 times.
✗ Branch 1 not taken.
541 std::unique_lock<std::recursive_mutex> lock(modelsLock);
2271
1/2
✓ Branch 0 taken 541 times.
✗ Branch 1 not taken.
541 PixelOverlayModel* pmodel = models[name].model;
2272
6/8
✓ Branch 0 taken 528 times.
✓ Branch 1 taken 13 times.
✓ Branch 2 taken 528 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 528 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 528 times.
✓ Branch 7 taken 13 times.
541 if (pmodel && pmodel->isAutoCreated()) {
2273 // Every submodel of this model lets go of it before it is freed. A
2274 // submodel caches its parent pointer the first time it resolves it,
2275 // and the parent's destructor only reaches the children currently
2276 // enabled within it, so a disabled one (or one refused for lying
2277 // outside the parent) kept a dangling pointer across an output
2278 // reload. Enabling it again called into the freed model and put it
2279 // back on the active list. Done under modelsLock, before the model
2280 // leaves the map, so nothing can resolve it again in between.
2281
2/2
✓ Branch 0 taken 4010 times.
✓ Branch 1 taken 528 times.
4538 for (auto& m : models) {
2282
2/2
✓ Branch 0 taken 2286 times.
✓ Branch 1 taken 1724 times.
4010 PixelOverlayModelSub* sub = dynamic_cast<PixelOverlayModelSub*>(m.second.model);
2283
8/12
✓ Branch 0 taken 19 times.
✓ Branch 1 taken 3991 times.
✓ Branch 2 taken 19 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 19 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 19 times.
✓ Branch 7 taken 3991 times.
✓ Branch 8 taken 19 times.
✓ Branch 9 taken 3991 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
4010 if (sub && sub->getParentName() == name) {
2284
1/2
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
19 sub->resetParent();
2285 }
2286 }
2287 // Retired before it leaves the map: from here a reference holder can
2288 // still use it, but nothing may schedule it or make it active again.
2289 {
2290
1/2
✓ Branch 0 taken 528 times.
✗ Branch 1 not taken.
528 std::lock_guard<std::mutex> l(s_retiredLock);
2291
1/2
✓ Branch 0 taken 528 times.
✗ Branch 1 not taken.
528 s_retired.insert(pmodel);
2292 528 }
2293 // Its shm names go now, under modelsLock, before a replacement can
2294 // be created with them: a holder can keep it alive past that, and its
2295 // destructor unlinked the replacement's segments.
2296
1/2
✓ Branch 0 taken 528 times.
✗ Branch 1 not taken.
528 pmodel->releaseSharedMemoryNames();
2297
1/2
✓ Branch 0 taken 528 times.
✗ Branch 1 not taken.
528 std::shared_ptr<PixelOverlayModel> owner = std::move(models[name].owner);
2298 528 modelNames.remove(name);
2299
1/2
✓ Branch 0 taken 528 times.
✗ Branch 1 not taken.
528 models[name].model = nullptr;
2300 528 ++modelsGeneration;
2301
3/4
✓ Branch 0 taken 528 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✓ Branch 3 taken 528 times.
543 for (auto& l : models[name].listeners) {
2302
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 l.second(nullptr);
2303 }
2304
1/2
✓ Branch 0 taken 528 times.
✗ Branch 1 not taken.
528 lock.unlock();
2305
2306 // The only caller that actually frees a model, so the only one that
2307 // needs to wait for a running update to finish. modelsLock was
2308 // released above, which this variant requires.
2309
1/2
✓ Branch 0 taken 528 times.
✗ Branch 1 not taken.
528 removePeriodicUpdateAndWait(pmodel);
2310
1/2
✓ Branch 0 taken 528 times.
✗ Branch 1 not taken.
528 std::unique_lock<std::recursive_mutex> alock(activeModelsLock);
2311 528 activeModels.remove(pmodel);
2312
1/2
✓ Branch 0 taken 528 times.
✗ Branch 1 not taken.
528 updateActiveCountLocked();
2313
1/2
✓ Branch 0 taken 528 times.
✗ Branch 1 not taken.
528 alock.unlock();
2314
2315 // Frees it now, or when the last getModelShared() holder lets go -
2316 // a bulk data PUT or effect start that looked it up before this, a
2317 // playlist image entry until its next draw, or a submodel's
2318 // setState() on the effect or output thread. The destructor takes no
2319 // overlay lock and touches nothing by name, so any of those threads
2320 // can run it.
2321 528 owner.reset();
2322 528 }
2323 541 }
2324
2325 290268 Json::Value PixelOverlayManager::getActiveOverlayEffects() {
2326
1/2
✓ Branch 0 taken 290268 times.
✗ Branch 1 not taken.
290268 Json::Value ret = Json::arrayValue;
2327
2328 // quickly create a snapshot of the active models
2329 // so we don't hold the lock while iterating through them
2330 // and to avoid deadlocks if the model is being modified
2331 // while we are iterating through it
2332 //
2333 // The snapshot holds references, not the raw active-list pointers: it is
2334 // walked with no lock held, and removeAutoOverlayModel() can free a model
2335 // meanwhile. modelsLock -> activeModelsLock is the sanctioned order.
2336 290268 std::vector<std::shared_ptr<PixelOverlayModel>> am;
2337 {
2338
1/2
✓ Branch 0 taken 290268 times.
✗ Branch 1 not taken.
290268 std::unique_lock<std::recursive_mutex> mlock(modelsLock);
2339
1/2
✓ Branch 0 taken 290268 times.
✗ Branch 1 not taken.
290268 std::unique_lock<std::recursive_mutex> lock(activeModelsLock);
2340
2/2
✓ Branch 0 taken 160000 times.
✓ Branch 1 taken 290268 times.
450268 for (auto* raw : activeModels) {
2341
1/2
✓ Branch 0 taken 160000 times.
✗ Branch 1 not taken.
160000 auto it = models.find(raw->getName());
2342
5/6
✓ Branch 0 taken 160000 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 159998 times.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 159998 times.
✓ Branch 5 taken 2 times.
160000 if (it != models.end() && it->second.owner.get() == raw) {
2343
1/2
✓ Branch 0 taken 159998 times.
✗ Branch 1 not taken.
159998 am.push_back(it->second.owner);
2344 }
2345 }
2346 290268 }
2347
2348
2/2
✓ Branch 0 taken 159998 times.
✓ Branch 1 taken 290268 times.
450266 for (auto& m : am) {
2349
1/2
✓ Branch 0 taken 159998 times.
✗ Branch 1 not taken.
159998 std::unique_lock<std::recursive_mutex> l(m->getRunningEffectMutex());
2350 159998 RunningEffect* eff = m->getRunningEffect();
2351
2/2
✓ Branch 0 taken 151258 times.
✓ Branch 1 taken 8740 times.
159998 if (eff) {
2352
1/2
✓ Branch 0 taken 151258 times.
✗ Branch 1 not taken.
151258 Json::Value model;
2353
1/2
✓ Branch 0 taken 151258 times.
✗ Branch 1 not taken.
151258 m->toJson(model);
2354
2/4
✓ Branch 0 taken 151258 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 151258 times.
✗ Branch 3 not taken.
151258 eff->toJson(model["effect"]);
2355
1/2
✓ Branch 0 taken 151258 times.
✗ Branch 1 not taken.
151258 ret.append(model);
2356 151258 }
2357 159998 }
2358 290268 return ret;
2359 290268 }
2360
2361 // `updates` is keyed by GetMonotonicTimeMS(). It was the wall clock: a step
2362 // back (NTP, an RTC that boots in the past) froze every running overlay
2363 // effect for the size of the step, and a step forward ran an hour of 25ms
2364 // requeues back to back.
2365 78 void PixelOverlayManager::doOverlayModelEffects() {
2366
2/4
✓ Branch 0 taken 78 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 78 times.
✗ Branch 3 not taken.
78 SetThreadName("FPP-OverlayME");
2367
1/2
✓ Branch 0 taken 78 times.
✗ Branch 1 not taken.
78 std::unique_lock<std::mutex> l(threadLock);
2368
1/2
✓ Branch 0 taken 91944 times.
✗ Branch 1 not taken.
91944 while (threadKeepRunning) {
2369 91944 uint32_t waitTime = 1000;
2370
2/2
✓ Branch 0 taken 91932 times.
✓ Branch 1 taken 12 times.
91944 if (!updates.empty()) {
2371
1/2
✓ Branch 0 taken 91932 times.
✗ Branch 1 not taken.
91932 uint64_t curTime = GetMonotonicTimeMS();
2372
6/6
✓ Branch 0 taken 135916 times.
✓ Branch 1 taken 535 times.
✓ Branch 2 taken 44519 times.
✓ Branch 3 taken 91397 times.
✓ Branch 4 taken 44519 times.
✓ Branch 5 taken 91932 times.
136451 while (!updates.empty() && updates.begin()->first <= curTime) {
2373 // Drain the due batch one model at a time while holding
2374 // threadLock rather than snapshotting the whole list and
2375 // erasing the map entry up front. A snapshot survives
2376 // removePeriodicUpdate() -- it only edits `updates` -- so a
2377 // model removed (and freed) part way through the batch was
2378 // still run afterwards. Leaving the remainder in the map
2379 // makes that removal authoritative for every model that has
2380 // not started yet, and runEffectUpdateLocked() covers the one
2381 // that has.
2382 44519 uint64_t startTime = updates.begin()->first;
2383
2/4
✓ Branch 0 taken 60743 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 60743 times.
✗ Branch 3 not taken.
121486 while (!updates.empty() && updates.begin()->first == startTime &&
2384
4/4
✓ Branch 0 taken 16224 times.
✓ Branch 1 taken 44519 times.
✓ Branch 2 taken 16224 times.
✓ Branch 3 taken 44519 times.
121486 !updates.begin()->second.empty()) {
2385 16224 PixelOverlayModel* m = updates.begin()->second.front();
2386 16224 updates.begin()->second.pop_front();
2387 16224 int32_t ms = 0;
2388
1/2
✓ Branch 0 taken 16224 times.
✗ Branch 1 not taken.
16224 runEffectUpdateLocked(m, l, &ms);
2389
2/2
✓ Branch 0 taken 15656 times.
✓ Branch 1 taken 568 times.
16224 if (ms > 0) {
2390 15656 uint64_t t = startTime + ms;
2391
2/4
✓ Branch 0 taken 15656 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15656 times.
✗ Branch 3 not taken.
15656 updates[t].push_back(m);
2392
2/2
✓ Branch 0 taken 424 times.
✓ Branch 1 taken 144 times.
568 } else if (ms < 0) {
2393
1/2
✓ Branch 0 taken 424 times.
✗ Branch 1 not taken.
424 afterOverlayModels.push_back(m);
2394 }
2395 }
2396 // Requeues above may have re-created the entry at a later time;
2397 // only drop it when this batch really is empty.
2398
3/6
✓ Branch 0 taken 44519 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 44519 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 44519 times.
✗ Branch 5 not taken.
89038 if (!updates.empty() && updates.begin()->first == startTime &&
2399
1/2
✓ Branch 0 taken 44519 times.
✗ Branch 1 not taken.
89038 updates.begin()->second.empty()) {
2400
1/2
✓ Branch 0 taken 44519 times.
✗ Branch 1 not taken.
44519 updates.erase(updates.begin());
2401 }
2402
1/2
✓ Branch 0 taken 44519 times.
✗ Branch 1 not taken.
44519 curTime = GetMonotonicTimeMS();
2403 }
2404
2/2
✓ Branch 0 taken 91397 times.
✓ Branch 1 taken 535 times.
91932 if (!updates.empty()) {
2405 91397 waitTime = updates.begin()->first - curTime;
2406 }
2407 }
2408
1/2
✓ Branch 0 taken 91866 times.
✗ Branch 1 not taken.
91944 threadCV.wait_for(l, std::chrono::milliseconds(waitTime));
2409 }
2410 ✗ }
2411 // Runs updateRunningEffects() on `m` with threadLock dropped, marking the model
2412 // in flight for the duration so removePeriodicUpdateAndWait() cannot let it be
2413 // freed underneath the call. `l` must own threadLock on entry and does again on exit.
2414 // threadLock must be held. Marks every in-flight update of `m` as removed so
2415 // it reports EFFECT_DONE instead of requeueing, and takes `m` out of both
2416 // schedules.
2417 38312 void PixelOverlayManager::StampAndStripLocked(PixelOverlayModel* m) {
2418
2/2
✓ Branch 0 taken 13 times.
✓ Branch 1 taken 38312 times.
38325 for (auto& e : inFlightModels) {
2419
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 12 times.
13 if (e.m == m) {
2420 1 e.removed = true;
2421 }
2422 }
2423
2/2
✓ Branch 0 taken 844648 times.
✓ Branch 1 taken 38312 times.
882960 for (auto& a : updates) {
2424 844648 a.second.remove(m);
2425 }
2426 38312 afterOverlayModels.remove(m);
2427 38312 }
2428
2429 16597 void PixelOverlayManager::runEffectUpdateLocked(PixelOverlayModel* m,
2430 std::unique_lock<std::mutex>& l,
2431 int32_t* msOut) {
2432 16597 const auto self = std::this_thread::get_id();
2433
1/2
✓ Branch 0 taken 16597 times.
✗ Branch 1 not taken.
16597 inFlightModels.push_back({ m, self, false });
2434
1/2
✓ Branch 0 taken 16597 times.
✗ Branch 1 not taken.
16597 l.unlock();
2435
1/2
✓ Branch 0 taken 16597 times.
✗ Branch 1 not taken.
16597 int32_t ms = m->updateRunningEffects();
2436
1/2
✓ Branch 0 taken 16597 times.
✗ Branch 1 not taken.
16597 l.lock();
2437 16597 bool removed = false;
2438
1/2
✓ Branch 0 taken 16597 times.
✗ Branch 1 not taken.
16597 for (auto it = inFlightModels.begin(); it != inFlightModels.end(); ++it) {
2439
3/6
✓ Branch 0 taken 16597 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 16597 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 16597 times.
✗ Branch 5 not taken.
16597 if (it->m == m && it->tid == self) {
2440 16597 removed = it->removed;
2441 16597 inFlightModels.erase(it);
2442 16597 break;
2443 }
2444 }
2445 16597 inFlightCV.notify_all();
2446
2/2
✓ Branch 0 taken 16224 times.
✓ Branch 1 taken 373 times.
16597 if (msOut) {
2447 // A stamped entry means the model has been taken out of the schedule
2448 // and may be about to be deleted. Report EFFECT_DONE so the caller
2449 // does not requeue it -- requeuing here, under the same threadLock hold
2450 // that a waiting deleter needs to wake, is exactly how a removed model
2451 // came back to life and got dereferenced after the free.
2452
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 16223 times.
16224 *msOut = removed ? 0 : ms;
2453 }
2454 16597 }
2455
2456 // Stamp-and-strip, non-blocking. See the header for why this must never wait:
2457 // setRunningEffect() reaches here holding the model's effectLock, and an
2458 // in-flight entry is registered before updateRunningEffects() takes that lock.
2459 37256 void PixelOverlayManager::removePeriodicUpdate(PixelOverlayModel* m) {
2460
1/2
✓ Branch 0 taken 37256 times.
✗ Branch 1 not taken.
37256 std::unique_lock<std::mutex> l(threadLock);
2461
1/2
✓ Branch 0 taken 37256 times.
✗ Branch 1 not taken.
37256 StampAndStripLocked(m);
2462 37256 }
2463
2464 // Strips the model from the schedule and does not return until no *other*
2465 // thread is inside updateRunningEffects() on it, so removeAutoOverlayModel()
2466 // can delete it.
2467 //
2468 // NOTE: this blocks, and must not be called with effectLock or modelsLock held.
2469 // * effectLock: an in-flight entry is registered before updateRunningEffects()
2470 // acquires effectLock, so a waiter holding it would be waiting on a thread
2471 // that needs it -- a hard deadlock, and a silent one (fppd keeps answering
2472 // /api/fppd/status). That is why setRunningEffect() gets the non-blocking
2473 // variant above.
2474 // * modelsLock: removeAutoOverlayModel() unlocks before calling in, but
2475 // modelsLock is recursive, so a future caller that already held it one level
2476 // up would only drop to depth 1 and run this wait under the lock -- and an
2477 // in-flight effect that needs modelsLock (PixelOverlayModelSub resolving a
2478 // parent) would deadlock against it.
2479 528 void PixelOverlayManager::removePeriodicUpdateAndWait(PixelOverlayModel* m) {
2480
1/2
✓ Branch 0 taken 528 times.
✗ Branch 1 not taken.
528 std::unique_lock<std::mutex> l(threadLock);
2481 // Stamp first: the stamp is what stops runEffectUpdateLocked()'s caller
2482 // putting the model back into the schedule after the wait below is already
2483 // satisfiable. The in-flight thread erases its entry and then requeues
2484 // while still holding threadLock, so a waiter -- which cannot wake until
2485 // that lock is released -- would otherwise observe an empty registry and a
2486 // model that has just been put back, and delete it anyway.
2487
1/2
✓ Branch 0 taken 528 times.
✗ Branch 1 not taken.
528 StampAndStripLocked(m);
2488
1/2
✓ Branch 0 taken 528 times.
✗ Branch 1 not taken.
528 inFlightCV.wait(l, [this, m]() {
2489 528 auto self = std::this_thread::get_id();
2490
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 528 times.
534 for (const auto& e : inFlightModels) {
2491
2/6
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 6 times.
6 if (e.m == m && e.tid != self) {
2492 ✗ return false;
2493 }
2494 }
2495 528 return true;
2496 });
2497 // Belt and suspenders. The stamp should have prevented any requeue while
2498 // we waited; strip again so the model is gone from the schedule even if
2499 // some future path finds another way to put it back.
2500
1/2
✓ Branch 0 taken 528 times.
✗ Branch 1 not taken.
528 StampAndStripLocked(m);
2501 528 }
2502
2503 72028 void PixelOverlayManager::addPeriodicUpdate(int32_t initialDelayMS, PixelOverlayModel* m) {
2504
1/2
✓ Branch 0 taken 72028 times.
✗ Branch 1 not taken.
72028 std::unique_lock<std::mutex> l(threadLock);
2505 // Checked under threadLock so the check and the push below are one step
2506 // against removeAutoOverlayModel(), which retires the model before it
2507 // drains the schedule under this lock. Without it a reference holder
2508 // setting an effect afterwards (the bulk data PUT stops the running one
2509 // with a 25ms requeue) put the model back, to be run after it was freed.
2510
2/4
✓ Branch 0 taken 72028 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 72028 times.
72028 if (isRetired(m)) {
2511 ✗ return;
2512 }
2513
2/2
✓ Branch 0 taken 78 times.
✓ Branch 1 taken 71950 times.
72028 if (updateThread == nullptr) {
2514 78 threadKeepRunning = true;
2515
3/8
✓ Branch 0 taken 78 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 78 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 78 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
78 updateThread = new std::thread(&PixelOverlayManager::doOverlayModelEffects, this);
2516 }
2517
1/2
✓ Branch 0 taken 72028 times.
✗ Branch 1 not taken.
72028 if (initialDelayMS > 0) {
2518
1/2
✓ Branch 0 taken 72028 times.
✗ Branch 1 not taken.
72028 uint64_t nextTime = GetMonotonicTimeMS();
2519 72028 nextTime += initialDelayMS;
2520
2/4
✓ Branch 0 taken 72028 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 72028 times.
✗ Branch 3 not taken.
72028 updates[nextTime].push_back(m);
2521 } else {
2522 ✗ afterOverlayModels.push_back(m);
2523 }
2524
1/2
✓ Branch 0 taken 72028 times.
✗ Branch 1 not taken.
72028 l.unlock();
2525 72028 threadCV.notify_all();
2526
1/2
✓ Branch 0 taken 72028 times.
✗ Branch 1 not taken.
72028 }
2527