Summary
RaspberryPi.module_init() in epdconfig.py calls self.SPI.open(0, 0) unconditionally. spidev's open() does not close an already-open handle, so every call after the first leaks one /dev/spidev0.0 file descriptor.
A long-running program that redraws the panel periodically will eventually exhaust its file descriptors. Because the leaked descriptors consume the process-wide NOFILE budget, the first visible symptom is usually unrelated network calls failing, which makes this quite hard to trace back.
This is reachable from the documented API
Switching between 4Gray and 1Gray requires calling init(mode) again — there is no other API for it. The bundled example does exactly that:
RaspberryPi_JetsonNano/python/examples/epd_3in7_test.py
23: epd.init(0)
88: epd.init(1) # 1 Gary mode
103: epd.init(0)
114: epd3in7.epdconfig.module_exit()
Three init() calls, one module_exit() — so running the example as shipped leaks two descriptors. It exits immediately so nobody notices, but a daemon doing the same thing does not.
Measured
Raspberry Pi 3B, epd3in7, refreshing about once a minute (a full 4Gray repaint plus a 1Gray partial, so init() is called on each cycle):
open FDs: 1024 <- the default NOFILE soft limit
of which: 1012 x /dev/spidev0.*
Time from a cold start to hitting the ceiling: ~16.7 hours. After that, every outbound HTTPS request failed with:
Failed to establish a new connection: [Errno 22] Invalid argument
...which is what sent me looking at the network for two days. The display had simply frozen while the process stayed alive and healthy-looking.
Quick check for anyone else seeing this:
ls -l /proc/<pid>/fd | grep -c spidev # healthy: 1
Suggested fix
The same guard SunriseX3.module_init() already uses in this file:
if self.Flag == 0:
self.Flag = 1
self.SPI.open(2, 0)
...
Applying that pattern to RaspberryPi, and resetting Flag in module_exit() so a genuine teardown still re-opens. PR to follow.
Possibly related
#87 and #32 report the mirror-image symptom — Bad file descriptor when init() is called after sleep(), i.e. the handle being closed rather than leaked. Both stem from the same thing: module_init()/module_exit() track no state about whether the SPI handle is open. Tracking it addresses both directions.
Summary
RaspberryPi.module_init()inepdconfig.pycallsself.SPI.open(0, 0)unconditionally.spidev'sopen()does not close an already-open handle, so every call after the first leaks one/dev/spidev0.0file descriptor.A long-running program that redraws the panel periodically will eventually exhaust its file descriptors. Because the leaked descriptors consume the process-wide
NOFILEbudget, the first visible symptom is usually unrelated network calls failing, which makes this quite hard to trace back.This is reachable from the documented API
Switching between 4Gray and 1Gray requires calling
init(mode)again — there is no other API for it. The bundled example does exactly that:RaspberryPi_JetsonNano/python/examples/epd_3in7_test.pyThree
init()calls, onemodule_exit()— so running the example as shipped leaks two descriptors. It exits immediately so nobody notices, but a daemon doing the same thing does not.Measured
Raspberry Pi 3B,
epd3in7, refreshing about once a minute (a full 4Gray repaint plus a 1Gray partial, soinit()is called on each cycle):Time from a cold start to hitting the ceiling: ~16.7 hours. After that, every outbound HTTPS request failed with:
...which is what sent me looking at the network for two days. The display had simply frozen while the process stayed alive and healthy-looking.
Quick check for anyone else seeing this:
Suggested fix
The same guard
SunriseX3.module_init()already uses in this file:Applying that pattern to
RaspberryPi, and resettingFlaginmodule_exit()so a genuine teardown still re-opens. PR to follow.Possibly related
#87 and #32 report the mirror-image symptom —
Bad file descriptorwheninit()is called aftersleep(), i.e. the handle being closed rather than leaked. Both stem from the same thing:module_init()/module_exit()track no state about whether the SPI handle is open. Tracking it addresses both directions.