Update workstation UI, ROS task control and device integration
This commit is contained in:
@@ -12,11 +12,47 @@ assert app.property('darkTheme') is True
|
||||
assert window._theme_selector.currentData()=='dark'
|
||||
window._apply_theme('light',persist=False)
|
||||
assert app.property('darkTheme') is False
|
||||
# A first status message and its expiry must not resize the dashboard.
|
||||
app.processEvents()
|
||||
content_before=window.centralWidget().geometry()
|
||||
window.statusBar().showMessage('刷新相机 · 状态提示 '*30)
|
||||
app.processEvents()
|
||||
assert window.centralWidget().geometry()==content_before
|
||||
window.statusBar().clearMessage();app.processEvents()
|
||||
assert window.centralWidget().geometry()==content_before
|
||||
assert window._theme_selector.currentData()=='light'
|
||||
assert 'rclpy' not in sys.modules
|
||||
assert 'serial' not in sys.modules
|
||||
assert 'station.operator_console' not in sys.modules
|
||||
assert set(window._pages) == {'main','calibration','settings','ros_monitor'}
|
||||
# Offline display uses the configured HOME, while real feedback always wins.
|
||||
assert window._home_poses[window._arm] != [0.]*7
|
||||
assert window._arm_sketch._pose_key[0] == tuple(round(q,3) for q in window._home_poses[window._arm])
|
||||
assert not window._arm_sketch._live
|
||||
from unittest.mock import patch
|
||||
live_snap=dict(window._snap)
|
||||
live_snap['arm_live']={window._arm:{'pose_live':True,'q':[.12]*7}}
|
||||
with patch.object(ui,'status',return_value=live_snap):
|
||||
window._refresh()
|
||||
assert window._arm_sketch._pose_key[0]==(.12,)*7
|
||||
assert window._arm_sketch._live
|
||||
window._refresh()
|
||||
# Calibration follows arm/mode and shares the existing camera preview path.
|
||||
from unittest.mock import patch
|
||||
window._show_page('calibration')
|
||||
window._snap['camera_ownership']=[{'id':cam['id'],'owned':True} for cam in ui.camera_view.targets()]
|
||||
with patch.object(ui.camera_view, 'watch') as watch:
|
||||
window._calibration_mode.setCurrentIndex(0)
|
||||
window._sync_camera_watch()
|
||||
wrist=window._calibration_camera()
|
||||
assert wrist and wrist['arm']==window._arm
|
||||
assert wrist['id']+'_color' in watch.call_args.args[0]
|
||||
window._calibration_mode.setCurrentIndex(1)
|
||||
outside=window._calibration_camera()
|
||||
assert outside and not outside.get('arm')
|
||||
assert outside['id']+'_color' in watch.call_args.args[0]
|
||||
window._calibration_mode.setCurrentIndex(0)
|
||||
window._show_page('main')
|
||||
# A photo must preserve the current RGB frame; no capture/device operation is needed.
|
||||
import tempfile
|
||||
from unittest.mock import patch
|
||||
@@ -34,6 +70,66 @@ with tempfile.TemporaryDirectory() as pictures:
|
||||
assert selected['id'] in window._camera_windows
|
||||
window._camera_windows[selected['id']].close()
|
||||
assert not window._camera_windows
|
||||
# Device controls only submit explicit actions; changing settings never writes to hardware.
|
||||
assert not hasattr(window,'_grip_force') and not hasattr(window,'_grip_target')
|
||||
assert window._gripper_probe_button.text()=='连接 / 探测'
|
||||
assert window._sensor_refresh_button.text()=='刷新传感器'
|
||||
assert window._sensor_reset_button.text()=='重置底图'
|
||||
assert window._sensor_reset_button.size()==window._sensor_refresh_button.size()
|
||||
assert window._gripper_probe_button.size()==window._sensor_refresh_button.size()
|
||||
assert window._gripper_probe_button.height()==28
|
||||
original_arm=window._arm
|
||||
window._set_arm('91')
|
||||
assert window._gripper_sketch._cad_static and window._gripper_sketch._gl_count>0
|
||||
assert window._gripper_sketch._caption()==''
|
||||
window._set_arm(original_arm)
|
||||
assert not window._gripper_sketch._cad_static
|
||||
assert window._gripper_sketch._caption()==''
|
||||
from PySide6.QtCore import QByteArray,QBuffer,QIODevice
|
||||
sample_image=ui.QImage(32,32,ui.QImage.Format.Format_RGB888);sample_image.fill(ui.QColor('#1683ff'))
|
||||
encoded=QByteArray();buffer=QBuffer(encoded);buffer.open(QIODevice.OpenModeFlag.WriteOnly)
|
||||
assert sample_image.save(buffer,'JPG');buffer.close()
|
||||
fake_snap=dict(window._snap)
|
||||
fake_snap['backend']={**fake_snap.get('backend',{}),'error':''}
|
||||
fake_snap['arm_live']={original_arm:{'tcp_pose':[500.,120.,325.,179.2,.5,88.1]}}
|
||||
fake_snap['tactile']={'left':{'connected':True,'fps':29.,'fnormal':12000.,'fshearx':20000.,'fsheary':-10000.,
|
||||
'force_n':{'fnormal':.477,'fshearx':.65,'fsheary':-.31}}}
|
||||
fake_snap['history_n']={'left':[{'t':time.time()-.1,'fnormal':.477,'fshearx':.65,'fsheary':-.31}]}
|
||||
with patch.object(ui,'status',return_value=fake_snap),patch.object(ui.runtime.tactile_view,'frame',return_value=bytes(encoded)):
|
||||
window._refresh();window._paint_images()
|
||||
assert [v.text() for v in window._tcp_values]==['500.0','120.0','325.0','179.2','0.5','88.1']
|
||||
assert '(29fps)' in window._flow_slots[0][0].text()
|
||||
assert '·' not in window._flow_slots[0][0].text()
|
||||
assert '当前法向力 0.48N' in window._flow_slots[0][0].text()
|
||||
assert not hasattr(window,'_flow_details')
|
||||
assert window._flow_axis_charts['left']['x']._samples[-1]['fshearx']==.65
|
||||
assert window._flow_axis_charts['left']['y']._samples[-1]['fsheary']==-.31
|
||||
assert window._flow_axis_charts['left']['x']._scale==window._flow_axis_charts['left']['y']._scale==10.
|
||||
assert window._flow_axis_charts['left']['x'].MAX_SAMPLES==150
|
||||
app.processEvents()
|
||||
for triplet in window._flow_triplets.values():
|
||||
rectangles=[item.geometry() for item in triplet.items]
|
||||
assert all(rect.width()==rect.height() for rect in rectangles),rectangles
|
||||
assert len({rect.width() for rect in rectangles})==1,rectangles
|
||||
assert len({rect.y() for rect in rectangles})==1,rectangles
|
||||
assert not window._flow_slots[0][1]._source.isNull()
|
||||
# Recent samples are spread across a shorter window without altering source data.
|
||||
chart=window._flow_axis_charts['left']['x']
|
||||
history=[{'seq':i,'fshearx':.2,'fsheary':-.2} for i in range(1800)]
|
||||
chart.set_samples(history)
|
||||
assert len(chart._samples)==150 and chart._samples[0]['seq']==1650
|
||||
assert history[-1]['fshearx']==.2
|
||||
assert ui._display_force(.299)==ui._display_force(-.299)==0.
|
||||
assert ui._display_force(.3)==.3 and ui._display_force(-.3)==-.3
|
||||
assert window._flow_slots[0][0].textFormat()==ui.Qt.TextFormat.PlainText
|
||||
title=window._flow_slots[0][0]
|
||||
triplet=window._flow_triplets['left']
|
||||
assert title.geometry().top()>=triplet.geometry().bottom()
|
||||
fake_snap['history_n']['left'][-1]['fnormal']=.25
|
||||
window._refresh();window._paint_images()
|
||||
assert '当前法向力 0N' in title.text()
|
||||
|
||||
|
||||
errors=[];ticks=[]
|
||||
duration=float(os.environ.get("ORISYS_GUI_TEST_SECONDS","2.2"))
|
||||
started=time.monotonic()
|
||||
@@ -50,6 +146,20 @@ original=sys.excepthook
|
||||
def fail(*args): errors.append(str(args[1]));original(*args)
|
||||
sys.excepthook=fail
|
||||
beat=ui.QTimer();beat.timeout.connect(tick);beat.start(50)
|
||||
# The GUI must not serialize read-only requests behind a long arm operation.
|
||||
parallel=[]
|
||||
def fake_command(action,aid,*args,**kwargs):
|
||||
parallel.append((action,time.monotonic()))
|
||||
if action=='stage_track':
|
||||
time.sleep(.65)
|
||||
parallel.append(('motion_finished',time.monotonic()))
|
||||
return {'ok':True,'message':'isolated test'}
|
||||
ui.run_action=fake_command
|
||||
ui.stop_started=lambda *args:(parallel.append(('stop',time.monotonic())),{'ok':True,'message':'isolated stop'})[1]
|
||||
ui.QTimer.singleShot(220,lambda:window._ask('stage_track'))
|
||||
ui.QTimer.singleShot(260,lambda:window._ask('handeye_show'))
|
||||
ui.QTimer.singleShot(280,lambda:window._ask('stage_track'))
|
||||
ui.QTimer.singleShot(300,window._stop_arm)
|
||||
ui.QTimer.singleShot(200,lambda:window._start(lambda:(time.sleep(.7),{'ok':True,'message':'test'})[1],'test'))
|
||||
ui.QTimer.singleShot(500,lambda:window._show_page('calibration'))
|
||||
ui.QTimer.singleShot(1000,lambda:window._show_page('calibration'))
|
||||
@@ -59,6 +169,23 @@ ui.QTimer.singleShot(round(duration*1000),window.close)
|
||||
ui.QTimer.singleShot(round((duration+15)*1000),app.quit)
|
||||
app.exec()
|
||||
assert len(ticks)>20,len(ticks)
|
||||
assert [name for name,_ in parallel].count('stage_track')==1,parallel
|
||||
assert dict(parallel)['handeye_show']<dict(parallel)['motion_finished'],parallel
|
||||
assert dict(parallel)['stop']<dict(parallel)['motion_finished'],parallel
|
||||
text = window._detail.toPlainText()
|
||||
assert '[点击] 检测跟踪' in text,text
|
||||
assert '[未重复发送]' in text,text
|
||||
assert 'isolated test' in text,text
|
||||
# Repeated refreshes cannot duplicate backend lifecycle events.
|
||||
window._snap['operation_log']={'session':'log-test','events':[
|
||||
{'seq':1,'time':time.time(),'action':'stage_track','arm':'89','task_id':'123456789','message':'收到操作请求'},
|
||||
{'seq':2,'time':time.time(),'action':'stage_track','arm':'89','task_id':'123456789','message':'识别阶段'}]}
|
||||
window._sync_operation_log();window._sync_operation_log()
|
||||
assert window._detail.toPlainText().count('识别阶段')==1
|
||||
assert '#12345678' in window._detail.toPlainText()
|
||||
window._ran({'ok':False,'operation':'对孔测试','message':'执行失败','detail':'测试用内部错误明细','log':'/tmp/isolated-motion.log'})
|
||||
assert '测试用内部错误明细' in window._detail.toPlainText()
|
||||
assert '/tmp/isolated-motion.log' in window._detail.toPlainText()
|
||||
assert not errors,errors
|
||||
assert runtime._runtime is None
|
||||
assert not any(job.isRunning() for job in window._jobs.values())
|
||||
|
||||
Reference in New Issue
Block a user