| 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 |