Here on Hackaday, we’re generally designers of hacks that live in the real world. Nevertheless, I’m convinced that some of the most interesting feats are at the mercy of what’s possible in software, not hardware. Without the right software, no 3D printer could print and no quadcopter could fly. The source code that drives these machines may take months of refinement to flesh out their structure. In a nutshell, these software packages are complicated, and they don’t happen overnight.
So how do they happen; better yet: how could we make that happen? How do we write software that’s flexible enough to coexist peacefully on all sorts of hardware variations?
What separates the big open-source software projects like ROS, LinuxCNC, and Multiwii from the occastional hackathon code are the underlying principles that govern how they’re written. In object-oriented programming, these principles are called design patterns. In the next couple posts, I’m going to crack the lid on a few of these. What’s more, I’ll package them into real-world examples so that we can see these patterns interact with real hardware. The next time you pop open the source code for a big open source hardware project, I hope you can tease out some of these patterns in the code. Better yet, I hope you carry these patterns into your next robot and machine project. Let’s get started.
For readability, all of the examples run in Python3. The snippets below are truncated for brevity, but the real examples in the repository will work if you’ve got a similar hardware setup.
Software Prophets of Yore
Before jumping in, I need to promise you that these design patterns aren’t mine. In fact, most of them are decades old. If you’re curious, check out the book: Design Patterns, written by a group of authors who’s names are so hard to remember that we shall collectively refer to them as “the Big Four.” For many software engineers, this book stands as the feature-complete reference for object-oriented design patterns, and it’s rock solid. I thought about delivering this post as a book review where I’d drool over all the swanky software patterns that you could put in your next blinky project. The problem? No blinkies! In fact, the entire book builds examples around writing a graphical document editor. For us hardware monkeys, that’s boring and bordering on unacceptable!
With that in mind, I’ll be presenting the words of the wise embellished with the proper treatment of real-world hardware.
Getting the Most from these Examples
Now for the unanswered question: who are these examples for? Hackaday readers come in all flavors. In writing this piece, I’m hoping to bridge two communities: the software folk eager to dabble in hardware and the hardware folk hungry to write better software. For the software folks, these examples are here to showcase what you already know and give them some real hardware context. For the hardware folks, these design patterns are here to muscle-up your toolbox of software techniques with sensors you’ve likely seen before.
To set the base line for getting the most out of what comes next, I’ll assume two things. (1) You’ve had a taste of object-oriented programming by writing a few classes in Python, and (2) you understand the basics of class inheritance. We’ll be building on more advanced types of inheritance in the coming examples.
Getting Cozy with the Hardware
Today I’ll get us warmed up with our design patterns by reading a variety of temperature sensors. The plan? I need a code base that provides me with a way to easily read two different temperature sensor types: a thermocouple and a thermistor. The key ideas behind this are three-fold:
- isolate behavior that can stand on it’s own.
- hide (with abstraction) details that are unnecessary for the end-user.
- don’t repeat yourself; share behaviors by following the first clause.
At first, it might seem like we’re just writing a few drivers to read a few temperature sensors. In reality, we have to think at a higher level! Lets ask ourselves: “what’s the core feature that our software provides? How might someone else with a different setup adapt this code to get that same core feature without having to rewrite it?” In our case, we’re providing a common way of performing temperature measurement with a path for expansion to other temperature sensors.
Without further ado, I present today’s hardware setups.
The first setup is a Beaglebone set to read an off-the-shelf I2C-based analog-to-digital converter that’s reading a Thermocouple amplifier.
The second setup is Beaglebone set to read the same I2C-based analog-to-digital converter that’s now reading a voltage-divider setup with a thermistor.
With the real hardware in mind, let’s now discuss our design patterns as we build up a flexible codebase to read both sensors.
Polymorphism
We start by teasing out the underlying objects that will comprise our system. First off, we have two separate real-world sensors, both of which mandate a different way of being read as input. Next, we also know that, although these sensors require slightly-different setups, they’re both measuring the same thing: temperature!
With that in mind, let’s write out some requirements. Let’s say that we need to:
- isolate device-specific behaviors that are needed to read a particular sensor.
- provide a common software interface for reading the temperature regardless of temperature sensor type.
Given the requirements, it sounds like we want to write two separate classes, each with specific behavior for reading temperature data. It also sounds like we should make sure that we’re consistent in naming conventions for reading the temperature. Let’s say we write a class for each sensor.
class Thermistor: ... def read_temperature_c(self): # thermistor-specific details here
class AD8495ThermocoupleAmplifier: ... def read_temperature_c(self): # thermocouple-specific details here
Here we’ve mocked up something that does just that. Device-specific behavior is wrapped into classes, and all our temperature-reading methods have the same name. We’re done, right?
While this idea works, it’s a bit naive. In the source code above, we’re under no design contract to give these methods the same name. Heck, we could call one read_temp and another hi_mom, and no one can stop us–not even our mothers! Lucky for us, we can use a design pattern to enforce that these methods be named identically.
Looking at our problem, we have two different types of temperature sensors, but, by golly, I just want a consistent way to ask for the temperature from each of them. Behold, Polymorphism with abstract base classes. Polymorphism is a design pattern that promises a common interface to entities that may not all be the same type. Here, we have three different sensor classes. Polymorphism can ensure that they all use the same method to read out the temperature.
To implement polymorphism, we use an abstract base class. An abstract base class is a special type of base class that can’t stand on it’s own. It needs to be inherited. In fact, if we try to instantiate the base class by itself, Python will throw an error. Abstract base classes have one extra bonus feature: abstract methods. These are placeholder methods declared in the abstract base class that don’t do anything. Instead, they serve as tags and force the derived class to define them. If we try to instantiate an child class of the abstract base class without defining all the abstract methods, Python will also throw an error.
With two new ways to throw errors, it’s worth asking: “what’s the benefit of constraining ourselves this way?” It turns out that these constraints are actually a way of making a promise. Here, we’re promising that each child class we write for our temperature sensors will define any abstract methods in the base class.
Since both of these temperature sensors is just that–a type of temperature sensor–we’ll use TemperatureSensor as our base class name.
from abc import abstractmethod, ABCMeta # abstract base class library tools class TemperatureSensor(object, metaclass=ABCMeta): ... @abstractmethod def read_temperature_c(self): """ returns the temperature in Celsius """ pass
Above is the Python3 way for defining an abstract base class. Here, the method read_temperature_c is just a placeholder. We don’t need to define what it does here because the details are specific to our sensors. Here we just declare it and decorate it with the @abstractmethod decorator to tell Python that any child class of TemperatureSensor must define the read_temperature_c method.
To finish this example, lets make our Thermistor class inherit our abstract base class:
class Thermistor(TemperatureSensor): ... def read_temperature_k(self): """ returns the temperature in Kelvin """ voltage_v = self.voltage_input.read() r1_ohms = self._read_resistance(voltage_v) return (1/298.15 + 1/self.b * log(r1_ohms/self.thermistor_ohms))**(-1) def read_temperature_c(self): return self.read_temperature_k() - 273.15
First, notice that our Thermistor class inherits our TemperatureSensor class. We’re telling our users: “hey, this is a Thermistor class, but you can read it like a TemperatureSensor.” It also tells us, the writers, “hey, I’m promising to define a read_temperature_c method.” Finally, we just write out all the code necessary for reading a thermistor and converting it to Celsius. And that’s it!
Let’s take a step back and think about why this is powerful. Our user could be someone who might not know a thing about how thermistors work, but they can still take measurements with the generic read_temperature_c method. Encapsulating domain-specific information is exactly what we want to do here such that our users don’t need to know every detail about our software stack to get something working. Here, our code is hiding he messy details of converting a resistance to a temperature from a polynomial-fitted line. By hiding those details, we enable them to move on to the next step and write some interesting temperature sensing applications.
Bridges
In the last section, we blew past one line without discussing it.
voltage_v = self.voltage_input.read()
Fear not! We’ll cover that part now.
At this point, we’ve got a common interface to ask for the temperature, but we could have dozens of different ways to actually collect the raw data. How do we write a single class that properly encapsulates the details of each temperature sensor without getting to heavily committed to the downstream devices necessary for reading them?
To get a better picture of the problem, let’s have another look at a block diagram of our physical hardware:
But that’s not our only possible configuration. Since I’m using a Beaglebone in these examples, I could also use the built-in ADC on the BeagleBone ARM chip itself. That hardware configuration might look like this:
What we need to write is a series of classes for our setup that somehow detach the thermocouple from the extra hardware that’s being used to read it. To do so, we need to decide what components are specifically necessary for reading this thermocouple and what components can be replaced without changing the end-to-end system behavior. In this case, our AD8495 is tightly coupled with the thermocouple. Without it, we just can’t read it at all! On the other hand, that ADS1015 is just a vanilla analog-to-digital converter. Heck, we could replace it with any A-to-D, and we would still get our thermocouple measurements. For this reason, we’ll start off by writing two classes: a class that encapsulates the AD8495 behavior and a class to handle the analog interface.
Now’s a great time to map out our software with a block diagram to get a picture of the code we’ll be writing. As a heads-up: these diagrams aren’t certified “UML class diagrams”; rather, for clarity’s sake, I’ve doodled up a simplification.
Let’s have a look at the block diagram on the left. These boxes represent the relationship between classes. Boxes-within-boxes represent inheritance. We call this the “is-a” relationship. Arrows from one box to another represent a class attribute that happens to be another class. We call this the “has-a” relationship.
In our block diagram, the AD8495TCAmplifier is a TemperatureSensor because it inherits from the TemperatureSensor class. (That’s the Polymorphism we described above.) The AD8495TCAmplifier also has a VoltageInputInterface. In other words, our AD8495TCAmplifier owns a reference to another class that we can invoke with self.voltage_input (mentioned earlier).
So what exactly is that VoltageInputInterface class, and how does it interact with the real world? Well, the short answer is that it depends on our hardware setup! In the first setup, the VoltageInputInterface is one of the four available analog inputs on the AD1015 A-to-D. On the second input, the VoltageInputInterface is a single pin on the Beaglebone’s exposed analog port. In all cases though, each VoltageInputInterface needs to provide a read method that does the actual work of reading a single analog input and returning the value in volts. How might we guarantee that? With an abstract base class like we did in the previous section! Now that we understand the relationship, we can refine our software block diagram with both optional setups.
Notice how, in both diagrams, the AD8495TCAmplifier owns a different subclass of a VoltageInputInterface, but it treats them like a vanilla VoltageInputInterface. Even though the underlying class is different, we can still treat identically by using the base class read method.
What we’re describing is the bridge design pattern. The bridge, according to the Gang-of-Four, is meant to “decouple an abstraction from its implementation so that the two can vary independently.” Here, our abstraction is a single analog input. Since there are various ways of providing our Beaglebone with an analog input, we need an abstraction such that we can address all sorts of ADC hardware the same way.
This design pattern is rather similar to Polymorphism, which I described first. In fact, just like before, we’ll use Python’s abstract-base class library to implement it. The difference comes in our use-case. Beforehand, we needed a common way to address all types of temperature sensors. Now, we’re trying to separate a temperature sensor from any hardware that’s not specific to the temperature sensor setup.
Now that we have our diagram, we can divvy up system behavior across classes. Everything related to converting voltage to temperature will live in our AD8495TCAmplifier class. Everything relating to reading our analog input will live in a separate class.
Without further ado, let’s write our AD8495TCAmplifier class:
class AD8495TCAmplifier(TemperatureSensor): def __init__(self, voltage_input_interface, v1_v=1.25, t1_c=0.0, v2_v=1.5, t2_c=50.0): # generate slop and y-intercept for line formula based on two data points: self.gain = (t1_c - t2_c)/(v1_v - v2_v) self.offset = self.gain * (0 - v1_v) + t1_c self.voltage_input = voltage_input_interface def read_temperature_c(self): voltage_v = self.voltage_input.read() return self.gain * voltage_v + self.offset
Just like described before, our AD8495TCAmplifier is just a child class of TemperatureSensor, which means we’ll need to implement a read_temperature_c method. Next off, to instantiate this class, notice that we pass in a reference to a voltage_input_interface. That’s our VoltageInputInterface object described above. Notice how, in the read_temperature_c method, we call it’s read method to get a voltage out. (Don’t get too bogged down the the other parameters: v1_v, t1_c, v2_v, and t2_c. Those are just two datapoints that we can derive from the datasheet and the circuit’s reference voltage that will dictate the gain and offset.)
Next off, comes the VoltageInputInterface and ADS1015VoltageInputInterface child class that inherits it.
from abc import abstractmethod, ABCMeta # abstract base class library tools class VoltageInputInterface(object, metaclass=ABCMeta): ... @abstractmethod def read(self): """ returns the analog voltage in volts """ pass class ADS1015VoltageInputInterface(VoltageInputInterface): # Gain to Volts-per-bit conversion from From datasheet Table 1 volts_per_bit= \ { 2/3: 0.003, 1: 0.002, 2: 0.001, 4: 0.5, 8: 0.25, 16: 0.125 } def __init__(self, ads1x15, channel_index, gain=2/3): super().__init__() self.ads1015 = ads1x15 self.gain = gain self.channel_index = channel_index self.ads1015.start_adc(channel_index, gain) def read(self): raw_bits = self.ads1015.get_last_result() return raw_bits * self.__class__.volts_per_bit[self.gain]
Notice how the ADS1015VoltageInputInterface does quite a bit of heavy lifting to hide away all the details of the actual ADS1015 driver just to expose it with a single read method. That’s exactly what we want–to trade all the unnecessary device-specific details of our voltage input for a simple interface.
Now we just write a Python script to “wire them all up,” connecting our object dependencies.
import time from object_oriented_hardware.ads1x15 import ADS1015 from object_oriented_hardware.ads1x15 import ADS1015VoltageInputInterface from object_oriented_hardware.temperature_sensors import AD8495TCAmplifier from object_oriented_hardware.beaglebone_i2c import BBI2CBus2 i2c_bus_2 = BBI2CBus2() adc_bank = ADS1015(i2c_bus_2) voltage_input = ADS1015VoltageInputInterface(adc_bank, channel_index=0) thermocouple = AD8495TCAmplifier(voltage_input) while True: print(thermocouple.read_temperature_c()) time.sleep(0.5)
And that’s it! That about covers it for the first week. This time we covered polymorphism, which provides different classes with a common interface, and bridges, which detaches our classes from some implementation-specific components. Both of these patterns make our code more generalizable and flexible across different types of hardware. If you’re looking for more context, or you want to run these examples, have a go at the source code. Until then, tune in next time where we’ll talk singletons and stubs!
{written by a group of authors who’s names are so hard to remember that we shall collectively refer to them as “the Big Four.”}
Did you coin that? If so then why? They have been known as “The Gang of Four” for a long time!
No, just messed it up with Metallica, Slayer, Megadeth and Anthrax.
Ok, so I couldn’t get far in reading without commenting….perhaps you were just listing examples of software on hardware and not good software on hardware when you mentioned 3d printers. Granted, its capabilities are awesome, and I give the devs credit for that, but the code quality? Atrocious. All I wanted to do was refactor everything into logical units….and untangle the spaghetti. Maybe the mention of LinuxCNC is where the good code is….I’ve not looked at that. Either way, I wouldn’t hail 3d printer software as bastions of great object oriented code making use of design patterns. Perhaps they are using a huge collection of design patterns with no architectural design pattern, which really amounts to naming spaghetti strands, and what I’ve seen the industry shift to once design patterns became a really hot topic and separated the competent from the idiots. Spaghetti, by any other name is still spaghetti, and last I checked 3d printer code running on the printer (the so called firmware….though we know from an earlier hackaday post that’s not really firmware), it was most definitely spaghetti!
I’m waiting for the tomato pattern.
You get that in CI when someone changes a line of code in the spaghetti and all the test result dashboards turn red.
Firmware and OOP don’t really mix. Unless you really, really hate future developers of a given source code. And while object-oriented programmers were still fondling their objects, world had moved on and functional programming is a thing now. And most of those object fondlers will follow every anti-pattern possible. And what they do is not even spaghetti. Spaghetti is fun and tasty. They write code that is like hair on that dog I once saw in “Animal Cops” on TV that basically turned into very dirty, very smelly, puke-inducing felt…
While I’ve seen people writing OOP C code for microcontrollers with bad results, OOP C++ code for microcontrollers can be quite good.
While I agree with most of what you said, one, functional was around, and “was a thing” before object oriented programming was ever a thing. C++ was once referred to as C (a functional language) with class.
What you’ve likely never witnessed is GOOD object oriented code. I’ve seen my fair share of trash, and admittedly, there’s more trash than diamonds….but that’s true in functional programming as well.
The spaghetti comment reflects my own of people generating these micro-design patterns, that aren’t really design patterns at all, but they call them such, so they too can be part of the good design crowd that uses patterns. In your example, it would be naming the individual hairs on that dog you saw on animal cops.
My point is, having written good OO systems, frameworks where I have seen “bad” entry level devs manage to be productive, you’d know if you had seen good OO code. Good OO code will be modular, scalable, and much more clear. The things hidden will be the complex stuff that bogs people down, but it won’t be hidden behind a mass of twisted code, its hidden behind an interface, the proper way, so anyone wishing to understand it doesn’t have to look through 1000 classes to find the part they’re looking for, and should someone have a better way, they can easily reimplement it, or implement it for a different system…Its not hard, but there are a lot of developers who have never experienced good OO, so they don’t even know what to do, so they continue making the garbage most of us see and have to work on.
“For readability, all of the examples run in Python3”
Python? PYTHON? I stopped reading right there.
When I started with the job I’m in now, I had a project to read data in from a Satec BFM energy meter over Modbus and dump the data out in a CSV-like format… I was given two language options…
* Python
* PHP
I considered myself a better PHP and Perl programmer than Python at the time, but I chose Python… and found it to be a joy. Years later, still doing similar work, and while we do some NodeJS too, and I do a lot of C, I still do lots of Python. If I need to make a script to generate some boilerplate C code for instance, I’ll find myself reaching for the Python interpreter to do it. Thus I wouldn’t be too quick to criticise.
2 days left on the Humble Bundle Python collection. The one on robotics seems nice.
One thing I’d recommend with design patterns is to weigh cautiously if you really need them, each pattern has been designed to tackle a specific problem, if that problem is not present, don’t use a pattern, that’d be just a waste of time.
I’m also fond of the YAGNI principle, don’t use a pattern just because you may need it in the future (but do If you know for sure that problem will arise).
I know it’s hard to tell for sure what the software will be in 6 months. At work a specific way of sorting a project’s files has been pushed on us as mandatory about 8 years ago “in case”, I can tell you that case never occurred on any web or desktop software I worked on.
Every programmer I know, who can’t program for beans, spends time in design pattern seminars — while the rest of us write software.
Sort of the difference between the architect, and the tradesman.
Yahoo, Patterns are back!! I joke of course. Patterns never really being in the picture (universities don’t teach it…). I defy any programmer to declare he/she uses it on a day basis. How comes nothing really changed in software business in 30 years? The buzzwords are differents but this is the same coding techniques as before. What we call Deep Intelligence are the same unreliable neural networks as we used to code in the 90’s, exactly the same algorithms, same crap with local minima and blind spots, everything. Why in 30 years we invented nothing in software programming so far?
This article has got nothing about using software design patterns for hardware. It’s about using software design patterns for … software.
The author has implicitly stated this article will be a series… Just wait.
I am from a s/w background, and got into patterns about 10 years ago, I find it interesting that patterns are being mentioned on this site. However, Eventually I associated such OOP approaches with Unit Testing and Test Driven Development (TDD) and the whole “refactoring OOP code” scene (i.e. Fowler etc).
One itch I have yet to scratch is – how can I unit test hardware? That is something I have never worked out how to do, and I really hope anyone can give me some ideas on what approach I could be taking.
I mean if you imagine you are making 10 pro mini based temp sensors with NRF24 radios. You’d start off with one, and would you have tests like TestNRFIsConnectedProperly() So for your 2nd and nth boards, you’d run the unit test code first as a way of double checking your wiring and components worked as expected, rather than going straight to adding just your code implementation.
Does anyone do this or similar?
You may use the term unit tests, but then again it might be called wiring test or sanity test I suppose, or do you do what I seem to do first, and just watch for patterns of blinking LEDs, or output over serial. My code seems overstuffed with code like this
if(DEBUG) sprintf(somevar);
In my experience, there usually is a split between (hardware) production tests and software tests. The production tests run during production to verify that the hardware works properly. The production tests might be in a separate firmware that is replaced with application firmware after tests have passed. Sometimes there are also self tests that run on startup, or at will, that tries to verify that the hardware still works properly. Then we have the software tests that verify that your particular software works on the given hardware. Both software and production tests can be part of the build, but in this case you want to verify that the production test itself works and can be used to qualify each hardware unit in a production run.
Thanks for your reply [Knut E]. I tried searching the term production tests and arduino, but came up with a couple of ideas, but not much. I like your idea of a hardware self-test on startup, the only problem I can see being that of finding space for the extra code needed.
Forgive me for sticking with the arduino theme here, but it serves as an example. There exists a unit test “framework” for arduino s/w code (arduinounit), with some very simple useful data comparison assertions and boolean “truthy” assertions.
This is probably demonstrates what I imagine a production test framework would supply :
#include genericProductionTests.h
void setup() // set serial baud etc
bool test_NRF24_SPI_is_connected() {
// extends/implements test_SPI(LIST_OF_uC_PINS_HERE);
}
The production test run-time code builds out from a the library of basic known tests (e.g. SPI, I2C), which contains the normal queuing and reporting framework one associates with a unit tester. Sorry if this is sounding like pointless rambling.
Learned some new stuff with very detailed information.