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:
Junhan Chang
2025-04-17 15:09:58 +08:00
committed by GitHub
parent 7ccb425e39
commit a62a695812
266 changed files with 40772 additions and 2 deletions

View File

View File

@@ -0,0 +1,333 @@
import tkinter as tk
from tkinter import ttk # 使用 ttk 替换 tk 控件
from tkinter import messagebox
from tkinter.font import Font
from threading import Thread
from ttkthemes import ThemedTk
import time
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import clr # pythonnet library
import sys
import threading
import datetime
jifenshijian = 10 #积分时间
shuaxinshijian = 0.01 #刷新时间
zihaodaxiao = 16 #字号大小
ymax = 70000
ymin = -2000
# 加载DLL
dll_path = "C:\\auto\\UV_spec\\idea-sdk 3.0.9\\idea-sdk.UPI\\IdeaOptics.dll"
clr.AddReference(dll_path)
from IdeaOptics import Wrapper
# 初始化Wrapper对象和光谱仪
wrapper = Wrapper()
number_of_spectrometers = wrapper.OpenAllSpectrometers()
if number_of_spectrometers > 0:
spectrometer_index = 0 # 假设使用第一个光谱仪
integration_time = jifenshijian # 设置积分时间
wrapper.setIntegrationTime(spectrometer_index, integration_time)
class App:
def __init__(self, root):
self.root = root
self.root.title("光谱测试")
self.is_continuous = False
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
self.stop_event = threading.Event() # 使用Event代替布尔标志
self.continuous_thread = None # 在这里初始化
self.background_spectrum = None
self.correct_background = False
self.test_count = 0
self.background_count = 0
self.source_spectrum = None # 初始化光源强度变量
self.transmission_mode = False # 初始化透射模式标志
self.data_ready = False
# 使用 grid 布局
self.root.columnconfigure(0, weight=1)
self.root.rowconfigure(0, weight=1)
self.root.rowconfigure(1, weight=1)
self.root.rowconfigure(2, weight=1)
self.root.rowconfigure(3, weight=1)
self.current_ylim = [-100, 1000] # 初始化y轴范围
# 创建一个 Style 对象
style = ttk.Style()
# 定义一个新的样式
style.configure('Custom.TButton', font=('Helvetica', zihaodaxiao, 'bold'), foreground='white')
# 创建滑动条和按钮的容器 Frame
control_frame = ttk.Frame(self.root)
control_frame.grid(row=0, column=0, sticky="ew")
# 创建一个滑动条来选择平滑次数
self.boxcar_width_slider = tk.Scale(control_frame, from_=0, to=10, orient=tk.HORIZONTAL, length=300, label="平滑次数", font=("Helvetica", zihaodaxiao, 'bold'))
self.boxcar_width_slider.grid(row=0, column=0, padx=10, pady=10)
# 创建一个滑动条来选择平均次数
self.scans_to_average_slider = tk.Scale(control_frame, from_=1, to=10, orient=tk.HORIZONTAL, length=300, label="平均次数", font=("Helvetica", zihaodaxiao, 'bold'))
self.scans_to_average_slider.grid(row=0, column=1, padx=10, pady=10)
# 调整 Scale 控件的外观
self.boxcar_width_slider.config(bg='grey', fg='white')
self.scans_to_average_slider.config(bg='grey', fg='white')
# 字体设置
entry_font = ('Helvetica', zihaodaxiao, 'bold')
# 添加输入框的容器 Frame
entry_frame = ttk.Frame(self.root)
entry_frame.grid(row=1, column=0, sticky="ew")
# 创建并放置"积分时间(ms)"输入框
ttk.Label(entry_frame, text="积分时间(ms):", font=entry_font).grid(row=0, column=0, padx=10, pady=10)
self.integration_time_entry = ttk.Entry(entry_frame, font=entry_font)
self.integration_time_entry.grid(row=0, column=1, padx=10, pady=10)
self.integration_time_entry.insert(0, "10") # 设置默认值
# 创建并放置"刷新间隔(s)"输入框
ttk.Label(entry_frame, text="刷新间隔(s):", font=entry_font).grid(row=0, column=2, padx=10, pady=10)
self.refresh_interval_entry = ttk.Entry(entry_frame, font=entry_font)
self.refresh_interval_entry.grid(row=0, column=3, padx=10, pady=10)
self.refresh_interval_entry.insert(0, "0.01") # 设置默认值
# 创建按钮的容器 Frame
button_frame = ttk.Frame(self.root)
button_frame.grid(row=2, column=0, sticky="ew")
# 创建并放置按钮
ttk.Button(button_frame, text="测试一下", style='Custom.TButton', command=self.single_test).grid(row=0, column=0, padx=10, pady=10)
ttk.Button(button_frame, text="连续测试", style='Custom.TButton', command=self.start_continuous_test).grid(row=0, column=1, padx=10, pady=10)
ttk.Button(button_frame, text="停止测试", style='Custom.TButton', command=self.stop_continuous_test).grid(row=0, column=2, padx=10, pady=10)
# 创建背景相关按钮的容器 Frame
background_frame = ttk.Frame(self.root)
background_frame.grid(row=3, column=0, sticky="ew")
# 创建并放置“采集背景”按钮
self.collect_background_button = ttk.Button(background_frame, text="采集背景", style='Custom.TButton', command=self.collect_background)
self.collect_background_button.grid(row=0, column=0, padx=10, pady=10)
# 创建并放置“背景校正”按钮
self.correct_background_button = ttk.Button(background_frame, text="背景校正", style='Custom.TButton', command=self.toggle_background_correction)
self.correct_background_button.grid(row=0, column=1, padx=10, pady=10)
# 创建“光源采集”按钮
ttk.Button(background_frame, text="光源采集", style='Custom.TButton', command=self.collect_source).grid(row=0, column=2, padx=10, pady=10)
# 创建“透射模式”按钮
self.transmission_button = ttk.Button(background_frame, text="透射模式", style='Custom.TButton', command=self.toggle_transmission_mode)
self.transmission_button.grid(row=0, column=3, padx=10, pady=10)
# 创建 matplotlib 画布
plt.style.use('ggplot') # 使用预定义的样式,如 'ggplot'
self.figure, self.ax = plt.subplots(figsize=(10, 8))
self.canvas = FigureCanvasTkAgg(self.figure, self.root)
self.canvas_widget = self.canvas.get_tk_widget()
self.canvas_widget.grid(row=3, column=0, sticky="ew")
# 使用 grid 布局来放置 matplotlib 画布
self.canvas_widget = self.canvas.get_tk_widget()
self.canvas_widget.grid(row=4, column=0, sticky="ew")
# 创建文件名并打开文件
start_time = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
self.data_file = open(f"C:\\auto\\UV_spec\\data\\{start_time}.txt", "w")
def get_spectrum_data(self):
# 获取波长和光谱值
pixels = wrapper.getNumberOfPixels(spectrometer_index)
spectrum = wrapper.getSpectrum(spectrometer_index)
wavelengths = wrapper.getWavelengths(spectrometer_index)
# 转换.NET数组到Python列表
spectrum_list = [spectrum[i] for i in range(pixels)]
wavelengths_list = [wavelengths[i] for i in range(pixels)]
self.data_ready = True
return wavelengths_list, spectrum_list
def collect_source(self):
# 采集光源强度数据
wavelengths, self.source_spectrum = self.get_spectrum_data()
conditions = f"jifenshijian = {jifenshijian} shuaxinshijian = {shuaxinshijian} zihaodaxiao = {zihaodaxiao}"
self.write_data_to_file("source", 1, conditions, self.source_spectrum)
self.update_plot(wavelengths, self.source_spectrum)
def toggle_transmission_mode(self):
# 切换透射模式
self.transmission_mode = not self.transmission_mode
self.transmission_button.config(text="正在透射" if self.transmission_mode else "透射模式")
def calculate_transmission(self, spectrum):
# 计算透射率
transmission = []
for s, b, src in zip(spectrum, self.background_spectrum, self.source_spectrum):
denominator = max(src - b, 0.1)
trans_value = (s - b) / denominator * 100
trans_value = max(0, min(trans_value, 100))
transmission.append(trans_value)
return transmission
def update_plot(self, wavelengths, spectrum, plot_type='spectrum'):
if not self.data_ready:
return
self.ax.clear()
if plot_type == 'transmission':
# 透射率模式的绘图设置
self.ax.plot(wavelengths, spectrum, label='Transmission (%)')
self.ax.set_ylim(-10, 110) # 设置y轴范围为0%到100%
self.ax.set_ylabel('Transmission (%)', fontname='Arial', fontsize=zihaodaxiao)
else:
# 普通光谱模式的绘图设置
self.ax.plot(wavelengths, spectrum)
self.ax.set_ylim(self.current_ylim) # 使用当前y轴范围
# 计算新的最大值和最小值
new_min, new_max = min(spectrum), max(spectrum)
# 检查新的最大值或最小值是否超过当前y轴范围
while new_min < self.current_ylim[0] or new_max > self.current_ylim[1]:
# 扩大y轴范围
self.current_ylim = [self.current_ylim[0] * 2, self.current_ylim[1] * 2]
# 确保新的y轴范围不超过最大限制
if self.current_ylim[0] < ymin:
self.current_ylim[0] = ymin
if self.current_ylim[1] > ymax:
self.current_ylim[1] = ymax
break
self.ax.set_ylabel('Intensity', fontname='Arial', fontsize=zihaodaxiao)
self.ax.set_xlabel('Wavelength (nm)', fontname='Arial', fontsize=zihaodaxiao)
self.ax.set_title('Spectrum', fontname='Arial', fontsize=zihaodaxiao)
self.canvas.draw()
self.data_ready = False
def draw_plot(self):
self.canvas.draw()
def write_data_to_file(self, test_type, test_number, conditions, spectrum):
data_str = " ".join(map(str, spectrum))
self.data_file.write(f"{test_type}{test_number}\n{conditions}\n{data_str}\n\n")
self.data_file.flush()
def collect_background(self):
# 设置平滑次数
boxcar_width = self.boxcar_width_slider.get()
wrapper.setBoxcarWidth(spectrometer_index, boxcar_width)
# 设置平均次数
scans_to_average = self.scans_to_average_slider.get()
wrapper.setScansToAverage(spectrometer_index, scans_to_average)
# 采集背景数据
wavelengths, self.background_spectrum = self.get_spectrum_data()
conditions = f"jifenshijian = {jifenshijian} shuaxinshijian = {shuaxinshijian} zihaodaxiao = {zihaodaxiao} pinghuacishu = {self.boxcar_width_slider.get()} pingjuncishu = {self.scans_to_average_slider.get()}"
self.background_count += 1
self.write_data_to_file("background", self.background_count, conditions, self.background_spectrum)
self.update_plot(wavelengths, self.background_spectrum)
def toggle_background_correction(self):
self.correct_background = not self.correct_background
self.correct_background_button.config(text="正在校正" if self.correct_background else "背景校正")
def apply_background_correction(self, spectrum):
if self.background_spectrum is not None and self.correct_background:
return [s - b for s, b in zip(spectrum, self.background_spectrum)]
return spectrum
def single_test(self):
# 获取输入框的值
jifenshijian = float(self.integration_time_entry.get())
shuaxinshijian = float(self.refresh_interval_entry.get())
# 设置平滑次数
boxcar_width = self.boxcar_width_slider.get()
wrapper.setBoxcarWidth(spectrometer_index, boxcar_width)
# 设置平均次数
scans_to_average = self.scans_to_average_slider.get()
wrapper.setScansToAverage(spectrometer_index, scans_to_average)
conditions = f"jifenshijian = {jifenshijian} shuaxinshijian = {shuaxinshijian} zihaodaxiao = {zihaodaxiao} pinghuacishu = {self.boxcar_width_slider.get()} pingjuncishu = {self.scans_to_average_slider.get()}"
self.test_count += 1
wavelengths, spectrum = self.get_spectrum_data()
# 在透射模式下计算透射率,否则应用背景校正
if self.transmission_mode and self.background_spectrum is not None and self.source_spectrum is not None:
transmission = self.calculate_transmission(spectrum)
self.update_plot(wavelengths, transmission, plot_type='transmission')
else:
corrected_spectrum = self.apply_background_correction(spectrum)
self.update_plot(wavelengths, corrected_spectrum, plot_type='spectrum')
def continuous_test(self):
while not self.stop_event.is_set():
# 获取输入框的值
jifenshijian = float(self.integration_time_entry.get())
shuaxinshijian = float(self.refresh_interval_entry.get())
# 设置平滑次数和平均次数
boxcar_width = self.boxcar_width_slider.get()
wrapper.setBoxcarWidth(spectrometer_index, boxcar_width)
scans_to_average = self.scans_to_average_slider.get()
wrapper.setScansToAverage(spectrometer_index, scans_to_average)
conditions = f"jifenshijian = {jifenshijian} shuaxinshijian = {shuaxinshijian} zihaodaxiao = {zihaodaxiao} pinghuacishu = {self.boxcar_width_slider.get()} pingjuncishu = {self.scans_to_average_slider.get()}"
self.test_count += 1
wavelengths, spectrum = self.get_spectrum_data()
self.write_data_to_file("test", self.test_count, conditions, spectrum)
# 根据当前模式计算并更新图表
if self.transmission_mode and self.background_spectrum is not None and self.source_spectrum is not None:
transmission = self.calculate_transmission(spectrum)
self.update_plot(wavelengths, transmission, plot_type='transmission')
else:
corrected_spectrum = self.apply_background_correction(spectrum)
self.update_plot(wavelengths, corrected_spectrum)
time.sleep(shuaxinshijian)
def start_continuous_test(self):
self.stop_event.clear() # 重置事件
self.continuous_thread = Thread(target=self.continuous_test)
self.continuous_thread.start()
def stop_continuous_test(self):
self.stop_event.set() # 设置事件通知线程停止
self.continuous_thread = None
def on_closing(self):
if self.data_file:
self.data_file.close()
if messagebox.askyesno("退出", "实验g了"):
self.stop_continuous_test()
self.root.destroy()
sys.exit()
if __name__ == "__main__":
# 使用 ThemedTk 而不是普通的 Tk
root = ThemedTk(theme="equilux") # 使用 'arc' 主题
# 由于我们已经使用了 ttkthemes 来设置主题,下面这些行可以省略
# style = ttk.Style()
# style.theme_use('arc')
app = App(root)
root.mainloop()

View File

View File

View File

@@ -0,0 +1,101 @@
import socket
import json
import time
from pydantic import BaseModel
class AgvNavigator:
def __init__(self, host):
self.control_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.receive_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.control_socket.connect((host, 19206))
self.receive_socket.connect((host, 19204))
self.rec_cmd_code = {
"pose" : "03EC",
"status" : "03FC",
"nav" : "0BEB"
}
self.status_list = ['NONE', 'WAITING', 'RUNNING', 'SUSPENDED', 'COMPLETED', 'FAILED', 'CANCELED']
self._pose = []
self._status = 'NONE'
self.success = False
@property
def pose(self) -> list:
data = self.send('pose')
try:
self._pose = [data['x'], data['y'], data['angle']]
except:
print(data)
return self._pose
@property
def status(self) -> str:
data = self.send('status')
self._status = self.status_list[data['task_status']]
return self._status
def send(self, cmd, ex_data = '', obj = 'receive_socket'):
data = bytearray.fromhex(f"5A 01 00 01 00 00 00 00 {self.rec_cmd_code[cmd]} 00 00 00 00 00 00")
if ex_data:
data_ = ex_data
data[7] = len(data_)
data= data + bytearray(data_,"utf-8")
cmd_obj = getattr(self, obj)
cmd_obj.sendall(data)
response_data = b""
while True:
part = cmd_obj.recv(4096) # 每次接收 4096 字节
response_data += part
if len(part) < 4096: # 当接收到的数据少于缓冲区大小时,表示接收完毕
break
response_str = response_data.hex()
json_start = response_str.find('7b') # 找到JSON的开始位置
if json_start == -1:
raise "Error: No JSON data found in response."
json_data = bytes.fromhex(response_str[json_start:])
# 尝试解析 JSON 数据
try:
parsed_json = json.loads(json_data.decode('utf-8'))
return parsed_json
except json.JSONDecodeError as e:
raise f"JSON Decode Error: {e}"
def send_nav_task(self, command:str):
self.success = False
# source,target = cmd.replace(' ','').split("->")
target = json.loads(command)['target']
json_data = {}
# json_data["source_id"] = source
json_data["id"] = target
# json_data["use_down_pgv"] = True
result = self.send('nav', ex_data=json.dumps(json_data), obj="control_socket")
try:
if result['ret_code'] == 0:
# print(result)
while True:
if self.status == 'COMPLETED':
break
time.sleep(1)
self.success = True
except:
self.success = False
def __del__(self):
self.control_socket.close()
self.receive_socket.close()
if __name__ == "__main__":
agv = AgvNavigator("192.168.1.42")
# print(agv.pose)
agv.send_nav_task('LM14')

View File

@@ -0,0 +1,54 @@
{
"pretreatment": [
[
"moveL",
[0.443, -0.0435, 0.40, 1.209, 1.209, 1.209]
],
[
"moveL",
[0.543, -0.0435, 0.50, 0, 1.57, 0]
],
[
"moveL",
[0.443, -0.0435, 0.60, 0, 1.57, 0]
],
[
"wait",
[2]
],
[
"gripper",
[255,255,255]
],
[
"wait",
[2]
],
[
"gripper",
[0,0,0]
],
[
"set",
[0.3,0.2]
],
[
"moveL",
[0.643, -0.0435, 0.40, 1.209, 1.209, 1.209]
]
],
"pose": [
[
"moveL",
[0.243, -0.0435, 0.30, 0.0, 3.12, 0.0]
],
[
"moveL",
[0.443, -0.0435, 0.40, 0.0, 3.12, 0.0]
],
[
"moveL",
[0.243, -0.0435, 0.50, 0.0, 3.12, 0.0]
]
]
}

View File

