The point of this small embedded project was to get familiar with object-oriented programming (oop) in the context of embedded devices. Before this I’d only used basic Micropython or C for embedded programming, so I spent most of the effort from my two brain cells on trying to designing sensible interfaces and inheritance for the components present in the system.
I also did some more reading on cmake build systems and how to better configure them, which directly affected the general project source structure. Overall, I’m quite happy with how it turned out, but there were some issues with passing the correct board type to pico-sdk that I didn’t have time to fully resolve. Something to do with my cmake fragments and their active scope, I suspect…
Source Structure
GDOOR
βββ module
βΒ Β βββ paho-mqtt
βΒ Β βββ pico-sdk
βββ include
βΒ Β βββ components.hpp
βΒ Β βββ config.hpp
βΒ Β βββ gdoor.hpp
βββ src
βββ components.cpp
βββ gdoor.cpp
βββ main.cpp
Above is the relevant source structure for the code in this project. I split the SDKs and other external modules into a separate module directory, to leave the src (implementations), and include (definitions) clean for project-specific code only.
Beginning from the main.cpp, we simply instantiate the device object and call the statemachine-style
member function on an endless loop.
1#include <stdio.h>
2#include "gdoor.hpp"
3
4int main() {
5 stdio_init_all();
6 printf("\n<BOOT>\n");
7
8 GarageDoor gdoor;
9 while (true) gdoor.run();
10
11 return 0;
12}
Definitions

