265 lines
11 KiB
Python
265 lines
11 KiB
Python
"""AutoCal's preference schema and storage.
|
|
|
|
The shared store keys off whatever schema it is handed, so AutoCal gets its own
|
|
key set rather than inheriting the studio's camera/plot/3D keys. The values here
|
|
are the ones an operator would otherwise retype at every start-up.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from orisys_app_shared import prefs as _prefs
|
|
from orisys_app_shared.paths import preference_path
|
|
|
|
from .motion.service import DEFAULT_BAS_TASK, PLATFORM_AXES, clamp_bas_task
|
|
from .motion.units import DEFAULT_ACCEL, DEFAULT_SPEED, DEFAULT_UNITS
|
|
|
|
#: Connection modes, in the order the combo offers them.
|
|
CONNECTION_MODES = ("eth", "com", "pci", "local", "demo")
|
|
|
|
#: Addresses the controller is normally reached on, tried first by a scan.
|
|
DEFAULT_CONTROLLER_IPS = ("192.168.0.11", "127.0.0.1")
|
|
|
|
#: Grid modes the sequence profile offers (workflow P5).
|
|
SURFACE_MODES = ("flat", "curved")
|
|
|
|
#: Point visiting orders (workflow P5).
|
|
POINT_ORDERS = ("row", "serpentine", "distance")
|
|
|
|
#: The design target is a well-lit bench, so light is the default; dark stays
|
|
#: selectable for a dim room.
|
|
DEFAULT_THEME = "light"
|
|
|
|
#: The sensor-design export the finger debug screen loads by default. The bench
|
|
#: confirmed this revision, and it carries the mesh, housing, intrinsics and the
|
|
#: pose the unit scale is derived from.
|
|
DEFAULT_FINGER_BUNDLE = (
|
|
r"D:\orisys_ws\orisys-sensor-design\build\export\finger\r01\compute"
|
|
)
|
|
|
|
#: The UV-space artwork for that revision. Resolved from the bundle when it can
|
|
#: be, so the two cannot be pointed at different revisions; this preference is
|
|
#: the override for a checkout that keeps its textures elsewhere.
|
|
DEFAULT_FINGER_TEXTURE = (
|
|
r"D:\orisys_ws\orisys-sensor-design\build\textures\finger\r01"
|
|
r"\patch_texture_compute.png"
|
|
)
|
|
|
|
|
|
def _default_sequence_profile() -> dict[str, Any]:
|
|
"""The point-generation and run parameters of workflow P5.
|
|
|
|
One profile is global for now; named, saved sequences come with the runner.
|
|
The numbers are starting points an operator confirms at the bench, not
|
|
measured values — in particular ``home_mode``-style unknowns are left at 0 so
|
|
nothing is silently inherited from another machine.
|
|
"""
|
|
return {
|
|
# --- grid shape
|
|
"mode": "flat",
|
|
"pitch_mm": 2.0,
|
|
"margin_mm": 1.0,
|
|
"radius_mm": 0.0,
|
|
# --- motion
|
|
"clear_height_mm": 5.0,
|
|
"approach_speed_mm_s": 2.0,
|
|
"press_speed_mm_s": 0.5,
|
|
"press_depth_mm": 0.2,
|
|
"dwell_s": 0.5,
|
|
# --- measurement
|
|
"settle_window_s": 0.5,
|
|
"repeats": 3,
|
|
# --- order
|
|
"order": "serpentine",
|
|
}
|
|
|
|
|
|
def _default_safety_limits() -> dict[str, float]:
|
|
"""Workflow P0: the limits that must have a value before anything moves.
|
|
|
|
``0`` means "not set" for the ones where a real value is required, and the
|
|
app reports that at the P0 gate rather than treating it as unlimited.
|
|
"""
|
|
return {
|
|
"max_travel_mm": 100.0,
|
|
"max_descent_mm": 10.0,
|
|
"max_press_depth_mm": 1.0,
|
|
"follow_error_mm": 0.5,
|
|
#: How far the axis may descend looking for contact before the run is
|
|
#: aborted (workflow P7, no-contact abort).
|
|
"contact_search_mm": 3.0,
|
|
}
|
|
|
|
|
|
def _default_sensor_setup() -> dict[str, Any]:
|
|
"""Workflow P3/P4: what is under test, what touches it, and the transform.
|
|
|
|
``sensor_to_platform`` stays empty until the transform is established by
|
|
probing known features; the residual travels with it so a re-fixtured sensor
|
|
cannot silently reuse a stale transform.
|
|
"""
|
|
return {
|
|
"sensor_type": "box",
|
|
"config_ref": "",
|
|
"geometry_id": "",
|
|
#: Which axis presses, and the probe tip offset from the platform's
|
|
#: reference point. Empty offset means "not set", which blocks P5.
|
|
"press_axis": 2,
|
|
"probe_tip_offset_mm": [0.0, 0.0, 0.0],
|
|
"probe_tip_set": False,
|
|
#: 4x4 row-major sensor -> platform transform, plus how well it fitted.
|
|
"sensor_to_platform": [],
|
|
"transform_residual_mm": 0.0,
|
|
"transform_feature_count": 0,
|
|
}
|
|
|
|
|
|
def _default_axis_params() -> dict[str, dict[str, float | int]]:
|
|
"""One parameter block per platform axis.
|
|
|
|
``home_mode`` starts at 0, which is **not** a valid CiA-402 homing mode:
|
|
the operator must choose one for the drive in front of them rather than
|
|
inherit a number from another machine. The reference program pre-filled 17,
|
|
which is a specific drive's mode, so it is deliberately not copied here.
|
|
"""
|
|
return {
|
|
str(axis): {
|
|
"units": DEFAULT_UNITS,
|
|
"speed": DEFAULT_SPEED,
|
|
"accel": DEFAULT_ACCEL,
|
|
"creep": 10.0,
|
|
"home_mode": 0,
|
|
"home_high": 50.0,
|
|
"home_low": 10.0,
|
|
"home_offset": 0.0,
|
|
}
|
|
for axis in range(PLATFORM_AXES)
|
|
}
|
|
|
|
|
|
def default_preferences() -> dict[str, Any]:
|
|
"""AutoCal's default preference document."""
|
|
return {
|
|
"language": "zh_CN",
|
|
"theme": DEFAULT_THEME,
|
|
"window_geometry": None,
|
|
"connection": {
|
|
"mode": "eth",
|
|
"ip": DEFAULT_CONTROLLER_IPS[0],
|
|
"com_id": 0,
|
|
"card_num": 0,
|
|
"local_string": "0",
|
|
},
|
|
# Remembered so "download the BAS program" is one click after the first
|
|
# time, instead of re-picking the file every session.
|
|
"bas_path": "",
|
|
#: The controller task the BAS program runs on. The vendor reference
|
|
#: starts it on 4 and re-initialises it with ``RUN "<file>",4``; stopping
|
|
#: any other task restarts nothing. Kept configurable because the task
|
|
#: number is a property of the project loaded on the controller.
|
|
"bas_task": DEFAULT_BAS_TASK,
|
|
#: Displayed millimetres are derived from this; it is a bench-measured
|
|
#: constant, not a calibrated length (see motion/units.py).
|
|
"units_per_mm": DEFAULT_UNITS,
|
|
"state_poll_ms": 100,
|
|
"axis_params": _default_axis_params(),
|
|
#: {node, index, subindex, type} of the last SDO read/written, so the
|
|
#: operator can repeat a drive-setting check quickly.
|
|
"sdo_last": {"node": 0, "index": 24704, "subindex": 0, "type": 0},
|
|
"rtex_last": {"axis": 0, "parameter": 0},
|
|
"show_diagnostics": False,
|
|
# --- workflow blocks (see apps/autocal/docs/workflow.md) --------------
|
|
#: P0: limits enforced from P2 onward.
|
|
"safety_limits": _default_safety_limits(),
|
|
#: P3/P4: sensor under test, probe tip, sensor<->platform transform.
|
|
"sensor_setup": _default_sensor_setup(),
|
|
#: P5: grid generation and run parameters.
|
|
"sequence_profile": _default_sequence_profile(),
|
|
# --- finger debug screen ---------------------------------------------
|
|
#: The export bundle the debug screen loads. Empty means "ask", and the
|
|
#: default below points at the revision the bench confirmed.
|
|
"finger_bundle_path": DEFAULT_FINGER_BUNDLE,
|
|
#: The UV-space texture for the flat picker. Preferred over the derived
|
|
#: path when it exists; empty falls back to the layout map.
|
|
"finger_texture_path": DEFAULT_FINGER_TEXTURE,
|
|
#: How the sensor actually sits on the stack when the machine is at zero:
|
|
#: x/y/z translation along the table's axes and a yaw about its z. **The z
|
|
#: is the mounting plate**, so the plate is not a setting of its own — it
|
|
#: lifts the sensing face and does nothing else, and two fields for one
|
|
#: number drift apart.
|
|
"finger_sensor_offset_mm": [0.0, 0.0, 0.0],
|
|
"finger_sensor_yaw_deg": 0.0,
|
|
#: Where the tool is, in the table frame, mm. Straight above the table
|
|
#: origin unless the real machine says otherwise; every XY move is measured
|
|
#: against it, so it is what makes the reported move match the bench. A tool
|
|
#: tip offset belongs here — it used to be a separate field, and having two
|
|
#: of them is what let the picture and the numbers disagree.
|
|
"finger_tool_xy_mm": [0.0, 0.0],
|
|
#: Stage travel, degrees. Confirmed at the bench: the vertical stage turns
|
|
#: a full half-turn either way (±180), and the horizontal stage turns
|
|
#: continuously. An earlier value of 90 was wrong and made half the patch
|
|
#: look unreachable when it is not.
|
|
"finger_yaw_limit_deg": 180.0,
|
|
"finger_pitch_limit_deg": 360.0,
|
|
}
|
|
|
|
|
|
def load_preferences() -> dict[str, Any]:
|
|
"""Stored preferences with AutoCal's defaults filling the gaps."""
|
|
prefs = _prefs.load_preferences(default_preferences())
|
|
# A stored document from an older build may miss a newly added axis block.
|
|
axis_params = prefs.setdefault("axis_params", {})
|
|
for axis, values in _default_axis_params().items():
|
|
axis_params.setdefault(axis, values)
|
|
|
|
# The mounting plate used to be a setting of its own. It is now the z of the
|
|
# sensor's mounting pose, so a stored value is carried over rather than quietly
|
|
# dropped — a machine that had it tuned would otherwise come back 4 mm off.
|
|
plate = prefs.pop("finger_plate_thickness_mm", None)
|
|
try:
|
|
plate = float(plate) if plate is not None else 0.0
|
|
except (TypeError, ValueError):
|
|
plate = 0.0
|
|
offset = prefs.get("finger_sensor_offset_mm")
|
|
offset = list(offset) if isinstance(offset, list) else [0.0, 0.0, 0.0]
|
|
while len(offset) < 3:
|
|
offset.append(0.0)
|
|
if plate and not offset[2]:
|
|
offset[2] = plate
|
|
prefs["finger_sensor_offset_mm"] = offset
|
|
|
|
# The tool tip offset used to be a vector added to the reported coordinates
|
|
# while the drawing ignored it, so the picture and the numbers disagreed by
|
|
# exactly that much. It is the tool's position now: fold a stored x/y into it,
|
|
# and take whatever is left out of the document so nothing reads it again. Its
|
|
# z had no meaning the model kept — the tool's height is the Z axis's job — so
|
|
# it is dropped, deliberately and visibly rather than silently applied.
|
|
legacy = prefs.pop("finger_tool_offset_mm", None)
|
|
tool = prefs.get("finger_tool_xy_mm")
|
|
tool = list(tool) if isinstance(tool, list) else [0.0, 0.0]
|
|
while len(tool) < 2:
|
|
tool.append(0.0)
|
|
if isinstance(legacy, list) and len(legacy) >= 2:
|
|
for index in range(2):
|
|
try:
|
|
value = float(legacy[index])
|
|
except (TypeError, ValueError):
|
|
continue
|
|
if value and not tool[index]:
|
|
tool[index] = value
|
|
prefs["finger_tool_xy_mm"] = tool
|
|
# A hand-edited task number must not reach the controller as typed.
|
|
prefs["bas_task"] = clamp_bas_task(prefs.get("bas_task"))
|
|
return prefs
|
|
|
|
|
|
def save_preferences(prefs: dict[str, Any]) -> Path:
|
|
return _prefs.save_preferences(prefs, default_preferences())
|
|
|
|
|
|
def path() -> Path:
|
|
"""Where AutoCal's ``preference.json`` lives (for the About box)."""
|
|
return preference_path()
|