"""
AGIBOT X2 interface specification.

IMPORTANT: every constant in this file was read back from the robot itself
(10.255.254.84, PC2 / Jetson Orin) by introspecting the installed `aimdk_msgs`
package and the live ROS 2 graph - not copied from the published documentation.
Where the two disagree, the robot wins. Notable differences found:

  * McAction has 18 modes, not 5. PASSIVE_DEFAULT (1) and ZERO_TORQUE_DEFAULT
    (4) are distinct; the docs conflated them.
  * McControlArea is a BITMASK (LEFT_HAND=1, RIGHT_HAND=2, HEAD=4, WAIST=8),
    which is why the docs' "area 3" (both arms) and "area 11" (whole body)
    work out - 3 = 1|2 and 11 = 1|2|8.
  * Several documented preset motion IDs (1007, 1010, 1011, 3017, 3024, 3025,
    3031) do not exist in the firmware enum. The real list is below.
  * PmuState spells its fan field `fan_pecentage` (sic) and reports charge in
    `battery_remaining_capacity_percentage`; `battery_remaining_capacity` is
    mAh, not percent.
  * Joint names carry a `_joint` suffix.
  * This unit exposes SIX working camera feeds and a chest LiDAR. An earlier
    revision of this file claimed one camera, no rear camera and an unreachable
    Orbbec; all three were wrong. Corrected by subscribing to each topic and
    decoding a real frame off it - see CAMERA_INVENTORY. SLAM command topics
    are still genuinely absent.
"""

from __future__ import annotations

# --------------------------------------------------------------------------
# Network
# --------------------------------------------------------------------------
# PC1 runs motion control and must never be used as a build/run host.
PC1_MOTION_CONTROL_IP = "10.0.1.40"
# PC2 (this robot's development unit) as seen on the internal develop0 network.
PC2_DEVELOP_IP = "10.0.1.41"

DEFAULT_ROS_DOMAIN_ID = 0

# Port the on-robot agent listens on.
AGENT_PORT = 8781

# Ports probed when scanning a subnet for the robot.
DISCOVERY_PORTS = [AGENT_PORT, 22, 8080, 9090]


# --------------------------------------------------------------------------
# Sensor topics  (verified present on the live graph)
# --------------------------------------------------------------------------
TOPIC_IMU_CHEST = "/aima/hal/imu/chest/state"
TOPIC_IMU_TORSO = "/aima/hal/imu/torso/state"
TOPIC_TOUCH_HEAD = "/aima/hal/sensor/touch_head"
TOPIC_MODULE_INFO = "/aima/hal/sensor/module_info"

# Every feed is on demand - `always_on` is gone. Nothing is subscribed on the
# robot until the operator switches that feed on in the Vision tab, and the
# agent drops the lot when the last browser disconnects. A single frame here is
# 170-430 KB; holding six of them open for a tab nobody is looking at competes
# with the robot's own traffic on the same Wi-Fi.
CAMERAS = [
    {
        "key": "rgb_head_front_center",
        "label": "Head camera (front)",
        "kind": "rgb",
        "rate_hz": 10,
        "topic": "/aima/hal/sensor/rgb_head_front_center/rgb_image/compressed",
        "info_topic": "/aima/hal/sensor/rgb_head_front_center/camera_info",
        "flip": False,
    },
    {
        "key": "rgb_head_rear",
        "label": "Head camera (rear)",
        "kind": "rgb",
        "rate_hz": 10,
        "topic": "/aima/hal/sensor/rgb_head_rear/rgb_image/compressed",
        "info_topic": "/aima/hal/sensor/rgb_head_rear/camera_info",
        "flip": False,
    },
    {
        "key": "rgbd_head_front",
        "label": "RGB-D colour (Orbbec)",
        "kind": "rgb",
        "rate_hz": 10,
        "topic": "/aima/hal/sensor/rgbd_head_front/rgb_image/compressed",
        "info_topic": "/aima/hal/sensor/rgbd_head_front/rgb_camera_info",
        "flip": True,
        "note": "This module is mounted upside down, so the feed is rotated 180 by "
                "default. Use the button below if yours is not.",
    },
    {
        "key": "depth_front",
        "label": "Depth (colourised)",
        "kind": "depth",
        "rate_hz": 14,
        "topic": "/camera/depth/image_raw/compressedDepth",
        "flip": True,
        "note": "16-bit millimetre map, rendered to a JET ramp over 0-4.5 m on the robot "
                "and rotated 180 like the colour feed it shares a module with. "
                "Black is 'no return', not 'very close'.",
    },
    {
        "key": "stereo_head_front_left",
        "label": "Stereo left",
        "kind": "rgb",
        "rate_hz": 10,
        "topic": "/aima/hal/sensor/stereo_head_front_left/rgb_image/compressed",
        "info_topic": "/aima/hal/sensor/stereo_head_front_left/camera_info",
        "flip": False,
    },
    {
        "key": "stereo_head_front_right",
        "label": "Stereo right",
        "kind": "rgb",
        "rate_hz": 10,
        "topic": "/aima/hal/sensor/stereo_head_front_right/rgb_image/compressed",
        "info_topic": "/aima/hal/sensor/stereo_head_front_right/camera_info",
        "flip": False,
    },
    {
        "key": "perception_input",
        "label": "Perception input",
        "kind": "debug",
        "rate_hz": 10,
        "topic": "/mono_perception/debug/input_image",
        "flip": False,
        "note": "Only carries frames while the mono-perception module is running.",
    },
    {
        "key": "perception_seg",
        "label": "Segmentation mask",
        "kind": "debug",
        "rate_hz": 10,
        "topic": "/mono_perception/debug/seg_mask",
        "flip": False,
        "note": "Only carries frames while the mono-perception module is running.",
    },
    {
        "key": "perception_lines",
        "label": "Line colour map",
        "kind": "debug",
        "rate_hz": 10,
        "topic": "/mono_perception/debug/line_color_map",
        "flip": False,
        "note": "Only carries frames while the mono-perception module is running.",
    },
]

# What this unit actually has, established the only way that settles it: by
# subscribing to each topic and decoding a frame. A previous revision inferred
# the inventory from the v4l2 device map and the em app list, and got it wrong
# three ways - the rear camera and the Orbbec feeds are on the shared ROS graph
# and publish perfectly well to an ordinary `agi` subscriber. Reading the device
# tree tells you which sensors the SoC enumerates, not which topics carry data.
CAMERA_INVENTORY = [
    {
        "name": "Head front (imx900c)",
        "detail": "MIPI/GMSL sensor 2-0039, 2064x1552 12-bit Bayer, ISP-processed "
                  "and published as 2688x1944 JPEG at ~10 Hz.",
        "status": "live",
        "where": "/aima/hal/sensor/rgb_head_front_center/rgb_image/compressed",
    },
    {
        "name": "Head rear",
        "detail": "Fitted and publishing 2064x1552 JPEG at ~10 Hz, mounted upright "
                  "(no rotation needed). Confirmed by decoding a frame.",
        "status": "live",
        "where": "/aima/hal/sensor/rgb_head_rear/rgb_image/compressed",
    },
    {
        "name": "Orbbec Gemini 335 (RGB-D)",
        "detail": "Colour and depth both reachable from the agi account. Depth is a "
                  "16-bit millimetre map published as compressedDepth at ~14 Hz, "
                  "topping out around 4.2 m. The module is mounted upside down - both "
                  "its feeds are rotated 180 by default.",
        "status": "live",
        "where": "/aima/hal/sensor/rgbd_head_front/* and /camera/depth/image_raw/compressedDepth",
    },
    {
        "name": "Stereo pair (front left / right)",
        "detail": "Both eyes publish JPEG at ~10 Hz. Useful side by side for depth "
                  "the RGB-D sensor misses on reflective surfaces.",
        "status": "live",
        "where": "/aima/hal/sensor/stereo_head_front_{left,right}/rgb_image/compressed",
    },
    {
        "name": "Perception debug views",
        "detail": "Input image, segmentation mask and line colour map. Real topics, "
                  "but only carry frames while the mono-perception module is running.",
        "status": "intermittent",
        "where": "/mono_perception/debug/*",
    },
]

# --------------------------------------------------------------------------
# Chest LiDAR
# --------------------------------------------------------------------------
# Live on this unit at 2 Hz. Field layout read off the topic rather than
# assumed: x/y/z float32 at offsets 0/4/8, intensity float32 at 16, ring uint16
# at 20, timestamp float64 at 24, point_step 32. A scan is ~25.5k points
# (816 KB), decimated on the robot before it crosses the network.
LIDAR = {
    "key": "lidar_chest_front",
    "label": "Chest LiDAR",
    "topic": "/aima/hal/sensor/lidar_chest_front/lidar_pointcloud_down_sampling",
    "status_topic": "/aima/hal/sensor/lidar_chest_front/lidar_status",
    "type": "sensor_msgs/PointCloud2",
    "rate_hz": 2,
    "frame": "lidar_chest_front",
    "max_points": 4000,
}

SENSOR_TOPICS = [
    {"topic": TOPIC_IMU_CHEST, "type": "sensor_msgs/Imu", "rate_hz": 500, "label": "Chest IMU"},
    {"topic": TOPIC_IMU_TORSO, "type": "sensor_msgs/Imu", "rate_hz": 500, "label": "Torso IMU"},
    {"topic": TOPIC_TOUCH_HEAD, "type": "aimdk_msgs/TouchState", "rate_hz": 100, "label": "Head touch"},
    {"topic": "/aima/hal/pmu/state", "type": "aimdk_msgs/PmuState", "rate_hz": 1, "label": "Power management"},
    {"topic": "/aima/hal/joint/head/state", "type": "aimdk_msgs/JointStateArray", "rate_hz": 100, "label": "Head joints"},
    {"topic": "/aima/hal/joint/waist/state", "type": "aimdk_msgs/JointStateArray", "rate_hz": 100, "label": "Waist joints"},
    {"topic": "/aima/hal/joint/arm/state", "type": "aimdk_msgs/JointStateArray", "rate_hz": 100, "label": "Arm joints"},
    {"topic": "/aima/hal/joint/leg/state", "type": "aimdk_msgs/JointStateArray", "rate_hz": 100, "label": "Leg joints"},
    {"topic": "/aima/hal/joint/hand/state", "type": "aimdk_msgs/HandStateArray", "rate_hz": 30, "label": "Hand state"},
    {"topic": CAMERAS[0]["topic"], "type": "sensor_msgs/CompressedImage", "rate_hz": 10, "label": "Head camera"},
    {"topic": "/aima/mc/leg_odometry", "type": "nav_msgs/Odometry", "rate_hz": 50, "label": "Leg odometry"},
    {"topic": "/face_ui_proxy/status", "type": "aimdk_msgs/FaceEmojiStatus", "rate_hz": 1, "label": "Face status"},
]

# Head touch reports 8 independent zones.
TOUCH_ZONE_COUNT = 8


# --------------------------------------------------------------------------
# Power management  (exact PmuState field names)
# --------------------------------------------------------------------------
TOPIC_PMU_STATE = "/aima/hal/pmu/state"
PMU_RATE_HZ = 1.0

