A.R.G.U.S 2.1.0
Adaptive Real-Time Guardian for Unsafe Situations
Loading...
Searching...
No Matches
PhysicalButtonModule.cpp
Go to the documentation of this file.
1
7
8#include <algorithm>
9#include <array>
10#include <cerrno>
11#include <cctype>
12#include <cstdint>
13#include <cstdlib>
14#include <cstring>
15#include <dirent.h>
16#include <fcntl.h>
17#include <limits>
18#include <string>
19#include <vector>
20
21#include <linux/gpio.h>
22#include <poll.h>
23#include <sys/ioctl.h>
24#include <unistd.h>
25
26namespace {
27
28constexpr int kDefaultAckGpio = 24;
29constexpr const char* kButtonConsumer = "ARGUS_BUTTON";
30
31bool parseBoolText(const char* text, bool default_value) noexcept {
32 if (text == nullptr) {
33 return default_value;
34 }
35
36 std::string value(text);
37 for (char& ch : value) {
38 ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
39 }
40
41 if (value == "1" || value == "true" || value == "yes" || value == "on") {
42 return true;
43 }
44 if (value == "0" || value == "false" || value == "no" || value == "off") {
45 return false;
46 }
47 return default_value;
48}
49
50std::string toLowerCopy(std::string text) {
51 for (char& ch : text) {
52 ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
53 }
54 return text;
55}
56
57bool containsInsensitive(const std::string& haystack, const char* needle) {
58 if (needle == nullptr || *needle == '\0') {
59 return false;
60 }
61
62 const std::string haystack_lower = toLowerCopy(haystack);
63 const std::string needle_lower = toLowerCopy(needle);
64 return haystack_lower.find(needle_lower) != std::string::npos;
65}
66
67std::string boundedCString(const char* text, std::size_t max_length) {
68 if (text == nullptr) {
69 return {};
70 }
71
72 return std::string(text, ::strnlen(text, max_length));
73}
74
75bool equalsInsensitive(const std::string& lhs, const std::string& rhs) {
76 return toLowerCopy(lhs) == toLowerCopy(rhs);
77}
78
79struct GpioChipCandidate {
80 std::string path;
81 std::string name;
82 std::string label;
83 unsigned int lines = 0;
84 int preference = 1;
85};
86
87std::vector<std::string> buildLineNameCandidates(int gpio) {
88 return {
89 "GPIO" + std::to_string(gpio),
90 "gpio" + std::to_string(gpio),
91 };
92}
93
94int chipPreference(const GpioChipCandidate& candidate) {
95 if (containsInsensitive(candidate.label, "pinctrl") ||
96 containsInsensitive(candidate.label, "rp1") ||
97 containsInsensitive(candidate.name, "pinctrl") ||
98 containsInsensitive(candidate.name, "rp1") ||
99 containsInsensitive(candidate.name, "bcm")) {
100 return 0;
101 }
102 return 1;
103}
104
105std::vector<GpioChipCandidate> enumerateGpioChips() {
106 std::vector<GpioChipCandidate> candidates;
107
108 DIR* dir = ::opendir("/dev");
109 if (dir == nullptr) {
110 return candidates;
111 }
112
113 while (dirent* entry = ::readdir(dir)) {
114 if (std::strncmp(entry->d_name, "gpiochip", 8) != 0) {
115 continue;
116 }
117
118 std::string path = "/dev/";
119 path += entry->d_name;
120
121 int chip_fd = ::open(path.c_str(), O_RDONLY | O_CLOEXEC);
122 if (chip_fd < 0) {
123 continue;
124 }
125
126 gpiochip_info info{};
127 if (::ioctl(chip_fd, GPIO_GET_CHIPINFO_IOCTL, &info) == 0) {
128 GpioChipCandidate candidate;
129 candidate.path = path;
130 candidate.name = boundedCString(info.name, sizeof(info.name));
131 candidate.label = boundedCString(info.label, sizeof(info.label));
132 candidate.lines = info.lines;
133 candidate.preference = chipPreference(candidate);
134 candidates.push_back(candidate);
135 }
136
137 ::close(chip_fd);
138 }
139
140 ::closedir(dir);
141
142 std::stable_sort(candidates.begin(),
143 candidates.end(),
144 [](const GpioChipCandidate& lhs, const GpioChipCandidate& rhs) {
145 if (lhs.preference != rhs.preference) {
146 return lhs.preference < rhs.preference;
147 }
148 return lhs.path < rhs.path;
149 });
150 return candidates;
151}
152
153std::uint64_t makePreferredLineFlags(bool active_low) {
154 std::uint64_t flags = static_cast<std::uint64_t>(GPIO_V2_LINE_FLAG_INPUT) |
155 static_cast<std::uint64_t>(GPIO_V2_LINE_FLAG_EDGE_RISING) |
156 static_cast<std::uint64_t>(GPIO_V2_LINE_FLAG_EDGE_FALLING);
157 if (active_low) {
158 flags |= static_cast<std::uint64_t>(GPIO_V2_LINE_FLAG_ACTIVE_LOW);
159 flags |= static_cast<std::uint64_t>(GPIO_V2_LINE_FLAG_BIAS_PULL_UP);
160 } else {
161 flags |= static_cast<std::uint64_t>(GPIO_V2_LINE_FLAG_BIAS_PULL_DOWN);
162 }
163 return flags;
164}
165
166std::uint64_t makeFallbackLineFlags(bool active_low) {
167 std::uint64_t flags = static_cast<std::uint64_t>(GPIO_V2_LINE_FLAG_INPUT) |
168 static_cast<std::uint64_t>(GPIO_V2_LINE_FLAG_EDGE_RISING) |
169 static_cast<std::uint64_t>(GPIO_V2_LINE_FLAG_EDGE_FALLING);
170 if (active_low) {
171 flags |= static_cast<std::uint64_t>(GPIO_V2_LINE_FLAG_ACTIVE_LOW);
172 }
173 return flags;
174}
175
176bool requestLineFromChip(const std::string& chip_path,
177 int line_offset,
178 std::uint64_t flags,
179 int& line_fd,
180 std::string& error_text) noexcept {
181 line_fd = -1;
182
183 int chip_fd = ::open(chip_path.c_str(), O_RDWR | O_CLOEXEC);
184 if (chip_fd < 0) {
185 error_text = chip_path + ": open failed: " + std::strerror(errno);
186 return false;
187 }
188
189 gpio_v2_line_request request{};
190 request.offsets[0] = static_cast<std::uint32_t>(line_offset);
191 request.num_lines = 1;
192 request.config.flags = flags;
193 request.config.num_attrs = 0;
194 std::memset(request.consumer, 0, sizeof(request.consumer));
195 std::strncpy(request.consumer, kButtonConsumer, sizeof(request.consumer) - 1);
196
197 if (::ioctl(chip_fd, GPIO_V2_GET_LINE_IOCTL, &request) == 0 &&
198 request.fd >= 0) {
199 line_fd = request.fd;
200 ::close(chip_fd);
201 return true;
202 }
203
204 const int saved_errno = errno;
205 error_text = chip_path + " line " + std::to_string(line_offset) +
206 ": " + std::strerror(saved_errno);
207 ::close(chip_fd);
208 return false;
209}
210
211std::optional<int> findNamedLineOffset(const GpioChipCandidate& chip, int gpio) {
212 const std::vector<std::string> target_names = buildLineNameCandidates(gpio);
213
214 int chip_fd = ::open(chip.path.c_str(), O_RDONLY | O_CLOEXEC);
215 if (chip_fd < 0) {
216 return std::nullopt;
217 }
218
219 std::optional<int> resolved_offset;
220 for (unsigned int offset = 0; offset < chip.lines; ++offset) {
221 gpio_v2_line_info info{};
222 info.offset = offset;
223 if (::ioctl(chip_fd, GPIO_V2_GET_LINEINFO_IOCTL, &info) != 0) {
224 continue;
225 }
226
227 const std::string line_name = boundedCString(info.name, sizeof(info.name));
228 for (const std::string& target_name : target_names) {
229 if (equalsInsensitive(line_name, target_name)) {
230 resolved_offset = static_cast<int>(offset);
231 break;
232 }
233 }
234
235 if (resolved_offset.has_value()) {
236 break;
237 }
238 }
239
240 ::close(chip_fd);
241 return resolved_offset;
242}
243
244bool requestButtonLine(int gpio,
245 bool active_low,
246 std::string& device_path,
247 int& line_fd,
248 std::string& error_text) {
249 const std::vector<GpioChipCandidate> chips = enumerateGpioChips();
250 if (chips.empty()) {
251 error_text = "no /dev/gpiochip* devices found";
252 return false;
253 }
254
255 const std::uint64_t preferred_flags = makePreferredLineFlags(active_low);
256 const std::uint64_t fallback_flags = makeFallbackLineFlags(active_low);
257
258 auto tryResolvedRequest = [&](const GpioChipCandidate& chip,
259 int requested_offset,
260 const char* resolution_label) {
261 std::string attempt_error;
262 if (requestLineFromChip(chip.path,
263 requested_offset,
264 preferred_flags,
265 line_fd,
266 attempt_error) ||
267 requestLineFromChip(chip.path,
268 requested_offset,
269 fallback_flags,
270 line_fd,
271 attempt_error)) {
272 device_path = chip.path + " " + resolution_label + " GPIO" +
273 std::to_string(gpio) + " (offset " +
274 std::to_string(requested_offset) + ")";
275 error_text.clear();
276 return true;
277 }
278
279 error_text = attempt_error;
280 return false;
281 };
282
283 for (const GpioChipCandidate& chip : chips) {
284 const std::optional<int> named_offset = findNamedLineOffset(chip, gpio);
285 if (!named_offset.has_value()) {
286 continue;
287 }
288
289 if (tryResolvedRequest(chip, *named_offset, "named")) {
290 return true;
291 }
292 }
293
294 for (const GpioChipCandidate& chip : chips) {
295 if (gpio < 0 || static_cast<unsigned int>(gpio) >= chip.lines) {
296 continue;
297 }
298
299 if (tryResolvedRequest(chip, gpio, "raw-offset")) {
300 return true;
301 }
302 }
303
304 if (error_text.empty()) {
305 error_text = "unable to request GPIO" + std::to_string(gpio) +
306 " on any gpiochip device";
307 }
308
309 return false;
310}
311
312} // namespace
313
316
318 if (arm_channel_.line_fd >= 0) {
319 ::close(arm_channel_.line_fd);
320 arm_channel_.line_fd = -1;
321 }
322 if (disarm_channel_.line_fd >= 0) {
323 ::close(disarm_channel_.line_fd);
324 disarm_channel_.line_fd = -1;
325 }
326 if (acknowledge_channel_.line_fd >= 0) {
327 ::close(acknowledge_channel_.line_fd);
328 acknowledge_channel_.line_fd = -1;
329 }
330}
331
333 : config_(config) {
334 initialiseChannel(arm_channel_, config_.arm_gpio, PhysicalButtonEvent::ARM_REQUEST);
335 initialiseChannel(disarm_channel_,
336 config_.disarm_gpio,
338 initialiseChannel(acknowledge_channel_,
339 config_.acknowledge_gpio,
341
342 available_ = acknowledge_channel_.active;
343 if (acknowledge_channel_.active) {
344 status_string_ = acknowledge_channel_.device_path +
345 ", initial=" +
346 std::string(acknowledge_channel_.stable_pressed ? "PRESSED"
347 : "RELEASED") +
348 ", debounce=" + std::to_string(config_.debounce.count()) +
349 "ms";
350 }
351 if (!available_ && last_error_.empty()) {
352 last_error_ = "acknowledge button not configured";
353 }
354}
355
356bool PhysicalButtonModule::available() const noexcept {
357 return available_;
358}
359
361 const auto now = std::chrono::steady_clock::now();
362
363 return sampleChannel(disarm_channel_, now, event) ||
364 sampleChannel(acknowledge_channel_, now, event) ||
365 sampleChannel(arm_channel_, now, event);
366}
367
369 std::chrono::milliseconds timeout) noexcept {
370 std::array<ChannelState*, 3> channels{
371 &disarm_channel_,
372 &acknowledge_channel_,
373 &arm_channel_,
374 };
375 std::vector<pollfd> fds;
376 std::vector<ChannelState*> ready_channels;
377 fds.reserve(channels.size());
378 ready_channels.reserve(channels.size());
379
380 for (ChannelState* channel : channels) {
381 if (channel == nullptr || !channel->active || channel->line_fd < 0) {
382 continue;
383 }
384
385 pollfd fd{};
386 fd.fd = channel->line_fd;
387 fd.events = POLLIN;
388 fds.push_back(fd);
389 ready_channels.push_back(channel);
390 }
391
392 if (fds.empty()) {
393 return false;
394 }
395
396 int timeout_ms = -1;
397 if (timeout.count() >= 0) {
398 const long long requested_timeout = timeout.count();
399 timeout_ms = static_cast<int>(
400 std::min<long long>(requested_timeout, std::numeric_limits<int>::max()));
401 }
402
403 const int ready = ::poll(fds.data(), static_cast<nfds_t>(fds.size()), timeout_ms);
404 if (ready <= 0) {
405 return false;
406 }
407
408 const auto now = std::chrono::steady_clock::now();
409 for (std::size_t i = 0; i < fds.size(); ++i) {
410 if ((fds[i].revents & POLLIN) == 0) {
411 continue;
412 }
413
414 ChannelState& channel = *ready_channels[i];
415 gpio_v2_line_event line_event{};
416 const ssize_t read_bytes = ::read(channel.line_fd, &line_event, sizeof(line_event));
417 if (read_bytes != static_cast<ssize_t>(sizeof(line_event))) {
418 continue;
419 }
420
421 bool pressed = false;
422 if (line_event.id == GPIO_V2_LINE_EVENT_RISING_EDGE) {
423 pressed = true;
424 } else if (line_event.id == GPIO_V2_LINE_EVENT_FALLING_EDGE) {
425 pressed = false;
426 } else {
427 continue;
428 }
429
430 if (pressed == channel.stable_pressed) {
431 continue;
432 }
433
434 const auto since_last_transition = now - channel.last_transition;
435 if (since_last_transition < config_.debounce) {
436 continue;
437 }
438
439 channel.last_transition = now;
440 channel.last_sample_pressed = pressed;
441 channel.stable_pressed = pressed;
442 if (pressed) {
443 event = channel.event;
444 return true;
445 }
446 }
447
448 return false;
449}
450
452 if (!acknowledge_channel_.active) {
453 return false;
454 }
455
456 if (!readPressedState(acknowledge_channel_, pressed)) {
457 last_error_ = std::string("unable to read ") + acknowledge_channel_.device_path +
458 " (" + acknowledge_channel_.value_path + ")";
459 return false;
460 }
461
462 return true;
463}
464
465const char* PhysicalButtonModule::lastErrorString() const noexcept {
466 if (last_error_.empty()) {
467 return "no error";
468 }
469 return last_error_.c_str();
470}
471
472const char* PhysicalButtonModule::statusString() const noexcept {
473 if (status_string_.empty()) {
474 return "no status";
475 }
476 return status_string_.c_str();
477}
478
481 config.arm_gpio = parseEnvInt("ARGUS_BUTTON_ARM_GPIO");
482 config.disarm_gpio = parseEnvInt("ARGUS_BUTTON_DISARM_GPIO");
483 config.acknowledge_gpio =
484 parseEnvInt("ARGUS_BUTTON_ACK_GPIO").value_or(kDefaultAckGpio);
485 config.active_low = parseEnvBool("ARGUS_BUTTON_ACTIVE_LOW", true);
486 config.debounce =
487 parseEnvDuration("ARGUS_BUTTON_DEBOUNCE_MS", std::chrono::milliseconds(50));
488 return config;
489}
490
492 switch (event) {
494 return "ARM_REQUEST";
496 return "DISARM_REQUEST";
498 return "ACK_REQUEST";
499 default:
500 return "UNKNOWN";
501 }
502}
503
504std::optional<int> PhysicalButtonModule::parseEnvInt(const char* name) noexcept {
505 const char* text = std::getenv(name);
506 if (text == nullptr || *text == '\0') {
507 return std::nullopt;
508 }
509
510 errno = 0;
511 char* end = nullptr;
512 const long value = std::strtol(text, &end, 10);
513 if (errno != 0 || end == text || *end != '\0' ||
514 value < 0 || value > std::numeric_limits<int>::max()) {
515 return std::nullopt;
516 }
517
518 return static_cast<int>(value);
519}
520
521bool PhysicalButtonModule::parseEnvBool(const char* name, bool default_value) noexcept {
522 return parseBoolText(std::getenv(name), default_value);
523}
524
525std::chrono::milliseconds PhysicalButtonModule::parseEnvDuration(
526 const char* name,
527 std::chrono::milliseconds default_value) noexcept {
528 const auto value = parseEnvInt(name);
529 if (!value.has_value()) {
530 return default_value;
531 }
532
533 return std::chrono::milliseconds(*value);
534}
535
536std::string PhysicalButtonModule::makeValuePath(int gpio) {
537 return "GPIO" + std::to_string(gpio);
538}
539
540void PhysicalButtonModule::initialiseChannel(ChannelState& channel,
541 std::optional<int> gpio,
542 PhysicalButtonEvent event) noexcept {
543 if (channel.line_fd >= 0) {
544 ::close(channel.line_fd);
545 channel.line_fd = -1;
546 }
547
548 channel.gpio = gpio;
549 channel.event = event;
550 channel.value_path.clear();
551 channel.device_path.clear();
552 channel.active = false;
553 channel.initialised = false;
554 channel.last_sample_pressed = false;
555 channel.stable_pressed = false;
556 channel.last_transition = std::chrono::steady_clock::time_point{};
557
558 if (!gpio.has_value()) {
559 return;
560 }
561
562 channel.value_path = makeValuePath(*gpio);
563
564 std::string error_text;
565 if (!requestButtonLine(*gpio,
566 config_.active_low,
567 channel.device_path,
568 channel.line_fd,
569 error_text)) {
571 last_error_ = error_text.empty()
572 ? std::string("unable to request ") + channel.value_path
573 : error_text;
574 }
575 return;
576 }
577
578 bool pressed = false;
579 if (!readPressedState(channel, pressed)) {
581 last_error_ = std::string("unable to read ") + channel.device_path +
582 " (" + channel.value_path + ")";
583 }
584 ::close(channel.line_fd);
585 channel.line_fd = -1;
586 return;
587 }
588
589 channel.active = true;
590 channel.initialised = true;
591 channel.last_sample_pressed = pressed;
592 channel.stable_pressed = pressed;
593 channel.last_transition = std::chrono::steady_clock::now();
594}
595
596bool PhysicalButtonModule::sampleChannel(ChannelState& channel,
597 std::chrono::steady_clock::time_point now,
598 PhysicalButtonEvent& event) noexcept {
599 if (!channel.active) {
600 return false;
601 }
602
603 bool pressed = false;
604 if (!readPressedState(channel, pressed)) {
605 if (channel.event == PhysicalButtonEvent::ACK_REQUEST) {
606 last_error_ = std::string("unable to read ") + channel.device_path +
607 " (" + channel.value_path + ")";
608 }
609 return false;
610 }
611
612 if (!channel.initialised) {
613 channel.initialised = true;
614 channel.last_sample_pressed = pressed;
615 channel.stable_pressed = pressed;
616 channel.last_transition = now;
617 return false;
618 }
619
620 if (pressed != channel.last_sample_pressed) {
621 channel.last_sample_pressed = pressed;
622 channel.last_transition = now;
623 }
624
625 if (pressed == channel.stable_pressed) {
626 return false;
627 }
628
629 const auto elapsed = now - channel.last_transition;
630 if (elapsed < config_.debounce) {
631 return false;
632 }
633
634 channel.stable_pressed = pressed;
635 if (pressed) {
636 event = channel.event;
637 return true;
638 }
639
640 return false;
641}
642
643bool PhysicalButtonModule::readPressedState(const ChannelState& channel,
644 bool& pressed) noexcept {
645 if (channel.line_fd < 0) {
646 return false;
647 }
648
649 gpio_v2_line_values values{};
650 values.mask = 1;
651 if (::ioctl(channel.line_fd, GPIO_V2_LINE_GET_VALUES_IOCTL, &values) != 0) {
652 return false;
653 }
654
655 pressed = (values.bits & 1ULL) != 0ULL;
656 return true;
657}
Debounced physical operator-button input module.
PhysicalButtonEvent
Semantic operator requests produced by button input.
@ DISARM_REQUEST
Request to disarm/stop controlled motion.
@ ACK_REQUEST
Request acknowledge/continue after freeze.
@ ARM_REQUEST
Request to arm/start controlled motion.
GPIO character-device based physical button module.
static const char * eventToString(PhysicalButtonEvent event) noexcept
Convert semantic event enum to readable string.
bool poll(PhysicalButtonEvent &event) noexcept
Poll channels and emit a debounced semantic event if available.
const char * statusString() const noexcept
Get compact status summary string.
bool readAcknowledgePressed(bool &pressed) noexcept
Read current acknowledge-button pressed state.
~PhysicalButtonModule()
Destroy module and release any open line descriptors.
bool available() const noexcept
Check whether at least one input channel is available.
const char * lastErrorString() const noexcept
Get last error string.
PhysicalButtonModule()
Construct module from environment-derived configuration.
static PhysicalButtonConfig configFromEnvironment() noexcept
Build button configuration from environment variables.
bool waitForEvent(PhysicalButtonEvent &event, std::chrono::milliseconds timeout) noexcept
Wait for next debounced event using blocking GPIO edge reads.
Configuration for physical button inputs.
std::optional< int > disarm_gpio
BCM GPIO for disarm request, if present.
std::chrono::milliseconds debounce
Debounce interval.
bool active_low
Interpret low level as pressed when true.
std::optional< int > acknowledge_gpio
BCM GPIO for acknowledge request, if present.
std::optional< int > arm_gpio
BCM GPIO for arm request, if present.