step.params =========== .. py:module:: step.params .. autoapi-nested-parse:: Typed, string-free mission parameters entered via the setup UI. A *parameter* is a single value the operator dials in on the robot's screen during the setup mission (e.g. a positional offset, a speed) and that the normal mission code then reads back for its calculations. The design goal is to **never expose a string key at the call site**. A parameter is declared once as a typed descriptor on a :class:`ParamSet`. Python's ``__set_name__`` hands the descriptor its own attribute name, so the internal storage key is derived automatically — a typo becomes an ``AttributeError`` at import time instead of a silently-wrong ``0.0`` at runtime, and IDE autocomplete/rename work across the whole codebase. Declare parameters once:: from raccoon import NumberParam, ParamSet class P(ParamSet): cube_offset = NumberParam(default=0.0, unit="cm", persist=True) ramp_speed = NumberParam(default=0.5) Ask for them in the setup mission — ``.ask()`` returns a ready-to-use step and pulls unit/range/default straight off the descriptor:: class MySetup(SetupMission): def sequence(self) -> Sequential: return seq( [ P.cube_offset.ask("Cube-row offset"), P.ramp_speed.ask("Ramp speed"), ] ) Read them in normal mission code — typed, autocompleted, rename-safe:: def sequence(self) -> Sequential: off = P.cube_offset.get() # -> float return seq([drive_forward(cm=60 + off)]) Or bind the value lazily inside a declarative ``seq([...])`` tree with the existing :func:`defer` primitive, so the calculation happens at execution time regardless of build order:: seq( [ defer(lambda r: drive_forward(cm=60 + P.cube_offset.get())), ] ) Persistence is opt-in: ``persist=True`` mirrors the value into ``racoon.calibration.yml`` (via the shared :class:`CalibrationStore`) so it survives a process restart and is reused under ``--no-calibrate``. Without it a parameter lives only in RAM for the current process — set once in setup, read by every later mission. Profiles (game-table / side scoping) ------------------------------------- Some parameters differ per physical game table and per starting side. Mark such a parameter ``scoped=True`` and it is stored *under the active profile* — a ``(game-table, side)`` pair chosen once during setup — instead of globally. ``.get()`` transparently resolves against whatever profile is active, so mission code never mentions the table or side:: class P(ParamSet): cube_offset = NumberParam(default=0.0, unit="cm", scoped=True, persist=True) drum_speed = NumberParam(default=0.5) # global — unaffected by profile Drive the whole flow from the setup mission with a single step: it shows one keypad screen where the operator types the table number and taps ``A`` / ``B`` (e.g. ``1B``) and — if a *complete* set of saved values already exists for that profile — offers to reuse them (asking first) instead of re-entering every value:: class MySetup(SetupMission): def sequence(self) -> Sequential: return seq( [ configure_mission( params=[ (P.cube_offset, "Cube-row offset"), ], ), ] ) Un-scoped parameters ignore the active profile completely, so mixing scoped and global parameters in one mission is fine. Attributes ---------- .. autoapisummary:: step.params.ParamPrompt Classes ------- .. autoapisummary:: step.params.NumberParam step.params.ParamSet step.params.AskNumber step.params.ConfigureMission Functions --------- .. autoapisummary:: step.params.reset_params step.params.set_active_profile step.params.get_active_profile step.params.clear_active_profile step.params.load_persisted_profile step.params.ask step.params.configure_mission Module Contents --------------- .. py:function:: reset_params() -> None Clear all runtime parameter values (test helper). Only touches the in-memory cache and the active profile; persisted YAML entries are left alone. .. py:function:: set_active_profile(table: str, side: str, *, persist: bool = True) -> None Select the active ``(game-table, side)`` profile for scoped parameters. :param table: Game-table name (as shown in the setup menu). :param side: Starting side (e.g. ``"A"`` / ``"B"``). :param persist: When ``True`` (default) the selection is mirrored into the calibration YAML so a restart / ``--no-calibrate`` run can restore it without re-asking. .. py:function:: get_active_profile() -> tuple[str, str] | None Return the active ``(game-table, side)`` profile, or ``None``. .. py:function:: clear_active_profile() -> None Forget the active profile (test helper; persisted YAML is untouched). .. py:function:: load_persisted_profile() -> tuple[str, str] | None Restore and activate the last persisted profile, if any. Used on the ``--no-calibrate`` path so a headless run reuses the profile (and therefore the scoped values) picked during an earlier interactive setup. Returns the activated profile, or ``None`` when nothing is stored. .. py:class:: NumberParam(default: float = 0.0, *, unit: str = '', persist: bool = False, scoped: bool = False, key: str | None = None) A numeric mission parameter, declared once as a :class:`ParamSet` field. The descriptor owns the value's metadata (default, unit, persistence) and, via ``__set_name__``, its own storage key — so callers interact with the *attribute*, never a string:: class P(ParamSet): cube_offset = NumberParam(default=0.0, unit="cm") P.cube_offset.get() # -> float P.cube_offset.ask("Offset...") # -> Step for the setup mission :param default: Value returned by :meth:`get` before anything is entered. :param unit: Display unit shown on the input screen (e.g. ``"cm"``). :param persist: When ``True``, entered values are mirrored to ``racoon.calibration.yml`` and reused across restarts / ``--no-calibrate``. :param scoped: When ``True``, the value is stored *per active profile* (game-table + side) rather than globally — see :func:`configure_mission`. Combine with ``persist=True`` so saved per-profile values survive a restart and can be reused. :param key: Explicit storage key. Normally omitted — the attribute name is used automatically. Only needed when declaring a parameter outside a class body (e.g. a module-level constant). .. py:attribute:: default .. py:attribute:: unit :value: '' .. py:attribute:: persist :value: False .. py:attribute:: scoped :value: False .. py:property:: key :type: str .. py:method:: get() -> float Return the current value, or :attr:`default` if none was entered. .. py:method:: set(value: float) -> None Set the value directly. .. py:method:: is_set() -> bool Whether a value has been entered/persisted (vs. falling back to default). .. py:method:: ask(prompt: str, *, title: str = 'Setup') -> step.base.Step Return a setup step that asks the operator for this value. The input screen is pre-filled with the current value (persisted or default) and shows the declared unit. Cancelling keeps the existing value. Under ``--no-calibrate`` the UI is skipped and the persisted/default value is used as-is. :param prompt: Question shown above the numeric keypad. :param title: Screen title. Defaults to ``"Setup"``. :returns: An :class:`AskNumber` step, ready to drop into a ``seq([...])``. .. py:class:: ParamSet Base for a group of parameter declarations. Subclassing is optional — ``__set_name__`` works on any class — but it provides :meth:`all` for iterating declared parameters (handy for tests or a "reset everything" screen). Example:: class P(ParamSet): cube_offset = NumberParam(default=0.0, unit="cm") ramp_speed = NumberParam(default=0.5) .. py:method:: all() -> list[NumberParam] :classmethod: Return every :class:`NumberParam` declared on this class. .. py:class:: AskNumber(param: NumberParam, prompt: str, *, title: str = 'Setup') Bases: :py:obj:`raccoon.ui.step.UIStep` Setup step: ask the operator for a numeric parameter via the screen. Prefer ``param.ask("...")`` or the :func:`ask` factory over constructing this directly. Shows a numeric keypad pre-filled with the parameter's current value and stores whatever the operator confirms back into the parameter. Under ``--no-calibrate`` the screen is skipped entirely and the existing (persisted or default) value is kept, matching the behaviour of the calibration steps. .. py:function:: ask(param: NumberParam, prompt: str, *, title: str = 'Setup') -> step.base.Step Ask the operator for a numeric parameter during setup. Free-function form of :meth:`NumberParam.ask`; both build the same step. Some prefer ``ask(P.cube_offset, "...")`` over ``P.cube_offset.ask("...")``. :param param: The :class:`NumberParam` to fill in. :param prompt: Question shown above the numeric keypad. :param title: Screen title. Defaults to ``"Setup"``. :returns: An :class:`AskNumber` step, ready to drop into a ``seq([...])``. Example:: from raccoon import ask, NumberParam, ParamSet class P(ParamSet): cube_offset = NumberParam(default=0.0, unit="cm") seq([ask(P.cube_offset, "Cube-row offset")]) .. py:data:: ParamPrompt .. py:class:: ConfigureMission(params: list[ParamPrompt], *, title: str = 'Setup') Bases: :py:obj:`raccoon.ui.step.UIStep` Setup step: enter a game-table / side profile, then fill scoped params. Prefer the :func:`configure_mission` factory over constructing this directly. At runtime it drives the full profile flow: 1. shows one keypad screen where the operator types the table number and taps ``A`` / ``B`` (e.g. ``1B``), 2. activates that ``(table, side)`` profile so every scoped parameter now resolves under it, 3. if a *complete* set of saved values already exists for the profile, offers to reuse them (asking first) and skips the questions, 4. otherwise asks for each parameter in turn and stores it under the profile. Under ``--no-calibrate`` the UI is skipped entirely: the last persisted profile is restored (so scoped values read back from YAML) and no questions are asked. .. py:function:: configure_mission(params: list[ParamPrompt], *, title: str = 'Setup') -> step.base.Step Enter a game-table / side profile, then fill the scoped parameters. One-stop setup step for missions whose parameters differ per physical game table and starting side. It shows a single keypad screen where the operator types the table number and taps ``A`` / ``B`` (e.g. ``1B``), activates that profile (so every ``scoped=True`` parameter now reads and writes under it), and then either reuses a complete set of previously saved values (after confirming) or asks for each parameter in turn. Prerequisites: The parameters passed here should be declared with ``scoped=True`` (and usually ``persist=True`` so saved values survive a restart). Un-scoped parameters are unaffected and can still be asked with :func:`ask`. :param params: ``(param, prompt)`` pairs — the scoped parameters to fill and the question shown for each. :param title: Screen title for every screen in the flow. Defaults to ``"Setup"``. :returns: A :class:`ConfigureMission` step, ready to drop into a ``seq([...])``. Example:: from raccoon import NumberParam, ParamSet, configure_mission class P(ParamSet): cube_offset = NumberParam(default=0.0, unit="cm", scoped=True, persist=True) ramp_speed = NumberParam(default=0.5, scoped=True, persist=True) seq( [ configure_mission( params=[ (P.cube_offset, "Cube-row offset"), (P.ramp_speed, "Ramp approach speed"), ], ), ] )