GCC Code Coverage Report


Directory: ./
File: WLEDAPIResponder.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 629 699 90.0%
Functions: 34 38 89.5%
Branches: 865 1639 52.8%

Line Branch Exec Source
1 /*
2 * This file is part of the Falcon Player (FPP) and is Copyright (C)
3 * 2026 by the Falcon Player Developers.
4 *
5 * The Falcon Player (FPP) is free software, and is covered under
6 * multiple Open Source licenses. Please see the included 'LICENSES'
7 * file for descriptions of what files are covered by each license.
8 *
9 * This source file is covered under the LGPL v2.1 as described in the
10 * included LICENSE.LGPL file.
11 */
12
13 #include "fpp-pch.h"
14
15 #include "fpp-json.h"
16
17 #include <arpa/inet.h>
18 #include <dirent.h>
19 #include <errno.h>
20 #include <fcntl.h>
21 #include <ifaddrs.h>
22 #include <net/if.h>
23 #include <netinet/in.h>
24 #include <openssl/sha.h>
25 #include <poll.h>
26 #include <strings.h>
27 #include <sys/socket.h>
28 #include <sys/time.h>
29 #include <unistd.h>
30
31 #include <algorithm>
32 #include <chrono>
33 #include <cstring>
34 #include <sstream>
35
36 #include "WLEDAPIResponder.h"
37
38 #include "common.h"
39 #include "fppversion.h"
40 #include "log.h"
41 #include "settings.h"
42
43 #include "commands/Commands.h"
44 #include "overlays/PixelOverlay.h"
45 #include "overlays/PixelOverlayEffects.h"
46 #include "overlays/PixelOverlayModel.h"
47 #include "overlays/WLEDEffects.h"
48 #include "SysSocket.h"
49
50 WLEDAPIResponder WLEDAPIResponder::INSTANCE;
51
52 namespace {
53
54 // RFC 6455 magic GUID, appended to the client's Sec-WebSocket-Key before
55 // SHA1 + base64 to produce the Sec-WebSocket-Accept response.
56 constexpr const char* WS_MAGIC = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
57
58 745262 std::string headerValue(const std::string& headers, const std::string& name) {
59 // Case-insensitive header lookup.
60
1/2
✓ Branch 0 taken 745262 times.
✗ Branch 1 not taken.
745262 std::string lname = name;
61 745262 std::transform(lname.begin(), lname.end(), lname.begin(), ::tolower);
62
1/2
✓ Branch 0 taken 745262 times.
✗ Branch 1 not taken.
745262 std::istringstream is(headers);
63 745262 std::string line;
64
4/6
✓ Branch 0 taken 1987377 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1987377 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1490555 times.
✓ Branch 5 taken 496822 times.
1987377 while (std::getline(is, line)) {
65 1490555 auto colon = line.find(':');
66
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1490555 times.
2732670 if (colon == std::string::npos) continue;
67
1/2
✓ Branch 0 taken 1490555 times.
✗ Branch 1 not taken.
1490555 std::string h = line.substr(0, colon);
68 1490555 std::transform(h.begin(), h.end(), h.begin(), ::tolower);
69
2/2
✓ Branch 0 taken 1242115 times.
✓ Branch 1 taken 248440 times.
1490555 if (h != lname) continue;
70
1/2
✓ Branch 0 taken 248440 times.
✗ Branch 1 not taken.
248440 std::string v = line.substr(colon + 1);
71 // Trim leading spaces and trailing CR/whitespace.
72 248440 size_t s = v.find_first_not_of(" \t");
73 248440 size_t e = v.find_last_not_of(" \t\r\n");
74
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 248440 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
248440 if (s == std::string::npos) return "";
75
1/2
✓ Branch 0 taken 248440 times.
✗ Branch 1 not taken.
248440 return v.substr(s, e - s + 1);
76
2/2
✓ Branch 0 taken 1242115 times.
✓ Branch 1 taken 248440 times.
1738995 }
77
1/2
✓ Branch 0 taken 496822 times.
✗ Branch 1 not taken.
993644 return "";
78 745262 }
79
80 // Read until "\r\n\r\n", which separates HTTP headers from body.
81 // Returns the full headers-and-line string, or empty on error.
82 248447 bool readUntilHeadersEnd(int fd, std::string& out, size_t maxBytes = 16384) {
83 char buf[1024];
84
1/2
✓ Branch 0 taken 248447 times.
✗ Branch 1 not taken.
248447 while (out.size() < maxBytes) {
85
1/2
✓ Branch 0 taken 248447 times.
✗ Branch 1 not taken.
248447 ssize_t n = ::recv(fd, buf, sizeof(buf), 0);
86
2/2
✓ Branch 0 taken 22 times.
✓ Branch 1 taken 248425 times.
248447 if (n <= 0) return false;
87
1/2
✓ Branch 0 taken 248425 times.
✗ Branch 1 not taken.
248425 out.append(buf, n);
88
1/2
✓ Branch 0 taken 248425 times.
✗ Branch 1 not taken.
248425 if (out.find("\r\n\r\n") != std::string::npos) return true;
89 }
90 ✗ return false;
91 }
92
93 87303 bool readExact(int fd, void* dst, size_t n) {
94 87303 auto* p = static_cast<uint8_t*>(dst);
95
2/2
✓ Branch 0 taken 87303 times.
✓ Branch 1 taken 87291 times.
174594 while (n) {
96 87303 ssize_t r = ::recv(fd, p, n, 0);
97
2/2
✓ Branch 0 taken 12 times.
✓ Branch 1 taken 87291 times.
87303 if (r <= 0) return false;
98 87291 p += r;
99 87291 n -= r;
100 }
101 87291 return true;
102 }
103
104 311260 bool writeAll(int fd, const void* src, size_t n) {
105 311260 auto* p = static_cast<const uint8_t*>(src);
106
2/2
✓ Branch 0 taken 311262 times.
✓ Branch 1 taken 311257 times.
622519 while (n) {
107 311262 ssize_t w = ::send(fd, p, n, MSG_NOSIGNAL);
108
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 311259 times.
311262 if (w <= 0) return false;
109 311259 p += w;
110 311259 n -= w;
111 }
112 311257 return true;
113 }
114
115 14 std::string sha1Base64(const std::string& in) {
116 unsigned char hash[SHA_DIGEST_LENGTH];
117
1/2
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
14 SHA1(reinterpret_cast<const unsigned char*>(in.data()), in.size(), hash);
118
1/2
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
28 return base64Encode(hash, SHA_DIGEST_LENGTH);
119 }
120
121 // Readers for JSON that came off the network: a field of the wrong type is
122 // treated as absent rather than thrown on (jsoncpp's as*() throw, and an
123 // exception on a client thread terminates fppd).
124 127 int jsonInt(const Json::Value& v, int dflt) {
125
6/6
✓ Branch 0 taken 106 times.
✓ Branch 1 taken 21 times.
✓ Branch 2 taken 104 times.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 104 times.
✓ Branch 5 taken 23 times.
127 if (v.isNumeric() && v.isConvertibleTo(Json::intValue)) {
126 104 return v.asInt();
127 }
128 23 return dflt;
129 }
130
131 96506 bool jsonBool(const Json::Value& v, bool dflt) {
132
1/2
✓ Branch 0 taken 96506 times.
✗ Branch 1 not taken.
96506 if (v.isBool()) {
133 96506 return v.asBool();
134 }
135 ✗ if (v.isNumeric()) {
136 ✗ return v.asDouble() != 0;
137 }
138 ✗ return dflt;
139 }
140
141 // The status line's reason phrase: every response used to say "OK", even
142 // a 404.
143 248410 const char* reasonPhrase(int code) {
144
1/6
✓ Branch 0 taken 248410 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
248410 switch (code) {
145 248410 case 200:
146 248410 return "OK";
147 ✗ case 400:
148 ✗ return "Bad Request";
149 ✗ case 404:
150 ✗ return "Not Found";
151 ✗ case 405:
152 ✗ return "Method Not Allowed";
153 ✗ case 413:
154 ✗ return "Payload Too Large";
155 ✗ default:
156 ✗ return code < 400 ? "OK" : "Error";
157 }
158 }
159
160 254048 void closeFd(int& fd) {
161
1/2
✓ Branch 0 taken 254048 times.
✗ Branch 1 not taken.
254048 if (fd >= 0) {
162 254048 ::close(fd);
163 254048 fd = -1;
164 }
165 254048 }
166
167 // Pick the first non-loopback interface and return its MAC (lowercase
168 // hex, no colons — the form WLED uses) along with its primary IPv4.
169 209520 void primaryNetIdentity(std::string& macFlat, std::string& ipv4) {
170 209520 macFlat.clear();
171 209520 ipv4.clear();
172 209520 std::string ifname;
173
174
2/4
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 209520 times.
✗ Branch 3 not taken.
209520 if (DIR* d = opendir("/sys/class/net")) {
175 struct dirent* e;
176
2/4
✓ Branch 0 taken 838080 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 838080 times.
✗ Branch 3 not taken.
838080 while ((e = readdir(d)) != nullptr) {
177
12/18
✓ Branch 0 taken 419040 times.
✓ Branch 1 taken 419040 times.
✓ Branch 2 taken 419040 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 419040 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 209520 times.
✓ Branch 7 taken 209520 times.
✓ Branch 8 taken 419040 times.
✓ Branch 9 taken 419040 times.
✓ Branch 10 taken 419040 times.
✓ Branch 11 taken 419040 times.
✓ Branch 12 taken 628560 times.
✓ Branch 13 taken 209520 times.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
1676160 if (e->d_name[0] == '.' || std::string(e->d_name) == "lo") continue;
178 char path[256];
179 209520 snprintf(path, sizeof(path), "/sys/class/net/%s/address", e->d_name);
180
1/2
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
209520 FILE* f = fopen(path, "r");
181
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 209520 times.
209520 if (!f) continue;
182 209520 char buf[32] = {};
183
2/4
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 209520 times.
✗ Branch 3 not taken.
209520 if (fgets(buf, sizeof(buf), f)) {
184
1/2
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
209520 std::string s(buf);
185
1/2
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
209520 s.erase(s.find_last_not_of(" \t\r\n") + 1);
186
4/8
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 209520 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 209520 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 209520 times.
✗ Branch 7 not taken.
209520 if (!s.empty() && s != "00:00:00:00:00:00") {
187
5/6
✓ Branch 0 taken 2514240 times.
✓ Branch 1 taken 1047600 times.
✓ Branch 2 taken 2514240 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3561840 times.
✓ Branch 5 taken 209520 times.
3771360 for (char c : s) if (c != ':') macFlat.push_back(std::tolower(c));
188
1/2
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
209520 ifname = e->d_name;
189
1/2
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
209520 fclose(f);
190 209520 break;
191 }
192
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 209520 times.
209520 }
193 ✗ fclose(f);
194 }
195
1/2
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
209520 closedir(d);
196 }
197
198
1/2
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
209520 if (!ifname.empty()) {
199 209520 ifaddrs* ifa = nullptr;
200
1/2
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
209520 if (getifaddrs(&ifa) == 0) {
201
1/2
✓ Branch 0 taken 838080 times.
✗ Branch 1 not taken.
838080 for (auto* p = ifa; p; p = p->ifa_next) {
202
3/4
✓ Branch 0 taken 838080 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 419040 times.
✓ Branch 3 taken 419040 times.
1047600 if (!p->ifa_addr || p->ifa_addr->sa_family != AF_INET) continue;
203
3/4
✓ Branch 0 taken 419040 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 209520 times.
✓ Branch 3 taken 209520 times.
419040 if (ifname != p->ifa_name) continue;
204 char buf[INET_ADDRSTRLEN];
205 209520 auto* in = reinterpret_cast<sockaddr_in*>(p->ifa_addr);
206
1/2
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
209520 if (inet_ntop(AF_INET, &in->sin_addr, buf, sizeof(buf))) {
207
1/2
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
209520 ipv4 = buf;
208 209520 break;
209 }
210 }
211 209520 freeifaddrs(ifa);
212 }
213 }
214 209520 }
215
216 } // namespace
217
218 2346 WLEDAPIResponder::WLEDAPIResponder() = default;
219
220 458 WLEDAPIResponder::~WLEDAPIResponder() {
221 458 Cleanup();
222 458 }
223
224 1887 void WLEDAPIResponder::Initialize() {
225
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1887 times.
1887 if (m_running) return;
226
227 // Allow disabling via /media/settings.
228
1/2
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
1887 std::string en = getSetting("WLEDAPIResponder");
229
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 not taken.
✓ Branch 11 taken 1887 times.
✗ Branch 12 not taken.
✓ Branch 13 taken 1887 times.
1887 if (en == "0" || en == "false" || en == "off") {
230 ✗ LogInfo(VB_GENERAL, "WLEDAPIResponder disabled by setting\n");
231 ✗ return;
232 }
233
234
1/2
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
1887 int p = getSettingInt("WLEDAPIPort", 0);
235
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 1887 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
1887 if (p > 0 && p < 65536) m_port = p;
236
237 1887 m_socket = OpenSocket(AF_INET, SOCK_STREAM, 0);
238
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1887 times.
1887 if (m_socket < 0) {
239 ✗ LogErr(VB_GENERAL, "WLEDAPIResponder: socket() failed: %s\n", FPPstrerror(errno));
240 ✗ return;
241 }
242
243 1887 int opt = 1;
244 1887 ::setsockopt(m_socket, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
245 1887 ::setsockopt(m_socket, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
246
247 1887 sockaddr_in addr{};
248 1887 addr.sin_family = AF_INET;
249 1887 addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); // proxied through Apache
250 1887 addr.sin_port = htons(m_port);
251
252
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1887 times.
1887 if (::bind(m_socket, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) < 0) {
253 ✗ LogErr(VB_GENERAL, "WLEDAPIResponder: bind(%d) failed: %s\n", m_port, FPPstrerror(errno));
254 ✗ closeFd(m_socket);
255 ✗ return;
256 }
257
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1887 times.
1887 if (::listen(m_socket, 8) < 0) {
258 ✗ LogErr(VB_GENERAL, "WLEDAPIResponder: listen() failed: %s\n", FPPstrerror(errno));
259 ✗ closeFd(m_socket);
260 ✗ return;
261 }
262 // The accept thread waits in poll() on the socket and this pipe, and
263 // Cleanup() wakes it through the pipe. It used to wait in accept() and be
264 // woken by shutdown() on the listening socket, which only Linux honours:
265 // on macOS accept() went on waiting and fppd hung on every stop. The
266 // socket is non-blocking so a connection that goes away between poll()
267 // and accept() can't leave the thread in accept() either.
268
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1887 times.
1887 if (::pipe(m_wakePipe) < 0) {
269 ✗ LogErr(VB_GENERAL, "WLEDAPIResponder: pipe() failed: %s\n", FPPstrerror(errno));
270 ✗ closeFd(m_socket);
271 ✗ return;
272 }
273
2/2
✓ Branch 0 taken 3774 times.
✓ Branch 1 taken 1887 times.
5661 for (int fd : m_wakePipe) {
274
1/2
✓ Branch 0 taken 3710 times.
✗ Branch 1 not taken.
3774 fcntl(fd, F_SETFD, FD_CLOEXEC);
275 }
276
2/4
✓ Branch 0 taken 1855 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1855 times.
✗ Branch 3 not taken.
1887 fcntl(m_wakePipe[1], F_SETFL, fcntl(m_wakePipe[1], F_GETFL) | O_NONBLOCK);
277
2/4
✓ Branch 0 taken 1855 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1855 times.
✗ Branch 3 not taken.
1887 fcntl(m_socket, F_SETFL, fcntl(m_socket, F_GETFL) | O_NONBLOCK);
278
279 1887 m_running = true;
280
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 1887 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
3774 m_acceptThread = new std::thread([this]() {
281
2/4
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1887 times.
✗ Branch 3 not taken.
1887 SetThreadName("FPP-WLEDAPI");
282 1887 AcceptLoop();
283
2/4
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1887 times.
✗ Branch 3 not taken.
3754 });
284
285
1/2
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
1887 LogInfo(VB_GENERAL, "WLEDAPIResponder: listening on 127.0.0.1:%d\n", m_port);
286
1/2
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
1887 }
287
288 2325 void WLEDAPIResponder::Cleanup() {
289
5/6
✓ Branch 0 taken 458 times.
✓ Branch 1 taken 1867 times.
✓ Branch 2 taken 458 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 458 times.
✓ Branch 5 taken 1867 times.
2325 if (!m_running && m_socket < 0) return;
290 1867 m_running = false;
291
292 // Wake the accept thread out of its poll(). The descriptors are closed
293 // only after it has been joined below: closed while that thread could
294 // still loop back into poll() and accept(), they raced it, and accept()
295 // could land on a closed descriptor or one already reused elsewhere.
296
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 if (m_wakePipe[1] >= 0) {
297 1867 char c = 0;
298
2/4
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1867 times.
1867 if (::write(m_wakePipe[1], &c, 1) < 0) {
299 ✗ LogWarn(VB_GENERAL, "WLEDAPIResponder: could not wake the accept thread: %s\n", FPPstrerror(errno));
300 }
301 }
302
303 // Shut down every per-connection socket too — accept-thread might
304 // have handed off a connection that's now blocked in recv().
305 {
306
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 std::unique_lock<std::mutex> lock(m_clientsLock);
307
2/2
✓ Branch 0 taken 12 times.
✓ Branch 1 taken 1867 times.
1879 for (int fd : m_clientFds) ::shutdown(fd, SHUT_RDWR);
308 1867 m_wsClients.clear();
309 1867 }
310
311
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 if (m_acceptThread) {
312 1867 m_acceptThread->join();
313
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 delete m_acceptThread;
314 1867 m_acceptThread = nullptr;
315 }
316 1867 closeFd(m_socket);
317 1867 closeFd(m_wakePipe[0]);
318 1867 closeFd(m_wakePipe[1]);
319
2/2
✓ Branch 0 taken 30 times.
✓ Branch 1 taken 1867 times.
1897 for (auto& t : m_clientThreads) {
320
2/4
✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
30 if (t->thread.joinable()) t->thread.join();
321 }
322 1867 m_clientThreads.clear();
323 {
324
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 std::unique_lock<std::mutex> lock(m_clientsLock);
325 1867 m_clientFds.clear();
326 1867 }
327 }
328
329 1887 void WLEDAPIResponder::AcceptLoop() {
330 1887 std::chrono::steady_clock::time_point lastAcceptWarn{};
331 1887 int acceptFailures = 0;
332
2/2
✓ Branch 0 taken 250368 times.
✓ Branch 1 taken 1 times.
250369 while (m_running) {
333 250368 struct pollfd fds[2] = { { m_socket, POLLIN, 0 }, { m_wakePipe[0], POLLIN, 0 } };
334
3/8
✓ Branch 0 taken 250348 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 250348 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 250348 times.
250368 if (::poll(fds, 2, -1) < 0 && errno != EINTR) {
335 ✗ LogErr(VB_GENERAL, "WLEDAPIResponder: poll() failed: %s\n", FPPstrerror(errno));
336 1866 break;
337 }
338
5/6
✓ Branch 0 taken 248482 times.
✓ Branch 1 taken 1866 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 248482 times.
✓ Branch 4 taken 1866 times.
✓ Branch 5 taken 248482 times.
250348 if (!m_running || fds[1].revents) {
339 1866 break;
340 }
341
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 248482 times.
248482 if (!fds[0].revents) {
342 35 continue;
343 }
344
1/2
✓ Branch 0 taken 248482 times.
✗ Branch 1 not taken.
248482 int client = AcceptSocket(m_socket, nullptr, nullptr);
345
2/2
✓ Branch 0 taken 35 times.
✓ Branch 1 taken 248447 times.
248482 if (client < 0) {
346 35 int err = errno;
347
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 35 times.
35 if (!m_running) {
348 ✗ break;
349 }
350
4/8
✓ 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.
✗ Branch 6 not taken.
✓ Branch 7 taken 35 times.
35 if (err == EINTR || err == ECONNABORTED || err == EAGAIN || err == EWOULDBLOCK) {
351 35 continue;
352 }
353 // Out of descriptors (EMFILE/ENFILE) or memory, the pending
354 // connection stays queued and accept() fails again at once: this
355 // loop spun a core and wrote a log line per spin. Back off, and
356 // log once per interval with a count.
357 35 ++acceptFailures;
358 35 auto now = std::chrono::steady_clock::now();
359
4/6
✓ Branch 0 taken 35 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 35 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 34 times.
35 if (now - lastAcceptWarn >= std::chrono::seconds(10)) {
360
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 LogWarn(VB_GENERAL, "WLEDAPIResponder: accept failed: %s (%d failures)\n", FPPstrerror(err), acceptFailures);
361 1 lastAcceptWarn = now;
362 1 acceptFailures = 0;
363 }
364
1/2
✓ Branch 0 taken 35 times.
✗ Branch 1 not taken.
35 std::this_thread::sleep_for(std::chrono::milliseconds(100));
365 35 continue;
366 35 }
367 // The client thread reads with blocking calls; on macOS an accepted
368 // socket inherits the listening socket's O_NONBLOCK.
369
2/4
✓ Branch 0 taken 248447 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 248447 times.
✗ Branch 3 not taken.
248447 fcntl(client, F_SETFL, fcntl(client, F_GETFL) & ~O_NONBLOCK);
370 // Writes to a client happen under m_clientsLock (the state
371 // broadcast), so one that stops reading must not block them for
372 // good: give up on it after a couple of seconds.
373 248447 struct timeval tv = { 2, 0 };
374 248447 ::setsockopt(client, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
375
376 // Hand off to a per-connection thread. WebSocket streams are
377 // long-lived, but there will only ever be a handful of clients
378 // (one mobile app at a time, maybe a couple), so a dedicated
379 // thread per connection is simpler than juggling select/epoll.
380 248447 std::vector<std::unique_ptr<ClientThread>> finished;
381 {
382 // Track every connection fd so Cleanup() can shutdown
383 // them all. The client thread removes its fd, and then
384 // closes it, when the connection ends.
385
1/2
✓ Branch 0 taken 248447 times.
✗ Branch 1 not taken.
248447 std::unique_lock<std::mutex> lock(m_clientsLock);
386
1/2
✓ Branch 0 taken 248447 times.
✗ Branch 1 not taken.
248447 m_clientFds.push_back(client);
387
2/2
✓ Branch 0 taken 949655 times.
✓ Branch 1 taken 248447 times.
1198102 for (auto it = m_clientThreads.begin(); it != m_clientThreads.end();) {
388
2/2
✓ Branch 0 taken 248417 times.
✓ Branch 1 taken 701238 times.
949655 if ((*it)->done) {
389
1/2
✓ Branch 0 taken 248417 times.
✗ Branch 1 not taken.
248417 finished.push_back(std::move(*it));
390
1/2
✓ Branch 0 taken 248417 times.
✗ Branch 1 not taken.
248417 it = m_clientThreads.erase(it);
391 } else {
392 701238 ++it;
393 }
394 }
395 248447 }
396
2/2
✓ Branch 0 taken 248417 times.
✓ Branch 1 taken 248447 times.
496864 for (auto& f : finished) {
397
1/2
✓ Branch 0 taken 248417 times.
✗ Branch 1 not taken.
248417 f->thread.join(); // done is its last act, so this is immediate
398 }
399
1/2
✓ Branch 0 taken 248447 times.
✗ Branch 1 not taken.
248447 auto ct = std::make_unique<ClientThread>();
400 248447 ClientThread* self = ct.get();
401 745341 ct->thread = std::thread([this, client, self]() {
402
2/4
✓ Branch 0 taken 248447 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 248447 times.
✗ Branch 3 not taken.
248447 SetThreadName("FPP-WLEDAPI-cli");
403 try {
404
1/2
✓ Branch 0 taken 248447 times.
✗ Branch 1 not taken.
248447 ClientLoop(client);
405 ✗ } catch (const std::exception& ex) {
406 // A request is never worth the daemon; ClientLoop's own
407 // checks should keep anything from getting here.
408 ✗ LogWarn(VB_GENERAL, "WLEDAPIResponder: dropped a request: %s\n", ex.what());
409 ✗ }
410 {
411
1/2
✓ Branch 0 taken 248447 times.
✗ Branch 1 not taken.
248447 std::unique_lock<std::mutex> lock(m_clientsLock);
412
1/2
✓ Branch 0 taken 248447 times.
✗ Branch 1 not taken.
496894 m_clientFds.erase(
413
1/2
✓ Branch 0 taken 248447 times.
✗ Branch 1 not taken.
248447 std::remove(m_clientFds.begin(), m_clientFds.end(), client),
414 248447 m_clientFds.end());
415
1/2
✓ Branch 0 taken 248447 times.
✗ Branch 1 not taken.
496894 m_wsClients.erase(
416
1/2
✓ Branch 0 taken 248447 times.
✗ Branch 1 not taken.
248447 std::remove(m_wsClients.begin(), m_wsClients.end(), client),
417 248447 m_wsClients.end());
418 248447 }
419 // Closed only here, after it has left both lists. ClientLoop
420 // used to close it itself: accept() could then hand the same
421 // number to a new connection, which the erase above removed
422 // along with this one, so Cleanup() never woke it.
423 248447 int fd = client;
424
1/2
✓ Branch 0 taken 248447 times.
✗ Branch 1 not taken.
248447 closeFd(fd);
425 248447 self->done = true;
426
1/2
✓ Branch 0 taken 248447 times.
✗ Branch 1 not taken.
745341 });
427
1/2
✓ Branch 0 taken 248447 times.
✗ Branch 1 not taken.
248447 std::unique_lock<std::mutex> lock(m_clientsLock);
428
1/2
✓ Branch 0 taken 248447 times.
✗ Branch 1 not taken.
248447 m_clientThreads.push_back(std::move(ct));
429 248447 }
430 1867 }
431
432 // Never closes fd: the thread that runs it closes it once it is untracked.
433 248447 void WLEDAPIResponder::ClientLoop(int fd) {
434 248447 std::string headerBlock;
435
3/4
✓ Branch 0 taken 248447 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 22 times.
✓ Branch 3 taken 248425 times.
248447 if (!readUntilHeadersEnd(fd, headerBlock)) {
436 22 return;
437 }
438
439 248425 auto headerEnd = headerBlock.find("\r\n\r\n");
440 248425 std::string requestLine;
441 248425 std::string headers;
442
1/2
✓ Branch 0 taken 248425 times.
✗ Branch 1 not taken.
248425 std::string body = headerBlock.substr(headerEnd + 4);
443
444 248425 auto firstLineEnd = headerBlock.find("\r\n");
445
1/2
✓ Branch 0 taken 248425 times.
✗ Branch 1 not taken.
248425 if (firstLineEnd != std::string::npos) {
446
1/2
✓ Branch 0 taken 248425 times.
✗ Branch 1 not taken.
248425 requestLine = headerBlock.substr(0, firstLineEnd);
447
1/2
✓ Branch 0 taken 248425 times.
✗ Branch 1 not taken.
248425 headers = headerBlock.substr(firstLineEnd + 2, headerEnd - firstLineEnd - 2);
448 }
449
450 // If the request is a WebSocket upgrade, do the handshake and then
451 // keep the connection open in WS mode.
452
2/4
✓ Branch 0 taken 248425 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 248425 times.
✗ Branch 3 not taken.
248425 std::string upgrade = headerValue(headers, "Upgrade");
453 248425 std::transform(upgrade.begin(), upgrade.end(), upgrade.begin(), ::tolower);
454
2/4
✓ Branch 0 taken 248425 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 248425 times.
✗ Branch 3 not taken.
248425 std::string wsKey = headerValue(headers, "Sec-WebSocket-Key");
455
456
6/8
✓ Branch 0 taken 248425 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✓ Branch 3 taken 248411 times.
✓ Branch 4 taken 14 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 14 times.
✓ Branch 7 taken 248411 times.
248425 if (upgrade == "websocket" && !wsKey.empty()) {
457
2/4
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 14 times.
14 if (!DoWebSocketHandshake(fd, wsKey)) {
458 ✗ return;
459 }
460 // Send initial state snapshot.
461
2/4
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✗ Branch 3 not taken.
14 SendTextFrame(fd, BuildCombinedJson());
462 {
463
1/2
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
14 std::unique_lock<std::mutex> lock(m_clientsLock);
464
1/2
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
14 m_wsClients.push_back(fd);
465 14 }
466
1/2
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
14 RunWebSocket(fd);
467 {
468
1/2
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
14 std::unique_lock<std::mutex> lock(m_clientsLock);
469
2/4
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✗ Branch 3 not taken.
14 m_wsClients.erase(std::remove(m_wsClients.begin(), m_wsClients.end(), fd),
470 14 m_wsClients.end());
471 14 }
472 14 return;
473 }
474
475 // Otherwise treat as plain HTTP. We need the body if Content-Length
476 // says so; recv any remainder. Apache proxies /ws here without the UI
477 // password, so the length is a stranger's claim: cap it (a WLED state
478 // post is a few hundred bytes; frames are capped the same in ReadFrame)
479 // and don't let a stalled sender hold this thread forever.
480 static constexpr long MAX_HTTP_BODY = 64 * 1024;
481
2/4
✓ Branch 0 taken 248411 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 248411 times.
✗ Branch 3 not taken.
248411 long contentLen = std::strtol(headerValue(headers, "Content-Length").c_str(), nullptr, 10);
482
3/4
✓ Branch 0 taken 248411 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 248410 times.
248411 if (contentLen < 0 || contentLen > MAX_HTTP_BODY) {
483 static const char tooLarge[] = "HTTP/1.1 413 Payload Too Large\r\nContent-Length: 0\r\nConnection: close\r\n\r\n";
484
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 writeAll(fd, tooLarge, sizeof(tooLarge) - 1);
485 1 return;
486 }
487
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 248409 times.
248410 if (static_cast<long>(body.size()) < contentLen) {
488 // curl and others wait for this before sending the body; without it
489 // Apache and this thread wait on each other until the proxy timeout.
490
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 std::string expect = headerValue(headers, "Expect");
491
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (strcasecmp(expect.c_str(), "100-continue") == 0) {
492 static const char cont[] = "HTTP/1.1 100 Continue\r\n\r\n";
493
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 writeAll(fd, cont, sizeof(cont) - 1);
494 }
495 1 struct timeval tv = { 5, 0 };
496 1 ::setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
497 1 }
498
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 248410 times.
248411 while (static_cast<long>(body.size()) < contentLen) {
499 char buf[1024];
500
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 ssize_t n = ::recv(fd, buf, sizeof(buf), 0);
501
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (n <= 0) break;
502
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 body.append(buf, n);
503 }
504
505
1/2
✓ Branch 0 taken 248410 times.
✗ Branch 1 not taken.
248410 HandleHTTPRequest(fd, requestLine, headers, body);
506
12/12
✓ Branch 0 taken 248410 times.
✓ Branch 1 taken 15 times.
✓ Branch 2 taken 248410 times.
✓ Branch 3 taken 15 times.
✓ Branch 4 taken 248410 times.
✓ Branch 5 taken 15 times.
✓ Branch 6 taken 248410 times.
✓ Branch 7 taken 15 times.
✓ Branch 8 taken 248410 times.
✓ Branch 9 taken 15 times.
✓ Branch 10 taken 248410 times.
✓ Branch 11 taken 37 times.
248522 }
507
508 14 bool WLEDAPIResponder::DoWebSocketHandshake(int fd, const std::string& key) {
509
2/4
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✗ Branch 3 not taken.
14 std::string accept = sha1Base64(key + WS_MAGIC);
510
1/2
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
14 std::ostringstream resp;
511 resp << "HTTP/1.1 101 Switching Protocols\r\n"
512 << "Upgrade: websocket\r\n"
513 << "Connection: Upgrade\r\n"
514 << "Sec-WebSocket-Accept: " << accept << "\r\n"
515 << "Server: fppd/" << getFPPVersion() << "\r\n"
516
11/22
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 14 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 14 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 14 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 14 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 14 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 14 times.
✗ Branch 15 not taken.
✓ Branch 16 taken 14 times.
✗ Branch 17 not taken.
✓ Branch 18 taken 14 times.
✗ Branch 19 not taken.
✓ Branch 20 taken 14 times.
✗ Branch 21 not taken.
14 << "\r\n";
517
1/2
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
14 std::string s = resp.str();
518
1/2
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
28 return writeAll(fd, s.data(), s.size());
519 14 }
520
521 62834 bool WLEDAPIResponder::SendTextFrame(int fd, const std::string& payload) {
522 62834 size_t len = payload.size();
523 uint8_t header[10];
524 62834 size_t headerLen = 0;
525 62834 header[headerLen++] = 0x81; // FIN + text opcode
526
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 62834 times.
62834 if (len < 126) {
527 ✗ header[headerLen++] = static_cast<uint8_t>(len);
528
1/2
✓ Branch 0 taken 62834 times.
✗ Branch 1 not taken.
62834 } else if (len <= 0xffff) {
529 62834 header[headerLen++] = 126;
530 62834 header[headerLen++] = (len >> 8) & 0xff;
531 62834 header[headerLen++] = len & 0xff;
532 } else {
533 ✗ header[headerLen++] = 127;
534 ✗ for (int i = 7; i >= 0; --i) header[headerLen++] = (len >> (i * 8)) & 0xff;
535 }
536 62834 std::vector<uint8_t> frame;
537
1/2
✓ Branch 0 taken 62834 times.
✗ Branch 1 not taken.
62834 frame.reserve(headerLen + len);
538
1/2
✓ Branch 0 taken 62834 times.
✗ Branch 1 not taken.
62834 frame.insert(frame.end(), header, header + headerLen);
539
1/2
✓ Branch 0 taken 62834 times.
✗ Branch 1 not taken.
62834 frame.insert(frame.end(), payload.begin(), payload.end());
540
1/2
✓ Branch 0 taken 62834 times.
✗ Branch 1 not taken.
125668 return writeAll(fd, frame.data(), frame.size());
541 62834 }
542
543 ✗ bool WLEDAPIResponder::SendCloseFrame(int fd) {
544 ✗ uint8_t close[2] = { 0x88, 0x00 };
545 ✗ return writeAll(fd, close, sizeof(close));
546 }
547
548 29109 int WLEDAPIResponder::ReadFrame(int fd, std::string& out) {
549 uint8_t hdr[2];
550
3/4
✓ Branch 0 taken 29109 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✓ Branch 3 taken 29097 times.
29109 if (!readExact(fd, hdr, 2)) return -1;
551
552 29097 uint8_t opcode = hdr[0] & 0x0f;
553 29097 bool masked = (hdr[1] & 0x80) != 0;
554 29097 uint64_t len = hdr[1] & 0x7f;
555
556
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 29096 times.
29097 if (len == 126) {
557 uint8_t e[2];
558
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
1 if (!readExact(fd, e, 2)) return -1;
559 1 len = (uint64_t(e[0]) << 8) | e[1];
560
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 29096 times.
29096 } else if (len == 127) {
561 uint8_t e[8];
562 ✗ if (!readExact(fd, e, 8)) return -1;
563 ✗ len = 0;
564 ✗ for (int i = 0; i < 8; ++i) len = (len << 8) | e[i];
565 }
566
567 29097 uint8_t mask[4] = {};
568
4/8
✓ Branch 0 taken 29097 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 29097 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 29097 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 29097 times.
29097 if (masked && !readExact(fd, mask, 4)) return -1;
569
570 // Sanity cap — the WLED iOS app sends very small messages.
571
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 29097 times.
29097 if (len > 65536) return -1;
572 // RFC 6455 5.5: a control frame (close, ping, pong) carries at most 125
573 // bytes. A longer ping was echoed with its length in a single byte,
574 // whose top bit is the MASK flag, so the pong was unparseable.
575
3/4
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 29096 times.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
29097 if ((opcode & 0x8) && len > 125) return -1;
576
577 29096 std::string payload;
578
1/2
✓ Branch 0 taken 29096 times.
✗ Branch 1 not taken.
29096 payload.resize(len);
579
4/8
✓ Branch 0 taken 29096 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 29096 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 29096 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 29096 times.
29096 if (len && !readExact(fd, payload.data(), len)) return -1;
580
1/2
✓ Branch 0 taken 29096 times.
✗ Branch 1 not taken.
29096 if (masked) {
581
2/2
✓ Branch 0 taken 349325 times.
✓ Branch 1 taken 29096 times.
378421 for (uint64_t i = 0; i < len; ++i) payload[i] ^= mask[i & 3];
582 }
583
584
1/5
✓ Branch 0 taken 29096 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
29096 switch (opcode) {
585 29096 case 0x1: // text
586 29096 out = std::move(payload);
587 29096 return 1;
588 ✗ case 0x8: { // close
589 // Under m_clientsLock, as every other write to a WebSocket is: a
590 // frame written alongside a broadcast would interleave with it.
591 ✗ std::unique_lock<std::mutex> lock(m_clientsLock);
592 ✗ SendCloseFrame(fd);
593 ✗ return -1;
594 ✗ }
595 ✗ case 0x9: { // ping → pong with same payload (at most 125 bytes, above)
596 ✗ std::vector<uint8_t> pong;
597 ✗ pong.push_back(0x8a);
598 ✗ pong.push_back(static_cast<uint8_t>(payload.size()));
599 ✗ pong.insert(pong.end(), payload.begin(), payload.end());
600 ✗ std::unique_lock<std::mutex> lock(m_clientsLock);
601 // A pong only partly written leaves the stream mid-frame.
602 ✗ return writeAll(fd, pong.data(), pong.size()) ? 0 : -1;
603 ✗ }
604 ✗ case 0xa: // pong
605 ✗ return 0;
606 ✗ default:
607 // Binary, continuation, or anything else — ignore for now.
608 ✗ return 0;
609 }
610 29096 }
611
612 14 void WLEDAPIResponder::RunWebSocket(int fd) {
613 14 std::string msg;
614
1/2
✓ Branch 0 taken 29109 times.
✗ Branch 1 not taken.
29109 while (m_running) {
615
1/2
✓ Branch 0 taken 29109 times.
✗ Branch 1 not taken.
29109 int r = ReadFrame(fd, msg);
616
2/2
✓ Branch 0 taken 13 times.
✓ Branch 1 taken 29096 times.
29110 if (r < 0) break;
617
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 29096 times.
29096 if (r == 0) continue;
618
619 // Apply incoming WledState JSON, then echo back the new combined
620 // {state, info} just like the real WLED node does.
621
1/2
✓ Branch 0 taken 29096 times.
✗ Branch 1 not taken.
29096 ApplyStatePost(msg);
622 // Under m_clientsLock, as NotifyStateChanged() writes: two writers on
623 // one socket would interleave their frames.
624
1/2
✓ Branch 0 taken 29096 times.
✗ Branch 1 not taken.
29096 std::string reply = BuildCombinedJson();
625
1/2
✓ Branch 0 taken 29096 times.
✗ Branch 1 not taken.
29096 std::unique_lock<std::mutex> lock(m_clientsLock);
626
3/4
✓ Branch 0 taken 29096 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 29095 times.
29096 if (!SendTextFrame(fd, reply)) break;
627
4/4
✓ Branch 0 taken 29095 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 29095 times.
✓ Branch 3 taken 1 times.
29097 }
628 14 }
629
630 96542 void WLEDAPIResponder::NotifyStateChanged() {
631
1/2
✓ Branch 0 taken 96542 times.
✗ Branch 1 not taken.
96542 std::string snapshot = BuildCombinedJson();
632
1/2
✓ Branch 0 taken 96542 times.
✗ Branch 1 not taken.
96542 std::unique_lock<std::mutex> lock(m_clientsLock);
633
2/2
✓ Branch 0 taken 33724 times.
✓ Branch 1 taken 96542 times.
130266 for (auto it = m_wsClients.begin(); it != m_wsClients.end();) {
634
3/4
✓ Branch 0 taken 33724 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 33722 times.
✓ Branch 3 taken 2 times.
33724 if (SendTextFrame(*it, snapshot)) {
635 33722 ++it;
636 33722 continue;
637 }
638 // A send that failed or timed out (SO_SNDTIMEO) may have written
639 // part of the frame, so whatever followed on this connection would
640 // be read from the middle of it; and a client that stopped reading
641 // would cost every later broadcast the full timeout again, all of
642 // it under this lock. Drop it: shutdown() ends its ClientLoop's
643 // read, and that thread untracks and closes the fd.
644 2 ::shutdown(*it, SHUT_RDWR);
645
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 it = m_wsClients.erase(it);
646 }
647 96542 }
648
649 248410 bool WLEDAPIResponder::HandleHTTPRequest(int fd, const std::string& reqLine,
650 const std::string& /*headers*/,
651 const std::string& body) {
652 248410 std::string method, path;
653 {
654
1/2
✓ Branch 0 taken 248410 times.
✗ Branch 1 not taken.
248410 std::istringstream is(reqLine);
655
2/4
✓ Branch 0 taken 248410 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 248410 times.
✗ Branch 3 not taken.
248410 is >> method >> path;
656 248410 }
657
658 248410 auto respond = [&](int code, const std::string& ctype, const std::string& payload) {
659
1/2
✓ Branch 0 taken 248410 times.
✗ Branch 1 not taken.
248410 std::ostringstream r;
660
2/4
✓ Branch 0 taken 248410 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 248410 times.
✗ Branch 3 not taken.
248410 r << "HTTP/1.1 " << code << " " << reasonPhrase(code) << "\r\n"
661 << "Content-Type: " << ctype << "\r\n"
662 << "Access-Control-Allow-Origin: *\r\n"
663 << "Cache-Control: no-store\r\n"
664
10/20
✓ Branch 0 taken 248410 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 248410 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 248410 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 248410 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 248410 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 248410 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 248410 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 248410 times.
✗ Branch 15 not taken.
✓ Branch 16 taken 248410 times.
✗ Branch 17 not taken.
✓ Branch 18 taken 248410 times.
✗ Branch 19 not taken.
248410 << "Content-Length: " << payload.size() << "\r\n"
665 << "Connection: close\r\n\r\n"
666
3/6
✓ Branch 0 taken 248410 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 248410 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 248410 times.
✗ Branch 5 not taken.
248410 << payload;
667
1/2
✓ Branch 0 taken 248410 times.
✗ Branch 1 not taken.
248410 std::string s = r.str();
668
1/2
✓ Branch 0 taken 248410 times.
✗ Branch 1 not taken.
248410 writeAll(fd, s.data(), s.size());
669 248410 };
670
671
7/10
✓ Branch 0 taken 248410 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 164562 times.
✓ Branch 3 taken 83848 times.
✓ Branch 4 taken 164562 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 164562 times.
✓ Branch 8 taken 83848 times.
✓ Branch 9 taken 164562 times.
248410 if (path == "/json/info" || path == "/json/info/") {
672
3/6
✓ Branch 0 taken 83848 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 83848 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 83848 times.
✗ Branch 5 not taken.
251544 respond(200, "application/json", BuildInfoJson());
673
7/10
✓ Branch 0 taken 164562 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 25 times.
✓ Branch 3 taken 164537 times.
✓ Branch 4 taken 25 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 25 times.
✓ Branch 8 taken 164537 times.
✓ Branch 9 taken 25 times.
164562 } else if (path == "/json/state" || path == "/json/state/") {
674
8/12
✓ Branch 0 taken 164537 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 97091 times.
✓ Branch 3 taken 67446 times.
✓ Branch 4 taken 97091 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 97091 times.
✓ Branch 8 taken 67446 times.
✓ Branch 9 taken 97091 times.
✓ Branch 10 taken 67446 times.
✗ Branch 11 not taken.
164537 if (method == "POST" || method == "PUT") ApplyStatePost(body);
675
3/6
✓ Branch 0 taken 164537 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 164537 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 164537 times.
✗ Branch 5 not taken.
493611 respond(200, "application/json", BuildStateJson());
676
7/10
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20 times.
✓ Branch 3 taken 5 times.
✓ Branch 4 taken 20 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 20 times.
✓ Branch 8 taken 5 times.
✓ Branch 9 taken 20 times.
25 } else if (path == "/json/eff" || path == "/json/eff/") {
677
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 respond(200, "application/json", BuildEffectsJson());
678
3/10
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 20 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✓ Branch 8 taken 20 times.
✗ Branch 9 not taken.
20 } else if (path == "/json" || path == "/json/") {
679
3/6
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 20 times.
✗ Branch 5 not taken.
60 respond(200, "application/json", BuildCombinedJson());
680 } else {
681 ✗ respond(404, "text/plain", "not found\n");
682 }
683 248410 return true;
684 248410 }
685
686 // ----- JSON builders ------------------------------------------------------
687
688 209520 std::string WLEDAPIResponder::BuildInfoJson() {
689
1/2
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
209520 Json::Value info;
690
3/6
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 209520 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 209520 times.
✗ Branch 5 not taken.
209520 info["ver"] = getFPPVersion();
691
2/4
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 209520 times.
✗ Branch 3 not taken.
209520 info["vid"] = 0;
692
2/4
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 209520 times.
✗ Branch 3 not taken.
209520 info["cn"] = "FPP";
693
3/6
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 209520 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 209520 times.
✗ Branch 5 not taken.
209520 info["release"] = getFPPVersion();
694 // Prefer the actual hostname for the display name. HostDescription
695 // is an optional free-text label; if it's empty or still set to the
696 // default placeholder "FPP", fall back to HostName so the WLED
697 // client shows the same identifier the user sees on the FPP UI and
698 // on the network (matching the mDNS service name).
699
1/2
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
209520 std::string hn = getSetting("HostName");
700
1/2
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
209520 std::string hd = getSetting("HostDescription");
701 209520 std::string nm;
702
4/8
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 209520 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 209520 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 209520 times.
209520 if (!hd.empty() && hd != "FPP") {
703 ✗ nm = hd;
704
1/2
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
209520 } else if (!hn.empty()) {
705
1/2
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
209520 nm = hn;
706 } else {
707 ✗ nm = "FPP";
708 }
709
2/4
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 209520 times.
✗ Branch 3 not taken.
209520 info["name"] = nm;
710
2/4
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 209520 times.
✗ Branch 3 not taken.
209520 info["brand"] = "FPP";
711
2/4
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 209520 times.
✗ Branch 3 not taken.
209520 info["product"] = "FalconPlayer";
712
3/6
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 209520 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 209520 times.
✗ Branch 5 not taken.
209520 info["arch"] = getSetting("Platform");
713
714
1/2
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
209520 Json::Value leds(Json::objectValue);
715 209520 int totalPixels = 0;
716 209520 int segCount = 0;
717
3/4
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 140887 times.
✓ Branch 3 taken 209520 times.
350407 for (const auto& name : PixelOverlayManager::INSTANCE.getModelNamesSnapshot()) {
718
1/2
✓ Branch 0 taken 140887 times.
✗ Branch 1 not taken.
140887 auto m = PixelOverlayManager::INSTANCE.getModelShared(name);
719
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 140887 times.
140887 if (!m) continue;
720 140887 int w = m->getWidth();
721 140887 int h = m->getHeight();
722 140887 totalPixels += std::max(1, w) * std::max(1, h);
723 140887 ++segCount;
724
1/2
✓ Branch 0 taken 140887 times.
✗ Branch 1 not taken.
350407 }
725
4/6
✓ Branch 0 taken 140887 times.
✓ Branch 1 taken 68633 times.
✓ Branch 2 taken 209520 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 209520 times.
✗ Branch 5 not taken.
209520 leds["count"] = totalPixels ? totalPixels : 1;
726
2/4
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 209520 times.
✗ Branch 3 not taken.
209520 leds["pwr"] = 0;
727
2/4
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 209520 times.
✗ Branch 3 not taken.
209520 leds["fps"] = 40;
728
2/4
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 209520 times.
✗ Branch 3 not taken.
209520 leds["maxpwr"] = 0;
729
4/6
✓ Branch 0 taken 140887 times.
✓ Branch 1 taken 68633 times.
✓ Branch 2 taken 209520 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 209520 times.
✗ Branch 5 not taken.
209520 leds["maxseg"] = segCount ? segCount : 1;
730
2/4
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 209520 times.
✗ Branch 3 not taken.
209520 leds["bootps"] = 0;
731
2/4
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 209520 times.
✗ Branch 3 not taken.
209520 leds["rgbw"] = false;
732
2/4
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 209520 times.
✗ Branch 3 not taken.
209520 leds["wv"] = 0;
733
2/4
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 209520 times.
✗ Branch 3 not taken.
209520 leds["cct"] = false;
734
2/4
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 209520 times.
✗ Branch 3 not taken.
209520 leds["lc"] = 1;
735
2/4
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 209520 times.
✗ Branch 3 not taken.
209520 info["leds"] = leds;
736
737
3/6
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 209520 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 209520 times.
✗ Branch 5 not taken.
209520 info["fxcount"] = static_cast<int>(WLEDEffect::getWLEDAPIEffectNames().size());
738
2/4
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 209520 times.
✗ Branch 3 not taken.
209520 info["palcount"] = 1;
739 {
740 209520 std::string mac, ip;
741
1/2
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
209520 primaryNetIdentity(mac, ip);
742
2/4
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 209520 times.
✗ Branch 3 not taken.
209520 info["mac"] = mac;
743
2/4
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 209520 times.
✗ Branch 3 not taken.
209520 info["ip"] = ip;
744 209520 }
745
2/4
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 209520 times.
✗ Branch 3 not taken.
209520 info["udpport"] = 21324;
746
2/4
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 209520 times.
✗ Branch 3 not taken.
209520 info["live"] = false;
747 {
748 // Client threads add and remove themselves while others build this;
749 // every caller builds it before taking m_clientsLock.
750
1/2
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
209520 std::unique_lock<std::mutex> lock(m_clientsLock);
751
2/4
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 209520 times.
✗ Branch 3 not taken.
209520 info["ws"] = static_cast<int>(m_wsClients.size());
752 209520 }
753
2/4
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 209520 times.
✗ Branch 3 not taken.
209520 info["str"] = false;
754
755 // The WLED Codable expects a `wifi` object even on wired devices.
756 // Reporting a fake-good signal keeps clients from rendering us as
757 // a degraded node; for an Ethernet-only FPP this is conventional.
758
1/2
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
209520 Json::Value wifi(Json::objectValue);
759
2/4
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 209520 times.
✗ Branch 3 not taken.
209520 wifi["bssid"] = "00:00:00:00:00:00";
760
2/4
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 209520 times.
✗ Branch 3 not taken.
209520 wifi["rssi"] = -50;
761
2/4
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 209520 times.
✗ Branch 3 not taken.
209520 wifi["signal"] = 100;
762
2/4
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 209520 times.
✗ Branch 3 not taken.
209520 wifi["channel"] = 0;
763
2/4
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 209520 times.
✗ Branch 3 not taken.
209520 info["wifi"] = wifi;
764
765
2/4
✓ Branch 0 taken 209520 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 209520 times.
✗ Branch 3 not taken.
419040 return SaveJsonToString(info, "");
766 209520 }
767
768 290209 std::string WLEDAPIResponder::BuildStateJson() {
769
1/2
✓ Branch 0 taken 290209 times.
✗ Branch 1 not taken.
290209 Json::Value state;
770
1/2
✓ Branch 0 taken 290209 times.
✗ Branch 1 not taken.
290209 Json::Value segs(Json::arrayValue);
771
772 290209 bool anyOn = false;
773 290209 int idx = 0;
774
1/2
✓ Branch 0 taken 290209 times.
✗ Branch 1 not taken.
290209 const auto& effects = WLEDEffect::getWLEDAPIEffectNames();
775
3/4
✓ Branch 0 taken 290209 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 183436 times.
✓ Branch 3 taken 290209 times.
473645 for (const auto& name : PixelOverlayManager::INSTANCE.getModelNamesSnapshot()) {
776
1/2
✓ Branch 0 taken 183436 times.
✗ Branch 1 not taken.
183436 auto m = PixelOverlayManager::INSTANCE.getModelShared(name);
777
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 183436 times.
183436 if (!m) continue;
778
779 183436 int w = m->getWidth();
780 183436 int h = m->getHeight();
781 183436 int len = std::max(1, w) * std::max(1, h);
782
783 183436 bool on = false;
784 183436 std::string fxName;
785 {
786
1/2
✓ Branch 0 taken 183436 times.
✗ Branch 1 not taken.
183436 std::unique_lock<std::recursive_mutex> lock(m->getRunningEffectMutex());
787
2/2
✓ Branch 0 taken 42 times.
✓ Branch 1 taken 183394 times.
183436 if (m->getRunningEffect()) {
788 42 on = true;
789
2/4
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 42 times.
✗ Branch 3 not taken.
42 fxName = m->getRunningEffect()->name();
790 }
791 183436 }
792
3/4
✓ Branch 0 taken 183436 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 183327 times.
✓ Branch 3 taken 109 times.
183436 if ((int)m->getState().getState() > 0) on = true;
793
2/2
✓ Branch 0 taken 183327 times.
✓ Branch 1 taken 109 times.
183436 if (on) anyOn = true;
794
795 183436 int fxIdx = 0;
796
2/2
✓ Branch 0 taken 42 times.
✓ Branch 1 taken 183394 times.
183436 if (!fxName.empty()) {
797 42 int i = 0;
798
1/2
✓ Branch 0 taken 1008 times.
✗ Branch 1 not taken.
1008 for (const auto& en : effects) {
799
2/2
✓ Branch 0 taken 42 times.
✓ Branch 1 taken 966 times.
1008 if (en == fxName) { fxIdx = i; break; }
800 966 ++i;
801 }
802 }
803
804
1/2
✓ Branch 0 taken 183436 times.
✗ Branch 1 not taken.
183436 Json::Value s;
805
2/4
✓ Branch 0 taken 183436 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 183436 times.
✗ Branch 3 not taken.
183436 s["id"] = idx;
806
2/4
✓ Branch 0 taken 183436 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 183436 times.
✗ Branch 3 not taken.
183436 s["start"] = 0;
807
2/4
✓ Branch 0 taken 183436 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 183436 times.
✗ Branch 3 not taken.
183436 s["stop"] = len;
808
2/4
✓ Branch 0 taken 183436 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 183436 times.
✗ Branch 3 not taken.
183436 s["len"] = len;
809
2/4
✓ Branch 0 taken 183436 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 183436 times.
✗ Branch 3 not taken.
183436 s["grp"] = 1;
810
2/4
✓ Branch 0 taken 183436 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 183436 times.
✗ Branch 3 not taken.
183436 s["spc"] = 0;
811
2/4
✓ Branch 0 taken 183436 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 183436 times.
✗ Branch 3 not taken.
183436 s["of"] = 0;
812
2/4
✓ Branch 0 taken 183436 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 183436 times.
✗ Branch 3 not taken.
183436 s["on"] = on;
813
2/4
✓ Branch 0 taken 183436 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 183436 times.
✗ Branch 3 not taken.
183436 s["frz"] = false;
814
2/4
✓ Branch 0 taken 183436 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 183436 times.
✗ Branch 3 not taken.
183436 s["bri"] = 255;
815
2/4
✓ Branch 0 taken 183436 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 183436 times.
✗ Branch 3 not taken.
183436 s["cct"] = 127;
816
2/4
✓ Branch 0 taken 183436 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 183436 times.
✗ Branch 3 not taken.
183436 s["set"] = 0;
817
2/4
✓ Branch 0 taken 183436 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 183436 times.
✗ Branch 3 not taken.
183436 s["n"] = name;
818
1/2
✓ Branch 0 taken 183436 times.
✗ Branch 1 not taken.
183436 Json::Value cols(Json::arrayValue);
819
2/2
✓ Branch 0 taken 550308 times.
✓ Branch 1 taken 183436 times.
733744 for (int c = 0; c < 3; ++c) {
820
1/2
✓ Branch 0 taken 550308 times.
✗ Branch 1 not taken.
550308 Json::Value rgbw(Json::arrayValue);
821
8/10
✓ Branch 0 taken 733744 times.
✓ Branch 1 taken 1467488 times.
✓ Branch 2 taken 550308 times.
✓ Branch 3 taken 183436 times.
✓ Branch 4 taken 2201232 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2201232 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2201232 times.
✓ Branch 9 taken 550308 times.
2751540 for (int b = 0; b < 4; ++b) rgbw.append(c == 0 && b < 3 ? 255 : 0);
822
1/2
✓ Branch 0 taken 550308 times.
✗ Branch 1 not taken.
550308 cols.append(rgbw);
823 550308 }
824
2/4
✓ Branch 0 taken 183436 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 183436 times.
✗ Branch 3 not taken.
183436 s["col"] = cols;
825
2/4
✓ Branch 0 taken 183436 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 183436 times.
✗ Branch 3 not taken.
183436 s["fx"] = fxIdx;
826
2/4
✓ Branch 0 taken 183436 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 183436 times.
✗ Branch 3 not taken.
183436 s["sx"] = 128;
827
2/4
✓ Branch 0 taken 183436 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 183436 times.
✗ Branch 3 not taken.
183436 s["ix"] = 128;
828
2/4
✓ Branch 0 taken 183436 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 183436 times.
✗ Branch 3 not taken.
183436 s["pal"] = 0;
829
2/4
✓ Branch 0 taken 183436 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 183436 times.
✗ Branch 3 not taken.
183436 s["sel"] = idx == 0;
830
2/4
✓ Branch 0 taken 183436 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 183436 times.
✗ Branch 3 not taken.
183436 s["rev"] = false;
831
2/4
✓ Branch 0 taken 183436 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 183436 times.
✗ Branch 3 not taken.
183436 s["mi"] = false;
832
1/2
✓ Branch 0 taken 183436 times.
✗ Branch 1 not taken.
183436 segs.append(s);
833 183436 ++idx;
834
1/2
✓ Branch 0 taken 183436 times.
✗ Branch 1 not taken.
473645 }
835
3/4
✓ Branch 0 taken 290209 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 106773 times.
✓ Branch 3 taken 183436 times.
290209 if (segs.empty()) {
836
1/2
✓ Branch 0 taken 106773 times.
✗ Branch 1 not taken.
106773 Json::Value s;
837
8/16
✓ Branch 0 taken 106773 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 106773 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 106773 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 106773 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 106773 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 106773 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 106773 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 106773 times.
✗ Branch 15 not taken.
106773 s["id"] = 0; s["start"] = 0; s["stop"] = 1; s["len"] = 1;
838
8/16
✓ Branch 0 taken 106773 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 106773 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 106773 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 106773 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 106773 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 106773 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 106773 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 106773 times.
✗ Branch 15 not taken.
106773 s["on"] = false; s["bri"] = 255; s["fx"] = 0; s["sx"] = 128;
839
6/12
✓ Branch 0 taken 106773 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 106773 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 106773 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 106773 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 106773 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 106773 times.
✗ Branch 11 not taken.
106773 s["ix"] = 128; s["pal"] = 0; s["sel"] = true;
840
2/4
✓ Branch 0 taken 106773 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 106773 times.
✗ Branch 3 not taken.
106773 s["n"] = "Placeholder";
841
1/2
✓ Branch 0 taken 106773 times.
✗ Branch 1 not taken.
106773 segs.append(s);
842 106773 }
843
844
2/4
✓ Branch 0 taken 290209 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 290209 times.
✗ Branch 3 not taken.
290209 state["on"] = anyOn;
845
2/4
✓ Branch 0 taken 290209 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 290209 times.
✗ Branch 3 not taken.
290209 state["bri"] = 255;
846
2/4
✓ Branch 0 taken 290209 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 290209 times.
✗ Branch 3 not taken.
290209 state["transition"] = 7;
847
2/4
✓ Branch 0 taken 290209 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 290209 times.
✗ Branch 3 not taken.
290209 state["ps"] = -1;
848
2/4
✓ Branch 0 taken 290209 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 290209 times.
✗ Branch 3 not taken.
290209 state["pl"] = -1;
849
2/4
✓ Branch 0 taken 290209 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 290209 times.
✗ Branch 3 not taken.
290209 state["mainseg"] = 0;
850
2/4
✓ Branch 0 taken 290209 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 290209 times.
✗ Branch 3 not taken.
290209 state["seg"] = segs;
851
2/4
✓ Branch 0 taken 290209 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 290209 times.
✗ Branch 3 not taken.
580418 return SaveJsonToString(state, "");
852 290209 }
853
854 125672 std::string WLEDAPIResponder::BuildCombinedJson() {
855
1/2
✓ Branch 0 taken 125672 times.
✗ Branch 1 not taken.
125672 Json::Value root;
856
1/2
✓ Branch 0 taken 125672 times.
✗ Branch 1 not taken.
125672 Json::Reader r;
857
2/4
✓ Branch 0 taken 125672 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 125672 times.
✗ Branch 3 not taken.
125672 Json::Value s, i;
858
2/4
✓ Branch 0 taken 125672 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 125672 times.
✗ Branch 3 not taken.
125672 r.parse(BuildStateJson(), s);
859
2/4
✓ Branch 0 taken 125672 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 125672 times.
✗ Branch 3 not taken.
125672 r.parse(BuildInfoJson(), i);
860
2/4
✓ Branch 0 taken 125672 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 125672 times.
✗ Branch 3 not taken.
125672 root["state"] = s;
861
2/4
✓ Branch 0 taken 125672 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 125672 times.
✗ Branch 3 not taken.
125672 root["info"] = i;
862
2/4
✓ Branch 0 taken 125672 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 125672 times.
✗ Branch 3 not taken.
251344 return SaveJsonToString(root, "");
863 125672 }
864
865 5 std::string WLEDAPIResponder::BuildEffectsJson() {
866
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 Json::Value arr(Json::arrayValue);
867
5/8
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1095 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1095 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1095 times.
✓ Branch 7 taken 5 times.
1100 for (auto& n : WLEDEffect::getWLEDAPIEffectNames()) arr.append(n);
868
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
10 return SaveJsonToString(arr, "");
869 5 }
870
871 96542 void WLEDAPIResponder::ApplyStatePost(const std::string& jsonBody) {
872
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 96542 times.
96542 if (jsonBody.empty()) return;
873
1/2
✓ Branch 0 taken 96542 times.
✗ Branch 1 not taken.
96542 Json::Value root;
874
1/2
✓ Branch 0 taken 96542 times.
✗ Branch 1 not taken.
96542 Json::Reader rdr;
875
2/4
✓ Branch 0 taken 96542 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 96542 times.
96542 if (!rdr.parse(jsonBody, root)) {
876 ✗ LogDebug(VB_GENERAL, "WLEDAPIResponder: ignoring malformed JSON body\n");
877 ✗ return;
878 }
879
880 // Snapshot the current model name list as a vector so we can index it.
881
1/2
✓ Branch 0 taken 96542 times.
✗ Branch 1 not taken.
96542 std::vector<std::string> modelNames = PixelOverlayManager::INSTANCE.getModelNamesSnapshot();
882
883
2/4
✓ Branch 0 taken 96542 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 96542 times.
96542 if (!root.isObject()) {
884 ✗ return;
885 }
886
887 65529 auto applyOnOff = [&](const std::string& modelName, bool on) {
888
1/2
✓ Branch 0 taken 65529 times.
✗ Branch 1 not taken.
65529 auto m = PixelOverlayManager::INSTANCE.getModelShared(modelName);
889
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 65529 times.
65529 if (!m) return;
890
1/2
✓ Branch 0 taken 65529 times.
✗ Branch 1 not taken.
131058 m->setState(PixelOverlayState(on ? PixelOverlayState::Enabled
891
1/2
✓ Branch 0 taken 65529 times.
✗ Branch 1 not taken.
65529 : PixelOverlayState::Disabled));
892
1/2
✓ Branch 0 taken 65529 times.
✗ Branch 1 not taken.
65529 };
893
894 // Read only through a const reference: a non-const operator[] inserts a
895 // null member, and in jsonInt(seg["fx"], seg.isMember("fx") ? ...)
896 // whether that happened before the isMember() test depended on the
897 // compiler's argument evaluation order.
898 96542 const Json::Value& req = root;
899
900
8/10
✓ Branch 0 taken 96542 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 96511 times.
✓ Branch 3 taken 31 times.
✓ Branch 4 taken 96511 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 96510 times.
✓ Branch 7 taken 1 times.
✓ Branch 8 taken 96506 times.
✓ Branch 9 taken 36 times.
193052 if (req.isMember("on") && !req.isMember("seg") &&
901
7/12
✓ Branch 0 taken 96510 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 96510 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✓ Branch 5 taken 96506 times.
✓ Branch 6 taken 4 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 4 times.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✓ Branch 11 taken 4 times.
96510 (req["on"].isBool() || req["on"].isNumeric())) {
902
2/4
✓ Branch 0 taken 96506 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 96506 times.
✗ Branch 3 not taken.
96506 bool on = jsonBool(req["on"], false);
903
3/4
✓ Branch 0 taken 65529 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 65529 times.
✓ Branch 3 taken 96506 times.
162035 for (auto& n : modelNames) applyOnOff(n, on);
904 }
905
906 // WLED takes "seg" as one segment object as well as an array of them.
907
1/2
✓ Branch 0 taken 96542 times.
✗ Branch 1 not taken.
96542 Json::Value segList(Json::arrayValue);
908
4/6
✓ Branch 0 taken 96542 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 96542 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 31 times.
✓ Branch 5 taken 96511 times.
96542 if (req["seg"].isArray()) {
909
2/4
✓ Branch 0 taken 31 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 31 times.
✗ Branch 3 not taken.
31 segList = req["seg"];
910
4/6
✓ Branch 0 taken 96511 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 96511 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 96510 times.
96511 } else if (req["seg"].isObject()) {
911
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 segList.append(req["seg"]);
912 }
913
914
3/4
✓ Branch 0 taken 96542 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 32 times.
✓ Branch 3 taken 96510 times.
96542 if (!segList.empty()) {
915
1/2
✓ Branch 0 taken 32 times.
✗ Branch 1 not taken.
32 const auto& effList = WLEDEffect::getWLEDAPIEffectNames();
916
1/2
✓ Branch 0 taken 32 times.
✗ Branch 1 not taken.
32 std::vector<std::string> effects(effList.begin(), effList.end());
917
918
7/12
✓ Branch 0 taken 32 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 32 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 32 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 32 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 64 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 32 times.
✓ Branch 11 taken 32 times.
64 for (const auto& seg : segList) {
919
3/4
✓ Branch 0 taken 32 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 30 times.
32 if (!seg.isObject()) continue;
920 // No id means the main segment (0); one that isn't a valid
921 // index means none.
922
4/8
✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 30 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 30 times.
✗ Branch 7 not taken.
30 int id = seg.isMember("id") ? jsonInt(seg["id"], -1) : 0;
923
5/6
✓ Branch 0 taken 26 times.
✓ Branch 1 taken 4 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 26 times.
✓ Branch 4 taken 4 times.
✓ Branch 5 taken 26 times.
30 if (id < 0 || id >= (int)modelNames.size()) continue;
924 26 const std::string& modelName = modelNames[id];
925
926
7/20
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 26 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 26 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 26 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 26 times.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✓ Branch 11 taken 26 times.
✗ Branch 12 not taken.
✓ Branch 13 taken 26 times.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
26 if (seg["on"].isBool() || seg["on"].isNumeric()) applyOnOff(modelName, jsonBool(seg["on"], false));
927
928 // Effect change via WLED `fx` index → look up name. We also
929 // rebuild the args from the rest of the segment fields any
930 // time a fx index, sx/ix/bri, or col[] is provided so live
931 // slider drags update the running effect.
932
4/6
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 1 times.
29 bool hasParamUpdate = seg.isMember("fx") || seg.isMember("sx") ||
933
4/8
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
✗ Branch 7 not taken.
2 seg.isMember("ix") || seg.isMember("bri") ||
934
6/10
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 23 times.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 2 times.
29 seg.isMember("col") || seg.isMember("pal");
935
2/2
✓ Branch 0 taken 24 times.
✓ Branch 1 taken 2 times.
26 if (hasParamUpdate) {
936
1/2
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
24 auto m = PixelOverlayManager::INSTANCE.getModelShared(modelName);
937
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 24 times.
24 if (!m) continue;
938 24 std::string fxName;
939
3/4
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 23 times.
✓ Branch 3 taken 1 times.
24 if (seg.isMember("fx")) {
940
2/4
✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 23 times.
✗ Branch 3 not taken.
23 int fxIdx = jsonInt(seg["fx"], -1);
941
5/6
✓ Branch 0 taken 21 times.
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 21 times.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 21 times.
23 if (fxIdx < 0 || fxIdx >= (int)effects.size()) continue;
942
1/2
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
21 fxName = effects[fxIdx];
943
3/4
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 19 times.
21 if (fxName == "RSVD") {
944 // A retired effect's number, kept so the ones after
945 // it don't move.
946
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 LogWarn(VB_GENERAL, "WLEDAPIResponder: effect %d is no longer offered -- ignoring\n", fxIdx);
947 2 continue;
948 }
949 } else {
950 // A slider drag or colour pick without "fx" adjusts the
951 // effect that is running. It used to fall back to index
952 // 0, which is "Stop Effects".
953
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 std::unique_lock<std::recursive_mutex> lock(m->getRunningEffectMutex());
954
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (m->getRunningEffect()) {
955
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 fxName = m->getRunningEffect()->name();
956 }
957 1 }
958
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 20 times.
20 if (fxName.empty()) continue;
959
1/2
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
20 auto* eff = PixelOverlayEffect::GetPixelOverlayEffect(fxName);
960
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 20 times.
20 if (!eff) continue;
961
962
1/2
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
20 std::vector<std::string> args = BuildEffectArgs(eff, seg);
963 // Through applyEffect(), not eff->apply(): it checks the
964 // argument count every effect relies on.
965
2/4
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20 times.
✗ Branch 3 not taken.
40 m->applyEffect("Enabled", fxName, args);
966
4/4
✓ Branch 0 taken 20 times.
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 20 times.
✓ Branch 3 taken 4 times.
28 }
967 }
968 32 }
969
970
1/2
✓ Branch 0 taken 96542 times.
✗ Branch 1 not taken.
96542 NotifyStateChanged();
971
3/6
✓ Branch 0 taken 96542 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 96542 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 96542 times.
✗ Branch 5 not taken.
96542 }
972
973 // Translate a WLED segment object into the std::vector<std::string>
974 // argument list that PixelOverlayEffect::apply() takes. We walk the
975 // effect's declared CommandArg list and fill each slot by matching the
976 // arg's name to the relevant WLED segment field. Unrecognized args fall
977 // back to the CommandArg's default value, which keeps WLED-ported and
978 // FPP-native effects equally happy.
979 20 std::vector<std::string> WLEDAPIResponder::BuildEffectArgs(PixelOverlayEffect* eff,
980 const Json::Value& seg) {
981 20 std::vector<std::string> out;
982 27 auto colHex = [&](int idx) -> std::string {
983
11/18
✓ Branch 0 taken 27 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✓ Branch 3 taken 12 times.
✓ Branch 4 taken 15 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 15 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 15 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 15 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 15 times.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✓ Branch 15 taken 15 times.
✓ Branch 16 taken 12 times.
✓ Branch 17 taken 15 times.
27 if (!seg.isMember("col") || !seg["col"].isArray() || (int)seg["col"].size() <= idx) {
984
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
24 return "";
985 }
986
2/4
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✗ Branch 3 not taken.
15 const auto& c = seg["col"][idx];
987
5/12
✓ 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.
✗ Branch 6 not taken.
✓ Branch 7 taken 15 times.
✗ Branch 8 not taken.
✓ Branch 9 taken 15 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
15 if (!c.isArray() || c.size() < 3) return "";
988 char buf[16];
989 45 snprintf(buf, sizeof(buf), "#%02X%02X%02X",
990
6/12
✓ 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.
✓ Branch 6 taken 15 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 15 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 15 times.
✗ Branch 11 not taken.
15 jsonInt(c[0], 0) & 0xFF, jsonInt(c[1], 0) & 0xFF, jsonInt(c[2], 0) & 0xFF);
991
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
30 return buf;
992 20 };
993
994 29 auto rangeScaled = [&](int wledVal, int min, int max, int wledMax = 255) -> std::string {
995 // WLED slider values are 0-255 (or 0-31 for c3). FPP effect args
996 // declare their own min/max; if the FPP arg has the same range we
997 // pass through, otherwise we linearly rescale. The value is a
998 // network peer's: clamp it to the slider's range first, as an
999 // out-of-range one overflowed the int conversion below.
1000
1/2
✓ Branch 0 taken 29 times.
✗ Branch 1 not taken.
29 wledVal = std::clamp(wledVal, 0, wledMax);
1001
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 29 times.
29 if (max <= 0) return std::to_string(wledVal);
1002 29 int v = wledVal;
1003
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 24 times.
29 if (max != wledMax) {
1004 // e.g. Blink declares Speed range 1-5000.
1005 5 double f = (double)wledVal / wledMax;
1006 5 v = min + (int)(f * ((double)max - min));
1007 }
1008
2/4
✓ Branch 0 taken 29 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 29 times.
✗ Branch 3 not taken.
29 if (min <= max) v = std::clamp(v, min, max);
1009 29 return std::to_string(v);
1010 };
1011
1012 // RawWLEDEffect (every WLED-ported FX) declares an explicit role
1013 // per arg via getArgRoles(). Use that when available; fall back to
1014 // matching by arg name for the hand-written effects (Blink, Bars,
1015 // ColorFade, Text, and the FPP-native ones) whose arg names match
1016 // the WLED slider naming convention.
1017 // Only the WLED effects are WLEDEffects: the FPP-native ones (Bars,
1018 // Text, Color Fade, Image, Stop Effects) are not, and calling
1019 // getArgRoles() through a static_cast on one of those jumped through a
1020 // vtable slot it doesn't have.
1021 20 std::vector<WLEDArgRole> roles;
1022
3/4
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✓ Branch 3 taken 11 times.
20 if (auto* we = dynamic_cast<WLEDEffect*>(eff)) {
1023
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 roles = we->getArgRoles();
1024 }
1025
1026 20 int idx = 0;
1027
2/2
✓ Branch 0 taken 122 times.
✓ Branch 1 taken 20 times.
142 for (const auto& a : eff->args) {
1028 122 WLEDArgRole role = WLEDArgRole::Unknown;
1029
2/2
✓ Branch 0 taken 65 times.
✓ Branch 1 taken 57 times.
122 if (idx < (int)roles.size()) role = roles[idx];
1030
1031
2/2
✓ Branch 0 taken 57 times.
✓ Branch 1 taken 65 times.
122 if (role == WLEDArgRole::Unknown) {
1032
1/2
✓ Branch 0 taken 57 times.
✗ Branch 1 not taken.
57 std::string lname = a.name;
1033 57 std::transform(lname.begin(), lname.end(), lname.begin(), ::tolower);
1034
3/4
✓ Branch 0 taken 57 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 56 times.
57 if (lname == "brightness") role = WLEDArgRole::Brightness;
1035
7/10
✓ Branch 0 taken 56 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 53 times.
✓ Branch 3 taken 3 times.
✓ Branch 4 taken 53 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 53 times.
✓ Branch 8 taken 3 times.
✓ Branch 9 taken 53 times.
56 else if (lname == "speed" || lname == "sx") role = WLEDArgRole::Speed;
1036
7/10
✓ Branch 0 taken 53 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 52 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 52 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 52 times.
✓ Branch 8 taken 1 times.
✓ Branch 9 taken 52 times.
53 else if (lname == "intensity" || lname == "ix") role = WLEDArgRole::Intensity;
1037
3/4
✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 50 times.
52 else if (lname == "color1") role = WLEDArgRole::Color1;
1038
3/4
✓ Branch 0 taken 50 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✓ Branch 3 taken 47 times.
50 else if (lname == "color2") role = WLEDArgRole::Color2;
1039
3/4
✓ Branch 0 taken 47 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 45 times.
47 else if (lname == "color3") role = WLEDArgRole::Color3;
1040
3/4
✓ Branch 0 taken 45 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 44 times.
45 else if (lname == "palette") role = WLEDArgRole::Palette;
1041 57 }
1042
1043
1/2
✓ Branch 0 taken 122 times.
✗ Branch 1 not taken.
122 std::string v = a.defaultValue;
1044
13/14
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 11 times.
✓ Branch 2 taken 9 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 1 times.
✓ Branch 6 taken 1 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1 times.
✓ Branch 9 taken 10 times.
✓ Branch 10 taken 10 times.
✓ Branch 11 taken 7 times.
✓ Branch 12 taken 9 times.
✓ Branch 13 taken 52 times.
122 switch (role) {
1045 9 case WLEDArgRole::Brightness:
1046
3/6
✓ 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.
9 v = rangeScaled(jsonInt(seg["bri"], 255), a.min, a.max);
1047 9 break;
1048 11 case WLEDArgRole::Speed:
1049
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.
11 v = rangeScaled(jsonInt(seg["sx"], 128), a.min, a.max);
1050 11 break;
1051 9 case WLEDArgRole::Intensity:
1052
3/6
✓ 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.
9 v = rangeScaled(jsonInt(seg["ix"], 128), a.min, a.max);
1053 9 break;
1054 1 case WLEDArgRole::Custom1:
1055
2/10
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
1 if (seg.isMember("c1")) v = rangeScaled(jsonInt(seg["c1"], 0), a.min, a.max);
1056 1 break;
1057 1 case WLEDArgRole::Custom2:
1058
2/10
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
1 if (seg.isMember("c2")) v = rangeScaled(jsonInt(seg["c2"], 0), a.min, a.max);
1059 1 break;
1060 1 case WLEDArgRole::Custom3:
1061
2/10
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
1 if (seg.isMember("c3")) v = rangeScaled(jsonInt(seg["c3"], 0), a.min, a.max, 31);
1062 1 break;
1063 1 case WLEDArgRole::Check1:
1064
2/12
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
1 if (seg.isMember("o1")) v = jsonBool(seg["o1"], false) ? "1" : "0";
1065 1 break;
1066 ✗ case WLEDArgRole::Check2:
1067 ✗ if (seg.isMember("o2")) v = jsonBool(seg["o2"], false) ? "1" : "0";
1068 ✗ break;
1069 1 case WLEDArgRole::Check3:
1070
2/12
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
1 if (seg.isMember("o3")) v = jsonBool(seg["o3"], false) ? "1" : "0";
1071 1 break;
1072
4/6
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✓ Branch 3 taken 4 times.
✓ Branch 4 taken 6 times.
✗ Branch 5 not taken.
10 case WLEDArgRole::Color1: { auto c = colHex(0); if (!c.empty()) v = c; break; }
1073
4/6
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✓ Branch 3 taken 4 times.
✓ Branch 4 taken 6 times.
✗ Branch 5 not taken.
10 case WLEDArgRole::Color2: { auto c = colHex(1); if (!c.empty()) v = c; break; }
1074
4/6
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✓ Branch 3 taken 4 times.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
7 case WLEDArgRole::Color3: { auto c = colHex(2); if (!c.empty()) v = c; break; }
1075 9 case WLEDArgRole::Palette:
1076 // WLED sends a numeric palette index; the WLED-ported effects
1077 // here want the palette name. Without a full palette catalog
1078 // round-trip we leave the default in place.
1079 9 break;
1080 52 default:
1081 52 break;
1082 }
1083
1/2
✓ Branch 0 taken 122 times.
✗ Branch 1 not taken.
122 out.push_back(v);
1084 122 ++idx;
1085 122 }
1086 20 return out;
1087 20 }
1088