|
|
|
@@ -0,0 +1,121 @@
|
|
|
|
|
"""ROS adapters; numerical/model logic lives in the separately installable package."""
|
|
|
|
|
import threading,time,json,queue
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from robot_bt_coordinator.plan import strict_json,canonical
|
|
|
|
|
from robot_robobrain.service import BrainService
|
|
|
|
|
from robot_robobrain.dopamine import DenseFeedbackService
|
|
|
|
|
from robot_robobrain.backends import ProcessBackend,FixtureBackend
|
|
|
|
|
from robot_robobrain.observations import Observation,ObservationCache
|
|
|
|
|
|
|
|
|
|
def ns(t):return t.sec*1_000_000_000+t.nanosec
|
|
|
|
|
def assign_time(t,n):t.sec=int(n)//1_000_000_000;t.nanosec=int(n)%1_000_000_000
|
|
|
|
|
|
|
|
|
|
def perception_goal(g,kind):
|
|
|
|
|
q=dict(task_id=g.task_id,subtask_id=g.subtask_id,target_ref=g.target_ref,
|
|
|
|
|
target_description=g.target_description,capture_after=ns(g.capture_after),
|
|
|
|
|
timeout=g.timeout.sec+g.timeout.nanosec/1e9)
|
|
|
|
|
if kind=='shelf':
|
|
|
|
|
q.update(source_region_ref=g.source_region_ref,observation_station_id=g.observation_station_id,
|
|
|
|
|
station_registry_version=g.station_registry_version)
|
|
|
|
|
else:
|
|
|
|
|
q.update(expected_geometry_epoch=g.expected_geometry_epoch,shelf_id=g.shelf_id,
|
|
|
|
|
column_id=g.column_id,tier_id=g.tier_id,station_binding_ref=g.station_binding_ref)
|
|
|
|
|
return q
|
|
|
|
|
|
|
|
|
|
def run_node(dense=False):
|
|
|
|
|
import rclpy
|
|
|
|
|
from rclpy.node import Node
|
|
|
|
|
from rclpy.action import ActionServer,GoalResponse,CancelResponse
|
|
|
|
|
from rclpy.callback_groups import ReentrantCallbackGroup
|
|
|
|
|
from rclpy.executors import MultiThreadedExecutor
|
|
|
|
|
from bt_skill_interfaces.action import PlanTask,LocateShelfColumn,LocalizeTarget3D,EvaluateProgress
|
|
|
|
|
from bt_skill_interfaces.msg import VisualObservation,DenseProgress
|
|
|
|
|
class Server(Node):
|
|
|
|
|
def __init__(self):
|
|
|
|
|
super().__init__('robodopamine_server' if dense else 'robobrain_server')
|
|
|
|
|
self.group=ReentrantCallbackGroup();self.lock=threading.Lock();self.busy=False
|
|
|
|
|
simulation=self.declare_parameter('simulation',False).value
|
|
|
|
|
record_dir=self.declare_parameter('record_directory','').value
|
|
|
|
|
if not record_dir:raise ValueError('persistent record_directory required')
|
|
|
|
|
media=self.declare_parameter('media_root','').value
|
|
|
|
|
if not media:raise ValueError('trusted media_root required')
|
|
|
|
|
self.cache=ObservationCache(media)
|
|
|
|
|
if simulation:
|
|
|
|
|
if not self.get_namespace().startswith('/sim'):raise ValueError('fixture server restricted to /sim namespace')
|
|
|
|
|
fixture=self.declare_parameter('fixture_file','').value
|
|
|
|
|
with open(fixture) as f:raw=strict_json(f.read())
|
|
|
|
|
backend=FixtureBackend(lambda request:canonical(raw[request['capability']]))
|
|
|
|
|
else:
|
|
|
|
|
argv=strict_json(self.declare_parameter('worker_argv_json','[]').value)
|
|
|
|
|
backend=ProcessBackend(argv,self.declare_parameter('model_version','').value)
|
|
|
|
|
backend.infer({'capability':'__health__'},float(self.declare_parameter('model_load_timeout_seconds',300.).value))
|
|
|
|
|
self.service=DenseFeedbackService(backend,record_dir) if dense else BrainService(backend,record_dir)
|
|
|
|
|
self.publisher=self.create_publisher(DenseProgress,'monitor/dense_progress',10) if dense else None
|
|
|
|
|
self.subscription=self.create_subscription(VisualObservation,'observations/scene',self.observation,10,callback_group=self.group)
|
|
|
|
|
self.servers=[]
|
|
|
|
|
for name,action,kind in ([('monitor/evaluate_progress',EvaluateProgress,'progress')] if dense else [('tasks/plan',PlanTask,'plan'),('skills/locate_shelf_column',LocateShelfColumn,'shelf'),('skills/localize_target_3d',LocalizeTarget3D,'localize3d')]):
|
|
|
|
|
self.servers.append(ActionServer(self,action,name,execute_callback=lambda h,a=action,k=kind:self.execute(h,a,k),goal_callback=self.admit,cancel_callback=lambda _:CancelResponse.ACCEPT,callback_group=self.group))
|
|
|
|
|
def observation(self,m):
|
|
|
|
|
try:self.cache.put(Observation(m.observation_id,ns(m.header.stamp),m.header.frame_id,m.image_path,m.station_id,m.registry_version,m.shelf_id,m.calibration_id,m.geometry_epoch))
|
|
|
|
|
except (ValueError,OSError) as ex:self.get_logger().warning(str(ex))
|
|
|
|
|
def admit(self,g):
|
|
|
|
|
if not 0<g.timeout.sec+g.timeout.nanosec/1e9<=3600 or not 0<=g.timeout.nanosec<1e9:return GoalResponse.REJECT
|
|
|
|
|
with self.lock:
|
|
|
|
|
if self.busy:return GoalResponse.REJECT
|
|
|
|
|
self.busy=True
|
|
|
|
|
return GoalResponse.ACCEPT
|
|
|
|
|
def work(self,g,kind,cancel):
|
|
|
|
|
timeout=g.timeout.sec+g.timeout.nanosec/1e9
|
|
|
|
|
if kind=='plan':
|
|
|
|
|
q=dict(task_id=g.task_id,task_revision=g.task_revision,planning_generation=g.planning_generation,instruction=g.instruction,known_info=strict_json(g.known_info_json),context=strict_json(g.context_snapshot_json),constraints=strict_json(g.constraints_json),timeout=timeout)
|
|
|
|
|
return self.service.plan(q,cancel)
|
|
|
|
|
if kind=='progress':
|
|
|
|
|
frames=strict_json(g.window_json)
|
|
|
|
|
if not isinstance(frames,list):raise ValueError('window must be an array')
|
|
|
|
|
for frame in frames:
|
|
|
|
|
for path in frame['views'].values():
|
|
|
|
|
p=Path(path).resolve(strict=True)
|
|
|
|
|
if not p.is_relative_to(self.cache.root) or not p.is_file() or p.stat().st_size>32*1024*1024:raise ValueError('invalid window media path')
|
|
|
|
|
q=dict(task_id=g.trace.task_id,run_id=g.trace.run_id,subtask_id=g.trace.subtask_id,task_description=g.task_description,capture_after=ns(g.capture_after)/1e9,sequence=g.sequence,timeout=timeout)
|
|
|
|
|
return self.service.evaluate(q,frames,self.get_clock().now().nanoseconds/1e9,cancel)
|
|
|
|
|
q=perception_goal(g,kind)
|
|
|
|
|
obs=self.cache.get();method=self.service.shelf if kind=='shelf' else self.service.localize
|
|
|
|
|
return method(q,obs,self.get_clock().now().nanoseconds,cancel=cancel)
|
|
|
|
|
def execute(self,h,action,kind):
|
|
|
|
|
cancel=threading.Event();completed=queue.Queue(maxsize=1);deadline=time.monotonic()+h.request.timeout.sec+h.request.timeout.nanosec/1e9
|
|
|
|
|
def worker():
|
|
|
|
|
try:completed.put(self.work(h.request,kind,cancel))
|
|
|
|
|
except Exception as ex:completed.put(dict(status='FAILED',state='UNKNOWN',error_code='SERVICE_ERROR',message=str(ex)))
|
|
|
|
|
thread=threading.Thread(target=worker,daemon=True);thread.start();sequence=0;expired=False
|
|
|
|
|
while thread.is_alive():
|
|
|
|
|
if h.is_cancel_requested or time.monotonic()>=deadline:cancel.set();expired=time.monotonic()>=deadline
|
|
|
|
|
sequence+=1;f=action.Feedback();f.stamp=self.get_clock().now().to_msg();f.sequence=sequence;f.phase=1;f.message='canceling inference' if cancel.is_set() else 'processing';h.publish_feedback(f)
|
|
|
|
|
thread.join(.2)
|
|
|
|
|
data=completed.get();r=action.Result()
|
|
|
|
|
try:
|
|
|
|
|
if cancel.is_set():data=dict(status='FAILED',state='UNKNOWN',error_code='TIMEOUT' if expired else 'CANCELED',record_ref=data.get('record_ref',''))
|
|
|
|
|
if kind=='plan':
|
|
|
|
|
r.status={'PLAN_READY':0,'NEEDS_CLARIFICATION':1}.get(data.get('status'),2);r.task_plan_json=canonical(data.get('plan',{}));r.planning_record_ref=data.get('record_ref','');r.error_code=data.get('error_code','');r.message=data.get('message','')
|
|
|
|
|
elif kind=='progress':
|
|
|
|
|
f=r.feedback_state;f.trace=h.request.trace;f.sequence=h.request.sequence;f.state=data.get('state','UNKNOWN');f.progress=float(data.get('progress',0));f.progress_valid=f.state!='UNKNOWN';f.hop_json=canonical(data.get('hop'));f.record_ref=data.get('record_ref','');assign_time(f.observed_at,int(data.get('stamp',0)*1e9));r.error_code=data.get('error_code','');self.publisher.publish(f)
|
|
|
|
|
else:
|
|
|
|
|
r.status={'SUCCEEDED':0,'NOT_FOUND':2,'AMBIGUOUS':3}.get(data.get('status'),1);r.error_code=data.get('error_code','');r.message=data.get('message','');r.record_ref=data.get('record_ref','')
|
|
|
|
|
if kind=='shelf':
|
|
|
|
|
for field in ('shelf_id','side_id','column_id','tier_id','observation_id'):setattr(r,field,data.get(field,''))
|
|
|
|
|
r.confidence=float(data.get('confidence',0));assign_time(r.observed_at,data.get('observed_at',0))
|
|
|
|
|
else:
|
|
|
|
|
r.target_ref=h.request.target_ref;r.geometry_valid=False;r.grasp_point_valid=False;r.position_error_bound_valid=False;r.measurement_source=1;r.quality_code=data.get('quality_code','INVALID');r.observation_id=data.get('observation_id','');r.calibration_id=data.get('calibration_id','');r.geometry_epoch=data.get('geometry_epoch',0)
|
|
|
|
|
if 'target_point' in data:
|
|
|
|
|
p=data['target_point'];r.target_point.header.frame_id=p['frame_id'];assign_time(r.target_point.header.stamp,p['stamp_ns']);assign_time(r.rgb_stamp,p['stamp_ns']);r.target_point.point.x,r.target_point.point.y,r.target_point.point.z=map(float,p['point'])
|
|
|
|
|
if h.is_cancel_requested:h.canceled()
|
|
|
|
|
elif expired:h.abort()
|
|
|
|
|
elif kind=='plan' or kind=='progress' or data.get('status')=='SUCCEEDED':h.succeed()
|
|
|
|
|
else:h.abort()
|
|
|
|
|
finally:
|
|
|
|
|
with self.lock:self.busy=False
|
|
|
|
|
return r
|
|
|
|
|
rclpy.init();node=Server();executor=MultiThreadedExecutor(num_threads=4);executor.add_node(node)
|
|
|
|
|
try:executor.spin()
|
|
|
|
|
finally:node.service.backend.close();executor.shutdown();node.destroy_node();rclpy.shutdown()
|
|
|
|
|
def brain_main():run_node(False)
|
|
|
|
|
def dopamine_main():run_node(True)
|