Skip to content

Commit 06e6676

Browse files
authored
feat: 添加ChinWe设备控制类,支持串口通信和电机控制功能 (#79)
1 parent 98ce360 commit 06e6676

File tree

4 files changed

+293
-0
lines changed

4 files changed

+293
-0
lines changed

unilabos/devices/ChinWe/chinwe.py

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
import sys
2+
import threading
3+
import serial
4+
import serial.tools.list_ports
5+
import re
6+
import time
7+
from typing import Optional, List, Dict, Tuple
8+
9+
class ChinweDevice:
10+
"""
11+
ChinWe设备控制类
12+
提供串口通信、电机控制、传感器数据读取等功能
13+
"""
14+
15+
def __init__(self, port: str, baudrate: int = 115200, debug: bool = False):
16+
"""
17+
初始化ChinWe设备
18+
19+
Args:
20+
port: 串口名称,如果为None则自动检测
21+
baudrate: 波特率,默认115200
22+
"""
23+
self.debug = debug
24+
self.port = port
25+
self.baudrate = baudrate
26+
self.serial_port: Optional[serial.Serial] = None
27+
self._voltage: float = 0.0
28+
self._ec_value: float = 0.0
29+
self._ec_adc_value: int = 0
30+
self._is_connected = False
31+
self.connect()
32+
33+
@property
34+
def is_connected(self) -> bool:
35+
"""获取连接状态"""
36+
return self._is_connected and self.serial_port and self.serial_port.is_open
37+
38+
@property
39+
def voltage(self) -> float:
40+
"""获取电源电压值"""
41+
return self._voltage
42+
43+
@property
44+
def ec_value(self) -> float:
45+
"""获取电导率值 (ms/cm)"""
46+
return self._ec_value
47+
48+
@property
49+
def ec_adc_value(self) -> int:
50+
"""获取EC ADC原始值"""
51+
return self._ec_adc_value
52+
53+
54+
@property
55+
def device_status(self) -> Dict[str, any]:
56+
"""
57+
获取设备状态信息
58+
59+
Returns:
60+
包含设备状态的字典
61+
"""
62+
return {
63+
"connected": self.is_connected,
64+
"port": self.port,
65+
"baudrate": self.baudrate,
66+
"voltage": self.voltage,
67+
"ec_value": self.ec_value,
68+
"ec_adc_value": self.ec_adc_value
69+
}
70+
71+
def connect(self, port: Optional[str] = None, baudrate: Optional[int] = None) -> bool:
72+
"""
73+
连接到串口设备
74+
75+
Args:
76+
port: 串口名称,如果为None则使用初始化时的port或自动检测
77+
baudrate: 波特率,如果为None则使用初始化时的baudrate
78+
79+
Returns:
80+
连接是否成功
81+
"""
82+
if self.is_connected:
83+
return True
84+
85+
target_port = port or self.port
86+
target_baudrate = baudrate or self.baudrate
87+
88+
try:
89+
self.serial_port = serial.Serial(target_port, target_baudrate, timeout=0.5)
90+
self._is_connected = True
91+
self.port = target_port
92+
self.baudrate = target_baudrate
93+
connect_allow_times = 5
94+
while not self.serial_port.is_open and connect_allow_times > 0:
95+
time.sleep(0.5)
96+
connect_allow_times -= 1
97+
print(f"尝试连接到 {target_port} @ {target_baudrate},剩余尝试次数: {connect_allow_times}", self.debug)
98+
raise ValueError("串口未打开,请检查设备连接")
99+
print(f"已连接到 {target_port} @ {target_baudrate}", self.debug)
100+
threading.Thread(target=self._read_data, daemon=True).start()
101+
return True
102+
except Exception as e:
103+
print(f"ChinweDevice连接失败: {e}")
104+
self._is_connected = False
105+
return False
106+
107+
def disconnect(self) -> bool:
108+
"""
109+
断开串口连接
110+
111+
Returns:
112+
断开是否成功
113+
"""
114+
if self.serial_port and self.serial_port.is_open:
115+
try:
116+
self.serial_port.close()
117+
self._is_connected = False
118+
print("已断开串口连接")
119+
return True
120+
except Exception as e:
121+
print(f"断开连接失败: {e}")
122+
return False
123+
return True
124+
125+
def _send_motor_command(self, command: str) -> bool:
126+
"""
127+
发送电机控制命令
128+
129+
Args:
130+
command: 电机命令字符串,例如 "M 1 CW 1.5"
131+
132+
Returns:
133+
发送是否成功
134+
"""
135+
if not self.is_connected:
136+
print("设备未连接")
137+
return False
138+
139+
try:
140+
self.serial_port.write((command + "\n").encode('utf-8'))
141+
print(f"发送命令: {command}")
142+
return True
143+
except Exception as e:
144+
print(f"发送命令失败: {e}")
145+
return False
146+
147+
def rotate_motor(self, motor_id: int, turns: float, clockwise: bool = True) -> bool:
148+
"""
149+
使电机转动指定圈数
150+
151+
Args:
152+
motor_id: 电机ID(1, 2, 3...)
153+
turns: 转动圈数,支持小数
154+
clockwise: True为顺时针,False为逆时针
155+
156+
Returns:
157+
命令发送是否成功
158+
"""
159+
if clockwise:
160+
command = f"M {motor_id} CW {turns}"
161+
else:
162+
command = f"M {motor_id} CCW {turns}"
163+
return self._send_motor_command(command)
164+
165+
def set_motor_speed(self, motor_id: int, speed: float) -> bool:
166+
"""
167+
设置电机转速(如果设备支持)
168+
169+
Args:
170+
motor_id: 电机ID(1, 2, 3...)
171+
speed: 转速值
172+
173+
Returns:
174+
命令发送是否成功
175+
"""
176+
command = f"M {motor_id} SPEED {speed}"
177+
return self._send_motor_command(command)
178+
179+
def _read_data(self) -> List[str]:
180+
"""
181+
读取串口数据并解析
182+
183+
Returns:
184+
读取到的数据行列表
185+
"""
186+
print("开始读取串口数据...")
187+
if not self.is_connected:
188+
return []
189+
190+
data_lines = []
191+
try:
192+
while self.serial_port.in_waiting:
193+
time.sleep(0.1) # 等待数据稳定
194+
try:
195+
line = self.serial_port.readline().decode('utf-8', errors='ignore').strip()
196+
if line:
197+
data_lines.append(line)
198+
self._parse_sensor_data(line)
199+
except Exception as ex:
200+
print(f"解码数据错误: {ex}")
201+
except Exception as e:
202+
print(f"读取串口数据错误: {e}")
203+
204+
return data_lines
205+
206+
def _parse_sensor_data(self, line: str) -> None:
207+
"""
208+
解析传感器数据
209+
210+
Args:
211+
line: 接收到的数据行
212+
"""
213+
# 解析电源电压
214+
if "电源电压" in line:
215+
try:
216+
val = float(line.split(":")[1].replace("V", "").strip())
217+
self._voltage = val
218+
if self.debug:
219+
print(f"电源电压更新: {val}V")
220+
except Exception:
221+
pass
222+
223+
# 解析电导率和ADC原始值(支持两种格式)
224+
if "电导率" in line and "ADC原始值" in line:
225+
try:
226+
# 支持格式如:电导率:2.50ms/cm, ADC原始值:2052
227+
ec_match = re.search(r"电导率[::]\s*([\d\.]+)", line)
228+
adc_match = re.search(r"ADC原始值[::]\s*(\d+)", line)
229+
if ec_match:
230+
ec_val = float(ec_match.group(1))
231+
self._ec_value = ec_val
232+
if self.debug:
233+
print(f"电导率更新: {ec_val:.2f} ms/cm")
234+
if adc_match:
235+
adc_val = int(adc_match.group(1))
236+
self._ec_adc_value = adc_val
237+
if self.debug:
238+
print(f"EC ADC原始值更新: {adc_val}")
239+
except Exception:
240+
pass
241+
# 仅电导率,无ADC原始值
242+
elif "电导率" in line:
243+
try:
244+
val = float(line.split(":")[1].replace("ms/cm", "").strip())
245+
self._ec_value = val
246+
if self.debug:
247+
print(f"电导率更新: {val:.2f} ms/cm")
248+
except Exception:
249+
pass
250+
# 仅ADC原始值(如有分开回传场景)
251+
elif "ADC原始值" in line:
252+
try:
253+
adc_val = int(line.split(":")[1].strip())
254+
self._ec_adc_value = adc_val
255+
if self.debug:
256+
print(f"EC ADC原始值更新: {adc_val}")
257+
except Exception:
258+
pass
259+
260+
def spin_when_ec_ge_0():
261+
pass
262+
263+
264+
def main():
265+
"""测试函数"""
266+
print("=== ChinWe设备测试 ===")
267+
268+
# 创建设备实例
269+
device = ChinweDevice("/dev/tty.usbserial-A5069RR4", debug=True)
270+
try:
271+
# 测试5: 发送电机命令
272+
print("\n5. 发送电机命令测试:")
273+
print(" 5.3 使用通用函数控制电机20顺时针转2圈:")
274+
device.rotate_motor(2, 20.0, clockwise=True)
275+
time.sleep(0.5)
276+
finally:
277+
time.sleep(10)
278+
# 测试7: 断开连接
279+
print("\n7. 断开连接:")
280+
device.disconnect()
281+
if __name__ == "__main__":
282+
main()

unilabos/devices/pump_and_valve/runze_backbone.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from enum import Enum
44
from dataclasses import dataclass
55
import time
6+
import traceback
67
from typing import Any, Union, Optional, overload
78

89
import serial.tools.list_ports
@@ -386,3 +387,8 @@ def close(self):
386387
def list():
387388
for item in serial.tools.list_ports.comports():
388389
yield RunzeSyringePumpInfo(port=item.device)
390+
391+
392+
if __name__ == "__main__":
393+
r = RunzeSyringePump("/dev/tty.usbserial-D30JUGG5", "1", 25.0)
394+
r.initialize()

unilabos/registry/registry.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,8 @@ def load_device_types(self, path: os.PathLike, complete_registry: bool):
447447
if complete_registry:
448448
device_config["class"]["status_types"].clear()
449449
enhanced_info = get_enhanced_class_info(device_config["class"]["module"], use_dynamic=True)
450+
if not enhanced_info.get("dynamic_import_success", False):
451+
continue
450452
device_config["class"]["status_types"].update(
451453
{k: v["return_type"] for k, v in enhanced_info["status_methods"].items()}
452454
)

unilabos/ros/nodes/presets/protocol_node.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ def __init__(
8888
if device_config.get("type", "device") != "device":
8989
continue
9090
# 设置硬件接口代理
91+
if device_id not in self.sub_devices:
92+
self.lab_logger().error(f"[Protocol Node] {device_id} 还没有正确初始化,跳过...")
93+
continue
9194
d = self.sub_devices[device_id]
9295
if d:
9396
hardware_interface = d.ros_node_instance._hardware_interface

0 commit comments

Comments
 (0)