@@ -0,0 +1,298 @@
"""Module to control Robotiq's grippers - tested with HAND-E"""
import socket
import threading
import time
from enum import Enum
from typing import Union, Tuple, OrderedDict
class RobotiqGripper:
"""
Communicates with the gripper directly, via socket with string commands, leveraging string names for variables.
"""
# WRITE VARIABLES (CAN ALSO READ)
ACT = 'ACT' # act : activate (1 while activated, can be reset to clear fault status)
GTO = 'GTO' # gto : go to (will perform go to with the actions set in pos, for, spe)
ATR = 'ATR' # atr : auto-release (emergency slow move)
ADR = 'ADR' # adr : auto-release direction (open(1) or close(0) during auto-release)
FOR = 'FOR' # for : force (0-255)
SPE = 'SPE' # spe : speed (0-255)
POS = 'POS' # pos : position (0-255), 0 = open
# READ VARIABLES
STA = 'STA' # status (0 = is reset, 1 = activating, 3 = active)
PRE = 'PRE' # position request (echo of last commanded position)
OBJ = 'OBJ' # object detection (0 = moving, 1 = outer grip, 2 = inner grip, 3 = no object at rest)
FLT = 'FLT' # fault (0=ok, see manual for errors if not zero)
ENCODING = 'UTF-8' # ASCII and UTF-8 both seem to work
class GripperStatus(Enum):
"""Gripper status reported by the gripper. The integer values have to match what the gripper sends."""
RESET = 0
ACTIVATING = 1
# UNUSED = 2 # This value is currently not used by the gripper firmware
ACTIVE = 3
class ObjectStatus(Enum):
"""Object status reported by the gripper. The integer values have to match what the gripper sends."""
MOVING = 0
STOPPED_OUTER_OBJECT = 1
STOPPED_INNER_OBJECT = 2
AT_DEST = 3
def __init__(self ,host):
"""Constructor."""
self.socket = None
self.command_lock = threading.Lock()
self._min_position = 0
self._max_position = 255
self._min_speed = 0
self._max_speed = 255
self._min_force = 0
self._max_force = 255
self.connect(host)
# self.activate()
def connect(self, hostname: str, port: int = 63352, socket_timeout: float = 2.0) -> None:
"""Connects to a gripper at the given address.
:param hostname: Hostname or ip.
:param port: Port.
:param socket_timeout: Timeout for blocking socket operations.
"""
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.connect((hostname, port))
self.socket.settimeout(socket_timeout)
def disconnect(self) -> None:
"""Closes the connection with the gripper."""
self.socket.close()
def _set_vars(self, var_dict: OrderedDict[str, Union[int, float]]):
"""Sends the appropriate command via socket to set the value of n variables, and waits for its 'ack' response.
:param var_dict: Dictionary of variables to set (variable_name, value).
:return: True on successful reception of ack, false if no ack was received, indicating the set may not
have been effective.
"""
# construct unique command
cmd = "SET"
for variable, value in var_dict.items():
cmd += f" {variable} {str(value)}"
cmd += '\n' # new line is required for the command to finish
# atomic commands send/rcv
with self.command_lock:
self.socket.sendall(cmd.encode(self.ENCODING))
data = self.socket.recv(1024)
return self._is_ack(data)
def _set_var(self, variable: str, value: Union[int, float]):
"""Sends the appropriate command via socket to set the value of a variable, and waits for its 'ack' response.
:param variable: Variable to set.
:param value: Value to set for the variable.
:return: True on successful reception of ack, false if no ack was received, indicating the set may not
have been effective.
"""
return self._set_vars(OrderedDict([(variable, value)]))
def _get_var(self, variable: str):
"""Sends the appropriate command to retrieve the value of a variable from the gripper, blocking until the
response is received or the socket times out.
:param variable: Name of the variable to retrieve.
:return: Value of the variable as integer.
"""
# atomic commands send/rcv
with self.command_lock:
cmd = f"GET {variable}\n"
self.socket.sendall(cmd.encode(self.ENCODING))
data = self.socket.recv(1024)
# expect data of the form 'VAR x', where VAR is an echo of the variable name, and X the value
# note some special variables (like FLT) may send 2 bytes, instead of an integer. We assume integer here
var_name, value_str = data.decode(self.ENCODING).split()
if var_name != variable:
raise ValueError(f"Unexpected response {data} ({data.decode(self.ENCODING)}): does not match '{variable}'")
value = int(value_str)
return value
@staticmethod
def _is_ack(data: str):
return data == b'ack'
def _reset(self):
"""
Reset the gripper.
The following code is executed in the corresponding script function
def rq_reset(gripper_socket="1"):
rq_set_var("ACT", 0, gripper_socket)
rq_set_var("ATR", 0, gripper_socket)
while(not rq_get_var("ACT", 1, gripper_socket) == 0 or not rq_get_var("STA", 1, gripper_socket) == 0):
rq_set_var("ACT", 0, gripper_socket)
rq_set_var("ATR", 0, gripper_socket)
sync()
end
sleep(0.5)
end
"""
self._set_var(self.ACT, 0)
self._set_var(self.ATR, 0)
while (not self._get_var(self.ACT) == 0 or not self._get_var(self.STA) == 0):
self._set_var(self.ACT, 0)
self._set_var(self.ATR, 0)
time.sleep(0.5)
def activate(self, auto_calibrate: bool = True):
"""Resets the activation flag in the gripper, and sets it back to one, clearing previous fault flags.
:param auto_calibrate: Whether to calibrate the minimum and maximum positions based on actual motion.
The following code is executed in the corresponding script function
def rq_activate(gripper_socket="1"):
if (not rq_is_gripper_activated(gripper_socket)):
rq_reset(gripper_socket)
while(not rq_get_var("ACT", 1, gripper_socket) == 0 or not rq_get_var("STA", 1, gripper_socket) == 0):
rq_reset(gripper_socket)
sync()
end
rq_set_var("ACT",1, gripper_socket)
end
end
def rq_activate_and_wait(gripper_socket="1"):
if (not rq_is_gripper_activated(gripper_socket)):
rq_activate(gripper_socket)
sleep(1.0)
while(not rq_get_var("ACT", 1, gripper_socket) == 1 or not rq_get_var("STA", 1, gripper_socket) == 3):
sleep(0.1)
end
sleep(0.5)
end
end
"""
if not self.is_active():
self._reset()
while (not self._get_var(self.ACT) == 0 or not self._get_var(self.STA) == 0):
time.sleep(0.01)
self._set_var(self.ACT, 1)
time.sleep(1.0)
while (not self._get_var(self.ACT) == 1 or not self._get_var(self.STA) == 3):
time.sleep(0.01)
# auto-calibrate position range if desired
if auto_calibrate:
self.auto_calibrate()
def is_active(self):
"""Returns whether the gripper is active."""
status = self._get_var(self.STA)
return RobotiqGripper.GripperStatus(status) == RobotiqGripper.GripperStatus.ACTIVE
def get_min_position(self) -> int:
"""Returns the minimum position the gripper can reach (open position)."""
return self._min_position
def get_max_position(self) -> int:
"""Returns the maximum position the gripper can reach (closed position)."""
return self._max_position
def get_open_position(self) -> int:
"""Returns what is considered the open position for gripper (minimum position value)."""
return self.get_min_position()
def get_closed_position(self) -> int:
"""Returns what is considered the closed position for gripper (maximum position value)."""
return self.get_max_position()
def is_open(self):
"""Returns whether the current position is considered as being fully open."""
return self.get_current_position() <= self.get_open_position()
def is_closed(self):
"""Returns whether the current position is considered as being fully closed."""
return self.get_current_position() >= self.get_closed_position()
def get_current_position(self) -> int:
"""Returns the current position as returned by the physical hardware."""
return self._get_var(self.POS)
def auto_calibrate(self, log: bool = True) -> None:
"""Attempts to calibrate the open and closed positions, by slowly closing and opening the gripper.
:param log: Whether to print the results to log.
"""
# first try to open in case we are holding an object
(position, status) = self.move_and_wait_for_pos(self.get_open_position(), 64, 1)
if RobotiqGripper.ObjectStatus(status) != RobotiqGripper.ObjectStatus.AT_DEST:
raise RuntimeError(f"Calibration failed opening to start: {str(status)}")
# try to close as far as possible, and record the number
(position, status) = self.move_and_wait_for_pos(self.get_closed_position(), 64, 1)
if RobotiqGripper.ObjectStatus(status) != RobotiqGripper.ObjectStatus.AT_DEST:
raise RuntimeError(f"Calibration failed because of an object: {str(status)}")
assert position <= self._max_position
self._max_position = position
# try to open as far as possible, and record the number
(position, status) = self.move_and_wait_for_pos(self.get_open_position(), 64, 1)
if RobotiqGripper.ObjectStatus(status) != RobotiqGripper.ObjectStatus.AT_DEST:
raise RuntimeError(f"Calibration failed because of an object: {str(status)}")
assert position >= self._min_position
self._min_position = position
if log:
print(f"Gripper auto-calibrated to [{self.get_min_position()}, {self.get_max_position()}]")
def move(self, position: int, speed: int, force: int) -> Tuple[bool, int]:
"""Sends commands to start moving towards the given position, with the specified speed and force.
:param position: Position to move to [min_position, max_position]
:param speed: Speed to move at [min_speed, max_speed]
:param force: Force to use [min_force, max_force]
:return: A tuple with a bool indicating whether the action it was successfully sent, and an integer with
the actual position that was requested, after being adjusted to the min/max calibrated range.
"""
def clip_val(min_val, val, max_val):
return max(min_val, min(val, max_val))
clip_pos = clip_val(self._min_position, position, self._max_position)
clip_spe = clip_val(self._min_speed, speed, self._max_speed)
clip_for = clip_val(self._min_force, force, self._max_force)
# moves to the given position with the given speed and force
var_dict = OrderedDict([(self.POS, clip_pos), (self.SPE, clip_spe), (self.FOR, clip_for), (self.GTO, 1)])
return self._set_vars(var_dict), clip_pos
def move_and_wait_for_pos(self, position: int, speed: int, force: int) -> Tuple[int, ObjectStatus]: # noqa
"""Sends commands to start moving towards the given position, with the specified speed and force, and
then waits for the move to complete.
:param position: Position to move to [min_position, max_position]
:param speed: Speed to move at [min_speed, max_speed]
:param force: Force to use [min_force, max_force]
:return: A tuple with an integer representing the last position returned by the gripper after it notified
that the move had completed, a status indicating how the move ended (see ObjectStatus enum for details). Note
that it is possible that the position was not reached, if an object was detected during motion.
"""
set_ok, cmd_pos = self.move(position, speed, force)
if not set_ok:
raise RuntimeError("Failed to set variables for move.")
# wait until the gripper acknowledges that it will try to go to the requested position
while self._get_var(self.PRE) != cmd_pos:
time.sleep(0.001)
# wait until not moving
cur_obj = self._get_var(self.OBJ)
while RobotiqGripper.ObjectStatus(cur_obj) == RobotiqGripper.ObjectStatus.MOVING:
cur_obj = self._get_var(self.OBJ)
# report the actual position and the object status
final_pos = self._get_var(self.POS)
final_obj = cur_obj
return final_pos, RobotiqGripper.ObjectStatus(final_obj)
if __name__ == '__main__':
gripper = RobotiqGripper('192.168.1.178')
gripper.move(255,0,0)
print(gripper.move(255,0,0))

View File

@@ -0,0 +1,166 @@
import rtde_control
import dashboard_client
import time
import json
from unilabos.devices.agv.robotiq_gripper import RobotiqGripper
import rtde_receive
from std_msgs.msg import Float64MultiArray
from pydantic import BaseModel
class UrArmTask():
def __init__(self, host, retry=30):
self.init_flag = False
self.dash_c = None
n = 0
while self.dash_c is None:
try:
self.dash_c = dashboard_client.DashboardClient(host)
if not self.dash_c.isConnected():
self.dash_c.connect()
self.dash_c.loadURP('camera/250111_put_board.urp')
self.arm_init()
self.dash_c.running()
except Exception as e:
print(e)
self.dash_c = None
time.sleep(1)
n += 1
if n > retry:
raise Exception('Can not connect to the robot dashboard server!')
self.vel = 0.1
self.acc = 0.1
self.rtde_c = None
self.rtde_r = None
self.gripper = None
self._pose = [0.0,0.0,0.0,0.0,0.0,0.0]
self._gripper_pose = None
self._status = 'IDLE'
self._gripper_status = 'AT_DEST'
self.gripper_s_list = ['MOVING','STOPPED_OUTER_OBJECT','STOPPED_INNER_OBJECT','AT_DEST']
self.dash_c.loadURP('camera/250111_put_board.urp')
self.arm_init()
self.success = True
self.init_flag = True
n = 0
while self.gripper is None:
try:
self.gripper = RobotiqGripper(host)
self.gripper.activate()
# self._gripper_status = self.gripper_s_list[self.gripper._get_var('OBJ')]
except:
self.gripper = None
time.sleep(1)
n += 1
if n > retry:
raise Exception('Can not connect to the robot gripper server!')
n = 0
while self.rtde_r is None:
try:
self.rtde_r = rtde_receive.RTDEReceiveInterface(host)
if not self.rtde_r.isConnected():
self.rtde_r.reconnect()
self._pose = self.rtde_r.getActualTCPPose()
except Exception as e:
print(e)
self.rtde_r = None
time.sleep(1)
n += 1
if n > retry:
raise Exception('Can not connect to the arm info server!')
self.pose_data = {}
self.pose_file = 'C:\\auto\\unilabos\\unilabos\\devices\\agv\\pose.json'
self.reload_pose()
self.dash_c.stop()
def arm_init(self):
self.dash_c.powerOn()
self.dash_c.brakeRelease()
self.dash_c.unlockProtectiveStop()
running = self.dash_c.running()
while running:
running = self.dash_c.running()
time.sleep(1)
# def __del__(self):
# self.dash_c.disconnect()
# self.rtde_c.disconnect()
# self.rtde_r.disconnect()
# self.gripper.disconnect()
def load_pose_file(self,file):
self.pose_file = file
self.reload_pose()
def reload_pose(self):
self.pose_data = json.load(open(self.pose_file))
def load_pose_data(self,data):
self.pose_data = json.loads(data)
@property
def arm_pose(self) -> list:
try:
if not self.rtde_r.isConnected():
self.rtde_r.reconnect()
print('_'*30,'Reconnect to the arm info server!')
self._pose = self.rtde_r.getActualTCPPose()
# print(self._pose)
except Exception as e:
self._pose = self._pose
print('-'*20,'zhixing_arm\n',e)
return self._pose
@property
def gripper_pose(self) -> float:
if self.init_flag:
try:
self._gripper_status = self.gripper_s_list[self.gripper._get_var('OBJ')]
self._gripper_pose = self.gripper.get_current_position()
except Exception as e:
self._gripper_status = self._gripper_status
self._gripper_pose = self._gripper_pose
print('-'*20,'zhixing_gripper\n',e)
return self._gripper_pose
@property
def arm_status(self) -> str:
return self._status
@property
def gripper_status(self) -> str:
if self.init_flag:
return self._gripper_status
def move_pos_task(self,command):
self.success = False
task_name = json.loads(command)['task_name']
self.dash_c.loadURP(task_name)
self.dash_c.play()
time.sleep(0.5)
self._status = 'RUNNING'
while self._status == 'RUNNING':
running = self.dash_c.running()
if not running:
self._status = 'IDLE'
time.sleep(1)
self.success = True
if __name__ == "__main__":
arm = UrArmTask("192.168.1.178")
# arm.move_pos_task('t2_y4_transfer3.urp')
# print(arm.arm_pose())

View File

View File

@@ -0,0 +1,265 @@
import os
import asyncio
from asyncio import Event, Future, Lock, Task
from enum import Enum
from dataclasses import dataclass
import re
import time
from typing import Any, Union, Optional, overload
import serial.tools.list_ports
from serial import Serial
from serial.serialutil import SerialException
from unilabos.messages import Point3D
class GrblCNCConnectionError(Exception):
pass
@dataclass(frozen=True, kw_only=True)
class GrblCNCInfo:
port: str
address: str = "1"
limits: tuple[int, int, int, int, int, int] = (-150, 150, -200, 0, 0, 60)
def create(self):
return GrblCNCAsync(self.port, self.address, self.limits)
class GrblCNCAsync:
_status: str = "Offline"
_position: Point3D = Point3D(x=0.0, y=0.0, z=0.0)
def __init__(self, port: str, address: str = "1", limits: tuple[int, int, int, int, int, int] = (-150, 150, -200, 0, 0, 60)):
self.port = port
self.address = address
self.limits = limits
try:
self._serial = Serial(
baudrate=115200,
port=port
)
except (OSError, SerialException) as e:
raise GrblCNCConnectionError from e
self._busy = False
self._closing = False
self._pose_number = self.pose_number_remaining = -1
self._error_event = Event()
self._query_future = Future[Any]()
self._query_lock = Lock()
self._read_task: Optional[Task[None]] = None
self._read_extra_line = False
self._run_future: Optional[Future[Any]] = None
self._run_lock = Lock()
def _read_all(self):
data = self._serial.read_until(b"\n")
data_decoded = data.decode()
while not "ok" in data_decoded and not "Grbl" in data_decoded:
data += self._serial.read_until(b"\n")
data_decoded = data.decode()
return data
async def _read_loop(self):
try:
while True:
self._receive((await asyncio.to_thread(lambda: self._read_all())))
except SerialException as e:
raise GrblCNCConnectionError from e
finally:
if not self._closing:
self._error_event.set()
if self._query_future and not self._query_future.done():
self._query_future.set_exception(GrblCNCConnectionError())
if self._run_future and not self._run_future.done():
self._run_future.set_exception(GrblCNCConnectionError())
@overload
async def _query(self, command: str, dtype: type[bool]) -> bool:
pass
@overload
async def _query(self, command: str, dtype: type[int]) -> int:
pass
@overload
async def _query(self, command: str, dtype = None) -> str:
pass
async def _query(self, command: str, dtype: Optional[type] = None):
async with self._query_lock:
if self._closing or self._error_event.is_set():
raise GrblCNCConnectionError
self._query_future = Future[Any]()
self._read_extra_line = command.startswith("?")
run = ''
full_command = f"{command}{run}\n"
full_command_data = bytearray(full_command, 'ascii')
try:
# await asyncio.to_thread(lambda: self._serial.write(full_command_data))
self._serial.write(full_command_data)
return self._parse(await asyncio.wait_for(asyncio.shield(self._query_future), timeout=5.0), dtype=dtype)
except (SerialException, asyncio.TimeoutError) as e:
self._error_event.set()
raise GrblCNCConnectionError from e
finally:
self._query_future = None
def _parse(self, data: bytes, dtype: Optional[type] = None):
response = data.decode()
if dtype == bool:
return response == "1"
elif dtype == int:
return int(response)
else:
return response
def _receive(self, data: bytes):
ascii_string = "".join(chr(byte) for byte in data)
was_busy = self._busy
self._busy = "Idle" not in ascii_string
# if self._read_extra_line and ascii_string.startswith("ok"):
# self._read_extra_line = False
# return
if self._run_future and was_busy and not self._busy:
self._run_future.set_result(data)
if self._query_future:
self._query_future.set_result(data)
else:
raise Exception("Dropping data")
async def _run(self, command: str):
async with self._run_lock:
self._run_future = Future[Any]()
# self._busy = True
try:
await self._query(command)
while True:
await asyncio.sleep(0.2) # Wait for 0.5 seconds before polling again
status = await self.get_status()
if "Idle" in status:
break
await asyncio.shield(self._run_future)
finally:
self._run_future = None
async def initialize(self):
time.sleep(0.5)
await self._run("G0X0Y0Z0")
status = await self.get_status()
return status
# Operations
# Status Queries
@property
def status(self) -> str:
return self._status
async def get_status(self):
__pos_pattern__ = re.compile('.Pos:(\-?\d+\.\d+),(\-?\d+\.\d+),(\-?\d+\.\d+)')
__status_pattern__ = re.compile('<([a-zA-Z]+),')
response = await self._query("?")
pat = re.search(__pos_pattern__, response)
if pat is not None:
pos = pat.group().split(":")[1].split(",")
self._status = re.search(__status_pattern__, response).group(1).lstrip("<").rstrip(",")
self._position = Point3D(x=float(pos[0]), y=float(pos[1]), z=float(pos[2]))
return self.status
# Position Setpoint and Queries
@property
def position(self) -> Point3D:
# 由于此时一定调用过 get_status所以 position 一定是被更新过的
return self._position
def get_position(self):
return self.position
async def set_position(self, position: Point3D):
"""
Move to absolute position (unit: mm)
Args:
x, y, z: float
Returns:
None
"""
x = max(self.limits[0], min(self.limits[1], position.x))
y = max(self.limits[2], min(self.limits[3], position.y))
z = max(self.limits[4], min(self.limits[5], position.z))
return await self._run(f"G0X{x:.3f}Y{y:.3f}Z{z:.3f}")
async def move_through_points(self, points: list[Point3D]):
for i, point in enumerate(points):
self._pose_number = i
self.pose_number_remaining = len(points) - i
await self.set_position(point)
await asyncio.sleep(0.5)
self._step_number = -1
async def stop_operation(self):
return await self._run("T")
# Queries
async def wait_error(self):
await self._error_event.wait()
async def __aenter__(self):
await self.open()
return self
async def __aexit__(self, exc_type, exc, tb):
await self.close()
async def open(self):
if self._read_task:
raise GrblCNCConnectionError
self._read_task = asyncio.create_task(self._read_loop())
try:
await self.get_status()
except Exception:
await self.close()
raise
async def close(self):
if self._closing or not self._read_task:
raise GrblCNCConnectionError
self._closing = True
self._read_task.cancel()
try:
await self._read_task
except asyncio.CancelledError:
pass
finally:
del self._read_task
self._serial.close()
@staticmethod
def list():
for item in serial.tools.list_ports.comports():
yield GrblCNCInfo(port=item.device)

View File

