|
|
|
|
@ -16,6 +16,7 @@ import threading
|
|
|
|
|
import struct
|
|
|
|
|
from enum import Enum, auto
|
|
|
|
|
from abc import ABC, abstractmethod
|
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
|
|
|
|
|
# # XBee 模組
|
|
|
|
|
# from xbee.frame import APIFrame
|
|
|
|
|
@ -28,7 +29,7 @@ from .utils import RingBuffer, setup_logger
|
|
|
|
|
logger = setup_logger(os.path.basename(__file__))
|
|
|
|
|
MODULE_VER = "0.80"
|
|
|
|
|
|
|
|
|
|
rx_module_ack = RingBuffer(capacity=256, buffer_id=253)
|
|
|
|
|
rx_module_ack = RingBuffer(capacity=64, buffer_id=253)
|
|
|
|
|
|
|
|
|
|
# ====================== State DEFINITION =====================
|
|
|
|
|
|
|
|
|
|
@ -39,6 +40,29 @@ class SerialMode(Enum):
|
|
|
|
|
XBEEAPI2AT = auto() # XBee API 模式
|
|
|
|
|
NOT_USE = auto() # 不使用
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ====================== AT Frame Data Classes =====================
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class ATRequest:
|
|
|
|
|
"""送往 dongle 的 AT 指令"""
|
|
|
|
|
command: bytes # 例如 b'DB'
|
|
|
|
|
parameter: bytes # 寫入用指令的參數,讀取型通常為空
|
|
|
|
|
frame_id: int # XBee frame id;0x00 表示不要求回應
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class ATResponse:
|
|
|
|
|
"""dongle 回傳的 AT Response (0x88)"""
|
|
|
|
|
frame_id: int
|
|
|
|
|
command: bytes
|
|
|
|
|
status: int
|
|
|
|
|
data: bytes
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def is_ok(self) -> bool:
|
|
|
|
|
return self.status == 0x00
|
|
|
|
|
|
|
|
|
|
# ====================== Frame Processor Base and Implementation =====================
|
|
|
|
|
|
|
|
|
|
class FrameProcessor(ABC):
|
|
|
|
|
@ -229,49 +253,51 @@ class ATCommandHandler:
|
|
|
|
|
"""由 SerialHandler 注入實際寫入 serial 的 callable"""
|
|
|
|
|
self.writer = writer
|
|
|
|
|
|
|
|
|
|
def send_command(self, command: bytes, parameter: bytes, frame_id: int):
|
|
|
|
|
"""
|
|
|
|
|
發送一筆 AT 指令給 dongle
|
|
|
|
|
- command: 2 bytes AT 指令名稱,例如 b'DB'
|
|
|
|
|
- parameter: 指令參數 bytes(讀取用指令通常為空)
|
|
|
|
|
- frame_id: XBee frame id,用來配對回應(0x00 表示不要求回應)
|
|
|
|
|
"""
|
|
|
|
|
def send_command(self, request: ATRequest):
|
|
|
|
|
"""發送一筆 AT 指令給 dongle"""
|
|
|
|
|
if self.writer is None:
|
|
|
|
|
logger.warning(
|
|
|
|
|
f"[{self.serial_port}] AT writer 尚未就緒,指令 {command!r} 丟棄"
|
|
|
|
|
f"[{self.serial_port}] AT writer 尚未就緒,指令 {request.command!r} 丟棄"
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
frame_bytes = self._build_at_request(command, parameter, frame_id)
|
|
|
|
|
self.writer(frame_bytes)
|
|
|
|
|
self.writer(self._build_at_request(request))
|
|
|
|
|
# logger.debug(
|
|
|
|
|
# f"[{self.serial_port}] send AT {command.decode(errors='replace')} "
|
|
|
|
|
# f"(frame_id=0x{frame_id:02X})"
|
|
|
|
|
# f"[{self.serial_port}] send AT {request.command.decode(errors='replace')} "
|
|
|
|
|
# f"(frame_id=0x{request.frame_id:02X})"
|
|
|
|
|
# ) # dev
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _build_at_request(command: bytes, parameter: bytes, frame_id: int) -> bytes:
|
|
|
|
|
"""
|
|
|
|
|
將 AT 指令組成 XBee API AT Command Request frame (0x08)
|
|
|
|
|
callers 必須明確指定 frame_id
|
|
|
|
|
"""
|
|
|
|
|
def _build_at_request(request: ATRequest) -> bytes:
|
|
|
|
|
"""將 ATRequest 組成 XBee API AT Command Request frame (0x08) bytes"""
|
|
|
|
|
frame_type = ATCommandHandler.FRAME_TYPE_AT_COMMAND
|
|
|
|
|
|
|
|
|
|
frame = struct.pack(">B", frame_type) + struct.pack(">B", frame_id)
|
|
|
|
|
frame += command + parameter
|
|
|
|
|
frame = struct.pack(">B", frame_type) + struct.pack(">B", request.frame_id)
|
|
|
|
|
frame += request.command + request.parameter
|
|
|
|
|
checksum = 0xFF - (sum(frame) & 0xFF)
|
|
|
|
|
return b'\x7E' + struct.pack(">H", len(frame)) + frame + struct.pack("B", checksum)
|
|
|
|
|
|
|
|
|
|
# ---- 接收端 ----
|
|
|
|
|
def handle_frame(self, frame: bytes) -> None:
|
|
|
|
|
"""接收一整個 AT Response frame,內部完成 parse + dispatch"""
|
|
|
|
|
"""
|
|
|
|
|
接收一整個 AT Response frame:
|
|
|
|
|
1. 解析成 ATResponse
|
|
|
|
|
2. 推進 rx_module_ack 供其他模組消費
|
|
|
|
|
3. 本地 dispatch 給對應的 _handle_xxx
|
|
|
|
|
"""
|
|
|
|
|
parsed = self._parse(frame)
|
|
|
|
|
if parsed is None:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
if not rx_module_ack.put(parsed):
|
|
|
|
|
logger.warning(
|
|
|
|
|
f"[{self.serial_port}] rx_module_ack overflow, drop {parsed.command!r}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self._dispatch(parsed)
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _parse(frame: bytes) -> dict:
|
|
|
|
|
def _parse(frame: bytes) -> ATResponse:
|
|
|
|
|
"""解析 AT Command Response (0x88);不符格式回傳 None"""
|
|
|
|
|
if len(frame) < 8:
|
|
|
|
|
return None
|
|
|
|
|
@ -279,44 +305,40 @@ class ATCommandHandler:
|
|
|
|
|
if frame[3] != 0x88:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
frame_id = frame[4]
|
|
|
|
|
at_command = frame[5:7]
|
|
|
|
|
status = frame[7]
|
|
|
|
|
data = frame[8:] if len(frame) > 8 else b''
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
'frame_id': frame_id,
|
|
|
|
|
'command': at_command,
|
|
|
|
|
'status': status,
|
|
|
|
|
'data': data,
|
|
|
|
|
'is_ok': status == 0x00,
|
|
|
|
|
}
|
|
|
|
|
return ATResponse(
|
|
|
|
|
frame_id=frame[4],
|
|
|
|
|
command=frame[5:7],
|
|
|
|
|
status=frame[7],
|
|
|
|
|
data=frame[8:] if len(frame) > 8 else b'',
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def _dispatch(self, response: dict) -> None:
|
|
|
|
|
def _dispatch(self, response: ATResponse) -> None:
|
|
|
|
|
"""根據 AT 指令類型分派處理"""
|
|
|
|
|
# print(f"[{self.serial_port}] AT Response: {response}") # dev
|
|
|
|
|
|
|
|
|
|
if not response['is_ok']:
|
|
|
|
|
if not response.is_ok:
|
|
|
|
|
logger.warning(
|
|
|
|
|
f"[{self.serial_port}] AT {response['command'].decode()} "
|
|
|
|
|
f"失敗,狀態碼: {response['status']}"
|
|
|
|
|
f"[{self.serial_port}] AT {response.command.decode()} "
|
|
|
|
|
f"失敗,狀態碼: {response.status}"
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
handler = self.handlers.get(response['command'])
|
|
|
|
|
handler = self.handlers.get(response.command)
|
|
|
|
|
if handler:
|
|
|
|
|
handler(response['data'])
|
|
|
|
|
handler(response.data)
|
|
|
|
|
else:
|
|
|
|
|
logger.debug(
|
|
|
|
|
f"[{self.serial_port}] 未處理的 AT 指令: "
|
|
|
|
|
f"{response['command'].decode()}"
|
|
|
|
|
f"{response.command.decode()}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def _handle_rssi(self, data: bytes):
|
|
|
|
|
"""處理 DB (RSSI) 回應:單 byte 無號值,單位 dBm"""
|
|
|
|
|
if data:
|
|
|
|
|
print(f"[{self.serial_port}] RSSI = -{data[0]} dBm") # dev
|
|
|
|
|
|
|
|
|
|
pass
|
|
|
|
|
# if data:
|
|
|
|
|
# print(f"[{self.serial_port}] RSSI = -{data[0]} dBm") # dev
|
|
|
|
|
# logger.debug(f"[{self.serial_port}] RSSI = -{data[0]} dBm") # dev
|
|
|
|
|
|
|
|
|
|
def _handle_serial_high(self, data: bytes):
|
|
|
|
|
"""處理 SH (Serial Number High)"""
|
|
|
|
|
pass
|
|
|
|
|
@ -644,13 +666,11 @@ class serial_manager:
|
|
|
|
|
ret[key] = obj.serial_port
|
|
|
|
|
return ret
|
|
|
|
|
|
|
|
|
|
def send_at_command(self, serial_id, command: bytes, parameter: bytes = b'', frame_id: int = 0x52) -> bool:
|
|
|
|
|
def send_at_command(self, serial_id, request: ATRequest) -> bool:
|
|
|
|
|
"""
|
|
|
|
|
對指定 serial_id 的 XBee dongle 發送一筆 AT 指令(thread-safe)
|
|
|
|
|
- serial_id: create_serial_link 取得的編號
|
|
|
|
|
- command: 例如 b'DB'
|
|
|
|
|
- parameter: 指令參數,讀取型指令通常為空
|
|
|
|
|
- frame_id: XBee frame id,0x00 代表不要求回應
|
|
|
|
|
- request: ATRequest 物件,攜帶 command / parameter / frame_id
|
|
|
|
|
回傳是否成功排進事件圈
|
|
|
|
|
"""
|
|
|
|
|
if not self.running or not self.loop:
|
|
|
|
|
@ -670,9 +690,7 @@ class serial_manager:
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
at_handler = serial_obj.serial_handler.processor.at_handler
|
|
|
|
|
self.loop.call_soon_threadsafe(
|
|
|
|
|
at_handler.send_command, command, parameter, frame_id
|
|
|
|
|
)
|
|
|
|
|
self.loop.call_soon_threadsafe(at_handler.send_command, request)
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
@ -727,8 +745,9 @@ if __name__ == '__main__':
|
|
|
|
|
|
|
|
|
|
# 等 connection_made 完成 writer 注入,再發一筆 AT 指令測試
|
|
|
|
|
time.sleep(5)
|
|
|
|
|
rssi_request = ATRequest(command=b'DB', parameter=b'', frame_id=0x52)
|
|
|
|
|
for i in range(60):
|
|
|
|
|
sm.send_at_command(1, b'DB', frame_id=0x52)
|
|
|
|
|
sm.send_at_command(1, rssi_request)
|
|
|
|
|
time.sleep(1)
|
|
|
|
|
|
|
|
|
|
sm.remove_serial_link(1)
|
|
|
|
|
|