mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-27 16:51:12 -07:00
commissioning: the layer, the acceptance tests, the harness rule, the docs, and the bench drills
meta-forgefirm: the forgefirm-users init replays the account at boot; sshd refuses root and empty passwords and runs only while the panel turns it on; the release image keeps an empty root password for the console; the console banner; avahi announces forgefirm.local; https in libmicrohttpd and ulfius; the panel on 80 and 443; the license bundle on the rootfs; release.sh checks the root policy on the built rootfs. forgetest: the commission suites (commission, commission_dark, commission_sheet: 23 cases); the runner turns cloud mode on with the typed phrase for a test that declares it; the baseline's motor_lock is 0; the log-export test checks the bundle for the camera key; the record helpers write bytes as given and join the daemon's paths as POSIX. The stream harness gains rule 24: a hold verdict is held again after a resume. Bench drills: lens_travel.py and lens_stop_accel.py. Docs: BRINGUP carries the present state; CAMPAIGN-LOG carries the dated record.
This commit is contained in:
@@ -35,6 +35,8 @@ page's takeover does that; from a host, stop them first.
|
||||
| `live_fire_drills.py` | **LIVE LASER** drills, on the board (the bench page) or from a LAN host (`GF_HOST`): `live_fire_drills.py <drill> [S] [F]` - `witness` (emission witness, lid-IR peaks vs the ambient baseline, HV current, job-based disarm on M2), `hold` (disarm grace in Hold), `faultpos` (armed job refuses a stale origin after an underrun), `ircut` (lid-IR characterization cut at S/F), `pthresh` (laser power-threshold ladder: 13 constant-power rungs from 2 % to 30 % of full on scrap; the lowest rung that marks is the tube's striking threshold and reads directly as the `$35` value - requires `$35` = 0 for the run), `dladder` (density ladder at a chosen base period), `pcurve` (laser performance-curve ladder: one 100 mm line per level at 10 mm/s under M3, the laser off between rungs and a mid-ladder rung repeated at the end; reads `pic/hv_current` and the head thermopile `head/beam_detect_analog` (a scatter detector in the beam path upstream of the final mirror, so it sees the beam, not the material) from sysfs at ~25 Hz on the board, brackets each rung on the controller's Run/Idle states, and reports per rung the current with a clipped-at-1023 flag, the thermopile delta over its laser-off baseline and in-line drift, then the normalized curve, monotonicity, a line fit with its threshold intercept and the repeat-rung drift; JSON record with the raw trace in the bench data directory; rungs follow `laser_power_model`, a comma list overrides; a curve measurement wants `$35` = 0), `dpatch [F] [pitch] [length]` (depth witness for the density dose curve: two rows of small serpentine-filled patches, row A CW at feeds giving relative doses 1.0 to 0.25 of the reference feed, row B at the reference feed at 100/80/60/45/30 % density; the operator matches each row-B patch to the row-A patch of equal depth, which reads the density's light fraction off the material beside the thermopile's prediction; JSON record), `m4feeds [S] [F1] [F2]` (the density time base across feeds: one out-and-back line pair per feed at the same S under M4 density, one armed run; the operator reads within-line evenness and reversal darkness at both feeds - M4's velocity scaling is what holds dose per mm through the accel), `m4corner [S] [F]` (M4 velocity-scaled power into corners: a corner-heavy vector pattern at 30 % under M4 density, one armed run; the operator confirms every commanded segment marks - the floor makes a dropout unreachable - and the drill asserts the arm report, one discharge window and dark after), `m5dark` (the rapids after an M5 ship dark: one 20 mm line at M3 S400, M5, dwell, rapid back, dwell, rapid forward; PASS when the 25 Hz current trace shows one discharge segment and reads dark after the M5 and `laser_on_sampled` never re-lights; the catalog's `laser.m5-rapid-dark` is its port), `flowload` (cooling under laser load, one armed run per invocation, the conf keys it writes put back at the end, the pump never commanded off: `t1` reproduces the flow-check trip with the check on at its defaults and two 30 x 4 mm CW fills at F1500 starting on the press with no dark dwell, and reports the engine's rise/dT verdict beside the 25 Hz trace of both coolant sensors, the current, the digital witness and the heater output in 5 s bins across the window, with the shape at fire start; `t2 <secs> [pct]` runs with the check off and one fill of about `secs` lit seconds at CW or at `pct` density, and reports the lag to each sensor, the rise per raw-second of `hv_current` and what a full 50 s window would add against the 1.6 C margin; `fit` fits rise against dose over every t2 record; JSON records), `expstop` (armed kill on the expected-stop path; needs the panel token - `GF_TOKEN`, or the board's token file) and `ctrlstart` (the separate controller restart after it). Every drill waits for the operator's physical arm press; eye protection, fire watch, extinguisher, and exhaust are mandatory. |
|
||||
| `pacing_test.py` | Protocol-loop pacing check (runs on the board, dry motion): idle and parked-in-Hold states are coarse-paced, active motion is tight-paced, and a feed-hold/resume mid-move preserves position with no feeder starve. |
|
||||
| `gfbench.py` | Not a tool: the helper the board/host tools share - `HOST`/`LOCAL` from `GF_HOST`, `board(cmd)` (local `sh -c` or ssh), the factory coolant conversion `degc()`, `data_path()` (`FORGETEST_BENCH_DATA` or next to the tool), forgectrl's HTTP API with the panel token, `setting(key)` (from forgectrl, or from `/data/forgefirm.conf` on the board while forgectrl is stopped). |
|
||||
| `lens_travel.py` | Lens travel drill (runs on the board; stops the controller through forgectrl and restarts it; lens motion only): steps the lens over sysfs the way the focus card's lens home does and counts, per condition and round, the hall's hysteresis band (down off the rising edge until the hall leaves home, back up until it reads home), the rising edge's height above the bottom stop after a drive onto it, and the top stop's height above the edge (a drive up, down until the hall leaves home, back up; the difference). Conditions: the card's half-step drive onto the stop, a short drive, a long one, the factory's full-step home mode; `--ladder` drives a list of half-step descents below the leave-home point and counts each back (exact until the stop, short by an even number after a stall), `--current` picks the run current, the hold current, or hold down / run up, `--settle` and `--cadence` vary the timing; `--park bottom|top|edge-N|edge+N` holds the lens there for a depth-gauge reading with the controller in standby, `--park edge` returns it to the rising edge and restarts the controller. Stall drills on the bench reference machine only. The posture found is put back. |
|
||||
| `lens_stop_accel.py` | Lens stop detection by the head accelerometer (runs on the board; stops the controller through forgectrl and restarts it; lens motion only; stall drills on the bench reference machine only): reads the LIS2HH12 straight over `/dev/i2c-3` in six-byte bursts at 800 Hz (the iio path waits a sample period per read), steps the lens one half-step at a time from the hall's rising edge toward each stop and past it, and prints per step the peak-to-peak on each axis. A free step rings strongly on every second half-step; at a stop the ring dies, and a rotor slip a few steps later is a burst three times any free ring. `--find N` runs the contact rule N times per stop (a strong-parity step ringing under `--thresh`, or a burst over four times it, calls contact; the lens backs off `--back` and the count home proves no slip). `--save` keeps the per-step sample traces as JSON. |
|
||||
| `fan_test.py` | Fan/coolant bench (board or host; controller running): snapshots fan PWMs/tachs/temps, drives M8 → cut fans, M9 → cooldown → idle, verifying via tach readbacks. |
|
||||
| `fan_floor_measure.py` | The numbers the airflow gates ship with (board or host): `spinup` opens a run session with M8 from idle and samples the four tachs and the purge current at 1 Hz, reporting per fan the steady speed, the time to 90 percent and the spread over the steady window, plus the purge current at idle and at run duty (the pump is always on; a dead one reads about 1), and candidate floors at 55 percent; `cut` samples only, during a real cut, for the spread under load. Results as JSON in the bench data directory. |
|
||||
| `flow_characterize.py` | Coolant flow characterization using the factory temperature curve (board or host; forgectrl and controller stopped): baseline → flow → no-flow → recovery, printing the ΔT bands and their separation. Takes the heater duty as an argument (`flow_characterize.py 30`); aborts if downstream passes 45 °C. |
|
||||
|
||||
@@ -16,7 +16,7 @@ import sys
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
from live_fire_drills import Grbl, HOST, PORT
|
||||
from live_fire_drills import Grbl, HOST, PORT, BASE
|
||||
|
||||
|
||||
def raw():
|
||||
@@ -28,11 +28,11 @@ def raw():
|
||||
|
||||
|
||||
def status():
|
||||
with urllib.request.urlopen('http://127.0.0.1:8080/status', timeout=2) as r:
|
||||
with urllib.request.urlopen(BASE + '/status', timeout=2) as r:
|
||||
s = json.load(r)
|
||||
with urllib.request.urlopen('http://127.0.0.1:8080/cool/status', timeout=2) as r:
|
||||
with urllib.request.urlopen(BASE + '/cool/status', timeout=2) as r:
|
||||
c = json.load(r)
|
||||
with urllib.request.urlopen('http://127.0.0.1:8080/settings', timeout=2) as r:
|
||||
with urllib.request.urlopen(BASE + '/settings', timeout=2) as r:
|
||||
st = json.load(r)
|
||||
return (s['coolant']['down_c'], s['coolant']['up_c'], c['down_c'], c['up_c'], c['phase'],
|
||||
st.get('cool_aa_offset_counts'))
|
||||
@@ -58,7 +58,7 @@ def post_settings(**kv):
|
||||
tok = f.read().strip()
|
||||
except OSError:
|
||||
tok = ''
|
||||
req = urllib.request.Request('http://127.0.0.1:8080/settings',
|
||||
req = urllib.request.Request(BASE + '/settings',
|
||||
data=urllib.parse.urlencode(kv).encode(),
|
||||
headers={'X-ForgeFIRM-Token': tok})
|
||||
with urllib.request.urlopen(req, timeout=4) as r:
|
||||
@@ -76,7 +76,7 @@ def main():
|
||||
# A run session starts the flow check, whose heater warms the
|
||||
# downstream sensor inside this check's dwell: off for this session,
|
||||
# back afterward.
|
||||
with urllib.request.urlopen('http://127.0.0.1:8080/settings', timeout=2) as r:
|
||||
with urllib.request.urlopen(BASE + '/settings', timeout=2) as r:
|
||||
was = json.load(r).get('cool_flow_check_s')
|
||||
if was in (None, ''):
|
||||
was = '50' # unset reads as empty: the shipped default
|
||||
@@ -95,7 +95,7 @@ def main():
|
||||
post_settings(cool_flow_check_s=str(was))
|
||||
else:
|
||||
post_settings(cool_flow_check_s='50')
|
||||
with urllib.request.urlopen('http://127.0.0.1:8080/settings', timeout=2) as r:
|
||||
with urllib.request.urlopen(BASE + '/settings', timeout=2) as r:
|
||||
print('flow check restored: cool_flow_check_s = %s' % json.load(r).get('cool_flow_check_s'))
|
||||
time.sleep(25.0) # the session closes, the fans idle
|
||||
r2, s2 = mean_over(8.0)
|
||||
|
||||
@@ -15,5 +15,6 @@ TC="$FF_BUILD_TOP/tmp/work/cortexa9t2hf-neon-fslc-linux-gnueabi/ulfius/2.7.15"
|
||||
"$TC/recipe-sysroot-native/usr/bin/arm-fslc-linux-gnueabi/arm-fslc-linux-gnueabi-gcc" \
|
||||
--sysroot="$TC/recipe-sysroot" \
|
||||
-mthumb -mfpu=neon -mfloat-abi=hard -mcpu=cortex-a9 \
|
||||
-D_TIME_BITS=64 -D_FILE_OFFSET_BITS=64 \
|
||||
-O2 -Wall -Wextra -o "$SP/feeder" "$SP/feeder.c"
|
||||
echo FEEDER-OK
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#!/bin/bash
|
||||
# Cross-compiles forgectrl (the sibling repo) for the factory board,
|
||||
# borrowing the Yocto cross toolchain + sysroot from the forgectrl
|
||||
# recipe work directory (which carries ulfius and libjpeg). If that
|
||||
# path ages out after a clean, regenerate it with: bitbake forgectrl.
|
||||
# recipe work directory (which carries ulfius, jansson, gnutls,
|
||||
# libxcrypt and libjpeg). The newest version directory under the
|
||||
# recipe's work tree is taken. If that path ages out after a clean,
|
||||
# regenerate it with: bitbake forgectrl.
|
||||
#
|
||||
# Environment (defaults derive from this script's location, assuming
|
||||
# the standard multi-repo checkout layout):
|
||||
@@ -12,8 +14,11 @@ set -e
|
||||
SP="$(cd "$(dirname "$0")" && pwd)"
|
||||
FF_SRC_TOP="${FF_SRC_TOP:-$(cd "$SP/../../.." && pwd)}"
|
||||
FF_BUILD_TOP="${FF_BUILD_TOP:-$FF_SRC_TOP/forgefirm/build}"
|
||||
TC="$FF_BUILD_TOP/tmp/work/cortexa9t2hf-neon-fslc-linux-gnueabi/forgectrl/0.1.0"
|
||||
[ -d "$TC/recipe-sysroot" ] || { echo "toolchain not staged at $TC (run: bitbake forgectrl)"; exit 1; }
|
||||
TCROOT="$FF_BUILD_TOP/tmp/work/cortexa9t2hf-neon-fslc-linux-gnueabi/forgectrl"
|
||||
TC=$(ls -d "$TCROOT"/*/ 2>/dev/null | sort -V | tail -n 1)
|
||||
TC="${TC%/}"
|
||||
[ -n "$TC" ] && [ -d "$TC/recipe-sysroot" ] || { echo "toolchain not staged under $TCROOT (run: bitbake forgectrl)"; exit 1; }
|
||||
echo "toolchain: $TC"
|
||||
export PATH="$TC/recipe-sysroot-native/usr/bin:$TC/recipe-sysroot-native/usr/bin/arm-fslc-linux-gnueabi:$PATH"
|
||||
LOG=$(mktemp -t fcbuild.XXXXXX)
|
||||
cd "$FF_SRC_TOP/forgectrl"
|
||||
@@ -22,7 +27,7 @@ cmake -B build-arm \
|
||||
-DCMAKE_SYSTEM_NAME=Linux -DCMAKE_SYSTEM_PROCESSOR=arm \
|
||||
-DCMAKE_C_COMPILER=arm-fslc-linux-gnueabi-gcc \
|
||||
-DCMAKE_BUILD_TYPE=None \
|
||||
"-DCMAKE_C_FLAGS=--sysroot=$TC/recipe-sysroot -mthumb -mfpu=neon -mfloat-abi=hard -mcpu=cortex-a9 -O2 -g" \
|
||||
"-DCMAKE_C_FLAGS=--sysroot=$TC/recipe-sysroot -mthumb -mfpu=neon -mfloat-abi=hard -mcpu=cortex-a9 -D_TIME_BITS=64 -D_FILE_OFFSET_BITS=64 -O2 -g" \
|
||||
"-DCMAKE_EXE_LINKER_FLAGS=--sysroot=$TC/recipe-sysroot" \
|
||||
> "$LOG" 2>&1
|
||||
cmake --build build-arm -j8 >> "$LOG" 2>&1 || { tail -30 "$LOG"; exit 1; }
|
||||
|
||||
@@ -22,7 +22,7 @@ cmake -B build-arm \
|
||||
-DCMAKE_SYSTEM_NAME=Linux -DCMAKE_SYSTEM_PROCESSOR=arm \
|
||||
-DCMAKE_C_COMPILER=arm-fslc-linux-gnueabi-gcc \
|
||||
-DCMAKE_BUILD_TYPE=None \
|
||||
"-DCMAKE_C_FLAGS=--sysroot=$TC/recipe-sysroot -mthumb -mfpu=neon -mfloat-abi=hard -mcpu=cortex-a9 -O1 -g" \
|
||||
"-DCMAKE_C_FLAGS=--sysroot=$TC/recipe-sysroot -mthumb -mfpu=neon -mfloat-abi=hard -mcpu=cortex-a9 -D_TIME_BITS=64 -D_FILE_OFFSET_BITS=64 -O1 -g" \
|
||||
"-DCMAKE_EXE_LINKER_FLAGS=--sysroot=$TC/recipe-sysroot" \
|
||||
> "$LOG" 2>&1
|
||||
cmake --build build-arm -j8 >> "$LOG" 2>&1 || { tail -30 "$LOG"; exit 1; }
|
||||
|
||||
@@ -10,8 +10,8 @@ P=$(pidof forgectrl)
|
||||
CTRL=$(pidof grblHAL_glowforge)
|
||||
echo "forgectrl pid $P, controller pid ${CTRL:-none}"
|
||||
# Kick the helpers off in the background.
|
||||
( curl -s -X POST -H "X-ForgeFIRM-Token: $TOK" http://127.0.0.1:8080/update/check >/tmp/upd.out 2>&1 ) &
|
||||
( curl -s -o /tmp/snap.jpg http://127.0.0.1:8080/cam/snapshot ) &
|
||||
( curl -s -X POST -H "X-ForgeFIRM-Token: $TOK" http://127.0.0.1:${FORGECTRL_PORT:-80}/update/check >/tmp/upd.out 2>&1 ) &
|
||||
( curl -s -o /tmp/snap.jpg http://127.0.0.1:${FORGECTRL_PORT:-80}/cam/snapshot ) &
|
||||
hits=0; seen=0; names=""
|
||||
i=0
|
||||
while [ $i -lt 60 ]; do # ~6 s of scanning at 10 Hz
|
||||
|
||||
@@ -18,7 +18,7 @@ degc(raw) the factory B-equation coolant conversion
|
||||
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 (:8080) with the panel token
|
||||
forgectrl_* the machine-services HTTP API (:80) with the panel token
|
||||
from GF_TOKEN or, on the board, /data/forgefirm/panel.token.
|
||||
"""
|
||||
import json
|
||||
@@ -90,7 +90,7 @@ def data_path(name):
|
||||
# ------------------------------------------------------------- forgectrl
|
||||
|
||||
def forgectrl_base():
|
||||
return os.environ.get("FORGECTRL_URL") or "http://%s:8080" % HOST
|
||||
return os.environ.get("FORGECTRL_URL") or "http://%s:%s" % (HOST, os.environ.get("FORGECTRL_PORT") or "80")
|
||||
|
||||
|
||||
def token():
|
||||
|
||||
+1076
-1013
File diff suppressed because it is too large
Load Diff
@@ -79,6 +79,12 @@ over TCP, then checks the dumps against the kernel feeder contract:
|
||||
the default gamma of 2 than at gamma 1, and the cruise middle
|
||||
renders the same - the exponent shapes only the velocity-scaled
|
||||
rolloff, never the programmed level
|
||||
24. a hold verdict is held again after a resume: with the engine's
|
||||
verdict at its fail tier (hold, fire blocked, no resume) the client
|
||||
holds the job; a ~ under that verdict, which is what a button press
|
||||
or a sender does, moves the head for at most one client poll, dark,
|
||||
before the client holds it again and says so; the clean verdict then
|
||||
resumes the hold the client took, and the rest of the line cuts lit
|
||||
|
||||
The analog sessions select the reference mode through the config; on
|
||||
hardware the controller ignores it (density is the only product model -
|
||||
@@ -138,6 +144,16 @@ JOB_M4 = [
|
||||
"M5",
|
||||
]
|
||||
|
||||
# Session Z: the lens is in the stream. The sender references the lens
|
||||
# (M103, as a commissioning card does), then a 1 mm move up and back at
|
||||
# the screw's 2.922 half-steps per millimeter: three Z steps with the
|
||||
# direction bit set, three with it clear.
|
||||
JOB_Z = [
|
||||
"M103 Z3 P14 Q20",
|
||||
"G0 Z4",
|
||||
"G0 Z3",
|
||||
]
|
||||
|
||||
# Session B: M3 constant power to the end of the stream. The core never
|
||||
# issues a laser-off update for M3, so the stream engine itself must
|
||||
# terminate the cycle dark (rule 7).
|
||||
@@ -372,17 +388,28 @@ def wait_state(sock, log, prefix, timeout=5.0):
|
||||
fail("controller never reached %s" % prefix)
|
||||
|
||||
|
||||
# The published verdict is clean unless a session sets this: then it is
|
||||
# the engine's fail tier (hold, fire blocked, no resume), what an airflow
|
||||
# fault publishes. A ("verdict", "hold") step sets it, ("verdict",
|
||||
# "clean") clears it.
|
||||
VERDICT_HOLD = threading.Event()
|
||||
|
||||
|
||||
def publish_verdicts(path, stop):
|
||||
"""Publish a fresh, clean cooling verdict every 0.5 s (the arm flow
|
||||
refuses without one; freshness window is 2 s). Same-host monotonic
|
||||
clock, atomic rename so the reader never sees a torn file. "armed"
|
||||
is the engine's acknowledgment that it has taken the controller's
|
||||
armed window; the arm waits for it, so a stand-in engine that means
|
||||
to let jobs run must assert it."""
|
||||
"""Publish a fresh cooling verdict every 0.5 s (the arm flow refuses
|
||||
without one; freshness window is 2 s), clean unless VERDICT_HOLD is
|
||||
set. Same-host monotonic clock, atomic rename so the reader never
|
||||
sees a torn file. "armed" is the engine's acknowledgment that it has
|
||||
taken the controller's armed window; the arm waits for it, so a
|
||||
stand-in engine that means to let jobs run must assert it."""
|
||||
while not stop.is_set():
|
||||
body = ('{"ts_mono":%.3f,"fire_ok":true,"hold":false,'
|
||||
'"resume_ok":true,"armed":true,"reason":""}'
|
||||
% time.clock_gettime(time.CLOCK_MONOTONIC))
|
||||
hold = VERDICT_HOLD.is_set()
|
||||
body = ('{"ts_mono":%.3f,"fire_ok":%s,"hold":%s,'
|
||||
'"resume_ok":%s,"armed":true,"reason":"%s"}'
|
||||
% (time.clock_gettime(time.CLOCK_MONOTONIC),
|
||||
"false" if hold else "true", "true" if hold else "false",
|
||||
"false" if hold else "true",
|
||||
"harness: airflow fault" if hold else ""))
|
||||
tmp = path + ".tmp"
|
||||
with open(tmp, "w") as f:
|
||||
f.write(body)
|
||||
@@ -412,6 +439,7 @@ def run_session(name, steps, conf=None, workdir=None, keep=False,
|
||||
env["GFHOME_CONF"] = conf_path
|
||||
|
||||
stop = threading.Event()
|
||||
VERDICT_HOLD.clear()
|
||||
pub = threading.Thread(target=publish_verdicts, args=(verdict, stop), daemon=True)
|
||||
pub.start()
|
||||
|
||||
@@ -444,6 +472,11 @@ def run_session(name, steps, conf=None, workdir=None, keep=False,
|
||||
sock.sendall(step[1]) # a realtime character: no ok follows
|
||||
elif isinstance(step, tuple) and step[0] == "wait_state":
|
||||
wait_state(sock, log, step[1])
|
||||
elif isinstance(step, tuple) and step[0] == "verdict":
|
||||
if step[1] == "hold":
|
||||
VERDICT_HOLD.set()
|
||||
else:
|
||||
VERDICT_HOLD.clear()
|
||||
else:
|
||||
send_line(sock, step, log)
|
||||
|
||||
@@ -466,6 +499,7 @@ def run_session(name, steps, conf=None, workdir=None, keep=False,
|
||||
proc.kill()
|
||||
stop.set()
|
||||
pub.join(2)
|
||||
VERDICT_HOLD.clear()
|
||||
|
||||
data = open(dump, "rb").read()
|
||||
if not data and arm_required:
|
||||
@@ -497,6 +531,18 @@ def check_fire_gaps(name, data):
|
||||
return worst
|
||||
|
||||
|
||||
def check_z_move(name, data):
|
||||
"""The lens in the stream: a job's Z move steps it, up with the
|
||||
direction bit set, down with it clear, the count the scale gives."""
|
||||
ticks = tick_bytes(data)
|
||||
up = sum(1 for b in ticks if b & 0x20 and b & 0x40)
|
||||
down = sum(1 for b in ticks if b & 0x20 and not b & 0x40)
|
||||
if up != 3 or down != 3:
|
||||
fail("[%s] Z steps up %d, down %d (expected 3 and 3 for 1 mm at "
|
||||
"2.922 half-steps per mm)" % (name, up, down))
|
||||
print("PASS [%s]: a 1 mm Z move steps the lens %d up and %d back" % (name, up, down))
|
||||
|
||||
|
||||
def check_termination(name, data):
|
||||
"""Rule 7: the stream's final tick byte must carry FIRE clear."""
|
||||
ticks = tick_bytes(data)
|
||||
@@ -735,6 +781,10 @@ def main():
|
||||
data = run_session("m4", JOB_M4, conf=ANALOG_CONF)
|
||||
fire_ticks, powers, x_max, tail_steps = check_m4_job(data)
|
||||
check_termination("m4", data)
|
||||
|
||||
# --- session Z: the lens in the stream --------------------------------
|
||||
zdata = run_session("z", JOB_Z, arm_required=False)
|
||||
check_z_move("z", zdata)
|
||||
gap_a = check_fire_gaps("m4", data)
|
||||
print("PASS [m4]: %d bytes, %d power bytes, %d fire ticks, powers %s, "
|
||||
"X peak %d steps net 0, %d dark return steps, max fire gap %d"
|
||||
@@ -1005,6 +1055,57 @@ def main():
|
||||
"(%d ticks), lit from the first step out (%d fire ticks)"
|
||||
% (mode, decel, dlen, accel))
|
||||
|
||||
# --- rule 24: a hold verdict is held again after a resume -----------
|
||||
# One long line at 50 mm/s. Mid-move the engine's verdict goes to its
|
||||
# fail tier (hold, fire blocked, no resume: an airflow fault) and the
|
||||
# client takes the feed hold. A ~ then resumes the job under the
|
||||
# standing verdict, which is what a button press or a sender does;
|
||||
# the client must hold it again within its poll, saying so, and the
|
||||
# stretch it moved in between ships dark. The clean verdict then
|
||||
# resumes the hold the client took, and the rest of the line cuts lit.
|
||||
TICK_HZ = 28160
|
||||
steps = ["G90", "G21", "M3 S500", "G1 X150 F3000", ("sleep", 0.7),
|
||||
("verdict", "hold"), ("wait_state", "Hold:0"), ("sleep", 0.5),
|
||||
("rt", b"~"), ("sleep", 1.2), ("wait_state", "Hold:0"),
|
||||
("sleep", 0.5), ("verdict", "clean"), WAIT_IDLE, "M5"]
|
||||
data = run_session("verdict-rehold", steps, conf=DENSITY_CONF_FLOORED)
|
||||
text = run_session.text
|
||||
if "held again" not in text:
|
||||
fail("[verdict-rehold] the client did not say it held the job again")
|
||||
if "resuming" not in text:
|
||||
fail("[verdict-rehold] the client did not resume its own hold once the verdict cleared")
|
||||
ticks = tick_bytes(data)
|
||||
step = [1 if t & 0x05 else 0 for t in ticks]
|
||||
fire = [1 if t & 0x10 else 0 for t in ticks]
|
||||
first = step.index(1)
|
||||
last = len(step) - 1 - step[::-1].index(1)
|
||||
holds, run = [], 0 # the stationary stretches inside the motion
|
||||
for i in range(first, last + 1):
|
||||
if step[i]:
|
||||
if run >= 2000:
|
||||
holds.append((i - run, i))
|
||||
run = 0
|
||||
else:
|
||||
run += 1
|
||||
if len(holds) != 2:
|
||||
fail("[verdict-rehold] expected two holds in the stream, found %d: %s"
|
||||
% (len(holds), holds))
|
||||
(_h1s, h1e), (h2s, h2e) = holds
|
||||
between = sum(fire[h1e:h2s])
|
||||
if between:
|
||||
fail("[verdict-rehold] FIRE while resumed under the hold verdict: %d fire ticks "
|
||||
"between the holds" % between)
|
||||
if h2s - h1e > TICK_HZ:
|
||||
fail("[verdict-rehold] the second hold came late: %d ticks (%.2f s) of motion under "
|
||||
"the verdict" % (h2s - h1e, (h2s - h1e) / float(TICK_HZ)))
|
||||
lit_after = sum(fire[h2e:last + 1])
|
||||
if lit_after < 50:
|
||||
fail("[verdict-rehold] the resume after the clean verdict ran dark (%d fire ticks)"
|
||||
% lit_after)
|
||||
print("PASS [verdict-rehold]: held, resumed dark for %d ticks (%.2f s), held again, "
|
||||
"lit after the clear (%d fire ticks)"
|
||||
% (h2s - h1e, (h2s - h1e) / float(TICK_HZ), lit_after))
|
||||
|
||||
# --- rule 22: a jog never fires, whatever the modal spindle says ----
|
||||
# The arm flow runs on the M3 (window open), the modal spindle is on
|
||||
# at S1000, and the jogs come from Idle: exactly what a sender's Fire
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Lens stop detection by the head accelerometer: a bench drill.
|
||||
|
||||
Steps the lens one half-step at a time from the hall's rising edge toward
|
||||
each stop and a little past it, sampling the head accelerometer through
|
||||
the step's whole cadence window, and prints per step the peak-to-peak on
|
||||
each axis. A normal step rings a little; a stall against a stop (the
|
||||
rotor slipping, the carriage still) should ring differently. If it does,
|
||||
a shipped lens move could find a stop with a single slipped step instead
|
||||
of driving onto it.
|
||||
|
||||
The accelerometer (an ST LIS2HH12 on i2c-3 at 0x1e, the crash watch's
|
||||
chip) is read the way the crash watch reads it: straight over the bus in
|
||||
six-byte bursts, its rate register set to 800 Hz for the run and put
|
||||
back after. The iio path waits a sample period per read and is far too
|
||||
slow for this.
|
||||
|
||||
Runs on the board with the controller stopped through forgectrl and
|
||||
restarted after (the same handling as lens_travel.py). Lens motion only;
|
||||
nothing fires. Stall drills run on the bench reference machine only.
|
||||
|
||||
lens_stop_accel.py [--past N] [--window MS] [--save FILE]
|
||||
"""
|
||||
import argparse
|
||||
import fcntl
|
||||
import json
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
SYS = "/sys/glowforge/"
|
||||
TOKEN_FILE = "/data/forgefirm/panel.token"
|
||||
BASE = "http://127.0.0.1"
|
||||
STEP_S = 0.180
|
||||
|
||||
I2C_DEV = "/dev/i2c-3"
|
||||
I2C_SLAVE_FORCE = 0x0706
|
||||
ADDR = 0x1E
|
||||
CTRL1 = 0x20 # ODR [6:4], BDU, XYZ enables
|
||||
CTRL1_RUN = 0x6F # 800 Hz, BDU, XYZ on (the crash watch's run value)
|
||||
OUT_X_L = 0x28
|
||||
AUTO_INC = 0x80
|
||||
|
||||
|
||||
def rd(attr):
|
||||
with open(SYS + attr) as f:
|
||||
return f.read().strip()
|
||||
|
||||
|
||||
def wr(attr, value):
|
||||
with open(SYS + attr, "w") as f:
|
||||
f.write(str(value))
|
||||
|
||||
|
||||
def api(method, path):
|
||||
with open(TOKEN_FILE) as f:
|
||||
token = f.read().strip()
|
||||
req = urllib.request.Request(BASE + path, method=method, headers={"X-ForgeFIRM-Token": token})
|
||||
with urllib.request.urlopen(req, timeout=15) as r:
|
||||
raw = r.read().decode()
|
||||
return json.loads(raw) if raw.strip().startswith("{") else raw
|
||||
|
||||
|
||||
def at_home():
|
||||
return rd("head/hall_sensor") == "0"
|
||||
|
||||
|
||||
class Accel:
|
||||
"""The chip over the bus: bursts of x, y, z at up to the bus rate."""
|
||||
|
||||
def __init__(self):
|
||||
self.fd = os.open(I2C_DEV, os.O_RDWR)
|
||||
fcntl.ioctl(self.fd, I2C_SLAVE_FORCE, ADDR)
|
||||
self.ctrl1_found = self.reg(CTRL1)
|
||||
|
||||
def reg(self, r):
|
||||
os.write(self.fd, bytes([r]))
|
||||
return os.read(self.fd, 1)[0]
|
||||
|
||||
def set_reg(self, r, v):
|
||||
os.write(self.fd, bytes([r, v]))
|
||||
|
||||
def start(self):
|
||||
self.set_reg(CTRL1, CTRL1_RUN)
|
||||
time.sleep(0.02)
|
||||
|
||||
def restore(self):
|
||||
self.set_reg(CTRL1, self.ctrl1_found)
|
||||
|
||||
def read(self):
|
||||
os.write(self.fd, bytes([OUT_X_L | AUTO_INC]))
|
||||
return struct.unpack("<hhh", os.read(self.fd, 6))
|
||||
|
||||
def sample(self, seconds, keep=None):
|
||||
"""Peak-to-peak per axis and the sample count over `seconds`;
|
||||
the samples appended to `keep` when given."""
|
||||
lo = [None] * 3
|
||||
hi = [None] * 3
|
||||
n = 0
|
||||
end = time.monotonic() + seconds
|
||||
while time.monotonic() < end:
|
||||
v = self.read()
|
||||
if keep is not None:
|
||||
keep.append(v)
|
||||
for i in range(3):
|
||||
if lo[i] is None or v[i] < lo[i]:
|
||||
lo[i] = v[i]
|
||||
if hi[i] is None or v[i] > hi[i]:
|
||||
hi[i] = v[i]
|
||||
n += 1
|
||||
return tuple(hi[i] - lo[i] for i in range(3)), n
|
||||
|
||||
|
||||
def step_sampled(accel, up, window_s, keep=None):
|
||||
"""One half-step, the accelerometer sampled through the window."""
|
||||
wr("cnc/z_step", "1" if up else "0")
|
||||
p2p, n = accel.sample(window_s, keep)
|
||||
rest = STEP_S - window_s
|
||||
if rest > 0:
|
||||
time.sleep(rest)
|
||||
return p2p, n
|
||||
|
||||
|
||||
def until(home, up, limit):
|
||||
for n in range(limit):
|
||||
if at_home() == home:
|
||||
return n
|
||||
wr("cnc/z_step", "1" if up else "0")
|
||||
time.sleep(STEP_S)
|
||||
return -1
|
||||
|
||||
|
||||
def to_edge():
|
||||
if at_home():
|
||||
if until(False, False, 60) < 0:
|
||||
return False
|
||||
return until(True, True, 80) >= 0
|
||||
|
||||
|
||||
def fmt(p2p):
|
||||
return "x %5d y %5d z %5d" % p2p
|
||||
|
||||
|
||||
def run_leg(accel, up, count, window_s, expect, trace):
|
||||
"""From the edge, `count` single steps `up` or down, each sampled."""
|
||||
rows = []
|
||||
label = "above" if up else "below"
|
||||
print("\n== %s the edge, one half-step at a time (the bench stop is about %d %s)" % (label, expect, label))
|
||||
for i in range(1, count + 1):
|
||||
keep = []
|
||||
p2p, n = step_sampled(accel, up, window_s, keep)
|
||||
k = i if up else -i
|
||||
rows.append((i, p2p))
|
||||
trace.append({"k": k, "p2p": p2p, "samples": keep})
|
||||
mark = " <- past the expected stop" if i > expect else ""
|
||||
print(" %+3d: %s (%d samples)%s" % (k, fmt(p2p), n, mark))
|
||||
return rows
|
||||
|
||||
|
||||
def summarize(rows, expect):
|
||||
free = [p for i, p in rows if i <= expect - 2]
|
||||
stall = [p for i, p in rows if i > expect]
|
||||
if not free or not stall:
|
||||
return
|
||||
for i, a in enumerate("xyz"):
|
||||
fv = sorted(p[i] for p in free)
|
||||
sv = sorted(p[i] for p in stall)
|
||||
print(" %s: free steps p2p min %d median %d max %d; past the stop min %d median %d max %d"
|
||||
% (a, fv[0], fv[len(fv) // 2], fv[-1], sv[0], sv[len(sv) // 2], sv[-1]))
|
||||
|
||||
|
||||
def find_stop(accel, up, window_s, limit, thresh):
|
||||
"""Step `up` or down from the edge one half-step at a time until the
|
||||
carriage stops moving. A free step rings strongly on every second
|
||||
half-step (the parity is learned from the first two), so contact is
|
||||
called the moment a strong-parity step rings under `thresh`; a burst
|
||||
over four times `thresh` is a rotor slip, which means the stop was
|
||||
reached a step or two earlier. Returns (count, slipped), or (-1, 0)."""
|
||||
sums = []
|
||||
strong_parity = None
|
||||
for i in range(1, limit + 1):
|
||||
p2p, n = step_sampled(accel, up, window_s)
|
||||
total = sum(p2p)
|
||||
sums.append(total)
|
||||
k = i if up else -i
|
||||
note = ""
|
||||
if total > 4 * thresh:
|
||||
note = " <- slip burst"
|
||||
elif strong_parity is None and i == 2:
|
||||
strong_parity = 2 if sums[1] > sums[0] else 1
|
||||
note = " (strong steps are the %s ones)" % ("even" if strong_parity == 2 else "odd")
|
||||
print(" %+3d: %s sum %6d%s" % (k, fmt(p2p), total, note))
|
||||
if total > 4 * thresh:
|
||||
return i, 1
|
||||
if strong_parity is not None and i % 2 == strong_parity % 2 and total < thresh:
|
||||
return i, 0
|
||||
return -1, 0
|
||||
|
||||
|
||||
def find_leg(accel, up, window_s, limit, thresh, back):
|
||||
label = "top" if up else "bottom"
|
||||
print("\n== finding the %s stop by the ring (contact = two quiet steps in a row, sum under %d)"
|
||||
% (label, thresh))
|
||||
if not to_edge():
|
||||
print(" could not find the edge first")
|
||||
return None
|
||||
i, slipped = find_stop(accel, up, window_s, limit, thresh)
|
||||
if i < 0:
|
||||
print(" no contact within %d half-steps" % limit)
|
||||
return None
|
||||
for _ in range(back):
|
||||
wr("cnc/z_step", "0" if up else "1")
|
||||
time.sleep(STEP_S)
|
||||
print(" contact called at %+d%s; backed off %d" % (i if up else -i, " by a slip" if slipped else "", back))
|
||||
# The proof of gentleness: the count back to the edge equals the
|
||||
# steps taken only if nothing slipped.
|
||||
if up:
|
||||
c = until(False, False, 80)
|
||||
c2 = until(True, True, 60)
|
||||
print(" back down to leave home: %d, up to the edge: %d (expected %d and the band)"
|
||||
% (c, c2, i - back))
|
||||
else:
|
||||
c = until(True, True, 80)
|
||||
print(" back up to the edge: %d (expected %d: no slip)" % (c, i - back))
|
||||
return i
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--past", type=int, default=6, help="half-steps past each expected stop")
|
||||
ap.add_argument("--window", type=int, default=170, help="ms sampled after each step (of the 180 cadence)")
|
||||
ap.add_argument("--below", type=int, default=18, help="the bench bottom stop, half-steps below the edge")
|
||||
ap.add_argument("--above", type=int, default=20, help="the bench top stop, half-steps above the edge")
|
||||
ap.add_argument("--save", default=None, help="write the per-step sample traces to this JSON file")
|
||||
ap.add_argument("--find", type=int, default=0,
|
||||
help="instead of the legs: find each stop by the ring this many times")
|
||||
ap.add_argument("--thresh", type=int, default=8000, help="the quiet-step threshold on the summed p2p")
|
||||
ap.add_argument("--back", type=int, default=2, help="half-steps to back off after contact")
|
||||
args = ap.parse_args()
|
||||
window_s = min(args.window, 180) / 1000.0
|
||||
|
||||
dark = api("GET", "/wiz/dark")
|
||||
if dark.get("running"):
|
||||
sys.exit("a wizard is running: %s" % dark.get("id"))
|
||||
mode = api("GET", "/mode")
|
||||
print("mode before:", mode)
|
||||
accel = Accel()
|
||||
print("accelerometer: %s at 0x%02x, CTRL1 found 0x%02x" % (I2C_DEV, ADDR, accel.ctrl1_found))
|
||||
found = {a: rd(a) for a in ("cnc/motor_lock", "head/z_current", "head/z_mode", "head/z_enable")}
|
||||
stopped = False
|
||||
trace = []
|
||||
try:
|
||||
if mode.get("controller") == "running":
|
||||
print("stopping the controller:", api("POST", "/controller/stop"))
|
||||
stopped = True
|
||||
time.sleep(2)
|
||||
accel.start()
|
||||
p2p, n = accel.sample(2.0)
|
||||
print("at rest, 2 s: %s (%d samples, %.0f Hz)" % (fmt(p2p), n, n / 2.0))
|
||||
wr("cnc/motor_lock", "0")
|
||||
wr("head/z_current", "0")
|
||||
wr("head/z_mode", "1")
|
||||
wr("head/z_enable", "0")
|
||||
time.sleep(0.3)
|
||||
p2p, n = accel.sample(1.0)
|
||||
print("energized, at rest, 1 s: %s" % fmt(p2p))
|
||||
if not to_edge():
|
||||
sys.exit("could not find the hall edge")
|
||||
|
||||
if args.find:
|
||||
for r in range(args.find):
|
||||
print("### find round %d" % (r + 1))
|
||||
find_leg(accel, False, window_s, 30, args.thresh, args.back)
|
||||
find_leg(accel, True, window_s, 32, args.thresh, args.back)
|
||||
to_edge()
|
||||
print(" back on the edge")
|
||||
return
|
||||
|
||||
below = run_leg(accel, False, args.below + args.past, window_s, args.below, trace)
|
||||
summarize(below, args.below)
|
||||
back = until(True, True, 80)
|
||||
print(" back up to the edge: %d half-steps" % back)
|
||||
|
||||
above = run_leg(accel, True, args.above + args.past, window_s, args.above, trace)
|
||||
summarize(above, args.above)
|
||||
to_edge()
|
||||
print(" back on the edge")
|
||||
finally:
|
||||
try:
|
||||
accel.restore()
|
||||
except OSError as e:
|
||||
print("restore of the accelerometer rate failed: %s" % e)
|
||||
for a, v in found.items():
|
||||
try:
|
||||
wr(a, v)
|
||||
except OSError as e:
|
||||
print("restore %s=%s failed: %s" % (a, v, e))
|
||||
print("posture restored:", {a: rd(a) for a in found}, "hall:", rd("head/hall_sensor"),
|
||||
"CTRL1 0x%02x" % accel.reg(CTRL1))
|
||||
if stopped:
|
||||
print("starting the controller:", api("POST", "/controller/start"))
|
||||
time.sleep(4)
|
||||
print("mode after:", api("GET", "/mode"))
|
||||
if args.save:
|
||||
with open(args.save, "w") as f:
|
||||
json.dump(trace, f)
|
||||
print("traces saved to", args.save)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,306 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Lens travel drill: where the carriage's stops sit against the hall edge.
|
||||
|
||||
Runs on the board with the controller stopped (through forgectrl, so the
|
||||
supervisor holds it), steps the lens over sysfs the way the focus card's
|
||||
lens home does, and counts, under each condition and for several rounds:
|
||||
|
||||
band the hall's hysteresis: from the rising edge, the half-steps
|
||||
down until the hall leaves home, and back up until it reads
|
||||
home again
|
||||
below the half-steps from the bottom stop up to the hall edge
|
||||
(down off the edge, a drive of N steps onto the stop, the stall
|
||||
takes the rest, then up until the hall reads home)
|
||||
above the half-steps from the hall edge up to the top stop (a drive
|
||||
of 36 half-steps up from the edge, down until the hall leaves
|
||||
home, then up until it reads home again; the top sits the
|
||||
difference above the rising edge)
|
||||
|
||||
Conditions: the focus card's (half-step, run current, the whole travel
|
||||
driven onto the stop), a short drive that may not reach the stop, a long
|
||||
drive well past it, and the factory's lens home mode (full steps at the
|
||||
run current; the counts are doubled to half-steps). The current is the
|
||||
run current throughout: the lens does not rise at the hold current.
|
||||
|
||||
Lens motion only; nothing fires. The posture found is put back (motor
|
||||
lock, current, mode, enable) and the controller restarted.
|
||||
|
||||
lens_travel.py [--rounds N] [--only wizard|short|long|full] [--no-above]
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
SYS = "/sys/glowforge/"
|
||||
TOKEN_FILE = "/data/forgefirm/panel.token"
|
||||
BASE = "http://127.0.0.1"
|
||||
STEP_S = 0.180 # the cadence of every lens reference (the service's and the card's)
|
||||
TRAVEL_HALF = 36 # the carriage's travel in half-steps of its screw
|
||||
|
||||
CONDITIONS = [
|
||||
# key, label, z_mode (1 half, 0 full), the drive onto the stop in that mode's steps
|
||||
("wizard", "half-step, run current, drive 36 half-steps onto the stop (the focus card)", 1, 36),
|
||||
("short", "half-step, run current, drive 16 half-steps (may not reach the stop)", 1, 16),
|
||||
("long", "half-step, run current, drive 60 half-steps (well past the stop)", 1, 60),
|
||||
("full", "full-step, run current, drive 18 full steps (the factory's lens home mode)", 0, 18),
|
||||
]
|
||||
|
||||
|
||||
def rd(attr):
|
||||
with open(SYS + attr) as f:
|
||||
return f.read().strip()
|
||||
|
||||
|
||||
def wr(attr, value):
|
||||
with open(SYS + attr, "w") as f:
|
||||
f.write(str(value))
|
||||
|
||||
|
||||
def api(method, path):
|
||||
with open(TOKEN_FILE) as f:
|
||||
token = f.read().strip()
|
||||
req = urllib.request.Request(BASE + path, method=method, headers={"X-ForgeFIRM-Token": token})
|
||||
with urllib.request.urlopen(req, timeout=15) as r:
|
||||
raw = r.read().decode()
|
||||
return json.loads(raw) if raw.strip().startswith("{") else raw
|
||||
|
||||
|
||||
def at_home():
|
||||
return rd("head/hall_sensor") == "0"
|
||||
|
||||
|
||||
def step(up):
|
||||
wr("cnc/z_step", "1" if up else "0")
|
||||
time.sleep(STEP_S)
|
||||
|
||||
|
||||
SETTLE_S = 0.0 # extra rest before every hall read (--settle)
|
||||
|
||||
|
||||
def until(home, up, limit):
|
||||
"""Step `up` until the hall reads `home`; the steps taken, or -1."""
|
||||
for n in range(limit):
|
||||
if SETTLE_S:
|
||||
time.sleep(SETTLE_S)
|
||||
if at_home() == home:
|
||||
return n
|
||||
step(up)
|
||||
return -1
|
||||
|
||||
|
||||
CURRENT = "high" # high | low | low-down (--current)
|
||||
|
||||
|
||||
def count_below(drive):
|
||||
"""Down off the edge, `drive` steps onto the stop, up to the edge.
|
||||
Returns (count, the steps it took to leave home, error)."""
|
||||
if CURRENT == "low-down":
|
||||
wr("head/z_current", "1")
|
||||
left = until(False, False, 60)
|
||||
if left < 0:
|
||||
return None, left, "the hall never left home going down"
|
||||
for _ in range(drive):
|
||||
step(False)
|
||||
if CURRENT == "low-down":
|
||||
wr("head/z_current", "0")
|
||||
time.sleep(0.1)
|
||||
c = until(True, True, 80)
|
||||
return (c if c >= 0 else None), left, ("" if c >= 0 else "the hall never read home going up")
|
||||
|
||||
|
||||
def to_edge():
|
||||
"""Put the lens on the rising edge: below it if needed, then up."""
|
||||
if at_home():
|
||||
if until(False, False, 60) < 0:
|
||||
return False
|
||||
return until(True, True, 80) >= 0
|
||||
|
||||
|
||||
def count_band():
|
||||
"""From the rising edge: down until the hall leaves home, then up
|
||||
until it reads home again."""
|
||||
if not to_edge():
|
||||
return None, None, "could not find the edge"
|
||||
down = until(False, False, 60)
|
||||
if down < 0:
|
||||
return None, None, "the hall never left home going down"
|
||||
up = until(True, True, 60)
|
||||
return down, (up if up >= 0 else None), ("" if up >= 0 else "the hall never read home going up")
|
||||
|
||||
|
||||
def count_above(drive):
|
||||
"""From the edge, `drive` steps up onto the top stop, down until the
|
||||
hall leaves home (a), up until it reads home again (b): the top is
|
||||
a - b above the rising edge. The lens ends on the edge."""
|
||||
if not to_edge():
|
||||
return None, None, "could not find the edge first"
|
||||
for _ in range(drive):
|
||||
step(True)
|
||||
a = until(False, False, 80)
|
||||
if a < 0:
|
||||
return None, None, "the hall never left home coming down"
|
||||
b = until(True, True, 60)
|
||||
if b < 0:
|
||||
return a, None, "the hall never read home going back up"
|
||||
return a, b, ""
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--rounds", type=int, default=3)
|
||||
ap.add_argument("--only", choices=[c[0] for c in CONDITIONS])
|
||||
ap.add_argument("--no-above", action="store_true", help="skip the top-stop count")
|
||||
ap.add_argument("--settle", type=int, default=0, help="extra ms of rest before every hall read")
|
||||
ap.add_argument("--cadence", type=int, default=180, help="ms per step (180 is every reference's)")
|
||||
ap.add_argument("--ladder", default=None,
|
||||
help="instead of the conditions: comma-separated half-step drives below the "
|
||||
"leave-home point, each counted back up; half-step mode, run current")
|
||||
ap.add_argument("--full", action="store_true", help="the ladder in full-step mode")
|
||||
ap.add_argument("--current", choices=["high", "low", "low-down"], default="high",
|
||||
help="the drive current: the run current, the hold current, or the hold "
|
||||
"current for the drive onto the stop and the run current for the count")
|
||||
ap.add_argument("--park",
|
||||
help="park the lens for a depth-gauge measurement: bottom, top, edge-N or "
|
||||
"edge+N (N half-steps from the rising edge, stall-free; the controller "
|
||||
"stays stopped), or edge (back on the hall edge; the controller restarts)")
|
||||
args = ap.parse_args()
|
||||
global SETTLE_S, STEP_S, CURRENT
|
||||
SETTLE_S = args.settle / 1000.0
|
||||
STEP_S = args.cadence / 1000.0
|
||||
CURRENT = args.current
|
||||
print("step cadence %d ms, extra settle before each hall read %d ms, current %s"
|
||||
% (STEP_S * 1000, args.settle, CURRENT))
|
||||
|
||||
dark = api("GET", "/wiz/dark")
|
||||
if dark.get("running"):
|
||||
sys.exit("a wizard is running: %s" % dark.get("id"))
|
||||
mode = api("GET", "/mode")
|
||||
print("mode before:", mode)
|
||||
found = {a: rd(a) for a in ("cnc/motor_lock", "head/z_current", "head/z_mode", "head/z_enable")}
|
||||
print("posture found:", found, "hall:", rd("head/hall_sensor"))
|
||||
|
||||
stopped = False
|
||||
results = []
|
||||
try:
|
||||
if mode.get("controller") == "running":
|
||||
print("stopping the controller:", api("POST", "/controller/stop"))
|
||||
stopped = True
|
||||
time.sleep(2)
|
||||
wr("cnc/motor_lock", "0")
|
||||
wr("head/z_current", "1" if CURRENT == "low" else "0")
|
||||
wr("head/z_enable", "0")
|
||||
if args.park:
|
||||
wr("head/z_mode", "1")
|
||||
time.sleep(0.2)
|
||||
if args.park == "bottom":
|
||||
left = until(False, False, 60)
|
||||
for _ in range(24):
|
||||
step(False)
|
||||
print("parked on the bottom stop (left home after %d, then 24 half-steps down)" % left)
|
||||
elif args.park == "top":
|
||||
to_edge()
|
||||
for _ in range(26):
|
||||
step(True)
|
||||
print("parked on the top stop (26 half-steps up from the rising edge)")
|
||||
elif args.park.startswith("edge-") or args.park.startswith("edge+"):
|
||||
n = int(args.park[5:])
|
||||
up = args.park[4] == "+"
|
||||
to_edge()
|
||||
for _ in range(n):
|
||||
step(up)
|
||||
print("parked %d half-steps %s the rising edge, stall-free" % (n, "above" if up else "below"))
|
||||
else:
|
||||
wr("head/z_current", "0")
|
||||
to_edge()
|
||||
print("parked on the hall's rising edge")
|
||||
stopped = True # the controller restarts whatever state it was found in
|
||||
if args.park != "edge":
|
||||
# the hold posture keeps the lens where it is; the controller
|
||||
# stays stopped so nothing moves the head during the measurement
|
||||
for a, v in found.items():
|
||||
wr(a, v)
|
||||
print("posture:", {a: rd(a) for a in found}, "hall:", rd("head/hall_sensor"))
|
||||
print("the controller stays stopped: run --park edge to return and restart it")
|
||||
stopped = False
|
||||
found = {}
|
||||
return
|
||||
mult = 2 if args.full else 1
|
||||
wr("head/z_mode", 0 if args.full else 1)
|
||||
time.sleep(0.2)
|
||||
print("\n== ladder of drives below the leave-home point (%s-step mode)"
|
||||
% ("full" if args.full else "half"))
|
||||
for r in range(args.rounds):
|
||||
row = []
|
||||
for n in [int(x) for x in args.ladder.split(",")]:
|
||||
c, left, err = count_below(n // mult)
|
||||
row.append((n, None if c is None else c * mult, left * mult))
|
||||
print(" round %d: " % (r + 1) + " ".join("drive %d -> up %s (left %d)" % t for t in row))
|
||||
args.only = "none"
|
||||
for key, label, zmode, drive in CONDITIONS:
|
||||
if args.only and key != args.only:
|
||||
continue
|
||||
wr("head/z_mode", zmode)
|
||||
time.sleep(0.2)
|
||||
mult = 1 if zmode == 1 else 2
|
||||
unit = "half-steps" if mult == 1 else "full steps"
|
||||
print("\n== %s" % label)
|
||||
bands = []
|
||||
for r in range(args.rounds):
|
||||
d, u, err = count_band()
|
||||
if d is None or u is None:
|
||||
print(" band %d: FAILED: %s (down %s)" % (r + 1, err, d))
|
||||
continue
|
||||
bands.append((d * mult, u * mult))
|
||||
print(" band %d: home for %d half-steps below the edge going down, home again %d "
|
||||
"half-steps up" % (r + 1, d * mult, u * mult))
|
||||
below = []
|
||||
for r in range(args.rounds):
|
||||
t0 = time.time()
|
||||
c, left, err = count_below(drive)
|
||||
if c is None:
|
||||
print(" round %d: FAILED: %s" % (r + 1, err))
|
||||
below.append(None)
|
||||
continue
|
||||
below.append(c * mult)
|
||||
print(" round %d: the edge %d half-steps above the bottom stop (%d %s; left home "
|
||||
"after %d; %.1f s)" % (r + 1, c * mult, c, unit, left * mult, time.time() - t0))
|
||||
above = None
|
||||
if not args.no_above:
|
||||
for r in range(2):
|
||||
t0 = time.time()
|
||||
a, b, err = count_above(TRAVEL_HALF // mult)
|
||||
if a is None or b is None:
|
||||
print(" top %d: FAILED: %s (a=%s b=%s)" % (r + 1, err, a, b))
|
||||
continue
|
||||
above = (a - b) * mult
|
||||
print(" top %d: down %d half-steps to leave home, %d back up to the edge: the top "
|
||||
"stop %d half-steps above the rising edge (%.1f s)"
|
||||
% (r + 1, a * mult, b * mult, above, time.time() - t0))
|
||||
results.append((key, bands, below, above))
|
||||
wr("head/z_mode", "1")
|
||||
wr("head/z_current", "0")
|
||||
# leave the lens on the edge, as every reference does
|
||||
to_edge()
|
||||
finally:
|
||||
for a, v in found.items():
|
||||
try:
|
||||
wr(a, v)
|
||||
except OSError as e:
|
||||
print("restore %s=%s failed: %s" % (a, v, e))
|
||||
print("\nposture restored:", {a: rd(a) for a in found}, "hall:", rd("head/hall_sensor"))
|
||||
if stopped:
|
||||
print("starting the controller:", api("POST", "/controller/start"))
|
||||
time.sleep(4)
|
||||
print("mode after:", api("GET", "/mode"))
|
||||
|
||||
print("\n%-8s %-22s %-22s %-6s %s" % ("cond", "band down/up", "below (half-steps)", "above", "total"))
|
||||
for key, bands, below, above in results:
|
||||
ok = [b for b in below if b is not None]
|
||||
tot = (ok[-1] + above) if ok and above is not None else None
|
||||
print("%-8s %-22s %-22s %-6s %s" % (key, bands, below, above, tot))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Live-fire bench drills - Phases 4, 5, 6. Runs on the board (the bench
|
||||
page) or from a LAN host, against grblHAL over TCP (port 23) and
|
||||
forgectrl over HTTP (:8080); the machine is GF_HOST, default 127.0.0.1.
|
||||
forgectrl over HTTP (:80, FORGECTRL_PORT overrides); the machine is GF_HOST, default 127.0.0.1.
|
||||
LIVE LASER: the operator must be armed with eye protection, a fire
|
||||
watch, an extinguisher, and the exhaust running. Every drill waits for
|
||||
the operator to press the physical arm button before the machine fires;
|
||||
@@ -184,7 +184,8 @@ import urllib.request
|
||||
|
||||
HOST = os.environ.get('GF_HOST') or '127.0.0.1'
|
||||
PORT = 23
|
||||
BASE = 'http://%s:8080' % HOST
|
||||
CTRL_PORT = int(os.environ.get('FORGECTRL_PORT') or 80)
|
||||
BASE = 'http://%s:%d' % (HOST, CTRL_PORT)
|
||||
|
||||
|
||||
def panel_token():
|
||||
@@ -3021,7 +3022,7 @@ def post_ctrl(action):
|
||||
# http.client preserves the header-name case exactly as given.
|
||||
import http.client
|
||||
tok = panel_token()
|
||||
c = http.client.HTTPConnection(HOST, 8080, timeout=8)
|
||||
c = http.client.HTTPConnection(HOST, CTRL_PORT, timeout=8)
|
||||
c.putrequest('POST', '/controller/' + action)
|
||||
c.putheader('X-ForgeFIRM-Token', tok)
|
||||
c.putheader('Content-Length', '0')
|
||||
|
||||
@@ -32,6 +32,9 @@ import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
# forgectrl on the board (FORGECTRL_PORT overrides the port)
|
||||
BASE = 'http://127.0.0.1:%s' % (os.environ.get('FORGECTRL_PORT') or '80')
|
||||
|
||||
SYSFS = '/sys/glowforge/'
|
||||
LEDS = '/sys/class/leds/'
|
||||
HZ = 25
|
||||
@@ -225,9 +228,9 @@ def run_armed():
|
||||
|
||||
def status():
|
||||
try:
|
||||
with urllib.request.urlopen('http://127.0.0.1:8080/status', timeout=2) as r:
|
||||
with urllib.request.urlopen(BASE + '/status', timeout=2) as r:
|
||||
s = _json.load(r)
|
||||
with urllib.request.urlopen('http://127.0.0.1:8080/cool/status', timeout=2) as r:
|
||||
with urllib.request.urlopen(BASE + '/cool/status', timeout=2) as r:
|
||||
c = _json.load(r)
|
||||
return {'t': time.time(), 'armed': c.get('armed'), 'phase': c.get('phase'),
|
||||
'hv_enable': (s.get('switches') or {}).get('hv_enable'),
|
||||
|
||||
Reference in New Issue
Block a user