From e1f561aa9066be6372e51b805ce8d0f94e7735ef Mon Sep 17 00:00:00 2001 From: Gijs de Jong Date: Tue, 28 Jul 2026 16:12:47 +0200 Subject: [PATCH] Initial 1.7 support --- Cargo.lock | 1 + README.md | 13 +- booster_sdk/src/client/audio.rs | 392 ++++++++++++++- booster_sdk/src/client/camera.rs | 44 ++ booster_sdk/src/client/handeye_calib.rs | 196 ++++++++ booster_sdk/src/client/light_control.rs | 40 ++ booster_sdk/src/client/loco.rs | 60 ++- booster_sdk/src/client/mod.rs | 2 + booster_sdk/src/dds/messages.rs | 10 + booster_sdk/src/dds/topics.rs | 12 + booster_sdk/src/types/b1.rs | 75 ++- booster_sdk_py/Cargo.toml | 1 + booster_sdk_py/booster_sdk/client/__init__.py | 12 +- booster_sdk_py/booster_sdk/client/audio.py | 24 + booster_sdk_py/booster_sdk/client/booster.py | 6 + booster_sdk_py/booster_sdk/client/camera.py | 12 + .../booster_sdk/client/handeye_calib.py | 21 + .../booster_sdk_bindings.pyi | 306 +++++++++++- booster_sdk_py/src/client/audio.rs | 446 +++++++++++++++++- booster_sdk_py/src/client/booster.rs | 168 ++++++- booster_sdk_py/src/client/camera.rs | 41 ++ booster_sdk_py/src/client/handeye_calib.rs | 265 +++++++++++ booster_sdk_py/src/client/light_control.rs | 15 +- booster_sdk_py/src/client/mod.rs | 4 + booster_sdk_py/src/lib.rs | 6 + 25 files changed, 2144 insertions(+), 28 deletions(-) create mode 100644 booster_sdk/src/client/camera.rs create mode 100644 booster_sdk/src/client/handeye_calib.rs create mode 100644 booster_sdk_py/booster_sdk/client/camera.py create mode 100644 booster_sdk_py/booster_sdk/client/handeye_calib.py create mode 100644 booster_sdk_py/src/client/camera.rs create mode 100644 booster_sdk_py/src/client/handeye_calib.rs diff --git a/Cargo.lock b/Cargo.lock index 54dd41c..0249a0c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -72,6 +72,7 @@ version = "0.1.1-alpha.2" dependencies = [ "booster_sdk", "pyo3", + "serde_json", "tokio", "tracing", "tracing-subscriber", diff --git a/README.md b/README.md index 4f03197..1064659 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,18 @@ client.move_robot(0.5, 0.0, 0.0) ``` -The Python bindings cover core control flows, including locomotion, gripper control, AI/LUI RPC calls, vision RPC calls, and X5 camera RPC calls. +The Rust crate and Python bindings support the Booster 1.7 SDK, including +locomotion, robot/device catalog discovery, gripper and LED control, AI/LUI and +vision RPCs, camera discovery, hand-eye calibration, and audio +device/Bluetooth management. + +SDK 1.7 additions include: + +- timed head rotation and selectable v1/v2 get-up behavior; +- sensor, hand, robot-model, and camera catalog queries; +- multi-pixel LED updates; +- audio device enumeration and Bluetooth scan/connect/disconnect/forget APIs; +- hand-eye calibration start, status, result, and apply APIs. ## Contributing diff --git a/booster_sdk/src/client/audio.rs b/booster_sdk/src/client/audio.rs index 2e743b6..0c2bac8 100644 --- a/booster_sdk/src/client/audio.rs +++ b/booster_sdk/src/client/audio.rs @@ -1,4 +1,4 @@ -//! Audio service RPC client for Booster SDK v1.6. +//! Audio service RPC client for Booster SDK v1.7. use std::{ collections::HashMap, @@ -13,7 +13,9 @@ use serde::{Deserialize, Serialize, de::DeserializeOwned}; use serde_json::{Map, Value, json}; use crate::{ - dds::{RpcClient, RpcClientOptions}, + dds::{ + BluetoothEvent, DdsSubscription, RpcClient, RpcClientOptions, audio_bluetooth_event_topic, + }, types::{Result, RpcError}, }; @@ -48,6 +50,13 @@ enum AudioServiceMethod { StopCaptureStream, DestroyCaptureStream, GetCaptureStreamInfo, + GetDevices, + StartBluetoothScan, + StopBluetoothScan, + GetBluetoothDevices, + ConnectBluetoothDevice, + DisconnectBluetoothDevice, + ForgetBluetoothDevice, } impl AudioServiceMethod { @@ -80,6 +89,13 @@ impl AudioServiceMethod { Self::StopCaptureStream => "rt/booster/audio/stop_capture_stream", Self::DestroyCaptureStream => "rt/booster/audio/destroy_capture_stream", Self::GetCaptureStreamInfo => "rt/booster/audio/get_capture_stream_info", + Self::GetDevices => "rt/booster/audio/get_devices", + Self::StartBluetoothScan => "rt/booster/audio/start_bluetooth_scan", + Self::StopBluetoothScan => "rt/booster/audio/stop_bluetooth_scan", + Self::GetBluetoothDevices => "rt/booster/audio/get_bluetooth_devices", + Self::ConnectBluetoothDevice => "rt/booster/audio/connect_bluetooth_device", + Self::DisconnectBluetoothDevice => "rt/booster/audio/disconnect_bluetooth_device", + Self::ForgetBluetoothDevice => "rt/booster/audio/forget_bluetooth_device", } } } @@ -94,6 +110,84 @@ crate::api_id_enum! { } } +crate::api_id_enum! { + /// Direction used when querying audio devices. + AudioDeviceQueryType { + Inputs = 0, + Outputs = 1, + } +} + +crate::api_id_enum! { + /// Audio device direction. + AudioDeviceDirection { + Input = 0, + Output = 1, + } +} + +crate::api_id_enum! { + /// Audio device transport. + AudioDeviceTransport { + Builtin = 0, + Usb = 1, + Bluetooth = 2, + Virtual = 3, + Unknown = 4, + } +} + +crate::api_id_enum! { + /// Backend responsible for an audio device. + AudioDeviceBackendAffinity { + Pulse = 0, + BoosterAecArray = 1, + AlsaDiagnostic = 2, + } +} + +crate::api_id_enum! { + /// Bluetooth scanning state. + BluetoothScanState { + Idle = 0, + Scanning = 1, + } +} + +crate::api_id_enum! { + /// Bluetooth device connection state. + BluetoothDeviceState { + Unknown = 0, + Paired = 1, + Connecting = 2, + Connected = 3, + Disconnecting = 4, + Failed = 5, + } +} + +crate::api_id_enum! { + /// Bluetooth major device class. + BluetoothMajorClass { + Audio = 0, + Peripheral = 1, + Phone = 2, + Computer = 3, + Other = 4, + } +} + +crate::api_id_enum! { + /// Bluetooth audio profile. + BluetoothAudioProfile { + None = 0, + A2dpSink = 1, + A2dpSource = 2, + HfpHeadset = 3, + HfpAg = 4, + } +} + crate::api_id_enum! { /// Player state. PlayerState { @@ -285,6 +379,118 @@ pub struct AudioCaptureStreamInfo { pub dropped_frames: i64, } +/// Audio input or output device descriptor. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AudioDeviceInfo { + pub device_id: String, + pub display_name: String, + pub direction: AudioDeviceDirection, + pub transport: AudioDeviceTransport, + pub backend_affinity: AudioDeviceBackendAffinity, + pub is_available: bool, + pub is_system_default: bool, + pub supports_input: bool, + pub supports_output: bool, + #[serde(default)] + pub provider_name: String, + #[serde(default)] + pub native_id: String, + #[serde(default)] + pub metadata_json: String, +} + +/// Bluetooth device descriptor. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BluetoothDeviceInfo { + pub address: String, + #[serde(default)] + pub name: String, + #[serde(default)] + pub rssi: i16, + pub state: BluetoothDeviceState, + pub major_class: BluetoothMajorClass, + #[serde(default)] + pub paired: bool, + #[serde(default)] + pub trusted: bool, + #[serde(default)] + pub connected: bool, + #[serde(default)] + pub is_audio_sink: bool, + #[serde(default)] + pub is_audio_source: bool, + #[serde(default)] + pub is_hfp_capable: bool, + #[serde(default)] + pub connected_profiles: Vec, + #[serde(default)] + pub linked_pulse_sink_id: String, + #[serde(default)] + pub linked_pulse_source_id: String, + #[serde(default)] + pub last_seen_ms: i64, +} + +/// Options for Bluetooth discovery. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct BluetoothScanOptions { + pub timeout_ms: i32, + pub audio_only: bool, +} + +impl Default for BluetoothScanOptions { + fn default() -> Self { + Self { + timeout_ms: 30_000, + audio_only: true, + } + } +} + +/// Options for connecting a Bluetooth audio device. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BluetoothConnectOptions { + pub address: String, + pub auto_pair: bool, + pub make_default: bool, + pub preferred_profile: BluetoothAudioProfile, + pub timeout_ms: i32, +} + +impl BluetoothConnectOptions { + #[must_use] + pub fn new(address: impl Into) -> Self { + Self { + address: address.into(), + auto_pair: true, + make_default: true, + preferred_profile: BluetoothAudioProfile::None, + timeout_ms: 15_000, + } + } +} + +/// Result returned after connecting a Bluetooth audio device. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BluetoothConnectResult { + pub device: BluetoothDeviceInfo, + #[serde(default)] + pub pulse_sink_id: String, + #[serde(default)] + pub pulse_source_id: String, + pub active_profile: BluetoothAudioProfile, + #[serde(default)] + pub pulse_endpoint_ready: bool, + #[serde(default)] + pub default_sink_applied: bool, + #[serde(default)] + pub default_source_applied: bool, + #[serde(default)] + pub default_route_error_code: i32, + #[serde(default)] + pub default_route_error_msg: String, +} + impl AudioCaptureStreamInfo { #[must_use] pub fn state_enum(&self) -> Option { @@ -460,6 +666,37 @@ impl AudioClient { self.call_raw(method, Value::Object(object)).await } + async fn call_service_with_timeout( + &self, + method: AudioServiceMethod, + request: Value, + timeout: Duration, + ) -> Result + where + R: DeserializeOwned + Send + 'static, + { + let client_id = self.ensure_registered().await?; + let request_id = self.next_request_id(); + let mut object = match request { + Value::Object(object) => object, + Value::Null => Map::new(), + other => { + let mut object = Map::new(); + object.insert("value".to_owned(), other); + object + } + }; + object.insert("client_id".to_owned(), Value::String(client_id)); + object.insert("request_id".to_owned(), Value::String(request_id)); + self.rpc_client(method)? + .call_with_body( + AUDIO_RPC_API_ID, + Value::Object(object).to_string(), + Some(timeout), + ) + .await + } + async fn call_result(&self, method: AudioServiceMethod, request: Value) -> Result<()> { let result: ServiceResult = self.call_service(method, request).await?; ensure_ret_code(result.ret_code, result.ret_msg) @@ -728,4 +965,155 @@ impl AudioClient { ensure_ret_code(response.ret_code, response.ret_msg)?; Ok(response.info) } + + /// Query available input or output devices. + pub async fn get_devices( + &self, + query_type: AudioDeviceQueryType, + ) -> Result> { + #[derive(Deserialize)] + struct Response { + ret_code: i32, + #[serde(default)] + ret_msg: String, + #[serde(default)] + devices: Vec, + } + + let response: Response = self + .call_service( + AudioServiceMethod::GetDevices, + json!({ "query_type": i32::from(query_type) }), + ) + .await?; + ensure_ret_code(response.ret_code, response.ret_msg)?; + Ok(response.devices) + } + + /// Start Bluetooth device discovery. + pub async fn start_bluetooth_scan(&self, options: &BluetoothScanOptions) -> Result<()> { + self.call_result( + AudioServiceMethod::StartBluetoothScan, + serialize_request(options)?, + ) + .await + } + + /// Stop Bluetooth device discovery. + pub async fn stop_bluetooth_scan(&self) -> Result<()> { + self.call_result(AudioServiceMethod::StopBluetoothScan, Value::Null) + .await + } + + /// List Bluetooth devices known to the audio service. + pub async fn get_bluetooth_devices( + &self, + include_unpaired: bool, + ) -> Result> { + #[derive(Deserialize)] + struct Response { + ret_code: i32, + #[serde(default)] + ret_msg: String, + #[serde(default)] + devices: Vec, + } + + let response: Response = self + .call_service( + AudioServiceMethod::GetBluetoothDevices, + json!({ "include_unpaired": include_unpaired }), + ) + .await?; + ensure_ret_code(response.ret_code, response.ret_msg)?; + Ok(response.devices) + } + + /// Connect and optionally route audio through a Bluetooth device. + pub async fn connect_bluetooth_device( + &self, + options: &BluetoothConnectOptions, + ) -> Result { + #[derive(Deserialize)] + struct Response { + ret_code: i32, + #[serde(default)] + ret_msg: String, + #[serde(flatten)] + result: BluetoothConnectResult, + } + + let timeout_ms = options.timeout_ms.max(0) as u64 + 2_000; + let response: Response = self + .call_service_with_timeout( + AudioServiceMethod::ConnectBluetoothDevice, + serialize_request(options)?, + Duration::from_millis(timeout_ms), + ) + .await?; + ensure_ret_code(response.ret_code, response.ret_msg)?; + Ok(response.result) + } + + /// Disconnect a Bluetooth device by address. + pub async fn disconnect_bluetooth_device(&self, address: impl Into) -> Result<()> { + self.call_result( + AudioServiceMethod::DisconnectBluetoothDevice, + json!({ "address": address.into() }), + ) + .await + } + + /// Remove a paired Bluetooth device from the robot. + pub async fn forget_bluetooth_device(&self, address: impl Into) -> Result<()> { + self.call_result( + AudioServiceMethod::ForgetBluetoothDevice, + json!({ "address": address.into() }), + ) + .await + } + + /// Subscribe to Bluetooth lifecycle events. + pub fn subscribe_bluetooth_events(&self) -> Result> { + self.rpc_client(AudioServiceMethod::RegisterClient)? + .node() + .subscribe(&audio_bluetooth_event_topic(), 16) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn bluetooth_options_match_wire_schema() { + let options = BluetoothConnectOptions::new("AA:BB:CC:DD:EE:FF"); + assert_eq!( + serde_json::to_value(options).unwrap(), + json!({ + "address": "AA:BB:CC:DD:EE:FF", + "auto_pair": true, + "make_default": true, + "preferred_profile": 0, + "timeout_ms": 15000 + }) + ); + } + + #[test] + fn sdk_1_7_audio_topics_match_installed_service() { + assert_eq!( + AudioServiceMethod::GetDevices.topic(), + "rt/booster/audio/get_devices" + ); + assert_eq!( + AudioServiceMethod::ConnectBluetoothDevice.topic(), + "rt/booster/audio/connect_bluetooth_device" + ); + assert_eq!( + AudioServiceMethod::ForgetBluetoothDevice.topic(), + "rt/booster/audio/forget_bluetooth_device" + ); + } } diff --git a/booster_sdk/src/client/camera.rs b/booster_sdk/src/client/camera.rs new file mode 100644 index 0000000..c58744e --- /dev/null +++ b/booster_sdk/src/client/camera.rs @@ -0,0 +1,44 @@ +//! Camera catalog RPC client introduced with Booster SDK 1.7. + +use std::time::Duration; + +use crate::{ + dds::{CAMERA_API_TOPIC, RpcClient, RpcClientOptions}, + types::{DeviceInfo, DeviceInfoKind, Result}, +}; + +crate::api_id_enum! { + /// Camera service RPC API identifiers. + CameraApiId { + GetCameras = 3100, + } +} + +/// Client for camera device discovery. +pub struct CameraClient { + rpc: RpcClient, +} + +impl CameraClient { + pub fn new() -> Result { + Self::with_options(RpcClientOptions::for_service(CAMERA_API_TOPIC)) + } + + pub fn with_startup_wait(startup_wait: Duration) -> Result { + Self::with_options( + RpcClientOptions::for_service(CAMERA_API_TOPIC).with_startup_wait(startup_wait), + ) + } + + pub fn with_options(options: RpcClientOptions) -> Result { + Ok(Self { + rpc: RpcClient::for_topic(options, CAMERA_API_TOPIC)?, + }) + } + + /// Query the camera catalog. The response body contains a `cameras` array. + pub async fn get_cameras(&self) -> Result { + let body = self.rpc.call_response(CameraApiId::GetCameras, "").await?; + Ok(DeviceInfo::new(DeviceInfoKind::Camera, body)) + } +} diff --git a/booster_sdk/src/client/handeye_calib.rs b/booster_sdk/src/client/handeye_calib.rs new file mode 100644 index 0000000..5a3055a --- /dev/null +++ b/booster_sdk/src/client/handeye_calib.rs @@ -0,0 +1,196 @@ +//! Hand-eye calibration RPC client introduced with Booster SDK 1.7. + +use std::time::Duration; + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use crate::{ + dds::{HAND_EYE_CALIB_API_TOPIC, RpcClient, RpcClientOptions}, + types::Result, +}; + +crate::api_id_enum! { + /// Hand-eye calibration RPC API identifiers. + HandEyeCalibApiId { + StartCalibration = 3100, + StopCalibration = 3101, + GetStatus = 3102, + GetResult = 3103, + ApplyResult = 3104, + } +} + +/// Options for starting a hand-eye calibration job. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct StartHandEyeCalibParameter { + #[serde(default = "default_publish_feedback")] + pub publish_feedback: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub square_size_m: Option, +} + +const fn default_publish_feedback() -> bool { + true +} + +impl Default for StartHandEyeCalibParameter { + fn default() -> Self { + Self { + publish_feedback: true, + square_size_m: None, + } + } +} + +/// Current calibration job status. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +pub struct HandEyeCalibStatus { + #[serde(default)] + pub status: String, + #[serde(default)] + pub job_id: String, + #[serde(default)] + pub stage: String, + #[serde(default)] + pub started_at: String, + #[serde(default)] + pub finished_at: String, + #[serde(default)] + pub progress: f64, + #[serde(default)] + pub stage2_done: i32, + #[serde(default)] + pub stage2_total: i32, + #[serde(default)] + pub stage3_done: i32, + #[serde(default)] + pub stage3_total: i32, + #[serde(default)] + pub error: Option, +} + +/// Calibration result and quality metrics. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +pub struct HandEyeCalibResult { + #[serde(default)] + pub status: String, + #[serde(default)] + pub job_id: String, + #[serde(default)] + pub summary: String, + #[serde(default)] + pub result: Option, + #[serde(default)] + pub reprojection_error_px: f64, + #[serde(default)] + pub progress: f64, + #[serde(default)] + pub stage2_done: i32, + #[serde(default)] + pub stage2_total: i32, + #[serde(default)] + pub stage3_done: i32, + #[serde(default)] + pub stage3_total: i32, + #[serde(default)] + pub error: Option, +} + +/// Result of applying calibration data to the robot. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +pub struct HandEyeCalibApplyResult { + #[serde(default)] + pub status: String, + #[serde(default)] + pub applied_path: String, + #[serde(default)] + pub error: Option, +} + +/// Client for the hand-eye calibration service. +pub struct HandEyeCalibClient { + rpc: RpcClient, +} + +impl HandEyeCalibClient { + pub fn new() -> Result { + Self::with_options(RpcClientOptions::for_service(HAND_EYE_CALIB_API_TOPIC)) + } + + pub fn with_startup_wait(startup_wait: Duration) -> Result { + Self::with_options( + RpcClientOptions::for_service(HAND_EYE_CALIB_API_TOPIC).with_startup_wait(startup_wait), + ) + } + + pub fn with_options(options: RpcClientOptions) -> Result { + Ok(Self { + rpc: RpcClient::for_topic(options, HAND_EYE_CALIB_API_TOPIC)?, + }) + } + + pub async fn start_calibration(&self, param: &StartHandEyeCalibParameter) -> Result<()> { + self.rpc + .call_serialized(HandEyeCalibApiId::StartCalibration, param) + .await + } + + pub async fn stop_calibration(&self) -> Result<()> { + self.rpc + .call_void(HandEyeCalibApiId::StopCalibration, "") + .await + } + + pub async fn get_status(&self) -> Result { + self.rpc + .call_response(HandEyeCalibApiId::GetStatus, "") + .await + } + + pub async fn get_result(&self) -> Result { + let mut response: HandEyeCalibResult = self + .rpc + .call_response(HandEyeCalibApiId::GetResult, "") + .await?; + if response.reprojection_error_px == 0.0 { + response.reprojection_error_px = response + .result + .as_ref() + .and_then(|result| result.get("rms_px")) + .and_then(Value::as_f64) + .unwrap_or(0.0); + } + Ok(response) + } + + pub async fn apply_result(&self) -> Result { + self.rpc + .call_response(HandEyeCalibApiId::ApplyResult, "") + .await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn default_start_payload_matches_cpp_sdk() { + assert_eq!( + serde_json::to_value(StartHandEyeCalibParameter::default()).unwrap(), + json!({"publish_feedback": true}) + ); + } + + #[test] + fn result_accepts_extensible_nested_payload() { + let result: HandEyeCalibResult = serde_json::from_value(json!({ + "status": "success", + "result": {"rms_px": 0.42, "matrix": [[1.0]]} + })) + .unwrap(); + assert_eq!(result.result.unwrap()["rms_px"], 0.42); + } +} diff --git a/booster_sdk/src/client/light_control.rs b/booster_sdk/src/client/light_control.rs index 2b66e47..cb911c2 100644 --- a/booster_sdk/src/client/light_control.rs +++ b/booster_sdk/src/client/light_control.rs @@ -12,6 +12,7 @@ crate::api_id_enum! { LightApiId { SetLedLightColor = 2000, StopLedLightControl = 2001, + SetLedLightColors = 2002, } } @@ -23,6 +24,12 @@ pub struct SetLedLightColorParameter { pub b: u8, } +/// Multi-pixel LED color payload introduced in SDK 1.7. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SetLedLightColorsParameter { + pub colors: Vec, +} + impl SetLedLightColorParameter { /// Parse a `#RRGGBB` color string. #[must_use] @@ -77,6 +84,18 @@ impl LightControlClient { .await } + /// Set multiple LED pixel colors in one request. + pub async fn set_led_light_colors(&self, colors: &[SetLedLightColorParameter]) -> Result<()> { + self.rpc + .call_serialized( + LightApiId::SetLedLightColors, + &SetLedLightColorsParameter { + colors: colors.to_vec(), + }, + ) + .await + } + /// Stop LED light control. pub async fn stop_led_light_control(&self) -> Result<()> { self.rpc @@ -84,3 +103,24 @@ impl LightControlClient { .await } } + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn serializes_sdk_1_7_multi_pixel_payload() { + let payload = SetLedLightColorsParameter { + colors: vec![ + SetLedLightColorParameter { r: 255, g: 0, b: 1 }, + SetLedLightColorParameter { r: 2, g: 3, b: 4 }, + ], + }; + assert_eq!( + serde_json::to_value(payload).unwrap(), + json!({"colors": [{"r": 255, "g": 0, "b": 1}, {"r": 2, "g": 3, "b": 4}]}) + ); + assert_eq!(i32::from(LightApiId::SetLedLightColors), 2002); + } +} diff --git a/booster_sdk/src/client/loco.rs b/booster_sdk/src/client/loco.rs index 1592335..c46d982 100644 --- a/booster_sdk/src/client/loco.rs +++ b/booster_sdk/src/client/loco.rs @@ -11,10 +11,11 @@ use crate::dds::{ video_stream_topic, }; use crate::types::{ - BoosterHandType, CustomTrainedTraj, DanceId, DexterousFingerParameter, Frame, GaitType, - GetModeResponse, GetRobotInfoResponse, GetStatusResponse, GripperControlMode, GripperMode, - GripperMotionParameter, Hand, HandAction, HandIndex, LoadCustomTrainedTrajResponse, LocoApiId, - Result, RobotMode, Transform, VisualKickVersion, WholeBodyDanceId, + BoosterHandType, CustomTrainedTraj, DanceId, DeviceInfo, DeviceInfoKind, + DexterousFingerParameter, Frame, GaitType, GetModeResponse, GetRobotInfoResponse, + GetStatusResponse, GetUpVersion, GripperControlMode, GripperMode, GripperMotionParameter, Hand, + HandAction, HandIndex, LoadCustomTrainedTrajResponse, LocoApiId, Result, RobotMode, Transform, + VisualKickVersion, WholeBodyDanceId, }; use serde::{Deserialize, Serialize}; use serde_json::json; @@ -93,6 +94,19 @@ impl BoosterClient { self.rpc.call_void(LocoApiId::RotateHead, param).await } + /// Rotate the head to absolute pitch/yaw angles within a requested duration. + pub async fn rotate_head_with_time( + &self, + pitch: f32, + yaw: f32, + time_millis: i32, + ) -> Result<()> { + let param = json!({ "pitch": pitch, "yaw": yaw, "time_millis": time_millis }).to_string(); + self.rpc + .call_void(LocoApiId::RotateHeadWithTime, param) + .await + } + /// Trigger a right-hand wave action. pub async fn wave_hand(&self, action: HandAction) -> Result<()> { let param = json!({ @@ -126,15 +140,49 @@ impl BoosterClient { /// Command the robot to get up. pub async fn get_up(&self) -> Result<()> { - self.rpc.call_void(LocoApiId::GetUp, "").await + self.get_up_with_version(GetUpVersion::V1).await + } + + /// Command the robot to get up using a specific behavior implementation. + pub async fn get_up_with_version(&self, version: GetUpVersion) -> Result<()> { + let param = json!({ "version": i32::from(version) }).to_string(); + self.rpc.call_void(LocoApiId::GetUp, param).await } /// Command the robot to get up into a specific mode. pub async fn get_up_with_mode(&self, mode: RobotMode) -> Result<()> { - let param = json!({ "mode": i32::from(mode) }).to_string(); + self.get_up_with_mode_and_version(mode, GetUpVersion::V1) + .await + } + + /// Command the robot to get up into a mode using a specific behavior implementation. + pub async fn get_up_with_mode_and_version( + &self, + mode: RobotMode, + version: GetUpVersion, + ) -> Result<()> { + let param = json!({ "mode": i32::from(mode), "version": i32::from(version) }).to_string(); self.rpc.call_void(LocoApiId::GetUpWithMode, param).await } + /// Query the static IMU sensor catalog from the robot configuration. + pub async fn get_sensors(&self) -> Result { + let body = self.rpc.call_response(LocoApiId::GetSensors, "").await?; + Ok(DeviceInfo::new(DeviceInfoKind::Sensors, body)) + } + + /// Query the static hand end-effector catalog from the robot configuration. + pub async fn get_hands(&self) -> Result { + let body = self.rpc.call_response(LocoApiId::GetHands, "").await?; + Ok(DeviceInfo::new(DeviceInfoKind::Hands, body)) + } + + /// Query the static URDF-derived robot model. + pub async fn get_robot_model(&self) -> Result { + let body = self.rpc.call_response(LocoApiId::GetRobotModel, "").await?; + Ok(DeviceInfo::new(DeviceInfoKind::RobotModel, body)) + } + /// Trigger a shoot action. pub async fn shoot(&self) -> Result<()> { self.rpc.call_void(LocoApiId::Shoot, "").await diff --git a/booster_sdk/src/client/mod.rs b/booster_sdk/src/client/mod.rs index f5957e2..58bf7cc 100644 --- a/booster_sdk/src/client/mod.rs +++ b/booster_sdk/src/client/mod.rs @@ -2,6 +2,8 @@ pub mod ai; pub mod audio; +pub mod camera; +pub mod handeye_calib; pub mod light_control; pub mod loco; pub mod vision; diff --git a/booster_sdk/src/dds/messages.rs b/booster_sdk/src/dds/messages.rs index c48d8df..457e512 100644 --- a/booster_sdk/src/dds/messages.rs +++ b/booster_sdk/src/dds/messages.rs @@ -155,3 +155,13 @@ pub struct SafeMode { /// Raw payload for safe mode (schema not documented in DDS reference). pub data: Vec, } + +/// Bluetooth lifecycle event published by the SDK 1.7 audio service. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BluetoothEvent { + pub timestamp_ms: i64, + pub event_type: String, + pub address: String, + pub payload_json: String, + pub error_code: i32, +} diff --git a/booster_sdk/src/dds/topics.rs b/booster_sdk/src/dds/topics.rs index 21dd732..ab8cd74 100644 --- a/booster_sdk/src/dds/topics.rs +++ b/booster_sdk/src/dds/topics.rs @@ -44,6 +44,7 @@ pub const TYPE_LIGHT_CONTROL: &str = "booster_interface::msg::dds_::LightControl pub const TYPE_SAFE_MODE: &str = "booster_msgs::msg::dds_::BinaryData_"; pub const TYPE_SUBTITLE: &str = "booster_interface::msg::dds_::Subtitle_"; pub const TYPE_ASR_CHUNK: &str = "booster_interface::msg::dds_::AsrChunk_"; +pub const TYPE_BLUETOOTH_EVENT: &str = "booster_msgs::audio::BluetoothEventTopic"; pub const LOCO_API_TOPIC: &str = "rt/LocoApiTopic"; pub const AI_API_TOPIC: &str = "rt/AiApiTopic"; @@ -51,6 +52,8 @@ pub const LUI_API_TOPIC: &str = "rt/LuiApiTopic"; pub const LIGHT_CONTROL_API_TOPIC: &str = "rt/LightControlApiTopic"; pub const VISION_API_TOPIC: &str = "rt/VisionApiTopic"; pub const X5_CAMERA_CONTROL_API_TOPIC: &str = "rt/X5CameraControl"; +pub const CAMERA_API_TOPIC: &str = "rt/CameraApiTopic"; +pub const HAND_EYE_CALIB_API_TOPIC: &str = "rt/HandEyeCalibApiTopic"; pub fn rpc_request_topic(service_topic: &str) -> TopicSpec { TopicSpec { @@ -185,3 +188,12 @@ pub fn lui_asr_chunk_topic() -> TopicSpec { kind: TopicKind::NoKey, } } + +pub fn audio_bluetooth_event_topic() -> TopicSpec { + TopicSpec { + name: "rt/booster/audio/bluetooth_events".to_owned(), + type_name: TYPE_BLUETOOTH_EVENT, + qos: qos_reliable_keep_last(16), + kind: TopicKind::NoKey, + } +} diff --git a/booster_sdk/src/types/b1.rs b/booster_sdk/src/types/b1.rs index 0954418..4f601df 100644 --- a/booster_sdk/src/types/b1.rs +++ b/booster_sdk/src/types/b1.rs @@ -47,15 +47,20 @@ crate::api_id_enum! { LionDanceStart = 2040, LionDanceMove = 2041, SwitchGait = 2042, + RotateHeadWithTime = 2043, + GetSensors = 2044, + GetHands = 2045, + GetRobotModel = 2046, } } crate::api_id_enum! { - /// B1 v1.6 gait selectors. + /// B1 gait selectors. GaitType { WholeBodyHumanlikeGait = 0, HalfBodyHumanlikeGait = 1, HalfBodyHumanlikeGaitV2 = 2, + WholeBodyHumanlikeGaitV2 = 3, } } @@ -75,6 +80,29 @@ crate::api_id_enum! { InsideFoot = 10, Goalie = 11, WbcGait = 12, + LionDancePreparePose = 13, + VisualKickV1 = 14, + } +} + +crate::api_id_enum! { + /// Get-up behavior implementation. + GetUpVersion { + /// Initial/base get-up behavior. + V1 = 0, + /// BMM get-up behavior introduced with SDK 1.7. + V2 = 1, + } +} + +crate::api_id_enum! { + /// Kind of catalog returned by a device-discovery RPC. + DeviceInfoKind { + Unknown = -1, + Sensors = 0, + Hands = 1, + RobotModel = 2, + Camera = 3, } } @@ -301,6 +329,24 @@ pub struct GetRobotInfoResponse { pub region: String, } +/// JSON-backed device catalog returned by SDK 1.7 discovery APIs. +/// +/// The robot owns these schemas and may extend them between firmware releases, +/// so the body remains a [`serde_json::Value`] just like the C++ SDK's +/// `DeviceInfo::json_`. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct DeviceInfo { + pub kind: DeviceInfoKind, + pub body: serde_json::Value, +} + +impl DeviceInfo { + #[must_use] + pub fn new(kind: DeviceInfoKind, body: serde_json::Value) -> Self { + Self { kind, body } + } +} + /// Model parameters used by custom trajectories. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct CustomModelParams { @@ -332,3 +378,30 @@ pub struct LoadCustomTrainedTrajResponse { /// Convenience alias matching the C++ naming. pub type HandIndex = Hand; + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn sdk_1_7_ids_match_cpp_sdk() { + assert_eq!(i32::from(LocoApiId::RotateHeadWithTime), 2043); + assert_eq!(i32::from(LocoApiId::GetSensors), 2044); + assert_eq!(i32::from(LocoApiId::GetHands), 2045); + assert_eq!(i32::from(LocoApiId::GetRobotModel), 2046); + assert_eq!(i32::from(GaitType::WholeBodyHumanlikeGaitV2), 3); + assert_eq!(i32::from(BodyControl::VisualKickV1), 14); + } + + #[test] + fn device_info_preserves_extensible_json_body() { + let info = DeviceInfo::new( + DeviceInfoKind::Sensors, + json!({"imus": [{"index": 0, "future_field": true}]}), + ); + let encoded = serde_json::to_value(&info).unwrap(); + assert_eq!(encoded["kind"], 0); + assert_eq!(encoded["body"]["imus"][0]["future_field"], true); + } +} diff --git a/booster_sdk_py/Cargo.toml b/booster_sdk_py/Cargo.toml index 21e5b82..ff87d17 100644 --- a/booster_sdk_py/Cargo.toml +++ b/booster_sdk_py/Cargo.toml @@ -17,3 +17,4 @@ pyo3 = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } +serde_json = { workspace = true } diff --git a/booster_sdk_py/booster_sdk/client/__init__.py b/booster_sdk_py/booster_sdk/client/__init__.py index 72e9b28..675224f 100644 --- a/booster_sdk_py/booster_sdk/client/__init__.py +++ b/booster_sdk_py/booster_sdk/client/__init__.py @@ -2,4 +2,14 @@ from __future__ import annotations -__all__ = ["ai", "audio", "booster", "light_control", "lui", "vision", "x5_camera"] +__all__ = [ + "ai", + "audio", + "booster", + "camera", + "handeye_calib", + "light_control", + "lui", + "vision", + "x5_camera", +] diff --git a/booster_sdk_py/booster_sdk/client/audio.py b/booster_sdk_py/booster_sdk/client/audio.py index aef0b4c..84de21f 100644 --- a/booster_sdk_py/booster_sdk/client/audio.py +++ b/booster_sdk_py/booster_sdk/client/audio.py @@ -20,6 +20,18 @@ PlayerInfo = bindings.PlayerInfo RecorderInfo = bindings.RecorderInfo AudioCaptureStreamInfo = bindings.AudioCaptureStreamInfo +AudioDeviceQueryType = bindings.AudioDeviceQueryType +AudioDeviceDirection = bindings.AudioDeviceDirection +AudioDeviceTransport = bindings.AudioDeviceTransport +AudioDeviceBackendAffinity = bindings.AudioDeviceBackendAffinity +AudioDeviceInfo = bindings.AudioDeviceInfo +BluetoothDeviceState = bindings.BluetoothDeviceState +BluetoothMajorClass = bindings.BluetoothMajorClass +BluetoothAudioProfile = bindings.BluetoothAudioProfile +BluetoothDeviceInfo = bindings.BluetoothDeviceInfo +BluetoothScanOptions = bindings.BluetoothScanOptions +BluetoothConnectOptions = bindings.BluetoothConnectOptions +BluetoothConnectResult = bindings.BluetoothConnectResult __all__ = [ "AudioClient", @@ -38,4 +50,16 @@ "PlayerInfo", "RecorderInfo", "AudioCaptureStreamInfo", + "AudioDeviceQueryType", + "AudioDeviceDirection", + "AudioDeviceTransport", + "AudioDeviceBackendAffinity", + "AudioDeviceInfo", + "BluetoothDeviceState", + "BluetoothMajorClass", + "BluetoothAudioProfile", + "BluetoothDeviceInfo", + "BluetoothScanOptions", + "BluetoothConnectOptions", + "BluetoothConnectResult", ] diff --git a/booster_sdk_py/booster_sdk/client/booster.py b/booster_sdk_py/booster_sdk/client/booster.py index a39d53c..faeb389 100644 --- a/booster_sdk_py/booster_sdk/client/booster.py +++ b/booster_sdk_py/booster_sdk/client/booster.py @@ -18,6 +18,9 @@ WholeBodyDanceId = bindings.WholeBodyDanceId VisualKickVersion = bindings.VisualKickVersion GaitType = bindings.GaitType +GetUpVersion = bindings.GetUpVersion +DeviceInfoKind = bindings.DeviceInfoKind +DeviceInfo = bindings.DeviceInfo JointOrder = bindings.JointOrder BodyControl = bindings.BodyControl Action = bindings.Action @@ -51,6 +54,9 @@ "WholeBodyDanceId", "VisualKickVersion", "GaitType", + "GetUpVersion", + "DeviceInfoKind", + "DeviceInfo", "JointOrder", "BodyControl", "Action", diff --git a/booster_sdk_py/booster_sdk/client/camera.py b/booster_sdk_py/booster_sdk/client/camera.py new file mode 100644 index 0000000..ba33158 --- /dev/null +++ b/booster_sdk_py/booster_sdk/client/camera.py @@ -0,0 +1,12 @@ +"""Camera catalog client bindings.""" + +from __future__ import annotations + +import booster_sdk_bindings as bindings + +CameraClient = bindings.CameraClient +DeviceInfo = bindings.DeviceInfo +DeviceInfoKind = bindings.DeviceInfoKind +BoosterSdkError = bindings.BoosterSdkError + +__all__ = ["CameraClient", "DeviceInfo", "DeviceInfoKind", "BoosterSdkError"] diff --git a/booster_sdk_py/booster_sdk/client/handeye_calib.py b/booster_sdk_py/booster_sdk/client/handeye_calib.py new file mode 100644 index 0000000..1716b36 --- /dev/null +++ b/booster_sdk_py/booster_sdk/client/handeye_calib.py @@ -0,0 +1,21 @@ +"""Hand-eye calibration client bindings.""" + +from __future__ import annotations + +import booster_sdk_bindings as bindings + +HandEyeCalibClient = bindings.HandEyeCalibClient +StartHandEyeCalibParameter = bindings.StartHandEyeCalibParameter +HandEyeCalibStatus = bindings.HandEyeCalibStatus +HandEyeCalibResult = bindings.HandEyeCalibResult +HandEyeCalibApplyResult = bindings.HandEyeCalibApplyResult +BoosterSdkError = bindings.BoosterSdkError + +__all__ = [ + "HandEyeCalibClient", + "StartHandEyeCalibParameter", + "HandEyeCalibStatus", + "HandEyeCalibResult", + "HandEyeCalibApplyResult", + "BoosterSdkError", +] diff --git a/booster_sdk_py/booster_sdk_bindings/booster_sdk_bindings.pyi b/booster_sdk_py/booster_sdk_bindings/booster_sdk_bindings.pyi index c6ea131..b6a6574 100644 --- a/booster_sdk_py/booster_sdk_bindings/booster_sdk_bindings.pyi +++ b/booster_sdk_py/booster_sdk_bindings/booster_sdk_bindings.pyi @@ -209,6 +209,7 @@ class GaitType: WHOLE_BODY_HUMANLIKE_GAIT: GaitType HALF_BODY_HUMANLIKE_GAIT: GaitType HALF_BODY_HUMANLIKE_GAIT_V2: GaitType + WHOLE_BODY_HUMANLIKE_GAIT_V2: GaitType def __repr__(self) -> str: """Return a stable enum-style representation.""" @@ -220,6 +221,36 @@ class GaitType: """Return ``True`` when both values represent the same gait.""" ... +class GetUpVersion: + """Get-up behavior implementation.""" + + V1: GetUpVersion + V2: GetUpVersion + + def __int__(self) -> int: ... + def __eq__(self, other: object) -> bool: ... + +class DeviceInfoKind: + """Kind of catalog returned by a device-discovery RPC.""" + + UNKNOWN: DeviceInfoKind + SENSORS: DeviceInfoKind + HANDS: DeviceInfoKind + ROBOT_MODEL: DeviceInfoKind + CAMERA: DeviceInfoKind + + def __int__(self) -> int: ... + def __eq__(self, other: object) -> bool: ... + +class DeviceInfo: + """JSON-backed device catalog returned by SDK 1.7.""" + + @property + def kind(self) -> DeviceInfoKind: ... + @property + def body(self) -> object: ... + def __repr__(self) -> str: ... + class JointOrder: """Joint indexing convention used by custom trajectory models.""" @@ -252,6 +283,8 @@ class BodyControl: INSIDE_FOOT: BodyControl GOALIE: BodyControl WBC_GAIT: BodyControl + LION_DANCE_PREPARE_POSE: BodyControl + VISUAL_KICK_V1: BodyControl def __repr__(self) -> str: """Return a stable enum-style representation.""" @@ -935,6 +968,10 @@ class BoosterClient: """Rotate head to target pitch/yaw angles.""" ... + def rotate_head_with_time(self, pitch: float, yaw: float, time_millis: int) -> None: + """Rotate head to target angles within the requested duration.""" + ... + def wave_hand(self, action: HandAction) -> None: """Trigger hand wave/open-close style action.""" ... @@ -949,14 +986,28 @@ class BoosterClient: """Command robot to lie down.""" ... - def get_up(self) -> None: + def get_up(self, version: GetUpVersion | None = ...) -> None: """Command robot to stand up.""" ... - def get_up_with_mode(self, mode: RobotMode) -> None: + def get_up_with_mode( + self, mode: RobotMode, version: GetUpVersion | None = ... + ) -> None: """Stand up and transition into specified mode.""" ... + def get_sensors(self) -> DeviceInfo: + """Fetch the robot's static IMU sensor catalog.""" + ... + + def get_hands(self) -> DeviceInfo: + """Fetch the robot's hand end-effector catalog.""" + ... + + def get_robot_model(self) -> DeviceInfo: + """Fetch the URDF-derived robot model.""" + ... + def shoot(self) -> None: """Trigger shoot action.""" ... @@ -1415,8 +1466,155 @@ class AudioCaptureStreamInfo: """Number of capture frames dropped by the service.""" ... +class AudioDeviceQueryType: + INPUTS: AudioDeviceQueryType + OUTPUTS: AudioDeviceQueryType + def __int__(self) -> int: ... + def __eq__(self, other: object) -> bool: ... + +class AudioDeviceDirection: + INPUT: AudioDeviceDirection + OUTPUT: AudioDeviceDirection + def __int__(self) -> int: ... + def __eq__(self, other: object) -> bool: ... + +class AudioDeviceTransport: + BUILTIN: AudioDeviceTransport + USB: AudioDeviceTransport + BLUETOOTH: AudioDeviceTransport + VIRTUAL: AudioDeviceTransport + UNKNOWN: AudioDeviceTransport + def __int__(self) -> int: ... + def __eq__(self, other: object) -> bool: ... + +class AudioDeviceBackendAffinity: + PULSE: AudioDeviceBackendAffinity + BOOSTER_AEC_ARRAY: AudioDeviceBackendAffinity + ALSA_DIAGNOSTIC: AudioDeviceBackendAffinity + def __int__(self) -> int: ... + def __eq__(self, other: object) -> bool: ... + +class BluetoothDeviceState: + UNKNOWN: BluetoothDeviceState + PAIRED: BluetoothDeviceState + CONNECTING: BluetoothDeviceState + CONNECTED: BluetoothDeviceState + DISCONNECTING: BluetoothDeviceState + FAILED: BluetoothDeviceState + def __int__(self) -> int: ... + def __eq__(self, other: object) -> bool: ... + +class BluetoothMajorClass: + AUDIO: BluetoothMajorClass + PERIPHERAL: BluetoothMajorClass + PHONE: BluetoothMajorClass + COMPUTER: BluetoothMajorClass + OTHER: BluetoothMajorClass + def __int__(self) -> int: ... + def __eq__(self, other: object) -> bool: ... + +class BluetoothAudioProfile: + NONE: BluetoothAudioProfile + A2DP_SINK: BluetoothAudioProfile + A2DP_SOURCE: BluetoothAudioProfile + HFP_HEADSET: BluetoothAudioProfile + HFP_AG: BluetoothAudioProfile + def __int__(self) -> int: ... + def __eq__(self, other: object) -> bool: ... + +class AudioDeviceInfo: + @property + def device_id(self) -> str: ... + @property + def display_name(self) -> str: ... + @property + def direction(self) -> AudioDeviceDirection: ... + @property + def transport(self) -> AudioDeviceTransport: ... + @property + def backend_affinity(self) -> AudioDeviceBackendAffinity: ... + @property + def is_available(self) -> bool: ... + @property + def is_system_default(self) -> bool: ... + @property + def supports_input(self) -> bool: ... + @property + def supports_output(self) -> bool: ... + @property + def provider_name(self) -> str: ... + @property + def native_id(self) -> str: ... + @property + def metadata_json(self) -> str: ... + +class BluetoothDeviceInfo: + @property + def address(self) -> str: ... + @property + def name(self) -> str: ... + @property + def rssi(self) -> int: ... + @property + def state(self) -> BluetoothDeviceState: ... + @property + def major_class(self) -> BluetoothMajorClass: ... + @property + def paired(self) -> bool: ... + @property + def trusted(self) -> bool: ... + @property + def connected(self) -> bool: ... + @property + def is_audio_sink(self) -> bool: ... + @property + def is_audio_source(self) -> bool: ... + @property + def is_hfp_capable(self) -> bool: ... + @property + def connected_profiles(self) -> list[BluetoothAudioProfile]: ... + @property + def linked_pulse_sink_id(self) -> str: ... + @property + def linked_pulse_source_id(self) -> str: ... + @property + def last_seen_ms(self) -> int: ... + +class BluetoothScanOptions: + def __init__(self, timeout_ms: int = ..., audio_only: bool = ...) -> None: ... + +class BluetoothConnectOptions: + def __init__( + self, + address: str, + auto_pair: bool = ..., + make_default: bool = ..., + preferred_profile: BluetoothAudioProfile | None = ..., + timeout_ms: int = ..., + ) -> None: ... + +class BluetoothConnectResult: + @property + def device(self) -> BluetoothDeviceInfo: ... + @property + def pulse_sink_id(self) -> str: ... + @property + def pulse_source_id(self) -> str: ... + @property + def active_profile(self) -> BluetoothAudioProfile: ... + @property + def pulse_endpoint_ready(self) -> bool: ... + @property + def default_sink_applied(self) -> bool: ... + @property + def default_source_applied(self) -> bool: ... + @property + def default_route_error_code(self) -> int: ... + @property + def default_route_error_msg(self) -> str: ... + class AudioClient: - """Client for the v1.6 audio service RPC APIs.""" + """Client for the v1.7 audio service RPC APIs.""" def __init__(self, startup_wait_sec: float | None = ...) -> None: """Create an audio client.""" @@ -1507,6 +1705,21 @@ class AudioClient: def get_capture_stream_info(self, session_id: int) -> AudioCaptureStreamInfo: """Fetch current capture stream session info.""" ... + def get_devices( + self, query_type: AudioDeviceQueryType + ) -> list[AudioDeviceInfo]: ... + def start_bluetooth_scan( + self, options: BluetoothScanOptions | None = ... + ) -> None: ... + def stop_bluetooth_scan(self) -> None: ... + def get_bluetooth_devices( + self, include_unpaired: bool = ... + ) -> list[BluetoothDeviceInfo]: ... + def connect_bluetooth_device( + self, options: BluetoothConnectOptions + ) -> BluetoothConnectResult: ... + def disconnect_bluetooth_device(self, address: str) -> None: ... + def forget_bluetooth_device(self, address: str) -> None: ... class AiClient: """Client for AI chat and speech features.""" @@ -1585,10 +1798,97 @@ class LightControlClient: """Set LED strip color using RGB values (0-255).""" ... + def set_led_light_colors(self, colors: list[tuple[int, int, int]]) -> None: + """Set multiple LED pixel colors in one request.""" + ... + def stop_led_light_control(self) -> None: """Stop active LED control program/effect.""" ... +class CameraClient: + """Client for SDK 1.7 camera catalog discovery.""" + + def __init__(self, startup_wait_sec: float | None = ...) -> None: ... + def get_cameras(self) -> DeviceInfo: ... + +class StartHandEyeCalibParameter: + def __init__( + self, + publish_feedback: bool = ..., + square_size_m: float | None = ..., + ) -> None: ... + @property + def publish_feedback(self) -> bool: ... + @property + def square_size_m(self) -> float | None: ... + +class HandEyeCalibStatus: + @property + def status(self) -> str: ... + @property + def job_id(self) -> str: ... + @property + def stage(self) -> str: ... + @property + def started_at(self) -> str: ... + @property + def finished_at(self) -> str: ... + @property + def progress(self) -> float: ... + @property + def stage2_done(self) -> int: ... + @property + def stage2_total(self) -> int: ... + @property + def stage3_done(self) -> int: ... + @property + def stage3_total(self) -> int: ... + @property + def error(self) -> object | None: ... + +class HandEyeCalibResult: + @property + def status(self) -> str: ... + @property + def job_id(self) -> str: ... + @property + def summary(self) -> str: ... + @property + def result(self) -> object | None: ... + @property + def reprojection_error_px(self) -> float: ... + @property + def progress(self) -> float: ... + @property + def stage2_done(self) -> int: ... + @property + def stage2_total(self) -> int: ... + @property + def stage3_done(self) -> int: ... + @property + def stage3_total(self) -> int: ... + @property + def error(self) -> object | None: ... + +class HandEyeCalibApplyResult: + @property + def status(self) -> str: ... + @property + def applied_path(self) -> str: ... + @property + def error(self) -> object | None: ... + +class HandEyeCalibClient: + """Client for SDK 1.7 hand-eye calibration.""" + + def __init__(self, startup_wait_sec: float | None = ...) -> None: ... + def start_calibration(self, param: StartHandEyeCalibParameter) -> None: ... + def stop_calibration(self) -> None: ... + def get_status(self) -> HandEyeCalibStatus: ... + def get_result(self) -> HandEyeCalibResult: ... + def apply_result(self) -> HandEyeCalibApplyResult: ... + class VisionClient: """Client for vision inference APIs.""" diff --git a/booster_sdk_py/src/client/audio.rs b/booster_sdk_py/src/client/audio.rs index c7efb10..bfb6bab 100644 --- a/booster_sdk_py/src/client/audio.rs +++ b/booster_sdk_py/src/client/audio.rs @@ -2,7 +2,10 @@ use std::sync::Arc; use booster_sdk::client::audio::{ AudioCaptureStreamInfo, AudioCaptureStreamOptions, AudioCaptureStreamState, AudioClient, - AudioSourceType, InitCaptureStreamResponse, InitPlayerResponse, InitRecorderResponse, + AudioDeviceBackendAffinity, AudioDeviceDirection, AudioDeviceInfo, AudioDeviceQueryType, + AudioDeviceTransport, AudioSourceType, BluetoothAudioProfile, BluetoothConnectOptions, + BluetoothConnectResult, BluetoothDeviceInfo, BluetoothDeviceState, BluetoothMajorClass, + BluetoothScanOptions, InitCaptureStreamResponse, InitPlayerResponse, InitRecorderResponse, PcmFormat, PlayerInfo, PlayerInitOptions, PlayerPriority, PlayerState, RecorderInfo, RecorderInitOptions, RecorderState, }; @@ -10,6 +13,110 @@ use pyo3::{Bound, prelude::*, types::PyModule}; use crate::{runtime::wait_for_future, startup_wait_from_seconds, to_py_err}; +macro_rules! py_int_enum { + ($py_name:ident, $public_name:literal, $inner:ty, { $($attr:ident => $variant:ident),+ $(,)? }) => { + #[pyclass(module = "booster_sdk_bindings", name = $public_name, eq)] + #[derive(Clone, Copy, PartialEq, Eq)] + pub struct $py_name($inner); + + #[pymethods] + impl $py_name { + $( + #[classattr] + const $attr: Self = Self(<$inner>::$variant); + )+ + + fn __int__(&self) -> i32 { + i32::from(self.0) + } + } + + impl From<$py_name> for $inner { + fn from(value: $py_name) -> Self { + value.0 + } + } + + impl From<$inner> for $py_name { + fn from(value: $inner) -> Self { + Self(value) + } + } + }; +} + +py_int_enum!( + PyAudioDeviceQueryType, + "AudioDeviceQueryType", + AudioDeviceQueryType, + { INPUTS => Inputs, OUTPUTS => Outputs } +); +py_int_enum!( + PyAudioDeviceDirection, + "AudioDeviceDirection", + AudioDeviceDirection, + { INPUT => Input, OUTPUT => Output } +); +py_int_enum!( + PyAudioDeviceTransport, + "AudioDeviceTransport", + AudioDeviceTransport, + { + BUILTIN => Builtin, + USB => Usb, + BLUETOOTH => Bluetooth, + VIRTUAL => Virtual, + UNKNOWN => Unknown + } +); +py_int_enum!( + PyAudioDeviceBackendAffinity, + "AudioDeviceBackendAffinity", + AudioDeviceBackendAffinity, + { + PULSE => Pulse, + BOOSTER_AEC_ARRAY => BoosterAecArray, + ALSA_DIAGNOSTIC => AlsaDiagnostic + } +); +py_int_enum!( + PyBluetoothDeviceState, + "BluetoothDeviceState", + BluetoothDeviceState, + { + UNKNOWN => Unknown, + PAIRED => Paired, + CONNECTING => Connecting, + CONNECTED => Connected, + DISCONNECTING => Disconnecting, + FAILED => Failed + } +); +py_int_enum!( + PyBluetoothMajorClass, + "BluetoothMajorClass", + BluetoothMajorClass, + { + AUDIO => Audio, + PERIPHERAL => Peripheral, + PHONE => Phone, + COMPUTER => Computer, + OTHER => Other + } +); +py_int_enum!( + PyBluetoothAudioProfile, + "BluetoothAudioProfile", + BluetoothAudioProfile, + { + NONE => None, + A2DP_SINK => A2dpSink, + A2DP_SOURCE => A2dpSource, + HFP_HEADSET => HfpHeadset, + HFP_AG => HfpAg + } +); + #[pyclass(module = "booster_sdk_bindings", name = "AudioSourceType", eq)] #[derive(Clone, Copy, PartialEq, Eq)] pub struct PyAudioSourceType(AudioSourceType); @@ -490,6 +597,252 @@ impl From for PyAudioCaptureStreamInfo { } } +#[pyclass(module = "booster_sdk_bindings", name = "AudioDeviceInfo")] +#[derive(Clone)] +pub struct PyAudioDeviceInfo(AudioDeviceInfo); + +#[pymethods] +impl PyAudioDeviceInfo { + #[getter] + fn device_id(&self) -> String { + self.0.device_id.clone() + } + #[getter] + fn display_name(&self) -> String { + self.0.display_name.clone() + } + #[getter] + fn direction(&self) -> PyAudioDeviceDirection { + self.0.direction.into() + } + #[getter] + fn transport(&self) -> PyAudioDeviceTransport { + self.0.transport.into() + } + #[getter] + fn backend_affinity(&self) -> PyAudioDeviceBackendAffinity { + self.0.backend_affinity.into() + } + #[getter] + fn is_available(&self) -> bool { + self.0.is_available + } + #[getter] + fn is_system_default(&self) -> bool { + self.0.is_system_default + } + #[getter] + fn supports_input(&self) -> bool { + self.0.supports_input + } + #[getter] + fn supports_output(&self) -> bool { + self.0.supports_output + } + #[getter] + fn provider_name(&self) -> String { + self.0.provider_name.clone() + } + #[getter] + fn native_id(&self) -> String { + self.0.native_id.clone() + } + #[getter] + fn metadata_json(&self) -> String { + self.0.metadata_json.clone() + } +} + +impl From for PyAudioDeviceInfo { + fn from(value: AudioDeviceInfo) -> Self { + Self(value) + } +} + +#[pyclass(module = "booster_sdk_bindings", name = "BluetoothDeviceInfo")] +#[derive(Clone)] +pub struct PyBluetoothDeviceInfo(BluetoothDeviceInfo); + +#[pymethods] +impl PyBluetoothDeviceInfo { + #[getter] + fn address(&self) -> String { + self.0.address.clone() + } + #[getter] + fn name(&self) -> String { + self.0.name.clone() + } + #[getter] + fn rssi(&self) -> i16 { + self.0.rssi + } + #[getter] + fn state(&self) -> PyBluetoothDeviceState { + self.0.state.into() + } + #[getter] + fn major_class(&self) -> PyBluetoothMajorClass { + self.0.major_class.into() + } + #[getter] + fn paired(&self) -> bool { + self.0.paired + } + #[getter] + fn trusted(&self) -> bool { + self.0.trusted + } + #[getter] + fn connected(&self) -> bool { + self.0.connected + } + #[getter] + fn is_audio_sink(&self) -> bool { + self.0.is_audio_sink + } + #[getter] + fn is_audio_source(&self) -> bool { + self.0.is_audio_source + } + #[getter] + fn is_hfp_capable(&self) -> bool { + self.0.is_hfp_capable + } + #[getter] + fn connected_profiles(&self) -> Vec { + self.0 + .connected_profiles + .iter() + .copied() + .map(Into::into) + .collect() + } + #[getter] + fn linked_pulse_sink_id(&self) -> String { + self.0.linked_pulse_sink_id.clone() + } + #[getter] + fn linked_pulse_source_id(&self) -> String { + self.0.linked_pulse_source_id.clone() + } + #[getter] + fn last_seen_ms(&self) -> i64 { + self.0.last_seen_ms + } +} + +impl From for PyBluetoothDeviceInfo { + fn from(value: BluetoothDeviceInfo) -> Self { + Self(value) + } +} + +#[pyclass(module = "booster_sdk_bindings", name = "BluetoothScanOptions")] +#[derive(Clone, Copy)] +pub struct PyBluetoothScanOptions(BluetoothScanOptions); + +#[pymethods] +impl PyBluetoothScanOptions { + #[new] + #[pyo3(signature = (timeout_ms=30000, audio_only=true))] + fn new(timeout_ms: i32, audio_only: bool) -> Self { + Self(BluetoothScanOptions { + timeout_ms, + audio_only, + }) + } +} + +impl From for BluetoothScanOptions { + fn from(value: PyBluetoothScanOptions) -> Self { + value.0 + } +} + +#[pyclass(module = "booster_sdk_bindings", name = "BluetoothConnectOptions")] +#[derive(Clone)] +pub struct PyBluetoothConnectOptions(BluetoothConnectOptions); + +#[pymethods] +impl PyBluetoothConnectOptions { + #[new] + #[pyo3(signature = (address, auto_pair=true, make_default=true, preferred_profile=None, timeout_ms=15000))] + fn new( + address: String, + auto_pair: bool, + make_default: bool, + preferred_profile: Option, + timeout_ms: i32, + ) -> Self { + Self(BluetoothConnectOptions { + address, + auto_pair, + make_default, + preferred_profile: preferred_profile + .map(Into::into) + .unwrap_or(BluetoothAudioProfile::None), + timeout_ms, + }) + } +} + +impl From for BluetoothConnectOptions { + fn from(value: PyBluetoothConnectOptions) -> Self { + value.0 + } +} + +#[pyclass(module = "booster_sdk_bindings", name = "BluetoothConnectResult")] +#[derive(Clone)] +pub struct PyBluetoothConnectResult(BluetoothConnectResult); + +#[pymethods] +impl PyBluetoothConnectResult { + #[getter] + fn device(&self) -> PyBluetoothDeviceInfo { + self.0.device.clone().into() + } + #[getter] + fn pulse_sink_id(&self) -> String { + self.0.pulse_sink_id.clone() + } + #[getter] + fn pulse_source_id(&self) -> String { + self.0.pulse_source_id.clone() + } + #[getter] + fn active_profile(&self) -> PyBluetoothAudioProfile { + self.0.active_profile.into() + } + #[getter] + fn pulse_endpoint_ready(&self) -> bool { + self.0.pulse_endpoint_ready + } + #[getter] + fn default_sink_applied(&self) -> bool { + self.0.default_sink_applied + } + #[getter] + fn default_source_applied(&self) -> bool { + self.0.default_source_applied + } + #[getter] + fn default_route_error_code(&self) -> i32 { + self.0.default_route_error_code + } + #[getter] + fn default_route_error_msg(&self) -> String { + self.0.default_route_error_msg.clone() + } +} + +impl From for PyBluetoothConnectResult { + fn from(value: BluetoothConnectResult) -> Self { + Self(value) + } +} + #[pyclass(module = "booster_sdk_bindings", name = "AudioClient", unsendable)] pub struct PyAudioClient { client: Arc, @@ -715,6 +1068,85 @@ impl PyAudioClient { .map(Into::into) .map_err(to_py_err) } + + fn get_devices( + &self, + py: Python<'_>, + query_type: PyAudioDeviceQueryType, + ) -> PyResult> { + let client = Arc::clone(&self.client); + wait_for_future( + py, + async move { client.get_devices(query_type.into()).await }, + ) + .map(|devices| devices.into_iter().map(Into::into).collect()) + .map_err(to_py_err) + } + + #[pyo3(signature = (options=None))] + fn start_bluetooth_scan( + &self, + py: Python<'_>, + options: Option, + ) -> PyResult<()> { + let client = Arc::clone(&self.client); + let options: BluetoothScanOptions = options.map(Into::into).unwrap_or_default(); + wait_for_future( + py, + async move { client.start_bluetooth_scan(&options).await }, + ) + .map_err(to_py_err) + } + + fn stop_bluetooth_scan(&self, py: Python<'_>) -> PyResult<()> { + let client = Arc::clone(&self.client); + wait_for_future(py, async move { client.stop_bluetooth_scan().await }).map_err(to_py_err) + } + + #[pyo3(signature = (include_unpaired=true))] + fn get_bluetooth_devices( + &self, + py: Python<'_>, + include_unpaired: bool, + ) -> PyResult> { + let client = Arc::clone(&self.client); + wait_for_future(py, async move { + client.get_bluetooth_devices(include_unpaired).await + }) + .map(|devices| devices.into_iter().map(Into::into).collect()) + .map_err(to_py_err) + } + + fn connect_bluetooth_device( + &self, + py: Python<'_>, + options: PyBluetoothConnectOptions, + ) -> PyResult { + let client = Arc::clone(&self.client); + let options: BluetoothConnectOptions = options.into(); + wait_for_future(py, async move { + client.connect_bluetooth_device(&options).await + }) + .map(Into::into) + .map_err(to_py_err) + } + + fn disconnect_bluetooth_device(&self, py: Python<'_>, address: String) -> PyResult<()> { + let client = Arc::clone(&self.client); + wait_for_future(py, async move { + client.disconnect_bluetooth_device(address).await + }) + .map_err(to_py_err) + } + + fn forget_bluetooth_device(&self, py: Python<'_>, address: String) -> PyResult<()> { + let client = Arc::clone(&self.client); + wait_for_future( + py, + async move { client.forget_bluetooth_device(address).await }, + ) + .map_err(to_py_err) + } } pub(crate) fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { @@ -733,6 +1165,18 @@ pub(crate) fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; m.add_class::()?; Ok(()) } diff --git a/booster_sdk_py/src/client/booster.rs b/booster_sdk_py/src/client/booster.rs index 0ff530e..a85719e 100644 --- a/booster_sdk_py/src/client/booster.rs +++ b/booster_sdk_py/src/client/booster.rs @@ -4,15 +4,16 @@ use booster_sdk::{ client::loco::{BoosterClient, GripperCommand}, types::{ Action, BodyControl, BoosterHandType, CustomModel, CustomModelParams, CustomTrainedTraj, - DanceId, DexterousFingerParameter, Frame, GaitType, GetModeResponse, GetRobotInfoResponse, - GetStatusResponse, GripperControlMode, GripperMode, GripperMotionParameter, Hand, - HandAction, JointOrder, LoadCustomTrainedTrajResponse, Orientation, Position, Posture, - Quaternion, RobotMode, Transform, VisualKickVersion, WholeBodyDanceId, + DanceId, DeviceInfo, DeviceInfoKind, DexterousFingerParameter, Frame, GaitType, + GetModeResponse, GetRobotInfoResponse, GetStatusResponse, GetUpVersion, GripperControlMode, + GripperMode, GripperMotionParameter, Hand, HandAction, JointOrder, + LoadCustomTrainedTrajResponse, Orientation, Position, Posture, Quaternion, RobotMode, + Transform, VisualKickVersion, WholeBodyDanceId, }, }; use pyo3::{Bound, prelude::*, types::PyModule}; -use crate::{runtime::wait_for_future, startup_wait_from_seconds, to_py_err}; +use crate::{json_value_to_py, runtime::wait_for_future, startup_wait_from_seconds, to_py_err}; #[pyclass(module = "booster_sdk_bindings", name = "RobotMode", eq)] #[derive(Clone, Copy, PartialEq, Eq)] @@ -410,12 +411,17 @@ impl PyGaitType { const HALF_BODY_HUMANLIKE_GAIT: Self = Self(GaitType::HalfBodyHumanlikeGait); #[classattr] const HALF_BODY_HUMANLIKE_GAIT_V2: Self = Self(GaitType::HalfBodyHumanlikeGaitV2); + #[classattr] + const WHOLE_BODY_HUMANLIKE_GAIT_V2: Self = Self(GaitType::WholeBodyHumanlikeGaitV2); fn __repr__(&self) -> String { match self.0 { GaitType::WholeBodyHumanlikeGait => "GaitType.WHOLE_BODY_HUMANLIKE_GAIT".to_string(), GaitType::HalfBodyHumanlikeGait => "GaitType.HALF_BODY_HUMANLIKE_GAIT".to_string(), GaitType::HalfBodyHumanlikeGaitV2 => "GaitType.HALF_BODY_HUMANLIKE_GAIT_V2".to_string(), + GaitType::WholeBodyHumanlikeGaitV2 => { + "GaitType.WHOLE_BODY_HUMANLIKE_GAIT_V2".to_string() + } } } @@ -430,6 +436,81 @@ impl From for GaitType { } } +#[pyclass(module = "booster_sdk_bindings", name = "GetUpVersion", eq)] +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct PyGetUpVersion(GetUpVersion); + +#[pymethods] +impl PyGetUpVersion { + #[classattr] + const V1: Self = Self(GetUpVersion::V1); + #[classattr] + const V2: Self = Self(GetUpVersion::V2); + + fn __int__(&self) -> i32 { + i32::from(self.0) + } +} + +impl From for GetUpVersion { + fn from(value: PyGetUpVersion) -> Self { + value.0 + } +} + +#[pyclass(module = "booster_sdk_bindings", name = "DeviceInfoKind", eq)] +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct PyDeviceInfoKind(DeviceInfoKind); + +#[pymethods] +impl PyDeviceInfoKind { + #[classattr] + const UNKNOWN: Self = Self(DeviceInfoKind::Unknown); + #[classattr] + const SENSORS: Self = Self(DeviceInfoKind::Sensors); + #[classattr] + const HANDS: Self = Self(DeviceInfoKind::Hands); + #[classattr] + const ROBOT_MODEL: Self = Self(DeviceInfoKind::RobotModel); + #[classattr] + const CAMERA: Self = Self(DeviceInfoKind::Camera); + + fn __int__(&self) -> i32 { + i32::from(self.0) + } +} + +#[pyclass(module = "booster_sdk_bindings", name = "DeviceInfo")] +#[derive(Clone)] +pub struct PyDeviceInfo(pub(crate) DeviceInfo); + +#[pymethods] +impl PyDeviceInfo { + #[getter] + fn kind(&self) -> PyDeviceInfoKind { + PyDeviceInfoKind(self.0.kind) + } + + #[getter] + fn body(&self, py: Python<'_>) -> PyResult> { + json_value_to_py(py, &self.0.body) + } + + fn __repr__(&self) -> String { + format!( + "DeviceInfo(kind={}, body={})", + i32::from(self.0.kind), + self.0.body + ) + } +} + +impl From for PyDeviceInfo { + fn from(value: DeviceInfo) -> Self { + Self(value) + } +} + #[pyclass(module = "booster_sdk_bindings", name = "JointOrder", eq)] #[derive(Clone, Copy, PartialEq, Eq)] pub struct PyJointOrder(JointOrder); @@ -491,6 +572,10 @@ impl PyBodyControl { const GOALIE: Self = Self(BodyControl::Goalie); #[classattr] const WBC_GAIT: Self = Self(BodyControl::WbcGait); + #[classattr] + const LION_DANCE_PREPARE_POSE: Self = Self(BodyControl::LionDancePreparePose); + #[classattr] + const VISUAL_KICK_V1: Self = Self(BodyControl::VisualKickV1); fn __repr__(&self) -> String { match self.0 { @@ -507,6 +592,8 @@ impl PyBodyControl { BodyControl::InsideFoot => "BodyControl.INSIDE_FOOT".to_string(), BodyControl::Goalie => "BodyControl.GOALIE".to_string(), BodyControl::WbcGait => "BodyControl.WBC_GAIT".to_string(), + BodyControl::LionDancePreparePose => "BodyControl.LION_DANCE_PREPARE_POSE".to_string(), + BodyControl::VisualKickV1 => "BodyControl.VISUAL_KICK_V1".to_string(), } } @@ -1412,6 +1499,20 @@ impl PyBoosterClient { wait_for_future(py, async move { client.rotate_head(pitch, yaw).await }).map_err(to_py_err) } + fn rotate_head_with_time( + &self, + py: Python<'_>, + pitch: f32, + yaw: f32, + time_millis: i32, + ) -> PyResult<()> { + let client = Arc::clone(&self.client); + wait_for_future(py, async move { + client.rotate_head_with_time(pitch, yaw, time_millis).await + }) + .map_err(to_py_err) + } + fn wave_hand(&self, py: Python<'_>, action: PyHandAction) -> PyResult<()> { let client = Arc::clone(&self.client); wait_for_future(py, async move { client.wave_hand(action.into()).await }).map_err(to_py_err) @@ -1437,20 +1538,60 @@ impl PyBoosterClient { wait_for_future(py, async move { client.lie_down().await }).map_err(to_py_err) } - fn get_up(&self, py: Python<'_>) -> PyResult<()> { + #[pyo3(signature = (version=None))] + fn get_up(&self, py: Python<'_>, version: Option) -> PyResult<()> { let client = Arc::clone(&self.client); - wait_for_future(py, async move { client.get_up().await }).map_err(to_py_err) + wait_for_future(py, async move { + match version { + Some(version) => client.get_up_with_version(version.into()).await, + None => client.get_up().await, + } + }) + .map_err(to_py_err) } - fn get_up_with_mode(&self, py: Python<'_>, mode: PyRobotMode) -> PyResult<()> { + #[pyo3(signature = (mode, version=None))] + fn get_up_with_mode( + &self, + py: Python<'_>, + mode: PyRobotMode, + version: Option, + ) -> PyResult<()> { let client = Arc::clone(&self.client); - wait_for_future( - py, - async move { client.get_up_with_mode(mode.into()).await }, - ) + wait_for_future(py, async move { + match version { + Some(version) => { + client + .get_up_with_mode_and_version(mode.into(), version.into()) + .await + } + None => client.get_up_with_mode(mode.into()).await, + } + }) .map_err(to_py_err) } + fn get_sensors(&self, py: Python<'_>) -> PyResult { + let client = Arc::clone(&self.client); + wait_for_future(py, async move { client.get_sensors().await }) + .map(Into::into) + .map_err(to_py_err) + } + + fn get_hands(&self, py: Python<'_>) -> PyResult { + let client = Arc::clone(&self.client); + wait_for_future(py, async move { client.get_hands().await }) + .map(Into::into) + .map_err(to_py_err) + } + + fn get_robot_model(&self, py: Python<'_>) -> PyResult { + let client = Arc::clone(&self.client); + wait_for_future(py, async move { client.get_robot_model().await }) + .map(Into::into) + .map_err(to_py_err) + } + fn shoot(&self, py: Python<'_>) -> PyResult<()> { let client = Arc::clone(&self.client); wait_for_future(py, async move { client.shoot().await }).map_err(to_py_err) @@ -1824,6 +1965,9 @@ pub(crate) fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/booster_sdk_py/src/client/camera.rs b/booster_sdk_py/src/client/camera.rs new file mode 100644 index 0000000..eef8f56 --- /dev/null +++ b/booster_sdk_py/src/client/camera.rs @@ -0,0 +1,41 @@ +use std::sync::Arc; + +use booster_sdk::client::camera::CameraClient; +use pyo3::{Bound, prelude::*, types::PyModule}; + +use super::booster::PyDeviceInfo; +use crate::{runtime::wait_for_future, startup_wait_from_seconds, to_py_err}; + +#[pyclass(module = "booster_sdk_bindings", name = "CameraClient", unsendable)] +pub struct PyCameraClient { + client: Arc, +} + +#[pymethods] +impl PyCameraClient { + #[new] + #[pyo3(signature = (startup_wait_sec=None))] + fn new(startup_wait_sec: Option) -> PyResult { + let startup_wait = startup_wait_from_seconds(startup_wait_sec)?; + let client = match startup_wait { + Some(wait) => CameraClient::with_startup_wait(wait), + None => CameraClient::new(), + } + .map_err(to_py_err)?; + Ok(Self { + client: Arc::new(client), + }) + } + + fn get_cameras(&self, py: Python<'_>) -> PyResult { + let client = Arc::clone(&self.client); + wait_for_future(py, async move { client.get_cameras().await }) + .map(Into::into) + .map_err(to_py_err) + } +} + +pub(crate) fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_class::()?; + Ok(()) +} diff --git a/booster_sdk_py/src/client/handeye_calib.rs b/booster_sdk_py/src/client/handeye_calib.rs new file mode 100644 index 0000000..7bbf8c2 --- /dev/null +++ b/booster_sdk_py/src/client/handeye_calib.rs @@ -0,0 +1,265 @@ +use std::sync::Arc; + +use booster_sdk::client::handeye_calib::{ + HandEyeCalibApplyResult, HandEyeCalibClient, HandEyeCalibResult, HandEyeCalibStatus, + StartHandEyeCalibParameter, +}; +use pyo3::{Bound, prelude::*, types::PyModule}; + +use crate::{json_value_to_py, runtime::wait_for_future, startup_wait_from_seconds, to_py_err}; + +#[pyclass(module = "booster_sdk_bindings", name = "StartHandEyeCalibParameter")] +#[derive(Clone)] +pub struct PyStartHandEyeCalibParameter(StartHandEyeCalibParameter); + +#[pymethods] +impl PyStartHandEyeCalibParameter { + #[new] + #[pyo3(signature = (publish_feedback=true, square_size_m=None))] + fn new(publish_feedback: bool, square_size_m: Option) -> Self { + Self(StartHandEyeCalibParameter { + publish_feedback, + square_size_m, + }) + } + + #[getter] + fn publish_feedback(&self) -> bool { + self.0.publish_feedback + } + + #[getter] + fn square_size_m(&self) -> Option { + self.0.square_size_m + } +} + +#[pyclass(module = "booster_sdk_bindings", name = "HandEyeCalibStatus")] +#[derive(Clone)] +pub struct PyHandEyeCalibStatus(HandEyeCalibStatus); + +#[pymethods] +impl PyHandEyeCalibStatus { + #[getter] + fn status(&self) -> String { + self.0.status.clone() + } + #[getter] + fn job_id(&self) -> String { + self.0.job_id.clone() + } + #[getter] + fn stage(&self) -> String { + self.0.stage.clone() + } + #[getter] + fn started_at(&self) -> String { + self.0.started_at.clone() + } + #[getter] + fn finished_at(&self) -> String { + self.0.finished_at.clone() + } + #[getter] + fn progress(&self) -> f64 { + self.0.progress + } + #[getter] + fn stage2_done(&self) -> i32 { + self.0.stage2_done + } + #[getter] + fn stage2_total(&self) -> i32 { + self.0.stage2_total + } + #[getter] + fn stage3_done(&self) -> i32 { + self.0.stage3_done + } + #[getter] + fn stage3_total(&self) -> i32 { + self.0.stage3_total + } + #[getter] + fn error(&self, py: Python<'_>) -> PyResult>> { + self.0 + .error + .as_ref() + .map(|value| json_value_to_py(py, value)) + .transpose() + } +} + +impl From for PyHandEyeCalibStatus { + fn from(value: HandEyeCalibStatus) -> Self { + Self(value) + } +} + +#[pyclass(module = "booster_sdk_bindings", name = "HandEyeCalibResult")] +#[derive(Clone)] +pub struct PyHandEyeCalibResult(HandEyeCalibResult); + +#[pymethods] +impl PyHandEyeCalibResult { + #[getter] + fn status(&self) -> String { + self.0.status.clone() + } + #[getter] + fn job_id(&self) -> String { + self.0.job_id.clone() + } + #[getter] + fn summary(&self) -> String { + self.0.summary.clone() + } + #[getter] + fn result(&self, py: Python<'_>) -> PyResult>> { + self.0 + .result + .as_ref() + .map(|value| json_value_to_py(py, value)) + .transpose() + } + #[getter] + fn reprojection_error_px(&self) -> f64 { + self.0.reprojection_error_px + } + #[getter] + fn progress(&self) -> f64 { + self.0.progress + } + #[getter] + fn stage2_done(&self) -> i32 { + self.0.stage2_done + } + #[getter] + fn stage2_total(&self) -> i32 { + self.0.stage2_total + } + #[getter] + fn stage3_done(&self) -> i32 { + self.0.stage3_done + } + #[getter] + fn stage3_total(&self) -> i32 { + self.0.stage3_total + } + #[getter] + fn error(&self, py: Python<'_>) -> PyResult>> { + self.0 + .error + .as_ref() + .map(|value| json_value_to_py(py, value)) + .transpose() + } +} + +impl From for PyHandEyeCalibResult { + fn from(value: HandEyeCalibResult) -> Self { + Self(value) + } +} + +#[pyclass(module = "booster_sdk_bindings", name = "HandEyeCalibApplyResult")] +#[derive(Clone)] +pub struct PyHandEyeCalibApplyResult(HandEyeCalibApplyResult); + +#[pymethods] +impl PyHandEyeCalibApplyResult { + #[getter] + fn status(&self) -> String { + self.0.status.clone() + } + #[getter] + fn applied_path(&self) -> String { + self.0.applied_path.clone() + } + #[getter] + fn error(&self, py: Python<'_>) -> PyResult>> { + self.0 + .error + .as_ref() + .map(|value| json_value_to_py(py, value)) + .transpose() + } +} + +impl From for PyHandEyeCalibApplyResult { + fn from(value: HandEyeCalibApplyResult) -> Self { + Self(value) + } +} + +#[pyclass( + module = "booster_sdk_bindings", + name = "HandEyeCalibClient", + unsendable +)] +pub struct PyHandEyeCalibClient { + client: Arc, +} + +#[pymethods] +impl PyHandEyeCalibClient { + #[new] + #[pyo3(signature = (startup_wait_sec=None))] + fn new(startup_wait_sec: Option) -> PyResult { + let startup_wait = startup_wait_from_seconds(startup_wait_sec)?; + let client = match startup_wait { + Some(wait) => HandEyeCalibClient::with_startup_wait(wait), + None => HandEyeCalibClient::new(), + } + .map_err(to_py_err)?; + Ok(Self { + client: Arc::new(client), + }) + } + + fn start_calibration( + &self, + py: Python<'_>, + param: PyStartHandEyeCalibParameter, + ) -> PyResult<()> { + let client = Arc::clone(&self.client); + let param = param.0; + wait_for_future(py, async move { client.start_calibration(¶m).await }) + .map_err(to_py_err) + } + + fn stop_calibration(&self, py: Python<'_>) -> PyResult<()> { + let client = Arc::clone(&self.client); + wait_for_future(py, async move { client.stop_calibration().await }).map_err(to_py_err) + } + + fn get_status(&self, py: Python<'_>) -> PyResult { + let client = Arc::clone(&self.client); + wait_for_future(py, async move { client.get_status().await }) + .map(Into::into) + .map_err(to_py_err) + } + + fn get_result(&self, py: Python<'_>) -> PyResult { + let client = Arc::clone(&self.client); + wait_for_future(py, async move { client.get_result().await }) + .map(Into::into) + .map_err(to_py_err) + } + + fn apply_result(&self, py: Python<'_>) -> PyResult { + let client = Arc::clone(&self.client); + wait_for_future(py, async move { client.apply_result().await }) + .map(Into::into) + .map_err(to_py_err) + } +} + +pub(crate) fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/booster_sdk_py/src/client/light_control.rs b/booster_sdk_py/src/client/light_control.rs index 5780961..4abcd30 100644 --- a/booster_sdk_py/src/client/light_control.rs +++ b/booster_sdk_py/src/client/light_control.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use booster_sdk::client::light_control::LightControlClient; +use booster_sdk::client::light_control::{LightControlClient, SetLedLightColorParameter}; use pyo3::{Bound, prelude::*, types::PyModule}; use crate::{runtime::wait_for_future, startup_wait_from_seconds, to_py_err}; @@ -37,6 +37,19 @@ impl PyLightControlClient { .map_err(to_py_err) } + fn set_led_light_colors(&self, py: Python<'_>, colors: Vec<(u8, u8, u8)>) -> PyResult<()> { + let client = Arc::clone(&self.client); + let colors: Vec<_> = colors + .into_iter() + .map(|(r, g, b)| SetLedLightColorParameter { r, g, b }) + .collect(); + wait_for_future( + py, + async move { client.set_led_light_colors(&colors).await }, + ) + .map_err(to_py_err) + } + fn stop_led_light_control(&self, py: Python<'_>) -> PyResult<()> { let client = Arc::clone(&self.client); wait_for_future(py, async move { client.stop_led_light_control().await }).map_err(to_py_err) diff --git a/booster_sdk_py/src/client/mod.rs b/booster_sdk_py/src/client/mod.rs index 071a82d..2247592 100644 --- a/booster_sdk_py/src/client/mod.rs +++ b/booster_sdk_py/src/client/mod.rs @@ -1,6 +1,8 @@ mod ai; mod audio; mod booster; +mod camera; +mod handeye_calib; mod light_control; mod lui; mod vision; @@ -10,6 +12,8 @@ use pyo3::{Bound, PyResult, types::PyModule}; pub(crate) fn register_classes(m: &Bound<'_, PyModule>) -> PyResult<()> { booster::register(m)?; + camera::register(m)?; + handeye_calib::register(m)?; ai::register(m)?; audio::register(m)?; lui::register(m)?; diff --git a/booster_sdk_py/src/lib.rs b/booster_sdk_py/src/lib.rs index e26b90e..74f0876 100644 --- a/booster_sdk_py/src/lib.rs +++ b/booster_sdk_py/src/lib.rs @@ -35,6 +35,12 @@ pub(crate) fn startup_wait_from_seconds( Ok(Some(Duration::from_secs_f64(seconds))) } +pub(crate) fn json_value_to_py(py: Python<'_>, value: &serde_json::Value) -> PyResult> { + PyModule::import(py, "json")? + .call_method1("loads", (value.to_string(),)) + .map(Bound::unbind) +} + fn rpc_debug_enabled() -> bool { std::env::var("BOOSTER_RPC_DEBUG") .map(|value| {