|
@@ -1,4 +1,5 @@
|
|
|
from typing import Optional
|
|
from typing import Optional
|
|
|
|
|
+import threading
|
|
|
|
|
|
|
|
|
|
|
|
|
# ========== 地址配置协议 ==========
|
|
# ========== 地址配置协议 ==========
|
|
@@ -27,8 +28,8 @@ def parse_device_response(data: bytes) -> Optional[dict]:
|
|
|
uid_bytes = data[2:14]
|
|
uid_bytes = data[2:14]
|
|
|
uid_hex = uid_bytes.hex()
|
|
uid_hex = uid_bytes.hex()
|
|
|
|
|
|
|
|
- # CRC 校验失败则拒绝
|
|
|
|
|
- if not verify_crc16(data[:14]):
|
|
|
|
|
|
|
+ # CRC 校验失败则拒绝(传入完整 16 字节帧,最后两字节为 CRC)
|
|
|
|
|
+ if not verify_crc16(data):
|
|
|
return {'error': 'CRC校验失败', 'raw_data': data.hex()}
|
|
return {'error': 'CRC校验失败', 'raw_data': data.hex()}
|
|
|
|
|
|
|
|
# UID 有效性检查:拒绝明显是噪声或碰撞产生的数据
|
|
# UID 有效性检查:拒绝明显是噪声或碰撞产生的数据
|
|
@@ -108,8 +109,9 @@ class AddressConfigProtocol:
|
|
|
|
|
|
|
|
def __init__(self, serial_port):
|
|
def __init__(self, serial_port):
|
|
|
self.serial = serial_port
|
|
self.serial = serial_port
|
|
|
- self.default_timeout = 2.0
|
|
|
|
|
|
|
+ self.default_timeout = 5.0 # 设备广播响应可能较慢,给足时间
|
|
|
self.stored_devices = {}
|
|
self.stored_devices = {}
|
|
|
|
|
+ self.config_lock = threading.RLock() # 地址配置期间占用,避免与轮询冲突
|
|
|
|
|
|
|
|
def add_stored_device(self, uid: str, address: int):
|
|
def add_stored_device(self, uid: str, address: int):
|
|
|
"""添加已存储的设备"""
|
|
"""添加已存储的设备"""
|
|
@@ -123,30 +125,55 @@ class AddressConfigProtocol:
|
|
|
"""send broadcast query and collect all valid 00 41 responses"""
|
|
"""send broadcast query and collect all valid 00 41 responses"""
|
|
|
import time
|
|
import time
|
|
|
|
|
|
|
|
- request = build_broadcast_query()
|
|
|
|
|
- logger.info(f"send broadcast: {request.hex()}")
|
|
|
|
|
|
|
+ with self.config_lock:
|
|
|
|
|
+ request = build_broadcast_query()
|
|
|
|
|
+ logger.info(f"send broadcast: {request.hex()}")
|
|
|
|
|
|
|
|
- timeout = timeout or self.default_timeout
|
|
|
|
|
- response = self.serial.send_and_wait(request, timeout=timeout, min_response_bytes=16)
|
|
|
|
|
-
|
|
|
|
|
- responses = []
|
|
|
|
|
- if response and len(response) >= 16:
|
|
|
|
|
- logger.info(f"got raw response ({len(response)}B): {response.hex()}")
|
|
|
|
|
- # RS485 半双工会回显已发送的数据,导致 response 开头可能有 5 字节回显
|
|
|
|
|
- # 在 response 中搜索所有 00 41 标记,尝试从中提取 16 字节帧解析
|
|
|
|
|
- for i in range(len(response) - 15 + 1):
|
|
|
|
|
- if response[i] == 0x00 and response[i + 1] == 0x41:
|
|
|
|
|
- chunk = response[i:i + 16]
|
|
|
|
|
- if len(chunk) >= 16:
|
|
|
|
|
- parsed = parse_device_response(chunk)
|
|
|
|
|
- if "error" not in parsed:
|
|
|
|
|
- if not any(p.get('uid') == parsed['uid'] for p in responses):
|
|
|
|
|
- responses.append(parsed)
|
|
|
|
|
- else:
|
|
|
|
|
- logger.info(f"no response or too short: {len(response) if response else 0}B")
|
|
|
|
|
-
|
|
|
|
|
- logger.info(f"broadcast done, got {len(responses)} valid unique responses")
|
|
|
|
|
- return responses
|
|
|
|
|
|
|
+ timeout = timeout or self.default_timeout
|
|
|
|
|
+ # 先清空输入缓冲区,避免残留数据干扰
|
|
|
|
|
+ self.serial.flush_input()
|
|
|
|
|
+ time.sleep(0.05)
|
|
|
|
|
+
|
|
|
|
|
+ # 发送广播并等待响应;部分设备需要 2~3 秒才应答
|
|
|
|
|
+ response = self.serial.send_and_wait(request, timeout=timeout, min_response_bytes=16)
|
|
|
|
|
+
|
|
|
|
|
+ # 部分从机应答会被后台读取线程滞后收到,再额外等待一段收集窗口
|
|
|
|
|
+ # 注意:这里不能再次调用 send_and_wait,因为它会清空缓冲区
|
|
|
|
|
+ time.sleep(0.8)
|
|
|
|
|
+ extra = b''
|
|
|
|
|
+ with self.serial.lock:
|
|
|
|
|
+ while self.serial.raw_response_buffer:
|
|
|
|
|
+ try:
|
|
|
|
|
+ extra += bytes.fromhex(self.serial.raw_response_buffer.pop(0))
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ self.serial.raw_response_buffer.pop(0)
|
|
|
|
|
+ if self.serial.ser and self.serial.ser.is_open and self.serial.ser.in_waiting > 0:
|
|
|
|
|
+ extra += self.serial.ser.read(self.serial.ser.in_waiting)
|
|
|
|
|
+ if extra:
|
|
|
|
|
+ logger.info(f"broadcast extra response ({len(extra)}B): {extra.hex()}")
|
|
|
|
|
+ if response:
|
|
|
|
|
+ response += extra
|
|
|
|
|
+ else:
|
|
|
|
|
+ response = extra
|
|
|
|
|
+
|
|
|
|
|
+ responses = []
|
|
|
|
|
+ if response and len(response) >= 16:
|
|
|
|
|
+ logger.info(f"got raw response ({len(response)}B): {response.hex()}")
|
|
|
|
|
+ # RS485 半双工会回显已发送的数据,导致 response 开头可能有 5 字节回显
|
|
|
|
|
+ # 在 response 中搜索所有 00 41 标记,尝试从中提取 16 字节帧解析
|
|
|
|
|
+ for i in range(len(response) - 15 + 1):
|
|
|
|
|
+ if response[i] == 0x00 and response[i + 1] == 0x41:
|
|
|
|
|
+ chunk = response[i:i + 16]
|
|
|
|
|
+ if len(chunk) >= 16:
|
|
|
|
|
+ parsed = parse_device_response(chunk)
|
|
|
|
|
+ if "error" not in parsed:
|
|
|
|
|
+ if not any(p.get('uid') == parsed['uid'] for p in responses):
|
|
|
|
|
+ responses.append(parsed)
|
|
|
|
|
+ else:
|
|
|
|
|
+ logger.info(f"no response or too short: {len(response) if response else 0}B")
|
|
|
|
|
+
|
|
|
|
|
+ logger.info(f"broadcast done, got {len(responses)} valid unique responses")
|
|
|
|
|
+ return responses
|
|
|
|
|
|
|
|
def _next_available_address(self) -> int:
|
|
def _next_available_address(self) -> int:
|
|
|
"""从 1~247 中找一个未被 stored_devices 占用的地址"""
|
|
"""从 1~247 中找一个未被 stored_devices 占用的地址"""
|
|
@@ -160,104 +187,106 @@ class AddressConfigProtocol:
|
|
|
"""处理广播查询响应:给每个应答设备分配地址(优先使用已记录地址)"""
|
|
"""处理广播查询响应:给每个应答设备分配地址(优先使用已记录地址)"""
|
|
|
import time
|
|
import time
|
|
|
|
|
|
|
|
- results = []
|
|
|
|
|
-
|
|
|
|
|
- for resp in responses:
|
|
|
|
|
- if 'error' in resp:
|
|
|
|
|
- results.append({'status': 'error', 'message': resp['error']})
|
|
|
|
|
- continue
|
|
|
|
|
-
|
|
|
|
|
- uid = resp['uid']
|
|
|
|
|
- src_addr = resp.get('source_address', 0)
|
|
|
|
|
- device_address = self.stored_devices.get(uid)
|
|
|
|
|
-
|
|
|
|
|
- # 优先使用地址表中已保存的地址;没有则分配下一个可用地址
|
|
|
|
|
- if device_address:
|
|
|
|
|
- new_address = device_address
|
|
|
|
|
- logger.info(f"UID={uid} 源地址={src_addr},地址表存在记录,分配已有地址={new_address}")
|
|
|
|
|
- else:
|
|
|
|
|
- new_address = self._next_available_address()
|
|
|
|
|
- logger.info(f"UID={uid} 源地址={src_addr},地址表无记录,分配新地址={new_address}")
|
|
|
|
|
-
|
|
|
|
|
- request = build_assign_address(bytes.fromhex(uid), new_address)
|
|
|
|
|
- logger.info(f"发送分配地址帧: {request.hex()}")
|
|
|
|
|
- self.serial.flush_input()
|
|
|
|
|
- time.sleep(0.05)
|
|
|
|
|
- success, msg = self.serial.send_raw(request)
|
|
|
|
|
- if not success:
|
|
|
|
|
- logger.error(f"UID={uid} 分配地址帧发送失败: {msg}")
|
|
|
|
|
- results.append({
|
|
|
|
|
- 'status': 'error',
|
|
|
|
|
- 'uid': uid,
|
|
|
|
|
- 'message': f"send failed: {msg}"
|
|
|
|
|
- })
|
|
|
|
|
- continue
|
|
|
|
|
-
|
|
|
|
|
- # 等待并解析从机的 43 确认响应
|
|
|
|
|
- time.sleep(0.1)
|
|
|
|
|
- ack = self.serial.send_and_wait(b'', timeout=0.5, min_response_bytes=5)
|
|
|
|
|
- ack_parsed = None
|
|
|
|
|
- if ack and len(ack) >= 5:
|
|
|
|
|
- # 过滤回显/噪声,找到 43 响应帧
|
|
|
|
|
- for i in range(len(ack) - 4):
|
|
|
|
|
- if ack[i + 1] == 0x43:
|
|
|
|
|
- chunk = ack[i:i + 5]
|
|
|
|
|
- ack_parsed = parse_address_assignment_response(chunk)
|
|
|
|
|
- break
|
|
|
|
|
-
|
|
|
|
|
- if ack_parsed and 'error' not in ack_parsed:
|
|
|
|
|
- logger.info(f"UID={uid} 地址={new_address} 已确认 (响应: {ack_parsed.get('raw_data')})")
|
|
|
|
|
- self.stored_devices[uid] = new_address
|
|
|
|
|
- results.append({
|
|
|
|
|
- 'status': 'assigned',
|
|
|
|
|
- 'uid': uid,
|
|
|
|
|
- 'address': new_address,
|
|
|
|
|
- 'request': request.hex(),
|
|
|
|
|
- 'ack': ack_parsed.get('raw_data')
|
|
|
|
|
- })
|
|
|
|
|
- else:
|
|
|
|
|
- # 即使未收到确认也保存地址:部分从机不返回 43,但实际已生效
|
|
|
|
|
- logger.warning(f"UID={uid} 地址={new_address} 未收到 43 确认,仍视为已分配")
|
|
|
|
|
- self.stored_devices[uid] = new_address
|
|
|
|
|
- results.append({
|
|
|
|
|
- 'status': 'assigned',
|
|
|
|
|
- 'uid': uid,
|
|
|
|
|
- 'address': new_address,
|
|
|
|
|
- 'request': request.hex(),
|
|
|
|
|
- 'ack': ack.hex() if ack else None
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- return results
|
|
|
|
|
|
|
+ with self.config_lock:
|
|
|
|
|
+ results = []
|
|
|
|
|
+
|
|
|
|
|
+ for resp in responses:
|
|
|
|
|
+ if 'error' in resp:
|
|
|
|
|
+ results.append({'status': 'error', 'message': resp['error']})
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ uid = resp['uid']
|
|
|
|
|
+ src_addr = resp.get('source_address', 0)
|
|
|
|
|
+ device_address = self.stored_devices.get(uid)
|
|
|
|
|
+
|
|
|
|
|
+ # 优先使用地址表中已保存的地址;没有则分配下一个可用地址
|
|
|
|
|
+ if device_address:
|
|
|
|
|
+ new_address = device_address
|
|
|
|
|
+ logger.info(f"UID={uid} 源地址={src_addr},地址表存在记录,分配已有地址={new_address}")
|
|
|
|
|
+ else:
|
|
|
|
|
+ new_address = self._next_available_address()
|
|
|
|
|
+ logger.info(f"UID={uid} 源地址={src_addr},地址表无记录,分配新地址={new_address}")
|
|
|
|
|
+
|
|
|
|
|
+ request = build_assign_address(bytes.fromhex(uid), new_address)
|
|
|
|
|
+ logger.info(f"发送分配地址帧: {request.hex()}")
|
|
|
|
|
+ self.serial.flush_input()
|
|
|
|
|
+ time.sleep(0.05)
|
|
|
|
|
+ success, msg = self.serial.send_raw(request)
|
|
|
|
|
+ if not success:
|
|
|
|
|
+ logger.error(f"UID={uid} 分配地址帧发送失败: {msg}")
|
|
|
|
|
+ results.append({
|
|
|
|
|
+ 'status': 'error',
|
|
|
|
|
+ 'uid': uid,
|
|
|
|
|
+ 'message': f"send failed: {msg}"
|
|
|
|
|
+ })
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ # 等待并解析从机的 43 确认响应
|
|
|
|
|
+ time.sleep(0.1)
|
|
|
|
|
+ ack = self.serial.send_and_wait(b'', timeout=0.5, min_response_bytes=5)
|
|
|
|
|
+ ack_parsed = None
|
|
|
|
|
+ if ack and len(ack) >= 5:
|
|
|
|
|
+ # 过滤回显/噪声,找到 43 响应帧
|
|
|
|
|
+ for i in range(len(ack) - 4):
|
|
|
|
|
+ if ack[i + 1] == 0x43:
|
|
|
|
|
+ chunk = ack[i:i + 5]
|
|
|
|
|
+ ack_parsed = parse_address_assignment_response(chunk)
|
|
|
|
|
+ break
|
|
|
|
|
+
|
|
|
|
|
+ if ack_parsed and 'error' not in ack_parsed:
|
|
|
|
|
+ logger.info(f"UID={uid} 地址={new_address} 已确认 (响应: {ack_parsed.get('raw_data')})")
|
|
|
|
|
+ self.stored_devices[uid] = new_address
|
|
|
|
|
+ results.append({
|
|
|
|
|
+ 'status': 'assigned',
|
|
|
|
|
+ 'uid': uid,
|
|
|
|
|
+ 'address': new_address,
|
|
|
|
|
+ 'request': request.hex(),
|
|
|
|
|
+ 'ack': ack_parsed.get('raw_data')
|
|
|
|
|
+ })
|
|
|
|
|
+ else:
|
|
|
|
|
+ # 即使未收到确认也保存地址:部分从机不返回 43,但实际已生效
|
|
|
|
|
+ logger.warning(f"UID={uid} 地址={new_address} 未收到 43 确认,仍视为已分配")
|
|
|
|
|
+ self.stored_devices[uid] = new_address
|
|
|
|
|
+ results.append({
|
|
|
|
|
+ 'status': 'assigned',
|
|
|
|
|
+ 'uid': uid,
|
|
|
|
|
+ 'address': new_address,
|
|
|
|
|
+ 'request': request.hex(),
|
|
|
|
|
+ 'ack': ack.hex() if ack else None
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ return results
|
|
|
|
|
|
|
|
def auto_configure(self, timeout: float = None) -> dict:
|
|
def auto_configure(self, timeout: float = None) -> dict:
|
|
|
"""自动配置所有未配置的设备"""
|
|
"""自动配置所有未配置的设备"""
|
|
|
- logger.info("开始自动配置设备...")
|
|
|
|
|
|
|
+ with self.config_lock:
|
|
|
|
|
+ logger.info("开始自动配置设备...")
|
|
|
|
|
|
|
|
- responses = self.broadcast_query(timeout)
|
|
|
|
|
|
|
+ responses = self.broadcast_query(timeout)
|
|
|
|
|
|
|
|
- if not responses:
|
|
|
|
|
- return {
|
|
|
|
|
- 'success': True,
|
|
|
|
|
- 'message': '未发现任何从机设备',
|
|
|
|
|
- 'discovered': 0,
|
|
|
|
|
- 'configured': 0,
|
|
|
|
|
- 'results': []
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if not responses:
|
|
|
|
|
+ return {
|
|
|
|
|
+ 'success': True,
|
|
|
|
|
+ 'message': '未发现任何从机设备',
|
|
|
|
|
+ 'discovered': 0,
|
|
|
|
|
+ 'configured': 0,
|
|
|
|
|
+ 'results': []
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- results = self.process_responses(responses)
|
|
|
|
|
|
|
+ results = self.process_responses(responses)
|
|
|
|
|
|
|
|
- assigned = sum(1 for r in results if r.get('status') == 'assigned')
|
|
|
|
|
- errors = sum(1 for r in results if r.get('status') == 'error')
|
|
|
|
|
|
|
+ assigned = sum(1 for r in results if r.get('status') == 'assigned')
|
|
|
|
|
+ errors = sum(1 for r in results if r.get('status') == 'error')
|
|
|
|
|
|
|
|
- return {
|
|
|
|
|
- 'success': errors == 0,
|
|
|
|
|
- 'discovered': len(responses),
|
|
|
|
|
- 'confirmed': 0,
|
|
|
|
|
- 'assigned': assigned,
|
|
|
|
|
- 'errors': errors,
|
|
|
|
|
- 'results': results,
|
|
|
|
|
- 'stored_devices': self.get_stored_devices()
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ return {
|
|
|
|
|
+ 'success': errors == 0,
|
|
|
|
|
+ 'discovered': len(responses),
|
|
|
|
|
+ 'confirmed': 0,
|
|
|
|
|
+ 'assigned': assigned,
|
|
|
|
|
+ 'errors': errors,
|
|
|
|
|
+ 'results': results,
|
|
|
|
|
+ 'stored_devices': self.get_stored_devices()
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
def save_config(self, filepath: str) -> bool:
|
|
def save_config(self, filepath: str) -> bool:
|
|
|
"""保存配置到文件"""
|
|
"""保存配置到文件"""
|