PMU_FIELDS = {
    "battery_pct": "battery_remaining_capacity_percentage",
    "battery_voltage": "battery_voltage",
    "battery_pack_voltage": "battery_pack_voltage",
    "battery_current": "battery_current",
    "battery_temp": "battery_temperature",
    "battery_cycles": "battery_cycle_count",
    "battery_capacity_mah": "battery_remaining_capacity",
    "battery_power": "battery_output_power",
    "pmu_temp": "pmu_temperature",
    "fan_rpm": "fan_speed",
    "fan_pct": "fan_pecentage",          # firmware spells it this way
}

# (display label, voltage field, current field, nominal volts)
PMU_RAILS = [
    {"key": "bus_48v", "label": "48 V bus", "voltage": "bus_48v_voltage",
     "current": "bus_48v_current", "nominal": 48.0},
    {"key": "output_48v", "label": "48 V output", "voltage": "output_48v_voltage",
     "current": "output_48v_current", "nominal": 48.0},
    {"key": "output_12v", "label": "12 V output", "voltage": "output_12v_voltage",
     "current": "output_12v_current", "nominal": 12.0},
    {"key": "head_power", "label": "Head power", "voltage": "head_power_voltage",
     "current": "head_power_current", "nominal": 24.0},
    {"key": "orin", "label": "Orin NX", "voltage": "orin_voltage",
     "current": "orin_current", "nominal": 19.0},
    {"key": "rk3588", "label": "RK3588", "voltage": "rk3588_voltage",
     "current": "rk3588_current", "nominal": 12.0},
    {"key": "fan", "label": "Fan rail", "voltage": "fan_voltage",
     "current": None, "nominal": 12.0},
    {"key": "bus_48v_pmos", "label": "48 V PMOS", "voltage": "bus_48v_pmos_voltage",
     "current": None, "nominal": 48.0},
]

PMU_INFO_FIELDS = [
    ("bms_manufacturer", "BMS manufacturer"),
    ("bms_serial_number", "BMS serial"),
    ("bms_hardware_version", "BMS hardware"),
    ("bms_software_version", "BMS software"),
    ("pmu_software_version", "PMU software"),
    ("pmu_hardware_version", "PMU hardware"),
    ("pmu_protocol_version", "PMU protocol"),
]


# --------------------------------------------------------------------------
# Motion modes  (aimdk_msgs/msg/McAction constants, read from the robot)
# --------------------------------------------------------------------------
SRV_GET_MC_ACTION = "/aimdk_5Fmsgs/srv/GetMcAction"
SRV_SET_MC_ACTION = "/aimdk_5Fmsgs/srv/SetMcAction"

MC_MODES = [
    # -- safety / low level -------------------------------------------------
    {"id": "PASSIVE_DEFAULT", "value": 1, "label": "Passive", "group": "basic",
     "desc": "Joints released, no holding force. The robot must be supported.",
     "danger": True},
    {"id": "ZERO_TORQUE_DEFAULT", "value": 4, "label": "Zero torque", "group": "basic",
     "desc": "Explicit zero-torque command. The robot will collapse if free-standing.",
     "danger": True},
    {"id": "SOFT_EMERGENCY_STOP", "value": 2, "label": "Soft E-stop", "group": "basic",
     "desc": "Controlled emergency stop.", "danger": True},
    {"id": "DAMPING_DEFAULT", "value": 3, "label": "Damping", "group": "basic",
     "desc": "Joints hold damping - the safe resting state.", "danger": False},

    # -- position control ---------------------------------------------------
    {"id": "JOINT_DEFAULT", "value": 100, "label": "Joint control", "group": "joint",
     "desc": "Position-controlled joints. Required for direct joint commands.",
     "danger": False},
    {"id": "JOINT_FREEZE", "value": 101, "label": "Joint freeze", "group": "joint",
     "desc": "Hold the current joint positions.", "danger": False},

    # -- standing -----------------------------------------------------------
    {"id": "STAND_DEFAULT", "value": 200, "label": "Stable stand", "group": "stand",
     "desc": "Active balance. Prerequisite for preset motions and walking.",
     "danger": False},
    {"id": "STAND_BODY_CONTROL", "value": 201, "label": "Body control", "group": "stand",
     "desc": "Standing with body pose control enabled.", "danger": False},

    # -- locomotion ---------------------------------------------------------
    {"id": "LOCOMOTION_DEFAULT", "value": 300, "label": "Walk", "group": "locomotion",
     "desc": "Normal walking gait.", "danger": False},
    {"id": "LOCOMOTION_STEP", "value": 302, "label": "Step", "group": "locomotion",
     "desc": "Stepping gait.", "danger": False},
    {"id": "RUN_DEFAULT", "value": 301, "label": "Run", "group": "locomotion",
     "desc": "Running gait. Needs clear space.", "danger": True},
    {"id": "ASCEND_STAIRS", "value": 2006, "label": "Ascend stairs", "group": "locomotion",
     "desc": "Stair-climbing gait.", "danger": True},
    {"id": "DESCEND_STAIRS", "value": 2008, "label": "Descend stairs", "group": "locomotion",
     "desc": "Stair-descending gait.", "danger": True},

    # -- postures -----------------------------------------------------------
    {"id": "STAND_UP_DEFAULT", "value": 2005, "label": "Stand up", "group": "posture",
     "desc": "Rise to standing.", "danger": False},
    {"id": "SIT_DOWN_DEFAULT", "value": 2000, "label": "Sit down", "group": "posture",
     "desc": "Sit down from standing.", "danger": False},
    {"id": "CROUCH_DOWN_DEFAULT", "value": 2002, "label": "Crouch", "group": "posture",
     "desc": "Crouch down.", "danger": False},
    {"id": "LIE_DOWN_DEFAULT", "value": 2004, "label": "Lie down", "group": "posture",
     "desc": "Lie down.", "danger": False},

    # -- external -----------------------------------------------------------
    {"id": "VR_REMOTE_CONTROLLER", "value": 400, "label": "VR teleop", "group": "external",
     "desc": "Hand control to the VR teleoperation bridge.", "danger": False},
]

