308 lines
9.5 KiB
Python
308 lines
9.5 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
AT客户端模块
|
||
负责4G模块的AT命令通信和URC解析
|
||
"""
|
||
import _thread
|
||
from maix import time
|
||
import re
|
||
import threading
|
||
|
||
class ATClient:
|
||
"""
|
||
单读者 AT/URC 客户端:唯一读取 uart4g,避免 tcp_main/at()/OTA 抢读导致 EOF / 丢包。
|
||
- send(cmd, expect, timeout_ms) : 发送 AT 并等待 expect
|
||
- pop_tcp_payload() : 获取 +MIPURC:"rtcp" 的 payload(已按长度裁剪)
|
||
- pop_http_event() : 获取 +MHTTPURC 事件(header/content)
|
||
"""
|
||
def __init__(self, uart_obj):
|
||
self.uart = uart_obj
|
||
self._cmd_lock = threading.Lock()
|
||
self._q_lock = threading.Lock()
|
||
self._rx = b""
|
||
self._tcp_payloads = []
|
||
self._http_events = []
|
||
|
||
# 当前命令等待状态(仅允许单命令 in-flight)
|
||
self._waiting = False
|
||
self._expect = b"OK"
|
||
self._resp = b""
|
||
|
||
self._running = False
|
||
|
||
def start(self):
|
||
if self._running:
|
||
return
|
||
self._running = True
|
||
_thread.start_new_thread(self._reader_loop, ())
|
||
|
||
def stop(self):
|
||
self._running = False
|
||
|
||
def flush(self):
|
||
"""清空内部缓存与队列(用于 OTA/异常恢复)"""
|
||
with self._q_lock:
|
||
self._rx = b""
|
||
self._tcp_payloads.clear()
|
||
self._http_events.clear()
|
||
self._resp = b""
|
||
|
||
def pop_tcp_payload(self):
|
||
with self._q_lock:
|
||
if self._tcp_payloads:
|
||
return self._tcp_payloads.pop(0)
|
||
return None
|
||
|
||
def pop_http_event(self):
|
||
with self._q_lock:
|
||
if self._http_events:
|
||
return self._http_events.pop(0)
|
||
return None
|
||
|
||
def _push_tcp_payload(self, payload: bytes):
|
||
# 注意:在 _reader_loop 内部解析 URC 时已经持有 _q_lock,
|
||
# 这里不要再次 acquire(锁不可重入,会死锁)。
|
||
self._tcp_payloads.append(payload)
|
||
|
||
def _push_http_event(self, ev):
|
||
# 同上:避免在 _reader_loop 持锁期间二次 acquire
|
||
self._http_events.append(ev)
|
||
|
||
def send(self, cmd: str, expect: str = "OK", timeout_ms: int = 2000):
|
||
"""
|
||
发送 AT 命令并等待 expect(子串匹配)。
|
||
注意:expect=">" 用于等待 prompt。
|
||
"""
|
||
expect_b = expect.encode() if isinstance(expect, str) else expect
|
||
with self._cmd_lock:
|
||
# 初始化等待
|
||
self._waiting = True
|
||
self._expect = expect_b
|
||
self._resp = b""
|
||
|
||
# 发送
|
||
if cmd:
|
||
# 注意:这里不要再用 uart4g_lock(否则外层已经持锁时会死锁)。
|
||
# 写入由 _cmd_lock 串行化即可。
|
||
self.uart.write((cmd + "\r\n").encode())
|
||
|
||
t0 = time.ticks_ms()
|
||
while time.ticks_ms() - t0 < timeout_ms:
|
||
if (not self._waiting) or (self._expect in self._resp):
|
||
self._waiting = False
|
||
break
|
||
time.sleep_ms(5)
|
||
|
||
# 超时也返回已收集内容(便于诊断)
|
||
self._waiting = False
|
||
try:
|
||
return self._resp.decode(errors="ignore")
|
||
except:
|
||
return str(self._resp)
|
||
|
||
def _find_urc_tag(self, tag: bytes):
|
||
"""
|
||
只在"真正的 URC 边界"查找 tag,避免误命中 HTTP payload 内容。
|
||
规则:tag 必须出现在 buffer 开头,或紧跟在 b"\\r\\n" 后面。
|
||
"""
|
||
try:
|
||
i = 0
|
||
rx = self._rx
|
||
while True:
|
||
j = rx.find(tag, i)
|
||
if j < 0:
|
||
return -1
|
||
if j == 0:
|
||
return 0
|
||
if j >= 2 and rx[j - 2:j] == b"\r\n":
|
||
return j
|
||
i = j + 1
|
||
except:
|
||
return -1
|
||
|
||
def _parse_mipurc_rtcp(self):
|
||
"""
|
||
解析:+MIPURC: "rtcp",<link_id>,<len>,<payload...>
|
||
之前硬编码 link_id=0 会导致在多连接/重连场景下收不到数据。
|
||
"""
|
||
prefix = b'+MIPURC: "rtcp",'
|
||
i = self._find_urc_tag(prefix)
|
||
if i < 0:
|
||
return False
|
||
# 丢掉前置噪声
|
||
if i > 0:
|
||
self._rx = self._rx[i:]
|
||
i = 0
|
||
|
||
j = len(prefix)
|
||
# 解析 link_id
|
||
k = j
|
||
while k < len(self._rx) and 48 <= self._rx[k] <= 57:
|
||
k += 1
|
||
if k == j or k >= len(self._rx):
|
||
return False
|
||
if self._rx[k:k+1] != b",":
|
||
self._rx = self._rx[1:]
|
||
return True
|
||
try:
|
||
link_id = int(self._rx[j:k].decode())
|
||
except:
|
||
self._rx = self._rx[1:]
|
||
return True
|
||
|
||
# 解析 len
|
||
j2 = k + 1
|
||
k2 = j2
|
||
while k2 < len(self._rx) and 48 <= self._rx[k2] <= 57:
|
||
k2 += 1
|
||
if k2 == j2 or k2 >= len(self._rx):
|
||
return False
|
||
if self._rx[k2:k2+1] != b",":
|
||
self._rx = self._rx[1:]
|
||
return True
|
||
try:
|
||
n = int(self._rx[j2:k2].decode())
|
||
except:
|
||
self._rx = self._rx[1:]
|
||
return True
|
||
|
||
payload_start = k2 + 1
|
||
payload_end = payload_start + n
|
||
if len(self._rx) < payload_end:
|
||
return False # payload 未收齐
|
||
|
||
payload = self._rx[payload_start:payload_end]
|
||
# 把 link_id 一起带上,便于上层过滤(如果需要)
|
||
self._push_tcp_payload((link_id, payload))
|
||
self._rx = self._rx[payload_end:]
|
||
return True
|
||
|
||
def _parse_mhttpurc_header(self):
|
||
tag = b'+MHTTPURC: "header",'
|
||
i = self._find_urc_tag(tag)
|
||
if i < 0:
|
||
return False
|
||
if i > 0:
|
||
self._rx = self._rx[i:]
|
||
i = 0
|
||
|
||
# header: +MHTTPURC: "header",<id>,<code>,<hdr_len>,<hdr_text...>
|
||
j = len(tag)
|
||
comma_count = 0
|
||
k = j
|
||
while k < len(self._rx) and comma_count < 3:
|
||
if self._rx[k:k+1] == b",":
|
||
comma_count += 1
|
||
k += 1
|
||
if comma_count < 3:
|
||
return False
|
||
|
||
prefix = self._rx[:k]
|
||
m = re.search(rb'\+MHTTPURC: "header",\s*(\d+),\s*(\d+),\s*(\d+),', prefix)
|
||
if not m:
|
||
self._rx = self._rx[1:]
|
||
return True
|
||
urc_id = int(m.group(1))
|
||
code = int(m.group(2))
|
||
hdr_len = int(m.group(3))
|
||
|
||
text_start = k
|
||
text_end = text_start + hdr_len
|
||
if len(self._rx) < text_end:
|
||
return False
|
||
|
||
hdr_text = self._rx[text_start:text_end].decode("utf-8", "ignore")
|
||
self._push_http_event(("header", urc_id, code, hdr_text))
|
||
self._rx = self._rx[text_end:]
|
||
return True
|
||
|
||
def _parse_mhttpurc_content(self):
|
||
tag = b'+MHTTPURC: "content",'
|
||
i = self._find_urc_tag(tag)
|
||
if i < 0:
|
||
return False
|
||
if i > 0:
|
||
self._rx = self._rx[i:]
|
||
i = 0
|
||
|
||
# content: +MHTTPURC: "content",<id>,<total>,<sum>,<cur>,<payload...>
|
||
j = len(tag)
|
||
comma_count = 0
|
||
k = j
|
||
while k < len(self._rx) and comma_count < 4:
|
||
if self._rx[k:k+1] == b",":
|
||
comma_count += 1
|
||
k += 1
|
||
if comma_count < 4:
|
||
return False
|
||
|
||
prefix = self._rx[:k]
|
||
m = re.search(rb'\+MHTTPURC: "content",\s*(\d+),\s*(\d+),\s*(\d+),\s*(\d+),', prefix)
|
||
if not m:
|
||
self._rx = self._rx[1:]
|
||
return True
|
||
urc_id = int(m.group(1))
|
||
total_len = int(m.group(2))
|
||
sum_len = int(m.group(3))
|
||
cur_len = int(m.group(4))
|
||
|
||
payload_start = k
|
||
payload_end = payload_start + cur_len
|
||
if len(self._rx) < payload_end:
|
||
return False
|
||
|
||
payload = self._rx[payload_start:payload_end]
|
||
self._push_http_event(("content", urc_id, total_len, sum_len, cur_len, payload))
|
||
self._rx = self._rx[payload_end:]
|
||
return True
|
||
|
||
def _reader_loop(self):
|
||
while self._running:
|
||
# 关键:UART 驱动偶发 read failed,必须兜住,否则线程挂了 OTA/TCP 都会卡死
|
||
try:
|
||
d = self.uart.read(4096) # 8192 在一些驱动上更容易触发 read failed
|
||
except Exception as e:
|
||
try:
|
||
print("[ATClient] uart read failed:", e)
|
||
except:
|
||
pass
|
||
time.sleep_ms(50)
|
||
continue
|
||
|
||
if not d:
|
||
time.sleep_ms(1)
|
||
continue
|
||
|
||
with self._q_lock:
|
||
self._rx += d
|
||
if self._waiting:
|
||
self._resp += d
|
||
|
||
while True:
|
||
progressed = (
|
||
self._parse_mipurc_rtcp()
|
||
or self._parse_mhttpurc_header()
|
||
or self._parse_mhttpurc_content()
|
||
)
|
||
if not progressed:
|
||
break
|
||
|
||
# 使用 ota_manager 访问 ota_in_progress
|
||
try:
|
||
from ota_manager import ota_manager
|
||
ota_flag = ota_manager.ota_in_progress
|
||
except:
|
||
ota_flag = False
|
||
|
||
has_http_hint = (b"+MHTTP" in self._rx) or (b"+MHTTPURC" in self._rx)
|
||
if ota_flag or has_http_hint:
|
||
if len(self._rx) > 512 * 1024:
|
||
self._rx = self._rx[-256 * 1024:]
|
||
else:
|
||
if len(self._rx) > 16384:
|
||
self._rx = self._rx[-4096:]
|
||
|
||
|
||
|