#!/usr/bin/env python3
"""
R1 arm-action RPC client — the R1-NATIVE way to move the upper body (arms + head)
while the firmware keeps balancing the legs.

Why this and not rt/arm_sdk: the R1 firmware does NOT expose live joint streaming
of the head/arms. The 'arm' service (a.k.a. r1_arm_example) owns rt/arm_sdk
internally; clients drive the upper body ONLY through this RPC — preset gesture
actions (by id) and custom "teach" actions (by name, recorded via the app's
teaching). Captured from the app's own traffic (2026-07-06):
  release_arm -> ExecuteAction(99); replay recorded motion -> ExecuteCustomAction(name).

Service "arm", api version "1.0.0.14". Same Client base as R1LocoClient.
State is published on rt/arm/action/state: {"holding":bool,"id":int,"name":str}.
"""
import json
from unitree_sdk2py.rpc.client import Client

ARM_ACTION_SERVICE_NAME = "arm"
ARM_ACTION_API_VERSION = "1.0.0.14"

API_EXECUTE_ACTION = 7106          # {"action_id": N}   preset actions
API_GET_ACTION_LIST = 7107         # -> JSON list
API_EXECUTE_CUSTOM_ACTION = 7108   # {"action_name": "..."}  recorded teach actions
API_RECORD = 7110                  # start = {"action_name":NAME} ; stop+save = "" (empty str)
API_STOP_CUSTOM_ACTION = 7113

RELEASE_ARM = 99                   # go compliant / release a held action


class R1ArmActionClient(Client):
    def __init__(self):
        super().__init__(ARM_ACTION_SERVICE_NAME, False)

    def Init(self):
        self._SetApiVerson(ARM_ACTION_API_VERSION)  # SDK spells it "Verson"
        for api in (API_EXECUTE_ACTION, API_GET_ACTION_LIST,
                    API_EXECUTE_CUSTOM_ACTION, API_RECORD, API_STOP_CUSTOM_ACTION):
            self._RegistApi(api, 0)

    def ExecuteAction(self, action_id: int):
        """Run a preset action by id (99 = release). Returns rc (0 = ok)."""
        code, _ = self._Call(API_EXECUTE_ACTION, json.dumps({"action_id": int(action_id)}))
        return code

    def ExecuteCustomAction(self, action_name: str):
        """Replay a recorded teach action by name (from the app's teaching)."""
        code, _ = self._Call(API_EXECUTE_CUSTOM_ACTION, json.dumps({"action_name": str(action_name)}))
        return code

    def StopCustomAction(self):
        code, _ = self._Call(API_STOP_CUSTOM_ACTION, "{}")
        return code

    def ReleaseArm(self):
        return self.ExecuteAction(RELEASE_ARM)

    def GetActionList(self):
        """Return (rc, data) — data is the JSON list of available actions."""
        code, data = self._Call(API_GET_ACTION_LIST, "")
        parsed = None
        if code == 0 and data:
            try:
                parsed = json.loads(data)
            except (ValueError, TypeError):
                parsed = data
        return code, parsed
