kevinkk's instincts appear to have been correct. I removed the MQTT message checking mqtt.check_message() from the loop, and it ran quite a bit longer (about a week). Until, that is, I controlled the device via local switches, and caught (via telnet) an EHOSTUNREACHABLE error which caused MQTT publish to abort the loop. So presumably this was what was happening with check_message() as well (though never logged via telnet).
I upgraded to uPy v1.13, porting over to uasyncio and Peter's fantastic mqtt_as
super-resilient MQTT library, using qos=1. I also eliminated the IRQ's for the 4 input Pins (which were just setting values being checked every 50ms anyway). The result has been super-stable and far more responsive.
Once you get used to it, uasyncio is really straightforward. In particular, setTimeout type logic with callbacks can be replaced with much more obvious-looking calls like:
If you are using MQTT, I definitely recommend giving uasyncio and Peter's library a try. The only catches I found:
- To avoid performance bottlenecks, the MQTT receiving callback is run synchronously, so you need to setup an asyncio task (create_task) quickly and return. I ended up saving such tasks for later cancellation, which worked out well. There are also queues and Events which can be used for more complex control.
- mqtt.simple takes ints and other non-strings as publish values, but mqtt_as doesn't. Just convert these first.
Other than that, and not really a catch, once you start thinking async, you will wish everything
had an async interface: file system (file existence), GPIO Pins, etc. Honestly I think it's absolutely the right paradigm for embedded programming with various network/hardware inputs/outputs.
Thanks all for the helpful suggestions.