BRINGUP gains the Phase 4 (stale-gate cluster) record - code-complete,
host-verified, hot-deployable, bench drills listed. LIGHTBURN.md now
describes the job-based armed window: relock at program end, on a
sender change, or after the spindle-off grace (counting even in Hold).
Code-complete and host-verified across forgectrl, the GRBL controller,
the cloud clients, and the kernel module; kernel rows ride the pending
image flash, bench drills listed.
- Move the passwordless-root debug-tweaks image feature out of the
shared kas config into forgefirm-image-dev.bb, so the release
forgefirm-image built from the same config is not passwordless-root.
release.sh gains a gate that reads the built rootfs /etc/shadow and
fails on an empty root password, plus a config-level guard that
debug-tweaks is not present in the resolved kas dump. (B-1)
- The installer copies ffboot out of the signature-verified new rootfs
it already mounts, instead of fetching and executing it from a mutable
GitHub raw ref. (B-2)
- Record audit remediation Phase 2 (GATE B) status in BRINGUP.md,
including the bench pass still required to close the gate.
- LIGHTBURN.md: mandatory "Before you cut" safety section; the
walkthrough now reflects the firing machine (dry runs need the
layer output off or M5; live first-cut instructions); the homing
entry documents homing_mode and the gfcloud method; the machine
address is a placeholder.
- README.md: condensed safety section linking the full text and the
regulatory notes.
- INSTALL.md: "Regulatory and legal" section ahead of the install
steps; routine updates route through the panel updater rather than
the installer.
- BRINGUP.md: the release signing key is described as held offline
(no on-disk path); bench address and credential notes removed;
Next-work item 7 corrected (the installer embeds the production
release key); status entry for audit remediation Phases 0-1; the
GATE A kernel drills join the pending image-flash checklist.
- bench scripts: the target host comes from GF_HOST (or argv) instead
of a hardcoded address.
- laser_stream_test.py: per-session controller runs with a hermetic
cooling-verdict publisher; new assertions that every stream
terminates with FIRE clear (including M3 held to stream end) and
that no FIRE bit rides a zero-step gap; a cycle-churn session
exercises the stop/start seams.
Audit findings D-1, D-2, D-3, D-5, D-10, D-12, B-10, and the harness
half of D-4/G-1.
Add Next work item 9: the panic handler, control_12v removal, decay-mode
state tracking, module build hygiene, SDMA/EPIT/GPIO probe guards and
head_make_safe are code-complete and build-verified, with the bench check
each one still needs and a note that the batch ships with a full image
flash rather than a module hot-swap.
kas/README: drop the control_12v residue (the node is gone), state that
the buzzer driver is not part of ForgeFIRM, and record that the pre-SPDX
LICENSE strings remain only in layers this build does not use.
The controller now maps the door pair and the interlock loop onto the core's
safety-door signal, with the e-stop bit behind a machine setting. Records what
is gated and what is deliberately not, the answer to the audit's latch-reset
question, and the bench items the change still needs.
The interlock_circuit bitmask is fully mapped: b0/b1/b3 from the scope
experiment already recorded in the gate section, b2/b4 from the factory decode
the attributes were ported from, and the armed kill-mid-FIRE drills read the
mask consistently across armed, firing, idle and disarmed states. Trip recovery
was exercised in commissioning runs.
The safety-mapping item now says what is actually left of it: door and e-stop
evdev into the controller's feed-hold and halt path, plus the question of a
software reset path for the laser latch.
README: replace the stalled REST/GUI roadmap with what the firmware does -
GRBL mode over TCP for LightBurn/UGS/cncjs, optional cloud mode, the web
control panel, cameras, A/B install beside the factory firmware - plus the
supported hardware incl. the 8 MP camera limitation and a real roadmap.
kas/README: xvclk is the board's 24 MHz oscillator; the pin/push rule covers
every source repo; camera and motion sections are hardware-validated, with the
OV8856 caveat spelled out; the device-tree item closes with the control_12v
residue named; forgectrl is the machine-services daemon with two supervised
controllers.
LIGHTBURN.md: drop the pre-first-light status line.
BUILD.md: state the NXP firmware licensing and where the EULA lives on the
machine. The image now installs firmware-imx-lic, so the EULA text ships beside
the VPU/EPDC blobs it covers; a bbappend declares that package at parse time,
which is what makes it installable from an image recipe.
Record the finished shared-services state: forgectrl is the one
machine-services daemon behind both controller modes, both controllers
are cooling-engine clients, and the drill and soak coverage that proves
it. Attribute the cooling policy records to the engine's present home.
Add the remaining polish as Next work item 8 - diagnostics as engine
modes, the rail-policy remainder, cloud per-job fan profile
confirmation, /cool/status cosmetics, and button edge detection.
The runbook operational sections now describe the machine as it runs:
forgectrl as the machine-services daemon (supervisor, pulse-device
broker, motion-liveness gate, cooling engine, cameras), controllers
spawned and supervised rather than init-started, standalone driver
runs as the bench/debug exception, diagnostics suspending the active
controller through the supervisor, the cooling engine as the thermal
owner with the drivers as thin clients, and the new /mode and /cool
endpoints. A dated correction closes the no-motion record with the
DRV8825 wedge diagnosis and the liveness/homing hardening; the
cold-start section gains the first-light and shared-services
milestones.
Hardware facts from the 2026-08-11 bench session: the DRV8825 drivers
can come out of a 40 V rail power-up unserviceable (playback and
counters run, motors dead; recovery is a longer true power-off, at
worst a machine power cycle) - so counters/anchors are never proof of
motion and the rail stays up. The head accelerometer is the motion
truth (device mapping, bench-characterized thresholds, read-rate
limits), and every probe move goes +X first - a cable lives at the
end of left travel.
The controller lifecycle belongs to the forgectrl supervisor (it
spawns the selected controller as a direct child); the init script no
longer starts grblHAL and remains only as a manual emergency stop.
BRINGUP item 2: grbl-mode laser software implemented + bench-verified
without fire (record in the gate list); first light pending.
LIGHTBURN.md: arming/button-press operation, S-max 1000, fire gates.
scripts/bench/laser_stream_test.py: host-side stream-dump contract
verification against the null-sink build.
The cloud-action-surface branches are merged: forgectrl builds from main
(same revision), and the forgefirm-app include pins the python3-gfhardware
master head (adds docs/CLOUD.md and the clean-shutdown gfhome).
gfhome.py, ffmachine.py, and gfcloud.py (with the gfcloud init script
and gfhome conf sample) live in the python3-gfhardware repository's
forgefirm-app/ directory. The three recipes fetch that repo through a
shared include (forgefirm-app.inc) carrying a single pinned SRCREV,
replacing the local file:// copies. Package names, dependencies, and
installed paths are unchanged.
ws_connect() returns the running WsClient, so gfhome no longer needs to
build the client by hand to get a clean disconnect; the exit path uses
WsClient.shutdown() to stop and join the socket thread.
Points the forgectrl recipe at the pushed cloud-action-surface commit
(controller_mode=cloud + the compatibility banner) so a test image
carries the cloud-mode UI. Repoint to main once the branch is merged.
Enable the read-only firmware-version probe and point it at
/data/forgefirm/gf-latest.json, where the web-service client records the
latest firmware Glowforge advertises and the version this release was
tested against. forgectrl reads that file for the cloud-mode
compatibility banner.
gfcloud runs the machine under the Glowforge web service (the factory
cloud experience: the app drives homing, framing, printing). Its init
service starts it only when controller_mode = cloud - mutually exclusive
with grblHAL, which already stands down for that mode - so it owns
/dev/glowforge exclusively. Persistent GFUIService loop with a clean
SIGTERM shutdown that safes the hardware; the init script mirrors
grblhal.init. The image installs gfcloud and python3-ffmachine.
Extract the shared-config identity overrides and the forgectrl-routed
ForgectrlMachine from gfhome into an installed module (python3-ffmachine);
gfhome imports it instead of carrying its own copy, so the coming gfcloud
daemon builds the same machine the same way.
Record the Glowforge service/firmware version each release validated
cloud mode against, kept distinct from the version cloud mode advertises;
forgectrl reads it to warn when the live service has moved past it.
Replace the local dispatch table with gfutilities' dispatch_action
(allow_print=False), so the homing runner and GFUIService share one
action surface and cannot drift.
b5c6d9c declares the shared config readers in glowforge_homing.c;
without the declaration the hard-float build read the homing-session
timeout from the wrong register and SIGTERMed every gfcloud homing
runner ~55 ms after spawn.
Bump to the forgectrl revision whose startup pass reloads
regulatory.db (required while the flashed kernel still has built-in
cfg80211 - its boot-time load fails pre-mount and stays failed
without a reload), hints a region only when one is set (unset =
automatic: the AP's 802.11d country, else world; a 00 hint over the
kernel's own world default reports the confusing intersection alias
"country 98"), and pins wlan0 power save off.
BRINGUP.md: bench record for the wireless-regulatory diagnosis and
the live-verified wifi_country / power-save flow; the kernel-batch
note now carries CFG80211/MAC80211=m, DEFAULT_PS off, and the lm75
vs-supply.
Bump to the forgectrl revision that adds the wifi_country setting
(System-tab region picker); the daemon applies it with iw reg set at
startup and on change, so iw joins RDEPENDS. regulatory.db itself
ships via the glowforge-image base (wireless-regdb-static).
Before writing the target slot, the installer now shows what it holds
(factory firmware v<ver>, ForgeFIRM, an unrecognized filesystem, or
unknown/unreadable content). Factory images are archived as before;
anything else requires the operator to type ERASE, since it is
overwritten without a backup. The archive manifest now records the
semantic FIRMWARE_VERSION (ver=), which the update manager displays in
the restore list. Bump forgectrl to the matching GUI change.
A factory slot's displayed version is now the semantic FIRMWARE_VERSION
from /etc/build (e.g. v2.6.0-2228) rather than the build datetime in
/etc/version - what the factory itself calls the release, and what the
update-manager GUI shows for each slot. The datetime still orders
releases for the -e newest-factory selection (a monotonic counter;
mashing the semantic version's digits would misorder a major-version
rollover). Bench: slot shows v2.6.0-2228, -e picks it correctly.
/etc/forgefirm/keys ships the ForgeFIRM release-signing public key
and the Glowforge factory keyring (public keys only) - the update
manager verifies release downloads/uploads against the former and
factory archives against the latter. forgectrl SRCREV bumped to the
update-manager commit; runtime deps on ffboot, fwup, the keyring, and
curl made explicit.
The embedded pubkey is the production key from the signing ceremony;
release.sh's key-match gate now refuses any other signer. Verified:
production-signed archives pass fwup 1.16 and the factory's 0.14.2;
dev-signed archives are rejected.
The runbook no longer records the bench machine's fuse identity (a
fuse identity cannot be rotated, so a public document must never
carry it); the forgectrl pin follows that repo's history rewrite.
Proves a fresh clone builds the release image on a hosted runner
(sibling checkouts for meta-openglow and the kernel-module externalsrc,
rm_work to fit the disk budget) and publishes artifact checksums for
comparison against locally built releases. Never produces release
artifacts - releases are built and signed on the maintainer's host.
Gates (clean tree, version single-source across FORGEFIRM_RELEASE /
rootfs stamp / .fw meta-version / tag, rootfs-vs-slot size with early
warning, installer-embedded pubkey must match the signing key,
factory-era fwup verification of the packed archive), then build,
pack, sign, checksum, and stage forgefirm.fw + sha256sums.txt +
forgefirm-image-glowforge.rootfs.wic.gz with the gh publish command
(--publish runs it where gh is authenticated). release.sh --dev packs
a dev-key-signed forgefirm-dev.fw from the release rootfs for the GUI
upload path. Signing keys are always passed explicitly - no defaults.
kas/README release order and the plan doc updated to match.
The image's fstab keeps the factory slots mounted under /factory, and
busybox mount's auto-type iteration against an already-mounted ext4
device provokes a cosmetic kernel 'Can't open blockdev' for each
foreign-type claim (reproduced and pinned on the bench: ext3-typed
mount of an ext4-held device prints it; ext4-typed does not). Probes
now reuse an existing mountpoint from /proc/mounts and mount fresh
targets with an explicit -t ext4.
Documents the new installer: A/B slot install with no repartitioning,
factory archives to /data, signature-verified forgefirm.fw, ffboot
switching, offline install, and the automatic legacy-layout
migration.
The image's busybox has no blockdev applet, so the grow path silently
skipped. Sector counts now come from /sys/class/block (no external
tool); log lines also go to /dev/kmsg so migration results are visible
in dmesg after boot.
Newer factory firmware's generic /etc/fw_env.config points at the
wrong device; its per-device /etc/fw_env_mmcblk2.config is the correct
one for the eMMC environment. The read-back verify caught the failed
write and aborted before the flip, as designed.
dd|gzip runs backgrounded while the installer prints compressed MB
every few seconds (old busybox dd has no status=progress); dd's exit
status is captured through a file so a device read failure is not
masked by gzip succeeding on truncated input.
The gf_hostname override is gone (the forgectrl UI no longer offers
it): the hostname is a pure derivation of the serial - base 23 over
the factory consonant alphabet - so a gf_serial override re-derives
MACHINE.HOSTNAME and the fuse derivation stands otherwise. BRINGUP
records the panel rework (units, fuse identity, always-on position)
as offline-verified; board deploy and pin bumps held during the
firmware-upgrade bench testing.
Newer factory firmware (2024) has no /factory/imgN mounts and a
read-only rootfs, so slot probing and post-write verification mount
under /tmp, with the active slot read from the running root. The
target-slot unmount sweeps /proc/mounts (older firmware DOES mount the
slots). A failed ffboot download keeps an existing /data/ffboot
instead of aborting, so a local-.fw install works fully offline.
install-forgefirm.sh is now single-stage and never repartitions: run
from factory firmware, it archives every factory slot version plus the
recovery boot partitions to /data/forgefirm/archive (manifest with
md5s), verifies the signed forgefirm.fw against the embedded ForgeFIRM
pubkey (raw 32-byte form for the factory's fwup 0.14.2; dev key until
the production key ceremony), applies it to the INACTIVE slot with the
factory's own fwup, post-verifies the written rootfs, installs
/data/ffboot, and flips the saved env with read-back verification. The
booted factory slot stays installed and bootable; /data is untouched
beyond the archive. Fixed release asset name forgefirm.fw (version in
the fwup metadata and release tag).
slotmigrate (new recipe, rcS before mountall) reclaims the legacy
layout on eMMC-slot boots: deletes p4, grows p3 to the end of the
disk (sfdisk + partx BLKPG - works with a sibling partition as root),
then e2fsck+resize2fs. Every step is keyed off the actual disk state,
so interrupted runs resume and factory-layout disks are a no-op; SD
boots never touch the eMMC.
Basic/Plus share the passive closed-loop cooling and the 60-75 F
window per Glowforge's tech specs and owners-forum consensus; the
Pro's solid-state TEC buys 60-81 F. Spec-level only - tec_present
stays a user setting regardless, since tec_on has no readback.
Low side: factory floors (CM* window minimums ~1-4 C, the ~16 C
'warming up' operating floor) and the planned cool_temp_min /
cool_temp_start keys with a heater-driven warm-up phase. TEC:
presence is undetectable (tec_on is write-only, Pro-only hardware on
a common board) so tec_present becomes a user setting, with
hysteresis control toward the factory ~18 C setpoints when enabled.
-l inventories every bootable partition (SD, eMMC slots 1/2, legacy
p4) as machine-parsable key=value lines: firmware type
(forgefirm/factory), version, kernel presence, booted/next markers,
plus the saved-env selection - the shared probe for the installer and
the forgectrl update manager.
Boot switching now writes all four selection variables (mmcdev,
mmchwpart, mmcpart, mmcroot) in one fw_setenv -s transaction and
read-back verifies, falling back from the libubootenv script format
to the classic u-boot-tools format to per-variable writes - the same
script works on factory firmware and ForgeFIRM. mmchwpart was
previously never set and a mixed env could survive a mid-flip power
cut. Switch targets must pass a content probe (rootfs mounts, kernel
present) unless -f. -e picks the newest factory slot by probing,
excluding slots occupied by ForgeFIRM.
The new ffboot recipe installs it as /usr/sbin/ffboot with
/etc/fw_env.config (factory-identical redundant env layout at eMMC
0x80000/0x82000) - previously the image shipped fw_setenv with no
config. Added to forgefirm-image.
Slot-agnostic boot proven on the bench: the same release ext4 boots
from SD and from eMMC p4 steered by the saved env alone. fwup
cross-version compatibility proven; slot-sized rootfs, size gate,
ext4 artifact and mkfw.sh in place. Found for Phase 1: the image
ships no /etc/fw_env.config.
The release image now targets the 200 MiB factory eMMC slot: content
plus 40 MiB working space, hard build failure past the slot size. The
raw ext4 is deployed alongside the wic; scripts/mkfw.sh packs it into
a signed .fw with factory-pattern upgrade.a/upgrade.b tasks. fwup
1.16.0 recipe (applies ForgeFIRM and Glowforge-signed archives on
device) is installed in both images. Dev images stay SD-sized with a
256 MiB working margin and no ceiling.
Verified on the 20260808153331 build: release ext4 180.8 MiB; signed
.fw applies byte-exact with fwup 1.16.0 and with the factory's 0.14.2
(raw-format pubkey), and 0.14.2 -V verifies the signature.
flow_confirm_drill.py walks the driver's suspicion/confirmation state
machine through every verdict with real pump-off transients in one M8
session; flow_escalate_drill.py exercises the starved-re-check
escalation against a short GFCOOL_CONFIRM_MAX_S. BRINGUP records the
triage resolution (the 2026-08-03 faults were a real transient
stagnation, probable pump airlock - the check was right), the slug/
circulation measurements, and the new check semantics.
Every image writes /etc/forgefirm-version and echoes "ForgeFIRM
<version>" on the serial-console login prompt (/etc/issue, beneath the
OpenGlow banner) and at SSH login (motd). Release images carry
v${FORGEFIRM_RELEASE}; the dev image stamps the build timestamp - the
same DATETIME as the artifact name - tagged (dev) so a bench machine
is never mistaken for a release.
The init script consults controller_mode in /data/forgefirm.conf:
'cloud' (once that mode exists, with its own service reading the same
key) keeps grblHAL down; grbl, unset, or a missing config starts it.
Board-verified both ways: cloud in the conf leaves the controller
stopped with a clear message, grbl starts and serves normally.
forgectrl ad0b441: OpenGlow branding, operational /status dashboard.
grblHAL-glowforge c472a13: position anchor at homing for the status
readers. Runbook updated.
Non-empty gf_serial / gf_password / gf_hostname in /data/forgefirm.conf
(set from the forgectrl GF Cloud tab) are applied with set_cfg before
Machine() is built, so they beat the OCOTP fuse identity - Machine sets
its fuse values with keep_value. The --timeout default comes from
GFHOME_TIMEOUT_S when the controller provides it, so one GUI setting
governs the whole session. Docs: control-panel runbook notes.
The runner's Machine subclass fetches lid/head images from the
forgectrl snapshot endpoint - forgectrl owns the imx-media pipeline
whenever a stream client (LightBurn) is connected, so direct V4L2
grabs fail busy. Head captures request lamp=0 (the cloud's focus
analysis needs torch-off images); the HCil measure-laser LED is still
driven directly. Direct capture remains the fallback when the daemon
is unreachable.
The sample config gains the factory-board settings: the estop motion
gate stays off (the board's estop sense reads low during any motion),
and the forgectrl URL is configurable.
New recipe installing /usr/sbin/gfhome.py and /etc/gfhome.conf.sample
(copied to /data/etc/gfhome.conf on first run). The runner signs the
machine in with its fused identity, opens the WSS control channel with
a held client reference (a clean disconnect is impossible through
ws_connect), and drives the GFUIService dispatch table itself - minus
print - so the service's camera homing sequence (settings -> hunt ->
lid image -> corner move -> lid image) runs against the real hardware
Machine. The service ends the sequence silently, so completion is a
hunt plus at least one motion followed by a configurable quiet window;
the lens is then re-referenced against the hall sensor for a
deterministic Z. Lid/e-stop are checked before the session, print
actions are refused, and exit codes distinguish configuration and
connection failures from an incomplete homing.
Installed in both images; invoked by the grblHAL controller for $H
when homing_mode = gfcloud in /data/forgefirm.conf.
gitsm-pinned build of the controller with a sysvinit script (defaults 92, after forgectrl), installed in both images. Reboot-verified on the bench: controller and forgectrl come up unattended and Grbl answers on TCP:23.