esp32 不寫死sysid
This commit is contained in:
+413
-30
@@ -5,30 +5,72 @@ import gc
|
|||||||
|
|
||||||
# =========================================================
|
# =========================================================
|
||||||
# ESP32 / MicroPython:UAV 端 XBee <-> Flight Controller Bridge
|
# ESP32 / MicroPython:UAV 端 XBee <-> Flight Controller Bridge
|
||||||
# Packet-size TDMA + DONE 標籤版
|
# Packet-size TDMA + DONE + SYSID / GCS address 自動學習版
|
||||||
#
|
#
|
||||||
# 核心行為:
|
# 對應新版 GCS discovery 設計:
|
||||||
# 1. FC -> ESP32:持續把 MAVLink bytes 存進 tx_buf。
|
# 1. GCS 開機前 30 秒自動送 DISC。
|
||||||
# 2. GCS -> ESP32:收到 POLL + SYSID + quota_bytes 才允許上行傳送。
|
# 2. GCS 之後停止自動 DISC。
|
||||||
# 3. ESP32 -> GCS:每次最多送 quota_bytes,但只送「完整 MAVLink frame」。
|
# 3. 若後續新增 UAV,可在 GCS GUI 按 Scan UAV,送 DSCF 強制掃描。
|
||||||
# 4. ESP32 -> GCS:送完後獨立送 DONE report,GCS 收到後可立即換下一台 UAV。
|
|
||||||
#
|
#
|
||||||
# 注意:DONE 不是接在 MAVLink bytes 後面,而是獨立 RF payload,避免污染 MAVLink stream。
|
# ESP32 端核心行為:
|
||||||
|
# 1. FC -> ESP32:持續讀 MAVLink bytes,存進 tx_buf。
|
||||||
|
# 2. ESP32 從 FC MAVLink frame header 自動學自己的 MY_SYSID。
|
||||||
|
# 3. ESP32 收到 GCS 的 DISC / DSCF / POLL 時,從 XBee 0x90 src64 自動學 DEST_64。
|
||||||
|
# 4. 收到 DISC:
|
||||||
|
# 同一個 GCS 只回一次 HELO,避免 HELO 一直洗版。
|
||||||
|
# 5. 收到 DSCF:
|
||||||
|
# 強制回 HELO,給 GCS 手動重新掃描用。
|
||||||
|
# 6. 收到 POLL + SYSID + quota_bytes:
|
||||||
|
# 若 target_sysid == MY_SYSID,才根據 quota_bytes 傳完整 MAVLink frames。
|
||||||
|
# 7. 傳完本輪資料後,獨立送 DONE report。
|
||||||
|
#
|
||||||
|
# 注意:
|
||||||
|
# - DONE / HELO 都是獨立 RF payload,不會接在 MAVLink stream 後面。
|
||||||
|
# - MY_SYSID 不寫死,由飛控 MAVLink 自動學。
|
||||||
|
# - DEST_64 不寫死,由 GCS 封包的 XBee 0x90 src64 自動學。
|
||||||
# =========================================================
|
# =========================================================
|
||||||
|
|
||||||
|
|
||||||
# ================= 設定區 =================
|
# ================= 設定區 =================
|
||||||
FC_BAUDRATE = 115200
|
FC_BAUDRATE = 115200
|
||||||
XB_BAUDRATE = 115200
|
XB_BAUDRATE = 115200
|
||||||
|
|
||||||
# 這裡要改成你的地面站 / coordinator XBee 64-bit address
|
# 不寫死地面站 XBee address。
|
||||||
DEST_64 = b'\x00\x13\xA2\x00\x42\x5B\x9D\xC8'
|
# 收到 GCS 的 DISC / DSCF / POLL / 下行 MAVLink 時,
|
||||||
|
# 從 XBee 0x90 frame 的 src64 自動學到。
|
||||||
|
DEST_64 = None
|
||||||
|
|
||||||
|
# 不寫死每台 UAV 的 SYSID。
|
||||||
|
# 會從 FC -> ESP32 的 MAVLink frame header 自動學到。
|
||||||
|
MY_SYSID = None
|
||||||
|
|
||||||
|
# 為了避免雜訊誤判 SYSID,要求連續確認幾次相同 SYSID 才正式採用。
|
||||||
|
SYSID_LEARN_CONFIRM_COUNT = 3
|
||||||
|
_sysid_candidate = None
|
||||||
|
_sysid_candidate_count = 0
|
||||||
|
|
||||||
# XBee API Transmit Request 內每次 RF payload 最大切片。
|
# XBee API Transmit Request 內每次 RF payload 最大切片。
|
||||||
# 900HP / DigiMesh 實測常用 80~100 bytes 較穩。
|
# 900HP / DigiMesh 實測常用 80~100 bytes 較穩。
|
||||||
XBEE_MAX_PAYLOAD = 100
|
XBEE_MAX_PAYLOAD = 100
|
||||||
|
|
||||||
# 每台 UAV 都要設定成自己的 SYSID
|
# GCS -> UAV discovery:
|
||||||
MY_SYSID = 15 # 第二台可改 10,第三台可改 15,依你的 MAVLink SYSID 設定
|
# DISC:一般 discovery,同一個 GCS 只回一次 HELO。
|
||||||
|
# DSCF:force discovery,收到後強制回 HELO,給 GUI Scan UAV 按鍵用。
|
||||||
|
DISC_MAGIC = b'DISC'
|
||||||
|
DISC_FORCE_MAGIC = b'DSCF'
|
||||||
|
|
||||||
|
# UAV -> GCS hello:
|
||||||
|
# HELO + sysid(1)
|
||||||
|
HELLO_MAGIC = b'HELO'
|
||||||
|
|
||||||
|
# 一般 HELO 最小間隔,避免短時間內重複回太多。
|
||||||
|
HELLO_MIN_INTERVAL_MS = 1000
|
||||||
|
_last_hello_ms = 0
|
||||||
|
|
||||||
|
# 對同一個 GCS 是否已經送過 HELO。
|
||||||
|
# 收到 DISC 時,如果 HELLO_SENT=True,就不再回 HELO。
|
||||||
|
# 收到 DSCF 時,不管 HELLO_SENT,都強制回 HELO。
|
||||||
|
HELLO_SENT = False
|
||||||
|
|
||||||
# GCS -> UAV poll 格式:
|
# GCS -> UAV poll 格式:
|
||||||
# 舊版:b'POLL' + sysid(1)
|
# 舊版:b'POLL' + sysid(1)
|
||||||
@@ -43,21 +85,86 @@ DONE_MAGIC = b'DONE'
|
|||||||
# ESP32 RAM 有限,不要讓 FC 資料無限累積
|
# ESP32 RAM 有限,不要讓 FC 資料無限累積
|
||||||
MAX_BUF_SIZE = 6144
|
MAX_BUF_SIZE = 6144
|
||||||
READ_FC_BYTES = 250
|
READ_FC_BYTES = 250
|
||||||
|
|
||||||
|
# UART 腳位。若你的接線不同,只要改這裡。
|
||||||
|
FC_UART_ID = 1
|
||||||
|
FC_TX_PIN = 32
|
||||||
|
FC_RX_PIN = 33
|
||||||
|
|
||||||
|
XB_UART_ID = 2
|
||||||
|
XB_TX_PIN = 25
|
||||||
|
XB_RX_PIN = 26
|
||||||
# =========================================
|
# =========================================
|
||||||
|
|
||||||
uart_fc = UART(1, baudrate=FC_BAUDRATE, tx=32, rx=33, rxbuf=4096)
|
|
||||||
uart_xb = UART(2, baudrate=XB_BAUDRATE, tx=25, rx=26, rxbuf=4096)
|
uart_fc = UART(FC_UART_ID, baudrate=FC_BAUDRATE, tx=FC_TX_PIN, rx=FC_RX_PIN, rxbuf=4096)
|
||||||
|
uart_xb = UART(XB_UART_ID, baudrate=XB_BAUDRATE, tx=XB_TX_PIN, rx=XB_RX_PIN, rxbuf=4096)
|
||||||
|
|
||||||
tx_buf = bytearray() # FC -> GCS 等待 TDMA poll 的 MAVLink stream
|
tx_buf = bytearray() # FC -> GCS 等待 TDMA poll 的 MAVLink stream
|
||||||
rx_buf = bytearray() # XBee API frame parser buffer
|
rx_buf = bytearray() # XBee API frame parser buffer
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# 基本工具函式
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
def get_checksum(data):
|
def get_checksum(data):
|
||||||
return 0xFF - (sum(data) & 0xFF)
|
return 0xFF - (sum(data) & 0xFF)
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_addr64(addr):
|
||||||
|
if addr is None:
|
||||||
|
return False
|
||||||
|
if len(addr) != 8:
|
||||||
|
return False
|
||||||
|
if addr == b'\x00\x00\x00\x00\x00\x00\x00\x00':
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def learn_dest64_from_src64(src64):
|
||||||
|
"""
|
||||||
|
UAV 收到 GCS 的 XBee 0x90 frame 時,
|
||||||
|
0x90 裡面的 src64 就是 GCS / coordinator XBee 的 64-bit address。
|
||||||
|
|
||||||
|
若第一次學到 GCS address:
|
||||||
|
DEST_64 = src64
|
||||||
|
HELLO_SENT = False
|
||||||
|
|
||||||
|
若後來收到不同 src64:
|
||||||
|
視為地面站 XBee 改變,更新 DEST_64,並允許重新回 HELO。
|
||||||
|
"""
|
||||||
|
global DEST_64
|
||||||
|
global HELLO_SENT
|
||||||
|
|
||||||
|
if not is_valid_addr64(src64):
|
||||||
|
return False
|
||||||
|
|
||||||
|
src64 = bytes(src64)
|
||||||
|
|
||||||
|
if DEST_64 is None:
|
||||||
|
DEST_64 = src64
|
||||||
|
HELLO_SENT = False
|
||||||
|
return True
|
||||||
|
|
||||||
|
if DEST_64 != src64:
|
||||||
|
DEST_64 = src64
|
||||||
|
HELLO_SENT = False
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def build_api_tx_frame(payload):
|
def build_api_tx_frame(payload):
|
||||||
"""建立 XBee API frame type 0x10,把 payload 送到 DEST_64。"""
|
"""
|
||||||
|
建立 XBee API frame type 0x10,把 payload 送到 DEST_64。
|
||||||
|
若 DEST_64 尚未學到,回傳 None。
|
||||||
|
"""
|
||||||
|
global DEST_64
|
||||||
|
|
||||||
|
if DEST_64 is None:
|
||||||
|
return None
|
||||||
|
|
||||||
frame_content = bytearray()
|
frame_content = bytearray()
|
||||||
frame_content.append(0x10) # Transmit Request
|
frame_content.append(0x10) # Transmit Request
|
||||||
frame_content.append(0x00) # Frame ID = 0,不要求 XBee ACK,降低延遲
|
frame_content.append(0x00) # Frame ID = 0,不要求 XBee ACK,降低延遲
|
||||||
@@ -68,17 +175,25 @@ def build_api_tx_frame(payload):
|
|||||||
frame_content.extend(payload)
|
frame_content.extend(payload)
|
||||||
|
|
||||||
length = len(frame_content)
|
length = len(frame_content)
|
||||||
|
|
||||||
packet = bytearray()
|
packet = bytearray()
|
||||||
packet.append(0x7E)
|
packet.append(0x7E)
|
||||||
packet.append((length >> 8) & 0xFF)
|
packet.append((length >> 8) & 0xFF)
|
||||||
packet.append(length & 0xFF)
|
packet.append(length & 0xFF)
|
||||||
packet.extend(frame_content)
|
packet.extend(frame_content)
|
||||||
packet.append(get_checksum(frame_content))
|
packet.append(get_checksum(frame_content))
|
||||||
|
|
||||||
return packet
|
return packet
|
||||||
|
|
||||||
|
|
||||||
def send_to_xbee_chunked(payload):
|
def send_to_xbee_chunked(payload):
|
||||||
"""把 payload 依 XBEE_MAX_PAYLOAD 切成多個 XBee API TX frame 送出。"""
|
"""
|
||||||
|
把 payload 依 XBEE_MAX_PAYLOAD 切成多個 XBee API TX frame 送出。
|
||||||
|
若 DEST_64 尚未學到,直接不送,回傳 False。
|
||||||
|
"""
|
||||||
|
if DEST_64 is None:
|
||||||
|
return False
|
||||||
|
|
||||||
total_len = len(payload)
|
total_len = len(payload)
|
||||||
sent_len = 0
|
sent_len = 0
|
||||||
|
|
||||||
@@ -86,22 +201,95 @@ def send_to_xbee_chunked(payload):
|
|||||||
end_len = min(sent_len + XBEE_MAX_PAYLOAD, total_len)
|
end_len = min(sent_len + XBEE_MAX_PAYLOAD, total_len)
|
||||||
chunk = payload[sent_len:end_len]
|
chunk = payload[sent_len:end_len]
|
||||||
sent_len = end_len
|
sent_len = end_len
|
||||||
uart_xb.write(build_api_tx_frame(chunk))
|
|
||||||
|
pkt = build_api_tx_frame(chunk)
|
||||||
|
if pkt is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
uart_xb.write(pkt)
|
||||||
|
|
||||||
# 很短的讓步,避免 ESP32 UART/XBee 本地 buffer 瞬間塞爆
|
# 很短的讓步,避免 ESP32 UART/XBee 本地 buffer 瞬間塞爆
|
||||||
time.sleep_ms(1)
|
time.sleep_ms(1)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# HELO / DONE 回報
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def send_hello_report(force=False):
|
||||||
|
"""
|
||||||
|
UAV 回報自己存在:
|
||||||
|
HELO + MY_SYSID
|
||||||
|
|
||||||
|
force=False:
|
||||||
|
若 HELLO_SENT=True,代表同一個 GCS 已經註冊過,不再回 HELO。
|
||||||
|
|
||||||
|
force=True:
|
||||||
|
不管 HELLO_SENT,都重新回 HELO。
|
||||||
|
用於 GCS GUI 按 Scan UAV 送 DSCF 時。
|
||||||
|
"""
|
||||||
|
global _last_hello_ms
|
||||||
|
global HELLO_SENT
|
||||||
|
|
||||||
|
if MY_SYSID is None:
|
||||||
|
return False
|
||||||
|
if DEST_64 is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if HELLO_SENT and not force:
|
||||||
|
return False
|
||||||
|
|
||||||
|
now = time.ticks_ms()
|
||||||
|
if not force:
|
||||||
|
if time.ticks_diff(now, _last_hello_ms) < HELLO_MIN_INTERVAL_MS:
|
||||||
|
return False
|
||||||
|
|
||||||
|
payload = HELLO_MAGIC + struct.pack('>B', MY_SYSID)
|
||||||
|
pkt = build_api_tx_frame(payload)
|
||||||
|
if pkt is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
uart_xb.write(pkt)
|
||||||
|
_last_hello_ms = now
|
||||||
|
HELLO_SENT = True
|
||||||
|
|
||||||
|
time.sleep_ms(1)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def send_done_report(sent_len, remain_len):
|
def send_done_report(sent_len, remain_len):
|
||||||
"""送出 TDMA DONE 標籤。此封包不會轉給飛控,只給 GCS scheduler 使用。"""
|
"""
|
||||||
|
送出 TDMA DONE 標籤。
|
||||||
|
此封包不會轉給飛控,只給 GCS scheduler 使用。
|
||||||
|
|
||||||
|
DONE payload:
|
||||||
|
b'DONE' + sysid(1) + sent_len(2) + remain_len(2)
|
||||||
|
"""
|
||||||
|
if MY_SYSID is None:
|
||||||
|
return False
|
||||||
|
if DEST_64 is None:
|
||||||
|
return False
|
||||||
|
|
||||||
if sent_len > 65535:
|
if sent_len > 65535:
|
||||||
sent_len = 65535
|
sent_len = 65535
|
||||||
if remain_len > 65535:
|
if remain_len > 65535:
|
||||||
remain_len = 65535
|
remain_len = 65535
|
||||||
|
|
||||||
payload = DONE_MAGIC + struct.pack('>BHH', MY_SYSID, sent_len, remain_len)
|
payload = DONE_MAGIC + struct.pack('>BHH', MY_SYSID, sent_len, remain_len)
|
||||||
uart_xb.write(build_api_tx_frame(payload))
|
pkt = build_api_tx_frame(payload)
|
||||||
time.sleep_ms(1)
|
if pkt is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
uart_xb.write(pkt)
|
||||||
|
time.sleep_ms(1)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# MAVLink frame 解析與 MY_SYSID 自動學習
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
def find_first_mavlink_magic(buf):
|
def find_first_mavlink_magic(buf):
|
||||||
"""找 MAVLink v1/v2 magic:0xFE 或 0xFD。"""
|
"""找 MAVLink v1/v2 magic:0xFE 或 0xFD。"""
|
||||||
@@ -122,6 +310,7 @@ def mavlink_frame_length(buf, start_idx):
|
|||||||
|
|
||||||
MAVLink v1:
|
MAVLink v1:
|
||||||
magic 0xFE, payload_len at byte 1, total = payload_len + 8
|
magic 0xFE, payload_len at byte 1, total = payload_len + 8
|
||||||
|
|
||||||
MAVLink v2:
|
MAVLink v2:
|
||||||
magic 0xFD, payload_len at byte 1, incompat_flags at byte 2,
|
magic 0xFD, payload_len at byte 1, incompat_flags at byte 2,
|
||||||
total = payload_len + 12,若 signed 則 +13
|
total = payload_len + 12,若 signed 則 +13
|
||||||
@@ -132,7 +321,6 @@ def mavlink_frame_length(buf, start_idx):
|
|||||||
magic = buf[start_idx]
|
magic = buf[start_idx]
|
||||||
|
|
||||||
if magic == 0xFE:
|
if magic == 0xFE:
|
||||||
# v1 至少需要 magic + len
|
|
||||||
if len(buf) - start_idx < 2:
|
if len(buf) - start_idx < 2:
|
||||||
return None
|
return None
|
||||||
payload_len = buf[start_idx + 1]
|
payload_len = buf[start_idx + 1]
|
||||||
@@ -142,7 +330,6 @@ def mavlink_frame_length(buf, start_idx):
|
|||||||
return total_len
|
return total_len
|
||||||
|
|
||||||
if magic == 0xFD:
|
if magic == 0xFD:
|
||||||
# v2 至少需要 magic + len + incompat_flags
|
|
||||||
if len(buf) - start_idx < 3:
|
if len(buf) - start_idx < 3:
|
||||||
return None
|
return None
|
||||||
payload_len = buf[start_idx + 1]
|
payload_len = buf[start_idx + 1]
|
||||||
@@ -156,6 +343,90 @@ def mavlink_frame_length(buf, start_idx):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_mavlink_sysid(buf, start_idx):
|
||||||
|
"""
|
||||||
|
從 MAVLink frame header 讀出 SYSID。
|
||||||
|
不做 CRC 驗證,但會搭配多次確認降低誤判。
|
||||||
|
"""
|
||||||
|
if start_idx >= len(buf):
|
||||||
|
return None
|
||||||
|
|
||||||
|
magic = buf[start_idx]
|
||||||
|
|
||||||
|
# MAVLink v1:
|
||||||
|
# 0xFE | len | seq | sysid | compid | msgid | payload | checksum
|
||||||
|
if magic == 0xFE:
|
||||||
|
if len(buf) - start_idx >= 6:
|
||||||
|
return buf[start_idx + 3]
|
||||||
|
|
||||||
|
# MAVLink v2:
|
||||||
|
# 0xFD | len | incompat | compat | seq | sysid | compid | msgid(3) | payload | checksum
|
||||||
|
if magic == 0xFD:
|
||||||
|
if len(buf) - start_idx >= 10:
|
||||||
|
return buf[start_idx + 5]
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def learn_my_sysid_from_tx_buf():
|
||||||
|
"""
|
||||||
|
從 FC -> ESP32 的 tx_buf 中找完整 MAVLink frame,
|
||||||
|
並自動學習飛控的 MAVLink SYSID。
|
||||||
|
|
||||||
|
為避免雜訊造成誤判,要求連續多次看到相同 SYSID 才正式設定 MY_SYSID。
|
||||||
|
"""
|
||||||
|
global MY_SYSID
|
||||||
|
global _sysid_candidate
|
||||||
|
global _sysid_candidate_count
|
||||||
|
global HELLO_SENT
|
||||||
|
global tx_buf
|
||||||
|
|
||||||
|
if len(tx_buf) == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
idx = 0
|
||||||
|
checked = 0
|
||||||
|
|
||||||
|
while idx < len(tx_buf) and checked < 8:
|
||||||
|
sub = tx_buf[idx:]
|
||||||
|
rel_start = find_first_mavlink_magic(sub)
|
||||||
|
if rel_start == -1:
|
||||||
|
return
|
||||||
|
|
||||||
|
start = idx + rel_start
|
||||||
|
frame_len = mavlink_frame_length(tx_buf, start)
|
||||||
|
if frame_len is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
sysid = get_mavlink_sysid(tx_buf, start)
|
||||||
|
|
||||||
|
if sysid is not None and sysid > 0:
|
||||||
|
if MY_SYSID is not None:
|
||||||
|
# 正常情況不任意改變已學到的 SYSID。
|
||||||
|
# 如果你真的會在飛行中改 FC SYSID,再另行設計重學條件。
|
||||||
|
return
|
||||||
|
|
||||||
|
if _sysid_candidate == sysid:
|
||||||
|
_sysid_candidate_count += 1
|
||||||
|
else:
|
||||||
|
_sysid_candidate = sysid
|
||||||
|
_sysid_candidate_count = 1
|
||||||
|
|
||||||
|
if _sysid_candidate_count >= SYSID_LEARN_CONFIRM_COUNT:
|
||||||
|
MY_SYSID = _sysid_candidate
|
||||||
|
|
||||||
|
# 新學到 SYSID 後,允許對已知 GCS 回一次 HELO
|
||||||
|
HELLO_SENT = False
|
||||||
|
return
|
||||||
|
|
||||||
|
idx = start + frame_len
|
||||||
|
checked += 1
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# tx_buf MAVLink frame 取出與裁切
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
def pop_mavlink_frames_by_quota(quota_bytes):
|
def pop_mavlink_frames_by_quota(quota_bytes):
|
||||||
"""
|
"""
|
||||||
從 tx_buf 取出不超過 quota_bytes 的完整 MAVLink frames。
|
從 tx_buf 取出不超過 quota_bytes 的完整 MAVLink frames。
|
||||||
@@ -173,6 +444,7 @@ def pop_mavlink_frames_by_quota(quota_bytes):
|
|||||||
if start == -1:
|
if start == -1:
|
||||||
tx_buf = bytearray()
|
tx_buf = bytearray()
|
||||||
return b''
|
return b''
|
||||||
|
|
||||||
if start > 0:
|
if start > 0:
|
||||||
tx_buf = tx_buf[start:]
|
tx_buf = tx_buf[start:]
|
||||||
|
|
||||||
@@ -212,7 +484,9 @@ def pop_mavlink_frames_by_quota(quota_bytes):
|
|||||||
|
|
||||||
|
|
||||||
def trim_tx_buffer_if_needed():
|
def trim_tx_buffer_if_needed():
|
||||||
"""tx_buf 過大時,丟掉較舊的完整 MAVLink frames,保留較新的資料。"""
|
"""
|
||||||
|
tx_buf 過大時,丟掉較舊的完整 MAVLink frames,保留較新的資料。
|
||||||
|
"""
|
||||||
global tx_buf
|
global tx_buf
|
||||||
|
|
||||||
if len(tx_buf) <= MAX_BUF_SIZE:
|
if len(tx_buf) <= MAX_BUF_SIZE:
|
||||||
@@ -225,6 +499,7 @@ def trim_tx_buffer_if_needed():
|
|||||||
if start == -1:
|
if start == -1:
|
||||||
tx_buf = bytearray()
|
tx_buf = bytearray()
|
||||||
return
|
return
|
||||||
|
|
||||||
if start > 0:
|
if start > 0:
|
||||||
tx_buf = tx_buf[start:]
|
tx_buf = tx_buf[start:]
|
||||||
continue
|
continue
|
||||||
@@ -249,6 +524,13 @@ def flush_tx_buffer(grant_bytes):
|
|||||||
"""
|
"""
|
||||||
global tx_buf
|
global tx_buf
|
||||||
|
|
||||||
|
# 尚未學到 SYSID 或地面站地址,不進行上行傳送。
|
||||||
|
if MY_SYSID is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if DEST_64 is None:
|
||||||
|
return
|
||||||
|
|
||||||
if grant_bytes <= 0:
|
if grant_bytes <= 0:
|
||||||
send_done_report(0, len(tx_buf))
|
send_done_report(0, len(tx_buf))
|
||||||
return
|
return
|
||||||
@@ -262,11 +544,16 @@ def flush_tx_buffer(grant_bytes):
|
|||||||
send_done_report(sent_len, len(tx_buf))
|
send_done_report(sent_len, len(tx_buf))
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# GCS control payload 解析
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
def parse_poll_payload(real_data):
|
def parse_poll_payload(real_data):
|
||||||
"""
|
"""
|
||||||
支援兩種 poll:
|
支援兩種 poll:
|
||||||
舊版:POLL + sysid,共 5 bytes
|
舊版:POLL + sysid,共 5 bytes
|
||||||
新版:POLL + sysid + quota,共 7 bytes
|
新版:POLL + sysid + quota,共 7 bytes
|
||||||
|
|
||||||
回傳 (target_sysid, grant_bytes),不是 poll 則回傳 (None, None)。
|
回傳 (target_sysid, grant_bytes),不是 poll 則回傳 (None, None)。
|
||||||
"""
|
"""
|
||||||
if not real_data.startswith(POLL_MAGIC):
|
if not real_data.startswith(POLL_MAGIC):
|
||||||
@@ -283,6 +570,27 @@ def parse_poll_payload(real_data):
|
|||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def is_discovery_payload(real_data):
|
||||||
|
"""
|
||||||
|
一般 discovery:
|
||||||
|
b'DISC'
|
||||||
|
"""
|
||||||
|
return real_data.startswith(DISC_MAGIC)
|
||||||
|
|
||||||
|
|
||||||
|
def is_force_discovery_payload(real_data):
|
||||||
|
"""
|
||||||
|
強制 discovery:
|
||||||
|
b'DSCF'
|
||||||
|
用於 GCS GUI Scan UAV 按鍵。
|
||||||
|
"""
|
||||||
|
return real_data.startswith(DISC_FORCE_MAGIC)
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# XBee API RX parser
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
def process_xbee_buffer():
|
def process_xbee_buffer():
|
||||||
global rx_buf
|
global rx_buf
|
||||||
|
|
||||||
@@ -313,45 +621,115 @@ def process_xbee_buffer():
|
|||||||
checksum_recv = rx_buf[3 + pkt_len]
|
checksum_recv = rx_buf[3 + pkt_len]
|
||||||
|
|
||||||
if get_checksum(packet) == checksum_recv:
|
if get_checksum(packet) == checksum_recv:
|
||||||
if packet[0] == 0x90:
|
if len(packet) > 0 and packet[0] == 0x90:
|
||||||
# XBee Receive Packet 0x90:RF data 從 packet[12:] 開始
|
# XBee Receive Packet 0x90:
|
||||||
|
# packet = 90 | src64(8) | src16(2) | options(1) | RF data
|
||||||
|
if len(packet) >= 12:
|
||||||
|
src64 = bytes(packet[1:9])
|
||||||
real_data = packet[12:]
|
real_data = packet[12:]
|
||||||
|
|
||||||
target_sysid, grant_bytes = parse_poll_payload(real_data)
|
# -------------------------------------------------
|
||||||
if target_sysid is not None:
|
# GCS 一般 DISC:
|
||||||
if target_sysid == MY_SYSID:
|
# 同一個 GCS 只回一次 HELO,避免一直 HELO。
|
||||||
flush_tx_buffer(grant_bytes)
|
# -------------------------------------------------
|
||||||
|
if is_discovery_payload(real_data):
|
||||||
|
learn_dest64_from_src64(src64)
|
||||||
|
learn_my_sysid_from_tx_buf()
|
||||||
|
|
||||||
|
if MY_SYSID is not None and DEST_64 is not None:
|
||||||
|
# 加一點由 SYSID / 時間產生的延遲,降低多機同時 HELO 撞包機率。
|
||||||
|
# 若 HELLO_SENT=True,send_hello_report() 會自己擋掉。
|
||||||
|
delay_ms = 20 + (((MY_SYSID * 37) + (time.ticks_ms() & 0xFF)) % 180)
|
||||||
|
time.sleep_ms(delay_ms)
|
||||||
|
send_hello_report(force=False)
|
||||||
|
|
||||||
|
# -------------------------------------------------
|
||||||
|
# GCS 強制 DSCF:
|
||||||
|
# 不管 HELLO_SENT,重新回 HELO。
|
||||||
|
# 用於 GCS GUI Scan UAV 按鍵。
|
||||||
|
# -------------------------------------------------
|
||||||
|
elif is_force_discovery_payload(real_data):
|
||||||
|
learn_dest64_from_src64(src64)
|
||||||
|
learn_my_sysid_from_tx_buf()
|
||||||
|
|
||||||
|
if MY_SYSID is not None and DEST_64 is not None:
|
||||||
|
delay_ms = 20 + (((MY_SYSID * 37) + (time.ticks_ms() & 0xFF)) % 180)
|
||||||
|
time.sleep_ms(delay_ms)
|
||||||
|
send_hello_report(force=True)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
# -------------------------------------------------
|
||||||
|
# 判斷是不是 POLL
|
||||||
|
# -------------------------------------------------
|
||||||
|
target_sysid, grant_bytes = parse_poll_payload(real_data)
|
||||||
|
|
||||||
|
if target_sysid is not None:
|
||||||
|
# 收到 GCS 的 POLL,也可以從 src64 學地面站地址。
|
||||||
|
learn_dest64_from_src64(src64)
|
||||||
|
|
||||||
|
# 嘗試從 FC buffer 學 MY_SYSID。
|
||||||
|
learn_my_sysid_from_tx_buf()
|
||||||
|
|
||||||
|
# 只有 target_sysid 等於自己飛控的 SYSID,才回傳。
|
||||||
|
if MY_SYSID is not None and target_sysid == MY_SYSID:
|
||||||
|
flush_tx_buffer(grant_bytes)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# -------------------------------------------------
|
||||||
# 一般 GCS -> FC MAVLink 下行資料
|
# 一般 GCS -> FC MAVLink 下行資料
|
||||||
|
# 只接受來自已知 GCS address 的資料,避免其他節點污染飛控。
|
||||||
|
# -------------------------------------------------
|
||||||
|
if DEST_64 is None:
|
||||||
|
learn_dest64_from_src64(src64)
|
||||||
|
|
||||||
|
if DEST_64 is not None and src64 == DEST_64:
|
||||||
uart_fc.write(real_data)
|
uart_fc.write(real_data)
|
||||||
|
|
||||||
rx_buf = rx_buf[total_len:]
|
rx_buf = rx_buf[total_len:]
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
# checksum 錯誤,丟掉第一個 byte,重新對齊
|
||||||
rx_buf = rx_buf[1:]
|
rx_buf = rx_buf[1:]
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
# 初始清理
|
# 初始清理
|
||||||
gc_ran = False
|
# =========================================================
|
||||||
|
|
||||||
try:
|
try:
|
||||||
gc.collect()
|
gc.collect()
|
||||||
gc_ran = True
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# 主迴圈
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
|
# -------------------------------------------------
|
||||||
# FC -> ESP32 tx buffer
|
# FC -> ESP32 tx buffer
|
||||||
|
# -------------------------------------------------
|
||||||
if uart_fc.any():
|
if uart_fc.any():
|
||||||
data = uart_fc.read(READ_FC_BYTES)
|
data = uart_fc.read(READ_FC_BYTES)
|
||||||
if data:
|
if data:
|
||||||
tx_buf.extend(data)
|
tx_buf.extend(data)
|
||||||
|
|
||||||
|
# 從飛控 MAVLink stream 自動學 MY_SYSID
|
||||||
|
learn_my_sysid_from_tx_buf()
|
||||||
|
|
||||||
|
# 避免 tx_buf 無限長大
|
||||||
trim_tx_buffer_if_needed()
|
trim_tx_buffer_if_needed()
|
||||||
|
|
||||||
|
# -------------------------------------------------
|
||||||
# XBee -> ESP32
|
# XBee -> ESP32
|
||||||
|
# -------------------------------------------------
|
||||||
if uart_xb.any():
|
if uart_xb.any():
|
||||||
chunk = uart_xb.read()
|
chunk = uart_xb.read()
|
||||||
if chunk:
|
if chunk:
|
||||||
rx_buf.extend(chunk)
|
rx_buf.extend(chunk)
|
||||||
|
|
||||||
process_xbee_buffer()
|
process_xbee_buffer()
|
||||||
|
|
||||||
time.sleep_ms(1)
|
time.sleep_ms(1)
|
||||||
@@ -360,7 +738,12 @@ while True:
|
|||||||
# OOM 時丟掉暫存資料,避免整顆 ESP32 死掉
|
# OOM 時丟掉暫存資料,避免整顆 ESP32 死掉
|
||||||
tx_buf = bytearray()
|
tx_buf = bytearray()
|
||||||
rx_buf = bytearray()
|
rx_buf = bytearray()
|
||||||
|
|
||||||
|
try:
|
||||||
gc.collect()
|
gc.collect()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
time.sleep_ms(10)
|
time.sleep_ms(10)
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
+394
-92
@@ -14,18 +14,27 @@ from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
|||||||
|
|
||||||
# =========================================================
|
# =========================================================
|
||||||
# GCS 端:XBee Serial <-> UDP Bridge
|
# GCS 端:XBee Serial <-> UDP Bridge
|
||||||
# Packet-size TDMA + DONE 標籤 + 精準 RSSI 歸屬 + 最近 5 次 RSSI 平均
|
# Packet-size TDMA + DONE + RSSI + Auto Discovery + Force Scan
|
||||||
|
#
|
||||||
|
# 對應 ESP32 端:
|
||||||
|
# - DISC:一般 discovery。UAV 對同一個 GCS 只回一次 HELO。
|
||||||
|
# - DSCF:force discovery。GUI 按 Scan UAV 時送出,UAV 強制回 HELO。
|
||||||
|
# - HELO:UAV -> GCS,回報 SYSID。
|
||||||
|
# - POLL:GCS -> UAV,授權某台 SYSID 傳送 quota_bytes。
|
||||||
|
# - DONE:UAV -> GCS,表示本輪資料已送完,GCS 可立即換下一台。
|
||||||
#
|
#
|
||||||
# 核心行為:
|
# 核心行為:
|
||||||
# 1. GCS 對某個 SYSID 發 POLL + quota_bytes。
|
# 1. 程式啟動前 AUTO_DISCOVERY_DURATION_SEC 秒自動 broadcast DISC。
|
||||||
# 2. UAV 收到自己的 POLL 後,送出最多 quota_bytes 的完整 MAVLink frames。
|
# 2. 收到 HELO 後,GCS 從 XBee 0x90 src64 學 UAV XBee address,
|
||||||
# 3. UAV 送完後回 DONE,GCS 收到 DONE 後立即 poll 下一台。
|
# 從 HELO payload 學 MAVLink SYSID,建立 SYSID <-> src64 對應。
|
||||||
# 4. 收到 DONE 後,GCS 立即發 ATDB 查詢本地 XBee 最近一包 RF packet 的 RSSI。
|
# 3. TDMA scheduler 依 ACTIVE_SYSIDS 輪詢。
|
||||||
# 5. 因為 DONE payload 帶有 SYSID,且 XBee 0x90 frame 帶有 src64,RSSI 不再用 MAVLink SYSID 猜。
|
# 4. send_poll() 若已知 SYSID 對應 src64,則 unicast;未知則 fallback broadcast。
|
||||||
# 6. RSSI 顯示採用「最近 5 次 DB RSSI」平均值,降低跳動。
|
# 5. 收到 DONE 後立即查 ATDB,將 RSSI 歸屬到該 SYSID。
|
||||||
|
# 6. GUI 的 Scan UAV 按鈕會送 DSCF burst,讓新加入或已存在的 UAV 強制回 HELO。
|
||||||
# =========================================================
|
# =========================================================
|
||||||
|
|
||||||
# === 多組設備設定 ===
|
|
||||||
|
# ================= 多組設備設定 =================
|
||||||
# Windows 測試時建議先只留你的實際 COM port,例如 COM15。
|
# Windows 測試時建議先只留你的實際 COM port,例如 COM15。
|
||||||
CONFIGS = [
|
CONFIGS = [
|
||||||
{"serial_port": "/dev/ttyUSB0", "udp_port": 14551},
|
{"serial_port": "/dev/ttyUSB0", "udp_port": 14551},
|
||||||
@@ -37,36 +46,43 @@ CONFIGS = [
|
|||||||
SERIAL_BAUDRATE = 115200
|
SERIAL_BAUDRATE = 115200
|
||||||
UDP_REMOTE_IP = '127.0.0.1'
|
UDP_REMOTE_IP = '127.0.0.1'
|
||||||
|
|
||||||
# Broadcast:由 RF payload 內的 SYSID 決定哪台 ESP32 回應。
|
# 若你要讓外部程式固定送 UDP 到本 bridge,可改成 True。
|
||||||
|
# False:本程式只負責 sendto 到 udp_port;本地接收 port 由系統隨機配置。
|
||||||
|
# True :本程式綁定 local UDP port = config['udp_port']。
|
||||||
|
UDP_LISTEN_FIXED_PORT = False
|
||||||
|
|
||||||
|
# XBee broadcast address
|
||||||
TARGET_ADDR64 = b'\x00\x00\x00\x00\x00\x00\xFF\xFF'
|
TARGET_ADDR64 = b'\x00\x00\x00\x00\x00\x00\xFF\xFF'
|
||||||
|
|
||||||
ACTIVE_SYSIDS = [3, 10, 15]
|
# 起始已知 SYSID。完全自動 discovery 可設空 list。
|
||||||
|
# 若你想保留舊版行為,可改成 [3, 10, 15]。
|
||||||
|
INITIAL_ACTIVE_SYSIDS = []
|
||||||
|
ACTIVE_SYSIDS = list(INITIAL_ACTIVE_SYSIDS)
|
||||||
|
|
||||||
# 如果你知道每台 UAV XBee 的 64-bit address,可以填在這裡。
|
# 若你知道每台 UAV XBee 的 64-bit address,可以預先填入。
|
||||||
# 即使不填,本程式也會從 DONE frame 自動學習 src64 -> SYSID。
|
# 即使不填,本程式也會從 HELO / DONE / MAVLink data 自動學習。
|
||||||
# 範例:
|
|
||||||
# XBEE_ADDR64_TO_SYSID = {
|
|
||||||
# b'\x00\x13\xA2\x00\x42\x5B\x9D\xC8': 15,
|
|
||||||
# b'\x00\x13\xA2\x00\x42\x5B\x9D\xAA': 10,
|
|
||||||
# b'\x00\x13\xA2\x00\x42\x5B\x9D\xBB': 3,
|
|
||||||
# }
|
|
||||||
XBEE_ADDR64_TO_SYSID = {}
|
XBEE_ADDR64_TO_SYSID = {}
|
||||||
AUTO_LEARN_XBEE_ADDR = True
|
AUTO_LEARN_XBEE_ADDR = True
|
||||||
|
|
||||||
# GCS -> UAV poll 格式:b'POLL' + sysid(1) + quota_bytes(2)
|
# 控制封包 magic
|
||||||
POLL_MAGIC = b'POLL'
|
DISC_MAGIC = b'DISC' # GCS -> UAV 一般 discovery
|
||||||
|
DISC_FORCE_MAGIC = b'DSCF' # GCS -> UAV 強制 discovery,GUI Scan UAV 用
|
||||||
|
HELLO_MAGIC = b'HELO' # UAV -> GCS: HELO + sysid(1)
|
||||||
|
POLL_MAGIC = b'POLL' # GCS -> UAV: POLL + sysid(1) + quota_bytes(2)
|
||||||
|
DONE_MAGIC = b'DONE' # UAV -> GCS: DONE + sysid(1) + sent_len(2) + remain_len(2)
|
||||||
|
|
||||||
# UAV -> GCS done 格式:b'DONE' + sysid(1) + sent_len(2) + remain_len(2)
|
# ================= Discovery 參數 =================
|
||||||
DONE_MAGIC = b'DONE'
|
AUTO_DISCOVERY_ENABLED = True
|
||||||
|
AUTO_DISCOVERY_DURATION_SEC = 30.0
|
||||||
|
DISCOVERY_INTERVAL_SEC = 1.5
|
||||||
|
|
||||||
# === TDMA 參數 ===
|
# GUI 按 Scan UAV 時送出 DSCF burst
|
||||||
# 每次 poll 授權該 UAV 最多送多少 MAVLink 原始資料 bytes。
|
FORCE_DISCOVERY_BURST_COUNT = 5
|
||||||
|
FORCE_DISCOVERY_BURST_GAP_SEC = 0.20
|
||||||
|
|
||||||
|
# ================= TDMA 參數 =================
|
||||||
current_grant_bytes = 600
|
current_grant_bytes = 600
|
||||||
|
|
||||||
# 切換下一台 UAV 前的保護時間,避免最後一包還在 XBee/UART buffer 裡。
|
|
||||||
current_guard_ms = 20
|
current_guard_ms = 20
|
||||||
|
|
||||||
# INITIALIZING 模式給較大的 quota,用於參數載入。
|
|
||||||
INIT_GRANT_BYTES = 1200
|
INIT_GRANT_BYTES = 1200
|
||||||
|
|
||||||
# 丟包率計算時間視窗
|
# 丟包率計算時間視窗
|
||||||
@@ -75,18 +91,17 @@ LOSS_TIME_WINDOW_SEC = 5.0
|
|||||||
# RSSI 最近 N 次平均
|
# RSSI 最近 N 次平均
|
||||||
RSSI_AVG_WINDOW = 5
|
RSSI_AVG_WINDOW = 5
|
||||||
|
|
||||||
# 是否也在資料封包時查 DB。預設 False:只在 DONE 後查 DB,比較不干擾資料流。
|
# 是否也在資料封包時查 DB。預設 False:只在 DONE / HELO 後查 DB,較不干擾資料流。
|
||||||
RSSI_QUERY_ON_DATA_FRAME = False
|
RSSI_QUERY_ON_DATA_FRAME = False
|
||||||
RSSI_DATA_QUERY_MIN_INTERVAL_SEC = 0.5
|
RSSI_DATA_QUERY_MIN_INTERVAL_SEC = 0.5
|
||||||
|
|
||||||
uav_states = {
|
|
||||||
sysid: {
|
|
||||||
"mode": "NORMAL",
|
|
||||||
} for sysid in ACTIVE_SYSIDS
|
|
||||||
}
|
|
||||||
|
|
||||||
# === 狀態追蹤 ===
|
# ================= 全域狀態 =================
|
||||||
rssi_history = defaultdict(lambda: deque(maxlen=5000)) # 畫圖用:RSSI avg5
|
uav_states = {}
|
||||||
|
for sid in ACTIVE_SYSIDS:
|
||||||
|
uav_states[sid] = {"mode": "NORMAL"}
|
||||||
|
|
||||||
|
rssi_history = defaultdict(lambda: deque(maxlen=5000)) # 畫圖用:RSSI avgN
|
||||||
rssi_raw_windows = defaultdict(lambda: deque(maxlen=RSSI_AVG_WINDOW))
|
rssi_raw_windows = defaultdict(lambda: deque(maxlen=RSSI_AVG_WINDOW))
|
||||||
rssi_time_history = defaultdict(lambda: deque(maxlen=5000))
|
rssi_time_history = defaultdict(lambda: deque(maxlen=5000))
|
||||||
rssi_latest_stats = defaultdict(lambda: {
|
rssi_latest_stats = defaultdict(lambda: {
|
||||||
@@ -110,7 +125,17 @@ learned_sysid_to_addr64 = {}
|
|||||||
tdma_done_events = {}
|
tdma_done_events = {}
|
||||||
tdma_last_reports = defaultdict(lambda: {'time': 0.0, 'sent_len': 0, 'remain_len': 0})
|
tdma_last_reports = defaultdict(lambda: {'time': 0.0, 'sent_len': 0, 'remain_len': 0})
|
||||||
|
|
||||||
# === 工具函式 ===
|
# HELO 回報紀錄
|
||||||
|
hello_last_reports = defaultdict(lambda: {'time': 0.0, 'src64': None})
|
||||||
|
|
||||||
|
# asyncio loop / serial protocols 給 GUI button 使用
|
||||||
|
ASYNC_LOOP = None
|
||||||
|
SERIAL_PROTOCOLS = []
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# 工具函式
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
def format_addr64(addr):
|
def format_addr64(addr):
|
||||||
if not addr:
|
if not addr:
|
||||||
@@ -118,6 +143,29 @@ def format_addr64(addr):
|
|||||||
return ''.join(f'{b:02X}' for b in addr)
|
return ''.join(f'{b:02X}' for b in addr)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_active_sysid(sysid):
|
||||||
|
"""若發現新的 SYSID,加入 ACTIVE_SYSIDS 並建立狀態。"""
|
||||||
|
if sysid is None:
|
||||||
|
return
|
||||||
|
if sysid <= 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
if sysid not in ACTIVE_SYSIDS:
|
||||||
|
ACTIVE_SYSIDS.append(sysid)
|
||||||
|
ACTIVE_SYSIDS.sort()
|
||||||
|
print(f"[DISCOVERY] New SYSID discovered: {sysid}")
|
||||||
|
|
||||||
|
if sysid not in uav_states:
|
||||||
|
uav_states[sysid] = {"mode": "NORMAL"}
|
||||||
|
|
||||||
|
if sysid not in tdma_done_events:
|
||||||
|
try:
|
||||||
|
tdma_done_events[sysid] = asyncio.Event()
|
||||||
|
except RuntimeError:
|
||||||
|
# 若從 GUI thread 呼叫時沒有 running loop,scheduler 之後會補建。
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def infer_sysid_from_addr64(src64):
|
def infer_sysid_from_addr64(src64):
|
||||||
if not src64:
|
if not src64:
|
||||||
return None
|
return None
|
||||||
@@ -129,14 +177,37 @@ def infer_sysid_from_addr64(src64):
|
|||||||
|
|
||||||
|
|
||||||
def learn_xbee_source(sysid, src64):
|
def learn_xbee_source(sysid, src64):
|
||||||
|
"""
|
||||||
|
建立 UAV XBee src64 <-> MAVLink SYSID 對應。
|
||||||
|
GCS 端收到 UAV 的 0x90 frame 時,src64 是 UAV 的 XBee address。
|
||||||
|
"""
|
||||||
if not AUTO_LEARN_XBEE_ADDR:
|
if not AUTO_LEARN_XBEE_ADDR:
|
||||||
return
|
return
|
||||||
if sysid is None or src64 is None:
|
if sysid is None or src64 is None:
|
||||||
return
|
return
|
||||||
if sysid <= 0:
|
if sysid <= 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
src64 = bytes(src64)
|
||||||
learned_addr64_to_sysid[src64] = sysid
|
learned_addr64_to_sysid[src64] = sysid
|
||||||
learned_sysid_to_addr64[sysid] = src64
|
learned_sysid_to_addr64[sysid] = src64
|
||||||
|
ensure_active_sysid(sysid)
|
||||||
|
|
||||||
|
|
||||||
|
def get_poll_dest_addr64(sysid):
|
||||||
|
"""
|
||||||
|
已知 SYSID -> src64 時,優先 unicast。
|
||||||
|
未知時 fallback broadcast。
|
||||||
|
"""
|
||||||
|
if sysid in learned_sysid_to_addr64:
|
||||||
|
return learned_sysid_to_addr64[sysid]
|
||||||
|
|
||||||
|
# 使用者預填的 XBEE_ADDR64_TO_SYSID 反查
|
||||||
|
for addr, sid in XBEE_ADDR64_TO_SYSID.items():
|
||||||
|
if sid == sysid:
|
||||||
|
return addr
|
||||||
|
|
||||||
|
return TARGET_ADDR64
|
||||||
|
|
||||||
|
|
||||||
def record_rssi(sysid, rssi_positive_db, src64=None):
|
def record_rssi(sysid, rssi_positive_db, src64=None):
|
||||||
@@ -147,6 +218,8 @@ def record_rssi(sysid, rssi_positive_db, src64=None):
|
|||||||
if sysid is None:
|
if sysid is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
ensure_active_sysid(sysid)
|
||||||
|
|
||||||
raw_dbm = -int(rssi_positive_db)
|
raw_dbm = -int(rssi_positive_db)
|
||||||
win = rssi_raw_windows[sysid]
|
win = rssi_raw_windows[sysid]
|
||||||
win.append(raw_dbm)
|
win.append(raw_dbm)
|
||||||
@@ -166,6 +239,9 @@ def record_rssi(sysid, rssi_positive_db, src64=None):
|
|||||||
|
|
||||||
def calculate_packet_loss(sysid, compid, current_seq):
|
def calculate_packet_loss(sysid, compid, current_seq):
|
||||||
global mavlink_sequence_tracker, packet_loss_stats
|
global mavlink_sequence_tracker, packet_loss_stats
|
||||||
|
|
||||||
|
ensure_active_sysid(sysid)
|
||||||
|
|
||||||
tracker = mavlink_sequence_tracker[sysid]
|
tracker = mavlink_sequence_tracker[sysid]
|
||||||
now = time.time()
|
now = time.time()
|
||||||
|
|
||||||
@@ -216,6 +292,9 @@ def calculate_packet_loss(sysid, compid, current_seq):
|
|||||||
|
|
||||||
|
|
||||||
def build_api_tx_frame(data: bytes, dest_addr64: bytes, frame_id=0x00) -> bytes:
|
def build_api_tx_frame(data: bytes, dest_addr64: bytes, frame_id=0x00) -> bytes:
|
||||||
|
"""
|
||||||
|
GCS XBee API TX frame type 0x10。
|
||||||
|
"""
|
||||||
frame = b'\x10' + struct.pack('>B', frame_id) + dest_addr64 + b'\xFF\xFE\x00\x00' + data
|
frame = b'\x10' + struct.pack('>B', frame_id) + dest_addr64 + b'\xFF\xFE\x00\x00' + data
|
||||||
return b'\x7E' + struct.pack('>H', len(frame)) + frame + struct.pack('B', 0xFF - (sum(frame) & 0xFF))
|
return b'\x7E' + struct.pack('>H', len(frame)) + frame + struct.pack('B', 0xFF - (sum(frame) & 0xFF))
|
||||||
|
|
||||||
@@ -236,6 +315,8 @@ def estimate_tdma_timeout(grant_bytes):
|
|||||||
|
|
||||||
async def grant_one_uav(serial_protocols, sysid, grant_bytes):
|
async def grant_one_uav(serial_protocols, sysid, grant_bytes):
|
||||||
"""授權一台 UAV;收到 DONE 立即結束,否則 timeout 後換下一台。"""
|
"""授權一台 UAV;收到 DONE 立即結束,否則 timeout 後換下一台。"""
|
||||||
|
ensure_active_sysid(sysid)
|
||||||
|
|
||||||
ev = tdma_done_events.get(sysid)
|
ev = tdma_done_events.get(sysid)
|
||||||
if ev is None:
|
if ev is None:
|
||||||
ev = asyncio.Event()
|
ev = asyncio.Event()
|
||||||
@@ -254,6 +335,10 @@ async def grant_one_uav(serial_protocols, sysid, grant_bytes):
|
|||||||
await asyncio.sleep(current_guard_ms / 1000.0)
|
await asyncio.sleep(current_guard_ms / 1000.0)
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# SerialToUDP:XBee Serial API parser
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
class SerialToUDP(asyncio.Protocol):
|
class SerialToUDP(asyncio.Protocol):
|
||||||
def __init__(self, udp_protocol, serial_port):
|
def __init__(self, udp_protocol, serial_port):
|
||||||
self.udp_protocol = udp_protocol
|
self.udp_protocol = udp_protocol
|
||||||
@@ -269,6 +354,24 @@ class SerialToUDP(asyncio.Protocol):
|
|||||||
self.pending_db = {} # frame_id -> {'sysid', 'src64', 'time', 'reason'}
|
self.pending_db = {} # frame_id -> {'sysid', 'src64', 'time', 'reason'}
|
||||||
self.last_data_db_query_time = 0.0
|
self.last_data_db_query_time = 0.0
|
||||||
|
|
||||||
|
def connection_made(self, transport):
|
||||||
|
self.transport = transport
|
||||||
|
if hasattr(self.udp_protocol, 'set_serial_transport'):
|
||||||
|
self.udp_protocol.set_serial_transport(self)
|
||||||
|
print(f"[{self.serial_port}] Serial connection established.")
|
||||||
|
|
||||||
|
# -------------------- GCS -> UAV control --------------------
|
||||||
|
|
||||||
|
def send_discovery(self, force=False):
|
||||||
|
"""
|
||||||
|
發 discovery:
|
||||||
|
- force=False:DISC,一般 discovery;UAV 對同一 GCS 只回一次 HELO。
|
||||||
|
- force=True :DSCF,強制 discovery;UAV 會重新回 HELO。
|
||||||
|
"""
|
||||||
|
payload = DISC_FORCE_MAGIC if force else DISC_MAGIC
|
||||||
|
api_frame = build_api_tx_frame(payload, TARGET_ADDR64, 0x00)
|
||||||
|
self.transport.write(api_frame)
|
||||||
|
|
||||||
def send_poll(self, target_sysid, grant_bytes=None):
|
def send_poll(self, target_sysid, grant_bytes=None):
|
||||||
if grant_bytes is None:
|
if grant_bytes is None:
|
||||||
grant_bytes = current_grant_bytes
|
grant_bytes = current_grant_bytes
|
||||||
@@ -278,14 +381,13 @@ class SerialToUDP(asyncio.Protocol):
|
|||||||
self.current_poll_time = time.time()
|
self.current_poll_time = time.time()
|
||||||
|
|
||||||
poll_payload = POLL_MAGIC + struct.pack('>BH', target_sysid, grant_bytes)
|
poll_payload = POLL_MAGIC + struct.pack('>BH', target_sysid, grant_bytes)
|
||||||
api_frame = build_api_tx_frame(poll_payload, TARGET_ADDR64, 0x00)
|
|
||||||
|
dest_addr64 = get_poll_dest_addr64(target_sysid)
|
||||||
|
api_frame = build_api_tx_frame(poll_payload, dest_addr64, 0x00)
|
||||||
|
|
||||||
self.transport.write(api_frame)
|
self.transport.write(api_frame)
|
||||||
|
|
||||||
def connection_made(self, transport):
|
# -------------------- Serial RX parser --------------------
|
||||||
self.transport = transport
|
|
||||||
if hasattr(self.udp_protocol, 'set_serial_transport'):
|
|
||||||
self.udp_protocol.set_serial_transport(self)
|
|
||||||
print(f"[{self.serial_port}] Serial connection established.")
|
|
||||||
|
|
||||||
def data_received(self, data):
|
def data_received(self, data):
|
||||||
self.buffer.extend(data)
|
self.buffer.extend(data)
|
||||||
@@ -306,8 +408,7 @@ class SerialToUDP(asyncio.Protocol):
|
|||||||
full_length = 3 + length + 1
|
full_length = 3 + length + 1
|
||||||
|
|
||||||
# 本程式使用 XBee RF payload 約 80~100 bytes,正常 length 不會很大。
|
# 本程式使用 XBee RF payload 約 80~100 bytes,正常 length 不會很大。
|
||||||
# 若你把 XBEE_MAX_PAYLOAD 調大,可同步放寬此限制。
|
if length > 700:
|
||||||
if length > 600:
|
|
||||||
self.buffer.pop(0)
|
self.buffer.pop(0)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -329,7 +430,7 @@ class SerialToUDP(asyncio.Protocol):
|
|||||||
|
|
||||||
if frame_type == 0x90:
|
if frame_type == 0x90:
|
||||||
# XBee Receive Packet:
|
# XBee Receive Packet:
|
||||||
# whole API frame: 7E | len(2) | 90 | src64(8) | src16(2) | options(1) | RF data | checksum
|
# 7E | len(2) | 90 | src64(8) | src16(2) | options(1) | RF data | checksum
|
||||||
src64 = frame[4:12]
|
src64 = frame[4:12]
|
||||||
rf_data = frame[15:-1]
|
rf_data = frame[15:-1]
|
||||||
self.handle_rx_packet(src64, rf_data)
|
self.handle_rx_packet(src64, rf_data)
|
||||||
@@ -342,7 +443,7 @@ class SerialToUDP(asyncio.Protocol):
|
|||||||
# 其他如 0x8B Transmit Status 可視需求處理;目前忽略。
|
# 其他如 0x8B Transmit Status 可視需求處理;目前忽略。
|
||||||
|
|
||||||
def handle_rx_packet(self, src64, rf_data):
|
def handle_rx_packet(self, src64, rf_data):
|
||||||
# 先用 src64 查。若尚未學習,DONE payload 會帶 sysid,可建立對應。
|
# 先用 src64 查。若尚未學習,HELO / DONE payload 會帶 sysid,可建立對應。
|
||||||
sysid_hint = infer_sysid_from_addr64(src64)
|
sysid_hint = infer_sysid_from_addr64(src64)
|
||||||
|
|
||||||
# 若在最近 poll 時槽內收到資料,也可作為備援歸屬。
|
# 若在最近 poll 時槽內收到資料,也可作為備援歸屬。
|
||||||
@@ -350,12 +451,13 @@ class SerialToUDP(asyncio.Protocol):
|
|||||||
if time.time() - self.current_poll_time <= 2.0:
|
if time.time() - self.current_poll_time <= 2.0:
|
||||||
sysid_hint = self.current_poll_sysid
|
sysid_hint = self.current_poll_sysid
|
||||||
|
|
||||||
done_sysid = self.udp_protocol.process_rf_data(rf_data, src64=src64, sysid_hint=sysid_hint)
|
result = self.udp_protocol.process_rf_data(rf_data, src64=src64, sysid_hint=sysid_hint)
|
||||||
|
|
||||||
if done_sysid is not None:
|
if result is not None:
|
||||||
# DONE payload 明確帶 SYSID;這裡可精準學習該 SYSID 對應的 XBee src64。
|
# result: ('DONE' or 'HELO', sysid)
|
||||||
learn_xbee_source(done_sysid, src64)
|
reason, sysid = result
|
||||||
self.send_at_command_db(done_sysid, src64=src64, reason='DONE')
|
learn_xbee_source(sysid, src64)
|
||||||
|
self.send_at_command_db(sysid, src64=src64, reason=reason)
|
||||||
return
|
return
|
||||||
|
|
||||||
if RSSI_QUERY_ON_DATA_FRAME and sysid_hint is not None:
|
if RSSI_QUERY_ON_DATA_FRAME and sysid_hint is not None:
|
||||||
@@ -364,6 +466,8 @@ class SerialToUDP(asyncio.Protocol):
|
|||||||
self.send_at_command_db(sysid_hint, src64=src64, reason='DATA')
|
self.send_at_command_db(sysid_hint, src64=src64, reason='DATA')
|
||||||
self.last_data_db_query_time = now
|
self.last_data_db_query_time = now
|
||||||
|
|
||||||
|
# -------------------- ATDB RSSI --------------------
|
||||||
|
|
||||||
def next_at_frame_id(self):
|
def next_at_frame_id(self):
|
||||||
self.at_frame_id += 1
|
self.at_frame_id += 1
|
||||||
if self.at_frame_id > 0xFE:
|
if self.at_frame_id > 0xFE:
|
||||||
@@ -373,7 +477,7 @@ class SerialToUDP(asyncio.Protocol):
|
|||||||
def send_at_command_db(self, sysid, src64=None, reason=''):
|
def send_at_command_db(self, sysid, src64=None, reason=''):
|
||||||
"""
|
"""
|
||||||
查本地 XBee 的 DB 值。DB 代表本地 XBee 最近收到 RF packet 的 RSSI。
|
查本地 XBee 的 DB 值。DB 代表本地 XBee 最近收到 RF packet 的 RSSI。
|
||||||
這裡在 DONE 後立即查,所以 RSSI 會歸屬到剛完成 TDMA slot 的 sysid。
|
這裡在 DONE / HELO 後立即查,所以 RSSI 會歸屬到剛剛回報的 sysid。
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
frame_id = self.next_at_frame_id()
|
frame_id = self.next_at_frame_id()
|
||||||
@@ -429,6 +533,8 @@ class SerialToUDP(asyncio.Protocol):
|
|||||||
if sysid is not None:
|
if sysid is not None:
|
||||||
record_rssi(sysid, rssi_value, src64=src64)
|
record_rssi(sysid, rssi_value, src64=src64)
|
||||||
|
|
||||||
|
# -------------------- UDP -> UAV downlink --------------------
|
||||||
|
|
||||||
def write_to_serial(self, data):
|
def write_to_serial(self, data):
|
||||||
self.gcs_tx_queue.extend(data)
|
self.gcs_tx_queue.extend(data)
|
||||||
|
|
||||||
@@ -436,6 +542,8 @@ class SerialToUDP(asyncio.Protocol):
|
|||||||
"""GCS -> UAV 下行資料,小批量送出,避免阻塞上行 TDMA。"""
|
"""GCS -> UAV 下行資料,小批量送出,避免阻塞上行 TDMA。"""
|
||||||
if not self.gcs_tx_queue:
|
if not self.gcs_tx_queue:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# 若還沒任何 UAV address,仍 broadcast。ESP32 端只接受已知 GCS 來源資料。
|
||||||
send_limit = min(len(self.gcs_tx_queue), 150)
|
send_limit = min(len(self.gcs_tx_queue), 150)
|
||||||
data_to_send = self.gcs_tx_queue[:send_limit]
|
data_to_send = self.gcs_tx_queue[:send_limit]
|
||||||
self.gcs_tx_queue = self.gcs_tx_queue[send_limit:]
|
self.gcs_tx_queue = self.gcs_tx_queue[send_limit:]
|
||||||
@@ -450,6 +558,8 @@ class SerialToUDP(asyncio.Protocol):
|
|||||||
chunk = data[sent_len:end_len]
|
chunk = data[sent_len:end_len]
|
||||||
sent_len = end_len
|
sent_len = end_len
|
||||||
|
|
||||||
|
# 下行命令目前 broadcast,所有 ESP32 收到後會寫給 FC。
|
||||||
|
# 若你要精準下行到某台 UAV,需在 MAVLink router 層或 UI 選定 sysid 後 unicast。
|
||||||
api_frame = build_api_tx_frame(chunk, TARGET_ADDR64, 0x00)
|
api_frame = build_api_tx_frame(chunk, TARGET_ADDR64, 0x00)
|
||||||
self.transport.write(api_frame)
|
self.transport.write(api_frame)
|
||||||
await asyncio.sleep(0.01)
|
await asyncio.sleep(0.01)
|
||||||
@@ -457,6 +567,10 @@ class SerialToUDP(asyncio.Protocol):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# UDPHandler:UDP / MAVLink / HELO / DONE
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
class UDPHandler(asyncio.DatagramProtocol):
|
class UDPHandler(asyncio.DatagramProtocol):
|
||||||
def __init__(self, udp_port):
|
def __init__(self, udp_port):
|
||||||
self.udp_port = udp_port
|
self.udp_port = udp_port
|
||||||
@@ -474,35 +588,74 @@ class UDPHandler(asyncio.DatagramProtocol):
|
|||||||
if self.serial_transport:
|
if self.serial_transport:
|
||||||
self.serial_transport.write_to_serial(data)
|
self.serial_transport.write_to_serial(data)
|
||||||
|
|
||||||
def handle_done_report(self, rf_data):
|
def handle_hello_report(self, rf_data, src64=None):
|
||||||
|
"""
|
||||||
|
HELO + sysid(1)
|
||||||
|
回傳 sysid 或 None。
|
||||||
|
"""
|
||||||
|
if len(rf_data) < 5 or not rf_data.startswith(HELLO_MAGIC):
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
sysid = rf_data[4]
|
||||||
|
|
||||||
|
hello_last_reports[sysid] = {
|
||||||
|
'time': time.time(),
|
||||||
|
'src64': src64,
|
||||||
|
}
|
||||||
|
|
||||||
|
learn_xbee_source(sysid, src64)
|
||||||
|
print(f"[HELO] SYSID {sysid}, src64={format_addr64(src64)}")
|
||||||
|
return sysid
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def handle_done_report(self, rf_data, src64=None):
|
||||||
|
"""
|
||||||
|
DONE + sysid(1) + sent_len(2) + remain_len(2)
|
||||||
|
回傳 sysid 或 None。
|
||||||
|
"""
|
||||||
if len(rf_data) < 9 or not rf_data.startswith(DONE_MAGIC):
|
if len(rf_data) < 9 or not rf_data.startswith(DONE_MAGIC):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
sysid, sent_len, remain_len = struct.unpack('>BHH', rf_data[4:9])
|
sysid, sent_len, remain_len = struct.unpack('>BHH', rf_data[4:9])
|
||||||
|
|
||||||
tdma_last_reports[sysid] = {
|
tdma_last_reports[sysid] = {
|
||||||
'time': time.time(),
|
'time': time.time(),
|
||||||
'sent_len': sent_len,
|
'sent_len': sent_len,
|
||||||
'remain_len': remain_len,
|
'remain_len': remain_len,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
learn_xbee_source(sysid, src64)
|
||||||
|
|
||||||
ev = tdma_done_events.get(sysid)
|
ev = tdma_done_events.get(sysid)
|
||||||
if ev is not None:
|
if ev is not None:
|
||||||
ev.set()
|
ev.set()
|
||||||
|
|
||||||
return sysid
|
return sysid
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def process_rf_data(self, rf_data, src64=None, sysid_hint=None):
|
def process_rf_data(self, rf_data, src64=None, sysid_hint=None):
|
||||||
"""
|
"""
|
||||||
處理 XBee 0x90 的 RF data。
|
處理 XBee 0x90 的 RF data。
|
||||||
回傳:若是 DONE,回傳 done_sysid;若是 MAVLink data,回傳 None。
|
回傳:
|
||||||
|
('HELO', sysid) 若是 HELO
|
||||||
|
('DONE', sysid) 若是 DONE
|
||||||
|
None 若是 MAVLink data
|
||||||
"""
|
"""
|
||||||
done_sysid = self.handle_done_report(rf_data)
|
hello_sysid = self.handle_hello_report(rf_data, src64=src64)
|
||||||
if done_sysid is not None:
|
if hello_sysid is not None:
|
||||||
return done_sysid
|
return ('HELO', hello_sysid)
|
||||||
|
|
||||||
|
done_sysid = self.handle_done_report(rf_data, src64=src64)
|
||||||
|
if done_sysid is not None:
|
||||||
|
return ('DONE', done_sysid)
|
||||||
|
|
||||||
|
# MAVLink data
|
||||||
try:
|
try:
|
||||||
for byte in rf_data:
|
for byte in rf_data:
|
||||||
msg = self.mav_decoder.parse_char(bytes([byte]))
|
msg = self.mav_decoder.parse_char(bytes([byte]))
|
||||||
@@ -513,11 +666,11 @@ class UDPHandler(asyncio.DatagramProtocol):
|
|||||||
if sysid == 0:
|
if sysid == 0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# MAVLink sysid 用於 packet loss 計算;XBee src64 用於 RSSI 歸屬。
|
|
||||||
if src64 is not None:
|
if src64 is not None:
|
||||||
learn_xbee_source(sysid, src64)
|
learn_xbee_source(sysid, src64)
|
||||||
|
|
||||||
calculate_packet_loss(sysid, compid, seq)
|
calculate_packet_loss(sysid, compid, seq)
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -527,8 +680,13 @@ class UDPHandler(asyncio.DatagramProtocol):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# Bridge setup / scheduler / discovery
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
async def setup_bridge(config):
|
async def setup_bridge(config):
|
||||||
port, udp = config['serial_port'], config['udp_port']
|
port, udp = config['serial_port'], config['udp_port']
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ser = serial.Serial(port, SERIAL_BAUDRATE)
|
ser = serial.Serial(port, SERIAL_BAUDRATE)
|
||||||
ser.close()
|
ser.close()
|
||||||
@@ -538,17 +696,55 @@ async def setup_bridge(config):
|
|||||||
|
|
||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
udp_handler = UDPHandler(udp)
|
udp_handler = UDPHandler(udp)
|
||||||
await loop.create_datagram_endpoint(lambda: udp_handler, local_addr=('0.0.0.0', 0))
|
|
||||||
|
local_port = udp if UDP_LISTEN_FIXED_PORT else 0
|
||||||
|
await loop.create_datagram_endpoint(lambda: udp_handler, local_addr=('0.0.0.0', local_port))
|
||||||
|
|
||||||
serial_proto = SerialToUDP(udp_handler, port)
|
serial_proto = SerialToUDP(udp_handler, port)
|
||||||
await serial_asyncio.create_serial_connection(loop, lambda: serial_proto, port, baudrate=SERIAL_BAUDRATE)
|
await serial_asyncio.create_serial_connection(loop, lambda: serial_proto, port, baudrate=SERIAL_BAUDRATE)
|
||||||
return serial_proto
|
return serial_proto
|
||||||
|
|
||||||
|
|
||||||
|
async def auto_discovery_task(serial_protocols):
|
||||||
|
"""
|
||||||
|
程式啟動前 AUTO_DISCOVERY_DURATION_SEC 秒自動送 DISC。
|
||||||
|
時間到後停止自動 DISC,避免 UAV 一直回 HELO。
|
||||||
|
後續新增 UAV 請用 GUI 的 Scan UAV 按鈕送 DSCF。
|
||||||
|
"""
|
||||||
|
if not AUTO_DISCOVERY_ENABLED:
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"[DISCOVERY] Auto DISC started for {AUTO_DISCOVERY_DURATION_SEC:.1f} seconds")
|
||||||
|
|
||||||
|
start_t = time.time()
|
||||||
|
while time.time() - start_t < AUTO_DISCOVERY_DURATION_SEC:
|
||||||
|
for sp in serial_protocols:
|
||||||
|
if hasattr(sp, 'send_discovery'):
|
||||||
|
sp.send_discovery(force=False)
|
||||||
|
await asyncio.sleep(DISCOVERY_INTERVAL_SEC)
|
||||||
|
|
||||||
|
print("[DISCOVERY] Auto DISC stopped. Use GUI Scan UAV for new aircraft.")
|
||||||
|
|
||||||
|
|
||||||
|
async def force_discovery_burst(serial_protocols):
|
||||||
|
"""
|
||||||
|
GUI Scan UAV:送 DSCF burst。
|
||||||
|
UAV 收到 DSCF 後會強制回 HELO。
|
||||||
|
"""
|
||||||
|
print("[DISCOVERY] Force scan started")
|
||||||
|
for _ in range(FORCE_DISCOVERY_BURST_COUNT):
|
||||||
|
for sp in serial_protocols:
|
||||||
|
if hasattr(sp, 'send_discovery'):
|
||||||
|
sp.send_discovery(force=True)
|
||||||
|
await asyncio.sleep(FORCE_DISCOVERY_BURST_GAP_SEC)
|
||||||
|
print("[DISCOVERY] Force scan finished")
|
||||||
|
|
||||||
|
|
||||||
async def tdma_scheduler(serial_protocols):
|
async def tdma_scheduler(serial_protocols):
|
||||||
print(f"Packet-size TDMA Scheduler Started... 找到 {len(serial_protocols)} 個可用端口")
|
print(f"Packet-size TDMA Scheduler Started... 找到 {len(serial_protocols)} 個可用端口")
|
||||||
|
|
||||||
for sysid in ACTIVE_SYSIDS:
|
for sysid in list(ACTIVE_SYSIDS):
|
||||||
|
ensure_active_sysid(sysid)
|
||||||
tdma_done_events[sysid] = asyncio.Event()
|
tdma_done_events[sysid] = asyncio.Event()
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
@@ -556,13 +752,21 @@ async def tdma_scheduler(serial_protocols):
|
|||||||
for sp in serial_protocols:
|
for sp in serial_protocols:
|
||||||
if hasattr(sp, 'flush_gcs_queue'):
|
if hasattr(sp, 'flush_gcs_queue'):
|
||||||
sp.flush_gcs_queue()
|
sp.flush_gcs_queue()
|
||||||
|
|
||||||
await asyncio.sleep(current_guard_ms / 1000.0)
|
await asyncio.sleep(current_guard_ms / 1000.0)
|
||||||
|
|
||||||
# UAV 上行:以 byte quota 授權,不以固定 time slot 等待
|
# 若尚未發現任何 UAV,避免空轉太快
|
||||||
for sysid in ACTIVE_SYSIDS:
|
if not ACTIVE_SYSIDS:
|
||||||
state = uav_states[sysid]
|
await asyncio.sleep(0.2)
|
||||||
|
continue
|
||||||
|
|
||||||
if state['mode'] == 'INITIALIZING':
|
# UAV 上行:以 byte quota 授權,不以固定 time slot 等待
|
||||||
|
for sysid in list(ACTIVE_SYSIDS):
|
||||||
|
ensure_active_sysid(sysid)
|
||||||
|
|
||||||
|
state = uav_states.get(sysid, {"mode": "NORMAL"})
|
||||||
|
|
||||||
|
if state.get('mode') == 'INITIALIZING':
|
||||||
for _ in range(4):
|
for _ in range(4):
|
||||||
await grant_one_uav(serial_protocols, sysid, INIT_GRANT_BYTES)
|
await grant_one_uav(serial_protocols, sysid, INIT_GRANT_BYTES)
|
||||||
else:
|
else:
|
||||||
@@ -570,28 +774,57 @@ async def tdma_scheduler(serial_protocols):
|
|||||||
|
|
||||||
|
|
||||||
async def async_main():
|
async def async_main():
|
||||||
|
global ASYNC_LOOP
|
||||||
|
global SERIAL_PROTOCOLS
|
||||||
|
|
||||||
|
ASYNC_LOOP = asyncio.get_running_loop()
|
||||||
|
|
||||||
protocols = await asyncio.gather(*(setup_bridge(cfg) for cfg in CONFIGS))
|
protocols = await asyncio.gather(*(setup_bridge(cfg) for cfg in CONFIGS))
|
||||||
valid_protocols = [p for p in protocols if p is not None]
|
valid_protocols = [p for p in protocols if p is not None]
|
||||||
|
SERIAL_PROTOCOLS = valid_protocols
|
||||||
|
|
||||||
if valid_protocols:
|
if valid_protocols:
|
||||||
|
asyncio.create_task(auto_discovery_task(valid_protocols))
|
||||||
asyncio.create_task(tdma_scheduler(valid_protocols))
|
asyncio.create_task(tdma_scheduler(valid_protocols))
|
||||||
else:
|
else:
|
||||||
print('No valid serial ports found.')
|
print('No valid serial ports found.')
|
||||||
|
|
||||||
await asyncio.Future()
|
await asyncio.Future()
|
||||||
|
|
||||||
|
|
||||||
# === GUI 介面區 ===
|
# =========================================================
|
||||||
|
# GUI
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
def start_gui():
|
def start_gui():
|
||||||
root = tk.Tk()
|
root = tk.Tk()
|
||||||
root.title('UAV Packet-size TDMA Control Station - RSSI avg5')
|
root.title('UAV Packet-size TDMA Control Station - Auto Discovery / Force Scan')
|
||||||
root.geometry('1300x880')
|
root.geometry('1350x900')
|
||||||
|
|
||||||
# --- 左側控制面板 ---
|
# --- 左側控制面板 ---
|
||||||
control_frame = tk.Frame(root, width=350, bg='#f0f0f0', padx=20, pady=20)
|
control_frame = tk.Frame(root, width=390, bg='#f0f0f0', padx=20, pady=20)
|
||||||
control_frame.pack(side=tk.LEFT, fill=tk.Y)
|
control_frame.pack(side=tk.LEFT, fill=tk.Y)
|
||||||
|
|
||||||
tk.Label(control_frame, text='Packet-size TDMA 控制', font=('Arial', 14, 'bold'), bg='#f0f0f0').pack(pady=10)
|
tk.Label(control_frame, text='Packet-size TDMA 控制', font=('Arial', 14, 'bold'), bg='#f0f0f0').pack(pady=10)
|
||||||
|
|
||||||
|
# --- Discovery buttons ---
|
||||||
|
def on_scan_uav():
|
||||||
|
if ASYNC_LOOP is not None and SERIAL_PROTOCOLS:
|
||||||
|
asyncio.run_coroutine_threadsafe(force_discovery_burst(SERIAL_PROTOCOLS), ASYNC_LOOP)
|
||||||
|
scan_label.config(text='已送出 DSCF 強制掃描')
|
||||||
|
else:
|
||||||
|
scan_label.config(text='尚未建立 serial / asyncio loop')
|
||||||
|
|
||||||
|
scan_btn = tk.Button(control_frame, text='Scan UAV / 發送 DSCF',
|
||||||
|
font=('Arial', 12, 'bold'), bg='#2196F3', fg='white',
|
||||||
|
command=on_scan_uav)
|
||||||
|
scan_btn.pack(pady=(5, 5), fill=tk.X)
|
||||||
|
|
||||||
|
scan_label = tk.Label(control_frame, text=f'開機前 {AUTO_DISCOVERY_DURATION_SEC:.0f}s 自動 DISC,之後可手動掃描',
|
||||||
|
bg='#f0f0f0', font=('Arial', 9), wraplength=330, justify=tk.LEFT)
|
||||||
|
scan_label.pack(pady=(0, 15), anchor=tk.W)
|
||||||
|
|
||||||
|
# --- grant bytes slider ---
|
||||||
grant_var = tk.IntVar(value=current_grant_bytes)
|
grant_var = tk.IntVar(value=current_grant_bytes)
|
||||||
|
|
||||||
def on_grant_change(val):
|
def on_grant_change(val):
|
||||||
@@ -599,11 +832,13 @@ def start_gui():
|
|||||||
current_grant_bytes = int(float(val))
|
current_grant_bytes = int(float(val))
|
||||||
grant_label.config(text=f'每台授權: {current_grant_bytes} bytes')
|
grant_label.config(text=f'每台授權: {current_grant_bytes} bytes')
|
||||||
|
|
||||||
grant_slider = ttk.Scale(control_frame, from_=100, to_=1800, orient='horizontal', variable=grant_var, command=on_grant_change)
|
grant_slider = ttk.Scale(control_frame, from_=100, to_=1800, orient='horizontal',
|
||||||
|
variable=grant_var, command=on_grant_change)
|
||||||
grant_slider.pack(fill=tk.X, pady=10)
|
grant_slider.pack(fill=tk.X, pady=10)
|
||||||
grant_label = tk.Label(control_frame, text=f'每台授權: {current_grant_bytes} bytes', bg='#f0f0f0')
|
grant_label = tk.Label(control_frame, text=f'每台授權: {current_grant_bytes} bytes', bg='#f0f0f0')
|
||||||
grant_label.pack()
|
grant_label.pack()
|
||||||
|
|
||||||
|
# --- guard time slider ---
|
||||||
guard_var = tk.IntVar(value=current_guard_ms)
|
guard_var = tk.IntVar(value=current_guard_ms)
|
||||||
|
|
||||||
def on_guard_change(val):
|
def on_guard_change(val):
|
||||||
@@ -612,34 +847,46 @@ def start_gui():
|
|||||||
guard_label.config(text=f'切換保護時間: {current_guard_ms} ms')
|
guard_label.config(text=f'切換保護時間: {current_guard_ms} ms')
|
||||||
|
|
||||||
tk.Label(control_frame, text='Guard Time', font=('Arial', 11, 'bold'), bg='#f0f0f0').pack(pady=(20, 0))
|
tk.Label(control_frame, text='Guard Time', font=('Arial', 11, 'bold'), bg='#f0f0f0').pack(pady=(20, 0))
|
||||||
guard_slider = ttk.Scale(control_frame, from_=0, to_=100, orient='horizontal', variable=guard_var, command=on_guard_change)
|
guard_slider = ttk.Scale(control_frame, from_=0, to_=100, orient='horizontal',
|
||||||
|
variable=guard_var, command=on_guard_change)
|
||||||
guard_slider.pack(fill=tk.X, pady=10)
|
guard_slider.pack(fill=tk.X, pady=10)
|
||||||
guard_label = tk.Label(control_frame, text=f'切換保護時間: {current_guard_ms} ms', bg='#f0f0f0')
|
guard_label = tk.Label(control_frame, text=f'切換保護時間: {current_guard_ms} ms', bg='#f0f0f0')
|
||||||
guard_label.pack()
|
guard_label.pack()
|
||||||
|
|
||||||
|
# --- UAV INIT mode ---
|
||||||
tk.Label(control_frame, text='群機初始化控制 (INIT)', font=('Arial', 14, 'bold'), bg='#f0f0f0').pack(pady=(30, 10))
|
tk.Label(control_frame, text='群機初始化控制 (INIT)', font=('Arial', 14, 'bold'), bg='#f0f0f0').pack(pady=(30, 10))
|
||||||
|
|
||||||
init_var = tk.IntVar(value=0)
|
init_var = tk.IntVar(value=0)
|
||||||
radios = {}
|
radios = {}
|
||||||
status_labels = {}
|
status_labels = {}
|
||||||
|
|
||||||
|
radio_container = tk.Frame(control_frame, bg='#f0f0f0')
|
||||||
|
radio_container.pack(fill=tk.X)
|
||||||
|
|
||||||
def on_radio_change():
|
def on_radio_change():
|
||||||
selected = init_var.get()
|
selected = init_var.get()
|
||||||
for sysid in ACTIVE_SYSIDS:
|
for sysid in list(ACTIVE_SYSIDS):
|
||||||
|
ensure_active_sysid(sysid)
|
||||||
if sysid == selected:
|
if sysid == selected:
|
||||||
uav_states[sysid]['mode'] = 'INITIALIZING'
|
uav_states[sysid]['mode'] = 'INITIALIZING'
|
||||||
else:
|
else:
|
||||||
uav_states[sysid]['mode'] = 'NORMAL'
|
uav_states[sysid]['mode'] = 'NORMAL'
|
||||||
|
|
||||||
tk.Radiobutton(control_frame, text='全體 NORMAL', variable=init_var, value=0,
|
tk.Radiobutton(radio_container, text='全體 NORMAL', variable=init_var, value=0,
|
||||||
command=on_radio_change, bg='#f0f0f0', font=('Arial', 11, 'bold'), fg='blue').pack(anchor=tk.W, pady=(0, 10))
|
command=on_radio_change, bg='#f0f0f0',
|
||||||
|
font=('Arial', 11, 'bold'), fg='blue').pack(anchor=tk.W, pady=(0, 10))
|
||||||
|
|
||||||
for sysid in ACTIVE_SYSIDS:
|
def ensure_radio_row(sysid):
|
||||||
frame = tk.Frame(control_frame, bg='#f0f0f0')
|
if sysid in radios:
|
||||||
frame.pack(fill=tk.X, pady=5)
|
return
|
||||||
|
|
||||||
rb = tk.Radiobutton(frame, text=f'SYSID {sysid} 專屬載入', variable=init_var, value=sysid,
|
frame = tk.Frame(radio_container, bg='#f0f0f0')
|
||||||
command=on_radio_change, bg='#f0f0f0', font=('Arial', 11))
|
frame.pack(fill=tk.X, pady=3)
|
||||||
|
|
||||||
|
rb = tk.Radiobutton(frame, text=f'SYSID {sysid} 專屬載入',
|
||||||
|
variable=init_var, value=sysid,
|
||||||
|
command=on_radio_change,
|
||||||
|
bg='#f0f0f0', font=('Arial', 11))
|
||||||
rb.pack(side=tk.LEFT)
|
rb.pack(side=tk.LEFT)
|
||||||
radios[sysid] = rb
|
radios[sysid] = rb
|
||||||
|
|
||||||
@@ -647,6 +894,9 @@ def start_gui():
|
|||||||
lbl.pack(side=tk.RIGHT, padx=5)
|
lbl.pack(side=tk.RIGHT, padx=5)
|
||||||
status_labels[sysid] = lbl
|
status_labels[sysid] = lbl
|
||||||
|
|
||||||
|
for sid in ACTIVE_SYSIDS:
|
||||||
|
ensure_radio_row(sid)
|
||||||
|
|
||||||
is_locked = False
|
is_locked = False
|
||||||
|
|
||||||
def toggle_tdma_mode():
|
def toggle_tdma_mode():
|
||||||
@@ -669,38 +919,81 @@ def start_gui():
|
|||||||
lock_btn = tk.Button(control_frame, text='參數載入完畢,鎖定進入 TDMA',
|
lock_btn = tk.Button(control_frame, text='參數載入完畢,鎖定進入 TDMA',
|
||||||
font=('Arial', 12, 'bold'), bg='#4CAF50', fg='white',
|
font=('Arial', 12, 'bold'), bg='#4CAF50', fg='white',
|
||||||
command=toggle_tdma_mode)
|
command=toggle_tdma_mode)
|
||||||
lock_btn.pack(pady=25, fill=tk.X)
|
lock_btn.pack(pady=20, fill=tk.X)
|
||||||
|
|
||||||
report_title = tk.Label(control_frame, text='最近 DONE 回報', font=('Arial', 12, 'bold'), bg='#f0f0f0')
|
# --- Reports ---
|
||||||
|
report_title = tk.Label(control_frame, text='最近 HELO / DONE 回報', font=('Arial', 12, 'bold'), bg='#f0f0f0')
|
||||||
report_title.pack(pady=(10, 5))
|
report_title.pack(pady=(10, 5))
|
||||||
|
|
||||||
|
report_container = tk.Frame(control_frame, bg='#f0f0f0')
|
||||||
|
report_container.pack(fill=tk.X)
|
||||||
|
|
||||||
report_labels = {}
|
report_labels = {}
|
||||||
for sysid in ACTIVE_SYSIDS:
|
hello_labels = {}
|
||||||
lbl = tk.Label(control_frame, text=f'SYSID {sysid}: sent=0, remain=0', font=('Arial', 10), bg='#f0f0f0')
|
|
||||||
lbl.pack(anchor=tk.W)
|
def ensure_report_row(sysid):
|
||||||
report_labels[sysid] = lbl
|
if sysid not in hello_labels:
|
||||||
|
hl = tk.Label(report_container, text=f'SYSID {sysid}: HELO no report',
|
||||||
|
font=('Arial', 9), bg='#f0f0f0', justify=tk.LEFT)
|
||||||
|
hl.pack(anchor=tk.W)
|
||||||
|
hello_labels[sysid] = hl
|
||||||
|
|
||||||
|
if sysid not in report_labels:
|
||||||
|
dl = tk.Label(report_container, text=f'SYSID {sysid}: DONE sent=0, remain=0',
|
||||||
|
font=('Arial', 9), bg='#f0f0f0', justify=tk.LEFT)
|
||||||
|
dl.pack(anchor=tk.W)
|
||||||
|
report_labels[sysid] = dl
|
||||||
|
|
||||||
|
for sid in ACTIVE_SYSIDS:
|
||||||
|
ensure_report_row(sid)
|
||||||
|
|
||||||
rssi_title = tk.Label(control_frame, text=f'RSSI 最近 {RSSI_AVG_WINDOW} 次平均', font=('Arial', 12, 'bold'), bg='#f0f0f0')
|
rssi_title = tk.Label(control_frame, text=f'RSSI 最近 {RSSI_AVG_WINDOW} 次平均', font=('Arial', 12, 'bold'), bg='#f0f0f0')
|
||||||
rssi_title.pack(pady=(18, 5))
|
rssi_title.pack(pady=(18, 5))
|
||||||
|
|
||||||
|
rssi_container = tk.Frame(control_frame, bg='#f0f0f0')
|
||||||
|
rssi_container.pack(fill=tk.X)
|
||||||
|
|
||||||
rssi_labels = {}
|
rssi_labels = {}
|
||||||
for sysid in ACTIVE_SYSIDS:
|
|
||||||
lbl = tk.Label(control_frame, text=f'SYSID {sysid}: RSSI avg -- dBm', font=('Arial', 10), bg='#f0f0f0', justify=tk.LEFT)
|
def ensure_rssi_row(sysid):
|
||||||
|
if sysid in rssi_labels:
|
||||||
|
return
|
||||||
|
lbl = tk.Label(rssi_container, text=f'SYSID {sysid}: RSSI avg -- dBm',
|
||||||
|
font=('Arial', 9), bg='#f0f0f0', justify=tk.LEFT)
|
||||||
lbl.pack(anchor=tk.W)
|
lbl.pack(anchor=tk.W)
|
||||||
rssi_labels[sysid] = lbl
|
rssi_labels[sysid] = lbl
|
||||||
|
|
||||||
|
for sid in ACTIVE_SYSIDS:
|
||||||
|
ensure_rssi_row(sid)
|
||||||
|
|
||||||
|
# --- status updater ---
|
||||||
def update_status_gui():
|
def update_status_gui():
|
||||||
now = time.time()
|
now = time.time()
|
||||||
|
|
||||||
|
for sysid in list(ACTIVE_SYSIDS):
|
||||||
|
ensure_radio_row(sysid)
|
||||||
|
ensure_report_row(sysid)
|
||||||
|
ensure_rssi_row(sysid)
|
||||||
|
|
||||||
for sysid, lbl in status_labels.items():
|
for sysid, lbl in status_labels.items():
|
||||||
mode = uav_states[sysid]['mode']
|
mode = uav_states.get(sysid, {"mode": "NORMAL"}).get("mode", "NORMAL")
|
||||||
if mode == 'INITIALIZING':
|
if mode == 'INITIALIZING':
|
||||||
lbl.config(text='INIT', fg='orange')
|
lbl.config(text='INIT', fg='orange')
|
||||||
else:
|
else:
|
||||||
lbl.config(text='NORMAL', fg='green')
|
lbl.config(text='NORMAL', fg='green')
|
||||||
|
|
||||||
|
for sysid, lbl in hello_labels.items():
|
||||||
|
rep = hello_last_reports[sysid]
|
||||||
|
age = now - rep['time'] if rep['time'] else 999.0
|
||||||
|
age_text = f'{age:.1f}s ago' if age < 99 else 'no report'
|
||||||
|
addr_text = format_addr64(rep.get('src64'))
|
||||||
|
lbl.config(text=f"SYSID {sysid}: HELO ({age_text}) src64={addr_text}")
|
||||||
|
|
||||||
for sysid, lbl in report_labels.items():
|
for sysid, lbl in report_labels.items():
|
||||||
rep = tdma_last_reports[sysid]
|
rep = tdma_last_reports[sysid]
|
||||||
age = now - rep['time'] if rep['time'] else 999.0
|
age = now - rep['time'] if rep['time'] else 999.0
|
||||||
age_text = f'{age:.1f}s ago' if age < 99 else 'no report'
|
age_text = f'{age:.1f}s ago' if age < 99 else 'no report'
|
||||||
lbl.config(text=f"SYSID {sysid}: sent={rep['sent_len']}, remain={rep['remain_len']} ({age_text})")
|
lbl.config(text=f"SYSID {sysid}: DONE sent={rep['sent_len']}, remain={rep['remain_len']} ({age_text})")
|
||||||
|
|
||||||
for sysid, lbl in rssi_labels.items():
|
for sysid, lbl in rssi_labels.items():
|
||||||
st = rssi_latest_stats[sysid]
|
st = rssi_latest_stats[sysid]
|
||||||
@@ -722,14 +1015,15 @@ def start_gui():
|
|||||||
plot_frame = tk.Frame(root)
|
plot_frame = tk.Frame(root)
|
||||||
plot_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
|
plot_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(9.5, 8.3), dpi=100)
|
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(9.5, 8.5), dpi=100)
|
||||||
canvas = FigureCanvasTkAgg(fig, master=plot_frame)
|
canvas = FigureCanvasTkAgg(fig, master=plot_frame)
|
||||||
canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
|
canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
def update_plot(frame):
|
def update_plot(frame):
|
||||||
ax1.clear()
|
ax1.clear()
|
||||||
ax2.clear()
|
ax2.clear()
|
||||||
ax1.set_title(f'RSSI avg{RSSI_AVG_WINDOW} from XBee ATDB after DONE', fontsize=12)
|
|
||||||
|
ax1.set_title(f'RSSI avg{RSSI_AVG_WINDOW} from XBee ATDB after HELO / DONE', fontsize=12)
|
||||||
ax1.set_xlim(10, 0)
|
ax1.set_xlim(10, 0)
|
||||||
ax1.set_ylim(-100, -10)
|
ax1.set_ylim(-100, -10)
|
||||||
ax1.grid(True, alpha=0.3)
|
ax1.grid(True, alpha=0.3)
|
||||||
@@ -740,7 +1034,7 @@ def start_gui():
|
|||||||
ax2.grid(True, alpha=0.3)
|
ax2.grid(True, alpha=0.3)
|
||||||
|
|
||||||
now = time.time()
|
now = time.time()
|
||||||
colors = ['blue', 'red', 'green', 'orange', 'purple', 'brown']
|
colors = ['blue', 'red', 'green', 'orange', 'purple', 'brown', 'cyan', 'magenta']
|
||||||
|
|
||||||
try:
|
try:
|
||||||
sysids = sorted(list(set(list(rssi_history.keys()) + list(packet_loss_history.keys()) + ACTIVE_SYSIDS)))
|
sysids = sorted(list(set(list(rssi_history.keys()) + list(packet_loss_history.keys()) + ACTIVE_SYSIDS)))
|
||||||
@@ -806,9 +1100,13 @@ def start_gui():
|
|||||||
if abs(real_y - text_y) > 1.0:
|
if abs(real_y - text_y) > 1.0:
|
||||||
ax2.plot([lbl['x_real'], 0.5], [real_y, text_y], color=color, linestyle=':', alpha=0.6)
|
ax2.plot([lbl['x_real'], 0.5], [real_y, text_y], color=color, linestyle=':', alpha=0.6)
|
||||||
|
|
||||||
|
if ax1.lines:
|
||||||
ax1.legend(loc='upper left')
|
ax1.legend(loc='upper left')
|
||||||
|
if ax2.lines:
|
||||||
ax2.legend(loc='upper left')
|
ax2.legend(loc='upper left')
|
||||||
|
|
||||||
|
canvas.draw_idle()
|
||||||
|
|
||||||
ani = animation.FuncAnimation(fig, update_plot, interval=1000)
|
ani = animation.FuncAnimation(fig, update_plot, interval=1000)
|
||||||
|
|
||||||
def on_closing():
|
def on_closing():
|
||||||
@@ -819,6 +1117,10 @@ def start_gui():
|
|||||||
root.mainloop()
|
root.mainloop()
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# Entry point
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
threading.Thread(target=lambda: asyncio.run(async_main()), daemon=True).start()
|
threading.Thread(target=lambda: asyncio.run(async_main()), daemon=True).start()
|
||||||
start_gui()
|
start_gui()
|
||||||
|
|||||||
Reference in New Issue
Block a user