DIY Connected Espresso Machine: Main Class and Indicators (Part 5)
After the last part, most of the components are wired and have their own object representation, so it’s time to encapsulate everything we wrote till this moment in the main class to provide a single API (class) to control the espresso machine and execute commands regular espresso machines do.
Just not to forget about the hardware I’ll also replace regular 220V indicators my espresso machine had with LEDs to show the boiler state as well as commands execution result.
To continue the tradition put in the previous articles, let’s start with hardware: LED indicators.
First (red) will indicate whether the boiler is working, so its anode can be directly connected to the microcontroller pin that controls the relay responsible for switching the boiler on and off. It was pin 3 for Arduino and D2 for NodeMCU.
Second (green) will represent the command status, let’s call it a “Done” indicator. I connected its anode with pin 4 for Arduino and D7 for NodeMCU.
I wired both anodes through 330Ω resistors and cathodes with the ground.
Okay, after a short warm-up let’s think about what the espresso machine is: it’s a device consisting of hardware components such as a pump, boiler, toggle, and also the “Done” pin we just introduced.
The espresso machine can execute different commands such as pouring and boiling water, making coffee with certain parameters, etc. Based on the command requested it operates its components to achieve the desired state. And also it can tell its components state if needed.
One way to implement commands can be to provide a method per command we’d like our espresso machine to execute, such as pourWater(), boil(), makeCoffee(seconds) which is explicit and straightforward.
However, after doing it this way, I found it’s quite verbose and introduces a lot of boilerplate and difficulties to handle commands in different places, so eventually, chose to have a single method command() that expects one of the symbols from a defined set of commands.
Internal and External Commands
It’s important to remember that the espresso machine (as most of the devices you would like to “connect”) also has the physical interface, mechanic toggle in my case, that is another way to control the device, for example, pour and boil water, make steam.
I suggest separating commands triggered by the toggle and calling them “internal”, while others — “external”. Such commands are internal because they are triggered by a component internal to the device. Having commands separated gives us a way to understand the source of the command and behave respectively.
Similar to the ToggleState implementation I prefer enum to describe possible commands. Having both “internal” and “external” commands in one set also helps to avoid multiple places to operate components respectively.
Composition versus Inheritance
In contrast to the Boiler class which uses inheritance to enrich the Relay behavior, the EspressoMachine class (which we’ll code in a minute) will use a composition of its components since it’s a more organic way to represent the object consisting of a number of heterogeneous objects.
Follow the article on Wikipedia if you want to learn more about differences and applications in more complex scenarios since implementation in this article is quite straightforward.
EspressoMachine Class Header
EspressoMachine class starts with a header file. We need a few properties:
- pump, boiler, toggle to keep references to the components used;
- donePin to keep the pin for the “Done” indicator;
- lastCommand to describe the last command requested and needed to execute;
- isCommandChanged boolean to flag whether the command, well, was changed;
- isDone boolean to represent the command execution result.
Nota Bene! EspressoMachineCommand is omitted to keep the code clear, however, it goes to the beginning of the same file.
The constructor expects a number of pins for the hardware components as well as for the “Done” indicator. Components getters will tell the user about their state.
The rest methods are used to:
- getCommand() — return the last (current) command requested and is being executed;
- getIsCommandChanged() — one-time getter to return and reset the corresponding flag;
- getIsDone() — return the corresponding flag;
- command() — request a command to be executed;
- work() — operate internal components to achieve the desired state, similar to other components we wrote.
First of all, the implementation described is without the making a coffee command to keep things simple.
General getters implementation is straightforward, however, there is one thing to mention about the constructor: right after the arguments list, we pass them to the corresponding components constructors.
Things become a little bit more interesting when we shift to the commands-related methods.
The separate setCommand() method is used to flag the fact of the command change which is reset when the user triggers the one-time getter getIsCommandChanged(). User’s code reacting to the command change should go right after the getter usage, similar to the Toggle::getIsToggled() implementation.
In the command() method we programmatically restrict “internal” commands usage, but also avoid changing the command if the toggle is not in the “Off” position. Rather than using throw/try/catch syntax in C++ we just return a boolean representing success (true) or failure (false) as the behavior is expected, not an exception.
The work() method is the most complex one in a certain sense since it operates internal components based on the command requested in a single place.
Firstly, it checks the toggle state and sets the command respectively. Secondly, it triggers components behavior based on the command value.
Next, it triggers boiler.work() to update its state, and eventually updates the “Done” indicator in a private method operateDone() which is separated to encapsulate the algorithm determining whether the command is executed.
At the moment the only commands we may consider as having “done” results are when the target temperature is achieved.
Make Coffee Command
Till the moment commands are quite simple since trigger internal components right away, however, we may introduce more complex commands that may have additional interim states.
As an example, I came up with the “make coffee” command that takes a number of seconds to make coffee, heats water, and pours it during this time. Also, if the temperature falls below the required, the command pauses the pouring and until the temperature becomes suitable again.
In case there are more components such as digit temperature or water flow sensor, more complex (and helpful) scenarios can be implemented.
To support the “make coffee” command we need a few helpers:
- makeCoffeeMillisLeft to represent milliseconds left to complete the command;
- makeCoffeePourWaterStartMillis to represent milliseconds when the water pouring started;
- makeCoffeeOperate() to encapsulate operations needed to execute the command.
The command() method overloading also needed to pass seconds as an argument when requesting the command.
The overloaded command() method is similar to the regular, only additional checks are added to programmatically deny incorrect usage.
The work() method extended to trigger the makeCoffeeOperate() which carries the algorithm to operate components. The latter one checks helper variables and counts the time left to complete the command. Follow the comments to understand how it works better.
To test the implementation we can trigger the command based on the user input available through the Serial port.
We can also detect and output when the command changed, for example with the toggle.
Wow, that was a long coding part! The last missing part of the Connected Espresso Machine is the “Connected”, so the next article will describe how to implement Over-the-Air updates with a Wi-Fi module.
The project code is available here: https://github.com/loginov-rocks/Connected-Espresso-Machine — it’s not finished by the moment I write this article so I will work on this during the next episodes.
That’s all for today, see you next time!