GCC Code Coverage Report


Directory: ./
File: oled/FPPStatusOLEDPage.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 172 558 30.8%
Functions: 11 24 45.8%
Branches: 154 1104 13.9%

Line Branch Exec Source
1 #include "fpp-pch.h"
2
3 #include "fpp-json.h"
4 #include <fstream>
5
6 #include <linux/nl80211.h>
7 #include <net/if.h>
8 #include <netlink/genl/ctrl.h>
9 #include <netlink/genl/genl.h>
10 #include <netlink/netlink.h>
11
12 #include <netinet/in.h>
13 #include <ifaddrs.h>
14 #include <netdb.h>
15 #include <unistd.h>
16
17 #include "../common.h"
18 #include "../CurlSocket.h"
19
20 #include "FPPMainMenu.h"
21 #include "FPPStatusOLEDPage.h"
22
23 #define MAX_PAGE 2
24
25 // nl80211 query for station signal strength. Replaces the legacy
26 // SIOCGIWRANGE/SIOCGIWSTATS ioctl path, which causes the kernel to log
27 // "uses wireless extensions which will stop working for Wi-Fi 7 hardware;
28 // use nl80211" and won't be supported by Wi-Fi 7 drivers.
29 namespace {
30 struct SignalQuery {
31 int signal_dbm = 0;
32 bool found = false;
33 int done = 0;
34 };
35
36 ✗ int signalValidCb(struct nl_msg* msg, void* arg) {
37 ✗ auto* q = static_cast<SignalQuery*>(arg);
38 ✗ auto* gnlh = static_cast<struct genlmsghdr*>(nlmsg_data(nlmsg_hdr(msg)));
39 struct nlattr* tb[NL80211_ATTR_MAX + 1];
40 ✗ nla_parse(tb, NL80211_ATTR_MAX, genlmsg_attrdata(gnlh, 0),
41 genlmsg_attrlen(gnlh, 0), nullptr);
42 ✗ if (!tb[NL80211_ATTR_STA_INFO]) {
43 ✗ return NL_SKIP;
44 }
45 struct nlattr* sinfo[NL80211_STA_INFO_MAX + 1];
46 static struct nla_policy stats_policy[NL80211_STA_INFO_MAX + 1] = {};
47 ✗ stats_policy[NL80211_STA_INFO_SIGNAL].type = NLA_U8;
48 ✗ if (nla_parse_nested(sinfo, NL80211_STA_INFO_MAX,
49 ✗ tb[NL80211_ATTR_STA_INFO], stats_policy) != 0) {
50 ✗ return NL_SKIP;
51 }
52 ✗ if (sinfo[NL80211_STA_INFO_SIGNAL]) {
53 // Reported as u8 but is a signed dBm value.
54 ✗ q->signal_dbm = (int8_t)nla_get_u8(sinfo[NL80211_STA_INFO_SIGNAL]);
55 ✗ q->found = true;
56 }
57 ✗ return NL_SKIP;
58 }
59
60 ✗ int signalFinishCb(struct nl_msg*, void* arg) {
61 ✗ static_cast<SignalQuery*>(arg)->done = 1;
62 ✗ return NL_SKIP;
63 }
64
65 ✗ int signalAckCb(struct nl_msg*, void* arg) {
66 ✗ static_cast<SignalQuery*>(arg)->done = 1;
67 ✗ return NL_STOP;
68 }
69
70 ✗ int signalErrorCb(struct sockaddr_nl*, struct nlmsgerr*, void* arg) {
71 ✗ static_cast<SignalQuery*>(arg)->done = 1;
72 ✗ return NL_SKIP;
73 }
74 } // namespace
75
76 ✗ int FPPStatusOLEDPage::getSignalStrength(char* iwname) {
77 ✗ unsigned int ifindex = if_nametoindex(iwname);
78 ✗ if (ifindex == 0) {
79 ✗ return 0;
80 }
81
82 ✗ struct nl_sock* sk = nl_socket_alloc();
83 ✗ if (!sk) {
84 ✗ return 0;
85 }
86 ✗ if (genl_connect(sk) != 0) {
87 ✗ nl_socket_free(sk);
88 ✗ return 0;
89 }
90 ✗ int family = genl_ctrl_resolve(sk, "nl80211");
91 ✗ if (family < 0) {
92 // Not a wireless interface (or nl80211 unavailable).
93 ✗ nl_socket_free(sk);
94 ✗ return 0;
95 }
96
97 ✗ struct nl_msg* msg = nlmsg_alloc();
98 ✗ struct nl_cb* cb = nl_cb_alloc(NL_CB_DEFAULT);
99 ✗ if (!msg || !cb) {
100 ✗ if (msg) nlmsg_free(msg);
101 ✗ if (cb) nl_cb_put(cb);
102 ✗ nl_socket_free(sk);
103 ✗ return 0;
104 }
105
106 ✗ SignalQuery q;
107 ✗ genlmsg_put(msg, 0, 0, family, 0, NLM_F_DUMP,
108 NL80211_CMD_GET_STATION, 0);
109 ✗ nla_put_u32(msg, NL80211_ATTR_IFINDEX, ifindex);
110
111 ✗ nl_cb_set(cb, NL_CB_VALID, NL_CB_CUSTOM, signalValidCb, &q);
112 ✗ nl_cb_set(cb, NL_CB_FINISH, NL_CB_CUSTOM, signalFinishCb, &q);
113 ✗ nl_cb_set(cb, NL_CB_ACK, NL_CB_CUSTOM, signalAckCb, &q);
114 ✗ nl_cb_err(cb, NL_CB_CUSTOM, signalErrorCb, &q);
115
116 ✗ if (nl_send_auto(sk, msg) >= 0) {
117 ✗ while (!q.done) {
118 ✗ int r = nl_recvmsgs(sk, cb);
119 ✗ if (r < 0) {
120 ✗ break;
121 }
122 }
123 }
124
125 ✗ nlmsg_free(msg);
126 ✗ nl_cb_put(cb);
127 ✗ nl_socket_free(sk);
128
129 ✗ if (!q.found) {
130 ✗ return 0;
131 }
132 // Map dBm to a 0-100 quality. -50 dBm or better -> 100%, -100 dBm or
133 // worse -> 0%; linear in between. Matches the "2*(level+100)" rule
134 // commonly used by wpa_supplicant/iw consumers.
135 ✗ int pct = 2 * (q.signal_dbm + 100);
136 ✗ if (pct < 0) {
137 ✗ pct = 0;
138 ✗ } else if (pct > 100) {
139 ✗ pct = 100;
140 }
141 ✗ return pct;
142 }
143
144 static bool debug = false;
145
146 4 static int writer(char* data, size_t size, size_t nmemb,
147 std::string* writerData) {
148
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 if (writerData == NULL)
149 ✗ return 0;
150 4 writerData->append(data, size * nmemb);
151 4 return size * nmemb;
152 }
153
154 12 FPPStatusOLEDPage::FPPStatusOLEDPage() :
155
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 _currentTest(""),
156 12 _testSpeed(500),
157 12 _curPage(0),
158 12 _doFullStatus(true),
159 12 _iterationCount(0),
160 24 _hasSensors(false) {
161
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 curl = curl_easy_init();
162
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 SetCurlCloseOnExec(curl);
163
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1);
164
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 curl_easy_setopt(curl, CURLOPT_URL, "http://localhost:32322/fppd/status");
165
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
166
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, 50);
167
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 250);
168
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writer);
169
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buffer);
170
171
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 readCapeImage();
172
3/8
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 12 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
12 mainMenu = new FPPMainMenu(this);
173 12 }
174 ✗ FPPStatusOLEDPage::~FPPStatusOLEDPage() {
175 ✗ curl_easy_cleanup(curl);
176 ✗ delete mainMenu;
177 ✗ }
178
179 ✗ void FPPStatusOLEDPage::outputNetwork(int idx, int y) {
180 ✗ printString(0, y, networks[idx]);
181 ✗ if (signalStrength[idx]) {
182 ✗ y = y + 7;
183 ✗ int cur = 5;
184 ✗ for (int x = 0; x < 7; x++) {
185 ✗ if (signalStrength[idx] > cur) {
186 ✗ drawLine(127 - (x / 2), y, 128, y);
187 }
188 ✗ y--;
189 ✗ cur += 15;
190 }
191 }
192 ✗ }
193
194 2 static bool compareLines(int x, std::vector<std::string>& lines1, std::vector<std::string>& lines2) {
195
3/6
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 2 times.
2 if (x < lines1.size() && x < lines2.size()) {
196 ✗ return lines1[x] != lines2[x];
197
2/6
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
2 } else if (x < lines1.size() || x < lines2.size()) {
198 2 return true;
199 }
200 ✗ return false;
201 }
202
203 4 bool FPPStatusOLEDPage::checkIfStatusChanged(Json::Value& result) {
204
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 std::string mode = result["mode_name"].asString();
205
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 mode[0] = toupper(mode[0]);
206
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
4 if (currentMode != mode) {
207 2 return true;
208 }
209
210 // if the status has materially changed, turn the oled back on
211 2 std::vector<std::string> lines;
212
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 int maxLines = getLinesPage0(lines, result, true);
213
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (compareLines(0, lines, _lastStatusLines)) {
214 // main status
215
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 _lastStatusLines = lines;
216 2 return true;
217 }
218 ✗ if (compareLines(1, lines, _lastStatusLines)) {
219 // running sequence
220 ✗ _lastStatusLines = lines;
221 ✗ return true;
222 }
223 ✗ if (compareLines(2, lines, _lastStatusLines)) {
224 // running song
225 ✗ _lastStatusLines = lines;
226 ✗ return true;
227 }
228 ✗ if (compareLines(5, lines, _lastStatusLines)) {
229 // running playlist
230 ✗ _lastStatusLines = lines;
231 ✗ return true;
232 }
233
234 ✗ return false;
235 4 }
236
237 6 int FPPStatusOLEDPage::getLinesPage0(std::vector<std::string>& lines,
238 Json::Value& result,
239 bool allowBlank) {
240
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
6 std::string status = result["status_name"].asString();
241
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
6 currentMode = result["mode_name"].asString();
242
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
6 currentMode[0] = toupper(currentMode[0]);
243
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 std::string line = currentMode;
244
2/8
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 6 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
6 bool isIdle = (status == "idle" || status == "testing");
245 6 int maxLines = 6;
246
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
6 if (currentMode != "Bridge") {
247 // bridge is always "idle" which isn't really true
248
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
6 line += ": " + status;
249 } else {
250 // bridge mode doesn't output a playlist section
251 ✗ isIdle = false;
252 ✗ maxLines = 5;
253 }
254
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (line.length() > 21) {
255 ✗ line.resize(21);
256 }
257
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 lines.push_back(line);
258
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (!_doFullStatus) {
259 ✗ return 1;
260 }
261
262
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (!isIdle) {
263 ✗ std::string line = result["current_sequence"].asString();
264 ✗ if (line != "" || allowBlank) {
265 ✗ lines.push_back(line);
266 }
267 ✗ line = result["current_song"].asString();
268 ✗ if (line != "" || allowBlank) {
269 ✗ lines.push_back(line);
270 }
271 ✗ line = result["time_elapsed"].asString();
272 ✗ if (line != "" || allowBlank) {
273 ✗ if (line != "") {
274 ✗ line = "Elapsed: " + line;
275 }
276 ✗ lines.push_back(line);
277 }
278 ✗ line = result["time_remaining"].asString();
279 ✗ if (line != "" || allowBlank) {
280 ✗ if (line != "") {
281 ✗ line = "Remaining: " + line;
282 }
283 ✗ lines.push_back(line);
284 }
285 ✗ line = result["current_playlist"]["playlist"].asString();
286 ✗ if (line != "" || allowBlank) {
287 ✗ if (line != "") {
288 ✗ line = "PL: " + line;
289 }
290 ✗ lines.push_back(line);
291 }
292 ✗ }
293
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
6 _hasSensors = result["sensors"].size() > 0;
294 6 return maxLines;
295 6 }
296 ✗ int FPPStatusOLEDPage::getLinesPage1(std::vector<std::string>& lines,
297 Json::Value& result,
298 bool allowBlank) {
299 ✗ std::vector<std::string> topLines;
300 ✗ for (int x = 0; x < result["sensors"].size(); x++) {
301 ✗ std::string line;
302 ✗ if (result["sensors"][x]["valueType"].asString() == "Temperature") {
303 ✗ line = result["sensors"][x]["label"].asString() + result["sensors"][x]["formatted"].asString();
304 ✗ line += "C (";
305 ✗ float i = result["sensors"][x]["value"].asFloat();
306 ✗ i *= 1.8;
307 ✗ i += 32;
308 char buf[25];
309 ✗ snprintf(buf, sizeof(buf), "%.1f", i);
310 ✗ line += buf;
311 ✗ line += "F)";
312 } else {
313 ✗ line = result["sensors"][x]["label"].asString() + " " + result["sensors"][x]["formatted"].asString();
314 }
315 ✗ if (topLines.size() < _topLine) {
316 ✗ topLines.push_back(line);
317 } else {
318 ✗ lines.push_back(line);
319 }
320 ✗ }
321 ✗ if (lines.empty()) {
322 ✗ _topLine = 0;
323 }
324 ✗ for (auto& l : topLines) {
325 ✗ lines.push_back(l);
326 }
327 ✗ if (lines.empty()) {
328 ✗ return getLinesPage0(lines, result, allowBlank);
329 }
330 ✗ return 6;
331 ✗ }
332 52 int FPPStatusOLEDPage::outputTopPart(int startY, int count) {
333
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 52 times.
52 if (networks.size() > 1) {
334 ✗ int idx = (count / 3) % networks.size();
335 ✗ int lines = GetLEDDisplayHeight() >= 64 ? 2 : 1;
336 ✗ if (networks.size() <= 5 && GetLEDDisplayHeight() > 65) {
337 ✗ idx = 0;
338 ✗ lines = networks.size() < 5 ? networks.size() : 5;
339 ✗ if (lines < 2)
340 ✗ lines = 2;
341 }
342 ✗ if (networks.size() <= lines) {
343 ✗ idx = 0;
344 }
345 ✗ outputNetwork(idx, startY);
346 ✗ startY += 8;
347 ✗ if (GetLEDDisplayHeight() > 65) {
348 ✗ startY += 2; // little extra space
349 }
350 ✗ for (int x = 1; x < lines; x++) {
351 ✗ idx++;
352 ✗ if (idx >= networks.size()) {
353 ✗ idx = 0;
354 }
355 ✗ outputNetwork(idx, startY);
356 ✗ startY += 8;
357 ✗ if (GetLEDDisplayHeight() > 65) {
358 ✗ startY += 2; // little extra space
359 }
360 }
361 } else {
362 // fppd answering means boot is done; past that, no address is "No Network"
363
3/4
✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 47 times.
✓ Branch 3 taken 5 times.
52 if (count < 60 && !_fppdSeen) {
364
2/4
✓ Branch 0 taken 47 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 47 times.
✗ Branch 3 not taken.
94 printString(0, startY, "FPP Booting...");
365 } else {
366
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
10 printString(0, startY, "No Network");
367 }
368 52 startY += 8;
369
1/2
✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
52 if (GetLEDDisplayHeight() >= 64) {
370 52 startY += 8;
371 }
372 }
373 52 return startY;
374 }
375 52 bool FPPStatusOLEDPage::getCurrentStatus(Json::Value& result) {
376 52 buffer.clear();
377 52 bool gotStatus = false;
378 52 auto rc = curl_easy_perform(curl);
379
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 48 times.
52 if (rc == CURLE_OK) {
380
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 if (LoadJsonFromString(buffer, result)) {
381
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 std::string status = result["status_name"].asString();
382 4 return true;
383
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
4 } else if (debug) {
384 ✗ LogWarn(VB_GENERAL, "Invalid json\n");
385 }
386
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 48 times.
48 } else if (debug) {
387 ✗ LogWarn(VB_GENERAL, "Curl returned bad status: %d\n", rc);
388 }
389 48 return false;
390 }
391
392 52 int FPPStatusOLEDPage::outputBottomPart(int startY, int count, bool statusValid, Json::Value& result) {
393
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 48 times.
52 if (statusValid) {
394 4 std::vector<std::string> lines;
395 4 std::string line;
396 4 int maxLines = 5;
397
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 if (_curPage == 0) {
398
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 maxLines = getLinesPage0(lines, result, GetLEDDisplayHeight() >= 64);
399 } else {
400 ✗ maxLines = getLinesPage1(lines, result, GetLEDDisplayHeight() >= 64);
401 }
402
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 if (maxLines > lines.size()) {
403 4 maxLines = lines.size();
404 }
405
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 if (GetLEDDisplayHeight() >= 64) {
406
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 4 times.
8 for (int x = 0; x < maxLines; x++) {
407
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 line = lines[x];
408
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 if (line.length() > 21) {
409 ✗ line.resize(21);
410 }
411
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 printString(0, startY, line);
412 4 startY += 8;
413
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 4 times.
4 if (GetLEDDisplayHeight() > 65) {
414 ✗ startY += 2;
415 }
416 }
417 } else {
418 ✗ int numLines = (GetLEDDisplayHeight() - startY) / 8;
419 ✗ int idx = count % maxLines;
420 ✗ if (numLines >= maxLines) {
421 ✗ idx = 0;
422 }
423 ✗ line = lines[idx];
424 ✗ if (line.length() > 21) {
425 ✗ line.resize(21);
426 }
427 ✗ printString(0, startY, line);
428 ✗ startY += 8;
429 ✗ idx++;
430 ✗ if (idx == maxLines) {
431 ✗ idx = 0;
432 }
433 ✗ if (maxLines > 1) {
434 ✗ line = lines[idx];
435 ✗ if (line.length() > 21) {
436 ✗ line.resize(21);
437 }
438 ✗ printString(0, startY, line);
439 }
440 }
441 4 } else {
442
1/2
✓ Branch 0 taken 48 times.
✗ Branch 1 not taken.
48 if (count < 75) {
443 // if less than 75 seconds since start, we'll assume we are booting up
444
1/2
✓ Branch 0 taken 48 times.
✗ Branch 1 not taken.
48 std::string line = "Booting.";
445 48 int idx = count % 8;
446
2/2
✓ Branch 0 taken 129 times.
✓ Branch 1 taken 48 times.
177 for (int i = 0; i < idx; i++) {
447
1/2
✓ Branch 0 taken 129 times.
✗ Branch 1 not taken.
129 line += ".";
448 }
449
1/2
✓ Branch 0 taken 48 times.
✗ Branch 1 not taken.
48 printString(10, startY, line);
450 48 startY += 8;
451 48 } else {
452 ✗ printString(0, startY, "FPPD is not running..");
453 ✗ startY += 8;
454 }
455
3/4
✓ Branch 0 taken 44 times.
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 44 times.
✗ Branch 3 not taken.
48 if (_capeImageWidth && oledType != OLEDType::TEXT_ONLY) {
456 44 int y = startY;
457
1/2
✓ Branch 0 taken 44 times.
✗ Branch 1 not taken.
44 if (oledType != OLEDType::TWO_COLOR) {
458 44 --y;
459 }
460
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 44 times.
44 if (GetLEDDisplayHeight() > 65) {
461 ✗ y += 4;
462 }
463
464 44 drawBitmap(0, y, &_capeImage[0], _capeImageWidth, _capeImageHeight);
465 }
466 }
467 52 return startY;
468 }
469
470 52 bool FPPStatusOLEDPage::doIteration(bool& displayOn) {
471 52 _iterationCount++;
472
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 52 times.
52 if (oledType == OLEDType::NONE) {
473 ✗ return false;
474 }
475
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 52 times.
52 if (oledForcedOff)
476 ✗ return false;
477
478 52 bool retVal = false;
479 52 int lastNSize = networks.size();
480
3/6
✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 52 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 52 times.
✗ Branch 5 not taken.
52 if ((_iterationCount % 30) == 0 || networks.size() <= 1) {
481 // every 30 seconds, rescan network for new connections
482
1/2
✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
52 fillInNetworks();
483 // if networks aren't configured or have changed, keep the oled on
484
4/6
✓ Branch 0 taken 40 times.
✓ Branch 1 taken 12 times.
✓ Branch 2 taken 40 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 52 times.
✗ Branch 5 not taken.
52 if (lastNSize != networks.size() || networks.size() <= 1) {
485 52 retVal = true;
486 52 displayOn = true;
487 }
488 }
489
490
1/2
✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
52 Json::Value result;
491
1/2
✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
52 bool statusValid = getCurrentStatus(result);
492
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 48 times.
52 if (statusValid) {
493 4 _fppdSeen = true;
494
3/6
✓ 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.
4 if (!displayOn || _lastStatusLines.empty()) {
495
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 displayOn = checkIfStatusChanged(result);
496
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 if (displayOn) {
497 4 retVal = true;
498 }
499 }
500 }
501
502
1/2
✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
52 if (displayOn) {
503
1/2
✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
52 clearDisplay();
504 52 int startY = 0;
505 52 int count = _iterationCount;
506 52 bool doWiFi = false;
507
4/18
✗ Branch 0 not taken.
✓ Branch 1 taken 52 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 52 times.
✗ Branch 10 not taken.
✓ Branch 11 taken 52 times.
✗ Branch 12 not taken.
✓ Branch 13 taken 52 times.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
52 if (count > 15 && loadWiFiImage()) {
508 ✗ count -= 15;
509 ✗ count %= 30;
510 ✗ doWiFi = count < 15;
511 }
512
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 52 times.
52 if (doWiFi) {
513 ✗ displayWiFiQR();
514
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 52 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
52 } else if (oledType != OLEDType::TWO_COLOR || !oledFlipped) {
515
1/2
✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
52 startY = outputTopPart(startY, _iterationCount);
516
2/4
✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 52 times.
✗ Branch 3 not taken.
52 if (oledType != OLEDType::TWO_COLOR && oledType != OLEDType::TEXT_ONLY) {
517 // two color display doesn't need the separator line
518
1/2
✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
52 drawLine(0, startY, 127, startY);
519 52 startY++;
520
2/4
✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 52 times.
52 if (GetLEDDisplayHeight() > 65) {
521 ✗ startY += 2;
522 }
523 }
524
1/2
✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
52 startY = outputBottomPart(startY, _iterationCount, statusValid, result);
525 } else {
526 // strange case... with 2 color display flipped, we still need to keep the
527 // network part in the "yellow" which is now below the main part
528 ✗ outputBottomPart(0, _iterationCount, statusValid, result);
529 ✗ outputTopPart(48, _iterationCount);
530 }
531
1/2
✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
52 flushDisplay();
532 }
533 52 return retVal;
534 52 }
535
536 52 void FPPStatusOLEDPage::fillInNetworks() {
537 52 networks.clear();
538 52 signalStrength.clear();
539
540 char hostname[HOST_NAME_MAX];
541 int result;
542 52 result = gethostname(hostname, HOST_NAME_MAX);
543
1/2
✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
52 std::string hn = "Host: ";
544
1/2
✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
52 hn += hostname;
545
1/2
✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
52 networks.push_back(hn);
546
1/2
✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
52 signalStrength.push_back(0);
547
548 // get all the addresses
549 struct ifaddrs *interfaces, *tmp;
550 52 getifaddrs(&interfaces);
551 52 tmp = interfaces;
552 char addressBuf[128];
553
2/2
✓ Branch 0 taken 156 times.
✓ Branch 1 taken 52 times.
208 while (tmp) {
554
1/2
✓ Branch 0 taken 156 times.
✗ Branch 1 not taken.
156 int max = OLEDPage::GetLEDDisplayWidth() / 6;
555
3/4
✓ Branch 0 taken 156 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 52 times.
✓ Branch 3 taken 104 times.
156 if (tmp->ifa_addr && tmp->ifa_addr->sa_family == AF_INET) {
556
1/2
✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
52 if (strncmp("usb", tmp->ifa_name, 3) != 0) {
557 // skip the usb* interfaces as we won't support multisync on those
558
1/2
✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
52 GetInterfaceAddress(tmp->ifa_name, addressBuf, NULL, NULL);
559
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 52 times.
52 if (strcmp(addressBuf, "127.0.0.1")) {
560 ✗ hn = tmp->ifa_name;
561 ✗ hn += ":";
562 ✗ hn += addressBuf;
563 ✗ if (hn.size() > max) {
564 ✗ hn = addressBuf;
565 }
566 ✗ networks.push_back(hn);
567 ✗ int i = getSignalStrength(tmp->ifa_name);
568 ✗ signalStrength.push_back(i);
569 }
570 }
571
1/2
✓ Branch 0 taken 104 times.
✗ Branch 1 not taken.
104 } else if (tmp->ifa_addr && tmp->ifa_addr->sa_family == AF_INET6) {
572 // FIXME for ipv6 multisync
573 } else {
574 // printf("Not a netork: %s\n", tmp->ifa_name);
575 }
576 156 tmp = tmp->ifa_next;
577 }
578 52 freeifaddrs(interfaces);
579 52 }
580
581 ✗ void FPPStatusOLEDPage::runTest(const std::string& test, bool ms) {
582 ✗ LogInfo(VB_GENERAL, "Running Test: %s Speed: %dms\n", test.c_str(), _testSpeed);
583
584 ✗ Json::Value val;
585 ✗ if (test == "Off") {
586 ✗ val["command"] = "Test Stop";
587 ✗ val["args"] = Json::Value(Json::ValueType::arrayValue);
588 ✗ _currentTest = "";
589 } else {
590 ✗ val["command"] = "Test Start";
591 ✗ val["args"] = Json::Value(Json::ValueType::arrayValue);
592 ✗ val["args"].append(std::to_string(_testSpeed));
593 ✗ _currentTest = test;
594 }
595 ✗ _multisyncTest = ms;
596 ✗ val["multisyncCommand"] = _multisyncTest;
597 ✗ val["multisyncHosts"] = "";
598 ✗ std::string channelRange = "";
599
600 ✗ if (test == "R-G-B Chase") {
601 ✗ val["args"].append("RGB Chase");
602 ✗ val["args"].append(channelRange);
603 ✗ val["args"].append("R-G-B");
604 ✗ } else if (test == "R-G-B-W Chase") {
605 ✗ val["args"].append("RGB Chase");
606 ✗ val["args"].append(channelRange);
607 ✗ val["args"].append("R-G-B-All");
608 ✗ } else if (test == "R-G-B-W-N Chase") {
609 ✗ val["args"].append("RGB Chase");
610 ✗ val["args"].append(channelRange);
611 ✗ val["args"].append("R-G-B-All-None");
612 ✗ } else if (test == "R-G-B Cycle") {
613 ✗ val["args"].append("RGB Cycle");
614 ✗ val["args"].append(channelRange);
615 ✗ val["args"].append("R-G-B");
616 ✗ } else if (test == "R-G-B-W Cycle") {
617 ✗ val["args"].append("RGB Cycle");
618 ✗ val["args"].append(channelRange);
619 ✗ val["args"].append("R-G-B-All");
620 ✗ } else if (test == "R-G-B-W-N Cycle") {
621 ✗ val["args"].append("RGB Cycle");
622 ✗ val["args"].append(channelRange);
623 ✗ val["args"].append("R-G-B-All-None");
624 ✗ } else if (test == "White") {
625 ✗ val["args"].append("RGB Single Color");
626 ✗ val["args"].append(channelRange);
627 ✗ val["args"].append("#ffffff");
628 ✗ } else if (test == "Red") {
629 ✗ val["args"].append("RGB Single Color");
630 ✗ val["args"].append(channelRange);
631 ✗ val["args"].append("#ff0000");
632 ✗ } else if (test == "Green") {
633 ✗ val["args"].append("RGB Single Color");
634 ✗ val["args"].append(channelRange);
635 ✗ val["args"].append("#00ff00");
636 ✗ } else if (test == "Blue") {
637 ✗ val["args"].append("RGB Single Color");
638 ✗ val["args"].append(channelRange);
639 ✗ val["args"].append("#0000ff");
640 ✗ } else if (_currentTest != "") {
641 ✗ LogWarn(VB_GENERAL, "Unknown test %s\n", test.c_str());
642 ✗ return;
643 }
644
645 ✗ std::string data = SaveJsonToString(val);
646
647 ✗ LogDebug(VB_COMMAND, "fppoled sending command to /api/command: %s\n", data.c_str());
648
649 ✗ buffer.clear();
650 ✗ CURL* curl = curl_easy_init();
651 ✗ SetCurlCloseOnExec(curl);
652 ✗ curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1);
653 ✗ curl_easy_setopt(curl, CURLOPT_URL, "http://localhost/api/command");
654
655 ✗ curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
656 ✗ curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, 50);
657 ✗ curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 250);
658 ✗ curl_easy_setopt(curl, CURLOPT_POST, 1);
659 ✗ curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data.c_str());
660 ✗ curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, -1L);
661 ✗ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writer);
662 ✗ curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buffer);
663 ✗ curl_easy_perform(curl);
664 ✗ curl_easy_cleanup(curl);
665 ✗ }
666 static std::vector<std::string> TESTS = { "Off", "R-G-B Cycle", "R-G-B-W-N Cycle", "R-G-B Chase", "R-G-B-W-N Chase" };
667 ✗ void FPPStatusOLEDPage::cycleTest() {
668 ✗ int idx = 1;
669 ✗ for (int x = 0; x < TESTS.size(); x++) {
670 ✗ if (TESTS[x] == _currentTest) {
671 ✗ idx = x + 1;
672 ✗ break;
673 }
674 }
675 ✗ if (idx >= TESTS.size()) {
676 ✗ idx = 0;
677 }
678 ✗ _currentTest = TESTS[idx];
679 ✗ runTest(_currentTest, _multisyncTest);
680 ✗ }
681
682 ✗ void FPPStatusOLEDPage::displayWiFiQR() {
683 ✗ clearDisplay();
684 ✗ int dh = GetLEDDisplayHeight();
685 ✗ int y = 0;
686 ✗ if (oledType == OLEDType::TWO_COLOR) {
687 ✗ if (GetOLEDOrientationFlipped()) {
688 ✗ y = 0;
689 ✗ } else if (_wifiImageHeight > 48) {
690 ✗ y = 15;
691 } else {
692 ✗ y = 16;
693 }
694 } else {
695 ✗ if (_wifiImageHeight > dh) {
696 ✗ y = 0;
697 } else {
698 ✗ y = (dh - _wifiImageHeight) / 2;
699 }
700 }
701 ✗ drawBitmap(2, y, &_wifiImage[0], _wifiImageWidth, _wifiImageHeight);
702 ✗ printString(12 + _wifiImageWidth, y + 12, "WiFi");
703 ✗ printString(12 + _wifiImageWidth, y + 20, "Hot Spot");
704 ✗ }
705
706 ✗ bool FPPStatusOLEDPage::loadWiFiImage(const std::string& tp) {
707 ✗ std::string fn = "/home/fpp/media/tmp/wifi-" + tp + ".ascii";
708 ✗ if (_wifiImageWidth) {
709 ✗ if (!FileExists(fn)) {
710 // file no longer exists, clear
711 ✗ _wifiImageWidth = 0;
712 ✗ _wifiImageHeight = 0;
713 ✗ _wifiImage.clear();
714 ✗ return false;
715 }
716 ✗ return true;
717 }
718 ✗ _wifiImageWidth = 0;
719 ✗ _wifiImageHeight = 0;
720 ✗ int maxSz = 0;
721 ✗ if (GetOLEDType() == OLEDPage::OLEDType::SMALL) {
722 ✗ maxSz = 15;
723 ✗ } else if (GetOLEDType() == OLEDPage::OLEDType::SINGLE_COLOR) {
724 ✗ maxSz = 66;
725 ✗ } else if (GetOLEDType() == OLEDPage::OLEDType::TWO_COLOR) {
726 ✗ maxSz = 50;
727 }
728 ✗ if (maxSz && FileExists(fn)) {
729 ✗ std::ifstream file(fn);
730 ✗ if (file.is_open()) {
731 ✗ std::string line;
732 ✗ bool readingBytes = false;
733 ✗ int scale = 2;
734 ✗ while (std::getline(file, line)) {
735 ✗ if (_wifiImageWidth == 0) {
736 ✗ if (line.size() > maxSz) {
737 // going to have to scale, first check if we can get the "H" version
738 ✗ if (tp == "L" && loadWiFiImage("H")) {
739 ✗ return true;
740 }
741 ✗ _wifiImageWidth = (int)line.size() / 2;
742 ✗ scale = 1;
743 } else {
744 ✗ _wifiImageWidth = (int)line.size();
745 }
746 }
747 ✗ for (int lc = 0; lc < scale; lc++) {
748 ✗ for (int x = 0; x < (_wifiImageWidth / scale);) {
749 ✗ uint8_t v = 0;
750 ✗ for (int y = 0; y < 8 / scale; y++) {
751 ✗ int idx = x * 2 + y * 2;
752 ✗ v <<= 1;
753 ✗ if (idx < line.size() && line[idx] == '#') {
754 ✗ v |= 1;
755 ✗ if (scale == 2) {
756 ✗ v <<= 1;
757 ✗ v |= 1;
758 }
759 ✗ } else if (scale == 2) {
760 ✗ v <<= 1;
761 }
762 }
763 ✗ x += 8 / scale;
764 ✗ _wifiImage.push_back(v);
765 }
766 ✗ ++_wifiImageHeight;
767 }
768 }
769 ✗ file.close();
770 ✗ }
771 ✗ return true;
772 ✗ }
773 ✗ return false;
774 ✗ }
775
776 5 bool FPPStatusOLEDPage::doAction(const std::string& action) {
777 // printf("In do action %s %d %s\n", action.c_str(), _testSpeed, _currentTest.c_str());
778
4/8
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 5 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 5 times.
5 if (action == "Test" || action == "Test/Down" || action == "Test Multisync") {
779 ✗ _multisyncTest = action == "Test Multisync";
780 ✗ _testSpeed = 500;
781 ✗ cycleTest();
782 ✗ _curPage = 0;
783 ✗ _topLine = 0;
784
2/6
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 5 times.
5 } else if (action == "Up" && _currentTest != "") {
785 ✗ _testSpeed *= 2;
786 ✗ runTest(_currentTest, _multisyncTest);
787
2/6
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 5 times.
5 } else if (action == "Down" && _currentTest != "") {
788 ✗ _testSpeed /= 2;
789 ✗ if (_testSpeed < 100) {
790 ✗ _testSpeed = 100;
791 }
792 ✗ runTest(_currentTest, _multisyncTest);
793
2/8
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 5 times.
5 } else if (action == "Down" && _hasSensors && _curPage == 1) {
794 ✗ _topLine++;
795
2/10
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 5 times.
5 } else if (action == "Up" && _hasSensors && _curPage == 1 && _topLine > 0) {
796 ✗ _topLine--;
797
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 } else if (action == "Enter" && !oledForcedOff) {
798
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
5 if (_hasSensors) {
799 ✗ _curPage++;
800 ✗ _topLine = 0;
801 ✗ if (_curPage == MAX_PAGE) {
802 ✗ _curPage = 0;
803 ✗ SetCurrentPage(mainMenu);
804 }
805 } else {
806 5 SetCurrentPage(mainMenu);
807 }
808 }
809 5 return false;
810 }
811