@@ -0,0 +1,205 @@
import os
import asyncio
from threading import Event, Lock
from enum import Enum
from dataclasses import dataclass
import re
import time
from typing import Any, Union, Optional, overload
import serial.tools.list_ports
from serial import Serial
from serial.serialutil import SerialException
from unilabos.messages import Point3D
class GrblCNCConnectionError(Exception):
pass
@dataclass(frozen=True, kw_only=True)
class GrblCNCInfo:
port: str
address: str = "1"
limits: tuple[int, int, int, int, int, int] = (-150, 150, -200, 0, -80, 0)
def create(self):
return GrblCNC(self.port, self.address, self.limits)
class GrblCNC:
_status: str = "Offline"
_position: Point3D = Point3D(x=0.0, y=0.0, z=0.0)
_spindle_speed: float = 0.0
def __init__(self, port: str, address: str = "1", limits: tuple[int, int, int, int, int, int] = (-150, 150, -200, 0, -80, 0)):
self.port = port
self.address = address
self.limits = limits
try:
self._serial = Serial(
baudrate=115200,
port=port
)
except (OSError, SerialException) as e:
raise GrblCNCConnectionError from e
self._busy = False
self._closing = False
self._pose_number = self.pose_number_remaining = -1
self._query_lock = Lock()
self._run_lock = Lock()
self._error_event = Event()
def _read_all(self):
data = self._serial.read_until(b"\n")
data_decoded = data.decode()
while not "ok" in data_decoded and not "Grbl" in data_decoded:
data += self._serial.read_until(b"\n")
data_decoded = data.decode()
return data
@overload
def _query(self, command: str, dtype: type[bool]) -> bool:
pass
@overload
def _query(self, command: str, dtype: type[int]) -> int:
pass
@overload
def _query(self, command: str, dtype = None) -> str:
pass
def _query(self, command: str, dtype: Optional[type] = None):
with self._query_lock:
if self._closing or self._error_event.is_set():
raise GrblCNCConnectionError
self._read_extra_line = command.startswith("?")
run = ''
full_command = f"{command}{run}\n"
full_command_data = bytearray(full_command, 'ascii')
try:
# await asyncio.to_thread(lambda: self._serial.write(full_command_data))
self._serial.write(full_command_data)
time.sleep(0.1)
return self._receive(self._read_all())
except (SerialException, asyncio.TimeoutError) as e:
self._error_event.set()
raise GrblCNCConnectionError from e
def _receive(self, data: bytes):
ascii_string = "".join(chr(byte) for byte in data)
was_busy = self._busy
self._busy = "Idle" not in ascii_string
return ascii_string
def _run(self, command: str):
with self._run_lock:
try:
self._query(command)
while True:
time.sleep(0.2) # Wait for 0.5 seconds before polling again
status = self.get_status()
if "Idle" in status:
break
except:
self._error_event.set()
def initialize(self):
time.sleep(0.5)
self._run("G0X0Y0Z0")
status = self.get_status()
return status
# Operations
# Status Queries
@property
def status(self) -> str:
return self._status
def get_status(self):
__pos_pattern__ = re.compile('.Pos:(\-?\d+\.\d+),(\-?\d+\.\d+),(\-?\d+\.\d+)')
__status_pattern__ = re.compile('<([a-zA-Z]+),')
response = self._query("?")
pat = re.search(__pos_pattern__, response)
if pat is not None:
pos = pat.group().split(":")[1].split(",")
self._status = re.search(__status_pattern__, response).group(1).lstrip("<").rstrip(",")
self._position = Point3D(x=float(pos[0]), y=float(pos[1]), z=float(pos[2]))
return self.status
# Position Setpoint and Queries
@property
def position(self) -> Point3D:
# 由于此时一定调用过 get_status所以 position 一定是被更新过的
return self._position
def get_position(self):
return self.position
def set_position(self, position: Point3D):
"""
Move to absolute position (unit: mm)
Args:
x, y, z: float
Returns:
None
"""
x = max(self.limits[0], min(self.limits[1], position.x))
y = max(self.limits[2], min(self.limits[3], position.y))
z = max(self.limits[4], min(self.limits[5], position.z))
return self._run(f"G0X{x:.3f}Y{y:.3f}Z{z:.3f}")
def move_through_points(self, positions: list[Point3D]):
for i, point in enumerate(positions):
self._pose_number = i
self.pose_number_remaining = len(positions) - i
self.set_position(point)
time.sleep(0.5)
self._pose_number = -1
@property
def spindle_speed(self) -> float:
return self._spindle_speed
# def get_spindle_speed(self):
# self._spindle_speed = float(self._query("M3?"))
# return self.spindle_speed
def set_spindle_speed(self, spindle_speed: float, max_velocity: float = 500):
if spindle_speed < 0:
spindle_speed = 0
self._run("M5")
else:
spindle_speed = min(max_velocity, spindle_speed)
self._run(f"M3 S{spindle_speed}")
self._spindle_speed = spindle_speed
def stop_operation(self):
return self._run("T")
# Queries
async def wait_error(self):
await self._error_event.wait()
@staticmethod
def list():
for item in serial.tools.list_ports.comports():
yield GrblCNCInfo(port=item.device)

View File

@@ -0,0 +1,42 @@
import time
import asyncio
from pydantic import BaseModel
class Point3D(BaseModel):
x: float
y: float
z: float
def d(a: Point3D, b: Point3D) -> float:
return ((a.x - b.x) ** 2 + (a.y - b.y) ** 2 + (a.z - b.z) ** 2) ** 0.5
class MockCNCAsync:
def __init__(self):
self._position: Point3D = Point3D(x=0.0, y=0.0, z=0.0)
self._status = "Idle"
@property
def position(self) -> Point3D:
return self._position
async def get_position(self):
return self.position
@property
def status(self) -> str:
return self._status
async def set_position(self, position: Point3D, velocity: float = 10.0):
self._status = "Running"
current_pos = self.position
move_time = d(position, current_pos) / velocity
for i in range(20):
self._position.x = current_pos.x + (position.x - current_pos.x) / 20 * (i+1)
self._position.y = current_pos.y + (position.y - current_pos.y) / 20 * (i+1)
self._position.z = current_pos.z + (position.z - current_pos.z) / 20 * (i+1)
await asyncio.sleep(move_time / 20)
self._status = "Idle"

View File

View File

@@ -0,0 +1,46 @@
import time
class MockGripper:
def __init__(self):
self._position: float = 0.0
self._velocity: float = 2.0
self._torque: float = 0.0
self._status = "Idle"
@property
def position(self) -> float:
return self._position
@property
def velocity(self) -> float:
return self._velocity
@property
def torque(self) -> float:
return self._torque
@property
def status(self) -> str:
return self._status
def push_to(self, position: float, torque: float, velocity: float = 0.0):
self._status = "Running"
current_pos = self.position
if velocity == 0.0:
velocity = self.velocity
move_time = abs(position - current_pos) / velocity
for i in range(20):
self._position = current_pos + (position - current_pos) / 20 * (i+1)
self._torque = torque / (20 - i)
self._velocity = velocity
time.sleep(move_time / 20)
self._torque = torque
self._status = "Idle"
def edit_id(self, wf_name: str = "gripper_run", params: str = "{}", resource: dict = {"Gripper1": {}}):
v = list(resource.values())[0]
v["sample_id"] = "EDITED"
time.sleep(10)
return resource

View File

@@ -0,0 +1,408 @@
import time
from asyncio import Event
from enum import Enum, auto
from dataclasses import dataclass
from typing import Dict, Tuple
from pymodbus.client import ModbusSerialClient as ModbusClient
from pymodbus.client import ModbusTcpClient as ModbusTcpClient
class CommandType(Enum):
COMMAND_NONE = 0
COMMAND_GO_HOME = 1
COMMAND_DELAY = 2
COMMAND_MOVE_ABSOLUTE = 3
COMMAND_PUSH = 4
COMMAND_MOVE_RELATIVE = 5
COMMAND_PRECISE_PUSH = 6
class ParamType(Enum):
BOOLEAN = 0
INT32 = 1
FLOAT = 2
ENUM = 3
class ParamEdit(Enum):
NORMAL = 0
READONLY = 1
@dataclass
class Param:
type: ParamType
editability: ParamEdit
address: int
# 用于存储参数的字典类型
ParamsDict = Dict[str, Param]
# Constants and other required elements can be defined as needed
IO_GAP_TIME = 0.01
EXECUTE_COMMAND_INDEX = 15 # Example index
COMMAND_REACH_SIGNAL = "reach_15"
# Define other constants or configurations as needed
def REVERSE(x):
return ((x << 16) & 0xFFFF0000) | ((x >> 16) & 0x0000FFFF)
def int32_to_uint16_list(int32_list):
uint16_list = []
for num in int32_list:
lower_half = num & 0xFFFF
upper_half = (num >> 16) & 0xFFFF
uint16_list.extend([upper_half, lower_half])
return uint16_list
def uint16_list_to_int32_list(uint16_list):
if len(uint16_list) % 2 != 0:
raise ValueError("Input list must have even number of uint16 elements.")
int32_list = []
for i in range(0, len(uint16_list), 2):
# Combine two uint16 values into one int32 value
high = uint16_list[i + 1]
low = uint16_list[i]
# Assuming the uint16_list is in big-endian order
int32_value = (high << 16) | low
int32_list.append(int(int32_value))
return int32_list
class RMAxis:
modbus_device = {}
def __init__(self, port, is_modbus_rtu, baudrate: int = 115200, address: str = "", slave_id: int = 1):
self.device = port
self.is_modbus_rtu = is_modbus_rtu
if is_modbus_rtu:
self.client = ModbusClient(port=port, baudrate=baudrate, parity='N', stopbits=1, bytesize=8, timeout=3)
else:
self.client = ModbusTcpClient(address, port)
if not self.client.connect():
raise Exception(f"Modbus Connection failed")
self.slave_id = slave_id
self._error_event = Event()
self.device_params = {} # Assuming some initialization for parameters
self.command_edited = {}
self.init_parameters(self.device_params)
def init_parameters(self, params):
params["current_command_position"] = Param(ParamType.FLOAT, ParamEdit.NORMAL, 4902)
params["current_command_velocity"] = Param(ParamType.FLOAT, ParamEdit.NORMAL, 4904)
params["current_command_acceleration"] = Param(ParamType.FLOAT, ParamEdit.NORMAL, 4906)
params["current_command_deacceleration"] = Param(ParamType.FLOAT, ParamEdit.NORMAL, 4908)
params["current_position"] = Param(ParamType.FLOAT, ParamEdit.READONLY, 0)
params["current_velocity"] = Param(ParamType.FLOAT, ParamEdit.READONLY, 2)
params["control_torque"] = Param(ParamType.FLOAT, ParamEdit.NORMAL, 4)
params["error"] = Param(ParamType.INT32, ParamEdit.READONLY, 6)
params["current_force_sensor"] = Param(ParamType.FLOAT, ParamEdit.READONLY, 18)
params["io_in_go_home"] = Param(ParamType.BOOLEAN, ParamEdit.NORMAL, 1401)
params["io_in_error_reset"] = Param(ParamType.BOOLEAN, ParamEdit.NORMAL, 1402)
params["io_in_start"] = Param(ParamType.BOOLEAN, ParamEdit.NORMAL, 1403)
params["io_in_servo"] = Param(ParamType.BOOLEAN, ParamEdit.NORMAL, 1404)
params["io_in_stop"] = Param(ParamType.BOOLEAN, ParamEdit.NORMAL, 1405)
params["io_in_force_reset"] = Param(ParamType.BOOLEAN, ParamEdit.NORMAL, 1424)
params["io_in_save_parameters"] = Param(ParamType.BOOLEAN, ParamEdit.NORMAL, 1440)
params["io_in_load_parameters"] = Param(ParamType.BOOLEAN, ParamEdit.NORMAL, 1441)
params["io_in_save_positions"] = Param(ParamType.BOOLEAN, ParamEdit.NORMAL, 1442)
params["io_in_load_positions"] = Param(ParamType.BOOLEAN, ParamEdit.NORMAL, 1443)
params["io_out_gone_home"] = Param(ParamType.BOOLEAN, ParamEdit.NORMAL, 1501)
params["command_address"] = Param(ParamType.INT32, ParamEdit.NORMAL, 5000)
params["selected_command_index"] = Param(ParamType.INT32, ParamEdit.NORMAL, 4001)
params["io_out_reach_15"] = Param(ParamType.BOOLEAN, ParamEdit.NORMAL, 1521)
params["io_out_moving"] = Param(ParamType.BOOLEAN, ParamEdit.NORMAL, 1505)
params["limit_pos"] = Param(ParamType.FLOAT, ParamEdit.NORMAL, 74)
params["limit_neg"] = Param(ParamType.FLOAT, ParamEdit.NORMAL, 72)
params["hardware_direct_output"] = Param(ParamType.INT32, ParamEdit.NORMAL, 158)
params["hardware_direct_input"] = Param(ParamType.INT32, ParamEdit.NORMAL, 160)
def get_version(self):
version_major = self.client.read_input_registers(8, 1, unit=self.slave_id).registers[0]
version_minor = self.client.read_input_registers(10, 1, unit=self.slave_id).registers[0]
version_build = self.client.read_input_registers(12, 1, unit=self.slave_id).registers[0]
version_type = self.client.read_input_registers(14, 1, unit=self.slave_id).registers[0]
return (version_major, version_minor, version_build, version_type)
def set_input_signal(self, signal, level):
param_name = f"io_in_{signal}"
self.set_parameter(param_name, level)
def get_output_signal(self, signal):
param_name = f"io_out_{signal}"
return self.get_device_parameter(param_name)
def config_motion(self, velocity, acceleration, deacceleration):
self.set_parameter("current_command_velocity", velocity)
self.set_parameter("current_command_acceleration", acceleration)
self.set_parameter("current_command_deacceleration", deacceleration)
def move_to(self, position):
self.set_parameter("current_command_position", position)
def go_home(self):
self.set_input_signal("go_home", False)
time.sleep(IO_GAP_TIME) # Assuming IO_GAP_TIME is 0.1 seconds
self.set_input_signal("go_home", True)
def move_absolute(self, position, velocity, acceleration, deacceleration, band):
command = {
'type': CommandType.COMMAND_MOVE_ABSOLUTE.value,
'position': position,
'velocity': velocity,
'acceleration': acceleration,
'deacceleration': deacceleration,
'band': band,
'push_force': 0,
'push_distance': 0,
'delay': 0,
'next_command_index': -1
}
self.execute_command(command)
def move_relative(self, position, velocity, acceleration, deacceleration, band):
command = {
'type': CommandType.COMMAND_MOVE_RELATIVE.value,
'position': position,
'velocity': velocity,
'acceleration': acceleration,
'deacceleration': deacceleration,
'band': band,
'push_force': 0,
'push_distance': 0,
'delay': 0,
'next_command_index': -1
}
self.execute_command(command)
def push(self, force, distance, velocity):
command = {
'type': CommandType.COMMAND_PUSH.value,
'position': 0,
'velocity': velocity,
'acceleration': 0,
'deacceleration': 0,
'band': 0,
'push_force': force,
'push_distance': distance,
'delay': 0,
'next_command_index': -1
}
self.execute_command(command)
def precise_push(self, force, distance, velocity, force_band, force_check_time):
command = {
'type': CommandType.COMMAND_PRECISE_PUSH.value,
'position': 0,
'velocity': velocity,
'acceleration': 0,
'deacceleration': 0,
'band': force_band,
'push_force': force,
'push_distance': distance,
'delay': force_check_time,
'next_command_index': -1
}
self.execute_command(command)
def is_moving(self):
return self.get_output_signal("moving")
def is_reached(self):
return self.get_output_signal(COMMAND_REACH_SIGNAL)
def is_push_empty(self):
return not self.is_moving()
def set_command(self, index, command):
print("Setting command", command)
self.command_edited[index] = True
command_buffer = [
command['type'],
int(command['position'] * 1000),
int(command['velocity'] * 1000),
int(command['acceleration'] * 1000),
int(command['deacceleration'] * 1000),
int(command['band'] * 1000),
int(command['push_force'] * 1000),
int(command['push_distance'] * 1000),
int(command['delay']),
int(command['next_command_index'])
]
buffer = int32_to_uint16_list(command_buffer)
response = self.client.write_registers(self.device_params["command_address"].address + index * 20, buffer, self.slave_id)
def get_command(self, index):
response = self.client.read_holding_registers(self.device_params["command_address"].address + index * 20, 20, self.slave_id)
print(response)
buffer = response.registers
command_buffer = uint16_list_to_int32_list(buffer)
command = {
'type': command_buffer[0],
'position': command_buffer[1] / 1000.0,
'velocity': command_buffer[2] / 1000.0,
'acceleration': command_buffer[3] / 1000.0,
'deacceleration': command_buffer[4] / 1000.0,
'band': command_buffer[5] / 1000.0,
'push_force': command_buffer[6] / 1000.0,
'push_distance': command_buffer[7] / 1000.0,
'delay': command_buffer[8],
'next_command_index': command_buffer[9]
}
return command
def execute_command(self, command):
self.set_command(EXECUTE_COMMAND_INDEX, command)
self.save_commands()
self.trig_command(EXECUTE_COMMAND_INDEX)
time.sleep(IO_GAP_TIME) # Assuming IO_GAP_TIME is 0.1 seconds
def trig_command(self, index):
print("Triggering command", index)
self.set_parameter("selected_command_index", index)
self.set_input_signal("start", False)
time.sleep(IO_GAP_TIME) # Assuming IO_GAP_TIME is 0.1 seconds
self.set_input_signal("start", True)
def load_commands(self):
self.set_input_signal("load_positions", False)
time.sleep(IO_GAP_TIME) # Assuming IO_GAP_TIME is 0.1 seconds
self.set_input_signal("load_positions", True)
def save_commands(self):
for index, edited in self.command_edited.items():
if edited:
self.set_parameter("selected_command_index", index)
self.set_input_signal("save_positions", False)
time.sleep(IO_GAP_TIME) # Assuming IO_GAP_TIME is 0.1 seconds
self.set_input_signal("save_positions", True)
time.sleep(IO_GAP_TIME) # Assuming IO_GAP_TIME is 0.1 seconds
self.command_edited[index] = False
@property
def position(self) -> float:
return self.get_device_parameter("current_position")
def get_position(self) -> float:
return self.get_device_parameter("current_position")
@property
def velocity(self) -> float:
return self.get_device_parameter("current_velocity")
@property
def torque(self) -> float:
return self.get_device_parameter("control_torque")
@property
def force_sensor(self) -> float:
return self.get_device_parameter("current_force_sensor")
def error_code(self):
return self.get_device_parameter("error")
def get_device_parameter(self, name):
# Assuming self.device_params is a dictionary with address and type
param = self.device_params.get(name)
if not param:
self._error_event.set()
raise Exception(f"parameter {name} does not exist")
address = param.address
if param.editability == ParamEdit.READONLY:
if param.type == ParamType.BOOLEAN:
return self.client.read_input_discretes(address, 1).bits[0]
elif param.type == ParamType.ENUM:
return self.client.read_input_registers(address, 1).registers[0]
elif param.type == ParamType.INT32:
return self.client.read_input_registers(address, 2).registers[0] # Handle as needed
elif param.type == ParamType.FLOAT:
return self.client.read_input_registers(address, 2).registers[0] # Handle as needed
else:
self._error_event.set()
raise Exception(f"parameter {name} has unknown data type {param.type}")
else:
if param.type == ParamType.BOOLEAN:
return self.client.read_holding_registers(address, 1).registers[0]
elif param.type == ParamType.ENUM:
return self.client.read_holding_registers(address, 1).registers[0]
elif param.type == ParamType.INT32:
return self.client.read_holding_registers(address, 2).registers[0] # Handle as needed
elif param.type == ParamType.FLOAT:
return self.client.read_holding_registers(address, 2).registers[0] # Handle as needed
else:
self._error_event.set()
raise Exception(f"parameter {name} has unknown data type {param['type']}")
def set_parameter(self, name, value):
param = self.device_params.get(name)
if not param:
self._error_event.set()
raise Exception(f"parameter {name} does not exist")
address = param.address
if param.editability == ParamEdit.READONLY:
raise Exception(f"parameter {name} is read only")
else:
if param.type == ParamType.BOOLEAN:
self.client.write_coil(address, bool(value))
elif param.type == ParamType.ENUM:
self.client.write_register(address, value)
elif param.type == ParamType.INT32:
self.client.write_register(address, int(value))
elif param.type == ParamType.FLOAT:
self.client.write_register(address, float(value))
else:
self._error_event.set()
raise Exception(f"parameter {name} has unknown data type {param['type']}")
def load_parameters(self):
self.set_input_signal("load_parameters", False)
time.sleep(IO_GAP_TIME) # Assuming IO_GAP_TIME is 0.1 seconds
self.set_input_signal("load_parameters", True)
def save_parameters(self):
self.set_input_signal("save_parameters", False)
time.sleep(IO_GAP_TIME) # Assuming IO_GAP_TIME is 0.1 seconds
self.set_input_signal("save_parameters", True)
def reset_error(self):
self.set_input_signal("error_reset", False)
time.sleep(IO_GAP_TIME) # Assuming IO_GAP_TIME is 0.1 seconds
self.set_input_signal("error_reset", True)
def set_servo_on_off(self, on_off):
self.set_input_signal("servo", on_off)
def stop(self):
self.set_input_signal("stop", False)
time.sleep(IO_GAP_TIME) # Assuming IO_GAP_TIME is 0.1 seconds
self.set_input_signal("stop", True)
def soft_reset(self):
self.client.write_register(186, 0x22205682)
async def wait_error(self):
await self._error_event.wait()
def close(self):
self.client.close()
del self.client
if __name__ == "__main__":
# gripper = RMAxis.create_rmaxis_modbus_rtu("COM7", 115200, 1)
gripper = RMAxis.create_rmaxis_modbus_rtu('/dev/tty.usbserial-B002YGXY', 115200, 0)
gripper.go_home()
# gripper.move_to(20)
# print("Moving abs...")
# gripper.move_absolute(20, 5, 100, 100, 0.1)
# print(gripper.get_command(EXECUTE_COMMAND_INDEX))
# gripper.go_home()
# print("Pushing...")
# gripper.push(0.7, 10, 20)

View File

@@ -0,0 +1,197 @@
import json
import serial
import time as systime
class HeaterStirrer_DaLong:
def __init__(self, port: str = 'COM6', temp_warning = 50.0, baudrate: int = 9600):
try:
self.serial = serial.Serial(port, baudrate, timeout=2)
except serial.SerialException as e:
print("串口错误", f"无法打开串口{port}: {e}")
self._status = "Idle"
self._stir_speed = 0.0
self._temp = 0.0
self._temp_warning = temp_warning
self.set_temp_warning(temp_warning)
self._temp_target = 20.0
self.success = False
@property
def status(self) -> str:
return self._status
def get_status(self) -> str:
self._status = "Idle" if self.stir_speed == 0 else "Running"
@property
def stir_speed(self) -> float:
return self._stir_speed
def set_stir_speed(self, speed: float):
try:
# 转换速度为整数
speed_int = int(speed)
# 确保速度在允许的范围内
if speed_int < 0 or speed_int > 65535:
raise ValueError("速度必须在0到65535之间")
except ValueError as e:
print("输入错误", str(e))
return
# 计算高位和低位
speed_high = speed_int >> 8
speed_low = speed_int & 0xFF
# 构建搅拌控制指令
command = bytearray([0xfe, 0xB1, speed_high, speed_low, 0x00])
# 计算校验和
command.append(sum(command[1:]) % 256)
# 发送指令
self.serial.write(command)
# 检查响应
response = self.serial.read(6)
if len(response) == 6 and response[0] == 0xfd and response[1] == 0xB1 and response[2] == 0x00:
print("成功", "搅拌速度更新成功")
self._stir_speed = speed
else:
print("失败", "搅拌速度更新失败")
def heatchill(
self,
vessel: str,
temp: float,
time: float = 3600,
stir: bool = True,
stir_speed: float = 300,
purpose: str = "reaction"
):
self.set_temp_target(temp)
if stir:
self.set_stir_speed(stir_speed)
self.status = "Stirring"
systime.sleep(time)
self.set_stir_speed(0)
self.status = "Idle"
@property
def temp(self) -> float:
self._temp = self.get_temp()
return self._temp
def get_temp(self):
# 构建读取温度的指令
command = bytearray([0xfe, 0xA2, 0x00, 0x00, 0x00])
command.append(sum(command[1:]) % 256)
# 发送指令
self.serial.write(command)
# 读取响应
systime.sleep(0.1)
num_bytes = self.serial.in_waiting
response = self.serial.read(num_bytes)
try:
high_value = response[8]
low_value = response[9]
raw_temp = (high_value << 8) + low_value
if raw_temp & 0x8000: # 如果低位寄存器最高位为1表示负值
raw_temp -= 0x10000 # 转换为正确的负数表示
temp = raw_temp / 10
return temp
except:
return None
@property
def temp_warning(self) -> float:
return self._temp_warning
def set_temp_warning(self, temp):
self.success = False
# temp = round(float(warning_temp), 1)
if self.set_temp_inner(float(temp), "warning"):
self._temp_warning = round(float(temp), 1)
self.success = True
@property
def temp_target(self) -> float:
return self._temp_target
def set_temp_target(self, temp):
self.success = False
# temp = round(float(target_temp), 1)
if self.set_temp_inner(float(temp), "target"):
self._temp_target = round(float(temp), 1)
self.success = True
def set_temp_inner(self, temp: float, type: str = "warning"):
try:
# 转换为整数
temp_int = int(temp*10)
except ValueError as e:
print("输入错误", str(e))
return
# 计算高位和低位
temp_high = temp_int >> 8
temp_low = temp_int & 0xFF
# 构建控制指令
if type == "warning":
command = bytearray([0xfe, 0xB4, temp_high, temp_low, 0x00])
elif type == "target":
command = bytearray([0xfe, 0xB2, temp_high, temp_low, 0x00])
else:
return False
# 计算校验和
command.append(sum(command[1:]) % 256)
print(command)
# 发送指令
self.serial.write(command)
# 检查响应
systime.sleep(0.1)
response = self.serial.read(6)
print(response)
if len(response) == 6 and response[0] == 0xfd and response[1] == 0xB4 and response[2] == 0x00:
print("成功", "安全温度设置成功")
return True
else:
print("失败", "安全温度设置失败")
return False
def close(self):
self.serial.close()
if __name__ == "__main__":
import tkinter as tk
from tkinter import messagebox
heaterstirrer = HeaterStirrer_DaLong()
# heaterstirrer.set_mix_speed(0)
heaterstirrer.get_temp()
# heaterstirrer.set_warning(17)
print(heaterstirrer.temp)
print(heaterstirrer.temp_warning)
# 创建主窗口
# root = tk.Tk()
# root.title("搅拌速度控制")
# # 创建速度变量
# speed_var = tk.StringVar()
# # 创建输入框
# speed_entry = tk.Entry(root, textvariable=speed_var)
# speed_entry.pack(pady=10)
# # 创建按钮
# set_speed_button = tk.Button(root, text="确定", command=heaterstirrer.set_mix_speed)
# # set_speed_button = tk.Button(root, text="确定", command=heaterstirrer.read_temp)
# set_speed_button.pack(pady=5)
# # 运行主事件循环
# root.mainloop()
# 关闭串口
heaterstirrer.serial.close()

View File

@@ -0,0 +1,470 @@
import traceback
from datetime import datetime
import os
import re
from typing import TypedDict
import pyautogui
from pywinauto import Application
from pywinauto.application import WindowSpecification
from pywinauto.controls.uiawrapper import UIAWrapper
from pywinauto.uia_element_info import UIAElementInfo
from unilabos.app.oss_upload import oss_upload
from unilabos.device_comms import universal_driver as ud
from unilabos.device_comms.universal_driver import UniversalDriver
class DeviceStatusInfo(TypedDict):
name: str
name_obj: UIAWrapper
status: str
status_obj: UIAWrapper
open_btn: UIAWrapper
close_btn: UIAWrapper
sub_item: UIAWrapper
class DeviceStatus(TypedDict):
VWD: DeviceStatusInfo
JinYangQi: DeviceStatusInfo
Beng: DeviceStatusInfo
ShouJiQi: DeviceStatusInfo
class HPLCDriver(UniversalDriver):
# 设备状态
_device_status: DeviceStatus = None
_is_running: bool = False
_success: bool = False
_finished: int = None
_total_sample_number: int = None
_status_text: str = ""
# 外部传入
_wf_name: str = ""
# 暂时用不到用来支持action name
gantt: str = ""
status: str = ""
@property
def status_text(self) -> str:
return self._status_text
@property
def device_status(self) -> str:
return f", ".join([f"{k}:{v.get('status')}" for k, v in self._device_status.items()])
@property
def could_run(self) -> bool:
return self.driver_init_ok and all([v.get('status') == "空闲" for v in self._device_status.values()])
@property
def driver_init_ok(self) -> bool:
for k, v in self._device_status.items():
if v.get("open_btn") is None:
return False
if v.get("close_btn") is None:
return False
return len(self._device_status) == 4
@property
def is_running(self) -> bool:
return self._is_running
@property
def success(self) -> bool:
return self._success
@property
def finish_status(self) -> str:
return f"{self._finished}/{self._total_sample_number}"
def try_open_sub_device(self, device_name: str = None):
if not self.driver_init_ok:
self._success = False
print(f"仪器还没有初始化完成,无法查询设备:{device_name}")
return
if device_name is None:
for k, v in self._device_status.items():
self.try_open_sub_device(k)
return
target_device_status = self._device_status[device_name]
if target_device_status["status"] == "未就绪":
print(f"尝试打开{device_name}设备")
target_device_status["open_btn"].click()
else:
print(f"{device_name}设备状态不支持打开:{target_device_status['status']}")
def try_close_sub_device(self, device_name: str = None):
if not self.driver_init_ok:
self._success = False
print(f"仪器还没有初始化完成,无法查询设备:{device_name}")
return
if device_name is None:
for k, v in self._device_status.items():
self.try_close_sub_device(k)
return
target_device_status = self._device_status[device_name]
if target_device_status["status"] == "空闲":
print(f"尝试关闭{device_name}设备")
target_device_status["close_btn"].click()
else:
print(f"{device_name}设备状态不支持关闭:{target_device_status['status']}")
def _get_resource_sample_id(self, wf_name, idx):
try:
root = list(self.resource_info[wf_name].values())[0]
# print(root)
plates = root["children"]
plate_01 = list(plates.values())[0]
pots = list(plate_01["children"].values())
return pots[idx]['sample_id']
except Exception as ex:
traceback.print_exc()
def start_sequence(self, wf_name: str, params: str = None, resource: dict = None):
print("!!!!!! 任务启动")
self.resource_info[wf_name] = resource
# 后续workflow_name将同步一下
if self.is_running:
print("设备正在运行,无法启动序列")
self._success = False
return False
if not self.driver_init_ok:
print(f"仪器还没有初始化完成,无法启动序列")
self._success = False
return False
if not self.could_run:
print(f"仪器不处于空闲状态,无法运行")
self._success = False
return False
# 参考:
# with UIPath(u"PREP-LC (联机): 方法和运行控制 ||Window"):
# with UIPath(u"panelNavTabChem||Pane->||Pane->panelControlChemStation||Pane->||Tab->仪器控制||Pane->||Pane->panelChemStation||Pane->PREP-LC (联机): 方法和运行控制 ||Pane->ViewMGR||Pane->MRC view||Pane->||Pane->||Pane->||Pane->||Custom->||Custom"):
# click(u"||Button#[0,1]")
app = Application(backend='uia').connect(title=u"PREP-LC (联机): 方法和运行控制 ")
window = app['PREP-LC (联机): 方法和运行控制']
window.allow_magic_lookup = False
panel_nav_tab = window.child_window(title="panelNavTabChem", auto_id="panelNavTabChem", control_type="Pane")
first_pane = panel_nav_tab.child_window(auto_id="uctlNavTabChem1", control_type="Pane")
panel_control_station = first_pane.child_window(title="panelControlChemStation", auto_id="panelControlChemStation", control_type="Pane")
instrument_control_tab: WindowSpecification = panel_control_station.\
child_window(auto_id="tabControlChem", control_type="Tab").\
child_window(title="仪器控制", auto_id="tabPage1", control_type="Pane").\
child_window(auto_id="uctrlChemStation", control_type="Pane").\
child_window(title="panelChemStation", auto_id="panelChemStation", control_type="Pane").\
child_window(title="PREP-LC (联机): 方法和运行控制 ", control_type="Pane").\
child_window(title="ViewMGR", control_type="Pane").\
child_window(title="MRC view", control_type="Pane").\
child_window(auto_id="mainMrcControlHost", control_type="Pane").\
child_window(control_type="Pane", found_index=0).\
child_window(control_type="Pane", found_index=0).\
child_window(control_type="Custom", found_index=0).\
child_window(control_type="Custom", found_index=0)
instrument_control_tab.dump_tree(3)
btn: UIAWrapper = instrument_control_tab.child_window(auto_id="methodButtonStartSequence", control_type="Button").wrapper_object()
self.start_time = datetime.now()
btn.click()
self._wf_name = wf_name
self._success = True
return True
def check_status(self):
app = Application(backend='uia').connect(title=u"PREP-LC (联机): 方法和运行控制 ")
window = app['PREP-LC (联机): 方法和运行控制']
ui_window = window.child_window(title="靠顶部", control_type="Group").\
child_window(title="状态", control_type="ToolBar").\
child_window(title="项目", control_type="Button", found_index=0)
# 检测pixel的颜色
element_info: UIAElementInfo = ui_window.element_info
rectangle = element_info.rectangle
point_x = int(rectangle.left + rectangle.width() * 0.15)
point_y = int(rectangle.top + rectangle.height() * 0.15)
r, g, b = pyautogui.pixel(point_x, point_y)
if 270 > r > 250 and 200 > g > 180 and b < 10: # 是黄色
self._is_running = False
self._status_text = "Not Ready"
elif r > 110 and g > 190 and 50 < b < 60:
self._is_running = False
self._status_text = "Ready"
elif 75 > r > 65 and 135 > g > 120 and 240 > b > 230:
self._is_running = True
self._status_text = "Running"
else:
print(point_x, point_y, "未知的状态", r, g, b)
def extract_data_from_txt(self, file_path):
# 打开文件
print(file_path)
with open(file_path, mode='r', encoding='utf-16') as f:
lines = f.readlines()
# 定义一个标志变量来判断是否已经找到“馏分列表”
started = False
data = []
for line in lines:
# 查找“馏分列表”,并开始提取后续行
if line.startswith("-----|-----|-----"):
started = True
continue # 跳过当前行
if started:
# 遇到"==="表示结束读取
if '=' * 80 in line:
break
# 使用正则表达式提取馏分、孔、位置和原因
res = re.split(r'\s+', line.strip())
if res:
fraction, hole, position, reason = res[0], res[1], res[2], res[-1]
data.append({
'馏分': fraction,
'': hole,
'位置': position,
'原因': reason.strip()
})
return data
def get_data_file(self, mat_index: str = None, after_time: datetime = None) -> tuple[str, str]:
"""
获取数据文件
after_time: 由于HPLC是启动后生成一个带时间的目录所以会选取after_time后的文件
"""
if mat_index is None:
print(f"mat_index不能为空")
return None
if after_time is None:
after_time = self.start_time
files = [i for i in os.listdir(self.data_file_path) if i.startswith(self.using_method)]
time_to_files: list[tuple[datetime, str]] = [(datetime.strptime(i.split(" ", 1)[1], "%Y-%m-%d %H-%M-%S"), i) for i in files]
time_to_files.sort(key=lambda x: x[0])
choose_folder = None
for i in time_to_files:
if i[0] > after_time:
print(i[0], after_time)
print(f"选取时间{datetime.strftime(after_time, '%Y-%m-%d %H-%M-%S')}之后的文件夹{i[1]}")
choose_folder = i[1]
break
if choose_folder is None:
print(f"没有找到{self.using_method} {datetime.strftime(after_time, '%Y-%m-%d %H-%M-%S')}之后的文件夹")
return None
current_data_path = os.path.join(self.data_file_path, choose_folder)
# 需要匹配 数字数字数字-.* 001-P2-E1-DQJ-4-70.D
target_row = [i for i in os.listdir(current_data_path) if re.match(r"\d{3}-.*", i)]
index2filepath = {int(k.split("-")[0]): os.path.join(current_data_path, k) for k in target_row}
print(f"查找文件{mat_index}")
if int(mat_index) not in index2filepath:
print(f"没有找到{mat_index}的文件 已找到:{index2filepath}")
return None
mat_final_path = index2filepath[int(mat_index)]
pdf = os.path.join(mat_final_path, "Report.PDF")
txt = os.path.join(mat_final_path, "Report.TXT")
fractions = self.extract_data_from_txt(txt)
print(fractions)
return pdf, txt
def __init__(self, driver_debug=False):
super().__init__()
self.data_file_path = r"D:\ChemStation\1\Data"
self.using_method = f"1106-dqj-4-64"
self.start_time = datetime.now()
self._device_status = dict()
self.resource_info: dict[str, dict] = dict()
# 启动所有监控器
self.checkers = [
InstrumentChecker(self, 1),
RunningChecker(self, 1),
RunningResultChecker(self, 1),
]
if not driver_debug:
for checker in self.checkers:
checker.start_monitoring()
class DriverChecker(ud.DriverChecker):
driver: HPLCDriver
class InstrumentChecker(DriverChecker):
_instrument_control_tab = None
_instrument_control_tab_wrapper = None
def get_instrument_status(self):
if self._instrument_control_tab is not None:
return self._instrument_control_tab
# 连接到目标窗口
app = Application(backend='uia').connect(title=u"PREP-LC (联机): 方法和运行控制 ")
window = app['PREP-LC (联机): 方法和运行控制']
window.allow_magic_lookup = False
panel_nav_tab = window.child_window(title="panelNavTabChem", auto_id="panelNavTabChem", control_type="Pane")
first_pane = panel_nav_tab.child_window(auto_id="uctlNavTabChem1", control_type="Pane")
panel_control_station = first_pane.child_window(title="panelControlChemStation", auto_id="panelControlChemStation", control_type="Pane")
instrument_control_tab: WindowSpecification = panel_control_station.\
child_window(auto_id="tabControlChem", control_type="Tab").\
child_window(title="仪器控制", auto_id="tabPage1", control_type="Pane").\
child_window(auto_id="uctrlChemStation", control_type="Pane").\
child_window(title="panelChemStation", auto_id="panelChemStation", control_type="Pane").\
child_window(title="PREP-LC (联机): 方法和运行控制 ", control_type="Pane").\
child_window(title="ViewMGR", control_type="Pane").\
child_window(title="MRC view", control_type="Pane").\
child_window(auto_id="mainMrcControlHost", control_type="Pane").\
child_window(control_type="Pane", found_index=0).\
child_window(control_type="Pane", found_index=0).\
child_window(control_type="Custom", found_index=0).\
child_window(best_match="Custom6").\
child_window(auto_id="ListBox_DashboardPanel", control_type="List")
if self._instrument_control_tab is None:
self._instrument_control_tab = instrument_control_tab
self._instrument_control_tab_wrapper = instrument_control_tab.wrapper_object()
return self._instrument_control_tab
def check(self):
self.get_instrument_status()
if self._instrument_control_tab_wrapper is None or self._instrument_control_tab is None:
return
item: UIAWrapper
index = 0
keys = list(self.driver._device_status.keys())
for item in self._instrument_control_tab_wrapper.children():
info: UIAElementInfo = item.element_info
if info.control_type == "ListItem" and item.window_text() == "Agilent.RapidControl.StatusDashboard.PluginViewModel":
sub_item: WindowSpecification = self._instrument_control_tab.\
child_window(title="Agilent.RapidControl.StatusDashboard.PluginViewModel", control_type="ListItem", found_index=index).\
child_window(control_type="Custom", found_index=0)
if index < len(keys):
deviceStatusInfo = self.driver._device_status[keys[index]]
name = deviceStatusInfo["name"]
deviceStatusInfo["status"] = deviceStatusInfo["status_obj"].window_text()
print(name, index, deviceStatusInfo["status"], "刷新")
if deviceStatusInfo["open_btn"] is not None and deviceStatusInfo["close_btn"] is not None:
index += 1
continue
else:
name_obj = sub_item.child_window(control_type="Text", found_index=0).wrapper_object()
name = name_obj.window_text()
self.driver._device_status[name] = dict()
self.driver._device_status[name]["name_obj"] = name_obj
self.driver._device_status[name]["name"] = name
print(name, index)
status = sub_item.child_window(control_type="Custom", found_index=0).\
child_window(auto_id="TextBlock_StateLabel", control_type="Text")
status_obj: UIAWrapper = status.wrapper_object()
self.driver._device_status[name]["status_obj"] = status_obj
self.driver._device_status[name]["status"] = status_obj.window_text()
print(status.window_text())
sub_item = sub_item.wrapper_object()
found_index = 0
open_btn = None
close_btn = None
for btn in sub_item.children():
if btn.element_info.control_type == "Button":
found_index += 1
if found_index == 5:
open_btn = btn
elif found_index == 6:
close_btn = btn
self.driver._device_status[name]["open_btn"] = open_btn
self.driver._device_status[name]["close_btn"] = close_btn
index += 1
class RunningChecker(DriverChecker):
def check(self):
self.driver.check_status()
class RunningResultChecker(DriverChecker):
_finished: UIAWrapper = None
_total_sample_number: UIAWrapper = None
def check(self):
if self._finished is None or self._total_sample_number is None:
app = Application(backend='uia').connect(title=u"PREP-LC (联机): 方法和运行控制 ")
window = app['PREP-LC (联机): 方法和运行控制']
window.allow_magic_lookup = False
panel_nav_tab = window.child_window(title="panelNavTabChem", auto_id="panelNavTabChem", control_type="Pane")
first_pane = panel_nav_tab.child_window(auto_id="uctlNavTabChem1", control_type="Pane")
panel_control_station = first_pane.child_window(title="panelControlChemStation", auto_id="panelControlChemStation", control_type="Pane")
instrument_control_tab: WindowSpecification = panel_control_station.\
child_window(auto_id="tabControlChem", control_type="Tab").\
child_window(title="仪器控制", auto_id="tabPage1", control_type="Pane").\
child_window(auto_id="uctrlChemStation", control_type="Pane").\
child_window(title="panelChemStation", auto_id="panelChemStation", control_type="Pane").\
child_window(title="PREP-LC (联机): 方法和运行控制 ", control_type="Pane").\
child_window(title="ViewMGR", control_type="Pane").\
child_window(title="MRC view", control_type="Pane").\
child_window(auto_id="mainMrcControlHost", control_type="Pane").\
child_window(control_type="Pane", found_index=0).\
child_window(control_type="Pane", found_index=0).\
child_window(control_type="Custom", found_index=0).\
child_window(auto_id="mainControlExpanderSampleInformation", control_type="Group").\
child_window(auto_id="controlsSampleInfo", control_type="Custom")
self._finished = instrument_control_tab.child_window(best_match="Static15").wrapper_object()
self._total_sample_number = instrument_control_tab.child_window(best_match="Static16").wrapper_object()
try:
temp = int(self._finished.window_text())
if self.driver._finished is None or temp > self.driver._finished:
if self.driver._finished is None:
self.driver._finished = 0
for i in range(self.driver._finished, temp):
sample_id = self.driver._get_resource_sample_id(self.driver._wf_name, i) # 从0开始计数
pdf, txt = self.driver.get_data_file(i + 1)
device_id = self.driver.device_id if hasattr(self.driver, "device_id") else "default"
oss_upload(pdf, f"hplc/{sample_id}/{os.path.basename(pdf)}", process_key="example", device_id=device_id)
oss_upload(txt, f"hplc/{sample_id}/{os.path.basename(txt)}", process_key="HPLC-txt-result", device_id=device_id)
# self.driver.extract_data_from_txt()
except Exception as ex:
self.driver._finished = 0
print("转换数字出错", ex)
try:
self.driver._total_sample_number = int(self._total_sample_number.window_text())
except Exception as ex:
self.driver._total_sample_number = 0
print("转换数字出错", ex)
# 示例用法
if __name__ == "__main__":
# obj = HPLCDriver.__new__(HPLCDriver)
# obj.start_sequence()
# obj = HPLCDriver.__new__(HPLCDriver)
# obj.data_file_path = r"D:\ChemStation\1\Data"
# obj.using_method = r"1106-dqj-4-64"
# obj.get_data_file("001", after_time=datetime(2024, 11, 6, 19, 3, 6))
obj = HPLCDriver.__new__(HPLCDriver)
obj.data_file_path = r"D:\ChemStation\1\Data"
obj.using_method = r"1106-dqj-4-64"
obj._wf_name = "test"
obj.resource_info = {
"test": {
"1": {
"children": {
"11": {
"children": {
"111": {
"sample_id": "sample-1"
},
"112": {
"sample_id": "sample-2"
}
}
}
}
}
}
}
sample_id = obj._get_resource_sample_id("test", 0)
pdf, txt = obj.get_data_file("1", after_time=datetime(2024, 11, 6, 19, 3, 6))
oss_upload(pdf, f"hplc/{sample_id}/{os.path.basename(pdf)}", process_key="example")
oss_upload(txt, f"hplc/{sample_id}/{os.path.basename(txt)}", process_key="HPLC-txt-result")
# driver = HPLCDriver()
# for i in range(10000):
# print({k: v for k, v in driver._device_status.items() if isinstance(v, str)})
# print(driver.device_status)
# print(driver.could_run)
# print(driver.driver_init_ok)
# print(driver.is_running)
# print(driver.finish_status)
# print(driver.status_text)
# time.sleep(5)

