572 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import time
import socket
import struct
import numpy as np
import atexit
import threading
from typing import Optional, Tuple
from matplotlib import pyplot as plt
import csv
from datetime import datetime
class XjcSensor:
def __init__(self, host: str, port: int, rate: int = 250):
"""
初始化 TCP 连接的力传感器
:param host: 设备 IP 地址(如 "192.168.1.100"
:param port: 设备端口(如 502
:param rate: 数据采样率100/250/500Hz
"""
self.host = host
self.port = port
self.rate = rate
self.slave_address = 0x01 # 默认从机地址
self.sock = None # TCP socket 对象
self.crc16_table = self.generate_crc16_table()
# 后台读取相关的属性
self._background_thread = None
self._stop_background = False
self._last_reading = None
self._reading_lock = threading.Lock()
# 建立 TCP 连接
self.connect()
# 注册退出时的清理函数
atexit.register(self.disconnect)
def __new__(cls, *args, **kwargs):
"""单例模式"""
if not hasattr(cls, 'instance'):
cls.instance = super(XjcSensor, cls).__new__(cls)
return cls.instance
def connect(self):
"""建立 TCP 连接"""
try:
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.settimeout(1.0) # 设置超时时间
self.sock.connect((self.host, self.port))
print(f"Connected to {self.host}:{self.port}")
except Exception as e:
print(f"TCP connection failed: {e}")
raise
def send_and_receive(self, data: bytes, expected_response_len: int) -> Optional[bytes]:
"""
发送数据并接收响应
:param data: 要发送的字节数据
:param expected_response_len: 预期响应的长度
:return: 收到的字节数据(失败返回 None
"""
try:
self.sock.sendall(data)
response = self.sock.recv(expected_response_len)
return response
except socket.timeout:
print("Timeout while waiting for response")
return None
except Exception as e:
print(f"TCP communication error: {e}")
return None
def set_zero(self) -> int:
"""传感器置零TCP 版本)"""
if self.slave_address == 0x01:
zero_cmd = bytes.fromhex('01 10 46 1C 00 02 04 00 00 00 00 E8 95')
elif self.slave_address == 0x09:
zero_cmd = bytes.fromhex('09 10 46 1C 00 02 04 00 00 00 00 C2 F5')
response = self.send_and_receive(zero_cmd, 8)
if not response:
print("set zero fail")
return -1
# CRC 校验(假设协议要求)
hi_check, lo_check = self.crc16(response[:-2])
if (response[0] != self.slave_address or
response[1] != 0x10 or
response[-2] != lo_check or
response[-1] != hi_check):
print("Set zero failed: CRC check error")
return -1
print("Set zero success!")
return 0
def read_data_f32(self) -> np.ndarray:
"""
TCP版本 - 读取六维力传感器数据 (float32格式)
返回:
np.ndarray(6,) - 六维力数据 [Fx,Fy,Fz,Mx,My,Mz]
读取失败返回 -1
"""
# 构造读取命令
if self.slave_address == 0x01:
command = bytes.fromhex('01 04 00 00 00 0C F0 0F')
elif self.slave_address == 0x09:
command = bytes.fromhex('09 04 00 54 00 0C B0 97')
try:
# 清空接收缓冲区
self._clear_tcp_buffer()
# 接收完整响应
response = self.send_and_receive(command, 29)
# CRC校验
Hi_check, Lo_check = self.crc16(response[:-2])
if (response[0] != self.slave_address or
response[1] != 0x04 or
response[2] != 24 or # 数据长度字节
response[-2] != Lo_check or
response[-1] != Hi_check):
print(f"Protocol error! Received: {response.hex(' ')}")
print(f"Expected slave:{self.slave_address}, "
f"func:0x04, len:24, "
f"CRC:{Lo_check:02X}{Hi_check:02X}")
return -1
# 解析6个float32数据 (大端序)
sensor_data = struct.unpack('>ffffff', response[3:27])
return np.array(sensor_data)
except socket.timeout:
print("Timeout while waiting for sensor response")
return -1
except ConnectionError as e:
print(f"TCP connection error: {e}")
return -1
except Exception as e:
print(f"Unexpected error in read_data_f32: {str(e)}")
return -1
def _clear_tcp_buffer(self):
"""清空TCP接收缓冲区"""
self.sock.settimeout(0.1) # 短暂超时,避免阻塞
try:
while True:
data = self.sock.recv(1024)
if not data: # 连接关闭或无数据
break
print(f"Cleared TCP buffer: {data.hex(' ')}") # 调试输出
except socket.timeout:
pass # 预期内的超时,表示缓冲区已空
finally:
self.sock.settimeout(2.0) # 恢复默认超时
def enable_active_transmission(self) -> int:
"""启用主动传输模式TCP 版本)"""
if self.rate == 100:
cmd = bytes.fromhex('01 10 01 9A 00 01 02 00 00 AB 6A') if self.slave_address == 0x01 else \
bytes.fromhex('09 10 01 9A 00 01 02 00 00 CC AA')
elif self.rate == 250:
cmd = bytes.fromhex('01 10 01 9A 00 01 02 00 01 6A AA') if self.slave_address == 0x01 else \
bytes.fromhex('09 10 01 9A 00 01 02 00 01 0D 6A')
elif self.rate == 500:
cmd = bytes.fromhex('01 10 01 9A 00 01 02 00 02 2A AB') if self.slave_address == 0x01 else \
bytes.fromhex('09 10 01 9A 00 01 02 00 02 4D 6B')
else:
print("Unsupported rate")
return -1
response = self.send_and_receive(cmd, 8)
if response and response[1] == 0x10: # 检查响应功能码
print(f"Active transmission enabled at {self.rate}Hz")
return 0
else:
print("Failed to enable active transmission")
return -1
def disconnect(self):
"""关闭 TCP 连接"""
if self.sock:
try:
self.sock.close()
print("TCP connection closed")
except Exception as e:
print(f"Error while closing socket: {e}")
self.sock = None
def start_background_reading(self):
"""
启动后台读取任务,每秒读取一次传感器数据
"""
if self._background_thread is not None and self._background_thread.is_alive():
print("Background reading is already running")
return False
self._stop_background = False
self._background_thread = threading.Thread(target=self._background_reading_task)
self._background_thread.daemon = True # 设置为守护线程
self._background_thread.start()
return True
def stop_background_reading(self):
"""
停止后台读取任务
"""
if self._background_thread is None or not self._background_thread.is_alive():
print("No background reading is running")
return False
self._stop_background = True
self._background_thread.join(timeout=2.0) # 等待线程结束最多等待2秒
self._background_thread = None
return True
def _background_reading_task(self):
"""
后台读取任务的实现
"""
while not self._stop_background:
try:
data = self.read_data_f32()
if isinstance(data, np.ndarray):
with self._reading_lock:
self._last_reading = data
else:
self.connect()
except Exception as e:
print(f"Error in background reading: {e}")
time.sleep(1.0) # 每秒读取一次
def get_last_reading(self):
"""
获取最近一次的读数
"""
with self._reading_lock:
return self._last_reading
# ------------------------- 以下方法保持不变 -------------------------
@staticmethod
def generate_crc16_table():
"""CRC16 表生成(与原代码相同)"""
table = []
polynomial = 0xA001
for i in range(256):
crc = i
for _ in range(8):
if crc & 0x0001:
crc = (crc >> 1) ^ polynomial
else:
crc >>= 1
table.append(crc)
return table
def crc16(self, data: bytes):
"""CRC16 计算(与原代码相同)"""
crc = 0xFFFF
for byte in data:
crc = (crc >> 8) ^ self.crc16_table[(crc ^ byte) & 0xFF]
return (crc >> 8) & 0xFF, crc & 0xFF
def __del__(self):
"""析构时自动断开连接"""
self.disconnect()
def disable_active_transmission(self) -> int:
"""
禁用传感器的主动传输模式TCP 版本)
Returns:
0 表示成功,-1 表示失败
"""
try:
# 构造禁用命令(原代码中的 FF x11 字节流)
disable_cmd = bytes.fromhex('FF FF FF FF FF FF FF FF FF FF FF')
print("Disabling active transmission mode...")
# 检查 TCP 连接是否有效
if not self.sock:
raise ConnectionError("TCP connection is not established")
# 发送禁用命令
self.sock.sendall(disable_cmd)
print("Active transmission disabled successfully")
return 0
except ConnectionError as e:
print(f"Connection error: {e}")
return -1
except socket.timeout:
print("Timeout while sending disable command")
return -1
except Exception as e:
print(f"Unexpected error: {e}")
return -1
def read(self):
"""
TCP Version - Read the sensor's data in passive mode.
Returns:
np.ndarray(6,) - Six-axis force/torque data [Fx, Fy, Fz, Mx, My, Mz]
None if reading fails
"""
try:
# Clear any existing data in the TCP buffer first
# self._clear_tcp_buffer()
# Search for frame header (0x20 0x4E)
header_found = False
start_time = time.time()
timeout = 1.0 # 1 second timeout
while not header_found and (time.time() - start_time < timeout):
# Read one byte at a time looking for header
byte1 = self.sock.recv(1)
if not byte1:
continue # No data available yet
if byte1 == b'\x20':
byte2 = self.sock.recv(1)
if byte2 == b'\x4E':
header_found = True
if not header_found:
print("Frame header not found within timeout period")
return None
# Now read the remaining 14 bytes of the frame
response = bytearray([0x20, 0x4E]) # Start with the header we found
remaining_bytes = 14
bytes_received = 0
start_time = time.time()
while bytes_received < remaining_bytes and (time.time() - start_time < timeout):
chunk = self.sock.recv(remaining_bytes - bytes_received)
if chunk:
response.extend(chunk)
bytes_received += len(chunk)
if bytes_received < remaining_bytes:
print(f"Incomplete frame received. Got {len(response)} bytes, expected 16")
return None
# Verify CRC checksum
Hi_check, Lo_check = self.crc16(response[:-2])
if response[-1] != Hi_check or response[-2] != Lo_check:
print("CRC check failed!")
print(f"Received CRC: {response[-2]:02X}{response[-1]:02X}")
print(f"Calculated CRC: {Lo_check:02X}{Hi_check:02X}")
return None
# Parse the sensor data
sensor_data = self.parse_data_passive(response)
return sensor_data
except socket.timeout:
print("Timeout while waiting for sensor data")
return None
except ConnectionError as e:
print(f"TCP connection error: {e}")
return None
except Exception as e:
print(f"Unexpected error in TCP read(): {str(e)}")
return None
def parse_data_passive(self, buffer):
values = [
int.from_bytes(buffer[i:i+2], byteorder='little', signed=True)
for i in range(2, 14, 2)
]
Fx, Fy, Fz = np.array(values[:3]) / 10.0
Mx, My, Mz = np.array(values[3:]) / 1000.0
return np.array([Fx, Fy, Fz, Mx, My, Mz])
# 外部
def test_sensor_frequency(sensor, mode='active', duration=5.0):
"""
测试传感器在不同模式下的实际数据获取频率
参数:
sensor: XjcSensor实例
mode: 'active'(主动模式) 或 'polling'(查询模式)
duration: 测试持续时间(秒)
返回:
dict: 包含测试结果的字典
"""
# 准备测试环境
if mode == 'active':
print("\n=== 测试主动传输模式 ===")
sensor.enable_active_transmission()
read_func = sensor.read
else:
print("\n=== 测试查询模式 ===")
sensor.disable_active_transmission() # 确保不在主动模式
time.sleep(0.5)
sensor.disable_active_transmission() # 确保不在主动模式
time.sleep(0.5)
sensor.disable_active_transmission() # 确保不在主动模式
time.sleep(0.5)
read_func = sensor.read_data_f32
# 初始化测试变量
timestamps = []
data_count = 0
start_time = time.perf_counter()
end_time = start_time + duration
print(f"开始测试,持续时间 {duration} 秒...")
# 主测试循环
while time.perf_counter() < end_time:
data = read_func()
print(data)
if data is not None:
timestamps.append(time.perf_counter())
data_count += 1
# print(timestamps)
# 计算统计结果
if data_count < 2:
print("警告: 采集到的数据点不足")
return None
intervals = np.diff(timestamps)
save_intervals_to_csv(timestamps)
avg_interval = np.mean(intervals)
min_interval = np.min(intervals)
max_interval = np.max(intervals)
std_interval = np.std(intervals)
avg_freq = 1.0 / avg_interval
# 打印结果
print("\n测试结果:")
print(f"总数据点数: {data_count}")
print(f"平均频率: {avg_freq:.2f} Hz")
print(f"最小间隔: {min_interval*1000:.3f} ms")
print(f"最大间隔: {max_interval*1000:.3f} ms")
print(f"间隔标准差: {std_interval*1000:.3f} ms")
# 返回结果字典
return {
'mode': mode,
'duration': duration,
'data_count': data_count,
'avg_freq': avg_freq,
'min_interval': min_interval,
'max_interval': max_interval,
'std_interval': std_interval,
'timestamps': timestamps,
'intervals': intervals
}
def plot_test_results(active_results, polling_results):
"""绘制两种模式的测试结果对比图"""
plt.figure(figsize=(12, 8))
# 间隔时间分布图
plt.subplot(2, 1, 1)
plt.hist(active_results['intervals']*1000, bins=50, alpha=0.7, label='主动模式')
plt.hist(polling_results['intervals']*1000, bins=50, alpha=0.7, label='查询模式')
plt.xlabel('间隔时间 (ms)')
plt.ylabel('出现次数')
plt.title('数据间隔时间分布')
plt.legend()
plt.grid(True)
# 间隔时间序列图
plt.subplot(2, 1, 2)
plt.plot(np.arange(len(active_results['intervals'])),
active_results['intervals']*1000, 'b.', label='主动模式')
plt.plot(np.arange(len(polling_results['intervals'])),
polling_results['intervals']*1000, 'r.', label='查询模式')
plt.xlabel('数据点序号')
plt.ylabel('间隔时间 (ms)')
plt.title('数据间隔时间序列')
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()
def compare_modes(sensor, duration=5.0):
"""比较两种工作模式的性能"""
# 测试主动模式
active_results = test_sensor_frequency(sensor, 'active', duration)
time.sleep(1) # 模式切换间隔
# 测试查询模式
polling_results = test_sensor_frequency(sensor, 'polling', duration)
time.sleep(1)
# 打印对比报告
print("\n=== 模式对比报告 ===")
print(f"{'指标':<15} {'主动模式':>15} {'查询模式':>15}")
print(f"{'平均频率(Hz)':<15} {active_results['avg_freq']:>15.2f} {polling_results['avg_freq']:>15.2f}")
print(f"{'最小间隔(ms)':<15} {active_results['min_interval']*1000:>15.3f} {polling_results['min_interval']*1000:>15.3f}")
print(f"{'最大间隔(ms)':<15} {active_results['max_interval']*1000:>15.3f} {polling_results['max_interval']*1000:>15.3f}")
print(f"{'间隔标准差(ms)':<15} {active_results['std_interval']*1000:>15.3f} {polling_results['std_interval']*1000:>15.3f}")
# 绘制对比图表
plot_test_results(active_results, polling_results)
return active_results, polling_results
def save_intervals_to_csv(timestamps, filename="sensor_intervals.csv"):
"""
Save timestamp intervals to CSV file with metadata
Args:
timestamps: List of timestamp values (from time.perf_counter())
filename: Output CSV filename
"""
if len(timestamps) < 2:
print("Not enough timestamps to calculate intervals")
return
# Calculate intervals in milliseconds
intervals_ms = np.diff(timestamps) * 1000
# Prepare data rows
rows = []
for i, interval in enumerate(intervals_ms):
rows.append({
"reading_number": i+1,
"interval_ms": f"{interval:.4f}",
"timestamp": timestamps[i],
"expected_interval_ms": (1000/250) if i==0 else "", # For 250Hz
"deviation_ms": f"{interval - (1000/250):.4f}" if i>0 else ""
})
# CSV header
fieldnames = ["reading_number", "interval_ms", "timestamp",
"expected_interval_ms", "deviation_ms"]
# Write to file
with open(filename, 'w', newline='') as csvfile:
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(rows)
print(f"Saved {len(intervals_ms)} intervals to {filename}")
if __name__ == "__main__":
# 替换为你的传感器 IP 和端口
sensor = XjcSensor("192.168.5.1", 60000)
# 示例操作
sensor.disable_active_transmission()
time.sleep(0.5)
sensor.disable_active_transmission()
time.sleep(0.5)
sensor.disable_active_transmission()
time.sleep(0.5)
sensor.set_zero()
# sensor.set_zero()
# sensor.disable_active_transmission()
# while True:
# data = sensor.read_data_f32()
# if data is not None:
# print(f"Force data: {data}")
# sensor.enable_active_transmission()
# while True:
# sensor_data = sensor.read()
# if sensor_data is None:
# print('failed to get force sensor data!')
# print(sensor_data)
test_sensor_frequency(sensor)
# compare_modes(sensor)