MC_MODE_GROUPS = [
    {"key": "basic", "label": "Basic / safety"},
    {"key": "joint", "label": "Joint control"},
    {"key": "stand", "label": "Standing"},
    {"key": "locomotion", "label": "Locomotion"},
    {"key": "posture", "label": "Posture"},
    {"key": "external", "label": "External control"},
]

MC_MODE_BY_ID = {m["id"]: m for m in MC_MODES}
MC_MODE_BY_VALUE = {m["value"]: m for m in MC_MODES}
MC_MODE_IDS = [m["id"] for m in MC_MODES]

# Modes in which the robot will accept a velocity command.
DRIVEABLE_MODES = {"LOCOMOTION_DEFAULT", "LOCOMOTION_STEP", "RUN_DEFAULT",
                   "STAND_DEFAULT", "STAND_BODY_CONTROL"}

# Modes in which direct joint commands make sense.
JOINT_CONTROL_MODES_ALLOWED = {"JOINT_DEFAULT", "JOINT_FREEZE"}

MC_ACTION_STATUS = {0: "Idle", 100: "Running", 200: "Transition"}

COMMON_STATE = {
    0: "Unknown", 1: "Success", 2: "Failure", 3: "Aborted", 4: "Timeout",
    5: "Invalid", 6: "In manual", 100: "Not ready", 200: "Pending",
    300: "Created", 400: "Running",
}


# --------------------------------------------------------------------------
# Locomotion
# --------------------------------------------------------------------------
TOPIC_LOCOMOTION_VELOCITY = "/aima/mc/locomotion/velocity"
TOPIC_LEG_ODOMETRY = "/aima/mc/leg_odometry"
TOPIC_BODY_POSE = "/aima/mc/body_pose"

VELOCITY_THRESHOLDS = {"forward": 0.09, "lateral": 0.60, "angular": 0.03}

VELOCITY_LIMITS = {
    "forward": {"min": -0.6, "max": 0.8, "step": 0.01, "unit": "m/s"},
    "lateral": {"min": -0.7, "max": 0.7, "step": 0.01, "unit": "m/s"},
    "angular": {"min": -0.8, "max": 0.8, "step": 0.01, "unit": "rad/s"},
}

LOCOMOTION_PUBLISH_HZ = 20
LOCOMOTION_DEADMAN_S = 0.5


# --------------------------------------------------------------------------
# Input source arbitration
# --------------------------------------------------------------------------
SRV_GET_CURRENT_INPUT_SOURCE = "/aimdk_5Fmsgs/srv/GetCurrentInputSource"
SRV_SET_MC_INPUT_SOURCE = "/aimdk_5Fmsgs/srv/SetMcInputSource"

INPUT_SOURCE_ACTION = {
    "ADD": 1001, "MODIFY": 1002, "DELETE": 1003, "ENABLE": 2001, "DISABLE": 2002,
}

BUILTIN_INPUT_SOURCES = [
    {"name": "rc", "priority": 80, "timeout": 1000, "desc": "Handheld remote controller"},
    {"name": "vr", "priority": 70, "timeout": 1000, "desc": "VR teleoperation"},
    {"name": "app_proxy", "priority": 60, "timeout": 1000, "desc": "Mobile app"},
    {"name": "interaction", "priority": 50, "timeout": 1000, "desc": "Interaction module"},
    {"name": "pnc", "priority": 40, "timeout": 1000, "desc": "Planning and control"},
]

DASHBOARD_INPUT_SOURCE = {"name": "x2_dashboard", "priority": 30, "timeout": 1000}


# --------------------------------------------------------------------------
# Preset motions  (aimdk_msgs/msg/McPresetMotion constants, read from robot)
# McControlArea is a bitmask: LEFT_HAND=1 RIGHT_HAND=2 HEAD=4 WAIST=8
# --------------------------------------------------------------------------
SRV_SET_PRESET_MOTION = "/aimdk_5Fmsgs/srv/SetMcPresetMotion"

AREA_LEFT_HAND = 1
AREA_RIGHT_HAND = 2
AREA_BOTH_HANDS = 3
AREA_HEAD = 4
AREA_WAIST = 8
AREA_BODY = 11          # both hands + waist

MC_CONTROL_AREAS = {
    0: "None", 1: "Left arm", 2: "Right arm", 3: "Both arms",
    4: "Head", 8: "Waist", 11: "Whole body",
}

