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