From 8cd98de605fee66547f00b5a17a86c13ff402453 Mon Sep 17 00:00:00 2001 From: ScottW514 Date: Fri, 7 Aug 2026 20:05:49 -0400 Subject: [PATCH] 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. --- docs/BRINGUP.md | 33 +++++++++---- .../gfhome/files/gfhome.conf.sample | 4 +- .../recipes-forgefirm/gfhome/files/gfhome.py | 46 +++++++++++++++++-- 3 files changed, 68 insertions(+), 15 deletions(-) diff --git a/docs/BRINGUP.md b/docs/BRINGUP.md index 6d15714..e837b45 100644 --- a/docs/BRINGUP.md +++ b/docs/BRINGUP.md @@ -1,13 +1,19 @@ # ForgeFIRM bring-up status & cold-start runbook -Last updated: **2026-08-07** — homing is runtime-selectable -(forgectrl web UI) and **Glowforge web-service (gfcloud) homing is -LIVE-VERIFIED end-to-end** ($H → cloud homing sequence → homed at the -factory corner in 65 s). The first live run surfaced and fixed four -platform bugs — see Next work #3, incl. the new hardware fact that -the estop sense reads low during any motion. Same day: fd-blocking -protocol pacing (idle CPU ~2%) and the fortify step_us_min fix. -Previous milestone: camera service (forgectrl MJPEG on :8080). +Last updated: **2026-08-07 (late)** — forgectrl is now the machine +CONTROL PANEL: tabbed web UI (Status / Machine / GF Cloud / GRBL) with +a status-first landing page (scaled lid snapshot + on-demand live +stream), homing-position calibration (`gfcloud_home_x/y/z`), +cloud-identity overrides (`gf_serial`/`gf_password`/`gf_hostname`, +fuses = fallback, applied by the runner via set_cfg before Machine()), +and a validated multi-key `/settings` API (empty value = clear; clears +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 this repo; per-finding status of the 2026-07-03 audit) and `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 cameras as MJPEG over the mainline imx-media pipeline: -- `GET /` — index page with a live view; `/?action=stream|snapshot` are - the mjpg-streamer-compatible aliases (lid camera). +- `GET /` — the tabbed machine control panel (Status / Machine / + 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 Bayer-superpixel demosaic, JPEG q75; `FORGECTRL_STREAM_Q` overrides; `FORGECTRL_STREAM_FPS` caps the frame rate, unset/0 = sensor max). diff --git a/meta-forgefirm/recipes-forgefirm/gfhome/files/gfhome.conf.sample b/meta-forgefirm/recipes-forgefirm/gfhome/files/gfhome.conf.sample index 1b37072..214de54 100644 --- a/meta-forgefirm/recipes-forgefirm/gfhome/files/gfhome.conf.sample +++ b/meta-forgefirm/recipes-forgefirm/gfhome/files/gfhome.conf.sample @@ -1,6 +1,8 @@ # gfhome - Glowforge web-service homing configuration. # 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] server_url: https://app.glowforge.com diff --git a/meta-forgefirm/recipes-forgefirm/gfhome/files/gfhome.py b/meta-forgefirm/recipes-forgefirm/gfhome/files/gfhome.py index ede613c..90ec0fd 100644 --- a/meta-forgefirm/recipes-forgefirm/gfhome/files/gfhome.py +++ b/meta-forgefirm/recipes-forgefirm/gfhome/files/gfhome.py @@ -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 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 -(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 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 json import logging +import os import queue import shutil import signal @@ -37,12 +41,13 @@ import time from pathlib import Path 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.websocket import get_session, WsClient CONF = '/data/etc/gfhome.conf' 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') logger = logging.getLogger('openglow') @@ -70,6 +75,30 @@ def load_config(path: str) -> bool: 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(): """Build the hardware Machine with captures routed through forgectrl. @@ -252,10 +281,17 @@ def home(machine, args) -> 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.add_argument('-c', '--config', default=CONF, help='config file (default %s)' % CONF) - ap.add_argument('--timeout', type=int, default=240, - help='overall time budget in seconds (default 240)') + ap.add_argument('--timeout', type=int, default=timeout_default, + help='overall time budget in seconds (default %d)' % timeout_default) ap.add_argument('--start-timeout', type=int, default=120, help='max seconds to wait for the service to begin homing (default 120)') ap.add_argument('--quiet', type=int, default=10, @@ -267,6 +303,8 @@ def main() -> int: if not load_config(args.config): return 1 + apply_identity_overrides() + # Machine() reads the OCOTP identity and head info; it fails cleanly # when the controller still owns /dev/glowforge. try: