Skip to content

Commit 8fa7b57

Browse files
authored
Update and test on actual robot hardware (#2)
1 parent 9431ced commit 8fa7b57

16 files changed

Lines changed: 1129 additions & 43 deletions

File tree

.github/workflows/build_wheels.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: Build Python Wheels
2+
3+
on:
4+
pull_request:
5+
workflow_dispatch:
6+
7+
permissions:
8+
contents: read
9+
10+
jobs:
11+
build:
12+
runs-on: ${{ matrix.platform.runner }}
13+
strategy:
14+
matrix:
15+
platform:
16+
- runner: ubuntu-22.04
17+
- runner: ubuntu-22.04-arm
18+
- runner: macos-14
19+
steps:
20+
- uses: actions/checkout@v4
21+
- uses: prefix-dev/setup-pixi@v0.9.1
22+
with:
23+
pixi-version: v0.63.2
24+
environments: "py"
25+
- name: Build wheels
26+
run: pixi run py-build-wheel
27+
- name: Upload wheels
28+
uses: actions/upload-artifact@v4
29+
with:
30+
name: wheels-${{ matrix.platform.runner }}
31+
path: booster_sdk_py/dist/

.github/workflows/checks_rust.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
name: Rust Checks
2+
3+
on:
4+
pull_request:
5+
workflow_dispatch:
6+
7+
permissions:
8+
contents: read
9+
10+
jobs:
11+
rust-checks:
12+
runs-on: ubuntu-22.04
13+
steps:
14+
- uses: actions/checkout@v4
15+
- uses: prefix-dev/setup-pixi@v0.9.1
16+
with:
17+
pixi-version: v0.63.2
18+
- name: Rust checks
19+
run: pixi run rs-check
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: Python Checks
2+
3+
on:
4+
pull_request:
5+
workflow_dispatch:
6+
7+
permissions:
8+
contents: read
9+
10+
jobs:
11+
python-checks:
12+
runs-on: ubuntu-22.04
13+
steps:
14+
- uses: actions/checkout@v4
15+
- uses: prefix-dev/setup-pixi@v0.9.1
16+
with:
17+
pixi-version: v0.63.2
18+
environments: "py"
19+
- name: Python lint
20+
run: pixi run py-lint

.github/workflows/release.yml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
name: Release
2+
3+
on:
4+
workflow_dispatch:
5+
6+
permissions:
7+
contents: read
8+
9+
jobs:
10+
build:
11+
runs-on: ${{ matrix.platform.runner }}
12+
strategy:
13+
matrix:
14+
platform:
15+
- runner: ubuntu-22.04
16+
- runner: ubuntu-22.04-arm
17+
- runner: macos-14
18+
steps:
19+
- uses: actions/checkout@v4
20+
- uses: prefix-dev/setup-pixi@v0.9.1
21+
with:
22+
pixi-version: v0.63.2
23+
environments: "py"
24+
- name: Build wheels
25+
run: pixi run py-build-wheel
26+
- name: Upload wheels
27+
uses: actions/upload-artifact@v4
28+
with:
29+
name: wheels-${{ matrix.platform.runner }}
30+
path: booster_sdk_py/dist/
31+
32+
publish:
33+
runs-on: ubuntu-22.04
34+
needs: build
35+
environment: release
36+
permissions:
37+
id-token: write
38+
steps:
39+
- uses: actions/download-artifact@v4
40+
with:
41+
pattern: wheels-*
42+
merge-multiple: true
43+
path: dist/
44+
- name: Install uv
45+
uses: astral-sh/setup-uv@v5
46+
- name: Publish to PyPI
47+
run: uv publish dist/*

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ members = ["booster_sdk", "booster_sdk_py"]
33
resolver = "2"
44

55
[workspace.package]
6-
version = "0.1.0-alpha.4"
6+
version = "0.1.0-alpha.5"
77
edition = "2024"
88
authors = ["Team whIRLwind"]
99
license = "MIT OR Apache-2.0"

booster_sdk/examples/locomotion_control.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
1515

1616
tracing::info!("Starting locomotion control example");
1717

18-
// Create client with 2-second timeout
18+
// Create client
1919
let client = BoosterClient::new()?;
2020

2121
// Change to walking mode

booster_sdk/src/client/loco_client.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ use serde::{Deserialize, Serialize};
1414
const CHANGE_MODE_API_ID: i32 = 2000;
1515
const MOVE_API_ID: i32 = 2001;
1616

17+
// The controller may send an intermediate pending status (-1) before the
18+
// final success response. Mode transitions (especially PREPARE) can take
19+
// several seconds.
20+
const CHANGE_MODE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
21+
1722
#[derive(Deserialize)]
1823
struct EmptyResponse {}
1924

@@ -59,7 +64,7 @@ impl BoosterClient {
5964
&Params {
6065
mode: i32::from(mode),
6166
},
62-
None,
67+
Some(CHANGE_MODE_TIMEOUT),
6368
)
6469
.await?;
6570

booster_sdk/src/dds/messages.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ pub struct RpcRespMsg {
1414
pub uuid: String,
1515
pub header: String,
1616
pub body: String,
17-
pub status_code: i32,
1817
}
1918

2019
#[derive(Debug, Clone, Serialize, Deserialize)]

booster_sdk/src/dds/rpc.rs

Lines changed: 82 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! RPC client for high-level API requests over DDS.
22
33
use serde::{Serialize, de::DeserializeOwned};
4+
use serde_json::Value;
45
use std::time::{Duration, Instant};
56
use uuid::Uuid;
67

@@ -23,7 +24,9 @@ impl Default for RpcClientOptions {
2324
fn default() -> Self {
2425
Self {
2526
domain_id: 0,
26-
default_timeout: Duration::from_millis(1000),
27+
// 5 s is a safe default for most commands. Mode changes are slow,
28+
// so change_mode passes its own longer timeout.
29+
default_timeout: Duration::from_secs(5),
2730
}
2831
}
2932
}
@@ -35,6 +38,32 @@ pub struct RpcClient {
3538
default_timeout: Duration,
3639
}
3740

41+
fn parse_status_value(value: &Value) -> Option<i32> {
42+
match value {
43+
Value::Number(n) => n.as_i64().and_then(|v| i32::try_from(v).ok()),
44+
Value::String(s) => s.parse::<i32>().ok(),
45+
_ => None,
46+
}
47+
}
48+
49+
fn parse_status_from_header(raw_json: &str) -> Option<i32> {
50+
let value: Value = serde_json::from_str(raw_json.trim()).ok()?;
51+
let object = value.as_object()?;
52+
object.get("status").and_then(parse_status_value)
53+
}
54+
55+
fn decode_response_body<R>(body: &str) -> std::result::Result<R, serde_json::Error>
56+
where
57+
R: DeserializeOwned,
58+
{
59+
let trimmed = body.trim();
60+
if trimmed.is_empty() {
61+
return serde_json::from_str("{}");
62+
}
63+
64+
serde_json::from_str(trimmed)
65+
}
66+
3867
impl RpcClient {
3968
pub fn new(options: RpcClientOptions) -> Result<Self> {
4069
let node = DdsNode::new(super::DdsConfig {
@@ -102,22 +131,25 @@ impl RpcClient {
102131
continue;
103132
}
104133

105-
if response.status_code == -1 {
134+
let status_code = parse_status_from_header(&response.header).unwrap_or(0);
135+
136+
if status_code == -1 {
106137
continue;
107138
}
108139

109-
if response.status_code != 0 {
110-
return Err(RpcError::from_status_code(
111-
response.status_code,
112-
response.body,
113-
)
114-
.into());
140+
if status_code != 0 {
141+
let message = if response.body.trim().is_empty() {
142+
response.header
143+
} else {
144+
response.body
145+
};
146+
return Err(RpcError::from_status_code(status_code, message).into());
115147
}
116148

117-
let result: R = serde_json::from_str(&response.body).map_err(|err| {
149+
let result: R = decode_response_body(&response.body).map_err(|err| {
118150
RpcError::RequestFailed {
119-
status: response.status_code,
120-
message: format!("Failed to deserialize response: {err}"),
151+
status: status_code,
152+
message: format!("Failed to deserialize response body: {err}"),
121153
}
122154
})?;
123155

@@ -134,3 +166,42 @@ impl RpcClient {
134166
.map_err(|err| DdsError::ReceiveFailed(err.to_string()))?
135167
}
136168
}
169+
170+
#[cfg(test)]
171+
mod tests {
172+
use super::{decode_response_body, parse_status_from_header, parse_status_value};
173+
use serde_json::json;
174+
175+
#[derive(serde::Deserialize)]
176+
struct EmptyResponse {}
177+
178+
#[test]
179+
fn parse_status_from_header_reads_status_field() {
180+
assert_eq!(parse_status_from_header(r#"{"status":0}"#), Some(0));
181+
assert_eq!(parse_status_from_header(r#"{"status":"-1"}"#), Some(-1));
182+
}
183+
184+
#[test]
185+
fn parse_status_value_handles_number_and_string() {
186+
assert_eq!(parse_status_value(&json!(0)), Some(0));
187+
assert_eq!(parse_status_value(&json!("-1")), Some(-1));
188+
assert_eq!(parse_status_value(&json!("not-a-number")), None);
189+
}
190+
191+
#[test]
192+
fn parse_status_from_header_ignores_other_fields() {
193+
assert_eq!(parse_status_from_header(r#"{"status_code":0}"#), None);
194+
assert_eq!(parse_status_from_header(r#"{"code":0}"#), None);
195+
}
196+
197+
#[test]
198+
fn empty_body_deserializes_as_empty_object() {
199+
let _: EmptyResponse = decode_response_body("").expect("empty body should parse");
200+
}
201+
202+
#[test]
203+
fn non_json_body_fails_deserialization() {
204+
let parsed = decode_response_body::<EmptyResponse>("not-json");
205+
assert!(parsed.is_err());
206+
}
207+
}

0 commit comments

Comments
 (0)