Files
forgefirm/scripts/bench/gfbench.py
T
ScottW514 64301d3221 Keep the configuration files inside /data/forgefirm; pin the three components
ForgeFIRM's own files live under /data/forgefirm; two configuration
files did not. The machine settings sat at /data/forgefirm.conf, in the
root of /data beside the factory's own files, and the cloud-mode
configuration sat at /data/etc/gfhome.conf, inside a directory the
factory owns. Both move:

  /data/forgefirm.conf   -> /data/forgefirm/forgefirm.conf
  /data/etc/gfhome.conf  -> /data/forgefirm/gfhome.conf

There is no migration: only the bench has ever run this firmware.
/data/etc now holds only the factory's wpa_supplicant.conf.

The acceptance check of the file modes reads the settings file at its
new path, and the two bench tools that read it directly follow. The
pins move to the revisions that carry the change, forgectrl also
bringing the fix that reads the module's disabled state as idle:

  forgectrl           468ee21 (0.1.12)
  grblhal-glowforge   9ee624b (0.1.10)
  forgefirm-app       56f134a (0.1.28+git)

The lock moves meta-openglow to b7ad6d9, which pins python3-gfhardware
on the same revision. The four upstream layers stay where they were:
`kas lock --update` moves every floating repository, and a release is
not the place to take poky, meta-openembedded and meta-freescale along
for the ride.
2026-09-09 15:32:54 -04:00

175 lines
5.6 KiB
Python

#!/usr/bin/env python3
"""Shared helpers for the bench tools that can run either ON the board or
from a LAN host.
from gfbench import HOST, LOCAL, board, degc, data_path
HOST / LOCAL the machine address and whether this process runs on it.
GF_HOST names a remote machine (host mode: sysfs through
ssh, Grbl and forgectrl over the LAN). Unset, the tool
runs on the board itself when /sys/glowforge exists (local
mode: sysfs directly, Grbl and forgectrl on 127.0.0.1) -
the acceptance tool's bench page runs them that way.
board(cmd) run a shell command on the machine and return its stdout
(local: sh -c; host: ssh, or the client named by GF_SSH,
e.g. GF_SSH='wsl -d <distro> -- ssh').
degc(raw) the factory B-equation coolant conversion
(docs.forgefirm.org, the sensors page).
data_path(f) where a tool keeps its data files: FORGETEST_BENCH_DATA
when set (the bench page passes <data>/bench/), else next
to the tool.
forgectrl_* the machine-services HTTP API (:80) with the panel token
from GF_TOKEN or, on the board, /data/forgefirm/panel.token.
"""
import json
import math
import os
import shlex
import subprocess
import urllib.error
import urllib.parse
import urllib.request
HERE = os.path.dirname(os.path.abspath(__file__))
_LOCAL_HOSTS = ("127.0.0.1", "localhost", "::1")
def _resolve():
host = os.environ.get("GF_HOST")
if host:
return host, host in _LOCAL_HOSTS
if os.path.isdir("/sys/glowforge"):
return "127.0.0.1", True
raise SystemExit("set GF_HOST to the machine IP address (or run this on the board)")
HOST, LOCAL = _resolve()
SSH = shlex.split(os.environ.get("GF_SSH", "ssh"))
def board(cmd, timeout=30):
"""stdout of a shell command run on the machine (not stripped)."""
if LOCAL:
argv = ["sh", "-c", cmd]
else:
argv = SSH + ["-o", "PreferredAuthentications=none", "root@" + HOST, cmd]
r = subprocess.run(argv, capture_output=True, text=True, timeout=timeout)
return r.stdout
# Factory B-equation conversion: 10k B3380 NTC in a 10k divider behind a
# 1.3x gain stage, 10-bit ADC.
F = 1024.0 * 1.3
RD, BETA = 10000.0, 3380.0
RINF = 10000.0 * math.exp(-3380.0 / 298.15)
def degc(raw):
try:
raw = float(raw)
except (TypeError, ValueError):
return float("nan")
if raw <= 1.0 or raw >= F:
return float("nan")
r = RD / (F / raw - 1.0)
return BETA / math.log(r / RINF) - 273.15
def data_dir():
d = os.environ.get("FORGETEST_BENCH_DATA")
if d:
os.makedirs(d, exist_ok=True)
return d
return HERE
def data_path(name):
return os.path.join(data_dir(), name)
# ------------------------------------------------------------- forgectrl
def forgectrl_base():
return os.environ.get("FORGECTRL_URL") or "http://%s:%s" % (HOST, os.environ.get("FORGECTRL_PORT") or "80")
def token():
tok = os.environ.get("GF_TOKEN")
if tok:
return tok
if LOCAL:
try:
with open("/data/forgefirm/panel.token", "r", encoding="utf-8") as f:
return f.read().strip()
except OSError:
pass
return ""
def forgectrl_request(method, path, params=None, data=None, timeout=8.0):
"""(status, body): body is parsed JSON when the response is JSON, else
text. Raises OSError-derived errors when the daemon is unreachable."""
url = forgectrl_base() + path
if params:
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(params)
body = None
hdrs = {"Host": urllib.parse.urlsplit(forgectrl_base()).netloc}
if data is not None:
body = urllib.parse.urlencode(data).encode()
hdrs["Content-Type"] = "application/x-www-form-urlencoded"
tok = token()
if tok:
hdrs["X-ForgeFIRM-Token"] = tok
req = urllib.request.Request(url, data=body, method=method, headers=hdrs)
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
status, content, ctype = resp.status, resp.read(), resp.headers.get("Content-Type", "")
except urllib.error.HTTPError as e:
status, content = e.code, e.read()
ctype = e.headers.get("Content-Type", "") if e.headers else ""
text = content.decode("utf-8", "replace")
if "json" in ctype:
try:
return status, json.loads(text)
except ValueError:
pass
return status, text
def forgectrl_get(path, **kw):
return forgectrl_request("GET", path, **kw)
def forgectrl_post(path, **kw):
return forgectrl_request("POST", path, **kw)
SETTINGS_FILE = "/data/forgefirm/forgefirm.conf"
def setting(key, default=None):
"""One shared machine setting (forgectrl's /settings; on the board with
forgectrl stopped - a takeover - the settings file itself), or default
when unset or unreadable."""
try:
st, body = forgectrl_get("/settings")
if st == 200 and isinstance(body, dict):
val = body.get(key)
return default if val in (None, "") else val
except OSError:
pass
if LOCAL:
try:
with open(SETTINGS_FILE, "r", encoding="utf-8") 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)
if k.strip() == key:
v = v.strip()
return default if v == "" else v
except OSError:
pass
return default