Complex Arduino project design explained on the ultimate cloud cat feeder source code
How do you write a fairly complex Arduino project? Are there any best practices? …and for those who waited, the cloud cat feeder source code is now publicly available.
The challenges of programming an Arduino microcontroller
If you started your Arduino journey from a simple project - controlling LED with a simple switch, probably you faced different loop-based programming quirks… For example:
- How to filter out the period when you start to press the switch, but it’s not fully pressed yet, and when you read it’s status in a loop, you actually read it several thousand times, triggering the LED off and on all the time?
- How to be sensitive and quick for the switch state change, but turn the LED on/off exactly once?
- What if the user will hold the switch?
- What if you want to extend the program? For example, how to blink the LED every second, when it’s in “on mode”?
- How do you execute multiple operations at once, if you have just a single thread and a single core? (Even when using ESP32 with two cores, one is fully dedicated to WiFi, leaving you with a single core)
- What if you want to build a device which will measure 3 load cells few times per second, aggregate each 12 results, filter out min and max readings, calculate the average, send it to the cloud, while continuously measuring time and displaying smooth rainbow animation on a triple color LED ring?
Before I started working on the ultimate cloud cat feeder, I was researching a little for Arduino best practices and design patterns, and I’ve found very little information. Here is my contribution, as well as explanation how the firmware of the cat feeder works on a high level.
Ultimate cloud cat feeder - firmware architecture on a high-level
If you haven’t seen it before, this is the ultimate cloud cat feeder:
Music Credits: Fruits by JayJen Music https://soundcloud.com/jayjenmusic
All of it is controlled by single microcontroller, - ESP32 running Arduino (and a few more parts):
Edit it on Draw.io
Find the code here: TheUltimateCloudCatFeeder-Software - GitHub repository
1. Use protothreads
Protothreads, developed by Adam Dunkels are beautiful way of achieving multi-thread-like-ish environment with a single thread. Quoting his website: “Protothreads are extremely lightweight stackless threads designed for severely memory constrained systems (…)”. Because of that, the cat feeder main loop just runs (almost non-blocking) protothreads and measures theirs performance. Of course, some microcontroller operations are blocking, like digitalWrite, however no Arduino delay function should be used in the main execution path. Instead, use expiring timers and give away the computing power from waiting protothread. Example - using PT_WAIT_UNTIL instead of delay when playing a note
2. Create hardware abstraction
Although low-level hardware interaction can be verbose in code, usually that interaction is well defined and easy to abstract. Consider - for example - LidMotor. All we need from that physical component is to open and close the food lid. The reality is much more complicated: first, I need to check the lid position (to not close the lid, if it’s already closed) and for that I need to read a Hall sensor. Once position of the lid is established, the stepper motor has to start making steps, sleeping certain duration in between the steps. This has to be repeated until the second Hall sensor is triggered. After that, I need to open the lid a tiny bit more, so the lid is fully opened/closed. After this operation, I want to store how many steps were done in total, so I can spot any issues before they will impact the user (my cat!). Abstraction here is powerful: LidMotor::openLid(). No more details are needed to use this method.
Additionally, having hardware abstraction will allow you to write unit tests, if you would ever want to.
3. Limit the number of common utils, but do use them
I’ve found that I need just 3 utils used almost by every class: logging, timer and InMemoryStore:
- Logging allows me to put logs to Serial Monitor/SD Card.
- Timer allows me to create and set a timer, which is used in many places by protothreads, instead of blocking Arduino delay function.
- InMemoryStore contains a buffer of encoded metrics. Any protothread can put new metrics to the store and there is another protothread constantly monitoring the buffer utilization. If it gets close to 4.5kB (AWS IoT charges every 5kB), the protothread publishes the message (there are other conditions to push the message to the cloud, for example when the cat consumed the food - I want to get Emma’s weight ASAP :) ). The process of metrics collection, and integration with the cloud is straightforward:
Edit it on Draw.io
More details in upcoming posts.
4. Monitor everything
In one of the earlier versions of the cat feeder there was a problem that it was stopping working once every week or two, without an apparent reason. Having all the metrics I have, the problem was easy to spot. The number of free heap size was “casually” going down to 0 bytes. Why it was going down to 0? On another graph I spotted that the free heap size drop was happening whenever the “AWS IoT reconnection flag” was up. Conclusion: the library I was using to connect to the AWS IoT had a memory leak! Three hours later, I swapped out the library for another one, fixing the issue. I can’t even imagine how long it would take me to figure out the problem without the metrics.
Firmware - What needs improvement?
Don’t get me wrong, the cloud cat feeder design and code is NOT perfect. For instance, here are some things I would improve:
- Protothreads library require you to define the variables as static. I wish there could be more object-oriented way of using protothreads.
- There are multiple TODOs left in code - mostly hardcoded values, which should be specified in the config file.
- C++ is not my native language - there are probably some bad constructs in the code. For sure I would want to introduce proper C++ singleton pattern for all the hardware abstractions.
- Unit tests are missing.
- There can be many more features added - like opening the cat feeder from the cloud, or changing the food schedule.
What worked well?
I’m pretty happy that the code is running now (almost) flawlessly for a longer period of time (3+ months). The design approach described here turned out to work great for me, yielding minimum amount of bugs. I’m super happy I went into protothreads path - some components were dead simple to write.
You can find more details on the cat feeder (building progress, features and technical details) here - all posts with cat-feeder tag.
Please note: the views I express are mine alone and they do not necessarily reflect the views of Amazon.com.