GCC Code Coverage Report


Directory: ./
File: NetworkController.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 502 575 87.3%
Functions: 42 42 100.0%
Branches: 765 1581 48.4%

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 #include "fpp-pch.h"
13
14 #include "fpp-json.h"
15
16 #include <regex>
17 #include <stdlib.h>
18 #include <string>
19 #include <vector>
20
21 #include "CurlManager.h"
22 #include "NetworkController.h"
23 #include "common.h"
24 #include "log.h"
25 #include "settings.h"
26 #include "commands/Commands.h"
27 #include "util/RegExCache.h"
28
29 270 NetworkController::NetworkController(const std::string& ipStr) :
30 270 ip(ipStr),
31
1/2
✓ Branch 0 taken 270 times.
✗ Branch 1 not taken.
270 hostname(ipStr),
32
1/2
✓ Branch 0 taken 270 times.
✗ Branch 1 not taken.
540 vendor("Unknown"),
33
1/2
✓ Branch 0 taken 270 times.
✗ Branch 1 not taken.
540 vendorURL("Unknown"),
34 270 typeId(kSysTypeUnknown),
35
1/2
✓ Branch 0 taken 270 times.
✗ Branch 1 not taken.
540 typeStr("Unknown"),
36
1/2
✓ Branch 0 taken 270 times.
✗ Branch 1 not taken.
270 uuid("Unknown"),
37
1/2
✓ Branch 0 taken 270 times.
✗ Branch 1 not taken.
540 ranges("0-0"),
38
1/2
✓ Branch 0 taken 270 times.
✗ Branch 1 not taken.
810 version("Unknown"),
39 270 majorVersion(0),
40 270 minorVersion(0),
41 270 systemMode(PLAYER_MODE) {
42 270 }
43
44 // Detector regexes run over text a LAN device sends us. libstdc++'s std::regex
45 // recurses once per repeated character (GCC 14 overflows the 8MB stack on a
46 // ~30KB run of digits), so every quantifier in them is bounded, and the page
47 // is truncated to this before any detector sees it.
48 static constexpr size_t MAX_DETECT_HTML = 256 * 1024;
49
50 // One in-progress detection. The detectors below are tried in order, and any
51 // of them may have to ask the device a question before it can say yes or no --
52 // so the walk cannot be a loop. Each step either claims the device
53 // (matched()), passes (noMatch(), which advances to the next detector), or
54 // issues a fetch() whose completion does one of those two later. The object
55 // lives until one of those outcomes is reached and deletes it.
56 class NetworkController::Detection {
57 public:
58 // `blocking` drives the whole walk inline on the calling thread, using the
59 // synchronous curl helpers, so the callback has run by the time run()
60 // returns. That is only for the deprecated blocking overload below; FPP's
61 // own callers leave it false.
62 270 Detection(const std::string& ipStr, const std::string& htmlStr,
63 270 std::function<void(NetworkController*)>&& cb, bool blockingCalls = false) :
64 270 ip(ipStr),
65 // Every detector matches near the top of a controller's page; the cap
66 // bounds the regex work a hostile LAN device can make us do.
67
1/2
✓ Branch 0 taken 270 times.
✗ Branch 1 not taken.
270 html(htmlStr.substr(0, MAX_DETECT_HTML)),
68
3/8
✓ Branch 0 taken 270 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 270 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 270 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
270 nc(new NetworkController(ipStr)),
69 270 callback(std::move(cb)),
70 270 blocking(blockingCalls) {
71 270 }
72
73 const std::string ip;
74 const std::string html;
75
76 // Deliberately shared by every detector in turn, exactly as the old
77 // sequential version shared one object: a detector that fills in vendor and
78 // typeId and only then fails its probe leaves that behind for whichever
79 // detector claims the device to overwrite.
80 NetworkController* const nc;
81
82 std::function<void(NetworkController*)> callback;
83 const bool blocking = false;
84 size_t step = 0;
85
86 // Run the detector at `step`, or finish empty once the list is exhausted.
87 2941 void run() {
88 typedef void (NetworkController::*DetectStep)(Detection*);
89 // Order is significant and is the order the blocking version used.
90 static const DetectStep STEPS[] = {
91 &NetworkController::DetectFPP,
92 &NetworkController::DetectFalconController,
93 &NetworkController::DetectSanDevicesController,
94 &NetworkController::DetectESPixelStickController,
95 &NetworkController::DetectBaldrickController,
96 &NetworkController::DetectAlphaPixController,
97 &NetworkController::DetectHinksPixController,
98 &NetworkController::DetectDIYLEDExpressController,
99 &NetworkController::DetectExperienceController,
100 &NetworkController::DetectWLEDController,
101 // Last: a Twinkly has no web UI to recognise, so its detector can
102 // only key off the 404 its server returns for "/" and then ask the
103 // device. Anything with a real page of its own has been claimed by
104 // now, so that weak signature costs nothing.
105 &NetworkController::DetectTwinklyController,
106 };
107
2/2
✓ Branch 0 taken 217 times.
✓ Branch 1 taken 2724 times.
2941 if (step >= (sizeof(STEPS) / sizeof(STEPS[0]))) {
108 217 finish(nullptr);
109 217 return;
110 }
111
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2724 times.
2724 (nc->*STEPS[step])(this);
112 }
113
114 46 void matched() {
115 46 finish(nc);
116 46 }
117
118 // Only the detectors that settle it from the HTML alone reach this
119 // synchronously; the rest come back through it from a curl completion. The
120 // recursion is bounded by the length of STEPS.
121 2671 void noMatch() {
122 2671 ++step;
123 2671 run();
124 2671 }
125
126 // Ask the device something. `ok` is false only when the transfer itself
127 // failed -- which is exactly what the blocking urlGet() this replaced
128 // reported, so a 404 still reaches the handler with the error page as its
129 // body and the detectors, which only look for their own fields in the
130 // response, behave as they did before.
131 121 void fetch(const std::string& url, std::function<void(bool ok, const std::string& resp)>&& handler) {
132
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 120 times.
121 if (blocking) {
133 1 std::string resp;
134
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 bool ok = urlGet(url, resp);
135
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 deliver(handler, ok, resp);
136 1 return;
137 1 }
138
5/10
✓ Branch 0 taken 120 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 120 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 120 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 120 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 120 times.
✗ Branch 9 not taken.
480 CurlManager::INSTANCE.addGet(url, [this, handler](int rc, const std::string& resp) {
139 113 deliver(handler, rc != 0, resp);
140 113 });
141 }
142
143 // Same contract as fetch(), for the one vendor whose status is only
144 // reachable by POST. Kept beside it so a detector never has to touch
145 // CurlManager directly and risk a path that reaches none of matched() /
146 // noMatch() / fetch().
147 1 void post(const std::string& url, const std::string& body, const std::string& contentType,
148 std::function<void(bool ok, const std::string& resp)>&& handler) {
149
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (blocking) {
150 ✗ std::string resp;
151 ✗ bool ok = urlHelper("POST", url, body, resp, { "Content-Type: " + contentType });
152 ✗ deliver(handler, ok, resp);
153 ✗ return;
154 ✗ }
155
4/8
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✗ Branch 7 not taken.
2 CurlManager::INSTANCE.addPost(url, body, contentType, [this, handler](int rc, const std::string& resp) {
156 1 deliver(handler, rc != 0, resp);
157 1 });
158 ✗ }
159
160 private:
161 // Runs a detector's reply handler. The handlers read what a device sent
162 // with jsoncpp, which throws on a field of the wrong type ({"HostName":{}}),
163 // and a throw reached neither matched() nor noMatch(): the walk stopped
164 // where it was, this object leaked, and the caller's discovery slot was
165 // never given back -- enough of those and discovery stopped for good. A
166 // reply that can't be read is a no-match. Every handler calls matched(),
167 // noMatch() or the next fetch() as its last act, so a throw always comes
168 // before the walk has moved on.
169 115 void deliver(const std::function<void(bool, const std::string&)>& handler, bool ok, const std::string& resp) {
170 try {
171
2/2
✓ Branch 0 taken 59 times.
✓ Branch 1 taken 56 times.
115 handler(ok, resp);
172
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 56 times.
56 } catch (const std::exception& e) {
173
1/2
✓ Branch 0 taken 56 times.
✗ Branch 1 not taken.
56 LogWarn(VB_SYNC, "Controller detection at %s: could not read the reply: %s\n", ip.c_str(), e.what());
174
1/2
✓ Branch 0 taken 56 times.
✗ Branch 1 not taken.
56 noMatch();
175 56 }
176 115 }
177
178 263 void finish(NetworkController* result) {
179 // Move the callback out and destroy ourselves before running it: it may
180 // well start the next piece of work, and it must not do that while a
181 // half-finished Detection is still alive.
182 263 std::function<void(NetworkController*)> cb = std::move(callback);
183
2/2
✓ Branch 0 taken 217 times.
✓ Branch 1 taken 46 times.
263 if (!result) {
184
1/2
✓ Branch 0 taken 217 times.
✗ Branch 1 not taken.
217 delete nc;
185 }
186
1/2
✓ Branch 0 taken 263 times.
✗ Branch 1 not taken.
263 delete this;
187 // Nothing may leave here by exception: finish() is reached from inside
188 // a detector's reply handler, and deliver() would catch the throw and
189 // call noMatch() on the Detection deleted just above.
190 try {
191
2/2
✓ Branch 0 taken 262 times.
✓ Branch 1 taken 1 times.
263 cb(result);
192
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 } catch (const std::exception& e) {
193
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogWarn(VB_SYNC, "Controller detection callback threw: %s\n", e.what());
194 1 }
195 263 }
196 };
197
198 268 void NetworkController::DetectControllerViaHTML(const std::string& ip, const std::string& html,
199 std::function<void(NetworkController*)>&& callback) {
200
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 268 times.
268 if (html.empty()) {
201 ✗ callback(nullptr);
202 ✗ return;
203 }
204
3/8
✓ Branch 0 taken 268 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 268 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 268 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
268 (new Detection(ip, html, std::move(callback)))->run();
205 }
206
207 2 NetworkController* NetworkController::DetectControllerViaHTML(const std::string& ip, const std::string& html) {
208
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (html.empty()) {
209 ✗ return nullptr;
210 }
211 // The same detector walk, driven with the blocking curl helpers so it is
212 // finished before this returns. Kept for out-of-tree plugins compiled
213 // against the pre-async signature; nothing in FPP calls it.
214 2 NetworkController* result = nullptr;
215
4/10
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 2 times.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
2 (new Detection(ip, html, [&result](NetworkController* nc) { result = nc; }, true))->run();
216 2 return result;
217 }
218
219 270 void NetworkController::DetectFPP(Detection* st) {
220
2/2
✓ Branch 0 taken 206 times.
✓ Branch 1 taken 64 times.
270 if (st->html.find("Falcon Player - FPP") == std::string::npos) {
221 206 st->noMatch();
222 206 return;
223 }
224
3/6
✓ Branch 0 taken 64 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 64 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 64 times.
✗ Branch 5 not taken.
192 st->fetch(buildHttpURL(st->ip, "/api/system/info?simple=1"), [this, st](bool ok, const std::string& resp) {
225
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 56 times.
57 if (!ok) {
226
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 st->noMatch();
227 1 return;
228 }
229
1/2
✓ Branch 0 taken 56 times.
✗ Branch 1 not taken.
56 Json::Value v;
230
1/2
✓ Branch 0 taken 56 times.
✗ Branch 1 not taken.
56 LoadJsonFromString(resp, v, JsonRoot::Object);
231
3/4
✓ Branch 0 taken 56 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✓ Branch 3 taken 51 times.
56 hostname = v["HostName"].asString();
232
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 vendor = "FPP";
233
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 vendorURL = "https://falconchristmas.com/forum/";
234
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.
10 if (JsonHas(v, "channelRanges")) {
235
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 ranges = v["channelRanges"].asString();
236 }
237
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.
10 if (JsonHas(v, "uuid")) {
238
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 uuid = v["uuid"].asString();
239 }
240
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 version = v["Version"].asString();
241
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 typeStr = v["Variant"].asString();
242
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 typeId = MultiSync::ModelStringToType(typeStr);
243
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 2 times.
5 if (typeId == kSysTypeFPP) {
244 // Pi's tend to have a just the model in the Variant, we'll try mapping those
245
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 typeId = MultiSync::ModelStringToType("Raspberry " + typeStr);
246
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 if (typeId != kSysTypeFPP) {
247
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 typeStr = "Raspberry " + typeStr;
248 }
249 }
250
251
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 std::string md = v["Mode"].asString();
252
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 5 times.
5 if (md == "bridge") {
253 ✗ systemMode = BRIDGE_MODE;
254
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 5 times.
5 } else if (md == "player") {
255 ✗ systemMode = PLAYER_MODE;
256
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 } else if (md == "remote") {
257 5 systemMode = REMOTE_MODE;
258 ✗ } else if (md == "master") {
259 ✗ systemMode = PLAYER_MODE;
260 ✗ sendingMultiSync = true;
261 }
262
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 5 times.
5 if (v.isMember("multisync")) {
263 ✗ sendingMultiSync = v["multisync"].asBool();
264 }
265
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 majorVersion = v["majorVersion"].asInt();
266
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 minorVersion = v["minorVersion"].asInt();
267
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 st->matched();
268 56 });
269 }
270
271 258 void NetworkController::DetectFalconController(Detection* st) {
272
1/2
✓ Branch 0 taken 258 times.
✗ Branch 1 not taken.
258 LogExcess(VB_SYNC, "Checking if %s is a Falcon controller\n", st->ip.c_str());
273
274
2/4
✓ Branch 0 taken 258 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 258 times.
✗ Branch 3 not taken.
258 RegExCache re("\"css/falcon.css\"|\"/f16v2.js\"|\"js/cntrlr_(\\d{1,8}).js\"");
275 258 std::smatch m;
276
277
3/4
✓ Branch 0 taken 258 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 257 times.
✓ Branch 3 taken 1 times.
258 if (!std::regex_search(st->html, m, *re.regex)) {
278
1/2
✓ Branch 0 taken 257 times.
✗ Branch 1 not taken.
257 st->noMatch();
279 257 return;
280 }
281
282
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogExcess(VB_SYNC, "%s is potentially a Falcon controller, checking further\n", st->ip.c_str());
283
284
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 vendor = "Falcon";
285
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 vendorURL = "https://pixelcontroller.com";
286
287
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 st->fetch(buildHttpURL(st->ip, "/status.xml"), [this, st](bool ok, const std::string& resp) {
288
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 std::size_t fStart = ok ? resp.find("<p>") : std::string::npos;
289
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (fStart == std::string::npos) {
290 ✗ st->noMatch();
291 ✗ return;
292 }
293
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 typeId = (systemType)(atoi(getSimpleXMLTag(resp, "p").c_str()));
294
295
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (typeId >= 0x80) { // v4 is just 0x80
296
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (typeId == 0x82) {
297 ✗ typeId = kSysTypeFalconF16v5;
298
4/8
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✗ Branch 7 not taken.
2 } else if (getSimpleXMLTag(resp, "np") == "16") {
299 1 typeId = kSysTypeFalconF16v4;
300 ✗ } else if (getSimpleXMLTag(resp, "np") == "48" || getSimpleXMLTag(resp, "np") == "32") {
301 ✗ typeId = kSysTypeFalconF48v4;
302 }
303 } else { // v3 and below, 0x80 + p tag
304 ✗ typeId = (systemType)(atoi(resp.substr(fStart + 3).c_str()) + 0x80);
305 }
306
307
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 typeStr = MultiSync::GetTypeString(typeId);
308
309
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 version = getSimpleXMLTag(resp, "fv");
310 1 systemMode = BRIDGE_MODE;
311
312
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if ((typeId == kSysTypeFalconF16v2) ||
313
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 (typeId == kSysTypeFalconF4v2_64Mb) ||
314
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 (typeId == kSysTypeFalconF16v2R) ||
315
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 (typeId == kSysTypeFalconF4v2)) {
316 ✗ hostname = getSimpleHTMLTTag(st->html, "Name:</td>", "\">", "</td>");
317 ✗ version = getSimpleHTMLTTag(st->html, "SW Version:</td>", "\">", "</td>");
318 ✗ if ((hostname != "") && (startsWith(version, hostname))) {
319 ✗ std::string tmpStr(hostname);
320 ✗ version.erase(0, hostname.length());
321 ✗ TrimWhiteSpace(version);
322
323 ✗ if (startsWith(version, "- "))
324 ✗ version.erase(0, 2);
325 ✗ }
326
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 } else if ((typeId == kSysTypeFalconF16v4) ||
327 ✗ (typeId == kSysTypeFalconF48v4)) {
328
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 hostname = getSimpleXMLTag(resp, "n");
329 1 std::size_t spacePos = version.find(" ");
330
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (spacePos != std::string::npos) {
331
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 majorVersion = atoi(version.substr(spacePos + 1).c_str());
332 }
333 1 } else {
334 ✗ hostname = getSimpleXMLTag(resp, "n");
335
336 ✗ if (version != "") {
337 ✗ majorVersion = atoi(version.c_str());
338
339 ✗ std::size_t verDot = version.find(".");
340 ✗ if (verDot != std::string::npos) {
341 ✗ minorVersion = atoi(version.substr(verDot + 1).c_str());
342 }
343 }
344 }
345
346 // status.xml carries no identity on any generation. The V4/V5 line has
347 // a JSON API that reports one, reachable only by POST; everything older
348 // answers 404 there and keeps the derived identity MultiSync gives it.
349 //
350 // Deliberately not allowed to affect detection: the XML above already
351 // settled that this is a Falcon, so the request only ever adds a uuid,
352 // and both outcomes reach matched(). An F48 predating the API is a
353 // real example of the 404 path -- it is still a Falcon.
354
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if ((typeId == kSysTypeFalconF16v4) ||
355 ✗ (typeId == kSysTypeFalconF48v4) ||
356 ✗ (typeId == kSysTypeFalconF16v5)) {
357
5/10
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1 times.
✗ Branch 9 not taken.
6 st->post(buildHttpURL(st->ip, "/api"),
358 "{\"T\":\"Q\",\"M\":\"ST\",\"B\":0,\"E\":0,\"I\":0,\"P\":{}}",
359 "application/json",
360 1 [this, st](bool aok, const std::string& aresp) {
361
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 Json::Value av;
362 // isObject() guards the get() below: jsoncpp rejects a
363 // keyed lookup on a scalar, and nothing here controls
364 // what a device puts in "P".
365
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 if (aok && LoadJsonFromString(aresp, av, JsonRoot::Object) &&
366
10/24
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 1 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 1 times.
✗ Branch 15 not taken.
✓ Branch 16 taken 1 times.
✗ Branch 17 not taken.
✓ Branch 18 taken 1 times.
✗ Branch 19 not taken.
✗ Branch 20 not taken.
✗ Branch 21 not taken.
✗ Branch 22 not taken.
✗ Branch 23 not taken.
3 JsonHas(av, "P") && av["P"].isObject()) {
367 // Same spelling MultiSync uses for an ARP-derived
368 // address, so a device keeps one identity however
369 // it was discovered.
370
5/10
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1 times.
✗ Branch 9 not taken.
1 std::string mac = NormalizeMacAddress(av["P"].get("C", "").asString());
371
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (!mac.empty()) {
372
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 uuid = MAC_UUID_PREFIX + mac;
373 }
374 1 }
375
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 DumpControllerInfo();
376
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 st->matched();
377 1 });
378 1 return;
379 }
380
381 ✗ DumpControllerInfo();
382 ✗ st->matched();
383 });
384
4/4
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 257 times.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 257 times.
515 }
385
386 257 void NetworkController::DetectSanDevicesController(Detection* st) {
387
1/2
✓ Branch 0 taken 257 times.
✗ Branch 1 not taken.
257 LogExcess(VB_SYNC, "Checking if %s is a SanDevices controller\n", st->ip.c_str());
388
2/4
✓ Branch 0 taken 257 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 257 times.
✗ Branch 3 not taken.
257 RegExCache re("Controller Model (E[0-9]{1,8})");
389 257 std::smatch m;
390
391
3/4
✓ Branch 0 taken 257 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 256 times.
✓ Branch 3 taken 1 times.
257 if (!std::regex_search(st->html, m, *re.regex)) {
392
1/2
✓ Branch 0 taken 256 times.
✗ Branch 1 not taken.
256 st->noMatch();
393 256 return;
394 }
395
396
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogExcess(VB_SYNC, "%s is potentially a SanDevices controller, checking further\n", st->ip.c_str());
397
398
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 vendor = "SanDevices";
399
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 vendorURL = "http://sandevices.com/";
400 1 typeId = kSysTypeSanDevices;
401
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 typeStr = m[1];
402 1 systemMode = BRIDGE_MODE;
403
404 // The status page states the board's MAC, so this costs no extra request --
405 // and unlike the ARP fallback in MultiSync it works for a board on another
406 // subnet. Matched loosely across the intervening markup rather than against
407 // one firmware's exact table layout, which the version regexes below already
408 // have to be spelled twice to cope with. Measured equal to the address ARP
409 // reports for the same board, so it shares the one identity namespace (see
410 // MultiSync::ApplyUUIDHint() for why that matters).
411
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 RegExCache macre("MAC Address:[\\s\\S]{0,64}?((?:[0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2})");
412 1 std::smatch mm;
413
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 if (std::regex_search(st->html, mm, *macre.regex)) {
414
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 std::string mac = NormalizeMacAddress(mm[1].str());
415
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (!mac.empty()) {
416 // Same spelling MultiSync uses for an ARP-derived address, so a
417 // device keeps one identity however it was discovered.
418
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 uuid = MAC_UUID_PREFIX + mac;
419 }
420 1 }
421
422
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 RegExCache v4re("Firmware Version:</th></td><td></td><td>([0-9]{1,8}.[0-9]{1,8})</td>");
423
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 RegExCache v5re("Firmware Version:</th></td><td>\\s?([0-9]{1,8}.[0-9]{1,8})(-W\\d{1,8})?</td>");
424
425
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.
2 if ((std::regex_search(st->html, m, *v4re.regex)) ||
426
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 (std::regex_search(st->html, m, *v5re.regex))) {
427
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 version = m[1];
428
429
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 if (version != "") {
430 1 majorVersion = atoi(version.c_str());
431
432 1 std::size_t verDot = version.find(".");
433
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (verDot != std::string::npos) {
434
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 minorVersion = atoi(version.substr(verDot + 1).c_str());
435 }
436 }
437
438
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 DumpControllerInfo();
439
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 st->matched();
440 1 return;
441 }
442
443 ✗ st->noMatch();
444
6/12
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 1 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 1 times.
✗ Branch 8 not taken.
✓ Branch 9 taken 257 times.
✗ Branch 10 not taken.
✓ Branch 11 taken 257 times.
518 }
445
446 256 void NetworkController::DetectESPixelStickController(Detection* st) {
447 256 LogExcess(VB_SYNC, "Checking if %s is running ESPixelStick firmware\n", st->ip.c_str());
448
449
4/6
✓ Branch 0 taken 256 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 256 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 254 times.
✓ Branch 5 taken 2 times.
512 if (!contains(st->html, "\"esps.js\"")) {
450 254 st->noMatch();
451 254 return;
452 }
453
454 2 LogExcess(VB_SYNC, "%s is potentially an ESPixelStick, checking further\n", st->ip.c_str());
455
456 2 vendor = "ESPixelStick";
457 2 vendorURL = "https://forkineye.com";
458 2 typeId = kSysTypeESPixelStick;
459 2 typeStr = "ESPixelStick";
460 2 systemMode = BRIDGE_MODE;
461
462 // buildHttpURL() rather than the bare "<ip>/conf" this used to build: that
463 // relied on curl guessing the scheme, and it could not work at all for an
464 // IPv6 literal, which has to be bracketed.
465
3/6
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
6 st->fetch(buildHttpURL(st->ip, "/conf"), [this, st](bool ok, const std::string& resp) {
466
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (ok) {
467
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 Json::Value config;
468
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 LoadJsonFromString(resp, config, JsonRoot::Object);
469
12/32
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 2 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 2 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 2 times.
✗ Branch 15 not taken.
✓ Branch 16 taken 2 times.
✗ Branch 17 not taken.
✓ Branch 18 taken 2 times.
✗ Branch 19 not taken.
✓ Branch 20 taken 2 times.
✗ Branch 21 not taken.
✓ Branch 22 taken 2 times.
✗ 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.
✗ Branch 30 not taken.
✗ Branch 31 not taken.
10 if (JsonHas(config, "network") && JsonHas(config["network"], "hostname")) {
470
4/6
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 1 times.
2 hostname = config["network"]["hostname"].asString();
471 }
472
473
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 if (hostname != "") {
474
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 DumpControllerInfo();
475
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 st->matched();
476 1 return;
477 }
478
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
2 }
479 ✗ st->noMatch();
480 });
481 }
482
483 // Pulls "v3.5.4" apart into major/minor. Tolerates the leading "v" being
484 // absent, and leaves the numbers alone if the string is not that shape.
485 3 void NetworkController::ParseBaldrickVersion(const std::string& fw) {
486
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 version = fw;
487
5/8
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✗ Branch 7 not taken.
3 std::string verNum = (!version.empty() && version[0] == 'v') ? version.substr(1) : version;
488 3 std::size_t verDot = verNum.find(".");
489
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 2 times.
3 if (verDot == std::string::npos) {
490 1 return;
491 }
492
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 majorVersion = atoi(verNum.substr(0, verDot).c_str());
493 2 std::size_t verDot2 = verNum.find(".", verDot + 1);
494
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (verDot2 != std::string::npos) {
495
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 minorVersion = atoi(verNum.substr(verDot + 1, verDot2 - (verDot + 1)).c_str());
496 }
497
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 times.
3 }
498
499 255 void NetworkController::DetectBaldrickController(Detection* st) {
500 255 LogExcess(VB_SYNC, "Checking if %s is a Baldrick controller\n", st->ip.c_str());
501
502
4/6
✓ Branch 0 taken 255 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 255 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 250 times.
✓ Branch 5 taken 5 times.
510 if (!contains(st->html, "Baldrick Board")) {
503 250 st->noMatch();
504 250 return;
505 }
506
507 5 LogExcess(VB_SYNC, "%s is potentially a Baldrick controller, checking further\n", st->ip.c_str());
508
509 5 vendor = "ILightThat";
510 5 vendorURL = "https://www.ilightthat.com";
511 5 typeId = kSysTypeBaldrick;
512 5 typeStr = "Baldrick";
513 5 systemMode = BRIDGE_MODE;
514
515
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 st->fetch(buildHttpURL(st->ip, "/system_state"), [this, st](bool ok, const std::string& resp) {
516
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 4 times.
5 if (!ok) {
517
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 st->noMatch();
518 1 return;
519 }
520
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 Json::Value state;
521
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 LoadJsonFromString(resp, state, JsonRoot::Object);
522
4/6
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✓ Branch 5 taken 1 times.
8 if (JsonHas(state, "board_model")) {
523
3/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 1 times.
3 typeStr = state["board_model"].asString();
524 }
525
526 // Newer firmware states the board's OWN id at the top level; older
527 // firmware states only its neighbours'. Note this is the ESP32 base
528 // address, which is NOT what ARP sees for a board on Ethernet (that is
529 // base+3) -- see MultiSync::ApplyUUIDHint() for why a vendor-reported
530 // id must win over ARP rather than merely fill in behind it. Measured
531 // on a board reporting both: the id it gives for itself is byte-for-byte
532 // the one its neighbour gives for it, so the two sources agree and
533 // neither has to be preferred for correctness.
534
4/6
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 2 times.
6 if (JsonHas(state, "board_id")) {
535
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 std::string bid = NormalizeMacAddress(state["board_id"].asString());
536
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (!bid.empty()) {
537
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 uuid = MAC_UUID_PREFIX + bid;
538 }
539 1 }
540
541 // The neighbour list is still read, and still matters for two reasons:
542 // it is the only identity a board on older firmware ever has, and it
543 // names Baldricks of other kinds -- signal boards, DMX boards, input
544 // boards -- that discovery may reach before or without reaching this
545 // one. Recorded against the peer address for the caller to apply;
546 // nothing here invents a system that discovery has not otherwise found.
547
11/22
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 2 times.
✓ Branch 6 taken 1 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 3 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 3 times.
✗ Branch 15 not taken.
✓ Branch 16 taken 1 times.
✓ Branch 17 taken 2 times.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
✗ Branch 20 not taken.
✗ Branch 21 not taken.
9 if (JsonHas(state, "buddies") && state["buddies"].isArray()) {
548
8/14
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 4 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 4 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 5 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 4 times.
✓ Branch 13 taken 1 times.
5 for (const auto& buddy : state["buddies"]) {
549
3/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 3 times.
4 if (!buddy.isObject()) {
550 1 continue;
551 }
552
3/6
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
3 std::string bip = buddy.get("ip", "").asString();
553
4/8
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 3 times.
✗ Branch 7 not taken.
3 std::string bid = NormalizeMacAddress(buddy.get("board_id", "").asString());
554
3/6
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
3 if (!bip.empty() && !bid.empty()) {
555
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 peerUUIDs[bip] = MAC_UUID_PREFIX + bid;
556 }
557 3 }
558 }
559
3/6
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
6 if (JsonHas(state, "ota")) {
560
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 const Json::Value& ota = state["ota"];
561 // Two firmware generations, two shapes. The older boards put the
562 // version directly on "ota"; current ones report a list of
563 // separately-updatable components under "ota.updatable" and carry
564 // the version on each entry. The first entry that has one is the
565 // board itself.
566
4/6
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 1 times.
6 if (JsonHas(ota, "current_firmware_version")) {
567
3/6
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
2 ParseBaldrickVersion(ota["current_firmware_version"].asString());
568
9/22
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 1 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 1 times.
✗ Branch 15 not taken.
✓ Branch 16 taken 1 times.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
✗ Branch 20 not taken.
✗ Branch 21 not taken.
3 } else if (JsonHas(ota, "updatable") && ota["updatable"].isArray()) {
569
7/14
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 3 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 3 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 3 times.
✗ Branch 13 not taken.
3 for (const auto& part : ota["updatable"]) {
570
13/20
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 1 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.
✓ Branch 10 taken 2 times.
✓ Branch 11 taken 1 times.
✓ Branch 12 taken 2 times.
✓ Branch 13 taken 1 times.
✓ Branch 14 taken 1 times.
✓ Branch 15 taken 2 times.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
7 if (part.isObject() && JsonHas(part, "current_firmware_version")) {
571
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 ParseBaldrickVersion(part["current_firmware_version"].asString());
572 1 break;
573 }
574 }
575 }
576 }
577
578 // /system_state has no hostname -- that lives in /settings, which is why
579 // this used to be left showing the bare IP. A failure here is not fatal:
580 // the board is already identified, it just keeps the IP as its name.
581
3/6
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
9 st->fetch(buildHttpURL(st->ip, "/settings"), [this, st](bool sok, const std::string& sresp) {
582
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 Json::Value settings;
583
4/6
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 1 times.
5 if (sok && LoadJsonFromString(sresp, settings, JsonRoot::Object) &&
584
9/16
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
✓ Branch 7 taken 1 times.
✓ Branch 8 taken 2 times.
✓ Branch 9 taken 1 times.
✓ Branch 10 taken 2 times.
✓ Branch 11 taken 1 times.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
7 JsonHas(settings, "hostname")) {
585
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 std::string h = settings["hostname"].asString();
586
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (!h.empty()) {
587
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 hostname = h;
588 }
589 2 }
590
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 DumpControllerInfo();
591 // Answering /system_state at all is what identifies the board; the
592 // old `hostname != ""` test here could never fail, because the
593 // constructor seeds hostname with the IP.
594
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 st->matched();
595 3 });
596 4 });
597 }
598
599 252 void NetworkController::DetectAlphaPixController(Detection* st) {
600
1/2
✓ Branch 0 taken 252 times.
✗ Branch 1 not taken.
252 LogExcess(VB_SYNC, "Checking if %s is a AlphaPix controller\n", st->ip.c_str());
601
602
2/4
✓ Branch 0 taken 252 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 252 times.
✗ Branch 3 not taken.
504 RegExCache re("AlphaPix (\\d{1,8}|Flex|Evolution)");
603
2/4
✓ Branch 0 taken 252 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 252 times.
✗ Branch 3 not taken.
252 RegExCache re2("(\\d{1,8}) Port Ethernet to SPI Controller");
604 252 std::smatch m, m2;
605
606 // These boards serve one client at a time. While another computer holds
607 // the session every page, this one included, is replaced by a stub that
608 // names no model and no version -- so without this the board matches
609 // nothing, is dropped as an unrecognised address, and simply vanishes from
610 // discovery with nothing said about why. Claim it anyway: an AlphaPix with
611 // no detail is far more use to somebody looking for a missing controller
612 // than no row at all, and the log line names the actual cause.
613
4/6
✓ Branch 0 taken 252 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 252 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 251 times.
504 if (contains(st->html, "Existing user login")) {
614
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogWarn(VB_SYNC, "%s looks like an AlphaPix whose web session is held by another client; "
615 "no model or version can be read until that session is released\n",
616 st->ip.c_str());
617
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 vendor = "HolidayCoro";
618
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 vendorURL = "https://www.holidaycoro.com/";
619 1 typeId = kSysTypeAlphaPix;
620
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 typeStr = "AlphaPix";
621 1 systemMode = BRIDGE_MODE;
622
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 DumpControllerInfo();
623
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 st->matched();
624 1 return;
625 }
626
627
1/2
✓ Branch 0 taken 251 times.
✗ Branch 1 not taken.
251 bool matchedModel = std::regex_search(st->html, m, *re.regex);
628
7/8
✓ Branch 0 taken 247 times.
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 247 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 245 times.
✓ Branch 5 taken 2 times.
✓ Branch 6 taken 245 times.
✓ Branch 7 taken 6 times.
251 if (!matchedModel && !std::regex_search(st->html, m2, *re2.regex)) {
629
1/2
✓ Branch 0 taken 245 times.
✗ Branch 1 not taken.
245 st->noMatch();
630 245 return;
631 }
632
633
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 LogExcess(VB_SYNC, "%s is potentially a AlphaPix controller, checking further\n", st->ip.c_str());
634
635
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 vendor = "HolidayCoro";
636
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 vendorURL = "https://www.holidaycoro.com/";
637 6 typeId = kSysTypeAlphaPix;
638 // Read from whichever pattern actually matched. This took m[1]
639 // unconditionally, but m holds the result of the FAILED search whenever the
640 // model is named only as "<n> Port Ethernet to SPI Controller" -- so every
641 // board whose page uses that wording alone was typed as an empty string.
642
6/10
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2 times.
✗ Branch 9 not taken.
6 typeStr = matchedModel ? m[1].str() : m2[1].str();
643 6 systemMode = BRIDGE_MODE;
644
645 // These boards state a "Device ID", and on the one that could be measured
646 // it is the low 24 bits of the board's MAC written in decimal: 0404361 is
647 // 0x062B89, and the address ARP reports is 00:00:00:06:2B:89. The vendor
648 // appears to use an all-zero OUI and hand out the id as the rest, which
649 // makes this the only identity these boards expose that survives crossing a
650 // subnet -- they publish no MAC on any page, and the xLights implementation
651 // extracts none either.
652 //
653 // Spelled as the MAC it encodes rather than as an id of its own, so a board
654 // identified from this page and one identified only from the ARP table land
655 // on the same value instead of on two names for one device.
656
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
6 RegExCache didre("Device ID:\\s{0,16}([0-9]{1,16})");
657
3/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 4 times.
6 if (std::regex_search(st->html, m, *didre.regex)) {
658
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 long id = strtol(m[1].str().c_str(), nullptr, 10); // base 10: the id is zero-padded
659
3/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 1 times.
2 if (id > 0 && id <= 0xFFFFFF) {
660 char buf[16];
661 1 snprintf(buf, sizeof(buf), "000000%06lX", id);
662 // Same spelling MultiSync uses for an ARP-derived address, so a
663 // device keeps one identity however it was discovered.
664
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 uuid = MAC_UUID_PREFIX + std::string(buf);
665 }
666 }
667
668
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
6 RegExCache vre("Currently Installed Firmware Version: ([0-9]{1,8}.[0-9]{1,8})");
669
670 // The version is reported when it can be read, but is NOT what decides
671 // whether this is an AlphaPix -- the model above already settled that.
672 // Requiring it here meant a board whose page states a model but spells the
673 // version differently (the 2.16+ web UI is a different layout) was rejected
674 // outright rather than reported without a version.
675
3/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 2 times.
6 if (std::regex_search(st->html, m, *vre.regex)) {
676
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 version = m[1];
677
678
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 if (version != "") {
679 4 majorVersion = atoi(version.c_str());
680
681 4 std::size_t verDot = version.find(".");
682
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 if (verDot != std::string::npos) {
683
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 minorVersion = atoi(version.substr(verDot + 1).c_str());
684 }
685 }
686 }
687
688
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 DumpControllerInfo();
689
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 st->matched();
690
8/8
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 246 times.
✓ Branch 2 taken 6 times.
✓ Branch 3 taken 246 times.
✓ Branch 4 taken 6 times.
✓ Branch 5 taken 246 times.
✓ Branch 6 taken 6 times.
✓ Branch 7 taken 246 times.
990 }
691
692 245 void NetworkController::DetectHinksPixController(Detection* st) {
693 245 LogExcess(VB_SYNC, "Checking if %s is a HinksPix controller\n", st->ip.c_str());
694
4/6
✓ Branch 0 taken 245 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 245 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 239 times.
✓ Branch 5 taken 6 times.
490 if (!contains(st->html, "HinksPix Config")) {
695 239 st->noMatch();
696 239 return;
697 }
698 6 LogExcess(VB_SYNC, "%s is potentially a HinksPix controller, checking further\n", st->ip.c_str());
699
700 6 vendor = "HolidayCoro";
701 6 vendorURL = "https://www.holidaycoro.com/";
702 6 typeId = kSysTypeHinksPix;
703 6 typeStr = "HinksPix";
704 6 systemMode = BRIDGE_MODE;
705
706 // The status page renders its firmware info via JS/AJAX, so it never
707 // shows up in the initial HTML fetch above. xLights identifies these
708 // controllers via the same endpoint (src-core/controllers/HinksPix.cpp):
709 // each of MCPU/PCPU/ECPU/WEB is a 3-char prefix ("MS_"/"PS_"/"EZ_"/"WF_")
710 // followed by a bare build number, not a dotted version -- so majorVersion
711 // is that build number directly and there is no separate minor version.
712
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.
18 st->fetch(buildHttpURL(st->ip, "/XLights_BoardInfo.cgi"), [this, st](bool ok, const std::string& resp) {
713 // JsonString(), not asString(): jsoncpp throws on a non-string value
714 // and this callback runs on the CurlManager thread with nothing to
715 // catch it, so an odd reply from the device would take fppd down.
716
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 Json::Value v;
717 6 std::string mcpuRaw;
718
21/36
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 5 times.
✓ Branch 5 taken 1 times.
✓ Branch 6 taken 5 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 5 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 5 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 4 times.
✓ Branch 13 taken 1 times.
✓ Branch 14 taken 5 times.
✓ Branch 15 taken 1 times.
✓ Branch 16 taken 5 times.
✓ Branch 17 taken 1 times.
✓ Branch 18 taken 5 times.
✓ Branch 19 taken 1 times.
✓ Branch 20 taken 5 times.
✓ Branch 21 taken 1 times.
✓ Branch 22 taken 5 times.
✓ Branch 23 taken 1 times.
✓ Branch 24 taken 4 times.
✓ Branch 25 taken 2 times.
✗ Branch 26 not taken.
✗ Branch 27 not taken.
✗ Branch 28 not taken.
✗ Branch 29 not taken.
✗ Branch 30 not taken.
✗ Branch 31 not taken.
✗ Branch 32 not taken.
✗ Branch 33 not taken.
✗ Branch 34 not taken.
✗ Branch 35 not taken.
26 if (ok && LoadJsonFromString(resp, v, JsonRoot::Object) && !(mcpuRaw = JsonString(v, "MCPU")).empty()) {
719 9 auto stripPrefix = [](const std::string& s) {
720
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 1 times.
9 return s.size() > 3 ? s.substr(3) : s;
721 };
722
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 std::string mcpu = stripPrefix(mcpuRaw);
723 4 majorVersion = atoi(mcpu.c_str());
724
725
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 version = "MAIN:" + mcpu;
726 4 std::string s;
727
5/8
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✓ Branch 7 taken 3 times.
16 if (!(s = JsonString(v, "PCPU")).empty()) {
728
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 version += ",POWER:" + stripPrefix(s);
729 }
730
5/8
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✓ Branch 7 taken 3 times.
16 if (!(s = JsonString(v, "ECPU")).empty()) {
731
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 version += ",WIFI:" + stripPrefix(s);
732 }
733
5/8
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 3 times.
✓ Branch 7 taken 1 times.
16 if (!(s = JsonString(v, "WEB")).empty()) {
734
3/6
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
3 version += ",WEB:" + stripPrefix(s);
735 }
736 4 }
737
738
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 DumpControllerInfo();
739
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 st->matched();
740 6 });
741 }
742
743 239 void NetworkController::DetectDIYLEDExpressController(Detection* st) {
744
1/2
✓ Branch 0 taken 239 times.
✗ Branch 1 not taken.
239 LogExcess(VB_SYNC, "Checking if %s is a DIYLEDExpress controller\n", st->ip.c_str());
745
746
2/4
✓ Branch 0 taken 239 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 239 times.
✗ Branch 3 not taken.
239 RegExCache re("DIYLEDExpress E1.31 Bridge Configuration Page");
747 239 std::smatch m;
748
749
2/4
✓ Branch 0 taken 239 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 239 times.
✗ Branch 3 not taken.
239 if (!std::regex_search(st->html, *re.regex)) {
750
1/2
✓ Branch 0 taken 239 times.
✗ Branch 1 not taken.
239 st->noMatch();
751 239 return;
752 }
753
754 ✗ LogExcess(VB_SYNC, "%s is potentially a DIYLEDExpress controller, checking further\n", st->ip.c_str());
755
756 ✗ vendor = "DIYLEDExpress";
757 ✗ vendorURL = "http://www.diyledexpress.com/";
758 ✗ typeId = kSysTypeDIYLEDExpress;
759 ✗ typeStr = "E1.31 Bridge";
760 ✗ systemMode = BRIDGE_MODE;
761
762 // Firmware Rev: 4.02
763 ✗ RegExCache vre("Firmware Rev: ([0-9]{1,8}.[0-9]{1,8})");
764
765 ✗ if (std::regex_search(st->html, m, *vre.regex)) {
766 ✗ version = m[1];
767
768 ✗ if (version != "") {
769 ✗ majorVersion = atoi(version.c_str());
770
771 ✗ std::size_t verDot = version.find(".");
772 ✗ if (verDot != std::string::npos) {
773 ✗ minorVersion = atoi(version.substr(verDot + 1).c_str());
774 }
775 }
776
777 ✗ DumpControllerInfo();
778 ✗ st->matched();
779 ✗ return;
780 }
781
782 ✗ st->noMatch();
783
2/6
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 239 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 239 times.
478 }
784
785 // The Experience controllers -- the Genius Pixel / PRO / Long Range / Pixel Link
786 // range, plus the rebadged LOR AURORA CORE and YPS VIVID units -- all serve the
787 // same petite-vue app. Their <title> carries the retail brand, so it is not
788 // something we can match across the family; the app's own component markers are
789 // far more stable. Either way the HTML only decides whether it is worth asking:
790 // nothing is accepted until /api/state answers with the fields this firmware
791 // defines, so a stray HTML match cannot misidentify some other device.
792 //
793 // These controllers do answer MultiSync pings, and that ping reports an exact
794 // model type. This path exists for the two things the ping cannot do: it
795 // carries no UUID field at all, and it does not reach a controller on another
796 // subnet -- which is exactly the case co-universes.json seeding is there to
797 // cover.
798 // Pulls the hardware address out of one of these controllers' "network" object.
799 // Both firmware lines report every interface, so prefer the one actually
800 // answering on the address we are talking to: that is the MAC the ARP table
801 // would have given for the same device, and identity must not depend on which
802 // route found it.
803 4 static std::string ExperienceNetworkMac(const std::string& ip, const Json::Value& network) {
804
3/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 3 times.
4 if (!network.isObject()) {
805
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
2 return "";
806 }
807 3 std::string fallback;
808
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 2 times.
8 for (const char* key : { "wired", "wifi" }) {
809
11/22
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 5 times.
✓ Branch 5 taken 1 times.
✓ Branch 6 taken 5 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 5 times.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✓ Branch 11 taken 5 times.
✓ Branch 12 taken 6 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 6 times.
✗ Branch 15 not taken.
✓ Branch 16 taken 1 times.
✓ Branch 17 taken 5 times.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
✗ Branch 20 not taken.
✗ Branch 21 not taken.
18 if (!JsonHas(network, key) || !network[key].isObject()) {
810 1 continue;
811 }
812
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 const Json::Value& iface = network[key];
813
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.
5 std::string mac = NormalizeMacAddress(iface.get("mac_address", "").asString());
814
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
5 if (mac.empty()) {
815 ✗ continue;
816 }
817
5/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 1 times.
✓ Branch 7 taken 4 times.
5 if (iface.get("ip_address", "").asString() == ip) {
818 1 return mac;
819 }
820
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1 times.
4 if (fallback.empty()) {
821
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 fallback = mac;
822 }
823
2/3
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
5 }
824 2 return fallback;
825 3 }
826
827 239 void NetworkController::DetectExperienceController(Detection* st) {
828
2/2
✓ Branch 0 taken 237 times.
✓ Branch 1 taken 1 times.
477 if (st->html.find("FriendlyNameBar()") == std::string::npos &&
829
6/6
✓ Branch 0 taken 238 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 232 times.
✓ Branch 3 taken 5 times.
✓ Branch 4 taken 232 times.
✓ Branch 5 taken 7 times.
477 st->html.find("CurrentMonitoring()") == std::string::npos &&
830 237 st->html.find("<title>Genius") == std::string::npos) {
831 232 st->noMatch();
832 232 return;
833 }
834
835 7 LogExcess(VB_SYNC, "%s is potentially an Experience controller, checking further\n", st->ip.c_str());
836
837
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.
21 st->fetch(buildHttpURL(st->ip, "/api/state"), [this, st](bool ok, const std::string& resp) {
838
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 Json::Value v;
839
9/22
✓ 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.
✓ Branch 8 taken 7 times.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✓ Branch 11 taken 7 times.
✓ Branch 12 taken 7 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 7 times.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✓ Branch 17 taken 7 times.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
✗ Branch 20 not taken.
✗ Branch 21 not taken.
21 if (!ok || !LoadJsonFromString(resp, v, JsonRoot::Object) || !JsonHas(v, "system")) {
840 ✗ st->noMatch();
841 ✗ return;
842 }
843
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 const Json::Value& sys = v["system"];
844
13/30
✓ 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.
✓ Branch 8 taken 7 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1 times.
✓ Branch 11 taken 6 times.
✓ Branch 12 taken 7 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 7 times.
✗ Branch 15 not taken.
✓ Branch 16 taken 7 times.
✗ Branch 17 not taken.
✓ Branch 18 taken 7 times.
✗ Branch 19 not taken.
✓ Branch 20 taken 1 times.
✓ Branch 21 taken 6 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.
35 if (!JsonHas(sys, "controller_model") || !JsonHas(sys, "controller_model_name")) {
845
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 st->noMatch();
846 1 return;
847 }
848
849
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 vendor = "Experience";
850 // A guess: it can run as a player too, and then its own MultiSync pings,
851 // which win over this, carry the mode it really has.
852 6 systemMode = BRIDGE_MODE;
853
854 // Deliberately the generic Experience type rather than a model-code lookup.
855 // A table mapping every retail code to its own kSysTypeExperience* value
856 // would be guesswork for the models we cannot test, and it would rot as the
857 // range grows. The generic id still lands in the range the UI treats as
858 // this family, the exact model name below is the part a user reads, and
859 // where a ping does reach the device it supplies the precise type anyway --
860 // MultiSyncSystem::update() will not let this HTTP result overwrite it.
861 6 typeId = kSysTypeExperienceGenius;
862
3/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✓ Branch 3 taken 1 times.
6 typeStr = sys["controller_model_name"].asString();
863
864 // The firmware string is not a stable shape across releases: the 1.x line
865 // reported "Genius_PRO_Controller_16 v1.3.1-2", the 2.x line reports a bare
866 // "2.2.0-0". Accept the dotted version either at the start of the string or
867 // after a "v", and require the dot so a model number ("...16 Port") cannot
868 // be mistaken for one. Anything unrecognised is passed through as-is rather
869 // than dropped.
870
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 std::string fw = sys.get("firmware_version", "").asString();
871 5 std::smatch m;
872
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 RegExCache re("(?:^|v)([0-9]{1,8})\\.([0-9]{1,8})(?:\\.([0-9]{1,8}))?");
873
3/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✓ Branch 3 taken 2 times.
5 if (std::regex_search(fw, m, *re.regex)) {
874
6/12
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 3 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 3 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 3 times.
✗ Branch 11 not taken.
3 version = m[1].str() + "." + m[2].str();
875
3/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 1 times.
3 if (m[3].matched) {
876
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 version += "." + m[3].str();
877 }
878
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 majorVersion = atoi(m[1].str().c_str());
879
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 minorVersion = atoi(m[2].str().c_str());
880
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
2 } else if (!fw.empty()) {
881
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 version = fw;
882 }
883
884 // The 1.x line carries the interface addresses in the state document, so
885 // keep them: if the identity endpoint below comes up empty they are the
886 // MAC fallback, and `v` does not survive this callback.
887
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 Json::Value network = v["network"];
888
889 // The identity endpoint is the only place these report something stable and
890 // their own. Build the same string the statistics collector has always
891 // built from it, so a controller is not identified two different ways
892 // depending on which side of FPP is asking.
893
5/10
✓ 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.
✓ Branch 8 taken 5 times.
✗ Branch 9 not taken.
15 st->fetch(buildHttpURL(st->ip, "/update/identity"), [this, st, network](bool idOk, const std::string& idResp) {
894
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 Json::Value idv;
895
13/22
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✓ Branch 5 taken 2 times.
✓ Branch 6 taken 3 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 3 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 3 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 3 times.
✓ Branch 13 taken 2 times.
✓ Branch 14 taken 3 times.
✓ Branch 15 taken 2 times.
✓ Branch 16 taken 3 times.
✓ Branch 17 taken 2 times.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
✗ Branch 20 not taken.
✗ Branch 21 not taken.
11 if (idOk && LoadJsonFromString(idResp, idv, JsonRoot::Object) && JsonHas(idv, "id")) {
896
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 std::string id = idv["id"].asString();
897
3/6
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
3 std::string hw = idv.get("hardware", "").asString();
898
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 times.
3 if (!id.empty()) {
899
7/12
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1 times.
✓ Branch 9 taken 1 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
2 uuid = hw.empty() ? id : (hw + "-" + id);
900 }
901 3 }
902
6/8
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 3 times.
✓ Branch 6 taken 2 times.
✓ Branch 7 taken 3 times.
5 if (!uuid.empty() && uuid != "Unknown") {
903
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 DumpControllerInfo();
904
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 st->matched();
905 2 return;
906 }
907
908 // No identity of its own. The 2.x firmware dropped the endpoint above
909 // entirely, so for that line this is the only identity there is -- and
910 // unlike the ARP table it works for a controller on another subnet, which
911 // is exactly where seeding discovery from the configured output addresses
912 // reaches. The 1.x line carries the addresses in the state document
913 // already fetched; 2.x moved them to their own endpoint, so only ask for it
914 // when the first place came up empty.
915
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 std::string mac = ExperienceNetworkMac(st->ip, network);
916
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 times.
3 if (!mac.empty()) {
917 // Same spelling MultiSync uses for an ARP-derived address, so a
918 // device keeps one identity however it was discovered.
919
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 uuid = MAC_UUID_PREFIX + mac;
920
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 DumpControllerInfo();
921
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 st->matched();
922 2 return;
923 }
924
925
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 st->fetch(buildHttpURL(st->ip, "/api/current_state"), [this, st](bool curOk, const std::string& curResp) {
926
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 Json::Value cur;
927
4/8
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✗ Branch 7 not taken.
1 if (curOk && LoadJsonFromString(curResp, cur, JsonRoot::Object)) {
928
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 std::string curMac = ExperienceNetworkMac(st->ip, cur["network"]);
929
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (!curMac.empty()) {
930
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 uuid = MAC_UUID_PREFIX + curMac;
931 }
932 1 }
933
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 DumpControllerInfo();
934
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 st->matched();
935 1 });
936
4/4
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 4 times.
7 });
937
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 1 times.
7 });
938 }
939
940 234 void NetworkController::DetectWLEDController(Detection* st) {
941
2/2
✓ Branch 0 taken 218 times.
✓ Branch 1 taken 16 times.
234 if (st->html.find("WLED UI") == std::string::npos) {
942 218 st->noMatch();
943 218 return;
944 }
945
946
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.
48 st->fetch(buildHttpURL(st->ip, "/json/info"), [this, st](bool ok, const std::string& resp) {
947
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 16 times.
16 if (!ok) {
948 ✗ st->noMatch();
949 ✗ return;
950 }
951
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 Json::Value v;
952
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 LoadJsonFromString(resp, v, JsonRoot::Object);
953
954
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 vendor = "WLED";
955
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 vendorURL = "https://github.com/Aircoookie/WLED";
956
957 // WLED reports its own MAC in the document already being read here, so
958 // this costs no extra request. Unlike the ARP fallback in MultiSync it
959 // works for a controller on another subnet, which is where discovery
960 // seeded from configured output addresses usually reaches.
961
4/8
✓ 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.
✓ Branch 6 taken 16 times.
✗ Branch 7 not taken.
16 std::string mac = NormalizeMacAddress(v.get("mac", "").asString());
962
2/2
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 7 times.
16 if (!mac.empty()) {
963 // Same spelling MultiSync uses for an ARP-derived address, so a
964 // device keeps one identity however it was discovered.
965
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 uuid = MAC_UUID_PREFIX + mac;
966 }
967
968
2/4
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 16 times.
✗ Branch 3 not taken.
16 version = v["ver"].asString();
969
3/4
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✓ Branch 3 taken 1 times.
16 hostname = v["name"].asString();
970
2/4
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✗ Branch 3 not taken.
15 typeStr = v["arch"].asString();
971 15 typeId = kSysTypeWLED;
972 15 systemMode = BRIDGE_MODE;
973
974
2/4
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✗ Branch 3 not taken.
15 if (version != "") {
975 15 std::size_t verDot = version.find(".");
976
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 if (verDot != std::string::npos) {
977
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 majorVersion = atoi(version.substr(0, verDot).c_str());
978 15 std::size_t verDot2 = version.find(".", verDot + 1);
979
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 if (verDot2 != std::string::npos) {
980
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 minorVersion = atoi(version.substr(verDot + 1, verDot2 - (verDot + 1)).c_str());
981 }
982 }
983 }
984
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 st->matched();
985 17 });
986 }
987
988 // Twinkly lights serve no web UI at all -- "/" is a 404 -- so unlike every other
989 // vendor here there is no page to read. The 404 body is the only thing the HTML
990 // can offer, and it is just a pre-filter; the device's own API settles it.
991 //
992 // Nothing below authenticates, and that is a requirement rather than an
993 // accident. A Twinkly holds exactly ONE verified token: a second login+verify
994 // silently invalidates the first, which would cost a running Twinkly channel
995 // output the token it is streaming with and blank the lights until its next
996 // periodic check. Both endpoints used here answer without a token, so detection
997 // can run as often as discovery does without disturbing an output.
998 219 void NetworkController::DetectTwinklyController(Detection* st) {
999
2/2
✓ Branch 0 taken 210 times.
✓ Branch 1 taken 9 times.
219 if (st->html.find("This URI does not exist") == std::string::npos) {
1000 210 st->noMatch();
1001 210 return;
1002 }
1003
1004
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.
27 st->fetch(buildHttpURL(st->ip, "/xled/v1/gestalt"), [this, st](bool ok, const std::string& resp) {
1005
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (!ok) {
1006 ✗ st->noMatch();
1007 6 return;
1008 }
1009
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 Json::Value v;
1010
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 LoadJsonFromString(resp, v, JsonRoot::Object);
1011
6/8
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 8 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 6 times.
✓ Branch 7 taken 2 times.
9 if (v["product_name"].asString() != "Twinkly") {
1012
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 st->noMatch();
1013 6 return;
1014 }
1015
1016
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 vendor = "Twinkly";
1017
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 vendorURL = "https://www.twinkly.com";
1018 2 typeId = kSysTypeTwinkly;
1019 2 systemMode = BRIDGE_MODE;
1020
1021
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 hostname = v["device_name"].asString();
1022
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (hostname.empty()) {
1023 ✗ hostname = ip;
1024 }
1025 // product_code is the model as printed on the box, e.g. "TWS600STP".
1026
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 typeStr = v["product_code"].asString();
1027
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (typeStr.empty()) {
1028 ✗ typeStr = "Twinkly";
1029 }
1030
1031 // These report a real UUID of their own. Prefer it over the MAC-derived
1032 // stand-in: MultiSync treats a MAC: identity as provisional precisely so
1033 // a device's own can replace it.
1034
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 std::string id = v["uuid"].asString();
1035
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (!id.empty()) {
1036
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 uuid = id;
1037 } else {
1038 ✗ std::string mac = NormalizeMacAddress(v.get("mac", "").asString());
1039 ✗ if (!mac.empty()) {
1040 ✗ uuid = MAC_UUID_PREFIX + mac;
1041 }
1042 ✗ }
1043
1044 // The channel range is deliberately left alone. gestalt reports how
1045 // many LEDs the string has, but not where the user mapped it, and a
1046 // range without a start channel would be worse than none. MultiSync
1047 // backfills it from the configured output instead.
1048
1049
3/6
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
6 st->fetch(buildHttpURL(st->ip, "/xled/v1/fw/version"), [this, st](bool ok2, const std::string& resp2) {
1050 // Firmware version is a nicety; the device is identified either way.
1051
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (ok2) {
1052
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 Json::Value fw;
1053
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 LoadJsonFromString(resp2, fw, JsonRoot::Object);
1054
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 version = fw["version"].asString();
1055
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 if (version != "") {
1056 2 std::size_t verDot = version.find(".");
1057
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (verDot != std::string::npos) {
1058
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 majorVersion = atoi(version.substr(0, verDot).c_str());
1059 2 std::size_t verDot2 = version.find(".", verDot + 1);
1060
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (verDot2 != std::string::npos) {
1061
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 minorVersion = atoi(version.substr(verDot + 1, verDot2 - (verDot + 1)).c_str());
1062 }
1063 }
1064 }
1065 2 }
1066 2 st->matched();
1067 2 });
1068
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 6 times.
9 });
1069 }
1070
1071 24 void NetworkController::DumpControllerInfo(void) {
1072
2/4
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 24 times.
✗ Branch 3 not taken.
24 LogDebug(VB_SYNC, "Network Controller Info:\n"
1073 "IP : %s\n"
1074 "UUID : %s\n"
1075 "Hostname : %s\n"
1076 "Vendor : %s\n"
1077 "Vendor URL : %s\n"
1078 "TypeID : %d\n"
1079 "TypeStr : %s\n"
1080 "Channel Ranges : %s\n"
1081 "Firmware Version: %s\n"
1082 "Firmware MajorV : %u\n"
1083 "Firmware MinorV : %u\n"
1084 "System Mode : %s\n",
1085 ip.c_str(), uuid.c_str(), hostname.c_str(), vendor.c_str(), vendorURL.c_str(),
1086 (int)typeId, typeStr.c_str(), ranges.c_str(), version.c_str(),
1087 majorVersion, minorVersion, getFPPmodeStr(systemMode).c_str());
1088 24 }
1089