To the Home Page

Automatic door controller

A custom device used to control motor drivers according to the schedule, with many configurable parameters, using the Raspberry Pi Pico development board

Published on January 6, 2024 · Reading time: 4 minutes

Code on GitHub

Modular design

Assemble the device without extra knowledge, for less than €50

Based on off-the-shelf electronic components, and simple carrier and button boards designed by myself

Modular design

User-friendly menus

Set parameters, invoke actions and check the event logs conveniently

Simple user interface based on easy to navigate menus, using the character LCD and just three buttons

User-friendly menus

Day and night schedules

Adjust the time and duration of the opening and closing actions independently

The door can also be opened or closed manually at any time. Custom code is run for either action

Day and night schedules

Partial operation mode

Split the opening or closing actions into evenly spaced, byte-sized operations

According to the starting and ending times and the defined number of operations per day

Partial operation mode

Technical details

It was supposed to be a quick project when I started in early 2023, but it took me an entire year to finish it. There are many things that can go wrong when it comes to electronic circuit design if you are not experienced. I have run through multiple prototypes and bought a CNC machine to make things less annoying, and documented my experience in a separate article. Let’s focus on the Adafruit’s CircuitPython runtime itself.

Asynchronous operation

There are two main processes running, the scheduler and the menu, that need to be run in parallel. CircuitPython does not support threads, interrupts or concurrency by default, but a first-party asyncio substitute is available.

However, some actions are performed synchronously, for example the opening and closing itself. There is no guarantee that await asyncio.sleep(1) will take exactly 1 second, while the blocking version, time.sleep(1), will provide good enough results. Asynchronous tasks are performed on some menu screens as well.

User menu

Every menu screen inherits the Menu class, which represents the finite list of editable variables in “navigation” or “edit” cursor mode. Implementations have their own state and can change the behavior of every button or the displayed text. For example, the IdleMenu shows the current time and device status but has no visible cursor.

The display I have picked for this project does not have built-in backlight control and can only show basic English or Japanese text and up to 8 custom symbols. When user presses the button, the character set is dynamically updated. The display’s backlight can be controlled manually via simple transistor circuit and a PWM signal.

CircuitPython can handle single buttons and key matrices via the built-in keypad module. It does not report for how long the buttons have been pressed, though. I have added this feature in order to reduce a number of keys.

Persistence

The AT24LC32 EEPROM chip is commonly found on cheap RTC + EEPROM modules and provides 4kB of non-volatile memory. There is no file system, so I had to roll out my own solution for storing settings and log entries.

Each block of data is 8 bytes long and includes the checksum (basic XOR). If the read data is invalid, the settings are reset to the safe defaults. Up to 127 log entries are stored and rotated, with EOF marker updated after every write.

Reliability

CircuitPython has good hardware support and a wide array of additional libraries, but in reality, it is meant to be used for learning and prototyping. I have decided to take the risk and use it for my always-on device.

RP2040 chip found in Raspberry Pi Pico has an internal watchdog timer that is used to reboot the board if the program does not “feed” it for a specified amount of time. Since the CircuitPython 8.1 (mid-2023), it is also possible to create a safemode.py file that defines the custom behavior after a crash when it is caused by something other than my code.

One of the prototype boards had PWM issues caused by pin collisions. According to the RP2040 documentation, section 4.5.2. Programmer’s Model, there are eight 2-channel PWM slices. You can’t reliably use pins 0 and 16 at the same time. The same goes for pins 1 and 17, 8 and 18, and so on. I couldn’t fix it in software, so I had to change the PCB layout.

Memory optimizations

The entire program is more than 1000 lines long, and it’s difficult to keep everything in a single file. On the other hand, hardware modules depend on each other more than ever, and code splitting would result in an increased memory usage and cyclic import issues. I have created a few unit tests for partial actions, so I had to have at least two modules to make tests run in the CPython interpreter. The entire menu has been moved to a separate file as well.

Furthermore, on-device bytecode generation greatly increases the startup time. Thankfully, there is a mpy-cross utility, used by Adafruit developers themselves, that can do this ahead of time to mitigate this.

Dependency management

The recommended way of installing additional libraries is by using the circup tool. It can read dependencies from the requirements.txt file, but unfortunately, it ignores version numbers, and uses the latest “compiled” version of each package.

Alternatively, it is possible to use source repositories of each library and select one of the tagged commits, which I did during the upgrade to CircuitPython 9.0. Dependencies are installed as Git submodules that can be downloaded automatically when the repository of this project is cloned.

Bill of materials

All components are THT (through-hole), not SMD (surface mounted).

Check out other blog posts: