Files
Junhan Chang a62a695812 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>
2025-04-17 15:09:58 +08:00

538 lines
22 KiB
Python

import json
import time
import traceback
from typing import Any, Union, List, Dict, Callable, Optional, Tuple
from pydantic import BaseModel
from pymodbus.client import ModbusSerialClient, ModbusTcpClient
from pymodbus.framer import FramerType
from typing import TypedDict
from unilabos.device_comms.modbus_plc.node.modbus import DeviceType, HoldRegister, Coil, InputRegister, DiscreteInputs, DataType, WorderOrder
from unilabos.device_comms.modbus_plc.node.modbus import Base as ModbusNodeBase
from unilabos.device_comms.universal_driver import UniversalDriver
from unilabos.utils.log import logger
import pandas as pd
class ModbusNode(BaseModel):
name: str
device_type: DeviceType
address: int
data_type: DataType = DataType.INT16
slave: int = 1
class PLCWorkflow(BaseModel):
name: str
actions: List[
Union[
"PLCWorkflow",
Callable[
[Callable[[str], ModbusNodeBase]],
None
]]
]
class Action(BaseModel):
name: str
rw: bool # read是0 write是1
class WorkflowAction(BaseModel):
init: Optional[Callable[[Callable[[str], ModbusNodeBase]], bool]] = None
start: Optional[Callable[[Callable[[str], ModbusNodeBase]], bool]] = None
stop: Optional[Callable[[Callable[[str], ModbusNodeBase]], bool]] = None
cleanup: Optional[Callable[[Callable[[str], ModbusNodeBase]], None]] = None
class ModbusWorkflow(BaseModel):
name: str
actions: List[Union["ModbusWorkflow", WorkflowAction]]
""" 前后端Json解析用 """
class AddressFunctionJson(TypedDict):
func_name: str
node_name: str
mode: str
value: Any
class InitFunctionJson(AddressFunctionJson):
pass
class StartFunctionJson(AddressFunctionJson):
pass
class StopFunctionJson(AddressFunctionJson):
pass
class CleanupFunctionJson(AddressFunctionJson):
pass
class ActionJson(TypedDict):
address_function_to_create: list[AddressFunctionJson]
create_init_function: Optional[InitFunctionJson]
create_start_function: Optional[StartFunctionJson]
create_stop_function: Optional[StopFunctionJson]
create_cleanup_function: Optional[CleanupFunctionJson]
class WorkflowCreateJson(TypedDict):
name: str
action: list[Union[ActionJson, 'WorkflowCreateJson'] | str]
class ExecuteProcedureJson(TypedDict):
register_node_list_from_csv_path: Optional[dict[str, Any]]
create_flow: list[WorkflowCreateJson]
execute_flow: list[str]
class BaseClient(UniversalDriver):
client: Optional[Union[ModbusSerialClient, ModbusTcpClient]] = None
_node_registry: Dict[str, ModbusNodeBase] = {}
DEFAULT_ADDRESS_PATH = ""
def __init__(self):
super().__init__()
# self.register_node_list_from_csv_path()
def _set_client(self, client: Optional[Union[ModbusSerialClient, ModbusTcpClient]]) -> None:
if client is None:
raise ValueError('client is not valid')
# if not isinstance(client, TCPClient ) or not isinstance(client, RTUClient):
# raise ValueError('client is not valid')
self.client = client
def _connect(self) -> None:
logger.info('try to connect client...')
if self.client:
if self.client.connect():
logger.info('client connected!')
else:
logger.error('client connect failed')
else:
raise ValueError('client is not initialized')
@classmethod
def load_csv(cls, file_path: str):
df = pd.read_csv(file_path)
df = df.drop_duplicates(subset='Name', keep='first') # FIXME: 重复的数据应该报错
data_dict = df.set_index('Name').to_dict(orient='index')
nodes = []
for k, v in data_dict.items():
deviceType = v.get('DeviceType', None)
addr = v.get('Address', 0)
dataType = v.get('DataType', 'BOOL')
if not deviceType or not addr:
continue
if deviceType == DeviceType.COIL.value:
byteAddr = int(addr / 10)
bitAddr = addr % 10
addr = byteAddr * 8 + bitAddr
if dataType == 'BOOL':
# noinspection PyTypeChecker
dataType = 'INT16'
# noinspection PyTypeChecker
if pd.isna(dataType):
print(v, "没有注册成功!")
continue
dataType: DataType = DataType[dataType]
nodes.append(ModbusNode(name=k, device_type=DeviceType(deviceType), address=addr, data_type=dataType))
return nodes
def use_node(self, name: str) -> ModbusNodeBase:
if name not in self._node_registry:
raise ValueError(f'node {name} is not registered')
return self._node_registry[name]
def get_node_registry(self) -> Dict[str, ModbusNodeBase]:
return self._node_registry
def register_node_list_from_csv_path(self, path: str = None) -> "BaseClient":
if path is None:
path = self.DEFAULT_ADDRESS_PATH
nodes = self.load_csv(path)
return self.register_node_list(nodes)
def register_node_list(self, node_list: List[ModbusNode]) -> "BaseClient":
if not self.client:
raise ValueError('client is not connected')
if not node_list or len(node_list) == 0:
logger.warning('node list is empty')
return self
logger.info(f'start to register {len(node_list)} nodes...')
for node in node_list:
if node is None:
continue
if node.name in self._node_registry:
logger.info(f'node {node.name} already exists')
exist = self._node_registry[node.name]
if exist.type != node.device_type:
raise ValueError(f'node {node.name} type {node.device_type} is diplicated with {exist.type}')
if exist.address != node.address:
raise ValueError(f'node {node.name} address is duplicated with {exist.address}')
continue
if not isinstance(node.device_type, DeviceType):
raise ValueError(f'node {node.name} type is not valid')
if node.device_type == DeviceType.HOLD_REGISTER:
self._node_registry[node.name] = HoldRegister(self.client, node.name, node.address, node.data_type)
elif node.device_type == DeviceType.COIL:
self._node_registry[node.name] = Coil(self.client, node.name, node.address, node.data_type)
elif node.device_type == DeviceType.INPUT_REGISTER:
self._node_registry[node.name] = InputRegister(self.client, node.name, node.address, node.data_type)
elif node.device_type == DeviceType.DISCRETE_INPUTS:
self._node_registry[node.name] = DiscreteInputs(self.client, node.name, node.address, node.data_type)
else:
raise ValueError(f'node {node.name} type {node.device_type} is not valid')
logger.info('register nodes done.')
return self
def run_plc_workflow(self, workflow: PLCWorkflow) -> None:
if not self.client:
raise ValueError('client is not connected')
logger.info(f'start to run workflow {workflow.name}...')
for action in workflow.actions:
if isinstance(action, PLCWorkflow):
self.run_plc_workflow(action)
elif isinstance(action, Callable):
action(self.use_node)
else:
raise ValueError(f'invalid action {action}')
def call_lifecycle_fn(
self,
workflow: ModbusWorkflow,
fn: Optional[Callable[[Callable], bool]],
) -> bool:
if not fn:
raise ValueError('fn is not valid in call_lifecycle_fn')
try:
return fn(self.use_node)
except Exception as e:
traceback.print_exc()
logger.error(f'execute {workflow.name} lifecycle failed, err: {e}')
return False
def run_modbus_workflow(self, workflow: ModbusWorkflow) -> bool:
if not self.client:
raise ValueError('client is not connected')
logger.info(f'start to run workflow {workflow.name}...')
for action in workflow.actions:
if isinstance(action, ModbusWorkflow):
if self.run_modbus_workflow(action):
logger.info(f"{action.name} workflow done.")
continue
else:
logger.error(f"{action.name} workflow failed")
return False
elif isinstance(action, WorkflowAction):
init = action.init
start = action.start
stop = action.stop
cleanup = action.cleanup
if not init and not start and not stop:
raise ValueError(f'invalid action {action}')
is_err = False
try:
if init and not self.call_lifecycle_fn(workflow, init):
raise ValueError(f"{workflow.name} init action failed")
if not self.call_lifecycle_fn(workflow, start):
raise ValueError(f"{workflow.name} start action failed")
if not self.call_lifecycle_fn(workflow, stop):
raise ValueError(f"{workflow.name} stop action failed")
logger.info(f"{workflow.name} action done.")
except Exception as e:
is_err = True
traceback.print_exc()
logger.error(f"{workflow.name} action failed, err: {e}")
finally:
logger.info(f"{workflow.name} try to run cleanup")
if cleanup:
self.call_lifecycle_fn(workflow, cleanup)
else:
logger.info(f"{workflow.name} cleanup is not defined")
if is_err:
return False
return True
else:
raise ValueError(f'invalid action type {type(action)}')
return True
function_name: dict[str, Callable[[Callable[[str], ModbusNodeBase]], bool]] = {}
@classmethod
def pack_func(cls, func, value="UNDEFINED"):
def execute_pack_func(use_node: Callable[[str], ModbusNodeBase]):
if value == "UNDEFINED":
func()
else:
func(use_node, value)
return execute_pack_func
def create_address_function(self, func_name: str = None, node_name: str = None, mode: str = None, value: Any = None, data_type: Optional[DataType] = None, word_order: WorderOrder = None, slave: Optional[int] = None) -> Callable[[Callable[[str], ModbusNodeBase]], bool]:
def execute_address_function(use_node: Callable[[str], ModbusNodeBase]) -> Union[bool, Tuple[Union[int, float, str, list[bool], list[int], list[float]], bool]]:
param = {"value": value}
if data_type is not None:
param["data_type"] = data_type
if word_order is not None:
param["word_order"] = word_order
if slave is not None:
param["slave"] = slave
target_node = use_node(node_name)
print("执行", node_name, type(target_node).__name__, target_node.address, mode, value)
if mode == 'read':
return use_node(node_name).read(**param)
elif mode == 'write':
return not use_node(node_name).write(**param)
return False
if func_name is None:
func_name = node_name + '_' + mode + '_' + str(value)
print("创建 address function", mode, func_name)
self.function_name[func_name] = execute_address_function
return execute_address_function
def create_init_function(self, func_name: str = None, node_name: str = None, mode: str = None, value: Any = None, data_type: Optional[DataType] = None, word_order: WorderOrder = None, slave: Optional[int] = None):
return self.create_address_function(func_name, node_name, mode, value, data_type, word_order, slave)
def create_stop_function(self, func_name: str = None, node_name: str = None, mode: str = None, value: Any = None, data_type: Optional[DataType] = None, word_order: WorderOrder = None, slave: Optional[int] = None):
return self.create_address_function(func_name, node_name, mode, value, data_type, word_order, slave)
def create_cleanup_function(self, func_name: str = None, node_name: str = None, mode: str = None, value: Any = None, data_type: Optional[DataType] = None, word_order: WorderOrder = None, slave: Optional[int] = None):
return self.create_address_function(func_name, node_name, mode, value, data_type, word_order, slave)
def create_start_function(self, func_name: str, write_functions: list[str], condition_functions: list[str], stop_condition_expression: str):
def execute_start_function(use_node: Callable[[str], ModbusNodeBase]) -> bool:
for write_function in write_functions:
self.function_name[write_function](use_node)
while True:
next_loop = False
condition_source = {}
for condition_function in condition_functions:
read_res, read_err = self.function_name[condition_function](use_node)
if read_err:
next_loop = True
break
condition_source[condition_function] = read_res
if not next_loop:
if stop_condition_expression:
condition_source["__RESULT"] = None
exec(f"__RESULT = {stop_condition_expression}", {}, condition_source) # todo: safety check
res = condition_source["__RESULT"]
print("取得计算结果;", res)
if res:
break
else:
time.sleep(0.3)
return True
return execute_start_function
def create_action_from_json(self, data: ActionJson):
for i in data["address_function_to_create"]:
self.create_address_function(**i)
init = None
start = None
stop = None
cleanup = None
if data["create_init_function"]:
print("创建 init function")
init = self.create_init_function(**data["create_init_function"])
if data["create_start_function"]:
print("创建 start function")
start = self.create_start_function(**data["create_start_function"])
if data["create_stop_function"]:
print("创建 stop function")
stop = self.create_stop_function(**data["create_stop_function"])
if data["create_cleanup_function"]:
print("创建 cleanup function")
cleanup = self.create_cleanup_function(**data["create_cleanup_function"])
return WorkflowAction(init=init, start=start, stop=stop, cleanup=cleanup)
workflow_name = {}
def create_workflow_from_json(self, data: list[WorkflowCreateJson]):
for ind, flow in enumerate(data):
print("正在创建 workflow", ind, flow["name"])
actions = []
for i in flow["action"]:
if isinstance(i, str):
print("沿用 已有workflow 作为action", i)
action = self.workflow_name[i]
else:
print("创建 action")
action = self.create_action_from_json(i)
actions.append(action)
flow_instance = ModbusWorkflow(name=flow["name"], actions=actions)
print("创建完成 workflow", flow["name"])
self.workflow_name[flow["name"]] = flow_instance
def execute_workflow_from_json(self, data: list[str]):
for i in data:
print("正在执行 workflow", i)
self.run_modbus_workflow(self.workflow_name[i])
def execute_procedure_from_json(self, data: ExecuteProcedureJson):
if data["register_node_list_from_csv_path"]:
print("注册节点 csv", data["register_node_list_from_csv_path"])
self.register_node_list_from_csv_path(**data["register_node_list_from_csv_path"])
print("创建工作流")
self.create_workflow_from_json(data["create_flow"])
print("执行工作流")
self.execute_workflow_from_json(data["execute_flow"])
class TCPClient(BaseClient):
def __init__(self, addr: str, port: int):
super().__init__()
self._set_client(ModbusTcpClient(host=addr, port=port))
# self._connect()
class RTUClient(BaseClient):
def __init__(self, port: str, baudrate: int, timeout: int):
super().__init__()
self._set_client(ModbusSerialClient(framer=FramerType.RTU, port=port, baudrate=baudrate, timeout=timeout))
self._connect()
if __name__ == '__main__':
""" 代码写法① """
def idel_init(use_node: Callable[[str], ModbusNodeBase]) -> bool:
# 修改速度
use_node('M01_idlepos_velocity_rw').write(20.0)
# 修改位置
# use_node('M01_idlepos_position_rw').write(35.22)
return True
def idel_position(use_node: Callable[[str], ModbusNodeBase]) -> bool:
use_node('M01_idlepos_coil_w').write(True)
while True:
pos_idel, idel_err = use_node('M01_idlepos_coil_r').read(1)
pos_stop, stop_err = use_node('M01_manual_stop_coil_r').read(1)
time.sleep(0.5)
if not idel_err and not stop_err and pos_idel[0] and pos_stop[0]:
break
return True
def idel_stop(use_node: Callable[[str], ModbusNodeBase]) -> bool:
use_node('M01_idlepos_coil_w').write(False)
return True
move_idel = ModbusWorkflow(name="测试待机位置", actions=[WorkflowAction(
init=idel_init,
start=idel_position,
stop=idel_stop,
)])
def pipetter_init(use_node: Callable[[str], ModbusNodeBase]) -> bool:
# 修改速度
# use_node('M01_idlepos_velocity_rw').write(10.0)
# 修改位置
# use_node('M01_idlepos_position_rw').write(35.22)
return True
def pipetter_position(use_node: Callable[[str], ModbusNodeBase]) -> bool:
use_node('M01_pipette0_coil_w').write(True)
while True:
pos_idel, isError = use_node('M01_pipette0_coil_r').read(1)
pos_stop, isError = use_node('M01_manual_stop_coil_r').read(1)
time.sleep(0.5)
if pos_idel[0] and pos_stop[0]:
break
return True
def pipetter_stop(use_node: Callable[[str], ModbusNodeBase]) -> bool:
use_node('M01_pipette0_coil_w').write(False)
return True
move_pipetter = ModbusWorkflow(name="测试待机位置", actions=[WorkflowAction(
init=None,
start=pipetter_position,
stop=pipetter_stop,
)])
workflow_test_2 = ModbusWorkflow(name="测试水平移动并停止", actions=[
move_idel,
move_pipetter,
])
# .run_modbus_workflow(move_2_left_workflow)
""" 代码写法② """
# if False:
# modbus_tcp_client_test2 = TCPClient('192.168.3.2', 502)
# modbus_tcp_client_test2.register_node_list_from_csv_path('M01.csv')
# init = modbus_tcp_client_test2.create_init_function('idel_init', 'M01_idlepos_velocity_rw', 'write', 20.0)
#
# modbus_tcp_client_test2.create_address_function('pos_tip', 'M01_idlepos_coil_w', 'write', True)
# modbus_tcp_client_test2.create_address_function('pos_tip_read', 'M01_idlepos_coil_r', 'read', 1)
# modbus_tcp_client_test2.create_address_function('manual_stop', 'M01_manual_stop_coil_r', 'read', 1)
# start = modbus_tcp_client_test2.create_start_function(
# 'idel_position',
# write_functions=[
# 'pos_tip'
# ],
# condition_functions=[
# 'pos_tip_read',
# 'manual_stop'
# ],
# stop_condition_expression='pos_tip_read[0] and manual_stop[0]'
# )
# stop = modbus_tcp_client_test2.create_stop_function('idel_stop', 'M01_idlepos_coil_w', 'write', False)
#
# move_idel = ModbusWorkflow(name="归位", actions=[WorkflowAction(
# init=init,
# start=start,
# stop=stop,
# )])
#
# modbus_tcp_client_test2.create_address_function('pipetter_position', 'M01_pipette0_coil_w', 'write', True)
# modbus_tcp_client_test2.create_address_function('pipetter_position_read', 'M01_pipette0_coil_r', 'read', 1)
# modbus_tcp_client_test2.create_address_function('pipetter_stop_read', 'M01_manual_stop_coil_r', 'read', 1)
# pipetter_position = modbus_tcp_client_test2.create_start_function(
# 'pipetter_start',
# write_functions=[
# 'pipetter_position'
# ],
# condition_functions=[
# 'pipetter_position_read',
# 'pipetter_stop_read'
# ],
# stop_condition_expression='pipetter_position[0] and pipetter_stop_read[0]'
# )
# pipetter_stop = modbus_tcp_client_test2.create_stop_function('pipetter_stop', 'M01_pipette0_coil_w', 'write', False)
#
# move_pipetter = ModbusWorkflow(name="测试待机位置", actions=[WorkflowAction(
# init=None,
# start=pipetter_position,
# stop=pipetter_stop,
# )])
#
# workflow_test_2 = ModbusWorkflow(name="测试水平移动并停止", actions=[
# move_idel,
# move_pipetter,
# ])
#
# workflow_test_2.run_modbus_workflow()
""" 代码写法③ """
with open('example_json.json', 'r', encoding='utf-8') as f:
example_json = json.load(f)
modbus_tcp_client_test2 = TCPClient('127.0.0.1', 5021)
modbus_tcp_client_test2.execute_procedure_from_json(example_json)
# .run_modbus_workflow(move_2_left_workflow)
# init_client(FramerType.SOCKET, "", '192.168.3.2', 502)