PRESET_MOTIONS = [
    # -- greeting -----------------------------------------------------------
    {"key": "wave_right", "label": "Wave", "side": "right", "motion": 1002, "area": AREA_RIGHT_HAND, "group": "greeting", "enum": "WAVE_HAND"},
    {"key": "wave_left", "label": "Wave", "side": "left", "motion": 1002, "area": AREA_LEFT_HAND, "group": "greeting", "enum": "WAVE_HAND"},
    {"key": "shake_right", "label": "Handshake", "side": "right", "motion": 1003, "area": AREA_RIGHT_HAND, "group": "greeting", "enum": "SHAKE_HAND"},
    {"key": "shake_left", "label": "Handshake", "side": "left", "motion": 1003, "area": AREA_LEFT_HAND, "group": "greeting", "enum": "SHAKE_HAND"},
    {"key": "raise_right", "label": "Raise hand", "side": "right", "motion": 1001, "area": AREA_RIGHT_HAND, "group": "greeting", "enum": "RAISE_HAND"},
    {"key": "raise_left", "label": "Raise hand", "side": "left", "motion": 1001, "area": AREA_LEFT_HAND, "group": "greeting", "enum": "RAISE_HAND"},
    {"key": "raise_both", "label": "Raise both hands", "side": "both", "motion": 1001, "area": AREA_BOTH_HANDS, "group": "greeting", "enum": "RAISE_HAND"},
    {"key": "salute_right", "label": "Salute", "side": "right", "motion": 1013, "area": AREA_RIGHT_HAND, "group": "greeting", "enum": "SALUTE"},
    {"key": "salute_left", "label": "Salute", "side": "left", "motion": 1013, "area": AREA_LEFT_HAND, "group": "greeting", "enum": "SALUTE"},
    {"key": "turn_wave", "label": "Turn and wave", "side": "body", "motion": 2001, "area": AREA_BODY, "group": "greeting", "enum": "TURN_WAVE_HAND"},
    {"key": "bow", "label": "Bow", "side": "body", "motion": 3001, "area": AREA_BODY, "group": "greeting", "enum": "INTERACTION_BOW"},

    # -- expressive ---------------------------------------------------------
    {"key": "kiss_right", "label": "Flying kiss", "side": "right", "motion": 1004, "area": AREA_RIGHT_HAND, "group": "expressive", "enum": "FLYING_KISS_HAND"},
    {"key": "kiss_left", "label": "Flying kiss", "side": "left", "motion": 1004, "area": AREA_LEFT_HAND, "group": "expressive", "enum": "FLYING_KISS_HAND"},
    {"key": "blowkiss", "label": "Blow kiss", "side": "body", "motion": 3012, "area": AREA_BODY, "group": "expressive", "enum": "INTERACTION_BLOWKISS"},
    {"key": "sweatheart", "label": "Heart", "side": "body", "motion": 3004, "area": AREA_BODY, "group": "expressive", "enum": "INTERACTION_SWEATHEART"},
    {"key": "like", "label": "Thumbs up", "side": "body", "motion": 3002, "area": AREA_BODY, "group": "expressive", "enum": "INTERACTION_LIKE"},
    {"key": "ye", "label": "Peace sign", "side": "body", "motion": 3003, "area": AREA_BODY, "group": "expressive", "enum": "INTERACTION_YE"},
    {"key": "hug", "label": "Hug", "side": "body", "motion": 3008, "area": AREA_BODY, "group": "expressive", "enum": "INTERACTION_HUG"},
    {"key": "cheer", "label": "Cheer", "side": "body", "motion": 3011, "area": AREA_BODY, "group": "expressive", "enum": "INTERACTION_CHEER"},
    {"key": "sad", "label": "Sad", "side": "body", "motion": 3006, "area": AREA_BODY, "group": "expressive", "enum": "INTERACTION_SAD"},
    {"key": "speak", "label": "Speaking gesture", "side": "body", "motion": 3016, "area": AREA_BODY, "group": "expressive", "enum": "INTERACTION_SPEAK"},

    # -- gesture ------------------------------------------------------------
    {"key": "clap", "label": "Clap", "side": "both", "motion": 1008, "area": AREA_BOTH_HANDS, "group": "gesture", "enum": "CLAP_HAND"},
    {"key": "hitclap", "label": "Hit clap", "side": "body", "motion": 3015, "area": AREA_BODY, "group": "gesture", "enum": "HITCLAP"},
    {"key": "fist_right", "label": "Fist", "side": "right", "motion": 1009, "area": AREA_RIGHT_HAND, "group": "gesture", "enum": "CLIPFIST"},
    {"key": "fist_left", "label": "Fist", "side": "left", "motion": 1009, "area": AREA_LEFT_HAND, "group": "gesture", "enum": "CLIPFIST"},
    {"key": "handx", "label": "Cross arms", "side": "body", "motion": 3009, "area": AREA_BODY, "group": "gesture", "enum": "INTERACTION_HANDX"},
    {"key": "chestwave", "label": "Chest wave", "side": "body", "motion": 3010, "area": AREA_BODY, "group": "gesture", "enum": "INTERACTION_CHESTWAVE"},
    {"key": "lightwave", "label": "Light wave", "side": "body", "motion": 3007, "area": AREA_BODY, "group": "gesture", "enum": "INTERACTION_LIGHTWAVE"},

    # -- performance --------------------------------------------------------
    {"key": "dance1", "label": "Bass dance 1", "side": "body", "motion": 3013, "area": AREA_BODY, "group": "performance", "enum": "INTERACTION_BASSDANCE1"},
    {"key": "dance2", "label": "Bass dance 2", "side": "body", "motion": 3014, "area": AREA_BODY, "group": "performance", "enum": "INTERACTION_BASSDANCE2"},
    {"key": "photo", "label": "Photo pose", "side": "body", "motion": 3018, "area": AREA_BODY, "group": "performance", "enum": "INTERACTION_PHOTOPOSTURE"},
    {"key": "photo3", "label": "Triple photo pose", "side": "body", "motion": 3019, "area": AREA_BODY, "group": "performance", "enum": "INTERACTION_PHOTOTRIPPLEPOSTURE"},

    # -- head ---------------------------------------------------------------
    {"key": "point_head", "label": "Point head", "side": "head", "motion": 4001, "area": AREA_HEAD, "group": "head", "enum": "POINT_HEAD"},
    {"key": "shake_head", "label": "Shake head", "side": "head", "motion": 4002, "area": AREA_HEAD, "group": "head", "enum": "SHAKE_HEAD"},
]

