# Yocto FCB1010 Looper System - Implementation Guide ## Project Overview Build a minimal, real-time Linux system for an audio looper pedal using Yocto Project. The system boots from initramfs only for maximum speed, includes RT kernel patches, and supports over-the-air updates via SWUpdate with A/B partitioning. **Key Requirements:** - x64 hardware (NUC-class) - Static IP: 192.168.0.50 - SSH access with public key authentication - USB live installer that can dd copy to disk - A/B partition scheme for reliable updates - RT kernel with PREEMPT_RT - JACK audio stack - SWUpdate for OTA updates ## Architecture Decisions **Boot Strategy:** EFI boot → initramfs-only (no rootfs mounting) **Network:** WiFi with static IP, credentials baked into image **Updates:** SWUpdate web interface + SSH API access **Installation:** USB boot → SSH → dd copy entire USB to target disk **Partition Layout:** EFI boot partition with initramfs-A.cpio.gz and initramfs-B.cpio.gz ## Yocto Implementation Approach **Custom Layer Structure:** Create meta-looper with recipes for kernel config (RT patches), network setup (static IP + WiFi), SSH keys, audio stack (JACK), and two image types: main system (initramfs) and USB installer. **Build Configuration:** - MACHINE = "genericx86-64" - Kernel: linux-yocto with RT features - Minimal install, not Pokey distribution ## Key Implementation Points **Kernel:** Enable PREEMPT_RT, high-res timers, and audio subsystem. Use kernel fragment files (.cfg) to configure RT options. **Networking:** Create recipes for wpa_supplicant.conf and network interfaces with static IP. Include WiFi credentials at build time. **SSH:** Recipe to install public key to root's authorized_keys during image build. **SWUpdate:** Configure for A/B updates, web interface on port 8080, and integration with systemd-boot for partition switching. **USB Installer:** Second image that boots live system with installation script. Script partitions target disk, creates EFI boot partition, and dd copies the installer image to create installed system. ## Running The build and testing platform is built in Docker. A Compose file is used to script the separate targets: - docker compose run disk-image - docker compose run qemu-server - docker compose run push-update - docker compose run ssh-client The compose file mounts the proper directories for cache, output, ... It also forwards the proper ports for qemu. The run script wraps docker compose run and adds some environment variables. ## Testing Strategy for AI Agent **QEMU Testing:** Use runqemu with network forwarding to test SSH, networking, and SWUpdate functionality. QEMU supports graphics for GUI testing and USB forwarding for hardware validation. **Build Validation:** Verify initramfs size (<200MB), boot time (<10 seconds), and RT kernel configuration. Test static IP assignment and SSH key authentication. **Update Testing:** Create test SWUpdate packages and verify A/B switching works correctly. Test both web interface and SSH-based update deployment. **Audio Validation:** Confirm JACK installation, RT scheduling capabilities with cyclictest, and audio device detection. **Installation Testing:** Boot USB installer in QEMU, SSH in, run installation script on virtual disk, then boot installed system to verify complete workflow. The implementation should focus on creating minimal, working recipes that can be tested incrementally in QEMU before hardware deployment. Each component (kernel, network, SSH, audio, updates) should be testable independently. ## Core Architecture and Directory Structure The system integrates **Yocto Project 5.0 Scarthgap LTS** with Docker containerization, organizing all components within a Rust workspace directory structure. This approach minimizes host dependencies while providing a robust embedded Linux development environment with reliable over-the-air updates. ### Project Organization Within Rust Workspace ``` image/ ├── Dockerfile ├── docker-compose.yml ├── run │ ├── yocto/ │ ├── poky/ │ ├── meta-openembedded/ │ ├── meta-rust-bin/ # Pre-built Rust toolchain │ └── meta-swupdate/ # SWUpdate integration ├── meta-layers/ │ ├── meta-custom/ # Custom project layer │ │ ├── conf/ │ │ │ ├── distro/ │ │ │ ├── machine/ │ │ │ └── layer.conf │ │ ├── recipes-apps/ │ │ │ └── egui-kiosk/ │ │ ├── recipes-core/ │ │ │ └── images/ │ │ └── recipes-support/ │ │ └── swupdate/ │ └── meta-custom-bsp/ # BSP-specific configurations ├── scripts/ │ ├── setup-environment.sh │ ├── build-images.sh │ ├── deploy-qemu.sh │ └── create-update.sh └── conf/ ├── local.conf └── bblayers.conf ``` The **Rust workspace configuration** unifies all application components: ```toml # rust-workspace/Cargo.toml [workspace] resolver = "2" members = [ "apps/*", "libs/*" ] [workspace.package] version = "1.0.0" edition = "2021" rust-version = "1.70" [workspace.dependencies] egui = "0.24" eframe = { version = "0.24", features = ["wgpu"] } serde = { version = "1.0", features = ["derive"] } tokio = { version = "1.0", features = ["rt-multi-thread"] } ``` ## Yocto Layer Creation and Rust Integration The custom layer provides comprehensive Rust application support with modern toolchain integration using **meta-rust-bin** for faster builds and easier maintenance. ### Custom Layer Configuration ```bash # meta-layers/meta-custom/conf/layer.conf BBPATH .= ":${LAYERDIR}" BBFILES += "${LAYERDIR}/recipes-*/*/*.bb ${LAYERDIR}/recipes-*/*/*.bbappend" BBFILE_COLLECTIONS += "custom-layer" BBFILE_PATTERN_custom-layer = "^${LAYERDIR}/" BBFILE_PRIORITY_custom-layer = "10" LAYERVERSION_custom-layer = "1" LAYERSERIES_COMPAT_custom-layer = "scarthgap" LAYERDEPENDS_custom-layer = "core openembedded-layer meta-rust-bin" ``` ### Rust egui Application Recipe The recipe integrates seamlessly with the Rust workspace, handling cross-compilation and dependencies automatically: ```bash # meta-layers/meta-custom/recipes-apps/egui-kiosk/egui-kiosk_1.0.bb SUMMARY = "Rust egui Fullscreen Kiosk Application" DESCRIPTION = "Cross-platform GUI kiosk application built with egui framework" LICENSE = "MIT" LIC_FILES_CHKSUM = "file://LICENSE;md5=935a9b2a57ae70704d8125b9c0e39059" inherit cargo_bin systemd # Enable network access for cargo dependencies do_compile[network] = "1" SRC_URI = "\ git://localhost/rust-workspace.git;protocol=file;branch=main \ file://egui-kiosk.service \ file://egui-kiosk-init.sh \ file://weston.ini \ " SRCREV = "${AUTOREV}" S = "${WORKDIR}/git" # Point to specific app in workspace CARGO_SRC_DIR = "apps/egui-kiosk" # Cargo features for kiosk mode CARGO_FEATURES = "kiosk-mode fullscreen wayland" # Runtime dependencies for GUI applications RDEPENDS_${PN} += "\ wayland \ weston \ wayland-protocols \ libxkbcommon \ fontconfig \ liberation-fonts \ systemd \ " # Build dependencies DEPENDS += "\ wayland-native \ wayland-protocols-native \ libxkbcommon \ fontconfig \ " # Install systemd service and configuration do_install_append() { install -d ${D}${systemd_system_unitdir} install -m 0644 ${WORKDIR}/egui-kiosk.service ${D}${systemd_system_unitdir}/ install -d ${D}${bindir} install -m 0755 ${WORKDIR}/egui-kiosk-init.sh ${D}${bindir}/ install -d ${D}${sysconfdir}/xdg/weston install -m 0644 ${WORKDIR}/weston.ini ${D}${sysconfdir}/xdg/weston/ } SYSTEMD_SERVICE_${PN} = "egui-kiosk.service" SYSTEMD_AUTO_ENABLE_${PN} = "enable" FILES_${PN} += "\ ${systemd_system_unitdir}/egui-kiosk.service \ ${bindir}/egui-kiosk-init.sh \ ${sysconfdir}/xdg/weston/weston.ini \ " ``` ## Rust egui Application Implementation The application leverages **egui 0.24** with fullscreen kiosk capabilities and system integration: ```rust // rust-workspace/apps/egui-kiosk/src/main.rs use eframe::{egui, App, Frame, NativeOptions}; use std::env; struct KioskApp { counter: i32, show_confirmation_dialog: bool, } impl KioskApp { fn new() -> Self { Self { counter: 0, show_confirmation_dialog: false, } } } impl App for KioskApp { fn update(&mut self, ctx: &egui::Context, _frame: &mut Frame) { egui::CentralPanel::default().show(ctx, |ui| { ui.heading("Production Kiosk System"); ui.add_space(20.0); ui.horizontal(|ui| { if ui.button("Increment").clicked() { self.counter += 1; } ui.label(format!("Counter: {}", self.counter)); }); ui.add_space(20.0); if ui.button("System Settings").clicked() { self.show_confirmation_dialog = true; } }); if self.show_confirmation_dialog { egui::Window::new("Confirm Action") .collapsible(false) .resizable(false) .show(ctx, |ui| { ui.label("Are you sure you want to access system settings?"); ui.horizontal(|ui| { if ui.button("Yes").clicked() { // Handle system settings access self.show_confirmation_dialog = false; } if ui.button("No").clicked() { self.show_confirmation_dialog = false; } }); }); } } } fn main() -> Result<(), eframe::Error> { let options = NativeOptions { fullscreen: true, decorated: false, resizable: false, always_on_top: true, ..Default::default() }; eframe::run_native( "Kiosk Application", options, Box::new(|_cc| Box::new(KioskApp::new())), ) } ``` ## Systemd Service Configuration for Auto-Start The systemd service ensures reliable application startup with proper dependencies and restart capabilities: ```ini # files/egui-kiosk.service [Unit] Description=Rust egui Kiosk Application Documentation=https://github.com/company/egui-kiosk After=graphical.target weston.service Wants=weston.service Requires=graphical.target StartLimitIntervalSec=30 StartLimitBurst=3 [Service] Type=simple User=kiosk Group=kiosk Environment="DISPLAY=:0" Environment="WAYLAND_DISPLAY=wayland-0" Environment="XDG_RUNTIME_DIR=/run/user/1000" Environment="RUST_LOG=info" ExecStartPre=/bin/sleep 3 ExecStart=/usr/bin/egui-kiosk ExecReload=/bin/kill -HUP $MAINPID Restart=always RestartSec=5 WatchdogSec=30 # Resource limits MemoryMax=256M CPUQuota=80% # Security settings NoNewPrivileges=true PrivateTmp=true ProtectSystem=strict ProtectHome=true ReadWritePaths=/tmp /run/user/1000 [Install] WantedBy=graphical.target ``` ## initramfs Integration Strategy The initramfs approach embeds the Rust application directly in the boot image for fastest startup times and minimal attack surface. ### initramfs Image Recipe ```bash # meta-layers/meta-custom/recipes-core/images/initramfs-egui-image.bb DESCRIPTION = "Minimal initramfs image with Rust GUI application" PACKAGE_INSTALL = "\ initramfs-framework-base \ busybox \ base-files \ egui-kiosk \ weston \ liberation-fonts \ ${VIRTUAL-RUNTIME_base-utils} \ " # Minimize image size IMAGE_LINGUAS = "" LICENSE = "MIT" inherit core-image # Optimize for minimal size IMAGE_ROOTFS_SIZE ?= "32768" IMAGE_ROOTFS_EXTRA_SPACE = "0" IMAGE_OVERHEAD_FACTOR = "1.0" # Bundle with kernel INITRAMFS_IMAGE_BUNDLE = "1" ``` ### initramfs Boot Module ```bash # meta-layers/meta-custom/recipes-core/initramfs-egui-boot/files/egui-boot #!/bin/sh egui_boot_enabled() { return 0 } egui_boot_run() { msg "Starting GUI boot sequence..." # Mount essential filesystems mount -t proc proc /proc mount -t sysfs sysfs /sys mount -t devtmpfs devtmpfs /dev # Setup runtime directories mkdir -p /run/user/1000 chown 1000:1000 /run/user/1000 # Start Wayland compositor export XDG_RUNTIME_DIR="/run/user/1000" export WAYLAND_DISPLAY="wayland-0" weston --backend=drm-backend.so --idle-time=0 & sleep 2 # Start Rust GUI application egui-kiosk & msg "GUI application started successfully" } ``` ## SWUpdate A/B Partitioning System The A/B update system provides **atomic updates with automatic rollback**, ensuring system reliability even during power failures or failed updates. ### Partition Layout Configuration ```bash # Recommended eMMC/SSD partition layout /dev/sda ├── sda1 # Boot partition (EFI/BIOS, 64MB) ├── sda2 # RootFS A (ext4, 2GB) ├── sda3 # RootFS B (ext4, 2GB) ├── sda4 # Data partition (ext4, remaining) └── sda5 # U-Boot environment (1MB) ``` ### WIC Image Configuration for A/B Layout ```bash # meta-layers/meta-custom/wic/dual-rootfs.wks # Boot partition with bootloader part /boot --source bootimg-partition --sourceparams="loader=u-boot" \ --ondisk sda --fstype=vfat --label boot --size 64M --align 1024 # Root filesystem A (active partition) part / --source rootfs --rootfs-dir=${IMAGE_ROOTFS} \ --ondisk sda --fstype=ext4 --label rootfs_a --size 2G # Root filesystem B (update target, initially empty) part --ondisk sda --fstype=ext4 --label rootfs_b --size 2G # Persistent data partition part /data --ondisk sda --fstype=ext4 --label data --size 1G \ --fsoptions="defaults" # U-Boot environment for A/B switching part --ondisk sda --size 1M --label uboot_env bootloader u-boot ``` ### SWUpdate Configuration ```libconfig # files/sw-description software = { version = "1.0.0"; description = "Dual-copy Rust GUI system update"; hardware-compatibility: [ "1.0", "2.0" ]; stable = { copy1 = { images: ( { filename = "core-image-egui-minimal.ext4.gz"; type = "raw"; compressed = true; device = "/dev/sda2"; sha256 = "$swupdate_get_sha256(core-image-egui-minimal.ext4.gz)"; } ); scripts: ( { filename = "post-update.sh"; type = "shellscript"; } ); bootenv: ( { name = "rootfs_part"; value = "2"; }, { name = "upgrade_available"; value = "1"; } ); }; copy2 = { images: ( { filename = "core-image-egui-minimal.ext4.gz"; type = "raw"; compressed = true; device = "/dev/sda3"; sha256 = "$swupdate_get_sha256(core-image-egui-minimal.ext4.gz)"; } ); scripts: ( { filename = "post-update.sh"; type = "shellscript"; } ); bootenv: ( { name = "rootfs_part"; value = "3"; }, { name = "upgrade_available"; value = "1"; } ); }; }; scripts: ( { filename = "partition-select.lua"; type = "lua"; } ); } ``` ### Automatic Partition Selection Logic ```lua -- files/partition-select.lua function preinst() -- Determine current root partition local f = io.open("/proc/cmdline", "r") local cmdline = f:read("*all") f:close() local current_part = cmdline:match("root=/dev/sda(%d)") if current_part == "2" then -- Currently on partition 2, update partition 3 swupdate.notify(RECOVERY, "Updating partition 3 (copy2)") return 0, "copy2" else -- Currently on partition 3 or default, update partition 2 swupdate.notify(RECOVERY, "Updating partition 2 (copy1)") return 0, "copy1" end end function postinst() swupdate.notify(RECOVERY, "Update installation completed") return 0 end ``` ## QEMU Testing Configuration QEMU provides comprehensive testing capabilities with **KVM acceleration** and proper graphics support for egui application validation. ### QEMU Launch Configuration ```bash #!/bin/bash # scripts/deploy-qemu.sh set -e IMAGE_PATH="yocto/build/tmp/deploy/images/qemux86-64" KERNEL="${IMAGE_PATH}/bzImage" ROOTFS="${IMAGE_PATH}/core-image-egui-minimal-qemux86-64.ext4" # Launch QEMU with graphics support qemu-system-x86_64 \ -enable-kvm \ -m 2048 \ -smp 4 \ -cpu host \ -kernel "${KERNEL}" \ -drive file="${ROOTFS}",if=virtio,format=raw \ -append "root=/dev/vda rw console=ttyS0 console=tty0 quiet" \ -netdev user,id=net0,hostfwd=tcp::8080-:8080,hostfwd=tcp::2222-:22 \ -device virtio-net-pci,netdev=net0 \ -device virtio-vga-gl \ -display gtk,gl=on \ -audiodev pulse,id=audio0 \ -device intel-hda \ -device hda-duplex,audiodev=audio0 \ -serial stdio \ -show-cursor ``` ### Automated Testing Framework ```bash #!/bin/bash # scripts/test-system.sh test_boot_time() { echo "Testing boot time..." start_time=$(date +%s.%N) timeout 60 qemu-system-x86_64 \ -enable-kvm -m 1024 -nographic \ -kernel "${KERNEL}" \ -drive file="${ROOTFS}",if=virtio,format=raw \ -append "root=/dev/vda rw console=ttyS0 init=/bin/sh" & QEMU_PID=$! wait $QEMU_PID end_time=$(date +%s.%N) boot_time=$(echo "$end_time - $start_time" | bc) echo "Boot time: ${boot_time} seconds" if (( $(echo "$boot_time < 15" | bc -l) )); then echo "PASS: Boot time acceptable" else echo "FAIL: Boot time too slow" exit 1 fi } test_application_startup() { echo "Testing application startup..." # Launch system and verify egui application starts timeout 120 expect -c ' spawn qemu-system-x86_64 -enable-kvm -m 1024 -nographic \ -kernel '"${KERNEL}"' \ -drive file='"${ROOTFS}"',if=virtio,format=raw \ -append "root=/dev/vda rw console=ttyS0" expect "login:" { send "root\r" } expect "# " { send "systemctl status egui-kiosk\r" } expect "active (running)" { puts "SUCCESS: egui application running" exit 0 } timeout { puts "FAIL: Application not started in time" exit 1 } ' } test_boot_time test_application_startup echo "All tests passed!" ``` ## Minimal Linux System Configuration The system optimizes for **minimal resource usage** while maintaining essential functionality for the Rust GUI application. ### Minimal Image Recipe ```bash # meta-layers/meta-custom/recipes-core/images/core-image-egui-minimal.bb SUMMARY = "Minimal Linux system with Rust egui application" IMAGE_INSTALL = "\ packagegroup-core-boot \ egui-kiosk \ weston \ liberation-fonts \ systemd \ networkmanager \ swupdate \ ${CORE_IMAGE_EXTRA_INSTALL} \ " # Remove unnecessary features DISTRO_FEATURES_remove = "alsa bluetooth wifi nfs zeroconf pci 3g nfc x11" IMAGE_FEATURES = "read-only-rootfs" # Package optimization PACKAGE_CLASSES = "package_ipk" IMAGE_FSTYPES = "ext4 ext4.gz wic" # Size constraints IMAGE_ROOTFS_SIZE ?= "512000" # 512MB IMAGE_OVERHEAD_FACTOR = "1.1" LICENSE = "MIT" inherit core-image # Remove debug packages PACKAGE_EXCLUDE = "\ gdb \ gdbserver \ strace \ ${@bb.utils.contains('DISTRO_FEATURES', 'x11', 'x11-common', '', d)} \ " # Optimize systemd IMAGE_INSTALL_append = " systemd-analyze" SYSTEMD_DEFAULT_TARGET = "graphical.target" ``` ### System Optimization Configuration ```bash # meta-layers/meta-custom/recipes-core/systemd/systemd/system.conf [Manager] LogLevel=warning LogTarget=kmsg SystemMaxUse=50M RuntimeMaxUse=50M DefaultTimeoutStartSec=10s DefaultTimeoutStopSec=5s DefaultRestartSec=5s ``` ## Build and Deployment Workflows The complete workflow integrates Docker containerization with automated builds, testing, and deployment. ### Environment Setup Script ```bash #!/bin/bash # scripts/setup-environment.sh set -e echo "Setting up Yocto development environment..." # Export user ID for Docker export USER_ID=$(id -u) export GROUP_ID=$(id -g) # Initialize Git repositories if not present if [ ! -d "yocto/sources/poky" ]; then echo "Cloning Yocto repositories..." mkdir -p yocto/sources cd yocto/sources git clone git://git.yoctoproject.org/poky -b scarthgap git clone git://git.openembedded.org/meta-openembedded -b scarthgap git clone https://github.com/rust-embedded/meta-rust-bin.git -b master git clone https://github.com/sbabic/meta-swupdate.git -b scarthgap cd ../.. fi # Start Docker environment echo "Starting containerized build environment..." docker-compose up -d yocto-builder # Wait for container to be ready sleep 5 # Initialize build environment inside container docker-compose exec yocto-builder bash -c " cd /workspace/yocto && source sources/poky/oe-init-build-env build && # Configure layers bitbake-layers add-layer ../sources/meta-openembedded/meta-oe bitbake-layers add-layer ../sources/meta-openembedded/meta-python bitbake-layers add-layer ../sources/meta-openembedded/meta-networking bitbake-layers add-layer ../sources/meta-rust-bin bitbake-layers add-layer ../sources/meta-swupdate bitbake-layers add-layer ../../meta-layers/meta-custom # Configure build cat >> conf/local.conf << 'EOF' MACHINE = \"qemux86-64\" DISTRO = \"poky\" # Rust support ENABLE_RUST = \"1\" # Performance optimizations BB_NUMBER_THREADS = \"8\" PARALLEL_MAKE = \"-j8\" # Package management PACKAGE_CLASSES = \"package_ipk\" # SWUpdate integration PREFERRED_PROVIDER_u-boot-fw-utils = \"libubootenv\" IMAGE_FSTYPES += \"ext4.gz wic\" # Add applications to rootfs CORE_IMAGE_EXTRA_INSTALL += \"egui-kiosk swupdate swupdate-www\" # Enable systemd DISTRO_FEATURES_append = \" systemd\" VIRTUAL-RUNTIME_init_manager = \"systemd\" DISTRO_FEATURES_BACKFILL_CONSIDERED = \"sysvinit\" VIRTUAL-RUNTIME_initscripts = \"\" EOF " echo "Environment setup complete!" echo "To enter the build environment, run: docker-compose exec yocto-builder bash" ``` ### Complete Build Script ```bash #!/bin/bash # scripts/build-images.sh set -e echo "Building Yocto images..." # Build in container docker-compose exec yocto-builder bash -c " cd /workspace/yocto && source sources/poky/oe-init-build-env build && # Build core system echo 'Building minimal egui system...' bitbake core-image-egui-minimal # Build update image echo 'Building SWUpdate package...' bitbake update-image # Build SDK for development echo 'Building extensible SDK...' bitbake core-image-egui-minimal -c populate_sdk_ext echo 'Build completed successfully!' echo 'Images available in: build/tmp/deploy/images/qemux86-64/' " # Test the built image echo "Testing built image..." ./scripts/test-system.sh echo "Build and test completed successfully!" ``` ### Update Package Creation ```bash #!/bin/bash # scripts/create-update.sh set -e VERSION=${1:-$(date +%Y%m%d-%H%M%S)} OUTPUT_DIR="updates" echo "Creating SWUpdate package version: $VERSION" mkdir -p $OUTPUT_DIR # Build update in container docker-compose exec yocto-builder bash -c " cd /workspace/yocto && source sources/poky/oe-init-build-env build && # Update version in sw-description sed -i 's/version = \".*\"/version = \"$VERSION\"/' \ /workspace/meta-layers/meta-custom/recipes-support/update-image/files/sw-description # Build update package bitbake update-image # Copy to output directory cp tmp/deploy/images/qemux86-64/update-image-qemux86-64.swu \ /workspace/$OUTPUT_DIR/system-update-$VERSION.swu " echo "Update package created: $OUTPUT_DIR/system-update-$VERSION.swu" echo "Deploy with: swupdate -i $OUTPUT_DIR/system-update-$VERSION.swu" ``` ## Production Deployment and Validation This comprehensive system provides a **production-ready embedded Linux platform** with automatic updates, security hardening, and reliable operation. The containerized build approach ensures consistent results across development teams, while the A/B update system provides robust field update capabilities with automatic rollback protection. **Key advantages of this architecture:** - **Minimal host dependencies** through complete Docker containerization - **Fast development cycles** with shared state caching and incremental builds - **Reliable updates** with atomic A/B partition switching and automatic rollback - **Optimized performance** with minimal Linux configuration and Rust applications - **Comprehensive testing** with QEMU emulation and automated validation The system scales from single-developer projects to large embedded teams, providing a solid foundation for Rust-based embedded GUI applications with modern DevOps practices and reliable over-the-air update capabilities.