autocal layout first design
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
"""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 PLATFORM_AXES
|
||||
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"
|
||||
|
||||
|
||||
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": "",
|
||||
#: 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(),
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
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()
|
||||
Reference in New Issue
Block a user