It’s been a while since my last post. But fear not – the blog is not dead! No sir. In fact, I’ve gotten myself into a new project which urges to be written about. The project is called “Jerry Annihilator” and it’s a robot that eventually will bring me a fame, a fortune, and a world domination. Here he is, my ticket to the bright future:
OK, he doesn’t look intimidating now, but first – it’s not the latest revision. And the second – the guy’s just a few months old, give him a break.
So anyway, despite the age, the project is backed by a quite a bit of code, which, though being maintained by GitLab server (basement edition), still has zero unit tests and equal amount of continuous integration. Of cause, I could justify it by me being an ex web-developer, so any CI/CD and unit testing activities in Arduino IDE / ESP32 C++ project environment would be disturbingly unnatural. However, being a consultant now, especially one who also specializes in CI/CD, makes that “ex web-developer” excuse invalid.
So today I try to make the things right and for starters fix the CI part – if code hits the server, it should be built. That should be a good start.
Main ingredient
The thing that prevented me from adding CI in the past was Arduino IDE. Yup, that window hungry and toolchain concealing tool, which I had no idea how to cross breed with windowless containerized build server. However, Arduino IDE’s got its younger brother now – arduino-cli
, which sounds like a silver bullet for my problem. If it does what it says, I’ll simply need to draft a good old .gitlab-ci.yml
, point it to GitLab CI’s docker executor, install and configure arduino-cli
, download the same libraries that I used with Arduino IDE and voila!
I won’t get into details about how GitLab CI itself works. I also won’t describe its runners and especially the ones that use docker. That’s just too much info, which already was written about.
Instead, will start right at the point where GitLab CI already exists and it has properly configured docker runner tagged with the word docker
. So the obvious first step is to create .gitlab-ci.yml
file.
.gitlab-ci.yml
Here’s what our .gitlab-ci.yml
should do:
- Request a build in docker container
- When in there, install
arduino-cli
and all imaginable dependencies - After that, build project
Why in docker? Well, I don’t want to pollute the environment. Go green and stuff. Plus, building a project and configuring all of its dependencies would be the ultimate proof of us knowing both how to build, and how to configure the environment for that.
1. Request a build in docker executor
.gitlab-ci.yml
can target specific GitLab CI runner by specifying its tag. As I mentioned before, our docker runner is tagged by docker
, so the same tag should be mentioned CI’s build step.
Another thing is that I don’t remember what docker image that runner uses by default. In fact, that’s a good thing, as being explicit about ones expectations is a virtue in many contexts. The project’s ‘home’ OS is Ubuntu, so let’s request ubuntu:18.04
image for a build as well.
Having said that, here’s what .gitlab-ci.yml
starts to look like:
1 2 3 4 5 6 7 8 9 10 11 |
image: ubuntu:18.04 stages: - build compile: stage: build tags: - docker script: - echo "Something like compilation should go here" |
2. Installing arduino-cli and its dependencies
2.1 Setting up arduino-cli
arduino-cli
is just a downloadable archive, so a few lines of bash script can be called ‘an installation’. However, after these few bash lines many other will follow, so let’s create a new shell file and put everything in there. I’ll call it setup-build-env.sh
:
1 2 3 4 5 6 7 8 9 10 11 |
#!/bin/bash apt-get update cd ~ # Install arduino-cli apt-get install curl -y curl -L -o arduino-cli.tar.bz2 https://downloads.arduino.cc/arduino-cli/arduino-cli-latest-linux64.tar.bz2 tar xjf arduino-cli.tar.bz2 rm arduino-cli.tar.bz2 mv `ls -1` /usr/bin/arduino-cli |
The file has zero sudo
‘s, because 1) most likely it’s already going to run as root
in Docker container and 2) even if it’s not, it’s easier to add one sudo
before the file name than one before every other line in the file itself.
2.2 Setting up ESP32 support
Now the fun part begins. I remember that in order to add ESP32 support to Arduino IDE I had to open some board manager window and copy-paste mysterious URL. Apparently, the same stuff needs to be done here, but this time through cli.
1 2 3 4 |
# Install esp32 core printf "board_manager:\n additional_urls:\n - https://dl.espressif.com/dl/package_esp32_index.json\n" > .arduino-cli.yaml arduino-cli core update-index --config-file .arduino-cli.yaml arduino-cli core install esp32:esp32 --config-file .arduino-cli.yaml |
That’s very easy. First, we create .arduino-cli.yaml
file. The name has no special meaning, it’s just the first one that came into my mind. I believe arduino-cli
does have some default file name, but I don’t trust defaults in a tool that’s still released in ‘preview’ edition.
The last line installs magical esp32:esp32
, which I learned about by executing arduino-cli core search esp32 --config-file .arduino-cli.yaml
.
Having ESP32 core installed, we can, for instance, run another command – arduino-cli board listall
– and find out the identifier of the board for which we’ll compile the project. I’m using a ESP32 Dev Board equivalent, whose fully qualified board name appears to be esp32:esp32:esp32
.
1 2 3 4 5 6 7 |
arduino-cli board listall # ... # Dongsen Tech Pocket 32 esp32:esp32:pocket_32 # ESP32 Dev Module esp32:esp32:esp32 # ESP32 FM DevKit esp32:esp32:fm-devkit # ESP32 Pico Kit esp32:esp32:pico32 # ... |
2.3 Installing standard packages
It took me some time, but finally I came up with the list of Arduino libraries that I installed over the past couple of months. Now we’ll need that list to reinstall those libraries for arduino-cli
.
1 2 3 4 5 6 |
# Install 'native' packages arduino-cli lib install "Adafruit BME280 Library" arduino-cli lib install "Adafruit Unified Sensor" arduino-cli lib install "HCSR04 ultrasonic sensor" arduino-cli lib install "ArduinoJson" arduino-cli lib install "MPU9250_asukiaaa" |
2.4 Installing non-standard packages
Along with standard Arduino packages I also used a number of Github projects which I cloned into Arduino libraries
folder. If we find where arduino-cli
stores its stuff, we could git clone
them again.
Fortunately, arduino-cli config dump
can tell us exactly where:
1 2 3 4 5 |
arduino-cli config dump # proxy_type: auto # sketchbook_path: /root/Arduino # arduino_data: /root/.arduino15 # board_manager: {} |
So:
1 2 3 4 5 6 7 8 |
# Install 'third-party' packages: find proper location and 'git clone' apt-get install git -y cd `arduino-cli config dump | grep sketchbook | sed 's/.*\ //'`/libraries # e.g. /root/Arduino/libraries git clone https://github.com/me-no-dev/AsyncTCP.git git clone https://github.com/me-no-dev/ESPAsyncWebServer.git git clone https://github.com/ThingPulse/esp8266-oled-ssd1306.git git clone https://github.com/RoboticsBrno/ESP32-Arduino-Servo-Library.git cd - |
2.5 Setting up python and pyserial
If I’m not mistaken, these two are required by ESP32 IDF – the native ESP32 toolchain that Arduino runs under the hood. Whether or not that is true, without python
and pyserial
arduino-cli
refuses to compile ESP32 boards, so we’ll need them anyway:
1 2 3 4 5 |
# Install python, pip and pyserial apt-get install python -y curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py python get-pip.py pip install pyserial |
2.6 …and putting it all together
Putting all these pieces into setup-build-env.sh
file, here’s what we get in the end:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
#!/bin/bash apt-get update cd ~ # Install arduino-cli apt-get install curl -y curl -L -o arduino-cli.tar.bz2 https://downloads.arduino.cc/arduino-cli/arduino-cli-latest-linux64.tar.bz2 tar xjf arduino-cli.tar.bz2 rm arduino-cli.tar.bz2 mv `ls -1` /usr/bin/arduino-cli # Install python, pip and pyserial apt-get install python -y curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py python get-pip.py pip install pyserial # Install esp32 core printf "board_manager:\n additional_urls:\n - https://dl.espressif.com/dl/package_esp32_index.json\n" > .arduino-cli.yaml arduino-cli core update-index --config-file .arduino-cli.yaml arduino-cli core install esp32:esp32 --config-file .arduino-cli.yaml # Install 'native' packages arduino-cli lib install "Adafruit BME280 Library" arduino-cli lib install "Adafruit Unified Sensor" arduino-cli lib install "HCSR04 ultrasonic sensor" arduino-cli lib install "ArduinoJson" arduino-cli lib install "MPU9250_asukiaaa" cd - # Install 'third-party' packages: find proper location and 'git clone' apt-get install git -y cd `arduino-cli config dump | grep sketchbook | sed 's/.*\ //'`/libraries git clone https://github.com/me-no-dev/AsyncTCP.git git clone https://github.com/me-no-dev/ESPAsyncWebServer.git git clone https://github.com/ThingPulse/esp8266-oled-ssd1306.git git clone https://github.com/RoboticsBrno/ESP32-Arduino-Servo-Library.git cd - |
The remaining part is to add it to .gitlab-ci.yml
and call just before the build.
1 2 3 4 5 6 7 8 |
image: ubuntu:18.04 before_script: - ./setup-build-env.sh stages: - build # ... |
Ideally, instead of installing the dependencies before every build we should’ve baked them into underlying Docker image and focus only at the build part. However, for that we’d also need the docker registry to store that image, configure gitlab runner to know where the registry is, come up with the image lifecycle, etc. Given the frequency of my commits, the time overhead caused by fully configuring the environment is bearable.
3. Finally, the build step
So the only missing step is the compilation itself. I already know fully qualified board name, I know how to spell the word compile
, so putting it all together and pointing it to a project file called robot.ino
, here’s what build line ends up being:
1 |
arduino-cli compile --fqbn esp32:esp32:esp32 -o ignored.bin robot.ino |
PROFIT!
This is the final .gitlab-ci.yml
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
image: ubuntu:18.04 before_script: - ./setup-build-env.sh stages: - build compile: stage: build tags: - docker script: - arduino-cli compile --fqbn esp32:esp32:esp32 -o ignored.bin robot.ino |
And this is how happy GitLab CI server is when it sees it!
It’s alive!
Conclusion
You see, setting up CI for ESP32 and Arduino project wasn’t that hard after all. My result is not entirely an example of a perfect design, but it works, it does its job, and it’s definitely better than not having CI at all. After all, now I at least know what dependencies my project has and how to reconfigure the build environment on other laptops. Without any doubt, that’s a total win.
I guess ‘platformio’ is much simpler to use. But thx I’ve learnd alot about “arduino-cli”.
json
image: python:2.7
stages:
- build
before_script:
- "pip install -U platformio"
job:
stage: build
script: "platformio run"
Thanks for your script explanations!
Some notes:
Once you call the script “create-build-env.sh”, the other time its “setup-build-env.sh”
You are absolutely correct – it all should be
setup-build-env.sh
.Hey, very nice writeup, gonna help me when i try similar stuff.
One thing, not sure if it’s only nitpicking – and i do hope it’s not – currently your CI generates the binary/artifact, but does not really integrate it anywhere.
I would have loved to hear your thoughts on that. I can imagine something like OTA update of an ESP32 and then pulling a file from a web server on it that holds a build tag or similar to verify it’s been updated and the new image is working.
You’re much farther in how you understand gitlab CI, the only time I used it intensively was in 2018 and back then time pressure was so high I could not really learn, I just kept mental footnotes about approaches that might exist.
I did name my test vms after the commit ID they’d be spin up on, and then wrote logs to a remote syslog server where I could look for a syslog message that basically said “commit ID: is happy”.
But I couldn’t ever even go as far as pulling that result back into gitlab ci.
tl’dr
You’re building your image now and super close to having a end to end CI, I wish you would explain if that’s something you want to have or rather pass on.
I never intended to implement the CD part of the process. Even CI part is incomplete. All I needed was to track the dependencies in the repo and to make sure that it’s all buildable. After all, the project started by manually clicking through Arduino IDE’s “Add library” dialogues, which is not the True Way.
A logical continuation for CI part would be to add an actual testing stage. To physically connect a board or a whole robot to the build server, flash it with the artifact, and run some assertions – sensors are responding, the wheels are turning, etc. Since the board/robot would be statically connected to the server, it’s easy to both flash it and to choose the right time for that.
For the CD part, OTA update would work, but the trick is that it would be a very asynchronous operation. You can’t just flash the firmware at will – the robot must not be doing something, it should be sufficiently charged, etc. The best that one could do is to send a message to the robot with a new version (a number, a flag, or actual binary), and then let the robot choose when it’s ready to install it. I vaguely remember people using MQTT for OTA updates, but never had to go deep in it.
I hope that helps, and thanks for the great question.
I forgot to click on wanting to receive notifications so here’s that.