Yocto & CI/CD

BitBake to Board — Flash Yocto Images Without Touching Hardware

Your bitbake build finishes. Now what? Stop hunting for SD cards and USB readers. Flash your image to a real board with a single API call — from anywhere.

Feb 17, 2026 · 8 min read

The last mile of every Yocto build

You have spent hours tuning your local.conf, writing custom recipes, and waiting for BitBake to churn through a full build. The output is a pristine .wic image sitting in tmp/deploy/images/. And then the workflow breaks down.

The traditional next step looks something like this:

  • Eject the SD card from the board
  • Plug it into a USB reader on your laptop
  • Run bmaptool copy or dd
  • Swap the card back into the board
  • Power-cycle and hope the serial cable is still connected

This ceremony wastes time. Worse, it does not scale. When you have multiple boards, remote teammates, or a CI pipeline that needs to test on real hardware, the SD-card shuffle becomes a bottleneck.

This post shows how to eliminate that bottleneck entirely. After reading it, your workflow will be: build → upload → flash → boot → verify, all without leaving your terminal.

What you need

Before we start, here is the setup:

  • A Yocto build environment — any release from Dunfell onward works. The examples use Scarthgap, but the concepts apply to any version.
  • A board registered on fernsteuerung.io — with a USB-SD-Mux attached. This is the hardware that lets the platform switch the SD card between the host machine and the target board without physical intervention.
  • An API token — generated in the fernsteuerung.io dashboard under your device settings.

If you do not have a board set up yet, the minimal setup guide walks you through the hardware side.

Step 1 — Build your Yocto image

Nothing changes here. Build your image the way you always do:

# Initialize the build environment
source oe-init-build-env

# Build a minimal image (or your custom distro)
bitbake core-image-minimal

When BitBake finishes, your image lands in the deploy directory:

ls tmp/deploy/images/<MACHINE>/
# core-image-minimal-<MACHINE>.rootfs.wic.gz
# core-image-minimal-<MACHINE>.rootfs.wic.bmap

The .wic.gz is your compressed disk image. The .bmap file is a block map that makes flashing faster — fernsteuerung.io uses bmaptool under the hood, so keep both files.

Step 2 — Upload the image

fernsteuerung.io provides an S3-backed image hub. You can upload images through the dashboard or via the API. For a scriptable workflow, the API is the way to go:

# Upload the .wic.gz image to the image hub
curl -X POST \
  "https://fernsteuerung.io/api/images" \
  -H "Authorization: Bearer $TOKEN" \
  -F "image=@tmp/deploy/images/<MACHINE>/core-image-minimal-<MACHINE>.rootfs.wic.gz" \
  -F "bmap=@tmp/deploy/images/<MACHINE>/core-image-minimal-<MACHINE>.rootfs.wic.bmap"

The response gives you an image ID you can reference later. If you rebuild frequently, consider adding a version tag or commit SHA to keep track of which build produced which image.

Step 3 — Lease a device and flash

Before flashing, you need to lease the target board. A lease is an exclusive lock — it guarantees nobody else will power-cycle or flash your board while you are using it.

# Acquire a lease on the device
curl -X POST \
  "https://fernsteuerung.io/api/devices/<DEVICE_UUID>/lease" \
  -H "Authorization: Bearer $TOKEN"

With the lease active, trigger the flash:

# Flash the uploaded image to the board's SD card
curl -X POST \
  "https://fernsteuerung.io/api/devices/<DEVICE_UUID>/flash" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"image_id": "<IMAGE_ID>"}'

Behind the scenes, fernsteuerung.io tells the node server to:

  1. Switch the USB-SD-Mux so the host owns the SD card
  2. Write the image using bmaptool for fast, verified block copies
  3. Switch the SD-Mux back so the board owns the card

The flash endpoint returns an SSE stream URL. You can follow the progress in real time:

# Watch flash progress via Server-Sent Events
curl -N \
  "https://fernsteuerung.io/api/devices/<DEVICE_UUID>/flash/progress" \
  -H "Authorization: Bearer $TOKEN"

# Output:
# data: {"percent": 12, "stage": "writing"}
# data: {"percent": 58, "stage": "writing"}
# data: {"percent": 100, "stage": "done"}

Step 4 — Power-cycle and watch the boot

Once the flash completes, power-cycle the board to boot from the freshly written image:

# Power-cycle the board
curl -X POST \
  "https://fernsteuerung.io/api/devices/<DEVICE_UUID>/power" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"action": "cycle"}'

Now open the serial console. You can use the web dashboard, or connect directly via WebSocket from your terminal using websocat:

# Stream serial console output
websocat "wss://fernsteuerung.io/node/<DEVICE_UUID>/console"

# U-Boot 2024.01 (Jan 15 2024 - 12:34:56 +0000)
# ...
# Starting kernel ...
# [    0.000000] Booting Linux on physical CPU 0x0
# [    1.234567] Run /sbin/init as init process
# Poky (Yocto Project Reference Distro) 5.0 <MACHINE> ttyS0
# <MACHINE> login:

You are watching your Yocto image boot on real hardware — from a coffee shop, a home office, or a CI runner in the cloud.

Putting it all together