View File

View File

@@ -0,0 +1,90 @@
import time
import sys
import io
# sys.path.insert(0, r'C:\kui\winprep_cli\winprep_c_Uni-lab\x64\Debug')
import winprep_c
from queue import Queue
class Revvity:
_success: bool = False
_status: str = "Idle"
_status_queue: Queue = Queue()
def __init__(self):
self._status = "Idle"
self._success = False
self._status_queue = Queue()
@property
def success(self) -> bool:
# print("success")
return self._success
@property
def status(self) -> str:
if not self._status_queue.empty():
self._status = self._status_queue.get()
return self._status
def _run_script(self, file_path: str):
output = io.StringIO()
sys.stdout = output # 重定向标准输出
try:
# 执行 winprep_c.test_mtp_script 并打印结果
winprep_c.test_mtp_script(file_path)
except Exception as e:
# 捕获执行过程中的异常
print(e)
self._status_queue.put(f"Error: {str(e)}")
finally:
sys.stdout = sys.__stdout__ # 恢复标准输出
# 获取捕获的输出并逐行更新状态
for line in output.getvalue().splitlines():
print(line)
self._status_queue.put(line)
self._status=line
def run(self, file_path: str, params:str, resource: dict = {"AichemecoHiwo": {"id": "AichemecoHiwo"}}):
# 设置状态为 Running
self._status = "Running"
winprep_c.test_mtp_script(file_path)
# 在一个新的线程中运行 MTP 脚本,避免阻塞主线程
# thread = threading.Thread(target=self._run_script, args=(file_path,))
# thread.start()
# self._run_script(file_path)
#
# # 在主线程中持续访问状态
# while thread.is_alive() or self._success == False:
# current_status = self.status() # 获取当前的状态
# print(f"Current Status: {current_status}")
# time.sleep(0.5)
# output = io.StringIO()
# sys.stdout = output # 重定向标准输出
# try:
# # 执行 winprep_c.test_mtp_script 并打印结果
# winprep_c.test_mtp_script(file_path)
# finally:
# sys.stdout = sys.__stdout__ # 恢复标准输出
# # 获取捕获的输出并逐行更新状态
# for line in output.getvalue().splitlines():
# self._status_queue.put(line)
# self._success = True
# 修改物料信息
workstation = list(resource.values())[0]
input_plate_wells = list(workstation["children"]["test-GL96-2A02"]["children"].values())
output_plate_wells = list(workstation["children"]["HPLC_Plate"]["children"].values())
for j in range(8):
output_plate_wells[j]["data"]["liquid"] += input_plate_wells[j]["data"]["liquid"]
output_plate_wells[j]["sample_id"] = input_plate_wells[j]["sample_id"]
self._status = "Idle"
self._success = True

View File

@@ -0,0 +1,4 @@
class Config(object):
DEBUG_MODE = True
OPEN_DEVICE = True
DEVICE_ADDRESS = 0x01

View File

@@ -0,0 +1,21 @@
class FakeSerial:
def __init__(self):
self.data = b''
def write(self, data):
print("发送数据: ", end="")
for i in data:
print(f"{i:02x}", end=" ")
print() # 换行
# 这里可模拟把假数据写到某个内部缓存
# self.data = ...
def setRTS(self, b):
pass
def read(self, n):
# 这里可返回预设的响应,例如 b'\x01\x03\x02\x00\x19\x79\x8E'
return b'\x01\x03\x02\x00\x19\x79\x8E'
def close(self):
pass

View File

@@ -0,0 +1,153 @@
import time
from serial import Serial
from threading import Thread
from unilabos.device_comms.universal_driver import UniversalDriver
class EleGripper(UniversalDriver):
@property
def status(self) -> str:
return f"spin_pos: {self.rot_data[0]}, grasp_pos: {self.gri_data[0]}, spin_v: {self.rot_data[1]}, grasp_v: {self.gri_data[1]}, spin_F: {self.rot_data[2]}, grasp_F: {self.gri_data[2]}"
def __init__(self,port,baudrate=115200, pos_error=-11, id = 9):
self.ser = Serial(port,baudrate)
self.pos_error = pos_error
self.success = False
# [pos, speed, force]
self.gri_data = [0,0,0]
self.rot_data = [0,0,0]
self.id = id
self.init_gripper()
self.wait_for_gripper_init()
t = Thread(target=self.data_loop)
t.start()
self.gripper_move(0,255,255)
self.rotate_move_abs(0,255,255)
def modbus_crc(self, data: bytes) -> bytes:
crc = 0xFFFF
for pos in data:
crc ^= pos
for _ in range(8):
if (crc & 0x0001) != 0:
crc >>= 1
crc ^= 0xA001
else:
crc >>= 1
return crc.to_bytes(2, byteorder='little')
def send_cmd(self, id, fun, address, data:str):
data_len = len(bytes.fromhex(data))
data_ = f"{id:02X} {fun} {address} {data_len//2:04X} {data_len:02X} {data}"
data_bytes = bytes.fromhex(data_)
data_with_checksum = data_bytes + self.modbus_crc(data_bytes)
self.ser.write(data_with_checksum)
time.sleep(0.01)
self.ser.read(8)
def read_address(self, id , address, data_len):
data = f"{id:02X} 03 {address} {data_len:04X}"
data_bytes = bytes.fromhex(data)
data_with_checksum = data_bytes + self.modbus_crc(data_bytes)
self.ser.write(data_with_checksum)
time.sleep(0.01)
res_len = 5+data_len*2
res = self.ser.read(res_len)
return res
def init_gripper(self):
self.send_cmd(self.id,'10','03 E8','00 01')
self.send_cmd(self.id,'10','03 E9','00 01')
def gripper_move(self, pos, speed, force):
self.send_cmd(self.id,'10', '03 EA', f"{speed:02x} {pos:02x} {force:02x} 01")
def rotate_move_abs(self, pos, speed, force):
pos += self.pos_error
if pos < 0:
pos = (1 << 16) + pos
self.send_cmd(self.id,'10', '03 EC', f"{(pos):04x} {force:02x} {speed:02x} 0000 00 01")
# def rotate_move_rel(self, pos, speed, force):
# if pos < 0:
# pos = (1 << 16) + pos
# print(f'{pos:04x}')
# self.send_cmd(self.id,'10', '03 EC', f"0000 {force:02x} {speed:02x} {pos:04x} 00 02")
def wait_for_gripper_init(self):
res = self.read_address(self.id, "07 D0", 1)
res_r = self.read_address(self.id, "07 D1", 1)
while res[4] == 0x08 or res_r[4] == 0x08:
res = self.read_address(self.id, "07 D0", 1)
res_r = self.read_address(self.id, "07 D1", 1)
time.sleep(0.1)
def wait_for_gripper(self):
while self.gri_data[1] != 0:
time.sleep(0.1)
def wait_for_rotate(self):
while self.rot_data[1] != 0:
time.sleep(0.1)
def data_reader(self):
res_g = self.read_address(self.id, "07 D2", 2)
res_r = self.read_address(self.id, "07 D4", 4)
int32_value = (res_r[3] << 8) | res_r[4]
if int32_value > 0x7FFF:
int32_value -= 0x10000
self.gri_data = (int(res_g[4]), int(res_g[3]), int(res_g[5]))
self.rot_data = (int32_value, int(res_r[5]), int(res_r[6]))
def data_loop(self):
while True:
self.data_reader()
time.sleep(0.1)
def node_gripper_move(self, cmd:str):
self.success = False
try:
cmd_list = [int(x) for x in cmd.replace(' ','').split(',')]
self.gripper_move(*cmd_list)
self.wait_for_gripper()
except Exception as e:
raise e
self.success = True
def node_rotate_move(self, cmd:str):
self.success = False
try:
cmd_list = [int(x) for x in cmd.replace(' ','').split(',')]
self.rotate_move_abs(*cmd_list)
self.wait_for_rotate()
except Exception as e:
raise e
self.success = True
def move_and_rotate(self, spin_pos, grasp_pos, spin_v, grasp_v, spin_F, grasp_F):
self.gripper_move(grasp_pos, grasp_v, grasp_F)
self.wait_for_gripper()
self.rotate_move_abs(spin_pos, spin_v, spin_F)
self.wait_for_rotate()
if __name__ == "__main__":
gripper = EleGripper("COM12")
gripper.init_gripper()
gripper.wait_for_gripper_init()
gripper.gripper_move(210,127,255)
gripper.wait_for_gripper()
gripper.rotate_move_abs(135,10,255)
gripper.data_reader()
print(gripper.rot_data)

View File

@@ -0,0 +1,87 @@
import struct
import time
from typing import Union
from .Consts import Config
def calculate_modbus_crc16(data: bytes) -> tuple[int, int]:
"""
计算 Modbus RTU 的 CRC16 校验码,返回 (low_byte, high_byte)。
data 可以是 bytes 或者 bytearray
"""
crc = 0xFFFF
for byte in data:
crc ^= byte
for _ in range(8):
if (crc & 0x0001):
crc = (crc >> 1) ^ 0xA001
else:
crc >>= 1
# 低字节在前、高字节在后
low_byte = crc & 0xFF
high_byte = (crc >> 8) & 0xFF
return low_byte, high_byte
def create_command(slave_id, func_code, address, data):
"""
生成完整的 Modbus 通信指令:
- 第1字节: 从站地址
- 第2字节: 功能码
- 第3~4字节: 寄存器地址 (大端)
- 第5~6字节: 数据(或读寄存器个数) (大端)
- 第7~8字节: CRC校验, 先低后高
"""
# 按照大端格式打包B(1字节), B(1字节), H(2字节), H(2字节)
# 例如: 0x03, 0x03, 0x0191, 0x0001
# 生成的命令将是: 03 03 01 91 00 01 (不含 CRC)
command = struct.pack(">B B H H", slave_id, func_code, address, data)
# 计算CRC并将低字节、后高字节拼到末尾
low_byte, high_byte = calculate_modbus_crc16(command)
return command + bytes([low_byte, high_byte])
def send_command(ser, command) -> Union[bytes, str]:
"""通过串口发送指令并打印响应"""
# Modbus RTU 半双工,发送前拉高 RTS
ser.setRTS(True)
time.sleep(0.02)
ser.write(command) # 发送指令
if Config.OPEN_DEVICE:
# 如果是实际串口就打印16进制的发送内容
print(f"发送的数据: ", end="")
for ind, c in enumerate(command.hex().upper()):
if ind % 2 == 0 and ind != 0:
print(" ", end="")
print(c, end="")
# 发送完成后,切换到接收模式
ser.setRTS(False)
# 读取响应,具体长度要看从站返回,有时多字节
response = ser.read(8) # 假设响应是8字节
print(f"接收到的数据: ", end="")
for ind, c in enumerate(response.hex().upper()):
if ind % 2 == 0 and ind != 0:
print(" ", end="")
print(c, end="")
print()
return response
def get_result_byte_int(data: bytes, byte_start: int = 6, byte_end: int = 10) -> int:
return int(data.hex()[byte_start:byte_end], 16)
def get_result_byte_str(data: bytes, byte_start: int = 6, byte_end: int = 10) -> str:
return data.hex()[byte_start:byte_end]
def run_commands(ser, duration=0.1, *commands):
for cmd in commands:
if isinstance(cmd, list):
for c in cmd:
send_command(ser, c)
time.sleep(duration)
else:
send_command(ser, cmd)
time.sleep(duration)

View File

View File

@@ -0,0 +1,37 @@
from enum import Enum
class PR_PATH(Enum):
# TYPE (Bit0-3)
TYPE_NO_ACTION = 0x0000 # 无动作
TYPE_POSITIONING = 0x0001 # 位置定位
TYPE_VELOCITY = 0x0002 # 速度运行
TYPE_HOME = 0x0003 # 回零
# INS (Bit4) - 默认都是插断模式
INS_INTERRUPT = 0x0010 # 插断
# OVLP (Bit5) - 默认都是不重叠
OVLP_NO_OVERLAP = 0x0000 # 不重叠
# POSITION MODE (Bit6)
POS_ABSOLUTE = 0x0000 # 绝对位置
POS_RELATIVE = 0x0040 # 相对位置
# MOTOR MODE (Bit7)
MOTOR_ABSOLUTE = 0x0000 # 绝对电机
MOTOR_RELATIVE = 0x0080 # 相对电机
# 常用组合(默认都是插断且不重叠)
# 位置定位相关
ABS_POS = TYPE_POSITIONING | INS_INTERRUPT | OVLP_NO_OVERLAP | POS_ABSOLUTE # 绝对定位
REL_POS = TYPE_POSITIONING | INS_INTERRUPT | OVLP_NO_OVERLAP | POS_RELATIVE # 相对定位
# 速度运行相关
VELOCITY = TYPE_VELOCITY | INS_INTERRUPT | OVLP_NO_OVERLAP # 速度模式
# 回零相关
HOME = TYPE_HOME | INS_INTERRUPT | OVLP_NO_OVERLAP # 回零模式
# 电机模式组合
ABS_POS_REL_MOTOR = ABS_POS | MOTOR_RELATIVE # 绝对定位+相对电机
REL_POS_REL_MOTOR = REL_POS | MOTOR_RELATIVE # 相对定位+相对电机

View File

@@ -0,0 +1,92 @@
from ..base_types.PrPath import PR_PATH
from ..Utils import create_command, get_result_byte_int, get_result_byte_str, send_command
def create_position_commands(slave_id: int, which: int, how: PR_PATH,
position: float, velocity: int = 300,
acc_time: int = 50, dec_time: int = 50,
special: int = 0) -> list[list[bytes]]:
"""
创建位置相关的Modbus命令列表
Args:
slave_id: 从站地址
which: PR路径号(0-7)
how: PR_PATH枚举定义运动方式
position: 目标位置(Pulse)
velocity: 运行速度(rpm)
acc_time: 加速时间(ms/Krpm)默认50
dec_time: 减速时间(ms/Krpm)默认50
special: 特殊参数默认0
Returns:
包含所有设置命令的列表
"""
if not 0 <= which <= 7:
raise ValueError("which必须在0到7之间")
base_addr = 0x6200 + which * 8
# 位置值保持原样Pulse
position = int(position)
print(f"路径方式 {' '.join(bin(how.value)[2:])} 位置 {position} 速度 {velocity}")
position_high = (position >> 16) & 0xFFFF # 获取高16位
position_low = position & 0xFFFF # 获取低16位
# 速度值rpm转换为0x0000格式
velocity_value = 0x0000 + velocity
# 加减速时间ms/Krpm转换为0x0000格式
acc_time_value = 0x0000 + int(acc_time)
dec_time_value = 0x0000 + int(dec_time)
# 特殊参数转换为0x0000格式
special_value = 0x0000 + special
return [
create_command(slave_id, 0x06, base_addr + 0, how.value), # 路径方式
create_command(slave_id, 0x06, base_addr + 1, position_high), # 位置高16位
create_command(slave_id, 0x06, base_addr + 2, position_low), # 位置低16位
create_command(slave_id, 0x06, base_addr + 3, velocity_value), # 运行速度
create_command(slave_id, 0x06, base_addr + 4, acc_time_value), # 加速时间
create_command(slave_id, 0x06, base_addr + 5, dec_time_value), # 减速时间
create_command(slave_id, 0x06, base_addr + 6, special_value), # 特殊参数
]
def create_position_run_command(slave_id: int, which: int) -> list[list[bytes]]:
print(f"运行路径 PR{which}")
return [create_command(slave_id, 0x06, 0x6002, 0x0010 + which)]
def run_set_position_zero(ser, DEVICE_ADDRESS) -> list[list[bytes]]:
print(f"手动回零")
send_command(ser, create_command(DEVICE_ADDRESS, 0x06, 0x6002, 0x0021))
def run_stop(ser, DEVICE_ADDRESS) -> list[list[bytes]]:
print(f"急停")
send_command(ser, create_command(DEVICE_ADDRESS, 0x06, 0x6002, 0x0040))
def run_set_forward_run(ser, DEVICE_ADDRESS) -> list[list[bytes]]:
print(f"设定正方向运动")
send_command(ser, create_command(DEVICE_ADDRESS, 0x06, 0x0007, 0x0000))
def run_set_backward_run(ser, DEVICE_ADDRESS) -> list[list[bytes]]:
print(f"设定反方向运动")
send_command(ser, create_command(DEVICE_ADDRESS, 0x06, 0x0007, 0x0001))
def run_get_command_position(ser, DEVICE_ADDRESS, print_pos=True) -> int:
retH = send_command(ser, create_command(DEVICE_ADDRESS, 0x03, 0x602A, 0x0001)) # 命令位置H
retL = send_command(ser, create_command(DEVICE_ADDRESS, 0x03, 0x602B, 0x0001)) # 命令位置L
value = get_result_byte_str(retH) + get_result_byte_str(retL)
value = int(value, 16)
if print_pos:
print(f"命令位置: {value}")
return value
def run_get_motor_position(ser, DEVICE_ADDRESS, print_pos=True) -> int:
retH = send_command(ser, create_command(DEVICE_ADDRESS, 0x03, 0x602C, 0x0001)) # 电机位置H
retL = send_command(ser, create_command(DEVICE_ADDRESS, 0x03, 0x602D, 0x0001)) # 电机位置L
value = get_result_byte_str(retH) + get_result_byte_str(retL)
value = int(value, 16)
if print_pos:
print(f"电机位置: {value}")
return value

View File

@@ -0,0 +1,44 @@
import time
from ..Utils import create_command, send_command
from .PositionControl import run_get_motor_position
def run_get_status(ser, DEVICE_ADDRESS, print_status=True) -> list[list[bytes]]:
# Bit0 故障
# Bit1 使能
# Bit2 运行
# Bit4 指令完成
# Bit5 路径完成
# Bit6 回零完成
ret = send_command(ser, create_command(DEVICE_ADDRESS, 0x03, 0x1003, 0x0001))
status = bin(int(ret.hex()[6:10], 16))[2:]
# 用0左位补齐
status = status.zfill(8)
status_info_list = []
if status[-1] == "1":
status_info_list.append("故障")
if status[-2] == "1":
status_info_list.append("使能")
if status[-3] == "1":
status_info_list.append("运行")
if status[-5] == "1":
status_info_list.append("指令完成")
if status[-6] == "1":
status_info_list.append("路径完成")
if status[-7] == "1":
status_info_list.append("回零完成")
if print_status:
print(f"获取状态: {' '.join(status_info_list)}")
return status_info_list
def run_until_status(ser, DEVICE_ADDRESS, status_info: str, max_time=10):
start_time = time.time()
while True:
ret = run_get_status(ser, DEVICE_ADDRESS)
if status_info in ret:
break
if time.time() - start_time > max_time:
print(f"状态未达到 {status_info} 超时")
return False
time.sleep(0.05)
return True

View File

@@ -0,0 +1,12 @@
from serial import Serial
from ..Consts import Config
from ..Utils import create_command, send_command
def create_elimate_warning_command(DEVICE_ADDRESS):
return create_command(DEVICE_ADDRESS, 0x06, 0x0145, 0x0087)
def run_elimate_warning(ser: Serial, DEVICE_ADDRESS):
send_command(ser, create_elimate_warning_command(DEVICE_ADDRESS))
print("清除警报")

View File

