-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathpico-i2cd.c
494 lines (446 loc) · 14.9 KB
/
pico-i2cd.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
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
/**\file
* \brief UPS PIco I2C input driver.
*
* The PIco UPS for the Raspberry Pi by pimodules.com has a few buttons that are
* not normally available to userspace programmes. They can be scanned through
* I2C, however, and this daemon is designed to do so and make these button
* events available to userspace via the uinput kernel subsystem.
*
* Assuming you have the uinput kernel module loaded, upon running this daemon
* you will see a new input device pop up in /dev/input. This will most likely
* be recognised as a joystick, and it exposes three buttons to anything using
* the input device: BTN_A, BTN_B and BTN_C, corresponding to KEY_A, KEY_B and
* KEY_F on the PIco. (There is no BTN_F, and it felt wrong to use keyboard scan
* codes for this.)
*
* \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 errno */
#include <errno.h>
/* for ioctl() */
#include <sys/ioctl.h>
/* for I2C /dev interface macros */
/* note that this requires the version of this file from libi2c-dev */
#include <linux/i2c-dev.h>
/* for Linux input device macros */
#include <linux/uinput.h>
/**\brief Daemon version
*
* The version number of this daemon. Will be increased around release time.
*/
static const int version = 2;
/**\brief I2C state
*
* Contains the current state - as we know it - of the I2C device we have open.
*/
struct i2c {
/**\brief Device file descriptor
*
* The OS file descriptor for the open device file.
*/
int device;
/**\brief Current I2C address we dialed to
*
* Different PIco commands are using different I2C addresses as well as
* registers. This is to remember the last address we dialed, so we don't need
* to re-issue those syscalls to change to a different address.
*/
int addr;
};
/**\brief Decode BCD word values
*
* The PIco exports a lot of data encoded in binary-coded decimal values. For
* reference, this basically means that every 4-bit nibble is holding a single
* digit, encoded in regular binary form.
*
* High/low byte order is presumably kept, though this is not documented.
*
* \param[in] w The word to parse.
*
* \returns The decoded value.
*/
static long getBCD(long w) {
return ((w >> 0x0) & 0xf)
+ ((w >> 0x4) & 0xf) * 10
+ ((w >> 0x8) & 0xf) * 100
+ ((w >> 0xc) & 0xf) * 1000;
}
/**\brief Select I2C address
*
* Sets the I2C address to read data from. If the current address is the same as
* the one that was dialed last, this function does not issue a syscall to
* change the address.
*
* \param[out] i2c The I2C state struct.
* \param[in] addr The I2C address to select.
*
* \returns 0 on success, negative values otherwise.
*/
static int selectAddr(struct i2c *i2c, int addr) {
if (i2c->addr == addr) {
return 0;
}
if (ioctl(i2c->device, I2C_SLAVE, addr) < 0) {
return -1;
}
i2c->addr = addr;
return 0;
}
/**\brief Read word from I2C via SMBUS
*
* Reads a word from the given I2C address and register via SMBUS.
*
* \param[out] i2c The I2C state struct.
* \param[in] addr The I2C address to read from.
* \param[in] reg The register to read.
*
* \returns Negative values on failure; the read value otherwise.
*/
static long getWord(struct i2c *i2c, int addr, int reg) {
if (selectAddr(i2c, addr) < 0) {
return -1;
} else {
long res = i2c_smbus_read_word_data(i2c->device, reg);
if (res < 0) {
return -3;
}
return res;
}
}
/**\brief Read byte from I2C via SMBUS
*
* Reads a byte from the given I2C address and register via SMBUS.
*
* \param[out] i2c The I2C state struct.
* \param[in] addr The I2C address to read from.
* \param[in] reg The register to read.
*
* \returns Negative values on failure; the read value otherwise.
*/
static long getByte(struct i2c *i2c, int addr, int reg) {
if (selectAddr(i2c, addr) < 0) {
return -1;
} else {
long res = i2c_smbus_read_byte_data(i2c->device, reg);
if (res < 0) {
return -3;
}
return res;
}
}
/**\brief Store byte to I2C via SMBUS
*
* Stores a byte at the given I2C address and register via SMBUS.
*
* \param[out] i2c The I2C state struct.
* \param[in] addr The I2C address to read from.
* \param[in] reg The register to read.
* \param[in] value The value to write.
*
* \returns Negative values on failure; 0 otherwise.
*/
static long setByte(struct i2c *i2c, int addr, int reg, int value) {
if (selectAddr(i2c, addr) < 0) {
return -1;
} else {
long res = i2c_smbus_write_byte_data(i2c->device, reg, value);
if (res < 0) {
return -3;
}
return res;
}
}
/**\brief Get PIco battery voltage.
*
* Read the voltage of battery connected to the PIco. The battery has a nominal
* voltage of around 3.7 V, and a useful voltage down to about 3.5 V.
*
* \param[out] i2c The I2C state struct.
*
* \returns The voltage, in centi-volts. Negative values on error.
*/
static long getBatteryVoltage(struct i2c *i2c) {
return getBCD(getWord(i2c, 0x69, 0x01));
}
/**\brief Get Raspberry Pi 5V pin voltage.
*
* Read the voltage of the 5V input line, as seen by the PIco.
*
* \param[out] i2c The I2C state struct.
*
* \returns The voltage, in centi-volts. Negative values on error.
*/
static long getHostVoltage(struct i2c *i2c) {
return getBCD(getWord(i2c, 0x69, 0x03));
}
/**\brief Read out PIco firmware version.
*
* This functions reads the firmware version register of the PIco. Note that
* these are typically written out in hexadecimal, so the readout may look
* different than what you're expecting if you don't adjust for that.
*
* \param[out] i2c The I2C state struct.
*
* \returns Negative numbers on errors, or the version number otherwise.
* Some version numbers have a special meaning. See the PIco manual for more
* info on those.
*/
static long getVersion(struct i2c *i2c) { return getByte(i2c, 0x6b, 0x00); }
/**\brief Read out power mode.
*
* Reds the power mode register on the PIco, to find out if the device is
* currently on battery power or not.
*
* \param[out] i2c The I2C state struct.
*
* \returns 1 if the device is plugged in, 2 if it's on battery power. Any other
* code means that something is wrong.
*/
static long getMode(struct i2c *i2c) { return getByte(i2c, 0x69, 0x00); }
/**\brief Get key status.
*
* PIco key presses are sensed via I2C. Once a key is pressed, the corresponding
* register is set to 1, otherwise it is set to 0.
*
* This function is used to read the I2C register for a given key. Possible
* values are 0 for KEY_A, 1 for KEY_B and 2 for KEY_F.
*
* \param[out] i2c The I2C state struct.
* \param[in] key The key to read the state of (0, 1 or 2).
*
* \returns Negative number on failure, 0 otherwise.
*/
static long getKey(struct i2c *i2c, int key) {
return getByte(i2c, 0x69, 0x09 + key);
}
/**\brief Set key to 0.
*
* PIco key presses are sensed via I2C. Once a key is pressed, the corresponding
* register is set to 1. It has to manually be set back to 0 after successfully
* reading it, which is what this function does.
*
* \param[out] i2c The I2C state struct.
* \param[in] key The key to set to 0 (0, 1 or 2).
*
* \returns Negative number on failure, 0 otherwise.
*/
static long resetKey(struct i2c *i2c, int key) {
return setByte(i2c, 0x69, 0x09 + key, 0);
}
/**\brief Read out temperature sensor.
*
* The PIco has up to two temperature sensors, one built in by default and one
* as part of the fan kit. This function can be used to read either, depending
* on the value of the 'sensor' parameter: 0 for the built-in sensor and 1 for
* the external one.
*
* \param[out] i2c The I2C state struct.
* \param[in] sensor The sensor to read out (0 or 1).
*
* \returns Negative number on failure, or the readout as degrees Celsius.
*/
static long getTemperature(struct i2c *i2c, int sensor) {
return getBCD(getByte(i2c, 0x69, 0x0c + sensor));
}
/**\brief PIco I2C driver main function
*
* Parses some command line variables and then opens an I2C connection to the
* PIco module. If the connection attempt succeeds, the code will then dump the
* current state of the PIco and/or create a virtual input device for the
* buttons on the PIco.
*
* The state is dumped with the '-s' parameter, and the output format is roughly
* compatible with the text format used by the Prometheus monitoring programme.
*
* The virtual input device is created using the uinput kernel driver, which
* allows a user-space programme to act as an input device. For this mode, the
* programme will most likely need to be run as root, as /dev/uinput is usually
* only writable by the root user.
*
* * -a [address] selects the I2C device to use. The default is /dev/i2c-1,
* which is the typical I2C device file on Raspberry Pi 2 and B+.
* * -d launches the programme as a daemon. Setup is performed before the
* daemon() call, which allows error reporting for that.
* * -i Do not run the input device loop. The default is to run it.
* * -s Dump current PIco state. The default is not to do so.
* * -u [uinput] selects the uinput device file. /dev/uinput seems to be used by
* Debian, even though the canonical location is /dev/input/uinput.
* * -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 *adaptor = "/dev/i2c-1";
char *uinput = "/dev/uinput";
struct i2c i2c = {0, 0};
char daemonise = 0;
char status = 0;
char input_loop = 1;
int opt;
while ((opt = getopt(argc, argv, "a:disu:v")) != -1) {
switch (opt) {
case 'a':
adaptor = optarg;
break;
case 'd':
daemonise = 1;
break;
case 'i':
input_loop = 0;
break;
case 's':
status = 1;
break;
case 'u':
uinput = optarg;
break;
case 'v':
printf("pico-i2cd/%i\n", version);
return 0;
default:
printf("Usage: %s [-a <adaptor>] [-d] [-i] [-s] [-u <uinput>] [-v]\n",
argv[0]);
return -3;
}
}
i2c.device = open(adaptor, O_RDWR);
if (i2c.device < 0) {
fprintf(stderr, "Could not open adaptor: '%s'; ERRNO=%d.\n", adaptor,
errno);
return -1;
}
if (status) {
printf("pico_firmware_version %ld\n", getVersion(&i2c));
printf("pico_mode %ld\n", getMode(&i2c));
printf("pico_battery_centivolts %ld\n", getBatteryVoltage(&i2c));
printf("pico_host_centivolts %ld\n", getHostVoltage(&i2c));
printf("pico_temperature_1_celsius_degrees %ld\n", getTemperature(&i2c, 0));
printf("pico_temperature_2_celsius_degrees %ld\n", getTemperature(&i2c, 1));
}
if (input_loop) {
int device = open(uinput, O_WRONLY | O_NONBLOCK), i;
struct uinput_user_dev userdev = {
"Raspberry Pi PIco UPS", {BUS_I2C, 0x0000, 0x0000, version}, 0};
struct input_event event = {{0}, EV_KEY, 0};
struct input_event syn = {{0}, EV_SYN, SYN_REPORT};
int code[3] = {BTN_A, BTN_B, BTN_C};
char release[3] = {0, 0, 0};
char synchronise = 0;
if (device < 0) {
fprintf(stderr, "Could not open uinput: '%s'; ERRNO=%d.\n", uinput,
errno);
return -2;
}
if (daemonise == 1) {
if (daemon(0, 0) < 0) {
printf("Failed to daemonise properly; ERRNO=%d.\n", errno);
return -3;
}
}
if (ioctl(device, UI_SET_EVBIT, EV_KEY) < 0) {
fprintf(stderr, "Could not set event bits: ERRNO=%d.\n", errno);
return -5;
}
if (ioctl(device, UI_SET_EVBIT, EV_SYN) < 0) {
fprintf(stderr, "Could not set event bits: ERRNO=%d.\n", errno);
return -5;
}
for (i = 0; i < 3; i++) {
if (ioctl(device, UI_SET_KEYBIT, code[i]) < 0) {
fprintf(stderr, "Could not declare key code: ERRNO=%d.\n", errno);
return -5;
}
}
if (write(device, &userdev, sizeof(userdev)) != sizeof(userdev)) {
fprintf(stderr, "Could not write device id: ERRNO=%d.\n", errno);
return -5;
}
if (ioctl(device, UI_DEV_CREATE) < 0) {
fprintf(stderr, "Could not create input device: ERRNO=%d.\n", errno);
return -5;
}
while (1) {
for (i = 0; i < 3; i++) {
int scan = getKey(&i2c, i);
if (release[i] > 0) {
if (scan == 0) {
event.code = code[i];
event.value = 0;
if (write(device, &event, sizeof(event)) == sizeof(event)) {
/* event has been sent successfully */
release[i] = 0;
synchronise = 1;
}
} else {
/* This is what happens when the button is still being pressed, so
since we saw that again we'll just reset it to 0 again. */
resetKey(&i2c, i);
if (release[i] == 4) {
/* we've detected a long press (of several scan cycles) */
event.code = code[i];
event.value = 2;
if (write(device, &event, sizeof(event)) == sizeof(event)) {
/* event has been sent successfully */
synchronise = 1;
}
}
/* keep counting up a few times to detect a long press. */
if (release[i] < 5) {
release[i]++;
}
}
} else {
if (scan > 0) {
event.code = code[i];
event.value = 1;
if (write(device, &event, sizeof(event)) == sizeof(event)) {
/* event has been sent successfully */
release[i] = 1;
resetKey(&i2c, i);
synchronise = 1;
}
}
}
}
if (synchronise) {
(void)write(device, &syn, sizeof(syn));
/* AFAICT the SYN for this should be optional, we're only sending it
for completeness' sake. Therefore, if it couldn't be sent right, we
ought to be able to ignore it. */
synchronise = 0;
}
(void)usleep(100000);
/* ignore the return status */
}
/* we should never reach this part of the code. */
(void)ioctl(device, UI_DEV_DESTROY);
/* clean up, but ignore the return status since this should not be
reachable. */
(void)close(device);
/* same here. */
}
/* we only ever reach this part of the code IFF we disabled the input loop. */
(void)close(i2c.device);
/* ignore this return value, as we're terminating the programme next, which
also closes the file. */
return 0;
}