774 lines
38 KiB
Python
774 lines
38 KiB
Python
"""
|
|
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"],
|
|
},
|
|
}
|