Update workstation UI, ROS task control and device integration

This commit is contained in:
cheng
2026-10-20 15:46:13 +08:00
parent 2b74c50ba8
commit e5b06df6da
141 changed files with 91520 additions and 748 deletions
+89 -6
View File
@@ -8,27 +8,40 @@ from station.control_service import StationService
class Gateway:
active_arm='89'
def __init__(self): self.started=None;self.requests=[]
def graph(self): return {'nodes':[],'topics':[],'services':['/station/grippers/arm_89/execute']}
def graph(self):
return {'nodes':['dh_pgea'],'topics':['/gripper/states'],
'services':['/gripper/open','/gripper/close','/gripper/initialize','/dh_pgea/execute']}
def publisher_count(self, topic): return 1 if topic=='/gripper/states' else 0
def states(self):
elapsed=time.monotonic()-self.started if self.started else 0
return {},{'89':{'connected':True,'moving':bool(self.started and elapsed<.25),
'grip_state':0 if self.started and elapsed<.25 else 1,'permille':500.}}
moving=bool(self.started and elapsed<getattr(self,'motion_duration',.05))
last=self.requests[-1][0] if self.requests else ''
if last=='/gripper/close':
permille=0. if not moving else 400.
elif last=='/gripper/open':
permille=1000. if not moving else 400.
else:
permille=500.
return {},{'89':{'connected':True,'moving':moving,
'grip_state':0 if moving else 1,'permille':permille,
'gripped':False,'opening_mm':permille/1000.*20}}
def call(self,typ,name,request,event,timeout):
self.requests.append(request);self.started=time.monotonic()
self.requests.append((name,request));self.started=time.monotonic()
return type('Reply',(),{'success':True,'message':'accepted'})()
def close(self): pass
class GripperCommandsTest(TestCase):
def test_waits_for_physical_completion_and_rejects_bounds(self):
gateway=Gateway()
gateway=Gateway();gateway.motion_duration=.25
with tempfile.TemporaryDirectory() as root:
service=StationService(root=root,gateway=gateway)
try:
arm=service.arms['89'];start=time.monotonic()
result=service._gripper('gripper_tune',arm,{'kind':'pos','pos':500,'force':33,'speed':25},threading.Event())
self.assertTrue(result['ok']);self.assertGreater(time.monotonic()-start,.2)
self.assertEqual(gateway.requests[0].command.position,arm['stroke_mm']/2)
self.assertEqual(gateway.requests[0][0],'/dh_pgea/execute')
self.assertEqual(gateway.requests[0][1].command.position,arm['stroke_mm']/2)
count=len(gateway.requests)
with self.assertRaises(ValueError):
service._gripper('gripper_tune',arm,{'kind':'pos','pos':500,'force':999,'speed':25},threading.Event())
@@ -49,3 +62,73 @@ class GripperCommandsTest(TestCase):
self.assertEqual(gateway.requests,[])
self.assertTrue(service._frozen_config.is_file())
finally: service.close()
def test_open_close_do_not_require_initialized(self):
gateway=Gateway()
original=gateway.states
def states():
arms,grips=original()
row=dict(grips['89']);row['initialized']=False
return arms,{'89':row}
gateway.states=states
with tempfile.TemporaryDirectory() as root:
service=StationService(root=root,gateway=gateway)
try:
closed=service._gripper('gripper_close',service.arms['89'],{},threading.Event())
self.assertTrue(closed['ok'])
self.assertEqual(gateway.requests[-1][0],'/gripper/close')
opened=service._gripper('gripper_open',service.arms['89'],{},threading.Event())
self.assertTrue(opened['ok'])
self.assertEqual(gateway.requests[-1][0],'/gripper/open')
finally: service.close()
def test_need_init_summary_does_not_block_open_close(self):
from station.operator_gripper_state import present
row=present('89',rotary=False,stroke_mm=20.0,
reading={'connected':True,'initialized':False,'permille':981,
'grip_state':1,'source':'ROS'})
self.assertIn('零点未标定', row['summary'])
self.assertIn('开口 19.6 mm', row['summary'])
self.assertNotIn('不会动', row['summary'])
self.assertNotIn('请先点到位', row['summary'])
def test_open_close_init_call_dh_pgea_triggers_and_close_must_reach_zero(self):
gateway=Gateway()
with tempfile.TemporaryDirectory() as root:
service=StationService(root=root,gateway=gateway)
try:
arm=service.arms['89']
closed=service._gripper('gripper_close',arm,{},threading.Event())
self.assertTrue(closed['ok'])
self.assertEqual(gateway.requests[-1][0],'/gripper/close')
opened=service._gripper('gripper_open',arm,{},threading.Event())
self.assertTrue(opened['ok'])
self.assertEqual(gateway.requests[-1][0],'/gripper/open')
inited=service._gripper('gripper_init',arm,{},threading.Event())
self.assertTrue(inited['ok'])
self.assertEqual(gateway.requests[-1][0],'/gripper/initialize')
finally: service.close()
def test_close_fails_when_firmware_says_arrived_but_opening_remains(self):
gateway=Gateway()
def states():
return {},{'89':{'connected':True,'moving':False,'grip_state':1,'permille':130.,
'gripped':False,'opening_mm':2.6}}
gateway.states=states
with tempfile.TemporaryDirectory() as root:
service=StationService(root=root,gateway=gateway)
try:
result=service._gripper('gripper_close',service.arms['89'],{},threading.Event())
self.assertFalse(result['ok'])
self.assertIn('未闭合到位', result['message'])
self.assertIn('2.6', result['message'])
finally: service.close()
def test_gripper_plan_starts_dh_pgea_without_auto_init(self):
from station.launch_plans import gripper_plan
argv=gripper_plan({'id':'89','rotary':False,'port':'/dev/ttyUSB0','stroke_mm':20.0,'gripper':'PGEA-100-20-W'},
{'baud':115200,'force_pct':20,'speed_pct':50})
joined=' '.join(str(part) for part in argv)
self.assertIn('arm_application.dh_pgea_node', joined)
self.assertIn('auto_init:=false', joined)
self.assertNotIn('gripper_node', joined)