From c0f0787e7610b66df71dcc6b1eb9c91c01120f72 Mon Sep 17 00:00:00 2001 From: Gijs de Jong Date: Thu, 30 Apr 2026 17:56:42 +0200 Subject: [PATCH 1/3] Support v1.6 SDK features --- booster_sdk/src/client/audio.rs | 731 +++++++++++++++++ booster_sdk/src/client/loco.rs | 42 +- booster_sdk/src/client/mod.rs | 3 +- booster_sdk/src/dds/rpc.rs | 2 +- booster_sdk/src/types/b1.rs | 29 +- booster_sdk_py/booster_sdk/client/__init__.py | 2 +- booster_sdk_py/booster_sdk/client/audio.py | 41 + booster_sdk_py/booster_sdk/client/booster.py | 4 + .../booster_sdk_bindings.pyi | 436 ++++++++++- booster_sdk_py/src/client/audio.rs | 738 ++++++++++++++++++ booster_sdk_py/src/client/booster.rs | 149 +++- booster_sdk_py/src/client/mod.rs | 2 + 12 files changed, 2163 insertions(+), 16 deletions(-) create mode 100644 booster_sdk/src/client/audio.rs create mode 100644 booster_sdk_py/booster_sdk/client/audio.py create mode 100644 booster_sdk_py/src/client/audio.rs diff --git a/booster_sdk/src/client/audio.rs b/booster_sdk/src/client/audio.rs new file mode 100644 index 0000000..2e743b6 --- /dev/null +++ b/booster_sdk/src/client/audio.rs @@ -0,0 +1,731 @@ +//! Audio service RPC client for Booster SDK v1.6. + +use std::{ + collections::HashMap, + sync::{ + Arc, Mutex, + atomic::{AtomicU64, Ordering}, + }, + time::Duration, +}; + +use serde::{Deserialize, Serialize, de::DeserializeOwned}; +use serde_json::{Map, Value, json}; + +use crate::{ + dds::{RpcClient, RpcClientOptions}, + types::{Result, RpcError}, +}; + +const AUDIO_RPC_API_ID: i32 = 0; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +enum AudioServiceMethod { + RegisterClient, + InitPlayer, + StartPlayer, + PausePlayer, + StopPlayer, + ResetPlayer, + DestroyPlayer, + SetPlayerVolume, + GetPlayerInfo, + SendPcmData, + InitRecorder, + StartRecorder, + PauseRecorder, + StopRecorder, + DestroyRecorder, + GetRecorderInfo, + GetDoaAngle, + SetSystemVolume, + GetSystemVolume, + SetSystemMute, + GetSystemMute, + InitCaptureStream, + StartCaptureStream, + PauseCaptureStream, + StopCaptureStream, + DestroyCaptureStream, + GetCaptureStreamInfo, +} + +impl AudioServiceMethod { + fn topic(self) -> &'static str { + match self { + Self::RegisterClient => "rt/booster/audio/register_client", + Self::InitPlayer => "rt/booster/audio/init_player", + Self::StartPlayer => "rt/booster/audio/start_player", + Self::PausePlayer => "rt/booster/audio/pause_player", + Self::StopPlayer => "rt/booster/audio/stop_player", + Self::ResetPlayer => "rt/booster/audio/reset_player", + Self::DestroyPlayer => "rt/booster/audio/destroy_player", + Self::SetPlayerVolume => "rt/booster/audio/set_volume", + Self::GetPlayerInfo => "rt/booster/audio/get_player_info", + Self::SendPcmData => "rt/booster/audio/send_pcm_data", + Self::InitRecorder => "rt/booster/audio/init_recorder", + Self::StartRecorder => "rt/booster/audio/start_recorder", + Self::PauseRecorder => "rt/booster/audio/pause_recorder", + Self::StopRecorder => "rt/booster/audio/stop_recorder", + Self::DestroyRecorder => "rt/booster/audio/destroy_recorder", + Self::GetRecorderInfo => "rt/booster/audio/get_recorder_info", + Self::GetDoaAngle => "rt/booster/audio/get_doa_angle", + Self::SetSystemVolume => "rt/booster/audio/set_system_volume", + Self::GetSystemVolume => "rt/booster/audio/get_system_volume", + Self::SetSystemMute => "rt/booster/audio/set_system_mute", + Self::GetSystemMute => "rt/booster/audio/get_system_mute", + Self::InitCaptureStream => "rt/booster/audio/init_capture_stream", + Self::StartCaptureStream => "rt/booster/audio/start_capture_stream", + Self::PauseCaptureStream => "rt/booster/audio/pause_capture_stream", + 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", + } + } +} + +crate::api_id_enum! { + /// Audio source type. + AudioSourceType { + PcmFile = 0, + WavFile = 1, + PcmStream = 2, + Mp3File = 3, + } +} + +crate::api_id_enum! { + /// Player state. + PlayerState { + Idle = 0, + Ready = 1, + Playing = 2, + Paused = 3, + Stopped = 4, + Completed = 5, + Error = 6, + } +} + +crate::api_id_enum! { + /// Player priority. + PlayerPriority { + Low = 0, + Medium = 1, + High = 2, + } +} + +crate::api_id_enum! { + /// Recorder state. + RecorderState { + Idle = 0, + Ready = 1, + Recording = 2, + Paused = 3, + Stopped = 4, + Error = 5, + } +} + +crate::api_id_enum! { + /// Audio capture stream state. + AudioCaptureStreamState { + Idle = 0, + Ready = 1, + Streaming = 2, + Paused = 3, + Stopped = 4, + Error = 5, + } +} + +/// PCM format descriptor. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct PcmFormat { + pub sample_rate_hz: i32, + pub channels: i32, + pub bits_per_sample: i32, +} + +impl Default for PcmFormat { + fn default() -> Self { + Self { + sample_rate_hz: 16_000, + channels: 1, + bits_per_sample: 16, + } + } +} + +/// Player initialization options. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PlayerInitOptions { + pub source_type: AudioSourceType, + pub source_uri: String, + pub sample_rate_hz: i32, + pub channels: i32, + pub bits_per_sample: i32, + pub priority: PlayerPriority, +} + +impl PlayerInitOptions { + #[must_use] + pub fn pcm_stream() -> Self { + Self { + source_type: AudioSourceType::PcmStream, + source_uri: "pcm_stream".to_owned(), + sample_rate_hz: 16_000, + channels: 1, + bits_per_sample: 16, + priority: PlayerPriority::Medium, + } + } +} + +/// Recorder initialization options. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RecorderInitOptions { + pub output_path: String, + pub sample_rate_hz: i32, + pub channels: i32, + pub bits_per_sample: i32, +} + +/// Audio capture stream initialization options. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AudioCaptureStreamOptions { + pub enable_raw_pcm: bool, + pub enable_naec_pcm: bool, + pub requested_raw_format: PcmFormat, +} + +/// Generic audio service result. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ServiceResult { + pub ret_code: i32, + #[serde(default)] + pub ret_msg: String, +} + +/// Response returned by player initialization. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct InitPlayerResponse { + pub ret_code: i32, + #[serde(default)] + pub ret_msg: String, + pub session_id: i64, +} + +/// Response returned by recorder initialization. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct InitRecorderResponse { + pub ret_code: i32, + #[serde(default)] + pub ret_msg: String, + pub session_id: i64, +} + +/// Response returned by audio capture stream initialization. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct InitCaptureStreamResponse { + pub ret_code: i32, + #[serde(default)] + pub ret_msg: String, + pub session_id: i64, + #[serde(default)] + pub data_topic_name: String, +} + +/// Player status. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct PlayerInfo { + pub state: i32, + pub played_bytes: i64, + pub total_bytes: i64, + pub volume: f32, +} + +impl PlayerInfo { + #[must_use] + pub fn state_enum(&self) -> Option { + PlayerState::try_from(self.state).ok() + } +} + +/// Recorder status. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RecorderInfo { + pub state: i32, + pub captured_bytes: i64, +} + +impl RecorderInfo { + #[must_use] + pub fn state_enum(&self) -> Option { + RecorderState::try_from(self.state).ok() + } +} + +/// Audio capture stream status. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AudioCaptureStreamInfo { + pub state: i32, + #[serde(default)] + pub raw_enabled: bool, + #[serde(default)] + pub naec_enabled: bool, + #[serde(default)] + pub actual_raw_format: PcmFormat, + #[serde(default)] + pub actual_naec_format: PcmFormat, + #[serde(default)] + pub published_frames: i64, + #[serde(default)] + pub dropped_frames: i64, +} + +impl AudioCaptureStreamInfo { + #[must_use] + pub fn state_enum(&self) -> Option { + AudioCaptureStreamState::try_from(self.state).ok() + } +} + +#[derive(Debug, Deserialize)] +struct RegisterClientResponse { + ret_code: i32, + #[serde(default)] + ret_msg: String, + client_id: String, +} + +#[derive(Debug, Deserialize)] +struct SystemVolumeResponse { + ret_code: i32, + #[serde(default)] + ret_msg: String, + volume: f32, +} + +#[derive(Debug, Deserialize)] +struct SystemMuteResponse { + ret_code: i32, + #[serde(default)] + ret_msg: String, + mute: bool, +} + +#[derive(Debug, Deserialize)] +struct DoaAngleResponse { + ret_code: i32, + #[serde(default)] + ret_msg: String, + angle_deg: i32, +} + +fn service_result_error(ret_code: i32, ret_msg: String) -> crate::types::BoosterError { + RpcError::RequestFailed { + status: ret_code, + message: ret_msg, + } + .into() +} + +fn ensure_ret_code(ret_code: i32, ret_msg: String) -> Result<()> { + if ret_code == 0 { + Ok(()) + } else { + Err(service_result_error(ret_code, ret_msg)) + } +} + +fn serialize_request(request: &T) -> Result { + Ok(serde_json::to_value(request)?) +} + +/// High-level client for the v1.6 audio service. +pub struct AudioClient { + options: RpcClientOptions, + clients: Mutex>>, + client_id: Mutex>, + request_sequence: AtomicU64, +} + +impl AudioClient { + /// Create an audio client with default options. + pub fn new() -> Result { + Self::with_options(RpcClientOptions::default()) + } + + /// Create an audio client with a custom startup wait before first RPC on each audio channel. + pub fn with_startup_wait(startup_wait: Duration) -> Result { + Self::with_options(RpcClientOptions::default().with_startup_wait(startup_wait)) + } + + /// Create an audio client with custom RPC options. + pub fn with_options(options: RpcClientOptions) -> Result { + Ok(Self { + options, + clients: Mutex::new(HashMap::new()), + client_id: Mutex::new(None), + request_sequence: AtomicU64::new(1), + }) + } + + /// Register with the robot audio service and cache the returned client id. + pub async fn init(&self) -> Result { + self.register_client().await + } + + /// Return the cached audio client id, if registration has completed. + pub fn client_id(&self) -> Option { + self.client_id + .lock() + .expect("audio client_id mutex") + .clone() + } + + fn next_request_id(&self) -> String { + let seq = self.request_sequence.fetch_add(1, Ordering::Relaxed); + format!("audio_req_{seq}") + } + + fn rpc_client(&self, method: AudioServiceMethod) -> Result> { + let mut clients = self.clients.lock().expect("audio clients mutex"); + if let Some(client) = clients.get(&method) { + return Ok(Arc::clone(client)); + } + + let client = Arc::new(RpcClient::for_topic( + self.options.clone(), + method.topic().to_owned(), + )?); + clients.insert(method, Arc::clone(&client)); + Ok(client) + } + + async fn register_client(&self) -> Result { + if let Some(client_id) = self.client_id() { + return Ok(client_id); + } + + let response: RegisterClientResponse = self + .call_raw( + AudioServiceMethod::RegisterClient, + json!({ "request_id": self.next_request_id() }), + ) + .await?; + ensure_ret_code(response.ret_code, response.ret_msg)?; + + let mut client_id = self.client_id.lock().expect("audio client_id mutex"); + *client_id = Some(response.client_id.clone()); + Ok(response.client_id) + } + + async fn ensure_registered(&self) -> Result { + match self.client_id() { + Some(client_id) => Ok(client_id), + None => self.register_client().await, + } + } + + async fn call_raw(&self, method: AudioServiceMethod, request: Value) -> Result + where + R: DeserializeOwned + Send + 'static, + { + let client = self.rpc_client(method)?; + client + .call_response(AUDIO_RPC_API_ID, request.to_string()) + .await + } + + async fn call_service(&self, method: AudioServiceMethod, request: Value) -> 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.call_raw(method, Value::Object(object)).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) + } + + /// Initialize an audio player and return its session id response. + pub async fn init_player(&self, options: &PlayerInitOptions) -> Result { + let response: InitPlayerResponse = self + .call_service(AudioServiceMethod::InitPlayer, serialize_request(options)?) + .await?; + ensure_ret_code(response.ret_code, response.ret_msg.clone())?; + Ok(response) + } + + pub async fn start_player(&self, session_id: i64) -> Result<()> { + self.call_result( + AudioServiceMethod::StartPlayer, + json!({ "session_id": session_id }), + ) + .await + } + + pub async fn pause_player(&self, session_id: i64) -> Result<()> { + self.call_result( + AudioServiceMethod::PausePlayer, + json!({ "session_id": session_id }), + ) + .await + } + + pub async fn stop_player(&self, session_id: i64) -> Result<()> { + self.call_result( + AudioServiceMethod::StopPlayer, + json!({ "session_id": session_id }), + ) + .await + } + + pub async fn reset_player(&self, session_id: i64) -> Result<()> { + self.call_result( + AudioServiceMethod::ResetPlayer, + json!({ "session_id": session_id }), + ) + .await + } + + pub async fn destroy_player(&self, session_id: i64) -> Result<()> { + self.call_result( + AudioServiceMethod::DestroyPlayer, + json!({ "session_id": session_id }), + ) + .await + } + + pub async fn set_player_volume(&self, session_id: i64, volume: f32) -> Result<()> { + self.call_result( + AudioServiceMethod::SetPlayerVolume, + json!({ "session_id": session_id, "volume": volume }), + ) + .await + } + + pub async fn get_player_info(&self, session_id: i64) -> Result { + #[derive(Deserialize)] + struct Response { + ret_code: i32, + #[serde(default)] + ret_msg: String, + state: i32, + played_bytes: i64, + total_bytes: i64, + volume: f32, + } + + let response: Response = self + .call_service( + AudioServiceMethod::GetPlayerInfo, + json!({ "session_id": session_id }), + ) + .await?; + ensure_ret_code(response.ret_code, response.ret_msg)?; + Ok(PlayerInfo { + state: response.state, + played_bytes: response.played_bytes, + total_bytes: response.total_bytes, + volume: response.volume, + }) + } + + pub async fn send_pcm_data(&self, session_id: i64, pcm_bytes: Vec) -> Result<()> { + self.call_result( + AudioServiceMethod::SendPcmData, + json!({ "session_id": session_id, "pcm_bytes": pcm_bytes }), + ) + .await + } + + pub async fn init_recorder( + &self, + options: &RecorderInitOptions, + ) -> Result { + let response: InitRecorderResponse = self + .call_service( + AudioServiceMethod::InitRecorder, + serialize_request(options)?, + ) + .await?; + ensure_ret_code(response.ret_code, response.ret_msg.clone())?; + Ok(response) + } + + pub async fn start_recorder(&self, session_id: i64) -> Result<()> { + self.call_result( + AudioServiceMethod::StartRecorder, + json!({ "session_id": session_id }), + ) + .await + } + + pub async fn pause_recorder(&self, session_id: i64) -> Result<()> { + self.call_result( + AudioServiceMethod::PauseRecorder, + json!({ "session_id": session_id }), + ) + .await + } + + pub async fn stop_recorder(&self, session_id: i64) -> Result<()> { + self.call_result( + AudioServiceMethod::StopRecorder, + json!({ "session_id": session_id }), + ) + .await + } + + pub async fn destroy_recorder(&self, session_id: i64) -> Result<()> { + self.call_result( + AudioServiceMethod::DestroyRecorder, + json!({ "session_id": session_id }), + ) + .await + } + + pub async fn get_recorder_info(&self, session_id: i64) -> Result { + #[derive(Deserialize)] + struct Response { + ret_code: i32, + #[serde(default)] + ret_msg: String, + state: i32, + captured_bytes: i64, + } + + let response: Response = self + .call_service( + AudioServiceMethod::GetRecorderInfo, + json!({ "session_id": session_id }), + ) + .await?; + ensure_ret_code(response.ret_code, response.ret_msg)?; + Ok(RecorderInfo { + state: response.state, + captured_bytes: response.captured_bytes, + }) + } + + pub async fn get_doa_angle(&self) -> Result { + let response: DoaAngleResponse = self + .call_service(AudioServiceMethod::GetDoaAngle, Value::Null) + .await?; + ensure_ret_code(response.ret_code, response.ret_msg)?; + Ok(response.angle_deg) + } + + pub async fn set_system_volume(&self, volume: f32) -> Result<()> { + self.call_result( + AudioServiceMethod::SetSystemVolume, + json!({ "volume": volume }), + ) + .await + } + + pub async fn get_system_volume(&self) -> Result { + let response: SystemVolumeResponse = self + .call_service(AudioServiceMethod::GetSystemVolume, Value::Null) + .await?; + ensure_ret_code(response.ret_code, response.ret_msg)?; + Ok(response.volume) + } + + pub async fn set_system_mute(&self, mute: bool) -> Result<()> { + self.call_result(AudioServiceMethod::SetSystemMute, json!({ "mute": mute })) + .await + } + + pub async fn get_system_mute(&self) -> Result { + let response: SystemMuteResponse = self + .call_service(AudioServiceMethod::GetSystemMute, Value::Null) + .await?; + ensure_ret_code(response.ret_code, response.ret_msg)?; + Ok(response.mute) + } + + pub async fn init_capture_stream( + &self, + options: &AudioCaptureStreamOptions, + ) -> Result { + let response: InitCaptureStreamResponse = self + .call_service( + AudioServiceMethod::InitCaptureStream, + serialize_request(options)?, + ) + .await?; + ensure_ret_code(response.ret_code, response.ret_msg.clone())?; + Ok(response) + } + + pub async fn start_capture_stream(&self, session_id: i64) -> Result<()> { + self.call_result( + AudioServiceMethod::StartCaptureStream, + json!({ "session_id": session_id }), + ) + .await + } + + pub async fn pause_capture_stream(&self, session_id: i64) -> Result<()> { + self.call_result( + AudioServiceMethod::PauseCaptureStream, + json!({ "session_id": session_id }), + ) + .await + } + + pub async fn stop_capture_stream(&self, session_id: i64) -> Result<()> { + self.call_result( + AudioServiceMethod::StopCaptureStream, + json!({ "session_id": session_id }), + ) + .await + } + + pub async fn destroy_capture_stream(&self, session_id: i64) -> Result<()> { + self.call_result( + AudioServiceMethod::DestroyCaptureStream, + json!({ "session_id": session_id }), + ) + .await + } + + pub async fn get_capture_stream_info(&self, session_id: i64) -> Result { + #[derive(Deserialize)] + struct Response { + ret_code: i32, + #[serde(default)] + ret_msg: String, + #[serde(flatten)] + info: AudioCaptureStreamInfo, + } + + let response: Response = self + .call_service( + AudioServiceMethod::GetCaptureStreamInfo, + json!({ "session_id": session_id }), + ) + .await?; + ensure_ret_code(response.ret_code, response.ret_msg)?; + Ok(response.info) + } +} diff --git a/booster_sdk/src/client/loco.rs b/booster_sdk/src/client/loco.rs index 8340afa..3524094 100644 --- a/booster_sdk/src/client/loco.rs +++ b/booster_sdk/src/client/loco.rs @@ -11,10 +11,10 @@ use crate::dds::{ video_stream_topic, }; use crate::types::{ - BoosterHandType, CustomTrainedTraj, DanceId, DexterousFingerParameter, Frame, GetModeResponse, - GetRobotInfoResponse, GetStatusResponse, GripperControlMode, GripperMode, + BoosterHandType, CustomTrainedTraj, DanceId, DexterousFingerParameter, Frame, GaitType, + GetModeResponse, GetRobotInfoResponse, GetStatusResponse, GripperControlMode, GripperMode, GripperMotionParameter, Hand, HandAction, HandIndex, LoadCustomTrainedTrajResponse, LocoApiId, - Result, RobotMode, Transform, WholeBodyDanceId, + Result, RobotMode, Transform, VisualKickVersion, WholeBodyDanceId, }; use serde::{Deserialize, Serialize}; use serde_json::json; @@ -390,10 +390,44 @@ impl BoosterClient { .await } + /// Start or stop a visual kick (side-foot kick) with an explicit behavior version. + pub async fn visual_kick_with_version( + &self, + start: bool, + version: VisualKickVersion, + ) -> Result<()> { + let param = json!({ "start": start, "version": i32::from(version) }).to_string(); + self.rpc.call_void(LocoApiId::VisualKick, param).await + } + /// Start or stop a visual kick (side-foot kick). pub async fn visual_kick(&self, start: bool) -> Result<()> { + self.visual_kick_with_version(start, VisualKickVersion::V1) + .await + } + + /// Enter or exit the lion dance prepare posture. + pub async fn lion_dance_prepare(&self, start: bool) -> Result<()> { let param = json!({ "start": start }).to_string(); - self.rpc.call_void(LocoApiId::VisualKick, param).await + self.rpc.call_void(LocoApiId::LionDancePrepare, param).await + } + + /// Start a lion dance by numeric dance index. + pub async fn lion_dance_start(&self, dance_idx: i32) -> Result<()> { + let param = json!({ "dance_idx": dance_idx }).to_string(); + self.rpc.call_void(LocoApiId::LionDanceStart, param).await + } + + /// Start or stop lion dance movement synchronization. + pub async fn lion_dance_move(&self, start: bool) -> Result<()> { + let param = json!({ "start": start }).to_string(); + self.rpc.call_void(LocoApiId::LionDanceMove, param).await + } + + /// Switch robot gait type. + pub async fn switch_gait(&self, gait_type: GaitType) -> Result<()> { + let param = json!({ "gait_type": i32::from(gait_type) }).to_string(); + self.rpc.call_void(LocoApiId::SwitchGait, param).await } /// Publish a raw gripper control topic message. diff --git a/booster_sdk/src/client/mod.rs b/booster_sdk/src/client/mod.rs index e4f615f..f5957e2 100644 --- a/booster_sdk/src/client/mod.rs +++ b/booster_sdk/src/client/mod.rs @@ -1,6 +1,7 @@ //! High-level client APIs for the Booster Robotics SDK. pub mod ai; +pub mod audio; pub mod light_control; pub mod loco; pub mod vision; @@ -105,7 +106,7 @@ macro_rules! api_id_enum { impl TryFrom for $name { type Error = &'static str; - fn try_from(value: i32) -> std::result::Result { + fn try_from(value: i32) -> std::result::Result { match value { $( $value => Ok(Self::$variant), diff --git a/booster_sdk/src/dds/rpc.rs b/booster_sdk/src/dds/rpc.rs index e213cd4..b7b9f49 100644 --- a/booster_sdk/src/dds/rpc.rs +++ b/booster_sdk/src/dds/rpc.rs @@ -15,7 +15,7 @@ use super::DdsNode; use super::messages::{RpcReqMsg, RpcRespMsg}; use super::topics::{LOCO_API_TOPIC, rpc_request_topic, rpc_response_topic}; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct RpcClientOptions { pub domain_id: u16, pub default_timeout: Duration, diff --git a/booster_sdk/src/types/b1.rs b/booster_sdk/src/types/b1.rs index 76122ae..0954418 100644 --- a/booster_sdk/src/types/b1.rs +++ b/booster_sdk/src/types/b1.rs @@ -43,6 +43,19 @@ crate::api_id_enum! { ExitWbcGait = 2036, MoveDualHandEndEffector = 2037, VisualKick = 2038, + LionDancePrepare = 2039, + LionDanceStart = 2040, + LionDanceMove = 2041, + SwitchGait = 2042, + } +} + +crate::api_id_enum! { + /// B1 v1.6 gait selectors. + GaitType { + WholeBodyHumanlikeGait = 0, + HalfBodyHumanlikeGait = 1, + HalfBodyHumanlikeGaitV2 = 2, } } @@ -140,9 +153,19 @@ crate::api_id_enum! { MichaelDance1 = 1, MichaelDance2 = 2, MichaelDance3 = 3, - MoonWalk = 4, BoxingStyleKick = 5, RoundhouseKick = 6, + ShanHeGuRenDance = 7, + GaiGeChunFengDance = 8, + MichaelDance1And2 = 9, + } +} + +crate::api_id_enum! { + /// Visual kick behavior version. + VisualKickVersion { + V1 = 0, + V2 = 1, } } @@ -272,6 +295,10 @@ pub struct GetRobotInfoResponse { pub version: String, pub model: String, pub serial_number: String, + #[serde(default)] + pub edition: String, + #[serde(default)] + pub region: String, } /// Model parameters used by custom trajectories. diff --git a/booster_sdk_py/booster_sdk/client/__init__.py b/booster_sdk_py/booster_sdk/client/__init__.py index 6e49639..72e9b28 100644 --- a/booster_sdk_py/booster_sdk/client/__init__.py +++ b/booster_sdk_py/booster_sdk/client/__init__.py @@ -2,4 +2,4 @@ from __future__ import annotations -__all__ = ["ai", "booster", "light_control", "lui", "vision", "x5_camera"] +__all__ = ["ai", "audio", "booster", "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 new file mode 100644 index 0000000..aef0b4c --- /dev/null +++ b/booster_sdk_py/booster_sdk/client/audio.py @@ -0,0 +1,41 @@ +"""Audio service client bindings.""" + +from __future__ import annotations + +import booster_sdk_bindings as bindings + +AudioClient = bindings.AudioClient +AudioSourceType = bindings.AudioSourceType +PlayerPriority = bindings.PlayerPriority +PlayerState = bindings.PlayerState +RecorderState = bindings.RecorderState +AudioCaptureStreamState = bindings.AudioCaptureStreamState +PcmFormat = bindings.PcmFormat +PlayerInitOptions = bindings.PlayerInitOptions +RecorderInitOptions = bindings.RecorderInitOptions +AudioCaptureStreamOptions = bindings.AudioCaptureStreamOptions +InitPlayerResponse = bindings.InitPlayerResponse +InitRecorderResponse = bindings.InitRecorderResponse +InitCaptureStreamResponse = bindings.InitCaptureStreamResponse +PlayerInfo = bindings.PlayerInfo +RecorderInfo = bindings.RecorderInfo +AudioCaptureStreamInfo = bindings.AudioCaptureStreamInfo + +__all__ = [ + "AudioClient", + "AudioSourceType", + "PlayerPriority", + "PlayerState", + "RecorderState", + "AudioCaptureStreamState", + "PcmFormat", + "PlayerInitOptions", + "RecorderInitOptions", + "AudioCaptureStreamOptions", + "InitPlayerResponse", + "InitRecorderResponse", + "InitCaptureStreamResponse", + "PlayerInfo", + "RecorderInfo", + "AudioCaptureStreamInfo", +] diff --git a/booster_sdk_py/booster_sdk/client/booster.py b/booster_sdk_py/booster_sdk/client/booster.py index 4a5df12..a39d53c 100644 --- a/booster_sdk_py/booster_sdk/client/booster.py +++ b/booster_sdk_py/booster_sdk/client/booster.py @@ -16,6 +16,8 @@ BoosterHandType = bindings.BoosterHandType DanceId = bindings.DanceId WholeBodyDanceId = bindings.WholeBodyDanceId +VisualKickVersion = bindings.VisualKickVersion +GaitType = bindings.GaitType JointOrder = bindings.JointOrder BodyControl = bindings.BodyControl Action = bindings.Action @@ -47,6 +49,8 @@ "BoosterHandType", "DanceId", "WholeBodyDanceId", + "VisualKickVersion", + "GaitType", "JointOrder", "BodyControl", "Action", 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 59b9467..13350b4 100644 --- a/booster_sdk_py/booster_sdk_bindings/booster_sdk_bindings.pyi +++ b/booster_sdk_py/booster_sdk_bindings/booster_sdk_bindings.pyi @@ -171,9 +171,11 @@ class WholeBodyDanceId: MICHAEL_DANCE_1: WholeBodyDanceId MICHAEL_DANCE_2: WholeBodyDanceId MICHAEL_DANCE_3: WholeBodyDanceId - MOON_WALK: WholeBodyDanceId BOXING_STYLE_KICK: WholeBodyDanceId ROUNDHOUSE_KICK: WholeBodyDanceId + SHAN_HE_GU_REN_DANCE: WholeBodyDanceId + GAI_GE_CHUN_FENG_DANCE: WholeBodyDanceId + MICHAEL_DANCE_1_AND_2: WholeBodyDanceId def __repr__(self) -> str: """Return a stable enum-style representation.""" @@ -185,6 +187,39 @@ class WholeBodyDanceId: """Return ``True`` when both values represent the same dance id.""" ... +class VisualKickVersion: + """Visual-kick behavior version for :meth:`BoosterClient.visual_kick_with_version`.""" + + V1: VisualKickVersion + V2: VisualKickVersion + + def __repr__(self) -> str: + """Return a stable enum-style representation.""" + ... + def __int__(self) -> int: + """Return the raw integer value used by the RPC API.""" + ... + def __eq__(self, other: object) -> bool: + """Return ``True`` when both values represent the same version.""" + ... + +class GaitType: + """Gait selectors for :meth:`BoosterClient.switch_gait`.""" + + WHOLE_BODY_HUMANLIKE_GAIT: GaitType + HALF_BODY_HUMANLIKE_GAIT: GaitType + HALF_BODY_HUMANLIKE_GAIT_V2: GaitType + + def __repr__(self) -> str: + """Return a stable enum-style representation.""" + ... + def __int__(self) -> int: + """Return the raw integer value used by the RPC API.""" + ... + def __eq__(self, other: object) -> bool: + """Return ``True`` when both values represent the same gait.""" + ... + class JointOrder: """Joint indexing convention used by custom trajectory models.""" @@ -738,6 +773,8 @@ class GetRobotInfoResponse: version: str, model: str, serial_number: str, + edition: str = ..., + region: str = ..., ) -> None: """Create robot info payload.""" ... @@ -767,6 +804,16 @@ class GetRobotInfoResponse: """Hardware serial number.""" ... + @property + def edition(self) -> str: + """Robot edition string.""" + ... + + @property + def region(self) -> str: + """Robot region string.""" + ... + class LoadCustomTrainedTrajResponse: """Response for ``load_custom_trained_traj`` containing trajectory id.""" @@ -1060,6 +1107,26 @@ class BoosterClient: """Start or stop a visual kick (side-foot kick).""" ... + def visual_kick_with_version(self, start: bool, version: VisualKickVersion) -> None: + """Start or stop visual kick using a specific v1.6 behavior version.""" + ... + + def lion_dance_prepare(self, start: bool) -> None: + """Start or stop lion-dance preparation.""" + ... + + def lion_dance_start(self, dance_idx: int) -> None: + """Start lion dance routine by dance index.""" + ... + + def lion_dance_move(self, start: bool) -> None: + """Start or stop lion-dance movement.""" + ... + + def switch_gait(self, gait_type: GaitType) -> None: + """Switch between supported humanlike gait controllers.""" + ... + def publish_gripper_command(self, command: GripperCommand) -> None: """Publish low-level gripper command message.""" ... @@ -1074,6 +1141,373 @@ class BoosterClient: """Convenience wrapper for publishing gripper command fields.""" ... +class AudioSourceType: + """Audio source type accepted by :class:`PlayerInitOptions`.""" + + PCM_FILE: AudioSourceType + WAV_FILE: AudioSourceType + PCM_STREAM: AudioSourceType + MP3_FILE: AudioSourceType + + def __int__(self) -> int: + """Return the raw integer value used by the RPC API.""" + ... + def __eq__(self, other: object) -> bool: + """Return ``True`` when both values represent the same source type.""" + ... + +class PlayerPriority: + """Playback priority used when initializing a player session.""" + + LOW: PlayerPriority + MEDIUM: PlayerPriority + HIGH: PlayerPriority + + def __int__(self) -> int: + """Return the raw integer value used by the RPC API.""" + ... + def __eq__(self, other: object) -> bool: + """Return ``True`` when both values represent the same priority.""" + ... + +class PlayerState: + """Player state values returned by :meth:`PlayerInfo.state_enum`.""" + + IDLE: PlayerState + READY: PlayerState + PLAYING: PlayerState + PAUSED: PlayerState + STOPPED: PlayerState + COMPLETED: PlayerState + ERROR: PlayerState + + def __int__(self) -> int: + """Return the raw integer value used by the RPC API.""" + ... + def __eq__(self, other: object) -> bool: + """Return ``True`` when both values represent the same state.""" + ... + +class RecorderState: + """Recorder state values returned by :meth:`RecorderInfo.state_enum`.""" + + IDLE: RecorderState + READY: RecorderState + RECORDING: RecorderState + PAUSED: RecorderState + STOPPED: RecorderState + ERROR: RecorderState + + def __int__(self) -> int: + """Return the raw integer value used by the RPC API.""" + ... + def __eq__(self, other: object) -> bool: + """Return ``True`` when both values represent the same state.""" + ... + +class AudioCaptureStreamState: + """Capture-stream state values returned by :meth:`AudioCaptureStreamInfo.state_enum`.""" + + IDLE: AudioCaptureStreamState + READY: AudioCaptureStreamState + STREAMING: AudioCaptureStreamState + PAUSED: AudioCaptureStreamState + STOPPED: AudioCaptureStreamState + ERROR: AudioCaptureStreamState + + def __int__(self) -> int: + """Return the raw integer value used by the RPC API.""" + ... + def __eq__(self, other: object) -> bool: + """Return ``True`` when both values represent the same state.""" + ... + +class PcmFormat: + """PCM sample format used by audio player, recorder, and capture APIs.""" + + def __init__( + self, + sample_rate_hz: int = ..., + channels: int = ..., + bits_per_sample: int = ..., + ) -> None: + """Create a PCM format descriptor.""" + ... + @property + def sample_rate_hz(self) -> int: + """Sample rate in Hz.""" + ... + @property + def channels(self) -> int: + """Number of audio channels.""" + ... + @property + def bits_per_sample(self) -> int: + """Bits per PCM sample.""" + ... + +class PlayerInitOptions: + """Options used to initialize an audio playback session.""" + + def __init__( + self, + source_type: AudioSourceType, + source_uri: str, + sample_rate_hz: int = ..., + channels: int = ..., + bits_per_sample: int = ..., + priority: PlayerPriority | None = ..., + ) -> None: + """Create playback initialization options.""" + ... + @staticmethod + def pcm_stream() -> PlayerInitOptions: + """Create options for a PCM streaming player session.""" + ... + +class RecorderInitOptions: + """Options used to initialize an audio recorder session.""" + + def __init__( + self, + output_path: str, + sample_rate_hz: int = ..., + channels: int = ..., + bits_per_sample: int = ..., + ) -> None: + """Create recorder initialization options.""" + ... + +class AudioCaptureStreamOptions: + """Options used to initialize an audio capture stream session.""" + + def __init__( + self, + enable_raw_pcm: bool = ..., + enable_naec_pcm: bool = ..., + requested_raw_format: PcmFormat | None = ..., + ) -> None: + """Create capture-stream initialization options.""" + ... + +class InitPlayerResponse: + """Response returned by :meth:`AudioClient.init_player`.""" + + @property + def ret_code(self) -> int: + """Service return code.""" + ... + @property + def ret_msg(self) -> str: + """Service return message.""" + ... + @property + def session_id(self) -> int: + """Initialized player session id.""" + ... + +class InitRecorderResponse: + """Response returned by :meth:`AudioClient.init_recorder`.""" + + @property + def ret_code(self) -> int: + """Service return code.""" + ... + @property + def ret_msg(self) -> str: + """Service return message.""" + ... + @property + def session_id(self) -> int: + """Initialized session id.""" + ... + +class InitCaptureStreamResponse: + """Response returned by :meth:`AudioClient.init_capture_stream`.""" + + @property + def ret_code(self) -> int: + """Service return code.""" + ... + @property + def ret_msg(self) -> str: + """Service return message.""" + ... + @property + def session_id(self) -> int: + """Initialized capture stream session id.""" + ... + @property + def data_topic_name(self) -> str: + """DDS topic name carrying capture frame data for this session.""" + ... + +class PlayerInfo: + """Current player session state.""" + + @property + def state(self) -> int: + """Raw player state value.""" + ... + def state_enum(self) -> PlayerState | None: + """Player state converted to ``PlayerState`` when known.""" + ... + @property + def played_bytes(self) -> int: + """Number of bytes already played.""" + ... + @property + def total_bytes(self) -> int: + """Total bytes for the playback source when known.""" + ... + @property + def volume(self) -> float: + """Player volume.""" + ... + +class RecorderInfo: + """Current recorder session state.""" + + @property + def state(self) -> int: + """Raw recorder state value.""" + ... + def state_enum(self) -> RecorderState | None: + """Recorder state converted to ``RecorderState`` when known.""" + ... + @property + def captured_bytes(self) -> int: + """Number of bytes captured by the recorder.""" + ... + +class AudioCaptureStreamInfo: + """Current audio capture stream state.""" + + @property + def state(self) -> int: + """Raw capture-stream state value.""" + ... + def state_enum(self) -> AudioCaptureStreamState | None: + """Capture-stream state converted to ``AudioCaptureStreamState`` when known.""" + ... + @property + def raw_enabled(self) -> bool: + """Whether raw PCM capture is enabled.""" + ... + @property + def naec_enabled(self) -> bool: + """Whether NAEC PCM capture is enabled.""" + ... + @property + def actual_raw_format(self) -> PcmFormat: + """Actual raw PCM format returned by the service.""" + ... + @property + def actual_naec_format(self) -> PcmFormat: + """Actual NAEC PCM format returned by the service.""" + ... + @property + def published_frames(self) -> int: + """Number of capture frames published by the service.""" + ... + @property + def dropped_frames(self) -> int: + """Number of capture frames dropped by the service.""" + ... + +class AudioClient: + """Client for the v1.6 audio service RPC APIs.""" + + def __init__(self, startup_wait_sec: float | None = ...) -> None: + """Create an audio client.""" + ... + def init(self) -> str: + """Register this client with the audio service and return its client id.""" + ... + def client_id(self) -> str | None: + """Return the cached audio service client id, if initialized.""" + ... + def init_player(self, options: PlayerInitOptions) -> InitPlayerResponse: + """Initialize a player session.""" + ... + def start_player(self, session_id: int) -> None: + """Start playback for a player session.""" + ... + def pause_player(self, session_id: int) -> None: + """Pause playback for a player session.""" + ... + def stop_player(self, session_id: int) -> None: + """Stop playback for a player session.""" + ... + def reset_player(self, session_id: int) -> None: + """Reset a player session.""" + ... + def destroy_player(self, session_id: int) -> None: + """Destroy a player session.""" + ... + def set_player_volume(self, session_id: int, volume: float) -> None: + """Set volume for a player session.""" + ... + def get_player_info(self, session_id: int) -> PlayerInfo: + """Fetch current player session info.""" + ... + def send_pcm_data(self, session_id: int, pcm_bytes: bytes) -> None: + """Send PCM bytes to a PCM stream player session.""" + ... + def init_recorder(self, options: RecorderInitOptions) -> InitRecorderResponse: + """Initialize a recorder session.""" + ... + def start_recorder(self, session_id: int) -> None: + """Start recording.""" + ... + def pause_recorder(self, session_id: int) -> None: + """Pause recording.""" + ... + def stop_recorder(self, session_id: int) -> None: + """Stop recording.""" + ... + def destroy_recorder(self, session_id: int) -> None: + """Destroy a recorder session.""" + ... + def get_recorder_info(self, session_id: int) -> RecorderInfo: + """Fetch current recorder session info.""" + ... + def get_doa_angle(self) -> int: + """Fetch direction-of-arrival angle in degrees.""" + ... + def set_system_volume(self, volume: float) -> None: + """Set system output volume.""" + ... + def get_system_volume(self) -> float: + """Fetch system output volume.""" + ... + def set_system_mute(self, mute: bool) -> None: + """Set system mute state.""" + ... + def get_system_mute(self) -> bool: + """Fetch system mute state.""" + ... + def init_capture_stream( + self, options: AudioCaptureStreamOptions + ) -> InitCaptureStreamResponse: + """Initialize an audio capture stream session.""" + ... + def start_capture_stream(self, session_id: int) -> None: + """Start capture streaming.""" + ... + def pause_capture_stream(self, session_id: int) -> None: + """Pause capture streaming.""" + ... + def stop_capture_stream(self, session_id: int) -> None: + """Stop capture streaming.""" + ... + def destroy_capture_stream(self, session_id: int) -> None: + """Destroy a capture stream session.""" + ... + def get_capture_stream_info(self, session_id: int) -> AudioCaptureStreamInfo: + """Fetch current capture stream session info.""" + ... + class AiClient: """Client for AI chat and speech features.""" diff --git a/booster_sdk_py/src/client/audio.rs b/booster_sdk_py/src/client/audio.rs new file mode 100644 index 0000000..c7efb10 --- /dev/null +++ b/booster_sdk_py/src/client/audio.rs @@ -0,0 +1,738 @@ +use std::sync::Arc; + +use booster_sdk::client::audio::{ + AudioCaptureStreamInfo, AudioCaptureStreamOptions, AudioCaptureStreamState, AudioClient, + AudioSourceType, InitCaptureStreamResponse, InitPlayerResponse, InitRecorderResponse, + PcmFormat, PlayerInfo, PlayerInitOptions, PlayerPriority, PlayerState, RecorderInfo, + RecorderInitOptions, RecorderState, +}; +use pyo3::{Bound, prelude::*, types::PyModule}; + +use crate::{runtime::wait_for_future, startup_wait_from_seconds, to_py_err}; + +#[pyclass(module = "booster_sdk_bindings", name = "AudioSourceType", eq)] +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct PyAudioSourceType(AudioSourceType); + +#[pymethods] +impl PyAudioSourceType { + #[classattr] + const PCM_FILE: Self = Self(AudioSourceType::PcmFile); + #[classattr] + const WAV_FILE: Self = Self(AudioSourceType::WavFile); + #[classattr] + const PCM_STREAM: Self = Self(AudioSourceType::PcmStream); + #[classattr] + const MP3_FILE: Self = Self(AudioSourceType::Mp3File); + + fn __int__(&self) -> i32 { + i32::from(self.0) + } +} + +impl From for AudioSourceType { + fn from(value: PyAudioSourceType) -> Self { + value.0 + } +} + +#[pyclass(module = "booster_sdk_bindings", name = "PlayerPriority", eq)] +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct PyPlayerPriority(PlayerPriority); + +#[pymethods] +impl PyPlayerPriority { + #[classattr] + const LOW: Self = Self(PlayerPriority::Low); + #[classattr] + const MEDIUM: Self = Self(PlayerPriority::Medium); + #[classattr] + const HIGH: Self = Self(PlayerPriority::High); + + fn __int__(&self) -> i32 { + i32::from(self.0) + } +} + +impl From for PlayerPriority { + fn from(value: PyPlayerPriority) -> Self { + value.0 + } +} + +#[pyclass(module = "booster_sdk_bindings", name = "PlayerState", eq)] +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct PyPlayerState(PlayerState); + +#[pymethods] +impl PyPlayerState { + #[classattr] + const IDLE: Self = Self(PlayerState::Idle); + #[classattr] + const READY: Self = Self(PlayerState::Ready); + #[classattr] + const PLAYING: Self = Self(PlayerState::Playing); + #[classattr] + const PAUSED: Self = Self(PlayerState::Paused); + #[classattr] + const STOPPED: Self = Self(PlayerState::Stopped); + #[classattr] + const COMPLETED: Self = Self(PlayerState::Completed); + #[classattr] + const ERROR: Self = Self(PlayerState::Error); + + fn __int__(&self) -> i32 { + i32::from(self.0) + } +} + +impl From for PyPlayerState { + fn from(value: PlayerState) -> Self { + Self(value) + } +} + +#[pyclass(module = "booster_sdk_bindings", name = "RecorderState", eq)] +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct PyRecorderState(RecorderState); + +#[pymethods] +impl PyRecorderState { + #[classattr] + const IDLE: Self = Self(RecorderState::Idle); + #[classattr] + const READY: Self = Self(RecorderState::Ready); + #[classattr] + const RECORDING: Self = Self(RecorderState::Recording); + #[classattr] + const PAUSED: Self = Self(RecorderState::Paused); + #[classattr] + const STOPPED: Self = Self(RecorderState::Stopped); + #[classattr] + const ERROR: Self = Self(RecorderState::Error); + + fn __int__(&self) -> i32 { + i32::from(self.0) + } +} + +impl From for PyRecorderState { + fn from(value: RecorderState) -> Self { + Self(value) + } +} + +#[pyclass(module = "booster_sdk_bindings", name = "AudioCaptureStreamState", eq)] +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct PyAudioCaptureStreamState(AudioCaptureStreamState); + +#[pymethods] +impl PyAudioCaptureStreamState { + #[classattr] + const IDLE: Self = Self(AudioCaptureStreamState::Idle); + #[classattr] + const READY: Self = Self(AudioCaptureStreamState::Ready); + #[classattr] + const STREAMING: Self = Self(AudioCaptureStreamState::Streaming); + #[classattr] + const PAUSED: Self = Self(AudioCaptureStreamState::Paused); + #[classattr] + const STOPPED: Self = Self(AudioCaptureStreamState::Stopped); + #[classattr] + const ERROR: Self = Self(AudioCaptureStreamState::Error); + + fn __int__(&self) -> i32 { + i32::from(self.0) + } +} + +impl From for PyAudioCaptureStreamState { + fn from(value: AudioCaptureStreamState) -> Self { + Self(value) + } +} + +#[pyclass(module = "booster_sdk_bindings", name = "PcmFormat")] +#[derive(Clone, Copy)] +pub struct PyPcmFormat(PcmFormat); + +#[pymethods] +impl PyPcmFormat { + #[new] + #[pyo3(signature = (sample_rate_hz=16000, channels=1, bits_per_sample=16))] + fn new(sample_rate_hz: i32, channels: i32, bits_per_sample: i32) -> Self { + Self(PcmFormat { + sample_rate_hz, + channels, + bits_per_sample, + }) + } + + #[getter] + fn sample_rate_hz(&self) -> i32 { + self.0.sample_rate_hz + } + + #[getter] + fn channels(&self) -> i32 { + self.0.channels + } + + #[getter] + fn bits_per_sample(&self) -> i32 { + self.0.bits_per_sample + } +} + +impl From for PcmFormat { + fn from(value: PyPcmFormat) -> Self { + value.0 + } +} + +impl From for PyPcmFormat { + fn from(value: PcmFormat) -> Self { + Self(value) + } +} + +#[pyclass(module = "booster_sdk_bindings", name = "PlayerInitOptions")] +#[derive(Clone)] +pub struct PyPlayerInitOptions(PlayerInitOptions); + +#[pymethods] +impl PyPlayerInitOptions { + #[new] + #[pyo3(signature = (source_type, source_uri, sample_rate_hz=16000, channels=1, bits_per_sample=16, priority=None))] + fn new( + source_type: PyAudioSourceType, + source_uri: String, + sample_rate_hz: i32, + channels: i32, + bits_per_sample: i32, + priority: Option, + ) -> Self { + Self(PlayerInitOptions { + source_type: source_type.into(), + source_uri, + sample_rate_hz, + channels, + bits_per_sample, + priority: priority.map(Into::into).unwrap_or(PlayerPriority::Medium), + }) + } + + #[staticmethod] + fn pcm_stream() -> Self { + Self(PlayerInitOptions::pcm_stream()) + } +} + +impl From for PlayerInitOptions { + fn from(value: PyPlayerInitOptions) -> Self { + value.0 + } +} + +#[pyclass(module = "booster_sdk_bindings", name = "RecorderInitOptions")] +#[derive(Clone)] +pub struct PyRecorderInitOptions(RecorderInitOptions); + +#[pymethods] +impl PyRecorderInitOptions { + #[new] + #[pyo3(signature = (output_path, sample_rate_hz=16000, channels=1, bits_per_sample=16))] + fn new(output_path: String, sample_rate_hz: i32, channels: i32, bits_per_sample: i32) -> Self { + Self(RecorderInitOptions { + output_path, + sample_rate_hz, + channels, + bits_per_sample, + }) + } +} + +impl From for RecorderInitOptions { + fn from(value: PyRecorderInitOptions) -> Self { + value.0 + } +} + +#[pyclass(module = "booster_sdk_bindings", name = "AudioCaptureStreamOptions")] +#[derive(Clone)] +pub struct PyAudioCaptureStreamOptions(AudioCaptureStreamOptions); + +#[pymethods] +impl PyAudioCaptureStreamOptions { + #[new] + #[pyo3(signature = (enable_raw_pcm=true, enable_naec_pcm=false, requested_raw_format=None))] + fn new( + enable_raw_pcm: bool, + enable_naec_pcm: bool, + requested_raw_format: Option, + ) -> Self { + Self(AudioCaptureStreamOptions { + enable_raw_pcm, + enable_naec_pcm, + requested_raw_format: requested_raw_format.map(Into::into).unwrap_or_default(), + }) + } +} + +impl From for AudioCaptureStreamOptions { + fn from(value: PyAudioCaptureStreamOptions) -> Self { + value.0 + } +} + +#[pyclass(module = "booster_sdk_bindings", name = "InitPlayerResponse")] +#[derive(Clone)] +pub struct PyInitPlayerResponse(InitPlayerResponse); + +#[pymethods] +impl PyInitPlayerResponse { + #[getter] + fn ret_code(&self) -> i32 { + self.0.ret_code + } + + #[getter] + fn ret_msg(&self) -> String { + self.0.ret_msg.clone() + } + + #[getter] + fn session_id(&self) -> i64 { + self.0.session_id + } +} + +impl From for PyInitPlayerResponse { + fn from(value: InitPlayerResponse) -> Self { + Self(value) + } +} + +#[pyclass(module = "booster_sdk_bindings", name = "InitRecorderResponse")] +#[derive(Clone)] +pub struct PyInitRecorderResponse(InitRecorderResponse); + +#[pymethods] +impl PyInitRecorderResponse { + #[getter] + fn ret_code(&self) -> i32 { + self.0.ret_code + } + + #[getter] + fn ret_msg(&self) -> String { + self.0.ret_msg.clone() + } + + #[getter] + fn session_id(&self) -> i64 { + self.0.session_id + } +} + +impl From for PyInitRecorderResponse { + fn from(value: InitRecorderResponse) -> Self { + Self(value) + } +} + +#[pyclass(module = "booster_sdk_bindings", name = "InitCaptureStreamResponse")] +#[derive(Clone)] +pub struct PyInitCaptureStreamResponse(InitCaptureStreamResponse); + +#[pymethods] +impl PyInitCaptureStreamResponse { + #[getter] + fn ret_code(&self) -> i32 { + self.0.ret_code + } + + #[getter] + fn ret_msg(&self) -> String { + self.0.ret_msg.clone() + } + + #[getter] + fn session_id(&self) -> i64 { + self.0.session_id + } + + #[getter] + fn data_topic_name(&self) -> String { + self.0.data_topic_name.clone() + } +} + +impl From for PyInitCaptureStreamResponse { + fn from(value: InitCaptureStreamResponse) -> Self { + Self(value) + } +} + +#[pyclass(module = "booster_sdk_bindings", name = "PlayerInfo")] +#[derive(Clone)] +pub struct PyPlayerInfo(PlayerInfo); + +#[pymethods] +impl PyPlayerInfo { + #[getter] + fn state(&self) -> i32 { + self.0.state + } + + fn state_enum(&self) -> Option { + self.0.state_enum().map(Into::into) + } + + #[getter] + fn played_bytes(&self) -> i64 { + self.0.played_bytes + } + + #[getter] + fn total_bytes(&self) -> i64 { + self.0.total_bytes + } + + #[getter] + fn volume(&self) -> f32 { + self.0.volume + } +} + +impl From for PyPlayerInfo { + fn from(value: PlayerInfo) -> Self { + Self(value) + } +} + +#[pyclass(module = "booster_sdk_bindings", name = "RecorderInfo")] +#[derive(Clone)] +pub struct PyRecorderInfo(RecorderInfo); + +#[pymethods] +impl PyRecorderInfo { + #[getter] + fn state(&self) -> i32 { + self.0.state + } + + fn state_enum(&self) -> Option { + self.0.state_enum().map(Into::into) + } + + #[getter] + fn captured_bytes(&self) -> i64 { + self.0.captured_bytes + } +} + +impl From for PyRecorderInfo { + fn from(value: RecorderInfo) -> Self { + Self(value) + } +} + +#[pyclass(module = "booster_sdk_bindings", name = "AudioCaptureStreamInfo")] +#[derive(Clone)] +pub struct PyAudioCaptureStreamInfo(AudioCaptureStreamInfo); + +#[pymethods] +impl PyAudioCaptureStreamInfo { + #[getter] + fn state(&self) -> i32 { + self.0.state + } + + fn state_enum(&self) -> Option { + self.0.state_enum().map(Into::into) + } + + #[getter] + fn raw_enabled(&self) -> bool { + self.0.raw_enabled + } + + #[getter] + fn naec_enabled(&self) -> bool { + self.0.naec_enabled + } + + #[getter] + fn actual_raw_format(&self) -> PyPcmFormat { + self.0.actual_raw_format.into() + } + + #[getter] + fn actual_naec_format(&self) -> PyPcmFormat { + self.0.actual_naec_format.into() + } + + #[getter] + fn published_frames(&self) -> i64 { + self.0.published_frames + } + + #[getter] + fn dropped_frames(&self) -> i64 { + self.0.dropped_frames + } +} + +impl From for PyAudioCaptureStreamInfo { + fn from(value: AudioCaptureStreamInfo) -> Self { + Self(value) + } +} + +#[pyclass(module = "booster_sdk_bindings", name = "AudioClient", unsendable)] +pub struct PyAudioClient { + client: Arc, +} + +#[pymethods] +impl PyAudioClient { + #[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) => AudioClient::with_startup_wait(wait), + None => AudioClient::new(), + } + .map_err(to_py_err)?; + Ok(Self { + client: Arc::new(client), + }) + } + + fn init(&self, py: Python<'_>) -> PyResult { + let client = Arc::clone(&self.client); + wait_for_future(py, async move { client.init().await }).map_err(to_py_err) + } + + fn client_id(&self) -> Option { + self.client.client_id() + } + + fn init_player( + &self, + py: Python<'_>, + options: PyPlayerInitOptions, + ) -> PyResult { + let client = Arc::clone(&self.client); + let options: PlayerInitOptions = options.into(); + wait_for_future(py, async move { client.init_player(&options).await }) + .map(Into::into) + .map_err(to_py_err) + } + + fn start_player(&self, py: Python<'_>, session_id: i64) -> PyResult<()> { + let client = Arc::clone(&self.client); + wait_for_future(py, async move { client.start_player(session_id).await }).map_err(to_py_err) + } + + fn pause_player(&self, py: Python<'_>, session_id: i64) -> PyResult<()> { + let client = Arc::clone(&self.client); + wait_for_future(py, async move { client.pause_player(session_id).await }).map_err(to_py_err) + } + + fn stop_player(&self, py: Python<'_>, session_id: i64) -> PyResult<()> { + let client = Arc::clone(&self.client); + wait_for_future(py, async move { client.stop_player(session_id).await }).map_err(to_py_err) + } + + fn reset_player(&self, py: Python<'_>, session_id: i64) -> PyResult<()> { + let client = Arc::clone(&self.client); + wait_for_future(py, async move { client.reset_player(session_id).await }).map_err(to_py_err) + } + + fn destroy_player(&self, py: Python<'_>, session_id: i64) -> PyResult<()> { + let client = Arc::clone(&self.client); + wait_for_future(py, async move { client.destroy_player(session_id).await }) + .map_err(to_py_err) + } + + fn set_player_volume(&self, py: Python<'_>, session_id: i64, volume: f32) -> PyResult<()> { + let client = Arc::clone(&self.client); + wait_for_future(py, async move { + client.set_player_volume(session_id, volume).await + }) + .map_err(to_py_err) + } + + fn get_player_info(&self, py: Python<'_>, session_id: i64) -> PyResult { + let client = Arc::clone(&self.client); + wait_for_future(py, async move { client.get_player_info(session_id).await }) + .map(Into::into) + .map_err(to_py_err) + } + + fn send_pcm_data(&self, py: Python<'_>, session_id: i64, pcm_bytes: Vec) -> PyResult<()> { + let client = Arc::clone(&self.client); + wait_for_future(py, async move { + client.send_pcm_data(session_id, pcm_bytes).await + }) + .map_err(to_py_err) + } + + fn init_recorder( + &self, + py: Python<'_>, + options: PyRecorderInitOptions, + ) -> PyResult { + let client = Arc::clone(&self.client); + let options: RecorderInitOptions = options.into(); + wait_for_future(py, async move { client.init_recorder(&options).await }) + .map(Into::into) + .map_err(to_py_err) + } + + fn start_recorder(&self, py: Python<'_>, session_id: i64) -> PyResult<()> { + let client = Arc::clone(&self.client); + wait_for_future(py, async move { client.start_recorder(session_id).await }) + .map_err(to_py_err) + } + + fn pause_recorder(&self, py: Python<'_>, session_id: i64) -> PyResult<()> { + let client = Arc::clone(&self.client); + wait_for_future(py, async move { client.pause_recorder(session_id).await }) + .map_err(to_py_err) + } + + fn stop_recorder(&self, py: Python<'_>, session_id: i64) -> PyResult<()> { + let client = Arc::clone(&self.client); + wait_for_future(py, async move { client.stop_recorder(session_id).await }) + .map_err(to_py_err) + } + + fn destroy_recorder(&self, py: Python<'_>, session_id: i64) -> PyResult<()> { + let client = Arc::clone(&self.client); + wait_for_future(py, async move { client.destroy_recorder(session_id).await }) + .map_err(to_py_err) + } + + fn get_recorder_info(&self, py: Python<'_>, session_id: i64) -> PyResult { + let client = Arc::clone(&self.client); + wait_for_future( + py, + async move { client.get_recorder_info(session_id).await }, + ) + .map(Into::into) + .map_err(to_py_err) + } + + fn get_doa_angle(&self, py: Python<'_>) -> PyResult { + let client = Arc::clone(&self.client); + wait_for_future(py, async move { client.get_doa_angle().await }).map_err(to_py_err) + } + + fn set_system_volume(&self, py: Python<'_>, volume: f32) -> PyResult<()> { + let client = Arc::clone(&self.client); + wait_for_future(py, async move { client.set_system_volume(volume).await }) + .map_err(to_py_err) + } + + fn get_system_volume(&self, py: Python<'_>) -> PyResult { + let client = Arc::clone(&self.client); + wait_for_future(py, async move { client.get_system_volume().await }).map_err(to_py_err) + } + + fn set_system_mute(&self, py: Python<'_>, mute: bool) -> PyResult<()> { + let client = Arc::clone(&self.client); + wait_for_future(py, async move { client.set_system_mute(mute).await }).map_err(to_py_err) + } + + fn get_system_mute(&self, py: Python<'_>) -> PyResult { + let client = Arc::clone(&self.client); + wait_for_future(py, async move { client.get_system_mute().await }).map_err(to_py_err) + } + + fn init_capture_stream( + &self, + py: Python<'_>, + options: PyAudioCaptureStreamOptions, + ) -> PyResult { + let client = Arc::clone(&self.client); + let options: AudioCaptureStreamOptions = options.into(); + wait_for_future( + py, + async move { client.init_capture_stream(&options).await }, + ) + .map(Into::into) + .map_err(to_py_err) + } + + fn start_capture_stream(&self, py: Python<'_>, session_id: i64) -> PyResult<()> { + let client = Arc::clone(&self.client); + wait_for_future( + py, + async move { client.start_capture_stream(session_id).await }, + ) + .map_err(to_py_err) + } + + fn pause_capture_stream(&self, py: Python<'_>, session_id: i64) -> PyResult<()> { + let client = Arc::clone(&self.client); + wait_for_future( + py, + async move { client.pause_capture_stream(session_id).await }, + ) + .map_err(to_py_err) + } + + fn stop_capture_stream(&self, py: Python<'_>, session_id: i64) -> PyResult<()> { + let client = Arc::clone(&self.client); + wait_for_future( + py, + async move { client.stop_capture_stream(session_id).await }, + ) + .map_err(to_py_err) + } + + fn destroy_capture_stream(&self, py: Python<'_>, session_id: i64) -> PyResult<()> { + let client = Arc::clone(&self.client); + wait_for_future(py, async move { + client.destroy_capture_stream(session_id).await + }) + .map_err(to_py_err) + } + + fn get_capture_stream_info( + &self, + py: Python<'_>, + session_id: i64, + ) -> PyResult { + let client = Arc::clone(&self.client); + wait_for_future(py, async move { + client.get_capture_stream_info(session_id).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::()?; + 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 b52a0fd..0ff530e 100644 --- a/booster_sdk_py/src/client/booster.rs +++ b/booster_sdk_py/src/client/booster.rs @@ -4,10 +4,10 @@ use booster_sdk::{ client::loco::{BoosterClient, GripperCommand}, types::{ Action, BodyControl, BoosterHandType, CustomModel, CustomModelParams, CustomTrainedTraj, - DanceId, DexterousFingerParameter, Frame, GetModeResponse, GetRobotInfoResponse, + DanceId, DexterousFingerParameter, Frame, GaitType, GetModeResponse, GetRobotInfoResponse, GetStatusResponse, GripperControlMode, GripperMode, GripperMotionParameter, Hand, HandAction, JointOrder, LoadCustomTrainedTrajResponse, Orientation, Position, Posture, - Quaternion, RobotMode, Transform, WholeBodyDanceId, + Quaternion, RobotMode, Transform, VisualKickVersion, WholeBodyDanceId, }, }; use pyo3::{Bound, prelude::*, types::PyModule}; @@ -328,11 +328,15 @@ impl PyWholeBodyDanceId { #[classattr] const MICHAEL_DANCE_3: Self = Self(WholeBodyDanceId::MichaelDance3); #[classattr] - const MOON_WALK: Self = Self(WholeBodyDanceId::MoonWalk); - #[classattr] const BOXING_STYLE_KICK: Self = Self(WholeBodyDanceId::BoxingStyleKick); #[classattr] const ROUNDHOUSE_KICK: Self = Self(WholeBodyDanceId::RoundhouseKick); + #[classattr] + const SHAN_HE_GU_REN_DANCE: Self = Self(WholeBodyDanceId::ShanHeGuRenDance); + #[classattr] + const GAI_GE_CHUN_FENG_DANCE: Self = Self(WholeBodyDanceId::GaiGeChunFengDance); + #[classattr] + const MICHAEL_DANCE_1_AND_2: Self = Self(WholeBodyDanceId::MichaelDance1And2); fn __repr__(&self) -> String { match self.0 { @@ -340,9 +344,17 @@ impl PyWholeBodyDanceId { WholeBodyDanceId::MichaelDance1 => "WholeBodyDanceId.MICHAEL_DANCE_1".to_string(), WholeBodyDanceId::MichaelDance2 => "WholeBodyDanceId.MICHAEL_DANCE_2".to_string(), WholeBodyDanceId::MichaelDance3 => "WholeBodyDanceId.MICHAEL_DANCE_3".to_string(), - WholeBodyDanceId::MoonWalk => "WholeBodyDanceId.MOON_WALK".to_string(), WholeBodyDanceId::BoxingStyleKick => "WholeBodyDanceId.BOXING_STYLE_KICK".to_string(), WholeBodyDanceId::RoundhouseKick => "WholeBodyDanceId.ROUNDHOUSE_KICK".to_string(), + WholeBodyDanceId::ShanHeGuRenDance => { + "WholeBodyDanceId.SHAN_HE_GU_REN_DANCE".to_string() + } + WholeBodyDanceId::GaiGeChunFengDance => { + "WholeBodyDanceId.GAI_GE_CHUN_FENG_DANCE".to_string() + } + WholeBodyDanceId::MichaelDance1And2 => { + "WholeBodyDanceId.MICHAEL_DANCE_1_AND_2".to_string() + } } } @@ -357,6 +369,67 @@ impl From for WholeBodyDanceId { } } +#[pyclass(module = "booster_sdk_bindings", name = "VisualKickVersion", eq)] +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct PyVisualKickVersion(VisualKickVersion); + +#[pymethods] +impl PyVisualKickVersion { + #[classattr] + const V1: Self = Self(VisualKickVersion::V1); + #[classattr] + const V2: Self = Self(VisualKickVersion::V2); + + fn __repr__(&self) -> String { + match self.0 { + VisualKickVersion::V1 => "VisualKickVersion.V1".to_string(), + VisualKickVersion::V2 => "VisualKickVersion.V2".to_string(), + } + } + + fn __int__(&self) -> i32 { + i32::from(self.0) + } +} + +impl From for VisualKickVersion { + fn from(value: PyVisualKickVersion) -> Self { + value.0 + } +} + +#[pyclass(module = "booster_sdk_bindings", name = "GaitType", eq)] +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct PyGaitType(GaitType); + +#[pymethods] +impl PyGaitType { + #[classattr] + const WHOLE_BODY_HUMANLIKE_GAIT: Self = Self(GaitType::WholeBodyHumanlikeGait); + #[classattr] + const HALF_BODY_HUMANLIKE_GAIT: Self = Self(GaitType::HalfBodyHumanlikeGait); + #[classattr] + const HALF_BODY_HUMANLIKE_GAIT_V2: Self = Self(GaitType::HalfBodyHumanlikeGaitV2); + + 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(), + } + } + + fn __int__(&self) -> i32 { + i32::from(self.0) + } +} + +impl From for GaitType { + fn from(value: PyGaitType) -> Self { + value.0 + } +} + #[pyclass(module = "booster_sdk_bindings", name = "JointOrder", eq)] #[derive(Clone, Copy, PartialEq, Eq)] pub struct PyJointOrder(JointOrder); @@ -1175,12 +1248,15 @@ pub struct PyGetRobotInfoResponse(GetRobotInfoResponse); #[pymethods] impl PyGetRobotInfoResponse { #[new] + #[pyo3(signature = (name, nickname, version, model, serial_number, edition=String::new(), region=String::new()))] fn new( name: String, nickname: String, version: String, model: String, serial_number: String, + edition: String, + region: String, ) -> Self { Self(GetRobotInfoResponse { name, @@ -1188,6 +1264,8 @@ impl PyGetRobotInfoResponse { version, model, serial_number, + edition, + region, }) } @@ -1216,10 +1294,26 @@ impl PyGetRobotInfoResponse { self.0.serial_number.clone() } + #[getter] + fn edition(&self) -> String { + self.0.edition.clone() + } + + #[getter] + fn region(&self) -> String { + self.0.region.clone() + } + fn __repr__(&self) -> String { format!( - "GetRobotInfoResponse(name='{}', nickname='{}', version='{}', model='{}', serial_number='{}')", - self.0.name, self.0.nickname, self.0.version, self.0.model, self.0.serial_number + "GetRobotInfoResponse(name='{}', nickname='{}', version='{}', model='{}', serial_number='{}', edition='{}', region='{}')", + self.0.name, + self.0.nickname, + self.0.version, + self.0.model, + self.0.serial_number, + self.0.edition, + self.0.region ) } } @@ -1653,6 +1747,45 @@ impl PyBoosterClient { wait_for_future(py, async move { client.visual_kick(start).await }).map_err(to_py_err) } + fn visual_kick_with_version( + &self, + py: Python<'_>, + start: bool, + version: PyVisualKickVersion, + ) -> PyResult<()> { + let client = Arc::clone(&self.client); + wait_for_future(py, async move { + client.visual_kick_with_version(start, version.into()).await + }) + .map_err(to_py_err) + } + + fn lion_dance_prepare(&self, py: Python<'_>, start: bool) -> PyResult<()> { + let client = Arc::clone(&self.client); + wait_for_future(py, async move { client.lion_dance_prepare(start).await }) + .map_err(to_py_err) + } + + fn lion_dance_start(&self, py: Python<'_>, dance_idx: i32) -> PyResult<()> { + let client = Arc::clone(&self.client); + wait_for_future(py, async move { client.lion_dance_start(dance_idx).await }) + .map_err(to_py_err) + } + + fn lion_dance_move(&self, py: Python<'_>, start: bool) -> PyResult<()> { + let client = Arc::clone(&self.client); + wait_for_future(py, async move { client.lion_dance_move(start).await }).map_err(to_py_err) + } + + fn switch_gait(&self, py: Python<'_>, gait_type: PyGaitType) -> PyResult<()> { + let client = Arc::clone(&self.client); + wait_for_future( + py, + async move { client.switch_gait(gait_type.into()).await }, + ) + .map_err(to_py_err) + } + fn publish_gripper_command(&self, command: PyGripperCommand) -> PyResult<()> { let command: GripperCommand = command.into(); self.client @@ -1689,6 +1822,8 @@ 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::()?; diff --git a/booster_sdk_py/src/client/mod.rs b/booster_sdk_py/src/client/mod.rs index 51189f8..071a82d 100644 --- a/booster_sdk_py/src/client/mod.rs +++ b/booster_sdk_py/src/client/mod.rs @@ -1,4 +1,5 @@ mod ai; +mod audio; mod booster; mod light_control; mod lui; @@ -10,6 +11,7 @@ use pyo3::{Bound, PyResult, types::PyModule}; pub(crate) fn register_classes(m: &Bound<'_, PyModule>) -> PyResult<()> { booster::register(m)?; ai::register(m)?; + audio::register(m)?; lui::register(m)?; light_control::register(m)?; vision::register(m)?; From 83be49036318b2a256f1629907cbd391b3ad2bf4 Mon Sep 17 00:00:00 2001 From: Gijs de Jong Date: Thu, 30 Apr 2026 18:02:27 +0200 Subject: [PATCH 2/3] Make V2 kick the default version --- booster_sdk/src/client/loco.rs | 2 +- booster_sdk_py/booster_sdk_bindings/booster_sdk_bindings.pyi | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/booster_sdk/src/client/loco.rs b/booster_sdk/src/client/loco.rs index 3524094..1592335 100644 --- a/booster_sdk/src/client/loco.rs +++ b/booster_sdk/src/client/loco.rs @@ -402,7 +402,7 @@ impl BoosterClient { /// Start or stop a visual kick (side-foot kick). pub async fn visual_kick(&self, start: bool) -> Result<()> { - self.visual_kick_with_version(start, VisualKickVersion::V1) + self.visual_kick_with_version(start, VisualKickVersion::V2) .await } 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 13350b4..c6ea131 100644 --- a/booster_sdk_py/booster_sdk_bindings/booster_sdk_bindings.pyi +++ b/booster_sdk_py/booster_sdk_bindings/booster_sdk_bindings.pyi @@ -1104,7 +1104,7 @@ class BoosterClient: ... def visual_kick(self, start: bool) -> None: - """Start or stop a visual kick (side-foot kick).""" + """Start or stop a visual kick (side-foot kick) using v1.6 V2 behavior.""" ... def visual_kick_with_version(self, start: bool, version: VisualKickVersion) -> None: From 12c0e316e1ce9311e66a82dc99797c41967e4732 Mon Sep 17 00:00:00 2001 From: Gijs de Jong Date: Thu, 30 Apr 2026 18:04:04 +0200 Subject: [PATCH 3/3] v0.1.1-alpha.2 --- Cargo.lock | 8 ++++---- Cargo.toml | 2 +- examples/rust/locomotion/Cargo.toml | 2 +- examples/rust/look_around/Cargo.toml | 2 +- pixi.toml | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4ca09b6..54dd41c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,7 +52,7 @@ checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "booster_sdk" -version = "0.1.1-alpha.1" +version = "0.1.1-alpha.2" dependencies = [ "futures", "rustdds", @@ -68,7 +68,7 @@ dependencies = [ [[package]] name = "booster_sdk_py" -version = "0.1.1-alpha.1" +version = "0.1.1-alpha.2" dependencies = [ "booster_sdk", "pyo3", @@ -646,7 +646,7 @@ dependencies = [ [[package]] name = "locomotion" -version = "0.1.1-alpha.1" +version = "0.1.1-alpha.2" dependencies = [ "booster_sdk", "tokio", @@ -662,7 +662,7 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "look_around" -version = "0.1.1-alpha.1" +version = "0.1.1-alpha.2" dependencies = [ "booster_sdk", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 8b8f199..940a2fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = ["booster_sdk", "booster_sdk_py", "examples/rust/*"] resolver = "2" [workspace.package] -version = "0.1.1-alpha.1" +version = "0.1.1-alpha.2" edition = "2024" authors = ["Team whIRLwind"] license = "MIT OR Apache-2.0" diff --git a/examples/rust/locomotion/Cargo.toml b/examples/rust/locomotion/Cargo.toml index e7a63bf..d6cd226 100644 --- a/examples/rust/locomotion/Cargo.toml +++ b/examples/rust/locomotion/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "locomotion" -version = "0.1.1-alpha.1" +version = "0.1.1-alpha.2" edition = "2024" [dependencies] diff --git a/examples/rust/look_around/Cargo.toml b/examples/rust/look_around/Cargo.toml index 9fee7f9..cb2152a 100644 --- a/examples/rust/look_around/Cargo.toml +++ b/examples/rust/look_around/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "look_around" -version = "0.1.1-alpha.1" +version = "0.1.1-alpha.2" edition = "2024" [dependencies] diff --git a/pixi.toml b/pixi.toml index 779361b..f3d4082 100644 --- a/pixi.toml +++ b/pixi.toml @@ -3,7 +3,7 @@ authors = ["Team whIRLwind"] channels = ["conda-forge"] name = "booster-sdk" platforms = ["osx-arm64", "linux-64", "linux-aarch64"] -version = "0.1.1-alpha.1" +version = "0.1.1-alpha.2" [environments] py = ["wheel-build", "python-tasks"]