host.controller_web_editor.routes.config

JSON <-> YAML round-trip for FullConfig.

The frontend never parses YAML. Server-side, we hand off to utils.controller.config_io so loading, saving, default-omission, and legacy-format migration stay in one place.

 1"""JSON <-> YAML round-trip for FullConfig.
 2
 3The frontend never parses YAML.  Server-side, we hand off to
 4:mod:`utils.controller.config_io` so loading, saving, default-omission,
 5and legacy-format migration stay in one place.
 6"""
 7
 8from __future__ import annotations
 9
10from dataclasses import asdict
11from pathlib import Path
12
13from utils.controller.config_io import (
14    CONFIG_VERSION,
15    _dict_to_action,
16    _dict_to_controller,
17    load_config,
18    save_config,
19)
20from utils.controller.model import (
21    ActionDefinition,
22    ControllerConfig,
23    EventTriggerMode,
24    FullConfig,
25    InputType,
26)
27
28
29def config_to_json(config: FullConfig) -> dict:
30    """Serialize FullConfig to JSON-safe primitives.
31
32    Enums become their string values; sets become sorted lists.
33    """
34    actions = {}
35    for qname, action in config.actions.items():
36        d = asdict(action)
37        d["input_type"] = action.input_type.value
38        d["trigger_mode"] = action.trigger_mode.value
39        actions[qname] = d
40
41    controllers = {}
42    for port, ctrl in config.controllers.items():
43        controllers[str(port)] = {
44            "port": ctrl.port,
45            "name": ctrl.name,
46            "controller_type": ctrl.controller_type,
47            "bindings": {k: list(v) for k, v in ctrl.bindings.items()},
48        }
49
50    return {
51        "version": config.version or CONFIG_VERSION,
52        "actions": actions,
53        "controllers": controllers,
54        "empty_groups": sorted(config.empty_groups),
55    }
56
57
58def config_from_json(payload: dict) -> FullConfig:
59    """Inverse of :func:`config_to_json`."""
60    actions: dict[str, ActionDefinition] = {}
61    for qname, raw in (payload.get("actions") or {}).items():
62        group = raw.get("group", "general")
63        name = raw.get("name") or qname.split(".", 1)[-1]
64        action = _dict_to_action(name, raw, group=group)
65        actions[action.qualified_name] = action
66
67    controllers: dict[int, ControllerConfig] = {}
68    for port_str, raw in (payload.get("controllers") or {}).items():
69        port = int(port_str)
70        controllers[port] = _dict_to_controller(port, raw)
71
72    return FullConfig(
73        actions=actions,
74        controllers=controllers,
75        empty_groups=set(payload.get("empty_groups") or []),
76        version=payload.get("version") or CONFIG_VERSION,
77    )
78
79
80def load_as_json(path: Path) -> dict:
81    """Read a YAML config file and return JSON-safe primitives."""
82    return config_to_json(load_config(path))
83
84
85def save_from_json(path: Path, payload: dict) -> None:
86    """Write a JSON payload back to a YAML config file."""
87    save_config(config_from_json(payload), path)
88
89
90# Re-exported for callers that want to inspect enum vocabularies.
91__all__ = [
92    "EventTriggerMode",
93    "InputType",
94    "config_from_json",
95    "config_to_json",
96    "load_as_json",
97    "save_from_json",
98]
class EventTriggerMode(enum.Enum):
43class EventTriggerMode(Enum):
44    """How a button/analog action is triggered or shaped.
45
46    Button modes control when commands fire via ``commands2.button.Trigger``
47    bindings (not to be confused with the Xbox "trigger" — the analog
48    left_trigger / right_trigger inputs).  Each mode maps to a Trigger
49    binding method: ON_TRUE -> ``.onTrue()``, WHILE_TRUE -> ``.whileTrue()``,
50    etc.
51
52    Analog modes control how the analog value is shaped/curved before
53    it reaches the subsystem.
54    """
55    # Button modes
56    ON_TRUE = "on_true"
57    ON_FALSE = "on_false"
58    WHILE_TRUE = "while_true"
59    WHILE_FALSE = "while_false"
60    TOGGLE_ON_TRUE = "toggle_on_true"
61    # Analog response curves
62    RAW = "raw"
63    SCALED = "scaled"
64    SQUARED = "squared"
65    SEGMENTED = "segmented"
66    SPLINE = "spline"

How a button/analog action is triggered or shaped.

Button modes control when commands fire via commands2.button.Trigger bindings (not to be confused with the Xbox "trigger" — the analog left_trigger / right_trigger inputs). Each mode maps to a Trigger binding method: ON_TRUE -> .onTrue(), WHILE_TRUE -> .whileTrue(), etc.

