"""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 "",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()