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