Analog modes control how the analog value is shaped/curved before it reaches the subsystem.

ON_TRUE = <EventTriggerMode.ON_TRUE: 'on_true'>
ON_FALSE = <EventTriggerMode.ON_FALSE: 'on_false'>
WHILE_TRUE = <EventTriggerMode.WHILE_TRUE: 'while_true'>
WHILE_FALSE = <EventTriggerMode.WHILE_FALSE: 'while_false'>
TOGGLE_ON_TRUE = <EventTriggerMode.TOGGLE_ON_TRUE: 'toggle_on_true'>
RAW = <EventTriggerMode.RAW: 'raw'>
SCALED = <EventTriggerMode.SCALED: 'scaled'>
SQUARED = <EventTriggerMode.SQUARED: 'squared'>
SEGMENTED = <EventTriggerMode.SEGMENTED: 'segmented'>
SPLINE = <EventTriggerMode.SPLINE: 'spline'>
class InputType(enum.Enum):
86class InputType(Enum):
87    """Type of controller input an action is designed for.
88
89    BOOLEAN_TRIGGER converts an analog axis (e.g. left_trigger) into a
90    boolean using a threshold comparison — not related to
91    ``commands2.button.Trigger``.
92    """
93    BUTTON = "button"
94    ANALOG = "analog"
95    OUTPUT = "output"
96    BOOLEAN_TRIGGER = "boolean_trigger"
97    VIRTUAL_ANALOG = "virtual_analog"

Type of controller input an action is designed for.

BOOLEAN_TRIGGER converts an analog axis (e.g. left_trigger) into a boolean using a threshold comparison — not related to commands2.button.Trigger.

BUTTON = <InputType.BUTTON: 'button'>
ANALOG = <InputType.ANALOG: 'analog'>
OUTPUT = <InputType.OUTPUT: 'output'>
BOOLEAN_TRIGGER = <InputType.BOOLEAN_TRIGGER: 'boolean_trigger'>
VIRTUAL_ANALOG = <InputType.VIRTUAL_ANALOG: 'virtual_analog'>
def config_from_json(payload: dict) -> utils.controller.FullConfig:
59def config_from_json(payload: dict) -> FullConfig:
60    """Inverse of :func:`config_to_json`."""
61    actions: dict[str, ActionDefinition] = {}
62    for qname, raw in (payload.get("actions") or {}).items():
63        group = raw.get("group", "general")
64        name = raw.get("name") or qname.split(".", 1)[-1]
65        action = _dict_to_action(name, raw, group=group)
66        actions[action.qualified_name] = action
67
68    controllers: dict[int, ControllerConfig] = {}
69    for port_str, raw in (payload.get("controllers") or {}).items():
70        port = int(port_str)
71        controllers[port] = _dict_to_controller(port, raw)
72
73    return FullConfig(
74        actions=actions,
75        controllers=controllers,
76        empty_groups=set(payload.get("empty_groups") or []),
77        version=payload.get("version") or CONFIG_VERSION,
78    )

Inverse of config_to_json().

def config_to_json(config: utils.controller.FullConfig) -> dict:
30def config_to_json(config: FullConfig) -> dict:
31    """Serialize FullConfig to JSON-safe primitives.
32
33    Enums become their string values; sets become sorted lists.
34    """
35    actions = {}
36    for qname, action in config.actions.items():
37        d = asdict(action)
38        d["input_type"] = action.input_type.value
39        d["trigger_mode"] = action.trigger_mode.value
40        actions[qname] = d
41
42    controllers = {}
43    for port, ctrl in config.controllers.items():
44        controllers[str(port)] = {
45            "port": ctrl.port,
46            "name": ctrl.name,
47            "controller_type": ctrl.controller_type,
48            "bindings": {k: list(v) for k, v in ctrl.bindings.items()},
49        }
50
51    return {
52        "version": config.version or CONFIG_VERSION,
53        "actions": actions,
54        "controllers": controllers,
55        "empty_groups": sorted(config.empty_groups),
56    }

Serialize FullConfig to JSON-safe primitives.

Enums become their string values; sets become sorted lists.

def load_as_json(path: pathlib.Path) -> dict:
81def load_as_json(path: Path) -> dict:
82    """Read a YAML config file and return JSON-safe primitives."""
83    return config_to_json(load_config(path))

Read a YAML config file and return JSON-safe primitives.

def save_from_json(path: pathlib.Path, payload: dict) -> None:
86def save_from_json(path: Path, payload: dict) -> None:
87    """Write a JSON payload back to a YAML config file."""
88    save_config(config_from_json(payload), path)

Write a JSON payload back to a YAML config file.