GarageDoor
Next would be appropriate to go over the definition of the machine itself, as well as the namespaces that are used for configuration and tuning of the device and its components.
Here’s the gdoor.hpp which defines the device interfaces and data fields:
1#pragma once
2
3#include "components.hpp"
4#include "config.hpp"
5
6class GarageDoor {
7 private:
8 // Components
9 Components::StepperMotor motor;
10 Components::Button button_a, button_b;
11 Components::Limit floor, ceil;
12 Components::RotaryEncoder rot;
13 Components::Eeprom storage;
14 Components::WifiMQTT remote_control;
15 Components::Led led1;
16 Components::Led led2;
17 Components::Led led3;
18
19 // States and Data fields
20 uint8_t magic = Config::GDOOR_MAGIC;
21 Config::GDOOR_STATE state = Config::GDOOR_STATE::UNKNOWN;
22 Config::GDOOR_STATE state_prev = Config::GDOOR_STATE::UNKNOWN;
23 Config::GDOOR_MOV movement = Config::GDOOR_MOV::NONE;
24 uint32_t last_input = 0;
25 uint8_t stuck_counter = 0;
26 bool calibrated = false;
27 uint8_t calib_steps = 0;
28 uint8_t current_step = 0;
29 bool remote_needs_informing = false;
30 public:
31 // Constructor
32 GarageDoor();
33
34 // Member functions
35 bool at_floor();
36 bool at_ceil();
37 bool check_stuck(int rotary_initial_value);
38
39 Config::GDOOR_STATE close(bool in_calib);
40 Config::GDOOR_STATE open(bool in_calib);
41 Config::GDOOR_STATE calibrate();
42 Config::GDOOR_STATE stuck();
43 Config::GDOOR_STATE change_state();
44
45 void mqtt_send_state(Config::GDOOR_STATE new_state); // TODO: obs notify?
46
47 bool load_data();
48 bool save_data();
49
50 void print_data();
51 void run();
52};
This is quite self-explanatory as well. The machine definition depends on the definition of
its components in components.hpp and the namespaces available in config.hpp.
There are some data fields that could –and should– be simplified away, but I made some
shortsighted decisions in the implementation that neccessitated the availability of some
data outside their original scope in member functions. I would additionally restrict some
of the member functions to be private, because they realistically don’t need to be called
from the outside (e.g. at_floor(), at_ceil()).
Components
The components.hpp defines the elementary level components used in the actual device.
Most of these are really simple GPIO-pin derived classes that just have a consistent
constructor to configure a specific combination of pullups, io, or inversion (see: Button, Led, Limit, Driver).
This file, naturally, depends on the actual SDK implementations for configuring the hardware via the official pico-sdk as RP2040 was used for this project.
I wasn’t in charge of the Wifi and MQTT implementation used in this project, so I can’t comment on it too much, but I feel like that component on its own has enough dependencies that it could’ve maybe been separated into its own files.
1#pragma once
2
3#include <stdio.h>
4
5#include "pico/stdlib.h"
6#include "hardware/gpio.h"
7
8#include "config.hpp"
9
10#include "MQTTClient.h"
11#include "MQTTConnect.h"
12#include "MQTTPacket.h"
13
14#include "mqtt/IPStack.h"
15#include "mqtt/Countdown.h"
16
17
18namespace Components {
19 class GPIOPin {
20 private:
21 uint pin;
22 bool output;
23 bool pullup;
24 bool invert;
25 public:
26 // Constructor
27 GPIOPin(uint pin0, bool output0, bool pullup0, bool invert0);
28
29 // Member functions
30 bool read();
31 void write(bool value);
32 uint pin_nr();
33
34 // Operators
35 bool operator() ();
36 void operator() (bool value);
37 operator int();
38 };
39
40 class Button : public GPIOPin {
41 public:
42 // Constructor
43 Button(uint pin0);
44
45 // Member functions
46 bool pressed();
47 };
48
49 class Led : public GPIOPin {
50 private:
51 uint32_t state_last_changed = 0;
52 public:
53 Led(uint pin0);
54 void blinks();
55 };
56
57 class Limit : public GPIOPin {
58 public: Limit(uint pin0);
59 };
60
61 class Driver : public GPIOPin {
62 public: Driver(uint pin0);
63 };
64
65 class StepperMotor {
66 private:
67 Driver a, b, c, d;
68 uint16_t drive_pos;
69 public:
70 // Constructor
71 StepperMotor(uint pin_a, uint pin_b, uint pin_c, uint pin_d);
72
73 // Member functions
74 void drive(bool reverse);
75 };
76
77 class RotaryEncoder {
78 private:
79 GPIOPin pin_a, pin_b;
80 static RotaryEncoder* instance;
81 uint32_t last_active;
82 volatile int counter = 0; // TODO: use pico que?
83 public:
84 // Constructor
85 RotaryEncoder(uint pin_a0, uint pin_b0);
86
87 // Member functions
88 void react(int x);
89 static void trampoline(uint gpio, uint32_t event_mask);
90 void handler(uint gpio, uint32_t event_mask);
91
92 int get_count();
93 void reset_count();
94 uint32_t get_last_active();
95 };
96
97 class Eeprom {
98 private:
99 uint address;
100 uint op_delay_ms;
101 public:
102 // Constructor
103 Eeprom(uint pin_sda, uint pin_scl, uint address0, uint op_delay_ms0);
104
105 // Member Functions
106 int split_u32_to_8(uint32_t src, uint8_t *dst, int pos);
107 int split_u16_to_8(uint16_t src, uint8_t *dst, int pos);
108 uint32_t merge_u32_from_8(uint8_t *src, int *pos);
109 uint16_t merge_u16_from_8(uint8_t *src, int *pos);
110 uint16_t crc16(const uint8_t *pData, size_t len);
111
112 bool read(uint16_t address, uint8_t *buffer, uint count);
113 bool write(uint16_t address, uint8_t *buffer, uint count);
114 };
115
116
117 class WifiMQTT{
118 // As the name implies, this class could do
119 // with splitting into two, but time constraints
120 // are secondary to functionality for now.
121 private:
122 const char *ssid;
123 const char *pass;
124 const char *broker_ip;
125 const int broker_port;
126
127 static WifiMQTT* instance;
128
129 bool wifi_connection = false;
130 bool mqtt_connection = false;
131
132 char last_mqtt_msg[128];
133 int last_mqtt_msg_len = 0;
134 bool msg_handled = false;
135
136 Countdown countdown;
137 IPStack ipstack;
138 MQTT::Client<IPStack, Countdown> client;
139 MQTTPacket_connectData data;
140 public:
141 // Constructor
142 WifiMQTT(const char *ssid0, const char *pass0,
143 const char *broker_ip0, int broker_port0);
144
145 // Member functions
146 bool connect_wifi();
147 bool connect_mqtt();
148 bool is_connected();
149
150 bool mqtt_subscribe(const char* topic);
151 bool mqtt_send_msg(const char* msg);
152 bool new_msg();
153 Config::GDOOR_STATE mqtt_get_cmd();
154
155 void loop(int timeout_ms);
156
157 void messageArrived(MQTT::MessageData &md);
158 static void messageArrivedWrapper(MQTT::MessageData &md);
159 };
160};
Most of the definitions here look okay to me. If I had more time I would’ve split some functionality off from the components (serialisation functions from Eeprom, e.g.). I just jammed it into one when I did testing to speed things up, as I didn’t want to spend time deliberating on what kind of additional namespaces would’ve been needed.
The RotaryEncoder class has a notable issue in it, not because it doesn’t work, but because its a pain to use in practice. It doesn’t use a queue for tracking the inputs from the hardware irq, instead it has a counter. This is kind of okay for this project since I just need to differentiate between the door moving up and down (I don’t care about the switch button in the rotary). But crucially this makes the counter itself live in the RotaryEncoder object, making tracking it a little more cumbersome than needed in the main device object itself. This is very evident when states need to be saved and loaded from the Eeprom! Effectively requiring another counter in the main device anyhow.
Configuration
The config.hpp is the file that contains our general configuration values (Config::),
and pin assignments (Pins::).
A larger project would probably benefit from better separation, specially between component and device specific values, but this was completely workable for a project this small.
1#pragma once
2
3#include <cstdint>
4
5#include "pico/stdlib.h"
6#include "hardware/gpio.h"
7#include "hardware/i2c.h"
8
9
10namespace Config {
11 constexpr bool DEBUG_MODE = true;
12 constexpr uint32_t DEBOUNCE_MS = 75;
13 constexpr uint32_t DEBOUNCE_US = DEBOUNCE_MS * 1000;
14 constexpr uint32_t INPUT_CD_MS = 200;
15 constexpr uint32_t INPUT_CD_US = INPUT_CD_MS * 1000;
16 constexpr uint32_t LED_BLINK_INTERVAL_MS = 750;
17 constexpr uint32_t LED_BLINK_INTERVAL_US = LED_BLINK_INTERVAL_MS * 1000;
18
19 // Stepper Motor
20 constexpr int STEPPER_INCR = 128;
21 constexpr int STEPPER_STUCK_DELTA = 1;
22 constexpr int STEPPER_STUCK_CTR_MAX = 3; // belt wobble makes this a pain
23 constexpr uint8_t STEPPER_ROWS = 8;
24 constexpr uint8_t STEPPER_TABLE[8][4] = {
25 {1, 0, 0, 0},
26 {1, 1, 0, 0},
27 {0, 1, 0, 0},
28 {0, 1, 1, 0},
29 {0, 0, 1, 0},
30 {0, 0, 1, 1},
31 {0, 0, 0, 1},
32 {1, 0, 0, 1}
33 };
34 constexpr uint16_t STEPPER_STEPS = 4096;
35 constexpr uint16_t STEPPER_SLEEP_US = 1250;
36 constexpr int STEPPER_CALIB_DELTA = 3; // shrinks calibration span from limits
37
38 // Eeprom
39 constexpr auto I2C_UNIT = i2c0;
40 constexpr int I2C_BAUD_HZ = 100 * 1000;
41 constexpr uint EEPROM_DEV_ADDR = 0x50;
42 constexpr uint EEPROM_DATA_ADDR = 8 * 64;
43 constexpr uint EEPROM_WRITE_DELAY_MS = 5;
44 constexpr uint EEPROM_PAGES = 512;
45 constexpr uint EEPROM_PAGE_SIZE = 64;
46 constexpr uint EEPROM_MEMBER_SIZE = 8;
47
48 // Wifi & MQTT
49 constexpr const char* WIFI_SSID = "MySSID";
50 constexpr const char* WIFI_PASS = "MySecurePassword";
51
52 constexpr const char* MQTT_BROKER_IP = "192.168.1.123";
53 constexpr int MQTT_BROKER_PORT = 1883;
54
55 // Garage Door
56 constexpr int GDOOR_MAGIC = 0x2A;
57 constexpr uint8_t GDOOR_DATA_LEN = 8+2; // BYTE length, depends on saved fields!
58
59 enum class GDOOR_STATE : uint8_t {
60 UNKNOWN, // door is not calibrated
61 CALIBRATING, // door is calibrating
62 CEIL, // door is at ceiling
63 FLOOR, // door is at floor
64 CLOSING, // door was last seen as closing
65 OPENING, // door was last seen as opening
66 STOPPED, // door was stopped during movement
67 BAD_CMD, // remote controller sent bad command
68 ERROR // door encountered error
69 };
70 enum class GDOOR_MOV : uint8_t {
71 NONE,
72 UP,
73 DOWN,
74 };
75}
76
77namespace Pins {
78 // Stepper Motor
79 constexpr uint STEPPER0 = 2;
80 constexpr uint STEPPER1 = 3;
81 constexpr uint STEPPER2 = 6;
82 constexpr uint STEPPER3 = 13;
83
84 // Buttons
85 constexpr uint BUTTON_A = 7;
86 constexpr uint BUTTON_B = 9;
87
88 // Leds
89 constexpr uint LED0 = 20;
90 constexpr uint LED1 = 21;
91 constexpr uint LED2 = 22;
92
93 // Rotary Encoder
94 constexpr uint ROT_A = 14;
95 constexpr uint ROT_B = 15;
96
97 // Limit Switches
98 constexpr uint LIM_FLOOR = 4;
99 constexpr uint LIM_CEIL = 5;
100
101 // Eeprom
102 constexpr uint EEPROM_SDA = 16;
103 constexpr uint EEPROM_SCL = 17;
104}
Closing Thoughts
During this project/assignment I focused above all on OOP, interfaces, namespaces, and source structure in the build system. I think I found a good balance between separation, readability, and maintainability.
The more complex WifiMQTT element should’ve definitely been a separate file with its own definitions for easier testing and development, and this is something I will definitely consider in the future when I see a component requiring more unique dependencies to others.
Miscellaneous
As an aside… A thing I spent an ungodly amount of time on (somehow) was the local controls. Namely the button_a and button_b in the local controls of the door. I had a hell of a time making them behave exactly how I wanted (non-blocking, triggering in a way that felt good to use, and proper debounce).
It felt like when I fixed one aspect, a new problem cropped up. I mean how damn difficult could a simple switch button really be? It’s literally about as simple as it gets… In hindsight I should’ve designed the button class with a couple different press detection member functions (press, long press, double press, falling edge, …), which would’ve allowed me to just choose the implementation I want to use at different parts of the device code when polling for user input.
Combining these multiple press detection functions with a global input cooldown on the device side would’ve been the perfect solution with easy to maintain interfaces and granular component specific limits.
The more you know…