@@ -0,0 +1,126 @@
import os
import sys
from abc import abstractmethod
from typing import Optional
import serial
from .Consts import Config
from .FakeSerial import FakeSerial
from .Utils import run_commands
from .base_types.PrPath import PR_PATH
from .commands.PositionControl import run_get_command_position, run_set_forward_run, create_position_commands, \
create_position_run_command
from .commands.Warning import run_elimate_warning
try:
from unilabos.utils.pywinauto_util import connect_application, get_process_pid_by_name, get_ui_path_with_window_specification, print_wrapper_identifiers
from unilabos.device_comms.universal_driver import UniversalDriver, SingleRunningExecutor
from unilabos.device_comms import universal_driver as ud
except Exception as e:
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")))
from unilabos.utils.pywinauto_util import connect_application, get_process_pid_by_name, get_ui_path_with_window_specification, print_wrapper_identifiers
from unilabos.device_comms.universal_driver import UniversalDriver, SingleRunningExecutor
from unilabos.devices.template_driver import universal_driver as ud
print(f"使用文件DEBUG运行: {e}")
class iCL42Driver(UniversalDriver):
_ser: Optional[serial.Serial | FakeSerial] = None
_motor_position: Optional[int] = None
_DEVICE_COM: Optional[str] = None
_DEVICE_ADDRESS: Optional[int] = None
# 运行状态
_is_executing_run: bool = False
_success: bool = False
@property
def motor_position(self) -> int:
return self._motor_position
@property
def is_executing_run(self) -> bool:
return self._is_executing_run
@property
def success(self) -> bool:
return self._success
def init_device(self):
# 配置串口参数
if Config.OPEN_DEVICE:
self._ser = serial.Serial(
port=self._DEVICE_COM, # 串口号
baudrate=38400, # 波特率
bytesize=serial.EIGHTBITS, # 数据位
parity=serial.PARITY_NONE, # 校验位 N-无校验
stopbits=serial.STOPBITS_TWO, # 停止位
timeout=1 # 超时时间
)
else:
self._ser = FakeSerial()
def run_motor(self, mode: str, position: float, velocity: int):
if self._ser is None:
print("Device is not initialized")
self._success = False
return False
def post_func(res, _):
self._success = res
if not res:
self._is_executing_run = False
ins: SingleRunningExecutor = SingleRunningExecutor.get_instance(
self.execute_run_motor, post_func
)
# if not ins.is_ended and ins.is_started:
# print("Function is running")
# self._success = False
# return False
# elif not ins.is_started:
# print("Function started")
# ins.start() # 开始执行
# else:
# print("Function reset and started")
ins.reset()
ins.start(mode=mode, position=position, velocity=velocity)
def execute_run_motor(self, mode: str, position: float, velocity: int):
position = int(position * 1000)
PR = 0
run_elimate_warning(self._ser, self._DEVICE_ADDRESS)
run_set_forward_run(self._ser, self._DEVICE_ADDRESS)
run_commands(
self._ser, 0.1,
create_position_commands(self._DEVICE_ADDRESS, PR, PR_PATH[mode], position, velocity), # 41.8cm 21.8cm
create_position_run_command(self._DEVICE_ADDRESS, PR),
)
# if run_until_status(self._ser, self._DEVICE_ADDRESS, "路径完成"):
# pass
def __init__(self, device_com: str = "COM9", device_address: int = 0x01):
self._DEVICE_COM = device_com
self._DEVICE_ADDRESS = device_address
self.init_device()
# 启动所有监控器
self.checkers = [
# PositionChecker(self, 1),
]
for checker in self.checkers:
checker.start_monitoring()
@abstractmethod
class DriverChecker(ud.DriverChecker):
driver: iCL42Driver
class PositionChecker(DriverChecker):
def check(self):
# noinspection PyProtectedMember
if self.driver._ser is not None:
# noinspection PyProtectedMember
self.driver._motor_position = run_get_command_position(self.driver._ser, self.driver._DEVICE_ADDRESS)
# 示例用法
if __name__ == "__main__":
driver = iCL42Driver("COM3")
driver._is_executing_run = True

View File

@@ -0,0 +1,79 @@
# 使用pyserial进行485的协议通信端口指定为COM4
import serial
from serial.rs485 import RS485Settings
import traceback
from Consts import Config
from FakeSerial import FakeSerial
from base_types.PrPath import PR_PATH
from Utils import run_commands
from commands.PositionControl import create_position_commands, create_position_run_command, run_get_command_position, run_get_motor_position, run_set_forward_run
from commands.Status import run_get_status, run_until_status
from commands.Warning import run_elimate_warning
from Grasp import EleGripper
DEVICE_ADDRESS = Config.DEVICE_ADDRESS
# 配置串口参数
if Config.OPEN_DEVICE:
ser = serial.Serial(
port='COM11', # 串口号
baudrate=38400, # 波特率
bytesize=serial.EIGHTBITS, # 数据位
parity=serial.PARITY_NONE, # 校验位 N-无校验
stopbits=serial.STOPBITS_TWO, # 停止位
timeout=1 # 超时时间
)
else:
ser = FakeSerial()
# RS485模式支持如果需要
try:
ser.rs485_mode = RS485Settings(
rts_level_for_tx=True,
rts_level_for_rx=False,
# delay_before_tx=0.01,
# delay_before_rx=0.01
)
except AttributeError:
traceback.print_exc()
print("RS485模式需要支持的硬件和pyserial版本")
# run_set_position_zero(ser, DEVICE_ADDRESS)
# api.get_running_state(ser, DEVICE_ADDRESS)
gripper = EleGripper("COM12")
gripper.init_gripper()
gripper.wait_for_gripper_init()
PR = 0
run_get_status(ser, DEVICE_ADDRESS)
run_elimate_warning(ser, DEVICE_ADDRESS)
run_set_forward_run(ser, DEVICE_ADDRESS)
run_commands(
ser, 0.1,
create_position_commands(DEVICE_ADDRESS, PR, PR_PATH.ABS_POS, 20 * 1000, 300), # 41.8cm 21.8cm
create_position_run_command(DEVICE_ADDRESS, PR),
)
if run_until_status(ser, DEVICE_ADDRESS, "路径完成"):
pass
gripper.gripper_move(210,127,255)
gripper.wait_for_gripper()
gripper.rotate_move_abs(135,10,255)
gripper.data_reader()
print(gripper.rot_data)
run_commands(
ser, 0.1,
create_position_commands(DEVICE_ADDRESS, PR, PR_PATH.ABS_POS, 30 * 1000, 300), # 41.8cm 21.8cm
create_position_run_command(DEVICE_ADDRESS, PR),
)
gripper.gripper_move(210,127,255)
gripper.wait_for_gripper()
gripper.rotate_move_abs(135,10,255)
gripper.data_reader()
# run_get_command_position(ser, DEVICE_ADDRESS)
# run_get_motor_position(ser, DEVICE_ADDRESS)
# ser.close()

View File

@@ -0,0 +1,295 @@
import time
import traceback
from typing import Optional
import requests
from pywinauto.application import WindowSpecification
from pywinauto.controls.uiawrapper import UIAWrapper
from pywinauto_recorder import UIApplication
from pywinauto_recorder.player import UIPath, click, focus_on_application, exists, find, FailedSearch
from unilabos.device_comms import universal_driver as ud
from unilabos.device_comms.universal_driver import UniversalDriver, SingleRunningExecutor
from unilabos.utils.pywinauto_util import connect_application, get_process_pid_by_name, \
get_ui_path_with_window_specification
class NivoDriver(UniversalDriver):
# 初始指定
_device_ip: str = None
# 软件状态检测
_software_enabled: bool = False
_software_pid: int = None
_http_service_available: bool = False
# 初始化状态
_is_initialized: bool = False
# 任务是否成功
_success: bool = False
# 运行状态
_is_executing_run: bool = False
_executing_ui_path: Optional[UIPath] = None
_executing_index: Optional[int] = None
_executing_status: Optional[str] = None
_total_tasks: Optional[list[str]] = None
_guide_app: Optional[UIApplication] = None
@property
def executing_status(self) -> str:
if self._total_tasks is None:
return f"无任务"
if self._executing_index is None:
return f"等待任务开始,总计{len(self._total_tasks)}".encode('utf-8').decode('utf-8')
else:
return f"正在执行第{self._executing_index + 1}/{len(self._total_tasks)}个任务,当前状态:{self._executing_status}".encode('utf-8').decode('utf-8')
@property
def device_ip(self) -> str:
return self._device_ip
@property
def success(self) -> bool:
return self._success
@property
def status(self) -> str:
return f"Software: {self._software_enabled}, HTTP: {self._http_service_available} Initialized: {self._is_initialized} Executing: {self._is_executing_run}"
def set_device_addr(self, device_ip_str):
self._device_ip = device_ip_str
print(f"Set device IP to: {self.device_ip}")
def run_instrument(self):
if not self._is_initialized:
print("Instrument is not initialized")
self._success = False
return False
def post_func(res, _):
self._success = res
if not res:
self._is_executing_run = False
ins: SingleRunningExecutor = SingleRunningExecutor.get_instance(self.execute_run_instrument, post_func)
if not ins.is_ended and ins.is_started:
print("Function is running")
self._success = False
return False
elif not ins.is_started:
print("Function started")
ins.start() # 开始执行
else:
print("Function reset and started")
ins.reset()
ins.start()
def execute_run_instrument(self):
process_found, process_pid = get_process_pid_by_name("Guide.exe", min_memory_mb=20)
if not process_found:
uiapp = connect_application(process=self._software_pid)
focus_on_application(uiapp)
ui_window: WindowSpecification = uiapp.app.top_window()
button: WindowSpecification = ui_window.child_window(title="xtpBarTop", class_name="XTPDockBar").child_window(
title="Standard", class_name="XTPToolBar").child_window(title="Protocol", control_type="Button")
click(button.wrapper_object())
for _ in range(5):
time.sleep(1)
process_found, process_pid = get_process_pid_by_name("Guide.exe", min_memory_mb=20)
if process_found:
break
if not process_found:
print("Guide.exe not found")
self._success = False
return False
uiapp = connect_application(process=process_pid)
self._guide_app = uiapp
focus_on_application(uiapp)
wrapper_object = uiapp.app.top_window().wrapper_object()
ui_path = get_ui_path_with_window_specification(wrapper_object)
self._executing_ui_path = ui_path
with ui_path:
try:
click(u"||Custom->||RadioButton-> Run||Text-> Run||Text")
except FailedSearch as e:
print(f"未找到Run按钮可能已经在执行了")
with UIPath(u"WAA.Guide.Guide.RunControlViewModel||Custom->||Custom"):
click(u"Start||Button")
with UIPath(u"WAA.Guide.Guide.RunControlViewModel||Custom->||Custom->||Group->WAA.Guide.Guide.StartupControlViewModel||Custom->||Custom->Ok||Button"):
while self._executing_index is None or self._executing_index == 0:
if exists(None, timeout=2):
click(u"Ok||Text")
print("Run Init Success!")
self._is_executing_run = True
return True
else:
print("Wait for Ok button")
def check_execute_run_status(self):
if not self._is_executing_run:
return False
if self._executing_ui_path is None:
return False
total_tasks = []
executing_index = 0
executing_status = ""
procedure_name_found = False
with self._executing_ui_path:
with UIPath(u"WAA.Guide.Guide.RunControlViewModel||Custom->||Custom"):
with UIPath("Progress||Group->||DataGrid"):
wrappered_object: UIAWrapper = find(timeout=0.5) # BUG: 在查找的时候会触发全局锁建议还是使用Process来检测
for custom_wrapper in wrappered_object.children():
if len(custom_wrapper.children()) == 1:
each_custom_wrapper = custom_wrapper.children()[0]
if len(each_custom_wrapper.children()) == 2:
if not procedure_name_found:
procedure_name_found = True
continue
task_wrapper = each_custom_wrapper.children()[0]
total_tasks.append(task_wrapper.window_text())
status_wrapper = each_custom_wrapper.children()[1]
status = status_wrapper.window_text()
if len(status) > 0:
executing_index = len(total_tasks) - 1
executing_status = status
try:
if self._guide_app is not None:
wrapper_object = self._guide_app.app.top_window().wrapper_object()
ui_path = get_ui_path_with_window_specification(wrapper_object)
with ui_path:
with UIPath("OK||Button"):
btn = find(timeout=1)
if btn is not None:
btn.set_focus()
click(btn, timeout=1)
self._is_executing_run = False
print("运行完成!")
except:
pass
self._executing_index = executing_index
self._executing_status = executing_status
self._total_tasks = total_tasks
return True
def initialize_instrument(self, force=False):
if not self._software_enabled:
print("Software is not opened")
self._success = False
return
if not self._http_service_available:
print("HTTP Server Not Available")
self._success = False
return
if self._is_initialized and not force:
print("Already Initialized")
self._success = True
return True
ins: SingleRunningExecutor = SingleRunningExecutor.get_instance(self.execute_initialize, lambda res, _: setattr(self, '_success', res))
if not ins.is_ended and ins.is_started:
print("Initialize is running")
self._success = False
return False
elif not ins.is_started:
print("Initialize started")
ins.start()
else: # 可能外面is_initialized被设置为False又进来重新初始化了
print("Initialize reset and started")
ins.reset()
ins.start()
return True
def execute_initialize(self, process=None) -> bool:
if process is None:
process = self._software_pid
try:
uiapp = connect_application(process=process)
ui_window: WindowSpecification = uiapp.app.top_window()
button = ui_window.child_window(title="xtpBarTop", class_name="XTPDockBar").child_window(
title="Standard", class_name="XTPToolBar").child_window(title="Initialize Instrument", control_type="Button")
focus_on_application(uiapp)
click(button.wrapper_object())
with get_ui_path_with_window_specification(ui_window):
with UIPath("Regex: (Initializing|Resetting|Perking).*||Window"):
# 检测窗口是否存在
for i in range(3):
try:
initializing_windows = exists(None, timeout=2)
break
except:
pass
print("window has recovered", initializing_windows)
time.sleep(5) # another wait
self._is_initialized = True
return True
except Exception as e:
print("An error occurred during initialization:")
traceback.print_exc()
return False
def __init__(self):
self._device_ip = "192.168.0.2"
# 启动所有监控器
self.checkers = [
ProcessChecker(self, 1),
HttpServiceChecker(self, 3),
RunStatusChecker(self, 1),
OkButtonChecker(self, 2) # 添加新的Checker
]
for checker in self.checkers:
checker.start_monitoring()
class DriverChecker(ud.DriverChecker):
driver: NivoDriver
class ProcessChecker(DriverChecker):
def check(self):
process_found, process_pid = get_process_pid_by_name("JANUS.exe", min_memory_mb=20)
self.driver._software_pid = process_pid
self.driver._software_enabled = process_found
if not process_found:
self.driver._is_initialized = False
class HttpServiceChecker(DriverChecker):
def check(self):
http_service_available = False
if self.driver.device_ip:
try:
response = requests.get(f"http://{self.driver.device_ip}", timeout=5)
http_service_available = response.status_code == 200
except requests.RequestException:
pass
self.driver._http_service_available = http_service_available
class RunStatusChecker(DriverChecker):
def check(self):
process_found, process_pid = get_process_pid_by_name("Guide.exe", min_memory_mb=20)
if not process_found:
self.driver._is_executing_run = False
return
self.driver.check_execute_run_status()
class OkButtonChecker(DriverChecker):
def check(self):
if not self.driver._is_executing_run or self.driver._guide_app is None:
return
# uiapp = connect_application(process=11276)
# self.driver._guide_app = uiapp
try:
ui_window: UIAWrapper = self.driver._guide_app.app.top_window()
btn: WindowSpecification = ui_window.child_window(title="OK", auto_id="2", control_type="Button")
if btn.exists(2):
click(btn.wrapper_object())
self.driver._is_executing_run = False
print("运行完成!")
except Exception as e:
# traceback.print_exc()
pass
# 示例用法
if __name__ == "__main__":
driver = NivoDriver()
driver.set_device_addr("192.168.0.2") # 设置设备 IP 地址
driver._is_executing_run = True

View File

@@ -0,0 +1,166 @@
import psutil
import pywinauto
from pywinauto_recorder import UIApplication
from pywinauto_recorder.player import UIPath, click, focus_on_application, exists, find, get_wrapper_path
from pywinauto.controls.uiawrapper import UIAWrapper
from pywinauto.application import WindowSpecification
from pywinauto import findbestmatch
import sys
import codecs
import os
import locale
import six
def connect_application(backend="uia", **kwargs):
app = pywinauto.Application(backend=backend)
app.connect(**kwargs)
top_window = app.top_window().wrapper_object()
native_window_handle = top_window.handle
return UIApplication(app, native_window_handle)
def get_ui_path_with_window_specification(obj):
return UIPath(get_wrapper_path(obj))
def get_process_pid_by_name(process_name: str, min_memory_mb: float = 0) -> tuple[bool, int]:
"""
通过进程名称和最小内存要求获取进程PID
Args:
process_name: 进程名称
min_memory_mb: 最小内存要求(MB)默认为0表示不检查内存
Returns:
tuple[bool, int]: (是否找到进程, 进程PID)
"""
process_found = False
process_pid = None
min_memory_bytes = min_memory_mb * 1024 * 1024 # 转换为字节
try:
for proc in psutil.process_iter(['name', 'pid', 'memory_info']):
try:
# 获取进程信息
proc_info = proc.info
if proc_info['name'] == process_name:
# 如果设置了内存限制,则检查内存
if min_memory_mb > 0:
memory_info = proc_info.get('memory_info')
if memory_info and memory_info.rss > min_memory_bytes:
process_found = True
process_pid = proc_info['pid']
break
else:
# 不检查内存,直接返回找到的进程
process_found = True
process_pid = proc_info['pid']
break
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
continue
except Exception as e:
print(f"获取进程信息时发生错误: {str(e)}")
return process_found, process_pid
def print_wrapper_identifiers(wrapper_object, depth=None, filename=None):
"""
打印控件及其子控件的标识信息
Args:
wrapper_object: UIAWrapper对象
depth: 打印的最大深度,None表示打印全部
filename: 输出文件名,None表示打印到控制台
"""
if depth is None:
depth = sys.maxsize
# 创建所有控件的列表(当前控件及其所有子代)
all_ctrls = [wrapper_object, ] + wrapper_object.descendants()
# 创建所有可见文本控件的列表
txt_ctrls = [ctrl for ctrl in all_ctrls if ctrl.can_be_label and ctrl.is_visible() and ctrl.window_text()]
# 构建唯一的控件名称字典
name_ctrl_id_map = findbestmatch.UniqueDict()
for index, ctrl in enumerate(all_ctrls):
ctrl_names = findbestmatch.get_control_names(ctrl, all_ctrls, txt_ctrls)
for name in ctrl_names:
name_ctrl_id_map[name] = index
# 反转映射关系(控件索引到名称列表)
ctrl_id_name_map = {}
for name, index in name_ctrl_id_map.items():
ctrl_id_name_map.setdefault(index, []).append(name)
def print_identifiers(ctrls, current_depth=1, log_func=print):
"""递归打印控件及其子代的标识信息"""
if len(ctrls) == 0 or current_depth > depth:
return
indent = (current_depth - 1) * u" | "
for ctrl in ctrls:
try:
ctrl_id = all_ctrls.index(ctrl)
except ValueError:
continue
ctrl_text = ctrl.window_text()
if ctrl_text:
# 将多行文本转换为单行
ctrl_text = ctrl_text.replace('\n', r'\n').replace('\r', r'\r')
output = indent + u'\n'
output += indent + u"{class_name} - '{text}' {rect}\n"\
"".format(class_name=ctrl.friendly_class_name(),
text=ctrl_text,
rect=ctrl.rectangle())
output += indent + u'{}'.format(ctrl_id_name_map[ctrl_id])
title = ctrl_text
class_name = ctrl.class_name()
auto_id = None
control_type = None
if hasattr(ctrl.element_info, 'automation_id'):
auto_id = ctrl.element_info.automation_id
if hasattr(ctrl.element_info, 'control_type'):
control_type = ctrl.element_info.control_type
if control_type:
class_name = None # 如果有control_type就不需要class_name
else:
control_type = None # 如果control_type为空,仍使用class_name
criteria_texts = []
recorder_texts = []
if title:
criteria_texts.append(u'title="{}"'.format(title))
recorder_texts.append(f"{title}")
if class_name:
criteria_texts.append(u'class_name="{}"'.format(class_name))
if auto_id:
criteria_texts.append(u'auto_id="{}"'.format(auto_id))
if control_type:
criteria_texts.append(u'control_type="{}"'.format(control_type))
recorder_texts.append(f"||{control_type}")
if title or class_name or auto_id:
output += u'\n' + indent + u'child_window(' + u', '.join(criteria_texts) + u')' + " / " + "".join(recorder_texts)
if six.PY3:
log_func(output)
else:
log_func(output.encode(locale.getpreferredencoding(), errors='backslashreplace'))
print_identifiers(ctrl.children(), current_depth + 1, log_func)
if filename is None:
print("Control Identifiers:")
print_identifiers([wrapper_object, ])
else:
log_file = codecs.open(filename, "w", locale.getpreferredencoding())
def log_func(msg):
log_file.write(str(msg) + os.linesep)
log_func("Control Identifiers:")
print_identifiers([wrapper_object, ], log_func=log_func)
log_file.close()

