|
|
|
|
@ -14,28 +14,28 @@ from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
|
|
|
|
|
|
|
|
|
# =========================================================
|
|
|
|
|
# GCS 端:XBee Serial <-> UDP Bridge
|
|
|
|
|
# Packet-size TDMA + DONE + RSSI + Auto Discovery + Force Scan
|
|
|
|
|
# Packet-size TDMA + DONE + RSSI + Auto Discovery
|
|
|
|
|
#
|
|
|
|
|
# 對應 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 可立即換下一台。
|
|
|
|
|
# 本版已刪除「一般 DISC / 強制 DSCF」雙模式。
|
|
|
|
|
# 現在只保留一種 discovery 封包,名稱統一叫 DISC。
|
|
|
|
|
#
|
|
|
|
|
# 核心行為:
|
|
|
|
|
# 1. 程式啟動前 AUTO_DISCOVERY_DURATION_SEC 秒自動 broadcast DISC。
|
|
|
|
|
# 2. 收到 HELO 後,GCS 從 XBee 0x90 src64 學 UAV XBee address,
|
|
|
|
|
# 從 HELO payload 學 MAVLink SYSID,建立 SYSID <-> src64 對應。
|
|
|
|
|
# 3. TDMA scheduler 依 ACTIVE_SYSIDS 輪詢。
|
|
|
|
|
# 4. send_poll() 若已知 SYSID 對應 src64,則 unicast;未知則 fallback broadcast。
|
|
|
|
|
# 5. 收到 DONE 後立即查 ATDB,將 RSSI 歸屬到該 SYSID。
|
|
|
|
|
# 6. GUI 的 Scan UAV 按鈕會送 DSCF burst,讓新加入或已存在的 UAV 強制回 HELO。
|
|
|
|
|
# 重要:
|
|
|
|
|
# - DISC 的功能等同於原本的 DSCF,也就是強制 discovery。
|
|
|
|
|
# - GCS 開機自動 discovery 與 GUI Scan UAV 都送 DISC。
|
|
|
|
|
# - UAV 收到 DISC 後會重新回 HELO。
|
|
|
|
|
#
|
|
|
|
|
# 封包格式:
|
|
|
|
|
# GCS -> UAV:
|
|
|
|
|
# DISC
|
|
|
|
|
# POLL + target_sysid(1) + grant_bytes(2)
|
|
|
|
|
#
|
|
|
|
|
# UAV -> GCS:
|
|
|
|
|
# HELO + sysid(1)
|
|
|
|
|
# DONE + sysid(1) + sent_len(2) + remain_len(2)
|
|
|
|
|
# =========================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ================= 多組設備設定 =================
|
|
|
|
|
# Windows 測試時建議先只留你的實際 COM port,例如 COM15。
|
|
|
|
|
CONFIGS = [
|
|
|
|
|
{"serial_port": "/dev/ttyUSB0", "udp_port": 14551},
|
|
|
|
|
{"serial_port": "COM15", "udp_port": 14590},
|
|
|
|
|
@ -46,52 +46,40 @@ CONFIGS = [
|
|
|
|
|
SERIAL_BAUDRATE = 115200
|
|
|
|
|
UDP_REMOTE_IP = '127.0.0.1'
|
|
|
|
|
|
|
|
|
|
# 若你要讓外部程式固定送 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'
|
|
|
|
|
|
|
|
|
|
# 起始已知 SYSID。完全自動 discovery 可設空 list。
|
|
|
|
|
# 若你想保留舊版行為,可改成 [3, 10, 15]。
|
|
|
|
|
INITIAL_ACTIVE_SYSIDS = []
|
|
|
|
|
ACTIVE_SYSIDS = list(INITIAL_ACTIVE_SYSIDS)
|
|
|
|
|
|
|
|
|
|
# 若你知道每台 UAV XBee 的 64-bit address,可以預先填入。
|
|
|
|
|
# 即使不填,本程式也會從 HELO / DONE / MAVLink data 自動學習。
|
|
|
|
|
XBEE_ADDR64_TO_SYSID = {}
|
|
|
|
|
AUTO_LEARN_XBEE_ADDR = True
|
|
|
|
|
|
|
|
|
|
# 控制封包 magic
|
|
|
|
|
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)
|
|
|
|
|
# 只保留 DISC;功能等同原本 DSCF。
|
|
|
|
|
DISC_MAGIC = b'DISC'
|
|
|
|
|
HELLO_MAGIC = b'HELO'
|
|
|
|
|
POLL_MAGIC = b'POLL'
|
|
|
|
|
DONE_MAGIC = b'DONE'
|
|
|
|
|
|
|
|
|
|
# ================= Discovery 參數 =================
|
|
|
|
|
AUTO_DISCOVERY_ENABLED = True
|
|
|
|
|
AUTO_DISCOVERY_DURATION_SEC = 30.0
|
|
|
|
|
DISCOVERY_INTERVAL_SEC = 1.5
|
|
|
|
|
|
|
|
|
|
# GUI 按 Scan UAV 時送出 DSCF burst
|
|
|
|
|
FORCE_DISCOVERY_BURST_COUNT = 5
|
|
|
|
|
FORCE_DISCOVERY_BURST_GAP_SEC = 0.20
|
|
|
|
|
# GUI 按 Scan UAV 時送出 DISC burst
|
|
|
|
|
DISCOVERY_BURST_COUNT = 5
|
|
|
|
|
DISCOVERY_BURST_GAP_SEC = 0.20
|
|
|
|
|
|
|
|
|
|
# ================= TDMA 參數 =================
|
|
|
|
|
current_grant_bytes = 600
|
|
|
|
|
current_guard_ms = 20
|
|
|
|
|
INIT_GRANT_BYTES = 1200
|
|
|
|
|
|
|
|
|
|
# 丟包率計算時間視窗
|
|
|
|
|
LOSS_TIME_WINDOW_SEC = 5.0
|
|
|
|
|
|
|
|
|
|
# RSSI 最近 N 次平均
|
|
|
|
|
RSSI_AVG_WINDOW = 5
|
|
|
|
|
|
|
|
|
|
# 是否也在資料封包時查 DB。預設 False:只在 DONE / HELO 後查 DB,較不干擾資料流。
|
|
|
|
|
RSSI_QUERY_ON_DATA_FRAME = False
|
|
|
|
|
RSSI_DATA_QUERY_MIN_INTERVAL_SEC = 0.5
|
|
|
|
|
|
|
|
|
|
@ -101,7 +89,7 @@ uav_states = {}
|
|
|
|
|
for sid in ACTIVE_SYSIDS:
|
|
|
|
|
uav_states[sid] = {"mode": "NORMAL"}
|
|
|
|
|
|
|
|
|
|
rssi_history = defaultdict(lambda: deque(maxlen=5000)) # 畫圖用:RSSI avgN
|
|
|
|
|
rssi_history = defaultdict(lambda: deque(maxlen=5000))
|
|
|
|
|
rssi_raw_windows = defaultdict(lambda: deque(maxlen=RSSI_AVG_WINDOW))
|
|
|
|
|
rssi_time_history = defaultdict(lambda: deque(maxlen=5000))
|
|
|
|
|
rssi_latest_stats = defaultdict(lambda: {
|
|
|
|
|
@ -117,18 +105,14 @@ packet_loss_time_history = defaultdict(lambda: deque(maxlen=1000))
|
|
|
|
|
mavlink_sequence_tracker = defaultdict(dict)
|
|
|
|
|
packet_loss_stats = defaultdict(lambda: {'loss_rate': 0.0, 'total_received': 0, 'total_lost': 0})
|
|
|
|
|
|
|
|
|
|
# 自動學習到的 XBee address 對應
|
|
|
|
|
learned_addr64_to_sysid = {}
|
|
|
|
|
learned_sysid_to_addr64 = {}
|
|
|
|
|
|
|
|
|
|
# 每個 SYSID 一個 asyncio.Event;收到 DONE 後 set,scheduler 立即換下一台。
|
|
|
|
|
tdma_done_events = {}
|
|
|
|
|
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 = []
|
|
|
|
|
|
|
|
|
|
@ -144,7 +128,6 @@ def format_addr64(addr):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ensure_active_sysid(sysid):
|
|
|
|
|
"""若發現新的 SYSID,加入 ACTIVE_SYSIDS 並建立狀態。"""
|
|
|
|
|
if sysid is None:
|
|
|
|
|
return
|
|
|
|
|
if sysid <= 0:
|
|
|
|
|
@ -162,7 +145,6 @@ def ensure_active_sysid(sysid):
|
|
|
|
|
try:
|
|
|
|
|
tdma_done_events[sysid] = asyncio.Event()
|
|
|
|
|
except RuntimeError:
|
|
|
|
|
# 若從 GUI thread 呼叫時沒有 running loop,scheduler 之後會補建。
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -177,10 +159,6 @@ def infer_sysid_from_addr64(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:
|
|
|
|
|
return
|
|
|
|
|
if sysid is None or src64 is None:
|
|
|
|
|
@ -189,20 +167,29 @@ def learn_xbee_source(sysid, src64):
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
src64 = bytes(src64)
|
|
|
|
|
|
|
|
|
|
old_sysid = learned_addr64_to_sysid.get(src64)
|
|
|
|
|
if old_sysid is not None and old_sysid != sysid:
|
|
|
|
|
print(f"[DISCOVERY] src64 {format_addr64(src64)} changed SYSID {old_sysid} -> {sysid}")
|
|
|
|
|
learned_sysid_to_addr64.pop(old_sysid, None)
|
|
|
|
|
if old_sysid in ACTIVE_SYSIDS:
|
|
|
|
|
ACTIVE_SYSIDS.remove(old_sysid)
|
|
|
|
|
uav_states.pop(old_sysid, None)
|
|
|
|
|
tdma_done_events.pop(old_sysid, None)
|
|
|
|
|
|
|
|
|
|
old_src64 = learned_sysid_to_addr64.get(sysid)
|
|
|
|
|
if old_src64 is not None and old_src64 != src64:
|
|
|
|
|
learned_addr64_to_sysid.pop(old_src64, None)
|
|
|
|
|
|
|
|
|
|
learned_addr64_to_sysid[src64] = sysid
|
|
|
|
|
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
|
|
|
|
|
@ -211,10 +198,6 @@ def get_poll_dest_addr64(sysid):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def record_rssi(sysid, rssi_positive_db, src64=None):
|
|
|
|
|
"""
|
|
|
|
|
XBee ATDB 回傳通常是正值,例如 55 表示 -55 dBm。
|
|
|
|
|
這裡統一轉成負 dBm,並做最近 RSSI_AVG_WINDOW 次平均。
|
|
|
|
|
"""
|
|
|
|
|
if sysid is None:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
@ -238,8 +221,6 @@ def record_rssi(sysid, rssi_positive_db, src64=None):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def calculate_packet_loss(sysid, compid, current_seq):
|
|
|
|
|
global mavlink_sequence_tracker, packet_loss_stats
|
|
|
|
|
|
|
|
|
|
ensure_active_sysid(sysid)
|
|
|
|
|
|
|
|
|
|
tracker = mavlink_sequence_tracker[sysid]
|
|
|
|
|
@ -292,18 +273,11 @@ def calculate_packet_loss(sysid, compid, current_seq):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
return b'\x7E' + struct.pack('>H', len(frame)) + frame + struct.pack('B', 0xFF - (sum(frame) & 0xFF))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def estimate_tdma_timeout(grant_bytes):
|
|
|
|
|
"""
|
|
|
|
|
DONE 正常收到時不會等到 timeout。
|
|
|
|
|
timeout 只在 DONE 掉包或該台沒回應時保護 scheduler。
|
|
|
|
|
"""
|
|
|
|
|
grant_bytes = max(0, int(grant_bytes))
|
|
|
|
|
max_payload = 100
|
|
|
|
|
chunks = max(1, (grant_bytes + max_payload - 1) // max_payload)
|
|
|
|
|
@ -314,7 +288,6 @@ def estimate_tdma_timeout(grant_bytes):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def grant_one_uav(serial_protocols, sysid, grant_bytes):
|
|
|
|
|
"""授權一台 UAV;收到 DONE 立即結束,否則 timeout 後換下一台。"""
|
|
|
|
|
ensure_active_sysid(sysid)
|
|
|
|
|
|
|
|
|
|
ev = tdma_done_events.get(sysid)
|
|
|
|
|
@ -351,7 +324,7 @@ class SerialToUDP(asyncio.Protocol):
|
|
|
|
|
self.current_poll_time = 0.0
|
|
|
|
|
|
|
|
|
|
self.at_frame_id = 0x20
|
|
|
|
|
self.pending_db = {} # frame_id -> {'sysid', 'src64', 'time', 'reason'}
|
|
|
|
|
self.pending_db = {}
|
|
|
|
|
self.last_data_db_query_time = 0.0
|
|
|
|
|
|
|
|
|
|
def connection_made(self, transport):
|
|
|
|
|
@ -360,16 +333,12 @@ class SerialToUDP(asyncio.Protocol):
|
|
|
|
|
self.udp_protocol.set_serial_transport(self)
|
|
|
|
|
print(f"[{self.serial_port}] Serial connection established.")
|
|
|
|
|
|
|
|
|
|
# -------------------- GCS -> UAV control --------------------
|
|
|
|
|
|
|
|
|
|
def send_discovery(self, force=False):
|
|
|
|
|
def send_discovery(self):
|
|
|
|
|
"""
|
|
|
|
|
發 discovery:
|
|
|
|
|
- force=False:DISC,一般 discovery;UAV 對同一 GCS 只回一次 HELO。
|
|
|
|
|
- force=True :DSCF,強制 discovery;UAV 會重新回 HELO。
|
|
|
|
|
發 DISC。
|
|
|
|
|
本版 DISC 功能等同原本 DSCF,UAV 收到後會強制回 HELO。
|
|
|
|
|
"""
|
|
|
|
|
payload = DISC_FORCE_MAGIC if force else DISC_MAGIC
|
|
|
|
|
api_frame = build_api_tx_frame(payload, TARGET_ADDR64, 0x00)
|
|
|
|
|
api_frame = build_api_tx_frame(DISC_MAGIC, TARGET_ADDR64, 0x00)
|
|
|
|
|
self.transport.write(api_frame)
|
|
|
|
|
|
|
|
|
|
def send_poll(self, target_sysid, grant_bytes=None):
|
|
|
|
|
@ -387,8 +356,6 @@ class SerialToUDP(asyncio.Protocol):
|
|
|
|
|
|
|
|
|
|
self.transport.write(api_frame)
|
|
|
|
|
|
|
|
|
|
# -------------------- Serial RX parser --------------------
|
|
|
|
|
|
|
|
|
|
def data_received(self, data):
|
|
|
|
|
self.buffer.extend(data)
|
|
|
|
|
|
|
|
|
|
@ -407,7 +374,6 @@ class SerialToUDP(asyncio.Protocol):
|
|
|
|
|
length = (self.buffer[1] << 8) | self.buffer[2]
|
|
|
|
|
full_length = 3 + length + 1
|
|
|
|
|
|
|
|
|
|
# 本程式使用 XBee RF payload 約 80~100 bytes,正常 length 不會很大。
|
|
|
|
|
if length > 700:
|
|
|
|
|
self.buffer.pop(0)
|
|
|
|
|
continue
|
|
|
|
|
@ -429,8 +395,6 @@ class SerialToUDP(asyncio.Protocol):
|
|
|
|
|
frame_type = frame[3]
|
|
|
|
|
|
|
|
|
|
if frame_type == 0x90:
|
|
|
|
|
# XBee Receive Packet:
|
|
|
|
|
# 7E | len(2) | 90 | src64(8) | src16(2) | options(1) | RF data | checksum
|
|
|
|
|
src64 = frame[4:12]
|
|
|
|
|
rf_data = frame[15:-1]
|
|
|
|
|
self.handle_rx_packet(src64, rf_data)
|
|
|
|
|
@ -440,13 +404,9 @@ class SerialToUDP(asyncio.Protocol):
|
|
|
|
|
self.handle_at_response(frame)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# 其他如 0x8B Transmit Status 可視需求處理;目前忽略。
|
|
|
|
|
|
|
|
|
|
def handle_rx_packet(self, src64, rf_data):
|
|
|
|
|
# 先用 src64 查。若尚未學習,HELO / DONE payload 會帶 sysid,可建立對應。
|
|
|
|
|
sysid_hint = infer_sysid_from_addr64(src64)
|
|
|
|
|
|
|
|
|
|
# 若在最近 poll 時槽內收到資料,也可作為備援歸屬。
|
|
|
|
|
if sysid_hint is None and self.current_poll_sysid is not None:
|
|
|
|
|
if time.time() - self.current_poll_time <= 2.0:
|
|
|
|
|
sysid_hint = self.current_poll_sysid
|
|
|
|
|
@ -454,7 +414,6 @@ class SerialToUDP(asyncio.Protocol):
|
|
|
|
|
result = self.udp_protocol.process_rf_data(rf_data, src64=src64, sysid_hint=sysid_hint)
|
|
|
|
|
|
|
|
|
|
if result is not None:
|
|
|
|
|
# result: ('DONE' or 'HELO', sysid)
|
|
|
|
|
reason, sysid = result
|
|
|
|
|
learn_xbee_source(sysid, src64)
|
|
|
|
|
self.send_at_command_db(sysid, src64=src64, reason=reason)
|
|
|
|
|
@ -466,8 +425,6 @@ class SerialToUDP(asyncio.Protocol):
|
|
|
|
|
self.send_at_command_db(sysid_hint, src64=src64, reason='DATA')
|
|
|
|
|
self.last_data_db_query_time = now
|
|
|
|
|
|
|
|
|
|
# -------------------- ATDB RSSI --------------------
|
|
|
|
|
|
|
|
|
|
def next_at_frame_id(self):
|
|
|
|
|
self.at_frame_id += 1
|
|
|
|
|
if self.at_frame_id > 0xFE:
|
|
|
|
|
@ -475,10 +432,6 @@ class SerialToUDP(asyncio.Protocol):
|
|
|
|
|
return self.at_frame_id
|
|
|
|
|
|
|
|
|
|
def send_at_command_db(self, sysid, src64=None, reason=''):
|
|
|
|
|
"""
|
|
|
|
|
查本地 XBee 的 DB 值。DB 代表本地 XBee 最近收到 RF packet 的 RSSI。
|
|
|
|
|
這裡在 DONE / HELO 後立即查,所以 RSSI 會歸屬到剛剛回報的 sysid。
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
frame_id = self.next_at_frame_id()
|
|
|
|
|
frame_type = 0x08
|
|
|
|
|
@ -495,7 +448,6 @@ class SerialToUDP(asyncio.Protocol):
|
|
|
|
|
'reason': reason,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# 清掉太舊 pending,避免長時間累積
|
|
|
|
|
now = time.time()
|
|
|
|
|
stale = [fid for fid, info in self.pending_db.items() if now - info['time'] > 2.0]
|
|
|
|
|
for fid in stale:
|
|
|
|
|
@ -506,7 +458,6 @@ class SerialToUDP(asyncio.Protocol):
|
|
|
|
|
print(f"[{self.serial_port}] send ATDB failed: {e}")
|
|
|
|
|
|
|
|
|
|
def handle_at_response(self, frame):
|
|
|
|
|
# AT Command Response: 0x88 | frame_id | AT(2) | status | value...
|
|
|
|
|
if len(frame) < 9:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
@ -523,7 +474,6 @@ class SerialToUDP(asyncio.Protocol):
|
|
|
|
|
info = self.pending_db.pop(frame_id, None)
|
|
|
|
|
|
|
|
|
|
if info is None:
|
|
|
|
|
# 理論上不該發生;做保底:歸屬到目前 TDMA slot。
|
|
|
|
|
sysid = self.current_poll_sysid
|
|
|
|
|
src64 = learned_sysid_to_addr64.get(sysid)
|
|
|
|
|
else:
|
|
|
|
|
@ -533,17 +483,13 @@ class SerialToUDP(asyncio.Protocol):
|
|
|
|
|
if sysid is not None:
|
|
|
|
|
record_rssi(sysid, rssi_value, src64=src64)
|
|
|
|
|
|
|
|
|
|
# -------------------- UDP -> UAV downlink --------------------
|
|
|
|
|
|
|
|
|
|
def write_to_serial(self, data):
|
|
|
|
|
self.gcs_tx_queue.extend(data)
|
|
|
|
|
|
|
|
|
|
def flush_gcs_queue(self):
|
|
|
|
|
"""GCS -> UAV 下行資料,小批量送出,避免阻塞上行 TDMA。"""
|
|
|
|
|
if not self.gcs_tx_queue:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# 若還沒任何 UAV address,仍 broadcast。ESP32 端只接受已知 GCS 來源資料。
|
|
|
|
|
send_limit = min(len(self.gcs_tx_queue), 150)
|
|
|
|
|
data_to_send = self.gcs_tx_queue[:send_limit]
|
|
|
|
|
self.gcs_tx_queue = self.gcs_tx_queue[send_limit:]
|
|
|
|
|
@ -558,8 +504,6 @@ class SerialToUDP(asyncio.Protocol):
|
|
|
|
|
chunk = data[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)
|
|
|
|
|
self.transport.write(api_frame)
|
|
|
|
|
await asyncio.sleep(0.01)
|
|
|
|
|
@ -591,7 +535,6 @@ class UDPHandler(asyncio.DatagramProtocol):
|
|
|
|
|
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
|
|
|
|
|
@ -612,10 +555,6 @@ class UDPHandler(asyncio.DatagramProtocol):
|
|
|
|
|
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):
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
@ -640,13 +579,6 @@ class UDPHandler(asyncio.DatagramProtocol):
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def process_rf_data(self, rf_data, src64=None, sysid_hint=None):
|
|
|
|
|
"""
|
|
|
|
|
處理 XBee 0x90 的 RF data。
|
|
|
|
|
回傳:
|
|
|
|
|
('HELO', sysid) 若是 HELO
|
|
|
|
|
('DONE', sysid) 若是 DONE
|
|
|
|
|
None 若是 MAVLink data
|
|
|
|
|
"""
|
|
|
|
|
hello_sysid = self.handle_hello_report(rf_data, src64=src64)
|
|
|
|
|
if hello_sysid is not None:
|
|
|
|
|
return ('HELO', hello_sysid)
|
|
|
|
|
@ -655,7 +587,6 @@ class UDPHandler(asyncio.DatagramProtocol):
|
|
|
|
|
if done_sysid is not None:
|
|
|
|
|
return ('DONE', done_sysid)
|
|
|
|
|
|
|
|
|
|
# MAVLink data
|
|
|
|
|
try:
|
|
|
|
|
for byte in rf_data:
|
|
|
|
|
msg = self.mav_decoder.parse_char(bytes([byte]))
|
|
|
|
|
@ -708,8 +639,7 @@ async def setup_bridge(config):
|
|
|
|
|
async def auto_discovery_task(serial_protocols):
|
|
|
|
|
"""
|
|
|
|
|
程式啟動前 AUTO_DISCOVERY_DURATION_SEC 秒自動送 DISC。
|
|
|
|
|
時間到後停止自動 DISC,避免 UAV 一直回 HELO。
|
|
|
|
|
後續新增 UAV 請用 GUI 的 Scan UAV 按鈕送 DSCF。
|
|
|
|
|
本版 DISC 是強制 discovery,UAV 收到後會重新回 HELO。
|
|
|
|
|
"""
|
|
|
|
|
if not AUTO_DISCOVERY_ENABLED:
|
|
|
|
|
return
|
|
|
|
|
@ -720,24 +650,23 @@ async def auto_discovery_task(serial_protocols):
|
|
|
|
|
while time.time() - start_t < AUTO_DISCOVERY_DURATION_SEC:
|
|
|
|
|
for sp in serial_protocols:
|
|
|
|
|
if hasattr(sp, 'send_discovery'):
|
|
|
|
|
sp.send_discovery(force=False)
|
|
|
|
|
sp.send_discovery()
|
|
|
|
|
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):
|
|
|
|
|
async def discovery_burst(serial_protocols):
|
|
|
|
|
"""
|
|
|
|
|
GUI Scan UAV:送 DSCF burst。
|
|
|
|
|
UAV 收到 DSCF 後會強制回 HELO。
|
|
|
|
|
GUI Scan UAV:送 DISC burst。
|
|
|
|
|
"""
|
|
|
|
|
print("[DISCOVERY] Force scan started")
|
|
|
|
|
for _ in range(FORCE_DISCOVERY_BURST_COUNT):
|
|
|
|
|
print("[DISCOVERY] Manual DISC scan started")
|
|
|
|
|
for _ in range(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")
|
|
|
|
|
sp.send_discovery()
|
|
|
|
|
await asyncio.sleep(DISCOVERY_BURST_GAP_SEC)
|
|
|
|
|
print("[DISCOVERY] Manual DISC scan finished")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def tdma_scheduler(serial_protocols):
|
|
|
|
|
@ -748,19 +677,16 @@ async def tdma_scheduler(serial_protocols):
|
|
|
|
|
tdma_done_events[sysid] = asyncio.Event()
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
# GCS 下行:每輪小批量送出,不再固定空等 100 ms
|
|
|
|
|
for sp in serial_protocols:
|
|
|
|
|
if hasattr(sp, 'flush_gcs_queue'):
|
|
|
|
|
sp.flush_gcs_queue()
|
|
|
|
|
|
|
|
|
|
await asyncio.sleep(current_guard_ms / 1000.0)
|
|
|
|
|
|
|
|
|
|
# 若尚未發現任何 UAV,避免空轉太快
|
|
|
|
|
if not ACTIVE_SYSIDS:
|
|
|
|
|
await asyncio.sleep(0.2)
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
# UAV 上行:以 byte quota 授權,不以固定 time slot 等待
|
|
|
|
|
for sysid in list(ACTIVE_SYSIDS):
|
|
|
|
|
ensure_active_sysid(sysid)
|
|
|
|
|
|
|
|
|
|
@ -798,24 +724,22 @@ async def async_main():
|
|
|
|
|
|
|
|
|
|
def start_gui():
|
|
|
|
|
root = tk.Tk()
|
|
|
|
|
root.title('UAV Packet-size TDMA Control Station - Auto Discovery / Force Scan')
|
|
|
|
|
root.title('UAV Packet-size TDMA Control Station - Auto Discovery')
|
|
|
|
|
root.geometry('1350x900')
|
|
|
|
|
|
|
|
|
|
# --- 左側控制面板 ---
|
|
|
|
|
control_frame = tk.Frame(root, width=390, bg='#f0f0f0', padx=20, pady=20)
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
# --- 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 強制掃描')
|
|
|
|
|
asyncio.run_coroutine_threadsafe(discovery_burst(SERIAL_PROTOCOLS), ASYNC_LOOP)
|
|
|
|
|
scan_label.config(text='已送出 DISC 掃描')
|
|
|
|
|
else:
|
|
|
|
|
scan_label.config(text='尚未建立 serial / asyncio loop')
|
|
|
|
|
|
|
|
|
|
scan_btn = tk.Button(control_frame, text='Scan UAV / 發送 DSCF',
|
|
|
|
|
scan_btn = tk.Button(control_frame, text='Scan UAV / 發送 DISC',
|
|
|
|
|
font=('Arial', 12, 'bold'), bg='#2196F3', fg='white',
|
|
|
|
|
command=on_scan_uav)
|
|
|
|
|
scan_btn.pack(pady=(5, 5), fill=tk.X)
|
|
|
|
|
@ -824,7 +748,6 @@ def start_gui():
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
def on_grant_change(val):
|
|
|
|
|
@ -838,7 +761,6 @@ def start_gui():
|
|
|
|
|
grant_label = tk.Label(control_frame, text=f'每台授權: {current_grant_bytes} bytes', bg='#f0f0f0')
|
|
|
|
|
grant_label.pack()
|
|
|
|
|
|
|
|
|
|
# --- guard time slider ---
|
|
|
|
|
guard_var = tk.IntVar(value=current_guard_ms)
|
|
|
|
|
|
|
|
|
|
def on_guard_change(val):
|
|
|
|
|
@ -853,7 +775,6 @@ def start_gui():
|
|
|
|
|
guard_label = tk.Label(control_frame, text=f'切換保護時間: {current_guard_ms} ms', bg='#f0f0f0')
|
|
|
|
|
guard_label.pack()
|
|
|
|
|
|
|
|
|
|
# --- UAV INIT mode ---
|
|
|
|
|
tk.Label(control_frame, text='群機初始化控制 (INIT)', font=('Arial', 14, 'bold'), bg='#f0f0f0').pack(pady=(30, 10))
|
|
|
|
|
|
|
|
|
|
init_var = tk.IntVar(value=0)
|
|
|
|
|
@ -921,7 +842,6 @@ def start_gui():
|
|
|
|
|
command=toggle_tdma_mode)
|
|
|
|
|
lock_btn.pack(pady=20, fill=tk.X)
|
|
|
|
|
|
|
|
|
|
# --- Reports ---
|
|
|
|
|
report_title = tk.Label(control_frame, text='最近 HELO / DONE 回報', font=('Arial', 12, 'bold'), bg='#f0f0f0')
|
|
|
|
|
report_title.pack(pady=(10, 5))
|
|
|
|
|
|
|
|
|
|
@ -966,7 +886,6 @@ def start_gui():
|
|
|
|
|
for sid in ACTIVE_SYSIDS:
|
|
|
|
|
ensure_rssi_row(sid)
|
|
|
|
|
|
|
|
|
|
# --- status updater ---
|
|
|
|
|
def update_status_gui():
|
|
|
|
|
now = time.time()
|
|
|
|
|
|
|
|
|
|
@ -1011,7 +930,6 @@ def start_gui():
|
|
|
|
|
|
|
|
|
|
update_status_gui()
|
|
|
|
|
|
|
|
|
|
# --- 右側圖表區 ---
|
|
|
|
|
plot_frame = tk.Frame(root)
|
|
|
|
|
plot_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
|
|
|
|
|
|
|
|
|
|
|