Build¶
ForgeFIRM is built with kas. kas manages the
Yocto layers and pins their versions. The forgefirm repository is the base
of the build: it controls the build, and the images land in it. All the
commands on this page run in that repository.
Baseline¶
| Item | Value |
|---|---|
| Yocto release | Scarthgap 5.0 LTS |
| Kernel | linux-fslc 6.12 (mainline LTS, from meta-freescale) |
| Machine | glowforge (the i.MX6 Solo SOM in the Basic, the Plus, and the Pro) |
| Distro | forgefirm |
| Images | forgefirm-image (the release image) and forgefirm-image-dev (the dev image, which the bench runs) |
Prepare the host¶
A Linux host is necessary. WSL2 on Windows is also possible; Yocto supports it officially.
WSL2
Keep the full tree on the native ext4 file system of WSL2, not on a
Windows drive mount. The Windows mount breaks case sensitivity and
permissions, and it is very slow for Yocto. Give the WSL2 VM sufficient
RAM and disk in .wslconfig.
On Ubuntu or Debian (WSL2 included), install the Yocto host packages and kas:
sudo apt-get install -y gawk wget git diffstat unzip texinfo gcc build-essential \
chrpath socat cpio python3 python3-pip python3-pexpect xz-utils debianutils \
iputils-ping python3-git python3-jinja2 python3-subunit zstd liblz4-tool file \
locales libacl1 lz4 rsync
sudo locale-gen en_US.UTF-8
pipx install kas # pipx, because Ubuntu 24.04 (PEP 668) blocks "pip install --user"
For other distributions, see the Yocto Project Quick Build. Do not build as root. The Yocto sanity checks refuse it.
Get the sources¶
The convention for work on the project is one base working directory,
openglow-forgefirm. Every source repository of the project is checked out
into it, as a sibling of the others. The directory itself is not a
repository. The kas configuration and the bench build scripts depend on this
layout: they refer to the siblings by relative path.
Make the directory, then clone the two repositories that a build needs:
mkdir openglow-forgefirm && cd openglow-forgefirm
git clone https://github.com/openglow-org/forgefirm.git
git clone -b scarthgap https://github.com/openglow-org/meta-openglow.git
The kas configuration refers to meta-openglow (branch scarthgap) at
../meta-openglow. kas gets the upstream Yocto layers itself. Each recipe
gets its ForgeFIRM source repository from GitHub at a pinned revision. Thus
a build needs no other checkout.
For work on a component, clone its repository into the same directory. The
build scripts in forgefirm/scripts/bench/ find it there, and a local
externalsrc bbappend can point to it (Build one
component).
openglow-forgefirm/ the base working directory (not a repository)
├── forgefirm/ the base repository; the build runs here
│ ├── kas/
│ │ └── forgefirm-glowforge.yml the build entry point
│ ├── meta-forgefirm/ the ForgeFIRM layer
│ ├── layers/ the upstream layers that kas clones (gitignored)
│ ├── build/ the bitbake output, images included (gitignored)
│ └── downloads/ sstate-cache/ the caches (gitignored)
├── meta-openglow/ the Glowforge BSP layers (necessary for a build)
├── forgectrl/ a component checkout, for work on it (optional)
├── grblHAL-glowforge/ the same
├── kernel-module-glowforge/ the same
├── python3-gfhardware/ the same
├── Glowforge-Utilities/ the same
└── forgefirm-docs/ this site (optional)
The configuration refers to meta-openglow as a local sibling. Thus the
build uses your in-place edits to the BSP. The configuration also has a
pinned-remote block, commented out. With that block, the forgefirm
repository is fully self-contained (Release flow).
Build the images¶
Run the commands from the root of the forgefirm repository, so that the
outputs land in it:
cd forgefirm
kas build kas/forgefirm-glowforge.yml # get the layers, then a full build
kas shell kas/forgefirm-glowforge.yml # an interactive bitbake environment
kas dump kas/forgefirm-glowforge.yml # show the resolved configuration
kas build builds the kas target, forgefirm-image. A build for the bench
makes both images in one bitbake run. The bench runs the dev image, and the
two images from one run have the same manifest identity (Test):
kas clones the upstream layers into forgefirm/layers/, builds in
forgefirm/build/, and writes the images to
forgefirm/build/tmp/deploy/images/glowforge/:
forgefirm-image-glowforge.rootfs.wic.gz the release image
forgefirm-image-dev-glowforge.rootfs.wic.gz the dev image
The source variant¶
A release publishes the source of the software that it installs. The
overlay kas/source-bundle.yml turns on the Yocto archiver, and the build
writes the source of each recipe to forgefirm/build/tmp/deploy/sources/:
The release pipeline merges this overlay into the release build, so the
source and the image come from one build. scripts/source-bundle.py packs
the archives into the release asset. Release flow, "The
source bundle", has the whole procedure.
The debug-kernel variant¶
One dev image carries a kernel with the lock-correctness options
(DEBUG_MUTEXES, lockdep, DEBUG_ATOMIC_SLEEP). Build it with the debug
kas config after the normal build:
The debug kernel has a different config signature. bitbake rebuilds the
kernel and the dev image under it. The other images stay in the deploy
directory. The debug image lands beside them as
forgefirm-image-dev-debug-glowforge.rootfs-*.wic.gz, version string
(dev-debug).
Do not ship the debug image. The options make the kernel slow. Boot the
debug image one time to run the debug-kernel drills
(scripts/bench/debug_kernel_drills.py), then flash the real image. The
drills cycle the 40 V rail, so the machine must be idle.
forgefirm-image-dev is a strict superset of forgefirm-image. It adds a
root login without a password, python3, gdb and strace, the acceptance tool
forgetest, and the bench tools. A release image never has them. The debug
features are in forgefirm-image-dev.bb, not in the kas configuration. Thus one build gives a hardened release image and a debug dev image.
release.sh refuses a release rootfs that has a root entry without a
password.
The U-Boot binary
The build also writes u-boot-glowforge.imx to the deploy directory.
It is for reference only. Each supported install and boot flow keeps the
factory bootloader on the eMMC. Its environment configuration agrees with
the factory layout (0x80000 primary, 0x82000 redundant), but no install
path uses it. Do not flash it.
Build in a container¶
kas can run the build in its own container (Docker or Podman), for a reproducible host:
Lock the layer versions¶
The configuration follows the scarthgap branch of each upstream layer. To
lock each layer to an exact commit:
kas loads the lockfile automatically on the next runs. The lockfile is committed. Refresh it only when you decide to.
Build-time facts¶
- The kas configuration sets
ACCEPT_FSL_EULA = "1". The i.MX6 BSP installs NXP firmware blobs (VPU, EPDC) under the NXP firmware EULA. The packagefirmware-imx-licputs the license text next to the blobs, at/usr/share/licenses/firmware-imx/EULA. Keep it there in each image that you redistribute. The SDMA firmware comes fromlinux-firmware, which has its own license package. - Each
LICENSEstring in the layers (meta-forgefirm,meta-glowforge-bsp,meta-openglow-core) is an SPDX identifier. A recipe for a third-party component with more than one license (wlconf,python3-gfhardware) declares each license with a checksum of its license text. - The kernel is
linux-fslc. The device tree, the configuration fragment, and the layer patches are inmeta-openglow/meta-glowforge-bsp/recipes-kernel/linux/. The bbappend header lists the patches, andglowforge.cfgdescribes the configuration. The bootloader recipe isu-boot_2020.01.bbinrecipes-bsp. - The parallelism in the kas configuration is for a build VM with 12 cores
and 16 GB (
BB_NUMBER_THREADS = "8",PARALLEL_MAKE = "-j 8"). Increase it on a larger host. The download cache and the sstate cache are in theforgefirmcheckout.
Write the image to an SD card¶
cd build/tmp/deploy/images/glowforge
sudo zcat forgefirm-image-glowforge.rootfs.wic.gz | dd of=/dev/sdX bs=1M
The bench boots the dev image from an SD card (The bench). To install ForgeFIRM on the factory eMMC, use the installation instructions. The installer puts ForgeFIRM in the unused A/B slot, and it archives the factory firmware first.
Build one component¶
The recipes build the components. For fast iteration on one component, you have two options:
- Bump its pin for each iteration (Release flow).
- Add a local, untracked
externalsrcbbappend that points to a working checkout. Never commit it. If you do, released images no longer agree with the pins.
The sections below are the builds of the components outside Yocto.
grblHAL-glowforge¶
On a host, the driver runs in null-sink mode (no GFSINK). The host tests
and the CI harnesses use that build (Test). The board binary is
a cross-compile with the i.MX6 toolchain. scripts/bench/build-glowforge.sh
in the forgefirm repository does this in the Yocto build environment, with
the toolchain of the recipe. This is the production controller build. The
controller is a userspace program. To deploy a new binary, replace the binary
on the board. An image flash is not necessary.
Under the ForgeFIRM image, the driver runs as a supervised child of
forgectrl. It receives /dev/glowforge as an inherited file descriptor
(GF_PULSE_FD). Standalone, it opens the device itself:
| Variable | Meaning |
|---|---|
GFSINK |
The pulse device. Unset = null-sink test mode. |
GFSINK_RATE |
A machine tick override. The default is the XY microstep mode's: 28160 Hz at 8 (the travel-move tick of the factory firmware), 56320 at 16, 112640 at 32, from xy_microsteps in the shared config. Accepted range 1000 to 165000; the driver holds $110/$111 under what the tick carries. |
GFSINK_DEPTH_MS |
The queue depth of the shipper. Default 200 ms, which is the feed-hold latency. Minimum 20. Maximum: half the stream ring at the selected rate. |
GFSINK_LEAD_MS |
The lead of the producer over the ship cursor. Default 10 ms, range 2 to 10. The per-run debug line reports the measured minimum margin against it. The ceiling is enforced: above it a cycle that resumes while the kernel still drains keeps production ahead of the wall cursor, the re-base onto that cursor is skipped, and the overshoot is inherited by every cycle after it as dark pad the machine still has to play. |
GFSINK_DUMP |
Null-sink mode only. The file that receives the pulse stream. The CI harnesses read it. |
GF_PULSE_FD |
The inherited pulse-device descriptor, under the supervision of forgectrl. |
FORGECTRL_PORT |
The port the cooling client reports to. |
The driver reports a value that is out of range and uses the default. The
FFLOG_* variables are the same as for forgectrl (below).
Test hooks. These exist so the host harnesses can drive edges that need hardware on a machine. They are for the null-sink build, and nothing on a machine sets them.
| Variable or key | Purpose |
|---|---|
GF_SWITCH_FILE |
A file-backed EV_SW word, so a harness can drive the lid, interlock and button edges. |
GF_VERDICT_FILE |
A cooling verdict file the harness writes, in place of the engine's. |
GF_STATE_DIR |
Where the controller publishes grbl.state and grbl.settings. |
gfcloud_home_cmd |
The homing runner to spawn for $H. Honored only without GFSINK. |
laser_power_model, laser_floor_analog |
Select the retired analog rendering and its duty floor, which exist only as the harness's conservatism reference (the grblHAL driver). |
forgectrl¶
forgectrl builds with CMake. It links against ulfius, libjpeg, and zlib. The
target needs Linux with imx-media, the coda VPU driver, and v4l-utils
(media-ctl, v4l2-ctl). The meta-forgefirm recipe builds it for the
image and installs the sysvinit script from init/.
scripts/bench/build-forgectrl.sh in the forgefirm repository
cross-compiles it in the same way as the controller, with the toolchain
from the work directory of the recipe. After a clean, run bitbake forgectrl
to make that directory again.
.devcontainer/ packages the host build for VS Code (Dev Containers, with
Docker or Podman). The container is Ubuntu 24.04 with the build
dependencies. The build and the CI unit tests run in it. The task "panel: dev
server" starts the dev server of the panel when the folder opens, and port
8081 is forwarded. Interactive shells in the container export .env. Thus
the bench tools see GF_HOST and GF_TOKEN too.
| Variable | Default | Purpose |
|---|---|---|
FORGECTRL_PORT |
80 | The HTTP port: the read-only routes, loopback writes, and the redirect to HTTPS |
FORGECTRL_TLS_PORT |
443 | The HTTPS port: the login, the panel, every state change |
FORGECTRL_STREAM_Q |
75 | The JPEG quality of the stream (1 to 100) |
FORGECTRL_STREAM_FPS |
unset | The frame-rate ceiling of the stream (frames/s). Unset or 0 = the sensor maximum |
FORGECTRL_LAMP |
132 | The illumination level during capture (0 to 1023) |
FORGECTRL_NO_VPU |
unset | Force the libjpeg software encoder |
FORGECTRL_NO_NEON |
unset | Force the scalar demosaic |
FORGECTRL_NO_CACHED_BUFS |
unset | Force uncached capture buffers and a bounce copy |
FORGECTRL_NEON_CHECK |
unset | A one-shot NEON/scalar equivalence check (logged) |
FORGECTRL_NO_GPU |
unset | Force the processor demosaic (no GPU) |
FORGECTRL_GPU_CHECK |
unset | A one-shot GPU/processor demosaic equivalence check (logged), with a tighter stats cadence and the render-versus-copy split |
FORGECTRL_GPU_PASSES |
unset | Limit the GPU render passes per frame, for tuning |
FORGECTRL_NO_H264 |
unset | Serve no H.264 stream; /cam/h264 answers 503 and MJPEG remains |
FORGECTRL_H264_KBPS, FORGECTRL_H264_GOP |
engine defaults | The H.264 bit rate and GOP length |
FORGECTRL_NO_HW_SKIP |
unset | Encode every frame; no CSI hardware frame skipping |
FFLOG_LEVEL |
from the settings | Override the emit level (off to debug) |
FFLOG_STDERR |
unset | Echo the log lines to stderr, also when stderr is not a terminal (for harnesses) |
FFLOG_CONF, FFLOG_SOCK |
/data/forgefirm.conf, /dev/log |
The settings file and the syslog socket (for host tests) |
Work on the control panel¶
The panel is a plain static page under src/ui/:
index.htmltheme.css: the OpenGlow theme. Each color is a token, for light and dark, mapped onto the component variables of Bootstrap.help.js: the help text, one entry for each "?" button, each with its link to the documentation.forms.js: the shared dirty set, the save bar, the tab guard, the theme toggle, and the toasts.panel.js: the tabs, the telemetry rendering, and each action.vendor/: Bootstrap, pinned, with its license. There are no external assets and no build tools other than CMake.
The build bundles these files into one self-contained page, gzips it, and
embeds the compressed bytes in the daemon (src/ui/embed.cmake, run by
CMake). The bundled page also lands in build/ui/index.html. The daemon
decompresses the page once, at the first request, and puts the token in it.
Thus the page that ships is one plain response. The page is compressed in
the binary because the rootfs is raw ext4: bytes in .rodata are bytes on
the image.
tools/devserver.py (Python 3, standard library only) serves the files as
they are, with live reload. The browser sees the real file names and line
numbers. The open tab reloads when you save a file under src/ui/. The
option --bundle serves the page inlined, in the same way as the daemon. The
API calls from the page go to one of two backends:
- A real machine. Set
GF_HOST(an IP literal, with:portwhen the machine does not listen on the default port) andGF_TOKEN(the panel token,/data/forgefirm/panel.tokenon the machine). Put them in the environment, or in a git-ignored.envat the root of the repository..env.exampleis the template, and the server reads the file again when it changes. The server embeds the token in the page in the same way as the daemon. It proxies the requests with the address-literalHostof the machine, and the MJPEG stream passes through. Thus the panel shows live data, and its actions reach the hardware. - The built-in mock (
--mock, or automatically withoutGF_HOST): in-memory settings, status, diagnostics, slots, logs, and a placeholder camera. The mock does the same token check as the daemon on the state-changing calls, and its tables and reply shapes are the daemon's: a host test in the forgectrl repository (tests/test_devserver_mock.py) reads them out of the C sources and holds the mock to them.
cp .env.example .env # then fill in GF_HOST / GF_TOKEN
python3 tools/devserver.py # http://127.0.0.1:8081
python3 tools/devserver.py --mock
python3 tools/devserver.py --dump > panel.html # the bundled page
The page of the acceptance tool shares theme.css and the vendored Bootstrap
with the panel, byte for byte. scripts/check-ui-vendor.py in the
forgefirm repository does that check, and CI runs it (Test).
kernel-module-glowforge¶
The recipe builds the module against the kernel of the image, and the module
ships in the image. Do not build it by hand for the board. The
kernel-independent parts have host tests (make -C tests check). CI
cross-builds the module against linux-fslc 6.12 with the BSP overlay and the
patches applied (Test). A .ko change is validated on the
image that ships it (The bench). After an edit under the overlay
of the kernel recipe, the module loads only from a full image flash.
The Python components¶
The recipes install gfhardware (with its _cam extension), the cloud
applications in forgefirm-app/, and gfutilities from the pinned
revisions. gfutilities is also on PyPI (pip install gfutilities). For
work on the emulator, install it from source with pip install -e ..