fix: align navigation contract and readiness handling
This commit is contained in:
@@ -98,6 +98,7 @@ class RosDriver final : public robot_bt::GoalDriver {
|
||||
bool fresh(robot_bt::RosTime observed, robot_bt::RosTime valid_until,
|
||||
robot_bt::RosTime capture_after = 0) const;
|
||||
robot_bt::ExecutionResult execution(const iface::msg::ExecutionResult&,const robot_bt::GoalRequest&) const;
|
||||
robot_bt::ExecutionResult execution(const iface::msg::NavigationResult&,const robot_bt::GoalRequest&) const;
|
||||
robot_bt::ExecutionResult readonly_result(rclcpp_action::ResultCode, bool valid,
|
||||
robot_bt::SkillResponse) const;
|
||||
robot_bt::SnapshotMeta meta(const robot_bt::GoalRequest&, robot_bt::RosTime,
|
||||
@@ -137,13 +138,13 @@ class RosDriver final : public robot_bt::GoalDriver {
|
||||
const std::shared_ptr<const typename Action::Feedback> feedback) {
|
||||
if (!handle || !feedback || feedback->sequence == 0 || feedback->phase > max_phase) return;
|
||||
if constexpr (std::is_same_v<Action, Navigate>) {
|
||||
if(feedback->errors_valid&&(!std::isfinite(feedback->position_error)||feedback->position_error<0||
|
||||
!std::isfinite(feedback->orientation_error)||std::abs(feedback->orientation_error)>std::acos(-1.0)))return;
|
||||
if(feedback->pose_valid) {
|
||||
if(feedback->error_valid&&(!std::isfinite(feedback->position_error)||feedback->position_error<0||
|
||||
!std::isfinite(feedback->yaw_error)||std::abs(feedback->yaw_error)>std::acos(-1.0)))return;
|
||||
if(feedback->current_pose_valid) {
|
||||
const auto& p=feedback->current_pose;
|
||||
robot_bt::Pose pose{p.header.frame_id,p.pose.position.x,p.pose.position.y,p.pose.position.z,
|
||||
p.pose.orientation.x,p.pose.orientation.y,p.pose.orientation.z,p.pose.orientation.w};
|
||||
if(!robot_bt::valid_pose(pose))return;
|
||||
if(pose.frame_id!="map"||!robot_bt::valid_pose(pose))return;
|
||||
}
|
||||
}
|
||||
if constexpr (std::is_same_v<Action, Manipulate>) {
|
||||
@@ -168,9 +169,9 @@ class RosDriver final : public robot_bt::GoalDriver {
|
||||
payload["progress_valid"]=feedback->progress_valid;payload["progress"]=feedback->progress;
|
||||
}
|
||||
if constexpr (std::is_same_v<Action, Navigate>) {
|
||||
payload["pose_valid"]=feedback->pose_valid;payload["errors_valid"]=feedback->errors_valid;
|
||||
payload["position_error"]=feedback->position_error;payload["orientation_error"]=feedback->orientation_error;
|
||||
payload["blocked_valid"]=feedback->blocked_valid;payload["blocked"]=feedback->blocked;
|
||||
payload["current_pose_valid"]=feedback->current_pose_valid;payload["error_valid"]=feedback->error_valid;
|
||||
payload["position_error"]=feedback->position_error;payload["yaw_error"]=feedback->yaw_error;
|
||||
payload["blocked"]=feedback->blocked;
|
||||
}
|
||||
event.feedback_snapshot=payload.dump();events_.push_back(event);
|
||||
};
|
||||
|
||||
@@ -170,6 +170,24 @@ ExecutionResult RosDriver::execution(const iface::msg::ExecutionResult& m,const
|
||||
r.stop=StopState::CONFIRMED;
|
||||
return r;
|
||||
}
|
||||
ExecutionResult RosDriver::execution(const iface::msg::NavigationResult& m,const GoalRequest& request)const {
|
||||
// Navigation outcomes have different numeric values from other skill results.
|
||||
iface::msg::ExecutionResult common;
|
||||
common.error_code=m.error_code;common.message=m.message;
|
||||
common.stop_state=m.stop_state;common.stopped_at=m.stopped_at;common.stop_evidence_ref=m.stop_evidence_ref;
|
||||
using Nav=iface::msg::NavigationResult;
|
||||
using Common=iface::msg::ExecutionResult;
|
||||
switch(m.status) {
|
||||
case Nav::SUCCEEDED:common.status=Common::COMPLETED;break;
|
||||
case Nav::CANCELED:common.status=Common::CANCELED;break;
|
||||
case Nav::TIMEOUT:common.status=Common::TIMED_OUT;break;
|
||||
case Nav::BLOCKED:common.status=Common::FAILED;if(common.error_code.empty())common.error_code="NAV_BLOCKED";break;
|
||||
case Nav::NOT_READY:common.status=Common::REJECTED;if(common.error_code.empty())common.error_code="NAV_NOT_READY";break;
|
||||
case Nav::FAILED:common.status=Common::FAILED;break;
|
||||
default:{ExecutionResult invalid;invalid.error_code="NAV_RESULT_PROTOCOL_ERROR";return invalid;}
|
||||
}
|
||||
return execution(common,request);
|
||||
}
|
||||
ExecutionResult RosDriver::readonly_result(rclcpp_action::ResultCode native_code,bool valid,SkillResponse response)const {
|
||||
ExecutionResult r;r.response=std::move(response);r.response.valid=valid;
|
||||
// These servers are contractually read-only. Their native terminal is enough
|
||||
@@ -210,13 +228,13 @@ void RosDriver::send(const GoalRequest& r) {
|
||||
});break;
|
||||
}
|
||||
Navigate::Goal g;g.trace=trace_msg(r.trace);g.target_pose=pose_msg(*r.registered_pose,now);
|
||||
g.position_tolerance=r.position_tolerance_m;g.orientation_tolerance=r.orientation_tolerance_rad;g.timeout=timeout();
|
||||
send_typed<Navigate>(navigate_,g,r,6,[this,r](const Navigate::Result& m,auto){
|
||||
auto out=execution(m.result,r);out.response.valid=m.pose_valid&&m.errors_valid&&
|
||||
std::isfinite(m.final_position_error)&&std::isfinite(m.final_orientation_error)&&
|
||||
m.final_position_error>=0&&std::abs(m.final_orientation_error)<=std::acos(-1.0)&&
|
||||
m.final_position_error<=r.position_tolerance_m&&std::abs(m.final_orientation_error)<=r.orientation_tolerance_rad;
|
||||
if(m.pose_valid)out.response.final_pose=pose_core(m.final_pose);
|
||||
g.position_tolerance=r.position_tolerance_m;g.yaw_tolerance=r.orientation_tolerance_rad;g.timeout=timeout();
|
||||
send_typed<Navigate>(navigate_,g,r,Navigate::Feedback::STOPPING,[this,r](const Navigate::Result& m,auto){
|
||||
auto out=execution(m.result,r);out.response.valid=m.final_pose_valid&&
|
||||
std::isfinite(m.final_position_error)&&std::isfinite(m.final_yaw_error)&&
|
||||
m.final_position_error>=0&&std::abs(m.final_yaw_error)<=std::acos(-1.0)&&
|
||||
m.final_position_error<=r.position_tolerance_m&&std::abs(m.final_yaw_error)<=r.orientation_tolerance_rad;
|
||||
if(m.final_pose_valid)out.response.final_pose=pose_core(m.final_pose);
|
||||
out.response.base_stopped=out.stop==StopState::CONFIRMED;return out;});break;
|
||||
}
|
||||
case Skill::PICK:case Skill::PLACE: {
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// Test-only live DDS probe. Build via tests/helpers/native_navigation_contract.py.
|
||||
#include <bt_executor/ros_driver.hpp>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
|
||||
int main(int argc,char** argv) {
|
||||
if(argc!=4) return 2;
|
||||
const int scenario=std::stoi(argv[1]);
|
||||
const std::string journal=argv[2], ns=argv[3];
|
||||
if(ns.rfind("/sim/",0)!=0) return 3;
|
||||
rclcpp::init(0,nullptr);
|
||||
try {
|
||||
auto node=std::make_shared<rclcpp::Node>("native_navigation_probe",ns);
|
||||
bt_executor::RosDriver driver(*node,"robot_01",journal+"/uuids.jsonl",0.5,10000000000LL,robot_bt::Milliseconds(5000));
|
||||
robot_bt::TaskConfig task; task.route="LEGACY";
|
||||
driver.bind_task(task,robot_bt::SiteConfig{},1);
|
||||
robot_bt::ActiveGoalRegistry registry(driver,journal+"/goals.jsonl");
|
||||
auto until=robot_bt::SteadyClock::now()+std::chrono::seconds(12);
|
||||
while(!driver.ready(robot_bt::Skill::NAVIGATE)&&robot_bt::SteadyClock::now()<until) {
|
||||
rclcpp::spin_some(node); std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
if(!driver.ready(robot_bt::Skill::NAVIGATE)) throw std::runtime_error("Navigate discovery timed out");
|
||||
robot_bt::GoalRequest request;
|
||||
request.goal_id="probe-"+std::to_string(scenario);request.robot_id="robot_01";
|
||||
request.trace.task_id=request.goal_id;request.trace.run_id=request.goal_id;request.trace.subtask_id="navigate";
|
||||
request.skill=robot_bt::Skill::NAVIGATE;request.registered_pose=robot_bt::Pose{"map",double(scenario),0,0,0,0,0,1};
|
||||
request.capture_after=node->now().nanoseconds();
|
||||
const auto started=registry.start(request,robot_bt::SteadyClock::now());
|
||||
if(!started) throw std::runtime_error("start refused");
|
||||
const std::string active_id=*started;
|
||||
bool canceled=false;
|
||||
until=robot_bt::SteadyClock::now()+std::chrono::seconds(12);
|
||||
while(robot_bt::SteadyClock::now()<until) {
|
||||
rclcpp::spin_some(node);registry.pump(robot_bt::SteadyClock::now());
|
||||
auto record=registry.find(active_id);
|
||||
if(!record) throw std::runtime_error("registry lost active goal: "+active_id);
|
||||
if((scenario==1||scenario==6)&&record->accepted&&!canceled) {
|
||||
registry.request_cancel(active_id,robot_bt::SteadyClock::now());canceled=true;
|
||||
}
|
||||
if(record->result) break;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
// Pump beyond terminal delivery to expose accidental sends/retries in live transport.
|
||||
until=robot_bt::SteadyClock::now()+std::chrono::milliseconds(350);
|
||||
while(robot_bt::SteadyClock::now()<until) {
|
||||
rclcpp::spin_some(node);registry.pump(robot_bt::SteadyClock::now());
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
auto record=registry.find(active_id);
|
||||
if(!record||!record->result) throw std::runtime_error("result timeout");
|
||||
const auto& result=*record->result;
|
||||
bool blocked_redispatch=false;
|
||||
if(scenario==6) {
|
||||
auto next=request;next.goal_id+="-forbidden-retry";
|
||||
next.trace.subtask_id="navigate-forbidden-retry";++next.trace.attempt;
|
||||
blocked_redispatch=!registry.start(next,robot_bt::SteadyClock::now()).has_value();
|
||||
}
|
||||
nlohmann::json report={{"scenario",scenario},{"code",int(result.code)},{"stop",int(result.stop)},
|
||||
{"state",int(record->state)},{"robot_locked",registry.robot_locked("robot_01")},
|
||||
{"error_code",result.error_code},{"detail",result.detail},{"feedback_sequence",record->last_sequence},
|
||||
{"feedback",record->feedback_snapshot},{"wire_request_type",record->request.wire_request_type},
|
||||
{"wire_result_type",result.wire_result_type},{"wire_result_bytes",result.wire_result_snapshot.size()/2},
|
||||
{"response_valid",result.response.valid},{"mapping_count",driver.mappings().size()},
|
||||
{"unknown_stop_blocks_redispatch",blocked_redispatch}};
|
||||
std::cout<<report.dump()<<std::endl;
|
||||
rclcpp::shutdown();return 0;
|
||||
}catch(const std::exception& e){std::cerr<<e.what()<<std::endl;rclcpp::shutdown();return 1;}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ from bt_skill_interfaces.action import (
|
||||
EvaluateProgress, ExecuteTask, LocalizeTarget3D, LocateShelfColumn,
|
||||
Navigate, NavigateSemantic, PlanTask, VerifyState,
|
||||
)
|
||||
from bt_skill_interfaces.msg import (DenseProgress, ExecutionResult, RobotState, SafetyState,
|
||||
from bt_skill_interfaces.msg import (DenseProgress, ExecutionResult, NavigationResult, RobotState, SafetyState,
|
||||
VerificationEvidence, VisualObservation)
|
||||
from bt_skill_interfaces.srv import GetRobotState, ReconcileGoal
|
||||
from std_msgs.msg import String
|
||||
@@ -43,8 +43,8 @@ ACTION_ENDPOINTS = {
|
||||
}
|
||||
MOTION = frozenset(("navigate", "navigate_semantic", "execute_manipulation", "execute_posture", "execute_task"))
|
||||
SUCCESS_PHASES = {
|
||||
"navigate": (Navigate.Feedback.CHECKING, Navigate.Feedback.PLANNING,
|
||||
Navigate.Feedback.NAVIGATING, Navigate.Feedback.ARRIVING),
|
||||
"navigate": (Navigate.Feedback.ACCEPTED, Navigate.Feedback.CHECKING,
|
||||
Navigate.Feedback.NAVIGATING),
|
||||
"execute_manipulation": (ExecuteManipulation.Feedback.PREPARING,
|
||||
ExecuteManipulation.Feedback.WAITING_OBSERVATION,
|
||||
ExecuteManipulation.Feedback.INFERRING,
|
||||
@@ -68,11 +68,25 @@ def elapsed_message(seconds):
|
||||
return Duration(sec=nanoseconds // 1000000000, nanosec=nanoseconds % 1000000000)
|
||||
|
||||
|
||||
def normalized_navigation_pose(target_pose):
|
||||
pose = copy.deepcopy(target_pose)
|
||||
q = pose.pose.orientation
|
||||
scale = max(abs(q.x), abs(q.y), abs(q.z), abs(q.w))
|
||||
values = [value / scale for value in (q.x, q.y, q.z, q.w)]
|
||||
norm = math.sqrt(sum(value * value for value in values))
|
||||
q.x, q.y, q.z, q.w = (value / norm for value in values)
|
||||
return pose
|
||||
|
||||
|
||||
def lifecycle_phases(name, kind):
|
||||
if name == "navigate" and kind == "obstacle_recovery":
|
||||
return (Navigate.Feedback.CHECKING, Navigate.Feedback.PLANNING,
|
||||
Navigate.Feedback.NAVIGATING, Navigate.Feedback.WAITING_OBSTACLE,
|
||||
Navigate.Feedback.RECOVERING, Navigate.Feedback.ARRIVING)
|
||||
return (Navigate.Feedback.ACCEPTED, Navigate.Feedback.CHECKING,
|
||||
Navigate.Feedback.NAVIGATING, Navigate.Feedback.BLOCKED,
|
||||
Navigate.Feedback.NAVIGATING)
|
||||
if name == "navigate" and kind == "blocked":
|
||||
return (*SUCCESS_PHASES[name], Navigate.Feedback.BLOCKED)
|
||||
if name == "navigate" and kind == "not_ready":
|
||||
return (Navigate.Feedback.ACCEPTED, Navigate.Feedback.CHECKING)
|
||||
return SUCCESS_PHASES.get(name, (0,))
|
||||
|
||||
|
||||
@@ -170,13 +184,13 @@ class MockSkills(Node):
|
||||
raise ValueError("capture boundary is in the future")
|
||||
if name == "navigate":
|
||||
p, q = request.target_pose.pose.position, request.target_pose.pose.orientation
|
||||
values = [p.x, p.y, p.z, q.x, q.y, q.z, q.w, request.position_tolerance, request.orientation_tolerance]
|
||||
if not all(math.isfinite(v) for v in values) or not request.target_pose.header.frame_id:
|
||||
values = [p.x, p.y, p.z, q.x, q.y, q.z, q.w, request.position_tolerance, request.yaw_tolerance]
|
||||
if not all(math.isfinite(v) for v in values) or request.target_pose.header.frame_id != "map":
|
||||
raise ValueError("navigation pose is invalid")
|
||||
if request.position_tolerance <= 0 or not 0 < request.orientation_tolerance <= math.pi:
|
||||
if request.position_tolerance <= 0 or request.yaw_tolerance <= 0:
|
||||
raise ValueError("navigation tolerances are invalid")
|
||||
if abs(q.x*q.x + q.y*q.y + q.z*q.z + q.w*q.w - 1.0) > 0.001:
|
||||
raise ValueError("navigation quaternion must have unit norm")
|
||||
if not any((q.x, q.y, q.z, q.w)):
|
||||
raise ValueError("navigation quaternion must be nonzero")
|
||||
elif name == "execute_manipulation":
|
||||
if request.skill not in ("pick", "place") or not request.instruction.strip():
|
||||
raise ValueError("manipulation skill/instruction is invalid")
|
||||
@@ -310,7 +324,7 @@ class MockSkills(Node):
|
||||
self._feedback(name, handle, sequence, now - started, phases[phase_index])
|
||||
emitted_phase = phase_index
|
||||
time.sleep(min(0.02, max(0, deadline - now)))
|
||||
if kind == "failed" and outcome == ExecutionResult.COMPLETED:
|
||||
if (kind == "failed" or name == "navigate" and kind in ("blocked", "not_ready")) and outcome == ExecutionResult.COMPLETED:
|
||||
outcome = ExecutionResult.FAILED
|
||||
if kind == "stop_unknown" and outcome == ExecutionResult.COMPLETED:
|
||||
outcome, stop_state = ExecutionResult.FAILED, ExecutionResult.UNKNOWN
|
||||
@@ -320,7 +334,7 @@ class MockSkills(Node):
|
||||
# Unknown execution state stays reserved. A client cannot infer stop from this exception.
|
||||
stop_state = ExecutionResult.UNKNOWN if name in MOTION else ExecutionResult.CONFIRMED
|
||||
if hasattr(result, "result"):
|
||||
result.result = self._execution_result(ExecutionResult.FAILED, stop_state, goal_id, "MOCK_EXCEPTION", str(exc))
|
||||
result.result = self._execution_result(ExecutionResult.FAILED, stop_state, goal_id, "MOCK_EXCEPTION", str(exc), name=name)
|
||||
elif hasattr(result, "evidence"):
|
||||
result.evidence.status = VerificationEvidence.UNKNOWN
|
||||
result.evidence.error_code = "MOCK_EXCEPTION"
|
||||
@@ -371,13 +385,12 @@ class MockSkills(Node):
|
||||
if hasattr(feedback, "elapsed_time"):
|
||||
feedback.elapsed_time = elapsed_message(elapsed)
|
||||
if name == "navigate":
|
||||
feedback.pose_valid = True
|
||||
feedback.current_pose = copy.deepcopy(handle.request.target_pose)
|
||||
feedback.errors_valid = True
|
||||
feedback.current_pose_valid = True
|
||||
feedback.current_pose = normalized_navigation_pose(handle.request.target_pose)
|
||||
feedback.error_valid = True
|
||||
feedback.position_error = 0.0
|
||||
feedback.orientation_error = 0.0
|
||||
feedback.blocked_valid = True
|
||||
feedback.blocked = False
|
||||
feedback.yaw_error = 0.0
|
||||
feedback.blocked = feedback.phase == Navigate.Feedback.BLOCKED
|
||||
if name == "execute_manipulation":
|
||||
feedback.progress_valid = False
|
||||
handle.publish_feedback(feedback)
|
||||
@@ -389,9 +402,18 @@ class MockSkills(Node):
|
||||
if stopping is not None:
|
||||
self._feedback(name, handle, sequence, elapsed, stopping)
|
||||
|
||||
def _execution_result(self, outcome, stop_state, goal_id, error="", message="SIMULATED execution only"):
|
||||
result = ExecutionResult()
|
||||
result.status, result.stop_state = outcome, stop_state
|
||||
def _execution_result(self, outcome, stop_state, goal_id, error="", message="SIMULATED execution only", name="", kind="normal"):
|
||||
result = NavigationResult() if name == "navigate" else ExecutionResult()
|
||||
if name == "navigate":
|
||||
status = {ExecutionResult.COMPLETED: NavigationResult.SUCCEEDED,
|
||||
ExecutionResult.CANCELED: NavigationResult.CANCELED,
|
||||
ExecutionResult.TIMED_OUT: NavigationResult.TIMEOUT,
|
||||
ExecutionResult.FAILED: NavigationResult.FAILED}[outcome]
|
||||
if outcome == ExecutionResult.FAILED and kind in ("blocked", "not_ready"):
|
||||
status = NavigationResult.BLOCKED if kind == "blocked" else NavigationResult.NOT_READY
|
||||
else:
|
||||
status = outcome
|
||||
result.status, result.stop_state = status, stop_state
|
||||
result.error_code = error or ("" if outcome == ExecutionResult.COMPLETED else "MOCK_TERMINATED")
|
||||
result.message = message
|
||||
if stop_state == ExecutionResult.CONFIRMED:
|
||||
@@ -408,14 +430,20 @@ class MockSkills(Node):
|
||||
record = "sim://" + name + "/" + goal_id
|
||||
ok = outcome == ExecutionResult.COMPLETED
|
||||
if hasattr(result, "result"):
|
||||
result.result = self._execution_result(outcome, stop_state, goal_id)
|
||||
error = ""
|
||||
if name == "navigate" and outcome == ExecutionResult.FAILED:
|
||||
if kind == "blocked": error = "BLOCKED"
|
||||
if kind == "not_ready": error = fixture.get("error_code", "INPUTS_UNHEALTHY")
|
||||
result.result = self._execution_result(outcome, stop_state, goal_id, error, name=name, kind=kind)
|
||||
if name in ("navigate", "navigate_semantic"):
|
||||
if ok and stop_state == ExecutionResult.CONFIRMED:
|
||||
with self.lock:
|
||||
self.geometry_epoch += 1
|
||||
if name == "navigate":
|
||||
result.pose_valid = result.errors_valid = ok
|
||||
result.final_pose = copy.deepcopy(request.target_pose)
|
||||
result.final_pose_valid = ok
|
||||
if ok:
|
||||
result.final_pose = normalized_navigation_pose(request.target_pose)
|
||||
result.final_position_error = result.final_yaw_error = 0.0
|
||||
else:
|
||||
pose = fixture.get("final_pose")
|
||||
result.pose_valid = result.errors_valid = ok and pose is not None
|
||||
|
||||
@@ -17,7 +17,7 @@ KINDS = {
|
||||
"unavailable", "invalid_pose", "emergency_stop", "protective_stop",
|
||||
"model_estimate", "fused",
|
||||
"cancel_stop_unknown", "timeout_stop_unknown",
|
||||
"obstacle_recovery",
|
||||
"obstacle_recovery", "blocked", "not_ready",
|
||||
}
|
||||
FIXTURE_FIELDS = {
|
||||
"kind", "duration_seconds", "shelf_id", "side_id", "column_id", "tier_id",
|
||||
@@ -26,12 +26,12 @@ FIXTURE_FIELDS = {
|
||||
"observation_id", "image_path", "station_id", "registry_version",
|
||||
"calibration_id", "geometry_epoch", "status_json",
|
||||
"stop_delay_seconds",
|
||||
"final_pose",
|
||||
"final_pose", "error_code",
|
||||
}
|
||||
|
||||
BASE_KINDS = {"normal", "failed", "timeout", "silence", "reject", "native_mismatch"}
|
||||
ACTION_KINDS = {
|
||||
"navigate": BASE_KINDS | {"stop_unknown", "cancel_stop_unknown", "timeout_stop_unknown", "obstacle_recovery"},
|
||||
"navigate": BASE_KINDS | {"stop_unknown", "cancel_stop_unknown", "timeout_stop_unknown", "obstacle_recovery", "blocked", "not_ready"},
|
||||
"navigate_semantic": BASE_KINDS | {"stop_unknown", "cancel_stop_unknown", "timeout_stop_unknown"},
|
||||
"execute_manipulation": BASE_KINDS | {"stop_unknown", "cancel_stop_unknown", "timeout_stop_unknown"},
|
||||
"execute_posture": BASE_KINDS | {"stop_unknown", "cancel_stop_unknown", "timeout_stop_unknown"},
|
||||
@@ -86,6 +86,12 @@ def parse_scenarios(raw):
|
||||
raise ValueError("unsupported fixture kind")
|
||||
if fixture.get("kind", "normal") not in ACTION_KINDS[name]:
|
||||
raise ValueError("fixture kind has no effect for " + name)
|
||||
if "error_code" in fixture:
|
||||
code = fixture["error_code"]
|
||||
if (name != "navigate" or fixture.get("kind") != "not_ready" or
|
||||
code not in {"INPUTS_UNHEALTHY", "ROBOT_STATE_UNAVAILABLE", "BACKEND_NOT_CONFIGURED",
|
||||
"ROBOT_EMERGENCY_STOP", "ROBOT_PROTECTIVE_STOP", "ROBOT_MOTION_NOT_ALLOWED"}):
|
||||
raise ValueError("error_code requires a supported navigation readiness reason")
|
||||
delay = fixture.get("duration_seconds", 0.2)
|
||||
if type(delay) not in (int, float) or not math.isfinite(delay) or not 0 <= delay <= 120:
|
||||
raise ValueError("fixture duration must be finite in [0,120]")
|
||||
|
||||
@@ -10,6 +10,7 @@ rosidl_generate_interfaces(${PROJECT_NAME}
|
||||
"msg/ObjectTarget.msg"
|
||||
"msg/RegionTarget.msg"
|
||||
"msg/ExecutionResult.msg"
|
||||
"msg/NavigationResult.msg"
|
||||
"msg/ObservationContext.msg"
|
||||
"msg/RobotState.msg"
|
||||
"msg/SafetyState.msg"
|
||||
|
||||
@@ -1,33 +1,30 @@
|
||||
# Preserved outer interface from BT DR pp13-14. Canonical ROS2 type: Navigate.
|
||||
# Navigation contract: trace and stop evidence retained across all attempts.
|
||||
# Enum values are explicit; both peers must build this same interface package.
|
||||
bt_skill_interfaces/TaskTrace trace
|
||||
geometry_msgs/PoseStamped target_pose
|
||||
float64 position_tolerance
|
||||
float64 orientation_tolerance
|
||||
float64 yaw_tolerance
|
||||
builtin_interfaces/Duration timeout
|
||||
---
|
||||
bt_skill_interfaces/ExecutionResult result
|
||||
bool pose_valid
|
||||
bt_skill_interfaces/NavigationResult result
|
||||
bool final_pose_valid
|
||||
geometry_msgs/PoseStamped final_pose
|
||||
bool errors_valid
|
||||
float64 final_position_error
|
||||
float64 final_orientation_error
|
||||
float64 final_yaw_error
|
||||
---
|
||||
uint8 CHECKING=0
|
||||
uint8 PLANNING=1
|
||||
uint8 ACCEPTED=0
|
||||
uint8 CHECKING=1
|
||||
uint8 NAVIGATING=2
|
||||
uint8 WAITING_OBSTACLE=3
|
||||
uint8 RECOVERING=4
|
||||
uint8 ARRIVING=5
|
||||
uint8 STOPPING=6
|
||||
uint8 BLOCKED=3
|
||||
uint8 STOPPING=4
|
||||
builtin_interfaces/Time stamp
|
||||
uint32 sequence
|
||||
uint8 phase
|
||||
bool pose_valid
|
||||
bool current_pose_valid
|
||||
geometry_msgs/PoseStamped current_pose
|
||||
bool errors_valid
|
||||
bool error_valid
|
||||
float64 position_error
|
||||
float64 orientation_error
|
||||
bool blocked_valid
|
||||
float64 yaw_error
|
||||
bool blocked
|
||||
builtin_interfaces/Duration elapsed_time
|
||||
string message
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# Navigation-specific outcomes; do not decode with ExecutionResult enum values.
|
||||
uint8 SUCCEEDED=0
|
||||
uint8 CANCELED=1
|
||||
uint8 TIMEOUT=2
|
||||
uint8 BLOCKED=3
|
||||
uint8 NOT_READY=4
|
||||
uint8 FAILED=5
|
||||
uint8 UNKNOWN=0
|
||||
uint8 CONFIRMED=1
|
||||
uint8 status
|
||||
string error_code
|
||||
string message
|
||||
uint8 stop_state
|
||||
builtin_interfaces/Time stopped_at
|
||||
string stop_evidence_ref
|
||||
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0"?>
|
||||
<package format="3">
|
||||
<name>bt_skill_interfaces</name>
|
||||
<version>1.2.0</version>
|
||||
<description>Candidate v1 robot behavior-tree skill and evidence contracts.</description>
|
||||
<version>2.0.0</version>
|
||||
<description>Robot skill contracts with navigation-specific outcomes and stop evidence.</description>
|
||||
<maintainer email="feiyuwang1998@gmail.com">wangfeiyu</maintainer>
|
||||
<license>Proprietary</license>
|
||||
<buildtool_depend>ament_cmake</buildtool_depend>
|
||||
|
||||
Reference in New Issue
Block a user