diff --git a/docs/BRINGUP.md b/docs/BRINGUP.md index c3d8b07..afd982f 100644 --- a/docs/BRINGUP.md +++ b/docs/BRINGUP.md @@ -215,13 +215,30 @@ cameras as MJPEG over the mainline imx-media pipeline: GF Cloud / GRBL / Diagnostics; ui.c): status page with the controller-mode selector (GRBL active; factory cloud disabled until implemented), the operational dashboard, a scaled lid snapshot + - on-demand live stream, and the settings forms for homing method, - home-position calibration, the nine cooling tunables, identity - overrides, and the session timeout. All + on-demand live stream, and the settings forms for display units, + homing method, home-position calibration, the nine cooling + tunables, identity overrides, and the session timeout. All settings controls disable (with a banner) while the machine is not idle **or a diagnostic is running**. `/?action=stream|snapshot` remain the mjpg-streamer- compatible aliases (lid camera; LightBurn uses the stream one). + **Panel conventions (2026-08-08, operator-directed):** the header + identifies the machine by its **fuse identity** — the factory + hostname derived from the OCOTP serial (HW_OCOTP_MAC0 base-23 over + `BCDFGHJKMQRTVWXY2346789`, XXX-YYY; the C implementation matches + gfhardware id.py over 200k random serials) — regardless of any + cloud identity override; the `gf_hostname` override is REMOVED + (the service hostname always derives from whichever serial is in + effect — gfhome.py re-derives it from an overridden gf_serial); + **units** are a display-only preference (`ui_units` metric | + imperial): the backend stores metric, lengths convert mm↔in, + absolute temps °C↔°F, temperature DELTAS (the flow-rise family) + scale by 1.8 with no offset, and saves post **only fields whose + display string changed** (dirty tracking — unit round-trips never + masquerade as edits); **position always shows**, counters-only and + painted red while unreferenced (the machine moves fine unhomed — + relative to wherever it started), normal once anchored; sender + hints are unopinionated (no named Grbl clients). - `GET/POST /settings` — the shared machine settings store (/data/forgefirm.conf, validated keys incl. controller_mode and the cool_* cooling tunables, @@ -413,6 +430,21 @@ browser-verified: the click wrote `cool_flow_rise = 14.8` to the conf (cleared after; the compiled default stands until the operator chooses otherwise). +**Units/identity/position panel rework (2026-08-08, later): +OFFLINE-VERIFIED ONLY — board deploy + bump HELD during the +operator's firmware-upgrade bench testing.** Verified against the +`tools/mock.py` harness in forgectrl (serves the ui.c panel with +mock endpoints; POSTs logged): fuse-identity header (XXX-XXX), red +unreferenced position (needed the `.kv>span:first-child` selector +fix — the old descendant selector out-specified `.b-bad` on nested +value spans), imperial placeholders 14.4→25.9 (delta) / 33→91.4 +(absolute), position 12.34 mm→0.486 in, dirty-save posting exactly +one changed key converted back (27 °F→15 °C), diag bands ×1.8 with +Apply still posting metric, and a units round-trip leaving nothing +dirty. The C serial→hostname derivation matches gfhardware id.py on +200k random 32-bit serials (host-side cross-check). Pending on +go-ahead: deploy both binaries, live re-check, then SRCREV bumps. + ## Hardware facts bank (measured) - SDMA pulse engine: ring size = the `ring_mb` module parameter diff --git a/meta-forgefirm/recipes-forgefirm/gfhome/files/gfhome.conf.sample b/meta-forgefirm/recipes-forgefirm/gfhome/files/gfhome.conf.sample index 214de54..13ad37c 100644 --- a/meta-forgefirm/recipes-forgefirm/gfhome/files/gfhome.conf.sample +++ b/meta-forgefirm/recipes-forgefirm/gfhome/files/gfhome.conf.sample @@ -1,8 +1,9 @@ # 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; -# gf_serial / gf_password / gf_hostname in /data/forgefirm.conf (the -# forgectrl UI, GF Cloud tab) override it. +# gf_serial / gf_password in /data/forgefirm.conf (the forgectrl UI, +# GF Cloud tab) override it. The service hostname always derives from +# whichever serial is in effect. [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 90ec0fd..463fead 100644 --- a/meta-forgefirm/recipes-forgefirm/gfhome/files/gfhome.py +++ b/meta-forgefirm/recipes-forgefirm/gfhome/files/gfhome.py @@ -14,8 +14,9 @@ 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). 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. +gf_password; the fuse identity is the fallback), managed from the +forgectrl UI. The service hostname is always derived from whichever +serial is in effect - it is never set independently. The service ends the sequence silently - there is no completion message - so the run is considered homed once a hunt and at least one @@ -75,11 +76,25 @@ def load_config(path: str) -> bool: return True +def _hostname_for(serial): + """The factory serial -> hostname encoding (base 23 over the + consonant alphabet, up to six characters, split XXX-YYY) - the + same derivation gfhardware applies to the fuse serial.""" + enc = '' + serial = int(serial) + while serial > 0 and len(enc) < 6: + enc = 'BCDFGHJKMQRTVWXY2346789'[serial % 23] + enc + serial //= 23 + return '{}-{}'.format(enc[:3], enc[3:]) + + 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.""" + forgectrl UI): non-empty gf_serial / gf_password beat the OCOTP + fuse identity - Machine.__init__ sets its fuse values with + keep_value, so whatever is in the config store first wins. The + hostname is never overridden independently: it derives from the + serial, so a serial override re-derives it.""" keys = {} try: with open(MACHINE_CONF) as f: @@ -92,11 +107,18 @@ def apply_identity_overrides(): except OSError: return for key, cfg in (('gf_serial', 'MACHINE.SERIAL'), - ('gf_password', 'MACHINE.PASSWORD'), - ('gf_hostname', 'MACHINE.HOSTNAME')): + ('gf_password', 'MACHINE.PASSWORD')): if keys.get(key): set_cfg(cfg, keys[key]) logger.info('identity override: %s from %s', cfg, MACHINE_CONF) + if keys.get('gf_serial'): + try: + set_cfg('MACHINE.HOSTNAME', _hostname_for(keys['gf_serial'])) + logger.info('identity override: MACHINE.HOSTNAME derived ' + 'from gf_serial') + except ValueError: + logger.warning('gf_serial is not numeric; hostname left ' + 'at the fuse derivation') def make_machine():