GCC Code Coverage Report


Directory: ./
File: gpio.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 392 599 65.4%
Functions: 30 43 69.8%
Branches: 524 1266 41.4%

Line Branch Exec Source
1 /*
2 * This file is part of the Falcon Player (FPP) and is Copyright (C)
3 * 2013-2022 by the Falcon Player Developers.
4 *
5 * The Falcon Player (FPP) is free software, and is covered under
6 * multiple Open Source licenses. Please see the included 'LICENSES'
7 * file for descriptionsE 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 "fpp-pch.h"
14
15 #include "fpp-json.h"
16 #include "fpphttp.h" // drogon/HTTP helpers used here; no longer pulled transitively (see fpphttp_types.h)
17 #include "fpphttp_clientip.h" // getEffectiveClientIP() - FPP-internal, not in the plugin API
18 #include "Events.h"
19
20 #include <array>
21 #include <list>
22 #include <memory>
23 #include <map>
24 #include <mutex>
25 #include <string.h>
26 #include <string>
27 #include <vector>
28
29 #include "EPollManager.h"
30 #include "common.h"
31 #include "log.h"
32 #include "settings.h"
33 #include "commands/Commands.h"
34 #include "util/GPIOUtils.h"
35
36 #include "Player.h"
37 #include "Timers.h"
38 #include "gpio.h"
39 #include "Warnings.h"
40
41 GPIOManager GPIOManager::INSTANCE;
42
43 // Guards GPIOManager::INSTANCE.fppCommandLastValue (see gpio.h). A file
44 // static rather than a member so the class layout doesn't change.
45 static std::mutex s_commandLastValueLock;
46 // eventStates/pollStates change (and their states are deleted) on the main
47 // loop, during an output reload (OutputMonitor's eFuse pins) and at
48 // shutdown. The main loop's own walks need no lock; GetInputPinValue() is
49 // also called from HTTP and command threads, and takes this.
50 static std::mutex s_statesLock;
51
52 248 void GPIOManager::SetCommandLastValue(const std::string& pin, bool value) {
53
1/2
✓ Branch 0 taken 248 times.
✗ Branch 1 not taken.
248 std::lock_guard<std::mutex> lock(s_commandLastValueLock);
54
1/2
✓ Branch 0 taken 248 times.
✗ Branch 1 not taken.
248 INSTANCE.fppCommandLastValue[pin] = value;
55 248 }
56 324 bool GPIOManager::GetCommandLastValue(const std::string& pin, bool& value) {
57
1/2
✓ Branch 0 taken 324 times.
✗ Branch 1 not taken.
324 std::lock_guard<std::mutex> lock(s_commandLastValueLock);
58
1/2
✓ Branch 0 taken 324 times.
✗ Branch 1 not taken.
324 auto it = INSTANCE.fppCommandLastValue.find(pin);
59
2/2
✓ Branch 0 taken 80 times.
✓ Branch 1 taken 244 times.
324 if (it == INSTANCE.fppCommandLastValue.end()) {
60 80 return false;
61 }
62 244 value = it->second;
63 244 return true;
64 324 }
65 240 bool GPIOManager::ToggleCommandLastValue(const std::string& pin) {
66
1/2
✓ Branch 0 taken 240 times.
✗ Branch 1 not taken.
240 std::lock_guard<std::mutex> lock(s_commandLastValueLock);
67
1/2
✓ Branch 0 taken 240 times.
✗ Branch 1 not taken.
240 bool& v = INSTANCE.fppCommandLastValue[pin];
68 240 v = !v;
69 240 return v;
70 240 }
71
72 class FPPGPIOCommand : public Command {
73 public:
74 1885 FPPGPIOCommand() :
75
2/4
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1885 times.
✗ Branch 3 not taken.
3770 Command("GPIO") {
76
7/14
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1885 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1885 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1885 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1885 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1885 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 1885 times.
✗ Branch 13 not taken.
15080 args.push_back(CommandArg("pin", "string", "Pin").setContentListUrl("api/options/GPIOLIST"));
77
6/12
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1885 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1885 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1885 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1885 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1885 times.
✗ Branch 11 not taken.
15080 args.push_back(CommandArg("on", "string", "Action").setContentList({ "On", "Off", "Opposite" }));
78 1885 }
79 242 virtual std::unique_ptr<Command::Result> run(const std::vector<std::string>& args) override {
80
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 242 times.
242 if (args.size() != 2) {
81 ✗ return std::make_unique<Command::ErrorResult>("Invalid number of arguments. GPIO needs two arguments.");
82 }
83
1/2
✓ Branch 0 taken 242 times.
✗ Branch 1 not taken.
242 std::string n = args[0];
84
1/2
✓ Branch 0 taken 242 times.
✗ Branch 1 not taken.
242 std::string v = args[1];
85
1/2
✓ Branch 0 taken 242 times.
✗ Branch 1 not taken.
242 const PinCapabilities& p = PinCapabilities::getPinByName(n);
86 242 int inputValue = 0;
87
7/10
✓ Branch 0 taken 242 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 242 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 242 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✓ Branch 7 taken 241 times.
✓ Branch 8 taken 1 times.
✓ Branch 9 taken 241 times.
242 if (p.ptr() && GPIOManager::INSTANCE.GetInputPinValue(n, inputValue)) {
88 // see POST /api/gpio: driving it would kill the configured input
89
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 return std::make_unique<Command::ErrorResult>("Pin " + n + " is configured as a GPIO input");
90 }
91
2/4
✓ Branch 0 taken 241 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 241 times.
✗ Branch 3 not taken.
241 if (p.ptr()) {
92
3/6
✓ Branch 0 taken 241 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 241 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 241 times.
✗ Branch 5 not taken.
964 p.configPin();
93 // setValue() on a line that was never acquired is a silent no-op, so
94 // without this the command reports OK for a pin it never drove.
95
3/4
✓ Branch 0 taken 241 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 240 times.
241 if (!p.isAcquired()) {
96
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 return std::make_unique<Command::ErrorResult>("Pin " + n + " could not be acquired; it is in use by another driver");
97 }
98
11/22
✓ Branch 0 taken 240 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 240 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 240 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 240 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 240 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 240 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 240 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 240 times.
✗ Branch 15 not taken.
✓ Branch 16 taken 240 times.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✓ Branch 19 taken 240 times.
✗ Branch 20 not taken.
✓ Branch 21 taken 240 times.
240 if (v == "On" || v == "on" || v == "true" || v == "True" || v == "1") {
99 ✗ p.setValue(true);
100 ✗ GPIOManager::SetCommandLastValue(n, true);
101
11/22
✓ Branch 0 taken 240 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 240 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 240 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 240 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 240 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 240 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 240 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 240 times.
✗ Branch 15 not taken.
✓ Branch 16 taken 240 times.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✓ Branch 19 taken 240 times.
✗ Branch 20 not taken.
✓ Branch 21 taken 240 times.
240 } else if (v == "Off" || v == "off" || v == "false" || v == "False" || v == "0") {
102 ✗ p.setValue(false);
103 ✗ GPIOManager::SetCommandLastValue(n, false);
104
3/10
✓ Branch 0 taken 240 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 240 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✓ Branch 8 taken 240 times.
✗ Branch 9 not taken.
240 } else if (v == "Opposite" || v == "opposite") {
105
2/4
✓ Branch 0 taken 240 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 240 times.
✗ Branch 3 not taken.
240 p.setValue(GPIOManager::ToggleCommandLastValue(n));
106 } else {
107 ✗ return std::make_unique<Command::ErrorResult>("Invalid Action" + v);
108 }
109
1/2
✓ Branch 0 taken 240 times.
✗ Branch 1 not taken.
240 return std::make_unique<Command::Result>("OK");
110 }
111 ✗ return std::make_unique<Command::ErrorResult>("No Pin Named " + n);
112 242 }
113 };
114
115 2346 GPIOManager::GPIOManager() {
116 2346 }
117 458 GPIOManager::~GPIOManager() {
118 458 }
119
120 1885 void GPIOManager::Initialize(std::map<int, std::function<bool(int)>>& callbacks) {
121
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 SetupGPIOInput(callbacks);
122
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 std::vector<std::string> pins = PinCapabilities::getPinNames();
123
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 if (!pins.empty()) {
124
5/12
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1885 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1885 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1885 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1885 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3930 CommandManager::INSTANCE.addCategorizedCommand(new FPPGPIOCommand(), "Outputs", 1);
125 }
126 1885 }
127
128 497 bool GPIOManager::GetInputPinValue(const std::string& name, int& value) const {
129
1/2
✓ Branch 0 taken 497 times.
✗ Branch 1 not taken.
497 std::lock_guard<std::mutex> lock(s_statesLock);
130
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 495 times.
497 for (auto* s : eventStates) {
131
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 (s->pin && s->pin->name == name) {
132 2 value = s->lastValue;
133 2 return true;
134 }
135 }
136
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 495 times.
495 for (auto* s : pollStates) {
137 ✗ if (s->pin && s->pin->name == name) {
138 ✗ value = s->lastValue;
139 ✗ return true;
140 }
141 }
142 495 return false;
143 497 }
144
145 // Poll-based pins: detect edges each main loop iteration.
146 // Non-debounced edges fire immediately. Debounced edges schedule a
147 // timer via Timers to re-read and confirm after the settle window.
148 464825 void GPIOManager::CheckGPIOInputs(void) {
149
2/2
✓ Branch 0 taken 464664 times.
✓ Branch 1 taken 161 times.
464825 if (pollStates.empty()) {
150 464664 return;
151 }
152
153
2/2
✓ Branch 0 taken 161 times.
✓ Branch 1 taken 161 times.
322 for (auto a : pollStates) {
154 // Skip pins that already have a pending debounce timer
155
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 159 times.
161 if (a->pendingValue != a->lastValue) {
156 2 continue;
157 }
158
1/2
✓ Branch 0 taken 159 times.
✗ Branch 1 not taken.
159 int val = a->pin->getValue();
159
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 154 times.
159 if (val != a->lastValue) {
160
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 5 times.
5 if (!a->shouldDebounce(val)) {
161 ✗ a->doAction(val);
162 } else {
163 // New transition detected — start the settle timer.
164 5 a->pendingValue = val;
165
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 a->pendingTime = GetMonotonicTimeMicros();
166
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 scheduleDebounceCheck(a);
167 }
168 }
169 }
170 }
171
172 // Timer-driven debounce check for all pins (event-based and poll-based).
173 // Called when a debounce settle timer fires via Timers/EPollManager.
174 21 void GPIOManager::checkDebounceTimers() {
175
1/2
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
21 long long tm = GetMonotonicTimeMicros();
176 42 auto checkList = [&](std::list<GPIOState*>& states) {
177
2/2
✓ Branch 0 taken 21 times.
✓ Branch 1 taken 42 times.
63 for (auto a : states) {
178
1/2
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
21 if (a->pendingValue != a->lastValue) {
179
1/2
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
21 if ((tm - a->pendingTime) >= (long long)a->debounceTime) {
180 // Settle window elapsed — confirm pin is still at pending value.
181
1/2
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
21 int val = a->pin->getValue();
182
2/2
✓ Branch 0 taken 20 times.
✓ Branch 1 taken 1 times.
21 if (val == a->pendingValue) {
183
1/2
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
20 a->doAction(val);
184 } else {
185 // Bounced back — cancel.
186 1 a->pendingValue = a->lastValue;
187 1 a->pendingTime = 0;
188 }
189 } else {
190 // Still within settle window — schedule another check.
191 ✗ scheduleDebounceCheck(a);
192 }
193 }
194 }
195 42 };
196
1/2
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
21 checkList(eventStates);
197
1/2
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
21 checkList(pollStates);
198 21 }
199
200 // Schedule a timer to re-check debounce for an event-based pin.
201 23 void GPIOManager::scheduleDebounceCheck(GPIOState* state) {
202
1/2
✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
23 long long remainingUS = (long long)state->debounceTime - (GetMonotonicTimeMicros() - state->pendingTime);
203 23 long long remainingMS = (remainingUS + 999) / 1000; // round up to ms
204
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 23 times.
23 if (remainingMS < 1) {
205 ✗ remainingMS = 1;
206 }
207
1/2
✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
23 std::string timerName = "gpio_db_" + state->pin->name;
208
2/4
✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 23 times.
✗ Branch 3 not taken.
23 Timers::INSTANCE.addTimer(timerName, GetTimeMS() + remainingMS, [this]() {
209 21 checkDebounceTimers();
210 21 });
211 23 }
212
213 1867 void GPIOManager::Cleanup() {
214 // out of the lists first, so GetInputPinValue() can't find one being deleted
215 1867 decltype(eventStates) events;
216 1867 decltype(pollStates) polls;
217 {
218
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 std::lock_guard<std::mutex> lock(s_statesLock);
219 1867 events.swap(eventStates);
220 1867 polls.swap(pollStates);
221 1867 }
222
2/2
✓ Branch 0 taken 11 times.
✓ Branch 1 taken 1867 times.
1878 for (auto a : events) {
223
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 11 times.
11 if (!a->ledPin.empty())
224 ✗ Timers::INSTANCE.stopPeriodicTimer("gpio_led_pulse_" + a->pin->name);
225
1/2
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
11 if (a->file != -1)
226
1/2
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
11 a->pin->releaseGPIOD();
227
1/2
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
11 delete a;
228 }
229
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1867 times.
1868 for (auto a : polls) {
230
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (!a->ledPin.empty())
231 ✗ Timers::INSTANCE.stopPeriodicTimer("gpio_led_pulse_" + a->pin->name);
232
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (a->file != -1)
233 ✗ a->pin->releaseGPIOD();
234
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 delete a;
235 }
236 1867 }
237
238 // --------------------------------------------------------------------------
239 // OpenAPI docs for the /gpio/* endpoints handled below.
240 // --------------------------------------------------------------------------
241
242 /**
243 * List the available GPIO pins. Add ?list=true for just the pin names.
244 *
245 * @route GET /api/gpio
246 * @param boolean list Return just the pin names instead of full capabilities.
247 * @response 200 Array of pin capability objects (or pin names when `list=true`).
248 */
249
250 /**
251 * Read the last value set on a GPIO pin via the API/commands. Only pins with a
252 * cached value can be read; output pins must be SET before they can be read.
253 *
254 * @route GET /api/gpio/{pin}
255 * @response 200 Object with `pin` and `value` (0 or 1).
256 * @response 400 The pin has no cached value.
257 */
258 346 HttpResponsePtr GPIOManager::render_GET(const HttpRequestPtr& req) {
259
2/4
✓ Branch 0 taken 346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 346 times.
✗ Branch 3 not taken.
346 auto pieces = getPathPieces(req->path());
260 346 int plen = pieces.size();
261
1/2
✓ Branch 0 taken 346 times.
✗ Branch 1 not taken.
346 std::string p1 = pieces[0];
262
2/4
✓ Branch 0 taken 346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 346 times.
✗ Branch 3 not taken.
346 if (p1 == "gpio") {
263
2/2
✓ Branch 0 taken 16 times.
✓ Branch 1 taken 330 times.
346 if (plen <= 1) {
264 16 bool simpleList = false;
265
4/8
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 16 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 16 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 16 times.
32 if (getRequestArg(req, "list") == "true") {
266 ✗ simpleList = true;
267 }
268
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 std::vector<std::string> pins = PinCapabilities::getPinNames();
269
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 16 times.
16 if (pins.empty()) {
270 ✗ return makeStringResponse("[]", 200, "application/json");
271 }
272
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 Json::Value result;
273
2/2
✓ Branch 0 taken 468 times.
✓ Branch 1 taken 16 times.
484 for (auto& a : pins) {
274
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 468 times.
468 if (simpleList) {
275 ✗ result.append(a);
276 } else {
277
3/6
✓ Branch 0 taken 468 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 468 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 468 times.
✗ Branch 5 not taken.
468 result.append(PinCapabilities::getPinByName(a).toJSON());
278 }
279 }
280
2/4
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 16 times.
✗ Branch 3 not taken.
32 std::string resultStr = SaveJsonToString(result);
281
2/4
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 16 times.
✗ Branch 3 not taken.
16 return makeStringResponse(resultStr, 200, "application/json");
282
2/2
✓ Branch 0 taken 320 times.
✓ Branch 1 taken 10 times.
346 } else if (plen == 2) {
283 // Handle reading individual GPIO pin value: /gpio/{pin_name}
284
1/2
✓ Branch 0 taken 320 times.
✗ Branch 1 not taken.
320 std::string pinName = pieces[1];
285
1/2
✓ Branch 0 taken 320 times.
✗ Branch 1 not taken.
320 Json::Value result;
286
287 // Check if we have a tracked value for this pin FIRST (from previous SET operations)
288 // This avoids touching the pin hardware which could interfere with output pins
289 320 bool cachedValue = false;
290
3/4
✓ Branch 0 taken 320 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 244 times.
✓ Branch 3 taken 76 times.
320 if (GPIOManager::GetCommandLastValue(pinName, cachedValue)) {
291 // Return the last set value without touching the pin
292
3/4
✓ Branch 0 taken 124 times.
✓ Branch 1 taken 120 times.
✓ Branch 2 taken 244 times.
✗ Branch 3 not taken.
244 LogDebug(VB_HTTP, "GPIO GET: Returning cached value for %s: %d\n", pinName.c_str(), cachedValue ? 1 : 0);
293
2/4
✓ Branch 0 taken 244 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 244 times.
✗ Branch 3 not taken.
244 result["pin"] = pinName;
294
4/6
✓ Branch 0 taken 124 times.
✓ Branch 1 taken 120 times.
✓ Branch 2 taken 244 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 244 times.
✗ Branch 5 not taken.
244 result["value"] = cachedValue ? 1 : 0;
295
2/4
✓ Branch 0 taken 244 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 244 times.
✗ Branch 3 not taken.
244 result["Status"] = "OK";
296
2/4
✓ Branch 0 taken 244 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 244 times.
✗ Branch 3 not taken.
244 result["respCode"] = 200;
297
298
2/4
✓ Branch 0 taken 244 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 244 times.
✗ Branch 3 not taken.
488 std::string resultStr = SaveJsonToString(result);
299
2/4
✓ Branch 0 taken 244 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 244 times.
✗ Branch 3 not taken.
244 return makeStringResponse(resultStr, 200, "application/json");
300 244 }
301
302 // No tracked value - pin hasn't been set via API/commands yet
303 // Return error rather than trying to read it which could interfere with output pins
304
1/2
✓ Branch 0 taken 76 times.
✗ Branch 1 not taken.
76 LogWarn(VB_HTTP, "GPIO GET: No cached value for pin %s - cannot read output pins safely\n", pinName.c_str());
305
306
2/4
✓ Branch 0 taken 76 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 76 times.
✗ Branch 3 not taken.
76 result["pin"] = pinName;
307
2/4
✓ Branch 0 taken 76 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 76 times.
✗ Branch 3 not taken.
76 result["Status"] = "ERROR";
308
2/4
✓ Branch 0 taken 76 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 76 times.
✗ Branch 3 not taken.
76 result["respCode"] = 400;
309
2/4
✓ Branch 0 taken 76 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 76 times.
✗ Branch 3 not taken.
76 result["Message"] = "Pin has no cached value. For output pins, you must SET a value before you can GET it. For input pins, configure them via the GPIO Input configuration page.";
310
311
2/4
✓ Branch 0 taken 76 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 76 times.
✗ Branch 3 not taken.
152 std::string resultStr = SaveJsonToString(result);
312
2/4
✓ Branch 0 taken 76 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 76 times.
✗ Branch 3 not taken.
76 return makeStringResponse(resultStr, 400, "application/json");
313 320 }
314 }
315
3/6
✓ 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.
40 return makeStringResponse("Not Found", 404, "text/plain");
316 346 }
317
318 /**
319 * Configure a GPIO pin for output and set its value. Body: `{"value": 0|1}`.
320 *
321 * @route POST /api/gpio/{pin}
322 * @body {"value": 1}
323 * @response 200 Object with `pin` and the applied `value`.
324 * @response 400 Missing/invalid `value` field.
325 * @response 404 The named pin does not exist.
326 * @response 500 Error setting the pin.
327 */
328 521 HttpResponsePtr GPIOManager::render_POST(const HttpRequestPtr& req) {
329
2/4
✓ Branch 0 taken 521 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 521 times.
✗ Branch 3 not taken.
521 auto pieces = getPathPieces(req->path());
330 521 int plen = pieces.size();
331
1/2
✓ Branch 0 taken 521 times.
✗ Branch 1 not taken.
521 std::string p1 = pieces[0];
332
333
6/8
✓ Branch 0 taken 521 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 521 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 476 times.
✓ Branch 5 taken 45 times.
✓ Branch 6 taken 476 times.
✓ Branch 7 taken 45 times.
521 if (p1 == "gpio" && plen == 2) {
334 // Handle setting individual GPIO pin value: POST /gpio/{pin_name}
335
1/2
✓ Branch 0 taken 476 times.
✗ Branch 1 not taken.
476 std::string pinName = pieces[1];
336
1/2
✓ Branch 0 taken 476 times.
✗ Branch 1 not taken.
476 const PinCapabilities& pin = PinCapabilities::getPinByName(pinName);
337
338
3/4
✓ Branch 0 taken 476 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 251 times.
✓ Branch 3 taken 225 times.
476 if (pin.ptr()) {
339
1/2
✓ Branch 0 taken 251 times.
✗ Branch 1 not taken.
251 Json::Value data;
340
1/2
✓ Branch 0 taken 251 times.
✗ Branch 1 not taken.
251 Json::Value result;
341
342 // Parse POST data
343
3/6
✓ Branch 0 taken 251 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 251 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 251 times.
✗ Branch 5 not taken.
251 if (getRequestContent(req) != "") {
344
3/6
✓ Branch 0 taken 251 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 251 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 251 times.
251 if (!LoadJsonFromString(getRequestContent(req), data)) {
345 ✗ Json::Value errorResult;
346 ✗ errorResult["pin"] = pinName;
347 ✗ errorResult["Status"] = "ERROR";
348 ✗ errorResult["respCode"] = 400;
349 ✗ errorResult["Message"] = "Error parsing POST content";
350
351 ✗ std::string errorStr = SaveJsonToString(errorResult);
352 ✗ return makeStringResponse(errorStr, 400, "application/json");
353 ✗ }
354 }
355
356 // Check for required 'value' field
357
2/4
✓ Branch 0 taken 251 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 251 times.
251 if (!data.isMember("value")) {
358 ✗ Json::Value errorResult;
359 ✗ errorResult["pin"] = pinName;
360 ✗ errorResult["Status"] = "ERROR";
361 ✗ errorResult["respCode"] = 400;
362 ✗ errorResult["Message"] = "'value' field not specified. Use {\"value\": 0} or {\"value\": 1}";
363
364 ✗ std::string errorStr = SaveJsonToString(errorResult);
365 ✗ return makeStringResponse(errorStr, 400, "application/json");
366 ✗ }
367
368 251 int inputValue = 0;
369
3/4
✓ Branch 0 taken 251 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 250 times.
251 if (GPIOManager::INSTANCE.GetInputPinValue(pinName, inputValue)) {
370 // Reconfiguring it as an output releases the edge-event
371 // request and closes the fd the main loop watches: the input
372 // would go silently dead until fppd restarts.
373
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 Json::Value errorResult;
374
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 errorResult["pin"] = pinName;
375
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 errorResult["Status"] = "ERROR";
376
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 errorResult["respCode"] = 409;
377
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 errorResult["Message"] = "GPIO pin " + pinName + " is configured as a GPIO input";
378
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.
3 return makeStringResponse(SaveJsonToString(errorResult), 409, "application/json");
379 1 }
380
381 try {
382 // Configure the pin for output
383
3/6
✓ Branch 0 taken 250 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 250 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 250 times.
✗ Branch 5 not taken.
1000 pin.configPin("gpio", true, "GPIO Output");
384
385 // configPin() only warns when the line request fails, and setValue()
386 // on an unacquired line is a silent no-op that still updates the
387 // cached value -- so without this the request answers 200 "set
388 // successfully" for a pin it never drove, and the GET above then
389 // reports the cached value back as though it had taken.
390
3/4
✓ Branch 0 taken 250 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 248 times.
250 if (!pin.isAcquired()) {
391
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 Json::Value errorResult;
392
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 errorResult["pin"] = pinName;
393
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 errorResult["Status"] = "ERROR";
394
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 errorResult["respCode"] = 409;
395
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 errorResult["Message"] = "GPIO pin " + pinName + " could not be acquired; it is in use by another driver";
396
397
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 LogWarn(VB_HTTP, "POST /api/gpio/%s: line not acquired, refusing to report success\n", pinName.c_str());
398
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
4 std::string errorStr = SaveJsonToString(errorResult);
399
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 return makeStringResponse(errorStr, 409, "application/json");
400 2 }
401
402 // Set the value (convert to boolean)
403
10/18
✓ Branch 0 taken 248 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 248 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 121 times.
✓ Branch 5 taken 127 times.
✓ Branch 6 taken 121 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 121 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 121 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 121 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 121 times.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✓ Branch 17 taken 121 times.
248 bool value = data["value"].asBool() || (data["value"].isInt() && data["value"].asInt() != 0);
404
1/2
✓ Branch 0 taken 248 times.
✗ Branch 1 not taken.
248 pin.setValue(value);
405
406 // Update the last value tracking (for Opposite command functionality and GET requests)
407
1/2
✓ Branch 0 taken 248 times.
✗ Branch 1 not taken.
248 GPIOManager::SetCommandLastValue(pinName, value);
408
5/6
✓ Branch 0 taken 128 times.
✓ Branch 1 taken 120 times.
✓ Branch 2 taken 247 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 248 times.
✗ Branch 5 not taken.
248 LogDebug(VB_COMMAND, "POST /api/gpio/%s from %s: set to %d, cached value\n", pinName.c_str(), getEffectiveClientIP(req).c_str(), value ? 1 : 0);
409
410
2/4
✓ Branch 0 taken 248 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 248 times.
✗ Branch 3 not taken.
248 result["pin"] = pinName;
411
4/6
✓ Branch 0 taken 127 times.
✓ Branch 1 taken 121 times.
✓ Branch 2 taken 248 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 248 times.
✗ Branch 5 not taken.
248 result["value"] = value ? 1 : 0;
412
2/4
✓ Branch 0 taken 248 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 248 times.
✗ Branch 3 not taken.
248 result["Status"] = "OK";
413
2/4
✓ Branch 0 taken 248 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 248 times.
✗ Branch 3 not taken.
248 result["respCode"] = 200;
414
2/4
✓ Branch 0 taken 248 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 248 times.
✗ Branch 3 not taken.
248 result["Message"] = "GPIO pin value set successfully";
415
416
2/4
✓ Branch 0 taken 248 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 248 times.
✗ Branch 3 not taken.
496 std::string resultStr = SaveJsonToString(result);
417
2/4
✓ Branch 0 taken 248 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 248 times.
✗ Branch 3 not taken.
248 return makeStringResponse(resultStr, 200, "application/json");
418
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
248 } catch (const std::exception& e) {
419 ✗ Json::Value errorResult;
420 ✗ errorResult["pin"] = pinName;
421 ✗ errorResult["Status"] = "ERROR";
422 ✗ errorResult["respCode"] = 500;
423 ✗ errorResult["Message"] = std::string("Error setting GPIO pin: ") + e.what();
424
425 ✗ std::string errorStr = SaveJsonToString(errorResult);
426 ✗ return makeStringResponse(errorStr, 500, "application/json");
427 ✗ }
428 251 } else {
429
1/2
✓ Branch 0 taken 225 times.
✗ Branch 1 not taken.
225 Json::Value errorResult;
430
2/4
✓ Branch 0 taken 225 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 225 times.
✗ Branch 3 not taken.
225 errorResult["pin"] = pinName;
431
2/4
✓ Branch 0 taken 225 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 225 times.
✗ Branch 3 not taken.
225 errorResult["Status"] = "ERROR";
432
2/4
✓ Branch 0 taken 225 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 225 times.
✗ Branch 3 not taken.
225 errorResult["respCode"] = 404;
433
3/6
✓ Branch 0 taken 225 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 225 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 225 times.
✗ Branch 5 not taken.
225 errorResult["Message"] = "GPIO pin not found: " + pinName;
434
435
2/4
✓ Branch 0 taken 225 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 225 times.
✗ Branch 3 not taken.
450 std::string errorStr = SaveJsonToString(errorResult);
436
2/4
✓ Branch 0 taken 225 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 225 times.
✗ Branch 3 not taken.
225 return makeStringResponse(errorStr, 404, "application/json");
437 225 }
438 476 }
439
440
3/6
✓ Branch 0 taken 45 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 45 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 45 times.
✗ Branch 5 not taken.
180 return makeStringResponse("Not Found", 404, "text/plain");
441 521 }
442
443 1885 void GPIOManager::SetupGPIOInput(std::map<int, std::function<bool(int)>>& callbacks) {
444
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 LogDebug(VB_GPIO, "SetupGPIOInput()\n");
445
446 1885 int enabledCount = 0;
447
448
3/6
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1885 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1885 times.
✗ Branch 5 not taken.
1885 std::string file = FPP_DIR_CONFIG("/gpio.json");
449
3/4
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✓ Branch 3 taken 1872 times.
1885 if (FileExists(file)) {
450
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 Json::Value root;
451
3/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✓ Branch 3 taken 1 times.
13 if (LoadJsonFromFile(file, root, JsonRoot::Array)) {
452
3/4
✓ Branch 0 taken 27 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✓ Branch 3 taken 12 times.
27 for (int x = 0; x < root.size(); x++) {
453 // An input with a value of the wrong JSON type threw at
454 // startup, to std::terminate; skip that input instead. The
455 // state is owned here until addState() takes it, so a throw
456 // part way through frees it.
457 try {
458
2/4
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✗ Branch 3 not taken.
15 Json::Value v = root[x];
459
5/6
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 13 times.
✓ Branch 3 taken 1 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 13 times.
15 if (!v["enabled"].asBool()) {
460 ✗ continue;
461 }
462
5/14
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 13 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 13 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✓ Branch 11 taken 13 times.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
13 std::string pin = v.isMember("pin") ? v["pin"].asString() : "";
463
5/14
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 13 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 13 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✓ Branch 11 taken 13 times.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
13 std::string mode = v.isMember("mode") ? v["mode"].asString() : "gpio";
464
2/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
13 const PinCapabilities* pc = PinCapabilities::getPinByName(pin).ptr();
465
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 if (pc) {
466
2/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 13 times.
13 std::unique_ptr<GPIOState> owned(new GPIOState());
467 13 GPIOState* state = owned.get();
468
3/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 11 times.
13 if (v.isMember("debounceTime")) {
469
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 state->debounceTime = v["debounceTime"].asInt() * 1000;
470 }
471
472 // Load debounce edge setting: "both" (default), "rising", or "falling"
473
3/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 12 times.
13 if (v.isMember("debounceEdge")) {
474
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 std::string edge = v["debounceEdge"].asString();
475
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
1 if (edge == "rising") {
476 ✗ state->debounceEdge = GPIOState::DebounceEdge::Rising;
477
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
1 } else if (edge == "falling") {
478 ✗ state->debounceEdge = GPIOState::DebounceEdge::Falling;
479 } else {
480 1 state->debounceEdge = GPIOState::DebounceEdge::Both;
481 }
482 1 }
483
484 13 state->pin = pc;
485
486 // Parse rising/falling: accept both old single-object format and
487 // new array-of-commands format for backward compatibility.
488 39 auto parseActions = [](const Json::Value& jv, const std::string& key) -> std::vector<Json::Value> {
489 39 std::vector<Json::Value> result;
490
3/4
✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✓ Branch 3 taken 22 times.
39 if (!jv.isMember(key))
491 17 return result;
492
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
22 const Json::Value& actions = jv[key];
493
3/4
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✓ Branch 3 taken 8 times.
22 if (actions.isArray()) {
494
3/4
✓ Branch 0 taken 27 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✓ Branch 3 taken 14 times.
27 for (int i = 0; i < (int)actions.size(); i++) {
495
3/6
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 13 times.
✗ Branch 5 not taken.
13 std::string cmd = actions[i]["command"].asString();
496
4/8
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 13 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 13 times.
✗ Branch 7 not taken.
13 if (!cmd.empty() && cmd != "OLED Navigation")
497
2/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
13 result.push_back(actions[i]);
498 13 }
499
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
8 } else if (actions.isObject()) {
500
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
8 std::string cmd = actions["command"].asString();
501
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 (!cmd.empty() && cmd != "OLED Navigation")
502
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 result.push_back(actions);
503 8 }
504 22 return result;
505 ✗ };
506
507
2/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
26 state->risingActions = parseActions(v, "rising");
508
2/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
26 state->fallingActions = parseActions(v, "falling");
509
2/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
13 state->holdActions = parseActions(v, "hold");
510
511
3/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 11 times.
13 if (v.isMember("holdTime"))
512
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 state->holdTime = v["holdTime"].asInt();
513
514
2/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 13 times.
13 if (v.isMember("ledPin"))
515 ✗ state->ledPin = v["ledPin"].asString();
516
517
2/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 13 times.
13 if (v.isMember("ledActiveHigh"))
518 ✗ state->ledActiveHigh = v["ledActiveHigh"].asBool();
519
520 // LED idle mode — what the LED does when not in a trigger effect
521
2/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 13 times.
13 if (v.isMember("ledIdleMode")) {
522 ✗ std::string m = v["ledIdleMode"].asString();
523 ✗ if (m == "on") state->ledIdleMode = GPIOState::LEDIdleMode::On;
524 ✗ else if (m == "pulse") state->ledIdleMode = GPIOState::LEDIdleMode::Pulse;
525 ✗ else state->ledIdleMode = GPIOState::LEDIdleMode::Off;
526 ✗ }
527
2/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 13 times.
13 if (v.isMember("ledPulseRateMs"))
528 ✗ state->ledPulseRateMs = std::max(50u, v["ledPulseRateMs"].asUInt());
529
530 // LED trigger mode — what happens to the LED when the button fires
531
2/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 13 times.
13 if (v.isMember("ledTriggerMode")) {
532 ✗ std::string m = v["ledTriggerMode"].asString();
533 ✗ if (m == "none") state->ledTriggerMode = GPIOState::LEDTriggerMode::None;
534 ✗ else if (m == "flash") state->ledTriggerMode = GPIOState::LEDTriggerMode::Flash;
535 ✗ else if (m == "timed_on") state->ledTriggerMode = GPIOState::LEDTriggerMode::TimedOn;
536 ✗ else state->ledTriggerMode = GPIOState::LEDTriggerMode::FollowInput;
537 ✗ }
538
2/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 13 times.
13 if (v.isMember("ledTriggerParam"))
539 ✗ state->ledTriggerParam = v["ledTriggerParam"].asUInt();
540
541 // Re-enable mode — controls how long input is suppressed post-trigger.
542
2/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 13 times.
13 if (v.isMember("reEnableMode")) {
543 ✗ std::string m = v["reEnableMode"].asString();
544 ✗ if (m == "timed")
545 ✗ state->reEnableMode = GPIOState::ReEnableMode::Timed;
546 ✗ else if (m == "player_idle")
547 ✗ state->reEnableMode = GPIOState::ReEnableMode::OnPlayerIdle;
548 else
549 ✗ state->reEnableMode = GPIOState::ReEnableMode::Always;
550 ✗ }
551
2/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 13 times.
13 if (v.isMember("reEnableDelay"))
552 ✗ state->reEnableDelay = v["reEnableDelay"].asUInt();
553
554 13 bool hasAnyAction = !state->risingActions.empty() ||
555
3/4
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 11 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
13 !state->fallingActions.empty() ||
556 ✗ !state->holdActions.empty();
557
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 if (hasAnyAction) {
558
2/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
26 state->pin->configPin(mode, false, "GPIO Input");
559
560 // configPin() only warns when the line request fails, which
561 // it does for a pin the kernel has given to a peripheral --
562 // under strict pinmux that is permanent. getValue() then
563 // returns a flat 0 forever, so the input is polled every
564 // main loop and can never change: silently dead, with
565 // nothing but a warning to say why. Say so once, plainly,
566 // and do not spend a poll slot on it.
567
3/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 12 times.
13 if (!state->pin->isAcquired()) {
568
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogWarn(VB_GPIO, "GPIO Input %s: the line could not be acquired, so it cannot be read - input disabled\n",
569 pin.c_str());
570 1 continue;
571 }
572
573
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 addState(owned.release());
574 12 enabledCount++;
575 }
576
2/2
✓ Branch 0 taken 12 times.
✓ Branch 1 taken 1 times.
13 }
577
7/8
✓ Branch 0 taken 12 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 12 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 12 times.
✓ Branch 5 taken 1 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 2 times.
19 } catch (const std::exception& e) {
578
3/6
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
6 WarningHolder::InvalidConfigEntry("gpio.json", e.what());
579 2 }
580 }
581 }
582 13 }
583
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 LogDebug(VB_GPIO, "%d GPIO Input(s) enabled\n", enabledCount);
584
585 // Apply initial LED idle state for all configured inputs.
586
3/4
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 11 times.
✓ Branch 3 taken 1885 times.
1896 for (auto* s : eventStates) s->initLED();
587
3/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✓ Branch 3 taken 1885 times.
1888 for (auto* s : pollStates) s->initLED();
588 1885 }
589
590 // Event callback uses settle-then-fire logic.
591 // On a debounced edge, we record pendingValue/pendingTime and schedule a
592 // timer to re-check after the settle window. Non-debounced edges fire immediately.
593 11 void GPIOManager::addGPIOCallback(GPIOState* a) {
594 41 std::function<bool(int)> f = [a, this](int i) {
595 19 int v = a->pin->readEventFromFile();
596
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 19 times.
19 if (v < 0) {
597 ✗ return false;
598 }
599
2/2
✓ Branch 0 taken 18 times.
✓ Branch 1 taken 1 times.
19 if (v != a->lastValue) {
600
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 18 times.
18 if (!a->shouldDebounce(v)) {
601 // No debounce on this edge — fire immediately.
602 ✗ a->doAction(v);
603 } else {
604 // Settle-then-fire: start the settle timer if this is a new direction.
605
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 if (v != a->pendingValue) {
606 18 a->pendingValue = v;
607 18 a->pendingTime = GetMonotonicTimeMicros();
608 }
609 18 scheduleDebounceCheck(a);
610 }
611 }
612 19 return false;
613 11 };
614
1/2
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
11 EPollManager::INSTANCE.addFileDescriptor(a->file, f);
615 11 }
616
617 3 void GPIOManager::AddGPIOCallback(const PinCapabilities* pin, const std::function<bool(int)>& cb) {
618
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 GPIOState* state = new GPIOState();
619 3 state->pin = pin;
620 3 state->callback = cb;
621 3 state->hasCallback = true;
622 3 addState(state);
623 3 }
624
625 3 void GPIOManager::RemoveGPIOCallback(const PinCapabilities* pin) {
626
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 for (auto it = eventStates.begin(); it != eventStates.end(); ++it) {
627 ✗ GPIOState* a = *it;
628 ✗ if (a->pin == pin) {
629 ✗ if (a->file != -1) {
630 ✗ EPollManager::INSTANCE.removeFileDescriptor(a->file);
631 ✗ a->pin->releaseGPIOD();
632 }
633 {
634 ✗ std::lock_guard<std::mutex> lock(s_statesLock);
635 ✗ eventStates.erase(it);
636 ✗ delete a;
637 ✗ }
638 ✗ return;
639 }
640 }
641
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 for (auto it = pollStates.begin(); it != pollStates.end(); ++it) {
642 3 GPIOState* a = *it;
643
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 if (a->pin == pin) {
644 {
645
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 std::lock_guard<std::mutex> lock(s_statesLock);
646 3 pollStates.erase(it);
647
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 delete a;
648 3 }
649 3 return;
650 }
651 }
652 }
653
654 // pendingValue is initialised to match lastValue so no spurious settle
655 // is pending at boot.
656 15 void GPIOManager::addState(GPIOState* state) {
657 15 int currentVal = state->pin->getValue();
658 15 state->lastValue = currentVal;
659 15 state->futureValue = currentVal; // kept for compatibility
660 15 state->pendingValue = currentVal; // no pending transition at startup
661 15 state->pendingTime = 0;
662 15 state->lastTriggerTime = 0;
663 15 state->file = -1;
664
665
2/2
✓ Branch 0 taken 11 times.
✓ Branch 1 taken 4 times.
15 if (state->pin->supportsGpiod()) {
666 // Request events for any edge that has actions or is needed for hold detection.
667
3/4
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 10 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
11 bool wantRising = !state->risingActions.empty() || !state->holdActions.empty();
668
3/4
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 7 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 4 times.
11 bool wantFalling = !state->fallingActions.empty() || !state->holdActions.empty();
669 // Edge detection is stateful: doAction() only fires when the read value
670 // differs from lastValue. If we only listen for one edge, lastValue latches
671 // after the first trigger and is never reset (the opposite edge never
672 // arrives), so the input fires exactly once and then goes dead until fppd
673 // restarts. Whenever a command-action pin wants either edge, monitor BOTH so
674 // lastValue stays in sync; doAction() already no-ops the edge with no actions.
675 // Raw callback registrations (no actions) keep their prior behavior.
676
3/4
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 10 times.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
11 if (wantRising || wantFalling) {
677 11 wantRising = wantFalling = true;
678 }
679 // A raw callback registration (eFuse status lines, etc.) has no actions,
680 // so neither edge is "wanted" above. Requesting a line with no edge
681 // detection still succeeds under libgpiod v2 and hands back an fd that
682 // never becomes readable, which parked the callback in eventStates
683 // where it could never fire. (Under v1 that request had no event fd, so
684 // it fell through to polling and worked.) Ask for both edges; if the
685 // line cannot do events, requestEventFile() returns -1 and the state
686 // lands in the polling list as before.
687
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 11 times.
11 if (state->hasCallback) {
688 ✗ wantRising = wantFalling = true;
689 }
690 11 state->file = state->pin->requestEventFile(wantRising, wantFalling);
691 } else {
692 4 state->file = -1;
693 }
694
695
2/2
✓ Branch 0 taken 11 times.
✓ Branch 1 taken 4 times.
15 if (state->file > 0) {
696 {
697
1/2
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
11 std::lock_guard<std::mutex> lock(s_statesLock);
698
1/2
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
11 eventStates.push_back(state);
699 11 }
700 11 addGPIOCallback(state);
701 } else {
702
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 std::lock_guard<std::mutex> lock(s_statesLock);
703
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 pollStates.push_back(state);
704 4 }
705 15 }
706
707 20 void GPIOManager::GPIOState::doAction(int v) {
708 20 LogDebug(VB_GPIO, "GPIO %s triggered. Value: %d\n", pin->name.c_str(), v);
709 // Publish GPIO edge events to MQTT
710
2/2
✓ Branch 0 taken 12 times.
✓ Branch 1 taken 8 times.
20 if (v == 1) {
711
2/4
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
12 std::string risingTopic = "gpio/" + pin->name + "/rising";
712
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 Events::Publish(risingTopic, 1);
713 12 } else {
714
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
8 std::string fallingTopic = "gpio/" + pin->name + "/falling";
715
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 Events::Publish(fallingTopic, 0);
716 8 }
717 // When the input is suppressed post-trigger, skip all commands and LED
718 // effects — the LED is already parked off by startLEDIdle() and should
719 // stay that way until the input is re-enabled.
720
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 20 times.
20 if (inputDisabled) {
721 ✗ lastTriggerTime = GetTime();
722 ✗ lastValue = v;
723 ✗ futureValue = v;
724 ✗ pendingValue = v;
725 ✗ pendingTime = 0;
726 ✗ return;
727 }
728
729
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 18 times.
20 if (hasCallback) {
730 2 callback(v);
731 } else {
732
2/2
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 8 times.
18 if (v == 1) {
733 // Rising edge — run rising commands, then schedule hold timer if needed.
734
2/2
✓ Branch 0 taken 12 times.
✓ Branch 1 taken 10 times.
22 for (auto& action : risingActions) {
735
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 LogDebug(VB_COMMAND, "GPIO %s rising edge running command \"%s\"\n", pin->name.c_str(), action["command"].asString().c_str());
736
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 CommandManager::INSTANCE.run(action);
737 }
738 10 holdFired = false;
739 10 holdStartTime = GetTimeMS();
740
5/6
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 8 times.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 8 times.
10 if (holdTime > 0 && !holdActions.empty()) {
741 2 long long scheduled = holdStartTime;
742
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 Timers::INSTANCE.addTimer(
743
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
4 "gpio_hold_" + pin->name,
744
1/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
2 GetTimeMS() + holdTime,
745
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
6 [this, scheduled]() { checkHoldTimer(scheduled); });
746 }
747 // Suppress the input until re-enable condition is met.
748
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 10 times.
10 if (reEnableMode != ReEnableMode::Always) {
749 ✗ inputDisabled = true;
750 ✗ scheduleReEnable();
751 }
752 } else {
753 // Falling edge — cancel any pending hold (by resetting holdStartTime),
754 // then run falling commands only if hold has not already fired.
755 8 bool wasHeld = holdFired;
756 8 holdStartTime = 0;
757 8 holdFired = false;
758
2/2
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 1 times.
8 if (!wasHeld) {
759
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 7 times.
10 for (auto& action : fallingActions) {
760
3/6
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
3 LogDebug(VB_COMMAND, "GPIO %s falling edge running command \"%s\"\n", pin->name.c_str(), action["command"].asString().c_str());
761
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 CommandManager::INSTANCE.run(action);
762 }
763 }
764 }
765
766 // Drive the associated LED output if one is configured.
767 18 triggerLEDEffect(v);
768 }
769 20 lastTriggerTime = GetTime();
770 20 lastValue = v;
771 20 futureValue = v;
772 // Clear pending state now that the action has fired.
773 20 pendingValue = v;
774 20 pendingTime = 0;
775 }
776
777 // Called by the hold timer. Only fires hold actions when the button is still
778 // pressed and belongs to the same press session that scheduled this timer.
779 2 void GPIOManager::GPIOState::checkHoldTimer(long long scheduledStart) {
780
4/6
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 1 times.
2 if (holdStartTime != scheduledStart || holdFired || lastValue != 1)
781 1 return;
782 1 LogDebug(VB_GPIO, "GPIO %s hold timer fired.\n", pin->name.c_str());
783
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
2 for (auto& action : holdActions) {
784
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 LogDebug(VB_COMMAND, "GPIO %s hold running command \"%s\"\n", pin->name.c_str(), action["command"].asString().c_str());
785
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 CommandManager::INSTANCE.run(action);
786 }
787 1 holdFired = true;
788 }
789
790 // Returns true if debounce should apply to this edge.
791 // v == 1 is rising, v == 0 is falling.
792 23 bool GPIOManager::GPIOState::shouldDebounce(int v) const {
793
1/3
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✓ Branch 2 taken 23 times.
23 switch (debounceEdge) {
794 ✗ case DebounceEdge::Rising:
795 ✗ return (v == 1);
796 ✗ case DebounceEdge::Falling:
797 ✗ return (v == 0);
798 23 case DebounceEdge::Both:
799 default:
800 23 return true;
801 }
802 }
803
804 // ── LED helpers ──────────────────────────────────────────────────────────────
805
806 ✗ void GPIOManager::GPIOState::setLEDState(bool on) {
807 ✗ if (ledPin.empty()) return;
808 ✗ const PinCapabilities* ledCap = PinCapabilities::getPinByName(ledPin).ptr();
809 ✗ if (!ledCap) return;
810 ✗ bool pinState = on ? ledActiveHigh : !ledActiveHigh;
811 ✗ ledCap->configPin("gpio", true);
812 ✗ ledCap->setValue(pinState);
813 ✗ GPIOManager::SetCommandLastValue(ledPin, pinState);
814 }
815
816 ✗ void GPIOManager::GPIOState::stopLEDIdle() {
817 ✗ if (ledPin.empty()) return;
818 ✗ Timers::INSTANCE.stopPeriodicTimer("gpio_led_pulse_" + pin->name);
819 }
820
821 ✗ void GPIOManager::GPIOState::startLEDIdle() {
822 ✗ if (ledPin.empty()) return;
823 ✗ stopLEDIdle();
824 ✗ if (inputDisabled) {
825 // Input is suppressed — hold the LED off so it visually reflects
826 // that the input is not yet ready for another trigger.
827 ✗ setLEDState(false);
828 ✗ return;
829 }
830 ✗ switch (ledIdleMode) {
831 ✗ case LEDIdleMode::Off:
832 ✗ setLEDState(false);
833 ✗ break;
834 ✗ case LEDIdleMode::On:
835 ✗ setLEDState(true);
836 ✗ break;
837 ✗ case LEDIdleMode::Pulse:
838 ✗ ledPulseState = false;
839 ✗ setLEDState(false);
840 ✗ Timers::INSTANCE.addPeriodicTimer(
841 ✗ "gpio_led_pulse_" + pin->name,
842 ✗ ledPulseRateMs,
843 ✗ [this]() {
844 ✗ ledPulseState = !ledPulseState;
845 ✗ setLEDState(ledPulseState);
846 ✗ });
847 ✗ break;
848 }
849 }
850
851 14 void GPIOManager::GPIOState::initLED() {
852
1/2
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
14 if (ledPin.empty()) return;
853 // Start in idle state; trigger effects will override when the button fires.
854 ✗ startLEDIdle();
855 }
856
857 18 void GPIOManager::GPIOState::triggerLEDEffect(int v) {
858
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 if (ledPin.empty()) return;
859 ✗ switch (ledTriggerMode) {
860 ✗ case LEDTriggerMode::None:
861 // LED ignores trigger events; idle mode drives it.
862 ✗ break;
863 ✗ case LEDTriggerMode::FollowInput:
864 ✗ stopLEDIdle();
865 ✗ setLEDState(v == 1);
866 ✗ if (v == 0) startLEDIdle(); // restore idle when released
867 ✗ break;
868 ✗ case LEDTriggerMode::Flash:
869 ✗ if (v == 1) {
870 ✗ stopLEDIdle();
871 ✗ uint32_t count = (ledTriggerParam > 0) ? ledTriggerParam : 3;
872 ✗ doLEDFlash((int)(count * 2)); // 2 half-periods per flash
873 }
874 ✗ break;
875 ✗ case LEDTriggerMode::TimedOn:
876 ✗ if (v == 1) {
877 ✗ stopLEDIdle();
878 ✗ setLEDState(true);
879 ✗ uint32_t dur = (ledTriggerParam > 0) ? ledTriggerParam : 1000;
880 ✗ Timers::INSTANCE.addTimer(
881 ✗ "gpio_led_timed_" + pin->name,
882 ✗ GetTimeMS() + dur,
883 ✗ [this]() { startLEDIdle(); });
884 }
885 ✗ break;
886 }
887 }
888
889 // ── Re-enable helpers ────────────────────────────────────────────────────────
890
891 ✗ void GPIOManager::GPIOState::scheduleReEnable() {
892 ✗ std::string timerName = "gpio_reenable_" + pin->name;
893 ✗ switch (reEnableMode) {
894 ✗ case ReEnableMode::Always:
895 ✗ inputDisabled = false;
896 ✗ break;
897 ✗ case ReEnableMode::Timed: {
898 ✗ uint32_t delay = (reEnableDelay > 0) ? reEnableDelay : 1000;
899 ✗ Timers::INSTANCE.addTimer(timerName, GetTimeMS() + delay, [this]() {
900 ✗ inputDisabled = false;
901 ✗ startLEDIdle();
902 ✗ LogDebug(VB_GPIO, "GPIO %s re-enabled after timed delay.\n", pin->name.c_str());
903 ✗ });
904 ✗ break;
905 }
906 ✗ case ReEnableMode::OnPlayerIdle: {
907 // Give the player 200 ms to start before polling.
908 ✗ long long startMs = GetMonotonicTimeMS();
909 ✗ Timers::INSTANCE.addTimer(timerName, GetTimeMS() + 200, [this, startMs]() {
910 ✗ checkReEnable(startMs, false);
911 ✗ });
912 ✗ break;
913 }
914 }
915 ✗ }
916
917 ✗ void GPIOManager::GPIOState::checkReEnable(long long startMs, bool playerHasStarted) {
918 ✗ if (!inputDisabled) return;
919 ✗ std::string timerName = "gpio_reenable_" + pin->name;
920 // startMs and now are monotonic (a wall-clock step back held the 2 s
921 // fallback off, and the input stayed disabled); timer deadlines are
922 // GetTimeMS() as Timers takes them.
923 ✗ long long now = GetMonotonicTimeMS();
924 ✗ PlaylistStatus status = Player::INSTANCE.GetStatus();
925
926 ✗ if (status != FPP_STATUS_IDLE) {
927 // Player is active — keep polling every 500 ms.
928 ✗ Timers::INSTANCE.addTimer(timerName, GetTimeMS() + 500, [this, startMs]() {
929 ✗ checkReEnable(startMs, true);
930 ✗ });
931 ✗ } else if (playerHasStarted) {
932 // Player started and has returned to idle — safe to re-enable.
933 ✗ inputDisabled = false;
934 ✗ startLEDIdle();
935 ✗ LogDebug(VB_GPIO, "GPIO %s re-enabled: player returned to idle.\n", pin->name.c_str());
936 ✗ } else if (now - startMs > 2000) {
937 // Player never started within 2 s (action wasn't a playlist) — re-enable anyway.
938 ✗ inputDisabled = false;
939 ✗ startLEDIdle();
940 ✗ LogDebug(VB_GPIO, "GPIO %s re-enabled: player-idle timeout.\n", pin->name.c_str());
941 } else {
942 // Still waiting for player to start — check again soon.
943 ✗ Timers::INSTANCE.addTimer(timerName, GetTimeMS() + 200, [this, startMs]() {
944 ✗ checkReEnable(startMs, false);
945 ✗ });
946 }
947 ✗ }
948
949 // Countdown-based LED flash: halfPeriodsRemaining decrements each 150 ms half-period.
950 // Even remaining → LED on; odd → LED off. When 0 → restore idle.
951 ✗ void GPIOManager::GPIOState::doLEDFlash(int halfPeriodsRemaining) {
952 ✗ if (halfPeriodsRemaining <= 0) {
953 ✗ startLEDIdle();
954 ✗ return;
955 }
956 ✗ setLEDState(halfPeriodsRemaining % 2 == 0);
957 ✗ Timers::INSTANCE.addTimer(
958 ✗ "gpio_led_flash_" + pin->name,
959 ✗ GetTimeMS() + 150,
960 ✗ [this, halfPeriodsRemaining]() { doLEDFlash(halfPeriodsRemaining - 1); });
961 }
962