| // Copyright 2024 The Pigweed Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| // use this file except in compliance with the License. You may obtain a copy of |
| // the License at |
| // |
| // https://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| // License for the specific language governing permissions and limitations under |
| // the License. |
| |
| // Usage: pw_digital_io_linux_cli COMMAND ... |
| // Commands: |
| // get [-i] CHIP LINE |
| // Configure the GPIO as an input and read its value. |
| // |
| // set [-i] CHIP LINE VALUE |
| // Configure the GPIO as an output and set its value. |
| // |
| // watch [-i] [{-ta,-tb,-td}] CHIP LINE |
| // Configure the GPIO as an input and watch for interrupt events. |
| // |
| // Options: |
| // -t Trigger for an interrupt: |
| // -ta - activating edge |
| // -tb - both edges (default) |
| // -td - deactivating edge |
| // |
| // Args: |
| // CHIP: gpiochip path (e.g. /dev/gpiochip0) |
| // LINE: line number (e.g. 1) |
| // VALUE: the value to set (0 or 1) |
| // |
| // Options: |
| // -i Invert; configure as active-low. |
| |
| #include <fcntl.h> |
| #include <unistd.h> |
| |
| #include <cerrno> |
| #include <charconv> |
| #include <cstdint> |
| #include <cstring> |
| #include <iostream> |
| #include <list> |
| #include <optional> |
| #include <string> |
| |
| #include "pw_digital_io_linux/digital_io.h" |
| #include "pw_log/log.h" |
| #include "pw_status/try.h" |
| |
| using pw::digital_io::InterruptTrigger; |
| using pw::digital_io::LinuxDigitalIoChip; |
| using pw::digital_io::LinuxGpioNotifier; |
| using pw::digital_io::LinuxInputConfig; |
| using pw::digital_io::LinuxOutputConfig; |
| using pw::digital_io::Polarity; |
| using pw::digital_io::State; |
| |
| namespace { |
| |
| pw::Status SetOutput(LinuxDigitalIoChip& chip, |
| const LinuxOutputConfig& config) { |
| auto maybe_output = chip.GetOutputLine(config); |
| if (!maybe_output.ok()) { |
| PW_LOG_ERROR("Failed to get output line: %s", maybe_output.status().str()); |
| return pw::Status::Unavailable(); |
| } |
| auto output = std::move(maybe_output.value()); |
| |
| if (auto status = output.Enable(); !status.ok()) { |
| PW_LOG_ERROR("Failed to enable output line: %s", status.str()); |
| return status; |
| } |
| |
| // Nothing to do... default value applied. |
| PW_LOG_INFO("Set line %u to %s\n", |
| config.index, |
| config.default_state == State::kActive ? "active" : "inactive"); |
| |
| // NOTE: When this function returns and `output` goes out of scope, its |
| // file descriptor is closed. Depending on the GPIO driver, this could |
| // result in the pin being immediately returned to its default state. |
| // See https://manpages.debian.org/bookworm/gpiod/gpioset.1.en.html. |
| |
| return pw::OkStatus(); |
| } |
| |
| const char* InterruptTriggerStr(InterruptTrigger trigger) { |
| switch (trigger) { |
| case InterruptTrigger::kActivatingEdge: |
| return "activating edge"; |
| case InterruptTrigger::kBothEdges: |
| return "both edges"; |
| case InterruptTrigger::kDeactivatingEdge: |
| return "deactivating edge"; |
| default: |
| return "?"; |
| } |
| } |
| |
| pw::Status WatchInput(LinuxDigitalIoChip& chip, |
| const LinuxInputConfig& config, |
| InterruptTrigger trigger) { |
| PW_TRY_ASSIGN(auto notifier, LinuxGpioNotifier::Create()); |
| |
| auto maybe_input = chip.GetInterruptLine(config, notifier); |
| if (!maybe_input.ok()) { |
| PW_LOG_ERROR("Failed to get input line: %s", maybe_input.status().str()); |
| return pw::Status::Unavailable(); |
| } |
| auto input = std::move(maybe_input.value()); |
| |
| auto handler = [](State state) { |
| if (state == State::kActive) { |
| std::cout << "Activated" << std::endl; |
| } else { |
| std::cout << "Deactivated" << std::endl; |
| } |
| }; |
| |
| PW_TRY(input.SetInterruptHandler(trigger, handler)); |
| |
| if (auto status = input.EnableInterruptHandler(); !status.ok()) { |
| PW_LOG_ERROR("Failed to enable input interrupt: %s", status.str()); |
| return status; |
| } |
| |
| if (auto status = input.Enable(); !status.ok()) { |
| PW_LOG_ERROR("Failed to enable input line: %s", status.str()); |
| return status; |
| } |
| |
| PW_LOG_INFO("Watching for events (%s)", InterruptTriggerStr(trigger)); |
| |
| // Process events |
| notifier->Run(); |
| |
| return pw::OkStatus(); |
| } |
| |
| pw::Status GetInput(LinuxDigitalIoChip& chip, const LinuxInputConfig& config) { |
| auto maybe_input = chip.GetInputLine(config); |
| if (!maybe_input.ok()) { |
| PW_LOG_ERROR("Failed to get input line: %s", maybe_input.status().str()); |
| return pw::Status::Unavailable(); |
| } |
| auto input = std::move(maybe_input.value()); |
| |
| if (auto status = input.Enable(); !status.ok()) { |
| PW_LOG_ERROR("Failed to enable input line: %s", status.str()); |
| return status; |
| } |
| |
| auto maybe_state = input.GetState(); |
| if (!maybe_state.ok()) { |
| PW_LOG_ERROR("Failed to get input line state: %s", |
| maybe_state.status().str()); |
| return pw::Status::Unavailable(); |
| } |
| std::cout << (maybe_state.value() == State::kActive ? "active" : "inactive") |
| << std::endl; |
| return pw::OkStatus(); |
| } |
| |
| void UsageError(const std::string& error) { |
| PW_LOG_ERROR("%s", error.c_str()); |
| std::cerr << "Usage: pw_digital_io_linux_cli COMMAND ..." << std::endl; |
| std::cerr << std::endl; |
| std::cerr << " Commands:" << std::endl; |
| std::cerr << " get [-i] CHIP LINE" << std::endl; |
| std::cerr << " set [-i] CHIP LINE VALUE" << std::endl; |
| std::cerr << " watch [-i] [{-ta,-tb,-td}] CHIP LINE" << std::endl; |
| std::cerr << " Options:" << std::endl; |
| std::cerr << " -t Trigger for an interrupt:" << std::endl; |
| std::cerr << " -ta - activating edge" << std::endl; |
| std::cerr << " -tb - both edges (default)" << std::endl; |
| std::cerr << " -td - deactivating edge" << std::endl; |
| std::cerr << std::endl; |
| std::cerr << " Common Options:" << std::endl; |
| std::cerr << " -i Invert; configure as active-low." << std::endl; |
| } |
| |
| } // namespace |
| |
| int main(int argc, char* argv[]) { |
| std::list<std::string> args; |
| for (int i = 1; i < argc; ++i) { |
| args.emplace_back(argv[i]); |
| } |
| |
| // The first argument is the command name. |
| if (args.empty()) { |
| UsageError("Missing command"); |
| return 1; |
| } |
| std::string command = args.front(); |
| args.pop_front(); |
| |
| // These are currently the only commands, and they take the same options (-i) |
| // and first two arguments (chip and line). |
| if (!(command == "get" || command == "set" || command == "watch")) { |
| UsageError("Invalid command: \"" + command + "\""); |
| return 1; |
| } |
| |
| // Process options |
| Polarity polarity = Polarity::kActiveHigh; |
| InterruptTrigger trigger = InterruptTrigger::kBothEdges; |
| for (auto argi = args.begin(); argi != args.end(); /* Advance in body. */) { |
| std::string option = *argi; |
| if (!(option.size() >= 2 && option[0] == '-')) { |
| // Not an option. |
| ++argi; // Advance. |
| continue; |
| } |
| option = option.substr(1); |
| argi = args.erase(argi); // Advance. |
| |
| if (option == "i") { |
| polarity = Polarity::kActiveLow; |
| continue; |
| } |
| if (command == "watch") { |
| if (option == "ta") { |
| trigger = InterruptTrigger::kActivatingEdge; |
| continue; |
| } else if (option == "tb") { |
| trigger = InterruptTrigger::kBothEdges; |
| continue; |
| } else if (option == "td") { |
| trigger = InterruptTrigger::kDeactivatingEdge; |
| continue; |
| } |
| } |
| UsageError("Invalid option: \"-" + option + "\""); |
| return 1; |
| } |
| |
| // Process args |
| if (args.size() < 2) { |
| UsageError("Missing arguments: CHIP, LINE"); |
| return 1; |
| } |
| std::string path = args.front(); |
| args.pop_front(); |
| uint32_t index = std::stoi(args.front()); |
| args.pop_front(); |
| |
| // "set" also takes a value argument. |
| std::optional<State> set_value = std::nullopt; |
| if (command == "set") { |
| if (args.empty()) { |
| UsageError("Missing argument: VALUE"); |
| return 1; |
| } |
| set_value = |
| (std::stoi(args.front()) > 0) ? State::kActive : State::kInactive; |
| args.pop_front(); |
| } |
| |
| if (!args.empty()) { |
| UsageError("Unexpected argument: \"" + args.front() + "\""); |
| return 1; |
| } |
| |
| // Open the chip. |
| auto maybe_chip = LinuxDigitalIoChip::Open(path.c_str()); |
| if (!maybe_chip.ok()) { |
| PW_LOG_ERROR("Failed to open %s: %s", path.c_str(), std::strerror(errno)); |
| return 2; |
| } |
| auto chip = std::move(maybe_chip.value()); |
| PW_LOG_INFO("Opened %s", path.c_str()); |
| |
| // Handle the get or set. |
| pw::Status status; |
| if (command == "get") { |
| LinuxInputConfig config( |
| /* gpio_index= */ index, |
| /* gpio_polarity= */ polarity); |
| status = GetInput(chip, config); |
| } else if (command == "set") { |
| LinuxOutputConfig config( |
| /* gpio_index= */ index, |
| /* gpio_polarity= */ polarity, |
| /* gpio_default_state= */ *set_value); |
| status = SetOutput(chip, config); |
| } else if (command == "watch") { |
| LinuxInputConfig config( |
| /* gpio_index= */ index, |
| /* gpio_polarity= */ polarity); |
| status = WatchInput(chip, config, trigger); |
| } |
| |
| // Handle the return status accordingly. |
| return status.ok() ? 0 : 2; |
| } |