Skip to content

Commit e3c0cc6

Browse files
committed
unix/machine_pin: Add inotify-based port removal handling.
There will be a more descriptive commit message if this is going to be considered for inclusion! Signed-off-by: Alessandro Gatti <a.gatti@frob.it>
1 parent b451a8d commit e3c0cc6

2 files changed

Lines changed: 177 additions & 0 deletions

File tree

ports/unix/machine_pin.c

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@
3535
#if MICROPY_PY_GPIO_IRQ && !MICROPY_PY_THREAD
3636
#error "GPIO IRQ support needs threading to work"
3737
#endif
38+
#if MICROPY_PY_GPIO_DYNAMIC_ALLOCATION && !MICROPY_PY_THREAD
39+
#error "GPIO dynamic pin allocation needs threading to work"
40+
#endif
3841

3942
#include <dirent.h>
4043
#include <fcntl.h>
@@ -48,6 +51,10 @@
4851
#include <signal.h>
4952
#include <sys/epoll.h>
5053
#endif
54+
#if MICROPY_PY_GPIO_DYNAMIC_ALLOCATION
55+
#include <unistd.h>
56+
#include <sys/inotify.h>
57+
#endif
5158

5259
#include <linux/gpio.h>
5360

@@ -84,6 +91,9 @@ typedef struct _gpio_port_t {
8491
mp_obj_t *pins;
8592
int fd;
8693
uint32_t lines;
94+
#if MICROPY_PY_GPIO_DYNAMIC_ALLOCATION
95+
int watch_descriptor;
96+
#endif
8797
} gpio_port_t;
8898

8999
#if MICROPY_PY_GPIO_IRQ
@@ -96,6 +106,16 @@ static volatile bool gpio_epoll_thread_stop = false;
96106

97107
#endif
98108

