Retrospective: GDOOR

2025-10-09 @ 11 minute(s)

notes

βš™οΈπŸ’»πŸ“Ÿ


Device diagram listing the used components and the layout.
Device diagram listing the used components and the layout.

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

GDOOR and Component interfaces

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…