PRESET_GROUPS = [
    {"key": "greeting", "label": "Greeting"},
    {"key": "expressive", "label": "Expressive"},
    {"key": "gesture", "label": "Gesture"},
    {"key": "performance", "label": "Performance"},
    {"key": "head", "label": "Head"},
]


# --------------------------------------------------------------------------
# Joints  (names read from live JointStateArray messages)
# Limits: about_agibot_X2/joint_name_and_limit.html, X2 Ultra ranges.
# --------------------------------------------------------------------------
SRV_GET_ALL_JOINT_STATE = "/aimdk_5Fmsgs/srv/GetAllJointState"


def _j(name, label, lo, hi):
    return {"name": name, "label": label, "min_deg": lo, "max_deg": hi}


JOINT_GROUPS = [
    {
        "key": "head", "label": "Head",
        "command_topic": "/aima/hal/joint/head/command",
        "state_topic": "/aima/hal/joint/head/state",
        "joints": [
            _j("head_yaw_joint", "Head yaw", -20.0, 20.0),
            _j("head_pitch_joint", "Head pitch", 0.0, 0.0),
        ],
    },
    {
        "key": "waist", "label": "Waist",
        "command_topic": "/aima/hal/joint/waist/command",
        "state_topic": "/aima/hal/joint/waist/state",
        "joints": [
            _j("waist_yaw_joint", "Waist yaw", -196.5, 126.5),
            _j("waist_pitch_joint", "Waist pitch", -18.0, 18.0),
            _j("waist_roll_joint", "Waist roll", -28.0, 28.0),
        ],
    },
    {
        "key": "arm", "label": "Arms",
        "command_topic": "/aima/hal/joint/arm/command",
        "state_topic": "/aima/hal/joint/arm/state",
        "joints": [
            _j("left_shoulder_pitch_joint", "L shoulder pitch", -116.5, 176.5),
            _j("left_shoulder_roll_joint", "L shoulder roll", -3.5, 174.5),
            _j("left_shoulder_yaw_joint", "L shoulder yaw", -146.5, 146.5),
            _j("left_elbow_joint", "L elbow", -135.0, 0.0),
            _j("left_wrist_yaw_joint", "L wrist yaw", -146.5, 146.5),
            _j("left_wrist_pitch_joint", "L wrist pitch", -33.0, 33.0),
            _j("left_wrist_roll_joint", "L wrist roll", -86.5, 41.5),
            _j("right_shoulder_pitch_joint", "R shoulder pitch", -116.5, 176.5),
            _j("right_shoulder_roll_joint", "R shoulder roll", -3.5, 174.5),
            _j("right_shoulder_yaw_joint", "R shoulder yaw", -146.5, 146.5),
            _j("right_elbow_joint", "R elbow", -135.0, 0.0),
            _j("right_wrist_yaw_joint", "R wrist yaw", -146.5, 146.5),
            _j("right_wrist_pitch_joint", "R wrist pitch", -33.0, 33.0),
            _j("right_wrist_roll_joint", "R wrist roll", -86.5, 41.5),
        ],
    },
    {
        "key": "leg", "label": "Legs",
        "command_topic": "/aima/hal/joint/leg/command",
        "state_topic": "/aima/hal/joint/leg/state",
        "joints": [
            _j("left_hip_pitch_joint", "L hip pitch", -146.5, 146.5),
            _j("left_hip_roll_joint", "L hip roll", -13.5, 166.5),
            _j("left_hip_yaw_joint", "L hip yaw", -196.5, 96.5),
            _j("left_knee_joint", "L knee", 0.0, 138.0),
            _j("left_ankle_pitch_joint", "L ankle pitch", -26.0, 46.0),
            _j("left_ankle_roll_joint", "L ankle roll", -15.0, 15.0),
            _j("right_hip_pitch_joint", "R hip pitch", -146.5, 146.5),
            _j("right_hip_roll_joint", "R hip roll", -13.5, 166.5),
            _j("right_hip_yaw_joint", "R hip yaw", -196.5, 96.5),
            _j("right_knee_joint", "R knee", 0.0, 138.0),
            _j("right_ankle_pitch_joint", "R ankle pitch", -26.0, 46.0),
            _j("right_ankle_roll_joint", "R ankle roll", -15.0, 15.0),
        ],
    },
]

JOINT_GROUP_BY_KEY = {g["key"]: g for g in JOINT_GROUPS}

JOINT_CONTROL_MODES = [
    {"id": "position", "label": "Position", "unit": "rad", "desc": "Target angle."},
    {"id": "velocity", "label": "Velocity", "unit": "rad/s", "desc": "Target angular velocity."},
    {"id": "torque", "label": "Torque", "unit": "N·m", "desc": "Direct effort output."},
]


# --------------------------------------------------------------------------
# End effectors
# --------------------------------------------------------------------------
TOPIC_HAND_COMMAND = "/aima/hal/joint/hand/command"
TOPIC_HAND_STATE = "/aima/hal/joint/hand/state"
SRV_GET_HAND_TYPE = "/aimdk_5Fmsgs/srv/GetHandType"

HAND_TYPES = {
    0: "None", 1: "Nimble hands", 2: "Claw gripper",
    3: "Leisai nimble hands", 255: "Error",
}

