When you are developing an IoT application, developing a good software architecture for your project can make it or break it. As you probably know, IoT development is more than just coding. The embedded programmer strives to not just fulfill the project requirements but must think of the scalability and how the system will evolve over future iterations or updates.
Here are some things you should know about building applications that require a bit of this and a bit of that.
Since Zerynth pioneers the development of Python for IoT applications, let’s look at 3 best practices in developing a secure and scalable IoT Solution.
1. Use Multi-threaded Design
Let’s say you are developing an IoT application that measures data from an industrial sensor and sends the data to a cloud service.
You have two approaches, To execute each operation sequentially just like in a super loop, or you can use two threads to execute the same application functionality.
It is advisable, in this case, to use a multi-threaded design as follows:
- The first thread is responsible for using the appropriate peripheral to communicate with the sensor and saves the data to flash memory.
- The second thread would be responsible for reading the flash memory and sending the contents to the cloud service.
This approach ensures that
- Each process is not blocking the other one. If there is an error with a sensor reading, the other thread is not blocked waiting for new data.
- The data is never lost if the second thread has any kind of error with the cloud service. The other thread gets sensor data and saves it in the flash for later usage.
On the other hand, If you use a superloop approach, each of these two operations would block the other if there is an error.
In general, Designing firmware that is robust and reliable in typical harsh working conditions of an IoT device is not an easy task. Data loss must be reduced to a minimum in case of possible power loss or absence of connectivity.
Zerynth provides a dedicated module for logging data in a non-volatile way and ensures corruption resistance.
2. Develop Less Error-prone Code
Let’s face it, you will have errors and bugs in your code no matter what level of experience you have. Our approach is to try to eliminate the syntax and logic errors during development and to catch Runtime errors before they escalate into a full system reset.
Say you want to code a helper function that divides one number by another. In the case of dividing by zero, returning None seems natural because the result is undefined.
This is a common mistake in Python code when None has a special meaning. This is why returning None from a function is prone to return an error.
The better way to reduce these errors is to never return any at all. Instead, raise an exception to the caller and make them deal with it.
This is an important coding standard you should follow, as it enables you to bullet-proof your code from any bugs like these while allowing you to catch these errors gracefully.
You Should Assert
assert is a useful function to find bugs in code during runtime. assert tests that invariants, preconditions, or postconditions are met at runtime. Basically, assert is used to make sure that a program is always in a valid state. It finds the bug, that caused the program to get into an invalid state, as early as possible.
As Tyler Hoffman brilliantly discusses in his article, Asserts should be used because:
- Asserts generally occur while the system is still in control (e.g. not in a HardFault, etc). This means that the system can safely record debugging information.
- Developers connected to a debugger very quickly find program logic mistakes and invalid use of API.
- Asserting function arguments removes the need to check for argument validity and error handling code in upper layers which, may, in turn, reduce code size.
- Good placement of asserts can reduce chances of exceptions by catching out-of-bounds accesses, invalid pointer dereferences, invalid state machine transitions, and nonsense operations.
In Zerynth, Asserts raise a critical error flag and restarts the system.
Watch Dogs are very handy as a last resort if the MCU gets stuck in an irrecoverable state.
It ensures that the software is running as expected and, if not, it will initiate a Hardware reset.
Generally speaking, a watchdog timer is based on a counter that counts down from some initial value to zero. The embedded software selects the counter’s initial value and, If the counter ever reaches zero before the software, restarts it.
3. Avoiding the copy-paste-modify trap
While the topic of code abstraction and modularity is beyond the scope of this article, I would like to discuss one topic, which was addressed brilliantly, by Brian Amos in his book: Hands on RTOS with micro-controllers.
In this case, we’ve got a piece of code “algorithm.c” that has been proven to work well and we have a new project coming up. How should we go about creating the new project? Do we just copy the working project and start making changes? The problem isn’t the act of copying and modifying the code. The problem is trying to maintain all of the copies over time.
Let’s assume that we have algorithm.c that is copied and pasted into 3 projects. Although at the moment of developing the project, each application was working as intended, we may notice the following drawbacks:
- If Algorithm.c has a bug, it will need to be fixed in three different places.
- Testing a potential fix for Algo will need to be validated separately for each project. The only way to tell if a fix corrected the bug is probably by testing on actual hardware “in-system”; this is a very time-intensive task and it is technically difficult to hit all of the edge cases.
- The forked Algo function will likely morph over time (possibly, inadvertently); this will further complicate maintenance because examining the differences between implementations will be even more difficult.
- Bugs are harder to find, understand, and fix because of all the slight differences between the six projects.
- Creating project 4 may come with a high degree of uncertainty (it is hard to tell exactly which features of Algo will be brought in, which intricacies/bugs from the SPI or ADC drivers will follow, and so on).
- If MCU1 becomes obsolete, porting algorithm.c needs to be done three separate times.
A better approach would be to develop a hardware abstraction layer between the algorithm and the MCU, SPI layers and to develop an interface layer between the algorithm and the main.c application file.
This will give us the following advantages:
- If Algo has a bug, it will only need to be fixed in one place.
- It is impossible for Algo to morph over time since there is only one copy. It will always be trivial to see whether or not the algorithm used is different between projects.
- Bugs are easier to find, understand, and fix due to the decreased interdependencies of the dependencies. A bug in Algo is guaranteed to show up in all six projects (since there is only one copy). However, it is less likely to occur, since testing Algo during development was easier, thanks to the interface.
- Creating project 4 is likely to be fast and efficient with a high degree of certainty due to the consistency across the other three projects.
- If MCU1 becomes obsolete, porting algorithm.c isn’t even necessary since it has no direct dependency on an MCU—only the ADC interface. Instead, a different BSP will need to be selected/developed.
Have you checked our Demos and Tutorials using Zerynth OS?
If you have questions, drop them at our community forum.