1 // Copyright 2024 The Pigweed Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License"); you may not
4 // use this file except in compliance with the License. You may obtain a copy of
5 // the License at
6 //
7 // https://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 // License for the specific language governing permissions and limitations under
13 // the License.
14
15 // Usage: pw_digital_io_linux_cli COMMAND ...
16 // Commands:
17 // get [-i] CHIP LINE
18 // Configure the GPIO as an input and read its value.
19 //
20 // set [-i] CHIP LINE VALUE
21 // Configure the GPIO as an output and set its value.
22 //
23 // watch [-i] [{-ta,-tb,-td}] CHIP LINE
24 // Configure the GPIO as an input and watch for interrupt events.
25 //
26 // Options:
27 // -t Trigger for an interrupt:
28 // -ta - activating edge
29 // -tb - both edges (default)
30 // -td - deactivating edge
31 //
32 // Args:
33 // CHIP: gpiochip path (e.g. /dev/gpiochip0)
34 // LINE: line number (e.g. 1)
35 // VALUE: the value to set (0 or 1)
36 //
37 // Options:
38 // -i Invert; configure as active-low.
39
40 #include <fcntl.h>
41 #include <unistd.h>
42
43 #include <cerrno>
44 #include <charconv>
45 #include <cstdint>
46 #include <cstring>
47 #include <iostream>
48 #include <list>
49 #include <optional>
50 #include <string>
51
52 #include "pw_digital_io_linux/digital_io.h"
53 #include "pw_log/log.h"
54 #include "pw_status/try.h"
55
56 using pw::digital_io::InterruptTrigger;
57 using pw::digital_io::LinuxDigitalIoChip;
58 using pw::digital_io::LinuxGpioNotifier;
59 using pw::digital_io::LinuxInputConfig;
60 using pw::digital_io::LinuxOutputConfig;
61 using pw::digital_io::Polarity;
62 using pw::digital_io::State;
63
64 namespace {
65
SetOutput(LinuxDigitalIoChip & chip,const LinuxOutputConfig & config)66 pw::Status SetOutput(LinuxDigitalIoChip& chip,
67 const LinuxOutputConfig& config) {
68 auto maybe_output = chip.GetOutputLine(config);
69 if (!maybe_output.ok()) {
70 PW_LOG_ERROR("Failed to get output line: %s", maybe_output.status().str());
71 return pw::Status::Unavailable();
72 }
73 auto output = std::move(maybe_output.value());
74
75 if (auto status = output.Enable(); !status.ok()) {
76 PW_LOG_ERROR("Failed to enable output line: %s", status.str());
77 return status;
78 }
79
80 // Nothing to do... default value applied.
81 PW_LOG_INFO("Set line %u to %s\n",
82 config.index,
83 config.default_state == State::kActive ? "active" : "inactive");
84
85 // NOTE: When this function returns and `output` goes out of scope, its
86 // file descriptor is closed. Depending on the GPIO driver, this could
87 // result in the pin being immediately returned to its default state.
88 // See https://manpages.debian.org/bookworm/gpiod/gpioset.1.en.html.
89
90 return pw::OkStatus();
91 }
92
InterruptTriggerStr(InterruptTrigger trigger)93 const char* InterruptTriggerStr(InterruptTrigger trigger) {
94 switch (trigger) {
95 case InterruptTrigger::kActivatingEdge:
96 return "activating edge";
97 case InterruptTrigger::kBothEdges:
98 return "both edges";
99 case InterruptTrigger::kDeactivatingEdge:
100 return "deactivating edge";
101 default:
102 return "?";
103 }
104 }
105
WatchInput(LinuxDigitalIoChip & chip,const LinuxInputConfig & config,InterruptTrigger trigger)106 pw::Status WatchInput(LinuxDigitalIoChip& chip,
107 const LinuxInputConfig& config,
108 InterruptTrigger trigger) {
109 PW_TRY_ASSIGN(auto notifier, LinuxGpioNotifier::Create());
110
111 auto maybe_input = chip.GetInterruptLine(config, notifier);
112 if (!maybe_input.ok()) {
113 PW_LOG_ERROR("Failed to get input line: %s", maybe_input.status().str());
114 return pw::Status::Unavailable();
115 }
116 auto input = std::move(maybe_input.value());
117
118 auto handler = [](State state) {
119 if (state == State::kActive) {
120 std::cout << "Activated" << std::endl;
121 } else {
122 std::cout << "Deactivated" << std::endl;
123 }
124 };
125
126 PW_TRY(input.SetInterruptHandler(trigger, handler));
127
128 if (auto status = input.EnableInterruptHandler(); !status.ok()) {
129 PW_LOG_ERROR("Failed to enable input interrupt: %s", status.str());
130 return status;
131 }
132
133 if (auto status = input.Enable(); !status.ok()) {
134 PW_LOG_ERROR("Failed to enable input line: %s", status.str());
135 return status;
136 }
137
138 PW_LOG_INFO("Watching for events (%s)", InterruptTriggerStr(trigger));
139
140 // Process events
141 notifier->Run();
142
143 return pw::OkStatus();
144 }
145
GetInput(LinuxDigitalIoChip & chip,const LinuxInputConfig & config)146 pw::Status GetInput(LinuxDigitalIoChip& chip, const LinuxInputConfig& config) {
147 auto maybe_input = chip.GetInputLine(config);
148 if (!maybe_input.ok()) {
149 PW_LOG_ERROR("Failed to get input line: %s", maybe_input.status().str());
150 return pw::Status::Unavailable();
151 }
152 auto input = std::move(maybe_input.value());
153
154 if (auto status = input.Enable(); !status.ok()) {
155 PW_LOG_ERROR("Failed to enable input line: %s", status.str());
156 return status;
157 }
158
159 auto maybe_state = input.GetState();
160 if (!maybe_state.ok()) {
161 PW_LOG_ERROR("Failed to get input line state: %s",
162 maybe_state.status().str());
163 return pw::Status::Unavailable();
164 }
165 std::cout << (maybe_state.value() == State::kActive ? "active" : "inactive")
166 << std::endl;
167 return pw::OkStatus();
168 }
169
UsageError(const std::string & error)170 void UsageError(const std::string& error) {
171 PW_LOG_ERROR("%s", error.c_str());
172 std::cerr << "Usage: pw_digital_io_linux_cli COMMAND ..." << std::endl;
173 std::cerr << std::endl;
174 std::cerr << " Commands:" << std::endl;
175 std::cerr << " get [-i] CHIP LINE" << std::endl;
176 std::cerr << " set [-i] CHIP LINE VALUE" << std::endl;
177 std::cerr << " watch [-i] [{-ta,-tb,-td}] CHIP LINE" << std::endl;
178 std::cerr << " Options:" << std::endl;
179 std::cerr << " -t Trigger for an interrupt:" << std::endl;
180 std::cerr << " -ta - activating edge" << std::endl;
181 std::cerr << " -tb - both edges (default)" << std::endl;
182 std::cerr << " -td - deactivating edge" << std::endl;
183 std::cerr << std::endl;
184 std::cerr << " Common Options:" << std::endl;
185 std::cerr << " -i Invert; configure as active-low." << std::endl;
186 }
187
188 } // namespace
189
main(int argc,char * argv[])190 int main(int argc, char* argv[]) {
191 std::list<std::string> args;
192 for (int i = 1; i < argc; ++i) {
193 args.emplace_back(argv[i]);
194 }
195
196 // The first argument is the command name.
197 if (args.empty()) {
198 UsageError("Missing command");
199 return 1;
200 }
201 std::string command = args.front();
202 args.pop_front();
203
204 // These are currently the only commands, and they take the same options (-i)
205 // and first two arguments (chip and line).
206 if (!(command == "get" || command == "set" || command == "watch")) {
207 UsageError("Invalid command: \"" + command + "\"");
208 return 1;
209 }
210
211 // Process options
212 Polarity polarity = Polarity::kActiveHigh;
213 InterruptTrigger trigger = InterruptTrigger::kBothEdges;
214 for (auto argi = args.begin(); argi != args.end(); /* Advance in body. */) {
215 std::string option = *argi;
216 if (!(option.size() >= 2 && option[0] == '-')) {
217 // Not an option.
218 ++argi; // Advance.
219 continue;
220 }
221 option = option.substr(1);
222 argi = args.erase(argi); // Advance.
223
224 if (option == "i") {
225 polarity = Polarity::kActiveLow;
226 continue;
227 }
228 if (command == "watch") {
229 if (option == "ta") {
230 trigger = InterruptTrigger::kActivatingEdge;
231 continue;
232 } else if (option == "tb") {
233 trigger = InterruptTrigger::kBothEdges;
234 continue;
235 } else if (option == "td") {
236 trigger = InterruptTrigger::kDeactivatingEdge;
237 continue;
238 }
239 }
240 UsageError("Invalid option: \"-" + option + "\"");
241 return 1;
242 }
243
244 // Process args
245 if (args.size() < 2) {
246 UsageError("Missing arguments: CHIP, LINE");
247 return 1;
248 }
249 std::string path = args.front();
250 args.pop_front();
251 uint32_t index = std::stoi(args.front());
252 args.pop_front();
253
254 // "set" also takes a value argument.
255 std::optional<State> set_value = std::nullopt;
256 if (command == "set") {
257 if (args.empty()) {
258 UsageError("Missing argument: VALUE");
259 return 1;
260 }
261 set_value =
262 (std::stoi(args.front()) > 0) ? State::kActive : State::kInactive;
263 args.pop_front();
264 }
265
266 if (!args.empty()) {
267 UsageError("Unexpected argument: \"" + args.front() + "\"");
268 return 1;
269 }
270
271 // Open the chip.
272 auto maybe_chip = LinuxDigitalIoChip::Open(path.c_str());
273 if (!maybe_chip.ok()) {
274 PW_LOG_ERROR("Failed to open %s: %s", path.c_str(), std::strerror(errno));
275 return 2;
276 }
277 auto chip = std::move(maybe_chip.value());
278 PW_LOG_INFO("Opened %s", path.c_str());
279
280 // Handle the get or set.
281 pw::Status status;
282 if (command == "get") {
283 LinuxInputConfig config(
284 /* gpio_index= */ index,
285 /* gpio_polarity= */ polarity);
286 status = GetInput(chip, config);
287 } else if (command == "set") {
288 LinuxOutputConfig config(
289 /* gpio_index= */ index,
290 /* gpio_polarity= */ polarity,
291 /* gpio_default_state= */ *set_value);
292 status = SetOutput(chip, config);
293 } else if (command == "watch") {
294 LinuxInputConfig config(
295 /* gpio_index= */ index,
296 /* gpio_polarity= */ polarity);
297 status = WatchInput(chip, config, trigger);
298 }
299
300 // Handle the return status accordingly.
301 return status.ok() ? 0 : 2;
302 }
303