|
|
|
@ -20,7 +20,11 @@ from pymavlink import mavutil
|
|
|
|
|
|
|
|
|
|
|
|
# 自定義的 import
|
|
|
|
# 自定義的 import
|
|
|
|
from . import mavlinkObject as mo
|
|
|
|
from . import mavlinkObject as mo
|
|
|
|
|
|
|
|
from . import serialManager as sm
|
|
|
|
|
|
|
|
|
|
|
|
from .utils import RingBuffer, setup_logger
|
|
|
|
from .utils import RingBuffer, setup_logger
|
|
|
|
|
|
|
|
from .utils import acquireSerial, acquirePort
|
|
|
|
|
|
|
|
from .utils.acquirePort import find_available_port
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -32,13 +36,17 @@ class PanelState:
|
|
|
|
termination_start_time = None
|
|
|
|
termination_start_time = None
|
|
|
|
self.mavlink_bridge_state = "Stopped"
|
|
|
|
self.mavlink_bridge_state = "Stopped"
|
|
|
|
self.object_manager_state = "Stopped"
|
|
|
|
self.object_manager_state = "Stopped"
|
|
|
|
self.socket_object_list = []
|
|
|
|
self.serial_manager_state = "Stopped"
|
|
|
|
|
|
|
|
self.socket_object_list = [] # 已有的 mavlink object
|
|
|
|
|
|
|
|
self.linked_serial_dict = {} # 已連線的 serial 端口
|
|
|
|
self.panel_info_msg_list = [] # 顯示在面板上的資訊訊息
|
|
|
|
self.panel_info_msg_list = [] # 顯示在面板上的資訊訊息
|
|
|
|
|
|
|
|
|
|
|
|
# 這邊是儲存關於 socket object 的資料
|
|
|
|
# 這邊是儲存關於 socket object 的資料
|
|
|
|
self.udp_info_temp = {"IP": "127.0.0.1", "Port": "", "Direction": ""} # 暫存 UDP 設定資訊
|
|
|
|
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": "",
|
|
|
|
self.socket_info_single = {"socket_type": "", "socket_state": "", "bridge_msg_types": "", "return_msg_types": "",
|
|
|
|
"target_sockets": "", "primary_socket_id": "", "InfoReady": False} # 暫存單一 socket 的資訊
|
|
|
|
"target_sockets": "", "primary_socket_id": "", "socket_connection_string": "",
|
|
|
|
|
|
|
|
"InfoReady": False} # 暫存單一 socket 的資訊
|
|
|
|
|
|
|
|
|
|
|
|
def intoSTART(self):
|
|
|
|
def intoSTART(self):
|
|
|
|
self.panel_status = "Running"
|
|
|
|
self.panel_status = "Running"
|
|
|
|
@ -121,7 +129,9 @@ class ControlPanel:
|
|
|
|
|
|
|
|
|
|
|
|
return user_input
|
|
|
|
return user_input
|
|
|
|
|
|
|
|
|
|
|
|
def create_object_list_menu(self, state: PanelState, page=0, items_per_page=5):
|
|
|
|
# ================ 關於 mavlink object 的部份 ===================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def create_object_list_menu(self, state: PanelState, page=0, items_per_page=8):
|
|
|
|
"""動態創建 mavlink_object 列表選單(支持分頁)"""
|
|
|
|
"""動態創建 mavlink_object 列表選單(支持分頁)"""
|
|
|
|
children = []
|
|
|
|
children = []
|
|
|
|
|
|
|
|
|
|
|
|
@ -139,33 +149,33 @@ class ControlPanel:
|
|
|
|
obj_menu = MenuNode(f"Socket #{socket_id}", f"連結口 {socket_id}", None, children=[
|
|
|
|
obj_menu = MenuNode(f"Socket #{socket_id}", f"連結口 {socket_id}", None, children=[
|
|
|
|
MenuNode("Info", "查看詳細資訊", "INSPECT_MAV_OBJECT"),
|
|
|
|
MenuNode("Info", "查看詳細資訊", "INSPECT_MAV_OBJECT"),
|
|
|
|
MenuNode("Make Link", "建立轉發連結", "MAVOBJ_MAKE_LINK"),
|
|
|
|
MenuNode("Make Link", "建立轉發連結", "MAVOBJ_MAKE_LINK"),
|
|
|
|
MenuNode("Remove", "移除此連結口", "REMOVE_MAV_OBJECT"),
|
|
|
|
MenuNode("Cancel Link", "取消轉發連結", "MAVOBJ_CANCEL_LINK"),
|
|
|
|
MenuNode("Add Target", "添加轉發目標(工程)", "MAVOBJ_ADD_TARGET"),
|
|
|
|
MenuNode("Add Target", "添加轉發目標(工程)", "MAVOBJ_ADD_TARGET"),
|
|
|
|
MenuNode("Remove Target", "移除轉發目標(工程)", "MAVOBJ_REMOVE_TARGET"),
|
|
|
|
MenuNode("Remove", "移除此連結口", "REMOVE_MAV_OBJECT"),
|
|
|
|
MenuNode("返回", "回到列表", "BACK"),
|
|
|
|
MenuNode("返回", "回到列表", "BACK"),
|
|
|
|
])
|
|
|
|
])
|
|
|
|
# 將 socket_id 附加到每個子選單項目上
|
|
|
|
# 將 socket_id 附加到每個子選單項目上
|
|
|
|
for child in obj_menu.children:
|
|
|
|
for child in obj_menu.children:
|
|
|
|
child.socket_id = socket_id
|
|
|
|
child.socket_id = socket_id
|
|
|
|
children.append(obj_menu)
|
|
|
|
children.append(obj_menu)
|
|
|
|
|
|
|
|
|
|
|
|
# 添加分頁控制
|
|
|
|
# 添加分頁控制
|
|
|
|
if total_pages > 1:
|
|
|
|
if total_pages > 1:
|
|
|
|
children.append(MenuNode("---", "---", None))
|
|
|
|
children.append(MenuNode("---", f"第 {page+1}/{total_pages} 頁", None))
|
|
|
|
if page > 0:
|
|
|
|
if page > 0:
|
|
|
|
prev_node = MenuNode("◀ 上一頁", f"第 {page}/{total_pages} 頁", "PREV_PAGE")
|
|
|
|
prev_node = MenuNode("◀ Prev", "上頁", "PREV_PAGE")
|
|
|
|
prev_node.page = page - 1
|
|
|
|
prev_node.page = page - 1
|
|
|
|
children.append(prev_node)
|
|
|
|
children.append(prev_node)
|
|
|
|
if page < total_pages - 1:
|
|
|
|
if page < total_pages - 1:
|
|
|
|
next_node = MenuNode("下一頁 ▶", f"第 {page + 2}/{total_pages} 頁", "NEXT_PAGE")
|
|
|
|
next_node = MenuNode("Next ▶", "下頁", "NEXT_PAGE")
|
|
|
|
next_node.page = page + 1
|
|
|
|
next_node.page = page + 1
|
|
|
|
children.append(next_node)
|
|
|
|
children.append(next_node)
|
|
|
|
|
|
|
|
|
|
|
|
children.append(MenuNode("返回", "回到上層選單", "BACK"))
|
|
|
|
children.append(MenuNode("返回", "回到上層選單", "BACK"))
|
|
|
|
menu = MenuNode("Object List", f"連結口列表 (第 {page + 1} 頁)", children=children)
|
|
|
|
menu = MenuNode("Object List", f"連結口列表 (第 {page + 1} 頁)", children=children)
|
|
|
|
menu.current_page = page
|
|
|
|
menu.current_page = page
|
|
|
|
return menu
|
|
|
|
return menu
|
|
|
|
|
|
|
|
|
|
|
|
def show_object_info(self, stdscr, socket_id, state: PanelState):
|
|
|
|
def show_object_info(self, stdscr, socket_id, state: PanelState):
|
|
|
|
"""顯示物件詳細資訊的對話框"""
|
|
|
|
"""顯示物件詳細資訊的對話框"""
|
|
|
|
height, width = stdscr.getmaxyx()
|
|
|
|
height, width = stdscr.getmaxyx()
|
|
|
|
@ -183,9 +193,10 @@ class ControlPanel:
|
|
|
|
|
|
|
|
|
|
|
|
# 這裡顯示基本資訊
|
|
|
|
# 這裡顯示基本資訊
|
|
|
|
dialog_win.addstr(2, 2, f"Socket ID : {socket_id}")
|
|
|
|
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 : 運行中")
|
|
|
|
# show_str = ", ".join(map(str, state.socket_info_single.get('socket_type', '')))
|
|
|
|
# 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, 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', '')}")
|
|
|
|
show_str = ",".join(map(str, state.socket_info_single.get('bridge_msg_types', '')))
|
|
|
|
show_str = ",".join(map(str, state.socket_info_single.get('bridge_msg_types', '')))
|
|
|
|
dialog_win.addstr(5, 2, f"Bridge Pack : {show_str if show_str else 'N/A'}")
|
|
|
|
dialog_win.addstr(5, 2, f"Bridge Pack : {show_str if show_str else 'N/A'}")
|
|
|
|
show_str = ",".join(map(str, state.socket_info_single.get('return_msg_types', '')))
|
|
|
|
show_str = ",".join(map(str, state.socket_info_single.get('return_msg_types', '')))
|
|
|
|
@ -262,11 +273,111 @@ class ControlPanel:
|
|
|
|
stdscr.clear()
|
|
|
|
stdscr.clear()
|
|
|
|
stdscr.refresh()
|
|
|
|
stdscr.refresh()
|
|
|
|
return None
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ================ 關於 serial link 的部份 ===================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def create_serial_port_menu(self, state: PanelState, page=0, items_per_page=8):
|
|
|
|
|
|
|
|
"""動態創建 serial port 列表選單(支持分頁)"""
|
|
|
|
|
|
|
|
children = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 獲取可用的 Serial 連接埠列表
|
|
|
|
|
|
|
|
# serial_ports = acquireSerial.get_serial_ports() # debug 全部抓一抓
|
|
|
|
|
|
|
|
serial_ports = acquireSerial.get_serial_ports_with_filter('/dev/ttyUSB*')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if not serial_ports:
|
|
|
|
|
|
|
|
children.append(MenuNode("(空)", "目前沒有串口設備", None))
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
total_items = len(serial_ports)
|
|
|
|
|
|
|
|
total_pages = (total_items + items_per_page - 1) // items_per_page
|
|
|
|
|
|
|
|
start_idx = page * items_per_page
|
|
|
|
|
|
|
|
end_idx = min(start_idx + items_per_page, total_items)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 顯示當前頁的串口
|
|
|
|
|
|
|
|
for port in serial_ports[start_idx:end_idx]:
|
|
|
|
|
|
|
|
port_menu = MenuNode(f"{port}", children=[
|
|
|
|
|
|
|
|
MenuNode("Set Comm Type", "設定通訊形態", "SET_SERIAL_COMM", children=[
|
|
|
|
|
|
|
|
MenuNode("XBee(API-AT)", "XBee 模式", "SET_SERIAL_COMM_XBEE"),
|
|
|
|
|
|
|
|
# MenuNode("Telemetry", "數傳模式", "SET_SERIAL_COMM_TELEMETRY"),
|
|
|
|
|
|
|
|
]),
|
|
|
|
|
|
|
|
MenuNode("Set Baud", "設定 Baud", "TEXT_BAUD_SERIAL"),
|
|
|
|
|
|
|
|
MenuNode("Create", "建立此串口", "CREATE_SERIAL_PORT"),
|
|
|
|
|
|
|
|
MenuNode("返回", "回到列表", "BACK"),
|
|
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
# 將 port 附加到每個子選單項目上
|
|
|
|
|
|
|
|
for child in port_menu.children:
|
|
|
|
|
|
|
|
child.port = port
|
|
|
|
|
|
|
|
children.append(port_menu)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 添加分頁控制
|
|
|
|
|
|
|
|
if total_pages > 1:
|
|
|
|
|
|
|
|
children.append(MenuNode("---", f"第 {page+1}/{total_pages} 頁", None))
|
|
|
|
|
|
|
|
if page > 0:
|
|
|
|
|
|
|
|
prev_node = MenuNode("◀ Prev", "上頁", "PREV_PAGE")
|
|
|
|
|
|
|
|
prev_node.page = page - 1
|
|
|
|
|
|
|
|
children.append(prev_node)
|
|
|
|
|
|
|
|
if page < total_pages - 1:
|
|
|
|
|
|
|
|
next_node = MenuNode("Next ▶", "下頁", "NEXT_PAGE")
|
|
|
|
|
|
|
|
next_node.page = page + 1
|
|
|
|
|
|
|
|
children.append(next_node)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
children.append(MenuNode("返回", "回到上層選單", "BACK"))
|
|
|
|
|
|
|
|
menu = MenuNode("Serial Port List", f"串口列表 (第 {page + 1} 頁)", children=children)
|
|
|
|
|
|
|
|
menu.current_page = page
|
|
|
|
|
|
|
|
return menu
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def create_linked_serial_menu(self, state: PanelState, page=0, items_per_page=8):
|
|
|
|
|
|
|
|
"""動態創建 已連線的 serial port 列表選單(支持分頁)並包含後續管理功能"""
|
|
|
|
|
|
|
|
children = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if not state.linked_serial_dict:
|
|
|
|
|
|
|
|
children.append(MenuNode("(空)", "目前沒有連結口", None))
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
total_items = len(state.linked_serial_dict)
|
|
|
|
|
|
|
|
total_pages = (total_items + items_per_page - 1) // items_per_page
|
|
|
|
|
|
|
|
start_idx = page * items_per_page
|
|
|
|
|
|
|
|
end_idx = min(start_idx + items_per_page, total_items)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 顯示當前頁的物件
|
|
|
|
|
|
|
|
linked_serial_id_list = list(state.linked_serial_dict.keys())
|
|
|
|
|
|
|
|
for serial_id in linked_serial_id_list[start_idx:end_idx]:
|
|
|
|
|
|
|
|
# 為每個 socket 創建子選單
|
|
|
|
|
|
|
|
obj_menu = MenuNode(f"Serial #{serial_id}", f"連結口 {serial_id}", None, children=[
|
|
|
|
|
|
|
|
MenuNode("Info", "查看詳細資訊", "INSPECT_LINKED_SERIAL"),
|
|
|
|
|
|
|
|
MenuNode("Remove", "移除此連結口", "REMOVE_LINKED_SERIAL"),
|
|
|
|
|
|
|
|
# MenuNode("Change UDP Target", "變更目標 UDP (工程)", "CHANGE_LINKED_SERIAL_TARGET"),
|
|
|
|
|
|
|
|
MenuNode("返回", "回到列表", "BACK"),
|
|
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
# 將 serial_id 附加到每個子選單項目上
|
|
|
|
|
|
|
|
for child in obj_menu.children:
|
|
|
|
|
|
|
|
child.serial_id = serial_id
|
|
|
|
|
|
|
|
children.append(obj_menu)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 添加分頁控制
|
|
|
|
|
|
|
|
if total_pages > 1:
|
|
|
|
|
|
|
|
children.append(MenuNode("---", f"第 {page+1}/{total_pages} 頁", None))
|
|
|
|
|
|
|
|
if page > 0:
|
|
|
|
|
|
|
|
prev_node = MenuNode("◀ Prev", "上頁", "PREV_PAGE")
|
|
|
|
|
|
|
|
prev_node.page = page - 1
|
|
|
|
|
|
|
|
children.append(prev_node)
|
|
|
|
|
|
|
|
if page < total_pages - 1:
|
|
|
|
|
|
|
|
next_node = MenuNode("Next ▶", "下頁", "NEXT_PAGE")
|
|
|
|
|
|
|
|
next_node.page = page + 1
|
|
|
|
|
|
|
|
children.append(next_node)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
children.append(MenuNode("返回", "回到上層選單", "BACK"))
|
|
|
|
|
|
|
|
menu = MenuNode("Linked Serial List", f"連結口列表 (第 {page + 1} 頁)", children=children)
|
|
|
|
|
|
|
|
menu.current_page = page
|
|
|
|
|
|
|
|
return menu
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def show_linked_serial_info(self):
|
|
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ================ 關於 主要選單 的部份 ===================
|
|
|
|
|
|
|
|
|
|
|
|
def menu_tree(self):
|
|
|
|
def menu_tree(self):
|
|
|
|
"""建立多層選單結構"""
|
|
|
|
"""建立多層選單結構"""
|
|
|
|
return MenuNode("Main Menu", children=[
|
|
|
|
return MenuNode("Main Menu", children=[
|
|
|
|
MenuNode("MavLink Object", "控制 MavLink 物件", children=[
|
|
|
|
MenuNode("MavLink Object", "MavLink 通道選項", children=[
|
|
|
|
MenuNode("New+", children=[
|
|
|
|
MenuNode("New+", children=[
|
|
|
|
MenuNode("UDP InBound", children=[
|
|
|
|
MenuNode("UDP InBound", children=[
|
|
|
|
MenuNode("IP(Listen)", "設定監聽的 IP 位址", "TEXT_UDP_IP"),
|
|
|
|
MenuNode("IP(Listen)", "設定監聽的 IP 位址", "TEXT_UDP_IP"),
|
|
|
|
@ -281,9 +392,14 @@ class ControlPanel:
|
|
|
|
]),
|
|
|
|
]),
|
|
|
|
MenuNode("ListAll", "顯示並管理所有連結口", "LIST_MAV_OBJECT"),
|
|
|
|
MenuNode("ListAll", "顯示並管理所有連結口", "LIST_MAV_OBJECT"),
|
|
|
|
]),
|
|
|
|
]),
|
|
|
|
|
|
|
|
MenuNode("Serial Manager", "Serial 連接埠選項", children=[
|
|
|
|
|
|
|
|
MenuNode("New+", "新增 Serial 連接埠", action = "LIST_SERIAL_RES"),
|
|
|
|
|
|
|
|
MenuNode("ListAll", "顯示已連線的 Serial", action = "LIST_SERIAL_LINKS"),
|
|
|
|
|
|
|
|
]),
|
|
|
|
MenuNode("Engineer Mode", "工程模式", children=[
|
|
|
|
MenuNode("Engineer Mode", "工程模式", children=[
|
|
|
|
MenuNode("Stop Manager", "停止 Mavlink 物件管理", "STOP_MANAGER"), #TODO: 尚未實作
|
|
|
|
MenuNode("Stop Manager", "停止 Mavlink 物件管理", "STOP_MANAGER"),
|
|
|
|
MenuNode("Stop Bridge", "停止 Mavlink-ROS 橋接", "STOP_BRIDGE"), #TODO: 尚未實作
|
|
|
|
MenuNode("Stop Bridge", "停止 Mavlink-ROS 橋接", "STOP_BRIDGE"),
|
|
|
|
|
|
|
|
MenuNode("Stop Serial M.", "停止 Serial 端口轉接", "STOP_SERIAL_MANAGER"),
|
|
|
|
]),
|
|
|
|
]),
|
|
|
|
MenuNode("Shutdown", "關閉整個系統", children=[
|
|
|
|
MenuNode("Shutdown", "關閉整個系統", children=[
|
|
|
|
MenuNode("Return", "繼續運行", "BACK"),
|
|
|
|
MenuNode("Return", "繼續運行", "BACK"),
|
|
|
|
@ -302,11 +418,11 @@ class ControlPanel:
|
|
|
|
curses.echo()
|
|
|
|
curses.echo()
|
|
|
|
curses.endwin()
|
|
|
|
curses.endwin()
|
|
|
|
|
|
|
|
|
|
|
|
def panel_shutdown():
|
|
|
|
def pre_panel_shutdown():
|
|
|
|
# 先關閉所有模組 再關閉面板
|
|
|
|
# 先關閉所有模組 再關閉面板
|
|
|
|
cmd_q.put("SHUTDOWN_BRIDGE")
|
|
|
|
cmd_q.put("SHUTDOWN_BRIDGE")
|
|
|
|
cmd_q.put("SHUTDOWN_MANAGER")
|
|
|
|
cmd_q.put("SHUTDOWN_MANAGER")
|
|
|
|
|
|
|
|
cmd_q.put("SHUTDOWN_SERIAL_MANAGER")
|
|
|
|
|
|
|
|
|
|
|
|
def draw_menu(screen):
|
|
|
|
def draw_menu(screen):
|
|
|
|
nonlocal stdscr
|
|
|
|
nonlocal stdscr
|
|
|
|
@ -323,9 +439,6 @@ class ControlPanel:
|
|
|
|
state.intoSTART() # 設定狀態為運行中
|
|
|
|
state.intoSTART() # 設定狀態為運行中
|
|
|
|
|
|
|
|
|
|
|
|
while not stop_evt.is_set():
|
|
|
|
while not stop_evt.is_set():
|
|
|
|
# 檢查是否需要退出
|
|
|
|
|
|
|
|
if stop_evt.is_set():
|
|
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
current_menu = menu_stack[-1]
|
|
|
|
current_menu = menu_stack[-1]
|
|
|
|
current_idx = idx_stack[-1]
|
|
|
|
current_idx = idx_stack[-1]
|
|
|
|
@ -333,23 +446,26 @@ class ControlPanel:
|
|
|
|
# 獲取終端機尺寸
|
|
|
|
# 獲取終端機尺寸
|
|
|
|
height, width = stdscr.getmaxyx()
|
|
|
|
height, width = stdscr.getmaxyx()
|
|
|
|
# 簡單暴力的限制視窗的大小
|
|
|
|
# 簡單暴力的限制視窗的大小
|
|
|
|
if height < 20 or width < 60:
|
|
|
|
MIN_HEIGHT = (
|
|
|
|
|
|
|
|
2 + # 邊界
|
|
|
|
|
|
|
|
6 + # 狀態列 操作說明列 一個空白
|
|
|
|
|
|
|
|
11+ # 最大選單 與 空白區
|
|
|
|
|
|
|
|
5 # 訊息區域
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
if height < MIN_HEIGHT or width < 60:
|
|
|
|
logger.error("Terminal size too small for Control Panel.")
|
|
|
|
logger.error("Terminal size too small for Control Panel.")
|
|
|
|
break
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
stdscr.clear()
|
|
|
|
stdscr.clear()
|
|
|
|
|
|
|
|
|
|
|
|
stdscr.border()
|
|
|
|
stdscr.border()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 更新模組狀態顯示
|
|
|
|
stdscr.addstr(0, 10, " MavLink MiddleWare ", curses.A_BOLD)
|
|
|
|
stdscr.addstr(0, 10, " MavLink MiddleWare ", curses.A_BOLD)
|
|
|
|
stdscr.addstr(1, 2, f" Panel Status : {state.panel_status}")
|
|
|
|
stdscr.addstr(1, 2, f" Panel Status : {state.panel_status}")
|
|
|
|
stdscr.addstr(2, 2, f"mavlink Bridge State : {state.mavlink_bridge_state}")
|
|
|
|
stdscr.addstr(2, 2, f"Object Manager State : {state.object_manager_state}")
|
|
|
|
stdscr.addstr(3, 2, f"Object Manager State : {state.object_manager_state}")
|
|
|
|
stdscr.addstr(3, 2, f"Mavlink Bridge State : {state.mavlink_bridge_state}")
|
|
|
|
stdscr.addstr(4, 2, f"Socket Object number : {len(state.socket_object_list)}")
|
|
|
|
stdscr.addstr(4, 2, f"Socket Object number : {len(state.socket_object_list)}")
|
|
|
|
|
|
|
|
stdscr.addstr(2, 36, f"Serial Manager State : {state.serial_manager_state}")
|
|
|
|
# # 更新模組狀態顯示
|
|
|
|
|
|
|
|
# stdscr.addstr(2, 25, f"{state.mavlink_bridge_state}")
|
|
|
|
|
|
|
|
# stdscr.addstr(3, 25, f"{state.object_manager_state}")
|
|
|
|
|
|
|
|
# stdscr.addstr(4, 25, f"{len(state.socket_object_list)} ")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 顯示當前選單項目
|
|
|
|
# 顯示當前選單項目
|
|
|
|
start_line = 6
|
|
|
|
start_line = 6
|
|
|
|
@ -361,6 +477,10 @@ class ControlPanel:
|
|
|
|
desc = f"{child.desc} [{state.udp_info_temp['IP']}]"
|
|
|
|
desc = f"{child.desc} [{state.udp_info_temp['IP']}]"
|
|
|
|
elif child.action == "TEXT_UDP_PORT" and state.udp_info_temp["Port"]:
|
|
|
|
elif child.action == "TEXT_UDP_PORT" and state.udp_info_temp["Port"]:
|
|
|
|
desc = f"{child.desc} [{state.udp_info_temp['Port']}]"
|
|
|
|
desc = f"{child.desc} [{state.udp_info_temp['Port']}]"
|
|
|
|
|
|
|
|
elif child.action == "SET_SERIAL_COMM" and state.serial_info_temp["CommunicationType"]:
|
|
|
|
|
|
|
|
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']}]"
|
|
|
|
|
|
|
|
|
|
|
|
line = f"{marker}{child.name:15s} – {desc}"
|
|
|
|
line = f"{marker}{child.name:15s} – {desc}"
|
|
|
|
attr = curses.A_REVERSE if i == current_idx else curses.A_NORMAL
|
|
|
|
attr = curses.A_REVERSE if i == current_idx else curses.A_NORMAL
|
|
|
|
@ -397,30 +517,34 @@ class ControlPanel:
|
|
|
|
# 操作說明
|
|
|
|
# 操作說明
|
|
|
|
# help_line = start_line + len(current_menu.children) + 2
|
|
|
|
# help_line = start_line + len(current_menu.children) + 2
|
|
|
|
help_line = height - 2
|
|
|
|
help_line = height - 2
|
|
|
|
stdscr.addstr(help_line, 2, "操作: ↑↓選擇 Enter確認 ←返回上層 →進入下層 q退出", curses.A_DIM)
|
|
|
|
stdscr.addstr(help_line, 2, "操作: ↑↓選擇 Enter確認 ←返回上層 →進入下層", curses.A_DIM)
|
|
|
|
|
|
|
|
|
|
|
|
stdscr.refresh()
|
|
|
|
stdscr.refresh()
|
|
|
|
|
|
|
|
|
|
|
|
# 若進入 TERMINATION 狀態,畫面可以刷新 但是不能操作
|
|
|
|
# 若進入 TERMINATION 狀態,畫面可以刷新 但是不能操作
|
|
|
|
# 驗證 bridge 跟 manager 狀態 兩者都停止後 就進入 STOPPED 狀態並跳出迴圈
|
|
|
|
# 驗證 其他附屬模組的狀態都停止後 就進入 STOPPED 狀態並跳出迴圈
|
|
|
|
# 超過幾秒沒有反應就強制關閉
|
|
|
|
# 超過幾秒沒有反應就強制關閉
|
|
|
|
if state.panel_status == "Terminating":
|
|
|
|
if state.panel_status == "Terminating":
|
|
|
|
if time.time() - state.termination_start_time > 3:
|
|
|
|
if time.time() - state.termination_start_time > 7: # 其他組件設定5秒 這邊給多一點
|
|
|
|
logger.warning("Control Panel forced shutdown after timeout.")
|
|
|
|
logger.warning("Control Panel forced shutdown after timeout.")
|
|
|
|
state.intoSTOPPED()
|
|
|
|
state.intoSTOPPED()
|
|
|
|
stop_evt.set()
|
|
|
|
# stop_evt.set()
|
|
|
|
continue
|
|
|
|
# continue
|
|
|
|
|
|
|
|
break
|
|
|
|
time.sleep(0.1)
|
|
|
|
time.sleep(0.1)
|
|
|
|
if state.mavlink_bridge_state == "Stopped" and state.object_manager_state == "Stopped":
|
|
|
|
if (state.mavlink_bridge_state == "Stopped" and
|
|
|
|
|
|
|
|
state.object_manager_state == "Stopped" and
|
|
|
|
|
|
|
|
state.serial_manager_state == "Stopped"):
|
|
|
|
state.intoSTOPPED()
|
|
|
|
state.intoSTOPPED()
|
|
|
|
stop_evt.set()
|
|
|
|
# stop_evt.set()
|
|
|
|
|
|
|
|
break
|
|
|
|
continue
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# 設定短暫的 timeout,讓執行緒能夠響應 stop_evt
|
|
|
|
# 設定短暫的 timeout,讓執行緒能夠響應 stop_evt
|
|
|
|
stdscr.timeout(100) # 100ms timeout
|
|
|
|
stdscr.timeout(100)
|
|
|
|
ch = stdscr.getch()
|
|
|
|
ch = stdscr.getch()
|
|
|
|
|
|
|
|
|
|
|
|
if ch == -1: # timeout,繼續檢查 stop_evt
|
|
|
|
if ch == -1: # 沒有操作
|
|
|
|
continue
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# 處理按鍵
|
|
|
|
# 處理按鍵
|
|
|
|
@ -431,8 +555,12 @@ class ControlPanel:
|
|
|
|
idx_stack[-1] = (current_idx + 1) % len(current_menu.children)
|
|
|
|
idx_stack[-1] = (current_idx + 1) % len(current_menu.children)
|
|
|
|
|
|
|
|
|
|
|
|
elif ch == (ord('O')):
|
|
|
|
elif ch == (ord('O')):
|
|
|
|
# 直接進入工程模式
|
|
|
|
# 進入工程模式
|
|
|
|
state.intoENGINEER()
|
|
|
|
state.intoENGINEER()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
elif ch == (ord('o')):
|
|
|
|
|
|
|
|
# 離開工程模式
|
|
|
|
|
|
|
|
state.intoSTART()
|
|
|
|
|
|
|
|
|
|
|
|
elif ch == curses.KEY_LEFT:
|
|
|
|
elif ch == curses.KEY_LEFT:
|
|
|
|
# 返回上層
|
|
|
|
# 返回上層
|
|
|
|
@ -448,8 +576,9 @@ class ControlPanel:
|
|
|
|
idx_stack.append(0)
|
|
|
|
idx_stack.append(0)
|
|
|
|
|
|
|
|
|
|
|
|
elif ch in (ord('q'), 27):
|
|
|
|
elif ch in (ord('q'), 27):
|
|
|
|
state.intoTERMINATION()
|
|
|
|
if state.panel_status == "Engineer":
|
|
|
|
panel_shutdown()
|
|
|
|
state.intoTERMINATION()
|
|
|
|
|
|
|
|
pre_panel_shutdown()
|
|
|
|
|
|
|
|
|
|
|
|
elif ch in (curses.KEY_ENTER, 10, 13):
|
|
|
|
elif ch in (curses.KEY_ENTER, 10, 13):
|
|
|
|
selected = current_menu.children[current_idx]
|
|
|
|
selected = current_menu.children[current_idx]
|
|
|
|
@ -466,7 +595,7 @@ class ControlPanel:
|
|
|
|
|
|
|
|
|
|
|
|
elif selected.action == "QUIT":
|
|
|
|
elif selected.action == "QUIT":
|
|
|
|
state.intoTERMINATION()
|
|
|
|
state.intoTERMINATION()
|
|
|
|
panel_shutdown()
|
|
|
|
pre_panel_shutdown()
|
|
|
|
|
|
|
|
|
|
|
|
elif selected.action == "TEXT_UDP_IP":
|
|
|
|
elif selected.action == "TEXT_UDP_IP":
|
|
|
|
result = ControlPanel.input_dialog(stdscr, "請輸入監聽的 IP 位址: ")
|
|
|
|
result = ControlPanel.input_dialog(stdscr, "請輸入監聽的 IP 位址: ")
|
|
|
|
@ -477,15 +606,15 @@ class ControlPanel:
|
|
|
|
result = ControlPanel.input_dialog(stdscr, "請輸入監聽的 Port: ")
|
|
|
|
result = ControlPanel.input_dialog(stdscr, "請輸入監聽的 Port: ")
|
|
|
|
if result is not None:
|
|
|
|
if result is not None:
|
|
|
|
state.udp_info_temp["Port"] = result
|
|
|
|
state.udp_info_temp["Port"] = result
|
|
|
|
|
|
|
|
|
|
|
|
elif selected.action == "CREATE_UDP_INBOUND":
|
|
|
|
elif selected.action == "CREATE_UDP_INBOUND":
|
|
|
|
cmd_q.put("CREATE_UDP_INBOUND")
|
|
|
|
cmd_q.put("CREATE_UDP_INBOUND")
|
|
|
|
# 確認後回到上兩層
|
|
|
|
# 確認後回到上兩層
|
|
|
|
if len(menu_stack) > 1:
|
|
|
|
if len(menu_stack) > 1:
|
|
|
|
menu_stack.pop()
|
|
|
|
menu_stack.pop()
|
|
|
|
idx_stack.pop()
|
|
|
|
idx_stack.pop()
|
|
|
|
menu_stack.pop()
|
|
|
|
# menu_stack.pop()
|
|
|
|
idx_stack.pop()
|
|
|
|
# idx_stack.pop()
|
|
|
|
|
|
|
|
|
|
|
|
elif selected.action == "CREATE_UDP_OUTBOUND":
|
|
|
|
elif selected.action == "CREATE_UDP_OUTBOUND":
|
|
|
|
cmd_q.put("CREATE_UDP_OUTBOUND")
|
|
|
|
cmd_q.put("CREATE_UDP_OUTBOUND")
|
|
|
|
@ -493,31 +622,73 @@ class ControlPanel:
|
|
|
|
if len(menu_stack) > 1:
|
|
|
|
if len(menu_stack) > 1:
|
|
|
|
menu_stack.pop()
|
|
|
|
menu_stack.pop()
|
|
|
|
idx_stack.pop()
|
|
|
|
idx_stack.pop()
|
|
|
|
|
|
|
|
# menu_stack.pop()
|
|
|
|
|
|
|
|
# idx_stack.pop()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
elif selected.action == "TEXT_BAUD_SERIAL":
|
|
|
|
|
|
|
|
result = ControlPanel.input_dialog(stdscr, "請輸入 Baud Rate (e.g., 9600, 115200): ")
|
|
|
|
|
|
|
|
if result is not None:
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
baud_rate = int(result)
|
|
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
|
|
state.panel_info_msg_list.append(("Invalid Baud Rate input.", time.time()))
|
|
|
|
|
|
|
|
state.serial_info_temp["Baud"] = baud_rate
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
elif selected.action == "SET_SERIAL_COMM_XBEE":
|
|
|
|
|
|
|
|
state.serial_info_temp["CommunicationType"] = "XBee(API-AT)"
|
|
|
|
|
|
|
|
menu_stack.pop()
|
|
|
|
|
|
|
|
idx_stack.pop()
|
|
|
|
|
|
|
|
elif selected.action == "SET_SERIAL_COMM_TELEMETRY":
|
|
|
|
|
|
|
|
state.serial_info_temp["CommunicationType"] = "XBee(AT-AT)"
|
|
|
|
|
|
|
|
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")
|
|
|
|
|
|
|
|
# 確認後回到上兩層
|
|
|
|
|
|
|
|
if len(menu_stack) > 1:
|
|
|
|
menu_stack.pop()
|
|
|
|
menu_stack.pop()
|
|
|
|
idx_stack.pop()
|
|
|
|
idx_stack.pop()
|
|
|
|
|
|
|
|
menu_stack.pop()
|
|
|
|
|
|
|
|
idx_stack.pop()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
elif selected.action == "LIST_SERIAL_RES":
|
|
|
|
|
|
|
|
created_list_menu = self.create_serial_port_menu(state, page=0)
|
|
|
|
|
|
|
|
menu_stack.append(created_list_menu)
|
|
|
|
|
|
|
|
idx_stack.append(0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
elif selected.action == "LIST_SERIAL_LINKS":
|
|
|
|
|
|
|
|
created_list_menu = self.create_linked_serial_menu(state, page=0)
|
|
|
|
|
|
|
|
menu_stack.append(created_list_menu)
|
|
|
|
|
|
|
|
idx_stack.append(0)
|
|
|
|
|
|
|
|
|
|
|
|
elif selected.action == "LIST_MAV_OBJECT":
|
|
|
|
elif selected.action == "LIST_MAV_OBJECT":
|
|
|
|
# 動態生成 mavlink_object 列表選單
|
|
|
|
# 動態生成 mavlink_object 列表選單
|
|
|
|
object_list_menu = self.create_object_list_menu(state, page=0)
|
|
|
|
created_list_menu = self.create_object_list_menu(state, page=0)
|
|
|
|
menu_stack.append(object_list_menu)
|
|
|
|
menu_stack.append(created_list_menu)
|
|
|
|
idx_stack.append(0)
|
|
|
|
idx_stack.append(0)
|
|
|
|
|
|
|
|
|
|
|
|
elif selected.action == "PREV_PAGE":
|
|
|
|
elif selected.action in ("PREV_PAGE", "NEXT_PAGE"):
|
|
|
|
# 上一頁
|
|
|
|
|
|
|
|
if hasattr(selected, 'page'):
|
|
|
|
|
|
|
|
menu_stack.pop()
|
|
|
|
|
|
|
|
idx_stack.pop()
|
|
|
|
|
|
|
|
object_list_menu = self.create_object_list_menu(state, page=selected.page)
|
|
|
|
|
|
|
|
menu_stack.append(object_list_menu)
|
|
|
|
|
|
|
|
idx_stack.append(0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
elif selected.action == "NEXT_PAGE":
|
|
|
|
|
|
|
|
# 下一頁
|
|
|
|
|
|
|
|
if hasattr(selected, 'page'):
|
|
|
|
if hasattr(selected, 'page'):
|
|
|
|
|
|
|
|
current_list_menu = menu_stack[-1]
|
|
|
|
menu_stack.pop()
|
|
|
|
menu_stack.pop()
|
|
|
|
idx_stack.pop()
|
|
|
|
idx_stack.pop()
|
|
|
|
object_list_menu = self.create_object_list_menu(state, page=selected.page)
|
|
|
|
|
|
|
|
menu_stack.append(object_list_menu)
|
|
|
|
# 依據選單種類 重新建立分頁
|
|
|
|
|
|
|
|
if current_list_menu.name == "Serial Port List":
|
|
|
|
|
|
|
|
created_list_menu = self.create_serial_port_menu(state, page=selected.page)
|
|
|
|
|
|
|
|
elif current_list_menu.name == "Object List":
|
|
|
|
|
|
|
|
created_list_menu = self.create_object_list_menu(state, page=selected.page)
|
|
|
|
|
|
|
|
elif current_list_menu.name == "Linked Serial List":
|
|
|
|
|
|
|
|
created_list_menu = self.create_linked_serial_menu(state, page=selected.page)
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
# 不支援的選單類型,回到原本的選單
|
|
|
|
|
|
|
|
menu_stack.append(current_list_menu)
|
|
|
|
|
|
|
|
idx_stack.append(0)
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
menu_stack.append(created_list_menu)
|
|
|
|
idx_stack.append(0)
|
|
|
|
idx_stack.append(0)
|
|
|
|
|
|
|
|
|
|
|
|
elif selected.action == "INSPECT_MAV_OBJECT":
|
|
|
|
elif selected.action == "INSPECT_MAV_OBJECT":
|
|
|
|
@ -537,22 +708,23 @@ class ControlPanel:
|
|
|
|
# 反正刷新列表會出錯 乾脆再退一層 在下一次進入列表時刷新就好
|
|
|
|
# 反正刷新列表會出錯 乾脆再退一層 在下一次進入列表時刷新就好
|
|
|
|
menu_stack.pop()
|
|
|
|
menu_stack.pop()
|
|
|
|
idx_stack.pop()
|
|
|
|
idx_stack.pop()
|
|
|
|
# # 刷新列表頁面
|
|
|
|
|
|
|
|
# if len(menu_stack) > 1:
|
|
|
|
|
|
|
|
# current_page = menu_stack[-1].current_page if hasattr(menu_stack[-1], 'current_page') else 0
|
|
|
|
|
|
|
|
# menu_stack.pop()
|
|
|
|
|
|
|
|
# idx_stack.pop()
|
|
|
|
|
|
|
|
# time.sleep(0.1) # 等待物件被移除
|
|
|
|
|
|
|
|
# object_list_menu = self.create_object_list_menu(state, page=current_page)
|
|
|
|
|
|
|
|
# menu_stack.append(object_list_menu)
|
|
|
|
|
|
|
|
# idx_stack.append(0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
elif selected.action == "MAVOBJ_MAKE_LINK":
|
|
|
|
elif selected.action == "MAVOBJ_MAKE_LINK":
|
|
|
|
# 建立轉發連結
|
|
|
|
# 建立轉發連結
|
|
|
|
if hasattr(selected, 'socket_id'):
|
|
|
|
if hasattr(selected, 'socket_id'):
|
|
|
|
target_id = self.select_target_socket(stdscr, selected.socket_id, state)
|
|
|
|
target_id = self.select_target_socket(stdscr, selected.socket_id, state)
|
|
|
|
if target_id is not None:
|
|
|
|
if target_id is not None:
|
|
|
|
cmd_q.put(("MAVOBJ_MAKE_LINK", selected.socket_id, target_id))
|
|
|
|
# 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)) # 雙向連結
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
elif selected.action == "MAVOBJ_CANCEL_LINK":
|
|
|
|
|
|
|
|
# 取消轉發連結
|
|
|
|
|
|
|
|
if hasattr(selected, 'socket_id'):
|
|
|
|
|
|
|
|
target_id = self.select_target_socket(stdscr, selected.socket_id, state, remove_mode=True)
|
|
|
|
|
|
|
|
if target_id is not None:
|
|
|
|
|
|
|
|
cmd_q.put(("MAVOBJ_REMOVE_TARGET", selected.socket_id, target_id))
|
|
|
|
|
|
|
|
cmd_q.put(("MAVOBJ_REMOVE_TARGET", target_id, selected.socket_id)) # 雙向取消連結
|
|
|
|
|
|
|
|
|
|
|
|
elif selected.action == "MAVOBJ_ADD_TARGET":
|
|
|
|
elif selected.action == "MAVOBJ_ADD_TARGET":
|
|
|
|
# 添加目標端口
|
|
|
|
# 添加目標端口
|
|
|
|
@ -563,16 +735,6 @@ class ControlPanel:
|
|
|
|
target_id = self.select_target_socket(stdscr, selected.socket_id, state)
|
|
|
|
target_id = self.select_target_socket(stdscr, selected.socket_id, state)
|
|
|
|
if target_id is not None:
|
|
|
|
if target_id is not None:
|
|
|
|
cmd_q.put(("MAVOBJ_ADD_TARGET", selected.socket_id, target_id))
|
|
|
|
cmd_q.put(("MAVOBJ_ADD_TARGET", selected.socket_id, target_id))
|
|
|
|
|
|
|
|
|
|
|
|
elif selected.action == "MAVOBJ_REMOVE_TARGET":
|
|
|
|
|
|
|
|
# 移除目標端口
|
|
|
|
|
|
|
|
if state.panel_status != "Engineer":
|
|
|
|
|
|
|
|
state.panel_info_msg_list.append(("Not in Engineer Mode.", time.time()))
|
|
|
|
|
|
|
|
continue # 只有在工程模式下才能操作
|
|
|
|
|
|
|
|
if hasattr(selected, 'socket_id'):
|
|
|
|
|
|
|
|
target_id = self.select_target_socket(stdscr, selected.socket_id, state, remove_mode=True)
|
|
|
|
|
|
|
|
if target_id is not None:
|
|
|
|
|
|
|
|
cmd_q.put(("MAVOBJ_REMOVE_TARGET", selected.socket_id, target_id))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
elif selected.action == "STOP_MANAGER":
|
|
|
|
elif selected.action == "STOP_MANAGER":
|
|
|
|
if state.panel_status != "Engineer":
|
|
|
|
if state.panel_status != "Engineer":
|
|
|
|
@ -585,6 +747,13 @@ class ControlPanel:
|
|
|
|
state.panel_info_msg_list.append(("Not in Engineer Mode.", time.time()))
|
|
|
|
state.panel_info_msg_list.append(("Not in Engineer Mode.", time.time()))
|
|
|
|
continue # 只有在工程模式下才能操作
|
|
|
|
continue # 只有在工程模式下才能操作
|
|
|
|
cmd_q.put("SHUTDOWN_BRIDGE")
|
|
|
|
cmd_q.put("SHUTDOWN_BRIDGE")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
elif selected.action == "STOP_SERIAL_MANAGER":
|
|
|
|
|
|
|
|
if state.panel_status != "Engineer":
|
|
|
|
|
|
|
|
state.panel_info_msg_list.append(("Not in Engineer Mode.", time.time()))
|
|
|
|
|
|
|
|
continue # 只有在工程模式下才能操作
|
|
|
|
|
|
|
|
cmd_q.put("SHUTDOWN_SERIAL_MANAGER")
|
|
|
|
|
|
|
|
|
|
|
|
elif callable(selected.action):
|
|
|
|
elif callable(selected.action):
|
|
|
|
# 執行函式
|
|
|
|
# 執行函式
|
|
|
|
cmd_q.put(selected.action)
|
|
|
|
cmd_q.put(selected.action)
|
|
|
|
@ -594,14 +763,13 @@ class ControlPanel:
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
pass
|
|
|
|
pass
|
|
|
|
finally:
|
|
|
|
finally:
|
|
|
|
stop_evt.set()
|
|
|
|
|
|
|
|
cleanup()
|
|
|
|
cleanup()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Orchestrator:
|
|
|
|
class Orchestrator:
|
|
|
|
def __init__(self, stop_sig):
|
|
|
|
def __init__(self, stop_sig):
|
|
|
|
self.stop_evt = stop_sig
|
|
|
|
self.stop_evt = stop_sig # 外部操作去中斷 "面板" 執行緒的訊號 (內部自己停止的話不需要用這個)
|
|
|
|
|
|
|
|
|
|
|
|
# === 1) 面板部分的準備 ===
|
|
|
|
# === 1) 面板部分的準備 ===
|
|
|
|
self.cmd_q = queue.Queue()
|
|
|
|
self.cmd_q = queue.Queue()
|
|
|
|
@ -615,6 +783,9 @@ class Orchestrator:
|
|
|
|
self.manager = mo.async_io_manager()
|
|
|
|
self.manager = mo.async_io_manager()
|
|
|
|
self.bridge = mo.mavlink_bridge()
|
|
|
|
self.bridge = mo.mavlink_bridge()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# === 3) serial_manager 部分的準備 ===
|
|
|
|
|
|
|
|
self.plumber = sm.serial_manager()
|
|
|
|
|
|
|
|
|
|
|
|
def engageWholeSystem(self):
|
|
|
|
def engageWholeSystem(self):
|
|
|
|
"""啟動整個系統"""
|
|
|
|
"""啟動整個系統"""
|
|
|
|
# === 1) 面板部分的啟動 ===
|
|
|
|
# === 1) 面板部分的啟動 ===
|
|
|
|
@ -625,10 +796,14 @@ class Orchestrator:
|
|
|
|
self.manager.start()
|
|
|
|
self.manager.start()
|
|
|
|
self.bridge.start()
|
|
|
|
self.bridge.start()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# === 3) serial_manager 部分的啟動 ===
|
|
|
|
|
|
|
|
self.plumber.start()
|
|
|
|
|
|
|
|
|
|
|
|
def mainLoop(self):
|
|
|
|
def mainLoop(self):
|
|
|
|
logger.info("Main orchestrator started <-")
|
|
|
|
logger.info("Main orchestrator started <-")
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
while not self.stop_evt.is_set():
|
|
|
|
# while not self.stop_evt.is_set():
|
|
|
|
|
|
|
|
while self.panel_thread.is_alive():
|
|
|
|
|
|
|
|
|
|
|
|
# A. 更新模組狀態
|
|
|
|
# A. 更新模組狀態
|
|
|
|
if self.manager.running:
|
|
|
|
if self.manager.running:
|
|
|
|
@ -644,6 +819,14 @@ class Orchestrator:
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
self.panelState.mavlink_bridge_state = 'Stopped'
|
|
|
|
self.panelState.mavlink_bridge_state = 'Stopped'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if self.plumber.running:
|
|
|
|
|
|
|
|
self.panelState.serial_manager_state = 'Running'
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
self.panelState.serial_manager_state = 'Stopped'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
linked_serial_dict = self.plumber.get_serial_link()
|
|
|
|
|
|
|
|
self.panelState.linked_serial_dict = linked_serial_dict
|
|
|
|
|
|
|
|
|
|
|
|
# 取出面板丟過來的「動作」
|
|
|
|
# 取出面板丟過來的「動作」
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
cmd = self.cmd_q.get_nowait()
|
|
|
|
cmd = self.cmd_q.get_nowait()
|
|
|
|
@ -661,10 +844,6 @@ class Orchestrator:
|
|
|
|
self.remove_target_from_object(s_id, socket_id)
|
|
|
|
self.remove_target_from_object(s_id, socket_id)
|
|
|
|
# 再移除該物件
|
|
|
|
# 再移除該物件
|
|
|
|
self.delete_mavlink_object(socket_id)
|
|
|
|
self.delete_mavlink_object(socket_id)
|
|
|
|
elif action == "MAVOBJ_MAKE_LINK":
|
|
|
|
|
|
|
|
source_id, target_id = cmd[1], cmd[2]
|
|
|
|
|
|
|
|
self.add_target_to_object(source_id, target_id)
|
|
|
|
|
|
|
|
self.add_target_to_object(target_id, source_id) # 雙向連結
|
|
|
|
|
|
|
|
elif action == "MAVOBJ_ADD_TARGET":
|
|
|
|
elif action == "MAVOBJ_ADD_TARGET":
|
|
|
|
source_id, target_id = cmd[1], cmd[2]
|
|
|
|
source_id, target_id = cmd[1], cmd[2]
|
|
|
|
self.add_target_to_object(source_id, target_id)
|
|
|
|
self.add_target_to_object(source_id, target_id)
|
|
|
|
@ -681,6 +860,9 @@ class Orchestrator:
|
|
|
|
self.panelState.socket_info_single["return_msg_types"] = mav_obj.return_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
|
|
|
|
self.panelState.socket_info_single["primary_socket_id"] = mav_obj.primary_socket_id
|
|
|
|
self.panelState.socket_info_single["target_sockets"] = mav_obj.target_sockets
|
|
|
|
self.panelState.socket_info_single["target_sockets"] = mav_obj.target_sockets
|
|
|
|
|
|
|
|
ip_info = mav_obj.mavlink_socket.port.getsockname()
|
|
|
|
|
|
|
|
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 # 標記資訊已準備好
|
|
|
|
self.panelState.socket_info_single["InfoReady"] = True # 標記資訊已準備好
|
|
|
|
|
|
|
|
|
|
|
|
elif cmd == "CREATE_UDP_INBOUND":
|
|
|
|
elif cmd == "CREATE_UDP_INBOUND":
|
|
|
|
@ -689,10 +871,14 @@ class Orchestrator:
|
|
|
|
elif cmd == "CREATE_UDP_OUTBOUND":
|
|
|
|
elif cmd == "CREATE_UDP_OUTBOUND":
|
|
|
|
self.panelState.udp_info_temp["direction"] = "outbound"
|
|
|
|
self.panelState.udp_info_temp["direction"] = "outbound"
|
|
|
|
self.create_udp_object()
|
|
|
|
self.create_udp_object()
|
|
|
|
|
|
|
|
elif cmd == "CREATE_SERIAL_PORT":
|
|
|
|
|
|
|
|
self.create_serial_port_object()
|
|
|
|
elif cmd == "SHUTDOWN_BRIDGE":
|
|
|
|
elif cmd == "SHUTDOWN_BRIDGE":
|
|
|
|
self.bridge.stop()
|
|
|
|
self.bridge.stop()
|
|
|
|
elif cmd == "SHUTDOWN_MANAGER":
|
|
|
|
elif cmd == "SHUTDOWN_MANAGER":
|
|
|
|
self.manager.shutdown()
|
|
|
|
self.manager.shutdown()
|
|
|
|
|
|
|
|
elif cmd == "SHUTDOWN_SERIAL_MANAGER":
|
|
|
|
|
|
|
|
self.plumber.shutdown()
|
|
|
|
except queue.Empty:
|
|
|
|
except queue.Empty:
|
|
|
|
pass
|
|
|
|
pass
|
|
|
|
except Exception as e:
|
|
|
|
except Exception as e:
|
|
|
|
@ -706,17 +892,31 @@ class Orchestrator:
|
|
|
|
except Exception as e:
|
|
|
|
except Exception as e:
|
|
|
|
logger.error(f"Unexpected error in main loop: {e}")
|
|
|
|
logger.error(f"Unexpected error in main loop: {e}")
|
|
|
|
finally:
|
|
|
|
finally:
|
|
|
|
logger.info("Main orchestrator END!")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 關閉 mavlink_bridge (裡面有一個執行緒)
|
|
|
|
# 驗證並確保所有模組都被下達關閉訊號
|
|
|
|
self.bridge.stop()
|
|
|
|
# 若是由面板操作結束系統 這些關閉行為將於 ControlPanel.pre_panel_shutdown() 觸發
|
|
|
|
|
|
|
|
if self.bridge.thread.is_alive():
|
|
|
|
|
|
|
|
if self.bridge.running:
|
|
|
|
|
|
|
|
self.bridge.stop()
|
|
|
|
|
|
|
|
self.bridge.thread.join(timeout=2)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if self.manager.thread.is_alive():
|
|
|
|
|
|
|
|
if self.manager.running:
|
|
|
|
|
|
|
|
self.manager.shutdown()
|
|
|
|
|
|
|
|
self.manager.thread.join(timeout=2)
|
|
|
|
|
|
|
|
|
|
|
|
# 關閉 async_io_manager (裡面有一個執行緒)
|
|
|
|
if self.plumber.thread.is_alive():
|
|
|
|
self.manager.shutdown()
|
|
|
|
if self.plumber.running:
|
|
|
|
|
|
|
|
self.plumber.shutdown()
|
|
|
|
|
|
|
|
self.plumber.thread.join(timeout=2)
|
|
|
|
|
|
|
|
|
|
|
|
# 關閉面板執行緒
|
|
|
|
# 關閉面板執行緒
|
|
|
|
if self.panel_thread.is_alive():
|
|
|
|
if self.panel_thread.is_alive():
|
|
|
|
self.panel_thread.join(timeout=2)
|
|
|
|
self.panel_thread.join(timeout=2)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
logger.info("Main orchestrator END!")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============== 面板動作 - Mavlink Object ===============
|
|
|
|
|
|
|
|
|
|
|
|
def create_udp_object(self):
|
|
|
|
def create_udp_object(self):
|
|
|
|
if self.panelState.udp_info_temp["direction"] == "inbound":
|
|
|
|
if self.panelState.udp_info_temp["direction"] == "inbound":
|
|
|
|
@ -764,6 +964,46 @@ class Orchestrator:
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
self.panelState.panel_info_msg_list.append((f"Fail Removing target {target_id} from socket {source_id}", time.time()))
|
|
|
|
self.panelState.panel_info_msg_list.append((f"Fail Removing target {target_id} from socket {source_id}", time.time()))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============== 面板動作 - Serial Manager ===============
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def create_serial_port_object(self):
|
|
|
|
|
|
|
|
# 獲取可用的 udp port
|
|
|
|
|
|
|
|
udp_port_tmp = find_available_port(19000, 20000)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 定義通訊類型映射表
|
|
|
|
|
|
|
|
COMM_TYPE_MAP = {
|
|
|
|
|
|
|
|
"XBee(API-AT)": sm.SerialReceiverType.XBEEAPI2AT,
|
|
|
|
|
|
|
|
# "XBee(AT-AT)": sm.SerialReceiverType.TELEMETRY, # TODO: 之後再弄
|
|
|
|
|
|
|
|
# 新增區
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 驗證輸入
|
|
|
|
|
|
|
|
comm_type = self.panelState.serial_info_temp['CommunicationType']
|
|
|
|
|
|
|
|
if not comm_type:
|
|
|
|
|
|
|
|
self.panelState.panel_info_msg_list.append(
|
|
|
|
|
|
|
|
("Please select Communication Type first.", time.time())
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 查找對應的通訊類型
|
|
|
|
|
|
|
|
comm_type_tmp = COMM_TYPE_MAP.get(comm_type)
|
|
|
|
|
|
|
|
if comm_type_tmp is None:
|
|
|
|
|
|
|
|
self.panelState.panel_info_msg_list.append(
|
|
|
|
|
|
|
|
(f"Communication type '{comm_type}' not supported yet.", time.time())
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ret = self.plumber.create_serial_link(
|
|
|
|
|
|
|
|
serial_port=self.panelState.serial_info_temp['Port'],
|
|
|
|
|
|
|
|
baudrate=self.panelState.serial_info_temp['Baud'],
|
|
|
|
|
|
|
|
target_port=udp_port_tmp,
|
|
|
|
|
|
|
|
receiver_type=comm_type_tmp,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if not ret:
|
|
|
|
|
|
|
|
self.panelState.panel_info_msg_list.append((f"Failed to create Serial Port object at {self.panelState.serial_info_temp['Port']}.", time.time()))
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
def main():
|
|
|
|
|
|
|
|
|
|
|
|
stop_evt = threading.Event()
|
|
|
|
stop_evt = threading.Event()
|
|
|
|
|