Skip to content

Commit 55da8a0

Browse files
awoll-bdaiexploy-bot
authored andcommitted
Add command configurations to API (#82)
### What change is being made Please provide a detailed description of WHAT change is being made such that the reader is able to understand the change holistically. ### Why this change is being made Please provide the rationale behind the change and additional context like links to documents, related work or issues. ### Tested Please provide a description how this change was tested, e.g. unit tests, hardware tests or commands you run. GitOrigin-RevId: 390e4c59813e388b8a53a2a84fec709623dc4675
1 parent 5e41571 commit 55da8a0

9 files changed

Lines changed: 142 additions & 24 deletions

control/command_interface.hpp

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,29 @@ struct SE2VelocityConfig {
1717
std::optional<SE2VelocityRanges> ranges{};
1818
};
1919

20+
/**
21+
* @brief Configuration for SE3 pose commands.
22+
*/
23+
struct SE3PoseConfig {
24+
// Currently no configuration options, but this struct is defined for consistency and future
25+
// extensibility.
26+
};
27+
28+
/**
29+
* @brief Configuration for boolean selector commands.
30+
*/
31+
struct BooleanSelectorConfig {
32+
// Currently no configuration options, but this struct is defined for consistency and future
33+
// extensibility.
34+
};
35+
36+
/**
37+
* @brief Configuration for float scalar commands.
38+
*/
39+
struct FloatScalarConfig {
40+
std::optional<Range> range{};
41+
};
42+
2043
/**
2144
* @class CommandInterface
2245
*
@@ -33,6 +56,7 @@ class CommandInterface {
3356
*
3457
* @param command_name The name of the command.
3558
* @param config The configuration for the commanded se2 velocity.
59+
* @return True if initialization succeeded, false otherwise.
3660
*/
3761
virtual bool initSe2Velocity(const std::string& command_name,
3862
const SE2VelocityConfig& /*config*/) {
@@ -55,8 +79,10 @@ class CommandInterface {
5579
* Called once during initialization (usually non real-time).
5680
*
5781
* @param command_name The name of the command.
82+
* @param config The configuration for the commanded SE3 pose.
83+
* @return True if initialization succeeded, false otherwise.
5884
*/
59-
virtual bool initSe3Pose(const std::string& command_name) {
85+
virtual bool initSe3Pose(const std::string& command_name, const SE3PoseConfig& /*config*/) {
6086
LOG_STREAM(ERROR, "initSe3Pose() not implemented for command: " << command_name);
6187
return false;
6288
}
@@ -76,8 +102,11 @@ class CommandInterface {
76102
* Called once during initialization (usually non real-time).
77103
*
78104
* @param command_name The name of the command.
105+
* @param config The configuration for the commanded boolean selector.
106+
* @return True if initialization succeeded, false otherwise.
79107
*/
80-
virtual bool initBooleanSelector(const std::string& command_name) {
108+
virtual bool initBooleanSelector(const std::string& command_name,
109+
const BooleanSelectorConfig& /*config*/) {
81110
LOG_STREAM(ERROR, "initBooleanSelector() not implemented for command: " << command_name);
82111
return false;
83112
}
@@ -97,8 +126,11 @@ class CommandInterface {
97126
* Called once during initialization (usually non real-time).
98127
*
99128
* @param command_name The name of the command.
129+
* @param config The configuration for the commanded float scalar.
130+
* @return True if initialization succeeded, false otherwise.
100131
*/
101-
virtual bool initFloatValue(const std::string& command_name) {
132+
virtual bool initFloatValue(const std::string& command_name,
133+
const FloatScalarConfig& /*config*/) {
102134
LOG_STREAM(ERROR, "initFloatValue() not implemented for command: " << command_name);
103135
return false;
104136
}

control/components.cpp

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,7 @@ CommandSE3PoseInput::CommandSE3PoseInput(const std::string& key, const std::stri
411411
: key_(key), command_name_(command_name) {}
412412

413413
bool CommandSE3PoseInput::init(RobotStateInterface& /*state*/, CommandInterface& command) {
414-
return command.initSe3Pose(command_name_);
414+
return command.initSe3Pose(command_name_, SE3PoseConfig{});
415415
}
416416

417417
bool CommandSE3PoseInput::read(OnnxRuntime& runtime, RobotStateInterface& /*state*/,
@@ -451,7 +451,7 @@ CommandBooleanInput::CommandBooleanInput(const std::string& key, const std::stri
451451
: key_(key), command_name_(command_name) {}
452452

453453
bool CommandBooleanInput::init(RobotStateInterface& /*state*/, CommandInterface& command) {
454-
return command.initBooleanSelector(command_name_);
454+
return command.initBooleanSelector(command_name_, BooleanSelectorConfig{});
455455
}
456456

457457
bool CommandBooleanInput::read(OnnxRuntime& runtime, RobotStateInterface& /*state*/,
@@ -465,11 +465,12 @@ bool CommandBooleanInput::read(OnnxRuntime& runtime, RobotStateInterface& /*stat
465465
}
466466

467467
// Implementation of CommandFloatInput methods
468-
CommandFloatInput::CommandFloatInput(const std::string& key, const std::string& command_name)
469-
: key_(key), command_name_(command_name) {}
468+
CommandFloatInput::CommandFloatInput(const std::string& key, const std::string& command_name,
469+
const metadata::FloatCommandMetadata& metadata)
470+
: key_(key), command_name_(command_name), metadata_(metadata) {}
470471

471472
bool CommandFloatInput::init(RobotStateInterface& /*state*/, CommandInterface& command) {
472-
return command.initFloatValue(command_name_);
473+
return command.initFloatValue(command_name_, FloatScalarConfig{.range = metadata_.range});
473474
}
474475

475476
bool CommandFloatInput::read(OnnxRuntime& runtime, RobotStateInterface& /*state*/,

control/components.hpp

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -483,15 +483,18 @@ class CommandFloatInput : public Input {
483483
*
484484
* @param key ONNX input tensor name (e.g., "commands.speed_scale").
485485
* @param command_name Name of the float command to read.
486+
* @param metadata Float command metadata containing optional value range.
486487
*/
487-
CommandFloatInput(const std::string& key, const std::string& command_name);
488+
CommandFloatInput(const std::string& key, const std::string& command_name,
489+
const metadata::FloatCommandMetadata& metadata);
488490

489491
bool init(RobotStateInterface& state, CommandInterface& command) override;
490492
bool read(OnnxRuntime& runtime, RobotStateInterface& state, CommandInterface& command) override;
491493

492494
private:
493-
std::string key_; ///< ONNX input tensor name.
494-
std::string command_name_; ///< Float command name.
495+
std::string key_; ///< ONNX input tensor name.
496+
std::string command_name_; ///< Float command name.
497+
metadata::FloatCommandMetadata metadata_; ///< Float command configuration.
495498
};
496499

497500
/**

control/matcher.cpp

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,13 @@ bool CommandFloatMatcher::matches(const Match& maybe_match) {
397397
std::vector<std::unique_ptr<Input>> CommandFloatMatcher::createInputs() const {
398398
std::vector<std::unique_ptr<Input>> inputs;
399399
for (const auto& [name, match] : found_matches_) {
400-
inputs.push_back(std::make_unique<CommandFloatInput>(match.name, name));
400+
metadata::FloatCommandMetadata metadata;
401+
if (match.metadata.has_value()) {
402+
auto maybe_metadata =
403+
metadata::safe_json_get<metadata::FloatCommandMetadata>(match.metadata.value());
404+
if (maybe_metadata.has_value()) metadata = maybe_metadata.value();
405+
}
406+
inputs.push_back(std::make_unique<CommandFloatInput>(match.name, name, metadata));
401407
}
402408
return inputs;
403409
}
@@ -415,12 +421,13 @@ bool CommandSE2VelocityMatcher::matches(const Match& maybe_match) {
415421
std::vector<std::unique_ptr<Input>> CommandSE2VelocityMatcher::createInputs() const {
416422
std::vector<std::unique_ptr<Input>> inputs;
417423
for (const auto& [name, match] : found_matches_) {
418-
if (!match.metadata.has_value()) continue;
419-
auto maybe_metadata =
420-
metadata::safe_json_get<metadata::SE2VelocityCommandMetadata>(match.metadata.value());
421-
if (!maybe_metadata.has_value()) continue;
422-
inputs.push_back(
423-
std::make_unique<CommandSE2VelocityInput>(match.name, name, maybe_metadata.value()));
424+
metadata::SE2VelocityCommandMetadata metadata;
425+
if (match.metadata.has_value()) {
426+
auto maybe_metadata =
427+
metadata::safe_json_get<metadata::SE2VelocityCommandMetadata>(match.metadata.value());
428+
if (maybe_metadata.has_value()) metadata = maybe_metadata.value();
429+
}
430+
inputs.push_back(std::make_unique<CommandSE2VelocityInput>(match.name, name, metadata));
424431
}
425432
return inputs;
426433
}

control/metadata.hpp

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,31 @@ inline void from_json(const json& j, SE2VelocityCommandMetadata& cmd) {
9797
}
9898
}
9999

100+
/**
101+
* @brief Metadata for float scalar commands.
102+
*
103+
* Specifies optional range constraints for float scalar commands.
104+
*/
105+
struct FloatCommandMetadata {
106+
std::optional<Range> range{}; ///< Optional range constraint for the float command.
107+
};
108+
109+
/**
110+
* @brief Parse FloatCommandMetadata from JSON.
111+
*
112+
* @param j JSON object optionally containing "range" field.
113+
* @param cmd FloatCommandMetadata object to populate.
114+
*/
115+
inline void from_json(const json& j, FloatCommandMetadata& cmd) {
116+
if (j.contains("range") && j["range"].is_array() && j["range"].size() == 2) {
117+
Range range;
118+
j.at("range").get_to(range);
119+
cmd.range = range;
120+
} else {
121+
cmd.range = std::nullopt;
122+
}
123+
}
124+
100125
/**
101126
* @brief Metadata for height scan sensors.
102127
*

control/test/components_test.cpp

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) 2026 Robotics and AI Institute LLC dba RAI Institute. All rights reserved.
22

33
#include "components.hpp"
4+
#include "matcher.hpp"
45
#include "mock_command_interface.hpp"
56
#include "mock_state_interface.hpp"
67
#include "onnx_runtime.hpp"
@@ -211,4 +212,49 @@ TEST_F(OnnxComponentsTest, MemoryOutput_WithRealRuntime) {
211212
EXPECT_TRUE(memory_was_overwritten) << "Memory should be overwritten with new output data";
212213
}
213214

215+
// --------------- Matcher tests for default metadata --------------------------------
216+
217+
TEST(CommandFloatMatcherTest, CreatesInputWithoutMetadata) {
218+
CommandFloatMatcher matcher;
219+
Match match_without_metadata{.name = "cmd.float.gain"};
220+
ASSERT_TRUE(matcher.matches(match_without_metadata));
221+
222+
auto inputs = matcher.createInputs();
223+
ASSERT_EQ(inputs.size(), 1u) << "Should create input even without metadata";
224+
}
225+
226+
TEST(CommandFloatMatcherTest, CreatesInputWithMetadata) {
227+
CommandFloatMatcher matcher;
228+
Match match_with_metadata{
229+
.name = "cmd.float.scale",
230+
.metadata = R"({"range": [-1.0, 1.0]})",
231+
};
232+
ASSERT_TRUE(matcher.matches(match_with_metadata));
233+
234+
auto inputs = matcher.createInputs();
235+
ASSERT_EQ(inputs.size(), 1u);
236+
}
237+
238+
TEST(CommandSE2VelocityMatcherTest, CreatesInputWithoutMetadata) {
239+
CommandSE2VelocityMatcher matcher;
240+
Match match_without_metadata{.name = "cmd.se2_velocity.vel"};
241+
ASSERT_TRUE(matcher.matches(match_without_metadata));
242+
243+
auto inputs = matcher.createInputs();
244+
ASSERT_EQ(inputs.size(), 1u) << "Should create input even without metadata";
245+
}
246+
247+
TEST(CommandSE2VelocityMatcherTest, CreatesInputWithMetadata) {
248+
CommandSE2VelocityMatcher matcher;
249+
Match match_with_metadata{
250+
.name = "cmd.se2_velocity.vel",
251+
.metadata =
252+
R"({"ranges": {"lin_vel_x": [-1.0, 1.0], "lin_vel_y": [-0.5, 0.5], "ang_vel_z": [-2.0, 2.0]}})",
253+
};
254+
ASSERT_TRUE(matcher.matches(match_with_metadata));
255+
256+
auto inputs = matcher.createInputs();
257+
ASSERT_EQ(inputs.size(), 1u);
258+
}
259+
214260
} // namespace exploy::control

control/test/controller_test.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -206,9 +206,9 @@ class OnnxControllerTest : public ::testing::Test {
206206
.WillOnce(Return(true));
207207
EXPECT_CALL(command_mock_, initSe2Velocity("vel_with_range", HasRanges(kRanges)))
208208
.WillOnce(Return(true));
209-
EXPECT_CALL(command_mock_, initSe3Pose("pose")).WillOnce(Return(true));
210-
EXPECT_CALL(command_mock_, initBooleanSelector("selector")).WillOnce(Return(true));
211-
EXPECT_CALL(command_mock_, initFloatValue("value")).WillOnce(Return(true));
209+
EXPECT_CALL(command_mock_, initSe3Pose("pose", _)).WillOnce(Return(true));
210+
EXPECT_CALL(command_mock_, initBooleanSelector("selector", _)).WillOnce(Return(true));
211+
EXPECT_CALL(command_mock_, initFloatValue("value", _)).WillOnce(Return(true));
212212
}
213213

214214
void ExpectInitCustom() {

control/test/mock_command_interface.hpp

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,16 @@ class MockCommandInterface : public CommandInterface {
1212
(const std::string& command_name, const SE2VelocityConfig& config), (override));
1313
MOCK_METHOD(std::optional<SE2Velocity>, se2Velocity, (const std::string& command_name),
1414
(override));
15-
MOCK_METHOD(bool, initSe3Pose, (const std::string& command_name), (override));
15+
MOCK_METHOD(bool, initSe3Pose, (const std::string& command_name, const SE3PoseConfig& config),
16+
(override));
1617
MOCK_METHOD(std::optional<SE3Pose>, se3Pose, (const std::string& command_name),
1718
(const, override));
18-
MOCK_METHOD(bool, initBooleanSelector, (const std::string& command_name), (override));
19+
MOCK_METHOD(bool, initBooleanSelector,
20+
(const std::string& command_name, const BooleanSelectorConfig& config), (override));
1921
MOCK_METHOD(std::optional<bool>, booleanSelector, (const std::string& command_name),
2022
(const override));
21-
MOCK_METHOD(bool, initFloatValue, (const std::string& command_name), (override));
23+
MOCK_METHOD(bool, initFloatValue,
24+
(const std::string& command_name, const FloatScalarConfig& config), (override));
2225
MOCK_METHOD(std::optional<float>, floatValue, (const std::string& command_name),
2326
(const override));
2427
};

control/test/testdata/test_onnx_generator.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ def get_command_metadata() -> dict:
199199
"ang_vel_z": [-2.5, 2.5],
200200
},
201201
},
202+
"cmd.float.value": {},
202203
}
203204

204205

0 commit comments

Comments
 (0)