GCC Code Coverage Report


Directory: ./
File: boot/FPPINIT_Config.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 883 1125 78.5%
Functions: 62 66 93.9%
Branches: 1372 3115 44.0%

Line Branch Exec Source
1 /*
2 * This file is part of the Falcon Player (FPP) and is Copyright (C)
3 * 2013-2024 by the Falcon Player Developers.
4 *
5 * The Falcon Player (FPP) is free software, and is covered under
6 * multiple Open Source licenses. Please see the included 'LICENSES'
7 * file for descriptions of what files are covered by each license.
8 *
9 * This source file is covered under the LGPL v2.1 as described in the
10 * included LICENSE.LGPL file.
11 */
12
13 #include <algorithm>
14 #include <chrono>
15 #include "fpp-json.h"
16 #include <cctype>
17 #include <cmath>
18 #include <cstdio>
19 #include <cstdlib>
20 #include <cstring>
21 #include <fcntl.h>
22 #include <filesystem>
23 #include <fstream>
24 #include <functional>
25 #include <iterator>
26 #include <pwd.h>
27 #include <sys/stat.h>
28 #include <sys/wait.h>
29 #include <thread>
30 #include <unistd.h>
31 #include <vector>
32
33 #if __has_include(<gpiod.hpp>)
34 #include <gpiod.hpp>
35 #define HASGPIOD
36 #if __has_include(<gpiodcxx/chip.hpp>)
37 #define IS_GPIOD_CXX_V2
38 #endif
39 #endif
40
41 #include "FPPINIT.h"
42
43 3 void DetectCape() {
44
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 times.
3 if (!FileExists("/etc/fpp/container")) {
45 #ifdef CAPEDETECT
46 2 int count = 0;
47 #ifdef PLATFORM_PI
48 2 modprobe("i2c-dev");
49 #endif
50
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (!FileExists(I2C_DEV)) {
51 2 printf("FPP - Waiting up to 3s for %s to appear for Cape/Hat detection\n", I2C_DEV.c_str());
52 }
53
5/6
✓ Branch 0 taken 1202 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1200 times.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 1200 times.
✓ Branch 5 taken 2 times.
1202 while (!FileExists(I2C_DEV) && count < 600) {
54
1/2
✓ Branch 0 taken 1200 times.
✗ Branch 1 not taken.
1200 std::this_thread::sleep_for(std::chrono::milliseconds(5));
55 1200 count++;
56 }
57 2 printf("FPP - Checking for Cape/Hat\n");
58
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
4 exec("/opt/fpp/src/fppcapedetect -no-set-permissions");
59 #endif
60 }
61
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.
7 PutFileContents(FPP_MEDIA_DIR + "/tmp/cape_detect_done", "1");
62 3 }
63
64 3 void checkSSHKeys() {
65 3 int keyCount = 0;
66 3 int pubCount = 0;
67
5/8
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 25 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 25 times.
✓ Branch 7 taken 3 times.
28 for (const auto& entry : std::filesystem::directory_iterator("/etc/ssh")) {
68
3/4
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20 times.
✓ Branch 3 taken 5 times.
25 if (entry.is_regular_file()) {
69
1/2
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
20 const auto filename = entry.path().filename();
70
5/8
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 20 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 12 times.
✓ Branch 7 taken 8 times.
60 if (contains(filename, "ssh_host")) {
71
4/6
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 6 times.
✓ Branch 5 taken 6 times.
12 if (filename.extension() == ".pub") {
72 6 pubCount++;
73 } else {
74 6 keyCount++;
75 }
76 }
77 20 }
78 3 }
79 // make sure we have at least two private and public keys (really should be three, but some platforms don't support ed25519, so we'll just check for two)
80
3/4
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
3 if (keyCount >= 2 && pubCount >= 2) {
81 2 return;
82 }
83 1 printf(" - Regenerating SSH keys\n");
84
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (FileExists("/dev/hwrng")) {
85 // if the hwrng exists, use it to seed the urandom generator
86 // with some random data
87 ✗ exec("dd if=/dev/hwrng of=/dev/urandom count=1 bs=4096 status=none");
88 } else {
89 // No hardware RNG to seed from: ssh-keygen's getrandom() can block
90 // waiting for the CRNG to seed on a freshly booted board with little
91 // entropy. fppinit no longer waits for haveged on every boot (it's only
92 // needed right here -- for first-boot key generation on a board without
93 // an RNG), so start it on demand to seed the pool before generating keys.
94
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 exec("/usr/bin/systemctl start haveged.service");
95 }
96
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 execbg("/usr/bin/ssh-keygen -q -N \"\" -t ecdsa -f /etc/ssh/ssh_host_ecdsa_key &");
97
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 execbg("/usr/bin/ssh-keygen -q -N \"\" -t ed25519 -f /etc/ssh/ssh_host_ed25519_key &");
98
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 execbg("/usr/bin/ssh-keygen -q -N \"\" -t rsa -b 2048 -f /etc/ssh/ssh_host_rsa_key &");
99 }
100
101 // Copies files/config from the /boot partition to /home/fpp/media
102 // On the Pi, /boot (or /boot/firmware) is vfat and thus the
103 // user could stick a default config on there
104 3 void handleBootPartition() {
105
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 std::string bootDir = "/boot";
106
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 if (DirectoryExists("/boot/firmware")) {
107
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 bootDir = "/boot/firmware";
108 }
109
3/6
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 3 times.
3 if (FileExists(bootDir + "/fpp_boot.sh")) {
110 ✗ std::string cmd = "/bin/bash " + bootDir + "/fpp_boot.sh";
111 ✗ exec(cmd);
112 ✗ }
113
3/6
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 3 times.
3 if (DirectoryExists(bootDir + "/fpp")) {
114 ✗ if (!FileExists(bootDir + "/fpp/copy_done")) {
115 ✗ std::string cmd = "/usr/bin/cp -a " + bootDir + "/fpp/* " + FPP_MEDIA_DIR;
116 ✗ exec(cmd);
117 ✗ PutFileContents(bootDir + "/fpp/copy_done", "1");
118 ✗ }
119 }
120 3 }
121
122 3 void checkHostName() {
123 3 std::string hn;
124
3/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 2 times.
3 if (FileExists("/etc/fpp/container")) {
125
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 std::string CID = execAndReturn("/usr/bin/head -1 /proc/1/cgroup | sed -e \"s/.*docker-//\" | cut -c1-12");
126
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 hn = execAndReturn("/usr/bin/hostname");
127
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 TrimWhiteSpace(hn);
128
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 TrimWhiteSpace(CID);
129
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 setRawSetting("HostName", hn);
130
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 setRawSetting("HostDescription", "Docker ID: " + CID);
131 1 } else {
132
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 getRawSetting("HostName", hn);
133 // By default on installation, hostname is fpp, but it does not appear in the settings file
134
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 if (hn == "") {
135
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 hn = "fpp";
136 }
137 // Goes to a root shell below; the UI only stores [A-Za-z0-9-] names.
138
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
2 if (!isShellSafeWord(hn)) {
139 ✗ printf("Ignoring invalid HostName setting\n");
140 ✗ return;
141 }
142 char hostname[256];
143 2 int result = gethostname(hostname, 256);
144
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 if (result == 0 && hn != hostname) {
145 // need to reset the hostname
146
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 printf("Changing hostname from %s to %s\n", hostname, hn.c_str());
147
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 std::string cmd = "/usr/bin/hostname " + hn;
148
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 exec(cmd);
149
150
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.
4 PutFileContents("/etc/hostname", hn + "\n");
151
152
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 std::string hosts = GetFileContents("/etc/hosts");
153
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 std::vector<std::string> lines = split(hosts, '\n');
154
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 hosts = "";
155 2 bool found = false;
156
2/2
✓ Branch 0 taken 12 times.
✓ Branch 1 taken 2 times.
14 for (auto& line : lines) {
157
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 10 times.
12 if (line.starts_with("127.0.1.1")) {
158
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 hosts += "127.0.1.1\t";
159
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 hosts += hn;
160 2 found = true;
161 } else {
162
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 hosts += line;
163 }
164
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 hosts += "\n";
165 }
166
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (!found) {
167 ✗ hosts += "127.0.1.1\t";
168 ✗ hosts += hn;
169 }
170
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
4 PutFileContents("/etc/hosts", hosts);
171
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 execbg("/usr/bin/systemctl restart avahi-daemon &");
172 2 }
173 }
174
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 }
175
176 8 void runScripts(const std::string tp, bool userBefore) {
177
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 std::string pfx = "FPPDIR=/opt/fpp SRCDIR=/opt/fpp/src ";
178
179
8/14
✓ 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 1 times.
✓ Branch 7 taken 7 times.
✓ Branch 8 taken 8 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1 times.
✓ Branch 11 taken 7 times.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
8 if (userBefore && FileExists(FPP_MEDIA_DIR + "/scripts/UserCallbackHook.sh")) {
180
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 printf("FPP - Running UserCallbackHook.sh %s\n", tp.c_str());
181
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 execbg(pfx + FPP_MEDIA_DIR + "/scripts/UserCallbackHook.sh " + tp);
182 }
183
6/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 2 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2 times.
✓ Branch 9 taken 8 times.
10 for (const auto& entry : std::filesystem::directory_iterator(FPP_MEDIA_DIR + "/plugins")) {
184
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 if (entry.is_directory()) {
185
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 std::string filename = entry.path().filename();
186
5/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 taken 2 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2 times.
✗ Branch 9 not taken.
2 std::string cmd = FPP_MEDIA_DIR + "/plugins/" + filename + "/scripts/" + tp + ".sh";
187
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 if (FileExists(cmd)) {
188
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 printf("FPP - Running plugin %s script for %s\n", filename.c_str(), tp.c_str());
189
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 execbg(pfx + cmd);
190 }
191 2 }
192 8 }
193
3/14
✗ Branch 0 not taken.
✓ Branch 1 taken 8 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 8 times.
✗ Branch 10 not taken.
✓ Branch 11 taken 8 times.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
8 if (!userBefore && FileExists(FPP_MEDIA_DIR + "/scripts/UserCallbackHook.sh")) {
194 ✗ printf("FPP - Running UserCallbackHook.sh %s\n", tp.c_str());
195 ✗ execbg(pfx + FPP_MEDIA_DIR + "/scripts/UserCallbackHook.sh " + tp);
196 }
197 8 }
198
199 3 void checkFSTAB() {
200
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 std::string cont = GetFileContents("/etc/fstab");
201
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 2 times.
3 if (cont.find("home/fpp/media") == std::string::npos) {
202
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 cont += "\n";
203
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 cont += "#####################################\n";
204
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 cont += "#/dev/sda1 /home/fpp/media auto defaults,noatime,nodiratime,exec,nofail,flush,uid=1000,gid=1000 0 0\n";
205
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 cont += "#####################################\n";
206
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 PutFileContents("/etc/fstab", cont);
207 }
208 3 }
209
210 3 void createDirectories() {
211 static std::vector<std::string> DIRS = { "", "config", "effects", "logs", "music", "playlists", "scripts", "sequences",
212 "upload", "videos", "plugins", "plugindata", "exim4", "images", "cache",
213
3/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 not taken.
✗ Branch 7 not taken.
9 "backups", "tmp", "virtualdisplay_assets" };
214
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 printf("FPP - Checking for required directories\n");
215 3 uid_t fppUid = 0;
216 3 gid_t fppGid = 0;
217
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 bool haveFppUser = GetUserIds("fpp", &fppUid, &fppGid);
218
2/2
✓ Branch 0 taken 54 times.
✓ Branch 1 taken 3 times.
57 for (auto& d : DIRS) {
219
2/4
✓ Branch 0 taken 54 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 54 times.
✗ Branch 3 not taken.
54 std::string dir = FPP_MEDIA_DIR + "/" + d;
220
3/4
✓ Branch 0 taken 54 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✓ Branch 3 taken 42 times.
54 if (!DirectoryExists(dir)) {
221
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 printf(" Creating directory %s\n", dir.c_str());
222 12 mkdir(dir.c_str(), 0775);
223
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 4 times.
12 if (haveFppUser) {
224 8 chown(dir.c_str(), fppUid, fppGid);
225 }
226 }
227 54 }
228 3 }
229
230 3 void setupApache() {
231
3/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 not taken.
✗ Branch 7 not taken.
3 static const std::string UIPASSCONF = FPP_MEDIA_DIR + "/config/ui-password-config.conf";
232
3/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 not taken.
✗ Branch 7 not taken.
3 static const std::string HTPWD = FPP_MEDIA_DIR + "/config/.htpasswd";
233
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 if (!FileExists(UIPASSCONF)) {
234
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 std::string content = "<RequireAny>\n Require all granted\n</RequireAny>\n\nSetEnvIf Host ^ LOCAL_PROTECT=0\n";
235
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 PutFileContents(UIPASSCONF, content);
236 3 } /* else {
237 std::string content = GetFileContents(UIPASSCONF);
238 if (content.find("php_value") != std::string::npos) {
239 printf(" Modifying .htaccess file\n");
240 std::vector<std::string> lines = split(content, '\n');
241 content = "";
242 for (auto& line : lines) {
243 if (line.find("php_value") == std::string::npos) {
244 content += line;
245 }
246 content += "\n";
247 }
248 PutFileContents(UIPASSCONF, content);
249 }
250 } */
251
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 if (!FileExists(HTPWD)) {
252
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
6 PutFileContents(HTPWD, "");
253 }
254 // Apache sends /api's CORS headers while this file exists; www's
255 // SetupCrossOriginAPI() keeps it in step when the setting changes.
256
3/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 not taken.
✗ Branch 7 not taken.
3 static const std::string CROSSORIGIN = FPP_MEDIA_DIR + "/config/api-cross-origin";
257 3 std::string allow;
258
6/20
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 3 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✓ Branch 10 taken 3 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 3 times.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✓ Branch 15 taken 3 times.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
9 if (getRawSetting("AllowCrossOriginAPI", allow) && allow == "1") {
259 ✗ if (!FileExists(CROSSORIGIN)) {
260 ✗ PutFileContents(CROSSORIGIN, "Written by FPP for the AllowCrossOriginAPI setting\n");
261 }
262
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 3 times.
3 } else if (FileExists(CROSSORIGIN)) {
263 ✗ unlink(CROSSORIGIN.c_str());
264 }
265 /* if (!FileExists("/opt/fpp/www/proxy/.htaccess")) {
266 printf("Creating proxy .htaccess link\n");
267 if (!FileExists(FPP_MEDIA_DIR + "/config/proxies")) {
268 PutFileContents(FPP_MEDIA_DIR + "/config/proxies", "");
269 }
270 symlink((FPP_MEDIA_DIR + "/config/proxies").c_str(), "/opt/fpp/www/proxy/.htaccess");
271 } */
272 3 }
273
274 3 void handleBootActions() {
275 3 std::string v;
276
6/18
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 3 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✓ Branch 8 taken 3 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 3 times.
✗ Branch 11 not taken.
✗ Branch 12 not taken.
✓ Branch 13 taken 3 times.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
9 if (getRawSetting("BootActions", v) && !v.empty()) {
277 ✗ exec("/opt/fpp/scripts/handle_boot_actions");
278 }
279 3 }
280
281 3 inline const std::string& mapBBBLedValue(const std::string& v) {
282 #ifdef PLATFORM_BB64
283
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 if (v == "cpu") {
284
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 static const std::string activity = "activity";
285 3 return activity;
286 }
287 #endif
288 ✗ return v;
289 }
290
291 #ifdef PLATFORM_BB64
292 //
293 // The PocketBeagle2's identity lives in an on-board EEPROM that u-boot reads to
294 // decide how much DDR the board has. Some boards leave the factory with it
295 // blank or half-written, and until now that was only ever noticed by the
296 // flashing paths (SD/BB64-AutoFlash.sh, SD/flash_storage.sh) -- a board that
297 // reached FPP any other way kept an unusable identity, which also costs it its
298 // M5- stats UUID.
299 //
300 // The detection is done here rather than in the shell script because the script
301 // is only cheap on a board that needs nothing done to it in the *industrial*
302 // case: on a healthy base board its "programmed PB20 but has eMMC" cross-check
303 // waits out the full eMMC timeout, ~10s, every single boot. That check is
304 // there to stop a misidentified board being FLASHED, which is not what is
305 // happening here -- by the time we boot, u-boot has already sized the RAM. So
306 // read the two fields that decide whether anything needs doing, and only fork
307 // the script when they say it does. A healthy board costs one 56-byte read.
308 //
309 3 static bool pb2EepromValid(const std::string& path) {
310
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 std::ifstream in(path, std::ios::binary);
311
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 3 times.
3 if (!in.good()) {
312 ✗ return false;
313 }
314 unsigned char buf[56];
315
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 in.read((char*)buf, sizeof(buf));
316
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (in.gcount() != (std::streamsize)sizeof(buf)) {
317 ✗ return false;
318 }
319 // Magic header, then the six ASCII digits of the unit number at 50..55.
320 // These are exactly the fields check_pb2_eeprom.sh decides NEEDS_FIX from.
321
3/4
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
3 if (buf[0] != 0xAA || buf[1] != 0x55) {
322 1 return false;
323 }
324
2/2
✓ Branch 0 taken 12 times.
✓ Branch 1 taken 2 times.
14 for (int i = 50; i < 56; i++) {
325
2/4
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 12 times.
12 if (buf[i] < '0' || buf[i] > '9') {
326 ✗ return false;
327 }
328 }
329 2 return true;
330 3 }
331
332 // Both paths are overridable so this can be exercised against fixtures, exactly
333 // as capes/drivers/bb64/*.sh already allow. Nothing in FPP sets them, and the
334 // same names are honoured by the script we fork, so a fixture run stays a
335 // fixture run all the way down.
336 6 static std::string pb2Path(const char* env, const char* dflt) {
337 6 const char* v = getenv(env);
338
9/16
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 3 times.
✓ 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 taken 3 times.
✓ Branch 10 taken 3 times.
✓ Branch 11 taken 3 times.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
21 return (v && *v) ? std::string(v) : std::string(dflt);
339 }
340
341 3 static void checkPB2EEPROM() {
342 // Only the PocketBeagle2 family has this EEPROM at this address with this
343 // layout. The model string does not distinguish base from industrial, but
344 // it does not need to -- the script works that out if it is called.
345
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 std::ifstream m(pb2Path("PB2_MODEL", "/proc/device-tree/model"), std::ios::binary);
346
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 3 times.
3 if (!m.good()) {
347 ✗ return;
348 }
349
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
6 std::string model((std::istreambuf_iterator<char>(m)), std::istreambuf_iterator<char>());
350
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (model.find("PocketBeagle2") == std::string::npos) {
351 ✗ return;
352 }
353
354
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 const std::string eeprom = pb2Path("PB2_EEPROM", "/sys/bus/i2c/devices/0-0050/eeprom");
355
3/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 2 times.
3 if (!FileExists(eeprom)) {
356 // Nothing to repair: with 0x50 silent there is nothing to write to.
357 // The script's full diagnosis of why costs a 10s wait and cannot fix
358 // it from here, so leave that to the flashing path and just say so.
359
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 printf("FPP - PocketBeagle2: no EEPROM at %s, board identity unavailable\n", eeprom.c_str());
360 1 return;
361 }
362
3/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 1 times.
2 if (pb2EepromValid(eeprom)) {
363 1 return;
364 }
365
366
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 printf("FPP - PocketBeagle2 EEPROM identity is blank or malformed, repairing\n");
367
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 exec("/bin/bash /opt/fpp/capes/drivers/bb64/check_pb2_eeprom.sh");
368
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 if (pb2EepromValid(eeprom)) {
369 // u-boot read the old contents at power-on, so an industrial board that
370 // came up as the 512MB base variant stays that way until it reboots.
371
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 printf("FPP - PocketBeagle2 EEPROM repaired; reboot for u-boot to pick up the identity\n");
372 }
373
6/6
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 2 times.
7 }
374 #endif
375
376 6 void configureBBB() {
377 #ifdef PLATFORM_BBB
378 ✗ if (FileExists("/dev/mmcblk1")) {
379 // full size beagle, check the bootloader
380 ✗ int fd = open("/dev/mmcblk1", O_RDONLY);
381 ✗ lseek(fd, 393488, SEEK_SET);
382 uint8_t buf[25];
383 ✗ read(fd, buf, 23);
384 ✗ close(fd);
385 ✗ buf[23] = 0;
386 ✗ std::string uboot = (const char*)buf;
387 ✗ if (uboot != "U-Boot 2022.04-g5509547") {
388 ✗ printf("Installing new bootloader\n");
389 ✗ exec("/opt/fpp/bin.bbb/bootloader/install.sh");
390 ✗ setRawSetting("rebootFlag", "1");
391 }
392 ✗ } else {
393 // its a pocketbeagle
394 // bug in kernel/device tree on pcoketbeagle where in P2-36 is not
395 // properly set to be AIN so sensors won't read properly
396 ✗ exec("/usr/sbin/i2cset -y -f 0 0x24 9 5");
397 }
398
399 // Beagle LEDS
400 ✗ std::string pled;
401 ✗ if (getRawSetting("BBBLedPWR", pled) && !pled.empty()) {
402 ✗ if (pled == "0") {
403 ✗ pled = "0x00";
404 } else {
405 ✗ pled = "0x38";
406 }
407 ✗ exec("/usr/sbin/i2cset -f -y 0 0x24 0x0b 0x6e");
408 ✗ exec("/usr/sbin/i2cset -f -y 0 0x24 0x13 " + pled);
409 ✗ exec("/usr/sbin/i2cset -f -y 0 0x24 0x0b 0x6e");
410 ✗ exec("/usr/sbin/i2cset -f -y 0 0x24 0x13 " + pled);
411 }
412 #endif
413 #ifdef PLATFORM_BB64
414
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 checkPB2EEPROM();
415 #endif
416 #if defined(PLATFORM_BBB) || defined(PLATFORM_BB64)
417 3 std::string led;
418
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 int offset = FileExists("/sys/class/leds/beaglebone:green:usr0/trigger") ? 0 : 1;
419
6/18
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 3 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✓ Branch 8 taken 3 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 3 times.
✗ Branch 11 not taken.
✗ Branch 12 not taken.
✓ Branch 13 taken 3 times.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
9 if (getRawSetting("BBBLeds0", led) && !led.empty()) {
420 ✗ PutFileContents("/sys/class/leds/beaglebone:green:usr" + std::to_string(offset) + "/trigger", mapBBBLedValue(led));
421 } else {
422
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.
9 PutFileContents("/sys/class/leds/beaglebone:green:usr" + std::to_string(offset) + "/trigger", "heartbeat");
423 }
424
6/18
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 3 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✓ Branch 8 taken 3 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 3 times.
✗ Branch 11 not taken.
✗ Branch 12 not taken.
✓ Branch 13 taken 3 times.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
9 if (getRawSetting("BBBLeds1", led) && !led.empty()) {
425 ✗ PutFileContents("/sys/class/leds/beaglebone:green:usr" + std::to_string(1 + offset) + "/trigger", mapBBBLedValue(led));
426 } else {
427
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.
9 PutFileContents("/sys/class/leds/beaglebone:green:usr" + std::to_string(1 + offset) + "/trigger", "mmc0");
428 }
429
6/18
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 3 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✓ Branch 8 taken 3 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 3 times.
✗ Branch 11 not taken.
✗ Branch 12 not taken.
✓ Branch 13 taken 3 times.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
9 if (getRawSetting("BBBLeds2", led) && !led.empty()) {
430 ✗ PutFileContents("/sys/class/leds/beaglebone:green:usr" + std::to_string(2 + offset) + "/trigger", mapBBBLedValue(led));
431 } else {
432
5/10
✓ 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.
9 PutFileContents("/sys/class/leds/beaglebone:green:usr" + std::to_string(2 + offset) + "/trigger", mapBBBLedValue("cpu"));
433 }
434
6/18
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 3 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✓ Branch 8 taken 3 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 3 times.
✗ Branch 11 not taken.
✗ Branch 12 not taken.
✓ Branch 13 taken 3 times.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
9 if (getRawSetting("BBBLeds3", led) && !led.empty()) {
435 ✗ PutFileContents("/sys/class/leds/beaglebone:green:usr" + std::to_string(3 + offset) + "/trigger", led);
436 } else {
437
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.
9 PutFileContents("/sys/class/leds/beaglebone:green:usr" + std::to_string(3 + offset) + "/trigger", "mmc1");
438 }
439 #endif
440 6 }
441
442 // Fan thermal trip settings. The web UI (PrintFanThermalSettings in
443 // www/common.php) creates settings named FanTrip_<zone type stripped to
444 // alphanumerics>_<trip index> holding degrees C for thermal zones that have a
445 // fan cooling device bound to them (the Pi5 case/active-cooler fan, cape fans
446 // wired into a thermal zone via device tree). Trip points reset to their
447 // device tree defaults on every boot, so applyThermalSettings() runs during
448 // boot; it is also invoked directly ("fppinit applyThermal") when one of the
449 // settings is changed in the UI.
450 //
451 // The first pass each boot records the untouched device tree trip values into
452 // a snapshot in media/tmp (cleared on every boot) BEFORE applying any
453 // settings, so resetThermalSettings() ("fppinit resetThermal", the UI's
454 // "Reset to Defaults" button) can restore the hardware defaults without a
455 // reboot.
456 static const std::string FAN_TRIP_DEFAULTS_FILE = FPP_MEDIA_DIR + "/tmp/fan_thermal_defaults.json";
457 static const std::string FAN_PROBE_FILE = FPP_MEDIA_DIR + "/tmp/fan_probe.json";
458
459 8 static std::string fanTripKey(const std::string& ztype, int trip) {
460 8 std::string key;
461
2/2
✓ Branch 0 taken 88 times.
✓ Branch 1 taken 8 times.
96 for (char c : ztype) {
462
2/2
✓ Branch 0 taken 80 times.
✓ Branch 1 taken 8 times.
88 if (isalnum((unsigned char) c)) {
463
1/2
✓ Branch 0 taken 80 times.
✗ Branch 1 not taken.
80 key += c;
464 }
465 }
466
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 key += "_";
467
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 key += std::to_string(trip);
468 8 return key;
469 ✗ }
470
471 // Walk every "active" trip point of every thermal zone, calling
472 // cb(tripTempFile, "<sanitized zone type>_<trip index>"). Passive/critical
473 // trips belong to the kernel's throttle/shutdown logic and are never visited.
474 2 static void forEachActiveTripPoint(const std::function<void(const std::string&, const std::string&)>& cb) {
475
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 for (int zn = 0; zn < 32; zn++) {
476
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 std::string base = "/sys/class/thermal/thermal_zone" + std::to_string(zn);
477
4/6
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 2 times.
4 if (!FileExists(base + "/type")) {
478 2 break;
479 }
480
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 std::string ztype = GetFileContents(base + "/type");
481
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 TrimWhiteSpace(ztype);
482
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 for (int t = 0; t < 16; t++) {
483
3/6
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 12 times.
✗ Branch 5 not taken.
12 std::string tripFile = base + "/trip_point_" + std::to_string(t) + "_temp";
484
3/4
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 10 times.
12 if (!FileExists(tripFile)) {
485 2 break;
486 }
487
4/8
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 10 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 10 times.
✗ Branch 7 not taken.
10 std::string tripType = GetFileContents(base + "/trip_point_" + std::to_string(t) + "_type");
488
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 TrimWhiteSpace(tripType);
489
3/4
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 8 times.
10 if (tripType != "active") {
490 2 continue;
491 }
492
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
8 cb(tripFile, fanTripKey(ztype, t));
493
5/5
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 8 times.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 2 times.
14 }
494
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
4 }
495 2 }
496
497 // captureDefaults must ONLY be set from the boot paths, where sysfs still holds
498 // the untouched device tree values. Capturing during an on-demand apply (the
499 // UI changing a setting) could record an already-customized temperature as the
500 // "default" if the snapshot had been lost, which would make a later reset
501 // restore the user's value instead of the hardware one.
502 26 void applyThermalSettings(bool captureDefaults) {
503
3/4
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 24 times.
✓ Branch 3 taken 2 times.
26 if (FileExists("/etc/fpp/container")) {
504 24 return;
505 }
506
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 Json::Value defaults;
507
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
2 if (FileExists(FAN_TRIP_DEFAULTS_FILE)) {
508 ✗ LoadJsonFromString(GetFileContents(FAN_TRIP_DEFAULTS_FILE), defaults, JsonRoot::Object);
509 }
510 2 bool defaultsChanged = false;
511
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 forEachActiveTripPoint([&](const std::string& tripFile, const std::string& key) {
512
4/8
✓ 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.
8 if (captureDefaults && !JsonHas(defaults, key)) {
513
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 std::string cur = GetFileContents(tripFile);
514
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 TrimWhiteSpace(cur);
515
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 if (!cur.empty()) {
516
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
8 defaults[key] = atoi(cur.c_str());
517 8 defaultsChanged = true;
518 }
519 8 }
520 // Read as a string and parse a float: the setting holds degrees C to one
521 // decimal place so that whole degrees F survive the conversion both ways
522 // (see PutSetting() in www/api/controllers/settings.php). getRawSettingInt()
523 // must not be used here - its hand-rolled digit loop does not stop at the
524 // decimal point and would turn "29.4" into 2884.
525 8 std::string tempStr;
526
3/6
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 8 times.
8 if (getRawSetting("FanTrip_" + key, tempStr)) {
527 ✗ double temp = strtod(tempStr.c_str(), nullptr);
528 ✗ if (temp > 0) {
529 ✗ printf("FPP - Setting fan trip point %s to %.1fC\n", key.c_str(), temp);
530 ✗ PutFileContents(tripFile, std::to_string((int) std::lround(temp * 1000.0)));
531 }
532 }
533 8 });
534
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (defaultsChanged) {
535
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 PutFileContents(FAN_TRIP_DEFAULTS_FILE, SaveJsonToString(defaults));
536 }
537 2 }
538
539 ✗ void resetThermalSettings() {
540 ✗ if (FileExists("/etc/fpp/container")) {
541 ✗ return;
542 }
543 // Remove all FanTrip_ settings (same rewrite approach as setRawSetting)
544 ✗ std::string content = GetFileContents(FPP_MEDIA_DIR + "/settings");
545 ✗ std::vector<std::string> lines = split(content, '\n');
546 ✗ content = "";
547 ✗ for (auto& line : lines) {
548 ✗ if (!line.empty() && !line.starts_with("FanTrip_")) {
549 ✗ content += line;
550 ✗ content += "\n";
551 }
552 }
553 ✗ PutFileContents(FPP_MEDIA_DIR + "/settings", content);
554
555 // Restore the device tree default temperatures captured at boot. If the
556 // snapshot is somehow gone (media/tmp cleared by hand mid-run), the
557 // settings removed above still restore the defaults on the next boot, so
558 // warn rather than fail.
559 ✗ Json::Value defaults;
560 ✗ if (FileExists(FAN_TRIP_DEFAULTS_FILE) && LoadJsonFromString(GetFileContents(FAN_TRIP_DEFAULTS_FILE), defaults, JsonRoot::Object)) {
561 ✗ forEachActiveTripPoint([&](const std::string& tripFile, const std::string& key) {
562 ✗ if (JsonHas(defaults, key)) {
563 ✗ printf("FPP - Restoring fan trip point %s to %dmC\n", key.c_str(), defaults[key].asInt());
564 ✗ PutFileContents(tripFile, std::to_string(defaults[key].asInt()));
565 }
566 ✗ });
567 } else {
568 ✗ printf("FPP - No fan trip point defaults snapshot; settings cleared, defaults restore on next boot\n");
569 }
570 ✗ }
571
572 // A pwm-fan hwmon node always exposes fanN_input (the tachometer) even when no
573 // fan -- or a dead fan -- is wired to it: it simply reads 0 RPM. That is
574 // indistinguishable from a healthy fan the thermal governor has merely turned
575 // off, so the reading is worse than useless (it looks like a stalled fan). To
576 // tell "off" from "absent/dead", force each fan on and read its tachometer.
577 //
578 // Lowering a trip point does NOT reliably wake the (poll-disabled) thermal
579 // governor on the Pi5/K16Pro kernels, and driving the cooling-device cur_state
580 // ramps only gradually, so drive the hwmon PWM directly (pwm1_enable=1 +
581 // pwm1=max). A healthy fan spins up to thousands of RPM within ~150ms; an
582 // absent/dead one stays 0. The pre-probe PWM state is restored afterwards (the
583 // governor reasserts control on the next thermal event).
584 //
585 // The result is written to media/tmp/fan_probe.json (wiped every boot), keyed by
586 // "<hwmon name>:<fan index>", so Sensors::DetectFanSensors() in fppd can suppress
587 // the RPM display for a fan whose tachometer can neither be controlled nor read.
588 // This runs during the boot "start" phase, before fppd caches its sensor list.
589 3 void probeFanPresence() {
590
3/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 2 times.
3 if (FileExists("/etc/fpp/container")) {
591 1 return;
592 }
593
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 Json::Value result;
594
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 for (int h = 0; h < 32; h++) {
595
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 std::string hwmon = "/sys/class/hwmon/hwmon" + std::to_string(h);
596
3/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 4 times.
6 if (!DirectoryExists(hwmon)) {
597 2 break;
598 }
599 4 std::vector<int> fans;
600
2/2
✓ Branch 0 taken 32 times.
✓ Branch 1 taken 4 times.
36 for (int f = 1; f <= 8; f++) {
601
6/10
✓ Branch 0 taken 32 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 32 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 32 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 32 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2 times.
✓ Branch 9 taken 30 times.
32 if (FileExists(hwmon + "/fan" + std::to_string(f) + "_input")) {
602
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 fans.push_back(f);
603 }
604 }
605
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
4 if (fans.empty()) {
606 2 continue;
607 }
608
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 std::string name = GetFileContents(hwmon + "/name");
609
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 TrimWhiteSpace(name);
610
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (name.empty()) {
611 ✗ name = "hwmon" + std::to_string(h);
612 }
613
614 // Force the fan on via the hwmon PWM interface, remembering the current
615 // state so it can be restored. pwm1 drives every fan on a pwm-fan node.
616
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 std::string pwmFile = hwmon + "/pwm1";
617
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 std::string enFile = hwmon + "/pwm1_enable";
618
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 bool forced = FileExists(pwmFile);
619 2 std::string origPwm, origEn;
620
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (forced) {
621
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 origPwm = GetFileContents(pwmFile);
622
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 TrimWhiteSpace(origPwm);
623
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 if (FileExists(enFile)) {
624
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 origEn = GetFileContents(enFile);
625
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 TrimWhiteSpace(origEn);
626
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
4 PutFileContents(enFile, "1");
627 }
628
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
4 PutFileContents(pwmFile, "255");
629 }
630
631 // Sample each tachometer until it reads non-zero (healthy fans spin up
632 // within ~150ms); cap at ~0.7s so an absent/dead fan can't stall boot.
633
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
4 std::vector<int> rpm(fans.size(), 0);
634
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 1 times.
9 for (int s = 0; s < 7; s++) {
635
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 std::this_thread::sleep_for(std::chrono::milliseconds(100));
636 8 bool anyZero = false;
637
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 8 times.
16 for (size_t fi = 0; fi < fans.size(); fi++) {
638
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 if (rpm[fi] == 0) {
639
4/8
✓ 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.
8 std::string r = GetFileContents(hwmon + "/fan" + std::to_string(fans[fi]) + "_input");
640
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 TrimWhiteSpace(r);
641 8 rpm[fi] = atoi(r.c_str());
642
2/2
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 1 times.
8 if (rpm[fi] == 0) {
643 7 anyZero = true;
644 }
645 8 }
646 }
647
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 7 times.
8 if (!anyZero) {
648 1 break;
649 }
650 }
651
652 // Restore the pre-probe PWM state.
653
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (forced) {
654
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 PutFileContents(pwmFile, origPwm);
655
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (!origEn.empty()) {
656
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 PutFileContents(enFile, origEn);
657 }
658 }
659
660
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
4 for (size_t fi = 0; fi < fans.size(); fi++) {
661
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 std::string key = name + ":" + std::to_string(fans[fi]);
662
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 Json::Value e;
663
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 e["rpm"] = rpm[fi];
664
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 e["present"] = rpm[fi] > 0;
665
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 result[key] = e;
666
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
4 printf("FPP - Fan probe %s: %d RPM (%s)\n", key.c_str(), rpm[fi],
667
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
2 rpm[fi] > 0 ? "present" : "no tach signal, RPM display suppressed");
668 2 }
669
5/5
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 2 times.
8 }
670
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 if (result.size() > 0) {
671
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 PutFileContents(FAN_PROBE_FILE, SaveJsonToString(result));
672 }
673 2 }
674
675 26 void setFileOwnership() {
676
2/4
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 26 times.
✗ Branch 3 not taken.
26 exec("/usr/bin/chown -R fpp:fpp " + FPP_MEDIA_DIR);
677 26 }
678
679 3 bool checkUnpartitionedSpace() {
680 3 bool ret = false;
681
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 if (!FileExists("/etc/fpp/desktop")) {
682
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 std::string sourceDev = execAndReturn("/usr/bin/findmnt -n -o SOURCE " + FPP_MEDIA_DIR);
683
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 TrimWhiteSpace(sourceDev);
684
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (sourceDev.empty()) {
685 ✗ sourceDev = execAndReturn("/usr/bin/findmnt -n -o SOURCE /");
686 ✗ TrimWhiteSpace(sourceDev);
687 }
688
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 if (!sourceDev.empty()) {
689
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 sourceDev = sourceDev.substr(5);
690 3 std::string osd;
691
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 getRawSetting("storageDevice", osd);
692
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 if (osd != sourceDev) {
693
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
6 setRawSetting("storageDevice", osd);
694 }
695 3 }
696
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 std::string fs = "0";
697
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 3 times.
3 if (FileExists(SD_CARD_DEVICE)) {
698 ✗ fs = execAndReturn("/usr/sbin/sfdisk -F " + SD_CARD_DEVICE + " | tail -n 1");
699 ✗ TrimWhiteSpace(fs);
700 ✗ auto splits = split(fs, ' ');
701 ✗ fs = splits.back();
702 ✗ if (endsWith(fs, "G")) {
703 ✗ fs = fs.substr(0, fs.size() - 1);
704 } else {
705 ✗ fs = "0";
706 }
707 ✗ }
708
5/10
✓ 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 not taken.
✓ Branch 7 taken 3 times.
✗ Branch 8 not taken.
✓ Branch 9 taken 3 times.
3 if (FileExists("/boot/firmware/fpp_expand_rootfs") || FileExists("/boot/fpp_expand_rootfs")) {
709 ✗ fs = "0";
710 ✗ std::string rootPart = execAndReturn("/usr/bin/findmnt -n -o SOURCE /");
711 ✗ TrimWhiteSpace(rootPart);
712 ✗ if (startsWith(rootPart, SD_CARD_DEVICE)) {
713 ✗ std::string lastPartNum = execAndReturn("/usr/sbin/parted " + SD_CARD_DEVICE + " -ms unit s p | tail -n 1 | cut -f 1 -d:");
714 ✗ TrimWhiteSpace(lastPartNum);
715 ✗ std::string startPos = execAndReturn("/usr/sbin/parted " + SD_CARD_DEVICE + " -ms unit s p | grep \"^" + lastPartNum + "\" | cut -f 2 -d: | sed 's/[^0-9]//g'");
716 ✗ TrimWhiteSpace(startPos);
717 ✗ std::string fdiskInstructions = "p\nd\n" + lastPartNum + "\nn\np\n" + lastPartNum + "\n" + startPos + "\n\np\nw\n";
718 ✗ PutFileContents("/tmp/fdisk.cmds", fdiskInstructions);
719 ✗ exec("/usr/sbin/fdisk " + SD_CARD_DEVICE + " < /tmp/fdisk.cmds");
720 ✗ unlink("/tmp/fdisk.cmds");
721 ✗ exec("systemctl enable fpp-expand-rootfs.service");
722 ✗ setRawSetting("rebootFlag", "1");
723 ✗ ret = true;
724 ✗ }
725 ✗ unlink("/boot/firmware/fpp_expand_rootfs");
726 ✗ unlink("/boot/fpp_expand_rootfs");
727 ✗ }
728 3 std::string oldfs;
729
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 getRawSetting("UnpartitionedSpace", oldfs);
730
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 if (oldfs != fs) {
731
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
6 setRawSetting("UnpartitionedSpace", fs);
732 }
733 3 }
734 3 return ret;
735 }
736 ✗ void resizeRootFS() {
737 ✗ std::string rootPart = execAndReturn("/usr/bin/findmnt -n -o SOURCE /");
738 ✗ TrimWhiteSpace(rootPart);
739 ✗ exec("/usr/sbin/resize2fs " + rootPart);
740 // --no-reload is important: a bare "systemctl disable" performs an implicit
741 // daemon-reload, which on a single-core SBC (e.g. BeagleBone) takes ~11s and
742 // freezes systemd's entire job queue mid-boot -- stalling dbus, networkd, and
743 // fppinit. The removed symlink takes effect next boot regardless, so there's
744 // no reason to reload now.
745 ✗ exec("systemctl disable --no-reload fpp-expand-rootfs.service");
746 ✗ }
747 27 void setupTimezone() {
748 27 std::string s;
749
2/4
✓ Branch 0 taken 27 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 27 times.
✗ Branch 3 not taken.
27 getRawSetting("TimeZone", s);
750
1/2
✓ Branch 0 taken 27 times.
✗ Branch 1 not taken.
27 TrimWhiteSpace(s);
751
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 26 times.
27 if (!s.empty()) {
752 // Debian 13 (trixie) no longer ships /etc/timezone -- the timezone is
753 // tracked solely by the /etc/localtime symlink. Reading the old file
754 // therefore always returned empty, so this used to run the expensive
755 // `timedatectl set-timezone` (a dbus round-trip to systemd-timedated
756 // that blocks ~15s on a single-core SBC) plus a backgrounded
757 // `dpkg-reconfigure tzdata` on EVERY boot, and twice (fppinit start +
758 // postNetwork). Derive the current zone by reading the /etc/localtime
759 // symlink target (a single syscall, no fork) and only reconfigure when
760 // it actually differs. The target looks like
761 // ".../usr/share/zoneinfo/America/New_York"; the zone is everything
762 // after "zoneinfo/".
763 1 std::string c;
764 1 std::error_code ec;
765
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 std::filesystem::path tzTarget = std::filesystem::read_symlink("/etc/localtime", ec);
766
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (!ec) {
767
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 std::string t = tzTarget.string();
768 1 size_t pos = t.find("zoneinfo/");
769
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (pos != std::string::npos) {
770
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 c = t.substr(pos + 9); // strlen("zoneinfo/")
771 }
772 1 }
773
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 TrimWhiteSpace(c);
774
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
1 if (!isShellSafeWord(s)) {
775 ✗ printf("Ignoring invalid TimeZone setting\n");
776
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 } else if (c != s) {
777
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 printf("Resetting timezone from %s to %s\n", c.c_str(), s.c_str());
778
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 exec("/usr/bin/timedatectl set-timezone " + s);
779
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 execbg("/usr/sbin/dpkg-reconfigure -f noninteractive tzdata &");
780 }
781 1 }
782 27 }
783
784 23 void cleanupChromiumFiles() {
785
2/4
✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 23 times.
✗ Branch 3 not taken.
23 exec("/usr/bin/rm -rf /home/fpp/.config/chromium/Singleton* 2>/dev/null > /dev/null");
786 23 }
787
788 3 void setupKiosk(bool force) {
789
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 int km = getRawSettingInt("Kiosk", 0);
790
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 3 times.
3 if (km || force) {
791 ✗ std::string url = "http://localhost/";
792 ✗ getRawSetting("KioskUrl", url);
793
794 // FPP UI doesn't delete the setting, it makes it ""
795 ✗ if (url == "\"\"") {
796 ✗ url = "http://localhost/";
797 }
798
799 ✗ std::string value = "{\"RestoreOnStartup\": 4,\"RestoreOnStartupURLs\": [\"" + url + "\"]}";
800 ✗ mkdir("/etc/chromium/", 0775);
801 ✗ mkdir("/etc/chromium/policies", 0775);
802 ✗ mkdir("/etc/chromium/policies/managed", 0775);
803 ✗ PutFileContents("/etc/chromium/policies/managed/policy.json", value);
804 ✗ }
805 3 }
806 3 void checkInstallKiosk() {
807
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 int km = getRawSettingInt("Kiosk", 0);
808
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (FileExists("/fpp_kiosk")) {
809 ✗ km = true;
810 }
811
812
2/6
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 3 times.
3 if (km && !FileExists("/etc/fpp/kiosk")) {
813 ✗ std::string s = execAndReturn("/usr/bin/systemctl is-enabled fpp-install-kiosk");
814 ✗ TrimWhiteSpace(s);
815 ✗ if (s == "disabled") {
816 ✗ exec("/usr/bin/systemctl enable fpp-install-kiosk");
817 ✗ exec("/usr/sbin/reboot");
818 }
819 ✗ }
820 3 }
821 ✗ void installKiosk() {
822 ✗ int km = getRawSettingInt("Kiosk", 0);
823 ✗ if (FileExists("/fpp_kiosk")) {
824 ✗ unlink("/fpp_kiosk");
825 ✗ km = true;
826 }
827 ✗ if (km && !FileExists("/etc/fpp/kiosk")) {
828 // need to re-install kiosk mode
829 ✗ exec("/opt/fpp/SD/FPP_Kiosk.sh");
830 ✗ if (FileExists("/etc/fpp/kiosk")) {
831 ✗ exec("/usr/sbin/reboot");
832 }
833 ✗ setupKiosk(true);
834 }
835 ✗ }
836
837 // Run "apt-get <args>" synchronously and return true only if it exited 0.
838 7 static bool runAptGet(const std::vector<std::string>& args) {
839 7 pid_t pid = fork();
840
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 14 times.
14 if (pid == -1) {
841 ✗ printf("Error: Failed to fork process\n");
842 ✗ return false;
843
2/2
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 7 times.
14 } else if (pid == 0) {
844 7 std::vector<char*> argv;
845
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 argv.push_back(const_cast<char*>("apt-get"));
846
2/2
✓ Branch 0 taken 45 times.
✓ Branch 1 taken 7 times.
52 for (const auto& a : args) {
847
1/2
✓ Branch 0 taken 45 times.
✗ Branch 1 not taken.
45 argv.push_back(const_cast<char*>(a.c_str()));
848 }
849
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 argv.push_back(nullptr);
850 7 execvp("apt-get", argv.data());
851 // If execvp fails, exit the child directly (don't fall back into the
852 // parent's control flow).
853
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
7 printf("Error: Failed to execute apt-get\n");
854 ✗ _exit(127);
855 ✗ }
856 7 int status = 0;
857
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 waitpid(pid, &status, 0);
858
2/4
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
7 return WIFEXITED(status) && WEXITSTATUS(status) == 0;
859 }
860
861 // A package name apt takes as one package and never as an option: Debian
862 // policy characters, 2-255 long, optional ":arch". The PHP side validates the
863 // same way before writing the list, but the file is hand-editable and
864 // restorable from a backup, and every name here becomes root apt-get argv --
865 // a leading '-' would be an apt option. Hand-rolled rather than std::regex so
866 // it cannot throw. Apt's other trap, an unanchored regex fallback for a name
867 // containing '.' that no longer exists, is closed by packageKnownToApt().
868 10 static bool validPackageName(const std::string& n) {
869
3/6
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 10 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 10 times.
10 if (n.size() < 2 || n.size() > 255) {
870 ✗ return false;
871 }
872 10 size_t colon = n.find(':');
873
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 std::string base = n.substr(0, colon);
874
6/8
✓ Branch 0 taken 61 times.
✓ Branch 1 taken 8 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 61 times.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 7 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 1 times.
69 auto lowerAlnum = [](char c) { return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'); };
875
6/10
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✓ Branch 3 taken 3 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 7 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✓ Branch 8 taken 3 times.
✓ Branch 9 taken 7 times.
10 if (base.size() < 2 || !lowerAlnum(base.front()) || (!lowerAlnum(base.back()) && base.back() != '+')) {
876 3 return false;
877 }
878
2/2
✓ Branch 0 taken 52 times.
✓ Branch 1 taken 7 times.
59 for (char c : base) {
879
6/10
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 47 times.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 5 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 5 times.
✗ Branch 8 not taken.
✓ Branch 9 taken 52 times.
52 if (!lowerAlnum(c) && c != '+' && c != '.' && c != '-') {
880 ✗ return false;
881 }
882 }
883
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 7 times.
7 if (colon != std::string::npos) {
884 ✗ std::string arch = n.substr(colon + 1);
885 ✗ if (arch.empty()) {
886 ✗ return false;
887 }
888 ✗ for (char c : arch) {
889 ✗ if (!lowerAlnum(c) && c != '-') {
890 ✗ return false;
891 }
892 }
893 ✗ }
894 7 return true;
895 10 }
896
897 // Whether apt's lists know this exact name. 'apt-get install' falls back to
898 // an unanchored regex for an unknown name containing '.', so a versioned
899 // name that vanished in the new release (libfoo1.2) could select something
900 // else entirely; 'apt-cache pkgnames' lists exact names only, one per line
901 // (--all-names so a virtual package name apt can install is not refused).
902 // The name has passed validPackageName() (no shell characters), so the
903 // usual execAndReturn() is fine here.
904 5 static bool packageKnownToApt(const std::string& name) {
905
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 std::string base = name.substr(0, name.find(':'));
906 5 std::string out;
907 try {
908
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 out = execAndReturn("apt-cache --all-names pkgnames " + base);
909 ✗ } catch (const std::exception&) {
910 ✗ return true; // can't check: let apt decide, as before
911 ✗ }
912
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 return ("\n" + out).find("\n" + base + "\n") != std::string::npos;
913 5 }
914
915 // Extract a package name from a userpackages.json entry. Supports both the
916 // legacy schema (a bare string) and the ownership schema
917 // ({"package": "name", "requestedBy": [...], "via": [...]} written by the PHP
918 // package helpers). Returns "" when the entry carries no usable package name,
919 // and for an entry with a non-empty "via": that is a dependency some other
920 // entry's install pulled in, and its parent's install re-pulls it under
921 // whatever the new OS calls it -- replaying the recorded (often versioned)
922 // name would fail on every boot once the release renames it.
923 12 static std::string packageNameFromJson(const Json::Value& item) {
924
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 6 times.
12 if (item.isString()) {
925 6 return item.asString();
926 }
927
9/22
✓ 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.
✓ Branch 6 taken 6 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 6 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 6 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 6 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 6 times.
✗ Branch 15 not taken.
✓ Branch 16 taken 6 times.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
✗ Branch 20 not taken.
✗ Branch 21 not taken.
18 if (JsonHas(item, "package") && item["package"].isString()) {
928
14/28
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 4 times.
✓ 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 6 times.
✗ Branch 19 not taken.
✓ Branch 20 taken 6 times.
✗ Branch 21 not taken.
✓ Branch 22 taken 2 times.
✓ Branch 23 taken 4 times.
✗ Branch 24 not taken.
✗ Branch 25 not taken.
✗ Branch 26 not taken.
✗ Branch 27 not taken.
18 if (JsonHas(item, "via") && item["via"].isArray() && !item["via"].empty()) {
929
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
4 return "";
930 }
931 4 return item["package"].asString();
932 }
933 ✗ return "";
934 }
935
936 // Reads the user package list into $out: size-capped so a runaway file
937 // cannot exhaust a small board's memory before it is even parsed, every name
938 // validated, duplicates dropped. Returns false when the file is missing or
939 // unusable -- nothing to replay, and nothing a retry would fix. jsoncpp's
940 // parser throws on input nested past its stack limit; the caller catches.
941 14 static bool readUserPackages(const std::string& filePath, std::vector<std::string>& out) {
942 14 std::error_code ec;
943
1/2
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
14 auto size = std::filesystem::file_size(filePath, ec);
944
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 8 times.
14 if (ec) {
945
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 if (ec == std::errc::no_such_file_or_directory) {
946
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 printf("No user package list at %s, nothing to install\n", filePath.c_str());
947 } else {
948 ✗ printf("Error: user package list %s: %s; skipping\n", filePath.c_str(), ec.message().c_str());
949 }
950 6 return false;
951 }
952
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 7 times.
8 if (size > 4 * 1024 * 1024) {
953
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 printf("Error: user package list is %ju bytes; refusing to process it\n", (uintmax_t)size);
954 1 return false;
955 }
956
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 Json::Value root;
957
5/6
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 3 times.
9 if (!LoadJsonFromString(GetFileContents(filePath), root, JsonRoot::Array)) {
958 // Malformed or wrong-shaped; won't fix itself on retry. The loader has
959 // already said what was wrong with it.
960
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 printf("Error: user package list %s is not usable; skipping\n", filePath.c_str());
961 2 return false;
962 }
963 // A real list is tens of entries. Beyond this the de-dup below and the
964 // one-apt-call-per-name loop after it are minutes of boot.
965
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 3 times.
3 if (root.size() > 1000) {
966 ✗ printf("Error: user package list has %u entries; refusing to process it\n", root.size());
967 ✗ return false;
968 }
969 3 int rejected = 0;
970 3 std::string firstRejected;
971
7/12
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 12 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 12 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 15 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 12 times.
✓ Branch 11 taken 3 times.
15 for (const auto& item : root) {
972
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 std::string pkg = packageNameFromJson(item);
973
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 10 times.
12 if (pkg.empty()) {
974 2 continue;
975 }
976
3/4
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✓ Branch 3 taken 7 times.
10 if (!validPackageName(pkg)) {
977
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 times.
3 if (rejected++ == 0) {
978 // Printable ASCII only, truncated: this goes to the boot log.
979
3/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 46 times.
✓ Branch 3 taken 2 times.
48 for (char c : pkg.substr(0, 64)) {
980
3/6
✓ Branch 0 taken 46 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 46 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 46 times.
✗ Branch 5 not taken.
46 firstRejected += (c >= 0x20 && c < 0x7f) ? c : '?';
981 2 }
982 }
983 3 continue;
984 3 }
985 7 bool seen = false;
986
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 5 times.
11 for (const auto& have : out) {
987
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 4 times.
6 if (have == pkg) {
988 2 seen = true;
989 2 break;
990 }
991 }
992
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 2 times.
7 if (!seen) {
993
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 out.push_back(pkg);
994 }
995
2/2
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 5 times.
12 }
996
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 times.
3 if (rejected) {
997
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 printf("Skipping %d invalid package name(s) in user package list (first: '%s')\n",
998 rejected, firstRejected.c_str());
999 }
1000 3 return true;
1001 7 }
1002
1003 // Returns true if there is nothing to do or every package installed; false only
1004 // when an apt failure occurred that is worth retrying on the next boot. The
1005 // DPkg::Lock::Timeout option lets apt wait for a concurrent install at boot
1006 // instead of failing immediately.
1007 14 bool installPackagesFromJson(const std::string& filePath) {
1008 14 std::vector<std::string> packages;
1009 try {
1010
4/4
✓ Branch 0 taken 12 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 9 times.
✓ Branch 3 taken 3 times.
14 if (!readUserPackages(filePath, packages)) {
1011 9 return true;
1012 }
1013
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 } catch (const std::exception& e) {
1014 // jsoncpp throws Json::RuntimeError on a file nested deeper than its
1015 // stack limit (a 1001-character "[[[[" file). This is fppinit: a bad
1016 // list must never take boot down.
1017
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 printf("Error: user package list could not be processed (%s); skipping\n", e.what());
1018 2 return true;
1019 2 }
1020
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (packages.empty()) {
1021 ✗ return true;
1022 }
1023
1024 // Non-Debian platforms (Fedora, MacOS, ...) have no apt-get; there is
1025 // nothing we can replay. Consume the trigger rather than retrying forever.
1026
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 3 times.
3 if (!FileExists("/usr/bin/apt-get")) {
1027 ✗ printf("apt-get not available on this platform; skipping user package install\n");
1028 ✗ return true;
1029 }
1030
1031 // Refresh the package lists first. A freshly flashed OS may ship stale/empty
1032 // lists, and this can run at boot before the network is fully up, so retry a
1033 // few times before giving up.
1034 3 bool updated = false;
1035
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 for (int i = 1; i <= 3; i++) {
1036
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 if (runAptGet({ "-o", "DPkg::Lock::Timeout=60", "update" })) {
1037 3 updated = true;
1038 3 break;
1039 }
1040 ✗ printf("Warning: apt-get update failed (attempt %d), retrying...\n", i);
1041 ✗ sleep(5);
1042 }
1043
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (!updated) {
1044 ✗ printf("Error: apt-get update never succeeded - will retry user packages on next boot\n");
1045 ✗ return false;
1046 }
1047
1048 3 bool allOk = true;
1049
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 3 times.
8 for (const auto& pkg : packages) {
1050
3/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 4 times.
5 if (!packageKnownToApt(pkg)) {
1051 // Renamed or dropped in the new release. Not retryable, and not
1052 // something to hand to apt's regex fallback.
1053
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 printf("Warning: no package named %s in the new OS; skipping\n", pkg.c_str());
1054 1 continue;
1055 }
1056
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 printf("Installing: %s\n", pkg.c_str());
1057 // confdef/confold: with no stdin, dpkg's conffile prompt (a config
1058 // file the old OS left behind) would fail the install and leave the
1059 // package half-configured for every later apt run.
1060
5/10
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 36 times.
✓ Branch 5 taken 4 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 4 times.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
48 if (!runAptGet({ "-o", "DPkg::Lock::Timeout=60", "-o", "Dpkg::Options::=--force-confdef", "-o", "Dpkg::Options::=--force-confold", "install", "-y", pkg })) {
1061 ✗ printf("Warning: Package installation failed for %s\n", pkg.c_str());
1062 ✗ allOk = false;
1063 }
1064 }
1065 3 return allOk;
1066
9/22
✓ 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 4 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 4 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 4 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 4 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 4 times.
✗ Branch 15 not taken.
✓ Branch 16 taken 4 times.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
✗ Branch 20 not taken.
✗ Branch 21 not taken.
22 }
1067
1068 // A plugin is considered "installed" if its directory contains a pluginInfo.json
1069 // manifest (same rule the web layer's GetInstalledPlugins() uses). Used to decide
1070 // whether the post-FPPOS-upgrade "plugins must be reinstalled" warning applies.
1071 14 static bool anyPluginsInstalled() {
1072
1/2
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
14 std::string dir = FPP_MEDIA_DIR + "/plugins";
1073
2/4
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 14 times.
14 if (!DirectoryExists(dir)) {
1074 ✗ return false;
1075 }
1076
4/8
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
✓ Branch 7 taken 12 times.
14 for (const auto& entry : std::filesystem::directory_iterator(dir)) {
1077
9/22
✓ 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 not taken.
✗ Branch 19 not taken.
✗ Branch 20 not taken.
✗ Branch 21 not taken.
2 if (entry.is_directory() && FileExists(entry.path().string() + "/pluginInfo.json")) {
1078 2 return true;
1079 }
1080
6/6
✓ Branch 0 taken 12 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 12 times.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 12 times.
✓ Branch 5 taken 2 times.
18 }
1081 12 return false;
1082 14 }
1083
1084 // FPP 10 changed the default MultiSync send method from Multicast to
1085 // Unicast-to-known-remotes for fresh installs (see www/settings.json's
1086 // MultiSyncUnicast "default": 1). An FPPOS reflash replaces /opt/fpp
1087 // (settings.json included) but preserves /home/fpp/media/settings from the
1088 // prior install untouched -- so an existing multisync user who never
1089 // explicitly recorded a send method (multicast was implicit/default before
1090 // unicast existed) would otherwise silently flip to unicast the moment this
1091 // new code boots. Pin them to their historical multicast behavior instead.
1092 // (Fresh installs and normal in-place `fpp upgrade`s are handled separately,
1093 // by upgrade/122/upgrade.sh via the version-gated upgrade_config path.)
1094 14 static void migrateMultiSyncDefaultToMulticast() {
1095
4/6
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 10 times.
✓ Branch 5 taken 4 times.
28 if (getRawSettingInt("MultiSyncEnabled", 0) != 1) {
1096 10 return;
1097 }
1098
1099 // No send method recorded at all -> the user was relying on the pre-FPP-10
1100 // implicit multicast behavior. Make it explicit.
1101
4/10
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 3 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
12 if (getRawSettingInt("MultiSyncMulticast", 0) != 1 &&
1102
10/18
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 3 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2 times.
✓ Branch 9 taken 1 times.
✓ Branch 10 taken 3 times.
✓ Branch 11 taken 1 times.
✓ Branch 12 taken 4 times.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
20 getRawSettingInt("MultiSyncBroadcast", 0) != 1 &&
1103
10/16
✓ 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.
✓ Branch 6 taken 2 times.
✓ Branch 7 taken 2 times.
✓ Branch 8 taken 2 times.
✓ Branch 9 taken 2 times.
✓ Branch 10 taken 3 times.
✓ Branch 11 taken 1 times.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
8 getRawSettingInt("MultiSyncUnicast", 0) != 1) {
1104
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.
4 setRawSetting("MultiSyncMulticast", "1");
1105 }
1106
1107 // Pin MultiSyncUnicast off whenever the key is simply ABSENT, independent of
1108 // what the other two are set to. This must not be folded into the branch
1109 // above: a user who had explicitly chosen multicast or broadcast skips that
1110 // branch entirely, so the key stayed absent -- and an absent key is not
1111 // neutral. It resolves differently depending on who is asking:
1112 //
1113 // www/common.php IfSettingEqualPrint() -> settings.json "default": 1
1114 // src/settings.cpp LoadSettingsInfo() -> settings.json "default": 1
1115 // getRawSettingInt() (right here) -> 0, the file is read directly
1116 //
1117 // So an upgraded box would show "Send MultiSync to ALL KNOWN remotes via
1118 // Unicast" ticked in the UI *and* have fppd genuinely unicast to every
1119 // remote, on top of whatever method the user actually chose -- while this
1120 // migration, reading raw, saw nothing wrong. Writing the key makes all
1121 // three readers agree.
1122 //
1123 // Only absence is corrected: an explicit 0 or 1 is the user's own choice.
1124 4 std::string existing;
1125
4/6
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 2 times.
8 if (!getRawSetting("MultiSyncUnicast", existing)) {
1126
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.
8 setRawSetting("MultiSyncUnicast", "0");
1127 }
1128 4 }
1129
1130 // FPP 10 removed the "Remote ..." commands. Each took the remote host as its
1131 // first argument and forwarded the rest, unchanged, to the command named here
1132 // on that host; the same thing is now the plain command with Multisync turned
1133 // on and the host in multisyncHosts. Rewrites one stored command in place.
1134 6 static bool convertRemoteCommand(Json::Value& cmd) {
1135 static const std::map<std::string, std::string> REPLACEMENTS = {
1136 { "Remote Trigger Command Preset", "Trigger Command Preset" },
1137 { "Remote Trigger Command Preset Slot", "Trigger Command Preset Slot" },
1138 { "Remote Effect Start", "Effect Start" },
1139 { "Remote FSEQ Effect Start", "FSEQ Effect Start" },
1140 { "Remote Effect Stop", "Effect Stop" },
1141 { "Remote Playlist Start", "Start Playlist" },
1142 { "Remote Run Script", "Run Script" },
1143
6/12
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 5 times.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 7 times.
✓ Branch 7 taken 1 times.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
15 };
1144
10/20
✓ 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.
✓ Branch 6 taken 6 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 6 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 6 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 6 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 6 times.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✓ Branch 17 taken 6 times.
✗ Branch 18 not taken.
✓ Branch 19 taken 6 times.
6 if (!cmd["command"].isString() || !cmd["args"].isArray() || cmd["args"].empty()) {
1145 ✗ return false;
1146 }
1147
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.
6 auto it = REPLACEMENTS.find(cmd["command"].asString());
1148
6/12
✓ 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.
✓ Branch 6 taken 6 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 6 times.
✗ Branch 10 not taken.
✓ Branch 11 taken 6 times.
6 if (it == REPLACEMENTS.end() || !cmd["args"][0].isString()) {
1149 ✗ return false;
1150 }
1151 // An empty host would become "every host", and multisyncHosts is not
1152 // variable-substituted the way the old argument was; leave those for the
1153 // user to fix (the playlist validator reports them).
1154
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.
6 std::string host = cmd["args"][0].asString();
1155
6/6
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 4 times.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 4 times.
6 if (host.empty() || host.find('%') != std::string::npos) {
1156 2 return false;
1157 }
1158
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 Json::Value args(Json::arrayValue);
1159
4/6
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 8 times.
✓ Branch 5 taken 4 times.
12 for (Json::ArrayIndex i = 1; i < cmd["args"].size(); i++) {
1160
3/6
✓ 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.
8 args.append(cmd["args"][i]);
1161 }
1162
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 cmd["command"] = it->second;
1163
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 cmd["args"] = args;
1164
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 cmd["multisyncCommand"] = true;
1165
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 cmd["multisyncHosts"] = host;
1166 4 return true;
1167
7/18
✓ 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 not taken.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
7 }
1168
1169 // Walks any stored config -- playlist, schedule, command presets, GPIO inputs
1170 // -- converting every object shaped like a command.
1171 56 static int convertRemoteCommands(Json::Value& v) {
1172 56 int count = 0;
1173
2/2
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 47 times.
56 if (v.isObject()) {
1174
6/6
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 4 times.
✓ Branch 5 taken 5 times.
9 if (v.isMember("command") && convertRemoteCommand(v)) {
1175 4 count++;
1176 }
1177
3/4
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 34 times.
✓ Branch 3 taken 9 times.
43 for (const auto& name : v.getMemberNames()) {
1178
2/4
✓ Branch 0 taken 34 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 34 times.
✗ Branch 3 not taken.
34 count += convertRemoteCommands(v[name]);
1179 9 }
1180
2/2
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 37 times.
47 } else if (v.isArray()) {
1181
6/10
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 18 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 28 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 18 times.
✓ Branch 9 taken 10 times.
28 for (auto& e : v) {
1182
2/4
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 18 times.
✗ Branch 3 not taken.
18 count += convertRemoteCommands(e);
1183 }
1184 }
1185 56 return count;
1186 }
1187
1188 // "master" mode was folded into Player in FPP 5.0 (sending sync packets became
1189 // the MultiSyncEnabled setting). The loaders in settings.cpp and config.php still
1190 // map a stored fppMode=master to player in memory, but never write it back, so the
1191 // settings file kept the old value indefinitely -- invisible to the UI and API
1192 // (both report the mapped value), but visible to backup/restore and anything
1193 // reading the file directly. Write the same mapping back once. Runs before
1194 // every fppd start; idempotent, since it only acts while fppMode is "master".
1195 3 void migrateLegacyFppMode() {
1196 3 std::string mode;
1197
11/20
✓ 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.
✓ Branch 6 taken 2 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1 times.
✓ Branch 9 taken 1 times.
✓ Branch 10 taken 3 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 3 times.
✗ Branch 13 not taken.
✓ 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.
9 if (getRawSetting("fppMode", mode) && mode == "master") {
1198
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 printf("FPP - Converting legacy fppMode=master to player with MultiSync enabled\n");
1199
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.
4 setRawSetting("fppMode", "player");
1200
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.
4 setRawSetting("MultiSyncEnabled", "1");
1201 }
1202 3 }
1203
1204 // Runs before every fppd start, so fppd never loads one of the removed
1205 // commands, whether the file came from an older install, a restore, or a copy
1206 // from another player. Idempotent: a file is only parsed if it mentions a
1207 // Remote command, and only rewritten if something was converted.
1208 3 void migrateRemoteCommands() {
1209 std::vector<std::string> files = {
1210 FPP_MEDIA_DIR + "/config/schedule.json",
1211 FPP_MEDIA_DIR + "/config/commandPresets.json",
1212 FPP_MEDIA_DIR + "/config/gpio.json",
1213
3/6
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✓ Branch 3 taken 3 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
15 };
1214
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 std::string playlistDir = FPP_MEDIA_DIR + "/playlists";
1215
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 if (DirectoryExists(playlistDir)) {
1216
5/8
✓ 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 not taken.
✓ Branch 6 taken 2 times.
✓ Branch 7 taken 3 times.
5 for (const auto& entry : std::filesystem::directory_iterator(playlistDir)) {
1217
8/20
✓ 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 not taken.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
2 if (entry.is_regular_file() && entry.path().extension() == ".json") {
1218
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 files.push_back(entry.path().string());
1219 }
1220 3 }
1221 }
1222
2/2
✓ Branch 0 taken 11 times.
✓ Branch 1 taken 3 times.
14 for (const auto& file : files) {
1223
3/4
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✓ Branch 3 taken 5 times.
11 if (!FileExists(file)) {
1224 7 continue;
1225 }
1226
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 std::string contents = GetFileContents(file);
1227
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 4 times.
5 if (contents.find("\"Remote ") == std::string::npos) {
1228 1 continue;
1229 }
1230 try {
1231
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 Json::Value root;
1232
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 4 times.
4 if (!LoadJsonFromString(contents, root)) {
1233 ✗ continue;
1234 }
1235
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 int count = convertRemoteCommands(root);
1236
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 if (count) {
1237
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 Json::StreamWriterBuilder wbuilder;
1238
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.
12 wbuilder["indentation"] = "\t";
1239 // Keep e.g. a 1.1 duration from coming back as 1.1000000000000001.
1240
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.
12 wbuilder["precision"] = 15;
1241
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 PutFileContents(file, Json::writeString(wbuilder, root));
1242
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 printf("FPP - Converted %d removed Remote command(s) to Multisync in %s\n", count, file.c_str());
1243 4 }
1244
1/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
4 } catch (const std::exception& e) {
1245 ✗ printf("FPP - Could not convert Remote commands in %s: %s\n", file.c_str(), e.what());
1246 ✗ }
1247
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 1 times.
5 }
1248
3/10
✓ 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 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
6 }
1249
1250 // Touched by a restore (www/backup.php, www/copystorage.php). It asks for
1251 // the part of a reflash's reconciliation a restore needs -- the restored
1252 // userpackages.json installed, the restored settings migrated -- and not the
1253 // rest: the OS and the plugins built for it are still this box's.
1254 static const char* FPP_RESTORED_MARKER = "/fpp_restored";
1255
1256 // Config-state migrations that must survive an FPPOS reflash, gated on the
1257 // same /fppos_upgraded marker as checkInstallPackages() (touched by
1258 // upgradeOS-part2.sh, which is always sourced from the target image being
1259 // flashed -- see the note in upgradeOS-part1.sh -- so this runs with current
1260 // code regardless of how old the box being upgraded was). Unlike
1261 // checkInstallPackages(), this does NOT unlink /fppos_upgraded itself: package
1262 // installs may still be retrying (e.g. no network yet), and each migration
1263 // here is written to be safely idempotent, so simply re-running on a later
1264 // boot is harmless. Add future OS-upgrade-surviving config migrations here.
1265 23 void checkConfigMigrations() {
1266
5/6
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 14 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 9 times.
✓ Branch 4 taken 14 times.
✓ Branch 5 taken 9 times.
23 if (FileExists("/fppos_upgraded") || FileExists(FPP_RESTORED_MARKER)) {
1267 14 migrateMultiSyncDefaultToMulticast();
1268 }
1269 23 }
1270
1271 // The URL a plugin checkout was cloned from ("remote.origin.url" in its
1272 // .git/config), or "" when it is not a git checkout.
1273 4 static std::string pluginOriginURL(const std::string& dir) {
1274
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 std::string config = GetFileContents(dir + "/.git/config");
1275 4 bool inOrigin = false;
1276
3/6
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 20 times.
✗ Branch 5 not taken.
20 for (auto line : split(config, '\n')) {
1277 20 size_t b = line.find_first_not_of(" \t");
1278
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 20 times.
20 if (b == std::string::npos) {
1279 ✗ continue;
1280 }
1281
1/2
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
20 line = line.substr(b);
1282
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 12 times.
20 if (line[0] == '[') {
1283
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 inOrigin = line.rfind("[remote \"origin\"]", 0) == 0;
1284
6/8
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 8 times.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 4 times.
✓ Branch 7 taken 8 times.
12 } else if (inOrigin && line.rfind("url", 0) == 0) {
1285 4 size_t eq = line.find('=');
1286
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 (eq != std::string::npos && line.find_first_not_of(" \t", 3) == eq) {
1287
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 std::string url = line.substr(eq + 1);
1288 4 size_t ub = url.find_first_not_of(" \t");
1289 4 size_t ue = url.find_last_not_of(" \t\r");
1290
3/10
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 4 times.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
4 return ub == std::string::npos ? "" : url.substr(ub, ue - ub + 1);
1291 4 }
1292 }
1293
3/5
✓ Branch 0 taken 16 times.
✓ Branch 1 taken 4 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
24 }
1294 ✗ return "";
1295 4 }
1296
1297 // A clone URL without what could carry a credential: the userinfo before the
1298 // host and any query or fragment, as PluginHistoryURL() does in PHP. A plugin
1299 // cloned while gitHubUser/gitHubPAT were set has the token in its origin URL
1300 // (InjectGitHubCredentials()), and the settings file is readable through the
1301 // API and carried by every backup.
1302 4 static std::string pluginURLWithoutCredentials(std::string url) {
1303
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 url = url.substr(0, url.find_first_of("?#"));
1304 4 size_t scheme = url.find("://");
1305
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 size_t hostStart = scheme == std::string::npos ? 0 : scheme + 3;
1306 4 size_t hostEnd = url.find('/', hostStart);
1307
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 size_t at = url.rfind('@', hostEnd == std::string::npos ? std::string::npos : hostEnd);
1308
3/4
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
4 if (at != std::string::npos && at >= hostStart) {
1309
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 url.erase(hostStart, at + 1 - hostStart);
1310 }
1311 8 return url;
1312 }
1313
1314 // IsOfficialPluginSrcURL(): published by the FPP project, judged on the host
1315 // and the owner, not on a prefix that any userinfo or port defeats.
1316 4 static bool isOfficialPluginURL(const std::string& url) {
1317 4 size_t scheme = url.find("://");
1318
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 if (scheme == std::string::npos) {
1319 ✗ return false;
1320 }
1321
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 std::string rest = toLowerCopy(url.substr(scheme + 3));
1322 4 size_t slash = rest.find('/');
1323
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 std::string host = rest.substr(0, std::min(slash, rest.find(':')));
1324
4/8
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 4 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 4 times.
4 if (host != "github.com" || slash == std::string::npos) {
1325 ✗ return false;
1326 }
1327 4 size_t owner = rest.find_first_not_of('/', slash);
1328
5/8
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 2 times.
✓ Branch 6 taken 2 times.
✗ Branch 7 not taken.
8 return owner != std::string::npos && rest.compare(owner, 15, "falconchristmas") == 0 &&
1329
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
8 (owner + 15 == rest.size() || rest[owner + 15] == '/');
1330 4 }
1331
1332 // "repoName|srcURL" records, comma-separated (the PluginUnverifiedRepos form,
1333 // see www/api/controllers/plugin.php), for every plugin installed in
1334 // pluginDir that the FPP project did not publish. A linkName alias is a
1335 // symlink to another plugin, not one itself.
1336 14 static std::string pluginSourceRecordsToVerify(const std::string& pluginDir) {
1337 14 std::string records;
1338
2/4
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 14 times.
14 if (!DirectoryExists(pluginDir)) {
1339 ✗ return records;
1340 }
1341 14 std::vector<std::string> names;
1342
5/8
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 4 times.
✓ Branch 7 taken 14 times.
18 for (const auto& entry : std::filesystem::directory_iterator(pluginDir)) {
1343
11/26
✓ 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 4 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 4 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 4 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 4 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 4 times.
✗ Branch 15 not taken.
✓ Branch 16 taken 4 times.
✗ Branch 17 not taken.
✓ Branch 18 taken 4 times.
✗ Branch 19 not taken.
✓ Branch 20 taken 4 times.
✗ Branch 21 not taken.
✗ Branch 22 not taken.
✗ Branch 23 not taken.
✗ Branch 24 not taken.
✗ Branch 25 not taken.
4 if (entry.is_directory() && !entry.is_symlink() && FileExists(entry.path().string() + "/pluginInfo.json")) {
1344
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 names.push_back(entry.path().filename().string());
1345 }
1346 14 }
1347
1/2
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
14 std::sort(names.begin(), names.end());
1348
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 14 times.
18 for (const auto& name : names) {
1349
4/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 4 times.
✗ Branch 7 not taken.
4 std::string url = pluginURLWithoutCredentials(pluginOriginURL(pluginDir + "/" + name));
1350 // Published by the FPP project, never flagged.
1351
3/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 2 times.
4 if (isOfficialPluginURL(url)) {
1352 2 continue;
1353 }
1354 // ',' and '|' separate records and fields, '"' would end the setting.
1355 4 auto clean = [](std::string v) {
1356
5/10
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 98 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 98 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 98 times.
102 v.erase(std::remove_if(v.begin(), v.end(), [](char c) { return c == ',' || c == '|' || c == '"'; }), v.end());
1357 4 return v;
1358 };
1359
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (!records.empty()) {
1360 ✗ records += ",";
1361 }
1362
7/14
✓ 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.
2 records += clean(name) + "|" + clean(url);
1363
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
4 }
1364 14 return records;
1365 14 }
1366
1367 // The "Unknown Plugins" health check flag (PluginUnknownEverInstalled) is
1368 // sticky because an unknown plugin's uninstall can't be trusted to undo what
1369 // its install did to the OS. A reflash replaces the OS, so start the record
1370 // over -- but the plugin clones in /home/fpp/media survive, and their scripts
1371 // still run. So rather than simply clearing it, park every surviving plugin
1372 // as unverified, keyed on the URL it was cloned from: the Plugin Manager
1373 // settles those against the plugin list (ResolveUnverifiedPlugins()) and
1374 // raises the flag again only for one that is not listed. Rebuilt from what
1375 // is on disk each time, so the retry of /fppos_upgraded on a later boot gives
1376 // the same answer.
1377 14 static void resetPluginSourceAfterOS() {
1378
3/6
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 14 times.
✗ Branch 5 not taken.
56 setRawSetting("PluginUnknownEverInstalled", "");
1379
3/6
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 14 times.
✗ Branch 5 not taken.
42 setRawSetting("PluginUnknownRepos", "");
1380
4/8
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 14 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 14 times.
✗ Branch 7 not taken.
42 setRawSetting("PluginUnverifiedRepos", pluginSourceRecordsToVerify(FPP_MEDIA_DIR + "/plugins"));
1381 14 }
1382
1383 23 void checkInstallPackages() {
1384 23 bool reflashed = FileExists("/fppos_upgraded");
1385
5/6
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 14 times.
✓ Branch 2 taken 9 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 9 times.
✓ Branch 5 taken 14 times.
23 if (!reflashed && !FileExists(FPP_RESTORED_MARKER)) {
1386 9 return;
1387 }
1388
1/2
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
14 if (reflashed) {
1389 // Only a reflash: a restore keeps this OS, so whatever an unknown
1390 // plugin's install did to it is still there, and the plugins on
1391 // disk were built for it (a File Copy that brings plugins in asks
1392 // for their reinstall itself, in copystorage.php).
1393 14 resetPluginSourceAfterOS();
1394 // An FPPOS reflash replaces /opt/fpp (new fppd, headers, plugin API
1395 // version) while /home/fpp/media -- including the plugin clones -- is
1396 // preserved, and any rootfs artifacts a plugin's fpp_install.sh dropped
1397 // are gone. Native (.so) plugins were built against the old headers;
1398 // the API version check only refuses them if every layout change was
1399 // paired with a bump, so fppd instead declines to load a native plugin
1400 // at all while it is on this list (PluginManager::
1401 // awaitingReinstallAfterOS). If plugins are present, set a persisted
1402 // flag so fppd can raise a (non-dismissible) warning prompting the user
1403 // to Reinstall All. The Plugin Manager turns "1" into the list of
1404 // plugins still to reinstall and clears it once every one has been
1405 // reinstalled or uninstalled. Set independent of the apt result below;
1406 // a retry next boot just rewrites the same value.
1407
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 12 times.
14 if (anyPluginsInstalled()) {
1408
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.
8 setRawSetting("pluginReinstallNeededAfterOS", "1");
1409 }
1410 }
1411
1412 14 printf("Installing User Packages\n");
1413 // Only consume the markers once the packages actually installed. If the
1414 // install can't complete (e.g. no network yet at boot), leave them in
1415 // place so it is retried on the next boot rather than silently never
1416 // reinstalling the user's packages.
1417
3/6
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 14 times.
✗ Branch 5 not taken.
14 if (installPackagesFromJson(FPP_MEDIA_DIR + "/config/userpackages.json")) {
1418 14 unlink("/fppos_upgraded");
1419 14 unlink(FPP_RESTORED_MARKER);
1420 } else {
1421 ✗ printf("User package install incomplete - leaving the upgrade/restore marker set to retry next boot\n");
1422 }
1423 }
1424 23 void startZRAMSwap() {
1425
2/4
✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 23 times.
✗ Branch 3 not taken.
23 if (!FileExists("/usr/sbin/zramswap")) {
1426 23 return;
1427 }
1428 // Gate on whether zram is already ACTIVE as swap, not on whether the device
1429 // node exists: kernels built with CONFIG_ZRAM=y (e.g. the BeagleBone) always
1430 // present /dev/zram0 even when nothing has configured it, so the old
1431 // FileExists("/dev/zram0") check made this a permanent no-op and zram swap
1432 // never came up. (On the Pi the systemd-zram-generator/rpi-swap path may
1433 // already have set it up, in which case /proc/swaps shows it and we skip.)
1434 ✗ std::string swaps = GetFileContents("/proc/swaps");
1435 ✗ if (swaps.find("zram") == std::string::npos) {
1436 ✗ execbg("/usr/sbin/zramswap start 2>/dev/null > /dev/null &");
1437 }
1438 ✗ }
1439 23 void startDiskSwap() {
1440 // The image ships a disk swap partition (e.g. mmcblk0p2) marked "noauto" in
1441 // fstab so systemd does NOT activate it during boot: swapping to eMMC/SD
1442 // wears the flash, and waiting for the swap device to be tagged by udev
1443 // would otherwise gate swap.target on the slow single-core coldplug. zram is
1444 // the primary swap; this larger disk swap is only useful for the occasional
1445 // heavy job (e.g. compiling FPP). Bring it up here, late in postNetwork, off
1446 // the boot critical path. swapon is harmless/idempotent if already active.
1447
2/4
✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 23 times.
✗ Branch 3 not taken.
23 exec("/usr/bin/awk '$3 == \"swap\" && $1 !~ /zram/ { print $1 }' /etc/fstab 2>/dev/null "
1448 "| while read -r dev; do /sbin/swapon \"$dev\" 2>/dev/null || true; done");
1449 23 }
1450
1451 #ifdef PLATFORM_PI
1452 // Shrink an oversized vc4-KMS CMA pool on boards that cannot afford it.
1453 //
1454 // The image ships "dtoverlay=vc4-kms-v3d,cma-256" for every Pi (FPP_Install.sh
1455 // adds it to cap the Pi4's 512MB firmware default -- issue #2679). But the
1456 // overlay's own README documents cma-256, and everything above cma-128, as
1457 // "needs 1GB", and the firmware does NOT clamp it: a 512MB Pi Zero 2 W really
1458 // comes up with CmaTotal 262144 kB. That is half the board handed to a GPU pool
1459 // on a headless controller, and it is enough to break a source rebuild -- it
1460 // leaves ~163MB usable, well under what cc1plus needs.
1461 //
1462 // So cap at cma-128, the largest size the overlay does not gate behind 1GB.
1463 // FPP's real DMA needs are far below that: the DPI envelope declared below is
1464 // 1920x997 RGB888, about 7.6MB.
1465 //
1466 // Only ever REDUCE: a deliberate smaller override (cma-64) is left alone, since
1467 // raising it would defeat the purpose. Rewrites `content` in place and sets
1468 // `changed` so the caller's existing reboot handles it; comparing before writing
1469 // keeps it idempotent, so it fires once and never loops.
1470 // MemTotal counts the CMA reservation, so it is the board's usable total, not
1471 // its marketing size: a 512MB board reports ~425-500MB, a 1GB board ~900MB+.
1472 // 700MB separates them with room on both sides.
1473 static constexpr long CMA_ONE_GB_BOARD_KB = 700000;
1474 static constexpr int CMA_MAX_UNDER_1GB = 128;
1475
1476 // Pure half: no file I/O, so it can be lifted out between the markers below and
1477 // exercised directly against sample config.txt text. Edits `content` in place
1478 // and returns true if it changed anything.
1479 // --- BEGIN capVC4CMALine ---
1480 15 static bool capVC4CMALine(std::string& content, long memKB) {
1481
3/4
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✓ Branch 3 taken 2 times.
15 if (memKB <= 0 || memKB >= CMA_ONE_GB_BOARD_KB) {
1482 13 return false;
1483 }
1484 // Match the overlay line itself, never a commented-out or indented copy.
1485 // Covers both the generic overlay and the -pi5 variant.
1486 2 size_t vidx = content.find("dtoverlay=vc4-kms-v3d");
1487
4/8
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 2 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 2 times.
2 while (vidx != std::string::npos && vidx != 0 && content[vidx - 1] != '\n') {
1488 ✗ vidx = content.find("dtoverlay=vc4-kms-v3d", vidx + 1);
1489 }
1490
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (vidx == std::string::npos) {
1491 ✗ return false;
1492 }
1493 2 size_t eol = content.find("\n", vidx);
1494
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (eol == std::string::npos) {
1495 ✗ eol = content.size();
1496 }
1497
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 std::string line = content.substr(vidx, eol - vidx);
1498
1499 2 size_t cidx = line.find("cma-");
1500
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (cidx == std::string::npos) {
1501 ✗ return false;
1502 }
1503 2 size_t dstart = cidx + strlen("cma-");
1504 2 size_t dend = dstart;
1505
5/6
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 6 times.
✓ Branch 5 taken 2 times.
8 while (dend < line.size() && isdigit(static_cast<unsigned char>(line[dend]))) {
1506 6 dend++;
1507 }
1508
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (dend == dstart) {
1509 // "cma-size" (a raw byte count) or some other spelling -- leave it be.
1510 ✗ return false;
1511 }
1512 2 int current = static_cast<int>(strtol(line.c_str() + dstart, nullptr, 10));
1513
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
2 if (current <= CMA_MAX_UNDER_1GB) {
1514 1 return false;
1515 }
1516
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 line.replace(cidx, dend - cidx, "cma-" + std::to_string(CMA_MAX_UNDER_1GB));
1517
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 printf("FPP - %ldkB board: capping vc4 CMA at %dMB (was %dMB)\n",
1518 memKB, CMA_MAX_UNDER_1GB, current);
1519
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 content.replace(vidx, eol - vidx, line);
1520 1 return true;
1521 2 }
1522 // --- END capVC4CMALine ---
1523
1524 15 static void capVC4CMAForBoard(std::string& content, bool& changed) {
1525
2/4
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✗ Branch 3 not taken.
15 std::string meminfo = GetFileContents("/proc/meminfo");
1526 15 size_t midx = meminfo.find("MemTotal:");
1527
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 15 times.
15 if (midx == std::string::npos) {
1528 ✗ return;
1529 }
1530 15 long memKB = strtol(meminfo.c_str() + midx + strlen("MemTotal:"), nullptr, 10);
1531
3/4
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 14 times.
15 if (capVC4CMALine(content, memKB)) {
1532
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 PutFileContents("/boot/firmware/config.txt", content);
1533 1 changed = true;
1534 }
1535
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 }
1536 #endif
1537
1538 #ifdef PLATFORM_PI
1539 // The Pi 5 has a built-in RTC, and it wins rtc0 by probe order -- it registers
1540 // around 0.35s where an i2c RTC on a cape is not up until ~2.5s. Whatever holds
1541 // rtc0 is what the kernel sets the system clock from (CONFIG_RTC_HCTOSYS_DEVICE
1542 // is "rtc0"), so on a board with no cell on the Pi's RTC header the clock is set
1543 // from an unset RTC early in boot and stays wrong until fpprtc runs seconds
1544 // later. A cape that ships a pi5 overlay variant renumbering itself to rtc0
1545 // fixes the ordering, but the Pi's own RTC is still there reading 1970, and a
1546 // user who knows there is no battery is better off with it gone entirely.
1547 //
1548 // "dtparam=rtc=off" is a stock firmware override (the rtc entry in the Pi 5
1549 // DTB's __overrides__ maps to the rpi_rtc node's status), so this needs no
1550 // overlay of FPP's own -- just the line, under a [pi5] filter so that an SD card
1551 // moved to an earlier Pi does not draw an "Unknown dtparam" warning.
1552 //
1553 // The block is rewritten from the setting, which is what makes removal work:
1554 // clear the setting and the block goes with it. Anything outside the markers is
1555 // left alone, and the block always closes with [all] so a later append does not
1556 // land inside the filter.
1557 //
1558 // It is rewritten IN PLACE, and created ahead of CapeUtils' cape-overlay-variant
1559 // block. That block is managed by stripping it and re-appending it at EOF, so if
1560 // this one were written the same way the two would leapfrog: each boot one lands
1561 // after the other, both see a changed config.txt, and both ask for a reboot.
1562 // Since this runs in fppinit's "start" action -- before networking comes up --
1563 // that loop is a box that never reaches the network to be fixed.
1564 static const std::string PI_RTC_BLOCK_BEGIN = "# FPP Pi RTC - BEGIN (managed by fppinit, do not edit)";
1565 static const std::string PI_RTC_BLOCK_END = "# FPP Pi RTC - END";
1566 // Only the marker text, matched not written: CapeUtils owns that block.
1567 static const std::string CAPE_VARIANT_BLOCK_BEGIN = "# FPP Cape Overlay Variants - BEGIN";
1568
1569 // Pure half: no file I/O, so it can be lifted out between the markers below and
1570 // exercised directly against sample config.txt text. Edits `content` in place
1571 // and returns true if it changed anything.
1572 // --- BEGIN applyDisablePiRTCBlock ---
1573 3 static bool applyDisablePiRTCBlock(std::string& content, bool disable) {
1574
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 const std::string orig = content;
1575 3 std::string desired;
1576
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 times.
3 if (disable) {
1577
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 desired = PI_RTC_BLOCK_BEGIN + "\n[pi5]\ndtparam=rtc=off\n[all]\n" + PI_RTC_BLOCK_END + "\n";
1578 }
1579
1580 // Strip every copy written before -- more than one only from an interrupted
1581 // write, and a truncated block (BEGIN with no END) cuts to end of file rather
1582 // than leaving a marker the next run would nest inside. Remember where the
1583 // first one started: the replacement goes back THERE, not at EOF. That is
1584 // load bearing, see the note above on leapfrogging the cape variant block.
1585 3 size_t at = std::string::npos;
1586 3 size_t begin = content.find(PI_RTC_BLOCK_BEGIN);
1587
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 3 times.
5 while (begin != std::string::npos) {
1588
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (at == std::string::npos) {
1589 2 at = begin;
1590 }
1591 2 size_t end = content.find(PI_RTC_BLOCK_END, begin);
1592
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
4 end = (end == std::string::npos) ? content.length()
1593 2 : end + PI_RTC_BLOCK_END.length();
1594
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 if (end < content.length() && content[end] == '\n') {
1595 2 ++end;
1596 }
1597
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 content.erase(begin, end - begin);
1598 2 begin = content.find(PI_RTC_BLOCK_BEGIN);
1599 }
1600
1601
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 times.
3 if (!desired.empty()) {
1602
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
2 if (at == std::string::npos) {
1603 // Creating it for the first time: go in AHEAD of the cape variant
1604 // block so that block keeps the end of the file to itself.
1605 1 at = content.find(CAPE_VARIANT_BLOCK_BEGIN);
1606
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (at == std::string::npos) {
1607 ✗ while (!content.empty() && content.back() == '\n') {
1608 ✗ content.pop_back();
1609 }
1610 ✗ content += "\n\n";
1611 ✗ at = content.length();
1612 } else {
1613
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 desired += "\n";
1614 }
1615 }
1616
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 content.insert(at, desired);
1617 }
1618 6 return content != orig;
1619 3 }
1620 // --- END applyDisablePiRTCBlock ---
1621 #endif
1622
1623 // Reconcile config.txt with the DisablePiRTC setting. Runs after DetectCape()
1624 // so a cape that ships DisablePiRTC in its defaultSettings takes effect on the
1625 // same boot it is first detected on -- that path reboots, since the setting only
1626 // means anything to the firmware. The UI toggle calls this via `fppinit
1627 // setupPiRTC` with rebootIfChanged false: the setting is declared "reboot": 1, so
1628 // the user is already being prompted and should not have the box go down under
1629 // the save.
1630 6 void setupPiRTCConfig(bool rebootIfChanged) {
1631 #ifdef PLATFORM_PI
1632
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 std::string content = GetFileContents("/boot/firmware/config.txt");
1633
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 3 times.
5 if (content.empty()) {
1634 2 return;
1635 }
1636
5/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 2 times.
✓ Branch 7 taken 1 times.
6 if (applyDisablePiRTCBlock(content, getRawSettingInt("DisablePiRTC", 0) != 0)) {
1637
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 PutFileContents("/boot/firmware/config.txt", content);
1638
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 printf("FPP - Pi RTC configuration changed in config.txt\n");
1639
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (rebootIfChanged) {
1640 ✗ printf("\n\nRebooting to load new settings.\n\n");
1641 ✗ exec("/usr/sbin/reboot");
1642 }
1643 }
1644 #endif
1645
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 2 times.
6 }
1646
1647 #ifdef PLATFORM_PI
1648 // Suppress the initial CEC Active Source at boot that wakes projectors/TVs
1649 // (e.g. Epson L210SF, issue #2938). When enabled, adds hdmi_ignore_cec_init=1
1650 // to config.txt. Unlike hdmi_ignore_cec=1 this only suppresses the boot
1651 // broadcast — the CEC plugin can still power the display on/off on demand.
1652 //
1653 // Managed block pattern mirrors applyDisablePiRTCBlock above: strip every
1654 // prior copy (including truncated), remember first insertion point, and
1655 // re-insert ahead of the cape variant block to avoid leapfrog reboot loops.
1656 static const std::string HDMI_CEC_BLOCK_BEGIN = "# FPP HDMI CEC - BEGIN (managed by fppinit, do not edit)";
1657 static const std::string HDMI_CEC_BLOCK_END = "# FPP HDMI CEC - END";
1658
1659 // --- BEGIN applyDisableHDMICECBlock ---
1660 1 static bool applyDisableHDMICECBlock(std::string& content, bool disable) {
1661
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 const std::string orig = content;
1662 1 std::string desired;
1663
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (disable) {
1664 ✗ desired = HDMI_CEC_BLOCK_BEGIN + "\n[all]\nhdmi_ignore_cec_init=1\n[all]\n" + HDMI_CEC_BLOCK_END + "\n";
1665 }
1666
1667 1 size_t at = std::string::npos;
1668 1 size_t begin = content.find(HDMI_CEC_BLOCK_BEGIN);
1669
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
2 while (begin != std::string::npos) {
1670
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (at == std::string::npos) {
1671 1 at = begin;
1672 }
1673 1 size_t end = content.find(HDMI_CEC_BLOCK_END, begin);
1674
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
2 end = (end == std::string::npos) ? content.length()
1675 1 : end + HDMI_CEC_BLOCK_END.length();
1676
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 if (end < content.length() && content[end] == '\n') {
1677 1 ++end;
1678 }
1679
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 content.erase(begin, end - begin);
1680 1 begin = content.find(HDMI_CEC_BLOCK_BEGIN);
1681 }
1682
1683
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (!desired.empty()) {
1684 ✗ if (at == std::string::npos) {
1685 ✗ at = content.find(CAPE_VARIANT_BLOCK_BEGIN);
1686 ✗ if (at == std::string::npos) {
1687 ✗ while (!content.empty() && content.back() == '\n') {
1688 ✗ content.pop_back();
1689 }
1690 ✗ content += "\n\n";
1691 ✗ at = content.length();
1692 } else {
1693 ✗ desired += "\n";
1694 }
1695 }
1696 ✗ content.insert(at, desired);
1697 }
1698 2 return content != orig;
1699 1 }
1700 // --- END applyDisableHDMICECBlock ---
1701 #endif
1702
1703 4 void setupHDMICECConfig(bool rebootIfChanged) {
1704 #ifdef PLATFORM_PI
1705
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 std::string content = GetFileContents("/boot/firmware/config.txt");
1706
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 times.
3 if (content.empty()) {
1707 2 return;
1708 }
1709 // Pi 5 has no firmware HDMI path (RP1/KMS), so hdmi_ignore_cec_init is
1710 // ignored. Never write the block on Pi 5; this also cleans up a stale
1711 // block left from when the setting previously defaulted to on.
1712
4/18
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✓ Branch 11 taken 1 times.
✗ Branch 12 not taken.
✓ Branch 13 taken 1 times.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
1 bool want = !isPi5() && getRawSettingInt("DisableHDMICECInit", 0) != 0;
1713
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 if (applyDisableHDMICECBlock(content, want)) {
1714
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 PutFileContents("/boot/firmware/config.txt", content);
1715
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 printf("FPP - HDMI CEC configuration changed in config.txt\n");
1716
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (rebootIfChanged) {
1717 ✗ printf("\n\nRebooting to load new settings.\n\n");
1718 ✗ exec("/usr/sbin/reboot");
1719 }
1720 }
1721 #endif
1722
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 2 times.
4 }
1723
1724 #ifdef PLATFORM_PI
1725 // CM4/CM5 carry both a PCB antenna and a U.FL socket; the firmware picks one via
1726 // dtparam=ant1 (PCB, the default) or ant2 (U.FL). Only those base DTBs define the
1727 // ant2 override, which is what /proc/device-tree/__overrides__/ant2 tests for, and
1728 // the block is filtered to [cm4]/[cm5] anyway so a card moved into a board without
1729 // the param carries an inert block rather than a dterror. ant2 disables the PCB
1730 // antenna outright, so selecting it with nothing on the socket leaves WiFi with
1731 // almost no range -- hence a setting, not a default.
1732 //
1733 // Same managed block pattern as applyDisablePiRTCBlock above.
1734 static const std::string WIFI_ANTENNA_BLOCK_BEGIN = "# FPP WiFi Antenna - BEGIN (managed by fppinit, do not edit)";
1735 static const std::string WIFI_ANTENNA_BLOCK_END = "# FPP WiFi Antenna - END";
1736
1737 // --- BEGIN applyWifiAntennaBlock ---
1738 3 static bool applyWifiAntennaBlock(std::string& content, bool external) {
1739
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 const std::string orig = content;
1740 3 std::string desired;
1741
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 times.
3 if (external) {
1742
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 desired = WIFI_ANTENNA_BLOCK_BEGIN + "\n[cm4]\ndtparam=ant2\n[cm5]\ndtparam=ant2\n[all]\n" + WIFI_ANTENNA_BLOCK_END + "\n";
1743 }
1744
1745 3 size_t at = std::string::npos;
1746 3 size_t begin = content.find(WIFI_ANTENNA_BLOCK_BEGIN);
1747
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 3 times.
5 while (begin != std::string::npos) {
1748
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (at == std::string::npos) {
1749 2 at = begin;
1750 }
1751 2 size_t end = content.find(WIFI_ANTENNA_BLOCK_END, begin);
1752
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
4 end = (end == std::string::npos) ? content.length()
1753 2 : end + WIFI_ANTENNA_BLOCK_END.length();
1754
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 if (end < content.length() && content[end] == '\n') {
1755 2 ++end;
1756 }
1757
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 content.erase(begin, end - begin);
1758 2 begin = content.find(WIFI_ANTENNA_BLOCK_BEGIN);
1759 }
1760
1761
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 times.
3 if (!desired.empty()) {
1762
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
2 if (at == std::string::npos) {
1763 1 at = content.find(CAPE_VARIANT_BLOCK_BEGIN);
1764
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (at == std::string::npos) {
1765 ✗ while (!content.empty() && content.back() == '\n') {
1766 ✗ content.pop_back();
1767 }
1768 ✗ content += "\n\n";
1769 ✗ at = content.length();
1770 } else {
1771
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 desired += "\n";
1772 }
1773 }
1774
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 content.insert(at, desired);
1775 }
1776 6 return content != orig;
1777 3 }
1778 // --- END applyWifiAntennaBlock ---
1779 #endif
1780
1781 7 void setupWifiAntennaConfig(bool rebootIfChanged) {
1782 #ifdef PLATFORM_PI
1783 // Leave config.txt alone on boards without the param: there is no setting
1784 // to reconcile (the UI hides it), and any block left from a CM is inert.
1785
3/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✓ Branch 3 taken 3 times.
6 if (!FileExists("/proc/device-tree/__overrides__/ant2")) {
1786 3 return;
1787 }
1788
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 std::string content = GetFileContents("/boot/firmware/config.txt");
1789
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (content.empty()) {
1790 ✗ return;
1791 }
1792
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
6 std::string antenna = "internal";
1793
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 getRawSetting("WifiAntenna", antenna);
1794
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.
3 if (applyWifiAntennaBlock(content, antenna == "external")) {
1795
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 PutFileContents("/boot/firmware/config.txt", content);
1796
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 printf("FPP - WiFi antenna configuration changed in config.txt\n");
1797
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (rebootIfChanged) {
1798 ✗ printf("\n\nRebooting to load new settings.\n\n");
1799 ✗ exec("/usr/sbin/reboot");
1800 }
1801 }
1802 #endif
1803
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
4 }
1804
1805 // The 64-bit Pi images keep the Pi 5's 16K-page kernel, and its modules, but
1806 // park the kernel under a name the firmware never loads; a Pi 5 would otherwise
1807 // prefer kernel_2712.img over the 4K-page kernel8.img. The developer-level
1808 // Pi16KPageKernel setting copies it back into place. The 32-bit images, images
1809 // from before this, and stock Raspberry Pi OS installs have no parked copy, and
1810 // are left alone.
1811 //
1812 // Runs at every boot as well as from the UI: an FPPOS upgrade replaces the boot
1813 // partition, which drops the active copy, and the boot pass puts it back.
1814 5 void setup16KPageKernel(bool rebootIfChanged) {
1815 #ifdef PLATFORM_PI
1816
3/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 not taken.
✗ Branch 7 not taken.
12 static const std::string ACTIVE = "/boot/firmware/kernel_2712.img";
1817
3/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 not taken.
✗ Branch 7 not taken.
12 static const std::string PARKED = "/boot/firmware/kernel_2712.img.fpp-disabled";
1818
3/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 2 times.
4 if (!FileExists(PARKED)) {
1819 2 return;
1820 }
1821
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 bool want = getRawSettingInt("Pi16KPageKernel", 0) != 0;
1822
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 bool have = FileExists(ACTIVE);
1823 2 std::error_code ec;
1824
3/4
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 if (want && !have) {
1825 // Copy to a temporary name first so a power cut mid-copy can't leave a
1826 // truncated kernel_2712.img for the firmware to boot.
1827
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 std::string tmp = ACTIVE + ".tmp";
1828
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::filesystem::copy_file(PARKED, tmp, std::filesystem::copy_options::overwrite_existing, ec);
1829
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (!ec) {
1830
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 std::filesystem::rename(tmp, ACTIVE, ec);
1831 }
1832
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (ec) {
1833 ✗ printf("FPP - Could not enable the 16K-page kernel: %s\n", ec.message().c_str());
1834 ✗ std::filesystem::remove(tmp, ec);
1835 ✗ return;
1836 }
1837
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 printf("FPP - Enabled the 16K-page kernel\n");
1838
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 } else if (!want && have) {
1839 // Park the active copy rather than deleting it: it is the one that
1840 // matches the 16K module tree, should anything have updated it.
1841
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 std::filesystem::rename(ACTIVE, PARKED, ec);
1842
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (ec) {
1843 ✗ printf("FPP - Could not disable the 16K-page kernel: %s\n", ec.message().c_str());
1844 ✗ return;
1845 }
1846
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 printf("FPP - Disabled the 16K-page kernel\n");
1847 } else {
1848 ✗ return;
1849 }
1850 2 sync();
1851 // Only the Pi 5 family loads kernel_2712.img; elsewhere the change is inert.
1852
2/8
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 2 times.
2 if (rebootIfChanged && isPi5()) {
1853 ✗ printf("\n\nRebooting to load new settings.\n\n");
1854 ✗ exec("/usr/sbin/reboot");
1855 }
1856 #endif
1857 1 }
1858
1859 19 void setupChannelOutputs() {
1860 #ifdef PLATFORM_PI
1861 15 bool hasDPI = false;
1862 15 bool hasRPIMatrix = false;
1863
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 Json::Value v;
1864 // Both files are read on every boot, and fppinit has no one to restart it:
1865 // an entry with a field of the wrong JSON type is skipped, not thrown on.
1866 7 auto isOutput = [](const Json::Value& co) {
1867
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 1 times.
12 return co.isObject() && co["type"].isConvertibleTo(Json::stringValue) &&
1868
5/6
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
12 co["subType"].isConvertibleTo(Json::stringValue) && co["enabled"].isConvertibleTo(Json::intValue);
1869 };
1870
4/6
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✓ Branch 5 taken 12 times.
15 if (FileExists(FPP_MEDIA_DIR + "/config/channeloutputs.json")) {
1871
5/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 2 times.
✓ Branch 7 taken 1 times.
3 if (LoadJsonFromString(GetFileContents(FPP_MEDIA_DIR + "/config/channeloutputs.json"), v, JsonRoot::Object)) {
1872
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 const Json::Value& cos = v["channelOutputs"];
1873
8/10
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
✓ Branch 7 taken 1 times.
✓ Branch 8 taken 2 times.
✓ Branch 9 taken 2 times.
4 for (Json::ArrayIndex x = 0; cos.isArray() && x < cos.size(); x++) {
1874
5/30
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 2 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
✗ Branch 20 not taken.
✗ Branch 21 not taken.
✗ Branch 22 not taken.
✗ Branch 23 not taken.
✗ Branch 24 not taken.
✓ Branch 25 taken 2 times.
✗ Branch 26 not taken.
✓ Branch 27 taken 2 times.
✗ Branch 28 not taken.
✗ Branch 29 not taken.
2 if (isOutput(cos[x]) && cos[x]["subType"].asString() == "RGBMatrix" && cos[x]["enabled"].asInt() == 1) {
1875 ✗ hasRPIMatrix = true;
1876 }
1877 }
1878 }
1879 }
1880
4/6
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✓ Branch 5 taken 11 times.
15 if (FileExists(FPP_MEDIA_DIR + "/config/co-pixelStrings.json")) {
1881
4/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 4 times.
✗ Branch 7 not taken.
4 if (LoadJsonFromString(GetFileContents(FPP_MEDIA_DIR + "/config/co-pixelStrings.json"), v, JsonRoot::Object)) {
1882 4 bool migrated = false;
1883
9/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 taken 9 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 5 times.
✓ Branch 11 taken 4 times.
✓ Branch 12 taken 5 times.
✓ Branch 13 taken 4 times.
9 for (Json::ArrayIndex x = 0; v["channelOutputs"].isArray() && x < v["channelOutputs"].size(); x++) {
1884
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 3 times.
✓ Branch 7 taken 2 times.
5 if (!isOutput(v["channelOutputs"][x])) {
1885 3 continue;
1886 }
1887
6/12
✓ 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.
2 if (v["channelOutputs"][x]["type"].asString() == "RPIWS281X") {
1888 // The RPIWS281X driver has been removed. DPI drives the same pins without
1889 // conflicting with the onboard audio, so migrate the config to DPIPixels.
1890 // Capes we don't have a mapping for keep their subType; DPIPixels resolves
1891 // the matching string config at runtime.
1892
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 std::string subType = v["channelOutputs"][x]["subType"].asString();
1893
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 v["channelOutputs"][x]["type"] = "DPIPixels";
1894
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
2 if (subType == "PiHat") {
1895 ✗ v["channelOutputs"][x]["subType"] = "PiHat-DPIPixels";
1896
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
2 } else if (subType == "rPi-MFC") {
1897 ✗ v["channelOutputs"][x]["subType"] = "rPi-MFC-DPIPixels";
1898
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 } else if (subType == "rPi-28D") {
1899 // The rPi-28D's third output was ws2801 over SPI which DPI cannot drive.
1900 // The 4 output variant drives CN1's clock and data pins as WS281x instead.
1901
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 v["channelOutputs"][x]["subType"] = "rPi-28D-DPIPixels-4";
1902
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 Json::Value& outputs = v["channelOutputs"][x]["outputs"];
1903
7/10
✓ 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 5 times.
✓ Branch 7 taken 2 times.
✓ Branch 8 taken 5 times.
✓ Branch 9 taken 2 times.
7 for (Json::ArrayIndex o = 0; outputs.isArray() && o < outputs.size(); o++) {
1904
9/14
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✓ Branch 5 taken 1 times.
✓ Branch 6 taken 4 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 4 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 4 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 3 times.
✓ Branch 13 taken 1 times.
8 if (outputs[o].isObject() && outputs[o]["protocol"].isString() &&
1905
10/16
✓ 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 1 times.
✓ Branch 9 taken 2 times.
✓ Branch 10 taken 3 times.
✓ Branch 11 taken 2 times.
✓ Branch 12 taken 1 times.
✓ Branch 13 taken 4 times.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
8 outputs[o]["protocol"].asString() == "ws2801") {
1906
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 outputs[o]["protocol"] = "ws2811";
1907 }
1908 }
1909 }
1910 2 migrated = true;
1911 2 }
1912
13/28
✓ 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 taken 2 times.
✗ Branch 25 not taken.
✗ Branch 26 not taken.
✗ Branch 27 not taken.
2 if (v["channelOutputs"][x]["type"].asString() == "DPIPixels" && v["channelOutputs"][x]["enabled"].asInt() == 1) {
1913 2 hasDPI = true;
1914 }
1915 }
1916
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
4 if (migrated) {
1917
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 PutFileContents(FPP_MEDIA_DIR + "/config/co-pixelStrings.json", SaveJsonToString(v));
1918 }
1919 }
1920 }
1921 15 bool changed = false;
1922 // Only the RGBMatrix output still needs the onboard audio out of the way. Pixel strings
1923 // are driven by DPI now, which coexists with snd_bcm2835.
1924
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 15 times.
15 if (hasRPIMatrix) {
1925 ✗ if (!FileExists("/etc/modprobe.d/blacklist-bcm2835.conf")) {
1926 ✗ PutFileContents("/etc/modprobe.d/blacklist-bcm2835.conf", "blacklist snd_bcm2835");
1927 ✗ changed = true;
1928 }
1929 // Preemptively load the USB sound driver so it will detect before snd-dummy is created
1930 // and get the default card0 slot
1931 ✗ exec("modprobe snd_usb_audio");
1932
1933 // load the dummy sound driver so things that expect a sound device to be there will still work
1934 ✗ exec("modprobe snd-dummy");
1935 ✗ exec("modprobe snd-seq-dummy");
1936 } else {
1937
2/4
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 15 times.
15 if (FileExists("/etc/modprobe.d/blacklist-bcm2835.conf")) {
1938 ✗ unlink("/etc/modprobe.d/blacklist-bcm2835.conf");
1939 }
1940
2/4
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✗ Branch 3 not taken.
30 exec("modprobe snd-bcm2835");
1941
2/4
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✗ Branch 3 not taken.
30 exec("modprobe snd_usb_audio");
1942 }
1943
2/4
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✗ Branch 3 not taken.
15 std::string content = GetFileContents("/boot/firmware/config.txt");
1944
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 capVC4CMAForBoard(content, changed);
1945 15 size_t idx = content.find("dtoverlay=vc4-kms-dpi-fpp");
1946
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 std::string origLine = "";
1947
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 14 times.
15 if (idx != std::string::npos) {
1948 1 size_t idx2 = content.find("\n", idx);
1949
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 origLine = content.substr(idx, idx2 - idx);
1950 }
1951
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 13 times.
15 if (hasDPI) {
1952 // The DPI overlay only bootstraps the 1920-wide, 38.4MHz pipeline; its
1953 // vertical resolution is just the boot/default mode. DPIPixelsOutput
1954 // overrides the vertical timing at runtime (sizing vactive to the longest
1955 // string and setting the frame rate purely via the vertical blanking, no
1956 // reboot). Declare the tallest envelope we support - vactive=997 (the old
1957 // 20fps mode, the lowest rate / longest string) - so every runtime mode is
1958 // a subset of what the overlay advertises.
1959
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
4 std::string width = "1920";
1960
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 std::string height = "997";
1961
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 std::string line = "dtoverlay=vc4-kms-dpi-fpp,rgb888,hactive=" + width + ",hfp=0,hsync=1,hbp=0,vactive=" + height + ",vfp=1,vsync=1,vbp=1";
1962
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (line != origLine) {
1963
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
2 if (origLine != "") {
1964 ✗ content.replace(idx, origLine.size(), line);
1965 } else {
1966
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 content.append(line).append("\n");
1967 }
1968
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 PutFileContents("/boot/firmware/config.txt", content);
1969 2 changed = true;
1970 }
1971
3/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 12 times.
15 } else if (origLine != "") {
1972 1 size_t idx2 = content.find("\n", idx);
1973
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 content.erase(idx, idx2 - idx);
1974
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 PutFileContents("/boot/firmware/config.txt", content);
1975 1 changed = true;
1976 }
1977
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 11 times.
15 if (changed) {
1978
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 printf("\n\nRebooting to load new settings.\n\n");
1979
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
8 exec("/usr/sbin/reboot");
1980 }
1981 #endif
1982 19 }
1983
1984 // Drive a GPIO line, looked up by the name the kernel exposes for it
1985 // (gpio-line-names in the device tree), to the given value. Uses libgpiod
1986 // directly rather than the gpioset/gpioinfo tools whose command line and
1987 // output format changed incompatibly between libgpiod 1.x and 2.x.
1988 //
1989 // Like gpioset, the line is released as soon as the value has been written:
1990 // the pin keeps the level the driver last wrote to it.
1991 1 static bool setNamedGPIO(const std::string& pin, int val) {
1992 #ifdef HASGPIOD
1993 1 bool found = false;
1994
5/8
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 11 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 11 times.
✓ Branch 7 taken 1 times.
12 for (const auto& entry : std::filesystem::directory_iterator("/dev")) {
1995
4/6
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 11 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 6 times.
✓ Branch 5 taken 5 times.
11 if (!entry.path().filename().string().starts_with("gpiochip")) {
1996 10 continue;
1997 }
1998
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 std::string path = entry.path().string();
1999 try {
2000 #ifdef IS_GPIOD_CXX_V2
2001
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 gpiod::chip chip(path);
2002
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 int offset = chip.get_line_offset_from_name(pin);
2003
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 1 times.
5 if (offset < 0) {
2004 4 continue;
2005 }
2006
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 gpiod::line_settings settings;
2007
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 settings.set_direction(gpiod::line::direction::OUTPUT);
2008
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 settings.set_output_value(val ? gpiod::line::value::ACTIVE : gpiod::line::value::INACTIVE);
2009
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 gpiod::line_request request = chip.prepare_request()
2010
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
2 .set_consumer("FPPINIT")
2011
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
2 .add_line_settings(offset, settings)
2012
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 .do_request();
2013
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 request.release();
2014 #else
2015 gpiod::chip chip(path, gpiod::chip::OPEN_BY_PATH);
2016 gpiod::line line = chip.find_line(pin);
2017 if (!line) {
2018 continue;
2019 }
2020 int offset = line.offset();
2021 gpiod::line_request request;
2022 request.consumer = "FPPINIT";
2023 request.request_type = gpiod::line_request::DIRECTION_OUTPUT;
2024 line.request(request, val);
2025 line.release();
2026 #endif
2027
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 printf("FPP - Set GPIO %s (%s line %d) to %d\n", pin.c_str(), path.c_str(), offset, val);
2028 1 found = true;
2029
2/4
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 4 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
5 } catch (const std::exception& ex) {
2030 ✗ printf("FPP - Could not set GPIO %s on %s: %s\n", pin.c_str(), path.c_str(), ex.what());
2031 ✗ }
2032
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 4 times.
6 }
2033
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (!found) {
2034 ✗ printf("FPP - Could not find a GPIO line named %s\n", pin.c_str());
2035 }
2036 1 return found;
2037 #else
2038 printf("FPP - Cannot set GPIO %s, no libgpiod support\n", pin.c_str());
2039 return false;
2040 #endif
2041 }
2042
2043 1 void handleRebootActions() {
2044
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 if (FileExists(FPP_MEDIA_DIR + "/tmp/cape-info.json")) {
2045
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 Json::Value v;
2046
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 (LoadJsonFromString(GetFileContents(FPP_MEDIA_DIR + "/tmp/cape-info.json"), v)) {
2047
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 if (v.isMember("rebootActions")) {
2048
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 1 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 2 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 1 times.
✓ Branch 13 taken 1 times.
2 for (const auto& action : v["rebootActions"]) {
2049
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 (action["type"].asString() == "gpio") {
2050
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 std::string pin = action["pin"].asString();
2051
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (pin.empty()) {
2052 ✗ continue;
2053 }
2054 1 int val = 0;
2055
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (pin[0] == '+') {
2056
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 pin = pin.substr(1);
2057 1 val = 1;
2058 }
2059
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 setNamedGPIO(pin, val);
2060
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 }
2061 }
2062 }
2063 }
2064 1 }
2065 1 }
2066