DEXHAND_JOINTS = [
    "ThumbRoll", "ThumbAbAd", "ThumbMCP",
    "IndexAbAd", "IndexPIP", "MiddlePIP",
    "RingAbAd", "RingPIP", "PinkyAbAd", "PinkyPIP",
]

HAND_PRESETS = [
    {"key": "open", "label": "Open", "positions": [0.0] * 10},
    {"key": "close", "label": "Close", "positions": [0.6, 0.6, 1.2, 0.2, 1.4, 1.4, 0.2, 1.4, 0.2, 1.4]},
    {"key": "pinch", "label": "Pinch", "positions": [0.4, 0.5, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0]},
    {"key": "point", "label": "Point", "positions": [0.6, 0.6, 1.2, 0.0, 0.0, 1.4, 0.2, 1.4, 0.2, 1.4]},
]


# --------------------------------------------------------------------------
# Voice / audio
# --------------------------------------------------------------------------
SRV_PLAY_TTS = "/aimdk_5Fmsgs/srv/PlayTts"
SRV_GET_VOLUME = "/aimdk_5Fmsgs/srv/GetVolume"
SRV_SET_VOLUME = "/aimdk_5Fmsgs/srv/SetVolume"
SRV_GET_MUTE = "/aimdk_5Fmsgs/srv/GetMute"
SRV_SET_MUTE = "/aimdk_5Fmsgs/srv/SetMute"
TOPIC_AUDIO_OUTPUT = "/agent/process_audio_output"
TOPIC_TTS_STATUS = "/interaction/tts_status"

# aimdk_msgs/msg/TtsPriorityLevel constants, read from the robot.
TTS_PRIORITIES = [
    {"id": "SAFETY_L10", "value": 10, "label": "Safety", "desc": "Emergency alerts"},
    {"id": "WARNING_L8", "value": 8, "label": "Warning", "desc": "Hazard warnings"},
    {"id": "SYSTEM_L7", "value": 7, "label": "System", "desc": "System notices"},
    {"id": "INTERACTION_L6", "value": 6, "label": "Interaction", "desc": "User responses"},
    {"id": "MISSION_L4", "value": 4, "label": "Mission", "desc": "Task execution"},
    {"id": "SERVICE_L2", "value": 2, "label": "Service", "desc": "Proactive services"},
    {"id": "BACKGROUND_L1", "value": 1, "label": "Background", "desc": "Background logging"},
]

AUDIO_VAD_STATES = {0: "None", 1: "Speech begin", 2: "Processing", 3: "Speech end"}


# --------------------------------------------------------------------------
# Screen / face
# --------------------------------------------------------------------------
SRV_PLAY_EMOJI = "/aimdk_5Fmsgs/srv/PlayEmoji"
TOPIC_FACE_STATUS = "/face_ui_proxy/status"

EMOJI_PLAY_MODES = [{"value": 1, "label": "Once"}, {"value": 2, "label": "Loop"}]

FACE_STATUS = {0: "Idle", 1: "Start", 2: "Running", 3: "Finished", 4: "Stopped"}

EMOJIS = [
    {"id": 1, "label": "Blink", "glyph": "\U0001F60C", "group": "neutral"},
    {"id": 10, "label": "Calm (eyes 1)", "glyph": "\U0001F642", "group": "neutral"},
    {"id": 11, "label": "Calm (eyes 2)", "glyph": "\U0001F642", "group": "neutral"},
    {"id": 20, "label": "Calm (game)", "glyph": "\U0001F3AE", "group": "neutral"},
    {"id": 30, "label": "Cute 1", "glyph": "\U0001F970", "group": "neutral"},
    {"id": 31, "label": "Cute 2", "glyph": "\U0001F970", "group": "neutral"},
    {"id": 32, "label": "Cute 3", "glyph": "\U0001F970", "group": "neutral"},
    {"id": 33, "label": "Cute 4", "glyph": "\U0001F970", "group": "neutral"},
    {"id": 40, "label": "Close eyes", "glyph": "\U0001F636", "group": "neutral"},
    {"id": 50, "label": "Open eyes", "glyph": "\U0001F440", "group": "neutral"},
    {"id": 60, "label": "Bored", "glyph": "\U0001F644", "group": "neutral"},
    {"id": 80, "label": "Sleeping", "glyph": "\U0001F634", "group": "neutral"},

    {"id": 90, "label": "Happy", "glyph": "\U0001F604", "group": "positive"},
    {"id": 100, "label": "Extra happy", "glyph": "\U0001F606", "group": "positive"},
    {"id": 101, "label": "Ecstatic", "glyph": "\U0001F929", "group": "positive"},
    {"id": 150, "label": "Acting cute", "glyph": "\U0001F60A", "group": "positive"},
    {"id": 200, "label": "Adoration", "glyph": "\U0001F60D", "group": "positive"},
    {"id": 210, "label": "Extra adoring", "glyph": "\U0001F929", "group": "positive"},

    {"id": 110, "label": "Sad", "glyph": "\U0001F622", "group": "negative"},
    {"id": 120, "label": "Sympathy", "glyph": "\U0001F97A", "group": "negative"},
    {"id": 130, "label": "Confused", "glyph": "\U0001F615", "group": "negative"},
    {"id": 140, "label": "Shocked", "glyph": "\U0001F62E", "group": "negative"},
    {"id": 160, "label": "Serious", "glyph": "\U0001F610", "group": "negative"},
    {"id": 170, "label": "Thinking", "glyph": "\U0001F914", "group": "negative"},
    {"id": 180, "label": "Angry", "glyph": "\U0001F620", "group": "negative"},
    {"id": 190, "label": "Extra angry", "glyph": "\U0001F621", "group": "negative"},

    {"id": 70, "label": "Abnormal", "glyph": "⚠️", "group": "system"},
    {"id": 220, "label": "Charging", "glyph": "\U0001F50B", "group": "system"},
]

