mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2025-12-19 05:51:17 +00:00
Ready for open source (#47)
* Create app/main API * create example device * create ROS backend and example device SDK Wrapper * Add ROS host and host starting from app.py * Add gripper device and mock implementation * add "status_types" & "action_types" to ROS device decorator * add ActionServer debug example * [bugfix] complete mock gripper example * ROS Backend Host for Device action calling and Resource management * add conda/mamba ENV file * add host_node communication with app/main.py * add action message value mappings and converters * Update ilabos.yaml * Update issue templates * example devices.json and resources.json * Fix Device wrapper to use async property and actions (#7) * Fix Device wrapper to use async property and actions * Resolve #1 : support async get methods and actions. Give new examples. * add both sync/async GRBL controller SDK * 2 call device actions from appmainpy api to ros hostpy (#8) * feature: add job * fix:node start * feature:add get job status * fix:get device * clean * Resolve #5 device connection diagram and workflow compilation support (#9) * add syringe pump device and its compilation using device connection diagram * add RunzeSyringePump real device with ROS2 example * Prototype machine with 1 pump and 1 CNC * add ROS2ProtocolNode and related functions * add ilabos_msgs (to use PumpTransfer action) * add example device connection graph * refactor protocol_node code into separate file * add ROS2SerialNode * add SerialCommand srv in ilabos_msgs * add pump_protocol example, and fix bugs * [fix] serial service: avoid async service deadlock by directly call serial `send_command` * use SendCmd instead of SingleJointPosition for valve control * initialize device connection graph when server starts * Fix #5: async workflow execution (#16) * add rclpyx and protocol example for async-native workflow * use async in ROS2ProtocolNode, and host initialization * add examples of "ros-async" protocol implementation, and `run_in_event_loop` for using native async functions * use "ros-async" in protocols and device nodes * fix pump_protocol: push to 0.0 μL * Envs, docs, and conda recipes (#19) * update ENV: use python 3.11 and deprecate ros-humble-gazebo-ros * add ilabos-msgs conda recipe * Update ilabos.yaml * fix recipe and env yaml * Add sphinx docs * add aichemeco * add bioyong * add bioyong * Support XDL devices & protocols (#20) * [Feature] support multiple protocols in a single ProtocolNode * add Junjie's code * Support "Clean" protocol * Update Grignard_flow_batchreact_single_pumpvalve.json * test_grignard_add * add stir device node and example * Update device_node.py add print_publish flag to control the node's log output * NH4Cl_add * add "HeaterStirrer" device and "HeatChill" action * add wait time after each pump action for equilibration * fix stir * add Separate protocol * Refactor Separator device and Stir action * add rotavap_node * fix stir * add chiller node * Move rinsings into PumpTransfer * Fix SeparateProtocol under refactored Separator device and Stir action * Supports automatically add new protocol action_types * fix PumpTransfer protocol because of rinsing * Add Rotavap and Chiller devices * fix SeparateProtocol * add EvaporateProtocol * add rotavap devices config * fix HeaterStirrer and SeparatorController IO * Fix automatically add new protocol action_types * Add HeaterStirrer and SeparatorController device config * fix pump protocols * Fix Evaporate action * Update evaporate_protocol.py * add temp_sensor node and add function remap * update docs --------- Co-authored-by: 王俊杰 <1800011822@pku.edu.cn> Co-authored-by: q434343 <554662886@qq.com> * fix aichemeco * [Bugfix] fix Windows conda packaging * add file upload api * update dependencies: force to use 3.11 and remove conflict in WIN64 and OSX64 * update dependencies: force to use 3.11 and remove conflict in WIN64 and OSX64 * Create aichemeco_simple.py * fix * update * add aichemeco file * MQTT [1/2]: action start (#25) * add mq * fix * clean * add class * fix excel * update bioyong * add api * fix --------- Co-authored-by: caok@dp.tech <xiaoyeqiannian@163.com> * motor & grasp * Add Grasp motor support and enhance EleGripper class - Introduced a new motor configuration for Grasp in sjtu.json. - Updated EleGripper class to inherit from UniversalDriver and added status property. - Implemented move_and_rotate method for coordinated movement. - Adjusted threading logic in EleGripper initialization. - Registered Grasp motor in ROS2 device node configuration. This commit enhances the motor control capabilities by integrating the Grasp motor and improving the existing EleGripper functionality. * fix read data lenth * Update Grasp.py * MQTT (2/2): publish Device Status, Action Feedback & Results (#27) * Add bridges in HostNode for device_status publishing * Add "bridges" selection (fastapi & mqtt) when app start * add MQ feedback & result publisher, and fix message converter * fix UUID converting between ROS and MQ * lint api model.py * Continuous controllers: PID, MPC, custom controllers etc. (#23) * add controller config & wrapper * add controller setup at app.main * control loop example * fix com port * add agv , ur_arm and raman * MQTT (3/4): Unified Resources and Sync when starting the server (#28) * update http upload api * generate uuid when init device * example resource json * fix * add new example full-content json (device, resource, graph) * fix full-content json and related reading code * fix * add json_schema when initialize resources * fix * update schema * refactor heaterstirrer.dalong * fix * fix refactor heaterstirrer.dalong * refactor syringepump.runze: use ml instead of μL * Update ilabos/ros/host.py Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --------- Co-authored-by: 王俊杰 <1800011822@pku.edu.cn> Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Distributed initialization with self-organizing network (#29) * add distributed launching option "--without_host" * fix --------- Co-authored-by: 王俊杰 <1800011822@pku.edu.cn> * Refactor Workstation: Add resource service and tracking (#30) * move ilabos/ros/rpc to ilabos/device_comms/rpc, and merge bioyond/aichemeco files under /devices * add Resource srv and message_converter * move graphio to ilabos/resources * refactor resources type conversion * add resource clients in device_node * add mock resources service * pass Gripper1 resource test * update http resource services * add AGV compile function * add AGV transfer protocol * update full mock_gripper edit_id example * update full mock_gripper edit_id example * get and update resource also in protocol_node * mock resource update in AichemecoHiwo * Create HT_hiwo.json * add children in resources * bugfixes * fix rpc * add Revvity winprep --------- Co-authored-by: wjjxxx <43375851+wjjxxx@users.noreply.github.com> Co-authored-by: 3218923350 <105201755+3218923350@users.noreply.github.com> * Distributed launch (2/2): distributed resource create (#32) * add resource_add request to host for slave mode * add AGV * fix protocol resources * optimize host callbacks * bugfixes * add revvity registry --------- Co-authored-by: 王俊杰 <1800011822@pku.edu.cn> Co-authored-by: wjjxxx <43375851+wjjxxx@users.noreply.github.com> * Refactor Driver Files Structure (#33) * Integration with pywinauto & recorder Added execute run and initialize procdure * 酶标仪状态检测、使用示例,整体流程待测试 * nivo ready version * Add HPLC driver and example script - Introduced HPLCDriver class for managing HPLC device status and operations. - Implemented device status monitoring and command execution via ROS2 actions. - Added example script (hplc.py) demonstrating how to run commands on the HPLC device. - Created PlayerUtil and UniversalDriver classes for shared functionality across devices. - Refactored NivoDriver to utilize the new UniversalDriver structure. - Enhanced error handling and process management in the NivoDriver. * 修复start的错误定位 * hplc tested ok * relative path to build msgs * template_driver & jiageng devices * fetch correct status type and action type * fix mtype fetch * gpc bus integration * ilab build * remove chs * recipe rename * modbus update 1 * json available * hplc & modbus rewrite * Update AgilentHPLC.py hplc datafile reader * move ilabos/ros/rpc to ilabos/device_comms/rpc, and merge bioyond/aichemeco files under /devices * modbus分设备 * gpc * gpc 2 * fix address * default register node * fix MainScreenGPC * add Resource srv and message_converter * move graphio to ilabos/resources * refactor resources type conversion * add resource clients in device_node * add mock resources service * pass Gripper1 resource test * update http resource services * add AGV compile function * add AGV transfer protocol * update recipe.yaml * update full mock_gripper edit_id example * update full mock_gripper edit_id example * get and update resource also in protocol_node * mock resource update in AichemecoHiwo * feat: add other jiageng PLC device code * ilabos compile * correct format * correct recipe format * correct setup.py format * remove unnecessary files * remove unnecessary files * Create HT_hiwo.json * add children in resources * hplc support sample_id * correct hplc sample_id * correct hplc sample_id * hplc upload * fix type hint * oss upload tested ver * recipe yaml fix for linux * update installation yaml * refactor: moved all driver files according to its feat * merge main to dev --------- Co-authored-by: 王俊杰 <2201110460@stu.pku.edu.cn> Co-authored-by: Junhan Chang <changjh@pku.edu.cn> Co-authored-by: jiawei <miaojiawei@dp.tech> * add: NMR LH and RU device control (#34) * Add Registry for device drivers and Support GraphML (#35) * read chemputer graphml * read graphml in app/main * add devices in ros/devices * add schema for devices * read registry directory and initialize when entry from main * Delete devices.py * Update add_protocol.md * delete unecessary files * feat: 2278 devices registry yaml (#36) * read chemputer graphml * read graphml in app/main * add devices in ros/devices * add schema for devices * read registry directory and initialize when entry from main * Delete devices.py * add: NMR LH and RU device control * fix: modify jiageng devices registry --------- Co-authored-by: Junhan Chang <changjh@pku.edu.cn> * Device/Resource Registry and GraphML support (#37) * add resource type conversion to PLR * add resource registry and test * add docs * fix registry * add solenoid_valve_mock, its registry and test * fix registry for directly using examples * add EvacuateAndRefillProtocol and testcases * allow function sequence call in ACTION * add read & write & extra_info for hardware_interface * Update device_node.py * add solenoid valve * add doc developer guide yaml * fixes for starting IK station * add graphml grouping parser * fix graphml grouping parser * add communication edge parser * fix io solenoid valve * Update .gitignore * Update plates.yaml --------- Co-authored-by: ColumbiaCC <2100011801@stu.pku.edu.cn> * Uni-Lab Doc v0.2 (#39) * add Uni-Lab docs * change doc name * Dev (#41) * Integration with pywinauto & recorder Added execute run and initialize procdure * 酶标仪状态检测、使用示例,整体流程待测试 * nivo ready version * Add HPLC driver and example script - Introduced HPLCDriver class for managing HPLC device status and operations. - Implemented device status monitoring and command execution via ROS2 actions. - Added example script (hplc.py) demonstrating how to run commands on the HPLC device. - Created PlayerUtil and UniversalDriver classes for shared functionality across devices. - Refactored NivoDriver to utilize the new UniversalDriver structure. - Enhanced error handling and process management in the NivoDriver. * 修复start的错误定位 * hplc tested ok * relative path to build msgs * template_driver & jiageng devices * fetch correct status type and action type * fix mtype fetch * gpc bus integration * ilab build * remove chs * recipe rename * modbus update 1 * json available * hplc & modbus rewrite * Update AgilentHPLC.py hplc datafile reader * move ilabos/ros/rpc to ilabos/device_comms/rpc, and merge bioyond/aichemeco files under /devices * modbus分设备 * gpc * gpc 2 * fix address * default register node * fix MainScreenGPC * add Resource srv and message_converter * move graphio to ilabos/resources * refactor resources type conversion * add resource clients in device_node * add mock resources service * pass Gripper1 resource test * update http resource services * add AGV compile function * add AGV transfer protocol * update recipe.yaml * update full mock_gripper edit_id example * update full mock_gripper edit_id example * get and update resource also in protocol_node * mock resource update in AichemecoHiwo * feat: add other jiageng PLC device code * ilabos compile * correct format * correct recipe format * correct setup.py format * remove unnecessary files * remove unnecessary files * Create HT_hiwo.json * add children in resources * hplc support sample_id * correct hplc sample_id * correct hplc sample_id * hplc upload * fix type hint * oss upload tested ver * recipe yaml fix for linux * update installation yaml * refactor: moved all driver files according to its feat * merge main to dev * add HPLC registry and json * 升级 ros2-distro-mutex 依赖版本至 0.6 * 修改 ros2-distro-mutex 依赖版本为通配符匹配 * 更新 ros-humble-ilabos-msgs 依赖为 robostack-humble 命名空间 * add resource type conversion to PLR * add resource registry and test * feat: 更新oss上传 * fix device id * add docs * fix registry * add solenoid_valve_mock, its registry and test * fix registry for directly using examples * add EvacuateAndRefillProtocol and testcases * allow function sequence call in ACTION * add read & write & extra_info for hardware_interface * Update device_node.py * add solenoid valve * add doc developer guide yaml * use robostack-staging * rclpy version test * lower rclpy * ensure 0.6* env * fixes for starting IK station * add graphml grouping parser * fix graphml grouping parser * add communication edge parser * fix io solenoid valve * Update .gitignore * Update plates.yaml * Feature/device node later init (#40) * 修改config路径,方便后续打包 增加device_node打印 * 支持plr序列化/init创建 * 统一命名 * import mgr logger optimize banner print * 日志OK * fix unicorn frame * banner print * correct import format * file path changes * 取消后补全,在加载设备的时候直接替换 * converter update * web page update * 在线device更新,node继承替换 * 修复动作、状态的类型缺失 和 命令提示 * web功能实现结束 * host节点更改完成 新增status时间戳管理 新增每10s动态发现其他node * ros2类型的节点也应该被包一次 * 修复类型提示 * websocket 动态显示状态 * add workflow & book theme for docs * add workflow & book theme for docs * fix workflow build * fix workflow build * 理清启动关系 * stm32 example * mac . name * device_instance device_cls * 新增config添加方式 更新mqtt提示 * plr test * 移动is_host_mode 新增slave_no_host * 确保config优先修改生效 * fix graph io * 支持带参数传入 * 支持物料解析 * 支持物料解析 * device为空的时候不进行绑定或初始化 * protocol node new * protocol node runnable * protocol node runnable --------- Co-authored-by: 王俊杰 <2201110460@stu.pku.edu.cn> Co-authored-by: Junhan Chang <changjh@pku.edu.cn> Co-authored-by: jiawei <miaojiawei@dp.tech> Co-authored-by: ColumbiaCC <2100011801@stu.pku.edu.cn> * Dev (#45) * Integration with pywinauto & recorder Added execute run and initialize procdure * 酶标仪状态检测、使用示例,整体流程待测试 * nivo ready version * Add HPLC driver and example script - Introduced HPLCDriver class for managing HPLC device status and operations. - Implemented device status monitoring and command execution via ROS2 actions. - Added example script (hplc.py) demonstrating how to run commands on the HPLC device. - Created PlayerUtil and UniversalDriver classes for shared functionality across devices. - Refactored NivoDriver to utilize the new UniversalDriver structure. - Enhanced error handling and process management in the NivoDriver. * 修复start的错误定位 * hplc tested ok * relative path to build msgs * template_driver & jiageng devices * fetch correct status type and action type * fix mtype fetch * gpc bus integration * ilab build * remove chs * recipe rename * modbus update 1 * json available * hplc & modbus rewrite * Update AgilentHPLC.py hplc datafile reader * move ilabos/ros/rpc to ilabos/device_comms/rpc, and merge bioyond/aichemeco files under /devices * modbus分设备 * gpc * gpc 2 * fix address * default register node * fix MainScreenGPC * add Resource srv and message_converter * move graphio to ilabos/resources * refactor resources type conversion * add resource clients in device_node * add mock resources service * pass Gripper1 resource test * update http resource services * add AGV compile function * add AGV transfer protocol * update recipe.yaml * update full mock_gripper edit_id example * update full mock_gripper edit_id example * get and update resource also in protocol_node * mock resource update in AichemecoHiwo * feat: add other jiageng PLC device code * ilabos compile * correct format * correct recipe format * correct setup.py format * remove unnecessary files * remove unnecessary files * Create HT_hiwo.json * add children in resources * hplc support sample_id * correct hplc sample_id * correct hplc sample_id * hplc upload * fix type hint * oss upload tested ver * recipe yaml fix for linux * update installation yaml * refactor: moved all driver files according to its feat * merge main to dev * add HPLC registry and json * 升级 ros2-distro-mutex 依赖版本至 0.6 * 修改 ros2-distro-mutex 依赖版本为通配符匹配 * 更新 ros-humble-ilabos-msgs 依赖为 robostack-humble 命名空间 * add resource type conversion to PLR * add resource registry and test * feat: 更新oss上传 * fix device id * add docs * fix registry * add solenoid_valve_mock, its registry and test * fix registry for directly using examples * add EvacuateAndRefillProtocol and testcases * allow function sequence call in ACTION * add read & write & extra_info for hardware_interface * Update device_node.py * add solenoid valve * add doc developer guide yaml * use robostack-staging * rclpy version test * lower rclpy * ensure 0.6* env * fixes for starting IK station * add graphml grouping parser * fix graphml grouping parser * add communication edge parser * fix io solenoid valve * Update .gitignore * Update plates.yaml * Feature/device node later init (#40) * 修改config路径,方便后续打包 增加device_node打印 * 支持plr序列化/init创建 * 统一命名 * import mgr logger optimize banner print * 日志OK * fix unicorn frame * banner print * correct import format * file path changes * 取消后补全,在加载设备的时候直接替换 * converter update * web page update * 在线device更新,node继承替换 * 修复动作、状态的类型缺失 和 命令提示 * web功能实现结束 * host节点更改完成 新增status时间戳管理 新增每10s动态发现其他node * ros2类型的节点也应该被包一次 * 修复类型提示 * websocket 动态显示状态 * add workflow & book theme for docs * add workflow & book theme for docs * fix workflow build * fix workflow build * 理清启动关系 * stm32 example * mac . name * device_instance device_cls * 新增config添加方式 更新mqtt提示 * plr test * 移动is_host_mode 新增slave_no_host * 确保config优先修改生效 * fix graph io * 支持带参数传入 * 支持物料解析 * 支持物料解析 * device为空的时候不进行绑定或初始化 * protocol node new * protocol node runnable * protocol node runnable * Feature/device node later init (#42) * 修改config路径,方便后续打包 增加device_node打印 * 支持plr序列化/init创建 * 统一命名 * import mgr logger optimize banner print * 日志OK * fix unicorn frame * banner print * correct import format * file path changes * 取消后补全,在加载设备的时候直接替换 * converter update * web page update * 在线device更新,node继承替换 * 修复动作、状态的类型缺失 和 命令提示 * web功能实现结束 * host节点更改完成 新增status时间戳管理 新增每10s动态发现其他node * ros2类型的节点也应该被包一次 * 修复类型提示 * websocket 动态显示状态 * add workflow & book theme for docs * add workflow & book theme for docs * fix workflow build * fix workflow build * 理清启动关系 * stm32 example * mac . name * device_instance device_cls * 新增config添加方式 更新mqtt提示 * plr test * 移动is_host_mode 新增slave_no_host * 确保config优先修改生效 * fix graph io * 支持带参数传入 * 支持物料解析 * 支持物料解析 * device为空的时候不进行绑定或初始化 * protocol node new * protocol node runnable * protocol node runnable * action * plr suc * plr suc!! * plr suc!! * plr suc!! * plr msgs * Feature/device node later init (#43) * 修改config路径,方便后续打包 增加device_node打印 * 支持plr序列化/init创建 * 统一命名 * import mgr logger optimize banner print * 日志OK * fix unicorn frame * banner print * correct import format * file path changes * 取消后补全,在加载设备的时候直接替换 * converter update * web page update * 在线device更新,node继承替换 * 修复动作、状态的类型缺失 和 命令提示 * web功能实现结束 * host节点更改完成 新增status时间戳管理 新增每10s动态发现其他node * ros2类型的节点也应该被包一次 * 修复类型提示 * websocket 动态显示状态 * add workflow & book theme for docs * add workflow & book theme for docs * fix workflow build * fix workflow build * 理清启动关系 * stm32 example * mac . name * device_instance device_cls * 新增config添加方式 更新mqtt提示 * plr test * 移动is_host_mode 新增slave_no_host * 确保config优先修改生效 * fix graph io * 支持带参数传入 * 支持物料解析 * 支持物料解析 * device为空的时候不进行绑定或初始化 * protocol node new * protocol node runnable * protocol node runnable * action * plr suc * plr suc!! * plr suc!! * plr suc!! * plr msgs * plr * action * plr reg fix * Feature/device node later init (#44) * 修改config路径,方便后续打包 增加device_node打印 * 支持plr序列化/init创建 * 统一命名 * import mgr logger optimize banner print * 日志OK * fix unicorn frame * banner print * correct import format * file path changes * 取消后补全,在加载设备的时候直接替换 * converter update * web page update * 在线device更新,node继承替换 * 修复动作、状态的类型缺失 和 命令提示 * web功能实现结束 * host节点更改完成 新增status时间戳管理 新增每10s动态发现其他node * ros2类型的节点也应该被包一次 * 修复类型提示 * websocket 动态显示状态 * add workflow & book theme for docs * add workflow & book theme for docs * fix workflow build * fix workflow build * 理清启动关系 * stm32 example * mac . name * device_instance device_cls * 新增config添加方式 更新mqtt提示 * plr test * 移动is_host_mode 新增slave_no_host * 确保config优先修改生效 * fix graph io * 支持带参数传入 * 支持物料解析 * 支持物料解析 * device为空的时候不进行绑定或初始化 * protocol node new * protocol node runnable * protocol node runnable * action * plr suc * plr suc!! * plr suc!! * plr suc!! * plr msgs * plr * fix convert error fix async logic error added async error print * new device test * test resource add * test resource add * test resource add * test resource add * local env setup * node type fix temp fix root_node error fix convert res from type error * resource tracker * fix bug from qhh * fix bug from qhh * fix bug from qhh * fix bug from qhh * refactor MQTT client logging and connection handling; update group ID in config * driver_params allow empty * allow other init param * fix driver param and enhance type hint * refactor MQConfig to use double quotes for string literals * fix wrong function calling * fix wrong function calling * fix log for mac * fix networkx compatibility * add mqtt loggers * add action to jsonschema converter * random client id * type converter & registry * correct conversion * fix action publish only from discovered devices * add "Bio" tag for action doc generation * 改进module提示 * Fix doc * mqtt不连接也可用 性价样例提示 * add docs * 更新plr test案例 * Update intro.md * 更新有机案例 * skip --------- Co-authored-by: Harvey Que <Q-Query@outlook.com> Co-authored-by: Junhan Chang <1700011741@pku.edu.cn> --------- Co-authored-by: 王俊杰 <2201110460@stu.pku.edu.cn> Co-authored-by: Junhan Chang <changjh@pku.edu.cn> Co-authored-by: jiawei <miaojiawei@dp.tech> Co-authored-by: ColumbiaCC <2100011801@stu.pku.edu.cn> Co-authored-by: Harvey Que <Q-Query@outlook.com> Co-authored-by: Junhan Chang <1700011741@pku.edu.cn> * Canonicalize before Open Source (#46) * big big refactor try01 * refactor 02 --------- Co-authored-by: ck <xiaoyeqiannian@163.com> Co-authored-by: 王俊杰 <1800011822@pku.edu.cn> Co-authored-by: q434343 <554662886@qq.com> Co-authored-by: Xuwznln <xuwznln@gmail.com> Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Co-authored-by: wjjxxx <43375851+wjjxxx@users.noreply.github.com> Co-authored-by: 3218923350 <105201755+3218923350@users.noreply.github.com> Co-authored-by: Xuwznln <1023025701@qq.com> Co-authored-by: 王俊杰 <2201110460@stu.pku.edu.cn> Co-authored-by: jiawei <miaojiawei@dp.tech> Co-authored-by: Jiawei <91898272+jiawei723@users.noreply.github.com> Co-authored-by: ColumbiaCC <2100011801@stu.pku.edu.cn> Co-authored-by: Harvey Que <Q-Query@outlook.com>
This commit is contained in:
0
unilabos/devices/UV_test/__init__.py
Normal file
0
unilabos/devices/UV_test/__init__.py
Normal file
333
unilabos/devices/UV_test/fuxiang2.py
Normal file
333
unilabos/devices/UV_test/fuxiang2.py
Normal 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()
|
||||
0
unilabos/devices/__init__.py
Normal file
0
unilabos/devices/__init__.py
Normal file
0
unilabos/devices/agv/__init__.py
Normal file
0
unilabos/devices/agv/__init__.py
Normal file
101
unilabos/devices/agv/agv_navigator.py
Normal file
101
unilabos/devices/agv/agv_navigator.py
Normal 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')
|
||||
54
unilabos/devices/agv/pose.json
Normal file
54
unilabos/devices/agv/pose.json
Normal 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]
|
||||
]
|
||||
]
|
||||
}
|
||||
298
unilabos/devices/agv/robotiq_gripper.py
Normal file
298
unilabos/devices/agv/robotiq_gripper.py
Normal 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))
|
||||
166
unilabos/devices/agv/ur_arm_task.py
Normal file
166
unilabos/devices/agv/ur_arm_task.py
Normal 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())
|
||||
|
||||
0
unilabos/devices/cnc/__init__.py
Normal file
0
unilabos/devices/cnc/__init__.py
Normal file
265
unilabos/devices/cnc/grbl_async.py
Normal file
265
unilabos/devices/cnc/grbl_async.py
Normal 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)
|
||||
205
unilabos/devices/cnc/grbl_sync.py
Normal file
205
unilabos/devices/cnc/grbl_sync.py
Normal 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)
|
||||
42
unilabos/devices/cnc/mock.py
Normal file
42
unilabos/devices/cnc/mock.py
Normal 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"
|
||||
0
unilabos/devices/gripper/__init__.py
Normal file
0
unilabos/devices/gripper/__init__.py
Normal file
46
unilabos/devices/gripper/mock.py
Normal file
46
unilabos/devices/gripper/mock.py
Normal 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
|
||||
408
unilabos/devices/gripper/rmaxis_v4.py
Normal file
408
unilabos/devices/gripper/rmaxis_v4.py
Normal 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)
|
||||
0
unilabos/devices/heaterstirrer/__init__.py
Normal file
0
unilabos/devices/heaterstirrer/__init__.py
Normal file
197
unilabos/devices/heaterstirrer/dalong.py
Normal file
197
unilabos/devices/heaterstirrer/dalong.py
Normal 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()
|
||||
470
unilabos/devices/hplc/AgilentHPLC.py
Normal file
470
unilabos/devices/hplc/AgilentHPLC.py
Normal 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)
|
||||
0
unilabos/devices/hplc/__init__.py
Normal file
0
unilabos/devices/hplc/__init__.py
Normal file
0
unilabos/devices/liquid_handling/__init__.py
Normal file
0
unilabos/devices/liquid_handling/__init__.py
Normal file
90
unilabos/devices/liquid_handling/revvity.py
Normal file
90
unilabos/devices/liquid_handling/revvity.py
Normal 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
|
||||
4
unilabos/devices/motor/Consts.py
Normal file
4
unilabos/devices/motor/Consts.py
Normal file
@@ -0,0 +1,4 @@
|
||||
class Config(object):
|
||||
DEBUG_MODE = True
|
||||
OPEN_DEVICE = True
|
||||
DEVICE_ADDRESS = 0x01
|
||||
21
unilabos/devices/motor/FakeSerial.py
Normal file
21
unilabos/devices/motor/FakeSerial.py
Normal 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
|
||||
153
unilabos/devices/motor/Grasp.py
Normal file
153
unilabos/devices/motor/Grasp.py
Normal 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)
|
||||
|
||||
87
unilabos/devices/motor/Utils.py
Normal file
87
unilabos/devices/motor/Utils.py
Normal 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)
|
||||
0
unilabos/devices/motor/__init__.py
Normal file
0
unilabos/devices/motor/__init__.py
Normal file
37
unilabos/devices/motor/base_types/PrPath.py
Normal file
37
unilabos/devices/motor/base_types/PrPath.py
Normal 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 # 相对定位+相对电机
|
||||
0
unilabos/devices/motor/base_types/__init__.py
Normal file
0
unilabos/devices/motor/base_types/__init__.py
Normal file
92
unilabos/devices/motor/commands/PositionControl.py
Normal file
92
unilabos/devices/motor/commands/PositionControl.py
Normal 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
|
||||
44
unilabos/devices/motor/commands/Status.py
Normal file
44
unilabos/devices/motor/commands/Status.py
Normal 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
|
||||
12
unilabos/devices/motor/commands/Warning.py
Normal file
12
unilabos/devices/motor/commands/Warning.py
Normal 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("清除警报")
|
||||
0
unilabos/devices/motor/commands/__init__.py
Normal file
0
unilabos/devices/motor/commands/__init__.py
Normal file
126
unilabos/devices/motor/iCL42.py
Normal file
126
unilabos/devices/motor/iCL42.py
Normal 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
|
||||
79
unilabos/devices/motor/test.py
Normal file
79
unilabos/devices/motor/test.py
Normal 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()
|
||||
295
unilabos/devices/platereader/NivoDriver.py
Normal file
295
unilabos/devices/platereader/NivoDriver.py
Normal 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
|
||||
166
unilabos/devices/platereader/PlayerUtil.py
Normal file
166
unilabos/devices/platereader/PlayerUtil.py
Normal 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()
|
||||
|
||||
0
unilabos/devices/platereader/__init__.py
Normal file
0
unilabos/devices/platereader/__init__.py
Normal file
0
unilabos/devices/powder_dispense/__init__.py
Normal file
0
unilabos/devices/powder_dispense/__init__.py
Normal file
0
unilabos/devices/pump_and_valve/__init__.py
Normal file
0
unilabos/devices/pump_and_valve/__init__.py
Normal file
394
unilabos/devices/pump_and_valve/runze_async.py
Normal file
394
unilabos/devices/pump_and_valve/runze_async.py
Normal 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)
|
||||
388
unilabos/devices/pump_and_valve/runze_backbone.py
Normal file
388
unilabos/devices/pump_and_valve/runze_backbone.py
Normal 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)
|
||||
51
unilabos/devices/pump_and_valve/solenoid_valve.py
Normal file
51
unilabos/devices/pump_and_valve/solenoid_valve.py
Normal 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
|
||||
38
unilabos/devices/pump_and_valve/solenoid_valve_mock.py
Normal file
38
unilabos/devices/pump_and_valve/solenoid_valve_mock.py
Normal 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
|
||||
31
unilabos/devices/pump_and_valve/vacuum_pump_mock.py
Normal file
31
unilabos/devices/pump_and_valve/vacuum_pump_mock.py
Normal 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
|
||||
0
unilabos/devices/raman_uv/__init__.py
Normal file
0
unilabos/devices/raman_uv/__init__.py
Normal file
198
unilabos/devices/raman_uv/home_made_raman.py
Normal file
198
unilabos/devices/raman_uv/home_made_raman.py
Normal 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()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
0
unilabos/devices/rotavap/__init__.py
Normal file
0
unilabos/devices/rotavap/__init__.py
Normal file
64
unilabos/devices/rotavap/rotavap_one.py
Normal file
64
unilabos/devices/rotavap/rotavap_one.py
Normal 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')
|
||||
0
unilabos/devices/separator/__init__.py
Normal file
0
unilabos/devices/separator/__init__.py
Normal file
156
unilabos/devices/separator/homemade_grbl_conductivity.py
Normal file
156
unilabos/devices/separator/homemade_grbl_conductivity.py
Normal 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)
|
||||
0
unilabos/devices/temperature/__init__.py
Normal file
0
unilabos/devices/temperature/__init__.py
Normal file
67
unilabos/devices/temperature/chiller.py
Normal file
67
unilabos/devices/temperature/chiller.py
Normal 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)
|
||||
69
unilabos/devices/temperature/prototype_sensor.py
Normal file
69
unilabos/devices/temperature/prototype_sensor.py
Normal 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
|
||||
|
||||
|
||||
90
unilabos/devices/temperature/sensor.py
Normal file
90
unilabos/devices/temperature/sensor.py
Normal 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()
|
||||
117
unilabos/devices/temperature/sensor_node.py
Normal file
117
unilabos/devices/temperature/sensor_node.py
Normal 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
|
||||
0
unilabos/devices/vaccum/__init__.py
Normal file
0
unilabos/devices/vaccum/__init__.py
Normal file
Reference in New Issue
Block a user