# AutoCal workflow — platform bring-up to calibration data This is the operating workflow the application is built around. It is written to be argued with: each phase names what it needs, what it produces, why it exists, and what must be true before the next one starts. A **gate** is a checkpoint. The app's job at a gate is to make the operator's answer trustworthy, not to ask "are you sure?". ```text P0 Session & safety baseline [gate] P1 ECAT setup [gate] P2 Homing + test motion [gate] P3 Sensor & probe setup [gate] P4 Coordinate setup: sensor <-> platform [gate] P5 Sequence design [gate] P6 Simulate & dry run [gate] P7 Run + acquire [gate] P8 Store raw P9 Post-process [gate] P10 Calibration export P11 Verify [gate] ``` --- ## P0 — Session & safety baseline **Why:** the machine can move from P2 onward. Everything that limits that motion should be decided while nothing is moving. **Inputs:** operator identity, machine/bench identifier, units per axis, `units_per_mm`. **Safety limits, set here, enforced from P2:** | Limit | Meaning | |---|---| | maximum travel per axis | never commanded past this, in user units | | maximum Z descent per step | a single approach may not exceed this | | press-depth ceiling | the press depth a sequence profile is allowed to request | | no-contact abort | if contact is not detected within the descent budget, stop the run | | following-error bound | how far a drive may lag before the move counts as stalled | **Outputs:** a session record — who, when, which machine, and the limits in force. Stamped onto everything the session produces. **Gate:** every limit has a value; no limit is "unset" (an unset limit is an unlimited machine). **Currently missing from the workflow:** this phase did not exist. Without it, step 1 went from bus setup straight to jogging five axes. --- ## P1 — ECAT setup **Why:** nothing else can be trusted until the controller has actually found its slaves. A missing drive discovered at P7 wastes the whole run. **Inputs:** controller address, the matching `.bas` program (`ECAT.bas` / `RTEX.bas`). **Steps:** connect → **adopt a program the controller is already running, or download one if `BUS_TYPE` is unreadable** → watch `Bus_InitStatus` to completion or timeout → read `BUS_TYPE`, `Bus_TotalAxisnum`, and the node count. Step 2 is conditional because a controller usually still has the program in ROM from the last session, and a download is a bus re-scan the operator did not ask for. Both controls live in the **device setup dialog** (设备设置), opened from the status strip's button, its connection/bus/init chips, or 文件 → 设备设置: they are used once per machine start-up, so the rail keeps only what is read while working. The strip continues to report connection, bus and init state while the dialog is open or shut. **Re-initialising without a download:** `STOPTASK ` then `RUN "",`, on the task from `bas_task` in `preference.json` (default 4 — the task the vendor reference uses; stopping another task restarts nothing). Two things are easy to get wrong here, and both have been: - the file name must be the one the *controller* has. When the session adopted a running program, the controller does not report the name, so the app carries the remembered `bas_path` into the connect for exactly this purpose; - the controller answers a `RUN` naming a file it does not have with **code 0** and the diagnosis in its reply text. The reply is checked, and its text is shown instead of reporting an initialisation that is not happening. **Outputs:** bus type, node count, axis count, init status. **Gate:** init status is success, **and** the node/axis counts match what this bench is expected to have. A mismatch is a finding, not a warning. **Existing code:** `MotionService.download_bas` / `reinit_bus` / `poll` implement the state machine and the 25 s timeout. **Connection faults are a different family.** A `ZAux_*` code in 20000–29999 is a PC-library fault (handshake, socket, address), not a controller code: ZDevelop's table cannot contain it, so the app answers it with cable/IP/no-second-client advice and retries an Ethernet open up to three times, because a first open on a cold route can fail and succeed immediately after. `20008` at the bench was that case. --- ## P2 — Homing + test motion **Why:** a drive that homes and jogs cleanly at low speed is the precondition for letting a sequence drive it. **Inputs:** per-axis homing mode, speeds, offset; a test-motion profile. **Steps:** enable → home each axis → low-speed jog each axis in both directions → confirm movement and return. **Outputs:** per-axis pass/fail with the evidence (distance moved, following error, alarm state). **Gate — the test motion needs a criterion.** This was missing. Proposed: - the axis moved at least a configured minimum distance; - it returned within a tolerance of its start position; - following error stayed under the bound from P0; - no alarm before, during or after. **Existing code:** the axis panel does the jog/homing; the criterion is new. --- ## P3 — Sensor & probe setup **Why:** which sensor is under test, what touches it, and how far the touching point is from the axis reference. Contact depth is meaningless without the tool. **Inputs:** - sensor type and config reference (the same `config/sensors//sensor.json` the studio uses); - the sensor's 3D scene package and its `geometry_id`; - the **probe tip** (tool): which axis presses, and the tip offset from the platform's reference point; - the mounting pose: how the sensor is fixtured on the platform. **Outputs:** a saved setup block. **Gate:** - the calibration's `geometry_id` matches the scene's, or the app says which is which rather than colouring the wrong geometry; - the probe tip offset is set (a zero offset that was never entered is not the same as a probe at the origin). --- ## P4 — Coordinate setup: sensor ↔ platform **Why this is the load-bearing phase.** "Which points on the sensor should be tested" is a question in the *sensor's* frame. The platform moves in *platform* coordinates. Without the transform between them: - a generated grid cannot be checked against the sensor's surface; - a measured point cannot be placed on that surface for the sensitivity map; - a calibration cannot be re-applied to a re-fixtured sensor. **Inputs:** the probe tip from P3, and a set of **at least four non-collinear known features** on the sensor or its fixture, probed in platform coordinates. **Steps:** probe each feature → solve for the rigid transform (rotation and translation; scale if the units are not yet trusted) → report the residual. **Outputs:** a 4×4 transform from sensor frame to platform frame, with its residual and the feature set that produced it. **Gate:** residual under a configured tolerance, and at least four non-collinear points (four collinear points determine a rotation about that line, not a transform — the app should refuse that set rather than return a number). **Not built in this step:** the solver. Only the workflow step, the stored transform, and the place its residual is displayed. --- ## P5 — Sequence design **Why:** this is the decision the whole run exists to carry out. **Inputs (the profile, persisted and reusable):** | Parameter | Meaning | |---|---| | mode | flat surface or curved (a cylindrical/spherical sensor area) | | grid pitch | spacing between points, in mm | | margin / radius | how far in from the active edge the grid stays; radius for curved | | clear height | standoff above the surface to travel at | | approach speed | descent speed before contact | | press speed | descent speed after contact | | press depth | how far past contact to press | | dwell | how long to hold the press | | settle window | how long to wait before the measurement is considered settled | | repeats | how many times each point is measured | | point order | row-wise, serpentine, or by distance | **Steps:** generate the grid → validate **every point** three ways: 1. **on the sensor** — its sensor-frame position falls on the modelled surface, not off the edge and not inside a hole; 2. **within travel** — the platform can reach it inside the P0 limits; 3. **confirmable** — the sensor's sensing area is under the point, so a contact there can actually be seen by the sensor. **Outputs:** a point list with a sensor-frame position and a platform-frame target for each, plus a validation verdict per point. **Gate:** no point may be unusable without being marked as such. A run may proceed with skipped points if the operator says so explicitly; it may not proceed with unvalidated ones. **The interface:** three zones in the Sequence tab — profile parameters, a 3D preview of the surface with the points and their visiting order, and the validation list. See `apps/autocal/README.md`. --- ## P6 — Simulate & dry run **Why:** a wrong Z or a wrong grid is much cheaper to find now. **Simulate:** play the full toolpath in the 3D view — a moving marker with a phase label (standoff → descend → press → dwell → retract), the estimated duration, and per-point reachability. No motion. **Dry run:** execute the real XY path, descending only to the clear height, never pressing. This proves the geometry, the ordering, and the timing. **Outputs:** estimated duration, per-point reachability, and (dry run) the measured time per point. **Gate:** the operator has seen the path and a dry run has completed without a guard violation. **The single-point dry run is built**, in the finger debug screen: 执行动作 sends the selected point's *solved pose* to the platform — the stages together with the table X/Y, and the press axis only once its zero is calibrated (设备设置 → 压轴); until then it is held, so the move cannot reach the sensor by accident. The order follows the model's own composition (`R_pitch · R_yaw`), so every intermediate pose is one the operator watched in the preview. Before it moves it checks the mapping, the target against the P0 limits (millimetres for x/y/z, degrees for yaw/pitch) and the rotary calibration — all in `motion/service.py`, so a later caller cannot walk around them. 停止 next to it is the same command as the strip's 全部停止; 回初始位 in the rail sends every axis to zero with the press axis first. --- ## P7 — Run + acquire **Why:** the only phase that touches the sensor. **Per step, in order:** 1. move to the point's XY at clear height; 2. descend at approach speed; 3. **detect contact by position** — the axis stalls / following error rises while descending; 4. **confirm with the sensor under test** — its reading must change when the probe presses. This is the second opinion: position says "I stopped moving", the sensor says "something real is happening"; 5. press to the configured depth at press speed; 6. dwell, then acquire the settle window; 7. retract, move to the next point. **Failure semantics (per point, because resume needs them):** | Outcome | Run behaviour | |---|---| | contact confirmed | record the point as measured | | contact detected, sensor shows no change | mark **suspect**, continue; a suspect point is visible, not silently averaged in | | no contact within the descent budget | **abort** the run (P0 limit) and leave the point unfinished | | guard violation | abort the run, name the step and the limit | **Writes are incremental**: each point is written as it completes, with its status, so an interrupted run resumes at the first unfinished point and partial data stays valid. **Existing code:** the motion layer has the guards and the poller; the step machine, the contact detection and the acquisition binding are new. --- ## P8 — Store raw **Why:** post-processing will change. The raw record must not. **Contents:** - the sequence profile and the instantiated point list (positions and order); - per point: the position trace, the raw sensor series, the settle window used, timings, the contact position, and the status; - the setup: sensor identity, geometry id, probe tip, the P4 transform and its residual; - the session record and app/version stamps from P0. Nothing derived is authoritative. The requirement that makes this reproducible: **a dataset can be regenerated from the profile, the setup and the raw series**, with no dependency on a previously computed result. --- ## P9 — Post-process **Why:** turning samples into a number is a decision, and it has to be visible. **Steps:** choose the settle window per point → stability check (is the point actually settled, or drifting?) → sensitivity per point per axis → outlier detection → residual per point → fit the surface across points. **Outputs:** per-point sensitivity with a quality verdict, and a surface field over the sensor's geometry ready for the 3D colour map. **Gate:** every point is either valid or explicitly excluded, with a reason. A point with too few samples or a drifting window is not a coefficient. --- ## P10 — Calibration export **Per-point results and the fitted surface, explicitly linked.** They are one calibration; exporting them as unrelated artefacts is how a map ends up in a report that does not match the table beside it. --- ## P11 — Verify **Why this exists:** P9 and P10 produce numbers. Nothing yet checks they are right. "Post process → calibration data" had no acceptance test. **Steps:** apply the calibration → re-measure a small subset of the points → compare the measured sensitivity against what the calibration predicts → report the discrepancy per point. **Gate:** the discrepancy is within tolerance, or the verification is recorded as failed. A calibration that was never verified is labelled as such wherever it is used. --- ## Gaps this document closes | # | Gap | Phase | |---|---|---| | 1 | no limits before motion | P0 | | 2 | no session identity (who/when/which machine) | P0 | | 3 | the sensor↔platform transform was absent | P4 | | 4 | the probe tip (tool) was never defined | P3 | | 5 | the test motion had no pass/fail criterion | P2 | | 6 | grid validity was not checked per point | P5 | | 7 | the settle window and stability criterion were undefined | P9 | | 8 | a point's failure had no defined behaviour | P7 | | 9 | the sequence was transient, not a saved, versioned artefact | P8 | | 10 | no verification pass | P11 | ## Data flow ```text P0 limits ─┬───────────────────────────────> enforced at P2, P6, P7 P1 bus ────┴─> axis availability ─────────> P2 P3 sensor setup ─┬─ geometry id ──────────> P5 validation, P10 surface └─ probe tip ────────────> P4, P7 press depth P4 transform ─────> sensor <-> platform ──> P5 point generation, P9 map (residual) P11 re-fixture check P5 profile + points ──> P6 simulate/dry run ──> P7 run P7 raw series ─────> P8 dataset ──> P9 post-process ──> P10 export └─> P11 verify ```