-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathpicod.c
396 lines (337 loc) · 10.6 KB
/
picod.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
/**\file
* \brief UPS PIco control daemon.
*
* The PIco UPS for the Raspberry Pi by pimodules.com requires some userspace
* help to function correctly. The vendor provides a Python script -
* picofssd.py - to do so, but it seems strange to force users to use Python
* for an embedded device's UPS.
*
* This programme replaces the aforementioned Python script. It's a lot smaller
* than the original, nicely compiled and adds the ability to spawn into a
* daemon proper, so you don't have to screw around with nohup and & in your
* rc.local script.
*
* \copyright
* This programme is released as open source, under the terms of an MIT/X style
* licence. See the accompanying LICENSE file for details.
*
* \see Source Code Repository: https://github.com/ef-gy/rpi-ups-pico
* \see Documentation: https://ef.gy/documentation/rpi-ups-pico
* \see Hardware: http://pimodules.com/_pdf/_pico/UPS_PIco_BL_FSSD_V1.0.pdf
* \see Hardware Vendor: http://pimodules.com/
* \see Licence Terms: https://github.com/ef-gy/rpi-ups-pico/blob/master/LICENSE
*/
/* for open() */
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
/* for usleep(), read(), write(), getopt(), daemon() */
#include <unistd.h>
/* for snprintf() */
#include <stdio.h>
/* for system() */
#include <stdlib.h>
/* for errno */
#include <errno.h>
/**\brief Maximum length of GPIO file name.
*
* This is the maximum length of a GPIO file that we're willing to support.
*/
#define MAX_GPIO_FN 256
/**\brief Internal buffer size.
*
* Size of internal buffers used throughout the code.
*/
#define MAX_BUFFER 32
/**\brief Daemon version
*
* The version number of this daemon. Will be increased around release time.
*/
static const int version = 3;
/**\brief Maximum number of retries.
*
* Used during GPIO pin setup, to prevent random setup delays from causing
* initialisation to fail.
*/
static const int maxRetries = 8;
/**\brief Export GPIO pin
*
* Linux's sysfs interface for GPIO pins requires setting up the pins that you
* intend to use by first "exporting" them, which creates the pin's control
* files in sysfs. This function tells the kernel to do so.
*
* \param[in] gpio The pin to export.
*
* \returns 0 on success, negative numbers on (partial) failures.
*/
static int export(int gpio) {
int fd;
char bf[MAX_BUFFER];
int rv = 0;
int blen = 0;
blen = snprintf(bf, MAX_BUFFER, "%i", gpio);
if (blen < 0) {
return -1;
}
fd = open("/sys/class/gpio/export", O_WRONLY);
if (fd < 0) {
return -2;
}
if (write(fd, bf, blen) < blen) {
rv = -3;
}
do {
rv = close(fd);
} while ((rv < 0) && (errno == EINTR));
return rv;
}
/**\brief Set a GPIO pin's I/O direction.
*
* Set the I/O direction of a GPIO pin that has previously been exported.
*
* \param[in] gpio The pin to set up.
* \param[in] output Nonzero for output, 0 for input.
*
* \returns 0 on success, negative numbers on (partial) failures.
*/
static int direction(int gpio, char output) {
int fd;
char fn[MAX_GPIO_FN];
int rv = 0;
if (snprintf(fn, MAX_GPIO_FN, "/sys/class/gpio/gpio%i/direction", gpio) < 0) {
return -1;
}
fd = open(fn, O_WRONLY);
if (fd < 0) {
return -2;
}
if (output) {
if (write(fd, "out\n", 4) < 4) {
rv = -3;
}
} else {
if (write(fd, "in\n", 3) < 3) {
rv = -3;
}
}
do {
rv = close(fd);
} while ((rv < 0) && (errno == EINTR));
return rv;
}
/**\brief Export GPIO pin and set I/O direction.
*
* Calls export() and then direction() to set up a GPIO pin.
*
* \param[in] gpio The pin to set up.
* \param[in] output Nonzero for output, 0 for input.
*
* \returns 0 on success, negative numbers on (partial) failures.
*/
static int setup(int gpio, char output) {
int rv = 0;
int retries = 0;
rv = export(gpio);
if (rv < 0) {
return rv;
}
do {
if (retries > 0) {
/* setting the pin IO direction may fail for a few milliseconds after
* exporting the pin, so retry this step a few times. Each time we try
* this, we wait a bit longer. */
usleep(retries * retries * 1000);
}
rv = direction(gpio, output);
} while ((rv < 0) && (retries++ < maxRetries));
if (rv < 0) {
return rv;
}
return 0;
}
/**\brief Set a GPIO pin's state
*
* Pins can either be LOW or HIGH. This function sets a pin to the given target
* state. The pin must previously have been set up as an output pin.
*
* \param[in] gpio The pin to set.
* \param[in] state The state to set the pin to.
*
* \returns 0 on success, negative numbers on (partial) failures.
*/
static int set(int gpio, char state) {
char fn[MAX_GPIO_FN];
int rv = 0;
if (snprintf(fn, MAX_GPIO_FN, "/sys/class/gpio/gpio%i/value", gpio) < 0) {
return -1;
} else {
int fd = open(fn, O_WRONLY);
if (fd) {
(void)write(fd, state ? "1\n" : "0\n", 2);
/* we ignore the return value of that write because we use this in a loop
anyway, and it's quite unlikely to fail. */
do {
rv = close(fd);
} while ((rv < 0) && (errno == EINTR));
}
}
return rv;
}
/**\brief Get the value of a GPIO pin.
*
* Queries a GPIO pin's value. The pin must have been set up to be an input pin
* beforehand.
*
* \param[in] gpio The pin to query.
*
* \returns 1 if the pin is HIGH, 0 if the pin is LOW, negative numbers on
* (partial) failures.
*/
static int get(int gpio) {
char fn[MAX_GPIO_FN];
int rv = 0;
if (snprintf(fn, MAX_GPIO_FN, "/sys/class/gpio/gpio%i/value", gpio) < 0) {
return -1;
} else {
int fd = open(fn, O_RDONLY);
if (fd) {
char buf[MAX_BUFFER];
int r = read(fd, buf, MAX_BUFFER);
if (r < 1) {
rv = -2;
} else {
rv = (buf[0] == '1');
}
do {
r = close(fd);
} while ((r < 0) && (errno == EINTR));
}
}
return rv;
}
/**\brief Create a pulse on a GPIO pin.
*
* This function creates a pulse on a GPIO pin that has previously been set up
* to be an output pin. The pin will be set to HIGH for the given duration, then
* set to LOW for the remainder of the period.
*
* \note The duration must be smaller than the period.
*
* \param[in] gpio The pin to send the pulse to.
* \param[in] period The amount of time for the full pulse; in usec.
* \param[in] duration The amount of time to set the pin to HIGH; in usec.
*
* \returns 0 on success, negative numbers on (partial) failures.
*/
static int pulse(int gpio, unsigned int period, unsigned int duration) {
if (set(gpio, 1) != 0) {
return -1;
}
(void)usleep(duration);
/* we ignore usleep()'s return value, because the only error would be to be
* interrupted by a signal, which is OK as the PIco does not seem to be that
* particular about the exact shape of the pulse train. */
if (set(gpio, 0) != 0) {
return -2;
}
(void)usleep(period - duration);
return 0;
}
/**\brief picod's main function.
*
* Parses some command line options and then creates a pulse train on pin #22,
* which is what the PIco UPS requires for it to figure out that the Raspberry
* Pi it's connected to is running.
*
* The only exit condition for the daemon is if pin #27 is set to LOW, which
* will trigger what the hardware vendor dubbed a "File Safe Shut Down." I.e. a
* good old "shutdown -h now."
*
* Because of this, and the whole thing about the GPIO pins, this daemon needs
* to be run as root and can't drop privileges. If you were to set things up
* such that the GPIO pin #22 is available to ordinary users, and you used -n to
* disable the FSSD function, you could run it as non-root.
*
* * -n disables the FSSD test, if you don't care about this feature.
* * -d launches the programme as a daemon. Pin setup is performed before the
* daemon() call, which allows error reporting for that.
* * -v prints the version of the daemon and then exits.
*
* \param[in] argc Argument count.
* \param[in] argv Argument vecotr.
*
* \returns 0 on success, negative numbers for programme setup errors.
*/
int main(int argc, char **argv) {
char daemonise = 0;
char fssd = 1;
char initialPulse = 1;
char fssdWasHigh = 0;
int opt;
while ((opt = getopt(argc, argv, "dnv")) != -1) {
switch (opt) {
case 'd':
daemonise = 1;
break;
case 'n':
fssd = 0;
break;
case 'v':
printf("picod/%i\n", version);
return 0;
default:
printf("Usage: %s [-d] [-n] [-v]\n", argv[0]);
return -3;
}
}
if (setup(22, 1) != 0) {
printf("Could not set up pin #22 as an output pin for the pulse train.\n");
return -1;
}
if (fssd == 1) {
if (setup(27, 0) != 0) {
printf("Could not set up pin #27 as input for the FSSD feature.\n");
return -4;
}
}
if (daemonise == 1) {
if (daemon(0, 0) < 0) {
printf("Failed to daemonise properly; ERRNO=%d.\n", errno);
return -2;
}
}
/* create a pulse train with the same modulation as the PIco's FSSD script. */
while (1) {
int fssdSignal = (fssd == 1) ? get(27) : 1;
/* if processing the FSSD signal is disabled, assume it's HIGH so as not to
* trigger a shutdown, ever. */
fssdWasHigh = (fssdSignal == 1) ? 1 : fssdWasHigh;
/* keep track of whether we've ever seen the FSSD signal in a HIGH state; if
* we haven't, then we assume the PIco has not been installed. */
if ((initialPulse == 1) || (fssdWasHigh == 1)) {
/* only send the pulse train if the FSSD signal scanned HIGH recently;
* this means that the pulse train is not sent if shutdown has been
* initiated due to a low battery state, or if the PIco has not been
* installed. */
(void)pulse(22, 500000, 250000);
/* note how we don't use the return value here, because we'd really just
* send another pulse. */
initialPulse = 0;
/* we send one initial pulse at boot up, just in case the PIco firmware
* would only set pin #27 to HIGH upon receiving the initial pulse; not
* sure if this is needed, but it shouldn't hurt, either. */
}
if ((fssdWasHigh == 1) && (fssdSignal == 0)) {
/* we ignore the error condition on the get() because the only thing to do
* in that case is to re-issue that, and we'll do that in 500ms. */
(void)system("shutdown -h now");
/* there's nothing else to do here - regardless of whether the call fails.
* so we bail after this. */
fssdWasHigh = 0;
/* reset the FSSD HIGH sensing; the daemon will keep running and reinstate
* the pulse train if power is restored, though we can't cancel the
* shutdown so something external would have to do that. */
}
}
return 0;
}