EMOJI_GROUPS = [
    {"key": "neutral", "label": "Neutral"},
    {"key": "positive", "label": "Positive"},
    {"key": "negative", "label": "Negative"},
    {"key": "system", "label": "System"},
]


# --------------------------------------------------------------------------
# LED strip
# --------------------------------------------------------------------------
SRV_SET_PMU_LED = "/aimdk_5Fmsgs/srv/SetPmuLed"
TOPIC_PMU_LED_STATE = "/aima/hal/pmu/led_state"

LED_MODES = [
    {"value": 0, "label": "Steady", "desc": "Continuous solid colour"},
    {"value": 1, "label": "Breathing", "desc": "4 s sinusoidal brightness cycle"},
    {"value": 2, "label": "Blinking", "desc": "1 s cycle, toggles every 0.5 s"},
    {"value": 3, "label": "Flowing", "desc": "2 s cycle, left to right then off"},
]

LED_DEFAULT_PRIORITY = 6

LED_SWATCHES = [
    {"label": "Ice", "r": 57, "g": 135, "b": 229},
    {"label": "Aqua", "r": 25, "g": 158, "b": 112},
    {"label": "Amber", "r": 201, "g": 133, "b": 0},
    {"label": "Ember", "r": 217, "g": 89, "b": 38},
    {"label": "Rose", "r": 213, "g": 81, "b": 129},
    {"label": "Violet", "r": 144, "g": 133, "b": 233},
    {"label": "White", "r": 255, "g": 255, "b": 255},
    {"label": "Off", "r": 0, "g": 0, "b": 0},
]


# --------------------------------------------------------------------------
# System / diagnostics topics present on this robot
# --------------------------------------------------------------------------
TOPIC_ALERT_CODES = "/aima/hds/alert_code_list"
TOPIC_SM_SYSTEM_STATE = "/aima/sm/system_state"
TOPIC_TASK_MASTER_STATE = "/task_master/state"
PROCESS_INFO_TOPICS = [
    "/aima/hds/process/info/soc0",
    "/aima/hds/process/info/soc1",
    "/aima/hds/process/info/soc2",
]


# --------------------------------------------------------------------------
# Safety notes shown in the UI
# --------------------------------------------------------------------------
SAFETY_NOTES = [
    {"scope": "mode", "level": "critical",
     "text": "Passive and Zero-torque release all joint holding force. A free-standing robot "
             "will collapse. Only select them when the robot is supported or already seated."},
    {"scope": "locomotion", "level": "warning",
     "text": "Enter Stable stand and register an input source before driving. Keep the handheld "
             "remote controller within reach - it outranks this dashboard."},
    {"scope": "preset", "level": "warning",
     "text": "Preset motions require Stable stand and clear space around the robot."},
    {"scope": "network", "level": "critical",
     "text": f"Never use PC1 ({PC1_MOTION_CONTROL_IP}) as a build or run host. The dashboard "
             f"agent runs on PC2 ({PC2_DEVELOP_IP})."},
]


def client_spec() -> dict:
    """Everything the browser needs, in one payload."""
    return {
        "modes": MC_MODES,
        "mode_groups": MC_MODE_GROUPS,
        "driveable_modes": sorted(DRIVEABLE_MODES),
        "joint_control_modes_allowed": sorted(JOINT_CONTROL_MODES_ALLOWED),
        "action_status": MC_ACTION_STATUS,
        "presets": PRESET_MOTIONS,
        "preset_groups": PRESET_GROUPS,
        "control_areas": MC_CONTROL_AREAS,
        "joint_groups": JOINT_GROUPS,
        "joint_modes": JOINT_CONTROL_MODES,
        "velocity_limits": VELOCITY_LIMITS,
        "velocity_thresholds": VELOCITY_THRESHOLDS,
        "cameras": CAMERAS,
        "camera_inventory": CAMERA_INVENTORY,
        "lidar": LIDAR,
        "sensor_topics": SENSOR_TOPICS,
        "touch_zones": TOUCH_ZONE_COUNT,
        "pmu_rails": PMU_RAILS,
        "pmu_info_fields": PMU_INFO_FIELDS,
        "emojis": EMOJIS,
        "emoji_groups": EMOJI_GROUPS,
        "emoji_modes": EMOJI_PLAY_MODES,
        "face_status": FACE_STATUS,
        "led_modes": LED_MODES,
        "led_swatches": LED_SWATCHES,
        "tts_priorities": TTS_PRIORITIES,
        "hand_presets": HAND_PRESETS,
        "hand_types": HAND_TYPES,
        "dexhand_joints": DEXHAND_JOINTS,
        "builtin_input_sources": BUILTIN_INPUT_SOURCES,
        "dashboard_input_source": DASHBOARD_INPUT_SOURCE,
        "safety_notes": SAFETY_NOTES,
        "agent_port": AGENT_PORT,
        "topics": {
            "locomotion": TOPIC_LOCOMOTION_VELOCITY,
            "pmu": TOPIC_PMU_STATE,
            "imu_chest": TOPIC_IMU_CHEST,
            "imu_torso": TOPIC_IMU_TORSO,
            "touch_head": TOPIC_TOUCH_HEAD,
            "hand_command": TOPIC_HAND_COMMAND,
            "hand_state": TOPIC_HAND_STATE,
            "odometry": TOPIC_LEG_ODOMETRY,
            "camera": CAMERAS[0]["topic"],
        },
    }
