mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2026-02-07 15:35:10 +00:00
Ready for open source (#47)
* Create app/main API * create example device * create ROS backend and example device SDK Wrapper * Add ROS host and host starting from app.py * Add gripper device and mock implementation * add "status_types" & "action_types" to ROS device decorator * add ActionServer debug example * [bugfix] complete mock gripper example * ROS Backend Host for Device action calling and Resource management * add conda/mamba ENV file * add host_node communication with app/main.py * add action message value mappings and converters * Update ilabos.yaml * Update issue templates * example devices.json and resources.json * Fix Device wrapper to use async property and actions (#7) * Fix Device wrapper to use async property and actions * Resolve #1 : support async get methods and actions. Give new examples. * add both sync/async GRBL controller SDK * 2 call device actions from appmainpy api to ros hostpy (#8) * feature: add job * fix:node start * feature:add get job status * fix:get device * clean * Resolve #5 device connection diagram and workflow compilation support (#9) * add syringe pump device and its compilation using device connection diagram * add RunzeSyringePump real device with ROS2 example * Prototype machine with 1 pump and 1 CNC * add ROS2ProtocolNode and related functions * add ilabos_msgs (to use PumpTransfer action) * add example device connection graph * refactor protocol_node code into separate file * add ROS2SerialNode * add SerialCommand srv in ilabos_msgs * add pump_protocol example, and fix bugs * [fix] serial service: avoid async service deadlock by directly call serial `send_command` * use SendCmd instead of SingleJointPosition for valve control * initialize device connection graph when server starts * Fix #5: async workflow execution (#16) * add rclpyx and protocol example for async-native workflow * use async in ROS2ProtocolNode, and host initialization * add examples of "ros-async" protocol implementation, and `run_in_event_loop` for using native async functions * use "ros-async" in protocols and device nodes * fix pump_protocol: push to 0.0 μL * Envs, docs, and conda recipes (#19) * update ENV: use python 3.11 and deprecate ros-humble-gazebo-ros * add ilabos-msgs conda recipe * Update ilabos.yaml * fix recipe and env yaml * Add sphinx docs * add aichemeco * add bioyong * add bioyong * Support XDL devices & protocols (#20) * [Feature] support multiple protocols in a single ProtocolNode * add Junjie's code * Support "Clean" protocol * Update Grignard_flow_batchreact_single_pumpvalve.json * test_grignard_add * add stir device node and example * Update device_node.py add print_publish flag to control the node's log output * NH4Cl_add * add "HeaterStirrer" device and "HeatChill" action * add wait time after each pump action for equilibration * fix stir * add Separate protocol * Refactor Separator device and Stir action * add rotavap_node * fix stir * add chiller node * Move rinsings into PumpTransfer * Fix SeparateProtocol under refactored Separator device and Stir action * Supports automatically add new protocol action_types * fix PumpTransfer protocol because of rinsing * Add Rotavap and Chiller devices * fix SeparateProtocol * add EvaporateProtocol * add rotavap devices config * fix HeaterStirrer and SeparatorController IO * Fix automatically add new protocol action_types * Add HeaterStirrer and SeparatorController device config * fix pump protocols * Fix Evaporate action * Update evaporate_protocol.py * add temp_sensor node and add function remap * update docs --------- Co-authored-by: 王俊杰 <1800011822@pku.edu.cn> Co-authored-by: q434343 <554662886@qq.com> * fix aichemeco * [Bugfix] fix Windows conda packaging * add file upload api * update dependencies: force to use 3.11 and remove conflict in WIN64 and OSX64 * update dependencies: force to use 3.11 and remove conflict in WIN64 and OSX64 * Create aichemeco_simple.py * fix * update * add aichemeco file * MQTT [1/2]: action start (#25) * add mq * fix * clean * add class * fix excel * update bioyong * add api * fix --------- Co-authored-by: caok@dp.tech <xiaoyeqiannian@163.com> * motor & grasp * Add Grasp motor support and enhance EleGripper class - Introduced a new motor configuration for Grasp in sjtu.json. - Updated EleGripper class to inherit from UniversalDriver and added status property. - Implemented move_and_rotate method for coordinated movement. - Adjusted threading logic in EleGripper initialization. - Registered Grasp motor in ROS2 device node configuration. This commit enhances the motor control capabilities by integrating the Grasp motor and improving the existing EleGripper functionality. * fix read data lenth * Update Grasp.py * MQTT (2/2): publish Device Status, Action Feedback & Results (#27) * Add bridges in HostNode for device_status publishing * Add "bridges" selection (fastapi & mqtt) when app start * add MQ feedback & result publisher, and fix message converter * fix UUID converting between ROS and MQ * lint api model.py * Continuous controllers: PID, MPC, custom controllers etc. (#23) * add controller config & wrapper * add controller setup at app.main * control loop example * fix com port * add agv , ur_arm and raman * MQTT (3/4): Unified Resources and Sync when starting the server (#28) * update http upload api * generate uuid when init device * example resource json * fix * add new example full-content json (device, resource, graph) * fix full-content json and related reading code * fix * add json_schema when initialize resources * fix * update schema * refactor heaterstirrer.dalong * fix * fix refactor heaterstirrer.dalong * refactor syringepump.runze: use ml instead of μL * Update ilabos/ros/host.py Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --------- Co-authored-by: 王俊杰 <1800011822@pku.edu.cn> Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Distributed initialization with self-organizing network (#29) * add distributed launching option "--without_host" * fix --------- Co-authored-by: 王俊杰 <1800011822@pku.edu.cn> * Refactor Workstation: Add resource service and tracking (#30) * move ilabos/ros/rpc to ilabos/device_comms/rpc, and merge bioyond/aichemeco files under /devices * add Resource srv and message_converter * move graphio to ilabos/resources * refactor resources type conversion * add resource clients in device_node * add mock resources service * pass Gripper1 resource test * update http resource services * add AGV compile function * add AGV transfer protocol * update full mock_gripper edit_id example * update full mock_gripper edit_id example * get and update resource also in protocol_node * mock resource update in AichemecoHiwo * Create HT_hiwo.json * add children in resources * bugfixes * fix rpc * add Revvity winprep --------- Co-authored-by: wjjxxx <43375851+wjjxxx@users.noreply.github.com> Co-authored-by: 3218923350 <105201755+3218923350@users.noreply.github.com> * Distributed launch (2/2): distributed resource create (#32) * add resource_add request to host for slave mode * add AGV * fix protocol resources * optimize host callbacks * bugfixes * add revvity registry --------- Co-authored-by: 王俊杰 <1800011822@pku.edu.cn> Co-authored-by: wjjxxx <43375851+wjjxxx@users.noreply.github.com> * Refactor Driver Files Structure (#33) * Integration with pywinauto & recorder Added execute run and initialize procdure * 酶标仪状态检测、使用示例,整体流程待测试 * nivo ready version * Add HPLC driver and example script - Introduced HPLCDriver class for managing HPLC device status and operations. - Implemented device status monitoring and command execution via ROS2 actions. - Added example script (hplc.py) demonstrating how to run commands on the HPLC device. - Created PlayerUtil and UniversalDriver classes for shared functionality across devices. - Refactored NivoDriver to utilize the new UniversalDriver structure. - Enhanced error handling and process management in the NivoDriver. * 修复start的错误定位 * hplc tested ok * relative path to build msgs * template_driver & jiageng devices * fetch correct status type and action type * fix mtype fetch * gpc bus integration * ilab build * remove chs * recipe rename * modbus update 1 * json available * hplc & modbus rewrite * Update AgilentHPLC.py hplc datafile reader * move ilabos/ros/rpc to ilabos/device_comms/rpc, and merge bioyond/aichemeco files under /devices * modbus分设备 * gpc * gpc 2 * fix address * default register node * fix MainScreenGPC * add Resource srv and message_converter * move graphio to ilabos/resources * refactor resources type conversion * add resource clients in device_node * add mock resources service * pass Gripper1 resource test * update http resource services * add AGV compile function * add AGV transfer protocol * update recipe.yaml * update full mock_gripper edit_id example * update full mock_gripper edit_id example * get and update resource also in protocol_node * mock resource update in AichemecoHiwo * feat: add other jiageng PLC device code * ilabos compile * correct format * correct recipe format * correct setup.py format * remove unnecessary files * remove unnecessary files * Create HT_hiwo.json * add children in resources * hplc support sample_id * correct hplc sample_id * correct hplc sample_id * hplc upload * fix type hint * oss upload tested ver * recipe yaml fix for linux * update installation yaml * refactor: moved all driver files according to its feat * merge main to dev --------- Co-authored-by: 王俊杰 <2201110460@stu.pku.edu.cn> Co-authored-by: Junhan Chang <changjh@pku.edu.cn> Co-authored-by: jiawei <miaojiawei@dp.tech> * add: NMR LH and RU device control (#34) * Add Registry for device drivers and Support GraphML (#35) * read chemputer graphml * read graphml in app/main * add devices in ros/devices * add schema for devices * read registry directory and initialize when entry from main * Delete devices.py * Update add_protocol.md * delete unecessary files * feat: 2278 devices registry yaml (#36) * read chemputer graphml * read graphml in app/main * add devices in ros/devices * add schema for devices * read registry directory and initialize when entry from main * Delete devices.py * add: NMR LH and RU device control * fix: modify jiageng devices registry --------- Co-authored-by: Junhan Chang <changjh@pku.edu.cn> * Device/Resource Registry and GraphML support (#37) * add resource type conversion to PLR * add resource registry and test * add docs * fix registry * add solenoid_valve_mock, its registry and test * fix registry for directly using examples * add EvacuateAndRefillProtocol and testcases * allow function sequence call in ACTION * add read & write & extra_info for hardware_interface * Update device_node.py * add solenoid valve * add doc developer guide yaml * fixes for starting IK station * add graphml grouping parser * fix graphml grouping parser * add communication edge parser * fix io solenoid valve * Update .gitignore * Update plates.yaml --------- Co-authored-by: ColumbiaCC <2100011801@stu.pku.edu.cn> * Uni-Lab Doc v0.2 (#39) * add Uni-Lab docs * change doc name * Dev (#41) * Integration with pywinauto & recorder Added execute run and initialize procdure * 酶标仪状态检测、使用示例,整体流程待测试 * nivo ready version * Add HPLC driver and example script - Introduced HPLCDriver class for managing HPLC device status and operations. - Implemented device status monitoring and command execution via ROS2 actions. - Added example script (hplc.py) demonstrating how to run commands on the HPLC device. - Created PlayerUtil and UniversalDriver classes for shared functionality across devices. - Refactored NivoDriver to utilize the new UniversalDriver structure. - Enhanced error handling and process management in the NivoDriver. * 修复start的错误定位 * hplc tested ok * relative path to build msgs * template_driver & jiageng devices * fetch correct status type and action type * fix mtype fetch * gpc bus integration * ilab build * remove chs * recipe rename * modbus update 1 * json available * hplc & modbus rewrite * Update AgilentHPLC.py hplc datafile reader * move ilabos/ros/rpc to ilabos/device_comms/rpc, and merge bioyond/aichemeco files under /devices * modbus分设备 * gpc * gpc 2 * fix address * default register node * fix MainScreenGPC * add Resource srv and message_converter * move graphio to ilabos/resources * refactor resources type conversion * add resource clients in device_node * add mock resources service * pass Gripper1 resource test * update http resource services * add AGV compile function * add AGV transfer protocol * update recipe.yaml * update full mock_gripper edit_id example * update full mock_gripper edit_id example * get and update resource also in protocol_node * mock resource update in AichemecoHiwo * feat: add other jiageng PLC device code * ilabos compile * correct format * correct recipe format * correct setup.py format * remove unnecessary files * remove unnecessary files * Create HT_hiwo.json * add children in resources * hplc support sample_id * correct hplc sample_id * correct hplc sample_id * hplc upload * fix type hint * oss upload tested ver * recipe yaml fix for linux * update installation yaml * refactor: moved all driver files according to its feat * merge main to dev * add HPLC registry and json * 升级 ros2-distro-mutex 依赖版本至 0.6 * 修改 ros2-distro-mutex 依赖版本为通配符匹配 * 更新 ros-humble-ilabos-msgs 依赖为 robostack-humble 命名空间 * add resource type conversion to PLR * add resource registry and test * feat: 更新oss上传 * fix device id * add docs * fix registry * add solenoid_valve_mock, its registry and test * fix registry for directly using examples * add EvacuateAndRefillProtocol and testcases * allow function sequence call in ACTION * add read & write & extra_info for hardware_interface * Update device_node.py * add solenoid valve * add doc developer guide yaml * use robostack-staging * rclpy version test * lower rclpy * ensure 0.6* env * fixes for starting IK station * add graphml grouping parser * fix graphml grouping parser * add communication edge parser * fix io solenoid valve * Update .gitignore * Update plates.yaml * Feature/device node later init (#40) * 修改config路径,方便后续打包 增加device_node打印 * 支持plr序列化/init创建 * 统一命名 * import mgr logger optimize banner print * 日志OK * fix unicorn frame * banner print * correct import format * file path changes * 取消后补全,在加载设备的时候直接替换 * converter update * web page update * 在线device更新,node继承替换 * 修复动作、状态的类型缺失 和 命令提示 * web功能实现结束 * host节点更改完成 新增status时间戳管理 新增每10s动态发现其他node * ros2类型的节点也应该被包一次 * 修复类型提示 * websocket 动态显示状态 * add workflow & book theme for docs * add workflow & book theme for docs * fix workflow build * fix workflow build * 理清启动关系 * stm32 example * mac . name * device_instance device_cls * 新增config添加方式 更新mqtt提示 * plr test * 移动is_host_mode 新增slave_no_host * 确保config优先修改生效 * fix graph io * 支持带参数传入 * 支持物料解析 * 支持物料解析 * device为空的时候不进行绑定或初始化 * protocol node new * protocol node runnable * protocol node runnable --------- Co-authored-by: 王俊杰 <2201110460@stu.pku.edu.cn> Co-authored-by: Junhan Chang <changjh@pku.edu.cn> Co-authored-by: jiawei <miaojiawei@dp.tech> Co-authored-by: ColumbiaCC <2100011801@stu.pku.edu.cn> * Dev (#45) * Integration with pywinauto & recorder Added execute run and initialize procdure * 酶标仪状态检测、使用示例,整体流程待测试 * nivo ready version * Add HPLC driver and example script - Introduced HPLCDriver class for managing HPLC device status and operations. - Implemented device status monitoring and command execution via ROS2 actions. - Added example script (hplc.py) demonstrating how to run commands on the HPLC device. - Created PlayerUtil and UniversalDriver classes for shared functionality across devices. - Refactored NivoDriver to utilize the new UniversalDriver structure. - Enhanced error handling and process management in the NivoDriver. * 修复start的错误定位 * hplc tested ok * relative path to build msgs * template_driver & jiageng devices * fetch correct status type and action type * fix mtype fetch * gpc bus integration * ilab build * remove chs * recipe rename * modbus update 1 * json available * hplc & modbus rewrite * Update AgilentHPLC.py hplc datafile reader * move ilabos/ros/rpc to ilabos/device_comms/rpc, and merge bioyond/aichemeco files under /devices * modbus分设备 * gpc * gpc 2 * fix address * default register node * fix MainScreenGPC * add Resource srv and message_converter * move graphio to ilabos/resources * refactor resources type conversion * add resource clients in device_node * add mock resources service * pass Gripper1 resource test * update http resource services * add AGV compile function * add AGV transfer protocol * update recipe.yaml * update full mock_gripper edit_id example * update full mock_gripper edit_id example * get and update resource also in protocol_node * mock resource update in AichemecoHiwo * feat: add other jiageng PLC device code * ilabos compile * correct format * correct recipe format * correct setup.py format * remove unnecessary files * remove unnecessary files * Create HT_hiwo.json * add children in resources * hplc support sample_id * correct hplc sample_id * correct hplc sample_id * hplc upload * fix type hint * oss upload tested ver * recipe yaml fix for linux * update installation yaml * refactor: moved all driver files according to its feat * merge main to dev * add HPLC registry and json * 升级 ros2-distro-mutex 依赖版本至 0.6 * 修改 ros2-distro-mutex 依赖版本为通配符匹配 * 更新 ros-humble-ilabos-msgs 依赖为 robostack-humble 命名空间 * add resource type conversion to PLR * add resource registry and test * feat: 更新oss上传 * fix device id * add docs * fix registry * add solenoid_valve_mock, its registry and test * fix registry for directly using examples * add EvacuateAndRefillProtocol and testcases * allow function sequence call in ACTION * add read & write & extra_info for hardware_interface * Update device_node.py * add solenoid valve * add doc developer guide yaml * use robostack-staging * rclpy version test * lower rclpy * ensure 0.6* env * fixes for starting IK station * add graphml grouping parser * fix graphml grouping parser * add communication edge parser * fix io solenoid valve * Update .gitignore * Update plates.yaml * Feature/device node later init (#40) * 修改config路径,方便后续打包 增加device_node打印 * 支持plr序列化/init创建 * 统一命名 * import mgr logger optimize banner print * 日志OK * fix unicorn frame * banner print * correct import format * file path changes * 取消后补全,在加载设备的时候直接替换 * converter update * web page update * 在线device更新,node继承替换 * 修复动作、状态的类型缺失 和 命令提示 * web功能实现结束 * host节点更改完成 新增status时间戳管理 新增每10s动态发现其他node * ros2类型的节点也应该被包一次 * 修复类型提示 * websocket 动态显示状态 * add workflow & book theme for docs * add workflow & book theme for docs * fix workflow build * fix workflow build * 理清启动关系 * stm32 example * mac . name * device_instance device_cls * 新增config添加方式 更新mqtt提示 * plr test * 移动is_host_mode 新增slave_no_host * 确保config优先修改生效 * fix graph io * 支持带参数传入 * 支持物料解析 * 支持物料解析 * device为空的时候不进行绑定或初始化 * protocol node new * protocol node runnable * protocol node runnable * Feature/device node later init (#42) * 修改config路径,方便后续打包 增加device_node打印 * 支持plr序列化/init创建 * 统一命名 * import mgr logger optimize banner print * 日志OK * fix unicorn frame * banner print * correct import format * file path changes * 取消后补全,在加载设备的时候直接替换 * converter update * web page update * 在线device更新,node继承替换 * 修复动作、状态的类型缺失 和 命令提示 * web功能实现结束 * host节点更改完成 新增status时间戳管理 新增每10s动态发现其他node * ros2类型的节点也应该被包一次 * 修复类型提示 * websocket 动态显示状态 * add workflow & book theme for docs * add workflow & book theme for docs * fix workflow build * fix workflow build * 理清启动关系 * stm32 example * mac . name * device_instance device_cls * 新增config添加方式 更新mqtt提示 * plr test * 移动is_host_mode 新增slave_no_host * 确保config优先修改生效 * fix graph io * 支持带参数传入 * 支持物料解析 * 支持物料解析 * device为空的时候不进行绑定或初始化 * protocol node new * protocol node runnable * protocol node runnable * action * plr suc * plr suc!! * plr suc!! * plr suc!! * plr msgs * Feature/device node later init (#43) * 修改config路径,方便后续打包 增加device_node打印 * 支持plr序列化/init创建 * 统一命名 * import mgr logger optimize banner print * 日志OK * fix unicorn frame * banner print * correct import format * file path changes * 取消后补全,在加载设备的时候直接替换 * converter update * web page update * 在线device更新,node继承替换 * 修复动作、状态的类型缺失 和 命令提示 * web功能实现结束 * host节点更改完成 新增status时间戳管理 新增每10s动态发现其他node * ros2类型的节点也应该被包一次 * 修复类型提示 * websocket 动态显示状态 * add workflow & book theme for docs * add workflow & book theme for docs * fix workflow build * fix workflow build * 理清启动关系 * stm32 example * mac . name * device_instance device_cls * 新增config添加方式 更新mqtt提示 * plr test * 移动is_host_mode 新增slave_no_host * 确保config优先修改生效 * fix graph io * 支持带参数传入 * 支持物料解析 * 支持物料解析 * device为空的时候不进行绑定或初始化 * protocol node new * protocol node runnable * protocol node runnable * action * plr suc * plr suc!! * plr suc!! * plr suc!! * plr msgs * plr * action * plr reg fix * Feature/device node later init (#44) * 修改config路径,方便后续打包 增加device_node打印 * 支持plr序列化/init创建 * 统一命名 * import mgr logger optimize banner print * 日志OK * fix unicorn frame * banner print * correct import format * file path changes * 取消后补全,在加载设备的时候直接替换 * converter update * web page update * 在线device更新,node继承替换 * 修复动作、状态的类型缺失 和 命令提示 * web功能实现结束 * host节点更改完成 新增status时间戳管理 新增每10s动态发现其他node * ros2类型的节点也应该被包一次 * 修复类型提示 * websocket 动态显示状态 * add workflow & book theme for docs * add workflow & book theme for docs * fix workflow build * fix workflow build * 理清启动关系 * stm32 example * mac . name * device_instance device_cls * 新增config添加方式 更新mqtt提示 * plr test * 移动is_host_mode 新增slave_no_host * 确保config优先修改生效 * fix graph io * 支持带参数传入 * 支持物料解析 * 支持物料解析 * device为空的时候不进行绑定或初始化 * protocol node new * protocol node runnable * protocol node runnable * action * plr suc * plr suc!! * plr suc!! * plr suc!! * plr msgs * plr * fix convert error fix async logic error added async error print * new device test * test resource add * test resource add * test resource add * test resource add * local env setup * node type fix temp fix root_node error fix convert res from type error * resource tracker * fix bug from qhh * fix bug from qhh * fix bug from qhh * fix bug from qhh * refactor MQTT client logging and connection handling; update group ID in config * driver_params allow empty * allow other init param * fix driver param and enhance type hint * refactor MQConfig to use double quotes for string literals * fix wrong function calling * fix wrong function calling * fix log for mac * fix networkx compatibility * add mqtt loggers * add action to jsonschema converter * random client id * type converter & registry * correct conversion * fix action publish only from discovered devices * add "Bio" tag for action doc generation * 改进module提示 * Fix doc * mqtt不连接也可用 性价样例提示 * add docs * 更新plr test案例 * Update intro.md * 更新有机案例 * skip --------- Co-authored-by: Harvey Que <Q-Query@outlook.com> Co-authored-by: Junhan Chang <1700011741@pku.edu.cn> --------- Co-authored-by: 王俊杰 <2201110460@stu.pku.edu.cn> Co-authored-by: Junhan Chang <changjh@pku.edu.cn> Co-authored-by: jiawei <miaojiawei@dp.tech> Co-authored-by: ColumbiaCC <2100011801@stu.pku.edu.cn> Co-authored-by: Harvey Que <Q-Query@outlook.com> Co-authored-by: Junhan Chang <1700011741@pku.edu.cn> * Canonicalize before Open Source (#46) * big big refactor try01 * refactor 02 --------- Co-authored-by: ck <xiaoyeqiannian@163.com> Co-authored-by: 王俊杰 <1800011822@pku.edu.cn> Co-authored-by: q434343 <554662886@qq.com> Co-authored-by: Xuwznln <xuwznln@gmail.com> Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Co-authored-by: wjjxxx <43375851+wjjxxx@users.noreply.github.com> Co-authored-by: 3218923350 <105201755+3218923350@users.noreply.github.com> Co-authored-by: Xuwznln <1023025701@qq.com> Co-authored-by: 王俊杰 <2201110460@stu.pku.edu.cn> Co-authored-by: jiawei <miaojiawei@dp.tech> Co-authored-by: Jiawei <91898272+jiawei723@users.noreply.github.com> Co-authored-by: ColumbiaCC <2100011801@stu.pku.edu.cn> Co-authored-by: Harvey Que <Q-Query@outlook.com>
This commit is contained in:
0
unilabos/app/__init__.py
Normal file
0
unilabos/app/__init__.py
Normal file
35
unilabos/app/backend.py
Normal file
35
unilabos/app/backend.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import threading
|
||||
|
||||
from unilabos.utils import logger
|
||||
|
||||
|
||||
# 根据选择的 backend 启动相应的功能
|
||||
def start_backend(
|
||||
backend: str,
|
||||
devices_config: dict = {},
|
||||
resources_config: dict = {},
|
||||
graph=None,
|
||||
controllers_config: dict = {},
|
||||
bridges=[],
|
||||
without_host: bool = False,
|
||||
**kwargs
|
||||
):
|
||||
if backend == "ros":
|
||||
# 假设 ros_main, simple_main, automancer_main 是不同 backend 的启动函数
|
||||
from unilabos.ros.main_slave_run import main, slave # 如果选择 'ros' 作为 backend
|
||||
elif backend == 'simple':
|
||||
# 这里假设 simple_backend 和 automancer_backend 是你定义的其他两个后端
|
||||
# from simple_backend import main as simple_main
|
||||
pass
|
||||
elif backend == 'automancer':
|
||||
# from automancer_backend import main as automancer_main
|
||||
pass
|
||||
else:
|
||||
raise ValueError(f"Unsupported backend: {backend}")
|
||||
|
||||
backend_thread = threading.Thread(
|
||||
target=main if not without_host else slave,
|
||||
args=(devices_config, resources_config, graph, controllers_config, bridges)
|
||||
)
|
||||
backend_thread.start()
|
||||
logger.info(f"Backend {backend} started.")
|
||||
34
unilabos/app/controler.py
Normal file
34
unilabos/app/controler.py
Normal file
@@ -0,0 +1,34 @@
|
||||
|
||||
import json
|
||||
import uuid
|
||||
from unilabos.app.model import JobAddReq, JobData
|
||||
from unilabos.ros.nodes.presets.host_node import HostNode
|
||||
|
||||
|
||||
def get_resources() -> tuple:
|
||||
if HostNode.get_instance() is None:
|
||||
return False, "Host node not initialized"
|
||||
|
||||
return True, HostNode.get_instance().resources_config
|
||||
|
||||
def devices() -> tuple:
|
||||
if HostNode.get_instance() is None:
|
||||
return False, "Host node not initialized"
|
||||
|
||||
return True, HostNode.get_instance().devices_config
|
||||
|
||||
def job_info(id: str):
|
||||
get_goal_status = HostNode.get_instance().get_goal_status(id)
|
||||
return JobData(jobId=id, status=get_goal_status)
|
||||
|
||||
def job_add(req: JobAddReq) -> JobData:
|
||||
if req.job_id is None:
|
||||
req.job_id = str(uuid.uuid4())
|
||||
action_name = req.data["action"]
|
||||
action_kwargs = req.data["action_kwargs"]
|
||||
req.data['action'] = action_name
|
||||
if action_name == "execute_command_from_outer":
|
||||
action_kwargs = {"command": json.dumps(action_kwargs)}
|
||||
print(f"job_add:{req.device_id} {action_name} {action_kwargs}")
|
||||
HostNode.get_instance().send_goal(req.device_id, action_name=action_name, action_kwargs=action_kwargs, goal_uuid=req.job_id)
|
||||
return JobData(jobId=req.job_id)
|
||||
155
unilabos/app/main.py
Normal file
155
unilabos/app/main.py
Normal file
@@ -0,0 +1,155 @@
|
||||
import argparse
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import json
|
||||
import yaml
|
||||
from copy import deepcopy
|
||||
|
||||
# 首先添加项目根目录到路径
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
ilabos_dir = os.path.dirname(os.path.dirname(current_dir))
|
||||
if ilabos_dir not in sys.path:
|
||||
sys.path.append(ilabos_dir)
|
||||
|
||||
from unilabos.config.config import load_config, BasicConfig
|
||||
from unilabos.utils.banner_print import print_status, print_unilab_banner
|
||||
|
||||
|
||||
def parse_args():
|
||||
"""解析命令行参数"""
|
||||
parser = argparse.ArgumentParser(description="Start Uni-Lab Edge server.")
|
||||
parser.add_argument("-g", "--graph", help="Physical setup graph.")
|
||||
parser.add_argument("-d", "--devices", help="Devices config file.")
|
||||
parser.add_argument("-r", "--resources", help="Resources config file.")
|
||||
parser.add_argument("-c", "--controllers", default=None, help="Controllers config file.")
|
||||
parser.add_argument(
|
||||
"--registry_path",
|
||||
type=str,
|
||||
default=None,
|
||||
action="append",
|
||||
help="Path to the registry",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--backend",
|
||||
choices=["ros", "simple", "automancer"],
|
||||
default="ros",
|
||||
help="Choose the backend to run with: 'ros', 'simple', or 'automancer'.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--app_bridges",
|
||||
nargs="+",
|
||||
default=["mqtt", "fastapi"],
|
||||
help="Bridges to connect to. Now support 'mqtt' and 'fastapi'.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--without_host",
|
||||
action="store_true",
|
||||
help="Run the backend as slave (without host).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--slave_no_host",
|
||||
action="store_true",
|
||||
help="Slave模式下跳过等待host服务",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--config",
|
||||
type=str,
|
||||
default=None,
|
||||
help="配置文件路径,支持.py格式的Python配置文件",
|
||||
)
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
# 解析命令行参数
|
||||
args = parse_args()
|
||||
args_dict = vars(args)
|
||||
|
||||
# 加载配置文件 - 这里保持最先加载配置的逻辑
|
||||
if args_dict.get("config"):
|
||||
config_path = args_dict["config"]
|
||||
if not os.path.exists(config_path):
|
||||
print_status(f"配置文件 {config_path} 不存在", "error")
|
||||
elif not config_path.endswith(".py"):
|
||||
print_status(f"配置文件 {config_path} 不是Python文件,必须以.py结尾", "error")
|
||||
else:
|
||||
load_config(config_path)
|
||||
|
||||
# 设置BasicConfig参数
|
||||
BasicConfig.is_host_mode = not args_dict.get("without_host", False)
|
||||
BasicConfig.slave_no_host = args_dict.get("slave_no_host", False)
|
||||
|
||||
from unilabos.resources.graphio import (
|
||||
read_node_link_json,
|
||||
read_graphml,
|
||||
dict_from_graph,
|
||||
dict_to_nested_dict,
|
||||
initialize_resources,
|
||||
)
|
||||
from unilabos.app.mq import mqtt_client
|
||||
from unilabos.registry.registry import build_registry
|
||||
from unilabos.app.backend import start_backend
|
||||
from unilabos.web import http_client
|
||||
from unilabos.web import start_server
|
||||
|
||||
# 显示启动横幅
|
||||
print_unilab_banner(args_dict)
|
||||
|
||||
# 注册表
|
||||
build_registry(args_dict["registry_path"])
|
||||
|
||||
if args_dict["graph"] is not None:
|
||||
import unilabos.resources.graphio as graph_res
|
||||
graph_res.physical_setup_graph = (
|
||||
read_node_link_json(args_dict["graph"])
|
||||
if args_dict["graph"].endswith(".json")
|
||||
else read_graphml(args_dict["graph"])
|
||||
)
|
||||
devices_and_resources = dict_from_graph(graph_res.physical_setup_graph)
|
||||
args_dict["resources_config"] = initialize_resources(list(deepcopy(devices_and_resources).values()))
|
||||
args_dict["devices_config"] = dict_to_nested_dict(deepcopy(devices_and_resources), devices_only=False)
|
||||
# args_dict["resources_config"] = dict_to_tree(devices_and_resources, devices_only=False)
|
||||
args_dict["graph"] = graph_res.physical_setup_graph
|
||||
else:
|
||||
if args_dict["devices"] is None or args_dict["resources"] is None:
|
||||
print_status("Either graph or devices and resources must be provided.", "error")
|
||||
sys.exit(1)
|
||||
args_dict["devices_config"] = json.load(open(args_dict["devices"], encoding="utf-8"))
|
||||
args_dict["resources_config"] = initialize_resources(
|
||||
list(json.load(open(args_dict["resources"], encoding="utf-8")).values())
|
||||
)
|
||||
|
||||
print_status(f"{len(args_dict['resources_config'])} Resources loaded:", "info")
|
||||
for i in args_dict["resources_config"]:
|
||||
print_status(f"DeviceId: {i['id']}, Class: {i['class']}", "info")
|
||||
|
||||
if args_dict["controllers"] is not None:
|
||||
args_dict["controllers_config"] = yaml.safe_load(open(args_dict["controllers"], encoding="utf-8"))
|
||||
else:
|
||||
args_dict["controllers_config"] = None
|
||||
|
||||
args_dict["bridges"] = []
|
||||
|
||||
if "mqtt" in args_dict["app_bridges"]:
|
||||
args_dict["bridges"].append(mqtt_client)
|
||||
if "fastapi" in args_dict["app_bridges"]:
|
||||
args_dict["bridges"].append(http_client)
|
||||
if "mqtt" in args_dict["app_bridges"]:
|
||||
|
||||
def _exit(signum, frame):
|
||||
mqtt_client.stop()
|
||||
sys.exit(0)
|
||||
|
||||
signal.signal(signal.SIGINT, _exit)
|
||||
signal.signal(signal.SIGTERM, _exit)
|
||||
mqtt_client.start()
|
||||
|
||||
start_backend(**args_dict)
|
||||
start_server()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
137
unilabos/app/model.py
Normal file
137
unilabos/app/model.py
Normal file
@@ -0,0 +1,137 @@
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class RespCode:
|
||||
Success = 0
|
||||
|
||||
ErrorHostNotInit = 2001 # Host node not initialized
|
||||
ErrorInvalidReq = 2002 # Invalid request data
|
||||
|
||||
|
||||
class DeviceAction(BaseModel):
|
||||
x: str
|
||||
y: str
|
||||
action: str
|
||||
|
||||
|
||||
class Device(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
action: DeviceAction
|
||||
|
||||
|
||||
class DeviceList(BaseModel):
|
||||
items: list[Device] = []
|
||||
page: int
|
||||
pageSize: int
|
||||
|
||||
|
||||
class DevicesResponse(BaseModel):
|
||||
code: int
|
||||
data: DeviceList
|
||||
|
||||
|
||||
class DeviceInfoResponse(BaseModel):
|
||||
code: int
|
||||
data: Device
|
||||
|
||||
|
||||
class PageResp(BaseModel):
|
||||
item: list = []
|
||||
page: int = 1
|
||||
pageSize: int = 10
|
||||
|
||||
|
||||
class Resp(BaseModel):
|
||||
code: int = RespCode.Success
|
||||
data: dict = {}
|
||||
message: str = "success"
|
||||
|
||||
|
||||
class JobAddReq(BaseModel):
|
||||
device_id: str = Field(examples=["Gripper"], description="device id")
|
||||
data: dict = Field(examples=[{"position": 30, "torque": 5, "action": "push_to"}])
|
||||
job_id: str = Field(examples=["sfsfsfeq"], description="goal uuid")
|
||||
node_id: str = Field(examples=["sfsfsfeq"], description="node uuid")
|
||||
|
||||
|
||||
class JobStepFinishReq(BaseModel):
|
||||
token: str = Field(examples=["030944"], description="token")
|
||||
request_time: str = Field(
|
||||
examples=["2024-12-12 12:12:12.xxx"], description="requestTime"
|
||||
)
|
||||
data: dict = Field(
|
||||
examples=[
|
||||
{
|
||||
"orderCode": "任务号。字符串",
|
||||
"orderName": "任务名称。字符串",
|
||||
"stepName": "步骤名称。字符串",
|
||||
"stepId": "步骤Id。GUID",
|
||||
"sampleId": "通量Id。GUID",
|
||||
"startTime": "开始时间。时间格式",
|
||||
"endTime": "完成时间。时间格式",
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class JobPreintakeFinishReq(BaseModel):
|
||||
token: str = Field(examples=["030944"], description="token")
|
||||
request_time: str = Field(
|
||||
examples=["2024-12-12 12:12:12.xxx"], description="requestTime"
|
||||
)
|
||||
data: dict = Field(
|
||||
examples=[
|
||||
{
|
||||
"orderCode": "任务号。字符串",
|
||||
"orderName": "任务名称。字符串",
|
||||
"sampleId": "通量Id。GUID",
|
||||
"startTime": "开始时间。时间格式",
|
||||
"endTime": "完成时间。时间格式",
|
||||
"Status": "通量状态,0待生产、2进样、10开始、完成20、异常停止-2、人工停止或取消-3",
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class JobFinishReq(BaseModel):
|
||||
token: str = Field(examples=["030944"], description="token")
|
||||
request_time: str = Field(
|
||||
examples=["2024-12-12 12:12:12.xxx"], description="requestTime"
|
||||
)
|
||||
data: dict = Field(
|
||||
examples=[
|
||||
{
|
||||
"orderCode": "任务号。字符串",
|
||||
"orderName": "任务名称。字符串",
|
||||
"startTime": "开始时间。时间格式",
|
||||
"endTime": "完成时间。时间格式",
|
||||
"status": "通量状态,完成30、异常停止-11、人工停止或取消-12",
|
||||
"usedMaterials": [
|
||||
{
|
||||
"materialId": "物料Id。GUID",
|
||||
"locationId": "库位Id。GUID",
|
||||
"typeMode": "物料类型。 样品1、试剂2、耗材0",
|
||||
"usedQuantity": "使用的数量。 数字",
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class JobData(BaseModel):
|
||||
jobId: str = Field(examples=["sfsfsfeq"], description="goal uuid")
|
||||
status: int = Field(
|
||||
examples=[0, 1],
|
||||
default=0,
|
||||
description="0:UNKNOWN, 1:ACCEPTED, 2:EXECUTING, 3:CANCELING, 4:SUCCEEDED, 5:CANCELED, 6:ABORTED",
|
||||
)
|
||||
|
||||
|
||||
class JobStatusResp(Resp):
|
||||
data: JobData
|
||||
|
||||
|
||||
class JobAddResp(Resp):
|
||||
data: JobData
|
||||
177
unilabos/app/mq.py
Normal file
177
unilabos/app/mq.py
Normal file
@@ -0,0 +1,177 @@
|
||||
import json
|
||||
import time
|
||||
import uuid
|
||||
|
||||
import paho.mqtt.client as mqtt
|
||||
import ssl, base64, hmac
|
||||
from hashlib import sha1
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
from unilabos.config.config import MQConfig
|
||||
from unilabos.app.controler import devices, job_add
|
||||
from unilabos.app.model import JobAddReq, JobAddResp
|
||||
from unilabos.utils import logger
|
||||
from unilabos.utils.type_check import TypeEncoder
|
||||
|
||||
|
||||
class MQTTClient:
|
||||
mqtt_disable = True
|
||||
|
||||
def __init__(self):
|
||||
self.mqtt_disable = not MQConfig.lab_id
|
||||
self.client_id = f"{MQConfig.group_id}@@@{MQConfig.lab_id}{uuid.uuid4()}"
|
||||
self.client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=self.client_id, protocol=mqtt.MQTTv5)
|
||||
self._setup_callbacks()
|
||||
|
||||
def _setup_callbacks(self):
|
||||
self.client.on_log = self._on_log
|
||||
self.client.on_connect = self._on_connect
|
||||
self.client.on_message = self._on_message
|
||||
self.client.on_disconnect = self._on_disconnect
|
||||
|
||||
def _on_log(self, client, userdata, level, buf):
|
||||
logger.info(f"[MQTT] log: {buf}")
|
||||
|
||||
def _on_connect(self, client, userdata, flags, rc, properties=None):
|
||||
logger.info("[MQTT] Connected with result code " + str(rc))
|
||||
client.subscribe(f"labs/{MQConfig.lab_id}/job/start/", 0)
|
||||
isok, data = devices()
|
||||
if not isok:
|
||||
logger.error("[MQTT] on_connect ErrorHostNotInit")
|
||||
return
|
||||
|
||||
def _on_message(self, client, userdata, msg):
|
||||
logger.info("[MQTT] on_message<<<< " + msg.topic + " " + str(msg.payload))
|
||||
try:
|
||||
payload_str = msg.payload.decode("utf-8")
|
||||
payload_json = json.loads(payload_str)
|
||||
logger.debug(f"Topic: {msg.topic}")
|
||||
logger.debug("Payload:", json.dumps(payload_json, indent=2, ensure_ascii=False))
|
||||
if msg.topic == f"labs/{MQConfig.lab_id}/job/start/":
|
||||
logger.debug("job_add", type(payload_json), payload_json)
|
||||
job_req = JobAddReq.model_validate(payload_json)
|
||||
data = job_add(job_req)
|
||||
return JobAddResp(data=data)
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"[MQTT] JSON 解析错误: {e}")
|
||||
logger.error(f"[MQTT] Raw message: {msg.payload}")
|
||||
except Exception as e:
|
||||
logger.error(f"[MQTT] 处理消息时出错: {e}")
|
||||
|
||||
def _on_disconnect(self, client, userdata, rc, reasonCode=None, properties=None):
|
||||
if rc != 0:
|
||||
logger.error(f"[MQTT] Unexpected disconnection {rc}")
|
||||
|
||||
def _setup_ssl_context(self):
|
||||
temp_files = []
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(mode="w", delete=False) as ca_temp:
|
||||
ca_temp.write(MQConfig.ca_content)
|
||||
temp_files.append(ca_temp.name)
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode="w", delete=False) as cert_temp:
|
||||
cert_temp.write(MQConfig.cert_content)
|
||||
temp_files.append(cert_temp.name)
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode="w", delete=False) as key_temp:
|
||||
key_temp.write(MQConfig.key_content)
|
||||
temp_files.append(key_temp.name)
|
||||
|
||||
context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
|
||||
context.load_verify_locations(cafile=temp_files[0])
|
||||
context.load_cert_chain(certfile=temp_files[1], keyfile=temp_files[2])
|
||||
self.client.tls_set_context(context)
|
||||
finally:
|
||||
for temp_file in temp_files:
|
||||
try:
|
||||
os.unlink(temp_file)
|
||||
except:
|
||||
pass
|
||||
|
||||
def start(self):
|
||||
if self.mqtt_disable:
|
||||
logger.warning("MQTT is disabled, skipping connection.")
|
||||
return
|
||||
userName = f"Signature|{MQConfig.access_key}|{MQConfig.instance_id}"
|
||||
password = base64.b64encode(
|
||||
hmac.new(MQConfig.secret_key.encode(), self.client_id.encode(), sha1).digest()
|
||||
).decode()
|
||||
|
||||
self.client.username_pw_set(userName, password)
|
||||
self._setup_ssl_context()
|
||||
|
||||
# 创建连接线程
|
||||
def connect_thread_func():
|
||||
try:
|
||||
self.client.connect(MQConfig.broker_url, MQConfig.port, 60)
|
||||
self.client.loop_start()
|
||||
|
||||
# 添加连接超时检测
|
||||
max_attempts = 5
|
||||
attempt = 0
|
||||
while not self.client.is_connected() and attempt < max_attempts:
|
||||
logger.info(
|
||||
f"[MQTT] 正在连接到 {MQConfig.broker_url}:{MQConfig.port},尝试 {attempt+1}/{max_attempts}"
|
||||
)
|
||||
time.sleep(3)
|
||||
attempt += 1
|
||||
|
||||
if self.client.is_connected():
|
||||
logger.info(f"[MQTT] 已成功连接到 {MQConfig.broker_url}:{MQConfig.port}")
|
||||
else:
|
||||
logger.error(f"[MQTT] 连接超时,可能是账号密码错误或网络问题")
|
||||
self.client.loop_stop()
|
||||
except Exception as e:
|
||||
logger.error(f"[MQTT] 连接失败: {str(e)}")
|
||||
|
||||
connect_thread_func()
|
||||
# connect_thread = threading.Thread(target=connect_thread_func)
|
||||
# connect_thread.daemon = True
|
||||
# connect_thread.start()
|
||||
|
||||
def stop(self):
|
||||
if self.mqtt_disable:
|
||||
return
|
||||
self.client.disconnect()
|
||||
self.client.loop_stop()
|
||||
|
||||
def publish_device_status(self, device_status: dict, device_id, property_name):
|
||||
# status = device_status.get(device_id, {})
|
||||
if self.mqtt_disable:
|
||||
return
|
||||
status = {"data": device_status.get(device_id, {}), "device_id": device_id}
|
||||
address = f"labs/{MQConfig.lab_id}/devices"
|
||||
self.client.publish(address, json.dumps(status), qos=2)
|
||||
logger.critical(f"Device status published: address: {address}, {status}")
|
||||
|
||||
def publish_job_status(self, feedback_data: dict, job_id: str, status: str):
|
||||
if self.mqtt_disable:
|
||||
return
|
||||
jobdata = {"job_id": job_id, "data": feedback_data, "status": status}
|
||||
self.client.publish(f"labs/{MQConfig.lab_id}/job/list/", json.dumps(jobdata), qos=2)
|
||||
|
||||
def publish_registry(self, device_id: str, device_info: dict):
|
||||
if self.mqtt_disable:
|
||||
return
|
||||
address = f"labs/{MQConfig.lab_id}/registry/"
|
||||
registry_data = json.dumps({device_id: device_info}, ensure_ascii = False, cls = TypeEncoder)
|
||||
self.client.publish(address, registry_data, qos=2)
|
||||
logger.debug(f"Registry data published: address: {address}, {registry_data}")
|
||||
|
||||
def publish_actions(self, action_id: str, action_info: dict):
|
||||
if self.mqtt_disable:
|
||||
return
|
||||
address = f"labs/{MQConfig.lab_id}/actions/"
|
||||
action_type_name = action_info["title"]
|
||||
action_info["title"] = action_id
|
||||
action_data = json.dumps({action_type_name: action_info}, ensure_ascii=False)
|
||||
self.client.publish(address, action_data, qos=2)
|
||||
logger.debug(f"Action data published: address: {address}, {action_data}")
|
||||
|
||||
|
||||
mqtt_client = MQTTClient()
|
||||
|
||||
if __name__ == "__main__":
|
||||
mqtt_client.start()
|
||||
231
unilabos/app/oss_upload.py
Normal file
231
unilabos/app/oss_upload.py
Normal file
@@ -0,0 +1,231 @@
|
||||
import argparse
|
||||
import os
|
||||
import time
|
||||
from typing import Dict, Optional, Tuple
|
||||
|
||||
import requests
|
||||
|
||||
from unilabos.config.config import OSSUploadConfig
|
||||
|
||||
|
||||
def _init_upload(file_path: str, oss_path: str, filename: Optional[str] = None,
|
||||
process_key: str = "file-upload", device_id: str = "default",
|
||||
expires_hours: int = 1) -> Tuple[bool, Dict]:
|
||||
"""
|
||||
初始化上传过程
|
||||
|
||||
Args:
|
||||
file_path: 本地文件路径
|
||||
oss_path: OSS目标路径
|
||||
filename: 文件名,如果为None则使用file_path的文件名
|
||||
process_key: 处理键
|
||||
device_id: 设备ID
|
||||
expires_hours: 链接过期小时数
|
||||
|
||||
Returns:
|
||||
(成功标志, 响应数据)
|
||||
"""
|
||||
if filename is None:
|
||||
filename = os.path.basename(file_path)
|
||||
|
||||
# 构造初始化请求
|
||||
url = f"{OSSUploadConfig.api_host}{OSSUploadConfig.init_endpoint}"
|
||||
headers = {
|
||||
"Authorization": OSSUploadConfig.authorization,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
payload = {
|
||||
"device_id": device_id,
|
||||
"process_key": process_key,
|
||||
"filename": filename,
|
||||
"path": oss_path,
|
||||
"expires_hours": expires_hours
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
if response.status_code == 201:
|
||||
result = response.json()
|
||||
if result.get("code") == "10000":
|
||||
return True, result.get("data", {})
|
||||
|
||||
print(f"初始化上传失败: {response.status_code}, {response.text}")
|
||||
return False, {}
|
||||
except Exception as e:
|
||||
print(f"初始化上传异常: {str(e)}")
|
||||
return False, {}
|
||||
|
||||
|
||||
def _put_upload(file_path: str, upload_url: str) -> bool:
|
||||
"""
|
||||
执行PUT上传
|
||||
|
||||
Args:
|
||||
file_path: 本地文件路径
|
||||
upload_url: 上传URL
|
||||
|
||||
Returns:
|
||||
是否成功
|
||||
"""
|
||||
try:
|
||||
with open(file_path, "rb") as f:
|
||||
response = requests.put(upload_url, data=f)
|
||||
if response.status_code == 200:
|
||||
return True
|
||||
|
||||
print(f"PUT上传失败: {response.status_code}, {response.text}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"PUT上传异常: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
def _complete_upload(uuid: str) -> bool:
|
||||
"""
|
||||
完成上传过程
|
||||
|
||||
Args:
|
||||
uuid: 上传的UUID
|
||||
|
||||
Returns:
|
||||
是否成功
|
||||
"""
|
||||
url = f"{OSSUploadConfig.api_host}{OSSUploadConfig.complete_endpoint}"
|
||||
headers = {
|
||||
"Authorization": OSSUploadConfig.authorization,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
payload = {
|
||||
"uuid": uuid
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
if result.get("code") == "10000":
|
||||
return True
|
||||
|
||||
print(f"完成上传失败: {response.status_code}, {response.text}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"完成上传异常: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
def oss_upload(file_path: str, oss_path: str, filename: Optional[str] = None,
|
||||
process_key: str = "file-upload", device_id: str = "default") -> bool:
|
||||
"""
|
||||
文件上传主函数,包含重试机制
|
||||
|
||||
Args:
|
||||
file_path: 本地文件路径
|
||||
oss_path: OSS目标路径
|
||||
filename: 文件名,如果为None则使用file_path的文件名
|
||||
process_key: 处理键
|
||||
device_id: 设备ID
|
||||
|
||||
Returns:
|
||||
是否成功上传
|
||||
"""
|
||||
max_retries = OSSUploadConfig.max_retries
|
||||
retry_count = 0
|
||||
|
||||
while retry_count < max_retries:
|
||||
try:
|
||||
# 步骤1:初始化上传
|
||||
init_success, init_data = _init_upload(
|
||||
file_path=file_path,
|
||||
oss_path=oss_path,
|
||||
filename=filename,
|
||||
process_key=process_key,
|
||||
device_id=device_id
|
||||
)
|
||||
|
||||
if not init_success:
|
||||
print(f"初始化上传失败,重试 {retry_count + 1}/{max_retries}")
|
||||
retry_count += 1
|
||||
time.sleep(1) # 等待1秒后重试
|
||||
continue
|
||||
|
||||
# 获取UUID和上传URL
|
||||
uuid = init_data.get("uuid")
|
||||
upload_url = init_data.get("upload_url")
|
||||
|
||||
if not uuid or not upload_url:
|
||||
print(f"初始化上传返回数据不完整,重试 {retry_count + 1}/{max_retries}")
|
||||
retry_count += 1
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
# 步骤2:PUT上传文件
|
||||
put_success = _put_upload(file_path, upload_url)
|
||||
if not put_success:
|
||||
print(f"PUT上传失败,重试 {retry_count + 1}/{max_retries}")
|
||||
retry_count += 1
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
# 步骤3:完成上传
|
||||
complete_success = _complete_upload(uuid)
|
||||
if not complete_success:
|
||||
print(f"完成上传失败,重试 {retry_count + 1}/{max_retries}")
|
||||
retry_count += 1
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
# 所有步骤都成功
|
||||
print(f"文件 {file_path} 上传成功")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"上传过程异常: {str(e)},重试 {retry_count + 1}/{max_retries}")
|
||||
retry_count += 1
|
||||
time.sleep(1)
|
||||
|
||||
print(f"文件 {file_path} 上传失败,已达到最大重试次数 {max_retries}")
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# python -m unilabos.app.oss_upload -f /path/to/your/file.txt
|
||||
# 命令行参数解析
|
||||
parser = argparse.ArgumentParser(description='文件上传测试工具')
|
||||
parser.add_argument('--file', '-f', type=str, required=True, help='要上传的本地文件路径')
|
||||
parser.add_argument('--path', '-p', type=str, default='/HPLC1/Any', help='OSS目标路径')
|
||||
parser.add_argument('--device', '-d', type=str, default='test-device', help='设备ID')
|
||||
parser.add_argument('--process', '-k', type=str, default='HPLC-txt-result', help='处理键')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 检查文件是否存在
|
||||
if not os.path.exists(args.file):
|
||||
print(f"错误:文件 {args.file} 不存在")
|
||||
exit(1)
|
||||
|
||||
print("=" * 50)
|
||||
print(f"开始上传文件: {args.file}")
|
||||
print(f"目标路径: {args.path}")
|
||||
print(f"设备ID: {args.device}")
|
||||
print(f"处理键: {args.process}")
|
||||
print("=" * 50)
|
||||
|
||||
# 执行上传
|
||||
success = oss_upload(
|
||||
file_path=args.file,
|
||||
oss_path=args.path,
|
||||
filename=None, # 使用默认文件名
|
||||
process_key=args.process,
|
||||
device_id=args.device
|
||||
)
|
||||
|
||||
# 输出结果
|
||||
if success:
|
||||
print("\n√ 文件上传成功!")
|
||||
exit(0)
|
||||
else:
|
||||
print("\n× 文件上传失败!")
|
||||
exit(1)
|
||||
|
||||
Reference in New Issue
Block a user