Here is a single shell script that chains the full workflow. Drop it into your Yocto build directory or call it from a CI job:

#!/usr/bin/env bash
# flash-and-boot.sh — Build, upload, flash, and boot a Yocto image
set -euo pipefail

API="https://fernsteuerung.io/api"
DEVICE="<DEVICE_UUID>"
TOKEN="<YOUR_API_TOKEN>"
MACHINE="<MACHINE>"
IMAGE="core-image-minimal"
DEPLOY_DIR="tmp/deploy/images/${MACHINE}"

# 1. Build
source oe-init-build-env
bitbake ${IMAGE}

# 2. Upload
IMAGE_ID=$(curl -s -X POST "${API}/images" \
  -H "Authorization: Bearer ${TOKEN}" \
  -F "image=@${DEPLOY_DIR}/${IMAGE}-${MACHINE}.rootfs.wic.gz" \
  -F "bmap=@${DEPLOY_DIR}/${IMAGE}-${MACHINE}.rootfs.wic.bmap" \
  | jq -r '.id')

echo "Uploaded image: ${IMAGE_ID}"

# 3. Lease
curl -s -X POST "${API}/devices/${DEVICE}/lease" \
  -H "Authorization: Bearer ${TOKEN}"

# 4. Flash
curl -s -X POST "${API}/devices/${DEVICE}/flash" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d "{\"image_id\": \"${IMAGE_ID}\"}"

echo "Flashing... waiting for completion"
sleep 30  # adjust based on image size

# 5. Power-cycle and boot
curl -s -X POST "${API}/devices/${DEVICE}/power" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"action": "cycle"}'

echo "Board is booting. Open the console:"
echo "  https://fernsteuerung.io/devices/${DEVICE}/control"

Bonus: Drop it into a Jenkins pipeline

If you already use Jenkins for your Yocto builds, extending the pipeline to flash and test on hardware is straightforward. The shell script above becomes a pipeline stage:

pipeline {
    agent { label 'yocto-builder' }

    environment {
        FERN_TOKEN  = credentials('fernsteuerung-token')
        DEVICE_UUID = 'your-device-uuid'
        MACHINE     = 'raspberrypi4-64'
    }

    stages {
        stage('Build') {
            steps {
                sh 'source oe-init-build-env && bitbake core-image-minimal'
            }
        }

        stage('Flash to Hardware') {
            steps {
                sh './flash-and-boot.sh'
            }
        }

        stage('Hardware Tests') {
            steps {
                sh '''
                    # Connect to serial console and run smoke tests
                    # e.g., check kernel version, verify services
                '''
            }
        }
    }

    post {
        always {
            // Release the lease so other jobs can use the board
            sh """
                curl -s -X DELETE \\
                  "https://fernsteuerung.io/api/devices/${DEVICE_UUID}/lease" \\
                  -H "Authorization: Bearer ${FERN_TOKEN}"
            """
        }
    }
}

The post { always } block ensures the lease is released even if the build fails. This is important — leaked leases block other engineers and jobs from accessing the board.

Why this matters for Yocto teams

Yocto builds are slow. A full build can take hours. When the output finally lands, the feedback loop to real hardware should be as short as possible. Every manual step between bitbake completing and seeing a login prompt on the serial console is wasted time and a chance for human error.

With remote flashing, you get:

  • Faster iteration — flash and boot in the time it takes to grab a coffee, not walk to a lab
  • Reproducible deployments — the same API call, the same bmaptool invocation, every time. No “wrong card slot” accidents
  • Hardware-in-the-loop CI — Jenkins, GitLab CI, or GitHub Actions can flash real boards as a pipeline stage, catching BSP regressions before they reach QA
  • Team access — any engineer with a browser can flash and test, regardless of where the hardware physically sits

Recipes for common Yocto scenarios

Testing a device-tree change

You modified a .dts file and rebuilt. Flash the new image and check the serial console for probe errors or missing nodes. No need to swap cables or find the right board — just pick the device in the dashboard and flash.

Bisecting a boot regression

You have five candidate images from different commits. Upload them all, then flash each one in sequence. The serial console gives you instant visibility into where the boot breaks. What used to take an afternoon of SD-card swapping becomes a scripted loop.

Multi-board validation

Your BSP layer supports three machine configs: imx8mm-evk, raspberrypi4-64, and beaglebone-yocto. Build all three, upload the images, and flash them to their respective boards in parallel. One pipeline, three boards, zero hallway walks.

Nightly BSP health checks

Schedule a cron job or a nightly Jenkins build that pulls the latest meta- layers, builds, flashes, and checks that the board reaches a login prompt. If it fails, the team gets a notification before anyone starts their day.

From build to boot — no hardware required

The gap between bitbake finishing and seeing your image run on hardware does not have to involve USB readers, SD cards, or walking to a lab. With a USB-SD-Mux, a power switch, and the fernsteuerung.io API, the entire flash-and-boot cycle becomes a scriptable, repeatable operation.

Whether you are a solo developer iterating on a BSP or a team running hardware-in-the-loop CI, the workflow is the same: build → upload → flash → boot → verify.

Ready to connect your first board? Create a free account and follow the minimal setup guide to get started.