gfhome: identity overrides from the shared config; budget from env

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.
This commit is contained in:
ScottW514
2026-08-07 20:05:49 -04:00
parent 8ba0ce84f3
commit 8cd98de605
3 changed files with 68 additions and 15 deletions
+23 -10
View File
@@ -1,13 +1,19 @@
# ForgeFIRM bring-up status & cold-start runbook # ForgeFIRM bring-up status & cold-start runbook
Last updated: **2026-08-07** — homing is runtime-selectable Last updated: **2026-08-07 (late)** — forgectrl is now the machine
(forgectrl web UI) and **Glowforge web-service (gfcloud) homing is CONTROL PANEL: tabbed web UI (Status / Machine / GF Cloud / GRBL) with
LIVE-VERIFIED end-to-end** ($H → cloud homing sequence → homed at the a status-first landing page (scaled lid snapshot + on-demand live
factory corner in 65 s). The first live run surfaced and fixed four stream), homing-position calibration (`gfcloud_home_x/y/z`),
platform bugs — see Next work #3, incl. the new hardware fact that cloud-identity overrides (`gf_serial`/`gf_password`/`gf_hostname`,
the estop sense reads low during any motion. Same day: fd-blocking fuses = fallback, applied by the runner via set_cfg before Machine()),
protocol pacing (idle CPU ~2%) and the fortify step_us_min fix. and a validated multi-key `/settings` API (empty value = clear; clears
Previous milestone: camera service (forgectrl MJPEG on :8080). must ride the query string — empty form-body values are dropped by the
HTTP stack). Bench + browser-verified (save/clear roundtrips from the
real UI, live toggle, no JS errors). Earlier same day: **gfcloud
homing LIVE-VERIFIED end-to-end** ($H → homed at the factory corner in
65 s; four platform bugs fixed — see Next work #3, incl. the
estop-sense-reads-low-during-motion hardware fact); fd-blocking
protocol pacing; the fortify step_us_min fix.
Read together with `AUDIT_ACTION_PLAN.md` in the project root (sibling of Read together with `AUDIT_ACTION_PLAN.md` in the project root (sibling of
this repo; per-finding status of the 2026-07-03 audit) and this repo; per-finding status of the 2026-07-03 audit) and
`kernel-module-glowforge/UAPI.md` (the pulse-stream feeder contract). `kernel-module-glowforge/UAPI.md` (the pulse-stream feeder contract).
@@ -183,8 +189,15 @@ repo's `init/`; bench builds cross-compile with
pattern as build-glowforge.sh). One ulfius daemon exposes both OV5648 pattern as build-glowforge.sh). One ulfius daemon exposes both OV5648
cameras as MJPEG over the mainline imx-media pipeline: cameras as MJPEG over the mainline imx-media pipeline:
- `GET /` — index page with a live view; `/?action=stream|snapshot` are - `GET /` — the tabbed machine control panel (Status / Machine /
the mjpg-streamer-compatible aliases (lid camera). GF Cloud / GRBL; ui.c): status page with a scaled lid snapshot +
on-demand live stream, and the settings forms for homing method,
home-position calibration, identity overrides, and the session
timeout. `/?action=stream|snapshot` remain the mjpg-streamer-
compatible aliases (lid camera; LightBurn uses the stream one).
- `GET/POST /settings` — the shared machine settings store
(/data/forgefirm.conf, validated keys, empty-value-clears via query
params; gf_password write-only).
- `GET /cam/stream?cam=lid|head` — multipart MJPEG at 1296×972 (2×2 - `GET /cam/stream?cam=lid|head` — multipart MJPEG at 1296×972 (2×2
Bayer-superpixel demosaic, JPEG q75; `FORGECTRL_STREAM_Q` overrides; Bayer-superpixel demosaic, JPEG q75; `FORGECTRL_STREAM_Q` overrides;
`FORGECTRL_STREAM_FPS` caps the frame rate, unset/0 = sensor max). `FORGECTRL_STREAM_FPS` caps the frame rate, unset/0 = sensor max).
@@ -1,6 +1,8 @@
# gfhome - Glowforge web-service homing configuration. # gfhome - Glowforge web-service homing configuration.
# Copied to /data/etc/gfhome.conf on first run; edit the copy. # Copied to /data/etc/gfhome.conf on first run; edit the copy.
# Machine identity (serial/password) is read from the OCOTP fuses. # Machine identity (serial/password) is read from the OCOTP fuses;
# gf_serial / gf_password / gf_hostname in /data/forgefirm.conf (the
# forgectrl UI, GF Cloud tab) override it.
[SERVICE] [SERVICE]
server_url: https://app.glowforge.com server_url: https://app.glowforge.com
@@ -12,7 +12,10 @@ back-left home corner, Z at the top-of-travel hall trigger.
The grblHAL-glowforge controller invokes this for $H when The grblHAL-glowforge controller invokes this for $H when
homing_mode = gfcloud is set in /data/forgefirm.conf, releasing homing_mode = gfcloud is set in /data/forgefirm.conf, releasing
/dev/glowforge for the duration of the run. It can also be run by hand /dev/glowforge for the duration of the run. It can also be run by hand
(with the controller stopped or its homing session active). (with the controller stopped or its homing session active). The same
shared config supplies optional identity overrides (gf_serial /
gf_password / gf_hostname; the fuse identity is the fallback), managed
from the forgectrl UI.
The service ends the sequence silently - there is no completion The service ends the sequence silently - there is no completion
message - so the run is considered homed once a hunt and at least one message - so the run is considered homed once a hunt and at least one
@@ -29,6 +32,7 @@ SPDX-License-Identifier: MIT
import argparse import argparse
import json import json
import logging import logging
import os
import queue import queue
import shutil import shutil
import signal import signal
@@ -37,12 +41,13 @@ import time
from pathlib import Path from pathlib import Path
from queue import Queue from queue import Queue
from gfutilities.configuration import parse, get_cfg, log_level from gfutilities.configuration import parse, get_cfg, log_level, set_cfg
from gfutilities.service.authentication import authenticate_machine from gfutilities.service.authentication import authenticate_machine
from gfutilities.service.websocket import get_session, WsClient from gfutilities.service.websocket import get_session, WsClient
CONF = '/data/etc/gfhome.conf' CONF = '/data/etc/gfhome.conf'
CONF_SAMPLE = '/etc/gfhome.conf.sample' CONF_SAMPLE = '/etc/gfhome.conf.sample'
MACHINE_CONF = os.environ.get('GFHOME_CONF', '/data/forgefirm.conf')
logging.basicConfig(format='(%(levelname)s) %(module)s:%(funcName)s %(message)s') logging.basicConfig(format='(%(levelname)s) %(module)s:%(funcName)s %(message)s')
logger = logging.getLogger('openglow') logger = logging.getLogger('openglow')
@@ -70,6 +75,30 @@ def load_config(path: str) -> bool:
return True return True
def apply_identity_overrides():
"""Identity overrides from the shared machine config (set in the
forgectrl UI): non-empty gf_serial / gf_password / gf_hostname beat
the OCOTP fuse identity - Machine.__init__ sets its fuse values
with keep_value, so whatever is in the config store first wins."""
keys = {}
try:
with open(MACHINE_CONF) as f:
for line in f:
line = line.strip()
if not line or line.startswith('#') or '=' not in line:
continue
k, v = line.split('=', 1)
keys[k.strip()] = v.strip()
except OSError:
return
for key, cfg in (('gf_serial', 'MACHINE.SERIAL'),
('gf_password', 'MACHINE.PASSWORD'),
('gf_hostname', 'MACHINE.HOSTNAME')):
if keys.get(key):
set_cfg(cfg, keys[key])
logger.info('identity override: %s from %s', cfg, MACHINE_CONF)
def make_machine(): def make_machine():
"""Build the hardware Machine with captures routed through forgectrl. """Build the hardware Machine with captures routed through forgectrl.
@@ -252,10 +281,17 @@ def home(machine, args) -> int:
def main() -> int: def main() -> int:
try:
# The controller exports its own $H budget minus a margin, so
# the runner always gives up before the controller kills it.
timeout_default = max(30, int(os.environ.get('GFHOME_TIMEOUT_S', 240)))
except ValueError:
timeout_default = 240
ap = argparse.ArgumentParser(description='ForgeFIRM one-shot Glowforge cloud homing') ap = argparse.ArgumentParser(description='ForgeFIRM one-shot Glowforge cloud homing')
ap.add_argument('-c', '--config', default=CONF, help='config file (default %s)' % CONF) ap.add_argument('-c', '--config', default=CONF, help='config file (default %s)' % CONF)
ap.add_argument('--timeout', type=int, default=240, ap.add_argument('--timeout', type=int, default=timeout_default,
help='overall time budget in seconds (default 240)') help='overall time budget in seconds (default %d)' % timeout_default)
ap.add_argument('--start-timeout', type=int, default=120, ap.add_argument('--start-timeout', type=int, default=120,
help='max seconds to wait for the service to begin homing (default 120)') help='max seconds to wait for the service to begin homing (default 120)')
ap.add_argument('--quiet', type=int, default=10, ap.add_argument('--quiet', type=int, default=10,
@@ -267,6 +303,8 @@ def main() -> int:
if not load_config(args.config): if not load_config(args.config):
return 1 return 1
apply_identity_overrides()
# Machine() reads the OCOTP identity and head info; it fails cleanly # Machine() reads the OCOTP identity and head info; it fails cleanly
# when the controller still owns /dev/glowforge. # when the controller still owns /dev/glowforge.
try: try: