GCC Code Coverage Report


Directory: ./
File: channeloutput/Twinkly.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 201 244 82.4%
Functions: 18 23 78.3%
Branches: 191 416 45.9%

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 GPL v2 as described in the
10 * included LICENSE.GPL file.
11 */
12
13 #include "fpp-pch.h"
14
15 #include "fpp-json.h"
16
17 #include <arpa/inet.h>
18 #include <curl/curl.h>
19 #include <netinet/in.h>
20 #include <sys/socket.h>
21 #include <sys/time.h>
22 #include <sys/types.h>
23 #include <ctype.h>
24 #include <errno.h>
25 #include <netdb.h>
26 #include <signal.h>
27
28 #include "../Warnings.h"
29 #include "ChannelOutputSetup.h"
30 #include "../common.h"
31 #include "../fppversion.h"
32 #include "../log.h"
33
34 #include "Twinkly.h"
35
36 #include "../CurlManager.h"
37 #include "../CurlSocket.h"
38 #include "../Timers.h"
39
40 static const std::string TWINKLYTYPE = "Twinkly";
41
42 constexpr int TOKEN_LEN = 8;
43 constexpr int HEADER_LEN = (1 + TOKEN_LEN + 2 + 1);
44 constexpr int TWINKLY_TOKEN_VALIDATE_TIME = 120; // check the token every 120s
45
46 constexpr uint32_t MAXPACKETS = 40 * 60 * 60 * 4; // about 4 hours at 40fps,
47
48 2 const std::string& TwinklyOutputData::GetOutputTypeString() const {
49 2 return TWINKLYTYPE;
50 }
51
52 15 TwinklyOutputData::TwinklyOutputData(const Json::Value& config) :
53
3/6
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 15 times.
✗ Branch 5 not taken.
45 UDPOutputData(config) {
54 15 memset((char*)&twinklyAddress, 0, sizeof(sockaddr_in));
55 15 twinklyAddress.sin_family = AF_INET;
56 15 twinklyAddress.sin_port = htons(TWINKLY_PORT);
57
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 if (active) {
58
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 twinklyAddress.sin_addr.s_addr = toInetAddr(ipAddress, valid);
59
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 15 times.
15 if (!valid) {
60 ✗ AddOutputConfigWarning(14, "Could not resolve host name " + ipAddress + " - disabling output");
61 ✗ active = false;
62 }
63 }
64
65 15 portCount = (channelCount + 899) / 900;
66
67 15 twinklyIovecs = (struct iovec*)calloc(portCount * 2, sizeof(struct iovec));
68 15 twinklyBuffers = (uint8_t**)calloc(portCount, sizeof(unsigned char*));
69
70 15 int chanToOutput = channelCount;
71 15 int chan = startChannel - 1;
72
2/2
✓ Branch 0 taken 18 times.
✓ Branch 1 taken 15 times.
33 for (int x = 0; x < portCount; x++) {
73 // zeroed: the token bytes go on the wire before the first auth fills them
74 18 twinklyBuffers[x] = (unsigned char*)calloc(1, HEADER_LEN);
75 18 twinklyBuffers[x][0] = 3;
76 18 twinklyBuffers[x][9] = 0;
77 18 twinklyBuffers[x][10] = 0;
78 18 twinklyBuffers[x][11] = x;
79
80 // use scatter/gather for the packet. One IOV will contain
81 // the header, the second will point into the raw channel data
82 // and will be set at output time. This avoids any memcpy.
83 18 twinklyIovecs[x * 2].iov_base = twinklyBuffers[x];
84 18 twinklyIovecs[x * 2].iov_len = HEADER_LEN;
85 18 twinklyIovecs[x * 2 + 1].iov_base = nullptr;
86 18 twinklyIovecs[x * 2 + 1].iov_len = chanToOutput >= 900 ? 900 : chanToOutput;
87
88
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 13 times.
18 if (chanToOutput >= 900) {
89 5 chanToOutput -= 900;
90 5 chan += 900;
91 }
92 }
93 15 }
94 90 TwinklyOutputData::~TwinklyOutputData() {
95 // Every request above captures this. Drop the ones still in flight
96 // instead of letting their callbacks run on a freed object; deleted on
97 // the main loop (a reload, or shutdown), where cancelling is allowed.
98 30 CurlManager::INSTANCE.cancelRequests(curlOwner);
99 // The token timer captures this. StoppingOutput() normally removes it, but
100 // the timer must never outlive the object whatever path deleted it. Only
101 // while still started: the name is per address, so once stopped it may
102 // belong to a replacement output for the same device.
103
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 15 times.
30 if (outputStarted) {
104 ✗ Timers::INSTANCE.stopPeriodicTimer("Twinkly" + ipAddress);
105 }
106
2/2
✓ Branch 0 taken 18 times.
✓ Branch 1 taken 15 times.
66 for (int x = 0; x < portCount; x++) {
107 36 free(twinklyBuffers[x]);
108 }
109 30 free(twinklyBuffers);
110 30 free(twinklyIovecs);
111 60 }
112
113 30 void TwinklyOutputData::GetRequiredChannelRange(int& min, int& max) {
114 // channelCount is the total across all of the device's 900 channel ports
115 30 min = startChannel - 1;
116 30 max = startChannel + channelCount - 1;
117 30 }
118
119 1615 void TwinklyOutputData::PrepareData(unsigned char* channelData, UDPOutputMessages& msgs) {
120
3/6
✓ Branch 0 taken 1615 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1615 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1615 times.
✗ Branch 5 not taken.
1615 if (valid && active) {
121
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 1607 times.
1615 if (tokenPending) {
122 // A new token is waiting. It is copied into the packet headers here
123 // rather than from the curl callback that fetched it: the headers are
124 // handed to the sending threads as iovecs and are only safe to touch
125 // from this thread, which UDPOutput::PrepData() calls holding
126 // socketMutex. Writing them from the main loop could put half of an
127 // old token and half of a new one on the wire.
128
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 std::unique_lock<std::mutex> lk(authLock);
129
2/2
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 8 times.
17 for (int x = 0; x < portCount; x++) {
130 9 memcpy(&twinklyBuffers[x][1], authTokenBytes, TOKEN_LEN);
131 }
132 8 tokenPending = false;
133 8 }
134
135 // No token: the login failed (the device was unreachable or still
136 // booting when output started) or has not finished. Retrying only
137 // on the 120s token check left a device that came back sooner
138 // getting frames it ignores; valid means it answers pings again.
139
1/2
✓ Branch 0 taken 1615 times.
✗ Branch 1 not taken.
1615 uint64_t now = GetTimeMS();
140
11/14
✓ Branch 0 taken 1080 times.
✓ Branch 1 taken 535 times.
✓ Branch 2 taken 926 times.
✓ Branch 3 taken 154 times.
✓ Branch 4 taken 926 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 5 times.
✓ Branch 7 taken 921 times.
✓ Branch 8 taken 926 times.
✓ Branch 9 taken 689 times.
✓ Branch 10 taken 5 times.
✓ Branch 11 taken 1610 times.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
1615 if (!authInFlight && now - lastAuthAttemptMS > 5000 && getAuthToken().empty()) {
141 5 lastAuthAttemptMS = now;
142
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 authenticate();
143 }
144
145 1615 reauthCount++;
146
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1615 times.
1615 if (reauthCount > MAXPACKETS) {
147 // need to re-authenticate or lights will stop eventually. If this
148 // attempt fails, the periodic verifyToken() will retry within
149 // TWINKLY_TOKEN_VALIDATE_TIME rather than waiting another MAXPACKETS.
150 ✗ reauthCount = 0;
151 ✗ authenticate();
152 }
153
154 1615 int start = 0;
155 struct mmsghdr msg;
156 1615 memset(&msg, 0, sizeof(msg));
157
158 1615 msg.msg_hdr.msg_name = &twinklyAddress;
159 1615 msg.msg_hdr.msg_namelen = sizeof(sockaddr_in);
160 1615 bool anySkipped = false;
161 1615 bool allSkipped = true;
162
2/2
✓ Branch 0 taken 1644 times.
✓ Branch 1 taken 1615 times.
3259 for (int p = 0; p < portCount; p++) {
163
1/2
✓ Branch 0 taken 1644 times.
✗ Branch 1 not taken.
1644 bool nto = NeedToOutputFrame(channelData, startChannel - 1, start, twinklyIovecs[p * 2 + 1].iov_len);
164
1/2
✓ Branch 0 taken 1644 times.
✗ Branch 1 not taken.
1644 if (nto) {
165 1644 msg.msg_hdr.msg_iov = &twinklyIovecs[p * 2];
166 1644 msg.msg_hdr.msg_iovlen = 2;
167 1644 msg.msg_len = twinklyIovecs[p * 2 + 1].iov_len + twinklyIovecs[p * 2].iov_len;
168
169
2/4
✓ Branch 0 taken 1644 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1644 times.
✗ Branch 3 not taken.
1644 msgs[twinklyAddress.sin_addr.s_addr].push_back(msg);
170
171 // set the pointer to the channelData for the universe
172 1644 twinklyIovecs[p * 2 + 1].iov_base = (void*)(&channelData[startChannel - 1 + start]);
173 1644 allSkipped = false;
174 } else {
175 ✗ anySkipped = true;
176 }
177 1644 start += twinklyIovecs[p * 2 + 1].iov_len;
178 }
179
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1615 times.
1615 if (anySkipped) {
180 ✗ skippedFrames++;
181 } else {
182 1615 skippedFrames = 0;
183 }
184
1/2
✓ Branch 0 taken 1615 times.
✗ Branch 1 not taken.
1615 if (!allSkipped) {
185
1/2
✓ Branch 0 taken 1615 times.
✗ Branch 1 not taken.
1615 SaveFrame(&channelData[startChannel - 1], start);
186 }
187 }
188 1615 }
189
190 17 void TwinklyOutputData::StartingOutput() {
191 17 outputStarted = true;
192 17 authenticate();
193
2/4
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
17 Timers::INSTANCE.addPeriodicTimer("Twinkly" + ipAddress, TWINKLY_TOKEN_VALIDATE_TIME * 1000, [this]() {
194 ✗ verifyToken();
195 ✗ });
196 17 }
197 17 void TwinklyOutputData::StoppingOutput() {
198 // Clear this first. An authentication chain may be part way through on the
199 // main loop; without it, the chain would finish after we are done here and
200 // put the device back into "rt" mode with nothing left to send it frames.
201 17 outputStarted = false;
202
2/4
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
17 Timers::INSTANCE.stopPeriodicTimer("Twinkly" + ipAddress);
203 // This runs on the output thread or under outputThreadLock on a reload,
204 // and the call blocks for up to 5s. Only make it for a device that is
205 // answering and that this output put into rt mode; one whose ping failed
206 // is stopped here too, and would only time out.
207
9/12
✓ Branch 0 taken 16 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 16 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 7 times.
✓ Branch 5 taken 9 times.
✓ Branch 6 taken 16 times.
✓ Branch 7 taken 1 times.
✓ Branch 8 taken 7 times.
✓ Branch 9 taken 10 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
17 if (valid && !getAuthToken().empty()) {
208
3/6
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 7 times.
✗ Branch 5 not taken.
28 callRestAPI(true, "xled/v1/led/mode", "{\"mode\": \"off\"}");
209 }
210
2/4
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
17 setAuthToken("");
211 17 }
212
213 949 std::string TwinklyOutputData::getAuthToken() {
214
1/2
✓ Branch 0 taken 949 times.
✗ Branch 1 not taken.
949 std::unique_lock<std::mutex> lk(authLock);
215
1/2
✓ Branch 0 taken 949 times.
✗ Branch 1 not taken.
1898 return authToken;
216 949 }
217 17 void TwinklyOutputData::setAuthToken(const std::string& token) {
218
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
17 std::unique_lock<std::mutex> lk(authLock);
219
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
17 authToken = token;
220 17 }
221
222 8 bool TwinklyOutputData::applyAuthToken(const std::string& token) {
223
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 std::vector<uint8_t> decoded = base64Decode(token);
224
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (decoded.empty()) {
225 ✗ return false;
226 }
227 8 int len = std::min(TOKEN_LEN, (int)decoded.size());
228
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 std::unique_lock<std::mutex> lk(authLock);
229
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 authToken = token;
230 // A short token leaves the rest of the header zeroed rather than carrying
231 // bytes of the previous one.
232 8 memset(authTokenBytes, 0, TOKEN_LEN);
233 8 memcpy(authTokenBytes, &decoded[0], len);
234
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 lk.unlock();
235 8 tokenPending = true;
236 8 return true;
237 8 }
238
239 18 void TwinklyOutputData::finishAuth() {
240 18 authInFlight = false;
241
2/6
✗ Branch 0 not taken.
✓ Branch 1 taken 18 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 18 times.
18 if (authPending.exchange(false) && outputStarted) {
242 ✗ authenticate();
243 }
244 18 }
245
246 // The xled API reports failures in the body, not the status line: a rejected
247 // token comes back as HTTP 200 with {"code":1102}, so the response code on its
248 // own never shows an error.
249 17 static bool xledCallSucceeded(int rc, const std::string& resp) {
250
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 17 times.
17 if (rc != 200) {
251 ✗ return false;
252 }
253 try {
254
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
17 Json::Value v = LoadJsonFromString(resp);
255
2/4
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
17 return v["code"].asInt() == 1000;
256
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
17 } catch (std::exception& ex) {
257 ✗ return false;
258 ✗ }
259 }
260
261 22 void TwinklyOutputData::authenticate() {
262
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 22 times.
22 if (!outputStarted) {
263 ✗ return;
264 }
265
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 22 times.
22 if (authInFlight.exchange(true)) {
266 // One is already running. Remember this one instead of dropping it:
267 // the in-flight attempt may be about to fail, or may have been started
268 // before a stop/start cycle invalidated what it is fetching.
269 ✗ authPending = true;
270 ✗ return;
271 }
272
273
2/4
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 22 times.
✗ Branch 3 not taken.
22 const std::string base = "http://" + ipAddress + "/xled/v1/";
274
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
66 const std::list<std::string> jsonHeaders = { "Accept: application/json", "Content-Type: application/json" };
275
276
5/10
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 22 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 22 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 22 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 22 times.
✗ Branch 9 not taken.
154 CurlManager::INSTANCE.add(
277
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
44 base + "login", "POST",
278 "{\"challenge\": \"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=\"}", jsonHeaders,
279
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
22 [this, base](int rc, const std::string& resp) {
280
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 17 times.
18 if (!outputStarted) {
281
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 finishAuth();
282 9 return;
283 }
284 17 std::string token;
285
2/2
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 8 times.
17 if (rc == 200) {
286 try {
287
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 Json::Value v = LoadJsonFromString(resp);
288
5/14
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 9 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 9 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✓ Branch 11 taken 9 times.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
9 token = v.isMember("authentication_token") ? v["authentication_token"].asString() : "";
289
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
9 } catch (std::exception& ex) {
290 // fall through with an empty token and try again next cycle
291 ✗ }
292 }
293
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 9 times.
17 if (token.empty()) {
294
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 LogWarn(VB_CHANNELOUT, "Twinkly %s: login failed (http %d), will retry\n", ipAddress.c_str(), rc);
295
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 finishAuth();
296 8 return;
297 }
298
2/4
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 9 times.
9 if (base64Decode(token).empty()) {
299 ✗ LogWarn(VB_CHANNELOUT, "Twinkly %s: could not decode the authentication token, will retry\n", ipAddress.c_str());
300 ✗ finishAuth();
301 ✗ return;
302 }
303
304 // The token is carried explicitly through the chain rather than read
305 // back off the member: another attempt may replace it in between.
306 // POST is what every shipping release has used to verify - some units
307 // answer a GET here too, but not all firmware routes it.
308
3/6
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✓ Branch 3 taken 9 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
27 const std::list<std::string> authHeader = { "X-Auth-Token: " + token };
309
7/18
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 9 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 9 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 9 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 9 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 9 times.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
54 CurlManager::INSTANCE.add(base + "verify", "POST", "", authHeader, [this, base, token](int rc, const std::string& resp) {
310
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (!outputStarted) {
311 ✗ finishAuth();
312 1 return;
313 }
314
3/4
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 8 times.
9 if (!xledCallSucceeded(rc, resp)) {
315 // Carrying on would put the device into "rt" with a token it
316 // has not accepted, and every frame after that is dropped.
317
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogWarn(VB_CHANNELOUT, "Twinkly %s: token was not verified (http %d), will retry\n", ipAddress.c_str(), rc);
318
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 finishAuth();
319 1 return;
320 }
321 // Only a token the device accepted becomes ours. Installing it
322 // at login left a refused one in place, which stopped
323 // PrepareData()'s no-token retry from ever logging in again.
324
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 applyAuthToken(token);
325 const std::list<std::string> modeHeaders = { "Accept: application/json",
326 "Content-Type: application/json",
327
3/6
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 24 times.
✓ Branch 3 taken 8 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
48 "X-Auth-Token: " + token };
328
5/10
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 8 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 8 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 8 times.
✗ Branch 9 not taken.
40 CurlManager::INSTANCE.add(base + "led/mode", "POST", "{\"mode\": \"rt\"}", modeHeaders,
329 8 [this](int rc, const std::string& resp) {
330
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (!xledCallSucceeded(rc, resp)) {
331 ✗ LogWarn(VB_CHANNELOUT, "Twinkly %s: could not switch to realtime mode (http %d)\n", ipAddress.c_str(), rc);
332 }
333 8 finishAuth();
334 8 },
335 8 curlOwner);
336
3/10
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 8 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
24 },
337
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 curlOwner);
338
3/8
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✓ Branch 3 taken 8 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
26 },
339
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
22 curlOwner);
340 22 }
341 ✗ void TwinklyOutputData::verifyToken() {
342 ✗ if (!outputStarted) {
343 ✗ return;
344 }
345 ✗ std::string url = "http://" + ipAddress + "/xled/v1/verify";
346 ✗ std::list<std::string> extraHeaders;
347 ✗ std::string xat = "X-Auth-Token: " + getAuthToken();
348 ✗ extraHeaders.push_back(xat);
349 ✗ CurlManager::INSTANCE.add(url, "POST", "", extraHeaders, [this](int rc, const std::string& resp) {
350 // printf("%d: %s\n", rc, resp.c_str());
351 ✗ if (!xledCallSucceeded(rc, resp)) {
352 // no answer, or the token was rejected. Retry async
353 ✗ authenticate();
354 }
355 ✗ },
356 ✗ curlOwner);
357 ✗ }
358
359 // URL Helpers
360 ✗ size_t urlWriteData(void* buffer, size_t size, size_t nmemb, void* userp) {
361 ✗ std::string* str = (std::string*)userp;
362
363 ✗ str->append(static_cast<const char*>(buffer), size * nmemb);
364
365 ✗ return size * nmemb;
366 }
367 7 Json::Value TwinklyOutputData::callRestAPI(bool isPost, const std::string& path, const std::string& data) {
368
3/6
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 7 times.
✗ Branch 5 not taken.
7 std::string url = "http://" + ipAddress + "/" + path;
369
370
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 CURL* curl = curl_easy_init();
371
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 SetCurlCloseOnExec(curl);
372 7 struct curl_slist* headers = NULL;
373
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 std::string userAgent = "FPP/";
374
2/4
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
7 userAgent += getFPPVersionTriplet();
375
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 std::string resp = "";
376
377 CURLcode status;
378
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, urlWriteData);
379
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 curl_easy_setopt(curl, CURLOPT_WRITEDATA, &resp);
380
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
381
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1);
382
11/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 7 times.
✗ Branch 11 not taken.
✓ 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 7 times.
✗ Branch 21 not taken.
✗ 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 (startsWith(data, "{") && endsWith(data, "}")) {
383
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 headers = curl_slist_append(headers, "Accept: application/json");
384
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 headers = curl_slist_append(headers, "Content-Type: application/json");
385 }
386
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 const std::string token = getAuthToken();
387
2/4
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
7 if (token != "") {
388
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 const std::string at = "X-Auth-Token: " + token;
389
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 headers = curl_slist_append(headers, at.c_str());
390 7 }
391
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 if (headers) {
392
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
393 }
394
395
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 if (isPost) {
396
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data.c_str());
397
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 curl_easy_setopt(curl, CURLOPT_POST, 1);
398 }
399
400
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)5);
401
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 2);
402
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 curl_easy_setopt(curl, CURLOPT_USERAGENT, userAgent.c_str());
403
404
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 status = curl_easy_perform(curl);
405
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 2 times.
7 if (status != CURLE_OK) {
406
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 LogErr(VB_GENERAL, "curl_easy_perform() failed: %s\n", curl_easy_strerror(status));
407
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 curl_easy_cleanup(curl);
408
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 curl_slist_free_all(headers);
409
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 return Json::Value();
410 }
411
412 // printf("%s: %s\n", url.c_str(), resp.c_str());
413
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 LogDebug(VB_GENERAL, "%s %s resp: %s\n", isPost ? "POST" : "GET", url.c_str(), resp.c_str());
414
415
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 curl_easy_cleanup(curl);
416
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 curl_slist_free_all(headers);
417
418 try {
419
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 return LoadJsonFromString(resp);
420 ✗ } catch (std::exception& ex) {
421 // not sure what to do here. The only call that uses the return is the authentication
422 // so we'll return an empty json and hope that is good enough
423 ✗ return Json::Value();
424 ✗ }
425 7 }
426
427 15 void TwinklyOutputData::DumpConfig() {
428 15 LogDebug(VB_CHANNELOUT, "Twinkly: %s %d:%d:%d:%d %s\n",
429 description.c_str(),
430 active,
431 startChannel,
432 channelCount,
433 type,
434 ipAddress.c_str());
435 15 }
436