|
|
|
|
@ -38,15 +38,21 @@ class PanelState:
|
|
|
|
|
self.object_manager_state = "Stopped"
|
|
|
|
|
self.serial_manager_state = "Stopped"
|
|
|
|
|
self.socket_object_list = [] # 已有的 mavlink object
|
|
|
|
|
self.linked_serial_dict = {} # 已連線的 serial 端口
|
|
|
|
|
self.linked_serial_dict = {} # 已連線的 serial 端口 serial id num : serial_port string
|
|
|
|
|
self.panel_info_msg_list = [] # 顯示在面板上的資訊訊息
|
|
|
|
|
|
|
|
|
|
# 這邊是儲存關於 socket object 的資料
|
|
|
|
|
# 關於創建通道時的暫存資訊
|
|
|
|
|
self.udp_info_temp = {"IP": "127.0.0.1", "Port": "", "Direction": ""} # 暫存 UDP 設定資訊
|
|
|
|
|
self.serial_info_temp = {"Port": "", "Baud": 115200, "CommunicationType": "", "Go2Middleware": True} # 暫存 Serial 設定資訊
|
|
|
|
|
self.socket_info_single = {"socket_type": "", "socket_state": "", "bridge_msg_types": "", "return_msg_types": "",
|
|
|
|
|
"target_sockets": "", "primary_socket_id": "", "socket_connection_string": "",
|
|
|
|
|
"InfoReady": False} # 暫存單一 socket 的資訊
|
|
|
|
|
self.serial_info_temp = {"Port": "", "Baud": 115200, "CommunicationType": "", "Go2Middleware": False} # 暫存 Serial 設定資訊
|
|
|
|
|
|
|
|
|
|
# 關於顯示通道資訊
|
|
|
|
|
self.socket_info_single = {
|
|
|
|
|
"socket_type": "", "socket_state": "", "bridge_msg_types": "", "return_msg_types": "",
|
|
|
|
|
"target_sockets": "", "primary_socket_id": "", "socket_connection_string": "",
|
|
|
|
|
"InfoReady": False} # 暫存單一 socket 的資訊
|
|
|
|
|
self.serial_info_single = {
|
|
|
|
|
"serial_port": "", "baudrate": "", "receiver_type": "", "target_port": "",
|
|
|
|
|
"InfoReady": False} # 暫存單一 serial 連結的資訊
|
|
|
|
|
|
|
|
|
|
def intoSTART(self):
|
|
|
|
|
self.panel_status = "Running"
|
|
|
|
|
@ -178,6 +184,15 @@ class ControlPanel:
|
|
|
|
|
|
|
|
|
|
def show_object_info(self, stdscr, socket_id, state: PanelState):
|
|
|
|
|
"""顯示物件詳細資訊的對話框"""
|
|
|
|
|
|
|
|
|
|
start = time.time()
|
|
|
|
|
while not state.socket_info_single.get('InfoReady', False):
|
|
|
|
|
# 太久沒有回應
|
|
|
|
|
if time.time() - start > 2:
|
|
|
|
|
state.panel_info_msg_list.append(("Fail! Socket Info NOT Aquire!", time.time()))
|
|
|
|
|
return
|
|
|
|
|
time.sleep(0.05) # 等待資訊準備好
|
|
|
|
|
|
|
|
|
|
height, width = stdscr.getmaxyx()
|
|
|
|
|
dialog_height = 15
|
|
|
|
|
dialog_width = min(70, width - 4)
|
|
|
|
|
@ -188,12 +203,9 @@ class ControlPanel:
|
|
|
|
|
dialog_win.border()
|
|
|
|
|
dialog_win.addstr(0, 2, f" Socket #{socket_id} 詳細資訊 ", curses.A_BOLD)
|
|
|
|
|
|
|
|
|
|
while not state.socket_info_single.get('InfoReady', False):
|
|
|
|
|
time.sleep(0.05) # 等待資訊準備好
|
|
|
|
|
|
|
|
|
|
# 這裡顯示基本資訊
|
|
|
|
|
dialog_win.addstr(2, 2, f"Socket ID : {socket_id}")
|
|
|
|
|
# dialog_win.addstr(3, 2, f"Socket status : 運行中")
|
|
|
|
|
dialog_win.addstr(3, 2, f"Socket status : {state.socket_info_single.get('socket_state', 'N/A')}")
|
|
|
|
|
# show_str = ", ".join(map(str, state.socket_info_single.get('socket_type', '')))
|
|
|
|
|
dialog_win.addstr(4, 2, f"Socket Type : {state.socket_info_single.get('socket_type', '')}")
|
|
|
|
|
dialog_win.addstr(4, 30, f"{state.socket_info_single.get('socket_connection_string', '')}")
|
|
|
|
|
@ -300,6 +312,10 @@ class ControlPanel:
|
|
|
|
|
# MenuNode("Telemetry", "數傳模式", "SET_SERIAL_COMM_TELEMETRY"),
|
|
|
|
|
]),
|
|
|
|
|
MenuNode("Set Baud", "設定 Baud", "TEXT_BAUD_SERIAL"),
|
|
|
|
|
MenuNode("Link to Middleware", "方便功能 可以直接建立 UDP object", "LINK_SERIAL_TO_MIDDLEWARE_UDP", children=[
|
|
|
|
|
MenuNode("Yes", action = "LINK_SERIAL_TO_MIDDLEWARE_UDP_YES"),
|
|
|
|
|
MenuNode("No", action = "LINK_SERIAL_TO_MIDDLEWARE_UDP_NO"),
|
|
|
|
|
]),
|
|
|
|
|
MenuNode("Create", "建立此串口", "CREATE_SERIAL_PORT"),
|
|
|
|
|
MenuNode("返回", "回到列表", "BACK"),
|
|
|
|
|
])
|
|
|
|
|
@ -369,8 +385,55 @@ class ControlPanel:
|
|
|
|
|
menu.current_page = page
|
|
|
|
|
return menu
|
|
|
|
|
|
|
|
|
|
def show_linked_serial_info(self):
|
|
|
|
|
pass
|
|
|
|
|
def show_linked_serial_info(self, stdscr, serial_id, state: PanelState):
|
|
|
|
|
"""顯示 Serial 連結詳細資訊的對話框"""
|
|
|
|
|
|
|
|
|
|
start = time.time()
|
|
|
|
|
while not state.serial_info_single.get('InfoReady', False):
|
|
|
|
|
# 太久沒有回應
|
|
|
|
|
if time.time() - start > 2:
|
|
|
|
|
state.panel_info_msg_list.append(("Fail! Serial Info NOT Aquire!", time.time()))
|
|
|
|
|
return
|
|
|
|
|
time.sleep(0.05) # 等待資訊準備好
|
|
|
|
|
|
|
|
|
|
height, width = stdscr.getmaxyx()
|
|
|
|
|
dialog_height = 15
|
|
|
|
|
dialog_width = min(70, width - 4)
|
|
|
|
|
start_y = (height - dialog_height) // 2
|
|
|
|
|
start_x = (width - dialog_width) // 2
|
|
|
|
|
|
|
|
|
|
dialog_win = curses.newwin(dialog_height, dialog_width, start_y, start_x)
|
|
|
|
|
dialog_win.border()
|
|
|
|
|
dialog_win.addstr(0, 2, f" Serial Link #{serial_id} 詳細資訊 ", curses.A_BOLD)
|
|
|
|
|
|
|
|
|
|
# 從 linked_serial_dict 獲取資訊
|
|
|
|
|
serial_info = state.linked_serial_dict.get(serial_id, {})
|
|
|
|
|
|
|
|
|
|
if not serial_info:
|
|
|
|
|
dialog_win.addstr(2, 2, f"無法取得 Serial #{serial_id} 的資訊")
|
|
|
|
|
else:
|
|
|
|
|
# 顯示基本資訊
|
|
|
|
|
dialog_win.addstr(2, 2, f"Serial ID : {serial_id}")
|
|
|
|
|
dialog_win.addstr(3, 2, f"Serial Port : {state.serial_info_single.get('serial_port', 'N/A')}")
|
|
|
|
|
dialog_win.addstr(4, 2, f"Baudrate : {state.serial_info_single.get('baudrate', 'N/A')}")
|
|
|
|
|
dialog_win.addstr(5, 2, f"Communication : {state.serial_info_single.get('receiver_type', 'N/A')}")
|
|
|
|
|
dialog_win.addstr(6, 2, f"Target UDP Port : {state.serial_info_single.get('target_port', 'N/A')}")
|
|
|
|
|
dialog_win.addstr(7, 2, f"Status : {state.serial_info_single.get('status', 'Running')}")
|
|
|
|
|
|
|
|
|
|
# 如果有額外資訊可以繼續添加
|
|
|
|
|
if 'thread_alive' in serial_info:
|
|
|
|
|
thread_status = "Alive" if serial_info['thread_alive'] else "Stopped"
|
|
|
|
|
dialog_win.addstr(8, 2, f"Thread Status : {thread_status}")
|
|
|
|
|
|
|
|
|
|
state.serial_info_single['InfoReady'] = False # 重置狀態以便下次使用
|
|
|
|
|
|
|
|
|
|
dialog_win.addstr(dialog_height - 2, 2, "按任意鍵返回...")
|
|
|
|
|
dialog_win.refresh()
|
|
|
|
|
|
|
|
|
|
dialog_win.getch()
|
|
|
|
|
del dialog_win
|
|
|
|
|
stdscr.clear()
|
|
|
|
|
stdscr.refresh()
|
|
|
|
|
|
|
|
|
|
# ================ 關於 主要選單 的部份 ===================
|
|
|
|
|
|
|
|
|
|
@ -481,6 +544,9 @@ class ControlPanel:
|
|
|
|
|
desc = f"{child.desc} [{state.serial_info_temp['CommunicationType']}]"
|
|
|
|
|
elif child.action == "TEXT_BAUD_SERIAL" and state.serial_info_temp["Baud"]:
|
|
|
|
|
desc = f"{child.desc} [{state.serial_info_temp['Baud']}]"
|
|
|
|
|
elif child.action == "LINK_SERIAL_TO_MIDDLEWARE_UDP":
|
|
|
|
|
link_status = "Yes" if state.serial_info_temp["Go2Middleware"] else "No"
|
|
|
|
|
desc = f"{child.desc} [{link_status}]"
|
|
|
|
|
|
|
|
|
|
line = f"{marker}{child.name:15s} – {desc}"
|
|
|
|
|
attr = curses.A_REVERSE if i == current_idx else curses.A_NORMAL
|
|
|
|
|
@ -643,6 +709,16 @@ class ControlPanel:
|
|
|
|
|
menu_stack.pop()
|
|
|
|
|
idx_stack.pop()
|
|
|
|
|
|
|
|
|
|
elif selected.action == "LINK_SERIAL_TO_MIDDLEWARE_UDP_YES":
|
|
|
|
|
logger.info("mark A")
|
|
|
|
|
state.serial_info_temp["Go2Middleware"] = True
|
|
|
|
|
menu_stack.pop()
|
|
|
|
|
idx_stack.pop()
|
|
|
|
|
elif selected.action == "LINK_SERIAL_TO_MIDDLEWARE_UDP_NO":
|
|
|
|
|
state.serial_info_temp["Go2Middleware"] = False
|
|
|
|
|
menu_stack.pop()
|
|
|
|
|
idx_stack.pop()
|
|
|
|
|
|
|
|
|
|
elif selected.action == "CREATE_SERIAL_PORT":
|
|
|
|
|
state.serial_info_temp["Port"] = menu_stack[-1].name # 從選單取得 Port 名稱
|
|
|
|
|
cmd_q.put("CREATE_SERIAL_PORT")
|
|
|
|
|
@ -663,6 +739,24 @@ class ControlPanel:
|
|
|
|
|
menu_stack.append(created_list_menu)
|
|
|
|
|
idx_stack.append(0)
|
|
|
|
|
|
|
|
|
|
elif selected.action == "INSPECT_LINKED_SERIAL":
|
|
|
|
|
# 顯示 Serial 連結詳細資訊
|
|
|
|
|
if hasattr(selected, 'serial_id'):
|
|
|
|
|
cmd_q.put(("INSPECT_LINKED_SERIAL", selected.serial_id))
|
|
|
|
|
self.show_linked_serial_info(stdscr, selected.serial_id, state)
|
|
|
|
|
|
|
|
|
|
elif selected.action == "REMOVE_LINKED_SERIAL":
|
|
|
|
|
# 移除 Serial 連結
|
|
|
|
|
if hasattr(selected, 'serial_id'):
|
|
|
|
|
cmd_q.put(("REMOVE_LINKED_SERIAL", selected.serial_id))
|
|
|
|
|
# 返回上層(回到列表)
|
|
|
|
|
if len(menu_stack) > 1:
|
|
|
|
|
menu_stack.pop()
|
|
|
|
|
idx_stack.pop()
|
|
|
|
|
# 一樣退兩層
|
|
|
|
|
menu_stack.pop()
|
|
|
|
|
idx_stack.pop()
|
|
|
|
|
|
|
|
|
|
elif selected.action == "LIST_MAV_OBJECT":
|
|
|
|
|
# 動態生成 mavlink_object 列表選單
|
|
|
|
|
created_list_menu = self.create_object_list_menu(state, page=0)
|
|
|
|
|
@ -714,7 +808,6 @@ class ControlPanel:
|
|
|
|
|
if hasattr(selected, 'socket_id'):
|
|
|
|
|
target_id = self.select_target_socket(stdscr, selected.socket_id, state)
|
|
|
|
|
if target_id is not None:
|
|
|
|
|
# cmd_q.put(("MAVOBJ_MAKE_LINK", selected.socket_id, target_id))
|
|
|
|
|
cmd_q.put(("MAVOBJ_ADD_TARGET", selected.socket_id, target_id))
|
|
|
|
|
cmd_q.put(("MAVOBJ_ADD_TARGET", target_id, selected.socket_id)) # 雙向連結
|
|
|
|
|
|
|
|
|
|
@ -770,6 +863,7 @@ class ControlPanel:
|
|
|
|
|
class Orchestrator:
|
|
|
|
|
def __init__(self, stop_sig):
|
|
|
|
|
self.stop_evt = stop_sig # 外部操作去中斷 "面板" 執行緒的訊號 (內部自己停止的話不需要用這個)
|
|
|
|
|
self.occupied_ip_ports = {} # 紀錄已被佔用的 ip:port 組合 "ip str" : [port int, port int, ...]
|
|
|
|
|
|
|
|
|
|
# === 1) 面板部分的準備 ===
|
|
|
|
|
self.cmd_q = queue.Queue()
|
|
|
|
|
@ -855,7 +949,7 @@ class Orchestrator:
|
|
|
|
|
mav_obj = self.manager.managed_objects.get(socket_id, None)
|
|
|
|
|
if mav_obj:
|
|
|
|
|
self.panelState.socket_info_single["socket_type"] = mav_obj.socket_type
|
|
|
|
|
# self.panelState.socket_info_single["socket_state"] = "Running" if mav_obj.running
|
|
|
|
|
self.panelState.socket_info_single["socket_state"] = mav_obj.state.name
|
|
|
|
|
self.panelState.socket_info_single["bridge_msg_types"] = mav_obj.bridge_msg_types
|
|
|
|
|
self.panelState.socket_info_single["return_msg_types"] = mav_obj.return_msg_types
|
|
|
|
|
self.panelState.socket_info_single["primary_socket_id"] = mav_obj.primary_socket_id
|
|
|
|
|
@ -864,6 +958,18 @@ class Orchestrator:
|
|
|
|
|
self.panelState.socket_info_single["socket_connection_string"] = f"{ip_info[0]}:{ip_info[1]}"
|
|
|
|
|
# getattr(mav_obj.mavlink_socket, "connection_string", "")
|
|
|
|
|
self.panelState.socket_info_single["InfoReady"] = True # 標記資訊已準備好
|
|
|
|
|
elif action == "INSPECT_LINKED_SERIAL":
|
|
|
|
|
serial_id = cmd[1]
|
|
|
|
|
serial_obj = self.plumber.serial_objects.get(serial_id, None)
|
|
|
|
|
if serial_obj:
|
|
|
|
|
self.panelState.serial_info_single["serial_port"] = serial_obj.serial_port
|
|
|
|
|
self.panelState.serial_info_single["baudrate"] = serial_obj.baudrate
|
|
|
|
|
self.panelState.serial_info_single["receiver_type"] = serial_obj.receiver_type.name
|
|
|
|
|
self.panelState.serial_info_single["target_port"] = serial_obj.target_port
|
|
|
|
|
self.panelState.serial_info_single["InfoReady"] = True # 標記資訊已準備好
|
|
|
|
|
elif action == "REMOVE_LINKED_SERIAL":
|
|
|
|
|
serial_id = cmd[1]
|
|
|
|
|
self.plumber.remove_serial_link(serial_id)
|
|
|
|
|
|
|
|
|
|
elif cmd == "CREATE_UDP_INBOUND":
|
|
|
|
|
self.panelState.udp_info_temp["direction"] = "inbound"
|
|
|
|
|
@ -919,18 +1025,35 @@ class Orchestrator:
|
|
|
|
|
# =============== 面板動作 - Mavlink Object ===============
|
|
|
|
|
|
|
|
|
|
def create_udp_object(self):
|
|
|
|
|
# 監聽部分
|
|
|
|
|
if self.panelState.udp_info_temp["direction"] == "inbound":
|
|
|
|
|
connection_string = f"udp:{self.panelState.udp_info_temp['IP']}:{self.panelState.udp_info_temp['Port']}"
|
|
|
|
|
# 監聽的 port 要先檢查是否被佔用
|
|
|
|
|
ip = self.panelState.udp_info_temp['IP']
|
|
|
|
|
port = int(self.panelState.udp_info_temp['Port'])
|
|
|
|
|
port_check_list = self.occupied_ip_ports.get(ip, []) + self.occupied_ip_ports.get("0.0.0.0", [])
|
|
|
|
|
if port in port_check_list:
|
|
|
|
|
self.panelState.panel_info_msg_list.append((f"Failed! Port {port} on IP {ip} occupied.", time.time()-1))
|
|
|
|
|
return
|
|
|
|
|
# 再記錄被佔用的 port
|
|
|
|
|
if ip in self.occupied_ip_ports:
|
|
|
|
|
self.occupied_ip_ports[ip].append(port)
|
|
|
|
|
else:
|
|
|
|
|
self.occupied_ip_ports[ip] = [port]
|
|
|
|
|
# 外放資訊部分
|
|
|
|
|
elif self.panelState.udp_info_temp["direction"] == "outbound":
|
|
|
|
|
connection_string = f"udpout:{self.panelState.udp_info_temp['IP']}:{self.panelState.udp_info_temp['Port']}"
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
# 檢測這個 connection_string 是否能成功建立 mavlink 連結
|
|
|
|
|
mavlink_socket = mavutil.mavlink_connection(connection_string)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
self.panelState.panel_info_msg_list.append((f"Failed to create UDP {self.panelState.udp_info_temp['direction']} object: {e}", time.time()-1))
|
|
|
|
|
return
|
|
|
|
|
# mavlink 連結建立成功 把他丟到 mavlink_object
|
|
|
|
|
mavlink_object = mo.mavlink_object(mavlink_socket)
|
|
|
|
|
mavlink_object.socket_type = "UDP " + self.panelState.udp_info_temp['direction'].capitalize()
|
|
|
|
|
# 再把 mavlink_object 丟到 manager 的 event loop 裡面去管理與執行
|
|
|
|
|
self.manager.add_mavlink_object(mavlink_object)
|
|
|
|
|
self.panelState.panel_info_msg_list.append((f"Created UDP {self.panelState.udp_info_temp['direction']} object: {connection_string}", time.time()))
|
|
|
|
|
|
|
|
|
|
@ -967,6 +1090,14 @@ class Orchestrator:
|
|
|
|
|
# =============== 面板動作 - Serial Manager ===============
|
|
|
|
|
|
|
|
|
|
def create_serial_port_object(self):
|
|
|
|
|
# 先檢查是否已有相同的 Serial Port 被建立
|
|
|
|
|
serial_port_strs = self.panelState.linked_serial_dict.values() # linked_serial_dict 會在上面的 mainLoop 被不斷更新
|
|
|
|
|
if self.panelState.serial_info_temp['Port'] in serial_port_strs:
|
|
|
|
|
self.panelState.panel_info_msg_list.append(
|
|
|
|
|
(f"Fail! Serial Port {self.panelState.serial_info_temp['Port']} already linked.", time.time())
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# 獲取可用的 udp port
|
|
|
|
|
udp_port_tmp = find_available_port(19000, 20000)
|
|
|
|
|
|
|
|
|
|
@ -1004,6 +1135,16 @@ class Orchestrator:
|
|
|
|
|
self.panelState.panel_info_msg_list.append((f"Failed to create Serial Port object at {self.panelState.serial_info_temp['Port']}.", time.time()))
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
self.panelState.panel_info_msg_list.append((f"Created Serial Port object at {self.panelState.serial_info_temp['Port']}.", time.time()))
|
|
|
|
|
|
|
|
|
|
# 自動建立對應的 UDP 監聽端口
|
|
|
|
|
if self.panelState.serial_info_temp['Go2Middleware']:
|
|
|
|
|
self.panelState.udp_info_temp['IP'] = "127.0.0.1"
|
|
|
|
|
self.panelState.udp_info_temp['Port'] = str(udp_port_tmp)
|
|
|
|
|
self.panelState.udp_info_temp['direction'] = "inbound"
|
|
|
|
|
self.create_udp_object()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
|
|
|
|
|
|
stop_evt = threading.Event()
|
|
|
|
|
@ -1024,3 +1165,15 @@ if __name__ == "__main__":
|
|
|
|
|
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
================= 改版記錄 ============================
|
|
|
|
|
|
|
|
|
|
2025.12.23
|
|
|
|
|
1. 新增 serial 通道的資訊顯示完整化
|
|
|
|
|
2. 新增 serial 通道刪除功能
|
|
|
|
|
3. 新增 serial 直接順便開 ip object
|
|
|
|
|
4. 修改 避免 serial 與 ip port 重複建立相同的通道
|
|
|
|
|
5. 修改 show_object_info 與 show_linked_serial_info 改變檢核 Ready 方式 避免卡死
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
|