21#include <linux/gpio.h>
28constexpr int kDefaultAckGpio = 24;
29constexpr const char* kButtonConsumer =
"ARGUS_BUTTON";
31bool parseBoolText(
const char* text,
bool default_value)
noexcept {
32 if (text ==
nullptr) {
36 std::string value(text);
37 for (
char& ch : value) {
38 ch =
static_cast<char>(std::tolower(
static_cast<unsigned char>(ch)));
41 if (value ==
"1" || value ==
"true" || value ==
"yes" || value ==
"on") {
44 if (value ==
"0" || value ==
"false" || value ==
"no" || value ==
"off") {
50std::string toLowerCopy(std::string text) {
51 for (
char& ch : text) {
52 ch =
static_cast<char>(std::tolower(
static_cast<unsigned char>(ch)));
57bool containsInsensitive(
const std::string& haystack,
const char* needle) {
58 if (needle ==
nullptr || *needle ==
'\0') {
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;
67std::string boundedCString(
const char* text, std::size_t max_length) {
68 if (text ==
nullptr) {
72 return std::string(text, ::strnlen(text, max_length));
75bool equalsInsensitive(
const std::string& lhs,
const std::string& rhs) {
76 return toLowerCopy(lhs) == toLowerCopy(rhs);
79struct GpioChipCandidate {
83 unsigned int lines = 0;
87std::vector<std::string> buildLineNameCandidates(
int gpio) {
89 "GPIO" + std::to_string(gpio),
90 "gpio" + std::to_string(gpio),
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")) {
105std::vector<GpioChipCandidate> enumerateGpioChips() {
106 std::vector<GpioChipCandidate> candidates;
108 DIR* dir = ::opendir(
"/dev");
109 if (dir ==
nullptr) {
113 while (dirent* entry = ::readdir(dir)) {
114 if (std::strncmp(entry->d_name,
"gpiochip", 8) != 0) {
118 std::string path =
"/dev/";
119 path += entry->d_name;
121 int chip_fd = ::open(path.c_str(), O_RDONLY | O_CLOEXEC);
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);
142 std::stable_sort(candidates.begin(),
144 [](
const GpioChipCandidate& lhs,
const GpioChipCandidate& rhs) {
145 if (lhs.preference != rhs.preference) {
146 return lhs.preference < rhs.preference;
148 return lhs.path < rhs.path;
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);
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);
161 flags |=
static_cast<std::uint64_t
>(GPIO_V2_LINE_FLAG_BIAS_PULL_DOWN);
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);
171 flags |=
static_cast<std::uint64_t
>(GPIO_V2_LINE_FLAG_ACTIVE_LOW);
176bool requestLineFromChip(
const std::string& chip_path,
180 std::string& error_text)
noexcept {
183 int chip_fd = ::open(chip_path.c_str(), O_RDWR | O_CLOEXEC);
185 error_text = chip_path +
": open failed: " + std::strerror(errno);
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);
197 if (::ioctl(chip_fd, GPIO_V2_GET_LINE_IOCTL, &request) == 0 &&
199 line_fd = request.fd;
204 const int saved_errno = errno;
205 error_text = chip_path +
" line " + std::to_string(line_offset) +
206 ": " + std::strerror(saved_errno);
211std::optional<int> findNamedLineOffset(
const GpioChipCandidate& chip,
int gpio) {
212 const std::vector<std::string> target_names = buildLineNameCandidates(gpio);
214 int chip_fd = ::open(chip.path.c_str(), O_RDONLY | O_CLOEXEC);
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) {
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);
235 if (resolved_offset.has_value()) {
241 return resolved_offset;
244bool requestButtonLine(
int gpio,
246 std::string& device_path,
248 std::string& error_text) {
249 const std::vector<GpioChipCandidate> chips = enumerateGpioChips();
251 error_text =
"no /dev/gpiochip* devices found";
255 const std::uint64_t preferred_flags = makePreferredLineFlags(active_low);
256 const std::uint64_t fallback_flags = makeFallbackLineFlags(active_low);
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,
267 requestLineFromChip(chip.path,
272 device_path = chip.path +
" " + resolution_label +
" GPIO" +
273 std::to_string(gpio) +
" (offset " +
274 std::to_string(requested_offset) +
")";
279 error_text = attempt_error;
283 for (
const GpioChipCandidate& chip : chips) {
284 const std::optional<int> named_offset = findNamedLineOffset(chip, gpio);
285 if (!named_offset.has_value()) {
289 if (tryResolvedRequest(chip, *named_offset,
"named")) {
294 for (
const GpioChipCandidate& chip : chips) {
295 if (gpio < 0 ||
static_cast<unsigned int>(gpio) >= chip.lines) {
299 if (tryResolvedRequest(chip, gpio,
"raw-offset")) {
304 if (error_text.empty()) {
305 error_text =
"unable to request GPIO" + std::to_string(gpio) +
306 " on any gpiochip device";
318 if (arm_channel_.line_fd >= 0) {
319 ::close(arm_channel_.line_fd);
320 arm_channel_.line_fd = -1;
322 if (disarm_channel_.line_fd >= 0) {
323 ::close(disarm_channel_.line_fd);
324 disarm_channel_.line_fd = -1;
326 if (acknowledge_channel_.line_fd >= 0) {
327 ::close(acknowledge_channel_.line_fd);
328 acknowledge_channel_.line_fd = -1;
335 initialiseChannel(disarm_channel_,
338 initialiseChannel(acknowledge_channel_,
342 available_ = acknowledge_channel_.active;
343 if (acknowledge_channel_.active) {
344 status_string_ = acknowledge_channel_.device_path +
346 std::string(acknowledge_channel_.stable_pressed ?
"PRESSED"
348 ", debounce=" + std::to_string(config_.
debounce.count()) +
351 if (!available_ && last_error_.empty()) {
352 last_error_ =
"acknowledge button not configured";
361 const auto now = std::chrono::steady_clock::now();
363 return sampleChannel(disarm_channel_, now, event) ||
364 sampleChannel(acknowledge_channel_, now, event) ||
365 sampleChannel(arm_channel_, now, event);
369 std::chrono::milliseconds timeout)
noexcept {
370 std::array<ChannelState*, 3> channels{
372 &acknowledge_channel_,
375 std::vector<pollfd> fds;
376 std::vector<ChannelState*> ready_channels;
377 fds.reserve(channels.size());
378 ready_channels.reserve(channels.size());
380 for (ChannelState* channel : channels) {
381 if (channel ==
nullptr || !channel->active || channel->line_fd < 0) {
386 fd.fd = channel->line_fd;
389 ready_channels.push_back(channel);
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()));
403 const int ready = ::poll(fds.data(),
static_cast<nfds_t
>(fds.size()), timeout_ms);
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) {
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))) {
421 bool pressed =
false;
422 if (line_event.id == GPIO_V2_LINE_EVENT_RISING_EDGE) {
424 }
else if (line_event.id == GPIO_V2_LINE_EVENT_FALLING_EDGE) {
430 if (pressed == channel.stable_pressed) {
434 const auto since_last_transition = now - channel.last_transition;
435 if (since_last_transition < config_.debounce) {
439 channel.last_transition = now;
440 channel.last_sample_pressed = pressed;
441 channel.stable_pressed = pressed;
443 event = channel.event;
452 if (!acknowledge_channel_.active) {
456 if (!readPressedState(acknowledge_channel_, pressed)) {
457 last_error_ = std::string(
"unable to read ") + acknowledge_channel_.device_path +
458 " (" + acknowledge_channel_.value_path +
")";
466 if (last_error_.empty()) {
469 return last_error_.c_str();
473 if (status_string_.empty()) {
476 return status_string_.c_str();
481 config.
arm_gpio = parseEnvInt(
"ARGUS_BUTTON_ARM_GPIO");
482 config.
disarm_gpio = parseEnvInt(
"ARGUS_BUTTON_DISARM_GPIO");
484 parseEnvInt(
"ARGUS_BUTTON_ACK_GPIO").value_or(kDefaultAckGpio);
485 config.
active_low = parseEnvBool(
"ARGUS_BUTTON_ACTIVE_LOW",
true);
487 parseEnvDuration(
"ARGUS_BUTTON_DEBOUNCE_MS", std::chrono::milliseconds(50));
494 return "ARM_REQUEST";
496 return "DISARM_REQUEST";
498 return "ACK_REQUEST";
504std::optional<int> PhysicalButtonModule::parseEnvInt(
const char* name)
noexcept {
505 const char* text = std::getenv(name);
506 if (text ==
nullptr || *text ==
'\0') {
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()) {
518 return static_cast<int>(value);
521bool PhysicalButtonModule::parseEnvBool(
const char* name,
bool default_value)
noexcept {
522 return parseBoolText(std::getenv(name), default_value);
525std::chrono::milliseconds PhysicalButtonModule::parseEnvDuration(
527 std::chrono::milliseconds default_value)
noexcept {
528 const auto value = parseEnvInt(name);
529 if (!value.has_value()) {
530 return default_value;
533 return std::chrono::milliseconds(*value);
536std::string PhysicalButtonModule::makeValuePath(
int gpio) {
537 return "GPIO" + std::to_string(gpio);
540void PhysicalButtonModule::initialiseChannel(ChannelState& channel,
541 std::optional<int> gpio,
543 if (channel.line_fd >= 0) {
544 ::close(channel.line_fd);
545 channel.line_fd = -1;
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{};
558 if (!gpio.has_value()) {
562 channel.value_path = makeValuePath(*gpio);
564 std::string error_text;
565 if (!requestButtonLine(*gpio,
571 last_error_ = error_text.empty()
572 ? std::string(
"unable to request ") + channel.value_path
578 bool pressed =
false;
579 if (!readPressedState(channel, pressed)) {
581 last_error_ = std::string(
"unable to read ") + channel.device_path +
582 " (" + channel.value_path +
")";
584 ::close(channel.line_fd);
585 channel.line_fd = -1;
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();
596bool PhysicalButtonModule::sampleChannel(ChannelState& channel,
597 std::chrono::steady_clock::time_point now,
599 if (!channel.active) {
603 bool pressed =
false;
604 if (!readPressedState(channel, pressed)) {
606 last_error_ = std::string(
"unable to read ") + channel.device_path +
607 " (" + channel.value_path +
")";
612 if (!channel.initialised) {
613 channel.initialised =
true;
614 channel.last_sample_pressed = pressed;
615 channel.stable_pressed = pressed;
616 channel.last_transition = now;
620 if (pressed != channel.last_sample_pressed) {
621 channel.last_sample_pressed = pressed;
622 channel.last_transition = now;
625 if (pressed == channel.stable_pressed) {
629 const auto elapsed = now - channel.last_transition;
630 if (elapsed < config_.debounce) {
634 channel.stable_pressed = pressed;
636 event = channel.event;
643bool PhysicalButtonModule::readPressedState(
const ChannelState& channel,
644 bool& pressed)
noexcept {
645 if (channel.line_fd < 0) {
649 gpio_v2_line_values values{};
651 if (::ioctl(channel.line_fd, GPIO_V2_LINE_GET_VALUES_IOCTL, &values) != 0) {
655 pressed = (values.bits & 1ULL) != 0ULL;