109+
#if MICROPY_PY_GPIO_DYNAMIC_ALLOCATION
110+
111+
static void *gpio_inotify_thread(void *arg);
112+
113+
static pthread_t gpio_inotify_thread_id = 0;
114+
static int gpio_inotify_fd = -1;
115+
static volatile bool gpio_inotify_thread_stop = false;
116+
117+
#endif
118+
99119
static void gpio_ioctl(int fd, int call, void *payload) {
100120
MP_THREAD_GIL_EXIT();
101121
int result = ioctl(fd, call, payload);
@@ -380,6 +400,45 @@ static void gpio_epoll_remove_pin(mp_obj_t pin_in) {
380400

381401
#endif
382402

403+
#if MICROPY_PY_GPIO_DYNAMIC_ALLOCATION
404+
405+
static void gpio_inotify_init(void) {
406+
MP_THREAD_GIL_EXIT();
407+
gpio_inotify_fd = inotify_init1(IN_CLOEXEC);
408+
MP_THREAD_GIL_ENTER();
409+
if (gpio_inotify_fd < 0) {
410+
mp_raise_OSError(errno);
411+
}
412+
413+
MP_THREAD_GIL_EXIT();
414+
int result = pthread_create(&gpio_inotify_thread_id, NULL, &gpio_inotify_thread, (void *)&gpio_inotify_thread_stop);
415+
MP_THREAD_GIL_ENTER();
416+
if (result != 0) {
417+
mp_raise_OSError(errno);
418+
}
419+
}
420+
421+
static void gpio_inotify_deinit(void) {
422+
gpio_inotify_thread_stop = true;
423+
MP_THREAD_GIL_EXIT();
424+
close(gpio_inotify_fd);
425+
MP_THREAD_GIL_ENTER();
426+
gpio_inotify_fd = -1;
427+
MP_THREAD_GIL_EXIT();
428+
int result = pthread_cancel(gpio_inotify_thread_id);
429+
MP_THREAD_GIL_ENTER();
430+
assert(result >= 0 && "pthread_cancel failed to cancel the inotify thread");
431+
MP_THREAD_GIL_EXIT();
432+
result = pthread_join(gpio_inotify_thread_id, NULL);
433+
MP_THREAD_GIL_ENTER();
434+
assert(result >= 0 && "pthread_join failed to wait until the inotify exited");
435+
(void)result;
436+
gpio_inotify_thread_id = 0;
437+
m_del(struct inotify_event, MP_STATE_PORT(inotify_events), MICROPY_PY_GPIO_ALLOCATION_QUEUE_SIZE);
438+
}
439+
440+
#endif
441+
383442
// This expects to receive an already resolved port name!
384443
static gpio_port_t *gpio_add_port(mp_obj_t path) {
385444
mp_map_t *ports = &MP_STATE_PORT(ports);
@@ -396,6 +455,10 @@ static gpio_port_t *gpio_add_port(mp_obj_t path) {
396455
mp_raise_OSError(errno);
397456
}
398457

458+
#if MICROPY_PY_GPIO_DYNAMIC_ALLOCATION
459+
int watch_descriptor = -1;
460+
#endif
461+
399462
gpio_port_t *port;
400463
nlr_buf_t nlr;
401464
if (nlr_push(&nlr) == 0) {
@@ -417,12 +480,27 @@ static gpio_port_t *gpio_add_port(mp_obj_t path) {
417480
port->fd = fd;
418481
port->lines = info.lines;
419482

483+
#if MICROPY_PY_GPIO_DYNAMIC_ALLOCATION
484+
// TODO: See if an inotify watch on the /dev entry is sufficient, otherwise
485+
// rebuild a sysfs path out of this.
486+
watch_descriptor = inotify_add_watch(gpio_inotify_fd, mp_obj_str_get_str(path), IN_DELETE | IN_DELETE_SELF);
487+
if (watch_descriptor < 0) {
488+
mp_raise_OSError(errno);
489+
}
490+
port->watch_descriptor = watch_descriptor;
491+
#endif
492+
420493
slot = mp_map_lookup(ports, path, MP_MAP_LOOKUP_ADD_IF_NOT_FOUND);
421494
assert(slot && "No slot to fill was returned on port add");
422495
slot->value = MP_OBJ_FROM_PTR(port);
423496

424497
nlr_pop();
425498
} else {
499+
#if MICROPY_PY_GPIO_DYNAMIC_ALLOCATION
500+
if (watch_descriptor != -1) {
501+
inotify_rm_watch(gpio_inotify_fd, watch_descriptor);
502+
}
503+
#endif
426504
close(fd);
427505

428506
// Re-raise the exception.
@@ -1098,16 +1176,92 @@ static void *gpio_epoll_thread(void *arg) {
10981176
}
10991177
#endif
11001178

1179+
#if MICROPY_PY_GPIO_DYNAMIC_ALLOCATION
1180+
static void *gpio_inotify_thread(void *arg) {
1181+
// Keep a reference to the stop variable.
1182+
volatile bool *stop = (volatile bool *)arg;
1183+
1184+
for (;;) {
1185+
ssize_t read_count = read(gpio_inotify_fd, MP_STATE_PORT(inotify_events), MICROPY_PY_GPIO_ALLOCATION_QUEUE_SIZE * sizeof(struct inotify_event));
1186+
if (*stop) {
1187+
goto done;
1188+
}
1189+
1190+
if (read_count < 0) {
1191+
// This is not correct but the code isn't in its final form yet.
1192+
continue;
1193+
}
1194+
1195+
const char *current_pointer = MP_STATE_PORT(inotify_events);
1196+
for (;;) {
1197+
const struct inotify_event *event = (const struct inotify_event *)current_pointer;
1198+
mp_uint_t atomic_state = MICROPY_BEGIN_ATOMIC_SECTION();
1199+
// Find which port is involved with the removal event.
1200+
mp_map_t *ports = &MP_STATE_PORT(ports);
1201+
if (ports->table != NULL) {
1202+
for (size_t port_index = 0; port_index < ports->alloc; port_index++) {
1203+
if (!mp_map_slot_is_filled(ports, port_index)) {
1204+
continue;
1205+
}
1206+
mp_map_elem_t slot = ports->table[port_index];
1207+
gpio_port_t *port = MP_OBJ_TO_PTR(slot.value);
1208+
if (port->watch_descriptor != event->wd) {
1209+
continue;
1210+
}
1211+
// This port was removed. Evict all pins so the last one
1212+
// will deallocate the port as well.
1213+
for (size_t pin = 0; pin < port->lines; pin++) {
1214+
if (port->pins[pin] != MP_OBJ_NULL) {
1215+
gpio_evict_pin(port->pins[pin]);
1216+
}
1217+
}
1218+
// Remove the watch.
1219+
inotify_rm_watch(gpio_inotify_fd, event->wd);
1220+
break;
1221+
}
1222+
}
1223+
MICROPY_END_ATOMIC_SECTION(atomic_state);
1224+
current_pointer += sizeof(struct inotify_event) + event->len;
1225+
if (((ptrdiff_t)current_pointer - (ptrdiff_t)MP_STATE_PORT(inotify_events)) >= read_count) {
1226+
break;
1227+
}
1228+
}
1229+
}
1230+
1231+
done:
1232+
return NULL;
1233+
}
1234+
#endif
1235+
11011236
void mp_pin_init(void) {
11021237
mp_map_init(&MP_STATE_PORT(ports), 0);
11031238

11041239
#if MICROPY_PY_GPIO_IRQ
11051240
MP_STATE_PORT(epoll_events) = m_new(struct epoll_event, MICROPY_PY_GPIO_IRQ_QUEUE_SIZE);
1241+
#endif
1242+
#if MICROPY_PY_GPIO_DYNAMIC_ALLOCATION
1243+
// The documentation suggests to align the buffer using the structure size
1244+
// as the alignment boundary, to improve performance. In this case we can
1245+
// make the trade-off to wait a millisecond or so later to know that a
1246+
// GPIO port went away. Even if something happens between the port
1247+
// disappearance and the inotify report, any ioctl on the port's pin file
1248+
// descriptors (or on the port's descriptor itself) would fail, and this
1249+
// should be accounted for anyway.
1250+
MP_STATE_PORT(inotify_events) = m_new(struct inotify_event, MICROPY_PY_GPIO_ALLOCATION_QUEUE_SIZE);
1251+
#endif
1252+
1253+
#if MICROPY_PY_GPIO_IRQ
11061254
gpio_epoll_init();
11071255
#endif
1256+
#if MICROPY_PY_GPIO_DYNAMIC_ALLOCATION
1257+
gpio_inotify_init();
1258+
#endif
11081259
}
11091260

11101261
void mp_pin_deinit(void) {
1262+
#if MICROPY_PY_GPIO_DYNAMIC_ALLOCATION
1263+
gpio_inotify_deinit();
1264+
#endif
11111265
#if MICROPY_PY_GPIO_IRQ
11121266
gpio_epoll_deinit();
11131267
#endif
@@ -1217,4 +1371,8 @@ MP_REGISTER_ROOT_POINTER(void *epoll_events);
12171371
MP_REGISTER_ROOT_POINTER(mp_set_t epoll_pins);
12181372
#endif
12191373

1374+
#if MICROPY_PY_GPIO_DYNAMIC_ALLOCATION
1375+
MP_REGISTER_ROOT_POINTER(void *inotify_events);
1376+
#endif
1377+
12201378
#endif

ports/unix/mpconfigport.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,25 @@ static inline unsigned long mp_random_seed_init(void) {
209209
#endif
210210

211211
#if MICROPY_PY_GPIO
212+
// If the GPIO pins are provided by a device that may or may not be always
213+
// attached to the system (ie. USB GPIO extender, PCI device that can be
214+
// disabled via `rmmod`, etc.), then enable this at the expense of a larger
215+
// binary. Otherwise, if the pins are provided by a SoC (f.e. via a device
216+
// tree file) or if you can guarantee the device providing the pins will never
217+
// be removed from a running system, feel free to disable this (with a smaller
218+
// footprint and slightly faster execution).
219+
//
220+
// This requires threading support to be enabled, as it is essentially a
221+
// background thread doing blocking reads on a file descriptor.
222+
#define MICROPY_PY_GPIO_DYNAMIC_ALLOCATION (MICROPY_PY_THREAD)
223+
224+
// Device removal events may be batched, this indicates how many events can be
225+
// reported at the same time per inotify operation. Each entry takes up
226+
// sizeof(struct inotify_event) bytes of heap.
227+
//
228+
// TODO: Make this configurable at runtime instead?
229+
#define MICROPY_PY_GPIO_ALLOCATION_QUEUE_SIZE (16)
230+
212231
// If you do not need IRQs or you are polling the GPIO pins anyway, you can
213232
// disable this (with a smaller footprint and slightly faster execution).
214233
//

0 commit comments

Comments
 (0)