View File

View File

@@ -0,0 +1,394 @@
import asyncio
from asyncio import Event, Future, Lock, Task
from enum import Enum
from dataclasses import dataclass
from typing import Any, Union, Optional, overload
import serial.tools.list_ports
from serial import Serial
from serial.serialutil import SerialException
class RunzeSyringePumpMode(Enum):
Normal = 0
AccuratePos = 1
AccuratePosVel = 2
pulse_freq_grades = {
6000: "0" ,
5600: "1" ,
5000: "2" ,
4400: "3" ,
3800: "4" ,
3200: "5" ,
2600: "6" ,
2200: "7" ,
2000: "8" ,
1800: "9" ,
1600: "10",
1400: "11",
1200: "12",
1000: "13",
800 : "14",
600 : "15",
400 : "16",
200 : "17",
190 : "18",
180 : "19",
170 : "20",
160 : "21",
150 : "22",
140 : "23",
130 : "24",
120 : "25",
110 : "26",
100 : "27",
90 : "28",
80 : "29",
70 : "30",
60 : "31",
50 : "32",
40 : "33",
30 : "34",
20 : "35",
18 : "36",
16 : "37",
14 : "38",
12 : "39",
10 : "40",
}
class RunzeSyringePumpConnectionError(Exception):
pass
@dataclass(frozen=True, kw_only=True)
class RunzeSyringePumpInfo:
port: str
address: str = "1"
volume: float = 25000
mode: RunzeSyringePumpMode = RunzeSyringePumpMode.Normal
def create(self):
return RunzeSyringePumpAsync(self.port, self.address, self.volume, self.mode)
class RunzeSyringePumpAsync:
def __init__(self, port: str, address: str = "1", volume: float = 25000, mode: RunzeSyringePumpMode = None):
self.port = port
self.address = address
self.volume = volume
self.mode = mode
self.total_steps = self.total_steps_vel = 6000
try:
self._serial = Serial(
baudrate=9600,
port=port
)
except (OSError, SerialException) as e:
raise RunzeSyringePumpConnectionError from e
self._busy = False
self._closing = False
self._error_event = Event()
self._query_future = Future[Any]()
self._query_lock = Lock()
self._read_task: Optional[Task[None]] = None
self._run_future: Optional[Future[Any]] = None
self._run_lock = Lock()
def _adjust_total_steps(self):
self.total_steps = 6000 if self.mode == RunzeSyringePumpMode.Normal else 48000
self.total_steps_vel = 48000 if self.mode == RunzeSyringePumpMode.AccuratePosVel else 6000
async def _read_loop(self):
try:
while True:
self._receive((await asyncio.to_thread(lambda: self._serial.read_until(b"\n")))[3:-3])
except SerialException as e:
raise RunzeSyringePumpConnectionError from e
finally:
if not self._closing:
self._error_event.set()
if self._query_future and not self._query_future.done():
self._query_future.set_exception(RunzeSyringePumpConnectionError())
if self._run_future and not self._run_future.done():
self._run_future.set_exception(RunzeSyringePumpConnectionError())
@overload
async def _query(self, command: str, dtype: type[bool]) -> bool:
pass
@overload
async def _query(self, command: str, dtype: type[int]) -> int:
pass
@overload
async def _query(self, command: str, dtype = None) -> str:
pass
async def _query(self, command: str, dtype: Optional[type] = None):
async with self._query_lock:
if self._closing or self._error_event.is_set():
raise RunzeSyringePumpConnectionError
self._query_future = Future[Any]()
run = 'R' if not command.startswith("?") else ''
full_command = f"/{self.address}{command}{run}\r\n"
full_command_data = bytearray(full_command, 'ascii')
try:
await asyncio.to_thread(lambda: self._serial.write(full_command_data))
return self._parse(await asyncio.wait_for(asyncio.shield(self._query_future), timeout=2.0), dtype=dtype)
except (SerialException, asyncio.TimeoutError) as e:
self._error_event.set()
raise RunzeSyringePumpConnectionError from e
finally:
self._query_future = None
def _parse(self, data: bytes, dtype: Optional[type] = None):
response = data.decode()
if dtype == bool:
return response == "1"
elif dtype == int:
return int(response)
else:
return response
def _receive(self, data: bytes):
ascii_string = "".join(chr(byte) for byte in data)
was_busy = self._busy
self._busy = ((data[0] & (1 << 5)) < 1) or ascii_string.startswith("@")
if self._run_future and was_busy and not self._busy:
self._run_future.set_result(data)
if self._query_future:
self._query_future.set_result(data)
else:
raise Exception("Dropping data")
async def _run(self, command: str):
async with self._run_lock:
self._run_future = Future[Any]()
try:
await self._query(command)
while True:
await asyncio.sleep(0.5) # Wait for 0.5 seconds before polling again
status = await self.query_device_status()
if status == '`':
break
await asyncio.shield(self._run_future)
finally:
self._run_future = None
async def initialize(self):
response = await self._run("Z")
if self.mode:
self.set_step_mode(self.mode)
else:
# self.mode = RunzeSyringePumpMode.Normal
# # self.set_step_mode(self.mode)
self.mode = await self.query_step_mode()
return response
# Settings
async def set_baudrate(self, baudrate):
if baudrate == 9600:
return await self._run("U41")
elif baudrate == 38400:
return await self._run("U47")
else:
raise ValueError("Unsupported baudrate")
# Mode Settings and Queries
async def set_step_mode(self, mode: RunzeSyringePumpMode):
self.mode = mode
self._adjust_total_steps()
command = f"N{mode.value}"
return await self._run(command)
async def query_step_mode(self):
response = await self._query("?28")
status, mode = response[0], int(response[1])
self.mode = RunzeSyringePumpMode._value2member_map_[mode]
self._adjust_total_steps()
return self.mode
# Speed Settings and Queries
async def set_speed_grade(self, speed: Union[int, str]):
return await self._run(f"S{speed}")
async def set_speed_max(self, speed: float):
pulse_freq = int(speed / self.volume * self.total_steps_vel)
pulse_freq = min(6000, pulse_freq)
return await self._run(f"V{speed}")
async def query_speed_grade(self):
pulse_freq, speed = await self.query_speed_max()
g = "-1"
for freq, grade in pulse_freq_grades.items():
if pulse_freq >= freq:
g = grade
break
return g
async def query_speed_init(self):
response = await self._query("?1")
status, pulse_freq = response[0], int(response[1:])
speed = pulse_freq / self.total_steps_vel * self.volume
return pulse_freq, speed
async def query_speed_max(self):
response = await self._query("?2")
status, pulse_freq = response[0], int(response[1:])
speed = pulse_freq / self.total_steps_vel * self.volume
return pulse_freq, speed
async def query_speed_end(self):
response = await self._query("?3")
status, pulse_freq = response[0], int(response[1:])
speed = pulse_freq / self.total_steps_vel * self.volume
return pulse_freq, speed
# Operations
# Valve Setpoint and Queries
async def set_valve_position(self, position: Union[int, str]):
command = f"I{position}" if type(position) == int or ord(position) <= 57 else position.upper()
return await self._run(command)
async def query_valve_position(self):
response = await self._query("?6")
status, pos_valve = response[0], response[1].upper()
return pos_valve
# Plunger Setpoint and Queries
async def move_plunger_to(self, volume: float):
"""
Move to absolute volume (unit: μL)
Args:
volume (float): absolute position of the plunger, unit: μL
Returns:
None
"""
pos_step = int(volume / self.volume * self.total_steps)
return await self._run(f"A{pos_step}")
async def pull_plunger(self, volume: float):
"""
Pull a fixed volume (unit: μL)
Args:
volume (float): absolute position of the plunger, unit: μL
Returns:
None
"""
pos_step = int(volume / self.volume * self.total_steps)
return await self._run(f"P{pos_step}")
async def push_plunger(self, volume: float):
"""
Push a fixed volume (unit: μL)
Args:
volume (float): absolute position of the plunger, unit: μL
Returns:
None
"""
pos_step = int(volume / self.volume * self.total_steps)
return await self._run(f"D{pos_step}")
async def report_position(self):
response = await self._query("?0")
status, pos_step = response[0], int(response[1:])
return pos_step / self.total_steps * self.volume
async def query_plunger_position(self):
response = await self._query("?4")
status, pos_step = response[0], int(response[1:])
return pos_step / self.total_steps * self.volume
async def stop_operation(self):
return await self._run("T")
# Queries
async def query_device_status(self):
return await self._query("Q")
async def query_command_buffer_status(self):
return await self._query("?10")
async def query_backlash_position(self):
return await self._query("?12")
async def query_aux_input_status_1(self):
return await self._query("?13")
async def query_aux_input_status_2(self):
return await self._query("?14")
async def query_software_version(self):
return await self._query("?23")
async def wait_error(self):
await self._error_event.wait()
async def __aenter__(self):
await self.open()
return self
async def __aexit__(self, exc_type, exc, tb):
await self.close()
async def open(self):
if self._read_task:
raise RunzeSyringePumpConnectionError
self._read_task = asyncio.create_task(self._read_loop())
try:
await self.query_device_status()
except Exception:
await self.close()
raise
async def close(self):
if self._closing or not self._read_task:
raise RunzeSyringePumpConnectionError
self._closing = True
self._read_task.cancel()
try:
await self._read_task
except asyncio.CancelledError:
pass
finally:
del self._read_task
self._serial.close()
@staticmethod
def list():
for item in serial.tools.list_ports.comports():
yield RunzeSyringePumpInfo(port=item.device)

View File

@@ -0,0 +1,388 @@
import asyncio
from threading import Lock, Event
from enum import Enum
from dataclasses import dataclass
import time
from typing import Any, Union, Optional, overload
import serial.tools.list_ports
from serial import Serial
from serial.serialutil import SerialException
class RunzeSyringePumpMode(Enum):
Normal = 0
AccuratePos = 1
AccuratePosVel = 2
pulse_freq_grades = {
6000: "0" ,
5600: "1" ,
5000: "2" ,
4400: "3" ,
3800: "4" ,
3200: "5" ,
2600: "6" ,
2200: "7" ,
2000: "8" ,
1800: "9" ,
1600: "10",
1400: "11",
1200: "12",
1000: "13",
800 : "14",
600 : "15",
400 : "16",
200 : "17",
190 : "18",
180 : "19",
170 : "20",
160 : "21",
150 : "22",
140 : "23",
130 : "24",
120 : "25",
110 : "26",
100 : "27",
90 : "28",
80 : "29",
70 : "30",
60 : "31",
50 : "32",
40 : "33",
30 : "34",
20 : "35",
18 : "36",
16 : "37",
14 : "38",
12 : "39",
10 : "40",
}
class RunzeSyringePumpConnectionError(Exception):
pass
@dataclass(frozen=True, kw_only=True)
class RunzeSyringePumpInfo:
port: str
address: str = "1"
max_volume: float = 25.0
mode: RunzeSyringePumpMode = RunzeSyringePumpMode.Normal
def create(self):
return RunzeSyringePump(self.port, self.address, self.max_volume, self.mode)
class RunzeSyringePump:
def __init__(self, port: str, address: str = "1", max_volume: float = 25.0, mode: RunzeSyringePumpMode = None):
self.port = port
self.address = address
self.max_volume = max_volume
self.total_steps = self.total_steps_vel = 6000
self._status = "Idle"
self._mode = mode
self._max_velocity = 0
self._valve_position = "I"
self._position = 0
try:
# if port in serial_ports and serial_ports[port].is_open:
# self.hardware_interface = serial_ports[port]
# else:
# serial_ports[port] = self.hardware_interface = Serial(
# baudrate=9600,
# port=port
# )
self.hardware_interface = Serial(
baudrate=9600,
port=port
)
except (OSError, SerialException) as e:
# raise RunzeSyringePumpConnectionError from e
self.hardware_interface = port
self._busy = False
self._closing = False
self._error_event = Event()
self._query_lock = Lock()
self._run_lock = Lock()
def _adjust_total_steps(self):
self.total_steps = 6000 if self.mode == RunzeSyringePumpMode.Normal else 48000
self.total_steps_vel = 48000 if self.mode == RunzeSyringePumpMode.AccuratePosVel else 6000
def send_command(self, full_command: str):
full_command_data = bytearray(full_command, 'ascii')
response = self.hardware_interface.write(full_command_data)
time.sleep(0.05)
output = self._receive(self.hardware_interface.read_until(b"\n"))
return output
def _query(self, command: str):
with self._query_lock:
if self._closing:
raise RunzeSyringePumpConnectionError
run = 'R' if not "?" in command else ''
full_command = f"/{self.address}{command}{run}\r\n"
output = self.send_command(full_command)[3:-3]
return output
def _parse(self, data: bytes, dtype: Optional[type] = None):
response = data.decode()
if dtype == bool:
return response == "1"
elif dtype == int:
return int(response)
else:
return response
def _receive(self, data: bytes):
ascii_string = "".join(chr(byte) for byte in data)
was_busy = self._busy
self._busy = ((data[0] & (1 << 5)) < 1) or ascii_string.startswith("@")
return ascii_string
def _run(self, command: str):
with self._run_lock:
try:
response = self._query(command)
while True:
time.sleep(0.5) # Wait for 0.5 seconds before polling again
status = self.get_status()
if status == 'Idle':
break
finally:
pass
return response
def initialize(self):
print("Initializing Runze Syringe Pump")
response = self._run("Z")
# if self.mode:
# self.set_mode(self.mode)
# else:
# # self.mode = RunzeSyringePumpMode.Normal
# # self.set_mode(self.mode)
# self.mode = self.get_mode()
return response
# Settings
def set_baudrate(self, baudrate):
if baudrate == 9600:
return self._run("U41")
elif baudrate == 38400:
return self._run("U47")
else:
raise ValueError("Unsupported baudrate")
# Device Status
@property
def status(self) -> str:
return self._status
def _standardize_status(self, status_raw):
return "Idle" if status_raw == "`" else "Busy"
def get_status(self):
status_raw = self._query("Q")
self._status = self._standardize_status(status_raw)
return self._status
# Mode Settings and Queries
@property
def mode(self) -> int:
return self._mode
# def set_mode(self, mode: RunzeSyringePumpMode):
# self.mode = mode
# self._adjust_total_steps()
# command = f"N{mode.value}"
# return self._run(command)
# def get_mode(self):
# response = self._query("?28")
# status_raw, mode = response[0], int(response[1])
# self.mode = RunzeSyringePumpMode._value2member_map_[mode]
# self._adjust_total_steps()
# return self.mode
# Speed Settings and Queries
@property
def max_velocity(self) -> float:
return self._max_velocity
def set_max_velocity(self, velocity: float):
self._max_velocity = velocity
pulse_freq = int(velocity / self.max_volume * self.total_steps_vel)
pulse_freq = min(6000, pulse_freq)
return self._run(f"V{pulse_freq}")
def get_max_velocity(self):
response = self._query("?2")
status_raw, pulse_freq = response[0], int(response[1:])
self._status = self._standardize_status(status_raw)
self._max_velocity = pulse_freq / self.total_steps_vel * self.max_volume
return self._max_velocity
def set_velocity_grade(self, velocity: Union[int, str]):
return self._run(f"S{velocity}")
def get_velocity_grade(self):
response = self._query("?2")
status_raw, pulse_freq = response[0], int(response[1:])
g = "-1"
for freq, grade in pulse_freq_grades.items():
if pulse_freq >= freq:
g = grade
break
return g
def get_velocity_init(self):
response = self._query("?1")
status_raw, pulse_freq = response[0], int(response[1:])
self._status = self._standardize_status(status_raw)
velocity = pulse_freq / self.total_steps_vel * self.max_volume
return pulse_freq, velocity
def get_velocity_end(self):
response = self._query("?3")
status_raw, pulse_freq = response[0], int(response[1:])
self._status = self._standardize_status(status_raw)
velocity = pulse_freq / self.total_steps_vel * self.max_volume
return pulse_freq, velocity
# Operations
# Valve Setpoint and Queries
@property
def valve_position(self) -> str:
return self._valve_position
def set_valve_position(self, position: Union[int, str, float]):
if type(position) == float:
position = round(position / 120)
command = f"I{position}" if type(position) == int or ord(position) <= 57 else position.upper()
response = self._run(command)
self._valve_position = f"{position}" if type(position) == int or ord(position) <= 57 else position.upper()
return response
def get_valve_position(self) -> str:
response = self._query("?6")
status_raw, pos_valve = response[0], response[1].upper()
self._valve_position = pos_valve
self._status = self._standardize_status(status_raw)
return pos_valve
# Plunger Setpoint and Queries
@property
def position(self) -> float:
return self._position
def get_position(self):
response = self._query("?0")
status_raw, pos_step = response[0], int(response[1:])
self._status = self._standardize_status(status_raw)
return pos_step / self.total_steps * self.max_volume
def set_position(self, position: float, max_velocity: float = None):
"""
Move to absolute volume (unit: ml)
Args:
position (float): absolute position of the plunger, unit: ml
max_velocity (float): maximum velocity of the plunger, unit: ml/s
Returns:
None
"""
if max_velocity is not None:
self.set_max_velocity(max_velocity)
pulse_freq = int(max_velocity / self.max_volume * self.total_steps_vel)
pulse_freq = min(6000, pulse_freq)
velocity_cmd = f"V{pulse_freq}"
else:
velocity_cmd = ""
pos_step = int(position / self.max_volume * self.total_steps)
return self._run(f"{velocity_cmd}A{pos_step}")
def pull_plunger(self, volume: float):
"""
Pull a fixed volume (unit: ml)
Args:
volume (float): absolute position of the plunger, unit: mL
Returns:
None
"""
pos_step = int(volume / self.max_volume * self.total_steps)
return self._run(f"P{pos_step}")
def push_plunger(self, volume: float):
"""
Push a fixed volume (unit: ml)
Args:
volume (float): absolute position of the plunger, unit: mL
Returns:
None
"""
pos_step = int(volume / self.max_volume * self.total_steps)
return self._run(f"D{pos_step}")
def get_plunger_position(self):
response = self._query("?4")
status, pos_step = response[0], int(response[1:])
return pos_step / self.total_steps * self.max_volume
def stop_operation(self):
return self._run("T")
# Queries
def query_command_buffer_status(self):
return self._query("?10")
def query_backlash_position(self):
return self._query("?12")
def query_aux_input_status_1(self):
return self._query("?13")
def query_aux_input_status_2(self):
return self._query("?14")
def query_software_version(self):
return self._query("?23")
def wait_error(self):
self._error_event.wait()
def close(self):
if self._closing:
raise RunzeSyringePumpConnectionError
self._closing = True
self.hardware_interface.close()
@staticmethod
def list():
for item in serial.tools.list_ports.comports():
yield RunzeSyringePumpInfo(port=item.device)

View File

@@ -0,0 +1,51 @@
import time
import serial
class SolenoidValve:
def __init__(self, io_device_port: str):
self._status = "Idle"
self._valve_position = "OPEN"
self.io_device_port = io_device_port
try:
self.hardware_interface = serial.Serial(str(io_device_port), 9600, timeout=1)
except serial.SerialException:
# raise Exception(f"Failed to connect to the device at {io_device_port}")
self.hardware_interface = str(io_device_port)
@property
def status(self) -> str:
return self._status
@property
def valve_position(self) -> str:
return self._valve_position
def send_command(self, command):
self.hardware_interface.write(command)
def read_data(self):
return self.hardware_interface.read()
def get_valve_position(self) -> str:
self._valve_position = "OPEN" if self.read_data() else "CLOSED"
return self._valve_position
def set_valve_position(self, position):
self._status = "Busy"
self.send_command(1 if position == "OPEN" else 0)
time.sleep(5)
self._status = "Idle"
def open(self):
self._valve_position = "OPEN"
def close(self):
self._valve_position = "CLOSED"
def is_open(self):
return self._valve_position
def is_closed(self):
return not self._valve_position

View File

@@ -0,0 +1,38 @@
import time
class SolenoidValveMock:
def __init__(self, port: str = "COM6"):
self._status = "Idle"
self._valve_position = "OPEN"
@property
def status(self) -> str:
return self._status
@property
def valve_position(self) -> str:
return self._valve_position
def get_valve_position(self) -> str:
return self._valve_position
def set_valve_position(self, position):
self._status = "Busy"
time.sleep(5)
self._valve_position = position
time.sleep(5)
self._status = "Idle"
def open(self):
self._valve_position = "OPEN"
def close(self):
self._valve_position = "CLOSED"
def is_open(self):
return self._valve_position
def is_closed(self):
return not self._valve_position

View File

