# Using devcontainers NOTE: Unless otherwise specified all commands below should be run from the main workspace folder of the betaflight repository. NOTE: All examples here are using a Moblite7 1S tiny whoop based on STM32F411CEU6. The betaflight target is CRAZYBEEF4DX. ## Why use devcontainers? Devcontainers provide a consistent, reproducible development environment that works the same across machines. They eliminate “works on my machine” issues by packaging tools, dependencies, and configurations inside a container. This makes onboarding faster, simplifies collaboration, and ensures your development setup stays clean and isolated. ## How is this different from the betaflight/cloudbuild repository? The betaflight/cloudbuild repository provides a containerized CI/CD environment for building and testing Betaflight firmware in the cloud. It is optimized for automated builds and may not include all the tools and configurations needed for local development. This devcontainer includes DFU utilities and OpenOCD debugger for example. While similar to the cloudbuild container it is tailored for interactive development and debugging and avoids disruptions caused by changes in the cloudbuild repo. ## Defining UDEV rules on the host Devcontainers for HW development are a bit special, as they require HW access to the device. It can get tricky to make this universal, because not all Linux distributions use the same group numbers to control permissions. Typically to do HW development on an STM32 board the user would need at least `dialout` and `plugdev` permissions. The `dialout` groups for example can be number 11, 18 or 20 on Void Linux, Fedora and Debian, respectively. The easiest way to deal with this is to define custom `udev` rules. To allow the container access to the HW, the device would need to be configured with the right permissions when plugged in. Here are the steps required: 1. plug in the device and check its IDs and default permissions ```bash # List the USB connected devices $ lsusb Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub Bus 001 Device 002: ID 0483:5740 STMicroelectronics Virtual COM Port <==== this is our FC! Note the `{idVendor}` and `{idProduct}` IDs! Bus 001 Device 003: ID 1fd2:8005 Melfas LGDisplay Incell Touch Bus 001 Device 004: ID 04f2:b681 Chicony Electronics Co., Ltd ThinkPad T490 Webcam Bus 001 Device 005: ID 06cb:00bd Synaptics, Inc. Prometheus MIS Touch Fingerprint Reader Bus 001 Device 006: ID 8087:0aaa Intel Corp. Bluetooth 9460/9560 Jefferson Peak (JfP) Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub # Check the default permissions $ ls -la /dev/ttyACM0 crw-rw---- 1 root dialout 166, 0 Jan 24 09:35 /dev/ttyACM0 <==== device is owned by root with access via the `dialout` group. ``` 2. create a `udev` rule for the device by creating this file: `sudo nano /etc/udev/rules.d/99-betaflight.rules` ```bash SUBSYSTEM=="tty", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="5740", MODE="0666", TAG+="uaccess" SUBSYSTEM=="usb", ATTR{idVendor}=="0483", ATTR{idProduct}=="df11", MODE="0666" ``` The `{idVendor}` and `{idProduct}` IDs must match those of the device. The two `SUBSYSTEM` rules cover both the normal operation and DFU mode of the flight controller. 3. plug the device out and in again. You should see: ```bash $ ls -la /dev/ttyACM0 crw-rw-rw- 1 root dialout 166, 0 Jan 24 09:39 /dev/ttyACM0 <==== note the third `rw` allowing access to all ``` NOTE: this example is with a ready made flight controller on UART, but the same procedure works for STLink debuggers for example. ## Installation of tools Two examples are given here - one using Docker and the other using Podman. For the Docker example we will use Microsoft's VSCode with the Dev Containers extension. For the Podman example we will use fully open source development chain with `devpod` and VSCodium. ### Installing Podman Podman is a rootless, daemonless alternative to Docker that provides a compatible command‑line interface for managing containers. Installation instructions for all supported platforms are available in the [official Podman documentation](https://podman.io/docs/installation). There are no special daemons to be run or group permissions to be added. Validate the installation with: ```bash podman run docker.io/library/hello-world ``` ### Installing Docker (CLI) Docker provides the docker CLI for building and running containers. Follow the [official installation guide](https://docs.docker.com/engine/install/). After installation usually the docker service needs to be started and the user should be added to the `docker` group. Details on how to do this is linux distro dependent - refer to your distribution manual for details on how to achieve this. Below is an example that should work on distributions with `systemd` (Debian and derivatives, RedHat and derivatives, etc.). ```bash sudo systemctl enable --now docker # Enable and start the service sudo usermod -aG docker $USERNAME # you might need to log out and back in after this step docker run docker.io/library/hello-world # Run an example to validate the install ``` ### Installing VS Code and the Dev Containers Extension To use devcontainers more effectively, you can install Visual Studio Code along with the Dev Containers extension. You can install VS Code by following the [official instructions provided](https://code.visualstudio.com/docs/setup/setup-overview) for your operating system. Once VS Code is installed, add the Dev Containers extension using the [official guide](https://code.visualstudio.com/docs/devcontainers/containers). With these tools installed, you can open the betaflight repository in VS Code and use the Dev Containers extension to build and run the devcontainer defined in the `.devcontainer` folder. NOTE: Dev Containers extension in VSCode comes pre-configured for the `docker` binary. You can simply type `podman` in the extension setting to change the container provider. ### Fully open-source solution via devpod and VSCodium VSCode from Microsoft and its Dev Containers Extension are not fully open-source. The [VSCodium project](https://vscodium.com/) builds from the open-source repository of VSCode stripping Microsoft proprietary code. Similarly the [devpod project](https://devpod.sh/) provides a wrapper similar to the Microsoft Dev Containers Extension around the open [development container specification](https://containers.dev/). The two can be used together to achieve the same as Microsoft's proprietary solution. Example of running with `devpod`: ```bash $ podman --version # check podman version podman version 5.7.1 $ devpod version # check devpod version v0.6.15 $ devpod provider list # check that the podman provider is configured NAME | VERSION | DEFAULT | INITIALIZED | DESCRIPTION ---------+---------+---------+-------------+------------------- docker | v0.0.1 | false | true | DevPod on Docker podman | v0.0.1 | true | true | DevPod on Podman $ devpod ide list # check that the VSCodium IDE is configured NAME | DEFAULT ------------------+---------- clion | false codium | true cursor | false dataspell | false fleet | false goland | false intellij | false jupyternotebook | false none | false openvscode | false phpstorm | false positron | false pycharm | false rider | false rstudio | false rubymine | false rustrover | false vscode | false vscode-insiders | false webstorm | false zed | false $ devpod up . # start the devcontainer in the current folder ``` The last command will open VSCodium in the devcontainer context. ## Example usage inside the container Once inside the devcontainer terminal you can check the device access: ```bash devpod@bdaab82778ae:/workspaces/betaflight$ ls -la /dev/ttyACM0 crw-rw-rw- 1 nobody nogroup 166, 0 Jan 24 09:24 /dev/ttyACM0 ``` You can then build and flash the firmware as usual: ```bash devpod@bdaab82778ae:/workspaces/betaflight$ make clean devpod@bdaab82778ae:/workspaces/betaflight$ make CRAZYBEEF4DX Building target config CRAZYBEEF4DX make --no-print-directory fwo CONFIG=CRAZYBEEF4DX EF HASH -> ./obj/main/STM32F411_CRAZYBEEF4DX/.efhash_4e3f673b7beeb9bf243f745d779b10e0 %% startup_stm32f411xe.s %% (speed optimised) ./src/platform/common/stm32/system.c . . . %% (optimised) ./src/main/drivers/usb_io.c Linking STM32F411_CRAZYBEEF4DX Memory region Used Size Region Size %age Used FLASH: 8268 B 16 KB 50.46% FLASH_CONFIG: 0 B 16 KB 0.00% FLASH1: 371710 B 480 KB 75.62% SYSTEM_MEMORY: 0 B 29 KB 0.00% RAM: 81960 B 128 KB 62.53% MEMORY_B1: 0 B 0 B text data bss dec hex filename 374254 5724 75824 455802 6f47a ./obj/main/betaflight_STM32F411_CRAZYBEEF4DX.elf Creating HEX ./obj/betaflight_2025.12.0-beta_STM32F411_CRAZYBEEF4DX.hex Building target config CRAZYBEEF4DX succeeded. devpod@bdaab82778ae:/workspaces/betaflight$ make dfu_flash CONFIG=CRAZYBEEF4DX # potentially this is because the MCU already is in DFU mode, try anyway echo -n 'R' > /dev/ttyACM0 sleep 1 make --no-print-directory ./obj/betaflight_2025.12.0-beta_STM32F411_CRAZYBEEF4DX.dfu Creating DFU ./obj/betaflight_2025.12.0-beta_STM32F411_CRAZYBEEF4DX.dfu dfu-util -a 0 -D ./obj/betaflight_2025.12.0-beta_STM32F411_CRAZYBEEF4DX.dfu -s :leave dfu-util 0.11 Copyright 2005-2009 Weston Schmidt, Harald Welte and OpenMoko Inc. Copyright 2010-2021 Tormod Volden and Stefan Schmidt This program is Free Software and has ABSOLUTELY NO WARRANTY Please report bugs to http://sourceforge.net/p/dfu-util/tickets/ Match vendor ID from file: 0483 Match product ID from file: df11 Multiple alternate interfaces for DfuSe file Opening DFU capable USB device... Device ID 0483:df11 Device DFU version 011a Claiming USB DFU Interface... Setting Alternate Interface #0 ... Determining device status... DFU state(10) = dfuERROR, status(10) = Device's firmware is corrupt. It cannot return to run-time (non-DFU) operations Clearing status Determining device status... DFU state(2) = dfuIDLE, status(0) = No error condition is present DFU mode device DFU version 011a Device returned transfer size 2048 DfuSe interface name: "Internal Flash " File contains 1 DFU images Parsing DFU image 1 Target name: ST... Image for alternate setting 0, (2 elements, total size = 379994) Setting Alternate Interface #0 ... Parsing element 1, address = 0x08000000, size = 8268 Erase [=========================] 100% 8268 bytes Erase done. Download [=========================] 100% 8268 bytes Download done. Parsing element 2, address = 0x08008000, size = 371710 Erase [=========================] 100% 371710 bytes Erase done. Download [=========================] 100% 371710 bytes Download done. Done parsing DfuSe file Submitting leave request... Transitioning to dfuMANIFEST state ``` ## Gazebo SITL Simulation A separate container is provided for running Betaflight SITL with [Gazebo Harmonic](https://gazebosim.org/) simulation. This allows testing firmware changes without physical hardware. ### Architecture ```text ┌─────────────────────────────────────────────────────┐ │ Gazebo Container │ │ │ │ ┌──────────────┐ UDP 9002/9003 ┌──────────────┐ │ │ │ Gazebo │◄──────────────►│ Betaflight │ │ │ │ Harmonic │ motor/sensor │ SITL (.elf) │ │ │ │ + Bridge │ │ │ │ │ └──────────────┘ └──────┬───────┘ │ │ │ TCP 5761 │ └─────────────────────────────────────────┼──────────┘ │ ┌───────────▼──────────┐ │ Betaflight App │ │ ws://localhost:6761 │ └──────────────────────┘ ``` ### Building the Gazebo container ```bash docker build -t bf-dev-gazebo -f .devcontainer/containerfile.gazebo .devcontainer/ ``` ### Running a simulation The simulation requires three processes running concurrently inside the container. Use three separate host terminals — the first starts the container, the others attach to it. > **Note:** Gazebo's GUI requires a display. On Linux the `docker run` command below forwards your host X11 socket into the container. On macOS, install [XQuartz](https://www.xquartz.org) and run `xhost +localhost` first. **Host terminal 1 — start the container and build SITL:** ```bash docker run -it --rm \ --name bf-gazebo \ --network=host \ -e DISPLAY=$DISPLAY \ -v /tmp/.X11-unix:/tmp/.X11-unix \ -v "$(pwd)":/workspace \ bf-dev-gazebo # Inside the container — build SITL (only needed once per workspace) make TARGET=SITL # Then start SITL ./obj/main/betaflight_SITL.elf ``` **Host terminal 2 — attach and start Gazebo:** ```bash docker exec -it bf-gazebo bash # Inside the container gz sim -r ~/aeroloop_gazebo/worlds/betaloop_iris_betaflight_demo_harmonic.sdf ``` **Host terminal 3 — attach and start the WebSocket proxy:** ```bash docker exec -it bf-gazebo bash # Inside the container websockify 127.0.0.1:6761 127.0.0.1:5761 ``` Then connect the Betaflight App to `ws://127.0.0.1:6761`. ### Ports | Port | Protocol | Purpose | |------|----------|---------| | 9002 | UDP | SITL → Gazebo (motor commands) | | 9003 | UDP | Gazebo → SITL (sensor data) | | 9004 | UDP | External → SITL (RC input) | | 5761 | TCP | SITL UART1 (MSP serial proxy) | | 6761 | TCP | WebSocket proxy for Betaflight App |