@@ -0,0 +1,31 @@
import time
class VacuumPumpMock:
def __init__(self, port: str = "COM6"):
self._status = "OPEN"
@property
def status(self) -> str:
return self._status
def get_status(self) -> str:
return self._status
def set_status(self, position):
time.sleep(5)
self._status = position
time.sleep(5)
def open(self):
self._status = "OPEN"
def close(self):
self._status = "CLOSED"
def is_open(self):
return self._status
def is_closed(self):
return not self._status

View File

View File

@@ -0,0 +1,198 @@
import json
import serial
import struct
import crcmod
import tkinter as tk
from tkinter import messagebox
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import time
class RamanObj:
def __init__(self, port_laser,port_ccd, baudrate_laser=9600, baudrate_ccd=921600):
self.port_laser = port_laser
self.port_ccd = port_ccd
self.baudrate_laser = baudrate_laser
self.baudrate_ccd = baudrate_ccd
self.success = False
self.status = "None"
self.ser_laser = serial.Serial( self.port_laser,
self.baudrate_laser,
bytesize=serial.EIGHTBITS,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
timeout=1)
self.ser_ccd = serial.Serial( self.port_ccd,
self.baudrate_ccd,
bytesize=serial.EIGHTBITS,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
timeout=1)
def laser_on_power(self, output_voltage_laser):
def calculate_crc(data):
"""
计算Modbus CRC校验码
"""
crc16 = crcmod.predefined.mkCrcFun('modbus')
return crc16(data)
device_id = 0x01 # 从机地址
register_address = 0x0298 # 寄存器地址
output_voltage = int(output_voltage_laser) # 输出值
# 将输出值转换为需要发送的格式
output_value = int(output_voltage) # 确保是整数
high_byte = (output_value >> 8) & 0xFF
low_byte = output_value & 0xFF
# 构造Modbus RTU数据包
function_code = 0x06 # 写单个寄存器
message = struct.pack('>BBHH', device_id, function_code, register_address, output_value)
crc = calculate_crc(message)
crc_bytes = struct.pack('<H', crc)
full_message = message + crc_bytes
try:
# 打开串口
self.ser_laser.write(full_message)
# 读取响应
response = self.ser_laser.read(8) # Modbus响应通常为8字节
print(f"接收到的响应: {response.hex().upper()}")
# 校验响应CRC
if len(response) >= 5:
response_crc = calculate_crc(response[:-2])
received_crc = struct.unpack('<H', response[-2:])[0]
if response_crc == received_crc:
print("响应CRC校验成功")
else:
print("响应CRC校验失败")
else:
print("响应长度不足,无法校验")
except Exception as e:
print(f"发生错误: {e}")
return
def ccd_time(self, int_time):
# 发送命令
def send_command(ser, command):
try:
ser.write(command)
print(f"\u53d1\u9001: {command.hex()}")
except Exception as e:
print(f"\u53d1\u9001\u5931\u8d25: {e}")
# 接收数据
def receive_data(ser, expected_bytes):
try:
data = ser.read(expected_bytes)
print(f"\u63a5\u6536: {data.hex()}")
return data
except Exception as e:
print(f"\u63a5\u6536\u5931\u8d25: {e}")
return None
# 读取CCD数据并绘图
def read_and_plot(serial_port, int_time):
# 发送数据清空指令
clear_command = bytes([0xAA, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x55])
send_command(serial_port, clear_command)
time.sleep(0.1)
clear_command = bytes([0xAA, 0x10, 0x00, 0x00, 0x00, 0x00, 0x10, 0x55])
send_command(serial_port, clear_command)
receive_data(serial_port, 4136)
time.sleep(0.1)
clear_command = bytes([0xAA, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x55])
send_command(serial_port, clear_command)
time.sleep(0.1)
# 发送数据读取指令
clear_command = bytes([0xAA, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x55])
send_command(serial_port, clear_command)
int_time = int(int_time)
time.sleep(int_time)
read_command = bytes([0xAA, 0x10, 0x00, 0x00, 0x00, 0x00, 0x10, 0x55])
send_command(serial_port, read_command)
# 接收 4136 字节的数据
data = receive_data(serial_port, 4136)
if not data or len(data) != 4136:
messagebox.showerror("\u9519\u8bef", "\u63a5\u6536\u6570\u636e\u5931\u8d25\u6216\u6570\u636e\u957f\u5ea6\u4e0d\u6b63\u786e")
return
# 将接收到的字节数据转换为 16 位无符号整型数组
values = [struct.unpack(">H", data[i:i + 2])[0] for i in range(0, len(data), 2)]
return values
try:
ser = self.ser_ccd
values = read_and_plot(ser, int_time) # 修正传递serial对象
print(f"\u8fd4\u56de\u503c: {values}")
return values
except Exception as e:
messagebox.showerror("\u9519\u8bef", f"\u4e32\u53e3\u521d\u59cb\u5316\u5931\u8d25: {e}")
return None
def raman_without_background(self,int_time, laser_power):
self.laser_on_power(0)
time.sleep(0.1)
ccd_data_background = self.ccd_time(int_time)
time.sleep(0.1)
self.laser_on_power(laser_power)
time.sleep(0.2)
ccd_data_total = self.ccd_time(int_time)
self.laser_on_power(0)
ccd_data = [x - y for x, y in zip(ccd_data_total, ccd_data_background)]
return ccd_data
def raman_without_background_average(self, sample_name , int_time, laser_power, average):
ccd_data = [0] * 4136
for i in range(average):
ccd_data_temp = self.raman_without_background(int_time, laser_power)
ccd_data = [x + y for x, y in zip(ccd_data, ccd_data_temp)]
ccd_data = [x / average for x in ccd_data]
#保存数据 用时间命名
t = time.strftime("%Y-%m-%d-%H-%M-%S-%MS", time.localtime())
folder_path = r"C:\auto\raman_data"
with open(f'{folder_path}/{sample_name}_{t}.txt', 'w') as f:
for i in ccd_data:
f.write(str(i) + '\n')
return ccd_data
def raman_cmd(self, command:str):
self.success = False
try:
cmd_dict = json.loads(command)
ccd_data = self.raman_without_background_average(**cmd_dict)
self.success = True
# return ccd_data
except Exception as e:
# return None
raise f"error: {e}"
if __name__ == "__main__":
raman = RamanObj(port_laser='COM20', port_ccd='COM2')
ccd_data = raman.raman_without_background_average('test44',0.5,3000,1)
plt.plot(ccd_data)
plt.xlabel("Pixel")
plt.ylabel("Intensity")
plt.title("Raman Spectrum")
plt.show()

View File

View File

@@ -0,0 +1,64 @@
import json
from serial import Serial
import time as time_
import threading
class RotavapOne:
def __init__(self, port, rate=9600):
self.ser = Serial(port,rate)
self.pump_state = 'h'
self.pump_time = 0
self.rotate_state = 'h'
self.rotate_time = 0
self.success = True
if not self.ser.is_open:
self.ser.open()
# self.main_loop()
t = threading.Thread(target=self.main_loop ,daemon=True)
t.start()
def cmd_write(self, cmd):
self.ser.write(cmd)
self.ser.read_all()
def set_rotate_time(self, time):
self.success = False
self.rotate_time = time
self.success = True
def set_pump_time(self, time):
self.success = False
self.pump_time = time
self.success = True
def set_timer(self, command):
self.success = False
timer = json.loads(command)
rotate_time = timer['rotate_time']
pump_time = timer['pump_time']
self.rotate_time = rotate_time
self.pump_time = pump_time
self.success = True
def main_loop(self):
param = ['rotate','pump']
while True:
for i in param:
if getattr(self, f'{i}_time') <= 0:
setattr(self, f'{i}_state','l')
else:
setattr(self, f'{i}_state','h')
setattr(self, f'{i}_time',getattr(self, f'{i}_time')-1)
cmd = f'1{self.pump_state}2{self.rotate_state}3l4l\n'
self.cmd_write(cmd.encode())
time_.sleep(1)
if __name__ == '__main__':
ro = RotavapOne(port='COM15')

View File

View File

@@ -0,0 +1,156 @@
import json
import serial
import time as systime
import threading
class SeparatorController:
"""Controls the operation of a separator device using serial communication.
This class manages the interaction with both an executor and a sensor through serial ports, allowing for stirring, settling, and extracting operations based on sensor data.
Args:
port_executor (str): The serial port for the executor device.
port_sensor (str): The serial port for the sensor device.
baudrate_executor (int, optional): The baud rate for the executor device. Defaults to 115200.
baudrate_sensor (int, optional): The baud rate for the sensor device. Defaults to 115200.
Attributes:
serial_executor (Serial): The serial connection to the executor device.
serial_sensor (Serial): The serial connection to the sensor device.
sensordata (float): The latest sensor data read from the sensor device.
success (bool): Indicates whether the last operation was successful.
status (str): The current status of the controller, which can be 'Idle', 'Stirring', 'Settling', or 'Extracting'.
"""
def __init__(
self,
port_executor: str,
port_sensor: str,
baudrate_executor: int = 115200,
baudrate_sensor: int = 115200,
):
self.serial_executor = serial.Serial(port_executor, baudrate_executor)
self.serial_sensor = serial.Serial(port_sensor, baudrate_sensor)
if not self.serial_executor.is_open:
self.serial_executor.open()
if not self.serial_sensor.is_open:
self.serial_sensor.open()
self.sensordata = 0.00
self.success = False
self.status = "Idle" # 'Idle', 'Stirring' ,'Settling' , 'Extracting',
systime.sleep(2)
t = threading.Thread(target=self.read_sensor_loop, daemon=True)
t.start()
def write(self, data):
self.serial_executor.write(data)
a = self.serial_executor.read_all()
def stir(self, stir_time: float = 10, stir_speed: float = 300, settling_time: float = 10):
"""Controls the stirring operation of the separator.
This function initiates a stirring process for a specified duration and speed, followed by a settling phase. It updates the status of the controller and communicates with the executor to perform the stirring action.
Args:
stir_time (float, optional): The duration for which to stir, in seconds. Defaults to 10.
stir_speed (float, optional): The speed of stirring, in RPM. Defaults to 300.
settling_time (float, optional): The duration for which to settle after stirring, in seconds. Defaults to 10.
Returns:
None
"""
self.success = False
start_time = systime.time()
self.status = "Stirring"
stir_speed_second = stir_speed / 60
cmd = f"G91 Z{stir_speed_second}\n"
cmd_data = bytearray(cmd, "ascii")
self.write(bytearray(f"$112={stir_speed_second}", "ascii"))
while self.status != "Idle":
# print(self.sensordata)
if self.status == "Stirring":
if systime.time() - start_time < stir_time:
self.write(cmd_data)
systime.sleep(1)
else:
self.status = "Settling"
start_time = systime.time()
elif self.status == "Settling":
if systime.time() - start_time > settling_time:
break
self.success = True
def valve_open(self, condition, value):
"""Opens the valve, then wait to close the valve based on a specified condition.
This function sends a command to open the valve and continuously monitors the sensor data until the specified condition is met. Once the condition is satisfied, it closes the valve and updates the status of the controller.
Args:
condition (str): The condition to be monitored, either 'delta' or 'time'.
value (float): The threshold value for the condition.
`delta > 0.05`, `time > 60`
Returns:
None
"""
if condition not in ["delta", "time"]:
raise ValueError("Invalid condition")
elif condition == "delta":
valve_position = 0.66
else:
valve_position = 0.8
self.write((f"G91 X{valve_position}\n").encode())
last = self.sensordata
start_time = systime.time()
while True:
data = self.sensordata
delta = abs(data - last)
time = systime.time() - start_time
if eval(f"{condition} > {value}"):
break
last = data
systime.sleep(0.05)
self.status = "Idle"
self.write((f"G91 X-{valve_position}\n").encode())
def valve_open_cmd(self,command:str):
self.success = False
try:
cmd_dict = json.loads(command)
self.valve_open(**cmd_dict)
self.success = True
except Exception as e:
raise f"error: {e}"
def read_sensor_loop(self):
while True:
msg = self.serial_sensor.readline()
ascii_string = "".join(chr(byte) for byte in msg)
# print(msg)
if ascii_string.startswith("A3"):
try:
self.sensordata = float(ascii_string.split(":")[1])
except:
return
self.serial_sensor.read_all()
systime.sleep(0.05)
if __name__ == "__main__":
e = SeparatorController(port_sensor="COM40", port_executor="COM41")
print(e.status)
e.stir(10, 720, 10)
e.valve_open("delta", 0.3)

View File

View File

@@ -0,0 +1,67 @@
import json
import serial
class Chiller():
# def xuanzheng_temp_set(tempset: int):
# 设置目标温度
def __init__(self, port,rate=9600):
self.T_set = 24
self.success = True
self.ser = serial.Serial(
port=port,
baudrate=rate,
bytesize=8,
parity='N',
stopbits=1,
timeout=1
)
def modbus_crc(self, data: bytes) -> bytes:
crc = 0xFFFF
for pos in data:
crc ^= pos
for _ in range(8):
if (crc & 0x0001) != 0:
crc >>= 1
crc ^= 0xA001
else:
crc >>= 1
return crc.to_bytes(2, byteorder='little')
def build_modbus_frame(self,device_address: int, function_code: int, register_address: int, value: int) -> bytes:
frame = bytearray([
device_address,
function_code,
(register_address >> 8) & 0xFF,
register_address & 0xFF,
(value >> 8) & 0xFF,
value & 0xFF
])
crc = self.modbus_crc(frame)
return frame + crc
def convert_temperature_to_modbus_value(self, temperature: float, decimal_points: int = 1) -> int:
factor = 10 ** decimal_points
value = int(temperature * factor)
return value & 0xFFFF
def set_temperature(self, command):
T_set = json.loads(command)['temperature']
self.T_set = int(T_set)
self.success = False
temperature_value = self.convert_temperature_to_modbus_value(self.T_set, decimal_points=1)
device_address = 0x01
function_code = 0x06
register_address = 0x000B
frame = self.build_modbus_frame(device_address, function_code, register_address, temperature_value)
self.ser.write(frame)
response = self.ser.read(8)
self.success = True
def stop(self):
self.set_temperature(25)
if __name__ == '__main__':
ch = Chiller(port='COM17')
ch.set_temperature(20)

View File

@@ -0,0 +1,69 @@
import serial
import struct
class TempSensor:
def __init__(self,port,baudrate=9600):
# 配置串口
self.ser = serial.Serial(
port=port,
baudrate=baudrate,
bytesize=serial.EIGHTBITS,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
timeout=1
)
def calculate_crc(self,data):
crc = 0xFFFF
for pos in data:
crc ^= pos
for i in range(8):
if (crc & 0x0001) != 0:
crc >>= 1
crc ^= 0xA001
else:
crc >>= 1
return crc
def build_modbus_request(self, device_id, function_code, register_address, register_count):
request = struct.pack('>BBHH', device_id, function_code, register_address, register_count)
crc = self.calculate_crc(request)
request += struct.pack('<H', crc)
return request
def read_modbus_response(self,response):
if len(response) < 5:
return None
data = response[:-2]
crc_received = struct.unpack('<H', response[-2:])[0]
crc_calculated = self.calculate_crc(data)
if crc_received == crc_calculated:
return data[3:]
return None
def send_command(self ,command):
function_code = 0x04 # 读输入寄存器
register_address = 0x0003 # 温度高位寄存器地址
register_count = 0x0002 # 读取两个寄存器
request = self.build_modbus_request(command, function_code, register_address, register_count)
self.ser.write(request)
response = self.ser.read(9) # 读取9字节的响应
data = self.read_modbus_response(response)
if data is None:
return None
high_value = struct.unpack('>H', data[:2])[0]
low_value = struct.unpack('>H', data[2:])[0]
# 组合高位和低位并计算实际温度
raw_temperature = (high_value << 16) | low_value
if raw_temperature & 0x8000: # 如果低位寄存器最高位为1表示负值
raw_temperature -= 0x10000 # 转换为正确的负数表示
actual_temperature = raw_temperature / 10.0
return actual_temperature

View File

@@ -0,0 +1,90 @@
import serial
import time
import struct
import tkinter as tk
from tkinter import ttk
# 配置串口
ser = serial.Serial(
port='COM13',
baudrate=9600,
bytesize=serial.EIGHTBITS,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
timeout=1
)
def calculate_crc(data):
crc = 0xFFFF
for pos in data:
crc ^= pos
for i in range(8):
if (crc & 0x0001) != 0:
crc >>= 1
crc ^= 0xA001
else:
crc >>= 1
return crc
def build_modbus_request(device_id, function_code, register_address, register_count):
request = struct.pack('>BBHH', device_id, function_code, register_address, register_count)
crc = calculate_crc(request)
request += struct.pack('<H', crc)
return request
def read_modbus_response(response):
if len(response) < 5:
return None
data = response[:-2]
crc_received = struct.unpack('<H', response[-2:])[0]
crc_calculated = calculate_crc(data)
if crc_received == crc_calculated:
return data[3:]
return None
def read_temperature():
device_id = 0xC0 # 设备地址
function_code = 0x04 # 读输入寄存器
register_address = 0x0003 # 温度高位寄存器地址
register_count = 0x0002 # 读取两个寄存器
request = build_modbus_request(device_id, function_code, register_address, register_count)
ser.write(request)
response = ser.read(9) # 读取9字节的响应
data = read_modbus_response(response)
if data is None:
return None
high_value = struct.unpack('>H', data[:2])[0]
low_value = struct.unpack('>H', data[2:])[0]
# 组合高位和低位并计算实际温度
raw_temperature = (high_value << 16) | low_value
if raw_temperature & 0x8000: # 如果低位寄存器最高位为1表示负值
raw_temperature -= 0x10000 # 转换为正确的负数表示
actual_temperature = raw_temperature / 10.0
return actual_temperature
def update_temperature_label():
temperature = read_temperature()
if temperature is not None:
temperature_label.config(text=f"反应温度: {temperature:.1f} °C")
else:
temperature_label.config(text="Error reading temperature")
root.after(1000, update_temperature_label) # 每秒更新一次
# 创建主窗口
root = tk.Tk()
root.title("反应温度监控")
# 创建标签来显示温度
temperature_label = ttk.Label(root, text="反应温度: -- °C", font=("Helvetica", 20))
temperature_label.pack(padx=20, pady=20)
# 开始更新温度
update_temperature_label()
# 运行主循环
root.mainloop()

View File

@@ -0,0 +1,117 @@
import json
import threading,time
# class TempSensorNode:
# def __init__(self,port,warning,address):
# self._value = 0.0
# self.warning = warning
# self.device_id = address
# self.hardware_interface = port
# # t = threading.Thread(target=self.read_temperature, daemon=True)
# # t.start()
# def send_command(self ,command):
# print('send_command---------------------')
# pass
# @property
# def value(self) -> float:
# self._value = self.send_command(self.device_id)
# return self._value
# # def read_temperature(self):
# # while True:
# # self.value = self.send_command(self.device_id)
# # print(self.value,'-----------')
# # time.sleep(1)
# def set_warning(self, warning_temp):
# self.warning = warning_temp
import serial
import struct
from rclpy.node import Node
import rclpy
import threading
class TempSensorNode():
def __init__(self,port,warning,address,baudrate=9600):
self._value = 0.0
self.warning = warning
self.device_id = address
self.success = False
# 配置串口
self.hardware_interface = serial.Serial(
port=port,
baudrate=baudrate,
bytesize=serial.EIGHTBITS,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
timeout=1
)
self.lock = threading.Lock()
def calculate_crc(self,data):
crc = 0xFFFF
for pos in data:
crc ^= pos
for i in range(8):
if (crc & 0x0001) != 0:
crc >>= 1
crc ^= 0xA001
else:
crc >>= 1
return crc
def build_modbus_request(self, device_id, function_code, register_address, register_count):
request = struct.pack('>BBHH', device_id, function_code, register_address, register_count)
crc = self.calculate_crc(request)
request += struct.pack('<H', crc)
return request
def read_modbus_response(self,response):
if len(response) < 5:
return None
data = response[:-2]
crc_received = struct.unpack('<H', response[-2:])[0]
crc_calculated = self.calculate_crc(data)
if crc_received == crc_calculated:
return data[3:]
return None
def send_prototype_command(self ,command):
# with self.lock:
function_code = 0x04 # 读输入寄存器
register_address = 0x0003 # 温度高位寄存器地址
register_count = 0x0002 # 读取两个寄存器
request = self.build_modbus_request(command, function_code, register_address, register_count)
self.hardware_interface.write(request)
response = self.hardware_interface.read(9) # 读取9字节的响应
data = self.read_modbus_response(response)
if data is None:
return None
high_value = struct.unpack('>H', data[:2])[0]
low_value = struct.unpack('>H', data[2:])[0]
# 组合高位和低位并计算实际温度
raw_temperature = (high_value << 16) | low_value
if raw_temperature & 0x8000: # 如果低位寄存器最高位为1表示负值
raw_temperature -= 0x10000 # 转换为正确的负数表示
actual_temperature = raw_temperature / 10.0
return actual_temperature
@property
def value(self) -> float:
self._value = self.send_prototype_command(self.device_id)
return self._value
def set_warning(self, command):
self.success = False
temp = json.loads(command)["warning_temp"]
self.warning = round(float(temp), 1)
self.success = True

View File